1 前言

WMS启动流程 中介绍了 WindowManagerService 的启动流程,本文将介绍 View 的添加流程,按照进程分为以下2步:

  • 应用进程:介绍从 WindowManagerImpl(addView 方法)到 Session(addToDisplay 方法)的调用流程;
  • system_server 进程:介绍从 Session(addToDisplay 方法)到 SurfaceSession(构造方法)的调用流程;

​ 为区分不同进程,将应用进程、system_server 进程分别标识为浅蓝色、深蓝色。

2 View 添加过程

2.1 从 WindowManagerImpl 到 Session

​ 如图,浅蓝色的类是应用进程中执行的,深蓝色的类是在 system_server 进程中执行的,黄色的类是指 AIDL 文件生成的接口(用于跨进程)。

​ 注意事项:

  • WMS 在 system_server 进程中单例运行,WindowManagerGlobal 在应用进程中单例运行,但是应用进程一般有多个,而进程之间内存隔离,因此每个应用进程中有且仅一个 WindowManagerGlobal 对象,并且会与一个 Session 对象一一对应。
  • 一个窗口对应一个根 View,在添加根 View 时,会创建一个 ViewRootImpl、W、WindowState 对象与之一一对应。

​ (1)addView

​ /frameworks/base/core/java/android/view/WindowManagerImpl.java

public void addView(View view, ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

​ (2)addView

​ /frameworks/base/core/java/android/view/WindowManagerGlobal.java

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
...
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
... ViewRootImpl root;
View panelParentView = null; //父窗口的根 View synchronized (mLock) {
...
//如果是子窗口,则通过 token 寻找父窗口的根 View
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW && wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i); //mViews = new ArrayList<View>(),存储了所有窗口的根 View
}
}
}
//每个窗口对应一个 ViewRootImpl 对象
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
//存留 view、root、params信息
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
root.setView(view, wparams, panelParentView);
}
...
}
}

​ 可以看到,每个根 View,都与一个 LayoutParams 和一个 ViewRootImpl 一一对应。

​ (3)setView

​ /frameworks/base/core/java/android/view/ViewRootImpl.java

​ 在 WindowManagerGlobal 的 addView() 方法中创建了 ViewRootImpl 对象,因此,先看看 ViewRootImpl 的构造方法。

public ViewRootImpl(Context context, Display display) {
mContext = context;
//IWindowSession 类型,单例对象,getWindowSession() 方法调用 WMS 的 openSession() 方法,new 一个 Session 对象
mWindowSession = WindowManagerGlobal.getWindowSession();
mDisplay = display;
...
mWindow = new W(this); //IWindow.Stub 的实现类
...
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context); //View.AttachInfo 类型
...
mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
...
}

​ 注意:在创建 ViewRootImpl 时,也创建了 W,进一步说明每个 根 View 都与一个 W 一一对应,即 mWindow.asBinder() 可以作为 根 View (或窗口)的身份标识。

​ 再看看 ViewRootImpl 的 setView 方法。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
...
mWindowAttributes.copyFrom(attrs);
...
mAttachInfo.mRootView = view;
...
if (panelParentView != null) {
mAttachInfo.mPanelParentWindowToken = panelParentView.getApplicationWindowToken();
}
...
if ((mWindowAttributes.inputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel();
}
...
//请求显示 View
requestLayout();
...
try {
...
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(),
mDisplay.getDisplayId(), mTmpFrame, mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel, mTempInsets);
setFrame(mTmpFrame);
}
...
}
}
}

2.2 从 Session 到 SurfaceSession

​ 注意事项:每个应用进程对应一个 Session 对象和一个 SurfaceSession 对象, 每个窗口对应一个 WindowState 对象。

​ (1)Session 的创建过程

​ 在 ViewRootImpl 的构造方法中,调用了 WindowManagerGlobal 的 getWindowSession() 方法中获取了 Session 对象,因此,先看看 Session 对象的创建过程。

​ /frameworks/base/core/java/android/view/WindowManagerGlobal.java

public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
...
IWindowManager windowManager = getWindowManagerService(); //获取 WMS
sWindowSession = windowManager.openSession(
new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {
ValueAnimator.setDurationScale(scale);
}
});
}
...
}
return sWindowSession; //private static IWindowSession sWindowSession,单例对象
}
}

​ /frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java

public IWindowSession openSession(IWindowSessionCallback callback) {
return new Session(this, callback);
}

​ /frameworks/base/services/core/java/com/android/server/wm/Session.java

public Session(WindowManagerService service, IWindowSessionCallback callback) {
mService = service;
mCallback = callback;
mUid = Binder.getCallingUid();
mPid = Binder.getCallingPid();
...
try {
mCallback.asBinder().linkToDeath(this, 0);
}
...
}

​ (2)addToDisplay

​ /frameworks/base/services/core/java/com/android/server/wm/Session.java

public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs, int viewVisibility,
int displayId, Rect outFrame, Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel, InsetsState outInsetsState) {
//mService 为 WMS
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame, outContentInsets,
outStableInsets, outOutsets, outDisplayCutout, outInputChannel, outInsetsState);
}

​ (3)addWindow

​ /frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java

public int addWindow(Session session, IWindow client, int seq, LayoutParams attrs, int viewVisibility,
int displayId, Rect outFrame, Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
DisplayCutout.ParcelableWrapper, InputChannel outInputChannel, InsetsState outInsetsState) {
...
//检查权限,不通过则直接返回
...
WindowState parentWindow = null; //父窗口的 WindowState
...
final int type = attrs.type;
synchronized (mGlobalLock) {
//获取 DisplayContent,不存在就创建,按照以下顺序创建,不为空就返回:
//mRoot.getWindowToken(attrs.token).getDisplayContent()
//mRoot.getDisplayContent(displayId)
//mRoot.createDisplayContent(mDisplayManager.getDisplay(displayId), null)
final DisplayContent displayContent = getDisplayContentOrCreate(displayId, attrs.token);
...
if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) { //待添加的窗口是 sub_win
parentWindow = windowForClientLocked(null, attrs.token, false); //parentWindow = mWindowMap.get(attrs.token)
...
}
...
//过滤:1.type=sub_win、parentWindow=null; 2.type = sub_win = parentWindow 等
...
AppWindowToken atoken = null;
final boolean hasParent = parentWindow != null;
//获取 WindowToken(有父窗口,则获取父窗口的)
WindowToken token = displayContent.getWindowToken(hasParent ? parentWindow.mAttrs.token : attrs.token);
//获取窗口 type(有父窗口,则获取父窗口的)
final int rootType = hasParent ? parentWindow.mAttrs.type : type;
...
if (token == null) { //如果 parentWindow != null, 则 token != null。因此,token = null,则 parentWindow = null,即此时的 token 和 rootType 是当前窗口的
...
//过滤:1.rootType=app_win; 2.rootType=ime_win; 3.rootType=wallpaper_win; rootType=toast_win 等,即不允许上述窗口的 token 为 null
...
final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
...
//前面没有过滤 sub_win,说明 sub_win 的 token 可以不用提前赋值,这里也会创建
token = new WindowToken(this, binder, type, false, displayContent, session.mCanAddInternalSystemWindow, isRoundedCornerOverlay);
} else if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) { //rootType=app_win
atoken = token.asAppWindowToken(); //atoken 不能为空,否则会返回错误码
...
}
...
//过滤:1.rootType = ime_win != token.windowType; 2.rootType = wallpaper_win != oken.windowType 等
...
else if (type == TYPE_TOAST) {
//若 token.windowType!=toast_win,可能会被过滤,即返回错误码
...
}
...
else if (token.asAppWindowToken() != null) { //AppWindowToken 是 WindowToken 的子类
attrs.token = null;
token = new WindowToken(this, client.asBinder(), type, false, displayContent, session.mCanAddInternalSystemWindow);
}
//创建 WindowState,说明每个根 View,都与一个 WindowState 一一对应
final WindowState win = new WindowState(this, session, client, token, parentWindow, appOp[0],
seq, attrs, viewVisibility, session.mUid, session.mCanAddInternalSystemWindow);
...
final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
displayPolicy.adjustWindowParamsLw(win, win.mAttrs, Binder.getCallingPid(), Binder.getCallingUid());
win.setShowToOwnerOnlyLocked(mPolicy.checkShowToOwnerOnly(attrs));
//前面已排除一些异常可能,接下来的代码不会有异常情况
res = displayPolicy.prepareAddWindowLw(win, attrs);
...
win.attach(); //mSessions.add(win.mSession)
mWindowMap.put(client.asBinder(), win);
...
win.mToken.addWindow(win); //win.mToken.addChild(win, mWindowComparator)
...
}
...
return res;
}

​ 特别提醒:请重点关注 WindowToken、WindowState 的创建过程。

​ (4)构造方法

​ /frameworks/base/services/core/java/com/android/server/wm/WindowState.java

WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token, WindowState parentWindow, int appOp,
int seq, WindowManager.LayoutParams a, int viewVisibility, int ownerId, boolean ownerCanAddInternalSystemWindow) {
this(service, s, c, token, parentWindow, appOp, seq, a, viewVisibility, ownerId,
ownerCanAddInternalSystemWindow, new PowerManagerWrapper(){...});
} WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token, WindowState parentWindow, int appOp,
int seq, WindowManager.LayoutParams a, int viewVisibility, int ownerId, boolean ownerCanAddInternalSystemWindow,
PowerManagerWrapper powerManagerWrapper) {
super(service); //mWmService = service
mSession = s;
mClient = c;
...
mToken = token;
mAppToken = mToken.asAppWindowToken();
...
mAttrs.copyFrom(a);
...
mPolicy = mWmService.mPolicy;
mContext = mWmService.mContext;
...
if (mAttrs.type >= FIRST_SUB_WINDOW && mAttrs.type <= LAST_SUB_WINDOW) { //计算子窗口图层
mBaseLayer = mPolicy.getWindowLayerLw(parentWindow) * TYPE_LAYER_MULTIPLIER + TYPE_LAYER_OFFSET;
mSubLayer = mPolicy.getSubWindowLayerFromTypeLw(a.type);
...
parentWindow.addChild(this, sWindowSubLayerComparator);
...
} else { //计算窗口图层
mBaseLayer = mPolicy.getWindowLayerLw(this) * TYPE_LAYER_MULTIPLIER + TYPE_LAYER_OFFSET;
mSubLayer = 0;
...
}
...
mWinAnimator = new WindowStateAnimator(this);
...
}

​ (5)attach

​ /frameworks/base/services/core/java/com/android/server/wm/WindowState.java

void attach() {
...
mSession.windowAddedLocked(mAttrs.packageName);
}

​ (6)windowAddedLocked

​ /frameworks/base/services/core/java/com/android/server/wm/Session.java

void windowAddedLocked(String packageName) {
//绑定应用进程包名
mPackageName = packageName;
...
if (mSurfaceSession == null) {
...
mSurfaceSession = new SurfaceSession();
...
//将 Session 添加到 WMS 中
mService.mSessions.add(this);
...
}
mNumWindow++;
}

​ 说明: Session 和 SurfaceSession 都与应用进程一一对应,SurfaceSession 持有 SurfaceComposerClient,作为跟 SurfaceFlinger 通信的代理对象。

​ (7)SurfaceSession

​ /frameworks/base/core/java/android/view/SurfaceSession.java

public final class SurfaceSession {
// SurfaceComposerClient
private long mNativeClient;
... public SurfaceSession() {
//创建 SurfaceComposerClient
mNativeClient = nativeCreate();
} ...
}

​ 说明: SurfaceComposerClient 是 ISurfaceComposer 的代理类,SurfaceFlinger 是 ISurfaceComposer 的实现类,它们通过 Binder 实现跨进程通讯。

3 添加 View 的场景

​ 看到这里,读者也许会疑惑,什么样的 View 会通过 WindowManager 的 addView() 走完上述流程?布局中的 TextView 组件的添加会走上述流程么?带着这些疑问,笔者列举了系统中 3 个 View的添加场景应用,分别是:Activity、Dialog、Toast 中 View 的添加,这些 View 一般命名为 mDecor(也有例外),它们一般是根 View。

​ (1)Activity

​ /frameworks/base/core/java/android/app/Activity.java

Activity extends ContextThemeWrapper implements ... {
private WindowManager mWindowManager;
private Window mWindow
View mDecor = null; void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager(); //mWindowManager
//通过 WindowManager 添加 View
wm.addView(mDecor, getWindow().getAttributes());
...
}
...
}
}

​ (2)Dialog.java

​ /frameworks/base/core/java/android/app/Dialog.java

public class Dialog implements DialogInterface ... {
private final WindowManager mWindowManager;
final Window mWindow;
View mDecor; public void show() {
...
mDecor = mWindow.getDecorView();
...
WindowManager.LayoutParams l = mWindow.getAttributes();
...
//通过 WindowManager 添加 View
mWindowManager.addView(mDecor, l);
...
}
}

​ (3)Toast

​ /frameworks/base/core/java/android/widget/Toast.java

public class Toast {
WindowManager mWM;
View mView; public void handleShow(IBinder windowToken) {
...
if (mView != mNextView) {
...
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
...
try {
//通过 WindowManager 添加 View
mWM.addView(mView, mParams);
...
}
...
}
}
}

4 WindowManager

​ WindowManager 是 View 添加的起点,其具体实现是 WindowManagerImpl。在获取 WindowManager 对象时,一般使用如下代码:

//Context 的具体实现是 ContextImpl
WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); //"window"

​ 下面跟踪一下上述代码。

​ /frameworks/base/core/java/android/app/ContextImpl.java

public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}

​ /frameworks/base/core/java/android/app/SystemServiceRegistry.java

final class SystemServiceRegistry {
...
//<serviceName, serviceFetcher>
private static final Map<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS = new ArrayMap<String, ServiceFetcher<?>>();
//服务缓存个数,每调用一次 registerService,服务个数+1,sServiceCacheSize 未被初始化,默认初值为 0
private static int sServiceCacheSize; //在类被加载时,注册服务
static {
...
registerService(Context.WINDOW_SERVICE, WindowManager.class,
new CachedServiceFetcher<WindowManager>() {
@Override
public WindowManager createService(ContextImpl ctx) {
return new WindowManagerImpl(ctx);
}});
} //创建缓存,在 ContextImpl 类初始化时调用:final Object[] mServiceCache = SystemServiceRegistry.createServiceCache()
public static Object[] createServiceCache() {
return new Object[sServiceCacheSize];
} //获取服务
public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
} //注册服务
private static <T> void registerService(String serviceName, Class<T> serviceClass, ServiceFetcher<T> serviceFetcher) {
...
SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}
...
}

​ 说明:CachedServiceFetcher 是 SystemServiceRegistry 的内部类,负责创建和缓存服务,每个服务对应一个缓存编号和 ServiceFetcher。在静态代码块里调用了 registerService() 方法,说明在 SystemServiceRegistry 类加载时就创建了一系列 ServiceFetcher,但是此时还没有创建服务,在第一次请求服务时才开始创建服务。

​ 注意:SystemServiceRegistry 类被应用进程加载,系统中可能同时存在多个应用进程,而应用之间内存隔离,因此,每个应用进程在加载 SystemServiceRegistry 类时,都创建了一系列 ServiceFetcher。

​ /frameworks/base/core/java/android/app/SystemServiceRegistry.CachedServiceFetcher.java

static abstract interface ServiceFetcher<T> {
T getService(ContextImpl ctx);
} //服务拾取器(每个 service 对应一个 cacheIndex 和 serviceFetcher)
static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> {
private final int mCacheIndex; //服务编号 CachedServiceFetcher() {
mCacheIndex = sServiceCacheSize++;
} public final T getService(ContextImpl ctx) {
//获取服务缓存数组,mServiceCache 属性在定义时就被初始化:mServiceCache = SystemServiceRegistry.createServiceCache()
final Object[] cache = ctx.mServiceCache;
...
for (;;) {
...
synchronized (cache) {
T service = (T) cache[mCacheIndex];
if (service != null || gates[mCacheIndex] == ContextImpl.STATE_NOT_FOUND) {
return service; //服务已存在,无需创建
}
...
} if (doInitialize) {
T service = null;
...
try {
//创建服务
service = createService(ctx);
...
}
...
} finally {
synchronized (cache) {
//缓存服务,保证服务在同一应用进程中以单例存在
cache[mCacheIndex] = service;
...
}
}
return service;
}
...
}
} public abstract T createService(ContextImpl ctx) throws ServiceNotFoundException;
}

​ 每个 ContextImpl 对象在初始化时创建了一个用于缓存服务的数组,每个服务在第一次请求时才被创建并缓存,因此,每个 Context 中都可能存在一个单例的 WindowManager 对象。

​ 声明:本文转自【framework】View添加过程

【framework】View添加过程的更多相关文章

  1. View绘制过程理解

    假期撸了几篇自定义View相关的东西,后两天下雨呆在家里还是效率太低Orz   每个Activity都包含一个Window对象,这个Window对象通常由PhoneWindow来实现[1],而每个Wi ...

  2. [原创]Android从xml加载到View对象过程解析

    我们从Activity的setContentView()入手,开始源码解析, //Activity.setContentView public void setContentView(int layo ...

  3. Android解析WindowManager(三)Window的添加过程

    前言 在此前的系列文章中我们学习了WindowManager体系和Window的属性,这一篇我们接着来讲Window的添加过程.建议阅读此篇文章前先阅读本系列的前两篇文章. 1.概述 WindowMa ...

  4. ZT 第9章 Framework的启动过程

    所在位置: 图书 -> 在线试读 -> Android内核剖析 第9章 Framework的启动过程 9.3 zygote的启动 前面小节介绍了Framework的运行环境,以及Dalvi ...

  5. Android窗口系统第二篇---Window的添加过程

    以前写过客户端Window的创建过程,大概是这样子的.我们一开始从Thread中的handleLaunchActivity方法开始分析,首先加载Activity的字节码文件,利用反射的方式创建一个Ac ...

  6. 给view添加类似系统上拉快捷菜单的手势

    iOS7以后从屏幕最下方上划会滑出快捷菜单,感觉这个效果不错,就想做个类似的效果,这个东西技术含量不高,每次都写一遍的话就太浪费时间了,所以就把它写成了一个分类,用起来会方便一点. demo地址:ht ...

  7. android绘制view的过程

    1 android绘制view的过程简单描述  简单描述可以解释为:计算大小(measure),布局坐标计算(layout),绘制到屏幕(draw):            下面看看每一步的动作到底是 ...

  8. 【转】Android绘制View的过程研究——计算View的大小

    Android绘制View的过程研究——计算View的大小 转自:http://liujianqiao398.blog.163.com/blog/static/18182725720121023218 ...

  9. 如何在IOS开发中在自己的framework中添加.bunble文件

    今天就跟大家介绍一下有关,如何在IOS开发中在自己的framework中添加.bunble文件,该文章我已经在IOS教程网(http://ios.662p.com)发布过来,个人觉得还是对大家有帮助的 ...

  10. 给View添加手势,防止点击View上其他视图触发点击效果

    在开发过程中,我们可能会遇到这个问题. 当我们给一个view添加了手势,但是我们又不想点击view上面的视图也触发手势.如下图: 我们在红色view上添加了手势,但是又不想点击黄色view也触发.其实 ...

随机推荐

  1. 【Gerrit】 快捷操作

    A:添加Reviewers V+B:Pachset刷到最新 D:Download patch J.K:文件移动选中 R:文件Reviewed状态修改 S:五星状态修改,可用于分类管理 U:返回上层 I ...

  2. [转帖]centos6 free 和 centos 7的free 的差异与对比

    目录 一 centos6 free 常用参数和含义 centos6 free 命令示例 free 值讲解 计算公式 二 centos7 free 常用的参数 centos7 free 命令示例 计算公 ...

  3. [转帖]shell脚本字符串截取的8种方法

    https://www.cnblogs.com/zwgblog/p/6031256.html 假设有变量 var=http://www.aaa.com/123.htm. 1. # 号截取,删除左边字符 ...

  4. RPM安装的Oracle19c 修改init.ora进行修复以及最简单开机启动Oracle的方法

    RPM安装的Oracle19c 修改init.ora进行修复以及最简单开机启动Oracle的方法 背景 今天开始使用自己心的ThinkBook14 的笔记本 因为已经没有了 Linux测试环境供我使用 ...

  5. 部署于K8S集群上面应用性能影响点推测

    前言 本人2017年第一次接触K8S. 中间断断续续学习K8S相关的内容. 但是最近一年,几乎没太有学习. 因为之前学习了四五年, 一直以为产品马上要用 结果一直被浇冷水. 去年开始学乖了. 不这么搞 ...

  6. Harbor简单搭建以及异常排查的过程与思路

    Harbor简单搭建以及异常排查的过程与思路 前言 我发现我总是能够遇到别人遇不到的问题. 本来搭建十分钟就可以搭建完成 结果我硬生生的搭建了四十分钟. 为了保证下次不再浪费时间. 这里加单总结一下遇 ...

  7. Vue基础系列文章11---router基本使用

    1.系统中引入路由js文件,加两个连接,分别到用户管理和用户注册页面 <router-link to="/user">用户列表</router-link> ...

  8. docker 发布web项目到linux

    一.准备工作 1.连接到Liunx的工具 MobaXterm 填好Ip 直接点ok就行 输入用户名和密码进入系统 2.已发布的.netcore网站或微服务 在要发布的项目上右键---->添加-- ...

  9. Redis启用认证

    要在Redis中启用认证,您需要在Redis配置文件中设置requirepass指令.以下是步骤: 找到Redis配置文件.这通常是redis.conf,可能位于/etc/redis/或/etc/目录 ...

  10. [3] 以逆向的角度来看循环语句——do、while、for的比较

    [3] 以逆向的角度来看循环语句--do.while.for的比较 1. do循环 ​ 先执行循环体,后比较判断 #include <stdio.h> int main(int argc, ...