【Android】自定义换肤框架02之自定义AssetManager和Resource

ResourceId是如何变成对应Resource的

在上一章中,我们已经讲过,apk中有个资源索引文件

其中保存了每个资源对应的id,name,type,path

资源文件的解析,主要涉及两个类,AssetManager和Resource

  • AssetManager,用于管理apk中的原生资源文件,包括asset和resource
  • AssetManager通过调用addAssetPath方法,来添加提供资源的apk
  • addAssetPath默认使用的是context.packageResourcePath,及当前安装包的位置
  • 如果想加载其它apk里面的资源,就得自定义AssetManager
  • AssetManager的构造函数是因此功能,必须通过反射才能自己创建新的实例
  • Resource,用于管理resource文件夹下的资源,如color,drawable等
  • Resource解析资源前,首先要拿到apk中的资源索引文件,和屏幕信息,配置信息
  • Resource对象的构建依赖于AssetManager,DisplayMetrics,Configuration三个对象
  • 如果我们想从其它apk中加载资源,则需要提供自定义的AssetManager给Resource
  • 由于DisplayMetrics和Configuration信息是固定的,因此不需要自定义
设计思路
  • 当我们想根据皮肤去替换某个资源时,在skin.apk中创建一份同名,但内容不同的资源
  • 自定义SkinnerAssetManager,并绑定skin.apk
  • 自定义SkinnerResources,并绑定SkinnerAssetManager
  • 相同名称的资源,在不同apk中的id是不一样,但我们可以通过name+type+package的方式去找到对应的id
  • 通过OriginResourceId+OriginResources,得到name+type+package
  • 通过SkinnerResources,以及name+type+package,拿到SkinnerResourceId
  • 通过SkinnerResources+SkinnerResourceId,解析出skin.apk中的color或drawable
  • 由于并不是所有属性都会跟随皮肤而变换,因此SkinnerResourceId有可能不存在
  • 如果SkinnerResourceId不存在,则使用OriginResources去加载原来的资源,这样大致实现了资源的自动加载
自定义SkinnerAssetManager
kotlin 复制代码
package com.android.library.skinner

import android.app.Application
import android.content.res.AssetManager
import android.content.res.Resources
import android.graphics.drawable.Drawable

@Suppress("Deprecated")
object SkinnerAssetManager {

    lateinit var context: Application
    lateinit var assetManager: AssetManager
    lateinit var skinnerResources: Resources
    lateinit var originResources: Resources

    fun init(application: Application, resourcePath: String) = apply {
        context = application
        createHookedAssetManager(resourcePath)
    }

    private fun createHookedAssetManager(resourcePath: String) {
        val assetManager = AssetManager::class.java.newInstance()
        val method = AssetManager::class.java.getDeclaredMethod("addAssetPath", String::class.java)
        method.invoke(assetManager, resourcePath)
        this.originResources = context.resources
        val resources = Resources(assetManager, originResources.displayMetrics, originResources.configuration)
        this.assetManager = assetManager
        this.skinnerResources = resources
    }

    fun skinResId(resId: Int): Int {
        return skinnerResources.getIdentifier(
            originResources.getResourceName(resId),
            originResources.getResourceTypeName(resId),
            originResources.getResourcePackageName(resId)
        )
    }

    fun skinColor(resId: Int): Int {
        val skinResId = skinResId(resId)
        if (skinResId > 0) {
            return skinnerResources.getColor(skinResId)
        }
        return originResources.getColor(resId)
    }

    fun skinDrawable(resId: Int): Drawable {
        val skinResId = skinResId(resId)
        if (skinResId > 0) {
            return skinnerResources.getDrawable(skinResId)
        }
        return originResources.getDrawable(resId)
    }
}
拷贝测试皮肤包到存储卡

这里我们将测试包放在asset文件夹里面,在应用启动时拷贝到存储卡,从而省去人工操作

kotlin 复制代码
private fun copySkinPackage() {
    val fis = application.assets.open("skin.apk")
    val fos = FileOutputStream("sdcard/skin.apk")
    val buffer = ByteArray(fis.available())
    fis.read(buffer)
    fos.write(buffer)
}
通过指定皮肤包初始化SkinnerAssetManager
kotlin 复制代码
SkinnerAssetManager.init(application, "sdcard/skin.apk")
使用自定义的SkinnerAssetManager加载资源
kotlin 复制代码
val drawable = SkinnerAssetManager.skinDrawable(R.drawable.icon_app)
binding.image.setImageDrawable(drawable)
十万个为什么

到目前为止,我们已经实现了从指定apk中加载同名资源

下一步问题是,如何让Activity/Fragment/View/Xml使用SkinnerResources,而不是默认的OriginResources

且听下回分解!

相关推荐
千里马学框架3 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台3 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone3 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc3 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo3 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077003 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼3 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone3 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen3 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone3 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui