Android插件式换肤以及资源加载流程分析

news2024/11/17 21:45:49

前言

APP更换皮肤的方式有很多,如系统自带的黑夜模式、插件换肤、通过下发配置文件加载不同主题等等,我们这里就浅谈下插件换肤方式。想实现插件换肤功能,我们就需要先弄清楚 :APP是如何完成资源加载的。

资源加载流程

这里我们以ImageView加载图片来进行分析,我们先看下ImageView获取drawable的源码:

   public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
            int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        initImageView();
		...
        final Drawable d = a.getDrawable(R.styleable.ImageView_src);
        if (d != null) {
            setImageDrawable(d);
        }
	...
    }

重点在a.getDrawable(R.styleable.ImageView_src)这段代码,我们继续跟进:
TypedArray.getDrawable()

    public Drawable getDrawable(@StyleableRes int index) {
        return getDrawableForDensity(index, 0);
    }

TypedArray.getDrawableForDensity()

    public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
     		...
            return mResources.loadDrawable(value, value.resourceId, density, mTheme);
        }
        return null;
    }

Resources.loadDrawable()

    Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme) throws NotFoundException {
        return mResourcesImpl.loadDrawable(this, value, id, density, theme);
    }

ResourcesImpl.loadDrawable()

 Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
            int density, @Nullable Resources.Theme theme)
            throws NotFoundException {
            ...
   			//如果使用缓存,先从缓存中取cachedDrawable
            if (!mPreloading && useCache) {
                final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
                if (cachedDrawable != null) {
          		  cachedDrawable.setChangingConfigurations(value.changingConfigurations);
                    return cachedDrawable;
                }
            }
    			...
    			// 重点就是loadDrawableForCookie方法
                dr = loadDrawableForCookie(wrapper, value, id, density);
                ...
            }
    }

从上面我们可以看到,资源加载通过Resources这个类,而它又将任务交给它的实现类ResourcesImpl,我们重点分析下ResourcesImpl.loadDrawableForCookie方法:

  private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
            int id, int density) {
		...
        final String file = value.string.toString();
		...
            try {
            	//加载xml资源,如drawable下定义的shape.xml文件
                if (file.endsWith(".xml")) {
                    final String typeName = getResourceTypeName(id);
                    if (typeName != null && typeName.equals("color")) {
                        dr = loadColorOrXmlDrawable(wrapper, value, id, density, file);
                    } else {
                        dr = loadXmlDrawable(wrapper, value, id, density, file);
                    }
                } else {
                	//通过mAssets(AssetManager类型)打开资源文件流实现加载
                    final InputStream is = mAssets.openNonAsset(
                            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                    final AssetInputStream ais = (AssetInputStream) is;
                    dr = decodeImageDrawable(ais, wrapper, value);
                }
            } finally {
                stack.pop();
            }
        } catch (Exception | StackOverflowError e) {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            final NotFoundException rnf = new NotFoundException(
                    "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
            rnf.initCause(e);
            throw rnf;
           ...
            }
        }

        return dr;
    }

这里我们可以看到最终是交给AssetManager来进行资源文件访问,读取数据流完成资源加载

通过上面源码分析,我们知道可以通过Resources来实现资源加载,那系统中Resources又是如何创建的呢?

Resources创建流程分析

我们在代码中经常这样使用:context.getResources().getDrawable(),那我们就从context的实现类ContextImpl抓起:

### ContextImpl

    public Context createApplicationContext(ApplicationInfo application, int flags)
           throws NameNotFoundException {
   		//  找到createResources方法
           c.setResources(createResources(mToken, pi, null, displayId, null,
                   getDisplayAdjustments(displayId).getCompatibilityInfo(), null));
           if (c.mResources != null) {
               return c;
           }
       }
   }

createResources方法跟进

    private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
           int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo,
           List<ResourcesLoader> resourcesLoader) {
       final String[] splitResDirs;
       final ClassLoader classLoader;
       try {
           splitResDirs = pi.getSplitPaths(splitName);
           classLoader = pi.getSplitClassLoader(splitName);
       } catch (NameNotFoundException e) {
           throw new RuntimeException(e);
       }
       return ResourcesManager.getInstance().getResources(activityToken,
               pi.getResDir(),
               splitResDirs,
               pi.getOverlayDirs(),
               pi.getApplicationInfo().sharedLibraryFiles,
               displayId,
               overrideConfig,
               compatInfo,
               classLoader,
               resourcesLoader);
   }

ResourcesManager的getResources方法:

    public @Nullable Resources getResources(
            @Nullable IBinder activityToken,
            @Nullable String resDir,
            @Nullable String[] splitResDirs,
            @Nullable String[] overlayDirs,
            @Nullable String[] libDirs,
            int displayId,
            @Nullable Configuration overrideConfig,
            @NonNull CompatibilityInfo compatInfo,
            @Nullable ClassLoader classLoader,
            @Nullable List<ResourcesLoader> loaders) {
        try {
           	...
            return createResources(activityToken, key, classLoader);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }

createResources方法如下:

    private @Nullable Resources createResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        synchronized (this) {
        	...
        	// Resources的创建需要resourcesImpl
            ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key);
            if (resourcesImpl == null) {
                return null;
            }
            if (activityToken != null) {
                return createResourcesForActivityLocked(activityToken, classLoader,
                        resourcesImpl, key.mCompatInfo);
            } else {
                return createResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
            }
        }
    }

createResourcesLocked方法如下:

    private @NonNull Resources createResourcesLocked(@NonNull ClassLoader classLoader,
            @NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) {
        cleanupReferences(mResourceReferences, mResourcesReferencesQueue);
        //系统源码中其实就是通过classLoader直接new了一个Resources,并初始化了resourcesImpl方便后续资源加载
        Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                : new Resources(classLoader);
        resources.setImpl(impl);
        resources.setCallbacks(mUpdateCallbacks);
        mResourceReferences.add(new WeakReference<>(resources, mResourcesReferencesQueue));
        if (DEBUG) {
            Slog.d(TAG, "- creating new ref=" + resources);
            Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
        }
        return resources;
    }

通过上面源码分析,我们可以得出结论:在ApplicationContext创建的时候,就完成了Resources的创建,创建是通过ResourcesManager来完成的。

那我们是不是就可以通过创建新的Resources来实现插件中资源的访问呢!!

插件换肤案例

我们先看下Resources的构造方法:

    @Deprecated
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

    /**
     * @hide
     */
    @UnsupportedAppUsage
    public Resources(@Nullable ClassLoader classLoader) {
        mClassLoader = classLoader == null ? ClassLoader.getSystemClassLoader() : classLoader;
    }

    /**
     * Only for creating the System resources.
     */
    @UnsupportedAppUsage
    private Resources() {
        this(null);

        final DisplayMetrics metrics = new DisplayMetrics();
        metrics.setToDefaults();

        final Configuration config = new Configuration();
        config.setToDefaults();

        mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config,
                new DisplayAdjustments());
    }

这里有三个构造方法,由于我们需要加载插件中的资源文件,通过上面的分析,我们知道资源访问是需要通过AssetManager来完成的,因此我们使用Resources(AssetManager assets, DisplayMetrics metrics, Configuration config)这个方式来完成插件资源加载:

 private lateinit var iv: ImageView
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        iv = findViewById<ImageView>(R.id.iv)
        iv.setImageDrawable(getDrawable(R.drawable.b))
        findViewById<Button>(R.id.btn).setOnClickListener {
            //更新皮肤
            updateSkin()
        }

    }

    private fun updateSkin() {
        //反射调用AssetManager的addAssetPath方法
        val assetMangerClazz = AssetManager::class.java
        val assetManger = assetMangerClazz.newInstance()
        //皮肤存放在当前包路径下
        val skinPath = filesDir.path + File.separator + "skin.skin"
        val method = assetMangerClazz.getDeclaredMethod("addAssetPath", String::class.java)
        method.isAccessible = true
        method.invoke(assetManger, skinPath)
        //创建皮肤的Resources对象
        val skinResources = Resources(assetManger, resources.displayMetrics, resources.configuration)
        //通过资源名称,类型,包获取Id
        val skinId = skinResources.getIdentifier("a", "drawable", "com.crystal.skin")
        val skinDrawable = skinResources.getDrawable(skinId, null)
        iv.setImageDrawable(skinDrawable)

    }

测试效果:
请添加图片描述

总结

通过源码分析,了解了资源加载的基本流程,对插件换肤的实现有了进一步的认知。

参考文档

插件式换肤框架搭建 - 资源加载源码分析

结语

如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/22474.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

植物大战僵尸变态辅助开发系列教程(E语言实现和VC6实现)(中)

植物大战僵尸变态辅助开发系列教程&#xff08;E语言实现和VC6实现&#xff09;&#xff08;中&#xff09;26、第一种方法实现变态加速功能27、第二种方法找出变态攻击加速的方法28、加快阳光、金币生产速度29、全屏僵尸29、全屏减速第一课30、全屏减速第二课31、全屏奶油的找…

(Transferrin)TF-PEG-DBCO/TCO/tetrazine 转铁蛋白-聚乙二醇-二苯基环辛炔/反式环辛烯/四嗪

产品名称&#xff1a;转铁蛋白-聚乙二醇-二苯基环辛炔 英文名称&#xff1a;(Transferrin)TF-PEG-DBCO 质量控制&#xff1a;95% 原料分散系数PDI&#xff1a;≤1.05 存储条件&#xff1a;-20C&#xff0c;避光&#xff0c;避湿 用 途&#xff1a;仅供科研实验使用&#xff0c…

Android App开发动画特效中插值器和估值器的讲解以及利用估值器实现弹幕动画实战(附源码和演示视频 可直接使用)

需要图片集和源码请点赞关注收藏后评论区留言~~~ 一、插值器和估值器 插值器用来控制属性值的变化速率&#xff0c;也可以理解为动画播放的速度&#xff0c;默认是先加速再减速。若要给动画播放指定某种速率形式&#xff0c;调用setInterpolator方法设置对应的插值器实现类即可…

Spring Boot 分离配置文件的 N 种方式

今天聊一个小伙伴在星球上的提问&#xff1a; 问题不难&#xff0c;解决方案也有很多&#xff0c;因此我决定撸一篇文章和大家仔细说说这个问题。 1. 配置文件位置 首先小伙伴们要明白&#xff0c;Spring Boot 默认加载的配置文件是 application.properties 或者 application…

【云计算大数据_牛客_Hbase】选择/判断——Hbase

1.Hive 1.下面关于Hive metastore的三种模式的描述错误的是() Derby方式是内嵌的方式,也是默认的启动方式,一般用于单元测试local模式中,使用MySQL 本地部署实现metastoreremote模式为远程MySQLDerby方式在同一时间只能有多个进程连接使用数据库 2. 百度文库 2、代码sel…

Android App开发中集合动画和属性动画的讲解及实战演示(附源码 简单易懂 可直接使用)

需要图片集和源码请点赞关注收藏后评论区留言~~~ 一、集合动画 有时一个动画效果会加入多种动画&#xff0c;比如一个旋转一边缩放&#xff0c;这时便会用到集合动画AnimationSet把几个补间动画组装起来&#xff0c;实现让某视图同时呈现多种动画的效果 因为集合动画和补间动…

Jetson Orin使用Yolo5开源数据集训练模型检测口罩

软硬件环境&#xff1a; 乌班图 20.04 64位蟒蛇与 3.8.10英伟AGX Orin库达11.4PyTorch1.12YOLOv5-6.1感谢开源数据集下载地址 正常都是自己收集完了训练&#xff0c;今天就省略这个步骤了。 如果想自己制作看下面的流程。 软硬件环境搭建教程链接 刷机的话使用官方教程或者…

DNS协议

DNS服务器 人类更喜欢记忆主机名&#xff0c;而路由器更喜欢定长的、有结构层次的IP地址&#xff0c;DNS应运而生&#xff1a;DNS能进行主机名到IP地址转换的目录服务。 DSN是&#xff1a; &#xff08;1&#xff09;一个由分层的DNS服务器&#xff08;DNS server&#xff09;实…

元数据管理-解决方案调研二:元数据管理解决方案——Saas/内部解决方案(2)

Saas/内部解决方案 2.5、Azure Purview 地址&#xff1a;Azure Purview - Unified Data Governance Solution | Microsoft Azure 特点&#xff1a; 1、创建跨整个数据资产的统一数据地图&#xff0c;为有效的数据治理和使用奠定基础 1.1、自动化和管理混合源的元数据&#xf…

重打包实现frida持久化 笔记

修改Dex Using Frida on Android without root 修改so [翻译]在未root的设备上使用frida 2个方法本质都是通过重打包让app自己加载frida-gadget.so &#xff08;但感觉没有谁家app会让人轻易重打包吧。。。&#xff09; apktool d org.telegram.messenger_4.8.4-12207.apk -r…

Tomcat配置SSL证书别名tomcat无法识别密钥项

Tomcat配置SSL证书一直启动失败&#xff0c;主要问题如下&#xff1a; java.io.IOException: Alias name tomcat does not identify a key entry at org.apache.tomcat.util.net.jsse.JSSEUtil.getKeyManagers(JSSEUtil.java:280) 别名tomcat无法识别密钥项&#xff0c;是因…

STM32单片机远程控制大棚种植系统

想要更多项目私wo!!! 一、电路设计 ​​​​​系统示意图硬件系统 系统由五个单片机系统组成的&#xff0c;其中51系列的单片机四个&#xff0c;STM32F407单片机一个&#xff0c;各个子系统之间通过NRF24L01无线模块进行通信。 系统硬件组成框图​​​​主控制板主要由STM32…

【VuePress2.0】快速开始(不用)

文章目录VuePress2.x1.1 安装安装VuePress2.x&#xff08;手动安装&#xff09;1.2 VuePress2.x 基本操作VuePress2.x主题2.1 安装VuePress2.x主题&#xff08;yarn&#xff09;2.2 报错解决&#xff08;没效果&#xff0c;不用看&#xff09;2.3 VuePress2.x主题 基本操作VueP…

Java基础深化和提高-------IO流

目录 IO流技术介绍 什么是IO 什么是数据源 流的概念 第一个简单的IO流程序 IO流的经典写法 IO流新语法经典写法 Java中流的概念细分 按流的方向分类&#xff1a; 按处理的数据单元分类&#xff1a; 按处理对象不同分类&#xff1a; Java中IO流类的体系 Java中IO的四大抽象…

CleanMyMac X真正好用的Mac电脑系统优化软件应用工具

最用户友好的Mac问题修复程序。删除系统垃圾、不需要的应用程序和恶意软件&#xff0c;并调整您的Mac以获得最高速度。对于速度较慢的计算机&#xff0c;CleanMyMac就能立即使用。 CleanMyMac2023之所以能够获得众多Mac的老用户们的喜爱&#xff0c;是因为其有着非常强大功能提…

数据可视化之大数据平台可视化

一 前言 在简化数据量和降低大数据应用的复杂性中&#xff0c;大数据分析发挥着关键的作用。可视化是其中一个重要的途径&#xff0c;它能够帮助大数据获得完整的数据视图并挖掘数据的价值。大数据分析和可视化应该无缝连接&#xff0c;这样才能在大数据应用中发挥最大的功效。…

nodejs+mysql航空飞机票销售酒店预订系统vue

(1)对机票预订管理系统进行需求分析、确定所需要的模块&#xff1b;建立数据字典、数据流等&#xff1b;书写可行性分析和需求分析说明书。 (2)对机票预订管理系统进行概要设计&#xff1a;建立软件体系结构&#xff0c;画出用例图、E-R图等&#xff1b;书写数据要求说明书和各…

【雷达通信】阵列信号处理(Matlab代码实现)

&#x1f468;‍&#x1f393;个人主页&#xff1a;研学社的博客 &#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜…

聊聊Go里面的闭包

以前写 Java 的时候&#xff0c;听到前端同学谈论闭包&#xff0c;觉得甚是新奇&#xff0c;后面自己写了一小段时间 JS&#xff0c;虽只学到皮毛&#xff0c;也大概了解到闭包的概念&#xff0c;现在工作常用语言是 Go&#xff0c;很多优雅的代码中总是有闭包的身影&#xff0…

嵌入式图形开发框架Qt——让牙科手术开始迈入机器人时代

Neocis软件工程总监,Jim Tieman&#xff1a; “我们Neocis是一家致力于提高科医生能力和促进病人护理的牙科机器人公司。之前我们有一个Microsoft Foundations Class (MFC)的应用程序&#xff0c;由于现在团队擅长MFC开发的技术员并不多&#xff0c;维护起来也很费劲。之前这个…