android插件化開發指南-讀書筆記(2)

8.最簡單的插件化解決方案

  • 插件化技術
    1.合併所有插件的dex,解決插件類加載問題
    2.預先在宿主中聲明所有插件中得四大組件
    3.把插件中的所有資源一次合併到宿主的資源中。

8.1 在宿主清單文件裏聲明插件中的組件

8.2hostapp 加載插件中的類

/**
* * 由於應用程序使用的ClassLoader爲PathClassLoader
 * 最終繼承自 BaseDexClassLoader
 * 查看源碼得知,這個BaseDexClassLoader加載代碼根據一個叫做
 * dexElements的數組進行, 因此我們把包含代碼的dex文件插入這個數組
 * 系統的classLoader就能幫助我們找到這個類
*/
  public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
            throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        // 獲取 BaseDexClassLoader : pathList
        Object pathListObj = RefInvoke.getFieldObject(DexClassLoader.class.getSuperclass(), cl, "pathList");

        // 獲取 PathList: Element[] dexElements
        Object[] dexElements = (Object[]) RefInvoke.getFieldObject(pathListObj, "dexElements");

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

        // 創建一個數組, 用來替換原始的數組
        Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);

        // 構造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 這個構造函數
        Class[] p1 = {File.class, boolean.class, File.class, DexFile.class};
        Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};
        Object o = RefInvoke.createObject(elementClass, p1, v1);

        Object[] toAddElementArray = new Object[] { o };
        // 把原始的elements複製進去
        System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
        // 插件的那個element複製進去
        System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);

        // 替換
        RefInvoke.setFieldObject(pathListObj, "dexElements", newElements);
    }

8.4加載插件中的資源

 private static void reloadInstalledPluginResources(ArrayList<String> pluginPaths) {
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);

            addAssetPath.invoke(assetManager, mBaseContext.getPackageResourcePath());

            for(String pluginPath: pluginPaths) {
                addAssetPath.invoke(assetManager, pluginPath);
            }

            Resources newResources = new Resources(assetManager,
                    mBaseContext.getResources().getDisplayMetrics(),
                    mBaseContext.getResources().getConfiguration());

//            RefInvoke.setFieldObject(mBaseContext, "mResources", newResources);
            //這是最主要的需要替換的,如果不支持插件運行時更新,只留這一個就可以了
            RefInvoke.setFieldObject(mPackageInfo, "mResources", newResources);

            //清除一下之前的resource的數據,釋放一些內存
            //因爲這個resource有可能還被系統持有着,內存都沒被釋放
            //clearResoucesDrawableCache(mNowResources);

            mNowResources = newResources;
            //需要清理mtheme對象,否則通過inflate方式加載資源會報錯
            //如果是activity動態加載插件,則需要把activity的mTheme對象也設置爲null
//            RefInvoke.setFieldObject(mBaseContext, "mTheme", null);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }

9.Activity的插件化解決方案

9.1啓動沒有在清單文件中聲明的插件activity

hook1:ActivityManagerNative 的gDefault變量 是個單例對象。獲取泛型對象
hook2:ActivityThread類中mH,handler中callback回調,如果是startActivity方法,把包裝的Intent轉換成真正要啓動的Activity.
hook3:啓動activity之前 AMS 會檢查當前啓動頁面的信息,也就是LoadedApk對象

case LAUNCH_ACTIVITY: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                } break;


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

            LoadedApk packageInfo = ref != null ? ref.get() : null;
            if (packageInfo == null || (packageInfo.mResources != null
                    && !packageInfo.mResources.getAssets().isUpToDate())) {
                if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
                        : "Loading resource-only package ") + aInfo.packageName
                        + " (in " + (mBoundApplication != null
                                ? mBoundApplication.processName : null)
                        + ")");
                packageInfo =
                    new LoadedApk(this, aInfo, compatInfo, baseLoader,
                            securityViolation, includeCode &&
                            (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);

                if (mSystemThread && "android".equals(aInfo.packageName)) {
                    packageInfo.installSystemApplicationInfo(aInfo,
                            getSystemContext().mPackageInfo.getClassLoader());
                }

                if (differentUser) {
                    // Caching not supported across users
                } else if (includeCode) {
                    mPackages.put(aInfo.packageName,
                            new WeakReference<LoadedApk>(packageInfo));
                } else {
                    mResourcePackages.put(aInfo.packageName,
                            new WeakReference<LoadedApk>(packageInfo));
                }
            }
            return packageInfo;
        }
    }

9.2 故意命中緩存

public class LoadedApkClassLoaderHookHelper {

    public static Map<String, Object> sLoadedApk = new HashMap<String, Object>();

    public static void hookLoadedApkInActivityThread(File apkFile) throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException, InstantiationException {

        // 先獲取到當前的ActivityThread對象
        Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread");

        // 獲取到 mPackages 這個靜態成員變量, 這裏緩存了dex包的信息
        Map mPackages = (Map) RefInvoke.getFieldObject(currentActivityThread, "mPackages");

        //準備兩個參數
        // android.content.res.CompatibilityInfo
        Object defaultCompatibilityInfo = RefInvoke.getStaticFieldObject("android.content.res.CompatibilityInfo", "DEFAULT_COMPATIBILITY_INFO");
        //從apk中取得ApplicationInfo信息
        ApplicationInfo applicationInfo = generateApplicationInfo(apkFile);

        //調用ActivityThread的getPackageInfoNoCheck方法loadedApk,得到,上面兩個數據都是用來做參數的
        Class[] p1 = {ApplicationInfo.class, Class.forName("android.content.res.CompatibilityInfo")};
        Object[] v1 = {applicationInfo, defaultCompatibilityInfo};
        Object loadedApk = RefInvoke.invokeInstanceMethod(currentActivityThread, "getPackageInfoNoCheck", p1, v1);

        //爲插件造一個新的ClassLoader
        String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath();
        String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath();
        ClassLoader classLoader = new CustomClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());
        RefInvoke.setFieldObject(loadedApk, "mClassLoader", classLoader);

        //把插件LoadedApk對象放入緩存
        WeakReference weakReference = new WeakReference(loadedApk);
        mPackages.put(applicationInfo.packageName, weakReference);

        // 由於是弱引用, 因此我們必須在某個地方存一份, 不然容易被GC; 那麼就前功盡棄了.
        sLoadedApk.put(applicationInfo.packageName, loadedApk);
    }

    /**
     * 這個方法的最終目的是調用
     * android.content.pm.PackageParser#generateActivityInfo(android.content.pm.PackageParser.Activity, int, android.content.pm.PackageUserState, int)
     */
    public static ApplicationInfo generateApplicationInfo(File apkFile)
            throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException {

        // 找出需要反射的核心類: android.content.pm.PackageParser
        Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
        Class<?> packageParser$PackageClass = Class.forName("android.content.pm.PackageParser$Package");
        Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");


        // 我們的終極目標: android.content.pm.PackageParser#generateApplicationInfo(android.content.pm.PackageParser.Package,
        // int, android.content.pm.PackageUserState)
        // 要調用這個方法, 需要做很多準備工作; 考驗反射技術的時候到了 - -!
        // 下面, 我們開始這場Hack之旅吧!

        // 首先拿到我們得終極目標: generateApplicationInfo方法
        // API 23 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        // public static ApplicationInfo generateApplicationInfo(Package p, int flags,
        //    PackageUserState state) {
        // 其他Android版本不保證也是如此.


        // 首先, 我們得創建出一個Package對象出來供這個方法調用
        // 而這個需要得對象可以通過 android.content.pm.PackageParser#parsePackage 這個方法返回得 Package對象得字段獲取得到
        // 創建出一個PackageParser對象供使用
        Object packageParser = packageParserClass.newInstance();

        // 調用 PackageParser.parsePackage 解析apk的信息
        // 實際上是一個 android.content.pm.PackageParser.Package 對象
        Class[] p1 = {File.class, int.class};
        Object[] v1 = {apkFile, 0};
        Object packageObj = RefInvoke.invokeInstanceMethod(packageParser, "parsePackage", p1, v1);


        // 第三個參數 mDefaultPackageUserState 我們直接使用默認構造函數構造一個出來即可
        Object defaultPackageUserState = packageUserStateClass.newInstance();

        // 萬事具備!!!!!!!!!!!!!!
        Class[] p2 = {packageParser$PackageClass, int.class, packageUserStateClass};
        Object[] v2 = {packageObj, 0, defaultPackageUserState};
        ApplicationInfo applicationInfo = (ApplicationInfo)RefInvoke.invokeInstanceMethod(packageParser, "generateApplicationInfo", p2, v2);

        String apkPath = apkFile.getPath();
        applicationInfo.sourceDir = apkPath;
        applicationInfo.publicSourceDir = apkPath;

        return applicationInfo;
    }
}

10.Service的插件化解決方案

11.

12.

13.

14.

15.

16.

16.

17.

18.

19.

20.

21.

22.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章