横向浅析Small,RePlugin两个插件化框架(二) small原理解析

106 阅读17分钟

#Small 为什么挑出Small这个插件化框架呢?看了源码你会发现这种插件化框架代表着现在插件化另外一种新思想,同时实现思路和DroidPlugin相比,属于另外一个方向。

那就是对插件的定义更为的广义。不单单只是指我们的独立之外的apk包,还指代了工程里面的所有模块也是插件。

闲话不多说。老规矩,我先贴上如何使用Small框架的文章,实际上很简单。里面的Sample也很简洁的显示出如何使用。 用法与常见问题:code.wequick.net/Small/cn/qu…

一个不错的解析文章:www.jianshu.com/p/8eca24846…

除去对gradle的命令操作和工程上的命名配置层面,代码上细分为两步。

第一步:在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);
    }

第二步:加载App内部的插件

Small.setUp(LaunchActivity.this, new net.wequick.small.Small.OnCompleteListener() {
            @Override
            public void onComplete() {
                long tEnd = System.nanoTime();
                se.putLong("setUpFinish", tEnd).apply();
                long offset = tEnd - tStart;
                if (offset < MIN_INTRO_DISPLAY_TIME) {
                    // 这个延迟仅为了让 "Small Logo" 显示足够的时间, 实际应用中不需要
                    getWindow().getDecorView().postDelayed(new Runnable() {
                        @Override
                        public void run() {
                            Small.openUri("main", LaunchActivity.this);
                            finish();
                        }
                    }, (MIN_INTRO_DISPLAY_TIME - offset) / 1000000);
                } else {
                    Small.openUri("main", LaunchActivity.this);
                    finish();
                }
            }
        });

做了这两步就能将工程的模块插件都加载进来。

##Small的三个基本概念介绍

Bundle

这个Bundle可不是我们Android常用来传递数据的Bundle,而是指代插件,类似Android中LoadApk的定位,指代的是Small中的插件,里面控制着每个插件之中的版本号,版本名,目录地址,每个Bundle的包名,类型,规则等等。这个概念也在阿里的Altas插件化框架中出现。

在这里Bundle对模块的名字做了规范,对于公共的模块,模块名字要叫做*.lib.*或 *.lib*.对于应用插件要叫做*.app.* 或 *.app*。或者在下方的bundle.json写好规则,详细的请去官网查看。

bundle.json

这个是Small的配置文件。 Example:

{
  "version": "1.0.0",
  "bundles": [
    {
      "type":"app",
      "uri": "home",
      "pkg": ".bundle.home",
      "rules": {
        "page1": ".MyPage1",
        "page2": ".MyPage2"
      }
    }
  ]
}
属性介绍
type类型 可选 app lib web
uri跳转转的url别名
pkg包名
rules包含的页面数组 如LoginActivity或LoginFragment 则 "login":"Login"

这个文件从一定程度上代替了Android的Mainfest.xml。通过这个文件我们可以轻易的查找到,每个模块插件所包含的信息,从而通过openUri方法跳转。

BundleLauncher

这个是整个Small框架的核心也不为过。这个BundleLauncher作为整个Small如何HookAndroid系统的核心抽象类。详细的等等再说。

这里先放出BundleLauncher类的关系图。

BundleLauncher类图关系.png

这里稍微解释一下,ActivityLauncher主要是解析包的数据,特别是Activity。SoBundleLauncher主要是因为在Small框架中,编译的时候会把每个模块打成so(当然也能选择不大打成so)所以叫做SoBundleLauncher。而ApkBundleLauncher主要是指对Android系统中Activity的流程的反射处理。主要是处理上面我所说的骗过AndroidMainfest.xml的流程。。AssetBundleLuancher主要是对资源的处理。

##Small源码分析 我们跟着demo的流程走一遍源码。这里先放出时序图。 Small启动流程图.png

从上面的时序图,我们可以清晰的清楚,在整个Small的启动加载流程中Bundle作为极其核心的地位,同时处理了三个BundleLauncher的地位。 按照类中的方法分析大致可以归为两类方法和属性。 静态属性都包含着List和List等,静态方法包含register等。了解到Bundle的静态操作是用来控制BundleLauncher和Bundle整体的行为。 而非静态方法,则是单一控制我上面所说的Bundle的概念。

换句话说,Bundle类实际上充当了两个角色Bundle和Bundle、BundleLauncher的控制器。

重新整理一下,Small的启动分两步,第一步perSetUp,setUp前的准备。第二步setUp,主要是运行BundleLauncher和解析bundle.json生成Bundle。

跟着时序图,让我们开始Small的源码之旅。

1.Small perSetUp

    public static void preSetUp(Application context) {
        if (sContext != null) {
            return;
        }

        sContext = context;

        // Register default bundle launchers
        registerLauncher(new ActivityLauncher());
        registerLauncher(new ApkBundleLauncher());
        registerLauncher(new WebBundleLauncher());
        Bundle.onCreateLaunchers(context);
    }

很简单也很重要,这里是通过Context来确定setUp前处理只有一次。接着把BundleLauncher注册到List中,以便后面循环统一调用对应的BundleLauncher的方法。

下面的onCreate的方法。实际上在这三个BundleLauncher中只有ApkBundleLauncher实现了该方法。让我们直接看看这个onCreate的方法做了什么。

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

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

        // Get activity thread
        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);
        }

        // Inject message handler
        ensureInjectMessageHandler(thread);

        // Get providers
        try {
            f = thread.getClass().getDeclaredField("mBoundApplication");
            f.setAccessible(true);
            Object/*AppBindData*/ data = f.get(thread);
            f = data.getClass().getDeclaredField("providers");
            f.setAccessible(true);
            providers = (List<ProviderInfo>) f.get(data);
        } catch (Exception e) {
            throw new RuntimeException("Failed to get providers from thread: " + thread);
        }

        sActivityThread = thread;
        sProviders = providers;
        sHostInstrumentation = base;
        sBundleInstrumentation = wrapper;
    }

殊途同归,根据注释,反射的名称和我之前所说的插件化基础模型,我们可以轻松的知道这个onCreate的思路。

第一步先获取ActivityThread的实例,也就是为插件化基础模型后半段做预备处理。

第二步获取mInstrumentation的实例,并把我们的mInstrumentation设置进去。实际上是为插件化基础模型前半段做准备。

解释:

Small开发的作者并没有像DroidPlugin的作者一样,直接获取AMS的跨进程通信对象而是获取了Instrument这个类的对象。结合我最上面的Activity启动的时序图,你会发现Instrument也是在Activity传递给传递给AMS之前,准确的说是在Instrument检测AndroidMainfest之前处理。

但是不是说好的动态代理只能代理实现接口的类吗?这就有涉及到了另外一个下钩子的技能。通过继承,让Instrument内部的方法对我们开放,这个方法我也经常使用。

这种做法的好处是什么呢?

避免了ActivityManagerNative的实例的位置出现了变动。

Small作者所担心的事情确实出现了,在Android 8.0中,ActivityManangerNative的单例存放位置出现了变动,跑到了ActivityManager中了。所以比较新的插件化框架都选择了这种方式去处理,就怕这个实例的位置再次变动。

那么为什么Hook Instrument呢?好处很明显,这个类实际上是暴露给测试用的,权限是public,也就是说他变动的可能性不大。所以综合考虑,给Instrument下钩子优于ActivityManangerNative。

看看new ApkBundleLauncher.InstrumentationWrapper(base)的继承关系

protected static class InstrumentationWrapper extends Instrumentation
            implements InstrumentationInternal 

这个类,我们想要关注什么函数,直接在对应的函数中重写即可。这实际上也是用一种桥接的设计模式。重写方法,实际上工作的base实例。

既然如此,我们看看,我们关注的函数execStartActivity。

这个函数分为三步:

第一步,包裹真实的Intent,用代理的Activity欺骗Android系统的检测。
第二步, 反射ActivityThread的mH这个Handler
第三步,反射调用execStartActivity这个方法。

详细就暂时不铺开说,之后我们再把这些线索串到一起。

第三步获取Providers。获取ContentProvide组件。

熟悉源码的人都知道实际上在绑定Application的时候,会获取一个App内部所有的provider。详细就不铺开说了。想要详细了解的,可以去看看任玉刚的书。

基础模型前后都准备好了,只剩下类的加载和资源加载都明白了,就知道Small的基本原理是什么了。

Small SetUp

根据上面的启动时序图,会直接走到loadBundles中,加载Bundle数据也就是模块插件。 直接看看源码。

private static void loadBundles(Context context) {
        JSONObject manifestData;
        try {
            File patchManifestFile = getPatchManifestFile();
            String manifestJson = getCacheManifest();
            if (manifestJson != null) {
                // Load from cache and save as patch
                if (!patchManifestFile.exists()) patchManifestFile.createNewFile();
                PrintWriter pw = new PrintWriter(new FileOutputStream(patchManifestFile));
                pw.print(manifestJson);
                pw.flush();
                pw.close();
                // Clear cache
                setCacheManifest(null);
            } else if (patchManifestFile.exists()) {
                // Load from patch
                BufferedReader br = new BufferedReader(new FileReader(patchManifestFile));
                StringBuilder sb = new StringBuilder();
                String line;
                while ((line = br.readLine()) != null) {
                    sb.append(line);
                }

                br.close();
                manifestJson = sb.toString();
            } else {
                // Load from built-in `assets/bundle.json'
                InputStream builtinManifestStream = context.getAssets().open(BUNDLE_MANIFEST_NAME);
                int builtinSize = builtinManifestStream.available();
                byte[] buffer = new byte[builtinSize];
                builtinManifestStream.read(buffer);
                builtinManifestStream.close();
                manifestJson = new String(buffer, 0, builtinSize);
            }

            // 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);
    }

这一段的源码实际上是在线程中处理的。我们可以从这段代码得知,实际上,Bundle第一次加载都会从asset的bundle.json文件中读取数据,通过这个json数据把插件里面的bundle实例化出来加载到List。看看parseManifest的方法。

    private static Manifest parseManifest(String version, JSONObject data) {
        if (version.equals("1.0.0")) {
            try {
                JSONArray bundleDescs = data.getJSONArray(BUNDLES_KEY);
                int N = bundleDescs.length();
                List<Bundle> bundles = new ArrayList<Bundle>(N);
                for (int i = 0; i < N; i++) {
                    try {
                        JSONObject object = bundleDescs.getJSONObject(i);
                        Bundle bundle = new Bundle(object);
                        bundles.add(bundle);
                    } catch (JSONException e) {
                        // Ignored
                    }
                }
                Manifest manifest = new Manifest();
                manifest.version = version;
                manifest.bundles = bundles;
                return manifest;
            } catch (JSONException e) {
                e.printStackTrace();
                return null;
            }
        }

        throw new UnsupportedOperationException("Unknown version " + version);
    }

通过bundle.json就能正确获取到每个插件中的信息了,以待后面调用。

setupLaunchers

实际上就是轮询加入到了BundleLauncher的List,调用setUp的方法。 依照顺序来看看都做了什么处理。

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

        // Read the registered classes in host's manifest file
        File sourceFile = new File(context.getApplicationInfo().sourceDir);
        BundleParser parser = BundleParser.parsePackage(sourceFile, context.getPackageName());
        parser.collectActivities();
        ActivityInfo[] as = parser.getPackageInfo().activities;
        if (as != null) {
            sActivityClasses = new HashSet<String>();
            for (ActivityInfo ai : as) {
                sActivityClasses.add(ai.name);
            }
        }
    }

这个方法主要动作实际上是解析包内部的数据。获取ActivityInfo,并且加入到sActivityClasses中的List,用于快速检测是否有这个Activity。

稍微看看如何解析的。

public static BundleParser parsePackage(File sourceFile, String packageName) {
        if (sourceFile == null || !sourceFile.exists()) return null;

        BundleParser bp = new BundleParser(sourceFile, packageName);
        if (!bp.parsePackage()) return null;

        return bp;
    }

    public boolean parsePackage() {
        AssetManager assmgr = null;
        boolean assetError = true;
        try {
            assmgr = ReflectAccelerator.newAssetManager();
            if (assmgr == null) return false;

            int cookie = ReflectAccelerator.addAssetPath(assmgr, mArchiveSourcePath);
            if(cookie != 0) {
                parser = assmgr.openXmlResourceParser(cookie, "AndroidManifest.xml");
                assetError = false;
            } else {
                Log.w(TAG, "Failed adding asset path:"+mArchiveSourcePath);
            }
        } catch (Exception e) {
            Log.w(TAG, "Unable to read AndroidManifest.xml of "
                    + mArchiveSourcePath, e);
        }
        if (assetError) {
            if (assmgr != null) assmgr.close();
            return false;
        }

        res = new Resources(assmgr, mContext.getResources().getDisplayMetrics(), null);
        return parsePackage(res, parser);
    }

首先Small会在编译的时候会重新打包一次,通过Gradle的插件在AndroidManifest.xml中插入用来占坑的代理Activity。接着通过AssetMananger.addAssetPath解析整个包的资源。在这里补充一下,在上面资源查找的时序图中的getOrCreateResources方法中实际上就是调用AssetMananger.addAssetPath来解析的。这里是直接调用该方法解析。

public boolean collectActivities() {
        if (mPackageInfo == null || mPackageInfo.applicationInfo == null) return false;
        AttributeSet attrs = parser;

        int type;
        try {
            List<ActivityInfo> activities = new ArrayList<ActivityInfo>();
            while ((type = parser.next()) != XmlResourceParser.END_DOCUMENT) {
                if (type != XmlResourceParser.START_TAG) {
                    continue;
                }

                String tagName = parser.getName();
                if (!tagName.equals("activity")) continue;

                // <activity ...
                ActivityInfo ai = new ActivityInfo();
                ai.applicationInfo = mPackageInfo.applicationInfo;
                ai.packageName = ai.applicationInfo.packageName;

                TypedArray sa = res.obtainAttributes(attrs,
                        R.styleable.AndroidManifestActivity);
                String name = sa.getString(R.styleable.AndroidManifestActivity_name);
                if (name != null) {
                    ai.name = ai.targetActivity = buildClassName(mPackageName, name);
                }

              //资源解析,该出省略
                ...

                activities.add(ai);

                sa.recycle();

                // <intent-filter ...
                List<IntentFilter> intents = new ArrayList<IntentFilter>();
                int outerDepth = parser.getDepth();
                while ((type=parser.next()) != XmlResourceParser.END_DOCUMENT
                        && (type != XmlResourceParser.END_TAG
                        || parser.getDepth() > outerDepth)) {
                    if (type == XmlResourceParser.END_TAG || type == XmlResourceParser.TEXT) {
                        continue;
                    }

                    if (parser.getName().equals("intent-filter")) {
                        IntentFilter intent = new IntentFilter();
                        
                        parseIntent(res, parser, attrs, true, true, intent);

                        if (intent.countActions() == 0) {
                            Log.w(TAG, "No actions in intent filter at "
                                    + mArchiveSourcePath + " "
                                    + parser.getPositionDescription());
                        } else {
                            intents.add(intent);
                            if (intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
                                mLauncherActivityName = ai.name;
                            }
                        }
                    }
                }

                if (intents.size() > 0) {
                    if (mIntentFilters == null) {
                        mIntentFilters = new ConcurrentHashMap<String, List<IntentFilter>>();
                    }
                    mIntentFilters.put(ai.name, intents);
                }
            }

            int N = activities.size();
            if (N > 0) {
                mPackageInfo.activities = new ActivityInfo[N];
                mPackageInfo.activities = activities.toArray(mPackageInfo.activities);
            }
            return true;
        } catch (XmlPullParserException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

这个方法就和上面DroidPlugin的思路不一样,上面DroidPlugin是希望让Android系统帮我们生成ActivityInfo,而这个是通过解析AndroidManifest.xml自己ActivityInfo。

这样关键点ActivityInfo就获取到了。同时ActivityLauncher也就完成了。

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

        Field f;

        // 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) {
            Log.e(TAG, "Failed to hook TaskStackBuilder. \n" +
                    "Please manually call `Small.wrapIntent` to ensure the notification intent can be opened. \n" +
                    "See https://github.com/wequick/Small/issues/547 for details.");
        }
    }

这个ApkLauncher的setUp方法是对TaskStackBuilder的进行一次包装,这个类用来创建一个回退栈。经常在点击通知的时候,通过这个类跳转到我们的需要跳转的Activity。点击回退就会到AndroidManifest.xml中配置的Activity。详细也不铺开说。由于也是启动Activity,那么也是用包装来骗过Android来打开插件的Activity。

WebBundleLauncher
@Override
    public void setUp(Context context) {
        super.setUp(context);
        if (Build.VERSION.SDK_INT < 24) return;

        Bundle.postUI(new Runnable() {
            @Override
            public void run() {
                // In android 7.0+, on firstly create WebView, it will replace the application
                // assets with the one who has join the WebView asset path.
                // If this happens after our assets replacement,
                // what we have done would be come to naught!
                // So, we need to push it enOOOgh ahead! (#347)
                new android.webkit.WebView(Small.getContext());
            }
        });
    }

对于7.0+的,WebView特殊处理。注释上已经很清晰了。就是因为第一次创建WebView会替换掉那些已经加入WebView的application的assetPath。所以需要先建立一个做准备。

loadBundles

private static void loadBundles(List<Bundle> bundles) {
        sPreloadBundles = bundles;

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

        // Handle I/O
        if (sIOActions != null) {
            ExecutorService executor = Executors.newFixedThreadPool(sIOActions.size());
            for (Runnable action : sIOActions) {
                executor.execute(action);
            }
            executor.shutdown();
            try {
                if (!executor.awaitTermination(LOADING_TIMEOUT_MINUTES, TimeUnit.MINUTES)) {
                    throw new RuntimeException("Failed to load bundles! (TIMEOUT > "
                            + LOADING_TIMEOUT_MINUTES + "minutes)");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            sIOActions = null;
        }

        // Wait for the things to be done on UI thread before `postSetUp`,
        // as on 7.0+ we should wait a WebView been initialized. (#347)
        while (sRunningUIActionCount != 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

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

        // Wait for the things to be done on UI thread after `postSetUp`,
        // like creating a bundle application.
        while (sRunningUIActionCount != 0) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 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的数据。分为以下三步,并分别解析:

1.每个Bundle通过prepareForLaunch做一次使用Bundle前处理,之后做真正的loadBundle。

在调用prepareForLaunch会直接调用每个BundleLauncher的resolveBundle方法。

protected void prepareForLaunch() {
        if (mIntent != null) return;

        if (mApplicableLauncher == null && sBundleLaunchers != null) {
            for (BundleLauncher launcher : sBundleLaunchers) {
                if (launcher.resolveBundle(this)) {
                    mApplicableLauncher = launcher;
                    break;
                }
            }
        }
    }
public boolean resolveBundle(Bundle bundle) {
        if (!preloadBundle(bundle)) return false;

        loadBundle(bundle);
        return true;
    }

看看每个BundleLauncher的preloadBundle方法。

ActivityLauncher
@Override
    public boolean preloadBundle(Bundle bundle) {
        if (sActivityClasses == null) return false;

        String pkg = bundle.getPackageName();
        return (pkg == null || pkg.equals("main"));
    }

检测之前解析好的sActivityClasses究竟是否为空,以及bundle传进来的是否为空或者为main主模块名称。做这个判断只要是为了检查是否解析包名。同时检查出哪些宿主哪些是插件 。

而在这个BundleLauncher并没有重写loadBundle。也就是说没有这一步。

SoBundleLauncher
@Override
    public boolean preloadBundle(Bundle bundle) {
        String packageName = bundle.getPackageName();
        if (packageName == null) return false;

        // Check if supporting
        String[] types = getSupportingTypes();
        if (types == null) return false;

        boolean supporting = false;
        String bundleType = bundle.getType();
        if (bundleType != null) {
            // Consider user-defined type in `bundle.json'
            for (String type : types) {
                if (type.equals(bundleType)) {
                    supporting = true;
                    break;
                }
            }
        } else {
            // Consider explicit type specify in package name as following:
            //  - com.example.[type].any
            //  - com.example.[type]any
            String[] pkgs = packageName.split("\\.");
            int N = pkgs.length;
            String aloneType = N > 1 ? pkgs[N - 2] : null;
            String lastComponent = pkgs[N - 1];
            for (String type : types) {
                if ((aloneType != null && aloneType.equals(type))
                        || lastComponent.startsWith(type)) {
                    supporting = true;
                    break;
                }
            }
        }
        if (!supporting) return false;

        // Initialize the extract path
        File extractPath = getExtractPath(bundle);
        if (extractPath != null) {
            if (!extractPath.exists()) {
                extractPath.mkdirs();
            }
            bundle.setExtractPath(extractPath);
        }

        // Select the bundle entry-point, `built-in' or `patch'
        File plugin = bundle.getBuiltinFile();
        BundleParser parser = BundleParser.parsePackage(plugin, packageName);
        File patch = bundle.getPatchFile();
        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
            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;
    }

这段代码主要是检测SoBundleLauncher支持怎么样的类型,通过抽象方法getSupportingTypes确定。接着再去解析Bundle,这一次的解析和上面的不一样,这一次的解析决定了是使用Asset中外部的插件,还是我们本工程的模块。

再来看看剩下两个子类launcher的loadBundle

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

        BundleParser parser = bundle.getParser();
        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.packageName = packageName;
            apk.path = apkPath;
            apk.nonResources = parser.isNonResources();
            if (pluginInfo.applicationInfo != null) {
                apk.applicationName = pluginInfo.applicationInfo.className;
            }
            apk.packagePath = bundle.getExtractPath();
            apk.optDexFile = new File(apk.packagePath, FILE_DEX);

            // 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;
        }

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

        // Record intent-filters for implicit action
        ConcurrentHashMap<String, List<IntentFilter>> filters = parser.getIntentFilters();
        if (filters != null) {
            if (sLoadedIntentFilters == null) {
                sLoadedIntentFilters = new ConcurrentHashMap<String, List<IntentFilter>>();
            }
            sLoadedIntentFilters.putAll(filters);
        }

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

这里的操作就是我上面个插件化基础模型中,寻找ClassLoader,加载类的步骤。这里是Small作者自定义自己的一个LoadApk类,丢到Map中自己亲自管理。通过上面setUp的步骤解析出来的信息,自己亲自生成一个和系统一样的LoadedApk。同时设置好dex文件的位置,为后面postSetUp对ClassLoader的处理做准备。

AssetBundleLauncher
@Override
    public void loadBundle(Bundle bundle) {
        String packageName = bundle.getPackageName();
        File unzipDir = new File(getBasePath(), packageName);
        File indexFile = new File(unzipDir, getIndexFileName());

        // Prepare index url
        String uri = indexFile.toURI().toString();
        if (bundle.getQuery() != null) {
            uri += "?" + bundle.getQuery();
        }
        URL url;
        try {
            url = new URL(uri);
        } catch (MalformedURLException e) {
            Log.e(TAG, "Failed to parse url " + uri + " for bundle " + packageName);
            return;
        }
        String scheme = url.getProtocol();
        if (!scheme.equals("http") &&
                !scheme.equals("https") &&
                !scheme.equals("file")) {
            Log.e(TAG, "Unsupported scheme " + scheme + " for bundle " + packageName);
            return;
        }
        bundle.setURL(url);
    }

检验每个Bundle对应的Url是否合法,合法则设置。

2.把之前通过postIO放入的线程操作全部运行
3.每个BundleLauncher postSetUp setUp完毕处理

我们一样跟着顺序来看。 ######ActivityLauncher 并没有重写postSetUp方法。

ApkBundleLauncher
@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
        final Application app = Small.getContext();
        String[] paths = new String[apks.size() + 1];
        paths[0] = app.getPackageResourcePath(); // add host asset path
        int i = 1;
        for (LoadedApk apk : apks) {
            if (apk.nonResources) continue; // ignores the empty entry to fix #62
            paths[i++] = apk.path; // add plugin asset path
        }
        if (i != paths.length) {
            paths = Arrays.copyOf(paths, i);
        }
        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++;
        }
        ReflectAccelerator.expandDexPathList(cl, dexPaths, dexFiles);

        // Expand the native library directories for host class loader if plugin has any JNIs. (#79)
        List<File> libPathList = new ArrayList<File>();
        for (LoadedApk apk : apks) {
            if (apk.libraryPath != null) {
                libPathList.add(apk.libraryPath);
            }
        }
        if (libPathList.size() > 0) {
            ReflectAccelerator.expandNativeLibraryDirectories(cl, libPathList);
        }

        // Trigger all the bundle application `onCreate' event
        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
        ...

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

####这个是重点。Small对于类和资源的方式 我们回顾以下我上面写的插件化的基础模型。该反射的类有了,基础模型前后部分都准备好了,还差什么?当然还差怎么找插件的类和资源了?和DroidPlugin不同,Small选择了保守的方式来加载类和资源。

####那么资源是怎么加载呢? 这里要展开ResourcesManager来看看是怎么回事。根据我的流程图走到了getResources就会走内部的getOrCreateResources的方法。注意这里和Android6.0的源码有比较大的区别,这里就展开说了,还是按照7.0的源码说明。

    private @NonNull Resources getOrCreateResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        synchronized (this) {
           ...
            if (activityToken != null) {
                final ActivityResources activityResources =
                        getOrCreateActivityResourcesStructLocked(activityToken);

                // Clean up any dead references so they don't pile up.
                ArrayUtils.unstableRemoveIf(activityResources.activityResources,
                        sEmptyReferencePredicate);

                // Rebase the key's override config on top of the Activity's base override.
               ...

                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    if (DEBUG) {
                        Slog.d(TAG, "- using existing impl=" + resourcesImpl);
                    }
                    return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                            resourcesImpl);
                }

                // We will create the ResourcesImpl object outside of holding this lock.

            } else {
                // Clean up any dead references so they don't pile up.
                ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);

                // Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    if (DEBUG) {
                        Slog.d(TAG, "- using existing impl=" + resourcesImpl);
                    }
                    return getOrCreateResourcesLocked(classLoader, resourcesImpl);
                }

                // We will create the ResourcesImpl object outside of holding this lock.
            }
        }

        // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
        ResourcesImpl resourcesImpl = createResourcesImpl(key);

        synchronized (this) {
            ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key);
            if (existingResourcesImpl != null) {
                if (DEBUG) {
                    Slog.d(TAG, "- got beat! existing impl=" + existingResourcesImpl
                            + " new impl=" + resourcesImpl);
                }
                resourcesImpl.getAssets().close();
                resourcesImpl = existingResourcesImpl;
            } else {
                // Add this ResourcesImpl to the cache.
                mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
            }

            final Resources resources;
            if (activityToken != null) {
                resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                        resourcesImpl);
            } else {
                resources = getOrCreateResourcesLocked(classLoader, resourcesImpl);
            }
            return resources;
        }
    }

根据上面的时序图,由于我们应用第一次查找资源的时候,activitytoken传进来是null。

都会走activityToken判断的下侧。首先清空缓存中不需要的资源,接着通过ResourceKey从mResourceImpls这个中寻找寻找ResourcesImpl这个资源实现类。找到则返回。

private ResourcesImpl findResourcesImplForKeyLocked(@NonNull ResourcesKey key) {
        WeakReference<ResourcesImpl> weakImplRef = mResourceImpls.get(key);
        ResourcesImpl impl = weakImplRef != null ? weakImplRef.get() : null;
        if (impl != null && impl.getAssets().isUpToDate()) {
            return impl;
        }
        return null;
    }

没有找到则从createResourcesImpl新建一个ResourcesImpl存放到mResourceImpls中。最后再通过getOrCreateResourcesLocked生成Resources类。

  private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,
            @NonNull ResourcesImpl impl) {
        // Find an existing Resources that has this ResourcesImpl set.
        final int refCount = mResourceReferences.size();
        for (int i = 0; i < refCount; i++) {
            WeakReference<Resources> weakResourceRef = mResourceReferences.get(i);
            Resources resources = weakResourceRef.get();
            if (resources != null &&
                    Objects.equals(resources.getClassLoader(), classLoader) &&
                    resources.getImpl() == impl) {
                if (DEBUG) {
                    Slog.d(TAG, "- using existing ref=" + resources);
                }
                return resources;
            }
        }

        // Create a new Resources reference and use the existing ResourcesImpl object.
        Resources resources = new Resources(classLoader);
        resources.setImpl(impl);
        mResourceReferences.add(new WeakReference<>(resources));
        if (DEBUG) {
            Slog.d(TAG, "- creating new ref=" + resources);
            Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
        }
        return resources;
    }

这样大家就明白实际上所有的ResourceImpl都会缓存到mResourceImpls,而Resource则会缓存到mResourceReferences。和Android6.0/5.0不同,6.0/5.0会缓存到mActiveResources中。

那么就会明白了,如果我们能够把反射获取这两个位置,将对应的Resources和ResourceImpl,把Resources加入。从原理上就能获取到Android的资源管理体系。

但是,问题还没有彻底解决,不同于DroidPlugin的思路,这里我们并非是把事情委托给系统完成,而是我们一步步来解决这些问题。所以我们还需要把对应路径下的资源读取出来才行,不然一切都是假的。

我们看看AssetManager源码和*ResourcesImpl的构造方法 和Android6.0之前不一样的是,以前的Resources会直接管理AssetManager,而在这里就被ResourcesImpl接管。所以我们要去ResourcesImpl的构造方法。

private @NonNull ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
        final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
        daj.setCompatibilityInfo(key.mCompatInfo);

        final AssetManager assets = createAssetManager(key);
        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
        if (DEBUG) {
            Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
        }
        return impl;
    }

这个createAssetManager就是关键。

 if (key.mResDir != null) {
            if (assets.addAssetPath(key.mResDir) == 0) {
                throw new Resources.NotFoundException("failed to add asset path " + key.mResDir);
            }
        }

其中这一段就是通过mResDir来生成对应的资源的管理类AssetManager。结合我之前的源码分析,很简单明白,如果我们想要获得自己的Resources,就要自己去调用一次这个方法,把生成的AssetManager生成一个ResourcesImpl就能生成自己想要的Resources了。

ReflectAccelerator.mergeResources(app, sActivityThread, paths);

Small 的资源加载源码

思路明白了,看看Small是怎么做的,这里只关注7.0部分。

public static void mergeResources(Application app, Object activityThread, String[] assetPaths) {
//第一段
        AssetManager newAssetManager;
        if (Build.VERSION.SDK_INT < 24) {
            newAssetManager = newAssetManager();
        } else {
            // On Android 7.0+, this should contains a WebView asset as base. #347
            newAssetManager = app.getAssets();
        }
        addAssetPaths(newAssetManager, assetPaths);
//第二段
        try {
            if (Build.VERSION.SDK_INT < 28) {
                Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks", new Class[0]);
                mEnsureStringBlocks.setAccessible(true);
                mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
            } else {
                // `AssetManager#ensureStringBlocks` becomes unavailable since android 9.0
            }

            Collection<WeakReference<Resources>> references;

            if (Build.VERSION.SDK_INT >= 19) {
                Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
                Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance", new Class[0]);
                mGetInstance.setAccessible(true);
                Object resourcesManager = mGetInstance.invoke(null, new Object[0]);
                try {
                    ...
                } catch (NoSuchFieldException ignore) {
                    Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
                    mResourceReferences.setAccessible(true);

                    references = (Collection) mResourceReferences.get(resourcesManager);
                }

                if (Build.VERSION.SDK_INT >= 24) {
                    Field fMResourceImpls = resourcesManagerClass.getDeclaredField("mResourceImpls");
                    fMResourceImpls.setAccessible(true);
                    sResourceImpls = (ArrayMap)fMResourceImpls.get(resourcesManager);
                }
            } else {
               ...
            }

            //to array
            WeakReference[] referenceArrays = new WeakReference[references.size()];
            references.toArray(referenceArrays);

            for (int i = 0; i < referenceArrays.length; i++) {
                Resources resources = (Resources) referenceArrays[i].get();
                if (resources == null) continue;

                try {
                    Field mAssets = Resources.class.getDeclaredField("mAssets");
                    mAssets.setAccessible(true);
                    mAssets.set(resources, newAssetManager);
                } catch (Throwable ignore) {
                    Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
                    mResourcesImpl.setAccessible(true);
                    Object resourceImpl = mResourcesImpl.get(resources);
                    Field implAssets;
                    try {
                        implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
                    } catch (NoSuchFieldException e) {
                        // Compat for MiUI 8+
                        implAssets = resourceImpl.getClass().getSuperclass().getDeclaredField("mAssets");
                    }
                    implAssets.setAccessible(true);
                    implAssets.set(resourceImpl, newAssetManager);

                    if (Build.VERSION.SDK_INT >= 24) {
                        if (resources == app.getResources()) {
                            sMergedResourcesImpl = resourceImpl;
                        }
                    }
                }

                resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
            }
//第三段
            if (Build.VERSION.SDK_INT >= 21) {
                for (int i = 0; i < referenceArrays.length; i++) {
                    Resources resources = (Resources) referenceArrays[i].get();
                    if (resources == null) continue;

                    // android.util.Pools$SynchronizedPool<TypedArray>
                    Field mTypedArrayPool = Resources.class.getDeclaredField("mTypedArrayPool");
                    mTypedArrayPool.setAccessible(true);
                    Object typedArrayPool = mTypedArrayPool.get(resources);
                    // Clear all the pools
                    Method acquire = typedArrayPool.getClass().getMethod("acquire");
                    acquire.setAccessible(true);
                    while (acquire.invoke(typedArrayPool) != null) ;
                }
            }
        } catch (Throwable e) {
            throw new IllegalStateException(e);
        }
    }

这里的思路稍微有点不一样,Small作者的想法是merge也就是合并资源。也就是使用原有的AssetManager,但是把原来的资源路径添加进去。

也就是说ResourcesImpl的Assetmanager换成我们新的AssetManager,让它能够找到我们的资源。

第一段,就是新建一个AssetManager,并且通过addAssetPaths把资源读入AssetManager中。

第二段,在低于api28的时候,我们还需要调用一次ensureStringBlocks确定native层也加载了资源数据。

获取ResourcesManager的单例,接着获取mResourceReferences这个属性,拿到Resources这个List集合。如果Api大于24则会获取mResourceImpls这个ResourcesImpl的ArrayMap集合。把这个集合取出放到全局变量中。

第三段,则是清空typedArrayPool这个用来缓存TypeArray缓存的Pool。为的是避免出现一些错误。

记住sMergedResourcesImpl这个ResourcesImpl,因为这个直接放入mResourceImpls这个集合中有个问题,那就是实际上Map的key是一个弱引用,gc以来就丢失,所以Small的作者就放到了跳转的时候再一次处理。这个问题我们之后等到Activity的启动的时候,就能看到了。

这里稍微提一句,就算是合并了也存可能存在id可能一致而导致映射错误的情况,这种情况在去年尝试着用这种方式加载资源的时候遇到过。Small的解决办法是通过gradle重新打包,重新分配资源id。这里涉及到native层的源码,这里不铺开说了,详细的可以看源码。

如何Small分配资源id的规则: github.com/wequick/Sma…

####资源找到了,那类又怎么找到呢?

那么什么是保守的方式呢?我之前举出了两种方式,第二种就是看看能不能在BaseDexClassLoader做点事情,让他能找到我们的Class。这样我们又要稍微看看源码了。

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

在BaseDexClassLoader中我们最后解析apk的dex数据都会到这个DexPathList中。

对于DexPathList这个类,注解是这么说的

 * A pair of lists of entries, associated with a {@code ClassLoader}.
 * One of the lists is a dex/resource path &mdash; typically referred
 * to as a "class path" &mdash; list, and the other names directories
 * containing native code libraries. Class path entries may be any of:
 * a {@code .jar} or {@code .zip} file containing an optional
 * top-level {@code classes.dex} file as well as arbitrary resources,
 * or a plain {@code .dex} file (with no possibility of associated
 * resources).
 *
 * <p>This class also contains methods to use these lists to look up
 * classes and resources.</p>

这里面包含了如何寻找相关类方法。也就是说我们只要获取这个对象,把我们插件dex的数据加到DexPathList的解析后面,就能通过App本身的ClassLoader找到我们的类了。

这就是我们所说的保守方式。

那解析的的dex数据又放在哪里呢?

    /**
     * List of dex/resource (class path) elements.
     * Should be called pathElements, but the Facebook app uses reflection
     * to modify 'dexElements' (http://b/7726934).
     */
    private Element[] dexElements;

就是这个dexElements,见名知其意。就是用来存放dex元素的。也就是说我们往这个dexElements后面添加我们解析好的数组就ok了。这个Element又是什么?实际上就是DexPathList的内部类,我们只要知道怎么把dex文件转化为Element就OK了。

public Element(File dir, boolean isDirectory, File zip, DexFile dexFile) {
            this.dir = dir;
            this.isDirectory = isDirectory;
            this.zip = zip;
            this.dexFile = dexFile;
        }

Small类的加载与原理解析

这里就直接看看Small怎么处理的。 调用方法如下:

ReflectAccelerator.expandDexPathList(cl, dexPaths, dexFiles);

核心方法:

public static boolean expandDexPathList(ClassLoader cl,
                                                String[] dexPaths, DexFile[] dexFiles) {
            try {
                int N = dexPaths.length;
                Object[] elements = new Object[N];
                for (int i = 0; i < N; i++) {
                    String dexPath = dexPaths[i];
                    File pkg = new File(dexPath);
                    DexFile dexFile = dexFiles[i];
                    elements[i] = makeDexElement(pkg, dexFile);
                }

                fillDexPathList(cl, elements);
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
            return true;
        }

makeDexElement就是创造Element,就是简单的反射构造器,这里不解释。看看合并。

private static void fillDexPathList(ClassLoader cl, Object[] elements)
                throws NoSuchFieldException, IllegalAccessException {
            if (sPathListField == null) {
                sPathListField = getDeclaredField(DexClassLoader.class.getSuperclass(), "pathList");
            }
            Object pathList = sPathListField.get(cl);
            if (sDexElementsField == null) {
                sDexElementsField = getDeclaredField(pathList.getClass(), "dexElements");
            }
            expandArray(pathList, sDexElementsField, elements, true);
        }

果然是这样先反射获取DexElement在获取里面的Element数组

private static void expandArray(Object target, Field arrField,
                                    Object[] extraElements, boolean push)
            throws IllegalAccessException {
        Object[] original = (Object[]) arrField.get(target);
        Object[] combined = (Object[]) Array.newInstance(
                original.getClass().getComponentType(), original.length + extraElements.length);
        if (push) {
            System.arraycopy(extraElements, 0, combined, 0, extraElements.length);
            System.arraycopy(original, 0, combined, extraElements.length, original.length);
        } else {
            System.arraycopy(original, 0, combined, 0, original.length);
            System.arraycopy(extraElements, 0, combined, original.length, extraElements.length);
        }
        arrField.set(target, combined);
    }

接下来就是简单的把两个数组拼接到一起,再把数据设置回去就万事具备了。

WebBundleLauncher

并没重写postSetUp。

总的来说,Small将会在启动的时候,提供各种用来线索,组成了一个Small的插件化框架。只要等到我们使用openUri的时候,将会通过uri作为钥匙,把这些线索全部串起来。 那么这里可以给每个LauncherBundler在启动的时候整理出如下图的生命周期。

BundleLauncher的启动生命周期.png

##Small启动Activity 让我们看看Small是怎么启动Activity的。

Small.openUri("main", LaunchActivity.this);

 public static boolean openUri(String uriString, Context context) {
        return openUri(makeUri(uriString), context);
    }

public static boolean openUri(Uri uri, Context context) {
        // System url schemes
        String scheme = uri.getScheme();
        if (scheme != null
                && !scheme.equals("http")
                && !scheme.equals("https")
                && !scheme.equals("file")
                && ApplicationUtils.canOpenUri(uri, context)) {
            ApplicationUtils.openUri(uri, context);
            return true;
        }

        // Small url schemes
        Bundle bundle = Bundle.getLaunchableBundle(uri);
        if (bundle != null) {
            bundle.launchFrom(context);
            return true;
        }
        return false;
    }

从openUri可以得知,实际上是两步,第一步

protected static Bundle getLaunchableBundle(Uri uri) {
        if (sPreloadBundles != null) {
            for (Bundle bundle : sPreloadBundles) {
                if (bundle.matchesRule(uri)) {
                    if (bundle.mApplicableLauncher == null) {
                        break;
                    }

                    if (!bundle.enabled) return null; // Illegal bundle (invalid signature, etc.)
                    return bundle;
                }
            }
        }

        // Downgrade to show webView
        if (uri.getScheme() != null) {
            Bundle bundle = new Bundle();
            try {
                bundle.url = new URL(uri.toString());
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
            bundle.prepareForLaunch();
            bundle.setQuery(uri.getEncodedQuery()); // Fix issue #6 from Spring-Xu.
            bundle.mApplicableLauncher = new WebBundleLauncher();
            bundle.mApplicableLauncher.prelaunchBundle(bundle);
            return bundle;
        }
        return null;
    }

先从已经预加载的Bundle的List集合中寻找对应规则的的Bundle。如果找不到就认为可能是webview的界面跳转webview,也就是我们路由框架里面常说的降级处理。这个我们不关心,只看启动Activity。

第二步,调用launchFrom,拿到Application当前的主处理Launcher。

protected void launchFrom(Context context) {
        if (mApplicableLauncher != null) {
            mApplicableLauncher.launchBundle(this, context);
        }
    }

这个mApplicableLauncher会在prepareForLaunch判断出当前当前的全局主Bundle的登录处理类为哪个。上面有贴源码。也就是说如果是第一次进来,按照注册是顺序,依次是ActivityLauncher,ApkLauncher,WebLauncher。由于只需要找到第一个判断为true就break了。

所以默认情况下,如果找到预加载的Bundle是主模块则mApplicableLauncher为ActivityLauncher,如果预加载的Bundle是插件则mApplicableLauncher为ApkLauncher。这里只关注ApkLauncher,所以让我们看看ApkLauncher的launchBundle。

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

先看看预登陆之前的准备。

@Override
    public void prelaunchBundle(Bundle bundle) {
        super.prelaunchBundle(bundle);
        Intent intent = new Intent();
        bundle.setIntent(intent);

        // Intent extras - class
        String activityName = bundle.getActivityName();
        if (!ActivityLauncher.containsActivity(activityName)) {
            if (sLoadedActivities == null) {
                throw new ActivityNotFoundException("Unable to find explicit activity class " +
                        "{ " + activityName + " }");
            }

            if (!sLoadedActivities.containsKey(activityName)) {
                if (activityName.endsWith("Activity")) {
                    throw new ActivityNotFoundException("Unable to find explicit activity class " +
                            "{ " + activityName + " }");
                }

                String tempActivityName = activityName + "Activity";
                if (!sLoadedActivities.containsKey(tempActivityName)) {
                    throw new ActivityNotFoundException("Unable to find explicit activity class " +
                            "{ " + activityName + "(Activity) }");
                }

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

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

在这里是先判断主模块是否包含这个Bundle中对应的Activity。接着根据activityname生成我们要跳转的Intent。

再看看登录launchBundle

public void launchBundle(Bundle bundle, Context context) {
        if (!bundle.isLaunchable()) {
            // TODO: Exit app

            return;
        }

        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());
        }
    }

在跳转方法中直接通过launchBundle直接跳转。

接下来的流程就是依照我最早给的Activity启动时序图,会到了mInstrumentation中,别忘了,这个mInstrumentation已经替换成我们的mInstrumentation,会先走我们自己的exactActivity方法。这个方法我早提过了。

       /** @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) {
            wrapIntent(intent);
            ensureInjectMessageHandler(sActivityThread);
            return ReflectAccelerator.execStartActivity(mBase,
                    who, contextThread, token, target, intent, requestCode, options);
        }

wrapIntent 就是做我上面那个插件化基础模型的第一步,在跳转的时候,我们会造一个假的Intent,这个Intent实际上是一个通过gradle插件预编写好的,代理Activity。接着把我们要跳转的Activity的Intent存到代理的(占坑的)Activity的Intent中,设置到Bundle中进行跳转。这样就能骗过AMS了。

ensureInjectMessageHandler,也是Hook了mH,也就是构建了我的插件化基础模型的后半段。接着经过AMS的处理之后,会先走Small的Callback。

 @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case LAUNCH_ACTIVITY:
                    redirectActivity(msg);
                    break;

对Activity做一次重定向。

private void redirectActivity(Message msg) {
            final Object/*ActivityClientRecord*/ r = msg.obj;
            Intent intent = ReflectAccelerator.getIntent(r);
            tryReplaceActivityInfo(intent, new ActivityInfoReplacer() {
                @Override
                public void replace(ActivityInfo targetInfo) {
                    ReflectAccelerator.setActivityInfo(r, targetInfo);
                }
            });
        }

这个时候实际上也就走我的插件化基础模型的后半段。把Intent写解析出来,接着替换回去。借助tryReplaceActivityInfo设置好在ResourcesManager的资源

public static void ensureCacheResources() {
        if (Build.VERSION.SDK_INT < 24) return;
        if (sResourceImpls == null || sMergedResourcesImpl == null) return;

        Set<?> resourceKeys = sResourceImpls.keySet();
        for (Object resourceKey : resourceKeys) {
            WeakReference resourceImpl = (WeakReference)sResourceImpls.get(resourceKey);
            if (resourceImpl != null && resourceImpl.get() != sMergedResourcesImpl) {
                // Sometimes? the weak reference for the key was released by what
                // we can not find the cache resources we had merged before.
                // And the system will recreate a new one which only build with host resources.
                // So we needs to restore the cache. Fix #429.
                // FIXME: we'd better to find the way to KEEP the weak reference.
                sResourceImpls.put(resourceKey, new WeakReference<Object>(sMergedResourcesImpl));
            }
        }
    }

还记得sMergedResourcesImpl吗?这个时候将会将资源设置到sResourceImpls中,之后系统会调用ResourcesMananger生成Resources时候,发现有这个ResourcesImpl就会从插件包中生成正确的资源。

生成好之后别忘了把ActivityInfo设置回去,不然前功尽弃了。

public static void setActivityInfo(Object/*ActivityClientRecord*/ r, ActivityInfo ai) {
        if (sActivityClientRecord_activityInfo_field == null) {
            sActivityClientRecord_activityInfo_field = getDeclaredField(
                    r.getClass(), "activityInfo");
        }
        setValue(sActivityClientRecord_activityInfo_field, r, ai);
    }

##Small思路总结 到这里,就完成了Small的Activity的跳转。根据这个流程我们可以得到一个启动的思维图,比较简单这里就不上时序图

Small的Activity跳转.png

看完源码之后,就很清楚的了解到,如果需要支持新的插件,或者更新插件。我们首先要更新bundle.json,接着重新setup一边Launcher,这样就能找到我们的新插件。实际上demo已经有了很好的演示了。

好的分析好之后,Small究竟从思想和源码的实现上大家估计都心中有数了。Small和RePlugin的比较我们放到后面去说。

别急先喝口水休息一下,我们慢慢来看看RePlugin。