Android不死保活分析
MAX保活方案利用Linux文件锁机制,通过双进程相互监控实现保活。核心思路是两个进程各自锁定文件并监听对方文件锁状态,一旦进程被杀死,锁立即释放,另一进程就能快速检测并重新启动它。为实现毫秒级快速拉起,方案在native层预先创建好Parcel并获取Binder,避免进程被系统杀死时临时创建的时间消耗。整体通过AppProcess启动独立守护进程、进程间文件锁监控和预初始化组件等技术,构建了一套高效的多进程相互守护体系。
博主博客
源码下载
https://github.com/yimudi003/KeepAlive2.git
集成方式
可以引入 module,和使用打包好的 AAR 形式。
public class App extends Application {
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
DaemonHolder.getInstance().attach(base, this);
}
}
注意:如果 targetSdk 为 31,需要将 module AndroidManifest.xml 中 AutoBootReceiver 增加 android:exported="true"
保活原理
文件锁保活思路
首先,Windows 和 Linux 有一套文件进程同步机制:一个进程可以给一个文件上锁,在操作完成之后再进行解锁。其他进程如果访问这个文件,会先检查一下这个文件是否有锁。如果有锁,代表别人正在操作,就会对文件延迟处理。那么当一个进程挂掉,他给文件加的锁也自然会消失。

按照这个机制,可以想到:
- 进程 A 给文件 A1 加锁
- 进程 B 给进程 B1 加锁
- 让进程 A 去阻塞读取进程 B1 文件
- 进程 B 去阻塞读取 A1 文件
那么如果有进程挂掉,那么另一个进程就会读取到被释放的文件,通过这个方式监听到对方挂掉。
这个方法 JAVA 层是无法做到的,所以需要使用 C,在 native 层来实现。
快速启动优化
思路是这样的,但是系统在杀死应用对应进程的时候时间非常快,只有几十毫秒,而发送一个 Intent 时间较长,它使用的是 ActivityManagerService 的一个代理类,通过 Binder 将 Intent 传给系统,执行完时间在百毫秒,很难发出去,所以要做到用最短的时间来启动。
我们发送 intent 的时候会初始化一个 Parcel,通过 binder transcate 过去。时间消耗在了创建 Parcel 上面,这里需要我们在进程开始的时候就要创建好 Parcel,再拿到 Binder 直接发送出去。
竞品实现方式:
p = Parcel.obtain();
p.writeInterfaceToken("android.app.IActivityManager");
p.writeStrongBinder(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
p.writeInt(1);
}
entity.intent.writeToParcel(p, 0);
p.writeString(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
p.writeInt(0);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
p.writeString(entity.intent.getComponent().getPackageName());
}
p.writeInt(0);
try {
Class<?> cls = Class.forName("android.app.ActivityManagerNative");
Object invoke = cls.getMethod("getDefault", new Class[0]).invoke(cls, new Object[0]);
Field field = invoke.getClass().getDeclaredField("mRemote");
field.setAccessible(true);
binder = (IBinder) field.get(invoke);
field.setAccessible(false);
Logger.v(Logger.TAG, "initAmsBinder: mRemote == iBinder " + binder);
} catch (Throwable th) {
binderManager.thrown(th);
}
if (binder == null) {
try {
binder = (IBinder) Class.forName("android.os.ServiceManager").getMethod(
"getService", new Class[]{String.class}).invoke(null,
new Object[]{"activity"});
} catch (Throwable th) {
binderManager.thrown(th);
}
}
Parcel 是在进程初始化的时候拿到的,而 Binder 是通过反射拿到的。
实现中的问题
大概思路是这样的,当然在实现中还会有很多问题,比如:
- binder transcate 无法启动 Service,需要在广播中去启动,还需要创建一个 parcel 来启动广播,使用广播来拉起进程
- 在 Native 层中直接传递 parcel,会导致监听不到进程被杀;改成传输 u8 数据解决了
项目架构分析
Application 初始化
现在我们来从头梳理一下,这个项目到底在 Application 中都做了什么?
在 DaemonHolder 中 attach 方法做了哪些事?
public void attach(Context base, Application app) {
app.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(final Activity activity, Bundle savedInstanceState) {
Logger.v(Logger.TAG, String.format("====> [%s] created", activity.getLocalClassName()));
ServiceHolder.getInstance().bindService(activity, DaemonService.class,
new ServiceHolder.OnServiceConnectionListener() {
@Override
public void onServiceConnection(ServiceConnection connection, boolean isConnected) {
if (isConnected) {
connCache.put(activity, connection);
}
}
});
}
..........
}
可以看到监听了 ActivityLifecycleCallbacks,在 onActivityCreated 通过 BindService 方式去启动了 DaemonService。
DaemonService
public class DaemonService extends DaemonBaseService {
@Override
public IBinder onBind(Intent intent) {
return super.onBind(intent);
}
@Override
public void onCreate() {
try {
ContextCompat.startForegroundService(this,
new Intent().setClassName(getPackageName(), NotifyResidentService.class.getName()));
} catch (Throwable th) {
Logger.e(Logger.TAG, "failed to start foreground service: " + th.getMessage());
}
Intent intent2 = new Intent();
intent2.setClassName(getPackageName(), AssistService1.class.getName());
startService(intent2);
Intent intent3 = new Intent();
intent3.setClassName(getPackageName(), AssistService2.class.getName());
startService(intent3);
super.onCreate();
}
}
DaemonService 中启动了前台通知 NotifyResidentService,和 AssistService1,AssistService2。NotifyResidentService 是一个前台通知 Service,而 AssistService1、AssistService2 就是在不同进程上的常规 Service。
JavaDaemon
再接着 attach 方法中往下看:
JavaDaemon.getInstance().fire(
base,
new Intent(base, DaemonService.class),
new Intent(base, DaemonReceiver.class),
new Intent(base, DaemonInstrumentation.class)
);
接着往下分析 JavaDaemon 中都干了什么,捋下来发现创建了三空文件。

pprivate void fire(Context context, DaemonEnv env, String[] strArr) {
Logger.i(Logger.TAG, "############################################## !!! fire(): " +
"env=" + env + ", strArr=" + Arrays.toString(strArr));
boolean z;
String processName = Utils.getProcessName();
Logger.e(Logger.TAG, "processName: " + processName);
if (processName.startsWith(context.getPackageName()) && processName.contains(COLON_SEPARATOR)) {
String substring = processName.substring(processName.lastIndexOf(COLON_SEPARATOR) + 1);
List<String> list = new ArrayList();
if (strArr != null) {
z = false;
for (String str : strArr) {
if (str.equals(substring)) {
z = true;
} else {
list.add(str);
}
}
} else {
z = false;
}
if (z) {
Logger.v(Logger.TAG, "app lock file start: " + substring);
NativeKeepAlive.lockFile(context.getFilesDir() + "/" + substring + "_d");
Logger.v(Logger.TAG, "app lock file finish");
String[] strArr2 = new String[list.size()];
for (int i = 0; i < strArr2.length; i++) {
strArr2[i] = context.getFilesDir() + "/" + list.get(i) + "_d";
Logger.e(Logger.TAG, "processName strarr2: " + strArr2[i]);
}
futureScheduler.scheduleFuture(new AppProcessRunnable(env, strArr2, "daemon"), 0);
}
} else if (processName.equals(context.getPackageName())) {
ContextCompat.startForegroundService(context, new Intent(context, DaemonService.class));
}
}
除此之外还有一段核心代码:
@Override
public void run() {
DaemonEntity entity = new DaemonEntity();
entity.str = str;
entity.strArr = strArr;
entity.intent = env.intent;
entity.intent2 = env.intent2;
entity.intent3 = env.intent3;
List<String> list = new ArrayList();
list.add("export CLASSPATH=$CLASSPATH:" + env.publicSourceDir);
if (env.nativeLibraryDir.contains("arm64")) {
list.add("export _LD_LIBRARY_PATH=/system/lib64/:/vendor/lib64/:" + env.nativeLibraryDir);
list.add("export LD_LIBRARY_PATH=/system/lib64/:/vendor/lib64/:" + env.nativeLibraryDir);
list.add(String.format("%s / %s %s --application --nice-name=%s &",
new Object[]{new File("/system/bin/app_process").exists() ?
"app_process" : "app_process", DaemonMain.class.getName(),
entity.toString(), str}));
} else {
list.add("export _LD_LIBRARY_PATH=/system/lib/:/vendor/lib/:" + env.nativeLibraryDir);
list.add("export LD_LIBRARY_PATH=/system/lib/:/vendor/lib/:" + env.nativeLibraryDir);
list.add(String.format("%s / %s %s --application --nice-name=%s &",
new Object[]{new File("/system/bin/app_process32").exists() ?
"app_process32" : "app_process", DaemonMain.class.getName(),
entity.toString(), str}));
}
Logger.i(Logger.TAG, "cmds: " + list);
File file = new File("/");
String[] strArr = new String[list.size()];
for (int i = 0; i < strArr.length; i++) {
strArr[i] = list.get(i);
}
ShellExecutor.execute(file, null, strArr);
}
DaemonEntity 储存了 context.getApplicationInfo() 中的信息,publicSourceDir,nativeLibraryDir,都是由 ApplicationInfo 对象获取的。然后通过 ProcessBuilder 对象以命令行的形式,来执行 list 中的内容。Android 底层是 Linux,也就是在 Linux 中去执行命令。
我们查看一下 list 中的数据:
export CLASSPATH=$CLASSPATH:/data/app/com.daemon.keepalive2-yTOyxGcJe4jXr41qdEREFg==/base.apk,
export _LD_LIBRARY_PATH=/system/lib64/:/vendor/lib64/:/data/app/com.daemon.keepalive2-yTOyxGcJe4jXr41qdEREFg==/lib/arm64,
export LD_LIBRARY_PATH=/system/lib64/:/vendor/lib64/:/data/app/com.daemon.keepalive2-yTOyxGcJe4jXr41qdEREFg==/lib/arm64,
app_process / com.keepalive.daemon.core.DaemonMain AgAAADIAAAAvAGQAYQB0AGEALwB1AHMAZQByAC8AMAAvAGMAbwBtAC4AZABhAGUAbQBvAG4ALgBrAGUAZQBwAGEAbABpAHYAZQAyAC8AZgBpAGwAZQBzAC8AYQBzAHMAaQBzAHQAMQBfAGQAAAAAADIAAAAvAGQAYQB0AGEALwB1AHMAZQByAC8AMAAvAGMAbwBtAC4AZABhAGUAbQBvAG4ALgBrAGUAZQBwAGEAbABpAHYAZQAyAC8AZgBpAGwAZQBzAC8AYQBzAHMAaQBzAHQAMgBfAGQAAAAAAAYAAABkAGEAZQBtAG8AbgAAAAAAAQAAAP////8AAAAA//////////8AAAAA/////xUAAABjAG8AbQAuAGQAYQBlAG0AbwBuAC4AawBlAGUAcABhAGwAaQB2AGUAMgAAADEAAABjAG8AbQAuAGsAZQBlAHAAYQBsAGkAdgBlAC4AZABhAGUAbQBvAG4ALgBjAG8AcgBlAC4AYwBvAG0AcABvAG4AZQBuAHQALgBEAGEAZQBtAG8AbgBTAGUAcgB2AGkAYwBlAAAAAAAAAAAAAAAAAAAAAAAAAP7/////////AAAAAAAAAAAAAAAAAAAAAAEAAAD/////AAAAAP//////////AAAAAP////8VAAAAYwBvAG0ALgBkAGEAZQBtAG8AbgAuAGsAZQBlAHAAYQBsAGkAdgBlADIAAAAyAAAAYwBvAG0ALgBrAGUAZQBwAGEAbABpAHYAZQAuAGQAYQBpAGUAbQBvAG4ALgBjAG8AcgBlAC4AYwBvAG0AcABvAG4AZQBuAHQALgBEAGEAZQBtAG8AbgBSAGUAYwBlAGkAdgBlAHIAAAAAAAAAAAAAAAAAAAAAAAAAAAA/v////////8AAAAAAAAAAAAAAAAAAAAAAQAAAP////8AAAAA//////////8AAAAA/////xUAAABjAG8AbQAuAGQAYQBpAGUAbQBvAG4ALgBrAGUAZQBwAGEAbABpAHYAZQAyAAAADQAAAGMAbwBtAC4AawBlAGUAcABhAGwAaQB2AGUALgBkAGEAaQBlAG0AbwBuAC4AYwBvAHIAZQAuAGMAbwBtAHAAbwBuAGUAbgB0AC4ARABhAGUAbQBvAG4ASQBuAHMAdAByAHUAbQBlAG4AdABhAHQAaQBvAG4AAAAAAAAAAAAAAAAAAAAAAAAD//////////8AAAAAAAAAAAAAAAAAAAA= --application --nice-name=daemon &
那这些数据到底是什么意思?前三条好理解,就是配置了 Linux 环境变量,对应的数据分别是 publicSourceDir,nativeLibraryDir,nativeLibraryDir。第四条猜测是将 DaemonMain 类在底层去运行,而 DaemonMain 是和进程每个进程单独持有的,也就是说每个进程都声明到了底层,然后具体类通过这个类去实现。
通过 debug 方式发现代码无法走到 DaemonMain 方法中,而查看 log 发现 DaemonMain 中是运行的!那 DaemonMain 就是关键了。
DaemonMain 核心类
public class DaemonMain {
private IBinderManager binderManager = new IBinderManager();
public DaemonEntity entity;
private Parcel p;
private Parcel p2;
private Parcel p3;
private IBinder binder;
private static volatile FutureScheduler futureScheduler;
private DaemonMain(DaemonEntity entity) {
this.entity = entity;
}
public static void main(String[] strArr) {
if (futureScheduler == null) {
synchronized (DaemonMain.class) {
if (futureScheduler == null) {
futureScheduler = new SingleThreadFutureScheduler(
"daemonmain-holder",
true
);
}
}
}
DaemonEntity entity = DaemonEntity.create(strArr[0]);
if (entity != null) {
new DaemonMain(entity).execute();
}
Logger.e(Logger.TAG, "DaemonMain.main Process.killProcess" + Process.myPid());
Process.killProcess(Process.myPid());
}
private void execute() {
try {
initAmsBinder();
assembleParcel();
NativeKeepAlive.nativeSetSid();
try {
Logger.v(Logger.TAG, "setArgV0: " + entity.str);
Process.class.getMethod("setArgV0", new Class[]{String.class}).invoke(null,
new Object[]{entity.str});
} catch (Exception e) {
e.printStackTrace();
}
for (int i = 1; i < entity.strArr.length; i++) {
futureScheduler.scheduleFuture(new DaemonRunnable(this, i), 0);
}
Logger.v(Logger.TAG, "[" + entity.str + "] wait file lock start: " + entity.strArr[0]);
NativeKeepAlive.waitFileLock(entity.strArr[0]);
Logger.v(Logger.TAG, "[" + entity.str + "] wait file lock finish");
startService();
broadcastIntent();
startInstrumentation();
Logger.v(Logger.TAG, "[" + entity.str + "] start android finish");
} catch (Throwable th) {
binderManager.thrown(th);
}
}
public void startInstrumentation() {
Logger.i(Logger.TAG, "call startInstrumentation(): " + p3);
if (p3 != null) {
try {
binder.transact(binderManager.startInstrumentation(), p3, null, 1);
} catch (Throwable th) {
binderManager.thrown(th);
}
}
}
public void broadcastIntent() {
Logger.i(Logger.TAG, "call broadcastIntent(): " + p2);
if (p2 != null) {
try {
binder.transact(binderManager.broadcastIntent(), p2, null, 1);
} catch (Throwable th) {
binderManager.thrown(th);
}
}
}
public void startService() {
Logger.i(Logger.TAG, "call startService(): " + p);
if (p != null) {
try {
binder.transact(binderManager.startService(), p, null, 1);
} catch (Throwable th) {
binderManager.thrown(th);
}
}
}
private void assembleParcel() {
assembleServiceParcel();
assembleBroadcastParcel();
assembleInstrumentationParcel();
}
private void assembleServiceParcel() {
p = Parcel.obtain();
p.writeInterfaceToken("android.app.IActivityManager");
p.writeStrongBinder(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
p.writeInt(1);
}
entity.intent.writeToParcel(p, 0);
p.writeString(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
p.writeInt(0);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
p.writeString(entity.intent.getComponent().getPackageName());
}
p.writeInt(0);
}
@SuppressLint("WrongConstant")
private void assembleBroadcastParcel() {
p2 = Parcel.obtain();
p2.writeInterfaceToken("android.app.IActivityManager");
p2.writeStrongBinder(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
p2.writeInt(1);
}
entity.intent2.setFlags(32);
entity.intent2.writeToParcel(p2, 0);
p2.writeString(null);
p2.writeStrongBinder(null);
p2.writeInt(-1);
p2.writeString(null);
p2.writeInt(0);
p2.writeStringArray(null);
p2.writeInt(-1);
p2.writeInt(0);
p2.writeInt(0);
p2.writeInt(0);
p2.writeInt(0);
}
private void assembleInstrumentationParcel() {
p3 = Parcel.obtain();
p3.writeInterfaceToken("android.app.IActivityManager");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
p3.writeInt(1);
}
entity.intent3.getComponent().writeToParcel(p3, 0);
p3.writeString(null);
p3.writeInt(0);
p3.writeInt(0);
p3.writeStrongBinder(null);
p3.writeStrongBinder(null);
p3.writeInt(0);
p3.writeString(null);
}
private void initAmsBinder() {
try {
Class<?> cls = Class.forName("android.app.ActivityManagerNative");
Object invoke = cls.getMethod("getDefault", new Class[0]).invoke(cls, new Object[0]);
Field field = invoke.getClass().getDeclaredField("mRemote");
field.setAccessible(true);
binder = (IBinder) field.get(invoke);
field.setAccessible(false);
Logger.v(Logger.TAG, "initAmsBinder: mRemote == iBinder " + binder);
} catch (Throwable th) {
binderManager.thrown(th);
}
if (binder == null) {
try {
binder = (IBinder) Class.forName("android.os.ServiceManager").getMethod(
"getService", new Class[]{String.class}).invoke(null,
new Object[]{"activity"});
} catch (Throwable th) {
binderManager.thrown(th);
}
}
}
class DaemonRunnable implements Runnable {
private WeakReference<DaemonMain> thiz;
private int index;
private DaemonRunnable(DaemonMain thiz, int index) {
this.thiz = new WeakReference<>(thiz);
this.index = index;
}
@Override
public void run() {
Logger.v(Logger.TAG, "[Thread] wait file lock start: " + index);
NativeKeepAlive.waitFileLock(thiz.get().entity.strArr[index]);
Logger.v(Logger.TAG, "[Thread] wait file lock finished");
thiz.get().startService();
thiz.get().broadcastIntent();
thiz.get().startInstrumentation();
Logger.v(Logger.TAG, "[Thread] start android finish");
}
}
}
可以看到通过反射的方式拿到了 Binder,也初始化了三个 Parcel,分别是服务、广播、还有 Instrumentation。果然和文件锁的原理差不多,它是对文件进行锁定操作了。
这里 Instrumentation 不做详细详解,可以去搜索相关源码进行了解:
http://developer.android.com/intl/zh-cn/reference/android/app/Instrumentation.html

大概意思是强大的跟踪 application 及 activity 生命周期的功能,用于 Android 应用测试框架中,被做为基类使用。
可以看到在 DaemonMain 中声明了 Main 方法,其中最显眼的就是这个 Process.killProcess(Process.myPid()); 这就令人奇怪了,为什么会手动结束进程呢,在结束之前又做了什么操作?
execute 方法分析
execute 方法中:
- 首先初始化 Binder
- 再初始化了 Parcel
- 紧接着调用 Native 方法中 setId,调用 setsid 函数的进程成为新的会话的领头进程,并与其父进程的会话组和进程组脱离
void keep_alive_set_sid(JNIEnv *env, jclass jclazz) {
setsid();
}
可以看见有一个 waitFileLock 方法,这是个阻塞方法,也就是读取文件状态是否被锁定。那么为什么在进程被杀死后还要 Process.killProcess(Process.myPid()) 呢?
原来 Process.killProcess 最终是调用 linux API kill() 发送 SIGKILL 信号,进行收到这个信息都会立即结束进程。
然而 Android 下不同的是 ActivityManager 一直监听者进程状态。如果发现进程被 kill,会立即重启进行,并重启之前状态对应的 Activity、Service、ContentProvider 等。
这就是为什么我们调用 Process.killProcess 后,发现程序是重启了,而不是被 kill 了。也在最大程度保证了进程被重新启动。
评论