0%

1.github.io无法打开

今天发现自己的博客hezd.github.io无法打开,打开VPN代理必须设置全局代理才能打开,这种体验十分槽糕,为了解决这个问题,查阅了很多资料最终解决。

2.解决办法

控制面板>网络连接和Internet>更改适配器设置,然后找到自己连接无线网络,右键属性>Internet协议版本4,双击打开设置DNS服务器

有两种方法

1.只设置首选DNS服务器为114.114.114.114

2.设置首选DNS服务器为223.5.5.5,备用DNS服务器223.6.6.6

我是用第二种方法测试成功打开我的博客https://hezd.github.io/

参考

https://segmentfault.com/a/1190000039848385

1.什么是gRPC

​ gRPC是一个现代开源高性能的远程调用(RPC,remote procedure call)框架,能在任何环境中运行。

使用Protocol Buffers作为接口定义语言(IDL,Interface Definition Language)和底层消息交换格式

2.什么是Protocol Buffers

​ Protocol Buffers提供了一种结构化序列化数据的机制,它像Json除了更快更小并且生成本地语言绑定。

Protocol Buffers是定义语言(DL)的组合(在proto文件中创建),通过proto编译器生成序列化的数据接口,用于本地序列化或网络传输。

3.Protocol Buffers定义语法

protocol buffers使用需要先定义一个proto文件进行数据结构定义,protocol buffers支持数据类型包括常用的原始数据类型,还有以下几个常用类型:

  • message 消息类型

  • enum 枚举类型

  • oneof 限定类型,当消息有多个可选字段最多只能同时设置一个时使用

  • map 映射表类型,key-value映射关系数据结构

下面通过一个简单例子来说明:

//helloworld.proto(文件名)
//1.指定使用proto3语法
syntax = "proto3";
//option 是指文件选项,对于proto编译器编译会产生影响
//2.是否生成多个类文件(对于顶级service,message类型),如果false,全部生成在一个类文件中
option java_multiple_files = true;
//3.生成类文件所属包名
option java_package = "io.grpc.examples.helloworld";
//4.生成java包装类的类名,如果不指定默认以proto文件名驼峰形式(proto文件会生成一个包装类)
option java_outer_classname = "HelloWorldProto";
//5.生成Object-C类的前缀
option objc_class_prefix = "HLW";
//6.声明包名,避免消息类型名称冲突
package helloworld;

// 7.用service声明RPC接口
service Greeter {
// 8.用rpc声明rpc方法,HelloRequest方法参数,HelloReply返回类型
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// 9.声明消息类型HelloRequest
message HelloRequest {
string name = 1;
}

// 10.声明消息类型HelloReply
message HelloReply {
// 11.声明字段 message类型为字符串,指定字段编码为1
string message = 1;
}

4.android中使用gRPC

开发环境:

windows10

Android Studio Bumblebee | 2021.1.1 Patch 2

Gradle 7.1.2

JDK 11

以gRPC客户端为例

4.1引入protobuf插件

工程顶层build.gradle中添加

id 'com.google.protobuf' version '0.8.18' apply false
4.2proto脚本配置

工程子module的build.gradle中进行配置

plugins {
//...
// 使用protobuf
id 'com.google.protobuf'
}
android {
//...
}
protobuf {
protoc { artifact = 'com.google.protobuf:protoc:3.19.2' }
plugins {
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.45.0' // CURRENT_GRPC_VERSION
}
}
generateProtoTasks {
all().each { task ->
task.builtins {
java { option 'lite' }
}
task.plugins {
grpc { // Options added to --grpc_out
option 'lite' }
}
}
}
}

dependencies {
//grpc dependencies
// You need to build grpc-java to obtain these libraries below.
implementation 'io.grpc:grpc-okhttp:1.44.1' // CURRENT_GRPC_VERSION
implementation 'org.apache.tomcat:annotations-api:6.0.53'
implementation 'io.grpc:grpc-protobuf-lite:1.45.0'
implementation 'io.grpc:grpc-stub:1.45.0'
}

项目build之后会自动生成gRPC相关的类文件

具体实现可参考gRPC for android和文中的示例程序gRPCSample

参考:

https://grpc.io/

https://developers.google.com/protocol-buffers/docs/overview

我们在Android开发中为了更好的保持代码的可测试、可维护和可扩展性,产生了很多架构模式,例如MVC、MVP、MVVM等等,下面我们来看看这些架构模式

MVC

示意图:

image

M:Model数据模型,包含数据以及负责数据存取

V:View视图,xml布局以及自定义view

C:Controller控制器(Activity/Fragment),负责具体业务逻辑

在最早android开发初期,我们会将业务逻辑和数据都放到Activity中处理,但是并没有做分层处理,Activity/Fragment作为Controller即负责业务逻辑又处理UI更新,因此随着迭代开发Controller会急速膨胀,并且不利于测试、维护和扩展

MVP

为了解决MVC的痛点,MVP架构模式开始被广泛使用,示意图:

image

M:类比MVC的Model

V:View视图,包含Activity/Fragment,xml 布局

P:Presenter中间人角色,持有View和Model,负责业务逻辑以及两者之间的通信,使M和V实现了解耦

MVP解决了MVC耦合性强,不利于测试、维护和扩展的问题,但是MVP也有自己的问题。首先,P持有V的引用如果使用不当容易导致泄漏,在就是P和V通常是1:1比例存在,随着业务复杂度提升也使P会急速膨胀。另外虽然MVP对M和V进行解耦但是P和V还是强耦合的

MVVM

为了解决MVP存在的问题,MVVM被广泛使用,示意图:

image

M:Model类比MVC/MVP

V:View类比MVC/MVP

VM:ViewModel类比MVP的Presenter,负责Model和View交互,但它不持有View而是通过观察者模式进行回调通知(数据绑定)

MVVM的ViewModel不持有View解决了MVP中V和P强耦合的问题,一个Activity可以持有多个ViewModel,多个Activity也可以共用一个ViewModel提高了代码的复用性,减少了代码膨胀问题,MVP虽然也可以做类似的处理但是因为V和P的强耦合性会带来复杂度和维护成本几何数的提升所以通常MVP中几乎没人这么干。

注意:

MVVM的ViewMode和Android的ViewModel不是一个概念,Android的ViewMode是一个数据持有类在设备配置改变例如屏幕旋转时数据不会丢失

MVVM最早是微软提出的双向绑定也是它的特性,Android也推出了databinding来实现双向绑定,但是广大开发者并不喜欢因此目前流行的android MVVM架构是通过LiveData实现的单向绑定

通用架构原则

android官网介绍了通用架构原则有两点

  • 分离关注点
  • 通过数据模型驱动页面

分离关注点:

​ 简单说就是分层每一层遵循单一职责原则,通过分离关注点可提高代码的可测试性、维护性和扩展性

数据驱动页面:

​ 通过数据驱动页面(最好是持久性数据),独立于页面元素和其他组件,与生命周期没有关联,更便于测试、更稳定可靠

总结

架构模式从被提出到前后端各自演变过程中出现了很多变种,各个版本是否正统也存在一些争议,本文讨论是基于Android端的架构实现,google在推荐架构中也规避相关架构概念名词也是为了避免给大家带来误解和争议,在明白各个架构概念后,互相讨论时能明白对方在说什么就OK了,不用刻意钻牛角尖。

参考:

自定义ViewGroup

我们在开发过程中最多需求是自定义ViewGroup,本文就以自定ViewGroup实现流布局为例来讲解自定义ViewGroup。

自定义ViewGroup通常是自定义测量和布局规则,在《View测量和布局》文章中已介绍了测量和布局原理,所以重写的onMeasure和onLayout就是自定义ViewGroup的核心方法。

案例分析

下面我们就通过ViewGroup来一步步实现流布局,流布局测量和布局是对内边距,换行,子View间距的综合计算结果,所以相对还是有点复杂的,针对相对复杂的自定义View我们可以采用由简到繁的方式,这样可以将复杂的问题简单化,不会感到无从下手,比如流布局我们可以先不考虑内边距,子View间距,换行,只考虑一行的情况是不是简单很多了呢,然后逐步完善。

代码实现

我们分三步来实现以简化难度,单行、多行、间距。

单行

首先自定义FLowLayout并重写onMeasure和onLayout

/**
*
*@author hezd
*Create on 2021/12/24 17:08
*/
class FlowLayout(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 因为我们要自定义测量规则所以可以将super方法注释掉
// super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {

}
}

首先需要测量,ViewGroup测量就是遍历测量子View根据测量尺寸来确定ViewGroup的测量尺寸,子View如何测量尺寸呢ViewGrop提供了MeasureChildWithMargins

protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

widthUsed是已被使用宽度,heightUsed是已被使用的高度,这两个参数是为了计算可用空间并最终调用getChildMeasureSpec确定子View的MeasureSpec约束,getChildMeasureSpec已在《View测量和布局》中做过解析此处不在赘述,不考虑内边距情况下widthUse和heightUsed为0,单行情况下取高度最高的子View的height作为FlowLayout的height,宽度就先已子View宽度之和作为ViewGroup的测量宽度。

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// Flowlayout测量尺寸
var finalHeight = 0
var finalWidth = 0
// 单行中高度最高的childHeight
var lineMaxHeight = 0

for(child in children){
// child测量以可用空间为基准,所以widthUsed和heightUsed都传0
measureChildWithMargins(child,widthMeasureSpec,0,heightMeasureSpec,0)
lineMaxHeight = max(lineMaxHeight, child.measuredHeight)
finalWidth+=child.measuredWidth
}
// 保存测量高度
setMeasuredDimension(finalWidth,finalHeight)
}

然后需要布局,ViewGroup的布局就是遍历子View然后根据测量尺寸和坐标进行水平layout,暂时只考虑单行情况所以所有子View的to都是0,代码也很好理解

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
// 已使用宽度
// 因为是水平layout所以,每个子view的left应该从已使用宽度开始计算
var usedWidth = 0
for(child in children){
child.layout(usedWidth,0,usedWidth+child.measuredWidth,child.measuredHeight)
usedWidth+=child.measuredWidth
}
}

创建布局文件并添加子View

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.hezd.taglayout.FlowLayout
android:id="@+id/tag_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/tag_bg"
android:gravity="center"
android:paddingLeft="15dp"
android:paddingTop="5dp"
android:paddingRight="15dp"
android:paddingBottom="5dp"
android:text="aa"
android:textAllCaps="true" />
<!--为了节省空间忽略雷同Textview-->
<!--....-->
</com.hezd.taglayout.FlowLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

单行情况已经处理完了,运行看看效果吧。咦,这是什么鬼

java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6899)
at com.hezd.taglayout.FlowLayout.onMeasure(FlowLayout.kt:23)
at android.view.View.measure(View.java:24851)
at androidx.constraintlayout.widget.ConstraintLayout$Measurer.measure(ConstraintLayout.java:811)

程序崩溃了类型转化异常,从堆栈信息可以看到异常发生在ViewGroup的measureChildWithMargins

protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// 子ViewLayoutParams强转为MarginLayoutParms
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

将子View的LayoutParams强转为MarginLayoutParms发生类型转化异常,不能将ViewGroup$LayoutParams转化为ViewGroup$MarginLayoutParams,那我们刚才布局添加的子View的LayoutParams哪里来的呢为什么是ViewGroup$LayoutParams呢,怎么才能转化为MarginLayoutParms,其实布局中子View的布局信息是在LayoutInflater解析布局文件时指定的

private void parseInclude(XmlPullParser parser, Context context, View parent,
AttributeSet attrs) throws XmlPullParserException, IOException {
// 忽略部分代码……

// 创建子View
final View view = createViewFromTag(parent, childName,
context, childAttrs, hasThemeOverride);
ViewGroup.LayoutParams params = null;
final ViewGroup group = (ViewGroup) parent;
try {
params = group.generateLayoutParams(attrs);
} catch (RuntimeException e) {
// Ignore, just fail over to child attrs.
}

if (params == null) {
params = group.generateLayoutParams(childAttrs);
}
view.setLayoutParams(params);
}

可以看到子View通过调用父View的generateLayoutPrams获取LayoutParams并setLayoutParams

public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}

返回的就是ViewGroup$LayoutParams,既然xml中声明的子View是通过调用父View的generateLayoutParams来获取LayoutParams我们可以重写generateLayoutParams方法

class FlowLayout(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
// 忽略部分代码……

override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context,attrs)
}
}

再次运行查看效果

需要注意的是这是在xml中添加子View的情况,如果是我们通过代码添加的情况呢,来看ViewGroup的addView

public void addView(View child) {
addView(child, -1);
}

public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException(
"generateDefaultLayoutParams() cannot return null ");
}
}
addView(child, index, params);
}

protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}

从代码可以看到如果addView时如果没有设置LayoutParams是通过调用generateDefaultLayoutParams来获取LayoutParams因此需要对这种情况做容错处理

class FlowLayout(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) {
// 忽略部分代码……

override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context,attrs)
}

override fun generateDefaultLayoutParams(): LayoutParams {
return MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT,MarginLayoutParams.WRAP_CONTENT)
}
}
多行

首先来看onMeasure实现多行,onMeasure最终目的是测量ViewGroup的宽高,什么时候需要换行呢,我们可以定义一个usedWidth表示当前行已占用的宽度,当usedWidth+child.measuredWidth>widthMeasureSpecSize时,并重置usedWidth为child.measuredWidth,如果有换行finalWidth的值应该为widthMeasureSpecSize。在来看高度计算,高度应该是每一行高度的总和。所以onMeasure的实现代码如下:

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// Flowlayout测量尺寸
var finalHeight = 0
var finalWidth = 0

val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
// 已使用行宽
var usedWidth = 0
// 单行中高度最高的尺寸大小
var maxLineHeight = 0
// 是否需要换行
var isNewLine: Boolean
for (child in children) {
// child测量
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
isNewLine = usedWidth+child.measuredWidth>widthSpecSize
if(isNewLine){
// 如果有换行可确定最大宽度
finalWidth = widthSpecSize
// 换行后缺省的最大行高
maxLineHeight = max(maxLineHeight,child.measuredHeight)
// 换行后测量高度累加
finalHeight+=maxLineHeight
// 状态重置
usedWidth = 0
maxLineHeight = 0
}
// 不管是否有换行都需要计算当前最大行高、已使用宽度、最大宽度
maxLineHeight = max(maxLineHeight,child.measuredHeight)
usedWidth+=child.measuredWidth
finalWidth = max(finalWidth,usedWidth)
}

// 需要把最后一行的最大高度也添加到finalHeight中
finalHeight += maxLineHeight

// 保存测量高度
setMeasuredDimension(finalWidth, finalHeight)
}

在来看onLayout,就是child的布局思路跟onMeasure类似,就是根据usedWith、child.measureWidth、是否需要换行来确定child的坐标,代码如下

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
// 已使用宽度
// 因为是水平layout所以,每个子view的left应该从已使用宽度开始计算
var usedWidth = 0
// 当前行起始纵坐标
var usedHeight = 0
// 单行中高度最高的尺寸大小
var maxLineHeight = 0
var isNeedNewLine: Boolean
for (child in children) {
isNeedNewLine = usedWidth + child.measuredWidth > measuredWidth
if (isNeedNewLine) {
//计算单行最大高度
maxLineHeight = max(maxLineHeight, child.measuredHeight)
// 布局top坐标更新
usedHeight += maxLineHeight
usedWidth = 0
maxLineHeight = 0
}

// 开始布局
child.layout(
usedWidth,
usedHeight,
usedWidth + child.measuredWidth,
usedHeight + child.measuredHeight
)

// 更新单行最大高度、已占用行宽度
maxLineHeight = max(maxLineHeight, child.measuredHeight)
usedWidth += child.measuredWidth

}
}

来看下运行效果

image-20211224202434667
间距

添加间距相对比较简单就是在测量和布局时将间距考虑进去,首先需要自定义属性行间距、条目间距

自定义属性添加在res/attrs.xml文件中

<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FlowLayout">
<attr name="lineSpacing" format="dimension"/>
<attr name="itemSpacing" format="dimension"/>
</declare-styleable>
</resources>

布局文件中设置属性值

<com.hezd.flowlayout.FlowLayout
android:id="@+id/tag_layout"
app:itemSpacing="10dp"
app:lineSpacing="10dp"
android:paddingLeft="10dp"
android:paddingRight="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent">

</com.hezd.flowlayout.FlowLayout>

FLowLayout最终代码

/**
*
*@author hezd
*Create on 2021/12/24 17:08
*/
class FlowLayout(context: Context, attrs: AttributeSet) : ViewGroup(context, attrs) {
var lineSpacing: Int
var itemSpacing: Int

init {
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout)
lineSpacing = typedArray.getDimensionPixelOffset(R.styleable.FlowLayout_lineSpacing, 0)
itemSpacing = typedArray.getDimensionPixelOffset(R.styleable.FlowLayout_itemSpacing, 0)
typedArray.recycle()
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

// Flowlayout测量尺寸
var finalHeight = 0
var finalWidth = 0

val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
// 已使用行宽
var usedWidth = 0
// 单行中高度最高的尺寸大小
var maxLineHeight = 0
// 是否需要换行
var isNewLine: Boolean
for (child in children) {
// child测量以可用空间为基准
measureChildWithMargins(
child, widthMeasureSpec,
getDefaultUsedWidth(), heightMeasureSpec, paddingTop + paddingBottom
)
isNewLine = usedWidth + child.measuredWidth > widthSpecSize
if (isNewLine) {
// 如果有换行可确定最大宽度
finalWidth = widthSpecSize
// 换行后缺省的最大行高
maxLineHeight = max(maxLineHeight, child.measuredHeight)
// 换行后测量高度累加
finalHeight += maxLineHeight + lineSpacing
// 状态重置
usedWidth = getDefaultUsedWidth()
maxLineHeight = 0
}
// 不管是否有换行都需要计算当前最大行高、已使用宽度、最大宽度
maxLineHeight = max(maxLineHeight, child.measuredHeight)
usedWidth += child.measuredWidth + itemSpacing
finalWidth = max(finalWidth, usedWidth)
}

// 需要把最后一行的最大高度也添加到finalHeight中
finalHeight += maxLineHeight

// 保存测量高度
setMeasuredDimension(finalWidth, finalHeight)
}

private fun getDefaultUsedWidth() = paddingLeft + paddingRight

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
// 已使用宽度
// 因为是水平layout所以,每个子view的left应该从已使用宽度开始计算
var usedWidth = getDefaultUsedWidth()
// 当前行起始纵坐标
var usedHeight = 0
// 单行中高度最高的尺寸大小
var maxLineHeight = 0
var isNeedNewLine: Boolean
for (child in children) {

isNeedNewLine = usedWidth + child.measuredWidth > measuredWidth
if (isNeedNewLine) {
//计算单行最大高度
maxLineHeight = max(maxLineHeight, child.measuredHeight)
// 布局top坐标更新
usedHeight += maxLineHeight + lineSpacing
usedWidth = getDefaultUsedWidth()
maxLineHeight = 0
}

// 开始布局
child.layout(
usedWidth,
usedHeight,
usedWidth + child.measuredWidth,
usedHeight + child.measuredHeight
)

// 更新单行最大高度、已占用行宽度
maxLineHeight = max(maxLineHeight, child.measuredHeight)
usedWidth += child.measuredWidth + itemSpacing

}
}

override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams {
return MarginLayoutParams(context, attrs)
}

override fun generateDefaultLayoutParams(): LayoutParams {
return MarginLayoutParams(MarginLayoutParams.WRAP_CONTENT, MarginLayoutParams.WRAP_CONTENT)
}
}

运行查看效果

尺寸修正

目前看来已经没有问题了是不是,别急我们把布局中FlowLayout高度改为50dp,会发现任然可以正常显示,这是什么鬼?因为我们测量完以后还需要考虑它的布局参数,进行尺寸修正,如何修正呢,View提供了resolveSize方法

public static int resolveSize(int size, int measureSpec) {
return resolveSizeAndState(size, measureSpec, 0) & MEASURED_SIZE_MASK;
}

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}

可以看到当我们布局中指定了高度时就会走EXACTLY这个case测量高度就是我们指定的值,所以最终测量高度不应该仅仅以child的测量高度来计算,尺寸修正后的onMeasure

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// Flowlayout测量尺寸
var finalHeight = 0
var finalWidth = 0

val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
// 已使用行宽
var usedWidth = 0
// 单行中高度最高的尺寸大小
var maxLineHeight = 0
// 是否需要换行
var isNewLine: Boolean
for (child in children) {
// child测量以可用空间为基准
measureChildWithMargins(
child, widthMeasureSpec,
getDefaultUsedWidth(), heightMeasureSpec, paddingTop + paddingBottom
)
isNewLine = usedWidth + child.measuredWidth > widthSpecSize
if (isNewLine) {
// 如果有换行可确定最大宽度
finalWidth = widthSpecSize
// 换行后缺省的最大行高
maxLineHeight = max(maxLineHeight, child.measuredHeight)
// 换行后测量高度累加
finalHeight += maxLineHeight + lineSpacing
// 状态重置
usedWidth = getDefaultUsedWidth()
maxLineHeight = 0
}
// 不管是否有换行都需要计算当前最大行高、已使用宽度、最大宽度
maxLineHeight = max(maxLineHeight, child.measuredHeight)
usedWidth += child.measuredWidth + itemSpacing
finalWidth = max(finalWidth, usedWidth)
}

// 需要把最后一行的最大高度也添加到finalHeight中
finalHeight += maxLineHeight

// 保存测量高度
setMeasuredDimension(
resolveSize(finalWidth, widthMeasureSpec),
resolveSize(finalHeight, heightMeasureSpec)
)
}

再次运行可以看到FlowLayout只显示大概一行的内容跟我们预期一致

代码Github仓库地址:FlowLayout

ScrollView嵌套ListView显示不全问题及解决

开发过程中如果ScrollView嵌套ListView并且layout_height都为match_parent,会发现ListView显示不全只显示第一个条目,在上面测量过程和父View对子View测量分析中我们可以知道View的高度是父VIew和子View配合协作的结果,下面就从源码角度去分析原因。

ScrollView高度怎么确定的

ScrollView高度是怎么确定的,为什么在布局里指定match_parent就填充满屏幕呢

首先来看ScrollView的onMeasure方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

// 忽略部分代码……
}

调用了父View的onMeasure也就是FrameLayout的onMeasure

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

// 忽略部分代码……

// 保存测量尺寸
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}

可以看到ScrollView的高度由heightMeasureSpec确定,heightMeasureSpec哪里来的呢?当然是它的父View也就是DecorView的子View就是mContentParent传递过来的约束(为什么是mContentParent可查看Activiey的setContentView源码),mContentParent的MeasureSpec来自它的父View就是DecorView,它测量是在ViewRootImpl的performTraversals中

private void performTraversals() {
// 忽略部分代码……
int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {

case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}

可以看到DecorView的MeasureSpec是由getRootMeasureSpec来的,根据DecorView的布局信息LayoutParms类型指定,DecorView的布局是在PhoneWindow的generateLayout中指定的

protected ViewGroup generateLayout(DecorView decor) {
// 忽略部分代码……

// 根据窗口features确定layout以其中一个为例
int layoutResource;
int features = getLocalFeatures();
layoutResource = R.layout.screen_simple;

}

screen_simple.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

可以看到DecorView的布局信息LayoutParams指定的match_parent,在回头看getRootMeasureSpec

private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}

由于布局信息LayoutParams是match_parent,所以DecorView的MeasureSpec尺寸是窗体可用空间,模式是EXACTLY。

mContentParent的MeasureSpec从哪儿来呢,来自DecorView的onMeasure对子view测量过程中传递而来,DecorView继承Framelayout最终会调用ViewGroup的getChildMeasureSpec

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 父view的MeasureSpec约束
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

// 通过父View的MeasureSpec结合子View的布局信息
// 计算出子View的MeasureSpec约束
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

因为DecorView的specMode是EXACTLY,size是窗体可用空间,从screen_simple布局文件中我们可以看到mContentParent的布局信息LayoutParams为match_parent可以得出mContentParent的specSize为窗体可用空间specMode为EXACTLY,而mContentParent又是一个FrameLayout同DecorView测量过程一样进而可以得出ScrollView的specSize为窗体可用空间specMode为EXACTLY

而我们知道View测量尺寸有两个过程一个是父View获取子VIew的约束MeasureSpec并传递给子View,另一个是setMeasureDimension最终确定测量尺寸。ScrollView的setMeasureSpec是由onMeasure->Framelayout的onMeasure->setMeasureDimension

FrameLayout.java

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();

final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();

int maxHeight = 0;
int maxWidth = 0;
int childState = 0;

for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}

// Account for padding too
maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

// Check against our minimum height and width
maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

// Check against our foreground's minimum height and width
final Drawable drawable = getForeground();
if (drawable != null) {
maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
}

// 确定最终测量尺寸
// maxHeight是子View高度这里就是ListView高度
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
}

setMeasureDimension的measureHeight又调用了resolveSizeAndState

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
// size在这里就是ListView高度
// specMode有上面分析知道specMode是EXACTLY
final int specMode = MeasureSpec.getMode(measureSpec);
// specSize由上面代码分析就是窗体高度
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
// ScrollView的specMode跟layout_height对应关系
// 可根据上面FrameLayout的measureChildWithMargins代码分析得出
switch (specMode) {
// ScrollView的layout_height指定wrap_content
case MeasureSpec.AT_MOST:
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
// ScrollView的layout_height指定match_parent
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}

结论:通过源码分析可以得出,ScrollView嵌套ListView当ScrollView高度layout_height为match_parent时高度为窗体可用高度,layout_height为wrap_content时高度为ListView的高度

ListView高度怎么确定

旧话重提View的高度是父View和子View配合协作的结果,先看ListView的onMeasure方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// Sets up mListPadding
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);

int childWidth = 0;
int childHeight = 0;
int childState = 0;

mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
|| heightMode == MeasureSpec.UNSPECIFIED)) {
final View child = obtainView(0, mIsScrap);

// Lay out child directly against the parent measure spec so that
// we can obtain exected minimum width and height.
// 如果父View传递的specMode约束为UNSPECIFIED测量第一个条目高度childHeight
measureScrapChild(child, 0, widthMeasureSpec, heightSize);

childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState());

if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
mRecycler.addScrapView(child, 0);
}
}

if (widthMode == MeasureSpec.UNSPECIFIED) {
widthSize = mListPadding.left + mListPadding.right + childWidth +
getVerticalScrollbarWidth();
} else {
widthSize |= (childState & MEASURED_STATE_MASK);
}

if (heightMode == MeasureSpec.UNSPECIFIED) {
// 如果父View传递的约束specMode为UNSPECIFIED
// ListView高度就为第一个条目高度childHeight(另外还有padding,edge)
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}

if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
// 如果父View传递的约束specMode是AT_MOST
// ListView高度为所有子条目高度累加结果
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
// 保存测量尺寸 heightSize就是ListView测量高度
setMeasuredDimension(widthSize, heightSize);

mWidthMeasureSpec = widthMeasureSpec;
}

从代码来看如果父View传递的约束specMode为UNSPECIFIED时ListView高度heightSize为第一个条目高度,跟我们开头说的现象一致,那么父View->ScrollView传递的约束specMode是不是UNSPECIFIED呢,来看ScrollView的onMeasure

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

// 如果ScrollView的fillViewport为false测量结束
if (!mFillViewport) {
return;
}

// 如果fillViewport为true,重新测量
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}

if (getChildCount() > 0) {
// 获取第一个条目child,ScrollView也只能有一个子View
final View child = getChildAt(0);
final int widthPadding;
final int heightPadding;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (targetSdkVersion >= VERSION_CODES.M) {
widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
} else {
widthPadding = mPaddingLeft + mPaddingRight;
heightPadding = mPaddingTop + mPaddingBottom;
}

final int desiredHeight = getMeasuredHeight() - heightPadding;
// 如果child测量高度小于ScrollView高度,重新测量
if (child.getMeasuredHeight() < desiredHeight) {
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
// 设置约束specMode为EXACTLY
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredHeight, MeasureSpec.EXACTLY);
// 重新测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}

从源码可以看到ScrollView子View可能会进行两次测量,第一次测量调用父类的Measure方法进行测量,如果fillViewport为true会进行二次测量,下面对两次测量进行分析

第一次测量

调用父类onMeasure方法,ScrollView的父类是FrameLayout来看它的onMeasure方法

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();

final boolean measureMatchParentChildren =
MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
mMatchParentChildren.clear();

int maxHeight = 0;
int maxWidth = 0;
int childState = 0;

for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 对子View进行测量
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
maxWidth = Math.max(maxWidth,
child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
maxHeight = Math.max(maxHeight,
child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}

// 忽略部分代码……

setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));

}

FrameLayout的onMeasure会调用measureChildWithMargins方法对子View进行测量,需要注意的是ScrollView重写了measureChildWithMargins方法,所以看ScrollView的measureChildWithMargins

@Override
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
heightUsed;

// 将子View约束specMode设置为UNSPECIFIED
final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
MeasureSpec.UNSPECIFIED);

// 调用子View的measure方法进行测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

所以到这里真相大白了,ScrollView传递给子View的约束specMode为UNSPECIFIED,所以导致了显示不全。另外上面提到了如果ScrollView的fillViewport为true子View会进行二次测量会对显示结果有什么影响呢

Go ahead

二次测量

在继续回看ScrollView的onMeasure

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

// 如果ScrollView的fillViewport为false测量结束
if (!mFillViewport) {
return;
}

// 如果fillViewport为true,重新测量
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.UNSPECIFIED) {
return;
}

if (getChildCount() > 0) {
// 获取第一个条目child,ScrollView也只能有一个子View
final View child = getChildAt(0);
final int widthPadding;
final int heightPadding;
final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (targetSdkVersion >= VERSION_CODES.M) {
widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
} else {
widthPadding = mPaddingLeft + mPaddingRight;
heightPadding = mPaddingTop + mPaddingBottom;
}

final int desiredHeight = getMeasuredHeight() - heightPadding;
// 如果child测量高度小于ScrollView高度,重新测量
if (child.getMeasuredHeight() < desiredHeight) {
final int childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec, widthPadding, lp.width);
// 设置约束specMode为EXACTLY
final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
desiredHeight, MeasureSpec.EXACTLY);
// 重新测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
}
}

现在在来看这段代码就很清晰了,如果fillViewport为true进行二次测量时ScrollView传递给子View的specMode为EXACTLY结合上面ListView的onMeasure代码分析可以知道如果fillViewport为true的情况下ListView的高度为子条目高度之和也会是我们预期结果。

结论

默认情况下ScrollView嵌套ListView只会显示第一个条目,如果ScrollView的fillViewport为true会进行二次测量结果ListView高度就是预期的所有子条目高度之和。

解决方案

通过上面源码分析很容易找到解决方案

1.重写ListView修正父View传递的约束specMode为AT_MOST或者EXACTLY

/**
*
*@author hezd
*Create on 2021/12/22 16:51
*/
class MyListView(context: Context?, attrs: AttributeSet?) : ListView(context, attrs) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val size = MeasureSpec.getSize(heightMeasureSpec)
val newHeightSpec = MeasureSpec.makeMeasureSpec(size,MeasureSpec.EXACTLY)
// val newHeightSpec = MeasureSpec.makeMeasureSpec(size,MeasureSpec.AT_MOST)
super.onMeasure(widthMeasureSpec, newHeightSpec)
}
}

2.将ScrollVIew的fillVIewport属性设置为true

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<data>

</data>

<ScrollView
android:fillViewport="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<ListView
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</ScrollView>
</layout>

两种方案对比:第一种方式最优因为只测量一次,第二种方式会两次测量性能有损耗

测量

从根View开始递归调用每一级子View的measure方法进行测量,实际测量工作是在onMeasure中完成,最终将测量尺寸保存以便后面布局时使用。

简单来说分两个过程

①父View获取子View的MeasureSpec约束并传递给子View

②子View调用setMeasureDimension最终确定测量尺寸并保存,保存之前通常还会进行测量尺寸修正

View.java

/**
* This is called to find out how big a view should be. The parent
* supplies constraint information in the width and height parameters.
* 从文档注释可以看出:widthMeasureSpec和heightMeasureSpec是父View对子View尺寸测量的一个约束信息
*/
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
//忽略部分代码...
if (forceLayout || needsLayout) {
// 忽略部分代码...

int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
if (cacheIndex < 0 || sIgnoreMeasureCache) {
// measure ourselves, this should set the measured dimension flag back
onMeasure(widthMeasureSpec, heightMeasureSpec);
mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
}
}
}

/**
* 实际测量工作是onMeasure方法
*/
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 保存测量结果
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

ViewGroup并没有实现onMeasure因为它是一个抽象类,每一个实现类的具体onMeasure行为不同,都是对子View遍历测量,RelativeLayout中是调用measureChild而LinearLayout是mearsureChildWithMargins以RelativeLayout为例

RelativeLayout.java

	@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 忽略部分代码...

int width = 0;
int height = 0;
for (int i = 0; i < count; i++) {
// ignore some code...
// 遍历测量每一个子View
measureChild(child, params, myWidth, myHeight);
}

// 保存测量结果
setMeasuredDimension(width, height);
}

private void measureChild(View child, LayoutParams params, int myWidth, int myHeight) {
// 获取子View的MeasureSpec约束
int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft,
params.mRight, params.width,
params.leftMargin, params.rightMargin,
mPaddingLeft, mPaddingRight,
myWidth);
int childHeightMeasureSpec = getChildMeasureSpec(params.mTop,
params.mBottom, params.height,
params.topMargin, params.bottomMargin,
mPaddingTop, mPaddingBottom,
myHeight);
//调用子View的measure测量并将约束传递给子View
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

measureChild最终调用getChildMeasureSpec获取子View的MeasureSpec约束,此方法在后面父View如何对子View进行测量部分在做详细解析

布局

从根布局开始递归调用每一级子View的layout方法进行布局,如果有子View还会调用onLayout,将测量过程中得到的尺寸和位置传递给子View并保存,以便确定View最终在屏幕中显示的位置

View.java

 public void layout(int l, int t, int r, int b) {
// 保存位置坐标
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 如果是ViewGroup需要对子View进行布局
onLayout(changed, l, t, r, b);
}
// 调用setFrame保存位置坐标
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
}

/**
* View的onLayout是空实现,ViewGroup需要重写此方法
* 对子View进行布局
* Called from layout when this view should
* assign a size and position to each of its children.
*
*/
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}

如果是自定义View我们需要根据需求自定义layout规则,例如流布局,具体实现会在《自定义ViewGroup》章节讲解,文章链接:自定义ViewGroup

MeasureSpec

/**
* A MeasureSpec encapsulates the layout requirements passed from parent to child.
* Each MeasureSpec represents a requirement for either the width or the height.
* A MeasureSpec is comprised of a size and a mode...
*/
public static class MeasureSpec

从文档注释可以看到MeasureSpec包含了父View对子View的布局要求也就是约束信息

MeasureSpec由32位组成的整型数,前两位代表测量模式mode后面30位代表specSize,mode有三种分为Exactly,At_most,unspecified

Exactly:父View指定了精确值

AT_MOST:父View指定了最大值

UNSPECIFIED:未指定,子View想要多大就多大

父View如何对子View进行测量

尺寸测量是一个父View和子View配合协作的过程,父View在测量过程中会计算子View的MeasureSpec约束并传递给子View,子View在根据MeasureSpec约束和自己的布局信息计算出实际尺寸。因此计算子View的MeasureSpec约束和子View最终测量决定了子View最终的测量尺寸下面分别来看:

子View的约束

在上面测量过程中介绍到ViewGroup没有onMeasure的具体实现因为不同的ViewGroup有不同行为要求,LinearLayout和ListView都是由ViewGroup的getChildMeasureSpec类获取子View的MeasureSpec约束,RelativeLayout是自己内部的getChildMeasure来获取约束,我们把ViewGroup的getChildMeasureSpec作为一个通用实现来看

ViewGroup.java

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 父View的MeasureSpec
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);

// 可用空间
int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
// 如果父View的specMode是EXACTLY
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// 如果父View的specMode是AT_MOST
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

// 如果父View的specMode是UNSPECIFIED
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

结论:

如果父View的测量模式是EXACTLY

  • 如果子View布局参数指定了确切值那么测量尺寸就取确切值测量模式为EXACTLY
  • 如果子View布局参数LayoutParams是MATCH_PARENT那么测量尺寸为剩余空间测量模式为EXACTLY
  • 如果子View的布局参数LayoutParams是WRAP_CONTENT那么测量尺寸为剩余空间测量模式为AT_MOST

如果父View的测量模式是AT_MOST

  • 如果子View布局参数指定了精确值那么测量尺寸为精确值测量模式为EXACTLY
  • 如果子View布局参数LayoutParams为MATCH_PARENT那么测量尺寸为剩余空间测量模式为AT_MOST
  • 如果子View布局参数LayoutParams为WRAP_CONTENT那么测量尺寸为剩余空间测量模式为AT_MOST

如果父View的测量模式是UNSPECIFIED

  • 如果子View布局参数指定了精确值那么测量尺寸为精确值测量模式为EXACTLY
  • 如果子View布局参数LayoutParams为MATCH_PARENT那么测量尺寸为0或剩余空间测量模式为UNSPECIFIED
  • 如果子View布局参数LayoutParams为WRAP_CONTENT那么测量尺寸为0或剩余空间测量模式为WRAP_CONTENT
子View最终测量

在上面测量过程中我们知道View实际测量工作是在onMeasure中,来查看View的onMeasure

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
boolean optical = isLayoutModeOptical(this);
if (optical != isLayoutModeOptical(mParent)) {
Insets insets = getOpticalInsets();
int opticalWidth = insets.left + insets.right;
int opticalHeight = insets.top + insets.bottom;

measuredWidth += optical ? opticalWidth : -opticalWidth;
measuredHeight += optical ? opticalHeight : -opticalHeight;
}
setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
mMeasuredWidth = measuredWidth;
mMeasuredHeight = measuredHeight;

mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

最终会调用setMeasuredDimensionRaw方法保存测量尺寸,而测量尺寸又由getDefaultSize方法最终确定

public static int getDefaultSize(int size, int measureSpec) {
// 建议的最小尺寸
int result = size;
// 子View的MeasureSpec约束
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);

// 根据View的MeasureSpec约束确定测量尺寸
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

建议的最小尺寸如果有背景的话就是背景drawable的尺寸否则为0

protected int getSuggestedMinimumWidth() {
return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

结论:

如果View的specMode约束是UNSPECIFIED测量尺寸为建议最小尺寸,否则就以specSize作为最终测量尺寸

案例分析

我们在开发中有时候需要使用ScrollView嵌套ListView,xml指定layout_height都为match_parent,但是会发现ListView只显示第一个条目,这是为什么呢,我们可以从源码角度解析问题的原因,因为篇幅问题放到另一篇文章介绍请戳此链接:

ScrollView嵌套LitView显示问题探究

View滑动

view滑动可通过下面几种方法:

  • layout
  • offsetLeftAndRight
  • offsetTopAndBottom
  • LayoutParams
  • Animation
  • Scroller

示意图

view_slide01

view_slide02:


layout

通过layout方法让view移动到指定的坐标位置

val HORIZONTAL_OFFSET = 100f.dp
val VERTICAL_OFFSET = 100f.dp
class SlideByLayoutActivity : BaseSlideActivity() {
override fun slide() {
slideView.layout(
HORIZONTAL_OFFSET,
VERTICAL_OFFSET,
HORIZONTAL_OFFSET + slideView.width,
VERTICAL_OFFSET + slideView.height
)
}
}

offsetLeftAndRight

通过view的offetLeftAndRight方法滑动到指定位置

override fun slide() {
slideView.offsetLeftAndRight(HORIZONTAL_OFFSET)
}

offsetTopAndBottom

通过view的offetTopAndBottom方法滑动到指定位置

override fun slide() {
slideView.offsetTopAndBottom(-VERTICAL_OFFSET)
}

LayoutParams

通过LayoutParams滑动到指定位置

override fun slide() {
val layoutParams = slideView.layoutParams as ConstraintLayout.LayoutParams
layoutParams.leftMargin = HORIZONTAL_OFFSET
layoutParams.topMargin = VERTICAL_OFFSET
slideView.layoutParams = layoutParams
}

Animation

通过位移动画滑动到指定位置,注意位移动画只是视觉移动View并不会真正发生移动,如果View有点击事件,还是响应原来的点击区域

override fun slide() {
val translateAnimation = TranslateAnimation(
0f,
(HORIZONTAL_OFFSET - slideView.left).toFloat(),
0f,
(VERTICAL_OFFSET - slideView.top).toFloat()
)
translateAnimation.duration = 500
translateAnimation.fillAfter = true
slideView.startAnimation(translateAnimation)
}

Scroller

通过layout,layoutparams,offsetLeftAndRight方法对view滑动是比较生硬的,可以使用Scroller实现平滑移动
实现原理:
Scroller的用法有固定模板
1.自定义View重写computeScroll实现滑动逻辑
2.提供对外调用的滑动方法,初始化scroller
自定义View并重写computeScroll方法,computeScroll会在View的onDraw方法中被调用,computeScroll实现细节是获取当前时刻scroller滑动的最新位置并调用View的Parent的scrollTo方法滑动并调用postInvalidate方法重绘触发下一次computeScroll循环直到滑动到指定位置。
然后需要提供一个对外暴露的滑动方法内部初始化Scroller并调用startScroll方法启动Scroller接着调用invalidate触发computeScroll方法。

class SlideView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
View(context, attrs) {
private var scroller: Scroller = Scroller(context)
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
(parent as View).scrollTo(scroller.currX, scroller.currY)
postInvalidate()
}
}

fun startSmooth(delX: Int, delY: Int) {
val destX = delX - scrollX
scroller.startScroll(scrollX, 0, destX, delY, 2000)
invalidate()
}
}

解惑Scroller:
1.为什么是调用Parent的scrollTo?
2.为什么滑动方向跟期望是相反的?

为什么是调用Parent的scrollTo?
我们很直观的会认为应该调用View的scrollTo方法发现View位置并没有发生变化,这是因为scrollTo移动的是View的内容Content,如果的TextView调用scrollTo你会发现文本内容发生了移动


为什么滑动方向跟期望是相反的?
当我们想要x轴放心移动100像素值时,实际结果恰好相反向x轴负方向发生了移动,这是为什么呢
借鉴《Android群英传》的一张图来解释

我们可以把View看做是镂空的下面是内容content就像画布一样,当我们调用scrollTo时是镂空的部分在移动,这个移动是相对于content的位移,相当于content并没有移动移动的是镂空的部分,我们想要button右移的时候,镂空的部分需要向左移动,这样大家应该可以更好的理解了。

Github代码地址:ViewSlide

参考:
《Android群英传》

1.AIDL是什么

AIDL(Android interface defination language)是一种dl语言,用于生成进程间通信的IPC代码。基于c/s架构,使用代理类进行客户端和服务端交互。

2.AIDL应用场景

在官方文档中提到只有在需要不同应用客户端通过IPC方式访问服务,并且需要在服务中进行多线程操作时才有必要使用AIDL.如果无需在跨应用执行并发IPC应该通过扩展Binder类(创建普通服务)的方式实现客户端和服务的交互.或者你想执行IPC但不需要处理多线程可以使用Messenger来实现.

3.AIDL实现

3.1创建AIDLDemo项目以及服务端aidlserver和客户端aidlclient

image

3.2服务端实现

  • 创建AIDL文件
    在src/main目录下用As可视化工具new一个aidl文件会自动生成一个aidl文件夹用来存放aidl文件,我们这里以图书管理为例,创建IBookManager.aidl、Book.aidl和Book.java
    image

Book是我们用到的图书类,在AIDL中如果要用到类需要创建同名的Java类和aidl文件
IBookManager中定义了远程服务提供的方法
AIDL支持的数据类型包括以下几种:
Java的8种基本数据类型:int,short,long,char,double,byte,float,boolean;
CharSequence类型,如String、SpannableString等;
ArrayList,并且T必须是AIDL所支持的数据类型;
HashMap<K,V>,并且K和V必须是AIDL所支持的数据类型;
所有Parceable接口的实现类,因为跨进程传输对象时,本质上是序列化与反序列化的过程;
AIDL接口,所有的AIDL接口本身也可以作为可支持的数据类型;

  • 创建远程服务
    创建远程服务,服务端Binder对象并在onBind方法中返回
    image
  • 清单文件中注册远程服务
    image
    注意:AS默认配置编译会报错需要配置aidl路径
    image
    3.3客户端实现
  • 创建AIDL文件
    将服务端的AIDL文件拷贝到客户端相同包名下
    image
  • 绑定服务
    image

通过代码我们可以看到在客户端调用服务端addBook方法向图书列表中添加一本图书并查询添加后的结果,首先启动服务端,启动客户端可以看到控制台日志,添加成功。

image

以上就是AIDL的用法,代码github地址

AIDLDemo

如果觉得有用,欢迎star

参考:
《Android艺术探索》
《AIDL使用详解》
《Android官方文档》

消息机制

主线程和子线程通信
消息机制涉及到三个角色,Handler、MessageQueue、Looper

基本实现

这里只介绍主线程handler创建方式,子线程后续源码部分在介绍

  • 创建Handler,重写handleMessage方法
  • Handler发送消息
  • handleMessage接收消息并处理
基本原理

Handler发送消息并最终添加到MessageQueue,Looper调用loop方法后开始轮询从MessageQueue中获取消息并调用对应的handler的dispatchMessage方法。

疑问
  • Handler如何发送消息的
  • Looper是如何初始化,如何获取消息的
  • 消息机制如何实现线程间如何切换的
  • Looper死循环为什么不会导致anr
  • Linux的epoll机制

带着疑问我们通过下面源码部分去解惑

源码解析
  • Handler到底是如何发送消息的
    首先来看Hander创建

    public Handler(Callback callback, boolean async) {
    //... 省略部分代码

    mLooper = Looper.myLooper();
    if (mLooper == null) {
    throw new RuntimeException(
    "Can't create handler inside thread " + Thread.currentThread()
    + " that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
    }

    创建时会调用Looper.myLooper()获取当前线程的Looper对象

    public static @Nullable Looper myLooper() {
    return sThreadLocal.get();
    }

    这里用到了ThreadLocal线程局部变量,内部维护着一个ThreadLocalMap以map形式存储局部变量,key是当前线程value是局部变量。这样就实现了线程之间数据隔离。避免数据冲突。

    创建完Handler以后继续看发送消息SendMessage

    public final boolean sendMessage(Message msg)
    {
    return sendMessageDelayed(msg, 0);
    }
    public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
    if (delayMillis < 0) {
    delayMillis = 0;
    }
    return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
    public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
    MessageQueue queue = mQueue;
    if (queue == null) {
    RuntimeException e = new RuntimeException(
    this + " sendMessageAtTime() called with no mQueue");
    Log.w("Looper", e.getMessage(), e);
    return false;
    }
    return enqueueMessage(queue, msg, uptimeMillis);
    }

    sendMessage会调用sendMessageDelayed紧接着又调用SendMessageAtTime,最终会调用enqueueMessage,将消息添加到消息队列。这里消息队列是哪里来的呢,在我们最开始Handler初始化时有一行代码mQueue = mLooper.mQueue;原来是来自Looper的MessageQueue,具体它的创建我们在后面Looper初始化在详解。
    接下来在看enqueueMessage如何将消息添加到消息队列

    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    msg.target = this;
    if (mAsynchronous) {
    msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
    }

    Handler的enqueueMessage做了两件事情,第一是将当前handler引用赋值给message的target建立绑定关系以便于后面消息分发处理的时候知道要处理的消息属于哪个Handler。第二件事情就是调用MessageQueue的enqueueMessage

    boolean enqueueMessage(Message msg, long when) {
    //...忽略部分代码

    synchronized (this) {
    //... 忽略部分代码

    msg.markInUse();
    msg.when = when;
    Message p = mMessages;
    boolean needWake;
    if (p == null || when == 0 || when < p.when) {
    // New head, wake up the event queue if blocked.
    msg.next = p;
    mMessages = msg;
    needWake = mBlocked;
    } else {
    // Inserted within the middle of the queue. Usually we don't have to wake
    // up the event queue unless there is a barrier at the head of the queue
    // and the message is the earliest asynchronous message in the queue.
    needWake = mBlocked && p.target == null && msg.isAsynchronous();
    Message prev;
    for (;;) {
    prev = p;
    p = p.next;
    if (p == null || when < p.when) {
    break;
    }
    if (needWake && p.isAsynchronous()) {
    needWake = false;
    }
    }
    msg.next = p; // invariant: p == prev.next
    prev.next = msg;
    }

    // We can assume mPtr != 0 because mQuitting is false.
    if (needWake) {
    nativeWake(mPtr);
    }
    }
    return true;
    }

    MessageQueue的enqueueMessage中会判断当前message是否需要立即执行如果需要就添加到表头,否则就根据时间轮询比对添加到中间位置。
    到这里Handler创建和消息发送以及添加到消息队列过程就很清晰了,接下来要看Looper的创建和消息如何轮询分发了。

  • Looper是如何初始化,如何获取消息的
    这里又要回到Handler的初始化,在Handler初始化的时候回获取当前线程的Looper对象

    public Handler(Callback callback, boolean async) {
    //...忽略部分代码
    mLooper = Looper.myLooper();
    if (mLooper == null) {
    throw new RuntimeException(
    "Can't create handler inside thread " + Thread.currentThread()
    + " that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
    }

    这里可以看到如果获取到的Looper对象为空就会抛出异常,所以Handler初始化之前必须要先初始化Looper,但是我们平时主线程并没有手动创建为什么不会报错呢?这是因为在app启动入口ActivityThread的main方法中系统已经为我们做好了主线程的Looper初始化

    public static void main(String[] args) {
    //...忽略部分代码
    Looper.prepareMainLooper();
    //...忽略部分代码
    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
    }

    那Looper到底是如何初始化的呢,接着看Looper的prepareMainLooper

    public static void prepareMainLooper() {
    prepare(false);
    synchronized (Looper.class) {
    if (sMainLooper != null) {
    throw new IllegalStateException("The main Looper has already been prepared.");
    }
    sMainLooper = myLooper();
    }
    }

    prepareMainLooper又调用了prepare方法

    private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
    throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
    }

    原来Looper的初始化是调用prepare方法,这里会创建looper对象并添加到ThreadLocal中,所以一个线程只能有一个Looper,并且prepare只能调用一次否则会报错,
    那Looper是如何获取消息进行分发的呢,在上面ActivityThread代码中我们也可以看到Looper初始化完以后会调用Looper.loop()真相就在这里

    public static void loop() {
    final Looper me = myLooper();
    //... 忽略部分代码
    for (;;) {
    Message msg = queue.next(); // might block
    if (msg == null) {
    // No message indicates that the message queue is quitting.
    return;
    }

    //...忽略部分代码
    final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
    final long dispatchEnd;
    try {
    msg.target.dispatchMessage(msg);
    dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
    } finally {
    if (traceTag != 0) {
    Trace.traceEnd(traceTag);
    }
    }
    //...忽略部分代码
    msg.recycleUnchecked();
    }
    }

    我们可以看到loop方法轮询从MessageQueue中取出消息,判断如果需要立即执行就会调用meesage的target的dispatchtMessage方法进行消息分发处理。message的target就是上面提到的message所属的handler,因此会回调到Handler的dispatchMessage方法

    public void dispatchMessage(Message msg) {
    if (msg.callback != null) {
    handleCallback(msg);
    } else {
    if (mCallback != null) {
    if (mCallback.handleMessage(msg)) {
    return;
    }
    }
    handleMessage(msg);
    }
    }

    dispatchMessage会判断message的callback是否有设置,查看源码可以知道setCallBack是hide方法只能通过反射调用它,如果它为空继续判断mCallBack是否为空,mCallBack哪儿来的呢,在Handler初始化时候有一个参数就是是否设置callback

    public Handler(Callback callback, boolean async) {
    //...忽略部分代码
    mCallback = callback;
    //...忽略部分代码
    }

    如果mCallBack有设置会直接调用CallBack的handleMessge方法否则继续调用handler的handlemessage方法,因为我们在初始化Handler的时候回重写handlemessage方法所以最终会回调到我们重写的handlemessage方法里。

  • 消息机制是如何实现线程间切换的?
    我们通常会使用handler在子线程发送消息,然后回调到主线程处理消息。那到底是如何实现线程切换的呢,我们通过前面的分析可以知道,一个完整的消息分发流程,应该是先调用Looper.prepare()初始化Looper和消息队列,然后调用Looper.loop()开启轮询。然后创建Handler在指定线程发送消息接受回调处理消息。那回调是在哪个线程呢,重点来了:因为消息分发是Looper通过轮询拿到Message的target也就是发送者Handler在调用Handler的dispatchMessage方法完成分发的,这个调用是在Looper中,所以调用者在哪个线程当前回调就是在哪个线程,所以Looper初始化时绑定的线程就是回调的线程。

  • Looper死循环为什么不会导致anr
    App本质上也是一个java程序,入口就是ActivityThread的main方法,如果main方法执行完程序就退出了,如何保证不退出就是写一个死循环,ActivityThread中初始化了Looper并调用了loop在loop方法中开启了一个死循环阻塞了主线程这样程序可以保证程序一直执行不会退出。几乎所有的GUI程序都是这么实现的。既然是死循环那么其他代码怎么运行,页面交互怎么处理呢?Android是基于事件驱动的,不管是页面刷新还是交互本质上都是事件,都会被封装成Message发送到MessageQueue由Looper进行分发处理的。ANR是什么,Application no responding应用无响应,为什么没响应,因为主线程做了好事操作,loop方法死循环也会阻塞主线程为什么不会anr,什么是响应,响应就是页面刷新,交互处理等,谁来响应,其实就是looper的loop方法,,主线程做了耗时操作会阻塞loop方法导致无法处理其他message所以导致anr。

  • Linux的epoll机制
    我们先来看下Message的next方法

    Message next() {
    //...省略部分代码
    for (;;) {
    if (nextPollTimeoutMillis != 0) {
    Binder.flushPendingCommands();
    }
    //重点关注这一行
    nativePollOnce(ptr, nextPollTimeoutMillis);

    //...省略部分代码
    }
    }

    在MessageQueue的next方法中会调用本地方法nativePollOnce,它是以阻塞的方式从native层的MessageQueue中获取可用消息,也就是会进行休眠。当有可用消息时进行唤醒。Looper的休眠和唤醒的实现原理是Linux的epoll机制,Looper的休眠和唤醒是通过对文件可读事件监听来实现唤醒。
    什么是Linux的epoll机制?
    Linux的epoll机制可以简单理解为事件监听,当空闲的时候让出cpu进行休眠,当事件触发的时候会被唤醒开始处理任务。

参考:看完这篇还不明白Handler你砍我