Android插件化系列三:技術流派和四大組件支持

Hello,各位朋友們,咱們繼續插件化系列的學習吧。下面是我這個系列文章的行文思路,java

Android插件化文章框架

本篇文章是本系列比較核心的一篇文章,我計劃這篇文章把插件化的大致技術給講清楚。期間會涉及到系列的前兩篇文章的內容,推薦先閱讀前面的兩篇基礎文章Android插件化系列一: 開篇前言,Binder機制,ClassLoaderAndroid插件化系列二: 資源與打包流程react

本篇文章預計須要半小時以上時間閱讀。讀完本篇文章,你將會了解:
1.插件化的發展和流派
2.插件化技術android

  • 如何加載插件中的類和資源
  • 如何解析插件中的信息
  • 如何利用aapt等方法解決宿主和插件資源衝突的問題
  • 如何支持四大組件的插件化

1.發展歷史和流派

先稍微介紹一下插件化的發展歷史。插件化技術,主要用在新聞,電商,閱讀,出行,視頻等領域,能夠看到包含了咱們生活的不少場景。在應用迭代的過程當中,1.能快速的修復應用出問題的部分,2.爲了搶佔市場,快速的根據市場反應進行迭代,3.將不經常使用功能模塊作成插件,減小包體積,這幾點對於應用的發展都是至關重要的事情。在這種背景下,插件化技術應運而生。git

下面是比較出名的幾個插件化框架,根據出現的時間排序,經過研究他們的原理,能夠把發展歷史大概分紅三代。github

時代 表明庫 特色
遠古 AndroidDynamicLoader(屠毅敏) adl基於動態替換Fragment來實現頁面的切換,雖然侷限大,可是給咱們提供了想象的基礎
第一代 dynamic-load-apk(任玉剛),DroidPlugin(張勇) dla經過建立ProxyActivity來進行分發,插件必須繼承ProxyActivity, 侵入性強且必須當心處理context。DroidPlugin是經過hook系統服務來進行Activity跳轉,缺點是hook太多,代碼複雜且不夠穩定。
第二代 VirtualApk, Small(林光亮),RePlugin 爲了同時達到插件開發的低侵入性(像開發普通app同樣開發插件)和框架的穩定性,在實現原理上都是趨近於選擇儘可能少的hook,並經過在manifest中預埋一些組件實現對四大組件的插件化。
第三代 VirtualApp,Atlas 在這一代中,插件兼容性,穩定性提高到更高的層次。同時,容器化框架的概念愈來愈流行。

2015年及之前,插件化技術分紅了明顯的兩派:以DroidPlugin爲表明的動態替換方案和以dynamic-load-apk爲表明的靜態代理方案。後來動態替換方案由於侵入性低,靈活穩定,逐步獲得了更多人的支持。而從熱修復方案和react native開始應用以來,插件化技術再也不是惟一的選擇,而是進入慢慢完善的階段,到2017年之後插件化技術基本成熟,兼容性和穩定性也達到了較高的層次。你們有興趣的能夠看看上面講到的幾個開源庫,體會插件化技術的發展歷程。編程

2.插件化技術

插件化技術的技術主要能夠歸納爲如下幾點:
1.插件和宿主之間的代碼和資源互用
2.插件的四大組件支持和跳轉數組

這裏咱們說到了插件和宿主之間的代碼和資源互用。其實這裏也是有學問的。插件根據是否須要共享資源代碼分爲獨立插件和耦合插件。獨立插件是單獨運行在一個進程中的,與宿主徹底隔離,崩潰不會影響到宿主。可是耦合插件倒是和宿主運行在一個進程中,因此插件崩潰,宿主也崩潰了。因此通常業務要根據資源和代碼的耦合程度,插件的可靠性等綜合考慮插件類型。bash

咱們接下來慢慢講解。app

2.1 代碼和資源互通

插件與dex

由於可能看我文章的還有沒接觸過插件化的同窗,因此增長這一部分講解插件和dex究竟是怎麼一種存在形式,插件,咱們能夠理解爲一個單獨打包出來的apk。在項目中咱們能夠創建module而且在模塊的build.gradle中把apply plugin: 'com.android.library'改成apply plugin: 'com.android.application'。這樣對這個模塊打包的產物就是apk。框架

apk在打包的過程當中,有一個class文件打入dex的操做,最終Apk中存在的是dex。加載這種dex中的類,使用的ClassLoader也頗有講究。前面咱們在Android插件化系列一: 開篇前言,Binder機制,ClassLoader中講到過,Android經常使用的就是PathClassLoader和DexClassLoader。PathClassLoader適用於已經安裝了的apk,通常做爲默認加載器。而這裏插件的apk是沒有安裝的,因此咱們須要使用DexClassLoader來加載插件dex中的類。下面是一段基本代碼,演示瞭如何從插件apk的dex中讀取類。

// 生成ClassLoader
File apkFile = File(apkPath, apkName);
String dexPath = apkFile.getPath();
File releaseFile = context.getDir("dex", 0);
DexClassLoader loader = new DexClassLoader(dexPath, releaseFile.getAbsolutePath(), null, getClassLoader());

// 加載類,使用類的方法
Class bean = loader.loadClass("xxx.xxx.xxx")  // 填入類的包名
Object obj = bean.newInstance();
Method method = bean.getMethod("xxx")  // 填入方法名
method.setAccessible(true);
method.invoke(obj)
複製代碼

這樣,咱們就能夠經過反射來獲取到類,並使用相應的方法了。

面向接口編程

你們會看到,若是像上面那樣大量的使用反射,代碼是至關醜陋的,擴展性能也差。這讓咱們想到了,能不能參考依賴倒置原則中的面向接口或抽象編程的思想,預先定義好接口。這樣等須要使用的時候,就只須要把對象轉換爲接口,就能調用接口的方法了。

好比咱們app模塊和插件模塊plugin依賴了接口模塊interface, interface中定義了接口IPlugin。IPlugin的定義是

interface IPlugin {
    void sayHello(String name) } 複製代碼

plugin中就能夠定義實現類

class PluginImpl implement IPlugin {
    @override
    void sayHello(String name) {
        Log.d("log""hello world" + name);
    }
}
複製代碼

這樣,咱們就能夠在宿主app模塊中去使用。具體的使用方法能夠有反射和服務發現機制。爲了簡單,這裏只用反射來調用具體的實現類。

Class pluginImpl = loader.loadClass("xxx.xxx.xxx")  // PluginIMpl 類的包名
Object obj = pluginImpl.newInstance();              // 生成PluginImpl對象
IPlugin plugin = (IPlugin)obj;
plugin.sayHello("AndroidEarlybird");
複製代碼

既然接口都給出了,咱們想作別的事情確定就駕輕就熟了。可是值得注意的是這裏的前提是宿主和插件都須要依賴接口模塊,也就是說雙方是有代碼和資源依賴的,所以這種方法只適用於耦合插件,獨立插件的話就只能用反射來調用了。

PMS

在插件化技術中,ActivityManagerServiche(AMS)和PackageManagerService(PMS)都是至關重要的系統服務。AMS自不用說,四大組件各類操做都須要跟它打交道,PMS也十分重要,完成了諸如權限校撿(checkPermission,checkUidPermission),Apk meta信息獲取(getApplicationInfo等),四大組件信息獲取(query系列方法)等重要功能。

使用PMS

android通常使用PMS來進行應用安裝,安裝的時候PMS須要藉助於PackageParser進行apk解析工做,主要負責解析出一個PackageParser.Package對象,這個對象仍是很大用途的。下面是這個Package對象的一些屬性值。

能夠看到咱們經過這個類能夠拿到apk中的四大組件,權限等信息,在插件化中,咱們有時候會須要利用這個類去拿到廣播的信息來處理插件中的靜態廣播

那麼如何使用PackageParser這個類呢?下面是VirtualApk的一些使用

public static final PackageParser.Package parsePackage(final Context context, final File apk, final int flags) throws PackageParser.PackageParserException {
        if (Build.VERSION.SDK_INT >= 24) {
            return PackageParserV24.parsePackage(context, apk, flags);
        } else if (Build.VERSION.SDK_INT >= 21) {
            return PackageParserLollipop.parsePackage(context, apk, flags);
        } else {
            return PackageParserLegacy.parsePackage(context, apk, flags);
        }
    }

    private static final class PackageParserV24 {
        static final PackageParser.Package parsePackage(Context context, File apk, int flags) throws PackageParser.PackageParserException {
            PackageParser parser = new PackageParser();
            PackageParser.Package pkg = parser.parsePackage(apk, flags);
            ReflectUtil.invokeNoException(PackageParser.class, null, "collectCertificates",
                    new Class[]{PackageParser.Package.class, int.class}, pkg, flags);
            return pkg;
        }
    }
複製代碼

由於PackageParser針對系統版本變化很大,因此VirtualApk對這個類作了多版本的適配,咱們這裏只展現了一種。

Hook PMS

正如咱們須要hook AMS去進行一些插件化的一些工做,有時候咱們也得對PMS進行hook。經過看源碼,咱們知道PMS的獲取也是經過Context獲取的,直奔ContextImpl類的getPackageManager方法。

public PackageManager getPackageManager() {
    if (mPackageManager != null) {
        return mPackageManager;
    }

    IPackageManager pm = ActivityThread.getPackageManager();
    if (pm != null) {
        // Doesn't matter if we make more than one instance.
        return (mPackageManager = new ApplicationPackageManager(this, pm));
    }
    return null;
}

// 繼續跟進到ActivityThread的getPackageManager方法中
public static IPackageManager getPackageManager() {
    if (sPackageManager != null) {
        return sPackageManager;
    }
    IBinder b = ServiceManager.getService("package");
    sPackageManager = IPackageManager.Stub.asInterface(b);
    return sPackageManager;
}
複製代碼

這裏咱們能夠看到,要想hook PMS須要把這兩個地方都hook住:

  • ActivityThread的靜態字段sPackageManager
  • 經過Context類的getPackageManager方法獲取到的ApplicationPackageManager對象裏面的mPM字段。

示例代碼以下:

// 獲取全局的ActivityThread對象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
Object currentActivityThread = currentActivityThreadMethod.invoke(null);

// 獲取ActivityThread裏面原始的 sPackageManager
Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
sPackageManagerField.setAccessible(true);
Object sPackageManager = sPackageManagerField.get(currentActivityThread);

// 準備好代理對象, 用來替換原始的對象
Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),
        new Class<?>[] { iPackageManagerInterface },
        new HookHandler(sPackageManager));

// 1. 替換掉ActivityThread裏面的 sPackageManager 字段
sPackageManagerField.set(currentActivityThread, proxy);

// 2. 替換 ApplicationPackageManager裏面的 mPM對象
PackageManager pm = context.getPackageManager();
Field mPmField = pm.getClass().getDeclaredField("mPM");
mPmField.setAccessible(true);
mPmField.set(pm, proxy);
複製代碼

管理ClassLoader

上面咱們講到了如何利用ClassLoader來加載dex中的類,如今咱們再來深刻聊聊這個話題。首先,須要明確的是,由於咱們插件的類都是位於沒有安裝的apk的dex中,因此咱們不能直接使用主app的ClassLoader。那麼就會有多種解決方案。

比較直接的思想是經過對每個插件都新建一個ClassLoader來作加載。那麼若是咱們插件不少的時候,咱們須要作的就是把每一個插件的ClassLoader給記錄下來,當使用某個插件的類的時候,用它對應的ClassLoader去加載。正如咱們上節的例子中展現的那樣。

另外一種思想是直接操做dex數組。宿主和插件的ClassLoader都會對應一個dex數組。那麼咱們若是能把插件的dex數組合併到宿主的dex數組裏面去的話,咱們就能用宿主的ClassLoader來反射加載插件的dex數組中的類了。這樣作的目的是不須要管理插件的ClassLoader,只要用宿主的ClassLoader就好了。好比咱們曾經在Android插件化系列一: 開篇前言,Binder機制,ClassLoader中講到DexClassLoader的源代碼。

public class BaseDexClassLoader extends ClassLoader {
    private final DexPathList pathList;

    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);  //見下文
        //收集dex文件和Native動態庫【見小節3.2】
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
}

public class DexPathList {
    private Element[] dexElements;

    public DexPathList(ClassLoader definingContext, String dexPath, String librarySearchPath, File optimizedDirectory) {
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions, definingContext);
    }

    private static List<File> splitDexPath(String path) {
       return splitPaths(path, false);
    }

    private static List<File> splitPaths(String searchPath, boolean directoriesOnly) {
        List<File> result = new ArrayList<>(); 
        if (searchPath != null) {
            for (String path : searchPath.split(File.pathSeparator)) {
                // 省略
            }
        }
        return result;
     }
}
複製代碼

從上面咱們能夠看出,dexPath字符串是由多個分號分割的。拆分紅字符串數組之後,每一個path都是一個外部的dex/apk路徑。那麼咱們很天然的想到,能不能把插件的dex路徑手動添加到宿主的dexElements數組中呢?答案固然是ok的,方案就是使用Hook。咱們能夠先反射獲取到ClassLoader的dexPathList,而後再獲取這個list的dexElements數組,而後手動把插件構建出Element,再拷貝到dexElements數組中。熱修復框架Nuwa也是使用這種思想。

第三種思路是ClassLoader delegate。本文推薦這種方法。首先咱們自定義ClassLoader,取代原先宿主的ClassLoader,而且把宿主做爲Parent,同時在自定義的ClassLoader中用一個集合放置全部插件的ClassLoader,而後這個自定義ClassLoader在加載任何一個類的時候,依據雙親委託機制,加載類都會先從宿主的ClassLoader中尋找,沒有的話再遍歷ClassLoader集合尋找能加載這個類的插件ClassLoader。固然這裏又會有提升效率的優化點,好比遍歷集合的方式能夠改成先從已加載過的集合中尋找,再從未加載過的集合中尋找。下面是示例代碼。

class PluginManager {
    public static void init(Application application) {
        //初始化一些成員變量和加載已安裝的插件
        mPackageInfo = RefInvoke.getFieldObject(application.getBaseContext(), "mPackageInfo");
        mBaseContext = application.getBaseContext();
        mNowResources = mBaseContext.getResources();

        mBaseClassLoader = mBaseContext.getClassLoader();
        mNowClassLoader = mBaseContext.getClassLoader();
        
        ZeusClassLoader classLoader = new ZeusClassLoader(mBaseContext.getPackageCodePath(), mBaseContext.getClassLoader());

        File dexOutputDir = mBaseContext.getDir("dex", Context.MODE_PRIVATE);
        final String dexOutputPath = dexOutputDir.getAbsolutePath();
        for(PluginItem plugin: plugins) {
            DexClassLoader dexClassLoader = new DexClassLoader(plugin.pluginPath,
                    dexOutputPath, null, mBaseClassLoader);
            classLoader.addPluginClassLoader(dexClassLoader);
        }
        // 替換原有的宿主的ClassLoader爲自定義ClassLoader,將原來的宿主ClassLoader做爲自定義ClassLoader的
        RefInvoke.setFieldObject(mPackageInfo, "mClassLoader", classLoader);
        Thread.currentThread().setContextClassLoader(classLoader);
        mNowClassLoader = classLoader;
    }
}

class ZeusClassLoader extends PathClassLoader {
    private List<DexClassLoader> mClassLoaderList = null;

    public ZeusClassLoader(String dexPath, ClassLoader parent, PathClassLoader origin) {
        super(dexPath, parent);

        mClassLoaderList = new ArrayList<DexClassLoader>();
    }

    /** * 添加一個插件到當前的classLoader中 */
    protected void addPluginClassLoader(DexClassLoader dexClassLoader) {
        mClassLoaderList.add(dexClassLoader);
    }

    @Override
    protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = null;
        try {
            //先查找parent classLoader,這裏實際就是系統幫咱們建立的classLoader,目標對應爲宿主apk
            clazz = getParent().loadClass(className);
        } catch (ClassNotFoundException ignored) {
        }

        if (clazz != null) {
            return clazz;
        }

        //挨個的到插件裏進行查找
        if (mClassLoaderList != null) {
            for (DexClassLoader classLoader : mClassLoaderList) {
                if (classLoader == null) continue;
                try {
                    //這裏只查找插件它本身的apk,不須要查parent,避免屢次無用查詢,提升性能
                    clazz = classLoader.loadClass(className);
                    if (clazz != null) {
                        return clazz;
                    }
                } catch (ClassNotFoundException ignored) {
                }
            }
        }
        throw new ClassNotFoundException(className + " in loader " + this);
    }
}

複製代碼

資源

Resources&AssetManager

android中的資源大體分爲兩類:一類是res目錄下存在的可編譯的資源文件,好比anim,string之類的,第二類是assets目錄下存放的原始資源文件。由於Apk編譯的時候不會編譯這些文件,因此不能經過id來訪問,固然也不能經過絕對路徑來訪問。因而Android系統讓咱們經過Resources的getAssets方法來獲取AssetManager,利用AssetManager來訪問這些文件。

Resources resources = context.getResources();
AssetManager manager = resources.getAssets();
InputStream is = manager.open("filename");
複製代碼

Resources和AssetManager的關係就像銷售和研發。Resources負責對外,外部須要的getString, getText等各類方法都是經過Resources這個類來調用的。而這些方法其實都是調用的AssetManager的私有方法。因此最終兩類資源都是AssetManager在兢兢業業的向Android系統要資源,爲外界服務着。

AssetManager裏有個很重要的方法addAssetPath(String path)方法,App啓動的時候會把當前apk的路徑傳進去,而後AssetManager就能訪問這個路徑下的全部資源也就是宿主apk的資源了。那麼idea就冒出來了,若是咱們把插件的地址也傳進這個方法去,是否是就能獲得一個能同時訪問宿主和插件的全部資源的「超級」AssetManager了呢?答案是確定的,這也是插件化對資源的一種解決方案

下面是一段示例代碼展現了獲取宿主的Resources中的AssetManager,而後調用addAssetPath添加插件路徑,最後生成一個新的Resources的方法

// 新生成AssetManager,調用addAssetPath
AssetManager assetManager = resources.getAssets();  // 先經過Resources拿到示例代碼
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, dexPath1);
mAssetManager = assetManager;

// 根據新生成的AssetManager生成Resources
mResources = new Resources(mAssetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
複製代碼

接下來咱們要分別將宿主和插件的原有Resources替換成咱們上面生成的Resources。注意這裏傳入的application應該是宿主和插件對應的Application。

Object contextImpl = RefInvoke.getFieldObject("android.app.ContextImpl", application, "getImpl")  // 獲取Application的context
LoadedApk loadedApk = (LoadedApk)RefInvoke.getFieldObject(contextImpl, "mPackageInfo")
RefInvoke.setFieldObject(loadedApk, "mResources", resources);
RefInvoke.setFieldObject(application.getBaseContext(), "mResources", resources);
複製代碼

除了須要替換Application的Resources對象,咱們也須要替換Activity的Resources對象,宿主和插件的Resources都須要替換。這是由於他們都是Context,只替換Application的並不能影響到Activity。咱們能夠在Instrumentation回調callActivityOnCreate的時候去替換。這點在後面Activity插件化處理部分再詳細講解。

上面只是展現了使用,想了解更多信息的能夠查看VirtualApk

解決資源衝突

Android插件化系列二: 資源與打包流程中咱們提到了插件和宿主分別打包的時候可能會存在資源id衝突的狀況,上面咱們使用了一個超級Resource以後,id若是重複了,運行的時候使用id來查找資源就會報錯。

爲了解決id衝突的問題通常有三種方案:

  1. 修改android打包的aapt工具,將插件資源前綴改成0x02 - 0x7e之間的數值
  2. 進入到哪一個插件,就爲哪一個插件生成新的AssetManager和Resources

其中,方案二比較複雜,而且不利於宿主和插件資源的互相調用。因此咱們在上節採用的是超級Resources的方案,因此這裏咱們介紹一下方案一,也就是修改aapt工具。

aapt是android打包資源的處理工具,大多數的插件話開源庫對齊進行改造無外乎都是兩種方式:

能夠看到aapt(1)處理插件化的資源並非很友好,開發和維護難度都比較大,後來google推出了Android App Bundle這個和插件很相似的feature,就推出了aapt2來支持了資源分包。咱們注意官網上這幾個aapt2的打包參數:

是否是發現官方已經給咱們支持好了按package區分資源前綴id,多美好啊哈哈。

固然這裏也是有坑的。那就是須要buildTools版本大於28.0.0 在buildTools 28.0.0之前,aapt2自帶了資源分區,經過–package-id參數指定。可是該分區只支持>0x7f的PP段,而在Android 8.0以前,是不支持>0x7f的PP段資源的,運行時會拋異常。可是當指定了一個<0x7f的PP段資源後,編譯資源時卻會報錯

error: invalid package ID 0x15. Must be in the range 0x7f-0xff..
複製代碼

因此對於Android P以前使用的buildTools版本(<28.0.0),咱們必須經過修改aapt2的源碼達到資源分區的目的。而在28.0.0之後,aapt2支持了<0x7f預留PP段分區的功能,只須要指定參數--allow-reserved-package-id便可。

--allow-reserved-package-id --package-id package-id
複製代碼
插件使用宿主資源

在咱們爲宿主開發插件的時候,常常不可避免的出現插件要使用宿主中資源的狀況,若是咱們把宿主的資源copy一份放在插件中,那無疑會大大增長包的大小,而且這些都是重複資源,是不該該在App中存在的。那麼咱們就得想辦法讓插件使用宿主的資源。好比這樣

前面已經講到了,咱們能夠經過爲插件和宿主一塊兒構建一個超級Resources,包括了插件和宿主全部的資源,理論上能夠經過資源id獲取到全部的資源,那麼問題來了,插件中的R文件是不包含宿主的R文件的,咱們在編碼的時候怎麼使用呢?

下面分代碼使用xml使用兩種使用方式來講解決方案: 代碼使用:在插件資源打包任務processResourcesTask完成後將宿主的R.txt文件(打包過程當中產生,位置在build/intermediates/symbols/xx/xx/R.txt)合併到插件的R.txt文件,而後再生成R.java,這樣就能夠正常的使用R文件來索引資源了

xml使用:咱們須要在aapt2打包的時候指定-I參數。

這樣,咱們經過-I指定宿主的資源包,就能夠在xml中使用宿主的資源了。

總結

本節咱們首先介紹插件代碼的dex加載,給出了利用反射和麪向接口編程來獲取插件中的代碼的方法,而後介紹了經過自定義delegate ClassLoader的方法來更好的加載插件和宿主中的類,接下來介紹了PMS如何獲取插件的信息以及如何進行自定義hook,最後講到了插件使用宿主資源的一些知識。到了這一步,咱們已經能夠獲取到插件的各類信息,能夠實現宿主和插件中的代碼互通,能夠實現插件調用宿主的資源,基本上算是邁出了一大步!可是隻有代碼和資源是不夠的,接下來咱們看看怎麼處理android的四大組件,這一塊纔是重頭戲,也是插件化的精髓

2.2 四大組件支持

android的四大組件其實有挺多的共通之處,好比他們都接受ActivityManagerService(AMS)的管理,都須要經過Binder機制請求AMS服務。而且他們的請求流程也是基本相通的,其中Activity又是最重要的組件,出鏡最多,同時也是平常開發接觸最多的組件,咱們將會主要以Activity爲例,講解插件化對四大組件的支持,其他三個組件有不一樣或值得注意的地方咱們會另外指出來。固然,針對四大組件的解決方案有不少種,本文限於篇幅只介紹DroidPlugin的動態替換方案。

Activity

AndroidManifest.xml預佔位

相信作過Android開發的都知道,四大組件基本都是要在AndroidManifest.xml中定義的,否則系統就會報錯,而後問你 have you declared this activity in your AndroidManifest.xml? 【必須在AndroidMainfest.xml中定義四大組件】這一點對插件化確實是比較嚴重的限制,畢竟咱們並無辦法提早就把插件中的Activity聲明進去,可是這個限制也並非沒辦法解決的。好比DroidPlugin就採用了預佔位Activity到AndroidManifest.xml中的方案。

DroidPlugin的方案思想很簡單,先在AndroidManifest.xml中預約義好各類LaunchMode的佔位Activity和其他三大組件。好比

<activity
    android:name=".StubSingleTaskActivity1"
    android:exported="true"
    android:launchMode="singleTask"
    android:theme="@style/Theme.NoActionBar"
    android:screenOrientation="portrait" />

<activity
    android:name=".StubSingleTopActivity1"
    android:exported="true"
    android:launchMode="singleTop"
    android:theme="@style/Theme.NoActionBar"
    android:screenOrientation="portrait" />
複製代碼

這樣的話,咱們就要實行狸貓換太子的方法,把原本想要開啓的Activity換成StubActivity,而後躲過了系統對【必須在AndroidMainfest.xml中定義四大組件】的審查真正的開始start Activity的時候再去打開真正的目的Activity。那麼咱們怎麼去實現這個想法呢,這就要求咱們熟悉Activity的啓動流程了。

startActivity流程

startActivity的流程比較繁雜,甚至能夠做爲一篇單獨的文章來說解。網上有不少的文章在講解,比較詳細牛逼的是老羅的Android應用程序的Activity啓動過程簡要介紹和學習計劃。你們若是有興趣的話能夠參考。我這裏只簡明扼要的講解部分的流程。

首先先看一個流程圖

首先咱們都是從startActivity進去的,展轉發現它調用了Instrumentation的execStartActivity方法,接着在這個函數裏面調用了ActivityManagerNative類的startActivity方法,請求到了ActivityManagerService的服務。這一點就是咱們在Android插件化系列一: 開篇前言,Binder機制,ClassLoader講到過的Binder機制在Activity啓動過程當中的體現。能夠看到就是在AMS的startActivity的方法中校驗了Activity是否註冊,肯定了Activity的啓動模式,AMS咱們沒辦法改啊,因此我們得出個結論必定要在校驗前的流程裏把Activity給替換掉。繼續往下看,能夠看到ActivityStackSupervisor把啓動的重任最終委託給了ApplicationThread。

咱們在前面的系列一中說過,Binder機制實際上是互爲Client和Server的,在app申請AMS服務的時候,AMS是Server,AMP是AMS在app的代理。而在申請到AMS服務之後,AMS須要請求App進行後續控制的時候,ApplicationThread就是Server,ApplicationThreadProxy就是ApplicationThread在AMS側的代理。

繼續往下看,能夠看到ActivityThread調用了H類,最終調用了handleLaunchActivity方法,由Instrumentation建立出了Activity對象,啓動流程結束。

「狸貓換太子」

看完了上面的啓動流程,你們能夠想到,在這個流程中我只要在調用AMS前把目標Activity替換成StubActivity(上半場),在AMS校驗完,立刻要打開Activity的時候替換爲目標Activity(下半場),這樣就能夠達到「狸貓換太子」啓動目標Activity的目的了啊。由於流程較長,參與的類較多,因此咱們能夠選擇的hook點也是至關多的,可是咱們越早hook,後續的操做越多越容易出問題,因此咱們選擇比較後面的流程去hook。這裏選擇:

  • 上半場,hook ActivityManagerNative對於startActivity方法的調用
  • 下半場,hook H.mCallback對象,替換爲咱們的自定義實現,

hook AMN 下面是一些示例代碼,能夠看到咱們替換掉交給AMS的intent對象,將裏面的TargetActivity的暫時替換成已經聲明好的替身StubActivity。

if ("startActivity".equals(method.getName())) {
            // 只攔截這個方法
            // 替換參數, 任你所爲;甚至替換原始Activity啓動別的Activity偷樑換柱

            // 找到參數裏面的第一個Intent 對象
            Intent raw;
            int index = 0;

            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break;
                }
            }
            raw = (Intent) args[index];

            Intent newIntent = new Intent();

            // 替身Activity的包名, 也就是咱們本身的包名
            String stubPackage = raw.getComponent().getPackageName();

            // 這裏咱們把啓動的Activity臨時替換爲 StubActivity
            ComponentName componentName = new ComponentName(stubPackage, StubActivity.class.getName());
            newIntent.setComponent(componentName);

            // 把咱們原始要啓動的TargetActivity先存起來
            newIntent.putExtra(AMSHookHelper.EXTRA_TARGET_INTENT, raw);

            // 替換掉Intent, 達到欺騙AMS的目的
            args[index] = newIntent;

            Log.d(TAG, "hook success");
            return method.invoke(mBase, args);

        }
複製代碼

hook H.mCallback 前面咱們說過,ActivityThread是藉助於H這個類完成四大組件的操做管理。H繼承自Handler,咱們看看Handler處理消息的dispatchMessage方法。

public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        if (mCallback != null) {
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        handleMessage(msg);
    }
}
複製代碼

而H的handleMessage方法中正是處理LAUNCH_ACTIVITY,CREATE_SERVICE等消息的地方。因此咱們就會想,在mCallback.handleMessage中替換回原來的Activity應該就是最晚的時間點了吧。下面是自定義的Callback類,反射設置爲ActivityThread的H的mCallback就好了。

class MockClass2 implements Handler.Callback {

    Handler mBase;

    public MockClass2(Handler base) {
        mBase = base;
    }

    @Override
    public boolean handleMessage(Message msg) {

        switch (msg.what) {
            // ActivityThread裏面 "LAUNCH_ACTIVITY" 這個字段的值是100
            // 原本使用反射的方式獲取最好, 這裏爲了簡便直接使用硬編碼
            case 100:
                handleLaunchActivity(msg);
                break;

        }

        mBase.handleMessage(msg);
        return true;
    }

    private void handleLaunchActivity(Message msg) {
        // 這裏簡單起見,直接取出TargetActivity;
        Object obj = msg.obj;

        // 把替身恢復成真身
        Intent intent = (Intent) RefInvoke.getFieldObject(obj, "intent");

        Intent targetIntent = intent.getParcelableExtra(AMSHookHelper.EXTRA_TARGET_INTENT);
        intent.setComponent(targetIntent.getComponent());
    }
}
複製代碼
替換Resources

還記得咱們在第一節留下了一個問題嗎,就是Activity的資源替換要在Instrumentation回調callActivityOnCreate的時候進行。這個時間點比較臨近onCreate,Instrumentation也比較方便去hook。下面展現這個技術,須要傳入超級Resources。

public void hookInstrumentation(){
    Object currentActivityThread = RefInvoke.invokeStaticMethod("android.app.ActivityThread", "currentActivityThread");
    // 拿到原始的 mInstrumentation字段
    Instrumentation mInstrumentation = (Instrumentation) RefInvoke.getFieldObject(currentActivityThread, "mInstrumentation");
    // 建立代理對象
    Instrumentation evilInstrumentation = new EvilInstrumentation(mInstrumentation, resources);  // 這裏的resources是咱們的超級Resources
    RefInvoke.setFieldObject(currentActivityThread, "mInstrumentation", evilInstrumentation);
}

// 這裏的Activity是在
public class EvilInstrumentation extends Instrumentation {
    Instrumentation mBase;
    Resources mRes;

    public EvilInstrumentation(Instrumentation base,Resources res) {
        mBase = base;
        mRes = res;
    }

    @override
    public void callActivityOnCreate(Activity activity, Bundle bundle) {
        // 替換Resources
        if (mRes != null) {
            RefInvoke.setFieldObject(activity.getBaseContext().getClass(), activity.getBaseContext(), "mResources", mRes);
        }
        super.callActivityOnCreate(activity, bundle);
    }
}
複製代碼

Service

Service的處理和Activity的基本同樣,區別是調用屢次startService並不會啓動多個Service實例,而是隻有一個實例,因此咱們的佔位Service得多定義一些。

BroadcastReceiver

BroadcastReceiver的插件化和Activity的不太同樣。Android中的廣播分爲兩種:靜態廣播和動態廣播,動態廣播不須要和AMS交互,就是一個普通類,只要按照前面的ClassLoader方案保證他能加載就好了。可是靜態廣播比較麻煩,除了須要在AndroidManifest.xml中進行註冊之外,他和Activity不同的是,他還附加了IntentFilter信息。而IntentFilter信息是隨機的,沒法被預佔位的。這個時候就只能把取出插件中的靜態廣播改成動態廣播了。雖然會有一些小問題,可是影響不大

前面咱們講到了PackageParser能夠獲取到插件的四大組件的信息,存儲到Package對象中,那麼咱們就有個思路,經過PMS獲取到BroadcastReceiver,而後把其中的靜態廣播改成動態廣播.

public static void preLoadReceiver(Context context, File apkFile) {
        // 首先調用parsePackage獲取到apk對象對應的Package對象
        Object packageParser = RefInvoke.createObject("android.content.pm.PackageParser");
        Class[] p1 = {File.class, int.class};
        Object[] v1 = {apkFile, PackageManager.GET_RECEIVERS};
        Object packageObj = RefInvoke.invokeInstanceMethod(packageParser, "parsePackage", p1, v1);

        // 讀取Package對象裏面的receivers字段,注意這是一個 List<Activity> (沒錯,底層把<receiver>看成<activity>處理)
        // 接下來要作的就是根據這個List<Activity> 獲取到Receiver對應的 ActivityInfo (依然是把receiver信息用activity處理了)
        List receivers = (List) RefInvoke.getFieldObject(packageObj, "receivers");

        for (Object receiver : receivers) {
            registerDynamicReceiver(context, receiver);
        }
    }

    // 解析出 receiver以及對應的 intentFilter
    // 手動註冊Receiver
    public static void registerDynamicReceiver(Context context, Object receiver) {
        //取出receiver的intents字段
        List<? extends IntentFilter> filters = (List<? extends IntentFilter>) RefInvoke.getFieldObject(
                "android.content.pm.PackageParser$Component", receiver, "intents");

        try {
            // 把解析出來的每個靜態Receiver都註冊爲動態的
            for (IntentFilter intentFilter : filters) {
                ActivityInfo receiverInfo = (ActivityInfo) RefInvoke.getFieldObject(receiver, "info");

                BroadcastReceiver broadcastReceiver = (BroadcastReceiver) RefInvoke.createObject(receiverInfo.name);
                context.registerReceiver(broadcastReceiver, intentFilter);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製代碼

ContentProvider

ContentProvider的插件化方法和BroadcastReceiver的很像,可是和BroadcastReceiver不一樣的是,BroadcastReceiver中的廣播叫作註冊,但ContentProvider是要「安裝」。方案是: 首先,調用PackageParser的parsePackage方法,把獲得的Package對象經過generateProviderInfo轉換爲ProviderInfo對象。

public static List<ProviderInfo> parseProviders(File apkFile) throws Exception {

        //獲取PackageParser對象實例
        Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
        Object packageParser = packageParserClass.newInstance();

        // 首先調用parsePackage獲取到apk對象對應的Package對象
        Class[] p1 = {File.class, int.class};
        Object[] v1 = {apkFile, PackageManager.GET_PROVIDERS};
        Object packageObj = RefInvoke.invokeInstanceMethod(packageParser, "parsePackage",p1, v1);

        // 讀取Package對象裏面的services字段
        // 接下來要作的就是根據這個List<Provider> 獲取到Provider對應的ProviderInfo
        List providers = (List) RefInvoke.getFieldObject(packageObj, "providers");

        // 調用generateProviderInfo 方法, 把PackageParser.Provider轉換成ProviderInfo

        //準備generateProviderInfo方法所須要的參數
        Class<?> packageParser$ProviderClass = Class.forName("android.content.pm.PackageParser$Provider");
        Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
        Object defaultUserState = packageUserStateClass.newInstance();
        int userId = (Integer) RefInvoke.invokeStaticMethod("android.os.UserHandle", "getCallingUserId");
        Class[] p2 = {packageParser$ProviderClass, int.class, packageUserStateClass, int.class};

        List<ProviderInfo> ret = new ArrayList<>();
        // 解析出intent對應的Provider組件
        for (Object provider : providers) {
            Object[] v2 = {provider, 0, defaultUserState, userId};
            ProviderInfo info = (ProviderInfo) RefInvoke.invokeInstanceMethod(packageParser, "generateProviderInfo",p2, v2);
            ret.add(info);
        }

        return ret;
    }
複製代碼

而後咱們須要調用ActivityThread的installContentProviders方法把這些ContentProvider「安裝」到宿主中。

public static void installProviders(Context context, File apkFile) throws Exception {
        List<ProviderInfo> providerInfos = parseProviders(apkFile);

        for (ProviderInfo providerInfo : providerInfos) {
            providerInfo.applicationInfo.packageName = context.getPackageName();
        }

        Object currentActivityThread = RefInvoke.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread");

        Class[] p1 = {Context.class, List.class};
        Object[] v1 = {context, providerInfos};

        RefInvoke.invokeInstanceMethod(currentActivityThread, "installContentProviders", p1, v1);
    }
複製代碼

ContentProvider的插件化還須要注意:

  1. App安裝本身的ContentProvider是在進程啓動時候進行,比Application的onCreate還要早,因此咱們要在Application的attachBaseContext方法中手動執行上述操做。
  2. 讓外界App直接調用插件的App,並非一件特別好的事情,最好是由App的ContentProvider做爲中轉。由於字符串是ContentProvider的惟一標誌,轉發機制就特別適用。

總結

本文首先介紹了插件化中宿主和插件代碼和資源互通的方式,而後介紹了四大組件的插件化方法,由於插件化技術太過繁雜,並無把全部的細節都覆蓋到,所介紹的方案也只是當今比較實用,經受過考驗的一套,並無介紹太多的方法。目的是讓讀者們和我一塊兒,先從總體上理解插件化的機制,而後就容易去區分各類開源庫的原理和思路了。

參考

感謝下面的各位老師的書籍或文章,讓我受益不淺。
1.包建強《Android插件化開發指南》
2.田維術的博客
3.插件化加載dex跟資源原理
4.深刻理解Android插件化技術
5.插件化-解決插件資源ID與宿主資源ID衝突的問題
6.官網aapt2
7.再談 aapt2 資源分區

我是Android笨鳥之旅,笨鳥也要有向上飛的心,我在這裏陪你一塊兒慢慢變強。

相關文章
相關標籤/搜索