混淆(Proguard & R8)和反混淆

    本篇来介绍下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堆栈,方便更快速的定位线上问题。如果对你有所帮助或启发,欢迎关注点赞。

猜你喜欢

转载自blog.csdn.net/qq_21154101/article/details/130686892