Android资源动态加载以及相关原理分析

思考

一般情况下,我们在设计一个插件化框架的时候,要解决的无非是下面几个问题:

  1. 四大组件的动态注册
  2. 组件相关的类的加载
  3. 资源的动态加载

实际上从目前的主流插件化框架来看,都是满足了以上的特点,当然因为Activity是大家最常用到的,因此一些插件化框架便只考虑了对Activity的支持,比如Small框架,从原理上来看,基本都差不多,Hook了系统相关的API来接管自己的加载逻辑,特别是Hook 了AMS(ActivityManagerService)以及ClassLoader这2个,因为这2个控制着四大组件的加载以及运行逻辑,这里的Hook指的是Hook了远端服务在本地进程的代理对象而已,由于进程隔离的存在,是没办法直接Hook远端进程(Xposed可以Hook掉系统服务,暂时不讨论这个),但根据Binder原理,只需要Hook掉本地进程的代理对象即可为我们服务,从而实现我们想要的逻辑,而资源的动态加载仅仅是本地进程的事情,今天我们来简单讨论一下。

动态加载资源例子

下面我们首先通过一个例子来说说,很简单的例子,就是动态加载图片,文本和布局,首先新建一个application的Model,

我们在string.xml加入一个文本,比如:

<resources>
    <string name="app_name">ResourcesProject</string>
    <string name="dynamic_load">动态加载文本测试</string>
</resources>

然后弄一个支付宝的图片用来测试,

然后写一个布局activity_text.xml用来动态加载,代码如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:gravity="center"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/text"
        android:text="动态加载布局"
        android:layout_width="wrap_content"
        android:textSize="20sp"
        android:layout_height="wrap_content" />
</LinearLayout>

我们将这个项目打包成一个apk文件,命名为plugin.apk,打包文件放在assets目录下面,最后放到SD卡目录下面的plugin目录下面就好,代码如下

public static void copyFileToSD(Context context) {
        try {
            InputStream fis = context.getAssets().open("plugin.apk");
            String sdPath = Environment.getExternalStorageDirectory().getAbsolutePath();
            File file = new File(sdPath, "plugin");
            if (!file.exists()) {
                file.mkdirs();
            }
            OutputStream bos = new FileOutputStream(file.getAbsolutePath() + File.separator + "plugin.apk");
            byte[] buffer = new byte[1024];
            int readCount = 0;
            while ((readCount = fis.read(buffer)) != -1) {
                bos.write(buffer, 0, readCount);
            }
            bos.flush();
            fis.close();
            bos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

当然6.0以上注意一下SD卡权限就好,

好了,已经把apk文件放在sd卡了,现在来加载测试一下吧,下面 是代码:

 private void loadPlugResources() {
        try {
            String resourcePath = Environment.getExternalStorageDirectory().toString() + "/plugin/plugin.apk";
            AssetManager mAsset=AssetManager.class.newInstance();
            Method method=mAsset.getClass().getDeclaredMethod("addAssetPath",String.class);
            method.setAccessible(true);
            method.invoke(mAsset,resourcePath);
            /**
             * 构建插件的资源Resources对象
             */
            Resources pluginResources=new Resources(mAsset,getResources().getDisplayMetrics(),getResources().getConfiguration());
            /**
             * 根据apk的文件路径获取插件的包名信息
             */
            PackageInfo packageInfo=getPackageManager().getPackageArchiveInfo(resourcePath, PackageManager.GET_ACTIVITIES);
            //获取资源的id并加载
            int imageId=pluginResources.getIdentifier("alipay","mipmap",packageInfo.packageName);
            int strId = pluginResources.getIdentifier("dynamic_load", "string", packageInfo.packageName);
            int layoutID = pluginResources.getIdentifier("activity_test", "layout", packageInfo.packageName);
            //生成XmlResourceParser
            XmlResourceParser xmlResourceParser=pluginResources.getXml(layoutID);
            imageView.setImageDrawable(pluginResources.getDrawable(imageId));
            textView.setText(pluginResources.getString(strId));
            View view= LayoutInflater.from(this).inflate(xmlResourceParser,null);
            mView.addView(view,0);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

我们简单分析一下上面的流程:

1.首先是根据AssetManager 的原理,调用隐藏方法addAssetPath把外部apk文件塞进一个AssetManager ,然后根据

      public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO);
    }

生成一个插件的Resource对象。

2.根据Resources对象调用getIdentifier方法获取了图片,文本以及布局的id,分别设置图片和文本,再动态加载了一个布局,调用Resources.getXml()方法获取XmlResourceParser 来解析布局,最后再加载布局显示,运行如图;

可以看到已经成功加载显示在界面上了。

动态加载资源原理分析

上面我们看了如何以插件的形式加载外部的资源,实际上无论是加载外部资源,还是加载宿主本身的资源,它们的原理都是相同的,只要我们弄懂了宿主自身的资源是如何加载的,那么对于上面的过程自然也就理解了.

在Android中,当我们需要加载一个资源时,一般都会先通过getResources()方法,得到一个Resources对象,再通过它提供的getXXX方法获取到对应的资源,下面将分析一下具体的调用逻辑,首先是当我们调用在Activity/Service/Application中调用getResources()时,由于它们都继承于ContextWrapper,该方法就会调用到ContextWrapper的getResources()方法,而该方法又会调用它内部的mBase变量的对应方法,

@Override
    public Resources getResources()
    {
        return mBase.getResources();
    }

这里的mBase是一个ContextImpl对象,因为Context是一个抽象类,真正的实现是在ContextIImpl里面的,它的getResources()方法,返回的是其内部的成员变量mResources,如下代码:

@Override
    public Resources getResources() {
        return mResources;
    }

可见是直接返回了一个mResources对象了,那么这个mResources是怎么来的呢,我们可以看到是在ContextImpl的构造函数里面赋值的,代码如下:

private ContextImpl(ContextImpl container, ActivityThread mainThread,
            LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
            Display display, Configuration overrideConfiguration, int createDisplayWithId) {
        mOuterContext = this;
        mMainThread = mainThread;
        mActivityToken = activityToken;
        mRestricted = restricted;
        if (user == null) {
            user = Process.myUserHandle();
        }
        mUser = user;
        mPackageInfo = packageInfo;
        mResourcesManager = ResourcesManager.getInstance();
        final int displayId = (createDisplayWithId != Display.INVALID_DISPLAY)
                ? createDisplayWithId
                : (display != null) ? display.getDisplayId() : Display.DEFAULT_DISPLAY;
        CompatibilityInfo compatInfo = null;
        if (container != null) {
            compatInfo = container.getDisplayAdjustments(displayId).getCompatibilityInfo();
        }
        if (compatInfo == null) {
            compatInfo = (displayId == Display.DEFAULT_DISPLAY)
                    ? packageInfo.getCompatibilityInfo()
                    : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
        }
        mDisplayAdjustments.setCompatibilityInfo(compatInfo);
        mDisplayAdjustments.setConfiguration(overrideConfiguration);
        mDisplay = (createDisplayWithId == Display.INVALID_DISPLAY) ? display
                : ResourcesManager.getInstance().getAdjustedDisplay(displayId, mDisplayAdjustments);
        //resources 是由packageInfo(LoadedApk )的getResources()方法获取;
        Resources resources = packageInfo.getResources(mainThread);
        if (resources != null) {
            if (displayId != Display.DEFAULT_DISPLAY
                    || overrideConfiguration != null
                    || (compatInfo != null && compatInfo.applicationScale
                            != resources.getCompatibilityInfo().applicationScale)) {
                resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
                        packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
                        packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
                        overrideConfiguration, compatInfo);
            }
        }
        //这里赋值
        mResources = resources;
}

其中packageInfo的类型为LoadedApk,LoadedApk是apk文件在内存中的表示,它内部包含了所关联的ActivityThread以及四大组件,我们在ContextImpl中赋值的其实就是它内部的mResources对象,代码如下: `

   public Resources getResources(ActivityThread mainThread) {
        if (mResources == null) {
            mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
                    mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
        }
        return mResources;
    }

可以看到如果为null,那么返回mainThread.getTopLevelResources方法,这个是主线程的方法,如果已经有了,那么就直接返回mResources对象,我们来看看主线程的getTopLevelResources方法:

/**
     * Creates the top level resources for the given package.
     */
    Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
            String[] libDirs, int displayId, Configuration overrideConfiguration,
            LoadedApk pkgInfo) {
        return mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs,
                displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo());
    }

这里也是根据安装的apk的目录来获取的,为了更加理解参数,我们来debug一下,如图:

通过debug,我们可以清楚的看到构造Resource对象所必须的参数的来源,因此,只要具备了这些,就可以任意构造,而不管位置是在哪里,因此最终调用的是mResourcesManager的getTopLevelResources方法,其实里面也差不多,主要是创建资源,然后缓存起来,也是利用了AssetManager原理:

 //创建ResourcesKey 
 ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfigCopy, scale);
//判断缓存,如果有缓存,直接返回,否则才创建
Resources r;
        synchronized (this) {
            // Resources is app scale dependent.
            if (DEBUG) Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);
            WeakReference<Resources> wr = mActiveResources.get(key);
            r = wr != null ? wr.get() : null;
            //if (r != null) Log.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
            if (r != null && r.getAssets().isUpToDate()) {
                if (DEBUG) Slog.w(TAG, "Returning cached resources " + r + " " + resDir
                        + ": appScale=" + r.getCompatibilityInfo().applicationScale
                        + " key=" + key + " overrideConfig=" + overrideConfiguration);
                return r;
            }
        }
AssetManager assets = new AssetManager();
        // resDir can be null if the 'android' package is creating a new Resources object.
        // This is fine, since each AssetManager automatically loads the 'android' package
        // already.
        if (resDir != null) {
            if (assets.addAssetPath(resDir) == 0) {
                return null;
            }
        }
        if (splitResDirs != null) {
            for (String splitResDir : splitResDirs) {
                if (assets.addAssetPath(splitResDir) == 0) {
                    return null;
                }
            }
        }
 // 缓存起来
 mActiveResources.put(key, new WeakReference<>(r));

下面我们来分析一下资源的管理者ResourcesManager的一些代码:

private static ResourcesManager sResourcesManager;
    private final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources =
            new ArrayMap<>();
    private final ArrayMap<Pair<Integer, DisplayAdjustments>, WeakReference<Display>> mDisplays =
            new ArrayMap<>();
    CompatibilityInfo mResCompatibilityInfo;
    Configuration mResConfiguration;
    public static ResourcesManager getInstance() {
        synchronized (ResourcesManager.class) {
            if (sResourcesManager == null) {
                sResourcesManager = new ResourcesManager();
            }
            return sResourcesManager;
        }
    }

我们可以看到是一个单例模式,并且有使用了mActiveResources 作为缓存资源对象,sResourcesManager在整个应用程序中只有一个实例的存在,我们上面分析了在创建mResources的时候,是首先判断是否有缓存的,如果有缓存了,则直接返回需要的mResources对象,没有的时候再创建并且存入缓存。

ResourcesKey 和ResourcesImpl 以及 Resources 和AssetManager的关系

上面创建资源的代码中都出现了他们,那他们到底是什么关系呢?

●. Resources其实只是一个代理对象,只是暴露给开发者的一个上层接口,我们平时调用的getResources().getString(),getgetIdentifier方法等都是给开发者直接用的.对于资源的使用者来说,看到的是Resources接口,其实在构建Resources对象时,同时也会创建一个ResourcesImpl对象作为它的成员变量,Resources会调用它来去获取资源,而ResourcesImpl访问资源都是通过AssetManager来完成

●. ResourcesKey 是一个缓存Resources的Key,也就是说对于一个应用程序,可以保存不同的Resource,是否返回之前的Resources对象,取决于ResourcesKey的equals方法是否相等

@Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ResourcesKey)) {
            return false;
        }
        ResourcesKey peer = (ResourcesKey) obj;
        if (!Objects.equals(mResDir, peer.mResDir)) {
            return false;
        }
        if (mDisplayId != peer.mDisplayId) {
            return false;
        }
        if (!mOverrideConfiguration.equals(peer.mOverrideConfiguration)) {
            return false;
        }
        if (mScale != peer.mScale) {
            return false;
        }
        return true;
    }

● ResourcesImpl ,看到命名,我们已经基本明白了是Resources的实现类,其内部包含了一个AssetManager,所有资源的访问都是通过它的Native方法来实现的

public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics,
            @Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) {
        mAssets = assets;
        mMetrics.setToDefaults();
        mDisplayAdjustments = displayAdjustments;
        mConfiguration.setToDefaults();
        updateConfiguration(config, metrics, displayAdjustments.getCompatibilityInfo());
        mAssets.ensureStringBlocks();
    }

通过构造函数便可以得知mAssets的来源,所有的资源都是通过mAssets访问的,比如:

int getIdentifier(String name, String defType, String defPackage) {
        if (name == null) {
            throw new NullPointerException("name is null");
        }
        try {
            return Integer.parseInt(name);
        } catch (Exception e) {
            // Ignore
        }
        return mAssets.getResourceIdentifier(name, defType, defPackage);
    }

其他也是类似的。

● AssetManager:作为资源获取的执行者,它是ResourcesImpl的内部成员变量。

通过上面的分析,我们已经知道了资源的访问最终是由AssetManager来完成,在AssetManager的创建过程中我们首先告诉它资源所在的路径,之后它就会去以下的几个地方查看资源,通过反射调用的addAssetPath。动态加载资源的关键,就是如何把包含资源的插件路径添加到AssetManager当中

public final int addAssetPath(String path) {
        synchronized (this) {
            int res = addAssetPathNative(path);
            makeStringBlocks(mStringBlocks);
            return res;
        }
    }

可以看到Java层的AssetManager只是个包装,真正关于资源处理的所有逻辑,其实都位于native层由C++实现的AssetManager。 执行addAssetPath就是解析这个格式,然后构造出底层数据结构的过程。整个解析资源的调用链是:

public final int addAssetPath(String path)
=jni=> android_content_AssetManager_addAssetPath
=> AssetManager::addAssetPath => AssetManager::appendPathToResTable => ResTable::add => ResTable::addInternal => ResTable::parsePackage

解析的细节比较繁琐,就不细细说明了,有兴趣的可以一层层研究下去。

作者 | stormWen

地址 | https://juejin.im/post/5a54e561518825733f6ddf42

声明 | 本文是 stormWen 原创,已获授权发布,未经原作者允许请勿转载

原文发布于微信公众号 - 刘望舒(liuwangshuAndroid)

原文发表时间:2018-01-17

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏向治洪

Volley解析之表单提交篇

要实现表单的提交,就要知道表单提交的数据格式是怎么样,这里我从某知名网站抓了一条数据,先来分析别人提交表单的数据格式。  数据包: Connection: ...

2245
来自专栏非著名程序员

Android 内存泄露简介、典型情景及检测解决

什么是内存泄露? Android虚拟机的垃圾回收采用的是根搜索算法。GC会从根节点(GC Roots)开始对heap进行遍历。到最后,部分没有直接或者间接引用到...

2108
来自专栏潇涧技术专栏

Pury Project Analysis

Pury的源码:https://github.com/NikitaKozlov/Pury

912
来自专栏开发之途

Android IPC机制(2)-AIDL

1624
来自专栏QQ音乐技术团队的专栏

JsBridge实现JavaScript和Java的互相调用

前端网页JavaScript(下文简称Js)和Java互相调用在手机应用中越来越常见,JsBridge是最常用的解决方案。 1. Js调用Java,Java调用...

6459
来自专栏Fish

Android判断网络状况

啊,调bug的时候发现在没有网络的时候程序会崩,因此决定加个网络判断的。就是这个代码啦~然后到了要用的时候,new一个类对象调用这个方法就可以了。 packag...

2239
来自专栏mukekeheart的iOS之旅

Android基础总结(6)——内容提供器

  前面学习的数据持久化技术包括文件存储、SharedPreferences存储以及数据库存储技术保存的数据都只能被当前应用程序所访问。虽然文件存储和Share...

4339
来自专栏Android 研究

APK安装流程详解15——PMS中的新安装流程下(装载)补充

代码位置在PackageManagerService的installPackageLI方法里面会调用到,代码如下: PackageManagerService...

2641
来自专栏Java与Android技术栈

用kotlin打印出漂亮的android日志写在最后

Kotlin号称是Android版本的swift,距离它1.0正式版本的推出快一年了。它像swift一样,可以写客户端也可以写服务端。由于公司项目比较繁忙,我一...

1272
来自专栏为数不多的Android技巧

Android 插件化原理解析——Service的插件化

在 Activity生命周期管理 以及 广播的管理 中我们详细探讨了Android系统中的Activity、BroadcastReceiver组件的工作原理以及...

2352

扫码关注云+社区

领取腾讯云代金券