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.yml apktool相关配置文件

接下来,替换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自动化之旅—适配渠道包

本页内容版权归属为原作者,如有侵犯您的权益,请通知我们删除。

PendingIntent的内部机制 - 2016-07-25 14:07:48

摘自;http://my.oschina.net/youranhongcha/blog/196933   1 概述         在Android中,我们常常使用PendingIntent来表达一种“留待日后处理”的意思。从这个角度来说,PendingIntent可以被理解为一种特殊的异步处理机制。不过,单就命名而言,PendingIntent其实具有一定误导性,因为它既不继承于Intent,也不包含Intent,它的核心可以粗略地汇总成四个字——“异步激发”。         很明显,这种异步激发常常
Android framework提供了许多标准的工具,来创建有吸引力的、功能丰富的用户图形界面。但是,如果你想要更多的控制权,比如在应用程序的屏幕上绘图,或者冒险进入三维图形,你需要使用不同的工具。通过Android framework提供的OpenGL ES的API提供了一套显示高端的工具,动画图像超出你的想象,许多Android设备的图像处理单元得到了加速(GPUs)。 这节课主要开发一个OpenGL应用程序、包括设置、画对象、移动对象元素、响应触摸输入事件。 这节课的示例代码使用的是OpenGL

来仿一仿retrofit - 2016-07-25 14:07:32

为什么要重复造轮子 在开发领域有一句很流行的话就是 不要重复造轮子 , 因为我们在开发中用到的很多东西早已有很多人去实现了, 而且这些实现都是经过时间和开发者检验过的, 一般不会遇到什么坑, 而如果我们自己去实现的话, 那不仅会增加工作量, 最大的隐患还是我们并不能预见以后是否会遇到大坑. 不过大家注意了吗. 上面 不要重复造轮子 的一个前提是 开发中 , 是的, 这句名言在开发中是适用的, 那在学习阶段的? 我可以大概的告诉你- 忘记这句话! , 为什么 不要重复造轮子 不适合在学习阶段使用呢? 如果我
背景 前面一篇总结了Serializable的序列化与反序列化,现在接着总结XML。主要内容:XML基本的序列化与反序列化方法、一些注意事项、以及自定义了一个XML注解框架(简洁代码,解放双手)。 XML的序列化与反序列化 先与Serializable进行简单的对比: Serializable存储的文件,打开后无法正常查看,安全性高。xml文件可通过文本编辑器查看与编辑,可读性高(浏览器会格式化xml文件,更方便查看),安全性低; Serializable文件通过了签名,只能在自己的程序中反序列化,或RM
每日更新关注 : http://weibo.com/hanjunqiang   新浪微博! iOS 开发者交流QQ群 : 446310206   有问题或技术交流可以咨询! 欢迎加入 ! 这篇直接搬了一份官方文档过来看的 由于之前没用markdown搞的乱七八糟的 所以重新做了一份 后面看到官网的中文文档更新不及时看着英文翻译了一点 搞的更乱了 :( 英文好的直接点右边- 官方OC文档 Realm 是一个移动端的数据库, Realm 是 SQLite 和 CoreData 的替代者。它可以节省你成千上万行

2.3.1 存储数据到data目录中 - 2016-07-25 14:07:09

当应用安装到Android后,系统会根据每个应用的包名创建一个/data/data/包名/的文件夹,访问自己包名下的目录是不需要权限的,并且Android已经提供了非常简便的API可以直接去访问该文件夹。 下面以一个案例来演示data目录的使用。 一、需求 如图1-5 所示,在用户将CheckBox选中的前提下点击登录,就将用户名和密码保存在data文件夹中,在用户不勾选CheckBox的前提下点击登录,就不保存用户名和密码,同时删除data文件中的历史数据。 如果数据已经保存到了data目录中,那么下次
本页面更新日期: 2016年07月23日 package 包 前面提到了 包 这个概念. 什么是包? 由于非常多的人参与 Java 的开发, 这难免会遇到一个问题 – 类名冲突 . 也就是说难免会遇到重名的情况. 所以 Java 引入了包这个机制. 提供了类的多层命名空间. 用于解决类的命名冲突 / 类文件管理等问题. Java允许将一组功能相关的类放在同一个 package 下. 从而组成逻辑上的 类库单元 . 如果希望把一个类放在指定的包结构下,应该在Java源程序的第一个非注释行放置如下格式的代码:
目录: APP项目如何与插件化无缝结合(一)  APP项目如何与插件化无缝结合(二)  APP项目如何与插件化无缝结合(三)  搬砖码字不易,转载请注明转自: http://blog.csdn.net/u011176685/article/details/52006474 上面一篇主要介绍了Small的原理,相信大家应该现在心里有个大概的了解。好,我们接下来继续开始! 一、Small的使用 关于Small的使用, Small的使用 这里讲的很详细,关于这里提下我当时遇到的问题和解决办法。 1.Small作
在Windows下试了试用Ionic开发Android应用,试通了。记录了过程。列在下面,供参考。 1. JDK 我用的jdk8,这里下载: http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html 。我老早下载的,8u66,i586(32位的),现在是8u102: http://download.oracle.com/otn-pub/java/jdk/8u102-b14/jdk-8u102-wi
最近写的项目中有用到数据库,写了不少蛋疼的sql语句,每次都是好几行代码,而且每次都是重复的没有一点技术含量的代码,虽然也有不少基于sqlite的封装,不过用起来还是感觉不够面向对象! 为了不再写重复的代码,花了几天时间,基于SQLite3简单封装了下,实现了一行代码解决增删改查等常用的功能!并没有太过高深的知识,主要用了runtime和KVC: 首先我们创建个大家都熟悉的Person类,并声明两个属性,下面将以类此展开分析 @interface Person : NSObject @property (