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
和DiskLruCache
LruCache
常被用来做内存缓存,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开启硬件加速。