嗨,你终于来啦 ~ 等你好久啦~ 喜欢的小伙伴欢迎关注,我会定期分享Android知识点及解析,还会不断更新的BATJ面试专题,欢迎大家前来探讨交流,如有好的文章也欢迎投稿。
一、Res资源加载流程
应用资源加载的过程 主要涉及两个类: Resource只与应用程序交互,负责加载资源的管理等等;AssetManager负责res目录中所有的资源文件,打开文件,并读取到内存中。
当使用Context.getDrawable()方法 通过资源ID 生成一个Drawable对象时,最终会调用到Resource的getDrawable(...)方法。
public Drawable getDrawable(@DrawableRes int id, @Nullable Theme theme)
throws NotFoundException {
return getDrawableForDensity(id, 0, theme);
}
内部函数调用如下:
ResourcesImpl 是Resource内部的一个静态代理类,实际负责与AssetManager的交互。
在loadDrawableForCookie() 方法中真正开始加载资源,假如该id 对应的是一个xml文件,则开始xml解析,假如该id对应的一个图片文件,则调用AssetManager打开文件。
AssetManager实际上调用Native方法打开文件。
public @NonNull InputStream openNonAsset(...) {
synchronized (this) {
final long asset = nativeOpenNonAsset(mObject, cookie, fileName, accessMode);
final AssetInputStream assetInputStream = new AssetInputStream(asset);
return assetInputStream;
}
}
二、AssetManager添加Res目录
要使用AssetManager可以打开res目录中资源文件,必须把res路径添加到AssetManager的path中。这里主要分两步:添加系统资源 路径 和 apk资源文件路径。
第一,添加系统资源。在程序进程创建时,由zygote进程调用createSystemAssetsInZygoteLocked(...)方法,添加到AssetManager中。FRAMEWORK_APK_PATH 即为系统资源路径。
private static final String FRAMEWORK_APK_PATH = "/system/framework/framework-res.apk";
/**
* This must be called from Zygote so that system assets are shared by all applications.
*/
@GuardedBy("sSync")
private static void createSystemAssetsInZygoteLocked() {
try {
final ArrayList<ApkAssets> apkAssets = new ArrayList<>();
apkAssets.add(ApkAssets.loadFromPath(FRAMEWORK_APK_PATH, true /*system*/));
loadStaticRuntimeOverlays(apkAssets);
sSystemApkAssetsSet = new ArraySet<>(apkAssets);
sSystemApkAssets = apkAssets.toArray(new ApkAssets[apkAssets.size()]);
sSystem = new AssetManager(true /*sentinel*/);
sSystem.setApkAssets(sSystemApkAssets, false /*invalidateCaches*/);
} catch (IOException e) {
throw new IllegalStateException("Failed to create system AssetManager", e);
}
}
复制代码
第二,添加apk资源目录。应用程序进程启动后,由AMS调用 创建Application时,会间接调用到ResourcesManager中的createAssetManager()方法,创建AssetManager对象时,添加apk资源相关目录。
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key){
final AssetManager.Builder builder = new AssetManager.Builder();
......
if (key.mLibDirs != null) {
for (final String libDir : key.mLibDirs) {
if (libDir.endsWith(".apk")) {
try {
builder.addApkAssets(loadApkAssets(libDir, true /*sharedLib*/,
false /*overlay*/));
} catch (IOException e) {
}
}
}
}
......
return builder.build();
}
而在AssetManager 中添加Res目录 正是应用换肤功能得以实现的第一步,只有将皮肤包的Res文件路径 添加到 AssetManager的Path中,应用才有可能获取到皮肤包内资源文件。
三、Resource包装流 解决方案
这个方案的思路在于拦截应用中 对于Resource对象的操作。即拦截ContextImp中的Resource对象。
1, 创建Resource对象
创建AssetManager对象,并将皮肤包资源路径添加到 AssetManager的Path数组中。(AssetManager.addAssetPath(...)方法为隐藏方法,需要反射调用)
private final static String ADD_ASSET_PATH = "addAssetPath";
private String loadSkin(String skinFile) {
......
//加载该皮肤资源
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = AssetManager.class.getMethod(ADD_ASSET_PATH, String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, skinFile);
...
}
使用AssetManager 和 默认Resource配置,创建Resource对象。
Resources resources = new Resources(assetManager,
sysResource.getDisplayMetrics(), sysResource.getConfiguration());
2,替换系统Resource对象
以Activity为例, 在Activity的attachBaseContext(Context newBase)方法回调时,使用反射替换newBase中 Resource对象实例。
private final static String CONTEXT_IMPL_CLASS_NAME = "android.app.ContextImpl";
private final static String CONTEXT_IMPL_FIELD_NAME = "mResources";
/**
* @param contextImp 替换ContextImp对象中的Resource对象
*/
public void createActivityResourceProxy(Context contextImp) {
try {
@SuppressLint("PrivateApi")
Class<?> clazz = Class.forName(CONTEXT_IMPL_CLASS_NAME);
Field field = clazz.getDeclaredField(CONTEXT_IMPL_FIELD_NAME);
field.setAccessible(true);
if (mResource == null) {
mResource = new MResource(mSkinResource);
}
field.set(contextImp, mResource);
} catch (Exception e) {
e.printStackTrace();
}
}
MResource为第一步中创建Resource的包装类
public class MResource extends Resources {
private SkinResource mSkinResource;
public MResource(SkinResource skinResource) {
super(...);
mSkinResource = skinResource;
}
@Override
@NonNull
public CharSequence getText(int id) throws NotFoundException {
Resources resource = mSkinResource.getRealResource(id);
int realUsedResId = mSkinResource.getRealUsedResId(id);
return resource.getText(realUsedResId);
}
......
复制代码
3,运行时动态映射
资源文件在编译打包后会生成一张资源表resourse.arsc, 将具体的资源文件与资源表中 ID一一对应。运行时,在由AssetManager根据资源表加载相应文件。但皮肤包中相同资源打包编译后,相同资源文件在资源表中 对应的ID却不一样。
[图片上传中...(image-2eed34-1564489959774-0)]
<figcaption></figcaption>
为解决这个问题,可以通过动态映射找出皮肤包中 对应的资源Id,原理是因为相同资源在不同的资源表中的Type和Name一样。
private int findSkinResId(int resId) {
//通过资源的 Name和Type,动态映射,找出皮肤包内 对应资源Id
Resources sysResource = mContext.getResources();
//资源名称 sample.jpg
String resourceName = sysResource.getResourceEntryName(resId);
//资源类型: drawable
String resourceType = sysResource.getResourceTypeName(resId);
int skinResId = mSkinResource.getIdentifier(resourceName, resourceType, mSkinPackageName);
if (skinResId > 0) {
//皮肤包内找到 对应资源
return skinResId;
}
return FLAG_RESOURCE_NOT_FOUND;
}
4,xml布局解析问题
通过以上步骤,在Activity中通过getResource().getDrawable(resId)方法 即可得到皮肤包中的Drawale,但是写在xml 布局文件中的资源却不能通过代理Resource加载。
写在布局中的资源,在xml解析后创建控件后,由TypedArray 解析加载资源。
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
......
}
TypedArray 解析加载资源 方法,比如getDrawableForDensity(...) 使用的是 mResources.loadDrawable(value, value.resourceId, density, mTheme)方法,绕过了我们设置代理。因此加载是应用中的资源文件。
@Nullable
public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
......
final TypedValue value = mValue;
if (getValueAt(index * STYLE_NUM_ENTRIES, value)) {
if (density > 0) {
mResources.getValueForDensity(value.resourceId, density, value, true);
}
return mResources.loadDrawable(value, value.resourceId, density, mTheme);
}
return null;
}
5,设置xml布局解析监听
为解决布局解析中资源加载的问题,我们可以使用自定义控件的方法, 使用Resource 加载资源 代替 TypeValue类。
通过为Activity添加布局解析监听, 全局替换自定义控件
//为当前Activity设置 布局解析监听
LayoutInflater inflater = LayoutInflater.from(activity);
LayoutInflaterCompat.setFactory2(inflater, new SkinFactory(activity));
public class SkinFactory implements LayoutInflater.Factory2 {
......
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = null;
switch (name) {
case "RelativeLayout":
view = new SkinnableRelativeLayout(context, attrs);
verifyNotNull(view, name);
break;
case "TextView":
view = new SkinnableTextView(context, attrs);
verifyNotNull(view, name);
break;
}
return view;
}
自定义控件 继承ISkinnableView接口,并实现updateSkin()方法,在换肤后,全局改变资源。
@Override
public void updateSkin() {
SkinManager skinManager = SkinManager.getInstance();
//设置字体颜色
key = R.styleable.SkinnableTextView[R.styleable.SkinnableTextView_android_textColor];
int textColorResourceId = attrsBean.getViewResource(key);
if (textColorResourceId > 0) {
ColorStateList color = skinManager.getColorStateList(textColorResourceId);
setTextColor(color);
}
}
相对于AssetManager替换流 解决方案来说,Resource包装流 解决方案实现相对简单,但是却复杂很多,需要实现自定义控件等待。
四、AssetManager替换流 解决方案研究
相对于Resource包装流 替换系统Resource对象,AssetManager替换流的方案是 直接hook 系统的AssetManager对象。从而更优雅的解决加载资源的问题。
1, hook系统AssetMananger对象
AssetManager能解析并加载到资源的原因 在于 系统资源路径及 应用的资源路径 都添加到了AssetManager的Path当中。
我们可以直接将皮肤包中的资源文件 也添加到系统AssetManager的Path数组当中。
public void addSkinPath(Context context, String skinPkgPath) throws Exception {
PackageManager packageManager = context.getPackageManager();
packageManager.getPackageArchiveInfo(skinPkgPath,
PackageManager.GET_SIGNATURES | PackageManager.GET_META_DATA);
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP){
addAssetPath.invoke(assetManager, context.getApplicationInfo().publicSourceDir);
addAssetPath.invoke(assetManager, skinPkgPath);
}else {
//5.0以上,需要將assets 资源文件单独添加
File assetsFile = new File(skinPkgPath);
// File assetsFile = Utils.generateIndependentAsssetsForl(new File((skinPkgPath)));
addAssetPath.invoke(assetManager, skinPkgPath);
addAssetPath.invoke(assetManager,context.getApplicationInfo().publicSourceDir);
addAssetPath.invoke(assetManager,assetsFile.getAbsolutePath());
}
}
在Activity 中attachBaseContext(Context newBase)方法中,将系统的context 替换成我们自己的context。
public Context wrapperContext(Context context) {
return new SkinContextWrapper(context);
}
public Context unWrapperContext(Context context) {
if (context instanceof ContextWrapper) {
return ((ContextWrapper) context).getBaseContext();
}
return context;
}
2, 编译期静态对齐
与Resource包装流类型, 使用AssetManager替换方案 也存在资源表中文件不对应问题。但是由于后者是直接使用 AssetManager读取资源文件,因此不能使用动态映射方案,只能使用在程序编译时,修改Resources.arsc文件。 将皮肤包中资源文件 对应的id数值 修改与应用程序中 一致。
大概思路是可以通过定制 AAPT程序来实现,但是很遗憾,目前这只是一种思路。
五,总结
Resource替换流的解决方案,Github中已有一个开源项目:github.com/ximsfei/And…
但是对于后一种方案,暂未找到相关实现。
本文相关代码见:github.com/deanxd/skin…
希望读到这的您能转发分享和关注一下我,以后还会分享Android知识点及解析,您的支持就是我最大的动力!!
网友评论