自己動手寫Android外掛化框架,讓老闆對你刮目相看
歡迎大家前往騰訊雲+社群,獲取更多騰訊海量技術實踐乾貨哦~
最近在工作中接觸到了Android外掛內的開發,發現自己這種技術還缺乏最基本的瞭解,以至於在一些基本問題上浪費不少時間,如外掛Context和主工程Context的區別,許可權必須在主工程申明等,因此花了點時間瞭解了一下外掛的歷史,並寫了兩個Demo作為總結。本文旨在通過兩個例項直觀的說明外掛的實現原理以加深對外掛內開發的理解,因此不會深入探討背景和原理,程式碼也儘量專注於核心邏輯。
原理與背景
Android外掛化從技術上來說就是如何啟動未安裝的apk(主要是四大元件)裡面的類,主要問題涉及如何載入類、如何載入資源、如何管理元件生命週期。
類載入
Android對於外部的dex檔案,主要通過DexClassLoader
類載入,因此,只需要給定外掛的路徑,就可以構造對應的類載入器:
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少量系統類達到了這個目標,外掛的開發和普通工程無異,接入成本極低。
瞭解了這些原理往往還不夠,知識往往需要經過推導和實踐才能變成自己的,因此,接下來我們結合這些原理來實現一個外掛化框架,不考慮相容性和健壯性,純粹來實踐上面提及的原理。
代理實現
首先建立一個PluginManager
類來實現外掛的載入:
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實現
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地址
部分原始碼無關核心邏輯,沒有給到,目錄結構也沒有說明,詳見Demo原始碼。
總結
看著理論感覺似懂非懂,實戰發現問題其實挺多的,尤其是Hook的時機,照搬網上的文章發現根本不可行。外掛化也不是一蹴而就的,而是在已有成果的基礎上一次一次的小創新積累起來的,跟著外掛化發展的路徑自己動手實踐一遍還是能發現很多自己理解不夠深刻的地方的。
以上。
參考
此文已由作者授權騰訊雲+社群釋出,更多原文請點選
搜尋關注公眾號「雲加社群」,第一時間獲取技術乾貨,關注後回覆1024 送你一份技術課程大禮包!
海量技術實踐經驗,盡在雲加社群!