淺析Android插件化

前言

Android P preview版本中,已限制對@hide api的反射調用,具體的原理能夠閱讀Android P調用隱藏API限制原理這篇文章。因爲最近團隊分享也在分享插件化、熱修復相關的東西。所以,寫一篇文章,好好記錄一下。android

準備知識

  • 反射、動態代理
  • Android中的幾個相關的ClassLoader,注意PathClassLoader在ART虛擬機上是能夠加載未安裝的APK的,Dalvik虛擬機則不能夠。
  • Android中四大組件的相關原理
  • PackageManagerServer
  • 資源加載、資源打包
  • 其餘

文章中所涉及到的代碼均經過Nexus 5(dalvik虛擬機) Android 6.0版本的測試git

文章中所涉及到的一切資源都在這個倉庫下github

特別說明,本博客不會特別解釋過多原理性的東西。若是讀者不具有相關的知識儲備,建議先閱讀weishu和gityuan兩位大神的博客,資源打包的知識能夠閱讀 老羅的博客。api

Activity的插件化

首先須要說明一點的是,啓動一個徹底沒有在AndroidManifest註冊的Activity是不可能的。由於在啓動的過程當中,存在一個校驗的過程,而這個校驗則是由PMS來完成的,這個咱們沒法干預。所以,Activity的插件化方案大多使用佔坑的思想。不一樣的是如何在檢驗以前替換,在生成對象的時候還原。就目前來看,有兩種比較好方案:bash

  • Hook Instrumentation方案
  • 干預startActivity等方法,干預ClassLoader findClass的方案

這裏說一下Hook Instrumentation方法。根據上面提到的想法,咱們須要在先繞過檢查,那麼,咱們如何繞過檢查呢?經過分析Activity的啓動流程會發現,在Instrumentation#execStartActivity中,會有個checkStartActivityResult的方法去檢查錯誤,所以,咱們能夠複寫這個方法,讓啓動參數能經過系統的檢查。那麼,咱們如何作呢?首先,咱們須要檢查要啓動的Intent能不能匹配到,匹配不到的話,將ClassName修改成咱們預先在AndroidManifest中配置的佔坑Activity,而且吧當前的這個ClassName放到當前intent的extra中,以便後續作恢復,看下代碼。架構

public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
        if (infos == null || infos.size() == 0) {
            //沒查到,要啓動的這個沒註冊
            intent.putExtra(TARGET_ACTIVITY, intent.getComponent().getClassName());
            intent.setClassName(who, "com.guolei.plugindemo.StubActivity");
        }

        Class instrumentationClz = Instrumentation.class;
        try {
            Method execMethod = instrumentationClz.getDeclaredMethod("execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
            return (ActivityResult) execMethod.invoke(mOriginInstrumentation, who, contextThread, token,
                    target, intent, requestCode, options);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
複製代碼

咱們繞過檢測了,如今須要解決的問題是還原,咱們知道,系統啓動Activity的最後會調用到ActivityThread裏面,在這裏,會經過Instrumentation#newActivity方法去反射構造一個Activity的對象,所以,咱們只須要在這裏還原便可。代碼以下:app

@Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,
            IllegalAccessException, ClassNotFoundException {
        if (!TextUtils.isEmpty(intent.getStringExtra(TARGET_ACTIVITY))) {
            return super.newActivity(cl, intent.getStringExtra(TARGET_ACTIVITY), intent);
        }
        return super.newActivity(cl, className, intent);
    }
複製代碼

一切準備就緒,咱們最後的問題是,如何替換掉系統的Instrumentation。要替換掉也簡單,替換掉ActivityThread中的mInstrumentation字段便可。ide

private void hookInstrumentation() {
        Context context = getBaseContext();
        try {
            Class contextImplClz = Class.forName("android.app.ContextImpl");
            Field mMainThread = contextImplClz.getDeclaredField("mMainThread");
            mMainThread.setAccessible(true);
            Object activityThread = mMainThread.get(context);
            Class activityThreadClz = Class.forName("android.app.ActivityThread");
            Field mInstrumentationField = activityThreadClz.getDeclaredField("mInstrumentation");
            mInstrumentationField.setAccessible(true);
            mInstrumentationField.set(activityThread,
                    new HookInstrumentation((Instrumentation) mInstrumentationField.get(activityThread),
                            context.getPackageManager()));
        } catch (Exception e) {
            e.printStackTrace();
            Log.e("plugin", "hookInstrumentation: error");
        }
    }
複製代碼

這樣,咱們就能啓動一個沒有註冊在AndroidManifest文件中的Activity了,可是這裏要注意一下,因爲咱們這裏使用的ClassLoader是宿主的ClassLoader,這樣的話,咱們須要將插件的dex文件添加到咱們宿主中。這一點很重要。有一些多ClassLoader架構的實現,這裏的代碼須要變下。測試

Service的插件化

啓動一個未註冊的Service,並不會崩潰退出,只不過有點警告。而且,service啓動直接由ContextImpl交給AMS處理了,咱們看下代碼。ui

private ComponentName startServiceCommon(Intent service, UserHandle user) {
        try {
            validateServiceIntent(service);
            service.prepareToLeaveProcess(this);
            ComponentName cn = ActivityManagerNative.getDefault().startService(
                mMainThread.getApplicationThread(), service, service.resolveTypeIfNeeded(
                            getContentResolver()), getOpPackageName(), user.getIdentifier());
            if (cn != null) {
                if (cn.getPackageName().equals("!")) {
                    throw new SecurityException(
                            "Not allowed to start service " + service
                            + " without permission " + cn.getClassName());
                } else if (cn.getPackageName().equals("!!")) {
                    throw new SecurityException(
                            "Unable to start service " + service
                            + ": " + cn.getClassName());
                }
            }
            return cn;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
複製代碼

而且建立對象的過程不禁Instrumentation來建立了,而直接在ActivityThread#handleCreateService反射生成。那麼,Activity的思路咱們就不能用了,怎麼辦呢?既然咱們沒法作替換還原,那麼,咱們能夠考慮代理,咱們啓動一個真實註冊了的Service,咱們啓動這個Service,並讓這個Service,就按照系統服務Service的處理,原模原樣的處理咱們插件的Service。

說作就作,咱們以startService爲例。咱們首先要作的是,hook掉AMS,由於AMS啓動service的時候,假如要啓動插件的Service,咱們須要怎麼作呢?把插件service替換成真是的代理Service,這樣,代理Service就啓動起來了,咱們在代理Service中,構建插件的Service,並調用attach、onCreate等方法。

Hook AMS代碼以下:

private void hookAMS() {
        try {
            Class activityManagerNative = Class.forName("android.app.ActivityManagerNative");
            Field gDefaultField = activityManagerNative.getDeclaredField("gDefault");
            gDefaultField.setAccessible(true);
            Object origin = gDefaultField.get(null);
            Class singleton = Class.forName("android.util.Singleton");
            Field mInstanceField = singleton.getDeclaredField("mInstance");
            mInstanceField.setAccessible(true);
            Object originAMN = mInstanceField.get(origin);
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                    new Class[]{Class.forName("android.app.IActivityManager")},
                    new ActivityManagerProxy(getPackageManager(),originAMN));
            mInstanceField.set(origin, proxy);
            Log.e(TAG, "hookAMS: success" );
        } catch (Exception e) {
            Log.e(TAG, "hookAMS: " + e.getMessage());
        }
    }
複製代碼

咱們在看一下ActivityManagerProxy這個代理。

@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getName().equals("startService")) {
            Intent intent = (Intent) args[1];
            List<ResolveInfo> infos = mPackageManager.queryIntentServices(intent, PackageManager.MATCH_ALL);
            if (infos == null || infos.size() == 0) {
                intent.putExtra(TARGET_SERVICE, intent.getComponent().getClassName());
                intent.setClassName("com.guolei.plugindemo", "com.guolei.plugindemo.StubService");
            }

        }
        return method.invoke(mOrigin, args);
    }
複製代碼

代碼很清晰、也很簡單,不須要在作多餘的了,那麼,咱們看下代理Service是如何啓動而且調用咱們的插件Service的。

@Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e(TAG, "onStartCommand: stub service ");
        if (intent != null && !TextUtils.isEmpty(intent.getStringExtra(TARGET_SERVICE))) {
            //啓動真正的service
            String serviceName = intent.getStringExtra(TARGET_SERVICE);
            try {
                Class activityThreadClz = Class.forName("android.app.ActivityThread");
                Method getActivityThreadMethod = activityThreadClz.getDeclaredMethod("getApplicationThread");
                getActivityThreadMethod.setAccessible(true);
                //獲取ActivityThread
                Class contextImplClz = Class.forName("android.app.ContextImpl");
                Field mMainThread = contextImplClz.getDeclaredField("mMainThread");
                mMainThread.setAccessible(true);
                Object activityThread = mMainThread.get(getBaseContext());
                Object applicationThread = getActivityThreadMethod.invoke(activityThread);
                //獲取token值
                Class iInterfaceClz = Class.forName("android.os.IInterface");
                Method asBinderMethod = iInterfaceClz.getDeclaredMethod("asBinder");
                asBinderMethod.setAccessible(true);
                Object token = asBinderMethod.invoke(applicationThread);
                //Service的attach方法
                Class serviceClz = Class.forName("android.app.Service");
                Method attachMethod = serviceClz.getDeclaredMethod("attach",
                        Context.class, activityThreadClz, String.class, IBinder.class, Application.class, Object.class);
                attachMethod.setAccessible(true);
                Class activityManagerNative = Class.forName("android.app.ActivityManagerNative");
                Field gDefaultField = activityManagerNative.getDeclaredField("gDefault");
                gDefaultField.setAccessible(true);
                Object origin = gDefaultField.get(null);
                Class singleton = Class.forName("android.util.Singleton");
                Field mInstanceField = singleton.getDeclaredField("mInstance");
                mInstanceField.setAccessible(true);
                Object originAMN = mInstanceField.get(origin);
                Service targetService = (Service) Class.forName(serviceName).newInstance();
                attachMethod.invoke(targetService, this, activityThread, intent.getComponent().getClassName(), token,
                        getApplication(), originAMN);
                //service的oncreate方法
                Method onCreateMethod = serviceClz.getDeclaredMethod("onCreate");
                onCreateMethod.setAccessible(true);
                onCreateMethod.invoke(targetService);
                targetService.onStartCommand(intent, flags, startId);
            } catch (Exception e) {
                e.printStackTrace();
                Log.e(TAG, "onStartCommand: " + e.getMessage());
            }
        }
        return super.onStartCommand(intent, flags, startId);
    }
複製代碼

代碼較長,邏輯以下:

  • 檢測到須要啓動插件Service
  • 構建插件Service attach方法須要的參數
  • 構造一個插件Service
  • 調用插件Service的attach方法
  • 調用插件Service的onCreate方法

這樣,一個插件Service就啓動起來了。

BroadcastReceiver的插件化

BroadcastReceiver分爲兩種,靜態註冊,和動態註冊。靜態註冊的是PMS在安裝或者系統啓動的時候掃描APK,解析配置文件,並存儲在PMS端的,這個咱們沒法干預,而且,咱們的插件因爲未安裝,靜態註冊的是沒法經過系統正常行爲裝載的。而動態註冊的,因爲沒有檢測這一步,所以,也不須要咱們干預。咱們如今須要解決的問題就是,怎麼能裝載插件中靜態註冊的。

咱們能夠經過解析配置文件,本身調用動態註冊的方法去註冊這個。

代碼這裏就不貼了,和下面ContentProvider的一塊兒貼。

ContentProvider的插件化

和其餘三個組件不同的是,ContentProvider是在進程啓動入口,也就是ActivityThread中進行安裝的。那麼咱們能夠按照這個思路,本身去進行安裝的操做。

代碼以下。

Field providersField = packageClz.getDeclaredField("providers");
            providersField.setAccessible(true);
            ArrayList providers = (ArrayList) providersField.get(packageObject);

            Class providerClz = Class.forName("android.content.pm.PackageParser$Provider");
            Field providerInfoField = providerClz.getDeclaredField("info");
            providersField.setAccessible(true);
            List<ProviderInfo> providerInfos = new ArrayList<>();
            for (int i = 0; i < providers.size(); i++) {
                ProviderInfo providerInfo = (ProviderInfo) providerInfoField.get(providers.get(i));
                providerInfo.applicationInfo = getApplicationInfo();
                providerInfos.add(providerInfo);
            }
            Class contextImplClz = Class.forName("android.app.ContextImpl");
            Field mMainThread = contextImplClz.getDeclaredField("mMainThread");
            mMainThread.setAccessible(true);
            Object activityThread = mMainThread.get(this.getBaseContext());
            Class activityThreadClz = Class.forName("android.app.ActivityThread");
            Method installContentProvidersMethod = activityThreadClz.getDeclaredMethod("installContentProviders", Context.class, List.class);
            installContentProvidersMethod.setAccessible(true);
            installContentProvidersMethod.invoke(activityThread, this, providerInfos);
複製代碼

貼一下總體的代碼,這裏的代碼,包括Multidex方法加dex,BroadcastReceiver的插件化以及ContentProvider的插件化。

private void loadClassByHostClassLoader() {
        File apkFile = new File("/sdcard/plugin_1.apk");
        ClassLoader baseClassLoader = this.getClassLoader();
        try {
            Field pathListField = baseClassLoader.getClass().getSuperclass().getDeclaredField("pathList");
            pathListField.setAccessible(true);
            Object pathList = pathListField.get(baseClassLoader);

            Class clz = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = clz.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            Object[] dexElements = (Object[]) dexElementsField.get(pathList);

            Class elementClz = dexElements.getClass().getComponentType();
            Object[] newDexElements = (Object[]) Array.newInstance(elementClz, dexElements.length + 1);
            Constructor<?> constructor = elementClz.getConstructor(File.class, boolean.class, File.class, DexFile.class);
            File file = new File(getFilesDir(), "test.dex");
            if (file.exists()) {
                file.delete();
            }
            file.createNewFile();
            Object pluginElement = constructor.newInstance(apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(),
                    file.getAbsolutePath(), 0));
            Object[] toAddElementArray = new Object[]{pluginElement};
            System.arraycopy(dexElements, 0, newDexElements, 0, dexElements.length);
            // 插件的那個element複製進去
            System.arraycopy(toAddElementArray, 0, newDexElements, dexElements.length, toAddElementArray.length);
            dexElementsField.set(pathList, newDexElements);

            AssetManager assetManager = getResources().getAssets();
            Method method = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            method.invoke(assetManager, apkFile.getPath());

//            PackageInfo packageInfo = getPackageManager().getPackageArchiveInfo(apkFile.getAbsolutePath(), PackageManager.GET_RECEIVERS);
//            if (packageInfo != null) {
//                for (ActivityInfo info : packageInfo.receivers) {
//                    Log.e(TAG, "loadClassByHostClassLoader: " + info.name );
//
//                }
//            }
            Class packageParseClz = Class.forName("android.content.pm.PackageParser");
            Object packageParser = packageParseClz.newInstance();
            Method parseMethod = packageParseClz.getDeclaredMethod("parsePackage", File.class, int.class);
            parseMethod.setAccessible(true);
            Object packageObject = parseMethod.invoke(packageParser, apkFile, 1 << 2);
            Class packageClz = Class.forName("android.content.pm.PackageParser$Package");
            Field receiversField = packageClz.getDeclaredField("receivers");
            receiversField.setAccessible(true);
            ArrayList receives = (ArrayList) receiversField.get(packageObject);

            Class componentClz = Class.forName("android.content.pm.PackageParser$Component");
            Field intents = componentClz.getDeclaredField("intents");
            intents.setAccessible(true);
            Field classNameField = componentClz.getDeclaredField("className");
            classNameField.setAccessible(true);
            for (int i = 0; i < receives.size(); i++) {
                ArrayList<IntentFilter> intentFilters = (ArrayList<IntentFilter>) intents.get(receives.get(i));
                String className = (String) classNameField.get(receives.get(i));
                registerReceiver((BroadcastReceiver) getClassLoader().loadClass(className).newInstance(), intentFilters.get(0));
            }

            // 安裝ContentProvider
            Field providersField = packageClz.getDeclaredField("providers");
            providersField.setAccessible(true);
            ArrayList providers = (ArrayList) providersField.get(packageObject);

            Class providerClz = Class.forName("android.content.pm.PackageParser$Provider");
            Field providerInfoField = providerClz.getDeclaredField("info");
            providersField.setAccessible(true);
            List<ProviderInfo> providerInfos = new ArrayList<>();
            for (int i = 0; i < providers.size(); i++) {
                ProviderInfo providerInfo = (ProviderInfo) providerInfoField.get(providers.get(i));
                providerInfo.applicationInfo = getApplicationInfo();
                providerInfos.add(providerInfo);
            }
            Class contextImplClz = Class.forName("android.app.ContextImpl");
            Field mMainThread = contextImplClz.getDeclaredField("mMainThread");
            mMainThread.setAccessible(true);
            Object activityThread = mMainThread.get(this.getBaseContext());
            Class activityThreadClz = Class.forName("android.app.ActivityThread");
            Method installContentProvidersMethod = activityThreadClz.getDeclaredMethod("installContentProviders", Context.class, List.class);
            installContentProvidersMethod.setAccessible(true);
            installContentProvidersMethod.invoke(activityThread, this, providerInfos);
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TAG, "loadClassByHostClassLoader: " + e.getMessage());
        }
    }
複製代碼

到這裏,四大組件的插件化方案介紹了一點點,雖然每種組件只介紹了一種方法。上面的內容忽略了大部分源碼細節。這部份內容須要你們本身去補。

資源的插件化方案

資源的插件化方案,目前有兩種

  • 合併資源方案
  • 各個插件構造本身的資源方案

今天,咱們介紹第一種方案,合併資源方案,合併資源方案,咱們只須要往現有的AssetManager中調用addAsset添加一個資源便可,固然,存在比較多適配問題,咱們暫時忽略。合併資源方案最大的問題就是資源衝突。要解決資源衝突,有兩種辦法。

  • 修改AAPT,能自由修改PP段
  • 干預編譯過程,修改ASRC和R文件

爲了簡單演示,我直接只用VirtualApk的編譯插件去作。實際上VirtualApk的編譯插件來自以Small的編譯插件。只要對文件格式熟悉,這個仍是很好寫的。

AssetManager assetManager = getResources().getAssets();
            Method method = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
            method.invoke(assetManager, apkFile.getPath());
複製代碼

咱們只須要上面簡單的代碼,就能完成資源的插件化。固然,這裏忽略了版本差別。

SO的插件化方案

so的插件化方案,我這裏介紹修改dexpathlist的方案。咱們要作的是什麼呢?只須要往nativeLibraryPathElements中添加SO的Element,而且往nativeLibraryDirectories添加so路徑就能夠了。 代碼以下。

Method findLibMethod = elementClz.getDeclaredMethod("findNativeLibrary",String.class);
            findLibMethod.setAccessible(true);
//            Object soElement = constructor.newInstance(new File("/sdcard/"), true, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(),
//                    file.getAbsolutePath(), 0));
//            findLibMethod.invoke(pluginElement,System.mapLibraryName("native-lib"));
            ZipFile zipFile = new ZipFile(apkFile);
            ZipEntry zipEntry = zipFile.getEntry("lib/armeabi/libnative-lib.so");
            InputStream inputStream = zipFile.getInputStream(zipEntry);
            File outSoFile = new File(getFilesDir(), "libnative-lib.so");
            if (outSoFile.exists()) {
                outSoFile.delete();
            }
            FileOutputStream outputStream = new FileOutputStream(outSoFile);
            byte[] cache = new byte[2048];
            int count = 0;
            while ((count = inputStream.read(cache)) != -1) {
                outputStream.write(cache, 0, count);
            }
            outputStream.flush();
            outputStream.close();
            inputStream.close();
            // 構造Element
            Object soElement = constructor.newInstance(getFilesDir(), true, null, null);
//            findLibMethod.invoke(soElement,System.mapLibraryName("native-lib"));

            // 將soElement填充到nativeLibraryPathElements中,
            Field soElementField = clz.getDeclaredField("nativeLibraryPathElements");
            soElementField.setAccessible(true);
            Object[] soElements = (Object[]) soElementField.get(pathList);
            Object[] newSoElements = (Object[]) Array.newInstance(elementClz, soElements.length + 1);
            Object[] toAddSoElementArray = new Object[]{soElement};
            System.arraycopy(soElements, 0, newSoElements, 0, soElements.length);
            // 插件的那個element複製進去
            System.arraycopy(toAddSoElementArray, 0, newSoElements, soElements.length, toAddSoElementArray.length);
            soElementField.set(pathList, newSoElements);

            //將so的文件夾填充到nativeLibraryDirectories中
            Field libDir = clz.getDeclaredField("nativeLibraryDirectories");
            libDir.setAccessible(true);
            List libDirs = (List) libDir.get(pathList);
            libDirs.add(getFilesDir());
            libDir.set(pathList,libDirs);
複製代碼

總結

在前人的精心研究下,插件化方案已經很成熟了。插件化方案的難點主要在適配方面。其餘倒還好。

PS:熱修復的相關知識,PPT已經寫好了,下篇應該會淺析一下熱修復。

相關文章
相關標籤/搜索