Android 开发艺术探索笔记
这几天看了Android 开发艺术探索这本书,感觉是一本很好的书。我目前看了前4章。Activity的生命周期、Android的IPC机制、Android事件分发机制、Android的View绘制机制。这些都是Android开发者从中级迈向高级所必须的知识。书中很多篇幅是源码解析,有些人看起来可能会有些吃力,所以我写了这篇笔记,不仅仅是帮助自己巩固知识,也希望能帮助大家更快的了解书中的源码分析。在笔记中我也整合了一些看书过程中所查阅的博客,还有我以前看过的Android书籍中的知识点,完善了书中的一些描述不清楚的地方,相信大家如果能结合这篇博客一起看书的话,肯定是大有裨益的,毕竟读者总是更了解读者的。关于后续的章节,我会在接下来的1个月里陆续啃完发出来,大家如果对于书中的知识有什么不了解的地方,欢迎和我一起讨论。第一章 Activity的生命周期和启动模式 1.Activity的生命周期全面分析
Activity的生命周期分为两部分:1.典型生命周期,即正常情况下有用户参与的生命周期 2.Activity被销毁重建之后的生命周期
先来说说典型生命周期:
文章图片
activity的典型生命周期对于大部分开发者来说是再熟悉不过了,所以就上一张图。
书中提到了两个问题:1.onStart和onResume、onPause和onStop有什么区别。答:onStart和onStop是从Activity是否可见这个角度来回调。而onResume和onPause则是从Activity是否位于前台的角度来进行回调。 2.从Activity1启动Activity2,是Activity1的onPause先调用还是Activity2的oResume先调用。答:是Activity1的onPause先调用,然后启动Activity2,最后调用Activity1的onStop。再来说说异常生命周期:
- 系统配置发生改变导致Activity被杀死,例如:旋转了屏幕。
- 1.当旋转了屏幕老的Acrivity会被销毁新的Activity会生成,在老Activity调用onStop之前会调用onSaveInstanceState用来保存当前Activity的状态和开发者自定义保存的数据。
- 2.在新Activity调用onStart之后会调用onRestoreInstanceState,让开发者恢复在onSavaInstanceState中保存的数据。
- 3.onSaveInstanceState和onRestoreInstanceState在普通Activity销毁的时候都不会调用 。
- 4.系统会默认恢复Activity视图中的一些View的数据,比如说文本框中的数据等等,每个View都有onSaveInstanceState和onRestoreInstanceState这两个方法,可以查看这两个方法了解某5.些View会保存哪些数据。
- 一般建议在onRestoreInstanceState中恢复数据,因为onCreate中的bundle在正常启动的时候为空,onRestoreInstanceState只有在异常销毁Activity的时候才会调用,所以bundle不会为空
- 内存资源不足导致优先级低的Activity被杀死
- 1.处于onResume的Activity优先级最高,即正在和用户交互。2.处于onPause的Activity优先级第二,即弹出了对话框但Activity依然可见。3.处于onStop的Activity优先级最低,即不可见的处于后台的Activity。
- 注意:一些后台工作不适合独立运行在后台,因为脱离了四大组件,单独的线程很容易在内存不足的时候被系统杀死,所以比较好的方法是放入Service之中
- 1.standard:标准模式,每次启动一个Activity都会创建一个新的实来例。
- 2.singleTop:栈顶复用模式,每次启动一个Activity,如果栈顶就是这个Activity的话,调用这个Activity的onNewIntent方法,获取新的请求信息,然后调用onResume。
- 3.singleTask:栈内复用模式,每次启动一个Activity,会在栈内寻找是否有该Activity,如果找到,就将该Activity上面的Activity全部出栈,然后和singleTop一样。
- 4.singleInstance:单例模式,在singleTask之上还加强了一点,就是此种模式下,Activity单独位于一个任务栈内。
第二章 IPC机制 1.Android IPC简介
略
2.Android中的多进程模式
- 1.如何开启多进程呢?在AndroidMenifest中的四大组件中指定android:progess属性。
- 2.Android系统为每个进程都分配了一个虚拟机,如果同一个应用中有两个进程的话,那么每个进程中都会分配一个类,这两个进程中的类是独立的。也就是说处于不同进程的组件要通过内存来共享数据都会失败。注意多进程会出现如下几个问题:
- 1.静态成员和单例完全失效(因为每个进程都会分配一组)。
- 2.线程同步会失效(因为不在一块内存了)。
- 3.SharedPreferences可靠性失效(其不支持两个进程同时去执行写操作,因为其底层是是通过写xml文件实现的)。
- 4.Application会多次创建(原因同1,单例失效)。
- 1.Serializable接口,java提供的序列化接口,只需implement接口即可。注意:serialVersionUID最好自己指定,因为这样的话在增加或者减少成员变量之后,还是能反序列化成功。
- 2.Parcelable接口,Android提供的接口,implement之后,对象能够通过Intent和Binder传递,基本类型和String不需要实现序列化,但是如果是自己定义的类型被当成成员变量的时候,自己定义的类型也需要实现序列化。注意:系统提供了许多已经实现了Parcelable接口的类比如说Intent、Bundle、Bitmap、List和Map(不过List和Map中的元素也必须是已经序列化的)。
- 3.Binder类,Android开发中Binder主要用在Service的通信上(无论是在同一进程还是不同进程)。一般常用的AIDL和Messager的底层都是基于Binder。所以可以从AIDL来分析Binder。
- 1.编写AIDL文件需要注意几点规则:
- 1.接口和方法前面不需要加访问权限修饰符,也不能用final,static等等。
- 2.AIDL支持java基本类型,和String,Map,List和CharSequence不需要导包,而自定义的类型需要显示import无论是不是在一个包内。
- 3.AIDL中使用的类型都必须是已经Parcelable。
- 4.AIDL中所有非基本类型的参数前面必须加上in、out、inout、标记以指明参数是输入参数还是输出参数还是输入输出参数。
- 5.如果AIDL中用到了自定义的Parcelable类,那么定义一个和它同名的AIDL文件,并在里面声明它为Parcelable类型。并且这个文件的路径要和那个自定义的类型完全一致。
- 2.Android Studio会自动根据AIDL文件生成相应的Java接口,而接口里除了AIDL中定义的方法,就只用一个抽象类Stub。(这个类继承了Binder并且实现了AIDL文件生成的接口。也就是说其内部有着Binder的能力,能够与不同进程的组件交互的能力(不过这一切都是底层实现好然后封住给开发者用的)然后其还implement了AIDL生成的Java接口,这就是我们的业务逻辑。其内部又包括了一下内容。):
- 1.DESCRIPTION:Binder的唯一标识,表明当前ADIL生成接口的类名。
- 2.多个int值:用来标记AIDL接口的方法。
- 3.Proxy类:实现了AIDL接口的类,里面有个Binder,是Binder类的代理类。Proxy类实现的AIDL接口的方法:这些方法是运行在客户端的,当在客户端调用了这个方法的时候会创建Parcel对象data和reply,然后把该方法的参数写入data中(如果有参数),接着调用Bidner方法的transact()来发起远程调用(RPC)请求,同时当前线程挂起(所以一般这些方法需要放在子线程)。然后调用后面要讲的onTransact(),直到RPC过程返回之后,当前线程继续执行,并且reply取出中写入的结果返回。
- 4.asInterface(IBinder obj):将Service的Binder对象转换成客户端所需要的AIDL接口对象。如果是同一进程的话就是Stub对象本身,如果是不同进程则返回Stub.Proxy对象,即Stub的代理对象。
- 5.asBinder(): 返回当前的Binder对象。
- 6.boolean onTransact(int code,Parcel data,Parcel reply,int flags):该方法运行在Service的Binder线程池中,当客户端发起跨线程请求的时候(即调用前面Proxy的AIDL接口实现方法)经过底层封装最后会调用这个方法。从data中取客户端传的参数(如果客户端有传),通过code和Stub中定义的方法标识来确定调用哪个方法,从reply中写入返回值(如果有返回值)。当onTransact()返回为false的时候,用户端请求失败。
- 3.最后再来介绍Binder了两个方法linkToDeath()和unlinkToDeath()。通过Service与客户端的Binder的linkToDeath(DeathRecipient,int)我们可以注册Binder的死亡监听。如果Service与客户端之间的连接断了,就会调用DeathRecipient的binderDied(),而我们可以重写这个方法来调用Binder的unlinkToDeath()来重新建立连接。
- 1.编写AIDL文件需要注意几点规则:
总的来说AIDL生成的代码可以拆分为三个文件:1.AIDL的java接口。2.同一进程使用的Stub类。3.不同进程使用的Proxy类。当客户端和Service在同一进程的时候,两个部分能够共享内存,此时客户端能够直接使用Service中定义的Stub类,这个时候的交互就和普通交互一样。而当客户端和Service处于不同的进程时,在客户端本地内存中是没有Service中定义的Stub实例的,此时客户端就创建一个Stub.Proxy类,用来代理Stub类,通过onTransact()方法来调用Service中的Stub实例中的方法。所以通过上面的分析,其实不需要ADIL文件,我们自己也可以写出能进程间交互的Binder来。4.Android中的IPC方式
- 1.使用Bundle:Android中的四大组件都可以通过Intent中放入Bundle来传递数据,不过Bundle的传递只能在使用了Intent的时候才能使用。
- 2.使用文件共享:在Android中支持多个线程一起对文件进行并发的读写(尽管这样容易出现问题)。所以以我们可以通过将序列化的类写在文件中,然后让在另一个进程中读取。(这样也容易出问题,因为这样的话必须两个操作有严格的先后顺序,而很多情况下两个进程中的操作是没有严格先后顺序的。)另一个例子就是SharedPreferences,虽然其底层是基于读写xml文件来实现储存的,但是由于其会在内存中保留一份缓存,所以一般不建议在进程中通信使用它。
- 3.使用Messager:由于Messager是系统已经封装好的基于AIDL的轻量级IPC方案,所以我们可以很容易的使用它进行IPC交互,并且由于它一次性处理一个请求,因此在服务端我们不用考虑线程同步的问题。建立一个能够双向传递数据的IPC通道需要下面几个步骤:
- 1.创建一个Service,并且在Service中创建一个Handler对象。
- 2.用一个Handler对象创建一个属于Service的Messager对象。
- 3.在Handler中根据Message.replyTo()获取客户端传过来的Messager对象,然后就可以通过这个Messager对象向客户端发送消息。
- 3.在Service的onBind()对象中返回Messager.getBinder()。
- 4.在客户端中根据返回的Messager.getBinder()创建一个和Service联系的Messager对象。
- 5.然后就可以通过客户端中的Service的Messager对象发送Message实例给Service了。
- 6.而对于接收信息也需要像Service中一样,创建一个Handler。
- 7.根据Handler创建一个属于客户端的Messager,并且要在Message实例中传入属于客户端的Messager。
经过上面的7个步骤就能建立一个IPC双向通道,其实总的来说就是在客户端和Service中各自建立一套属于自己的Messager和Handler,然后通过互相获取各种的Messager来传递Message实例,然后在Handler中解析Message。
- 1.主动调用需要以下步骤
- 1.新建AIDL文件1并在其中定义需要的函数。
- 2.生成AIDL接口1之后,在Service中实现Stub的抽象函数。
- 3.在Service的onBind中返回实现的Stub实例。
- 4.在客户端中根据返回的Stub实例,调用Stub的asInterface。获取AIDL接口1的实现类。
- 5.最后就可以在客户端通过调用AIDL接口的实现类的函数来调用Service的服务了。
- 6.需要注意的是由于Service中AIDL接口的所有方法都是运行在Binder的线程池中的,所以不需要关心阻塞问题,但是需要关心线程同步的问题。(CopyOnWriteArrayList和ConcurrentHashMap能够并发读写,可以有效解决这个问题)
- 2.在客户端监听Service,这个问题其实就是一种观察者模式,也就是说客户端定义一个Listener,然后传给Service当Service发生变化的时候,就回调Listener的方法。只不过这里是跨进程的监听,需要用到的接口也不是普通的接口,而是AIDL接口。在主动调用的基础上我们还需要以下步骤才能进行跨进程监听。
- 1.新建一个AIDL文件2,并在其中定义监听函数。
- 2.在之前定义的AIDL文件1中加入两个新的函数,分别用来注册监听接口和解绑监听接口。
- 3.在Service中建立一个Listener的List以便绑定和解绑。
- 4.实现AIDL文件1新增的两个函数,大概就是在List中添加和删除Listener。
- 5.在客户端中实现AIDL接口2的逻辑并生成Listener实例,通过之前实现的AIDL接口1的实现类注册Listener。
- 6.经过上述步骤之后,在Service中调用Listener的方法就能实现监听。
- 7.需要注意一个问题:我们使用了一个List储存Listenner,当注册添加的时候还行,但是一旦要解绑删除的时候就会出现一个问题:由于Binder在传输的时候,不是保持原来的对象,而是复制出一个数据相同的全新对象。这样一来就不能用一般的List来储存Listener。系统提供了一个RemoteCallBackList专门用来添加和删除跨进程Listener接口。
在讲这个之前,首先提出一个问题:目前我们是有一个AIDL服务就生成一个Service,如果项目复杂起来之后有许多AIDL服务的话,那是不是要生成同等数量的Service呢?Binder连接池就是解决这个问题。让我们无论多少个AIDL服务,都只需要建立一个Service。实现步骤:
- 1.首先,为每个业务模块创建AIDL接口并实现此接口及其业务方法。
- 2.创建IBinderPool的AIDL接口,定义IBinder queryBinder(int BinderCode)方法。外部通过调用此方法传入对应的code值来获取对应的Binder对象。
- 3.创建BinderPoolService,通过new BinderPool.BinderPoolImpl实例化Binder对象,通过onBind方法返回出去。
- 4.创建BinderPool类,单例模式,在构造方法中绑定Service,在onServiceConnected方法获取到BinderPoolImpl对象,这个BinderPoolImpl类是BinderPool的内部类,并实现了IBinderPool的业务方法。BinderPool类中向外暴露了queryBinder方法,这个方法其实调用的是BinderPoolImpl对象的queryBinder方法。
书上的图很清晰。
第三章 View事件体系 1.View的基础知识
- 1.什么是View:略
- 【Android 开发艺术探索笔记 前四章】2.View的位置参数:Android中有两套坐标体系,我们分别来说一下。
- 1.屏幕坐标体系:这套体系是以屏幕的左上角为原点,从原点向右是x轴的正半轴,从原点向下是y轴的正半轴。MotionEvent有以这个坐标系为基准的函数:getRawX(),getRawY()分别获取x,y坐标。
- 2.父视图坐标体系:这套体系是以父视图的左上角为原点,从原点向右是x轴的正半轴,从原点向下是y轴的正半轴。MotionEvent有以这个坐标系为基准的函数:getX(),getY()分别获取x,y坐标。除此之外,子View的位置也是基于这套坐标系。View有getLeft()和getTop(),getRight()和getBoom()这几个函数,分别获取View的左上顶点的x,y坐标,View的右下顶点的x,y坐标。
- 3.需要注意的是:View还有几个参数也是很重要的分别是x、y、translationX、translationY。这几个参数View都提供了get/set方法。x、y是View左上角的坐标(屏幕坐标系)。translationX、translationY是View左上角相对父View的偏移量。有这个公式:View.getX()=View.getLeft()+View.getTranslationX,Y轴也是一样。所以我们可以发现在View移动的过程中getLeft()和getTop()等标识原始左上角位置信息是不会改变的,此时发生改变的是x、y、translationX、translationY这四个参数。
- 3.MotionEvent和TouchSlop
- 1.MotionEvent:这个很简单略过。
- 2.TouchSlop:这个是系统能识别出来的最小滑动距离,和设备有关可以通过ViewConfiguration.get(getContext()).getScaledTouchSlop()获取。
- 4.VelocityTracker、GestureDetector和Scroll
- 1.VelocityTracker:这个类可以用于速度追踪,要使用有以下几个步骤
- 1.调用VelocityTracker.obtain()获取实例。
- 2.通过computeCurrentVelocity(int),设置速度单位,比如输入1000就是以1000毫秒为单位。
- 3.通过getXVelocity()获取速度,如果以步骤2为基础的话那么单位就是:像素/1000毫秒。
- 4.需要提醒的是:向左为负速度,向右为正速度。并且当不需要的时候要调用clear()和recycle()来回收内存。
- 2.GestureDetector:手势检测,用于辅助用户的单击、滑动、双击、长按等行为。
- 1.建立实例并且调用setIslongpressEnabled(false)。
- 2.在目标View的onTouchEvent()方法中调用实例的onTouchEvent(MotionEvent)方法。
- 3.最后我们就可以通过设置OnGestureListener和OnDoubleTapListener这两个监听器,再通过其回调来检测手势了。
- 3.Scroll:后面会讲它,所以先略过。
- 1.VelocityTracker:这个类可以用于速度追踪,要使用有以下几个步骤
- 1.layout():可以重写onTouchEvent(),计算手指偏移量offsetX,offsetY。然后获取View的长和宽,带入到layout()中即可完成滑动。需要注意的是layout()这个方法是瞬间变化的,也就是说如果偏移量过大,会导致类似卡顿的现象。
- 2.offsetLeftAndRight()和offsetTopAndBottom():同方法一,也是计算出offsetX,offsetY,然后分别传入两个方法。偏移过大也会有类似卡顿现象,总的来说就是layout()方法的简单版。
- 3.LayoutParams:这是一个类,保存了View在父View的布局参数,所以需要在父View中获取实例,然后修改参数再传回到父View中。
- 4.scrollBy():也是计算了offsetX,offsetY之后传入,不过这个函数是调用父View的,也就是说是参考系整个发生移动,所以一般计算了偏移量之后需要价格负号。
- 5.scrollTo():这个也是作用于父View上的,不过需要提供左上角坐标,和右下角坐标,4个参数。
- 6.Scroll:这是一个类,可以把一段指定的距离分成微小的间距,最后再调用上面的几个方法进行移动。其实就是代替onTouchEvent()中在MotionEvent.ACTION_MOVE中不断计算。整体需要三个步骤1.创建一个Scroll的实例 2.重写View的computeScroll(),在其中判断滑动是否结束,并且不断调用上面几个方法的一种 3.调用start()传入起始位置,和长距离的offsetX,offsetY。注意有一点就是在computeScroll()最后必须调用invalidate(),因为调用这个就会触发computeScroll()的调用,直到滑动完毕。
- 7.ViewDragHelper:这个方法总的来说就是把父View的触摸事件交给ViewDragHelper来处理,然后让开发者通过其暴露出来的几个接口结合其内部的Scroll类进行子View的滑动。书上讲的是一个侧滑控件,不过讲的不是很完全,有一些回调函数,比如触摸边缘的回调等等都没有涉及。所以建议大家看完这一节,去百度一些博客加深理解。大家可以看看这个我当时看的博客ViewDragHelper
看了这以小结之后真的感觉这是很重要的一节,所以花了点时间找了一些大牛的博客结合本节再加上阅读了源码,画出了一张图:View事件分发源码解析图,大家可以保存下来结合源码和书慢慢啃。
文章图片
我总结了一下View事件分发的几点策略,以下策略都可以在上面的图中找到依据。
- 从手指触摸屏幕到离开屏幕会产生:down->move->…->move->up。这一系列事件,android系统倾向于让某一个View处理这一整个事件序列。
- ViewGroup中,每次down事件都会让ViewGroup的标记重置。
- View一般来说是不需要拦截事件的,因为当事件传递到View的时候,其要么消耗要么返回给父ViewGroup。而ViewGroup如果需要先子View处理事件的话,就可以拦截事件。一旦ViewGroup选择了拦截事件,那么后面同一序列的事件将不会继续下传,都给本ViewGroup处理。(注意:这里有个前提条件就是事件能够传到本ViewGroup中,若其上层的ViewGroup拦截了事件的话,那么同样序列的事件都会给上层ViewGroup处理)
- 若一个ViewGroup决定开始拦截事件的话,那么其onInterceptTouchEvent()方法就不会再调用,这个可以看成(2)的补充。
- 如果最底层的View不消耗down,那么事件会重新返回到其父ViewGroup并调用父ViewGroup的onTouchEvent(),同理可以把父ViewGroup看作View,重复刚刚的行为直到传到Activity的onTouchEvent()。需要注意的是:View不消耗down事件之后,那么同一事件序列的其他事件都不会再交给该View了,而是转交给上一次消耗了down事件的ViewGroup,除非事件在向下传递的时候已经被拦截了。
- 如果底层的View只消耗down事件的话,那么其还是能接受到其他同一序列的事件的。但是此时,同一序列的其他事件就不会再给ViewGroup了,而是直接给Activity的onTouchEvent()
- ViewGroup默认不拦截任何事件即,onInterceptTouchEvent()默认返回false。
- View的onTouchEvent()默认都是会消耗事件的,即默认返回true。除非该View既不可点击,又不可长按。
- View的属性enable不影响onTouchEvent()的返回值,哪怕该View是disable的,只要能点击或者长按onTouchEvent()就会返回true。
- 当android系统处理down事件的时候,会维护一条View链表,这条链表是down事件走过的View形成的。所以在down事件之后,系统无需再遍历当前的View树,寻找事件所在的View区域。
5.View的滑动冲突
- 1.常见的滑动冲突场景:
- 1.内外滑动方向不一致:像这种问题可以通过重写外面的ViewGroup的onIntercreptTouchEvent(),拦截move事件判断是左右滑,还是上下滑。来决定将事件给谁处理。
- 2.内外滑动方向一致:一般这样的情景我们需要确立业务逻辑是怎样的,比如说优先将内部的ViewGroup画到顶部和底部之后再让外部的ViewGroup滑动。确立了逻辑之后就按照第一种办法处理即可。只不过换一下拦截move事件的逻辑而已。
- 3.上面两种情况一起的3重嵌套:这种情况也是优先确立业务逻辑,然后套用方法一更换拦截逻辑。
- 2.拦截的两种方法:
- 1.外部拦截法:顾名思义根据事件传递的规则,在事件传到子View之前先判断是否拦截事件。
- 1.重写onIntercreptTouchEvent()方法。
- 2.当事件为down时不拦截:原因是如果这个时候拦截了事件,那么之后的事件序列就都交由该ViewGroup处理了,而当事件是down的时候我们的所知道的滑动信息是很少的,此时很难判断出是否应该交给ViewGroup处理。
- 3.当事件为move的时候处理逻辑,判断是否应该拦截:此时刚好和down的时候不一样,因为我们已经可以根据滑动的信息判断室是否应该滑动ViewGroup了。
- 4.当事件为up的时候不拦截:假如前面ViewGroup的时候已经拦截过了事件的话,那么此时无需拦截事件,up自然会交由ViewGroup处理。反之如果之前ViewGroup没有拦截事件,那么up事件就传不到View中了,我们前面分析事件分发的时候了解到:View的onClickListener的onClick()事件是在up的时候调用的,所以这时会造成View的点击失效的情况。
- 2.内部拦截法:这种方法主要是通过在子View中设置ViewGroup的FLAG_DISALLOW_INTERCERT标记。因为设置了之后可以让父亲View不拦截任何事件,将所有事件交给View处理,让View决定是否消耗事件,如果不消耗事件则将事件再返回给ViewGroup让其处理。不过这种方法有点违反android系统的事件分发原则,所以建议使用第一种方法来进行外部拦截法。
- 1.外部拦截法:顾名思义根据事件传递的规则,在事件传到子View之前先判断是否拦截事件。
第四章 View的工作原理 1.初识ViewRoot和DecorView
我们都知道Activity中有个Window,Window中添加了一个顶级ViewGroup名为DecorView。在ActivityThread中,当Activity对象被创建完毕之后,会将DecorView添加到Window中,同时创建一个ViewRoot的实现类ViewRootImpl并与DecorView建立关系。而View的绘制就是从ViewRoot的performTraversals()方法开始的上一张View绘制流程框架图
文章图片
由图可以看出几点View绘制的基本策略:
- 1.ViewRootImpl的performTraversala()方法会依次调用performMeasure()、performLayout()和performDraw()来完成DecorView的绘制。
- 2.DecorView是一个ViewGroup,所以其在measure()、layout()和draw()过程中会调用子View的measure()、layout()和draw()方法,从而把绘制传到子View中。
- 3.(2)中说的子View可能是ViewGroup也可能是View,像这样最终会遍历整个View树,完成View的绘制。
- 1.先来说说MeasureSpec这个类:
- 1.View通过两个数据来调整自身的大小分别是mode和size。
- 2.mode分为三种:
- 1.MeasureSpec.EXACTLY,即精确测量模式,当xml中设置layout_width、layout_height和match_parent调用。
- 2.MeasureSpec.AT_MOST,即最大尺寸模式,当xml中设置wrap_content的时候使用。
- 3.MeasureSpec.UNSPECIFIED,不指定测量模式,在自定义View的时候使用。
-
- mode和size是整合在一个32位的int值中的,高2位是mode低30位是size。类MeasureSpec的两个方法能获取mode和size,分别是MeasureSpec.getMode(int)和MeasureSpec.getSize(int)。
- 2.MeasureSpec和LayoutParams的对应关系
- 1.在View测量的时候,系统会将LayoutParams在父View的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View的测量后的宽高。需要注意的是:MeasureSpec并不是由LayoutParams唯一决定的,父View也是会和LayoutParams共同作用出MeasureSpec
- 2.对于顶级View(DecorView)和普通View来说,MeasureSpec的产生略有不同。
- 1.对于DecorView,其MeasureSpec由窗口尺寸和其LayourParams决定。
- 2.而对于普通View则是上面说的:其MeasureSpec由父View的MeasureSpec和自身的LayoutParams决定。
- 3.来具体说一下DecorView的MeasureSpec产生的过程,ViewRootImpl中有个getRootMeasureSpec(int,int)方法,分别传入屏幕尺寸和LayoutParams的宽高类型。最后有如下几种选择:
- 1.当宽高类型为ViewGroup.LayoutParams.MATCH_PARENT时:大小就是窗口大小。
- 2.当宽高类型为ViewGroup.LayoutParams.Wrap_CONTENT时:需要获取子View的大小后才能得出自己的大小,但是不超过屏幕的大小。
- 3.当宽高类型书是上述两种的时候:大小就是LayoutParams中定义的大小。
- 4.再来说说普通View的MeasureSpec产生的过程,View的measure方法是由ViewGroup调用的,所以其MeasureSpec也是由ViewGroup产生的,我们可以在ViewGroup中找到一个getChildMeasureSpec(int,int,int)的函数,传入的参数分别为ViewGroup的MeasureSpec、ViewGroup的两个padding与View的两个margin的和、View的LayoutParams的宽高类型。先上一个图:
文章图片
这个图就是ViewGroup的getChildMeasureSpec(int,int,int)方法的总结表,我再用语言归纳一下:
- 1.首先UNSPECIFIED这一列我们可以不去看它,因为这个模式主要用于系统内部的多次Measure情形,一般来说我们不需要关注此模式。
- 2.这个的childSize是只View的LayoutParams中定义了的大小。这里的parentSize 是指,ViewGroup中可用的大小(注意我们前面说过,在getChildMeasureSpec方法的第二个参数是各种边界值的和,所以这里的可用大小就是ViewGroup从其MeasureSpec中解析出来的size与第二个参数的差。)
- 3.我们可以先来看看第二行的情况:可以得知当我们设置死了View的大小的时候,不管ViewGroup的MeasureSpec的mode是什么样,View都是精确模式,且大小等于在LayoutParams中设置过的大小。
- 4.再来看看第三行:可知当ViewGroup为精确模式的时候,View也为精确模式,且其大小为ViewGroup剩下的大小。当ViewGroup为最大值模式的时候,View也为最大值模式,且大小不超过ViewGroup的大小。
- 5.在来看看最后一行:我们发现不管ViewGroup的模式是精确模式还是最大值模式,View的模式始终是最大值模式且其大小不超过ViewGroup。(在这里有个疑问,我实际操作了一下,发现当ViewGroup为精确模式的时候,内部是View还是TextView的结果是不同的,当为View时View的大小是ViewGroup的大小,当为TextView的时TextView的大小是其内部字的大小。这个问题需要等到后面才能知道。)
- 6.最后再总结一下:在View精确的设置了宽高的时候,ViewGroup是不能影响View的。在View设置为wrap_content的时候,其模式是随着自己的。在View设置为match_parent的时候其模式是随着ViewGroup的。而除了精确设置以外,View的大小都是随着ViewGroup的、。
View的工作流程主要分为三大块:measure、layout和draw。我们一个个来分析。
- 1.measure过程,如果是仅仅是一个View的话,那么通过measure方法就完成了其测量过程,但是如果是一个ViewGroup的话,那么它除了完成自己的测量过程还需要遍历子View来完成所有子View的measure方法,各个子View再去递归执行这个流程,所以我们分成两部分来分析:
- 1.View的measure流程由一个final的方法measure()来完成。在View的measure()方法中会去调用View的onMeasure()方法,而onMeasure()方法中只调用了setMeasureDimension(int,int)方法,这个方法分别传入测量出来的宽和高。而View是通过getDefualtSize(int,int)来获取测量的宽高的。所以我们只要看这个函数就行了。
- 1.首先这个函数传入了getSuggesteMinnimum(Width/Height)()的返回值和(width/height)MeasureSpec。
- 2.getSuggesteMinnimum(Width/Height)()的逻辑是:如果View没有设置背景,那么就返回android:min(Width/Height)这个属性的值,默认为0。如果View设置了背景,则返回android:min(Width/Height)和背景值这两个中的最大值。而getSuggesteMinnimum(Width/Height)()返回的值就是View在UNSPECIFIED模式下测出的宽高。
- 3.我们接着就可以看getDefualtSize(int,int)这个方法了,在这个方法中我们可以看到当specMode为EXACTLY的时候,则返回specSize。当specMode为UNSPECIFIED时则返回我们2中提到的大小。而当specMode为AT_MOST的时候我们发现处理方式和EXACTLY一样。(在这里我们可以解决我在前面提到的View和TextView的疑问了,我们从前面的表可以知道当View中使用wrap_content的时候其specMode是AT_MOST,其specSize等于parentSize。所以此时View设置wrap_content和设置match_parent的效果是完全一样的。而TextView中有对设置成wrap_content的判断,所以最终测量的结果不是parentSize)
- 4.我们通过3可以知道,当我们需要自定义View的时候,需要重写onMeasure()这个方法,给wrap_content设置判断设置一个最小值,要么wrap_content这个设置就不起作用了。
- 2.再来说说ViewGroup的measure:我们前面说了ViewGroup除了measure自己还得measure子View,ViewGroup是一个抽象类,其没有重写View的onMeasure()方法,而是提供了一个measureChildren()的方法。 我们就来分析一下这个方法。
- 1.measureChildren()是遍历子View然后挨个调用measureChild(View,int,int)方法,第一个参数是每个子View,第二个和第三个参数是ViewGroup的measureSpec。
- 2.measureChild(View,int,int)方法的思想很简单:就是取出子View的Layoutparams,然后通过getChildMeasureSpec()来创建子View的MeasureSpec。这个getChildMeasureSpec()在前面已经分析过了,所以现在已经很清晰了。
- 3.我们知道android中有许多不同的布局,所以ViewGroup的onMeasure()方法被设置为一个抽象方法,让不同的布局去实现它。
- 3.我们在2中说了ViewGroup的measure过程,但是没有举出具体的例子,所以这里来说说LinearLayout的onMeasure()方法的流程。其我们都知道LinearLayout有横着和竖着两种模式,所以其onMeasure()方法中也有两种测量方式,我们就以measureVertical()来作为分析。
- 1.该方法首先。会遍历子元素并对每个子元素执行measureChildBeforeLayout()方法,这个方法内部会调用子元素的measure方法。
- 2.系统会在1遍历的时候用mTotalLength这个变量来储存LinearLayout在竖直方向初步的高度,而每测量一个子ViewmTotalLength就会增加子View的高度+子元素在数值方向上的margin等。
- 3.当子View测量完毕之后mTotalLength会加上LinearLayout的padding,然后设置自己的高度,针对竖直LinearLayout而言,它在水平方向的测量过程遵循View的测量过程。而数值方向上如果他采用的是match_parent,则也是和View一样,但是如果是采用wrap_content的话,那么其高度是mTotalLength,当然此时高度不能超过父ViewGroup的剩余高度。
- 4.我们知道当一个View经过了measure()方法之后,其大小一般就是View的最终大小了(这里留下一个疑问:在什么时候测量大小不等于View的大小呢?)。所以我们常常去获取View的测量大小来使用,但是我们可以在什么时候获取测量大小呢?是在Activity的onCreate()、onStart()还是onResume()?大家可以去试试,最终答案是都不行。因为View的measure过程和Activity的生命周期是不同步的,也就是说可能在Activity的onResume()方法时,View的measure还没完成,那就更别说onCreate()和onStart(),而如果View还没measure完的话,那获取的测量大小都是0。下面有4中方法解决这个问题:
- 1.Activity#onWindowFocusChanged():这个方法的在View全部被初始化完毕的时候调用,而且当Activity频繁的执行onResume()和onPause()这两个方法,那么onWindowFocusChanged()就会频繁的被调用。
- 2.View#post(Runnable):这个方式是将消息投递到消息队列的尾部,当Looper调用此Runnable的时候,View也初始化好了,我们可以再Runnable内部获取测量的数值。
- 3.使用ViewTreeObserver的回调来获取。
- 4.在View的measure()方法中手动获取。
- 1.View的measure流程由一个final的方法measure()来完成。在View的measure()方法中会去调用View的onMeasure()方法,而onMeasure()方法中只调用了setMeasureDimension(int,int)方法,这个方法分别传入测量出来的宽和高。而View是通过getDefualtSize(int,int)来获取测量的宽高的。所以我们只要看这个函数就行了。
- 2.layout过程:layout的作用是ViewGroup来确定子View的位置的。当ViewGroup的位置被确定之后,ViewGroup会在onLayout()方法中遍历所有子元素并调用子元素的layout()方法,然后layout()方法再调用onLayout()方法,像这样循环一直等到整个View树全部被Layout。由于ViewGroup的layout()方法是继承于View的,所以我们只需要看View的layout()方法就可以了。
- 1.在ViewGroup调用View的layout()时,会传入四个int参数,分别为距离ViewGroup左边,顶上,右边,下面的距离。
- 2.layout()内部首先会调用setFrame()来通过layout()传入的四个参数来设置View的四个定点在ViewGroup中的位置。接着会调用onLayout()方法,这个方式是为了让父View确定子View的位置而写的。而确定子View的位置方式与具体的ViewGroup有关,所以View和ViewGroup都没有onLayout()的具体实现
- 3.我们来看看LinearLayout中的onLayout()是怎么实现了:
- 1.根据模式的不同,onLayout()内调用的方法也不同,我们这里说说当为VERTICAL模式的时,会调用layoutVertical()传入四个int参数。
- 2.layoutVertical()方法也很简单,就是遍历子View,然后计算出子View距LinearLayout四个边缘的距离,然后调用setChildFrame(),将四个参数传进去。
- 3.setChildFrame()内部就是调用子View的layout()方法。值得注意的是,这里用到了measure完毕之后产生的View的宽高,用于计算View的四边距离ViewGroup的四边的距离。而我们最终的View的大小就是,这里传入的参数两两的差值。所以我们可以解决上面所说的疑问,在什么时候measure的大小不等于View的大小呢?我们可以自己定义layout()方法中传递进去的四个参数,这样一来的话,测量出来的大小就和最终View的大小不一样了,但是这样没有任何实际用处。
- 3.draw过程:draw的过程就简单了主要就是以下几步
- 1.drawBackground(canvas):画背景,如果需要的话
- 2.onDraw(canvas):绘制自己
- 3.dispatchDraw(canvas):绘制children
- 4.onDrawScrollBars(canvas):绘制装饰
- 1.自定义View的分类:
- 1.继承View重写onDraw()方法:采用这种方式的时候记得实现wrap_content这种模式原因之前讲过了,还有就是padding也可以处理一下。
- 2.继承ViewGroup:这种ViewGroup实现起来比较复杂:我们必须根据自己的需求重写onMeasure()和onLayout()这两个方法,并且处理好子View的测量和布局。
- 3.继承特定的View(如TextView)还有ViewGroup(比如LinearLayout):这种方法还是比较简单的。
- 2.自定义View的须知:
- 1.尽量不要在View中使用Handle来发送消息,因为View内部已经提供了post系列的方法来代替Handle。
- 2.View中如果有动画或者线程的话需要及时停止:包括但不限于包含此View的Activity退出了、View被remove了或者是View不可见了等等。而在这个时候View#onDetachedWindow()会被调用,所以这是一个退出的好时机。与之相对的则是View#onAttachedWindow()方法。
- 3.当View中有滑动的时候,必须处理好滑动冲突,具体参见事件分发那一小节。
- 3.自定义属性
- 1.在attr文件中用declare-styleable标签定义属性
- 2.在构造函数中定义TypedArray ta=Context.obtainStyleAttributes(AttributeSet,int),获取刚刚定义的属性
- 3.调用ta的函数获取在xml中定义的属性值
- 4.调用ta.recycle(),回收资源。
- 4.View的孪生兄弟——SurfaceView
- 1.首先使用SurfaceView的前提是,需要自定义View并且这个自定义View刷新频繁,适用于画板等需要展现实时画面动态的自定义View
- 2.使用SurfaceView需要定义几个变量:SurfaceHolder、Canvas、Runnale、Boolean。
- 3.在自定义的View中每个SurfaceView中已经有SurfaceHolder了所以直接调用getHolder()即可。
- 4.SurfaceHolder中有Canvas,通过lockCanvas()获得。可以通过在这个Canvas上直接画图来使SurfaceView发生变化。
- 5.而Runnale的作用就是开辟一个循环的线程,不断调用Canvas进行绘制,这样就能频繁的刷新自定义测View了。
- 6.最后一个Boolean则是起flag的作用,用来判断是否在线程中进行绘制。
推荐阅读
- Android源码|okhttp源码分析(一)——基本流程(超详细)
- okhttp源码分析(四)-ConnectInterceptor过滤器
- Android|BaseCanvas
- Android笔记|Retrofit2.5是如何解析在接口类中定义的方法()