文章目录
- 一. Android开发艺术探索
- 1. Activity的生命周期和启动模式
- 2. IPC机制
- 3. View的事件体系
- 4. View的工作原理
- 5. RemoteViews
- 6. Drawable
- 7. Android动画深入分析
- 8. 理解Window和WindowManager
- 9. 四大组件的工作过程
- 10. Android的消息机制
- 11. Android的线程和线程池
- 12. Bitmap的加载和Cache
- 13. 综合技术
- 14. JNI和NDK编程
- 15. Android性能优化
- 二. Kotlin核心编程
一. Android开发艺术探索
1. Activity的生命周期和启动模式
1.1 生命周期全面分析
- onPause和onStop
onPause后会快速调用onStop,极端条件下直接调用onResume
当用户打开新的activity,且用了透明色的主题,则不会用onStop
- 当前Activity为A,如果用户打开一个新的activity B,是先执行A的onPause还是B的onResume
结论:先A onPause再B onResume,Android5.0源码必须先栈顶的元素onPause后,新activty才能启动
涉及到Activty启动原理,暂时不深入探讨,包括Instrumentation,ActivtyThrea,ActivityManagerService(AMS)。简单理解如下:启动Activty的请求会由instrumentation处理,然后它通过binder向AMS发请求,AMS里面有一个ActivtyStack负责栈内activty的状态同步,AMS通过ActivtyThread去同步Activty状态并完成生命周期的调用
- 资源相关的系统配置发生改变导致Activity被杀死并重新创建
onSaveInstance:activity被异常终止时调用,在onStop之前,可能onPause调用的前后
onRestoreInstanceState:activity重新创建时调用
保存和恢复view的层级结构:activity委托window,委托顶层容器(一个viewGroup,通常时DecorView),一一遍历子容器
(委托思想)
- 资源不足导致低优先级的Activity被杀死
优先级:1.前台activity;2.可见但非前台(比如activity中弹出了一个对话框);3.后台activity(比如执行了onStop) - 让acitivity不重建的方法
比如不想让旋转屏幕重建,则:
android:configChanges="orientation"
onConfigurationChange: 是 Activity 类中的一个回调方法,它在设备的配置发生变化时被调用,例如屏幕方向变化、语言变化、字体大小变化等。这个方法使开发者能够处理这些变化,而无需重启活动
1.2 Activity的启动模式LaunchMode
- 标准模式
1.Activity A启动B,B就会进入A所在的栈。
2.如果用ApplicationContext启动activity就会报错,
解决办法:为新启动的activity指定标志位FLAG_ACTIVITY_NEW_TASK。
3.create/start/resume方法都会调用 - singleTop栈顶复用
create/start不会调用,调用onNewIntent方法 - singleTask栈内复用
回调onNewIntent,具有clearTop方法。比如栈S1:ADBC,启动D后,为AD - singleInstance单实例模式
- 特殊情况1:前台任务栈和后台栈
- 任务栈定义
taskAffinity 是一个用于定义活动(Activity)所在任务(Task)归属的属性。
要设置活动的 taskAffinity,可以在 AndroidManifest.xml 中为每个活动指定 android:taskAffinity 属性,不设置默认为包名。 - TaskAffinity与SignleTask联动
taskaffinity是具有该模式的activity的目前任务栈的名字,待启动的activity会运行在名字和taskAffinity相同的任务栈中 - TaskAffinity和allowTaskReparenting联动的特殊效果
应用A启动了应用B的Activity C后,按Home键回到桌面,再进入应用B。此时C运行在A的任务栈,但属于B - 给Activity指定启动模式
优先级:2>1
- 特殊情况2:前台任务栈和后台栈
核心结论:singleTask模式Actvity切换到栈顶会导致在它之上的站内的acitivty出栈 - Flags
1.3 IntentFilter的匹配规则
- 显示调用和隐式调用
显示调用:指明了activity的名称;隐式:需要匹配initentFilter - IntentFilter
包含actiion,category,data三个信息,可以有多个,必须同时三个信息都匹配才算完全匹配;也可以有多个IntentFilter,匹配其中任何一组intent-filter即可 - action匹配规则
字符串类型,区分大小写
只要Intent中的Action能够和过滤规则的任何一个action相同即可匹配成功
没有指定action,会失败 - category匹配规则
Intent中出现了category,不管有几个,每一个都必须是过滤规则里定义了的category。
Intent中也可以没有category,也可以匹配成功,系统会默认加上default类型(因此,过滤规则必须加上这一条) - data匹配规则
类似与action - 规避隐式匹配失败时报错
PackageManager -> resolveActivity 或者 queryIntentActivities
2. IPC机制
2.1 IPC简介
Inter-Process Communication 进程间通信或者跨进程通信;
Android 系统进程的通信方式:Binder,Socket
ContentProvider就是跨进程通信
2.2 Android中的多进程模式
- 开启多进程模式
指定android:progress - 查看进程
adb shell ps
adb shell ps | grep 包名
DDMS视图工具 - 多进程带来的问题
静态成员和单例模式完全失效
线程同步机制完全失效
SharedPreferences可靠性下降
Application多次创建
2.3 IPC基础概念介绍:Binder
- 从不同角度理解Binder
Android中的一个类,实现了IBinder;
IPC中一种跨进程通信方式;
虚拟物理设备;
从Android Framwork角度看,是ServiceManager连接各种Manager和相ManagerService的桥梁;
从应用层角度看,是客户端和服务端进行通信的媒介 - AIDL
AIDL(Android Interface Definition Language)是Android平台中用于实现进程间通信(IPC)的一种接口定义语言。通过AIDL,开发者可以定义服务和客户端之间的交互方式,使得不同进程可以相互调用方法。
当你在 Android 项目中定义了 AIDL 文件后,Android 构建系统会自动生成相应的 Java 文件,这些文件包含了用于进程间通信的 Binder 类和接口
使用AIDL的基本步骤包括:
定义AIDL接口:在.aidl文件中定义你想要暴露给其他进程的方法和类型。
实现AIDL接口:在服务端实现这个接口,并处理来自客户端的请求。
绑定服务:客户端通过bindService()方法与服务进行连接,获取服务的代理对象。
调用方法:客户端通过代理对象调用服务端的方法。 - Binder工作机制
当客户端发起请求时,当前线程会被挂起,因此耗时的远程方法不能在UI线程中发起请求 - 内部类Stub
本质是一个Binder类,当客户端和服务端在同一进程时,方法调用不会走跨进程的transact过程,若处于不同进程,则走transact过程,该逻辑由Stub的内部代理类Porxy来完成 - Descriptor
Binder的唯一标识 - asInterface
将服务端的Binder对象转换为客户端所需要的AIDL接口类型的对象,若二者处于同一进程,则返回服务端的Stub本身;若不处于同一进程,则返回封装后的proxy对象 - asBinder
返回当前的Binder对象 - onTransact
客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。 - 不需要通过AIDL也可以手动实现Binder
- linkToDeath和unlinkToDeath
服务端如果由于某种原因异常终止,会导致远程调用失败。
在客户端绑定到服务时,使用 linkToDeath 可以确保在服务进程崩溃或被杀死时,客户端能够做出反应,比如重新绑定服务或者释放资源。
在客户端或服务不再需要接收死亡通知时,应调用此方法解除绑定。
2.4 Android的IPC方式 (后续再深入了解)
-
使用Bundler
-
使用文件共享
适用于对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读写问题 -
使用Messenger
对AIDL进行了封装,只能处理串行任务
-
使用AIDL
基于 Binder:AIDL 是构建在 Binder 之上的,它本质上是对 Binder 机制的一种封装,使得进程间通信的开发更加简单和高效。
数据传输:AIDL 通过 Binder 对象传输数据,开发者只需要关注接口的定义,而 Binder 处理实际的通信细节。 -
使用ContentProvider
底层是Binder
定义一个ContentProvider:
写一个类继承ContentProvider,实现六个抽象方法;注册该provider
在外部通过getContentResolver().query()等方法执行 -
使用Socket
声明权限:
不能在主线程访问网络
适用于实时数据传输、聊天应用等场景。
2.5 Binder连接池
如果是每个AIDL就需要一个Service会导致应用重量化
2.6 选择合适的IPC方式
3. View的事件体系
3.1 View基础知识
- View和ViewGroup
ViewGroup继承了View,例子:LinearLayout是一个ViewGroup也是一个View,ViewGroup内部有子View - 位置参数
x = left + translationX (x是View左上角的坐标,translationX是View左上角相对于父容器的偏移量) - MotionEvent
通过MotionEvent可以得到点击事件发生的X和Y坐标,getX/getY返回的是相对于当前View的左上角的x和y坐标,getRawX/getRawY相对于手机屏幕左上角的坐标 - TouchSlop
系统所能识别的被认为是滑动的最小距离;
通过ViewConfiguration.get(getContext()).getScaledTouchSlop()获取 - VelocityTracker
// 在View的onTouchEvent方法中追踪当前单击事件的速度
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event)
// 获取速度
velocityTracker.computeCurrentVelocity(1000); //1000表示时间间隔为1000ms
int xVelocity = (int) velocityTracker.getXVelocity()
// 需要回收内存
velocityTracker.clear()
velocityTracker.recyle()
- GestureDetector
手势检测, 用于辅助检测单击,滑动,长按,双击等行为。
在实际开发中,也可以选择在View的onTouchEvent方法中实现所需要的监听,建议如果是滑动相关选择前者;如果是双击,选择GestureDetector
- Scroller
实现弹性滑动
3.2 View的滑动
- 使用View本身的scrollTo/scrollBy方法
public void scrollTo(int x, int y) {
// 检查目标位置 (x, y) 是否与当前滚动位置 (mScrollX, mScrollY) 不同
if (mScrollX != x || mScrollY != y) {
// 保存当前的滚动位置,以便后续使用
int oldX = mScrollX;
int oldY = mScrollY;
// 更新当前的滚动位置为目标位置 (x, y)
mScrollX = x;
mScrollY = y;
// 更新父视图的缓存,以确保它们反映新的滚动位置
invalidateParentCaches();
// 通知视图滚动发生变化,传递新的和旧的滚动位置
onScrollChanged(mScrollX, mScrollY, oldX, oldY);
// 尝试唤醒滚动条,如果未成功,则请求重绘视图
if (!awakenScrollBars()) {
postInvalidateOnAnimation(); // 在下一个动画周期内请求重绘
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
mScrollX和mScrollY指View的左(上)边缘和View内容左(上)边缘在水平(竖直)方向上的距离
View左边缘在View内容左边缘的右边,mScrollX为正值
这两个方法只能移动View的内容,不能移动View本身
下图灰色为View的内容
- 通过动画给View施加平移效果
方法1:采用View动画的代码
实际上不能真正改View的位置参数,需要将参数fillAfter设置为true,在动画结束后才会保留状态;
另外如果是Button,即使设置该参数后,点击事件也会失效
方法2:属性动画
但需要android3.0以上
- 改变View的LayoutParams参数
3.3 View的弹性滑动
核心思想:将一次大的滑动分成若干次小的滑动,并在一个时间段内完成
- Scroller
Scroller
是 Android 中用于实现平滑滑动效果的一个类,常用于实现弹性滑动(如惯性滚动)。下面是Scroller
实现弹性滑动的原理和相关细节:
1. 基本概念
Scroller
主要用于计算滚动动画的插值。它允许开发者根据时间推移计算出适当的滚动位置,以实现流畅的视觉效果。弹性滑动通常指的是用户在滑动过程中,手指离开屏幕后,视图继续滚动一段距离,模拟物理惯性。
2. 核心原理
Scroller
的弹性滑动效果主要通过以下几个步骤实现:
a. 初始化
在使用 Scroller
时,通常会在构造方法中初始化一个 Scroller
实例,并设置其滚动参数,包括起始位置、结束位置、持续时间、加速度等。
Scroller scroller = new Scroller(context);
b. 计算滚动
使用 startScroll()
或 fling()
方法启动滑动效果:
-
startScroll(int startX, int startY, int dx, int dy, int duration):
- 启动平滑滚动,从
startX
和startY
开始,滚动dx
和dy
的距离,持续时间为duration
毫秒。
- 启动平滑滚动,从
-
fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY):
- 启动"抛掷"滑动,基于给定的初始速度(
velocityX
和velocityY
),滚动到边界minX
、maxX
、minY
、maxY
。
- 启动"抛掷"滑动,基于给定的初始速度(
c. 插值计算
在每一帧的绘制过程中,可以通过 computeScrollOffset()
方法计算当前的滚动偏移量。该方法返回一个布尔值,表示滚动是否完成,并通过 getCurrX()
和 getCurrY()
获取当前的滚动位置。
if (scroller.computeScrollOffset()) {
int currX = scroller.getCurrX();
int currY = scroller.getCurrY();
// 更新视图的位置
}
3. 弹性效果
- 惯性滑动:当用户快速滑动时,
fling()
方法会根据滑动速度计算出合理的滚动路径,并使视图继续滚动,模拟惯性效果。 - 边界反弹:在达到边界时,可以通过判断当前滚动位置与边界的关系,实现反弹效果。例如,当视图达到上边界时,可以将 Y 轴位置设置为最大值,然后反向滚动。
4. 示例代码
下面是一个简单的示例,展示如何使用 Scroller
实现弹性滑动:
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
// 获取当前滚动位置
int currX = scroller.getCurrX();
int currY = scroller.getCurrY();
// 更新视图的位置
scrollTo(currX, currY);
// 请求重绘
postInvalidate();
}
}
public void startScrolling() {
// 启动平滑滚动
scroller.startScroll(startX, startY, dx, dy, duration);
}
public void fling(int velocityX, int velocityY) {
scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
}
总结
通过 Scroller
类,Android 提供了一个简单而强大的方式来实现平滑的弹性滑动效果。开发者可以利用 Scroller
来处理滑动和滚动动画,实现良好的用户体验。
- 动画
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "translationX", 0f, 100f);
animator.setDuration(300);
animator.start();
- 延时策略
private Handler mHandler = new Handler() { // 创建一个 Handler 对象,用于处理消息并执行 UI 操作
public void handleMessage(Message msg) { // 重写 handleMessage 方法,当消息到达时调用该方法
switch(msg.what) { // 根据消息的 what 值判断执行不同的操作
case MESSAGE_SCROLL_TO : { // 处理消息类型为 MESSAGE_SCROLL_TO 的情况
**mCount++; // 计数器自增,表示动画进度
if (mCount <= 30 ) { // 判断是否达到动画的最大帧数
float fraction = mCount / (float) 30; // 计算当前进度(0 到 1 之间)
int scrollX = (int) (fraction * 100); // 计算滚动的距离(0 到 100 像素)
mButton.scrollTo(scrollX, 0); // 更新按钮在 X 轴上的位置**
mHandler.sendEmptyMessageDelayed(1, 33); // 发送延迟消息,33 毫秒后继续更新
} // 如果 mCount 大于 30,则不再执行滚动
break; // 结束当前 case 的处理
}
default: // 默认情况下的处理
break; // 不执行任何操作
}
}
}
3.4 View的事件分发机制
参考资料 Android事件分发机制浅析
1. 点击事件传递规则
事件分发的三个重要方法
dispatchTouchEvent(MotionEvent event)
用来进行事件的分发,如果事件能传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法影响,表示是否消费当前事件。
onInterceptTouchEvent(MotionEvent ev)
在上述方法内部被调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
onTouchEvent(MotionEvent ev)
用来处理点击事件,在dispatchTouchEvent()方法中进行调用。返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
/**
- 点击事件产生后
*/
// 步骤1:调用dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false; //代表 是否会消费事件
// 步骤2:判断是否拦截事件
if (onInterceptTouchEvent(ev)) {
// a. 若拦截,则将该事件交给当前View进行处理
// 即调用onTouchEvent ()方法去处理点击事件
consume = onTouchEvent (ev) ;
} else {
// b. 若不拦截,则将该事件传递到下层
// 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程
// 直到点击事件被最终处理为止
consume = child.dispatchTouchEvent (ev) ;
}
// 步骤3:最终返回通知 该事件是否被消费(接收 & 处理)
return consume;
}
点击事件产生后,首先会传递给根ViewGroup,这个时候它的dispatchTouchEvent就会被调用,若此时这个ViewGroup的onIterceptTouchEvent方法返回true,则表示当前ViewGroup要拦截这个事件,接着这个事件就会交给此ViewGroup进行处理,即它的onTouchEvent方法就会被调用。
若onInterceptTouchEvent方法返回false,则表示当前ViewGroup不拦截这个事件,这时当前事件就会继续传递给它的子元素,接着子元素dispatchTouchEvent方法就会被调用,如此直到事件被最终处理。
当一个View需要处理事件时,如果它设置了onTouchListener,那么onTouchListener中的onTouch方法会被回调。这时事件如何处理还要看onTouch的返回值,如果返回false,则当前View的onTouchEvent方法会被调用,如果返回true,那么onTouchEvent方法将不会被调用。由此可见,给View设置的onTouchListener,其优先级比onTouchEvent要高。
在onTouchEvent方法中,如果当前设置的有onClickListener,那么它的onClick方法会被调用。可以看出,平时我们常用的onClickListener,其优先级更低,即处于事件传递的尾端。
2. 事件分发的流程
当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View,即事件总是先传递给Activity,Activity再传递给Window,最后Window再传递给顶级View。顶级View接收到事件后,就会按照事件分发机制去分发事件。
考虑一种情况,如果一个view的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依此类推,如果所有的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent方法会被调用。(程序员世界里的能力强弱问题,难题由上而下的分配,解决不了,交给上级解决)
3. Activity对点击事件的分发过程
点击事件用MotionEvent来表示,当一个点击操作发生的时候,事件最先传递给Activity,由Activity的dispatchTouchEvent来进行事件的派发,具体的工作是由Activity内部的window来完成的,window会将事件传递给decor view,decor view一般都是当前界面的底层容器(setContentView所设置的父容器),通过Activity.getWindow.getDecorView()获得
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//事件交给Activity所依附的window,如果true那就结束了
//superdispatchTouchEvent(ev)方法也是抽象的,必须找到window的实现类,window的实现类是phonewindow,phoneWindow将事件传递给了DecorView。
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
//phoneWindow将事件传递给了DecorView.
public boolean superDispatchTouchEvent(MotionEvent ev){
return mDecor.superDispatchTouchEvent(ev);
}
//从这里开始,事件已经传递到顶级View了,就是在Activity中通过setContentview所设置的View,另外顶级View也叫根View,顶级View一般来说都是VewGroup。
public class DecorView extends FrameLayout implements RootViewSurfaceTaker {
private DecorView mDecor;
@Override
public final View getDecorView(){
if(mDecor == null){
installDesor():
}
return mDecor;
}
}
4. ViewGroup对点击事件的分发过程 (viewGroup本身处理)
点击事件达到顶级view(一般是一个viewGroup)以后,会调用viewgroup的diapatchtouchevent方法,如果viewGroup拦截事件即onInterceptTouchEvent返回true,则事件由viewGroup处理,这是如果viewGroup的ontouchlistener被设置了,则onTouch会被调用,如果onTouch返回true,就会屏蔽掉onTouchEvent,如果返回false,会接着执行OnTouchEvent
// Check for interception.
final boolean intercepted;
//这里检查是否拦截事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}
-
ViewGroup在两种情况下都会判断是否要拦截当前事件:
事件类型为ACTION_DOWN:此前由我们触发的点击事件,也就是说ACTION_MOVE 和ACTION_UP事件来时,则不触发拦截事件
mFirstTouchTarget != null:当ViewGroup不拦截事件并将事件交给子View的时候该不等式成立。反过来,事件被ViewGroup拦截时,该不等式不成立 -
FLAG_DISALLOW_INTERCEPT 标记位
这个标记位是通过 requestDisallowInterceptTouchEvent方法来设置的,一般用于子 View FLAG_DISALLOW_INTERCEPT 一旦设置后, ViewGroup 将只能拦截ACTION_DOWN事件
原因是:因为 ViewGroup 在分发事件时,如果是ACTION_DOWN 就会重置 FLAG_DISALLOW_INTERCEPT 这个标记位,将导致子 View 中设置的这个标记位无效。 -
核心结论
-
当ViewGroup决定拦截事件后,那么后续的点击事件将会默认交给它处理并且不再调用它的onlnterceptTouchEvent 方法。
某个View一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件序列能够传递给它的话),并且它的onInterceprTouchEvent不会再被调用。这条也很好理解,就是说当一个View决定拦截一个事件后,那么系统会把同一个事件序列内的其他方法都直接交给它来处理,因此就不用再调用这个View的onInterceptTouchEvent去询问它是否要拦截了。
onlnterceptTouchEvent不是每次事件都会被调用的,如果我们想提前处理所有的点击事件,要选择dispatchTouchEvent方法,只有这个方法能确保每次都会调用,当然前提是事件能够传递到当前的 ViewGroup; -
FLAG_DISALLOW_INTERCEPT这个标志的作用是让 ViewGroup不再拦截事件,当然前提是ViewGroup 不拦截 ACTION_DOWN 事件。
事件传递过程是由外到内的,理解就是事件总是先传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterptTouchEvent方法可以在子元素中干预元素的事件分发过程,但是ACTION_DOWN除外。
5. ViewGroup对点击事件的分发过程 (ViewGroup 不拦截事件的时候,事件会向下分发交由它的子 View 进行处理)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
......
final View[] children = mChildren;
//遍历所有子View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
//判断子View是否能接收点击事件
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//判断子元素在播放动画时落在子元素的区域内
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//判断子元素点击事件是否落在子元素的区域内
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//事件传递到子View,下面追踪该方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// 完成了 mFirstTouchTarget 的赋值并终止对子元素的遍历
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
......
}
viewGroup直接使用for遍历所有子View,对子View的各种状态进行判断,最后调用dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)将事件传递给子View,下面是dispatchTransformedTouchEvent()方法的部分源码:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
// focus-1
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// 最后部分
// child 传递的不是null,因此它会直接调用子元素的 dispatchTouchEvent 方法,
// 这样事件就交由子元素处理了,从而完成了一轮事件分发。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
-
mFirstTouchTarget
mFirstTouchTarget 是一个指向某个视图的引用,通常是一个链表或列表中的第一个元素。它表示当前接收到触摸事件的第一个视图目标。一旦确定了 mFirstTouchTarget,该视图将处理后续的触摸事件,包括 ACTION_MOVE 和 ACTION_UP 事件。 -
如果遍历所有的子元素后事件都没有被合适地处理,这包含两种情况:
第一种是 ViewGroup 没有子元素;
第二种是子元素处理了点击事件,但是在 dispatchTouchEvent 中返回了 false,这一般是因为子元素在 onTouchEvent 中返回了 false,在这两种情况下,ViewGroup 会自己处理点击事件
// Dispatch to touch targets.
// 这里第三个参数 child 为 null,从前面的分析可以知道,它会调用 super.dispatchTouchEvent(event),
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}
6.View对点击事件的处理
- dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
......
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//这里开始判断
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
......
return result;
}
首先判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener的优先级高于onTouchEvent,这样的好处是方便在外界处理点击事件。
- onTouchEvent
public boolean onTouchEvent(MotionEvent event) {
......
//当View处于不可用状态下,也会消耗点击事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
......
//对点击事件的具体处理
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
......
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
......
}
}
return true;
}
......
}
只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true,不管它是不是DISABLE状态。
3.5 滑动冲突处理
1.常见的滑动冲突的场景
- 场景1–外部滑动方向和内部滑动方向不一
典型场景ViewPager和Fragment结合,ViewPagre内部做了处理 - 场景2–外部滑动方向和内部滑动方向一致
- 场景3–上面两种情况的嵌套
外层SlideMenu,内部ViewPager,ViewPager页面又有一个ListView
2.滑动冲突的处理规则
- 对于场景1,根据滑动是水平滑动还是竖直滑动来判断到底由谁来拦截事件。
如何判断滑动方向?可以通过水平和竖直方向的距离差来判断,比如竖直方向的滑动距离大就判断为竖直滑动,否则判断为水平滑动。 - 对于场景2,根据业务规则来决定由谁拦截事件。
- 对于场景3,根据业务规则来决定由谁拦截事件。
3.滑动冲突的解决方式
- 外部拦截法
处理场景1,点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截,这样就可以解决滑动冲突的问题,这种方法比较符合点击事件的分发机制。外部拦截法需要重写父容器的onInterceptTouchEvent方法,在内部做相应的拦截即可。伪代码如下:
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事件时必须返回false
// 因为如果拦截ACTION_DOWN的话,接下来的事件都会交给父容器处理,子View则没有机会收到事件,更谈不上处理事件。
//在拦截ACTION_UP事件也必须返回false
// 因为父容器拦截ACTION_UP事件没有意义,一旦返回true(拦截),子View就不能响应ACTION_UP事件,进一步导致子View的Click事件不能响应。
- 内部拦截法
父容器不做拦截,直接传递给子View处理事件。如果符合子View的滑动方式,就消耗这个事件,否则交回给父容器处理。
主要利用了子View设置父容器的一个标志位FLAG_DISALLOW_INTERCEPT,是否让父容器拦截事件。子View拦截ACTION_DOWN事件时,设置让父容器不能拦截事件。在ACTION_MOVE判断是否符合自己的滑动规则,如果不符合,允许父容器拦截事件。
requestDisallowInterceptTouchEvent() 是 Android 中 View 类的一个方法,主要用于处理触摸事件中的拦截逻辑。它的主要作用是告诉父视图在当前触摸事件期间不应该拦截此事件。
它的伪代码如下,我们需要重写子元素的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event){
boolean intercepted = false;
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 super.dispatchTouchEvent(event);
}
父元素也要默认拦截除了ACTION_DOWN以外的其它事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件
public boolean onInterceptTouchEvent(MotionEvent event){
int action = event.getAction();
if(action==MotionEvent.ACTION_DOWN){
return false;
}
else{
return true;
}
}
4. View的工作原理
4.1 ViewRoot和DecorView
- ViewRoot
定义: ViewRoot对应于ViewRootImpl。ViewRootImpl 是 View 的最高层级,是所有 View 的根。ViewRootImpl 实现了 View 和 WindowManager 之间所需要的协议。
初始化: View 的三大流程都是通过 RootViewImpl 来完成的,在 ActivityThread 中,当 Activity 对象被创建完毕后,在 onResume 后,就会通过 WindowManager 将 DecorView 添加到窗口上,在这个过程中会创建 ViewRootImpl (addView方法中创建)
ViewRootImpl:视图层次结构的顶部。一个 Window 对应着一个 ViewRootImpl 和 一个 VIew。这个 View 就是被 ViewRootImpl 操作的。
View的绘制流程是从ViewRoot的performTraversals方法开始,经过measure,layout,draw得到view
- Window
我们知道界面中所有的元素都是由 View 构成的,View 是依附于 Window 上面的。Window 只是一个抽象概念,把界面抽象成一个 窗口,也可以抽象成一个 View。 - ViewManange
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
-
WindowManager
也是一个接口,继承自 ViewManager,在应用程序中,通过 WindowManager 来管理 Window,将 View 附加到 Window 上。他有一个实现类 WindowManagerImpl。 -
WindowManagerGlobal
WindowManagerGlobal 是一个单例,也就是说一个进程中只有一个 WindowManagerGlobal对象,他服务与所有页面的 View。 -
ViewParent
一个接口,定义了将成为 View 父级类的职责。 -
DecorView
其实就是一个FrameLayout,内部有一个竖直方向的linearLayout,包含标题栏和内容(具体视不同的android版本)
4.2 MeasureSpec
1. 基本定义
-
MeasureSpec 是 Android 中用于测量视图尺寸的一个类,它主要用于在布局过程中确定视图的大小。
-
MeasureSpec 结构由两部分组成:Size一个整数值,表示测量的尺寸(像素),Mode一个整数值,表示测量模式
-
常见的测量模式
UNSPECIFIED : 父容器没有限制子视图的大小,子视图可以任意大小。表示当前 View 的尺寸不受父 View 的限制,想要多大就可以多大。这种情况下,SIZE 的值意义不大。一般来说,可滑动的父布局对子 View 施加的约束就是 UNSPECIFIED ,比如 ScrollView 和 RecyclerView。在滑动时,实际上是让子 View 在它们的内部滚动,这意味着它们的子 View 的尺寸要大于父 View,所以父 View 不应该对子 View 施加尺寸的约束。
EXACTLY: 父容器确定了子视图的大小,子视图必须符合这个大小。
AT_MOST: 父容器给了子视图一个最大大小,子视图可以小于或等于这个大小。 -
MeasureSpec常用方法
makeMeasureSpec(int size, int mode): 这个静态方法用于创建一个新的 MeasureSpec 对象,它结合了大小和模式。
getMode(int measureSpec): 这个方法解析 MeasureSpec,返回相应的测量模式。
getSize(int measureSpec): 这个方法解析 MeasureSpec,返回对应的大小。
2.MeasureSpec与LayoutParams的关系
- DecorView:.MeasureSpec由窗口尺寸和自身的LayoutParamas决定
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension){
case ViewGroup.LayoutParams.MATCH_PARENT:
mesureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.AT_MOST);
break;
default:
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension,MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
// LayoutParams.MATCH_PARENT:精确模式,大小就是窗口大小;
// LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小;
// 固定大小:精确模式,大小为LayoutParams中指定的大小。
- 普通View:MeasureSpec由父容器MeasureSpec和自身的LayoutParamas决定
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
//在不同的父 View 的 MeasureSpec.MODE 下,当子 View 的尺寸分别为具体值、MATCH_PARENT 和 WRAP_CONTENT 的时候,
//计算出子 View 的 MeasureSpec 并返回。所以一共有 3 x 3 = 9 种情况
- 问题1:当一个 View 的尺寸设置为 WRAP_CONTENT 时,它的 MeasureSpec.MODE 就是 AT_MOST吗?
- 问题2:需要一个 WRAP_CONTENT 的 RecyclerView,它的高度随 item 数目增加而变高,但是有最大高度的限制,超过这个高度不再增加。如何实现?
4.3 View的工作流程:measure,layout,draw
1. Measure
1.1 View的绘制流程
- onMeasure方法
onMeasure方法的作用是得到测量宽高,本质就是给View的mMeasuredWidth字段和mMeasuredHeight字段赋值。然后使用getMeasureWidth方法和getMeasureHeight方法可以得到测量后的宽和高
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
- setMeasuredDimension
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
//检查当前视图是否在光学布局模式下。
//光学布局模式是指视图的视觉布局可能与其实际边界不完全一致,例如某些图形元素的视觉边界可能需要调整
boolean optical = isLayoutModeOptical(this);
//比较当前视图和其父视图的光学布局模式。如果两者的模式不同,说明父视图的测量方式可能需要调整
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;
//如果当前视图在光学模式下,增加光学边距;如果不在光学模式,则减少光学边距
measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
- getDefaultSize
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
//如果测量模式是UNSPECIFIED,即不确定,即有Listview或ScrollView时是这种模式,这种模式返回值是size。
//如果是另外两种测量模式则返回测量大小
- getSuggestedMinimumWidth
protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}
1.2 ViewGroup绘制流程
ViewGroup是一个抽象类,没有重写View的onMeasure方法
- measureChildren
/**
- Ask all of the children of this view to measure themselves, taking into
- account both the MeasureSpec requirements for this view and its padding.
- We skip children that are in the GONE state The heavy lifting is done in
- getChildMeasureSpec.
- - @param widthMeasureSpec The width requirements for this view
- @param heightMeasureSpec The height requirements for this view
*/
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}·
- onMeasureChild
protected void onMeasureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
// 获取子视图的布局参数
LayoutParams layoutParams = child.getLayoutParams();
// 计算出子视图的测量宽度和高度
int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, layoutParams.width);
int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, layoutParams.height);
// 测量子视图
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
1.3 View的measure过程和Activity的生命周期不同步,如何准确获取View的宽高
- onWindowFocusChanged
一个Activity启动后onCreate、onStart、onResume等过程后,Activity并不是真正可见的,只有当 onWindowFocusChanged 方法最后调用并且参数为true的时候Activity才是真正的可见,这个时候才可以和用户进行交互。
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus)
if (hasWindowFocus) {
int width = view.getMeasureWidth();
int height = view.getMeasureHeight();
}
}
- View.post()
protected void onStart() {
super.onStart();
View.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasureWidth();
int height = view.getMeasureHeight();
}
}
}
- ViewTreeObserver()
int mHeaderViewHeight;
mHeaderView.getViewTreeObserver().addOnGlobalLayoutListener(
new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
mHeaderViewHeight = mHeaderView.getHeight();
//移除观察者
getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
});
2.Layout
- ViewGroup的Layout流程
public void layout(int l, int t, int r, int b) {
// ...省略代码...
// 1.先确定容器 LinearLayout 容器的位置。setFrame() 方法在上面分析过,此处不再重复。
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 2.当位置发生变化时,触发LinearLayout.onLayout方法。
onLayout(changed, l, t, r, b);
// ...省略代码...
}
// ...省略代码...
}
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (mOrientation == VERTICAL) {
layoutVertical(l, t, r, b);
} else {
layoutHorizontal(l, t, r, b);
}
}
void layoutVertical(int left, int top, int right, int bottom) {
final int paddingLeft = mPaddingLeft;
int childTop;
int childLeft;
// Where right end of child should go
final int width = right - left;
int childRight = width - mPaddingRight;
// Space available for child
int childSpace = width - paddingLeft - mPaddingRight;
// ...省略代码...
// 1.遍历子View
for (int i = 0; i < count; i++) {
final View child = getVirtualChildAt(i);
if (child == null) {
childTop += measureNullChild(i);
} else if (child.getVisibility() != GONE) {
// 2.获取子View的大小。getMeasuredWidth() 的值在 measure 流程结束后就会有值。
final int childWidth = child.getMeasuredWidth();
final int childHeight = child.getMeasuredHeight();
final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
// ...省略代码...
childTop += lp.topMargin;
// 3.设置子 View 的具体位置
setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
i += getChildrenSkipCount(child, i);
}
}
}
private void setChildFrame(View child, int left, int top, int width, int height) {
// 4.具体的设置子 View 的位置是在 View.layout() 方法中进行的。
child.layout(left, top, left + width, top + height);
}
- 单一View的绘制流程
参考资料
3.Draw
- 绘制背景background.draw(canvas)
- 绘制自己(onDraw)
- 绘制children(dipatchDraw)
- 绘制装饰(onDrawScrollBars)
4.4 自定义View
1. 注意事项
- 让View支持wrap_content
直接继承View或者ViewGroup的控件,如果不在onMeasure中对wrap_content做特殊处理,那么当外界在布局中使用wrap_content时就无法达到预期的效果 - 如果有必要,让你的View支持padding
这是因为直接继承View的控件,如果不在draw方法中处理padding,那么padding属性是无法起作用的。另外,直接继承自ViewGroup的控件需要在onMeasure和onLayout中考虑padding和子元素的margin对其造成的影响,不然将导致padding和子元素的margin失效。 - 尽量不要在View中使用HandIer,没必要
View内部本身就提供了post系列的方法 - View中如果有线程或者动画,需要及时停止
onDetachedFromWindow是一个很好的时机。当包含此View的Activity退出或者当前View被remove时,View的onDetachedFromWindow方法会被调用,和此方法对应的是onAttachedToWindow,当包含此View的Activity启动时,View的onAttachedToWindow方法会被调用。同时,当View变得不可见时我们也需要停止线程和动画,如果不及时处理这种问题,有可能会造成内存泄漏。 - View带有滑动嵌套情形时,需要处理好滑动冲突
2. 分类
- 继承View重写onDraw方法
用于实现一些不规则的效果.这里选择实现一个很简单的自定义控件,简单到只是绘制一个圆
处理wrap_content;处理padding;新增attr属性
public class CircleView extends View {
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.
CircleView);
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
a.recycle();
init();
}
private void init() {
mPaint.setColor(mColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
// 处理wrap content
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode ==
MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}
@Override
protected void onDraw(Canvas canvas) {
// 处理padding:核心就是减去两边上下的padding
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingLeft();
final int paddingTop = getPaddingLeft();
final int paddingBottom = getPaddingLeft();
int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(width, height) / 2;
canvas.drawCircle(paddingLeft + width / 2, paddingTop + height / 2,
radius, mPaint);
}
}
新增属性attr.xml
<? xml version="1.0" encoding="utf-8"? >
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color" />
</declare-styleable>
</resources>
- 继承ViewGroup派生特殊的Layout
- 继承特定的View(比如TextView)
- 继承特定的ViewGroup(比如LinearLayout)
5. RemoteViews
5.1 简介及应用
1. 基础概念
RemoteViews在实际开发中,主要用在通知栏和桌面小部件的开发过程中。
通知栏每个人都不陌生,主要是通过NotificationManager的notify方法来实现的,它除了默认效果外,还可以另外定义布局。
桌面小部件则是通过AppWidgetProvider来实现的,AppWidget-Provider本质上是一个广播。
通知栏和桌面小部件的开发过程中都会用到RemoteViews,它们在更新界面时无法像在Activity里面那样去直接更新View,这是因为二者的界面都运行在其他进程中,确切来说是系统的SystemServer进程。为了跨进程更新界面,RemoteViews提供了一系列set方法,并且这些方法只是View全部方法的子集。
2.通知栏
Notification notification = new Notification();
notification.icon = R.drawable.ic_launcher;
notification.tickerText = "hello world";
notification.when = System.currentTimeMillis();
notification.flags = Notification.FLAG_AUTO_CANCEL;
Intent intent = new Intent(this, DemoActivity_1.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this,
0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.
layout_notification);
remoteViews.setTextViewText(R.id.msg, "chapter_5");
remoteViews.setImageViewResource(R.id.icon, R.drawable.icon1);
PendingIntent openActivity2PendingIntent = PendingIntent.getActivity(this,
0, new Intent(this, DemoActivity_2.class), PendingIntent.FLAG_
UPDATE_CURRENT);
remoteViews.setOnClickPendingIntent(R.id.open_activity2, openActivity2-
PendingIntent);
notification.contentView = remoteViews;
notification.contentIntent = pendingIntent;
NotificationManager manager = (NotificationManager)getSystemService
(Context.NOTIFICATION_SERVICE);
manager.notify(2, notification);
- 只要提供当前应用的包名和布局文件的资源id即可创建一个RemoteViews对象
remoteViews. setTextViewText
remoteViews.setImageViewResource
remoteViews.setOnClickPendingIntent
- PendingIntent
它表示的是一种待定的Intent,这个Intent中所包含的意图必须由用户来触发
- Notification
3. RemoteViews在桌面小部件上的应用
AppWidgetProvider是Android中提供的用于实现桌面小部件的类,其本质是一个广播,继承于BroadcastReceiver。
- 定义小部件界面:在res/layout/下新建一个XML文件,命名为widget.xml
<? xml version="1.0" encoding="utf-8"? >
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<ImageView
android:id="@+id/imageView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/icon1" />
</LinearLayout>
- 定义小部件配置信息:在res/xml/下新建appwidget_provider_info.xml
initialLayout就是指小工具所使用的初始化布局,updatePeriodMillis定义小工具的自动更新周期,毫秒为单位,每隔一个周期,小工具的自动更新就会触发
<? xml version="1.0" encoding="utf-8"? >
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget"
android:minHeight="84dp"
android:minWidth="84dp"
android:updatePeriodMillis="86400000" >
</appwidget-provider>
- 定义小部件的实现类
public class MyAppWidgetProvider extends AppWidgetProvider {
public static final String TAG = "MyAppWidgetProvider";
public static final String CLICK_ACTION = "com.ryg.chapter_5.action.
CLICK";
public MyAppWidgetProvider() {
super();
}
@Override
public void onReceive(final Context context, Intent intent) {
super.onReceive(context, intent);
Log.i(TAG, "onReceive : action = " + intent.getAction());
// 这里判断是自己的action,做自己的事情,比如小部件被单击了要干什么,这里是做
一个动画效果
if (intent.getAction().equals(CLICK_ACTION)) {
Toast.makeText(context, "clicked it", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
Bitmap srcbBitmap = BitmapFactory.decodeResource(
context.getResources(), R.drawable.icon1);
AppWidgetManager appWidgetManager = AppWidgetManager.
getInstance(context);
for (int i = 0; i < 37; i++) {
float degree = (i * 10) % 360;
RemoteViews remoteViews = new RemoteViews(context .getPackageName(), R.layout.widget);
remoteViews.setImageViewBitmap(R.id.imageView1,
rotateBitmap(context, srcbBitmap, degree));
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.imageView1,pendingIntent);
appWidgetManager.updateAppWidget(new ComponentName(
context, MyAppWidgetProvider.class),
remoteViews);
SystemClock.sleep(30);
}
}
}).start();
}
/**
* 每次桌面小部件更新时都调用一次该方法
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager,
int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
Log.i(TAG, "onUpdate");
final int counter = appWidgetIds.length;
Log.i(TAG, "counter = " + counter);
for (int i = 0; i < counter; i++) {
int appWidgetId = appWidgetIds[i];
onWidgetUpdate(context, appWidgetManager, appWidgetId);
}
}
/**
*桌面小部件更新
*
* @param context
* @param appWidgeManger
* @param appWidgetId
*/
private void onWidgetUpdate(Context context,
AppWidgetManager appWidgeManger, int appWidgetId) {
Log.i(TAG, "appWidgetId = " + appWidgetId);
RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
R.layout.widget);
// “桌面小部件”单击事件发送的Intent广播
Intent intentClick = new Intent();
intentClick.setAction(CLICK_ACTION);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0,
intentClick, 0);
remoteViews.setOnClickPendingIntent(R.id.imageView1, pendingIntent);
appWidgeManger.updateAppWidget(appWidgetId, remoteViews);
}
private Bitmap rotateBitmap(Context context, Bitmap srcbBitmap, float
degree) {
Matrix matrix = new Matrix();
matrix.reset();
matrix.setRotate(degree);
Bitmap tmpBitmap = Bitmap.createBitmap(srcbBitmap, 0, 0,
srcbBitmap.getWidth(), srcbBitmap.getHeight(), matrix, true);
return tmpBitmap;
}
}
}
实现了一个简单的桌面小部件,在小部件上面显示一张图片,单击它后,这个图片就会旋转一周。当小部件被添加到桌面后,会通过RemoteViews来加载布局文件,而当小部件被单击后的旋转效果则是通过不断地更新RemoteViews来实现的,由此可见,桌面小部件不管是初始化界面还是后续的更新界面都必须使用RemoteViews来完成。
- 在AndroidManifest.xmI中声明小部件
<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.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
</receiver>
上面的代码中有两个Action,其中第一个Action用于识别小部件的单击行为,而第二个Action则作为小部件的标识而必须存在
- AppWidgetProvider其他方法
onEnable:当该窗口小部件第一次添加到桌面时调用该方法,可添加多次但只在第一次调用。
onUpdate:小部件被添加时或者每次小部件更新时都会调用一次该方法,小部件的更新时机由updatePeriodMillis来指定,每个周期小部件都会自动更新一次。
onDeleted:每删除一次桌面小部件就调用一次。
onDisabled:当最后一个该类型的桌面小部件被删除时调用该方法,注意是最后一个。
onReceive:这是广播的内置方法,用于分发具体的事件给其他方法。
4. PendingIntent概述
endingIntent和Intent的区别在于,PendingIntent是在将来的某个不确定的时刻发生,而Intent是立刻发生
- 典型的使用场景
是给RemoteViews添加单击事件,因为RemoteViews运行在远程进程中,因此RemoteViews不同于普通的View,所以无法直接向View那样通过setOnClickListener方法来设置单击事件。要想给RemoteViews设置单击事件,就必须使用PendingIntent, PendingIntent通过send和cancel方法来发送和取消特定的待定Intent
- 主要方法
- PendingIntent的匹配规则
如果两个PendingIntent它们内部的Intent相同并且requestCode也相同,那么这两个PendingIntent就是相同的
如果两个Intent的ComponentName和intent-filter都相同,那么这两个Intent就是相同的 - flags参数的含义
- FLAG_ONE_SHOT
(假设你正在开发一款短信应用,当用户收到新短信时,可以发送一条通知。点击这条通知后,打开短信详情页面。)
当前描述的PendingIntent只能被使用一次,然后它就会被自动cancel,如果后续还有相同的PendingIntent,那么它们的send方法就会调用失败。对于通知栏消息来说,如果采用此标记位,那么同类的通知只能使用一次,后续的通知单击后将无法打开 - FLAG_NO_CREATE
当前描述的PendingIntent不会主动创建,如果当前PendingIntent之前不存在,那么getActivity、getService和getBroadcast方法会直接返回null,即获取PendingIntent失败。这个标记位很少见,它无法单独使用,因此在日常开发中它并没有太多的使用意义,这里就不再过多介绍了。 - FLAG_CANCEL_CURRENT
(在下载文件的情况下,你可能需要更新通知以反映当前的下载进度。如果下载过程中用户的下载任务发生变化(例如,选择了不同的文件下载),你可以取消之前的通知并创建新的通知。)
当前描述的PendingIntent如果已经存在,那么它们都会被cancel,然后系统会创建一个新的PendingIntent。对于通知栏消息来说,那些被cancel的消息单击后将无法打开。 - FLAG_UPDATE_CURRENT
(在音乐播放器中,用户可能会多次点击播放、暂停、下一曲等按钮。这些操作需要使用相同的 PendingIntent 进行更新,以便控制音乐播放。)
当前描述的PendingIntent如果已经存在,那么它们都会被更新,即它们的Intent中的Extras会被替换成最新的。 - 结合manager.notify(1, notification)理解标志位
如果notify方法的id是常量,那么不管PendingIntent是否匹配,后面的通知会直接替换前面的通知。
如果notify方法的id每次都不同,那么当PendingIntent不匹配时,这里的匹配是指PendingIntent中的Intent相同并且requestCode相同,在这种情况下不管采用何种标记位,这些通知之间不会相互干扰。如果PendingIntent处于匹配状态时,这个时候要分情况讨论:如果采用了FLAG_ONE_SHOT标记位,那么后续通知中的PendingIntent会和第一条通知保持完全一致,包括其中的Extras,单击任何一条通知后,剩下的通知均无法再打开,当所有的通知都被清除后,会再次重复这个过程;如果采用FLAG_CANCEL_CURRENT标记位,那么只有最新的通知可以打开,之前弹出的所有通知均无法打开;如果采用FLAG_UPDATE_CURRENT标记位,那么之前弹出的通知中的PendingIntent会被更新,最终它们和最新的一条通知保持完全一致,包括其中的Extras,并且这些通知都是可以打开的。
5.2 内部机制
- 常用构造方法
public RemoteViews(String packageName, int layoutId),并不是所有类型都支持,如下类型支持:
Layout
FrameLayout、LinearLayout、RelativeLayout、GridLayout
View
AnalogClock、Button、Chronometer、ImageButton、ImageView、ProgressBar、TextView、ViewFlipper、ListView、GridView、StackView、AdapterViewFlipper、ViewStub。 - 常见set方法
- 流程梳理
RemoteViews会通过Binder传递到SystemServer进程 -> LayoutInflater去加载RemoteViews中的布局文件 -> 系统会对View执行一系列界面更新任务(set, set方法对View所做的更新并不是立刻执行的) - Action
Action代表一个View操作,Action同样实现了Parcelable接口。系统首先将View操作封装到Action对象并将这些对象跨进程传输到远程进程,接着在远程进程中执行Action对象中的具体操作。在我们的应用中每调用一次set方法,RemoteViews中就会添加一个对应的Action对象,当我们通过NotificationManager和AppWidgetManager来提交我们的更新时,这些Action对象就会传输到远程进程并在远程进程中依次执行
RemoteViews内部有一个mActions成员,它是一个ArrayList,外界每调用一次set方法,RemoteViews就会为其创建一个Action对象并加入到这个ArrayList中 - setTextViewText源码解析
public void setTextViewText(int viewId, CharSequence text) {
setCharSequence(viewId, "setText", text);
}
public void setCharSequence(int viewId, String methodName, CharSequence
value) {
addAction(new ReflectionAction(viewId, methodName, ReflectionAction.
CHAR_SEQUENCE, value));
}
private void addAction(Action a) {
…
if (mActions == null) {
mActions = new ArrayList<Action>();
}
mActions.add(a);
// update the memory usage stats
a.updateMemoryUsageEstimate(mMemoryUsageCounter);
}
- apply方法
public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
RemoteViews rvToApply = getRemoteViewsToApply(context);
View result;
...
LayoutInflater inflater = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
// Clone inflater so we load resources from correct context and
// we don't add a filter to the static version returned by getSystem-
Service.
inflater = inflater.cloneInContext(inflationContext);
inflater.setFilter(this);
result = inflater.inflate(rvToApply.getLayoutId(), parent, false);
//performApply的实现就比较好理解了,它的作用就是遍历mActions这个列表并执行每个Action对象的apply方法
rvToApply.performApply(result, parent, handler);
return result;
}
private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
if (mActions ! = null) {
handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
final int count = mActions.size();
for (int i = 0; i < count; i++) {
Action a = mActions.get(i);
a.apply(v, parent, handler);
}
}
}
apply会加载布局并更新界面,而reApply则只会更新界面
6. Drawable
6.1 简介
一种图像的概念,但是它们又不全是图片,通过颜色也可以构造出各式各样的图像的效果.常被用来作为View的背景使用。Drawable一般都是通过XML来定义的,当然我们也可以通过代码来创建具体的Drawable对象
Drawable的内部宽/高这个参数比较重要,通过getIntrinsicWidth和getIntrinsicHeight这两个方法可以获取到它们。但是并不是所有的Drawable都有内部宽/高,比如一张图片所形成的Drawable,它的内部宽/高就是图片的宽/高,但是一个颜色所形成的Drawable,它就没有内部宽/高的概念。另外需要注意的是,Drawable的内部宽/高不等同于它的大小,一般来说,Drawable是没有大小概念的,当用作View的背景时,Drawable会被拉伸至View的同等大小。
6.2 分类
1. BitmapDrawable
表示的就是一张图片
<? xml version="1.0" encoding="utf-8"? >
<bitmap
xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@[package:]drawable/drawable_resource"
android:antialias=["true" | "false"]
android:dither=["true" | "false"]
android:filter=["true" | "false"]
android:gravity=["top" | "bottom"|"left"|"right" | "center_vertical" |
"fill_vertical"|"center_horizontal" | "fill_horizontal" |
"center" | "fill" | "clip_vertical" | "clip_horizontal"]
android:mipMap=["true" | "false"]
android:tileMode=["disabled" | "clamp" | "repeat" | "mirror"] />
- android:src
这个很简单,就是图片的资源id。 - android:antialias
是否开启图片抗锯齿功能。开启后会让图片变得平滑,同时也会在一定程度上降低图片的清晰度,但是这个降低的幅度较低以至于可以忽略,因此抗锯齿选项应该开启。 - android:dither
是否开启抖动效果。当图片的像素配置和手机屏幕的像素配置不一致时,开启这个选项可以让高质量的图片在低质量的屏幕上还能保持较好的显示效果, - android:filter
是否开启过滤效果。当图片尺寸被拉伸或者压缩时,开启过滤效果可以保持较好的显示效果,因此此选项也应该开启。 - android:gravity
当图片小于容器的尺寸时,设置此选项可以对图片进行定位。这个属性的可选项比较多,不同的选项可以通过“|”来组合使用. - android:mipMap
这是一种图像相关的处理技术,也叫纹理映射,比较抽象,这里也不对其深究了,默认值为false,在日常开发中此选项不常用。 - android:tileMode
平铺模式。这个选项有如下几个值:[“disabled” | “clamp” | “repeat” | “mirror”],其中disable表示关闭平铺模式,这也是默认值,当开启平铺模式后,gravity属性会被忽略
2. ShapeDrawable
ShapeDrawable是一种很常见的Drawable,可以理解为通过颜色来构造的图形,它既可以是纯色的图形,也可以是具有渐变效果的图形。
<? xml version="1.0" encoding="utf-8"? >
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape=["rectangle" | "oval" | "line" | "ring"] >
<corners
android:radius="integer"
android:topLeftRadius="integer"
android:topRightRadius="integer"
android:bottomLeftRadius="integer"
android:bottomRightRadius="integer" />
<gradient
android:angle="integer"
android:centerX="integer"
android:centerY="integer"
android:centerColor="integer"
android:endColor="color"
android:gradientRadius="integer"
android:startColor="color"
android:type=["linear" | "radial" | "sweep"]
android:useLevel=["true" | "false"] />
<padding
android:left="integer"
android:top="integer"
android:right="integer"
android:bottom="integer" />
<size
android:width="integer"
android:height="integer" />
<solid
android:color="color" />
<stroke
android:width="integer"
android:color="color"
android:dashWidth="integer"
android:dashGap="integer" />
</shape>
- android:shape:
表示图形的形状,有四个选项:rectangle(矩形)、oval(椭圆)、line(横线)和ring(圆环) - 针对ring这个形状,有5个特殊的属性:android:innerRadius、android:thickness、android:innerRadiusRatio、android:thicknessRatio和android:useLevel
3. LayerDrawable
LayerDrawable对应的XML标签是,它表示一种层次化的Drawable集合,通过将不同的Drawable放置在不同的层上面从而达到一种叠加后的效果
<? xml version="1.0" encoding="utf-8"? >
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item>
<shape android:shape="rectangle" >
<solid android:color="#0ac39e" />
</shape>
</item>
<item android:bottom="6dp">
<shape android:shape="rectangle" >
<solid android:color="#ffffff" />
</shape>
</item>
<item
android:bottom="1dp"
android:left="1dp"
android:right="1dp">
<shape android:shape="rectangle" >
<solid android:color="#ffffff" />
</shape>
</item>
</layer-list>
4. StateListDrawable
StateListDrawable对应于标签,它也是表示Drawable集合,每个Drawable都对应着View的一种状态,这样系统就会根据View的状态来选择合适的Drawable。StateListDrawable主要用于设置可单击的View的背景,最常见的是Button,这个读者应该不陌生,它的语法如下所示。
<? xml version="1.0" encoding="utf-8"? >
<selector xmlns:android="http://schemas.android.com/apk/res/android"
android:constantSize=["true" | "false"]
android:dither=["true" | "false"]
android:variablePadding=["true" | "false"] >
<item
android:drawable="@[package:]drawable/drawable_resource"
android:state_pressed=["true" | "false"]
android:state_focused=["true" | "false"]
android:state_hovered=["true" | "false"]
android:state_selected=["true" | "false"]
android:state_checkable=["true" | "false"]
android:state_checked=["true" | "false"]
android:state_enabled=["true" | "false"]
android:state_activated=["true" | "false"]
android:state_window_focused=["true" | "false"] />
</selector>
5. LevelListDrawable
LevelListDrawable对应于标签,它同样表示一个Drawable集合,集合中的每个Drawable都有一个等级(level)的概念。根据不同的等级,LevelListDrawable会切换为对应的Drawable,它的语法如下所示。
<? xml version="1.0" encoding="utf-8"? >
<level-list
xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:drawable="@drawable/drawable_resource"
android:maxLevel="integer"
android:minLevel="integer" />
</level-list>
6. TransitionDrawable
TransitionDrawable对应于标签,它用于实现两个Drawable之间的淡入淡出效果,具体例子如下:
首先定义TransitionDrawable,
// res/drawable/transition_drawable.xml
<? xml version="1.0" encoding="utf-8"? >
<transition xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/drawable1" />
<item android:drawable="@drawable/drawable2" />
</transition>
接着将上面的TransitionDrawable设置为View的背景,如下所示。当然也可以在ImageView中直接作为Drawable来使用。
<TextView
android:id="@+id/button"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:background="@drawable/transition_drawable" />
最后,通过它的startTransition和reverseTransition方法来实现淡入淡出的效果以及它的逆过程,如下所示。
TextView textView = (TextView) findViewById(R.id.test_transition);
TransitionDrawable drawable = (TransitionDrawable) textView.getBackground();
drawable.startTransition(1000);
7. InsetDrawable
InsetDrawable对应于标签,它可以将其他Drawable内嵌到自己当中,并可以在四周留出一定的间距。当一个View希望自己的背景比自己的实际区域小的时候,可以采用InsetDrawable来实现,同时我们知道,通过LayerDrawable也可以实现这种效果。
<? xml version="1.0" encoding="utf-8"? >
<inset
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/drawable_resource"
android:insetTop="dimension"
android:insetRight="dimension"
android:insetBottom="dimension"
android:insetLeft="dimension" />
8. ScaleDrawable
ScaleDrawable对应于标签,它可以根据自己的等级(level)将指定的Drawable缩放到一定比例,它的语法如下所示。
<? xml version="1.0" encoding="utf-8"? >
<scale
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/drawable_resource"
android:scaleGravity=["top" | "bottom" | "left" | "right" | "center_
vertical" |"fill_vertical" | "center_horizontal" | "fill_horizontal" |
"center" | "fill" | "clip_vertical" | "clip_horizontal"]
android:scaleHeight="percentage"
android:scaleWidth="percentage" />
android:scaleGravity的含义等同于shape中的android:gravity,而android:scaleWidth和android:scaleHeight分别表示对指定Drawable宽和高的缩放比例,以百分比的形式表示
9. ClipDrawable
ClipDrawable对应于标签,它可以根据自己当前的等级(level)来裁剪另一个Drawable,裁剪方向可以通过android:clipOrientation和android:gravity这两个属性来共同控制,它的语法如下所示。
<? xml version="1.0" encoding="utf-8"? >
<clip
xmlns:android="http://schemas.android.com/apk/res/android"
android:drawable="@drawable/drawable_resource"
android:clipOrientation=["horizontal" | "vertical"]
android:gravity=["top" | "bottom"|"left" | "right"|"center_vertical" |
"fill_vertical"|"center_horizontal"|"fill_horizontal" |
"center" | "fill"|"clip_vertical"|"clip_horizontal"] />
6.3 自定义Drawable
Drawable的使用范围很单一,一个是作为ImageView中的图像来显示,另外一个就是作为View的背景,大多数情况下Drawable都是以View的背景这种形式出现的。
可以通过重写Drawable的draw方法来自定义Drawable。通常我们没有必要去自定义Drawable,这是因为自定义的Drawable无法在XML中使用,这就降低了自定义Drawable的使用范围。
7. Android动画深入分析
View动画的作用对象是View,它支持4种动画效果,分别是平移动画、缩放动画、旋转动画和透明度动画。除了这四种典型的变换效果外,帧动画也属于View动画,但是帧动画的表现形式和上面的四种变换效果不太一样。
7.1 View动画
1. 种类
View动画的四种变换效果对应着Animation的四个子类:TranslateAnimation、ScaleAnimation、RotateAnimation和AlphaAnimation。这四种动画既可以通过XML来定义(建议这种方式),也可以通过代码来动态创建
<? xml version="1.0" encoding="utf-8"? >
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@[package:]anim/interpolator_resource"
android:shareInterpolator=["true" | "false"] >
<alpha
android:fromAlpha="float"
android:toAlpha="float" />
<scale
android:fromXScale="float"
android:toXScale="float"
android:fromYScale="float"
android:toYScale="float"
android:pivotX="float"
android:pivotY="float" />
<translate
android:fromXDelta="float"
android:toXDelta="float"
android:fromYDelta="float"
android:toYDelta="float" />
<rotate
android:fromDegrees="float"
android:toDegrees="float"
android:pivotX="float"
android:pivotY="float" />
<set>
...
</set>
</set>
- android:interpolator
表示动画集合所采用的插值器,插值器影响动画的速度,比如非匀速动画就需要通过插值器来控制动画的播放过程。这个属性可以不指定,默认为@android:anim/accelerate_decelerate_interpolator,即加速减速插值器 - 如何应用
Button mButton = (Button) findViewById(R.id.button1);
Animation animation = AnimationUtils.loadAnimation(this, R.anim.animation_
test);
mButton.startAnimation(animation);
2. 自定义View动画
只需要继承Animation这个抽象类,然后重写它的initialize和applyTransformation方法,在initialize方法中做一些初始化工作,在applyTransformation中进行相应的矩阵变换即可,很多时候需要采用Camera来简化矩阵变换的过程。 (难点在于数学的矩阵变化)
例子:Rotate3dAnimation可以围绕y轴旋转并且同时沿着z轴平移从而实现一种类似于3D的效果,它的代码如下:
public class Rotate3dAnimation extends Animation {
private final float mFromDegrees;
private final float mToDegrees;
private final float mCenterX;
private final float mCenterY;
private final float mDepthZ;
private final boolean mReverse;
private Camera mCamera;
/**
* Creates a new 3D rotation on the Y axis. The rotation is defined by its
* start angle and its end angle. Both angles are in degrees. The rotation
* is performed around a center point on the 2D space, definied by a pair
* of X and Y coordinates, called centerX and centerY. When the animation
* starts, a translation on the Z axis (depth) is performed. The length
* of the translation can be specified, as well as whether the translation
* should be reversed in time.
*
* @param fromDegrees the start angle of the 3D rotation
* @param toDegrees the end angle of the 3D rotation
* @param centerX the X center of the 3D rotation
* @param centerY the Y center of the 3D rotation
* @param reverse true if the translation should be reversed, false
otherwise
*/
public Rotate3dAnimation(float fromDegrees, float toDegrees,
float centerX, float centerY, float depthZ, boolean reverse) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;
mReverse = reverse;
}
@Override
public void initialize(int width, int height, int parentWidth, int
parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
mCamera = new Camera();
}
@Override
protected void applyTransformation(float interpolatedTime, Transfor-
mation t) {
final float fromDegrees = mFromDegrees;
float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpo-
latedTime);
final float centerX = mCenterX;
final float centerY = mCenterY;
final Camera camera = mCamera;
final Matrix matrix = t.getMatrix();
camera.save();
if (mReverse) {
camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
} else {
camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolated-
Time));
}
camera.rotateY(degrees);
camera.getMatrix(matrix);
camera.restore();
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
}
}
3.帧动画
帧动画是顺序播放一组预先定义好的图片,类似于电影播放。不同于View动画,系统提供了另外一个类AnimationDrawable来使用帧动画。帧动画的使用比较简单,首先需要通过XML来定义一个AnimationDrawable,如下所示。
// res/drawable/frame_animation.xml
<? xml version="1.0" encoding="utf-8"? >
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item android:drawable="@drawable/image1" android:duration="500" />
<item android:drawable="@drawable/image2" android:duration="500" />
<item android:drawable="@drawable/image3" android:duration="500" />
</animation-list>
然后将上述的Drawable作为View的背景并通过Drawable来播放动画即可:
Button mButton = (Button)findViewById(R.id.button1);
mButton.setBackgroundResource(R.drawable.frame_animation);
AnimationDrawable drawable = (AnimationDrawable) mButton.getBackground();
drawable.start();
帧动画的使用比较简单,但是比较容易引起OOM,所以在使用帧动画时应尽量避免使用过多尺寸较大的图片。
7.2 View动画的特殊使用场景
1. LayoutAnimation
LayoutAnimation作用于ViewGroup,为ViewGroup指定一个动画,这样当它的子元素出场时都会具有这种动画效果。
应用实例:这种效果常常被用在ListView上,我们时常会看到一种特殊的ListView,它的每个item都以一定的动画的形式出现,其实这并非什么高深的技术,它使用的就是LayoutAnimation.实现步骤如下:
- 定义LayoutAnimation
// res/anim/anim_layout.xml
<layoutAnimation
xmlns:android="http://schemas.android.com/apk/res/android"
# 表示子元素开始动画的时间延迟,比如子元素入场动画的时间周期为300ms,
# 那么0.5表示每个子元素都需要延迟150ms才能播放入场动画。总体来说,
# 第一个子元素延迟150ms开始播放入场动画,第2个子元素延迟300ms开始播放入场动画,依次类推。
android:delay="0.5"
# 表示子元素动画的顺序,有三种选项:normal、reverse和random,
# 其中normal表示顺序显示,即排在前面的子元素先开始播放入场动画;reverse表示逆向显示,
# 即排在后面的子元素先开始播放入场动画;random则是随机播放入场动画
android:animationOrder="normal"
# 为子元素指定具体的入场动画。
android:animation="@anim/anim_item"/>
- 为子元素指定具体的入场动画
// res/anim/anim_item.xml
<? xml version="1.0" encoding="utf-8"? >
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:interpolator="@android:anim/accelerate_interpolator"
android:shareInterpolator="true" >
<alpha
android:fromAlpha="0.0"
android:toAlpha="1.0" />
<translate
android:fromXDelta="500"
android:toXDelta="0" />
</set>
- 为ViewGroup指定android:layoutAnimation属性:android:layoutAnimation=“@anim/anim_layout”
- 除了在XML中指定LayoutAnimation外,还可以通过LayoutAnimationController来实现,具体代码如下所示
ListView listView = (ListView) layout.findViewById(R.id.list);
Animation animation = AnimationUtils.loadAnimation(this, R.anim.anim_
item);
LayoutAnimationController controller = new LayoutAnimationController
(animation);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
listView.setLayoutAnimation(controller);
2. Activity切换效果
Activity有默认的切换效果,但是这个效果我们是可以自定义的,主要用到overridePendingTransition(int enterAnim, int exitAnim)这个方法,这个方法必须在startActivity(Intent)或者finish()之后被调用才能生效,它的参数含义如下:
enterAnim——Activity被打开时,所需的动画资源id;
exitAnim——Activity被暂停时,所需的动画资源id。
Intent intent = new Intent(this, TestActivity.class);
startActivity(intent);
overridePendingTransition(R.anim.enter_anim, R.anim.exit_anim);
@Override
public void finish() {
super.finish();
overridePendingTransition(R.anim.enter_anim, R.anim.exit_anim);
}
overridePendingTransition这个方法必须位于startActivity或者finish的后面,否则动画效果将不起作用
7.3 属性动画
属性动画是API 11新加入的特性,和View动画不同,它对作用对象进行了扩展,属性动画可以对任何对象做动画,甚至还可以没有对象。除了作用对象进行了扩展以外,属性动画的效果也得到了加强,不再像View动画那样只能支持四种简单的变换。属性动画中有ValueAnimator、ObjectAnimator和AnimatorSet等概念,通过它们可以实现绚丽的动画。
1. 使用属性动画
属性动画可以对任意对象的属性进行动画而不仅仅是View,动画默认时间间隔300ms,默认帧率10ms/帧。其可以达到的效果是:在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。
- 改变一个对象(myObject)的translationY属性,让其沿着Y轴向上平移一段距离
ObjectAnimator.ofFloat(myObject, "translationY", -myObject.getHeight()).
start();
- 改变一个对象的背景色属性,典型的情形是改变View的背景色
下面的动画可以让背景色在3秒内实现从0xFFFF8080到0xFF8080FF的渐变,动画会无限循环而且会有反转的效果。
ValueAnimator colorAnim = ObjectAnimator.ofInt(this, "backgroundColor",
/*Red*/0xFFFF8080, /*Blue*/0xFF8080FF);
colorAnim.setDuration(3000);
colorAnim.setEvaluator(new ArgbEvaluator());
colorAnim.setRepeatCount(ValueAnimator.INFINITE);
colorAnim.setRepeatMode(ValueAnimator.REVERSE);
colorAnim.start();
- 动画集合,5秒内对View的旋转、平移、缩放和透明度都进行了改变。
AnimatorSet set = new AnimatorSet();
set.playTogether(
ObjectAnimator.ofFloat(myView, "rotationX", 0, 360),
ObjectAnimator.ofFloat(myView, "rotationY", 0, 180),
ObjectAnimator.ofFloat(myView, "rotation", 0, -90),
ObjectAnimator.ofFloat(myView, "translationX", 0, 90),
ObjectAnimator.ofFloat(myView, "translationY", 0, 90),
ObjectAnimator.ofFloat(myView, "scaleX", 1, 1.5f),
ObjectAnimator.ofFloat(myView, "scaleY", 1, 0.5f),
ObjectAnimator.ofFloat(myView, "alpha", 1, 0.25f, 1)
);
set.setDuration(5 * 1000).start();
- XML定义
// // res/animator/property_animator.xml
<set
android:ordering=["together" | "sequentially"]>
<objectAnimator
android:propertyName="string"
android:duration="int"
android:valueFrom="float | int | color"
android:valueTo="float | int | color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode=["repeat" | "reverse"]
android:valueType=["intType" | "floatType"]/>
<animator
android:duration="int"
android:valueFrom="float | int | color"
android:valueTo="float | int | color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode=["repeat" | "reverse"]
android:valueType=["intType" | "floatType"]/>
<set>
...
</set>
</set>
在代码里使用
AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(myContext,
R.anim.property_animator);
set.setTarget(mButton);
set.start();
实际开发中建议采用代码来实现属性动画
2. 理解插值器和估值器
TimeInterpolator中文翻译为时间插值器,它的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有LinearInterpolator(线性插值器:匀速动画)、AccelerateDecelerateInterpolator(加速减速插值器:动画两头慢中间快)和Decelerate-Interpolator(减速插值器:动画越来越慢)等。
TypeEvaluator的中文翻译为类型估值算法,也叫估值器,它的作用是根据当前属性改变的百分比来计算改变后的属性值,系统预置的有IntEvaluator(针对整型属性)、FloatEvaluator(针对浮点型属性)和ArgbEvaluator(针对Color属性)。属性动画中的插值器(Interpolator)和估值器(TypeEvaluator)很重要,它们是实现非匀速动画的重要手段。
3. 属性动画的监听器
- AnimatorListener : 监听动画的开始、结束、取消以及重复播放
系统还提供了AnimatorListenerAdapter这个类,它是Animator-Listener的适配器类,这样我们就可以有选择地实现上面的4个方法了
public static interface AnimatorListener {
void onAnimationStart(Animator animation);
void onAnimationEnd(Animator animation);
void onAnimationCancel(Animator animation);
void onAnimationRepeat(Animator animation);
}
- AnimatorUpdateListener: 监听整个动画过程
public static interface AnimatorUpdateListener {
void onAnimationUpdate(ValueAnimator animation);
}
4. 对任意属性做动画
对object的属性abc做动画,如果想让动画生效,要同时满足两个条件:
- object必须要提供setAbc方法,如果动画的时候没有传递初始值,那么还要提供getAbc方法,因为系统要去取abc属性的初始值(如果这条不满足,程序直接Crash)
- object的setAbc对属性abc所做的改变必须能够通过某种方法反映出来,比如会带来UI的改变之类的(如果这条不满足,动画无效果但不会Crash)
以button为例子,直接对其进行动画操作,不会生效,原因如下(不满足条件2):
- Button内部虽然提供了getWidth和setWidth方法,但是这个setWidth方法并不是改变视图的大小。
- setWidth是TextView和其子类的专属方法,它的作用不是设置View的宽度,而是设置TextView的最大宽度和最小宽度的
/**
* Makes the TextView exactly this many pixels wide.
* You could do the same thing by specifying this number in the
* LayoutParams.
*
* @see #setMaxWidth(int)
* @see #setMinWidth(int)
* @see #getMinWidth()
* @see #getMaxWidth()
*
* @attr ref android.R.styleable#TextView_width
*/
@android.view.RemotableViewMethod
public void setWidth(int pixels) {
mMaxWidth = mMinWidth = pixels;
mMaxWidthMode = mMinWidthMode = PIXELS;
requestLayout();
invalidate();
}
/**
* Return the width of the your view.
*
* @return The width of your view, in pixels.
*/
@ViewDebug.ExportedProperty(category = "layout")
public final int getWidth() {
return mRight - mLeft;
}
针对上述问题,官方文档上告诉我们有3种解决方法:
- 给你的对象加上get和set方法,如果你有权限的话
- 用一个类来包装原始对象,间接为其提供get和set方法
private void performAnimate() {
ViewWrapper wrapper = new ViewWrapper(mButton);
ObjectAnimator.ofInt(wrapper, "width", 500).setDuration(5000).start();
}
@Override
public void onClick(View v) {
if (v == mButton) {
performAnimate();
}
}
private static class ViewWrapper {
private View mTarget;
public ViewWrapper(View target) {
mTarget = target;
}
public int getWidth() {
return mTarget.getLayoutParams().width;
}
public void setWidth(int width) {
mTarget.getLayoutParams().width = width;
mTarget.requestLayout();
}
}
- 采用ValueAnimator,监听动画过程,自己实现属性的改变。
private void performAnimate(final View target, final int start, final int
end) {
ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
// 持有一个IntEvaluator对象,方便下面估值的时候使用
private IntEvaluator mEvaluator = new IntEvaluator();
@Override
public void onAnimationUpdate(ValueAnimator animator) {
// 获得当前动画的进度值,整型,1~100之间
int currentValue = (Integer) animator.getAnimatedValue();
Log.d(TAG, "current value: " + currentValue);
// 获得当前进度占整个动画过程的比例,浮点型,0~1之间
float fraction = animator.getAnimatedFraction();
// 直接调用整型估值器,通过比例计算出宽度,然后再设给Button
target.getLayoutParams().width = mEvaluator.evaluate(fraction,
start, end);
target.requestLayout();
}
});
valueAnimator.setDuration(5000).start();
}
@Override
public void onClick(View v) {
if (v == mButton) {
performAnimate(mButton, mButton.getWidth(), 500);
}
}
5.属性动画的原理
属性动画要求动画作用的对象提供该属性的set方法,属性动画根据你传递的该属性的初始值和最终值,以动画的效果多次去调用set方法。
如果动画的时候没有传递初始值,那么还要提供get方法,因为系统要去获取属性的初始值。
- 源码分析:
- ObjectAnimator.ofInt(mButton, “width”, 500).setDuration (5000).start()
判断如果当前动画、等待的动画(Pending)和延迟的动画(Delay)中有和当前动画相同的动画,那么就把相同的动画给取消掉;接着就调用了父类的super.start()方法
public void start() {
// See if any of the current active/pending animators need to be canceled
AnimationHandler handler = sAnimationHandler.get();
if (handler ! = null) {
int numAnims = handler.mAnimations.size();
for (int i = numAnims -1; i >= 0; i--) {
if (handler.mAnimations.get(i) instanceof ObjectAnimator) {
ObjectAnimator anim = (ObjectAnimator) handler.mAnimations.
get(i);
if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
anim.cancel();
}
}
}
numAnims = handler.mPendingAnimations.size();
for (int i = numAnims -1; i >= 0; i--) {
if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {
ObjectAnimator anim = (ObjectAnimator) handler.mPending-
Animations.get(i);
if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
anim.cancel();
}
}
}
numAnims = handler.mDelayedAnims.size();
for (int i = numAnims -1; i >= 0; i--) {
if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {
ObjectAnimator anim = (ObjectAnimator) handler.mDelayed-
Anims.get(i);
if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
anim.cancel();
}
}
}
}
if (DBG) {
Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " +
getDuration());
for (int i = 0; i < mValues.length; ++i) {
PropertyValuesHolder pvh = mValues[i];
Log.d(LOG_TAG, " Values[" + i + "]: " +
pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0)
+ ", " +
pvh.mKeyframes.getValue(1));
}
}
super.start();
}
- ObjectAnimator继承了ValueAnimator,父类的ValueAnimator的Start方法
属性动画需要运行在有Looper的线程中。最终会调用Animation-Handler的start方法,这个AnimationHandler并不是Handler,它是一个Runnable
private void start(boolean playBackwards) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on
Looper threads");
}
mPlayingBackwards = playBackwards;
mCurrentIteration = 0;
mPlayingState = STOPPED;
mStarted = true;
mStartedDelay = false;
mPaused = false;
updateScaledDuration(); // in case the scale factor has changed since
creation time
AnimationHandler animationHandler = getOrCreateAnimationHandler();
animationHandler.mPendingAnimations.add(this);
if (mStartDelay == 0) {
// This sets the initial value of the animation, prior to actually
starting it running
setCurrentPlayTime(0);
mPlayingState = STOPPED;
mRunning = true;
notifyStartListeners();
}
animationHandler.start();
}
- ValueAnimator中的doAnimationFrame方法
通过底层分析得知,get和set都是通过反射调用
final boolean doAnimationFrame(long frameTime) {
if (mPlayingState == STOPPED) {
mPlayingState = RUNNING;
if (mSeekTime < 0) {
mStartTime = frameTime;
} else {
mStartTime = frameTime - mSeekTime;
// Now that we're playing, reset the seek time
mSeekTime = -1;
}
}
if (mPaused) {
if (mPauseTime < 0) {
mPauseTime = frameTime;
}
return false;
} else if (mResumed) {
mResumed = false;
if (mPauseTime > 0) {
// Offset by the duration that the animation was paused
mStartTime += (frameTime - mPauseTime);
}
}
// The frame time might be before the start time during the first frame of
// an animation. The "current time" must always be on or after the start
// time to avoid animating frames at negative time intervals. In practice, this
// is very rare and only happens when seeking backwards.
final long currentTime = Math.max(frameTime, mStartTime);
return animationFrame(currentTime);
}
void animateValue(float fraction) {
fraction = mInterpolator.getInterpolation(fraction);
mCurrentFraction = fraction;
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].calculateValue(fraction);
}
if (mUpdateListeners ! = null) {
int numListeners = mUpdateListeners.size();
for (int i = 0; i < numListeners; ++i) {
mUpdateListeners.get(i).onAnimationUpdate(this);
}
}
}
7.4 使用动画的注意事项
-
OOM问题
这个问题主要出现在帧动画中,当图片数量较多且图片较大时就极易出现OOM,这个在实际的开发中要尤其注意,尽量避免使用帧动画 -
内存泄露
在属性动画中有一类无限循环的动画,这类动画需要在Activity退出时及时停止,否则将导致Activity无法释放从而造成内存泄露,通过验证后发现View动画并不存在此问题 -
兼容性问题
动画在3.0以下的系统上有兼容性问题,在某些特殊场景可能无法正常工作,因此要做好适配工作。 -
View动画的问题
iew动画是对View的影像做动画,并不是真正地改变View的状态,因此有时候会出现动画完成后View无法隐藏的现象,即setVisibility(View.GONE)失效了,这个时候只要调用view.clearAnimation()清除View动画即可解决此问题。 -
不要使用px
-
动画元素的交互
将view移动(平移)后,在Android 3.0以前的系统上,不管是View动画还是属性动画,新位置均无法触发单击事件,同时,老位置仍然可以触发单击事件。尽管View已经在视觉上不存在了,将View移回原位置以后,原位置的单击事件继续生效。从3.0开始,属性动画的单击事件触发位置为移动后的位置,但是View动画仍然在原位置。 -
硬件加速
8. 理解Window和WindowManager
Window表示一个窗口的概念,在日常开发中直接接触Window的机会并不多,但是在某些特殊时候我们需要在桌面上显示一个类似悬浮窗的东西,那么这种效果就需要用到Window来实现。
Window是一个抽象类,它的具体实现是PhoneWindow。
创建一个Window是很简单的事,只需要通过WindowManager即可完成。
WindowManager是外界访问Window的入口,Window的具体实现位于WindowManagerService中,WindowManager和Window-ManagerService的交互是一个IPC过程。
Android中所有的视图都是通过Window来呈现的,不管是Activity、Dialog还是Toast,它们的视图实际上都是附加在Window上的,因此Window实际是View的直接管理者。
8.1 Window和WindowManager
- 如何使用WindowManager添加一个Window
mFloatingButton = new Button(this);
mFloatingButton.setText("button");
mLayoutParams = new WindowManager.LayoutParams(
LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0,
PixelFormat.TRANSPARENT);
mLayoutParams.flags = LayoutParams.FLAG_NOT_TOUCH_MODAL
| LayoutParams.FLAG_NOT_FOCUSABLE
| LayoutParams. FLAG_SHOW_WHEN_LOCKED
mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;
mLayoutParams.x = 100;
mLayoutParams.y = 300;
mWindowManager.addView(mFloatingButton, mLayoutParams);
- Flag常用参数:
FLAG_NOT_FOCUSABLE
表示Window不需要获取焦点,也不需要接收各种输入事件,此标记会同时启用FLAG_NOT_TOUCH_MODAL,最终事件会直接传递给下层的具有焦点的Window。
FLAG_NOT_TOUCH_MODAL
在此模式下,系统会将当前Window区域以外的单击事件传递给底层的Window,当前Window区域以内的单击事件则自己处理。这个标记很重要,一般来说都需要开启此标记,否则其他Window将无法收到单击事件。
FLAG_SHOW_WHEN_LOCKED
开启此模式可以让Window显示在锁屏的界面上。 - Type参数
表示Window的类型,Window有三种类型,分别是应用Window、子Window和系统Window。应用类Window对应着一个Activity。子Window不能单独存在,它需要附属在特定的父Window之中,比如常见的一些Dialog就是一个子Window。系统Window是需要声明权限在能创建的Window,比如Toast和系统状态栏这些都是系统Window。 - WindowManager
public interface ViewManager
{
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}
- 可以拖动的Window效果,实现代码
只需要根据手指的位置来设定LayoutParams中的x和y的值即可改变Window的位置。首先给View设置onTouchListener:mFloatingButton.setOnTouchListener(this)。然后在onTouch方法中不断更新View的位置即可
public boolean onTouch(View v, MotionEvent event) {
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE: {
mLayoutParams.x = rawX;
mLayoutParams.y = rawY;
mWindowManager.updateViewLayout(mFloatingButton, mLayoutParams);
break;
}
default:
break;
}
return false;
}
8.2 Window的内部机制
Window是一个抽象的概念,每一个Window都对应着一个View和一个ViewRootImpl, Window和View通过ViewRootImpl来建立联系,因此Window并不是实际存在的,它是以View的形式存在。这点从WindowManager的定义也可以看出,它提供的三个接口方法addView、updateViewLayout以及removeView都是针对View的,这说明View才是Window存在的实体。在实际使用中无法直接访问Window,对Window的访问必须通过WindowManager。
1. Window的添加过程
- WindowManagerImpl中Window的三大操作的实现如下
@Override
public void addView(View view, ViewGroup.LayoutParams params) {
mGlobal.addView(view, params, mDisplay, mParentWindow);
}
@Override
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
mGlobal.updateViewLayout(view, params);
}
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
WindowManagerImpl并没有直接实现Window的三大操作,而是全部交给了WindowManagerGlobal来处理,WindowManagerGlobal以工厂的形式向外提供自己的实例,在WindowManagerGlobal中有如下一段代码:private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance()。WindowManagerImpl这种工作模式是典型的桥接模式,将所有的操作全部委托给WindowManagerGlobal来实现,步骤如下:
-
- 检查参数是否合法,如果是子Window那么还需要调整一些布局参数
// 1.检查参数是否合法,如果是子Window那么还需要调整一些布局参数
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (! (params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.
LayoutParams");
}
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)
params;
if (parentWindow ! = null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
}
-
- 创建ViewRootImpI并将View添加到列表中
// 2. 创建ViewRootImpI并将View添加到列表中
// mViews存储的是所有Window所对应的View,
// mRoots存储的是所有Window所对应的ViewRootImpl,
// mParams存储的是所有Window所对应的布局参数,
// mDyingViews则存储了那些正在被删除的View对象,或者说是那些已经调用removeView方法但是删除操作还未完成的Window对象
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList
<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingViews = new ArraySet<View>();
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
-
- 通过ViewRootImpI来更新界面并完成Window的添加过程
这个步骤由ViewRootImpl的setView方法来完成,从第4章可以知道,View的绘制过程是由ViewRootImpl来完成的,这里当然也不例外,在setView内部会通过requestLayout来完成异步刷新请求。
- 通过ViewRootImpI来更新界面并完成Window的添加过程
public void requestLayout() {
if (! mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals(); // View绘制的入口
}
}
接着会通过WindowSession最终来完成Window的添加过程。在下面的代码中,mWindowSession的类型是IWindowSession,它是一个Binder对象,真正的实现类是Session,也就是Window的添加过程是一次IPC调用。
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mInputChannel);
} catch (RemoteException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
mInputChannel = null;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
throw new RuntimeException("Adding window failed", e);
}
在Session内部会通过WindowManagerService来实现Window的添加,代码如下所示。
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams
attrs,
int viewVisibility, int displayId, Rect outContentInsets,
InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility,
displayId,
outContentInsets, outInputChannel);
}
Window的添加请求就交给WindowManagerService去处理了,在Window-ManagerService内部会为每一个应用保留一个单独的Session
2. Window的删除过程
Window的删除过程和添加过程一样,都是先通过WindowManagerImpl后,再进一步通过WindowManagerGlobal来实现的。
- 下面是WindowManagerGlobal的removeView的实现:(异步删除)
public void removeView(View view, boolean immediate) {
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
synchronized (mLock) {
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
removeViewLocked(index, immediate);
if (curView == view) {
return;
}
throw new IllegalStateException("Calling with view " + view
+ " but the ViewAncestor is attached to " + curView);
}
}
- removeViewLocked
private void removeViewLocked(int index, boolean immediate) {
// 从 mRoots 中获取对应索引的 ViewRootImpl 对象
ViewRootImpl root = mRoots.get(index);
// 获取 ViewRootImpl 中的 View
View view = root.getView();
// 如果 view 不为 null
if (view != null) {
// 获取 InputMethodManager 实例
InputMethodManager imm = InputMethodManager.getInstance();
// 如果 InputMethodManager 实例不为 null
if (imm != null) {
// 通知输入法管理器窗口被关闭
imm.windowDismissed(mViews.get(index).getWindowToken());
}
}
// 调用 ViewRootImpl 的 die 方法,处理视图的销毁
boolean deferred = root.die(immediate);
// 如果 view 不为 null
if (view != null) {
// 将 view 的父视图设置为 null,解除其与父视图的关系
view.assignParent(null);
// 如果视图被延迟处理(deferred),则将其添加到 mDyingViews 中
if (deferred) {
mDyingViews.add(view);
}
}
}
在WindowManager中提供了两种删除接口removeView和removeViewImmediate,它们分别表示异步删除和同步删除,其中removeViewImmediate使用起来需要特别注意,一般来说不需要使用此方法来删除Window以免发生意外的错误。这里主要说异步删除的情况,具体的删除操作由ViewRoot-Impl的die方法来完成。
3. Window的更新过程
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
// 检查传入的 view 是否为 null
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
// 检查传入的 params 是否是 WindowManager.LayoutParams 的实例
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
}
// 将 params 强制转换为 WindowManager.LayoutParams
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
// 设置视图的布局参数
view.setLayoutParams(wparams);
// 同步代码块,确保线程安全
synchronized (mLock) {
// 找到视图在 mRoots 中的索引
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
// 更新参数列表
mParams.remove(index);
mParams.add(index, wparams);
// 更新 ViewRootImpl 的布局参数
root.setLayoutParams(wparams, false);
}
}
首先它需要更新View的LayoutParams并替换掉老的LayoutParams,接着再更新ViewRootImpl中的LayoutParams,这一步是通过ViewRootImpl的setLayoutParams方法来实现的。在ViewRootImpl中会通过scheduleTraversals方法来对View重新布局,包括测量、布局、重绘这三个过程。除了View本身的重绘以外,ViewRootImpl还会通过WindowSession来更新Window的视图,这个过程最终是由WindowManagerService的relayoutWindow()来具体实现的,它同样是一个IPC过程
8.3 Window的创建过程
1. Activity的Window创建过程
Activity的启动过程很复杂,最终会由ActivityThread中的performLaunchActivity()来完成整个启动过程,在这个方法内部会通过类加载器创建Activity的实例对象,并调用其attach方法为其关联运行过程中所依赖的一系列上下文环境变量
// 示应用程序包信息的对象,通过它获取与该应用程序相关的 ClassLoader。这个类加载器用于加载活动类
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
...
if (activity != null) {
// 为新创建的活动生成一个基础上下文
Context appContext = createBaseContextForActivity(r, activity);
// 加载活动标签
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
// 创建配置对象
Configuration config = new Configuration(mCompatConfiguration);
// 如果 DEBUG_CONFIGURATION 为 true,则记录调试信息,显示正在启动的活动名称和配置
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ r.activityInfo.name + " with config " + config);
// 将活动与上下文和其他必要的信息关联起来。这一步是活动生命周期的一部分
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.voiceInteractor);
...
}
在Activity的attach方法里,系统会创建Activity所属的Window对象并为其设置回调接口,Window对象的创建是通过PolicyManager的makeNewWindow方法实现的。由于Activity实现了Window的Callback接口,因此当Window接收到外界的状态改变时就会回调Activity的方法。Callback接口中的方法很多,但是有几个却是我们都非常熟悉的,比如onAttachedToWindow、onDetachedFromWindow、dispatchTouchEvent,等等,代码如下所示。
// 这段代码的主要目的是在 Android 应用中创建和配置一个新的窗口,确保窗口具有正确的回调、输入模式和用户界面选项。具体功能包括
// 窗口管理 回调机制 用户界面自定义
mWindow = PolicyManager.makeNewWindow(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode ! = WindowManager.LayoutP:PolicyManager**
Activity的Window是通过PolicyManager的一个工厂方法来创建的,但是从PolicyManager的类名可以看出,它不是一个普通的类,它是一个策略类
PolicyManager中实现的几个工厂方法全部在策略接口IPolicy中声明了,IPolicy的定义如下:
```java
public interface IPolicy {
public Window makeNewWindow(Context context);
public LayoutInflater makeNewLayoutInflater(Context context);
public WindowManagerPolicy makeNewWindowManager();
public FallbackEventHandler makeNewFallbackEventHandler(Context
context);
}
在实际的调用中,PolicyManager的真正实现是Policy类,Policy类中的makeNewWindow方法的实现如下,由此可以发现,Window的具体实现的确是PhoneWindow
public Window makeNewWindow(Context context) {
return new PhoneWindow(context);
}
- 分析Activity的视图是怎么附属在Window上的
public void setContentView(int layoutResID) {
getWindow().setContentView(layoutResID); // Activity将具体实现交给了Window处理
initWindowDecorActionBar();
}
- PhoneWindow的setContentView方法
- 如果没有DecorView,那么就创建它
- 将View添加到DecorView的mContentParent中
这一步直接将Activity的视图添加到DecorView的mContentParent中即可.由此可以理解Activity的setContentView这个方法的来历了。不知道读者是否曾经怀疑过:为什么不叫setView呢?它明明是给Activity设置视图的啊!从这里来看,它的确不适合叫setView,因为Activity的布局文件只是被添加到DecorView的mContentParent中,因此叫setContentView更加准确。 - 回调Activity的onContentChanged方法通知Activity视图已经发生改变
到这里为止DecorView已经被创建并初始化完毕,Activity的布局文件也已经成功添加到了DecorView的mContentParent中,但是这个时候DecorView还没有被WindowManager正式添加到Window中。这里需要正确理解Window的概念,Window更多表示的是一种抽象的功能集合,虽然说早在Activity的attach方法中Window就已经被创建了,但是这个时候由于DecorView并没有被WindowManager识别,所以这个时候的Window无法提供具体功能,因为它还无法接收外界的输入信息。在ActivityThread的handleResumeActivity方法中,首先会调用Activity的onResume方法,接着会调用Activity的makeVisible(),正是在makeVisible方法中,DecorView真正地完成了添加和显示这两个过程,到这里Activity的视图才能被用户看到,如下所示。
void makeVisible() {
if (! mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
2. Dialog的Window创建过程
- 创建Window
Dialog中Window的创建同样是通过PolicyManager的makeNewWindow方法来完成的,从8.3.1节中可以知道,创建后的对象实际上就是PhoneWindow,这个过程和Activity的Window的创建过程是一致的 - 初始化DecorView并将DiaIog的视图添加到DecorView中
public void setContentView(int layoutResID) {
mWindow.setContentView(layoutResID);
}
- 将DecorView添加到Window中并显示
当Dialog被关闭时,它会通过WindowManager来移除DecorView:mWindowManager.removeViewImmediate(mDecor)
mWindowManager.addView(mDecor, l);
mShowing = true;
- 其他事项
普通的Dialog有一个特殊之处,那就是必须采用Activity的Context,如果采用Application的Context,那么就会报错。
如果是系统的Digalog,可以选用TYPE_SYSTEM_OVERLAY来指定对话框的Window类型为系统Window,如下所示。
dialog.getWindow().setType(LayoutParams.TYPE_SYSTEM_ERROR)
// 声明权限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
3. Toast的Window创建过程
Toast也是基于Window来实现的,但是由于Toast具有定时取消这一功能,所以系统采用了Handler。在Toast的内部有两类IPC过程,第一类是Toast访问NotificationManagerService(NMS),第二类是Notification-ManagerService回调Toast里的TN接口。
TN这个类,它是一个Binder类****,在Toast和NMS进行IPC的过程中,当NMS处理Toast的显示或隐藏请求时会跨进程回调TN中的方法,这个时候由于TN运行在Binder线程池中,所以需要通过Handler将其切换到当前线程中。
Toast提供了show和cancel分别用于显示和隐藏Toast,它们的内部是一个IPC过程,show方法和cancel方法的实现如下:
public void show() {
// 检查 mNextView 是否为 null,如果是,则抛出异常
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
// 获取系统的通知管理服务
INotificationManager service = getService();
// 获取当前应用程序的包名
String pkg = mContext.getOpPackageName();
// 获取通知的 TN(Toast Notification)实例
TN tn = mTN;
// 将下一个视图设置到 TN 实例中
tn.mNextView = mNextView;
try {
// 将 Toast 请求排入服务队列
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// 捕获远程异常,但不执行任何操作
// 这里可以记录错误或其他处理,但当前为空
}
}
public void cancel() {
// 通过 TN 实例隐藏 Toast
mTN.hide();
try {
// 取消当前应用程序的 Toast
getService().cancelToast(mContext.getPackageName(), mTN);
} catch (RemoteException e) {
// 捕获远程异常,但不执行任何操作
// 这里可以记录错误或其他处理,但当前为空
}
}
9. 四大组件的工作过程
9.1 四大组件的运行状态
- 除了BroadcastReceiver以外,其他三种组件都必须在Android-Manifest中注册,对于BroadcastReceiver来说,它既可以在AndroidManifest中注册也可以通过代码来注册
- 在调用方式上,Activity、Service和BroadcastReceiver需要借助Intent,而ContentProvider则无须借助Intent
- Activity是一种展示型组件
- Service是一种计算型组件,用于在后台执行一系列计算任务
Service组件却有两种状态:启动状态和绑定状态。当Service组件处于启动状态时,这个时候Service内部可以做一些后台计算,并且不需要和外界有直接的交互。尽管Service组件是用于执行后台计算的,但是它本身是运行在主线程中的,因此耗时的后台计算仍然需要在单独的线程中去完成。当Service组件处于绑定状态时,这个时候Service内部同样可以进行后台计算,但是处于这种状态时外界可以很方便地和Service组件进行通信。
Service组件也是可以停止的,停止一个Service组件稍显复杂,需要灵活采用stopService和unBindService这两个方法才能完全停止一个Service组件 - BroadcastReceiver是一种消息型组件,用于在不同的组件乃至不同的应用之间传递消息
- ContentProvider是一种数据共享型组件,用于向其他组件乃至其他应用共享数据
9.2 Activity的工作过程
1. startActivityForResult方法
public void startActivityForResult(Intent intent, int requestCode,
@Nullable Bundle options) {
// 检查当前活动是否有父活动(即是否为根活动)
if (mParent == null) {
// 使用 Instrumentation 来启动活动并获取结果
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
// 如果返回的活动结果不为 null,则发送活动结果
if (ar != null) {
mMainThread.sendActivityResult(
mToken, mEmbeddedID, requestCode, ar.getResultCode(),
ar.getResultData());
}
// 如果请求代码大于等于 0,表示请求结果
if (requestCode >= 0) {
// 设置标志,表示活动已启动,避免在收到结果之前显示活动
// 这可以防止在活动创建或恢复时闪烁
mStartedActivity = true;
}
// 获取当前窗口的装饰视图(如果窗口不为 null)
final View decor = mWindow != null ? mWindow.peekDecorView() : null;
// 如果装饰视图不为 null,则取消其挂起的输入事件
if (decor != null) {
decor.cancelPendingInputEvents();
}
// TODO: 考虑清除/刷新其他子窗口的事件源和事件
} else {
// 如果当前活动有父活动,则通过父活动启动新活动
if (options != null) {
// 如果有选项 Bundle,使用带选项的启动方法
mParent.startActivityFromChild(this, intent, requestCode, options);
} else {
// 如果没有选项,仍然通过父活动启动,以保持兼容性
// 兼容已重写此方法的现有应用程序
mParent.startActivityFromChild(this, intent, requestCode);
}
}
// 如果有选项 Bundle 且当前活动不是任务的顶部,则启动退出过渡动画
if (options != null && !isTopOfTask()) {
mActivityTransitionState.startExitOutTransition(this, options);
}
}
只需要关注mParent == null的逻辑
mParent代表的是ActivityGroup, ActivityGroup最开始被用来在一个界面中嵌入多个子Activity,但是其在API 13中已经被废弃了,系统推荐采用Fragment来代替ActivityGroup。
**mMainThread.getApplicationThread()**这个参数,它的类型是ApplicationThread, ApplicationThread是ActivityThread的一个内部类,通过后面的分析可以发现,ApplicationThread和ActivityThread在Activity的启动过程中发挥着很重要的作用
2. Instrumentation的execStartActivity方法
- 整体代码流程
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
if (mActivityMonitors ! = null) {
synchronized (mSync) {
final int N = mActivityMonitors.size();
for (int i=0; i<N; i++) {
final ActivityMonitor am = mActivityMonitors.get(i);
if (am.match(who, null, intent)) {
am.mHits++;
if (am.isBlocking()) {
return requestCode >= 0 ? am.getResult() : null;
}
break;
}
}
}
}
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess();
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target ! = null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
}
return null;
}
启动Activity真正的实现由ActivityManagerNative.getDefault()的startActivity方法来完成 实际上是AMS
- checkStartActivityResult
Instrumentation的execStartActivity方法,其中有一行代码:checkStartActivityResult(result, intent),直观上看起来这个方法的作用像是在检查启动Activity的结果
/** @hide */
public static void checkStartActivityResult(int res, Object intent) {
if (res >= ActivityManager.START_SUCCESS) {
return;
}
switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
if(intent instanceof Intent&&((Intent)intent).getComponent() ! =
null)
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your
AndroidManifest.xml? ");
throw new ActivityNotFoundException(
"No Activity found to handle " + intent);
case ActivityManager.START_PERMISSION_DENIED:
throw new SecurityException("Not allowed to start activity "
+ intent);
case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
throw new AndroidRuntimeException(
"FORWARD_RESULT_FLAG used while also requesting a
result");
case ActivityManager.START_NOT_ACTIVITY:
throw new IllegalArgumentException(
"PendingIntent is not an activity");
case ActivityManager.START_NOT_VOICE_COMPATIBLE:
throw new SecurityException(
"Starting under voice control not allowed for: " +
intent);
default:
throw new AndroidRuntimeException("Unknown error code "
+ res + " when starting " + intent);
}
}
checkStartActivityResult的作用很明显,就是检查启动Activity的结果。当无法正确地启动一个Activity时,这个方法会抛出异常信息,其中最熟悉不过的就是“Unable to find explicit activity class; have you declared this activity in your AndroidManifest.xml? ”这个异常了,当待启动的Activity没有在AndroidManifest中注册时,就会抛出这个异常
3. ActivityManagerNative.getDefault()的startActivity
此处逻辑较多 略过,最终的点是:
Activity的启动过程最终回到了ApplicationThread中,ApplicationThread通过scheduleLaunchActivity方法来启动Activity
public final int startActivity(IApplicationThread caller, String callingPackage,
Intent intent, String resolvedType, IBinder resultTo, String
resultWho, int requestCode,
int startFlags, ProfilerInfo profilerInfo, Bundle options) {
return startActivityAsUser(caller, callingPackage, intent, resolved-
Type, resultTo,
resultWho, requestCode, startFlags, profilerInfo, options,
UserHandle.getCallingUserId());
}
public final int startActivityAsUser(IApplicationThread caller, String
callingPackage,
Intent intent, String resolvedType, IBinder resultTo, String
resultWho, int requestCode,
int startFlags, ProfilerInfo profilerInfo, Bundle options, int userId) {
enforceNotIsolatedCaller("startActivity");
userId = handleIncomingUser(Binder.getCallingPid(), Binder.getCalling-
Uid(), userId,
false, ALLOW_FULL_ONLY, "startActivity", null);
// TODO: Switch to user app stacks here.
return mStackSupervisor.startActivityMayWait(caller, -1, calling
Package, intent,
resolvedType, null, null, resultTo, resultWho, requestCode,
startFlags,
profilerInfo, null, null, options, userId, null, null);
}
4. ApplicationThread的scheduleLaunchActivity
scheduleLaunchActivity的实现很简单,就是发送一个启动Activity的消息交由Handler处理,这个Handler有着一个很简洁的名字:H。sendMessage的作用是发送一个消息给H处理.
从Handler H对“LAUNCH_ACTIVITY”这个消息的处理可以知道,Activity的启动过程由ActivityThread的handleLaunchActivity方法来实现
// we use token to identify this activity without having to send the
// activity itself back to the activity manager. (matters more with ipc)
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
ActivityInfo info, Configuration curConfig, CompatibilityInfo
compatInfo,
IVoiceInteractor voiceInteractor, int procState, Bundle state,
PersistableBundle persistentState, List<ResultInfo> pendingResults,
List<Intent> pendingNewIntents, boolean notResumed, boolean isForward,
ProfilerInfo profilerInfo) {
updateProcessState(procState, false);
ActivityClientRecord r = new ActivityClientRecord();
r.token = token;
r.ident = ident;
r.intent = intent;
r.voiceInteractor = voiceInteractor;
r.activityInfo = info;
r.compatInfo = compatInfo;
r.state = state;
r.persistentState = persistentState;
r.pendingResults = pendingResults;
r.pendingIntents = pendingNewIntents;
r.startsNotResumed = notResumed;
r.isForward = isForward;
r.profilerInfo = profilerInfo;
updatePendingConfiguration(curConfig);
sendMessage(H.LAUNCH_ACTIVITY, r);
}
5. Activity的启动过程由ActivityThread的handleLaunchActivity方法来实现
- 整体流程
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent){
...
if (localLOGV) Slog.v(
TAG, "Handling launch of " + r);
Activity a = performLaunchActivity(r, customIntent);
if (a ! = null) {
r.createdConfig = new Configuration(mConfiguration);
Bundle oldState = r.state;
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && ! r.startsNotResumed);
...
}
...
}
- performLaunchActivity
从ActivityCIientRecord中获取待启动的Activity的组件信息
通过Instrumentation的newActivity方法使用类加载器创建Activity对象
通过LoadedApk的makeAppIication方法来尝试创建AppIication对象
创建ContextImpI对象并通过Activity的attach方法来完成一些重要数据的初始化
调用Activity的onCreate方法 - handleResumeActivity
ActivityThread通过handleResumeActivity方法来调用被启动Activity的onResume这一生命周期方法。
9.3 Service的工作过程
Service分为两种工作状态,一种是启动状态,主要用于执行后台计算;另一种是绑定状态,主要用于其他组件和Service的交互。需要注意的是,Service的这两种状态是可以共存的,即Service既可以处于启动状态也可以同时处于绑定状态
Intent intentService = new Intent(this, MyService.class);
startService(intentService)
bindService(intentService, mServiceConnection, BIND_AUTO_CREATE);
1. Service的启动过程
- Service的启动过程从ContextWrapper的startActivity开始,如下所示。
public ComponentName startService(Intent service) {
return mBase.startService(service); //mBase的类型是ContextImpl,
}
- ActivityManagerNative.getDefault()这个对象来启动一个服务(AMS)
- scheduleCreateService
public final void scheduleCreateService(IBinder token,
ServiceInfo info, CompatibilityInfo compatInfo, int processState) {
updateProcessState(processState, false);
CreateServiceData s = new CreateServiceData();
s.token = token;
s.info = info;
s.compatInfo = compatInfo;
sendMessage(H.CREATE_SERVICE, s);
}
这个过程和Activity的启动过程是类似的,都是通过发送消息给Handler H来完成的。H会接收这个CREATE_SERVICE消息并通过ActivityThread的handleCreateService方法来完成Service的最终启动
- handleCreateService
首先通过类加载器创建Service的实例。
然后创建Application对象并调用其onCreate,当然Application的创建过程只会有一次。
接着创建ConTextImpl对象并通过Service的attach方法建立二者之间的关系,这个过程和Activity实际上是类似的,毕竟Service和Activity都是一个Context。
最后调用Service的onCreate方法并将Service对象存储到ActivityThread中的一个列表中
2. Service的绑定过程
- Service的绑定过程也是从ContextWrapper开始的
public boolean bindService(Intent service, ServiceConnection conn,
int flags) {
return mBase.bindService(service, conn, flags);
}
- 和启动Service不同的是,Service的绑定过程会调用app.thread的scheduleBindService方法,这个过程的实现在ActiveServices的requestServiceBindingLocked方法中
- app.thread这个对象多次出现过,对于它我们应该再熟悉不过了,它实际上就是ApplicationThread。ApplicationThread的一系列以schedule开头的方法,其内部都是通过Handler H来中转的,对于scheduleBindService方法来说也是如此,它的实现如下所示。
public final void scheduleBindService(IBinder token, Intent intent,
boolean rebind, int processState) {
updateProcessState(processState, false);
BindServiceData s = new BindServiceData();
s.token = token;
s.intent = intent;
s.rebind = rebind;
if (DEBUG_SERVICE)
Slog.v(TAG, "scheduleBindService token=" + token + " intent=" +
intent + " uid="
+ Binder.getCallingUid() + " pid=" + Binder.getCallingPid());
sendMessage(H.BIND_SERVICE, s);
}
- 客户端的onServiceConnected方法执行后,Service的绑定过程也就分析完成了
9.4 BroadcastReceiver的工作过程
- 首先要定义广播接收者,只需要继承BroadcastReceiver并重写onReceive方法即可
public class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// onReceive函数不能做耗时的事情,参考值:10s以内
Log.d("scott", "on receive action=" + intent.getAction());
String action = intent.getAction();
// do some works
}
}
- 定义好了广播接收者,接着还需要注册广播接收者,注册分为两种方式,既可以在AndroidManifest文件中静态注册,也可以通过代码动态注册。
<receiver android:name=".MyReceiver" >
<intent-filter>
<action android:name="com.ryg.receiver.LAUNCH" />
</intent-filter>
</receiver>
IntentFilter filter = new IntentFilter();
filter.addAction("com.ryg.receiver.LAUNCH");
registerReceiver(new MyReceiver(), filter);
- send方法来发送广播
Intent intent = new Intent();
intent.setAction("com.ryg.receiver.LAUNCH");
sendBroadcast(intent);
1. 广播的注册过程
- 广播的注册分为静态注册和动态注册,其中静态注册的广播在应用安装时由系统自动完成注册,具体来说是由PMS(PackageManagerService)来完成整个注册过程的,除了广播以外,其他三大组件也都是在应用安装时由PMS解析并注册的
- 动态注册的过程是从ContextWrapper的registerReceiver方法开始的
- 注册广播的真正实现过程是在AMS中,AMS的registerReceiver方法
public Intent registerReceiver(IApplicationThread caller, String callerPackage,
IIntentReceiver receiver, IntentFilter filter, String permission, int
userId) {
...
mRegisteredReceivers.put(receiver.asBinder(), rl);
BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage,
permission, callingUid, userId);
rl.add(bf);
if (! bf.debugCheck()) {
Slog.w(TAG, "==> For Dynamic broadast");
}
mReceiverResolver.addFilter(bf);
...
}
2. 广播的发送和接收过程
- 广播的发送仍然开始于ContextWrapper的sendBroadcast方法,之所以不是Context,那是因为Context的sendBroadcast是一个抽象方法
public void sendBroadcast(Intent intent) {
warnIfCallingFromSystemProcess();
String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
try {
intent.prepareToLeaveProcess();
ActivityManagerNative.getDefault().broadcastIntent(
mMainThread.getApplicationThread(), intent, resolvedType, null,
Activity.RESULT_OK, null, null, null, AppOpsManager.OP_NONE,
false, false,
getUserId());
} catch (RemoteException e) {
}
}
- 最终调用LoadedApk.ReceiverDispatcher的performReceive方法
public void performReceive(Intent intent, int resultCode, String data,
Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
if (ActivityThread.DEBUG_BROADCAST) {
int seq = intent.getIntExtra("seq", -1);
Slog.i(ActivityThread.TAG, "Enqueueing broadcast " + intent.
getAction() + " seq=" + seq + " to " + mReceiver);
}
Args args = new Args(intent, resultCode, data, extras, ordered,
sticky, sendingUser);
if (! mActivityThread.post(args)) {
if (mRegistered && ordered) {
IActivityManager mgr = ActivityManagerNative.getDefault();
if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG,
"Finishing sync broadcast to " + mReceiver);
args.sendFinished(mgr);
}
}
}
在上面的代码中,会创建一个Args对象并通过mActivityThread的post方法来执行Args中的逻辑,而Args实现了Runnable接口。mActivityThread是一个Handler,它其实就是ActivityThread中的mH, mH的类型是ActivityThread的内部类H,关于H这个类前面已经介绍过了,这里就不再多说了。在Args的run方法中有如下几行代码:
final BroadcastReceiver receiver = mReceiver;
receiver.setPendingResult(this);
receiver.onReceive(mContext, intent);
这个时候BroadcastReceiver的onReceive方法被执行了,也就是说应用已经接收到广播了,同时onReceive方法是在广播接收者的主线程中被调用的。
9.5 ContentProvider的工作过程
当一个应用启动时,入口方法为ActivityThread的main方法,main方法是一个静态方法,在main方法中会创建ActivityThread的实例并创建主线程的消息队列,然后在ActivityThread的attach方法中会远程调用AMS的attachApplication方法并将ApplicationThread对象提供给AMS。
ApplicationThread是一个Binder对象,它的Binder接口是IApplicationThread,它主要用于ActivityThread和AMS之间的通信,这一点在前面多次提到。在AMS的attachApplication方法中,会调用ApplicationThread的bindApplication方法,注意这个过程同样是跨进程完成的,bindApplication的逻辑会经过ActivityThread中的mH Handler切换到ActivityThread中去执行,具体的方法是handleBindApplication。在handleBindApplication方法中,ActivityThread会创建Application对象并加载ContentProvider。需要注意的是,ActivityThread会先加载ContentProvider,然后再调用Application的onCreate方法,整个流程如下图。
一般来说,ContentProvider都应该是单实例的。ContentProvider到底是不是单实例,这是由它的android:multiprocess属性来决定的,当android:multiprocess为false时,ContentProvider是单实例,这也是默认值;当android:multiprocess为true时,ContentProvider为多实例,这个时候在每个调用者的进程中都存在一个ContentProvider对象。由于在实际的开发中,并未发现多实例的ContentProvider的具体使用场景,官方文档中的解释是这样可以避免进程间通信的开销,但是这在实际开发中仍然缺少使用价值
10. Android的消息机制
Android的消息机制主要是指Handler的运行机制,Handler的运行需要底层的MessageQueue和Looper的支撑
- MessageQueue的中文翻译是消息队列,顾名思义,它的内部存储了一组消息,以队列的形式对外提供插入和删除的工作。虽然叫消息队列,但是它的内部存储结构并不是真正的队列,而是采用单链表的数据结构来存储消息列表。
- Looper的中文翻译为循环,在这里可以理解为消息循环。由于MessageQueue只是一个消息的存储单元,它不能去处理消息,而Looper就填补了这个功能,Looper会以无限循环的形式去查找是否有新消息,如果有的话就处理消息,否则就一直等待着。
- Looper中还有一个特殊的概念,那就是ThreadLocal,ThreadLocal并不是线程,它的作用是可以在每个线程中存储数据。我们知道,Handler创建的时候会采用当前线程的Looper来构造消息循环系统,那么Handler内部如何获取到当前线程的Looper呢?这就要使用ThreadLocal了,ThreadLocal可以在不同的线程中互不干扰地存储并提供数据,通过ThreadLocal可以轻松获取每个线程的Looper。
线程是默认没有Looper的,如果需要使用Handler就必须为线程创建Looper
ActivityThread被创建时就会初始化Looper,这也是在主线程中默认可以使用Handler的原因,不用手动创建Looper
10.1. Android的消息机制概述
系统之所以提供Handler,主要原因就是为了解决在子线程中无法访问UI的矛盾。延伸一点
- 系统为什么不允许在子线程中访问UI呢?
这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态, - 那为什么系统不对UI控件的访问加上锁机制呢?
缺点有两个:
首先加上锁机制会让UI访问的逻辑变得复杂;
其次锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。 - 一个线程对应一个Looper,一个MessageQueue,但是Handler可以有多个
当Handler的send方法被调用时,它会调用MessageQueue的enqueueMessage方法将这个消息放入消息队列中,然后Looper发现有新消息到来时,就会处理这个消息,最终消息中的Runnable或者Handler的handleMessage方法就会被调用。注意Looper是运行在创建Handler所在的线程中的,这样一来Handler中的业务逻辑就被切换到创建Handler所在的线程中去执行了
10.2. Android的消息机制分析
1. ThreadLocal的工作原理
ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据。
- ThreadLocal的使用场景
1.一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。比如对于Handler来说,它需要获取当前线程的Looper,很显然Looper的作用域就是线程并且不同线程具有不同的Looper,这个时候通过ThreadLocal就可以轻松实现Looper在线程中的存取
2.复杂逻辑下的对象传递,比如监听器的传递,有些时候一个线程中的任务过于复杂,这可能表现为函数调用栈比较深以及代码入口的多样性,在这种情况下,我们又需要监听器能够贯穿整个线程的执行过程 - ThreadLocal的内部实现
- set方法
public void set(T value) {
Thread currentThread = Thread.currentThread(); //获取当前线程
Values values = values(currentThread); //查找当前线程的本地储存区
if (values == null) {
//当线程本地存储区,尚未存储该线程相关信息时,则创建Values对象
values = initializeValues(currentThread);
}
//保存数据value到当前线程this
values.put(this, value);
}
ThreadLocal的值到底是如何在localValues中进行存储的。在localValues内部有一个数组:private Object[] table, ThreadLocal的值就存在在这个table数组中
- localValues是如何使用put方法将ThreadLocal的值存储到table数组中的
void put(ThreadLocal<?> key, Object value) {
cleanUp();
// Keep track of first tombstone. That's where we want to go back
// and add an entry if necessary.
int firstTombstone = -1;
//在哈希表中查找一个特定 key 的位置,初始位置由 key.hash & mask 计算得出,
//然后根据某种算法(由 next 方法定义)在出现哈希冲突时继续寻找下一个可能的位置。
//这个过程会不断进行,直到找到合适的位置或达到某个终止条件
for (int index = key.hash & mask;; index = next(index)) {
Object k = table[index];
if (k == key.reference) {
// Replace existing entry.
table[index + 1] = value;
return;
}
if (k == null) {
if (firstTombstone == -1) {
// Fill in null slot.
table[index] = key.reference;
table[index + 1] = value;
size++;
return;
}
// Go back and replace first tombstone.
table[firstTombstone] = key.reference;
table[firstTombstone + 1] = value;
tombstones--;
size++;
return;
}
// Remember first tombstone.
if (firstTombstone == -1 && k == TOMBSTONE) {
firstTombstone = index;
}
}
}
上面的代码实现了数据的存储过程,这里不去分析它的具体算法,但是我们可以得出一个存储规则,那就是ThreadLocal的值在table数组中的存储位置总是为ThreadLocal的reference字段所标识的对象的下一个位置
- get方法
@SuppressWarnings("unchecked")
public T get() {
Thread currentThread = Thread.currentThread(); //获取当前线程
Values values = values(currentThread); //查找当前线程的本地储存区
if (values != null) {
Object[] table = values.table;
int index = hash & values.mask;
if (this.reference == table[index]) {
return (T) table[index + 1]; //返回当前线程储存区中的数据
}
} else {
//创建Values对象
values = initializeValues(currentThread);
}
return (T) values.getAfterMiss(this); //从目标线程存储区没有查询是则返回null
}
- 总结
从ThreadLocal的set和get方法可以看出,它们所操作的对象都是当前线程的localValues对象的table数组,因此在不同线程中访问同一个ThreadLocal的set和get方法,它们对ThreadLocal所做的读/写操作仅限于各自线程的内部,这就是为什么ThreadLocal可以在多个线程中互不干扰地存储和修改数据
2.消息队列的工作原理
消息队列在Android中指的是MessageQueue, MessageQueue主要包含两个操作:插入和读取。
读取操作本身会伴随着删除操作,插入和读取对应的方法分别为enqueueMessage和next,其中
- enqueueMessage的作用是往消息队列中插入一条消息,
- 而next的作用是从消息队列中取出一条消息并将其从消息队列中移除。
尽管MessageQueue叫消息队列,但是它的内部实现并不是用的队列,实际上它是通过一个单链表的数据结构来维护消息列表,单链表在插入和删除上比较有优势。
- MessageQueue是消息机制的Java层和C++层的连接纽带,大部分核心方法都交给native层来处理,其中MessageQueue类中涉及的native方法如下:
private native static long nativeInit();
private native static void nativeDestroy(long ptr);
private native void nativePollOnce(long ptr, int timeoutMillis);
private native static void nativeWake(long ptr);
private native static boolean nativeIsPolling(long ptr);
private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events);
- enqueueMessage
boolean enqueueMessage(Message msg, long when) {
// 每一个普通Message必须有一个target
if (msg.target == null) {
// 检查消息的目标是否为空,如果为空则抛出异常。
throw new IllegalArgumentException("Message must have a target.");
}
if (msg.isInUse()) {
// 检查消息是否已经在使用中,如果是,则抛出异常。
throw new IllegalStateException(msg + " This message is already in use.");
}
synchronized (this) {
// 对当前对象进行同步,确保线程安全。
if (mQuitting) { //正在退出时,回收msg,加入到消息池
// 检查是否正在退出,如果是,则回收消息并返回false。
msg.recycle();
return false;
}
msg.markInUse();
// 将消息标记为正在使用中。
msg.when = when;
// 设置消息的触发时间。
Message p = mMessages;
// 获取当前消息队列中的第一个消息。
boolean needWake;
// 声明一个布尔变量用于判断是否需要唤醒。
if (p == null || when == 0 || when < p.when) {
// 如果消息队列为空,或者消息的触发时间为0,或者消息的触发时间早于队列中最早的消息。
msg.next = p;
// 将新消息的下一个指针指向当前第一个消息。
mMessages = msg;
// 将新消息设为队列的第一个消息。
needWake = mBlocked; //当阻塞时需要唤醒
// 检查当前是否处于阻塞状态,如果是则需要唤醒。
} else {
// 将消息按时间顺序插入到MessageQueue。
needWake = mBlocked && p.target == null && msg.isAsynchronous();
// 检查是否需要唤醒,条件是处于阻塞状态、队头没有目标并且消息是异步的。
Message prev;
// 声明一个消息变量用于跟踪前一个消息。
for (;;) {
// 进入无限循环,直到找到插入位置。
prev = p;
// 将当前消息指针赋值给前一个消息。
p = p.next;
// 移动到下一个消息。
if (p == null || when < p.when) {
// 如果下一个消息为空或者新消息的触发时间早于下一个消息的触发时间。
break;
// 退出循环。
}
if (needWake && p.isAsynchronous()) {
// 如果需要唤醒且当前消息是异步的。
needWake = false;
// 设置需要唤醒为false。
}
}
msg.next = p;
// 将新消息的下一个指针指向找到的插入位置。
prev.next = msg;
// 将前一个消息的下一个指针指向新消息。
}
//消息没有退出,我们认为此时mPtr != 0
if (needWake) {
// 如果需要唤醒。
nativeWake(mPtr);
// 调用本地方法唤醒消息队列。
}
}
return true;
// 返回true表示消息已成功入队。
}
- next方法
Message next() {
final long ptr = mPtr; // 获取当前消息队列的指针
if (ptr == 0) { // 当消息循环已经退出,则直接返回
return null; // 返回空,表示没有消息
}
int pendingIdleHandlerCount = -1; // 循环迭代的首次为-1,表示未初始化
int nextPollTimeoutMillis = 0; // 初始化下一次轮询的超时毫秒数
for (;;) { // 无限循环,直到返回消息或退出
if (nextPollTimeoutMillis != 0) { // 如果有超时设置
Binder.flushPendingCommands(); // 刷新待处理的命令
}
// 阻塞操作,当等待nextPollTimeoutMillis时长,或者消息队列被唤醒,都会返回
// 它只是表明所有消息的处理已完成, 线程正在等待下一个消息.
nativePollOnce(ptr, nextPollTimeoutMillis); // 进行一次原生的轮询
synchronized (this) { // 进入同步块,确保线程安全
final long now = SystemClock.uptimeMillis(); // 获取当前的系统时间
Message prevMsg = null; // 初始化前一条消息
Message msg = mMessages; // 获取当前消息队列的第一条消息
// 当消息的Handler为空时,则查询异步消息
if (msg != null && msg.target == null) { // 确保当前消息存在且目标为空
// 当查询到异步消息,则立刻退出循环
do {
prevMsg = msg; // 更新前一条消息为当前消息
msg = msg.next; // 移动到下一条消息
} while (msg != null && !msg.isAsynchronous()); // 循环直到找到异步消息或消息为空
}
if (msg != null) { // 如果找到了消息
if (now < msg.when) { // 如果当前时间小于消息的触发时间
// 当异步消息触发时间大于当前时间,则设置下一次轮询的超时时长
nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); // 计算超时时间
} else { // 当前时间已经超过或者等于消息的触发时间
// 获取一条消息,并返回
mBlocked = false; // 标记为不被阻塞
if (prevMsg != null) { // 如果前一条消息不为空
prevMsg.next = msg.next; // 将前一条消息的next指向当前消息的下一条
} else { // 如果前一条消息为空
mMessages = msg.next; // 更新消息队列的头为当前消息的下一条
}
msg.next = null; // 清空当前消息的next引用
// 设置消息的使用状态,即flags |= FLAG_IN_USE
msg.markInUse(); // 标记消息为正在使用
return msg; // 成功获取MessageQueue中的下一条消息并返回
}
} else { // 如果没有消息
// 没有消息
nextPollTimeoutMillis = -1; // 将超时设置为-1,表示不再等待
}
// 消息正在退出,返回null
if (mQuitting) { // 如果正在退出状态
dispose(); // 处理清理工作
return null; // 返回空,表示没有消息
}
// 当消息队列为空,或者是消息队列的第一个消息时
if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) { // 检查空闲处理器数量
pendingIdleHandlerCount = mIdleHandlers.size(); // 获取空闲处理器的数量
}
if (pendingIdleHandlerCount <= 0) { // 如果没有空闲处理器
// 没有idle handlers需要运行,则循环并等待
mBlocked = true; // 标记为阻塞状态
continue; // 继续下一次循环
}
if (mPendingIdleHandlers == null) { // 如果待处理的空闲处理器数组为空
mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; // 初始化数组
}
mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); // 将当前空闲处理器转换为数组
}
// 只有第一次循环时,会运行idle handlers,执行完成后,重置pendingIdleHandlerCount为0
for (int i = 0; i < pendingIdleHandlerCount; i++) { // 遍历所有空闲处理器
final IdleHandler idler = mPendingIdleHandlers[i]; // 获取当前空闲处理器
mPendingIdleHandlers[i] = null; // 去掉handler的引用,以避免内存泄漏
boolean keep = false; // 标记是否继续保持该处理器
try {
keep = idler.queueIdle(); // 执行空闲时的操作
} catch (Throwable t) { // 捕获异常
Log.wtf(TAG, "IdleHandler threw exception", t); // 输出错误日志
}
if (!keep) { // 如果不需要保持
synchronized (this) { // 进入同步块,确保线程安全
mIdleHandlers.remove(idler); // 从空闲处理器列表中移除该处理器
}
}
}
// 重置idle handler个数为0,以保证不会再次重复运行
pendingIdleHandlerCount = 0; // 重置计数
// 当调用一个空闲handler时,一个新message能够被分发,因此无需等待可以直接查询pending message
nextPollTimeoutMillis = 0; // 将超时设置为0,准备下次查询
}
}
可以发现next方法是一个无限循环的方法,如果消息队列中没有消息,那么next方法会一直阻塞在这里。
当有新消息到来时,next方法会返回这条消息并将其从单链表中移除。
3.Looper的工作原理
Looper在Android的消息机制中扮演着消息循环的角色,具体来说就是它会不停地从MessageQueue中查看是否有新消息,如果有新消息就会立刻处理,否则就一直阻塞在那里
- 构造方法中它会创建一个MessageQueue即消息队列,然后将当前线程的对象保存起来
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
- 为一个线程创建Looper
// 通过Looper.prepare()即可为当前线程创建一个Looper,接着通过Looper.loop()来开启消息循环
new Thread("Thread#2") {
@Override
public void run() {
Looper.prepare();
Handler handler = new Handler();
Looper.loop();
};
}.start();
prepareMainLooper方法,这个方法主要是给**主线程(UI线程)**也就是ActivityThread所在的线程创建Looper使用的,其本质也是通过prepare方法来实现的.getMainLooper方法,通过它可以在任何地方获取到主线程的Looper。
- 退出Looper
Looper也是可以退出的,Looper提供了quit和quitSafely来退出一个Looper,二者的区别是:quit会直接退出Looper,而quitSafely只是设定一个退出标记,然后把消息队列中的已有消息处理完毕后才安全地退出。
Looper退出后,通过Handler发送的消息会失败,这个时候Handler的send方法会返回false。在子线程中,如果手动为其创建了Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。
- Looper最重要的一个方法是loop方法,只有调用了loop后,消息循环系统才会真正地起作用
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
final Looper me = myLooper();//获取TLS存储的Looper对象
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;//获取Looper对象中的消息队列
Binder.clearCallingIdentity();
//确保在权限检查时基于本地进程,而不是调用进程。
final long ident = Binder.clearCallingIdentity();
for (;;) {//进入loop的主循环方法
Message msg = queue.next(); //可能会阻塞
if (msg == null) {
//没有消息,则退出循环
return;
}
//默认为null,可通过setMessageLogging()方法来指定输出,用于debug功能
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
msg.target.dispatchMessage(msg);//用于分发Message
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
//恢复调用者信息
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycleUnchecked();//将Message放入消息池
}
}
Looper的loop方法的工作过程也比较好理解,loop方法是一个死循环,唯一跳出循环的方式是MessageQueue的next方法返回了null。当Looper的quit方法被调用时,Looper就会调用MessageQueue的quit或者quitSafely方法来通知消息队列退出,当消息队列被标记为退出状态时,它的next方法就会返回null。也就是说,Looper必须退出,否则loop方法就会无限循环下去。
loop方法会调用MessageQueue的next方法来获取新消息,而next是一个阻塞操作,当没有消息时,next方法会一直阻塞在那里,这也导致loop方法一直阻塞在那里。如果MessageQueue的next方法返回了新消息,Looper就会处理这条消息:msg.target.dispatchMessage(msg),这里的msg.target是发送这条消息的Handler对象,这样Handler发送的消息最终又交给它的dispatchMessage方法来处理了。但是这里不同的是,Handler的dispatchMessage方法是在创建Handler时所使用的Looper中执行的,这样就成功地将代码逻辑切换到指定的线程中去执行了。
4. Handler的工作原理
Handler的工作主要包含消息的发送和接收过程。消息的发送可以通过post的一系列方法以及send的一系列方法来实现,post的一系列方法最终是通过send的一系列方法来实现的。
- 发送一条消息的典型过程如下所示
public final boolean sendMessage(Message msg)
{
return sendMessageDelayed(msg, 0);
}
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
Handler发送消息的过程仅仅是向消息队列中插入了一条消息,MessageQueue的next方法就会返回这条消息给Looper(Looper.loop方法内部调用了MessageQueue的next方法), Looper收到消息后就开始处理了,最终消息由Looper交由Handler处理,即Handler的dispatchMessage方法会被调用,这时Handler就进入了处理消息的阶段。
- 处理消息
public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
首先,检查Message的callback是否为null,不为null就通过handleCallback来处理消息。Message的callback是一个Runnable对象,实际上就是Handler的post方法所传递的Runnable参数
其次,callback(就是Handler的post方法所传递的Runnable参数)为null,再检查mCallback是否为null,不为null就调用mCallback的handleMessage方法来处理消息。
最后,调用Handler的handleMessage方法来处理消息
通过Callback可以采用如下方式来创建Handler对象:Handler handler = new Handler(callback)。那么Callback的意义是什么呢?源码里面的注释已经做了说明:可以用来创建一个Handler的实例但并不需要派生Handler的子类。
10.3 主线程的消息循环
- Android的主线程就是ActivityThread所在的UI线程,主线程的入口方法为ActivityThread的main,在main方法中系统会通过Looper.prepareMainLooper()来创建主线程的Looper以及MessageQueue,并通过Looper.loop()来开启主线程的消息循环
- 主线程的消息循环开始了以后,ActivityThread还需要一个Handler来和消息队列进行交互,这个Handler就是ActivityThread.H,它内部定义了一组消息类型,主要包含了四大组件的启动和停止等过程
- 主线程的消息循环模型
ActivityThread通过它的内部类ApplicationThread和AMS进行进程间通信,AMS以进程间通信的方式完成ActivityThread的请求后会回调ApplicationThread中的Binder方法,然后ApplicationThread会向H发送消息,H收到消息后会将ApplicationThread中的逻辑切换到ActivityThread中去执行,即切换到主线程中去执行,这个过程就是主线程的消息循环模型。
11. Android的线程和线程池
11.1 概述及主线程和子线程
- 概述
除了Thread本身以外,在Android中可以扮演线程角色的还有很多,比如AsyncTask和IntentService,同时HandlerThread也是一种特殊的线程。
尽管AsyncTask、IntentService以及HandlerThread的表现形式都有别于传统的线程,但是它们的本质仍然是传统的线程。对于AsyncTask来说,它的底层用到了线程池,对于IntentService和HandlerThread来说,它们的底层则直接使用了线程。
AsyncTask封装了线程池和Handler,它主要是为了方便开发者在子线程中更新UI。
HandlerThread是一种具有消息循环的线程,在它的内部可以使用Handler。
IntentService是一个服务,系统对其进行了封装使其可以更方便地执行后台任务,IntentService内部采用HandlerThread来执行任务,当任务执行完毕后IntentService会自动退出。 - 主线程和子线程
主线程也叫UI线程。主线程的作用是运行四大组件以及处理它们和用户的交互,而子线程的作用则是执行耗时任务,比如网络请求、I/O操作等
11.2 Android中的线程形态
1. AsyncTask
- 参数
AsyncTask是一个抽象的泛型类,它提供了Params、Progress和Result这三个泛型参数。Params表示参数的类型,Progress表示后台任务的执行进度的类型,Result则表示后台任务的返回结果的类型
- 核心方法
onPreExecute 在主线程中执行,在异步任务执行之前,此方法会被调用,一般可以用于做一些准备工作
doInBackground 在线程池中执行,此方法用于执行异步任务,params参数表示异步任务的输入参数。在此方法中可以通过publishProgress方法来更新任务的进度,publishProgress方法会调用onProgressUpdate方法。另外此方法需要返回计算结果给onPostExecute方法。
onProgressUpdate 在主线程中执行,当后台任务的执行进度发生改变时此方法会被调用
onPostExecute 在主线程中执行,主要进行UI的更新操作,在异步任务执行之后,此方法会被调用,其中result参数是后台任务(doInBackground中进行的异步任务)的返回值,即doInBackground的返回值
- AsyncTask在具体的使用过程中也是有一些条件限制的,主要有如下几点:
(1)AsyncTask的类必须在主线程中加载,这就意味着第一次访问AsyncTask必须发生在主线程,当然这个过程在Android 4.1及以上版本中已经被系统自动完成。在Android 5.0的源码中,可以查看ActivityThread的main方法,它会调用AsyncTask的init方法,这就满足了AsyncTask的类必须在主线程中进行加载这个条件了。
(2)AsyncTask的对象必须在主线程中创建。
(3)execute方法必须在UI线程调用。
(4)不要在程序中直接调用onPreExecute()、onPostExecute、doInBackground和onProgressUpdate方法。
(5)一个AsyncTask对象只能执行一次,即只能调用一次execute方法,否则会报运行时异常。
(6)在Android 1.6之前,AsyncTask是串行执行任务的,Android 1.6的时候AsyncTask开始采用线程池里处理并行任务,但是从Android 3.0开始,为了避免AsyncTask所带来的并发错误,AsyncTask又采用一个线程来串行执行任务。尽管如此,在Android 3.0以及后续的版本中,我们仍然可以通过AsyncTask的executeOnExecutor方法来并行地执行任务。
2. HandlerThread
HandlerThread继承了Thread,它是一种可以使用Handler的Thread,它的实现也很简单,就是在run方法中通过Looper.prepare()来创建消息队列,并通过Looper.loop()来开启消息循环,这样在实际的使用中就允许在HandlerThread中创建Handler了
@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll();
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}
从HandlerThread的实现来看,它和普通的Thread有显著的不同之处。
- 普通Thread主要用于在run方法中执行一个耗时任务,
- 而HandlerThread在run方法内部创建了消息队列,外界需要通过Handler的消息方式来通知HandlerThread执行一个具体的任务。
HandlerThread是一个很有用的类,它在Android中的一个具体的使用场景是IntentService。由于HandlerThread的run方法是一个无限循环,因此当明确不需要再使用HandlerThread时,可以通过它的quit或者quitSafely方法来终止线程的执行
3. IntentService
package android.app;
import android.annotation.WorkerThread;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
public abstract class IntentService extends Service {
private volatile Looper mServiceLooper;
private volatile ServiceHandler mServiceHandler;
private String mName;
private boolean mRedelivery;
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}
public IntentService(String name) {
super();
mName = name;
}
public void setIntentRedelivery(boolean enabled) {
mRedelivery = enabled;
}
@Override
public void onCreate() {
super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
@Override
public void onStart(Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}
@Override
public void onDestroy() {
mServiceLooper.quit();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@WorkerThread
protected abstract void onHandleIntent(Intent intent);
}
IntentService是一种特殊的Service,它继承了Service并且它是一个抽象类,因此必须创建它的子类才能使用IntentService。IntentService可用于执行后台耗时的任务,当任务执行后它会自动停止,同时由于IntentService是服务的原因,这导致它的优先级比单纯的线程要高很多,所以IntentService比较适合执行一些高优先级的后台任务,因为它优先级高不容易被系统杀死
- 当IntentService被第一次启动时,它的onCreate方法会被调用
- 每次启动IntentService,它的onStartCommand方法就会调用一次
- IntentService仅仅是通过mServiceHandler发送了一个消息,这个消息会在HandlerThread中被处理
每执行一个后台任务就必须启动一次IntentServic
如果目前只存在一个后台任务,那么onHandleIntent方法执行完这个任务后,stopSelf(int startId)就会直接停止服务;
如果目前存在多个后台任务,那么当onHandleIntent方法执行完最后一个任务时,stopSelf(int startId)才会直接停止服务。 - 使用案例
public class LocalIntentService extends IntentService {
private static final String TAG = "LocalIntentService";
public LocalIntentService() {
super(TAG);
}
@Override
protected void onHandleIntent(Intent intent) {
String action = intent.getStringExtra("task_action");
Log.d(TAG, "receive task :" + action);
SystemClock.sleep(3000);
if ("com.ryg.action.TASK1".equals(action)) {
Log.d(TAG, "handle task: " + action);
}
}
@Override
public void onDestroy() {
Log.d(TAG, "service destroyed.");
super.onDestroy();
}
}
11.3 Android中的线程池
- ThreadPoolExecutor
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory)
corePoolSize
线程池的核心线程数,默认情况下,核心线程会在线程池中一直存活,即使它们处于闲置状态。如果将ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,那么闲置的核心线程在等待新任务到来时会有超时策略,这个时间间隔由keepAliveTime所指定,当等待时间超过keepAliveTime所指定的时长后,核心线程就会被终止。
maximumPoolSize
线程池所能容纳的最大线程数,当活动线程数达到这个数值后,后续的新任务将会被阻塞。
keepAliveTime
非核心线程闲置时的超时时长,超过这个时长,非核心线程就会被回收。当ThreadPool-Executor的allowCoreThreadTimeOut
属性设置为true时,keepAliveTime同样会作用于核心线程。
unit
用于指定keepAliveTime参数的时间单位,这是一个枚举,常用的有TimeUnit. MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)以及TimeUnit.MINUTES(分钟)等。
workQueue
线程池中的任务队列,通过线程池的execute方法提交的Runnable对象会存储在这个参数中。
threadFactory
线程工厂,为线程池提供创建新线程的功能。ThreadFactory是一个接口,它只有一个方法:Thread newThread(Runnable r)。
- ThreadPoolExecutor执行任务时大致遵循如下规则
(1)如果线程池中的线程数量未达到核心线程的数量,那么会直接启动一个核心线程来执行任务。
(2)如果线程池中的线程数量已经达到或者超过核心线程的数量,那么任务会被插入到任务队列中排队等待执行。
(3)如果在步骤2中无法将任务插入到任务队列中,这往往是由于任务队列已满,这个时候如果线程数量未达到线程池规定的最大值,那么会立刻启动一个非核心线程来执行任务。
(4)如果步骤3中线程数量已经达到线程池规定的最大值,那么就拒绝执行此任务,ThreadPoolExecutor会调用RejectedExecutionHandler的rejectedExecution方法来通知调用者。 - ThreadPoolExecutor的参数配置在AsyncTask中有明显的体现
· 核心线程数等于CPU核心数 + 1;
· 线程池的最大线程数为CPU核心数的2倍 + 1;
· 核心线程无超时机制,非核心线程在闲置时的超时时间为1秒;
· 任务队列的容量为128。
12. Bitmap的加载和Cache
12.1 Bitmap的高效加载
- 如何加载一个Bitmap
Bitmap在Android中指的是一张图片,可以是png格式也可以是jpg等其他常见的图片格式。那么如何加载一个图片呢?BitmapFactory类提供了四类方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分别用于支持从文件系统、资源、输入流以及字节数组中加载出一个Bitmap对象 - 如何高效地加载Bitmap
采用BitmapFactory. Options来加载所需尺寸的图片。过BitmapFactory.Options来缩放图片,主要是用到了它的inSampleSize参数,即采样率。 - 实际场景应用
比如ImageView的大小是100×100像素,而图片的原始大小为200×200,那么只需将采样率inSampleSize设为2即可。但是如果图片大小为200×300呢?这个时候采样率还应该选择2,这样缩放后的图片大小为100×150像素,仍然是适合ImageView的,如果采样率为3,那么缩放后的图片大小就会小于ImageView所期望的大小,这样图片就会被拉伸从而导致模糊
(1)将BitmapFactory.Options的inJustDecodeBounds参数设为true并加载图片。
(2)从BitmapFactory.Options中取出图片的原始宽高信息,它们对应于outWidth和outHeight参数。
(3)根据采样率的规则并结合目标View的所需大小计算出采样率inSampleSize。
(4)将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后重新加载图片。
12.2 Android中的缓存策略
缓存策略主要包含缓存的添加、获取和删除这三类操作
目前常用的一种缓存算法是LRU(Least Recently Used), LRU是近期最少使用算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LruCache和DiskLruCache, LruCache用于实现内存缓存,而DiskLruCache则充当了存储设备缓存,通过这二者的完美结合,就可以很方便地实现一个具有很高实用价值的ImageLoader。
1. LruCache
LruCache是一个泛型类,它内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。
2. DiskLruCache
- DiskLruCache的创建
DiskLruCache并不能通过构造方法来创建,它提供了open方法用于创建自身,如下所示。
public static DiskLruCache open(File directory, int appVersion, int
valueCount, long maxSize)
- DiskLruCache的缓存添加
DiskLruCache的缓存添加的操作是通过Editor完成的,Editor表示一个缓存对象的编辑对象。这里仍然以图片缓存举例,首先需要获取图片url所对应的key,然后根据key就可以通过edit()来获取Editor对象,如果这个缓存正在被编辑,那么edit()会返回null,即DiskLruCache不允许同时编辑一个缓存对象。之所以要把url转换成key,是因为图片的url中很可能有特殊字符,这将影响url在Android中直接使用,一般采用url的md5值作为key,如下所示
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor ! = null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}
public boolean downloadUrlToStream(String urlString,
OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(),
IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) ! = -1) {
out.write(b);
}
return true;
} catch (IOException e) {
Log.e(TAG, "downloadBitmap failed." + e);
} finally {
if (urlConnection ! = null) {
urlConnection.disconnect();
}
MyUtils.close(out);
MyUtils.close(in);
}
return false;
}
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
并没有真正地将图片写入文件系统,还必须通过Editor的commit()来提交写入操作,如果图片下载过程发生了异常,那么还可以通过Editor的abort()来回退整个操作
- DiskLruCache的缓存查找
和缓存的添加过程类似,缓存查找过程也需要将url转换为key,然后通过DiskLruCache的get方法得到一个Snapshot对象,接着再通过Snapshot对象即可得到缓存的文件输入流,有了文件输出流,自然就可以得到Bitmap对象了。为了避免加载图片过程中导致的OOM问题,一般不建议直接加载原始图片。在第12.1节中已经介绍了通过BitmapFactory.Options对象来加载一张缩放后的图片,但是那种方法对FileInputStream的缩放存在问题,原因是FileInputStream是一种有序的文件流,而两次decodeStream调用影响了文件流的位置属性,导致了第二次decodeStream时得到的是null。为了解决这个问题,可以通过文件流来得到它所对应的文件描述符,然后再通过BitmapFactory.decodeFileDescriptor方法来加载一张缩放后的图片,这个过程的实现如下所示。
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot ! = null) {
FileInputStream fileInputStream = (FileInputStream)snapShot.getInput-
Stream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor
(fileDescriptor,
reqWidth, reqHeight);
if (bitmap ! = null) {
addBitmapToMemoryCache(key, bitmap);
}
}
3. ImageLoader的实现
- 一般来说,一个优秀的ImageLoader应该具备如下功能:
· 图片的同步加载;
· 图片的异步加载;
· 图片压缩;
· 内存缓存;
· 磁盘缓存;
· 网络拉取。
实现步骤: - 图片压缩功能的实现
public class ImageResizer {
private static final String TAG = "ImageResizer";
public ImageResizer() {
}
public Bitmap decodeSampledBitmapFromResource(Resources res,
int resId, int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd, null, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth,
reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd, null, options);
}
public int calculateInSampleSize(BitmapFactory.Options options,
int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
Log.d(TAG, "origin, w=" + width + " h=" + height);
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// Calculate the largest inSampleSize value that is a power of 2
and
// keeps both
// height and width larger than the requested height and width.
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
Log.d(TAG, "sampleSize:" + inSampleSize);
return inSampleSize;
}
- 内存缓存和磁盘缓存的实现
这里选择LruCache和DiskLruCache来分别完成内存缓存和磁盘缓存的工作。在ImageLoader初始化时,会创建LruCache和DiskLruCache,如下所示
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
private ImageLoader(Context context) {
mContext = context.getApplicationContext();
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (! diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
内存缓存和磁盘缓存创建完毕后,还需要提供方法来完成缓存的添加和获取功能。首先看内存缓存,它的添加和读取过程比较简单,如下所示。
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
磁盘缓存的添加和读取功能稍微复杂一些
磁盘缓存的添加需要通过Editor来完成,Editor提供了commit和abort方法来提交和撤销对文件系统的写操作,具体实现请参看下面的loadBitmap-FromHttp方法。磁盘缓存的读取需要通过Snapshot来完成,通过Snapshot可以得到磁盘缓存对象对应的FileInputStream,但是FileInputStream无法便捷地进行压缩,所以通过FileDescriptor来加载压缩后的图片,最后将加载后的Bitmap添加到内存缓存中
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int reqHeight)
throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI Thread.");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor ! = null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_
INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
}
return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth,
int reqHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "load bitmap from UI Thread, it's not recommended! ");
}
if (mDiskLruCache == null) {
return null;
}
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot ! = null) {
FileInputStream fileInputStream = (FileInputStream)snapShot.
getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor
(fileDescriptor,
reqWidth, reqHeight);
if (bitmap ! = null) {
addBitmapToMemoryCache(key, bitmap);
}
}
return bitmap;
}
- 同步加载和异步加载接口的设计
首先看同步加载,同步加载接口需要外部在线程中调用,这是因为同步加载很可能比较耗时,它的实现如下所示。
/**
* load bitmap from memory cache or disk cache or network.
* @param uri http url
* @param reqWidth the width ImageView desired
* @param reqHeight the height ImageView desired
* @return bitmap, maybe null.
*/
public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap ! = null) {
Log.d(TAG, "loadBitmapFromMemCache, url:" + uri);
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
if (bitmap ! = null) {
Log.d(TAG, "loadBitmapFromDisk, url:" + uri);
return bitmap;
}
bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
Log.d(TAG, "loadBitmapFromHttp, url:" + uri);
} catch (IOException e) {
e.printStackTrace();
}
if (bitmap == null && ! mIsDiskLruCacheCreated) {
Log.w(TAG, "encounter error, DiskLruCache is not created.");
bitmap = downloadBitmapFromUrl(uri);
}
return bitmap;
}
异步加载接口的设计
public void bindBitmap(final String uri, final ImageView imageView,
final int reqWidth, final int reqHeight) {
imageView.setTag(TAG_KEY_URI, uri);
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap ! = null) {
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
if (bitmap ! = null) {
LoaderResult result = new LoaderResult(imageView, uri, bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).
sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
从bindBitmap的实现来看,bindBitmap方法会尝试从内存缓存中读取图片,如果读取成功就直接返回结果,否则会在线程池中去调用loadBitmap方法,当图片加载成功后再将图片、图片的地址以及需要绑定的imageView封装成一个LoaderResult对象,然后再通过mMainHandler向主线程发送一个消息,这样就可以在主线程中给imageView设置图片了,之所以通过Handler来中转是因为子线程无法访问UI。
- 完整代码
public class ImageLoader {
private static final String TAG = "ImageLoader";
public static final int MESSAGE_POST_RESULT = 1;
private static final int CPU_COUNT = Runtime.getRuntime().available-
Processors();
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
private static final long KEEP_ALIVE = 10L;
private static final int TAG_KEY_URI = R.id.imageloader_uri;
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
private static final int IO_BUFFER_SIZE = 8 * 1024;
private static final int DISK_CACHE_INDEX = 0;
private boolean mIsDiskLruCacheCreated = false;
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());
}
};
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPool-
Executor(
CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
KEEP_ALIVE, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), sThreadFactory);
private Handler mMainHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
LoaderResult result = (LoaderResult) msg.obj;
ImageView imageView = result.imageView;
imageView.setImageBitmap(result.bitmap);
String uri = (String) imageView.getTag(TAG_KEY_URI);
if (uri.equals(result.uri)) {
imageView.setImageBitmap(result.bitmap);
} else {
Log.w(TAG, "set image bitmap, but url has changed, ignored! ");
}
};
};
private Context mContext;
private ImageResizer mImageResizer = new ImageResizer();
private LruCache<String, Bitmap> mMemoryCache;
private DiskLruCache mDiskLruCache;
private ImageLoader(Context context) {
mContext = context.getApplicationContext();
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (! diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* build a new instance of ImageLoader
* @param context
* @return a new instance of ImageLoader
*/
public static ImageLoader build(Context context) {
return new ImageLoader(context);
}
private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
private Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
/**
* load bitmap from memory cache or disk cache or network async, then bind
imageView and bitmap.
* NOTE THAT: should run in UI Thread
* @param uri http url
* @param imageView bitmap's bind object
*/
public void bindBitmap(final String uri, final ImageView imageView) {
bindBitmap(uri, imageView, 0, 0);
}
public void bindBitmap(final String uri, final ImageView imageView,
final int reqWidth, final int reqHeight) {
imageView.setTag(TAG_KEY_URI, uri);
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap ! = null) {
imageView.setImageBitmap(bitmap);
return;
}
Runnable loadBitmapTask = new Runnable() {
@Override
public void run() {
Bitmap bitmap = loadBitmap(uri, reqWidth, reqHeight);
if (bitmap ! = null) {
LoaderResult result = new LoaderResult(imageView, uri,
bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).
sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
/**
* load bitmap from memory cache or disk cache or network.
* @param uri http url
* @param reqWidth the width ImageView desired
* @param reqHeight the height ImageView desired
* @return bitmap, maybe null.
*/
public Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap ! = null) {
Log.d(TAG, "loadBitmapFromMemCache, url:" + uri);
return bitmap;
}
try {
bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
if (bitmap ! = null) {
Log.d(TAG, "loadBitmapFromDisk, url:" + uri);
return bitmap;
}
bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
Log.d(TAG, "loadBitmapFromHttp, url:" + uri);
} catch (IOException e) {
e.printStackTrace();
}
if (bitmap == null && ! mIsDiskLruCacheCreated) {
Log.w(TAG, "encounter error, DiskLruCache is not created.");
bitmap = downloadBitmapFromUrl(uri);
}
return bitmap;
}
private Bitmap loadBitmapFromMemCache(String url) {
final String key = hashKeyFormUrl(url);
Bitmap bitmap = getBitmapFromMemCache(key);
return bitmap;
}
private Bitmap loadBitmapFromHttp(String url, int reqWidth, int
reqHeight)
throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network from UI
Thread.");
}
if (mDiskLruCache == null) {
return null;
}
String key = hashKeyFormUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor ! = null) {
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_
INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
}
return loadBitmapFromDiskCache(url, reqWidth, reqHeight);
}
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth,
int reqHeight) throws IOException {
if (Looper.myLooper() == Looper.getMainLooper()) {
Log.w(TAG, "load bitmap from UI Thread, it's not recommended! ");
}
if (mDiskLruCache == null) {
return null;
}
Bitmap bitmap = null;
String key = hashKeyFormUrl(url);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
if (snapShot ! = null) {
FileInputStream fileInputStream = (FileInputStream)snapShot.
getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor
(fileDescriptor, reqWidth, reqHeight);
if (bitmap ! = null) {
addBitmapToMemoryCache(key, bitmap);
}
}
return bitmap;
}
public boolean downloadUrlToStream(String urlString,
OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(),
IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b;
while ((b = in.read()) ! = -1) {
out.write(b);
}
return true;
} catch (IOException e) {
Log.e(TAG, "downloadBitmap failed." + e);
} finally {
if (urlConnection ! = null) {
urlConnection.disconnect();
}
MyUtils.close(out);
MyUtils.close(in);
}
return false;
}
private Bitmap downloadBitmapFromUrl(String urlString) {
Bitmap bitmap = null;
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(),
IO_BUFFER_SIZE);
bitmap = BitmapFactory.decodeStream(in);
} catch (final IOException e) {
Log.e(TAG, "Error in downloadBitmap: " + e);
} finally {
if (urlConnection ! = null) {
urlConnection.disconnect();
}
MyUtils.close(in);
}
return bitmap;
}
private String hashKeyFormUrl(String url) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(0xFF & bytes[i]);
if (hex.length() == 1) {
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
public File getDiskCacheDir(Context context, String uniqueName) {
boolean externalStorageAvailable = Environment
.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
final String cachePath;
if (externalStorageAvailable) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
@TargetApi(VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
return path.getUsableSpace();
}
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
private static class LoaderResult {
public ImageView imageView;
public String uri;
public Bitmap bitmap;
public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
this.imageView = imageView;
this.uri = uri;
this.bitmap = bitmap;
}
}
}
12.3 ImageLoader的使用
1. 照片墙实现
- GridView所需的布局文件以及item的布局文件
// GridView的布局文件
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="5dp" >
<GridView
android:id="@+id/gridView1"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:horizontalSpacing="5dp"
android:verticalSpacing="5dp"
android:listSelector="@android:color/transparent"
android:numColumns="3"
android:stretchMode="columnWidth" >
</GridView>
</LinearLayout>
// GridView的item的布局文件
<? xml version="1.0" encoding="utf-8"? >
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical" >
// 自定义控件,正方形的imageView
<com.ryg.chapter_12.ui.SquareImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
android:src="@drawable/image_default" />
</LinearLayout>
- ImageAdapter
private class ImageAdapter extends BaseAdapter {
...
@Override
public int getCount() {
return mUrList.size();
}
@Override
public String getItem(int position) {
return mUrList.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent){
ViewHolder holder = null;
if (convertView == null) {
convertView = mInflater.inflate(R.layout.image_list_item,
parent, false);
holder = new ViewHolder();
holder.imageView = (ImageView) convertView.findViewById(R.
id.image);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
ImageView imageView = holder.imageView;
final String tag = (String)imageView.getTag();
final String uri = getItem(position);
if (! uri.equals(tag)) {
imageView.setImageDrawable(mDefaultBitmapDrawable);
}
if (mIsGridViewIdle && mCanGetBitmapFromNetWork) {
imageView.setTag(uri);
mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImage-
Width);
}
return convertView;
}
}
getView方法中核心代码只有一句话,那就是:**mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth)。**通过bindBitmap方法很轻松地将复杂的图片加载过程交给了ImageLoader, ImageLoader加载图片以后会把图片自动设置给imageView,而整个过程,包括内存缓存、磁盘缓存以及图片压缩等工作过程对ImageAdapter来说都是透明的
- 适配
mImageGridView = (GridView) findViewById(R.id.gridView1);
mImageAdapter = new ImageAdapter(this);
mImageGridView.setAdapter(mImageAdapter);
2.优化列表的卡顿现象
- 不要在getView中执行耗时操作
- 控制异步任务的执行频率
以照片墙来说,在getView方法中会通过ImageLoader的bindBitmap方法来异步加载图片,但是如果用户刻意地频繁上下滑动,这就会在一瞬间产生上百个异步任务,这些异步任务会造成线程池的拥堵并随即带来大量的UI更新操作,这是没有意义的。由于一瞬间存在大量的UI更新操作,这些UI操作是运行在主线程的,这就会造成一定程度的卡顿
考虑在列表滑动的时候停止加载图片,尽管这个过程是异步的,等列表停下来以后再加载图片仍然可以获得良好的用户体验。具体实现时,可以给ListView或者GridView设置setOnScrollListener,并在OnScrollListener的onScrollStateChanged方法中判断列表是否处于滑动状态,如果是的话就停止加载图片
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
mIsGridViewIdle = true;
mImageAdapter.notifyDataSetChanged();
} else {
mIsGridViewIdle = false;
}
}
然后在getView方法中,仅当列表静止时才能加载图片,如下所示。
if (mIsGridViewIdle && mCanGetBitmapFromNetWork) {
imageView.setTag(uri);
mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth);
}
- 绝大多数情况下,硬件加速都可以解决莫名的卡顿问题,通过设置android:hardwareAccelerated="true"即可为Activity开启硬件加速
13. 综合技术
13.1 使用CrashHandler来获取应用的crash信息
- 首先写一个类继承UncaughtExceptionHandler,采用okhttp方式上传
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private static final String TAG = "CrashHandler";
private static final boolean DEBUG = true;
private final String PATH = Environment.getExternalStorageDirectory().getPath() + "/CrashTest/log/";
private final String FILE_NAME = "crash";
private final String FILE_NAME_SUFFIX = ".trace";
private static CrashHandler mInstance = new CrashHandler();
private Thread.UncaughtExceptionHandler mDefaultCrashHandler;
private Context mContext;
private File file;
private CrashHandler() {
}
public static CrashHandler getInstance() {
return mInstance;
}
public void init(Context context) {
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
mContext = context.getApplicationContext();
Log.e(TAG, "init: " + "初始化CrashHandler");
}
/**
* 当程序发生未捕获的异常时,执行这里
* @param t
* @param e
*/
@Override
public void uncaughtException(Thread t, Throwable e) {
Log.e(TAG, "uncaughtException: " + "执行崩溃日志");
try {
dumpExceptionToSDCard(e);
uploadExceptionToService(e);
} catch (IOException e1) {
e1.printStackTrace();
}
e.printStackTrace();
if (mDefaultCrashHandler != null) {
mDefaultCrashHandler.uncaughtException(t, e);
} else {
Process.killProcess(Process.myPid());
}
}
/**
* 将异常信息以及手机软件等相关信息保存到本地
* @param e
* @throws IOException
*/
private void dumpExceptionToSDCard(Throwable e) throws IOException {
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
if (DEBUG) {
Log.w(TAG, "sdcard unmounted,skip dump exception");
return;
}
}
File dir = new File(PATH);
if (!dir.exists()) {
dir.mkdirs();
}
long current = System.currentTimeMillis();
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date(current));
file = new File(PATH + FILE_NAME + time + FILE_NAME_SUFFIX);
try {
PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(file)));
pw.println(time);
dumpPhoneInfo(pw);
pw.println();
e.printStackTrace(pw);
Log.e(TAG, "写入文件成功: " + file.getPath());
pw.close();
} catch (Exception e1) {
Log.e(TAG, "dump crash info failed" + e1);
}
}
private void dumpPhoneInfo(PrintWriter pw) throws PackageManager.NameNotFoundException {
PackageManager pm = mContext.getPackageManager();
PackageInfo pi = pm.getPackageInfo(mContext.getPackageName(), PackageManager.GET_ACTIVITIES);
pw.print("App Version:");
pw.print(pi.versionName);
pw.print('_');
pw.print(pi.versionCode);
//Android版本号
pw.print("OS Verson:");
pw.print(Build.VERSION.RELEASE);
pw.print("_");
pw.print(Build.VERSION.SDK_INT);
//手机制造商
pw.print("Vendor:");
pw.print(Build.MANUFACTURER);
//手机型号
pw.print("Model:");
pw.print(Build.MODEL);
//CPU架构
pw.print("CPU ABI:");
pw.print(Build.CPU_ABI);
}
/**
* 将异常信息发送到服务器
* @param e
* @throws IOException
*/
private void uploadExceptionToService(Throwable e) throws IOException {
Log.e(TAG, "开始上传文件: "+file.length()+"");
OkHttpClient mOkHttpClient = new OkHttpClient();
Request request = new Request.Builder()
.url("https://...")
.post(RequestBody.create(MediaType.parse("text/x-markdown; charset=utf-8"), file))
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
Log.e(TAG, "上传失败"+e);
}
@Override
public void onResponse(Call call, Response response) throws IOException {
Log.e(TAG, response.body().string());
}
});
}
}
- 在application中初始化
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
CrashHandler crashHandler = CrashHandler.getInstance();
crashHandler.init(this);
}
}
13.2 使用multidex来解决方法数越界
在Android中单个dex文件所能够包含的最大方法数为65536,这包含Android FrameWork、依赖的jar包以及应用本身的代码中的所有方法。
- 如果要使用multidex,首先要使用Android SDK Build Tools21.1及以上版本,
- 接着修改工程中app目录下的build.gradle文件,在defaultConfig中添加multiDexEnabled true这个配置项
android {
compileSdkVersion 22
buildToolsVersion "22.0.1"
defaultConfig {
applicationId "com.ryg.multidextest"
minSdkVersion 8
targetSdkVersion 22
versionCode 1
versionName "1.0"
// enable multidex support
multiDexEnabled true
}
...
}
- 接着还需要在dependencies中添加multidex的依赖:compile ‘com.android.support:multidex:1.0.0’,如下所示。
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.1.1'
compile 'com.android.support:multidex:1.0.0'
}
- 还需要做另一项工作,那就是在代码中加入支持multidex的功能
<application
android:name="android.support.multidex.MultiDexApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
...
</application>
- 采用multidex可能带来的问题
(1)应用启动速度会降低
(2)由于Dalvik linearAlloc的bug,这可能导致使用multidex的应用无法在Android 4.0以前的手机上运行,因此需要做大量的兼容性测试
13.3 Android的动态加载技术
- 动态加载技术(也叫插件化技术)在技术驱动型的公司中扮演着相当重要的角色,当项目越来越庞大的时候,需要通过插件化来减轻应用的内存和CPU占用,还可以实现热插拔,即在不发布新版本的情况下更新某些模块
- 不同的插件化方案各有各的特色,但是它们都必须要解决三个基础性问题:资源访问、Activity生命周期的管理和ClassLoader的管理。
宿主和插件的概念,宿主是指普通的apk,而插件一般是指经过处理的dex或者apk,在主流的插件化框架中多采用经过特殊处理的apk来作为插件,处理方式往往和编译以及打包环节有关,另外很多插件化框架都需要用到代理Activity的概念,插件Activity的启动大多数是借助一个代理Activity来实现的 - 资源访问
宿主访问插件的资源
Activity的工作主要是通过ContextImpl来完成的,Activity中有一个叫mBase的成员变量,它的类型就是ContextImpl。注意到Context中有如下两个抽象方法,看起来是和资源有关的,实际上Context就是通过它们来获取资源的。这两个抽象方法的真正实现在ContextImpl中,也就是说,只要实现这两个方法,就可以解决资源问题了
/** Return an AssetManager instance for your application's package. */
public abstract AssetManager getAssets();
/** Return a Resources instance for your application's package. */
public abstract Resources getResources();
protected void loadResources() {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod
("addAssetPath", String.class);
addAssetPath.invoke(assetManager, mDexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
mResources = new Resources(mAssetManager, superRes.getDisplay-
Metrics(),
superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
@Override
public AssetManager getAssets() {
return mAssetManager == null ? super.getAssets() : mAssetManager;
}
@Override
public Resources getResources() {
return mResources == null ? super.getResources() : mResources;
}
- Activity生命周期的管理
管理Activity生命周期的方式各种各样,比如:反射方式和接口方式。
接口方式:
将Activity的生命周期方法提取出来作为一个接口(比如叫DLPlugin),然后通过代理Activity去调用插件Activity的生命周期方法
public interface DLPlugin {
public void onStart();
public void onRestart();
public void onActivityResult(int requestCode, int resultCode, Intent
data);
public void onResume();
public void onPause();
public void onStop();
public void onDestroy();
public void onCreate(Bundle savedInstanceState);
public void setProxy(Activity proxyActivity, String dexPath);
public void onSaveInstanceState(Bundle outState);
public void onNewIntent(Intent intent);
public void onRestoreInstanceState(Bundle savedInstanceState);
public boolean onTouchEvent(MotionEvent event);
public boolean onKeyUp(int keyCode, KeyEvent event);
public void onWindowAttributesChanged(LayoutParams params);
public void onWindowFocusChanged(boolean hasFocus);
public void onBackPressed();
…
}
...
@Override
protected void onStart() {
mRemoteActivity.onStart();
super.onStart();
}
@Override
protected void onRestart() {
mRemoteActivity.onRestart();
super.onRestart();
}
@Override
protected void onResume() {
mRemoteActivity.onResume();
super.onResume();
}
...
- 插件CIassLoader的管理
为了更好地对多插件进行支持,需要合理地去管理各个插件的DexClassoader,这样同一个插件就可以采用同一个ClassLoader去加载类,从而避免了多个ClassLoader加载同一个类时所引发的类型转换错误。
13.4 反编译初步
- 使用dex2jar和jd-gui反编译apk
Dex2jar是一个将dex文件转换为jar包的工具,它在Windows和Linux上都有对应的版本,dex文件来源于apk,将apk通过zip包的方式解压缩即可提取出里面的dex文件。有了jar包还不行,因为jar包中都是class文件,这个时候还需要jd-gui将jar包进一步转换为Java代码,jd-gui仍然支持Windows和Linux,不管是dex2jar还是jd-gui,它们在不同的操作系统中的使用方式都是一致的。 - 使用apktool对apk进行二次打包
dex2jar和jd-gui的使用方式,通过它们可以将一个dex文件反编译为Java代码,但是它们无法反编译出apk中的二进制数据资源,但是采用apktool就可以做到这一点。apktool另外一个常见的用途是二次打包,也就是常见的山寨版应用。
14. JNI和NDK编程
- Java JNI的本意是Java Native Interface(Java本地接口),它是为了方便Java调用C、C++等本地代码所封装的一层接口
- NDK是Android所提供的一个工具集合,通过NDK可以在Android中更加方便地通过JNI来访问本地代码,比如C或者C++。NDK还提供了交叉编译器,开发人员只需要简单地修改mk文件就可以生成特定CPU平台的动态库。
- 本文选择Ubuntu 14.10(64位操作系统)作为开发环境,同时选择AndroidStuio作为IDE
14.1 JNI的开发流程
- 在Java中声明native方法
package com.ryg;
import java.lang.System;
public class JniTest {
static {
System.loadLibrary("jni-test");
}
public static void main(String args[]) {
JniTest jniTest = new JniTest();
System.out.println(jniTest.get());
jniTest.set("hello world");
}
// 声明了两个native方法:get和set(String),这两个就是需要在JNI中实现的方法
public native String get();
public native void set(String str);
}
- 编译Java源文件得到cIass文件,然后通过javah命令导出JNI的头文件
javac com/ryg/JniTest.java
javah com.ryg.JniTest
在当前目录下,会产生一个com_ryg_JniTest.h的头文件,它是javah命令自动生成的,内容如下所示。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_ryg_JniTest */
#ifndef _Included_com_ryg_JniTest
#define _Included_com_ryg_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: com_ryg_JniTest
* Method: get
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_com_ryg_JniTest_get
(JNIEnv *, jobject);
/*
* Class: com_ryg_JniTest
* Method: set
* Signature: (Ljava/lang/String; )V
*/
JNIEXPORT void JNICALL Java_com_ryg_JniTest_set
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
上面的代码需要做一下说明,首先函数名的格式遵循如下规则:Java_包名_类名_方法名。比如JniTest中的set方法,到这里就变成了JNIEXPORT void JNICALL Java_com_ryg_JniTest_set(JNIEnv *, jobject, jstring),其中com_ryg是包名,JniTest是类名,jstring是代表的是set方法的String类型的参数,其他参数解析如下:
· JNIEnv:表示一个指向JNI环境的指针,可以通过它来访问JNI提供的接口方法;
· jobject:表示Java对象中的this;
· JNIEXPORT和JNICALL:它们是JNI中所定义的宏,可以在jni.h这个头文件中查找到。
- 实现JNI方法
JNI方法是指Java中声明的native方法,这里可以选择用C++或者C来实现,它们的实现过程是类似的,只有少量的区别 - 编译so库并在Java中调用
so库的编译这里采用gcc,切换到jni目录中,对于test.cpp和test.c来说,它们的编译指令如下所示。
C++:gcc -shared -I /usr/lib/jvm/java-7-openjdk-amd64/include -fPIC test.cpp
14.2 NDK的开发流程
- 下载并配置NDK
- 创建一个Android项目,并声明所需的native方法
- 实现Android项目中所声明的native方法
在外部创建一个名为jni的目录,然后在jni目录下创建3个文件:test.cpp、Android.mk和Application.mk
Android.mk
和Application.mk
是 Android NDK(Native Development Kit)中用于构建 C/C++ 代码的两个重要文件。
Android.mk - 用途:
Android.mk
文件定义了如何构建一个特定的模块(例如库或可执行文件)。 - 内容:
- 模块的名称和类型(共享库、静态库或可执行文件)。
- 源文件和头文件的路径。
- 依赖关系和包含的库。
- 示例:
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := mylib LOCAL_SRC_FILES := mylib.c include $(BUILD_SHARED_LIBRARY)
Application.mk
- 用途:
Application.mk
文件用于设置整个 NDK 应用程序的构建配置。 - 内容:
- 应用的平台和架构设置。
- 是否启用调试信息。
- C++ 标准的版本等。
- 示例:
APP_ABI := all
APP_PLATFORM := android-21
NDK_DEBUG := 1
- 切换到jni目录的父目录,然后通过ndk-buiId命令编译产生so库
这个时候NDK会创建一个和jni目录平级的目录libs, libs下面存放的就是so库的目录 - 在上面的步骤中,需要将NDK编译的so库放置到jniLibs目录下,这个是AndroidStudio所识别的默认目录,如果想使用其他目录,可以按照如下方式修改App的build.gradle文件,其中jniLibs.srcDir选项指定了新的存放so库的目录。
android {
...
sourceSets.main {
jniLibs.srcDir 'src/main/jni_libs'
}
}
14.3 JNI的数据类型和类型签名
14.4 JNI调用Java方法的流程
JNI调用Java方法的流程是先通过类名找到类,然后再根据方法名找到方法的id,最后就可以调用这个方法了。如果是调用Java中的非静态方法,那么需要构造出类的对象后才能调用它
- 首先需要在Java中定义一个静态方法供JNI调用,如下所示。
public static void methodCalledByJni(String msgFromJni) {
Log.d(TAG, "methodCalledByJni, msg: " + msgFromJni);
}
- 然后在JNI中调用上面定义的静态方法:
void callJavaMethod(JNIEnv *env, jobject thiz) {
jclass clazz = env->FindClass("com/ryg/JniTestApp/MainActivity");
if (clazz == NULL) {
printf("find class MainActivity error! ");
return;
}
jmethodID id = env->GetStaticMethodID(clazz, "methodCalledByJni",
"(Ljava/lang/String; )V");
if (id == NULL) {
printf("find method methodCalledByJni error! ");
}
jstring msg = env->NewStringUTF("msg send by callJavaMethod in
test.cpp.");
env->CallStaticVoidMethod(clazz, id, msg);
}
程序首先根据类名com/ryg/JniTestApp/MainActivity找到类,然后再根据方法名methodCalledByJni找到方法,其中(Ljava/lang/String; )V是methodCalledByJni方法的签名,接着再通过JNIEnv对象的CallStaticVoidMethod方法来完成最终的调用过程。
- 最后在Java_com_ryg_JniTestApp_MainActivity_get方法中调用callJavaMethod方法,如下所示。
jstring Java_com_ryg_JniTestApp_MainActivity_get(JNIEnv *env, jobject thiz){
printf("invoke get in c++\n");
callJavaMethod(env, thiz);
return env->NewStringUTF("Hello from JNI in libjni-test.so ! ");
}
15. Android性能优化
15.1 Android的性能优化方法
1. 布局优化
include标签、merge标签和ViewStub。
include标签主要用于布局重用,merge标签一般和include配合使用,它可以降低减少布局的层级,而ViewStub则提供了按需加载的功能,当需要时才会将ViewStub中的布局加载到内存,这提高了程序的初始化效率
2. 绘制优化
绘制优化是指View的onDraw方法要避免执行大量的操作
3.内存泄露优化
- 场景1:静态变量导致的内存泄露
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private static Context sContext;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
sContext = this;
}
}
- 场景2:单例模式导致的内存泄露
首先提供一个单例模式的TestManager, TestManager可以接收外部的注册并将外部的监听器存储起来。
public class TestManager {
private List<OnDataArrivedListener> mOnDataArrivedListeners = new
ArrayList<OnDataArrivedListener>();
private static class SingletonHolder {
public static final TestManager INSTANCE = new TestManager();
}
private TestManager() {
}
public static TestManager getInstance() {
return SingletonHolder.INSTANCE;
}
public synchronized void registerListener(OnDataArrivedListener
listener) {
if (! mOnDataArrivedListeners.contains(listener)) {
mOnDataArrivedListeners.add(listener);
}
}
public synchronized void unregisterListener(OnDataArrivedListener
listener) {
mOnDataArrivedListeners.remove(listener);
}
public interface OnDataArrivedListener {
public void onDataArrived(Object data);
}
}
接着再让Activity实现OnDataArrivedListener接口并向TestManager注册监听,如下所示。下面的代码由于缺少解注册的操作所以会引起内存泄露,泄露的原因是Activity的对象被单例模式的TestManager所持有,而单例模式的特点是其生命周期和Application保持一致,因此Activity对象无法被及时释放。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TestManager.getInstance().registerListener(this);
}
- 场景3:属性动画导致的内存泄露
属性动画中有一类无限循环的动画,如果在Activity中播放此类动画且没有在onDestroy中去停止动画,那么动画会一直播放下去,尽管已经无法在界面上看到动画效果了,并且这个时候Activity的View会被动画持有,而View又持有了Activity,最终Activity无法释放。下面的动画是无限动画,会泄露当前Activity,解决方法是在Activity的onDestroy中调用animator.cancel()来停止动画。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mButton = (Button) findViewById(R.id.button1);
ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "rotation",
0, 360).setDuration(2000);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.start();
//animator.cancel();
}
4. 响应速度优化和ANR日志分析
- ANR定义
响应速度优化的核心思想是避免在主线程中做耗时操作,但是有时候的确有很多耗时操作,怎么办呢?可以将这些耗时操作放在线程中去执行,即采用异步的方式执行耗时操作。响应速度过慢更多地体现在Activity的启动速度上面,如果在主线程中做太多事情,会导致Activity启动时出现黑屏现象,甚至出现ANR。Android规定,Activity如果5秒钟之内无法响应屏幕触摸事件或者键盘输入事件就会出现ANR,而BroadcastReceiver如果10秒钟之内还未执行完操作也会出现ANR。
- ANR日志分析
在实际开发中,ANR是很难从代码上发现的,如果在开发过程中遇到了ANR,那么怎么定位问题呢?其实当一个进程发生ANR了以后,系统会在/data/anr目录下创建一个文件traces.txt
5. ListView和Bitmap优化
- ListView
首先要采用ViewHolder并避免在getView中执行耗时操作;
其次要根据列表的滑动状态来控制任务的执行频率,比如当列表快速滑动时显然是不太适合开启大量的异步任务的;
最后可以尝试开启硬件加速来使Listview的滑动更加流畅
- Bitmap
主要是通过BitmapFactory.Options来根据需要对图片进行采样,采样过程中主要用到了BitmapFactory.Options的inSampleSize参数
6. 线程优化
线程优化的思想是采用线程池,避免程序中存在大量的Thread。
7. 一些性能优化建议
· 避免创建过多的对象;
· 不要过多使用枚举,枚举占用的内存空间要比整型大;
· 常量请使用static final来修饰;
· 使用一些Android特有的数据结构,比如SparseArray和Pair等,它们都具有更好的性能;
· 适当使用软引用和软引用;
· 采用内存缓存和磁盘缓存;
· 尽量采用静态内部类,这样可以避免潜在的由于内部类而导致的内存泄露