JAVA動(dòng)態(tài)代理淺析

1 動(dòng)態(tài)代理使用

先看下動(dòng)態(tài)代理如何使用落午,然后再分析下實(shí)現(xiàn)原理

Object proxy = Proxy.newProxyInstance(person.getClass().getClassLoader(), person.getClass().getInterfaces(), new InvocationHandler() {
    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        String name = method.getName();

        Log.log("Object o, Method method, Object[] objects----------");
        if (method.getName().equals("setName")) {
            String item=(String) objects[0];
            objects[0]="modify="+item;
        }

        return method.invoke(person, objects);

    }
});

jdk1.8之前動(dòng)態(tài)代理在實(shí)現(xiàn)時(shí)反射會(huì)被頻繁調(diào)用到谎懦,所以在性能上會(huì)稍微差一些,但在jdk1.8對(duì)動(dòng)態(tài)代理的實(shí)現(xiàn)做了改良溃斋,性能有所提高

2 JDK 1.7動(dòng)態(tài)代理實(shí)現(xiàn)

看下在JDK1.7中動(dòng)態(tài)代理的實(shí)現(xiàn)邏輯界拦,大致如下:

根據(jù)classloader和動(dòng)態(tài)代理的接口類型先從緩存中獲取已經(jīng)生成的class對(duì)象,如果存在該對(duì)象則拿到該對(duì)象后通過反射生成代理對(duì)象梗劫。如果該class對(duì)象不存在則通過ProxyGenerator的generateProxyClass方法創(chuàng)建出對(duì)應(yīng)的byte數(shù)組
最終調(diào)用defineclass方法將byte數(shù)組轉(zhuǎn)換成class對(duì)象并緩存下次使用享甸。

源碼分析:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
        throws IllegalArgumentException {
    
    ...

    // 1、通過 loader 和 interfaces 創(chuàng)建動(dòng)態(tài)代理類
    Class<?> cl = getProxyClass0(loader, interfaces);

    try {
        // 2梳侨、通過反射機(jī)制獲取動(dòng)態(tài)代理類的構(gòu)造函數(shù)(參數(shù)類型是 InvocationHandler.class 類型)
        final Constructor<?> cons = cl.getConstructor(constructorParams);
        final InvocationHandler ih = h;
    
    ...

        // 3蛉威、通過動(dòng)態(tài)代理類的構(gòu)造函數(shù)和調(diào)用處理器對(duì)象創(chuàng)建代理類實(shí)例
        return newInstance(cons, ih);
    ...

    } catch (NoSuchMethodException e) {
        throw new InternalError(e.toString());
    }
}

調(diào)用getProxyClass0,如果緩存存在則直接使用走哺,沒有緩存則內(nèi)部創(chuàng)建瓷翻。拿到class對(duì)象后通過反射創(chuàng)建代理對(duì)象。

getProxyClass0整體邏輯如下割坠,先添個(gè)整體代碼,后面分段看具體邏輯:

private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) {
    Class<?> proxyClass = null;

    // 接口名稱數(shù)組妒牙,用于收集接口的名稱作為代理類緩存的 key
    String[] interfaceNames = new String[interfaces.length];

    // 接口集合彼哼,用于檢查是否重復(fù)的接口
    Set<Class<?>> interfaceSet = new HashSet<>();

    // 遍歷目標(biāo)對(duì)象實(shí)現(xiàn)的接口
    for (int i = 0; i < interfaces.length; i++) {
        // 獲取接口名稱
        String interfaceName = interfaces[i].getName();
        Class<?> interfaceClass = null;
        try {
            // 通過反射加載目標(biāo)類實(shí)現(xiàn)的接口到內(nèi)存中
            interfaceClass = Class.forName(interfaceName, false, loader);
        } catch (ClassNotFoundException e) {
        }
        if (interfaceClass != interfaces[i]) {
            throw new IllegalArgumentException(
                    interfaces[i] + " is not visible from class loader");
        }

    ...

        // 如果接口重復(fù),拋出異常
        if (interfaceSet.contains(interfaceClass)) {
            throw new IllegalArgumentException("repeated interface: " + interfaceClass.getName());
        }
        interfaceSet.add(interfaceClass);
        interfaceNames[i] = interfaceName;
    }

    // 將接口名稱數(shù)組轉(zhuǎn)換為接口名稱列表
    List<String> key = Arrays.asList(interfaceNames);

    // 通過 Classloader 獲取或者創(chuàng)建一個(gè)代理類緩存
    Map<List<String>, Object> cache;

    // 將一個(gè) ClassLoader 映射到該 ClassLoader 的代理類緩存
    // private static Map<ClassLoader, Map<List<String>, Object>> loaderToCache = new WeakHashMap<>();

    synchronized (loaderToCache) {
        cache = loaderToCache.get(loader);
        if (cache == null) {
            cache = new HashMap<>();
            loaderToCache.put(loader, cache);
        }
    }

    synchronized (cache) {
        do {
            Object value = cache.get(key);
            if (value instanceof Reference) {
                proxyClass = (Class<?>) ((Reference) value).get();
            }
            if (proxyClass != null) {
                return proxyClass;
            } else if (value == pendingGenerationMarker) {
                // 正在創(chuàng)建代理類湘今,等待敢朱,代理類創(chuàng)建完成后會(huì)執(zhí)行 notifyAll() 進(jìn)行通知
                try {
                    cache.wait();
                } catch (InterruptedException e) {
                }
                continue;
            } else {
                // 代理類為空,往代理類緩存中添加一個(gè) pendingGenerationMarker 標(biāo)識(shí)摩瞎,表示正在創(chuàng)建代理類
                cache.put(key, pendingGenerationMarker);
                break;
            }
        } while (true); //這是一個(gè)死循環(huán)拴签,直到代理類不為空時(shí),返回代理類
    }

    // 以下為生成代理類邏輯
    try {
        String proxyPkg = null;

        // 遍歷接口的訪問修飾符旗们,如果是非 public 的蚓哩,代理類包名為接口的包名
        for (int i = 0; i < interfaces.length; i++) {
            int flags = interfaces[i].getModifiers();
            if (!Modifier.isPublic(flags)) {
                String name = interfaces[i].getName();
                int n = name.lastIndexOf('.');
                String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
                if (proxyPkg == null) {
                    proxyPkg = pkg;
                } else if (!pkg.equals(proxyPkg)) {
                    throw new IllegalArgumentException("non-public interfaces from different packages");
                }
            }
        }

        if (proxyPkg == null) {
            // 如果接口都是 public 的,則用 com.sun.proxy 作為包名上渴,這個(gè)從 $Proxy0 類中可以看到
            proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
        }

        {
            long num;
            synchronized (nextUniqueNumberLock) {
                num = nextUniqueNumber++;
            }
            String proxyName = proxyPkg + proxyClassNamePrefix + num;

            // 根據(jù)代理類全路徑和接口創(chuàng)建代理類的字節(jié)碼
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces);
            try {
                // 根據(jù)代理類的字節(jié)碼生成代理類
                proxyClass = defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                throw new IllegalArgumentException(e.toString());
            }
        }

        // 創(chuàng)建的所有代理類集合
        // private static Map<Class<?>, Void> proxyClasses = Collections.synchronizedMap(new WeakHashMap<Class<?>, Void>());
        proxyClasses.put(proxyClass, null);
    } finally {
        synchronized (cache) {
            if (proxyClass != null) {
                // 創(chuàng)建好代理類后存到代理類緩存中
                cache.put(key, new WeakReference<Class<?>>(proxyClass));
            } else {
                // 否則岸梨,清除之前存入的 pendingGenerationMarker 標(biāo)識(shí)
                cache.remove(key);
            }
            cache.notifyAll();
        }
    }
    return proxyClass;
}

將上面的代碼拆分,先來看下動(dòng)態(tài)代理從緩存獲取代理class對(duì)象邏輯:

不同的classloader生成的class不是同一個(gè)對(duì)象稠氮,所以需要根據(jù)classloader和接口類型來唯一標(biāo)志一個(gè)代理class對(duì)象曹阔,接口類型如何獲取在JDK1.7和JDK1.8邏輯上有區(qū)分,這也是JDK1.8效率更高的原因隔披。先看下JDK1.7的接口獲取邏輯

    Class<?> proxyClass = null;

    // 接口名稱數(shù)組赃份,用于收集接口的名稱作為代理類緩存的 key
    String[] interfaceNames = new String[interfaces.length];

    // 接口集合,用于檢查是否重復(fù)的接口
    Set<Class<?>> interfaceSet = new HashSet<>();

// 遍歷目標(biāo)對(duì)象實(shí)現(xiàn)的接口
    for (int i = 0; i < interfaces.length; i++) {
        // 獲取接口名稱
        String interfaceName = interfaces[i].getName();
        Class<?> interfaceClass = null;
        try {
        // 通過反射加載目標(biāo)類實(shí)現(xiàn)的接口到內(nèi)存中
        interfaceClass = Class.forName(interfaceName, false, loader);
        } catch (ClassNotFoundException e) {
        }
        if (interfaceClass != interfaces[i]) {
        throw new IllegalArgumentException(
        interfaces[i] + " is not visible from class loader");
        }

        ...

        // 如果接口重復(fù)奢米,拋出異常
        if (interfaceSet.contains(interfaceClass)) {
        throw new IllegalArgumentException("repeated interface: " + interfaceClass.getName());
        }
        interfaceSet.add(interfaceClass);
        interfaceNames[i] = interfaceName;
        }

        // 將接口名稱數(shù)組轉(zhuǎn)換為接口名稱列表
        List<String> key = Arrays.asList(interfaceNames);

interfaces即外部傳入的需要實(shí)現(xiàn)的接口數(shù)組抓韩,然后會(huì)經(jīng)過一系列邏輯去重處理纠永,最終通過

List<String> key = Arrays.asList(interfaceNames);

來標(biāo)記需要實(shí)現(xiàn)動(dòng)態(tài)代理的class對(duì)象最終要實(shí)現(xiàn)哪些接口。
查看上述代碼可以發(fā)現(xiàn)在去重邏輯內(nèi)部使用到了反射生成class對(duì)象园蝠,該邏輯是每次調(diào)用newProxyInstance生成動(dòng)態(tài)代理對(duì)象時(shí)都會(huì)執(zhí)行的邏輯渺蒿,對(duì)性能是有一定影響的。

拿到唯一標(biāo)記key后如何從緩存拿到class代理對(duì)象邏輯如下:

    // 通過 Classloader 獲取或者創(chuàng)建一個(gè)代理類緩存
    Map<List<String>, Object> cache;

    synchronized (loaderToCache) {
        cache = loaderToCache.get(loader);
        if (cache == null) {
          cache = new HashMap<>();
          loaderToCache.put(loader, cache);
        }
    }

    synchronized (cache) {
        do {
          Object value = cache.get(key);
          if (value instanceof Reference) {
            proxyClass = (Class<?>) ((Reference) value).get();
          }
          if (proxyClass != null) {
            return proxyClass;
          } else if (value == pendingGenerationMarker) {
            // 正在創(chuàng)建代理類彪薛,等待茂装,代理類創(chuàng)建完成后會(huì)執(zhí)行 notifyAll() 進(jìn)行通知
            try {
              cache.wait();
            } catch (InterruptedException e) {
            }
            continue;
          } else {
            // 代理類為空,往代理類緩存中添加一個(gè) pendingGenerationMarker 標(biāo)識(shí)善延,表示正在創(chuàng)建代理類
            cache.put(key, pendingGenerationMarker);
            break;
          }
        } while (true); //這是一個(gè)死循環(huán)少态,直到代理類不為空時(shí),返回代理類
     }

loaderToCache定義如下:

private static Map<ClassLoader, Map<List<String>, Object>> loaderToCache = new WeakHashMap<>();

key是一個(gè)classloader對(duì)象易遣,value是該classloader生成的所有的動(dòng)態(tài)代理對(duì)象彼妻,但是不同的動(dòng)態(tài)代理對(duì)象可能實(shí)現(xiàn)了不同的接口,所以通過一個(gè)Map<List<String>, Object>來保存豆茫,List<String>用來唯一標(biāo)記接口類型侨歉,Object就是真正的代理class對(duì)象了。理解上述意思后再看上面的代碼就很清晰了揩魂。

最終class對(duì)象如何生成就是根據(jù)字節(jié)碼規(guī)則生成一個(gè)文件幽邓,然后往文件中寫入字段,方法等來實(shí)現(xiàn)火脉,實(shí)際上網(wǎng)上有一個(gè)著名的開源框架ASM就是專門用來處理字節(jié)碼插樁工作的牵舵,可以允許開發(fā)者在對(duì)字節(jié)碼并沒有非常熟悉的情況下也可以實(shí)現(xiàn)字節(jié)碼的插樁工作。但是JDK在實(shí)現(xiàn)字節(jié)碼插樁時(shí)并沒有借助該開源框架而是手寫插樁邏輯倦挂。到此JDK1.7的動(dòng)態(tài)代理分析就完成了畸颅。

3 JDK1.8動(dòng)態(tài)代理實(shí)現(xiàn)思路

總體和JDK1.7大致一致,但是在緩存處理上和1.7有部分出入方援。

private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
    proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
    
private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }

    // If the proxy class defined by the given loader implementing
    // the given interfaces exists, this will simply return the cached copy;
    // otherwise, it will create the proxy class via the ProxyClassFactory
    return proxyClassCache.get(loader, interfaces);
}

proxyClassCache獲取代理class也是通過classloader和接口來唯一標(biāo)志没炒。KeyFactory就是接口key的生成邏輯,
ProxyClassFactory就是生成class的工廠類犯戏,內(nèi)部邏輯和1.7大致一致窥浪。主要是KeyFactory如何生成key

private static final class KeyFactory
    implements BiFunction<ClassLoader, Class<?>[], Object>
{
    @Override
    public Object apply(ClassLoader classLoader, Class<?>[] interfaces) {
        switch (interfaces.length) {
            case 1: return new Key1(interfaces[0]); // the most frequent
            case 2: return new Key2(interfaces[0], interfaces[1]);
            case 0: return key0;
            default: return new KeyX(interfaces);
        }
    }
}

生成邏輯很簡(jiǎn)單就是通過interfaces長(zhǎng)度來生成不同的key。而在JDK1.7中是通過反射笛丙,然后去重等一系列操作來完成的漾脂,所以在性能上1.8的處理更優(yōu)。

proxyClassCache的get方法部分代碼:

public V get(K key, P parameter) {
    Objects.requireNonNull(parameter);

    expungeStaleEntries();

    Object cacheKey = CacheKey.valueOf(key, refQueue);

    // lazily install the 2nd level valuesMap for the particular cacheKey
    ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
    if (valuesMap == null) {
        ConcurrentMap<Object, Supplier<V>> oldValuesMap
                = map.putIfAbsent(cacheKey,
                valuesMap = new ConcurrentHashMap<>());
        if (oldValuesMap != null) {
            valuesMap = oldValuesMap;
        }
    }

    // create subKey and retrieve the possible Supplier<V> stored by that
    // subKey from valuesMap
    Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
    Supplier<V> supplier = valuesMap.get(subKey);
    
    ......
}

這里要在緩存中獲取到代理class對(duì)象胚鸯,需要classloader和接口來唯一標(biāo)志骨稿。JDK1.8中將classloader做為key,而把接口類型做為subkey,通過這兩個(gè)key來獲取class對(duì)象坦冠。知道這層關(guān)系后再看上面代碼就非常清楚了形耗,后面生成class對(duì)象的代碼省略,和JDK1.7大同小異辙浑。到此1.8的分析也就結(jié)束了激涤。

4 android動(dòng)態(tài)代理實(shí)現(xiàn)

android中動(dòng)態(tài)代理和JDK實(shí)現(xiàn)總體一致,但是真正在生成字節(jié)碼對(duì)象時(shí)的邏輯不是在java層處理判呕,generateProxy是一個(gè)jni方法

// Android-changed: Generate the proxy directly instead of calling
// through to ProxyGenerator.
List<Method> methods = getMethods(interfaces);
Collections.sort(methods, ORDER_BY_SIGNATURE_AND_SUBTYPE);
validateReturnTypes(methods);
List<Class<?>[]> exceptions = deduplicateAndGetExceptions(methods);

Method[] methodsArray = methods.toArray(new Method[methods.size()]);
Class<?>[][] exceptionsArray = exceptions.toArray(new Class<?>[exceptions.size()][]);

/*
 * Choose a name for the proxy class to generate.
 */
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;

return generateProxy(proxyName, interfaces, loader, methodsArray,
exceptionsArray);
}

5 JAVA動(dòng)態(tài)代理為什么只能代理有接口的類倦踢,而不能代理普通類

如果把JDK生成的代理類保存下來,可以看到類似如下結(jié)構(gòu)

public class $proxy0 extends proxy implements 接口類型{
......
}

可以看到代理類已經(jīng)繼承了proxy類侠草,由于java是單繼承結(jié)構(gòu)辱挥,所以代理類對(duì)象不能代理普通的類

為什么需要繼承proxy主要原因:

  • 1 newProxyInstance傳入了一個(gè)invocationHandler對(duì)象處理代理方法,如果生成的代理類不繼承proxy對(duì)象边涕,那么這個(gè)invocationHandler的調(diào)用時(shí)機(jī)晤碘,保存也需要通過字節(jié)碼寫入到代理class中,增加了邏輯復(fù)雜性功蜓。繼承proxy后通用邏輯就可以放在proxy處理

  • 2 在業(yè)務(wù)處理層面上园爷,一般會(huì)在接口層抽象一些公共處理能力,然后通過具體類去實(shí)現(xiàn)對(duì)應(yīng)接口是符合業(yè)務(wù)設(shè)計(jì)思想式撼,所以通過動(dòng)態(tài)代理相應(yīng)的接口童社,對(duì)相關(guān)的處理方法進(jìn)行攔截處理從設(shè)計(jì)角度上看是符合邏輯的。如果在業(yè)務(wù)層面上一定要代理普通類那么需要使用cglib庫來實(shí)現(xiàn)端衰。cglib底層也是通過字節(jié)碼插樁框架ASM來實(shí)現(xiàn)的,通過實(shí)現(xiàn)一個(gè)子類來重寫父類的非final方法達(dá)到動(dòng)態(tài)代理的目的

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末甘改,一起剝皮案震驚了整個(gè)濱河市旅东,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌十艾,老刑警劉巖抵代,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異忘嫉,居然都是意外死亡荤牍,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門庆冕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來康吵,“玉大人,你說我怎么就攤上這事访递』耷叮” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)惭载。 經(jīng)常有香客問我旱函,道長(zhǎng),這世上最難降的妖魔是什么描滔? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任棒妨,我火速辦了婚禮,結(jié)果婚禮上含长,老公的妹妹穿的比我還像新娘券腔。我一直安慰自己,他們只是感情好茎芋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布颅眶。 她就那樣靜靜地躺著,像睡著了一般田弥。 火紅的嫁衣襯著肌膚如雪涛酗。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天偷厦,我揣著相機(jī)與錄音商叹,去河邊找鬼。 笑死只泼,一個(gè)胖子當(dāng)著我的面吹牛剖笙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播请唱,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼弥咪,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了十绑?” 一聲冷哼從身側(cè)響起聚至,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎本橙,沒想到半個(gè)月后扳躬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡甚亭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年贷币,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片亏狰。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡役纹,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出暇唾,到底是詐尸還是另有隱情字管,我是刑警寧澤啰挪,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站嘲叔,受9級(jí)特大地震影響亡呵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜硫戈,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一锰什、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧丁逝,春花似錦汁胆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至罪既,卻和暖如春铸题,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背琢感。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工丢间, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人驹针。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓烘挫,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親柬甥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子饮六,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

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