android 源码角度全方位理解filter 及简单使用

写一个listview容易,写一个adapter容易,自己new一个线程过滤数据也容易,但是如何将过滤的效率发挥到最大化,不得不提一下android自带的filter类。 有同学肯定要问,过滤数据自己写一个完全没问题,为什么要用android自带的filter类?我原来也是自己写线程过滤,然而最近项目中遇到一个低配机,双核0.8GCPU,过滤效果实在是卡顿厉害,优化起见,使用了android内部filter试一下效果,结果真是比自己写的好用,于是认真学习了下源码,从头至尾备忘如下: ![](http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif) ![复制代码](http://common.cnblogs.com/images/copycode.gif) ``` private static final String LOG_TAG = “Filter”; private static final String THREAD_NAME = "Filter"; private static final int FILTER_TOKEN = 0xD0D0F00D; private static final int FINISH_TOKEN = 0xDEADBEEF; private Handler mThreadHandler; private Handler mResultHandler; private Delayer mDelayer; private final Object mLock = new Object(); <div class="cnblogs_code_toolbar"> <span class="cnblogs_code_copy"><a title="复制代码">![复制代码](http://common.cnblogs.com/images/copycode.gif)</a></span> </div> </div> </div> 其实用到的全局变量只有8个,并且有一半是常量:两个handler,一个delayer,一个对象锁。google开发大牛用这几个变量加上为数不多的几个局部变量就做出来了一个拓展性极佳的过滤器,不得不让人钦佩。 首先看构造方法: <div class="cnblogs_code"> ![](http://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif) <div id="cnblogs_code_open_c440c2da-b7c0-476f-bacb-a745b6a72f88" class="cnblogs_code_hide"> ``` public Filter() { mResultHandler = new ResultsHandler(); } 构造方法中二小强之一——ResultsHandler已经被创建了,顾名思义处理过滤操作结果。我们看看这个类的定义: ...

2015年12月4日 · 5 分钟 · 天边的星星

【新技能get】让App像Web一样发布新版本

2015.10.22 腾讯Bugly [微信分享](http://bugly.qq.com/blog/?p=781#) 背景 当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。 这时候就提出一个问题:**有没有办法以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装?** 虽然Android系统并没有提供这个技术,但是很幸运的告诉大家,答案是:可以。 解决方案 该方案基于的是android dex分包方案的,关于dex分包方案,网上有几篇解释了,所以这里就不再赘述,具体可以看这里:https://m.oschina.net/blog/308583(请复制链接到浏览器打开)。 简单的概括一下,就是把多个dex文件塞入到app的classloader之中,但是android dex拆包方案中的类是没有重复的,如果classes.dex和classes1.dex中有重复的类,当用到这个重复的类的时候,系统会选择哪个类进行加载呢? 让我们来看看类加载的代码: `&lt;strong>public&lt;/strong> Class findClass(String name, List&lt;Throwable&gt; suppressed) { &lt;strong>for&lt;/strong> (Element element : dexElements) { //每个Element就是一个dex文件 DexFile dex = element.dexFile; &lt;strong>if&lt;/strong> (dex != &lt;strong>null&lt;/strong>) { Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); &lt;strong>if &lt;/strong>(clazz != &lt;strong>null&lt;/strong>) { &lt;strong> return &lt;/strong>clazz; } } } &lt;strong> if &lt;/strong>(dexElementsSuppressedExceptions != &lt;strong>null&lt;/strong>) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } &lt;strong> return null&lt;/strong>; } ` 一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。 理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图: ![](https://mmbiz.qlogo.cn/mmbiz/tnZGrhTk4de39gh3QXrLudkAtkVzFOtDJoCpVwNq85HU9Lpw9tuCHLibDzQHjNicYnWNET0wvCWNVdZc6XUFI4yw/0?wx_fmt=jpeg) 在此基础上,我们构想了热补丁的方案,把有问题的类打包到一个dex(patch.dex)中去,然后把这个dex插入到Elements的最前面,如下图 ![](https://mmbiz.qlogo.cn/mmbiz/tnZGrhTk4de39gh3QXrLudkAtkVzFOtD4icmchNG1Z6HgDD20mzQBs11XyUyxlAL6OZBqO2Pnicf8t4vjnA01N8Q/0?wx_fmt=jpeg) 好,该方案基于第二个拆分dex的方案,方案实现如果懂拆分dex的原理的话,大家应该很快就会实现该方案,如果没有拆分dex的项目的话,可以参考一下谷歌的multidex方案实现。然后在插入数组的时候,把补丁包插入到最前面去。 好,看似问题很简单,轻松的搞定了,让我们来试验一下,修改某个类,然后打包成dex,插入到classloader,当加载类的时候出现了(本例中是ActivityManager要被替换): ![](https://mmbiz.qlogo.cn/mmbiz/tnZGrhTk4deljxecuyiaGnrNtAnOS3WkIicoAnyViaKrpmWcAwO3kebB20AbpkqbCVP0zlXkGQe5XpjZUiaYMw1NVw/0?wx_fmt=jpeg) **为什么会出现以上问题呢?** **从log的意思上来讲,ModuleManager引用了ActivityManager,但是发现这这两个类所在的dex不在一起,其中:** **1. ModuleManager在classes.dex中** **2. ActivityManager在patch.dex中** **结果发生了错误。** **这里有个问题,拆分dex的很多类都不是在同一个dex内的,怎么没有问题?** **让我们搜索一下抛出错误的代码所在,嘿咻嘿咻,找到了一下代码:** ![](https://mmbiz.qlogo.cn/mmbiz/tnZGrhTk4deljxecuyiaGnrNtAnOS3WkIiavUvrYI3Via0BrrMfBAXcicwZV2pejc56PVH1IwmruNmvibcbw8boCnVg/0?wx_fmt=png) 从代码上来看,如果两个相关联的类在不同的dex中就会报错,但是拆分dex没有报错这是为什么,原来这个校验的前提是: ![](https://mmbiz.qlogo.cn/mmbiz/tnZGrhTk4de39gh3QXrLudkAtkVzFOtDVqPD8yt3X72ETcJDG4icZyeDt3Sic6CLNAXAmicN3yZzQEu6ODr9CHWBw/0?wx_fmt=png) 如果引用者(也就是ModuleManager)这个类被打上了**CLASS_ISPREVERIFIED标志**,**那么就会进行dex的校验。那么这个标志是什么时候被打上去的?** **让我们在继续搜索一下代码,嘿咻嘿咻~~,在DexPrepare.cpp找到了一下代码:** ** ...

2015年12月2日 · 1 分钟 · 天边的星星

Dex分包变形记

原创 2015-11-26 李金涛 腾讯Bugly 一、背景 就在项目灰度测试前不久,爆出了在 Android 3.0以下手机上安装时出现 INSTALL _ FAILED_DEXOPT,导致安装失败。这一问题意味着项目将不能在 Android 3.0以下的手机上安装使用,对项目的发布有比较大的影响,所以必须尽快解决。 INSTAL L_FAILED_DEXOPT导致无法安装的问题,从根本上来说,可能是两个原因造成的: (1) 单个 dex 文件方法总数65K 的限制。 (2) Dexopt 的 LinearAlloc 限制。 当 Android 系统安装一个应用的时候,有一步是对 Dex 进行优化,这个过程有一个专门的工具来处理,叫 DexOpt。DexOpt 是在第一次加载 Dex 文件的时候执行的。这个过程会生成一个 ODEX 文件,即 Optimised Dex。执行 ODEX 的效率会比直接执行 Dex 文件的效率要高很多。 但是在早期的 Android 系统中,DexOpt 有两个问题。(一):DexOpt 会把每一个类的方法 id 检索起来,存在一个链表结构里面,但是这个链表的长度是用一个 short 类型来保存的,导致了方法 id 的数目不能够超过65536个。当一个项目足够大的时候,显然这个方法数的上限是不够的。(二):Dexopt 使用 LinearAlloc 来存储应用的方法信息。Dalvik LinearAlloc 是一个固定大小的缓冲区。在Android 版本的历史上,LinearAlloc 分别经历了4M/5M/8M/16M限制。Android 2.2和2.3的缓冲区只有5MB,Android 4.x提高到了8MB 或16MB。当方法数量过多导致超出缓冲区大小时,也会造成dexopt崩溃。 尽管在新版本的 Android 系统中,DexOpt 修复了方法数65K的限制问题,并且扩大了 LinearAlloc 限制,但是我们仍然需要对低版本的 Android 系统做兼容。 ...

2015年12月2日 · 5 分钟 · 天边的星星

Android开发者必须深入学习的10个应用开源项目

Android开发又将带来新一轮热潮,很多开发者都投入到这个浪潮中去了,创造了许许多多相当优秀的应用。其中也有许许多多的开发者提供了应用开源项 目,贡献出他们的智慧和创造力。学习开源代码是掌握技术的一个最佳方式。下面推荐几个应用开源项目,这些项目不仅提供了优秀的创意,也可以直接掌握 Android内核的接口使用: 1.Android团队提供的示例项目 如果不是从学习Android SDK中提供的那些样例代码开始,可能没有更好的方法来掌握在Android这个框架上开发。由Android的核心开发团队提供了15个优秀的示例项 目,包含了游戏、图像处理、时间显示、开始菜单快捷方式等。 地址:http://code.google.com/p/apps-for-android/ 2.Remote Droid RemoteDroid是一个Android应用,能够让用户使用自己的无线网络使用无线键盘、触摸屏操作手机。这个项目为开发者提供了如网络连接、触 摸屏手指运动等很好的样例。 地址:http://code.google.com/p/remotedroid/ 3.TorProxy和Shadow TorProxy应用实现了Android手机无线电电传通讯(TOR),和Shadow应用一起使用,可以使用手机匿名上网。从该项目源代码中,可以 掌握socket连接、管理cookie等方法。 地址:http://www.cl.cam.ac.uk/research/dtg/code/svn/android-tor/ http://www.cl.cam.ac.uk/research/dtg/android/tor/ 4、 Android SMSPopup SMSPopup可以截获短信内容显示在一个泡泡形状的窗口中。从这个项目中可以掌握到如何使用内置的短信SMS接口。 地址:http://code.google.com/p/android-smspopup/ 5、 Standup Timer Standup Timer应用用于控制站立会议时间,类似秒表倒计时,可以提醒每个人的讲话时间已到,从而保证每个与会者使用时间一样。从该项目的代码中,可以学会如何 使用时间函数。另外,这个项目的代码是采用视图view、模型model严格分离的设计思路。 地址:http://github.com/jwood/standup-timer 6、 Foursquare 是Foursquare.com的一个客户端应用,该应用主要分为两个模块:API(com.joelapenna.foursquare)和界面前端 (com.joelapenna.foursquared)两部分。从该项目代码中,可以学会如何同步、多线程、HTTP连接等技术。 地址:http://code.google.com/p/foursquared/ 7、 Pedometer Pedometer应用用于记录你每天走路步数的。尽管记录不一定精准,但是从这个项目中,可以学习几个不同的技术:加速器交互、语音更新、后台运行服 务等。 地址:http://code.google.com/p/pedometer/ 8、 OpenSudoku-android OpenSudoku是一个简单的九宫格数独游戏。从代码中可以学习到如何在视图中显示表格数据,以及如何和一个网站交互等技术。 地址:http://code.google.com/p/opensudoku-android/ 9、 ConnectBot ConnectBot是Android平台的一个客户端安全壳应用。从该项目代码中,可以学习到很多Android安全方面的内容,这些是你在开发应用 时经常需要考虑的安全问题。 地址:http://code.google.com/p/connectbot/ 10、 WordPress的Android应用 当然在最后不能不提WordPress的Android应用了,这是WordPress官方开发团队提供的一个项目。从代码中可以学习到XMLRPC调 用(当然还有更多的优秀内容)。 地址:http://android.svn.wordpress.org/trunk/

2015年12月2日 · 1 分钟 · 天边的星星

10 条提升 Android 性能的建议

每个人都知道一个 App 的成功,与这个 App 的性能体验有着很密切的关系。但是如何让你的 App 拥有极致性能体验呢?在 DroidCon NYC 2015 的这个分享里,Boris Farber 带来了他关于 Android Api 以及如何避免一些常见坑的经验。带你了解如何缩短启动时间,优化滑动效果,创建更加顺滑的用户体验。 <div class="company-info"> Transcription below provided by Realm: a replacement for SQLite that you can use in Java or Kotlin. [Check out the docs!](https://realm.io/docs/java/latest/) </div> [Sign up to be notified of new videos](http://eepurl.com/2mbQX) — we won’t email you for any other reason, ever. <hr /> <div class="author-info"> <div class="author-info-name"> About the Speaker: Boris Farber </div> <div class="author-info-bio"> 每个人都知道一个 App 的成功,更这个 App 的性能体验有着很密切的关系。但是如何让你的 App 拥有极致性能体验呢?在 DroidCon NYC 2015 的这个分享里,Boris Farber 带来了他关于 Android Api 以及如何避免一些常见的坑的经验。了解如何缩短启动时间,优化滑动效果,创建更加顺滑的用户体验。 </div> <div class="author-info-links"> [**@borisfarber](https://twitter.com/borisfarber) </div> </div> <hr /> *Save the date for [Droidcon SF](http://sf.droidcon.com/) in March — a conference with best-in-class presentations from leaders in all parts of the Android ecosystem.* <hr /> ### 简介 <a>(0:00)</a> 大家好,我是 Boris,现在是 Google 的一枚员工,目前专注于需要高性能的 App。这个分享是我长期以来从错误中,以及在给合作伙伴做咨询的时候攒下的最佳实践。如果你有一个小型的 App,读过之后,会在你的 App 成长阶段起到帮助。 我常常会见到那些启动时间很长,滑动不流畅,甚至出现没有反应的 App。我们通常要花很多时间去改善这些问题,毕竟我们都希望自己的 App 能够成功。 ### Activity 泄漏 <a>(1:17)</a> 我们第一个需要修复的问题就是 Activity 泄漏,我们先来看看内存泄漏是怎么发生的。 Activity 泄漏通常是内存泄漏的一种。为什么会泄漏呢?如果你持有一个未使用的 Activity 的引用,其实也就持有了 Activity 的布局,自然也就包含了所有的 View。最棘手的是持有静态引用。别忘了,Activity 和 Fragment 都有自己的生命周期。一旦我们持有了静态引用,Activity 和 Fragment 就不会被垃圾回收器清理掉了。这就是为什么静态引用很危险。 <div class="highlighter-rouge"> ``` &lt;span class="n">m_staticActivity&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">staticFragment&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="na">getActivity&lt;/span>&lt;span class="o">()&lt;/span> ...

2015年12月2日 · 4 分钟 · 天边的星星

Gradle脚本基础全攻略

**【工匠若水 [http://blog.csdn.net/yanbober](http://blog.csdn.net/yanbober) 转载请注明出处。[点我开始Android技术交流](https://github.com/yanbober/AndroidCommunication)】** # <a name="t0"></a>**1 背景** 在开始Gradle之前请务必保证自己已经初步了解了Groovy脚本,特别是闭包规则,如果还不了解Groovy则可以先看[《Groovy脚本基础全攻略》](http://blog.csdn.net/yanbober/article/details/49047515)这一篇博客速成一下Groovy基础,然后再看此文即可。关于Gradle速成干货基础详情也请参考[Geadle官方网站](https://www.gradle.org/),不好意思我太Low了。 ![这里写图片描述](http://img.blog.csdn.net/20151025165437078) Gradle核心是基于Groovy的领域特定语言(DSL,具体概念参见[《Groovy脚本基础全攻略》](http://blog.csdn.net/yanbober/article/details/49047515)),具有非常好的扩展性,所以不管是简单的独立项目还是大型的多项目构建它都能高效的提高构建任务,尤其对多项目支持是非常牛逼的;Gradle还提供了局部构建功能,譬如构建一个单独子项目时它会构建这个子项目依赖的所有子项目;当然了他对远程仓库和本地库的支持也很到位;哎呀,总之后面你就明白他的牛逼之处了。 既然Gradle核心是Groovy,Groovy本质又是Java,所以很明显可以发现Gradle环境必须依赖JDK与Groovy库,具体如下: - JDK版本必须是JDK6以上; - 因为Gradle自带Groovy库, 所以已安装的Groovy会被Gradle忽略; 具体Gradle环境配置好了以后如下图: ![这里写图片描述](http://img.blog.csdn.net/20151022113143209) **【工匠若水 [http://blog.csdn.net/yanbober](http://blog.csdn.net/yanbober) 转载请注明出处。[点我开始Android技术交流](https://github.com/yanbober/AndroidCommunication)】** # <a name="t1"></a>**2 Gradle DSL基础** Gradle的实质是配置脚本,执行一种类型的配置脚本时就会创建一个关联的对象,譬如执行Build script脚本就会创建一个Project对象,这个对象其实就是Gradle的代理对象。下面给出来各种类型Gradle对应的对象类型: <table> <tr> <th> 脚本类型 </th> <th> 关联对象类型 </th> </tr> <tr> <td> Build script </td> <td> Project </td> </tr> <tr> <td> Init script </td> <td> Gradle </td> </tr> <tr> <td> Settings script </td> <td> Settings </td> </tr> </table> Gradle的三种主要对象解释如下: - Project对象:每个build.gradle会转换成一个Project对象。 - Gradle对象:构建初始化时创建,整个构建执行过程中只有这么一个对象,一般很少去修改这个默认配置脚本。 - Settings对象:每个settings.gradle会转换成一个Settings对象。 可以看见,当我们编写指定类型Gradle脚本时我们可以直接使用关联对象的属性和方法;当然了,每个脚本也都实现了Script接口,也就是说我们也可以直接使用Script接口的属性与方法。 ## <a name="t2"></a>**2-1 构建脚本Build script(Project)** 在Gradle中每个待编译的工程都是一个Project(每个工程的build.gradle对应一个Project对象),每个Project在构建的时候都包含一系列Task,这些Task中很多又是Gradle的插件默认支持的。 PS:所谓的我们编写Gradle脚本,实质大多数时候都是在编写构建脚本Build script,所以说Project和Script对象的属性和方法等API非常重要。 每一个Project对象和build.gradle一一对应,一个项目在构建时都具备如下流程: - 为当前项目创建一个Settings类型的实例。 - 如果当前项目存在settings.gradle文件,则通过该文件配置刚才创建的Settings实例。 - 通过Settings实例的配置创建项目层级结构的Project对象实例。 - 最后通过上面创建的项目层级结构Project对象实例去执行每个Project对应的build.gradle脚本。 ## <a name="t3"></a>**2-2 初始化脚本Init script(Gradle)和设置脚本Settings script(Settings)** **Gradle对象:** 初始化脚本Init script(Gradle)类似于Gradle的其他类型脚本,这种脚本在构建开始之前运行,主要的用途是为接下来的Build script做一些准备工作。我们如果需要编写初始化脚本Init script,则可以把它按规则放置在USER_HOME/.gradle/相关目录下。譬如: ![这里写图片描述](http://img.blog.csdn.net/20151025171346780) 初始化脚本的Gradle对象代表了Gradle的调运,我们可以通过调用Project对象的getGradle()方法获得Gradle实例对象。 **Settings对象:** 在对工程进行配置(譬如多项目树构建)时Settings实例与settings.gradle文件一一对应,它用来进行一些项目设置的配置。这个文件一般放置在工程的根目录。譬如: ![这里写图片描述](http://img.blog.csdn.net/20151025171558864) ## <a name="t4"></a>**2-3 Build生命周期** Gradle的构建脚本生命周期具备三大步,如下: ![这里写图片描述](http://img.blog.csdn.net/20151025173643212) 可以看见,生命周期其实和上面构建脚本Build script的执行流程是可以关联上的。有了这个流程图我们接下里详细看下每个过程。 **settings.gradle文件:** 除了构建脚本文件,Gradle还定义了一个约定名称的设置文件(默认为settings.gradle)。该文件在初始化阶段被执行,对于多项目构建必须保证在根目录下有settings.gradle文件,对于单项目构建设置文件是可选的,不过建议还是写上。 如下是单项目构建的一个例子: ``` &lt;span class="hljs-comment">//settings.gradle&lt;/span> &lt;span class="hljs-built_in">println&lt;/span> &lt;span class="hljs-string">'This is executed during the initialization phase.'&lt;/span> ...

2015年12月2日 · 17 分钟 · 天边的星星

Android 6.0 运行时权限处理

运行时权限介绍 Android 6.0在我们原有的AndroidManifest.xml声明权限的基础上,又新增了运行时权限动态检测,以下权限都需要在运行时判断: 身体传感器 日历 摄像头 通讯录 地理位置 麦克风 电话 短信 存储空间 运行时权限处理 Android6.0系统默认为targetSdkVersion小于23的应用默认授予了所申请的所有权限,所以如果你以前的APP设置的targetSdkVersion低于23,在运行时也不会崩溃,但这也只是一个临时的救急策略,用户还是可以在设置中取消授予的权限。 声明目标SDK版本 我们需要在build.gradle中声明targetSdkVersion为23 android { compileSdkVersion 23 buildToolsVersion "23.0.1" defaultConfig { applicationId "com.yourcomany.app minSdkVersion 18 targetSdkVersion 23 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } 检查并申请权限 我们需要在用到权限的地方,每次都检查是否APP已经拥有权限,比如我们有一个下载功能,需要写SD卡的权限,我们在写入之前检查是否有WRITE_EXTERNAL_STORAGE权限,没有则申请权限 if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { //申请WRITE_EXTERNAL_STORAGE权限 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, WRITE_EXTERNAL_STORAGE_REQUEST_CODE); } 请求权限后,系统会弹出请求权限的Dialog ...

2015年12月1日 · 1 分钟 · 天边的星星

美团Android DEX自动拆包及动态加载简介

概述 作为一个android开发者,在开发应用时,随着业务规模发展到一定程度,不断地加入新功能、添加新的类库,代码在急剧的膨胀,相应的apk包的大小也急剧增加, 那么终有一天,你会不幸遇到这个错误: 生成的apk在android 2.3或之前的机器上无法安装,提示INSTALL_FAILED_DEXOPT 方法数量过多,编译时出错,提示: Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536 而问题产生的具体原因如下: 无法安装(Android 2.3 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崩溃。 超过最大方法数限制的问题,是由于DEX文件格式限制,一个DEX文件中method个数采用使用原生类型short来索引文件中的方法,也就是4个字节共计最多表达65536个method,field/class的个数也均有此限制。对于DEX文件,则是将工程所需全部class文件合并且压缩到一个DEX文件期间,也就是Android打包的DEX过程中, 单个DEX文件可被引用的方法总数(自己开发的代码以及所引用的Android框架、类库的代码)被限制为65536; 插件化? MultiDex? 解决这个问题,一般有下面几种方案,一种方案是加大Proguard的力度来减小DEX的大小和方法数,但这是治标不治本的方案,随着业务代码的添加,方法数终究会到达这个限制,一种比较流行的方案是插件化方案,另外一种是采用google提供的MultiDex方案,以及google在推出MultiDex之前Android Developers博客介绍的通过自定义类加载过程, 再就是Facebook推出的为Android应用开发的Dalvik补丁, 但facebook博客里写的不是很详细;我们在插件化方案上也做了探索和尝试,发现部署插件化方案,首先需要梳理和修改各个业务线的代码,使之解耦,改动的面和量比较巨大,通过一定的探讨和分析,我们认为对我们目前来说采用MultiDex方案更靠谱一些,这样我们可以快速和简洁的对代码进行拆分,同时代码改动也在可以接受的范围内; 这样我们采用了google提供的MultiDex方式进行了开发。 插件化方案在业内有不同的实现原理,这里不再一一列举,这里只列举下Google为构建超过65K方法数的应用提供官方支持的方案:MultiDex。 首先使用Android SDK Manager升级到最新的Android SDK Build Tools和Android Support Library。然后进行以下两步操作: 1.修改Gradle配置文件,启用MultiDex并包含MultiDex支持: android { compileSdkVersion 21 buildToolsVersion "21.1.0" defaultConfig { ... minSdkVersion 14 targetSdkVersion 21 ... // Enabling MultiDex support. MultiDexEnabled true } ... } dependencies { compile 'com.android.support:MultiDex:1.0.0' } 2.让应用支持多DEX文件。在官方文档中描述了三种可选方法: ...

2015年11月12日 · 4 分钟 · 天边的星星

美团Android资源混淆保护实践

前言 Android应用中的APK安全性一直遭人诟病,市面上充斥着各种被破解或者汉化的应用,破解者可以非常简单的通过破解工具就能对一个APK进行反编译、破解、汉化等等,这样就可以修改原有代码的逻辑、添加新代码、添加或修改资源、或者更有甚者植入病毒等等,从而破坏原有APK的安全和用户体验,最终伤害到用户和原有的开发者。 而事物都是有两方面的,有矛就有盾,针对Android应用安全的各种方案应运而生,大家比较熟悉一般是各类加壳加固的工具,我们可以使用这些工具来保护我们的APK,加壳加固是另外一个话题了,我们这里不对加壳加固进行介绍,后续如果有机会会单独开一个话题讨论,我们在开发过程中可以通过ProGuard或者DexGuard来保护我们的代码,从而实现相对的代码安全,但我们的资源呢?我们往往忽略对资源文件的保护,那这里将要分享的是如果采用常规方式对APK中的资源文件进行保护。 资源安全 资源安全这个话题目前大家关注度不算太高,相比较而言大家更关注代码安全,目前市面上各类APP基本都使用了ProGuard来保护代码的安全,但对资源文件的保护力度都不大,其实资源文件是存在比较大的安全隐患,那资源会有哪些安全隐患呢?下面我们通过一个比较简单的例子来说明下保护资源文件的重要性。 我们先用最常见的apktool工具来反编译一个应用来看看,通过运行下面命令就能进行反编译; apktool d -s xxx.apk 反编译成功后我们来看下反编译得到的文件结构(见下图); 通过上图中的目录结构,我们可以看到这个应用的资源文件大概有:anim、drawable、layout、menu、values等等,我们可以通过修改这些文件夹下的资源文件,并通过apktool进行回编译(apktool b 命令)就能创建一个经过修改过的APK应用,例如我们修改下图中红色横线所标示的layout文件,就能往原有APK的支付信息(根据资源名称猜测这个layout的意图)中添加一些我们自己的东西; 这个问题主要是因为我们在开发过程中倡导命名的规范性,一般都要求在命名时做到见名知意,这样能够方便我们自己的理解和维护,但同时这也方便了破解者,破解者可以轻松的根据文件名称来猜测这个文件的意图和作用,从而做破坏性的修改。 通过这个例子我们可以看出目前资源安全的重要性,那如何做到资源安全呢?安全都是相对的,没有绝对的安全,我们接下来要讨论的是类似Proguard方式的对我们的资源进行保护。我们主要是通过修改AAPT工具来对资源进行保护,为了方便理解,下面先讲一下Android应用是怎么查找资源的。 Android查找资源的流程 在Android系统中,每一个应用程序一般都会配置很多资源,用来适配不同密度、大小和方向的屏幕,以及适配不同的国家、地区和语言等等。这些资源是在应用程序运行时自动根据设备的当前配置信息进行适配的。这也就是说,给定一个相同的资源ID,在不同的设备配置之下,查找到的可能是不同的资源。 这个查找过程对应用程序来说,是完全透明的,这个过程主要是靠Android资源管理框架来完成的,而Android资源管理框架实际是由AssetManager和Resources两个类来实现的。其中,Resources类可以根据ID来查找资源,而AssetManager类根据文件名来查找资源。事实上,如果一个资源ID对应的是一个文件,那么Resources类是先根据ID来找到资源文件名称,然后再将该文件名称交给AssetManager类来打开对应的文件的。基本流程如下图: 通过上图我们可以看到Resources是通过resources.arsc把Resource的ID转化成资源文件的名称,然后交由AssetManager来加载的。 而Resources.arsc这个文件是存放在APK包中的,他是由AAPT工具在打包过程中生成的,他本身是一个资源的索引表,里面维护者资源ID、Name、Path或者Value的对应关系,AssetManager通过这个索引表,就可以通过资源的ID找到这个资源对应的文件或者数据。 AAPT介绍 AAPT是Android Asset Packaging Tool的缩写,它存放在SDK的tools/目录下,AAPT的功能很强大,可以通过它查看查看、创建、更新压缩文件(如 .zip文件,.jar文件, .apk文件), 它也可以把资源编译为二进制文件,并生成resources.arsc, AAPT这个工具在APK打包过程中起到了非常重要作用,在打包过程中使用AAPT对APK中用到的资源进行打包,这里不对AAPT这个工具做过多的讨论,只看一下AAPT这个工具在打包过程中起到的作用,下图是AAPT打包的流程: AAPT这个工具在打包过程中主要做了下列工作: 把”assets”和”res/raw”目录下的所有资源进行打包(会根据不同的文件后缀选择压缩或不压缩),而”res/”目录下的其他资源进行编译或者其他处理(具体处理方式视文件后缀不同而不同,例如:”.xml”会编译成二进制文件,”.png”文件会进行优化等等)后才进行打包; 会对除了assets资源之外所有的资源赋予一个资源ID常量,并且会生成一个资源索引表resources.arsc; 编译AndroidManifest.xml成二进制的XML文件; 把上面3个步骤中生成结果保存在一个*.ap_文件,并把各个资源ID常量定义在一个R.java中; .ap_这个文件会在生成APK时放入APK包中, .ap _这个文件本身是一个ZIP包,他里面包含resources.arsc、AndroidManifest.xml、assets以及所有的资源文件,下图是UNZIP后的截图: 可以看出*.ap_这个文件中包含的内容,这个文件存放在build/intermediates/res的目录下,下图是这个文件存放的路径截图: 资源保护 我们这里参考Proguard Obfuscator方式,对APK中资源文件名使用简短无意义名称进行替换,给破解者制造困难,从而做到资源的相对安全;通过上面分析,我们可以看出通过修改AAPT在生成resources.arsc和*.ap_时把资源文件的名称进行替换,从而保护资源。 通过阅读AAPT编译资源的代码,我们发现修改AAPT在处理资源文件相关的源码是能够做到资源文件名的替换,下面是Resource.cpp中makeFileResources()的修改的代码片段: static status_t makeFileResources(Bundle* bundle, const sp<AaptAssets>& assets, ResourceTable* table, const sp<ResourceTypeSet>& set, const char* resType) { String8 type8(resType); String16 type16(resType); bool hasErrors = false; ResourceDirIterator it(set, String8(resType)); ssize_t res; while ((res=it.next()) == NO_ERROR) { if (bundle->getVerbose()) { printf(" (new resource id %s from %s)\n", it.getBaseName().string(), it.getFile()->getPrintableSource().string()); } String16 baseName(it.getBaseName()); const char16_t* str = baseName.string(); const char16_t* const end = str + baseName.size(); while (str < end) { if (!((*str >= 'a' && *str <= 'z') || (*str >= '0' && *str <= '9') || *str == '_' || *str == '.')) { fprintf(stderr, "%s: Invalid file name: must contain only [a-z0-9_.]\n", it.getPath().string()); hasErrors = true; } str++; } String8 resPath = it.getPath(); resPath.convertToResPath(); String8 obfuscationName; String8 obfuscationPath = getObfuscationName(resPath, obfuscationName); table->addEntry(SourcePos(it.getPath(), 0), String16(assets->getPackage()), type16, baseName, // String16(obfuscationName), String16(obfuscationPath), // resPath NULL, &it.getParams()); assets->addResource(it.getLeafName(), obfuscationPath/*resPath*/, it.getFile(), type8); } return hasErrors ? UNKNOWN_ERROR : NO_ERROR; } 上述代码是在ResourceTable和Assets中添加资源文件时, 对资源文件名称进行修改,这就能够做到资源文件名称的替换,这样通过使用修改过的AAPT编译资源并进行打包,我们再用上面讲到的apktool这个工具进行反编译,下图是反编译后的截图: ...

2015年11月12日 · 1 分钟 · 天边的星星

美团Android自动化之旅—适配渠道包

概述 前一篇文章(美团Android自动化之旅—生成渠道包)介绍了Android中几种生成渠道包的方式,基本解决了打包慢的问题。 但是,随着渠道越来越多,不同渠道对应用的要求也不尽相同。例如,有的渠道要求美团客户端的应用名为美团,有的渠道要求应用名为美团团购。又比如,有些渠道要求应用不能使用第三方统计工具(如flurry)。总之,每次打包都需要对这些渠道进行适配。 之前的做法是为每个需要适配的渠道创建一个Git分支,发版时再切换到相应的分支,并合并主分支的代码。适配的渠道比较少的话这种方式还可以接受,如果分支比较多,对开发人员来说简直就是噩梦。还好,自从有了Gradle flavor,一切都变得简单了。本文假定读者使用过Gradle,如果还不了解建议先阅读相关文档。 Flavor 先来看build.gradle文件中的一段代码: android { .... productFlavors { flavor1 { minSdkVersion 14 } } } 上例定义了一个flavor:flavor1,并指定了应用的minSdkVersion为14(当然还可以配置更多的属性,具体可参考相关文档)。与此同时,Gradle还会为该flavor关联对应的sourceSet,默认位置为src/<flavorName>目录,对应到本例就是src/flavor1。 接下来,要做的就是根据具体的需求在build.gradle文件中配置flavor,并添加必要的代码和资源文件。以flavor1为例,运行gradle assembleFlavor1命令既可生成所需的适配包。下面主要介绍美团团购Android客户端的一些适配案例。 案例 使用不同的包名 美团团购Android客户端之前有两个版本:手机版(com.meituan.group)和hd版(com.meituan.group.hd),两个版本使用了不同的代码。目前hd版对应的代码已不再维护,希望能直接使用手机版的代码。解决该问题可以有多种方法,不过使用flavor相对比较简单,示例如下: productFlavors { hd { applicationId "com.meituan.group.hd" } } 上面的代码添加了一个名为hd的flavor,并指定了应用的包名为com.meituan.group.hd,运行gradle assembleHd命令即可生成hd适配包。 控制是否自动更新 美团团购Android客户端在启动时会默认检查客户端是否有更新,如果有更新就会提示用户下载。但是有些渠道和应用市场不允许这种默认行为,所以在适配这些渠道时需要禁止自动更新功能。 解决的思路是提供一个配置字段,应用启动的时候检查该字段的值以决定是否开启自动更新功能。使用flavor可以完美的解决这类问题。 Gradle会在generateSources阶段为flavor生成一个BuildConfig.java文件。BuildConfig类默认提供了一些常量字段,比如应用的版本名(VERSION_NAME),应用的包名(PACKAGE_NAME)等。更强大的是,开发者还可以添加自定义的一些字段。下面的示例假设wandoujia市场默认禁止自动更新功能: android { defaultConfig { buildConfigField "boolean", "AUTO_UPDATES", "true" } productFlavors { wandoujia { buildConfigField "boolean", "AUTO_UPDATES", "false" } } } 上面的代码会在BuildConfig类中生成AUTO_UPDATES布尔常量,默认值为true,在使用wandoujia flavor时,该值会被设置成false。接下来就可以在代码中使用AUTO_UPDATES常量来判断是否开启自动更新功能了。最后,运行gradle assembleWandoujia命令即可生成默认不开启自动升级功能的渠道包,是不是很简单。 使用不同的应用名 最常见的一类适配是修改应用的资源。例如,美团团购Android客户端的应用名是美团,但有的渠道需要把应用名修改为美团团购;还有,客户端经常会和一些应用分发市场合作,需要在应用的启动界面中加上第三方市场的Logo,类似这类适配形式还有很多。 Gradle在构建应用时,会优先使用flavor所属dataSet中的同名资源。所以,解决思路就是在flavor的dataSet中添加同名的字符串资源,以覆盖默认的资源。下面以适配wandoujia渠道的应用名为美团团购为例进行介绍。 首先,在build.gradle配置文件中添加如下flavor: android { productFlavors { wandoujia { } } } 上面的配置会默认src/wandoujia目录为wandoujia flavor的dataSet。 ...

2015年11月12日 · 1 分钟 · 天边的星星