IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> handler全解 -> 正文阅读

[移动开发]handler全解

一,handler简介及其使用场景

(1)handler简介

Handler主要用于异步消息的处理,通过消息队列,进行线程间的通讯。 这种机制通常用来处理相对耗时比较长的操作。

(2)handler使用场景

1,子线程通过handler更新ui
2,线程间的通讯,子线程间也可以通过handler来实现通讯
3,安卓系统内部使用。Android系统中遍布着Handler。事件监测(卡顿,生命周期切换,anr监测…)

二,handler工作原理分析

(1)handler工作流程

在这里插入图片描述

大致流程就是handler.post或者handler.send发送消息,消息进入消息队列,然后通过looper轮询取出消息,然后进行分发处理。值得注意的是,普通消息和异步消息结构体中,都有target属性,来告诉处理的线程,是哪个handler来处理消息。因为一个线程可能有多个handler,looper取出后,就是根据target来分发的。

(2)Message

(1)message结构
主要注意:
what:区别handler发送的消息类型
arg1,aeg2 :两个整形数据,如果数据量量少(比如消息仅仅作为信号),可直接使用
obj:发送的单个对象
data:bundle类型,用于数据较多的场景
replyTo,sendingUid:用于跨进程通讯
(2)message复用的实现
1-我们创建message,有三种方法
直接new 一个message
调用Handler类的 obtainMessage()方法

/*
obtainMessage()方法是还是调用了Message类的方法
只是指定了 target指定了是自己(this),还有初始化了 what ,以及对象 obj
下面我们分析Message类的 obtain()方法
/*
    public static Message obtain(Handler h, int what, Object obj) {
        Message m = obtain();
        m.target = h;
        m.what = what;
        m.obj = obj;

        return m;
    }
    

调用Message类的 obtain()方法。

    /**
     * Return a new Message instance from the global pool. Allows us to
     * avoid allocating new objects in many cases.
     */
    public static Message obtain() {
        synchronized (sPoolSync) {
            if (sPool != null) {
                Message m = sPool;
                sPool = m.next;
                m.next = null;
                m.flags = 0; // clear in-use flag
                sPoolSize--;
                return m;
            }
        }
        return new Message();
    }

注意:
1-复用的Message取自一个单链表(消息使用完成后,会插入单链表)
2-有一个同步锁 sPoolSync ,防止多线程造成混乱
3-如果链表为空,则直接new一个 message

2-消息插入复用链表的时机和方式
两个时机:
1-消息被取出并消费后
loop()->从MessageQueue里取出消息msg-> msg.recycleUnchecked()
2-消息入队列时,handler所处线程已经移除messagequeue
MessageQueue类 enqueueMessage方法-> msg.recycle();
代码片段

          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;
            }

recycle()方法
主要有个判断消息是否正在被使用的操作 通过 message的flags属性是否为1(正在被使用)判断。如果没有在使用,则执行 recycleUnchecked()方法
正常消息入队流程:获取消息 msg->enqueque ->没有执行 recycle方法->markInUse()->…
非正常:获取消息 msg->enqueque ->handler所在线程 消息队列退出,执行 recycle方法,消息被回收,没有入队
recycleUnchecked 方法 主要就是清空消息,把消息插入队头。
因为是链表,所以也用message的next属性

详细内容:

https://blog.csdn.net/yangsenhao211423/article/details/106852828

(3)Messagequeue

数据结构:链表,方便插入(因为有插队消息,延时消息的存在,必须方便插入)

1-普通消息

普通消息,插队消息,延时消息的区别:delayMillis

//这里普通消息,delayMillis为0,但是还是有SystemClock.uptimeMillis(),
安卓系统开机到现在的时间
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }

最终调用

//插队消息,uptimeMillis直接为0,所以一定插在队首
    private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }

enqueueMessage()方法流程:
判断是否target是否为空
判断该消息是否已经被使用
加锁
判断handler所在线程消息队列是否退出
为消息增加正在被使用表示
判断消息是否插在队列头部
根据when的大小来排序插入

注意:这里面代码里有几个线程唤醒的时机
1-如果插入队头,如果线程原来是阻塞的,那么唤醒
2-如果队头是屏障消息,而且线程阻塞,将要插入的msg是异步消息,且前面没有异步消息(最早被执行的异步消息),那么唤醒。具体阻塞/唤醒内容在最后讨论

2-同步屏障与异步消息

什么是同步屏障
target=null的消息,在取消息时,如果消息是屏障消息,则取出屏障后的异步消息执行。
同步屏障创建

//MessageQueue类
//屏障消息 target为空,直接在MessageQueue类里加入链表,不走enquue方法
 public int postSyncBarrier() {
        return postSyncBarrier(SystemClock.uptimeMillis());
    }

next()方法里,通过target判断是不是屏障消息,是则找到屏障后的第一个异步消息

               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());
                }

注意,同步屏障使用后,记得移除,否则屏障一直存在,导致屏障后普通消息不能执行【loop()方法里,死循环执行 Message msg = queue.next()。而queue.next()方法,每次都是从队头开始取消息】
异步消息和普通消息的区别
除了标志位:isAsynchronous 不同之外,没有什么区别。
异步消息发送有两种方式
1-直接创建异步handler。这个handler一直发异步消息
2-创建消息时,标志区改为true

同步屏障典型的使用场景
Android应用框架中为了更快的响应UI刷新事件在ViewRootImpl.scheduleTraversals中使用了同步屏障

void scheduleTraversals() {
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //设置同步障碍,确保mTraversalRunnable优先被执行
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //内部通过Handler发送了一个异步消息
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        if (!mUnbufferedInputDispatch) {
            scheduleConsumeBatchedInput();
        }
       //mTraversalRunnable调用了performTraversals执行measure、layout、draw
        notifyRendererOfFramePending();
        pokeDrawLockIfNeeded();
    }
}

https://blog.csdn.net/asdgbc/article/details/79148180

3-阻塞与唤醒(pipe/epoll机制)

因为Looper里loop()方法是死循环取消息,在没有消息需要取的时候,这个的循环会造成资源浪费,所以在不需要时,则·阻塞线程。避免资源浪费

阻塞时机:nativePollOnce(ptr, nextPollTimeoutMillis)方法
队列为空(nextPollTimeoutMillis=-1 一直阻塞), 消息执行时机未到
唤醒时机:
消息入队时,若插入的消息在链表最前端
有同步屏障时插入的是最早被执行的异步消息
移除同步屏障时,若消息列表为空或者同步屏障后面不是异步消息时
线程移除messagequeue时,执行Message的quit方法。

epoll IO多路复用模型实现机制
(1)什么是阻塞与唤醒
答:操作系统为了支持多任务,实现了进程调度的功能,会把进程分为“运行”和“等待”等几种状态。运行状态是进程获得cpu使用权,正在执行代码的状态;等待状态是阻塞状态。操作系统会根据进程调度算法执行各个运行状态的进程,由于速度很快,看上去就像是同时执行多个任务。

处于运行状态的进程会被工作队列所引用。
等待队列是个非常重要的结构,它指向所有需要等待该事件的进程
,也就是说,每个需要阻塞和唤醒的事件都一个等待队列。阻塞和唤醒过程就是进程在这两个队列之间调度

Select机制流程:操作系统把需阻塞和唤醒的进程分别加入事件们的等待队列中,当某个事件触发,唤醒进程,遍历事件列表,找到触发事件,获取数据。
select的缺点:
??其一,每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。

其二,进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。
epoll机制在selet的基础上增加了内核维护的就绪列表rdlist(就绪列表),将需要监听的事件加入监视列表。当某个事件触发,该事件就会被加入就绪列表,不要去遍历无关的整个事件列表。
监视列表:红黑树结构,方便的添加和移除,还要便于搜索,以避免重复添加
就绪列表:双向链表结构,便于插入。

借鉴于

https://blog.csdn.net/sunxianghuang/article/details/105028062

(4)Looper

Looper类:
用于为线程运行消息循环的类。默认线程没有与它们相关联的消息喜欢;所以要在运行循环的线程中调用prepare(),然后调用loop()让它循环处理消息,直到循环停止。
与消息循环的交互是通过Handler类

注意:如果开启一个子线程需要接收handler消息,就必须执行两个方法。
主线程里不需要我们写,是因为安卓源码已经帮我们写好。自己开启的子线程需要写
Looper.prepare():准备本线程的Looper(包括准备消息队列之类),并保存在ThreadLocal(后面会讨论)里

//主线程不允许退出,quitAllowed为false。而子线程为true
    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));
    }
    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

Looper.loop():循环从messageQueue里取并分发消息。这里就不贴代码了。

1-ThreadLocal机制

什么是threadLocal机制
答:ThreadLocal 用作保存每个线程独享的对象,为每个线程都创建一个副本,这样每个线程都可以修改自己所拥有的副本, 而不会影响其他线程的副本,确保了线程安全。这里的副本是相对于线程间上的副本,和可见性中拷贝进程的副本不是一个意义
ThreadLocal 用作每个线程内需要独立保存信息,以便供其他方法更方便地获取该信息的场景。

threadLocal通过get()方法获取本线程的ThreadLocalMap(以threadLocal为key,储存线程对象)
一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。
注意的点:
(1)entry对象

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

key为threadlocal的弱应用,value为强引用。
为什么key为弱应用?
若key为强引用,那么threadLocal对象由于被自己内部ThreadLocalMap 所引用,所以对象在堆空间里回收不了。

value为强引用导致theadlocal内存泄露
当value为强引用,那么如果该ThreadLocal所在的线程没有被及时回收(例如在线程池里常驻),而导致value一直不为null,甚至存在theadLocal为null,但value不为null(null,对象)的情况(虽然threadlLocal本身也有清理空key的方法,但也有力所不及的情况),导致内存泄露。

如何避免?
使用完ThreadLocal变量后,要手动调用remove()方法来清理ThreadLocalMap(一般在finally代码块中)。finally应该被重视利用,用来做一些善后工作,写高质量代码。

(2)threadLocalMap
数据结构:数组
元素 :Entry<key,value>
添加元素过程:可结合源码看
从头开始遍历数组
若key存在,则替换value
若key为null,则替换Entry的key和value
否则在数组末尾new一个Entry放入数组,再去清理key为null的entry。
清理之后,判断是否需要扩容。即先添加,再清理,再判断扩容。(这个过程后面再写笔记分析)

具体意义:除了Looper对象的储存,同一个runable在两个线程里跑,使用threadLocal,可以保证变量或者对象独立。

https://www.cnblogs.com/cy0628/p/15086201.html

(5)IdleHandler介绍与使用场景

IdleHandler会在MessageQueue中没有Message要处理或者要处理的Message都是延时任务的时候得到执行(代码里,是在阻塞之前执行idlehandle的,因为阻塞在下一次循环里执行),这种特性很重要,因为当MessageQueue中没有Message要处理或者要处理的Message都是延时任务的时候,就表明当前线程为空闲状态。

//在nextPollTimeoutMillis被赋值之后(说明即将阻塞,因为不阻塞得话,已经return了)
  if (pendingIdleHandlerCount < 0
                        && (mMessages == null || now < mMessages.when)) {
                    pendingIdleHandlerCount = mIdleHandlers.size();
                }
                if (pendingIdleHandlerCount <= 0) {
               //没有idlehandler的话,直接continue,进入下一个循环阻塞,不会走到
               //     nextPollTimeoutMillis = 0;这句
                    // No idle handlers to run.  Loop and wait some more.
                    mBlocked = true;
                    continue;
                }

                if (mPendingIdleHandlers == null) {
                    mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];
                }
                mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);
            }

            // Run the idle handlers.
            // We only ever reach this code block during the first iteration.
            for (int i = 0; i < pendingIdleHandlerCount; i++) {
                final IdleHandler idler = mPendingIdleHandlers[i];
                mPendingIdleHandlers[i] = null; // release the reference to the handler

                boolean keep = false;
                try {
                    keep = idler.queueIdle();
                } catch (Throwable t) {
                    Log.wtf(TAG, "IdleHandler threw exception", t);
                }

                if (!keep) {
                    synchronized (this) {
                        mIdleHandlers.remove(idler);
                    }
                }
            }

            // Reset the idle handler count to 0 so we do nt run them again.
            //mIderHandlers执行条件是pendingIdleHandlerCount=-1
            //所以idlehandler只执行一遍,防止进入死循环。
            pendingIdleHandlerCount = 0;

            // While calling an idle handler, a new message could have been delivered
            // so go back and look again for a pending message without waiting.
            //执行完idlehandler之后,赋值0,防止线程阻塞,导致第一个消息无法执行(存在idlehandler执行完,第一个消息时间已经过去的情况,导致线程无法被唤醒)
            nextPollTimeoutMillis = 0;

使用场景:页面第一帧回执完成检测

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    Looper.getMainLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            Log.e("DBL", "queueIdle");
            // UI第一帧绘制完成(可以理解为页面可见)
            return false;  // 返回false表示MessageQueue在执行完这段代码后将该IdleHandler删除,反之不删除,下一次继续执行
        }
    });
}

@Override
protected void onResume() {
    super.onResume();
    Log.e("DBL", "onResume");
}

注意:这样的闲时机制(jetpack),处理时机不确定。使用时需要谨慎,因为可能存在idlehander任务一直无法执行的情况

参考大佬博客

https://www.jianshu.com/p/eb7d15ac052a

一,handler常见问题分析

(1)内存泄露问题

  private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case 1:
                    mTextView.setText(msg.obj + "");
                    break;
                default:
                    break;
            }
        }
    };

之前基础太差,不知道为什么这里Handler被声明为内部类了。其实这里相当于继承Handler,然后重写了handlerMessage方法,是一个内部类。然后被实例化了。我的理解是这样的。

因为内部类(非静态)和匿名内部类会持有外部引用,所以如果消息队列里有消息没有处理完,外部Activity就不能被回收。

解决办法:静态内部类+弱引用

https://www.jianshu.com/p/804e774d9f76

(2)非ui线程操作view

我们知道非ui线程操作view可以使用hander和view.post
但是,在resume之前,子线程是可以直接操作view的

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
-----------------?// checkThread()干的活就是检测当前线程是否为主线程
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

(3)View.post与handler.post的区别

(1)handler.post()
在工作线程中执行耗时任务,当任务完成时,会返回UI线程,一般是更新UI。这时有两种方法可以达到目的。

一种是handler.sendMessage。发一个消息,再根据消息,执行相关任务代码。

另一种是handler.post?。r是要执行的任务代码。意思就是说r的代码实际是在UI线程执行的。可以写更新UI的代码,其实就是一个runable。

(2)view.post
view.post(),也是执行一个runable,例如用来获取view宽高。
源码


    /**
     * <p>Causes the Runnable to be added to the message queue.
     * The runnable will be run on the user interface thread.</p>
     *
     * @param action The Runnable that will be executed.
     *
     * @return Returns true if the Runnable was successfully placed in to the
     *         message queue.  Returns false on failure, usually because the
     *         looper processing the message queue is exiting.
     *
     * @see #postDelayed
     * @see #removeCallbacks
     */
    public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            return attachInfo.mHandler.post(action);
        }

        // Postpone the runnable until we know on which thread it needs to run.
        // Assume that the runnable will be successfully placed after attach.
        getRunQueue().post(action);
        return true;
    }

从代码可以看出,分为两种情况,attachInfo不为空(view已经attach到window),则通话主线程的handler来执行runable。否则就缓存起来(mRunQueue),等到view被attach(view的dispatchAttachedToWindow()方法被执行)被主线程的handler执行。

1-attachInfo赋值
dispatchAttachedToWindow(AttachInfo info, int visibility)方法,view被添加。在我们熟悉的ViewRootImpl类里performTraversals() 方法被调用
dispatchAttachedToWindow(AttachInfo info, int visibility)方法,view被卸载

https://www.cnblogs.com/dasusu/p/8047172.html

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-01-25 10:42:32  更:2022-01-25 10:42:38 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 12:09:40-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码