Handler

2020-08-09  本文已影响0人  小虫虫奇遇记

Q1:Handler.postDelayed(runnable, time),假设delay time == 3min,如果把手机时间调整为比当前时间快3min, runnable会立马执行吗?

答:不会。postDelayed最终会调用到sendMessageDelayed,注意此时用到的时间被处理成 SystemClock.uptimeMillis() + delayMillis,表示系统启动到当前的毫秒数+延时毫秒数。所以修改系统时间并不会改变这个数值,也就是说修改系统时间不影响handler的延时任务

// Handler.java
public final boolean sendMessageDelayed(Message msg, long delayMillis)
    {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

Q2:如果消息队列中的消息都剩下延时任务,最近的延时也要等到2s后,在此期间消息队列还会一直循环取消息吗?

答:不会 ,Looper.loop()消息循环取消息的过程,重点在于nextPollTimeoutMillis的赋值,如果有消息未到执行时间,nextPollTimeoutMillis 赋值为需要等待的时间,传递给nativePollOnce方法,告知native空闲等待。nativePollOnce该函数可以看做睡眠阻塞的入口,该函数是一个native函数

// MessageQueue.java
Message next() {
        // Return here if the message loop has already quit and been disposed.
        // This can happen if the application tries to restart a looper after quit
        // which is not supported.
        final long ptr = mPtr;
        if (ptr == 0) {
            return null;
        }

        int pendingIdleHandlerCount = -1; // -1 only during first iteration
        int nextPollTimeoutMillis = 0;
        for (;;) {
             //如果nextPollTimeoutMillis !=0 ,执行空闲等待阻塞循环;
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }
            //告知native底层,到达nextPollTimeoutMillis 的时候,再唤醒循环
            nativePollOnce(ptr, nextPollTimeoutMillis);
            //synchronized同步,所以没消息的情况下,message.next()可能会阻塞
            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                //优先处理异步消息;异步消息没有设置target且msg.setAsynchronous(true);
                if (msg != null && msg.target == null) {
                    // Stalled by a barrier.  Find the next asynchronous message in the queue.
                    do {
                        prevMsg = msg;
                        msg = msg.next;
                    } while (msg != null && !msg.isAsynchronous());
                }
                if (msg != null) {
                    if (now < msg.when) {
                        // 消息没到执行时间,则将nextPollTimeoutMillis设置为需要空闲等待阻塞时间。Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }

                // Process the quit message now that all pending messages have been handled.
                if (mQuitting) {
                    dispose();
                    return null;
                }
                ....
            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            nextPollTimeoutMillis = 0;
        }
    }

Q3.卡顿检测原理:

  /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
          ....  
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
            // This must be in a local variable, in case a UI event sets the logger
            final Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }
         ....
            final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0;
            final long dispatchEnd;
            try {
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            ....
          if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            msg.recycleUnchecked();
        }
    }

可以看到在msg.target.dispatchMessage(msg);执行前后,都调用了logging.print方法,这个logging 还可以自定义设置自己的logging,

Looper.getMainLooper().setMessageLogging(mBlockCanaryCore.monitor);

class MyLooperMonitor implements Printer {
    ....
    @Override
    public void println(String x) {
        if (!mPrintingStarted) {
            mStartTimestamp = System.currentTimeMillis();
            mStartThreadTimestamp = SystemClock.currentThreadTimeMillis();
            mPrintingStarted = true;
            startDump();      //收集卡顿堆栈信息 thread.getStackTrace
        } else {
            final long endTime = System.currentTimeMillis();
            mPrintingStarted = false;
            if (isBlock(endTime)) {    //判断消息执行时间是否超时,如果endTime -    mStartTimestamp超过3s,视为卡顿。
notifyBlockEvent(endTime);
            }
            stopDump();
        }
    }

本地检测卡顿工具:

adb shell dumpsys gfxinfo <PACKAGE_NAME>
可打印出帧率,FPS 等数据。

Q4:ThreadLocal会造成内存泄漏吗?

ThreadLocal:线程变量副本;每个线程保存自己的副本,互不干扰。

引用关系:Thread--- 内部变量:ThreadLocalMap map--->entry--->WeakReference<ThreadLocal> + value

image.png
public class Thread implements Runnable {
    // 每个Thread实例都持有一个ThreadLocalMap的属性
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

//ThreadLocal.java  
//ThreadLocalMap内部类 ,持有Entry, entry持有threadlocal的弱引用和对value的引用
  static class ThreadLocalMap {
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }


}

为什么使用弱引用?会造成内存泄漏吗?如何避免?多线程?

  1. 假设threadlocal使用强引用,对外部强引用 threadlocalInstance = null 后,如果线程还没结束,由于threadlocalmap的entry还持有threadlocal的强引用,导致threadlocalInstance依然不能被回收,这样就无法真正达到业务逻辑的目的,出现逻辑错误;

  2. 使用弱引用,如果threadLocal外部强引用被置为null(threadLocalInstance=null)的话,因为threadLocalMap.entry只持有threadlocal的弱引用,threadLocal实例就没有一条引用链路可达,很显然在GC (垃圾回收)的时候势必会被回收,因此entry就存在key为null的情况,无法通过一个Key为null去访问到该entry的value。同时,就存在了这样一条引用链:threadRef->currentThread->threadLocalMap->entry->valueRef->valueMemory,导致在垃圾回收的时候进行可达性分析的时候,value可达从而不会被回收掉,但是该value永远不能被访问到,这样就存在了内存泄漏**。当然,如果线程执行结束后,threadLocal,threadRef会断掉,因此threadLocal,threadLocalMap,entry都会被回收掉。可是,在实际使用中我们都是会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们关注.

   val sThreadLocal = ThreadLocal<String>()
        val pool = Executors.newSingleThreadExecutor()
        for (i in 0 until 10) {
            if (i == 0) {
                pool.execute(Runnable {
                   sThreadLocal.set("lijing")
//                 sThreadLocal.remove()
                })
            } else {
                pool.execute(Runnable {
                    Log.d("sThreadLocal", Thread.currentThread().name + sThreadLocal.get())
                })
            }

        }

假设Entry弱引用threadLocal,尽管会出现内存泄漏的问题,但是在threadLocal的生命周期里(set,getEntry,remove)里,都会针对key为null的脏entry进行处理。

从以上的分析可以看出,使用弱引用的话在threadLocal生命周期里会尽可能的保证不出现内存泄漏的问题,达到安全的状态。

ThreadLocal最佳实践

每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
参考

上一篇下一篇

猜你喜欢

热点阅读