插件化框架设计(二) 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 所加载的仅仅是一小部分。对于那些非”预装载”的系统资源则不会被缓冲到静态列表变量中,这时应用进程如果需要一个非预装载资源则会在各自进程中保持一个资源缓冲。

相关文章
|
4月前
|
物联网 区块链 vr&ar
未来已来:探索区块链、物联网与虚拟现实技术的融合与应用安卓与iOS开发中的跨平台框架选择
【8月更文挑战第30天】在科技的巨轮下,新技术不断涌现,引领着社会进步。本文将聚焦于当前最前沿的技术——区块链、物联网和虚拟现实,探讨它们各自的发展趋势及其在未来可能的应用场景。我们将从这些技术的基本定义出发,逐步深入到它们的相互作用和集成应用,最后展望它们如何共同塑造一个全新的数字生态系统。
|
5月前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台框架解析
在移动应用开发的广阔舞台上,安卓和iOS一直是两大主角。随着技术的进步,开发者们渴望能有一种方式,让他们的应用能同时在这两大平台上运行,而不必为每一个平台单独编写代码。这就是跨平台框架诞生的背景。本文将探讨几种流行的跨平台框架,包括它们的优势、局限性,以及如何根据项目需求选择合适的框架。我们将从技术的深度和广度两个维度,对这些框架进行比较分析,旨在为开发者提供一个清晰的指南,帮助他们在安卓和iOS的开发旅程中,做出明智的选择。
|
1月前
|
算法 JavaScript Android开发
|
29天前
|
开发框架 Dart Android开发
安卓与iOS的跨平台开发:Flutter框架深度解析
在移动应用开发的海洋中,Flutter作为一艘灵活的帆船,正引领着开发者们驶向跨平台开发的新纪元。本文将揭开Flutter神秘的面纱,从其架构到核心特性,再到实际应用案例,我们将一同探索这个由谷歌打造的开源UI工具包如何让安卓与iOS应用开发变得更加高效而统一。你将看到,借助Flutter,打造精美、高性能的应用不再是难题,而是变成了一场创造性的旅程。
|
2月前
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
137 1
|
3月前
|
前端开发 Java 数据库
💡Android开发者必看!掌握这5大框架,轻松打造爆款应用不是梦!🏆
在Android开发领域,框架犹如指路明灯,助力开发者加速应用开发并提升品质。本文将介绍五大必备框架:Retrofit简化网络请求,Room优化数据库访问,MVVM架构提高代码可维护性,Dagger 2管理依赖注入,Jetpack Compose革新UI开发。掌握这些框架,助你在竞争激烈的市场中脱颖而出,打造爆款应用。
426 3
|
3月前
|
编译器 Android开发 开发者
带你了解Android Jetpack库中的依赖注入框架:Hilt
本文介绍了Hilt,这是Google为Android开发的依赖注入框架,基于Dagger构建,旨在简化依赖注入过程。Hilt通过自动化的组件和注解减少了DI的样板代码,提高了应用的可测试性和可维护性。文章详细讲解了Hilt的主要概念、基本用法及原理,帮助开发者更好地理解和应用Hilt。
94 8
|
4月前
|
设计模式 Java Android开发
探索安卓应用开发:从新手到专家的旅程探索iOS开发中的SwiftUI框架
【8月更文挑战第29天】本文旨在通过一个易于理解的旅程比喻,带领读者深入探讨安卓应用开发的各个方面。我们将从基础概念入手,逐步过渡到高级技术,最后讨论如何维护和推广你的应用。无论你是编程新手还是有经验的开发者,这篇文章都将为你提供有价值的见解和实用的代码示例。让我们一起开始这段激动人心的旅程吧!
|
4月前
|
Android开发
基于Amlogic 安卓9.0, 驱动简说(三):使用misc框架,让驱动更简单
如何使用Amlogic T972安卓9.0系统上的misc框架来简化驱动程序开发,通过misc框架自动分配设备号并创建设备文件,从而减少代码量并避免设备号冲突。
57 0
基于Amlogic 安卓9.0, 驱动简说(三):使用misc框架,让驱动更简单
|
4月前
|
存储 前端开发 Java
Android MVVM框架详解与应用
在Android开发中,随着应用复杂度的增加,如何有效地组织和管理代码成为了一个重要的问题。MVVM(Model-View-ViewModel)架构模式因其清晰的结构和高效的开发效率,逐渐成为Android开发者们青睐的架构模式之一。本文将详细介绍Android MVVM框架的基本概念、优势、实现流程以及一个实际案例。
150 0