android开发实例(activity生命周期和启动模式、IPC机制)
android开发艺术探索笔记
参考了Android开发艺术探索超详细笔记
第一章Activity的生命周期和启动模式
1.1 生命周期
1.1.1 正常情况下的生命周期分析
(1)第一次启动:onCreate->onStart->onResume。
(2)当用户打开一个新的activity,或者切换到桌面时:onPause->onStop。
存在一种特殊情况,新的activity使用的是透明主题时:当前activity不会回调onStop
(3)当用户再次回到原Activity时:onRestart->onStart->onResume。
(4)当用户按Back会退时:onPause->onStop->onDestroy。
(5)对于生命周期来说,onCreate和onDestroy是配对的,且只可能被调用一次,onStart和onStop是配对的,onResume和onPause是配对的,onStart是是否可见,onResume是是否在前台。
(6)当另起一个activity时,执行顺序是:MainAActivity:onPause->MainBActivity:onCreate->MainBActivity:onStart->MainBActivity:onResume->MainAActivity:onStop。
onPause不能执行太耗时的操作,所以资源回收和释放应该放在onDestroy中执行。
1.1.2 异常情况下的生命周期分析
(1)横竖屏切换等改变系统配置
异常情况下终止,系统会在onStop之前调用onSaveInstanceState来保存当前activity的状态,当activity被重新创建后,系统会调用onRestoreInstanceState,并且把activity销毁时onSaveInstanceState方法所保存的Bundle对象作为参数同时传递给onRestoreInstanceState和onCreate方法,onRestoreInstanceState调用在onCreate之后。
(2)资源内存不足导致低优先级的activity被杀死
activity优先级从高到低:前台Activity->可见但非前台Activity->后台Activity
如果组件中没有四大组件在运行,则很容易被系统杀死,这种组件不建议放在后台执行,如要放在后台执行,建议放入service中从而保证进程有一定的优先级,不容易被系统杀死。
(3)我们可以在Activity的configChanges属性中指定某个选项,比如屏幕方向发生改变(android:configChanges=“orientation”),如果不指定选项,当配置发生改变之后,就会导致activity重建,常用的有locale(设备的本地位置发生改变,一般指切换了系统语音),orientation(屏幕方向发生改变),keyboardHidden(键盘的可访问性发生改变,比如用户调出了键盘)。
1.2 Activity的启动模式
(1)standard:标准模式,每次启动一个activity都会重新创建一个实例
(2)singleTop:栈顶复用模式,如果新Activity已经在栈顶,那么此Activity不会被重新创建,同时它的onNewIntent方法会被回调,通过此参数可以取出当前请求的信息。
(3)singleTask:栈内复用模式。这是一种单实例模式,只要activity在一个栈中存在,那么多次启动也不会重新创建activity。举例1:目前任务栈S1中的情况为ABC,要创建的是D且需要任务栈S2,由于实例和栈都不存在,所以会创建任务栈S2,再创建D进入S2。举例2:假设D所需的是任务栈S1,由于S1已经存在,则直接创建D进入S1。举例3:假如S1中为ADBC,此时由于存在D,则不会重新创建D,而是把D上面的activity全都移出栈,此时S1内情况为AD。
(4)singleInstance:单实例模式,加强的singleTask模式,创建的activity只能单独在一个任务栈中。
可以通过activity的launchMode属性来设置启动模式
<activity
android:launchMode="singleTask"
/>
或者通过在Intent中设置标志位来为Activity指定启动模式
Intent intent=new Intent();
intent.setClass(MainActivity.class,SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
两种设置方式,优先级第二种高于第一种,当两种同时存在是,以第二种方式为准。第一种方式无法直接为Activity添加FLAGACTIVITYCLEAR_TOP标识,第二种方式无法指定singleInstance模式。
1.2.2 Activity的Flag
FLAG_ACTIVITY_NEW_TASK
为activity指定“singleTask”启动模式
FLAG_ACTIVITY_SINGLE_TOP
为activity指定“singleTop”启动模式
FLAG_ACTIVITY_CLEAR_TOP
具有此标记位的activity,当它启动时,在同一个任务栈,位于它上面的activity都要出栈,一般与FLAG_ACTIVITY_NEW_TASK配合使用
1.2.3 IntentFilter的匹配规则
- 显示调用 明确指定被启动对象的组件信息,包括包名和类名
- 隐式调用 不需要明确指定组件信息,需要Intent能够匹配目标组件中的IntentFilter中所设置的过滤信息。
- 下面的action和category用来表明这是一个入口Activity,并且会出现在系统的应用列表中,二者缺一不可。
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
第二章 IPC机制
2.1 IPC简介
-
IPC即Inter-Process Communication,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
-
线程是CPU调度的最小单元,是一种有限的系统资源。进程一般指一个执行单元,在PC和移动设备上是指一个程序或者应用。进程与线程是包含与被包含的关系。一个进程可以包含多个线程。最简单的情况下一个进程只有一个线程,即主线程( 例如Android的UI线程) 。
-
任何操作系统都需要有相应的IPC机制。如Windows上的剪贴板、管道和邮槽;Linux上命名管道、共享内容、信号量等。Android中最有特色的进程间通信方式就是binder,另外还支持socket。contentProvider是Android底层实现的进程间通信。
-
在Android中,IPC的使用场景大概有以下:
-
有些模块由于特殊原因需要运行在单独的进程中。
-
通过多进程来获取多份内存空间。
-
当前应用需要向其他应用获取数据。
2.2 Android中的多进程模式
2.2.1 开启多进程模式
在android中使用多进程只有一种方法,那就是给四大组件在AndroidMenifest中指定android:process属性。
<activity
android:process=":remote"/>
<activity
android:process="com.ryg.chapter_2.remote"/>
使用 adb shell ps 或 adb shell ps|grep 包名 查看当前所存在的进程信息。
两种进程命名方式的区别
-
“:remote”
“:”的含义是指在当前的进程名前面附加上当前的包名,完整的进程名为“com.example.c2.remote"。这种进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中。 -
“com.example.c2.remote”
这是一种完整的命名方式。这种进程属于全局进程,其他应用可以通过ShareUID方式和它跑在同一个进程中。
2.2.2 多进程模式的运行机制
所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。
一般来说,使用多进程会造成如下几方面的问题
- (1)静态成员和单例模式完全失效
- (2)线程同步机制完全失效
- (3)ShardPreference的可靠性下降
-
(4)Application会多次创建
实现跨进程的方式有很多: -
- Intent传递数据。
-
- 共享文件和SharedPreferences。
-
- 基于Binder的Messenger和AIDL。
-
- Socket等
2.3 IPC基础概念介绍
主要介绍 Serializable、 Parcelable 、 Binder 。Serializable和Parcelable接口可以完成对象的序列化过程,我们通过Intent和Binder传输数据时就需要Parcelabel和Serializable。还有的时候我们需要对象持久化到存储设备上或者通过网络传输到其他客户端,也需要Serializable完成对象持久化。
2.3.1 Serializable接口
Serializable 是Java提供的一个序列化接口( 空接口) ,为对象提供标准的序列化和反序列化操作。只需要一个类去实现 Serializable 接口并声明一个 serialVersionUID 即可实现序列化。
private static final long serialVersionUID = 8711368828010083044L
比如User类是一个实现了Serializable接口的类,他可以被序列化和反序列化
public class User implements Serializable{
private static final long serialVersionUID = 8711368828010083044L
public int userId;
public String userName;
......
}
ObjectOutputStream和ObjectInputStream实现序列化和反序列化
//序列化过程
User user=new User(0,"jack");
ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.writeObject(user);
out.close();
//反序列化过程
ObjectInputStream in=new ObjectInputStream(new FileInputStream("cache.txt"));
User newUser=(User)in,readObject;
in.close();
-
虽然serialVersionUID即使没有设置也可以序列化,但没有这个可能会导致反序列化失败。如果不手动指定 serialVersionUID 的值,反序列化时如果当前类有所改变( 比如增删了某些成员变量) ,那么系统就会重新计算当前类的hash值并更新 serialVersionUID 。这个时候当前类的 serialVersionUID 就和序列化数据中的serialVersionUID 不一致,导致反序列化失败,程序就出现crash。
-
静态成员变量属于类不属于对象,不参与序列化过程,其次 transient 关键字标记的成员变量也不参与序列化过程。
-
通过重写writeObject和readObject方法可以改变系统默认的序列化过程。
2.3.2 Parcelable接口
Parcelable也是一个接口,只要实现这个接口,一个类的对象就可以实现序列化并通过Intent和Binder传递。下面举个典型用法
public class User implements Parcelable {
public int userId;
public String userName;
public boolean isMale;
public User(int userId,String userName,boolean isMale){
this.userId=userId;
this.userName=userName;
this.isMale=isMale;
}
private User(Parcel in) {
userId=in.readInt();
userName=in.readString();
isMale=in.readInt()==1;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(userId);
out.writeString(userName);
out.writeInt(isMale?1:0);
}
public static final Parcelable.Creator<User> CREATOR=new Parcelable.Creator<User>(){
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
}
序列化功能通过writeToParcel方法来完成,反序列化功能由CREATOR来完成。内容描述功能由describeContents方法来完成,几乎所有情况下这个方法都应该返回0,仅当当前对象中存在文件描述符时,此方法返回1。需要注意的是,如果User(Pracel in)中存在另一个可序列化对象,它的反序列化过程需要传递当前线程的上下文类加载器,否则会报无法找到类的错误。
- Serializable 是Java的序列化接口,使用简单但开销大,序列化和反序列化过程需要大量I/O操作。而 Parcelable 是Android中的序列化方式,适合在Android平台使用,效率高但是使用麻烦。 Parcelable 主要在内存序列化上,Parcelable 也可以将对象序列化到存储设备中或者将对象序列化后通过网络传输,但是稍显复杂,推荐使用 Serializable 。
2.3.3 Binder
Binder是Android中的一个类,实现了 IBinder 接口。从IPC角度说,Binder是Andoird的一种跨进程通讯方式,Binder还可以理解为一种虚拟物理设备,它的设备驱动是/dev/binder。从Android Framework角度来说,Binder是 ServiceManager 连接各种Manager( ActivityManager· 、 WindowManager )和相应 ManagerService 的桥梁。从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService时,服务端返回一个包含服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务器端提供的服务或者数据( 包括普通服务和基于AIDL的服务)。
-
Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。
-
图中的Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder驱动进行交互的,从而实现IPC通信方式。其中Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层,开发人员只需自定义实现client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。
-
Binder的工作机制
Binder这一块很多都没看懂,所以就记录这么多先。
2.4 Android中的IPC方式
主要有以下方式:
- Intent中附加extras
- 共享文件
- Binder
- ContentProvider
- Socket
2.4.1 使用Bundle
四大组件中的三大组件( Activity、Service、Receiver) 都支持在Intent中传递 Bundle 数据。
Bundle实现了Parcelable接口,因此可以方便的在不同进程间传输。当我们在一个进程中启动了另一个进程的Activity、Service、Receiver,可以再Bundle中附加我们需要传输给远程进程的消息并通过Intent发送出去。被传输的数据必须能够被序列化。
2.4.2 文件共享
两个进程通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B进程通过读取这个文件来获得数据。通过代码举个例子
//对象写入文件
private void persistToFile(){
new Thread(new Runnable() {
@Override
public void run() {
User user=new User(1,"hello word",false);
File dir=new File(Environment.getExternalStorageDirectory()+"/user.txt");
if (!dir.exists()){
dir.mkdirs();
}
File cachedFile=new File(getCacheDir().getPath());
ObjectOutputStream objectOutputStream=null;
try {
objectOutputStream=new ObjectOutputStream(new FileOutputStream(cachedFile));
objectOutputStream.writeObject(user);
Log.d(TAG,"persist user:"+user);
} catch (IOException e) {
e.printStackTrace();
}finally {
objectOutputStream.close();
}
}
}).start();
}
//从文件中读取对象
private void recoverFromFile(){
new Thread(new Runnable() {
@Override
public void run() {
User user=null;
File cachedFile=new File(getCacheDir().getPath());
if (cachedFile.exists()){
ObjectInputStream objectInputStream=null;
try {
objectInputStream=new ObjectInputStream(new FileInputStream(cachedFile));
user=(User) objectInputStream.readObject();
Log.d(TAG,"recover user:"+user);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}finally {
objectInputStream.close();
}
}
}
}).start();
}
注意一点,只会从文件中恢复之前存储的对象的内容,之所以说内容,是因为反序列化得到的对象的内容虽然和序列化之前的内容一样,但本质上是两个对象。
- 文件并发读写会导致读出的对象可能不是最新的,并发写的话那就更严重了 。所以文件共享方式适合对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读写问题。
- SharedPreferences 底层实现采用XML文件来存储键值对。系统对它的读/写有一定的缓存策略,即在内存中会有一份 SharedPreferences 文件的缓存,因此在多进程模式下,系统对它的读/写变得不可靠,面对高并发读/写时 SharedPreferences 有很大几率丢失数据,因此不建议在IPC中使用 SharedPreferences 。
2.4.3 使用Messenger
Messenger可以在不同进程间传递Message对象。是一种轻量级的IPC方案,底层实现是AIDL。它对AIDL进行了封装,使得我们可以更简便的进行IPC。
Messenger的工作原理如图所示
具体使用时,分为服务端和客户端:
-
服务端:创建一个Service来处理客户端请求,同时创建一个Handler并通过它来创建一个
Messenger,然后再Service的onBind中返回Messenger对象底层的Binder即可。
private final Messenger mMessenger = new Messenger (new xxxHandler()); - 客户端:绑定服务端的Sevice,利用服务端返回的IBinder对象来创建一个Messenger,通过这个Messenger就可以向服务端发送消息了,消息类型是 Message 。如果需要服务端响应,则需要创建一个Handler并通过它来创建一个Messenger( 和服务端一样) ,并通过 Message 的 replyTo 参数传递给服务端。服务端通过Message的 replyTo 参数就可以回应客户端了。
总而言之,就是客户端和服务端 拿到对方的Messenger来发送 Message 。只不过客户端通过bindService 而服务端通过 message.replyTo 来获得对方的Messenger。
Messenger中有一个 Hanlder 以串行的方式处理队列中的消息。不存在并发执行,因此我们不用考虑线程同步的问题。
2.4.4 使用AIDL
如果有大量的并发请求,使用Messenger就不太适合,同时如果需要跨进程调用服务端的方法,Messenger就无法做到了。这时我们可以使用AIDL。
流程如下:
- 服务端需要创建Service来监听客户端请求,然后创建一个AIDL文件,将暴露给客户端的接口在AIDL文件中声明,最后在Service中实现这个AIDL接口即可。
- 客户端首先绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。
- AIDL支持的数据类型:
- 基本数据类型、String、CharSequence
- List:只支持ArrayList,里面的每个元素必须被AIDL支持
- Map:只支持HashMap,里面的每个元素必须被AIDL支持
- Parcelable
- 所有的AIDL接口本身也可以在AIDL文件中使用
自定义的Parcelable对象和AIDL对象,不管它们与当前的AIDL文件是否位于同一个包,都必须显式import进来。
如果AIDL文件中使用了自定义的Parcelable对象,就必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。
package com.ryg.chapter_2.aidl;
parcelable Book;
AIDL接口中的参数除了基本类型以外都必须表明方向in/out。AIDL接口文件中只支持方法,不支持声明静态常量。建议把所有和AIDL相关的类和文件放在同一个包中,方便管理。
void addBook(in Book book);
AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接时,管理数据的集合直接采用 CopyOnWriteArrayList 来进行自动线程同步。类似的还有 ConcurrentHashMap 。
因为客户端的listener和服务端的listener不是同一个对象,所以 RecmoteCallbackList 是系统专门提供用于删除跨进程listener的接口,支持管理任意的AIDL接口,因为所有AIDL接口都继承自 IInterface 接口。
public class RemoteCallbackList<E extends IInterface>
它内部通过一个Map接口来保存所有的AIDL回调,这个Map的key是 IBinder 类型,value是 Callback 类型。当客户端解除注册时,遍历服务端所有listener,找到和客户端listener具有相同Binder对象的服务端listenr并把它删掉。
客户端RPC的时候线程会被挂起,由于被调用的方法运行在服务端的Binder线程池中,可能很耗时,不能在主线程中去调用服务端的方法。
权限验证
默认情况下,我们的远程服务任何人都可以连接,我们必须加入权限验证功能,权限验证失败则无法调用服务中的方法。通常有两种验证方法:
-
在onBind中验证,验证不通过返回null
验证方式比如permission验证,在AndroidManifest声明:
Android自定义权限和使用权限public IBinder onBind(Intent intent){
int check = checkCallingOrSelefPermission(“com.ryq.chapter2.permission.ACCESSBOOK_SERVICE”);
if(check == PackageManager.PERMISSION_DENIED){
return null;
}
return mBinder;
}
这种方法也适用于Messager。 -
在onTransact中验证,验证不通过返回false
可以permission验证,还可以采用Uid和Pid验证。
2.4.5 使用ContentProvider
ContentProvider是四大组件之一,天生就是用来进程间通信。和Messenger一样,其底层实现是用Binder。
系统预置了许多ContentProvider,比如通讯录、日程表等。要RPC访问这些信息,只需要通过ContentResolver的query、update、insert和delete方法即可。
创建自定义的ContentProvider,只需继承ContentProvider类并实现 onCreate 、 query 、 update、 insert 、 getType 六个抽象方法即可。getType用来返回一个Uri请求所对应的MIME类型,剩下四个方法对应于CRUD操作。这六个方法都运行在ContentProvider进程中,除了 onCreate 由系统回调并运行在主线程里,其他五个方法都由外界调用并运行在Binder线程池中。
ContentProvider是通过Uri来区分外界要访问的数据集合,例如外界访问ContentProvider中的表,我们需要为它们定义单独的Uri和UriCode。根据UriCode,我们就知道要访问哪个表了。
==query、update、insert、delete四大方法存在多线程并发访问,因此方法内部要做好线程同步。==若采用SQLite并且只有一个SQLiteDatabase,SQLiteDatabase内部已经做了同步处理。若是多个SQLiteDatabase或是采用List作为底层数据集,就必须做线程同步。
2.4.6 使用Socket
Socket也称为“套接字”,分为流式套接字和用户数据报套接字两种,分别对应于TCP和UDP协议。Socket可以实现计算机网络中的两个进程间的通信,当然也可以在本地实现进程间的通信。我们以一个跨进程的聊天程序来演示。
在远程Service建立一个TCP服务,然后在主界面中连接TCP服务。服务端Service监听本地端口,客户端连接指定的端口,建立连接成功后,拿到 Socket 对象就可以向服务端发送消息或者接受服务端发送的消息。
本例的客户端和服务端源代码
除了采用TCP套接字,也可以用UDP套接字。实际上socket不仅能实现进程间的通信,还可以实现设备间的通信(只要设备之间的IP地址互相可见)。
使用socket需要声明权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
2.5 Binder连接池
前面提到AIDL的流程是:首先创建一个service和AIDL接口,接着创建一个类继承自AIDL接口中的Stub类并实现Stub中的抽象方法,客户端在Service的onBind方法中拿到这个类的对象,然后绑定这个service,建立连接后就可以通过这个Stub对象进行RPC。
那么如果项目庞大,有多个业务模块都需要使用AIDL进行IPC,随着AIDL数量的增加,我们不能无限制地增加Service,我们需要把所有AIDL放在同一个Service中去管理。
Binder连接池的工作原理如图
- 服务端只有一个Service,把所有AIDL放在一个Service中,不同业务模块之间不能有耦合
- 服务端提供一个 queryBinder 接口,这个接口能够根据业务模块的特征来返回响应的Binder对象给客户端
- 不同的业务模块拿到所需的Binder对象就可以进行RPC了
2.6 选用合适的IPC方式
本文地址:https://blog.csdn.net/chen_xiaoke/article/details/107762910