系统为什么提供Handler
- 这点大家应该都知道一些,就是为了切换线程,主要就是为了解决在子线程无法访问UI的问题。
那么为什么系统不允许
在子线程中访问UI呢?
- 因为
Android
的UI控件不是线程安全的,所以采用单线程模型来处理UI操作,通过Handler切换UI访问的线程即可。
那么为什么不给UI控件加锁
呢?
- 因为加锁会让
UI
访问的逻辑变得复杂,而且会降低UI
访问的效率,阻塞线程执行。
Handler是怎么获取到当前线程的Looper的
- 大家应该都知道
Looper
是绑定到线程上的,他的作用域就是线程,而且不同线程具有不同的Looper
,也就是要从不同的线程取出线程中的Looper
对象,这里用到的就是ThreadLocal
。
假设我们不知道有这个类,如果要完成这样一个需求,从不同的线程获取线程中的Looper
,是不是可以采用一个全局对象,比如hashmap
,用来存储线程和对应的Looper
?所以需要一个管理Looper
的类,但是,线程中并不止这一个要存储和获取的数据,还有可能有其他的需求,也是跟线程所绑定的。所以,我们的系统就设计出了ThreadLocal
这种工具类。
ThreadLocal
的工作流程是这样的:我们从不同的线程可以访问同一个ThreadLocal
的get方法,然后ThreadLocal
会从各自的线程中取出一个数组,然后再数组中通过ThreadLocal
的索引找出对应的value值。具体逻辑呢,我们还是看看代码,分别是ThreadLocal
的get方法和set方法:
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
首先看看set方法,获取到当前线程,然后取出线程中的threadLocals
变量,是一个ThreadLocalMap
类,然后将当前的ThreadLocal
作为key,要设置的值作为value
存到这个map中。
get方法
就同理了,还是获取到当前线程,然后取出线程中的ThreadLocalMap
实例,然后从中取到当前ThreadLocal
对应的值。
其实可以看到,操作的对象都是线程中的ThreadLocalMap
实例,也就是读写操作都只限制在线程内部,这也就是ThreadLocal
故意设计的精妙之处了,他可以在不同的线程进行读写数据而且线程之间互不干扰。
画个图方便理解记忆:
当MessageQueue 没有消息的时候,在干什么,会占用CPU资源吗。
MessageQueue
没有消息时,便阻塞在 loop 的queue.next()
方法这里。具体就是会调用到nativePollOnce方法里,最终调用到epoll_wait()
进行阻塞等待。
这时,主线程会进行休眠状态,也就不会消耗CPU资源。当下个消息到达的时候,就会通过pipe管道写入数据然后唤醒主线程进行工作。
这里涉及到阻塞和唤醒的机制叫做 epoll 机制
。
先说说文件描述符和I/O多路复用:
在Linux操作系统中,可以将一切都看作是文件,而文件描述符简称fd,当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符,可以理解为一个索引值。
I/O多路复用是一种机制,让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作
所以I/O
多路复用其实就是一种监听读写的通知机制,而Linux提供的三种 IO 复用方式分别是:select、poll 和 epoll
。而这其中epoll
是性能最好的多路I/O就绪通知方法。
所以,这里用到的epoll
其实就是一种I/O多路复用方式,用来监控多个文件描述符的I/O事件。通过epoll_wait
方法等待I/O事件,如果当前没有可用的事件则阻塞调用线程。
Binder是什么
先借用神书《Android开发艺术探索》中的一段话:
直观的说,Binder是一个类,实现了IBinder接口。 从IPC(Inter-Process Communication,进程间通信)角度来说,Binder是Android中一种跨进程通信方式。 还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder。 从Android FrameWork角度来说,Binder是ServiceManager连接各种Manager(ActivityManager,WindowManager等等)和响应ManagerService的桥梁。 从Android应用层来说,Binder是客户端和服务端进行通信的媒介。
挺多概念的是吧,其实就说了一件事,Binder
就是用来进程间通信的,是一种IPC
方式。后面所有的解释都是Binder
实际应用涉及到的内容。
不管是获取其他的系统服务,亦或是服务端和客户端的通信,都是源于Binder
的进程间通信能力。
Binder通信过程和原理
首先,还是看一张图,原图也是出自神书中:
首先要明确的是客户端进程是无法直接操作服务端中的类和方法的,因为不同进程直接是不共享资源的。所以客户端这边操作的只是服务端进程的一个代理对象,也就是一个服务端的类引用,也就是Binder
引用。
总体通信流程
就是:
- 客户端通过代理对象向服务器发送请求。
- 代理对象通过
Binder
驱动发送到服务器进程 - 服务器进程处理请求,并通过
Binder
驱动返回处理结果给代理对象 - 代理对象将结果返回给客户端。
再看看在我们应用中常常用到的工作模型
,上图:
这就是在应用层面我们常用的工作模型,通过ServiceManager
去获取各种系统进程服务。这里的通信过程如下:
- 服务端跨进程的类都要继承
Binde
r类,所以也就是服务端对应的Binder
实体。这个类并不是实际真实的远程Binder
对象,而是一个Binder
引用(即服务端的类引用),会在Binder
驱动里还要做一次映射。 - 客户端要调用远程对象函数时,只需把数据写入到Parcel,在调用所持有的
Binder
引用的transact()
函数 transact
函数执行过程中会把参数、标识符(标记远程对象及其函数)等数据放入到Client的共享内存,Binde
r驱动从Client的共享内存中读取数据,根据这些数据找到对应的远程进程的共享内存。- 然后把数据拷贝到远程进程的共享内存中,并通知远程进程执行onTransact()函数,这个函数也是属于
Binder
类。 - 远程进程
Binder
对象执行完成后,将得到的写入自己的共享内存中,Binder驱动再将远程进程的共享内存数据拷贝到客户端的共享内存,并唤醒客户端线程。
所以通信过程中比较重要的就是这个服务端的Binder引用
,通过它来找到服务端并与之完成通信。
看到这里可能有的人疑惑了,图中线程池
怎么没用到啊?
- 可以从第一张图中看出,
Binder线程池
位于服务端,它的主要作用就是将每个业务模块的Binder请求统一转发到远程Servie中去执行,从而避免了重复创建Service的过程。也就是服务端只有一个,但是可以处理多个不同客户端的Binder
请求。
在Android中的应用
Binder在Android中的应用除了刚才的ServiceManager
,你还想到了什么呢?
- 系统服务是用过
getSystemService
获取的服务,内部也就是通过ServiceManager
。例如四大组件的启动调度等工作,就是通过Binder机制传递给ActivityManagerService,再反馈给Zygote
。而我们自己平时应用中获取服务也是通过getSystemService(getApplication().WINDOW_SERVICE)
代码获取。 AIDL(Android Interface definition language)
。例如我们定义一个IServer.aidl文件,aidl工具会自动生成一个IServer.java的java接口类(包含Stub,Proxy等内部类)。- 前台进程通过
bindService
绑定后台服务进程时,onServiceConnected(ComponentName name, IBinder service)传回IBinder对象,并且可以通过IServer.Stub.asInterface(service)获取IServer的内部类Proxy的对象,其实现了IServer接口。
Binder优势
在Linux中,进程通信的方式肯定不止Binder这一种,还有以下这些:
管道(Pipe) 信号(Signal) 消息队列(Message) 共享内存(Share Memory) 套接字(Socket) Binder
而Binder
在这之后主要有以下优点:
性能高,效率高
:传统的IPC(套接字、管道、消息队列)需要拷贝两次内存、Binder只需要拷贝一次内存、共享内存不需要拷贝内存。安全性好
:接收方可以从数据包中获取发送发的进程Id和用户Id,方便验证发送方的身份,其他IPC想要实验只能够主动存入,但是这有可能在发送的过程中被修改。
熟悉Zygote
的朋友可能知道,在fork()进程的时候,也就是向Zygote进程发出创建进程的消息的时候,用到的进程间通信方式就不是Binder了,而换成了Socket,这主要是因为fork不允许存在多线程,Binder
通讯偏偏就是多线程。
所以具体的情况还是要去具体选择合适的IPC方式。
讲一下RecyclerView的缓存机制,滑动10个,再滑回去,会有几个执行onBindView。缓存的是什么?cachedView会执行onBindView吗?
RecyclerView预取机制
这两个问题都是关于缓存的,我就一起说了。
1)首先说下RecycleView的缓存结构:
Recycleview有四级缓存,分别是mAttachedScrap(屏幕内),mCacheViews(屏幕外),mViewCacheExtension(自定义缓存),mRecyclerPool(缓存池)
mAttachedScrap(屏幕内)
,用于屏幕内itemview快速重用,不需要重新createView和bindViewmCacheViews(屏幕外)
,保存最近移出屏幕的ViewHolder,包含数据和position信息,复用时必须是相同位置的ViewHolder才能复用,应用场景在那些需要来回滑动的列表中,当往回滑动时,能直接复用ViewHolder数据,不需要重新bindView。mViewCacheExtension(自定义缓存)
,不直接使用,需要用户自定义实现,默认不实现。mRecyclerPool(缓存池)
,当cacheView满了后或者adapter被更换,将cacheView中移出的ViewHolder放到Pool中,放之前会把ViewHolder数据清除掉,所以复用时需要重新bindView。
2)四级缓存按照顺序需要依次读取。所以完整缓存流程是:
- 保存缓存流程:
- 插入或是删除
itemView
时,先把屏幕内的ViewHolder保存至AttachedScrap
中 - 滑动屏幕的时候,先消失的itemview会保存到
CacheView
,CacheView大小默认是2,超过数量的话按照先入先出原则,移出头部的itemview保存到RecyclerPool缓存池
(如果有自定义缓存就会保存到自定义缓存里),RecyclerPool缓存池会按照itemview的itemtype
进行保存,每个itemType缓存个数为5个,超过就会被回收。
- 获取缓存流程:
- AttachedScrap中获取,通过pos匹配holder——>获取失败,从
CacheView
中获取,也是通过pos获取holder缓存 ——>获取失败,从自定义缓存
中获取缓存——>获取失败,从mRecyclerPool
中获取 ——>获取失败,重新创建viewholder
——createViewHolder并bindview。
3)了解了缓存结构和缓存流程,我们再来看看具体的问题 滑动10个,再滑回去,会有几个执行onBindView?
- 由之前的缓存结构可知,需要重新执行
onBindView
的只有一种缓存区,就是缓存池mRecyclerPool
。
所以我们假设从加载RecyclView
开始盘的话(页面假设可以容纳7条数据):
- 首先,7条数据会依次调用
onCreateViewHolder
和onBindViewHolder
。 - 往下滑一条(position=7),那么会把position=0的数据放到
mCacheViews
中。此时mCacheViews
缓存区数量为1,mRecyclerPool
数量为0。然后新出现的position=7的数据通过postion在mCacheViews
中找不到对应的ViewHolder
,通过itemtype
也在mRecyclerPool
中找不到对应的数据,所以会调用onCreateViewHolder
和onBindViewHolder
方法。 - 再往下滑一条数据(position=8),如上。
- 再往下滑一条数据(position=9),position=2的数据会放到
mCacheViews
中,但是由于mCacheViews
缓存区默认容量为2,所以position=0的数据会被清空数据然后放到mRecyclerPool
缓存池中。而新出现的position=9数据由于在mRecyclerPool
中还是找不到相应type的ViewHolder,所以还是会走onCreateViewHolder
和onBindViewHolder
方法。所以此时mCacheViews
缓存区数量为2,mRecyclerPool
数量为1。 - 再往下滑一条数据(position=10),这时候由于可以在
mRecyclerPool
中找到相同viewtype的ViewHolder了。所以就直接复用了,并调用onBindViewHolder
方法绑定数据。 - 后面依次类推,刚消失的两条数据会被放到
mCacheViews
中,再出现的时候是不会调用onBindViewHolder方法,而复用的第三条数据是从mRecyclerPool
中取得,就会调用onBindViewHolder
方法了。
4)所以这个问题就得出结论了(假设mCacheViews
容量为默认值2):
- 如果一开始滑动的是新数据,那么滑动10个,就会走10个
bindview
方法。然后滑回去,会走10-2个bindview
方法。一共18次调用。 - 如果一开始滑动的是老数据,那么滑动10-2个,就会走8个
bindview
方法。然后滑回去,会走10-2个bindview
方法。一共16次调用。
但是但是,实际情况又有点不一样。因为Recycleview
在v25版本引入了一个新的机制,预取机制
。
预取机制
,就是在滑动过程中,会把将要展示的一个元素提前缓存到mCachedViews
中,所以滑动10个元素的时候,第11个元素也会被创建,也就多走了一次bindview
方法。但是滑回去的时候不影响,因为就算提前取了一个缓存数据,只是把bindview
方法提前了,并不影响总的绑定item数量。
所以滑动的是新数据的情况下就会多一次调用bindview
方法。
5)总结,问题怎么答呢?
- 四级缓存和流程说一下。
- 滑动10个,再滑回去,
bindview
可以是19次调用,可以是16次调用。 - 缓存的其实就是缓存item的view,在Recycleview中就是
viewholder
。 cachedView
就是mCacheViews
缓存区中的view,是不需要重新绑定数据的。
如何实现RecyclerView的局部更新,用过payload吗,notifyItemChange方法中的参数?
关于RecycleView的数据更新,主要有以下几个方法:
notifyDataSetChanged()
,刷新全部可见的item。*notifyItemChanged(int)
,刷新指定item。notifyItemRangeChanged(int,int)
,从指定位置开始刷新指定个item。notifyItemInserted(int)、notifyItemMoved(int)、notifyItemRemoved(int)
。插入、移动一个并自动刷新。notifyItemChanged(int, Object)
,局部刷新。
可以看到,关于view的局部刷新就是notifyItemChanged(int, Object)方法,下面具体说说:
notifyItemChange
有两个构造方法:
- notifyItemChanged(int position, @Nullable Object payload)
- notifyItemChanged(int position)
其中payload
参数可以认为是你要刷新的一个标示,比如我有时候只想刷新itemView
中的textview
,有时候只想刷新imageview
?又或者我只想某一个view的文字颜色进行高亮设置?那么我就可以通过payload
参数来标示这个特殊的需求了。
具体怎么做呢?比如我调用了notifyItemChanged(14,"changeColor")
,那么在onBindViewHolder
回调方法中做下判断即可:
@Override public void onBindViewHolder(ViewHolderholder, int position, List<Object> payloads) { if (payloads.isEmpty()) { // payloads为空,说明是更新整个ViewHolder onBindViewHolder(holder, position); } else { // payloads不为空,这只更新需要更新的View即可。 String payload = payloads.get(0).toString(); if ("changeColor".equals(payload)) { holder.textView.setTextColor(""); } } }
RecyclerView嵌套RecyclerView滑动冲突,NestScrollView嵌套RecyclerView。
1)RecyclerView
嵌套RecyclerView
的情况下,如果两者都要上下滑动,那么就会引起滑动冲突。默认情况下外层的RecycleView可滑,内层不可滑。
之前说过解决滑动冲突的办法有两种:内部拦截法和外部拦截法。这里我提供一种内部拦截法,还有一些其他的办法大家可以自己思考下。
holder.recyclerView.setOnTouchListener { v, event -> when(event.action){ //当按下操作的时候,就通知父view不要拦截,拿起操作就设置可以拦截,正常走父view的滑动。 MotionEvent.ACTION_DOWN,MotionEvent.ACTION_MOVE -> v.parent.requestDisallowInterceptTouchEvent(true) MotionEvent.ACTION_UP -> v.parent.requestDisallowInterceptTouchEvent(false) } false}
2)关于ScrclerView
的滑动冲突还是同样的解决办法,就是进行事件拦截。还有一个办法就是用Nestedscrollview
代替ScrollView
,Nestedscrollview
是官方为了解决滑动冲突问题而设计的新的View。它的定义就是支持嵌套滑动的ScrollView。
所以直接替换成Nestedscrollview
就能保证两者都能正常滑动了。但是要注意设置RecyclerView.setNestedScrollingEnabled(false)
这个方法,用来取消RecyclerView本身的滑动效果。
这是因为RecyclerView默认是setNestedScrollingEnabled(true)
,这个方法的含义是支持嵌套滚动的。也就是说当它嵌套在NestedScrollView
中时,默认会随着NestedScrollView
滚动而滚动,放弃了自己的滚动。所以给我们的感觉就是滞留、卡顿。所以我们将它设置为false就解决了卡顿问题,让他正常的滑动,不受外部影响。
参考
https://www.jianshu.com/p/1dab927b2f36https://juejin.im/post/6844903748574117901https://juejin.im/post/6844903729414537223https://blog.csdn.net/quwei3930921/article/details/85336552https://www.jianshu.com/p/aac6fcfae1e8https://mp.weixin.qq.com/s/wy9V4wXUoEFZ6ekzuLJySQhttps://www.cnblogs.com/hustcser/p/10228843.html