最近在研究apk瘦身时,发现代码混淆有很大作用,所以就去简单研究了一下。
我认为,代码混淆可以保护核心功能不泄漏以及apk瘦身;但是也有个缺点:就是人为的可能会把不能混淆的代码混淆,导致crash。
经过整理发现代码混淆可以分为三部分:
- 基本指令以及一些固定不混淆的代码;
- 某些第三方包;
- 自己书写的一些不要混淆的代码。
注:其中第二部分,可以根据情况扩展或去掉某些未曾用到的指令;第三部分需要根据自己的情况去添加;
代码混淆
俗话说,授人以鱼不如授人以渔,但是先有鱼总是好的,所以接下来就会先介绍一下代码混淆的基本使用。
鱼
1. 开启混淆代码
在app module下的gradle文件中
android {
...
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
默认minifyEnable是false,我们只需要改为true就开启了混淆。这里只需要在release的时候开启混淆即可,代码混淆会加长APK的生成时间,而且android studio2.0以后使用instance run会停用ProGuard。
这时候打包,安装,如果引入了其他第三方的代码的话,不出意外时会crash的,虽然proguard-android.txt文件中已经包含了一些不混淆的指令了,那些事系统常规的指令,对于自定义的一些指令就需要自己在app下的proguard-rules.pro文件中去定义。
2. PROGUARD模板
我看这个库里边的内容也用分割线做了一下简单的分割,包含的也是上文说到的三部分,下面简单介绍一下具体包含的内容。
基本指令:包含了压缩级别,忽略警告,混淆警告等;
固定不混淆的代码:包含继承四大组件中的内容,support包,view相关、序列化相关、R文件、枚举、native方法等;
第三方包:这一部分需要根据项目情况去处理,这里支持了我们公司常用框架里边的第三方包;
自己书写的不需要混淆的代码:这里就需要根据自己的情况去书写了,而我们只需要知道哪些东西是不能混淆的就能编写了。
下面来说一下,混淆的规则:第三方库。一般都会提供混淆规则,如果没有提供,报错后,我们就可以保护报错的类,不让其混淆,实在不行就用最暴力的解决办法,把其全部代码都不混淆。
运行时动态改变代码。一般例如反射,实体类。
被JNI中调用的类。
WebView中Js调用的方法。
View相关的类和事件。
这两步过后,基本就完成了代码混淆,运行后发现问题再解决即可,下面就开始介绍一些混淆中的语法。
渔
1、常用命令
命令 | 作用 |
---|---|
-keep | 防止该类所有内容被移除或重命名 |
-keepnames | 防止类和成员被重命名 |
-keepclassmembers | 防止成员被移除或者被重命名 |
-keepclasseswithmembers | 防止拥有该成员的类和成员被移除或者被重命名 |
-keepclasseswithmembernames | 防止拥有该成员的类和成员被重命名 |
2. 常用规则
类:需要使用完全限定名;
*:通配符,任意字符串,不包含包名分隔符(.);
**:通配符,任意字符串,包含包名分隔符(.);
extends:继承某类的类;
implement:实现某接口的类;
$:内部类;
<init>:所有构造方法;
<fields>:所有成员变量;
<methods>:所有方法;
…:任意参数;
修饰符:public private protected
3. 例子
含义 | 指令语句 |
---|---|
不混淆某个类 | -keep public class packageName.className{ *; } |
不混淆某个包的所有类 | -keep class packageName.**{ *; } |
不混淆某个类的子类 | -keep public class * extends packageName.className{ *; } |
不混淆某个接口的子类 | -keep public class * implements packageName.className{ *; } |
不混淆某个类的构造方法 | -keepclassmembers class packageName.className{ public <init style="box-sizing: border-box;">(); }</init> |
不混淆某个类的某个方法 | -keepclassmembers class packageName.className{ public void methodName(…); } |
不混淆某个类的内部类 | -keep class packageName.className$*{ *; } |
混淆的步骤
上文中介绍了代码的混淆,其实Android的混淆中还包括了资源压缩,整个过程包括:压缩、优化、混淆、以及预校验,其中第四步在Android中可不要,默认是去掉了的,另外三个都是默认开启的。
- 压缩:会移除未被使用的类和成员变量,会在优化后再次被执行;
- 优化:在字节码级别执行优化,让应用运行的更快;
- 混淆:增大反编译难度,类和类成员会被随机命名,除非用keep保护。
下面再简单介绍一下再开启资源压缩,和开启代码混淆在一起加上shrinkResources true即可。但是这时候所有未被使用的资源都会被移除,但是有的资源我们可能是动态使用的,就需要保留。
那么就可以创建一个包含标签的XML文件,并使用tools:keep属性指定保留的资源,使用tools:discard属性指定要删除的资源;多个就使用逗号隔开即可,还可以使用*通配符。例如:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
tools:discard="@layout/unused2" />
对于资源的保留,还有一个严格应用的检查,开启后,在使用Resources.getIdentifier()就可以根据动态生成的字符串查询资源名字,例如:
String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());
使用方式也很简单,如下:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:shrinkMode="strict" />
到这里,混淆相关的就基本上介绍完了,然后再总结一下需要注意的东西:
- 一定要根据混淆规则判断自己的代码是否需要混淆;
- 测试的版本一定需要打包后的apk;
还遗留的问题是:错误日志的定位问题?思路是根据输出的mapping文件去对应,没有详细的去落实,实际遇到了再补上具体的解决方案。
本文章参考文章
还遗留的问题是:错误日志的定位问题?思路是根据输出的mapping文件去对应,没有详细的去落实,实际遇到了再补上具体的解决方案。