Android应用内多进程

正常情况下,一个apk启动后只会运行在一个进程中,其进程名为AndroidManifest.xml文件中指定的应用包名,所有的基本组件都会在这个进程中运行。但是如果需要将某些组件(如Service、Activity等)运行在单独的进程中,就需要用到android:process属性了。我们可以为android的基础组件指定process属性来指定它们运行在指定进程中。

默认情况下,同一应用的所有组件均在相同的进程中运行,且大多数应用都不会改变这一点。 但是,如果您发现需要控制某个组件所属的进程,则可在清单文件中执行此操作。

各类组件元素的清单文件条目—<activity>、<service>、<receiver> 和 <provider>—均支持 android:process 属性,此属性可以指定该组件应在哪个进程运行。您可以设置此属性,使每个组件均在各自的进程中运行,或者使一些组件共享一个进程,而其他组件则不共享。 此外,您还可以设置 android:process,使不同应用的组件在相同的进程中运行,但前提是这些应用共享相同的 Linux 用户 ID 并使用相同的证书进行签署。

此外,<application> 元素还支持 android:process 属性,以设置适用于所有组件的默认值。

如果内存不足,而其他为用户提供更紧急服务的进程又需要内存时,Android 可能会决定在某一时刻关闭某一进程。在被终止进程中运行的应用组件也会随之销毁。 当这些组件需要再次运行时,系统将为它们重启进程。

决定终止哪个进程时,Android 系统将权衡它们对用户的相对重要程度。例如,相对于托管可见 Activity 的进程而言,它更有可能关闭托管屏幕上不再可见的 Activity 的进程。 因此,是否终止某个进程的决定取决于该进程中所运行组件的状态。 下面,我们介绍决定终止进程所用的规则。

进程生命周期

Android 系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,最终需要移除旧进程来回收内存。 为了确定保留或终止哪些进程,系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入“重要性层次结构”中。 必要时,系统会首先消除重要性最低的进程,然后是重要性略逊的进程,依此类推,以回收系统资源。

重要性层次结构一共有 5 级。以下列表按照重要程度列出了各类进程(第一个进程最重要,将是最后一个被终止的进程):

前台进程

用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程:

  • 托管用户正在交互的 Activity(已调用 Activity 的 onResume() 方法)
  • 托管某个 Service,后者绑定到用户正在交互的 Activity
  • 托管正在“前台”运行的 Service(服务已调用 startForeground())
  • 托管正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())
  • 托管正执行其 onReceive() 方法的 BroadcastReceiver 通常,在任意给定时间前台进程都为数不多。只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。 此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。
可见进程

没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。 如果一个进程满足以下任一条件,即视为可见进程:

  • 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,如果前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。
  • 托管绑定到可见(或前台)Activity 的 Service。 可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。
服务进程

正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

后台进程

包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。 有关保存和恢复状态的信息,请参阅 Activity文档。

空进程

不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。

根据进程中当前活动组件的重要程度,Android 会将进程评定为它可能达到的最高级别。例如,如果某进程托管着服务和可见 Activity,则会将此进程评定为可见进程,而不是服务进程。

此外,一个进程的级别可能会因其他进程对它的依赖而有所提高,即服务于另一进程的进程其级别永远不会低于其所服务的进程。 例如,如果进程 A 中的内容提供程序为进程 B 中的客户端提供服务,或者如果进程 A 中的服务绑定到进程 B 中的组件,则进程 A 始终被视为至少与进程 B 同样重要。

由于运行服务的进程其级别高于托管后台 Activity 的进程,因此启动长时间运行操作的 Activity 最好为该操作启动服务,而不是简单地创建工作线程,当操作有可能比 Activity 更加持久时尤要如此。例如,正在将图片上传到网站的 Activity 应该启动服务来执行上传,这样一来,即使用户退出 Activity,仍可在后台继续执行上传操作。使用服务可以保证,无论 Activity 发生什么情况,该操作至少具备“服务进程”优先级。 同理,广播接收器也应使用服务,而不是简单地将耗时冗长的操作放入线程中。

线程安全方法

在某些情况下,您实现的方法可能会从多个线程调用,因此编写这些方法时必须确保其满足线程安全的要求。

这一点主要适用于可以远程调用的方法,如绑定服务中的方法。如果对 IBinder 中所实现方法的调用源自运行 IBinder 的同一进程,则该方法在调用方的线程中执行。但是,如果调用源自其他进程,则该方法将在从线程池选择的某个线程中执行(而不是在进程的 UI 线程中执行),线程池由系统在与 IBinder 相同的进程中维护。 例如,即使服务的 onBind() 方法将从服务进程的 UI 线程调用,在 onBind() 返回的对象中实现的方法(例如,实现 RPC 方法的子类)仍会从线程池中的线程调用。 由于一个服务可以有多个客户端,因此可能会有多个池线程在同一时间使用同一 IBinder 方法。因此,IBinder 方法必须实现为线程安全方法。

同样,内容提供程序也可接收来自其他进程的数据请求。尽管 ContentResolver 和 ContentProvider 类隐藏了如何管理进程间通信的细节,但响应这些请求的 ContentProvider 方法(query()、insert()、delete()、update() 和 getType() 方法)将从内容提供程序所在进程的线程池中调用,而不是从进程的 UI 线程调用。 由于这些方法可能会同时从任意数量的线程调用,因此它们也必须实现为线程安全方法。

多进程好处

一般来说,Android应用多进程有三个好处。

  1. 我们知道Android系统对每个应用进程的内存占用是有限制的,而且占用内存越大的进程,通常被系统杀死的可能性越大。让一个组件运行在单独的进程中,可以减少主进程所占用的内存,降低被系统杀死的概率.
  2. 如果子进程因为某种原因崩溃了,不会直接导致主程序的崩溃,可以降低我们程序的崩溃率。
  3. 即使主进程退出了,我们的子进程仍然可以继续工作,假设子进程是推送服务,在主进程退出的情况下,仍然能够保证用户可以收到推送消息。

在Android中,虚拟机分配给各个进程的运行内存是有限制值的(这个值可以是32M,48M,64M等,根据机型而定),试想一下,如果在app中,增加了一个很常用的图片选择模块用于上传图片或者头像,加载大量Bitmap会使app的内存占用迅速增加,如果你还把查看过的图片缓存在了内存中,那么OOM的风险将会大大增加,如果此时还需要使用WebView加载一波网页,我就问你怕不怕!

陷阱

我们已经开启了应用内多进程,那么,开启多进程是不是只是我们看到的这么简单呢?其实这里面会有一些陷阱,稍微不注意就会陷入其中。我们首先要明确的一点是进程间的内存空间时不可见的。从而,开启多进程后,我们需要面临这样几个问题:

  1. 所有运行在不同的进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败
  2. Application的多次重建。
  3. 线程同步机制完全失效(不同的进程的锁都不是同一个对象)
  4. 静态成员和单例模式完全失效(不同进程的内存区域都不一样了)
  5. SharedPreferce的可靠性下降(SharedPreference底层是读写xml文件实现的,系统对它的读写有一定的缓存策略,在内存中会有一份SharedPreferce文件的缓存,所以多个进程并发写操作可能导致数据丢失)
  6. 断点调试问题。

多进程情况下会出现两个进程在同一时刻访问同一个数据库文件的情况。这就可能造成资源的竞争访问,导致诸如数据库损坏、数据丢失等。在多线程的情况下我们有锁机制控制资源的共享,但是在多进程中比较难,虽然有文件锁、排队等机制,但是在Android里很难实现。解决办法就是多进程的时候不并发访问同一个文件,比如子进程涉及到操作数据库,就可以考虑调用主进程进行数据库的操作。

调试就是跟踪程序运行过程中的堆栈信息,由于每个进程都有自己独立的内存空间和各自的堆栈,无法实现在不同的进程间调试。不过可以通过下面的方式实现:调试时去掉AndroidManifest.xml中android:process标签,这样保证调试状态下是在同一进程中,堆栈信息是连贯的。待调试完成后,再将标签复原。

process属性的设置有两种形式

第一种形式如 android:process=”:remote”,以冒号开头,冒号后面的字符串原则上是可以随意指定的。如果我们的包名为“com.example.processtest”,则实际的进程名为“com.example.processtest:remote”。这种设置形式表示该进程为当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中。

第二种情况如 android:process=”com.example.processtest.remote”,以小写字母开头,表示运行在一个以这个名字命名的全局进程中,其他应用通过设置相同的ShareUID可以和它跑在同一个进程。

线程与进程区别

进程是系统进行资源分配和调度的一个独立单位。 线程是进程的一个实体,是CPU调度和分配的基本单位,是比进程更小的能独立运行的基本单位,线程本身不拥有系统资源(除了必不可少的资源如程序计数器、寄存器、栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。 主要差别在于是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其他进程产生影响,而线程是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的健壮,但是多进程在切换时,资源耗费大,效率要差。

查看当前手机中进程方式

ps

各列参数意义:

  • USER 进程当前用户;
  • PID Process ID,进程ID;
  • PPID Process Parent ID,进程的父进程ID;
  • VSIZE Virtual Size,进程的虚拟内存大小;
  • RSS Resident Set Size,实际驻留”在内存中”的内存大小;
  • WCHAN 休眠进程在内核中的地址;
  • PC Program Counter;
  • NAME 进程名;

**有些手机需要ps -A”

oom_adj

Android 中对于内存的回收,主要依靠 Lowmemorykiller 来完成,是一种根据 OOM_ADJ 阈值级别触发相应力度的内存回收的机制。 关于OOM_ADJ的说明如下: tips-android-process-201938151316

其中红色部分代表比较容易被杀死的 Android 进程(OOM_ADJ>=4),绿色部分表示不容易被杀死的 Android 进程,其他表示非 Android 进程(纯 Linux 进程)。在 Lowmemorykiller 回收内存时会根据进程的级别优先杀死 OOM_ADJ 比较大的进程,对于优先级相同的进程则进一步受到进程所占内存和进程存活时间的影响。

Android 手机中进程被杀死可能有如下情况: |进程杀死场景|调用接口|可能影响范围| |—|—|—| |触发系统进程管理机制|Lowmemorykiller|从进程importance值由大到小依次杀死,释放内存| |被第三方应用杀死(无Root)|killBackgoundProcess|只能杀死OOM_ADJ为4以上的进程| |被第三方应用杀死(有Root)|force-stop或者kill|理论上可以杀死所有进程,一般只杀非系统关键进程和非前台和可见进程| |厂商杀进程功能|fource-stop或者kill|理论上可以杀所有进程,包括Native进程| |用户主动”强行停止”进程|force-stop|只能停用第三方和非system/phone进程应用(停用system进程应用会造成Android重启)|

通过cat /proc/进程id/oom_adj可以看到当前进程的adj指,比如输入cat /proc/32366/oom_adj,adj值具体决定了系统在资源吃紧的情况下该杀掉哪些进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
ps | grep "beike"
u0_a332 12952 3183 2072548 185728 SyS_epoll_ 0000000000 S com.lianjia.beike
u0_a332 12972 3183 1792404 45848 SyS_epoll_ 0000000000 S com.lianjia.beike:coreservice
u0_a332 13141 3183 1824480 61480 SyS_epoll_ 0000000000 S com.lianjia.beike:pushservice
u0_a332 13225 3183 1787776 49968 SyS_epoll_ 0000000000 S com.lianjia.beike:remote
nobleltechn:/ $ cat /proc/12952/oom_adj
0
nobleltechn:/ $ cat /proc/12972/oom_adj
1
nobleltechn:/ $ cat /proc/13141/oom_adj
8
nobleltechn:/ $ cat /proc/13225/oom_adj
1

而adj值则在ProcessList中定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
final class ProcessList {
// OOM adjustments for processes in various states:

// Adjustment used in certain places where we don't know it yet.
// (Generally this is something that is going to be cached, but we
// don't know the exact value in the cached range to assign yet.)
static final int UNKNOWN_ADJ = 16;

// This is a process only hosting activities that are not visible,
// so it can be killed without any disruption.
static final int CACHED_APP_MAX_ADJ = 15;
static final int CACHED_APP_MIN_ADJ = 9;

// The B list of SERVICE_ADJ -- these are the old and decrepit
// services that aren't as shiny and interesting as the ones in the A list.
static final int SERVICE_B_ADJ = 8;

// This is the process of the previous application that the user was in.
// This process is kept above other things, because it is very common to
// switch back to the previous app. This is important both for recent
// task switch (toggling between the two top recent apps) as well as normal
// UI flow such as clicking on a URI in the e-mail app to view in the browser,
// and then pressing back to return to e-mail.
static final int PREVIOUS_APP_ADJ = 7;

// This is a process holding the home application -- we want to try
// avoiding killing it, even if it would normally be in the background,
// because the user interacts with it so much.
static final int HOME_APP_ADJ = 6;

// This is a process holding an application service -- killing it will not
// have much of an impact as far as the user is concerned.
static final int SERVICE_ADJ = 5;

// This is a process with a heavy-weight application. It is in the
// background, but we want to try to avoid killing it. Value set in
// system/rootdir/init.rc on startup.
static final int HEAVY_WEIGHT_APP_ADJ = 4;

// This is a process currently hosting a backup operation. Killing it
// is not entirely fatal but is generally a bad idea.
static final int BACKUP_APP_ADJ = 3;

// This is a process only hosting components that are perceptible to the
// user, and we really want to avoid killing them, but they are not
// immediately visible. An example is background music playback.
static final int PERCEPTIBLE_APP_ADJ = 2;

// This is a process only hosting activities that are visible to the
// user, so we'd prefer they don't disappear.
static final int VISIBLE_APP_ADJ = 1;

// This is the process running the current foreground app. We'd really
// rather not kill it!
static final int FOREGROUND_APP_ADJ = 0;

// This is a process that the system or a persistent process has bound to,
// and indicated it is important.
static final int PERSISTENT_SERVICE_ADJ = -11;

// This is a system persistent process, such as telephony. Definitely
// don't want to kill it, but doing so is not completely fatal.
static final int PERSISTENT_PROC_ADJ = -12;

// The system process runs at the default adjustment.
static final int SYSTEM_ADJ = -16;

// Special code for native processes that are not being managed by the system (so
// don't have an oom adj assigned by the system).
static final int NATIVE_ADJ = -17;
}

较于Importance等级而言adj值可以赋予我们更多的参考价值,从上述adj值的定义中我们可以看到,值越小优先级越高,比如native进程的adj值为-17,对于这个adj值的进程来说,系统根本不会动它一分一毫,实质上当进程的adj值去到2时系统就很少会因为其它原因而去杀死它,这些在研究进程保活中都非常重要。

进程间通信

  • IPC: InterProcess Communication
  • RPC: Remote Procedure Call
进程间通信的方式-对比
名称 优点 缺点 适用场景
Intent 简单易用 只能传输Bundle所支持的数据类型 四大组件的进程间通信
文件共享 简单易用 不适合高并发 简单的数据共享,无高并发场景
AIDL 功能强大,支持一对多并发实时通信 适用稍微复杂,需要注意线程同步 复杂的进程间调用,Android中最常用
Messenger 比AIDL稍微简单易用些 比AIDL功能弱,只支持一对多串行实时通信 简单的进程间通信
ContentProvider 功能强大的数据共享能力,可通过call方法扩张 受约束的AIDL,主要对外提供数据线的CRUD操作 进程间的大量数据共享
RemoteViews 在跨进程方法UI方面有奇效 比较小众的通信方式 某些特殊的场景
Socket 跨主机,通信范围广 只能传输原始的字节流 常用与网络通信中

使用Bundle

  • 四大组件中的三大组件(Activity、Service、Receiver)都支持在Intent中传递Bundle数据
  • Bundle中的数据除了基本类型,其他的都需要可序列化
  • 适用于从一个进程启动另一个进程,比如在一个进程中启动另一个进程中的Activity、Service、Receiver,与此同时传递数据的情况
使用文件共享
  • 2个进程通过读写同一个文件来交换数据
  • 当然要把数据写入文件,必然要求数据可以序列化和反序列化
  • 文件共享对文件格式没有要求,只要读写双方约定好数据格式
  • 文件共享的方式存在并发读写的问题,适合对数据同步要求不高的进程间通信,并且要妥善处理并发读写的问题
使用Messager

使用Messager来传递Message,Message中能使用的字段只有what、arg1、arg2、Bundle和replyTo,自定义的Parcelable对象无法通过object字段来传输 Message中的Bundle支持多种数据类型,replyTo字段用于传输Messager对象,以便进程间相互通信 Messager以串行的方式处理客户端发来的消息,不适合有大量并发的请求 Messager方法只能传递消息,不能跨进程调用方法

使用AIDL

AIDL接口可以通过编写AIDL文件然后由系统生成对应的Binder类,通过Binder我们就可以进行跨进程通信了 AIDL文件中只支持如下几种类型:

  • 基本类型,如int、long等
  • String和CharSequence
  • List:只支持ArrayList,里面的每个元素都必须被AIDL支持
  • Map:只支持HashMap,里面的key、value都必须被AIDL支持
  • AIDL:所有的AIDL接口本身也可以在AIDL文件中使用

AIDL文件中用到的自定义Parcelable对象和AIDL对象必须要显示的import进来 除了基本数据类型,其他类型的参数必须标明是入参还是出参,in表示输入型参数,out表示输出型参数,inout表示输入输出型参数

进程保活机制

Android 进程拉活包括两个层面:

  1. 提供进程优先级,降低进程被杀死的概率
  2. 在进程被杀死后,进行拉活
提升进程优先级的方案
利用 Activity 提升权限
  1. 方案设计思想:监控手机锁屏解锁事件,在屏幕锁屏时启动1个像素的 Activity,在用户解锁时将 Activity 销毁掉。注意该 Activity 需设计成用户无感知。通过该方案,可以使进程的优先级在屏幕锁屏时间由4提升为最高优先级1。
  2. 方案适用范围
  • 适用场景:本方案主要解决第三方应用及系统管理工具在检测到锁屏事件后一段时间(一般为5分钟以内)内会杀死后台进程,已达到省电的目的问题。
  • 适用版本: 适用于所有的 Android 版本。
  1. 方案具体实现 首先定义 Activity,并设置 Activity 的大小为1像素:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    protected void onCreate(Bundle savedInstanceState){
    super.onCreate(savedInstanceState);
    Window window = getWindow();
    window.setGravity(Gravity.LEFT|Gravity.TOP);
    WindowManager.LayoutParams params = window.getAttributes();
    params.x = 0;
    params.y = 0;
    params.height = 1;
    params.width = 1;
    window.setAttributes(params);
    }

通过如下属性,排除Activity在RecentTask中的显示:

1
2
3
4
5
6
7
<activity
android:excludeFromRecents = "true"
android:exported = "false"
android:finishOnTaskLaunch = "false"
android:launchMode = "singleInstance"
android:process = ":live"
android:theme="@style/LiveActivityStyle"/>

控制Activity为透明:

1
2
3
4
5
6
7
<style name="LiveActivityStyle>
<item name="android:windowBackground>@android:color/transparent</item>
<item name="android:windowBackGround>@null</item>
<item name="android:windowContentOverlay>@null</item>
<item name="android:windowIsTranslucent">@null</item>
...
</style>

Activity启动与销毁时机的控制:监控Intent.ACTION_SCREEN_OFF与Intent.ACTION_USER_PRESENT广播接收者

利用 Notification 提升权限
  1. 方案设计思想: Android 中 Service 的优先级为4,通过 setForeground 接口可以将后台 Service 设置为前台 Service,使进程的优先级由4提升为2,从而使进程的优先级仅仅低于用户当前正在交互的进程,与可见进程优先级一致,使进程被杀死的概率大大降低。
  2. 方案实现挑战: 从 Android2.3 开始调用 setForeground 将后台 Service 设置为前台 Service 时,必须在系统的通知栏发送一条通知,也就是前台 Service 与一条可见的通知时绑定在一起的。对于不需要常驻通知栏的应用来说,该方案虽好,但却是用户感知的,无法直接使用。
  3. 方案挑战应对措施: 通过实现一个内部 Service,在 LiveService 和其内部 Service 中同时发送具有相同 ID 的 Notification,然后将内部 Service 结束掉。随着内部 Service 的结束,Notification 将会消失,但系统优先级依然保持为2。
  4. 方案适用范围: 适用于目前已知所有版本。
  5. 方案具体实现
进程死后拉活的方案
利用系统广播拉活
  1. 方案设计思想:在发生特定系统事件时,系统会发出响应的广播,通过在 AndroidManifest 中“静态”注册对应的广播监听器,即可在发生响应事件时拉活。常用的用于拉活的广播事件包括:开机广播,网络变化,文件挂载,屏幕亮灭,锁屏解屏,应用安装卸载
  2. 方案适用范围:适用于全部 Android 平台。但存在如下几个缺点:广播接收器被管理软件、系统软件通过“自启管理”等功能禁用的场景无法接收到广播,从而无法自启;系统广播事件不可控,只能保证发生事件时拉活进程,但无法保证进程挂掉后立即拉活。因此,该方案主要作为备用手段。
利用第三方应用广播拉活
  1. 方案设计思想:该方案总的设计思想与接收系统广播类似,不同的是该方案为接收第三方 Top 应用广播。通过反编译第三方 Top 应用,如:手机QQ、微信、支付宝、UC浏览器等,以及友盟、信鸽、个推等 SDK,找出它们外发的广播,在应用中进行监听,这样当这些应用发出广播时,就会将我们的应用拉活。
  2. 方案适用范围:该方案的有效程度除与系统广播一样的因素外,主要受如下因素限制:反编译分析过的第三方应用的多少;第三方应用的广播属于应用私有,当前版本中有效的广播,在后续版本随时就可能被移除或被改为不外发。这些因素都影响了拉活的效果。
利用系统Service机制拉活
  1. 方案设计思想:将 Service 设置为 START_STICKY,利用系统机制在 Service 挂掉后自动拉活:
  2. 方案适用范围:如下两种情况无法拉活. 1.Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉;。2.进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
利用Native进程拉活
  1. 方案设计思想:
  • 主要思想:利用 Linux 中的 fork 机制创建 Native 进程,在 Native 进程中监控主进程的存活,当主进程挂掉后,在 Native 进程中立即对主进程进行拉活。
  • 主要原理:在 Android 中所有进程和系统组件的生命周期受 ActivityManagerService 的统一管理。而且,通过 Linux 的 fork 机制创建的进程为纯 Linux 进程,其生命周期不受 Android 的管理。
  1. 方案实现挑战: 挑战一:在 Native 进程中如何感知主进程死亡。 要在 Native 进程中感知主进程是否存活有两种实现方式:
  • 在 Native 进程中通过死循环或定时器,轮训判断主进程是否存活,档主进程不存活时进行拉活。该方案的很大缺点是不停的轮询执行判断逻辑,非常耗电。
  • 在主进程中创建一个监控文件,并且在主进程中持有文件锁。在拉活进程启动后申请文件锁将会被堵塞,一旦可以成功获取到锁,说明主进程挂掉,即可进行拉活。由于 Android 中的应用都运行于虚拟机之上,Java 层的文件锁与 Linux 层的文件锁是不同的,要实现该功能需要封装 Linux 层的文件锁供上层调用。 封装 Linux 文件锁的代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int linuxflock(JNIEnv *env, jobject obj, jstring lockFilePath){
    LOGI("flock");
    int fd = linuxgetlockfd(env, lockFilePath);
    LOGD("Lock file fd : %d", fd);
    if(fd > 0){
    if(linuxtestflock(fd) == 0){
    LOGD("try to lock the file");
    return flock(fd, LOCK_EX);//创建文件排它锁
    }
    }
    return 1;
    }

Native 层中堵塞申请文件锁的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void *thread_watch(void* arg){
LOGI("jni thread_watch);
int fd = 0;
if((fd == open(mLockFilePath, O_RDWR))){
LOGE("jni thread_watch open file failed,errno: %d", errno);
pthread_exit(0);
}
LOGD("jni thread_watch,open file success, try to lock the file");
set_watch_state_symbol(1);
if(flock(fd, LOCK_EX) == 0)//申请文件的排他锁
{
LOGD("jni thread_watch successfuly get file lock");
try_pullup();
}else{
set_watch_state_symbol(0);
LOGD("jni thread_watch,lock file error!!!");
}
close(fd);//关闭文件会将锁自动释放
return 0;
}

挑战二:在 Native 进程中如何拉活主进程。 通过 Native 进程拉活主进程的部分代码如下,即通过 am 命令进行拉活。通过指定“–include-stopped-packages”参数来拉活主进程处于 forestop 状态的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static void try_pullup()
{
LOGI("jni try_pullup");
if(!is_application_exist(mPackageName))
{
LOGD("jni try_pullup process exit, because xlb has been removed!");
set_watch_state_symbol(0);
freeAppMsg();
exit(0);
}
LOGD("jni try_pullup try to wake up live service...");
char pkg[256] = "";
strcat(pkg, " -n ");
strcat(pkg, mPackageName);
strcat(pkg, "/com.xlb.keeplive.KeepLiveService\"");
int ret - execlp("am", "am", "startservice", "--user", mSerial, "-n", pkg, "--include-stopped-packages", NULL);
LOGD("start service, ret = %d", ret);
freeAppMsg();
set_watch_state_symbol(0);
pthread_exit(0);
}

挑战三:如何保证 Native 进程的唯一。 从可扩展性和进程唯一等多方面考虑,将 Native 进程设计层 C/S 结构模式,主进程与 Native 进程通过 Localsocket 进行通信。在Native进程中利用 Localsocket 保证 Native 进程的唯一性,不至于出现创建多个 Native 进程以及 Native 进程变成僵尸进程等问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int main(const int argc, const char *argv[])
{
LOGI("jni do_main");
if(argc < 2){
return -1;
}
struct sockaddr addr;
socklen_t alen;
int lsocket, s, count;

const char* socket_name = argv[1];
LOGD("jni socket name: %s", socket_name);
lsocket = socket_local_server(socket_name, ANDROID_SOCKET_NAMESPACE_ABSTRACT, SOCK_STREAM);
if(lsocket <0)
{
LOGE("jni Failed to get socket from environment: %s", strerror(errno));
exit(1);
}
fcntl(lsocket, F_SETFD< FD_CLOEXEC);
LOGD("jni native pid = %d", getpid());
...
}
  1. 方案适用范围:该方案主要适用于 Android5.0 以下版本手机。该方案不受 forcestop 影响,被强制停止的应用依然可以被拉活,在 Android5.0 以下版本拉活效果非常好。对于 Android5.0 以上手机,系统虽然会将native进程内的所有进程都杀死,这里其实就是系统“依次”杀死进程时间与拉活逻辑执行时间赛跑的问题,如果可以跑的比系统逻辑快,依然可以有效拉起。记得网上有人做过实验,该结论是成立的,在某些 Android 5.0 以上机型有效。
利用 JobScheduler 机制拉活
  1. 方案设计思想:Android5.0 以后系统对 Native 进程等加强了管理,Native 拉活方式失效。系统在 Android5.0 以上版本提供了 JobScheduler 接口,系统会定时调用该进程以使应用进行一些逻辑操作。

在本项目中,我对 JobScheduler 进行了进一步封装,兼容 Android5.0 以下版本。封装后 JobScheduler 接口的使用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class KeepLiveService extends JobService{
private final static String TAG = "keeplive";
private boolean isServiceExit = false;//通过变量控制是否需要退出service
private volatile static Service mKeepLiveService = null;
public static boolean isServiceLive(){
return mKeepLiveSErvice != null;
}
}
/**
* 启动JobScheduler拉活
* 适用范围:用于Android5.0以后版本进程保活,对被"强制停止"有效
*/
public void startJobScheduler(){
try{
int jobId = 1;
JobInfo.Builer builder = new JobInfo.Builder(jobId, new CompentName(FZApplication.self, KeepLiveSErvice.class));
builder.setPeriodic(10);
builder.setPersisted(true);
JobScheduler.getInstance(FZApplication.self()).schedule(builder.build());
}catch(Throwable t){
t.printStackTrace();
}
}
  1. 方案适用范围:该方案主要适用于 Android5.0 以上版本手机。方案在 Android5.0 以上版本中不受 forcestop 影响,被强制停止的应用依然可以被拉活,在 Android5.0 以上版本拉活效果非常好。仅在小米手机可能会出现有时无法拉活的问题。
利用账号同步机制拉活
  1. 方案设计思想:Android 系统的账号同步机制会定期同步账号进行,该方案目的在于利用同步机制进行进程的拉活。添加账号和设置同步周期的代码如下:

该方案需要在 AndroidManifest 中定义账号授权与同步服务。

1
2
3
4
/**
* 添加账户,并启用账号同步功能<br/>
* 利用账户同步功能进行拉活,适用范围为目前所有版本<br/>
* 这是设置的账号同步周期为30秒,可以通过常量SYNC_FREQUENCY修改

private void addAccount(){ AccountManager accountManager = (AccountManager)this.getSystemService(Context.ACCOUNT_SERVICE); Account account = null; Account[] accounts = accountManager.getAccountsByType(ACCOUNT_TYPE); if(accounts.length > 0){ account = accounts[0]; }else{ account = new Account(getString(R.string.account_name), ACCOUNT_TYPE); } if(accountManager.addAccountExplicitly(account,null,null)){ //开启同步,并设置同步周期 ContentResolver.setIsSyncable(Account, CONTENT_AUTHORITY, 1); ContentResolver.setSyncAutomatically(account,CONTENT_AUTHORITY, true); ContentResolver.addPeriodicSync(account, CONTENT_AUTHORITY, new Bundle…); } } 该方案需要在AndroidManifest中定义账号授权与同步服务.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<service
android:name="com.xlb.keeplive.KeepLiveService$AuthenticationSErvice"
android:exported="true"
android:process=":live>
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator"/>
</intent-filter>
<meta-data
android:name = "android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator"/>
/>
<service
android:name="com.xlb.keeplive.KeepLiveService$AccountSyncService"
android:exported="true"
android:process=":live>
<intent-filter>
<action android:name="android.accounts.SyncAdapter"/>
</intent-filter>
<meta-data
android:name = "android.accounts.SyncAdapter"
android:resource="@xml/SyncAdapter"/>
/>
<>
  1. 方案适用范围:该方案适用于所有的 Android 版本,包括被 forestop 掉的进程也可以进行拉活。最新 Android 版本(Android N)中系统好像对账户同步这里做了变动,该方法不再有效。
其他有效拉活方案

经研究发现还有其他一些系统拉活措施可以使用,但在使用时需要用户授权,用户感知比较强烈。 这些方案包括:

  1. 利用系统通知管理权限进行拉活
  2. 利用辅助功能拉活,将应用加入厂商或管理软件白名单。

这些方案需要结合具体产品特性来搞。

上面所有解释这些方案都是考虑的无 Root 的情况。其他还有一些技术之外的措施,比如说应用内 Push 通道的选择:

  1. 国外版应用:接入 Google 的 GCM。
  2. 国内版应用:根据终端不同,在小米手机(包括 MIUI)接入小米推送、华为手机接入华为推送;其他手机可以考虑接入腾讯信鸽或极光推送与小米推送做 A/B Test。

其他概念

Parcelable和Serializable
Serializable接口

Serializable接口实现序列化比较简单,只需要在需要实现序列化的类实现Serializable接口就行 serialVersionUID可以指定也可以不指定,不指定的话系统会默认给我们生成 serialVersionUID的作用是用于标识当前类的版本,便于在反序列化过程中判断类是否有更改,如果serialVersionUID不一致,反序列化就会失败 人为指定serialVersionUID后,如果不是破坏性的改动了类,比如版本升级后增加了一个字段,那反序列化以后仍然能成功;而如果没有指定,反序列化就会失败 serialVersionUID是否指定需要根据具体的需要来确定 静态成员变量和transient关键字标记的变量不参与序列化过程

Parcelable接口

Parcelable接口实现序列化稍微复杂,需要实现writeToParcel方法、describeContents方法以及生成器Creator 内容描述功能的describeContents方法一般都返回0,当前对象中存在文件描述符时才返回1 Parcelable接口中的Parcel内部包装了可序列化的数据,可以在Binder中自由传输

总结

Serializable使用简单但是开销很大,序列化和反序列化过程需要大量的IO操作,一般用于将对象序列化到存储设备中或者将对象序列化后通过网络传输。

Parcelable使用麻烦,但效率很高。

Binder

binder是Android中的一种跨进程通信方式 可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder. 从Android Framework角度来说,Binder是ServiceManager连接各种Manager和相应ManagerService的桥梁。 从Android应用层来说,Binder是客户端和服务端进行通信的媒介。当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。

更安全,比如socket的ip地址可以进行伪造,而Binder机制从协议本身就支持对通信双方做身份校验,因而大大提升了安全性,这个也是Android权限模型的基础。

tips-android-process-201938104639

如下图,伪装。即代理模式。对代理对象的操作会通过驱动最终转发到Binder本地对象上去完成,当然使用者无需关心这些细节。 tips-android-process-201938104833

Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,它的引用却遍布于系统的各个进程中。最诱人的是,这个引用和java里引用一样既可以是强类型,也可以是弱类型,而且可以从一个进程传给另一个进程,让大家都能访问同一Server,就像一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。

坚持原创技术分享,您的支持将鼓励我继续创作!