【系统之音】WindowManager工作机制详解
前言
目光所及,皆有Window!Window,顾名思义,窗口,它是应用与用户交互的一个窗口,我们所见到视图,都对应着一个Window。比如屏幕上方的状态栏、下方的导航栏、按音量键调出来音量控制栏、充电时的充电界面、屏幕中间的应用显示区域(Activity)、Dialog、Toast、PopWindow、菜单等,都依附于对应的Window。可以认为Window是View的实际直接管理者,所以理解Window相关的知识,对理解Android的视图机制有很大的帮助。
本文将介绍Window相关的基础知识,以及从源码的角度分析WindowManager是如何将View呈现在界面的。
一、一个悬浮按钮的demo
本demo实现了在屏幕上显示一个悬浮的Button,并可以跟随手指的移动而移动。代码如下:
public void drawFloatButton() {
requestWindowPermission();
final WindowManager windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
final Button button = new Button(this);
button.setText("button");
final WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
0,
0,
PixelFormat.TRANSPARENT);
params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
params.gravity = Gravity.LEFT | Gravity.TOP;
params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
params.x = 500;
params.y = 500;
windowManager.addView(button, params);
button.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
int rawX = (int) event.getRawX();
int rawY = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
params.x = rawX;
params.y = rawY;
windowManager.updateViewLayout(button, params);
break;
default:
break;
}
return false;
}
});
}
如果是在Android6.0及以上,需要处理权限问题,在AndroidManifest.xml中声明权限:
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
并在代码中动态申请
private void requestWindowPermission() {
//android 6.0或者之后的版本需要发一个intent让用户授权
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (!Settings.canDrawOverlays(getApplicationContext())) {
Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, 100);
}
}
}
由于该权限是一个敏感权限,所以启动时系统还会弹出一个界面让用户手动开启该权限:
演示效果如下所示:
该Button是个悬浮按钮,它并不是通过在xml布局文件中加入,然后通过Activity的setContentView方法将其显示在界面中的,而是通过第19行的WindowManager.addView方法实现的。gif中可以看到,该Button可以显示到Title区域,也可以在app退出后(不杀死进程)独立显示在桌面上。手指滑动过程中,通过第29行WindowManager.updateViewLayout更新Button的LayoutParams的坐标参数,不断更新该Button的位置。后续我们会对这两个方法做详细分析。
二、Window的相关说明
通过上述的demo,我们可以看到设置了LayoutParams的flags、type变量值,它们都是用来定义Window的特性的。
1、flag
flag变量用于设置window的属性,控制其显示特性,比如设置为不获取焦点、不接受触屏事件、显示在锁屏之上等。在WindowManager.LayoutParams类中定义了很多的flag常量,来丰富Window的功能及属性。
2、type
type变量用于表示Window的类型。系统规定了Window有三种类型,按取值由小到大依次为:
(1)应用Window:对应着一个Activity,要创建应用窗口就必须在Activity中完成。层级范围:1~99
(2)子Window:不能独立存在,需要依附于特定的父Window。比如Dialog,PopWindow,菜单等。层级范围:1000~1999
(3)系统Window:拥有系统权限才能创建的Window。比如Toast、系统状态栏、导航栏、手机低电量提示、输入法Window、搜索条、来电显示等。系统Window是独立于应用程序的,理论上讲应用程序没有权限创建系统Window,只有系统进程才有。层级范围:2000~2999
type的值越大,在Window体系中显示的层级就越高。为了理解这一点,我们了解一下窗口的Z-Order管理。
手机上采用的是层叠式的布局,它是一个三维空间,将手机水平方向作为X轴,竖直方向作为Y轴,垂直于屏幕由里向外的方向为Z轴,所有窗口就按照type值的顺序排列在Z轴上,type值越大Z值也就越大。如下图所示,所以系统Window往往会在上层显示:
三、WindowManager工作机制
这里一定要捋清楚ViewManager、WindowManager、WindowManagerImpl、WindowManagerGlobal之间的关系。下面先把关键代码摆出来:
1 public interface ViewManager
2 {
3 public void addView(View view, ViewGroup.LayoutParams params);
4 public void updateViewLayout(View view, ViewGroup.LayoutParams params);
5 public void removeView(View view);
6 }
7
8 public interface WindowManager extends ViewManager{
9 ......
10 }
11
12 public final class WindowManagerImpl implements WindowManager {
13 private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
14 @override
15 public void addView(view,params){
16 mGlobal.addView(...);
17 }
18 public void updateViewLayout(view,params){
19 mGlobal.updateViewLayout(...);
20 }
21 public void remove(view){
22 mGlobal.remove(...);
23 }25 }
26
27 public final class WindowManagerGlobal {
28 public void addView(...){
29 ......
30 }
31 public void updateViewLayout(...) {
32 ......
33 }
34 public void removeView(...) {
35 ......
36 }
37 }
看到上述这段代码,相信你一定已经清楚了它们之间的关系了。ViewManager是个接口,其中定义了三个方法,在Window体系中添加、更新、移除View的过程看起来比较复杂,但其实都是围绕着这三个方法展开的。WindowManager是个接口,继承了ViewManager,扩展了功能。WindowManagerImpl是个实现类,其中包含了WindowManagerGlobal实例。WindowManagerGlobal则真正完成了addView、updateViewLayout、removeView过程。所以,在demo中我们在使用WindowManager的实例调用addView,updateViewLayout方法时,实际上都是WindowManagerGlobal来完成的。这种方式称为桥接模式,这在系统源码种很常见,Context机制中也是这种方式,桥接模式这里就不展开讲了。
第13行中通过WindowManagerGlobal.getInstance()来获取的实例,这里看看其实现:
//=========WindowManagerGlobal.java========
private static WindowManagerGlobal sDefaultWindowManager;
public static WindowManagerGlobal getInstance() {
synchronized (WindowManagerGlobal.class) {
if (sDefaultWindowManager == null) {
sDefaultWindowManager = new WindowManagerGlobal();
}
return sDefaultWindowManager;
}
}
可见,它是通过单例模式的方式对外提供的实例。如果对单例模式比较了解的话,就能看出这种实现方式是有问题的(对比DCL方式),但不明白系统源码为什么要这样实现,可能是系统中调用该实例方法的场景比较简单吧,咱们自己在设计单例模式的时候可不能这样做。
在WindowMangerGlobal.java中维护着四个非常重要的list:
//=========WindowManagerGlobal.java=========
//所有Window对应的View
private final ArrayList<View> mViews = new ArrayList<View>();
//所有Window对应的ViewRootImpl
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
//所有Window对应的LayoutParams
private final ArrayList<WindowManager.LayoutParams> mParams = new ArrayList<WindowManager.LayoutParams>;
//正在被删除的View,或者已经执行了removeView方法但还没有完成删除操作的View
private final ArraySet<View> mDyingViews = new ArraySet<View>;
这四个list在addView、updateViewLayout、removeView的过程中都会频频出现,理清楚这四个list和这三个方法,理解WindowManager工作机制时会清晰很多。
在下面的源码中会详细讲到。
四、addView机制
//=========WindowManagerGlobal=========
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
......
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
......
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
......
//view不能重复添加,如果要添加需要先removeView,否则抛异常
int index = findViewLocked(view, false);
if (index >= 0) {
if (mDyingViews.contains(view)) {
// Don't wait for MSG_DIE to make it's way through root's queue.
mRoots.get(index).doDie();
} else {
throw new IllegalStateException("View " + view
+ " has already been added to the window manager.");
}
// The previous removeView() had not completed executing. Now it has.
}
//子window
// If this is a panel window, then find the window it is being
// attached to for future reference.
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);
}
}
}
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);8
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
上述代码中除了关键方法外,注意留意mViews、mRoots、mParams集合的操作。
//=========ViewRootImpl.java==========
/**
* We have one child
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
......
mAdded = true;
int res; /* = WindowManagerImpl.ADD_OKAY; */
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
......
try {
......
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
mAdded = false;
mView = null;
......
throw new RuntimeException("Adding window failed", e);
} finally {
......
}
......
}
}
}
第15行requestLayout()方法会执行View的绘制流程,在我之前的博文【朝花夕拾】Android自定义View篇之(一)View绘制流程中第三节有相关介绍。这里简单截取了一段UML图,读者明白其作用就可以了,想深入研究的可以去这篇文章看看。
第18行的mWindowSession.addToDisplay(...)方法,addView的实际逻辑处理就在这里面。我们继续分析mWindowSession和addToDisplay(...)的逻辑:
//========ViewRootImpl.java=========
public final class ViewRootImpl{
final IWindowSession mWindowSession;
public ViewRootImpl(...){
......
mWindowSession = WindowManagerGlobal.getWindowSession();
......
}
} //=======WindowManagerGlobal.java==========
public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
......
IWindowManager windowManager = getWindowManagerService();
sWindowSession = windowManager.openSession(
new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {
ValueAnimator.setDurationScale(scale);
}
},
......
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
return sWindowSession;
}
} public static IWindowManager getWindowManagerService() {
synchronized (WindowManagerGlobal.class) {
if (sWindowManagerService == null) {
sWindowManagerService = IWindowManager.Stub.asInterface(
ServiceManager.getService("window"));
......
}
return sWindowManagerService;
}
} //============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;
} public int addWindow(Session session, IWindow client, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
InputChannel outInputChannel) {
//在wms中真正实现
......
} //===========Session.java=========
final WindowManagerService mService;
public Session(WindowManagerService service, ......) {
mService = service;
......
} @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);
}
这里面的代码逻辑很容易理解,最后是把addView的工作交给了WMS的addWindow方法,所以真正添加view的逻辑是在WMS中完成的。这里面逻辑比较复杂繁琐,就不继续深入了,当目前为止就已经清楚整个流程了。
五、updateViewLayout更新机制
//============WindowManagerGlobal.java==========
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);
}
}
这里面对mParams进行了操作,将旧有的LayoutParams进行了替换。第10行执行了更新逻辑:
//=============ViewRootImpl.java==========
void setLayoutParams(WindowManager.LayoutParams attrs, boolean newView) {
...... //对新的LayoutParams参数做一些操作
scheduleTraversals(); //调用绘制流程
}
从上述过程可以发现更新过程相对比较简单,更新view的过程简单来说就是,将新的LayoutParams替换掉旧的,并启用绘制流程。
六、removeView移除机制
//============WindowManagerImpl.java==========
@Override
public void removeView(View view) {
mGlobal.removeView(view, false);
}
@Override
public void removeViewImmediate(View view) {
mGlobal.removeView(view, true);
}
WindowManagerImpl类中提供了两个方法用于移除view,从方法名称可以推测其差别在于Immediate,也就是是否立即移除的意思。
//==========WindowManagerGlobal.java=========
public void removeView(View view, boolean immediate) {
......
synchronized (mLock) {
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
removeViewLocked(index, immediate);
......
}
} private void removeViewLocked(int index, boolean immediate) {
ViewRootImpl root = mRoots.get(index);
View view = root.getView();
......
boolean deferred = root.die(immediate);
if (view != null) {
view.assignParent(null);
if (deferred) {
mDyingViews.add(view);
}
}
} //========ViewRootImpl.java=========
private final static int MSG_DIE = 3;
final ViewRootHandler mHandler = new ViewRootHandler();
/**
* @param immediate True, do now if not in traversal. False, put on queue and do later.
* @return True, request has been queued. False, request has been completed.
*/
boolean die(boolean immediate) {
// Make sure we do execute immediately if we are in the middle of a traversal or the damage
// done by dispatchDetachedFromWindow will cause havoc on return.
if (immediate && !mIsInTraversal) {
doDie();
return false;
}
......
mHandler.sendEmptyMessage(MSG_DIE);
return true;
} final class ViewRootHandler extends Handler {
......
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DIE:
doDie();
break;
......
}
}
}
如上代码验证了之前的猜想,removeView(View)和removeViewImmediate(View)的区别确实就在于是否立即移除。如果调用removeView,会通过handler来调用doDie(),而我们知道handler对应了一个MessageQueue的,需要排队等待执行的,这样就实现了延后执行。而如果调用removeViewImmediate,如果当前没有执行view的遍历,那就直接调用doDie()了。
//==========ViewRootImpl.java========
void doDie() {
......
synchronized (this) {
if (mRemoved) {
return;
}
mRemoved = true;
if (mAdded) {
dispatchDetachedFromWindow();
}
......
mAdded = false;
}
WindowManagerGlobal.getInstance().doRemoveView(this);
}
//移除的主要逻辑都在该方法内完成
void dispatchDetachedFromWindow() {
mView.dispatchDetachedFromWindow();
......
try {
mWindowSession.remove(mWindow);
} catch (RemoteException e) {
}
......
unscheduleTraversals();//停止绘制View
} //======View.java======
void dispatchDetachedFromWindow() {
......
onDetachedFromWindow();
......
} @CallSuper
protected void onDetachedFromWindow() {
//在view从window移除后会回调该方法,可以在其中做一些资源回收的操作,如终止动画、停止线程等。
} //========WindowManagerGlobal.java=======
//该方法主要用于刷新数据
void doRemoveView(ViewRootImpl root) {
synchronized (mLock) {
final int index = mRoots.indexOf(root);
if (index >= 0) {
mRoots.remove(index);
mParams.remove(index);
final View view = mViews.remove(index);
mDyingViews.remove(view);
}
}
......
} //==========Session.java=======
@Override
public void remove(IWindow window) {
mService.removeWindow(this, window);
} //==========WindowManagerService.java=======
void removeWindow(Session session, IWindow client) {
synchronized(mWindowMap) {
WindowState win = windowForClientLocked(session, client, false);
if (win == null) {
return;
}
win.removeIfPossible();
}
}
上述doDie()过程比较容易理解,第22行,参考addView中的逻辑分析可知,这里也是IPC方式,流程最终进入到了WMS中的removeWindow方法,同样到这里咱们不继续往下深入了。上述流程中,mRoots、mParams、mViews、mDyingViews四个集合也做了相应的操作。
上面讲到的三个主要方法中,可以看到它们都对mRoots、mParams、mViews、mDyingViews进行了刷新。
七、WMS
在前面的addView和removeView机制中,我们会发现具体的处理逻辑都交给了WMS(即WindowManagerService的简写)中,那WMS是什么呢?
WMS是一个非常重要的系统服务。它支撑着视图相关的各项业务,这非常符合软件设计的单一职责原则,其业务和ActivityManagerService(简称AMS)一起几乎占据了framework业务的半壁江山,可见其重要性。关于WMS的内容实在太多了,这里只简单介绍其大致功能以及启动流程。
1、WMS功能介绍
WMS的大概功能如下图所示:
这里先简单描述一下各项功能:
窗口管理:WMS是窗口管理者,结合WindowManager实现窗口的启动、添加、删除,以及管理窗口的大小、层级等。
窗口动画:在窗口切换时,使用窗口动画可以使这个过程看起来更炫更生动,这个窗口动画就是由WMS的动画子系统来负责的,动画子系统的管理者便是WindowAnimator。
输入系统的中转站: 触摸设备屏幕上的窗口时会产生触摸事件,InputManagerService(IMS)会对触摸事件进行处理,找到最合适的窗口来反馈事件。而WMS是这些窗口的管理者,那自然而然就成为了输入系统的中转站了。
Surface管理:窗口并不具备绘制功能,所以每个窗口都需要一个Surface来供自己绘制,WMS就是这个Surface的管理者。
该部分参考:Android解析WindowManagerService(一)WMS的诞生
2、WMS的启动流程
在我之前的文章:【系统之音】Android系统启动篇 中有介绍过系统的启动过程,其中提到过WMS的启动时机,这里从源码入手再稍微详细阐述一下:
//===========SystemServer.java==========
/**
* The main entry point from zygote.
*/
public static void main(String[] args) {
new SystemServer().run();
} private void run() {
......
startBootstrapServices();
startCoreServices();
startOtherServices();
......
} private void startOtherServices() {
......
WindowManagerService wm = null;
......
wm = WindowManagerService.main(context, inputManager,
mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL,
!mFirstBoot, mOnlyCore, new PhoneWindowManager());
ServiceManager.addService(Context.WINDOW_SERVICE, wm);
......
}
在第21行中得到了WMS的实例:
//=========WindowManagerService.java=======
private static WindowManagerService sInstance;
......
public static WindowManagerService main(final Context context, final InputManagerService im,
final boolean haveInputMethods, final boolean showBootMsgs, final boolean onlyCore,
WindowManagerPolicy policy) {
DisplayThread.getHandler().runWithScissors(() ->
sInstance = new WindowManagerService(context, im, haveInputMethods, showBootMsgs,
onlyCore, policy), 0);
return sInstance;
}
第7行调用了Handler的runWithScissors方法,该方法的作用是执行一个同步的task,即执行完第8行后才会执行第10行,所以main方法返回的是一个WMS实例。
第24行就是一个将WMS加入到系统服务的过程:
//============ServiceManager.java=========
/**
* Place a new @a service called @a name into the service
* manager.
*
* @param name the name of the new service
* @param service the service object
*/
public static void addService(String name, IBinder service) {
try {
getIServiceManager().addService(name, service, false);
} catch (RemoteException e) {
Log.e(TAG, "error in addService", e);
}
} private static IServiceManager sServiceManager;
private static IServiceManager getIServiceManager() {
if (sServiceManager != null) {
return sServiceManager;
}
// Find the service manager
sServiceManager = ServiceManagerNative
.asInterface(Binder.allowBlocking(BinderInternal.getContextObject()));
return sServiceManager;
}
这里调用的addService就是一个IPC过程,如果有阅读过AIDL实现Binder时编译生成的代码,看到ServiceManagerNative类后就会非常眼熟,它就是实现Binder的中间类。至于提供addService功能实现的Server是谁,笔者目前还未找到,所以先研究到这里,后续研究透彻了再补上。
到这里我们就清楚了,在SystemServer进程启动过程中,WMS启动,并通过ServiceManager中的addServie方法添加到系统中,供其它进程调用。
【系统之音】WindowManager工作机制详解的更多相关文章
- Hadoop框架:DataNode工作机制详解
本文源码:GitHub·点这里 || GitEE·点这里 一.工作机制 1.基础描述 DataNode上数据块以文件形式存储在磁盘上,包括两个文件,一个是数据本身,一个是数据块元数据包括长度.校验.时 ...
- Session的工作机制详解和安全性问题(PHP实例讲解)
我们先简单的了解一些http的知识,从而理解该协议的无状态特性.然后,学习一些关于cookie的基本操作.最后,我会一步步阐述如何使用一些简单,高效的方法来提高你的php应用程序的安全性以及稳定行. ...
- Hadoop框架:NameNode工作机制详解
本文源码:GitHub·点这里 || GitEE·点这里 一.存储机制 1.基础描述 NameNode运行时元数据需要存放在内存中,同时在磁盘中备份元数据的fsImage,当元数据有更新或者添加元数据 ...
- JVM结构、GC工作机制详解
JVM结构.内存分配.垃圾回收算法.垃圾收集器.下面我们一一来看. 一.JVM结构 根据<java虚拟机规范>规定,JVM的基本结构一般如下图所示: 从左图可知,JVM主要包括四个部分 ...
- JVM结构、GC工作机制详解(转)
原文地址:http://blog.csdn.NET/tonytfjing/article/details/44278233 JVM结构.内存分配.垃圾回收算法.垃圾收集器.下面我们一一来看. 一.JV ...
- 【转载】JVM结构、GC工作机制详解
文章主要分为以下四个部分 JVM结构.内存分配.垃圾回收算法.垃圾收集器.下面我们一一来看. 一.JVM结构 根据<java虚拟机规范>规定,JVM的基本结构一般如下图所示: 从左图可知, ...
- NIO组件Selector工作机制详解(上)
转自:http://blog.csdn.net/haoel/article/details/2224055 一. 前言 自从J2SE 1.4版本以来,JDK发布了全新的I/O类库,简称NIO,其不但 ...
- NIO组件Selector工作机制详解(下)
转自:http://blog.csdn.net/haoel/article/details/2224069 五. 迷惑不解 : 为什么要自己消耗资源? 令人不解的是为什么我们的Java的New I/ ...
- JVM、Gc工作机制详解
JVM主要包括四个部分: 类加载器(ClassLoad) 执行引擎 内存区: 本地方法接口:类似于jni调本地native方法 内存区包括四个部分: 1.方法区:包含了静态变量.常量池.构造函数等 2 ...
随机推荐
- javascript基础(五): jQuery
jQuery javaScript和jQuery的关系? jQuery库,里面存在大量的JavaScript函数 获取jQuery 公式:$(selector).action() <!DOCT ...
- JS 判断是否为数字 数字型特殊值
JS 数字型三个特殊值 Infinity ,代表无穷大,大于任何数值 -Infinity ,代表无穷小,小于任何数值 NaN ,Not a number,代表一个非数值 isNaN的使用: isNa ...
- Kafka Eagle V2.0.0新版预览
1.概述 Kafka Eagle是一款用于管理Kafka的监控系统,且完全开源.当前Kafka Eagle发布了2.0.0版本.今天笔者就为大家来介绍一下2.0.0更新了哪些功能. 官网地址:http ...
- vue 实现滑块验证码
图一为拖拽前效果,图二为拖拽后效果 一.新建文件JcRange.vue,代码如下: 1.模板代码: <template> <div class="jc-component_ ...
- bzoj2296【POJ Challenge】随机种子*
bzoj2296[POJ Challenge]随机种子 题意: 求一个≤10^16的数,使这个数包含123456789且为x的倍数.x≤1000000. 题解: 16-6刚好等于10.因此我们可以直接 ...
- unittest学习笔记
File "C:\Program Files\Python36\lib\site-packages\selenium\webdriver\remote\errorhandler.py&quo ...
- 手把手教你基于C#开发WinCC语音报警插件「附源代码」
写在前面 众所周知,WinCC本身是可以利用C脚本或者VBS脚本来做语音报警,但是这种方式的本质是调用已存在的音频文件,想要实现实时播报报警信息是不行的,灵活性还不够,本文主要介绍基于C#/.NET开 ...
- 黎曼函数ζ(2n)的几种求法
\(\zeta (2n)\)的几种求法 目录 $\zeta (2n)$的几种求法 结论 欧拉的证明 进一步探索,$\zeta$ 函数.余切.伯努利数的关系 傅立叶分析证明 留数法证明 参考资料 结论 ...
- C#中子类对基类方法的继承、重写和隐藏
提起子类.基类和方法继承这些概念,肯定大家都非常熟悉.毕竟,作为一门支持OOP的语言,掌握子类.基类是学习C#的基础.不过,这些概念虽然简单,但是也有一些初学者可能会遇到的坑,我们一起看看吧. 子 ...
- linux虚拟机正常安装完成后获取不到IP的解决办法-网卡
通常正常情况下安装完linux虚拟机,只需要使用桥接并修改配置文件/etc/sysconfig/network-scripts/ifcfg-eth0,将如下参数值改为如下: ONBOOT=yes NM ...