Activity的外掛化(一)

渣渣008發表於2017-12-13

首先說明一下:

本文的編寫借鑑參考了大量的文章,有的可能是直接把文字拷貝過來的,我會在文中給出連結,如果有侵權,請聯絡我刪除,謝謝。

我們知道,啟動Activity可以是通過Activity或者通過Context,這兩種啟動沒有太大的區別,最終都是呼叫Instrumentation的方法來啟動的,當然說是這樣說,其實還是有區別滴,Activity的startActivity()方法可使用預設配置的LAUNCH FLAG,而Context的startActivity()須包含FLAG_ACTIVITY_NEW_TASK的LAUNCH FLAG,原因是該Context可能沒有現存的任務棧供新建的Activity使用,必須顯式指定生成一個自己單獨的任務棧。

Activity啟動發起後,通過Binder,最終由system_server程式中的AMS(ActivityManagerService)啟動的。這裡不打算說Activity的啟動過程了,因為套路就是那樣,太多的部落格文章也分析過了過程。想看啟動過程的可以去看看下面的文章: startActivity啟動過程分析 [Activity啟動過程全解析](http://blog.csdn.net/zhaokaiqiang1992/article/details/49428287) 如果對上面的文章都不滿意,或者還是有細節問題沒搞清楚,可以這樣:

image.png
嗯,都系你想要滴。 這裡直接貼出別人文章裡面畫的時序圖了。
image.png
說明:此圖出處為startActivity啟動過程分析

下面列出一些重要類:

  • ActivityManagerService,簡稱AMS,服務端物件處於system_server程式,負責系統中所有Activity的生命週期
  • ActivityThread,App的真正入口。當開啟App之後,會呼叫main()開始執行,開啟訊息迴圈佇列,這就是傳說中的UI執行緒或者叫主執行緒。與ActivityManagerService配合,一起完成Activity的管理工作
  • ApplicationThread,用來實現ActivityManagerService與ActivityThread之間的互動。在ActivityManagerService需要管理相關Application中的Activity的生命週期時,通過ApplicationThread的代理物件與ActivityThread通訊,因為App和AMS通訊,App是客戶端,AMS所在程式為服務端,這個時候一般是客戶端呼叫服務端的方法,但是這個是單向的,如果服務端要呼叫客戶端怎麼辦呢,通過ApplicationThread,這個時候App是服務端,AMS所在system_server程式為客戶端。
  • ApplicationThreadProxy,是ApplicationThread在伺服器端的代理,負責和App程式的服務端物件ApplicationThread通訊。AMS就是通過該代理與ActivityThread進行通訊的
  • Instrumentation,每一個應用程式只有一個Instrumentation物件,每個Activity內都有一個對該物件的引用。Instrumentation可以理解為應用程式的管家,ActivityThread要建立或暫停某個Activity時,都需要通過Instrumentation來進行具體的操作。
  • ActivityStack,Activity在AMS的棧管理,用來記錄已經啟動的Activity的先後關係,狀態資訊等。通過ActivityStack決定是否需要啟動新的程式。
  • ActivityRecord,ActivityStack的管理物件,每個Activity在AMS對應一個ActivityRecord,來記錄Activity的狀態以及其他的管理資訊。其實就是伺服器端的Activity物件的映像。
  • TaskRecord,AMS抽象出來的一個“任務”的概念,是記錄ActivityRecord的棧,一個“Task”包含若干個ActivityRecord。AMS用TaskRecord確保Activity啟動和退出的順序。如果你清楚Activity的4種launchMode,那麼對這個概念應該不陌生。

下面說幾個問題:

1.啟動Activity為什麼這麼複雜,需要跨程式?

一個原因是安卓的四大元件設計的都是允許某個元件執行在一個單獨的程式中的,安卓裡面所有的App程式都是Zygote程式fork出來的(你不要想著自己建立程式,你建立出來的程式,他需要的一些系統資源你怎麼給),如果我們的Activity元件配置了新的程式,是需要Zygote程式做事的,這就是一個跨程式了吧。這裡說一下元件配置程式的方式。 一般是通過在AndroidManifest.xmlandroid:process屬性來實現的。 當android:process屬性值以”:”開頭,則代表該程式是私有的,只有該App可以使用,其他應用無法訪問; 當android:process屬性值不以”:“開頭,則代表的是全域性型程式,但這種情況需要注意的是程式名必須至少包含“.”字元。

另一個原因是Activity的生命週期其實是由system_server程式中的ActivityManagerService(AMS)管理的,除了onCreate是在new出來之後就本程式呼叫外,其餘的都是AMS管理的。我們看IActivityManager介面就知道。

public interface IActivityManager extends IInterface {
    public void finishSubActivity(IBinder token, String resultWho, int requestCode) throws RemoteException;
    public boolean finishActivityAffinity(IBinder token) throws RemoteException;
    public void finishVoiceTask(IVoiceInteractionSession session) throws RemoteException;
    public boolean releaseActivityInstance(IBinder token) throws RemoteException;
    public void releaseSomeActivities(IApplicationThread app) throws RemoteException;
    public boolean willActivityBeVisible(IBinder token) throws RemoteException;
    public Intent registerReceiver(IApplicationThread caller, String callerPackage,
            IIntentReceiver receiver, IntentFilter filter,
            String requiredPermission, int userId) throws RemoteException;
    public void unregisterReceiver(IIntentReceiver receiver) throws RemoteException;
    public int broadcastIntent(IApplicationThread caller, Intent intent,
            String resolvedType, IIntentReceiver resultTo, int resultCode,
            String resultData, Bundle map, String[] requiredPermissions,
            int appOp, Bundle options, boolean serialized, boolean sticky, int userId) throws RemoteException;
    public void unbroadcastIntent(IApplicationThread caller, Intent intent, int userId) throws RemoteException;
    public void finishReceiver(IBinder who, int resultCode, String resultData, Bundle map,
            boolean abortBroadcast, int flags) throws RemoteException;
    public void attachApplication(IApplicationThread app) throws RemoteException;
    public void activityResumed(IBinder token) throws RemoteException;
    public void activityIdle(IBinder token, Configuration config,
            boolean stopProfiling) throws RemoteException;
    public void activityPaused(IBinder token) throws RemoteException;
    public void activityStopped(IBinder token, Bundle state,
            PersistableBundle persistentState, CharSequence description) throws RemoteException;
    public void activitySlept(IBinder token) throws RemoteException;
    public void activityDestroyed(IBinder token) throws RemoteException;
}
複製程式碼

為什麼Activity的生命週期需要system_server來管理麼,不是我的人生我做主麼,這個問題大概想一下就知道,我們現在在使用一個App,停留在A介面並且正在播放小視訊,突然有人來了,so趕緊按了Home鍵,這個時候切換程式回到了桌面Launcher程式,這個時候我們肯定是希望A介面的視訊停止播放啊,這個時候如果是App自己管理生命,App根本不知道現在已經處於桌面了,所以很明顯這一個簡單的場景就知道Activity的生命週期自己回撥管理是不存在的。

#### 2.Activity是怎麼怎麼跨程式和ActivityManagerService通訊的? 這個答案是很明顯是通過Binder的,但是具體Binder怎麼通訊的,這個要說起來估計一篇文章也遠遠說不完。我在這裡一時半會也說不清,而且,我現在的描述和對Binder的理解也沒有特別到位,所以這裡只說Framework層Binder的使用。

Binder使用過程: ##### 2.1.制定協議介面 Binder是C/S架構的,對應著Client端和Server端。要使用Binder,首先我們要定一個協議,就是客戶端和服務端需要做什麼事情,這裡對應到Java端就是定一個客戶端和服務端通用的介面,這個藉口需要實現IInterface這個空介面,為什麼要實現這個介面呢,這個介面裡面定義了一個方法用於返回Binder物件,這個物件用於Binder通訊使用。

/**
* Base class for Binder interfaces.  When defining a new interface,
* you must derive it from IInterface.
*/
public interface IInterface
{
    /**
    * Retrieve the Binder object associated with this interface.
    * You must use this instead of a plain cast, so that proxy objects
    * can return the correct result.
    */
    public IBinder asBinder();
}
複製程式碼

舉例:這裡直接拿IApplicationThread舉例了,他是用於system_server程式來跨程式呼叫App方法,嗯,前面說的AMS是App程式跨程式呼叫system_server程式方法,剛好是相反滴,AIDL也是一樣噠。

public interface IApplicationThread extends IInterface {
    void schedulePauseActivity(IBinder token, boolean finished, boolean userLeaving,
            int configChanges, boolean dontReport) throws RemoteException;
    void scheduleStopActivity(IBinder token, boolean showWindow,
            int configChanges) throws RemoteException;
    void scheduleWindowVisibility(IBinder token, boolean showWindow) throws RemoteException;
    void scheduleSleeping(IBinder token, boolean sleeping) throws RemoteException;
    void scheduleResumeActivity(IBinder token, int procState, boolean isForward, Bundle resumeArgs)
            throws RemoteException;
}
int SCHEDULE_PAUSE_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION;
int SCHEDULE_STOP_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+2;
int SCHEDULE_WINDOW_VISIBILITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+3;
int SCHEDULE_RESUME_ACTIVITY_TRANSACTION = IBinder.FIRST_CALL_TRANSACTION+4;
複製程式碼

並且這裡給每個方法編號,來標識每個方法。

##### 2.2.服務端的實現 服務端的實現,繼承Binder類,實現上面定義的公共介面IApplicationThread。然後實現裡面的方法。

private class ApplicationThread extends ApplicationThreadNative {

    private void updatePendingConfiguration(Configuration config) {
        synchronized (mResourcesManager) {
            if (mPendingConfiguration == null ||
                    mPendingConfiguration.isOtherSeqNewer(config)) {
                mPendingConfiguration = config;
            }
        }
    }

    public final void schedulePauseActivity(IBinder token, boolean finished,
            boolean userLeaving, int configChanges, boolean dontReport) {
        sendMessage(
                finished ? H.PAUSE_ACTIVITY_FINISHING : H.PAUSE_ACTIVITY,
                token,
                (userLeaving ? 1 : 0) | (dontReport ? 2 : 0),
                configChanges);
    }
}
複製程式碼

這些方法就真正辦事情的方法,這裡繼承Binder了,還需要複寫另外一個onTransact方法,因為都說了是跨程式呼叫肯定不能直接呼叫方法的,肯定是客戶端和服務端用同樣的上面介面定義的標識,然後根據標識呼叫到對應的方法的。

@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
        throws RemoteException {
    switch (code) {
    case SCHEDULE_PAUSE_ACTIVITY_TRANSACTION:
    {
        data.enforceInterface(IApplicationThread.descriptor);
        IBinder b = data.readStrongBinder();
        boolean finished = data.readInt() != 0;
        boolean userLeaving = data.readInt() != 0;
        int configChanges = data.readInt();
        boolean dontReport = data.readInt() != 0;
        schedulePauseActivity(b, finished, userLeaving, configChanges, dontReport);
        return true;
    }

    case SCHEDULE_STOP_ACTIVITY_TRANSACTION:
    {
        data.enforceInterface(IApplicationThread.descriptor);
        IBinder b = data.readStrongBinder();
        boolean show = data.readInt() != 0;
        int configChanges = data.readInt();
        scheduleStopActivity(b, show, configChanges);
        return true;
    }
}
複製程式碼

##### 2.3.客戶端的實現 客戶端的實現,實現上面定義的公共介面IApplicationThread。然後實現裡面的方法。

class ApplicationThreadProxy implements IApplicationThread {
    private final IBinder mRemote;
    
    public ApplicationThreadProxy(IBinder remote) {
        mRemote = remote;
    }
    
    public final IBinder asBinder() {
        return mRemote;
    }
    
    public final void schedulePauseActivity(IBinder token, boolean finished,
            boolean userLeaving, int configChanges, boolean dontReport) throws RemoteException {
        Parcel data = Parcel.obtain();
        data.writeInterfaceToken(IApplicationThread.descriptor);
        data.writeStrongBinder(token);
        data.writeInt(finished ? 1 : 0);
        data.writeInt(userLeaving ? 1 :0);
        data.writeInt(configChanges);
        data.writeInt(dontReport ? 1 : 0);
        mRemote.transact(SCHEDULE_PAUSE_ACTIVITY_TRANSACTION, data, null,
                IBinder.FLAG_ONEWAY);
        data.recycle();
    }
}
複製程式碼

這裡的實現方法只是把*要呼叫的方法的標識,傳遞的引數,通過mRemote寫入Binder驅動,然後等待遠端方法的呼叫,最後把結果通過Binder驅動寫回來。*這裡的mRemote其實指的是BinderProxy這個類,裡面有native方法和Binder互動,具體是怎麼知道是這個類的,你們還是去看文章吧,一時半會也說不清。

##### 2.4.客戶端和服務端的轉換 我們可以從這裡看出,ActivityThread.attach方法,這裡呢,我們的App是服務端,AMS是客戶端。最終呼叫的是AMS的代理類ActivityManagerProxy

public void attachApplication(IApplicationThread app) throws RemoteException
{
    Parcel data = Parcel.obtain();
    Parcel reply = Parcel.obtain();
    data.writeInterfaceToken(IActivityManager.descriptor);
    data.writeStrongBinder(app.asBinder());
    mRemote.transact(ATTACH_APPLICATION_TRANSACTION, data, reply, 0);
    reply.readException();
    data.recycle();
    reply.recycle();
}
複製程式碼

對應到服務端ActivityManagerService。首先onTransact裡面:

case ATTACH_APPLICATION_TRANSACTION: {
    data.enforceInterface(IActivityManager.descriptor);
    IApplicationThread app = ApplicationThreadNative.asInterface(
            data.readStrongBinder());
    if (app != null) {
        attachApplication(app);
    }
    reply.writeNoException();
    return true;
}
複製程式碼

這裡data.readStrongBinder()得到的是BinderProxy物件,就拿到了ApplicationThreadProxy,至於中間的層層轉換也是Binder底層的操作。

#### 3.Activity可以怎麼HOOK? 啟動Activity,非常的簡單,startActivity方法即可搞定,但是安卓有一個限制,必須是在Manifest裡面宣告Activity才能被啟動。嗯,這個校驗過程並不在本地而在ActivityManagerService所在的system_server程式裡面,並不能做什麼手腳。 所以現在是衍生出了一些解法,既然要啟動的Activity必須是在Manifest裡面註冊,那可以提前註冊一些Activity以供使用噠。嗯,關於這個也份兩種做法。

3.1.代理Activity模式

所謂代理Activity模式主要特點是這樣: 主專案APK註冊一個代理Activity(命名為ProxyActivity),ProxyActivity是一個普通的Activity,但只是一個空殼,自身並沒有什麼業務邏輯。每次開啟外掛APK裡的某一個Activity的時候,都是在主專案裡使用標準的方式啟動ProxyActivity,再在ProxyActivity的生命週期裡同步呼叫外掛中的Activity例項的生命週期方法,從而執行外掛APK的業務邏輯。 上面的特點描述出自:代理Activity模式

由於現在的外掛化Activity的方式都是使用的接下來3.2中的第二種,並且代理Activity模式也確實不是很方便,所以不是要說的重點。 代理Activity模式外掛化框架的具體實現就是dynamic-load-apk 關於Activity定義了DLPlugin介面來表示:DLPlugin 把Activity關鍵的生命週期方法抽象成DLPlugin介面,ProxyActivity通過DLPlugin代理呼叫外掛Activity的生命週期。

image.png

image.png

載入外掛的時候,先解析apk檔案,然後建立ClassLoaderResources,這兩個問題也是特別麻煩的兩個問題,後面會說到,因為一時半會說不清楚。 準備工作程式碼:DLPluginManager

image.png
啟動Activity的核心程式碼也在這個類裡面的:
image.png
再看這個:
image.png
哈哈哈,是不是感覺dynamic-load-apk的程式碼特別簡單,輕鬆看懂,美滋滋,關於代理Activity模式的就說到這裡,如果想要了解更多去閱讀這個專案的原始碼吧,說實話程式碼也特別好看懂,比別的外掛化框架好懂太多,因為比較簡單。

3.2.動態建立Activity模式

其實上面的程式碼模式的Activity是有一定的缺陷的,比如開發要使用that關鍵字,啟動的都是ProxyActivity,LaunchMode的問題等等。所以呢,後面有人繼續研究,就出現了現在的動態建立Activity模式。 先說一點,動態建立Activity的基礎: 1.需要對Activity的啟動過程,Binder機制有一定的認識; 2.基於Hook,動態建立Activity模式是通過Hook了系統的部分api實現的,所以需要相容; 3.需要預註冊佔坑,前面就分析了,要啟動的Activity必須已經註冊了,所以呢,動態建立也不例外需要先建立好一些Activity放在Manifest裡面使用,可以建立一些不同啟動模式,甚至不同程式的以支援多程式; 要Hook掉Activity把真正要啟動的Activity在開始啟動的時候替換掉成已經註冊的Activity,並在system_server程式的AMS把事情辦完,就是一些Activity的管理以及一些校驗工作,回到App程式的時候替換回真正的Activity,可以想到大致有兩種方案。 1.Hook住startActivity方法,在這裡替換真正的Activity和預註冊的Activity,當然需要校驗一下啟動的Activity的一些引數,例如LaunchMode以便選取最優的預註冊Activity,在AMS做完事情回到App程式的時候Hook住handleLaunchActivity方法。 Hook startActivity 方法的時候,比較重一點的方式是Hook AMS,比較輕一點的方法可以Hook Instrumentation。

2.直接對ClassLoader動手腳,載入外掛類的時候做處理,這個360團隊的外掛化框架RePlugin就是這樣做的,當然這個會更麻煩。

具體操作下一篇說。

#### 4.ClassLoader處理? ClassLoader如果不知道嘎哈的,必須先去了解一下咯。

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    // ...
    Activity activity = null;
    try {
        java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess();
        if (r.state != null) {
            r.state.setClassLoader(cl);
        }
    } catch (Exception e) {
        if (!mInstrumentation.onException(activity, e)) {
            throw new RuntimeException(
                "Unable to instantiate activity " + component
                + ": " + e.toString(), e);
        }
    }
    // ...
    return activity;
}
複製程式碼

這個問題也可以直接看這篇文章:外掛載入機制 瞭解Activity的啟動流程了,我們知道最後啟動Activity是由AMS裡面呼叫ATP(ApplicationThreadProxy),跨程式呼叫到我們App的AT(ApplicationThread),然後AT傳送訊息給Handler H,然後呼叫ActivityThreadperformLaunchActivity方法,也就是上面我貼出的程式碼。因為Activity也是java物件的嘛,new的時候肯定是需要ClassLoader的,不光是Activity,載入外掛所有的類都需要ClassLoader的。 這樣其實就會遇到一個問題,如果Activity元件存在於獨立於宿主程式的檔案之中,系統的ClassLoader怎麼知道去哪裡載入呢?因此,如果不做額外的處理,外掛中的Activity物件甚至都沒有辦法建立出來,談何啟動? 關於外掛程式碼的載入ClassLoader,也有兩種方式: 1.自定義ClassLoader載入 自己建立ClassLoader去載入外掛,每個外掛一個ClassLoader。

2.委託系統ClassLoader載入 可以把我們的外掛apk路徑放到pathList的物件DexPathList的dexElements欄位裡面去,然後載入的時候就可以載入到了。

image.png

image.png
上面說的很不具體,詳細的可以看我推薦的那篇文章。 第一種方案,每一個外掛都有一個自己的ClassLoader,因此類的隔離性非常好,如果不同的外掛使用了同一個庫的不同版本,就是不同的外掛之前可以引用相同庫的不同版本,然而這也就意味著,如果採用這種方案的話,外掛之間,宿主與外掛之間,想使用相同的庫,都需要引入,這樣會導致外掛體積變大的。 他也還有一個好處,如果外掛需要升級,直接重新建立一個自定的ClassLoader載入新的外掛,然後替換掉原來的版本即可(Java中,不同ClassLoader載入的同一個類被認為是不同的類)。

第二種方案,宿主和外掛,外掛和外掛之間不能存在相同的類。外掛升級了之後需要下次啟動才能更新。關於這個有看到一個比較好的實現方案,在外掛更新了之後也能立即更新的。他是通過替換掉系統的ClassLoader,然後也是每個外掛對應一個ClassLoader,可以看看原始碼ZeusClassLoader

#### 5.資源處理? 資源的處理,之前有篇文章略微提及了,Android的資源管理器的建立過程,這個也確實很麻煩,會再單獨寫一篇文章來說明。

6.結束語

講完了?不存在的,因為Activity的起點涉及到很多,這裡面只是講了5個問題(第五個問題還沒細說,逃),下篇文章,會參考眾多的開源的外掛化專案,寫一個比較完整的Activity的外掛化的Demo,寫了Activity的外掛化的Demo之後,對說後面的BroadcastReceiver,Service,ContentProvider也有幫助。

相關文章