DroidPlugin代码分析(四) 进程管理

来源:互联网 发布:淘宝垃圾短信 编辑:程序博客网 时间:2024/04/29 20:46

之所以单列一篇写进程管理,是因为看到注释上写“这是一个复杂的进程管理程序”,但是仔细看了一下好像也没那么“复杂”...

这一篇通过分析代码试图搞清楚以下3个问题:

• 插件进程是如何被hook住的?

• 插件进程die是如何被检测到的?

• 插件进程是如何被管理的?

一、插件进程是如何被hook住的?

在写宿主程序的时候,我们知道需要在ApplicationonCreate()attachBaseContext()里调用PluginHelperAPI来安装hook。但是,插件程序本身是不会调用这些API的,那么被启动的插件程序是如何被hook住的呢?

首先我们要再次回顾一下activity启动的一些细节,图比较大切成了两张,缩进表示该方法是在上一级方法里调用的子方法。先看左半边图:


比较简单,宿主在一个新进程里启动插件的时候,AMS会向插件进程的ActivityThread发起两个调用:bindApplication()scheduleLaunchAcitivity()再看右半边图:


这张图主要描述了这两个调用具体干了什么,实际上它们只是向ActivityThreadmH里发送了两个消息,真正干活的是mH(看过第二篇的可能有印象,我们把mH里面的mCallback替换成了我们的PluginCallback)。

看看bindApplication()具体做了哪些事情:

• 调用getPackageInfoNoCheck()创建一个LoadedApk对象,注意,由于AMS并不知道关于插件的事情,所以这里加载的还是宿主apk!所以实际上插件是启动不起来的,具体怎么处理的后面会介绍。创建的LoadedApk会放到一个mPackagesmap中。

• 创建Instrumentation,调用LoadedApk.makeApplication()创建Application,注意只是创建,并没有调用ApplicationonCreate()。创建Application会放到一个mAllApplicationslist中。

再看看scheduleLaunchActivity()具体做了哪些事情:

• 通过Instrumentation加载、创建activity对象,这里会用到class loader

• 从mPackages中取出之前创建的LoadedApk,再次调用它的makeApplication()方法。这次调用和上次不同,由于Application已经创建过了所以会直接拿出来用,另外传入的第二个参数instrumentation不为空,因此会调用ApplicationonCreate()方法。

 

说了这么多,都只是Android默认的运行流程。那么DroidPlugin是如何让插件被加载和启动的呢?先上一张图描述一下概况,以免后面分析代码的时候会晕。我们和上一张图对比一下看主要区别在哪里:


其实说穿了也很简单,我们不是在PluginCallbackhookhandleLaunchActivity()方法吗?那就在这个hook里手动加载一下插件apk,创建LoadedApk对象并调用其makeApplication()方法,创建Application并调用其onCreate()。在这一切都做完以后,继续往下执行,调用宿主ApplicationonCreate()

也就是说,其实创建了两个Application对象,先调用插件ApplicationonCreate(),再调用宿主ApplicationonCreate()。这样就回答了文章开头提出的第一个问题了:在宿主ApplicationonCreate()里,我们是安装了所有hook的,这样插件apk就也被hook住啦~~

思路已经清楚了,下面分析代码,重点看一下PluginProcessManager2个关键APIpreLoadApk()preMakeApplication()。现在明白为什么这两个方法要带“pre”前缀了,因为新创建的Application确实是被先调用的呀。

preLoadApk()大家可能还有印象,是在PluginCallback里曾经露过脸,看一下具体代码:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public static void preLoadApk(Context hostContext, ComponentInfo pluginInfo) throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, PackageManager.NameNotFoundException, ClassNotFoundException {  
  2.     boolean found = false;  
  3.     synchronized (sPluginLoadedApkCache) {  
  4.         Object object = ActivityThreadCompat.currentActivityThread();  
  5.         if (object != null) {  
  6.             Object mPackagesObj = FieldUtils.readField(object, "mPackages");  
  7.             Object containsKeyObj = MethodUtils.invokeMethod(mPackagesObj, "containsKey", pluginInfo.packageName)  
  8.             if (containsKeyObj instanceof Boolean && !(Boolean) containsKeyObj) {  
  9.                 final Object loadedApk;  
  10.                 ... ...  
  11.                 loadedApk = MethodUtils.invokeMethod(object, "getPackageInfoNoCheck", pluginInfo.applicationInfo, CompatibilityInfoCompat.DEFAULT_COMPATIBILITY_INFO());  
  12.     //-------------------------------------------------------------------------------------  
  13.         String optimizedDirectory = PluginDirHelper.getPluginDalvikCacheDir(hostContext, pluginInfo.packageName);  
  14.         String libraryPath = PluginDirHelper.getPluginNativeLibraryDir(hostContext, pluginInfo.packageName);  
  15.         String apk = pluginInfo.applicationInfo.publicSourceDir;  
  16.         ... ...  
  17.         classloader = new PluginClassLoader(apk, optimizedDirectory, libraryPath, ClassLoader.getSystemClassLoader());  
  18.         synchronized (loadedApk) {  
  19.             FieldUtils.writeDeclaredField(loadedApk, "mClassLoader", classloader);  
  20.         }  
  21.         ... ...  
  22.         found = true;  
  23.     //-------------------------------------------------------------------------------------  
  24.     if (found) {  
  25.         PluginProcessManager.preMakeApplication(hostContext, pluginInfo);  
  26.     }  
  27. }  

根据代码的功能大致分为3段:

1. 判断ActivityThreadmPackages字段是否包含插件包,如果不包含,则调用getPackageInfoNoCheck()加载apk,获取LoadedApk对象。

mPackagesActivityThread里的一个mapkey是包名,value是对应的LoadedApk对象。getPackageInfoNoCheck()会创建一个新的LoadedApk对象,里面包含了apk的所有信息,有了这些信息以后就可以启动插件程序了。这个对象会被放进mPackages中待日后使用。

2. 替换掉LoadedApk对象的mClassLoader字段

LoadedApkclass loader最终会被传给Instrumentation,用来加载插件apk中的类。默认的class loader是一个PathClassLoader,这里替换成了PluginClassLoader,目的和之前一样是为了解决奇酷手机support V4库加载的问题。

3. 调用preMakeApplication(),下面节选了preMakeApplication()的代码:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. private static void preMakeApplication(Context hostContext, ComponentInfo pluginInfo) {  
  2.     try {  
  3.         final Object loadedApk = sPluginLoadedApkCache.get(pluginInfo.packageName);  
  4.         if (loadedApk != null) {  
  5.             Object mApplication = FieldUtils.readField(loadedApk, "mApplication");  
  6.             if (mApplication != null) {  
  7.                 return;  
  8.             }  
  9.         ... ...  
  10.         MethodUtils.invokeMethod(loadedApk, "makeApplication"false, ActivityThreadCompat.getInstrumentation());  
  11.         ... ...  
  12. }  

首先判断LoadedApkmApplication字段是否为空,这段感觉有点多余,因为makeApplication()方法的开头也会先判断一下的。然后就是调用makeApplication()方法啦,这样插件apkApplication就被创建出来了,onCreate()也会被执行。 

二、插件进程die是如何被检测到的?

上面分析过了,插件进程启动的时候,也会创建宿主Application并调用其onCreate(),因此会调用到PluginHelperapplicationOnCreate()方法。

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public void applicationOnCreate(final Context baseContext) {  
  2.     mContext = baseContext;  
  3.     initPlugin(baseContext);  
  4. }  

initPlugin()里执行了下面两个步骤:

• 添加一个ServiceConnectionPluginHelper实现了ServiceConnection接口)

• 调用PluginManagerinit()方法去连接PluginManagerService

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. private void initPlugin(Context baseContext) {  
  2.         ... ...  
  3.     PluginManager.getInstance().addServiceConnection(PluginHelper.this);  
  4.     PluginManager.getInstance().init(baseContext);  
  5.         ... ...  
  6. }  
[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // PluginManager.java  
  2. public void init(Context hostContext) {  
  3.     mHostContext = hostContext;  
  4.     connectToService();  
  5. }  

看一下connectToService()

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public void connectToService() {  
  2.     if (mPluginManager == null) {  
  3.         try {  
  4.             Intent intent = new Intent(mHostContext, PluginManagerService.class);  
  5.             intent.setPackage(mHostContext.getPackageName());  
  6.             mHostContext.startService(intent);  
  7.             mHostContext.bindService(intent, this, Context.BIND_AUTO_CREATE);  
  8.         } catch (Exception e) {  
  9.             Log.e(TAG, "connectToService", e);  
  10.         }  
  11.     }  
  12. }  

首先startService(),然后再bindService(),这样即使解绑了,服务还是可以继续保持运行。连接上服务以后,会调用PluginManageronServiceConnected(),这一步比较关键:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public void onServiceConnected(final ComponentName componentName, final IBinder iBinder) {  
  2.     ... ...  
  3.     mPluginManager.waitForReady();  
  4.     mPluginManager.registerApplicationCallback(new IApplicationCallback.Stub() {  
  5.         @Override  
  6.         public Bundle onCallback(Bundle extra) throws RemoteException {  
  7.             return extra;  
  8.         }  
  9.     });  
  10.     ... ...  
  11. }  

看到没,这里注册了一个ApplicationCallback,这是一个自定义的AIDL远程调用接口,会调用到远端的IPluginManagerImpl,进而调用进BaseActivityManagerService

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public boolean registerApplicationCallback(int callingPid, int callingUid, IApplicationCallback callback) {  
  2.     return mRemoteCallbackList.register(callback, new ProcessCookie(callingPid, callingUid));  
  3. }  

注意,这可不是一个普通的list哦,这是一个RemoteCallbackList,在binder对端死掉的时候,会收到一个通知,这样就能知道插件进程是死是活了,具体的处理放到onProcessDied()里去实现。看一下这个类的实现:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. private class MyRemoteCallbackList extends RemoteCallbackList<IApplicationCallback> {  
  2.     @Override  
  3.     public void onCallbackDied(IApplicationCallback callback, Object cookie) {  
  4.         super.onCallbackDied(callback, cookie);  
  5.         if (cookie != null && cookie instanceof ProcessCookie) {  
  6.             ProcessCookie p = (ProcessCookie) cookie;  
  7.             onProcessDied(p.pid, p.uid);  
  8.         }  
  9.     }  
  10. }  

最后我们看一下RemoteCallbackListregister()方法,在注册callback的时候会调用binderlinkToDeath,这样当对端死掉的时候就能收到通知啦,就是这么简单:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public boolean register(E callback, Object cookie) {  
  2.     synchronized (mCallbacks) {  
  3.         if (mKilled) {  
  4.             return false;  
  5.         }  
  6.         IBinder binder = callback.asBinder();  
  7.         try {  
  8.             Callback cb = new Callback(callback, cookie);  
  9.             binder.linkToDeath(cb, 0);  
  10.             mCallbacks.put(binder, cb);  
  11.             return true;  
  12.         } catch (RemoteException e) {  
  13.             return false;  
  14.         }  
  15.     }  
  16. }  

至此,插件进程die如何被检测到的问题就搞清楚了,如下图:


三、插件进程是如何被管理的?

目前DroidPlugin的进程管理还是比较粗糙的,没有考虑task affinity,策略也比较简单粗暴。

主要逻辑实现在MyActivityManagerService里,代码就不贴了比较简单,文字总结一下。

1. MyActivityManagerService里维护了两个进程列表:

    • 一个叫StaticProcessList,包含了AndroidManifest.xml里注册的所有进程

    • 一个叫RunningProcessList,包含了所有已经在运行的进程

2. 每次要启动插件时,首先在RunningProcessList里查找看是否有符合条件的进程:

    • 如果该进程加载过这个包,并且进程名与插件包一致,返回直接使用

    • 否则,遍历该进程加载的所有包,如果与插件包签名一致,返回直接使用

    • 使用这种方式,相同签名的多个插件包会运行在同一个进程中

3. 如果RunningProcessList里找不到,就到StaticProcessList里去查找看有没有符合条件的进程:

    • 如果该进程在运行,但是是个空进程,也就是没有启动任何插件包,返回直接使用

    • 如果该进程在运行但进程名为空,且该进程加载过这个包或者与插件包签名一致,返回直接使用

    • 如果该进程没有运行,返回使用之

找到合适的进程以后,还要选出未被占用stub组件(activity/service/provider),然后把该组件的targetProcessName设置为目标进程。

 

另外还有个问题:如果进程不够用了怎么办?需要设计一个进程回收策略。

每次selectStubXXX()activity或者service调用onDestroy()、以及进程die的时候,都会调用runProcessGC()回收进程资源(好像少了个判断?进程数量超过一个阈值的时候才需要回收吧)。如果是插件进程(非宿主进程),且不是持久进程:

    • 按进程优先级排序,>=IMPORTANCE_SERVICE优先级的杀掉(数字越低优先级越高)

    • 没有任何activityserviceprovider的空进程,杀掉

    • 没有activity,只有service的进程,获取该进程的所有服务,调用stopSelf()停掉服务


转载自:http://blog.csdn.net/turkeycock/article/details/51298338

0 0
原创粉丝点击