图解 Android 事件分发机制

在Android开发中,事件分发机制是一块Android比较重要的知识体系,了解并熟悉整套的分发机制有助于更好的分析各种点击滑动失效问题,更好去扩展控件的事件功能和开发自定义控件,同时事件分发机制也是Android面试必问考点之一,如果你能把下面的一些事件分发图当场画出来肯定加分不少。废话不多说,总结一句:事件分发机制很重要

Android 事件分发流

关于Android 事件分发机制网上的博文很多,但是很多都是写个Demo然后贴一下输出的Log或者拿源码分析,然后一堆的注释和说明,如果用心的去看肯定是收获不少但是确实很难把整个流程说清和记住。曾经也是拼命想记住整个流程,但是一段时间又忘了,最后觉得分析这种问题和事件流的走向,一张图来解释和说明会清晰很多,下面我根据画的一张事件分发流程图,说明的事件从用户点击之后,在不同函数不同返回值的情况的最终走向。

图 1.

注:

  • 仔细看的话,图分为3层,从上往下依次是Activity、ViewGroup、View
  • 事件从左上角那个白色箭头开始,由Activity的dispatchTouchEvent做分发
  • 箭头的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super 的意思是调用父类实现。
  • dispatchTouchEvent和 onTouchEvent的框里有个【true—->消费】的字,表示的意思是如果方法返回true,那么代表事件就此消费,不会继续往别的地方传了,事件终止。
  • 目前所有的图的事件是针对ACTION_DOWN的,对于ACTION_MOVE和ACTION_UP我们最后做分析。
  • 之前图中的Activity 的dispatchTouchEvent 有误(图已修复),只有return super.dispatchTouchEvent(ev) 才是往下走,返回true 或者 false 事件就被消费了(终止传递)。

仔细看整个图,我们得出事件流 走向的几个结论(希望读者专心的看下图 1,多看几遍,脑子有比较清晰的概念。)
1、如果事件不被中断,整个事件流向是一个类U型图,我们来看下这张图,可能更能理解U型图的意思。

图 2.

所以如果我们没有对控件里面的方法进行重写或更改返回值,而直接用super调用父类的默认实现,那么整个事件流向应该是从Activity—->ViewGroup—>View 从上往下调用dispatchTouchEvent方法,一直到叶子节点(View)的时候,再由View—>ViewGroup—>Activity从下往上调用onTouchEvent方法。

2、dispatchTouchEvent 和 onTouchEvent 一旦return true,事件就停止传递了(到达终点)(没有谁能再收到这个事件)。看下图中只要return true事件就没再继续传下去了,对于return true我们经常说事件被消费了,消费了的意思就是事件走到这里就是终点,不会往下传,没有谁能再收到这个事件了

图 3.

3、dispatchTouchEvent 和 onTouchEvent return false的时候事件都回传给父控件的onTouchEvent处理。

图 4.

看上图深蓝色的线,对于返回false的情况,事件都是传给父控件onTouchEvent处理。

  • 对于dispatchTouchEvent 返回 false 的含义应该是:事件停止往子View传递和分发同时开始往父控件回溯(父控件的onTouchEvent开始从下往上回传直到某个onTouchEvent return true),事件分发机制就像递归,return false 的意义就是递归停止然后开始回溯。
  • 对于onTouchEvent return false 就比较简单了,它就是不消费事件,并让事件继续往父控件的方向从下往上流动。

4、dispatchTouchEvent、onTouchEvent、onInterceptTouchEvent
ViewGroup 和View的这些方法的默认实现就是会让整个事件安装U型完整走完,所以 return super.xxxxxx() 就会让事件依照U型的方向的完整走完整个事件流动路径),中间不做任何改动,不回溯、不终止,每个环节都走到。

Paste_Image.png

所以如果看到方法return super.xxxxx() 那么事件的下一个流向就是走U型下一个目标,稍微记住上面这张图,你就能很快判断出下一个走向是哪个控件的哪个函数。
5、onInterceptTouchEvent 的作用

图 5.

Intercept 的意思就拦截,每个ViewGroup每次在做分发的时候,问一问拦截器要不要拦截(也就是问问自己这个事件要不要自己来处理)如果要自己处理那就在onInterceptTouchEvent方法中 return true就会交给自己的onTouchEvent的处理,如果不拦截就是继续往子控件往下传。默认是不会去拦截的,因为子View也需要这个事件,所以onInterceptTouchEvent拦截器return super.onInterceptTouchEvent()和return false是一样的,是不会拦截的,事件会继续往子View的dispatchTouchEvent传递

6、ViewGroup 和View 的dispatchTouchEvent方法返回super.dispatchTouchEvent()的时候事件流走向。

图 6

首先看下ViewGroup 的dispatchTouchEvent,之前说的return true是终结传递。return false 是回溯到父View的onTouchEvent,然后ViewGroup怎样通过dispatchTouchEvent方法能把事件分发到自己的onTouchEvent处理呢,return true和false 都不行,那么只能通过Interceptor把事件拦截下来给自己的onTouchEvent,所以ViewGroup dispatchTouchEvent方法的super默认实现就是去调用onInterceptTouchEvent,记住这一点
那么对于View的dispatchTouchEvent return super.dispatchTouchEvent()的时候呢事件会传到哪里呢,很遗憾View没有拦截器。但是同样的道理return true是终结。return false 是回溯会父类的onTouchEvent,怎样把事件分发给自己的onTouchEvent 处理呢,那只能return super.dispatchTouchEvent,View类的dispatchTouchEvent()方法默认实现就是能帮你调用View自己的onTouchEvent方法的。

说了这么多,不知道有说清楚没有,我这边最后总结一下:

  • 对于 dispatchTouchEvent,onTouchEvent,return true是终结事件传递。return false 是回溯到父View的onTouchEvent方法。
  • ViewGroup 想把自己分发给自己的onTouchEvent,需要拦截器onInterceptTouchEvent方法return true 把事件拦截下来。
  • ViewGroup 的拦截器onInterceptTouchEvent 默认是不拦截的,所以return super.onInterceptTouchEvent()=return false;
  • View 没有拦截器,为了让View可以把事件分发给自己的onTouchEvent,View的dispatchTouchEvent默认实现(super)就是把事件分发给自己的onTouchEvent。

ViewGroup和View 的dispatchTouchEvent 是做事件分发,那么这个事件可能分发出去的四个目标

注:——> 后面代表事件目标需要怎么做。
1、 自己消费,终结传递。——->return true ;
2、 给自己的onTouchEvent处理——-> 调用super.dispatchTouchEvent()系统默认会去调用 onInterceptTouchEvent,在onInterceptTouchEvent return true就会去把事件分给自己的onTouchEvent处理。
3、 传给子View——>调用super.dispatchTouchEvent()默认实现会去调用 onInterceptTouchEvent 在onInterceptTouchEvent return false,就会把事件传给子类。
4、 不传给子View,事件终止往下传递,事件开始回溯,从父View的onTouchEvent开始事件从下到上回归执行每个控件的onTouchEvent——->return false;
注: 由于View没有子View所以不需要onInterceptTouchEvent 来控件是否把事件传递给子View还是拦截,所以View的事件分发调用super.dispatchTouchEvent()的时候默认把事件传给自己的onTouchEvent处理(相当于拦截),对比ViewGroup的dispatchTouchEvent 事件分发,View的事件分发没有上面提到的4个目标的第3点。

ViewGroup和View的onTouchEvent方法是做事件处理的,那么这个事件只能有两个处理方式:

1、自己消费掉,事件终结,不再传给谁—–>return true;
2、继续从下往上传,不消费事件,让父View也能收到到这个事件—–>return false;View的默认实现是不消费的。所以super==false。

ViewGroup的onInterceptTouchEvent方法对于事件有两种情况:

1、拦截下来,给自己的onTouchEvent处理—>return true;
2、不拦截,把事件往下传给子View—->return false,ViewGroup默认是不拦截的,所以super==false;

关于ACTION_MOVE 和 ACTION_UP

上面讲解的都是针对ACTION_DOWN的事件传递,ACTION_MOVE和ACTION_UP在传递的过程中并不是和ACTION_DOWN 一样,你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个事件(如ACTION_DOWN)返回true,才会收到ACTION_MOVE和ACTION_UP的事件。具体这句话很多博客都说了,但是具体含义是什么呢?我们来看一下下面的具体分析。

上面提到过了,事件如果不被打断的话是会不断往下传到叶子层(View),然后又不断回传到Activity,dispatchTouchEvent 和 onTouchEvent 可以通过return true 消费事件,终结事件传递,而onInterceptTouchEvent 并不能消费事件,它相当于是一个分叉口起到分流导流的作用,ACTION_MOVE和ACTION_UP 会在哪些函数被调用,之前说了并不是哪个函数收到了ACTION_DOWN,就会收到 ACTION_MOVE 等后续的事件的。
下面通过几张图看看不同场景下,ACTION_MOVE事件和ACTION_UP事件的具体走向并总结一下规律。

1、我们在ViewGroup1 的dispatchTouchEvent 方法返回true消费这次事件

ACTION_DOWN 事件从(Activity的dispatchTouchEvent)——–> (ViewGroup1 的dispatchTouchEvent) 后结束传递,事件被消费(如下图红色的箭头代码ACTION_DOWN 事件的流向)。

//打印日志
Activity | dispatchTouchEvent --> ACTION_DOWN 
ViewGroup1 | dispatchTouchEvent --> ACTION_DOWN
---->消费

在这种场景下ACTION_MOVE和ACTION_UP 将如何呢,看下面的打出来的日志

Activity | dispatchTouchEvent --> ACTION_MOVE 
ViewGroup1 | dispatchTouchEvent --> ACTION_MOVE
----
TouchEventActivity | dispatchTouchEvent --> ACTION_UP 
ViewGroup1 | dispatchTouchEvent --> ACTION_UP
----

下图中
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

2、我们在ViewGroup2 的dispatchTouchEvent 返回true消费这次事件

Activity | dispatchTouchEvent --> ACTION_DOWN 
ViewGroup1 | dispatchTouchEvent --> ACTION_DOWN
ViewGroup1 | onInterceptTouchEvent --> ACTION_DOWN
ViewGroup2 | dispatchTouchEvent --> ACTION_DOWN
---->消费
Activity | dispatchTouchEvent --> ACTION_MOVE 
ViewGroup1 | dispatchTouchEvent --> ACTION_MOVE
ViewGroup1 | onInterceptTouchEvent --> ACTION_MOVE
ViewGroup2 | dispatchTouchEvent --> ACTION_MOVE
----
TouchEventActivity | dispatchTouchEvent --> ACTION_UP 
ViewGroup1 | dispatchTouchEvent --> ACTION_UP
ViewGroup1 | onInterceptTouchEvent --> ACTION_UP
ViewGroup2 | dispatchTouchEvent --> ACTION_UP
----

红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

Paste_Image.png

3、我们在View 的dispatchTouchEvent 返回true消费这次事件
这个我不就画图了,效果和在ViewGroup2 的dispatchTouchEvent return true的差不多,同样的收到ACTION_DOWN 的dispatchTouchEvent函数都能收到 ACTION_MOVE和ACTION_UP。
所以我们就基本可以得出结论如果在某个控件的dispatchTouchEvent 放回true消费终结事件,那么收到ACTION_DOWN 的函数也能收到 ACTION_MOVE和ACTION_UP。

4、我们在View 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

5、我们在ViewGroup 2 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

6、我们在ViewGroup 1 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

7、我们在Activity 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

8、我们在View的dispatchTouchEvent 返回false并且Activity 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

9、我们在View的dispatchTouchEvent 返回false并且ViewGroup 1 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

10、我们在View的dispatchTouchEvent 返回false并且在ViewGroup 2 的onTouchEvent 返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

11、我们在ViewGroup2的dispatchTouchEvent 返回false并且在ViewGroup1 的onTouchEvent返回true消费这次事件
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

12、我们在ViewGroup2的onInterceptTouchEvent 返回true拦截此次事件并且在ViewGroup 1 的onTouchEvent返回true消费这次事件。
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向

一下子画了好多图,还有好几种情况就不再画了,相信你也看出规律了,对于在onTouchEvent消费事件的情况:在哪个View的onTouchEvent 返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent 并结束本次事件传递过程。

对于ACTION_MOVE、ACTION_UP总结:ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传,如果ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递,如果ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。
转自:http://www.jianshu.com/p/e99b5e8bd67b

iOS复杂动画之抽丝剥茧(Objective-C & Swift)

一、前言

随着开发者的增多和时间的累积,AppStore已经有非常多的应用了,每年都有很多新的APP产生。但是我们手机上留存的应用有限,所以如何吸引用户,成为产品设计的一项重要内容。其中炫酷的动画效果是重要内容之一,我们会发现很多好的应用上面都有许多很炫的效果。可能一提到炫酷的动画,很多人都很头疼,因为动画并不是那么好做,实现一个好的动画需要时间、耐心和好的思路。下面我们就以一个有趣的动画(如下图)为例,抽丝剥茧,看看到底是怎么实现的!

二、分析

上面图中的动画第一眼看起来的确是有点复杂,但是我们来一步步分析,就会发现其实并不是那么难。仔细看一下就会发现,大致步骤如下:

1、先出来一个圆

2、圆形在水平和竖直方向上被挤压,呈椭圆形状的一个过程,最后恢复成圆形

3、圆形的左下角、右下角和顶部分别按顺序凸出一小部分

4、圆和凸出部分形成的图形旋转一圈后变成三角形

5、三角形的左边先后出来两条宽线,将三角形围在一个矩形中

6、矩形由底部向上被波浪状填满

7、被填满的矩形放大至全屏,弹出Welcome

动画大致就分为上面几个步骤,拆分后我们一步步来实现其中的效果(下面所示步骤中以Swift代码为例,demo中分别有Objective-C和Swift的实现)。

三、实现圆形以及椭圆的渐变

首先,我们创建了一个新工程后,然后新建了一个名AnimationView的类继承UIView,这个是用来显示动画效果的一个view。然后先添加CircleLayer(圆形layer),随后实现由小变大的效果。

其中expand()这个方法如下

运行效果如下

第一步做好了,接下来就是呈椭圆形状的变化了,仔细分析就比如一个弹性小球,竖直方向捏一下,水平方向捏一下这样的效果。这其实就是一个组合动画,如下

上面代码中实现了从 圆 → 椭圆(x方向长轴)→ 椭圆(y方向长轴)→ 圆这一系列的变化,最后组合成一个动画。这一步实现后效果如下

四、实现圆形边缘的凸出部分

关于这个凸出部分,乍一看可能感觉会比较难实现,看起来挺复杂的。其实实现的原理很简单,仔细分析我们会发现这三个凸出部分连起来刚好是一个三角形,那么第一步我们就在之前的基础上先加一个三角形的layer,如下

然后设置圆角


下面就是来做凸出部分了,原理其实很简单,就是将现在这个三角形保持中心不变,左边向左延伸即可

然后同理,保持中心不变分别按顺序向右和向上拉伸

具体过程是这样的

我们接下来把三角形的颜色改一下

这里颜色相同了我们就可以看到了这个凸出的这个效果,调到正常速率(为了演示,把动画速率调慢了) ,联合之前所有的动作,到现在为止,效果是这样的

到现在为止,看上去还不错,差不多已经完成一半了,继续下一步!

五、实现旋转和矩形

旋转来说很简单了,大家估计都做过旋转动画,这里就是把前面形成的图形旋转一下(当然要注意设置锚点anchorPoint)

旋转之后原图形被切成了一个三角形,思路就是把原来的大圆,按着这个大三角形的内切圆剪切一下即可

接下来就是画矩形,新建一个RectangleLayer,划线

最后面就是经典的水波纹动画了,不多说,直接上代码

找几个点控制水波形状,画出贝塞尔曲线即可,到这里基本就完成了。接下来最后一步,放大,并弹出Welcome

放大完以后设置代理,然后在主的vc中添加Welcome这个Label

到现在为止,动画全部完成



六、最后

同样,还是提供了两个版本(Objective-C & Swift),你可以在这里查看源码!

Android APK签名详解

App为什么需要签名?

  1. 对Apk的完整性和发布机构唯一性的校验
  2. 签名之后无法阻止APK被人修改,但修改后再签名就和原先的签名不一致,可以避免有些人用相同包名的APK来替换已有的应用
  3. 相同签名的升级软件可以覆盖安装旧版本的软件
  4. 签名的私钥在开发者那里,公钥打包在APK内,不同公钥对应不同私钥,通过比较公钥可以判断私钥是否一致
  5. 允许代码和数据共享。android中提供了一个基于签名的Permission标签。通过允许的设置,我们可以实现对不同App之间的访问和共享,如下:
1
AndroidManifest.xml:<permission android:protectionLevel="normal" />

其中protectionLevel标签有4种值:normal(缺省值),dangerous, signature,signatureOrSystem。normal是低风险的,所有的App不能访问和共享此App。dangerous是高风险的,所有的App都能访问和共享此App。signature是指具有相同签名的App可以访问和共享此App。

使用SDK工具生成的DEBUG Key签名

Android签名有两种方式:DEBUG和RELEASE,在开发测试期间使用DEBUG方式,BUILD时,会自动使用工具KeyTools创建KEY包括别名和密码。每次编译时,都会使用DEBUG的KEY进行签名。
在第一次安装Android开发环境的时候,SDK工具已经创建了缺省的keystore/key和账号、密码:

1
2
3
4
5
Keystore name – "debug.keystore"
Keystore password – "android"
Key alias"androiddebugkey"
Key password – "android"
CN – "CN=Android Debug,O=Android,C=US"

keystore其实就是一个文件,存放以上信息的文件,由于使用了加密难以看懂。DEBUG模式的签名只有365天有效期,过了有效期,编译会出错。但不用担心,只要将debug.keystore文件删除后,下次BUILD会自动生成的keystore和key的。debug.keystore文件一般在/home/username/.android目录下。

使用java命令行工具

使用Keytool生成key文件

创建key,需要用到keytool(位于JAVA_HOME\jre\bin目录下),在Shell中输入:

1
keytool -genkey -alias android.keystore -keyalg RSA -validity 36500 -keystore android.keystore

命令行参数解释:

1
2
3
4
5
-genkey:产生秘钥
-alias android.keystore 别名 android.keystore
-keyalg RSA 使用RSA算法对签名加密
-validity 36500 有效期限
-keystore android.keystore  存储的文件名

使用 jarsigner签名

jarsigner在目录JAVA_HOME\bin下,在Shell中输入:

1
jarsigner -verbose -keystore android.keystore -signedjar android_signed.apk android.apk android.keystore

命令行参数解释:

1
2
3
-verbose 输出签名的详细信息
-keystore  android.keystore 密钥库位置
-signedjar android_signed.apk android.apk android.keystore 正式签名,三个参数中依次为签名后产生的文件android_signed,要签名的文件android.apk和密钥库android.keystore

zipalign压缩对齐优化APK文件

1
zipalign -v 4 android_signed.apk release.apk

zipalign能够使apk文件中未压缩的数据在4个字节边界上对齐,这样android系统可以使用mmap()函数读取文件,在读取资源上获得较高的性能。

android系统中的Davlik虚拟机使用自己专有的格式DEX,DEX的结构是紧凑的,为了让运行时的性能更好,可以进一步用”对齐”进一步优化,但是大小一般会有所增加。

使用Gradle完成签名

前面已经介绍了DEBUG模式的签名,这里主要讲Release模式:
在Module的build.gradle文件的android配置代码块添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
android{
    signingConfigs {
        debug {
            storeFile file("/home/lippi/.android/debug.keystore")
        }
        relealse {
            //这样写就得把demo.jk文件放在项目目录
            storeFile file("android.keystore")
            storePassword "android"
            keyAlias "lippi"
            keyPassword "password"
        }
    }
   buildTypes {
        debug {
            // 显示Log
            buildConfigField "boolean", "LOG_DEBUG", "true"

            versionNameSuffix "-debug"
            minifyEnabled false
            zipAlignEnabled false
            shrinkResources false
            signingConfig signingConfigs.debug
        }

        release {
            // 不显示Log
            buildConfigField "boolean", "LOG_DEBUG", "false"
            //混淆
            minifyEnabled true
            //Zipalign优化
            zipAlignEnabled true

            // 移除无用的resource文件
            shrinkResources true
            //前一部分代表系统默认的android程序的混淆文件,该文件已经包含了基本的混淆声明
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard.cfg'
            //签名
            signingConfig signingConfigs.relealse
        }
    }
}

执行gradle 命令,

1
$ gradle assembleRelease

编译并发布。 在build/outputs/apk/ 下能看到未签名的apk 和 已经签名的apk。如果未用签名文件,使用debug mode的debug签名,那就会生成一个debug签名的apk。

签名密码放在Gradle文件中不安全

可以改成下面这样的格式这样在执行命令时,就会被要求输入密码

1
2
3
4
5
6
7
8
signingConfigs { 
    myConfig { 
        storeFile file("android.keystore")  
        storePassword System.console().readLine("\ninput Keystore password: ")  
        keyAlias "lippi"  
        keyPassword System.console().readLine("\n input Key password: ")  
    } 
}

使用Android Studio自带的签名工具

菜单Build > Generate Signed APK,具体的就不介绍了,看了前面的部分应该很简单完成。

Android反编译(逆向)教程

在网上搜索Android反编译教程,搜出来的文章要么是抄袭的,要么是太过与简单,经过自己的实践和摸索,我在这里记录下如下反编译一个Apk并修改一些资源文件,比如App名字、启动界面等,然后重新打包成apk,你可以参照这个教程修改游戏里的金币(估计没这么简单)。

工具

还是常用的三个工具:Apktool、dex2jar、jd-gui

Apktool

下载地址:Apktool

Linux下安装教程:

  1. 下载Linux wrapper script (鼠标右键另存为apktool)
  2. 这里下载最新版的apktool
  3. 把下载的Jar文件重命名为apktool.jar
  4. 把两个文件(apktool.jar & apktool)移动到/usr/local/bin (需要root权限),使用chmod +x添加执行权限
  5. 现在可以通过命令行来运行apktool了

使用方法

使用方式:
1.反编译

1
apktool d test.apk

反编译后在home目录下生成了一个文件夹,内容如下:

各个文件简单介绍如下:
apktool.yml: apk属性.
AndroidManifest.xml: 清单文件, 非二进制.
original: 原始数据, 应用的二进制清单和签名.
res: 资源文件.
smali: dex解析的代码.smali是dex虚拟机的解析格式,在 dex2jar中,也包含smali格式的转换.

打开AndroidManifest文件,内容如下:

可以看到app_name和app的图标:

1
android:icon="@drawable/icon_meetyou" android:label="@string/app_name"

接下来我们修改res/values/strings.xml下app_name的内容,并替换app图标
2.重新打包成apk

1
apktool b test

打包后在dist文件夹下生成新的apk文件。

dex2jar

dex2jar是dex转换为jar的工具,很多人不会看smali文件,还是看java类文件比较舒服,这个时候可以借助这个工具来转成java代码。
下载地址:dex2jar

使用

从Apk中解压出class.dex,使用dex2jar进行源码解析,在当前目录生成classes.dex.dex2jar.jar。
转换过程中, 如果发生内存不够, 修改 d2j_invoke.sh 的虚拟机配置.

1
java -Xms512m -Xmx1024m -classpath "${_classpath}" "$@"

接下来使用JD-GUI打开jar包,下载地址:jd-gui
界面如下所示:

其他工具

android killer

集成了上面几个工具,并添加了一些功能
下载地址:androidkiller

ClassyShark

下载地址:github

ClassyShark是一款可以查看Android可执行文件的浏览工具,支持.dex, .aar, .so, .apk, .jar, .class, .xml 等文件格式,分析里面的内容包括classes.dex文件,包、方法数量、类、字符串、使用的NativeLibrary等。

使用方法

1.打开apk文件java -jar ClassyShark.jar -open
2.将生成的所有数据导出到文本文件里java -jar ClassyShark.jar -dump
3.将指定类生成的文件导出到文本文件里java -jar ClassyShark.jar -dump
4.打开ClassyShark,在GUI界面展示某特定的类
5.java -jar ClassyShark.jar -open
6.检测APKjava -jar ClassyShark.jar -inspect
7.导出所有的字符串 java -jar ClassyShark.jar -stringdump

和其他的工具不同,ClassyShark通过分析App的项目结构和引用库的信息,我们大致掌握了该项目的架构,一些开发中的经验和不足,拓宽下开发视野,发现一些好用的开源库,增强我们的武器,这些都是我们在开发中可以借鉴的东西。比如我们查看美团App的结构,看它使用了哪些开源库,界面如下所示:

VOLLEY 超详细源码解析

为什么需要阅读Volley的源码

Volley是Google在2013年推出的一个网络库,用于解决复杂网络环境下网络请求问题。「Google出品,必属精品」,而且Volley被使用在包括「Google Plus」的一系列Google产品中,久经考验。因此我们通过学习Volley的源代码,可以学得很多Android网络处理方面的知识,同时可以看看Google 在设计Volley体系结构的时候,所使用的技巧。

在多如箭雨的情形下,Volley是如何帮你搞定一切的

Volley组件化的设计

设计良好的组件,在实现层面上也一定是组件完备的。通过一些基础组件的拼接,来架构起一些伟大的功能。

对于网络请求而已,相应大多数人,在设计之初开始,会想到外界输入一个网络请求,通过回调的方式给调用相应的反馈就可以了,遵循这样的设计思路下去,在顶向下地设计就会有一个架构设计,但如果出现一些需求的变动,这样的架构能否在较小代价的情况下来满足需求的变动呢?另一方面,如果我们试着自底向上地展开设计了?

开始先想想对于网络库而已,我们需要什么样的组件。「网络」(负责通过URL和参数来从网络中请求数据),「缓存」(将数据缓存下来,并提供接口供外界请求),「请求」(对请求的封装,比如参数,方法,优先级等),「响应」(对结果的封装),「错误」(请求过程中发生的问题)。这些Volley提出的实现是「Network.java」,「Cache.java」,「Request.java」,「Response.java」,「VolleyError.java」,现在有了这些基础组件后,还剩下2个工作:

  1. 为这些定义的组件提供实现。
  2. 尽可能地为这些组件提供一个统一的入口. 「外观模式

在下面的章节里面,来逐个分析每个组件,最后看看Volley是如何把这些组件联系在一起的。

Compoment [NetWork]

public interface Network {
    /**
     * Performs the specified request.
     * @param request Request to process
     * @return A {@link NetworkResponse} with data and caching metadata; will never be null
     * @throws VolleyError on errors
     */
    public NetworkResponse performRequest(Request<?> request) throws VolleyError;
}

这里的结构十分明晰,从接收Request输入到提供NetworkResponse输出,在发生异常的时候,抛出VolleyError。这里是对网络请求进行的封装,这与普通的网络请求不一样,因而Volley提供了另一个组件HttpStack

public interface HttpStack {
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
        throws IOException, AuthFailureError;
}

可以看出来,Network是针对HttpStack进行的包装,Volley实现了2种HttpStack,分别针对2.3以下和4.0以上系统。在2.3系统的时候,还没有UrlConnection, 因此用HttpClient来代替。原因可以参考这里

if (stack == null) {
    if (Build.VERSION.SDK_INT >= 9) {
        stack = new HurlStack();
    } else {
        // Prior to Gingerbread, HttpUrlConnection was unreliable.
        // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
        stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
    }
}

无论是HttpClientStack还是HUrlStack都是对request.getMethod()里面的几乎所有方法,包括PUT,POST,DELETE,GET四种RestFul风格的。这里指出这个,是想提醒调用者除了Get和POST外,还有其他方法可以调用,同时RestFul风格的API正得到广泛认可。Restful 风格把一切都当做资源,提供增,删,改,查四种方式,Volley是对Restful风格进行了良好的支持。调用方可以通过对Volley进行封装可以实现一个RestFul的RPC client。

再来看看Network的实现BasicNetwork,里面有许多值得学习的地方:

  1. 对etag,和 lastModified 的支持。当我们把一个Response缓存下来的时候,服务端可能返回Etag和lastModified,服务端通过这两个值就可以判断这个请求是否可以命中服务端的缓存,从而可以加快返回的速度。
if (entry.etag != null) {
    headers.put("If-None-Match", entry.etag);
}

if (entry.lastModified > 0) {
    Date refTime = new Date(entry.lastModified);
    headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
}
  1. 处理 NOT_MODIFIED 的情况。当服务端返回304的时候,即表示命中了缓存,在这里就不需要再走返回Response的步骤了,直接使用Cache中的数据就可以了。在实现上面,是通过Mock的一个NetworkResponse来实现的。

// Handle cache validation.
if (statusCode == HttpStatus.SC_NOT_MODIFIED) {

    Entry entry = request.getCacheEntry();
    if (entry == null) {
        return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null,
                responseHeaders, true,
                SystemClock.elapsedRealtime() - requestStart);
    }

    // A HTTP 304 response does not have all header fields. We
    // have to use the header fields from the cache entry plus
    // the new ones from the response.
    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
    entry.responseHeaders.putAll(responseHeaders);
    return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data,
            entry.responseHeaders, true,
            SystemClock.elapsedRealtime() - requestStart);
}

  1. 获取 Response Contents 的数据。如果responseContents使用了3态,亦即通过null,空和有数据来表示三种状态,这是一种很有意思的编程技巧。重点在于entityToBytes方法。 这个方法里面使用了一个字节池,来避免我们每次allocate 一个字节数组的开销。
// Some responses such as 204s do not have content.  We must check.
if (httpResponse.getEntity() != null) {
  responseContents = entityToBytes(httpResponse.getEntity());
} else {
  // Add 0 byte response as a way of honestly representing a
  // no-content request.
  responseContents = new byte[0];
}

再来看看ByteArrayPool是如何实现的,其实原理很简单,就是用空间来换取性能,避免OOM。在实现上面是使用了「惰性添加」的方式,最大限度的避免在不调用Volley的时候的开销,但也可以根据实际需求,先new出来一个字节数组。

核心方法是如下2个,分别是 getBufreturnBuf

public synchronized byte[] getBuf(int len) {
	// 遍历已经分配好了的buffer,然后返回这个,并从数组里面溢出点。
    for (int i = 0; i < mBuffersBySize.size(); i++) {
        byte[] buf = mBuffersBySize.get(i);
        if (buf.length >= len) {
            mCurrentSize -= buf.length;
            mBuffersBySize.remove(i);
            mBuffersByLastUse.remove(buf);
            return buf;
        }
    }
    // 否则新建一个
    return new byte[len];
}
public synchronized void returnBuf(byte[] buf) {
    if (buf == null || buf.length > mSizeLimit) {
        return;
    }
    // 持有引用,避免被内存回收
    mBuffersByLastUse.add(buf);
    // 二分搜索
    int pos = Collections.binarySearch(mBuffersBySize, buf, BUF_COMPARATOR);
    if (pos < 0) {
        pos = -pos - 1;
    }
    mBuffersBySize.add(pos, buf);
    mCurrentSize += buf.length;
    // 遍历一下,去掉超出maxSize后最近使用的一个字节数组
    trim();
}

Compoment [Cache]

现在看看Volley是如何实现Cache的,后面再接着分析其他几个组件。Cache里面的核心组件是Entry,这个类封装了Cache的一些细节知识,简单看看。

public static class Entry {
    /** The data returned from cache. */
    public byte[] data;

    /** ETag for cache coherency. */
    // etag 和 lastModified 用来向服务器端请求时带上,便于服务器看是否有缓存。
    public String etag;

    /** Date of this response as reported by the server. */
    // 记录这些数据是什么时候从服务器端返回的,从而可以有相对比较复杂的策略。
    public long serverDate;

    /** The last modified date for the requested object. */
    public long lastModified;

    /** TTL for this record. */
    // Time to Leave 客户端可以通过这个数据来判断当前内容是否过期。
    public long ttl;

    /** Soft TTL for this record. */
    // 在这个时间内,是否需要刷新
    public long softTtl;

    /** Immutable response headers as received from server; must be non-null. */
    public Map<String, String> responseHeaders = Collections.emptyMap();

    /** True if the entry is expired. */
    public boolean isExpired() {
        return this.ttl < System.currentTimeMillis();
    }

    /** True if a refresh is needed from the original data source. */
    public boolean refreshNeeded() {
        return this.softTtl < System.currentTimeMillis();
    }
}

在Cache提供的接口里面,除了put和get方法外,还有一个方法是initialize 方法,这个在CacheDispatcher启动的时候,会去执行。这里相当于给各种Cache策略,提供了一种在初始化的时候的Hook,通过这个Hook可以实现复杂的策略。

Volley提供了2种cache方式,一是NoCache,而是DiskBasedCache,NoCache比较简单,重点看看DiskBasedCache。 DiskBasedCache 提供了 get 和 remove 方法,便于写入和读取。文件级别的Cache默认最大大小为5M,在初始化的时候,会把内存中的Cache读入到内存中,通过LinkedHashMap来实现。

/** Magic number for current version of cache file format. */
// 这里有一个魔法数,当在读取值的时候,会去判断是否是与这个魔法值相同,以判断是否为Cache.
private static final int CACHE_MAGIC = 0x20150306;

Compoment [Request]

Request 相当于整个Volley系统的输入,通过Request,建立起与内部系统沟通的桥梁,因此Request的设计至关重要。我们来分析下,Request是如何与内部系统建立沟通的。

  1. 如何让Volley知道,我需要什么样的数据。
@Override
abstract protected Response<T> parseNetworkResponse(NetworkResponse response);

首先Request是支持泛型的,通过这个泛型来定义Request需要什么样的数据,同时volley 提供了这样的方法,来指定调用方如何通过response 来变成自己想要的数据。

  1. 如何让系统知道我需要优先执行我的任务。Volley通过 Priority 这个来判断优先级,在实际执行里面,是通过 PriorityBlockingQueue 来实现优先级高的队列来执行。
public enum Priority {
    LOW,
    NORMAL,
    HIGH,
    // 目前用于清除Cache
    IMMEDIATE
}
  1. 如何让 Volley 知道我要怎么去执行任务

这个是最根本的需求,调用方可以通过url,method等内容与Volley进行交互,传递参数通过getParams()来实现的。这样实现的一个好处在于可以把一些校验逻辑放在子类里面,子类里面可以校验参数是否合法,如果不合法,则抛出AuthFailureError异常。

/**
 * Returns a Map of parameters to be used for a POST or PUT request.  Can throw
 * {@link AuthFailureError} as authentication may be required to provide these values.
 *
 * <p>Note that you can directly override {@link #getBody()} for custom data.</p>
 *
 * @throws AuthFailureError in the event of auth failure
 */
protected Map<String, String> getParams() throws AuthFailureError {
    return null;
}
  1. 如何让 Volley 知道我想进行一些操作

目前volley仅支持 Cancel 操作,当用户想取消某个 request 的时候,实际上是设置了一个标志位,Volley 通过这个标志位来进行判断,以决定后续的操作。

Compoment [Request 和 Error]

Response 和 Error 就是Volley 对调用方的输出,在经历对调用方屏蔽内部细节过后,将Response 告诉调用者。Error 就是对 Exception 的简单封装,这里也就不细细描述了。

/** Callback interface for delivering parsed responses. */
public interface Listener<T> {
    /** Called when a response is received. */
    public void onResponse(T response);
}

/** Callback interface for delivering error responses. */
public interface ErrorListener {
    /**
     * Callback method that an error has been occurred with the
     * provided error code and optional user-readable message.
     */
    public void onErrorResponse(VolleyError error);
}

Volley 是如何整理各个组件的

Volley 的体型结构

Volley 体系结构

在Volley 的结构里面,组件都已经定义完毕了,剩下的工作就是如何把这些组件以合适的方式组合起来,供调用者使用。

RequestQueue的外观模式

RequestQueue 封装了 Request 队列的一系列操作,理论上用户知道RequestQueue就足够了,通过这个队列来进行任务的添加和取消,当Request 结束的时候,给调用者相应的回调即可。

public void start() {
 	   // 这里无需去管是否马上stop成功,因为线程引用已经被修改了
        stop();  // Make sure any currently running dispatchers are stopped.
        // Create the cache dispatcher and start it.
        mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
        // 启动Cache dispatcher
        mCacheDispatcher.start();
        // Create network dispatchers (and corresponding threads) up to the pool size.
        // 启动network dispatcher
	    for (int i = 0; i < mDispatchers.length; i++) {
        NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                mCache, mDelivery);
        mDispatchers[i] = networkDispatcher;
        networkDispatcher.start();
    }
}

CacheDispatcher & NetworkDispatcher 的操作

仔细想想,Volley 其实是一个生产者和消费者系统,调用方是生产者,而Volley是消费者。调用方通过RequestQueue 生产Request,而Vollery 消费Request 从而得到Response。那么负责调配这些生产者和消费者的就是Dispatcher,分别是Cache 和 Network 的Dispatcher。

Dispatcher 在实现上,其实比较简单。首先Dispatcher是Thread,线程的Run方法里面,是一个While循环,Run方法在开始的时候,会去读取Request,读取不到会一直Block在哪里;在读取完成后,就开始走相应的逻辑,比如写入缓存或者从网络中读取数据。

@Override
public void run() {
    if (DEBUG) VolleyLog.v("start new dispatcher");
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

    // Make a blocking call to initialize the cache.
    mCache.initialize();

    while (true) {
        try {
            // Get a request from the cache triage queue, blocking until
            // at least one is available.
            final Request<?> request = mCacheQueue.take();
            request.addMarker("cache-queue-take");

            // If the request has been canceled, don't bother dispatching it.
            if (request.isCanceled()) {
                request.finish("cache-discard-canceled");
                continue;
            }

            // Attempt to retrieve this item from cache.
            Cache.Entry entry = mCache.get(request.getCacheKey());
            if (entry == null) {
                request.addMarker("cache-miss");
                // Cache miss; send off to the network dispatcher.
                mNetworkQueue.put(request);
                continue;
            }

            // If it is completely expired, just send it to the network.
            if (entry.isExpired()) {
                request.addMarker("cache-hit-expired");
                request.setCacheEntry(entry);
                mNetworkQueue.put(request);
                continue;
            }

            // We have a cache hit; parse its data for delivery back to the request.
            request.addMarker("cache-hit");
            Response<?> response = request.parseNetworkResponse(
                    new NetworkResponse(entry.data, entry.responseHeaders));
            request.addMarker("cache-hit-parsed");

            if (!entry.refreshNeeded()) {
                // Completely unexpired cache hit. Just deliver the response.
                mDelivery.postResponse(request, response);
            } else {
                // Soft-expired cache hit. We can deliver the cached response,
                // but we need to also send the request to the network for
                // refreshing.
                request.addMarker("cache-hit-refresh-needed");
                request.setCacheEntry(entry);

                // Mark the response as intermediate.
                response.intermediate = true;

                // Post the intermediate response back to the user and have
                // the delivery then forward the request along to the network.
                mDelivery.postResponse(request, response, new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mNetworkQueue.put(request);
                        } catch (InterruptedException e) {
                            // Not much we can do about this.
                        }
                    }
                });
            }

        } catch (InterruptedException e) {
            // We may have been interrupted because it was time to quit.
            if (mQuit) {
                return;
            }
            continue;
        }
    }
}

EventBus 终极源码解析

EventBus 需要解决的问题

在日常编码里面,我们会遇到很多网络请求,数据库操作等等,一般情况下这些操作都是通过观察者模式来实现的。通过Volley来简单举个例子:

ImageRequest request = new ImageRequest(url,
    new Response.Listener<Bitmap>() {
        @Override
        public void onResponse(Bitmap bitmap) {
            mImageView.setImageBitmap(bitmap);
        }
    }, 0, 0, null,
    new Response.ErrorListener() {
        public void onErrorResponse(VolleyError error) {
            mImageView.setImageResource(R.drawable.image_load_error);
        }
    });

此时,你会发现并且开始思考一个问题,如果很多观察者模式需要使用了? 比如,你正在开发一个东西,需要监听网络状态变化,App的安装情况,内容的下载情况。当存在很多观察者模式,「如何将这些事件通知到监听者」是可以复用的模块,这就是EventBus存在的意义。这里需要大家想明白一个问题,观察者模式本身就是一个可以复用的模块。

  • 内容下载模块
  • 电量监听模块
  • App按照通知

他们都可以通过EventBus将自身的事件发布出去,使用者只需要在这个模块里面,注册对于自己感兴趣的内容就行。

EventBus 发布和接收模块

EventBus 带来的好处和引入的问题

好处比较明显,就是独立出一个发布订阅模块,调用者可以通过使用这个模块,屏蔽一些线程切换问题,简单地实现发布订阅功能。

坏处可能比较隐晦,但这些需要足够引起我们的重视

  • 大量的滥用,将导致逻辑的分散,出现问题后很难定位。
  • 没办法实现强类型,在编译的时候就发现问题,(Otto实现了这个,但性能有问题)。在实现上通过一个很弱的协议,比如onEvent{XXX}, {XXX}表示ThreadModel,来实现线程的切换。后面在代码解析的时候,会说明这个问题。
  • 代码可读性有些问题,IDE无法识别这些协议,对IDE不友好。

总得来说,如果项目里面有大量的事件交互,那么还是可以通过EventBus来实现,否则还是推荐自己在模块内部实现观察者模式

EventBus 源码解析

EventBus.java

源码阅读从外观类开始,这里是 EventBus.java,核心接口都在这个类里面实现,对内容感兴趣的调用方使用 register 方法,当有事件产生的时候,会在onEvent的时候收到相应的回调。

register(Object object);

registerSticky(Object object);

unRegister(Object object);

post(Object object);

先看看初始化部分,看看如何实现单例的(可选的)。

// volatile 这里是需要重视的,这个关键字保证了defaultInstance在不同线程间的可见性,也就是说在多线程环境下,看到的仍然是最新修改的值。
static volatile EventBus defaultInstance;
/** Convenience singleton for apps using a process-wide EventBus instance. */
public static EventBus getDefault() {
	// 这一步不存在线程问题,volatile保证了。如果没有defaultInstance实例化出来,
    if (defaultInstance == null) {
        synchronized (EventBus.class) {
        	// 进入同步块的时候,不能保证defaultInstance没有被实例化出来,所以需要进行double-check
            if (defaultInstance == null) {
                defaultInstance = new EventBus();
            }
        }
    }
    return defaultInstance;
}

// 这里实现的时候,考虑的是defaultInstance 不一定是每个人都需要创建的,否则没必要使用lazy的实现方式
// 下面是一种实现方式
static {
	defaultInstance = new EventBus();
}

EventBus实现了EventBusBuilder,通过Builder的方式使得构建的时候更加容易

public static EventBusBuilder builder() {
    return new EventBusBuilder();
}

下面重点看看register(Object subscriber, boolean sticky, int priority)方法

private synchronized void register(Object subscriber, boolean sticky, int priority) {
	// 用 subscriberMethodFinder 提供的方法,找到在 subscriber 这个类里面,订阅的内容。
    List<SubscriberMethod> subscriberMethods
        = subscriberMethodFinder.findSubscriberMethods(subscriber.getClass());
    for (SubscriberMethod subscriberMethod : subscriberMethods) {
    	// 遍历这些方法,subscribe 这些事件
        subscribe(subscriber, subscriberMethod, sticky, priority);
    }
}

findSubscriberMethods 这个方法是实现 EventBus 的核心代码,这里面包含了 EventBus 隐式定义的交互协议。从这个方法里面,可以看到如何争取地使用EventBus。

List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
    String key = subscriberClass.getName();
    List<SubscriberMethod> subscriberMethods;
    // 如果这个 Class 对应的方法被缓存,直接返回。
    synchronized (methodCache) {
        subscriberMethods = methodCache.get(key);
    }
    // 这个方法其实可以放在 前面的 synchronized 模块里面
    if (subscriberMethods != null) {
        return subscriberMethods;
    }
    subscriberMethods = new ArrayList<SubscriberMethod>();
    Class<?> clazz = subscriberClass;
    HashSet<String> eventTypesFound = new HashSet<String>();
    StringBuilder methodKeyBuilder = new StringBuilder();
    while (clazz != null) {
        String name = clazz.getName();
        // 跳过JDK里面的类
        if (name.startsWith("java.") || name.startsWith("javax.") || name.startsWith("android.")) {
            // Skip system classes, this just degrades performance
            break;
        }

        // Starting with EventBus 2.2 we enforced methods to be public (might change with annotations again)
        // 获取所有声明的方法
        Method[] methods = clazz.getDeclaredMethods();
        for (Method method : methods) {
            String methodName = method.getName();
            if (methodName.startsWith(ON_EVENT_METHOD_NAME)) {
                int modifiers = method.getModifiers();
                if ((modifiers & Modifier.PUBLIC) != 0 && (modifiers & MODIFIERS_IGNORE) == 0) {
                	// 方法是 Public 的
                    Class<?>[] parameterTypes = method.getParameterTypes();
                    if (parameterTypes.length == 1) {
                        String modifierString = methodName.substring(ON_EVENT_METHOD_NAME.length());
                        ThreadMode threadMode;

                        // 方法的前缀是否是 ‘OnEvent’, 如果是‘OnEvent’,查看后面的字符串,这里定义了 4 种基本类型
                        // ThreadModel 会在后面介绍
                        if (modifierString.length() == 0) {
                            threadMode = ThreadMode.PostThread;
                        } else if (modifierString.equals("MainThread")) {
                            threadMode = ThreadMode.MainThread;
                        } else if (modifierString.equals("BackgroundThread")) {
                            threadMode = ThreadMode.BackgroundThread;
                        } else if (modifierString.equals("Async")) {
                            threadMode = ThreadMode.Async;
                        } else {
                            if (skipMethodVerificationForClasses.containsKey(clazz)) {
                                continue;
                            } else {
                                throw new EventBusException("Illegal onEvent method, check for typos: " + method);
                            }
                        }

                        // 获取参数类型
                        Class<?> eventType = parameterTypes[0];
                        methodKeyBuilder.setLength(0);
                        methodKeyBuilder.append(methodName);
                        methodKeyBuilder.append('>').append(eventType.getName());
                        // 得到类似于一个句柄的东西,比如 onEventMainThread>DownloadInfo
                        String methodKey = methodKeyBuilder.toString();
                        if (eventTypesFound.add(methodKey)) {
                            // Only add if not already found in a sub class
                            subscriberMethods.add(new SubscriberMethod(method, threadMode, eventType));
                        }
                    }
                } else if (!skipMethodVerificationForClasses.containsKey(clazz)) {
                    Log.d(EventBus.TAG, "Skipping method (not public, static or abstract): " + clazz + "."
                            + methodName);
                }
            }
        }
        // 这里要为 EventBus 点个赞了,EventBus 是支持继承的
        clazz = clazz.getSuperclass();
    }
    if (subscriberMethods.isEmpty()) {
        throw new EventBusException("Subscriber " + subscriberClass + " has no public methods called "
                + ON_EVENT_METHOD_NAME);
    } else {
        synchronized (methodCache) {
            methodCache.put(key, subscriberMethods);
        }
        return subscriberMethods;
    }
}

现在看下如何把 subscriberClass 里面的内容订阅到 EventBus 里面去。

// Must be called in synchronized block
private void subscribe(Object subscriber, SubscriberMethod subscriberMethod, boolean sticky, int priority) {
    Class<?> eventType = subscriberMethod.eventType;
    // 获取订阅了某种类型数据的 Subscription 。 使用了 CopyOnWriteArrayList ,这个是线程安全的,
    // CopyOnWriteArrayList 会在更新的时候,重新生成一份 copy,其他线程使用的是 
    // copy,不存在什么线程安全性的问题。
    CopyOnWriteArrayList<Subscription> subscriptions = subscriptionsByEventType.get(eventType);
    Subscription newSubscription = new Subscription(subscriber, subscriberMethod, priority);
    if (subscriptions == null) {
        subscriptions = new CopyOnWriteArrayList<Subscription>();
        subscriptionsByEventType.put(eventType, subscriptions);
    } else {
        if (subscriptions.contains(newSubscription)) {
            throw new EventBusException("Subscriber " + subscriber.getClass() + " already registered to event "
                    + eventType);
        }
    }

    // Starting with EventBus 2.2 we enforced methods to be public (might change with annotations again)
    // subscriberMethod.method.setAccessible(true);

    int size = subscriptions.size();
    for (int i = 0; i <= size; i++) {
        // 根据优先级进行插入,其实这里可以替换为优先级队列的
        if (i == size || newSubscription.priority > subscriptions.get(i).priority) {
            subscriptions.add(i, newSubscription);
            break;
        }
    }

    List<Class<?>> subscribedEvents = typesBySubscriber.get(subscriber);
    if (subscribedEvents == null) {
        subscribedEvents = new ArrayList<Class<?>>();
        typesBySubscriber.put(subscriber, subscribedEvents);
    }
    subscribedEvents.add(eventType);

    if (sticky) {
        // 是否支持继承,这个可以在 Builder 的时候指定,如果不支持,那么可能有20%以上的性能提升
        if (eventInheritance) {
            // Existing sticky events of all subclasses of eventType have to be considered.
            // Note: Iterating over all events may be inefficient with lots of sticky events,
            // thus data structure should be changed to allow a more efficient lookup
            // (e.g. an additional map storing sub classes of super classes: Class -> List<Class>).
            Set<Map.Entry<Class<?>, Object>> entries = stickyEvents.entrySet();
            for (Map.Entry<Class<?>, Object> entry : entries) {
                Class<?> candidateEventType = entry.getKey();
                if (eventType.isAssignableFrom(candidateEventType)) {
                    Object stickyEvent = entry.getValue();
                    checkPostStickyEventToSubscription(newSubscription, stickyEvent);
                }
            }
        } else {
            // 检查是否有 sticky 的event, 如果存在就发布出去
            Object stickyEvent = stickyEvents.get(eventType);
            checkPostStickyEventToSubscription(newSubscription, stickyEvent);
        }
    }
}
// 取消订阅的代码
public synchronized void unregister(Object subscriber) {
    List<Class<?>> subscribedTypes = typesBySubscriber.get(subscriber);
    if (subscribedTypes != null) {
        for (Class<?> eventType : subscribedTypes) {
            unubscribeByEventType(subscriber, eventType);
        }
        typesBySubscriber.remove(subscriber);
    } else {
        Log.w(TAG, "Subscriber to unregister was not registered before: " + subscriber.getClass());
    }
}
// 重点看看发布的方法
private void postSingleEvent(Object event, PostingThreadState postingState) throws Error {
    Class<?> eventClass = event.getClass();
    boolean subscriptionFound = false;
    if (eventInheritance) {
        List<Class<?>> eventTypes = lookupAllEventTypes(eventClass);
        int countTypes = eventTypes.size();
        for (int h = 0; h < countTypes; h++) {
            Class<?> clazz = eventTypes.get(h);
            subscriptionFound |= postSingleEventForEventType(event, postingState, clazz);
        }
    } else {
        subscriptionFound = postSingleEventForEventType(event, postingState, eventClass);
    }
    if (!subscriptionFound) {
        if (logNoSubscriberMessages) {
            Log.d(TAG, "No subscribers registered for event " + eventClass);
        }
        if (sendNoSubscriberEvent && eventClass != NoSubscriberEvent.class &&
                eventClass != SubscriberExceptionEvent.class) {
            post(new NoSubscriberEvent(this, event));
        }
    }
}

EventBus ThreadModel

EventBus 一共提供了 4 种 ThreadModel, 分别是 PostThread, MainThread, BackgroundThread, Async。

  • PostThread 默认实现,执行发生在同一个线程
  • MainThread 执行在UI 线程上
  • BackgroundThread 回调发生在非 UI 线程上
  • Async 永远执行在一个其他的线程上

以上这四种类型,足以支持观察者模式里面需要进行的异步处理。

EventBus 如何实现线程转换的

但凡经历一些实际项目,就会发现,经常存在「生产」和「消费」冲突的情况,这里就需要使用「生产者与消费者」模式。

EventBus 中 生产者和消费者模式的实现主要是在 PendingPostQueue里面。

PendingPostQueue 的实现比较简单,主要是通过在 enqueue 和 poll 的时候进行 synchronized 同步来实现的。

synchronized void enqueue(PendingPost pendingPost) {
    if (pendingPost == null) {
        throw new NullPointerException("null cannot be enqueued");
    }
    // 将 Post 插入到队列尾部
    if (tail != null) {
        tail.next = pendingPost;
        tail = pendingPost;
    } else if (head == null) {
        // 在最开始的时候,建立头部和尾部的索引
        head = tail = pendingPost;
    } else {
        throw new IllegalStateException("Head present, but no tail");
    }
    notifyAll();
}

synchronized PendingPost poll() {
    PendingPost pendingPost = head;
    // 从头部获取
    if (head != null) {
        head = head.next;
        if (head == null) {
            tail = null;
        }
    }
    return pendingPost;
}
// 这里需要注意的地方是 PendingPost, 这里维护了一个 pendingPostPool 的池子, 当PendingPost 不再需要的时候,就释放回池子里面去,避免了新建对象的开销。
static void releasePendingPost(PendingPost pendingPost) {
    pendingPost.event = null;
    pendingPost.subscription = null;
    pendingPost.next = null;
    synchronized (pendingPostPool) {
        // Don't let the pool grow indefinitely
        if (pendingPostPool.size() < 10000) {
            pendingPostPool.add(pendingPost);
        }
    }
}

EventBus 如何发布时间的

// 每个ThreadModel (除了PostThread) 都维护了一个 Poster, 这个Post 里面维持了一个 ``生产者消费者模式``, 来消费和使用事件。
private void postToSubscription(Subscription subscription, Object event, boolean isMainThread) {
    switch (subscription.subscriberMethod.threadMode) {
        case PostThread:
            invokeSubscriber(subscription, event);
            break;
        case MainThread:
            // 主线程的poster
            if (isMainThread) {
                invokeSubscriber(subscription, event);
            } else {
                mainThreadPoster.enqueue(subscription, event);
            }
            break;
        case BackgroundThread:
            if (isMainThread) {
                backgroundPoster.enqueue(subscription, event);
            } else {
                invokeSubscriber(subscription, event);
            }
            break;
        case Async:
            asyncPoster.enqueue(subscription, event);
            break;
        default:
            throw new IllegalStateException("Unknown thread mode: " + subscription.subscriberMethod.threadMode);
    }
}

Android Annotation 实战

什么是注解

Java 注解又成为 Java 标注, 是 Java 语言在5.0版本以上开始支持加入源代码的特殊语法元数据。

我们常见的 @Override, @Despited 等这些都是注解,注解是一种可以在 Runtime, Source, Class 三个级别上来提供给我们进行语义表达的特殊数据格式。

JDK 定义了四中基础的注解元数据,分别是 @Documented, @Retention, @Target, @Inherited。

  1. @Documented 用于描述其它类型的annotation应该被作为被标注的程序成员的公共API
  2. @Retention 定义了该Annotation被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的 Annotation 可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。
  3. @Inherited 是否可以继承
  4. @Target Annotation的作用域
public enum ElementType {
    /**
     * Class, interface or enum declaration.
     */
    TYPE,
    /**
     * Field declaration.
     */
    FIELD,
    /**
     * Method declaration.
     */
    METHOD,
    /**
     * Parameter declaration.
     */
    PARAMETER,
    /**
     * Constructor declaration.
     */
    CONSTRUCTOR,
    /**
     * Local variable declaration.
     */
    LOCAL_VARIABLE,
    /**
     * Annotation type declaration.
     */
    ANNOTATION_TYPE,
    /**
     * Package declaration.
     */
    PACKAGE
}
@Documented // 在文档中可描述
@Retention(RetentionPolicy.RUNTIME) // 在运行时保留
@Target(ElementType.ANNOTATION_TYPE) // 作用在ANNOTATION上面
public @interface Target {
    ElementType[] value();
}

如何用注解来构建强大的功能了?

原来我们写的代码作用域都在 「Java Source Code」中,而如今 注解提供了一个机会,使得我们用更优雅,语义更明显的方式来完成更高层面的复用。举个例子,在进行网络请求的时候,我们需要指定网络获取的方法(GET / POST / DELETE …),以及相应的URL和参数。

public Data request(Method method, String url) {
    // handle request
}
@Method(HttpRequest.GET)
@URL("http://www.baidu.com")
public Data request(){

}

从这个例子里面可以看出,注解在一定程度上争强了扩展性和灵活性。

Android Support Animation 的小例子

Android 里面的Resource Id都是int, 如果我们想要限制 Res 为Drawable

public displayImage(int drawableRes) {
    // 显示图片
    // 如何确认 drawableRes 为drawable 的id?
}

@Documented
@Retention(SOURCE)
@Target({METHOD, PARAMETER, FIELD})
public @interface DrawableRes {
}

public displayImage(@DrawableRes int drawableRes) {
    // @DrawableRes 可以用来表示是资源图片, 这个注解会由IDE进行检查
    // 当 drawableRes 不为drawable的时候,会提出相应的 warning
}

动手开始实现一个Annotation

需求描述

public interface GitHubService {
  @GET("/users/{user}/repos")
  List<Repo> listRepos(@Path("user") String user);
}

这是从RetroFit里面截取的一段代码,里面涉及到两个 Annotation, 一个是 @PATH, 一个是 @GET, 分别用来标记 使用 GET 方法来获取内容, 另一方面是 通过 PATH 来指定 URL 里面的参数, 实现动态可替换。

GET 注解需要在 运行时 也可以获取,这样我们才能在运行时,拿去 URL 进行相应的操作,因而需要指定 Retention 为 RUNTIME, 另一方面,GET 只能用于修饰某个方法,因此 Target 需要为 METHOD。 那么我们如何对这个注解赋值呢? 需要告诉 GET 注解需要的URL, 注解是可以定义方法的, 因而我们这里实现了 value() 方法, 默认值为空.

/** Make a GET request to a REST path relative to base URL. */
@Documented // 文档可描述
@Target(METHOD)
@Retention(RUNTIME)
public @interface GET {
  String value() default "";
}

同理,PATH 注解的定义如下

@Documented
@Retention(RUNTIME)
@Target(PARAMETER)
public @interface Path {
  String value();

  /**
   * Specifies whether the argument value to the annotated method parameter is already URL encoded.
   */
  boolean encoded() default false;
}

完成这个两个注解后,我们来看看将这两个注解用起来。java 反射包里面实现了大部分的功能,通过这个包里面提供方法,可以达到我们的目的。

for (Annotation annotation : method.getAnnotations()) {
    // 通过 method.getAnnotations() 可以获得修饰方法的注解
    if (annotaiton instance GET) {
        // 处理注解是GET方法的情况
        String url = ((GET) annotation).value();

    }
}

if (methodParameterAnnotation instanceof Path) {
    addPathParam(((PATH) annotation).value(), argu);
}

void addPathParam(String name, String value, boolean encoded) {
    if (relativeUrl == null) {
      // The relative URL is cleared when the first query parameter is set.
      throw new AssertionError();
    }
    try {
      if (!encoded) {
        String encodedValue = URLEncoder.encode(String.valueOf(value), "UTF-8");
        // URLEncoder encodes for use as a query parameter. Path encoding uses %20 to
        // encode spaces rather than +. Query encoding difference specified in HTML spec.
        // Any remaining plus signs represent spaces as already URLEncoded.
        encodedValue = encodedValue.replace("+", "%20");
        relativeUrl = relativeUrl.replace("{" + name + "}", encodedValue);
      } else {
        relativeUrl = relativeUrl.replace("{" + name + "}", String.valueOf(value));
      }
    } catch (UnsupportedEncodingException e) {
      throw new RuntimeException(
          "Unable to convert path parameter \"" + name + "\" value to UTF-8:" + value, e);
    }
}

从实际例子出发讲解面向组合子编程

前言

一直以来想要表述下,在这几年的时间里面对 OO(面向对象) 和 CO(面向组合子编程) 的理解,终于在闲暇时间里,可以好好阐述下这个思想。

关于 OO(面向对象) 和 CO(面向组合子编程) 的理解,有一个很有意思的描述:

OO:白马是白色的马。 CO:白马非马,是由「白」和「马」组成的。

这个理解在一定程度上是正确的,但是更像是面向组件编程,而离「面向组合子编程」还是有一定的差距。面向组件更像是对「组合与继承」的一种反思,组合子编程则更类似于 「编写一门领域语言」。在接下来的章节里面,我们将为什么要「善用组合」,然后如何将思维进阶到「组合子编程」里面来。

善用组合

我们从实际的例子出发,来说明下组合的好处。设想我们需要开发一个 Android App 用来实现一个绘制图形的 App .刚开始 PM 提出了需求,这个 App 需要支持绘制红色的线条,和蓝色的线条,接到这个需求后,作为学过 OO 的小明,当即做出了用继承来实现这个需求的架构,简单的代码实现如下:


public class Line {
	public abstract void draw() {
		// draw line.
	}
}

public class RedLine extends Line {
	public void draw() {
		// draw line.
		// print red color.
	}
}

public class BuleLine extends Line {
	public void draw() {
		// draw line.
		// print blue color.
	}
}

这个 App 成功地上线后,用户广泛地好评,PM 的野心又出现了,既然可以画线,那么也可以画方块。 PM 又提出了一个需求, 需要实现对绘制红色方块和蓝色方块的需求。小明想了一下后,在原有的基础上再继承一下,就可以了吧。于是 AppV2 的代码就诞生了:


public class Rect {
	public abstract void draw() {
		// draw Rect.
	}
}

public class RedRect extends Rect {
	public void draw() {
		// draw Rect.
		// print red color.
	}
}

public class BuleRect extends Rect {
	public void draw() {
		// draw Rect.
		// print blue color.
	}
}

这样一共有6个类文件了,小明心想这PM该满足了吧。 在App上线一段时间后,用户觉得体验太好了,又提出了新的反馈,需要加入其他画笔。这个时候 PM 又提出了其他的需求了, 比如绿色的圆,黄色的五角星等等。 小明这下爆炸了, 原有的实现里面完全没法满足后续的需求了,整个项目充斥着类的爆炸,复用程度低下。那么如何来解决这些问题了?答案就是「善用组合」。


public interface Drawable {
	void draw();
}

public class Shape implements Drawable {
	public Shape(String type) {

	}

	public void draw() {
		// draw according to type.
	}
}

public class Color implements Drawable {
	public Color(int rgba) {

	}

	public void draw() {
		// draw color by rgba.
	}
}

public class Paint {
	public static void drawRedRect() {
		Shape rect = new Shape(ShapeType.Rect);
		Color red = new Color("#FF231424");
		canvas.draw(rect, red);
	}
}

这样实现后,复用性得以提升,逻辑也更加清晰,在下面的章节里面,我们再来思考如何用组合子的方式来考虑这个问题。

组合子的思考方式

再次回到这个例子里面,我们再次回到这个需求里面来,需要实现的是一系列的绘图操作。这个 App 里面,我们可以简单地分为几个模块,Color, Shape, Operation; 这几个模块通过组合的方式,就可以用复用度良好的方式来完成整个 App 了。按理说,如果我们采用组合的方式已经能够实现很好的鲁棒性和复用性了,那么还欠缺一些什么了?欠缺一些更加高阶的抽象!

我们的思路往往都是从自顶向下地来进行,先有了要做什么事情,然后再把事情做拆分,最后拆分成可执行的颗粒度足够细化的模块。这种思考方式在 OO 时代就被广泛采用,做出的项目设计方案也往往能够拥有不错的复用性和扩展性。而组合子编程思考的角度是自底向上的,这样考虑的方式会给我们另一种途径来解读项目设计。(PS 自顶向下是一种很好的方式,应该推崇)

当使用组合子编程的时候,我们考虑更多的是从底部出发,水到渠成地满足了需求,当需求变化的时候,也能很快地得以支持。回到前面这个 绘图 App 的例子里面来, 对于绘图这件事情,我们是有什么元操作呢? 我们需要如何简单地构建基础工具了,如何把设计好的组件的方式用一致的方式拼接起来?这里会有一些 DSL 的意思在里面。

我们来看看如何定义最基础的逻辑,一定是一个最基础的抽象,设想就是一个 Drawable 抽象。

public interface Drawable {
	public void draw(Canvas canvs) {
	}
}

那么我们现在是不是可以写 Pen,Color 这些抽象了呢?其实不用急,我们得让一起可以运作起来。我们来构建一个序列化执行的Drawable, 有了这个SerialDrawable,我们就可以支持起顺序架构这个关键功能了。比如我们可以实现前面提及的 RedRect, BlueLine 等等。

public class SerialDrawable implements Drawable {

	private List<Drawable> drawbles = new ArrayList<>();

	public void draw(Canvas canvas) {
		for (Drawable drawable : drawables) {
			drawable.draw(canvas);
		}
	}

	public void appendDrawable(Drawable drawable) {
		drawables.add(drawable);
	}
}

另外同样我们可以实现 NonDrawable 来表示空, FilterDrawable 来便是可过滤的Drawable. 通过这些原子的数据,我们就可以架构出整个世界了。(PS to be continued …)

更多内容请参考 ajooo-名著类

understand viewpager and pageradapter

ViewPager 作为展示一组页面的容器,在Android上被广泛使用,这边文章将围绕 ViewPager 如何显示页面展开,接口如何设计展开。

PagerAdapter 的接口设计

ViewPager 是与一组页面进行交互的容器,那么怎么设计交互的接口就成为设计成败的关键。我们会发现 ListView 中使用的「通信接口」是 BaseAdapter, 那么类似地,ViewPager 在设计的时候, 同样采用了 Adapter 的设计模式, 通过 PagerAdapter 来实现交互。

我们要达成的协议应该如下,ViewPager 负责显示页面,刷新页面,处理滑动等逻辑,而 PagerAdapter 负责实现如何渲染界面等具体接口。ViewPager 不直接操作页面,把这一切逻辑都放在 PagerAdapter 里面去,甚至页面复用这些逻辑也交由 PageAdapter 处理。那么我们来看看 PagerAdapter 是如何定义的?

PagerAdapter 提供了4种最基础的方法需要实现。

public Object instantiateItem(ViewGroup container, int position) {
    return instantiateItem((View) container, position);
}

public void destroyItem(ViewGroup container, int position, Object object) {
    destroyItem((View) container, position, object);
}

public abstract int getCount();

public abstract boolean isViewFromObject(View view, Object object);

首先是 instantiateItem 方法,这个方法在指定的位置,和容器上面实例化 Page, 需要注意的是这些操作必须在 {@link #finishUpdate(ViewGroup)} 之前完成(这个会在后面解释)。 ViewPager 会在合适的时机调用这个方法来显示页面。

DestroyItem 这个方法与 instantiateItem 类似,用于销毁页面,在实现的时候,我们可以在这个时间来做一些缓存或者回收等一些事情。同样这些事情也必须在{@link #finishUpdate(ViewGroup)} 保证执行结束。

getCount() 返回相应的数目

isViewFromObject(View view, Object object) 这个方法可以仔细说一下。首先看看instantiateItem 的返回值,这里是 Object,读者可能有疑问了,为什么不是 Fragment 了? 虽然在大多数 Android 程序中,ViewPager 都是用来显示一系列的 Fragment ,但在设计的时候,我们就不能这么闭塞地思考问题。根据开闭原则,我们对扩展是开放的,因而我们除了可以显示一系列 Fragment 以外,还可以显示 View, 或者其他别的什么,所以这么返回值限定为 Object。 返回值和 View 并没有什么关系,ViewPager 只是用这个来标记这个 item 的,也就是建立起 item <–> object 之间的映射。在这样的情况下,能够做更多事情。

例如我们要实现一个显示水果的 ViewPager, 分别是 apple / banner / pear / peach / watermelon。起初第一个版本,我们使用 Fragment 来显示这些水果。

public enum Fruit {
	Apple, Banner, Pear, Peach, Watermelon
}
public Object instantiateItem(ViewGroup container, int position) {
	// new fragment
	FruitFragment fragment = new FruitFragment();
	fragment.setArgument(fruits.getItem(position));

	fragmentTransaction.add(fragment);
	// return result.
	return fragment.
}

public boolean isViewFromObject(View view, Object object) {
	return ((Fragment)object).getView() == view;
}

同样我们也可以通过显示 View 来替代 Fragment 的实现。

public Object instantiateItem(ViewGroup container, int position) {
	// new fragment
	View view = ViewUtils.inflate(R.layout.fruit_item);
	view.setImageResource(fruit.getResId());
	// return result.
	return view.
}

public boolean isViewFromObject(View view, Object object) {
	return ((View)object).getView() == view;
}

### ViewPager 是如何与 PagerAdapter 进行沟通的

在前面的叙述中,ViewPager 是与 PagerAdapter 进行交互的,在具体实现中,ViewPager 在 PagerAdapter 里面注入了一个 Observer, 在 setAdapter(PagerAdapter adapter) 调用mAdapter.registerDataSetObserver(mObserver);

private class PagerObserver extends DataSetObserver {
    @Override
    public void onChanged() {
        dataSetChanged();
    }
    @Override
    public void onInvalidated() {
        dataSetChanged();
    }
}

当PagerAdapter 中的数据发生变化时,PagerAdapter 调用mObservable.notifyChanged(); 来通知 ViewPager 进行相应的处理。ViewPager 会收到相应的回调, 在 dataSetChanged() 方法中进行相应的处理。

FragmentPagerAdapter 与 FragmentStatePagerAdapter

Android System 针对大多数都是基于 Fragment 来进行页面展示的,因此实现了两个扩展类 FragmentPagerAdapter 与 FragmentStatePagerAdapter。 这两个类可以认为是对 PagerAdapter 进行了二次封装,实现了对 Fragment 的复用和管理。

在进行封装后,这两个类都只需要实现两个接口就可以 work 了(实际上,我们需要做的事情要远比这两个接口多)。

/**
 * Return the Fragment associated with a specified position.
 * 返回相应的Fragment
 */
public abstract Fragment getItem(int position);

/**
 * Return the number of views available.
 */
public abstract int getCount();

先看看 FragmentPagerAdapter 是怎么实现的。 FragmentPagerAdapter 继承了 PagerAdapter ,实现了大部分方法,主要适用于静态页面和页面不太多的情况。页面一旦被渲染出来,就会被保存到 FragmentManager里面去,当页面重新出现的时候,就重新attach上去,这样效率会比较好。

@Override
public Object instantiateItem(ViewGroup container, int position) {

	// 在instantiate的时候,添加Fragment
	// 在 finishUpdate的时候,commit transaction.
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    final long itemId = getItemId(position);

    // Do we already have this fragment?
    String name = makeFragmentName(container.getId(), itemId);
    // fragment 是否已经存在了
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
    	// 如果已经存在,就调用重新 attach 上
    	// 需要注意的地方是,如果Fragment new出来后,在viewpager 没被销毁的时候,Fragment 就不会被释放掉
    	// 当页面不在显示的时候,只是 detach from fragment manager.
    	// 因此当我们在Fragment 查询到对应tag 的 Fragment 存在,就直接 attach 上就好。
        if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
        mCurTransaction.attach(fragment);
    } else {
    	// 实例化 Fragment,这个就需要自己实现了
    	// 当Fragment实例化出来后,就添加的 fragment manager 里面去.
        fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
        mCurTransaction.add(container.getId(), fragment,
                makeFragmentName(container.getId(), itemId));
    }
    if (fragment != mCurrentPrimaryItem) {
        fragment.setMenuVisibility(false);
        fragment.setUserVisibleHint(false);
    }

    return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    // destroy的时候,并不销毁 Fragment,只是从detach掉
    if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
            + " v=" + ((Fragment)object).getView());
    mCurTransaction.detach((Fragment)object);
}

@Override
public void finishUpdate(ViewGroup container) {
    if (mCurTransaction != null) {
        mCurTransaction.commitAllowingStateLoss();
        mCurTransaction = null;
        mFragmentManager.executePendingTransactions();
    }
}

@Override
public boolean isViewFromObject(View view, Object object) {
    return ((Fragment)object).getView() == view;
}

FragmentStatePagerAdapter 与 FragmentPagerAdapter 类似,区别在于 FragmentStatePagerAdapter 更适合于对 Fragment 页面变化比较多,或者经常发生变动的情况。

@Override
public Object instantiateItem(ViewGroup container, int position) {
    // If we already have this item instantiated, there is nothing
    // to do.  This can happen when we are restoring the entire pager
    // from its saved state, where the fragment manager has already
    // taken care of restoring the fragments we previously had instantiated.

    // 如果 Fragment 存在,那么直接返回,不用add
    // 因为不可见的Fragment 都会被remove掉

    if (mFragments.size() > position) {
        Fragment f = mFragments.get(position);
        if (f != null) {
            return f;
        }
    }

    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }

    Fragment fragment = getItem(position);
    if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
    if (mSavedState.size() > position) {
    	// 获取可能存在的状态
        Fragment.SavedState fss = mSavedState.get(position);
        if (fss != null) {
            fragment.setInitialSavedState(fss);
        }
    }
    while (mFragments.size() <= position) {
        mFragments.add(null);
    }
    fragment.setMenuVisibility(false);
    fragment.setUserVisibleHint(false);
    mFragments.set(position, fragment);
    mCurTransaction.add(container.getId(), fragment);

    return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    Fragment fragment = (Fragment)object;

    if (mCurTransaction == null) {
        mCurTransaction = mFragmentManager.beginTransaction();
    }
    if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
            + " v=" + ((Fragment)object).getView());
    while (mSavedState.size() <= position) {
        mSavedState.add(null);
    }
    mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment));
    mFragments.set(position, null);
    // 不缓存,直接移除掉,这样可以节省内存
    mCurTransaction.remove(fragment);
}

使用 PagerAdapter 的正确姿势

FragmentPagerAdapter 的使用细节

  • 根据前面所说的情况,getItem() 并不能被确保调用,因此 getItem() 在传递参数的时候,只适合传递一些静态的内容。如果我们在 getItem() 方法调用了一些需要动态改变的东西,然后使用 notifyDataSetChanged() ,会发现不起作用,就是因为这个缘故。如果需要在生成 Fragment 对象后,将数据集中的一些数据传递给该 Fragment,这部分代码应该放到这个函数的重载里。在我们继承的子类中,重载该函数,并调用 FragmentPagerAdapter.instantiateItem() 取得该函数返回 Fragment 对象,然后,我们该 Fragment 对象中对应的方法,将数据传递过去,然后返回该对象(可参考这里的实现Fragment 如何与Activity 进行交互)。
  • 注意在 getItem() 的时候不能重复调用 SetArguments() 方法,这种数据传递方式只可能用一次,在 Fragment 被添加到 FragmentManager 后,一旦被使用,我们再次调用 setArguments() 将会导致 java.lang.IllegalStateException: Fragment already active 异常。因而可以采用前面提及的方法。
  • 当显示的页面发生变化的时候,需要 getItemPosition() 进行特殊处理。getItemPosition() 方法有两个魔法值。这个方法会在 ViewPager 需要调用查看当前页面是否发生改变的时候调用, 默认返回 POSITION_UNCHANGED 表示页面没有发生改变,这也是我们常出现bug的地方。返回 POSITION_NONE 来表示页面已经不存在,或者我们可以返回新的位置。通常我们可以返回 POSITION_NONE 来强制进行页面的刷新。
public static final int POSITION_UNCHANGED = -1;
public static final int POSITION_NONE = -2;

FragmentStatePagerAdapter 的使用细节

Fragment.setArguments()这种只会在新建 Fragment 时执行一次的参数传递代码,可以放在getItem()里面,其余的代码应该放在 instantiateItem() 里面去

Android 最佳实践

RAM 在任何软件开发中都是非常珍贵的资源,在内存受限的手机系统中显得更加弥足珍贵。尽管Android的Davlik虚拟机能够对内存自动进行垃圾回收,但这并不意味着你能忽略你的应用对内存的分配和释放。

为了使得垃圾回收器能够及时回收内存,需要避免内存泄漏(例如一个全局变量持有对你的引用),同时要保证在合适的时间释放掉这些引用对象。对于大多数应用而言,做到上面的内容就足够了,Davlik负责处理剩下的内容。

接下来的内容将说明Android系统如何管理 app 进程,内存分配,和如何在开发 Android 应用的时候降低内存使用。当需要得知一些如何在 Java 编程中合理利用内存资源,可以参考一些网上的其他书籍。Android 系统提供了比较方面的方式便于你去分析你应用的内存占用情况,链接在Investigating Your RAM Usage

Android 如何管理内存

Android系统并未给内存提供交互区,但还是使用了分页和mmapping的技术去管理内存。这也就意味着任何你修改的内存(无论是给新对象分配内存,还是引用了mmapped的内容)都会在RAM中被保存下来。因而完全释放内存的唯一方法就是释放你所持有的对象,使得内存能够被垃圾回收器回收。

共享内存

为了适应RAM中的一些需求,Android 系统尝试通过共享内存的方式来实现跨进程操作。

  1. 每一个应用进程都是从已经存在的 Zygote 进程中fork出来的。Zygote 进程在系统启动、加载FrameWork 层代码和相应资源的时候启动。 当需要开启一个新的应用进程的时候,系统会 fork Zygote 进程,然后将应用程序跑在这个新的进程上面。这就是使得绝大多数Framework层的代码和资源能够被所有应用进程使用到。
  2. 大多数静态数据都被 mmapped 到一个进程上面。这不仅仅使得同样的数据能够在进程间进行共享,而且使得它能够在需要的时候被paged out。常见的static数据包括Dalvik Code,app resources,so文件等。
  3. 大多数情况下,Android通过显式的分配共享内存区域(例如ashmem或者gralloc)来实现动态RAM区域能够在不同进程之间进行共享的机制。例如,Window Surface在App与Screen Compositor之间使用共享的内存,Cursor Buffers在Content Provider与Clients之间共享内存。

因为大量共享内存的使用,对于 App 所使用的内存资源就需要进行深思熟虑。

分配和回收内存资源

下面是一些关于 Android 系统如何分配和回收内存的一些Tips

  1. 每个进程的 Davlik 堆被指定到一块虚拟机内存大小。这个大小定义了堆大小,能够根据需要进行扩容(但是存在一个上限,这个上限由系统进行定义)
  2. 逻辑上讲的Heap Size和实际物理意义上使用的内存大小是不对等的,Proportional Set Size(PSS)记录了应用程序自身占用以及和其他进程进行共享的内存。

限制应用内存

为了支持多任务同时执行,Android对每个App设置了堆大小限制,准确的堆大小限制在不同机型上有不同的的表现。如果你的App分配了超出限制的 App 堆大小,就会收到 OutOfMemoryError.

在某些机型上,需要通过查询在当前设备商有多少可以内存大小,来决定缓存多少数据在内存中。可以通过查询 getMemoryClass() 来获取相应的信息。这个方法会返回当前可以堆大小。这里有更详细的介绍

应用切换

当用户切换的时候,Android 系统通过 LRU 的方式来对进程进行缓存。例如,当用户第一次启动了一个应用,系统会为此创建一个进程,当用户离开这个app的时候,进程不会立即被回收。系统缓存了这个进程,所以当用户回到这个应用的时候,进程能够被重新应用,以快速启动。

如果你的应用进程已经被缓存起来了,并且持有了暂时不被使用的内存(这部分内存不被用户所使用,而且会对系统的运行性能造成影响)。所以,当系统运行内存较少的时候,系统会回收掉最近没有使用的进程,即便这样对于某些内存敏感的进程还是会有些特殊照顾的。如何使得进程更长久的存在,将在后续的章节中说明。

关于进程和线程的说明,点击这里

应用该如何正确管理内存

在开放的每个阶段,都需要对RAM的使用进行充分的考虑。下面有些技巧可以帮助完成节省内存的目标。

节约使用 Service

如果你需要 Service 来完成后台工作,那么当它的工作完成后,记得将 Service 关闭掉。在释放 Service 的时候,也需要注意不会因为泄漏的缘故导致无法释放。

当你启动一个服务的时候,系统总是会缓存有后台服务正在运行的进程。这样会减少系统能够在LRU Cache中缓存的进程数目,使得应用切换没有那么高效,甚至会在内存不够的时候引发崩溃。最好的方式来限制后台服务的生命周期是 IntentService, IntentService 能够在执行完 Intent 指定的任务后,尽快将自己释放掉。

当用户界面不可见的时候释放掉内存

当用户跳转到其他界面上去后,界面UI不再被需要,你应该尽快释放被 UI 占据的资源。释放UI占据的资源将显著提升系统性能,增加能够被缓存的进程数量,这样就在一定程度上提升用户体验。

通过在 Activity 里面实现 onTrimMemory() 方法,可以在用户离开界面的时候,接受到相应的通知。在这个方法里面,需要处理 TRIM_MEMORY_UI_HIDDEN 这个级别的消息,来释放掉相应的内存。

需要注意的是 TRIM_MEMORY_UI_HIDDEN 消息不同于 onStop 回调。OnStop 方法会在用户界面不可见的时候,甚至在用户导航到其他 Activity 中的时候触发。尽管应该在 OnStop 方法里面处理 unRegister 等方法,但应该在 onTrimMemory(TRIM_MEMORY_UI_HIDDEN) 释放内存。这能保证及时用户从其他界面回来的时候,依然能够保证高效。

当内存不足的时候释放内存

onTrimMemory() 方法在这几个基本上都需要做特殊处理 TRIM_MEMORY_RUNNING_MODERATE、TRIM_MEMORY_RUNNING_LOW、TRIM_MEMORY_RUNNING_CRITICAL

Android Process 的进程回收策略可以保证当你占有的内存越少,越可能不被回收。

避免Bitmap内存问题

当需要加载图片的时候,需要记住只缓存当前屏幕展示区域大小的分辨率,当分辨率太高的时候可以对其进行缩放。记住Bitmap占据的内存大小呈现指数级增长。具体信息参看这里

使用合适的数据容器

尽量使用 Android 中提供的容器,例如 SparseArray,LongSparseArray。原生的 HashMap 非常消耗内存资源,他必须为每一个 Entry 都创建一个单独的对象。而相对而言,SparseArray就高效许多,SparseArray 能够避免系统对 Key 和 Value 的自动装箱操作。当然如果可以使用原生数组也会是不错的选择。

注意内存过载

需要清楚语言特性和 Lib 库所带来的内存使用,在进行设计和开发的时候,要时刻关注这些。通常有些我们不经意的地方结果耗费了大量的内存。例如:

  1. 枚举通常会消耗比常量2倍还多的内存,因而在 Android 上需要尽量避免使用 枚举。
  2. 每一个类,包括内部类将占用500左右的字节大小
  3. 每个接口大概占用 12-16 字节的内存大小
  4. 在 HashMap 中每增加一个Entry将额外消耗 32 字节的大小。

大量小字节的使用将很快使得内存占用过载,这也使得定位内存泄漏和分析问题带来了困扰。

使用轻量级的protobufs来序列化数据

Protocol Buffers 是跨语言,多平台,灵活的数据序列化方案,比 XML 更灵活,方便和迅速。如果你决定在项目中使用 Proto, 那么在客户端就应该尽量使用最轻量级的 protobufs代码。默认的protobufs生产了大量冗杂代码,这样会在客户端中带来很多问题。

避免使用依赖注入框架

很人多会选择使用诸如 Guice 等注入框架,这些方法能够简化你的代码,方便测试。然后这些框架会在执行的时候,会扫描本地代码寻找 Annotations, 因而会在内存中拥有许多变量,占用了内存。因而尽量使用在编译期的注解。

使用 ProGuard 来优化代码

ProGuard 能够优化,减小和混淆无用的代码,保留简写后的类名,方法名。使用 ProGuard 也能够减少内存使用。

使用多进程

另一个方法来减少你的内存占用的方法就是使用多进程,这种方法的使用需要你的app支持多进程调用。这种方式特别适合在前后台都需要做大量工作的事情。

常见的多进程例子就是,音乐播放器可以在后台独自播放音乐。

 

这篇文章主要讲一些能改善性能的一些小 Tips。使用正确的算法和数据结构是第一优先级的事情,但不在本文的讨论范围中。本文中的一些小 Tips 能够显著改善一些编程习惯。

简单而言,是2方面的工作

  1. 不做不必要的工作
  2. 不分配不必要的内存

在 Android 系统中进行开发面临的一个显著问题就是我们所开发的 App 将在多个机型上运行,这些机型的分辨率,性能等差异巨大。

避免创建不必要的对象

创建对象不是一个消费很低的事情。当你给应用中的 App 分配内存的时候,可能会引发强制垃圾回收,给用户造成卡顿的影响。尽管在 Android 2.3 开始引入的并行垃圾回收器对于这种现象有一定帮助,但是我们还是要尽量避免不必要的工作。

例如如下的一些例子:

  1. 如果一个方法返回String类型的数据,并且你知道这个返回值将用于添加到其他String中,那么直接返回 StringBuffer 就可以了,这样可以避免创建临时对象。
  2. 如果从一组输入数据中解析相应的值,那么返回原始数据中的一部分,而不是返回一份拷贝。如果你创建了新的String对象,那么 Char[] 会被重复利用,但这也导致即使你只有一部分数据被使用,整个String 还是会被缓存起来。
  3. 两个单独的int数组要比一对(int,int)的更具有效率。

一般来讲,应该尽量避免临时创建对象。更少的创建对象意味着更低频率的垃圾回收,那么对用户体验也有更多的好处。

尽量申明 Static 变量

如果你不用访问对象的内部的变量,那么把方法声明为 static,测试标明这种方式能够提速 15%-20%。而且这也是一条最佳实践,这能通过签名能够告诉方法调用者这个类不会修改对象内部状态。

对变量使用 Static Final

考虑如下的几个变量申明:

static int intVal = 42;
static String strVal = "Hello, world!";

编译器会生成一个 的方法,这个方法会在类第一次被加载的时候调用。这个方法会将 42 这个值存储到 intVal 中,并且持有一个对strVal的引用。其后这些变量也会被方法表引用。

通过添加 final 关键字可以解决这个问题。

static final int intVal = 42;
static final String strVal = "Hello, world!";

这个类就不再需要 方法,因为这些变量将直接在 dex 文件中的初始化。42 这个字会被直接存下来,strVal 将使用相对不那么笨重的常量指令而不是 field lookup表。

避免内部使用的Getter和Setter方法

使用 ProGuard 可以帮我们优化。