andorid 开发艺术探索 心得
第一章
- activity 的生命周期,包含在特殊情况下的处理,一些异常的分析。
activity 的四种启动模式
- standard : 标准模式
singleTop : 栈顶复用模式
如果当前activity在栈顶,则不会重新创建
singleTask : 站内复用模式(也是一种单实例模式)
当前栈内存在该activity,则不会重新创建,会把当前已经存在的activity回复到栈顶,同时clearTop(), 把在该activity前的activity回收掉.
singleInstance : 单实例模式,对singleTask的加强.
使当前的activity单独处于一个单独的栈内,避免了clearTop()问题.
TaskAffinity
属性,决定当前activity所在的栈.
任务栈分为:
前台任务栈
当前活动的activity
后台任务栈
当前处于暂停的activity.
- activity 的FLAG
- IntentFilter 的匹配规则.
第三章
view的事件传递
3.1 view 的基础知识
什么是view
Viewgroup 内部包含许多个控件view 的位置参数
- top (view左上角纵坐标)
- left (左上角横坐标)
- right (右下角横坐标)
- bottom (右下角纵坐标)
MotionEvent 和 TouchSlop
motionEvent : 手指接触屏幕所产生的一系列事件,几个典型事件:- ACTION_DOWN : 手指刚接触屏幕
- ACTION_MOVE : 手指在屏幕上移动
ACTION_UP : 手指从屏幕上松开的一瞬间。
正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件 :
点击屏幕后松开 : DOWN->UP
点击屏幕后滑动一会松开 : DOWN -> MOVE ->… -> MOVE -> UP
getX/getY: 返回的是相对于当前 view 左上角的x 和 y 坐标;getRawX/getRawY: 返回的是相对于手机屏幕左上角的x 和 y 坐标;
TouchSlopTouchSlop是系统所能识别出的被认为是滑动的最小距离。
可以通过: ViewConfiguration.get(getContext()).getScaledTouchSlop()
来获取到。
- VelocityTracker , GestureDetector 和 Scroller
VelocityTracker
为速度追踪,用于追踪手指哎滑动过程中的速度。包括水平和竖直方向上的速度.
首先在view 的onTouchEvent方法中追踪当前单击事件的速度
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
//获取速度
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int) velocityTracker.getXVelocity();
int yVelocity = (int) velocityTracker.getYVelocity();
获取速度之前,一定要先获取速度
computeCurrentVelocity()。
这里的速度是指一段时间内手指所划过的像素数.
当不需要的时候,需要调用clear()来重置并回收内存。
velocityTracker.clear();
velocityTracker.recycle();
GestureDetector
手势检测, 用于辅助检测用户的单击、滑动、长按、双击等行为.
创建一个GestureDetector 对象,并实现 OnGestureListener 接口,然后接管view 的onTouchEvent(),
GestureDetector gesture = new GestureDetector(this);
gesture.setIsLongpressEnabled(false);
在待监听View 的onTouchEvent方法中,实现
boolean consume = gesture.onTouchEvent(event);
return consume;
如果是在建通滑动相关的,建议在onTouchEvent中实现,如果要监听双击这种行为的话,那么就使用
GesutureDetector.
Scroller
弹性滑动对象,用于实现view的弹性滑动。Scroller 本身无法让view弹性滑动,它需要和view的computeScroller方法配合使用
才能完成这个功能。
3.2 View 的滑动
通过三种方式可实现View 的滑动 :
- 通过view本身提供的
scrollTo/scrollBy方法来实现滑动; - 第二种是通过动画给view 施加平移效果来实现滑动;
- 第三种是通过改变
View的LayoutParams使得View重新布局从而实现滑动。
使用 scrollTo/scrollBy
view 的边缘是指view 的位置,由四个顶点组成,而view 的内容边缘是指
view 中的内容的边缘。 scrollTo和scrollBy只能改变view 内容的位置
而不能改变view在布局中的位置。
使用动画
分为传统的view动画,和属性动画。
注意:如果使用属性动画,为了兼容3.0以下的版本,需要采用开源库nineoldandroids.
view 动画是对view的影像做操作,它并不能真正改变view的位置参数。
包括宽和高,如果希望动画后的状态保留,需要将fillAfter属性设置为true.view动画: 不能简单的给一个view做平移动画后并且希望它在新位置继续触发一些单击事件.
在3.0 上,可以通过属性动画解决这个问题。
改变布局参数
即改变LayoutParams.
MarginLayoutParams params = (MarginLayoutParams) mButton.getLayoutParams();
params.width += 100;
params.leftMargin += 100;
mButton.requestLayout();
通过改变LayoutParams 的方式去实现View 的滑动同样是一种很灵活的方法,需要根据
不同情况去做不同的处理。
各种滑动方式的对比
- scrollTo/scrollBy
操作简单,适合对view内容滑动, 但只能滑动view的内容,并不能滑动view本身. - 动画:
操作简单,主要试用于没有交互的view和实现复杂的动画效果。 - 改变布局参数 :
操作稍微复杂,适用于有交互的view
弹性滑动
3.3 弹性滑动
- 使用Scroller 滑动
通过动画
动画本身就是一种渐进的过程,通过它来实现的滑动天然就具有弹性效果。使用延时策略
具体来说可以使用Handler或view的postDelayed
3.4 View 的事件分发机制
view 的事件分发机制
view的一个难题滑动冲突,它的解决方法的理论基础就是时间分发机制.
3.4.1 点击事件的传递规则
public boolean dispatchTouchEvent(MotionEvent ev)
用来进行事件的分发。如果事件能够传递到当前view, 那么此方法一定会被调用,
返回结果受当前view的onTouchEvent和下级view 的dispatchTouchEvent()
的影响,表示是否消耗当前事件。public boolean onInterceptTouchEvent(MotionEvent event)
在上述方法内部调用,用来判断是否拦截某个事件,如果当前view拦截了某个事件,那
么在同一个事件序列当中,此方法不会被再次调用。返回结果表示是否拦截当前事件。public boolean onTouchEvent(MotionEvent event)
在
dispatchTouchEvent()方法中调用,用来处理点击事件,返回结果表示是否消耗
当前事件,如果不消耗,则在同一个事件序列中,当前view无法再次接收到事件。
伪代码:
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
注: 给view设置的
OnTouchListener, 其优先级比onTouchEvent要高。
当产生一个点击事件后,它的传递过程遵循如下顺序: activity -> window -> view.
关于事件传递
同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束
在这个过程中,事件以down 开始,中间含有数量不定的move 事件,最终以up 事件结束。正常情况下,一个事件序列只能被一个view拦截且消耗。
某个view一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件能够传递给它的话),并且
它的onInterceptTouchEvent不会再被调用。某个view一旦开始处理事件,如果它不消耗ACTION_DOWN 事件(
onTouchEvent返回了false),
那么同一个事件序列的其他事件都不会再交它来处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent,会被调用。如果view不消耗除ACTION_DOWN以外的其他事件,那么这个点记事件会消失,此时父元素的
onTouchEvent
并不会被调用,并且当前view可以持续收到后续的事件,最终这些消失的点击事件会传递给avtivity处理。ViewGroup默认不拦截任何事件。android源码中viewGroup的
onInterceptTouchEvent方法默认为false。- view没有
onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent()方法就会被调用. view 的
onTouchEvent()默认都会消耗事件(返回true), 除非它是不可点击的(clickable和longClickable同时为false).
view 的longClickable属性默认都为false,clickable属性要分情况,比如Button的clickable默认为true,而TextView的默认为false.view的enable属性不影响onTouchEvent的默认返回值。onClick会发生的前提是当前view是可点击的,并且它收到了down 和 up 的事件。事件的传递是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给view,通过
requestDisallowInterceptTouchEvent()
方法可以在子元素中干预父元素的分发过程,但是ACTION_DOWN 事件除外。
3.4.2 事件分发的源码分析
- Activity 对事件的分发过程
4. View 的工作原理
ViewRoot 和 DecorView
ViewRoot对应于ViewRootImpl类,它是连接WindowManger和DecorView的纽带,view的三大流程均是通过ViewRoot完成的。
MesureSpec
它在很大程度上决定了一个view 的尺寸规格
代表一个32位的int值, 高两位代表SpecMode(测量模式), 低30位代表SpecSize(某种测量模式下的大小);
view的工作流程只要是指mesaure、layout、draw, 这三大流程。
measure 确定view 的测量宽、高,, layout确定view的最终宽、高和四个顶点的位置, 而draw 则将view 绘制到屏幕上。
measure过程
分为 view 的measure过程, viewGroup的measure过程。
- view 的measure过程
由measure方法来完成, measure方法是一个final 类型的方法, 这意味着子类不能重写此方法。
在view的measure方法中,只会去调用view的onMesure方法,因此只需要看onMesure的实现即可,
view的 onMesure()如下:
protected void onMesure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasureDimension(getDefaultSize(getSuggestedMinimumWidth(),
widthMesureSpec), getDefaultSize(getSuggestedMinimumHeight(),
heightMeasureSpec));
}
- viewGroup 的过程
它没有mesure 方法, 有一个measureChildren()方法,在这个里面会对每一个view调用measureChild()方法,,在这个方法里,会调用view的measure()方法。
LinearLayout 的onMeasure(), 来分析ViewGroup的measure过程.
如何得到view的高和宽
在activity中调用
onWindowFocusChanged()缺点:会被多次调用,不太好
view.post(runnable)
通过post将一个
runnable投递到消息队列中,然后等待Looper调用此runnable的时候,view已经被初始化好了。例如:
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasureWidth();
int height = view.getMeasureHeight();
}
});
}
- 使用
ViewTreeObserver
它有很多回调接口可以完成这个功能,例如OnGlobalLayoutListener这个接口,当view树的状态发生改变或者view树内部的view的可见性发生改变时,onGlobalLayout()将被回调。
伴随着view树的状态的改变,
onGolbalLayout()会被调用多次。
- view.measure(int widthMeasureSpec, int heightMeasureSpec)
这种方法,有些复杂,而且情况多变,不一定会获取到
推荐第二种方法,即通过view.post(runnable)来实现
layout过程
layout 的过程是ViewGroup用来确定子元素的位置
当viewGroup的位置确定后,会在onLayout()遍历所有的子元素并调用其layout()(确定本身的位置), 在layout方法中onLayout()(确定子元素的位置)也会被调用。
draw过程
将view绘制到屏幕上,view的绘制遵循以下几步:
- 绘制背景 background.draw(canvas)
- 绘制自己(onDraw)
- 绘制children(dispatchDraw)
- 绘制装饰(onDrawScrollBars)
view里面还有一个方法:setWillNotDraw()
public void setWillNOtDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW, MASK);
}
如果一个view不需要绘制任何内容,那么设置这个标记后,系统会进行相应的优化。
默认情况下,view不会启用这个优化标志位,但是ViewGroup会默认启用这个优化标记位。
4.4 自定义View
大致分为4类
- 继承View 重写 onDraw()
- 继承ViewGroup派生特殊的Layout
- 继承特定的View(比如TextView)
- 继承特定的ViewGroup(比如LinearLayout)
继承view重写draw()
自定义View须知:
让view支持
wrap_content对于直接继承自View的控件,如果不对
wrap_content做特殊处理,那么使用的
wrap_content就相当于使用match_parent
需要在onMeasure()里面处理如果有必要,让你的view支持padding
margin属性,是有父控件控制的,它会在自定义的view中生效, 但padding 不会生效。
对这个问题,修改一下draw().尽量不要在view中使用Handler, 没必要
view本身提供了post()系列方法
view中如果有线程或者动画,需要及时停止,参考
View# onDetachedFromWindow如果不及时处理这种问题,有可能会造成内存泄漏。
view 有滑动嵌套情形时, 需要处理好滑动冲突。
继承view重写onDraw(), 需要注意上面两点
如何添加自定义的属性
- 在
attr.xml文件里添加自定义的属性
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color" />
</declare-styleable>
</resources>
- 在view的构造方法中解析这些自定义的属性(在含有defStyleAttr参数的构造函数中),并处理
TypedArray a = context.obtainStyleAttributes(attr, R.styleable.CircleView, defStyleattr, 0);
mColor = a.getColor(...);
//类似的操作
a.recycle();//释放资源
- 在布局文件中使用自定义属性
继承ViewGroup 派生处特殊的Layout
第9章 四大组件的工作过程
service 有启动状态(startService)和绑定状态(bindService)
Activity 的工作过程
启动activity 的真正实现是由 ActivityMangerNative.getDefault().startActivity() 完成的。ActivityMangerService (简称AMS), 继承自ActivityMangerNative,而ActivityMangerNative,继承自Binder并实现了IActivityManger这个Binder接口,
因此,AMS也是一个Binder, 它是IActivityManger的具体实现。
在ActivityMangerNative中,AMS这个Binder对象采用单例模式对外提供。
第10章 Android的消息机制
android 的消息处理有三个核心类:
looper,Handler,Message.
其中Looper里面包括MessageQueue(消息队列).
不允许在子线程中去修改主界面,需要Message, Handler,处理
Looper
它使一个普通线程变成Looper线程(循环工作的线程)
public class LooperThread extends Thread {
@Overide
public void run () {
//使当前线程变成Looper线程
Looper.prepare();
//...
//开始循环处理消息队列
Looper.loop();
}
}
Looper类中实例化的是 ThreadLocal(线程本地存储对象)
Looper.prepare()
在
Looper源码中,可以发现核心是将looper对象定义为ThreadLocal.Looper.loop()
调用loop()方法后,Looper线程就开始真正工作了,不断的从
MessageQueue取出
队头的消息执行。Looper.myLooper()
得到当前线程looper现象(一个线程只有一个looper)
public static final Looper myLooper() { //在任意线程中调用,返回的是那个线程的looper(即`ThreadLocal`) return (Looper)sThreadLocal.get(); }getThread()
得到looper对象所属线程:
public Thread getThread() { return mThread; }quit()
结束looper循环
public void quit() { //创建一个空的Message, 它的target 为null, 表示结束循环消息 Message msg = Message.obtin(); mQueue.enqueueMessage(msg, 0); }
handler
handler 扮演着在MQ上添加消息和处理消息的角色,通知MQ它要执行一个消息(sendMessage),
并在loop到自己的时候执行(handleMessage), 整个过程是异步的。
handler的处理
类里面,绑定关联MQ, 和looper
一个线程里只可以有一个Looper, 但可以有多个Handler
handler发送消息
handler处理消息
handleMessage(Message msg)
- handler 可以在任意线程发送消息,这些消息会被添加到关联的MQ上
- handler 是在它关联的looper线程中处理消息的
封装任务 Message
在消息处理机制中,message又叫task,
- 通过Message.obtain() 来从消息池中获得空消息对象,节省资源
- Message.arg1和Message.arg2传递信息,比Bundle更省内存
- 善用message.what 来标识信息,以便用不同方式处理message.
第12章 Bitmap 的记载和Cache
常用的缓存策略是
LruCache和DiskLruCacheLruCache常被用来做内存缓存,DiskLruCache常用作存储缓存
Bitmap 的高效加载
四类方法:
- decodeFile()
- decodeResource()
- decodeStream()
- decodeByteArray()
对应着
BitmapFactory类中的几个native方法
采用BitmapFactory.Options来加载所需的尺寸的图片。
图片的压缩
主要看一个参数inSampleSize,
通过一个函数来给inSampleSize赋值。
采样率
通过采样率可有效的加载图片:
主要的一个参数为inJustDecodeBounds
public static Bitmap decodeSampledBitmapFromResource(Resource res, int
resId, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
...
}
- 将BitmapFactory.Options 的inJustDecodeBounds 的参数设为true, 并加载图片
- 将BitmapFactory.Options 中取出图片的原始宽高信息,它们对应于outWidth和outHeight 参数
- 根据采样率的规则并结合目标Viewd的所需大小计算出采样率inSampleSize
- 将BitmapFactory.Options 的 inJustDecodeBounds 参数设为false, 然后重新加载
调用:
mImageView.setImageBitmap(
decodeSampledBitmapFromResources(), R.id.my_image, 100, 100);
Android 中的缓存策略
Lru LruCache
强引用StrongReference:
直接的对象引用(正常引用)
软引用SoftReference:
对于GC来说, SoftReference的强度明显低于 SrongReference.
SoftReference修饰的引用,其告诉GC:我是一个 软引用,当内存不足的时候,我指向的这个内存是可以给你释放掉的.
弱引用WeakReference:
WeakReference 的强度又明显低于 SoftReference 。 WeakReference 修饰的引用,其告诉GC:我是一个弱 引用,对于你的要求我没有话说,我指向的这个内存是可以给你释放掉的。
虚引用 PhantomReference:
虚引用其实和上面讲到的各种引用不是一回事的,他主要是为跟踪一个对象何时被GC回收。在android里面也是有用到的:FileCleaner.java
LruCache 是线程安全的,在里面有final LinkedHashMap
创建LruCache,实现sizeOf()方法
从LruCache中获取一个缓存对象:
mMemoryCache.get(key);
向LruCache中添加一个缓存对象:
mMemoryCache.add(key, bitmap);
通过remove 操作可删除一个指定的缓存对象
DiskLruCache
不能通过构造方法创建, 提供了一个open()方法用于构建自身
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize);
第一个参数表示磁盘缓存在文件系统中的存储路径。
第二个参数表示应用的版本号, 一般设为1即可。
第三个参数表示单个节点所对应的数据的个数, 一般为1 即可。
第四个参数表示缓存的总大小。
当缓存大小超出这个值后,DiskLruCache会清除一些缓存从而保证总大小不大于这个设定值。
DiskLruCache的缓存添加,是通过Editor完成的。(将url转换为key)DiskLruCache的缓存查找
ImageLoader的实现
一个优秀的ImageLoader的功能
- 图片的同步加载
- 图片的异步加载
- 图片的压缩(inSampleSize, inJustDecodeBounds)
- 内存缓存
- 磁盘缓存
- 网络拉取
优化列表的卡顿现象
不要在主线程中做太耗时的操作,
不要在getView()中执行耗时的操作
耗时的操作要通过异步的方式来处理。
控制异步任务的执行频率。
在列表滑动的时候停止加载图片,尽管这个过程是异步的,等列表停下来再去加载
图片仍然是可以获得良好的用户体验。
具体实现:
为RecyclerView 或是listView, gridView, 设置setOnScrollListener,
并在OnScrollListener的onScrollStateChanged()方法中判断列表是否处于滑动状态。如果是,就停止加载图片:public void onScrollStateChanged(AbsListView view, int scrollState) { if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) { mIsGridViewIdle = true; mImageAdapter.nitifyDataSetChanged(); } else { mIsGridViewIdle = false; } }然后再getView()方法中,仅当列表静止时才能加载图片。
if (mIsGridViewIdle && mCanGetBitmapFromNetWork) { imageView.setTag(uri); mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth); }
一般通过上面的两个方法就不会有列表的卡顿现象, 如果有某些特殊情况。
就可以通过硬件加速。
开启硬件加速
android:hardwareAccelerated="true"//即可为Activity开启硬件加速。