Android外掛化原理和實踐 (七) 之 專案實踐
我們在前面一系列文章中已經介紹完了外掛化原理以及三個根本問題的解決方案,本文主要就是作為前面幾篇文章的一個總結,通過專案實踐將前面的知識點串起來使完成一個入門級簡單的外掛化工程以及在實際外掛化開發中遇到的一些總結。
實踐
我們先通過Android Studio建立一個工程,工程中包括了兩個Application模組,分別是宿主Host和外掛PlugIn。工程結構如下左圖,我們的目標就是在宿主中通過前面文章介紹的知識點,將外掛中的程式碼和資源都合併到宿主去,並且要在宿主中通過動態替換和靜態代理兩種方式來啟動外掛中的兩個Activity。最終app啟動後如下右圖。
第一步建立宿主的Application:
public class HostApplication extends Application { private static Context sContext; @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); sContext = base; // 這是測試程式碼,只為模擬下載外掛過程 Utile.simulationDownload(base); // 載入外掛 PluginManager.load(base); // Hook Activity ActivityHookHelper.hookActivity(); } public static Context getContext() { return sContext; } }
在Application的啟動後,第一時間是下載外掛和載入外掛。
第二步是下載外掛,這裡為了方便演示,就採用了模擬下載邏輯,先將已經編好的外掛PlugIn-debug.apk檔案放置於宿主中的assets中,然後通過程式碼將其拷貝到files目錄下,程式碼如下:
public class Utile { /** * 模擬下載,實際上是將Assets中的外掛apk檔案複製到/files 目錄下 * * @param context */ public static void simulationDownload(Context context) { String sourceName = PluginManager.PLUGIN_NAME; AssetManager am = context.getAssets(); InputStream is = null; FileOutputStream fos = null; try { is = am.open(sourceName); File extractFile = context.getFileStreamPath(sourceName); fos = new FileOutputStream(extractFile); byte[] buffer = new byte[1024]; int count = 0; while ((count = is.read(buffer)) > 0) { fos.write(buffer, 0, count); } fos.flush(); } catch (IOException e) { e.printStackTrace(); } finally { try { if (is != null) { is.close(); } } catch (IOException e) { e.printStackTrace(); } try { if (fos != null) { fos.close(); } } catch (IOException e) { e.printStackTrace(); } } } }
第三步是載入外掛,這一步非常重要,外掛的dex程式碼合併和資源的合併都在這裡完成,請看程式碼:
public class PluginManager {
public final static String PLUGIN_NAME = "PlugIn-debug.apk";
private static Resources sResources;
/**
* 外掛載入入口
* @param applicationBaseContext
*/
public static void load(Context applicationBaseContext) {
try {
mergePluginDex(applicationBaseContext, PLUGIN_NAME);
Resources newResources = mergePluginResources(applicationBaseContext, PLUGIN_NAME);
initPlugIn(applicationBaseContext, newResources);
sResources = newResources;
} catch (Exception e) {
e.printStackTrace();
}
}
public static Resources getResources() {
return sResources;
}
/**
* 合併外掛中的程式碼
* @param context
* @param apkName
* @throws IllegalAccessException
* @throws NoSuchMethodException
* @throws IOException
* @throws InvocationTargetException
* @throws InstantiationException
* @throws NoSuchFieldException
*/
private static void mergePluginDex(Context context, String apkName)
throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
ClassLoader pathClassLoaderClass = context.getClassLoader();
// 獲取 PathClassLoader(BaseDexClassLoader) 的 DexPathList 物件變數 pathList
Class baseDexClassLoaderClass = BaseDexClassLoader.class;
Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathListObj = pathListField.get(pathClassLoaderClass);
// 獲取 DexPathList 的 Element[] 物件變數 dexElements
Class dexPathListClass = pathListObj.getClass();
Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
dexElementsField.setAccessible(true);
Object[] dexElementListObj = (Object[]) dexElementsField.get(pathListObj);
// 獲得 Element 型別
Class<?> elementClass = dexElementListObj.getClass().getComponentType();
// 建立一個新的Element[], 將用於替換原始的陣列
Object[] newElementListObj = (Object[]) Array.newInstance(elementClass, dexElementListObj.length + 1);
Object pluginElementObj = null;
File apkFile = context.getFileStreamPath(apkName);
File optDexFile = context.getFileStreamPath(apkName.replace(".apk", ".dex"));
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// 構造外掛的 Element,建構函式引數:(File file, boolean isDirectory, File zip, DexFile dexFile)
Class[] paramClass = {File.class, boolean.class, File.class, DexFile.class};
Object[] paramValue = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};
Constructor elementCtor = elementClass.getDeclaredConstructor(paramClass);
elementCtor.setAccessible(true);
pluginElementObj = elementCtor.newInstance(paramValue);
} else {
// 構造外掛的 Element,建構函式引數:(DexFile dexFile, File file)
Class[] paramClass = {DexFile.class, File.class};
Object[] paramValue = {DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0), apkFile};
Constructor elementCtor = elementClass.getDeclaredConstructor(paramClass);
elementCtor.setAccessible(true);
pluginElementObj = elementCtor.newInstance(paramValue);
}
Object[] pluginElementListObj = new Object[]{pluginElementObj};
// 把原來 PathClassLoader 中的 elements 複製進去新的Element[]中
System.arraycopy(dexElementListObj, 0, newElementListObj, 0, dexElementListObj.length);
// 把外掛的 element 複製進去新的 Element[] 中
System.arraycopy(pluginElementListObj, 0, newElementListObj, dexElementListObj.length, pluginElementListObj.length);
// 替換原來 PathClassLoader 中的 dexElements 值
Field field = pathListObj.getClass().getDeclaredField("dexElements");
field.setAccessible(true);
field.set(pathListObj, newElementListObj);
}
/**
* 合併外掛中的資源
* @param applicationBaseContext
* @param apkName
* @return 合併後新的資源物件
* @throws IllegalAccessException
* @throws InstantiationException
* @throws NoSuchMethodException
* @throws InvocationTargetException
* @throws NoSuchFieldException
*/
private static Resources mergePluginResources(Context applicationBaseContext, String apkName)
throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
// 建立一個新的 AssetManager 物件
AssetManager newAssetManagerObj = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
// 塞入原來宿主的資源
addAssetPath.invoke(newAssetManagerObj, applicationBaseContext.getPackageResourcePath());
// 塞入外掛的資源
File optDexFile = applicationBaseContext.getFileStreamPath(apkName);
addAssetPath.invoke(newAssetManagerObj, optDexFile.getAbsolutePath());
// ----------------------------------------------
// 建立一個新的 Resources 物件
Resources newResourcesObj = new Resources(newAssetManagerObj,
applicationBaseContext.getResources().getDisplayMetrics(),
applicationBaseContext.getResources().getConfiguration());
// ----------------------------------------------
// 獲取 ContextImpl 中的 Resources 型別的 mResources 變數,並替換它的值為新的 Resources 物件
Field resourcesField = applicationBaseContext.getClass().getDeclaredField("mResources");
resourcesField.setAccessible(true);
resourcesField.set(applicationBaseContext, newResourcesObj);
// ----------------------------------------------
// 獲取 ContextImpl 中的 LoadedApk 型別的 mPackageInfo 變數
Field packageInfoField = applicationBaseContext.getClass().getDeclaredField("mPackageInfo");
packageInfoField.setAccessible(true);
Object packageInfoObj = packageInfoField.get(applicationBaseContext);
// 獲取 mPackageInfo 變數物件中類的 Resources 型別的 mResources 變數,,並替換它的值為新的 Resources 物件
// 注意:這是最主要的需要替換的,如果不需要支援外掛執行時更新,只留這一個就可以了
Field resourcesField2 = packageInfoObj.getClass().getDeclaredField("mResources");
resourcesField2.setAccessible(true);
resourcesField2.set(packageInfoObj, newResourcesObj);
// ----------------------------------------------
// 獲取 ContextImpl 中的 Resources.Theme 型別的 mTheme 變數,並至空它
// 注意:清理mTheme物件,否則通過inflate方式載入資源會報錯, 如果是activity動態載入外掛,則需要把activity的mTheme物件也設定為null
Field themeField = applicationBaseContext.getClass().getDeclaredField("mTheme");
themeField.setAccessible(true);
themeField.set(applicationBaseContext, null);
return newResourcesObj;
}
/**
* 初始化外掛
* @param applicationBaseContext
* @param resources
* @throws ClassNotFoundException
*/
private static void initPlugIn(Context applicationBaseContext, Resources resources)
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
Class plugInApplicationClass = Class.forName("com.zyx.plugin.PlugInApplication");
Class[] paramClass = {Context.class, Resources.class};
Method initMethod = plugInApplicationClass.getMethod("init", paramClass);
initMethod.setAccessible(true);
Object[] paramValue = {applicationBaseContext, resources};
initMethod.invoke(null, paramValue);
}
}
程式碼中,mergePluginDex方法就是處理合並dex,而mergePluginResources方法就是處理合並資源並返回了合併後的資源物件。這兩處詳細介紹可見《Android外掛化原理和實踐 (三) 之 載入外掛中的元件程式碼》和《Android外掛化原理和實踐 (四) 之 合併外掛中的資源》。待這兩處邏輯處理好後,就是處理初始化外掛,使用了initPlugIn方法,將宿主的Application的Context和合並後的資源物件通過反射傳遞到外掛中。因為外掛中是不能存在自己的Application的和如果使用動態替換方式啟動Activity的話,Activity的getResources方法必須是要返回合併後的資源物件。
第四步執行HookActivity,讓動態替換Activity方案生效,詳細可見《Android外掛化原理和實踐 (六) 之 四大元件解決方案》。
public class ActivityHookHelper {
private static final String EXTRA_TARGET_INTENT = "extra_target_intent";
public static void hookActivity() {
try {
hookAMN();
hookActivityThread();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 第一處Hook地方,Hook ActivityManagerNative中通過getDefault方法獲得的物件,使其在呼叫startActivity時替換成替身Activity,以達到欺騙ActivityManagerSerfvice的目的
* @throws ClassNotFoundException
* @throws IllegalAccessException
* @throws NoSuchFieldException
*/
private static void hookAMN() throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException {
Object gDefaultObj = null;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
// 獲取 ActivityManagerNative 的 gDefault
Class activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
gDefaultField.setAccessible(true);
gDefaultObj = gDefaultField.get(null);
} else {
// 獲取 ActivityManager 的 IActivityManagerSingleton
Class activityManagerNativeClass = Class.forName("android.app.ActivityManager");
Field gDefaultField = activityManagerNativeClass.getDeclaredField("IActivityManagerSingleton");
gDefaultField.setAccessible(true);
gDefaultObj = gDefaultField.get(null);
}
// 獲取 gDefault 對應在 android.util.Singleton<T> 的單例物件 mInstance
Class singletonClass = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
final Object mInstanceObj = mInstanceField.get(gDefaultObj);
// 建立 gDefault 的代理
Class<?> classInterface = Class.forName("android.app.IActivityManager");
Object proxy = Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class<?>[] { classInterface },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 只 Hook startActivity 一個方法
if (!"startActivity".equals(method.getName())) {
return method.invoke(mInstanceObj, args);
}
// 找到引數裡面的Intent 物件
int index = 0;
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
Intent targetIntent = (Intent) args[index];
// 判斷宿主沒有宣告的Activity才走替換流程
boolean isExistActivity = isExistActivity(targetIntent.getComponent().getClassName());
if (isExistActivity) {
return method.invoke(mInstanceObj, args);
}
String stubPackage = targetIntent.getComponent().getPackageName();
Intent newIntent = new Intent();
newIntent.setComponent(new ComponentName(stubPackage, SubActivity.class.getName()));
// 把原來要啟動的目標Activity先存起來
newIntent.putExtra(EXTRA_TARGET_INTENT, targetIntent);
// 替換掉Intent, 即欺騙AMS要啟動的是替身Activity
args[index] = newIntent;
return method.invoke(mInstanceObj, args);
}
}
);
//把 gDefault 的 mInstance 欄位,替換成 proxy
mInstanceField.set(gDefaultObj, proxy);
}
/**
* 判斷Activity是否有在宿主中宣告
* @param activity
* @return
*/
private static boolean isExistActivity(String activity) {
try {
PackageManager packageManager = HostApplication.getContext().getPackageManager();
PackageInfo packageInfo = packageManager.getPackageInfo(HostApplication.getContext().getPackageName(), PackageManager.GET_ACTIVITIES);
ActivityInfo[] activities = packageInfo.activities;
for(int i = 0; i < activities.length; i++) {
if (activity.equalsIgnoreCase(activities[i].name)) {
return true;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* 第二處Hook地方,Hook ActivityThread 中的 Handle mCallback 物件,使接收 LAUNCH_ACTIVITY 訊息後將替身 Activity 換回目標 Activity
* @throws ClassNotFoundException
* @throws IllegalAccessException
* @throws NoSuchFieldException
*/
private static void hookActivityThread() throws ClassNotFoundException, IllegalAccessException, NoSuchFieldException {
// 獲取到當前的 ActivityThread 物件
Class activityThreadClass = Class.forName("android.app.ActivityThread");
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
Object currentActivityThreadObj = sCurrentActivityThreadField.get(null);
// 獲取 ActivityThread 物件中的 mH 變數
Field mHField = activityThreadClass.getDeclaredField("mH");
mHField.setAccessible(true);
final Handler mHObj = (Handler) mHField.get(currentActivityThreadObj);
// Hook Handler 的 mCallback 欄位
Class handlerClass = Handler.class;
Field mCallbackField = handlerClass.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(mHObj, new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
int LAUNCH_ACTIVITY = 0;
try {
Class hClass = Class.forName("android.app.ActivityThread$H");
Field launchActivityField = hClass.getDeclaredField("LAUNCH_ACTIVITY");
launchActivityField.setAccessible(true);
LAUNCH_ACTIVITY = (int) launchActivityField.get(null);
if (msg.what == LAUNCH_ACTIVITY) {
// 把替身 Activity 的 Intent 恢復成目標 Activity 的 Intent
Class c = msg.obj.getClass();
Field intentField = c.getDeclaredField("intent");
intentField.setAccessible(true);
Intent intent = (Intent) intentField.get(msg.obj);
Intent targetIntent = intent.getParcelableExtra(EXTRA_TARGET_INTENT);
if (targetIntent != null) {
intent.setComponent(targetIntent.getComponent());
}
}
} catch (Exception e) {
e.printStackTrace();
}
// 走完原來流程
mHObj.handleMessage(msg);
return true;
}
});
}
}
第五步建立動態替換啟動Activity所需要的替身Activity,這個Activity什麼也不做,只需在AndroidManifest.xml中有宣告就好了:
public class SubActivity extends Activity {
}
第六步建立靜態代理啟動Activity所需的代理Activity:
public class ProxyActivity extends Activity {
public final static String TRAGET_ACTIVITY_CLASS_NAME = "tragetActivityClassName";
private Object mRemoteActivity;
private String mTragetActivityClassName;
private HashMap<String, Method> mActivityLifecircleMethods = new HashMap<String, Method>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mTragetActivityClassName = getIntent().getStringExtra(TRAGET_ACTIVITY_CLASS_NAME);
launchTargetActivity();
invokeOnCreate();
}
@Override
public Resources getResources() {
if (PluginManager.getResources() != null) {
return PluginManager.getResources();
} else {
return super.getResources();
}
}
/**
* 反射目標 Activity
*/
void launchTargetActivity() {
try {
// 獲取外掛的 Activity 物件
Class<?> localClass = Class.forName(mTragetActivityClassName);
Constructor<?> localConstructor = localClass.getConstructor(new Class[] {});
mRemoteActivity = localConstructor.newInstance(new Object[] {});
// 執行外掛 Activity 的 setProxy 方法,傳遞this過去,使建立雙向引用
Method setProxy = localClass.getMethod("setProxy", new Class[] { Activity.class });
setProxy.setAccessible(true);
setProxy.invoke(mRemoteActivity, new Object[] { this });
// 反射外掛 Activity 的生命週期函式
launchTargetActivityLifecircleMethods(localClass);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 反射外掛 Activity 的生命週期函式
* @param localClass
*/
protected void launchTargetActivityLifecircleMethods(Class<?> localClass) {
Method onCreate = null;
try {
onCreate = localClass.getDeclaredMethod("onCreate", new Class[] { Bundle.class });
onCreate.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
mActivityLifecircleMethods.put("onCreate", onCreate);
String[] methodNames = new String[]{"onRestart", "onStart", "onResume", "onPause", "onStop", "onDestory"};
for (String methodName : methodNames) {
Method method = null;
try {
method = localClass.getDeclaredMethod(methodName, new Class[]{});
method.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
mActivityLifecircleMethods.put(methodName, method);
}
Method onActivityResult = null;
try {
onActivityResult = localClass.getDeclaredMethod("onActivityResult",
new Class[] { int.class, int.class, Intent.class });
onActivityResult.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
mActivityLifecircleMethods.put("onActivityResult", onActivityResult);
}
/**
* 執行外掛 Activity 的 onCreate 方法
*/
private void invokeOnCreate() {
Method onCreate = mActivityLifecircleMethods.get("onCreate");
if (onCreate != null) {
try {
Bundle bundle = new Bundle();
onCreate.invoke(mRemoteActivity, new Object[]{bundle});
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
invokeOnActivityResult(requestCode, resultCode, data);
}
private void invokeOnActivityResult(int requestCode, int resultCode, Intent data) {
Method onActivityResult = mActivityLifecircleMethods.get("onActivityResult");
if (onActivityResult != null) {
try {
onActivityResult.invoke(mRemoteActivity, new Object[] { requestCode, resultCode, data });
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onStart() {
super.onStart();
invokeOnStart();
}
private void invokeOnStart() {
Method onStart = mActivityLifecircleMethods.get("onStart");
if (onStart != null) {
try {
onStart.invoke(mRemoteActivity, new Object[] {});
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onRestart() {
super.onRestart();
invokeOnRestart();
}
private void invokeOnRestart() {
Method onRestart = mActivityLifecircleMethods.get("onRestart");
if (onRestart != null) {
try {
onRestart.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onResume() {
super.onResume();
invokeOnResume();
}
private void invokeOnResume() {
Method onResume = mActivityLifecircleMethods.get("onResume");
if (onResume != null) {
try {
onResume.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onPause() {
super.onPause();
invokeOnPause();
}
private void invokeOnPause() {
Method onPause = mActivityLifecircleMethods.get("onPause");
if (onPause != null) {
try {
onPause.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onStop() {
super.onStop();
invokeOnStop();
}
private void invokeOnStop() {
Method onStop = mActivityLifecircleMethods.get("onStop");
if (onStop != null) {
try {
onStop.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
invokeOnDestroy();
}
private void invokeOnDestroy() {
Method onDestroy = mActivityLifecircleMethods.get("onDestroy");
if (onDestroy != null) {
try {
onDestroy.invoke(mRemoteActivity, new Object[] { });
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
ProxyActivity主要是對外掛中Activity的引用和其生命週期的代理管理,詳細介紹可見《Android外掛化原理和實踐 (六) 之 四大元件解決方案》。
第七步建立外掛中的Application,外掛中的Application並非真實的Application,只是一個初始化的入口類罷了,因為外掛中不能存在自己的Application,請看程式碼:
public class PlugInApplication {
private static Resources sResources;
private static Context sApplicationBaseContext;
public static void init(Context applicationBaseContext, Resources resources) {
sApplicationBaseContext = applicationBaseContext;
sResources = resources;
}
public static Resources getResources() {
return sResources;
}
public static Context getApplicationBasecontext() {
return sApplicationBaseContext;
}
}
PlugInApplication類也就是第三步中初始化外掛中反射的類,它接收Application的Content物件和合並後的資源物件。
第八步建立一個用於演示動態替換啟動Activity的PlugInActivity1,並使其繼承父類BaseActivity1:
public class PlugInActivity1 extends BaseActivity1 {
private static final String TAG = "PlugInActivity1";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_plug_in1);
Log.d(TAG, "onCreate()");
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart()");
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "onPause()");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume()");
}
@Override
protected void onStop() {
super.onStop();
Log.d(TAG, "onStop()");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy()");
}
}
繼承BaseActivity1的目的是為了統一處理getResources。因為要啟動外掛中的Activity,在Activity中所需要的資源必須要通過重寫getResources方法返回合併後的資源物件才能生效。
public class BaseActivity1 extends Activity {
@Override
public Resources getResources() {
if (PlugInApplication.getResources() != null) {
return PlugInApplication.getResources();
} else {
return super.getResources();
}
}
}
第九步建立一個用於演示靜態代理啟動Activity的PlugInActivity2,並使其繼承父類BaseActivity2:
public class PlugInActivity2 extends BaseActivity2 {
private static final String TAG = "PlugInActivity2";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_plug_in2);
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart()");
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "onPause()");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume()");
}
@Override
protected void onStop() {
super.onStop();
Log.d(TAG, "onStop()");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy()");
}
}
繼承BaseActivity2的目的是為了統一封裝that變數和使Activity保持原生程式碼風格。that變數就是儲存了宿主ProxyActivity中反射將自己傳遞過來的引用物件。
public class BaseActivity2 extends Activity {
protected Activity that;
public void setProxy(Activity proxyActivity) {
that = proxyActivity;
}
@SuppressWarnings("all")
@Override
protected void onCreate(Bundle savedInstanceState) {
}
@Override
public void setContentView(int layoutResID) {
that.setContentView(layoutResID);
}
}
第十步就是修改aapt命令,為了便於團隊開發,所以我們打算將修改後的aapt檔案放置於工程根目錄,這樣就不需要再去替Android SDK中的aapt了。關於如何修改aapt請見《Android外掛化原理和實踐 (五) 之 解決合併資源後資源Id衝突的問題》。然後我們來修改外掛中的Gradle,讓其可自定義外掛包的資源id:
import com.android.sdklib.BuildToolInfo
import java.lang.reflect.Method
apply plugin: 'com.android.application'
Task modifyAaptPathTask = task('modifyAaptPath') << {
android.applicationVariants.all { variant ->
BuildToolInfo buildToolInfo = variant.androidBuilder.getTargetInfo().getBuildTools()
Method addMethod = BuildToolInfo.class.getDeclaredMethod("add", BuildToolInfo.PathId.class, File.class)
addMethod.setAccessible(true)
addMethod.invoke(buildToolInfo, BuildToolInfo.PathId.AAPT, new File(rootDir, "aapt_mac"))
println "[LOG] new aapt path = " + buildToolInfo.getPath(BuildToolInfo.PathId.AAPT)
}
}
android {
……
defaultConfig {
……
}
buildTypes {
……
}
preBuild.doFirst {
modifyAaptPathTask.execute()
}
aaptOptions {
aaptOptions.additionalParameters '--PLUG-resoure-id', '0x71'
}
}
dependencies {
……
}
因為Gradle是相容Java的,所以上面程式碼中,在preBuild任務執行完後,再執行了modifyAaptPathTask任務,該任務通過Java反射將aapt路徑指向工程根目錄。這裡使用的是aapt_mac,如果你是Windows開發環境,請將其替換成aapt_win.exe。點選下載aapt_win.exe
如果你工程中使用的編譯sdk版本是25或以上的,應該是預設使用了aapt2,aapt2是aapt的優化版本,如果要關閉使用aapt2的話,可以在a工程中的gradle.properties中加上一行配置程式碼:
android.enableAapt2=false
第十一步編譯外掛PlugIn工程,將生成的apk檔案拷貝到宿主Host工程中assets資源目錄中
第十二步修改宿主中MainActivity程式碼,讓其執行呼叫外掛中兩個Activity:
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 動態替換方式
Button btn1 = findViewById(R.id.btn_1);
btn1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 正常去啟動外掛中的Activity
Intent intent = new Intent();
String activityName = "com.zyx.plugin.PlugInActivity1";
intent.setClassName(MainActivity.this, activityName);
startActivity(intent);
}
});
// 靜態代理方式
Button btn2 = findViewById(R.id.btn_2);
btn2.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 要啟動外掛中的Activity,就要先啟動宿主中的ProxyActivity
Intent intent = new Intent();
intent.setClass(MainActivity.this, ProxyActivity.class);
intent.putExtra(ProxyActivity.TRAGET_ACTIVITY_CLASS_NAME, "com.zyx.plugin.PlugInActivity2");
startActivity(intent);
}
});
}
}
通過上述十二個步驟就完成了整個入門級外掛化工程演示了。其中有些點介紹得有點快,不過這些都是前面文章所詳細闡述過的,如果有不明白的地方,可見前面幾篇文章的詳細介紹。值得注意的是,因為當中使用了一些反射系統底層的程式碼,所以可能會存在在某些手機廠商定製系統或在不同的Android系統版本中存在失效情況。