前言:该文接上两篇博文App的打磨之路(上)和App的打磨之路(中),继续描述打包、反编译及加固。
每个Android应用在完成后都需要打成APK包,对于单个打包的方式在此就不赘述了,基本IDE都带,只是在对外发布的应用需要配置属于该应用的唯一签名,下文主要讲述需要上传多个市场的情况下怎么批量打包。
Maven是一个项目管理工具,它包含了一个项目对象模型(Project Object Model),一组标准集合,一个项目生命周期(ProjectLifecycle),一个依赖管理系统(Dependency Management System),和用来运行定义在生命周期阶段(phase)中插件(plugin)目标(goal)的逻辑。
Maven也是自动构建工具,配合使用android-maven-plugin插件,以及maven-resources-plugin插件可以很方便的生成渠道包,下面简要介绍下打包过程,更多Maven以及插件的使用方法请参考Maven教程。
首先,在AndroidManifest.xml的节点中添加如下元素,用来定义渠道的来源:
<meta-data
android:name="channel"
android:value="${channel}" />
定义好渠道来源后,接下来就可以在程序启动时读取渠道号了:
private String getChannel(Context context) {
try {
PackageManager pm = context.getPackageManager();
ApplicationInfo appInfo = pm.getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
return appInfo.metaData.getString("channel");
} catch (PackageManager.NameNotFoundException ignored) {
}
return "";
}
要替换AndroidManifest.xml文件定义的渠道号,还需要在pom.xml文件中配置Resources插件:
<resources>
<resource>
<directory>${project.basedir}directory>
<filtering>truefiltering>
<targetPath>${project.build.directory}/filtered-manifesttargetPath>
<includes>
<include>AndroidManifest.xmlinclude>
includes>
resource>
resources>
准备工作已经完成,现在需要的就是实际的渠道号了。下面的脚本会遍历渠道列表,逐个替换并打包:
#!/bin/bash
package(){
while read line
do
mvn clean
mvn -Dchannel=$line package
done < $1
}
package $1
从以上描述中可以看出该方式每打一个包都会重新构建,执行效率太低,对于少量渠道还可以接受,渠道包过多就没法满足需求了。
Apktool是一个逆向工程工具,可以用它解码(decode)并修改apk中的资源。接下来详细介绍如何使用apktool生成渠道包。
前期工作和用Maven打包一样,也需要在AndroidManifest.xml文件中定义
元素,并在应用启动的时候读取清单文件中的渠道号。具体请参考上面的代码。和Maven不一样的是,每次打包时不再需要重新构建项目。打包时,只需生成一个apk,然后在该apk的基础上生成其他渠道包即可。
首先,使用apktool decode应用程序,在终端中输入如下命令:
apktool d your_original_apk build
上面的命令会在build目录中decode应用文件,decode完成后的目录描述如下:
目录 | 描述 |
---|---|
assets目录 | 存放需要打包到apk中的静态文件 |
lib目录 | 程序依赖的native库 |
res目录 | 存放应用程序的资源 |
smail目录 | 存放Dalvik VM内部执行的smail代码 |
AndroidManifest.xml | 应用程序的配置文件 |
apktool.yml | apktool相关配置文件 |
接下来,替换AndroidManifest.xml文件中定义的渠道号,下面是一段python脚本:
import re
def replace_channel(channel, manifest):
pattern = r'( )'
replacement = r"\g<1>{channel}\g<3>".format(channel=channel)
return re.sub(pattern, replacement, manifest)
更多有关Python的使用可参考Python教程。
然后,使用apktool构建未签名的apk:
apktool b build your_unsigned_apk
最后,使用jarsigner重新签名apk:
jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore your_keystore_path -storepass your_storepass -signedjar your_signed_apk, your_unsigned_apk, your_alias
上面就是使用apktool打包的方法,通过使用脚本可以批量地生成渠道包。不像Maven,每打一个包都需要执行一次构建过程,该方法只需构建一次,大大节省了时间,但缺点是每生成一个包需要重新签名一次。
如果能直接修改APK的渠道号,而不需要再重新签名能节省不少打包的时间。上文APK瘦身中讲述过APK解压后的目录结构,其中有个META-INF目录,是存放签名相关信息用来校验APK的完整性的,如果在META-INF目录内添加空文件,可以不用重新签名应用。因此,通过为不同渠道的应用添加不同的空文件,可以唯一标识一个渠道。
下面的python代码用来给apk添加空的渠道文件,渠道名的前缀为channel_:
import zipfile
zipped = zipfile.ZipFile(your_apk, 'a', zipfile.ZIP_DEFLATED)
empty_channel_file = "META-INF/channel_{channel}".format(channel=your_channel)
zipped.write(your_empty_file, empty_channel_file)
假设渠道名为test,则添加完空渠道文件后META-INFO目录多了一个名为channel_test的空文件:
接下来就可以在代码中读取空渠道文件名了:
public static String getChannel(Context context) {
ApplicationInfo appinfo = context.getApplicationInfo();
String sourceDir = appinfo.sourceDir;
String ret = "";
ZipFile zipfile = null;
try {
zipfile = new ZipFile(sourceDir);
Enumeration> entries = zipfile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = ((ZipEntry) entries.nextElement());
String entryName = entry.getName();
if (entryName.startsWith("channel")) {
ret = entryName;
break;
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (zipfile != null) {
try {
zipfile.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
String[] split = ret.split("_");
if (split != null && split.length >= 2) {
return ret.substring(split[0].length() + 1);
} else {
return "";
}
}
这样,每打一个渠道包只需复制一个apk,在META-INF中添加一个使用渠道号命名的空文件即可。
更多关于打包详情可参考AndroidMultiChannelBuildTool.
关于Gradle多渠道打包可以参考我的另一篇博文Android Studio常用Gradle操作,下面主要讲解如何根据各个渠道不同的需求来定制化打包,如控制是否自动更新,使用不同的包名、应用名等。
productFlavors {
t1 {
applicationId "com.example.test1"
}
t2 {
applicationId "com.example.test1"
}
}
上面的代码添加了两个渠道,两个渠道的包名不同,运行gradle assemble命令即可生成两个不同渠道的适配包。
android {
defaultConfig {
buildConfigField "boolean", "AUTO_UPDATES", "true"
}
productFlavors {
t3 {
buildConfigField "boolean", "AUTO_UPDATES", "false"
}
}
}
上面的代码会在BuildConfig类中生成AUTO_UPDATES布尔常量,默认值为true,在使用t3渠道时,该值会被设置成false。接下来就可以在代码中使用AUTO_UPDATES常量来判断是否开启自动更新功能了。最后,运行gradle assembleT3命令即可生成默认不开启自动升级功能的渠道包。
android {
productFlavors {
t4 {
}
}
}
上面的配置会默认src/t4目录为t4 flavor的dataSet。
接下来,在src目录内创建t4目录,并添加如下应用名字符串资源(src/t4/res/values/appname.xml):
<resources>
<string name="app_name">Example2string>
resources>
默认的应用名字符串资源如下(src/main/res/values/strings.xml):
<resources>
<string name="app_name">Example1string>
resources>
最后,运行gradle assembleT4命令即可生成应用名为Example2的应用了。
com.example.test3:test:1.0.0
该库,那么可以像如下这样描述:android {
productFlavors {
t5 {
}
}
}
...
dependencies {
provided 'com.example.test3:test:1.0.0'
t5Compile 'com.example.test3:test:1.0.0'
}
上面添加了名为t5的flavor,并且指定编译和运行时都依赖com.example.test3:test:1.0.0
。而其他渠道只是在构建的时候依赖该SDK,打包的时候并不会添加它。
接下来,需要在代码中使用反射技术判断应用程序是否添加了该SDK,从而决定是否要显示该SDK提供的功能。部分代码如下:
class MyActivity extends Activity {
private boolean useSdk;
@override
public void onCreate(Bundle savedInstanceState) {
try {
Class.forName("com.example.test3.Test");
useSdk = true;
} catch (ClassNotFoundException ignored) {
}
}
}
最后,运行gradle assembleT5命令即可生成包含该SDK功能的渠道包了。
反编译,又称为逆向编译技术,是指将可执行文件变成高级语言源程序的过程。反编译技术依赖于编译技术,是编译过程的逆过程。
编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。语法分析以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息。中间代码是源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现。代码优化是指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。目标代码生成是编译的最后一个阶段。
反编译器分为前端和后端,前端是一个机器依赖的模块,包含句法分析二进制程序、分析其指令的语义、并且生成该程序的低级中间表示法和每一子程序的控制流向图,通用的反编译机器是一个与语言和机器无关的模块,分析低级中间代码,将它转换成对任何高级语言都可接受的高级表示法,并且分析控制流向图的结构、把它们转换成用高级控制结构表现的图;而后端是一个目标语言依赖的模块,生成目标语言代码。
C++、C语言一般不能反编译为源代码,只能反编译为asm(汇编)语言,因为C较为底层,编译之后不保留任何元信息,而计算机运行的二进制实际上就代表了汇编指令,所以反编译为汇编是较为简单的。
C#、Java这类高级语言,尤其是需要运行环境的语言,如果没有混淆,非常容易反编译。原因很简单,这类语言只会编译为中间语言(C#为MSIL,Java为Bytecode),而中间语言与原语言本身较为相似,加上保留的元信息(记录类名、成员函数等信息)就可以反向生成源代码,注意是由反编译器生成,不会与源代码完全相同,但可以编译通过。这些特性本来是为反射技术准备的,却被反编译器利用,现在的C#反编译器ILSpy甚至可以反向工程。
4.1、解压APK,获得其中的classes.dex文件;
4.2、拷贝classes.dex文件到dex2jar工具的解压目录下,使用如下命令:d2j-dex2jar classes.dex
获得classes-dex2jar.jar文件;
4.3、使用工具jd-gui打开classes-dex2jar.jar文件,如果代码未被混淆,那么打开后就可以对除资源外的源码进行分析了;
4.4、将APK拷贝到apktool的解压目录下,使用命令apktool -d ***.apk
,其中d是decode的意思,表示我们要对***.apk
这个文件进行解码。这样可得到一个以APK名称命名的目录,该目录下就是解码后的结果了,其中的资源都是可以查看的。apktool命令除了这个基本用法之外,我们还可以再加上一些附加参数来控制decode的更多行为:
-f 如果目标文件夹已存在,则强制删除现有文件夹(默认如果目标文件夹已存在,则解码失败)。
-o 指定解码目标文件夹的名称(默认使用APK文件的名字来命名目标文件夹)。
-s 不反编译dex文件,也就是说classes.dex文件会被保留(默认会将dex文件解码成smali文件)。
-r 不反编译资源文件,也就是说resources.arsc文件会被保留(默认会将resources.arsc解码成具体的资源文件)。
4.5、假如我们修改了解码后的部分代码或资源中的内容需要重新打包,那么则使用命令apktool b *** -o New_***.apk
进行打包;
4.6、打包后还不能安装,需要重新进行签名,签名过程上文已描述过,在此就不赘述该过程了;
4.7、Android还极度建议我们对签名后的APK文件进行一次对齐操作,因为这样可以使得我们的程序在Android系统中运行得更快,对齐操作使用的是zipalign工具,该工具存放于/build-tools/目录下,对齐使用命令如下:zipalign 4 New_***.apk New_***_aligned.apk
,其中4是固定值。
***
都表示该APK的名称,还有以上所描述过程仅用作技术交流,仅限于学习。Android中的Apk反编译可能是每个开发都会经历的事,但是在反编译的过程中,对于源程序的开发者来说那是不公平的,那么Apk加固也是应运而生,现在网上有很多Apk加固的第三方平台,如以下所示:
爱加密加固
360加固
梆梆加固
其实加固有些人认为很高深的技术,其实不然,说的简单点就是对源Apk进行加密,然后在套上一层壳即可,当然还有很多细节需要处理,其简单介绍如下:
1、加壳程序
任务:对源程序Apk进行加密,合并脱壳程序的Dex文件 ,然后输入一个加壳之后的Dex文件
语言:任何语言都可以,不限于Java语言
技术点:对Dex文件格式的解析
2、脱壳程序
任务:获取源程序Apk,进行解密,然后动态加载进来,运行程序
语言:Android项目(Java)
技术点:如何从Apk中获取Dex文件,动态加载Apk,使用反射运行Application
目前来说,不管是混淆、加密还是加固都不完全是安全的,不管何时,逆向和安全都永远不会停止战争。但对于一般的应用来说,混淆和加固基本就可以保证你应用的安全了,因为不管是出于什么原因都是需要考虑时间和人力成本的。
1、美团Android自动化之旅—生成渠道包
2、美团Android自动化之旅—适配渠道包