第一章 Activity
1.1 生命周期
1.正常情况下,Activity的生命周期变化:
onCreate:表示Activity正在创建,一般会在这里调用setContentView去加载界面布局资源、初始化Activity所需数据
onRestart:顾名思义,重新启动Activity的时候才会调用这个方法
onStart:表示Activity正在被启动,但尚未出现在前台
onResume:表示Activity已经可见,并出现在前台
onPause:表示Activity正在停止,可做存储数据、停止动画等工作
onStop:表示Activity即将停止
onDestroy:一般在这里做一些资源释放和回收工作
补充:
onStart和onStop是从Activity是否可见来回调的、
onPause和onStop是从Activity是否处于前台来回调的
在新Activity启动之前,栈顶的Activity需要先onPause后,新Activity才能启动(因此不能在onPause做耗时操作)
2.简单描述Activity的启动过程:
启动Activity的请求会由Instrumentation来处理,然后它通过Binder向AMS发请求,AMS内部维护着一个ActivityStack并负责栈内的Activity的状态同步,AMS通过ActivityThread去同步Activity的状态从而完成生命周期方法的调用。
3.异常情况下,生命周期的改变:
异常情况,例如设备配置改变、内存不足时,会调用onSaveInstanceState来保存当前Activity的状态,这个方法的调用发生在onStop方法之前。(与onPause方法的调用没有明显的先后关系,在Android9.0之后的版本,这个方法调用发生在onStop之后)
在Activity重新创建时,系统会调用 onRestoreInstanceState这个方法
补充:
-
不止Activity,每个View都有onSaveInstanceState和onRestoreInstanceState这两个方法
-
onCrate和onRestoreInstanceState 方法都可以用来恢复数据
-
对于内存不足的情况,Activity可分为三种情况,前台Activity,可见非前台Activity和后台Activity,系统会根据优先级来杀Activity
4.保存和恢复View页面结构的过程
首先Activity被意外终止时,Activity会调用onSaveInstanceState去保存数据,然后Activity会委托Window去保存数据,接着Window再委托它上面的顶级容器去保存数据。顶层容器是一个ViewGroup,一般来说它很可能是DecorView。最后顶层容器再去一一通知它的子元素来保存数据,这样整个数据保存过程就完成了
5.避免配置发生改变时,自动重新创建Activity的方法:
如果当某项内容发生改变后,我们不想系统重新创建Activity,可以给Activity指定configChanges属性。比如不想让Activity在屏幕旋转的时候重新创建,就可以给configChanges属性添加orientation这个值。
android:configChanges="orientation"
一般来说,常用的只有locale、orientation和keyboardHidden这三个选项
locale表示切换了系统语言
keyboardHidden表示键盘可访问性发生改变
orientation表示屏幕方向发生了改变
1.2Activity启动模式
1.四种启动模式
standard:标准模式。每次启动Activity都会创建一个实例,不管这个实例是否存在。如果是Activity A启动了Activity B,那么B就会存在于ActivityA的栈中。
singleTop:栈顶复用模式。如果新启动的Activity已经位于任务栈的栈顶,那么此Activity不会被重新创建,同时它的onNewIntent方法会被回调,如果新Activity的实例已存在但不是位于栈顶,那么新Activity仍然会重新重建。
singleTask:栈内复用模式。Activity在一个栈中存在,那么多次启动此Activity都不会重新创建实例,和singleTop一样,系统也会回调其onNewIntent。注意,这个模式,可能会自动帮你创建栈
singleInstan:单实例模式。那就是具有此种模式的Activity只能单独地位于一个任务栈中,且后续请求不会再创建Activity
补充:
- 在使用非Activity的context来启动标准模式的Activity时,需要为待启动Activity指定FLAG_ACTIVITY_NEW_TASK标记。因为非Activity的context没有Activity栈
- 任务栈:任务栈可以理解为所需要的栈,这个栈和参数TaskAffinity相关。TaskAffinity可以翻译为任务相关性。这个参数标识了一个Activity所需要的任务栈的名字,默认情况下,所有Activity所需的任务栈的名字为应用的包名。一般和
- 当TaskAffinity和singleTask启动模式配对使用的时候,它是具有该模式的Activity的目前任务栈的名字,待启动的Activity会运行在名字和TaskAffinity相同的任务栈中。
- 注意:singleTask模式的Activity切换到栈顶会导致在它之上的栈内的Activity出栈。
2.如何给Activity指定启动模式:
有两种方法。第一种是通过AndroidMenifest为Activity指定启动模式,第二种是通过在Intent中设置标志位来为Activity指定启动模式
第二种方法的优先级要高于第一种方法。但是两种方法也都有局限性,比如,第一种方式无法直接为Activity设定FLAG_ACTIVITY_CLEAR_TOP标识,而第二种方式无法为Activity指定singleInstance模式。
3.Activity的Flags
常见的Activity的标志位有:
-
FLAG_ACTIVITY_NEW_TASK 这个标记位的作用是为Activity指定“singleTask”启动模式,其效果和在XML中指定该启动模式相同。
-
FLAG_ACTIVITY_SINGLE_TOP 这个标记位的作用是为Activity指定“singleTop”启动模式,其效果和在XML中指定该启动模式相同。
-
FLAG_ACTIVITY_CLEAR_TOP 具有此标记位的Activity,当它启动时,在同一个任务栈中所有位于它上面的Activity都要出栈。这个模式一般需要和FLAG_ACTIVITY_NEW_TASK配合使用,在这种情况下,被启动Activity的实例如果已经存在,那么系统就会调用它的onNewIntent。如果被启动的Activity采用standard模式启动,那么它连同它之上的Activity都要出栈,系统会创建新的Activity实例并放入栈顶。singleTask启动模式其实默认就具有此标记位的效果。
-
FLAG_ACTIVITY_CLEAR_TASK 把栈里面的Activity全部清空
-
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 具有这个标记的Activity不会出现在历史Activity的列表中,也就不会存在与后台卡片上
4.IntentFilter的匹配规则
在隐式启动Activity的时候,我们需要匹配目标组件中的IntentFilter。只有一个Intent同时匹配action类别、category类别、data类别才算完全匹配,只有完全匹配才能成功启动目标Activity。另外一点,一个Activity中可以有多个intent-filter,一个Intent只要能匹配任何一组intent-filter即可成功启动对应的Activity
-
action的匹配规则:一个过滤规则中可以有多个action,那么只要Intent中的action能够和过滤规则中的任何一个action相同即可匹配成功。action和category都是字符串,区分大小写
-
category的匹配规则:Intent中如果出现了category,不管有几个category,对于每个category来说,它必须是过滤规则中已经定义了的category。对于category,在intent中可以不传入,因为在startActivity和startActivityForResult时会自动加上默认的category
-
data的匹配规则:data的语法较为复杂。data由两部分组成,mimeType和URI,mimeType指媒体类型,而URI就比较复杂了,有下面几个元素:
Scheme:URI的模式,或者说是协议比如http、file、conten
Host:URI的主机名,比如 百度的网址
Port:URI中的端口号,比如80‘
Path、pathPattern和pathPrefix:这三个参数都表述路径有关的信息
**注意:**当我们不指定uri时,默认为content或file
下面给出一个Intent的代码设置示例:
Intent intent = new Intent("com.example.a"); // 传入action
intent.addCategory("com.example.b");
intent.setDataAndType(Uri.parse("file://abc", "text/plain"));
startActivity(intent);
对于Serivce和BroadcastReceiver的匹配规则也是同样,但是这两个组件建议采用显式调用的方法启动。
5.判断是否能找到匹配Intent的方法:
可以采用PackageManager的resolveActivity方法或者Intent的resolveActivity方法,如果它们找不到匹配的Activity就会返回null,我们通过判断返回值就可以规避上述错误了。找到了会返回一个 ResolveInfo类型的对象
另外,PackageManager还提供了queryIntentActivities方法,这个方法和resolveActivity方法不同的是:它不是返回最佳匹配的Activity信息而是返回所有成功匹配的Activity信息
public abstract ResolveInfo resolveActivity(@NonNull Intent intent,
@ResolveInfoFlags int flags);
public abstract List<ResolveInfo> queryIntentActivities(@NonNull Intent intent,
@ResolveInfoFlags int flags);
补充:
第二个参数需要注意,我们要使用MATCH_DEFAULT_ONLY这个标记位,这个标记位的含义是仅仅匹配那些在intent-filter中声明了<"android.intent.category.DEFAULT这个category的Activity。因为没有这个category的Activity是无法隐式启动的
6.主Activity的intent-filter配置
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
这二者共同作用是用来标明这是一个入口Activity(主Activity)并且会出现在系统的应用列表,如果去掉 LAUNCHER这条category,那么应用就不会出现在桌面上。
第二章 IPC机制
2.1 IPC简介
1.什么是IPC?
IPC是Inter-Process Communication的缩写,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。任何一个操作系统都需要有相应的IPC机制,Windows上可以通过剪贴板、管道和邮槽等来进行进程间通信;Linux上可以通过命名管道、共享内容、信号量等来进行进程间通信
在Android中最有特色的进程间通信方式就是Binder了,通过Binder可以轻松地实现进程间通信。除了Binder, Android还支持Socket,通过Socket也可以实现任意两个终端之间的通信,当然同一个设备上的两个进程通过Socket通信自然也是可以的。
2.2Android多进程
1.Android如何开启多进程模式
正常情况下,一个应用就是一个进程。应用开启多进程有两个方法。一是通过给四大组件指定android:process属性,我们可以轻易地开启多进程模式。此外,还能通过JNI在native层去fork一个新的进程。
android:process=":remote"
android:process="com.ryg.chapter_2.remote"
第一个进程名为com.ryg.chapter_2:remote”
第二个进程名为com.ryg.chapter_2.remote”
注意,这两个进程名是不一样的,第一种命名方法附带了进程信息。进程名以“:”开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以“:”开头的进程属于全局进程,其他应用通过ShareUID方式可以和它跑在同一个进程中。
补充:
- android:process的值就是进程的名字。如果不指定,那么会运行在默认进程中,默认进程的名字就是包名
- shell查看进程的方法:adb shell ps或者adb shell ps | grep com.ryg.chapter_2。这当中的com.ryg.chapter_2是包名,第二个命令意思是根据包名,仅查看该应用的进程
- 如果一匹两个应用有相同的ShareUID并且签名相同。在这种情况下,它们可以互相访问对方的私有数据,比如data目录、组件信息等,不管它们是否跑在同一个进程中。当然如果它们跑在同一个进程中,那么除了能共享data目录、组件信息,还可以共享内存数据,或者说它们看起来就像是一个应用的两个部分。
2.多进程问题进阶
Android为每一个应用分配了一个独立的虚拟机,或者说为每个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机中访问同一个类的对象会产生多份副本。
导致的问题:一般来说,使用多进程会造成如下几方面的问题:(1)静态成员和单例模式完全失效。(2)线程同步机制完全失效。(3)SharedPreferences的可靠性下降。(4)Application会多次创建。
前三个问题好理解,这里记录一下第四个问题出现的原因:当一个组件跑在一个新的进程中的时候,由于系统要在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程。因此,相当于系统又把这个应用重新启动了一遍,既然重新启动了,那么自然会创建新的Application。
2.3IPC基础概念
1.序列化-Serializable接口
Serializable是Java所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。使用Serializable来实现序列化相当简单,只需要在类的声明中指定一个类似下面的标识即可自动实现默认的序列化过程。
private static final long serialVersionUID = 8888882828282;
补充:
- 首先静态成员变量属于类不属于对象,所以不会参与序列化过程;其次用transient关键字标记的成员变量不参与序列化过程
- 系统序列化过程,可以通过重写readObject() 和 writeObject()方法来改写
2.serialVersionUID有什么用?
序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中(也可能是其他中介),当反序列化的时候系统会去检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这个时候可以成功反序列化;否则就说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量、类型可能发生了改变,这个时候是无法正常反序列化的。
3.Parcelable接口的使用
在序列化过程中需要实现的功能有序列化、反序列化和内容描述
public class MyData implements Parcelable {
private int data1;
private int data2;
public MyData(){
}
protected MyData(Parcel in) {
readFromParcel(in);
}
// ...........................
public static final Creator<MyData> CREATOR = new Creator<MyData>() {
@Override
public MyData createFromParcel(Parcel in) {
return new MyData(in);
}
@Override
public MyData[] newArray(int size) {
return new MyData[size];
}
};
@Override
public int describeContents() {
return 0;
}
/** 将数据写入到Parcel **/
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(data1);
dest.writeInt(data2);
}
/** 从Parcel中读取数据 **/
public void readFromParcel(Parcel in){
data1 = in.readInt();
data2 = in.readInt();
}
// ..................................
}
writeToParcel 实现了序列化,最终是通过Parcel中的一系列write方法来完成的。
反序列化功能由CREATOR来完成,其内部标明了如何创建序列化对象和数组,并通过Parcel的一系列read方法来完成反序列化过程
内容描述功能由describeContents方法来完成,几乎在所有情况下这个方法都应该返回0,仅当当前对象中存在文件描述符时,此方法返回1。
补充:
系统已经为我们提供了许多实现了Parcelable接口的类,它们都是可以直接序列化的,比如Intent、Bundle、Bitmap等,同时List和Map也可以序列化,前提是它们里面的每个元素都是可序列化的。
4.Parcelable接口和Serializable接口的对比
Serializable是Java中的序列化接口,其使用起来简单但是开销很大,序列化和反序列化过程需要大量I/O操作。而Parcelable是Android中的序列化方式,因此更适合用在Android平台上,它的缺点就是使用起来稍微麻烦点,但是它的效率很高。
5.Binder的简介
-
直观来说,Binder是Android中的一个类,它继承了IBinder接口。
-
从IPC角度来说,Binder是Android中的一种跨进程通信方式,
-
Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder,该通信方式在Linux中没有;
-
从Android Framework角度来说,Binder是ServiceManager连接各种Manager(ActivityManager、WindowManager,等等)和相应ManagerService的桥梁;
-
从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。
6.AIDL使用和简介:(这个部分未深入理解)
AIDL的使用:
1.在main目录下,新建aidl目录
在书中的例子中:
1.跟据IBookManager.aidl系统为我们生成了IBookManager.java这个接口,它继承了IInterface这个接口,
2.首先,它声明了两个方法getBookList和addBook,显然这就是我们在IBookManager.aidl中所声明的方法,同时它还声明了两个整型的id分别用于标识这两个方法,这两个id用于标识在transact过程中客户端所请求的到底是哪个方法
3.声明了一个内部类Stub,这个Stub就是一个Binder类,当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的transact过程,而当两者位于不同进程时,方法调用需要走transact过程,这个逻辑由Stub的内部代理类Proxy来完成
生成的aidl文件中的重要方法:
onTransact
这个方法运行在服务端中的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。该方法的原型为public Boolean onTransact (int code, android.os.Parcel data, android.os.Parcel reply, int flags)。服务端通过code可以确定客户端所请求的目标方法是什么,接着从data中取出目标方法所需的参数(如果目标方法有参数的话),然后执行目标方法。当目标方法执行完毕后,就向reply中写入返回值(如果目标方法有返回值的话),onTransact方法的执行过程就是这样的。
getBookList(调用本地方法的过程)
方法运行在客户端,首先创建该方法所需要的输入型Parcel对象_data、输出型Parcel对象_reply和返回值对象List;然后把该方法的参数信息写入data中(如果有参数的话);接着调用transact方法来发起RPC(远程过程调用)请求,同时当前线程挂起;然后服务端的onTransact方法会被调用,直到RPC过程返回后,当前线程继续执行,并从 _reply中取出RPC过程的返回结果最后返回reply中的数据。
补充:
-
首先,当客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据,所以如果一个远程方法是很耗时的,那么不能在UI线程中发起此远程请求
-
其次,由于服务端的Binder方法运行在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在一个线程中了。
2.4 Android的IPC机制
1.Android的IPC机制简述
比如可以通过在Intent中附加extras来传递信息,或者通过共享文件的方式来共享数据,还可以采用Binder方式来跨进程通信,另外,ContentProvider天生就是支持跨进程访问的,因此我们也可以采用它来进行IPC。此外,通过网络通信也是可以实现数据传递的,所以Socket也可以实现IPC。
2.使用Bundle
四大组件中的三大组件(Activity、Service、Receiver)都是支持在Intent中传递Bundle数据的,由于Bundle实现了Parcelable接口,所以它可以方便地在不同的进程间传输
应用场景:
- 当我们在一个进程中启动了另一个进程的Activity、Service和Receiver,我们就可以在Bundle中附加我们需要传输给远程进程的信息并通过Intent发送出去
3.使用文件共享
共享文件也是一种不错的进程间通信方式,两个进程通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B进程通过读取这个文件来获取数据
补充:
当面对高并发的读/写访问,Sharedpreferences有很大几率会丢失数据,因此,不能在进程间通信中使用SharedPreferences。
4.使用Message进行通信
通过它可以在不同进程中传递Message对象,在Message中放入我们需要传递的数据,就可以轻松地实现数据的进程间传递了。Messenger是一种轻量级的IPC方案,它的底层实现是AIDL。
Message的使用:
服务端:首先,我们需要在服务端创建一个Service来处理客户端的连接请求,同时创建一个Handler并通过它来创建一个Messenger对象,然后在Service的onBind中返回这个Messenger对象底层的Binder即可。
客户端:户端进程中,首先要绑定服务端的Service,绑定成功后用服务端返回的IBinder对象创建一个Messenger,通过这个Messenger就可以向服务端发送消息了,发消息类型为Message对象。如果需要服务端能够回应客户端,就和服务端一样,我们还需要创建一个Handler并创建一个新的Messenger,并把这个Messenger对象通过Message的replyTo参数传递给服务端,服务端通过这个replyTo参数就可以回应客户端。
5.使用AIDL:
AIDL支持的数据类型:
-
基本数据类型(int、long、char、boolean、double等);
-
String和CharSequence;
-
List:只支持ArrayList,里面每个元素都必须能够被AIDL支持;·
-
Map:只支持HashMap,里面的每个元素都必须被AIDL支持,包括key和value;·
-
Parcelable:所有实现了Parcelable接口的对象;·
-
AIDL:所有的AIDL接口本身也可以在AIDL文件中使用。
-
补充:
如果AIDL文件中用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。在上面的IBookManager.aidl中
AIDL中除了基本数据类型,其他类型的参数必须标上方向:in、out或者inout, in表示输入型参数,out表示输出型参数,inout表示输入输出型参数
6.使用ContentProvider
-
ContentProvider的使用相对比较简单,只需要继承ContentProvider类并实现六个抽象方法即可:onCreate、query、update、insert、delete和getType。ContentProvider的进程中,除了onCreate由系统回调并运行在主线程里,其他五个方法均由外界回调并运行在Binder线程池中
-
Android系统所提供的MediaStore功能就是文件类型的ContentProvider,详细实现可以参考MediaStore
-
在注册contentProvider的时候,要写明android:authorities,还可以通过permission等字段,添加权限
-
如果采用的是SQLite并且只有一个SQLiteDatabase的连接,所以可以正确应对多线程的情况。具体原因是SQLiteDatabase内部对数据库的操作是有同步处理的,但是如果通过多个SQLiteDatabase对象来操作数据库就无法保证线程同步
7.使用Socket
两个进程可以通过Socket来实现信息的传输,Socket本身可以支持传输任意字节流
2.5Binder连接池
1.AIDL的使用流程简述:
首先创建一个Service和一个AIDL接口,接着创建一个类继承自AIDL接口中的Stub类并实现Stub中的抽象方法,在Service的onBind方法中返回这个类的对象,然后客户端就可以绑定服务端Service,建立连接后就可以访问远程服务端的方法了。
2.如何避免重复创建服务端Service
在这种模式下,整个工作机制是这样的:每个业务模块创建自己的AIDL接口并实现此接口,这个时候不同业务模块之间是不能有耦合的,所有实现细节我们要单独开来,然后向服务端提供自己的唯一标识和其对应的Binder对象;对于服务端来说,只需要一个Service就可以了,服务端提供一个queryBinder接口,这个接口能够根据业务模块的特征来返回相应的Binder对象给它们