写在前面
在Android的世界中,进程对我们开发人员来说可以说是无处不在但又似乎很陌生。我们对四大组件了如指掌,但大多数人并未尝试过使用多进程。然而四大组件都是运行在linux进程之上,并且对于四大组件的操作大多涉及到IPC(进程间通信)。可以说,进程对我们应用层开发人员来说是透明的,这也是Google有意为之,Android的设计理念就是弱化进程的概念,以组件作为我们应用的根基,将进程间的通信都封装在了四大组件中。本篇文章会对进程进行大致的介绍,并且会详细地讲述Android多进程模式的优缺点以及使用场景。
进程与线程
进程
进程是具有一定独立功能的程序、它是系统进行资源分配和调度的一个独立单位,重点在系统调度和单独的单位,也就是说进程是可以独立运行的一段程序。在大多数情况下,我们的App会运行在一个独立的Linux进程中。
线程
线程是进程的一个实体,是CPU调度和分派的最小单位,他是比进程更小的能独立运行的基本单位,线程自己基本上不拥有系统资源。默认情况下App一开始都会运行在同一个进程中的同一个线程,也就是我们常说的主线程或者UI线程。
进程与线程的关系
显然,进程与线程是从属关系。一个进程可以包含多个线程,多个线程之间可以共享该进程的资源。线程和进程均可以实现并发,但是多线程实现会更加容易且可控。
进程与应用的生命周期
前面有提到进程对我们来说是透明的,也就是说应用进程的一生都是由系统来控制。系统根据应用当前运行组件的重要性和可用内存的大小来决定。Android 系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,最终需要移除旧进程来回收内存。 为了确定保留或终止哪些进程,系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入“重要性层次结构”中。 必要时,系统会首先消除重要性最低的进程,然后是重要性略逊的进程,依此类推,以回收系统资源。Android 中对于内存的回收,主要依靠 LowmemoryKiller 来完成,是一种根据 OOM_ADJ 阈值级别触发相应力度的内存回收的机制。
重要性层次结构一共有 5 级,按重要性排序为:
前台进程
用户当前操作所必需的进程,比如进程中有一个Activity正显示在屏幕上,用户正在交互的。可见进程
没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。不是出于前台单仍然可见的Activity(调用了onPause),比如前台进程中弹出一个对话框。服务进程
已经启动了服务的进程,比如调用了startService在后台播放音乐。后台进程
包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。空进程
不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。
了解了组件运行对进程的影响,我们在开发过程中可以避免进程被杀掉。但我不是说让你去做进程保活这种事,而是在做一些重要的工作时避免被误杀。
多进程的优点及使用场景
前面铺垫了很久,现在开始讲多进程。既然Android刻意淡化了进程的概念,为何我们还要使用多进程技术呢?
- 获取更多内存,防止OOM
前面有提到过,进程是系统调度和资源的单位,Android系统是以进程为单位进行内存回收。Android一般会给一个进程分配几十M的内存,具体大小视手机内存而定。假如一个进程可以使用64M的内存,则一个应用开启两个进程则是double利用多进程使用更多的内存,避免OOM。当然避免OOM主要还是要我们合理使用回收,使用多进程应该是最后的选择。 - 常驻后台进程
在一个应用中,如果部分功能相对来说是比较独立且是在后台运行的则可以申请一个专用的进程来维护。比如后台推送或者音乐服务。 - 进程隔离
因为进程隔离的特性,在一个进程崩溃的时候只会关闭该进程内的组件而不会影响(准确来说是不会直接影响)另一个进程中的功能。同时这也将内存泄漏隔离了,比如我们经常使用的WebView。要知道现在Hybrid大行其道,一个App中有大量的页面由H5来完成,尤其是搞大促时的电商应用。这时将WebView运行在一个独立的进程中,既扩大了内存又隔离了内存泄漏,只需在合理的时机关闭进程即可。
使用多进程
开启多进程是十分简单的,在Manifest中给四大组件指定android:process即可。
<manifest ...>
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.Main" >
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SecondActivity"
android:process=".second"
/>
<service
android:name=".MusicService"
android:process=":music"
/>
</application>
</manifest>
细心的朋友应该观察到了,上面两个process属性分别是".second"和":music",这两中写法是有区别的。首先 ":" 的含义是在当前进程名前加上包名,另外它表示该进程是应用的私有进程。而非 ":" 开头的进程是全局进程,其他应用可以通过ShareUID的方式和它运行在同一个进程里。
还有一种开启多进程的方法是通过JNI到native层fork,但这是特殊情况,了解下即可。
多进程的坑
凡事都有两面性,多进程的特性既是优点又会带来麻烦。由于进程隔离,每个进程都会有它独立的 DalvikVM,自己的内存空间。
进程隔离是为保护操作系统中进程互不干扰而设计的一组不同硬件和软件的技术。 这个技术是为了避免进程A写入进程B的情况发生。 进程的隔离实现,使用了虚拟地址空间。
从应用开发的角度上来说就会有下面几个问题:
- 静态成员和单例设计模式失效
因为同一个类在多个进程中会多次加载。 - 线程同步机制失效
原因类似,无法锁对象或者类。 - SharedPreferences 可靠性下降
因为SharedPreferences底层是通过读写XML文件实现的,不支持多个进程的并发操作。 - Application 多次创建
同样是多个虚拟机导致的。我们一般都会在onCreate中进行一些初始化操作,不做另外处理的话会导致多次初始化。
由于以上问题的存在,在多进程开发中首先要有这方面的意识,其次对症下药。对于前两个问题,建议在后台进程中避免访问相关的类。而SP的问题可以通过ContentProvider来解决,毕竟ContentProvider就是Android封装过用于进程间数据通信的。
而第四个问题我们可以在运行时获取进程名来规避:
int pid = android.os.Process.myPid();
ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
if (processInfo.pid == pid) {
String currentProcName = processInfo.processName;
if (!TextUtils.isEmpty(currentProcName) && currentProcName.equals(":background")) {
// 跳过初始化操作
return;
}
}
}
写在最后
说了这么多,我们要不要使用多进程呢?这取决于你应用的情况以及你对多进程的掌握程度。目前来说音乐类App是最常见的一种使用场景,而臃肿的电商类App采用WebView独立进程也是一种不错的方案。OOM的问题还是建议合理使用回收来解决。如果你既没有以上需求又无法应对多进程带来的问题,还是对多进程敬而远之吧。最后,谢谢大家看到最后,如果文章中有什么纰漏欢迎在评论区指出。之后我会再写一篇进程间通信的文章 :)
参考
Processes and Threads
Processes and Application Lifecycle
Android开发艺术探索