一、SDK与APP混淆的区别
SDK混淆和APP相比除了常用的混淆配置以外,还需要避免混淆暴露给游戏的接口。同时为了避免游戏接入SDK之后混淆了关键代码导致闪退,SDK还需要提供一份混淆配置文件。
二、混淆基础
Android的代码混淆只需要在AS的build.gradle中配置开启即可。
示例代码如下:
buildTypes {
release {
minifyEnabled true // true - 打开混淆
shrinkResources false // true - 打开资源压缩
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro',
'../libModule/proguard-rules.pro' // 用于设置Proguard的规划路径;
}
}
minifyEnabled 为是否打开混淆
shrinkResources 为是否打开资源压缩。即删除APK中多余的资源。
但如果不是对包体大小很敏感,不建议开启shrinkResources,很容易踩坑。因为如果开启了只有在程序中使用了R.xx.xx的资源才不会被删除。而没有被引用或者通过文件名搜索等方法引用的资源,将会被优化掉。
如果一定要使用,注意要把没有被R.xx.xx引用的资源添加到 res–raw—keep.xml。
keep.xml的示例代码:
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/photo1,@drawable/photo2"
proguard-android.txt 是系统默认的混淆文件,具体在…/sdk/tools/proguard/ 目录下,其中包含了 android 最基本的混淆,一般不需要改动。
proguard-rules.pro 是我们需要配置的规则。如果要配置多个Module的混淆文件,只需要后面添加逗号跟混淆文件路径即可。
proguard-rules.pro基本规则:
keep关键字, 保留类和类中的成员,防止它们被混淆或移除。
示例:如果不想某个类中的代码被混淆
-keep public class com.yesdk.sdk.XSDK {
public static <fields>;
public <methods>;
}
注意如果XSDK类中的方法含有回调,也需要把该回调方法的类也添加到keep中。
例如:XSDK下的floatWindow含有floatWindowListener回调,即:
public void floatWindow(Activity activity, floatWindowListener listener) {
}
那如果想CP方正常调用floatWindow方法,则需要把floatWindowListener也添加到keep当中。
示例:
-keep public class com.yesdk.sdk.XSDK {
<fields>;
<methods>;
}
-keep public class com.yesdk.sdk.suspendbox.floatWindowListener {
<fields>;
<methods>;
}
其他keep的关键字还有:
keepnames:保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。
keepclassmembers:只保留类中的成员,防止它们被混淆或移除。
keepclassmembernames:只保留类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。
keepclasseswithmembers:保留类和类中的成员,防止它们被混淆或移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。
keepclasseswithmembernames:保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。
通配符:
<field>
匹配类中的所有字段
<method>
匹配类中的所有方法
<init>
匹配类中的所有构造函数
*
匹配任意长度字符,但不含包名分隔符(.)。
比如说我们的完整类名是com.example.test.MyActivity,使用com.*,或者com.exmaple.*都是无法匹配的,因为*无法匹配包名中的分隔符,正确的匹配方式是com.exmaple.*.*,或者com.exmaple.test.*,这些都是可以的。但如果你不写任何其它内容,只有一个*,那就表示匹配所有的东西。
**
匹配任意长度字符,并且包含包名分隔符(.)
***
匹配任意参数类型。
比如void set*(***)就能匹配任意传入的参数类型,*** get*()就能匹配任意返回值的类型。
…
匹配任意长度的任意类型参数。
比如void test(…)就能匹配任意void test(String a)或者是void test(int a, String b)这些方法。
三、SDK混淆
SDK需要提供给CP方接入,提供的方式可以有 jar包、aar、maven等几种方式。这里我是采用了jar包的形式提供SDK。
但 Android Studio 是不对 jar包进行混淆的,所以需要自行使用ProGuard对jar包进行混淆。
ProGuard是一个开源的Java代码混淆器。可以在 sdk\tools\proguard\bin目录中找到,并且proguardgui.bat为它的UI界面(一般不会使用)。
ProGuard的使用:
常用语法:
从给定的文件中读取配置参数-include {filename}
指定基础目录为以后相对的档案名称-basedirectory {directoryname}// 指定要处理的应用程序jar,war,ear和目录 -injars {class_path}
指定处理完后要输出的jar,war,ear和目录的名称 -outjars {class_path}
指定要处理的应用程序jar,war,ear和目录所需要的程序库文件 -libraryjars {classpath}
指定不去忽略非公共的库类。-dontskipnonpubliclibraryclasses
指定不去忽略包可见的库类的成员。-dontskipnonpubliclibraryclassmembers
jar包进行混淆,示例代码:
def proguard() {
def proguardDir = project.ext.proguardDir //混淆产物的输出路径(dump、mapping、seeds、usage等)
proguard.Configuration configuration = new proguard.Configuration()
configuration.libraryJars = new proguard.ClassPath()
configuration.programJars = new proguard.ClassPath()
File dir = new File(proguardDir) //如果没有这个文件夹就创建一个
if(!dir.exists()) {
dir.mkdirs()
}
configuration.dump = new File(proguardDir, 'dump.txt')
configuration.printMapping = new File(proguardDir, 'mapping.txt')
configuration.printSeeds = new File(proguardDir, 'seeds.txt')
configuration.printUsage = new File(proguardDir, 'usage.txt')
/**
* 获得sdk目录
*/
def sdkDir //Android SDK的目录
Properties properties = new Properties()
File localProps = rootProject.file("local.properties") //读取根目录下的local.properties,判断环境是否已安装Android SDK
if (localProps.exists()) {
properties.load(localProps.newDataInputStream())
sdkDir = properties.getProperty("sdk.dir")
} else {
sdkDir = System.getenv("ANDROID_HOME")
}
/**
* 将android.jar和apache的库加入依赖
*/
if (sdkDir) { //compileSdkVersion的版本 和 SDK目录里是否有此版本
def compileSdkVersion = android.compileSdkVersion
ClassPathEntry androidEntry = new ClassPathEntry(new File("${sdkDir}/platforms/${compileSdkVersion}/android.jar"), false);
configuration.libraryJars.add(androidEntry)
} else {
throw new InvalidUserDataException('$ANDROID_HOME is not defined')
}
File file = new File(project.ext.proguardLibraryDir) //混淆时依赖的jar包目录
file.listFiles().each {File child -> //把目录中的jar包都添加进去
ClassPathEntry childEntry = new ClassPathEntry(new File(child.absolutePath), false);
configuration.libraryJars.add(childEntry)
}
ClassPathEntry inJarEntry = new ClassPathEntry(new File(project.ext.rawJar), false) //未混淆的jar包路径(就是我们需要混淆的jar包的)
configuration.programJars.add(inJarEntry)
ClassPathEntry outJarEntry = new ClassPathEntry(new File(project.ext.proguardJar), true) //混淆完的输出路径
configuration.programJars.add(outJarEntry)
ConfigurationParser proguardParser = new ConfigurationParser(new File(project.ext.proguardPro)) //混淆配置文件
proguardParser.parse(configuration)
proguardParser.close()
/**
* 执行混淆
*/
ProGuard proguard = new ProGuard(configuration)
proguard.execute()
}