本篇来介绍下Android的混淆和反混淆,说起混淆,大家都会很自然地想到Proguard,此外还有R8。事实上,AGP3.3之后,官方默认使用R8做代码优化、混淆和压缩。ProGuard和R8常常用于混淆最终的Android项目,增加项目被反编译的难度。
目录
一、ProGuard
二、R8
三、Proguard和R8对比
四、混淆
五、反混淆
1、mapping文件
2、proguardgui.sh
一、ProGuard
ProGuard是一个压缩、优化和混淆Java字节码文件的免费的工具。混淆只是ProGuard的其中一项功能,ProGuard能够对Java类中的代码进行压缩(Shrink),优化(Optimize),混淆(Obfuscate),预检(Preveirfy)。以下是Proguard的工作流程:
1、压缩(Shrink)
删除没有使用的类,字段,方法和属性。
2、优化(Optimize)
对字节码进行优化,并且移除无用指令。
3、混淆(Obfuscate)
使用a,b,c等无意义的名称,对类,字段和方法进行重命名。
4、预检(Preveirfy)
在Java平台上对处理后的代码进行预检。
二、R8
R8是一个将java字节码转换为优化的dex的工具。它遍历整个应用程序,然后对其进行优化,例如删除未使用的类、方法等。它可以帮助我们减少构建的大小并使我们的应用程序更加安全。R8使用Proguard的规则,R8比Proguard更快更强。
1、代码压缩
安全地从App及其库依赖项中删除未使用的类,字段,方法和属性。
2、资源压缩
从打包的App中删除未使用的资源,包括应用程序库依赖项中未使用的资源。它与代码压缩一起使用,这样一旦删除了未使用的代码,也可以安全地删除不再引用的资源。
3、代码混淆
使用简短无意义的名称重命名代码里的类,字段和方法,从而减少DEX文件大小。
4、代码优化
删除未使用的代码或重写代码使其更简洁。
三、Proguard和R8对比
在使用 Proguard 时,应用程序代码由Java编译器转换为Java字节码.class文件。然后Proguard使用我们编写的规则对其进行优化产出优化后的.class文件,最后将其转换为可执行的Dalvik字节码。编译打包的流程如下:
R8引入之后,代码的混淆和转换为dex通过R8一步完成,编译打包的流程如下:
1、R8 有效地内联容器类并删除未使用的类、字段和方法,包体积更小。
2、与 Proguard 相比,R8 对 Kotlin 的支持更多。
3、R8比 Proguard 更快,从而减少了整体构建时间。
四、混淆
虽然Android Studio已经默认使用R8作为编译器,但是仍然需要我们在build.gradle(app)中配置一下是否开启代码和资源压缩:
buildTypes {
release {
// 是否开启代码压缩
minifyEnabled true
// 是否开启资源压缩
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
android.applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "demo_" + defaultConfig.versionName + "_release.apk"
}
}
}
}
本篇我们使用如下demo,代码造了一个空指针异常,这是为后面反混淆准备的:
package com.example.proguarddemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
private TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = findViewById(R.id.crash);
mTextView.setOnClickListener(v -> {
mTextView = null;
mTextView.setText("11111");
});
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/crash"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
既然AS已经默认使用R8作为编译器,那么我们不再去对比Proguard和R8的包体积和编译速度,我们使用本篇博客的demo来对比下开R8和不开R8打release包的体积。
(1)不开R8:
(2)开R8,但不开资源压缩:
(3)开R8,开资源压缩:
通过对比上面的包体积,可以看到,开了R8 + 资源压缩后,包体积缩减了55.6%。接下来,我们看下代码是否真的混淆了,装上打包后的release包,运行后点击textview,触发了崩溃,我们看下堆栈能不能看到源码:
2023-04-24 16:46:45.163 6037-6037/? E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.proguarddemo, PID: 6037
java.lang.NullPointerException: throw with null exception
at a1.a.onClick(SourceFile:5)
at android.view.View.performClick(View.java:7281)
at android.view.View.performClickInternal(View.java:7255)
at android.view.View.access$3600(View.java:828)
at android.view.View$PerformClick.run(View.java:27925)
at android.os.Handler.handleCallback(Handler.java:900)
at android.os.Handler.dispatchMessage(Handler.java:103)
at android.os.Looper.loop(Looper.java:219)
at android.app.ActivityThread.main(ActivityThread.java:8393)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)
可以看到,崩溃的堆栈是混淆后的,a1.a.onClick,代码确实是混淆后的,这增强了代码的安全性,让外人不那么容易反编译得到关键代码。
五、反混淆
承接上文说到的,代码混淆后代码的安全性增强了。正式包发版后,针对线上的crash收集到的崩溃堆栈也是混淆后的,增加了crash问题排查定位的难度。那么,如何反混淆呢?
混淆的原理是把一些类名、方法名、属性名等修改为无意义的字母等,降低代码可阅读性的同时,压缩代码体积。混淆的同时会生成一个mapping文件,记录的就是映射关系,通过映射关系可以拿到混淆前的类名和方法名。
1、mapping文件
打release包后,生成的mapping文件路径:app/build/outputs/mapping/release
2、proguardgui.sh
在sdk中有反混淆的工具:proguardgui.sh,可以帮助我们通过mapping文件解析崩溃堆栈。进入到Android/sdk/tools/proguard/bin,直接运行
./proguardgui.sh
会打开一个gui界面,选择mapping文件,粘贴crash堆栈后,点击Retrace!:
可以看到,是MainActivity的onClick方法的第三行发生了空指针:
本篇系统的介绍了Android的Proguard和R8,总结了二者的执行过程和对比,并通过实际的demo去验证R8优化后的包体积和混淆的结果。同时,也介绍了如何通过解析mapping的工具proguardgui.sh去辅助我们解析混淆后的crash堆栈,方便更快速的定位线上问题。如果对你有所帮助或启发,欢迎关注点赞。