ProGuard混淆及R8优化

news2024/11/22 15:13:36

前言:使用java编写的源代码编译后生成了对于的class文件,市面上很多软件都可以对class文件进行反编译,况且Android开发的应用程序是用Java代码写的,为了很好的保护Java源代码,我们需要对编译好后的class文件进行混淆。另外随着apk的版本迭代,功能需求越多,apk发布的包的体积也会越来越大,y因此人们更倾向于安装并保留较小和安装占用空间更小的应用,所以谷歌提供了R8 编译器,您可以通过压缩、混淆和优化,更全面的缩小应用体积。Android构建中,在AGP3.4.0之前也是使用的ProGuard 进行代码优化混淆,但是在3.4.0之后,谷歌将这一工作赋予给了性能更佳的R8编译器。虽然摒弃了ProGuard,但是R8编译器还是兼容ProGuard的配置规则

ProGuard混淆

ProGuard是一个混淆代码的开源项目,它的主要作用是混淆代码,但也包括压缩(Shrink)、优化(Optimize)、混淆(Obfuscate)、预检(Preveirfy),来自官网权威的解释:Proguard是一个Java类文件压缩器、优化器、混淆器、预校验器。压缩环节会检测以及移除没有用到的类、字段、方法以及属性。优化环节会分析以及优化方法的字节码。混淆环节会用无意义的短变量去重命名类、变量、方法。这些步骤让代码更精简,更高效,也更难被逆向。因为R8取代了ProGuard的压缩、优化及预检,保留了ProGuard的混淆配置,所以本文只讲下ProGuard的混淆的一些注意事项。

 混淆

定义:简而言之就是使用a,b,c,d这样简短而无意义的名称,对类、字段和方法进行重命名,从而提高反编译后的阅读成本。

使用:主项目的 build.gradle 设置 minifyEnabled trueproguard-rules.pro 加入混淆规则;

android {
    buildTypes {
        release {
            ...
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    ...
}

proguard-android-optimize.txt就是默认的基本的混淆文件,包含一些基本的混淆规则。具体在SDK目录下,给图有真相。sdk安装路径因人而异look下我的

混淆的一些常用规则:

  • 一颗星:表示保留当前包下的类名,如果有子包,子包中的类名也会被混淆
-keep class com.csj.test.*

 也就是com.csj.test.ui包下的所有类(MyView)都会被混淆,但MainActivity类不会被混淆

  • 两颗星:表示保留当前包下的类名,如果有子包,子包中的类名也会被保留。
-keep class com.csj.test.**
  • 上面的方式虽然保留了类名,但是内容还是会被混淆,使用下面方式保留内容:  
-keep class com.csj.test.* {*;}

这样的话com.csj.test包下的所有类名及内容都不会被混淆,但实际开发并不提倡这么使用,因为这样就失去了混淆的意义,所以我们可以针对特定的内容进行保留不被混淆。

在此基础上,我们也可以使用Java的基本规则来保护特定类不被混淆,比如我们可以用extends,implements等这些Java规则

例如:

-keep public class * extends android.app.Activity

以上代码的意思就是所有继承android.app.Activity这个类的子类都不会被混淆。同理

-keep public class * implements java.io.Serializable

保留Serializable序列化的所实现的类不被混淆

以上是针对整个类不被混淆,但如果还是觉得混淆的范围太大,就是一个类中你不希望保持全部内容不被混淆,而只是希望保护类下的特定内容,就可以使用

<init>;     //匹配所有构造器
<fields>;   //匹配所有域
<methods>;  //匹配所有方法方法

在或前面加上private 、public等来进一步指定不被混淆的内容,如

-keep class com.csj.test.MainActivity{
    public <methods>;
}
  • 当然你还可以加入参数,比如以下表示用String作为入参的构造函数不会被混淆:
-keep class com.csj.test.MainActivity{
    public <init>(String);
}

也可以直接指定具体哪个方法不被混淆

-keep class com.csj.test.ui.MyView{
    public void test();
}

还有一种就是不需要保持类名,只需要把该类下的特定方法保持不被混淆就好,那你就不能用keep方法了,keep方法会保持类名,而需要用keepclassmembers ,如此类名就不会被保持,为了便于对这些规则进行理解,官网给出了以下表格:

# -keep关键字
# keep:包留类和类中的成员,防止他们被混淆
# keepnames:保留类和类中的成员防止被混淆,但成员如果没有被引用将被删除
# keepclassmembers :只保留类中的成员,防止被混淆和移除。
# keepclassmembernames:只保留类中的成员,但如果成员没有被引用将被删除。
# keepclasseswithmembers:如果当前类中包含指定的方法,则保留类和类成员,否则将被混淆。
# keepclasseswithmembernames:如果当前类中包含指定的方法,则保留类和类成员,如果类成员没有被引用,则会被移除。

到这基本上开发常用的混淆就差不多够用了,接下来就是区分实际开发中,需要注意哪些内容不应该被混淆的。

首先是混淆的一些基本规则,任何APP都要使用,可以作为模板使用。如下:

# 代码混淆压缩比,在0和7之间,默认为5,一般不需要改
-optimizationpasses 5
 
# 混淆时不使用大小写混合,混淆后的类名为小写
-dontusemixedcaseclassnames
 
# 指定不去忽略非公共的库的类
-dontskipnonpubliclibraryclasses
 
# 指定不去忽略非公共的库的类的成员
-dontskipnonpubliclibraryclassmembers
 
# 不做预校验,preverify是proguard的4个步骤之一
# Android不需要preverify,去掉这一步可加快混淆速度
-dontpreverify
 
# 有了verbose这句话,混淆后就会生成映射文件
# 包含有类名->混淆后类名的映射关系
# 然后使用printmapping指定映射文件的名称
-verbose
-printmapping proguardMapping.txt
 
# 指定混淆时采用的算法,后面的参数是一个过滤器
# 这个过滤器是谷歌推荐的算法,一般不改变
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*
 
# 保护代码中的Annotation不被混淆,这在JSON实体映射时非常重要,比如fastJson
-keepattributes *Annotation*
 
# 避免混淆泛型,这在JSON实体映射时非常重要,比如fastJson
-keepattributes Signature
 
//抛出异常时保留代码行号,在异常分析中可以方便定位
-keepattributes SourceFile,LineNumberTable

-dontskipnonpubliclibraryclasses用于告诉ProGuard,不要跳过对非公开类的处理。默认情况下是跳过的,因为程序中不会引用它们,有些情况下人们编写的代码与类库中的类在同一个包下,并且对包中内容加以引用,此时需要加入此条声明。

-dontusemixedcaseclassnames,这个是给Microsoft Windows用户的,因为ProGuard假定使用的操作系统是能区分两个只是大小写不同的文件名,但是Microsoft Windows不是这样的操作系统,所以必须为ProGuard指定-dontusemixedcaseclassnames选项

再来说下哪些是需要保留不被混淆的

1、保留所有的本地native方法不被混淆

-keepclasseswithmembernames class * {
    native <methods>;
}

因为R8(ProGuard)并未对反射以及JNI等情况进行检测,如果配置文件中未处理,则这部分代码就会被丢弃,会出现NoClassFindException的异常,

2、反射用到的类不混淆

原因同上

3、保留了继承自Activity、Application这些类的子类

-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService

因为这些子类,都有可能被外部调用,第一行就保证了所有Activity的子类不要被混淆。

4、使用enum类型时需要注意避免以下两个方法混淆,因为enum类的特殊性,以下两个方法会被反射调用

-keepclassmembers enum * {  
    public static **[] values();  
    public static ** valueOf(java.lang.String);  
}

5、保留Parcelable、Serializable序列化的类不被混淆,包括自定义的一些和服务器交互的bean类

# 保留Parcelable序列化的类不被混淆
-keep class * implements android.os.Parcelable {
    public static final android.os.Parcelable$Creator *;
}
 
# 保留Serializable序列化的类不被混淆
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

使用GSON、fastjson等框架解析服务端数据时,所写的JSON对象类不混淆,否则无法将JSON解析成对应的对象;这一点也是开发中经常忽略的问题

6、使用第三方开源库或者引用其他第三方的SDK包时,如果有特别要求,也需要在混淆文件中加入对应的混淆规则;这个一般三方官网上会有文档说明、例如高德地图SDK

 7、有用到WebView的JS调用也需要保证写的接口方法不混淆,

# 保留JS方法不被混淆
-keepclassmembers class com.example.xxx.MainActivity$JSInterface1 {
    <methods>;
}

其中JSInterface是MainActivity的子类

8、对WebView的处理

# 对WebView的处理
-keepclassmembers class * extends android.webkit.webViewClient {
    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
    public boolean *(android.webkit.WebView, java.lang.String)
}
-keepclassmembers class * extends android.webkit.webViewClient {
    public void *(android.webkit.webView, java.lang.String)
}

9、内嵌类不被混淆

# 保留内嵌类不被混淆
-keep class com.example.xxx.MainActivity$* { *; }

这个$符号就是用来分割内嵌类与其母体的标志。

也可以在具体点,比如保持ScriptFragment内部类JavaScriptInterface中的所有public内容不被混淆。

-keepclassmembers class cc.csj.test.ScriptFragment$JavaScriptInterface {
   public *;
}

10、保留自定义控件(继承自View)不被混淆

# 保留自定义控件(继承自View)不被混淆
-keep public class * extends android.view.View {
    *** get*();
    void set*(***);
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
}

ok,到这里,基本proGuard的混淆规则就大致结束了。不用死记规则,但得知道有这么个东西,打包release包时,能定位出混淆所带来的问题就行了

什么是R8?

R8 是一个将我们的 java 字节码转换为优化的 dex 码的工具。它遍历整个应用程序,然后对其进行优化,例如删除未使用的类、方法等。它在编译时运行。它可以帮助我们减少构建的大小并使我们的应用程序更加安全。R8 使用 Proguard 规则来修改其默认行为。

R8 收缩是如何工作的?

在优化代码的同时,R8 减少了我们应用程序的代码,进而减小了 APK 的大小。

为了减小 APK 大小,我们采用了三种不同的技术:

  1. 收缩或摇树:收缩是从我们的 Android 项目中删除无法访问的代码的过程。R8 执行一些静态分析以摆脱无法访问的代码并删除未实例化的对象。

  2. 优化:这用于优化代码的大小。它涉及删除死代码、删除未使用的参数、选择性内联、类合并等。

  3. 标识符重命名:在这个过程中,我们混淆了类名和其他变量名。例如,如果类的名称是“ MainActivity ”,那么它将被混淆为“ a ”或其他名称,但大小更小。

/**
 * @Author shengjie.chen
 * @Date 2023-06-23 19:19
 */
public class Chen {

    private void unused() {
        System.out.println("没吊用的代码");
    }

    private static void greeting() {
        System.out.println("hello,端午安康!");
    }

    public static void main(String[] args) {
        greeting();
    }

}

程序的入口是 static void main 方法,我们使用以下 keep 规则 指定该方法:

-keep class com.csj.test.Chen{ public static void main(java.lang.String[]); }

R8 缩减算法的运作方式如下:

  • 首先,它从程序常见的入口点跟踪所有可访问的代码。这些入口点由 R8 keep 规则定义。例如,在此 Java 代码示例中,R8 会在 main 方法处开始运行。
  • 在该示例中,R8 从 main 方法跟踪到 greeting 方法。greeting 方法是在运行时被调用的,因此跟踪在此处停止。
  • 跟踪完成后,R8 使用摇树优化来删除未使用的代码。在此示例中,摇树删除了未使用的方法(unused),因为 R8 的跟踪过程检测到从任何已知的入口都无法到达该方法。
  • 接下来,R8 将标识重命名为较短的名称,这些名称在 DEX 文件中占用较少的空间。在示例中,R8 可能会将 greeting 方法重命名为短名称 a:
package com.csj.test;

/**
 * @Author shengjie.chen
 * @Date 2023-06-23 19:19
 */
public class Chen {

    private static void a() {
        System.out.println("hello,端午安康!");
    }

    public static void main(String[] args) {
        a();
    }

}
  • 最后,应用代码优化。缩减代码大小的内联是其一。在此示例中,将方法 a 的主体直接迁移到 main 中,代码会显得更简洁:
  • public class Chen {
       
        public static void main(String[] args) {
            System.out.println("hello,端午安康!");
        }
    
    }

    简而言之就是比如你项目中依赖了很多库,但是只使用了库里面少部分代码,为了移除这部分代码,R8会根据配置文件确定应用代码的所有入口点:包括应用启动的第一个Activity或者服务等,R8会根据入口,检测应用代码,并构建出一张图表,列出应用运行过程中可能访问的方法,成员变量和类等,并对图中没有关联到的代码,视为可移除代码。盗个图

图中入口位置:MainActivity,整个调用链路中,使用到了foo,bar函数以及AwesomeApi类中的faz函数,所以这部分代码会被构建到依赖图中,而OkayApi类以及其baz函数都未访问到,则这部分代码就可以被优化。图上所表示的内内容和我上面那个例子是差不多的意思,但人家的直观点。嘎嘎。。。

那么R8狠在哪里呢?好比你的项目引入个三方依赖,但只用了其中一小部分代码,而R8会在你打包的时候,把三方依赖里未关联的代码都会移除掉再打包你的apk中。

在说下R8的代码优化。

为了进一步缩减应用,R8 会在更深的层次上检查代码,以移除更多不使用的代码,或者在可能的情况下重写代码,以使其更简洁。下面是此类优化的几个示例:

1、如果您的代码从未采用过给定 if/else 语句的 else {} 分支,R8 可能会移除 else {} 分支的代码

package com.csj.test

import android.util.Log

/**
 * @Author shengjie.chen
 * @Date 2023-06-23 19:34
 */
class Demo {
    fun test() {
        if (true) {
            Log.e("TAG", "test: 端午安康")
        } else {
            Log.e("TAG", "test: 端午放假,但下大雨了")
        }
    }
}

2、如果您的代码只在一个位置调用某个方法,R8 可能会移除该方法并将其内嵌在这一个调用点

这个上面举过例子了,不举了!

3、如果 R8 确定某个类只有一个唯一子类且该类本身未实例化(例如,一个仅由一个具体实现类使用的抽象基类),它就可以将这两个类组合在一起并从应用中移除一个类。

class Father{}
class Son extends Father{}

这种情况,Son就被干掉。

用 ProGuard 还是 R8?

如果没有历史包袱,直接R8,毕竟兼容绝大部分的ProGuard规则,更快的编译速度,对Kotlin更友好。

还是简单描述下两者吧:

  • ProGuard → 压缩、优化和混淆Java字节码文件的免费工具,开源仓库地址:proguard
  • R8 → ProGuard的替代工具,支持现有ProGuard规则,更快更强AGP 3.4.0或更高版本,默认使用R8混淆编译器。

如果不想用R8,想用回ProGuard的话(可以但没必要),可以在 gradle.properties 文件中添加下述配置禁用R8:

android.enableR8=false
android.enableR8.libraries=false

编译APK时可能会报错:

 

proguard-rules.pro 文件中加上 -ignorewarnings 即可解决。

另外,使用ProGuard或R8构建项目会在 build\outputs\mapping\release 输出下述文件:

  • mapping.txt → 原始与混淆过的类、方法、字段名称间的转换;
  • seeds.txt → 未进行混淆的类与成员;
  • usage.txt → APK中移除的代码;
  • resources.txt → 资源优化记录文件,哪些资源引用了其他资源,哪些资源在使用,哪些资源被移除;

自动生成不用管。

总结

R8保留了Proguard 混淆规则且有效地内联容器类并删除未使用的类、字段和方法.它减小了应用程序的大小。R8 提供比 Proguard 更好的输出,并且比 Proguard 更快,从而减少了整体构建时间。


参考文章:

补齐Android技能树 - 从害怕到玩转Android代码混淆 - 掘金

【Android性能优化】:ProGuard,混淆,R8优化 - 掘金

Android 中的 R8 与 Proguard的区别 - 简书

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/677844.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

切底掌握Android中的Kotlin DSL

前言 在这篇文章中&#xff0c;我们将学习如何在您的 Android 项目中编写 Kotlin DSL。 这个文章会很长&#xff0c;所以花点时间&#xff0c;让我们一起来写你的 DSL。我们将讨论以下主题&#xff0c; 什么是简单英语中的 DSL&#xff1f;您使用任何 DSL 吗&#xff1f;为什…

微服务的文件配置

1 基于本地文件配置的痛点 ①修改本地配置文件 需要重启服务 ②viper能监听本地配置文件变动 修改内存中变量的值 貌似可以满足需求 痛点如果实例过多 手动改极有可能出错 很多服务都依赖一个配置 运维可以写脚本批量修改 出问题运维不想背锅 ③ 多语言开发的实例 使用…

ThreadX在mdk(AC5)中的移植

1.ThreadX简介 Threadx是由 Express Logic 公司开发的一款实时操作系统&#xff08;RTOS&#xff09;&#xff0c;2019年被微软收购&#xff0c;成为了微软的一款Azure RTOS。在2020年&#xff0c;ThreadX也加入了开源大军&#xff0c;将ThreadX内核及其各大组件开源免费。 Th…

电赛汇总(一):微控制器以其外围电路模块设计

电赛汇总(一)&#xff1a;微控制器以其外围电路模块设计 这一章节主要详细记录各种常用的微控制器的引脚功能、外围的电路模块等&#xff0c;以便随时查看翻阅。这部分内容出自黄智伟等学者著的《全国大学生电子设计竞赛教程–常用电路模块制作》一书中&#xff0c;感兴趣的朋…

PS2022版本修复打开闪退问题

前言 windows 11 系统最近换了一台电脑&#xff0c;重新装了一批摄影剪辑软件&#xff0c;在使用过程中发现 PS2022 版本一但导入图片就卡死闪退。起初我以为是版本不兼容问题&#xff0c;但是问了一下对应的朋友他们并未出现这种情况。后面我就从百度中开始捞答案&#xff0c…

适用于平坦草原的近地层以上风廓线推算方法

目录 引言1 数据观测和处理1.1 观测实验和仪器1.2 数据处理 引言 本文研究平坦草原近地层之上的风廓线特征&#xff0c;尤其是不同稳定度情况下风随高度的变化&#xff1b;得到适用于本地的粗糙度、边界层高度和地转风的估测方法。 在上述研究的基础上&#xff0c;本文用上述…

如何快速的阅读一本书

B站&#xff1a;【读书方法】读不进&#xff1f;记不住&#xff1f;5分钟教你如何快速高效读书 | 读书会犯的5大错误&#xff01; 1 看着书皮&#xff0c;思考一下自己为什么读这本书&#xff0c;是为了解决什么问题。 2 要看目录&#xff0c;根据目录看一下这本书能不能解决…

0002Java程序设计-SSM协同过滤算法的新闻推荐系统

摘 要 “互联网”的战略实施后&#xff0c;很多行业的信息化水平都有了很大的提升。但是目前很多行业的管理仍是通过人工管理的方式进行&#xff0c;需要在各个岗位投入大量的人力进行很多重复性工作&#xff0c;使得对人力物力造成诸多浪费&#xff0c;工作效率不高等情况&am…

FPGA时序约束--实战篇(读懂Vivado时序报告)

目录 一、新建工程 二、时序报告分析 1、打开时序报告界面 2、时序报告界面介绍 3、时序路径分析 三、总结 FPGA开发过程中&#xff0c;vivado和quartus等开发软件都会提供时序报告&#xff0c;以方便开发者判断自己的工程时序是否满足时序要求。 本文将详细介绍如何读懂…

VLAN基础知识3_VLAN间三层通信(VLANIF接口)

目录 1.VLAN间三层通信简介 2.VLAN间三层通信方式 3.VLANIF接口介绍 4.基于VLANIF接口VLAN间三层通信原理 5.VLAN间三层通信实验 5.1 常用配置命令 5.2 配置步骤 5.3 实验效果 1.VLAN间三层通信简介 VLAN间三层通信是指在VLAN网络中&#xff0c;不同VLAN之间进行IP通信…

python也可以使用克里金插值算法吗?

挪威大陆架的声学压缩慢度测量的空间变化 在处理地质和岩石物理数据时&#xff0c;我们通常希望了解这些数据在我们的地区是如何变化的。我们可以做到这一点的方法之一是对我们的实际测量值进行网格化&#xff0c;并推断这些值。 进行这种外推的一种特殊方法是克里金法&#xf…

三阶魔方有多少种状态

魔方有 3 种不同的方块&#xff0c;分别为角块&#xff08;8 个&#xff0c;每个角块有三种颜色&#xff09;&#xff0c;棱块&#xff08;12 个&#xff0c;每个棱块有两种颜色&#xff09;与中心块&#xff08;6 个&#xff0c;每个中心块有一种颜色&#xff09;。 魔方总共…

每天学一点知识有用吗

在探索如何学习的路上&#xff0c;我注意到了基于微习惯的学习方式&#xff0c;比如每天在用十分钟的时间练习下普通话&#xff0c;或者每天写500字的总结。 我简单回顾一下&#xff1a; 这种方法虽然颇受欢迎&#xff0c;但是它限制了你可以尝试的活动种类&#xff0c;有时候…

深度学习(24)——YOLO系列(4)

深度学习&#xff08;24&#xff09;——YOLO系列&#xff08;4&#xff09; 文章目录 深度学习&#xff08;24&#xff09;——YOLO系列&#xff08;4&#xff09;1. dataset准备&#xff08;1&#xff09;数据详解&#xff08;2&#xff09;dataset&#xff08;3&#xff09;…

广告数仓:全流程调度

系列文章目录 广告数仓&#xff1a;采集通道创建 广告数仓&#xff1a;数仓搭建 广告数仓&#xff1a;数仓搭建(二) 广告数仓&#xff1a;全流程调度 文章目录 系列文章目录前言一、ClickHouse安装1.修改环境2.安装依赖3.单机安装4.修改配置文件5.启动clickhouse6.创建需要的数…

012-从零搭建微服务-接口文档(二)

写在最前 如果这个项目让你有所收获&#xff0c;记得 Star 关注哦&#xff0c;这对我是非常不错的鼓励与支持。 源码地址&#xff08;后端&#xff09;&#xff1a;https://gitee.com/csps/mingyue 源码地址&#xff08;前端&#xff09;&#xff1a;https://gitee.com/csps…

统一拦截--过滤器Filter

1.过滤器Filter 1. 概述 概念: Filter过滤器&#xff0c;是JavaWeb三大组件(Servlet、Filter、Listener)之一。过滤器可以把对资源的请求拦截下来&#xff0c;从而实现一些特殊的功能。过滤器一般完成一些通用的操作&#xff0c;比如:登录校验、统一编码处理、敏感字符处理等…

Tcp协议的十大特性详解+示例

前言 之前我们简单了解了一下Tcp是什么及它的套接字如何使用:基于UDP和TCP套接字实现简单的回显客户端服务器程序_Crystal_bit的博客-CSDN博客 因为要给大家介绍Tcp的十大特性&#xff0c;所以这里给出Tcp报头结构&#xff1a; 目录 1. 确认应答 2. 超时重传 3. 连接管理 3…

【Android复习笔记】Parcelable 为什么速度优于 Serializable ?

Q:Parcelable 为什么速度优于 Serializable ? 首先,抛开应用场景谈技术方案都是在耍流氓,所以如果你遇到有面试官问这样的题目本身就是在给面试者挖坑。 序列化 将实例的状态转换为可以存储或传输的形式的过程。 Serializable 实现方式: Serializable 是属于 Java 自带的…

Solid Converter PDF v10 安装及使用教程

目录 一、软件介绍二、下载教程三、安装教程四、使用教程1.PDF转Word、Html等2.合并PDF文件 一、软件介绍 Solid Converter PDF是一套专门将PDF文件转换成Word的软件。 能够将PDF转换为Word、Excel、HTML、PowerPoint、纯文本文件从PDF文档中提取数据并以CSV等格式保存能够转…