Android Development Art

第1章 Activity的生命周期和启动模式

1. 正常情况下Activity的生命周期

onCreate onStart onResume onPause onStop onDestroy

两个Activity切换过程的生命周期

ActivityA onPause()

ActivityB onCreate()

ActivityB onStart()

ActivityB onResume()

ActivityA onStop()

2. 异常情况下Activity的生命周期

onSaveInstanceState在onStop之前,和onPause没有既定的时序关系。

onRestoreInstanceState在onStart之后

每个View都有自己的onSaveInstanceState和onRestoreInstanceState,可以用来知道系统会自动保存的内容。

Activity被终止->Activity调用onSaveInstanceState去保存数据->Activity委托Window去保存数据->Window再委托它上面的顶级容器去保存数据(ViewGroup,一般为DecorView)->顶层容器再去一一通知子元素来保存数据->数据保存过程完成

正常启动时onCreate的参数savedInstanceState为空,在重建时不为空,而onRestoreInstanceState只有在被重建时才会被调用,所以参数一定不会为空。同样,正常启动、销毁过程中不会去调用onSaveInstanceState和onRestoreInstanceState两个方法。

在AndroidManifest中为Activity添加configChanges选项,可以阻止Activity被重建,当configChanges里的属性变化时,会调用onConfigurationChanged方法,通过重写这个方法可以监听到config的变化(如键盘弹出、屏幕旋转等)

3. Activity的启动模式

四种启动模式:

(1)standard:标准模式

在同一个Task中重复创建多个实例。

非Activity的context(如Application Context)启动Activity时,在standard模式下需要添加FLAG_ACTIVITY_NEW_TASK,并实际上是以singleTask模式启动。

(2)singleTop:栈顶复用模式(应常用)

如果新的Activity A已在栈顶,Activity A不会被重建,onNewIntent方法会被调用,通过此方法可以取出请求信息。此Activity的onCreate、onStart不会被系统调用。如果新的Activity A不在栈顶,新Activity A会重建。

(3)singleTask:栈内复用模式

Acitivity A想要的栈 不存在: 重新创建一个任务栈

存在: 实例存在: 把A调到栈顶并调用它的onNewIntent方法(默认会clear top,即A之上的activity会被全部销毁)

实例不存在: 创建A,并把A压入栈中
(4)singleInstance:单例模式

加强的singleTask,除了具有singleTask的所有特性外,此模式Activity只能单独地位于一个任务栈中。

4. TaskAffinity

主要和singleTask启动模式或者allowTaskReparenting属性配对使用。

5. Flag

6. IntentFilter的匹配规则

标签可包含多个标签。

一个activity含有多个filter时,只要匹配其中一个filter即可启动该activity。intent最多只有一个action,可能含有多个category。

一个filter的三类标签如果存在都需要匹配才认为filter匹配成功

匹配规则:

:filter含有多个action时,intent只要匹配其中一个action则认为标签匹配,如果intent不含action则认为不匹配。(与category有区别)

:filter含有多个category时,intent中的所有category都要能在filter中找到对应的category,如果intent没有设置category,则认为category可以匹配(因为系统会自动添加android.intent.category.DEFAULT这个category,同时,为了能够匹配,我们的activity中需要手动添加这个category)。

:匹配规则和action相似。data有两部分:mimeType和URI

URI的结构:<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]

例如:cotent://com.example.project:200/folder/subfolder/etc

        http://www.baidu.com:80/search/info

scheme URI的模式,如http、file、content。如果没指定scheme,则整个URI的其他参数无效,即URI无效
host URI的主机名,如果host未指定,则整个URI的其他参数无效,即URI无效
port 仅当URI中指定了scheme和host参数的时候port才有意义
path,

pathPrefix,

pathPattern

path表示完整的路径信息;pathPattern也表示完整路径信息,它可以是一个正则表达式;pathPrefix表示路径的前缀信息。
要匹配的mimeType,intent必须要指定scheme为file或者content必须要调用.setDataAndType方法,且不能分开调用。如:intent.setDataAndType(Uri.parse("file://abc","image/png"))

同一个filter的多个共同组成同一个标签,如果重复,会产生覆盖

隐式启动前可以判断activity是否存在的两种方法:

(1)PackageManager的resolveActivity方法

(2)Intent的resolveActivity方法

如果找不到则会返回null。

PackageManager还提供了queryIntentActivities方法,它会返回所有成功匹配的Activity信息,而不只是最佳匹配

需要添加MATCH_DEFAULT_ONLY标志,否则会将不含android.intent.category.DEFAULT的activity返回,会导致启动失败。如果添加了这个标志位,返回结果不为null,则一定可以启动成功。(不加这个标志位,会有可能失败)

第2章 IPC机制

第3章 View的事件体系

1. View的位置参数

getLeft(),getTop(),getRight(),getBottom()得到的坐标是相对于父View的位置信息;

x,y,translationX,translationY也都是相对于父View的左上角位置;

x = left + translationX,translationX,translationY默认为0,View在移动过程中top,left表示的是原始左上角的位置信息,其值不会发生变化。

2. MotionEvent和TouchSlop

MotionEvent的getX/getY返回相对于当前View的左上角的x和y,getRawX/getRawY返回的是相对于屏幕左上角的x和y。

TouchSlop指滑动的最小距离常量,可以通过ViewConfiguration.get(getContext()).getScaledTouchSlop()获得。该值在不同设备上可能会有不同。

3. VelocityTracker, GestureDetector和Scroller的使用

4.View滑动的方法

(1)使用scrollTo/scrollBy

他们只改变View内容的位置,不改变View的位置。mSrollX的值总等于View左边缘和View内容左边缘在水平方向的距离,mSrollX可能为正也可能为负,当View左边缘在View内容左边缘的右边时,值为正,反之为负。mSrollY同理。

(2)使用动画

传统的动画方式

(3)改变布局参数

改变param的marginLeft等参数等,实现视觉上平移的效果。

5. View的事件分发机制

dispatchTouchEvent

onInterceptTouchEvent

onTouchEvent

调用范例:

public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
Activity->Window->View

优先级:

onInterceptTouchEvent:OnTouchListener(onTouch)

onTouchEvent: TouchDelegate(onTouchEvent), OnClickListener(onClick)

一系列事件:ACTION_DOWN -- (N次)ACTION_MOVE -- ACTION_UP

基类View没有onInterceptTouchEvent,如果没有事件传递进来则直接调用onTouchEvent

一个事件序列只能被一个View拦截消耗,如果一个View一旦决定拦截,则一系列事件均由它来处理,并且它的onInterceptTouchEvent事件不会再被调用。

ViewGroup默认不拦截任何事件,View的onTouchEvent默认都会消耗事件。

6. View事件分发原理解析

Window类是抽象类,具体实现类只有PhoneWindow。

DecorView是setContentView的父View,是一个FrameLayout的子类。可通过下面的方法获得set的View:

((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0);

7. 滑动冲突

三种情形:

(1)外部滑动方向和内部滑动方向不一致:根据x和y方向滑动距离区别,做不同的响应

(2)外部滑动方向和内部滑动方向一致:根据实际业务需要做响应

(3)上面两种情况的嵌套:是(1)和(2)的综合

解决方案:

外部拦截法和内部拦截法

(1)外部拦截法:

在父容器中解决滑动冲突,重写父容器的onInterceptTouchEvent方法,做相应的拦截。

(2)内部拦截法:

父容器不拦截任何事件,所有事件都传递给子元素。如果子元素需要此事件就直接消耗,否则就交由父容器进行处理。

需要配合requestDisallowInterceptTouchEvent,重写子元素的dispatchTouchEvent和父容器的onInterceptTouchEvent

第4章 View的工作原理

1. View的三大流程

ViewRoot是连接WindowManager和DecorView的纽带,实现类为ViewRootImpl。

performTraversals (ViewRoot, ViewRootImpl) View
测量流程 performMeasure measure onMeasure measure
布局流程 performLayout layout onLayout layout
绘制流程 performDraw draw onDraw draw
如果包含titleBar,DecorView则包含一个竖直方向的LinearLayout。

MeasureSpec:子View的MeasureSpec由父View的MeasureSpec和本身的LayoutParam共同决定,一般UNSPECIFIED用不到,不需要关注。

EXACTLY AT_MOST UNSPECIFIED
dp/px
EXACTLY

childSize

EXACTLY

childSize

EXACTLY

childSize

match_parent
EXACTLY

parentSize

AT_MOST

parentSize

UNSPECIFIED

0

wrap_content
AT_MOST

parentSize

AT_MOST

parentSize

UNSPECIFIED

0

measure :测量,确定View的测量宽/高,

layout:布局,确定View最终的宽/高

draw:绘制,将View绘制到屏幕上

自定义View时,需要重写onMeasure()方法,否则wrap_content的效果会和match_parent一样。

2. measure

在onLayout方法中去获取View的测量宽/高或者最终宽/高。

View的measure过程和Activity的生命周期方法不是同步执行的,所以在Activity的声明周期获得View的宽/高有可能导致获取结果不正确。可以通过以下四种方法获取:

(1)Activity/View#onWindowFocusChanged

(2)view.post(runnable)

(3)ViewTreeObserver

(4)view.measure(int widthMeasureSpec, int heightMeasureSpec),较为复杂。

第5章 理解RemoteViews

  1. 应用:

    通知栏 NotificationManager

    桌面小部件 AppWidgetProvider

  2. PendingIntent

  3. RemoteViews的单击事件

只支持发起PendingIntent,不支持onClickListener。

setOnClickPendingIntent (不能给ListView中的item通过此方式添加单击事件,开销过大,系统禁用)

setPendingIntentTemplate

setOnClickFillInIntent

第6章 Android的Drawable

  1. Drawable

常用作背景

内部宽高:getIntrinsicWidth和getIntrinsicHeight可以获取到它们。

但并不是所有Drawable都有宽高,图片类型有,但颜色组成的Drawable没有宽高的概念。

level为0-10000。

  1. 分类

1.)BitmapDrawable:

2.)ShapeDrawable: (实体类是GradientDrawable)

3.)LayerDrawable:

4.)StateListDrawable:

5.)LevelListDrawable: level有一个区间,切换不同的图片

6.)TransitionDrawable:

用于实现两个Drawable之间淡入淡出的效果,startTransition, reverseTransition

7.)InsetDrawable: 将其他drawable内嵌到自己当中,可以实现作为背景时,显示大小小于View实际

占用大小。通过LayerDrawable也可以实现这种效果。

8.)ScaleDrawable: 默认level为0,不显示,改变level会产生不同的效果

9.)ClipDrawable: 受gravity和level的影响产生不同的效果

  1. 自定义Drawable

getBounds可以获得Drawble的实际显示大小。

第7章 Android动画深入分析

  1. View动画

平移动画(TranslationAnimation,translate)

缩放动画(ScaleAnimation,scale)

旋转动画(RotateAnimation,rotate)

透明度动画(AlphaAnimation,alpha)

自定义View动画:

1.)继承Animation这个抽象类

2.)重写initialize和applyTransformation方法

3.)在initialize方法中做一些初始化工作

4.)在applyTransformation中进行相应的矩阵变换(Matrix,使用Camera简化矩阵变换的过程)

  1. 帧动画

  2. 属性动画

View动画的特殊使用场景:

LayoutAnimation

Activity的切换

第8章 理解Window和WindowManager

  1. Window是一个抽象类,它的具体实现是PhoneWindow

Window的三种类型:

1.) 应用Window 对应Activity {1~99}

2.) 子Window 不能单独存在,它需要附属在特定的父Window之中(如Dialog){1000~1999}

3.)系统Window 需要声明权限才能创建的Window(如Toast和系统状态栏){2000~2999}

Window是分层的,每个Window都有对应的z-ordered,层级大的覆盖层级小的window上面,层级范围对应着WindowManager.LayoutParams的type参数。

WindowManager继承ViewManager,提供常用的三种方法:

1.)添加View

2.)更新View

3.)删除View

实现拖动的方法:

根据手指的位置来设定LayoutParams中的x和y的值即可改变Window的位置。首先给View设置onTouchListener,然后在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;
}

  1. Window的内部机制

无法直接访问Window,对Window的访问必须通过WindowManager。

第9章 四大组件的工作过程

四大组件的启动

第10章 Android的消息机制

  1. ViewRootImpl对UI操作做了验证,保证只能在主线程中更新UI,ViewRootImpl的checkThread方法。

不采用多线程的加锁机制更新UI的两个原因:

1.)加锁机制会让UI访问的逻辑变得复杂

2.)锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。

  1. ThreadLocal的工作原理(Looper就是典型实例,Looper对象只在本线程有效)

ThreadLocal是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有在指定线程中可以获取到存储的数据。

使用场景:

1.)一般来说,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。

      对于Looper来说,如果不采用ThreadLocal,系统就必须提供一个全局的哈希表供Handler查找指定线程的Looper,就必须提供一个类似于LooperManager的类。

2.)复杂逻辑下的对象传递,调用栈较为复杂的时候。

ThreadLocal的内部最重要的几个方法:get(),set(),values.put()

3.MessageQueue的工作原理

MessageQueue的主要两个操作:

1.)插入 enqueueMessage

2.)读取 next:伴随着删除操作。无限循环,有新消息到来就返回这条消息,并从单链表中移除

MessageQueue的内部实现是单链表数据结构,而不是队列。

  1. Looper的工作原理

它会不停地从MessageQueue中查看是否有新消息,如果有新消息就会立刻处理,否则就一直阻塞在那里。

Looper的退出:

1.)quit(),直接退出

2.)quitSafely(),设定一个退出标记,然后把消息队列中的已有消息处理完毕后才安全退出。

Looper退出之后,通过Handler发送的消息会失败,send方法返回false。

如果是在子线程手动创建Looper,则应该在所有事情完成以后调用quit方法来终止消息循环,否则这个子线程会一直处于等待状态,而如果推出Looper以后,这个子线程会立刻终止。

prepare()方法是一个死循环,唯一跳出循环的方式是MessageQueue的next()方法返回了null,而next()只有在Looper被标记为退出状态时才会返回null,没有消息是next()会处于阻塞状态,所以looper也处于阻塞状态。

  1. Handler的工作原理

除了常用的用法外,还可以实现Callback接口,然后创建Handler对象:

public interface Callback{
public boolean handleMessage(Message msg);
}
Handler mHandler = new Handler(callback);

第11章 Android的线程和线程池

AsyncTask 主要是为了方便开发者在子线程中更新UI

IntentService 内部采用HandlerThread来执行任务,当任务执行完毕后IntentService会自动退出

HandlerThread

第12章 Bitmap的加载和Cache

三方面内容:

1.)如何有效加载Bitmap

2.)Android中常用的缓存策略,LruCache(用于内存缓存)和DiskLruCache(用于存储设备缓存)

3.)优化列表的卡顿现象

1.Bitmap的高效加载

BitmapFactory的四类方法加载Bitmap对象:

decodeFile,decodeResorce,decodeStream,decodeByteArray;(decodeFile,decodeResorce会间接调用decodeStream,四种方法最终都会在底层实现,对应native方法)

通过BitmapFactory.Options可以按一定的采样率来加载缩小后的图片,降低内存占用,避免OOM。四种方法都支持BitmapFactory.Options作为参数。设置inSampleSize。

使用BitmapFactory.Options需要四步:

(1)将BitmapFactory.Options的inJustDecodeBounds参数设为true,并decodeXXX()。用于测量原始宽高,但并不会真正进行解码工作。

(2)从BitmapFactory.Options中获取图片的原始宽高信息,他们对应于outWidth和outHeight。

(3)根据采样规则并结合目标View的所需大小计算出采样率inSampleSize.

(4)将BitmapFactory.Options的inJustDecodeBounds参数设为false,然后调用decodeXXX()进行真正的加载工作。