Android插件化之-Resource Hook

Android插件化在國內已再也不是幾個巨頭公司團隊在玩了,陸續有團隊開源其解決方案,例如 Small,VirtualAPK,RePlugin,Atlas,甚至Lody開發的VirtualApp。另外我司也在玩,方案與Replugin相似。
借用Atlas Github上的總結,Android上動態加載方案,始終都繞不過三個關鍵的點:html

  • 動態加載資源
  • 動態加載class
  • 處理四大組件 可以讓動態代碼中的四大組件在Android上正常跑起來

本文詳解如何Hook Resource,追溯Application,Activity,Service和Broadcast是如何與Resource綁定的。java

Resource使用追溯

插件化Resource Hook有兩種解決方案[1]android

  • 合併式:addAssetPath時加入全部插件和主工程的路徑
  • 獨立式:各個插件只添加本身apk路徑

clipboard.png

合併式解決資源衝突有重寫appt,arsc文件等方案,獨立式一個典型的實現是Replugin,資源要經過提供的API來共享訪問。
本文分析的是合併方式。獨立放至另外一篇文章分析。另外本文的源碼均摘至7.0Android系統源碼。git

獲取resource不外乎在Application,Activity,Service和Broadcast中經過getResource方法,而這幾個場景都會走到ContextImpl類中[2]github

clipboard.png

public class ContextWrapper extends Context {
    Context mBase; //mBase是ContextImpl實例

    public ContextWrapper(Context base) {
        mBase = base;
    }
    @Override
    public Resources getResources()
    {
        return mBase.getResources();
    }
 }

到這裏,咱們看到Resource都是在ContextImpl實例中獲取的。如今咱們要考慮Application,Activity,Service和Broadcast是在什麼時機注入ContextImpl實例的,以及Resource實例如何注入ContextImpl中。web

Application與mBase關聯流程分析

下面咱們來倒推Application與ContextImpl關聯流程cookie

//ContextWrapper的attachBaseContext方法關聯了mBase,這裏的mBase就是ContextImpl實例,咱們往下看
    protected void attachBaseContext(Context base) {
        if (mBase != null) {
            throw new IllegalStateException("Base context already set");
        }
        mBase = base;
    }
//Application的attach方法調用了attachBaseContext方法,和context關聯了,這裏的context就是ContextImpl實例,咱們往下看
    /* package */ final void attach(Context context) {
        attachBaseContext(context);
        mLoadedApk = ContextImpl.getImpl(context).mPackageInfo;
    }
public class Instrumentation {

    //LoadedApk.makeApplication會調用
    public Application newApplication(ClassLoader cl, String className, Context context)
            throws InstantiationException, IllegalAccessException, 
            ClassNotFoundException {   
        return newApplication(cl.loadClass(className), context);
    }
    
   //Application在這裏被建立
   static public Application newApplication(Class<?> clazz, Context context)
            throws InstantiationException, IllegalAccessException, 
            ClassNotFoundException {
        Application app = (Application)clazz.newInstance();
        app.attach(context);
        return app;
    }
}
public final class LoadedApk {
  public Application makeApplication(boolean forceDefaultAppClass,Instrumentation instrumentation) {
         // ......
         //這裏終於看到ContextImpl被建立了,並經過Instrumentation.newApplication與Application關聯起來了
         //另外這裏createAppContext是ContextImpl的mResource與LoadApk的mResource關聯的核心代碼                              
        ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
        app = mActivityThread.mInstrumentation.newApplication(
                cl, appClass, appContext);
        // ......
        mApplication = app;
        // ......
        return app;
  }
}
public final class ActivityThread {
    private void handleBindApplication(AppBindData data) {
           // ......
           // 獲取應用信息LoadedApk
          data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
           // 實例化Application
          Application app = data.info.makeApplication(data.restrictedBackupMode, null);
          mInitialApplication = app;
    }
}

從上面的倒推代碼調用,瞭解了Application與ContextImpl的關聯時機。如今來分析正序的代碼調用流程app

  • Luancher APP處理點擊,會調用到AMS。ActivityManagerService發送BIND_APPLICATION消息致ActivityThread,ActivityThread.handleBindApplication中調用了LoadedApk.makeApplication方法
  • ActivityThread.makeApplication方法建立了ContextImpl實例,並做爲參數調用Instrumentation.newApplication方法
  • Instrumentation.newApplication方法完成Application實例建立,並在application.attach方法完成Application實例與ContextImpl的關聯

固然,這只是正向的代碼分析流程,具體細節和各版本差別會有所不一樣。框架

mBase與Resource關聯流程分析

上面流程分析到ContextImpl.createAppContext方法是ContextImpl實例的mResource與LoadApk實例的mResource關聯的核心代碼,接下來咱們看下createAppContext方法ide

class ContextImpl extends Context {
  static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
          if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
           return new ContextImpl(null, mainThread,
                packageInfo, null, null, 0, null, null, Display.INVALID_DISPLAY);
       }
  private ContextImpl(ContextImpl container, ActivityThread mainThread,
                            LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
                            Display display, Configuration overrideConfiguration, int createDisplayWithId) {
            //...
            //從LoadApk建立Resources 實例
            Resources resources = packageInfo.getResources(mainThread);
            //...
            mResources = resources;
            //...
        }
}
//LoadedApk類
public Resources getResources(ActivityThread mainThread) {
        if (mResources == null) {
            mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
                    mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
        }
        return mResources;
}

從上面分析得知,若是咱們把LoadedApk.mResource Hook成咱們的插件框架Resource, 這樣就向跨宿主和插件資源訪問前進了一步。

資源合併流程分析

如何將插件的資源與宿主合併,照舊,咱們先來逆向分析代碼調用.

public class Resources {
  public CharSequence getText(@StringRes int id) throws NotFoundException {
        CharSequence res = mAssets.getResourceText(id);
        //......
    }
  public String[] getStringArray(@ArrayRes int id)
            throws NotFoundException {
        String[] res = mAssets.getResourceStringArray(id);
        //......
    }
}

與getText,getStringArray等方法獲取資源相似,都會調用mAssets。getResourcexxx方法,mAssets是一個AssetManager對象是從Resource構造函數中賦值。如如下代碼

/**
     * Create a new Resources object on top of an existing set of assets in an
     * AssetManager.
     *
     * @param assets Previously created AssetManager.
     * @param metrics Current display metrics to consider when
     *                selecting/computing resource values.
     * @param config Desired device configuration to consider when
     *               selecting/computing resource values (optional).
     */
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO);
    }

    /**
     * Creates a new Resources object with CompatibilityInfo.
     *
     * @param assets Previously created AssetManager.
     * @param metrics Current display metrics to consider when
     *                selecting/computing resource values.
     * @param config Desired device configuration to consider when
     *               selecting/computing resource values (optional).
     * @param compatInfo this resource's compatibility info. Must not be null.
     * @hide
     */
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config,
            CompatibilityInfo compatInfo) {
        mAssets = assets;
        mMetrics.setToDefaults();
        if (compatInfo != null) {
            mCompatibilityInfo = compatInfo;
        }
        updateConfiguration(config, metrics);
        assets.ensureStringBlocks();
    }

咱們先忽略除assets入參之外的參數,AssetManager有一個關鍵方法 addAssetPath,能夠把額外的apk或目錄的資源加入到AssetManager實例中。而且額外的一個關鍵點,AssetManager是一個單例。

/**
     * Add an additional set of assets to the asset manager.  This can be
     * either a directory or ZIP file.  Not for use by applications.  Returns
     * the cookie of the added asset, or 0 on failure.
     * {@hide}
     */
    public final int addAssetPath(String path) {
        synchronized (this) {
            int res = addAssetPathNative(path);
            makeStringBlocks(mStringBlocks);
            return res;
        }
    }

分析到這裏,咱們能夠想下,若是咱們把AssetManager單例加入插件的資源或宿主的資源,那資源共享就解決了一大半。
資源共享另外一半問題是咱們要解決資源id突衝問題,這篇咱們不細說,解決方案目前有重寫aapt,arsc等方案。

Activity與mBase關聯代碼分析

前面咱們看到ContextWrapper是在attachBaseContext中關聯ContextImpl對象的。先看下Activity.attachBaseContext在什麼方法中調用。

//Activity.attach方法
    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
        attachBaseContext(context);
        //.....
    }

從代碼看到,Activity.attach方法執行了attachBaseContext。Instrumentation管理Activity建立和生命週期回調。下面看下Instrumentation.performLaunchActivity方法。

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
         //......   
        Activity activity = null;
        //......   
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
        //......
                //createBaseContextForActivity返回了ContextImpl實例    
                Context appContext = createBaseContextForActivity(r, activity);
        //......    
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor);

        //......    
        return activity;
    }

Instrumentation.createBaseContextForActivity方法

private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) {
        //.....
        //ContextImpl.createActivityContext返回了ContextImpl實例   
        ContextImpl appContext = ContextImpl.createActivityContext(
                this, r.packageInfo, displayId, r.overrideConfig);
        appContext.setOuterContext(activity);
        Context baseContext = appContext;
        //.....
        return baseContext;
    }

轉至ContextImpl.createActivityContext方法

static ContextImpl createActivityContext(ActivityThread mainThread,
            LoadedApk packageInfo, int displayId, Configuration overrideConfiguration) {
        if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
        return new ContextImpl(null, mainThread, packageInfo, null, null, false,
                null, overrideConfiguration, displayId);
    }

上面咱們分析到ContextImpl構造函數會將LoadApk的mResource賦值給ContextImpl的mResource。至此,咱們能夠確認Activity和Application同樣,mBase.mResource就是LoadApk的mResource。

Service與mBase關聯代碼分析

Service與Activity相似,Service.attach在ActivityThread.handleCreateService調用。

//ActivityThread.handleCreateService
    private void handleCreateService(CreateServiceData data) {
            //......
            service = (Service) cl.loadClass(data.info.name).newInstance();
            //......
            ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
            context.setOuterContext(service);

            Application app = packageInfo.makeApplication(false, mInstrumentation);
            service.attach(context, this, data.info.name, data.token, app,
                    ActivityManagerNative.getDefault());
            //......
    }

上面咱們分析到ContextImpl.createAppContext會執行構造函數,在構造函數會將LoadedApk的mResource賦值給ContextImpl的mResource。至此,咱們能夠確認Service和Application同樣,mBase.mResource就是LoadApk的mResource。

Broadcast與mBase關聯代碼分析

Broadcast與Service相似,Broadcast.onReceive在ActivityThread.handleReceiver調用。

private void handleReceiver(ReceiverData data) {
      
        //......
        BroadcastReceiver receiver;
        try {
            java.lang.ClassLoader cl = packageInfo.getClassLoader();
            data.intent.setExtrasClassLoader(cl);
            data.intent.prepareToEnterProcess();
            data.setExtrasClassLoader(cl);
            receiver = (BroadcastReceiver)cl.loadClass(component).newInstance();
        } catch (Exception e) {
            //......
        }

            //......
            ContextImpl context = (ContextImpl)app.getBaseContext();
            sCurrentBroadcastIntent.set(data.intent);
            receiver.setPendingResult(data);
            
            //receiver.onReceive傳入的是ContextImpl.getReceiverRestrictedContext返回對象
            receiver.onReceive(context.getReceiverRestrictedContext(),
                    data.intent);
        //......
    }
//ContextImpl.getReceiverRestrictedContext
    final Context getReceiverRestrictedContext() {
        if (mReceiverRestrictedContext != null) {
            return mReceiverRestrictedContext;
        }
        return mReceiverRestrictedContext = new ReceiverRestrictedContext(getOuterContext());
    }

ReceiverRestrictedContext也是繼承ContextWrapper,其mBase是Application。

總結

至此,咱們看到Application,Activity,Service和Broadcast均會經過LoadedApk.mResource去獲取資源,咱們只要HOOK LoadedApk的mResource替換咱們的Resource便可。好比VirtualApk[4]的處理。

//ResourcesManager.hookResources
    public static void hookResources(Context base, Resources resources) {
        try {
            ReflectUtil.setField(base.getClass(), base, "mResources", resources);
            Object loadedApk = ReflectUtil.getPackageInfo(base);
            ReflectUtil.setField(loadedApk.getClass(), loadedApk, "mResources", resources);
          //......
    }

參考

[1] 《Android插件化技術——原理篇》:https://mp.weixin.qq.com/s/Uw...
[2] Android中Context詳解 ---- 你所不知道的Context:http://blog.csdn.net/qinjunin...
[3] 更深層次的理解Context:http://www.jcodecraeer.com/a/...
[4] VirtualApk:https://github.com/didi/Virtu...

相關文章
相關標籤/搜索