四百多个issue的插件化框架——Android Small全解析


导读
如果你没有耐心,这篇文章对你来说可能是沉重的负担,你可以直接页内搜索“尾声”,那里省略了阶梯过程,直接是标准答案。


Small是一个专注于插件化的框架,特点是轻盈简洁,便于定制。轻盈体现在什么地方呢?如下图:

Small类结构

这就是Android Small Library的类结构。这在动辄上百K的第三方插件库群体中,简直是清流般的存在。你可以说它是小学生级别的代码数量,但不可否认的是,麻雀虽小,五脏俱全。对于插件化这一老生常谈的问题,Small用较小的代码量,交待了清楚了其背后的原理和运作机制。

闲言少叙,让我们翻开这本精致的教材吧!

Small的Lib部分代码比较少,但其实它通过Gradle脚本侵入了较多的打包的过程,这一点在这篇文章先按下不表。Lib部分我们首先从全局初始化开始:

【一】初始化

[ CODE 1 ]

public class Application extends android.app.Application {

    public Application() {
        // This should be the very first of the application lifecycle.
        // It's also ahead of the installing of content providers by what we can avoid
        // the ClassNotFound exception on if the provider is unimplemented in the host.
        Small.preSetUp(this);
    }

    @Override
    public void onCreate() {
        super.onCreate();

        // Optional
        Small.setBaseUri("http://code.wequick.net/small-sample/");
        Small.setLoadFromAssets(BuildConfig.LOAD_FROM_ASSETS);
    }
}

看看Small.preSetUp()做了什么:
[ CODE 1.1 ]

public static void preSetUp(Application context) {
        ...

        // 用全局的数据结构保存了3个Launcher
        registerLauncher(new ActivityLauncher());
        registerLauncher(new ApkBundleLauncher());
        registerLauncher(new WebBundleLauncher());
        Bundle.onCreateLaunchers(context);
    }

这里有3个重要的类,ActivityLauncher,ApkBundleLauncher,WebBundleLauncher。这3个类是Small的灵魂。ActivityLauncher负责对宿主Activity启动进行处理,ApkBundleLauncher则是对插件Activity的启动进行处理。WebBundleLauncher是针对7.0系统新增的处理模块,这里不是重点,我们后续也基本会无视它。
在 Bundle.onCreateLaunchers()方法中,依次调用了每个launcher的onCreate()方法。我们依次分析3个Launcher的onCreate()方法。

只有ActivityLauncher没有实现onCreate(),看看ApkBundleLauncher的:
[ CODE 1.1.1 ]

@Override
    public void onCreate(Application app) {
        super.onCreate(app);

        Object/*ActivityThread*/ thread;
        List<ProviderInfo> providers;
        Instrumentation base;
        ApkBundleLauncher.InstrumentationWrapper wrapper;
        Field f;

        // 获得当前的ActivityThread对象
        // 通过ActivityThread.currentActivityThread()和Application.mLoadedApk.mActivityThread来获得
        thread = ReflectAccelerator.getActivityThread(app);

        // Replace instrumentation
        try {
            f = thread.getClass().getDeclaredField("mInstrumentation");
            f.setAccessible(true);
            base = (Instrumentation) f.get(thread);
            wrapper = new ApkBundleLauncher.InstrumentationWrapper(base);
            f.set(thread, wrapper);
        } catch (Exception e) {
            throw new RuntimeException("Failed to replace instrumentation for thread: " + thread);
        }

        // 替换ActivityThread的mH的mCallback,后面会有分析
        ensureInjectMessageHandler(thread);

        // Get content providers
        ...
    }

对于这一段代码,我们首先需要知道Small允许插件在自己的AndroidManifest自声明Activity的原理。Small预先在宿主应用中声明一定数量的空壳Activity(占桩),然后hook Activity的启动流程,将宿主占桩的activity替换为插件的activity对象,达到狸猫换太子的效果。

        <activity
            android:name="net.wequick.small.A"
            android:configChanges="0x40002fff" />

        <activity
            android:theme="@ref/0x0103000f"
            android:name="net.wequick.small.A1"
            android:configChanges="0x40002fff" />

        <activity
            android:name="net.wequick.small.A10"
            android:launchMode="1"
            android:configChanges="0x40002fff" />

        <activity
            android:name="net.wequick.small.A11"
            android:launchMode="1"
            android:configChanges="0x40002fff" />

        <activity
            android:name="net.wequick.small.A12"
            android:launchMode="1"
            android:configChanges="0x40002fff" />

        <activity
            android:name="net.wequick.small.A13"
            android:launchMode="1"
            android:configChanges="0x40002fff" />

        <activity
            android:name="net.wequick.small.A20"
            android:launchMode="2"
            android:configChanges="0x40002fff" />

        <activity
            android:name="net.wequick.small.A21"
            android:launchMode="2"
            android:configChanges="0x40002fff" />

        <activity
            android:name="net.wequick.small.A22"
            android:launchMode="2"
            android:configChanges="0x40002fff" />

        <activity
            android:name="net.wequick.small.A23"
            android:launchMode="2"
            android:configChanges="0x40002fff" />

        <activity
            android:name="net.wequick.small.A30"
            android:launchMode="3"
            android:configChanges="0x40002fff" />

        <activity
            android:name="net.wequick.small.A31"
            android:launchMode="3"
            android:configChanges="0x40002fff" />

        <activity
            android:name="net.wequick.small.A32"
            android:launchMode="3"
            android:configChanges="0x40002fff" />

        <activity
            android:name="net.wequick.small.A33"
            android:launchMode="3"
            android:configChanges="0x40002fff" />

对于当前进程,ActivityThread对象是唯一的。在ActivityThread对象内持有一个Instrumentation对象。所有start activity的指令最终都会调用到Instrumentation.execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options)方法中。因此,只要Instrumentation对象是唯一的(进程唯一),那么hook就可行。

万幸的是,Instrumentation确实是唯一的(不唯一也要强行唯一,大不了替换所有的Instrumentation)。持有Instrumentation对象的地方有多处,但五湖四海Instrumentation对象都来自同一个母亲——ActivityThread。比如Activity的mInstrumentation成员实际上是在Activity实例化以后,调用attach()方法,从ActivityThread中传递过来的。

很明显的,上面的代码中,ApkBundleLauncher.InstrumentationWrapper是启动插件activity的幕后黑手。我们先跟进去看看再说:
[ CODE 1.1.1.1 ]

/**
     * Class for redirect activity from Stub(AndroidManifest.xml) to Real(Plugin)
     */
    protected static class InstrumentationWrapper extends Instrumentation
            implements InstrumentationInternal {

        private Instrumentation mBase;  // 原始的Instrumentation
        // 占桩activity的数量。这里写死似乎并不太好,可以在编译阶段写入参数就更好了。对于插件比较复杂的应用,4个可能不够用
        private static final int STUB_ACTIVITIES_COUNT = 4;

        public InstrumentationWrapper(Instrumentation base) {
            mBase = base;
        }

        // 前面分析过了,这就是关键的execStartActivity方法
        /** @Override V21+
         * Wrap activity from REAL to STUB */
        public ActivityResult execStartActivity(
                Context who, IBinder contextThread, IBinder token, Activity target,
                Intent intent, int requestCode, android.os.Bundle options) {
            // 拦截插件activity的注册,将插件activity替换为占桩activity,使之“合法化”
            wrapIntent(intent);
            // 拦截注册后的实例化过程,使得插件activity得到实例化
            ensureInjectMessageHandler(sActivityThread);
            return ReflectAccelerator.execStartActivity(mBase,
                    who, contextThread, token, target, intent, requestCode, options);
        }

        /** @Override V20-
         * Wrap activity from REAL to STUB */
        public ActivityResult execStartActivity(
                Context who, IBinder contextThread, IBinder token, Activity target,
                Intent intent, int requestCode) {
            ...
        }

        @Override
        /** Prepare resources for REAL */
        public void callActivityOnCreate(Activity activity, android.os.Bundle icicle) {
            ...
        }

        @Override
        public void callActivityOnStop(Activity activity) {
            sHostInstrumentation.callActivityOnStop(activity);
            // 当activity不可见时(即onStop回调时),做如下检查:
            // 如果Small正在加载插件,那么检查当前进程是否位于前台,如果不是,那么直接结束进程(android.os.Process.killProcess())。
            // 这样做的目的是,杀死进程之后冷启动才可以加载新的类和新的资源
            ...
        }

        // 这一步尤为关键。在插件activity销毁时,将对应的占桩activity释放出来。
        // 占桩activity是有限的(这里写死了4个),如果不释放,后续启动插件activity就会因为找不到占桩activity而失败
        @Override
        public void callActivityOnDestroy(Activity activity) {
            do {
                if (sLoadedActivities == null) break;
                String realClazz = activity.getClass().getName();
                ActivityInfo ai = sLoadedActivities.get(realClazz);
                if (ai == null) break;
                inqueueStubActivity(ai, realClazz);
            } while (false);
            sHostInstrumentation.callActivityOnDestroy(activity);
        }

        @Override
        public boolean onException(Object obj, Throwable e) {
            ...
            // 当ContentProvider install failed的时候回调。
            // 这里的处理是将加载失败的provider添加到mLazyInitProviders当中,后面进行加载

            return super.onException(obj, e);
        }

        // 偷换activity全名
        private void wrapIntent(Intent intent) {
            ComponentName component = intent.getComponent();
            String realClazz;
            // 隐式查找activity
            if (component == null) {
                // Try to resolve the implicit action which has registered in host.
                component = intent.resolveActivity(Small.getContext().getPackageManager());
                if (component != null) {
                    // 非插件activity,当然不管了
                    return;
                }

                // Try to resolve the implicit action which has registered in bundles.
                realClazz = resolveActivity(intent);
                if (realClazz == null) {
                    // Cannot resolved, nothing to be done.
                    return;
                }
            } else {
                realClazz = component.getClassName();
                if (realClazz.startsWith(STUB_ACTIVITY_PREFIX)) {
                    // Re-wrap to ensure the launch mode works.
                    realClazz = unwrapIntent(intent);
                }
            }

            if (sLoadedActivities == null) return;

            // sLoadedActivities在后面会进行初始化,保存所有插件activity全名和ActivityInfo的对应关系
            ActivityInfo ai = sLoadedActivities.get(realClazz);
            if (ai == null) return;

            // 原本的category的字段被换成了特殊记号 + 插件activity全名,方便后面识别
            intent.addCategory(REDIRECT_FLAG + realClazz);
            // 找到一个可用的位于宿主的占桩activity,并返回该占桩activity全名
            String stubClazz = dequeueStubActivity(ai, realClazz);
            intent.setComponent(new ComponentName(Small.getContext(), stubClazz));
        }

        private String resolveActivity(Intent intent) {
            // sLoadedIntentFilters会在后面进行初始化,存放所有从插件解析的activity信息
            if (sLoadedIntentFilters == null) return null;

            Iterator<Map.Entry<String, List<IntentFilter>>> it =
                    sLoadedIntentFilters.entrySet().iterator();
            while (it.hasNext()) {
                Map.Entry<String, List<IntentFilter>> entry = it.next();
                List<IntentFilter> filters = entry.getValue();
                for (IntentFilter filter : filters) {
                    if (filter.hasAction(Intent.ACTION_VIEW)) {
                        // TODO: match uri
                    }

                    // 必须定义Intent.CATEGORY_DEFAULT,否则无法隐式匹配
                    if (filter.hasCategory(Intent.CATEGORY_DEFAULT)) {
                        // 这里的匹配方式非常有趣
                        // 由于插件activity并非由系统(PackageManagerService)官方解析的,所以无法借助PMS来进行隐式匹配
                        // 于是作者偷了一下懒,要想隐式启动,就定义一个独一无二的action来标记吧
                        if (filter.hasAction(intent.getAction())) {
                            // hit
                            return entry.getKey();
                        }
                    }
                }
            }
            return null;
        }

        private String[] mStubQueue;

        /** Get an usable stub activity clazz from real activity */
        private String dequeueStubActivity(ActivityInfo ai, String realActivityClazz) {
            if (ai.launchMode == ActivityInfo.LAUNCH_MULTIPLE) {
                // In standard mode, the stub activity is reusable.
                // Cause the `windowIsTranslucent' attribute cannot be dynamically set,
                // We should choose the STUB activity with translucent or not here.
                Resources.Theme theme = Small.getContext().getResources().newTheme();
                theme.applyStyle(ai.getThemeResource(), true);
                TypedArray sa = theme.obtainStyledAttributes(
                        new int[] { android.R.attr.windowIsTranslucent });
                boolean translucent = sa.getBoolean(0, false);
                sa.recycle();
                // 这里返回的值是:pkgName + ".A1"。这个activity对专门对应每次实例化新的activity的占桩activity
                return translucent ? STUB_ACTIVITY_TRANSLUCENT : STUB_ACTIVITY_PREFIX;
            }

            // 如果能查找到满足条件的空的占桩activity,则返回全名,反之,返回null
            int availableId = -1;
            int stubId = -1;
            int countForMode = STUB_ACTIVITIES_COUNT;
            int countForAll = countForMode * 3; // 3=[singleTop, singleTask, singleInstance]
            if (mStubQueue == null) {
                // Lazy init
                mStubQueue = new String[countForAll];
            }
            int offset = (ai.launchMode - 1) * countForMode;
            for (int i = 0; i < countForMode; i++) {
                String usedActivityClazz = mStubQueue[i + offset];
                if (usedActivityClazz == null) {
                    if (availableId == -1) availableId = i;
                } else if (usedActivityClazz.equals(realActivityClazz)) {
                    stubId = i;
                }
            }
            if (stubId != -1) {
                availableId = stubId;
            } else if (availableId != -1) {
                mStubQueue[availableId + offset] = realActivityClazz;
            } else {
                // TODO:
                Log.e(TAG, "Launch mode " + ai.launchMode + " is full");
            }
            return STUB_ACTIVITY_PREFIX + ai.launchMode + availableId;
        }

        /** Unbind the stub activity from real activity */
        private void inqueueStubActivity(ActivityInfo ai, String realActivityClazz) {
            if (ai.launchMode == ActivityInfo.LAUNCH_MULTIPLE) return;
            if (mStubQueue == null) return;

            int countForMode = STUB_ACTIVITIES_COUNT;
            int offset = (ai.launchMode - 1) * countForMode;
            for (int i = 0; i < countForMode; i++) {
                String stubClazz = mStubQueue[i + offset];
                if (stubClazz != null && stubClazz.equals(realActivityClazz)) {
                    mStubQueue[i + offset] = null;
                    break;
                }
            }
        }

上面的几段代码调用了一个方法ensureInjectMessageHandler()(见[ CODE 1.1.1.2 ]),这方法也非常关键。在上面的InstrumentationWrapper中,我们hook了execStartActivity()方法,这个方法是本地进程和AMS交互的起点。AMS认证,注册,管理的实际上是占桩的宿主activity。在这一步,我们成功瞒天过海,通过了AMS的考验。接下来,AMS经过一系列处理之后,会通过Binder通信回调本地进程的ActivityThread,通过ActivityThread的mH发送一个消息,在消息的处理逻辑里面,才开始真正实例化activity。这个时候我们hook消息处理的逻辑,使其真正实例化的是插件的activity,而不是占桩的activity。这样,插件activity就不再是没有出生证明的黑户,可以健康茁壮地成长了(拥有完整的activity生命周期)。
[ CODE 1.1.1.2 ]

private static void ensureInjectMessageHandler(Object thread) {
        try {
            Field f = thread.getClass().getDeclaredField("mH");
            f.setAccessible(true);
            Handler ah = (Handler) f.get(thread);
            f = Handler.class.getDeclaredField("mCallback");
            f.setAccessible(true);

            ...

            if (needsInject) {
                // Inject message handler
                sActivityThreadHandlerCallback = new ActivityThreadHandlerCallback();
                f.set(ah, sActivityThreadHandlerCallback);
            }
        } catch (Exception e) {
            throw new RuntimeException("Failed to replace message handler for thread: " + thread);
        }
    }

接下来跟踪ActivityThreadHandlerCallback,看看是如何实例化插件activity的:
[ CODE 1.1.1.1.1 ]

/**
     * Class for restore activity info from Stub to Real
     */
    private static class ActivityThreadHandlerCallback implements Handler.Callback {

        private static final int LAUNCH_ACTIVITY = 100;
        private static final int CREATE_SERVICE = 114;
        private static final int CONFIGURATION_CHANGED = 118;
        private static final int ACTIVITY_CONFIGURATION_CHANGED = 125;

        private Configuration mApplicationConfig;

        // 处理AMS回执的消息
        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case LAUNCH_ACTIVITY:
                    redirectActivity(msg);
                    break;

                case CREATE_SERVICE:
                    ensureServiceClassesLoadable(msg);
                    break;

                case CONFIGURATION_CHANGED:
                    recordConfigChanges(msg);
                    break;

                case ACTIVITY_CONFIGURATION_CHANGED:
                    return relaunchActivityIfNeeded(msg);

                default:
                    break;
            }

            return false;
        }

        private void redirectActivity(Message msg) {
            Object/*ActivityClientRecord*/ r = msg.obj;
            Intent intent = ReflectAccelerator.getIntent(r);
            // 这里根据前面“特殊记号 + 插件activity全名”的格式,还原出插件activity的全名
            String targetClass = unwrapIntent(intent);
            boolean hasSetUp = Small.hasSetUp();
            if (targetClass == null) {
                // The activity was register in the host.
                if (hasSetUp) return; // nothing to do

                if (intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
                    // The launcher activity will setup Small.
                    return;
                }

                // Launching an activity in remote process. Set up Small for it.
                Small.setUpOnDemand();
                return;
            }

            if (!hasSetUp) {
                // Restarting an activity after application recreated,
                // maybe upgrading or somehow the application was killed in background.
                Small.setUp();
            }

            // Replace with the REAL activityInfo
            ActivityInfo targetInfo = sLoadedActivities.get(targetClass);
            // 把插件activity的全名塞进去了
            ReflectAccelerator.setActivityInfo(r, targetInfo);
        }

        private void ensureServiceClassesLoadable(Message msg) {
            Object/*ActivityThread$CreateServiceData*/ data = msg.obj;
            ServiceInfo info = ReflectAccelerator.getServiceInfo(data);
            if (info == null) return;

            String appProcessName = Small.getContext().getApplicationInfo().processName;
            if (!appProcessName.equals(info.processName)) {
                // Cause Small is only setup in current application process, if a service is specified
                // with a different process('android:process=xx'), then we should also setup Small for
                // that process so that the service classes can be successfully loaded.
                Small.setUpOnDemand();
            } else {
                // The application might be started up by a background service
                if (Small.isFirstSetUp()) {
                    Log.e(TAG, "Starting service before Small has setup, this might block the main thread!");
                }
                Small.setUpOnDemand();
            }
        }

        private void recordConfigChanges(Message msg) {
            mApplicationConfig = (Configuration) msg.obj;
        }

        private boolean relaunchActivityIfNeeded(Message msg) {
            // 和sLoadedActivities比较配置信息是否已经更新。如果已经更新,那么relaunchActivity
        }
    }

我们知道ActivityThread内部的mH是一个Handler。Handler处理消息的优先级如下。我们看到优先级最高的是Message自带的callback。如果Message没有设置callback,那么将消息分发给Handler的callback,根据callback handleMessage()的返回值来确定是否回调Handler的handleMessage()。系统处理launch activity的逻辑都在mH的handleMessage()当中。所以只需要在mH的mCallback提前把ActivityInfo替换掉就可以了。但是如果要借助系统来初始化activity,那么一定要在callback的handleMessage()中返回false。

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

到目前为止,Small在activity的启动流程中做的手脚就已经分析完了。总结起来就是:
[1] 在向AMS注册activity之前把插件activity的全名替换成占桩的activity全名,使之合法化;
[2] AMS回调实例化启动的activity之前,把占桩的activity替换成插件activity,于是真正实例化的就是插件activity;
[3] 非ActivityInfo.LAUNCH_MULTIPLE模式的占桩activity是有限的,在插件activity销毁时,需要归还占桩activity。

接下来回到[ CODE 1 ]。在Application的onCreate()方法中,设置了base uri,并写入了配置LOAD_FROM_ASSETS。如果为true,那么加载apk插件,反之加载so插件。apk插件或者so插件是可以在打包时配置的,可以自由控制。

在Application的启动阶段,仅仅是为hook做了准备,提供了hook环境,但是并没有真正开始对插件的处理,仅有的耗时操作也就是反射替换了,整个过程还是比较环保的,基本上做到了插件懒加载。

【二】加载插件

做完热身运动之后,就开始真正的加载插件了。加载插件的动作一般由Small.setUp()触发:
[ CODE 2 ]

Small.setUp(LaunchActivity.this, new net.wequick.small.Small.OnCompleteListener() {
            @Override
            public void onComplete() {
                    Small.openUri("main", LaunchActivity.this);
                }
            }
        });

跟进setUp()方法,真正进入主逻辑的是Bundle.loadBundles()方法:
[ CODE 2.1 ]

private static void loadBundles(Context context) {
        JSONObject manifestData;
        try {
            // 读取bundle.json的内容,bundle.json见后面的代码段
            File patchManifestFile = getPatchManifestFile();
            // 从SharedPreferences中读取bundle.json内容,SP起了缓存作用
            String manifestJson = getCacheManifest();

            ...

            // Parse manifest file
            manifestData = new JSONObject(manifestJson);
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }

        Manifest manifest = parseManifest(manifestData);
        if (manifest == null) return;

        setupLaunchers(context);

        loadBundles(manifest.bundles);
    }

在上面的getPatchManifestFile()方法读取了一个叫做bundle.json的文件。这个文件也比较关键,它是我们配置插件的“首选项”文件。我们贴一下官方demo里面这个文件的内容:

{
  "version": "1.0.0",
  "bundles": [
    {
      "uri": "lib.utils",
      "pkg": "net.wequick.example.small.lib.utils"
    },
    {
      "uri": "lib.style",
      "pkg": "com.example.mysmall.lib.style"
    },
    {
      "uri": "lib.analytics",
      "pkg": "net.wequick.example.lib.analytics"
    },
    {
      "uri": "main",
      "pkg": "net.wequick.example.small.app.main"
    },
    {
      "uri": "home",
      "pkg": "net.wequick.example.small.app.home"
    },
    {
      "uri": "mine",
      "pkg": "net.wequick.example.small.app.mine"
    },
    {
      "uri": "detail",
      "pkg": "net.wequick.example.small.app.detail",
      "rules": {
        "sub": "Sub"
      }
    },
    {
      "uri": "stub",
      "type": "app",
      "pkg": "net.wequick.example.small.appok_if_stub"
    },
    {
      "uri": "about",
      "pkg": "net.wequick.example.small.web.about"
    }
  ]
}

包名不用多说,uri是我们启动各个插件四大组件的钥匙,type则是模块类型,Small定义了4种类型:host(宿主模块),stub(宿主拆分模块,属于主包,但是拆分成了独立module),lib(公共依赖库),app(插件模块,也就是我们重点研究的对象)。

回到[ CODE 2.1 ],继续看setupLaunchers()方法。这个方法依次调用了ActivityLauncher、ApkBundleLauncher和WebBundleLauncher类的setUp()方法。
首先是ActivityLauncher.setUp():
[ CODE 2.1.1 ]

@Override
    public void setUp(Context context) {
        super.setUp(context);

        // 从宿主的AndroidManifest读取宿主所有注册的activity信息
        File sourceFile = new File(context.getApplicationInfo().sourceDir);
        // 具体的解析类
        // 这个类里面几乎解析了所有AndroidManifest的元素:包名,版本号,主题,Application类名
        // 而下面的collectActivities()方法则搜集了所有的activity信息,重点搜集的是activity的全名和对应的intent-filter
        BundleParser parser = BundleParser.parsePackage(sourceFile, context.getPackageName());
        parser.collectActivities();
        ActivityInfo[] as = parser.getPackageInfo().activities;
        if (as != null) {
            sActivityClasses = new HashSet<String>();
            // 解析的结果:主包所有activity的全名列表
            for (ActivityInfo ai : as) {
                sActivityClasses.add(ai.name);
            }
        }
    }

ActivityLauncher相对简单,接下来是ApkBundleLauncher.setUp():
[ CODE 2.1.2 ]

@Override
    public void setUp(Context context) {
        super.setUp(context);

        Field f;

        // 这里使用了“AOP(面向切面编程)技术”,其实就是使用动态代理,hook TaskStackBuilder.IMPL.getPendingIntent()方法。
        // 如果是使用PendingIntent来启动插件activity,那么替换类名。
        // 这个场景似曾相识,没错,看看[ CODE 1.1.1.1 ]就明白了!
        // AOP for pending intent
        try {
            f = TaskStackBuilder.class.getDeclaredField("IMPL");
            f.setAccessible(true);
            final Object impl = f.get(TaskStackBuilder.class);
            InvocationHandler aop = new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    Intent[] intents = (Intent[]) args[1];
                    for (Intent intent : intents) {
                        sBundleInstrumentation.wrapIntent(intent);
                        intent.setAction(Intent.ACTION_MAIN);
                        intent.addCategory(Intent.CATEGORY_LAUNCHER);
                    }
                    return method.invoke(impl, args);
                }
            };
            Object newImpl = Proxy.newProxyInstance(context.getClassLoader(), impl.getClass().getInterfaces(), aop);
            f.set(TaskStackBuilder.class, newImpl);
        } catch (Exception ignored) {
            ignored.printStackTrace();
        }
    }

看了ApkBundleLauncher.setUp()的代码松了口气,代码量很小。但是这里也有一个问题,为什么这一步没有放到ApkBundleLauncher.onCreate()中一起做?

回到[ CODE 2.1 ],继续loadBundles(manifest.bundles)方法。这个方法做了很多事情:
[ CODE 2.1.3 ]

// 参数bundles是从bundle.json中解析得到的内容
private static void loadBundles(List<Bundle> bundles) {
        sPreloadBundles = bundles;

        // Prepare bundle
        for (Bundle bundle : bundles) {
            bundle.prepareForLaunch();
        }

        // Handle I/O
        if (sIOActions != null) {
            // 使用线程池,执行完所有的sIOActions
        }

        ...

        // Notify `postSetUp' to all launchers
        for (BundleLauncher launcher : sBundleLaunchers) {
            launcher.postSetUp();
        }

        // Free all unused temporary variables
        for (Bundle bundle : bundles) {
            if (bundle.parser != null) {
                bundle.parser.close();
                bundle.parser = null;
            }
            bundle.mBuiltinFile = null;
            bundle.mExtractPath = null;
        }
    }

首先,针对每个bundle(即每个在bundle.json里面注册的模块信息),遍历找到能够解析它的BundleLauncher。ActivityLauncher只能解析“main”(宿主)的,ApkBundleLauncher则能解析“lib”和“app”的。在bundle.prepareForLaunch()的调用过程中,会依次回调每一个BundleLauncher(ActivityLauncher、ApkBundleLauncher、WebBundleLauncher均继承自BundleLauncher)的resolveBundle()方法:

public boolean resolveBundle(Bundle bundle) {
        if (!preloadBundle(bundle)) return false;

        loadBundle(bundle);
        return true;
    }

ApkBundleLauncher对preloadBundle的处理比较特殊,实际是由SoBundleLauncher.preloadBundle()实现的。我们戳进去看看:
[ CODE 2.1.3.1 ]

@Override
    public boolean preloadBundle(Bundle bundle) {
        ...

        // 检查是否支持(是否lib或者app)

        ...

        // 版本比较,用较新版本的插件
        File plugin = bundle.getBuiltinFile();  // 之前使用过的插件(可能是旧版本的)
        // 这里的BundleParser好像在哪里见过,ActivityLauncher里面它就露脸了。用处是解析AndroidManifest几乎所有的信息
        BundleParser parser = BundleParser.parsePackage(plugin, packageName);
        File patch = bundle.getPatchFile();  // 直接下载下来在特定目录的apk或者so
        BundleParser patchParser = BundleParser.parsePackage(patch, packageName);
        if (parser == null) {
            if (patchParser == null) {
                return false;
            } else {
                parser = patchParser; // use patch
                plugin = patch;
            }
        } else if (patchParser != null) {
            if (patchParser.getPackageInfo().versionCode <= parser.getPackageInfo().versionCode) {
                Log.d(TAG, "Patch file should be later than built-in!");
                patch.delete();
            } else {
                parser = patchParser; // use patch
                plugin = patch;
            }
        }
        bundle.setParser(parser);

        // Check if the plugin has not been modified
        long lastModified = plugin.lastModified();
        long savedLastModified = Small.getBundleLastModified(packageName);
        if (savedLastModified != lastModified) {
            // If modified, verify (and extract) each file entry for the bundle
            // 有新的插件,那么进行CRC验证(保证传输过程中未被破坏)以及证书验证(保证来源合法)
            if (!parser.verifyAndExtract(bundle, this)) {
                bundle.setEnabled(false);
                return true; // Got it, but disabled
            }
            Small.setBundleLastModified(packageName, lastModified);
        }

        // Record version code for upgrade
        PackageInfo pluginInfo = parser.getPackageInfo();
        bundle.setVersionCode(pluginInfo.versionCode);
        bundle.setVersionName(pluginInfo.versionName);

        return true;
    }

我们可以看到,上面这个方法主要是进行了版本处理和合法校验。

接下来调用每个BundleLauncher的loadBundle()方法,重点是ApkBundleLauncher的实现:
[ CODE 2.1.3.1 ]

@Override
    public void loadBundle(Bundle bundle) {
        String packageName = bundle.getPackageName();

        BundleParser parser = bundle.getParser();
        // 这里似曾相识,在ActivityLauncher的setUp()方法中做过一模一样的搜集过程。
        parser.collectActivities();
        PackageInfo pluginInfo = parser.getPackageInfo();

        // Load the bundle
        String apkPath = parser.getSourcePath();
        // 初始化apk映射信息,非常重要
        if (sLoadedApks == null) sLoadedApks = new ConcurrentHashMap<String, LoadedApk>();
        LoadedApk apk = sLoadedApks.get(packageName);
        // 第一次肯定是空的
        if (apk == null) {
            apk = new LoadedApk();
            // 初始化apk变量

            ...

            // Load dex
            final LoadedApk fApk = apk;
            // 注意这里postIO()是将IO任务置入sIOActions任务队列中,这个队列后面会用到
            Bundle.postIO(new Runnable() {
                @Override
                public void run() {
                    try {
                        fApk.dexFile = DexFile.loadDex(fApk.path, fApk.optDexFile.getPath(), 0);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });

            // Extract native libraries with specify ABI
            String libDir = parser.getLibraryDirectory();
            if (libDir != null) {
                apk.libraryPath = new File(apk.packagePath, libDir);
            }
            sLoadedApks.put(packageName, apk);
        }

        ...

        // Record activities for intent redirection
        if (sLoadedActivities == null) sLoadedActivities = new ConcurrentHashMap<String, ActivityInfo>();
        for (ActivityInfo ai : pluginInfo.activities) {
            sLoadedActivities.put(ai.name, ai);
        }

        // 记录intent filter
        ...
        }

        // Set entrance activity
        bundle.setEntrance(parser.getDefaultActivityName());
    }

上面的loadBundle()主要是搜集activity信息,初始化插件apk信息,提交加载dex的任务,初始化一个很重要的变量sLoadedActivities,最后再设置一些信息。

回到[ CODE 2.1.3 ],往下来到Handle I/O部分。这里执行了刚刚提交的DexFile.loadDex()方法,将插件dex加载进来。然后又来到一个重要的方法:回调各个BundleLauncher的postSetUp()方法:
[ CODE 2.1.3.2 ]

@Override
    public void postSetUp() {
        super.postSetUp();

        if (sLoadedApks == null) {
            Log.e(TAG, "Could not find any APK bundles!");
            return;
        }

        Collection<LoadedApk> apks = sLoadedApks.values();

        // Merge all the resources in bundles and replace the host one
        // 上面这句注释已经说得很明白了,把宿主的资源和插件的资源做合并,然后替换宿主的资源
        // 这里合并,指的是把宿主和插件的资源路径(apk路径)合成数组,再调用AssetManager.addAssetPaths()方法
        final Application app = Small.getContext();
        // +1是因为要加入宿主的resource path
        String[] paths = new String[apks.size() + 1];
        paths[0] = app.getPackageResourcePath(); // add host asset path
        
        ...

        // 替换资源操作:1. 实例化AssetManager;2. AssetManager.addAssetPaths(paths);
        // 3. 反射替换系统所有持有AssetManager对象的地方。主要的工作量在第三步
        ReflectAccelerator.mergeResources(app, sActivityThread, paths);

        // Merge all the dex into host's class loader
        ClassLoader cl = app.getClassLoader();
        i = 0;
        int N = apks.size();
        String[] dexPaths = new String[N];
        DexFile[] dexFiles = new DexFile[N];
        for (LoadedApk apk : apks) {
            dexPaths[i] = apk.path;
            dexFiles[i] = apk.dexFile;
            if (Small.getBundleUpgraded(apk.packageName)) {
                // If upgraded, delete the opt dex file for recreating
                if (apk.optDexFile.exists()) apk.optDexFile.delete();
                Small.setBundleUpgraded(apk.packageName, false);
            }
            i++;
        }
        // 采用dex插桩的方式,加载插件的dex。
        // 3.2版本后,都是采用找到BaseDexClassLoader的dexElements成员,
        // 而dexElements是一个dalvik.system.DexPathList$Element数组。将插件的Elements放在dexElements数组元素的前面即可。
        ReflectAccelerator.expandDexPathList(cl, dexPaths, dexFiles);

        // Expand the native library directories for host class loader if plugin has any JNIs. (#79)
        // 原理同上

        ...

        ReflectAccelerator.expandNativeLibraryDirectories(cl, libPathList);

        // Trigger all the bundle application `onCreate' event
        // 回调所有插件Application的onCreate,这个细节都不放过,可以说是非常良心了
        for (final LoadedApk apk : apks) {
            String bundleApplicationName = apk.applicationName;
            if (bundleApplicationName == null) continue;

            try {
                final Class applicationClass = Class.forName(bundleApplicationName);
                Bundle.postUI(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            BundleApplicationContext appContext = new BundleApplicationContext(app, apk);
                            Application bundleApplication = Instrumentation.newApplication(
                                    applicationClass, appContext);
                            sHostInstrumentation.callApplicationOnCreate(bundleApplication);
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                });
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        // Lazy init content providers
        if (mLazyInitProviders != null) {
            try {
                Method m = sActivityThread.getClass().getDeclaredMethod(
                        "installContentProviders", Context.class, List.class);
                m.setAccessible(true);
                m.invoke(sActivityThread, app, mLazyInitProviders);
            } catch (Exception e) {
                throw new RuntimeException("Failed to lazy init content providers: " + mLazyInitProviders);
            }
        }

        // Free temporary variables
        sLoadedApks = null;
        sProviders = null;
    }

这个方法做了很多事情,(1)合并宿主资源和插件资源,并将合并后的AssetManager替换宿主中所有用到AssetManager的地方;(2)加载dex,用插桩的方式将插件dex放在dexElements的最前面,这样加载插件里面的类时,就会从dexElements中找到目标类。这里没有利用双亲委派原则,子classLoader加载插件dex的方式,感兴趣的读者可以去搜搜看另一种实现方式;(3)合并so,原理类似;(4)回调Application的onCreate。这一步可以看出插件是可以定义Application的,很有意思。但是这么做也有局限,因为插件可以懒加载,于是插件的Application的创建并不是和宿主Application创建同时进行的——这在很多情况下,尤其是插件完全独立于宿主的情况下会有歧义;(5)Content Provider加载。
至此,我们可以看到,四大组件,Small支持Activity和ContentProvider。

【三】启动Activity

那么,接下来就是要真正使用了。假设我们去打开一个插件的activity:
[ CODE 3 ]

// 这个“main”是哪里来的?参看前面bundle.json的文件内容
Small.openUri("main", context);

在对Uri进行解析的过程中,会首先把传入的Uri关键字拼接上我们在Application的onCreate中初始化的base uri。然后对uri进行判断,如果拼接后的Uri不是以“http”、“https”、“file”为scheme,那么Small不予处理。这也要求我们定义base uri的时候不能随心所欲。接着将此Uri寻找能够匹配的bundle(所有的bundle是之前从bundle.json中解析出来的),其实就是寻找能匹配的app(宿主)或者插件。那么,Uri的匹配规则是什么呢?我们假设bundle.json解析得到的Uri叫做声明Uri,把请求的Uri叫做请求Uri,那么:

  1. 请求Uri必须以声明Uri开头。请注意,这里的请求Uri是指base uri + 实际请求Uri。比如Small.openUri("main", context),Uri就是base uri + "main"。一般情况下,请求Uri和声明Uri两者是等价的关系;
  2. 不管请求Uri和声明Uri是否是完全等价关系,都必须满足:请求Uri = 声明Uri + rules里面定义的某个key。在解析bundle。json过程中会得到一个默认的rule,key-value形式如同:""-"Your value"。在请求Uri和声明Uri等价的情况下,就会默认匹配到这个rule;
  3. 如果value不为null,这个时候其实就已经匹配了。如果bundle.json里面没有定义rule,也会匹配。这时候Value其实是"",而不是null。最后,记录下来要匹配的path是上面的value值,而query是上面的请求Uri中“?”后面query params的部分。实际query会更复杂一些,这里不深入了。

一般情况下,path就是rules中定义的某个匹配的Value,query是空的。

匹配到合适的Uri之后,也就找到了能解析当前Uri的bundle,然后就能找到可以处理此Uri的BundleLauncher,即ActivityLauncher或者ApkBundleLauncher。顺理成章的,调用BundleLauncher的launchBundle()方法。首先看一下ActivityLauncher的launchBundle()方法,也即,看看如果要启动宿主包的Activity应该怎么做:

[ CODE 3.1 ]

@Override
    public void launchBundle(Bundle bundle, Context context) {
        prelaunchBundle(bundle);
        super.launchBundle(bundle, context);
    }

@Override
    public void prelaunchBundle(Bundle bundle) {
        // super是空实现
        super.prelaunchBundle(bundle);
        Intent intent = new Intent();
        bundle.setIntent(intent);

        // Intent extras - class
        String activityName = bundle.getActivityName();
        
        ...

        intent.setComponent(new ComponentName(Small.getContext(), activityName));

        // Intent extras - params
        String query = bundle.getQuery();
        if (query != null) {
            intent.putExtra(Small.KEY_QUERY, '?'+query);
        }
    }

上面的prelaunchBundle()方法调用了Bundle.getActivityName()。我们看一下它是怎么把activity name返回的:
[ CODE 3.1.1 ]

protected String getActivityName() {
        String activityName = path;

        String pkg = mPackageName != null ? mPackageName : Small.getContext().getPackageName();
        char c = activityName.charAt(0);
        if (c == '.') {
            activityName = pkg + activityName;
        } else if (c >= 'A' && c <= 'Z') {
            activityName = pkg + '.' + activityName;
        }
        return activityName;
    }

就是包名 + path。这又给我们命名activity提了要求,必须以包名开头,不然还是会找不到启动的activity。

取得Activity名称之后,执行super.launchBundle(bundle, context),也就是:
[ CODE 3.1.2 ]

public void launchBundle(Bundle bundle, Context context) {
        if (context instanceof Activity) {
            Activity activity = (Activity) context;
            if (shouldFinishPreviousActivity(activity)) {
                activity.finish();
            }
            activity.startActivityForResult(bundle.getIntent(), Small.REQUEST_CODE_DEFAULT);
        } else {
            context.startActivity(bundle.getIntent());
        }
    }

直接就启动了,把处理过程交给早先hook的Instrumentation和mH。

启动宿主activity是非常简单的,因为基本不需要额外的处理。接着看看ApkBundleLauncher的launchBundle():
[ CODE 3.2 ]

@Override
    public void loadBundle(Bundle bundle) {
        String packageName = bundle.getPackageName();

        BundleParser parser = bundle.getParser();
        // 这里似曾相识,在ActivityLauncher的setUp()方法中做过一模一样的搜集过程。
        parser.collectActivities();
        PackageInfo pluginInfo = parser.getPackageInfo();

        // Load the bundle
        String apkPath = parser.getSourcePath();
        if (sLoadedApks == null) sLoadedApks = new ConcurrentHashMap<String, LoadedApk>();
        LoadedApk apk = sLoadedApks.get(packageName);
        if (apk == null) {
            apk = new LoadedApk();
            
            // apk 初始化...

            // Load dex
            final LoadedApk fApk = apk;
            Bundle.postIO(new Runnable() {
                @Override
                public void run() {
                    try {
                        fApk.dexFile = DexFile.loadDex(fApk.path, fApk.optDexFile.getPath(), 0);
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            });

            // Extract native libraries with specify ABI
            String libDir = parser.getLibraryDirectory();
            if (libDir != null) {
                apk.libraryPath = new File(apk.packagePath, libDir);
            }
            sLoadedApks.put(packageName, apk);
        }

        if (pluginInfo.activities == null) {
            return;
        }

        // 进行一些初始化设置,包括Launcher activity等
        ...
    }

这一步主要是进行了插件所有activity信息的搜集,然后加载dex,最后进行一些初始化设置。
完成如上逻辑之后,仍然调用BundleLauncher.launchBundle()方法(参见[ CODE 3.1.2 ]),顺利启动插件activity。

【四】尾声

至此,Small的主要逻辑已经全部详细分析完成了。现在总结一下Small代码运作流程:


Small加载流程

还有Small的精髓,Activity启动流程的hook:

Activity启动

全部分析完成之后,我们可以看到也许Small并不是一个非常完美的插件化方案。它虽然号称轻量级,但是仍然对四大组件的启动流程侵入了很多自己的代码。并且在整个过程中,大量使用了反射。无论从稳定性和性能来讲,都会有消极的影响。但是不管如何,作者对四大组件启动流程的理解仍然是值得我们学习的。

有的读者看完之后也许有所感慨,回头看一眼标题的时候肯定会感到疑惑。四百多个issue,为什么正文只字未提?笔者是不是标题党?不是!!这里先奉上Small的github地址:
https://github.com/wequick/Small
以及官网:
http://code.wequick.net/Small
其实有很大部分问题都来自于编译过程,也就是本文并未涉及的Gradle脚本部分。还有资源id分配的问题,也是在Gradle脚本中解决的。其实Small有这么多的issue,恰好说明很受关注。这里先留点念想,下一篇文章我们继续分析。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容