第一章 Activity 的生命周期和启动模式
1.1 Activity 的生命周期全面分析
1.1.1 典型情况下的生命周期分析
1. 当用户打开新的Activity或者切换到桌面的时候,回调如下:onPause->onStop。
这里有一种特殊情况,如果新Activity采用了透明主题,那么当前Activity不会回调onStop。
2. onStart和onStop是从Activity是否可见这个角度来回调的,而onResume和onPause是从Activity是否位于前台这个角度来回调的,除了这种区别,在实际使用中没有明显区别。
1.1.2 异常情况下的生命周期分析q
1. 资源相关的系统配置发生改变导致Activity被杀死并被重新创建。
系统会调用onSaveInstanceState来保存当前Activity的状态。这个方法的调用时机是在onStop之前,它和onPause没有既定的时序关系,它既可能在onPause之前调用,也可能在onPause之后调用。onRestoreInstanceState的调用时机在onStart之后。
保存和恢复View的层次结构,系统的工作流程:首先Activity被意外终止时,Activity会调用onSaveInstanceState去保存数据,然后Activity会委托Window去保存数据,接着Window再去委托它上面的顶级容器去保存数据。顶级容器一般来说很有可能是DecorView。最后顶级容器再去一一通知它的子元素去保存数据,这样整个数据保存就完成了。
注意:系统只在Activity异常终止的情况下才会调用onSaveInstanceState和onRestoreInstanceState来保存和恢复数据,其它情况不会触发这个过程。但是在按home键和启动新的Activity时仍然会触发onSaveInstanceState,不过不会调用onRestoreInstanceState,因为当前的Activity并没有确定被系统销毁。
2. 资源内存不足导致低优先级的Activity被杀死。
注意:如果一个进程中没有四大组件在运行,那么很容易被系统杀死,因此,一些后台工作不适合脱离四大组件而独自工作在后台中,这样进程很容易被杀死。比较好的方法是将后台工作放入Service中,使进程有一定的优先级,从而不容易被系统杀死。
1.2 Activity 的启动模式
1.2.1 Activity 的 LaunchMode
1. standard。
2. singTop。
3. singleTask。
4. singleInstance。
TaskAffinity属性主要和singleTask启动模式或者allowTaskReparenting属性配对使用。
taskAffinity用于指定当前Activity(activity1)所关联的Task,allowTaskReparenting用于配置是否允许该activity可以更换从属task,通常情况二者连在一起使用,用于实现把一个应用程序的Activity移到另一个应用程序的Task中。
allowTaskReparenting用来标记Activity能否从启动的Task移动到taskAffinity指定的Task,默认继承至application中的allowTaskReparenting=false,如果为true,则表示可以更换;false表示不可以。
allowTaskReparenting用来标记Activity能否从启动的Task移动到taskAffinity指定的Task,默认继承至application中的allowTaskReparenting=false,如果为true,则表示可以更换;false表示不可以。
由于A启动了C,这个时候C只能运行在A的任务栈中,但是C属于B应用,正常情况下,它的TaskAffinity值肯定不可能和A的任务栈相同(因为包名不同)。所以,当B被启动后,B会创建自己的任务栈,这个时候系统发现C原本想要的任务栈已经被创建了,所以就把C从A的任务栈中转移过来了。
给Activity指定启动模式有2种方式,第一种是通过AndroidManifest给Activity指定启动模式,第二种是通过在Intent中设置标志位来为Activity指定启动模式。
第二种的优先级要高于第一种。第一种方式无法直接为Activity设置FLAG_ACTIVITY_Clear_TOP标识,第二种方式无法为Activity指定singleInstance模式。
1.2.2 Activity 的 Flags
FLAG_ACTIVITY_NEW_TASK
FLAG_ACTIVITY_SINGLE_TOP
FLAG_ACTIVITY_CLEAR_TOP
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
1.3 IntentFilter 的匹配规则
一个Activity中可以有多个intent-filter,一个Intent只要能匹配任何一组Intent-filter即可成功启动对应的Activity。
1. action 的匹配规则。
2. category 的匹配规则。
3. data 的匹配规则。
注意:在过滤规则没有指定URI的情况下,URI是有默认值的,默认值为content和file。也就是说,虽然没有指定URI,但是Intent中的URI部分的schema必须为content或者file才能匹配。
当我们通过隐式方式启动一个Activity的时候,可以做下判断,看是否Activity能够匹配我们的隐式Intent。判断的方法有两种:采用PackageManager的ResolveActivity方法或者Intent的ResolveActivity方法,如果它们找不到匹配的Activity就会返回null,我们通过判断返回值就可以规避错误。
第二章 IPC 机制
2.1 Android IPC 简介
2.2 Android 中的多进程模式
2.2.1 开启多进程模式
在Android中使用多进程只有一种方法,那就是给四大组件在AndroidManifest中指定android:process属性。其实还有另一种非常规的多进程方法,那就是通过JNI在native层去fork一个新的进程,但是这种方法属于特殊情况,也不是常用的创建多进程的方式。
“:“的含义是指要在当前的进程名前面附加上当前的包名。
进程名以“:”开头的进程属于当前应用的私有进程,其它组件应用不可以和它跑在同一个进程中,而进程名不以”:“开头的进程属于全局进程,其他应用通过ShareUID方式可以和它跑在同一个进程中。两个应用通过ShareUID跑在同一个进程中是有要求的,需要这两个应用有相同的ShareUID并且签名相同才可以。
2.2.2 多进程模式的运行机制
同一个应用间的多进程:它就相当于两个不同的应用采用了ShareUID模式。
2.3 IPC 基础概念介绍
2.3.1 Serializable 接口
如何进行对象的序列化和反序列化也非常简单,只需要采用ObjectInputStream和ObjectOutputStream即可轻松实现。
注意:静态成员属于类不属于对象,所以不会参与序列化过程;其次用transient(稳态)关键字标记的成员变量不参与序列化过程。
2.3.2 Parcelable 接口
内容描述功能有describeContents方法来完成,几乎所有情况下这个方法都应该返回0,仅当当前对象中存在文件描述符时,此方法返回1。
Android推荐的序列化方式,首选Parcelable。Parcelable主要用在内存序列化上,通过Parcelable将对象序列化到存储设备中或者将对象序列化后通过网络传输也都是可以的,当时这个过程会稍显复杂,因此在这两种情况下建议大家使用Serializable。
2.3.3 Binder
手动实现一个Binder的步骤如下:
1. 声明一个AIDL性质的接口,只需要继承IInterface接口即可,IInterface接口中只有一个asBinder方法。
2. 实现Stub类和Stub类中的Proxy代理类。
如果服务端进程由于某种原因异常终止,这个时候我们服务端的Binder连接断裂(称之为Binder死亡),如何解决?
Binder中提供了两个配对的方法linkToDeath和unlinkToDeath,通过linkToDeath我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知,这个时候就可以重新发起连接从而恢复连接。另外,通过Binder的方法isBinderAlive也可以判断Binder是否死亡。
2.4 Android 中的 IPC 方式
2.4.1 使用 Bundle
2.4.2 使用文件共享
文件共享方式适合在堆数据同步要求不高的进程之间进行通信,并且要求妥善处理并发读写问题。
在多进程模式下,系统对它的读/写就变得不可靠,当面对高并发的读/写访问SharedPreferences有很大几率丢失数据,因此,不建议在进程间通信中使用SharedPreferences。
2.4.3 使用 Messenger
同一个应用的不同组件,如果它们运行在不同的进程中,那么和它们分别属于两个应用没有本质区别。
2.4.4 使用 AIDL
使用AIDL进行进程间通信的流程,分为服务端和客户端两个方面。
1. 服务端。
服务端首先要创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在Service中实现这个AIDL接口即可。
2. 客户端。
客户端要做的事情比较简单,首先需要绑定服务端的Service,绑定成功以后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。
3. AIDL接口的创建。
AIDL接口支持6种数据类型:
1. 基本数据类型;
2. String和CharSequence;
3. List:只支持ArrayList,里面每个元素都必须能被AIDL支持;
4. Map:只支持HashMap,里面每个元素都必须能被AIDL支持,包括key和value。
5. Parcelable:所有实现了Parcelable的对象。
6. AIDL:所有AIDL接口本身也可以在AIDL中使用。
其中自定义的Parcelable对象和AIDL对象必须要显示import进来,不管它们是否和当前的AIDL文件是否位于同一个包中。
注意:如果AIDL文件中用到了自定义的Parcelable对象,必须建立一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。
package com.ryg.chapter_2.aidl;
parcelable Book;
我们需要注意,AIDL中每个实现了Parcelable接口的类都需要按照上面那种方式去创建相应的AIDL文件并声明那个类为Parcelable。
AIDL中除了基本数据类型,其他类型的参数必须标上方向:in、out或者inout。
AIDL接口中只支持方法,不支持声明静态常量,这一点区别于传统的接口。
AIDL的包结构在服务端和客户端要保持一致,否则运行会出错,这是由于客户端需要序列化服务端中和AIDL接口相关的所有类,如果类的完整路径不一样的话,就无法成功反序列化。
4. 远程服务端Service的实现
CopyOnWriteArrayList支持并发读/写,并能进行自动的线程同步。它在Binder中会按照List的规范去访问数据并最终形成一个新的ArrayList传递给客户端。与此类型的还有ConcurrentHashMap。
5. 客户端的实现。
对象是不能跨进程直接进行传输的,对象的跨进程传输的本质都是序列化的过程,这就是AIDL中的自定义对象需要实现Parcelable接口的原因。
如何实现解注册功能?
使用RemoteCallbackList。
客户端和服务端进行跨进程传输的同一个对象在这两端虽然是不同对象,但是它们底层的Binder对象是同一个。
当客户端解注册的时候,我们只要遍历服务端所有的listener,找出那个和解注册listener具有相同Binder对象的服务端listener并把它删掉,这就是RemoteCallbackList为我们做的事情。此外,当客户端进程终止后,它能够自动移除客户端所注册的listener,并且,RemoteCallbackList内部自动实现了线程同步的功能。
遍历RemoteCallbackList,必须要按照下面的方式进行,其中beginBroadCast和finishBroadcast必须要配对使用。
final int N = mListenerList.beginBroadcast();
for ( int i = 0; i < N; i++) {
IOnNewBookArrivedListener l = mListenerList.getBroadcastItem(i);
if (l ! = null) {
//TODO handle l
}
}
mListenerList.finishBroadcast();
客户端的onServiceConnected和onServiceDisconnected方法都运行在UI线程中。
如果要访问UI,请使用Handler切换到UI线程。
服务端意外停止了,需要重新连接服务:
1. 给Binder设置DeathRecipient监听。
2. 在onServiceDisconnected中重连远程服务。
区别:onServiceDisconnected在客户端的UI线程中被回调,而binderDied在客户端的Binder线程池中被回调。
可通过自定义权限在onBind方法和onTransact方法中进行权限验证。
2.4.5 使用 ContentProvider
通过ContentResolver的query、update、insert和delete方法即可进行跨进程访问信息。
getType用来返回一个Uri请求所对应的MIME类型(媒体型),比如图片、视频等。
android:authoritie是ContentProvider的唯一标识。
ContentProvider通过Uri来区分外界要访问的数据集合。
根据Uri先取出Uri_Code,根据Uri_Code再得到数据表名称,知道了外界要访问的表,接下来就可以响应外界的增删改查请求了。
要观察一个ContentProvider中的数据改变情况,可以通过ContentResolver的registerContentObserver和unregisterContentObserver方法来注册和解注册观察者。
注意:query、update、insert、delete四大方法是存在多线程并发访问的,因此要做好线程同步。
SQliteDatabase内部对数据库的操作是有同步处理的,但是如果通过多个SQLiteDatabase对象来操作数据库就无法保证线程同步,因为SQLiteDatabase对象来操作对象就无法保证线程同步,因此SQLiteDatabase对象之间无法进行线程同步。
ContentProvider除了支持对数据源的增删改查这四个操作,还支持自定义调用,这个过程是通过ContentResolver的Call方法和ContentProvider的Call方法来完成的。
2.4.6 使用 Socket
为了降低重试机制的开销,加入休眠机制,即每次重试的时间间隔为1000毫秒。
多进程不推荐使用这种方式,过于繁琐。
2.5 Binder 连接池
工作机制:每个业务模块创建自己的AIDL接口并实现此接口,这个时候不同业务模块之间是不能有耦合的,所有实现细节要单独开来,然后向服务端提供自己的唯一标识和其对应的Binder对象;对于服务端来说,只需要一个Service就可以了,服务端提供一个queryBinder接口,这个接口能够根据业务模块的特征来返回相应的Binder对象给它们,不同的业务模块拿到所需的Binder对象之后就可以进行远程方法调用了。
Binder连接池的作用主要是将每个业务模块的Binder请求统一转发到远程Service中去执行,从而避免了重复创建Service的过程。
Binder连接池的实现中,我们通过CountDownLatch将异步操作转化成了同步操作,这就意味着它有可能是耗时的,然后就是Binder方法的调用过程也可能是耗时的,因此不建议放在主线程中执行。
BinderPool有断线重连机制,当远程服务意外终止时,BinderPool会重新建立连接,这个时候如果业务模块中的Binder调用出现了异常,也需要手动去获取最新的Binder对象,这个是需要注意的。
2.6 选用合适的 IPC 方式
名称 | 优点 | 缺点 注意点 | 设用场景 |
Bundle | 简单易用 | 只能传输Bundle支持的数据类型 | 四大组件 间的进程通信 |
文件共享 | 简单易用 | 不适合高并发的情况, 并且无法做到进程间的即时通讯 | 无并发访问情况下, 交换简单的数据实时性不高的情况 |
AIDL | 功能强大,支持一对多并发通信 ,支持实时通讯 | 需要处理好线程同步 | 一对多通信且有RPC需求 |
Messager | 支持一对多串行通信,支持实时通讯 | 不能很好处理高并发情况,不支持RPC,数据通过Message进行传输,因此只能传输Bundle支持的数据类型 | 低并发的一对多即时通信,无 RPC需求,或者无需返回结果的RPC需求 |
ContentProvider | 在数据源访问方面功能强大,支持一对多并发数据共享,可通过call方法扩展其他操作 | 可以理解为受约束的AIDL,主要提供数据源的CRUD | 一对多的进程间数据共享 |
Socket | 功能强大,可以通过网络传输字节流,支持一对多并发实时通讯 | 实现细节有点繁琐,不支持直接的RPC | 网络数据交换 |
第三章 View 的事件体系
3.1 View 基础知识
3.1.1 什么是 View
View是Android中所有空间的基类。
3.1.2 View 的位置参数
注意:View在平移的过程中,top和left表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是x、y、translationX和translationY这四个参数。
3.1.3 MotionEvent 和 TouchSlop
1. MotionEvent
getX/getY返回的是相对于当前View的左上角的x和y坐标,而getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。
2. TouchSlop
TouchSlop是系统所能识别的被认为是滑动的最小距离。
这是一个常量,和设备有关,在不同的设备上这个值可能是不同的,通过如下方式来获取:
ViewConfiguration.get(getContext()).getScaledTouchSlop()
3.1.4 VelocityTracker、GestureDetector 和 Scroller
1. VelocityTracher
使用流程:
首先,在View的onTouchEvent方法中追踪当前单击事件的速度:
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
接着,获取当前的速度:
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracher.getYVelocity();
最后,不需要使用它的时候,调用clear方法来清除并回收内存:
velocityTracker.clear();
velocityTracher.recycle();
2. GestureDetector
用于辅助检测用户的单击、滑动、长按、双击等行为。
首先,需要创建一个GestureDetector对象并实现OnGestureListener接口,根据需要还可以实现OnDoubleTapListener从而能够监听双击的行为。
GestureDetector mGestureDetector = new GestureDetctor(this);
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
public void setIsLongpressEnabled (boolean isLongpressEnabled)
设置是否启用长按。如果启用长按,当用户按下并保持按下状态时,将收到一个长按事件,同时不再接收其它事件;如果禁用长按,当用户按下并保持按下状 态然后再移动手指时,将会接收到scroll事件。长按默认为启用。
接下来,在需要监听View的onTouchEvent方法中添加如下实现:
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;
最后,可以有选择地实现OnGestureListener和OnDoubleTapListener中的方法了。
建议:如果只是监听滑动相关的,建议自己在onTouchEvent中实现,如果要监听双击这种行为,那么就使用GestureDetector。
3. Scroller
当使用View的scrollTo/scrollBy方法来进行滑动的时候,其过程是瞬间完成的。而使用Scroller是实现有过渡效果的滑动。
Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用才能共同完成这个功能。
Scroller mScroller = new Scroller(mContext);
//缓慢滑动到指定位置
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
//1000 ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
3.2 View 的滑动
3.2.1 使用 scrollTo/scrollBy
如果从左向右滑动,那么mScrollX为负值,反之为正值;如果从上往下滑动,那么mScrollY为负值,反之为正值。
使用scrollTo和scrollBy来实现View的滑动,只能将View的内容进行移动,并不能将View本身进行移动。
3.2.2 使用动画
如果采用属性动画,为了兼容3.0以下的版本,需要采用开源动画库nineoldandroids(http://nineoldandroids.com/)。
如果希望动画后的状态得以保留还必须将fillAfter属性设为true。
在Android3.0以下使用nineoldandroids实现的属性动画实质上还是View动画。
针对View动画用户交互的问题,我们可以在新位置预先创建一个和目标Button一模一样的Button,它们的外观和onClick事件都一样。当目标Button完成平移动画后,就把目标Button隐藏,同时把预先创建的Button显示出来,通过这种间接的方式解决了上面的问题。
3.2.3 改变布局参数
通过改变布局参数里面的Margin即可。
3.2.4 各种滑动方式的对比
总结:
1. scrollTo/scrollBy:操作简单,适合对View内容的滑动。
2. 动画:操作简单,主要适合于没有交互的View和实现复杂的动画效果。
3. 改变布局参数:操作稍微复杂,适合有交互的View。
3.3 弹性滑动
3.3.1 使用 Scroller
注意这里的滑动是指View内容的滑动而非View本身位置的改变。
Scroller如何让View弹性滑动的呢?
答案就是startScroll方法下面的invalidate方法。
原理:当View重绘后会在draw方法中调用computeScroll,而computeScroll又会去向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动;接着又调用postInvalidate方法来进行第二次重绘,这一次重绘和第一次重绘一样,还是会导致computeScroll方法被调用;然后继续向Scroller获取当前的scrollX和scrollY,并通过scrollTo滑动到新的位置,如此反复,直到整个滑动过程结束。
View的每一次重绘都会导致View小幅度的滑动,多次重绘导致了弹性滑动的效果,这就是弹性滑动的工作机制。
3.3.2 通过动画
动画本身就是一个渐进的过程,用它来实现的滑动天然就具有滑动效果。
3.3.3 使用延时策略
它的核心思想是通过发送一系列延时消息从而达到一种渐进式的效果,具体来说可以使用Handler或View的postDelayed方法,也可以使用线程的sleep方法。对于postDelayed方法来说,我们可以通过它来延时发送一个消息,然后在消息中进行View的滑动,如果接连不断地发送这种延时消息,那么就可以进行View的弹性滑动。对于sleep方法来说,通过在while循环中不断地滑动View和sleep,就可以实现弹性滑动的效果。
注意:采用这种方式无法精确地定时,原因是系统的消息调度也是有时间的,并且所需时间不定。
3.4 View 的事件分发机制
3.4.1 点击事件的传递规则
给View设置的OnTouchListener,其优先级比onTouchEvent要高,而平时我们常用的OnClickListener优先级最低,处于事件传递的底部。
事件传递机制的结论:
1. 同一个事件序列中的事件不能分别由两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其它View处理。
2. 某个View一旦决定拦截,就不用再调用这个View的onInterceptTouchEvent去询问它是否需要拦截了。
3. 如果View不消耗除ACTION_DOWN以外的其它事件,那么这个点击事件就会消失,最终这些事件会传递个Activity来处理。
4. ViewGroup默认不拦截任何事件。
5. View没有onInterceptTouchEvent方法。
6. View的onTouchEvent默认都会消耗事件(返回true),除非它是不可点击的(clickable和longClickable同时为false)。View的clickable属性都为false,但是longClickable属性要看情况,例如Button的longClickable属性默认为true,TextView的longClickable属性默认false。
7. View的enable属性不影响onTouchEvent的默认返回值。
8. 通过requestDisallowInterceptTouchEvent方法可以在子元素中去干预父元素的事件分发过程,但是ACTION_DOWN事件除外。
3.4.2 事件分发过程的源码解析
1. Activity 对点击事件的分发过程
Window会将事件传递给decor view,decor view一般就是当前View的底层容器(即setContentView()所设置的View的父容器),通过Activity.getWindow().getDecorView()可以获得。
Window的唯一实现是PhoneWindow。
2. 顶级 View 对点击事件的分发过程
FLAG_DISALLOW_INTERCEPT标志位设置了之后,ViewGroup将无法拦截除了ACTION_DOWN以外的其它点击事件。
如果mFirstTouchTarget的值为null,那么ViewGroup会默认拦截同一事件序列中的所有点击事件。
3. View 对点击事件的处理过程
3.5 View 的滑动冲突
3.5.1 常见的滑动冲突场景
1. 场景1:内外滑动的方向一致。
2. 场景2:内外滑动的方向不一致。
3. 场景3:上面两种情况的嵌套。
对于场景1来说,本来这种情况下是有滑动冲突的,但是ViewPager处理了这种滑动冲突,如果是ScrolView的话则需要手动处理滑动冲突。
3.5.2 滑动冲突的处理规则
对于场景1,它的处理规则是:当用户左右滑动时,需要让外部的View拦截点击事件,当用户上下滑动时,需要让内部的View拦截点击事件。具体来说就是,根据是水平滑动还是竖直滑动来判断由谁来拦截事件。根据滑动过程中2个点的坐标就可以判断是水平还是竖直方向的滑动,通常我采用水平和竖直方向的距离差来判断。
3.5.3 滑动冲突的解决方式
1. 外部拦截法
固定伪代码模板如下:
public boolean onInterceptTouchEvent (MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要当前点击事件){
intercepted
= true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted
= false;
break;
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted
;
}
一旦父容器拦截了ACTION_DOWN事件,那么后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器来处理,这个时候事件就没法传递给子元素了。
2. 内部拦截法
需要配合requestDisallowInterceptTouchEvent方法才能正常工作。
public boolean dispatchTouchEvent (MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父类需要当前点击事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return parent.dispatchTouchEvent(event);
}
除了子元素需要做处理以外,父元素也需要默认拦截除ACTION_DOWN以外的其它事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父类才能拦截所需的事件。
为什么父容器不能拦截ACTION_DOWN事件呢?那是因为ACTION_DOWN事件不受FLAG_DISALLOW_INTERECPT这个标志位的控制。
if (!mScroller.isFinished()) {
//优化滑动体验
mScroller.abortAnimation();
intercepted = true;
}
推荐采用外部拦截法来解决常见的滑动冲突。
对于场景2和场景3来说,主要是根据需求来进行逻辑的指定,比如场景2,这里的父容器是StickyLayout,子元素是ListView。首先,当事落在Header上面时父容器不会拦截事件;接着,如果竖直距离差小于水平距离时,父容器也不会拦截事件;然后,当Header是展开状态并向上滑动时父容器拦截事件。最后,当ListView滑动到顶部了并且向下滑动时,父容器也会拦截事件。通过层层的判断,就可以得到我们想要的效果。
第四章 View 的工作原理
4.1 初识 ViewRoot 和 DecorView
在ActivityThread中,当Activity对象创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将DecorView和ViewRootImpl对象建立关联。
View的绘制流程是从ViewRoot的performTraversals方法开始的。
注意:performDraw的传递过程是在draw方法中通过dispatchDraw实现的。
4.2 理解 MeasureSpec
在测量过程中,系统会将View的LayoutParams根据父容器所施加的规则转化成对应的MeasureSpec,然后再根据这个MeasureSpec来测量出View的宽高。
4.2.1 MeasureSpec
MeasureSpec代表一个32位的int值,其中高2位代表SpecMode,低30位代表SpecSize。它将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了操作方便,还提供了对应的打包和解包方法。
SpecMode分为如下三类:
1.UNSPECIFIED
用于系统内部,表示一种测量的状态。
2.EXACTLY
它对应于LayoutParams中的match_parent和具体的数值这两种方式。
3.AT_MOST
它对应于LayoutParams中的wrap_content。
4.2.2 MeasureSpec 和 LayoutParams的对应关系
注意:MeasureSpec不是唯一由LayoutParams来决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec,从而进一步决定View的宽高。
对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同决定;对于普通View,MeasureSpec由自身的LayoutParams和父容器的MeasureSpec共同决定。
子元素的MeasureSpec的创建和父容器的MeasureSpec和子元素本身的LayoutParams有关,此外还和View的padding和margin有关。
4.3 View 的工作流程
4.3.1 measure 过程
1. View的measure过程
getSuggestedMinimumWidth的逻辑:如果View没有设置背景,那么返回android:minWidth这个属性所指定的值,这个值可以为0;如果View设置了背景,则返回android:minWidth和背景的最小宽度这两者中的最大值。
结论:直接继承View的的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。
解决方法:只需要给View指定一个默认的内部宽高,并在wrap_content时设置此值即可。
2. ViewGroup的测量过程
ViewGroup并没有定义具体的测量过程,这是因为ViewGroup是一个抽象类,其测量过程的onMeasure过程需要具体的子类去实现。
为什么ViewGroup不像View一样对其onMeasure方法做具体的实现?
因为不同的ViewGroup有不同的布局特性 。
注意:在onLayout方法中去获取View的测量宽高和最终宽高。
实际上在Activity的onCreat(),onStart()和onResume()方法中均不能够获取View的真实宽高,这是因为View的measure过程和Activity的生命周期方法不是同步的。如果View还没有测量完毕,那么获得的宽高就是0。
解决方案:
解决方案:
1. Activity/View#onWindowFocusChanged
onWindowFocusChanged这个方法的含义是:View已经初始化完毕了,宽高已经准备好了,这个时候去获取宽高是没有问题的。
注意:onWindowFocusChanged方法会被调用多次,当Activity的窗口得到焦点或失去焦点时均会调用一次。如果频繁地进行onResume和onStart,该方法也会被频繁地调用。
public void onWindowFocusChanged(boolean focus) {
super.onWindowFocusChanged(focus);
if (focus) {
int width = view.getMeasureWidth();
int height = view.getMeasureHeight();
}
}
2. view.post(runnable)
通过post将一个runnable投递到消息队列的尾部,然后等待Looper调用此runnable的时候,View也已经初始化好了。
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasureWidth();
int height = view.getMeasuredHeight();
}
});
}
3. ViewTreeObserver
使用ViewTreeObserver的众多回调可以完成这个功能。当View的状态发生改变或者View的可见性发生改变时,onGlobalLayout将会被回调。
prtected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addGlobalLayoutListener(new OnGlobalLayoutListener() {
@SupperssWarning("deprecation")
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayouListener(this);
int width = view.getMeasureWidth();
int height = view.getMeasureHeight();
}
});
}
4. view.measure(int widthMeasureSpec, int heightMeasureSpec);
处理方法根据view的LayoutParams来区分:
1. match_parent
无法计算。
2. 具体的数值(dp/px)
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY):
view.measure(widthMeasureSpec, heightMeasureSpec);
3. wrap_content
int widthMeasureSpec = MeasureSpec.makeMeasureSpec( (1 << 30)-1, MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec( (1 << 30)-1, MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec, heightMeasureSpec);
4.3.2 layout 过程
和onMeasure方法类型,onLayout方法的实现同样和具体的布局有关,所以View和ViewGroup均没有真正实现onLayout方法。
layoutVertical的setChildFrame,它仅仅是调用子元素的layout方法而已,这样父元素在layout方法中完成自己的定位之后,就通过onLayout方法去调用子元素的layout方法,子元素又会通过自己的layout方法来确定自己的位置,这样一层一层地传递下去就完成了整个View树的layout过程。
View的测量宽高和最终宽高有什么区别?(View的getMeasureWidth和getWidth方法有什么区别?)
在View的默认实现中,View的测量宽高和最终宽高是相等的,只不过测量宽高形成于View的measure过程,而最终宽高形成于View的layout过程,即两者的赋值时机不同,测量宽高的赋值时机稍微早一些。因此,在日常开发中,我们认为测量宽高就等于最终宽高。但是,有些情况确实会导致它们不同。
1.重写View的layout方法,如下:
public void layout(int l, int t, int r, int b) {
super.layout(l, t, r + 100, b+ 100);
}
这样最终宽高总是会比测量宽高大100px。
2.View需要多次测量才能确定自己的测量宽高,在前几次测量过程中得到的测量宽高不准可能不等于最终宽高,但是最终得到的测量宽高和最终宽高还是相同的。
4.3.3 draw 过程
View的绘制过程遵循如下几步:
1. 绘制背景background.draw(canvas)。
2. 绘制自己(onDraw)。
3. 绘制子类(dispatchDraw)。
4. 绘制装饰(onDrawScrollBars)。
View有一个特殊的方法setWillNotDraw,true的话它会设置一个标志位WILL_NOT_DRAW。
这个标记为对于实际开发的意义是:当我们的自定义控件继承于ViewGroup并且本身不具备绘制功能时,就需要开启这个标记位以便于后续的优化。如果我们需要通过onDraw来进行绘制内容时,则必须显示地关闭这个标记位。
4.4 自定义 View
4.4.1 自定义 View 的分类
1. 继承View重写onDraw方法。
2. 继承ViewGroup派生特殊的Layout方法。
3. 继承特定的View(如TextView)
4. 继承特定的ViewGroup(比如LinearLayout)
4.4.2 自定义 View 须知
1. 让View支持wrap_content。
2. 如果有必要,让你的View支持padding。
3. 尽量不要在View中使用Handler方法,除非很明确要使用。
4. View中如果有线程和动画,需要及时停止,参考View#onDetachFromWindow。
当包含此View的Activity退出或者当前View被remove时,View的onDeatchFromWidnow会被调用,和此方法对应的是onAttachToWindow方法。同时,当View变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。
5. View带有滑动嵌套情形时,需要处理好滑动冲突。、
4.4.3 自定义 View 示例
1.继承View重写onDraw方法。
主要用于实现一些不规则的效果,一般需要重写onDraw方法。采用这种方法需要自己处理wrap_content,并且padding也需要自己处理。由于margin属性是由父容器控制的,因此不需要在其中做特殊处理。
针对wrap_content问题,只需要指定一个wrap_content默认的宽高即可。
针对padding问题,只需要在绘制的时候考虑一下padding即可。
2.继承ViewGroup派生特殊的Layout。
1.没有子元素的时候应该根据LayoutParams中的宽/高来做相应处理。
2.考虑它的padding和子元素的margin。
可参考HorizontalScrollViewEx和StickyLayout。
4.4.4 自定义 View 的实现思想
首先要掌握基本功,比如View的弹性滑动,滑动冲突,绘制原理等。
熟练掌握基本功以后,在面对新的自定义View的时候,根据情况选择不同的方式去实现即可。
第五章 理解 RemoteViews
5.1 RemoteViews 的应用
RemoteViews在Android中的使用场景有两种:通知栏和桌面小部件。桌面小部件则是通过AppWidgetProvider来实现的,AppWidgetProvider本质上是一个广播。二者的界面都运行在系统的SystemServer进程中。
5.1.1 RemoteViews 在通知栏上的应用
5.1.2 RemoteViews 在桌面小部件上的应用
1. 定义小部件界面
定义XML文件,命名为widget.xml。
2. 定义小部件配置信息
定义XML文件,命名为appwidget_provider_info.xml。
3. 定义小部件的实现类
继承AppWidgetProvider类。
4. 在AndroidManifest.xml中声明小部件
<receiver
android:name=".MyAppWidgetProvider">
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/appwidget_provider_info">
<meta-data>
<intent-filter>
<action android:name="com.ryg.chapter_5.action.CLICK"/>
` <action android:name="android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
<intent-filter>
</receiver>
注:第一个Action用于识别小部件的单击行为,第二个Action作为小部件的标识必须存在。
onDisabled:当最后一个该类型的桌面小部件被删除时调用此方法。
5.1.3 PendingIntent 概述
PendingIntent的匹配规则:如果两个PendingIntent内部的Intent相同并且requestCode也相同,那么这两个PendingIntent相同。
Intent的匹配规则:如果两个Intent的ComponentName和intent_filter相同,那么这两个Intent相同。
flags常见的类型有4种:
FLAG_ONE_SHOT
当前的PendingInent只能被使用一次,然后它就会被自动cancel。
FLAG_NO_CREATE
无法单独使用,不常见。
FLAG_CANCEL_CURRENT
如果当前描述的PendingIntent已经存在,那么它们都会被取消,然后就会创建一个新的PendingIntent。
FLAG_UPDATE_CURRENT
PendingIntent的flags和notify方法的id对通知间的影响:
如果id是常量,那么不管PendingIntent是否匹配,后面的通知将会直接替换掉前面的通知。
如果id每次都不同,且当PendingIntent不匹配时,不管采用何种标志位,通知间都会互不干扰。
当PendingIntent匹配时,如果采用了FLAG_ONE_SHOT标记位,后续的通知会和第一条通知保持一致,单击任何一条通知后,后续的通知均无法打开。
如果采用了FLAG_CANCEL_CURRENT标记位,那么只有最新的通知才可以打开,之前弹出的通知均无法打开。
如果采用了FLAG_UPDATE_CURRENT标记位,那么之前弹出的通知中的PendingIntent会被更新,最终它们会和最新的通知保持一致,并且这些通知都是可以打开的。
5.2 RemoteViews 的内部机制
RemoteViews无法使用自定义View,它的一系列set方法大部分是通过反射来完成的,其它set方法一般直接设置即可。
通知栏和桌面小部件中的布局文件实际上是在NotificationManagerService和AppWidgetService中被加载的,而它们运行在系统的SystemServer中,因而构成了跨进程通信的场景。
RemoteViews的apply方法在内部调用了每个Action的apply方法,具体的View的更新操作是由Action的apply方法来完成的。
Action对象的apply方法就是真正操作View的地方。
apply会加载布局和更新界面,而reApply只会更新界面。
首先,setOnClickPending只适合给普通View设置单击事件,其次,如果要给ListView和StackView中的item添加点击事件,则必须将setPendingIntentTemplate和setOnClickFillInIntent组合使用才可以。
5.3 RemoteViews 的意义
一个应用需要能够更新另一个应用中的某个界面,如果界面中的View都是一些简单的且都被RemoteViews支持的View,那么可以使用RemoteViews。
如果A和B是两个不同的应用,那么B中的布局文件资源id传输到A中以后很有可能是无效的。既然资源id不相同,那我们就通过资源名称来加载布局文件。两个应用要约定好布局文件中的资源名称,比如“layout_simulated_notification"。
int layoutId = getResources().getIdentifier("layout_simulated_notification", "layout", getPackageName());
View view = getLayoutInflater().inflater(layoutId, mRemoteViewContent, false);
remoteViews.reApply(this, view);
mRemoteViewContent.addView(view);
第六章 Android 的 Drawable
6.1 Drawable 简介
优点:使用简单,比自定义View的成本要低;非图片类型的Drawable占用空间较小,这对减小apk的大小也很有帮助。
Drawable的内部宽/高这个参数比较重要,通过getIntrinsicWidth和getIntrinsicHeight这两个方法可以获取到它们。
一般来说,Drawable是没有大小概念的,当用作View的背景时,Drawable会被用作View的同等大小。
6.2 Drawable 的分类
6.2.1 BitmapDrawable
android:antialias
开启后会让图片变得平滑,同时也会在一定程度上降低图片的清晰度,但是降低的幅度可以忽略,因此应该开启。
android:dither
当图片的像素配置和手机屏幕的像素配置不一致时,开启这个选项可以让高质量的图片在低质量的屏幕上还能保持较好的显示效果。
android:filter
当图片的尺寸被拉伸或者压缩时,开启过滤效果可以保持较好的显示效果。
android:mipMap
这是一种图像相关的处理技术,也叫纹理映射。
android:tileMode
当开启平铺模式后,gravity属性会被忽略。
repeat表示的是简单的水平和竖直方向上的平铺效果;mirror表示一种在水平和竖直方向的镜面投影效果;而clamp表示的效果就更加奇特,图片四周的像素会扩展到周边区域。
.9图片可以自动地根据所需的宽/高进行相应的缩放并保证不失真。
在实际使用中发现在bitmap标签中也可以使用.9图,即BitmapDrawable也可以代表一个.9格式的图片。
6.2.2 ShapeDrawable
android:shape
line和ring需要通过<stroke>标签来制定线的宽度和颜色等信息,否则无法达到预期的显示效果。
<corners>
px表示。
<gradient>
solid表示纯色填充,而gradient表示渐变效果。
1.android:angle——渐变的角度,默认为0,其值必须为45的倍数,0表示从左到右,90表示从上到下。
2.android:useLevel——一般为false,当Drawable作为StateListDrawble使用时为true。
3.android:type——渐变类型,linear、radial(径向渐变)、sweep(扫面线渐变)。
4.android:size——<size>标签设置的宽/高就是ShapeDrawable的固有宽/高,但是作为View背景时,Shape还会被View拉伸或者缩小到View的大小。
6.2.3 LayerDrawable
比较常用的属性有android:top、android:bottom、android:left和android:right,它们分别表示Drawable相对于View的上下左右的偏移量,单位为像素。
6.2.4 StateListDrawable
StateListDrawable对应于<selector>标签。
android:constantSize
True表示它的固有大小是不变的,它的固有大小是内部所有Drawable的固有大小的最大值,false则会随着状态的改变而改变。此选项默认为false。
android:variablePadding
ture表示会随着状态的改变而改变,false则表示StateListDrawable的padding是内部所有Drawable的padding的最大值。
注意:因为默认的View不附带任何状态,所以它可以匹配View的任何状态。
6.2.5 LevelListDrawable
LevelListDrawable对应于<level-list>标签。
Drawable的等级是有范围的,即0~10000,最小的等级是0,这也是默认值。
6.2.6 TransitionDrawable
TransitionDrawable对应于<transition>标签,它用于实现两个Drawable之间的淡入淡出效果。
6.2.7 InsetDrawable
InsetDrawable对应于<inset>标签。当一个View希望自己的背景比实际区域小的时候,可以采用InsetDrawable来实现。
其中android:insetTop、android:insetBottom、android:insetRight、android:insetLeft分别表示顶部、底部、右边、左边内凹的大小。
6.2.8 ScaleDrawable
ScaleDrawable对应于<scale>标签。
android:scaleWidth和android:scaleHeight分别表示对指定Drawable宽和高的缩放比例。
等级0表示ScaleDrawable不可见,这是默认值。
如果ScaleDrawable的级别越大,那么内部的Drawble看起来越大;如果ScaleDrawable的XML中所定义的缩放比例越大,那么内部的Drawable看起来越小。
我们可以武断地把ScaleDrawable的等级设为20000,虽然也能正常工作,但是不推荐这么做,但是系统内部Drawable的等级范围为0到10000。
6.2.9 ClipDrawable
ClipDrawable对应于<clip>标签。裁剪方向由android:clipOrientation和android:gravity两个属性共同控制。
Drawable的等级是有范围的,即0~10000,最小的等级是0,即完全裁剪,最大是10000,即不裁剪。
6.3 自定义 Drawable
自定义的Drawable无法再XML中使用。另外,getIntrinsicWidth和getIntrinsicHeight这两个方法需要注意,当自定义的Drawable有固有大小时,最好重写一下这两个方法,因为它会影响到View的wrap_content布局。
需要注意的是,内部大小不等于Drawable实际区域的大小,Drawable的实际区域大小可以通过它的getBounds方法来获得,一般来说它和View的尺寸相同。
第七章 Android 动画深入分析
7.1 View 动画
7.1.1 View 动画的分类
对于View动画来说,建议采用XML来定义动画,这是因为XML动画的可读性更好。
7.1.2 自定义 View 动画
只需要基础Animation这个抽象类,然后重写它的initialize方法和applyTransformation方法,在initialize中做一些初始化工作,在applyTransformation中进行相应的矩阵变换即可,很多时候需要采用Camera来简化矩阵变换的过程。
7.1.3 帧动画
不同于View动画,系统提供了另一个类AnimationDrawable来使用帧动画。
7.2 View 动画的特殊使用场景
7.2.1 LayoutAnimation
LayoutAnimation作用于ViewGroup,为ViewGroup指定一个动画,这样当它的子元素出场时都会具有这种动画效果。
使用LayoutAnimation的步骤:
1. 定义LayoutAnimation
android:delay
如果时间周期为300ms,那么0.5就表示第一个动画需要延迟150ms才能进入动画,第二个动画需要延迟300ms才能进入动画。
2. 为子元素指定具体的入场动画
3. 为ViewGroup指定android:layoutAnimation属性。
除了在XML中使用LayoutAnimation属性外,还可以通过LayoutAnimationController来实现。
7.2.2 Activity 的切换效果
overridePendingTransition(int enterAnim, int exitAnim),必须在startActivity(Intent)或者finish()之后被调用后才能生效。
Fragment也可以添加切换动画,需要使用support—v4兼容包,通过FragmentTransaction中的setCustomAnimations()方法来添加切换动画。
7.3 属性动画
7.3.1 使用属性动画
Nineoldandroids对属性动画做了兼容,在API 11以前的版本其内部是通过代理View动画来实现的,因此在Android低版本上,它的本质还是View动画。
属性动画需要定义在res/animator/目录下。
android:startOffset——表示动画的延迟时间。
android:valueType——如果android:propertyName所指定的属性表示的是颜色,那么不需要指定android:valueType,系统会自动对颜色类型的属性做处理。
在实际开发中建议采用代码来实现属性动画,这是因为通过代码实现比较简单。
7.3.2 理解插值器和估值器
自定义插值器需要实现Interpolator或者TimeInterpolator,自定义估值算法需要实现TypeEvaluator。如果要对其它类型(非int、float、Color)做动画,那么必须要自定义类型估值算法。
7.3.3 属性动画的监听器
动画是由许多帧组成的,每播放一帧,onAnimationUpdate就会被调用一次。
7.3.4 对任意属性做动画
总结:我们队object的属性abc做动画,如果想让动画生效,要同时满足两个条件:
1.object必须要提高setAbc方法,如果动画的时候没有传递初值,那么还要提高getAbc方法,因为系统要去取abc属性的初始值(如果这条不满足,程序直接Crash)。
2.object的setAbc对属性abc所做的改变必须能够通过某种方法反映出来,比如会带来UI的改变之类的。
TextView的宽度对应XML中的android:layout_width属性,而TextView还有一个属性android:width,这个属性就对应了TextView的setWidth方法。
解决方法:
1. 给你的对象加上get和set方法,如果你有权限的话。
简单,往往不可行。
2. 用一个类来包装原始对象,间接为其提供get和set方法。
3. 采用ValueAnimator,监听动画过程,自己实现属性的改变。
7.3.5 属性动画的工作原理
AnimationHandler并不是Handler,它是一个Runnable。
calculateValue方法就是计算每帧动画所对应的属性的值。
PropertyValuesHolder的setupValue方法,其get方法是通过反射来调用的。
当动画的下一帧到来时,PropertyValuesHolder中的setAnimatedValue会将新的属性值设置给对象,调用其set方法。
7.4 动画使用的注意事项
1. OOM问题
2.内存泄漏
在属性动画中有一类无限循环的动画,这类动画需要在Activity退出时及时停止,否则将导致Activity无法释放从而造成内存泄漏,通过验证后发现View动画并不存在此问题。
3.兼容性问题54
4. View动画问题
当setVisibility(View.GONE)失效时,只要调用View.clearAnimation()清除View动画杰克解决此问题。
5. 不要使用px
要尽量使用dp,使用px会导致在不同的设备上有不同的效果。
6. 动画元素的交互
7. 硬件加速
建议开启,会提高动画的流畅性。
第八章 理解 Window 和 WindowManger
8.1 Window 和 WindowManager
Window常用的Flags如下:
1. FLAG_NOT_FOUSABLE
表示Window不需要获取焦点,也不需要接受各种输入事件。
2. FLAG_NOT_TOUCH_MODEL
系统会将Window以外的单击事件传递给底层的Window,当前Window区域以内的单击事件则自己处理。
3. FLAG_SHOW_WHEN_LOCKED
Window有三种类型,分别是应用Window、子Window、系统Window。
在三类Window中,应用Window的层级范围是1~99,子Window的层级范围是1000~1999,系统Window的层级范围是2000~2999。
WindowManager提供了三种功能,addView、updateViewLayout、removeView。
WindowManger操作Window的过程更像是操作Window中的View。
8.2 Window 中的内部机制
Window是一个抽象的概念,每一个Window都对应着一个View和一个ViewRootImpl,Window和View通过ViewRootImpl来建立联系。
8.2.1 Window 的添加过程
WindowManagerImpl这种工作模式是典型的桥接模式,它将工作全部委托给WindowManagerGlobal来实现。
WindowManagerGlobal的addView方法主要分为如下几步:
1. 检查参数是否合法,如果是子Window那么还需要调整一些布局参数。
2. 创建ViewRootImpl并将View添加到列表中。
在addView中通过如下方式将Window的一系列对象添加到列表中:
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
3. 通过ViewRootImpl来更新界面并完成Window的添加过程。
在setView内部会通过requestLayout来完成异步刷新请求。scheduleTraversals实际是View绘制的入口。
mWindowSession的类型是IWindowSession,它是一个Binder对象,真正的实现类是Session,也就是Window的添加过程是一次IPC调用。
在Session内部会通过WindowManagerService来实现Window的添加。
8.2.2 Window 的删除过程
在WindowManager中提供了两种删除接口RemoveView和RmoveViewImmediate,它们分别表示异步删除和同步删除。具体的删除操作由ViewRootImpl的die方法来完成。
在dodie方法中会调用dispatchDetachedFromWindow,它主要做以下四件事:
1. 垃圾回收相关工作,比如清除数据和消息、移除回调。
2.通过Session的remove方法来删除Window:mWindowSession.remove(mWindow,这同样是一个IPC调用过程,最终会调用WindowManagerService的removeWindow方法。
3. 调用View的dispatchDetachedFromWindow方法,在内部会调用View的onDetachedFromWindow()和onDetachedFromWindowInternal()。可以再
onDetachFromWindow()方法中做一些资源回收工作,比如终止动画、停止线程等。
4. 调用WindowManagerGlobal的doRemoveView方法刷新刷新数据,需要将当前Window所关联的这三类对象mRoots、mParams、mDyingViews从列表中删除。
8.2.3 Window 的更新过程
8.3 Window 的创建过程
8.3.1 Activity 的创建过程
Activity的启动过程很复杂,最终会由ActivityThread中的PerformLaunchActivity()来完成整个启动过程。
Window对象的创建时通过PolicyManger的makeNewWidnow方法实现的。
Window的具体实现的确是PhoneWindow。
PhoneWindow的setContentView的步骤如下:
1. 如果没有DecorView,那么就创建它。
2. 将View添加到DecorView的mContentParent中。
因为Activity的布局文件只是被添加到DecorView的mContentParent中,因此叫它setContentView更准确。
3. 回调Activity中的mContentChanged方法通知Activity视图已经发生改变。
在ActivityThread的handleResumeActivity方法中,首先会调用Activity的onResume方法,接着会调用Activity的makeVisible(),在makeVisible方法中,DecorView真正完成了添加和显示这两个过程。
8.3.2 Dialog 的 Window 的创建过程
Dialog的Window的创建步骤如下:
1. 创建Window。
2. 初始化DecorView并将Dialog的视图添加到DecorView中。
3. 将DecorView添加到Window中并显示。
在Dialog的show方法中,会通过WindowManger将DecorView添加到Window中。
普通的Dialog有一个特殊之处,那就是必须采用Activity的Context,如果采用Application的Conte xt,那么就会报错。
只要指定对话框的Window为系统Window,那么就会正常弹出对话框。
8.3.3 Toast 的 Window 创建过程
在Toast的内部有两类IPC调用过程,第一类是Toast访问NotificationManagerService,第二类是NotificationManagerService回调Toast里的TN接口。
需要注意的是TN这个类,它是一个Binder类,在Toast和NMS进行IPC的过程中,当NMS处理Toast的显示和隐藏时会跨进程回调TN中的方法,这个时候由于TN运行在Binder线程池中,所以需要通过Handler将其切换到当前线程中。对于非系统应用来说,mToastQueue中最多能同时存在50个ToastRecord,这样做是为了防止DOS。
第九章 四大组件的工作过程
9.1 四大组件的运行状态
隐式Intent则指向一个或多个目标Activity组件,当然也可能没有任何一个Activity组件可以处理这个隐式Intent。
尽管Service是用于执行后台技计算的,但是它本身是运行咋主线程中的,因此耗时的后台计算仍然需要在单独的线程中去完成。
Service组件也是可以停止的,停止一个Service组件稍显复杂,需要灵活运用stopService和unBindService这两个方法才能完全停止一个Service组件。
静态注册是指在AndroidManifest中注册广播,这种广播在应用安装时会被系统解析,此种形式的广播不需要应用启动就可以收到相应的广播。
由于BroadcastReceiver的特性,它不适合用来执行耗时操作。BroadcastReceiver不需要停止,它也没有停止的概念。
需要注意的是,ContentProvider内部的query、delete、update、insert方法需要处理好线程同步,因为这几个方法是在Binder线程池中被调用的,
另外ContentProvider组件也不需要手动停止。
9.2 Activity 的工作过程
startActivity方法有好几种重载方式,但它们最终都会调用startActivityForResult方法。
ApplicationThread和ActivityThread在Activity的启动过程中发挥着很重要的作用。接着看一下Instrumentation的execStartActivity方法。
启动Activity真正的实现由ActivityManagerNative.getDefault()的startActivity方法来完成。
由于ActivityManagerNative.getDefault()其实是一个IActivityManager类型的Binder对象,因此它的具体实现是AMS。
在AMS的startActivity中,又调用了ActivityStackSupervisor的startActivityMayWait方法,又调用了startActivityUncheckedLocked方法,
又
调用了ActivityStack的resumeTopActivitiesLocked方法,又调用了resumeTopActivityInnerLocked,又调用了ActivityStackSupervisor
的startSpecificActivityLocked方法,又调用了realStartActivityLocked方法。
从接口方法的命名可以猜测,IApplicationThread这个Binder接口的实现者完成了大量和Activity以及Service启动/停止相关的功能。
IApplicationThread的实现者就是ActivityThread中的内部类ApplicationThread。
ApplicationThread通过scheduleLaunchActivity方法来启动Activity。
scheduleLaunchActivity的实现很简单,就是发送一个启动Activity的消息交由Handler(H)处理。
接下来Activity的启动过程由ActivityThread的handleLaunchActivity方法来实现。
然后在performLaunchActivity方法最终完成了Activity对象的创建和启动过程。
performLaunchActivity这个方法主要完成了如下几件事。
1. 从ActivityClientRecord中获取待启动的Activity的组件信息。
2. 通过Instrumentation的newActivity方法使用类加载器创建Activity对象。
3. 通过LoadedApk的makeApplication方法来尝试创建Application对象。
Application对象的创建也是通过Instrumentation来完成的,这个过程和Activity对象的创建一样,都是通过类加载器来实现的。
4. 创建ContextImp对象并通过Activity的attach方法来完成一些重要数据的初始化。
ContexImpl是通过Activity的attach方法来和Activity建立关联的,除此之外,在attach方法中Activity还会完成Window的创建并建立自己
和Window的关联,这样当Window接收到外部输入事件后就可以将事件传递给Activity。
5. 调用Activity的onCreate方法。
9.3 Service 的工作过程
Service分为两种工作状态,一种是启动状态,主要用于执行后台计算;另一种是绑定状态,主要用于其他组件和Service的交互。
Service既可以处于启动状态也可以同时处于绑定状态。
9.3.1 Service 的启动过程
Service的启动过程从ContextWrapper的startService开始。其中采用了桥接模式,在ContextImpl中,startService方法会调用startServiceCommon
方法,其中又会通过ActivityManagerNative.getDefault()这个对象来启动一个服务。
ActiveServices是一个辅助AMS进行Service管理的类,包括Service的启动、绑定和停止等。在ActiveServices的startServiceLocked方法的尾部会调用startServiceInnerLocked方法。
后续的工作由bringUpServiceLocked方法处理,又调用了realStartServiceLocked方法。首先通过app.thread的scheduleCreateService方法来创建Service对象并调用其onCreate,接着在通过sendServiceArgsLocked方法来调用Service的其它方法,比如onStartCommand。接下来只需要看ApplicationThread对Service启动过程的处理即可,即scheduleCreateService方法。
H会接收这个CREATE_SERVICE消息并通过ActivityThread的handleCreateService方法来完成Service的最终启动。
handleCreateService主要完成了如下几件事:
1. 通过类加载器创建Service的实例 。
2. 创建Application对象并调用其onCreate。
3. 接着创建ContextImpl对象并通过Service的attach方法建立二者之间的关系,这个过程和Activity的过程是类似的,毕竟Servcie和Activity都是一个Context。
最后调用Service的onCreate方法并将Service对象存储到ActivityThread中的一个列表中。
除此之外,ActivityThread中还会调用handleServiceArgs方法调用Service的onStartCommand方法。
9.3.2 Service 的绑定过程
bindServiceCommon方法主要完成如下两件事情:qqq
首先将客户端的ServiceConnection对象转化为ServiceDispatcher.InnerConnection对象。ServiceDispatcher的内部类InnerConnection刚好
充当了Binder这个角色。其实ServiceDispatcher起着连接ServiceConnection和InnerConnection的作用。
当Service和客户端连接以后,系统会通过InnerConnection来调用ServiceConnection中的onServiceConnected方法,这个过程有可能是跨进程的。
当ServiceDispatcher创建好了以后,getServiceDispatcher会返回保存的ServiceConnection对象。
接着bindServiceCommon方法会通过AMS来完成Service的具体的绑定过程。AMS会调用ActiveServices的bindServiceLocked方法,又调用了bringUpServiceLocked,又调用了realStartServiceLocked方法。和启动Service不同的是,Service的绑定过程会调用app.thread的scheduleBindService
方法,这个过程的实现在ActiveServices的requestServiceBindingLocked方法中。
在H内部,接收到BIND_SERVICE这类消息时,会交给ActivityThread的handleBindService方法来处理。首先会根据Servcie的token取出Service对象,
然后调用Service的onBind方法,Service的onBind方法会返回一个Binder对象给客户端使用。为了确定客户端知道已经成功连接Service了,所以还必须
调用客户端的ServiceConnection的onServiceConnected,这个过程是由ActivityManagerNative.getDefault()的publishService方法来完成的。
Service有一个特性,当多次绑定同一个Servcie时,Service的onBind方法只会执行一次,除非Service被终止了。当Service的onBind方法执行了以后,
系统还要告诉客户端已经成功连接Service了。根据上面的分析,这个过程由AMS的publishService方法来实现的。
核心代码为c.conn.connected(r.name, service)。
对于Service的绑定过程来说,ServiceDispatcher的mActivityThread就是ActivityThread中的H。
ServiceDispatcher内部保存了客户端的ServiceConnected对象,因此可以很方便地调用ServiceConnection对象的onServiceConnected方法。
9.4 BroadcastReceiver 的工作过程
9.4.1 广播的注册过程
ContextImpl的registerReceiver方法调用了自己的registerReceiverInternal方法。BroadcastReceiver作为Android的一个组件是不能直接跨进程传递的,所以需通过IIntentReceiver来中转一下。
9.4.2 广播的发送和接受过程
在Android 5.0中,默认情况下广播不会发送给已经停止的应用。因为系统在Android 3.1中为Intent添加了两个标记位,分别是FLAG_INCLUDE_STOPPED_PACKAGES和FLAG_EXCLUDE_STOPPED_PACKAGES,用来控制广播是否要对处于停止状态的应用起作用。
Android 3.1中广播的这个特性同样会影响开机广播,从Android 3.1开始,处于停止状态的应用同样无法接受到开机广播。
在broadcastIntentLocked的内部,会根据intent-filter查出匹配的广播接受这并经过一系列的条件过滤,最终会将满足条件的广播接收者添加到
BroadcastQueue中,接着BroadcastQueue就会把广播发送给相应的广播接收者。
无序广播存储在mParallelBroadcasts中,系统会遍历mParallelBroadcast并将其中的广播发送给它们所有的接收者。deliverToRegisteredReceiverLocked
方法负责将一个广播发送给一个特定的接收者,它内部调用了performReceiveLocked方法来完成具体的发送过程。
ApplicationThread的scheduleRegisteredReceiver的实现比较简单,它通过InnerReceiver来实现广播的接收。
InnerReceiver的performReceive方法会调用LoadedApk.ReceivierDispatcher的performReceive方法,其中会创建一个Args对象并通过mActivityThread的post方法来执行Args中的逻辑,而Args实现了Runnable接口。在run方法中,onReceive方法被执行了。
9.5 ContentProvider 的工作过程
当ContentProvider所在的进程启动时,ContentProvider会同时启动并被发布到AMS中。需要注意的是,这个时候ContentProvider的onCreate要先于
Application的onCreate执行,这在四大组件中是一个少有的现象。
ApplicationThread是一个Binder对象,它的Binder接口是IApplicationThread,它主要用于ActivityThread和AMS之间的通信。
这四个方法都是通过Binder来调用的,外界无法直接访问ContentProvider,它只能通过AMS根据Uri来获取对应的ContentProvider的Binder接口的IContentProvider,然后通过IContentProvider来访问CntentProvider中的数据源。
ContentProvider可以避免进程间通信的开销,但是这在实际开发中仍然缺少使用价值。
ContentProvider被启动时会伴随着进程的启动,在AMS中,首先会启动ContentProvider所在的进程,然后在启动ContentProvider。
ActivityThread的handleBindApplication则完成了Application的创建以及ContentProvider的创建,分为如下四个步骤:
1. 创建ContentImpl和Instrumentation。
2. 创建Application对象。
3. 启动当前进程的ContentProvider并调用其onCreate方法。
在installProvider方法中通过类加载器完成了ContentProvider对象的创建。
4. 调用Application的onCreate方法。
需要注意的是,这里的ContentProvider的具体实现是ContentProviderNative和ContENTProvider.Transport,其中ContentProvider.Transport继承了ContentProviderNative。
第十章 Android 的消息机制
MessageQueue,虽然叫消息队列,但是它的内部存储结构并不是真正的队列,而是采用单链表的数据结构来存储消息列表。
Looper中还有一个特殊的概念,那就是ThreadLocal,ThreadLocal并不是线程,它的作用是可以在每个线程中存储数据。
当然需要注意的是,线程是默认没有Looper的,如果需要使用Handler就必须为线程创建Looper。我们经常提到的主线程,也叫UI线程,它就是ActivityThread,ActivityThread被创建时就会初始化Looper,这也是在主线程中默认可以使用Handler的原因。
10.1 Andorid 的消息机制
Handler的主要作用是将一个任务切换到指定的线程中去执行。
Android规定访问UI只能在主线程中进行,如果是在子线程中访问UI,那么程序就会抛出异常。
系统为什么不允许在子线程中访问UI呢?这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,那为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:首先加上锁机制会让UI访问的逻辑变得复杂;其次锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。
10.2 Android 的消息机制分析
10.2.1 ThreadLocal 的工作原理
一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以采用ThreadLocal。
其实获取的方法也是很简单的,在Thread类的内部有一个成员专门用于存储线程的ThreadLocal的数据:ThreadLocal.Values localValues,因此获取当前线程的ThreadLocal数据就变得异常简单了。如果LocalValues的值为null,那么就需要对其进行初始化,初始化在将ThreadLocal的值进行存储。
在localValues内部有一个数组:private Object[] table。
ThreadLocal的值在table数组中的存储位置总是为ThreadLocal的reference字段所标识的对象的下一个位置。
从ThreadLocal的get和set方法可以看出,它们所操作的对象都是当前线程的localValues对象的table数组,因此在不同线程中访问同一个ThreadLocal的set和get方法,它们对于ThreadLocal所做的读/写操作仅限于自己的线程内部,这就是为什么ThreadLocal可以在多个线程中互不干扰地存储和修改。
10.2.2 消息队列的工作原理
MessageQueue主要包含两个操作:插入和读取。读取操作本身会伴随着删除操作,插入和读取对应的方法分别为enqueueMessage和next,其中enqueueMessage的作用是向消息队列中插入一条消息,而next的作用是从消息队列中取出一条消息并将其从消息队列中移除。
可以发现next是一个无限循环的方法,如果消息队列中没有消息,那么next方法就会一直阻塞在这里。当有消息到来时,next方法会返回这条消息并将其从单链表中移除。
10.2.3 Looper 的工作原理
Looper除了prepare方法外,还提供了prepareMainLooper方法,这个方法主要是给主线程,也就是ActivityThread创建Looper使用的,其本质也是通过prepare方法来实现的。由于主线程的Looper比较特殊,所以Looper提供了一个getMainLooper方法,通过它可以在任何地方获取到主线程的Looper。
10.2.4 Handler 的工作原理
而Callback给我听了另外一种使用Handler的方式,当我们不想派生子类时,就可以通过Callback来实现。
10.3 主线程的消息循环
ActivityThread通过ApplicationThread和AMS进行进程间通信,AMS以进程间通信的方式完成ActivityThread的请求后会回调Application的Binder方法,然后Application会向H发送消息,H收到消息后会将ApplicationThread中的逻辑切换到ActivityThread中去执行,即切换到主线程中query执行,这个过程就是主线程的消息循环模型。
第十一章 Android 的线程和线程池
对于AsyncTask来说,它的底层用到了线程池,对于IntentService和HandlerThread来说,它们的底层则直接使用了线程。但是它们的本质仍然是传统的线程。
从任务执行的角度来看,IntentService的作用很像一个后台线程,但是IntentService是一种服务,它不容易被系统杀死从而能够尽量保证任务的执行,而如果是一个后台线程,由于这个时候进程中没有活动的四大组件,那么这个进程的优先级就会非常低,会很容易被系统杀死,这就是IntentService的优点。
Android中的线程池来源于Java,主要是通过Executor来派生特定的线程池,不同种类的线程池又具有各自的特性。
11.1 主线程和子线程
子线程也叫工作线程,除了主线程以外的线程都是子线程。
11.2 Android 中的线程形态
11.2.1 AsyncTask
AsyncTask并不适合进行特别耗时的后台任务,对于特别耗时的后台任务来说,建议采用线程池。
尽管如此,在Android 3.0以及后续的版本中,我们仍然可以通过AsyncTask的executeOnExecutor方法来并行地执行任务。
11.2.2 AsyncTask 的工作原理
AsyncTask中有两个线程池(SerialExecutor和THREAD_POOL_EXECUTOR)和一个Handler(InternalHandler),其中线程池SerialExecutor用于任务的排队,而线程池THREAD_POOL_EXECUTOR用于执行真正的任务,InternalHandler用于将执行环境从线程池切换到主线程。
从Android 3.0开始,默认情况下AsyncTask的确是串行执行的。
为了让AsyncTask可以在Android 3.0及以上的版本上并行,可以采用AsyncTask的executeOnExecutor方法,需要注意的是这个方法是Android 3.0新添加的方法,并不能在低版本下使用。
11.2.3 HandlerThread
由于HandlerThread的run方法是一个无限循环,因此当明确不需要再使用HandlerThread时,可以通过它的quit或者quitSafely方法来终止线程的执行,这是一个良好的编程习惯。
11.2.4 IntentService
IntentService是一种特殊的Servcie,他继承了Servcie并且它是一个抽象类,因此必须创建它的子类才能使用IntentService。
在实现上,IntentService封装了HandlerThread和Handler。
一般来说,stopSelf(int startId)在尝试停止服务之前会判断最近启动服务的次数是否和startId相同,如果相同就立即停止服务,不相同就不停止服务。
IntentService的onHandleIntent方法是一个抽象方法,它需要我们在子类中实现,它的作用是从Intent参数中区分具体的任务并执行这些任务。
这就意味着IntentService也是顺序执行后台任务的,当有多个后台任务同时存在时,这些后台任务会按照外界发起的顺序排队执行。
11.3 Android 中的线程池
11.3.1 ThreadPoolExecutor
下面是ThreadPoolLocal各个参数的含义:
corePoolSize
如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的线程在等待新任务到来时会有超时策略,这个时间间隔由keepAliveTime所指定,当等待时间超过keepAliveTiem所指定的时长后,核心线程就会被终止。
maximumPoolSize
线程池所能容纳的最大线程数。
keepAliveTime
非核心线程闲置时的超时时长,超过这个时长,非核心线程就会被回收。
unit
用于指定keepAliveTime参数的时间单位。
workQueue
线程池中的任务队列,通过线程池的execute方法提交的Runnable对象会存储在这个参数中。
threadFactory
threadFactory是一个接口,它只有一个方法new Thread(Runnable r)。
ThreadPoolExecutor为RejectedExecutionHandler提供了几个可选值:
CallerRunsPolicy、AbortPolicy、DiscardPolicy和DiscardOldestPolicy,其中AbortPolicy是默认值。
11.3.2 线程池的分类
1. FixedThredPool
FixedThreadPool中只有核心线程并且这些核心线程没有超时机制,另外任务队列也是没有大小限制的。
2. CachedThreadPool
它是一种线程数量不定的线程池,它只有非核心线程,并且其最大线程数为Integer.MAX_VALUE.由于Integer.MAX_VALUE可以是一个很大的数,实际上可以相当于它可以是任意大。
3. ScheduledThreadPool
它的核心线程数量是固定的,而非核心数量是没有限制的,并且当非核心限制时会立即被回收。
4. SingleThreadExecutor
这类线程池内部只有一个核心线程,它确保所欲的任务都在同一个线程中按顺序执行。
第十二章 Bitmap 的加载和 Cache
12.1 Bitmap 的高效加载
如何加载一个图片?
BitmapFactory类提供了四类方法:decodeFile、decodeStream、decodeResource、decodeByteArray,其中decodeFile和decodeResource又间接调用了decodeStream方法。
如何高效地加载Bitmap呢?
其实核心思想也很简单,那就是采用BitmapFactory.Options来加载所需尺寸的图片。
通过BitmapFactory.Options来缩放图片,主要用到了它的inSampleSize参数,即采样率。
有一种特殊情况,那就是当insampleSize小于1的时候,其作用相当于1,即无缩放效果。另外最新的官方文档指出,inSampleSize的取值应该总是为2的指数,当不为2的指数时,系统就会向下取整。
获取采样率也很简单,遵循如下流程:
1. 将BitmapFactory.Options的inJustDecodeBounds参数设置为true并加载图片。
2. 从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数。
3. 根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize。
4. 将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。
这里说明一下inJustDecodeBounds参数,当此参数设为true时,BitmapFactory只会解析图片的原始宽/高信息,并不会去真正加载图片,所以这个操作是轻量级的。
12.2 Android 中的缓存策略
如何定义缓存的新旧这是一个策略,不同的策略就对应着不同的缓存算法。
12.2.1 LruCache
为了能够兼容Android 2.2版本,在使用LruCache时建议采用support-v4兼容包中提供的LruCache,而不要直接使用Android 3.1提供的LruCache。
LruCache是一个泛型类,它内部采用了一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作。
一些特殊情况下,还需要重写LruCache的entryRemoved方法,LruCache移除旧缓存时会调用entryRemoved方法,因此可以在entryRemoved方法中完成一些资源回收工作(如果需要的话)。
12.2.2 DiskLruCache
它通过将缓存对象写入文件系统从而实现缓存的效果。
1. DisLruCache 的创建。
如果应用卸载后就希望删除缓存文件,那么就选择SD卡上的缓存目录,如果希望保持缓存数据就选择SD上的其它目录。
2. DisLruCache 的缓存添加。
DiskLruCache的缓存添加操作是通过Editor来完成的,Editor表示一个缓存对象的编辑对象。这里仍然以图片缓存举例,首先需要获取图片url所对应的key,
然后根据key就可以通过edit()来获取Editor对象,如果这个缓存正在被编辑,那么edit()会返回null,即DisLruCache不允许同时编辑一个缓存对象。
通过Editor就可以得到一个文件输出流。
当从网络上下载图片时,图片就可以通过这个文件输出流写入到文件系统上。
经过上面的步骤,其实并没有真正将图片写入文件系统,还必须通过Editor的commit()来提交写入操作,如果图片下载过程发生了异常,那么还可以通过Editor的abort()来回退整个操作。
3. DisLruCache 的缓存查找
和缓存的添加过程类似,缓存查找过程也需要将url转换为key,然后通过DiskLruCache的get方法得到一个SnapShot对象,接着再通过SnapShot对象即可得到缓存的文件输入流。
通过BitmapFactory.Options来加载一张压缩后的图片,但是那种方法对FileInputStream的缩放存在问题,原因是FileInputStream是一种有序的文件流,而两次decodeStream调用影响了文件流的位置属性,这导致了第二次decodeStream得到的是null。为了解决这个问题,可以通过文件流来得到它所对应的文件描述符,然后再通过BitmapFactory.decodeFileDescriptor方法来加载一张压缩后的图片。
除此之外,DiskLruCache还提供了remove,delete等方法用于磁盘缓存的删除操作。
12.2.3 ImageLoader 的实现
一般来说,一个优秀的ImageLoader应该具备如下的功能:
1. 图片的同步加载。
2. 图片的异步加载。
3. 图片压缩。
4. 磁盘缓存。
5. 内存缓存。
6. 网路拉取。
ImageLoader的实现步骤:
1. 图片压缩功能的实现。
2. 内存缓存和磁盘缓存的实现。
在创建磁盘缓存是,这里做了一个判断,即有可能磁盘剩余空间小于磁盘缓存所需的大小,一般是指用户的手机空间已经不租了,因此没有办法创建磁盘缓存,这个时候磁盘缓存就会失效。
3. 同步加载和异步加载接口的设计。
通过检查当前线程的Looper是否是主线程的Looper来判断当前线程是否是主线程,如果是主线程则直接抛出异常终止程序。
如果直接采用普通的线程去直接加载图片,随着列表的滑动这有可能会产生大量的线程,这样并不利于整体效率的提升。
在这里选择了线程池和Handler来提供ImageLoader的并发能力和访问UI的能力。
为了解决View复用所导致的列表错位问题,在给ImageView设置图片之前会检查它的url有没有发生变化,如果变化了就不给它设置图片,这样就解决了列表错位问题。
12.3 ImageLoader 的使用
12.3.1 照片墙效果
12.3.2 优化列表的卡顿现象
首先,不要在getView中进行耗时操作。
其次,控制异步任务的执行频率。
如果用户刻意地频繁上下滑动,这就会在一瞬间产生上百个异步任务,如何解决这个问题呢?
可以考虑在列表滑动的时候停止加载图片,尽管这个过程是异步的,等列表停下来以后再加载仍然可以获得良好的用户体验。具体实现时,可以给ListView和GridView设置setOnScrollListener,并在OnScrollListener的onScrollStateChanged方法中判断列表是否处于滑动状态,如果是则停止加载图片。
一般来说,经过上述两个步骤,列表都不会有卡顿现象,但是在某些特殊情况下,列表还是会有偶尔的卡顿现象,这个时候还可以开启硬件加速。通过设置
android:hardwareAccelerated=“true"即可为Activity开启硬件加速。
第十三章 综合技术
CrashHandler可以用来监视应用的crash信息,给程序设置一个CrashHandler,这样当程序crash时就会调用程序的unCaughtException方法了。
在Android中有一个限制,那就是应用的方法数不能超过65536,,否则就会出现编译错误,并且程序也无法成功地安装到手机上。
Google提供了multidex用来专门解决这个问题,通过将一个dex文件拆分为多个dex文件来避免单个dex文件方法数越界的问题。
方法数解决的另一种方式是动态加载。动态加载可以直接加载一个dex形式的文件,将部分代码单独打包到一个dex文件中(也可以是dex格式的jar或者apk),并在程序运行时根据需要去动态加载dex中的类,这种方式既可以解决方法数越界的问题,也可以给程序提供按需加载的特性,同时还为应用按模块更新提供了可能性。
13.1 使用 CrashHandler 来获取应用的 Crash 信息
首先需要实现一个UncaughtExceptionHandler对象,在它的uncaughtException方法中获取异常信息将其存储到SD卡上或者将其上传到服务器供开发人员来分析,然后调用Thread的setDefaultUncaughtExceptionHandler方法将它设置为线程默认的异常处理器,由于默认的异常处理器是Thread类的静态成员,
因此它的作用对象是当前进程中的所有线程。
如何使用上面的CrashHandler呢?
可以选择在Application初始化的时候为线程其设置CrashHandler。
13.2 只用 multidex 来解决方法数越界
dexpot是一个程序,应用在安装时,系统会通过dexpot来优化dex文件,在应用优化的过程中dexpot采用一个固定大小的缓冲区来存储应用中所有方法的信息,这个缓冲区就是LinearAlloc。LinearAlloc在新版本Android中的大小是8MB或者16MB。而在Android 2.2 和Android 2.3 中只有5MB,当待安装的应用中的方法数比较多时,尽管它的方法数还没有超过65536,但是它的存储信息仍然有可能超过5MB,这种情况下dexpot就有可能报错,从而导致安装失败,这种情况主要在Android 2.x 的手机上出现。
为了解决这个问题,Google在2014年提出了multidex的解决方案,通过multidex可以很好地解决方法数越界的问题,并且很容易使用。
在Android 5.0 之前使用multidex需要引入Google提供的android-support-multidex.jar这个jar包,这个jar包可以在Android SDK目录下的extras/android/support/multidex/library/lib下面找到。
在AndroidStudio和Gradle编译环境中,如果想要使用multidex,首先要使用Android SDK Build Tools 21.1 及以上版本,接着修改工程中app目录下的build.gradle文件,在defaultConfig中添加multidexEnabled true这个配置选项。
接着还需要在dependencies中添加multidex的依赖。
最后在代码中加上支持multidex的功能,有三种方案可以选择。
第一种方案,在manifest文件中指定Application为MutilDexApplication。
第二种方案,让应用的Application继承MultiDexApplication。
第三种方案,重写Application的attachBaseContext方法,这个方法比Application的onCreate要先执行。
在有些情况下,可能需要指定主dex文件中要包含那些类,这个时候可以通过multi-dex-list选项来实现这个功能。在bulid.gradle文件中添加afterEvaluate区域,在multi-dex-list区域内部用afterEvaluate指定了那些是主dex要包含的类。
注意maindexlist.txt这个文件名是可以修改的,但是它的内容必须遵守一定的格式。
需要注意的是,multidex中的jar包中的9个类也必须打包到主dex中,否则程序运行时会抛出异常,告知无法找到multide相关的类。
multidex可能带来的问题:
1. 应用启动速度会降低,要避免生成过大的dex文件。
2. 由于Dalvik linearAlloc的bug,可能会导致multidex的应用无法再Android 4.0 以前的手机上运行,因此需要做大量的兼容性测试。同时由于Dalvik linearAlloc的bug,有可能出现应用在运行中由于采用了multidex方案从而产生大量的内存消耗的情况,这会让应用崩溃。
在实际的项目中,1是客观存在的,2出现的可能性极少。
13.3 Android 的动态加载技术
不同的插件化方案各有各的特色,但是它们必须都要解决三个基础性的问题:资源访问,Activity生命周期的管理,ClassLoader的管理。
宿主是指普通的apk,而插件是指经过处理的dex后者apk。
1. 资源访问。
插件化的目的就是要减小宿主程序apk包的大小,同时降低宿主程序的更新频率并做到自由装载模块。
由于addAssetPath是隐藏API我们无法调用,所以只能通过反射。
传递的路径可以是一个zip文件也可以是一个资源目录,而apk是一个zip,所以直接将apk的路径传给它,资源就加载到AssetManager中了。然后再通过AssetManager来创建一个新的Resources对象,通过这个对象我们就可以访问插件apk中的资源了。
2. Activity 生命周期的管理。
管理Activity生命周期的方式各种各样,这里只介绍两种:反射方式和接口方式。
反射方式的缺点:
反射代码写起来比较复杂,过多使用反射会有一定的性能开销。
3. 插件 ClassLoader 的管理
使同一个插件可以采用同一个ClassLoader去加载类,通过将不同插件的ClassLoader存储在一个HashMap中,这样就可以保证不同插件中的类彼此可以互不干扰。
13.4 反编译初步
13.4.1 使用 dex2jar 和 jd-gui 反编译 apk
Dex2jar是一个将dex文件转换为jar包的工具,然后jd-gui将jar包进一步转换为java代码。
q
13.4.2 使用 apktool 对 apk 进行二次打包
作用:反编译出apk中的二进制的数据资源或者二次打包。
需要注意的是,由于Windows系统的兼容性问题,有时候会导致apktool.bat无法再Windows的一些版本上正常工作,比如Windows 8,这个时候可以安装Cygwin,然后采用Linux的方式解压即可。apktool在Linux上的打包成功率要比Windows高。
smali是dex文件反编译的结果(不同于dex2jar反编译的结果)。smali有自己的语法可以修改,然后可以被二次打包为apk。
需要注意的是,apk经过二次打包后并不能直接安装,必须经过签名后才能安装。
在实际开发中,很多产品都会做签名校验,简单的二次打包后得到的山寨版apk安装后无法运行。尽管如此,还是可以通过修改smali的方式来绕过签名校验,这就是市面中仍然还有这么多山寨版应用的原因。
第十四章 JNI 和 NDK 编程
Java JNI的本意是Java Native Interface(Java本地接口),它是为了Java方便调用C、C++等本地接口时所封装的一层接口。
NDK是Android所提供的一个工具集合,通过NDK可以在Android中更加方便地通过JNI来访问本地代码。
NDK还提供了交叉编译器,开发人员只需要简单地修改mk文件就可以生成特定平台的CPU动态库。使用NDK有如下好处:
1. 提高程序的安全性,由于so文件反编译比较困难,因此NDK文件提高了Android程序的安全性。
2. 可以很方便地使用目前已有的C/C++开源库。
3. 便于平台间的移植。
4. 提高程序在某些特定情形下的执行效率,但是不能明显提升Android程序的性能。
JNI和NDK主要用于底层开发,在Android的应用层开发比较少,本身更加侧重于C/C++方面的编程。
14.1 JNI 的开发流程
1. 在Java中声明native方法。
2. 编译Java文件得到class文件,然后通过javah命令导出JNI的头文件。
JNIEnv*:表示一个指向JNI环境的指针,可以用它来访问JNI提供的接口方法。
jobject:表示java对象中的this。
JNIEXPORT和JNICALL:它们是JN中定义的宏,可以再jni.h这个头文件中查找到。
3. 实现JNI的方法。
JNI方法是Java中声明的native方法,可以选择采用C++/C来实现。
4. 编译so库并在Java中调用。
libjni-test.so是生成的so库的名字,在Java中可以通过如下方式加载:System.LoadLibrary("jni-test"),其中so库的名字中的lib和.so是不需要指出的。
14.2 NDK 的开发流程
1. 下载并配置NDK。
2. 创建一个Android项目,并声明所需的native方法。
3. 实现Android项目中所声明的native方法。
Application.mk中常用的配置是APP_ABI,它表示CPU的架构平台的类型,目前市面上常见的架构平台有armeabi、x86和mips,其中在移动设备中占据主要地位的是armeabi,这也是大部分平台只包含armeabi类型的so库的原因。
4. 切换到jni的父目录,然后通过ndk-build命令编译产生so库。
需要注意的是,ndk-bulid默认指定jni目录为本地源码的目录,如果源码存放的目录名不是jni,那么ndk-bulid则无法成功编译。
14.3 JNI 的数据类型和类型签名
类的签名比较简单,它采用“L+包名+类名+;"的形式,只需要将其中得到.替换为/即可。比如java.lang.String,它的签名为Ljava/lang/String;,注意末尾的;也是签名的一部分。
对于对象来说,它的签名就是所属类的签名,比如String对象,就是Ljava/lang/String。
对于多维数组来说,它的签名就是n个[+类型签名,其中n表示数组的维度。
方法的签名为(参数类型签名)+返回值类型签名。
14.4 JNI 调用 Java 方法的流程
JNI调用Java方法的流程是先通过类名找到类,然后再根据方法名找到方法的id,最后就可以调用这个方法了。如果是调用Java中的非静态方法,那么需要构造出类的对象后才能调用它。
第十五章 Android 性能优化
过多地使用内存会导致程序内存溢出,即OOM,而过多地使用CPU资源,一般是指做大量的耗时任务,会导致手机变得卡顿甚至应用程序无法响应的情况,即ARN。
15.1 Android 的性能优化方法
15.1.1 布局优化
如果布局中既可以使用LinearLayout也可以使用RelativeLayout,那么就采用LinearLayout,这是因为RelativeLayout的功能比较复杂,它的布局过程需要花费更多CPU的时间。
<include>标签
<include>标签只支持android:layout开头的属性,不如android:layout_width、android:layout_height,其它属性是不支持的,如果android:background。当然,android:id这个属性是个特例。
需要注意的是,如果<include>标签指定了android:layout_*这种属性,那么哟啊球android:layout_widht、android:layout_height必须存在,否则其它android:layout_*形式的属性无法生效。
<merge>标签
如果当前布局的方向是一个竖直方向上的LinearLayout,并且它所包含的布局中的方向也是一个竖直方向上的LinearLayout,那么可以使用<merge>标签,这样就可以减少布局间的层级。
ViewStub
ViewStub的作用在于按需加载所需的布局。
注意:目前ViewStub暂时不支持<merge>标签。
15.1.2 绘制优化
首先,在onDraw中不要创建新的局部对象。
另一方面,onDraw方法中不要做耗时操作,也不能执行成千上万的循环操作。
View的绘制频率保持在60fps时是最佳的,这就要求每帧的绘制时间不超过16ms(16ms=1000/60)。
15.1.3 内存泄漏优化
内存泄漏的优点分为两个方面,一方面是在开发中避免写出有内存泄漏的代码,另一方面是通过一些分析工具比如MAT来找出潜在的内存泄漏继而解决。
场景1:静态变量导致的内存泄漏。
场景2:单例模式导致的内存泄漏。
单例模式的特点就是其生命浊气的Application的生命周期保持一致。
场景3:属性动画导致的内存泄漏。
解决方法是在Activity的onDestroy方法中通过animator.cancel()来停止动画。
15.1.4 响应速度优化和ANR日志分析
响应速度过慢更多地体现在Activity的启动速度上面,如果在主线程中做太多事情,会导致Activity启动时出现黑屏现象,甚至出现ANR。
当一个进程发生了ANR以后,系统会在data/anr目录下创建一个traces.txt,通过分析这个文件就能定位出ANR的原因。
15.1.5 ListView 和 Bitmap 的优化
注意ListView的优化策略完全适应于GridView。
15.1.6 线程优化
15.1.7 一些性能优化建议
1. 避免创建过多的对象。
2. 不要过多使用枚举,枚举占用的内存空间要比整形大。
3. 常量请使用static final来修饰。
4. 使用Android中特有的一些数组结构,比如SparseArray和Pair等,它们都具有更好的性能。
5. 适当使用软引用和弱引用。
6. 尽量地使用静态内部类,这样可以避免潜在的由于内部类而导致的内存泄漏。
15.2 内存泄漏分析值 MAT 工具
通过Histogram可以很直观地看出内存中不同类型的buffer数量和占用的内存大小。而Dominator Tree则把内存中的对象按照从大到小的顺序进行排序,并且可以分析对象之间的引用关系,内存泄漏分析就是通过Dominator Tree来分析的。
15.3 提供程序的可维护性
1. 私有成员以m开头,静态成员以s开头,常量则全部用大写字母来表示。
2. 代码的排版上要留出合理的空白来区分不同的代码块,其中同类变量的声明要留在一组,两类变量之间要留出一行空白用来作区分。
3. 仅为非常关键的地方添加注释,其他地方不写注释。
4. 单一职责和层次性相关联的,代码分层后,每一层仅仅关注少量的逻辑,这样就做到了单一职责。