保活分析
MAX 保活分析
可以引入module,和使用打包好的AAR形式。使用方法:
企业微信截图_16444873994239.png
注意:如果targetSdk为31 需要将module AndroidManifest.xml中AutoBootReceiver 增加android:exported="true"
首先讲一下使用文件锁来保活进程的思路,首先windows,linux是有一套文件的进程同步机制,一个进程可以给一个文件上锁,在操作完成之后再进行解锁,其他进程如果访问这个文件,会先检查一下这个文件是否有锁,如果有锁的话,代表别人正在操作,就会对文件延迟处理,那么当一个进程挂掉,他给文件加的锁也自然会消失。
企业微信截图_20220224151152.png
按照这个机制,可以想到进程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);
}
}
Pacel是在进程初始化的时候拿到的,而Binder是通过反射拿到的。
大概思路是这样的,当然在实现中还会有很多问题,比如:
1.binder transcate 无法启动Service,需要在广播中去启动,还需要创建一个parcel来启动广播,使用广播来拉起进程
2.在Native层中直接传递parcel,会导致监听不到进程被杀;改成传输u8数据解决了
现在我们来从头梳理一下,这个项目到底在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。
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,这里不过多介绍。
再接着attach方法中往下看
JavaDaemon.getInstance().fire(
base,
new Intent(base, DaemonService.class),
new Intent(base, DaemonReceiver.class),
new Intent(base, DaemonInstrumentation.class)
);
接着往下分析JavaDaemon中都干了什么,捋下来发现创建了三空文件,
image.png
private 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////8VAAAAYwBvAG0ALgBkAGEAZQBtAG8AbgAuAGsAZQBlAHAAYQBsAGkAdgBlADIAAAAyAAAAYwBvAG0ALgBrAGUAZQBwAGEAbABpAHYAZQAuAGQAYQBlAG0AbwBuAC4AYwBvAHIAZQAuAGMAbwBtAHAAbwBuAGUAbgB0AC4ARABhAGUAbQBvAG4AUgBlAGMAZQBpAHYAZQByAAAAAAAAAAAAAAAAAAAAAAAAAAAA/v////////8AAAAAAAAAAAAAAAAAAAAAAQAAAP////8AAAAA//////////8AAAAA/////xUAAABjAG8AbQAuAGQAYQBlAG0AbwBuAC4AawBlAGUAcABhAGwAaQB2AGUAMgAAADkAAABjAG8AbQAuAGsAZQBlAHAAYQBsAGkAdgBlAC4AZABhAGUAbQBvAG4ALgBjAG8AcgBlAC4AYwBvAG0AcABvAG4AZQBuAHQALgBEAGEAZQBtAG8AbgBJAG4AcwB0AHIAdQBtAGUAbgB0AGEAdABpAG8AbgAAAAAAAAAAAAAAAAAAAAAAAAD+/////////wAAAAAAAAAAAAAAAAAAAAA= --application --nice-name=daemon &]
那这些数据到底是什么意思?前三条好理解,就是配置了linux 环境变量,对应的数据分别是publicSourceDir,nativeLibraryDir,nativeLibraryDir,那第四条实在是看不懂,让我大胆猜测一下,通过下文代码得知,会不会是将DaemonMain类在底层去运行呢,而DaemonMain是和进程每个进程单独持有的,也就是说每个进程都声明到了底层,然后具体类通过这个类去实现。
有了猜测去实验,发现果然,通过debug方式发现代码无法走到DaemonMain方法中,而查看log发现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方法中首先初始化Binder,再初始化了Parcel,紧接着调用Native方法中setId,调用setsid函数的进程成为新的会话的领头进程,并与其父进程的会话组和进程组脱离
void keep_alive_set_sid(JNIEnv *env, jclass jclazz) {
setsid();
}
可以看见有一个waitFileLock 方法,这是个阻塞方法,也就是读取文件状态是否被锁定,那么为什么在进程被杀死后还要
Process.killProcess(Process.myPid())呢,
原来Process.killProcess 最终是调用 linuxAPI kill() 发送 SIGKILL 信号,进行收到这个信息都会立即结束进程。
然而Android 下不同的是 ActivityManager 一直监听者进程状态。如果发现进程被kill,会立即重启进行,并重启之前状态对应的Activity、Service、ContentProvider等。
这就是为什么我们调用Process.killProcess后,发现程序是重启了,而不是被kill了
也在最大程度保证了进程被重新启动。