「Android」Android開(kāi)發(fā)你需要知道的注解(Annotation)

本文來(lái)自尚妝Android團(tuán)隊(duì)路飛
發(fā)表于尚妝github博客,歡迎訂閱!

  • 一、什么是注解
    • 1、注解的作用
    • 2、注解都有哪些
  • 二、自定義注解
    • 1、RetentionPolicy.SOURCE
    • 2、RetentionPolicy.RUNTIME
    • 3、RetentionPolicy.CLASS

【說(shuō)在前面的話】

  • 要想看懂很多開(kāi)源庫(kù),如Arouter, dagger,Butter Knife等,不得不先看懂注解;
  • 想更好地提升開(kāi)發(fā)效率和代碼質(zhì)量,注解可以幫上很大的忙;

一、什么是注解

java.lang.annotation,接口 Annotation,在JDK5.0及以后版本引入。
注解是代碼里的特殊標(biāo)記,這些標(biāo)記可以在編譯、類加載、運(yùn)行時(shí)被讀取,并執(zhí)行相應(yīng)的處理。通過(guò)使用Annotation,開(kāi)發(fā)人員可以在不改變?cè)羞壿嫷那闆r下,在源文件中嵌入一些補(bǔ)充的信息。代碼分析工具、開(kāi)發(fā)工具和部署工具可以通過(guò)這些補(bǔ)充信息進(jìn)行驗(yàn)證、處理或者進(jìn)行部署。

在運(yùn)行時(shí)讀取需要使用Java反射機(jī)制進(jìn)行處理。

Annotation不能運(yùn)行,它只有成員變量,沒(méi)有方法。Annotation跟public、final等修飾符的地位一樣,都是程序元素的一部分,Annotation不能作為一個(gè)程序元素使用。

其實(shí)大家都是用過(guò)注解的,只是可能沒(méi)有過(guò)深入了解它的原理和作用,比如肯定見(jiàn)過(guò)@Override,@Deprecated等。

1、注解的作用

注解將一些本來(lái)重復(fù)性的工作,變成程序自動(dòng)完成,簡(jiǎn)化和自動(dòng)化該過(guò)程。比如用于生成Java doc,比如編譯時(shí)進(jìn)行格式檢查,比如自動(dòng)生成代碼等,用于提升軟件的質(zhì)量和提高軟件的生產(chǎn)效率。

2、注解都有哪些

平時(shí)我們使用的注解有來(lái)自JDK里包含的,也有Android SDK里包含的,也可以自定義。
2.1、JDK定義的元注解

Java提供了四種元注解,專門負(fù)責(zé)新注解的創(chuàng)建工作,即注解其他注解。

  • @Target
    定義了Annotation所修飾的對(duì)象范圍,取值:

    • ElementType.CONSTRUCTOR:用于描述構(gòu)造器
    • ElementType.FIELD:用于描述域
    • ElementType.LOCAL_VARIABLE:用于描述局部變量
    • ElementType.METHOD:用于描述方法
    • ElementType.PACKAGE:用于描述包
    • ElementType.PARAMETER:用于描述參數(shù)
    • ElementType.TYPE:用于描述類、接口(包括注解類型) 或enum聲明
  • @Retention
    定義了該Annotation被保留的時(shí)間長(zhǎng)短,取值:
     - RetentionPoicy.SOURCE:注解只保留在源文件,當(dāng)Java文件編譯成class文件的時(shí)候,注解被遺棄;用于做一些檢查性的操作,比如 @Override@SuppressWarnings
     - RetentionPoicy.CLASS:注解被保留到class文件,但jvm加載class文件時(shí)候被遺棄,這是默認(rèn)的生命周期;用于在編譯時(shí)進(jìn)行一些預(yù)處理操作,比如生成一些輔助代碼(如 ButterKnife
     - RetentionPoicy.RUNTIME:注解不僅被保存到class文件中,jvm加載class文件之后,仍然存在;用于在運(yùn)行時(shí)去動(dòng)態(tài)獲取注解信息。

  • @Documented
    標(biāo)記注解,用于描述其它類型的注解應(yīng)該被作為被標(biāo)注的程序成員的公共API,因此可以被例如javadoc此類的工具文檔化,不用賦值。

  • @Inherited
    標(biāo)記注解,允許子類繼承父類的注解。 這里一開(kāi)始有點(diǎn)理解不了,需要斷句一下,允許子類繼承父類的注解。示例:

Target(value = ElementType.TYPE)  
@Retention(RetentionPolicy.RUNTIME)  
@Inherited  
public @interface Sample {   
    public String name() default "";      
}


@Sample  
class Test{  
}  

class Test2 extents Test{  
}  

這樣類Test2其實(shí)也有注解@Sample 。

如果成員名稱是value,在賦值過(guò)程中可以簡(jiǎn)寫(xiě)。如果成員類型為數(shù)組,但是只賦值一個(gè)元素,則也可以簡(jiǎn)寫(xiě)。
示例以下三個(gè)寫(xiě)法都是等價(jià)的。

正常寫(xiě)法

@Documented
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.ANNOTATION_TYPE})
public @interface Target {
    ElementType[] value();
}

省略value的寫(xiě)法(只有成員名稱是value時(shí)才能省略)

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Target {
    ElementType[] value();
}

成員類型是數(shù)組,只賦值一個(gè)元素的簡(jiǎn)寫(xiě)

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

** 2.2 JDK內(nèi)置的其他注解 **

@Override、@Deprecated、@SuppressWarnings、@SafeVarargs、@FunctionalInterface、@Resources等。

** 2.3 Android SDK內(nèi)置的注解 **

Android SDK 內(nèi)置的注解都在包c(diǎn)om.android.support:support-annotations里,下面以'com.android.support:support-annotations:25.2.0'為例

  • 資源引用限制類:用于限制參數(shù)必須為對(duì)應(yīng)的資源類型
    @AnimRes @AnyRes @ArrayRes @AttrRes @BoolRes @ColorRes等
  • 線程執(zhí)行限制類:用于限制方法或者類必須在指定的線程執(zhí)行
    @AnyThread @BinderThread @MainThread @UiThread @WorkerThread
  • 參數(shù)為空性限制類:用于限制參數(shù)是否可以為空
    @NonNull @Nullable
  • 類型范圍限制類:用于限制標(biāo)注值的值范圍
    @FloatRang @IntRange
  • 類型定義類:用于限制定義的注解的取值集合
    @IntDef @StringDef
  • 其他的功能性注解:
    @CallSuper @CheckResult @ColorInt @Dimension @Keep @Px @RequiresApi @RequiresPermission @RestrictTo @Size @VisibleForTesting

二、自定義注解

使用收益最大的,還是需要根據(jù)自身需求自定義注解。下面依次介紹三種類型的注解自定義示例:

1、RetentionPolicy.SOURCE

一般函數(shù)的參數(shù)值有限定的情況,比如View.setVisibility 的參數(shù)就有限定,可以看到View.class源碼里

除了IntDef,還有StringDef

 @IntDef({VISIBLE, INVISIBLE, GONE})
 @Retention(RetentionPolicy.SOURCE)
 public @interface Visibility {}

  public static final int VISIBLE = 0x00000000;

  public static final int INVISIBLE = 0x00000004;

public static final int GONE = 0x00000008;
    
public void setVisibility(@Visibility int visibility) {
    setFlags(visibility, VISIBILITY_MASK);
}

2、RetentionPolicy.RUNTIME

運(yùn)行時(shí)注解的定義如下:

// 適用類、接口(包括注解類型)或枚舉  
Retention(RetentionPolicy.RUNTIME)  
Target(ElementType.TYPE)  
public @interface ClassInfo {  
    String value();  
}  
// 適用field屬性,也包括enum常量  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.FIELD)  
public @interface FieldInfo {  
    int[] value();  
}  
// 適用方法  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.METHOD)  
public @interface MethodInfo {  
    String name() default "long";  
    int age() default 27;  
}  

定義一個(gè)測(cè)試類來(lái)使用這些注解:

/** 
 * 測(cè)試運(yùn)行時(shí)注解 
 */  
@ClassInfo("Test Class")  
public class TestRuntimeAnnotation {  
  
    @FieldInfo(value = {1, 2})  
    public String fieldInfo = "FiledInfo";  
    
    @MethodInfo(name = "BlueBird")  
    public static String getMethodInfo() {  
        return return fieldInfo;  
    }  
}  

使用注解:

/** 
 * 測(cè)試運(yùn)行時(shí)注解 
 */  
private void _testRuntimeAnnotation() {  
    StringBuffer sb = new StringBuffer();  
    Class<?> cls = TestRuntimeAnnotation.class;  
    Constructor<?>[] constructors = cls.getConstructors();  
    // 獲取指定類型的注解  
    sb.append("Class注解:").append("\n");  
    ClassInfo classInfo = cls.getAnnotation(ClassInfo.class);  
    if (classInfo != null) {  
        sb.append(cls.getSimpleName()).append("\n");  
        sb.append("注解值: ").append(classInfo.value()).append("\n\n");  
    }  
  
    sb.append("Field注解:").append("\n");  
    Field[] fields = cls.getDeclaredFields();  
    for (Field field : fields) {  
        FieldInfo fieldInfo = field.getAnnotation(FieldInfo.class);  
        if (fieldInfo != null) {  
            sb.append(field.getName()).append("\n");  
            sb.append("注解值: ").append(Arrays.toString(fieldInfo.value())).append("\n\n");  
        }  
    }  
  
    sb.append("Method注解:").append("\n");  
    Method[] methods = cls.getDeclaredMethods();  
    for (Method method : methods) {  
        MethodInfo methodInfo = method.getAnnotation(MethodInfo.class);  
        if (methodInfo != null) {  
            sb.append(Modifier.toString(method.getModifiers())).append(" ")  
                    .append(method.getName()).append("\n");  
            sb.append("注解值: ").append("\n");  
            sb.append("name: ").append(methodInfo.name()).append("\n");  
            sb.append("age: ").append(methodInfo.age()).append("\n");  
        }  
    }  
  
    System.out.print(sb.toString());  
}  

所做的操作都是通過(guò)反射獲取對(duì)應(yīng)元素,再獲取元素上面的注解,最后得到注解的屬性值。因?yàn)樯婕暗椒瓷?,所以運(yùn)行時(shí)注解的效率多少會(huì)受到影響,現(xiàn)在很多的開(kāi)源項(xiàng)目使用的是編譯時(shí)注解。

3、RetentionPolicy.CLASS

** 3.1 添加依賴 **

如果Gradle 插件是2.2以上的話,不需要添加以下android-apt依賴。

classpath 'com.android.tools.build:gradle:2.2.1'

在整個(gè)工程的 build.gradle 中添加android-apt的依賴

buildscript {  
    repositories {  
        jcenter()  
        mavenCentral()  // add  
    }  
    dependencies {  
        classpath 'com.android.tools.build:gradle:2.1.2' //2.2以上無(wú)需添加apt依賴 
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'  // add  
    }  
}  

android-apt
android-apt 是一個(gè)Gradle插件,協(xié)助Android Studio 處理annotation processors, 它有兩個(gè)目的:

  • 允許配置只在編譯時(shí)作為注解處理器的依賴,而不添加到最后的APK或library
  • 設(shè)置源路徑,使注解處理器生成的代碼能被Android Studio正確的引用

伴隨著 Android Gradle 插件 2.2 版本的發(fā)布,近期 android-apt 作者在官網(wǎng)發(fā)表聲明證實(shí)了后續(xù)將不會(huì)繼續(xù)維護(hù) android-apt,并推薦大家使用 Android 官方插件提供的相同能力。也就是說(shuō),大約三年前推出的 android-apt 即將告別開(kāi)發(fā)者,退出歷史舞臺(tái),Android Gradle 插件提供了名為 annotationProcessor 的功能來(lái)完全代替 android-apt。

** 3.2 定義要使用的注解 **
建一個(gè)Java庫(kù)來(lái)專門放注解,庫(kù)名為:annotations
定義注解

@Retention(RetentionPolicy.CLASS)  
@Target(ElementType.TYPE)  
public @interface MyAnnotation {  
    String value();  
}  

** 3.3 定義注解處理器 **
另外建一個(gè)Java庫(kù)工程,庫(kù)名為:processors
這里必須為Java庫(kù),不然會(huì)找不到j(luò)avax包下的相關(guān)資源

build.gradle 要依賴以下:

 compile 'com.google.auto.service:auto-service:1.0-rc2'
 compile 'com.squareup:javapoet:1.7.0'
    
compile(project(':annotations'))

其中,
auto-service 自動(dòng)用于在 META-INF/services 目錄文件夾下創(chuàng)建 javax.annotation.processing.Processor 文件;
javapoet用于產(chǎn)生 .java 源文件的輔助庫(kù),它可以很方便地幫助我們生成需要的.java 源文件

示例:

package com.example;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;

@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {

    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        // Filer是個(gè)接口,支持通過(guò)注解處理器創(chuàng)建新文件
        filer = processingEnv.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement element : annotations) {
            //新建文件
            if (element.getQualifiedName().toString().equals(MyAnnotation.class.getCanonicalName())) {
                // 創(chuàng)建main方法
                MethodSpec main = MethodSpec.methodBuilder("main")
                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                        .returns(void.class)
                        .addParameter(String[].class, "args")
                        .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
                        .build();
                // 創(chuàng)建HelloWorld類
                TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
                        .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                        .addMethod(main)
                        .build();

                try {
                    // 生成 com.example.HelloWorld.java
                    JavaFile javaFile = JavaFile.builder("com.example", helloWorld)
                            .addFileComment(" This codes are generated automatically. Do not modify!")
                            .build();
                    // 生成文件
                    javaFile.writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        //在Gradle console 打印日志
        for (Element element : roundEnv.getElementsAnnotatedWith(MyAnnotation.class)) {
            System.out.println("------------------------------");
            // 判斷元素的類型為Class
            if (element.getKind() == ElementKind.CLASS) {
                // 顯示轉(zhuǎn)換元素類型
                TypeElement typeElement = (TypeElement) element;
                // 輸出元素名稱
                System.out.println(typeElement.getSimpleName());
                // 輸出注解屬性值System.out.println(typeElement.getAnnotation(MyAnnotation.class).value());
            }
            System.out.println("------------------------------");
        }
        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(MyAnnotation.class.getCanonicalName());
        return annotataions;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

** 3.4 在代碼中使用定義的注解 **:
需要依賴上面的兩個(gè)java庫(kù)annotations和processors

import com.example.MyAnnotation;

@MyAnnotation("test")
public class MainActivity extends AppCompatActivity {

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

編譯后就會(huì)生成指定包名的指定文件,如圖:


Alt text

** 3.5 注解處理器的輔助接口 **
在自定義注解處理器的初始化接口,可以獲取到以下4個(gè)輔助接口:

public class MyProcessor extends AbstractProcessor {  
  
    private Types typeUtils;  
    private Elements elementUtils;  
    private Filer filer;  
    private Messager messager;  
  
    @Override  
    public synchronized void init(ProcessingEnvironment processingEnv) {  
        super.init(processingEnv);  
        typeUtils = processingEnv.getTypeUtils();  
        elementUtils = processingEnv.getElementUtils();  
        filer = processingEnv.getFiler();  
        messager = processingEnv.getMessager();  
    }  
}  

Types: Types是一個(gè)用來(lái)處理TypeMirror的工具
Elements: Elements是一個(gè)用來(lái)處理Element的工具
Filer: 一般我們會(huì)用它配合JavaPoet來(lái)生成我們需要的.java文件
Messager: Messager提供給注解處理器一個(gè)報(bào)告錯(cuò)誤、警告以及提示信息的途徑

** 3.5 帶有注解的庫(kù)提供給第三方 **

以下例子默認(rèn)用gradle插件2.2以上,不再使用apt

一般使用編譯時(shí)注解的庫(kù),都會(huì)有三個(gè)module:

  • 定義注解的module , java庫(kù),xxxx-annotations
  • 實(shí)現(xiàn)注解器的module, java庫(kù),xxxx-compiler
  • 提供對(duì)外接口的module, android庫(kù),xxxx-api

module xxxx-api的依賴這么寫(xiě):
dependencies { annotationProcessor 'xxxx-compiler:1.0.0' compile ' xxxx-annotations:1.0.0' //....others }

然后第三方使用時(shí),可以像如下這樣依賴:

dependencies {
    ...
    compile 'com.google.dagger:dagger:2.9'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.9'
}

sample project見(jiàn)AnnotationSample

參考鏈接:
http://www.jb51.net/article/80240.htm
http://blog.csdn.net/github_35180164/article/details/52121038
http://blog.csdn.net/github_35180164/article/details/52107204
https://www.zhihu.com/question/36486629
https://github.com/alibaba/ARouter/
http://blog.csdn.net/asce1885/article/details/52878076

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,702評(píng)論 6 534
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,615評(píng)論 3 419
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,606評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,044評(píng)論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,826評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,227評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,307評(píng)論 3 442
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,447評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,992評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,807評(píng)論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,001評(píng)論 1 370
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,550評(píng)論 5 361
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,243評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,667評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,930評(píng)論 1 287
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,709評(píng)論 3 393
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,996評(píng)論 2 374

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