前言:

在App开发中,通常需要涉及到皮肤/主题的逻辑,以保证程序风格的多样化,保证符合大多数用户的不同口味。而一个优秀的皮肤管理框架不仅有利于开发者优雅的coding,同时需要做到能够动态切换皮肤,动态更换下载皮肤。

本篇内容从公司产品当前主题管理器的设计引入一种新型的皮肤管理方案。以期解决现有多主题编程时的各种难受,同时能够带来更好的皮肤动态性。

正题:

一. 皮肤/主题资源有哪些?

常用的主题资源有:背景色彩,文字色彩,图片背景,控件样式,字体样式,字体大小,控件尺寸,控件margin-padding.等。

二. 常见的皮肤切换方案有哪些?

1. 对于小型APP来说,设计一套功能完善的皮肤管理框架 稍显多余,因此通常都是以系统自带的 styleable主题切换方式为主。

此种方式,优点是不需要做额外的开发,只需要定义不同的theme样式,并在需要时通过context重新设置Theme资源。缺点也很明显,当资源内容成百上千时,多套主题的资源声明都集中在同一个APP中,且因为android的res 目录不能随意自定义文件夹,导致资源混乱,命名混乱,不太适合大型的APP应用。 同时切换主题时还需要重启Activity才能生效。

2. 我们的工程(某C)采用的方案是将资源打包在assets目录中的形式来解决多套皮肤资源的目录结构化,即assets中对每一套皮肤都可以有完整的自定义目录结构,以便于管理。同时我们工程中实现了类似系统Resource的资源定位方式,提供一系列API用于获取当前皮肤资源。不同的是,系统Resource对象需要的参数是 R.color.xxxx 这类常量声明,而我们工程中则需要以资源名称字符串进行访问。

此种实现方式,解决了多套皮肤的目录管理的问题,每套皮肤配置独立的目录结构,便于资源管理。但也存在如下缺点:

1.字符串查询资源;2. 资源文件(特别是包含图片)在多个业务中迅速膨胀。3: 无法动态下发丰富的资源包。

资源访问使用字符串形式的调用在开发过程中非常难受。同时在开发过程中还遇到以下非常让人难受的事情:

考虑以下场景:对于一个稍微复杂一点的布局, 且是以xml 形式存在的。 第一点较难受的事情 是,因为皮肤资源并不是在常规的res目录中存在的,因而xml中无法直接给出每个控件相应的默认资源值,即无法像引用@color/xxx等形式编写,这就导致,编写完xml后无法即时预览整个界面,必须要在代码中进行动态赋值并编译运行才可看到效果。

但令人烦恼的是,我们工程一旦改动资源文件如xml,就必须进行全量编译,编译时长>10min! 也就是从写完一个界面,到真正看到其运行效果需要无脑等待10min。  此时,你意外的发现,某个控件的尺寸与实际背景不太匹配,需要进行微调,怎么办? 再等10min钟吧!   这个过程在很长一段时间都困扰每个开发者,因此我们工程的代码中出现 了一个有趣的现象,大家都开始喜欢上直接用代码来coding一个界面,纯代码声明所有控件并进行复杂的属性设置,容器嵌套。因为这样就能够进行增量编译,时间成本将缩短到秒级。但类大小却在极速膨胀。

然而还没完,虽然使用代码进行编写各种控件了,但为了适配皮肤切换,我们还需要单独将所有需要适配各种模式的控件全部进行属性命名,并在主题变更监听中对所有控件进行资源获取/赋值操作。即使只是一根用做分隔线的View. 试想一个稍微复杂一点的界面,需要用多少代码来初始化控件,又需要多少代码来对主题切换负责?? 本来一个xml 就能解决的事情,却因为这样那样的原因写了一个庞大的自定义View类。而里边还没有多少业务逻辑。怎么想都不划算。

再有一点就是主题资源对包size的影响,每适配一套主题,就需要包含其各种图片资源,而我们工程中默认自带N>5套皮肤资源,如果要真正每个业务都去适配这多么皮肤,除去color资源不说,drawable资源的开销将成为很大问题。

3. 还有一种皮肤切换方案是基于分离apk的形式,类似于系统sdk自带的framework-res.apk,其中的所有资源我们都可在代码中直接使用,原因是这个apk中的所有资源ID都是永久固定的,也就是说在编译期和调用期都有唯一确定的常量值。但在新的Android 编译中,系统废弃了 publics.xml 的使用,也即我们无法再通过外部配置来固定我们的资源ID, 这样会导致不同的编译过程对于同名的资源无法保证其生成ID的唯一性。 简单讲,就是我们在主工程中通过一个资源常量没办法找到另一个apk中的同名资源值,就会出现运行时牛头不对马嘴的资源设置。

但分离式皮肤资源的方案也有着明显的优势,皮肤与主工程分离,可各自独立开发,甚至如果UI 工程师进行简单的android 布局学习就可以完全自己实现所有的UI界面。同时皮肤资源可以动态下发更换,进行独立的版本管理。 如果能解决分离apk 编译时资源ID变更带来的技术问题,那么工程开发将变得简单,你只需要在主工程中放入默认皮肤资源内容,并正常开发,完全不需要考虑如何获取不同的皮肤资源。

解决ID不唯一的方案是,通过资源名称反射R.class. 进行ID查找,但无法使用R.color.xxx的形式始终都觉得非常遗憾。隐形的导致开发者在使用资源的方式上不能以常规方式去使用,成本增加,并且通过反射方式调用的效率成本也很高。最后一点:R文件不能被混淆。

皮肤方案正确的姿势?

如果要问Developer,什么才是使用皮肤的正常姿势?他肯定会回答,没有皮肤适配才是最爽的!但很显然,从上面总结的一些皮肤方案中我们看到每种方案都不可避免的因为要考虑皮肤的存在而做出的各种妥协。明明可以使用R.color.xx但却不能用。 明明知道harcode字符串的方式去寻找资源并不明智,但却只能硬着头皮写着自己不喜欢的style.

那么有没有一种方案能够解决以上难题呢?答案绝对是可以的!

我们要解决哪些问题?

      1:我们不希望不同的皮肤资源混杂在一起放在res目录中来通过命名来进行区分,因此不打算采用第一种系统Theme方案。

      2:我们不希望资源放在assets中,通过实现复杂的资源查找/管理API,系统的Resource资源寻索接口总是要比自己实现的更加健壮可靠。

      3:我们也不希望明明能够使用Xml编写布局并且预览的正规写法却被一个皮肤方案逼迫的不得不去手工coding出一个界面。

      4:我们不希望同一个界面可能因为主题的不同而需要有不同的布局时,在主工程中去实现多个主题的界面,然后在代码中去进行if..else.

      3:我们也不希望使用字符串的形式去艰难的处理着主题变更时要去重新对每个控件进行资源赋值的难看的代码。

基于以上各种不满足和贪婪,我们需要重新定义皮肤的切换方案!

方案的核心思路:仍然采用分离资源APK的形式,但我们需要解决不同APK在编译时可能会出现的同资源却不同ID的问题。

一旦能够解决以上问题,那么我们在开发时,完全可以只考虑主工程的资源,而不需要考虑有哪些主题。从使用形式上也会变成普通的setXXXResource(id)的形式。

如何解决呢?方案有3:

  方案一:重写AAPT,恢复对public.xml固定ID的支持。同时,改写/增加编译步骤,在每次编译完成对R资源的汇总后,同时生成一份publics.xml资源列表,当然每次生成publics ID时需要跟上一次生成的文件进行资源合并,也就是只合并新增的资源ID。即资源ID的生成是固定递增式的。

有人可能会问了,如果要删除一些过时的资源时又该怎么办。dang~~, 我们的自定义编译步骤需要处理双向的 publics.xml文件同步。直接从publics.xml清理掉已删除的ID即可。

方案难度:3颗星。 本方案主要的难点是在aapt 工具的改造上,因android的各种工具都是开源的,源码随处可以拿到,但因为是C++写的,二次开发编译过程都要有专业的人才能搞定。如果你只是一个老老实实的java coder,请放弃此方案。 另外一个难点是编写gradle task,以处理每次编译时的publics 资源合并。

方案二:在不同apk中,虽然同名称的资源生成的ID值可能不同,但我们仍然可以通过资源名称进行访问,也就是说这里的资源名称将是我们优化资源查找的重要途径。如何处理好资源映射将是本方案的核心逻辑。 那么如何处理呢?很简单,我们只需要在R.java生成的时候,插入一个编译步骤,将资源名称<->ID进行读取,并生成一张映射表文件,写入到assets目录中去。同时对皮肤资源apk的编译也应用相同的编译步骤,那么当两个app都存在一个资源映射表的时候,我们就可以通过主工程的ID值反射推断出皮肤APK中同名资源的ID值,进而就可能直接通过Resource对象的 getColor(int id)/getDrawable(int id) 这样的接口进行资源查找了。

举个例子: 在主工程中,某颜色资源 R.color.color_bg_main = 0x7f040050.  而 在皮肤apk 编译时同名的资源R.color.color_bg_main = 0x7f040001, 如果我们直接拿0x7f040050 去皮肤apk中获取一个颜色值时,很显然无法得到我们想要的。 如果此时我们有两张hashMap, 分别保存了两个apk 中所有的资源名称->ID的映射。那我们就可以很方便的用0x7f040050 反向推断出某皮肤apk中的同名资源的ID, 也就可以达到目标。

方案难度:2颗星。 本方案的实现难点就在于编写合适的编译任务,以处理R文件生成后,同时生成一个map映射文件的逻辑。为了使这个编译脚本通用于所有工程,我们还可以将其做成gradle 插件的形式,一旦我们进行正常的工程编译,插件就自动帮我们在合适的位置插入一个gradle task。

插件已经进行实现,并且已上传jcenter, 你需要做的只是在主工程中以及各皮肤工程中 apply plugin 即可。具体依赖方式:

     1:root: build.gradle 中增加(省略了原有内容,只标出增加行)        buildscript{ repositories{                maven {url "https://dl.bintray.com/andrewlu1/maven/"}          }          dependencies{ classpath "cn.andrewlu.plugins:skinplugin:+"  }        }      2: project: build.gradle中 apply plugin: 'cn.andrewlu.plugins.skinplugin'

完成以上依赖,只需要重新build一次工程即可,你会发现在工程的assets目录中多了一个skin/data文件。 这个文件存储的就是所有资源字符串的 hashCode(int)+ID(int) 的二进制内容。因此不要改动此文件。 有了这个文件,我们在加载皮肤apk 并生成Resource对象的时候,就可以通过openAssets 读取此文件,并解析成hashMap.  剩下的工作就是实现一个ResourceManager,用于管理这多套皮肤对应的映射表,并在调用getResource().getColor(int)值时进行映射查找。  具体实现也已经完成,以jar的形式进行依赖,目前精力有限,方案还不完善,就不贴出来坑杀大家了。

方案三: 同样是为了解决资源ID映射的问题,因为每个apk在编译时都会生成R.class, 而R.class中则又存储着所有资源的ID值,因此如果我们能够在运行时,从另一个apk中反射拿到这个class, 并将所有的fieldName->ID值生成键值对映射,主工程同时也生成R.class的field->ID的映射, 那么原理就变得与上一方案相似了。

同时这里并不需要增加额外的编译步骤。这比每次查找资源时都用反射去获取资源ID的做法显然更加高效,因为只需要加载时反射一次,就会将所有资源映射缓存起来,这样每次查找资源仅是对hashmap的查找,并且可能简单的根据hashmap中的查找结果判断皮肤apk中是否声明了此资源,如果不存在此资源,就直接使用默认主工程的资源项。就不需要在每次获取资源时都来个try{}catch()【因为无法确认资源是否存在】

难度:1个星。 实现难点就是要能够获取到这些apk中的正确的R.class,  因为一些特殊的定制化开发可能会生成另类的R.class路径。你就完全不能通过package+R.class来定位这个文件了。

另一点,因为要反射R.class, 那么你就无法对其进行混淆了。实际上这点非常恐怖,正常的R文件会将 support包中的资源,以及一些系统资源一并包含在内,导致R中有几千个资源ID. 类大小也可能增长到上百K. 但实际上在代码中引用的R的属性 是会在编译时直接替换为常量数字的,也就是R并不会有代码引用,在混淆时完全可以将R.class精简到几KB 大小的。 优点是胜在思路简单,只要明白原理,基本上都能够写出一个ResManager管理类来实现ID的映射查找。

后记:

以上方案都在闲暇之余进行了一番探索和验证,且都是切实可行的。在此之后,皮肤的开发将能够与主代码工程的开发分离并行。并在合适的时候实现皮肤的动态下发,版本管理等常规逻辑即可。

但其实还有更多...... 

上面有提到过,最好的皮肤适配是:没有皮肤。

这里并不是说不需要皮肤模块,而是能够让开发者在开发过程中完全不需要考虑皮肤的适配逻辑,比如每个界面要监听主题的变更,然后重新对skinnalbe UI 进行资源获取/赋值。这个逻辑是否能沉入到皮肤框架中去呢?也就是不需要开发者自行监听并处理,能否做到框架层级的处理过程中去?

开发者只需要在xml中或者代码中直接对控件设置相应的属性ID如:setBackgroundResource(resID)的调用,此后不论皮肤如何切换,都不需要开发再去调用一次同样的设置以进行刷新。

答案仍然是YES!!!YES!!!YES!!!

一种做法是:重写所有的系统UI控件,并在attachToWindow时添加监听,dettachFromWindow时移除监听即可。同时需要重写类似setBackgroundResource()这样的API,以便将传入的ID值转换为皮肤apk中的ID值,然后获取相应的资源内容并赋值。  优点是,我们在xml中进行资源引用这些二次开发的UI控件,而在资源引用时使用@color/xxx这样的ID即可完成对不同皮肤的适配。并不需要特意关注皮肤变更的处理逻辑。

缺陷当然也有:系统控件那么多,重写工作完全变成了体力劳动。且不同的View可能有不同的资源赋值接口,都一一重写实现的话size将迅速膨胀。

再有一种方案是,不需要重写UI控件,该用什么控件还是什么控件。但需要解决两个问题:1:在xml中引用的@color/xxx的资源,如何在初始化xml布局的时候使其关联调用皮肤的资源获取接口,以达到直接显示皮肤样式的效果。 2:如何在代码中调用类似setBackgroundResource的时候关联调用皮肤资源的获取接口。

有关这个问题的解决办法,可以参考系统中为兼容旧版本而内部实现的各种Compat类。具体方案留给各位看客。

------------兴之所致,趣从中来----------

:致最后这段惨然的时光。【2018. 01 黑色星期五】

分离式皮肤资源解决方案探索相关推荐

  1. 分享一款博客园皮肤及其解决方案

    分享一款博客园皮肤及其解决方案 参考文章: (1)分享一款博客园皮肤及其解决方案 (2)https://www.cnblogs.com/vvjiang/p/8655963.html 备忘一下.

  2. 基于webpack的前端工程化开发解决方案探索(一):动态生成HTML

    基于webpack的前端工程化开发解决方案探索(一):动态生成HTML 参考文章: (1)基于webpack的前端工程化开发解决方案探索(一):动态生成HTML (2)https://www.cnbl ...

  3. 有赞 Flink 实时任务资源优化探索与实践

    简介:目前有赞实时计算平台对于 Flink 任务资源优化探索已经走出第一步. 作者|沈磊 随着 Flink K8s 化以及实时集群迁移完成,有赞越来越多的 Flink 实时任务运行在 K8s 集群上, ...

  4. 复旦MBA第二学位:畅享顶尖国际商科资源,探索全球发展新可能

    自2009年以来,复旦MBA项目一直致力于与顶级院校开展合作,拓宽学生的国际视野.目前,复旦MBA项目与三所国际顶尖合作院校达成了第二学位项目的合作:美国麻省理工学院斯隆管理学院管理学硕士学位(Mas ...

  5. 食品安全溯源区块链解决方案探索

    2019独角兽企业重金招聘Python工程师标准>>> 食品安全溯源区块链解决方案探索 本文节选自电子书<Netkiller Blockchain 手札> Netkill ...

  6. 关于Windows10显示无法快速启动,查询日志显示:错误状态为 0xC00000D4的解决方案探索

    关于Windows10显示无法快速启动,查询日志显示:错误状态为 0xC00000D4的解决方案探索 最近电脑正常工作时常出现无规则电脑黑屏无法唤醒,必须关断电源后重启电脑. 打开电脑系统日志查看,发 ...

  7. 以太坊物流场景解决方案探索

    2019独角兽企业重金招聘Python工程师标准>>> 本文节选自电子书<Netkiller Blockchain 手札> Netkiller Blockchain 手札 ...

  8. 基于webpack搭建前端工程解决方案探索

    关于前端工程 \\ 下面是百科关于"软件工程"的名词解释: \\ \ 软件工程是一门研究用工程化方法构建和维护有效的.实用的和高质量的软件的学科. \ \\ 其中,工程化是方法,是 ...

  9. 食品安全溯源区块链解决方案探索-转载

    一篇挺好的文章,可以细品下 内容摘要 这一部关于区块链开发及运维的电子书. 为什么会写区块链电子书?因为2018年是区块链年. 这本电子书是否会出版(纸质图书)? 不会,因为互联网技术更迭太快,纸质书 ...

最新文章

  1. 2.8 多任务学习-深度学习第三课《结构化机器学习项目》-Stanford吴恩达教授
  2. DevExpress的分隔条控件SplitterControl的使用
  3. Centos 7下安装nginx,使用yum install nginx,提示没有可用的软件包(亲测)
  4. 字符串转换成ascii码
  5. SQL Server中通用数据库角色权限的处理详解
  6. 新手学习Java必需要知道的这些基本概念!
  7. 程序员过关斩将--少年派登录安全的奇幻遐想
  8. DC / OS中具有Java和数据库应用程序的服务发现
  9. bigru参数计算_[数据挖掘]华中科技大学 李黎 周达明:基于CNN-BiGRU模型的操作票自动化校验方法...
  10. Swift中类的使用
  11. 《极客与团队》一说到底真正重要的还是代码本身
  12. Activity的Launch mode详解,A B C D的singleTask模式
  13. open cv+C++错误及经验总结(十二)
  14. Java线程池 与Lambda
  15. RabbitMQ-C客户端使用说明
  16. 工程项目管理问题那么多,什么软件可以实现工程项目管理自动化
  17. 如何快速学习PLC编程
  18. 计算机网络的通信主体,计算机网络试题及答案
  19. Swagger导出pdf文档
  20. MySQL 网站上的 GA 是什么意思?

热门文章

  1. 青橙商城项目总结day03-04
  2. 你是编程中的“快枪手”还是“慢悠悠”?
  3. 服务器监测开发OSHI java.lang.NoClassDefFoundError: com/sun/jna/platform/win32/VersionHelpers
  4. Trie Tree 介绍
  5. ajax scripmanager,ScriptManager.RegisterStartupScript()方法在ajax页面无效的解决方法
  6. 计算机支付不了怎么办理,支付宝打不开怎么办?
  7. xshell中打开vim后的颜色与colorscheme配置颜色不符合
  8. 【一】数据挖掘(DM)到底是何方神圣?
  9. PLUS模型教程3:用地扩张分析策略(LEAS)
  10. cssbefore图片大小_::before如何使用?