Android 插件化原理及實踐

概述

插件化是一個非常大的話題,他包含很多的知識點,我們今天簡單的學習一下他的原理,並且從零開始實現插件化,這裏主要用到了Hook技術

關聯文章

Android APK資源加載流程

Android 中的ClassLoader

Android App啓動過程

Android 熱修復原理實戰

設計模式 – 代理模式

插件化需要解決的問題和技術

  • Hook技術
  • 插件的類加載
  • 插件的資源加載
  • 啓動插件Activity

Hook技術

如果我們自己創建代理對象,然後把原始對象替換爲我們的代理對象(劫持原始對象),那麼就可以在這個代理對象爲所欲爲了,修改參數,替換返回值,我們稱之爲 Hook

我們可用用Hook技術來劫持原始對象,被劫持的對象叫做Hook點,什麼樣的對象比較容易Hook呢?當然是單例和靜態對象,在一個進程內單例和靜態對象不容易發生改變,用代理對象來替代Hook點,這樣我們就可以在代理對象中實現自己想做的事情,我們這裏Hook常用的startActivity方法來舉例

對於 startActivity過程有兩種方式:Context.startActivityActivity.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,這個mInstrumentationActivity成員變量,我們選擇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中的類,這個我之前分析過,不太瞭解的請看

Android 中的ClassLoader

Android 熱修復原理實戰

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,然後取出插件apkdexElements數組,我們知道dexElements封裝了Element,而Element內部封裝了DexFileDexFile用於加載dex文件,也就是說dexElements數組儲存着插件所有的類,然後再拿到應用中的dexElements數組,他存儲着應用中所有的類,最後把這倆個dexElements合併,然後把合併後的數組賦值給應用的dexElements變量,這時應用中就有了插件中所有類

啓動插件Activity

我們知道沒有在AndroidManifest中註冊的Activity是不能啓動的,但是我們插件中的Activity本來就沒有在AndroidManifest中註冊,無法啓動,那麼我們改咋麼辦呢?

使用佔坑的Activity通能過AMS的驗證

先在 Manifest 中預埋 TextActivity,啓動時 hook時,將 Intent 替換成 TextActivity。

我們在上面的ProxyInstrumentationexecStartActivity方法加入點邏輯

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消息進行處理,最終會調用ActivityonCreate方法

那麼我們改在哪裏還原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,就執行callbackhandleMessage方法,我們可以以mCallbackHook點,自定義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重寫getResourcesgetAssets返回我們插件的資源

  @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();
    }
}

現在資源也已經加載完了

注意事項

插件Activitystyle要切換成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地址

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

發佈了100 篇原創文章 · 獲贊 5 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章