Android开发之散记

保活分析

2022-02-24  本文已影响0人  达布遛

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

image.png
大概意思是强大的跟踪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了

也在最大程度保证了进程被重新启动。

上一篇 下一篇

猜你喜欢

热点阅读