Android Plugin插樁式實現外掛化開發(一)-實現原理及Activity外掛化實現
1. 前言
在現在一些大型的Android應用中都採用了外掛化的開發方式,比如美團,支付寶及我們常用的微信等採用了插修的化的開發方式來進行開發的,既然國內一流的網際網路公司都採用這樣的方式來開發那它一定能帶給開發部署大型應用帶來很大的便捷,那麼外掛化的優勢在哪裡呢?
1.1 外掛化的好處
首先在大型的專案裡往往是一堆人負責一個不同功能的APP,最終於上線的專案會把這些功能整合到一起,採用外掛化開發就不用後期繁雜的整合過程,安全快捷。
其次,各個功能的APP單獨開發單獨編譯,提高開發效率。
再次,可以利用外掛化這種思想來修復Bug.
由於外掛化開發只上線宿主APP,具體的功能APP是使用者點選之後動態的從網路端載入後執行這就大大減小了上線APP的體積大小,當然這也會導致使用者第一次進入時載入變慢的情況。
外掛化開發會定義自己的插化實現標準,這就使得一個宿主APP可以載入任意實現這套標準的APK,這就讓宿主可以載入海量的外掛而不必從新開發打包。
2. 外掛化的實現方式
本文介紹的外掛化的實現原理如下:
1. 通過 DexClassLoader 載入目標APK的Class物件,主要是Activity, Service, BroadcaseReceiver, Provider等四大元件的載入。
2. 通過Resource訪問目標物件的res資源。
3. 通過反射或者手動解析AndroidManifest檔案獲取類檔名,本方是通過反射PackageParse和PackageInfo的方式來獲取的。
4. 通過代理(插莊)ProxyActivity, ProxyService, ProxyBroadcaseReceiver等方式代理目標APK裡的相應物件。
5. 通過定義介面的方式模擬各各件的生命週期方法來達到Proxy的生命週期同步管理被代理物件的生命週期,對生命週期的管理是外掛化開發的一個重點工作。
3. 外掛化實現結構及主要功能
首先先上一個大致的程式碼結構圖,如下:
整個Demo實現包括以下3個方面:
1. 首先app是專案的主app,也就是外掛化的宿主部分,是最終我們打包上線的apk,其中主要包含了PluginManager用於解析目標物件的Class和Res等資訊,ProxyActivity,ProxyService,ProxyBroadcaseReceiver等插莊代理物件,用於代理目標物件中對應的Class。
2. plugin-standard是一個依懶模組,在其中主要定義兩個東西,一個是用於管理目標被代理物件的介面,在這個介面中宣告被代理物件的生命週期方法(與明白點,就是目標被代理物件必須實現這個介面,在Proxy中通過反射得到物件後強轉成這個interface型別然後呼叫其生命週期方法來做到同步生命週期管理的目的),其次是一個PluginXXX的類,需要被代理的物件需要extends對應的類,在這個類中做一些公用的操作,如定義Context,重寫其中某些方法來達到代理的目的(比如被代理的APK沒有Context的例項,我們需要在其中注入context,然後利用注入的context進行相應的操作)。
3. plugin-target,這個就很直接了,就是一個被代理的apk,在裡面定義的一系列的Activity,Service,BroadcaseReceiver,Provider等,這些被代理的元件物件都必須依懶實現plugin-standard中相對應的定義的Base物件以達到被控制生命週期和其中某些方法被重新實現。這個在實際開發中肯定是存在多個的。
4. 主要程式碼實現
4.1. 載入外掛apk,初始化DexClassLoader和Resources及PackageInfo
實際情況是肯定是使用者在點選某一個功能的入口後從網路上去下載然後載入到宿主程序中,在這裡我們就簡單的將plugin-target打包成apk後放到sdcard中,直接讀apk檔案,根據檔案的路徑建立DexClassLoader和Resources物件,用於訪問class和res目錄下的資原始檔
public void loadPath(Context context) {
File filesDir = context.getDir("plugin", Context.MODE_PRIVATE);
String name = "plugin.apk";
String path = new File(filesDir, name).getAbsolutePath();
//獲取被代理apk的PackageInfo
PackageManager packageManager = context.getPackageManager();
packageInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
//獲取被代理apk的ClassLoader
File dexOutFile = context.getDir("dex", Context.MODE_PRIVATE);
dexClassLoader = new DexClassLoader(path, dexOutFile.getAbsolutePath()
, null, context.getClassLoader());
//獲取被代理apk的Resource
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, path);
resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
parseReceivers(context, path);
}
DexClassLoader和Resource被初始後,在代理的ProxyXXX中就可以通過PluginManager提供的get方法獲取外掛apk的ClassLoader和Resource及PackageInfo。
/**
* Get Rerource for target apk
*
* @return
*/
public Resources getResources() {
return resources;
}
/**
* Get ClassLoader for target apk
*
* @return
*/
public DexClassLoader getDexClassLoader() {
return dexClassLoader;
}
public PackageInfo getPackageInfo() {
return packageInfo;
}
4.2 Activity外掛化實現
4.2.1. 宣告介面,用面向物件的方式控制生命週期
在plugin-stand定義IPluginActivity介面,它主要定義了Activity的生命週期方法,如onCreate,onDestory等,凡是Activity的生命週期方法都需要在它裡面重新宣告一次,後面我們會把通過ClassLoader反射出來的Activity物件轉換成這個介面物件來控制它的生命週期;其次就是需要定義一個注入Context的方法public void attach(Activity proxyActivity),由於被代理的Activity並沒有通過Android真正載入,所以其實是沒有Context物件的,也就是說我們通常在開發中用的this.xxx,MainActivity.this等都會拋NullPointException,我們通過attach方法注入ProxyActivity物件到真實的Activity中,凡是要用到context都用注入的物件進行操作。
public interface IPluginActivity {
public void attach(Activity proxyActivity);
/**
* 生命週期
* @param savedInstanceState
*/
public void onCreate(Bundle savedInstanceState);
public void onStart();
public void onResume();
public void onPause();
public void onStop();
public void onDestroy();
public void onSaveInstanceState(Bundle outState);
public boolean onTouchEvent(MotionEvent event);
public void onBackPressed();
}
4.2.2. 定義PluginActivity,重寫Activity中與context(this)相關的方法
在plugin-standard中定義PluginActivity extends Activity implements IPluginActivity,外掛APP中所有的Activity都必須要繼承這個類,在這個類中實現了通過attach方法獲取ProxyActivity的Context上下文物件並儲存了類變數that,然後在android.app.Activity中的關於context的方法都必須要重寫,用傳入的這個context替換,如setContentView需要重寫成that.setContentView,其它的都類似,實現程式碼如下:
public class PluginActivity extends Activity implements IPluginActivity {
/**
* Activity context -> ProxyActivity
*/
protected Activity that;
@Override
public void attach(Activity proxyActivity) {
this.that = proxyActivity;
}
@Override
public void setContentView(View view) {
if (that != null) {
that.setContentView(view);
}else {
super.setContentView(view);
}
}
@Override
public void setContentView(int layoutResID) {
if(that != null) {
that.setContentView(layoutResID);
}else {
super.setContentView(layoutResID);
}
}
@Override
public ComponentName startService(Intent service) {
if(that != null) {
Intent m = new Intent();
m.putExtra("serviceName", service.getComponent().getClassName());
return that.startService(m);
}
return super.startService(service);
}
@Override
public View findViewById(int id) {
if(that != null) {
return that.findViewById(id);
}
return super.findViewById(id);
}
@Override
public Intent getIntent() {
if(that!=null){
return that.getIntent();
}
return super.getIntent();
}
@Override
public ClassLoader getClassLoader() {
if(that != null) {
return that.getClassLoader();
}
return super.getClassLoader();
}
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
if(that != null) {
return that.registerReceiver(receiver, filter);
}
return super.registerReceiver(receiver, filter);
}
@Override
public void sendBroadcast(Intent intent) {
if(that != null) {
that.sendBroadcast(intent);
}else {
super.sendBroadcast(intent);
}
}
@Override
public void startActivity(Intent intent) {
if(that != null) {
//ProxyActivity --->className
Intent m = new Intent();
m.putExtra("className", intent.getComponent().getClassName());
that.startActivity(m);
}else {
super.startActivity(intent);
}
}
@NonNull
@Override
public LayoutInflater getLayoutInflater() {
if(that != null) {
return that.getLayoutInflater();
}
return super.getLayoutInflater();
}
@Override
public ApplicationInfo getApplicationInfo() {
if(that != null) {
return that.getApplicationInfo();
}
return super.getApplicationInfo();
}
@Override
public Window getWindow() {
if(that != null) {
return that.getWindow();
}
return super.getWindow();
}
@Override
public WindowManager getWindowManager() {
if(that != null) {
return that.getWindowManager();
}
return super.getWindowManager();
}
@SuppressLint("MissingSuperCall")
@Override
public void onCreate(Bundle savedInstanceState) {
}
@SuppressLint("MissingSuperCall")
@Override
public void onStart() {
}
@SuppressLint("MissingSuperCall")
@Override
public void onResume() {
}
@SuppressLint("MissingSuperCall")
@Override
public void onPause() {
}
@SuppressLint("MissingSuperCall")
@Override
public void onStop() {
}
@SuppressLint("MissingSuperCall")
@Override
public void onDestroy() {
}
@SuppressLint("MissingSuperCall")
@Override
public void onSaveInstanceState(Bundle outState) {
}
@SuppressLint("MissingSuperCall")
@Override
public boolean onTouchEvent(MotionEvent event) {
return false;
}
@SuppressLint("MissingSuperCall")
@Override
public void onBackPressed() {
}
}
4.2.3. 代理ProxyActivity實現
ProxyActivity執行在宿主程序中,其實它就是一個普通的Activity元件,需要在AndroidManifest中宣告,需要載入外掛app的時候就先呼叫PluginManager.getInstance().loadPath方法將ClassLoader,Resources和PackageInfo等系統物件生成,然後直接用intent方式跳轉Activity就可以了。實現程式碼如下:
public class ProxyActivity extends Activity {
//被載入外掛的apk的Launch Activity全名
private String className;
/**
* 目標外掛apk的activity
*/
IPluginActivity mTargetActivity;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
className = getIntent().getStringExtra("className");
try {
//這裡需要用DexClassLoader,不能用Class.forName,因為目標還是java檔案沒有被編譯成class檔案
Class activityClass = getClassLoader().loadClass(className);
Constructor activityConstructor = activityClass.getConstructor(new Class[]{});
Object activityObject = activityConstructor.newInstance(new Object[]{});
mTargetActivity = (IPluginActivity) activityObject;
mTargetActivity.attach(this);
Bundle bundle = new Bundle();
mTargetActivity.onCreate(bundle);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void startActivity(Intent intent) {
String className1 = intent.getStringExtra("className");
Intent intent1 = new Intent(this, ProxyActivity.class);
intent1.putExtra("className", className1);
super.startActivity(intent1);
}
/**
* 外掛apk中呼叫startService時,相當於呼叫傳入的Activity context.startService方法,這
* 裡重寫這個方法,相當於繫結一個ProxyService代理外掛中的Service,在ProxyService的生命
* 週期方法中呼叫被代理物件的生命週期方法。
* @param service
* @return
*/
@Override
public ComponentName startService(Intent service) {
String serviceName = service.getStringExtra("serviceName");
Intent intent1 = new Intent(this, ProxyService.class);
intent1.putExtra("serviceName", serviceName);
return super.startService(intent1);
}
/**
* 外掛apk中呼叫bindService時,相當於呼叫傳入的Activity context.bindService方法,這
* 裡重寫這個方法,相當於繫結一個ProxyService代理外掛中的Service,在ProxyService的生命
* 週期方法中呼叫被代理物件的生命週期方法。
* @param service
* @param conn
* @param flags
* @return
*/
@Override
public boolean bindService(Intent service, ServiceConnection conn, int flags) {
String serviceName = service.getStringExtra("serviceName");
Intent intent1 = new Intent(this, ProxyService.class);
intent1.putExtra("serviceName", serviceName);
return super.bindService(intent1, conn, flags);
}
@Override
public ClassLoader getClassLoader() {
return PluginManager.getInstance().getDexClassLoader();
}
/**
* 外掛apk中動態註冊廣播時會呼叫傳入的Activity context.registerReceiver
* 在這裡重寫這個方法,註冊一個ProxyBroadcastReceiver代理外掛中的廣播,在
* ProxyBroadcastReceiver的生命週期方法中呼叫被代理物件的生命週期方法。
* @param receiver
* @param filter
* @return
*/
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
IntentFilter newInterFilter = new IntentFilter();
for (int i=0;i<filter.countActions();i++) {
newInterFilter.addAction(filter.getAction(i));
}
return super.registerReceiver(new ProxyBroadcastReceiver(receiver.getClass().getName(),this),newInterFilter);
}
@Override
public Resources getResources() {
return PluginManager.getInstance().getResources();
}
@Override
protected void onStart() {
super.onStart();
mTargetActivity.onStart();
}
@Override
protected void onDestroy() {
super.onDestroy();
mTargetActivity.onDestroy();
}
@Override
protected void onPause() {
super.onPause();
mTargetActivity.onPause();
}
}
其中我們重寫了getClassLoader方法,使其返回PluginManager初始化的ClassLoader,否則它返回的物件為Null,為什麼?其實就是我們這個外掛APK根本就沒有被Android載入,自然也就沒有初始化這些物件,和前面提到的Context一樣。
其次我們在onCreate方法中首先通過ClassLoader去載入目標Activity物件,通過反射的方式生成它的一個例項物件,因為這個物件前面提到過必須繼承PluginActivity,當然它肯定就是一個IPluginActivity介面物件的一個例項,所以我們把它強制轉換成IPluginActivity並儲存在類成員變數mTargetActivity中,然後然後的關鍵呼叫mTargetActivity.onCreate(bundle)方法,這個時候也就是外掛中的Activity中的生命週期方法開始呼叫,當然在呼叫onCreate方法之前需要呼叫mTargetActivity.attach(this)將this注入到外掛中的Activity中。
前面提到將外掛中的Activity物件儲存在mTargetActivity中,所以在ProxyActivity的所有生命週期方法中都必須要呼叫mTargetActivity對應的那個生命週期方法,這樣是不是就達到了生命週期轉移的目的,外掛中的Activity的生命週期就和ProxyActivity的生命週期完全一樣了。這一點是插莊實現外掛化的關鍵點-生命週期的控制。
另外,對於Activity還有一點必須要說明,在ProxyActivity中必須要重寫startActivity方法,試想一個我們在外掛中的Activity也肯定有需要再次啟動另外一個Activity,這時就會呼叫這個方法,我們的實現辦法是在Intent中傳入它自身的class,也就是說其實還是設定到它自己這個Activity,只是次傳入的className換成目標的ClassName,說到這裡還得說一點,第一次呼叫啟動外掛時傳入的是外掛apk的Launch Activity,還記得我們在PluginManager例項化了一個PackageInfo物件嗎?我們就是通過這個物件來獲取啟動的ClassName的。
其它的如startService,bindService,registerReceiver方法我們在後面介紹Service和BroacastReceiver時再說明。
4.2.4. 測試頁面跳轉及簡單的外掛apk
首先是目標apk的打包生成,這個就很簡單了就是簡單的兩個頁面就可以了.
public class MainActivity extends PluginActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.img).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(that, "Target toast show...", Toast.LENGTH_SHORT).show();
startActivity(new Intent(that, SecondActivity.class));
startService(new Intent(that, TestService.class));
}
});
}
@Override
public void onStart() {
super.onStart();
}
@Override
public void onResume() {
super.onResume();
}
@Override
public void onPause() {
super.onPause();
}
@Override
public void onStop() {
super.onStop();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
@Override
public void onDestroy() {
super.onDestroy();
}
@Override
public void onBackPressed() {
super.onBackPressed();
}
}
MainActivity為外掛apk的LaunchActivity,也就是第一個跳轉的頁面,然後其中加了一個按紐跳轉到外掛中的另一個簡單的Activity,這裡就不在多說了。
然後是宿主App的頁面中載入這個apk,將這個apk從sdcard中copy到宿主程序的data目錄下,其實就是模擬第一次點選外掛按紐從時從網路上下載這個apk並儲存到本地的過程,然後呼叫PluginManager.getInstance().loadPath(this)初始化外掛APK的ClassLoader,Resources及PackageInfo物件,最後呼叫startActivity啟動外掛,這裡需要注意,呼叫startActivity時需要傳入className為外掛的LaunchActivity通PackageInfo獲取,然後跳轉的目標Activity為ProxyActivity,最終是在ProxyActivity中去實現的目標Activity的跳轉,這點上面已說明,程式碼如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
PluginManager.getInstance().setContext(this);
}
private void loadPlugin() {
File filesDir = this.getDir("plugin", Context.MODE_PRIVATE);
String name = "plugin.apk";
String filePath = new File(filesDir, name).getAbsolutePath();
File file = new File(filePath);
if (file.exists()) {
file.delete();
}
InputStream is = null;
FileOutputStream os = null;
try {
Log.i(TAG, "載入外掛 " + new File(Environment.getExternalStorageDirectory(), name).getAbsolutePath());
is = new FileInputStream(new File(Environment.getExternalStorageDirectory(), name));
os = new FileOutputStream(filePath);
int len = 0;
byte[] buffer = new byte[1024];
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
File f = new File(filePath);
if (f.exists()) {
Toast.makeText(this, "dex overwrite", Toast.LENGTH_SHORT).show();
}
PluginManager.getInstance().loadPath(this);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if(os != null) {
os.close();
os = null;
}
if(is != null) {
is.close();
is = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
@Override
protected void onStart() {
super.onStart();
}
public void click(View view) {
Intent intent = new Intent(this, ProxyActivity.class);
intent.putExtra("className", PluginManager.getInstance().getPackageInfo().activities[0].name);
startActivity(intent);
}
}
到這裡,Activity的件件化就已經可以實現了,寫部落格其實是給自己記個筆記,方便以後自己檢視,路過的朋友有興趣可以自己嘗試。
5. 最後
本文講了插莊式外掛化實現在大致原理和一些簡單的實現程式碼,並在這個原理的基礎上實現了Activity的外掛化,本來是想在這裡把Service的外掛化也一起記錄下來,但擡頭一看篇幅已有點長了,那Service及BrodcastReceier等我們就放到後面有時間再繼續記錄,下一篇文章應該就是Service,BroadcastReceiver外掛化的實現。
很晚了,睡覺!