要实现可以拖动的View该怎么做?
还是接着刚才的btn例子,如果要修改btn的位置,使用updateViewLayout即可,然后在ontouch方法中传入移动的坐标即可。
btn.setOnTouchListener { v, event -> val index = event.findPointerIndex(0) when (event.action) { ACTION_MOVE -> { windowParams.x = event.getRawX(index).toInt() windowParams.y = event.getRawY(index).toInt() windowManager.updateViewLayout(btn, windowParams) } else -> { } } false }
Window的添加、删除和更新过程。
Window的操作都是通过WindowManager
来完成的,而WindowManager是一个接口,他的实现类是WindowManagerImpl
,并且全部交给WindowManagerGlobal
来处理。下面具体说下addView,updateViewLayout,和removeView。
1)addView
//WindowManagerGlobal.java public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { if (parentWindow != null) { parentWindow.adjustLayoutParamsForSubWindow(wparams); } ViewRootImpl root; View panelParentView = null; root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view); mRoots.add(root); mParams.add(wparams); try { root.setView(view, wparams, panelParentView); } } }
- 这里可以看到,创建了一个ViewRootImpl实例,这样就说明了每个Window都对应着一个ViewRootImpl。
- 然后通过add方法修改了
WindowManagerGlobal
中的一些参数,比如mViews—存储了所有Window所对应的View,mRoots——所有Window所对应的ViewRootImpl,mParams—所有Window对应的布局参数。 - 最后调用了ViewRootImpl的setView方法,继续看看。
final IWindowSession mWindowSession; mWindowSession = WindowManagerGlobal.getWindowSession(); public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { // requestLayout(); res = mWindowSession.addToDisplay(mWindow,); }
setView
方法主要完成了两件事,一是通过requestLayout方法完成异步刷新界面的请求,进行完整的view绘制流程。其次,会通过IWindowSession进行一次IPC调用,交给到WMS来实现Window的添加。
其中mWindowSession是一个Binder对象,相当于在客户端的代理类,对应的服务端的实现为Session,而Session就是运行在SystemServer进程中,具体就是处于WMS服务中,最终就会调用到这个Session的addToDisplay方法,从方法名就可以猜到这个方法就是具体添加Window到屏幕的逻辑,具体就不分析了,下次说到屏幕绘制的时候再细谈。
2)updateViewLayout
public void updateViewLayout(View view, ViewGroup.LayoutParams params) { //... final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params; view.setLayoutParams(wparams); synchronized (mLock) { int index = findViewLocked(view, true); ViewRootImpl root = mRoots.get(index); mParams.remove(index); mParams.add(index, wparams); root.setLayoutParams(wparams, false); } }
这里更新了WindowManager.LayoutParams
和ViewRootImpl.LayoutParams
,然后在ViewRootImpl内部同样会重新对View进行绘制,最后通过IPC通信,调用到WMS的relayoutWindow完成更新。
3)removeView
public void removeView(View view, boolean immediate) { if (view == null) { throw new IllegalArgumentException("view must not be null"); } synchronized (mLock) { int index = findViewLocked(view, true); View curView = mRoots.get(index).getView(); removeViewLocked(index, immediate); if (curView == view) { return; } throw new IllegalStateException("Calling with view " + view + " but the ViewAncestor is attached to " + curView); } } private void removeViewLocked(int index, boolean immediate) { ViewRootImpl root = mRoots.get(index); View view = root.getView(); if (view != null) { InputMethodManager imm = view.getContext().getSystemService(InputMethodManager.class); if (imm != null) { imm.windowDismissed(mViews.get(index).getWindowToken()); } } boolean deferred = root.die(immediate); if (view != null) { view.assignParent(null); if (deferred) { mDyingViews.add(view); } } }
该方法中,通过view找到mRoots
中的对应索引,然后同样走到ViewRootImpl
中进行View删除工作,通过die
方法,最终走到dispatchDetachedFromWindow()
方法中,主要做了以下几件事:
- 回调onDetachedFromeWindow。
- 垃圾回收相关操作;
- 通过Session的remove()在WMS中删除Window;
- 通过Choreographer移除监听器
Activity、PhoneWindow、DecorView、ViewRootImpl 的关系?
看完上面的流程,我们再来理理这四个小伙伴之间的关系:
- PhoneWindow 其实是 Window 的唯一子类,是 Activity 和 View 交互系统的中间层,用来管理View的,并且在Window创建(添加)的时候就新建了ViewRootImpl实例。
- DecorView 是整个 View 层级的最顶层,ViewRootImpl是DecorView 的parent,但是他并不是一个真正的 View,只是继承了ViewParent接口,用来掌管View的各种事件,包括requestLayout、invalidate、dispatchInputEvent 等等。
Window中的token是什么,有什么用?
token?又是个啥呢?刚才window操作过程中也没出现啊。
token其实大家应该工作中会发现一点踪迹,比如application的上下文去创建dialog的时候,就会报错:
unable to add window --token null
所以这个token跟window操作是有关系的,翻到刚才的addview方法中,还有个细节我们没说到,就是adjustLayoutParamsForSubWindow方法。
//Window.java void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) { if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW && wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) { //子Window if (wp.token == null) { View decor = peekDecorView(); if (decor != null) { wp.token = decor.getWindowToken(); } } } else if (wp.type >= WindowManager.LayoutParams.FIRST_SYSTEM_WINDOW && wp.type <= WindowManager.LayoutParams.LAST_SYSTEM_WINDOW) { //系统Window } else { //应用Window if (wp.token == null) { wp.token = mContainer == null ? mAppToken : mContainer.mAppToken; } } }
上述代码分别代表了三个Window的类型:
- 子Window。需要从decorview中拿到token。
- 系统Window。不需要token。
- 应用Window。直接拿mAppToken,mAppToken是在setWindowManager方法中传进来的,也就是新建Window的时候就带进来了token。
然后在WMS中的addWindow方法会验证这个token,下次说到WMS的时候再看看。
所以这个token就是用来验证是否能够添加Window,可以理解为权限验证,其实也就是为了防止开发者乱用context创建window。
拥有token的context(比如Activity)就可以操作Window。没有token的上下文(比如Application)就不允许直接添加Window到屏幕(除了系统Window)。
Application中可以直接弹出Dialog吗?
这个问题其实跟上述问题相关:
- 如果直接使用Application的上下文是不能创建Window的,而Dialog的Window等级属于子Window,必须依附与其他的父Window,所以必须传入Activity这种有window的上下文。
- 那有没有其他办法可以在Application中弹出dialog呢?有,改成系统级Window:
//检查权限 if (!Settings.canDrawOverlays(this)) { val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION) intent.data = Uri.parse("package:$packageName") startActivityForResult(intent, 0) } dialog.window.setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG) <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
- 另外还有一种办法,在Application类中,可以通过registerActivityLifecycleCallbacks监听Activity生命周期,不过这种办法也是传入了Activity的context,只不过在Application类中完成这个工作。
关于事件分发,事件到底是先到DecorView还是先到Window的?
经过上述一系列问题,是不是对Window印象又深了点呢?最后再看一个问题,这个是wanandroid论坛上看到的(https://wanandroid.com/wenda/show/12119),
这里的window可以理解为PhoneWindow,其实这道题就是问事件分发在Activity、DecorView、PhoneWindow中的顺序。
当屏幕被触摸,首先会通过硬件产生触摸事件传入内核,然后走到FrameWork层(具体流程感兴趣的可以看看参考链接),最后经过一系列事件处理到达ViewRootImpl的processPointerEvent方法,接下来就是我们要分析的内容了:
//ViewRootImpl.java private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; ... //mView分发Touch事件,mView就是DecorView boolean handled = mView.dispatchPointerEvent(event); ... } //DecorView.java public final boolean dispatchPointerEvent(MotionEvent event) { if (event.isTouchEvent()) { //分发Touch事件 return dispatchTouchEvent(event); } else { return dispatchGenericMotionEvent(event); } } @Override public boolean dispatchTouchEvent(MotionEvent ev) { //cb其实就是对应的Activity final Window.Callback cb = mWindow.getCallback(); return cb != null && !mWindow.isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); } //Activity.java public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); } //PhoneWindow.java @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } //DecorView.java public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
事件的分发流程就比较清楚了:
ViewRootImpl——>DecorView——>Activity——>PhoneWindow——>DecorView——>ViewGroup
(这其中就用到了getCallback参数,也就是之前addView中传入的callback,也就是Activity本身)
但是这个流程确实有些奇怪,为什么绕来绕去的呢,光DecorView就走了两遍。
参考链接中的说法我还是比较认同的,主要原因就是解耦。
- ViewRootImpl并不知道有Activity这种东西存在,它只是持有了DecorView。所以先传给了DecorView,而DecorView知道有AC,所以传给了AC。
- Activity也不知道有DecorView,它只是持有PhoneWindow,所以这么一段调用链就形成了。
怎么理解Binder?
在java层面,其实Binder
就是一个实现了IBinder
接口的类。
真正跨进程的部分还是在客户端发起远程调用请求之后,系统底层封装好,交给服务端的时候。而这个系统底层封装,其实就是发生在Linux内核
中。
而在内核中完成这个通信关键功能的还是Binder
,这次不是Binder
类了,而是Binder驱动
。
驱动你可以理解为一种硬件接口,可以帮助操作系统来控制硬件设备。
Binder驱动
被添加运行到Linux内核空间
,这样,两个不同进程就可以通过访问内核
空间来完成数据交换:把数据传给Binder驱动
,然后处理好再交给对方进程,完成跨进程通信。
而刚才通过AIDL
的例子我们可以知道,客户端在请求服务端通信的时候,并不是直接和服务端的某个对象联系,而是用到了服务端的一个代理对象,通过对这个代理对象操作,然后代理类会把方法对应的code、传输的序列化数据、需要返回的序列化数据
交给底层,也就是Binder驱动。
然后Binder驱动把对应的数据交给服务器端,等结果计算好之后,再由Binder驱动
把数据返回给客户端。
最后借用《Android开发艺术探索》书中的内容总结下,希望大家回味回味。
直观的说,Binder是一个类,实现了IBinder接口。 从IPC(进程间通信)角度来说,Binder是Android中一种跨进程通信方式。 还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder。 从Android FrameWork角度来说,Binder是ServiceManager连接各种Manager(ActivityManager,WindowManager等等)和响应ManagerService的桥梁。 从Android应用层来说,Binder是客户端和服务端进行通信的媒介。
怎么理解ServiceManager
ServiceManager
其实是为了管理系统服务而设置的一种机制,每个服务注册在ServiceManager
中,由ServiceManager
统一管理,我们可以通过服务名在ServiceManager
中查询对应的服务代理,从而完成调用系统服务的功能。所以ServiceManager有点类似于DNS,可以把服务名称和具体的服务记录在案,供客户端来查找。
在我们这个AIDL的案例中,能直接获取到服务端的Service
,也就直接能获取到服务端的代理类IMsgManager
,所以就无需通过ServiceManager
这一层来寻找服务了。
而且ServiceManager
本身也运行在一个单独的线程,所以它本身也是一个服务端,客户端其实是先通过跨进程获取到ServiceManager
的代理对象,然后通过ServiceManager
代理对象再去找到对应的服务。
而ServiceManager
就像我们刚才AIDL中的Service一样,是可以直接找到的,他的句柄永远是0
,是一个“众所周知”的句柄,所以每个APP程序都可以通过binder机制在自己的进程空间中创建一个ServiceManager
代理对象。
所以通过ServiceManager
查找系统服务并调用方法的过程是进行了两次跨进程通信。
APP进程——>ServiceManager进程——>系统服务进程(比如AactivityManagerService)
网络通信的过程,以及中间用了什么协议
这个问题我之前专门做了一个动画,大家可以翻到上一篇文章看看:
再简单总结下:
客户端:
- 1、在浏览器输入网址
- 2、浏览器解析网址,并生成
http
请求消息 - 3、浏览器调用系统解析器,发送消息到DNS服务器查询域名对应的
ip
- 4、拿到ip后,和请求消息一起交给操作系统协议栈的
TCP模块
- 5、将数据分成一个个数据包,并加上TCP报头形成
TCP数据包
- 6、TCP报头包括发送方端口号、接收方端口号、数据包的
序号、ACK号
。 - 7、然后将
TCP消息
交给IP模块。 - 8、IP模块会添加
IP头部
和MAC头部
。 - 9、IP头部包括
IP地址
,为IP模块使用,MAC头部包括MAC地址,为数据链路层使用。 - 10、
IP模块
会把整个消息包交给网络硬件,也就是数据链路层,比如以太网,WIFI等 - 11、然后网卡会将这些包转换成
电信号或者在光信号
,通过网线或者光纤发送出去,再由路由器等转发设备送达接收方。
服务器端:
- 1、数据包到达服务器的
数据链路层
,比如以太网,然后会将其转换为数据包(数字信号)交给IP模块
。 - 2、
IP模块
会将MAC头部和IP头部后面的内容,也就是TCP数据包发送给TCP模块。 - 3、
TCP模块
会解析TCP头信息,然后和客户端沟通表示收到这个数据包了。 - 4、
TCP模块
在收到消息的所有数据包之后,就会封装好消息,生成相应报文发给应用层,也就是HTTP层。 - 5、
HTTP层
收到消息,比如是HTML数据,就会解析这个HTML数据,最终绘制到浏览器页面上。
TCP连接过程,三次握手和四次挥手,为什么?
连接阶段(三次握手):
- 创建套接字
Socket
,服务器会在启动的时候就创建好,客户端是在需要访问服务器的时候创建套接字 - 然后发起连接操作,其实就是Socket的
connect
方法 - 这时候客户端会生成一个TCP数据包。这个数据包的TCP头部有几个重要信息:
SYN、ACK、Seq、Ack
。
SYN,同步序列编号,是TCP/IP建立连接时使用的握手信号,如果这个值为1就代表是连接消息。ACK,确认标志,如果这个值为1就代表是确认消息。Seq,数据包序号,是发送数据的一个顺序编号。Ack Number,确认数字号,是接收数据的一个顺序编号。
- 所以客户端就生成了这样一个数据包,其中头部信息的控制位
SYN
设置为1,代表连接。SEQ
设置一个随机数,代表初始序号,比如100。 - 然后服务器端收到这个消息,知道了客户端是要来连接的
(SYN=1)
,知道了传输数据的初始序号(SEQ=100)
。 - 服务器端也要生成一个数据包发送给客户端,这个数据包的TCP头部会包含:表示我也要连接你的
SYN(SYN=1)
,我已经收到了你的上个数据包的确认号ACK=1(Ack=Seq+1=101)
,以及服务器端随机生成的一个序号Seq(比如Seq=200)
。 - 最后客户端收到这个消息后,表示客户端到服务器的连接是无误了,然后再发送一个数据包表示也确认收到了服务器发来的数据包,这个数据包的头部就主要就是一个
ACK=1(Ack=Seq+1=201)
。 - 至此,连接成功,三次握手结束,后面数据就会正常传输,并且每次都要带上TCP头部中的
Seq和Ack
。
这里有个问题是关于为什么需要三次握手
?
最主要的原因就是需要通信双方都确认自己的消息被准确传达过去了。
A发送消息给B,B回一条消息表示我收到了,这个过程就保证了A的通信能力。B发送消息给A,A回一条消息表示我收到了,这个过程就保证了B的通信能力。
也就是四条消息能保证双方的消息发送都是正常的,其中B回消息和B发消息,可以融合为一次消息,所以就有了三次握手
。
数据传输阶段:
数据传输阶段有个改变就是Ack确认号
不再是Seq+1
了,而是Seq+数据长度
。例如:
- A发送给B的数据包(Seq=100,长度=1000字节)
- B回给A的数据包(Ack=100+1000=1100)
这就是一次数据传输的头部信息,Ack
代表下个数据包应该从哪个字节开始所以等于上个数据包的Seq+长度,Seq就等于上个数据包的Ack。
当然,TCP通信是双向的,所以实际数据每个消息都会有Seq和Ack
:
- A发送给B的数据包(Ack=200,Seq=100,长度=1000字节)
- B回给A的数据包(Ack=100+1000=1100,Seq=上一个数据包的Ack=200,长度=500字节)
- A发送给B数据包(Seq=1100,Ack=200+500=700)
断开阶段(四次挥手):
和连接阶段一样,TCP头部也有一个专门用作关闭连接的值叫做FIN。
- 客户端准备关闭连接,会发送一个
TCP数据包
,头部信息中包括(FIN=1代表要断开连接)
。 - 服务器端收到消息,回复一个数据包给客户端,头部信息中包括
Ack确认号
。但是此时服务器端的正常业务可能没有完成,还要处理下数据,收个尾。 - 客户端收到消息。
- 服务器继续处理数据。
- 服务器处理数据完毕,准备关闭连接,会发送一个
TCP数据包
给客户端,头部信息中包括(FIN=1代表要断开连接) - 客户端端收到消息,回复一个数据包给服务器端,头部信息中包括
Ack确认号
。 - 服务器收到消息,到此服务器端完成连接关闭工作。
- 客户端经过一段时间(2MSL),自动进入
关闭状态
,到此客户端完成连接关闭工作。
MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
这里有个问题是关于为什么需要四次挥手?
A发送断开消息给B,B回一条消息表示我收到了,这个过程就保证了A断开成功。B发送断开消息给A,A回一条消息表示我收到了,这个过程就保证了B断开成功。
其实和连接阶段的区别就在于,这里的B的确认消息和断开消息不能融合
。因为A要断开的时候,B可能还有数据要处理要发送,所以要等正常业务处理完,在发送断开消息。