插件化框架设计(二) Android 资源加载机制详解(二)

简介: Android 提供了一种非常灵活的资源系统,可以根据不同的条件提供可替代资源。因此,系统基于很少的改造就能支持新特性,比如 Android N 中的分屏模式。这也是 Android 强大部分之一。本文主要讲述 Android 资源系统的实现原理,以及在应用开发中需要注意的事项。

三、加载资源

在使用资源时首先要把资源加载到内存。Resources 的作用主要就是加载资源,应用程序需要的所有资源(包括系统资源)都是通过此对象获取。一般情况下每个应用都会仅有一个 Resources 对象。

要访问资源首先要获取 Resources 对象。获取 Resources 对象有两种方法,一种是通过 Context,一种是通过 PackageManager。

1. 使用 Context 获取 Resources

抽象类Context内部个有getResources()方法,一般是在Activity对象或者Service对象中调用,因为 Activity 或者 Service 的本质是一个 Context,而真正实现Context 接口的是 ContextImpl 类。

ContextImpl 对象是在 ActivityThread 类中创建,所以 getResources()方法实际上是调用 ContextImpl.getResources()方法。在 ContextImpl 类中,该方法仅仅是返回内部的 mResources 变量,而对该变量赋值是在 init()方法中。

在创建ContextImpl 对象后,一般会调用 init()方法对 ContextImpl 对象内部变量初始化,
其中就包括 mResources 变量,如以下代码所示:

final void init(ActivityThread.PackageInfo packageInfo, IBinder activityToken, ActivityThread mainThread, Resources container){
   mPackageInfo = packageInfo;
   mResources = mPackageInfo.getResources(mainThread);
} 

从以上代码可以看出,mResources 又是调用 mPackageInfo 的 getResources()方法进行赋值。一个应用程序中可以有多个 ContextImpl,但多个 ContextImpl对象共享一个 PackageInfo 对象。所以多个 ContextImpl 对象中的 mResources变量实际上是同一个 Resources 对象。

PackageInfo.getResources()方法如下所示:

public Resources getResources(ActivityThread mainThread){
 if(mResources== null){
    mResources = mainThread.getTopLevelResources(mResDir,this);
 }
}

以上代码中,参数 mainThread 指的就是 ActivityThread 对象,每个应用程序只有一个 ActivityThread 对象。getTopLevelResources()方法就是获取本应用程序中的 Resources 对象。

在 ActivityThread 对象中,使用 HashMap<ResourcesKey,WeakReference>
mActiveResources 保存该应用程序所有的 Resources 对象,并且这些 Resources 都是以一个弱引用保存起来的,这样在内存紧张时可以释放 Resources 所占的内存。

在 mActiveResources 中,使用 ResourcesKey 映射 Resources 类,ResourcesKey仅仅是一个数据类,其创建方式如下所示:

ResourcesKey key = new ResourcesKey(resDir,compInfo.applicatioScale);

resDir 变量代表资源文件所在路径,实际是指 APK 程序所在路径,例如 /data/app/xxx.apk。该 APK 会对应/data/dalvik-cache 目录下的data@app@xxx.apk@classes.dex 文件,这两个文件也是应用程序安装后自动生成的文件

如果一个应用程序没有访问该应用程序以外的资源,那么 mActivieResources 变量中就仅有一个 Resources 对象。当应用程序想要访问其他应用程序的资源则需要构建不同的 ResourcesKey,也就是需要不同的 resDir,毕竟每一个ResourcesKey 对应一个 Resources 对象,这样该应用程序就可以访问其他应用
程序中的资源。

如果 mActiveResources 中还没有包含所要的 Resources 对象,那就需要重新创
建一个:

AssetManager assets = new AssetManager();
if(assets.addAssetPath(resDir)== 0){
 return null;
}
DisplayMetrics metrics = getDisplayMetricsLocked(false);
r = new Resources(assets,metrics,getConfiguration(),compInfo);

创建 Resources 需要一个 AssetManager 对象。在开发应用程序时,使用Resources.getAssets()获取的就是这里创建的 AssetManager 对象。

AssetManager 其实并不只是访问 res/assets 目录下的资源,而是可以访问 res
目录下的所有资源。

AssetManager 在初始化的时候会被赋予两个路径,一个是应用程序资源路径 /data/app/xxx.apk,一个是 Framework 资源路径/system/framework/framework-res.apk(系统资源会被打包到此 apk 中)。所以应用程序使用本地 Resources 既可访问应用程序资源,又可访问系统资源。

当创建好 Resources 后就把该对象放到 mActivieResources 中以便以后继续使用。

2. 使用 PackageManager 获取 Resources

该方法主要是用来访问其他应用程序中的资源,最典型的就是切换主题,但这种主题一般仅限于一个应用程序内部。获取 Resources 的过程如下所示:

使用 PackageManager 获取 Resources 对象:

PackageManager pm = mContext.getPackageManager();
pm.getResourcesForApplication("com.android...your package name");

其中 getPackageManager()返回一个 PackageManager 对象,PackageManager本身是一个 abstract 类,其真正实现类是 ApplicationPackageManager。其内部方法一般调用远程PackageManagerService。ApplicationPackageManager 在构造时传入一个远程服务的引用IPackageManager,该对象是通过调用getPackageManager()静态方法获取的。这种获取远程服务的方法大多数获取远程服务的方法类似:

public static IPackageManager getPackageManager(){
 if(sPackageManager !=null){
     return sPackageManager;
  } 
IBinder b = ServiceManager.getService("package");
sPackageManager = IPackageManager.Stub.asInterface(b); 
return sPackageManager;
}

获得了 PackageManager 对象后,接着调用 getResourcesForApplication()方法,该方法位于 ContextImpl.ApplicationPackageManager 中

image.png

以上代码内部调用 mMainThread.getTopLevelResources()方法,又回到了使用Context 获取Resources 对象的过程中。注意,此处调用参数的含义:如果目标资源程序和当前程序是同一个 uid,那么就使用目标程序的 sourceDir 作为路径,否则就使用目标程序的 publicSourceDir 目录,该目录可以在AndroidManifest.xml 中指定。在大多数情况下,目标程序和当前程序不属于同一个 uid,因此,多为 publicSourceDir,而该值默认情况下和 sourceDir 的值相同。

当进入 mMainThread.getTopLevelResources()方法后,ActivityThread 对象就会
在 mActivieResources 变量中保存一个新的 Resources 对象,其键值对应目标程
序的包名。

3. 加载应用程序资源

应用程序打包的最终文件是 xxx.apk。APK 本身是一个 zip 文件,可以使用压缩工具解压。系统在安装应用程序时首先解压,并将其中的文件放到指定目录。其中有一个文件名为 resources.arsc,APK 所有的资源均在其中定义。

resources.arsc 是一种二进制格式的文件。aapt 在对资源文件进行编译时,会为每一个资源分配唯一的 id 值,程序在执行时会根据这些 id 值读取特定的资源,而 resources.arsc 文件正是包含了所有 id 值得一个数据集合。在该文件中,如果某个 id 对应的资源是 String 或者数值(包括 int,long 等),那么该文件会直接包含相应的值,如果 id 对应的资源是某个 layout 或者 drawable 资源,那么该文件会存入对应资源的路径地址。

事实上,当程序运行时,所需要的资源都要从原始文件中读取(APK 在安装时都会被系统拷贝到/data/app 目录下)。加载资源时,首先加载 resources.arsc,然后根据 id 值找到指定的资源。

4. 加载 Framework 资源

系统资源是在 zygote 进程启动时被加载的,并且只有当加载了系统资源后才开始启动其他应用进程,从而实现其他应用进程共享系统资源的目标。

启动第一步就是加载系统资源,加载完毕后再调用 startSystemServer()启动系统进程,并最后调用 runSelectLoopMode()开始监听 Socket,并启动指定的应用进程。加载系统资源是通过preLoadResources()完成的,该方法关键代码如下所示:

Resources = Resources.getSystem();
mResources.startPreLoading();
if(PRELOAD_RESOURCES){
 long startTime = SystemClock.uptimeMillis();
TypeArray ar = mResources.obtainTypedArray(com.android.internal.R.array.
preloadingdrawables); 
int N = prelaodDrawables(runtime,ar);
Log.i(TAG,"...preloading " + N + "resources in " + (SystemClock.uptimeMi
llis()-startTime) + "ms.");
startTime = SystemClock.uptimeMillis();
ar = mResources.obtainTypedArray(com.android.internal.R.array.preloading
_color_state_lists);
N = preloadingColorStateLists(runtime,ar);
Log.i(TAG,"...preloaded " + N + "resources in " + (SystemClock.uptimeMil
lis()-startTime) + "ms.");
}
mResources.finishPreloading();

在以上代码中使用 Resources.getSystem()创建 Resources 对象,一般情况下应用程序不应该调用此方法,因为该方法返回的 Resources 仅能访问 Framework 资源。

当 Resources 对象创建完成后,调用 preloadDrawables()和preloadColorStateLists()装在需要”预装载”的资源。这两个方法都需要传入一个TypeArray,其来源是 res/values/arrays.xml 中定义的一个 array 数组资源,例如:

<array name="preloaded_drawables"> 
    <item>@drawable/sym_def_app_icon</item> 
    <item>@drawable/arrow_down_float</item>
</array>

<array name="preloaded_color_state_lists"> 
  <item>@color/hint_foreground_dark</item>   
  <item>@color/hint_foreground_light</item>
</array>

在 Resources 类中,相关资源读取函数需要将读取到的资源缓冲起来,以便以后使用,Resources 类中定义了四个静态变量缓冲这些资源:

image.png

其中后三个变量是列表类型,并且被 static 修饰,所有 Resources 对象均共享这三个变量。所以当应用程序创建新的 Resources 对象时可以访问系统资源。

第一个变量用来区分是 zygote 装在资源还是普通应用进程装在资源。因为zygote 与普通进程装载资源的方式类似,所以增加 sPreloaded变量进行区分。

sPreloaded在 startPreloading()中被置为 true,在 finishPreloading()中被置为false,而 startPreloading()和 finishPreloading()正是在 ZygoteInit.java 的preloadResources()中被调用,这就区别了 zygote 调用和普通进程调用。

最后,在 Resources 的具体资源读取方法中,会判断 mPreloaded 变量,如果为true,则同时把读取到的资源存储到三个静态列表中,否则把资源放到非静态列表中,这些非静态列表的作用范围为调用者所在进程。
Resources.loadDrawable()方法代码如下所示:

if(mPreloading){ 
    if(isColorDrawable){
       sPreloadedColorDrawables.put(key,cs); 
     } else {
        sPreloadedDrawables.put(key,cs); 
       }
     } else { synchronized(mTmpValue){
         if(isColorDrawbale){
             mColorDrawableCache.put(key,new WeakReference<ColorDrawable>(cs)); 
        }else {
             mDrawableCache.put(key,new WeakReference<Drawable>(cs)); 
       } 
      }
}

上面所介绍的资源加载仅仅只是加载在 res/values/arrays.xml 中预先定义的资源值,Framework 包含了更多的资源,zygote 所加载的仅仅是一小部分。对于那些非”预装载”的系统资源则不会被缓冲到静态列表变量中,这时应用进程如果需要一个非预装载资源则会在各自进程中保持一个资源缓冲。

相关文章
|
6月前
|
Java Android开发 开发者
1024程序节|Android框架之一 BRVAH【BaseRecyclerViewAdapterHelper】使用demo
BRVAH是一个强大的RecyclerAdapter框架(什么是RecyclerView?),它能节约开发者大量的开发时间,集成了大部分列表常用需求解决方案。为什么会有它?请查看「Android开源框架BRVAH由来篇」该框架于2016年4月10号发布的第1个版本到现在已经一年多了,经历了800多次代码提交,140多次版本打包,修复了1000多个问题,获得了9000多star,非常感谢大家的使用以及反馈。
141 0
|
23小时前
|
Android开发
Android源代码定制:Overlay目录定制|调试Overlay资源是否生效
Android源代码定制:Overlay目录定制|调试Overlay资源是否生效
4 0
|
8月前
|
Java Android开发
Android 保存资源图片到相册最新写法适用于Android10.0及以上
Android 保存资源图片到相册最新写法适用于Android10.0及以上
583 0
|
8月前
|
编解码 监控 前端开发
Android平台GB28181设备接入端如何降低资源占用和性能消耗
Android平台GB28181设备接入端如何降低资源占用和性能消耗?
|
4月前
|
缓存 JSON Android开发
[Android]网络框架之OkHttp(详细)(kotlin)
[Android]网络框架之OkHttp(详细)(kotlin)
150 0
|
4月前
|
XML Java Android开发
Android Studio App开发之利用图片加载框架Glide实现刷新验证码功能(附源码 简单易懂)
Android Studio App开发之利用图片加载框架Glide实现刷新验证码功能(附源码 简单易懂)
33 0
|
5月前
|
小程序 开发工具 Android开发
Donut多端框架小程序打包适配ios和安卓app
腾讯新出了一个 Donut 多端框架,可以直接将微信小程序转成 ios 和 安卓 app,小程序开发者工具里也集成了 app 相关升级、调试和打包的功能,终于可以一套代码开发出3个客户端了!
126 0
Donut多端框架小程序打包适配ios和安卓app
|
7月前
|
Java API Android开发
面试官,Android 怎样实现 Router 框架?(二)
面试官,Android 怎样实现 Router 框架?
|
7月前
|
ARouter IDE Java
面试官,Android 怎样实现 Router 框架?(一)
面试官,Android 怎样实现 Router 框架?
|
7月前
|
API Android开发
mPaaS(移动跨平台框架)目前已经支持了Android API级别21
mPaaS(移动跨平台框架)目前已经支持了Android API级别21
107 2