插件化框架解读之 Android- 资源加载机制详解(二),kotlin 语法印章类
抽象类 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 又是调用 mP
ackageInfo 的 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<Resources>> 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 既可访问应用程序资源,又可访问系统资源。
AssetManager 中很多获取资源的关键方法都是 native 实现,当使用 getXXX(int id)访问资源时,如果 id 小于 0x1000 0000 时表示访问系统资源,如果 id 都大于 0x7000 0000 则表示应用资源。aapt 在对系统资源进行编译时,所有资源 id 都被编译为小于 0x1000 0000。
当创建好 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 中:
@Override
public Resources getResourcesForApplication(ApplicationInfo app) throws NameNotFoundException{
if(app.packageName.equals("system")){
return mContext.mMainThread.getSystemContext().getResources();
}
Resources r = mContext.mMainThread.getTopLevelResources(app.uid == Process.myUid() ? app.sourceDir : app.publicSourceDir,mContext.mPackageInfo);
if(r != null){
return r;
}
throw new NameNotFoundException("Unable to open " + app.publicSourceDir);}
以上代码内部调用 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()完成的,该方法关键代码如下所示:
mResources = 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.uptimeMillis()-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.uptimeMillis()-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 类中定义了四个静态变量缓冲这些资源:
private static final LongSparseArray<Drawable.ConstantState> sPreloadedDrawables = new LongSparseArray<Drawable.ConstantState>();private static final LongSparseArray<ColorStateList> sPreloadedColorStateLists = new LongSparseArray<ColorStateList>();private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables = new LongSparseArray<Drawable.ConstantState>();private static boolean mPreloaded;
其中前三个变量是列表类型,并且被 static 修饰,所有 Resources 对象均共享这三个变量。所以当应用程序创建新的 Resources 对象时可以访问系统资源。
第四个变量用来区分是 zygote 装在资源还是普通应用进程装在资源。因为 zygote 与普通进程装载资源的方式类似,所以增加 mPreloaded 变量进行区分。
mPreloaded 在 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));
}
}}
评论