带着问题学习 Android Handler 消息机制

Marker_Sky 关注

 0.4 2018.02.06 18:04* 字数 3992 阅读 541评论 0喜欢 13
 
学习 Android Handler 消息机制

一、提出问题

面试时常被问到的问题:

  • 简述 Android 消息机制
  • Android 中 Handler,Looper,MessageQueue,Message 有什么关系?

这俩问题其实是一个问题,其实只要搞清楚了 Handler,Looper,MessageQueue,Message 的作用和联系,就理解了 Android 的 Handler 消息机制。那么再具体一点:

  1. 为什么在主线程可以直接使用 Handler?
  2. Looper 对象是如何绑定 MessageQueue 的?
  3. MessageQueue 里的消息从哪里来?Handler是如何往MessageQueue中插入消息的?
  4. Message 是如何绑定 Handler 的?
  5. Handler 如何绑定 MessageQueue?
  6. 关于 handler,在任何地方 new handler 都是什么线程下?
  7. Looper 循环拿到消息后怎么处理?

二、解决问题

那么,我们从主线程的消息机制开始分析:

2.1 主线程 Looper 的创建和循环

Android 应用程序的入口是 main 函数,主线程 Looper 的创建也是在这里完成的。

ActivityThread --> main() 函数

public static void main(){
// step1: 创建主线程Looper对象
Looper.prepareMainLooper(); ActivityThread thread = new ActivityThread();
// 绑定应用进程,布尔标记是否为系统进程
thread.attach(false);
// 实例化主线程 Handler
if(sMainThreadHandler == null){
sMainThreadHandler = thread.getHandler();
}
// step2: 开始循环
Loop.loop(); throw new RuntimeException("Main thread loop unexpectedly exited");
}

Looper.prepareMainLooper()用来创建主线程的 Looper 对象,接下来先看这个方法的实现。

2.1.1 创建主线程 Looper

Looper --> prepareMainLooper()

private static Looper sMainLooper;  // guarded by Looper.class

public static void prepareMainLooper(){
// step1: 调用本类 prepare 方法
prepare(false);
// 线程同步,如果变量 sMainLooper 不为空抛出主线程 Looper 已经创建
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
// step2: 调用本类 myLooper 方法
sMainLooper = myLooper();
}
}

prepareMainLooper() 方法主要是使用 prepare(false) 创建当前线程的 Looper 对象,再使用 myLooper() 方法来获取当前线程的 Looper 对象。

step1: Looper --> prepare()

// ThreadLocal 为每个线程保存单独的变量
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
// Looper 类的 MessageQueue 变量
final MessageQueue mQueue;
// quitAllowed 是否允许退出,这里是主线程的 Looper 不可退出
private static void prepare(boolean quitAllowed) {
// 首先判定 Looper 是否存在
if(sThreadLocal.get() != null){
throw new RuntimeException("Only one Looper may be created per thread");
}
// 保存线程的副本变量
sThreadLoacal.set(new Looper(quitAllowed));
} private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
  • prepare() 方法中用 ThreadLocal 来保存主线程的 Looper 对象。ThreadLocal 可以看作是一个用来储存数据的类,类似 HashMap、ArrayList等集合类,它存放着属于当前线程的变量。

  • ThreadLocal 提供了 get/set 方法分别用来获取和保存变量。
    比如在主线程通过 prepare() 方法来创建 Looper 对象,并使用 sThreadLoacal.set(new Looper(quitAllowed)) 来保存主线程的 Looper 对象,那么在主线程调用 myLooper()(实际调用了 sThreadLocal.get() 方法) 就是通过 ThreadLocal 来获取主线程的 Looper 对象。如果在子线程调用这些方法就是通过 ThreadLocal 保存和获取属于子线程的 Looper 对象。

更多关于 ThreadLocal 的原理:

深入剖析ThreadLocal实现原理以及内存泄漏问题

问题1:为什么在主线程可以直接使用 Handler?
因为主线程已经创建了 Looper 对象并开启了消息循环,通过上文的代码就可以看出来。

问题2:Looper 对象是如何绑定 MessageQueue 的?或者说 Looper 对象创建 MessageQueue 过程。
很简单,Looper 有个一成员变量 mQueue,它就是 Looper 对象默认保存的 MessageQueue。上面代码中 Looper 有一个构造器,新建 Looper 对象时会直接创建 MessageQueue 并赋值给 mQueue。
问题2解决:在 new Looper 时就创建了 MessageQueue 对象并赋值给 Looper 的成员变量 mQueue。

step2: Looper --> myLooper()

// 也就是使用本类的ThreadLocal对象获取之前创建保存的Looper对象
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}

这个方法就是通过 sThreadLocal 变量获取当前线程的 Looper 对象,比较常用的一个方法。上文主线程 Looper 对象创建后使用该方法获取了 Looper 对象。

2.1.2 开始循环处理消息

回到最开始的 main() 函数,在创建了 Looper 对象以后就调用了 Looper.loop() 来循环处理消息,贴一下大致代码:

public static void main(){
// step1: 创建主线程Looper对象
Looper.prepareMainLooper();
...
// step2: 开始循环
Loop.loop();
}

Looper --> loop()

public static void loop() {
// step1: 获取当前线程的 Looper 对象
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
// step2: 获取 Looper 保存的 MessageQueue 对象
final MessageQueue queue = me.mQueue; ...
// step3: 循环读取消息,如果有则调用消息对象中储存的 handler 进行发送
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
...
try {
// step4: 使用 Message 对象保存的 handler 对象处理消息
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
...
msg.recycleUnchecked();
}
}
  • step1 : myLooper() 方法就是通过 ThreadLocal 获取当前线程的 Looper 对象,注意在哪个线程使用该方法就获取的该线程的 Looper 对象。
  • step2 :me.mQueue,这个 mQueue 就是上面问题2所说的在 Looper 对象创建时新建的 MessageQueue 变量。
  • step3 :接下来是一个 for 循环,首先通过 queue.next() 来提取下一条消息,具体是怎么提取的可以参考下面文章的 4.2 节:

Android消息机制1-Handler(Java层)

获取到下一条消息,如果 MessageQueue 中没有消息,就会进行阻塞。那么如果存在消息,它又是怎么放入 MessageQueue 的呢?或者说MessageQueue 里的消息从哪里来?Handler是如何往MessageQueue中插入消息的?先不说这个,把这个问题叫作问题3后面分析。

  • step4 :msg.target.dispatchMessage(msg);这个方法最终会调用 Handler 的 handleMessage(msg) 方法。同时这里又产生个问题:msg.target 是何时被赋值的?,也就是说Message 是如何绑定 Handler 的?先称之为问题4。那么接着看 Handler 的 dispatchMessage 方法:

Handler --> dispatchMessage

public void dispatchMessage(Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
} private static void handleCallback(Message message) {
message.callback.run();
} public void handleMessage(Message msg) {
}

可以看到该方法最后执行了 handleMessage() 方法,这是一个空方法也就是需要我们覆写并实现的。另外 dispatchMessage() 也体现出一个问题:

消息分发的优先级:

  • Message 的回调方法:message.callback.run(); 优先级最高;
  • Handler 的回调方法:mCallback.handleMessage(msg)优先级次于上方;
  • Handler 的回调方法:handleMessage() 优先级最低。

到这里 Looper 循环并通过 Handler 发送消息有一个整体的流程了,接下来分析 Handler 在消息机制中的主要作用以及和 Looper、Message 的关系。

2.2 Handler 的创建和作用

上面说到 loop() 方法在不断从消息队列 MessageQueue 中取出消息(queue.next() 方法),如果没有消息则阻塞,反之交给 Message 绑定的 Handler 处理。回顾一下没解决的两个问题:

  • 问题3:MessageQueue 里的消息从哪里来?Handler 是如何往 MessageQueue 中插入消息的?
  • 问题4:msg.target 是何时被赋值的?,也就是说Message 是如何绑定 Handler 的?

既然要解决 Handler 插入消息的问题,就要看 Handler 发送消息的过程。

2.2.1 Handler 发送消息

Handler --> sendMessage(Message msg);

final MessageQueue mQueue;

public final boolean sendMessage(Message msg){
return sendMessageDelayed(msg, 0);
}
// 发送延时消息
public final boolean sendMessageDelayed(Message msg, long delayMillis){
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
// 指定时间发送消息
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
// 处理消息,赋值 Message 对象的 target,消息队列插入消息
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
msg.target = this;
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}

可以看到调用 sendMessage(Message msg) 方法最终会调用到 enqueueMessage() 方法,这个方法主要有两个作用:赋值 Message 对象的 target、消息队列插入消息。

  • 赋值 msg 的 target:msg.target = this 把发送消息的 Handler 赋值给 msg 对象的 target。那么问题 4 就解决了:Handler 执行发送消息的过程中将自己绑定给了 Message 的 target,这样两者之间就产生了联系;
  • 消息队列插入消息:queue.enqueueMessage(msg, uptimeMillis) queue 是 MessageQueue 的一个实例,queue.enqueueMessage(msg, uptimeMillis)是执行 MessageQueue 的enqueueMessage方法来插入消息。这样问题 3 就找到答案:Handler 在发送消息的时候执行 MessageQueue 的enqueueMessage方法来插入消息;关于 MessageQueue 是怎么执行插入消息的过程,参考下方文章 4.3 节

Android消息机制1-Handler(Java层)

  • 上面 Handler 发送消息使用了 MessageQueue 的实例 queue,可以看到这个 queue 是上一个方法 sendMessageAtTime 中由 Handler 的成员变量 mQueue 赋值的,那么 mQueue 是哪来的?问题 5:Handler 如何绑定 MessageQueue?先剧透一下 Handler 绑定的是 Looper 的 MessageQueue 对象,Looper 的 MessageQueue 对象是在 Looper 创建时就 new 的。

要了解 Handler 的 MessageQueue 对象是怎么赋值的就要看 Handler 的构造函数了,Handler 创建的时候作了一些列操作比如获取当前线程的 Looper,绑定 MessageQueue 对象等。

2.2.2 Handler 的创建

下面是 Handler 无参构造器和主要的构造器,另外几个重载的构造器有些是通过传递不同参数调用包含两个参数的构造器。两个参数构造函数第一个参数为 callback 回调,第二个函数用来标记消息是否异步。

// 无参构造器
public Handler() {
this(null, false);
} public Handler(Callback callback, boolean async) {
if (FIND_POTENTIAL_LEAKS) {
final Class<? extends Handler> klass = getClass();
if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
(klass.getModifiers() & Modifier.STATIC) == 0) {
Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
klass.getCanonicalName());
}
}
// step1:获取当前线程 Looper
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
// step2:获取 Looper 对象绑定的 MessageQueue 对象并赋值给 Handler 的 mQueue
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
}
  • step1:调用myLooper() 方法,该方法是使用 sThreadLocal 对象获取当前线程的 Looper 对象,回顾一下:
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}

如果获取的 Looper 对象为 null,说明没有执行 Looper.prepare() 为当前线程保存 Looper 变量,就会抛出 RuntimeException。这里又说明了Handler 必须在有 Looper 的线程中使用,报错不说,没有 Looper 就无法绑定 MessageQueue 对象也就无法进行更多有关消息的操作。

  • step2:mQueue = mLooper.mQueue 说明了 Handler 的 MessageQueue 对象是由当前线程 Looper 的 MessageQueue 对象赋值的。这里问题 5 解决:Handler 在创建时绑定了当前线程 Looper 的 MessageQueue 对象。
  • 由于 Handler 和 Looper 可以看作使用的是同一个 MessageQueue 对象,所以 Handler 和 Looper 可以共享消息队列 MessageQueue。Handler 发送消息(用 mQueue 往消息对列插入消息),Looper 可以方便的循环使用 mQueue 查询消息,如果查询到消息,就可以用 Message 对象绑定的 Handler 对象 target 去处理消息,反之则阻塞。

既然说到了 Handler 的构造器,就想到一个问题:问题 6:关于 handler,在任何地方 new handler 都是什么线程下?这个问题要分是否传递 Looper 对象来看。

  1. 不传递 Looper 创建 Handler:Handler handler = new Handler();上文就是 Handler 无参创建的源码,可以看到是通过 Looper.myLooper() 来获取 Looper 对象,也就是说对于不传递 Looper 对象的情况下,在哪个线程创建 Handler 默认获取的就是该线程的 Looper 对象,那么 Handler 的一系列操作都是在该线程进行的。
  2. 传递 Looper 对象创建 Handler:Handler handler = new Handler(looper);那么看看传入 Looper 的构造函数:
public Handler(Looper looper) {
this(looper, null, false);
}
public Handler(Looper looper, Callback callback) {
this(looper, callback, false);
}
// 第一个参数是 looper 对象,第二个 callback 对象,第三个消息处理方式(是否异步)
public Handler(Looper looper, Callback callback, boolean async) {
mLooper = looper;
mQueue = looper.mQueue;
mCallback = callback;
mAsynchronous = async;
}

可以看出来传递 Looper 对象 Handler 就直接使用了。所以对于传递 Looper 对象创建 Handler 的情况下,传递的 Looper 是哪个线程的,Handler 绑定的就是该线程。

到这里 Looper 和 Handler 就有一个大概的流程了,接下来看一个简单的子线程 Handler 使用例子:

new Thread() {
@Override
public void run() {
// step1
Looper.prepare();
// step2
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
if(msg.what == 1){
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this,"HandlerTest",Toast.LENGTH_SHORT).show();
}
});
// step5
Looper.myLooper().quit();
}
}
};
// step3
handler.sendEmptyMessage(1);
// step4
Looper.loop();
}
}.start();
  • step1: 调用 Looper.prepare(); 为当前线程创建 Looper 对象,同时也就创建了 MessageQueue,之后将该线程的 Looper 对象保存在 ThreadLocal 中。注意这里的一切操作都在子线程中,如果不调用 Looper.prepare() 就使用 Handler 会报错。
  • step2: 创建 Handler 对象,覆写 handleMessage 处理消息,等待该 Handler 发送的消息处理时会调用该方法。
  • step3: 使用 handler 发送消息,这里只是示例,毕竟自己给自己发送消息没啥必要。发送的过程中会将自己赋值给 msg.target,然后再将消息插入到 Looper 绑定的 MessageQueue 对象中。
  • step4: 调用 Looper.loop(); 首先获取当前线程的 Looper 对象,根据 Looper 对象就可以拿到 Looper 保存的 MessageQueue 对象 mQueue。有了 MessageQueue 对象就可以 for 循环获取它保存的消息 Message 对象,如果消息不存在就返回 null 阻塞,反之则使用 Message 中保存的 Handler:msg.target 来处理消息,最终调用 handleMessage 也就是之前覆写的方法来处理消息。
  • step5: 逻辑处理完毕以后,应在最后使用 quit 方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。

三、总结和其它

3.1 Handler、Looper、MessageQueue、Message

  1. Handler 用来发送消息,创建时先获取默认或传递来的 Looper 对象,并持有 Looper 对象包含的 MessageQueue,发送消息时使用该 MessageQueue 对象来插入消息并把自己封装到具体的 Message 中;
  2. Looper 用来为某个线程作消息循环。Looper 持有一个 MessageQueue 对象 mQueue,这样就可以通过循环来获取 MessageQueue 所维护的 Message。如果获取的 MessageQueue 没有消息时,便阻塞在 loop 的queue.next() 中的 nativePollOnce() 方法里,反之则唤醒主线程继续工作,之后便使用 Message 封装的 handler 对象进行处理。
  3. MessageQueue 是一个消息队列,它不直接添加消息,而是通过与 Looper 关联的 Handler 对象来添加消息。
  4. Message 包含了要传递的数据和信息。

3.2 Android中为什么主线程不会因为Looper.loop()里的死循环卡死?

这是知乎上的问题,感觉问的挺有意思。平时可能不太会太深究这些问题,正好有大神回答那就记录一下吧。

  1. 为什么不会因为死循环卡死?
    线程可以看作是一段可执行代码,当代码执行完毕线程的生命周期就该终止了。对于主线程来说我们不希望它执行一段时间后退出,所以简单做法就是可执行代码是能一直执行下去的,死循环便能保证不会被退出。既然是死循环那么怎么去处理消息呢,通过创建新线程的方式。
  2. 为这个死循环准备了一个新线程
    在进入死循环之前便创建了新binder线程,在代码ActivityThread.main()中:
public static void main(){
...
Looper.prepareMainLooper(); //创建ActivityThread对象
ActivityThread thread = new ActivityThread(); //建立Binder通道 (创建新线程)
thread.attach(false); if(sMainThreadHandler == null){
sMainThreadHandler = thread.getHandler();
}
// step2: 开始循环
Loop.loop(); throw new RuntimeException("Main thread loop unexpectedly exited");
}

thread.attach(false);便会创建一个Binder线程(具体是指ApplicationThread,Binder的服务端,用于接收系统服务AMS发送来的事件),该Binder线程通过Handler将Message发送给主线程。

  1. 主线程的死循环一直运行是不是特别消耗CPU资源呢?

其实不然,这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,详情见Android消息机制1-Handler(Java层),此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资。

  1. Activity的生命周期是怎么实现在死循环体外能够执行起来的?
    上文 main 函数有一部分获取 sMainThreadHandler 的代码:
final H mH = new H();

public static void main(){
...
if(sMainThreadHandler == null){
sMainThreadHandler = thread.getHandler();
}
...
} final Handler getHandler() {
return mH;
}

类 H 继承了 Handler,在主线程创建时就创建了这个 Handler 用于处理 Binder 线程发送来的消息。

Activity的生命周期都是依靠主线程的Looper.loop,当收到不同Message时则采用相应措施:

在H.handleMessage(msg)方法中,根据接收到不同的msg,执行相应的生命周期。
比如收到msg=H.LAUNCH_ACTIVITY,则调用ActivityThread.handleLaunchActivity()方法,最终会通过反射机制,创建Activity实例,然后再执行Activity.onCreate()等方法;
再比如收到msg=H.PAUSE_ACTIVITY,则调用ActivityThread.handlePauseActivity()方法,最终会执行Activity.onPause()等方法。 上述过程,我只挑核心逻辑讲,真正该过程远比这复杂。

3.3 Handler 使用造成内存泄露

  1. 有延时消息,要在Activity销毁的时候移除Messages
  2. 匿名内部类导致的泄露改为匿名静态内部类,并且对上下文或者Activity使用弱引用。

具体操作可以参考文章:

Handler内存泄露原理及解决方法

参考资料:

《Android 开发艺术探索》
Android消息机制1-Handler(Java层)
Android Handler消息机制实现原理

[ 转载 ] Handler详解的更多相关文章

  1. Message,MessageQueue,Looper,Handler详解+实例

    Message,MessageQueue,Looper,Handler详解+实例 原文地址 Android的Handler使用(这篇简单介绍Handler的使用) 一.几个关键概念 1.Message ...

  2. Message,MessageQueue,Looper,Handler详解

    Message,MessageQueue,Looper,Handler详解   一.几个关键概念 1.MessageQueue:是一种数据结构,见名知义,就是一个消息队列,存放消息的地方.每一个线程最 ...

  3. [转载]Pytorch详解NLLLoss和CrossEntropyLoss

    [转载]Pytorch详解NLLLoss和CrossEntropyLoss 来源:https://blog.csdn.net/qq_22210253/article/details/85229988 ...

  4. Android多线程----异步消息处理机制之Handler详解

    ​[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/ ...

  5. (转载)详解网络传输中的三张表,MAC地址表、ARP缓存表以及路由表

    郑重声明:原文转载于http://dengqi.blog.51cto.com/5685776/1223132 向好文章致敬!!! 一:MAC地址表详解 说到MAC地址表,就不得不说一下交换机的工作原理 ...

  6. 转载+++++iptables详解+++++转载

    转载:http://blog.chinaunix.net/uid-26495963-id-3279216.html 一.前言 防火墙,其实说白了讲,就是用于实现Linux下访问控制的功能的,它分为硬件 ...

  7. (转载)详解7.0带来的新工具类:DiffUtil

    [Android]详解7.0带来的新工具类:DiffUtil 标签: diffutil 2017-04-17 18:21 226人阅读 评论(0) 收藏 举报  分类: Android学习笔记(94) ...

  8. 【Android面试查漏补缺】之Handler详解,带你全面理解Handler消息机制

    在安卓面试中,关于 Handler 的问题是必备的,但是这些关于 Handler 的知识点你都知道吗? 一.题目层次 Handler 的基本原理 子线程中怎么使用 Handler MessageQue ...

  9. 【转载】详解CreateProcess调用内核创建进程的过程

    原文:详解CreateProcess调用内核创建进程的过程 昨天同学接到了腾讯的电面,有一题问到了CreateProcess创建进程的具体实现过程,他答得不怎么好吧应该是, 为了以防万一,也为了深入学 ...

随机推荐

  1. Jetson tx1 安装ROS

    注意,是 Jetson TX1 系统版本: R24.2 参考链接: https://www.youtube.com/watch?v=-So2P0kRYsk

  2. JS window对象的top、parent、opener含义介绍

    1.top该变更永远指分割窗口最高层次的浏览器窗口.如果计划从分割窗口的最高层次开始执行命令,就可以用top变量. 2.openeropener用于在window.open的页面引用执行该window ...

  3. python模块分析之typing(三)

    前言:很多人在写完代码一段时间后回过头看代码,很可能忘记了自己写的函数需要传什么参数,返回什么类型的结果,就不得不去阅读代码的具体内容,降低了阅读的速度,加上Python本身就是一门弱类型的语言,这种 ...

  4. java调用monkeyrunner(亲测绝对可行)

    我自己试验了下和官方的API编写不太一样,老别扭了,建议还是用Python写吧 昨天在网上查了一下一天,都是转来贴别人的,真正敲的很少,我真不知道转的大侠你们自己敲了么? 先截一段不负责任的blog图 ...

  5. Go语言规格说明书 之 通道类型(Channel types)

    go version go1.11 windows/amd64 本文为阅读Go语言中文官网的规则说明书(https://golang.google.cn/ref/spec)而做的笔记,介绍Go语言的 ...

  6. C 长字符串换行方法

    C中字符串有时候会出现很长的情况,如果不换行书写查看起来很不方便. 长字符串拆分成多行处理也是C规范的一部分. 方法1. 利用双引号" " ,将长字符串分成多个子串换行,C会自动无 ...

  7. LeetCode(8):字符串转整数(atoi)

    Medium! 题目描述: 实现 atoi,将字符串转为整数. 在找到第一个非空字符之前,需要移除掉字符串中的空格字符.如果第一个非空字符是正号或负号,选取该符号,并将其与后面尽可能多的连续的数字组合 ...

  8. noip 2017 时间复杂度

    自认为是少有的复杂的代码 这题思想很简单,就是大模拟 对于for循环,一行读入4个字符串,然后分类讨论: ①:如果是一个正常的O(n),那么累计n的指数加1 ②:如果是一个常数级别的,那么继续循环,但 ...

  9. Jmeter NonGUI模式

    一般情况下我们都是在NonGUI模式下运行jmeter.这样做有两个好处 节省系统资源,能够产生更大的负载 可以通过命令行参数对测试场景进行更精细的配置 示例 创建luzhi.jmx脚本 jmeter ...

  10. form总结

    在Javascript 中,页面上的每一对<form> 标记都解析为一个对象,即form 对象. 可以通过document.forms 获取以源顺序排列的文档中所有form 对象的集合. ...