View绘制流程-Window创建
前言:
View绘制流程中,主要流程是这样的:
1.用户进入页面,首先创建和绑定Window;
2.首次创建以及后续vsync信号来临时,会请求执行刷新流程;
3.刷新流程完成后,会通知SurfaceFlinger读取数据以及刷新页面。
本篇就是大流程中的第一个环节,重点讲解进入页面后,Window是如何创建以及绑定到系统侧的。
本文的流程主要分为以下三大块:
1.APP侧window和布局的创建流程;
2.APP侧window是如何绑定ViewRootImpl以及注册到系统侧的;
3.系统侧接收到window后,是如何处理的。
一.APP侧Window和View创建
1.1 创建Window
Activity启动时,会经历performLaunchActivity和handleResumeActivity的流程,而window的创建以及decorView的创建,就是在launch的过程中。
我们首先看一下ActivityThread.performLaunchActivity中的代码:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {...//Activity的创建Activity activity = null;activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);//Activity的关联activity.attach();//执行Activity的onCreate流程mInstrumentation.callActivityOnCreate()...
}
我们看一下activity.attach中实现的相关内容:
//android.app.Activity
final void attach(){//1mWindow = new PhoneWindow();//3mWindow.setWindowManager((WindowManager)context.getSystemService(Context.WINDOW_SERVICE),...);mWindow.setColorMode(info.colorMode);
}//com.android.internal.policy.PhoneWindow.java
public PhoneWindow(@UiContext Context context) {public PhoneWindow(@UiContext Context context) {super(context);mLayoutInflater = LayoutInflater.from(context);}
}
主要执行了以下的逻辑:
1.创建了Activity所绑定的Window,成员名为mWindow,类型为PhoneWindow。
2.在PhoneWindow中,mLayoutInflater赋值。我们的布局就是通过mLayoutInflater对象去解析的。
3.给Window对象绑定WindowManager,这个WindowManager实际上是WindowManagerImpl。
所以此时,Activity中的mWindow,以及PhoneWindow中的mWindowManager和mLayoutInflater都已经有值了。
1.2 DecorView和ContentParent创建
接下来,我们看下callActivityOnCreate的流程。Activity.onCreate流程没有什么有关window的逻辑,但是一般我们都会在onCreate中调用setContentView,这个方法中却大有玄机,我们一起看一下:
//android.app.Window.java
public void setContentView(@LayoutRes int layoutResID) {//1getWindow().setContentView(layoutResID);
}//com.android.internal.policy.PhoneWindow
public void setContentView(int layoutResID) {if (mContentParent == null) {//2installDecor();}...//3mLayoutInflater.inflate(layoutResID, mContentParent);
}private void installDecor() {if(mDecor == null){mDecor = generateDecor(-1);}if (mContentParent == null) {mContentParent = generateLayout(mDecor);}
}
主要执行了以下的逻辑:
1.调用Activity中所持有window去加载layout。
2.首次的时候,通过installDecor方法去创建根布局DecorView以及容器布局mContentParent。mContentParent是Activity上所有的View的父容器。
3.通过mLayoutInflater对象去解析生成布局对象,并且关联到mContentParent上。
具体的解析逻辑不是本文的核心,这里就不去细讲了。
1.3 小结
至此,Activity的创建和其onCreate的流程已经结束,此时Activity中的成员变量mWindowManager和mWindow对象已经完成了赋值,总结一下,如下图所示:
二.APP侧Window注册
2.1 Activity和ActivityClientRecord中成员变量赋值
在第一章中,Activity所对应的window及其中的布局创建完成了,所以下一步,就是需要把这个window向系统做一个绑定,这个流程,主要是在Activity的onResume周期中执行的。
首先,我们仍然看一下resume周期所对应的代码,如下:
//ActivityThread.java
public void handleResumeActivity(ActivityClientRecord r, ...) {final Activity a = r.activity;...if (r.window == null && !a.mFinished && willBeVisible) {r.window = r.activity.getWindow();...//1a.mDecor = decor;//2l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;//3wm.addView(decor, l);}
}
主要执行了以下的逻辑:
1.把window中的decor赋值给Activity中的mDecor;
2.设置Window.LayoutParams的type类型为WindowManager.LayoutParams.TYPE_BASE_APPLICATION;在安卓中,type决定window图层优先级,值越大优先级越高,部分图层优先级如下:
public static final int TYPE_BASE_APPLICATION = 1;//默认Activity对应的图层
public static final int FIRST_SYSTEM_WINDOW = 2000;//系统弹窗的图层
public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;//Toast的图层
public static final int TYPE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+6;//悬浮窗的图层等级
public static final int TYPE_APPLICATION_OVERLAY = FIRST_SYSTEM_WINDOW + 38;//同上,用于替代上面那个
3.通过windowManager添加decor。这里wm的对象,实际上是WindowManagerImpl,而其中的addView方法中,又交给了WindowManagerGlobal来处理,相关代码如下:
//WindowManagerImpl.java
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {mGlobal.addView(view, params,...);
}
2.2 WindowManagerGlobal装载Window
接下来,我们看一下WindowManagerGlobal.addView()中的逻辑。
public void addView(View view, ViewGroup.LayoutParams params,Display display, Window parentWindow, int userId) {ViewRootImpl root;View panelParentView = null;...//1if (windowlessSession == null) {root = new ViewRootImpl(view.getContext(), display);} else {root = new ViewRootImpl(view.getContext(), display, windowlessSession);}view.setLayoutParams(wparams);//2mViews.add(view);mRoots.add(root);mParams.add(wparams);//3root.setView(view, wparams, panelParentView, userId);
}
主要执行了以下的逻辑:
1.创建ViewRootImpl,ViewRootImpl的角色是页面刷新显示流程的执行者。
2.WindowManagerGlobal的角色是维护客户端所有的页面的,所以自然而然的,其中就维护了很多集合。比如存储所有根布局的mView对象等等,这里就是往集合中注册的。
@UnsupportedAppUsage
private final ArrayList<View> mViews = new ArrayList<View>();
@UnsupportedAppUsage
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
@UnsupportedAppUsage
private final ArrayList<WindowManager.LayoutParams> mParams =new ArrayList<WindowManager.LayoutParams>();
3.上面说到,ViewRootImpl是流程的具体执行者,那么window的绑定自然也是交给其来处理。所以这里通过ViewRootImpl.setView方法来负责。
2.3 ViewRootImpl负责视图的绑定
接下来,我们就看下ViewRootImpl的setView()方法。
ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView, int userId) {...if (mView == null) {mView = view;...//1requestLayout();//2InputChannel inputChannel = null;if ((mWindowAttributes.inputFeatures& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {inputChannel = new InputChannel();}//3res = mWindowSession.addToDisplayAsUser(...);}//mInputEventReceiver = new WindowInputEventReceiver(inputChannel,Looper.myLooper());
}
这一块的逻辑也是较为清晰的:
首先,通过requestLayout方法尝试进行首次View绘制的完整流程,虽然这时window还没有绑定上,但是并不影响View流程的开始,毕竟View流程中,只有最后的绘制流程才需要和SurfaceFlinger进行交互。
然后,生成InputChannel对象,这个对象类似于一个回调,通过后面的binder接口传递给系统侧。后面window上的点击事件,就会通过InputChannel回调通知到应用侧。后面把inputChannel绑定到WindowInputEventReceiver中,所以APP侧点击事件的来源,就是其中的onInputEvent方法。
最后,把相关的对象传递给系统侧,完成注册。传递的内容如下:
int addToDisplayAsUser(IWindow window, in WindowManager.LayoutParams attrs,in int viewVisibility, in int layerStackId, in int userId,in InsetsVisibilities requestedVisibilities, out InputChannel outInputChannel,out InsetsState insetsState, out InsetsSourceControl[] activeControls);
int addToDisplayAsUser(IWindow window, in WindowManager.LayoutParams attrs, in int viewVisibility, in int layerStackId, in int userId, in InsetsVisibilities requestedVisibilities, out InputChannel outInputChannel, out InsetsState insetsState, out InsetsSourceControl[] activeControls);
整个流程如下图所示:
给系统侧传递的数据列表如下:
对象类型 | 成员变量名 | 解释 |
IWindow | window | 对应ViewRootImpl中的IWindow.Stub,传递的一个binder对象 |
WindowManager.LayoutParams | attrs | window对应的layoutParams属性 |
int | viewVisibility | 根布局的显示状态 |
int | layerStackId | displayId,显示区域的唯一ID |
int | userId | 应用的userId |
InsetsVisibilities | requestedVisibilities | |
InputChannel | outInputChannel | 事件分发流程中,传递的通道 |
InsetsState | insetsState | |
InsetsSourceControl | activeControls |
三.系统侧Window绑定
介绍系统侧的流程前,我们先对系统侧的几个核心类简单介绍下,因为大多数的读者对于系统侧的了解较少。
3.1 核心类介绍
类名 | 功能介绍 |
com.android.server.wm.Session | 一个session对应一个应用进程,负责应用和系统之间的窗口注册/移除,SurfaceSession注册等等。 |
SurfaceSession | 这个类注册是native的实现。负责维护应用和surfaceFlinger之间的连接。所以,APP刷新时是直接通知SF,并不需要经过system_server。 |
WindowManagerService | 顾名思义,用于所有应用的窗口管理。这里只是维护窗口的关系,并负责具体的渲染流程。 |
3.2 应用进程绑定唯一的IWindowSession
上一章有讲到,一个应用会有一个维护所有的视图的容器WindowManagerGlobal,那么它其中,一定有一个负责和系统侧通信的对象,这个对象就是IWindowSession。相关代码如下:
//WindowManagerGlobal.java
public static IWindowSession getWindowSession() {if (sWindowSession == null) {IWindowManager windowManager = getWindowManagerService();sWindowSession = windowManager.openSession(...);}
}//WindowManagerService.java
@Override
public IWindowSession openSession(IWindowSessionCallback callback) {return new Session(this, callback);
}
也就是说,WindowManagerGlobal中只会持有一个sWindowSession对象,而WindowManagerGlobal对应一个应用的进程,所以IWindowSession是绑定唯一一个应用进程的。IWindowSession是一个binder的引用,其在系统侧的具体实现是Session。上面的addToDisplayAsUser方法,就是通过IWindowSession中提供的binder方法。
3.3 把window注册到系统侧
接下来我们就看一下第二中讲到的addToDisplayAsUser()方法,它负责把应用侧的Window向系统侧注册。我们看一下其在系统侧的实现:
//Session.java
class Session extends IWindowSession.Stub{public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities,InputChannel outInputChannel, InsetsState outInsetsState,InsetsSourceControl[] outActiveControls) {return mService.addWindow(this, ...);}
}
逻辑很简单,直接交给WindowServiceManger的addWindow方法去处理,接下来我们就看下这个方法:
//WindowManagerService.java
public int addWindow(Session session, ...) {WindowState parentWindow = null;...//1final DisplayContent displayContent = getDisplayContentOrCreate(displayId, attrs.token);...//2WindowToken token = displayContent.getWindowToken(hasParent ? parentWindow.mAttrs.token : attrs.token);...if (token == null) {if( hasParent ){token = parentWindow.mToken;} else if (){token = new WindowToken.Builder(this, binder, type)} else {token = new WindowToken.Builder(this, binder, type)}...}...//3final WindowState win = new WindowState(this, session, );...if (openInputChannels) {win.openInputChannel(outInputChannel);}...//4win.openInputChannel(outInputChannel);...//5win.attach();win.initAppOpsState();...win.mToken.addWindow(win);
}
首先,根据displayId找到归属的DisplayContent,DisplayContent的作用是用于跟踪一系列的WindowState;
然后,如果当前的window存在parent,则去查询其parent的WindowToken。WindowToken顾名思义,用于识别WindowState;
接下来,生成WindowState,这里的WindowState和APP侧的Window是对应的,WindowState就是在系统侧window的描述并负责和window进行通讯;
然后,绑定事件输入,这里的outInputChannel就是APP侧传递过来的。
最后,通过attch()方法完成绑定,我们重点看一下这个方法:
//WindowState.java
void attach() {if (DEBUG) Slog.v(TAG, "Attaching " + this + " token=" + mToken);mSession.windowAddedLocked();
}//Session.java
void windowAddedLocked() {if (mPackageName == null) {mPackageName = wpc.mInfo.packageName;}if (mSurfaceSession == null) {mSurfaceSession = new SurfaceSession();...mService.mSessions.add(this);}mNumWindow++;
}
简单来说,一个应用首次完成window.attch()的时候,初始化mPackageName和mSurfaceSession()。
而mSurfaceSession对应的就是显示在前台的区域,它初始化后,对应的就是native创建surface以及后续和surfaceFlinger交互的流程了,这个我们后面的文章来讲解。
最后使用mNumWindow记录Window的数量。
3.4 小结
我们仍然做一个小的总结,window注册在系统侧的实现。其实就是接受一个客户端传递过来的binder引用对象IWindow,然后生成一个唯一的对应对象WindowState。并且在应用进程级别生成一个SurfaceSession去维护应用的ViewRootImpl和surfaceFlinger的关系。流程图如下:
四.总结
最后,我们做一下总结,整个window的注册流程主要分为三块大块:
1.create流程主要是各种对象的初始化。流程中完成客户端window的创建以及mDecor,mContentParent等相关成员变量的初始化;
2.resume流程主要是window关系的维护。所以创建视图处理类ViewRootImpl,并且使用其把window向系统侧申请绑定;
3.系统收到后主要是生成window在系统侧的对象并记录。所以分别创建Window组的对象WindowToken和Window的系统侧对象WindowState并保存。
整体流程图如下:
五.扩展性问题
1.如果onCreate中不调用setContentView,那么会执行后面的流程吗?
答:会的,即使不调用setContentView,只是不会有ContentView,但是DecorView仍然会创建和绑定的,只不过这时候展示的会是黑屏。
相关文章:
View绘制流程-Window创建
前言: View绘制流程中,主要流程是这样的: 1.用户进入页面,首先创建和绑定Window; 2.首次创建以及后续vsync信号来临时,会请求执行刷新流程; 3.刷新流程完成后,会通知SurfaceFlin…...
Jenkins build包时虽然单元测试失败了,但是仍然可以成功build包(最终结束时build success)
1.尝试方案1: 尽管单元测试失败,Jenkins Maven仍然可以获得成功-Java 学习之路 将 -Dmaven.test.failure.ignorefalse 添加到 MAVEN_OPTS artifactoryMaven {goals "-U clean install -Dmaven.test.skipfalse -DallowSnapshotstrue -Dmaven.te…...

【vue3】基础知识点-setup语法糖
学习vue3,都会从基础知识点学起。了解setup函数,ref,recative,watch、comptued、pinia等如何使用 今天说vue3组合式api,setup函数 在学习过程中一开始接触到的是这样的,定义数据且都要通过return返回 <…...
idol!! 2023牛客暑期多校训练营6 C
登录—专业IT笔试面试备考平台_牛客网 题目大意:定义n!!等于与n的奇偶性相同的所有小于等于n的数的阶乘之和,问n!!的末尾有多少0 1<n<1e18 思路:因为末尾0的来源是2*5,而2的个数明显比5的个数多得多,所以末尾…...

深入理解Jdk5引入的Java泛型:类型安全与灵活性并存
深入理解Jdk5引入的Java泛型:类型安全与灵活性并存 在Java的中,有一个强大的工具,它可以让你在编写代码时既保持类型安全,又享受灵活性。**这个工具就是——泛型(Generics)。**本文将引导你深入了解Java…...

idea在控制台中输出文字显示乱码
VM options中加入下面这行 -Dfile.encodingutf-8...

hacksudo3 通关详解
环境配置 一开始桥接错网卡了 搞了半天 改回来就行了 信息收集 漏洞发现 扫个目录 大概看了一眼没什么有用的信息 然后对着login.php跑了一下弱口令 sqlmap 都没跑出来 那么利用点应该不在这 考虑到之前有过dirsearch字典太小扫不到东西的经历 换个gobuster扫一下 先看看g…...
CentOS 虚拟机磁盘扩容(非常实用)
新手村的选手在刚开始安装使用 CentOS 虚拟机时,很多选项都会按照推荐操作来,比如:磁盘推荐大小为 20 GB;但随着后面的使用,总会因为“磁盘根目录不足”原因,而导致软件无法安装、虚拟机无法正常运行等&…...

docker案例复现
$uri导致的CRLF注入漏洞 前期准备dockerdocker compose 漏洞配置 前期准备 docker 要完成这样的测试,需要我们有一定的环境,也就是需要大家去安装docker 更新系统软件包: sudo yum update 安装 Docker 的依赖软件包: sudo yum …...

淘宝资源采集(从零开始学习淘宝数据爬取)
1. 为什么要进行淘宝数据爬取? 淘宝数据爬取是指通过自动化程序从淘宝网站上获取数据的过程。这些数据可以包括商品信息、销售数据、评论等等。淘宝数据爬取可以帮助您了解市场趋势、优化您的产品选择以及提高销售额。 淘宝作为全球的电商平台,每天都有…...

【C语言】预处理详解
本文目录 1 预定义符号 2 #define 2.1 #define 定义标识符 2.2 #define 定义宏 2.3 #define 替换规则 2.4 #和## 2.5 带副作用的宏参数 2.6 宏和函数对比 2.7 命名约定 3 #undef 4 命令行定义 5 条件编译 6 文件包含 6.1 头文件被包含的方式 6.2 嵌套文件包含 1 预定义符号 __…...

2023中国(合肥)场景创新峰会成功举办,全息网御被纳入《合肥市第二批场景能力清单》
场景作为重要的城市资源,在驱动科技创新、产业发展、城市治理方面发挥着重要作用。近年来,为促进数字技术与实体经济深度融合,加速前沿科技转化落地、吸引全球创新资源集聚,合肥市聚焦“双找”:为产品找场景࿰…...

QT QLCDNumber 使用详解
本文详细的介绍了QLCDNumber控件的各种操作,例如:新建界面、源文件、设置显示位数、设置进制、设置外观、设置小数点、设置溢出、显示事件、其它文章等等操作。 实际开发中,一个界面上可能包含十几个控件,手动调整它们的位置既费时…...

明年,HarmonyOS不再兼容Android应用!
2023年华为开发者大会,不知道各位老铁们是否观看了,一个震撼的消息就是,首次公开了HarmonyOS NEXT的概念,简而言之就是,这是一款专为开发者打造的预览版操作系统,旨在提供"纯正鸿蒙操作系统"的体…...
华为OD机试 - 人气最高的店铺(Java JS Python)
题目描述 某购物城有m个商铺,现决定举办一场活动选出人气最高店铺。 活动共有n位市民参与,每位市民只能投一票,但1号店铺如果给该市民发放 q 元的购物补贴,该市民会改为投1号店铺。 请计算1号店铺需要最少发放多少元购物补贴才能成为人气最高店铺(即获得的票数要大于其…...
mysql sql 语句sum求和嵌套数学表达式
今天有个需求, 已减高度 高度 x 单双开(单开1 双开2) x 2,要直接写在sql语句中。 表字段 包含 高度 和 单双开字段 值是字符串 (双开 左单开 右单开) -- 已减高度 2 * 单双开 * 高度 sum( -- 求和 表达式 已减高度 2 * 单双开 * 高度 t_cloth.hegiht * 2 * (case WHEN l…...

Java课题笔记~ Servlet编程
1.Servlet编程基础 (1)什么是Servlet Servlet是基于Java语言的Web编程技术,部署在服务器端的Web容器里,获取客户端的访问请求,并根据请求生成响应信息返回给客户端。 创建Servlet的方式,有 如下图:一般创建Servlet都…...

修改IDEA的idea.vmoptions参数导致IDEA无法打开(ReservedCodeCacheSize)
事发原因 Maven导依赖的时候OOM,因此怀疑是内存太小,尝试修改idea.vmoptions的参数,然后发现IDEA重启后打不开了,卸载重装后也无法打开。。。 实际上如果导包爆出OOM的话应该调整下图参数,不过这都是后话了 解决思路…...
P1321 单词覆盖还原
题目描述 一个长度为 l l l 的字符串中被反复贴有 boy 和 girl 两单词,后贴上的可能覆盖已贴上的单词(没有被覆盖的用句点表示),最终每个单词至少有一个字符没有被覆盖。问贴有几个 boy 几个 girl? 输入格式 一行被…...

GODOT游戏引擎简介,包含与unity性能对比测试,以及选型建议
GODOT,是一个免费开源的3D引擎。本文以unity作对比,简述两者区别和选型建议。由于是很久以前写的ppt,技术原因视频和部分章节丢失了。建议当做业务参考。 GODOT目前为止遇到3个比较重大的基于,第一个是oprea的合作奖,…...

Tailwind CSS 实战:基于 Kooboo 构建 AI 对话框页面(六):图片上传交互功能
在 《Tailwind CSS 实战:基于 Kooboo 构建 AI 对话框页面(五)》 中,完成了语音交互功能的优化。本文作为该系列教程的第六篇,将聚焦于图片上传功能的开发。通过集成图片上传与预览能力,我们将进一步完善 AI…...
C#中的依赖注入
1. 依赖注入(Dependency Injection, DI)概述 定义 :依赖注入是一种设计模式,允许将组件的依赖关系从内部创建转移到外部提供。这样可以降低组件之间的耦合度,提高代码的可测试性、可维护性和可扩展性。 核心思想 &…...
模板方法模式:优雅封装不变,灵活扩展可变
引言:代码复用与扩展的艺术 在日常开发中,我们常遇到核心流程固定但某些步骤需差异化的场景。例如: 数据库操作的通用流程(连接→执行→关闭)HTTP请求的固定步骤(构建请求→发送→解析响应)报表生成的骨架(数据获取→格式转换→输出)模板方法模式正是为解决这类问题而…...
Linux 特殊权限位详解:SetUID, SetGID, Sticky Bit
Linux 特殊权限位详解:SetUID, SetGID, Sticky Bit 在Linux权限系统中,除了基本的读、写(w)、执行(x)权限外,还有三个特殊权限位:SetUID、SetGID和Sticky Bit。这些权限位提供了更精细的权限控制机制,尤其在需要临时提升权限或管理共享资源时非常有用。 一、SetUID (s位…...
web前端开发如何适配各分辨率
在开发Web应用时,适配不同的显示器分辨率是确保用户体验一致性的关键。以下是一些常见的显示器分辨率。 常见的显示器分辨率 PC屏幕分辨率 1366 x 768:普通液晶显示器 1920 x 1080:高清液晶显示器 2560 x 1440:2K高清显示器 4096…...

最新研究揭示云端大语言模型防护机制的成效与缺陷
一项全面新研究揭露了主流云端大语言模型(LLM)平台安全机制存在重大漏洞与不一致性,对当前人工智能安全基础设施现状敲响警钟。该研究评估了三大领先生成式AI平台的内容过滤和提示注入防御效果,揭示了安全措施在阻止有害内容生成与…...
深入浅出多路归并:原理、实现与实战案例解析
文章目录 二路归并多路归并方法一:指针遍历(多指针比较法)方法二:小根堆法(最小堆归并) 实际场景外部排序 经典题目丑数Ⅱ方法一:三指针法方法二:优先队列法(K路归并&…...
Java转Go日记(五十九):参数验证
1. 结构体验证 用gin框架的数据验证,可以不用解析数据,减少if else,会简洁许多。 package mainimport ("fmt""time""github.com/gin-gonic/gin" )//Person .. type Person struct {//不能为空并且大于10Age …...

重启路由器ip不变怎么回事?原因分析与解决方法
在日常生活中,我们经常会遇到网络问题,而重启路由器是解决网络故障的常用方法之一。然而,有些用户发现,即使重启了路由器,自己的IP地址却没有变化,这让他们感到困惑。那么,重启路由器IP不变是怎…...
DDD架构实战 领域层 事件驱动
目录 核心实现: 这种实现方式的优势: 在实际项目中,你可能需要: 事件驱动往往是在一个微服务内部实现的 领域时间是DDD架构中比较常见的概念 在领域层内部的一个模型更改了状态或者发生了一些行为 向外发送一些通知 这些通…...