Android Small 源码分析(二) 插件加载过程

这一节我们要继续接着上一节的内容,来讲讲插件的加载过程。

二. 插件加载过程:从Bundle 步入 BundleLauncher

上一节我们读到loadBundle中,里面涉及一个方法prepareForLaunch,我没有继续往下详解,搬来这里继续给大家做个介绍。

(1)步入BundleLauncher的 loadBundle的前期准备。

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

       // Prepare bundle
       for (Bundle bundle : bundles) {
           bundle.prepareForLaunch();
       }
       ...
}
protected void prepareForLaunch() {
       if (mIntent != null) return;

       if (mApplicableLauncher == null && sBundleLaunchers != null) {
           for (BundleLauncher launcher : sBundleLaunchers) {
               if (launcher.resolveBundle(this)) {
                   mApplicableLauncher = launcher;
                   break;
               }
           }
       }
   }

sBundleLaunchers这个变量已经很熟悉,在前面已经多次出现了,我们来回忆一下。



  里面只有三个类ApkBundleLauncher、ActivityLauncher、WebBundleLauncher,前面已经作了部分解释,从上图可知。三者的祖先都是BundleLauncher,resolveBundle方法定义在此处。

/**
     * Called when loading bundles by {@link Bundle#loadLaunchableBundles(Small.OnCompleteListener)}.
     *
     * <p>This method try to preload a bundle, if succeed, load it later.
     *
     * @hide
     * @param bundle the loading bundle
     * @return <tt>true</tt> if the <tt>bundle</tt> is resolved
     */
    public boolean resolveBundle(Bundle bundle) {
        if (!preloadBundle(bundle)) return false;

        loadBundle(bundle);
        return true;
    }

这里会继续调用preloadBundle的方法来判断,然后我发现preloadBundle在ActivityLauncher和SoBundleLauncher(直接影响ApkBundleLauncher、WebBundleLauncher)中重载了。


1.ActivityLauncher的preloadBundle

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

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

sActivityClasses是在ActivityLauncher setUp被填充的,它里面有宿主已经注册好的Activity。sActivityClasses一般不会为null,因此都进入判断pkg地方,pkg即包路径,因此preloadBundle会返回false,resolveBundle也会返回false,导致ActivityLauncher会被略过。
  ActivityLauncher 它的作用是启动宿主的Activity。如果bundle.json里pkg为空或者pkg为”main”的bundle,ActivityLauncher也不做特殊处理,因为loadBundle在ActivityLauncher中不被重载,仍为空方法。当启动的时候,把bundle里的uri当作Activity的类名。URI转换为Activity的名字规则如下:如果URI是空的,默认是MainActivity,否则使用URI,如果类不存在,加“Activity”后缀再进行查找。

2.SoBundleLauncher的preloadBundle

@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) {
         //这里为了解除对先前版本对包名的限制(*.lib.*或者*.app.*),如果在bundle.json声明type属性为“lib”或者“app”,将忽略包名限制
            // 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);//重载在ApkBundleLauncher和WebBundleLauncher中,定义不同的路径。
        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)) {//校验CRC、签名等,通过安排解压IO事项
                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;
    }

BundleParser 是一个精简版的PackageParser.java,用来解析AndroidManifest文件,获取package.versionCode,package.versionName,package.theme,activities,同时也会收集每个Activity对应的intent-filter。 这个类里还定义了一个内部类R,R文件里包含一个styleable类,此文件定义了平台公共资源ID,例如版本号id,版本名称id,主题Id等。

(2)步入BundleLauncher的 loadBundle

这里loadBundle方法在SoBundleLauncher子类(ApkBundleLauncher、AssetBundleLauncher)中被重载,先挑一个简短的看看,也就是AssetBundleLauncher的loadBundle。

  1. AssetBundleLauncher的loadBundle
 @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);
    }

这个过程主要是尝试将默认的index.html 转换成URL对象。

  1. ApkBundleLauncher的loadBundle
      这里主要是通过BundleParser解释AndroidManifest.xml,收集其中的activities,初始化LoadedApk的一些信息,加载Dex文件以及lib库。
    @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) {
            bundle.setLaunchable(false);
            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());
    }

使用sLoadedActivities记录插件中的activities来实现Intent重定向,使用sLoadedIntentFilters记录intent-filters来实现隐式调用。

(3)重新回到Bundle.loadBundles,分发到各个launcher postSetUp()

postSetUp前后都等待WebView 初始化完成,避免WebView asset路径影响。

private static void loadBundles(List<Bundle> bundles) {
        ...
        
        // 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();
            }
        }

        for (BundleLauncher launcher : sBundleLaunchers) {
            launcher.postSetUp();
        }

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

有效的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
        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;
        sActivityThread = null;
    }

这里可以看到它通过ReflectAccelerator.mergeResources去合并资源,里面特意增加对Android7.0的WebView Assets处理。这些过程都在反射的协助下完成。

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

继续通过ReflectAccelerator.expandDexPathList合并Dex

public static boolean expandDexPathList(ClassLoader cl, String[] dexPaths, DexFile[] dexFiles) {
        if (Build.VERSION.SDK_INT < 14) {
            return V9_13.expandDexPathList(cl, dexPaths, dexFiles);
        } else {
            return V14_.expandDexPathList(cl, dexPaths, dexFiles);
        }
    }

ReflectAccelerator.expandNativeLibraryDirectories继续加载JNI Native库:

public static void expandNativeLibraryDirectories(ClassLoader classLoader, List<File> libPath) {
        int v = Build.VERSION.SDK_INT;
        if (v < 14) {
            V9_13.expandNativeLibraryDirectories(classLoader, libPath);
        } else if (v < 23) {
            V14_22.expandNativeLibraryDirectories(classLoader, libPath);
        } else {
            V23_.expandNativeLibraryDirectories(classLoader, libPath);
        }
    }

往下就对每个插件的Application的OnCreate事件进行分发。

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

这里还会加载Provider的内容。

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

Small.preSetUp 已有对providers的处理

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

这里重新加载,是为了增添一些容错处理。

@Override
        public boolean onException(Object obj, Throwable e) {
            if (sProviders != null && e.getClass().equals(ClassNotFoundException.class)) {
                boolean errorOnInstallProvider = false;
                StackTraceElement[] stacks = e.getStackTrace();
                for (StackTraceElement st : stacks) {
                    if (st.getMethodName().equals("installProvider")) {
                        errorOnInstallProvider = true;
                        break;
                    }
                }

                if (errorOnInstallProvider) {
                    // We'll reinstall this content provider later, so just ignores it!!!
                    // FIXME: any better way to get the class name?
                    String msg = e.getMessage();
                    final String prefix = "Didn't find class \"";
                    if (msg.startsWith(prefix)) {
                        String providerClazz = msg.substring(prefix.length());
                        providerClazz = providerClazz.substring(0, providerClazz.indexOf("\""));
                        for (ProviderInfo info : sProviders) {
                            if (info.name.equals(providerClazz)) {
                                if (mLazyInitProviders == null) {
                                    mLazyInitProviders = new ArrayList<ProviderInfo>();
                                }
                                mLazyInitProviders.add(info);
                                break;
                            }
                        }
                    }
                    return true;
                }
            }

            return super.onException(obj, e);
        }

至此,启动流程的插件加载也说完了。整个启动过程基本就到这里了。

下一节我会介绍继续Small的更新流程的源码。

敬请期待!!!

第一节:Android Small 源码分析(一) 启动流程

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

推荐阅读更多精彩内容