Row 和 Column 分别是 Flutter 中的水平和垂直布局,它们都是 MultiChildRenderObjectWidget,所以它们都是可以渲染多个孩子的控件,而它们是如何渲染孩子的大小和位置的则是有 renderObject 定义的。它们的继承关系图如下:
Row、Column 的布局特性和 MainAxisAlignment(主轴)和 CrossAxisAlignment(交叉轴)有关。主轴是与当前控件方向一致的轴,而交叉轴就是与当前控件方向垂直方向的轴。主轴和交叉轴是相对于的,它们主要用于控制 Row,Column 的子控件排列位置。因此,在 Row 中,MainAxisAlignment 的方向是水平的,默认的起始位置在左边,排列方向是从左至右;而 CrossAxisAlignment 的方向则是垂直的,默认的起始位置在中间,排列方向是从上到下。
相对的,在 Column 中,MainAxisAlignment 的方向是垂直的,默认起始位置在上边,排列方向是从上至下;CrossAxisAlignment 的方向则是水平的,默认的起始位置在中间,排列方向是从左到右。另外,textDirection 和 verticalDirection 可以分别改变水平和垂直方向的起始位置和排列方向。
MainAxisAlignment 的值:
enum MainAxisAlignment {
//将子控件放在主轴的开始位置
start,
//将子控件放在主轴的结束位置
end,
//将子控件放在主轴的中间位置
center,
//将主轴空白位置进行均分,排列子元素,手尾没有空隙
spaceBetween,
//将主轴空白区域均分,使中间各个子控件间距相等,首尾子控件间距为中间子控件间距的一半
spaceAround,
//将主轴空白区域均分,使各个子控件间距相等
spaceEvenly,
}
CrossAxisAlignment 取值:
enum CrossAxisAlignment {
//将子控件放在交叉轴的起始位置
start,
//将子控件放在交叉轴的结束位置
end,
//将子控件放在交叉轴的中间位置
center,
//使子控件填满交叉轴
stretch,
//将子控件放在交叉轴的上,并且与基线相匹配(不常用)
baseline,
}
textDirection 取值:
enum TextDirection {
//从右到左排列
rtl,
//从左到右排列
ltr,
}
VerticalDirection 取值
enum VerticalDirection {
//从上到下
up,
//从下到上
down,
}
为了实例说明的方便,这里专门封装一个 box 控件来说明 Row 和 Column 布局特性。
1 | class Box extends StatelessWidget { |
1 | class _MyHomePageState extends State<MyHomePage> { |
可以看出来,Row 的默认起始位置在左边,排列方向是从左到右。也就是:
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
textDirection: TextDirection.ltr,
verticalDirection: VerticalDirection.down,
1 | class _MyHomePageState extends State<MyHomePage> { |
为了看出 verticalDirection: VerticalDirection.up 的效果,故意把 crossAxisAlignment 设置为了 CrossAxisAlignment.start
1 | class _MyHomePageState extends State<MyHomePage> { |
1 | class _MyHomePageState extends State<MyHomePage> { |
1 | class _MyHomePageState extends State<MyHomePage> { |
1 | class _MyHomePageState extends State<MyHomePage> { |
1 | class _MyHomePageState extends State<MyHomePage> { |
1 | class _MyHomePageState extends State<MyHomePage> { |
1 | class _MyHomePageState extends State<MyHomePage> { |
也就是 Column 的默认情况是:
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
textDirection: TextDirection.ltr,
verticalDirection: VerticalDirection.down,
1 | class _MyHomePageState extends State<MyHomePage> { |
1 | class _MyHomePageState extends State<MyHomePage> { |
1 | class _MyHomePageState extends State<MyHomePage> { |
1 | class _MyHomePageState extends State<MyHomePage> { |
1 | class _MyHomePageState extends State<MyHomePage> { |
1 | class _MyHomePageState extends State<MyHomePage> { |
1 | class _MyHomePageState extends State<MyHomePage> { |
之前从矩阵的角度分析了平移,旋转,缩放,对称的变换,文章在从矩阵来看Android中的一些动画变换。
在android中,图片的对象主要是bitmap,它是由点阵和颜色值组成的。
1、点阵是一个图片宽*图片高的矩阵,矩阵中的每一个元素对应着图片的一个像素。根据点阵可以计算出图片占用的内存大小。
1 | ALPHA_8: 每个像素占用1byte内存 |
1 | 图片占用内存 = 图片高度显示像素 * 图片宽度显示像素 * 每个像素占用的内存大小 |
2、颜色值,是由三原色和透明度决定的,即ARGB,分别对应着四个颜色通道,每个通道用8byte定义,所以一个颜色值就是一个int整型,可以表示256x256x256种颜色值。
在android中和颜色有关的几个常量:ARGB_8888、ARGB_4444、RGB_565。
ARGB_8888,是图片的透明度、R、G、B的每个颜色值占8bit,可以表示256x256x256种颜色,也就是可以表示最多的颜色值,图片质量也是最好的。
ARGB_4444,是图片的透明度、R、G、B的每个颜色值占4bit,可以表示16x16x16种颜色,相对ARGB_8888,它节省了空间,却失去了很多色彩。
RGB_565,它只有R、G、B三个颜色通道,没有透明度通道,可以表达32x64x32种颜色。
四个颜色通道是由一个4x5的变换矩阵控制的。
所以,可以知道,红色通道由第一行控制。
绿色通道由第二行控制。
蓝色通道由第三行控制。
透明度通道由第四行控制。
另外的,第五列是每个通道的偏移量。注意倍数和相加的影响,每个通道最后的值不应该大于256.
在android中,有一个和颜色矩阵相关的android.graphics.Matrix类,该类中有与颜色变换相关的方法。
关于Matrix方法的使用这里就不详细讲了。
实例:
1 | public static Bitmap testBitmap(Bitmap bitmap) |
在上面提到过一张图片占用多大内存的计算,现在我们的手机拍出来的照片占的内存越来越大,所以在开发的过程中,我们就很有必要对图片压缩后再上传。而比较好的压缩方法是JNI压缩。
]]>本篇文章主要讲23种设计模式中的7种结构型设计模式,包括适配器模式,装饰者模式,代理模式,外观模式,桥接模式,组合模式,享元模式。
适配器模式是将一个类的方法接口转换成客户端期望的接口表示。我们可以约定,把客户端期望的接口叫做目标Targetable,被转换的类叫source。适配器模式可以分为:类的适配器模式,对象的适配器,接口的适配器。
已有的被转换的类:
1 | public class SourceClass { |
期望的目标:
1 | public interface Targetable { |
实现目标,进行适配
1 | public class AdapterClass extends SourceClass implements Targetable { |
这样子就将SourceClass按照意愿Targetable适配转换成了AdapterClass,AdapterClass具有了SourceClass的所有的功能,同时也达到了扩展SourceClass。由于类的适配器模式是通过继承实现的,它具有了继承的优缺点。关于缺点,比如通过AdapterClass对象可以调用属于SourceClass而在Targetable接口中没有的方法。
对象的适配器模式,就是将原来类的对象转换为目标接口的对象。对象适配器模式没有继承被转换类,而是持有被转换类的对象。这可以避免继承被带来的副作用。
1 | public class AdapterObjectClass implements Targetable{ |
当一个接口有很多的抽象方法时,当我们写这个接口的实现类,必须实现该接口的全部方法。而有时候接口中并不是所有的抽象方法都是我们必须的,而我们只需要实现其中的某一些方法。为了解决这个问题,我们可以使用接口的适配器模式,引入一个抽象类,这个抽象类提供了接口所有抽象方法的空实现。我们可以继承这个抽象类,并只重写我们需要的方法即可。
比如,在上面我们只要Targetable的method2方法。
1 | public abstract class AdapterInterfaceClass implements Targetable{ |
1 | public class AdapterWraper extends AdapterInterfaceClass { |
装饰者模式的核心思想是,装饰者和被装饰者实现同一个接口,将被装饰者注入装饰者中,可以在装饰者中扩展被装饰者。
1 | public interface Person { |
被装饰者:
1 | public class Man implements Person { |
装饰者:
1 | public class ManDecorator implements Person { |
使用:
1 | Man man = new Man(); |
输出的结果:
1 | There is a man who is eating |
注意区别代理模式和动态代理。
生活中代理的例子。比如如果你要租房子,你可能不知道该地区的房子信息,这时你可以找一个熟悉的人来帮忙,这个帮你的人就是代理;又比如,打官司时,我们可能并不精通法律知识,这时我们可以找一个代理律师来帮我们。等等。。对于,代理的工作可以抽象为一个接口。
1 | public interface WorkInterface { |
一个房东:
1 | public class LandLady implements WorkInterface { |
代理房东的代理类:
1 | public class Proxy implements WorkInterface { |
租客去找代理租房子:
1 | WorkInterface proxy = new Proxy(); |
在医院里的前台接待员就是一个外观模式的体现。由于病人来到医院可能对医院内部和流程并不熟悉,那么可以由熟悉这些的接待员来帮病人来完成这些事情。
部门1
1 | public class ModuleA { |
部门2
1 | public class ModuleB { |
部门3
1 | public class ModuleC { |
外观类:
1 | public class ModuleFacade { |
当我们需要ModuleA,ModuleB, ModuleC的功能时,我们并不直接和他们打交道,也不需要了解部门的功能是如何实现的,而我们只需要去找外观类沟通即可。
外观模式的关键点是整合。
桥接模式,提供一个解耦或者连接抽象化和实现化的一个桥梁,使得二者可以独立变化。
一个接口作为桥,一个抽象类持有桥。桥和抽象类两者可以独立变化。
桥:
1 | public interface Qiao { |
抽象类:
1 | public abstract class FromArea { |
QiaoC.java
1 | public class QiaoC implements Qiao { |
QiaoD.java
1 | public class QiaoD implements Qiao { |
FromAreaA.java
1 | public class FromAreaA extends FromArea { |
FromAreaB.java
1 | public class FromAreaB extends FromArea { |
使用:
1 | FromAreaA fromAreaA = new FromAreaA(); |
从上面可以看出,Qiao和FromArea两者是独立变化的,它们的抽象和实现是分离的。
如果有更多的Qiao和FromArea的实现,只要扩展它们即可。
组合模式,又叫“整体-部分设计模式”。它一般用于实现树形结构。
节点
1 | public class TreeNode { |
整体,建立一棵树:
1 | public class Tree { |
享元模式主要是实现对象的共享。联想数据库的连接池。
1 | public class ConnectionPool { |
本文主要是介绍5种创建型模式中,除了单例模式外的其他创建型模式,包括建造者模式,工厂模式,抽象工厂模式,原型模式。
1、建造者模式,可以将对象的表现和创建(实现)分离开来,根据不同的创建步骤可以产生不同的对象,而对象的创建也是一次性的,创建后的对象是不可变。
2、工厂模式,根据形式的不同,工厂模式可以分为简单工厂方法模式、多工厂方法模式和抽象工厂方法模式。在简单工厂方法模式中只有一个工厂方法,工厂方法根据不同的条件生产不同的对象。多工厂方法模式,为每一个对象都提供一个工厂方法。抽象工厂方法模式,就是在多工厂方法模式的基础上将每个普通工厂方法变为静态工厂方法。另外,可以使用反射来生产产品。
3、抽象工厂模式,它是工厂模式的进一步抽象,它将产品和工厂都抽象为一个接口,每个具体的工厂生产一种具体的产品。
其实它们的特点是,简单工厂方法模式,用一个方法来生产各种产品。多工厂方法模式,一种对象对应一个方法。抽象工厂方法模式,在多工厂方法模式的基础上,进一步的将每个方法都变为静态的,这样子就不需要创建工厂对象了。抽象工厂模式,它将产品和工厂都进一步抽象为一个类或者接口。
一个方法=》多个方法=》多个静态方法=》抽象类(接口)
4、原型模式,就是从一个已经存在的对象(原型)通过克隆和复制创建一个对象。复制分为浅复制和深复制。
浅复制和深复制的区别:
浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。
深复制:将一个对象复制后,不论是基本数据类型还有引用类型,都是重新创建的。简单来说,就是深复制进行了完全彻底的复制,而浅复制不彻底。
1 | public class BuilderTest { |
BuilderTest的构造函数都是私有的,只在Builder中创建它的实例。Builder是一个公开的静态内部类,它的内部成员变量都是私有的,只能调用相对应的setter方法设置,并和BuilderTest中的成员变量是一样的,每个setter方法都是返回Builder自身的,可以链式的调用,当调用create时会将变量传递给BuilderTest和一次性初始化BuilderTest实例。
实际使用:
1 | BuilderTest builderTest = new BuilderTest.Builder() |
规范产品接口:
1 | public interface Product { |
产品类:
ProductA.java
1 | public class ProductA implements Product { |
ProductB.java
1 | public class ProductB implements Product { |
简单工厂类
1 | public class SimpleFactoryTest { |
使用:
1 | SimpleFactoryTest simpleFactoryTest = new SimpleFactoryTest(); |
MultiMethodFactory.java
1 | public class MultiMethodFactory { |
使用:
1 | MultiMethodFactory multiMethodFactory = new MultiMethodFactory(); |
1 | public class StaticMethodFactory { |
使用:
1 | Product productA = StaticMethodFactory.createProductA(); |
产品模版:
1 | public interface Product { |
ProductA.java
1 | public class ProductA implements Product { |
ProductB.java
1 | public class ProductB implements Product { |
工厂模版:
1 | public interface Factory { |
ProductAFactory.java
1 | public class ProductAFactory implements Factory { |
ProductBFactory.java
1 | public class ProductBFactory implements Factory { |
使用:
1 | ProductAFactory productAFactory = new ProductAFactory(); |
原型模式的关键是实现Cloneable接口,并重写Object的clone函数,Cloneable是一个空接口。
1 | public class PrototypeObject implements Cloneable { |
ProductA.java
1 | public class ProductA implements Product { |
使用:
1 | try { |
运行结果:
1 | 改变克隆对象值之前:prototypeObject=>intValue=34strValue=I am the First valuehelloTxt=Helle ! My name is ProductA |
在Object中,
Object#clone
1 | protected Object clone() throws CloneNotSupportedException { |
可以看出,Object的clone方法会调用native函数internalClone;
上面提到原型模式是一种浅复制,将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。而要彻底重新克隆和创建对象,需要使用深复制。
深复制,可以使用对象的序列化来实现。
1 | public class PrototypeObject implements Cloneable, Serializable { |
在这一篇文章中,主要讲一下如何使用Gson解释服务器返回的具有固定格式的数据。
服务器:在本地使用nodejs的express框架建立的简单服务器。它返回了的数据如下:
1 | var testArrayStr = "{\"data\": [{\"cnName\": \"jakewharton\",\"age\": 13,\"IsBoy\": true}, {\"cnName\": \"小红\",\"age\": 24,\"IsBoy\": false}],\"msg\": \"\",\"status\": 200}"; |
我们可以和服务器约定返回的格式模版如下,他们的主要区别是data,可以是对象或者对象的数组形式。
定义解释data为对象的模板:
1 | public class BaseObjectResult<T> { |
定义解释data为数组的模版:
1 | public class BaseArrayResult<T> { |
实体对象:
TestData.java
1 | public class TestData { |
自定义Converter.Factory
1 | public class DecodeConverterFactory extends Converter.Factory { |
CustomResponseBodyConverter.java
1 | public class CustomResponseBodyConverter<T> implements Converter<ResponseBody, T> { |
DecodeRequestBodyConverter.java
1 | public class DecodeRequestBodyConverter<T> implements Converter<T, RequestBody> { |
开始使用:
TestDataApi.java
1 | public interface TestDataApi { |
1 | Retrofit retrofit = new Retrofit.Builder() |
输出的结果是:
04-08 16:04:56.053 31894-31894/com.zhangsunyucong.chanxa.testproject D/hyj: msg= status=200 testData:cnName=小红 age=24 IsBogy=false
1 | Retrofit retrofit = new Retrofit.Builder() |
输出的结果是:
04-08 16:11:44.703 32440-32440/com.zhangsunyucong.chanxa.testproject D/hyj: msg= status=200 testData:cnName=jakewharton age=13 IsBogy=true
04-08 16:11:44.703 32440-32440/com.zhangsunyucong.chanxa.testproject D/hyj: msg= status=200 testData:cnName=小红 age=24 IsBogy=false
关键的代码是:
1 | private ParameterizedType type(final Class raw, final Type... args) { |
当data返回的是对象时:
1 | TestDataApi testDataApi = retrofit.create(TestDataApi.class); |
当返回的data是数组时:
1 | TestDataApi testDataApi = retrofit.create(TestDataApi.class); |
它们返回的结果和第一种方法的返回结果是一样的。
]]>单例模式是23种设计模式中最简单和易用的模式。在某些情境下,如在一个上市公司中,有很多不同级别的员工,但是公司的CEO或者CTO都是只有一个的,CEO或者CTO在公司里就要求是一个单例。单例模式,就是某个类因实际情况的需要,要求在全局的范围内只能有唯一的实例对象,这个对象是常驻内存的,可以重复使用,降低重复创建对象的开销。
下面主要讲解实现单例模式的方法以及它们的优缺点
单例模式的目的,就是要确保在全局范围内某个类的对象是唯一的。所以实现单例模式时,我们至少要考虑两个影响对象创建的因素。
在类第一次加载时,就进行对象的实例化。
1 | public class SingletonDemo { |
在类加载时不进行对象的实例化,只在对象被第一次访问时,才进行对象的实例化。
1 | public class SingletonDemo { |
明显,在多线程的环境下,上面两种实现方式都不是线程安全的。为了实现线程安全,我们首先可以想到使用synchronized关键字。
线程安全的懒汉模式
1 | public class SingletonDemo { |
关于synchronized关键字说明一下,synchronized声明的静态方法,同时只能被一个执行线程访问,但是其他线程可以访问这个对象的非静态方法。即:两个线程可以同时访问一个对象的两个不同的synchronized方法,其中一个是静态方法,一个是非静态方法。
所以,当有多个线程同时访问getInstance静态方法时,多个其他的线程只能等待,这时只有一个线程能够访问getInstance方法,等这个线程释放后其他线程才能访问。这样就会影响速度和效率。
为了提高懒汉模式的速度和效率,可以减小锁的粒度和次数。
双重校验锁法
1 | public class SingletonDemo { |
从上面可以看到,只有在第一次访问时才会锁定和创建类的对象,之后的访问都是直接使用已经创建好的对象,这样减少锁定的次数和范围,以达到提高单例模式的效率。
但是,对象的实例化,并不是一个原子性操作。即第11行代码处,它可以分成下面三个步骤:
1、new SingletonDemo(),为SingletonDemo实例分配内存
2、调用SingletonDemo的构造器,完成初始化工作
3、将mSingletonDemo指向分配的内存空间
由于java处理器可以乱序执行,即无法保证2和3的执行顺序。这对双重校验锁法实现的单例模式有什么影响呢?
当第一个线程访问getInstance方法时,会锁定临界区(第9行到第13行代码),它实例化对象的顺序是1=>3=>2,而在这时如果有第二个线程来访问getInstance方法,由于第一个线程在处理器中执行完了3未执行2,第二个线程会马上得到实例对象,因为第一个线程的3已经执行完即mSingletonDemo已经不为空。当第二个线程使用没有初始化的对象时就会出现问题。
所以,双重校验锁法也不是完美的,在并发环境下依然可能出现问题。
1 | public class SingletonDemo { |
第一次加载SingletonDemo类时并不会实例化INSTANCE,只有在第一次调用getInstance方法时,才会加载SingletonHolder内部类,创建SingletonDemo实例。这种方式不仅确保了线程安全,也保证单例对象的唯一性,同时也实现了单例对象的懒加载。
上面几种实现方式,可能会因为反序列化而创建新的实例,所以必须重写readResolve方法,在readResolve方法中返回已经创建的单例。
使用枚举可以很简单的实现单例模式,这也是Effective Java中提倡的方式。因为枚举本身就是类型安全的,并且枚举实例在任何情况下都是单例。
1 | public enum SingletonEnumDemo { |
枚举单例使用
1 | SingletonEnumDemo.INSTANCE.justDoYourThing(); |
1 | public class SingletonDemo { |
本文基于的retrofit版本是:2.1.0,文章会从retrofit的使用逐渐进入它的源码进行分析。retrofit是一个基于okhttp封装的,具有RESTful风格的HTTP网络请求框架。也就是说,它只负责网络接口配置和调用的封装,实际底层调用的工作还是由okhttp完成的。可以使用它以注解的形式配置请求的地址,请求参数等,还可以添加自定义拦截器、网络拦截器和数据转换器等进行处理和扩展。
从使用开始讲起。
创建retrofit实例
1 | Retrofit retrofit = new Retrofit.Builder() |
从形式上看,可以知道retrofit的创建使用了建造者模式。下面我们进入它的源码。
1 |
|
在Retrofit#Builder类中的成员变量和Retrofit的基本是一样的,这也正是建造者模式的特点。Retrofit的成员变量已经在源码中有注释。在Builder中,主要看Platform。
Platform的子类:
主要有两个子类,对应着retrofit支持的平台:Android和java8的平台。
Platform子类Android的源码如下:
1 | static class Android extends Platform { |
MainThreadExecutor静态内部类中,创建了主线程的handler,用于将请求处理的结果返回给Android主线程。
1 | retrofit.create(CSDNAPIService.class); |
进入create方法源码
Retrofit#create
1 | @SuppressWarnings("unchecked") // Single-interface proxy creation guarded by parameter safety. |
可以看到,上面使用了动态代理。proxy就是反射创建的类对象,method是对象要调用的方法,args是要调用方法的参数。主要分析loadServiceMethod方法。
Retrofit#loadServiceMethod
1 | ServiceMethod<?, ?> loadServiceMethod(Method method) { |
serviceMethodCache是一个缓存,首先从缓存中取数据,没有,线程锁定,调用ServiceMethod的相关代码。
1 | result = new ServiceMethod.Builder<>(this, method).build(); |
主要工作在build方法中,
ServiceMethod#build
1 | public ServiceMethod build() { |
这里主要看第11行,这里主要是解析使用时的各种配置注解。
ServiceMethod#parseMethodAnnotation
1 | private void parseMethodAnnotation(Annotation annotation) { |
从上面可以看到具体的注解。
分析retrofit的同步和异步请求过程:
OkHttpCall#enqueue
1 | @Override public void enqueue(final Callback<T> callback) { |
看第15行,createRawCall()创建了call,call是一个接口,它的子类有RealCall.
1 | public interface Call extends Cloneable |
进入OkHttpCall#createRawCall方法
1 | private okhttp3.Call createRawCall() throws IOException { |
它用参数创建http请求对象,然后创建call,并返回。
分析newCall方法,是callFactory的方法,看Factory
1 | interface Factory { |
它被OkHttpClient实现,所以会到OkHttpClient的newCall
OkHttpClient#newCall
1 | @Override public Call newCall(Request request) { |
现在分析到了RealCall。异步和同步请求调用的方法,都会来到RealCall调用enqueue和execute方法。
看异步的enqueue方法
RealCall#enqueue
1 | @Override public void enqueue(Callback responseCallback) { |
看第7行,AsyncCall是一个Runnable任务,会提交给线程池执行。responseCallback是异步请求结果的回调。
1 | final class AsyncCall extends NamedRunnable |
responseCallback是一个Callback
1 | public interface Callback { |
AsyncCall是一个Runnable任务,那它做的任务是什么呢?
AsyncCall#execute
1 | @Override protected void execute() { |
上面的第4行getResponseWithInterceptorChain返回了请求的结果,其实它里面发生了整个请求的过程。等一下会进去分析它是如何责任链的调用拦截器的和它是怎样根据响应结果调用callback的回调方法的。
AsyncCall#execute是重写NamedRunnable中的execute方法的。
1 | public abstract class NamedRunnable implements Runnable { |
看到,在任务的主要工作run方法中,调用了execute方法。而主要工作做了什么?就是上面的AsyncCall#execute所做的。
回到前面,RealCall#enqueue,进入第7行的Dispatcher的enqueue
Dispatcher#enqueue
1 | synchronized void enqueue(AsyncCall call) { |
runningAsyncCalls保存了异步执行的任务,它的解析如下的英文解释。如果同时执行的任务数没有超过线程池的最大可执行次数,就直接放到线程池中执行。
1 | /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */ |
之前在分析AsyncCall#execute时说过,在第4行的getResponseWithInterceptorrChain中完成了整个网络请求的过程,在过程中责任链式的调用了拦截器和网络拦截器。下面从源码上分析拦截器的调用过程。
AsyncCall#getResponseWithInterceptorChain
1 | Response getResponseWithInterceptorChain() throws IOException { |
可以看到,是先将所有的拦截器放到了interceptors列表中。然后在第14到16行调用了拦截器。这里还有一个主要的分析工作,就是各个系统提供的拦截器在网络请求过程负责做了什么。(本篇文章没讲)
RealInterceptorChain#proceed
1 | public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, |
index刚开始是0,从第一个拦截器开始调用,在RealInterceptorChain#proceed中又创建了下一个RealInterceptorChain,然后执行当前拦截器。
到这里暂停一下,先了解一个拦截器是怎么样定义的?
1 | public class CustomInterceptor implements Interceptor { |
最后那条语句正和AsyncCall#getResponseWithInterceptorChain的最后一句是一样的。这样子就形成了一条链,不断的index + 1即一个一个的按顺序执行完所有的拦截器,而每个拦截器负责自己的责任,这就是责任链模式。
OkHttpCall#execute
1 | @Override public Response<T> execute() throws IOException { |
同异步请求分析中一样,主要都是调用createRawCall方法,这在上面已经分析。然后会到OkHttpClient中调用的execute方法。因为是同步请求,最后在parseResponse中解析了请求返回的结果,回调给Android前端。
OkHttpClient#execute
1 | @Override public Response execute() throws IOException { |
第8行,进入源码是
1 | /** Used by {@code Call#execute} to signal it is in-flight. */ |
将RealCall添加到runningSyncCalls.
1 | /** Running synchronous calls. Includes canceled calls that haven't finished yet. */ |
在第9行,同样是调用了getResponseWithInterceptorChain方法得到请求的结果,这个在异步请求分析中已经讲过。
]]>public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)
主要是从源码角度理解下面三段代码的区别:
mInflater.inflate(R.layout.item, null);
mInflater.inflate(R.layout.item, parent ,false);
mInflater.inflate(R.layout.item, parent ,true);
第一句:直接返回了布局,不正确处理布局参数
第二句:返回布局,并能正确处理了布局参数
第三句:返回布局,能正确处理了布局参数,并将布局添加到parent中
从下面使用代码开始:
1 | LayoutInflater layoutInflater = LayoutInflater.from(HomeAcivity.this); |
最后会都会来到这里:
1 | public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { |
在这一篇文章中,会看HandlerThread和IntentService的源码。为什么一起讲它们呢?在Android中有一条思路,就是从java的线程,到Android中的消息机制,到将java线程和消息机制结合就是HandlerThread,而IntentService就是在HandlerThread基础上再与service结合在一起。
关于Android中的异步的东西,还有AsyncTask,AsyncTask是对java中的线程池的再次封装。进一步,可以联想到Loader.
回到本篇文章主题。
HandlerThread是一个直接继承于Thread的,并在run方法中将线程的Looper进行常规的初始化。而我们要做的就是提供一个Handler,并将Handler与HanderThread的Looper进行关联,通过Handler发送消息和处理消息。
IntentService直接继承于Service,在它的内部封装了HandlerThread的使用过程:提供一个Handler(即:ServiceHandler),并将Handler与HanderThread的Looper进行关联,然后它进一步将启动Service的Intent以消息的形式,通过Handler传给onHandleIntent方法,然后IntentService优雅的结束自己。我们要做的就是在onHandleIntent中做线程要做的事情。
HandlerThread.jva
1 | public class HandlerThread extends Thread { |
HandlerThread的run方法做了主要的工作。创建Looper,在onLooperPrepare做开始循环前的初始化工作,开始Looper循环。
在具体使用HandlerThread时,就是创建Handler,将Handler与HandlerThread的Looper进行关联,然后通过Handler发送消息,处理消息。说明,消息是在HandlerThread线程中处理的。
IntentService.java
1 | public abstract class IntentService extends Service { |
IntentService是一个service子类,在onCreate中初始化了HandlerThread和ServiceHandler,并将ServiceHandler与HandlerThread的Looper进行关联。在onStart中,将启动服务的Intent封装进Message中,然后发给ServiceHandler。ServiceHandler再将Intent传递给onHandleIntent,最后优雅的结束自己。
在具体使用IntentService时,就是在onHandleIntent中正确的处理启动service的Intent即可。说明,onHandleIntent做的事情是在HandlerThread中进行的,因为HandlerThread的Looper与ServiceHandler已经关联,onHandleIntent是在ServiceHandler中被调用的(好啰嗦)。
]]>这篇博客具体的分析过程和android实例。我只是参考和根据自己的理解写的。
在Android中,我们可以从数学的角度来看颜色和动画的变换。这里会从矩阵变换的角度来理解平移,旋转,缩放,对称的变换。
这些变换的完成实际上,是操作一个3X3的矩阵的。而这四种基本变换与操作和这个矩阵有什么样的关系呢?下面会分析。
在Android中,已经为每种变换提供了pre、set和post三种操作方式。
set 用于设置Matrix中的值。
pre 是先乘,因为矩阵的乘法不满足交换律,因此先乘、后乘必须要严格区分。先乘相当于矩阵运算中的右乘。
post 是后乘,因为矩阵的乘法不满足交换律,因此先乘、后乘必须要严格区分。后乘相当于矩阵运算中的左乘。
另外,除平移变换(Translate)外,旋转变换(Rotate)、缩放变换(Scale)和错切变换(Skew)都可以围绕一个中心点来进行,如果不指定,在默认情况下是围绕(0, 0)来进行相应的变换的。
假设坐标系中有A和B两个点,从A平移到B点,它们之间的关系上图所示。
在x和y轴的移动增量分别是:
则易得:
它的矩阵表示为:
由A点顺时针旋转一定角度到B点,如图所示。
由图易知:
由上面四个式子,可得:
矩阵表示,得:
假设旋转点是:
顺时针旋转,结合1、上面的推导结果,可以得到矩阵:
可以化为:
可知,围绕某一点进行旋转变换,可以分成3个步骤,即首先将坐标原点移至该点,然后围绕新的坐标原点进行旋转变换,再然后将坐标原点移回到原先的坐标原点。
A点的x,y坐标分别放大a,b倍。则有一下关系:
用三维矩阵表示为:
1、如果对称轴是x轴,则有:
用三维矩阵表示为:
2、如果对称轴是y轴,则有:
用三维矩阵表示为:
3、如果对称轴是y = x轴,如图
由等腰直角三角形可知:
已知中点在对称轴上,由中点坐标公式,易得:
联合两式子,2式先乘以2,再两式相加和相减,可得:
用三维矩阵表示为:
4、如果对称轴是y = -x轴。
同理,易推导得:
5、如果对称轴是y = kx时。如图
由图易知:
则有:
由直线的斜率公式,可得:
中点坐标在直线上,结合中点坐标公式,易得:
由上面两式,可求得:
用三维矩阵表示为:
k为任意实数,可以取特殊的值,验证前面对称推导的结果。k为1或者-1时,k为0时,k为无穷大时等等。
6、如果对称轴是y = kx + b时
只需要在5的基础上增加两次平移变换即可,即先将坐标原点移动到(0, b),然后做上面的关于y = kx的对称变换,再然后将坐标原点移回到原来的坐标原点即可。用矩阵表示大致是这样的:
马上就到2018年过年了,然后我又刚好有兴致,就来玩玩Android中的简单几何图形的绘制和使用Path类来绘制路径。
在Android中,和我们平时画图一样是有画笔和画布的,Path是画笔,Canvas是画布。与画的样式属性有关,如大小或者颜色等,是由Path来完成的;与画的形状,即画什么东西是由Canva完成的。关于这两个类的各个属性和方法的具体使用,可以浏览爱哥的博客几篇文章。在这里,只是用它们简单的几个函数画一些简单的图形,最后还会给出一个综合一点的demo,主要是为了加强认识绘制时坐标关系。
先贴上我的代码:
布局文件:
1 | <?xml version="1.0" encoding="utf-8"?> |
MyDrawView.java
1 | public class MyDrawView extends View { |
MyDrawView中没有考虑padding的影响。
几何图形中,最简单的就是点了,首先画点。
drawPoint(float x, float y, Paint paint)
drawPoints(float[] pts, Paint paint)
drawPoints(float[] pts, int offset, int count, Paint paint)
x是点的横坐标,y是点的纵坐标。坐标的点也可以放到数组pts中,可见数组的个数一般是偶数个,offset是开始绘制前,数组中忽略的元素个数。count是忽略了offset个点后,开始取count个元素来绘制点。
1 | canvas.drawPoints(mFPts, mPointPaint); |
由点组成线,两点确定一条直线。
drawLine(float startX, float startY, float stopX, float stopY, Paint paint)
drawLines(float[] pts, int offset, int count, Paint paint)
drawLines(float[] pts, Paint paint)
第一个是,直接指定直线的两个点坐标。pts是点的坐标,每两个数组元素确定一个点坐标,每四个元素确定直线的两个点的坐标。
1 | canvas.drawLine(mFPts[0], mFPts[1], mFPts[2], mFPts[2], mPointPaint); |
由线可以组成面。矩形可以是长方形,也可以是正方形。
RectF和Rect的区别是参数的类型不同,RectF的参数类型是float,Rect的参数类型是int。
drawRect(float left, float top, float right, float bottom, Paint paint)
drawRect(float left, float top, float right, float bottom, Paint paint)
drawRect(Rect r, Paint paint)
drawRect( RectF rect, Paint paint)
也就是,可以在RectF或者Rect中指定好顶点坐标再传给drawRect,也可以在drawRect方法中直接指定顶点坐标。
1 | mRectF = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()); |
代码说明,第一行代码是在onSizeChanged重写方法中的,第二行代码是在onDraw方法中的。因为onDraw方法是会不断被调用的,不适合在里面创建对象。
圆角矩形是在矩形的基础上生成的。
drawRoundRect(RectF rect, float rx, float ry, Paint paint)
drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint)
rx是生成圆角的椭圆的X轴半径
ry是生成圆角的椭圆的Y轴半径
1 | canvas.drawRoundRect(mRectF, getMeasuredWidth() / 4, getMeasuredHeight() / 4, mPointPaint); |
圆要指定圆心的坐标和半径的大小。
drawCircle(float cx, float cy, float radius, Paint paint)
cx和cy分别是圆心的横坐标和纵坐标,radius为半径。
1 | canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, |
椭圆是在矩形基础上生成的,以矩形的长为长轴,矩形的宽为短轴。特殊的,当长轴等于短轴时,椭圆就是圆。
drawOval(RectF oval, @NonNull Paint paint)
drawOval(float left, float top, float right, float bottom, Paint paint)
1 | mRectOvalF = new RectF(mPointStrokeWidth, mPointStrokeWidth, |
弧是在椭圆上按一定角度截取的一部分。
drawArc(RectF oval, float startAngle, float sweepAngle, boolean useCenter, Paint paint)
drawArc(float left, float top, float right, float bottom, float startAngle,
float sweepAngle, boolean useCenter, Paint paint)
oval是椭圆基于的矩形顶点的矩阵,或者在方法中直接指定四个顶点,startAngle是截取的起始角度,sweepAngle是弧持续的角度,useCenter是否显示长短半径。
1 | canvas.drawArc(mRectOvalF, 0, 90, true, mPointPaint); |
1 | canvas.drawArc(mRectOvalF, 0, 90, false, mPointPaint); |
在View的绘制过程中,有一个类叫做Path,Path可以帮助我们实现很多自定义形状的路径,特别是配合xfermode属性来使用的时候,可以实现很多效果。
路径开始绘制的点叫起始点坐标,默认是(0,0)。可以使用moveTo将绘制路径的起始点移动到某个位置。moveTo不进行绘制,一般用来移动画笔。
lineTo用来绘制一条直线路径。
1 | mPath.moveTo(getMeasuredWidth()/ 2, getMeasuredHeight() / 2); |
直线路径的起始点是(getMeasuredWidth()/ 2, getMeasuredHeight() / 2),终点是(getMeasuredWidth(), getMeasuredHeight())
quadTo用来画由一个控制点控制的贝塞尔曲线。
1 | mPath.moveTo(mPointStrokeWidth, getMeasuredHeight() / 2); |
起始点是(mPointStrokeWidth, getMeasuredHeight() / 2),控制点是(0, 0),终点是(getMeasuredWidth() / 2, mPointStrokeWidth)
cubicTo用来画由两个控制点控制的贝塞尔曲线。
1 | mPath.moveTo(mPointStrokeWidth, getMeasuredHeight() / 2); |
起始点是(mPointStrokeWidth, getMeasuredHeight() / 2),两个控制点是(0, 0)和(getMeasuredWidth() / 2, mPointStrokeWidth),终点是(getMeasuredWidth(), getMeasuredHeight() / 2)。
arcTo用来画一条圆弧路径。与前面画圆弧一样的,圆弧是截取椭圆的一部分,而椭圆是基于矩形的。
1 | mRectOvalF = new RectF(mPointStrokeWidth, mPointStrokeWidth, |
和刚开始的圆弧参数定义一样,指定基于的矩形的四个顶点,startAngle截取的起始角度,sweepAngle弧持续的角度,useCenter是否显示长短半径。
它们实现的几何路径,可以自己尝试一下。
在开头,mPointPaint首先设置画笔的样式为描边STROKE,后面为了更好看出Path.Op的效果会改为FILL填充。
1 | mRightBottomRectF = new RectF(getMeasuredWidth() / 2, getMeasuredHeight() /2, getMeasuredWidth() - mPointStrokeWidth, getMeasuredHeight() - mPointStrokeWidth); |
1 | mPath.addCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, |
1 | mPath.addCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, |
1 | mPath.addCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, |
1 | mPath.addCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, |
(一)例子一
1 | public class MyDrawView extends View { |
效果图:
(二)例子二
1 | public class MyDrawView extends View { |
关于画笔和画布的使用,到这里是未完的,其他的效果,以后有时间再补充。谢谢大家的观看。
]]>这篇文章主要讲两个内容,一是,初步认识Promise,二是,Async模块和Async/Await的使用
Promise表示一个异步操作的最终结果。一个Promise对象有一个then方法,then方法中返回一个Promise。
- promise是一个包含了兼容promise规范then方法的对象或函数,
Promise有三种状态:pending, fulfilled 或 rejected。pending是等待执行状态,fulfilled是成功执行状态,rejected是失败执行状态。
Promise只能从pending到fulfilled或者从pending到rejected状态,当状态发生改变时,promise.then(onFulfilled, onRejected)方法将被调用。Promise可以使用resolve或者reject将value或者reason作为下一个Promise的第一个回调参数。
来个简单的Promise基本用法:
1 | var promise = new Promise(function(resolve, reject){ |
上面的代码只是表示Promise用法的流程.
使用Promise/A+规范实现以下几个功能
- 上一步的结果可以作为下一步的参数
Promise的具体知识,可以参考这里
下面介绍Async模块和ES7的Async/Await的使用
Async模块的github地址:https://github.com/caolan/async/
配置好node的环境后(具体过程,自己百度),安装Async模块
npm install –save async
Async模块提供了很多关于集合,流程控制,工具方法,这里只体验几个常见的流程控制方法:series,parallel,waterfall,auto。其他方法的用法,可以查看官方文档:文档地址
series(tasks, callback)
tasks可以是数组或者对象
series是串行执行tasks中的任务,如果有一个任务执行返回了错误信息,则不再继续执行后面未执行的任务,并将结果以数组或者对象的形式传给callback。具体结果的格式由你定义tasks时使用的是数组还是对象。
1 | async.series([ |
运行结果:
one
two
three
null
[ ‘one’, ‘two’, ‘three’ ]
当tasks是对象:
1 | async.series({ |
运行结果:
one
two
three
{ one: ‘one’, two: ‘two’, three: ‘three’ }
上面代码中,从上到下的函数开始执行时间是逐渐减小的,而运行结果的输出顺序是one,two,three,说明series是串行执行任务的。
将第二个任务的代码改为以下的样子:
1 | function (callback) { |
运行的结果为:
one
two
errMsg
[ ‘one’, ‘two’ ]
可以看到,当第二个任务返回了错误信息,则不会再继续执行后面未执行的任务
parallel(tasks, callback)
tasks可以是一个数组或者对象
parallel是并行执行多个任务,如果有一个任务执行返回了一个错误信息,则不再继续执行后面未执行的任务,并将结果以数组或者对象的形式传给callback。
1 | async.parallel([ |
运行结果为:
three
two
one
null
[ ‘one’, ‘two’, ‘three’ ]
结果中的输出顺序是three,two,one,说明parallel是并行执行任务的。
同样,将第二个任务的代码改为:(数组定义tasks)
1 | function (callback) { |
运行的结果为:
three
two
errMsg
[ <1 empty item>, ‘two’, ‘three’ ]
one
将第二个任务代码改为:(数组定义tasks)
1 | function (callback) { |
将第三个任务代码改为:(数组定义tasks)
1 | function (callback) { |
也就是,第三个的开始执行时间改成和出现错误信息的第二个任务的时间一样。
运行的结果为:
two
errMsg
[ <1 empty item>, ‘two’ ]
three
one
从结果中可以看出,当前面执行的未完成的任务会占一个位置,而后面未完成的任务不会占数组的位置。
parallelLimit(tasks, limit, callback)
parallelLimit和parallel差不多,区别是它可以指定同时并行执行任务的最大数量。
1 | async.parallelLimit({ |
运行的结果为:
one
two
errMsg
{ one: ‘one’, two: ‘two’ }
three
如果是tasks是数组时,运行的结果是:
two
errMsg
[ <1 empty item>, ‘two’ ]
one
由于同时并行执行任务的最大数量是2,由于第二个任务产生错误信息,第三个任务还没开始执行。另外如果要取最后回调结果中的值,对象定义tasks可能会更好。
waterfall(tasks, callback)
tasks只能是数组类型
waterfall会串行执行tasks中的任务,前一个任务的结果可以作为下一个任务的参数。
1 | async.waterfall([ |
运行的结果是:
one
two参数:arg1是onearg2是two
three参数:arg3是three
done
输出的结果的顺序是one,two, three,是串行执行的。前一个任务的结果可以作为下一个任务的参数。
注意一下,代码中控制台输出的one,two,three代码是移到了定时器的外面。
auto(tasks, concurrencyopt, callback)
auto可以串行和并行执行任务,可以定义任务之间的依赖关系。没有依赖关系的任务会尽可能快的开始并行执行,串行是由于任务的依赖关系而实现的。concurrencyopt指定的是并行执行任务的最大数量。tasks只能是对象类型。
1 | async.auto({ |
运行的结果是:
task2
task1
task3 {“task2”:”data2”,”task1”:[“data”,”data1”]}
task3
task4 {“task2”:”data2”,”task1”:[“data”,”data1”],”task3”:”data3”}
task4
err = null
results = { task2: ‘data2’,
task1: [ ‘data’, ‘data1’ ],
task3: ‘data3’,
task4: { task2: ‘data2’, task4: ‘data4’ } }
task1和task2是不依赖于任何其他任务的,它们会尽可能的开始,而且由于它们是并行执行的,task2的开始时间较短,所以task2比task1先开始。task3依赖于task1和task2,所以task3等到task1和task2执行完毕后再执行。task4依赖task3,所以task4要等到task3执行完毕后再执行。
主要看它们的用法
1 | var task1 = function () { |
连续点击运行四次,运行的结果为:
start
start
start
start
result
end
result
end
result
end
result
end
async代表是一个async函数,await只能用在async函数中,await等待一个Promise的返回,直到Promise返回了才会继续执行await后面的代码。这里的Promise利用setTimeout模拟异步任务。
从输出结果中可以看出,每次执行都是到await时,就停止等待Promise的返回,后再继续执行await后面的代码。
]]>Android中的消息机制是指线程之间的通信机制。我们都知道,如果我们在UI主线程中做耗时的操作而无法及时处理时,程序会弹出ANR全名Application Not Responding, 也就是”应用无响应”的对话框。
首先来一张图,从整体上来看一下android消息机制。
Handler:用于发送消息和处理消息
MessageQueue: 一个先进先出的消息队列
Looper:循环者,它不断的循环的遍历查询消息队列
Looper中会创建一个消息队列,并进入消息循环,不断的从消息队列中取出消息,然后分发消息给对应的消息处理函数,如果消息队列为空,它会进入阻塞等待,直到有新的消息到来,然后被唤醒。
1 | private static void prepare(boolean quitAllowed) { |
这就是Looper的创建函数,它创建了一个Looper实例并放到ThreadLocal中。
ThreadLocal是一个线程共享和线程安全的,ThreadLocal变量在不同的线程中有不同的副本。
这里,首先检查线程是否有Looper,如果已经有,就报”Only one Looper may be created per thread”异常。也就是说一个线程只能有一个Looper,不能重复创建。
进入Looper的构造函数
1 | private Looper(boolean quitAllowed) { |
Looper的构造函数中主要是创建了一个消息队列,和赋值当前线程变量。
开启消息循环
1 | public static void loop() { |
Looper#loop方法的工作,在代码中已经进行注释说明。
Looper#loop中会将消息分发给对应的handler处理。
1 | msg.target.dispatchMessage(msg); |
现在我们进入handler。
1 | Handler handler = new Handler(Looper.myLooper()); |
首先看Handler的构造函数,可以知道Handler是怎么和Looper取得关联的。
1 | public Handler(Looper looper, Callback callback, boolean async) |
主要为Handler的四个变量赋值,其中确定了Handler是和哪一个Looper关联,和Handler发送消息到对应的哪个消息队列。可以知道,一个Handler只有一个Looper和对应的MessageQueue。而不同的Handler可以共享同一个Looper和MessageQueue,这就看你在初始化Handler时与哪个Looper关联了。
Handler无参数的构造函数是和哪个Looper关联呢?
1 | public Handler() { |
Handler无参数的构造函数仍然主要是为那四个变量赋值。它会首先取出当前线程的消息循环者,如果线程没有循环者,会报一个异常。
发送消息到循环队列
1 | public final boolean sendMessage(Message msg) |
sendMessage中会调用sendMessageDelayed,sendMessageDelayed再调用sendMessageAtTime,最后会调用enqueueMessage将消息入队。post开头的方法是调用相应send开头的方法的。
进入Handler#enqueueMessage
1 | private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { |
在分析Looper#loop时,其中有将消息的分发给相应的Handler处理的逻辑,而正是在第2行代码时,它们取得联系的。然后将消息放入Handler关联的Looper中的消息队列。
在MessageQueue#enqueueMessage中,消息入队时,如果消息队列是阻塞休眠状态,会唤醒消息队列。
1 | if (p == null || when == 0 || when < p.when) { |
在Looper#loop中,会将消息分发给对应的Handler处理函数dispatchMessage处理
1 | msg.target.dispatchMessage(msg); |
进入Handler#dispatchMessage
1 | public void dispatchMessage(Message msg) { |
java.lang.Callback
1 | public interface Runnable { |
Handler#Callback
1 | public interface Callback { |
优先调用Message的callback接口,如果Handler有Callback,调用Callback,否则会调用handleMessage方法。
Handler#handleMessage
1 | public void handleMessage(Message msg) { |
这是一个空方法,具体的消息逻辑由我们自己定义。
到此,这个流程已经解释完毕
在非UI线程中只要找好时机也是可以更新UI的。这个会在源码再分析。
]]>在这篇文章中,将会基于android 26源码上分析Activity从启动到显示到屏幕和Decorview添加到Window中的过程。另外在本文中,省略了很多内容,目的只是从源码中找到一条启动的线索。迟点再补充上流程图。
在应用层开发时,Acitvity跳转会写出下面的代码:
1 | public static void startAtcivity(BaseActivity activity) { |
首先看下activity的继承关系:
第一张图,知道activity是context的子类,第二张图,我们可以知道各种activity的关系。
另外会写一片文章,介绍context。
现在我们进入activity#startActivity
1 | @Override |
接着调用activity#startActivityForResult
1 | public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, |
在1、中,出现了Instrumentation,并调用了execStartActivity方法
进入Instrumentation#execStartActivity
1 | public ActivityResult execStartActivity( |
在1、中,出现了ActivityManager。取到IActivityManager,这里有涉及binder机制,ActivityManager.getService()得到的就是ActivityManagerService,ActivityManagerService实现了IActivityManager.Stub,而ActivityManager中有IActivityManager.Stub.asInterface的远程调用。
ActivityManager#getService
1 | public static IActivityManager getService() { |
在2、中,Instrumentation#checkStartActivityResult方法
1 | /** @hide */ |
这是检查在启动activity过程中,可能出现的异常。比如,启动的Acitivity没有在AndroidManifest.xml中注册,会出现代码中1、的异常。
继续,回到在execStartActivity的1、进入ActivityManagerService#startActivity
1 | @Override |
在1、处,出现了IApplicationThread,这里涉及到了binder机制,IApplicationThread的实现是在ActivityThread中的内部类ApplicationThread
ActivityThread#ApplicationThread
1 | private class ApplicationThread extends IApplicationThread.Stub { |
在ApplicationThread中,有很多与Activity,service,Application生命周期有关的方法。
其中scheduleLaunchActivity()应该就是负责Activity创建的。
ActivityManagerService#startActivity的2、处,调用了ActivityStarter的startActivityMayWait方法,它又调用了startActivityLocked方法
1 | 1、 |
在3、中,调用了startActivityUnchecked方法,startActivityUnchecked又调用了ActivityStackSupervisor#resumeFocusedStackTopActivityLocked方法,
ActivityStackSupervisor#resumeFocusedStackTopActivityLocked
1 | boolean resumeFocusedStackTopActivityLocked( |
上面方法中,接着调用ActivityStack的resumeTopActivityUncheckedLocked方法,
ActivityStack#resumeTopActivityUncheckedLocked
1 | boolean resumeTopActivityUncheckedLocked(ActivityRecord prev, ActivityOptions options) { |
接着调用resumeTopActivityInnerLocked方法,在resumeTopActivityInnerLocked中调用ActivityStackSupervisor的startSpecificActivityLocked方法
ActivityStackSupervisor#startSpecificActivityLocked
1 | void startSpecificActivityLocked(ActivityRecord r, |
方法中调用了realStartActivityLocked方法,它里面有下面的代码:
ActivityStackSupervisor#realStartActivityLocked
1 | app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken, |
上面的app.thread就是ApplicationThread,并调用scheduleLaunchActivity。
上面曾经说过ApplicationThread是AcitivityThread的内部类。
进入ApplicationThread的scheduleLaunchActivity方法,它最后会发送一个消息给名为H的handler
1 | sendMessage(H.LAUNCH_ACTIVITY, r); |
H.LAUNCH_ACTIVITY的消息处理逻辑是:
1 | case LAUNCH_ACTIVITY: { |
调用handleLaunchActivity方法。在handleLaunchActivity主要是分别调用performLaunchActivity和handleResumeActivity方法
进入ActivityThread#performLaunchActivity
1 | private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { |
在3、中,会调用Instrumentation#newActivity
1 | public Activity newActivity(ClassLoader cl, String className, |
可以看出是通过类加载器通过反射创建Activity实例的。
在4、中,调用了LoadedApk#makeApplication方法,
1 | app = mActivityThread.mInstrumentation.newApplication( |
makeApplication方法中,和newActivity差不多,也是由Instrumentation的newApplication方法,通过反射创建Application的
5、中调用Acitivty的attach方法,
Acitivty#attach
1 | final void attach(Context context, ActivityThread aThread, |
在该方法中,主要是会创建PhoneWindow。
在6和7中,分别调用了acitivity的生命周期方法,onCreate和onStart。
已经分析了在handleLaunchActivity的performLaunchActivity方法。
现在分析handleLaunchActivity的handleResumeActivity,在handleResumeActivity中会调用acitivity的生命周期方法onResume和将Decorview添加到Window中,并在makeVisible中显示出来。
Activity#handleResumeActivity
1 | final void handleResumeActivity(IBinder token, |
在1、处的performResumeActivity方法中,会调用以下代码:
1 | r.activity.performResume(); |
即调用activity的onResume生命周期方法。
在2、中,设置了Decorview为不可见
在3、中,将Decorview添加到window中,由于2中设置了Decorview为不可见,这时view还看不到。
在4、中,调用Activity的makeVisible方法。
Activity#makeVisible
1 | void makeVisible() { |
上面代码中,将Decorview设置为可见的。
在上面过程中的哪里开始涉及视图绘制。迟点再看。
分析追溯到Zygote中。
本篇文章的排版还有点乱。
优秀文章:http://blog.csdn.net/dd864140130/article/details/60466394
]]>View是Android中界面层控件的一个抽象。从上图中可以看出控件层的继承关系,TextView是View的直接子类,LinearLayout是ViewGroup的直接子类,ViewGroup是View的直接子类,所以TextView和LinearLayout都是一个view,都直接或者间接继承于View。View是所有控件和控件组的子类。
Android手机屏幕的坐标原点(0,0)在屏幕的左上角,向右为x轴的正方向,向下为y轴的正方向。
View的位置由四个顶点确定,且View的位置都是相对于父控件来说的。
View的四个顶点分别对应于View类中的mLeft,mTop,mRight,mBottom,它们在View中都提供了相应的get方法。
View的坐标和宽度为:
左上角坐标为(left, top),右下角坐标为(right,bottom)
宽度 = mRight - mLeft
高度 = mBottom - mTop
相应的,改变View位置(四个顶点)的方法有:
(1)View的layout(int left, int top, int right, int bottom)
(2)水平方向:offsetLeftAndRight,改变mLeft和mRight
——-垂直方法:offsetTopAndBottom,改变mTop和mBottom
关于View的坐标,Android也提供了x,y,translationX,translationY参数,它们也是相对于父控件而言的,与上面的参数有以下关系:
x = mLeft + translationX
y = mTop + translationY
刚开始时,translationX和translationY的默认值都是0。
Android中的scrollX和scrollY,它们和View的边缘以及View内容的边缘有关,产生于scrollTo和scrollBy。
当view内容的上边缘在view的上边缘的上面,scrollY为正值,反之为负值
当view内容的左边缘在view的左边缘的左面,scrollX为正值,反之为负值
MotionEvent相关的坐标是用于表示事件MotionEvent发生的坐标,有getX,getY,getScrollX,getScrollY。
相对于所在控件,有event.getX()和event.getY()
相对于屏幕,有event.getScrollX()和event.getScrollY()
(1)使用View的scrollTo和scrollBy
(2)使用View的布局参数,改变外边距
(3)使用View的layout、offsetLeftAndRight和offsetTopAndBottom方法
(4)使用动画
scrollTo和scrollBy都是View中的方法,scrollTo是view的绝对运动,scrollBy是相对于view当前位置的相对运动。它们滑动的是View的内容。
从源码分析,scrollTo和scrollBy的关系
View#scrollTo
1 | public void scrollTo(int x, int y) { |
View#scrollBy
1 | public void scrollBy(int x, int y) { |
从上面可以看出,scrollTo是直接赋值给mScrollX和mScrollY,并回调onScrollChanged。在scrollBy中是在原来值的基础上相加然后调用scrollTo的,即是相对于view当前位置的。
这种方法就是通过设置View布局参数的Margin值实现的。如:
1 | MarginLayoutParams mlp = (MarginLayoutParams)view.getLayoutParams(); |
向右滑动100个像素,但滑动效果几乎是瞬间完成的。
上面,我们已经知道View的位置是由四个顶点决定的,通过改变它的顶点坐标就可以改变view的位置。View的layout、offsetLeftAndRight和offsetTopAndBottom都可以直接改变view顶点的值。
使用动画是使用位移动画,改变translationX和translationY的值。位移动画要注意View动画和属性动画的区别,view动画并不能改变view的位置。属性动画是在Android3.0之后引入的,为了兼容之前的系统版本,可以使用nineoldandroids库。
(1)使用动画实现滑动
(2)使用scroller
动画本来就是在一定时间内完成的,所以使用动画可以实现弹性的滑动。
scroller实现弹性滑动的原理和动画是差不多的,都是在一定的时间内,从初始值到终值点,不断的改变scrollX和scrollY。
scroller实现滑动的经典代码段
1 | //1、初始化Scroller |
scroller是如何不断的改变scrollX和scrollY呢?
首先看View#startScroll
1 | public void startScroll(int startX, int startY, int dx, int dy, int duration) { |
startScroll其实只是设置滑动的始点值和根据滑动的距离计算终点值、及滑动的总时间等。接着就调用invalidate方法重绘,重绘会调用view的draw方法。在view的draw方法中会调用computeScroll方法。
进入computeScroll方法,首先是看computeScrollOffset源码
Scroller#computeScrollOffset
1 | public boolean computeScrollOffset() { |
computeScrollOffset判断滑动是否结束和计算滑动的值,而在computeScroll中接着会不断获取计算后的滑动值,使用scrollTo进行滑动,然后再调用invalidate方法重绘,即再调用view的draw方法。在view的draw方法中会调用computeScroll方法,如此反复…
]]>首先直接上效果图:
简单分析,首先画一个半径固定为R的颜色填充的圆,再画一些半径从R逐渐增大的圆就形成圆形不断的向外扩大的效果,并且这些圆形的透明度是与半径的相关的。最后在圆形的中心画文本。
自定义属性
attrs.xml
1 | <declare-styleable name="waveView"> |
text是圆形中心的文本,textSize是文本的大小,textColor是文本的颜色,color是WaveView的颜色。
WaveView.java
1 | public class WaveView extends View { |
一共画了三个不断扩散的圆形。用了R的二十分之六的宽度作为第一个固定圆形的半径,三个圆形从固定半径向两边增大至R。三个圆形从里到外的时间相隔33个单位,透明度是从220递减到0。减去2是为了扩散的效果更好看一点。
布局
1 | <?xml version="1.0" encoding="utf-8"?> |
WaveActivity.java中使用
1 | mWaveView =(WaveView) findViewById(R.id.rippleview); |
这篇文章主要讲解view的工作原理中的三大流程,包括测量流程,布局流程,绘制流程。这些都是自定义控件的基础。下面先对三大流程的职责做简要的概述:
测量流程确定了控件的测量的大小;
布局流程确定了控件在父控件中的四个位置的坐标和控件的实际大小;
绘制流程负责控件的绘制并显示在屏幕上。
View的绘制流程是从ViewRoot的performTraversals开始的。在performTraversals经过一堆的逻辑,会分别调用performMeasure,performLayout,performDraw。
然后在view树中,先后调用一下的方法:
performMeasure,measure onMeasure
performLayout,layout, onLayout
performDraw, draw, onDraw, dispatchDraw(绘制子view)
ViewRoot的实现类是ViewRootImpl,在ActivityThread中创建。
MeasureSpec是view的测量规格,是一个int数值。在java中的int是32位的,所以MeasureSpec可以利用32位的一个数值来表示view的大少size和规格mode。在ViewRootImpl.java中提供了MeasureSpec组合和分解的方法。MeasureSpec是ViewRootImpl.java中的一个公开静态内部类,源码如下:
ViewRootImpl#MeasureSpec
1 | public static class MeasureSpec { |
在上面,MODE_SHIFT为什么是30?因为它是使用高2位表示mode,低30为表示size。
MODE_MASK为0x3,二进制表示是
1 | 0000 0000 0000 0000 0000 0000 0000 0011 |
它左移30位后为
1 | 1100 0000 0000 0000 0000 0000 0000 0000 |
由MODE_MASK理解组合makeMeasureSpec中的(size & ~MODE_MASK) | (mode & MODE_MASK)
1 | size & ~MODE_MASK,就是 |
从源码中可以看出,mode有UNSPECIFIED,EXACTLY,AT_MOST。其中UNSPECIFIED等于0,AT_MOST是小于0,EXACTLY等于0。
MeasureSpec是由父容器的约束和布局参数LayoutParams共同决定的。它在DecorView和普通view中创建也是不一样的。DecorView的父容器是Window,所以DecorView的MeasureSpec由窗口大小和布局参数共同决定的。普通view是由父容器的MeasureSpec和布局参数共同决定的。
(1)在DecorView中
ViewRootImpl#measureHierarchy
1 | childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); |
1 |
|
当view设置MATCH_PARENT时,measureSpec的mode是MeasureSpec.EXACTLY,size是windowSize窗口大小。
当view设置WRAP_CONTENT时,measureSpec的mode是MeasureSpec.AT_MOST,size是windowSize窗口大小。
当view设置具体大小时,measureSpec的mode是MeasureSpec.EXACTLY,size是view设置的具体大小。
(2)在普通view中
ViewGroup#measureChildWithMargins
1 | protected void measureChildWithMargins(View child, |
在ViewGroup#getChildMeasureSpec
1 | public static int getChildMeasureSpec(int spec, int padding, int childDimension) { |
在getChildMeasureSpec中列出了在父容器MeasureSpec和view的布局参数下创建MeasureSpec的各种情况。得到MeasureSpec后,在measureChildWithMargins中将它传递给child的measure方法。在measure方法再传给onMeasure方法。这就是onMeasure方法中两个参数的来源。
View#onMeasure
1 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
View#setMeasuredDimension中会调用setMeasuredDimensionRaw方法
View#setMeasuredDimensionRaw
1 | private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { |
到这里就已经确定的view测量的大小。通过getMeasuredWidth()和getMeasuredHeight()就可以得到它们的值。
View#getSuggestedMinimumWidth
1 | protected int getSuggestedMinimumWidth() { |
这里是说,控件是否设置了背景和最小大小。对应于android:background和android:minWidth。
View#getDefaultSize
1 | public static int getDefaultSize(int size, int measureSpec) { |
UNSPECIFIED据说一般是用于表示内部正处于测量的状态。在普通view中我们只要关注AT_MOST和EXACTLY。当view设置match_parent和具体大小时,是EXACTLY,wrap_content时是AT_MOST。为什么会是这样?可以看getChildMeasureSpec方法中各种情况。
所以当我们自定义view时,如果不处理AT_MOST情况,即wrap_content时,控件的大少就是父控件的大小。EXACTLY是可以正常被getDefaultSize处理的。
在ViewGroup中是没有重写onMeasure方法的,因为ViewGroup的大小还与ViewGroup的具体的布局特性有关。如LinearLayout和RelativeLayout的onMeasure不一样的。所以自定义ViewGroup时,要重写onMeasure方法。
但是,ViewGroup提供了测量子view的方法的,measureChildren和measureChildWithMargins,measureChild。
ViewGroup#measureChildren
1 | protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) { |
ViewGroup#measureChild
1 | protected void measureChild(View child, int parentWidthMeasureSpec, |
ViewGroup#measureChildWithMargins
1 | protected void measureChildWithMargins(View child, |
measureChild在measureChildren被循环遍历子view时调用。measureChildren和measureChildWithMargins的区别是,measureChildren是减去父控件的padding,而measureChildWithMargins减去了父控件的padding和view的margin。这直接影响了测量的大小是否包含了padding和margin。也就是view可以设置的最大大小是减去父控件的padding和view的内边距。
综上所述,在view中,就可以确定view的大小,提供了默认的onMeasure方法,但是默认的onMeasure方法不能正确处理AT_MOST(Wrap_content)的情况。在ViewGroup中,因为ViewGroup的具体大小和ViewGroup的布局特性有关,自定义ViewGroup要重写该方法。
View#layout
1 | public void layout(int l, int t, int r, int b) { |
在layout中分别调用了setFrame或者setOpticalFrame和onLayout。
setFrame或者setOpticalFrame中,赋值给mLeft,mTop,mRight,mBottom,确定了view的四个顶点,通过它们的get方法可以得到相应的值。这就确定了view在父控件中的位置坐标和view的宽和高。
View#onLayout是一个空实现。因为view只需要确定自己在父控件的位置即可。onLayout是用于在ViewGroup中确定子view的位置的。而onLayout的实现同样是与具体的ViewGroup的布局特性有关的。需要在自定义ViewGroup实现。
1 | public void draw(Canvas canvas) { |
draw过程就是主要就是,上面源码所说的那几个步骤。
1、如果需要背景,绘制背景
2、onDraw中,绘制自身
3、dispatchDraw中,绘制子view
4、onDrawForeground中绘制装饰
在自定义ViewGroup时,可以在dispatchDraw中遍历子view进行绘制。
布局
1 | <?xml version="1.0" encoding="utf-8"?> |
CustomViewGroup1
1 | public class CustomViewGroup1 extends ViewGroup { |
CustomViewGroup是默认的实现可以
1 | public class CustomView extends View { |
其实这些代码是《Android的MotionEvent事件分发机制》中用的代码基础上加的.
]]>当用户触摸屏幕或者按键等时,形成事件,事件经过linux底层Event节点捕获之后,一直传到android应用层。中间传递的过程不是本文的重点,我也不是很清楚(哈哈哈)。本文的重点是事件在应用层的分发机制。
View树:
在Android中,事件的分发过程就是MotionEvent在view树分发的过程。默认是中从上而下,然后从下而上的传递的,直到有view、viewgroup或者Activity处理事件为止。
为什么要先从上而下?是为了在默认情况下,屏幕上层叠的所有控件都有机会处理事件。这个阶段我们称为事件下发阶段。
为什么要从下而上?是为了在从上而下分发时,事件没有控件处理时,再从下而上冒泡事件,是否有控件愿意处理事件。如果中间没有控件处理,事件就只能由Acitivity处理了。这个阶段我们称为事件的冒泡阶段。
事件序列:从用户手指触摸屏幕开始,经过滑动到手指离开屏幕。这个操作产生了一个dowm事件,一系列move事件,最后一个up事件结束。我们把这一个操作产生的事件称为一个事件序列。
Acitivity中和事件传递有关的函数
事件分发:dispatchTouchEvent
事件处理:onTouchEvent
ViewGrop中和事件传递有关的函数
事件分发:dispatchTouchEvent
事件拦截:onInterceptTouchEvent
事件处理:onTouchEvent
View中和事件传递有关的函数
事件分发:dispatchTouchEvent
事件处理:onTouchEvent
从上面可以看出,ViewGrop中多了事件拦截onInterceptTouchEvent函数,是为了询问自己是否拦截事件(在事件分发中询问),如果没有拦截就传递事件给直接子view,如果拦截就将事件交给自己的事件处理函数处理。View中没有事件拦截函数,因为view是在view树中的叶节点,已经没有子view。
下面是先进行源码分析,然后再验证得出一些结论。代码迟点上传github。
用图表示布局的层级关系:
这里分析事件的分发过程,是从down事件的分发开始,以及分析它在两个阶段的传递过程:下发阶段和冒泡阶段。
(1)在Acitvity中的源码分析:
Activity#dispatchTouchEvent
1 | public boolean dispatchTouchEvent(MotionEvent ev) { |
在第4行,Acivity将事件传递给了Window,Window是一个抽象类。在手机系统中它的实现是PhoneWindow.下面进入PhoneWindow中。
PhoneWindow#superDispatchTouchEvent
1 | @Override |
从上面可以看出,事件已经从Acitivity到PhoneWindow,再传到了DecorView。DecorView是一个继承FrameLayout的ViewGroup,从而事件进入了View树的传递。
重写在Acitvity中的事件传递方法
重写Activity#dispatchTouchEvent:
1、返回false,事件不分发,所有事件在Acitivity的分发函数中就中断(真的不见了),连Acitivity的事件处理函数都到达不了。
2、返回true,所有事件在Acitivity的分发函数中就中断,和false一样
3、返回父函数方法,事件就传给直接子view分发
进一步的,DecorView是一个FrameLayout,也即是一个ViewGruop。
(2)在ViewGruop中的源码分析:
ViewGruop#dispatchTouchEvent
1 | final int action = ev.getAction(); |
在5-11行,是每个新的事件系列开始前,会重置事件相关的状态。这里我们关注两个地方。第一个是第17行的disallowIntercept标志,第二个是第19行调用了事件拦截函数,询问是否拦截事件。
ViewGruop#onInterceptTouchEvent
1 | public boolean onInterceptTouchEvent(MotionEvent ev) { |
onInterceptTouchEvent的代码很简单。
重写在ViewGroup中的事件传递方法
重写ViewGroup#dispatchTouchEvent:
1、返回false,不分发,down事件给父ViewGroup处理,以后的事件全部直接通过父ViewGroup分发函数给父ViewGroup的事件处理函数处理。
2、返回true,则所有的事件都从头来到这里就中断,不见了。
3、返回父函数方法,看下面拦截函数
重写ViewGroup#onInterceptTouchEvent(询问是否拦截):
1、返回true,就调用处理函数,在处理函数中是否消耗down事件
2、返回false,是否是最后一个view?否,down事件就分发给子View;是,就调用一次它的处理函数,进入冒泡阶段(就是一寸事件处理函数调用)
3、返回父函数的方法,和返回false一样
重写ViewGroup的onTouchEvent,当down事件来到中onTouchEvent时,
1、返回true,就消耗down事件,后面全部事件从头分发到处理函数(不用再询问是否拦截)。后面的事件根据是否消耗而是否消失(不消耗就消失),消失的所有事件由Acitivity处理(注意消失的事件也是从头传递到这里再传给Acitivity的)。
2、返回false,将down事件冒泡回去,看谁会处理。
3、返回父函数方法,是默认不消耗。
(3)在View中的源码分析:
View#dispatchTouchEvent
1 | if (onFilterTouchEventForSecurity(event)) { |
这里关注的地方是,第9行和第13行。第9行是当前view如果设置了onTouch事件,并且它返回了true,那它就直接将result设置为true,事件就消耗了,不会再继续传递下去,只到达onTouch。第13行,是事件处理函数。可以看出onTouch是优先于onTouchEvent的。
View#onTouchEvent
1 | .... |
view根据是否可以点击等等一系列判断什么的。这里关注up事件中的第42-53行,有performClick。
View#performClick
1 | public boolean performClick() { |
如果view设置了mOnClickListener,即点击事件,会调用view的点击事件。如果在父view中拦截了up事件,使up事件到达不了这里,会使view的点击事件失效。
可以知道,onTouch是优先于onTouchEvent,onTouchEvent优先于onclick。
当down事件到达了最后一个子view,如果仍然没有view愿意处理它,就调用一次最后一个子view的事件处理函数,是否处理dowm事件,如果不处理,就一直冒泡回去,直到有view的onTouchEvent处理为止。如果都不处理,就只有Acitivity自己处理了。整个事件冒泡阶段就是一串onTouchEvent的回溯过程,自下而上。
]]>那个时候所有人只是把鸣人当做孤儿看待。他长相最多只能算一般,成绩吊车尾,缺家教,没才华,家世没有,血统没有,智商没有,然后还调皮爱恶作剧。
除非傻子才会喜欢那时的鸣人。但是雏田硬是喜欢上了,这一喜欢,不仅坚持了好多年,而且还因为这份喜欢,改变了自己。
雏田她硬是透过了种种外在因素,一眼就看清楚了鸣人的内在:阳光,乐观,有梦想,能努力,坚持到底,不服输。可以看出她是多么强大的主见,不理会别人看法的主见。
雏田总是在默默注视着鸣人,一直支持着他,追赶着他。
你知道当你一个人面临绝望的时候,此时却有一个义无反顾的身影挡在你面前,保护你,是什么感觉吗?
在佩恩来袭,鸣人最为脆弱的时候,挡在他身前保护他。鸣人在十尾的木遁下无处可逃时,她也毅然决然的挡在他身前。
那么一个弱弱小小的女孩子,她的勇敢和坚毅却超乎寻常的强大。
两个人第一次去约会,鸣人却因为没钱请不起高级料理,不知道怎么开口。雏田用白眼看鸣人的钱包,然后主动说去吃一乐拉面。
忍界大战开始中。《宁次之死》,让雏田和鸣人打击沉重,鸣人的意志开始动摇,在鸣人内心的防线即将崩溃时,雏田强忍失去亲人的伤痛,一巴掌打醒鸣人,告诉鸣人是宁次用生命换取他活下去的用意,告诉鸣人要清醒,要秉持自己的信念,不要放弃自己的忍道,鼓励他带领大家继续战斗。
雏田她看似弱不禁风的外表下其实有着很强大的内心和很坚强的意志。
忍界大战胜利后,鸣人成为救世主。
多年之后,鸣人的儿子博人问鸣人:“爸爸,你年轻的时候干了什么伟大的事啊?”
鸣人摸了摸他的头,然后说:“我用了十五年,帮我曾经最喜欢的女生追回了她的丈夫。”儿子又问:“那妈妈呢?”鸣人眼里光线都温柔了:“妈妈坚持爱到了我爱她的那一天。“
为什么想写这一篇文章呢?做android的开发也有两年的时间了,就想把以前学到的一些东西记录下来。于是首先就想在github.com上开一个项目MVPDemo,将一些自己认为比较好的知识点都串联起来。
主要目的:
1、初步认识和使用MVP、dagger2和rxJava2
2、使用对称和非对称加密加强前端与后台的安全机制
3、前后台的socket交互实现
其中3、中的socket实现,我专门建了一个github仓库NodeTestDemo,这个仓库不仅仅实现了前端的普通接口,还提供了一个socket服务。
1、采用了MVP架构,使用dagger2对象依赖注入框架解耦MVP的各个组件
2、界面采用了autolayout进行兼容适配,UI尺寸标准是720*1080.页面效果仿微信。
3、rxjava2、rxlifecycle2,rxbinding2等Rx系列的初级使用
4、与后台服务器接口交互使用了retrofit2,交互的数据格式为json
5、自定义retrofit2的ConverterFactory和Interceptor实现统一加解密交互的数据流程
6、事件总线eventbus3、控件注入框架butterknife、GreenDao3对象关系映射数据库的使用
7、socket的前端简单实现
8、PDF文档库android-pdf-viewer的使用
9、使用jsoup解析csdn网站的html页面获取博主的博客信息
10、接入bugly。可以使用budly跟踪异常奔溃信息和bugly基于tinker的热修复。
11、接入腾讯X5内核浏览器服务代替原生的webview
12、页面路由Arouter的初步使用
13、app端出现异常,在杀死应用前,启动异常页面并允许用户点击重启
14、Cmake的使用。可以将敏感或者需要保密的数据使用jni保护,如第三方开发者平台的appid等
1、后台服务器使用了leancloud和nodejs搭建。nodejs服务器源码
2、android端的数据加密流程:
nodejs使用的是node-rsa模块
(1)生成RSA加解密的公钥和私钥
1 | var rsa = require('node-rsa'); |
将服务器公钥分发给前端,私钥保存好放到服务器端。
(2)后台为一个前端生成一对AppId和AppScrect。前后端各保存一份,建议在android端将它们放到JNI中保护。
AppId用于在前端参与参数签名,AppScrect用于服务器返回数据的AES加密密钥。
(3)在Android端,应用每次启动时生成用于参数AES加密的密钥。这样可以使AES加密密钥是动态变化的。
(4)、将请求参数按照key的自然顺序进行排序,构造源串。然后在源串追加AppId得到签名字符串signString,用AES密钥加密signString,得到签名sign。
1 | /** 按照key的自然顺序进行排序,并返回 */ |
说明:如果要求服务器只允许一定时间范围内的请求,可以在getSignParamsString方法中添加时间戳作为接口签名的一部分,防止重放攻击。
(4)将签名sign和签名的字符串signString进行AES加密,将AES加密密钥用服务器公钥加密,后传给服务器.
1 | RSAUtils.encryptByServerPublicKey(App.getApp().getAESKey()); |
signString为什么在前端生成呢?
为了在服务器重新生成签名字符串时,防止由于前后端开发语言的不同而产生不一致。
(5)服务器解密
1 | function valideReqSign(req) { |
a、取出参数,用服务器RSA私钥解密AES密钥
b、用AES密钥解密签名和签名字符串
c、签名字符串追加分发给前端的AppScrect后,用a、得到的AES加密重新生产签名。
d、对比前端传来的签名和重新生成的签名是否一致。
(5)根据AppId找到对应的AppScrect,用AppScrect对服务器返回的结果进行AES加密。
注意:确保前后端在不同开发语言情况下,AES算法的结果是一样的。
后面会给出我用到的java和nodejs版本的RSA和AES加解密算法源码。
(6)前端从JNI中取出AppScrect对响应结果进行解密即可。
1 | import java.io.ByteArrayOutputStream; |
1 | import java.util.UUID; |
使用”node-rsa”: “^0.4.2”,模块
AES算法:aes.js
进一步改进,关注:
JIN的签名验证和