插樁式插件化跳轉
什麼是插件化
所謂插件化 就是將app分爲一個宿主apk和多個插件apk,由宿主apk啓動插件apk,在宿主apk中實現不安裝插件apk也可完成之間活動跳轉,發送廣播,啓動服務等。舉個例子:比如 支付寶內接了海量的應用,如 ofo小黃車,餓了麼等等,首先我們知道他們不可能是在一個項目裏完成開發,海量的應用,維護起來簡直是個噩夢。支付寶就是一個宿主app,內接的海量應用就是插件apk,當我們在支付寶內點擊餓了麼圖標時,就是先從服務器上下載下餓了麼的apk,然後由支付寶調用餓了麼插件apk的首頁,完成跳轉到另外一個apk。
插件化分爲幾種
插件化 原理上可分爲:插樁式 和 Hook式插樁式原理
- 由宿主apk訂製一套規則,宿主和插件共同遵守,只有按規則開發才能接入到宿主apk內
- 宿主context語法
- 瞞天過海式代理跳轉
下面 一個個解讀:
1.宿主app訂製的規則是什麼,按什麼規則開發:
項目中有兩個app,一個支付寶app(宿主app),另外一個爲餓了麼app(插件app),還有一個制定的標準lib,兩個app都需要依賴於這個標準,此lib中有三個接口,分別爲Acticity,廣播,服務 定製的標準接口,以activity的標準爲例
public interface PayInterfaceActivity {
/**
* 生命週期的方法
*/
public void attach(Activity proxyActivity);
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();
}
兩個apk會通過這些接口來進行調用或傳值,以實現接口的規則進行開發,後面會有詳細代碼說明。
- 宿主context語法是什麼?
此時你會不會想到 既然我是從一個宿主app內跳轉到另外一個沒有安裝的插件apk時,那插件內部還能否繼續調用自身的Context上下文?答案肯定是不能的,因爲插件apk都沒有進行安裝,也沒有運行,肯定是拿不到自身上下文的,那麼插件app中要用到上下文怎麼辦?答案是用宿主傳過來的Context。
public class BaseActivity extends Activity implements PayInterfaceActivity {
//這是插件的基類 在插件的基類中實現訂製標準的接口 在attach中將從宿主app內傳過來的上下文進行賦值
//然後重寫那些用到上下文的方法,將上下文改爲宿主的上下文對象,此即爲Context語法
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) {
that.setContentView(layoutResID);
}
@Override
public ComponentName startService(Intent service) {
Intent m = new Intent();
m.putExtra("serviceName", service.getComponent().getClassName());
return that.startService(m);
}
@Override
public View findViewById(int id) {
return that.findViewById(id);
}
@Override
public Intent getIntent() {
if(that!=null){
return that.getIntent();
}
return super.getIntent();
}
@Override
public ClassLoader getClassLoader() {
return that.getClassLoader();
}
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
return that.registerReceiver(receiver, filter);
}
@Override
public void sendBroadcast(Intent intent) {
that.sendBroadcast(intent);
}
@Override
public void startActivity(Intent intent) {
// ProxyActivity --->className
Intent m = new Intent();
m.putExtra("className", intent.getComponent().getClassName());
that.startActivity(m);
}
@NonNull
@Override
public LayoutInflater getLayoutInflater() {
return that.getLayoutInflater();
}
@Override
public ApplicationInfo getApplicationInfo() {
return that.getApplicationInfo();
}
@Override
public Window getWindow() {
return that.getWindow();
}
@Override
public WindowManager getWindowManager() {
return that.getWindowManager();
}
@Override
public void onCreate(Bundle savedInstanceState) {
}
@Override
public void onStart() {
}
@Override
public void onResume() {
}
@Override
public void onPause() {
}
@Override
public void onStop() {
}
@Override
public void onDestroy() {
}
@Override
public void onSaveInstanceState(Bundle outState) {
}
}
- 重頭戲:瞞天過海式代理跳轉
3.1 首先在宿主app內將插件的app加載到宿主app的目錄中去,在跳轉前一定要先完成加載,宿主apk可放在服務器上,在點擊跳轉前從服務器下載apk到本地。
private void loadPlugin() {
File filesDir = this.getDir("plugin", Context.MODE_PRIVATE);
String name = "pluginb.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) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
try {
os.close();
is.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
3.2 跳轉到插件的MainActivity
public void click(View view) {
Intent intent = new Intent(this, ProxyActivity.class);
intent.putExtra("className", PluginManager.getInstance().getPackageInfo().activities[0].name);
startActivity(intent);
}
咦,咋一看,這也不對啊,你這是騙我啊,說好得跳轉到MainActivity,怎麼一言不合就跳轉到本地的ProxyActivity中去了,別慌,首先看名字,ProxyActivity 代理activity的意思,作爲跳轉插件apk的代理app,一定要在xml文件中註冊,否則 會報錯,一會看ProxyActivity中具體做了什麼,先來看看 intent.putExtra中以className爲鍵,以PluginManager.getInstance().getPackageInfo().activities[0].name)爲值,這個值傳了什麼,首先先看最重要的類PluginManager。
public class PluginManager {
private PackageInfo packageInfo;
private Resources resources;
private Context context;
private DexClassLoader dexClassLoader;//首先理解DexClassLoader和PathClassLoader的區別
//簡要來說DexClassLoader加載任意路徑的類,PathclassLoader只能加載本地自己應用的類
private static final PluginManager ourInstance = new PluginManager();
public static PluginManager getInstance() {
return ourInstance;
}
private PluginManager() {
}
public void setContext(Context context) {
this.context = context;
}
public PackageInfo getPackageInfo() {
return packageInfo;
}
public void loadPath(Context context) {
//找到插件apk的文件 用PackageManager管理器獲取PackageInfo,PackageInfo內有我們想要的插件apk的包名,類名等信息。
File filesDir = context.getDir("plugin", Context.MODE_PRIVATE);
String name = "pluginb.apk";
String path = new File(filesDir, name).getAbsolutePath();
PackageManager packageManager=context.getPackageManager();
packageInfo=packageManager.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES);
//根據插件apk的路徑,加載路徑,緩存路徑,和本地類加載器 生成一個插件的類加載器,通過類加載器加載插件中的類
File dexOutFile = context.getDir("dex", Context.MODE_PRIVATE);
dexClassLoader = new DexClassLoader(path, dexOutFile.getAbsolutePath()
, null, context.getClassLoader());
try {
//通過反射獲取插件中resources 資源,包括佈局文件等
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();
}
}
public Resources getResources() {
return resources;
}
public DexClassLoader getDexClassLoader() {
return dexClassLoader;
}
}
回過頭來再繼續看
intent.putExtra("className", PluginManager.getInstance().getPackageInfo().activities[0].name);
packageInfo.activities[0].name返回的就是插件的第一個activity的全路徑,即com.test.eleme.MainActivity,將類全路徑傳到代理ProxyActivity中去,用ProxyActivity來欺騙AMS,通過AMS檢查,接着看最後一個關鍵點,代理類ProxyActivity中做了什麼:
public class ProxyActivity extends Activity {
private String className;
PayInterfaceActivity payInterfaceActivity;
@Override
public void onCreate(@Nullable Bundle savedInstanceState ) {
super.onCreate(savedInstanceState );
//1.獲取到從上一個活動傳進來的啓動插件的類全名
className = getIntent().getStringExtra("className");
try {
//2.用類加載器創建出MainActivity的類
Class activityClass = getClassLoader().loadClass(className);
Constructor constructor = activityClass.getConstructor(new Class[]{});
Object instance= constructor.newInstance(new Object[]{});
//到此 instance已經是MainActivity的類對象了
//將MainActivity強轉成訂製的activity標準
payInterfaceActivity = (PayInterfaceActivity) instance;
//將上下文對象傳入到插件中去,
payInterfaceActivity.attach(this);
//此處可以對插件Activity進行傳值
Bundle bundle = new Bundle();
//依次調用插件activity生命週期的方法 完成啓動
payInterfaceActivity.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);
}
@Override
public ClassLoader getClassLoader() {
return PluginManager.getInstance().getDexClassLoader();
}
@Override
public Resources getResources() {
return PluginManager.getInstance().getResources();
}
@Override
protected void onStart() {
super.onStart();
payInterfaceActivity.onStart();
}
@Override
protected void onDestroy() {
super.onDestroy();
payInterfaceActivity.onDestroy();
}
@Override
protected void onPause() {
super.onPause();
payInterfaceActivity.onPause();
}
}
總結:到此,插樁式插件化Activity跳轉已經完成,首先雙方需要共同遵守一套標準的接口,由接口進行傳值和調用,用類加載器實現加載插件中的類,用反射的方法調用插件中的資源,插件中因爲沒有自己的上下文對象,所以凡是用到Context上下文對象時,都要調用從宿主app中傳進來的Context對象。