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

  • 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中:

@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数组资源,例如:

@drawable/sym_def_app_icon @drawable/arrow_down_float @color/hint_foreground_dark @color/hint_foreground_light

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

private static final LongSparseArray<Drawable.ConstantState> sPreloadedDrawables = new LongSparseArray<Drawable.ConstantState>();
private static final LongSparseArray sPreloadedColorStateLists = new LongSparseArray();
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()方法代码如下所示:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

答应大伙的备战金三银四,大厂面试真题来啦!

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

《960全网最全Android开发笔记》

《379页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

如何使用它?
1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数

《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

腾讯、字节跳动、阿里、百度等BAT大厂 2020-2021面试真题解析

资料收集不易,如果大家喜欢这篇文章,或者对你有帮助不妨多多点赞转发关注哦。文章会持续更新的。绝对干货!!!

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

423157852)]

腾讯、字节跳动、阿里、百度等BAT大厂 2020-2021面试真题解析

[外链图片转存中…(img-62gEvwNF-1712423157852)]

资料收集不易,如果大家喜欢这篇文章,或者对你有帮助不妨多多点赞转发关注哦。文章会持续更新的。绝对干货!!!

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值