是時候來一波Android插件化了

是時候來一波Android插件化了


前言

今年(2017年)6月時候,有幸參加了在北京舉行的GMTC大會,恰巧360的張炅軒大神分享了360的插件化方案—— RePlugin ,聽了以后,受益匪淺。

因為是公司組織參加大會的,參會后需要技術(shù)分享,所以就選擇介紹RePlugin以及Android插件化相關(guān)內(nèi)容,本文也是主要介紹RePlugin以及自己對插件化的理解。

因為插件化涉及到的東西比較多,由于篇幅的限制,很多知識點只是簡單介紹一下,同時會給出相關(guān)鏈接,讀者可以點擊作參考。

這幾年,世面上就已經(jīng)出現(xiàn)了不少幾款插件化方案,同時熱更新技術(shù)也是遍地開花。當(dāng)時是比較抵觸這類技術(shù)的,個人覺的這樣會破壞Android的生態(tài)圈,但是畢竟出現(xiàn)了這么多的插件化方案,出現(xiàn)總是有道理的。本著學(xué)習(xí)的態(tài)度,還是要學(xué)習(xí)下插件化相關(guān)技術(shù)。

Android開發(fā)演進(jìn)

Android開發(fā)初期,基本上沒有什么框架的,什么東西都往Activity里面塞,最后Activity就變得很大。后面有些人借鑒了Java后端的思想,使用MVC模式,一定程度上解決了代碼亂堆的問題,
使用了一段時間MVC后,Activity依舊變的很大,因為Activity里面不光有UI的邏輯,還有數(shù)據(jù)的邏輯。

MVC

再后來有了MVP,MVP解決了UI邏輯和數(shù)據(jù)邏輯在一起的問題,同時也解決了Android代碼測試?yán)щy問題。

MVP

隨著業(yè)務(wù)的增多,架構(gòu)中有了Domain的概念,Domain從Data中獲取數(shù)據(jù),Data可能會是Net,F(xiàn)ile,Cache各種IO等,然后項目架構(gòu)變成了這樣。

MVP2

模塊化介紹

MVP升級版用了一段時間以后,新問題又出現(xiàn)了。隨著業(yè)務(wù)的增多,代碼變的越來越復(fù)雜,每個模塊之間的代碼耦合變得越來越嚴(yán)重,解耦問題急需解決,同時編譯時間也會越來越長。

開發(fā)人員增多,每個業(yè)務(wù)的組件各自實現(xiàn)一套,導(dǎo)致同一個App的UI風(fēng)格不一樣,技術(shù)實現(xiàn)也不一樣,團(tuán)隊技術(shù)也無法得到沉淀,重復(fù)早輪子嚴(yán)重。

Modular

然后模塊化(組件化)解決方案就出現(xiàn)了。

Modular2

插件化介紹

講道理,模塊化已經(jīng)是最終完美的解決方案了,為啥還需要插件化呢?

還是得從業(yè)務(wù)說起,如果一個公司有很多業(yè)務(wù),并且每個業(yè)務(wù)可以匯總成一個大的App,又或者某一個小業(yè)務(wù)又需要單獨做成一個小的App。

按照上面的說的模塊化解決方案,需要把這個業(yè)務(wù)設(shè)計成一個模塊,代碼最終打包成一個aar,主App和業(yè)務(wù)App設(shè)計成一個運行殼子,編譯打包時候使用Gradle做maven依賴即可。

舉例說明美團(tuán)和貓眼電影。

美團(tuán)和貓眼

實際上這樣做比較麻煩,主App和業(yè)務(wù)模塊會或多或少依賴一點公共代碼,如果公共代碼出現(xiàn)變動,則需要對應(yīng)做出修改。
同時業(yè)務(wù)代碼會設(shè)計成Android Lib project,開發(fā)、編譯、調(diào)試也有點麻煩,那么能不能這樣設(shè)計,某個業(yè)務(wù)模塊單獨做出一個Apk,主App直接使用插件的方式,如果需要某種功能,那么直接加載某一個apk,而不是直接依賴代碼的形式。

前提技術(shù)介紹

通過上面的業(yè)務(wù)演進(jìn),最終我們需要做的就是一個Apk調(diào)用另外一個Apk文件,這也就是我們今天的主題——插件化。

一個常識,大家都知道,Apk只有在安裝的情況下,才可以被運行調(diào)用。如果一個Apk只是一個文件,放置在存儲卡上,我們?nèi)绾尾拍苷{(diào)用起來呢?

對于這個問題,先保留,后面會做講解,當(dāng)然了已經(jīng)有幾種方案是可以這樣做的。但是為了了解插件化的原理,先回顧一下基礎(chǔ)知識。

APK構(gòu)成

Apk是App代碼最終編譯打包生成的文件,主要包含代碼(dex、so)、配置文件、資源問題、簽名校驗等。

Manifest

App中系統(tǒng)組件配置文件,包括Application、Activity、Service、Receiver、Provider等。

App中所有可運行的Activity必須要在這里定義,否則就不能運行,也包括其他組件,Receiver也可以動態(tài)注冊。(敲黑板,這里很重要,記住這句話。)

Application

App啟動,代碼中可以獲取到被運行調(diào)用的第一個類,常用來做一些初始化操作。

四大組件

四大系統(tǒng)組件Activity、Service、Receiver、Provider,代碼中繼承系統(tǒng)中的父類。如上面所說,必須要在manifest中配置定義,否則不可以被調(diào)用。

so

App中C、C++代碼編譯生成的二進(jìn)制文件,與手機的CPU架構(gòu)相關(guān),不同CPU架構(gòu)生成的文件有些不同。開發(fā)中常常會生成多份文件,然后打包到Apk中,不同CPU類型,會調(diào)用不同的文件。

resource

Android中資源文件比較多,通常放在res和assets文件夾下面。常見的有布局、圖片、字符、樣式、主題等。

安裝路徑

上面的介紹的Apk結(jié)構(gòu),那么Apk安裝以后,它的安裝位置在哪,資源和數(shù)據(jù)又放在哪里呢?

安裝路徑

/data/app/{package}/主要放置Apk文件,同時Cpu對應(yīng)的so文件也會被解壓到對應(yīng)的文件夾中,Android高級版本中還會對dex做優(yōu)化,生成odex文件也在這個文件夾中。

data/data/{package}/主要存放App生成的數(shù)據(jù),比如SharedPreferences、cache等其他文件。

那么問題來了,如果調(diào)用為安裝的Apk,假設(shè)能夠運行,那么他們的運行文件放在哪里?代碼中生成的數(shù)據(jù)文件又要放在哪里?

App啟動流程介紹

App的二進(jìn)制文件Apk安裝以后,就可以直接啟動了,直接點擊Launcher上面的圖片即可,但是我們需要的是一個App啟動另外一個apk文件,所以有必要了解下App的啟動流程。

IPC & Binder

在Android系統(tǒng)中,每一個應(yīng)用程序都是由一些Activity和Service組成的,這些Activity和Service有可能運行在同一個進(jìn)程中,也有可能運行在不同的進(jìn)程中。那么,不在同一個進(jìn)程的Activity或者Service是如何通信的呢?

Android系統(tǒng)提供一種Binder機制,能夠使進(jìn)程之間相互通信。

Android進(jìn)程間通信資料

AMS

Activity啟動流程說個一天也說不完,過程很長,也很繁瑣,不過我們只要記住了AMS就可以了。

Android系統(tǒng)應(yīng)用框架篇:Activity啟動流程

盜一張圖

AMS

插件化技術(shù)問題與解決方案

代碼加載

按照正常思路,如果一個主Apk需要運行一個插件Apk,那么怎么樣才能把里面的代碼加載過來呢?

Java ClassLoader

Java中提供了ClassLoader方式來加載代碼,然后就可以運行其中的代碼了。這里有一份資料(深入分析Java ClassLoader原理) ,可以簡單了解下。

  • 原理介紹

ClassLoader使用的是雙親委托模型來搜索類的,每個ClassLoader實例都有一個父類加載器的引用(不是繼承的關(guān)系,是一個包含的關(guān)系),虛擬機內(nèi)置的類加載器(Bootstrap ClassLoader)本身沒有父類加載器,但可以用作其它ClassLoader實例的的父類加載器。
當(dāng)一個ClassLoader實例需要加載某個類時,它會試圖親自搜索某個類之前,先把這個任務(wù)委托給它的父類加載器,這個過程是由上至下依次檢查的,首先由最頂層的類加載器Bootstrap ClassLoader試圖加載,
如果沒加載到,則把任務(wù)轉(zhuǎn)交給Extension ClassLoader試圖加載,如果也沒加載到,則轉(zhuǎn)交給App ClassLoader 進(jìn)行加載,如果它也沒有加載得到的話,則返回給委托的發(fā)起者,由它到指定的文件系統(tǒng)或網(wǎng)絡(luò)等URL中加載該類。
如果它們都沒有加載到這個類時,則拋出ClassNotFoundException異常。否則將這個找到的類生成一個類的定義,并將它加載到內(nèi)存當(dāng)中,最后返回這個類在內(nèi)存中的Class實例對象。

  • 為什么要使用雙親委托這種模型呢?

因為這樣可以避免重復(fù)加載,當(dāng)父親已經(jīng)加載了該類的時候,就沒有必要子ClassLoader再加載一次。
考慮到安全因素,我們試想一下,如果不使用這種委托模式,那我們就可以隨時使用自定義的String來動態(tài)替代java核心api中定義的類型,這樣會存在非常大的安全隱患,而雙親委托的方式,就可以避免這種情況,
因為String已經(jīng)在啟動時就被引導(dǎo)類加載器(Bootstrcp ClassLoader)加載,所以用戶自定義的ClassLoader永遠(yuǎn)也無法加載一個自己寫的String,除非你改變JDK中ClassLoader搜索類的默認(rèn)算法。

  • 但是JVM在搜索類的時候,又是如何判定兩個class是相同的呢?

JVM在判定兩個class是否相同時,不僅要判斷兩個類名是否相同,而且要判斷是否由同一個類加載器實例加載的。
只有兩者同時滿足的情況下,JVM才認(rèn)為這兩個class是相同的。就算兩個class是同一份class字節(jié)碼,如果被兩個不同的ClassLoader實例所加載,JVM也會認(rèn)為它們是兩個不同class。
比如網(wǎng)絡(luò)上的一個Java類org.classloader.simple.NetClassLoaderSimple,javac編譯之后生成字節(jié)碼文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB這兩個類加載器并讀取了NetClassLoaderSimple.class文件,
并分別定義出了java.lang.Class實例來表示這個類,對于JVM來說,它們是兩個不同的實例對象,但它們確實是同一份字節(jié)碼文件,如果試圖將這個Class實例生成具體的對象進(jìn)行轉(zhuǎn)換時,
就會拋運行時異常java.lang.ClassCaseException,提示這是兩個不同的類型。

Android ClassLoader

Android 的 Dalvik/ART 虛擬機如同標(biāo)準(zhǔn) Java 的 JVM 虛擬機一樣,也是同樣需要加載 class 文件到內(nèi)存中來使用,但是在 ClassLoader 的加載細(xì)節(jié)上會有略微的差別。

熱修復(fù)入門:Android 中的 ClassLoader比較詳細(xì)介紹了Android中ClassLoader。

在Android開發(fā)者官網(wǎng)上的ClassLoader的文檔說明中我們可以看到,
ClassLoader是個抽象類,其具體實現(xiàn)的子類有 BaseDexClassLoader和SecureClassLoader。

SecureClassLoader的子類是URLClassLoader,其只能用來加載jar文件,這在Android的 Dalvik/ART 上沒法使用的。

BaseDexClassLoader的子類是PathClassLoader和DexClassLoader 。

PathClassLoader

PathClassLoader 在應(yīng)用啟動時創(chuàng)建,從/data/app/{package}安裝目錄下加載 apk 文件。

有2個構(gòu)造函數(shù),如下所示,這里遵從之前提到的雙親委托模型:

public PathClassLoader(String dexPath, ClassLoader parent) {
    super(dexPath, null, null, parent);
}

public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) {
    super(dexPath, null, libraryPath, parent);
}
  • dexPath : 包含dex的jar文件或apk文件的路徑集,多個以文件分隔符分隔,默認(rèn)是“:”

  • libraryPath : 包含 C/C++ 庫的路徑集,多個同樣以文件分隔符分隔,可以為空

PathClassLoader 里面除了這2個構(gòu)造方法以外就沒有其他的代碼了,具體的實現(xiàn)都是在 BaseDexClassLoader 里面,其dexPath比較受限制,一般是已經(jīng)安裝應(yīng)用的 apk 文件路徑。

在Android中,App安裝到手機后,apk里面的class.dex中的class均是通過PathClassLoader來加載的。

DexClassLoader

介紹 DexClassLoader 之前,先來看看其官方描述:

A class loader that loads classes from .jar and .apk filescontaining a classes.dex entry. This can be used to execute code notinstalled as part of an application.

很明顯,對比 PathClassLoader 只能加載已經(jīng)安裝應(yīng)用的dex或apk文件,DexClassLoader則沒有此限制,可以從SD卡上加載包含class.dex的.jar和.apk 文件,這也是插件化和熱修復(fù)的基礎(chǔ),在不需要安裝應(yīng)用的情況下,完成需要使用的dex的加載。

DexClassLoader 的源碼里面只有一個構(gòu)造方法,這里也是遵從雙親委托模型:

public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}

參數(shù)說明:

  • String dexPath : 包含 class.dex 的 apk、jar 文件路徑 ,多個用文件分隔符(默認(rèn)是 :)分隔

  • String optimizedDirectory : 用來緩存優(yōu)化的 dex 文件的路徑,即從 apk 或 jar 文件中提取出來的 dex 文件。該路徑不可以為空,且應(yīng)該是應(yīng)用私有的,有讀寫權(quán)限的路徑(實際上也可以使用外部存儲空間

  • String libraryPath : 存儲 C/C++ 庫文件的路徑集

  • ClassLoader parent : 父類加載器,遵從雙親委托模型

資源獲取

我們知道,Android Apk里面除了代碼,剩下的就是資源,而且資源占了很大一部分空間,我們可以利用ClassLoader來加載代碼,那么如何來加載apk中的資源,而且Android中的資源種類又可以分為很多種,比如布局、圖片,字符、樣式、主題等。

在組件中獲取資源時使用getResource獲得Resource對象,通過這個對象我們可以訪問相關(guān)資源,比如文本、圖片、顏色等。

通過跟蹤源碼發(fā)現(xiàn),其實getResource方法是Context的一個抽象方法,getResource的實現(xiàn)是在ContextImp中實現(xiàn)的。
獲取的Resource對象是應(yīng)用的全局變量,然后繼續(xù)跟蹤源碼,發(fā)現(xiàn) Resource中有一個AssetManager的全局變量,在Resource的構(gòu)造函數(shù)中傳入的,所以最終獲取資源都是通過AssetManager獲取的,于是我們把注意力放到AssetManager上。

我們要解決下面兩個問題。

一、如何獲取AssetManager對象。

二、如何通過AssetManager對象獲取插件中apk的資源。

通過對AssetManager的相關(guān)源碼跟蹤,我們找到答案。

一、AssetManager的構(gòu)造函數(shù)沒有對api公開,不能使用new創(chuàng)建;context.getAssets()可用獲取當(dāng)前上下文環(huán)境的 AssetManager;利用反射 AssetManager.class.newInstance()這樣可用獲取對象。

二、如何獲取插件apk中的資源。我們發(fā)現(xiàn)AssetManager中有個重要的方法。

/**
 * Add an additional set of assets to the asset manager.  This can be
 * either a directory or ZIP file.  Not for use by applications.  Returns
 * the cookie of the added asset, or 0 on failure.
 * {@hide}
 */
public final int addAssetPath(String path) {
    return  addAssetPathInternal(path, false);
}

我們可以把一個包含資源的文件包添加到assets中。這就是AssetManager查找資源的第一個路徑。這個方法是一個隱藏方法,我們可以通過反射調(diào)用。

AssetManager assetManager = AssetManager.class.newInstance() ; // context .getAssets()?
AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(assetManager, apkPath);
Resources pluginResources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());

Hook

Hook就是可以修改函數(shù)的調(diào)用,通常可以通過代理模式就可以達(dá)到修改的目的。

比如有個Java示例代碼

public interface IService {

    void fun();
}
public class ServiceImpl implements IService {

    private static final String TAG = "ServiceImpl";

    @Override
    public void fun() {
        Log.i(TAG, "fun: ");
    }
}

正常調(diào)用直接這樣就可以了。

public class MainActivity extends AppCompatActivity {

    private IService iService;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        iService = new ServiceImpl();
        callService();
    }

    void callService() {
        iService.fun();
    }
}

上面代碼中MainActivity中含有iService字段,可以利用反射機制來替換它,然后當(dāng)有其他地方調(diào)用iService的時候,就可以對調(diào)用方法進(jìn)攔截和處理。

可以先實現(xiàn)自己的代理類,對需要Hook的地方添加下代碼。

public class ServiceProxy implements IService {

    private static final String TAG = "ServiceProxy";

    @NonNull
    private IService base;

    public ServiceProxy(@NonNull IService base) {
        this.base = base;
    }

    @Override
    public void fun() {
        Log.i(TAG, "fun: before");
        base.fun();
        Log.i(TAG, "fun: after");
    }
}

然后再修改MainActivity中的iService的值,首先獲取iService字段的值,傳給自己定義的Proxy對象,然后把Proxy對象再賦值給原先的iService字段,這樣調(diào)用iService中方法的時候,就會執(zhí)行Proxy的方法,然后由Proxy再進(jìn)行處理。

void reflectHock() {
    try {
        Class<? extends MainActivity> aClass = MainActivity.class;
        Field field = aClass.getDeclaredField("iService");
        field.setAccessible(true);
        IService service = (IService) field.get(this);
        IService proxy = new ServiceProxy(service);
        field.set(this, proxy);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

當(dāng)然有時候,實現(xiàn)自己的Proxy類是很麻煩的,可以利用Java的動態(tài)代理技術(shù)來搞定。

public class MyInvocationHandler implements InvocationHandler {

    private static final String TAG = "MyInvocationHandler";

    @NonNull
    private IService service;

    public MyInvocationHandler(@NonNull IService service) {
        this.service = service;
    }

    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        Log.i(TAG, "invoke: before");
        Object result = method.invoke(service, objects);
        Log.i(TAG, "invoke: after");
        return result;
    }
}

void proxyHook() {
    try {
        Class<? extends MainActivity> aClass = MainActivity.class;
        Field field = aClass.getDeclaredField("iService");
        field.setAccessible(true);
        IService value = (IService) field.get(this);

        InvocationHandler handler = new MyInvocationHandler(value);
        ClassLoader classLoader = value.getClass().getClassLoader();
        Object instance = Proxy.newProxyInstance(classLoader, value.getClass().getInterfaces(), handler);

        field.set(this, instance);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

主流框架方案

Fragment加載

早在2012年時候,出現(xiàn)了一個簡單的Android插件化方案,原理大致這樣的。

我們知道Android基本的頁面元素是Activity,如果要動態(tài)加載一個界面,那么需要動態(tài)加載加載一個Activity,但是Activity是需要注冊在Manifest中的。

所以就把目標(biāo)瞄向了Fragment,首先Fragment是不需要注冊的,使用的時候直接new出一個對象即可,然后放到了Activity容器中即可,那么能否從一個apk中加載出來一個FragmentClass,然后使用反射實例化,然后放入到Activity中呢?

答案是可以的,首先在Manifest中定義個容器HostContainerActivity,然后頁面跳轉(zhuǎn)的時候通過intent,把目標(biāo)的頁面的fragment的class寫成路徑,
當(dāng) HostContainerActivity 頁面啟動,從intent中獲取Fragment的路徑,然后利用反射,動態(tài)new出一個示例放入到布局中即可。

AndroidDynamicLoader就是這樣一個解決方案,但是這個方案是有限制的,所有的頁面必須是Fragment,這樣肯定不符合要求,所以這個方案就沒有流行起來。

Activity代理

上面說道了使用Fragment加載的形式,來顯示插件中的頁面,但是這個解決方案是有限制的,界面全部只能用Fragment,不能用Activity,不能稱的上是一種完美的插件化解決方案。

那到底能不能用到Activity的方式,答案是肯定的。

可以這樣,上面介紹了Fragment動態(tài)加載原理,我們把Fragment的路徑換成Activity的路徑,然后用原先的那個容器Activity,做為一個代理Activity,當(dāng)HostContainerActivity啟動時候,
初始化將要顯示的Activity,然后當(dāng)容器Activity依次執(zhí)行對應(yīng)的生命周期時候,容器Activity做一個代理Activity,也要相應(yīng)執(zhí)行動態(tài)加載的Activity。

大致代碼示例如下:

public class HostContainerActivity extends BaseActivity {

    public static final String EXTRA_BASE_ACTIVITY = "extra_base_activity";
    private BaseActivity remote;

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        String clazz = getIntent().getStringExtra(EXTRA_BASE_ACTIVITY);
        try {
            remote = (BaseActivity) Class.forName(clazz).newInstance();
            remote.onCreate(savedInstanceState);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onStart() {
        super.onStart();

        remote.onStart();
    }

    @Override
    public void onResume() {
        super.onResume();

        remote.onResume();
    }

    @Override
    public void onPause() {
        super.onPause();

        remote.onPause();
    }

    @Override
    public void onStop() {
        super.onStop();

        remote.onStop();
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        remote.onDestroy();
    }
}

dynamic-load-apk 這個動態(tài)化框架就是利用這個原理來實現(xiàn)的。

但是這個方案還是有限制的,因為插件中的Activity并不是系統(tǒng)直接運行的,而是由另外一個Activity作為代理運行的,這個Activity不是一個真正的Activity,
很多的功能是限制的,比如需要在Activity彈出一個Toast,則是不行的,因為當(dāng)前的Activity沒有context,所以dynamic-load-apk提出了1個關(guān)鍵字——that,
java中this表示對象本身,但是本對象不能當(dāng)做context使用,因為當(dāng)前的Activity只是一個Java對象,而that是真正運行的Activity對象。

Activity占坑

上面介紹Activity代理的方法,雖然插件中可以正常使用Activity,但是限制還是很多,用起來很不方便。

那到底有沒有最優(yōu)解,既可以不需要注冊Activity,又可以動態(tài)的加載Activity,答案是肯定的。我們可以來一個偷梁換柱,既然要注冊咱們就先注冊一個,然后啟動的時候,
把需要的運行的Activity當(dāng)做參數(shù)傳遞過去,讓系統(tǒng)啟動那個替身Activity,當(dāng)時機恰當(dāng)?shù)臅r候,我們再把那個Activity的對象給換回來即可,這個叫做瞞天過海。

這里有一篇文章詳細(xì)記載了Activity占坑方案是怎么運行的以及方案的原理。

360RePlugin介紹

Ok,上面說了這么多,全部都是引子,下面著重介紹今天的主角——RePlugin。

RePlugin是一套完整的、穩(wěn)定的、適合全面使用的,占坑類插件化方案,由360手機衛(wèi)士的RePlugin Team研發(fā),也是業(yè)內(nèi)首個提出”全面插件化“(全面特性、全面兼容、全面使用)的方案。

主要優(yōu)勢

  • 極其靈活:

主程序無需升級(無需在Manifest中預(yù)埋組件),即可支持新增的四大組件,甚至全新的插件

  • 非常穩(wěn)定:

Hook點僅有一處(ClassLoader),無任何Binder Hook!如此可做到其崩潰率僅為“萬分之一”,并完美兼容市面上近乎所有的Android ROM

  • 特性豐富:

支持近乎所有在“單品”開發(fā)時的特性。包括靜態(tài)Receiver、Task-Affinity坑位、自定義Theme、進(jìn)程坑位、AppCompat、DataBinding等

  • 易于集成:

無論插件還是主程序,只需“數(shù)行”就能完成接入

  • 管理成熟:

擁有成熟穩(wěn)定的“插件管理方案”,支持插件安裝、升級、卸載、版本管理,甚至包括進(jìn)程通訊、協(xié)議版本、安全校驗等

  • 數(shù)億支撐:

有360手機衛(wèi)士龐大的數(shù)億用戶做支撐,三年多的殘酷驗證,確保App用到的方案是最穩(wěn)定、最適合使用的

集成與Demo演示

集成也非常簡單,比如有2個工程,一個是主工程host,一個是插件工程sub。

本人寫作的時候,RePlugin版本為2.1.5,可能會與最新版本不一致。

  • 添加Host根目錄Gradle依賴
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
        classpath 'com.qihoo360.replugin:replugin-host-gradle:2.1.5'
    }
}
  • 添加Host項目Gradle依賴
apply plugin: 'com.android.application'
apply plugin: 'replugin-host-gradle'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.0"
    defaultConfig {
        applicationId "cn.mycommons.replugindemo"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

repluginHostConfig {
    useAppCompat = true
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.qihoo360.replugin:replugin-host-lib:2.1.5'

    testCompile 'junit:junit:4.12'
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
}
  • 添加Sub根目錄Gradle依賴
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
        classpath 'com.qihoo360.replugin:replugin-plugin-gradle:2.1.5'
    }
}
  • 添加Sub項目Gradle依賴
apply plugin: 'com.android.application'
apply plugin: 'replugin-plugin-gradle'

android {
    compileSdkVersion 26
    buildToolsVersion "26.0.0"

    defaultConfig {
        applicationId "cn.mycommons.repluginsdemo.sub"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

repluginPluginConfig {
    //插件名
    pluginName = "app"
    //宿主app的包名
    hostApplicationId = "cn.mycommons.replugindemo"
    //宿主app的啟動activity
    hostAppLauncherActivity = "cn.mycommons.replugindemo.MainActivity"

    // Name of 'App Module',use '' if root dir is 'App Module'. ':app' as default.
    appModule = ':app'

    // Injectors ignored
    // LoaderActivityInjector: Replace Activity to LoaderActivity
    // ProviderInjector: Inject provider method call.
    // ignoredInjectors = ['LoaderActivityInjector']
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])

    compile 'com.android.support:appcompat-v7:26.+'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.qihoo360.replugin:replugin-plugin-lib:2.1.5'

    testCompile 'junit:junit:4.12'
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
}

原理介紹

RePlugin源碼主要分為4部分,對比其他插件化,它的強大和特色,在于它只Hook住了ClassLoader。One Hook這個堅持,最大程度保證了穩(wěn)定性、兼容性和可維護(hù)性。

host lib

插件宿主庫,主要是對插件的管理,以及對ClassLoader的Hook,具體原理和管理邏輯不做詳細(xì)解釋。

host gradle

對插件宿主代碼編譯過程進(jìn)行處理,主要有config.json文件生成、RePluginHostConfig.java代碼生成、以及Activity坑位代碼插入到Manifest中。

比如我們內(nèi)置一個插件,按照官方文檔,這樣操作的。

  • 將APK改名為:[插件名].jar

  • 放入主程序的assets/plugins目錄

我們可以看看Host apk中包含哪些資源。

插件自動生成了plugin-builtin.json文件

同時也在Manifest中插入很多坑位。

[圖片上傳失敗...(image-469f3a-1513305916950)]

RePluginHostConfig.java代碼生成邏輯。

plugin lib

同宿主庫一樣,這個是給插件App提供基本的支持。

plugin gradle

對插件App代碼編譯過程進(jìn)行處理,主要修改插件中四大組建的父類,沒錯,就是這樣。

比如有個LoginActivity,它是繼承Activity的,那么會修改它的父類為PluginActivity,如果是AppCompatActivity,那么會替換成PluginAppCompatActivity

如:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
    }
}

反編譯Apk可以看到修改后的結(jié)果。

源碼里面也有體現(xiàn)

其他插件化方案

上次大致是RePlugin的原理,當(dāng)然除了RePlugin的解決方案以外,還有其他幾家廠商的解決方案。

Instant App

Android Instant App 官網(wǎng)

16年IO的時候,Google提出了Instant App特性,在17年IO正式發(fā)布這項技術(shù),不過這項技術(shù)在我寫這篇文章的時候,還是beta版本。

它的使用方式很簡單,你在 Android 手機上,朋友給你發(fā)來一個鏈接,比方說一家外賣店面。而恰好外賣App應(yīng)用也支持了 Instant Apps。你點擊了這個鏈接,就直接進(jìn)入了外賣應(yīng)用,即便手機并沒有安裝它。

實現(xiàn)原理大致是利用App linker喚起打開app的intent,Google Play檢測到支持該intent,而且沒有安裝后,直接通過類似Android插件化的原理,打開相關(guān)頁面。

但是這個Instant App必須發(fā)布在Google Play上, 國內(nèi)暫時沒有辦法使用。

淘寶Atlas

淘寶Atlas

Atlas是伴隨著手機淘寶的不斷發(fā)展而衍生出來的一個運行于Android系統(tǒng)上的一個容器化框架,我們也叫動態(tài)組件化(Dynamic Bundle)框架。它主要提供了解耦化、組件化、動態(tài)性的支持。覆蓋了工程師的工程編碼期、Apk運行期以及后續(xù)運維期的各種問題。

在工程期,實現(xiàn)工程獨立開發(fā),調(diào)試的功能,工程模塊可以獨立。

在運行期,實現(xiàn)完整的組件生命周期的映射,類隔離等機制。

在運維期,提供快速增量的更新修復(fù)能力,快速升級。

Atlas是工程期和運行期共同起作用的框架,我們盡量將一些工作放到工程期,這樣保證運行期更簡單,更穩(wěn)定。

相比multidex,atlas在解決了方法數(shù)限制的同時以O(shè)SGI為參考,明確了業(yè)務(wù)開發(fā)的邊界,使得業(yè)務(wù)在滿足并行迭代,快速開發(fā)的同時,能夠進(jìn)行靈活發(fā)布,動態(tài)更新以及提供了線上故障快速修復(fù)的能力。

與外界某些插件框架不同的是,atlas是一個組件框架,atlas不是一個多進(jìn)程的框架,他主要完成的就是在運行環(huán)境中按需地去完成各個bundle的安裝,加載類和資源。

滴滴VirtualAPK

VirtualAPK

VirtualAPK介紹

VirtualAPK是滴滴17年開源出來的一款插件化方案。

Small

Small

世界那么大,組件那么小。Small,做最輕巧的跨平臺插件化框架。 ——Galenlin

這是Small作者,林光亮老師,給Small一句概括。

總結(jié)

本文只是簡單的介紹下插件化相關(guān)內(nèi)容,很多內(nèi)容也是參照大神的博客的,感覺80%都是從別人那邊復(fù)制過來的,同時插件不只是簡單的加載界面和資源,包括BroadCastReceiver、Service等組件使用。

RePlugin使用方法還是蠻簡單的,大部分情況下,插件的開發(fā),相當(dāng)于單獨的一個App開發(fā)。

相對于其他廠商的方案,個人比較偏向于RePlugin,主要是因為開發(fā)簡單,比較穩(wěn)定,Hook點少,支持特性較多等。

相關(guān)資料

關(guān)于Android模塊化我有一些話不知當(dāng)講不當(dāng)講

Android插件化原理解析——Hook機制之動態(tài)代理

APK文件結(jié)構(gòu)和安裝過程

Android進(jìn)程間通信資料

Android系統(tǒng)應(yīng)用框架篇:Activity啟動流程

Android 插件化原理解析——Hook機制之AMS&PMS

深入分析Java ClassLoader原理

熱修復(fù)入門:Android中的ClassLoader

ANDROID應(yīng)用程序插件化研究之ASSETMANAGER

DroidPlugin

DynamicAPK

AndroidDynamicLoader,利用動態(tài)加載Fragment來解決

dynamic-load-apk

android-pluginmgr

Small

DynamicAPK

淘寶Atlas

VirtualAPK

VirtualAPK介紹

Android Instant App 官網(wǎng)

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

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,242評論 25 708
  • 最近幾年移動開發(fā)業(yè)界興起了「 插件化技術(shù) 」的旋風(fēng),各個大廠都推出了自己的插件化框架,各種開源框架都評價自身功能優(yōu)...
    斜杠時光閱讀 4,002評論 1 36
  • 最近開始學(xué)水彩,都是臨摹,第一幅的兔子給了我信心……
    鳳梨君是也閱讀 278評論 0 11
  • 在互聯(lián)網(wǎng)之前,大多數(shù)信息是不對稱的,信息鏈條是冗長的,現(xiàn)在的互聯(lián)網(wǎng)公司做的都是對信息不對稱的對稱化與扁平化,打破原...
    lavili閱讀 333評論 0 1