概述
插件化是一個非常大的話題,他包含很多的知識點,我們今天簡單的學習一下他的原理,並且從零開始實現插件化,這裏主要用到了Hook技術
關聯文章
插件化需要解決的問題和技術
- Hook技術
- 插件的類加載
- 插件的資源加載
- 啓動插件Activity
Hook技術
如果我們自己創建代理對象,然後把原始對象替換爲我們的代理對象(劫持原始對象),那麼就可以在這個代理對象爲所欲爲了,修改參數,替換返回值,我們稱之爲 Hook
。
我們可用用Hook
技術來劫持原始對象,被劫持的對象叫做Hook
點,什麼樣的對象比較容易Hook
呢?當然是單例和靜態對象,在一個進程內單例和靜態對象不容易發生改變,用代理對象來替代Hook
點,這樣我們就可以在代理對象中實現自己想做的事情,我們這裏Hook
常用的startActivity
方法來舉例
對於 startActivity
過程有兩種方式:Context.startActivity
和 Activity.startActivity
。這裏暫不分析其中的區別,以 Activity.startActivity
爲例說明整個過程的調用棧。
Activity
中的 startActivity
最終都是由 startActivityForResult
來實現的。
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
// Note we want to go through this call for compatibility with
// applications that may have overridden the method.
startActivityForResult(intent, -1);
}
}
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode) {
startActivityForResult(intent, requestCode, null);
}
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
...
//註釋1
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
...
}
我們看註釋1處,調用了mInstrumentation.execStartActivity
,來啓動Activity,這個mInstrumentation
是Activity
成員變量,我們選擇mInstrumentation
作爲Hook
點
首先先寫出代理Instrumentation類
public class ProxyInstrumentation extends Instrumentation {
private final Instrumentation instrumentation;
public ProxyInstrumentation(Instrumentation instrumentation){
this.instrumentation=instrumentation;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
Log.d("mmm", "Hook成功,執行了startActivity"+who);
Intent replaceIntent = new Intent(target, TextActivity.class);
replaceIntent.putExtra(TextActivity.TARGET_COMPONENT, intent);
intent = replaceIntent;
try {
Method execStartActivity = Instrumentation.class.getDeclaredMethod(
"execStartActivity",
Context.class,
IBinder.class,
IBinder.class,
Activity.class,
Intent.class,
int.class,
Bundle.class);
return (ActivityResult) execStartActivity.invoke(instrumentation, who, contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
ProxyInstrumentation
類繼承Instrumentation
,幷包含原始Instrumentation
的引用,實現了execStartActivity
方法,其內部會打印log並且反射調用原始Instrumentation
對象的execStartActivity
方法
接下來我們用ProxyInstrumentation
類替換原始的Instrumentation
,代碼如下:
public static void doInstrumentationHook(Activity activity){
// 拿到原始的 mInstrumentation字段
Field mInstrumentationField = null;
try {
mInstrumentationField = Activity.class.getDeclaredField("mInstrumentation");
mInstrumentationField.setAccessible(true);
// 創建代理對象
Instrumentation originalInstrumentation = (Instrumentation) mInstrumentationField.get(activity);
mInstrumentationField.set(activity, new ProxyInstrumentation(originalInstrumentation));
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
然後再MainActivity
中調用這個方法
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ProxyUtils.doInstrumentationHook(this);
}
然後啓動一個Activity
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this,TextActivity.class);
startActivity(intent);
}
})
看日誌
12-19 10:25:29.911 8957-8957/com.renxh.hook D/mmm: Hook成功,執行了startActivitycom.renxh.hook.MainActivity@71f98a6
這樣我們就成功Hook
住了Instrumentation
插件的類加載
這塊需要了解ClassLoader,以及如何合併不同ClassLoader中的類,這個我之前分析過,不太瞭解的請看
public class PluginHelper {
private static final String TAG = "mmm";
public static void loadPluginClass(Context context, ClassLoader hostClassLoader) {
// 獲取到插件apk,通常都是從網絡上下載,這裏爲了演示,直接將插件apk push到手機
String pluginPath = copyFile("/sdcard/plugin1.apk", context);
String dexopt = context.getDir("dexopt", 0).getAbsolutePath();
DexClassLoader pluginClassLoader = new DexClassLoader(pluginPath, dexopt, null, hostClassLoader);
// 通過反射獲取到pluginClassLoader中的pathList字段
Field baseDexpathList = null;
try {
//獲取插件中的類
baseDexpathList = BaseDexClassLoader.class.getDeclaredField("pathList");
baseDexpathList.setAccessible(true);
Object pathlist = baseDexpathList.get(pluginClassLoader);
Field dexElementsFiled = pathlist.getClass().getDeclaredField("dexElements");
dexElementsFiled.setAccessible(true);
Object[] dexElements = (Object[]) dexElementsFiled.get(pathlist);
//獲取應用內的類
Field baseDexpathList1 = BaseDexClassLoader.class.getDeclaredField("pathList");
baseDexpathList1.setAccessible(true);
Object pathlist1 = baseDexpathList1.get(hostClassLoader);
Field dexElementsFiled1 = pathlist1.getClass().getDeclaredField("dexElements");
dexElementsFiled1.setAccessible(true);
Object[] dexElements1 = (Object[]) dexElementsFiled1.get(pathlist1);
//創建一個數組
Object[] finalArray = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),
dexElements.length + dexElements1.length);
//合併插件和應用內的類
System.arraycopy(dexElements, 0, finalArray, 0, dexElements.length);
System.arraycopy(dexElements1, 0, finalArray, dexElements.length, dexElements1.length);
//把新數組替換掉原先的數組
dexElementsFiled1.set(pathlist1, finalArray);
Log.d("mmm","插件加載完成");
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
介紹下上方的代碼,首先利用DexClassLoader
加載未安裝的插件apk
,然後取出插件apk
的dexElements
數組,我們知道dexElements
封裝了Element
,而Element
內部封裝了DexFile
,DexFile
用於加載dex
文件,也就是說dexElements
數組儲存着插件所有的類,然後再拿到應用中的dexElements
數組,他存儲着應用中所有的類,最後把這倆個dexElements
合併,然後把合併後的數組賦值給應用的dexElements
變量,這時應用中就有了插件中所有類
啓動插件Activity
我們知道沒有在AndroidManifest
中註冊的Activity
是不能啓動的,但是我們插件中的Activity
本來就沒有在AndroidManifest
中註冊,無法啓動,那麼我們改咋麼辦呢?
使用佔坑的Activity通能過AMS的驗證
先在 Manifest 中預埋 TextActivity,啓動時 hook時,將 Intent 替換成 TextActivity。
我們在上面的ProxyInstrumentation
的execStartActivity
方法加入點邏輯
public class ProxyInstrumentation extends Instrumentation {
private final Instrumentation instrumentation;
public ProxyInstrumentation(Instrumentation instrumentation){
this.instrumentation=instrumentation;
}
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
Log.d("mmm", "Hook成功,執行了startActivity"+who);
Intent replaceIntent = new Intent(target, TextActivity.class);
replaceIntent.putExtra(TextActivity.TARGET_COMPONENT, intent);
intent = replaceIntent;
try {
Method execStartActivity = Instrumentation.class.getDeclaredMethod(
"execStartActivity",
Context.class,
IBinder.class,
IBinder.class,
Activity.class,
Intent.class,
int.class,
Bundle.class);
return (ActivityResult) execStartActivity.invoke(instrumentation, who, contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
我們把原來的intent
保存起來,然後創建一個TextActivity的intent
,達到通過AMS驗證的目的,這裏利用佔坑的TextActivity
通過驗證,不過我還最終還是要打開插件的PluginActivity
的,所以需要找個合適的時候,再把intent
還原回來
那麼什麼時候還原回來呢?
我們知道在Activity
啓動時ActivityThread
會收到Handler
的消息,然後再打開Activity
public void handleMessage(Message msg) {
if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
switch (msg.what) {
case LAUNCH_ACTIVITY: {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
r.packageInfo = getPackageInfoNoCheck(
r.activityInfo.applicationInfo, r.compatInfo);
handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
} break;
最終會到handleMessage
方法中,然後對LAUNCH_ACTIVITY
消息進行處理,最終會調用Activity
的onCreate
方法
那麼我們改在哪裏還原intent
呢?
我們看一下Handler
的源碼
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
- 當
msg
中有callback
時,則調用message.callback.run();
方法,其中的callback
指的Runnable
- 如果
callback
爲空,那麼則看一下成員變量的mCallback
是否爲空,這個是Handler
的構造方法傳入的 - 如果
mCallback
也爲空,則調用handleMessage
方法,這個一般在Handler
的子類中重寫
我們看到只要mCallback
不爲null
,就執行callback
的handleMessage
方法,我們可以以mCallback
爲Hook
點,自定義mCallback
,來替換當前·handler·對象的Callback
public class ProxyHandlerCallback implements Handler.Callback {
private Handler mBaseHandler;
public ProxyHandlerCallback(Handler mBaseHandler) {
this.mBaseHandler = mBaseHandler;
}
@Override
public boolean handleMessage(Message msg) {
Log.d("mmm", "接受到消息了msg:" + msg);
if (msg.what == LAUNCH_ACTIVITY) {
try {
Object obj = msg.obj;
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
Intent intent = (Intent) intentField.get(obj);
Intent targetIntent = intent.getParcelableExtra(TextActivity.TARGET_COMPONENT);
intent.setComponent(targetIntent.getComponent());
Log.e("mmmintentField", targetIntent.toString());
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
mBaseHandler.handleMessage(msg);
return true;
}
}
這個類繼承了了Handler.Callback
,重寫了handleMessage方法,收到消息的類型爲LAUNCH_ACTIVITY
,在這個方法內還原intent
然後我們定義一個Hook
這個Handler
的方法
public static void doHandlerHook() {
try {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThread = activityThreadClass.getDeclaredMethod("currentActivityThread");
Object activityThread = currentActivityThread.invoke(null);
Field mHField = activityThreadClass.getDeclaredField("mH");
mHField.setAccessible(true);
Handler mH = (Handler) mHField.get(activityThread);
Field mCallbackField = Handler.class.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(mH, new ProxyHandlerCallback(mH));
} catch (Exception e) {
e.printStackTrace();
}
}
現在插件類加載到了應用中,插件Activity
也已經還原了,還差一步,就是插件資源的加載
插件資源的加載
首先我們把插件apk
的資源加載出來
public class ResourceHelper {
public static Resources sPluginResources;
public static AssetManager sNewAssetManager ;
public static void addResource(Context context, String path) {
//利用反射創建一個新的AssetManager
try {
sNewAssetManager = AssetManager.class.getConstructor().newInstance();
//利用反射獲取addAssetPath方法
Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
mAddAssetPath.setAccessible(true);
//利用反射調用addAssetPath方法加載外部的資源(SD卡)
if (((Integer) mAddAssetPath.invoke(sNewAssetManager, path)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
sPluginResources = new Resources(sNewAssetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
Log.d("mmm","資源加載完畢");
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
首先創建一個AssetsManager
,然後加載外部資源,然後構建一個新的Resouce
然後再應用的Aplication
重寫getResources
和getAssets
返回我們插件的資源
@Override
public Resources getResources() {
return ResourceHelper.sPluginResources == null ? super.getResources() : ResourceHelper.sPluginResources;
}
@Override
public AssetManager getAssets() {
return ResourceHelper.sNewAssetManager == null ? super.getAssets() : ResourceHelper.sNewAssetManager;
}
因爲插件的四大組件都是在宿主中創建的,所以拿到的Application
其實也是宿主的,所以插件Activity
只需要getApplication().getResources()
就可以方便的使用插件中的資源
最後在插件Activity
重寫getResources和getAssets
方法
public class PluginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_plugin);
}
@Override
public Resources getResources() {
return getApplication() != null && getApplication().getResources() != null ? getApplication().getResources() : super.getResources();
}
@Override
public AssetManager getAssets() {
return getApplication() != null && getApplication().getAssets() != null ? getApplication().getAssets() : super.getAssets();
}
}
現在資源也已經加載完了
注意事項
插件Activity
的style
要切換成NoActionBar
,不然會出bug
,這個bug
我目前也不知道爲什麼
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
</resources>
Demo
demo
使用很簡單,把assets
文件夾下的plugin1.apk
推入sdcard
就可以使用
參考
《Android開發進階解密》
https://segmentfault.com/a/1190000015688023#item-4
https://www.jianshu.com/p/d3231a15afee
https://www.jianshu.com/p/ba00ac520aad