插件化框架解读之Android-资源加载机制详解(二)

1. 属性的定义

在APK程序中,属性定义在res/values/attrs.xml中,在系统中属性位于framework/base/core/res/res/values/attrs.xml文件中。具体定义如下所示:

styleable相当于一个属性集合,其在R.java文件中对应一个int[]数组,aapt为styleable中的每个attr(属性)分配一个id值,int[]中的每个id对应着styleable中的每一个attr。

对于<declare-styleable name="Window">,Window相当于属性集合的名称。 
对于<attr name="windowBackground">,windowBackground相当于属性的名称;属性名称在应用程序范围内必须唯一,既无论定义几个资源文件,无论定义几个styleable,windowBackground必须唯一。

在Java代码中,变量在一个作用域内只能声明一次,但可以多次使用。attr也是一样,只能声明一次,但可以多处引用。如上代码所示,在Window中声明了一个名为windowBackground的attr,在Window中引用了一个名为windowTitle的attr

如果一个attr后面仅仅有一个name,那么这就是引用;如果不光有name还有format那就是声明。windowBackground是属性的声明,其不能在其他styleable中再次声明;windowTitle则是属性的引用,其声明是在别的styleable中。

2. 值的定义

常见的值一般有以下几种:

  • String,Color,boolean,int类型:在res/values/xxx.xml文件中指定
  • Drawable类型:在res/drawable/xxx中指定
  • layout(布局):在res/layout/xxx.xml中指定
  • style(样式):在res/values/xxx.xml中指定

值的类型大致分为两类,一类是基本类型,一类是引用类型;对于int,boolean等类型在声明属性时使用如下方式: 
<attr name="width" format="integer"/> 
<attr name="text" format="string" /> 
<attr name="centerInParent"="boolean"/> 
对于Drawable,layout等类型在声明属性时: 
<attr name="background" format="reference"/>

二、解析资源

资源解析主要涉及到两个类,一个是AttributeSet,另一个是TypedArray。

1. AttributeSet

该类位于android.util.AttributeSet,纯粹是一个辅助类,当从XML文件解析时会返回AttributeSet对象,该对象包含了解析元素的所有属性及属性值。并且在解析的属性名称与attrs.xml中定义的属性名称之间建立联系。AttributeSet还提供了一组API接口从而可以方便的根据attrs.xml中已有的名称获取相应的值。

如果使用一般的XML解析工具,则可以通过类似getElementById()等方法获取属性的名称和属性值,然而这样并没有在获取的属性名称与attrs.xml定义的属性名称之间建立联系。

Attribute对象一般作为View的构造函数的参数传递过来,例如:

publlic TextView(Context context,AttributeSet attrs,int defStyle)

AttributeSet中的API可按功能分为以下几类,假定TextView定义如下所示:

第一类,操作特定属性:

  • public String getIdAttribute(),获取id属性对应的字符串,此处返回”@+id/tv”
  • public String getStyleAttribute(),获取style属性对应的字符串,返回”@style/text”
  • public int getIdAttributeResourceValue(int defaultValue),返回id属性对应的int值,此处对应R.id.tv。

第二类,操作通用属性:

  • public int getAttributeCount(),获取属性的数目,本例中返回4
  • public String getAttributeName(int index),根据属性所在位置返回相应的属性名称。例如,id=0,layout_width=1,layout_height=2,style=3,如果getAttributeName(2),则返回android:layout_height
  • public String getAttributeValue(int index),根据位置返回值。本例中,getAttributeValue(2)则返回”wrap_content”。
  • public String getAttributeValue(String namespace,String name),返回指定命名空间,指定名称的属性值,该方法说明AttributeSet允许给一个XML Element的属性增加多个命名空间的属性值。
  • public int getAttributeResource(int index),返回指定位置的属性id值。本例中,getAttributeResource(2)返回R.attr.layout_width。前面也说过,系统会为每一个attr分配一个唯一的id。

第三类,获取特定类型的值:

  • public XXXType getAttributeXXXType(int index,XXXType defaultValue),其中XXXType包括int、unsigned int、boolean、float类型。使用该方法时,必须明确知道某个位置(index)对应的数据类型,否则会返回错误。而且该方法仅适用于特定的类型,如果某个属性值为一个style类型,或者为一个layout类型,那么返回值都将无效。

2. TypedArray

程序员在开发应用程序时,在XML文件中引用某个变量通常是android:background=”@drawable/background”,该引用对应的元素一般为某个View/ViewGroup,而View/ViewGroup的构造函数中会通过obatinStyledAttributes方法返回一个TypedArray对象,然后再调用对象中的getDrawable()方法获取背景图片。

TypedArray是对AttributeSet数据类的某种抽象。对于andorid:layout_width="@dimen/width",如果使用AttributeSet的方法,仅仅能获取”@dimen/width”字符串。而实际上该字符串对应了一个dimen类型的数据。TypedArray可以将某个AttributeSet作为参数构造TypedArray对象,并提供更方便的方法直接获取该dimen的值。

TypedArray a = context.obtainStyledAttributes(attrs,com.android.internal.R.styleable.XXX,defStyle,0);

方法obtainStyledAttributes()的第一个参数是一个AttributeSet对象,它包含了一个XML元素中定义的所有属性。第二个参数是前面定义的styleable,appt会把一个styleable编译成一个int[]数组,该数组的内部实现正是通过遍历AttributeSet中的每一个属性,找到用户感兴趣的属性,然后把值和属性经过重定位,返回一个TypedArray对象。想要获取某个属性的值则调用相关的方法即可,比如TypedArray.getDrawbale(),TypedArray.getString()等。getDrawable(),getString()方法内部均通过Resources获取属性值。

三、加载资源

在使用资源时首先要把资源加载到内存。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<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中:

最后

这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

相信它会给大家带来很多收获:

img

当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

  • 无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,这四个字就是我的建议!!!
  • 我希望每一个努力生活的IT工程师,都会得到自己想要的,因为我们很辛苦,我们应得的。

当我们在抱怨环境,抱怨怀才不遇的时候,没有别的原因,一定是你做的还不够好!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值