App的打磨之路(下)

前言:该文接上两篇博文App的打磨之路(上)和App的打磨之路(中),继续描述打包、反编译及加固。

一、打包

每个Android应用在完成后都需要打成APK包,对于单个打包的方式在此就不赘述了,基本IDE都带,只是在对外发布的应用需要配置属于该应用的唯一签名,下文主要讲述需要上传多个市场的情况下怎么批量打包。

1、Maven打包

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

从以上描述中可以看出该方式每打一个包都会重新构建,执行效率太低,对于少量渠道还可以接受,渠道包过多就没法满足需求了。

2、Apktool打包

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,每打一个包都需要执行一次构建过程,该方法只需构建一次,大大节省了时间,但缺点是每生成一个包需要重新签名一次。

3、批量快速打包

如果能直接修改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.

4、Gradle定制化打包

关于Gradle多渠道打包可以参考我的另一篇博文Android Studio常用Gradle操作,下面主要讲解如何根据各个渠道不同的需求来定制化打包,如控制是否自动更新,使用不同的包名、应用名等。

  • 使用不同的包名
    如应用test有两个不同的包名,分别是com.example.test1和com.example.test2,需要对应上传到市场t1和t2,那么在productFlavors中进行如下描述:
productFlavors {
    t1 {
        applicationId "com.example.test1"
    }
    t2 {
        applicationId "com.example.test1"
    }
}

上面的代码添加了两个渠道,两个渠道的包名不同,运行gradle assemble命令即可生成两个不同渠道的适配包。

  • 控制是否自动更新
    有些客户端在启动时会默认检查客户端是否有更新,如果有更新就会提示用户下载。但是有些渠道和应用市场不允许这种默认行为,所以在适配这些渠道时需要禁止自动更新功能。一般的解决思路是提供一个配置字段,应用启动的时候检查该字段的值以决定是否开启自动更新功能。
    Gradle会在generateSources阶段为flavor生成一个BuildConfig.java文件。BuildConfig类默认提供了一些常量字段,比如应用的版本名(VERSION_NAME),应用的包名(PACKAGE_NAME)等。更强大的是,开发者还可以添加自定义的一些字段。下面的示例假设t3市场默认禁止自动更新功能:
android {
    defaultConfig {
        buildConfigField "boolean", "AUTO_UPDATES", "true"
    }

    productFlavors {
        t3 {
            buildConfigField "boolean", "AUTO_UPDATES", "false"
        }
    }
}

上面的代码会在BuildConfig类中生成AUTO_UPDATES布尔常量,默认值为true,在使用t3渠道时,该值会被设置成false。接下来就可以在代码中使用AUTO_UPDATES常量来判断是否开启自动更新功能了。最后,运行gradle assembleT3命令即可生成默认不开启自动升级功能的渠道包。

  • 使用不同的资源
    最常见的一类适配是修改应用的资源,如不同的应用名称、不同的logo、不同的启动页等。
    Gradle在构建应用时,会优先使用flavor所属dataSet中的同名资源。所以,解决思路就是在flavor的dataSet中添加同名的字符串资源,以覆盖默认的资源。下面以适配t4渠道的应用名为Example2为例进行介绍。
    首先,在build.gradle配置文件中添加如下flavor:
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的应用了。

  • 使用第三方SDK
    某些渠道会要求客户端嵌入第三方SDK来满足特定的适配需求,假设渠道t5需要引用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功能的渠道包了。

二、反编译

1、原理

反编译,又称为逆向编译技术,是指将可执行文件变成高级语言源程序的过程。反编译技术依赖于编译技术,是编译过程的逆过程。
编译程序把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。语法分析以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序。语义分析是审查源程序有无语义错误,为代码生成阶段收集类型信息。中间代码是源程序的一种内部表示,或称中间语言。中间代码的作用是可使编译程序的结构在逻辑上更为简单明确,特别是可使目标代码的优化比较容易实现。代码优化是指对程序进行多种等价变换,使得从变换后的程序出发,能生成更有效的目标代码。目标代码生成是编译的最后一个阶段。
反编译器分为前端和后端,前端是一个机器依赖的模块,包含句法分析二进制程序、分析其指令的语义、并且生成该程序的低级中间表示法和每一子程序的控制流向图,通用的反编译机器是一个与语言和机器无关的模块,分析低级中间代码,将它转换成对任何高级语言都可接受的高级表示法,并且分析控制流向图的结构、把它们转换成用高级控制结构表现的图;而后端是一个目标语言依赖的模块,生成目标语言代码。

2、语言介绍

C++、C语言一般不能反编译为源代码,只能反编译为asm(汇编)语言,因为C较为底层,编译之后不保留任何元信息,而计算机运行的二进制实际上就代表了汇编指令,所以反编译为汇编是较为简单的。
C#、Java这类高级语言,尤其是需要运行环境的语言,如果没有混淆,非常容易反编译。原因很简单,这类语言只会编译为中间语言(C#为MSIL,Java为Bytecode),而中间语言与原语言本身较为相似,加上保留的元信息(记录类名、成员函数等信息)就可以反向生成源代码,注意是由反编译器生成,不会与源代码完全相同,但可以编译通过。这些特性本来是为反射技术准备的,却被反编译器利用,现在的C#反编译器ILSpy甚至可以反向工程。

3、工具
  • dex2jar 这个工具用于将dex文件转换成jar文件
    下载地址:http://sourceforge.net/projects/dex2jar/files/
  • jd-gui 这个工具用于将jar文件转换成java代码
    下载地址:http://jd.benow.ca/
  • apktool 这个工具用于最大幅度地还原APK文件中的9-patch图片、布局、字符串等等一系列的资源
    下载地址:http://ibotpeaches.github.io/Apktool/install/
4、反编译过程

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自动化之旅—适配渠道包

你可能感兴趣的:(Android开发,Android学习笔记,打包,反编译,加固,Android)