Android 开发艺术探索

andorid 开发艺术探索 心得

第一章

  1. activity 的生命周期,包含在特殊情况下的处理,一些异常的分析
  2. activity 的四种启动模式

    • standard : 标准模式
    • singleTop : 栈顶复用模式

      如果当前activity在栈顶,则不会重新创建

    • singleTask : 站内复用模式(也是一种单实例模式)

      当前栈内存在该activity,则不会重新创建,会把当前已经存在的activity回复到栈顶,同时clearTop(), 把在该activity前的activity回收掉.

    • singleInstance : 单实例模式,对singleTask的加强.

      使当前的activity单独处于一个单独的栈内,避免了clearTop()问题.

TaskAffinity
属性,决定当前activity所在的栈.
任务栈分为:

  • 前台任务栈

    当前活动的activity

  • 后台任务栈

    当前处于暂停的activity.

  1. activity 的FLAG
  2. IntentFilter 的匹配规则.

第三章

view的事件传递

3.1 view 的基础知识

  1. 什么是view
    Viewgroup 内部包含许多个控件

  2. view 的位置参数

    • top (view左上角纵坐标)
    • left (左上角横坐标)
    • right (右下角横坐标)
    • bottom (右下角纵坐标)
  3. MotionEvent 和 TouchSlop
    motionEvent : 手指接触屏幕所产生的一系列事件,几个典型事件:

    • ACTION_DOWN : 手指刚接触屏幕
    • ACTION_MOVE : 手指在屏幕上移动
    • ACTION_UP : 手指从屏幕上松开的一瞬间。

      正常情况下,一次手指触摸屏幕的行为会触发一系列点击事件 :

    • 点击屏幕后松开 : DOWN->UP

    • 点击屏幕后滑动一会松开 : DOWN -> MOVE ->… -> MOVE -> UP

      getX/getY : 返回的是相对于当前 view 左上角的x 和 y 坐标;
      getRawX/getRawY : 返回的是相对于手机屏幕左上角的x 和 y 坐标;

TouchSlop
TouchSlop是系统所能识别出的被认为是滑动的最小距离。
可以通过: ViewConfiguration.get(getContext()).getScaledTouchSlop()
来获取到。

  1. 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 施加平移效果来实现滑动;
  • 第三种是通过改变ViewLayoutParams 使得View重新布局从而实现滑动。

使用 scrollTo/scrollBy

view 的边缘是指view 的位置,由四个顶点组成,而view 的内容边缘是指
view 中的内容的边缘。 scrollToscrollBy只能改变view 内容的位置
而不能改变view在布局中的位置。

使用动画

  1. 分为传统的view动画,和属性动画。

    注意:如果使用属性动画,为了兼容3.0以下的版本,需要采用开源库nineoldandroids.

  2. view 动画是对view的影像做操作,它并不能真正改变view的位置参数。
    包括宽和高,如果希望动画后的状态保留,需要将fillAfter属性设置为true.

  3. 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 弹性滑动

  1. 使用Scroller 滑动
  2. 通过动画
    动画本身就是一种渐进的过程,通过它来实现的滑动天然就具有弹性效果。

  3. 使用延时策略
    具体来说可以使用HandlerviewpostDelayed

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.

关于事件传递

  1. 同一个事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束
    在这个过程中,事件以down 开始,中间含有数量不定的move 事件,最终以up 事件结束。

  2. 正常情况下,一个事件序列只能被一个view拦截且消耗。

  3. 某个view一旦决定拦截,那么这一个事件序列都只能由它来处理(如果事件能够传递给它的话),并且
    它的onInterceptTouchEvent不会再被调用。

  4. 某个view一旦开始处理事件,如果它不消耗ACTION_DOWN 事件(onTouchEvent返回了false),
    那么同一个事件序列的其他事件都不会再交它来处理,并且事件将重新交由它的父元素去处理,即父元素的
    onTouchEvent ,会被调用。

  5. 如果view不消耗除ACTION_DOWN以外的其他事件,那么这个点记事件会消失,此时父元素的onTouchEvent
    并不会被调用,并且当前view可以持续收到后续的事件,最终这些消失的点击事件会传递给avtivity处理。

  6. ViewGroup默认不拦截任何事件。android源码中viewGroup的onInterceptTouchEvent方法默认为false。

  7. view没有onInterceptTouchEvent方法,一旦有点击事件传递给它,那么它的onTouchEvent()方法就会被调用.
  8. view 的onTouchEvent()默认都会消耗事件(返回true), 除非它是不可点击的(clickablelongClickable 同时为false).
    view 的longClickable属性默认都为false, clickable属性要分情况,比如 Buttonclickable 默认为true,而
    TextView的默认为false.

  9. viewenable属性不影响onTouchEvent的默认返回值。

  10. onClick会发生的前提是当前view是可点击的,并且它收到了down 和 up 的事件。

  11. 事件的传递是由外向内的,即事件总是先传递给父元素,然后再由父元素分发给view,通过requestDisallowInterceptTouchEvent()
    方法可以在子元素中干预父元素的分发过程,但是ACTION_DOWN 事件除外。

3.4.2 事件分发的源码分析

  1. Activity 对事件的分发过程

4. View 的工作原理

ViewRoot 和 DecorView

ViewRoot对应于ViewRootImpl类,它是连接WindowManger和DecorView的纽带,view的三大流程均是通过ViewRoot完成的。

MesureSpec

它在很大程度上决定了一个view 的尺寸规格

代表一个32位的int值, 高两位代表SpecMode(测量模式), 低30位代表SpecSize(某种测量模式下的大小);

view的工作流程只要是指mesaurelayoutdraw, 这三大流程。

measure 确定view 的测量宽、高,, layout确定view的最终宽、高和四个顶点的位置, 而draw 则将view 绘制到屏幕上。

measure过程

分为 view 的measure过程, viewGroup的measure过程。

  1. 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));
}
  1. viewGroup 的过程

它没有mesure 方法, 有一个measureChildren()方法,在这个里面会对每一个view调用measureChild()方法,,在这个方法里,会调用view的measure()方法。

LinearLayout 的onMeasure(), 来分析ViewGroup的measure过程.

如何得到view的高和宽

  1. 在activity中调用onWindowFocusChanged()

    缺点:会被多次调用,不太好

  2. view.post(runnable)

    通过post将一个runnable投递到消息队列中,然后等待Looper调用此runnable的时候,view已经被初始化好了。例如:

protected void onStart() {
    super.onStart();
    view.post(new Runnable() {
        @Override
        public void run() {
            int width = view.getMeasureWidth();
            int height = view.getMeasureHeight();
        }
    });
}
  1. 使用ViewTreeObserver

它有很多回调接口可以完成这个功能,例如OnGlobalLayoutListener这个接口,当view树的状态发生改变或者view树内部的view的可见性发生改变时,onGlobalLayout()将被回调。

伴随着view树的状态的改变,onGolbalLayout()会被调用多次。

  1. view.measure(int widthMeasureSpec, int heightMeasureSpec)

这种方法,有些复杂,而且情况多变,不一定会获取到

推荐第二种方法,即通过view.post(runnable)来实现

layout过程

layout 的过程是ViewGroup用来确定子元素的位置
当viewGroup的位置确定后,会在onLayout()遍历所有的子元素并调用其layout()(确定本身的位置), 在layout方法中onLayout()(确定子元素的位置)也会被调用。

draw过程

将view绘制到屏幕上,view的绘制遵循以下几步:

  1. 绘制背景 background.draw(canvas)
  2. 绘制自己(onDraw)
  3. 绘制children(dispatchDraw)
  4. 绘制装饰(onDrawScrollBars)

view里面还有一个方法:setWillNotDraw()

public void setWillNOtDraw(boolean willNotDraw) {
    setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW, MASK);
}

如果一个view不需要绘制任何内容,那么设置这个标记后,系统会进行相应的优化。
默认情况下,view不会启用这个优化标志位,但是ViewGroup会默认启用这个优化标记位。

4.4 自定义View

大致分为4类

  1. 继承View 重写 onDraw()
  2. 继承ViewGroup派生特殊的Layout
  3. 继承特定的View(比如TextView)
  4. 继承特定的ViewGroup(比如LinearLayout)

继承view重写draw()

自定义View须知:

  1. 让view支持 wrap_content

    对于直接继承自View的控件,如果不对wrap_content做特殊处理,那么使用的
    wrap_content就相当于使用match_parent
    需要在onMeasure()里面处理

  2. 如果有必要,让你的view支持padding

    margin属性,是有父控件控制的,它会在自定义的view中生效, 但padding 不会生效。
    对这个问题,修改一下draw().

  3. 尽量不要在view中使用Handler, 没必要

    view本身提供了post()系列方法

  4. view中如果有线程或者动画,需要及时停止,参考 View# onDetachedFromWindow

    如果不及时处理这种问题,有可能会造成内存泄漏。

  5. view 有滑动嵌套情形时, 需要处理好滑动冲突。

继承view重写onDraw(), 需要注意上面两点

如何添加自定义的属性

  1. attr.xml文件里添加自定义的属性
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color" />
    </declare-styleable>
</resources>
  1. 在view的构造方法中解析这些自定义的属性(在含有defStyleAttr参数的构造函数中),并处理
TypedArray a = context.obtainStyleAttributes(attr, R.styleable.CircleView, defStyleattr, 0);
mColor = a.getColor(...);
//类似的操作
a.recycle();//释放资源
  1. 在布局文件中使用自定义属性

继承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)

  1. handler 可以在任意线程发送消息,这些消息会被添加到关联的MQ上
  2. handler 是在它关联的looper线程中处理消息的

封装任务 Message

在消息处理机制中,message又叫task,

  1. 通过Message.obtain() 来从消息池中获得空消息对象,节省资源
  2. Message.arg1和Message.arg2传递信息,比Bundle更省内存
  3. 善用message.what 来标识信息,以便用不同方式处理message.

第12章 Bitmap 的记载和Cache

常用的缓存策略是LruCacheDiskLruCache
LruCache常被用来做内存缓存,DiskLruCache常用作存储缓存

Bitmap 的高效加载
四类方法:

  1. decodeFile()
  2. decodeResource()
  3. decodeStream()
  4. 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;
    ...
}
  1. 将BitmapFactory.Options 的inJustDecodeBounds 的参数设为true, 并加载图片
  2. 将BitmapFactory.Options 中取出图片的原始宽高信息,它们对应于outWidth和outHeight 参数
  3. 根据采样率的规则并结合目标Viewd的所需大小计算出采样率inSampleSize
  4. 将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)
  • 内存缓存
  • 磁盘缓存
  • 网络拉取

优化列表的卡顿现象
不要在主线程中做太耗时的操作,

  1. 不要在getView()中执行耗时的操作

    耗时的操作要通过异步的方式来处理。

  2. 控制异步任务的执行频率。

    在列表滑动的时候停止加载图片,尽管这个过程是异步的,等列表停下来再去加载
    图片仍然是可以获得良好的用户体验。
    具体实现:
    为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);
    }
    

一般通过上面的两个方法就不会有列表的卡顿现象, 如果有某些特殊情况。
就可以通过硬件加速。

  1. 开启硬件加速

    android:hardwareAccelerated="true"//即可为Activity开启硬件加速。