App的打磨之路(下)

Posted 幻影宇寰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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的节点中添加如下元素,用来定义渠道的来源:

<!-- 使用Maven打包时会用具体的渠道号替换掉$channel -->
<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>
        <!--描述存放资源的目录,该路径相对POM路径-->
        <directory>$project.basedir</directory>
        <!--是否使用参数值代替参数名-->
        <filtering>true</filtering>
        <!--描述资源的目标路径-->
        <targetPath>$project.build.directory/filtered-manifest</targetPath>
        <!--包含的模式列表,例如**/*.xml.-->
        <includes>
            <include>AndroidManifest.xml</include>
        </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文件中定义<meta-data>元素,并在应用启动的时候读取清单文件中的渠道号。具体请参考上面的代码。和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.ymlapktool相关配置文件

接下来,替换AndroidManifest.xml文件中定义的渠道号,下面是一段python脚本:

import re

def replace_channel(channel, manifest):
    pattern = r'(<meta-data\\s+android:name="channel"\\s+android:value=")(\\S+)("\\s+/>)'
    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">Example2</string>
</resources>

默认的应用名字符串资源如下(src/main/res/values/strings.xml):

<resources>
    <string name="app_name">Example1</string>
</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、工具
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自动化之旅—适配渠道包

以上是关于App的打磨之路(下)的主要内容,如果未能解决你的问题,请参考以下文章

Android开发学习之路--Notification之初体验

[js高手之路]打造通用的匀速运动框架

前端小小白的学习之路HTML5最佳实践web app

Python菜鸟之路:Django Admin后台管理功能使用

[js高手之路]构造函数的基本特性与优缺点

华为联手“北斗”4年打磨昆仑玻璃……揭秘 Mate 50 背后的技术故事!