接上篇手寫插件化,文末放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,感覺又學到了很多。