手写插件化二

接上篇手写插件化,文末放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阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352

推荐阅读更多精彩内容