手写插件化二
接上篇手写插件化,文末放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简单点做的话多预埋一些站桩也是可以的,当然还有多进程的情况,绑定模式启动远程服务。
- 本地服务,也就是不指定进程。
先在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>
- 远程服务,上面已经注册好了
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