Android的消息机制

Android的消息机制-从入门到精通

  • 前言
  • Android消息机制概述
  • Android 的消息机制分析
    • ThreadLocal 的工作原理
    • 消息队列的工作原理
    • Looper的工作原理
    • Handler的工作原理
  • 主线程的消息循环

前言

作为开发者,提及Android的消息机制,必然绕不开Handler,Handler是Android消息机制的上层接口,很多人认为其主要作用就是更新UI,这点也没错,但这仅仅是Handler的一个特殊使用场景:有时候需要在子线程中进行耗时的I/O操作,当完成该操作后需要在UI上进行一些改变,由于Android开发规范的限制,我们并不能直接在子线程中对UI控件进行操作,这个时候便可以通过使用Handler更新UI

Android的消息机制主要是指Handler的运行机制,Handler的运行需要底层的MessageQueue(消息队列)和Looper(循环)的支撑。

  • MessageQueue,内部存储一组消息,以队列形式对外提供插入和删除的工作,内部存储结构是单链表的数据结构
  • Looper:消息循环,由MessageQueue负责消息的存储单元,Looper负责去处理消息,它会以无限循环的形式去查找是否有新消息,否则就一直等待(Looper中还有一个概念:ThreadLocal)
  • ThreadLocal:在每个线程中存储数据,可以在不同线程中互不干扰的存储并提供数据

Handler创建时会采用当前线程的Looper来构建消息循环系统,其内部使用ThreadLocal来获取到当前线程的Looper,如果需要使用Handler就必须为线程创建Looper

Android消息机制概述

Android的消息机制主要是指Handler的运行机制以及Handler所附带的MessageQueue和Looper的工作过程 (PS:之所以提供Handler,是为了解决在子线程中无法访问UI的矛盾),下面主要讲解下在Android中Handler的主要执行过程及功能:

//非主线程操作UI,提示报错
void checkThread() {if (mThread != Thread.currentThread() ) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its view")}
}

简单描述下Handler的工作原理:

  • Handler创建时会采用当前线程的Looper来构建内部消息的循环系统,若当前线程没有Looper会报如下错误
    在这里插入图片描述
  • Handler创建完毕后,内部的Looper及MessageQueue可以与Handler一起协同工作,通过Handler的post方法将一个Runnable投递到Handler内部的Looper中处理(也可通过Handler的send方法发送消息到Looper中处理)
  • send方法工作过程:调用send后,它会调用MessageQueue的enqueueMessage方法,将消息放进消息队列中,后Looper进行消息处理操作,最终调用到消息中的Runnable或者Handler的handleMessage方法。
    在这里插入图片描述

Android 的消息机制分析

ThreadLocal 的工作原理

ThreadLocal是线程内部的数据存储类,可以在指定线程中存储数据。(使用场景:当某些数据是以线程为作用域并且不同线程具有不同数据副本的时候,可以考虑使用ThreadLocal,如Handler的使用),下面将举例演示

//定义一个ThreadLocal对象
private ThreadLocal<Boolean> mBooleanTheadLocal = new ThreadLocal<Boolean>();//分别在主线程及子线程1、子线程2中设置和访问该值
mBooleanTheadLocal.set(true);
Log.d(TAG,"[ThreadLocal#main] mBooleanThreadLocal = " + mBooleanThreadLocal.get());
new Thread("Thread#1") {@overridepublic void run() {mBooleanThreadLocal.set(false);Log.d(TAG,"[ThreadLocal#Thread1] mBooleanThreadLocal = " + mBooleanThreadLocal.get());};
}.start();new Thread("Thread#2") {@overridepublic void run() {Log.d(TAG,"[ThreadLocal#Thread2] mBooleanThreadLocal = " + mBooleanThreadLocal.get());};
}.start();

该代码,在主线程中设置mBooleanTheadLocal为true,子线程1中为false,子线程2中不设置,后分别在三个线程中通过get方式获取mBooleanThreadLocal的值,日志如下:

D/TestActivity(8676): [Thread#main] mBooleanThreadLocal=true
D/TestActivity(8676): [Thread#1] mBooleanThreadLocal=false
D/TestActivity(8676): [Thread#2] mBooleanThreadLocal=null

下面分析ThreadLocal的内部实现,ThreadLocal是一个泛型类,重点介绍其get与set方法

//ThreadLocal set 方法
public void set(T value) {Thread currentThread = Thread.currentThread();Values values = values(currentThread);if (values == null) {values = initializaValues(currentThread);}values.put(this, values);
}

在Thread类的内部有一个成员变量专门存储线程的ThreadLocal的数据:ThreadLocal.Values localValues,在localValues内部有一个数组:private Object[] table,ThreadLocal的值就存在这个table数组中

void put (ThreadLocal<?> key, Object value) {cleanUp();//keep track of first tombstone. That's where we want to go back//and add an entry if necessaryint firstTomstone = -1;for(int index = key.hash & mask;; index = next(index)) {Object k = table[index];if(k == key.reference) {//Replace existing entrytable[index + 1] = value;return;}if(k == null) {if(firstTombstone == -1) {//Fill in null slottable[index] = key.reference;table[index + 1] = value;size++;return;}//Go back and replace first tombstonetable[firstTombstone] = key.reference;table[firstTombstone + 1] = value;tombstone--;size++;return; }//Remember first tombstoneif(firstTombstone == -1 && k == TOMBSTONE) {firstTombstone = index;}}
}

由此得出一个存储规则:ThreadLocal的值在table数组中的存储位置总是为ThreadLocal的reference字段所标识的对象的下一个位置(比如ThreadLocal的reference对象在table数组中的索引为index,那么ThreadLocal的值在table数组中的索引就是index + 1)

//ThreadLocal get 方法
public T get() {//Optimized for the first path.Thread currentThread = Thread.currentThread();Values values = values(currentThread);if (values != null) {Object[] table = values.table;int index = hash & values.table;if(this,reference == table[index]) {return (T) table[index + 1];}}else {values = initializeValues(currentThread);}return (T) values.getAfterMiss(this);
}

ThreadLocal的get方法:取出当前线程的local-value对象,如果这个对象为null那么就返回初始值,初始值由ThreadLocal的initialValue方法来描述,默认情况下为null
如果localValues对象不为null,那就读取它的table数组并找出ThreadLocal的reference对象在table数组中的位置,然后table数组中的下一个位置所存储的数据就是ThreadLocal的值

从ThreadLocal的set 和 get 方法中可以发现,它们所操作的对象都是当前线程的localValues对象的table数组,因此在不同线程中访问同一个ThreadLocal的set 和 get 方法,它们对ThreadLocal所做的读/写操作仅限于各自线程的内部,这也是为什么ThreadLocal可以在多线程中互不干扰地存储和修改数据的原因!

消息队列的工作原理

消息队列MessageQueue主要包括两个操作:插入和读取。读取操作会涉及到删除操作。插入和读取对应的方法分别是equeueMessage 和 next ,其中enqueueMessage的作用是往消息队列中插入一条消息,而next的作用是从消息队列中取出一条信息并将其从消息队列中删除,MessageQueue的内部实现是一个单链表,本身在插入和删除上具有优势

//enqueueMessage 方式实现 主要操作就是单链表的插入
boolean enqueueMessage (Message msg, long when) {...synchronized (this) {...msg.markInUse();msg.when = when;Message p = mMessage;boolean needWake;if (p == null || when == 0 || when < p.when) {//New head, wake up the event queue if blocked.msg.next = p;mMessage = msg;needWake = mBlocked;} else {// Inserted within the middle of the queue. Usually we don't have to wake// up the event queue unless there is a barrier at the head of the queue// and the message is the earliest asynchronous message in the queue.needWake = mBlock && p.target == null && msg.isAsynchronous();Message prev;for(;;) {prev = p;p = p.next;if (p == null || when <p.when) {break;}if (needWake && p.isAsynchronous()) {needWake = false;}}msg.next = p;// invariant: p == prev.nextprev.next = msg;}//We can assume mPtr  != 0 because mQuitting is falseif (needWake) {nativeWake(mPtr);}}return true;
}
// next 方法实现
Message next() {...int pendingIdleHandlerCount = -1;// -1 only during first iterationint nextPollTimeoutMills = 0;for(;;) {if (nextPollTimeoutMills != 0) {Binder.flushPendingCommands();}nativePollOnce(ptr, nextPollTimeoutMills);synchronized(this) {//Try to retrieve the next message. Return if found.final long now = SystemClock.uptimeMills();Message prevMsg = null;Message msg = mMessages;if(msg != null && msg.target == null) {//Stalled by a barrier.Find the next asynchronous message in the queuedo {prevMsg = msg;msg = msg.next;} while (msg != null && !msg.isAsynchronous());}if (msg != null) {if (now < msg.when) {//Next message is not ready. Set a timeout to wake up when it is ready.nextPollTimeoutMills = (int) Math.min(msg.when - now, Integer.MAX_VALUE);} else {//Got a message.mBlocked = false;if (prevMsg != null) {prevMsg,next = msg.next;} else {mMessage = msg.next;}msg.next = null;if (false) Log.v("MessageQueue","Returning message:" + msg);return msg;} else {//No more message.nextPollTimeoutMills = -1;}...}...}}
}

源码分析next 方法的实现,可以知道该方法是一个无限循环方法,当消息队列中没有消息时,next方法就会一直阻塞在这里,当有新消息来时,next方法会返回这条消息并将其从单链表中移除。

Looper的工作原理

Looper在Android消息机制中扮演着消息循环的角色,具体将它会不停从MessageQueue中查看是否有新消息,如果有新消息就立刻处理,否则就阻塞在那里。

//Looper 的构造方法:创建一个MessageQueue,并将当前线程的对象保存起来
private Looper (boolean quitAllowed) {mQueue = new MessageQueue(quitAllowed);mThread = Thread.currentThread();
}//为当前线程创建一个Looper
new Thread("Thread#2") {@overridepublic void run () {Looper.prepare();Handler handler = new Handler();Looper.loop();//开启消息循环};
}.start();
Looper.getMainLooper()// 获取到主线程的Looper
Looper.quit()//退出一个Looper
Looper.quitSafely//设定一个退出标记

以下分析Looper.loop方法的具体实现:

/*** Run the message queue in this thread. Be sure to call* {@link #quit()} to end the loop.*/
public static void loop() {final Looper me = myLooper();if (me == null) {throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");}final MessageQueue queue = me.mQueue;//Make sure the identity of this thread is that of the local process,//and keep track of what that identity token actually is.Binder.clearCallingIdentity();final long ident = Binder.clearCallingIdentity();for(;;) {Message msg = queue.next();//might blockif (msg == null) {//No message indicates that the message queue is qutting.return;}//This must be in a local variable, in case a UI event sets the loggerPrinter logging = me.mLogging;if(logging != null) {logging.println(">>>>>Dispatching to+ msg.target "+ " " + msg.callback + ": " + msg.what) }//Make sure that during the course of dispatching the//identity of the thread wasn't corrupted.final long newIdent = Binder.clearCallingIdentity();if (ident != newIdent) {Log.wtf(TAG, "Thread identity changed from 0x"+ Long.toHexString(ident) + "to 0x"+ Long.toHexString(newIdent) + "while dispatching to"+ msg.target.getClass().getName() +" "+ msg.callback + "what=" + msg.what);}msg.recycleUnchecked();}
}

分析:loop方法是一个死循环,唯一跳出循环的条件是MessageQueue的next方法返回的是null。当Looper的quit方法被调用时,MessageQueue的quit或者quitSafely会被调用来通知消息队列退出,它的next方法会返回null,即Looper必须退出。否则loop方法就会无限循环下去。
loop方法调用MessageQueue的next方法来获取新消息,next是一个阻塞操作,如果返回新消息,Looper就会去处理:
msg.target是发送消息的Handler对象,Handler发送消息最终又交给它的dispatchMessage方法来处理,Handler的dispatchMessage方法是创建Handler时所使用的Looper中执行的,这样就成功将代码逻辑切换到指定线程中去了

Handler的工作原理

Handler的工作主要是信息的发送和接收,消息的发送可以通过post的一系列方法以及send的一系列方法来实现,以下举例演示:

public final boolean sendMessage(Message msg) {return sendMessageDelayed(msg, 0);
}public final boolean sendMessageDelayed(Message msg, long delayMills) {if (delayMillis < 0) {delayMillis = 0;}return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}public boolean sendMessageAtTime(Message msg, long uptimeMillis) {MessageQueue queue = mQueue;if (queue == null) {RuntimeExcption e = new RuntimeException(this + "sendMessageAtTime() called with no mQueue");Log.w("Looper", e.getMessage(), e);return false;}return enqueueMessage(queue, msg, uptimeMillis);
}private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMills) {msg.target = this;if (mAsynchronous) {msg.setAsynchronous(true);}return queue.equeueMessage(msg, uptimeMillis);
}

不难发现,Handler向消息队列中插入一条信息,MessageQueue的next方法就会返回这条信息给Looper处理,最终消息由Looper交由Handler处理,即Handler的dispatchMessage方法被调用,此时Handler进入处理消息阶段

//dispatchMessage具体实现
public void dispatchMessage(Message msg) {if(msg.callback != null) {handleCallback(msg);} else {if(mCallback != null) {if(mCallback.handlerMessage(msg)) {return;}}handleCallback(msg);}
}

Handler处理消息过程如下:

  • 首先,检查Message的callback是否为null,不为null就通过handleCallback来处理消息。Message的callback是一个Runnable对象,实际上就是Handler的post方法所传递的Runnable参数。
//handleCallback 具体实现
private static void handleCallback(Message message) {message.callback.run();
}
  • 其次,检查mCallback是否为null,不为null就调用mCallback的handleMessage方法来处理消息。
//Callback接口
/*** Callback interface you can use when instantiating a Handler to avoid* having to implement your own subclass of Handler.** @param msg A {@link android.os.Message Message} object* @return True if no further handling is desired*/
public interface Callback {public boolean handleMessage(Message msg);
}
  • 最后,调用Handler的handleMessage方法来处理消息。Handler处理信息流程图如下:
    在这里插入图片描述
    PS:Handler还有一个特殊的构造方法:及通过一个特定的Looper来构造Handler
public Handler(Looper looper) {this(looper, null, false);
}//示例:若当前线程没有Looper,会抛异常
public Handler(Callback callback, boolean async) {...mLooper = Looper.myLooper();if (mLooper == null) {throw new RuntimeException("Can't create handler inside thread that has not called Looper.prepare()");}mQueue = mLooper.mQueue;mCallback = callback;mAsynchronous = async;
}

主线程的消息循环

在Android主线程中的ActivityThread的main方法中:系统会通过Looper.prepareMainLooper()来创建主线程的Looper以及MessageQueue,通过Looper.loop()开启消息循环:

public static void main(String[] args) {...Process.setArgV0(""<pre-initialized>");Loopr.prepareMainLooper();ActivityThread thread = new ActivityThread();thread.attach(false);if (sMainThreadHandler == null) {sMainThreadHandler = thread.getHandler();} AsyncTask.init();if (false) {Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread"));Looper.loop();throw new RuntimeException("Main thread loop unexpectedly exited");}
}

打开主线程的消息循环后,ActivityThread还需要一个Handler来和消息队列进行交互,ActivityThread.H来完成这个工作(内部定义一组消息类型,主要包括四大组件的启动和停止)

private class H extends Handler {public static final int LAUNCH_ACTIVITY = 100;public static final int PAUSE_ACTIVITY = 101;public static final int PAUSE_ACTIVITY_FINISHING = 102;public static final int STOP_ACTIVITY_SHOW = 103;public static final int STOP_ACTIVITY_HIDE = 104;public static final int SHOW_WINDOW = 105;public static final int HIDE_WINDOW = 106;public static final int RESUME_ACTIVITY = 107;public static final int SEND_RESULT = 108;public static final int DESTROY_ACTIVITY = 109;public static final int BIND_APPLICATION = 110;public static final int EXIT_APPLICATION = 111;public static final int NEW_INTENT = 112;public static final int RECEIVER = 113;public static final int CREATE_SERVICE = 114;public static final int SERVICE_ARGS = 115;public static final int STOP_SERVICE = 116;...
}

ActivityThread通过ApplicationThread和AMS进行进程间通信,AMS以进程间通信的方式完成ActivityThread的请求后会回调ApplicationThread中的Binder方法,然后ApplicationThread会向H发送消息,H收到消息后会将ApplicationThread中的逻辑切换到ActivityThread(即主线程)中去执行,这个过程就是主线程的消息循环模型。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/36105.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

es-将知识库中的数据转换为向量存储到es并进行相似性检索

目录 为什么要将数据转为向量存入es? 数据准备 创建索引库 向量存储 验证 为什么要将数据转为向量存入es? 我之前把数据作为文档存入 ES&#xff0c;主要用于全文检索&#xff08;BM25 算法&#xff09;&#xff0c;但是它不适合语义匹配&#xff0c;比如如果用户输入的…

【资料分享】全志科技T113-i全国产(1.2GHz双核A7 RISC-V)工业核心板规格书

核心板简介 创龙科技SOM-TLT113 是一款基于全志科技T113-i 双核ARM Cortex-A7 玄铁C906 RISC-V HiFi4 DSP 异构多核处理器设计的全国产工业核心板&#xff0c;ARM Cortex-A7 处理单元主频高达1.2GHz。核心板 CPU、ROM、RAM、电源、晶振等所有元器件均采用国产工业级方案&…

wepy微信小程序自定义底部弹出框功能,显示与隐藏效果(淡入淡出,滑入滑出)

视图html部分 <view class"salePz"><view class"btnSelPz" tap"pzModelClick">去选择</view><!-- modal --><view class"modal modal-bottom-dialog" hidden"{{hideFlag}}"><view class&q…

基于Springboot+Typst的PDF生成方案,适用于报告打印/标签打印/二维码打印等

基于SpringbootTypst的PDF生成方案&#xff0c;适用于报告打印/标签打印/二维码打印等。 仅提供后端实现 Typst2pdf-for-report/label/QR code github 环境 JDK11linux/windows/mac 应用场景 适用于定制化的报告模板/标签/条码/二维码等信息的pdf生成方案。通过浏览器的p…

leetcode每日一题:使字符串平衡的最小交换次数

引言 今天开始&#xff0c;打算做一个新的系列&#xff1a;leetcode每日一题的题解。预期每天用90分钟的时间&#xff0c;去写一篇当天的每日一题的题解&#xff0c;这个目标跟早起结合在一起&#xff0c;才有足够的时间完成。其实早在前几年&#xff0c;就开始断断续续做leetc…

Learn Redis 5 (Java)

分布式锁 在面对高并发业务时&#xff0c;单个项目解决不过来&#xff0c;此时一个项目部署到多个机器&#xff0c;这就是集群模式&#xff0c;不同的项目实例就会对应不同的端口和JVM。 1.模拟集群模式 Nginx实现负载均衡&#xff08;轮询&#xff09; 2.使用集群模…

lua学习(三)

错误处理 assert断言 作用&#xff1a;确保某些数据是符合预期的&#xff0c;避免影响最终结果。 格式&#xff1a;assert(条件语句&#xff0c;报错信息) 当条件语句为true时&#xff0c;assert语句不会有任何行为&#xff0c;但是当为false时&#xff0c;assert会将报错信息…

基于eNSP的IPV4和IPV6企业网络规划

基于eNSP的IPV4和IPV6企业网络规划 前言网络拓扑设计功能设计技术详解一、网络设备基础配置二、虚拟局域网&#xff08;VLAN&#xff09;与广播域划分三、冗余协议与链路故障检测四、IP地址自动分配与DHCP相关配置五、动态路由与安全认证六、广域网互联及VPN实现七、网络地址转…

优选算法合集————双指针(专题四)

1&#xff0c;一维前缀和模版 题目描述&#xff1a; 描述 给定一个长度为n的数组a1,a2,....ana1​,a2​,....an​. 接下来有q次查询, 每次查询有两个参数l, r. 对于每个询问, 请输出alal1....aral​al1​....ar​ 输入描述&#xff1a; 第一行包含两个整数n和q. 第二行…

Web3游戏行业报告

一&#xff0c;gamefi经济 什么是gamefi GameFi是一个缩写&#xff0c;它结合了游戏和去中心化金融(“DeFi”)这两个术语&#xff0c;关注的是游戏玩法如何在去中心化系统中实现货币化。对于游戏而言&#xff0c;只要开放了交易市场&#xff0c;允许玩家自由买卖&#xff0c;…

【程序人生】成功人生架构图(分层模型)

文章目录 ⭐前言⭐一、根基层——价值观与使命⭐二、支柱层——健康与能量⭐三、驱动层——学习与进化⭐四、网络层——关系系统⭐五、目标层——成就与财富⭐六、顶层——意义与传承⭐外层&#xff1a;调节环——平衡与抗风险⭐思维导图 标题详情作者JosieBook头衔CSDN博客专家…

拖拽实现+摇杆实现

拖拽实现 拖拽事件实现: 半透明渐变贴图在ios设备下&#xff0c;使用压缩会造成图片质量损失&#xff0c;所以可以将半透明渐变UI切片单独制作真彩色图集 拖拽事件组 IBeginDragHandler:检测到射线后&#xff0c;当拖拽动作开始时执行一次回调函数 IDragHandler:拖拽开始后&a…

vs2017版本与arcgis10.1的ArcObject SDK for .NET兼容配置终结解决方案

因电脑用的arcgis10.1,之前安装的vs2010正常能使用AO和AE&#xff0c;安装vs2017后无法使用了&#xff0c;在重新按照新版本arcgis engine或者arcObject费时费力&#xff0c;还需要重新查找资源。 用vs2017与arc10.1的集成主要两个问题&#xff0c;1&#xff1a;安装后vs中没有…

C语言和C++到底有什么关系?

C 读作“C 加加”&#xff0c;是“C Plus Plus”的简称。 顾名思义&#xff0c;C 就是在 C 语言的基础上增加了新特性&#xff0c;玩出了新花样&#xff0c;所以才说“Plus”&#xff0c;就像 Win11 和 Win10、iPhone 15 和 iPhone 15 Pro 的关系。 C 语言是 1972 年由美国贝…

企业微信群聊机器人开发

拿到机器人hook 机器人开发文档 https://developer.work.weixin.qq.com/document/path/91770

AT指令集-NBIOT

是什么&#xff1f; 窄带物联网&#xff08;Narrow Band Internet of Things, NB-IoT&#xff09;成为万物互联网络的一个重要分支支持低功耗设备在广域网的蜂窝数据连接&#xff0c;也被叫作低功耗广域网(LPWAN)NB-IoT支持待机时间长、对网络连接要求较高设备的高效连接NB-Io…

网络爬虫【爬虫库urllib】

我叫不三不四&#xff0c;很高兴见到大家&#xff0c;欢迎一起学习交流和进步 今天来讲一讲爬虫 urllib介绍 Urllib是Python自带的标准库&#xff0c;无须安装&#xff0c;直接引用即可。 Urllib是一个收集几个模块来使用URL的软件包&#xff0c;大致具备以下功能。 ● urlli…

vue中js简单创建一个事件中心/中间件/eventBus

vue中js简单创建一个事件中心/中间件/eventBus 目录结构如下&#xff1a; eventBus.js class eventBus {constructor() {this.events {};}// 监听事件on(event, callback) {if (!this.events[event]) {this.events[event] [];}this.events[event].push(callback);}// 发射…

弹球小游戏-简单开发版

一、需求 弹球小游戏是一个简单的互动游戏&#xff0c;玩家需要控制一个挡板在窗口底部左右移动&#xff0c;以接住从上方落下的球。游戏的主要需求包括&#xff1a; (1) 游戏界面 &#xff1a;创建一个指定尺寸的游戏窗口&#xff0c;显示球和挡板。 (2) 球的运动 &#xf…

Cursor与Blender-MCP生成3D模型

随着DeepSeek的热度&#xff0c;各行各业接入AI智能&#xff0c;当然作为一个深受3D爱好者喜爱的软件——Blender&#xff0c;也接入了AI智能&#xff0c;通过Blender-MCP&#xff0c;开启一场Blender的智能化模型创建的世界之旅。 目录 1.准备工作2.环境配置2.1 Mac安装2.2 W…