歡迎你們前往騰訊雲+社區,獲取更多騰訊海量技術實踐乾貨哦~android
最近在工做中接觸到了Android插件內的開發,發現本身這種技術還缺少最基本的瞭解,以致於在一些基本問題上浪費很多時間,如插件Context和主工程Context的區別,權限必須在主工程申明等,所以花了點時間瞭解了一下插件的歷史,並寫了兩個Demo做爲總結。本文旨在經過兩個實例直觀的說明插件的實現原理以加深對插件內開發的理解,所以不會深刻探討背景和原理,代碼也儘可能專一於核心邏輯。github
Android插件化從技術上來講就是如何啓動未安裝的apk(主要是四大組件)裏面的類,主要問題涉及如何加載類、如何加載資源、如何管理組件生命週期。spring
Android對於外部的dex文件,主要經過DexClassLoader
類加載,所以,只須要給定插件的路徑,就能夠構造對應的類加載器:vim
private DexClassLoader createDexClassLoader(String apkPath) { File dexOutputDir = mContext.getDir("dex", Context.MODE_PRIVATE); DexClassLoader loader = new DexClassLoader(apkPath, dexOutputDir.getAbsolutePath(), null, mContext.getClassLoader()); return loader; }
Android系統經過Resource對象加載資源,所以只須要添加資源(即apk文件)所在路徑到AssetManager
中,便可實現對插件資源的訪問。設計模式
AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod(addAssetPathMethod, String.class); addAssetPath.invoke(assetManager, apkPath); Resources pluginRes = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration()); pluginApk = new PluginApk(pluginRes); pluginApk.classLoader = createDexClassLoader(apkPath);
插件化中較爲複雜的是對生命週期的管理,其中以Activity最爲複雜。早期的dynamic-load-apk採用的是代理的方式,經過一個空殼Activity做爲代理(Proxy),系統對該Activity的回調都會映射到插件Activity,如此即可以實現經過系統來管理插件的生命週期。這種方式十分直觀,可是須要全部的插件Activity都繼承這個用做代理的PluginActivity
(Demo中的命名),侵入性強,可結合後面的例子加深理解。所以,如何避免這種侵入性成了第二代插件化框架的目標,VirtualApk經過Hook少許系統類達到了這個目標,插件的開發和普通工程無異,接入成本極低。springboot
瞭解了這些原理每每還不夠,知識每每須要通過推導和實踐才能變成本身的,所以,接下來咱們結合這些原理來實現一個插件化框架,不考慮兼容性和健壯性,純粹來實踐上面說起的原理。session
首先創建一個PluginManager
類來實現插件的加載:app
public class PluginManager { static class PluginMgrHolder { static PluginManager sManager = new PluginManager(); } private static Context mContext; Map<String, PluginApk> sMap = new HashMap<>(); public static PluginManager getInstance() { return PluginMgrHolder.sManager; } public PluginApk getPluginApk(String packageName) { return sMap.get(packageName); } public static void init(Context context) { mContext = context.getApplicationContext(); } public final void loadApk(String apkPath) { PackageInfo packageInfo = queryPackageInfo(apkPath); if (packageInfo == null || TextUtils.isEmpty(packageInfo.packageName)) { return; } // check cache PluginApk pluginApk = sMap.get(packageInfo.packageName); if (pluginApk == null) { pluginApk = createApk(apkPath); if (pluginApk != null) { pluginApk.packageInfo = packageInfo; sMap.put(packageInfo.packageName, pluginApk); } else { throw new NullPointerException("PluginApk is null"); } } } private PluginApk createApk(String apkPath) { String addAssetPathMethod = "addAssetPath"; PluginApk pluginApk = null; try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod(addAssetPathMethod, String.class); addAssetPath.invoke(assetManager, apkPath); Resources pluginRes = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration()); pluginApk = new PluginApk(pluginRes); pluginApk.classLoader = createDexClassLoader(apkPath); } catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) { e.printStackTrace(); } return pluginApk; } private PackageInfo queryPackageInfo(String apkPath) { PackageInfo packageInfo = mContext.getPackageManager().getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES); if (packageInfo == null) { return null; } return packageInfo; } private DexClassLoader createDexClassLoader(String apkPath) { File dexOutputDir = mContext.getDir("dex", Context.MODE_PRIVATE); DexClassLoader loader = new DexClassLoader(apkPath, dexOutputDir.getAbsolutePath(), null, mContext.getClassLoader()); return loader; } public void startActivity(Intent intent) { Intent pluginIntent = new Intent(mContext, ProxyActivity.class); Bundle extra = intent.getExtras(); // complicate if statement if (extra == null || !extra.containsKey(Constants.PLUGIN_CLASS_NAME) && !extra.containsKey(Constants.PACKAGE_NAME)) { try { throw new IllegalAccessException("lack class of plugin and package name"); } catch (IllegalAccessException e) { e.printStackTrace(); } } pluginIntent.putExtras(intent); pluginIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mContext.startActivity(pluginIntent); } }
PluginApk
表示一個Apk文件:框架
public class PluginApk { public PackageInfo packageInfo; public DexClassLoader classLoader; public Resources pluginRes; public PluginApk(Resources pluginRes) { this.pluginRes = pluginRes; } }
全部插件Activity都要繼承一個父類PluginActivity
:
public abstract class PluginActivity extends Activity implements Pluginable, Attachable<Activity> { public final static String TAG = PluginActivity.class.getSimpleName(); protected Activity mProxyActivity; private Resources mResources; PluginApk mPluginApk; @Override public void attach(Activity proxy, PluginApk apk) { mProxyActivity = proxy; mPluginApk = apk; mResources = apk.pluginRes; } @Override public void setContentView(int layoutResID) { mProxyActivity.setContentView(layoutResID); } @Override public void setContentView(View view) { mProxyActivity.setContentView(view); } @Override public void setContentView(View view, ViewGroup.LayoutParams params) { mProxyActivity.setContentView(view, params); } @Override public View findViewById(int id) { return mProxyActivity.findViewById(id); } @Override public Resources getResources() { return mResources; } @Override public WindowManager getWindowManager() { return mProxyActivity.getWindowManager(); } @Override public ClassLoader getClassLoader() { return mProxyActivity.getClassLoader(); } @Override public Context getApplicationContext() { return mProxyActivity.getApplicationContext(); } @Override public MenuInflater getMenuInflater() { return mProxyActivity.getMenuInflater(); } @Override public Window getWindow() { return mProxyActivity.getWindow(); } @Override public Intent getIntent() { return mProxyActivity.getIntent(); } @Override public LayoutInflater getLayoutInflater() { return mProxyActivity.getLayoutInflater(); } @Override public String getPackageName() { return mPluginApk.packageInfo.packageName; } @Override public void onCreate(Bundle bundle) { // DO NOT CALL super.onCreate(bundle) // following same VLog.log(TAG + ": onCreate"); } @Override public void onStart() { } @Override public void onResume() { } @Override public void onStop() { } @Override public void onPause() { } @Override public void onDestroy() { } }
這個類只是一個殼,系統會經過ProxyActivity
觸發對應的方法的具體實現:
public class ProxyActivity extends Activity { LifeCircleController mPluginController = new LifeCircleController(this); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mPluginController.onCreate(getIntent().getExtras()); } @Override public Resources getResources() { // construct when loading apk Resources resources = mPluginController.getResources(); return resources == null ? super.getResources() : resources; } @Override public Resources.Theme getTheme() { Resources.Theme theme = mPluginController.getTheme(); return theme == null ? super.getTheme() : theme; } @Override public AssetManager getAssets() { return mPluginController.getAssets(); } @Override protected void onStart() { super.onStart(); mPluginController.onStart(); } @Override protected void onResume() { super.onResume(); mPluginController.onResume(); } @Override protected void onStop() { super.onStop(); mPluginController.onStop(); } @Override protected void onPause() { super.onPause(); mPluginController.onPause(); } @Override protected void onDestroy() { super.onDestroy(); mPluginController.onDestroy(); } }
這個類是系統實際啓動的類,其主要邏輯由LifeCircleController
負責:
public class LifeCircleController implements Pluginable { Activity mProxy; PluginActivity mPlugin; Resources mResources; Resources.Theme mTheme; PluginApk mPluginApk; String mPluginClazz; public LifeCircleController(Activity activity) { mProxy = activity; } public void onCreate(Bundle bundle) { mPluginClazz = bundle.getString(Constants.PLUGIN_CLASS_NAME); String packageName = bundle.getString(Constants.PACKAGE_NAME); mPluginApk = PluginManager.getInstance().getPluginApk(packageName); try { mPlugin = (PluginActivity) loadPluginable(mPluginApk.classLoader, mPluginClazz); mPlugin.attach(mProxy, mPluginApk); mResources = mPluginApk.pluginRes; mPlugin.onCreate(bundle); } catch (Exception e) { VLog.log("Fail in LifeCircleController onCreate"); VLog.log(e.getMessage()); e.printStackTrace(); } } private Object loadPluginable(ClassLoader classLoader, String pluginActivityClass) throws Exception { Class<?> pluginClz = classLoader.loadClass(pluginActivityClass); Constructor<?> constructor = pluginClz.getConstructor(new Class[] {}); constructor.setAccessible(true); return constructor.newInstance(new Object[] {}); } @Override public void onStart() { if (mPlugin != null) { mPlugin.onStart(); } } @Override public void onResume() { if (mPlugin != null) { mPlugin.onResume(); } } @Override public void onStop() { mPlugin.onStop(); } @Override public void onPause() { mPlugin.onPause(); } @Override public void onDestroy() { mPlugin.onDestroy(); } public Resources getResources() { return mResources; } public Resources.Theme getTheme() { return mTheme; } public AssetManager getAssets() { return mResources.getAssets(); } }
有點像Activity源碼的外觀模式,內部的分工和職責劃分對於使用者是不可見的。
最後在主工程啓動插件:
Intent intent = new Intent(); intent.putExtra(Constants.PACKAGE_NAME, PLUGIN_PACKAGE_NAME); intent.putExtra(Constants.PLUGIN_CLASS_NAME, PLUGIN_CLAZZ_NAME); mPluginManager.startActivity(intent);
插件Activity以下:
public class MainActivity extends PluginActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); setTitle("Plugin App"); ((ImageView) findViewById(R.id.iv_logo)).setImageResource(R.drawable.android); } }
效果:
Hook的方式須要基本瞭解系統啓動一個Activity的過程,通常來講系統會先檢查Activity是否註冊,而後再去生成該Activity,那麼咱們只須要在檢查的時候用一個已經註冊的Activity(樁,一般表示爲StubActivity)來給系統檢查,檢查經過後在生成的時候再替換成插件的就能夠了。
首先要本身實現一個Instrumentation
,在裏面作一些替換工做,而後去Hook掉系統持有的對象:
public class HookedInstrumentation extends Instrumentation implements Handler.Callback { public static final String TAG = "HookedInstrumentation"; protected Instrumentation mBase; private PluginManager mPluginManager; public HookedInstrumentation(Instrumentation base, PluginManager pluginManager) { mBase = base; mPluginManager = pluginManager; } /** * 覆蓋掉原始Instrumentation類的對應方法,用於插件內部跳轉Activity時適配 * * @Override */ public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { if (Constants.DEBUG) Log.e(TAG, "execStartActivity"); mPluginManager.hookToStubActivity(intent); try { Method execStartActivity = Instrumentation.class.getDeclaredMethod( "execStartActivity", Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class); execStartActivity.setAccessible(true); return (ActivityResult) execStartActivity.invoke(mBase, who, contextThread, token, target, intent, requestCode, options); } catch (Exception e) { e.printStackTrace(); throw new RuntimeException("do not support!!!" + e.getMessage()); } } @Override public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException { if (Constants.DEBUG) Log.e(TAG, "newActivity"); if (mPluginManager.hookToPluginActivity(intent)) { String targetClassName = intent.getComponent().getClassName(); PluginApp pluginApp = mPluginManager.getLoadedPluginApk(); Activity activity = mBase.newActivity(pluginApp.mClassLoader, targetClassName, intent); activity.setIntent(intent); ReflectUtil.setField(ContextThemeWrapper.class, activity, Constants.FIELD_RESOURCES, pluginApp.mResources); return activity; } if (Constants.DEBUG) Log.e(TAG, "super.newActivity(...)"); return super.newActivity(cl, className, intent); } @Override public boolean handleMessage(Message message) { if (Constants.DEBUG) Log.e(TAG, "handleMessage"); return false; } @Override public void callActivityOnCreate(Activity activity, Bundle icicle) { if (Constants.DEBUG) Log.e(TAG, "callActivityOnCreate"); super.callActivityOnCreate(activity, icicle); } }
在負責啓動的execStartActivity
設置爲啓動已註冊的Activity,再在newActivity
設置爲實際要啓動的插件的Activity。而後去Hook系統持有的該字段:
public class ReflectUtil { public static final String METHOD_currentActivityThread = "currentActivityThread"; public static final String CLASS_ActivityThread = "android.app.ActivityThread"; public static final String FIELD_mInstrumentation = "mInstrumentation"; public static final String TAG = "ReflectUtil"; private static Instrumentation sInstrumentation; private static Instrumentation sActivityInstrumentation; private static Field sActivityThreadInstrumentationField; private static Field sActivityInstrumentationField; private static Object sActivityThread; public static boolean init() { //獲取當前的ActivityThread對象 Class<?> activityThreadClass = null; try { activityThreadClass = Class.forName(CLASS_ActivityThread); Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod(METHOD_currentActivityThread); currentActivityThreadMethod.setAccessible(true); Object currentActivityThread = currentActivityThreadMethod.invoke(null); //拿到在ActivityThread類裏面的原始mInstrumentation對象 Field instrumentationField = activityThreadClass.getDeclaredField(FIELD_mInstrumentation); instrumentationField.setAccessible(true); sActivityThreadInstrumentationField = instrumentationField; sInstrumentation = (Instrumentation) instrumentationField.get(currentActivityThread); sActivityThread = currentActivityThread; sActivityInstrumentationField = Activity.class.getDeclaredField(FIELD_mInstrumentation); sActivityInstrumentationField.setAccessible(true); return true; } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException | NoSuchFieldException e) { e.printStackTrace(); } return false; } public static Instrumentation getInstrumentation() { return sInstrumentation; } public static Object getActivityThread() { return sActivityThread; } public static void setInstrumentation(Object activityThread, HookedInstrumentation hookedInstrumentation) { try { sActivityThreadInstrumentationField.set(activityThread, hookedInstrumentation); if (Constants.DEBUG) Log.e(TAG, "set hooked instrumentation"); } catch (IllegalAccessException e) { e.printStackTrace(); } } public static void setActivityInstrumentation(Activity activity, PluginManager manager) { try { sActivityInstrumentation = (Instrumentation) sActivityInstrumentationField.get(activity); HookedInstrumentation instrumentation = new HookedInstrumentation(sActivityInstrumentation, manager); sActivityInstrumentationField.set(activity, instrumentation); if (Constants.DEBUG) Log.e(TAG, "set activity hooked instrumentation"); } catch (IllegalAccessException e) { e.printStackTrace(); } } public static void setField(Class clazz, Object target, String field, Object object) { try { Field f = clazz.getDeclaredField(field); f.setAccessible(true); f.set(target, object); } catch (Exception e) { e.printStackTrace(); } } }
PluginManager
一樣負責加載插件的類和資源等:
public class PluginManager { private final static String TAG = "PluginManager"; private static PluginManager sInstance; private Context mContext; private PluginApp mPluginApp; public static PluginManager getInstance(Context context) { if (sInstance == null && context != null) { sInstance = new PluginManager(context); } return sInstance; } private PluginManager(Context context) { mContext = context; } public void hookInstrumentation() { try { Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(); final HookedInstrumentation instrumentation = new HookedInstrumentation(baseInstrumentation, this); Object activityThread = ReflectUtil.getActivityThread(); ReflectUtil.setInstrumentation(activityThread, instrumentation); } catch (Exception e) { e.printStackTrace(); } } public void hookCurrentActivityInstrumentation(Activity activity) { ReflectUtil.setActivityInstrumentation(activity, sInstance); } public void hookToStubActivity(Intent intent) { if (Constants.DEBUG) Log.e(TAG, "hookToStubActivity"); if (intent == null || intent.getComponent() == null) { return; } String targetPackageName = intent.getComponent().getPackageName(); String targetClassName = intent.getComponent().getClassName(); if (mContext != null && !mContext.getPackageName().equals(targetPackageName) && isPluginLoaded(targetPackageName)) { if (Constants.DEBUG) Log.e(TAG, "hook " + targetClassName + " to " + Constants.STUB_ACTIVITY); intent.setClassName(Constants.STUB_PACKAGE, Constants.STUB_ACTIVITY); intent.putExtra(Constants.KEY_IS_PLUGIN, true); intent.putExtra(Constants.KEY_PACKAGE, targetPackageName); intent.putExtra(Constants.KEY_ACTIVITY, targetClassName); } } public boolean hookToPluginActivity(Intent intent) { if (Constants.DEBUG) Log.e(TAG, "hookToPluginActivity"); if (intent.getBooleanExtra(Constants.KEY_IS_PLUGIN, false)) { String pkg = intent.getStringExtra(Constants.KEY_PACKAGE); String activity = intent.getStringExtra(Constants.KEY_ACTIVITY); if (Constants.DEBUG) Log.e(TAG, "hook " + intent.getComponent().getClassName() + " to " + activity); intent.setClassName(pkg, activity); return true; } return false; } private boolean isPluginLoaded(String packageName) { // TODO 檢查packageNmae是否匹配 return mPluginApp != null; } public PluginApp loadPluginApk(String apkPath) { String addAssetPathMethod = "addAssetPath"; PluginApp pluginApp = null; try { AssetManager assetManager = AssetManager.class.newInstance(); Method addAssetPath = assetManager.getClass().getMethod(addAssetPathMethod, String.class); addAssetPath.invoke(assetManager, apkPath); Resources pluginRes = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration()); pluginApp = new PluginApp(pluginRes); pluginApp.mClassLoader = createDexClassLoader(apkPath); } catch (IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) { e.printStackTrace(); } return pluginApp; } private DexClassLoader createDexClassLoader(String apkPath) { File dexOutputDir = mContext.getDir("dex", Context.MODE_PRIVATE); return new DexClassLoader(apkPath, dexOutputDir.getAbsolutePath(), null, mContext.getClassLoader()); } public boolean loadPlugin(String apkPath) { File apk = new File(apkPath); if (!apk.exists()) { return false; } mPluginApp = loadPluginApk(apkPath); return mPluginApp != null; } public PluginApp getLoadedPluginApk() { return mPluginApp; } }
在MainActivity中初始化,注意Hook的時機:
public class MainActivity extends Activity implements View.OnClickListener { // https://zhuanlan.zhihu.com/p/33017826 public static final boolean DEBUG = true; public static final String TAG = "MainActivity"; private String mPluginPackageName = "top.vimerzhao.image"; private String mPluginClassName = "top.vimerzhao.image.MainActivity"; //讀寫權限 private static String[] PERMISSIONS_STORAGE = {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}; //請求狀態碼 private static int REQUEST_PERMISSION_CODE = 1; private PluginManager mPluginManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initData(); initView(); initPlugin(); } private void initPlugin() { // !! must first ReflectUtil.init(); mPluginManager = PluginManager.getInstance(getApplicationContext()); mPluginManager.hookInstrumentation(); mPluginManager.hookCurrentActivityInstrumentation(this); } private void initData() { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_PERMISSION_CODE); } } } private void initView() { (findViewById(R.id.tv_launch)).setOnClickListener(this); } @Override protected void attachBaseContext(Context newBase) { super.attachBaseContext(newBase); // !!! 不要在此Hook,看源碼發現mInstrumentaion會在此方法後初始化 } @Override public void onClick(View view) { if (Constants.DEBUG) Log.e(TAG, "click view id: " + view.getId()); if (view.getId() == R.id.tv_launch) { // TODO launch plugin app if (mPluginManager.loadPlugin(Constants.PLUGIN_PATH)) { Intent intent = new Intent(); intent.setClassName(mPluginPackageName, mPluginClassName); startActivity(intent); } } } }
如今插件Activity不會有任何限制:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override protected void onResume() { super.onResume(); } }
效果和上圖相似。
部分源碼無關核心邏輯,沒有給到,目錄結構也沒有說明,詳見Demo源碼。
看着理論感受似懂非懂,實戰發現問題其實挺多的,尤爲是Hook的時機,照搬網上的文章發現根本不可行。插件化也不是一蹴而就的,而是在已有成果的基礎上一次一次的小創新積累起來的,跟着插件化發展的路徑本身動手實踐一遍仍是能發現不少本身理解不夠深入的地方的。
以上。
相關閱讀
得到InputStream,讀取配置文件的方式
聊聊springboot session timeout參數設置
Android開發之漫漫長途 XII——Fragment詳解
【每日課程推薦】機器學習實戰!快速入門在線廣告業務及CTR相應知識
此文已由做者受權騰訊雲+社區發佈,更多原文請點擊
搜索關注公衆號「雲加社區」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!
海量技術實踐經驗,盡在雲加社區!