Android性能优化之启动加速

一、前言

随着项目版本的迭代,App的性能问题会逐渐暴露出来,而好的用户体验与性能表现紧密相关,从本篇文章开始,我将开启一个Android应用性能优化的专题,从理论到实战,从入门到深挖,手把手将性能优化实践到项目中,欢迎持续关注!

那么*篇文章我就从应用的启动优化开始,根据实际案例,打造闪电般的App启动速度。

二、初识启动加速

来看一下Google官方文档《Launch-Time Performance》对应用启动优化的概述;

应用的启动分为冷启动、热启动、温启动,而启动*慢、挑战*大的就是冷启动:系统和App本身都有更多的工作要从头开始!
应用在冷启动之前,要执行三个任务:

  1. 加载启动App;
  2. App启动之后立即展示出一个空白的Window;
  3. 创建App的进程;

而这三个任务执行完毕之后会马上执行以下任务:

  1. 创建App对象;
  2. 启动Main Thread;
  3. 创建启动的Activity对象;
  4. 加载View;
  5. 布置屏幕;
  6. 进行*次绘制;

而一旦App进程完成了*次绘制,系统进程就会用Main Activity替换已经展示的Background Window,此时用户就可以使用App了。

%title插图%num

应用冷启动流程图

作为普通应用,App进程的创建等环节我们是无法主动控制的,可以优化的也就是Application、Activity创建以及回调等过程。

同样,Google也给出了启动加速的方向:

  1. 利用提前展示出来的Window,快速展示出来一个界面,给用户快速反馈的体验;
  2. 避免在启动时做密集沉重的初始化(Heavy app initialization);
  3. 定位问题:避免I/O操作、反序列化、网络操作、布局嵌套等。

备注:方向1属于治标不治本,只是表面上快;方向2、3可以真实的加快启动速度。
接下来我们就在项目中实际应用。

三、启动加速之主题切换

按照官方文档的说明:使用Activity的windowBackground主题属性来为启动的Activity提供一个简单的drawable。
Layout XML file:

        %title插图%num

资源文件配置

Manifest file:

%title插图%num

Manifest文件中

%title插图%num

Activity中

这样在启动的时候,会先展示一个界面,这个界面就是Manifest中设置的Style,等Activity加载完毕后,再去加载Activity的界面,而在Activity的界面中,我们将主题重新设置为正常的主题,从而产生一种快的感觉。不过如上文总结这种方式其实并没有真正的加速启动过程,而是通过交互体验来优化了展示的效果。
备注:截图同样来自官方文档《Launch-Time Performance》。

四、启动加速之Avoid Heavy App Initialization

通过代码分析我们可以得到App启动的业务工作流程图:

%title插图%num

App冷启动业务工作流程图

这一章节我们重点关注初始化的部分:在Application以及首屏Activity中我们主要做了:

  • MultiDex以及Tinker的初始化,*先执行;关于MultiDex的优化本文不再赘述,参考我之前Multidex的系列文章。
  • Application中主要做了各种三方组件的初始化;

项目中除听云之外其余所有三方组件都抢占先机,在Application主线程初始化。这样的初始化方式肯定是过重的:

  • 考虑异步初始化三方组件,不阻塞主线程;
  • 延迟部分三方组件的初始化;实际上我们粗粒度的把所有三方组件都放到异步任务里,可能会出现WorkThread中尚未初始化完毕但MainThread中已经使用的错误,因此这种情况建议延迟到使用前再去初始化;
  • 而如何开启WorkThread同样也有讲究,这个话题在下文详谈。

项目修改:

  1. 将友盟、Bugly、听云、GrowingIO、BlockCanary等组件放在WorkThread中初始化;
  2. 延迟地图定位、ImageLoader、自有统计等组件的初始化:地图及自有统计延迟4秒,此时应用已经打开;而ImageLoader
    因为调用关系不能异步以及过久延迟,初始化从Application延迟到SplashActivity;而EventBus因为再Activity中使用所以必须在Application中初始化。

%title插图%num

三方组件调用优化示例代码

注意:闪屏页的2秒停留可以利用,把耗时操作延迟到这个时间间隔里。

五、启动加速之Diagnosing The Problem

本节我们实际定位耗时的操作,在开发阶段我们一般使用BlockCanary或者ANRWatchDog找耗时操作,简单明了,但是无法得到每一个方法的执行时间以及更详细的对比信息。我们可以通过Method Tracing或者DDMS来获得更全面详细的信息。
启动应用,点击 Start Method Tracing,应用启动后再次点击,会自动打开刚才操作所记录下的.trace文件,建议使用DDMS来查看,功能更加方便全面。

%title插图%num

%title插图%num

优化之前应用启动trace文件分析图

左侧为发生的具体线程,右侧为发生的时间轴,下面是发生的具体方法信息。注意两列:Real Time/Call(实际发生时间),Calls+RecurCalls/Total(发生次数);
上图我们可以得到以下信息:

  • 可以直观看到MainThread的时间轴很长,说明大多数任务都是在MainThread中执行;
  • 通过Real Time/Call 降序排列可以看到程序中的部分代码确实非常耗时;
  • 在下一页可以看出来部分三方SDK也比较耗时;

即便是耗时操作,但是只要正确发生在WorkThread就没问题。因此我们需要确认这些方法执行的线程以及发生的时机。这些操作如果发生在主线程,可能不构成ANR的发生条件,但是卡顿是再算难免的!结合上章节图App冷启动业务工作流程图中业务操作以及分析图,再次查看代码我们可以看到:部分耗时操作例如IO读取等确实发生在主线程。事实上在traceview里点击执行函数的名称不仅可以跟踪到父类及子类的方法耗时,也可以在方法执行时间轴中看到具体在哪个线程以及耗时的界面闪动。

分析到部分耗时操作发生在主线程,那我们把耗时操作都改到子线程是不是就万事大吉了?非也!!

  • 卡顿不能都靠异步来解决,错误的使用工程线程不仅不能改善卡顿,反而可能加剧卡顿。是否需要开启工作线程需要根据具体的性能瓶颈根源具体分析,对症下药,不可一概而论;
  • 而如何开启线程同样也有学问:Thread、ThreadPoolExecutor、AsyncTask、HandlerThread、IntentService等都各有利弊;例如通常情况下ThreadPoolExecutor比Thread更加高效、优势明显,但是特定场景下单个时间点的表现Thread会比ThreadPoolExecutor好:同样的创建对象,ThreadPoolExecutor的开销明显比Thread大;
  • 正确的开启线程也不能包治百病,例如执行网络请求会创建线程池,而在Application中正确的创建线程池势必也会降低启动速度;因此延迟操作也必不可少。

通过对traceview的详细跟踪以及代码的详细比对,我发现卡顿发生在:

  • 部分数据库及IO的操作发生在首屏Activity主线程;
  • Application中创建了线程池;
  • 首屏Activity网络请求密集;
  • 工作线程使用未设置优先级;
  • 信息未缓存,重复获取同样信息;
  • 流程问题:例如闪屏图每次下载,当次使用;

以及其它细节问题:

  • 执行无用老代码;
  • 执行开发阶段使用的代码;
  • 执行重复逻辑;
  • 调用三方SDK里或者Demo里的多余代码;

项目修改:
1. 数据库及IO操作都移到工作线程,并且设置线程优先级为THREAD_PRIORITY_BACKGROUND,这样工作线程*多能获取到10%的时间片,优先保证主线程执行。

2. 流程梳理,延后执行;
实际上,这一步对项目启动加速*有效果。通过流程梳理发现部分流程调用时机偏早、失误等,例如:

  • 更新等操作无需在首屏尚未展示就调用,造成资源竞争;
  • 调用了IOS为了规避审核而做的开关,造成网络请求密集;
  • 自有统计在Application的调用里创建数量固定为5的线程池,造成资源竞争,在上图traceview功能说明图中*后一行可以看到编号12执行5次,耗时排名前列;此处线程池的创建是必要但可以延后的。
  • 修改广告闪屏逻辑为下次生效。

3.其它优化;

  • 去掉无用但被执行的老代码;
  • 去掉开发阶段使用但线上被执行的代码;
  • 去掉重复逻辑执行代码;
  • 去掉调用三方SDK里或者Demo里的多余代码;
  • 信息缓存,常用信息只在*次获取,之后从缓存中取;
  • 项目是多进程架构,只在主进程执行Application的onCreate();

%title插图%num

业务代码优化示例

通过以上三步及三方组件的优化:Application以及首屏Activity回调期间主线程就没有耗时、争抢资源等情况了。此外还涉及布局优化、内存优化等部分技术,因对于应用冷启动一般不是瓶颈点,这里不展开详谈,可根据实际项目实际处理。

六、对比效果:

通过ADB命令统计应用的启动时间:adb shell am start -W 首屏Activity。
同等条件下使用MX3及Nexus6P,启动5次,比较优化前与优化后的启动时间;

优化前:
MX3

ThisTime TotalTime WaitTime
1237 2205 2214
1280 2181 2189
1622 2508 2513
1485 2434 2443
1442 2418 2429

Nexus6P

ThisTime TotalTime WaitTime
1229 1832 1868
1268 1849 1880
1184 1780 1812
1262 1845 1876
1164 1766 1807

优化后:
MX3

ThisTime TotalTime WaitTime
865 1516 1523
911 1565 1573
812 1406 1418
962 1564 1574
925 1566 1577

Nexus6P

ThisTime TotalTime WaitTime
603 1192 1243
614 1076 1115
650 1120 1163
642 1107 1139
624 1084 1124

对比:
MX3提升35%

ThisTime平均数 TotalTime平均数 WaitTime平均数
优化前 1413 2349 2357
优化后 895 1523 1533

Nexus6P提升39%

ThisTime平均数 TotalTime平均数 WaitTime平均数
优化前 1221 1814 1848
优化后 626 1115 1156
  • 命令含义:
    ThisTime:*后一个启动的Activity的启动耗时;
    TotalTime:自己的所有Activity的启动耗时;
    WaitTime: ActivityManagerService启动App的Activity时的总时间(包括当前Activity的onPause()和自己Activity的启动)。

七、问题:

1、还可以继续优化的方向?

  • 项目里使用Retrofit网络请求库,FastConverterFactory做Json解析器,TraceView中看到FastConverterFactory在创建过程中也比较耗时,考虑将其换为GsonConverterFactory。但是因为类的继承关系短时间内无法直接替换,作为优化点暂时遗留;
  • 可以考虑根据实际情况将启动时部分接口合并为一,减少网络请求次数,降低频率;
  • 相同功能的组件只保留一个,例如:友盟、GrowingIO、自有统计等功能重复;
  • 使用ReDex进行优化;实验Redex发现Apk体积确实是小了一点,但是启动速度没有变化,或许需要继续研究。

2、异步、延迟初始化及操作的依据?
注意一点:并不是每一个组件的初始化以及操作都可以异步或延迟;是否可以取决组件的调用关系以及自己项目具体业务的需要。保证一个准则:可以异步的都异步,不可以异步的尽量延迟。让应用先启动,再操作。

3、通用应用启动加速套路?

  • 利用主题快速显示界面;
  • 异步初始化组件;
  • 梳理业务逻辑,延迟初始化组件、操作;
  • 正确使用线程;
  • 去掉无用代码、重复逻辑等。

4、其它

  • 将启动速度加快了35%不代表之前的代码都是问题,从业务角度上将,代码并没有错误,实现了业务需求。但是在启动时这个注重速度的阶段,忽略的细节就会导致性能的瓶颈。
  • 开发过程中,对核心模块与应用阶段如启动时,使用TraceView进行分析,尽早发现瓶颈。

Multidex之源码解析

一、初识MultiDex

开发Android应用的小伙伴,在经历了众多版本迭代、PM不断加入新功能、尝试新技术引入类库之后,产物Apk急剧膨胀;*终会遇到那个传说中的Android64K方法数问题;具体表现:

Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536

编译打包失败,不能愉快的开发了。

超过*大方法数限制的问题,是由于Dex文件格式限制,一个Dex文件中method个数采用使用原生类型short来索引文件中的方法,4个字节共计*多表达65536个method,field/class的个数也均有此限制。生成Dex文件的过程,是将工程所需全部class文件合并且压缩到一个Dex文件期间,也就是Android打包的Dex过程中, 单个Dex文件可被引用的方法总数(自己开发的代码以及所引用的Android框架、类库的代码)被限制为65536;

但是这种小问题怎么能难倒程序猿哥哥呢,相信大家或多或少都听说过Multidex:Google官方对64K方法数问题的一种补救措施,通俗的讲就是:既然你的代码这么多,一个Dex装不下,那就给你多个Dex来装呗。Multidex在构建打包阶段将Class拆分到多个Dex,使之不超过单Dex*大方法数的限制;这样打包就不会失败了。

但是只解决分Dex打包的问题还不够,我们知道Dalvik虚拟机应用启动时默认只会装载classes.dex,那ClassLoader肯定是无法从别的Dex中查找Class的,从而程序运行过程中的各种ClassNotFoundException画面太美简直不敢想象。于是机智如Google又赋予MultiDex另外一项能力:在运行时动态装载别的非主Dex,于是乎一个看似完美的分Dex加载方案就诞生了。

具体的使用指南可以异步官方文档;

二、Multidex工作流程

在分析源码之前,我们先来看一下MultiDex的工作流程,对它有一个初步的认识;

%title插图%num
屏幕快照 2016-12-14 下午8.41.00.png

总结:

  • 运行时提取别的非主Dex出来,然后动态装载执行。
  • 需要在源码分析过程中重点关注提取Dex以及动态装载这两个过程。

三、源码分析

MultiDex入口:MultiDex.install();

<pre class=”hljs undefined” data-original-code=”” public=”” static=”” void=”” install(context=”” context)=”” {“=”” data-snippet-id=”ext.bb72b527428b0b3e3b0168e932d26575″ data-snippet-saved=”false” data-codota-status=”done”>

  1. public static void install(Context context) {
  2. Log.i(“MultiDex”, “install”);
  3. if(IS_VM_MULTIDEX_CAPABLE) {
  4. //判断VM是否支持Multidex,本身就支持的话MultiDex库则被禁用;
  5. Log.i(“MultiDex”, “VM has multidex support, MultiDex support library is disabled.”);
  6. } else if(VERSION.SDK_INT < 4) {
  7. //*低兼容SDK版本是4,不过现在4以下的机器已经是纪念品了吧;
  8. throw new RuntimeException(“Multi dex installation failed. SDK “ + VERSION.SDK_INT + ” is unsupported. Min SDK version is “ + 4 + “.”);
  9. } else {
  10. try {
  11. //获取Apk信息;
  12. ApplicationInfo e = getApplicationInfo(context);
  13. if(e == null) {
  14. return;
  15. }
  16. Set var2 = installedApk;
  17. //加锁保证只执行一次;
  18. synchronized(installedApk) {
  19. String apkPath = e.sourceDir;
  20. if(installedApk.contains(apkPath)) {
  21. return;
  22. }
  23. installedApk.add(apkPath);
  24. if(VERSION.SDK_INT > 20) {
  25. Log.w(“MultiDex”, “MultiDex is not guaranteed to work in SDK version “ + VERSION.SDK_INT + “: SDK version higher than “ + 20 + ” should be backed by “ + “runtime with built-in multidex capabilty but it\’s not the “ + “case here: java.vm.version=\”” + System.getProperty(“java.vm.version”) + “\””);
  26. }
  27. ClassLoader loader;
  28. try {
  29. //获取当前ClassLoader实例,提取出来的Dex需要通过ClassLoader真正的被加载执行;
  30. loader = context.getClassLoader();
  31. } catch (RuntimeException var9) {
  32. Log.w(“MultiDex”, “Failure while trying to obtain Context class loader. Must be running in test mode. Skip patching.”, var9);
  33. return;
  34. }
  35. if(loader == null) {
  36. Log.e(“MultiDex”, “Context class loader is null. Must be running in test mode. Skip patching.”);
  37. return;
  38. }
  39. try {
  40. //清除OldDexDir的目录;此处有歧义,下面讲。
  41. clearOldDexDir(context);
  42. } catch (Throwable var8) {
  43. Log.w(“MultiDex”, “Something went wrong when trying to clear old MultiDex extraction, continuing without cleaning.”, var8);
  44. }
  45. //创建提取Dex缓存的路径。
  46. File dexDir = new File(e.dataDir, SECONDARY_FOLDER_NAME);
  47. //重要方法!!!提取除主Dex之外别的Dex出来;
  48. List files = MultiDexExtractor.load(context, e, dexDir, false);
  49. //校验提取出来的Dex文件的合法性;
  50. if(checkValidZipFiles(files)) {
  51. //重要方法!!!合法的话则进入安装过程;
  52. installSecondaryDexes(loader, dexDir, files);
  53. } else {
  54. //校验不合法,强制重新执行一次Dex的提取,不抛弃、不放弃,哈哈。
  55. Log.w(“MultiDex”, “Files were not valid zip files. Forcing a reload.”);
  56. files = MultiDexExtractor.load(context, e, dexDir, true);
  57. if(!checkValidZipFiles(files)) {
  58. //提取出来再次校验,仍然不合法,不能忍,放弃!分分钟抛异常给你看!
  59. throw new RuntimeException(“Zip files were not valid.”);
  60. }
  61. //合法的话则进入安装过程;
  62. installSecondaryDexes(loader, dexDir, files);
  63. }
  64. }
  65. } catch (Exception var11) {
  66. Log.e(“MultiDex”, “Multidex installation failure”, var11);
  67. throw new RuntimeException(“Multi dex installation failed (“ + var11.getMessage() + “).”);
  68. }
  69. Log.i(“MultiDex”, “install done”);
  70. }
  71. }

总结:

  • 进行各种预校验以及获取需要的信息;
  • 重要方法:MultiDexExtractor.load(context, e, dexDir, false),将Dex文件提取出来;
  • 重要方法:installSecondaryDexes(loader, dexDir, files),安装提取出来的Dex文件。

提取器MultiDexExtractor.load

  1. //重要方法
  2. static List<File> load(Context context, ApplicationInfo applicationInfo, File dexDir, boolean forceReload) throws IOException {
  3. Log.i(“MultiDex”, “MultiDexExtractor.load(“ + applicationInfo.sourceDir + “, “ + forceReload + “)”);
  4. File sourceApk = new File(applicationInfo.sourceDir);
  5. //获取Crc校验码,做文件完整性校验;
  6. long currentCrc = getZipCrc(sourceApk);
  7. List files;
  8. //是否是强制性提取或者源文件发生了变化
  9. if(!forceReload && !isModified(context, sourceApk, currentCrc)) {
  10. try {
  11. //非强制性提取,且源文件未发生变化,直接使用缓存的dex文件。
  12. files = loadExistingExtractions(context, sourceApk, dexDir);
  13. } catch (IOException var9) {
  14. Log.w(“MultiDex”, “Failed to reload existing extracted secondary dex files, falling back to fresh extraction”, var9);
  15. //异常则重新执行强制性提取,并更新提取出来的Dex信息,存在SharedPreference中。
  16. files = performExtractions(sourceApk, dexDir);
  17. //缓存下来lastModified时间戳;Crc校验码,Dex的总数量等信息用于下次比对。
  18. putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
  19. }
  20. } else {
  21. //强制性提取,并更新提取出来的Dex信息,存在SharedPreference中。
  22. Log.i(“MultiDex”, “Detected that extraction must be performed.”);
  23. files = performExtractions(sourceApk, dexDir);
  24. //缓存下来lastModified时间戳;Crc校验码,Dex的总数量等信息用于下次比对。
  25. putStoredApkInfo(context, getTimeStamp(sourceApk), currentCrc, files.size() + 1);
  26. }
  27. Log.i(“MultiDex”, “load found “ + files.size() + ” secondary dex files”);
  28. return files;
  29. }

总结:

  • 制性提取或者源文件发生变化则重新提取,否则直接使用缓存dex文件;

performExtractions()

  1. //重要方法
  2. private static List<File> performExtractions(File sourceApk, File dexDir) throws IOException {
  3. //匹配的后缀;
  4. String extractedFilePrefix = sourceApk.getName() + “.classes”;
  5. //准备Dex缓存的路径;
  6. prepareDexDir(dexDir, extractedFilePrefix);
  7. ArrayList files = new ArrayList();
  8. ZipFile apk = new ZipFile(sourceApk);
  9. try {
  10. int e = 2;
  11. for(ZipEntry dexFile = apk.getEntry(“classes” + e + “.dex”); dexFile != null; dexFile = apk.getEntry(“classes” + e + “.dex”)) {
  12. String fileName = extractedFilePrefix + e + “.zip”;
  13. //提取出来的文件,zip格式。
  14. File extractedFile = new File(dexDir, fileName);
  15. files.add(extractedFile);
  16. Log.i(“MultiDex”, “Extraction is needed for file “ + extractedFile);
  17. int numAttempts = 0;
  18. boolean isExtractionSuccessful = false;
  19. //每个dex的提取都尝试三次;
  20. while(numAttempts < 3 && !isExtractionSuccessful) {
  21. ++numAttempts;
  22. //真正的提取。将源Apk解压,将非主Dex文件写为zip文件。
  23. extract(apk, dexFile, extractedFile, extractedFilePrefix);
  24. isExtractionSuccessful = verifyZipFile(extractedFile);
  25. Log.i(“MultiDex”, “Extraction “ + (isExtractionSuccessful?“success”:“failed”) + ” – length “ + extractedFile.getAbsolutePath() + “: “ + extractedFile.length());
  26. if(!isExtractionSuccessful) {
  27. //提取出来的文件未校验通过则删除。
  28. extractedFile.delete();
  29. if(extractedFile.exists()) {
  30. Log.w(“MultiDex”, “Failed to delete corrupted secondary dex \'” + extractedFile.getPath() + “\'”);
  31. }
  32. }
  33. }
  34. if(!isExtractionSuccessful) {
  35. throw new IOException(“Could not create zip file “ + extractedFile.getAbsolutePath() + ” for secondary dex (“ + e + “)”);
  36. }
  37. ++e;
  38. }
  39. } finally {
  40. try {
  41. apk.close();
  42. } catch (IOException var16) {
  43. Log.w(“MultiDex”, “Failed to close resource”, var16);
  44. }
  45. }
  46. return files;
  47. }

总结:

  • 准备Dex缓存的目录,并且删除其中不是以name.apk.classes开头的文件;
  • 每个Dex的提取*多尝试三次;

真实提取extract(apk, dexFile, extractedFile, extractedFilePrefix);

  1. /**
  2. * 重要方法
  3. * @param apk apk源文件:/data/app/apkName.apk;
  4. * @param dexFile apk源文件解压出来的Dex文件:classes2.dex等;
  5. * @param extractTo 提取出来的文件;
  6. * @param extractedFilePrefix 提取出来的文件前缀;
  7. * @throws IOException
  8. * @throws FileNotFoundException
  9. */
  10. private static void extract(ZipFile apk, ZipEntry dexFile, File extractTo, String extractedFilePrefix) throws IOException, FileNotFoundException {
  11. InputStream in = apk.getInputStream(dexFile);
  12. ZipOutputStream out = null;
  13. File tmp = File.createTempFile(extractedFilePrefix, “.zip”, extractTo.getParentFile());
  14. Log.i(“MultiDex”, “Extracting “ + tmp.getPath());
  15. try {
  16. out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(tmp)));
  17. try {
  18. ZipEntry classesDex = new ZipEntry(“classes.dex”);
  19. classesDex.setTime(dexFile.getTime());
  20. out.putNextEntry(classesDex);
  21. byte[] buffer = new byte[16384];
  22. for(int length = in.read(buffer); length != -1; length = in.read(buffer)) {
  23. out.write(buffer, 0, length);
  24. }
  25. out.closeEntry();
  26. } finally {
  27. out.close();
  28. }
  29. Log.i(“MultiDex”, “Renaming to “ + extractTo.getPath());
  30. if(!tmp.renameTo(extractTo)) {
  31. throw new IOException(“Failed to rename \”” + tmp.getAbsolutePath() + “\” to \”” + extractTo.getAbsolutePath() + “\””);
  32. }
  33. } finally {
  34. closeQuietly(in);
  35. tmp.delete();
  36. }
  37. }

总结:

  • 将Apk源文件进行解压,将其中的非主Dex文件提取为zip文件。

终于将非主Dex文件提取出来了,接下来就是令人激动的安装过程了。
分析SDK19以上的为例:

  1. private static void installSecondaryDexes(ClassLoader loader, File dexDir, List<File> files) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException {
  2. //根据不同版本做不同处理;
  3. if(!files.isEmpty()) {
  4. if(VERSION.SDK_INT >= 19) {
  5. MultiDex.V19.install(loader, files, dexDir);
  6. } else if(VERSION.SDK_INT >= 14) {
  7. MultiDex.V14.install(loader, files, dexDir);
  8. } else {
  9. MultiDex.V4.install(loader, files);
  10. }
  11. }
  12. }
  13. private static final class V19 {
  14. private V19() {
  15. }
  16. private static void install(ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
  17. //反射获取到应用ClassLoader的pathList字段;
  18. Field pathListField = MultiDex.findField(loader, “pathList”);
  19. Object dexPathList = pathListField.get(loader);
  20. ArrayList suppressedExceptions = new ArrayList();
  21. //将刚刚提取出来的zip文件包装成Element对象,并扩展DexPathList中的dexElements数组字段;
  22. MultiDex.expandFieldArray(dexPathList, “dexElements”, makeDexElements(dexPathList, new ArrayList(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));
  23. if(suppressedExceptions.size() > 0) {
  24. Iterator suppressedExceptionsField = suppressedExceptions.iterator();
  25. while(suppressedExceptionsField.hasNext()) {
  26. IOException dexElementsSuppressedExceptions = (IOException)suppressedExceptionsField.next();
  27. Log.w(“MultiDex”, “Exception in makeDexElement”, dexElementsSuppressedExceptions);
  28. }
  29. Field suppressedExceptionsField1 = MultiDex.findField(loader, “dexElementsSuppressedExceptions”);
  30. IOException[] dexElementsSuppressedExceptions1 = (IOException[])((IOException[])suppressedExceptionsField1.get(loader));
  31. if(dexElementsSuppressedExceptions1 == null) {
  32. dexElementsSuppressedExceptions1 = (IOException[])suppressedExceptions.toArray(new IOException[suppressedExceptions.size()]);
  33. } else {
  34. IOException[] combined = new IOException[suppressedExceptions.size() + dexElementsSuppressedExceptions1.length];
  35. suppressedExceptions.toArray(combined);
  36. System.arraycopy(dexElementsSuppressedExceptions1, 0, combined, suppressedExceptions.size(), dexElementsSuppressedExceptions1.length);
  37. dexElementsSuppressedExceptions1 = combined;
  38. }
  39. suppressedExceptionsField1.set(loader, dexElementsSuppressedExceptions1);
  40. }
  41. }
  42. private static Object[] makeDexElements(Object dexPathList, ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
  43. //反射调用DexPathList对象中的makeDexElements方法,将刚刚提取出来的zip文件包装成Element对象;
  44. Method makeDexElements = MultiDex.findMethod(dexPathList, “makeDexElements”, new Class[]{ArrayList.class, File.class, ArrayList.class});
  45. return (Object[])((Object[])makeDexElements.invoke(dexPathList, new Object[]{files, optimizedDirectory, suppressedExceptions}));
  46. }
  47. }
  48. /**
  49. * makeDexElements方法*终会调用到这个方法;
  50. * 其中会在Native执行dexopt的优化操作,生成odex文件,此是一个耗时的操作。
  51. *
  52. * @param file dex文件
  53. * @param optimizedDirectory 优化后文件的保存路径
  54. * @return
  55. * @throws IOException
  56. */
  57. private static DexFile loadDexFile(File file, File optimizedDirectory)
  58. throws IOException {
  59. if (optimizedDirectory == null) {
  60. return new DexFile(file);
  61. } else {
  62. String optimizedPath = optimizedPathFor(file, optimizedDirectory);
  63. return DexFile.loadDex(file.getPath(), optimizedPath, 0);
  64. }
  65. }

总结:

  • 反射获取ClassLoader中的pathList字段;
  • 反射调用DexPathList对象中的makeDexElements方法,将刚刚提取出来的zip文件包装成Element对象;
  • 将包装成的Element对象扩展到DexPathList中的dexElements数组字段里;
  • makeDexElements中有dexopt的操作,是一个耗时的过程,产物是一个优化过的odex文件。

至此:提取出来的dex文件也被加到了ClassLoader里,而那些Class也就可以被ClassLoader所找到并使用。

跟随源码一步步揭开了Multidex的神秘面纱,再回头看Multidex的工作流程图,就更加清晰明了。

四、问题

1、clearOldDexDir(context)是干嘛的?每一次都清除上一次提取并缓存的Dex?

<br />No,如果只看multidex-1.0.1的代码,clearOldDexDir其实什么事情都没干,因为清除的是data/data/packageName/files/secondary-dexes文件夹下的文件,但是这个文件夹从始至终都没有被使用过。看*新MultiDex库文件Master分支的代码:
获取缓存Dex目录的时候出现过,如果正常缓存目录创建失败,则data/data/packageName/files/secondary-dexes作为临时缓存目录。

  1. /**
  2. * 获取缓存Dex文件的目录
  3. *
  4. * @param context
  5. * @param applicationInfo
  6. * @return
  7. * @throws IOException
  8. */
  9. private static File getDexDir(Context context, ApplicationInfo applicationInfo)
  10. throws IOException {
  11. File cache = new File(applicationInfo.dataDir, CODE_CACHE_NAME);
  12. try {
  13. // 优先在data/data/pgn/code_cache/secondary-dexes目录创建;
  14. mkdirChecked(cache);
  15. } catch (IOException e) {
  16. //创建失败则在data/data/pgn/files/secondary-dexes目录下创建,作为临时存储目录。
  17. /* If we can’t emulate code_cache, then store to filesDir. This means abandoning useless
  18. * files on disk if the device ever updates to android 5+. But since this seems to
  19. * happen only on some devices running android 2, this should cause no pollution.
  20. */
  21. cache = new File(context.getFilesDir(), CODE_CACHE_NAME);
  22. mkdirChecked(cache);
  23. }
  24. File dexDir = new File(cache, CODE_CACHE_SECONDARY_FOLDER_NAME);
  25. mkdirChecked(dexDir);
  26. return dexDir;
  27. }

发布的multidex-1.0.1其实不会出现这个目录,而且这段的逻辑也不严谨,如果临时目录也创建失败了呢?

2、动态装载Dex的过程为什么反射那些字段、方法就可以了?

<br />这就涉及到Android中的Class加载机制了,ClassLoader加载Class调用的是BaseDexClassLoader中findClass方法,会调用到DexPathList的findClass方法,其中会对dexElements数组进行遍历,数组每一个元素对应了一个DexFile,真正的加载是在DexFile实现。而正是因为这个数组,使我们有机会将Dex包装成的Element对象扩展到其中。这样ClassLoader加载Class的时候就也会遍历调用到加进来的Dex,从而找到需要的Class。

3、为什么上面写这是一个看似完美的分Dex加载方案?

<br />①INSTALL_FAILED_DEXOPT;在部分机型会出现无法安装的问题没有解决。

这是由于dexopt的LinearAlloc限制引起的,在Android版本不同分别经历了4M/5M/8M/16M限制,4.2.x系统上可能都已到16M, 在Gingerbread或者以下系统LinearAllocHdr分配空间只有5M大小的, 高于Gingerbread的系统提升到了8M。Dalvik linearAlloc是一个固定大小的缓冲区。在应用的安装过程中,系统会运行一个名为dexopt的程序为该应用在当前机型中运行做准备。dexopt使用LinearAlloc来存储应用的方法信息。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB或16MB。当方法数量过多导致超出缓冲区大小时,会造成dexopt崩溃。

也就是说,即便是方法数不超标,也不能保证一定能安装成功,因为DexOpt过程可能因为LinearAlloc的限制而失败。但是这个问题为什么之前没有提出呢?因为这个问题对目前的Android市场机型基本不存在,现在一般Android应用的*低兼容版本都是4.0,*可能出现这个问题的2.3之前的版本都不在考虑之列,而且目前5.0以上的机型占有率已经接近70%,低版4.0本机型已经越来越少,而且基本是4.0机型,也只是有可能触发这个限制,因此对目前来讲是个不是问题的问题。
<br />②ANR的问题:从以上MultiDex的工作流程可以看到:MultiDex工作在主线程,而Dex的提取与DexOpt的过程都是耗时的操作,所以ANR的问题是必然存在;而且业务量越大,拆分出来的Dex越多,对应ANR的几率也就越高。

apk签名机制

发布过Android应用的朋友们应该都知道,Android APK的发布是需要签名的。签名机制在Android应用和框架中有着十分重要的作用。

例如,Android系统禁止更新安装签名不一致的APK;如果应用需要使用system权限,必须保证APK签名与Framework签名一致,等等。在《APK Crack》一文中,我们了解到,要破解一个APK,必然需要重新对APK进行签名。而这个签名,一般情况无法再与APK原先的签名保持一致。(除非APK原作者的私钥泄漏,那已经是另一个层次的软件安全问题了。)

简单地说,签名机制标明了APK的发行机构。因此,站在软件安全的角度,我们就可以通过比对APK的签名情况,判断此APK是否由“官方”发行,而不是被破解篡改过重新签名打包的“盗版软件”。

Android签名机制

为了说明APK签名比对对软件安全的有效性,我们有必要了解一下Android APK的签名机制。为了更易于大家理解,我们从Auto-Sign工具的一条批处理命令说起。

在《APK Crack》一文中,我们了解到,要签名一个没有签名过的APK,可以使用一个叫作Auto-sign的工具。Auto-sign工具实际运行的是一个叫做Sign.bat的批处理命令。用文本编辑器打开这个批处理文件,我们可以发现,实现签名功能的命令主要是这一行命令:

 

java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk
这条命令的意义是:通过signapk.jar这个可执行jar包,以“testkey.x509.pem”这个公钥文件和“testkey.pk8”这个私钥文件对“update.apk”进行签名,签名后的文件保存为“update_signed.apk”。

对于此处所使用的私钥和公钥的生成方式,这里就不做进一步介绍了。这方面的资料大家可以找到很多。我们这里要讲的是signapk.jar到底做了什么。

signapk.jar是Android源码包中的一个签名工具。由于Android是个开源项目,所以,很高兴地,我们可以直接找到signapk.jar的源码!路径为/build/tools/signapk/SignApk.java。

对比一个没有签名的APK和一个签名好的APK,我们会发现,签名好的APK包中多了一个叫做META-INF的文件夹。里面有三个文件,分别名为MANIFEST.MF、CERT.SF和CERT.RSA。signapk.jar就是生成了这几个文件(其他文件没有任何改变。因此我们可以很容易去掉原有签名信息)。

通过阅读signapk源码,我们可以理清签名APK包的整个过程。

1、 生成MANIFEST.MF文件:

程序遍历update.apk包中的所有文件(entry),对非文件夹非签名文件的文件,逐个生成SHA1的数字签名信息,再用Base64进行编码。具体代码见这个方法:

 

private static Manifest addDigestsToManifest(JarFile jar)

关键代码如下:

1 for (JarEntry entry: byName.values()) {
2 String name = entry.getName();
3 if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) &&
4 !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) &&
5 (stripPattern == null ||!stripPattern.matcher(name).matches())) {
6 InputStream data = jar.getInputStream(entry);
7 while ((num = data.read(buffer)) > 0) {
8 md.update(buffer, 0, num);
9 }
10 Attributes attr = null;
11 if (input != null) attr = input.getAttributes(name);
12 attr = attr != null ? new Attributes(attr) : new Attributes();
13 attr.putValue(“SHA1-Digest”, base64.encode(md.digest()));
14 output.getEntries().put(name, attr);
15 }
16 }

之后将生成的签名写入MANIFEST.MF文件。关键代码如下:

 

1 Manifest manifest = addDigestsToManifest(inputJar);
2 je = new JarEntry(JarFile.MANIFEST_NAME);
3 je.setTime(timestamp);
4 outputJar.putNextEntry(je);
5 manifest.write(outputJar);

这里简单介绍下SHA1数字签名。简单地说,它就是一种安全哈希算法,类似于MD5算法。它把任意长度的输入,通过散列算法变成固定长度的输出(这里我们称作“摘要信息”)。你不能仅通过这个摘要信息复原原来的信息。另外,它保证不同信息的摘要信息彼此不同。因此,如果你改变了apk包中的文件,那么在apk安装校验时,改变后的文件摘要信息与MANIFEST.MF的检验信息不同,于是程序就不能成功安装。

2、 生成CERT.SF文件:

对前一步生成的Manifest,使用SHA1-RSA算法,用私钥进行签名。关键代码如下:

1 Signature signature = Signature.getInstance(“SHA1withRSA”);
2 signature.initSign(privateKey);
3 je = new JarEntry(CERT_SF_NAME);
4 je.setTime(timestamp);
5 outputJar.putNextEntry(je);
6 writeSignatureFile(manifest,
7 new SignatureOutputStream(outputJar, signature));

RSA是一种非对称加密算法。用私钥通过RSA算法对摘要信息进行加密。在安装时只能使用公钥才能解密它。解密之后,将它与未加密的摘要信息进行对比,如果相符,则表明内容没有被异常修改。

3、 生成CERT.RSA文件:

生成MANIFEST.MF没有使用密钥信息,生成CERT.SF文件使用了私钥文件。那么我们可以很容易猜测到,CERT.RSA文件的生成肯定和公钥相关。

CERT.RSA文件中保存了公钥、所采用的加密算法等信息。核心代码如下:

 

1 je = new JarEntry(CERT_RSA_NAME);
2 je.setTime(timestamp);
3 outputJar.putNextEntry(je);
4 writeSignatureBlock(signature, publicKey, outputJar);

其中writeSignatureBlock的代码如下:

 

1 private static void writeSignatureBlock(
2 Signature signature, X509Certificate publicKey, OutputStream out)
3 throws IOException, GeneralSecurityException {
4 SignerInfo signerInfo = new SignerInfo(
5 new X500Name(publicKey.getIssuerX500Principal().getName()),
6 publicKey.getSerialNumber(),
7 AlgorithmId.get(“SHA1”),
8 AlgorithmId.get(“RSA”),
9 signature.sign());
10
11 PKCS7 pkcs7 = new PKCS7(
12 new AlgorithmId[] { AlgorithmId.get(“SHA1”) },
13 new ContentInfo(ContentInfo.DATA_OID, null),
14 new X509Certificate[] { publicKey },
15 new SignerInfo[] { signerInfo });
16
17 pkcs7.encodeSignedData(out);
18 }

好了,分析完APK包的签名流程,我们可以清楚地意识到:

1、 Android签名机制其实是对APK包完整性和发布机构唯一性的一种校验机制。

2、 Android签名机制不能阻止APK包被修改,但修改后的再签名无法与原先的签名保持一致。(拥有私钥的情况除外)。

3、 APK包加密的公钥就打包在APK包内,且不同的私钥对应不同的公钥。换句话言之,不同的私钥签名的APK公钥也必不相同。所以我们可以根据公钥的对比,来判断私钥是否一致。

APK签名比对的实现方式

好了,通过Android签名机制的分析,我们从理论上证明了通过APK公钥的比对能判断一个APK的发布机构。并且这个发布机构是很难伪装的,我们暂时可以认为是不可伪装的。

有了理论基础后,我们就可以开始实践了。那么如何获取到APK文件的公钥信息呢?因为Android系统安装程序肯定会获取APK信息进行比对,所以我们可以通过Android源码获得一些思路和帮助。

源码中有一个隐藏的类用于APK包的解析。这个类叫PackageParser,路径为frameworks\base\core\java\android\content\pm\PackageParser.java。当我们需要获取APK包的相关信息时,可以直接使用这个类,下面代码就是一个例子函数:

 

1 private PackageInfo parsePackage(String archiveFilePath, int flags){
2
3 PackageParser packageParser = new PackageParser(archiveFilePath);
4 DisplayMetrics metrics = new DisplayMetrics();
5 metrics.setToDefaults();
6 final File sourceFile = new File(archiveFilePath);
7 PackageParser.Package pkg = packageParser.parsePackage(
8 sourceFile, archiveFilePath, metrics, 0);
9 if (pkg == null) {
10 return null;
11 }
12
13 packageParser.collectCertificates(pkg, 0);
14
15 return PackageParser.generatePackageInfo(pkg, null, flags, 0, 0);
16 }

其中参数archiveFilePath指定APK文件路径;flags需设置PackageManager.GET_SIGNATURES位,以保证返回证书签名信息。

具体如何通过PackageParser获取签名信息在此处不做详述,具体代码请参考PackageParser中的public boolean collectCertificates(Package pkg, int flags)和private Certificate[] loadCertificates(JarFile jarFile, JarEntry je, byte[] readBuffer)方法。至于如何在Android应用开发中使用隐藏的类及方法,可以参看我的这篇文章:《Android应用开发中如何使用隐藏API》。

紧接着,我们就可以通过packageInfo.signatures来访问到APK的签名信息。还需要说明的是 Android中Signature和Java中Certificate的对应关系。它们的关系如下面代码所示:

 

1 pkg.mSignatures = new Signature[certs.length];
2 for (int i=0; i 3 pkg.mSignatures[i] = new Signature(
4 certs[i].getEncoded());
5 }

也就是说signature = new Signature(certificate.getEncoded()); certificate证书中包含了公钥和证书的其他基本信息。公钥不同,证书肯定互不相同。我们可以通过certificate的getPublicKey方法获取公钥信息。所以比对签名证书本质上就是比对公钥信息。

OK,获取到APK签名证书之后,就剩下比对了。这个简单,功能函数如下所示:

1 private boolean IsSignaturesSame(Signature[] s1, Signature[] s2) {
2 if (s1 == null) {
3 return false;
4 }
5 if (s2 == null) {
6 return false;
7 }
8 HashSet set1 = new HashSet();
9 for (Signature sig : s1) {
10 set1.add(sig);
11 }
12 HashSet set2 = new HashSet();
13 for (Signature sig : s2) {
14 set2.add(sig);
15 }
16 // Make sure s2 contains all signatures in s1.
17 if (set1.equals(set2)) {
18 return true;
19 }
20 return false;
21 }

APK签名比对的应用场景

经过以上的论述,想必大家已经明白签名比对的原理和我的实现方式了。那么什么时候什么情况适合使用签名对比来保障Android APK的软件安全呢?

个人认为主要有以下三种场景:

1、 程序自检测。在程序运行时,自我进行签名比对。比对样本可以存放在APK包内,也可存放于云端。缺点是程序被破解时,自检测功能同样可能遭到破坏,使其失效。

2、 可信赖的第三方检测。由可信赖的第三方程序负责APK的软件安全问题。对比样本由第三方收集,放在云端。这种方式适用于杀毒安全软件或者APP Market之类的软件下载市场。缺点是需要联网检测,在无网络情况下无法实现功能。(不可能把大量的签名数据放在移动设备本地)。

3、 系统限定安装。这就涉及到改Android系统了。限定仅能安装某些证书的APK。软件发布商需要向系统发布上申请证书。如果发现问题,能追踪到是哪个软件发布商的责任。适用于系统提供商或者终端产品生产商。缺点是过于封闭,不利于系统的开放性。

以上三种场景,虽然各有缺点,但缺点并不是不能克服的。例如,我们可以考虑程序自检测的功能用native method的方法实现等等。软件安全是一个复杂的课题,往往需要多种技术联合使用,才能更好的保障软件不被恶意破坏

 

Android混淆设置

首先将位于tools/proguard/ 目录下的混淆设置复制到proguard-rules.pro 文件中

  1. optimizations !code/simplification/arithmetic,!field/*,!class/merging/* # 混淆时所采用的算法
  2. optimizationpasses 5 # 指定代码的压缩级别
  3. allowaccessmodification # 允许访问并修改有修饰符的类和类的成员
  4. dontpreverify # 混淆时不做预校验
  5. # The remainder of this file is identical to the non-optimized version
  6. # of the Proguard configuration file (except that the other file has
  7. # flags to turn off optimization).
  8. dontusemixedcaseclassnames # 是否使用大小写混合
  9. dontskipnonpubliclibraryclasses # 是否混淆第三方jar
  10. verbose # 混淆时记录日志
  11. keepattributes *Annotation* # 保留任何可选属性
  12. keep public class com.google.vending.licensing.ILicensingService
  13. keep public class com.android.vending.licensing.ILicensingService
  14. # For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
  15. keepclasseswithmembernames class * {
  16. native <methods>;
  17. }
  18. # keep setters in Views so that animations can still work.
  19. # see http://proguard.sourceforge.net/manual/examples.html#beans
  20. keepclassmembers public class * extends android.view.View {
  21. void set*(***);
  22. *** get*();
  23. }
  24. # We want to keep methods in Activity that could be used in the XML attribute onClick
  25. keepclassmembers class * extends android.app.Activity {
  26. public void *(android.view.View);
  27. }
  28. # For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
  29. keepclassmembers enum * {
  30. public static **[] values();
  31. public static ** valueOf(java.lang.String);
  32. }
  33. keepclassmembers class * implements android.os.Parcelable {
  34. public static final android.os.Parcelable$Creator CREATOR;
  35. }
  36. keepclassmembers class **.R$* {
  37. public static <fields>;
  38. }
  39. # The support library contains references to newer platform versions.
  40. # Don’t warn about those in case this app is linking against an older
  41. # platform version. We know about them, and they are safe.
  42. dontwarn android.support.**
  43. # Understand the @Keep support annotation.
  44. keep class android.support.annotation.Keep
  45. keep @android.support.annotation.Keep class * {*;}
  46. keepclasseswithmembers class * {
  47. @android.support.annotation.Keep <methods>;
  48. }
  49. keepclasseswithmembers class * {
  50. @android.support.annotation.Keep <fields>;
  51. }
  52. keepclasseswithmembers class * {
  53. @android.support.annotation.Keep <init>(…);
  54. }

 

通常不需混淆的Android类

  1. -keep public class * extends android.app.Fragment
  2. -keep public class * extends android.app.Activity
  3. -keep public class * extends android.app.Application
  4. -keep public class * extends android.app.Service
  5. -keep public class * extends android.content.BroadcastReceiver
  6. -keep public class * extends android.preference.Preference
  7. -keep public class * extends android.content.ContentProvider
  8. -keep public class * extends android.support.v4.**
  9. -keep public class * extends android.support.annotation.**
  10. -keep public class * extends android.support.v7.**

 

Butter Knife

  1. keep class butterknife.** { *; }
  2. dontwarn butterknife.internal.**
  3. keep class **$$ViewBinder { *; }
  4. keepclasseswithmembernames class * {
  5. @butterknife.* <fields>;
  6. }
  7. keepclasseswithmembernames class * {
  8. @butterknife.* <methods>;
  9. }

 

Retrofit

  1. dontwarn retrofit2.**
  2. keep class retrofit2.** { *; }
  3. keepattributes Signature
  4. keepattributes Exceptions

 

RxJava RxAndroid

  1. -dontwarn sun.misc.**
  2. -keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
  3. long producerIndex;
  4. long consumerIndex;
  5. }
  6. -keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef {
  7. rx.internal.util.atomic.LinkedQueueNode producerNode;
  8. }
  9. -keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef {
  10. rx.internal.util.atomic.LinkedQueueNode consumerNode;
  11. }

 

Gson

  1. keepattributes Signature-keepattributes *Annotation*
  2. keep class sun.misc.Unsafe { *; }
  3. keep class com.google.gson.stream.** { *; }
  4. # Application classes that will be serialized/deserialized over Gson 下面替换成自己的实体类
  5. keep class com.example.bean.** { *; }

 

OkHttp3

  1. dontwarn com.squareup.okhttp3.**
  2. keep class com.squareup.okhttp3.** { *;}
  3. dontwarn okio.**

 

Glide

  1. -keep public class * implements com.bumptech.glide.module.GlideModule
  2. keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
  3. **[] $VALUES;
  4. public *;
  5. }

 

友盟统计

  1. -keepclassmembers class * {
  2. public <init> (org.json.JSONObject);
  3. }
  4. -keep public class [您的应用包名].R$*{
  5. public static final int *;
  6. }
  7. -keepclassmembers enum * {
  8. public static **[] values(); public static ** valueOf(java.lang.String);
  9. }

Octotree Chrome安装与使用方法

Octotree Chrome安装与使用方法

Octotree Chrome作用:

  主要使你在github查看项目时可以清晰明了的看到项目的结构以及具体代码,使下载代码更具有目的性,减少不必要代码的下载,而且看起来更清楚。

效果图:(安装插件前)

%title插图%num

 

希望对想使用此插件的伙伴有所帮助!

效果图:(安装插件后)

%title插图%num

 

 

1、下载octotree  Chrome插件,

地址是:http://www.cnplugins.com/devtool/octotree/download.html

如果下载不成功,可到提供的github路径下去下载:https://github.com/zichenxuan/javatools

2、打开chrome浏览器进入软件界面够,我们在搜索栏中输入chrome://extensions,然后在左侧的功能中选择“扩展程序”。

3、直接拖动我们已经下载的octotree chrome插件至扩展程序界面。

4、随即弹出“要添加octotree吗”,用户点击“添加扩展程序”按钮即可进行添加。

%title插图%num

 

%title插图%num

 

 

5、添加成功以后打开github,在项目左上侧有一个三角收缩符号,点击三角符号,即可看到项目结构图以及具体代码:

%title插图%num

 

那么,这么好用的软件只有谷歌能用吗?NO,火狐也能用,请往下看:

1、打开火狐浏览器,在右上角找到附加组件:

%title插图%num

2、在搜索栏之间输入插件名称octotree

%title插图%num

3、安装启用

4、清晰的树形项目结构就展现在了你的眼前:

%title插图%num

 

Android安全攻防战,反编译与混淆技术完全解析(下)

在上一篇文章当中,我们学习了Android程序反编译方面的知识,包括反编译代码、反编译资源、以及重新打包等内容。通过这些内容我们也能看出来,其实我们的程序并没有那么的安全。可能资源被反编译影响还不是很大,重新打包又由于有签名的保护导致很难被盗版,但代码被反编译就有可能会泄漏核心技术了,因此一款安全性高的程序*起码要做到的一件事就是:对代码进行混淆。
混淆代码并不是让代码无法被反编译,而是将代码中的类、方法、变量等信息进行重命名,把它们改成一些毫无意义的名字。因为对于我们而言可能Cellphone类的call()方法意味着很多信息,而A类的b()方法则没有任何意义,但是对于计算机而言,它们都是平等的,计算机不会试图去理解Cellphone是什么意思,它只会按照设定好的逻辑来去执行这些代码。所以说混淆代码可以在不影响程序正常运行的前提下让破解者很头疼,从而大大提升了程序的安全性。
今天是我们Android安全攻防战系列的下篇,本篇文章的内容建立在上篇的基础之上,还没有阅读过的朋友可以先去参考 Android安全攻防战,反编译与混淆技术完全解析 。


混淆

本篇文章中介绍的混淆技术都是基于Android Studio的,Eclipse的用法也基本类似,但是就不再为Eclipse专门做讲解了。
我们要建立一个Android Studio项目,并在项目中添加一些能够帮助我们理解混淆知识的代码。这里我准备好了一些,我们将它们添加到Android Studio当中。
首先新建一个MyFragment类,代码如下所示:

  1. public class MyFragment extends Fragment {
  2. private String toastTip = “toast in MyFragment”;
  3. @Nullable
  4. @Override
  5. public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
  6. View view = inflater.inflate(R.layout.fragment_layout, container, false);
  7. methodWithGlobalVariable();
  8. methodWithLocalVariable();
  9. return view;
  10. }
  11. public void methodWithGlobalVariable() {
  12. Toast.makeText(getActivity(), toastTip, Toast.LENGTH_SHORT).show();
  13. }
  14. public void methodWithLocalVariable() {
  15. String logMessage = “log in MyFragment”;
  16. logMessage = logMessage.toLowerCase();
  17. System.out.println(logMessage);
  18. }
  19. }

 

可以看到,MyFragment是继承自Fragment的,并且MyFragment中有一个全局变量。onCreateView()方法是Fragment的生命周期函数,这个不用多说,在onCreateView()方法中又调用了methodWithGlobalVariable()和methodWithLocalVariable()方法,这两个方法的内部分别引用了一个全局变量和一个局部变量。
接下来新建一个Utils类,代码如下所示:

  1. public class Utils {
  2. public void methodNormal() {
  3. String logMessage = “this is normal method”;
  4. logMessage = logMessage.toLowerCase();
  5. System.out.println(logMessage);
  6. }
  7. public void methodUnused() {
  8. String logMessage = “this is unused method”;
  9. logMessage = logMessage.toLowerCase();
  10. System.out.println(logMessage);
  11. }
  12. }

 

这是一个非常普通的工具类,没有任何继承关系。Utils中有两个方法methodNormal()和methodUnused(),它们的内部逻辑都是一样的,唯一的据别是稍后methodNormal()方法会被调用,而methodUnused()方法不会被调用。
下面再新建一个NativeUtils类,代码如下所示:

  1. public class NativeUtils {
  2. public static native void methodNative();
  3. public static void methodNotNative() {
  4. String logMessage = “this is not native method”;
  5. logMessage = logMessage.toLowerCase();
  6. System.out.println(logMessage);
  7. }
  8. }

 

这个类中同样有两个方法,一个是native方法,一个是非native方法。
*后,修改MainActivity中的代码,如下所示:

  1. public class MainActivity extends AppCompatActivity {
  2. private String toastTip = “toast in MainActivity”;
  3. @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. setContentView(R.layout.activity_main);
  7. getSupportFragmentManager().beginTransaction().add(R.id.fragment, new MyFragment()).commit();
  8. Button button = (Button) findViewById(R.id.button);
  9. button.setOnClickListener(new View.OnClickListener() {
  10. @Override
  11. public void onClick(View v) {
  12. methodWithGlobalVariable();
  13. methodWithLocalVariable();
  14. Utils utils = new Utils();
  15. utils.methodNormal();
  16. NativeUtils.methodNative();
  17. NativeUtils.methodNotNative();
  18. Connector.getDatabase();
  19. }
  20. });
  21. }
  22. public void methodWithGlobalVariable() {
  23. Toast.makeText(MainActivity.this, toastTip, Toast.LENGTH_SHORT).show();
  24. }
  25. public void methodWithLocalVariable() {
  26. String logMessage = “log in MainActivity”;
  27. logMessage = logMessage.toLowerCase();
  28. System.out.println(logMessage);
  29. }
  30. }

 

可以看到,MainActivity和MyFragment类似,也是定义了methodWithGlobalVariable()和methodWithLocalVariable()这两个方法,然后MainActivity对MyFragment进行了添加,并在Button的点击事件里面调用了自身的、Utils的、以及NativeUtils中的方法。注意调用native方法需要有相应的so库实现,不然的话就会报UnsatisefiedLinkError,不过这里其实我也并没有真正的so库实现,只是演示一下让大家看看混淆结果。点击事件的*后一行调用的是LitePal中的方法,因为我们还要测试一下引用第三方Jar包的场景,到LitePal项目的主页去下载*新的Jar包,然后放到libs目录下即可。
完整的build.gradle内容如下所示:

  1. apply plugin: ‘com.android.application’
  2. android {
  3. compileSdkVersion 23
  4. buildToolsVersion “23.0.2”
  5. defaultConfig {
  6. applicationId “com.example.guolin.androidtest”
  7. minSdkVersion 15
  8. targetSdkVersion 23
  9. versionCode 1
  10. versionName “1.0”
  11. }
  12. buildTypes {
  13. release {
  14. minifyEnabled false
  15. proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
  16. }
  17. }
  18. }
  19. dependencies {
  20. compile fileTree(dir: ‘libs’, include: [‘*.jar’])
  21. compile ‘com.android.support:appcompat-v7:23.2.0’
  22. }

 

好的,到这里准备工作就已经基本完成了,接下来我们就开始对代码进行混淆吧。

混淆APK

在Android Studio当中混淆APK实在是太简单了,借助SDK中自带的Proguard工具,只需要修改build.gradle中的一行配置即可。可以看到,现在build.gradle中minifyEnabled的值是false,这里我们只需要把值改成true,打出来的APK包就会是混淆过的了。如下所示:

  1. release {
  2. minifyEnabled true
  3. proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
  4. }

 

其中minifyEnabled用于设置是否启用混淆,proguardFiles用于选定混淆配置文件。注意这里是在release闭包内进行配置的,因此只有打出正式版的APK才会进行混淆,Debug版的APK是不会混淆的。当然这也是非常合理的,因为Debug版的APK文件我们只会用来内部测试,不用担心被人破解。
那么现在我们来打一个正式版的APK文件,在Android Studio导航栏中点击Build->Generate Signed APK,然后选择签名文件并输入密码,如果没有签名文件就创建一个,*终点击Finish完成打包,生成的APK文件会自动存放在app目录下。除此之外也可以在build.gradle文件当中添加签名文件配置,然后通过gradlew assembleRelease来打出一个正式版的APK文件,这种方式APK文件会自动存放在app/build/outputs/apk目录下。
那么现在已经得到了APK文件,接下来就用上篇文章中学到的反编译知识来对这个文件进行反编译吧,结果如下图所示:

%title插图%num
很明显可以看出,我们的代码混淆功能已经生效了。
下面我们尝试来阅读一下这个混淆过后的代码,*顶层的包名结构主要分为三部分,*个a.a已经被混淆的面目全非了,但是可以猜测出这个包下是LitePal的所有代码。第二个android.support可以猜测出是我们引用的android support库的代码,第三个com.example.guolin.androidtest则很明显就是我们项目的主包名了,下面将里面所有的类一个个打开看一下。
首先MainActivity中的代码如下所示:

%title插图%num
可以看到,MainActivity的类名是没有混淆的,onCreate()方法也没有被混淆,但是我们定义的方法、全局变量、局部变量都被混淆了。
再来打开下一个类NativeUtils,如下所示:

%title插图%num
NativeUtils的类名没有被混淆,其中声明成native的方法也没有被混淆,但是非native方法的方法名和局部变量都被混淆了。
接下来是a类的代码,如下所示:

%title插图%num
很明显,这个是MainActivity中按钮点击事件的匿名类,在onClick()方法中的调用代码虽然都被混淆了,但是调用顺序是不会改变的,对照源代码就可以看出哪一行是调用的什么方法了。
再接下来是b类,代码如下所示:

%title插图%num
虽然被混淆的很严重,但是我们还是可以看出这个是MyFragment类。其中所有的方法名、全局变量、局部变量都被混淆了。
*后再来看下c类,代码如下所示:

%title插图%num
c类中只有一个a方法,从字符串的内容我们可以看出,这个是Utils类中的methodNormal()方法。
我为什么要创建这样的一个项目呢?因为从这几个类当中很能看出一些问题,接下来我们就分析一下上面的混淆结果。
首先像Utils这样的普通类肯定是会被混淆的,不管是类名、方法名还是变量都不会放过。除了混淆之外Utils类还说明了一个问题,就是minifyEnabled会对资源进行压缩,因为Utils类中我们明明定义了两个方法,但是反编译之后就只剩一个方法了,因为另外一个方法没有被调用,所以认为是多余的代码,在打包的时候就给移除掉了。不仅仅是代码,没有被调用的资源同样也会被移除掉,因此minifyEnabled除了混淆代码之外,还可以起到压缩APK包的作用。
接着看一下MyFragment,这个类也是混淆的比较彻底的,基本没有任何保留。那有些朋友可能会有疑问,Fragment怎么说也算是系统组件吧,就算普通方法名被混淆了,至少像onCreateView()这样的生命周期方法不应该被混淆吧?其实生命周期方法会不会被混淆和我们使用Fragment的方式有关,比如在本项目中,我使用的是android.support.v4.app.Fragment,support-v4包下的,就连Fragment的源码都被一起混淆了,因此生命周期方法当然也不例外了。但如果你使用的是android.app.Fragment,这就是调用手机系统中预编译好的代码了,很明显我们的混淆无法影响到系统内置的代码,因此这种情况下onCreateView()方法名就不会被混淆,但其它的方法以及变量仍然会被混淆。
接下来看一下MainActivity,同样也是系统组件之一,但MainActivity的保留程度就比MyFragment好多了,至少像类名、生命周期方法名都没有被混淆,这是为什么呢?根据我亲身测试得出结论,凡是需要在AndroidManifest.xml中去注册的所有类的类名以及从父类重写的方法名都自动不会被混淆。因此,除了Activity之外,这份规则同样也适用于Service、BroadcastReceiver和ContentProvider。
*后看一下NativeUtils类,这个类的类名也没有被混淆,这是由于它有一个声明成native的方法。只要一个类中有存在native方法,它的类名就不会被混淆,native方法的方法名也不会被混淆,因为C++代码要通过包名+类名+方法名来进行交互。 但是类中的别的代码还是会被混淆的。
除此之外,第三方的Jar包都是会被混淆的,LitePal不管是包名还是类名还是方法名都被完完全全混淆掉了。
这些就是Android Studio打正式APK时默认的混淆规则。
那么这些混淆规则是在哪里定义的呢?其实就是刚才在build.gradle的release闭包下配置的proguard-android.txt文件,这个文件存放于<Android SDK>/tools/proguard目录下,我们打开来看一下:

  1. ;}# keep setters in Views so that animations can still work.# see http://proguard.sourceforge.net/manual/examples.html#beans-keepclassmembers public class * extends android.view.View { void set*(***); *** get*();}# We want to keep methods in Activity that could be used in the XML attribute onClick-keepclassmembers class * extends android.app.Activity { public void *(android.view.View);}# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations-keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String);}-keepclassmembers class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator CREATOR;}-keepclassmembers class **.R$* { public static
  2. ;}# The support library contains references to newer platform versions.# Dont warn about those in case this app is linking against an older# platform version. We know about them, and they are safe.-dontwarn android.support.**123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657″ data-snippet-id=”ext.118e40b0b985117840cef1077ca78660″ data-snippet-saved=”false” data-codota-status=”done” style=”white-space: nowrap; word-wrap: break-word; box-sizing: border-box; position: relative; overflow-y: hidden; overflow-x: auto; margin-top: 0px; margin-bottom: 1.7em; line-height: 23.8px; font-family: “Source Code Pro”, monospace; padding: 5px 5px 5px 60px; font-size: 14px; word-break: break-all; color: rgb(51, 51, 51); background-color: rgba(128, 128, 128, 0.05); border: 0px solid rgb(136, 136, 136); border-radius: 0px;”>
  3. # This is a configuration file for ProGuard.
  4. # http://proguard.sourceforge.net/index.html#manual/usage.html
  5. -dontusemixedcaseclassnames
  6. -dontskipnonpubliclibraryclasses
  7. -verbose
  8. # Optimization is turned off by default. Dex does not like code run
  9. # through the ProGuard optimize and preverify steps (and performs some
  10. # of these optimizations on its own).
  11. -dontoptimize
  12. -dontpreverify
  13. # Note that if you want to enable optimization, you cannot just
  14. # include optimization flags in your own project configuration file;
  15. # instead you will need to point to the
  16. # "proguard-android-optimize.txt" file instead of this one from your
  17. # project.properties file.
  18. -keepattributes *Annotation*
  19. -keep public class com.google.vending.licensing.ILicensingService
  20. -keep public class com.android.vending.licensing.ILicensingService
  21. # For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
  22. -keepclasseswithmembernames class * {
  23. native <methods>;
  24. }
  25. # keep setters in Views so that animations can still work.
  26. # see http://proguard.sourceforge.net/manual/examples.html#beans
  27. -keepclassmembers public class * extends android.view.View {
  28. void set*(***);
  29. *** get*();
  30. }
  31. # We want to keep methods in Activity that could be used in the XML attribute onClick
  32. -keepclassmembers class * extends android.app.Activity {
  33. public void *(android.view.View);
  34. }
  35. # For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
  36. -keepclassmembers enum * {
  37. public static **[] values();
  38. public static ** valueOf(java.lang.String);
  39. }
  40. -keepclassmembers class * implements android.os.Parcelable {
  41. public static final android.os.Parcelable$Creator CREATOR;
  42. }
  43. -keepclassmembers class **.R$* {
  44. public static <fields>;
  45. }
  46. # The support library contains references to newer platform versions.
  47. # Dont warn about those in case this app is linking against an older
  48. # platform version. We know about them, and they are safe.
  49. -dontwarn android.support.**

这个就是默认的混淆配置文件了,我们来一起逐行阅读一下。
-dontusemixedcaseclassnames 表示混淆时不使用大小写混合类名。
-dontskipnonpubliclibraryclasses 表示不跳过library中的非public的类。
-verbose 表示打印混淆的详细信息。
-dontoptimize 表示不进行优化,建议使用此选项,因为根据proguard-android-optimize.txt中的描述,优化可能会造成一些潜在风险,不能保证在所有版本的Dalvik上都正常运行。
-dontpreverify 表示不进行预校验。这个预校验是作用在Java平台上的,Android平台上不需要这项功能,去掉之后还可以加快混淆速度。
-keepattributes *Annotation* 表示对注解中的参数进行保留。

  1. -keep public class com.google.vending.licensing.ILicensingService
  2. -keep public class com.android.vending.licensing.ILicensingService

 

表示不混淆上述声明的两个类,这两个类我们基本也用不上,是接入Google原生的一些服务时使用的。

  1. keepclasseswithmembernames class * {
  2. native <methods>;
  3. }

 

表示不混淆任何包含native方法的类的类名以及native方法名,这个和我们刚才验证的结果是一致的。

  1. -keepclassmembers public class * extends android.view.View {
  2. void set*(***);
  3. *** get*();
  4. }

 

表示不混淆任何一个View中的setXxx()和getXxx()方法,因为属性动画需要有相应的setter和getter的方法实现,混淆了就无法工作了。

  1. -keepclassmembers class * extends android.app.Activity {
  2. public void *(android.view.View);
  3. }

 

表示不混淆Activity中参数是View的方法,因为有这样一种用法,在XML中配置android:onClick=”buttonClick”属性,当用户点击该按钮时就会调用Activity中的buttonClick(View view)方法,如果这个方法被混淆的话就找不到了。

  1. -keepclassmembers enum * {
  2. public static **[] values();
  3. public static ** valueOf(java.lang.String);
  4. }

表示不混淆枚举中的values()和valueOf()方法,枚举我用的非常少,这个就不评论了。

  1. -keepclassmembers class * implements android.os.Parcelable {
  2. public static final android.os.Parcelable$Creator CREATOR;
  3. }
  • 1
  • 2
  • 3

表示不混淆Parcelable实现类中的CREATOR字段,毫无疑问,CREATOR字段是*对不能改变的,包括大小写都不能变,不然整个Parcelable工作机制都会失败。

  1. keepclassmembers class **.R$* {
  2. public static <fields>;
  3. }

 

表示不混淆R文件中的所有静态字段,我们都知道R文件是通过字段来记录每个资源的id的,字段名要是被混淆了,id也就找不着了。
-dontwarn android.support.** 表示对android.support包下的代码不警告,因为support包中有很多代码都是在高版本中使用的,如果我们的项目指定的版本比较低在打包时就会给予警告。不过support包中所有的代码都在版本兼容性上做足了判断,因此不用担心代码会出问题,所以直接忽略警告就可以了。
好了,这就是proguard-android.txt文件中所有默认的配置,而我们混淆代码也是按照这些配置的规则来进行混淆的。经过我上面的讲解之后,相信大家对这些配置的内容基本都能理解了。不过proguard语法中还真有几处非常难理解的地方,我自己也是研究了好久才搞明白,下面和大家分享一下这些难懂的语法部分。
proguard中一共有三组六个keep关键字,很多人搞不清楚它们的区别,这里我们通过一个表格来直观地看下:

关键字 描述
keep 保留类和类中的成员,防止它们被混淆或移除。
keepnames 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。
keepclassmembers 只保留类中的成员,防止它们被混淆或移除。
keepclassmembernames 只保留类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。
keepclasseswithmembers 保留类和类中的成员,防止它们被混淆或移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。
keepclasseswithmembernames 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。

除此之外,proguard中的通配符也比较让人难懂,proguard-android.txt中就使用到了很多通配符,我们来看一下它们之间的区别:

通配符 描述
<field> 匹配类中的所有字段
<method> 匹配类中的所有方法
<init> 匹配类中的所有构造函数
* 匹配任意长度字符,但不含包名分隔符(.)。比如说我们的完整类名是com.example.test.MyActivity,使用com.*,或者com.exmaple.*都是无法匹配的,因为*无法匹配包名中的分隔符,正确的匹配方式是com.exmaple.*.*,或者com.exmaple.test.*,这些都是可以的。但如果你不写任何其它内容,只有一个*,那就表示匹配所有的东西。
** 匹配任意长度字符,并且包含包名分隔符(.)。比如proguard-android.txt中使用的-dontwarn android.support.**就可以匹配android.support包下的所有内容,包括任意长度的子包。
*** 匹配任意参数类型。比如void set*(***)就能匹配任意传入的参数类型,*** get*()就能匹配任意返回值的类型。
匹配任意长度的任意类型参数。比如void test(…)就能匹配任意void test(String a)或者是void test(int a, String b)这些方法。

虽说上面表格已经解释的很详细了,但是很多人对于keep和keepclasseswithmembers这两个关键字的区别还是搞不懂。确实,它们之间用法有点太像了,我做了很多次试验它们的结果都是相同的。其实唯一的区别就在于类中声明的成员存不存在,我们还是通过一个例子来直接地看一下,先看keepclasseswithmember关键字:

  1. keepclasseswithmember class * {
  2. native <methods>;
  3. }

 

这段代码的意思其实很明显,就是保留所有含有native方法的类的类名和native方法名,而如果某个类中没有含有native方法,那就还是会被混淆。
但是如果改成keep关键字,结果会完全不一样:

  1. keep class * {
  2. native <methods>;
  3. }

 

使用keep关键字后,你会发现代码中所有类的类名都不会被混淆了,因为keep关键字看到class *就认为应该将所有类名进行保留,而不会关心该类中是否含有native方法。当然这样写只会保证类名不会被混淆,类中的成员还是会被混淆的。
比较难懂的用法大概就这些吧,掌握了这些内容之后我们就能继续前进了。
回到Android Studio项目当中,刚才打出的APK虽然已经成功混淆了,但是混淆的规则都是按照proguard-android.txt中默认的规则来的,当然我们也可以修改proguard-android.txt中的规则,但是直接在proguard-android.txt中修改会对我们本机上所有项目的混淆规则都生效,那么有没有什么办法只针对当前项目的混淆规则做修改呢?当然是有办法的了,你会发现任何一个Android Studio项目在app模块目录下都有一个proguard-rules.pro文件,这个文件就是用于让我们编写只适用于当前项目的混淆规则的,那么接下来我们就利用刚才学到的所有知识来对混淆规则做修改吧。
这里我们先列出来要实现的目标:

  • 对MyFragment类进行完全保留,不混淆其类名、方法名、以及变量名。
  • 对Utils类中的未调用方法进行保留,防止其被移除掉。
  • 对第三方库进行保留,不混淆android-support库,以及LitePal库中的代码。

下面我们就来逐一实现这些目标。
首先要对MyFragment类进行完全保留可以使用keep关键字,keep后声明完整的类名,然后保留类中的所有内容可以使用*通配符实现,如下所示:

  1. -keep class com.example.guolin.androidtest.MyFragment {
  2. *;
  3. }

 

然后保留Utils类中的未调用方法可以使用keepclassmembers关键字,后跟Utils完整类名,然后在内部声明未调用的方法,如下所示:

  1. -keepclassmembers class com.example.guolin.androidtest.Utils {
  2. public void methodUnused();
  3. }

 

*后不要混淆第三方库,目前我们使用了两种方式来引入第三方库,一种是通过本地jar包引入的,一种是通过remote引入的,其实这两种方式没什么区别,要保留代码都可以使用**这种通配符来实现,如下所示:

  1. keep class org.litepal.** {
  2. *;
  3. }
  4. keep class android.support.** {
  5. *;
  6. }

 

所有内容都在这里了,现在我们重新打一个正式版的APK文件,然后再反编译看看效果:

%title插图%num
可以看到,现在android-support包中所有代码都被保留下来了,不管是包名、类名、还是方法名都没有被混淆。LitePal中的代码也是同样的情况:

%title插图%num
再来看下MyFragment中的代码,如下所示:

%title插图%num
可以看到,MyFragment中的代码也没有被混淆,按照我们的要求被完全保留下来了。
*后再来看一下Utils类中的代码:

%title插图%num
很明显,Utils类并没有被完全保留下来,类名还是被混淆了,methodNormal()方法也被混淆了,但是methodUnused()没有被混淆,当然也没有被移除,因为我们的混淆配置生效了。
经过这些例子的演示,相信大家已经对Proguard的用法有了相当不错的理解了,那么根据自己的业务需求来去编写混淆配置相信也不是什么难事了吧?
Progaurd的使用非常灵活,基本上能够覆盖你所能想到的所有业务逻辑。这里再举个例子,之前一直有人问我使用LitePal时的混淆配置怎么写,其实真的很简单,LitePal作为开源库并不需要混淆,上面的配置已经演示了如何不混淆LitePal代码,然后所有代码中的Model是需要进行反射的,也不能混淆,那么只需要这样写就行了:

  1. -keep class * extends org.litepal.crud.DataSupport {
  2. *;
  3. }
  • 1
  • 2
  • 3

因为LitePal中所有的Model都是应该继承DataSupport类的,所以这里我们将所有继承自DataSupport的类都进行保留就可以了。
关于混淆APK的用法就讲这么多,如果你还想继续了解关于Proguard的更多用法,可以参考官方文档:http://proguard.sourceforge.net/index.html#manual/usage.html

混淆Jar

在本篇文章的第二部分我想讲一讲混淆Jar包的内容,因为APK不一定是我们交付的唯一产品。就比如说我自己,我在公司是负责写SDK的,对于我来说交付出去的产品就是Jar包,而如果Jar包不混淆的话将会很容易就被别人反编译出来,从而泄漏程序逻辑。
实际上Android对混淆Jar包的支持在很早之前就有了,不管你使用多老版本的SDK,都能在 <Android SDK>/tools目录下找到proguard这个文件夹。然后打开里面的bin目录,你会看到如下文件:

%title插图%num
其中proguardgui.bat文件是允许我们以图形化的方式来对Jar包进行混淆的一个工具,今天我们就来讲解一下这个工具的用法。
在开始讲解这个工具之前,首先我们需要先准备一个Jar包,当然你从哪里搞到一个Jar包都是可以的,不过这里为了和刚才的混淆逻辑统一,我们就把本篇文章中的项目代码打成一个Jar包吧。
Eclipse中导出Jar包的方法非常简单,相信所有人都会,可是Android Studio当中就比较让人头疼了,因为Android Studio并没有提供一个专门用于导出Jar包的工具,因此我们只能自己动手了。
我们需要知道,任何一个Android Studio项目,只要编译成功之后就会在项目模块的build/intermediates/classes/debug目录下生成代码编译过后的class文件,因此只需通过打包命令将这些class文件打包成Jar包就行了,打开cmd,切换到项目的根目录,然后输入如下命令:

jar -cvf androidtest.jar -C app/build/intermediates/classes/debug .
  • 1

在项目的根目录下就会生成androidtest.jar这个文件,这样我们就把Jar包准备好了。
现在双击proguardgui.bat打开混淆工具,如果是Mac或Ubuntu系统则使用sh proguardgui.sh命令打开混淆工具,界面如下图所示:

%title插图%num
其实从主界面上我们就能看出,这个Proguard工具支持Shrinking、Optimization、Obfuscation、Preverification四项操作,在左侧的侧边栏上也能看到相应的这些选项。Proguard的工作机制仍然还是要依赖于配置文件,当然我们也可以通过proguardgui工具来生成配置文件,不过由于配置选项太多了,每个都去一一设置太复杂,而且大多数还都是我们用不到的配置。因此*简单的方式就是直接拿现有的配置文件,然后再做些修改就行了。
那么我们从<Android SDK>/tools/proguard目录下将proguard-android.txt文件复制一份出来,然后点击主界面上的Load configuration按钮来加载复制出来的这份proguard-android.txt文件,完成后点击Next将进入Input/Output界面。
Input/Output界面是用于导入要混淆的Jar包、配置混淆后文件的输出路径、以及导入该Jar包所依赖的所有其它Jar包的。我们要混淆的当然就是androidtest.jar这个文件,那么这个Jar包又依赖了哪些Jar包呢?这里就需要整理一下了。

  • 首先我们写的都是Java代码,Java代码的运行要基于Jre基础之上,没有Jre计算机将无法识别Java的语法,因此*个要依赖的就是Jre的rt.jar。
  • 然后由于我们导出的Jar包中有Android相关的代码,比如Activity、Fragment等,因此还需要添加Android的编译库,android.jar。
  • 除此之外,我们使用的AppCompatActivity和Fragment分别来自于appcompat-v7包和support-v4包,那么这两个Jar包也是需要引入的。
  • *后就是代码中还引入了litepal-1.3.1.jar。

整理清楚了之后我们就来一个个添加,Input/Output有上下两个操作界面,上面是用于导入要混淆的Jar包和配置混淆后文件的输出路径的,下面则是导入该Jar包所依赖的所有其它Jar包的,全部导入后结果如下图所示:

%title插图%num
这些依赖的Jar包所存在的路径每台电脑都不一样,你所需要做的就是在你自己的电脑上成功找到这些依赖的Jar包并导入即可。
不过细心的朋友可能会发现,我在上面整理出了五个依赖的Jar包,但是在图中却添加了六个。这是我在写这篇文章时碰到的一个新的坑,也是定位了好久才解决的,我觉得有必要重点提一下。由于我平时混淆Jar包时里面很少会有Activity,所以没遇到过这个问题,但是本篇文章中的演示Jar包中不仅包含了Activty,还是继承自AppCompatActivity的。而AppCompatActivity的继承结构并不简单,如下图所示:

%title插图%num
其中AppCompatActivity是在appcompat-v7包中的,它的父类FragmentActivity是在support-v4包中的,这两个包我们都已经添加依赖了。但是FragmentActivity的父类就坑爹了,如果你去看BaseFragmentActivityHoneycomb和BaseFragmentActivityDonut这两个类的源码,你会发现它们都是在support-v4包中的:

%title插图%num 

%title插图%num
可是如果你去support-v4的Jar包中找一下,你会发现压根就没有这两个类,所以我当时一直混淆报错就是因为这两个类不存在,继承结构在这里断掉了。而这两个类其实被规整到了另外一个internal的Jar包中,所以当你要混淆的Jar包中有Activity,并且还是继承自AppCompatActivity或FragmentActivity的话,那么就一定要记得导入这个internal Jar包的依赖,如下图所示:

%title插图%num
接下来点击Next进入Shrink界面,这个界面没什么需要配置的东西,但记得要将Shrink选项钩掉,因为我们这个Jar包是独立存在的,没有任何项目引用,如果钩中Shrink选项的话就会认为我们所有的代码都是无用的,从而把所有代码全压缩掉,导出一个空的Jar包。
继续点击Next进入Obfuscation界面,在这里可以添加一些混淆的逻辑,和混淆APK时不同的是,这里并不会自动帮我们排除混淆四大组件,因此必须要手动声明一下才行。点击*下方的Add按钮,然后在弹出的界面上编写排除逻辑,如下图所示:

%title插图%num
很简单,就是在继承那一栏写上android.app.Activity就行了,其它的组件原理也相同。
继续点击Next进入Optimiazation界面,不用修改任何东西,因为我们本身就不启用Optimization功能。继续点击Next进入Information界面,也不用修改任何东西,因为我们也不启用Preverification功能。
接着点击Next,进入Process界面,在这里可以通过点击View configuration按钮来预览一下目前我们的混淆配置文件,内容如下所示:

  1. ;}-keep class * extends android.app.Activity-keep class * extends android.app.Service-keep class * extends android.content.BroadcastReceiver-keep class * extends android.content.ContentProvider# Also keep – Enumerations. Keep the special static methods that are required in# enumeration classes.-keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String);}# Keep names – Native method names. Keep all native class/method names.-keepclasseswithmembers,allowshrinking class * { native
  2. ;}1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162″ data-snippet-id=”ext.07d089fc1136fd617b9357a82b568778″ data-snippet-saved=”false” data-codota-status=”done” style=”white-space: nowrap; word-wrap: break-word; box-sizing: border-box; position: relative; overflow-y: hidden; overflow-x: auto; margin-top: 0px; margin-bottom: 1.7em; line-height: 23.8px; font-family: “Source Code Pro”, monospace; padding: 5px 5px 5px 60px; font-size: 14px; word-break: break-all; color: rgb(51, 51, 51); background-color: rgba(128, 128, 128, 0.05); border: 0px solid rgb(136, 136, 136); border-radius: 0px;”>
  3. -injars /Users/guolin/AndroidStudioProjects/AndroidTest/androidtest.jar
  4. -outjars /Users/guolin/androidtest_obfuscated.jar
  5. -libraryjars /Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar
  6. -libraryjars /Users/guolin/Library/Android/sdk/platforms/android-23/android.jar
  7. -libraryjars /Users/guolin/AndroidStudioProjects/AndroidTest/app/build/intermediates/exploded-aar/com.android.support/appcompat-v7/23.2.0/jars/classes.jar
  8. -libraryjars /Users/guolin/AndroidStudioProjects/AndroidTest/app/build/intermediates/exploded-aar/com.android.support/support-v4/23.2.0/jars/classes.jar
  9. -libraryjars /Users/guolin/AndroidStudioProjects/AndroidTest/app/build/intermediates/exploded-aar/com.android.support/support-v4/23.2.0/jars/libs/internal_impl-23.2.0.jar
  10. -libraryjars /Users/guolin/AndroidStudioProjects/AndroidTest/app/libs/litepal-1.3.1.jar
  11. -dontshrink
  12. -dontoptimize
  13. -dontusemixedcaseclassnames
  14. -keepattributes *Annotation*
  15. -dontpreverify
  16. -verbose
  17. -dontwarn android.support.**
  18. -keep public class com.google.vending.licensing.ILicensingService
  19. -keep public class com.android.vending.licensing.ILicensingService
  20. # keep setters in Views so that animations can still work.
  21. # see http://proguard.sourceforge.net/manual/examples.html#beans
  22. -keepclassmembers public class * extends android.view.View {
  23. void set*(***);
  24. *** get*();
  25. }
  26. # We want to keep methods in Activity that could be used in the XML attribute onClick
  27. -keepclassmembers class * extends android.app.Activity {
  28. public void *(android.view.View);
  29. }
  30. -keepclassmembers class * extends android.os.Parcelable {
  31. public static final android.os.Parcelable$Creator CREATOR;
  32. }
  33. -keepclassmembers class **.R$* {
  34. public static <fields>;
  35. }
  36. -keep class * extends android.app.Activity
  37. -keep class * extends android.app.Service
  38. -keep class * extends android.content.BroadcastReceiver
  39. -keep class * extends android.content.ContentProvider
  40. # Also keep - Enumerations. Keep the special static methods that are required in
  41. # enumeration classes.
  42. -keepclassmembers enum * {
  43. public static **[] values();
  44. public static ** valueOf(java.lang.String);
  45. }
  46. # Keep names - Native method names. Keep all native class/method names.
  47. -keepclasseswithmembers,allowshrinking class * {
  48. native <methods>;
  49. }

由此可见其实GUI工具只是给我们提供了一个方便操作的平台,背后工作的原理还是通过这些配置来实现的,相信上面的配置内容大家应该都能看得懂了吧。
接下来我们还可以点击Save configuration按钮来保存一下当前的配置文件,这样下次混淆的时候就可以直接Load进来而不用修改任何东西了。
*后点击Process!按钮来开始混淆处理,中间会提示一大堆的Note信息,我们不用理会,只要看到*终显示Processing completed successfully,就说明混淆Jar包已经成功了,如下图所示:

%title插图%num
混淆后的文件我将它配置在了/Users/guolin/androidtest_obfuscated.jar这里,如果反编译一下这个文件,你会发现和刚才反编译APK得到的结果是差不多的:MainActivity的类名以及从父类继承的方法名不会被混淆,NativeUtils的类名和其中的native方法名不会被混淆,Utils的methodUnsed方法不会被移除,因为我们禁用了Shrink功能,其余的代码都会被混淆。由于结果实在是太相似了,我就不再贴图了,参考本篇文章*部分的截图即可。


好了,本篇文章的内容就到这里,混淆技术掌握这么多相信已经足够大家在平时的工作当中使用了。当然除了使用混淆之外,还有一些加固软件也能提升程序的安全性,不过这些软件都是第三方的,并非Google原生支持,所以我就不进行讲解和推荐了。那么我们Android安全攻防战系列的文章到此结束,感谢大家有耐心看到*后。

为RecyclerView添加分隔线

就在昨天中午,我在简书上发布了我个人的*篇技术文档:RecyclerView系列之:RecyclerView添加Header和Footer,也很有幸,能够得到那么多人的支持,这让我迫不及待的赶紧写第二篇文章。今天我将谈谈:为RecyclerView添加分隔线。


一. 理解ListView和RecyclerView中的ChildView

在讲为Item加入分割线本质的前,先来介绍,认识一下ChildView,也就是平时我们用到的ListView,RecyclerView中的getChildAt(int position)这个返回的ChildView是哪一部分?到底是哪一部分呢?一开始的时候,我理解错了,但是经过下面两张图这么一比较,你就明白了:

%title插图%num
Item布局layout_margin == 0
%title插图%num
Item布局Layout_margin == 16dp

下面看代码的区别:
*张图的代码, 也就是每一个list_item的布局文件(下同)如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"              android:orientation="vertical"             
android:layout_width="match_parent"              
android:layout_height="50dp">    
      <TextView        
        android:id="@+id/list_item"        
        android:layout_width="match_parent" 
        android:layout_height="match_parent"        
        android:gravity="center"        
        android:textSize="20sp"        
        android:textColor="#262526"        
        android:background="#08da1d"/>
</LinearLayout>

第二张图的代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"              android:orientation="vertical"             
android:layout_width="match_parent"              
android:layout_height="50dp"
android:layout_margin="16dp">    
      <TextView        
        android:id="@+id/list_item"        
        android:layout_width="match_parent" 
        android:layout_height="match_parent"        
        android:gravity="center"        
        android:textSize="20sp"        
        android:textColor="#262526"        
        android:background="#08da1d"/>
</LinearLayout>

仔细看一下,它们的不同之处, 就是第二个图的代码中多了:

android:layout_margin = "16dp"

就多这一句而已。

所以到这里我们应该知道了ChildView是哪一部分了,就是图二中绿色这一部分,边距这一部分并不属于ChildView, 而是属于ChildView的布局。
这样我们了解ChildView之后,下面再来理解加入分隔线的原理就简单多了。


二. 理解加入分隔线的原理

在ListView中,Google为我们提供了SetDivider(Drawable divider)这样的方法来设置分隔线,那么在RecyclerView中,Google又为我们提供了什么样的方法去添加分隔线呢?通过查看官方文档,它,提供了:addItemDecoration(RecyclerView.ItemDecoration decor)这个方法了设置分隔线,那问题又来了,RecyclerView.ItemDecoration是什么东西呢?继续查:然后发现如下:它原来是一个类,里面封装了三个方法:
(1)void getItemOffsets ()
(2)void onDraw ()
(3)void onDrawOver ()


通过上面的三个方法,可以看出,这是要自己直接画上去,准确的说这几个方法是:添加Divider,主要是找到添加Divider的位置, 而Divider是在drawable文件中写好了的。 利用onDraw和onDrawOver都差不多,我们在创建自己的Decoration类继承RecyclerView.ItemDecoration的时候,我们只要重写getItemOffsets(),还有onDraw()和onDrawOver两者其中之一就可以了.


那getItemOffsets()方法有什么用呢?从字面意思就是Item要偏移, 由于我们在Item和Item之间加入了分隔线,线其实本质就是一个长方形,也是用户自定义的,既然线也有长宽高,就画横线来说,上面的Item加入了分隔线,那下面的Item就要往下平移,平移的量就是分隔线的高度。不理解每关系,后面看代码就容易理解了。


现在我们知道了如何添加了,就是通过画,那到底是画在哪里呢?画的位置又怎么确定呢?下面看图:

%title插图%num
分隔线的位置图

我现在拿画横线来说,从上面这个图中,我们很容易就可以看到,我们画分隔线的位置,是在每一个Item的布局之间,注意:是布局之间。

好了,我们确定了画在哪里,那我们怎么确定画线的具体的坐标位置呢?也就是我们要确定:分隔线的left, top, right, Bottom. 在Adapter中,我们很容易通过parent(这个parent它其实就是我们能看到的部分)获取每一个childView:
(1)left:parent.getPaddingLeft()
(2)right: parent. getWidth()-parent.getPaddingRight();
(3)top : 就是红线的上面:我们通过ChildView.getBottom()来得到这个Item的底部的高度,也就是蓝线位置,蓝线和红线之间间距:就是这个Item布局文件的:layout_marginBottom, 然后top的位置就是两者之和。
(4)bttom: 就是top加上分隔线的高度:top+线高


通过上面的解析,你也许知道了加入分隔线的原理,不理解也没有关系,说也不是说得很清楚,下面直接上代码,通过代码来理解。

三. Talk is cheap, show you the code.

(1)首先,先来看主布局文件:activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.study.wnw.recyclerviewdivider.MainActivity">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerview"
        android:layout_width="match_parent"
       android:layout_height="match_parent">    
    </android.support.v7.widget.RecyclerView>
</android.support.design.widget.CoordinatorLayout>

我在这里面仅仅加入了一个RecyclerView


(2)RecyclerView里面每个Item的布局文件:item_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent" 
              android:layout_height="50dp"
              android:layout_margin="16sp">
    <TextView
        android:id="@+id/list_item"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:textSize="20sp"
        android:textColor="#f7f4f7"
        android:background="#08da1d"/>
</LinearLayout>

这也没有什么可讲的,就是在里面添加一个TextView用来显示文本


(3)我们RecyclerView的适配器MyAdapater.java:

package com.study.wnw.recyclerviewdivider;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/** * Created by wnw on 16-5-22. */
public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    //定义一个集合,接收从Activity中传递过来的数据和上下文
    private List<String> mList;
    private Context mContext;

    MyAdapter(Context context, List<String> list){
        this.mContext = context;
        this.mList = list;
    }

    //得到child的数量
    @Override
    public int getItemCount() {
        return mList.size();
    }
    
    //创建ChildView
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View layout = LayoutInflater.from(mContext).inflate(R.layout.item_view, parent, false);
        return new MyHolder(layout);
    }

    //将数据绑定到每一个childView中
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof MyHolder){
            final String itemText = mList.get(position);
            ((MyHolder)holder).tv.setText(itemText);
        }
    }

    // 通过holder的方式来初始化每一个ChildView的内容
    class MyHolder extends RecyclerView.ViewHolder{
        TextView tv;
        public MyHolder(View itemView) {
            super(itemView);
            tv = (TextView)itemView.findViewById(R.id.list_item);
        }
    }
}

好了,这里也没有什么好讲的,也不是我们这篇文章的重点,下面重点来了。


(4)我们自定义的MyDecoration.java:(继承RecyclerView.ItemDecoration)

package com.study.wnw.recyclerviewdivider;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.View;

/** * Created by wnw on 16-5-22. */

public class MyDecoration extends RecyclerView.ItemDecoration{

    private Context mContext;
    private Drawable mDivider;
    private int mOrientation;
    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;
    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

    //我们通过获取系统属性中的listDivider来添加,在系统中的AppTheme中设置
    public static final int[] ATRRS  = new int[]{
            android.R.attr.listDivider
    };

    public MyDecoration(Context context, int orientation) {
        this.mContext = context;
        final TypedArray ta = context.obtainStyledAttributes(ATRRS);
        this.mDivider = ta.getDrawable(0);
        ta.recycle();
        setOrientation(orientation);
    }

    //设置屏幕的方向
    public void setOrientation(int orientation){
        if (orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST){
            throw new IllegalArgumentException("invalid orientation");        }        mOrientation = orientation;
    } 

   @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if (mOrientation == HORIZONTAL_LIST){
            drawVerticalLine(c, parent, state);
        }else {
            drawHorizontalLine(c, parent, state);
        }
    }

    //画横线, 这里的parent其实是显示在屏幕显示的这部分
    public void drawHorizontalLine(Canvas c, RecyclerView parent, RecyclerView.State state){
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++){
            final View child = parent.getChildAt(i);

            //获得child的布局信息
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)child.getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
            //Log.d("wnw", left + " " + top + " "+right+"   "+bottom+" "+i);
        }
    }

    //画竖线
    public void drawVerticalLine(Canvas c, RecyclerView parent, RecyclerView.State state){
        int top = parent.getPaddingTop();
        int bottom = parent.getHeight() - parent.getPaddingBottom();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++){
            final View child = parent.getChildAt(i); 

           //获得child的布局信息
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)child.getLayoutParams();
            final int left = child.getRight() + params.rightMargin;
            final int right = left + mDivider.getIntrinsicWidth();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    //由于Divider也有长宽高,每一个Item需要向下或者向右偏移
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if(mOrientation == HORIZONTAL_LIST){
            //画横线,就是往下偏移一个分割线的高度
            outRect.set(0, 0, 0, mDivider.getIntrinsicHeight());
        }else {
            //画竖线,就是往右偏移一个分割线的宽度
            outRect.set(0, 0, mDivider.getIntrinsicWidth(), 0);
        }
    }
}

从上面的代码中,我们还通过系统属性来适应屏幕的横屏和竖屏,然后确定画横的,还是竖的Divider,其实在里面我们做了三件事,*件是:获取到系统中的listDivider, 我们就是通过它在主题中去设置的,下面第(6)小点看一下代码就知道了。第二件事:就是找到我们需要添加Divider的位置,从onDraw方法中去找到,并将Divider添加进去。第三个是:得到Item的偏移量。


(5)看看我们的MainActivity.java

package com.study.wnw.recyclerviewdivider;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class MainActivity extends AppCompatActivity {
    //定义RecyclerView
    private RecyclerView mRecyclerView = null;

    //定义一个List集合,用于存放RecyclerView中的每一个数据
    private List<String> mData = null;

    //定义一个Adapter
    private MyAdapter mAdapter; 

   //定义一个LinearLayoutManager
    private LinearLayoutManager mLayoutManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //RecyclerView三步曲+LayoutManager
        initView();
        initData();
        mAdapter = new MyAdapter(this,mData);
        mRecyclerView.setLayoutManager(mLayoutManager);
        mRecyclerView.setAdapter(mAdapter); 

        //这句就是添加我们自定义的分隔线
        mRecyclerView.addItemDecoration(new MyDecoration(this, MyDecoration.VERTICAL_LIST));
    }

    //初始化View
    private void initView(){
        mLayoutManager = new LinearLayoutManager(this);
        mRecyclerView = (RecyclerView)findViewById(R.id.recyclerview);
    }

    //初始化加载到RecyclerView中的数据, 我这里只是给每一个Item添加了String类型的数据
    private void initData(){
        mData = new ArrayList<String>();
        for (int i = 0; i < 20; i++){
            mData.add("Item" + i);
        }
    }
}
(6)分隔线Divider的drawable文件:divider..xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <solid android:color="#7b7a7a"/>
    <size android:height="1dp"/>
</shape>

我们在这里面,画了一个:rectangle, 给它填充颜色,还有高度,这样就搞定了,高度小,显示出来也是一条线:其实线的本质就是长方形。这里可以根据个人需要,画不同类型的divider


(7)在styles.xml的AppTheme中,设置listDivider为我们的divider.xml文件:

<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
    <item name="android:listDivider">@drawable/divider</item>
</style>

这样,我们将系统的listDivider设置成我们自定义的divider. 还记得我们在MyDecoration中获取系统的listDivider这个属性吗,这样通过这个属性,我们就可以将我们的divider.xml文件和MyDecoration.java进行关联了。


到这里所有的工作就完成了,下面展示一下运行结果:

%title插图%num
竖屏效果图
%title插图%num
横屏效果图

经过几个小时的写作,终于搞定了,虽然仅仅是一个添加分隔线的功能,但是还是想尽可能的通过自己的语言去理解,去认知它的原理,这样做起来就简单多了。一开始的时候,我夜不知道怎么去用,也参考了别人写的文章,特别是鸿洋大神的:Android RecyclerView 使用完全解析 体验艺术般的控件, 写得特别的棒,从中也学到了一些知识。


好了,这篇文章暂时写到这里了,简单的介绍了一些RecyclerView分隔线的原理和添加方法,希望大家能够多多交流,过几天我会继续写下一篇文章,RecyclerView系列之(3):为RecyclerView添加下拉刷新和上拉加载的功能。*后还是要感谢大家,感谢这个平台,能够让我们一起交流,一切学习。

 

为RecyclerView添加Header和Footer

  • 过去的两天,在项目中,抛弃了ListView, 想试一试RecyclerView, 在用的过程中,遇到了一些问题,比如:如何为RecyclerView添加Header和Footer? 如何为RecyclerView添加分割线?如何为RecyclerView添加下拉刷新和上拉加载? 在今后的一段时间里,我会针对这几个问题,通过写简书的方式一一讲述, 今天为大家带来的是*个问题的解决方法,如何为RecyclerView添加Header和Footer?在这之前,我想分享一下我对RecyclerView的认识:

一 . 我眼中的RecyclerView

  • 过去的这一两年, RecyclerView的越来越引起了我们Android开发人员的注意,甚至很多人都说:ListView, GridView已经逐渐被RecyclerView替代, *主要的原因就是RecyclerView的灵活性, 还有性能上的提升。那么也许有很多人会问:RecyclerView和ListView, GridView到底是什么关系呢?通过Android官方文档的一组截图告诉你:
    %title插图%num
    ListView的家族谱(继承结构)
%title插图%num
RecyclerView的家族谱(继承结构)

* RecyclerView的家族谱(继承结构)*

  • 通过上面的两个图可以发现, ListView继承自:AbsListView。(GirdView也是), RecyclerView直接继承了ViewGroup , *后得出结论:RecyclerView是ListView的爷爷辈, 也就是RecyclerView是ListView的二爷, 所以从封装的层次上得出了为什么RecyclerView性能比ListView更好的原因, 因为封装的层次越高,查询执行的速度相对较慢,当然还有一个原因,RecyclerView中自带的Adapter中加入了Holder,强制要求开发人员使用,在ListView中,很多人都不懂使用Holder, 这也导致了使用ListView性能比较差。这也是RecyclerView性能提升的一个主要原因, 当然,封装的层越高越灵活,相对使用起来也相对难,很多方法都是通过自己去封装的,比如ListView中有addHeaderView(View view), addFooterView(View view)去添加自己的Header 和Footer, 但是在RecyclerView中没有,下面我将详细介绍:

二 . 为RecyclerView添加Header和Footer

1. 认识Header和Footer

通过下面简书的两张图,让你认识Header和Footer

%title插图%num
带Header的图

带Header的图

%title插图%num
带Footer的图

带Footer的图

  • 通过上面的两张图,可以看到,Header和Footer很多情况下是辅助功能的,比如Header通常用于做广告,而Footer更多的是为了显示下拉加载更多作为一个可视化组件来显示加载情况,提升用户体验。

2. 通过代码来解析如何为RecyclerView添加Header和Footer

RecyclerView的使用和ListView的使用差不多,无非就是那三步: *,初始化RecyclerView; 第二,初始化数据,并且将数据通过Adapter装在到View中; 第三,通过setAdapter方法,将Adapter绑定到RecyclerView中。 下面直接上代码:通过代码注释的方式来讲解添加Header和Footer: (1) 主布局layout_main.xml: 只是添加了一个RecyclerView而已

<pre class=”hljs undefined” data-original-code=”” “=”” data-snippet-id=”ext.035ab4069719aee80b16158b5db41e1e” data-snippet-saved=”false” data-codota-status=”done”>

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <RelativeLayoutxmlns:android=”http://schemas.android.com/apk/res/android
  3. xmlns:tools=“http://schemas.android.com/tools”
  4. android:layout_width=“match_parent”
  5. android:layout_height=“match_parent”
  6. android:paddingBottom=“@dimen/activity_vertical_margin”
  7. android:paddingLeft=“@dimen/activity_horizontal_margin”
  8. android:paddingRight=“@dimen/activity_horizontal_margin”
  9. android:paddingTop=“@dimen/activity_vertical_margin”
  10. tools:context=“com.study.wnw.recyclerviewheaderfooter.MainActivity”>
  11. <android.support.v7.widget.RecyclerView
  12. android:id=“@+id/recyclerview”
  13. android:layout_width=“match_parent”
  14. android:layout_height=“match_parent”/>
  15. </RelativeLayout>

(2)RecyclerView中ListView的Item布局list_Item.xml: 只是添加了一个TextView

  1. <?xml version=”1.0″ encoding=”utf-8″?><LinearLayout
  2. xmlns:android=“http://schemas.android.com/apk/res/android”
  3. android:orientation=“vertical”
  4. android:layout_width=“match_parent”
  5. android:layout_height=“50dp”>
  6. <TextView
  7. android:id=“@+id/item”
  8. android:layout_width=“match_parent”
  9. android:layout_height=“match_parent”
  10. android:textSize=“20sp”
  11. android:textColor=“@color/colorAccent”
  12. android:gravity=“center”
  13. android:background=“#08e630”/>
  14. </LinearLayout>

(3)HeaderView和FooterView的布局文件,也是一个TextView, 这里只贴出了HeaderView的布局,FooterView的布局文件和FooterView的一样:

HeaderView的布局文件: header.xml:

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
  3. android:orientation=“vertical”
  4. android:layout_width=“match_parent”
  5. android:layout_height=“100dp”>
  6. <TextView
  7. android:id=“@+id/header”
  8. android:layout_width=“match_parent”
  9. android:layout_height=“match_parent”
  10. android:text=“我是Header”
  11. android:textSize=“30sp”
  12. android:textColor=“#fde70b0b”
  13. android:background=“#f9777979”
  14. android:gravity=“center”/>
  15. </LinearLayout>

好了, 布局文件到这里我们已经弄好了, 下面我们直接看MainActivity中的内容:
(4) MainActivity.java中的内容为:

  1. package com.study.wnw.recyclerviewheaderfooter;
  2. import android.app.Activity;import android.os.Bundle;
  3. import android.support.v7.widget.LinearLayoutManager;
  4. import android.support.v7.widget.RecyclerView;
  5. import android.view.LayoutInflater;
  6. import android.view.View;
  7. import java.util.ArrayList;import java.util.List;
  8. public class MainActivity extends Activity {
  9. private RecyclerView mRecyclerView;
  10. private MyAdapter mMyAdapter;
  11. private List<String> mList;
  12. @Override
  13. protected void onCreate(Bundle savedInstanceState) {
  14. super.onCreate(savedInstanceState);
  15. setContentView(R.layout.activity_main);
  16. //RecyclerView三部曲+LayoutManager
  17. mRecyclerView = (RecyclerView)findViewById(R.id.recyclerview);
  18. LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
  19. mRecyclerView.setLayoutManager(linearLayoutManager);
  20. initData();
  21. mMyAdapter = new MyAdapter(mList);
  22. mRecyclerView.setAdapter(mMyAdapter);
  23. //为RecyclerView添加HeaderView和FooterView
  24. setHeaderView(mRecyclerView);
  25. setFooterView(mRecyclerView);
  26. }
  27. //初始化RecyclerView中每个item的数据
  28. private void initData(){
  29. mList = new ArrayList<String>();
  30. for (int i = 0; i < 20; i++){
  31. mList.add(“item” + i);
  32. }
  33. }
  34. private void setHeaderView(RecyclerView view){
  35. View header = LayoutInflater.from(this).inflate(R.layout.header, view, false);
  36. mMyAdapter.setHeaderView(header);
  37. }
  38. private void setFooterView(RecyclerView view){
  39. View footer = LayoutInflater.from(this).inflate(R.layout.footer, view, false);
  40. mMyAdapter.setFooterView(footer);
  41. }
  42. }
  • 从上面的代码中,我们可以看到,我们在MainActivity中做了两件事,一个是初始化RecyclerView相关的View, Adapter, data; 另一个是通过我们自定义的Adapter的setHeaderView()和setFooterView()方法为RecyclerView添加HeaderView和FooterView, 到这里,我们已经迫不及待的想知道MyAdapter中到底有什么东西, 直接上代码
    (5) MyAdapter.java的代码
  1. package com.study.wnw.recyclerviewheaderfooter;
  2. import android.support.v7.widget.RecyclerView;
  3. import android.view.LayoutInflater;import android.view.View;
  4. import android.view.ViewGroup;
  5. import android.widget.TextView;
  6. import java.util.List;
  7. /** * Created by wnw on 16-5-20. */
  8. public class MyAdapter extendsRecyclerView.Adapter<RecyclerView.ViewHolder> {
  9. public static final int TYPE_HEADER = 0; //说明是带有Header的
  10. public static final int TYPE_FOOTER = 1; //说明是带有Footer的
  11. public static final int TYPE_NORMAL = 2; //说明是不带有header和footer的
  12. //获取从Activity中传递过来每个item的数据集合
  13. private List<String> mDatas;
  14. //HeaderView, FooterView
  15. private View mHeaderView;
  16. private View mFooterView;
  17. //构造函数
  18. public MyAdapter(List<String> list){
  19. this.mDatas = list;
  20. }
  21. //HeaderView和FooterView的get和set函数
  22. public View getHeaderView() {
  23. return mHeaderView;
  24. }
  25. public void setHeaderView(View headerView) {
  26. mHeaderView = headerView;
  27. notifyItemInserted(0);
  28. }
  29. public View getFooterView() {
  30. return mFooterView;
  31. }
  32. public void setFooterView(View footerView) {
  33. mFooterView = footerView;
  34. notifyItemInserted(getItemCount()-1);
  35. }
  36. /** 重写这个方法,很重要,是加入Header和Footer的关键,我们通过判断item的类型,从而绑定不同的view * */
  37. @Override
  38. public int getItemViewType(int position) {
  39. if (mHeaderView == null && mFooterView == null){
  40. return TYPE_NORMAL;
  41. }
  42. if (position == 0){
  43. //*个item应该加载Header
  44. return TYPE_HEADER;
  45. }
  46. if (position == getItemCount()-1){
  47. //*后一个,应该加载Footer
  48. return TYPE_FOOTER;
  49. }
  50. return TYPE_NORMAL;
  51. }
  52. //创建View,如果是HeaderView或者是FooterView,直接在Holder中返回
  53. @Override
  54. public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
  55. if(mHeaderView != null && viewType == TYPE_HEADER) {
  56. return new ListHolder(mHeaderView);
  57. }
  58. if(mFooterView != null && viewType == TYPE_FOOTER){
  59. return new ListHolder(mFooterView);
  60. }
  61. View layout = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item, parent, false);
  62. return new ListHolder(layout);
  63. }
  64. //绑定View,这里是根据返回的这个position的类型,从而进行绑定的, HeaderView和FooterView, 就不同绑定了
  65. @Override
  66. public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
  67. if(getItemViewType(position) == TYPE_NORMAL){
  68. if(holder instanceof ListHolder) {
  69. //这里加载数据的时候要注意,是从position-1开始,因为position==0已经被header占用了
  70. ((ListHolder) holder).tv.setText(mDatas.get(position-1));
  71. return;
  72. }
  73. return;
  74. }else if(getItemViewType(position) == TYPE_HEADER){
  75. return;
  76. }else{
  77. return;
  78. }
  79. }
  80. //在这里面加载ListView中的每个item的布局
  81. class ListHolder extends RecyclerView.ViewHolder{
  82. TextView tv;
  83. public ListHolder(View itemView) {
  84. super(itemView);
  85. //如果是headerview或者是footerview,直接返回
  86. if (itemView == mHeaderView){
  87. return;
  88. }
  89. if (itemView == mFooterView){
  90. return;
  91. }
  92. tv = (TextView)itemView.findViewById(R.id.item);
  93. }
  94. }
  95. //返回View中Item的个数,这个时候,总的个数应该是ListView中Item的个数加上HeaderView和FooterView
  96. @Override
  97. public int getItemCount() {
  98. if(mHeaderView == null && mFooterView == null){
  99. return mDatas.size();
  100. }else if(mHeaderView == null && mFooterView != null){
  101. return mDatas.size() + 1;
  102. }else if (mHeaderView != null && mFooterView == null){
  103. return mDatas.size() + 1;
  104. }else {
  105. return mDatas.size() + 2;
  106. }
  107. }
  108. }
  • 从上面的MyAdapter类中,有setHeaderView()和setFooterView()两个方法,我们就是通过这两个方法从Activity将headerView和footerView传递过来的, 在Adapter中的onCreateViewHolder()方法中,利用getItemViewType()返回Item的类型(你这个Item是不是Header家的?还是Footer家的?或者是ListView家的?)根据不同的类型,我们创建不同的Item的View。大概的思路就是这样子,更多细节请看代码注释,下面是运行截图(由于不知道Linux下有什么好的GIF生成工具,只能截静态图,大家知道的话,可以推荐给我,谢谢各位大神):
%title插图%num
header
%title插图%num
footer
  • 终于写完了,需要慢慢消化和吸收,这只是一种实现HeaderView和FooterView的方式,我们从这里看到,HeaderView和FooterView是直接从Activity中传递过Adapter的,我们也可以像ListView一样,我们只传递HeaderView和FooterView的数据过Adapter中,然后在Adapter的内部再新建不同的HeaderHolder和FooterHolder, 根据返回Item的类型,加载不同的Holder即可,具体的实现我就不多说了,大家可以去实现一下。通过截图,你会发现,RecyclerView中,每个Item之间竟然没有分割线,而且RecyclerView中也没有setDivider()这样的方法去设置分割线,因为RecyclerView灵活啊,所以也是要自定义的,其实很简单,下一篇文章我会带来如何为RecyclerView添加分割线,这篇就到此结束了,有不足之处,望大家多多指教,谢谢了。
    RecyclerView中添加分隔线已经实现

Android安全攻防战,反编译与混淆技术完全解析(下)

混淆

本篇文章中介绍的混淆技术都是基于Android Studio的,Eclipse的用法也基本类似,但是就不再为Eclipse专门做讲解了。
我们要建立一个Android Studio项目,并在项目中添加一些能够帮助我们理解混淆知识的代码。这里我准备好了一些,我们将它们添加到Android Studio当中。
首先新建一个MyFragment类,代码如下所示:

  1. public class MyFragment extends Fragment {
  2. private String toastTip = “toast in MyFragment”;
  3. @Nullable
  4. @Override
  5. public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
  6. View view = inflater.inflate(R.layout.fragment_layout, container, false);
  7. methodWithGlobalVariable();
  8. methodWithLocalVariable();
  9. return view;
  10. }
  11. public void methodWithGlobalVariable() {
  12. Toast.makeText(getActivity(), toastTip, Toast.LENGTH_SHORT).show();
  13. }
  14. public void methodWithLocalVariable() {
  15. String logMessage = “log in MyFragment”;
  16. logMessage = logMessage.toLowerCase();
  17. System.out.println(logMessage);
  18. }
  19. }

 

可以看到,MyFragment是继承自Fragment的,并且MyFragment中有一个全局变量。onCreateView()方法是Fragment的生命周期函数,这个不用多说,在onCreateView()方法中又调用了methodWithGlobalVariable()和methodWithLocalVariable()方法,这两个方法的内部分别引用了一个全局变量和一个局部变量。
接下来新建一个Utils类,代码如下所示:

  1. public class Utils {
  2. public void methodNormal() {
  3. String logMessage = “this is normal method”;
  4. logMessage = logMessage.toLowerCase();
  5. System.out.println(logMessage);
  6. }
  7. public void methodUnused() {
  8. String logMessage = “this is unused method”;
  9. logMessage = logMessage.toLowerCase();
  10. System.out.println(logMessage);
  11. }
  12. }

 

这是一个非常普通的工具类,没有任何继承关系。Utils中有两个方法methodNormal()和methodUnused(),它们的内部逻辑都是一样的,唯一的据别是稍后methodNormal()方法会被调用,而methodUnused()方法不会被调用。
下面再新建一个NativeUtils类,代码如下所示:

  1. public class NativeUtils {
  2. public static native void methodNative();
  3. public static void methodNotNative() {
  4. String logMessage = “this is not native method”;
  5. logMessage = logMessage.toLowerCase();
  6. System.out.println(logMessage);
  7. }
  8. }

 

这个类中同样有两个方法,一个是native方法,一个是非native方法。
*后,修改MainActivity中的代码,如下所示:

  1. public class MainActivity extends AppCompatActivity {
  2. private String toastTip = “toast in MainActivity”;
  3. @Override
  4. protected void onCreate(Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState);
  6. setContentView(R.layout.activity_main);
  7. getSupportFragmentManager().beginTransaction().add(R.id.fragment, new MyFragment()).commit();
  8. Button button = (Button) findViewById(R.id.button);
  9. button.setOnClickListener(new View.OnClickListener() {
  10. @Override
  11. public void onClick(View v) {
  12. methodWithGlobalVariable();
  13. methodWithLocalVariable();
  14. Utils utils = new Utils();
  15. utils.methodNormal();
  16. NativeUtils.methodNative();
  17. NativeUtils.methodNotNative();
  18. Connector.getDatabase();
  19. }
  20. });
  21. }
  22. public void methodWithGlobalVariable() {
  23. Toast.makeText(MainActivity.this, toastTip, Toast.LENGTH_SHORT).show();
  24. }
  25. public void methodWithLocalVariable() {
  26. String logMessage = “log in MainActivity”;
  27. logMessage = logMessage.toLowerCase();
  28. System.out.println(logMessage);
  29. }
  30. }

 

可以看到,MainActivity和MyFragment类似,也是定义了methodWithGlobalVariable()和methodWithLocalVariable()这两个方法,然后MainActivity对MyFragment进行了添加,并在Button的点击事件里面调用了自身的、Utils的、以及NativeUtils中的方法。注意调用native方法需要有相应的so库实现,不然的话就会报UnsatisefiedLinkError,不过这里其实我也并没有真正的so库实现,只是演示一下让大家看看混淆结果。点击事件的*后一行调用的是LitePal中的方法,因为我们还要测试一下引用第三方Jar包的场景,到LitePal项目的主页去下载*新的Jar包,然后放到libs目录下即可。
完整的build.gradle内容如下所示:

  1. apply plugin: ‘com.android.application’
  2. android {
  3. compileSdkVersion 23
  4. buildToolsVersion “23.0.2”
  5. defaultConfig {
  6. applicationId “com.example.guolin.androidtest”
  7. minSdkVersion 15
  8. targetSdkVersion 23
  9. versionCode 1
  10. versionName “1.0”
  11. }
  12. buildTypes {
  13. release {
  14. minifyEnabled false
  15. proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
  16. }
  17. }
  18. }
  19. dependencies {
  20. compile fileTree(dir: ‘libs’, include: [‘*.jar’])
  21. compile ‘com.android.support:appcompat-v7:23.2.0’
  22. }

 

好的,到这里准备工作就已经基本完成了,接下来我们就开始对代码进行混淆吧。

混淆APK

在Android Studio当中混淆APK实在是太简单了,借助SDK中自带的Proguard工具,只需要修改build.gradle中的一行配置即可。可以看到,现在build.gradle中minifyEnabled的值是false,这里我们只需要把值改成true,打出来的APK包就会是混淆过的了。如下所示:

  1. release {
  2. minifyEnabled true
  3. proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’
  4. }

 

其中minifyEnabled用于设置是否启用混淆,proguardFiles用于选定混淆配置文件。注意这里是在release闭包内进行配置的,因此只有打出正式版的APK才会进行混淆,Debug版的APK是不会混淆的。当然这也是非常合理的,因为Debug版的APK文件我们只会用来内部测试,不用担心被人破解。
那么现在我们来打一个正式版的APK文件,在Android Studio导航栏中点击Build->Generate Signed APK,然后选择签名文件并输入密码,如果没有签名文件就创建一个,*终点击Finish完成打包,生成的APK文件会自动存放在app目录下。除此之外也可以在build.gradle文件当中添加签名文件配置,然后通过gradlew assembleRelease来打出一个正式版的APK文件,这种方式APK文件会自动存放在app/build/outputs/apk目录下。
那么现在已经得到了APK文件,接下来就用上篇文章中学到的反编译知识来对这个文件进行反编译吧,结果如下图所示:

%title插图%num

很明显可以看出,我们的代码混淆功能已经生效了。
下面我们尝试来阅读一下这个混淆过后的代码,*顶层的包名结构主要分为三部分,*个a.a已经被混淆的面目全非了,但是可以猜测出这个包下是LitePal的所有代码。第二个android.support可以猜测出是我们引用的android support库的代码,第三个com.example.guolin.androidtest则很明显就是我们项目的主包名了,下面将里面所有的类一个个打开看一下。
首先MainActivity中的代码如下所示:

%title插图%num

可以看到,MainActivity的类名是没有混淆的,onCreate()方法也没有被混淆,但是我们定义的方法、全局变量、局部变量都被混淆了。
再来打开下一个类NativeUtils,如下所示:

%title插图%num

NativeUtils的类名没有被混淆,其中声明成native的方法也没有被混淆,但是非native方法的方法名和局部变量都被混淆了。
接下来是a类的代码,如下所示:

%title插图%num

很明显,这个是MainActivity中按钮点击事件的匿名类,在onClick()方法中的调用代码虽然都被混淆了,但是调用顺序是不会改变的,对照源代码就可以看出哪一行是调用的什么方法了。
再接下来是b类,代码如下所示:

%title插图%num

虽然被混淆的很严重,但是我们还是可以看出这个是MyFragment类。其中所有的方法名、全局变量、局部变量都被混淆了。
*后再来看下c类,代码如下所示:

%title插图%num

c类中只有一个a方法,从字符串的内容我们可以看出,这个是Utils类中的methodNormal()方法。
我为什么要创建这样的一个项目呢?因为从这几个类当中很能看出一些问题,接下来我们就分析一下上面的混淆结果。
首先像Utils这样的普通类肯定是会被混淆的,不管是类名、方法名还是变量都不会放过。除了混淆之外Utils类还说明了一个问题,就是minifyEnabled会对资源进行压缩,因为Utils类中我们明明定义了两个方法,但是反编译之后就只剩一个方法了,因为另外一个方法没有被调用,所以认为是多余的代码,在打包的时候就给移除掉了。不仅仅是代码,没有被调用的资源同样也会被移除掉,因此minifyEnabled除了混淆代码之外,还可以起到压缩APK包的作用。
接着看一下MyFragment,这个类也是混淆的比较彻底的,基本没有任何保留。那有些朋友可能会有疑问,Fragment怎么说也算是系统组件吧,就算普通方法名被混淆了,至少像onCreateView()这样的生命周期方法不应该被混淆吧?其实生命周期方法会不会被混淆和我们使用Fragment的方式有关,比如在本项目中,我使用的是android.support.v4.app.Fragment,support-v4包下的,就连Fragment的源码都被一起混淆了,因此生命周期方法当然也不例外了。但如果你使用的是android.app.Fragment,这就是调用手机系统中预编译好的代码了,很明显我们的混淆无法影响到系统内置的代码,因此这种情况下onCreateView()方法名就不会被混淆,但其它的方法以及变量仍然会被混淆。
接下来看一下MainActivity,同样也是系统组件之一,但MainActivity的保留程度就比MyFragment好多了,至少像类名、生命周期方法名都没有被混淆,这是为什么呢?根据我亲身测试得出结论,凡是需要在AndroidManifest.xml中去注册的所有类的类名以及从父类重写的方法名都自动不会被混淆。因此,除了Activity之外,这份规则同样也适用于Service、BroadcastReceiver和ContentProvider。
*后看一下NativeUtils类,这个类的类名也没有被混淆,这是由于它有一个声明成native的方法。只要一个类中有存在native方法,它的类名就不会被混淆,native方法的方法名也不会被混淆,因为C++代码要通过包名+类名+方法名来进行交互。 但是类中的别的代码还是会被混淆的。
除此之外,第三方的Jar包都是会被混淆的,LitePal不管是包名还是类名还是方法名都被完完全全混淆掉了。
这些就是Android Studio打正式APK时默认的混淆规则。
那么这些混淆规则是在哪里定义的呢?其实就是刚才在build.gradle的release闭包下配置的proguard-android.txt文件,这个文件存放于<Android SDK>/tools/proguard目录下,我们打开来看一下:

 

  1. # This is a configuration file for ProGuard.
  2. # http://proguard.sourceforge.net/index.html#manual/usage.html
  3. -dontusemixedcaseclassnames
  4. -dontskipnonpubliclibraryclasses
  5. -verbose
  6. # Optimization is turned off by default. Dex does not like code run
  7. # through the ProGuard optimize and preverify steps (and performs some
  8. # of these optimizations on its own).
  9. -dontoptimize
  10. -dontpreverify
  11. # Note that if you want to enable optimization, you cannot just
  12. # include optimization flags in your own project configuration file;
  13. # instead you will need to point to the
  14. # “proguard-android-optimize.txt” file instead of this one from your
  15. # project.properties file.
  16. -keepattributes *Annotation*
  17. -keep public class com.google.vending.licensing.ILicensingService
  18. -keep public class com.android.vending.licensing.ILicensingService
  19. # For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
  20. -keepclasseswithmembernames class * {
  21. native <methods>;
  22. }
  23. # keep setters in Views so that animations can still work.
  24. # see http://proguard.sourceforge.net/manual/examples.html#beans
  25. -keepclassmembers public class * extends android.view.View {
  26. void set*(***);
  27. *** get*();
  28. }
  29. # We want to keep methods in Activity that could be used in the XML attribute onClick
  30. -keepclassmembers class * extends android.app.Activity {
  31. public void *(android.view.View);
  32. }
  33. # For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
  34. -keepclassmembers enum * {
  35. public static **[] values();
  36. public static ** valueOf(java.lang.String);
  37. }
  38. -keepclassmembers class * implements android.os.Parcelable {
  39. public static final android.os.Parcelable$Creator CREATOR;
  40. }
  41. -keepclassmembers class **.R$* {
  42. public static <fields>;
  43. }
  44. # The support library contains references to newer platform versions.
  45. # Dont warn about those in case this app is linking against an older
  46. # platform version. We know about them, and they are safe.
  47. -dontwarn android.support.**

 

这个就是默认的混淆配置文件了,我们来一起逐行阅读一下。
-dontusemixedcaseclassnames 表示混淆时不使用大小写混合类名。
-dontskipnonpubliclibraryclasses 表示不跳过library中的非public的类。
-verbose 表示打印混淆的详细信息。
-dontoptimize 表示不进行优化,建议使用此选项,因为根据proguard-android-optimize.txt中的描述,优化可能会造成一些潜在风险,不能保证在所有版本的Dalvik上都正常运行。
-dontpreverify 表示不进行预校验。这个预校验是作用在Java平台上的,Android平台上不需要这项功能,去掉之后还可以加快混淆速度。
-keepattributes *Annotation* 表示对注解中的参数进行保留。

  1. -keep public class com.google.vending.licensing.ILicensingService
  2. -keep public class com.android.vending.licensing.ILicensingService

 

表示不混淆上述声明的两个类,这两个类我们基本也用不上,是接入Google原生的一些服务时使用的。

  1. -keepclasseswithmembernames class * {
  2. native <methods>;
  3. }

 

表示不混淆任何包含native方法的类的类名以及native方法名,这个和我们刚才验证的结果是一致的。

  1. -keepclassmembers public class * extends android.view.View {
  2. void set*(***);
  3. *** get*();
  4. }

 

表示不混淆任何一个View中的setXxx()和getXxx()方法,因为属性动画需要有相应的setter和getter的方法实现,混淆了就无法工作了。

  1. -keepclassmembers class * extends android.app.Activity {
  2. public void *(android.view.View);
  3. }

 

表示不混淆Activity中参数是View的方法,因为有这样一种用法,在XML中配置android:onClick=”buttonClick”属性,当用户点击该按钮时就会调用Activity中的buttonClick(View view)方法,如果这个方法被混淆的话就找不到了。

  1. -keepclassmembers enum * {
  2. public static **[] values();
  3. public static ** valueOf(java.lang.String);
  4. }

 

表示不混淆枚举中的values()和valueOf()方法,枚举我用的非常少,这个就不评论了。

  1. -keepclassmembers class * implements android.os.Parcelable {
  2. public static final android.os.Parcelable$Creator CREATOR;
  3. }

 

表示不混淆Parcelable实现类中的CREATOR字段,毫无疑问,CREATOR字段是*对不能改变的,包括大小写都不能变,不然整个Parcelable工作机制都会失败。

  1. -keepclassmembers class **.R$* {
  2. public static <fields>;
  3. }

 

表示不混淆R文件中的所有静态字段,我们都知道R文件是通过字段来记录每个资源的id的,字段名要是被混淆了,id也就找不着了。
-dontwarn android.support.** 表示对android.support包下的代码不警告,因为support包中有很多代码都是在高版本中使用的,如果我们的项目指定的版本比较低在打包时就会给予警告。不过support包中所有的代码都在版本兼容性上做足了判断,因此不用担心代码会出问题,所以直接忽略警告就可以了。
好了,这就是proguard-android.txt文件中所有默认的配置,而我们混淆代码也是按照这些配置的规则来进行混淆的。经过我上面的讲解之后,相信大家对这些配置的内容基本都能理解了。不过proguard语法中还真有几处非常难理解的地方,我自己也是研究了好久才搞明白,下面和大家分享一下这些难懂的语法部分。
proguard中一共有三组六个keep关键字,很多人搞不清楚它们的区别,这里我们通过一个表格来直观地看下:

关键字 描述
keep 保留类和类中的成员,防止它们被混淆或移除。
keepnames 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。
keepclassmembers 只保留类中的成员,防止它们被混淆或移除。
keepclassmembernames 只保留类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。
keepclasseswithmembers 保留类和类中的成员,防止它们被混淆或移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。
keepclasseswithmembernames 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。

除此之外,proguard中的通配符也比较让人难懂,proguard-android.txt中就使用到了很多通配符,我们来看一下它们之间的区别:

通配符 描述
<field> 匹配类中的所有字段
<method> 匹配类中的所有方法
<init> 匹配类中的所有构造函数
* 匹配任意长度字符,但不含包名分隔符(.)。比如说我们的完整类名是com.example.test.MyActivity,使用com.*,或者com.exmaple.*都是无法匹配的,因为*无法匹配包名中的分隔符,正确的匹配方式是com.exmaple.*.*,或者com.exmaple.test.*,这些都是可以的。但如果你不写任何其它内容,只有一个*,那就表示匹配所有的东西。
** 匹配任意长度字符,并且包含包名分隔符(.)。比如proguard-android.txt中使用的-dontwarn android.support.**就可以匹配android.support包下的所有内容,包括任意长度的子包。
*** 匹配任意参数类型。比如void set*(***)就能匹配任意传入的参数类型,*** get*()就能匹配任意返回值的类型。
匹配任意长度的任意类型参数。比如void test(…)就能匹配任意void test(String a)或者是void test(int a, String b)这些方法。

虽说上面表格已经解释的很详细了,但是很多人对于keep和keepclasseswithmembers这两个关键字的区别还是搞不懂。确实,它们之间用法有点太像了,我做了很多次试验它们的结果都是相同的。其实唯一的区别就在于类中声明的成员存不存在,我们还是通过一个例子来直接地看一下,先看keepclasseswithmember关键字:

  1. -keepclasseswithmember class * {
  2. native <methods>;
  3. }

 

这段代码的意思其实很明显,就是保留所有含有native方法的类的类名和native方法名,而如果某个类中没有含有native方法,那就还是会被混淆。
但是如果改成keep关键字,结果会完全不一样:

  1. -keep class * {
  2. native <methods>;
  3. }

 

使用keep关键字后,你会发现代码中所有类的类名都不会被混淆了,因为keep关键字看到class *就认为应该将所有类名进行保留,而不会关心该类中是否含有native方法。当然这样写只会保证类名不会被混淆,类中的成员还是会被混淆的。
比较难懂的用法大概就这些吧,掌握了这些内容之后我们就能继续前进了。
回到Android Studio项目当中,刚才打出的APK虽然已经成功混淆了,但是混淆的规则都是按照proguard-android.txt中默认的规则来的,当然我们也可以修改proguard-android.txt中的规则,但是直接在proguard-android.txt中修改会对我们本机上所有项目的混淆规则都生效,那么有没有什么办法只针对当前项目的混淆规则做修改呢?当然是有办法的了,你会发现任何一个Android Studio项目在app模块目录下都有一个proguard-rules.pro文件,这个文件就是用于让我们编写只适用于当前项目的混淆规则的,那么接下来我们就利用刚才学到的所有知识来对混淆规则做修改吧。
这里我们先列出来要实现的目标:

  • 对MyFragment类进行完全保留,不混淆其类名、方法名、以及变量名。
  • 对Utils类中的未调用方法进行保留,防止其被移除掉。
  • 对第三方库进行保留,不混淆android-support库,以及LitePal库中的代码。

下面我们就来逐一实现这些目标。
首先要对MyFragment类进行完全保留可以使用keep关键字,keep后声明完整的类名,然后保留类中的所有内容可以使用*通配符实现,如下所示:

  1. -keep class com.example.guolin.androidtest.MyFragment {
  2. *;
  3. }

 

然后保留Utils类中的未调用方法可以使用keepclassmembers关键字,后跟Utils完整类名,然后在内部声明未调用的方法,如下所示:

  1. -keepclassmembers class com.example.guolin.androidtest.Utils {
  2. public void methodUnused();
  3. }

 

*后不要混淆第三方库,目前我们使用了两种方式来引入第三方库,一种是通过本地jar包引入的,一种是通过remote引入的,其实这两种方式没什么区别,要保留代码都可以使用**这种通配符来实现,如下所示:

  1. -keep class org.litepal.** {
  2. *;
  3. }
  4. -keep class android.support.** {
  5. *;
  6. }

 

所有内容都在这里了,现在我们重新打一个正式版的APK文件,然后再反编译看看效果:

%title插图%num

可以看到,现在android-support包中所有代码都被保留下来了,不管是包名、类名、还是方法名都没有被混淆。LitePal中的代码也是同样的情况:

%title插图%num

再来看下MyFragment中的代码,如下所示:

%title插图%num

可以看到,MyFragment中的代码也没有被混淆,按照我们的要求被完全保留下来了。
*后再来看一下Utils类中的代码:

%title插图%num

很明显,Utils类并没有被完全保留下来,类名还是被混淆了,methodNormal()方法也被混淆了,但是methodUnused()没有被混淆,当然也没有被移除,因为我们的混淆配置生效了。
经过这些例子的演示,相信大家已经对Proguard的用法有了相当不错的理解了,那么根据自己的业务需求来去编写混淆配置相信也不是什么难事了吧?
Progaurd的使用非常灵活,基本上能够覆盖你所能想到的所有业务逻辑。这里再举个例子,之前一直有人问我使用LitePal时的混淆配置怎么写,其实真的很简单,LitePal作为开源库并不需要混淆,上面的配置已经演示了如何不混淆LitePal代码,然后所有代码中的Model是需要进行反射的,也不能混淆,那么只需要这样写就行了:

 

  1. -keep class * extends org.litepal.crud.DataSupport {
  2. *;
  3. }

 

因为LitePal中所有的Model都是应该继承DataSupport类的,所以这里我们将所有继承自DataSupport的类都进行保留就可以了。
关于混淆APK的用法就讲这么多,如果你还想继续了解关于Proguard的更多用法,可以参考官方文档:http://proguard.sourceforge.net/index.html#manual/usage.html

混淆Jar

在本篇文章的第二部分我想讲一讲混淆Jar包的内容,因为APK不一定是我们交付的唯一产品。就比如说我自己,我在公司是负责写SDK的,对于我来说交付出去的产品就是Jar包,而如果Jar包不混淆的话将会很容易就被别人反编译出来,从而泄漏程序逻辑。
实际上Android对混淆Jar包的支持在很早之前就有了,不管你使用多老版本的SDK,都能在 <Android SDK>/tools目录下找到proguard这个文件夹。然后打开里面的bin目录,你会看到如下文件:

%title插图%num

其中proguardgui.bat文件是允许我们以图形化的方式来对Jar包进行混淆的一个工具,今天我们就来讲解一下这个工具的用法。
在开始讲解这个工具之前,首先我们需要先准备一个Jar包,当然你从哪里搞到一个Jar包都是可以的,不过这里为了和刚才的混淆逻辑统一,我们就把本篇文章中的项目代码打成一个Jar包吧。
Eclipse中导出Jar包的方法非常简单,相信所有人都会,可是Android Studio当中就比较让人头疼了,因为Android Studio并没有提供一个专门用于导出Jar包的工具,因此我们只能自己动手了。
我们需要知道,任何一个Android Studio项目,只要编译成功之后就会在项目模块的build/intermediates/classes/debug目录下生成代码编译过后的class文件,因此只需通过打包命令将这些class文件打包成Jar包就行了,打开cmd,切换到项目的根目录,然后输入如下命令:

 

jar -cvf androidtest.jar -C app/build/intermediates/classes/debug .

 

在项目的根目录下就会生成androidtest.jar这个文件,这样我们就把Jar包准备好了。
现在双击proguardgui.bat打开混淆工具,如果是Mac或Ubuntu系统则使用sh proguardgui.sh命令打开混淆工具,界面如下图所示:

%title插图%num

其实从主界面上我们就能看出,这个Proguard工具支持Shrinking、Optimization、Obfuscation、Preverification四项操作,在左侧的侧边栏上也能看到相应的这些选项。Proguard的工作机制仍然还是要依赖于配置文件,当然我们也可以通过proguardgui工具来生成配置文件,不过由于配置选项太多了,每个都去一一设置太复杂,而且大多数还都是我们用不到的配置。因此*简单的方式就是直接拿现有的配置文件,然后再做些修改就行了。
那么我们从<Android SDK>/tools/proguard目录下将proguard-android.txt文件复制一份出来,然后点击主界面上的Load configuration按钮来加载复制出来的这份proguard-android.txt文件,完成后点击Next将进入Input/Output界面。
Input/Output界面是用于导入要混淆的Jar包、配置混淆后文件的输出路径、以及导入该Jar包所依赖的所有其它Jar包的。我们要混淆的当然就是androidtest.jar这个文件,那么这个Jar包又依赖了哪些Jar包呢?这里就需要整理一下了。

 

  • 首先我们写的都是Java代码,Java代码的运行要基于Jre基础之上,没有Jre计算机将无法识别Java的语法,因此*个要依赖的就是Jre的rt.jar。
  • 然后由于我们导出的Jar包中有Android相关的代码,比如Activity、Fragment等,因此还需要添加Android的编译库,android.jar。
  • 除此之外,我们使用的AppCompatActivity和Fragment分别来自于appcompat-v7包和support-v4包,那么这两个Jar包也是需要引入的。
  • *后就是代码中还引入了litepal-1.3.1.jar。

整理清楚了之后我们就来一个个添加,Input/Output有上下两个操作界面,上面是用于导入要混淆的Jar包和配置混淆后文件的输出路径的,下面则是导入该Jar包所依赖的所有其它Jar包的,全部导入后结果如下图所示:

%title插图%num

这些依赖的Jar包所存在的路径每台电脑都不一样,你所需要做的就是在你自己的电脑上成功找到这些依赖的Jar包并导入即可。
不过细心的朋友可能会发现,我在上面整理出了五个依赖的Jar包,但是在图中却添加了六个。这是我在写这篇文章时碰到的一个新的坑,也是定位了好久才解决的,我觉得有必要重点提一下。由于我平时混淆Jar包时里面很少会有Activity,所以没遇到过这个问题,但是本篇文章中的演示Jar包中不仅包含了Activty,还是继承自AppCompatActivity的。而AppCompatActivity的继承结构并不简单,如下图所示:

%title插图%num

其中AppCompatActivity是在appcompat-v7包中的,它的父类FragmentActivity是在support-v4包中的,这两个包我们都已经添加依赖了。但是FragmentActivity的父类就坑爹了,如果你去看BaseFragmentActivityHoneycomb和BaseFragmentActivityDonut这两个类的源码,你会发现它们都是在support-v4包中的:

%title插图%num

 

%title插图%num

可是如果你去support-v4的Jar包中找一下,你会发现压根就没有这两个类,所以我当时一直混淆报错就是因为这两个类不存在,继承结构在这里断掉了。而这两个类其实被规整到了另外一个internal的Jar包中,所以当你要混淆的Jar包中有Activity,并且还是继承自AppCompatActivity或FragmentActivity的话,那么就一定要记得导入这个internal Jar包的依赖,如下图所示:

%title插图%num

接下来点击Next进入Shrink界面,这个界面没什么需要配置的东西,但记得要将Shrink选项钩掉,因为我们这个Jar包是独立存在的,没有任何项目引用,如果钩中Shrink选项的话就会认为我们所有的代码都是无用的,从而把所有代码全压缩掉,导出一个空的Jar包。
继续点击Next进入Obfuscation界面,在这里可以添加一些混淆的逻辑,和混淆APK时不同的是,这里并不会自动帮我们排除混淆四大组件,因此必须要手动声明一下才行。点击*下方的Add按钮,然后在弹出的界面上编写排除逻辑,如下图所示:

%title插图%num

很简单,就是在继承那一栏写上android.app.Activity就行了,其它的组件原理也相同。
继续点击Next进入Optimiazation界面,不用修改任何东西,因为我们本身就不启用Optimization功能。继续点击Next进入Information界面,也不用修改任何东西,因为我们也不启用Preverification功能。
接着点击Next,进入Process界面,在这里可以通过点击View configuration按钮来预览一下目前我们的混淆配置文件,内容如下所示:

 

  1. -injars /Users/guolin/AndroidStudioProjects/AndroidTest/androidtest.jar
  2. -outjars /Users/guolin/androidtest_obfuscated.jar
  3. -libraryjars /Library/Java/JavaVirtualMachines/jdk1.7.0_79.jdk/Contents/Home/jre/lib/rt.jar
  4. -libraryjars /Users/guolin/Library/Android/sdk/platforms/android-23/android.jar
  5. -libraryjars /Users/guolin/AndroidStudioProjects/AndroidTest/app/build/intermediates/exploded-aar/com.android.support/appcompat-v7/23.2.0/jars/classes.jar
  6. -libraryjars /Users/guolin/AndroidStudioProjects/AndroidTest/app/build/intermediates/exploded-aar/com.android.support/support-v4/23.2.0/jars/classes.jar
  7. -libraryjars /Users/guolin/AndroidStudioProjects/AndroidTest/app/build/intermediates/exploded-aar/com.android.support/support-v4/23.2.0/jars/libs/internal_impl-23.2.0.jar
  8. -libraryjars /Users/guolin/AndroidStudioProjects/AndroidTest/app/libs/litepal-1.3.1.jar
  9. -dontshrink
  10. -dontoptimize
  11. -dontusemixedcaseclassnames
  12. -keepattributes *Annotation*
  13. -dontpreverify
  14. -verbose
  15. -dontwarn android.support.**
  16. -keep public class com.google.vending.licensing.ILicensingService
  17. -keep public class com.android.vending.licensing.ILicensingService
  18. # keep setters in Views so that animations can still work.
  19. # see http://proguard.sourceforge.net/manual/examples.html#beans
  20. -keepclassmembers public class * extends android.view.View {
  21. void set*(***);
  22. *** get*();
  23. }
  24. # We want to keep methods in Activity that could be used in the XML attribute onClick
  25. -keepclassmembers class * extends android.app.Activity {
  26. public void *(android.view.View);
  27. }
  28. -keepclassmembers class * extends android.os.Parcelable {
  29. public static final android.os.Parcelable$Creator CREATOR;
  30. }
  31. -keepclassmembers class **.R$* {
  32. public static <fields>;
  33. }
  34. -keep class * extends android.app.Activity
  35. -keep class * extends android.app.Service
  36. -keep class * extends android.content.BroadcastReceiver
  37. -keep class * extends android.content.ContentProvider
  38. # Also keep – Enumerations. Keep the special static methods that are required in
  39. # enumeration classes.
  40. -keepclassmembers enum * {
  41. public static **[] values();
  42. public static ** valueOf(java.lang.String);
  43. }
  44. # Keep names – Native method names. Keep all native class/method names.
  45. -keepclasseswithmembers,allowshrinking class * {
  46. native <methods>;
  47. }

 

恩,由此可见其实GUI工具只是给我们提供了一个方便操作的平台,背后工作的原理还是通过这些配置来实现的,相信上面的配置内容大家应该都能看得懂了吧。
接下来我们还可以点击Save configuration按钮来保存一下当前的配置文件,这样下次混淆的时候就可以直接Load进来而不用修改任何东西了。
*后点击Process!按钮来开始混淆处理,中间会提示一大堆的Note信息,我们不用理会,只要看到*终显示Processing completed successfully,就说明混淆Jar包已经成功了,如下图所示:

%title插图%num

混淆后的文件我将它配置在了/Users/guolin/androidtest_obfuscated.jar这里,如果反编译一下这个文件,你会发现和刚才反编译APK得到的结果是差不多的:MainActivity的类名以及从父类继承的方法名不会被混淆,NativeUtils的类名和其中的native方法名不会被混淆,Utils的methodUnsed方法不会被移除,因为我们禁用了Shrink功能,其余的代码都会被混淆。由于结果实在是太相似了,我就不再贴图了,参考本篇文章*部分的截图即可。

 

Android安全攻防战,反编译与混淆技术完全解析

之前一直有犹豫过要不要写这篇文章,毕竟去反编译人家的程序并不是什么值得骄傲的事情。不过单纯从技术角度上来讲,掌握反编译功能确实是一项非常有用的技能,可能平常不太会用得到,但是一旦真的需要用到的了,而你却不会的话,那就非常头疼了。另外既然别人可以反编译程序,我们当然有理由应该对程序进行一定程度的保护,因此代码混淆也是我们必须要掌握的一项技术。那么*近的两篇文章我们就围绕反编译和混淆这两个主题来进行一次完全解析。


反编译

我们都知道,Android程序打完包之后得到的是一个APK文件,这个文件是可以直接安装到任何Android手机上的,我们反编译其实也就是对这个APK文件进行反编译。Android的反编译主要又分为两个部分,一个是对代码的反编译,一个是对资源的反编译,我们马上来逐个学习一下。
在开始学习之前,首先我们需要准备一个APK文件,为了尊重所有开发者,我就不拿任何一个市面上的软件来演示了,而是自己写一个Demo用来测试。
这里我希望代码越简单越好,因此我们建立一个新项目,在Activity里加入一个按钮,当点击按钮时弹出一个Toast,就这么简单,代码如下所示:

  1. public class MainActivity extends AppCompatActivity {
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout.activity_main);
  6. Button button = (Button) findViewById(R.id.button);
  7. button.setOnClickListener(new View.OnClickListener() {
  8. @Override
  9. public void onClick(View v) {
  10. Toast.makeText(MainActivity.this, “you clicked button”, Toast.LENGTH_SHORT).show();
  11. }
  12. });
  13. }
  14. }

 

activity_main.xml中的资源如下所示:

  1. <?xml version=”1.0″ encoding=”utf-8″?>
  2. <RelativeLayout
  3. xmlns:android=“http://schemas.android.com/apk/res/android”
  4. android:layout_width=“match_parent”
  5. android:layout_height=“match_parent”
  6. android:paddingBottom=“@dimen/activity_vertical_margin”
  7. android:paddingLeft=“@dimen/activity_horizontal_margin”
  8. android:paddingRight=“@dimen/activity_horizontal_margin”
  9. android:paddingTop=“@dimen/activity_vertical_margin”>
  10. <Button
  11. android:id=“@+id/button”
  12. android:layout_width=“wrap_content”
  13. android:layout_height=“wrap_content”
  14. android:text=“Button”/>
  15. </RelativeLayout>

 

然后我们将代码打成一个APK包,并命名成Demo.apk,再把它安装到手机上,结果如下所示:

%title插图%num
好的,到这里准备工作就已经基本完成了,接下来就让我们开始对这个Demo程序进行反编译吧。

反编译代码

要想将APK文件中的代码反编译出来,我们需要用到以下两款工具:

  • dex2jar 这个工具用于将dex文件转换成jar文件
    下载地址:http://sourceforge.net/projects/dex2jar/files/
  • jd-gui 这个工具用于将jar文件转换成java代码
    下载地址:http://jd.benow.ca/

将这两个工具都下载好并解压,然后我们就开始对Demo程序进行反编译。解压dex2jar压缩包后,你会发现有很多个文件,如下图所示:

%title插图%num
其中我们要用到的是d2j-dex2jar.bat这个文件,当然如果你是linux或mac系统的话就要用d2j-dex2jar.sh这个文件。
然后我们将Demo.apk文件也进行解压,如果不知道怎么直接解压的可以先将文件重命名成Demo.zip,然后用解压软件打开。解压之后你会发现里面有一个classes.dex文件,如下图所示:
%title插图%num
这个classes.dex文件就是存放所有java代码的地方了,我们将它拷贝到dex2jar解压后的目录下,并在cmd中也进入到同样的目录,然后执行:

d2j-dex2jar classes.dex
  • 1

执行结果如下图所示:

%title插图%num
没有报任何错误,这就说明我们已经转换成功了。现在观察dex2jar目录,你会发现多了一个文件,如下图所示:
%title插图%num
可以看到,classes-dex2jar.jar这个文件就是我们借助工具之后成功转换出来的jar文件了。但是对于我们而言,jar文件也不是可读的,因此这里还需要再借助一下jd-gui这个工具来将jar文件转换成java代码。
下面就很简单了,使用jd-gui工具打开classes-dex2jar.jar这个文件,结果如下图所示:
%title插图%num
OK,由此可见,我们的代码反编译工作已经成功了,MainActivity中的代码非常清晰,基本已经做到了90%以上的还原工作。但是如果想要做到100%的代码还原还是非常有难度的,因为像setContentView()方法传入的参数,其实就是一个资源的id值而已,那么这里反编译也就只能将相应的id值进行还原,而无法变成像R.layout.activity_main这样直观的代码展示。
另外,除了MainActivity之外,还有很多其它的代码也被反编译出来了,因为当前项目有引用support-v4和support-v7的包,这些引用的library也会作为代码的一部分被打包到classes.dex文件当中,因此反编译的时候这些代码也会一起被还原。
好的,学完了反编译代码,接下来我们看一下如何反编译资源。

反编译资源

其实细心的朋友可能已经观察到了,刚才Demo.apk的解压目录当中不是已经有资源文件了吗,有AndroidManifest.xml文件,也有res目录。进入res目录当中,内容如下图所示:

%title插图%num
这不是所有资源文件都在这里了么?其实这些资源文件都是在打包的时候被编译过了,我们直接打开的话是看不到明文的,不信的话我们打开AndroidManifest.xml文件来瞧一瞧,内容如下图所示:
%title插图%num
可以看到,这代码是完全没法阅读的。当然如果你去打开activity_main.xml看看,结果也不会好到哪儿去:
%title插图%num
由此可见,直接对APK包进行解压是无法得到它的原始资源文件的,因此我们还需要对资源进行反编译才行。
要想将APK文件中的资源反编译出来,又要用到另外一个工具了:

  • apktool 这个工具用于*大幅度地还原APK文件中的9-patch图片、布局、字符串等等一系列的资源。
    下载地址:http://ibotpeaches.github.io/Apktool/install/

关于这个工具的下载我还要再补充几句,我们需要的就是apktool.bat和apktool.jar这两个文件。目前apktool.jar的*新版本是2.0.3,这里我就下载*新的了,然后将apktool_2.0.3.jar重命名成apktool.jar,并将它们放到同一个文件夹下就可以了,如下图所示:

%title插图%num
接下来的工作就很简单了,我们将Demo.apk拷贝到和这两个文件同样的目录当中,然后cmd也进入到这个目录下,并在cmd中执行如下命令:

apktool d Demo.apk
  • 1

其中d是decode的意思,表示我们要对Demo.apk这个文件进行解码。那除了这个基本用法之外,我们还可以再加上一些附加参数来控制decode的更多行为:

  • -f 如果目标文件夹已存在,则强制删除现有文件夹(默认如果目标文件夹已存在,则解码失败)。
  • -o 指定解码目标文件夹的名称(默认使用APK文件的名字来命名目标文件夹)。
  • -s 不反编译dex文件,也就是说classes.dex文件会被保留(默认会将dex文件解码成smali文件)。
  • -r 不反编译资源文件,也就是说resources.arsc文件会被保留(默认会将resources.arsc解码成具体的资源文件)。

常用用法就这么多了,那么上述命令的执行结果如下图所示:

%title插图%num
这就说明反编译资源已经成功了。
当然即使你在和我执行一模一样的操作,也有可能会在这里反编译失败,比如说会报如下错误:
这里写图片描述
出现这个错误的原因很有可能是你之前使用过apktool的老版本进行过反编译操作,然后apktool就会在你系统的 C:\Users\Administrator\apktool\framework 这个目录下生成一个名字为 1.apk 的缓存文件,将这个缓存文件删除掉,然后再重新执行反编译命令应该就可以成功了。
现在你会发现在当前目录下多了一个Demo文件夹,这个文件夹中存放的就是反编译的结果了。我们可以打开AndroidManifest.xml来瞧一瞧,如下图所示:
%title插图%num
怎么样?这样就完全能看得懂了吧,然后可以再到res/layout中看一下activity_main.xml文件,如下图所示:
%title插图%num
可以看到,activity_main.xml中的内容基本和源代码中的内容是一致的,外层是一个RelativeLayout,里面则是一个Button。你可以再到其它目录中去看一看别的资源,基本上都是可以正常还原的,这样我们就把反编译资源的方法也已经掌握了。

重新打包

那么对于反编译出来的文件夹,我们能不能重新把它打包成APK文件呢?答案是肯定的,只不过我实在想不出有什么义正言辞的理由可以让我们这么做。有的人会说汉化,没错,汉化的方式确实就是将一个APK进行反编译,然后翻译其中的资源再重新打包,但是不管怎么说这仍然是将别人的程序进行破解,所以我并不认为这是什么光荣的事情。那么我们就不去讨论本身这件事情的对或错,这里只是站在技术的角度来学习一下重新打包的相关知识。
首先我们来看一下通过apktool反编译后的包目录情况,如下图所示:

%title插图%num
其中,original文件夹下存放的是未经反编译过、原始的AndroidManifest.xml文件,res文件夹下存放的是反编译出来的所有资源,smali文件夹下存放的是反编译出来的所有代码,AndroidManifest.xml则是经过反编译还原后的manifest文件。这里值得一提的是smali文件夹,如果你进入到这个文件夹中你会发现它的目录结构和我们源码中src的目录结构是几乎一样的,主要的区别就是所有的java文件都变成了smali文件。smali文件其实也是真正的源代码,只不过它的语法和java完全不同,它有点类似于汇编的语法,是Android虚拟机所使用的寄存器语言,语法结构大概如下所示:
%title插图%num
看上去有点晕头转向是吗?但是如果你一旦能够看得懂smali文件的话,那么你就可以做很恐怖的事情了——你可以随意修改应用程序内的逻辑,将其进行破解!
不过我对这种黑技术并没有什么太大的兴趣,因此我也没有去做具体研究,但即使是这样,也已经可以对程序的逻辑做一定程度的修改了。比如说当我们点击按钮时会弹出 you clicked button 这样一句Toast,逻辑是写在MainActivity按钮点击事件的匿名类当中的,因此这段代码反编译之后一定就会在MainActivity$1.smali这个文件当中,让我们打开瞧一瞧,部分代码如下所示:
%title插图%num
虽说多数的代码我是看不懂的,但其中第47行实在太明显了,Toast显示的内容不就是在这里定义的么,那么如果我们想把Demo程序hack掉,就可以将这段字符串给改掉,比如说我把它改成Your app is been hacked 。
关于smali的语法,网上的资料也非常多,如果你对这门技术十分感兴趣的话可以直接上网去搜,这里我只是简单介绍一下,就不再深入讲解相关知识了。
改了一处代码后我们再来改一处资源吧,比如这里想要把Demo的应用图标给换掉,那么首先我们要准备好一张新的图片,如下图所示:
%title插图%num然后从AndroidManifest.xml文件中可以看出,应用图标使用的是ic_launcher.png这张图片,那么我们将上面篮球这张图片命名成ic_launcher.png,然后拷贝到所有以res/mipmap开头的文件夹当中完成替换操作。
在做了两处改动之后,我们现在来把反编译后的Demo文件夹重新打包成APK吧,其实非常简单,只需要在cmd中执行如下命令:

apktool b Demo -o New_Demo.apk
  • 1

其中b是build的意思,表示我们要将Demo文件夹打包成APK文件,-o用于指定新生成的APK文件名,这里新的文件叫作New_Demo.apk。执行结果如下图所示:

%title插图%num
现在你会发现在同级目录下面生成了一个新的APK文件:
%title插图%num
不过不要高兴得太早了,目前这个New_Demo.apk还是不能安装的,因为它还没有进行签名。那么如果这是别人的程序的话,我们从哪儿能拿到它原来的签名文件呢?很显然,这是根本没有办法拿到的,因此我们只能拿自己的签名文件来对这个APK文件重新进行签名,但同时也表明我们重新打包出来的软件就是个十足的盗版软件。这里大家学学技术就好了,希望不要有任何人去做什么坏事情。
那么这里我就用一个之前生成好的签名文件了,使用Android Studio或者Eclipse都可以非常简单地生成一个签名文件。
有了签名文件之后在cmd中执行签名命令就可以进行签名了,命令格式如下:

jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore 签名文件名 -storepass 签名密码 待签名的APK文件名 签名的别名
  • 1

其中jarsigner命令文件是存放在jdk的bin目录下的,需要将bin目录配置在系统的环境变量当中才可以在任何位置执行此命令。
签名之后的APK文件现在已经可以安装到手机上了,不过在此之前Android还*度建议我们对签名后的APK文件进行一次对齐操作,因为这样可以使得我们的程序在Android系统中运行得更快。对齐操作使用的是zipalign工具,该工具存放于<Android SDK>/build-tools/<version>目录下,将这个目录配置到系统环境变量当中就可以在任何位置执行此命令了。命令格式如下:

zipalign 4 New_Demo.apk New_Demo_aligned.apk
  • 1

其中4是固定值不能改变,后面指定待对齐的APK文件名和对齐后的APK文件名。运行这段命令之后就会生成一个New_Demo_aligned.apk文件,如下所示:

%title插图%num
这个New_Demo_aligned.apk就是我们重新打包签名对齐后的文件了,现在把它安装到手机上,效果如下图所示:
%title插图%num
可以看到,应用图标已经成功改成了篮球,另外点击按钮后弹出的Toast的提示也变成了我们修改后的文字,说明重新打包操作确实已经成功了。


好的,我们把反编译代码、反编译资源、重新打包这三大主题的内容都已经掌握了,关于反编译相关的内容就到这里,下篇文章会介绍Android代码混淆方面的相关技术,感兴趣的朋友请继续阅读: Android安全攻防战,反编译与混淆技术完全解析(下)。