Android熱更新實踐

做者

你們好,我叫小鑫,也能夠叫我蠟筆小鑫😊;html

本人17年畢業於中山大學,於2018年7月加入37手遊安卓團隊,曾經就任於久邦數碼擔任安卓開發工程師;java

目前是37手遊安卓團隊的海外負責人,負責相關業務開發;同時兼顧一些基礎建設相關工做。android

1、什麼是插件化

什麼是插件化.jpg

一個運行的App做爲宿主,去加載一個未安裝的apk文件,而且運行起來,這就叫作插件化api

插件化的使用場景:數組

一、線上新增功能(如淘寶、支付寶等)markdown

二、熱修復(經過下發補丁插件,完成對功能的修復)app

三、當編譯太慢時,可使用插件化,對某些不改動的代碼作成插件,加快編譯速度框架

2、插件化的三種常見實現方式

一、佔位式實現插件化

一、特色

一、插件聽從宿主的定義的標準,使用宿主的上下文環境ide

二、優勢:只使用了少許反射,無hook,實現簡單函數

三、缺點:在插件中只能使用宿主提供的上下文環境,如插件Activity中,不能使用this當上下文,也就是說有必定侵入性,須要修改插件Activity的實現。

二、實現步驟

一、定義宿主的標準,下面以Activity爲例

public interface ActivityInterface {

    /** * 把宿主(app)的環境 給 插件 * @param appActivity */
    void insertAppContext(Activity appActivity);

    // 生命週期方法
    void onCreate(Bundle savedInstanceState);

    void onStart();

    void onResume();

    void onDestroy();

    //此處省略了其餘聲明週期,只作演示使用

}
複製代碼

二、在插件模塊中,根據標準實現插件Activity

//根據標準實現的插件模塊中的BaseActivity
public class BaseActivity implements ActivityInterface {
	//宿主傳遞過來的上下文
    public Activity appActivity; // 宿主的環境

    @Override
    public void insertAppContext(Activity appActivity) {
        this.appActivity = appActivity;
    }

    @SuppressLint("MissingSuperCall")
    @Override
    public void onCreate(Bundle savedInstanceState) {

    }

    @SuppressLint("MissingSuperCall")
    @Override
    public void onStart() {

    }

    @SuppressLint("MissingSuperCall")
    @Override
    public void onResume() {

    }

    @SuppressLint("MissingSuperCall")
    @Override
    public void onDestroy() {

    }

	//實際上走的是宿主的setContentView方法
    public void setContentView(int resId) {
        appActivity.setContentView(resId);
    }

    public View findViewById(int layoutId) {
        return appActivity.findViewById(layoutId);
    }

    @Override
    public void startActivity(Intent intent) {
        Intent intentNew = new Intent();
        intentNew.putExtra("className", intent.getComponent().getClassName()); // TestActivity 全類名
        appActivity.startActivity(intentNew);
    }
}

複製代碼
//BaseActivity是關鍵
public class PluginActivity extends BaseActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.plugin_main);

        // this 會報錯,由於插件沒有安裝,也沒有組件的環境,因此必須使用宿主環境
        Toast.makeText(appActivity, "我是插件", Toast.LENGTH_SHORT).show();
		
        findViewById(R.id.bt_start_activity).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            	//這個startActivity走的是BaseActivity的startActivity,也是被代理過的
                startActivity(new Intent(appActivity, TestActivity.class));
            }
        });
    }

}
複製代碼

三、將插件模塊打包apk,在宿主中加載該apk

對插件apk的加載主要分爲兩個步驟,即加載類和加載資源。加載類使用的是自定義的DexClassLoader,加載資源使用的是反射調用AssetManager的addAssetPath方法。

具體的代碼以下:

public class PluginManager {

    private static final String TAG = PluginManager.class.getSimpleName();

    private static PluginManager pluginManager;

    private Context context;

    public static PluginManager getInstance(Context context) {
        if (pluginManager == null) {
            synchronized (PluginManager.class) {
                if (pluginManager == null) {
                    pluginManager = new PluginManager(context);
                }
            }
        }
        return pluginManager;
    }

    public PluginManager(Context context) {
        this.context = context;
    }
    
    private DexClassLoader dexClassLoader;
    private Resources resources;

    /** * 一、加載類 * 二、加載資源 */
    public void loadPlugin() {
        try {
            File file = AssetUtils.copyAssetPlugin(context, "p.apk", "plugin");
            if (!file.exists()) {
                Log.d(TAG, "插件包 不存在...");
                return;
            }
            String pluginPath = file.getAbsolutePath();
            File fileDir = context.getDir("pDir", Context.MODE_PRIVATE);
            dexClassLoader = new DexClassLoader(pluginPath, fileDir.getAbsolutePath(), null, context.getClassLoader());

         
            // 加載資源
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPathMethod = assetManager.getClass().getMethod("addAssetPath", String.class); 
            addAssetPathMethod.invoke(assetManager, pluginPath); // 插件包的路徑 pluginPath
            Resources r = context.getResources(); // 宿主的資源配置信息
            // 特殊的 Resources,加載插件裏面的資源的 Resources
            resources = new Resources(assetManager, r.getDisplayMetrics(), r.getConfiguration()); 
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    public ClassLoader getClassLoader() {
        return dexClassLoader;
    }

    public Resources getResources() {
        return resources;
    }
}
複製代碼

四、在宿主中定義佔位的Activity

這裏最重要的步驟爲:

一、重寫getResources和getClassLoader方法,使用插件的ClassLoader和插件的Resources

二、實例化出來插件Activity

三、給插件Activity注入上下文

四、調用插件Activity的onCreate方法

代碼以下:

public class ProxyActivity extends Activity {

    //這裏使用的是插件中的資源
    @Override
    public Resources getResources() {
        return PluginManager.getInstance(this).getResources();
    }

    //這裏使用的是插件中的類加載器
    @Override
    public ClassLoader getClassLoader() {
        return PluginManager.getInstance(this).getClassLoader();
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 真正的加載 插件裏面的 Activity
        String className = getIntent().getStringExtra("className");

        try {
            Class mPluginActivityClass = getClassLoader().loadClass(className);
            // 實例化 插件包裏面的 Activity
            Constructor constructor = mPluginActivityClass.getConstructor(new Class[]{});
            Object mPluginActivity = constructor.newInstance(new Object[]{});

            ActivityInterface activityInterface = (ActivityInterface) mPluginActivity;

            // 給插件注入上下文
            activityInterface.insertAppContext(this);

            Bundle bundle = new Bundle();
            bundle.putString("appName", "我是宿主傳遞過來的信息");

            // 執行插件裏面的onCreate方法
            activityInterface.onCreate(bundle);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void startActivity(Intent intent) {
        String className = intent.getStringExtra("className");

        Intent proxyIntent = new Intent(this, ProxyActivity.class);
        proxyIntent.putExtra("className", className); // 包名+TestActivity
        // 要給TestActivity 進棧
        super.startActivity(proxyIntent);
    }

}
複製代碼

三、小結

至此,能夠實現簡單的佔位式插件化了,咱們來總結下步驟:

一、定義宿主和插件之間的標準,如Activity的標準爲IActivityInterface

二、根據標準實現插件模塊,打成apk文件(這裏最重要的是插件中使用的上下文是宿主中傳遞過來的)

三、宿主中加載插件模塊apk

四、定義佔位Activity,在OnCreate方法中,根據Intent攜帶的插件Activity信息,反射插件Activity實例,爲插件Activity注入佔位Activity的上下文,調用插件Activity實例的onCreate方法啓動

這種實現方式的優勢是:全程僅有少許的反射,並沒有hook系統操做,適配工做簡單。

缺點也很明顯,在插件Activity中,須要遵照宿主規則,若是要作成框架,侵入性這個問題難以解決

二、hook實現插件化

學完了佔位式插件化後,下面咱們來介紹一種在插件中可使用this的方式,採用hook系統api的方式實現插件化

一、特色

一、插件中的Activity可使用this,與常規寫法無異,無需像佔位式那樣聽從標準

二、hook操做較多,主要有兩個環節須要hook,一個是hook欺騙AMS,啓動沒有再AndroidManifest中註冊的Activity。另外一個是hook實現將插件的dex和宿主的dex合併,替換掉原先的dexElements

二、原理

一、startActivity的過程

hook實現插件化.jpg

從圖中看出,App告訴AMS啓動Activity時,是攜帶了Intent的,咱們平常看到的have you declared this activity in your AndroidManifest.xml這個錯誤,就是在調用了startActivity後,AMS對要啓動的Activity進行檢查時觸發的

也就是說,若是咱們要欺騙AMS,在startActivity時,攜帶的Intent中的Activity就必須是一個在AndroidManifest中註冊的Activity,而不能是咱們插件中的Activity。

這裏怎麼辦?偷龍轉鳳,偷樑換柱,狸貓換太子~

解決方法是,將Intent中的Component暫時替換成一個佔位Activity(在AndroidManifest中聲明過的),而且將真正要啓動的插件Activity以參數的形式存放到Intent中。

在AMS向App發送LAUNCH_ACTIVITY事件時,把真正要啓動的Activity啓動

基本原理就是這樣了,那麼要解決的重點問題以下:

一、在調用Activity時,替換掉啓動插件Activity的Intent爲啓動佔位Activity的Intent,並將啓動插件Activity的Intent以參數存放到啓動佔位Activity的Intent中

二、在AMS發送LAUNCH_ACTIVITY事件時,攔截,將Intent換回啓動插件Activity的Intent

三、啓動Activity時,是用的默認的ClassLoader加載Activity類,反射實例化的,因此須要把插件Activity加入到默認的ClassLoader中

下面咱們挨個問題來解決

三、實現步驟

一、hook AMS,偷龍轉鳳

private void hookAmsAction() throws Exception {

        Class mIActivityManagerClass = Class.forName("android.app.IActivityManager");

        // 咱們要拿到IActivityManager對象,才能讓動態代理裏面的 invoke 正常執行下
        // 執行此方法 static public IActivityManager getDefault(),就能拿到 IActivityManager
        Class mActivityManagerNativeClass2 = Class.forName("android.app.ActivityManagerNative");
        final Object mIActivityManager = mActivityManagerNativeClass2.getMethod("getDefault").invoke(null);

        // 動態代理IActivityManager
        Object mIActivityManagerProxy = Proxy.newProxyInstance(

                HookApplication.class.getClassLoader(),

                new Class[]{mIActivityManagerClass}, // 要監聽的接口

                new InvocationHandler() { // IActivityManager 接口的回調方法

                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                        if ("startActivity".equals(method.getName())) {
                            // 用ProxyActivity 繞過了 AMS檢查
                            Intent intent = new Intent(HookApplication.this, ProxyActivity.class);
                            // 把要啓動插件Activity的Intent當作參數存進去
                            intent.putExtra("actionIntent", ((Intent) args[2])); 
                            args[2] = intent;
                        }
                        Log.d("hook", "攔截到了IActivityManager裏面的方法" + method.getName());

                        // 讓系統繼續正常往下執行
                        return method.invoke(mIActivityManager, args);
                    }
                });

        /** * 爲了拿到 gDefault * 經過 ActivityManagerNative 拿到 gDefault變量(對象) */
        Class mActivityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
        Field gDefaultField = mActivityManagerNativeClass.getDeclaredField("gDefault");
        gDefaultField.setAccessible(true); // 受權
        Object gDefault = gDefaultField.get(null);

        // 替換點
        Class mSingletonClass = Class.forName("android.util.Singleton");
        // 獲取此字段 mInstance
        Field mInstanceField = mSingletonClass.getDeclaredField("mInstance");
        mInstanceField.setAccessible(true); 
        // 替換
        mInstanceField.set(gDefault, mIActivityManagerProxy); 
    }
複製代碼

二、hook LAUNCH_ACTIVITY事件,將要啓動的Activity換回來

/** * Hook LuanchActivity,即將要實例化Activity,要把ProxyActivity 給 換回來 ---》 TestActivity */
    private void hookLuanchActivity() throws Exception {

        Field mCallbackFiled = Handler.class.getDeclaredField("mCallback");
        mCallbackFiled.setAccessible(true); // 受權

        /** * handler對象怎麼來 * 1.尋找H,先尋找ActivityThread * * 執行此方法 public static ActivityThread currentActivityThread() * * 經過ActivityThread 找到 H * */
        Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
        // 得到ActivityThrea對象
        Object mActivityThread = mActivityThreadClass.getMethod("currentActivityThread").invoke(null);

        Field mHField = mActivityThreadClass.getDeclaredField("mH");
        mHField.setAccessible(true);
        // 獲取真正對象
        Handler mH = (Handler) mHField.get(mActivityThread);

        mCallbackFiled.set(mH, new MyCallback(mH)); // 替換 增長咱們本身的實現代碼
    }

    public static final int LAUNCH_ACTIVITY         = 100;

    class MyCallback implements Handler.Callback {

        private Handler mH;

        public MyCallback(Handler mH) {
            this.mH = mH;
        }

        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {

                case LAUNCH_ACTIVITY:
                    // 作咱們在本身的業務邏輯(把ProxyActivity 換成 TestActivity)
                    Object obj = msg.obj;

                    try {
                        // 咱們要獲取以前Hook攜帶過來的 TestActivity
                        Field intentField = obj.getClass().getDeclaredField("intent");
                        intentField.setAccessible(true);
                        // 獲取 intent 對象,才能取出攜帶過來的 actionIntent
                        Intent intent = (Intent) intentField.get(obj);
                        // actionIntent == 插件Activity的Intent
                        Intent actionIntent = intent.getParcelableExtra("actionIntent");
                        if (actionIntent != null) {
							// 把ProxyActivity換成真正的插件Activity
                            intentField.set(obj, actionIntent); 
                        }

                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
            }

			//事件正常往下執行
            mH.handleMessage(msg);
            return true; // 系統不會往下執行
        }
    }
複製代碼

三、將插件dex和宿主dex合併

private void pluginToAppAction() throws Exception {
        // 第一步:找到宿主 dexElements 獲得此對象 PathClassLoader表明是宿主
        PathClassLoader pathClassLoader = (PathClassLoader) this.getClassLoader(); // 本質就是PathClassLoader
        Class mBaseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
        // private final DexPathList pathList;
        Field pathListField = mBaseDexClassLoaderClass.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object mDexPathList = pathListField.get(pathClassLoader);

        Field dexElementsField = mDexPathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        // 本質就是 Element[] dexElements
        Object dexElements = dexElementsField.get(mDexPathList);

        /*** ---------------------- ***/


        // 第二步:找到插件 dexElements 獲得此對象,表明插件 DexClassLoader--表明插件
        File pluginDirFile = getDir("plugin", Context.MODE_PRIVATE);
        File file = new File(pluginDirFile.getAbsoluteFile() + File.separator + "p.apk");
        if (!file.exists()) {
            throw new FileNotFoundException("沒有找到插件包!!: " + file.getAbsolutePath());
        } else {
            Log.i("ZXX", "找到插件: " + file.getAbsolutePath());
        }
        String pluginPath = file.getAbsolutePath();
        File fileDir = this.getDir("pluginDir", Context.MODE_PRIVATE); // data/data/包名/pluginDir/
        DexClassLoader dexClassLoader = new
                DexClassLoader(pluginPath, fileDir.getAbsolutePath(), null, getClassLoader());

        Class mBaseDexClassLoaderClassPlugin = Class.forName("dalvik.system.BaseDexClassLoader");
        // private final DexPathList pathList;
        Field pathListFieldPlugin = mBaseDexClassLoaderClassPlugin.getDeclaredField("pathList");
        pathListFieldPlugin.setAccessible(true);
        Object mDexPathListPlugin = pathListFieldPlugin.get(dexClassLoader);

        Field dexElementsFieldPlugin = mDexPathListPlugin.getClass().getDeclaredField("dexElements");
        dexElementsFieldPlugin.setAccessible(true);
        // 本質就是 Element[] dexElements
        Object dexElementsPlugin = dexElementsFieldPlugin.get(mDexPathListPlugin);


        // 第三步:建立出 新的 dexElements []
        int mainDexLeng =  Array.getLength(dexElements);
        int pluginDexLeng =  Array.getLength(dexElementsPlugin);
        int sumDexLeng = mainDexLeng + pluginDexLeng;

        // 參數一:int[] String[] ... 咱們須要Element[]
        // 參數二:數組對象的長度
        // 本質就是 Element[] newDexElements
        Object newDexElements = Array.newInstance(dexElements.getClass().getComponentType(),sumDexLeng); // 建立數組對象


        // 第四步:宿主dexElements + 插件dexElements =----> 融合 新的 newDexElements
        for (int i = 0; i < sumDexLeng; i++) {
            // 先融合宿主
            if (i < mainDexLeng) {
                // 參數一:新要融合的容器 -- newDexElements
                Array.set(newDexElements, i, Array.get(dexElements, i));
            } else { // 再融合插件的
                Array.set(newDexElements, i, Array.get(dexElementsPlugin, i - mainDexLeng));
            }

        }

        // 第五步:把新的 newDexElements,設置到宿主中去
        // 宿主
        dexElementsField.set(mDexPathList, newDexElements);

        // 處理加載插件中的佈局,這裏和佔位式一致
        doPluginLayoutLoad();
    }
複製代碼

四、小結

hook插件化主要有三個重點步驟:

一、欺騙AMS,繞過AMS對插件Activity的檢測,主要是經過偷龍轉鳳的方式實現

二、hook AMS啓動Activity的LAUNCH_ACTIVITY事件,啓動插件Activity

三、將插件Dex和宿主Dex合併

這種方式實現的插件化,插件Activity中可使用this,侵入性低。但因爲用了許多hook操做,系統適配須要作的工做較多。重點的三個操做尤爲須要根據系統源碼作必定的適配

三、LoadedApk式實現插件化

在hook實現插件化中,因爲是將全部的插件都加入到dexElements中,宿主和插件用的仍是同一個ClassLoader。下面咱們來介紹LoadedApk式實現插件化,這事一種使用多個ClassLoader實現的插件化

一、特色

宿主和插件用的ClassLoader不是同一個

二、原理

hook實現插件化.jpg

欺騙AMS和偷龍轉鳳的實現和hook式實現插件化是同樣的,不一樣的是hook式實現插件化是在BaseDexClassLoader中的dexElements中加入插件的dex,來達到成功加載插件類的目的。而LoadedApk式則不是。下面來分析LoadedApk式實現插件化的原理

查看ActivityThread中啓動Activity的代碼

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

        ActivityInfo aInfo = r.activityInfo;
        if (r.packageInfo == null) {
        	//一、獲取LoadedApk
            r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                    Context.CONTEXT_INCLUDE_CODE);
        }
		。。。省略

        Activity activity = null;
        try {
        	//二、從LoadedApk中獲取ClassLoader,用於加載Activity類
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            //實例化Activity
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
           。。。省略
複製代碼

獲取PackageInfo的代碼以下:

private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo, ClassLoader baseLoader, boolean securityViolation, boolean includeCode, boolean registerPackage) {
        final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
        synchronized (mResourcesManager) {
            WeakReference<LoadedApk> ref;
            if (differentUser) {
                // Caching not supported across users
                ref = null;
            } else if (includeCode) {
            	//主要是這裏,根據包名從mPackages中獲取,那麼只要構造插件的LoadedApk對象,放入到mPackeges中就能實現
                ref = mPackages.get(aInfo.packageName);
            } else {
                ref = mResourcePackages.get(aInfo.packageName);
            }

複製代碼

三、實現步驟

欺騙AMS的操做和偷龍轉風環節和hook一致,再也不贅述。

一、下面介紹如何構建LoadedApk對象,加入到ActivityThread中的mPackages中

/** * 本身創造一個LoadedApk.ClassLoader 添加到 mPackages,此LoadedApk 專門用來加載插件裏面的 class */
    private void customLoadedApkAction() throws Exception {
        File pluginDirFile = getDir("plugin", Context.MODE_PRIVATE);
        File file = new File( pluginDirFile.getAbsoluteFile() + File.separator + "p.apk");
        if (!file.exists()) {
            throw new FileNotFoundException("插件包不存在..." + file.getAbsolutePath());
        }
        String pulginPath = file.getAbsolutePath();

        // mPackages 添加 自定義的LoadedApk
        // final ArrayMap<String, WeakReference<LoadedApk>> mPackages 添加自定義LoadedApk
        Class mActivityThreadClass = Class.forName("android.app.ActivityThread");

        // 執行此方法 public static ActivityThread currentActivityThread() 拿到 ActivityThread對象
        Object mActivityThread = mActivityThreadClass.getMethod("currentActivityThread").invoke(null);

        Field mPackagesField = mActivityThreadClass.getDeclaredField("mPackages");
        mPackagesField.setAccessible(true);
        // 拿到mPackages對象
        Object mPackagesObj = mPackagesField.get(mActivityThread);

        Map mPackages = (Map) mPackagesObj;

        // 如何自定義一個 LoadedApk,系統是如何創造LoadedApk的,咱們就怎麼去創造LoadedApk
        // 執行此 public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai, CompatibilityInfo compatInfo)
        Class mCompatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
        Field defaultField = mCompatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
        defaultField.setAccessible(true);
        Object defaultObj = defaultField.get(null);

        /** * ApplicationInfo 如何獲取,咱們以前學習 APK解析源碼分析 */
        ApplicationInfo applicationInfo = getApplicationInfoAction();

        Method mLoadedApkMethod = mActivityThreadClass.getMethod("getPackageInfoNoCheck", ApplicationInfo.class, mCompatibilityInfoClass); // 類類型
        // 執行 才能拿到 LoedApk 對象
        Object mLoadedApk = mLoadedApkMethod.invoke(mActivityThread, applicationInfo, defaultObj);

        // 自定義加載器 加載插件
        // String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent

        File fileDir = getDir("pulginPathDir", Context.MODE_PRIVATE);

        // 自定義 加載插件的 ClassLoader
        ClassLoader classLoader = new PluginClassLoader(pulginPath,fileDir.getAbsolutePath(), null, getClassLoader());

        Field mClassLoaderField = mLoadedApk.getClass().getDeclaredField("mClassLoader");
        mClassLoaderField.setAccessible(true);
        mClassLoaderField.set(mLoadedApk, classLoader); // 替換 LoadedApk 裏面的 ClassLoader

        // 添加自定義的 LoadedApk 專門加載 插件裏面的 class

        // 最終的目標 mPackages.put(插件的包名,插件的LoadedApk);
        WeakReference weakReference = new WeakReference(mLoadedApk); // 放入 自定義的LoadedApk --》 插件的
        mPackages.put(applicationInfo.packageName, weakReference); // 增長了咱們本身的LoadedApk
    }

    /** * 獲取 ApplicationInfo 爲插件服務的 * @return * @throws */
    private ApplicationInfo getApplicationInfoAction() throws Exception {
        // 執行此public static ApplicationInfo generateApplicationInfo方法,拿到ApplicationInfo
        Class mPackageParserClass = Class.forName("android.content.pm.PackageParser");

        Object mPackageParser = mPackageParserClass.newInstance();

        // generateApplicationInfo方法的類類型
        Class $PackageClass = Class.forName("android.content.pm.PackageParser$Package");
        Class mPackageUserStateClass = Class.forName("android.content.pm.PackageUserState");

        Method mApplicationInfoMethod = mPackageParserClass.getMethod("generateApplicationInfo",$PackageClass,
                int.class, mPackageUserStateClass);

        File dirFile = getDir("plugin", Context.MODE_PRIVATE);
        File file = new File(dirFile.getAbsoluteFile() + File.separator + "p.apk");
        String pulginPath = file.getAbsolutePath();

        // 執行此public Package parsePackage(File packageFile, int flags)方法,拿到 Package
        // 得到執行方法的對象
        Method mPackageMethod = mPackageParserClass.getMethod("parsePackage", File.class, int.class);
        Object mPackage = mPackageMethod.invoke(mPackageParser, file, PackageManager.GET_ACTIVITIES);

        // 參數 Package p, int flags, PackageUserState state
        ApplicationInfo applicationInfo = (ApplicationInfo)
                mApplicationInfoMethod.invoke(mPackageParser, mPackage, 0, mPackageUserStateClass.newInstance());

        // 得到的 ApplicationInfo 就是插件的 ApplicationInfo
        // 咱們這裏獲取的 ApplicationInfo
        // applicationInfo.publicSourceDir = 插件的路徑;
        // applicationInfo.sourceDir = 插件的路徑;
        applicationInfo.publicSourceDir = pulginPath;
        applicationInfo.sourceDir = pulginPath;
        return applicationInfo;
    }
複製代碼

二、hook AMS啓動Activity的回調

class MyCallback implements Handler.Callback {

        private Handler mH;

        public MyCallback(Handler mH) {
            this.mH = mH;
        }

        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {

                case LAUNCH_ACTIVITY:
                    // 作咱們在本身的業務邏輯(把ProxyActivity 換成 TestActivity)
                    Object obj = msg.obj; // 本質 ActivityClientRecord

                    try {
                        // 咱們要獲取以前Hook攜帶過來的 TestActivity
                        Field intentField = obj.getClass().getDeclaredField("intent");
                        intentField.setAccessible(true);

                        // 獲取 intent 對象,才能取出攜帶過來的 actionIntent
                        Intent intent = (Intent) intentField.get(obj);

                        Intent actionIntent = intent.getParcelableExtra("actionIntent");

                        if (actionIntent != null) {

                            intentField.set(obj, actionIntent); // 把ProxyActivity 換成 插件Activity

                            /*** * 咱們在如下代碼中,對插件 和 宿主 進行區分 */
                            Field activityInfoField = obj.getClass().getDeclaredField("activityInfo");
                            activityInfoField.setAccessible(true); //受權
                            ActivityInfo activityInfo = (ActivityInfo) activityInfoField.get(obj);

                            // 宿主的Intent的getPackage會拿到包名,插件的會是空,用來判斷是不是插件Intent
                            if (actionIntent.getPackage() == null) { 
                                //將applicationInfo的包名改成插件的包名,這樣拿到的LoadedApk纔是咱們自定義的
                                activityInfo.applicationInfo.packageName = actionIntent.getComponent().getPackageName();

                                // 這個是下個步驟,hook PMS,繞過PMS的檢測
                                hookGetPackageInfo();

                            } else { // 宿主
                                activityInfo.applicationInfo.packageName = actionIntent.getPackage();
                            }
                        }

                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    break;
            }


            mH.handleMessage(msg);
            // 讓系統繼續正常往下執行
            // return false; // 系統就會往下執行
            return true; // 系統不會往下執行
        }
複製代碼

三、hook PMS,繞過檢測

光這樣操做還不行,在Activity啓動時,PMS會檢測包名對應的Apk是否有安裝(LoadedApk中的initializeJavaContextClassLoader方法),沒有安裝會報錯。

調用流程:performLaunchActivity->makeApplication->initializeJavaContextClassLoader。

initializeJavaContextClassLoader()方法代碼以下:

IPackageManager pm = ActivityThread.getPackageManager();
        android.content.pm.PackageInfo pi;
        try {
            pi = pm.getPackageInfo(mPackageName, PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
                    UserHandle.myUserId());
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
        if (pi == null) {
            throw new IllegalStateException("Unable to get package info for "
                    + mPackageName + "; is package not installed?");
        }
複製代碼

所以還須要hook PMS,繞過檢測,實現代碼以下:

// Hook 攔截此 getPackageInfo 作本身的邏輯
    private void hookGetPackageInfo() {
        try {
            // sPackageManager 替換 咱們本身的動態代理
            Class mActivityThreadClass = Class.forName("android.app.ActivityThread");
            Field sCurrentActivityThreadField = mActivityThreadClass.getDeclaredField("sCurrentActivityThread");
            sCurrentActivityThreadField.setAccessible(true);

            Field sPackageManagerField = mActivityThreadClass.getDeclaredField("sPackageManager");
            sPackageManagerField.setAccessible(true);
            final Object packageManager = sPackageManagerField.get(null);

            /** * 動態代理 */
            Class mIPackageManagerClass = Class.forName("android.content.pm.IPackageManager");

            Object mIPackageManagerProxy = Proxy.newProxyInstance(getClassLoader(),

                    new Class[]{mIPackageManagerClass}, // 要監聽的接口

                    new InvocationHandler() {
                        @Override
                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                            if ("getPackageInfo".equals(method.getName())) {
                                // 如何才能繞過 PMS, 欺騙系統
                                // pi != null
                                return new PackageInfo(); // 成功繞過 PMS檢測
                            }
                            // 讓系統正常繼續執行下去
                            return method.invoke(packageManager, args);
                        }
                    });


            // 替換 狸貓換太子 換成咱們本身的 動態代理
            sPackageManagerField.set(null, mIPackageManagerProxy);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製代碼

四、小結

LoadedApk方式實現插件化,主要在啓動Activity時,加載這個環節和hook方式不一樣。

主要步驟是:

模仿系統源碼,實現插件apk的LoadedApk實例。並放置到ActivityThread中的mPackages對象中

在ActivityThread的mH回調中,偷龍轉鳳插件Activity的同時,將activityInfo中的applicationInfo中的包名替換爲插件的包名,從而讓後續邏輯使用插件的LoadedApk

最後,hook PMS,繞過PMS對插件是否安裝的檢測

四、三種實現插件化方式的小結

一、佔位式實現插件化是比較穩定的,兼容性較好,由於沒有hook系統的api。但因爲要時刻注意使用宿主的上下文,編寫插件是比較難受的

二、hook方式實現插件化,不用考慮宿主環境,可是對系統api進行了hook,兼容性較差。

三、LoadedApk方式實現插件化,和hook方式接近,不用考慮宿主環境,但對系統api進行了hook,兼容性較差

3、市面上的插件化框架

一、美團Robust

美團Robust插件化的實現方式不像上述講的三種方式,而是借鑑的instant run的實現,在在編譯打包階段對每一個函數都插入一段控制邏輯代碼

簡單看下修復流程:

下面的方法,在編譯期間會加入一段控制邏輯,判斷是走補丁操做,仍是原操做

public long getIndex() {
      return 100;
 }
複製代碼

編譯後的getIndex

public static ChangeQuickRedirect changeQuickRedirect;
public long getIndex() {
     if(changeQuickRedirect != null) {
         //PatchProxy中封裝了獲取當前className和methodName的邏輯,並在其內部最終調用了changeQuickRedirect的對應函數
         if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
             return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
         }
     }
     return 100L;
 }
複製代碼

加載補丁時,會反射給changeQuickRedirect設置實例,當該實例不爲空,則走插件補丁的邏輯

固然不是這麼簡單就能完美實現,具體可參考:tech.meituan.com/2016/09/14/…

二、騰訊QZone和Tinker

騰訊的QZone和Tinker本質上使用的是hook方式實現的插件化,操做的是dexElements

這裏咱們介紹下類的verify:

在apk安裝時,虛擬機會將classes.dex優化成odex文件,而後纔會執行。在這個過程當中,會進行類的verify操做,若是調用關係的類都在同一個dex,就會被打上CLASS_ISPREVERIFIED標誌,而後寫入odex文件。

而在運行時,若是是被打上了標誌的類引用了其餘dex的類,則會報錯。

所以要解決打上標誌這個問題。

QZone的作法是:在每一個類的構造方法中,去引用一個其餘Dex中的類,從而避免被打上標誌

而Tinker的作法是:將宿主dex和插件dex進行合併,而後將dexElements中的就dex刪除,將合併後的dex加入。全部的代碼都在同一個dex中,也就不會有CLASS_ISPREVERIFIED的問題了

tinker原理圖.jpg

4、SDK插件化

一、建議選擇佔位式實現

SDK插件化本質上和app的插件化區別不大,不過SDK若是四大組件很少,很是建議使用佔位式插件化,由於兼容性問題較少。

二、自定義插件Context

但因爲sdk一般依附於宿主Activity調用,但最好不要對宿主Activity的getClassLoader和getResources進行處理,這樣能夠避免影響宿主的邏輯。因而能夠實現一個具有插件ClassLoader和Resouces的Context給Sdk使用

public class SQwanCore implements ISQwanCore {

    @Override
    public void init(Context context) {
        //構造一個帶有插件classLoader和Resources的Context
        SdkContextProxy sdkContext = new SdkContextProxy(context);
        try {
            ISQwanCore sdkObj = (ISQwanCore) sdkContext.getClassLoader().loadClass("com.sq.plugin.PluginSQwanCore").newInstance();
            sdkObj.init(sdkContext);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

SdkContextProxy代碼以下:

public class SdkContextProxy extends ContextWrapper {

    private Context baseContext;

    public SdkContextProxy(Context base) {
        super(base);
        baseContext = base;
    }

    @Override
    public ClassLoader getClassLoader() {
        return PluginManager.getInstance(baseContext).getClassLoader();
    }

    @Override
    public Resources getResources() {
        return PluginManager.getInstance(baseContext).getResources();
    }

	//啓動Activity時,作特殊操做,引導到ProxyActivity,參照佔位式的ProxyActivity
    @Override
    public void startActivity(Intent intent) {
        String className = intent.getComponent().getClassName();
        Intent proxyIntent = new Intent(this, ProxyActivity.class);
        proxyIntent.putExtra("className", className); // 包名+插件Activity
        // 要給插件Activity進棧
        super.startActivity(proxyIntent);
    }
}
複製代碼

Sdk的插件實現:

public class PluginSQwanCore implements ISQwanCore {

    @Override
    public void init(Context context) {
    	//PluginActivity要根據標準IActivityInterface實現
        context.startActivity(new Intent(context, PluginActivity.class));
    }

}
複製代碼

其餘邏輯參照佔位式插件化實現便可

至於SDK插件化的CLASS_ISPREVERIFIED最簡單的處理方式是:將插件和宿主中共有的類,在插件中刪除便可,如本案例中ISQwanCore類在插件中刪除。

三、資源處理

常見問題:

AssetManager的適配(19以上和如下不一樣)

能代理的和不能代理的(xml文件中的資源引用)

隔離宿主和插件的Resources(利用ContextWrapper處理)

資源ID衝突怎麼處理(使用gradle修改資源ID)

getIdentifier衝突怎麼處理(插件內優先插件,ResourceWrapper使用)

將宿主和插件資源合併出一個大的Resources,爲何?

一、宿主的資源中是包含系統資源的,這塊須要用

二、sdk有些資源放在宿主中,方便切包(如閃屏圖)

public class SuperHostResources {

    private Context mContext;

    private Resources mResources;

    public SuperHostResources(Context context, String pluginPath) {
        mContext = context;
        mResources = buildHostResources(pluginPath);
    }


    private Resources buildHostResources(String pluginPath) {
        Resources hostResources = mContext.getResources();
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.KITKAT) {
            try {
                AssetManager assetManager = mContext.getResources().getAssets();
                Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
                addAssetPathMethod.setAccessible(true);
                addAssetPathMethod.invoke(assetManager, pluginPath);
                hostResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());
            } catch (Exception e) {
                e.printStackTrace();
                hostResources = mContext.getResources();
            }
        } else {
            try {
                AssetManager assetManager = AssetManager.class.newInstance();
                Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
                addAssetPathMethod.setAccessible(true);
                addAssetPathMethod.invoke(assetManager, pluginPath);
                //衝突了不行的,要改id後才能夠這麼作
                String baseApkPath = mContext.getApplicationInfo().sourceDir;
                addAssetPathMethod.invoke(assetManager, baseApkPath);
                hostResources = new Resources(assetManager, mContext.getResources().getDisplayMetrics(), mContext.getResources().getConfiguration());

            } catch (Exception e) {
                e.printStackTrace();
                hostResources = mContext.getResources();
            }
        }

        return hostResources;
    }


    public Resources get() {
        return mResources;
    }

}
複製代碼

使用ResourceWrapper,爲何?

一、如getIdentifier,能夠在這裏操做出優先加載插件,仍是優先加載宿主

public class MixResources extends ResourcesWrapper {

    private Resources mPluginResources;

    private String mPluginPkgName;

    public MixResources(Resources hostResources, Context context, String pluginPath) {
        super(hostResources);
        PluginResources pluginResourcesBuilder = new PluginResources(context, pluginPath);
        mPluginResources = pluginResourcesBuilder.get();
        mPluginPkgName = pluginResourcesBuilder.getPkgName();
    }

    public MixResources(Resources hostResources, Resources pluginResources, String pluginPkgName) {
        super(hostResources);
        mPluginResources = pluginResources;
        mPluginPkgName = pluginPkgName;
    }

    public String getPluginPkgName() {
        return mPluginPkgName;
    }

    @Override
    public CharSequence getText(int id) throws Resources.NotFoundException {
        try {
            return super.getText(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getText(id);
        }
    }

    @Override
    public String getString(int id) throws Resources.NotFoundException {
        try {
            return super.getString(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getString(id);
        }
    }

    @Override
    public String getString(int id, Object... formatArgs) throws Resources.NotFoundException {
        try {
            return super.getString(id,formatArgs);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getString(id,formatArgs);
        }
    }

    @Override
    public float getDimension(int id) throws Resources.NotFoundException {
        try {
            return super.getDimension(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getDimension(id);
        }
    }

    @Override
    public int getDimensionPixelOffset(int id) throws Resources.NotFoundException {
        try {
            return super.getDimensionPixelOffset(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getDimensionPixelOffset(id);
        }
    }

    @Override
    public int getDimensionPixelSize(int id) throws Resources.NotFoundException {
        try {
            return super.getDimensionPixelSize(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getDimensionPixelSize(id);
        }
    }

    @Override
    public Drawable getDrawable(int id) throws Resources.NotFoundException {
        try {
            return super.getDrawable(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getDrawable(id);
        }
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public Drawable getDrawable(int id, Resources.Theme theme) throws Resources.NotFoundException {
        try {
            return super.getDrawable(id, theme);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getDrawable(id,theme);
        }
    }

    @Override
    public Drawable getDrawableForDensity(int id, int density) throws Resources.NotFoundException {
        try {
            return super.getDrawableForDensity(id, density);
        } catch (Resources.NotFoundException e) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
                return mPluginResources.getDrawableForDensity(id, density);
            } else {
                return null;
            }
        }
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    @Override
    public Drawable getDrawableForDensity(int id, int density, Resources.Theme theme) {
        try {
            return super.getDrawableForDensity(id, density, theme);
        } catch (Exception e) {
            return mPluginResources.getDrawableForDensity(id,density,theme);
        }
    }

    @Override
    public int getColor(int id) throws Resources.NotFoundException {
        try {
            return super.getColor(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getColor(id);
        }
    }
    @TargetApi(Build.VERSION_CODES.M)
    @Override
    public int getColor(int id, Resources.Theme theme) throws Resources.NotFoundException {
        try {
            return super.getColor(id,theme);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getColor(id,theme);
        }
    }

    @Override
    public ColorStateList getColorStateList(int id) throws Resources.NotFoundException {
        try {
            return super.getColorStateList(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getColorStateList(id);
        }
    }
    @TargetApi(Build.VERSION_CODES.M)
    @Override
    public ColorStateList getColorStateList(int id, Resources.Theme theme) throws Resources.NotFoundException {
        try {
            return super.getColorStateList(id,theme);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getColorStateList(id,theme);
        }
    }

    @Override
    public boolean getBoolean(int id) throws Resources.NotFoundException {
        try {
            return super.getBoolean(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getBoolean(id);
        }
    }

    @Override
    public XmlResourceParser getLayout(int id) throws Resources.NotFoundException {
        try {
            return super.getLayout(id);
        } catch (Resources.NotFoundException e) {
           return mPluginResources.getLayout(id);
        }
    }

    @Override
    public String getResourceName(int resid) throws Resources.NotFoundException {
        try {
            return super.getResourceName(resid);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getResourceName(resid);
        }
    }

    @Override
    public int getInteger(int id) throws Resources.NotFoundException {
        try {
            return super.getInteger(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getInteger(id);
        }
    }

    @Override
    public CharSequence getText(int id, CharSequence def) {
        try {
            return super.getText(id,def);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getText(id,def);
        }
    }

    @Override
    public InputStream openRawResource(int id) throws Resources.NotFoundException {
        try {
            return super.openRawResource(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.openRawResource(id);
        }

    }

    @Override
    public XmlResourceParser getXml(int id) throws Resources.NotFoundException {
        try {
            return super.getXml(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getXml(id);
        }
    }

    @Override
    public void getValue(int id, TypedValue outValue, boolean resolveRefs) throws Resources.NotFoundException {
        try {
            super.getValue(id, outValue, resolveRefs);
        } catch (Resources.NotFoundException e) {
            mPluginResources.getValue(id, outValue, resolveRefs);
        }
    }

    @Override
    public Movie getMovie(int id) throws Resources.NotFoundException {
        try {
            return super.getMovie(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getMovie(id);
        }
    }

    @Override
    public XmlResourceParser getAnimation(int id) throws Resources.NotFoundException {
        try {
            return super.getAnimation(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.getAnimation(id);
        }
    }

    @Override
    public InputStream openRawResource(int id, TypedValue value) throws Resources.NotFoundException {
        try {
            return super.openRawResource(id,value);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.openRawResource(id,value);
        }
    }

    @Override
    public AssetFileDescriptor openRawResourceFd(int id) throws Resources.NotFoundException {
        try {
            return super.openRawResourceFd(id);
        } catch (Resources.NotFoundException e) {
            return mPluginResources.openRawResourceFd(id);
        }
    }

    @Override
    public int getIdentifier(String name, String defType, String defPackage) {
        int pluginId = super.getIdentifier(name, defType, defPackage);
        if (pluginId <= 0) {
            return mPluginResources.getIdentifier(name, defType, mPluginPkgName);
        }
        return pluginId;
    }

    public int getIdentifierFromPlugin(String name, String defType) {
        return mPluginResources.getIdentifier(name, defType, mPluginPkgName);
    }

}
複製代碼

5、小結

一、本文介紹了三種常見的插件化實現方案,包括佔位式、hook式、LoadedApk式以及他們各自的特色

二、介紹了市面上常見的插件化框架的方案,其中簡要介紹瞭如何避免CLASS_ISPREVERIFIED問題

三、介紹SDK的插件化,SDK的Context的實現方案

相關文章
相關標籤/搜索