资讯专栏INFORMATION COLUMN

Android插件化之-Resource Hook

jayce / 587人阅读

摘要:方法,是一个对象是从构造函数中赋值。上面我们分析到会执行构造函数,在构造函数会将的赋值给的。传入的是返回对象也是继承,其是。参考插件化技术原理篇中详解你所不知道的更深层次的理解

Android插件化在国内已不再是几个巨头公司团队在玩了,陆续有团队开源其解决方案,例如 Small,VirtualAPK,RePlugin,Atlas,甚至Lody开发的VirtualApp。另外我司也在玩,方案与Replugin类似。
借用Atlas Github上的总结,Android上动态加载方案,始终都绕不过三个关键的点:

动态加载资源

动态加载class

处理四大组件 能够让动态代码中的四大组件在Android上正常跑起来

本文详解如何Hook Resource,追溯Application,Activity,Service和Broadcast是如何与Resource绑定的。

Resource使用追溯

插件化Resource Hook有两种解决方案[1]

合并式:addAssetPath时加入所有插件和主工程的路径

独立式:各个插件只添加自己apk路径

合并式解决资源冲突有重写appt,arsc文件等方案,独立式一个典型的实现是Replugin,资源要通过提供的API来共享访问。
本文分析的是合并方式。独立放至另一篇文章分析。另外本文的源码均摘至7.0Android系统源码。

获取resource不外乎在Application,Activity,Service和Broadcast中通过getResource方法,而这几个场景都会走到ContextImpl类中[2]

  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中。

Application与mBase关联流程分析

下面我们来倒推Application与ContextImpl关联流程

    //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的关联时机。现在来分析正序的代码调用流程

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方法

 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...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68370.html

相关文章

  • Android 插件化原理学习 —— Hook 机制之动态代理

    摘要:什么样的对象容易找到静态变量和单例。在一个进程之内,静态变量和单例变量是相对不容易发生变化的,因此非常容易定位,而普通的对象则要么无法标志,要么容易改变。 前言 为了实现 App 的快速迭代更新,基于 H5 Hybrid 的解决方案有很多,由于 webview 本身的性能问题,也随之出现了很多基于 JS 引擎实现的原生渲染的方案,例如 React Native、weex 等,而国内一线...

    gekylin 评论0 收藏0

发表评论

0条评论

jayce

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<