《Android开发艺术探索》笔记

本笔记整理自: https://www.gitbook.com/book/tom510230/android_ka_fa_yi_shu_tan_suo/details
参考文章:http://szysky.com/tags/#笔记、http://blog.csdn.net/player_android/article/category/6577498
联系我:xiadeye@icloud.com

本书是一本Android进阶类书籍,采用理论、源码和实践相结合的方式来阐述高水准的Android应用开发要点。本书从三个方面来组织内容。

  1. 介绍Android开发者不容易掌握的一些知识点
  2. 结合Android源代码和应用层开发过程,融会贯通,介绍一些比较深入的知识点
  3. 介绍一些核心技术和Android的性能优化思想

目录

第1章 Activity的生命周期和启动模式
第2章 IPC机制
第3章 View的事件体系
第4章 View的工作原理
第5章 理解RemoteViews
第6章 Android的Drawable
第7章 Android动画深入分析
第8章 理解Window和WindowManager
第9章 四大组件的工作过程
第10章 Android的消息机制
第11章 Android的线程和线程池
第12章 Bitmap的加载和Cache
第13章 综合技术
第14章 JNI和NDK编程
第15章 Android性能优化

[TOC]

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

1.1 Activity的生命周期全面分析

用户正常使用情况下的生命周期 & 由于Activity被系统回收或者设备配置改变导致Activity被销毁重建情况下的生命周期。

1.1.1 典型情况下的生命周期分析

Activity的生命周期和启动模式


  1. Activity第一次启动:onCreate->onStart->onResume。
  2. Activity切换到后台( 用户打开新的Activity或者切换到桌面) ,onPause->onStop(如果新Activity采用了透明主题,则当前Activity不会回调onstop)。
  3. Activity从后台到前台,重新可见,onRestart->onStart->onResume。
  4. 用户退出Activity,onPause->onStop->onDestroy。
  5. onStart开始到onStop之前,Activity可见。onResume到onPause之前,Activity可以接受用户交互。
  6. 在新Activity启动之前,栈顶的Activity需要先onPause后,新Activity才能启动。所以不能在onPause执行耗时操作。
  7. onstop中也不可以太耗时,资源回收和释放可以放在onDestroy中。

1.1.2 异常情况下的生命周期分析

1 系统配置变化导致Activity销毁重建

例如Activity处于竖屏状态,如果突然旋转屏幕,由于系统配置发生了改变,Activity就会被销
毁并重新创建。
在异常情况下系统会在onStop之前调用onSaveInstanceState来保存状态。Activity重新创建后,会在onStart之后调用onRestoreInstanceState来恢复之前保存的数据。



保存数据的流程: Activity被意外终止,调用onSaveIntanceState保存数据-> Activity委托Window,Window委托它上面的顶级容器一个ViewGroup( 可能是DecorView) 。然后顶层容器在通知所有子元素来保存数据。

这是一种委托思想,Android中类似的还有:View绘制过程、事件分发等。

系统只在Activity异常终止的时候才会调用 onSaveInstanceState 和onRestoreInstanceState 方法。其他情况不会触发。

2 资源内存不足导致低优先级的Activity被回收
三种Activity优先级:前台- 可见非前台 -后台,从高到低。
如果一个进程没有四大组件,那么将很快被系统杀死。因此,后台工作最好放入service中。

android:configChanges="orientation" 在manifest中指定 configChanges 在系统配置变化后不重新创建Activity,也不会执行 onSaveInstanceState 和onRestoreInstanceState 方法,而是调用 onConfigurationChnaged 方法。
附:系统配置变化项目

configChanges 一般常用三个选项:

  1. locale 系统语言变化
  2. keyborardHidden 键盘的可访问性发生了变化,比如用户调出了键盘
  3. orientation 屏幕方向变化

1.2 Activity的启动模式

1.2.1 Activity的LaunchMode

Android使用栈来管理Activity。

  1. standard
    每次启动都会重新创建一个实例,不管这个Activity在栈中是否已经存在。谁启动了这个Activity,那么Activity就运行在启动它的那个Activity所在的栈中。
    用Application去启动Activity时会报错,原因是非Activity的Context没有任务栈。解决办法是为待启动Activity制定FLAG_ACTIVITY_NEW_TASH标志位,这样就会为它创建一个新的任务栈。
  2. singleTop
    如果新Activity位于任务栈的栈顶,那么此Activity不会被重新创建,同时回调 onNewIntent 方法。onCreate和onStart方法不会被执行。
  3. singleTask
    这是一种单实例模式。如果不存在activity所需要的任务栈,则创建一个新任务栈和新Activity实例;如果存在所需要的任务栈,不存在实例,则新创建一个Activity实例;如果存在所需要的任务栈和实例,则不创建,调用onNewIntent方法。同时使该Activity实例之上的所有Activity出栈。
    参考:taskAffinity标识Activity所需要的任务栈
  4. singleIntance
    单实例模式。具有singleTask模式的所有特性,同时具有此模式的Activity只能独自位于一个任务栈中。

假设两个任务栈,前台任务栈为12,后台任务栈为XY。Y的启动模式是singleTask。现在请求Y,整个后台任务栈会被切换到前台。如图所示:



设置启动模式

  1. manifest中 设置下的 android:launchMode 属性。
  2. 启动Activity的 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 。
  3. 两种同时存在时,以第二种为准。第一种方式无法直接为Activity添加FLAG_ACTIVITY_CLEAR_TOP标识,第二种方式无法指定singleInstance模式。
  4. 可以通过命令行 adb shell dumpsys activity 命令查看栈中的Activity信息。

1.2.2 Activity的Flags

这些FLAG可以设定启动模式、可以影响Activity的运行状态。

  • FLAG_ACTIVITY_NEW_TASK
    为Activity指定“singleTask”启动模式。
  • FLAG_ACTIVITY_SINGLE_TOP
    为Activity指定“singleTop"启动模式。
  • FLAG_ACTIVITY_CLEAR_TOP
    具有此标记位的Activity启动时,同一个任务栈中位于它上面的Activity都要出栈,一般和FLAG_ACTIVITY_NEW_TASK配合使用。
  • FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
    如果设置,新的Activity不会在最近启动的Activity的列表(就是安卓手机里显示最近打开的Activity那个系统级的UI)中保存。等同于在xml中指定android:exludeFromRecents="true"属性。

1.3 IntentFilter的匹配规则

Activity调用方式

  1. 显示调用 明确指定被启动对象的组件信息,包括包名和类名
  2. 隐式调用 不需要明确指定组件信息,需要Intent能够匹配目标组件中的IntentFilter中所设置的过滤信息。

匹配规则

  • IntentFilter中的过滤信息有action、category、data。
  • 只有一个Intent同时匹配action类别、category类别、data类别才能成功启动目标Activity。
  • 一个Activity可以有多个intent-filter,一个Intent只要能匹配任何一组intent-filter即可成功启动对应的Activity。

** action**
action是一个字符串,匹配是指与action的字符串完全一样,区分大小写。
一个intent-filter可以有多个aciton,只要Intent中的action能够和任何一个action相同即可成功匹配。
Intent中如果没有指定action,那么匹配失败。

** category**
category是一个字符串。
Intent可以没有category,但是如果你一旦有category,不管有几个,每个都必须与intent-filter中的其中一个category相同。
系统在 startActivity 和 startActivityForResult 的时候,会默认为Intent加上 android.intent.category.DEFAULT 这个category,所以为了我们的activity能够接收隐式调用,就必须在intent-filter中加上 android.intent.category.DEFAULT 这个category。

** data**
data的匹配规则与action一样,如果intent-filter中定义了data,那么Intent中必须要定义可匹配的data。
intent-filter中data的语法:

        <data android:scheme="string"
        android:host="string"
        android:port="string"
        android:path="string"
        android:pathPattern="string"
        android:pathPrefix="string"
        android:mimeType="string"/>

Intent中的data有两部分组成:mimeType和URI。mimeType是指媒体类型,比如
image/jpeg、audio/mpeg4-generic和video/等,可以表示图片、文本、视频等不同的媒
体格式。

URI的结构:

    <scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]

实际例子

    content://com.example.project:200/folder/subfolder/etc
    http://www.baidu.com:80/search/info

scheme:URI的模式,比如http、file、content等,默认值是 file 。
host:URI的主机名
port:URI的端口号
path、pathPattern和pathPrefix:这三个参数描述路径信息。
path、pathPattern可以表示完整的路径信息,其中pathPattern可以包含通配符 * ,表示0个或者多个任意字符。
pathPrefix只表示路径的前缀信息。

过滤规则的uri为空时,有默认值content和file,因此intent设置uri的scheme部分必须为content或file。
Intent指定data时,必须调用 setDataAndType 方法, setData 和 setType 会清除另一方的值。
对于service和BroadcastReceiver也是同样的匹配规则,不过对于service最好使用显式调用。

隐式调用需注意

  • 当通过隐式调用启动Activity时,没找到对应的Activity系统就会抛出 android.content.ActivityNotFoundException 异常,所以需要判断是否有Activity能够匹配我们的隐式Intent。

  • 采用 PackageManager 的 resloveActivity 方法或Intent 的 resloveActivity 方法
    public abstract List<ResolveInfo> queryIntentActivityies(Intent intent,int flags);
    public abstract ResolveInfo resloveActivity(Intent intent,int flags);

    以上的第二个参数使用 MATCH_DEFAULT_ONLY ,这个标志位的含义是仅仅匹配那些在
    intent-filter中声明了 android.intent.category.DEFAULT 这个category的Activity。因为如果把不含这个category的Activity匹配出来了,由于不含DEFAULT这个category的Activity是无法接受隐式Intent的从而导致startActivity失败。

  • 下面的action和category用来表明这是一个入口Activity,并且会出现在系统的应用列表中,二者缺一不可。
    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />

2 IPC机制

2.1 Android IPC 简介

  • IPC即Inter-Process Communication,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
  • 线程是CPU调度的最小单元,是一种有限的系统资源。进程一般指一个执行单元,在PC和移动设备上是指一个程序或者应用。进程与线程是包含与被包含的关系。一个进程可以包含多个线程。最简单的情况下一个进程只有一个线程,即主线程( 例如Android的UI线程) 。
  • 任何操作系统都需要有相应的IPC机制。如Windows上的剪贴板、管道和邮槽;Linux上命名管道、共享内容、信号量等。Android中最有特色的进程间通信方式就是binder,另外还支持socket。contentProvider是Android底层实现的进程间通信。
  • 在Android中,IPC的使用场景大概有以下:
    • 有些模块由于特殊原因需要运行在单独的进程中。
    • 通过多进程来获取多份内存空间。
    • 当前应用需要向其他应用获取数据。

2.2 Android中的多进程模式

2.2.1 开启多进程模式

在Android中使用多线程只有一种方法:给四大组件在Manifest中指定 android:process 属性。这个属性的值就是进程名。这意味着不能在运行时指定一个线程所在的进程。

tips:使用 adb shell ps 或 adb shell ps|grep 包名 查看当前所存在的进程信息。

两种进程命名方式的区别

  1. “:remote”
    “:”的含义是指在当前的进程名前面附加上当前的包名,完整的进程名为“com.example.c2.remote"。这种进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中。
  2. "com.example.c2.remote"
    这是一种完整的命名方式。这种进程属于全局进程,其他应用可以通过ShareUID方式和它跑在同一个进程中。

2.2.2 多线程模式的运行机制

Android为每个进程都分配了一个独立的虚拟机,不同虚拟机在内存分配上有不同的地址空间,导致不同的虚拟机访问同一个类的对象会产生多份副本。例如不同进程的Activity对静态变量的修改,对其他进程不会造成任何影响。所有运行在不同进程的四大组件,只要它们之间需要通过内存在共享数据,都会共享失败。四大组件之间不可能不通过中间层来共享数据。

多进程会带来以下问题:

  1. 静态成员和单例模式完全失效。
  2. 线程同步锁机制完全失效。
    这两点都是因为不同进程不在同一个内存空间下,锁的对象也不是同一个对象。
  3. SharedPreferences的可靠性下降。
    SharedPreferences底层是 通过读/写XML文件实现的,并发读/写会导致一定几率的数据丢失。
  4. Application会多次创建。
    由于系统创建新的进程的同时分配独立虚拟机,其实这就是启动一个应用的过程。在多进程模式中,不同进程的组件拥有独立的虚拟机、Application以及内存空间。

多进程相当于两个不同的应用采用了SharedUID的模式

实现跨进程的方式有很多:

  1. Intent传递数据。
  2. 共享文件和SharedPreferences。
  3. 基于Binder的Messenger和AIDL。
  4. Socket等

2.3 IPC基础概念介绍

主要介绍 Serializable 、 Parcelable 、 Binder 。Serializable和Parcelable接口可以完成对象的序列化过程,我们通过Intent和Binder传输数据时就需要Parcelabel和Serializable。还有的时候我们需要对象持久化到存储设备上或者通过网络传输到其他客户端,也需要Serializable完成对象持久化。

2.3.1 Serializable接口

Serializable 是Java提供的一个序列化接口( 空接口) ,为对象提供标准的序列化和反序列化操作。只需要一个类去实现 Serializable 接口并声明一个 serialVersionUID 即可实现序列化。

private static final long serialVersionUID = 8711368828010083044L

serialVersionUID也可以不声明。如果不手动指定 serialVersionUID 的值,反序列化时如果当前类有所改变( 比如增删了某些成员变量) ,那么系统就会重新计算当前类的hash值并更新 serialVersionUID 。这个时候当前类的 serialVersionUID 就和序列化数据中的serialVersionUID 不一致,导致反序列化失败,程序就出现crash。

静态成员变量属于类不属于对象,不参与序列化过程,其次 transient 关键字标记的成员变量也不参与序列化过程。

通过重写writeObject和readObject方法可以改变系统默认的序列化过程。

2.3.2 Parcelable接口

Parcel内部包装了可序列化的数据,可以在Binder中自由传输。序列化过程中需要实现的功能有序列化、反序列化和内容描述。

序列化功能由 writeToParcel 方法完成,最终是通过 Parcel 的一系列writer方法来完成。

   @Override
    public void writeToParcel(Parcel out, int flags) {
    out.writeInt(code);
    out.writeString(name);
    }

反序列化功能由 CREATOR 来完成,其内部表明了如何创建序列化对象和数组,通过 Parcel 的一系列read方法来完成。

public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
} 
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
protected Book(Parcel in) {
code = in.readInt();
name = in.readString();
}

在Book(Parcel in)方法中,如果有一个成员变量是另一个可序列化对象,在反序列化过程中需要传递当前线程的上下文类加载器,否则会报无法找到类的错误。

   book = in.readParcelable(Thread.currentThread().getContextClassLoader());

内容描述功能由 describeContents 方法完成,几乎所有情况下都应该返回0,仅当当前对象中存在文件描述符时返回1。

public int describeContents() {
return 0;
}

Serializable 是Java的序列化接口,使用简单但开销大,序列化和反序列化过程需要大量I/O操作。而 Parcelable 是Android中的序列化方式,适合在Android平台使用,效率高但是使用麻烦。 Parcelable 主要在内存序列化上,Parcelable 也可以将对象序列化到存储设备中或者将对象序列化后通过网络传输,但是稍显复杂,推荐使用 Serializable 。

2.3.3 Binder

Binder是Android中的一个类,实现了 IBinder 接口。从IPC角度说,Binder是Andoird的一种跨进程通讯方式,Binder还可以理解为一种虚拟物理设备,它的设备驱动是/dev/binder。从Android Framework角度来说,Binder是 ServiceManager 连接各种Manager( ActivityManager· 、 WindowManager )和相应 ManagerService 的桥梁。从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService时,服务端返回一个包含服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务器端提供的服务或者数据( 包括普通服务和基于AIDL的服务)。

Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。

图中的Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder驱动进行交互的,从而实现IPC通信方式。其中Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层,开发人员只需自定义实现client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。
http://gityuan.com/2015/10/31/binder-prepare/

Android中Binder主要用于 Service ,包括AIDL和Messenger。普通Service的Binder不涉及进程间通信,Messenger的底层其实是AIDL,所以下面通过AIDL分析Binder的工作机制。

由系统根据AIDL文件自动生成.java文件

  1. Book.java
    表示图书信息的实体类,实现了Parcelable接口。
  2. Book.aidl
    Book类在AIDL中的声明。
  3. IBookManager.aidl
    定义的管理Book实体的一个接口,包含 getBookList 和 addBook 两个方法。尽管Book类和IBookManager位于相同的包中,但是在IBookManager仍然要导入Book类。
  4. IBookManager.java
    系统为IBookManager.aidl生产的Binder类,在 gen 目录下。
    IBookManager继承了 IInterface 接口,所有在Binder中传输的接口都需要继IInterface接口。结构如下:
    • 声明了 getBookList 和 addBook 方法,还声明了两个整型id分别标识这两个方法,用于标识在 transact 过程中客户端请求的到底是哪个方法。
    • 声明了一个内部类 Stub ,这个 Stub 就是一个Binder类,当客户端和服务端位于同一进程时,方法调用不会走跨进程的 transact 。当二者位于不同进程时,方法调用需要走 transact 过程,这个逻辑有 Stub 的内部代理类 Proxy 来完成。
    • 这个接口的核心实现就是它的内部类 Stub 和 Stub 的内部代理类 Proxy 。

Stub和Proxy类的内部方法和定义

  1. DESCRIPTOR
    Binder的唯一标识,一般用Binder的类名表示。
  2. asInterface(android.os.IBinder obj)
    将服务端的Binder对象转换为客户端所需的AIDL接口类型的对象,如果C/S位于同一进
    程,此方法返回就是服务端的Stub对象本身,否则返回的就是系统封装后的Stub.proxy对
    象。
  3. asBinder
    返回当前Binder对象。
  4. onTransact
    这个方法运行在服务端的Binder线程池中,由客户端发起跨进程请求时,远程请求会通过
    系统底层封装后交由此方法来处理。该方法的原型是
    java public Boolean onTransact(int code,Parcelable data,Parcelable reply,int flags)
    1. 服务端通过code确定客户端请求的目标方法是什么,
    2. 接着从data取出目标方法所需的参数,然后执行目标方法。
    3. 执行完毕后向reply写入返回值( 如果有返回值) 。
    4. 如果这个方法返回值为false,那么服务端的请求会失败,利用这个特性我们可以来做权限验证。
  5. Proxy#getBookList 和Proxy#addBook
    这两个方法运行在客户端,内部实现过程如下:
    1. 首先创建该方法所需要的输入型对象Parcel对象_data,输出型Parcel对象_reply和返回值对象List。
    2. 然后把该方法的参数信息写入_data( 如果有参数)
    3. 接着调用transact方法发起RPC( 远程过程调用) ,同时当前线程挂起
    4. 然后服务端的onTransact方法会被调用知道RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果,最后返回_reply中的数据。

AIDL文件不是必须的,之所以提供AIDL文件,是为了方便系统为我们生成IBookManager.java,但我们完全可以自己写一个。

linkToDeath和unlinkToDeath
如果服务端进程异常终止,我们到服务端的Binder连接断裂。但是,如果我们不知道Binder连接已经断裂,那么客户端功能会受影响。通过linkTODeath我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知。

  1. 声明一个 DeathRecipient 对象。 DeathRecipient 是一个接口,只有一个方法 binderDied ,当Binder死亡的时候,系统就会回调 binderDied 方法,然后我们就可以重新绑定远程服务。
    private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
    @Override
    public void binderDied(){
    if(mBookManager == null){
    return;
    }
    mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
    mBookManager = null;
    // TODO:这里重新绑定远程Service
    }
    }
  2. 在客户端绑定远程服务成功后,给binder设置死亡代理:
    mService = IBookManager.Stub.asInterface(binder);
    binder.linkToDeath(mDeathRecipient,0);
  3. 另外,可以通过Binder的 isBinderAlive 判断Binder是否死亡。

2.4 Android中的IPC方式

主要有以下方式:

  1. Intent中附加extras
  2. 共享文件
  3. Binder
  4. ContentProvider
  5. Socket

2.4.1 使用Bundle

四大组件中的三大组件( Activity、Service、Receiver) 都支持在Intent中传递 Bundle 数据。
Bundle实现了Parcelable接口,因此可以方便的在不同进程间传输。当我们在一个进程中启动了另一个进程的Activity、Service、Receiver,可以再Bundle中附加我们需要传输给远程进程的消息并通过Intent发送出去。被传输的数据必须能够被序列化。

2.4.2 使用文件共享

我们可以序列化一个对象到文件系统中的同时从另一个进程中恢复这个对象。

  1. 通过 ObjectOutputStream / ObjectInputStream 序列化一个对象到文件中,或者在另一个进程从文件中反序列这个对象。注意:反序列化得到的对象只是内容上和序列化之前的对象一样,本质是两个对象。
  2. 文件并发读写会导致读出的对象可能不是最新的,并发写的话那就更严重了 。所以文件共享方式适合对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读写问题。
  3. SharedPreferences 底层实现采用XML文件来存储键值对。系统对它的读/写有一定的缓存策略,即在内存中会有一份 SharedPreferences 文件的缓存,因此在多进程模式下,系统对它的读/写变得不可靠,面对高并发读/写时 SharedPreferences 有很大几率丢失数据,因此不建议在IPC中使用 SharedPreferences 。

2.4.3 使用Messenger

Messenger可以在不同进程间传递Message对象。是一种轻量级的IPC方案,底层实现是AIDL。它对AIDL进行了封装,使得我们可以更简便的进行IPC。



具体使用时,分为服务端和客户端:

  1. 服务端:创建一个Service来处理客户端请求,同时创建一个Handler并通过它来创建一个
    Messenger,然后再Service的onBind中返回Messenger对象底层的Binder即可。
    private final Messenger mMessenger = new Messenger (new xxxHandler());
  2. 客户端:绑定服务端的Sevice,利用服务端返回的IBinder对象来创建一个Messenger,通过这个Messenger就可以向服务端发送消息了,消息类型是 Message 。如果需要服务端响应,则需要创建一个Handler并通过它来创建一个Messenger( 和服务端一样) ,并通过 Message 的 replyTo 参数传递给服务端。服务端通过Message的 replyTo 参数就可以回应客户端了。

总而言之,就是客户端和服务端 拿到对方的Messenger来发送 Message 。只不过客户端通过bindService 而服务端通过 message.replyTo 来获得对方的Messenger。

Messenger中有一个 Hanlder 以串行的方式处理队列中的消息。不存在并发执行,因此我们不用考虑线程同步的问题。

2.4.4 使用AIDL

如果有大量的并发请求,使用Messenger就不太适合,同时如果需要跨进程调用服务端的方法,Messenger就无法做到了。这时我们可以使用AIDL。

流程如下:

  1. 服务端需要创建Service来监听客户端请求,然后创建一个AIDL文件,将暴露给客户端的接口在AIDL文件中声明,最后在Service中实现这个AIDL接口即可。
  2. 客户端首先绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。

AIDL支持的数据类型:

  1. 基本数据类型、String、CharSequence
  2. List:只支持ArrayList,里面的每个元素必须被AIDL支持
  3. Map:只支持HashMap,里面的每个元素必须被AIDL支持
  4. Parcelable
  5. 所有的AIDL接口本身也可以在AIDL文件中使用

自定义的Parcelable对象和AIDL对象,不管它们与当前的AIDL文件是否位于同一个包,都必须显式import进来。

如果AIDL文件中使用了自定义的Parcelable对象,就必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。

    package com.ryg.chapter_2.aidl;
    parcelable Book;

AIDL接口中的参数除了基本类型以外都必须表明方向in/out。AIDL接口文件中只支持方法,不支持声明静态常量。建议把所有和AIDL相关的类和文件放在同一个包中,方便管理。

    void addBook(in Book book);

AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接时,管理数据的集合直接采用 CopyOnWriteArrayList 来进行自动线程同步。类似的还有 ConcurrentHashMap 。

因为客户端的listener和服务端的listener不是同一个对象,所以 RecmoteCallbackList 是系统专门提供用于删除跨进程listener的接口,支持管理任意的AIDL接口,因为所有AIDL接口都继承自 IInterface 接口。

    public class RemoteCallbackList<E extends IInterface>

它内部通过一个Map接口来保存所有的AIDL回调,这个Map的key是 IBinder 类型,value是 Callback 类型。当客户端解除注册时,遍历服务端所有listener,找到和客户端listener具有相同Binder对象的服务端listenr并把它删掉。

==客户端RPC的时候线程会被挂起,由于被调用的方法运行在服务端的Binder线程池中,可能很耗时,不能在主线程中去调用服务端的方法。==

权限验证
默认情况下,我们的远程服务任何人都可以连接,我们必须加入权限验证功能,权限验证失败则无法调用服务中的方法。通常有两种验证方法:

  1. 在onBind中验证,验证不通过返回null
    验证方式比如permission验证,在AndroidManifest声明:
    <permission
    android:name="com.rgy.chapter_2.permisson.ACCESS_BOOK_SERVICE"
    android:protectionLevel="normal"/>
    Android自定义权限和使用权限
    public IBinder onBind(Intent intent){
    int check = checkCallingOrSelefPermission("com.ryq.chapter_2.permission.ACCESS_BOOK_SERVICE");
    if(check == PackageManager.PERMISSION_DENIED){
    return null;
    }
    return mBinder;
    }
    这种方法也适用于Messager。
  2. 在onTransact中验证,验证不通过返回false
    可以permission验证,还可以采用Uid和Pid验证。

2.4.5 使用ContentProvider

==ContentProvider是四大组件之一,天生就是用来进程间通信。和Messenger一样,其底层实现是用Binder。==

系统预置了许多ContentProvider,比如通讯录、日程表等。要RPC访问这些信息,只需要通过ContentResolver的query、update、insert和delete方法即可。

创建自定义的ContentProvider,只需继承ContentProvider类并实现 onCreate 、 query 、 update 、 insert 、 getType 六个抽象方法即可。getType用来返回一个Uri请求所对应的MIME类型,剩下四个方法对应于CRUD操作。这六个方法都运行在ContentProvider进程中,除了 onCreate 由系统回调并运行在主线程里,其他五个方法都由外界调用并运行在Binder线程池中。

ContentProvider是通过Uri来区分外界要访问的数据集合,例如外界访问ContentProvider中的表,我们需要为它们定义单独的Uri和Uri_Code。根据Uri_Code,我们就知道要访问哪个表了。

==query、update、insert、delete四大方法存在多线程并发访问,因此方法内部要做好线程同步。==若采用SQLite并且只有一个SQLiteDatabase,SQLiteDatabase内部已经做了同步处理。若是多个SQLiteDatabase或是采用List作为底层数据集,就必须做线程同步。

2.4.6 使用Socket

Socket也称为“套接字”,分为流式套接字和用户数据报套接字两种,分别对应于TCP和UDP协议。Socket可以实现计算机网络中的两个进程间的通信,当然也可以在本地实现进程间的通信。我们以一个跨进程的聊天程序来演示。

在远程Service建立一个TCP服务,然后在主界面中连接TCP服务。服务端Service监听本地端口,客户端连接指定的端口,建立连接成功后,拿到 Socket 对象就可以向服务端发送消息或者接受服务端发送的消息。
本例的客户端和服务端源代码

除了采用TCP套接字,也可以用UDP套接字。实际上socket不仅能实现进程间的通信,还可以实现设备间的通信(只要设备之间的IP地址互相可见)。

2.5 Binder连接池

前面提到AIDL的流程是:首先创建一个service和AIDL接口,接着创建一个类继承自AIDL接口中的Stub类并实现Stub中的抽象方法,客户端在Service的onBind方法中拿到这个类的对象,然后绑定这个service,建立连接后就可以通过这个Stub对象进行RPC。

那么如果项目庞大,有多个业务模块都需要使用AIDL进行IPC,随着AIDL数量的增加,我们不能无限制地增加Service,我们需要把所有AIDL放在同一个Service中去管理。


  • 服务端只有一个Service,把所有AIDL放在一个Service中,不同业务模块之间不能有耦合
  • 服务端提供一个 queryBinder 接口,这个接口能够根据业务模块的特征来返回响应的Binder对象给客户端
  • 不同的业务模块拿到所需的Binder对象就可以进行RPC了

BinderPool源码

2.6 选用合适的IPC方式

3 View的事件体系

本章介绍View的事件分发和滑动冲突问题的解决方案。

3.1 view的基础知识

View的位置参数、MotionEvent和TouchSlop对象、VelocityTracker、GestureDetector和Scroller对象。

3.1.1什么是view

View是Android中所有控件的基类,View的本身可以是单个空间,也可以是多个控件组成的一组控件,即ViewGroup,ViewGroup继承自View,其内部可以有子View,这样就形成了View树的结构。

3.1.2 View的位置参数

View的位置主要由它的四个顶点来决定,即它的四个属性:top、left、right、bottom,分别表示View左上角的坐标点( top,left) 以及右下角的坐标点( right,bottom) 。



同时,我们可以得到View的大小:

width = right - left
height = bottom - top

而这四个参数可以由以下方式获取:

Left = getLeft();
Right = getRight();
Top = getTop();
Bottom = getBottom();

Android3.0后,View增加了x、y、translationX和translationY这几个参数。其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于容器的偏移量。他们之间的换算关系如下:

x = left + translationX;
y = top + translationY;

top,left表示原始左上角坐标,而x,y表示变化后的左上角坐标。在View没有平移时,x=left,y=top。==View平移的过程中,top和left不会改变,改变的是x、y、translationX和translationY。==

3.1.3 MotionEvent和TouchSlop

MotionEvent
事件类型

  • ACTION_DOWN 手指刚接触屏幕
  • ACTION_MOVE 手指在屏幕上移动
  • ACTION_UP 手指从屏幕上松开

点击事件类型

  • 点击屏幕后离开松开,事件序列为DOWN->UP
  • 点击屏幕滑动一会再松开,事件序列为DOWN->MOVE->...->MOVE->UP

通过MotionEven对象我们可以得到事件发生的x和y坐标,我们可以通过getX/getY和getRawX/getRawY得到。它们的区别是:getX/getY返回的是相对于当前View左上角的x和y坐标,getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。

TouchSloup
TouchSloup是系统所能识别出的被认为是滑动的最小距离,这是一个常量,与设备有关,可通过以下方法获得:

ViewConfiguration.get(getContext()).getScaledTouchSloup().

当我们处理滑动时,比如滑动距离小于这个值,我们就可以过滤这个事件(系统会默认过滤),从而有更好的用户体验。

3.1.4 VelocityTracker、GestureDetector和Scroller

VelocityTracker
速度追踪,用于追踪手指在滑动过程中的速度,包括水平放向速度和竖直方向速度。使用方法:

  • 在View的onTouchEvent方法中追踪当前单击事件的速度
    VelocityRracker velocityTracker = VelocityTracker.obtain();
    velocityTracker.addMovement(event);
  • 计算速度,获得水平速度和竖直速度
    velocityTracker.computeCurrentVelocity(1000);
    int xVelocity = (int)velocityTracker.getXVelocity();
    int yVelocity = (int)velocityTracker.getYVelocity();
    注意,获取速度之前必须先计算速度,即调用computeCurrentVelocity方法,这里指的速度是指一段时间内手指滑过的像素数,1000指的是1000毫秒,得到的是1000毫秒内滑过的像素数。速度可正可负:速度 = ( 终点位置 - 起点位置) / 时间段
  • 最后,当不需要使用的时候,需要调用clear()方法重置并回收内存:
    velocityTracker.clear();
    velocityTracker.recycle();

GestureDetector
手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。使用方法:

  • 创建一个GestureDetector对象并实现OnGestureListener接口,根据需要,也可实现OnDoubleTapListener接口从而监听双击行为:
    GestureDetector mGestureDetector = new GestureDetector(this);
    //解决长按屏幕后无法拖动的现象
    mGestureDetector.setIsLongpressEnabled(false);
  • 在目标View的OnTouchEvent方法中添加以下实现:
    boolean consume = mGestureDetector.onTouchEvent(event);
    return consume;
  • 实现OnGestureListener和OnDoubleTapListener接口中的方法



    其中常用的方法有:onSingleTapUp(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)和onDoubleTap( 双击)。建议:如果只是监听滑动相关的,可以自己在onTouchEvent中实现,如果要监听双击这种行为,那么就使用GestureDetector。

Scroller
弹性滑动对象,用于实现View的弹性滑动。其本身无法让View弹性滑动,需要和View的computeScroll方法配合使用才能完成这个功能。使用方法:

Scroller scroller = new Scroller(mContext);
//缓慢移动到指定位置
private void smoothScrollTo(int destX,int destY){
    int scrollX = getScrollX();
    int delta = destX - scrollX;
    //1000ms内滑向destX,效果就是慢慢滑动
    mScroller.startScroll(scrollX,0,delta,0,1000);
    invalidata();
} 
@Override
public void computeScroll(){
    if(mScroller.computeScrollOffset()){
    scrollTo(mScroller.getCurrX,mScroller.getCurrY());
    postInvalidate();
    }
}

3.2 View的滑动

三种方式实现View滑动

3.2.1 使用scrollTo/scrollBy

scrollBy实际调用了scrollTo,它实现了基于当前位置的相对滑动,而scrollTo则实现了绝对滑动。

==scrollTo和scrollBy只能改变View的内容位置而不能改变View在布局中的位置。滑动偏移量mScrollX和mScrollY的正负与实际滑动方向相反,即从左向右滑动,mScrollX为负值,从上往下滑动mScrollY为负值。==

3.2.2 使用动画

使用动画移动View,主要是操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画,如果使用属性动画,为了能够兼容3.0以下的版本,需要采用开源动画库nineolddandroids。 如使用属性动画:(View在100ms内向右移动100像素)

    ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

3.2.3 改变布局属性

通过改变布局属性来移动View,即改变LayoutParams。

3.2.4 各种滑动方式的对比

  • scrollTo/scrollBy:操作简单,适合对View内容的滑动;
  • 动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;
  • 改变布局参数:操作稍微复杂,适用于有交互的View。

3.3 弹性滑动

3.3.1 使用Scroller

使用Scroller实现弹性滑动的典型使用方法如下:

Scroller scroller = new Scroller(mContext);
//缓慢移动到指定位置
private void smoothScrollTo(int destX,int dextY){
    int scrollX = getScrollX();
    int deltaX = destX - scrollX;
    //1000ms内滑向destX,效果就是缓慢滑动
    mScroller.startSscroll(scrollX,0,deltaX,0,1000);
    invalidate();
} 
@override
public void computeScroll(){
    if(mScroller.computeScrollOffset()){
    scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
    postInvalidate();
    }
}

从上面代码可以知道,我们首先会构造一个Scroller对象,并调用他的startScroll方法,该方法并没有让view实现滑动,只是把参数保存下来,我们来看看startScroll方法的实现就知道了:

public void startScroll(int startX,int startY,int dx,int dy,int duration){
    mMode = SCROLL_MODE;
    mFinished = false;
    mDuration = duration;
    mStartTime = AnimationUtils.currentAminationTimeMills();
    mStartX = startX;
    mStartY = startY;
    mFinalX = startX + dx;
    mFinalY = startY + dy;
    mDeltaX = dx;
    mDeltaY = dy;
    mDurationReciprocal = 1.0f / (float)mDuration;
}

可以知道,startScroll方法的几个参数的含义,startX和startY表示滑动的起点,dx和dy表示的是滑动的距离,而duration表示的是滑动时间,注意,这里的滑动指的是View内容的滑动,在startScroll方法被调用后,马上调用invalidate方法,这是滑动的开始,invalidate方法会导致View的重绘,在View的draw方法中调用computeScroll方法,computeScroll又会去向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动,接着又调用postInvalidate方法进行第二次重绘,一直循环,直到computeScrollOffset()方法返回值为false才结束整个滑动过程。 我们可以看看computeScrollOffset方法是如何获得当前的scrollX和scrollY的:

public boolean computeScrollOffset(){
    ...
    int timePassed = (int)(AnimationUtils.currentAnimationTimeMills() - mStartTime);
    if(timePassed < mDuration){
        switch(mMode){
        case SCROLL_MODE:
        final float x = mInterpolator.getInterpolation(timePassed * mDuratio
        nReciprocal);
        mCurrX = mStartX + Math.round(x * mDeltaX);
        mCurrY = mStartY + Math.round(y * mDeltaY);
        break;
        ...
        }
    } 
    return true;
}

到这里我们就基本明白了,computeScroll向Scroller获取当前的scrollX和scrollY其实是通过计算时间流逝的百分比来获得的,每一次重绘距滑动起始时间会有一个时间间距,通过这个时间间距Scroller就可以得到View当前的滑动位置,然后就可以通过scrollTo方法来完成View的滑动了。

3.3.2 通过动画

动画本身就是一种渐近的过程,因此通过动画来实现的滑动本身就具有弹性。实现也很简单:

ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start()
;    
//当然,我们也可以利用动画来模仿Scroller实现View弹性滑动的过程:
final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
    @override
    public void onAnimationUpdate(ValueAnimator animator){
    float fraction = animator.getAnimatedFraction();
    mButton1.scrollTo(startX + (int) (deltaX * fraction) , 0);
    }
});
animator.start();

上面的动画本质上是没有作用于任何对象上的,他只是在1000ms内完成了整个动画过程,利用这个特性,我们就可以在动画的每一帧到来时获取动画完成的比例,根据比例计算出View所滑动的距离。采用这种方法也可以实现其他动画效果,我们可以在onAnimationUpdate方法中加入自定义操作。

3.3.3 使用延时策略

延时策略的核心思想是通过发送一系列延时信息从而达到一种渐近式的效果,具体可以通过Hander和View的postDelayed方法,也可以使用线程的sleep方法。 下面以Handler为例:

private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELATED_TIME = 33;
private int mCount = 0;
@suppressLint("HandlerLeak")
private Handler handler = new handler(){
    public void handleMessage(Message msg){
    switch(msg.what){
        case MESSAGE_SCROLL_TO:
        mCount ++ ;
        if (mCount <= FRAME_COUNT){
            float fraction = mCount / (float) FRAME_COUNT;
            int scrollX = (int) (fraction * 100);
            mButton1.scrollTo(scrollX,0);
            mHandelr.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO , DELAYED_TIME);
            } 
        break;
        default : break;
        }
    }
}

3.4 View的事件分发机制

3.4.1 点击事件的传递规则

点击事件是MotionEvent。首先我们先看看下面一段伪代码,通过它我们可以理解到点击事件的传递规则:

public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvnet(ev){
    consume = onTouchEvent(ev);
} else {
    consume = child.dispatchTouchEnvet(ev);
} 
return consume;
}

上面代码主要涉及到以下三个方法:

  • public boolean dispatchTouchEvent(MotionEvent ev);
    这个方法用来进行事件的分发。如果事件传递给当前view,则调用此方法。返回结果表示是否消耗此事件,受onTouchEvent和下级View的dispatchTouchEvent方法影响。
  • public boolean onInterceptTouchEvent(MotionEvent ev);
    这个方法用来判断是否拦截事件。在dispatchTouchEvent方法中调用。返回结果表示是否拦截。
  • public boolean onTouchEvent(MotionEvent ev);
    这个方法用来处理点击事件。在dispatchTouchEvent方法中调用,返回结果表示是否消耗事件。如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。

点击事件的传递规则:对于一个根ViewGroup,点击事件产生后,首先会传递给他,这时候就会调用他的dispatchTouchEvent方法,如果Viewgroup的onInterceptTouchEvent方法返回true表示他要拦截事件,接下来事件就会交给ViewGroup处理,调用ViewGroup的onTouchEvent方法;如果ViewGroup的onInteceptTouchEvent方法返回值为false,表示ViewGroup不拦截该事件,这时事件就传递给他的子View,接下来子View的dispatchTouchEvent方法,如此反复直到事件被最终处理。

当一个View需要处理事件时,如果它设置了OnTouchListener,那么onTouch方法会被调用,如果onTouch返回false,则当前View的onTouchEvent方法会被调用,返回true则不会被调用,同时,在onTouchEvent方法中如果设置了OnClickListener,那么他的onClick方法会被调用。==由此可见处理事件时的优先级关系: onTouchListener > onTouchEvent >onClickListener==

关于事件传递的机制,这里给出一些结论:

  1. 一个事件系列以down事件开始,中间包含数量不定的move事件,最终以up事件结束。
  2. 正常情况下,一个事件序列只能由一个View拦截并消耗。
  3. 某个View拦截了事件后,该事件序列只能由它去处理,并且它的onInterceptTouchEvent
    不会再被调用。
  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件( onTouchEvnet返回false) ,那么同一事件序列中的其他事件都不会交给他处理,并且事件将重新交由他的父元素去处理,即父元素的onTouchEvent被调用。
  5. 如果View不消耗ACTION_DOWN以外的其他事件,那么这个事件将会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终消失的点击事件会传递给Activity去处理。
  6. ViewGroup默认不拦截任何事件。
  7. View没有onInterceptTouchEvent方法,一旦事件传递给它,它的onTouchEvent方法会被调用。
  8. View的onTouchEvent默认消耗事件,除非他是不可点击的( clickable和longClickable同时为false) 。View的longClickable属性默认false,clickable默认属性分情况(如TextView为false,button为true)。
  9. View的enable属性不影响onTouchEvent的默认返回值。
  10. onClick会发生的前提是当前View是可点击的,并且收到了down和up事件。
  11. 事件传递过程总是由外向内的,即事件总是先传递给父元素,然后由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的分发过程,但是ACTION_DOWN事件除外。

3.4.2 事件分发的源码解析

3.5 滑动冲突

在界面中,只要内外两层同时可以滑动,这个时候就会产生滑动冲突。滑动冲突的解决有固定的方法。

3.5.1 常见的滑动冲突场景

  1. 外部滑动和内部滑动方向不一致;
    比如viewpager和listview嵌套,但这种情况下viewpager自身已经对滑动冲突进行了处理。
  2. 外部滑动方向和内部滑动方向一致;
  3. 上面两种情况的嵌套。
    只要解决1和2即可。

3.5.2 滑动冲突的处理规则

对于场景一,处理的规则是:当用户左右( 上下) 滑动时,需要让外部的View拦截点击事件,当用户上下( 左右) 滑动的时候,需要让内部的View拦截点击事件。根据滑动的方向判断谁来拦截事件。

对于场景二,由于滑动方向一致,这时候只能在业务上找到突破点,根据业务需求,规定什么时候让外部View拦截事件,什么时候由内部View拦截事件。

场景三的情况相对比较复杂,同样根据需求在业务上找到突破点。

3.5.3 滑动冲突的解决方式

外部拦截法
所谓外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。下面是伪代码:

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 = flase;
    } 
    break;
    } 
case MotionEvent.ACTION_UP:
    intercepted = false;
    break;
default : 
    break;
} 
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;

针对不同冲突,只需修改父容器需要当前事件的条件即可。其他不需修改也不能修改。

  • ACTION_DOWN:必须返回false。因为如果返回true,后续事件都会被拦截,无法传递给子View。
  • ACTION_MOVE:根据需要决定是否拦截
  • ACTION_UP:必须返回false。如果拦截,那么子View无法接受up事件,无法完成click操作。而如果是父容器需要该事件,那么在ACTION_MOVE时已经进行了拦截,根据上一节的结论3,ACTION_UP不会经过onInterceptTouchEvent方法,直接交给父容器处理。

内部拦截法
内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗,否则就交由父容器进行处理。这种方法与Android事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。下面是伪代码:

public boolean dispatchTouchEvent ( MotionEvent event ) {
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;
    }
}

优化滑动体验:

mScroller.abortAnimation();

外部拦截法实例:HorizontalScrollViewEx

4 View的工作原理

主要内容

  • View的工作原理
  • 自定义View的实现方式
  • 自定义View的底层工作原理,比如View的测量流程、布局流程、绘制流程
  • View常见的回调方法,比如构造方法、onAttach.onVisibilityChanged/onDetach等

4.1 初识ViewRoot和DecorView

ViewRoot的实现是 ViewRootImpl 类,是连接WindowManager和DecorView的纽带,View的三大流程( mearsure、layout、draw) 均是通过ViewRoot来完成。当Activity对象被创建完毕后,会将DecorView添加到Window中,同时创建 ViewRootImpl 对象,并将ViewRootImpl 对象和DecorView建立连接,源码如下:

root = new ViewRootImpl(view.getContext(),display);
root.setView(view,wparams, panelParentView);

View的绘制流程是从ViewRoot的performTraversals开始的


  1. measure用来测量View的宽高
  2. layout来确定View在父容器中的位置
  3. draw负责将View绘制在屏幕上

performTraversals会依次调用 performMeasure 、 performLayout 和performDraw 三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程。其中 performMeasure 中会调用 measure 方法,在 measure 方法中又会调用 onMeasure 方法,在 onMeasure 方法中则会对所有子元素进行measure过程,这样就完成了一次measure过程;子元素会重复父容器的measure过程,如此反复完成了整个View数的遍历。另外两个过程同理。

  • Measure完成后, 可以通过getMeasuredWidth 、getMeasureHeight 方法来获取View测量后的宽/高。特殊情况下,测量的宽高不等于最终的宽高,详见后面。
  • Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成后可通过 getTop 、 getBotton 、 getLeft 和 getRight 拿到View的四个定点坐标。

DecorView作为顶级View,其实是一个 FrameLayout ,它包含一个竖直方向的 LinearLayout ,这个 LinearLayout 分为标题栏和内容栏两个部分。


在Activity通过setContextView所设置的布局文件其实就是被加载到内容栏之中的。这个内容栏的id是 R.android.id.content ,通过

 ViewGroup content = findViewById(R.android.id.content);

可以得到这个contentView。View层的事件都是先经过DecorView,然后才传递到子View。

4.2 理解MeasureSpec

MeasureSpec决定了一个View的尺寸规格。但是父容器会影响View的MeasureSpec的创建过程。系统将View的 LayoutParams 根据父容器所施加的规则转换成对应的MeasureSpec,然后根据这个MeasureSpec来测量出View的宽高。

4.2.1 MeasureSpec

MeasureSpec代表一个32位int值,高2位代表SpecMode( 测量模式) ,低30位代表SpecSize( 在某个测量模式下的规格大小) 。
SpecMode有三种:

  • UNSPECIFIED :父容器不对View进行任何限制,要多大给多大,一般用于系统内部
  • EXACTLY:父容器检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,对应LayoutParams中的 match_parent 和具体数值这两种模式
  • AT_MOST :对应View的默认大小,不同View实现不同,View的大小不能大于父容器的SpecSize,对应 LayoutParams 中的 wrap_content

4.2.2 MeasureSpec和LayoutParams的对应关系

对于DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同确定。而View的MeasureSpec由父容器的MeasureSpec和自身的LayoutParams共同决定。

View的measure过程由ViewGroup传递而来,参考ViewGroup的 measureChildWithMargins 方法,通过调用子元素的 getChildMeasureSpec 方法来得到子元素的MeasureSpec,再调用子元素的 measure 方法。



parentSize是指父容器中目前可使用的大小。

  1. 当View采用固定宽/高时( 即设置固定的dp/px) ,不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,并且大小遵循我们设置的值。
  2. 当View的宽/高是 match_parent 时,View的MeasureSpec都是EXACTLY模式并且其大小等于父容器的剩余空间。
  3. 当View的宽/高是 wrap_content 时,View的MeasureSpec都是AT_MOST模式并且其大小不能超过父容器的剩余空间。
  4. 父容器的UNSPECIFIED模式,一般用于系统内部多次Measure时,表示一种测量的状态,一般来说我们不需要关注此模式。

4.3 View的工作流程

4.3.1 measure过程

View的measure过程

直接继承View的自定义控件需要重写 onMeasure 方法并设置 wrap_content ( 即specMode是 AT_MOST 模式) 时的自身大小,否则在布局中使用 wrap_content 相当于使用 match_parent 。对于非 wrap_content 的情形,我们沿用系统的测量值即可。

      @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);
          // 在 MeasureSpec.AT_MOST 模式下,给定一个默认值mWidth,mHeight。默认宽高灵活指定
          //参考TextView、ImageView的处理方式
          //其他情况下沿用系统测量规则即可
        if (widthSpecMode == MeasureSpec.AT_MOST
                && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWith, mHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(mWith, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, mHeight);
        }
    }

ViewGroup的measure过程

ViewGroup是一个抽象类,没有重写View的 onMeasure 方法,但是它提供了一个 measureChildren 方法。这是因为不同的ViewGroup子类有不同的布局特性,导致他们的测量细节各不相同,比如 LinearLayout 和 RelativeLayout ,因此ViewGroup没办法同一实现 onMeasure方法。

measureChildren方法的流程:

  1. 取出子View的 LayoutParams
  2. 通过 getChildMeasureSpec 方法来创建子元素的 MeasureSpec
  3. 将 MeasureSpec 直接传递给View的measure方法来进行测量

通过LinearLayout的onMeasure方法里来分析ViewGroup的measure过程:

  1. LinearLayout在布局中如果使用match_parent或者具体数值,测量过程就和View一致,即高度为specSize
  2. LinearLayout在布局中如果使用wrap_content,那么它的高度就是所有子元素所占用的高度总和,但不超过它的父容器的剩余空间
  3. LinearLayout的的最终高度同时也把竖直方向的padding考虑在内

View的measure过程是三大流程中最复杂的一个,measure完成以后,通过 getMeasuredWidth/Height 方法就可以正确获取到View的测量后宽/高。在某些情况下,系统可能需要多次measure才能确定最终的测量宽/高,所以在onMeasure中拿到的宽/高很可能不是准确的。

==如果我们想要在Activity启动的时候就获取一个View的宽高,怎么操作呢?==因为View的measure过程和Activity的生命周期并不是同步执行,无法保证在Activity的 onCreate、onStart、onResume 时某个View就已经测量完毕。所以有以下四种方式来获取View的宽高:

  1. Activity/View#onWindowFocusChanged
    onWindowFocusChanged这个方法的含义是:VieW已经初始化完毕了,宽高已经准备好了,需要注意:它会被调用多次,当Activity的窗口得到焦点和失去焦点均会被调用。
  2. view.post(runnable)
    通过post将一个runnable投递到消息队列的尾部,当Looper调用此runnable的时候,View也初始化好了。
  3. ViewTreeObserver
    使用 ViewTreeObserver 的众多回调可以完成这个功能,比如OnGlobalLayoutListener 这个接口,当View树的状态发送改变或View树内部的View的可见性发生改变时,onGlobalLayout 方法会被回调,这是获取View宽高的好时机。需要注意的是,伴随着View树状态的改变, onGlobalLayout 会被回调多次。
  4. view.measure(int widthMeasureSpec,int heightMeasureSpec)
    手动对view进行measure。需要根据View的layoutParams分情况处理:
    • match_parent:
      无法measure出具体的宽高,因为不知道父容器的剩余空间,无法测量出View的大小
    • 具体的数值( dp/px):
int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec,heightMeasureSpec);
  • wrap_content:
int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
// View的尺寸使用30位二进制表示,最大值30个1,在AT_MOST模式下,
// 我们用View理论上能支持的最大值去构造MeasureSpec是合理的
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1<<30)-1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec,heightMeasureSpec);

4.3.2 layout过程

layout的作用是ViewGroup用来确定子View的位置,当ViewGroup的位置被确定后,它会在onLayout中遍历所有的子View并调用其layout方法,在 layout 方法中, onLayout 方法又会被调用。

View的 layout 方法确定本身的位置,源码流程如下:

  1. setFrame 确定View的四个顶点位置,即确定了View在父容器中的位置
  2. 调用 onLayout 方法,确定所有子View的位置,和onMeasure一样,onLayout的具体实现和布局有关,因此View和ViewGroup均没有真正实现 onLayout 方法。

以LinearLayout的 onLayout 方法为例:

  1. 遍历所有子View并调用 setChildFrame 方法来为子元素指定对应的位置
  2. setChildFrame 方法实际上调用了子View的 layout 方法,形成了递归

==View的测量宽高和最终宽高的区别:==
在View的默认实现中,View的测量宽高和最终宽高相等,只不过测量宽高形成于measure过程,最终宽高形成于layout过程。但重写view的layout方法可以使他们不相等。

4.3.3 draw过程

View的绘制过程遵循如下几步:

  1. 绘制背景 drawBackground(canvas)
  2. 绘制自己 onDraw
  3. 绘制children dispatchDraw 遍历所有子View的 draw 方法
  4. 绘制装饰 onDrawScrollBars

ViewGroup会默认启用 setWillNotDraw 为ture,导致系统不会去执行 onDraw ,所以自定义ViewGroup需要通过onDraw来绘制内容时,必须显式的关闭 WILL_NOT_DRAW 这个优化标记位,即调用 setWillNotDraw(false);

4.4 自定义View

4.4.1 自定义View的分类

继承View 重写onDraw方法
通过 onDraw 方法来实现一些不规则的效果,这种效果不方便通过布局的组合方式来达到。这种方式需要自己支持 wrap_content ,并且padding也要去进行处理。

继承ViewGroup派生特殊的layout
实现自定义的布局方式,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子View的测量和布局过程。

继承特定的View子类( 如TextView、Button)
扩展某种已有的控件的功能,比较简单,不需要自己去管理 wrap_content 和padding。

** 继承特定的ViewGroup子类( 如LinearLayout)**
比较常见,实现几种view组合一起的效果。与方法二的差别是方法二更接近底层实现。

4.4.2 自定义View须知

  1. 直接继承View或ViewGroup的控件, 需要在onmeasure中对wrap_content做特殊处理。指定wrap_content模式下的默认宽/高。
  2. 直接继承View的控件,如果不在draw方法中处理padding,那么padding属性就无法起作用。直接继承ViewGroup的控件也需要在onMeasure和onLayout中考虑padding和子元素margin的影响,不然padding和子元素的margin无效。
  3. 尽量不要用在View中使用Handler,因为没必要。View内部提供了post系列的方法,完全可以替代Handler的作用。
  4. View中有线程和动画,需要在View的onDetachedFromWindow中停止。当View不可见时,也需要停止线程和动画,否则可能造成内存泄漏。
  5. View带有滑动嵌套情形时,需要处理好滑动冲突

4.4.3 自定义View实例

自定义属性设置方法:

  1. 在values目录下创建自定义属性的XML,如attrs.xml。
<?xml version="1.0" encoding="utf-8"?>
        <resources>
            <declare-styleable name="CircleView">
                <attr name="circle_color" format="color" />
            </declare-styleable>
        </resources>
  1. 在View的构造方法中解析自定义属性的值并做相应处理,这里我们解析circle_color。
        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();
        }
  1. 在布局文件中使用自定义属性
        <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#ffffff"
        android:orientation="vertical" >
        <com.ryg.chapter_4.ui.CircleView
            android:id="@+id/circleView1"
            android:layout_width="wrap_content"
            android:layout_height="100dp"
            android:layout_margin="20dp"
            android:background="#000000"
            android:padding="20dp"
            app:circle_color="@color/light_green" />
        </LinearLayout>

Android中的命名空间

  • 继承ViewGroup派生特殊的layout:HorizontalScrollViewEx
    onMeasure方法中,首先判断是否有子元素,没有的话根据LayoutParams中的宽高做相应处理。然后判断宽高是不是wrap_content,如果宽是,那么HorizontalScrollViewEx的宽就是所有所有子元素的宽度之和。如果高是wrap_content,HorizontalScrollViewEx的高度就是第一个子元素的高度。同时要处理padding和margin。
    onLayout方法中,在放置子元素时候也要考虑padding和margin。

4.4.4 自定义View的思想

  • 掌握基本功,比如View的弹性滑动、滑动冲突、绘制原理等
  • 面对新的自定义View时,对其分类并选择合适的实现思路。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 第一章:Activity生命周期和启动模式 Activity关闭时会调用onPause()和onStop(),如果...
    loneyzhou阅读 873评论 0 2
  • 一、Activity的生命周期和启动模式1.Activity的生命周期全面分析典型情况下的生命周期:onCreat...
    为梦想战斗阅读 274评论 0 0
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,498评论 25 707
  • 1.盒模型包括哪些属性? 盒模型包括:边距、边框、填充和实际内容; margin:清除边框区域,无背景颜色,是完全...
    璐璐熙可阅读 213评论 0 1
  • 听,窗外传来一串富有节奏感的声音,是什么呢?哦,原来是冰雪消融的声音啊! 珍惜现在,说不定等一会儿你醒来后会发现...
    陨_星阅读 207评论 0 0