wegame一键蹲替换文件_手把手讲解 Android hook技术实现一键换肤

a838b596fab699fe225d40d229827896.png

前言

产品大佬又提需求啦,要求app里面的图表要实现白天黑夜模式的切换,以满足不同光线下都能保证足够的图表清晰度. 怎么办?可能解决的办法很多,你可以给图表view增加一个toggle方法,参数String,day/night,然后切换之后postInvalidate 刷新重绘.
OK,可行,但是这种方式切换白天黑夜,只是单个View中有效,那么如果哪天产品又要另一个View换肤,难道我要一个一个去写toggle么?未免太low了.

那么能不能要实现一个全app内的一键换肤?一劳永逸~~~

正文大纲
  1. 什么是一键换肤

  2. 界面上哪些东西是可以换肤的

  3. 利用HOOK技术实现优雅的“一键换肤"

  4. 相关android源码一览

  5. "全app一键换肤" Demo源码详解

1、什么是一键换肤

所谓"一键",就是通过"一个"接口的调用,就能实现全app范围内的所有资源文件的替换.包括 文本,颜色,图片等。

一些换肤实现方式的对比:

方案1: 自定义View中,要换肤,那如同引言中所述,toggle方法,invalidate重绘。弊端:换肤范围仅限于这个View.

方案2:给静态变量赋值,然后重启Activity. 如果一个Activity内用静态变量定义了两种色系,那么确实是可以通过关闭Activity,再启动的方式,实现 貌似换肤的效果(其实是重新启动了Activity)弊端:太low,而且很浪费资源

也许还有其他方案吧,View重绘,重启Activity,都能实现,但是仍然不是最优雅的方案,那么,有没有一种方案,能够实现全app内的换肤效果,又不会像重启 Activity 这样浪费资源呢?请看下图:

20c52b2e1e744ad72beb5714b9d785f9.gif

这个动态图中,首先看到的是Activity1,点击换肤,可直接更换界面上的background,图片的src,还有textViewtextColor,跳转Activity2之后的textView颜色,在我换肤之前,和换肤之后,是不同的。换肤的过程我并没有启动另外的Activity,界面也没有闪烁。我在Activity1里面换肤,直接影响了Activity2textView字体颜色。

既然给出了效果,那么肯定要给出Demo,不然太没诚意,嘿嘿嘿。

github地址奉上:https://github.com/18598925736/HookSkinDemoFromHank

2、界面上哪些东西是可以换肤的

上面的换肤动态图,我换了ImageView,换了background,换了TextView的字体颜色,那么到底哪些东西可以换?

答案其实就一句话: 我们项目代码里面 res目录下的所有东西,几乎都可以被替换。
(为什么说几乎?因为一些犄角旮旯的东西我没有时间一个一个去试验….囧)

具体而言就是如下这些:

  • 动画

  • 背景图片

  • 字体

  • 字体颜色

  • 字体大小

  • 音频

  • 视频

3、 利用HOOK技术实现优雅的“一键换肤"

什么是hook?

如题,我是用hook实现一键换肤。那么什么是hook?
hook,钩子. 安卓中的hook技术,其实是一个抽象概念:对系统源码的代码逻辑进行"劫持",插入自己的逻辑,然后放行。注意:hook可能频繁使用java反射机制···

"一键换肤"中的hook思路

1 、"劫持"系统创建View的过程,我们自己来创建View
系统原本自己存在创建View的逻辑,我们要了解这部分代码,以便为我所用。

2、收集我们需要换肤的View(用自定义view属性来标记一个view是否支持一键换肤),保存到变量中,劫持了系统创建view的逻辑之后,我们要把支持换肤的这些view保存起来。

3、加载外部资源包,调用接口进行换肤,外部资源包是.apk后缀的一个文件,是通过gradle打包形成的。里面包含需要换肤的资源文件,但是必须保证,要换的资源文件,和原工程里面的文件名完全相同

4、 相关android源码一览

1、Activity 的 setContentView(R.layout.XXX) 到底在做什么?

回顾我们写app的习惯,创建Activity,写xxx.xml,在Activity里面setContentView(R.layout.xxx) 。 我们写的是xml,最终呈现出来的是一个一个的界面上的UI控件,那么setContentView到底做了什么事,使得XML里面的内容,变成了UI控件呢?

请看下图:

6718fd87b6c74d749031f693089e7f65.png

源码索引:setContentView(R.layout.activity_main);  ->getDelegate().setContentView(layoutResID);

OK,这里暴露出了两个方法,getDelegate()setContentView()

先看getDelegate:

这里返回了一个AppCompatDelegate对象,跟踪到AppCompatDelegate内部,阅读源码,可以得出一个结论:AppCompatDelegate 是替Activity生成View对象的委托类,它提供了一系列setContentView方法,在Activity中加入UI控件。

2、那它的AppCompatDelegatesetContentView方法又做了什么?

找到setContentView的具体过程:

838c2c45fa56c18b2b14adc5f70ac647.png

那么就进入下一个环节:LayoutInflater又做了什么?

LayoutInflater这个类是怎么把layout.xml 变成TextView对象的?

我们知道,我们传入的是int,是xxx.xml这个布局文件,在R文件里面的对应int值。LayoutInflater拿到了这个int之后,又干了什么事呢?

一路索引进去:会发现这个方法:

4180bb33296a1771aeb80486f8d4d0f7.png

9ffa245209ac118c1af3f8187d318903.png

发现一个关键方法:CreateViewFromTagtag是指的什么?其实就是 xml里面 的标签头:里的TextView

跟踪进去:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) {

这个方法有5个参数,意义分别是:

  • View parent 父组件

  • String name  xml标签名

  • Context context   上下文

  • AttributeSet attrs view属性

  • boolean ignoreThemeAttr 是否忽略theme属性

并且在这里,发现一段关键代码:

if (mFactory2 != 

实际上,可能有人要问了,你怎么知道这边是走的哪一个if分支呢?
方法:新创建一个Project,跟踪MainActivity onCreate里面setContentView()一路找到这段代码debug你会发现:

117e3faca581b9bd741cf656dd155d27.png

答案很明确了,系统在默认情况下就会走Factory2onCreateView(),应该有人好奇:这个mFactory2对象是哪来的?是什么时候set进去的,答案如下:

bce4f4c0abba85f3a09b20f10789941f.png

41a791d29842aa86912c69f01a1dac98.png

9f65a620ea581c807a39a62c64734659.png

这时,getDelegate()得到的对象,和 LayoutInflater里面mFactory2其实是同一个对象。

那么继续跟踪,一直到:AppCompatViewInflater 类:

9a810414095c258794531efb4eb0b86d.png

275259a36ac0e98776defde938c01358.png

23734244e8d9ed165fb2a9d28383c781.png

这边利用了大量的switch case来进行系统控件的创建,例如:TextView

@NonNull

都是new 出来一个具有兼容特性的TextView,返回出去。
但是,使用过switch的人都知道,这种case形式的分支,无法涵盖所有的类型怎么办呢?这里switch之后,view仍然可能是null。所以,switch之后,谷歌大佬加了一个if,但是很诡异,这段代码并未进入if,因为  originalContext != context并不满足….具体原因我也没查出来,(;´д`)ゞ

if (view == 

然而,这里的补救措施没有执行,那自然有地方有另外的补救措施,回到之前的LayoutInflater的下面这段代码:

if (mFactory2 != 

这段代码的下面,如果view是空,补救措施如下:

if (view == 

这里的两个方法onCreateView(parent, name, attrs)createView(name, null, attrs);都最终索引到:

1aaac5509e2947487d0ba819f657165f.png

69c3e17315293b1f0a5a12a6b03b6a04.png

这么一大段好像有点让人害怕。其实真正需要关注的,就是反射的代码,最后的newInstance()
OK,Activity上那些丰富多彩的View的来源,就说到这里。

4、app中资源文件大管家 Resources / AssetManager 是怎么工作的

从我们的终极目的出发:我们要做的是“换肤”,如果我们拿到了要换肤的View,可以对他们进行setXXX属性来改变UI,那么属性值从哪里来?
界面元素丰富多彩,但是这些View,都是用资源文件来进行 "装扮"出来的,资源文件大致可以分为:
图片,文字,颜色,声音视频,字体等。如果我们控制了资源文件,那么是不是有能力对界面元素进行set某某属性来进行“再装扮”呢? 当然,这是可行的。因为,我们平时拿到一个TextView,就能对它进行setTextColor,这种操作,在view还存活的时候,都可以进行操作,并且这种操作,并不会造成Activity的重启。
这些资源文件,有一个统一的大管家。可能有人说是R.java文件,它里面统筹了所有的资源文件int值.没错,但是这个R文件是如何产生作用的呢? 答案:Resources.

一张图说明一切:

cef34285b65d991a5419ffdf9855485e.png

5、 "全app一键换肤" Demo源码详解(戳这里获得源码)

项目工程结构:

f05d3c6efbfbd3e36acc090febf164ff.png

关键类 SkinFactory

SkinFactory类, 继承LayoutInflater.Factory2 ,它的实例,会负责创建View,收集 支持换肤的view

public 

关键类 SkinEngine

public 

关键类的调用方式:

1、 初始化"换肤引擎"

public 

2、劫持 系统创建view的过程

public 

3、 执行换肤操作

protected void changeSkin(String path) {

效果展示:

20c52b2e1e744ad72beb5714b9d785f9.gif

注意事项:

1、 皮肤包skin_plugin module,里面,只提供需要换肤的资源即可,不需要换肤的资源,还有src目录下的源码
(只是删掉java源码文件,不要删目录结构啊….(●´∀`●)),不要放在这里,无端增大皮肤包的体积.

2、 皮肤包 skin_plugin modulegradle sdk版本最好和app module的保持完全一致,否则无法保证不会出现奇葩问题.

3、 用皮肤包skin_plugin module打包生成的apk文件,常规来说,是放在手机内存里面,然后由app module内的代码去加载。至于是手机内存里面的哪个位置,那就见仁见智了. 我是使用的mumu模拟器,我放在了最外层的根目录下面,然后读取这个位置的代码是:File skinFile = new File(Environment.getExternalStorageDirectory(), "skin.apk");

cb3df658ba0a6a708949043547e36cec.png

4、上图中,打了两个皮肤包,要注意:打两个皮肤包运行demo,打之前,一定要记得替换drawable图片资源为同名文件,以及

b5484c8b210014bf10f2a1336684cd4c.png

不然切换没有效果。

结语

hook技术是安卓高级层次的技能,学起来并不简单,demo里面的注释我自认为写的很清楚了,如果还有不懂的,欢迎留言评论。读源码也并不是这么轻松的事,可是还是那句话,太简单的东西,不值钱,有高难度才有高回报。为了百万年薪,fighting!

作者:波澜步惊
链接:https://www.jianshu.com/p/4c8d46f58c4f
本文经作者授权推送。

---完---

阅读推荐:

反对996的人,就是对于社会价值创造理解不够彻底?

Android百度地图轨迹回放

Android开发一年,你是不是还做着拖拽改样的活?

f9b7b037f798b3cd12719b88a711ea0a.png 202ad3170bbb5a0f02f16f24c6b0d8c7.png  2019 随手点好看 年薪上百万!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值