安卓handler通信机制相关的几个问题

1. 为什么在activity中直接new一个handler的匿名内部类会有可能发生内存泄漏

scala 复制代码
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Handler(){
            @Override
            public void handleMessage(@NonNull Message msg) {
                System.out.println("zz");
            }
        };
        
    }
}

首先明确一点,在一个方法内部new一个匿名内部类的时候,这个匿名内部类会持有外部类的引用。 因此这个handler会持有activity这个对象引用。handler是被message所有的,可以看如下源码

ini 复制代码
public static Message obtain(Handler h, int what, int arg1, int arg2) {
    Message m = obtain();
    m.target = h;
    m.what = what;
    m.arg1 = arg1;
    m.arg2 = arg2;

    return m;
}

在这个源码中可以看出,这个message在获得的时候应该是和要处理的handler绑定的。也就是说message中已经知道了谁要去处理这个message。或者这样说更具体一点,在发送端发送这个message的时候呢,要进入共享的消息队列,这些都后面再详细说。

less 复制代码
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

因此可以看出这个message会和发送的这个handler进行绑定,相当于这一个handler要在主线程中进行处理,同时在子线程中作为一个成员变量进行发送!总之可以明确这个handler是和message绑定的。 同时可以看出,message是和messageQueue进行绑定的,message是messageQueue的一个属性。接下来看这个messageQueue,在looper这个类内部有一个messageQueue这个属性

ini 复制代码
private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

在代码中可以看出,这个mQueue在loope的构造方法中被创建。

csharp 复制代码
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

在后面的代码可以看出,这个looper被作为一个对象放到了sThreadLocal属性内部。同时看出这个sThreadLocal是一个静态变量。因此找到了GCRoot.因此,当这个message长期得不到处理的时候,就会发生内存泄漏。

2. 一个looper存在多个handler,如何保证线程安全的。

这个问题可以看一下handler整体的处理流程,在handler进行sendMessage操作的时候,会将这个messsage放入到messagequeue中

ini 复制代码
boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }

    synchronized (this) {
        if (msg.isInUse()) {
            throw new IllegalStateException(msg + " This message is already in use.");
        }

        if (mQuitting) {
            IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
            Log.w(TAG, e.getMessage(), e);
            msg.recycle();
            return false;
        }

        msg.markInUse();
        msg.when = when;
        Message p = mMessages;
        boolean needWake;
        if (p == null || when == 0 || when < p.when) {
            // New head, wake up the event queue if blocked.
            msg.next = p;
            mMessages = msg;
            needWake = mBlocked;

以上仅仅是复制了部分源码,可以看出在代码中引入了synchroized锁,目的是保证在message进入这个共享队列的时候的线程安全性。 在取出消息的时候呢?

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 (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }

        nativePollOnce(ptr, nextPollTimeoutMillis);

        synchronized (this) {
            // Try to retrieve the next message.  Return if found.
            final long now = SystemClock.uptimeMillis();
            Message prevMsg = null;
            Message msg = mMessages;

可以看上述代码,同时在取出的操作中也加入了synchroized锁。因此,这也是线程安全的。目的是不希望,插入队列和取出消息这两个操作产生冲突。

3.handler整个消息体制的一个运行过程

首先看最初始的源码,ActivityThread.java中的main方法,

ini 复制代码
Looper.prepareMainLooper();

准备好looper,并创建好消息队列

ini 复制代码
private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

之后

ini 复制代码
Looper.loop();

在这个方法内部进入一个for(;;)循环开始不断尝试进行取消息。因此,可以在主线程new一个handler,重写这个handlemessage方法。同时把这个handler放入到子线程中sendmessage进行跨线程通信。 同时应该明确的是看如下代码

less 复制代码
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) {
    mLooper = looper;
    mQueue = looper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}

可以看到同一个looper共享同一个messageQueue.而looper是和当前线程进行绑定的。看如下代码

ini 复制代码
private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

因此只要将一个handler仍到���同的线程里,就可以实现多线程之间的通信。

3.为什么new HandlerThread()默认是和主线程进行通信。看源码!

ini 复制代码
public void run() {
    mTid = Process.myTid();
    Looper.prepare();
    synchronized (this) {
        mLooper = Looper.myLooper();
        notifyAll();
    }
    Process.setThreadPriority(mPriority);
    onLooperPrepared();
    Looper.loop();
    mTid = -1;
}
csharp 复制代码
public static void prepare() {
    prepare(true);
}
csharp 复制代码
private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}
ini 复制代码
private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

关键之处就是在于looper会个当前的thread通过sThreadLocal进行绑定。

4.Message如何获取,handler是如何利用handler的。 在next()获取到消息后,

ini 复制代码
msg.recycleUnchecked();

会执行以上方法。在这个方法内部只要是将message的所有属性都清空。

ini 复制代码
void recycleUnchecked() {
    // Mark the message as in use while it remains in the recycled object pool.
    // Clear out all other details.
    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加入到缓存队列sPool中。 当取消息的时候呢?

ini 复制代码
public static Message obtain(Message orig) {
    Message m = obtain();
    m.what = orig.what;
    m.arg1 = orig.arg1;
    m.arg2 = orig.arg2;
    m.obj = orig.obj;
    m.replyTo = orig.replyTo;
    m.sendingUid = orig.sendingUid;
    m.workSourceUid = orig.workSourceUid;
    if (orig.data != null) {
        m.data = new Bundle(orig.data);
    }
    m.target = orig.target;
    m.callback = orig.callback;

    return m;
}

可以看出,并没有真的new一个message出来,而是将orig这个message 的信息赋给缓存队列中的一个空内容的mssage里面。这利用到了享元模式的概念。不频繁的创建空间,避免OOM的产生。

相关推荐
王哈哈^_^42 分钟前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie1 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic2 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿2 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具2 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端
qq_390161773 小时前
防抖函数--应用场景及示例
前端·javascript
John.liu_Test4 小时前
js下载excel示例demo
前端·javascript·excel
Yaml44 小时前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
PleaSure乐事4 小时前
【React.js】AntDesignPro左侧菜单栏栏目名称不显示的解决方案
前端·javascript·react.js·前端框架·webstorm·antdesignpro
哟哟耶耶4 小时前
js-将JavaScript对象或值转换为JSON字符串 JSON.stringify(this.SelectDataListCourse)
前端·javascript·json