手寫插件化二

接上篇手寫插件化,文末放demo鏈接。

上篇擼完了四大組件之Activity,成功加載插件Activity并運行。但是后續發現修改宿主APP資源為插件資源,然后調用宿主Activity的setContentView()設置布局時會引發一個bug。這篇就先解決這個bug,再擼一下Service。

fixbug

先說說bug原因:HostActivity繼承自AppCompatActivity,調用setContentView設置布局的時候報錯java.lang.NullPointerException: Attempt to invoke interface method 'void androidx.appcompat.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference,堆棧很明顯了,跟進去看一下。

AppCompatActivity.setContentView()的流程大家都比較熟悉了,不熟悉也沒關系可以參考這篇setContentView,下面直接定位到報錯點:

AppCompatDelegateImpl.createSubDecor()

        ......
        mDecorContentParent = (DecorContentParent) subDecor
                        .findViewById(R.id.decor_content_parent);
        mDecorContentParent.setWindowCallback(getWindowCallback());
        ......

        mWindow.setContentView(subDecor);

mDecorContentParent為null報錯空指針,很明顯findViewById沒有找到對應view,細想一下也很正常。還記得上篇我們在宿主HostActivity.onCreate()時反射創建插件資源Resources,然后重寫了getResources()方法嗎?此處系統使用到的R.id.decor_content_parent,findViewById時走的插件資源,應該是插件資源和宿主資源索引對應不上,也有可能插件apk包中就沒有這個系統資源。

    private var pluginResources: Resources? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        initCurrentActivity()
        initActivityResource()
        super.onCreate(savedInstanceState)
        pluginActivity?.onCreate(savedInstanceState)
    }

    override fun getResources(): Resources {
        return pluginResources ?: super.getResources()
    }

    private fun initActivityResource() {
        try {
            val pluginAssetManager = AssetManager::class.java.newInstance()
            val addAssetPathMethod = pluginAssetManager.javaClass
                .getMethod("addAssetPath", String::class.java)
            addAssetPathMethod.invoke(pluginAssetManager, apkPath)
            pluginResources = Resources(
                pluginAssetManager,
                super.getResources().displayMetrics,
                super.getResources().configuration
            )
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

找到原因其實就比較容易解決了。搜索一番發現有些小伙伴使用站樁方式寫插件化時也出現了這個問題,將HostActivity改為繼承Activity就能解決。其實也不叫解決,只是巧妙的規避了這個問題,因為Activity.setContentView()流程不太一樣,直接走的window.setContentView(),而AppCompatActivity.setContentView()是在createSubDecor()中搞了一堆操作之后才調用的mWindow.setContentView(subDecor)。還有一些插件化實現方式會將插件資源和宿主資源合并,這樣也不會引發這個問題。當然這是題外話了,繼續跟眼前的問題。

Activity.setContentView()

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

將繼承改為Activity解決是能解決,但是不太優雅。畢竟AppCompatActivity有通過LayoutFactory2將系統View轉換為同名AppCompatView以便在低版本支持tint屬性,而且createSubDecor()還有一堆操作,光是LayoutFactory2接口的作用就足以拋棄這個方案了。

思考一下,有沒有可能創建插件xml view時使用插件Resources,然后調用Host.setContentView()時改為走宿主Resources,設置完布局之后再改回為插件Resources。有點繞哈,直接上代碼了。

PluginActivity調用宿主封裝方法setHostContentView()

    fun setContentView(@LayoutRes layoutResID: Int) {
        host?.setHostContentView(layoutResID)
    }

    fun setContentView(view: View) {
        host?.setHostContentView(view)
    }

HostActivity

    private var pluginResources: Resources? = null
    private var realResources: Resources? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        initCurrentActivity()
        initActivityResource()
        super.onCreate(savedInstanceState)
        pluginActivity?.onCreate(savedInstanceState)
    }

    fun setHostContentView(@LayoutRes layoutResID: Int) {
        val view = LayoutInflater.from(this).inflate(layoutResID, null, false)
        setHostContentView(view)
    }

    fun setHostContentView(view: View) {
        beforeSetContentView()
        setContentView(view)
        afterSetContentView()
    }

    private fun beforeSetContentView() {
        realResources = super.getResources()
    }

    private fun afterSetContentView() {
        realResources = pluginResources
    }

    override fun getResources(): Resources {
        return realResources ?: super.getResources()
    }

    private fun initActivityResource() {
        try {
            val pluginAssetManager = AssetManager::class.java.newInstance()
            val addAssetPathMethod = pluginAssetManager.javaClass
                .getMethod("addAssetPath", String::class.java)
            addAssetPathMethod.invoke(pluginAssetManager, apkPath)
            pluginResources = Resources(
                pluginAssetManager,
                super.getResources().displayMetrics,
                super.getResources().configuration
            )
            realResources = pluginResources
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

重點看setHostContentView(@LayoutRes layoutResID: Int)方法,在插件onCreate()之前宿主已經調用initActivityResource()將資源改為了插件Resources,此時LayoutInflater.inflate()正常創建插件View。然后在真正調用setContentView(view)之前,先調用beforeSetContentView(),將資源改為宿主原本的Resources,如此一來AppCompatActivity.setContentView()內部不會出現找不到系統資源id的情況,在setContentView(view)之后再調用afterSetContentView()將資源改回為插件Resources

    fun setHostContentView(@LayoutRes layoutResID: Int) {
        val view = LayoutInflater.from(this).inflate(layoutResID, null, false)
        beforeSetContentView()
        setContentView(view)
        afterSetContentView()
    }

此方案對上層開發人員來說也是無感知的,開發插件Activity時仍然調用PluginActivity.setContentView()設置布局,只不過其內部調用到宿主時對資源進行了一番偷梁換柱,更確切的說是ABA操作。

Service

Service和Activity不一樣,Activity默認的啟動模式每次跳轉都會創建一個新的實例,所以一個launchMode為standard的站樁HostActivity就足以應付絕大多數場景。Service簡單點做的話多預埋一些站樁也是可以的,當然還有多進程的情況,綁定模式啟動遠程服務。

  1. 本地服務,也就是不指定進程。
    先在base module里面寫Service基類,HostService,有了HostActivity的經驗這次輕車熟路。
abstract class HostService : Service() {
    private var pluginClassLoader: PluginClassLoader? = null
    private var pluginService: PluginService? = null

    private var apkPath: String? = null
    private var pluginResources: Resources? = null

    override fun onCreate() {
        super.onCreate()
        pluginService?.onCreate()
    }

    private fun initCurrentService(intent: Intent?) {
        apkPath = "${cacheDir.absolutePath}${File.separator}plugin-debug.apk"
        pluginClassLoader = PluginClassLoader(
            dexPath = apkPath ?: "",
            optimizedDirectory = cacheDir.absolutePath,
            librarySearchPath = null,
            classLoader
        )
        val serviceName = intent?.getStringExtra("ServiceName") ?: ""
        pluginService = pluginClassLoader?.loadService(serviceName, this)
    }

    private fun initServiceResource() {
        try {
            val pluginAssetManager = AssetManager::class.java.newInstance()
            val addAssetPathMethod = pluginAssetManager.javaClass
                .getMethod("addAssetPath", String::class.java)
            addAssetPathMethod.invoke(pluginAssetManager, apkPath)
            pluginResources = Resources(
                pluginAssetManager,
                super.getResources().displayMetrics,
                super.getResources().configuration
            )
        } catch (e: Exception) {
            e.printStackTrace()
        }
    }

    override fun getResources(): Resources {
        return pluginResources ?: super.getResources()
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        if (pluginService == null) {
            initCurrentService(intent)
        }
        if (pluginResources == null) {
            initServiceResource()
        }
        return pluginService?.onStartCommand(intent, flags, startId) ?: super.onStartCommand(
            intent,
            flags,
            startId
        )
    }

    override fun onBind(intent: Intent?): IBinder? {
        if (pluginService == null) {
            initCurrentService(intent)
        }
        if (pluginResources == null) {
            initServiceResource()
        }
        return pluginService?.onBind(intent)
    }

    override fun onUnbind(intent: Intent?): Boolean {
        return pluginService?.onUnbind(intent) ?: false
    }
}

插件基類PluginService

open class PluginService : PluginServiceLifecycle {
    private var host: HostService? = null

    protected val context: Context?
        get() = host

    fun bindHost(host: HostService) {
        this.host = host
    }

    override fun onCreate() {

    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        return START_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? {
        return null
    }

    override fun onUnbind(intent: Intent?): Boolean {
        return false
    }
}

插件基類實現PluginServiceLifecycle接口,同步站樁HostService生命周期。

interface PluginServiceLifecycle {
    fun onCreate()
    fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int
    fun onBind(intent: Intent?): IBinder?
    fun onUnbind(intent: Intent?): Boolean
}

HostService有兩種:一種是本地服務,一種是遠程服務。這里先寫本地服務LocalHostService

class LocalHostService : HostService()

沒啥代碼,主要實現都在基類中,重點是在Manifest中注冊。

    <application>
        <activity
            android:name="com.chenxuan.base.activity.HostActivity"
            android:launchMode="standard" />

        <service android:name="com.chenxuan.base.service.LocalHostService" />

        <service
            android:name="com.chenxuan.base.service.RemoteHostService"
            android:process=":remote" />
    </application>
  1. 遠程服務,上面已經注冊好了RemoteHostService,指定進程名:remote
class RemoteHostService : HostService()

接下來封裝啟動服務的方法IntentKtx

fun Activity.jumpPluginActivity(activityName: String, pluginName: String? = "") {
    startActivity(Intent(this, HostActivity::class.java).apply {
        putExtra("ActivityName", activityName)
        putExtra("PluginName", pluginName)
    })
}

fun Activity.startLocalPluginService(
    serviceName: String,
) {
    startService(Intent(this, LocalHostService::class.java).apply {
        putExtra("ServiceName", serviceName)
    })
}

fun Activity.bindLocalPluginService(
    serviceName: String,
    conn: ServiceConnection,
    flags: Int
) {
    bindService(Intent(this, LocalHostService::class.java).apply {
        putExtra("ServiceName", serviceName)
    }, conn, flags)
}

fun Activity.bindRemotePluginService(
    serviceName: String,
    conn: ServiceConnection,
    flags: Int
) {
    bindService(Intent(this, RemoteHostService::class.java).apply {
        putExtra("ServiceName", serviceName)
    }, conn, flags)
}

然后是加載服務的方法PluginClassLoader.loadService()

class PluginClassLoader(
    dexPath: String,
    optimizedDirectory: String,
    librarySearchPath: String?,
    parent: ClassLoader
) : DexClassLoader(dexPath, optimizedDirectory, librarySearchPath, parent) {
    fun loadActivity(activityName: String, host: HostActivity): PluginActivity? {
        try {
            return (loadClass(activityName)?.newInstance() as PluginActivity?).apply {
                this?.bindHost(host)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return null
    }

    fun loadService(serviceName: String, host: HostService): PluginService? {
        try {
            return (loadClass(serviceName)?.newInstance() as PluginService?).apply {
                this?.bindHost(host)
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }
        return null
    }
}

流程寫來和Activity差不多,就是多預埋一個遠程服務。然后分別封裝本地服務的start、bind,遠程服務的start、bind方法。

下面在plugin module中編寫服務,然后run一下生成apk上傳到宿主私有cache目錄。

NormalService

class NormalService : PluginService() {
    override fun onCreate() {
        super.onCreate()
        context?.log("onCreate")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        context?.log("onStartCommand")
        return super.onStartCommand(intent, flags, startId)
    }
}

app module下MainActivity加個按鈕啟動本地服務,測試一下沒啥大問題。美中不足的是,插件Service實例是在onStartCommand()中加載,這已經在onCreate()之后了,所以插件Service沒有同步到onCreate()生命周期。而Service.onCreate()方法中拿不到Intent,無法取得Service全類名進行加載,后續再看看有什么方案可以在HostService.onCreate()之前實例化插件Service進行優化。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<TextView>(R.id.startService).setOnClickListener {
            startLocalPluginService("com.chenxuan.plugin.NormalService")
        }
    }

接下來是遠程服務,寫個AIDL比較方便。但是這里順便回顧一下AIDL的知識點,手寫一個AIDL,不用自帶的工具生成代碼。

手寫AIDL

在base module先定義通信接口IPerson繼承IInterface

public interface IPerson extends IInterface {
    public String eat(String food) throws RemoteException;

    public int age(int age) throws RemoteException;

    public String name(String name) throws RemoteException;
}

然后就是熟悉的靜態內部類Stub繼承Binder實現通信接口。asInterface方法是綁定服務需要用到的,在onServiceConnected中傳入IBinder實例也就是Stub實現類,返回通信接口IPerson。這里如果是本地服務queryLocalInterface直接就取到實例進行調用,如果是遠程服務返回的是代理類Proxy,Proxy寫AIDL也是生成的,這里就繼續手寫了。

    public static abstract class Stub extends Binder implements IPerson {
        private static final String DESCRIPTOR = "com.chenxuan.base.service.ipc.IPerson.Stub";
        private static final int Transact_eat = 10050;
        private static final int Transact_age = 10051;
        private static final int Transact_name = 10052;

        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        public static IPerson asInterface(IBinder iBinder) {
            if (iBinder == null) return null;
            IInterface iInterface = iBinder.queryLocalInterface(DESCRIPTOR);
            if (iInterface instanceof IPerson) {
                return (IPerson) iInterface;
            }
            return new Proxy(iBinder);
        }
    }

代理類Proxy實現通信接口IPerson,在對應接口方法,通過構造函數傳入的IBinder實例調用transact()傳入序列化的參數、方法標識code,經過binder驅動遠程調用到服務端方法實現。這是個同步調用,等待遠程服務端處理之后返回數據,然后通過_reply取得服務端寫入的返回值。

        public static class Proxy implements IPerson {
            private final IBinder remote;

            public Proxy(IBinder remote) {
                this.remote = remote;
            }

            @Override
            public IBinder asBinder() {
                return remote;
            }

            @Override
            public String eat(String food) throws RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                String _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeString(food);
                    remote.transact(Stub.Transact_eat, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readString();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            @Override
            public int age(int age) throws RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                int _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(age);
                    remote.transact(Stub.Transact_age, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            @Override
            public String name(String name) throws RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                String _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeString(name);
                    remote.transact(Stub.Transact_name, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readString();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }

remote.transact()調用到Stub.onTransact(),根據方法標識code取出方法參數,然后調用Stub實現類對應的的接口方法,得到結果后寫入reply,所以Proxy也就是取到這個reply結果返回給客戶端。最重要的中間遠程調用過程系統幫我們實現了,這部分無需關心。

        @Override
        protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case Transact_eat: {
                    data.enforceInterface(DESCRIPTOR);
                    String _food = data.readString();
                    String _result = eat(_food);
                    reply.writeNoException();
                    reply.writeString(_result);
                    return true;
                }
                case Transact_age: {
                    data.enforceInterface(DESCRIPTOR);
                    int _age = data.readInt();
                    int _result = age(_age);
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
                case Transact_name: {
                    data.enforceInterface(DESCRIPTOR);
                    String name = data.readString();
                    String _result = name(name);
                    reply.writeNoException();
                    reply.writeString(_result);
                    return true;
                }
                default:
                    return super.onTransact(code, data, reply, flags);
            }

最后在plugin module寫插件服務PersonService

class PersonService : PluginService() {
    private val binder = Binder()

    override fun onBind(intent: Intent?): IBinder {
        context?.log("onBind")
        return binder
    }

    override fun onUnbind(intent: Intent?): Boolean {
        context?.log("onUnbind")
        return super.onUnbind(intent)
    }

    inner class Binder : IPerson.Stub() {
        override fun eat(food: String): String {
            context?.log("eat", food)
            return food
        }

        override fun age(age: Int): Int {
            context?.log("age", "" + age)
            return age
        }

        override fun name(name: String): String {
            context?.log("name", name)
            return name
        }
    }
}

onBind()方法返回Stub實現類Binder,在內部類Binder中實現接口方法,這里就是真正的遠程服務調用處了。

回到app module的MainActivity,加幾個按鈕,在方法調用處打印log測試一下。綁定服務肯定都會寫,這部分就不多費口舌了。

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val connection: ServiceConnection = object : ServiceConnection {
            override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
                Toast.makeText(this@MainActivity, "onServiceConnected", Toast.LENGTH_SHORT).show()
                iPerson = IPerson.Stub.asInterface(service)
            }

            override fun onServiceDisconnected(name: ComponentName?) {
                Toast.makeText(this@MainActivity, "onServiceDisconnected", Toast.LENGTH_SHORT)
                    .show()
            }
        }

        val unbind = findViewById<Button>(R.id.unbindService).apply {
            setOnClickListener {
                unbindService(connection)
                this.visibility = View.GONE
            }
        }

        findViewById<Button>(R.id.bindService).setOnClickListener {
            bindRemotePluginService(
                "com.chenxuan.plugin.PersonService",
                connection,
                Context.BIND_AUTO_CREATE
            )
            unbind.visibility = View.VISIBLE
        }

        findViewById<Button>(R.id.eat).setOnClickListener {
            val food = iPerson?.eat("money")
            log("eat", food)
        }
        findViewById<Button>(R.id.age).setOnClickListener {
            val age = iPerson?.age(27)
            log("age", "$age")
        }
        findViewById<Button>(R.id.name).setOnClickListener {
            val name = iPerson?.name("chenxuan")
            log("name", name)
        }
    }

看下log,嗯哼~完成了插件遠程Service的綁定流程,而且還是手寫AIDL,感覺又學到了很多。


IPC log

項目地址 Plugin

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容