四百多個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的啟動流程中做的手腳就已經分析完了??偨Y起來就是:
[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的主要邏輯已經全部詳細分析完成了?,F在總結一下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,恰好說明很受關注。這里先留點念想,下一篇文章我們繼續分析。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容