zoukankan      html  css  js  c++  java
  • Android插件化 学习

    原文:http://weishu.me/2016/01/28/understand-plugin-framework-overview/

    插件化技术听起来高深莫测,实际上要解决的就是两个问题:

    1. 代码加载
    2. 资源加载

    代码加载

    类的加载可以使用Java的ClassLoader机制,但是对于Android来说,并不是说类加载进来就可以用了,很多组件都是有“生命”的;因此对于这些有血有肉的类,必须给它们注入活力,也就是所谓的组件生命周期管理

    另外,如何管理加载进来的类也是一个问题。假设多个插件依赖了相同的类,是抽取公共依赖进行管理还是插件单独依赖?这就是ClassLoader的管理问题

    资源加载

    资源加载方案大家使用的原理都差不多,都是用AssetManager的隐藏方法addAssetPath;但是,不同插件的资源如何管理?是公用一套资源还是插件独立资源?共用资源如何避免资源冲突?对于资源加载,有的方案共用一套资源并采用资源分段机制解决冲突(要么修改aapt要么添加编译插件);有的方案选择独立资源,不同插件管理自己的资源。

    --------------------------------------------分割线------------------------------------------------

    Activity生命周期管理

    瞒天过海——启动不在AndroidManifest.xml中声明的Activity

    原文:http://weishu.me/2016/03/21/understand-plugin-framework-activity-management/

    简要分析

    通过上文的分析,我们已经对Activity的启动过程了如指掌了;就让我们干点坏事吧 :D

    对与『必须在AndroidManifest.xml中显示声明使用的Activity』这个问题,上文给出了思路——瞒天过海;我们可以在AndroidManifest.xml里面声明一个替身Activity,然后在合适的时候把这个假的替换成我们真正需要启动的Activity就OK了。

    那么问题来了,『合适的时候』到底是什么时候?在前文Hook机制之动态代理中我们提到过Hook过程最重要的一步是寻找Hook点;如果是在同一个进程,startActivity到Activity真正启动起来这么长的调用链,我们随便找个地方Hook掉就完事儿了;但是问题木有这么简单。

    Activity启动过程中很多重要的操作(正如上文分析的『必须在AndroidManifest.xml中显式声明要启动的Activity』)都不是在App进程里面执行的,而是在AMS所在的系统进程system_server完成,由于进程隔离的存在,我们对别的进程无能为力;所以这个Hook点就需要花点心思了。

    这时候Activity启动过程的知识就派上用场了;虽然整个启动过程非常复杂,但其实一张图就能总结:

    简要启动过程

    先从App进程调用startActivity;然后通过IPC调用进入系统进程system_server,完成Activity管理以及一些校检工作,最后又回到了APP进程完成真正的Activioty对象创建。

    由于这个检验过程是在AMS进程完成的,我们对system_server进程里面的操作无能为力,只有在我们APP进程里面执行的过程才是有可能被Hook掉的,也就是第一步和第三步;具体应该怎么办呢?

    既然需要一个显式声明的Activity,那就声明一个!可以在第一步假装启动一个已经在AndroidManifest.xml里面声明过的替身Activity,让这个Activity进入AMS进程接受检验;最后在第三步的时候换成我们真正需要启动的Activity;这样就成功欺骗了AMS进程,瞒天过海!

    说到这里,是不是有点小激动呢?我们写个demo验证一下:『启动一个并没有在AndroidManifest.xml中显示声明的Activity』

    实战过程

    具体来说,我们打算实现如下功能:在MainActivity中启动一个并没有在AndroidManifest.xml中声明的TargetActivity;按照上文分析,我们需要声明一个替身Activity,我们叫它StubActivity;

    那么,我们的AndroidManifest.xml如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.weishu.intercept_activity.app">

    <application
    android:allowBackup="true"
    android:label="@string/app_name"
    android:icon="@mipmap/ic_launcher"
    >

    <activity android:name=".MainActivity">
    <intent-filter>
    <action android:name="android.intent.action.MAIN"/>
    <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
    </activity>

    <!-- 替身Activity, 用来欺骗AMS -->
    <activity android:name=".StubActivity"/>


    </application>

    </manifest>

    OK,那么我们启动TargetActivity很简单,就是个startActivity调用的事:

    1
    startActivity(new Intent(MainActivity.this, TargetActivity.class));

    如果你直接这么运行,肯定会直接抛出ActivityNotFoundException然后直接退出;我们接下来要做的就是让这个调用成功启动TargetActivity。

    1.狸猫换太子——使用替身Activity绕过AMS

    由于AMS进程会对Activity做显式声明验证,因此在
    启动Activity的控制权转移到AMS进程之前,我们需要想办法临时把TargetActivity替换成替身StubActivity;在这之间有很长的一段调用链,我们可以轻松Hook掉;选择什么地方Hook是一个很自由的事情,但是Hook的步骤越后越可靠——Hook得越早,后面的调用就越复杂,越容易出错。

    我们可以选择在进入AMS进程的入口进行Hook,具体来说也就是Hook AMS在本进程的代理对象ActivityManagerNative。如果你不知道如何Hook掉这个AMS的代理对象,请查阅我之前的文章 Hook机制之AMS&PMS

    我们Hook掉ActivityManagerNative对于startActivity方法的调用,替换掉交给AMS的intent对象,将里面的TargetActivity的暂时替换成已经声明好的替身StubActivity;这种Hook方式 前文 讲述的很详细,不赘述;替换的关键代码如下:

    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
    if ("startActivity".equals(method.getName())) {
    // 只拦截这个方法
    // 替换参数, 任你所为;甚至替换原始Activity启动别的Activity偷梁换柱
    // API 23:
    // public final Activity startActivityNow(Activity parent, String id,
    // Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state,
    // Activity.NonConfigurationInstances lastNonConfigurationInstances) {

    // 找到参数里面的第一个Intent 对象

    Intent raw;
    int index = 0;

    for (int i = 0; i < args.length; i++) {
    if (args[i] instanceof Intent) {
    index = i;
    break;
    }
    }
    raw = (Intent) args[index];

    Intent newIntent = new Intent();

    // 这里包名直接写死,如果再插件里,不同的插件有不同的包 传递插件的包名即可
    String targetPackage = "com.weishu.intercept_activity.app";

    // 这里我们把启动的Activity临时替换为 StubActivity
    ComponentName componentName = new ComponentName(targetPackage, StubActivity.class.getCanonicalName());
    newIntent.setComponent(componentName);

    // 把我们原始要启动的TargetActivity先存起来
    newIntent.putExtra(HookHelper.EXTRA_TARGET_INTENT, raw);

    // 替换掉Intent, 达到欺骗AMS的目的
    args[index] = newIntent;

    Log.d(TAG, "hook success");
    return method.invoke(mBase, args);

    }

    return method.invoke(mBase, args);

    通过这个替换过程,在ActivityManagerNative的startActivity调用之后,system_server端收到Binder驱动的消息,开始执行ActivityManagerService里面真正的startActivity方法;这时候AMS看到的intent参数里面的组件已经是StubActivity了,因此可以成功绕过检查,这时候如果不做后面的Hook,直接调用

    1
    startActivity(new Intent(MainActivity.this, TargetActivity.class));

    也不会出现上文的ActivityNotFoundException

    2.借尸还魂——拦截Callback恢复真身

    行百里者半九十。现在我们的startActivity启动一个没有显式声明的Activity已经不会抛异常了,但是要真正正确地把TargetActivity启动起来,还有一些事情要做。其中最重要的一点是,我们用替身StubActivity临时换了TargetActivity,肯定需要在『合适的』时候替换回来;接下来我们就完成这个过程。

    在AMS进程里面我们是没有办法换回来的,因此我们要等AMS把控制权交给App所在进程,也就是上面那个『Activity启动过程简图』的第三步。AMS进程转移到App进程也是通过Binder调用完成的,承载这个功能的Binder对象是IApplicationThread;在App进程它是Server端,在Server端接受Binder远程调用的是Binder线程池,Binder线程池通过Handler将消息转发给App的主线程;(我这里不厌其烦地叙述Binder调用过程,希望读者不要反感,其一加深印象,其二懂Binder真的很重要)我们可以在这个Handler里面将替身恢复成真身

    这里不打算讲述Handler 的原理,我们简单看一下Handler是如何处理接收到的Message的,如果我们能拦截这个Message的接收过程,就有可能完成替身恢复工作;Handler类的dispathMesage如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
    handleCallback(msg);
    } else {
    if (mCallback != null) {
    if (mCallback.handleMessage(msg)) {
    return;
    }
    }
    handleMessage(msg);
    }
    }

    从这个方法可以看出来,Handler类消息分发的过程如下:

    1. 如果传递的Message本身就有callback,那么直接使用Message对象的callback方法;
    2. 如果Handler类的成员变量mCallback存在,那么首先执行这个mCallback回调;
    3. 如果mCallback的回调返回true,那么表示消息已经成功处理;直接结束。
    4. 如果mCallback的回调返回false,那么表示消息没有处理完毕,会继续使用Handler类的handleMessage方法处理消息。

    那么,ActivityThread中的Handler类H是如何实现的呢?H的部分源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public void handleMessage(Message msg) {
    if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
    switch (msg.what) {
    case LAUNCH_ACTIVITY: {
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
    ActivityClientRecord r = (ActivityClientRecord)msg.obj;

    r.packageInfo = getPackageInfoNoCheck(
    r.activityInfo.applicationInfo, r.compatInfo);
    handleLaunchActivity(r, null);
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    } break;
    case RELAUNCH_ACTIVITY: {
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityRestart");
    ActivityClientRecord r = (ActivityClientRecord)msg.obj;
    handleRelaunchActivity(r);
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

    // 以下略
    }
    }

    可以看到H类仅仅重载了handleMessage方法;通过dispathMessage的消息分发过程得知,我们可以拦截这一过程:把这个H类的mCallback替换为我们的自定义实现,这样dispathMessage就会首先使用这个自定义的mCallback,然后看情况使用H重载的handleMessage

    这个Handler.Callback是一个接口,我们可以使用动态代理或者普通代理完成Hook,这里我们使用普通的静态代理方式;创建一个自定义的Callback类:

    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
    /* package */ class ActivityThreadHandlerCallback implements Handler.Callback {

    Handler mBase;

    public ActivityThreadHandlerCallback(Handler base) {
    mBase = base;
    }

    @Override
    public boolean handleMessage(Message msg) {

    switch (msg.what) {
    // ActivityThread里面 "LAUNCH_ACTIVITY" 这个字段的值是100
    // 本来使用反射的方式获取最好, 这里为了简便直接使用硬编码
    case 100:
    handleLaunchActivity(msg);
    break;
    }

    mBase.handleMessage(msg);
    return true;
    }

    private void handleLaunchActivity(Message msg) {
    // 这里简单起见,直接取出TargetActivity;

    Object obj = msg.obj;
    // 根据源码:
    // 这个对象是 ActivityClientRecord 类型
    // 我们修改它的intent字段为我们原来保存的即可.
    /* switch (msg.what) {
    / case LAUNCH_ACTIVITY: {
    / Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
    / final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
    /
    / r.packageInfo = getPackageInfoNoCheck(
    / r.activityInfo.applicationInfo, r.compatInfo);
    / handleLaunchActivity(r, null);
    */

    try {
    // 把替身恢复成真身
    Field intent = obj.getClass().getDeclaredField("intent");
    intent.setAccessible(true);
    Intent raw = (Intent) intent.get(obj);

    Intent target = raw.getParcelableExtra(HookHelper.EXTRA_TARGET_INTENT);
    raw.setComponent(target.getComponent());

    } catch (NoSuchFieldException e) {
    e.printStackTrace();
    } catch (IllegalAccessException e) {
    e.printStackTrace();
    }
    }
    }

    这个Callback类的使命很简单:把替身StubActivity恢复成真身TargetActivity;有了这个自定义的Callback之后我们需要把ActivityThread里面处理消息的Handler类H的的mCallback修改为自定义callback类的对象:

    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
    // 先获取到当前的ActivityThread对象
    Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
    Field currentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
    currentActivityThreadField.setAccessible(true);
    Object currentActivityThread = currentActivityThreadField.get(null);

    // 由于ActivityThread一个进程只有一个,我们获取这个对象的mH
    Field mHField = activityThreadClass.getDeclaredField("mH");
    mHField.setAccessible(true);
    Handler mH = (Handler) mHField.get(currentActivityThread);

    // 设置它的回调, 根据源码:
    // 我们自己给他设置一个回调,就会替代之前的回调;

    // public void dispatchMessage(Message msg) {
    // if (msg.callback != null) {
    // handleCallback(msg);
    // } else {
    // if (mCallback != null) {
    // if (mCallback.handleMessage(msg)) {
    // return;
    // }
    // }
    // handleMessage(msg);
    // }
    // }

    Field mCallBackField = Handler.class.getDeclaredField("mCallback");
    mCallBackField.setAccessible(true);

    mCallBackField.set(mH, new ActivityThreadHandlerCallback(mH));

    到这里,我们已经成功地绕过AMS,完成了『启动没有在AndroidManifest.xml中显式声明的Activity』的过程;瞒天过海,这种玩弄系统与股掌之中的快感你们能体会到吗?

    3.僵尸or活人?——能正确收到生命周期回调吗

    虽然我们完成了『启动没有在AndroidManifest.xml中显式声明的Activity 』,但是启动的TargetActivity是否有自己的生命周期呢,我们还需要额外的处理过程吗?

    实际上TargetActivity已经是一个有血有肉的Activity了:它具有自己正常的生命周期;可以运行Demo代码验证一下。

    这个过程是如何完成的呢?我们以onDestroy为例简要分析一下:

    从Activity的finish方法开始跟踪,最终会通过ActivityManagerNative到AMS然后接着通过ApplicationThread到ActivityThread,然后通过H转发消息到ActivityThread的handleDestroyActivity,接着这个方法把任务交给performDestroyActivity完成。

    在真正分析这个方法之前,需要说明一点的是:不知读者是否感受得到,App进程与AMS交互几乎都是这么一种模式,几个角色 ActivityManagerNative, ApplicationThread, ActivityThread以及Handler类H分工明确,读者可以按照这几个角色的功能分析AMS的任何调用过程,屡试不爽;这也是我的初衷——希望分析插件框架的过程中能帮助深入理解Android Framework。

    好了继续分析performDestroyActivity,关键代码如下:

    1
    2
    3
    4
    5
    ActivityClientRecord r = mActivities.get(token);

    // ...略

    mInstrumentation.callActivityOnDestroy(r.activity);

    这里通过mActivities拿到了一个ActivityClientRecord,然后直接把这个record里面的Activity交给Instrument类完成了onDestroy的调用。

    在我们这个demo的场景下,r.activity是TargetActivity还是StubActivity?按理说,由于我们欺骗了AMSAMS应该只知道StubActivity的存在,它压根儿就不知道TargetActivity是什么,为什么它能正确完成对TargetActivity生命周期的回调呢?

    一切的秘密在token里面。AMSActivityThread之间对于Activity的生命周期的交互,并没有直接使用Activity对象进行交互,而是使用一个token来标识,这个token是binder对象,因此可以方便地跨进程传递。Activity里面有一个成员变量mToken代表的就是它,token可以唯一地标识一个Activity对象,它在Activity的attach方法里面初始化;

    AMS处理Activity的任务栈的时候,使用这个token标记Activity,因此在我们的demo里面,AMS进程里面的token对应的是StubActivity,也就是AMS还在傻乎乎地操作StubActivity(关于这一点,你可以dump出任务栈的信息,可以观察到dump出的确实是StubActivity)。但是在我们App进程里面,token对应的却是TargetActivity!因此,在ActivityThread执行回调的时候,能正确地回调到TargetActivity相应的方法。

    为什么App进程里面,token对应的是TargetActivity呢?

    回到代码,ActivityClientRecord是在mActivities里面取出来的,确实是根据token取;那么这个token是什么时候添加进去的呢?我们看performLaunchActivity就完成明白了:它通过classloader加载了TargetActivity,然后完成一切操作之后把这个activity添加进了mActivities!另外,在这个方法里面我们还能看到对Ativityattach方法的调用,它传递给了新创建的Activity一个token对象,而这个token是在ActivityClientRecord构造函数里面初始化的。

    至此我们已经可以确认,通过这种方式启动的Activity有它自己完整而独立的生命周期!

    小节

    本文讲述了『启动一个并没有在AndroidManifest.xml中显示声明的Activity』的解决办法,我们成功地绕过了Android的这个限制,这个是插件Activity管理技术的基础;但是要做到启动一个插件Activity问题远没有这么简单。

    在本文所述例子中,TargetActivity与StubActivity存在于同一个Apk,因此系统的ClassLoader能够成功加载并创建TargetActivity的实例。但是在实际的插件系统中,要启动的目标Activity肯定存在于一个单独的文件中,系统默认的ClassLoader无法加载插件中的Activity类——系统压根儿就不知道要加载的插件在哪,谈何加载?因此还有一个很重要的问题需要处理:我们要完成插件系统中类的加载,这可以通过自定义ClassLoader实现。

    解决了『启动没有在AndroidManifest.xml中显式声明的,并且存在于外部文件中的Activity』的问题,插件系统对于Activity的管理才算得上是一个完全体。

    篇幅所限,欲知后事如何,请听下回分解!

    --------------------------------------------分割线------------------------------------------------

    插件加载机制

    原文:http://weishu.me/2016/04/05/understand-plugin-framework-classloader/

    思路分析

    Android系统使用了ClassLoader机制来进行Activity等组件的加载;apk被安装之后,APK文件的代码以及资源会被系统存放在固定的目录(比如/data/app/package_name/base-1.apk )系统在进行类加载的时候,会自动去这一个或者几个特定的路径来寻找这个类;但是系统并不知道存在于插件中的Activity组件的信息(插件可以是任意位置,甚至是网络,系统无法提前预知),因此正常情况下系统无法加载我们插件中的类;因此也没有办法创建Activity的对象,更不用谈启动组件了。

    解决这个问题有两个思路,要么全盘接管这个类加载的过程;要么告知系统我们使用的插件存在于哪里,让系统帮忙加载;这两种方式或多或少都需要干预这个类加载的过程。

    1.激进方案:Hook掉ClassLoader,自己操刀

    从上述分析中我们得知,在获取LoadedApk的过程中使用了一份缓存数据;这个缓存数据是一个Map,从包名到LoadedApk的一个映射。正常情况下,我们的插件肯定不会存在于这个对象里面;但是如果我们手动把我们插件的信息添加到里面呢?系统在查找缓存的过程中,会直接命中缓存!进而使用我们添加进去的LoadedApk的ClassLoader来加载这个特定的Activity类!这样我们就能接管我们自己插件类的加载过程了!

    。。。、、、

    2.保守方案:委托系统,让系统帮忙加载

    我们再次搬出ActivityThread中加载Activity类的代码:

    1
    2
    3
    4
    5
    java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
    activity = mInstrumentation.newActivity(
    cl, component.getClassName(), r.intent);
    StrictMode.incrementExpectedActivityCount(activity.getClass());
    r.intent.setExtrasClassLoader(cl);

    我们知道 这个r.packageInfo中的r是通过getPackageInfoNoCheck获取到的;在『激进方案』中我们把插件apk手动添加进缓存,采用自己加载办法解决;如果我们不干预这个过程,导致无法命中mPackages中的缓存,会发生什么?

    查阅 getPackageInfo方法如下:

    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
    private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
    ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
    boolean registerPackage) {
    final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
    synchronized (mResourcesManager) {
    WeakReference<LoadedApk> ref;
    if (differentUser) {
    // Caching not supported across users
    ref = null;
    } else if (includeCode) {
    ref = mPackages.get(aInfo.packageName);
    } else {
    ref = mResourcePackages.get(aInfo.packageName);
    }

    LoadedApk packageInfo = ref != null ? ref.get() : null;
    if (packageInfo == null || (packageInfo.mResources != null
    && !packageInfo.mResources.getAssets().isUpToDate())) {
    packageInfo =
    new LoadedApk(this, aInfo, compatInfo, baseLoader,
    securityViolation, includeCode &&
    (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);

    // 略
    }
    }

    可以看到,没有命中缓存的情况下,系统直接new了一个LoadedApk;注意这个构造函数的第二个参数aInfo,这是一个ApplicationInfo类型的对象。在『激进方案』中我们为了获取独立插件的ApplicationInfo花了不少心思;那么如果不做任何处理这里传入的这个aInfo参数是什么?

    追本溯源不难发现,这个aInfo是从我们的替身StubActivity中获取的!而StubActivity存在于宿主程序中,所以,这个aInfo对象代表的实际上就是宿主程序的Application信息!

    我们知道,接下来会使用new出来的这个LoadedApk的getClassLoader()方法获取到ClassLoader来对插件的类进行加载;而获取到的这个ClassLoader是宿主程序使用的ClassLoader,因此现在还无法加载插件的类;那么,我们能不能让宿主的ClasLoader获得加载插件类的能力呢?;如果我们告诉宿主使用的ClassLoader插件使用的类在哪里,就能帮助他完成加载!

    宿主的ClassLoader在哪里,是唯一的吗?

    上面说到,我们可以通过告诉宿主程序的ClassLoader插件使用的类,让宿主的ClasLoader完成对于插件类的加载;那么问题来了,我们如何获取到宿主的ClassLoader?宿主程序使用的ClasLoader默认情况下是全局唯一的吗?

    答案是肯定的。

    因为在FrameWork中宿主程序也是使用LoadedApk表示的,如同Activity启动是加载Activity类一样,宿主中的类也都是通过LoadedApk的getClassLoader()方法得到的ClassLoader加载的;由类加载机制的『双亲委派』特性,只要有一个应用程序类由某一个ClassLoader加载,那么它引用到的别的类除非父加载器能加载,否则都是由这同一个加载器加载的(不遵循双亲委派模型的除外)。

    表示宿主的LoadedApk在Application类中有一个成员变量mLoadedApk,而这个变量是从ContextImpl中获取的;ContextImpl重写了getClassLoader方法,因此我们在Context环境中直接getClassLoader()获取到的就是宿主程序唯一的ClassLoader

    LoadedApk的ClassLoader到底是什么?

    现在我们确保了『使用宿主ClassLoader帮助加载插件类』可行性;那么我们应该如何完成这个过程呢?

    知己知彼,百战不殆。

    不论是宿主程序还是插件程序都是通过LoadedApk的getClassLoader()方法返回的ClassLoader进行类加载的,返回的这个ClassLoader到底是个什么东西??这个方法源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public ClassLoader getClassLoader() {
    synchronized (this) {
    if (mClassLoader != null) {
    return mClassLoader;
    }

    if (mIncludeCode && !mPackageName.equals("android")) {
    // 略...
    mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib,
    mBaseClassLoader);

    StrictMode.setThreadPolicy(oldPolicy);
    } else {
    if (mBaseClassLoader == null) {
    mClassLoader = ClassLoader.getSystemClassLoader();
    } else {
    mClassLoader = mBaseClassLoader;
    }
    }
    return mClassLoader;
    }
    }

    可以看到,非android开头的包和android开头的包分别使用了两种不同的ClassLoader,我们只关心第一种;因此继续跟踪ApplicationLoaders类:

    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
    public ClassLoader getClassLoader(String zip, String libPath, ClassLoader parent)
    {

    ClassLoader baseParent = ClassLoader.getSystemClassLoader().getParent();

    synchronized (mLoaders) {
    if (parent == null) {
    parent = baseParent;
    }

    if (parent == baseParent) {
    ClassLoader loader = mLoaders.get(zip);
    if (loader != null) {
    return loader;
    }

    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
    PathClassLoader pathClassloader =
    new PathClassLoader(zip, libPath, parent);
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

    mLoaders.put(zip, pathClassloader);
    return pathClassloader;
    }

    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, zip);
    PathClassLoader pathClassloader = new PathClassLoader(zip, parent);
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    return pathClassloader;
    }
    }

    可以看到,应用程序使用的ClassLoader都是PathClassLoader类的实例。那么,这个PathClassLoader是什么呢?从Android SDK给出的源码只能看出这么多:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
    super((String)null, (File)null, (String)null, (ClassLoader)null);
    throw new RuntimeException("Stub!");
    }

    public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
    super((String)null, (File)null, (String)null, (ClassLoader)null);
    throw new RuntimeException("Stub!");
    }
    }

    SDK没有导出这个类的源码,我们去androidxref上面看;发现其实这个类真的就这么多内容;我们继续查看它的父类BaseDexClassLoader;ClassLoader嘛,我们查看findClass或者defineClass方法,BaseDexClassLoader的findClass方法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
    ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class "" + name + "" on path: " + pathList);
    for (Throwable t : suppressedExceptions) {
    cnfe.addSuppressed(t);
    }
    throw cnfe;
    }
    return c;
    }

    可以看到,查找Class的任务通过pathList完成;这个pathList是一个DexPathList类的对象,它的findClass方法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
    DexFile dex = element.dexFile;

    if (dex != null) {
    Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
    if (clazz != null) {
    return clazz;
    }
    }
    }
    if (dexElementsSuppressedExceptions != null) {
    suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
    }

    这个DexPathList内部有一个叫做dexElements的数组,然后findClass的时候会遍历这个数组来查找Class;如果我们把插件的信息塞进这个数组里面,那么不就能够完成类的加载过程吗?!!

    给默认ClassLoader打补丁

    通过上述分析,我们知道,可以把插件的相关信息放入BaseDexClassLoader的表示dex文件的数组里面,这样宿主程序的ClassLoader在进行类加载,遍历这个数组的时候,会自动遍历到我们添加进去的插件信息,从而完成插件类的加载!

    接下来,我们实现这个过程;我们会用到一些较为复杂的反射技术哦~不过代码非常短:

    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
    public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
    throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
    // 获取 BaseDexClassLoader : pathList
    Field pathListField = DexClassLoader.class.getSuperclass().getDeclaredField("pathList");
    pathListField.setAccessible(true);
    Object pathListObj = pathListField.get(cl);

    // 获取 PathList: Element[] dexElements
    Field dexElementArray = pathListObj.getClass().getDeclaredField("dexElements");
    dexElementArray.setAccessible(true);
    Object[] dexElements = (Object[]) dexElementArray.get(pathListObj);

    // Element 类型
    Class<?> elementClass = dexElements.getClass().getComponentType();

    // 创建一个数组, 用来替换原始的数组
    Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);

    // 构造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 这个构造函数
    Constructor<?> constructor = elementClass.getConstructor(File.class, boolean.class, File.class, DexFile.class);
    Object o = constructor.newInstance(apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0));

    Object[] toAddElementArray = new Object[] { o };
    // 把原始的elements复制进去
    System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
    // 插件的那个element复制进去
    System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);

    // 替换
    dexElementArray.set(pathListObj, newElements);

    }

    短短的二十几行代码,我们就完成了『委托宿主ClassLoader加载插件类』的任务;因此第二种方案也宣告完成!我们简要总结一下这种方式的原理:

    1. 默认情况下performLacunchActivity会使用替身StubActivity的ApplicationInfo也就是宿主程序的CLassLoader加载所有的类;我们的思路是告诉宿主ClassLoader我们在哪,让其帮助完成类加载的过程。
    2. 宿主程序的ClassLoader最终继承自BaseDexClassLoader,BaseDexClassLoader通过DexPathList进行类的查找过程;而这个查找通过遍历一个dexElements的数组完成;我们通过把插件dex添加进这个数组就让宿主ClasLoader获取了加载插件类的能力。

    小结

    本文中我们采用两种方案成功完成了『启动没有在AndroidManifest.xml中显示声明,并且存在于外部插件中的Activity』的任务。

    『激进方案』中我们自定义了插件的ClassLoader,并且绕开了Framework的检测;利用ActivityThread对于LoadedApk的缓存机制,我们把携带这个自定义的ClassLoader的插件信息添加进mPackages中,进而完成了类的加载过程。

    『保守方案』中我们深入探究了系统使用ClassLoader findClass的过程,发现应用程序使用的非系统类都是通过同一个PathClassLoader加载的;而这个类的最终父类BaseDexClassLoader通过DexPathList完成类的查找过程;我们hack了这个查找过程,从而完成了插件类的加载。

    这两种方案孰优孰劣呢?

    很显然,『激进方案』比较麻烦,从代码量和分析过程就可以看出来,这种机制异常复杂;而且在解析apk的时候我们使用的PackageParser的兼容性非常差,我们不得不手动处理每一个版本的apk解析api;另外,它Hook的地方也有点多:不仅需要Hook AMS和H,还需要Hook ActivityThread的mPackages和PackageManager!

    『保守方案』则简单得多(虽然原理也不简单),不仅代码很少,而且Hook的地方也不多;有一点正本清源的意思,从最最上层Hook住了整个类的加载过程。

    但是,我们不能简单地说『保守方案』比『激进方案』好。从根本上说,这两种方案的差异在哪呢?

    『激进方案』是多ClassLoader构架,每一个插件都有一个自己的ClassLoader,因此类的隔离性非常好——如果不同的插件使用了同一个库的不同版本,它们相安无事!『保守方案』是单ClassLoader方案,插件和宿主程序的类全部都通过宿主的ClasLoader加载,虽然代码简单,但是鲁棒性很差;一旦插件之间甚至插件与宿主之间使用的类库有冲突,那么直接GG。

    多ClassLoader还有一个优点:可以真正完成代码的热加载!如果插件需要升级,直接重新创建一个自定的ClassLoader加载新的插件,然后替换掉原来的版本即可(Java中,不同ClassLoader加载的同一个类被认为是不同的类);单ClassLoader的话实现非常麻烦,有可能需要重启进程。

    在J2EE领域中广泛使用ClasLoader的地方均采用多ClassLoader架构,比如Tomcat服务器,Java模块化事实标准的OSGi技术;所以,我们有足够的理由认为选择多ClassLoader架构在大多数情况下是明智之举

    目前开源的插件方案中,DroidPlugin采用的『激进方案』,Small采用的『保守方案』那么,有没有两种优点兼顾的方案呢??答案自然是有的。

  • 相关阅读:
    js判断undefined类型
    js replace 全部替换
    第五次作业--原型设计
    第三次作业--团队展示
    第二次作业——数独终盘
    软件工程实践2017第一次作业
    课堂作业2
    课堂作业1
    第四次作业 计算器第二步
    第三次作业
  • 原文地址:https://www.cnblogs.com/wytiger/p/10384147.html
Copyright © 2011-2022 走看看