参考:
反思 | Android 事件分发机制的设计与实现 【掘金】(本文其实相当于是对它的读后感)
Android 全面解析之window机制【掘金】(主要是讲window的)
Android 之window机制token验证 (wms和token相关)
Android 系统源码分析 - 事件分发(这里对Native层整个过程讲的比较细)
输入系统:进程间双向通信(socketpair+binder) (这个对事件分发Native层用的原理讲的很清楚)
一、概述
反思 | Android 事件分发机制的设计与实现 【掘金】里说
Android 系统中将输入事件定义为
InputEvent
,而InputEvent
根据输入事件的类型又分为了KeyEvent
和MotionEvent
,前者对应键盘事件,后者则对应屏幕触摸事件,这些事件统一由系统输入管理器InputManager
进行分发。
UI层级的事件分发,只是应用层级的事件分发的一小部分。ViewRootImpl
从InputManager
获取到新的输入事件时,会针对输入事件通过一个复杂的 **责任链 **进行底层的递归,将不同类型的输入事件(比如 屏幕触摸事件 和 键盘输入事件 )进行不同策略的分发,而只有部分符合条件的 屏幕触摸事件 最终才有可能进入到UI层级的事件分发
二、基础概念
InputEvent
InputEvent
是一个基类,它有两个子类:KeyEvent
和 MotionEvent
,后者对应的就是触摸事件。实际的触摸方式可能有很多,手指、笔、甚至华为的隔空触屏,它们全都抽象成了 MotionEvent
。
// 输入事件的基类
public abstract class InputEvent implements Parcelable { }
public class KeyEvent extends InputEvent implements Parcelable { }
public final class MotionEvent extends InputEvent implements Parcelable { }
InputManager(Native)
InputManager
是 Android 系统的输入管理器,它负责分发 InputEvent
。这是一个 native 层级的类,注意不要跟 java 层的同名类搞混了。
InputManager::InputManager(
const sp<EventHubInterface>& eventHub,
const sp<InputReaderPolicyInterface>& readerPolicy,
const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {
mDispatcher = new InputDispatcher(dispatcherPolicy);
mReader = new InputReader(eventHub, readerPolicy, mDispatcher);
initialize();
}
InputManagerService
java 层初始化和访问这个 native 层InputManager
是通过InputManagerService
,它也是运行在 SystemServer 进程里的系统级 Service,它的初始化时机是 WindowManagerService 是一起的,同时后者会持有前者的引用 。
public final class SystemServer {
private void startOtherServices() {
// 初始化 InputManagerService
InputManagerService inputManager = new InputManagerService(context);
// WindowManagerService 持有了 InputManagerService
WindowManagerService wm = WindowManagerService.main(context, inputManager,...);
inputManager.setWindowManagerCallbacks(wm.getInputMonitor());
inputManager.start();
}
}
public class InputManagerService extends IInputManager.Stub {
public InputManagerService(Context context) {
// ...通知native层初始化 InputManager
// 这个 ptr 应该跟MessageQueue里面的那个一样,是指向Native层对象的指针
mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
}
// native 函数
private static native long nativeInit(InputManagerService service, Context context, MessageQueue messageQueue);
}
Window(Java)
这里指的是 Java 层中的类 window
,它的唯一子类是 phoneWindow
,实际上它并不是我们常说的 window 机制中的 window。
同时它有一个管理类(管理的意思是管理PhoneWindow
内部的一些逻辑)也就是 WindowManager
,它的唯一实现是 WindowManagerImpl
。但是实际上这个类只是一个代理,它实际的逻辑是在 WindowManagerGlobal
中完成的,这是一个单例类,里面统一存放了本进程所有的 ViewrRootImpl
等。当然,对于外部来说,它们是只知道 WindowManagerImpl
的,这就是面向对象。
这些概念这篇文章讲的很好:Android 全面解析之window机制【掘金】
window 本身并不存在,他只是一个概念。… 因他不存在,所以也很难从源码中找到他的痕迹,window机制的操作单位都是view,如果要说他在源码中的存在形式,笔者目前的认知就是在WindowManagerService中每一个view对应一个windowStatus。
…
总结一下:
PhoneWindow本身不是真正意义上的window,他更多可以认为是辅助Activity操作window的工具类。
windowManagerImpl并不是管理window的类,而是管理PhoneWindow的类。真正管理window的是WMS。
PhoneWindow可以配合DecorView可以给其中的window按照一定的逻辑提供标准的UI策略。
PhoneWindow限制了不同的组件添加window的权限。
ViewRootImpl
这个类非常关键,它是连接 WindowManager
和 decorView
的桥梁,事件经过它实现从 window 到 view 的传递。同时它继承了 ViewParent
,是整个 view 树的根部,除了事件分发,view 测量绘制都由它来控制,重要性不言而喻。
三、建立通信
ViewRootImpl 的创建
根据上面的概念陈述,我们已经可以知道 ViewRootImpl
是非常重要的一个类,那么它是什么时候创建并进行初始化的呢?这个问题对于理解整个事件和绘制体系都非常关键。
PhoneWindow
和 decorView
的创建
在 Activity
的启动过程中,AMS
会通过Binder
调用 ApplicationThread
的相关方法(Api28之前是scheduleLaunchActivity
,之后全部生命周期都是回调scheduleTransaction
),然后调用到 AcitivityThread.performLaunchActivity
,在这个方法里面,会生成 Activity
的实例对象,同时生成对应的 PhoneWindow
、 WindowManagerImpl
和 decorView
对象。
ViewRootImpl
的创建
然后在回调 onResume
方法的 AcitivityThread.performResumeActivity
方法里,会调用 WindowManagerImpl.addview
方法,传入decorView
(onCreate 中已经添加了 ContentView 进去了) 和 WindowManager.LayoutParams
参数。
在这个方法里,完成了对 ViewRootImpl
的初始化,并调用 ViewRootImpl.setView
,这个方法非常重要。
// 1.WindowManager 的本质实际上是 WindowManagerImpl
public final class ActivityThread {
//...
@Override
public void handleResumeActivity(...){
//...
windowManager.addView(decorView, windowManagerLayoutParams);
}
}
public final class WindowManagerImpl implements WindowManager {
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
// 2.实际上调用了 WindowManagerGlobal.addView()
WindowManagerGlobal.getInstance().addView(...);
}
}
public final class WindowManagerGlobal {
public void addView(...) {
// 3.初始化 ViewRootImpl,并执行setView()函数
ViewRootImpl root = new ViewRootImpl(view.getContext(), display);
root.setView(view, wparams, panelParentView);
}
}
public final class ViewRootImpl {
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
// 4.该函数就是控测量(measure)、布局(layout)、绘制(draw)的开始
requestLayout();
// ...
// 5.此外还有通过Binder建立通信,这个下文再提
}
}
通信的建立
ViewRootImpl 创建完成了,但是系统输入服务(InputManagerService)和用户进程之间的连接是如何做到的呢?这里就会用到 InputChannel 了,这是一个 Pipe ,实际是一个 Socket。
Android 系统源码分析 - 事件分发 中的图:
ViewRootImpl.setView
在执行的过程中,会创建 InputChannel
(java),它实现了 Parcelable
,所以它是可以通过 Binder 进程跨进程传输的。
mWindowSession
是一个binder 代理对象,它的真实实现是 SystemServer
进程里面的 Session
类,这里会调用它的addToDisplay
方法。
最终这个方法会走到 WMS.addWindow
,实际上:每一个应用程序进程都会对应一个 Session
,它在调用WMS.addWindow
时,会将自己作为参数传入方法中, WMS 会用 一个List 来保存这些 Session
。
//ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
//...
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
//...
}
//...
}
}
//Session.java
public class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
final WindowManagerService mService;
//...
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outOutsets, outInputChannel);
}
//...
}
Window 的 token:
Activity
的 token 是在 attach 的时候,由 AMS 传给Activity
的,它会把它保存在 PhoneWindow
的 mWindowAttribute
变量中。
而子窗口(例如PopupWindow
)的token是父窗口的ViewRootImpl
中的 w(IWindow
对应的 Binder 对象)对象。所以需要父窗口对应的 ViewRootImpl
调用完 setView
方法(onResume
之后),将 w 存入 WMS 的 mWindowMap
了,才能展示。
//Activity
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//不会报错
Dialog dialog = new Dialog(this);
dialog.addContentView(new FrameLayout(this),
new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT));
dialog.show();
//报错:Unable to add window -- token null is not valid; is your activity running?
PopupWindow popupWindow = new PopupWindow(this);
popupWindow.setContentView(new FrameLayout(this));
popupWindow.showAsDropDown(LayoutInflater.from(this).inflate(R.layout.activity_main,null));
}
需要注意的是Dialog
实际上不是子窗口,它有自己的PhoneWindow
,其类型是TYPE_APPLICATION
,它跟 Activity
是共用的 WindowManager
,同时其中的 parentWindow
变量正是 Activity
的 PhoneWindow
,所以它在显示的过程中其实用的是 Activity
的 token 。
另外按常规流程来说,Activity
的 token,是在 attach 之后在ActivityStack.startActivityLocked
中, 才存入 WMS 的 mWindowMap
的。
//dialog
Dialog(@NonNull Context context, @StyleRes int themeResId,
boolean createContextThemeWrapper) {
//..
//这里实际拿到的是activity的windowManager
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
final Window w = new PhoneWindow(mContext);
mWindow = w;
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
//这里实际会走到下面那个方法
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
//WindowManagerImpl
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
//这个parentWindow实际就是activity的phoneWindow
return new WindowManagerImpl(mContext, parentWindow);
}
//WindowManagerGlobal
//dialog在show的时候也会走到这,具体就不说了,应该都知道了
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
//..
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
//这里是关键,会用parentWindow去初始化子window的LayoutParams
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
//...
}
}
//window
//注意这个方法是 parentWindow 调用,传入的是window的LayoutParams
void adjustLayoutParamsForSubWindow(WindowManager.LayoutParams wp) {
CharSequence curTitle = wp.getTitle();
if (wp.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wp.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
//这里是判断子window的,例如PopupWindow ...
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) {
//...
}else{
//这里是 dialog 的,使用跟 activity 一样的token
if (wp.token == null) {
wp.token = mContainer == null ? mAppToken : mContainer.mAppToken;
}
//...
}
//...
}
-----注意!下面的内容笔者并未完全搞懂,所以可能会有错漏的地方---------------------
系统进程:
WMS.addWindow
通过一系列的调用,会执行 InputChannel.nativeOpenInputChannelPair
,这是一个Native方法,它会创建两个 InputChannel
(Nativie),其中一个是与Client通信的。InputChannel
会创建两个 Socket
,组成一个 SocketPair
(为啥要建两个?个人觉得是因为常规Pipe 的一端一般只能负责读或者只负责写,也就是一个 Server 一个 Client,而这里要双向通信),由此就可以进行双向通信了。
有关 SocketPair
可以看看这篇文章:输入系统:进程间双向通信(socketpair+binder)
为什么是Socket而不是Binder?
参考:Android Input子系统为什么要使用socket,而不是binder?
个人理解有以下三点:
1、binder 是同步的调用,如果要实现 socket 异步读和写,那么需要在用户进程和系统进程都新增两个线程去做处理,这样开销当然会更大。
2、即使两端都新开了线程,由于binder调用是有内置的binder线程池的,两次调用不一定是在同一个线程,这就无法保证事件分发的有序性。
3、同样的,binder线程池还有一个问题就是它是有大小限制的,高频操作下,可能会出现创建大量binder线程的情况,那线程池的线程资源必然是不够用的。
总结一下,笔者感觉,次数频繁切需要异步进行的操作,不适合binder
到这里已经,大概知道系统进程是怎么建立通信和发送事件的了,那么用户进程又做了什么处理,如何去接收事件呢?
用户进程:
ViewRootImpl.setView
在 mWindowSession.addToDisplay
调用之后,会初始化一个 WindowInputEventReceiver
对象。
这是 ViewRootImpl
的一个内部类,初始化时候会调用 nativeInit
方法,它根据传入的 InputChannel
和MessageQueue
,会在 Native 层创建一个NativeInputEventReceiver
。后者将会拿 InputChannel
对象的 fd(用户进程和系统进程会是同一个fd) 去添加 epoll
监控,监听系统进程端是否有事件产生并且写入 socket 了。
有的话就开启 socket 去读出来之后,再经过一系列的调用,最后会通过JNI回调到 WindowInputEventReceiver
的 onInputEvent
方法,这个方法又会调用 ViewRootImpl
的方法enqueueInputEvent
。至此,事件终于到了 ViewRootImpl
中了。
在 ViewRootImpl
中处理完成之后,会调用InputEventReceiver
的 nativeFinishInputEvent
方法,结束应用层的事件分发,逻辑再度进入 native 层中进行。
public final class ViewRootImpl {
final class WindowInputEventReceiver extends InputEventReceiver {
@Override
public void onInputEvent(InputEvent event, int displayId) {
// 将输入事件加入队列
enqueueInputEvent(event, this, 0, true);
}
// 这实际它的父类 InputEventReceiver 的方法,放一起好看一些
public final void finishInputEvent(InputEvent event, boolean handled) {
//...
//调用native层方法,结束应用层的本次事件分发
nativeFinishInputEvent(mReceiverPtr, seq, handled);
}
}
}
四、应用层的事件分发
ViewRootImpl.enqueueInputEvent 从名字就能看出这是一个队列,当事件加入队列之后,就是事件的分发了。对于一个输入事件来说,有资格处理这个事件的成员必然是有多个的,这里用了 责任链 模式去实现判断和处理事件。单一功能、输入输出确定、多步骤或角色,毫无疑问这个地方是适合使用责任链的。
InputStage
这个类是事件分发责任链的节点基类,它有很多个子类,分别承担不同的功能。
// 事件分发不同阶段的基类
abstract class InputStage {
private final InputStage mNext; // 指向事件分发的下一阶段
}
// InputStage的子类,象征事件分发的各个阶段
final class ViewPreImeInputStage extends InputStage {}
final class EarlyPostImeInputStage extends InputStage {}
// 通常讲的 UI 层事件分发
final class ViewPostImeInputStage extends InputStage {}
final class SyntheticInputStage extends InputStage {}
abstract class AsyncInputStage extends InputStage {}
final class NativePreImeInputStage extends AsyncInputStage {}
final class ImeInputStage extends AsyncInputStage {}
final class NativePostImeInputStage extends AsyncInputStage {}
然后再简要说明一下各个 InputStage 的逻辑和作用,内容引用自:Android Framework 输入子系统 (09)InputStage解读
NativePreImeInputStage:
分发早于IME的InputEvent到NativeActivity中去处理, NativeActivity和普通acitivty的功能一致,不过是在native层实现,这样执行效率会更高,同时NativeActivity在游戏开发中很实用(不支持触摸事件)。ViewPreIMEInputStage:
分发早于IME的InputEvent到View框架处理,会调用view(输入焦点)的onkeyPreIme方法,同时会给View在输入法处理key事件之前先得到消息并优先处理,View系列控件可以直接复写onKeyPreIme( 不支持触摸事件)。ImeInputStage:
分发InputEvent到IME处理调用 ImeInputStage 的onProcess,InputMethodManager 的dispatchInputEvent 方法处理消息(不支持触摸事件)。EarlyPostImeInputStage:
与touchmode相关,比如你的手机有方向键,按方向键会退出touchmode,这个事件被消费,有可能会有view的背景变化,但不确定(支持触摸事件)。NativePostImeInputStage:
分发InputEvent事件到NativeActivity,IME处理完消息后能先于普通Activity处理消息(此时支持触摸事件)。ViewPostImeInputStage:
分发InputEvent事件到View框架,view的事件分发(支持触摸事件)。SyntheticInputStage:
未处理的 InputEvent 最后的综合处理。
这里可以看到有一个 ViewPostImeInputStage
,这个就是我们通常所说的“事件分发”的入口,当然通过前面的知识我们可以知道,那只是 UI 层级的事件分发,它的分发对象是 View ,后面我们会详细分析它。
同时,我们也能明白一个道理,如果一个 InputEvent
在到达 ViewPostImeInputStage
之前就被处理了,那么 View 层级上的那套事件分发,当然也就无从谈起了。
final class ViewPostImeInputStage extends InputStage {
public ViewPostImeInputStage(InputStage next) {
super(next);
}
@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
final int source = q.mEvent.getSource();
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
// 就是这里了
return processPointerEvent(q);
} else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
return processTrackballEvent(q);
} else {
return processGenericMotionEvent(q);
}
}
}
}
组装责任链
前面讲了责任链里面有哪些 InputStage
节点,那么这些节点又是在哪个时机,如何组装起来的呢?答案还是ViewRootImpl.setView
方法,这个方法出现了很多次了,重要性可想而知,我们整体来看一下它的逻辑:
public final class ViewRootImpl implements ViewParent {
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
// ...
// 1.开始根布局的绘制流程
requestLayout();
// 2.通过Binder建立双端的通信
res = mWindowSession.addToDisplay(...)
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper());
// 3.对责任链进行组装
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);
mFirstInputStage = nativePreImeStage;
mFirstPostImeInputStage = earlyPostImeStage;
// ...
}
}
ViewPostImeInputStage
这是 View 层级的事件分发体系的入口,但是别急,它不会直接进入到我们一般熟知的三件套方法:dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent,中间还有再绕一圈。
final class ViewPostImeInputStage extends InputStage {
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
mAttachInfo.mUnbufferedDispatchRequested = false;
mAttachInfo.mHandlingPointerEvent = true;
// 看起来直接到View的事件分发三件套了?其实还没有
boolean handled = mView.dispatchPointerEvent(event);
maybeUpdatePointerIcon(event);
maybeUpdateTooltip(event);
mAttachInfo.mHandlingPointerEvent = false;
if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
mUnbufferedInputDispatch = true;
if (mConsumeBatchedInputScheduled) {
scheduleConsumeBatchedInputImmediately();
}
}
return handled ? FINISH_HANDLED : FORWARD;
}
}
DecorView
代码里面的 mView.dispatchPointerEvent
,这个mView
我们跟一下就可以知道它是 decorView
,也就是 View 树的根节点。
调用了此方法之后,并不会立即进入 View 层级的事件分发,它会将事件传给 Activity
,然后 Activity
将事件传给 Window
(PhoneWindow
),然后 Window
再将事件分发给 decorView
(调用其 superDispatchTouchEvent
方法,其实也就是 dispatchTouchEvent
),由此才真正进入 View 层级的事件分发逻辑。
为什么要这样绕一圈呢?这里我觉得 反思 | Android 事件分发机制的设计与实现 【掘金】里说的非常好,直接引用过来:
事件绕了一个圈子最终回到了
DecorView
这里,对于初次阅读这段源码的读者来说,这里的设计平淡无奇,似乎说它莫名其妙也不过分。事实上这里是 面向对象程序设计 中灵活运用 多态 这一特征的有力体现——对于DecorView
而言,它承担了2个职责:
1.在接收到输入事件时,DecorView
不同于其它View,它需要先将事件转发给最外层的 Activity,使得开发者可以通过重写Activity.onTouchEvent()
函数以达到对当前屏幕触摸事件拦截控制的目的,这里DecorView
履行了自身(根节点)特殊的职责;
2.从Window
接收到事件时,作为View树的根节点,将事件分发给子View,这里DecorView
履行了一个普通的View的职责。实际上,不只是
DecorView
,接下来View层级的事件分发中也运用到了这个技巧,对于ViewGroup
的事件分发来说,其本质是递归思想的体现。
在 递流程 中,其本身被视为上游的ViewGroup
,需要自定义dispatchTouchEvent
函数,并调用child.dispatchTouchEvent
将事件分发给下游的子 View;
同时,在 归流程 中,其本身被视为一个View,需要调用View自身的方法以决定是否消费该事件(super.dispatchTouchEvent
),并将结果返回上游,直至回归到View树的根节点,至此整个UI树事件分发流程结束。
四、总结
这篇文章基本将Android 的 Window 体系和 一个事件从 native 到 java 层的传递流程讲了一遍,当然中间很多很多概念和实现都没有深入,事实上目前我也没有能力去深入搞懂整个流程中的每一个环节,所以也只能用这种“不求甚解”的方式,先梳理完成整个事件分发的结构了。
这样梳理下来,其实要理解 Android的事件分发体系,衔接 Window 和 View 的 ViewRootImpl 是关键中的关键,一定要充分的去理解它的作用。
最后其实没有讲 View 层的事件分发体系了,因为在另外一篇文章《Android View 层级的事件分发体系——源码探究》有细致梳理过一遍,那篇文章虽然写的是比较烂的,但是也是跟着 《Android 开发艺术探索》这本书的内容走的,此书对 View 层的事件分发差不多讲到极致了。
最后,本文一开始列出来的参考文章,每一篇都是挺值得一看的,本文也要很多内容是来自于这些文章,如果有原作者觉得侵权,可以联系笔者删除。