以前写过客户端Window的创建过程,大概是这样子的。我们一开始从Thread中的handleLaunchActivity方法开始分析,首先加载Activity的字节码文件,利用反射的方式创建一个Activity对象,调用Activity对象的attach方法,在attach方法中,创建系统需要的Window并为设置回调,这个回调定义在Window之中,由Activity实现,当Window的状态发生变化的时候,就会回调Activity实现的这些回调方法。调用attach方法之后,Window被创建完成,这时候需要关联我们的视图,在handleLaunchActivity中的attach执行之后就要执行handleLaunchActivity中的callActivityOnCreate,在onCreate中我们会调用setContentView方法。通过setContentView,创建了Activity的顶级View---DecorView,DecorView的内容栏(mContentParent)用来显示我们的布局,这只是添加的过程,还要有一个显示的过程,显示的过程就要调用ActivityThead中handleLaunchActivity中的handleResumeActivity方法了,最后会调用makeVisible方法,把这个DecorView显示出来。

那么今天所讨论的是话题主要有,创建后的Activity的窗口是怎么添加的,WindowManagerService是如何感知Activity窗口添加的?系统中有很多应用,每个应用有多个Activity,一个Activity对应一个Window,WindowManagerService是怎么管理的?本文基于Android7.0源码。

一、从ActivityThread#handleResumeActivity方法说起

final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
ActivityClientRecord r = mActivities.get(token);
....
if (r != null) {
final Activity a = r.activity;
....
//Activity的Window没有被添加过并且Activity没有finish和需要设置成可见
if (r.window == null && !a.mFinished && willBeVisible) {
//对Actiivty成员变量window赋值
r.window = r.activity.getWindow();
//获取Window的DecorView
View decor = r.window.getDecorView();
//将DecorView设置成可见
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 ...
if (a.mVisibleFromClient && !a.mWindowAdded) {
a.mWindowAdded = true;
//调用ViewManager的方法添加decor
wm.addView(decor, l);
}
...
}

ViewManager定义 了操作View的三大方法,addView,updateViewLayout,removeView;比如ViewGroup就实现了这个接口。

public interface ViewManager{
/**
* Assign the passed LayoutParams to the passed View and add the view to the window.
* <p>Throws {@link android.view.WindowManager.BadTokenException} for certain programming
* errors, such as adding a second view to a window without removing the first view.
* <p>Throws {@link android.view.WindowManager.InvalidDisplayException} if the window is on a
* secondary {@link Display} and the specified display can't be found
* (see {@link android.app.Presentation}).
* @param view The view to be added to this window.
* @param params The LayoutParams to assign to view.
*/
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}

但是这个地方,我们要看的ViewManager的实现类是WindowManagerImpl,所以调用的是WindowManagerImpl的addView方法,


private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance(); @Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mDisplay, mParentWindow);
}

WindowManagerImpl紧接就把这个活交给了WindowManagerGlobal,所以要去看WindowManagerGlobal的addView方法。WindowManagerGlobal是用来管理Window的全局类,它里面维护了几个全局的列表。

//存储所有Window所对应的View
private final ArrayList<View> mViews = new ArrayList<View>(); //存储所有Window对应的ViewRootImpl
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); //存储所有Window对应的布局参数
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>(); //存储所有将要被删除的View,即Window
private final ArraySet<View> mDyingViews = new ArraySet<View>();

下面是addView方法,很容易懂,主要做了如下几个事情

public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) { //第一件事情:检查参数是否合法,如果是子Window,还要调整布局参数
if (view == null) {
throw new IllegalArgumentException("view must not be null");
}
if (display == null) {
throw new IllegalArgumentException("display must not be null");
}
if (!(params instanceof WindowManager.LayoutParams)) {
throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
} final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
// If there's no parent and we're running on L or above (or in the
// system context), assume we want hardware acceleration.
final Context context = view.getContext();
if (context != null
&& context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
} ViewRootImpl root;
View panelParentView = null; synchronized (mLock) {
..... //第二件事情 创建ViewRootImpl,将View添加到列表中,这里每次都会new一个对象,所以说调用一次addView,就会有一个ViewRootImpl。
root = new ViewRootImpl(view.getContext(), display); view.setLayoutParams(wparams); mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
} // do this last because it fires off messages to start doing things
try { //第三件事情 :使用ViewRootImpl对象,调用setView
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
synchronized (mLock) {
final int index = findViewLocked(view, false);
if (index >= 0) {
removeViewLocked(index, true);
}
}
throw e;
}
}

ViewRootImpl是是个什么呢?

  • 简单来说,ViewRoot相当于是MVC模型中的Controller,它有以下职责:1. 负责为应用程序窗口视图创建Surface。 2. 配合WindowManagerService来管理系统的应用程序窗口。 3. 负责管理、布局和渲染应用程序窗口视图的UI。*我们在看ViewRootImpl的setView 方法。
 */
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view; ......
mAdded = true;
int res; /* = WindowManagerImpl.ADD_OKAY; */ //1、调用requestLayout方法进行绘制
requestLayout();
....
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
//2、调用mWindowSession添加View
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mInputChannel);
} catch (RemoteException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
mInputChannel = null;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
throw new RuntimeException("Adding window failed", e);
} finally {
if (restore) {
attrs.restore();
}
}
......
}
}

看看requestLayout方法,首先检验是不是主线程在修改UI,然后 调用scheduleTraversals,在scheduleTraversals开始执行了,才会有我们熟悉的onDraw,onLayout,onMeasure。

  @Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();http://www.cnblogs.com/GMCisMarkdownCraftsman/p/6117129.html
}
} void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}

二、APP端与WMS的IPC过程

继续回到setView方法,重点理解这行代码的意思。

      res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mInputChannel);

这行代码大部分人都知道这个是与WMS做IPC通信的,但是具体是个什么过程呢?我们可以将这个过程和AMS与应用进程IPC的过程做对比。

AMS与应用进程IPC

ActivityManager类内部调用ActivityManagerNative的getDefault函数得到一个ActivityManagerProxy对象,AMS继承ActivityManagerNative,通过它可与AMS通信。所以说,ActivityManagerProxy(是AMS在客户端进程的一个代理,通过AMP里面的方法请求AMS。那么客户端进程在AMS的代理呢?这个代理就是ApplicationThreadProxy,如果AMS要通知Activity,那么就是使用ApplicationThreadProxy。现在看APP端和WMS的IPC。

App与WMS的连接

App与WMS的连接,首先会建立一个Session到WMS,之后就会通过IWindowSession接口与WMS中的Session直接通信,IWindowSession类是什么?它指向了一个实现了IWindowSession接口的Session代理对象。当应用程序进程启动第一个Activity组件的时候,它就会请求WMS服务发送一个建立连接的Binder进程间通信请求。WMS服务接收到这个请求之后,就会在内部创建一个类型为Session的Binder本地对象,并且将这个Binder本地对象返回给应用程序进程,App就会得到一个Session代理对象,并且保存在ViewRootImpl类的成员变量mWindowSession中。大概代码如下。

public ViewRootImpl(Context context, Display display) {
     ....
//创建了WindowSession对象
mWindowSession = WindowManagerGlobal.getWindowSession();
//也创建了W的对象mWindow
mWindow = new W(this)
....
}
private static IWindowSession sWindowSession;

public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
InputMethodManager imm = InputMethodManager.getInstance();
IWindowManager windowManager = getWindowManagerService();
          //调用WMS的openSession创建一个Session对象
sWindowSession = windowManager.openSession(
new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {
ValueAnimator.setDurationScale(scale);
}
},
imm.getClient(), imm.getInputContext());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return sWindowSession;
}
}
 com.android.server.wm.WindowManagerService.java

 @Override
public IWindowSession openSession(IWindowSessionCallback callback, IInputMethodClient client, IInputContext inputContext) {
if (client == null) throw new IllegalArgumentException("null client");
if (inputContext == null) throw new IllegalArgumentException("null inputContext");
Session session = new Session(this, callback, client, inputContext);
return session;
}

当App有了这个远端Session代理对象mWindowSession之后,所有向WMS的请求都通过mWindowSession来进行。举例来说:ViewRootImpl要添加窗口,就使用mWindowSession代理对象的addToDisplay方法调用到远端Session对象的addToDisplay方法。

mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);

远端Session对象收到这个请求后,转接给WMS。

final 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);
  }
}

注意到addToDisplay中的参数mWindow是一个W对象,W对象是什么?ViewRootImpl::W:用于向WMS提供接口,让WMS控制App端的窗口。它可看作是个代理,很多时候会调用ViewRootImpl中的功能。

W的声明中有两个成员变量:mViewAncestor和mWindowSession,它一头连着App端的ViewRootImpl,一头连着WMS中的Session,且实现了IWindow的接口。意味着它是App和WMS的桥梁,是WMS用来回调App端,让ViewRootImpl做事用的。

举例来说,dispatchAppVisibility()的流程就是经过它来完成的:WMS ->ViewRootImpl::W->ViewRootHandler->handleAppVisibility()->scheduleTraversals()。

类似这种话结构的还有,PhoneWindow::DecorView,这种设计使得系统满足最小隔离原则,Client端该用到哪些接口就暴露哪些接口。因为这种类的函数都是跑在Binder线程中的,所以其中不能调用非线程安全的函数,也不能直接操作UI控件,所以一般都是往主线程消息队列里丢一个消息让其异步执行。

三、addWindow方法解读

addWindow方法比较长,粗略的分成下面9点来解释。

public int addWindow(Session session, IWindow client, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
InputChannel outInputChannel) {
1、窗口添加权限校验
2、检查特殊窗口attr.token和attr.type的一致性
3、创建窗口对象
4、调用adjustWindowParamsLw对窗口参数进行调整
5、创建pipe,用于输入消息的传递
6、调用窗口的attach,初始化Surface相关的变量,将窗口win放到mWindowMap中
7、如果type == TYPE_APPLICATION_STARTING ,说明这个是启动窗口,把win赋值给token.appWindowToken.startingWindow
8、添加窗口到Windows列表,确定窗口的位置
9、窗口已经添加了,调用assignLayersLocked调整一下层值
}

1、窗口添加权限校验

   int[] appOp = new int[1];
int res = mPolicy.checkAddPermission(attrs, appOp);
if (res != WindowManagerGlobal.ADD_OKAY) {
return res;
}

从参数attrs中可以取出窗口的type,对type类型除了Toast窗口、屏保窗口、输入法窗口、墙纸窗口、语音交互窗口等少数几个类型窗口不需要进行权限判断外,其余的窗口都需要检查是否有android.Manifest.permission.SYSTEM_ALERT_WINDOW权限或者 android.Manifest.permission.INTERNAL_SYSTEM_WINDOW权限。

2、检查特殊窗口attr.token和attr.type的一致性

synchronized(mWindowMap) {
//屏幕没有准备好,不给添加窗口
if (!mDisplayReady) {
throw new IllegalStateException("Display has not been initialialized");
}
//获取当前窗口需要添加在哪一个屏幕上
final DisplayContent displayContent = getDisplayContentLocked(displayId);
if (displayContent == null) {
Slog.w(TAG_WM, "Attempted to add window to a display that does not exist: "
+ displayId + ". Aborting.");
return WindowManagerGlobal.ADD_INVALID_DISPLAY;
}
//该窗口有没有权限在这个屏幕上添加
if (!displayContent.hasAccess(session.mUid)) {
Slog.w(TAG_WM, "Attempted to add window to a display for which the application "
+ "does not have access: " + displayId + ". Aborting.");
return WindowManagerGlobal.ADD_INVALID_DISPLAY;
}
//该窗口是否已经添加过,WMS会把添加的窗口Token保存在mWindowMap中
if (mWindowMap.containsKey(client.asBinder())) {
Slog.w(TAG_WM, "Window " + client + " is already added");
return WindowManagerGlobal.ADD_DUPLICATE_ADD;
}
    //如果窗口类型是子窗口
if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
         //把这个子窗口的父窗口取出来
attachedWindow = windowForClientLocked(null, attrs.token, false);
//父窗口如果为空,就会return,因为子窗口需要依赖一个父窗口存在
if (attachedWindow == null) {
Slog.w(TAG_WM, "Attempted to add window with token that is not a window: "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
//父窗口不为null,但是父窗口的类型也是一个子窗口,也会return
if (attachedWindow.mAttrs.type >= FIRST_SUB_WINDOW
&& attachedWindow.mAttrs.type <= LAST_SUB_WINDOW) {
Slog.w(TAG_WM, "Attempted to add window with token that is a sub-window: "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
}
//这个type表示该窗口是要添加到虚拟设备上的,但是该设备却不是虚拟的,也要返回
if (type == TYPE_PRIVATE_PRESENTATION && !displayContent.isPrivate()) {
Slog.w(TAG_WM, "Attempted to add private presentation window to a non-private display. Aborting.");
return WindowManagerGlobal.ADD_PERMISSION_DENIED;
} boolean addToken = false;
//从mTokenMap取出WindowToken,WindowToken是窗口分组的标志,因为addWindow方法调用之前,AMS等服务会提前向WMS注册一个token到mTokenMap,所以这里一般是可以取到的,也有些情况是取不到的,下面分析。
WindowToken token = mTokenMap.get(attrs.token);
AppWindowToken atoken = null;
boolean addToastWindowRequiresToken = false; if (token == null) {
//如果这个窗口是应用程序窗口,上面取出的token为null,需要返回
if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
Slog.w(TAG_WM, "Attempted to add application window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
//如果这个窗口是输出法窗口,上面取出的token为null,需要返回
if (type == TYPE_INPUT_METHOD) {
Slog.w(TAG_WM, "Attempted to add input method window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
//如果这个窗口是语音交互窗口,上面取出的token为null,需要返回
if (type == TYPE_VOICE_INTERACTION) {
Slog.w(TAG_WM, "Attempted to add voice interaction window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
       //如果这个窗口是墙纸窗口,上面取出的token为null,需要返回
if (type == TYPE_WALLPAPER) {
Slog.w(TAG_WM, "Attempted to add wallpaper window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
         //如果这个窗口是屏保窗口,上面取出的token为null,需要返回
if (type == TYPE_DREAM) {
Slog.w(TAG_WM, "Attempted to add Dream window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
         //如果这个窗口QS_DIALOG,上面取出的token为null,需要返回
if (type == TYPE_QS_DIALOG) {
Slog.w(TAG_WM, "Attempted to add QS dialog window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
         //如果这个窗口TYPE_ACCESSIBILITY_OVERLAY类型窗口,上面取出的token为null,需要返回
if (type == TYPE_ACCESSIBILITY_OVERLAY) {
Slog.w(TAG_WM, "Attempted to add Accessibility overlay window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
if (type == TYPE_TOAST) {
// Apps targeting SDK above N MR1 cannot arbitrary add toast windows.
if (doesAddToastWindowRequireToken(attrs.packageName, callingUid,
attachedWindow)) {
Slog.w(TAG_WM, "Attempted to add a toast window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
}
//如果不是上面这些类型窗口,并且从mWindowMap中又不能取出对应的token的话,
          这里会隐士的创建一个第四个参数等于false,代表隐士创建,隐士创建的token在窗口销毁的时候,是不需要移除的
token = new WindowToken(this, attrs.token, -1, false);
addToken = true;
//上面取出的窗口不为null,并且type是应用类型的窗口
} else if (type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW) {
         //取出appWindowToken,appWindowToken是WindowToken的子类,一个appWindowToken一般表示这个窗口是一个Activity窗口
atoken = token.appWindowToken;
//取出atoken为null的话,不能添加,窗口的添加是必须有一个token的
if (atoken == null) {
Slog.w(TAG_WM, "Attempted to add window with non-application token "
+ token + ". Aborting.");
return WindowManagerGlobal.ADD_NOT_APP_TOKEN;
} else if (atoken.removed) {
Slog.w(TAG_WM, "Attempted to add window with exiting application token "
+ token + ". Aborting.");
return WindowManagerGlobal.ADD_APP_EXITING;
}
//如果这个窗口的类型是一个启动窗口的话,atoken的firstWindowDrawn等于true,不需要添加
if (type == TYPE_APPLICATION_STARTING && atoken.firstWindowDrawn) {
// No need for this guy!
if (DEBUG_STARTING_WINDOW || localLOGV) Slog.v(
TAG_WM, "**** NO NEED TO START: " + attrs.getTitle());
return WindowManagerGlobal.ADD_STARTING_NOT_NEEDED;
}
} else if (type == TYPE_INPUT_METHOD) {
//窗口的类型是输入法窗口,但是它的token不是输入法类型的token,也要返回,不给添加,下面几个是类似的
if (token.windowType != TYPE_INPUT_METHOD) {
Slog.w(TAG_WM, "Attempted to add input method window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (type == TYPE_VOICE_INTERACTION) {
if (token.windowType != TYPE_VOICE_INTERACTION) {
Slog.w(TAG_WM, "Attempted to add voice interaction window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (type == TYPE_WALLPAPER) {
if (token.windowType != TYPE_WALLPAPER) {
Slog.w(TAG_WM, "Attempted to add wallpaper window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (type == TYPE_DREAM) {
if (token.windowType != TYPE_DREAM) {
Slog.w(TAG_WM, "Attempted to add Dream window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (type == TYPE_ACCESSIBILITY_OVERLAY) {
if (token.windowType != TYPE_ACCESSIBILITY_OVERLAY) {
Slog.w(TAG_WM, "Attempted to add Accessibility overlay window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (type == TYPE_TOAST) {
// Apps targeting SDK above N MR1 cannot arbitrary add toast windows.
addToastWindowRequiresToken = doesAddToastWindowRequireToken(attrs.packageName,
callingUid, attachedWindow);
if (addToastWindowRequiresToken && token.windowType != TYPE_TOAST) {
Slog.w(TAG_WM, "Attempted to add a toast window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (type == TYPE_QS_DIALOG) {
if (token.windowType != TYPE_QS_DIALOG) {
Slog.w(TAG_WM, "Attempted to add QS dialog window with bad token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (token.appWindowToken != null) {
Slog.w(TAG_WM, "Non-null appWindowToken for system window of type=" + type);
// It is not valid to use an app token with other system types; we will
// instead make a new token for it (as if null had been passed in for the token).
attrs.token = null;
token = new WindowToken(this, null, -1, false);
addToken = true;
}
.......

3、创建窗口对象

WindowState win = new WindowState(this, session, client, token,
attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);

WindowState是WMS中真正的窗口对象

4、调用PhoneWindowManager的adjustWindowParamsLw调整布局参数

public void adjustWindowParamsLw(WindowManager.LayoutParams attrs) {
switch (attrs.type) {
case TYPE_SYSTEM_OVERLAY:
case TYPE_SECURE_SYSTEM_OVERLAY:
attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
attrs.flags &= ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
break;
case TYPE_STATUS_BAR:
if (mKeyguardHidden) {
attrs.flags &= ~WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
attrs.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
}
break; case TYPE_SCREENSHOT:
attrs.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
break; case TYPE_TOAST:
if (attrs.hideTimeoutMilliseconds < 0
|| attrs.hideTimeoutMilliseconds > TOAST_WINDOW_TIMEOUT) {
attrs.hideTimeoutMilliseconds = TOAST_WINDOW_TIMEOUT;
}
attrs.windowAnimations = com.android.internal.R.style.Animation_Toast;
break;
} if (attrs.type != TYPE_STATUS_BAR) {
// The status bar is the only window allowed to exhibit keyguard behavior.
attrs.privateFlags &= ~WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
} if (ActivityManager.isHighEndGfx()) {
if ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0) {
attrs.subtreeSystemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
}
final boolean forceWindowDrawsStatusBarBackground =
(attrs.privateFlags & PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND)
!= 0;
if ((attrs.flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0
|| forceWindowDrawsStatusBarBackground
&& attrs.height == MATCH_PARENT && attrs.width == MATCH_PARENT) {
attrs.subtreeSystemUiVisibility |= View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
}
}
}

这段代码主要调整状态栏等几个特殊窗口的FLAG,比如不让它获取用户焦点,不让它响应触摸消息等。

5、创建pipe,用于输入消息的传递

      final boolean openInputChannels = (outInputChannel != null
&& (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);
if (openInputChannels) {
win.openInputChannel(outInputChannel);
}

窗口需要接收事件,所以需要向InputManagerService注册InputChannel。

6、调用窗口的attach,初始化Surface相关的变量,将窗口win放到mWindowMap中

 if (addToken) {
mTokenMap.put(attrs.token, token);
}
win.attach();
mWindowMap.put(client.asBinder(), win);
 void attach() {
if (WindowManagerService.localLOGV) Slog.v(
TAG, "Attaching " + this + " token=" + mToken
+ ", list=" + mToken.windows);
mSession.windowAddedLocked();
}
 void windowAddedLocked() {
if (mSurfaceSession == null) {
if (WindowManagerService.localLOGV) Slog.v(
TAG_WM, "First window added to " + this + ", creating SurfaceSession");
//创建窗口所对应的SurfaceSession
mSurfaceSession = new SurfaceSession();
if (SHOW_TRANSACTIONS) Slog.i(
TAG_WM, " NEW SURFACE SESSION " + mSurfaceSession);
mService.mSessions.add(this);
if (mLastReportedAnimatorScale != mService.getCurrentAnimatorScale()) {
mService.dispatchNewAnimatorScaleLocked(this);
}
}
mNumWindow++;
}
7、如果type == TYPE_APPLICATION_STARTING ,说明这个是启动窗口,把win赋值给token.appWindowToken.startingWindow
  if (type == TYPE_APPLICATION_STARTING && token.appWindowToken != null) {
token.appWindowToken.startingWindow = win;
if (DEBUG_STARTING_WINDOW) Slog.v (TAG_WM, "addWindow: " + token.appWindowToken
+ " startingWindow=" + win);
}
8、添加窗口到Windows列表,确定窗口的位置
     if (type == TYPE_INPUT_METHOD) {
win.mGivenInsetsPending = true;
mInputMethodWindow = win;
addInputMethodWindowToListLocked(win);
imMayMove = false;
} else if (type == TYPE_INPUT_METHOD_DIALOG) {
mInputMethodDialogs.add(win);
addWindowToListInOrderLocked(win, true);
moveInputMethodDialogsLocked(findDesiredInputMethodWindowIndexLocked(true));
imMayMove = false;
} else {
addWindowToListInOrderLocked(win, true);
if (type == TYPE_WALLPAPER) {
mWallpaperControllerLocked.clearLastWallpaperTimeoutTime();
displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
} else if ((attrs.flags&FLAG_SHOW_WALLPAPER) != 0) {
displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
} else if (mWallpaperControllerLocked.isBelowWallpaperTarget(win)) {
// If there is currently a wallpaper being shown, and
// the base layer of the new window is below the current
// layer of the target window, then adjust the wallpaper.
// This is to avoid a new window being placed between the
// wallpaper and its target.
displayContent.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
}
}

确定窗口位置,分成几种情况,如果是输入法类型的窗口,调用addInputMethodWindowToListLocked插入窗口列表,如果是输入法对话框,调用addWindowToListInOrderLocked插入窗口列表,如果是其他类型的窗口,比如应用类型的窗口,调用addWindowToListInOrderLocked插入窗口列表,现在重点关注addWindowToListInOrderLocked方法。

   private void addWindowToListInOrderLocked(final WindowState win, boolean addToToken) {
if (DEBUG_FOCUS) Slog.d(TAG_WM, "addWindowToListInOrderLocked: win=" + win +
" Callers=" + Debug.getCallers(4));
if (win.mAttachedWindow == null) {
final WindowToken token = win.mToken;
//tokenWindowsPos表示该WindowState对象在所属同一WindowToken的所有WindowState中的位置
int tokenWindowsPos = 0;
if (token.appWindowToken != null) {
tokenWindowsPos = addAppWindowToListLocked(win);
} else {
addFreeWindowToListLocked(win);
}
if (addToToken) {
if (DEBUG_ADD_REMOVE) Slog.v(TAG_WM, "Adding " + win + " to " + token);
// token.windows就是描述所属该token下的所有WindowState对象
  // 比如一个activity弹出了一个AlertDialog窗口,这两个窗口的AppWindowToken是一个
token.windows.add(tokenWindowsPos, win);
}
} else {
addAttachedWindowToListLocked(win, addToToken);
} final AppWindowToken appToken = win.mAppToken;
if (appToken != null) {
if (addToToken) {
appToken.addWindow(win);
}
}
}

先看窗口的父窗口mAttachedWindow是不是存在,如果不存在,说明这个窗口应用类型的窗口,反之就是一个子窗口,走else分之,使用addAttachedWindowToListLocked方法将子窗口插入窗口列表。假设不是一个子窗口,那么需要取出这个窗口的token,如果token里面的appWindowToken不为null的话,就表明这是一个Activity窗口,需要调用addAppWindowToListLocked将Activity窗口插入窗口堆栈,反之使用addFreeWindowToListLocked方法将非Activity窗口插入窗口堆栈。

 private int addAppWindowToListLocked(final WindowState win) {
//获取WindowState要插入的屏幕对象displayContent
final DisplayContent displayContent = win.getDisplayContent();
if (displayContent == null) {
// It doesn't matter this display is going away.
return 0;
}
final IWindow client = win.mClient;
final WindowToken token = win.mToken;
//获取当前屏幕上所有窗口列表对象windows
final WindowList windows = displayContent.getWindowList();
//获取属于displayContent这个屏幕上,属于token所描述的WindowList,WindowList存放了token所描述的WindowState的列表
WindowList tokenWindowList = getTokenWindowsOnDisplay(token, displayContent);
int tokenWindowsPos = 0;
//tokenWindowList不为null,说明WMS中已经有了和待插入窗口win一样的token,那么就会使用addAppWindowToTokenListLocked来插入
//比如当一个Activity窗口上弹出一个Dialog窗口,
     //那么这个待插入的Dialog窗口的token和Activity窗口的token是一样的,都是AppWindowToken,就会走这个逻辑
if (!tokenWindowList.isEmpty()) {
return addAppWindowToTokenListLocked(win, token, windows, tokenWindowList);
} // No windows from this token on this display
if (localLOGV) Slog.v(TAG_WM, "Figuring out where to add app window " + client.asBinder()
+ " (token=" + token + ")");
// Figure out where the window should go, based on the
// order of applications.
//pos记录源码插入的WindowState
WindowState pos = null;
//遍历这个屏幕中的Task
final ArrayList<Task> tasks = displayContent.getTasks();
int taskNdx;
int tokenNdx = -1;
//自顶向下遍历Task,每一个Task中取出它的tokens列表
for (taskNdx = tasks.size() - 1; taskNdx >= 0; --taskNdx) {
AppTokenList tokens = tasks.get(taskNdx).mAppTokens;
  //自顶向下遍历tokens列表,就可以取出AppWindowToken
for (tokenNdx = tokens.size() - 1; tokenNdx >= 0; --tokenNdx) {
final AppWindowToken t = tokens.get(tokenNdx);
//一般来说,第一个就是要匹配的AppWindowToken,因为都是自顶向下遍历的,这个时候,跳出循环
if (t == token) {
  //tokenNdx记录着AppWindowToken在tokens中的位置
--tokenNdx;
  //如果tokenNdx小于0,那么是无效的,需要取下一个Task,把它插入下一个Task的顶部 
if (tokenNdx < 0) {
--taskNdx;
if (taskNdx >= 0) {
tokenNdx = tasks.get(taskNdx).mAppTokens.size() - 1;
}
}
break;
} // We haven't reached the token yet; if this token
// is not going to the bottom and has windows on this display, we can
// use it as an anchor for when we do reach the token.
//如果当前tasks中第一个AppWindowToken不等于t,那么执行getTokenWindowsOnDisplay,
         //获取属于displayContent这个屏幕上,属于token所描述的WindowList
tokenWindowList = getTokenWindowsOnDisplay(t, displayContent);
if (!t.sendingToBottom && tokenWindowList.size() > 0) {
   //pos就指向当前遍历到AppWindowToken所属的最下面一个的WindowState
pos = tokenWindowList.get(0);
}
}
if (tokenNdx >= 0) {
// early exit
break;
}
} // We now know the index into the apps. If we found
// an app window above, that gives us the position; else
// we need to look some more.
//如果pos不为null,说明pos是上面指向的当前遍历到AppWindowToken所属的最下面一个的WindowState
if (pos != null) {
// Move behind any windows attached to this one.
WindowToken atoken = mTokenMap.get(pos.mClient.asBinder());
if (atoken != null) {
tokenWindowList =
getTokenWindowsOnDisplay(atoken, displayContent);
final int NC = tokenWindowList.size();
if (NC > 0) {
WindowState bottom = tokenWindowList.get(0);
if (bottom.mSubLayer < 0) {
//判断pos的token所描述的窗口列表中最下面的窗口对象bottom的子序是否小于0,
//如果小于0,pos需要指向这个窗口, 确保pos指向的是上面AppWindowToken的最后一个WindowState
pos = bottom;
}
}
}
  //插入最下面的一个WindowState的后面
placeWindowBefore(pos, win);
return tokenWindowsPos;
} // Continue looking down until we find the first
// token that has windows on this display.
//走到这里,说明第一个就是要匹配的AppWindowToken,taskNdx记录了是在哪一个taskNdx中,,再次自定向下遍历
for (; taskNdx >= 0; --taskNdx) {
AppTokenList tokens = tasks.get(taskNdx).mAppTokens;
for (; tokenNdx >= 0; --tokenNdx) {
//获取顶部最顶部Task中最顶部的AppWindowToken
final AppWindowToken t = tokens.get(tokenNdx);
tokenWindowList = getTokenWindowsOnDisplay(t, displayContent);
final int NW = tokenWindowList.size();
if (NW > 0) {
  //pos尽可能的指向最顶部TASK中最顶部的AppWindowToken所描述的WindowState
pos = tokenWindowList.get(NW - 1);
break;
}
}
if (tokenNdx >= 0) {
// found
break;
}
}
//pos!= null,说明找到了,检查pos描述的窗口上面有没有子窗口,如果有,需要把pos指向最顶部的子窗口
if (pos != null) {
// Move in front of any windows attached to this
// one.
WindowToken atoken = mTokenMap.get(pos.mClient.asBinder());
if (atoken != null) {
final int NC = atoken.windows.size();
if (NC > 0) {
WindowState top = atoken.windows.get(NC - 1);
if (top.mSubLayer >= 0) {
pos = top;
}
}
}
  //把win插入到pos的后面
placeWindowAfter(pos, win);
return tokenWindowsPos;
} // Just search for the start of this layer.
// 走到这里,说明该WindowState是新启动的一个activity的第一个窗口(新task的第一个WindowState),
// 因为新启动的窗口是没有可参考的activity窗口,所以需要通过mBaseLayer去插入
final int myLayer = win.mBaseLayer;
int i;
for (i = windows.size() - 1; i >= 0; --i) {
WindowState w = windows.get(i);
// Dock divider shares the base layer with application windows, but we want to always
// keep it above the application windows. The sharing of the base layer is intended
// for window animations, which need to be above the dock divider for the duration
// of the animation.
if (w.mBaseLayer <= myLayer && w.mAttrs.type != TYPE_DOCK_DIVIDER) {
break;
}
}
if (DEBUG_FOCUS || DEBUG_WINDOW_MOVEMENT || DEBUG_ADD_REMOVE) Slog.v(TAG_WM,
"Based on layer: Adding window " + win + " at " + (i + 1) + " of "
+ windows.size());
windows.add(i + 1, win);
mWindowsChanged = true;
return tokenWindowsPos;
}

上面有检查tokenWindowList,tokenWindowList不为null,说明WMS中已经有了和待插入窗口win一样的token,那么就会使用addAppWindowToTokenListLocked来插入,比如当一个Activity窗口上弹出一个Dialog窗口,那么这个待插入的Dialog窗口的token和Activity窗口的token是一样的,都是AppWindowToken,就会走addAppWindowToTokenListLocked这个逻辑。

private int addAppWindowToTokenListLocked(WindowState win, WindowToken token,
WindowList windows, WindowList tokenWindowList) {
int tokenWindowsPos;
// If this application has existing windows, we
// simply place the new window on top of them... but
// keep the starting window on top.
//如果是TYPE_BASE_APPLICATION窗口,则需要插入在该AppWindowToken所有窗口的最底部
if (win.mAttrs.type == TYPE_BASE_APPLICATION) {
// Base windows go behind everything else.
WindowState lowestWindow = tokenWindowList.get(0);
placeWindowBefore(lowestWindow, win);
tokenWindowsPos = indexOfWinInWindowList(lowestWindow, token.windows);
} else {
AppWindowToken atoken = win.mAppToken;
final int windowListPos = tokenWindowList.size();
WindowState lastWindow = tokenWindowList.get(windowListPos - 1);
   // 如果是starting window,则插入到starting window的下面
if (atoken != null && lastWindow == atoken.startingWindow) {
placeWindowBefore(lastWindow, win);
tokenWindowsPos = indexOfWinInWindowList(lastWindow, token.windows);
} else {
int newIdx = findIdxBasedOnAppTokens(win);
//there is a window above this one associated with the same
//apptoken note that the window could be a floating window
//that was created later or a window at the top of the list of
//windows associated with this token.
if (DEBUG_FOCUS || DEBUG_WINDOW_MOVEMENT || DEBUG_ADD_REMOVE) Slog.v(TAG_WM,
"not Base app: Adding window " + win + " at " + (newIdx + 1) + " of "
+ windows.size());
windows.add(newIdx + 1, win);
if (newIdx < 0) {
// No window from token found on win's display.
tokenWindowsPos = 0;
} else {
tokenWindowsPos = indexOfWinInWindowList(
windows.get(newIdx), token.windows) + 1;
}
mWindowsChanged = true;
}
}
return tokenWindowsPos;
}

窗口的插入还是比较复杂的,总结而言:
1.非应用窗口依据mBaseLayer插入,越高越靠前,墙纸和输入法会有特殊处理,在下面还会进行调整。
2.应用窗口参考activity的位置插入,通常应该被插入在其activity所在task的顶部或者该activity上面的activity的最后一个窗口的下面
3.子窗口依据mSubLayer插入
最终插入的地方有两个:DisplayContent所持有的记录该屏幕下所有窗口顺序的WindowList,以及新窗口的WindowToken所记录的所有属于它的WindowState中的WindowList列表。

9、窗口已经添加了,调用assignLayersLocked调整一下层值
//参数windows是窗口列表
final void assignLayersLocked(WindowList windows) {
if (DEBUG_LAYERS) Slog.v(TAG_WM, "Assigning layers based on windows=" + windows,
new RuntimeException("here").fillInStackTrace()); clear();
int curBaseLayer = 0;
int curLayer = 0;
boolean anyLayerChanged = false;
//遍历窗口列表,上面通过Z序的计算公式计算出来的Z序值保存在WindowState的变量mBaseLayer
中,这个循环的意思是,遇到同类型的窗口,后一个窗口在前一个窗口的基础上偏移5。
for (int i = 0, windowCount = windows.size(); i < windowCount; i++) {
final WindowState w = windows.get(i);
boolean layerChanged = false; int oldLayer = w.mLayer;
if (w.mBaseLayer == curBaseLayer || w.mIsImWindow || (i > 0 && w.mIsWallpaper)) {
curLayer += WINDOW_LAYER_MULTIPLIER;
} else {
curBaseLayer = curLayer = w.mBaseLayer;
}
// 更新该窗口的mAnimLayer,也就是动画显示时,该窗口的层级
assignAnimLayer(w, curLayer); // TODO: Preserved old behavior of code here but not sure comparing
// oldLayer to mAnimLayer and mLayer makes sense...though the
// worst case would be unintentionalp layer reassignment.
if (w.mLayer != oldLayer || w.mWinAnimator.mAnimLayer != oldLayer) {
layerChanged = true;
anyLayerChanged = true;
}      // 将当前应用窗口的最高显示层级记录在mHighestApplicationLayer中
if (w.mAppToken != null) {
mHighestApplicationLayer = Math.max(mHighestApplicationLayer,
w.mWinAnimator.mAnimLayer);
}
  // 对于分屏等相关的窗口,它们的显示层级需要再次处理
collectSpecialWindows(w); if (layerChanged) {
w.scheduleAnimationIfDimming();
}
}     // 调整特殊窗口的层级
adjustSpecialWindows(); //TODO (multidisplay): Magnification is supported only for the default display.
if (mService.mAccessibilityController != null && anyLayerChanged
&& windows.get(windows.size() - 1).getDisplayId() == Display.DEFAULT_DISPLAY) {
mService.mAccessibilityController.onWindowLayersChangedLocked();
} if (DEBUG_LAYERS) logDebugLayers(windows);
}

addWindow流程大致上分成以上九点,基本可以了解一个窗口是怎么添加到WMS中的。

作者:LooperJing
链接:https://www.jianshu.com/p/ba53cf8694f1
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

Android窗口系统第二篇---Window的添加过程的更多相关文章

  1. Android窗口系统第一篇---Window的类型与Z-Order确定

    Android的窗口系统是UI架构很重要的一部分,数据结构比较多,细节比较多.本篇文章主要介绍窗口相关数据结构和抽象概念理解,关于[窗口部分的博客]计划如下. 1.窗口Z-Order的管理 2.应用程 ...

  2. Android窗口系统第三篇---WindowManagerService中窗口的组织方式

    Android窗口系统第一篇—Window的类型与Z-Order确定 Android窗口系统第二篇—Window的添加过程 上面文章梳理了一个窗口的添加过程,系统中有很多应用,每个应用有多个Activ ...

  3. 第六篇 ANDROID窗口系统机制之显示机制

    第六篇 ANDROID窗口系统机制之显示机制 ANDROID的显示系统是整个框架中最复杂的系统之一,涉及包括窗口管理服务.VIEW视图系统.SurfaceFlinger本地服务.硬件加速等.窗口管理服 ...

  4. “MVC+Nhibernate+Jquery-EasyUI”信息发布系统 第二篇(数据库结构、登录窗口、以及主界面)

    “MVC+Nhibernate+Jquery-EasyUI”信息发布系统 第二篇(数据库结构.登录窗口.以及主界面) 一.在上一篇文章中,主要说的就是把主框架搭建起来,并且Nhibernate能达到增 ...

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

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

  6. Android解析WindowManagerService(二)WMS的重要成员和Window的添加过程

    前言 在本系列的上一篇文章中,我们学习了WMS的诞生,WMS被创建后,它的重要的成员有哪些?Window添加过程的WMS部分做了什么呢?这篇文章会给你解答. 1.WMS的重要成员 所谓WMS的重要成员 ...

  7. [转]Android开源项目第二篇——工具库篇

    本文为那些不错的Android开源项目第二篇--开发工具库篇,主要介绍常用的开发库,包括依赖注入框架.图片缓存.网络相关.数据库ORM建模.Android公共库.Android 高版本向低版本兼容.多 ...

  8. Android开源项目第二篇——工具库篇

    本文为那些不错的Android开源项目第二篇——开发工具库篇,**主要介绍常用的开发库,包括依赖注入框架.图片缓存.网络相关.数据库ORM建模.Android公共库.Android 高版本向低版本兼容 ...

  9. 建立apk定时自动打包系统第二篇——自动上传文件

    在<建立apk定时自动打包系统第一篇——Ant多渠道打包并指定打包目录和打包日期>这篇文章中介绍多渠道打包的流程.很多时候我们需要将打包好的apk上传到ftp中,这时候我可以修改custo ...

随机推荐

  1. TP5结合聚合数据API查询天气

    php根据城市查询天气情况看到有人分享java的查询全国天气情况的代码,于是我想分享一个php版本的查询天气接口.免费查询天气的接口有很多,比如百度的apistore的天气api接口,我本来想采用这个 ...

  2. 出现“Windows资源管理器已停止工作”错误

    出现"Windows资源管理器已停止工作"错误 什么是资源管理器呢,explorer.exe进程的作用就是让我们管理计算机中的资源! 今天开电脑的时候就一直提示windows资源管 ...

  3. leetcode-Symmetric Tree 对称树

    Given a binary tree, check whether it is a mirror of itself (ie, symmetric around its center). For e ...

  4. Asp 解析 XML并分页显示

    Asp 解析 XML并分页显示 Asp 解析 XML并分页显示,演示样例源代码例如以下: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Tr ...

  5. java 内存与内存溢出

    学习自:http://www.codeceo.com/article/jvm-memory-overflow.html 讲的很清楚

  6. redis远程登录

    #redis-cli -h 202.96.126.37 -p 6379 #auth 'd6d72fa9b2ff458e:GRjZmQ3MTN'

  7. 读懂JVM垃圾收集日志

    JVM垃圾收集搞懂原理很重要,但是连垃圾收集的日志都不懂,这tmd还分析个屁的问题啊,典型的空有理论知识,动手实践为零.本篇就来具体的学习一下JVM中的垃圾收集日志. 第一步,垃圾收集的选项 图1 J ...

  8. 精彩回顾 HUAWEI HiAI 亮相华为北研所

    ​​从普通照片变成艺术品,仅需3秒: 从随手拍下的讲解胶片到生成规整清晰的ppt,只要瞬间…… 5月25日在华为北京研究所举办的HUAWEI HiAI技术合作交流会上,伴随着一声声惊叹,数款接入HUA ...

  9. golang截取字符串

    对于字符串操作,截取字符串是一个常用的, 而当你需要截取字符串中的一部分时,可以使用像截取数组某部分那样来操作,示例代码如下: package main import "fmt" ...

  10. 如何将linux服务器作为文件服务器

    在开发过程中想要使用linux服务器作为文件服务器,可以通过 IP+文件名来获取文件信息,比如http://localhost/banner/a.jpg.设置过程如下 1.安装apache2 sudo ...