手把手講解 Android插件化啟動Activity

前言

手把手講解系列文章,是我寫給各位看官,也是寫給我自己的。
文章可能過分詳細,但是這是為了幫助到盡量多的人,畢竟工作5,6年,不能老吸血,也到了回饋開源的時候.
這個系列的文章:
1、用通俗易懂的講解方式,講解一門技術的實用價值
2、詳細書寫源碼的追蹤,源碼截圖,繪制類的結構圖,盡量詳細地解釋原理的探索過程
3、提供Github 的 可運行的Demo工程,但是我所提供代碼,更多是提供思路,拋磚引玉,請酌情cv
4、集合整理原理探索過程中的一些坑,或者demo的運行過程中的注意事項
5、用gif圖,最直觀地展示demo運行效果

如果覺得細節太細,直接跳過看結論即可。
本人能力有限,如若發現描述不當之處,歡迎留言批評指正。

學到老活到老,路漫漫其修遠兮。與眾君共勉 !


引子

用過微信支付寶的人應該都知道,其中有很多功能,都是可以靈活配置的,比如,支付寶里面,可以用“淘票票”來買電影票,還可以購買火車票飛機票,,甚至還有“餓了么”外賣,如果細細去數的話,這些雜七雜八的功能,加起來可能有幾十上百個了。
試想,這么多小功能,代碼都寫在支付寶里面,支付寶會不會爆炸 o(╯□╰)o !那支付寶的開發者如何做到這些功能的集成呢?

插件化開發Demo地址

支付寶 app本身更像是一個 “ 空殼 ”,里面可以搭載很多小功能,這些小功能都是以"插件"的形式存在,支持小功能的靈活配置,用戶不想要某個功能,可以不顯示出來。
插件化開發是當下大型app必備的一項技術,不可不學。


鳴謝

感謝 大佬 "瀟湘夜雨" 提供的Demo
感謝 享學課堂alvin老師 的的熱心幫助


正文大綱

1.插件化開發的核心難點

2.插件化所需的技術理論基礎

3.核心難點的解決方案

4.核心代碼結構

5.如何使用Demo

6.最終效果展示


正文

1.插件化開發的核心難點

根據引子中所說,支付寶中各種各樣的功能,都是插件形式存在的,那么具體是如何存在?
我們所說的插件,其實是apk文件,即 xxx.apk
插件化開發的套路: 外殼app module + 多個插件Module + 插件框架層library module

  • 外殼app 負責整個app的外部架構,并且給插件提供入口組件(比如,用一個button作為“余額寶”的入口,點擊button,進入“余額寶” );
  • 多個插件Module,負責分開開發各個功能。嚴格來說,每個功能必須可以單獨運行,也必須支持集成到外殼app時運行。
  • 插件框架層library module, 所有插件化的核心代碼,都集中到這里。并且這個library要同時被外殼app和插件module引用.

文字描述不夠直觀?
看下圖:

插件化開發的代碼結構.png

那么現在很清晰了,插件化開發的難點,就是如何讓外殼app,啟動插件apk中的Activity.
既然給出了demo的代碼架構,那就順便給出github地址了:Demo


2.插件化所需的技術理論基礎

可能從上面的代碼架構上看,這項技術并不是很復雜,但是也是需要一定的技術基礎的,不然出一點小問題,一臉懵逼,無從查起就很尷尬了.

學習插件化開發,首先要了解

1.Activity是如何啟動的.
在我們自己的Activity里,開啟另一個Activity,使用startActivity即可,但是startActivity之后,系統做了什么?

開始追蹤源碼(源碼追蹤基于SDK 28 - 9.0):
我們通常通常啟動Activity,一般都是在Activity中 使用startActivity(intent),像下面這樣

public class MainActivity extends AppCompatActivity(){
    private void xxxx(){
        Intent i = new Intent(this,XXXActivity.class);
        startActivity(i);
    }
}

那么,startActivity到底做了什么,點進去看找到下面的代碼:

@Override
    public void startActivity(Intent intent, @Nullable Bundle options) {
        if (options != null) {
            startActivityForResult(intent, -1, options);
        } else {
            // Note we want to go through this call for compatibility with
            // applications that may have overridden the method.
            startActivityForResult(intent, -1);
        }
    }

繼續,追蹤這兩個startActivityForResult,直接到下面的代碼:

public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
            @Nullable Bundle options) {
        if (mParent == null) {
            options = transferSpringboardActivityOptions(options);
            Instrumentation.ActivityResult ar =
                mInstrumentation.execStartActivity(
                    this, mMainThread.getApplicationThread(), mToken, this,
                    intent, requestCode, options);//注意看這里,mInstrumentation.execStartActivity
            if (ar != null) {
                mMainThread.sendActivityResult(
                    mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                    ar.getResultData());
            }
            if (requestCode >= 0) {
               ···
                mStartedActivity = true;
            }

            cancelInputsAndStartExitTransition(options);
            // TODO Consider clearing/flushing other event sources and events for child windows.
        } else {
            if (options != null) {
                mParent.startActivityFromChild(this, intent, requestCode, options);
            } else {
                // Note we want to go through this method for compatibility with
                // existing applications that may have overridden it.
                mParent.startActivityFromChild(this, intent, requestCode);
            }
        }
    }

mInstrumentation.execStartActivity在這里被執行,
然而另一個分支mParent 不為空時,會執行mParent.startActivityFromChild
那么追蹤它startActivityFromChild

public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent,
            int requestCode, @Nullable Bundle options) {
        options = transferSpringboardActivityOptions(options);
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity(
                this, mMainThread.getApplicationThread(), mToken, child,
                intent, requestCode, options);//然而,這里還是執行了mInstrumentation.execStartActivity
        if (ar != null) {
            mMainThread.sendActivityResult(
                mToken, child.mEmbeddedID, requestCode,
                ar.getResultCode(), ar.getResultData());
        }
        cancelInputsAndStartExitTransition(options);
    }

然而,這里還是執行了mInstrumentation.execStartActivity,
綜上所述,startActivity,最終都會執行到mInstrumentation.execStartActivity
那么繼續跟蹤這個execStartActivity:

public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
       ...省略一大段...
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess(who);
            int result = ActivityManager.getService()//這個不就是大名鼎鼎的AMS么
                .startActivity(whoThread, who.getBasePackageName(), intent,
                        intent.resolveTypeIfNeeded(who.getContentResolver()),
                        token, target != null ? target.mEmbeddedID : null,
                        requestCode, 0, null, options);
            checkStartActivityResult(result, intent);
        } catch (RemoteException e) {
            throw new RuntimeException("Failure from system", e);
        }
        return null;
    }

注意這里有一個ActivityManager.getService(),其實他就是安卓里大名鼎鼎的AMS(ActivityManagerService),負責對 Android四大組件(activityservicebroadcastcontentProvider)的管理,包括啟動,生命周期管理等.

Activity里面startActivity的追蹤就到這里。

PS:其實,Activity不只是可以在Activity里啟動,還可以使用 getApplicationContext().startActivity(),有興趣的可以去追蹤一下,最終結論還是一樣,都會執行AMS的startActivity.

結論:

我們通常在自己的XXXActivity里調用startActivity之后,最終會執行AMS的startActivity,從而讓啟動的那個Activity具有生命周期.
那么,如果只是new 一個 Activity實例,它會不會具有生命周期呢?顯而易見了.


2. apk包(其實是壓縮包)里的各個文件各自的作用
androidStudio里,運行app,或者gradle 執行assemble命令可以生成apk文件,那么,我們解壓apk文件之后,它里面的各種內部文件,各自都起到了什么作用呢?

請看下圖

一個apk解壓之后的內容

這里我們發現了這么幾個東西:

  • classes.dex 工程里面的java源碼編譯打包而成.
    classes.dex文件,包含了這個apk的所有java類,那么我們拿到了這個dex文件,就有能力反射創建其中的類對象.
    用AndroidStudio可以看到其內容:

    image.png

  • res目錄 所有的資源文件

    image.png

    外殼app 通過資源包,可以拿到包里面的任意資源,當然,前提是,宿主要創建對應資源包的Resources對象.

  • resources.arsc res下所有資源的映射

    image.png

  • META-INF app簽名的一些東西

  • AndroidManifest.xml 清單文件

3.核心難點的解決方案

了解了上面的技術基礎,那么現在擺出解決方案:
外殼app,作為一個"宿主"。插件apk中的所有東西,無論是classes.dex里的類,還是res資源,都是"宿主"之外的東西,那么宿主要想使用自己身外的類和資源,需要解決3個問題:

1. 取得插件中的Activity的Class
解決方案 ==> 使用DexClassLoader.它是專門加載外部apk的類加載器.

2. 取得插件中的資源
解決方案 ==> 使用hook技術,創建只屬于外部插件的Resouces資源管理器.

3. 反射創建了插件中的Activity對象,但是它是沒有生命周期的,不能像使用宿主自身的Activity一樣擁有完整的生命周期.如果不理解,請回去看 “2.插件化所需的技術理論基礎”
解決方案 ==> 使用 代理Activity作為真正插件Activity的"傀儡".


4.核心代碼結構 這是Demo地址

以demo為樣板進行細節講解,下圖是demo的項目結構:

外殼app的結構.png

可以看到,外殼app很簡單,唯一要說明的就是插件apk,我放置在src/main/assets目錄,只是為了演示demo方便.

  • MyApp.java ,只做了一件事,PluginManager.getInstance().init(this); ,對PluginManager進行初始化并且賦予上下文.
  • MainActivity.java 只做了兩件事,
  1. 將asssets里面的apk文件,通過工具類AssetUtil的copyAssetToCache方法,拷貝到了app的緩存目錄下,然后使用PluginManager去加載這個apk.
String path = AssetUtil.copyAssetToCache(MainActivity.this, "plugin_module-debug.apk");
PluginManager.getInstance().loadPluginApk(path);
  1. 跳轉到代理Activity,并且傳入真正要跳的目標Activity的name.
// 先跳到代理Activity,由代理Activity展示真正的Activity內容
               Intent intent = new Intent(MainActivity.this, ProxyActivity.class);
               intent.putExtra(PluginApkConst.TAG_CLASS_NAME, 
                       PluginManager.getInstance().getPackageInfo().activities[0].name);
               startActivity(intent);

這里涉及到了PluginManager類,下一小節詳述.

插件module.png

插件module十分簡單,它的作用,就是生成插件apk,對它進行編譯打包,取得apk文件即可。
兩個重點:

  1. 插件中的所有Activity,必須都集成來自plugin_lib的PluginBaseActivity,只有繼承了,才具有插件化特征,能夠被外殼app執行startActivity成功跳轉.
  2. 插件內部的Activity跳轉,上下文,必須使用PluginBaseActivityproxy變量.
    image.png

前面兩個module都很簡單,那么核心技術在哪里?

插件化框架library.png

插件框架層代碼,是插件化開發技術的核心。這個module要同時被外殼app和插件module引用.

其中,3個技術要點:

  1. PluginManager類
    它是一個單例,負責讀取插件apk的內容,并且創建出專屬于插件的類加載器DexClassLoader,資源管理器Resources,以及包信息 PackageInfo 并 用public get方法公開出去。
public class PluginManager {

    //應該是單例模式,因為一個宿主app只需要一個插件管理器對象即可
    private PluginManager() {
    }

    private volatile static PluginManager instance;//volatile 保證每一次取的instance對象都是最新的

    public static PluginManager getInstance() {
        if (instance == null) {
            synchronized (PluginManager.class) {
                if (instance == null) {
                    instance = new PluginManager();
                }
            }
        }
        return instance;
    }

    private Context mContext;//上下文

    private PackageInfo packageInfo;//包信息
    private DexClassLoader dexClassLoader;//類加載器
    private Resources resources;//資源包

    public void init(Context context) {
        mContext = context.getApplicationContext();//要用application 因為這是單例,直接用Activity對象作為上下文會導致內存泄漏
    }

    /**
     * 從插件apk中讀出我們所需要的信息
     *
     * @param apkPath
     */
    public void loadPluginApk(String apkPath) {
        //先拿到包信息
        packageInfo = mContext.getPackageManager().getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES);//只拿Activity
        if (packageInfo == null)
            throw new RuntimeException("插件加載失敗");//如果apkPath是傳的錯的,那就拿不到包信息了,下面的代碼也就不用執行

        //類加載器,DexClassLoader專門負責外部dex的類
        File outFile = mContext.getDir("odex", Context.MODE_PRIVATE);
        dexClassLoader = new DexClassLoader(apkPath, outFile.getAbsolutePath(), null, mContext.getClassLoader());

        //創建AssetManager,然后創建Resources
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            method.invoke(assetManager, apkPath);
            resources = new Resources(assetManager,
                    mContext.getResources().getDisplayMetrics(),
                    mContext.getResources().getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    //把這3個玩意公開出去
    public PackageInfo getPackageInfo() {
        return packageInfo;
    }

    public DexClassLoader getDexClassLoader() {
        return dexClassLoader;
    }

    public Resources getResources() {
        return resources;
    }
}
  1. ProxyActivity類
    它作為一個代理,一個傀儡,宿主能夠通過它,來間接地管理真正插件Activity的生命周期.
    那它是如何間接管理真正Activity的生命周期?用類似下面的代碼:
public void ProxyActivity extends Activity{
    @Override
    protected void onStart() {
        iPlugin.onStart();//iPlugin是插件Activity實現的接口,前面用IPlugin將插件Activity對象接收了
        super.onStart();
    }
}

然而,ProxyActivity的onCreate另有玄機

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        realActivityName = getIntent().getStringExtra(PluginApkConst.TAG_CLASS_NAME);//宿主,將真正的跳轉意圖,放在了這個參數className中,
        //拿到realActivityName,接下來的工作,自然就是展示出真正的Activity
        try {// 原則,反射創建RealActivity對象,但是,去拿這個它的class,只能用dexClassLoader
            Class<?> realActivityClz = PluginManager.getInstance().getDexClassLoader().loadClass(realActivityName);
            Object obj = realActivityClz.newInstance();
            if (obj instanceof IPlugin) {//所有的插件Activity,都必須是IPlugin的實現類
                iPlugin = (IPlugin) obj;
                Bundle bd = new Bundle();
                bd.putInt(PluginApkConst.TAG_FROM, IPlugin.FROM_EXTERNAL);
                iPlugin.attach(this);
                iPlugin.onCreate(bd);//反射創建的插件Activity的生命周期函數不會被執行,那么,就由ProxyActivity代為執行
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

前面PluginManager返回了專屬于插件的類加載器DexClassLoader,資源管理器Resources,那么這個ProxyActivity真正展示的是插件的Activity內容,就要使用插件自己的類加載器和資源管理器了.

    @Override
    public ClassLoader getClassLoader() {
        ClassLoader classLoader = PluginManager.getInstance().getDexClassLoader();
        return classLoader != null ? classLoader : super.getClassLoader();
    }

    @Override
    public Resources getResources() {
        Resources resources = PluginManager.getInstance().getResources();
        return resources != null ? resources : super.getResources();
    }

注意,前方大坑,
public class ProxyActivity extends Activity{},我的ProxyActivity繼承的是android.app.Activity,而不是 android.support.v7.app.AppCompatActivity,這是因為,AppCompatActivity會檢測上下文context,從而導致空指針.
至于更深層的原因,有興趣的大佬可以繼續挖掘,沒興趣的話直接用android.app.Activity就完事了.


  1. IPlugin接口和PluginBaseActivity類
    插件module中也許不只一個Activity,我們啟動插件Activity之后,插件內部如果需要跳轉,仍然要遵守插件化的規則,那就給他們創建一個共同的父類PluginBaseActivity.

IPlugin 接口

/**
 * 插件Activity的接口規范
 */
public interface IPlugin {

    int FROM_INTERNAL = 0;//插件單獨測試時的內部跳轉
    int FROM_EXTERNAL = 1;//宿主執行的跳轉邏輯

    /**
     * 給插件Activity指定上下文
     *
     * @param activity
     */
    void attach(Activity activity);

    // 以下全都是Activity生命周期函數,
    // 插件Activity本身 在被用作"插件"的時候不具備生命周期,由宿主里面的代理Activity類代為管理
    void onCreate(Bundle saveInstanceState);

    void onStart();

    void onResume();

    void onRestart();

    void onPause();

    void onStop();

    void onDestroy();

    void onActivityResult(int requestCode, int resultCode, Intent data);
}

PluginBaseActivity 抽象類

/**
 * 插件Activity的基類,插件中的所有Activity,都要繼承它
 */
public abstract class PluginBaseActivity extends AppCompatActivity implements IPlugin {

    private final String TAG = "PluginBaseActivityTag";
    protected Activity proxy;//上下文

    //這里基本上都在重寫原本Activity的函數,因為 要兼容“插件單獨測試” 和 "集成到宿主整體測試",所以要進行情況區分
    private int from = IPlugin.FROM_INTERNAL;//默認是“插件單獨測試”

    @Override
    public void attach(Activity proxyActivity) {
        proxy = proxyActivity;
    }

    @Override
    public void onCreate(Bundle saveInstanceState) {
        if (saveInstanceState != null)
            from = saveInstanceState.getInt(PluginApkConst.TAG_FROM);

        if (from == IPlugin.FROM_INTERNAL) {
            super.onCreate(saveInstanceState);
            proxy = this;//如果是從內部跳轉,那就將上下文定為自己
        }
    }

    @Override
    public void onStart() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onStart();
        } else {
            Log.d(TAG, "宿主啟動:onStart()");
        }
    }

    @Override
    public void onResume() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onResume();
        } else {
            Log.d(TAG, "宿主啟動:onResume()");
        }
    }

    @Override
    public void onRestart() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onRestart();
        } else {
            Log.d(TAG, "宿主啟動:onRestart()");
        }
    }

    @Override
    public void onPause() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onPause();
        } else {
            Log.d(TAG, "宿主啟動:onPause()");
        }
    }

    @Override
    public void onStop() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onStop();
        } else {
            Log.d(TAG, "宿主啟動:onStop()");
        }
    }

    @Override
    public void onDestroy() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onDestroy();
        } else {
            Log.d(TAG, "宿主啟動:onDestroy()");
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onActivityResult(requestCode, resultCode, data);
        } else {
            Log.d(TAG, "宿主啟動:onActivityResult()");
        }
    }

    //下面是幾個生命周期之外的重寫函數
    @Override
    public void setContentView(int layoutResID) {//設置contentView分情況
        if (from == IPlugin.FROM_INTERNAL) {
            super.setContentView(layoutResID);
        } else {
            proxy.setContentView(layoutResID);
        }
    }

    @Override
    public View findViewById(int id) {
        if (from == FROM_INTERNAL) {
            return super.findViewById(id);
        } else {
            return proxy.findViewById(id);
        }
    }

    @Override
    public void startActivity(Intent intent) {//同理
        if (from == IPlugin.FROM_INTERNAL) {
            super.startActivity(intent);//原intent只能用于插件單獨運行時
        } else {
            // 如果是集成模式下,插件內的跳轉,控制權 仍然是在宿主上下文里面,所以--!
            // 先跳到代理Activity,由代理Activity展示真正的Activity內容
            Intent temp = new Intent(proxy, ProxyActivity.class);
            temp.putExtra(PluginApkConst.TAG_CLASS_NAME, intent.getComponent().getClassName());
            proxy.startActivity(temp);//這里不能跳原來的intent,,必須重新創建
        }
    }

}

PluginBaseActivity 抽象類中,3個重點需要特別說明

1. 插件module需要單獨測試,也需要 作為插件來集成測試,所以這里IPlugin接口中定義了 FROM_INTERNAL和FROM_EXTERNAL 進行情形區分.

2. 除了IPlugin必須實現的一些生命周期方法之外,最后我還新增了3個方法:
setContentViewfindViewByIdstartActivity
設置布局,尋找組件,跳轉Activity,也是需要區分 單測還是集成測試的,所以,也要做if/else判定.
并且[看3.]

3. 上面說的startActivity,當從外部跳轉,也就是宿主來啟動插件Activity的時候,也只能跳到ProxyActivity,然后把真正的目標Activity放在參數中.


5.如何使用Demo

image.png

我的demo中已經有了一個插件apk,如上圖.
如果你更改了plugin_module的內容,請重新生成一個apk,放到上圖所示位置,文件名必須和外殼app內寫的一樣.放好之后,運行外殼app即可。


6.最終效果展示

集成測試,由外殼app啟動插件Activity


插件化集成測試.gif

插件單獨測試


插件單獨測試.gif

結語

可能有人說,目前自己公司的app還用不著插件化這種重量級的技術,如果這樣說的人,真的是這么想,那么可能永遠只能做小app了。做技術如果不想做大做強,那和咸魚有什么區別,o(╯□╰)o
另有問題需要探討,或者發現錯誤,歡迎給我留言!
技術之路漫漫長,碼字不易,希望看到的客官點個好評,謝謝支持.
Demo地址


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

推薦閱讀更多精彩內容

  • 嗯哼嗯哼蹦擦擦~~~ 轉載自:https://github.com/Tim9Liu9/TimLiu-iOS 目錄 ...
    philiha閱讀 5,001評論 0 6
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,200評論 4 61
  • 秋風乍起夜未央,海河麗景伴在旁, 情懷滿斟杯中酒,熱語歡聲意飛揚。
    幻兒11閱讀 285評論 0 1
  • 江湖不僅神棍多,神一樣的戶型也不少。相信大家看了以后,可能都無法想象這些戶型是怎么一層一層的通過設計公司、地產公司...
    蓋幫貴州閱讀 491評論 0 3
  • 霓虹聽起來就色彩斑斕字眼 霓虹燈是溫暖的,可愛的 讓人想到家 想到節日 生日與家人相聚的快樂 那些屬于小時候的記憶
    很深的綠閱讀 386評論 0 0