Android外掛化技術之旅 1 開篇 - 實現啟動外掛與呼叫外掛中的Activity和Service
前言
Android技術如今已很成熟了,元件化、外掛化、熱修復等等框架層出不窮,如果只停留在單純的會用框架上,技術永遠得不到成長,只有懂得其原理,能夠婉婉道來,能夠自己手動寫出,技術才會得到成長,與其焦慮未來,不如把握現在。本篇將手寫教大家寫出外掛化框架,外掛化技術是Android高階工程師必備的技術之一,懂其思想,知其原理。本篇專題將由10篇文章來詳細的講解外掛化技術,深耕一個技術領域,才能懂得如何更廣闊的橫向發展。
ofollow,noindex">本專題程式碼地址
什麼是外掛化?
外掛化技術的起源於免安裝執行APK的想法,這個免安裝的APK就是一個外掛,支援外掛化的app可以在執行時載入和執行外掛,這樣便可以將app中一些不常用的功能模組做成外掛,一方面減小了安裝包的大小,另一方面可以實現app功能的動態擴充套件。
在過去幾年有一款叫 23code 的應用,不知大家是否知道,這款應用大火在於,它可以直接下載demo並執行起來,這可以說是最早的外掛化應用。
現在並沒有什麼新的外掛化專案了,比較流行的框架有如下:
第一代:dynamic-load-apk最早使用ProxyActivity這種靜態代理技術,由ProxyActivity去控制外掛中PluginActivity的生命週期。該種方式缺點明顯,外掛中的activity必須繼承PluginActivity,開發時要小心處理context。而DroidPlugin通過Hook系統服務的方式啟動外掛中的Activity,使得開發外掛的過程和開發普通的app沒有什麼區別,但是由於hook過多系統服務,異常複雜且不夠穩定。
第二代:為了同時達到外掛開發的低侵入性(像開發普通app一樣開發外掛)和框架的穩定性,在實現原理上都是趨近於選擇儘量少的hook,並通過在manifest中預埋一些元件實現對四大元件的外掛化。另外各個框架根據其設計思想都做了不同程度的擴充套件,其中Small更是做成了一個跨平臺,元件化的開發框架。
第三代:VirtualApp比較厲害,能夠完全模擬app的執行環境,能夠實現app的免安裝執行和雙開技術。Atlas是阿里今年開源出來的一個結合元件化和熱修復技術的一個app基礎框架,其廣泛的應用與阿里系的各個app,其號稱是一個容器化框架。
外掛化的未來:去年比較火的ReactNative,儘管它不可能成為最終方案,但是移動應用Web化是一個必然的趨勢,就好像曾經的桌面應用由C/S到B/S的轉變。從Google今年推崇的Flutter,移動應用越來越趨向於Web化。
外掛化與元件化的區別?
至於什麼是元件化,大家可以看我寫的關於元件化的專題
元件化是將各個模組分成獨立的元件,每個元件都要打進apk包中。
而外掛化各個元件都可以單獨打成apk包,在需要時下載apk包,並載入執行。
外掛化原理
1 設計接納標準,符合這個標準的app都能接入進來,該標準跟生命週期相關。
2 設計好外掛遵循這個標準,由於外掛沒有安裝(上下文均不可用),需要給外掛傳遞一個上下文。
3 用一個空殼Activity 放入外掛內容。
4 反射得到Apk裡面MainActivity的全類名。
5 載入

image.png
從上圖可以看出,啟動一個差價,核心程式碼就是ProxyActivity和PluginInterfaceActivity.下面我們著重講解核心思想的程式碼。
程式碼其實很簡單,啟動外掛的過程:
- 首先需要一個空殼的ProxyActivity來啟動外掛的Activity。
- ProxyActivity根據外掛apk包的資訊,拿到外掛中的ClassLoader和Resource,然後通過反射並建立MainActivity 轉換為PluginInterfaceActivity,並呼叫其onCreate方法。
- 外掛呼叫的setContentView被重寫了,會去呼叫ProxyActivity的setContentView,由於ProxyActivity的getClassLoader和gerResource被重寫是外掛中的resource,所以ProxyActivity的setContentView能夠訪問外掛中的資源,findViewById同樣也是呼叫ProxyActivity的。
- ProxyActivity中的其他生命週期回撥函式中呼叫相應PluginActivity的生命週期。

image.png
直接上程式碼
下面程式碼定義了外掛Activity必須要實現的一個介面,也可說定義的一個標準,由於外掛並沒有安裝到手機上,無法拿到上下文,生命週期自然也無法呼叫,我們需要將宿主的一個空殼ProxyActivity,將生命週期傳遞給外掛。外掛需要用到上下文的方法都需要重寫。
public interface PluginInterfaceActivity { /** * 上下文的傳遞 通過上下文來啟動Activity * * @param activity */ void attach(Activity activity); //---------------------- 生命週期 傳遞到外掛中 -------------------------// void onCreate(@Nullable Bundle savedInstanceState); void onStart(); void onResume(); void onPause(); void onStop(); void onRestart(); void onDestroy(); void onSaveInstanceState(Bundle outState); boolean onTouchEvent(MotionEvent event); void onBackPressed(); }
重寫必須要用到的方法,外掛的Activity都需要繼承BaseActivity
public class BaseActivity extends AppCompatActivity implements PluginInterfaceActivity { protected Activity mActivity; @Override public void attach(Activity proxyActivity) { this.mActivity = proxyActivity; } @Override public void onCreate(@Nullable Bundle savedInstanceState) { } @Override public void onStart() { } @Override public void onResume() { } @Override public void onPause() { } @Override public void onStop() { } @Override public void onRestart() { } @Override public void onDestroy() { } @Override public void onSaveInstanceState(Bundle outState) { } //--------------------------凡事用到上下文的地方都需要重寫,需要重寫的方法-----------------------------// /** * 這裡我們要使用宿主傳遞過來的上下文進行載入view * * @param view */ @Override public void setContentView(View view) { if (mActivity != null) { mActivity.setContentView(view); } else { super.setContentView(view); } } @Override public void setContentView(int layoutResID) { if (mActivity != null) { mActivity.setContentView(layoutResID); } else { super.setContentView(layoutResID); } } @Override public <T extends View> T findViewById(int id) { if (mActivity != null) { return mActivity.findViewById(id); } else { return super.findViewById(id); } } @Override public Intent getIntent() { if (mActivity != null) { return mActivity.getIntent(); } return super.getIntent(); } @Override public ClassLoader getClassLoader() { if (mActivity != null) { return mActivity.getClassLoader(); } return super.getClassLoader(); } @Override public LayoutInflater getLayoutInflater() { if (mActivity != null) { return mActivity.getLayoutInflater(); } return super.getLayoutInflater(); } @Override public ApplicationInfo getApplicationInfo() { if (mActivity != null) { return mActivity.getApplicationInfo(); } return super.getApplicationInfo(); } @Override public Window getWindow() { return mActivity.getWindow(); } @Override public WindowManager getWindowManager() { return mActivity.getWindowManager(); } @Override public void startActivity(Intent intent) { if (mActivity != null) { //外掛的launchMode 只能夠是標準的 Intent intent1 = new Intent(); intent1.putExtra("className", intent.getComponent().getClassName()); mActivity.startActivity(intent1); } else { super.startActivity(intent); } } @Override public ComponentName startService(Intent service) { if (mActivity != null) { Intent m = new Intent(); m.putExtra("serviceName", service.getComponent().getClassName()); return mActivity.startService(m); } else { return super.startService(service); } } }
下面來看宿主是如何啟動外掛的呢?
通過一個空殼的Activity
ProxyActivity代理的方式最早是由dynamic-load-apk提出的,其思想很簡單,在主工程中放一個ProxyActivy,啟動外掛中的Activity時會先啟動ProxyActivity,在ProxyActivity中建立外掛Activity,並同步生命週期。
public class ProxyActivity extends AppCompatActivity { //需要載入外掛的全類名 private String className; private PluginInterfaceActivity pluginInterfaceActivity; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); className = getIntent().getStringExtra("className"); //Class.forName() //外掛並沒有被安裝到手機上的 try { //className 代表activity的全類名 Class activityClass = getClassLoader().loadClass(className); //呼叫建構函式 Constructor constructors = activityClass.getConstructor(new Class[]{}); //得到Activity物件 Object newInstance = constructors.newInstance(new Object[]{}); //最好不要反射 onCreate() //通過標準來 pluginInterfaceActivity = (PluginInterfaceActivity) newInstance; //注入上下文 pluginInterfaceActivity.attach(this); Bundle bundle = new Bundle();//將一些資訊傳遞 bundle.putString("test", "我是宿主給你傳遞資料"); pluginInterfaceActivity.onCreate(bundle); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } @Override public void startActivity(Intent intent) { String className = intent.getStringExtra("className"); Intent intent1 = new Intent(this, ProxyActivity.class); intent1.putExtra("className", className); super.startActivity(intent1); } @Override public ComponentName startService(Intent service) { String serviceName = service.getStringExtra("serviceName"); Intent intent = new Intent(this, ProxyService.class); intent.putExtra("serviceName", serviceName); return super.startService(intent); } //對外 @Override public ClassLoader getClassLoader() { return PluginManager.getInstance().getClassLoader(); } @Override public Resources getResources() { return PluginManager.getInstance().getResources(); } @Override protected void onStart() { super.onStart(); pluginInterfaceActivity.onStart(); } @Override protected void onResume() { super.onResume(); pluginInterfaceActivity.onResume(); } @Override protected void onPause() { super.onPause(); pluginInterfaceActivity.onPause(); } @Override protected void onStop() { super.onStop(); pluginInterfaceActivity.onStop(); } @Override protected void onDestroy() { super.onDestroy(); pluginInterfaceActivity.onDestroy(); } }
啟動外掛的程式碼如下:
public void loadPlugin(View view) { loadDownPlugin(); Intent intent = new Intent(this, ProxyActivity.class); //得到全類名 intent.putExtra("className", PluginManager.getInstance().getPackageInfo().activities[0].name); startActivity(intent); }
其實,啟動外掛就是啟動宿主的一個空殼的Activity,將這個空殼的Activity的上文和生命週期傳遞到外掛的Activity。
這便是啟動外掛的核心程式碼:
//className 代表activity的全類名 Class activityClass = getClassLoader().loadClass(className); //呼叫建構函式 Constructor constructors = activityClass.getConstructor(new Class[]{}); //得到Activity物件 Object newInstance = constructors.newInstance(new Object[]{}); //最好不要反射 onCreate() //通過標準來 pluginInterfaceActivity = (PluginInterfaceActivity) newInstance; //注入上下文 pluginInterfaceActivity.attach(this); Bundle bundle = new Bundle();//將一些資訊傳遞 bundle.putString("test", "我是宿主給你傳遞資料"); pluginInterfaceActivity.onCreate(bundle);
其實我們就是啟動了宿主中的一個空殼Activity,然後載入外掛APK包中的資源,並將生命週期傳遞,那麼下面我們思考一個問題:
外掛中的MainActivity呼叫外掛中的OtherActivity,是如何呼叫的呢?startActivity需不需要重寫?
答案是肯定的,啟用外掛中的其他Activity,其實就是重新建立一個新的空殼的Activity。
//重寫外掛中的startActivity 將要啟動的Activity的全類名傳遞給ProxyActivity @Override public void startActivity(Intent intent) { if (mActivity != null) { //外掛的launchMode 只能夠是標準的 Intent intent1 = new Intent(); intent1.putExtra("className", intent.getComponent().getClassName()); mActivity.startActivity(intent1); } else { super.startActivity(intent); } } //重寫ProxyActivity的startActivity,跳轉一個新的ProxyActivity,並跳轉Activity的全類名傳遞過去,相當於重走了一邊上面的過程 @Override public void startActivity(Intent intent) { String className = intent.getStringExtra("className"); Intent intent1 = new Intent(this, ProxyActivity.class); intent1.putExtra("className", className); super.startActivity(intent1); }
ProxyActivity代理方式主要注意兩點:
- ProxyActivity中需要重寫getResouces,getAssets,getClassLoader方法返回外掛的相應物件。生命週期函式以及和使用者互動相關函式,如onResume,onStop,onBackPressedon,KeyUponWindow,FocusChanged等需要轉發給外掛。
- PluginActivity中所有呼叫context的相關的方法,如setContentView,getLayoutInflater,getSystemService等都需要呼叫ProxyActivity的相應方法。
呼叫外掛中的Service
通過上述的講解,我們知道了呼叫外掛中的Activity,其實就是在宿主中建立一個空殼的Acitvity,然後載入外掛中的資源,傳遞上下文。
那麼呼叫外掛中的Service呢?原理是一樣的,原理是一樣的還是在宿主中建立一個空殼的Service ProxyService,ProxyService 將生命週期傳遞給外掛中的Service
自己可以去實現一下,這裡我只把核心程式碼給出
public class ProxyService extends Service { private String serviceName; private PluginInterfaceService pluginInterfaceService; @Override public IBinder onBind(Intent intent) { return null; } private void init(Intent intent) { //開啟某個服務的全類名 serviceName = intent.getStringExtra("serviceName"); //生成一個class DexClassLoader classLoader = PluginManager.getInstance().getClassLoader(); try { Class<?> aClass = classLoader.loadClass(serviceName); Constructor<?> constructor = aClass.getConstructor(new Class[]{}); //獲取某個service物件 Object newInstance = constructor.newInstance(new Object[]{}); pluginInterfaceService = (PluginInterfaceService) newInstance; pluginInterfaceService.attach(this); Bundle bundle = new Bundle(); pluginInterfaceService.onCreate(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } } @Override public int onStartCommand(Intent intent, int flags, int startId) { if (pluginInterfaceService == null) { init(intent); } return pluginInterfaceService.onStartCommand(intent, flags, startId); } @Override public void onStart(Intent intent, int startId) { super.onStart(intent, startId); pluginInterfaceService.onStart(intent, startId); } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); pluginInterfaceService.onConfigurationChanged(newConfig); } @Override public void onLowMemory() { super.onLowMemory(); pluginInterfaceService.onLowMemory(); } @Override public void onTrimMemory(int level) { super.onTrimMemory(level); pluginInterfaceService.onTrimMemory(level); } @Override public boolean onUnbind(Intent intent) { pluginInterfaceService.onUnbind(intent); return super.onUnbind(intent); } @Override public void onRebind(Intent intent) { super.onRebind(intent); pluginInterfaceService.onRebind(intent); } @Override public void onTaskRemoved(Intent rootIntent) { super.onTaskRemoved(rootIntent); pluginInterfaceService.onTaskRemoved(rootIntent); } @Override public void onDestroy() { super.onDestroy(); pluginInterfaceService.onDestroy(); } }
需要注意的是:ProxyActivity和ProxyService一定要在宿主的 Manifest 中註冊。
OK,這編文章的講解是外掛化最初的時候出現的設計原理,接下來會一步步深入講解。