欢迎光临
我们一直在努力

Handler核心源码分析

目录

Handler的使用

Handler初始化

发送消息

处理消息

MessageQueue的阻塞和唤醒

阻塞

唤醒

Handler对我们开发者的启发

亮点一

亮点二

Looper什么时候推出

Handler常见面试题


前言

对于一名开发者来说,阅读源码是一项必修的课程。在学习源码的过程中,我们可以了解到设计模式与源代码开发者的开发习惯。而在阅读源码的过程中,我一直秉承着郭霖大神的那句话“抽丝剥茧、点到即止”,我们没有必要完全深入每一行代码,通常我们可能只需要知道这一行代码的作用就行了。

大家有没有发现,我们在Android开发过程中,很少遇到过多线程并发的问题,这个就得益于Android为我们提供的线程间通信工具 handler了,所以我们要了解它是怎么实现跨线程通信的。

Handler的使用

我们首先知道Handler是怎么用的,再去剖析其核心源码。

import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.util.Log; import android.widget.TextView; public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private TextView mTvMain; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); } @Override protected void onStart() { super.onStart(); new Thread(() -> { try { // 模拟耗时操作 Thread.sleep(5000); // 获取 Message 实例对象 Message msg = Message.obtain(); // 设置 Message 对象的识别内容 msg.what = Constants.MSG_UPDATE_TEXT; // 通过 Handler 把消息发送出去 handler.sendMessage(msg); } catch (InterruptedException e) { Log.e(TAG, "onStart: InterruptedException"); } }).start(); } private void initView() { mTvMain = findViewById(R.id.tv_main); } @SuppressLint("HandlerLeak") private Handler handler = new Handler() { @Override public void handleMessage(@NonNull Message msg) { switch (msg.what) { case Constants.MSG_UPDATE_TEXT: mTvMain.setText("已完成更新操作"); } } }; }

接下来我们就按照上面代码案例去剖析Handler源码,顺序依次是Handler初始化、

Handler初始化

Handler核心源码分析

最终会调用如下方法:

 /* * 将此标志设置为 true 以检测扩展此 Handler 类的匿名类、本地类或成员类。 这种类可能会造成泄漏。 */ private static final boolean FIND_POTENTIAL_LEAKS = false; // 如果我们没有调用Looper.prepare(),sThreadLocal.get() 将返回null // @UnsupportedAppUsage 不让我们开发者使用 static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); final Looper mLooper; final MessageQueue mQueue; // @UnsupportedAppUsage 不让我们开发者使用 final Handler.Callback mCallback; final boolean mAsynchronous; public Handler(@Nullable Handler.Callback callback, boolean async) { if (FIND_POTENTIAL_LEAKS) { final Class<? extends Handler> klass = getClass(); // 如果我们创建的Handler类对象是匿名类,或者是成员类(内部类)、或者局部类(方法中创建的类)并且该类不是静态的 // 就有内存泄漏的风险。 if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &amp;& (klass.getModifiers() & Modifier.STATIC) == 0) { Log.w(TAG, "The following Handler class should be static or leaks might occur: " + klass.getCanonicalName()); } } mLooper = Looper.myLooper(); if (mLooper == null) { throw new RuntimeException( "Can't create handler inside thread " + Thread.currentThread() + " that has not called Looper.prepare()"); } mQueue = mLooper.mQueue; mCallback = callback; mAsynchronous = async; } /** * 返回与当前线程关联的 Looper 对象。 如果调用线程未与 Looper 关联,则返回 null。 * * @return Looper对象 */ public static @android.annotation.Nullable Looper myLooper() { return sThreadLocal.get(); }

这里我们重点关注一下 mLooper = Looper.myLooper(); 这行代码,myLooper的实现是:sThreadLocal.get();这里我们就要说说 ThreadLocal了,可参考我的另一篇博文:并发编程基础(二)—— ThreadLocal及CAS基本原理剖析

其实说白了,ThreadLocal 正如其名,它就是一个线程本地副本,我们的线程内部会有一个ThreadLocal.ThreadLocalMap的成员:

Handler核心源码分析

再来看看具体的get方法:

 /** * 返回此线程中 ThreadLocalMap成员以ThreadLocal为key对应的值(Object类型),如果ThreadLocalMap成员为null, * 则首先将其初始化,调用 {@link #initialValue} 。 * * @return 返回当前线程的中 ThreadLocal 对应的值 */ public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的 ThreadLocal.ThreadLocalMap 成员 ThreadLocalMap map = getMap(t); // 如果map不为空 if (map != null) { // 通过 ThreadLocal 获取Entry ThreadLocalMap.Entry e = map.getEntry(this);// 这里的this就是当前线程副本 ThreadLocal 的实例 // 如果 Entry 不为空,返回它的 value 属性的值 if (e != null) { @SuppressWarnings("unchecked") T result = (T) e.value; return result; } } //否则设置初始值 return setInitialValue(); }

上面的代码包含了 ThreadLocalMap.Entry:

Handler核心源码分析

我们发现 Entry 的构造函数包含 ThreadLocal 和 Object,而它又是 ThreadLocalMap 的内部类,那我们就可以理解为 ThreadLocalMap 是以 ThreadLocal 为 key,任意对象为 value 的键值对结构(Map结构),即是一一对应的。

既然有 get 方法,那就有 set 方法了,我们发现在 Looper.prepare() 调用了 set:

 static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); private static void prepare(boolean quitAllowed) { // 如果已经给当前线程的 ThreadLocal 设置过 一个Looper,则抛出异常,这就保证了 一个 ThreadLocal 只对应一个 Looper if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } // 否则 new 一个 Looper对象,并设置给当前线程的 ThreadLocal sThreadLocal.set(new Looper(quitAllowed)); } // Looper 的构造方法 private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); }

通过阅读如上代码,我们可以得出如下结论:

【1】Handler机制中,Thread 和 Looper是一一对应的:

要实现这个,我们就要保证我当前线程中只有唯一一个ThreadLocal对象可以绑定 Looper对象,且只能绑定一次。

Looper的构造方法是私有的,我们开发人员无法通过其他 ThreadLocal对象绑定 Looper,也就说说我们只能通过在某一个线程中调用 Looper.prepare实现ThreadLocal 和Looper 绑定,而 sThreadLocal 又是static final的,那也就是说一旦线程调用了 Looper .prepre 这个静态方法, sThreadLocal 就会被赋值,并且一旦赋值就不可更改,也就是说所有线程只能通过这一个 sThreadLocal 与Looper对象绑定。

而 prepare方法中,又通过判断 sThreadLocal.get() 是否为 null 的逻辑,保证了每一个线程的 sThreadLocal 只会调用一次 set 方法,即只能绑定一次 Looper。

Handler核心源码分析

【2】MessageQueue和 Looper一一对应

我们在 Android 源码中全局搜索 MessageQueue 的构造方法,发现只有在 Looper 的构造方法中调用了 new MessageQueue(quitAllowed),并且 MessageQueue 的构造方法是包管理权限,也就是说我们普通开发者是不能调用的,那也就说明了通过 Looper 构造方法唯一创建 MessageQueue 对象,实现了 Looper 和 MessageQueue 的一一对应。

综合以上两个结论,我们得出 Handler机制中,Thread、ThreadLocal、Looper、MessageQueue这四者是一一对应。

发送消息

Handler的主要方法

Handler核心源码分析

调用流程:

Handler.sendMessage() --> Handler.sendMessageDelayed()--> Handler.sendMessageAtTime()--> Handler.enqueueMessage()--> MessageQueue.enqueueMessage()

可以看到最终会调用 MessageQueue.enqueueMessage() 将 Message 插入队列。

Handler核心源码分析
Handler工作流程图

那么接下来我们剖析enquque方法:

 Message next; // Message的成员 Message mMessages;// MessageQueue 的成员,可以看作是队列的队头 boolean enqueueMessage(Message msg, long when) { // 省略不是很重要的代码 // 加锁保证线程安全,防止多个线程同时给MessageQueue中插入消息 synchronized (this) { // 省略不是很重要的代码 // 给要插入消息队列(MessageQueue)中的消息指定延迟时间,也就是在多久之后处理此消息 msg.when = when; // 把消息队列的队头赋值给 p Message p = mMessages; // 是否唤醒消息队列 boolean needWake; // 最开始,消息队列肯定是空的。那如果当前消息队列中没有消息,或者我们插入的消息的等待时间是0,那我们就把消息插入, // 并让其指向null; // 或者消息队列中有一个即将处理的消息,而我们插入的消息等待时间小于即将要处理的消息,那就把我们插入的消息放在其前面。 if (p == null || when == 0 || when < p.when) { // 插入的消息的下一个消息指向p,p可能为null msg.next = p; // 把我们要插入的消息赋值给消息队列的队头,实现插入队列操作 mMessages = msg; // 当消息队列中没有消息时,就会阻塞,此时 mBlocked为 true needWake = mBlocked; } // 这种情况是消息队列中有 N多个消息了 else { // 插入队列中间。 通常我们不必唤醒消息队列,除非队列头部有屏障,并且消息是队列中最早的异步消息。 needWake = mBlocked && p.target == null && msg.isAsynchronous(); Message prev; // 通过死循环,不停地比较我们要插入的消息与队列中已有的消息 for (;;) { // 当前消息队列中用于作比较的消息作为前一个消息 prev = p; // 然后让 prev指向 p(p指向p.next) p = p.next; // 从消息队列中的第一个消息开始遍历,直到消息队列的末尾即null值,方可跳出循环; // 或者要插入的消息等待时间小于当前我们正在做比较的消息,此时也跳出循环。 if (p == null || when < p.when) { break; } } // 插入消息 msg.next = p; // invariant: p == prev.next prev.next = msg; } } return true; }

Handler核心源码分析

Handler核心源码分析
Message入队示意图

处理消息

入队我们讲完了,下来该讲出队了。出队我们是通过 Looper.loop()实现:

 /** * 在当前线程中运行消息队列。 请务必调用 {@link #quit()} 来结束循环。以下代码省略不必要(看不懂)的代码进行解析 */ public static void loop() { // 获取当前线程的 Looper 对象 final Looper me = myLooper(); if (me == null) { // 这个异常在我们初学者会经常遇到,因为初学者总是忘记在子线程调用 Looper.prepare() throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } // 获取当前线程的 MessageQueue 对象 final MessageQueue queue = me.mQueue; for (; ; ) { // 取出消息 --- 出队 Message msg = queue.next(); // 可能会阻塞,当MessageQueue没有消息是,会调用nativePollOnce(ptr, -1);一直挂起 if (msg == null) { // 没有消息表示消息队列正在推出 return; } try { // target是Message中的 Handler 成员 msg.target.dispatchMessage(msg); } catch (Exception exception) { } finally { } // 回收可能正在使用的消息 msg.recycleUnchecked(); } }

loop方法中包含一个死循环,通过 MessageQueue.next() 不停地取出消息:

 /** * 消息出队,同样省略 N多行不是很重要的(看不懂的)代码 * * 尝试检索下一条消息, 如果找到返回。 * * @return 返回即将要被处理的消息 */ private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>(); Message next() { int pendingIdleHandlerCount = -1; // -1 only during first iteration int nextPollTimeoutMillis = 0; for (;;) { // 调用 native 方法睡眠 nextPollTimeoutMillis 这么多毫秒,如果该值为-1,则会无限等待 nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { // 自启动以来的非睡眠正常运行时间毫秒数。 final long now = SystemClock.uptimeMillis(); Message prevMsg = null; Message msg = mMessages;// mMessages可以看作是消息队列的队头 if (msg != null && msg.target == null) { // 被一道屏障挡住了,查找队列中的下一条异步消息。 do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } if (msg != null) { if (now < msg.when) { // 下一条消息尚未准备好,设置延迟时间以在它准备好时唤醒。 nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); } else { // 下一条消息已就绪,等待被处理,mBlocked置为 false mBlocked = false; if (prevMsg != null) { prevMsg.next = msg.next; } else { mMessages = msg.next; } msg.next = null; // 返回一个消息等待处理 return msg; } } else { // 没有消息的情况 nextPollTimeoutMillis = -1; } if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) { // mIdleHandlers是一个ArrayList,通过 addIdleHandler 添加,一般我们不会调用此方法, // 所以大多数情况下 mIdleHandlers.size()是0 pendingIdleHandlerCount = mIdleHandlers.size(); } if (pendingIdleHandlerCount <= 0) { // 没有要运行的空闲 Handler ,循环并等待更多时间 ,所以大多情况下是一直阻塞的,这也就解释了为什么我们的 // Activity 显示出来之后,我们只要一直亮屏,它就不会结束,因为ActivityThread的main方法调用了Looper.loop, // 使得程序一直挂起。 mBlocked = true; continue; } } } }

最终调用 Handler.dispatchMessage(),而 Handler.dispatchMessage() 就会回调 handleMessage()。

Handler核心源码分析

从对MessageQueue的源码剖析过程中,我们可以得出它是一个由单链表构成的具有优先级的队列。为什么这么说呢?
单链表:Message的next成员也是Message,形成一个单链表。

优先级:体现在每次消息入队的时候,我们最终调用 enqueueMessage(Message msg, long when) 方法,这里的 when 指的是消息等待处理的时间或者叫做延迟时间,而消息的处理同样会根据这个等待时间的大小顺序来。

队列:满足先进先出规则。

MessageQueue的阻塞和唤醒

阻塞

其实在上面的代码示例中,我已经讲过在调用 MessageQueue.next() 时,会调用 nativePollOnce(ptr, nextPollTimeoutMillis) 实现阻塞,当 nextPollTimeoutMillis 是-1时,会一直阻塞。没明白的同学再好好看看上面的代码(一定要看注释)。

唤醒

在MessageQueue.enqueueMessage() 中有这样一段逻辑:

// 是否需要唤醒 boolean needWake; // 如果消息队列没有消息,那么 mBlocked 就为 true,那我插入消息时就得唤醒消息队列 if (p == null || when == 0 || when < p.when) { // New head, wake up the event queue if blocked. msg.next = p; mMessages = msg; // 如果 mBlocked 为true,即队列阻塞,则需要唤醒 needWake = mBlocked; } if (needWake) { // 调用 native 方法唤醒队列 nativeWake(mPtr); } // 没有 Handler 要处理时,或者可以理解为消息队列中没有消息时,消息队列就会阻塞 if (pendingIdleHandlerCount <= 0) { // No idle handlers to run. Loop and wait some more. mBlocked = true; continue; }

上面的代码意思是说,在 MessageQueue 中没有 Message 时,它就会阻塞,此时我们插入 Message,就会唤醒 Message。

Handler对我们开发者的启发

亮点一

Handler核心源码分析

 /** * 在此处处理系统消息。 */ public void dispatchMessage(@NonNull Message msg) { // 如果 Message 自己有callback,就调用 Message.callback.run() if (msg.callback != null) { handleCallback(msg); } else { // 如果我们创建 Handler 时给 Callback 赋值了,就走这里 if (mCallback != null) { // 当 Callback 处理完消息之后,我们可以根据返回值确定,要不要走最后面的 handleMessage(msg); // 这个就与我们的View事件分发机制有些相似点了,根据返回值决定事件是否继续往下分发 if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } } private static void handleCallback(Message message) { message.callback.run(); } /** * 可以在实例化 Handler 时使用回调接口,以避免必须实现自己的 Handler 子类。 */ public interface Callback { /** * @param msg 一个{@link android.os.Message Message} 对象实例 * @return 如果不需要进一步处理,则为 true */ boolean handleMessage(@NonNull Message msg); }

上面代码就体现面向对象的封装思想,Message 封装了自己的 Callback ,如果 Message 是设置了它自己的 Callback,就回调自己的 callback.run();同样 Handler 封装了自己的 Callback ,如果 Handler 是设置了它自己的 Callback, 就回调自己的 handleMessage(),并且我们可以根据返回值决定要不要执行 Handler 子类重写的 handleMessage()。这样使得程序很灵活,有点像责任链模式

亮点二

大家有没有想过,Handler 调用了 dispatchMessage 之后,就把消息出来完了,那消息是怎么回收的?

其实在 Looper.loop() 中,最终会调用 Message.recycleUnchecked() 进行所谓的消息回收(其实消息并未被回收),我们来看源码:

 public static final Object sPoolSync = new Object(); /** * 回收可能正在使用的消息。 处理排队的消息时,由 MessageQueue 和 Looper 在内部使用。 */ @UnsupportedAppUsage void recycleUnchecked() { // 将消息标记为正在使用,同时保留在回收对象池中。清除所有其他详细信息。 flags = FLAG_IN_USE; what = 0; arg1 = 0; arg2 = 0; obj = null; replyTo = null; sendingUid = UID_NONE; workSourceUid = UID_NONE; when = 0; target = null; callback = null; data = null; // 加锁,避免多个线程回收消息时,消息池的消息混乱 synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { // 消息池的队头 next = sPool; // 把当前处理完的消息赋值给队头 sPool = this; // 消息池的消息量加一 sPoolSize++; } } }

然后再看看Message.obtain():

 /** * 从消息池中返回一个 Message 实例。 避免我们在很多情况下创建新的消息对象。 */ public static Message obtain() { synchronized (sPoolSync) { // 消息池队头不为空 if (sPool != null) { // 队头赋值给要返回的消息对象 Message m = sPool; // 队头指向 m 的下一个节点 sPool = m.next; // m 的下一个节点置空 m.next = null; m.flags = 0; // clear in-use flag sPoolSize--; return m; } } return new Message(); }

我们发现上述代码跟我们登机之前做安检的场景很像,Message 就好比是放行李的盒子,Message的成员what、arg1、arg2、objtct就相当于是行李,当我们过完安检之后,盒子不会被丢弃,而是放在安检门口以备下一个过安检的人使用,这好比我们的消息池,其实这里所用的就是享元模式

那么这样做有什么好处,从内存优化的角度思考,通过 Message.obtain() 获取消息大大减小了 new Message() 的调用,也就减少了连续内存空间被过度破坏,即不至于过度碎片化,也就是内存中连续空间更多了,那OOM出现的概率就小了。可能有同学说GC是干什么吃的,那大家有没有想过,既然 GC会帮我们回收垃圾,释放内存,为什么还会出现OOM。其实 GC即便用了标记整理算法,使得内存空间连续,但是GC线程工作的时候,会STW(stop the world),即其他所有线程都得挂起。而且我们不断地new 对象,又不断地触发 GC,会产生内存抖动,从而导致卡顿,所以从性能优化的角度来讲,我们尽量避免不必要的内存开销。

Looper什么时候退出

我们如果在子线程中创建 Looper 经常会有内存泄漏的问题,因为大部分同学都没有释放 Looper。那怎么办释放呢?通过调用 Looper.quitSafely() 或者 Looper.quit()

 /** * 安全退出Looper。 * 处理完消息队列中所有剩余的消息后,立即终止 {@link #loop} 方法。 但是,在 loop 终止之前,将不会传递未来到期的待处理的延 * 迟消息。在要求 Looper 退出后,任何向队列发布消息的尝试都将失败。 例如,{@link Handler#sendMessage(Message)} 方法 * 将返回 false。 */ public void quitSafely() { mQueue.quit(true);// 安全退出传的参数是 true,而Looper.quit()传的参数是 false }

最终会调用到MessageQueue.quit():

 /** * 退出Looper * * @param safe 是否需要安全退出 */ void quit(boolean safe) { // 如果不允许退出,会抛异常,ActivityThread中的Looper就不允许退出。 if (!mQuitAllowed) { throw new IllegalStateException("Main thread not allowed to quit."); } synchronized (this) { // 如果正在退出,则return if (mQuitting) { return; } mQuitting = true; if (safe) { // 安全退出 removeAllFutureMessagesLocked(); } else { // 不安全退出 removeAllMessagesLocked(); } // We can assume mPtr != 0 because mQuitting was previously false. nativeWake(mPtr); } }

安全退出的实现:

private void removeAllFutureMessagesLocked() { // 获取当前时间 final long now = SystemClock.uptimeMillis(); // 消息队列的队头赋值给 p Message p = mMessages; // 如果队头存在 if (p != null) { // 如果队头延迟时间大于当前时间,移除所有消息 if (p.when > now) { removeAllMessagesLocked(); } else {// 继续判断,取队列中所有大于当前时间的消息 Message n; for (;;) { n = p.next; if (n == null) { return; } if (n.when > now) { break; } p = n; } p.next = null; // 将所有所有大于当前时间的消息回收,延迟时间小于当前时间的消息即使消息队列退出了,仍然会继续被取出执行 do { p = n; n = p.next; p.recycleUnchecked(); } while (n != null); } } }

不安全退出的实现:

 /** * 移除消息队列所有消息,包括延迟时间小于当前时间的消息 */ private void removeAllMessagesLocked() { Message p = mMessages; // 轮循队列中所有 Message对象,一一缓存到消息池中 while (p != null) { Message n = p.next; // 缓存到消息池中 p.recycleUnchecked(); p = n; } // 队头置空 mMessages = null; }

Handler常见面试题

1.一个线程有几个 Handler?
在一个线程中我们可以创建多个 Handler。

2.一个线程中有几个 Looper?是如何保证的?
一个线程中只有一个 Looper;详情见本博文 Handler初始化 章节

3.Handler内存泄漏原因? 为什么其他的内部类没有说过有这个问题?

首先我们要有一个概念,就是内部类会持有外部类对象。我们在Activity中往往通过 new Handler并重写其 handleMessage 方法,即用匿名内部类的方式创建 Handler对象,此时 Handler就会持有 Activity对象。而我们通过 Handler.sendMessage 发送消息时,最终在 Handler.enqueueMessage 方法中,会让Message的 target成员持有 Handler。如果我们有一个 Message设置了延迟时间 20分钟之后再去处理,而 Activity在20分钟之内退出的话,根据可达性法则,在 Handler.enqueueMessage 方法中,Message可以看作是 GC Root ,Message持有Handler,Handler又持有Activity,就导致了内存泄漏。
4.如果想要在子线程中new Handler 要做些什么准备?为什么主线程不用做这些准备?

子线程中创建 Handler要先调用 Looper.prepare(),再调用 Looper.loop();

主线程其实就是 ActivityThread,Activity就处于 ActivityThread上运行,在 ActivityThread的 main方法中,通过 Looper.prepareMainLooper()获取到Looper对象,也调用了 Looper.loop()取消息。
5.子线程中维护的Looper,消息队列无消息的时候的处理方案是什么?有什么用?

6.既然可以存在多个 Handler 往 MessageQueue 中添加数据(发消息时各个 Handler 可能处于不同线程),那它内部是如何确保线程安全的?取消息呢?
很简单,加 synchronized 锁

7.我们使用 Message 时应该如何创建它?
Message.obtain();因为它采用享元模式,重复利用回收的消息,大大减少new Message() 的概率,从内存优化的角度看,减少不必要的内存开销,可以有效避免内存过度碎片化,从而降低出现OOM的概率。

8.Looper死循环为什么不会导致应用卡死?

某些题目答案过段时间再公布, 大家开动聪明的小脑袋先思考,欢迎评论区与我交流,文章有不准确之处,还望指正 ヾ( ̄ー ̄)X(^▽^)ゞ

  • 海报
海报图正在生成中...
赞(0) 打赏
声明:
1、本博客不从事任何主机及服务器租赁业务,不参与任何交易,也绝非中介。博客内容仅记录博主个人感兴趣的服务器测评结果及一些服务器相关的优惠活动,信息均摘自网络或来自服务商主动提供;所以对本博客提及的内容不作直接、间接、法定、约定的保证,博客内容也不具备任何参考价值及引导作用,访问者需自行甄别。
2、访问本博客请务必遵守有关互联网的相关法律、规定与规则;不能利用本博客所提及的内容从事任何违法、违规操作;否则造成的一切后果由访问者自行承担。
3、未成年人及不能独立承担法律责任的个人及群体请勿访问本博客。
4、一旦您访问本博客,即表示您已经知晓并接受了以上声明通告。
文章名称:《Handler核心源码分析》
文章链接:https://www.456zj.com/23928.html
本站资源仅供个人学习交流,请于下载后24小时内删除,不允许用于商业用途,否则法律问题自行承担。

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址