Android Handler機制之Handler 男窟、MessageQueue 盆赤、Looper

很隨意.jpg

該文章屬于Android Handler系列文章,如果想了解更多蝎宇,請點擊
《Android Handler機制之總目錄》

前言

上篇文章弟劲,我們講了ThreadLocal,了解了線程本地變量的實質(zhì)姥芥,如果有小伙伴還是不熟悉ThreadLocal原理的兔乞,請參看上篇文章《Android Handler機制之ThreadLocal》。如果你已經(jīng)閱讀 了該文章凉唐,那現(xiàn)在我們就一起來了解Handler與MessageQueue與Looper三者之間的關(guān)系及其內(nèi)部原理庸追。

Handler、MessageQueue台囱、Looper三者之間的關(guān)系

在了解其三者關(guān)系之前淡溯,我先給大家一個全局的關(guān)系圖,接下來的文章會根據(jù)該關(guān)系圖簿训,進行相應(yīng)的補充與描述咱娶。


HandlerLooperMessage關(guān)系.png

從上圖中我們可以看出幾點

  • Handler的創(chuàng)建是與Looper創(chuàng)建的線程是相同的米间。
  • Looper中內(nèi)部維護了一個MessageQueue(也就是消息隊列)。且該隊列是通過鏈表的形式實現(xiàn)的膘侮。
  • Hanlder最終通過sendMessage方法將消息發(fā)送到Looper中對應(yīng)的MessageQueue中屈糊。
  • Looper通過消息循環(huán)獲取消息后,會調(diào)用對應(yīng)的消息中的target(target對應(yīng)的是發(fā)消息的Handler)的dispatchMessage()方法來處理消息琼了。

Looper原理

因為消息隊列(MessageQueue的創(chuàng)建是在Looper中內(nèi)部創(chuàng)建的逻锐,同時Handler消息的發(fā)送與處理都是圍繞著Looper來進行的,所以我們首先來講Looper雕薪。

Looper是如何與主線程關(guān)聯(lián)的

在平時開發(fā)中昧诱,我們使用Handler主要是為了在主線程中去更新UI,那么Looper是如何與主線程進行關(guān)聯(lián)的呢所袁?在Android中盏档,App進程是由Zygote fork 而創(chuàng)建的,而我們的ActivityThread就是運行在該進程下的主線程中纲熏,那么在ActivityThread的main方法中妆丘,Looper會通過prepareMainLooper()來創(chuàng)建內(nèi)部的消息隊列(MessageQueue),同時會通過loop()構(gòu)建消息循環(huán)锄俄。具體代碼如下圖所示:

   public static void main(String[] args) {
        ...省略部分代碼
        Looper.prepareMainLooper();//
        
        ActivityThread thread = new ActivityThread();
        thread.attach(false);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }
        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

要了解當前Loooper如何與主線程進行關(guān)聯(lián)的局劲,需要繼續(xù)查看prepareMainLooper()方法。下述代碼中奶赠,為了大家方便鱼填,我將prepareMainLooper()方法所涉及到的方法全部羅列了出來。

   //創(chuàng)建主線程Looper對象
   public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }
    
  //Looper與當前主線程綁定
  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));//創(chuàng)建Looper對象毅戈,放入主線程局部變量中
    }
    
  //獲取當前主線程的Looper對象
  public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
   }

觀察上訴代碼苹丸,我們發(fā)現(xiàn),prepareMainLooper方法內(nèi)部調(diào)用prepare()方法(這里我們忽略該方法中的參數(shù) quitAllowed)苇经,而prepare內(nèi)部調(diào)用的是ThreadLocal的set()方法赘理。如果你閱讀了之前我寫的《Android Handler機制之ThreadLocal》。扇单,那么大家應(yīng)該知道了當前Looper對象已經(jīng)與主線程關(guān)聯(lián)了(也可以說商模,當前主線程中保存了當前Looper對象的引用)。

Looper內(nèi)部創(chuàng)建消息隊列

在了解了Looper對象怎么與當前線程關(guān)聯(lián)的后蜘澜,我們來看看Looper類中的具體方法施流。之前我們說過,在創(chuàng)建Looper對象的時候鄙信,當前Looper對象內(nèi)部也會創(chuàng)建與之關(guān)聯(lián)的消息隊列(MessageQueue)瞪醋。那么查看Looper對應(yīng)的構(gòu)造函數(shù):

    final MessageQueue mQueue;
    //Looper內(nèi)部會創(chuàng)建MessageQueue對象
    private Looper(boolean quitAllowed) {
        mQueue = new MessageQueue(quitAllowed);
        mThread = Thread.currentThread();
    }

從Looper對象的構(gòu)造函數(shù)中,我們很明顯的看出內(nèi)部創(chuàng)建了MessageQueue對象装诡,也驗證了我們之前的說法银受。

Looper的消息循環(huán)

當前Looper對象與主線程關(guān)聯(lián)后践盼,接著會調(diào)用Looper對象中的loop()方法來開啟消息循環(huán)。具體代碼如下圖所示:

    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;
        ...省略部分代碼
        for (;;) {//一直循環(huán)去獲取消息隊列中的消息
            Message msg = queue.next(); //該方法可能堵塞宾巍,
            if (msg == null) {
                //如果沒有消息宏侍,表示當前消息隊列已經(jīng)退出
                return;
            }

        ...省略部分代碼
            try {
             //獲取消息后,執(zhí)行發(fā)送消息的handler的dispatchMessage方法蜀漆。
                msg.target.dispatchMessage(msg);
                end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
        ...省略部分代碼
            }

            msg.recycleUnchecked();
        }
    }

通過上述代碼谅河,我們可以看出,在Looper中的loop()方法中會一直去拿當前消息隊列中的消息确丢,如果能取出消息會調(diào)用該消息的target去執(zhí)行dispatchMessage()方法绷耍。如果沒有消息,就直接退出消息循環(huán)鲜侥。

MessageQueue原理

MessageQueue的next()方法

因為Looper中l(wèi)oop()方法會循環(huán)調(diào)用MessageQueue中的next方法褂始,接下來帶著大家一起查看該方法。代碼如下圖所示:

 Message next() {
    ...省略部分代碼
     for (;;) {
         synchronized (this) {
         ...省略部分代碼
         if (msg != null) {
           if (now < msg.when) {
                nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        //遍歷消息列表描函,取出消息
                        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;
                    }
                }
              ...省略部分代碼
        }
      }
    }

上述代碼中崎苗,我省略了很多代碼,現(xiàn)在大家不需要關(guān)心省略的內(nèi)容舀寓,大家只要關(guān)心大的一個方向就夠了胆数,關(guān)于MessageQueue的next()具體詳解,會在下篇文章 《Android Handler機制之Message的發(fā)送與取出》具體介紹互墓。好了必尼,大家把狀態(tài)調(diào)整過來。
在上文中篡撵,我們說過MessageQueue是以鏈表的形式來存儲消息的判莉,從next()方法中我們能分析出來,next()方法會一直從MessageQueue中去獲取消息育谬,直到獲取消息后才會退出券盅。

MessageQueue的enqueueMessage()方法

通過上文,我們已經(jīng)了解Message取消息的流程膛檀,現(xiàn)在我們來看看消息隊列的加入過程锰镀。

boolean enqueueMessage(Message msg, long when) {
          ...省略部分代碼
        synchronized (this) {
          ...省略部分代碼
            if (p == null || when == 0 || when < p.when) {
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {
              ...省略部分代碼
                //循環(huán)遍歷消息隊列,把當前進入的消息放入合適的位置(比較等待時間)
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                //將消息插入合適的位置
                msg.next = p; 
                prev.next = msg;
            }

          ...省略部分代碼
        }
        return true;
    }

上訴代碼中宿刮,我們把重心放在for循環(huán)中互站,在for循環(huán)中主要干了 一件事,就是根據(jù)當前meesag.when的值僵缺,來確定當前插入的消息應(yīng)該放入消息隊列的位置胡桃。(當前小伙伴肯能會對message.when感到困惑,還是那句話磕潮,現(xiàn)階段我們只用關(guān)心主要的流程翠胰,具體的方法詳解會在下篇文章 《Android Handler機制之Message的發(fā)送與取出》具體介紹)

Handler的原理

了解了Looper與MessageQueue的原理后容贝,我們大致了解了整個消息處理的關(guān)系,現(xiàn)在就剩下發(fā)消息與處理消息的流程了之景。最后一點了斤富,大家堅持看完。

Handler是怎么與Looper進行關(guān)聯(lián)的

在文章最開始的圖中锻狗,Handler發(fā)消息最終會發(fā)送到對應(yīng)的Looper下的MessageQueue中满力。那么也就是說Handler與Looper之間必然有關(guān)聯(lián)。那么它是怎么與Looper進行關(guān)聯(lián)的呢轻纪?查看Handler的構(gòu)造函數(shù):

  //不帶Looper的構(gòu)造函數(shù)
  public Handler() {this(null, false);}
  public Handler(boolean async) {this(null, async);}
  public Handler(Callback callback) {this(callback, false);}
  public Handler(boolean async) {this(null, async);}
  public Handler(Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }
        //Looper.myLooper()內(nèi)部會調(diào)用sThreadLocal.get()油额,獲取線程中保存的looper局部變量
        mLooper = Looper.myLooper();
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread that has not called Looper.prepare()");
        }
        //獲取當前Looper中的MessageQueue
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }
    
 //帶Looper參數(shù)的構(gòu)造函數(shù)
  public Handler(Looper looper) { this(looper, null, false); }
  public Handler(Looper looper, Callback callback) { this(looper, callback, false);}
  public Handler(Looper looper, Callback callback, boolean async) {
        mLooper = looper;
        //獲取當前Looper中的MessageQueue
        mQueue = looper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }  

在Handler的構(gòu)造函數(shù)中,主要分為兩種類型的構(gòu)造函數(shù)刻帚,一種是帶Looper參數(shù)的構(gòu)造函數(shù)潦嘶,一種是不帶Looper參數(shù)的構(gòu)造函數(shù)。

  • 在不帶Looper參數(shù)的構(gòu)造函數(shù)中崇众,是通過Looper.myLooper()來獲取當前Looper對象的(也就是說掂僵,Handler獲取的Looper對象是與當前實例化當前Handler的線程相關(guān)的,那么如果Handler對象是在主線程中創(chuàng)建的顷歌,那么獲取的就是主線程的Looper锰蓬,注意前提條件當前線程線程已經(jīng)通過Looper.prepare()與Looper.loop()構(gòu)建了循環(huán)消息隊列,因為只有調(diào)用了該方法后衙吩,才會將當前Looper對象放入線程的局部變量中
  • 在帶Looper參數(shù)的構(gòu)造函數(shù)中互妓,Looper對象是通過外部直接傳入的。(這里其實給我們提供了一個思路坤塞,也就是我們可以構(gòu)建自己的消息處理循環(huán),具體細節(jié)參看類HandlerThread)

Handler怎么將消息發(fā)送到MessaageQueue(消息隊列)中去

在了解Handler怎么將消息發(fā)送到MessageQueue(消息隊列)澈蚌,我們先來了解Handler的發(fā)消息的系列方法摹芙。

//發(fā)送及時消息
public final boolean sendMessage(Message msg)
public final boolean sendEmptyMessage(int what)
public final boolean post(Runnable r)

//發(fā)送延時消息
public final boolean sendEmptyMessageDelayed(int what, long delayMillis)
public final boolean sendMessageDelayed(Message msg, long delayMillis)
public final boolean postDelayed(Runnable r, long delayMillis)

//發(fā)送定時消息
public boolean sendMessageAtTime(Message msg, long uptimeMillis)
public final boolean sendEmptyMessageAtTime(int what, long uptimeMillis)
public final boolean postAtTime(Runnable r, long uptimeMillis)

在Handler發(fā)消息的方法中,我們可以總共發(fā)消息的種類宛瞄,分為三種情況浮禾,第一種是及時消息,第二種是發(fā)送延時消息份汗,第三種是定時消息盈电。其中關(guān)于消息怎么在消息隊列中排列與處理。具體的方法詳解會在下篇文章《Android Handler機制之Message的發(fā)送與取出》具體介紹杯活。

通過查看Handler發(fā)送消息的幾個方法匆帚。我們發(fā)現(xiàn)內(nèi)部都調(diào)用了MessageQueue的enqueueMessage()方法。

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;//設(shè)置message.target為當前Handler對象
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);//獲取當前MessageQueue.將消息加入隊列中
    }

該方法內(nèi)部其實很簡單旁钧,就是獲取當前MessageQueue對象吸重,將消息將入消息隊列中去了互拾。其中需要大家需要的注意的是這段代碼msg.target = this。該代碼意思就是當前的消息保存著當前發(fā)送消息的Handler對象的應(yīng)用嚎幸。該行代碼非常重要颜矿。因為最后涉及到消息的處理。

Handler怎么處理消息

通過上文的描述嫉晶,現(xiàn)在我們已經(jīng)大致了解Handler是怎么將消息加入到消息隊列中去了骑疆,現(xiàn)在需要關(guān)心的是當前消息是怎么被處理的。大家還記的之前我們講過的Looper原理吧替废,Looper會調(diào)用loop()方法循環(huán)的取消息封断。當取出消息后會調(diào)用message.target.dispatchMessage(Message msg)方法。其中message.target從上文我們已經(jīng)知道了舶担,就是當前發(fā)送消息的Handler坡疼。那么最終也就會回到Handler中的dispatchMessage()方法。

    public void dispatchMessage(Message msg) {
        if (msg.callback != null) {//第一步衣陶,判斷msg.callback
            handleCallback(msg);
        } else {
            if (mCallback != null) {//第二步柄瑰、判斷Handler的callBack
                if (mCallback.handleMessage(msg)) {
                    return;
                }
            }
            handleMessage(msg);//第三步,執(zhí)行Handler的handleMessage方法
        }
    }

觀察該方法剪况,我們可以得出教沾,Handler中的dispatchMessage()方法主要處理了三個步驟,下面分別對這三個步驟進行講解

第一步译断,執(zhí)行message.callback

在Handler中的dispatchMessage()方法中授翻,我們已經(jīng)知道如果msg.callback != null,那么我們會直接走handleCallback(msg)方法。在了解該方法之前孙咪,首先我們要知道m(xù)sg.callback對于的類是什么堪唐。這里我就直接給大家列出來了。其實msg.callback對應(yīng)是以下四個方法的Runnable對象翎蹈。

public final boolean post(Runnable r)
public final boolean postAtTime(Runnable r, long uptimeMillis)
public final boolean postAtTime(Runnable r, Object token, long uptimeMillis)
public final boolean postDelayed(Runnable r, long delayMillis)

以上四個方法在發(fā)送Runnable對象時淮菠,都會調(diào)用getPostMessage(Runnable r) 方法,且該方法都會將Runnable封裝在Message對象的callback屬性上荤堪。具體如下getPostMessage(Runnable r) 方法所示:

  private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }

在了解了Message的callback到底什么過后合陵,我們再來看看handleCallback(Message message)方法

 private static void handleCallback(Message message) {
        message.callback.run();
    }

該方法其實很簡單,就是調(diào)用相應(yīng)Runnable的run()方法澄阳。

第二步拥知,執(zhí)行Handler的callBack

如果當前Message.callback為空,接下來會判斷Handler中的Callback回調(diào)是否為空碎赢,如果不為空則執(zhí)行Callback的handleMessage(Message msg)方法低剔。Callback的具體聲明如下:

  //避免創(chuàng)建Handler對象重新HandlerMessage方法,你可以直接傳入Callback接口實現(xiàn)
  public interface Callback {
        public boolean handleMessage(Message msg);
    }

其中在Handler的幾個構(gòu)造函數(shù)中揩抡,可以傳入相應(yīng)Callback接口實現(xiàn)户侥。

public Handler(Callback callback) 
public Handler(Looper looper, Callback callback) 
public Handler(Callback callback, boolean async)
public Handler(Looper looper, Callback callback, boolean async)

第三步镀琉,執(zhí)行Handler的handleMessage)

如果都不滿足上面描述的第一、第二情況時蕊唐,會最終調(diào)用Handler的handleMessage(Message msg)方法屋摔。

  //Handler內(nèi)部該方法是空實現(xiàn),需要子類具體實現(xiàn)
  public void handleMessage(Message msg) {  }

為了方便大家記憶替梨,我將Handler中的dispatchMessage()具體的邏輯流程畫了出來钓试。大家按需觀看。


dispatchMessage步驟.png

最后

看到最后大家已經(jīng)發(fā)現(xiàn)該篇文章主要著重于將Handler機制的整個流程副瀑,對于很多的代碼細節(jié)并沒有過多的描述弓熏,特別是關(guān)于Looper從MessageQueue(消息隊列)中取消息與MessageQueue(消息隊列)怎么放入消息的具體細節(jié)。不用擔心糠睡,關(guān)于這兩個知識點將會在下篇文章《Android Handler機制之Message的發(fā)送與取出》具體描述挽鞠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市狈孔,隨后出現(xiàn)的幾起案子信认,更是在濱河造成了極大的恐慌,老刑警劉巖均抽,帶你破解...
    沈念sama閱讀 222,183評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嫁赏,死亡現(xiàn)場離奇詭異,居然都是意外死亡油挥,警方通過查閱死者的電腦和手機潦蝇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,850評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來深寥,“玉大人攘乒,你說我怎么就攤上這事◆媛酰” “怎么了持灰?”我有些...
    開封第一講書人閱讀 168,766評論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長负饲。 經(jīng)常有香客問我,道長喂链,這世上最難降的妖魔是什么返十? 我笑而不...
    開封第一講書人閱讀 59,854評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮椭微,結(jié)果婚禮上洞坑,老公的妹妹穿的比我還像新娘臂拓。我一直安慰自己颗祝,他們只是感情好拥坛,可當我...
    茶點故事閱讀 68,871評論 6 398
  • 文/花漫 我一把揭開白布缀蹄。 她就那樣靜靜地躺著,像睡著了一般排拷。 火紅的嫁衣襯著肌膚如雪侧漓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,457評論 1 311
  • 那天监氢,我揣著相機與錄音布蔗,去河邊找鬼。 笑死浪腐,一個胖子當著我的面吹牛纵揍,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播议街,決...
    沈念sama閱讀 40,999評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼泽谨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了特漩?” 一聲冷哼從身側(cè)響起吧雹,我...
    開封第一講書人閱讀 39,914評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拾稳,沒想到半個月后吮炕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,465評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡访得,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,543評論 3 342
  • 正文 我和宋清朗相戀三年龙亲,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片悍抑。...
    茶點故事閱讀 40,675評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡鳄炉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出搜骡,到底是詐尸還是另有隱情拂盯,我是刑警寧澤,帶...
    沈念sama閱讀 36,354評論 5 351
  • 正文 年R本政府宣布记靡,位于F島的核電站谈竿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏摸吠。R本人自食惡果不足惜空凸,卻給世界環(huán)境...
    茶點故事閱讀 42,029評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望寸痢。 院中可真熱鬧呀洲,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,514評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至滓窍,卻和暖如春卖词,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背贰您。 一陣腳步聲響...
    開封第一講書人閱讀 33,616評論 1 274
  • 我被黑心中介騙來泰國打工坏平, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人锦亦。 一個月前我還...
    沈念sama閱讀 49,091評論 3 378
  • 正文 我出身青樓舶替,卻偏偏與公主長得像,于是被迫代替她去往敵國和親杠园。 傳聞我的和親對象是個殘疾皇子顾瞪,可洞房花燭夜當晚...
    茶點故事閱讀 45,685評論 2 360

推薦閱讀更多精彩內(nèi)容