iOS的KVO基礎(chǔ)與原理

前言

KVO(Key-Value Obsering)鍵值觀察袁滥。KVO是一種機(jī)制录择,該機(jī)制允許將需要被觀察的對象的指定屬性的更改通知給發(fā)送給觀察的對象脓钾。直接來說就是對某個對象的屬性的觀察監(jiān)聽宾濒,如果被觀察的屬性有發(fā)生了變化會以通知的形式發(fā)送給觀察的對象帕翻。KVO是基于KVC的基礎(chǔ)上的宠进,本人之前也寫了一篇介紹KVC的文章晕拆,具體可以看iOS的Key-Value Coding

KVO的主要好處是,您不必為每次更改屬性而寫一些其他代碼即可發(fā)送通知材蹬。但是與NSNotificationCenter通知不同实幕,NSNotificationCenter沒有中間對象為所有觀察者提供更改通知。而是在進(jìn)行更改時將通知直接發(fā)送到觀察對象堤器。并且NSObject提供了鍵值觀察的基本實(shí)現(xiàn)昆庇,只要是繼承NSObject就可以實(shí)現(xiàn)。

1.KVO的使用

通過實(shí)現(xiàn)以下三個步驟可以使對象接收KVO兼容屬性的鍵值觀察通知:

1.使用方法addObserver:forKeyPath:options:context:將觀察者注冊到觀察對象闸溃。

2.在觀察者內(nèi)部實(shí)現(xiàn)方法observeValueForKeyPath:ofObject:change:context:接收更改的通知整吆。

3.當(dāng)不再接收消息時拱撵,可以使用方法removeObserver:forKeyPath:注銷觀察者,至少在銷毀觀察者之前調(diào)用這個方法表蝙。

2.KVO的注冊

使用addObserver:forKeyPath:options:context:方法為需要觀察的屬性注冊一個觀察者裕膀。其中分別對options和context值進(jìn)行說明。

2.1 Option

options是一個NSKeyValueObservingOptions枚舉類型勇哗。在使用的時候可以單獨(dú)使用也可以用|符號多個連接使用昼扛。

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
   NSKeyValueObservingOptionNew = 0x01,
   NSKeyValueObservingOptionOld = 0x02,
   NSKeyValueObservingOptionInitial API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x04,
   NSKeyValueObservingOptionPrior API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0)) = 0x08
};
  • NSKeyValueObservingOptionNew:指明接受通知方法參數(shù)中的chang字典中包含改變后的新值,默認(rèn)情況下也是只接收新值欲诺。
  • NSKeyValueObservingOptionOld:指明接受通知方法參數(shù)中的change字典中包含改變前的舊值抄谐。
  • NSKeyValueObservingOptionInitial:當(dāng)指定了這個選項(xiàng)時,在addObserver:forKeyPath:options:context:消息被發(fā)出去后扰法,甚至不用等待這個消息返回蛹含,觀察者對象會馬上收到一個通知。這種通知只會發(fā)送一次塞颁,你可以利用這種“一次性”的通知來確定要觀察屬性的初始值浦箱。
  • NSKeyValueObservingOptionPrior:當(dāng)包含這個參數(shù)的時候,在被觀察的屬性的值改變前和改變后祠锣,系統(tǒng)各會給觀察者發(fā)送一個改變的通知酷窥;在屬性的值改變之前發(fā)送的改變的通知中,參數(shù)會包含NSKeyValueChangeNotificationIsPriorKey并且值為@YES伴网,但不會包含NSKeyValueChangeNewKey和它對應(yīng)的值蓬推。

2.2 Context

context指針可以是任意數(shù)據(jù),這些數(shù)據(jù)將在相應(yīng)的更改通知中傳遞回觀察者澡腾,也可以將context的值設(shè)置為NULL再通過依賴keyPath鍵值路徑字符串來確定更改通知的來源沸伏。但是這種方式會引發(fā)出問題,比如如果父類和子類都監(jiān)聽了相同的KeyPath鍵值路徑的話动分,這時就很難區(qū)分出來了毅糟。可能也有人會說澜公,可以根據(jù)observeValueForKeyPath:ofObject:change:context:方法的object來做判斷姆另,但是如果這樣的就有多層的嵌套,在沒有寫核心代碼的時候就有這樣的嵌套就顯得代碼很不優(yōu)雅.

注意:為什么是NULL不是nil呢玛瘸?因?yàn)镺C是C的超集蜕青,并且Context的參數(shù)指針類型的,
所以是NULL糊渊。什么時候可以是nil呢右核?一般是實(shí)例的時候可以為nil,類的時候Nil渺绒,指針的時候?yàn)镹ULL.

為了避免出現(xiàn)這種問題可以使用命名靜態(tài)變量地址的形式來設(shè)置context的值贺喝,可以為整個類選擇一個上下文菱鸥,然后依靠通知消息中的keyPath鍵路徑字符串來確定更改的內(nèi)容。另外躏鱼,還可以為每個觀察屬性的keyPath創(chuàng)建一個不同的context氮采,從而完全不需要進(jìn)行字符串比較,從而可以更有效地進(jìn)行通知解析例如:

static void *PersonNameContext = &PersonNameContext;
static void *PersonNickNameContext = &PersonNickNameContext;
static void *PersonFullNameContext = &PersonFullNameContext;
static void *PersonDataArrayContext = &PersonDataArrayContext;

observeValueForKeyPath:ofObject:change:context:方法中大概的實(shí)現(xiàn)和結(jié)果

-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change 
context:(void *)context{
    if(context == PersonNameContext){
        NSLog(@"處理name的代碼:%@",change);
    }else if(context == PersonNickNameContext){
        NSLog(@"處理nickName的代碼:%@",change);
    }else{
        [super observeValueForKeyPath:keyPath 
        ofObject:object change:change context:context];
    }
}

3.KVO的移除

在注冊使用完KVO了染苛,就需要對KVO移除鹊漠,實(shí)現(xiàn)調(diào)用removeObserver:forKeyPath:context:方法。因?yàn)橛^擦對象不會自動移除已經(jīng)注冊的KVO茶行,所以注冊和刪除KVO這兩個是需要成對出現(xiàn)的躯概,一般都是在init或者viewDidLoad方法中進(jìn)行注冊,在delloc方法中進(jìn)行刪除畔师,如果沒有移除會引發(fā)野指針錯誤娶靡。

4.手動更改通知

一般情況下,我們使用KVO的時候都是調(diào)用的系統(tǒng)的自動更改通知的看锉。但是姿锭,KVO也可以是手動設(shè)置的,需要觀察的對象里實(shí)現(xiàn)類方法automaticallyNotifiesObserversForKey默認(rèn)是返回YES的伯铣。如果設(shè)置返回NO呻此,并且在觀察屬性值之前調(diào)用willChangeValueForKey:和觀察值之后調(diào)用didChangeValueForKey:就改為手動的更改通知了。下面創(chuàng)建一個Person對象來簡單驗(yàn)證一下懂傀。

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Person : NSObject

@property(nonatomic,copy) NSString *fullName;
@property(nonatomic,copy) NSString *name;
@property(nonatomic,copy) NSString *nickName;
@property (nonatomic, strong) NSMutableArray *dateArray;

@end

NS_ASSUME_NONNULL_END


@implementation Person


+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    return NO;
}

@end

這是實(shí)現(xiàn)的部分代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor redColor];
    self.person = [[Person alloc] init];
    self.person.dateArray = [NSMutableArray arrayWithCapacity:1];
    [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
    [self.person addObserver:self forKeyPath:@"nickName" options:NSKeyValueObservingOptionNew context:PersonNickNameContext];
}

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self.person willChangeValueForKey:@"name"];
    self.person.name = @"jason";
    [self.person didChangeValueForKey:@"name"];
    self.person.nickName = @"煙火";
}

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if(context == PersonNameContext){
        NSLog(@"處理name的代碼:%@",change);
    }else if(context == PersonNickNameContext){
        NSLog(@"處理nickName的代碼:%@",change);
    }else{
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}

打印的結(jié)果:

2020-04-18 16:29:57.789887+0800 KVODemo[4950:209403] 處理name的代碼:{
    kind = 1;
    new = jason;
}

這時候會發(fā)現(xiàn)趾诗,如果改為手動更改通知的時候,那么例子中nickName這個屬性的自動更改通知就不會實(shí)現(xiàn)了蹬蚁。

如果一個操作造成了多個key的值的改變,則willChangeValueForKey:和didChangeValueForKey:必須嵌套著調(diào)用郑兴。官方文檔的例子:

- (void)setBalance:(double)theBalance {
    [self willChangeValueForKey:@"balance"];
    [self willChangeValueForKey:@"itemChanged"];
    _balance = theBalance;
    _itemChanged = _itemChanged+1;
    [self didChangeValueForKey:@"itemChanged"];
    [self didChangeValueForKey:@"balance"];
}

5.一對一的關(guān)系

例如:在Person中的fullName是依賴于name和nickName來設(shè)置值犀斋,在Person中獲取fullName的方法:

- (NSString *)fullName{
    return [NSString stringWithFormat:@"%@--%@",self.name,self.nickName];
}

這時候?yàn)榱擞^察fullName的值變化在Person中可以實(shí)現(xiàn)類方法keyPathsForValuesAffectingValueForKey:

+(NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];
    if ([key isEqualToString:@"fullName"]) {
        NSArray *affectingKeys = @[@"name", @"nickName"];
        keyPaths = [keyPaths setByAddingObjectsFromArray:affectingKeys];
    }
    return keyPaths;
}

實(shí)現(xiàn)的部分代碼:

 //監(jiān)聽的
[self.person addObserver:self forKeyPath:@"fullName" 
options:NSKeyValueObservingOptionNew context:PersonFullNameContext];

//對name和nickName的修改的
self.person.name = @"jason";
self.person.nickName = @"煙火";

//觀擦的回調(diào)
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change 
context:(void *)context{
    if(context == PersonFullNameContext){
        NSLog(@"fullName:%@",change[@"new"]);
    }
    else{
        [super observeValueForKeyPath:keyPath 
        ofObject:object change:change context:context];
    }
}

//打印的結(jié)果
2020-04-18 17:13:55.894622+0800 KVODemo[5486:237940] fullName:jason--煙火

6.多對多的關(guān)系

例如:在對Person中可變數(shù)組dateArray屬性進(jìn)行觀察,如果按照上面的方式來注冊情连,然后對數(shù)組添加數(shù)據(jù)叽粹,再監(jiān)聽context的值做操作,部分代碼:

//注冊數(shù)組屬性
[self.person addObserver:self forKeyPath:@"dateArray"
options:NSKeyValueObservingOptionNew context:PersonDataArrayContext];

//數(shù)組添加數(shù)據(jù)
[self.person.dateArray addObject:@"1"];

//監(jiān)聽
if(context == PersonDataArrayContext){
     NSLog(@"dataArray:%@",change[@"new"]);
}

但是這時候發(fā)現(xiàn)却舀,控制臺里什么東西都沒有打印出來虫几。這是為什么呢?因?yàn)镵VO對屬性的setter方法進(jìn)行監(jiān)聽的挽拔,可變數(shù)組的addObject方法沒有setter方法辆脸,所以就監(jiān)聽不了。但是是不是說這樣就監(jiān)聽不了數(shù)組了呢螃诅?并不是的啡氢,因?yàn)镵VO是建立在KVC的基礎(chǔ)上的状囱,主要改為

[[self.person mutableArrayValueForKey:@"dateArray"] addObject:@"1"];

就可以監(jiān)聽到了。

7.原理

7.1 isa_swizzling

根據(jù)蘋果的官方文檔可以知道倘是,KVO的實(shí)現(xiàn)原理是通過對象的isa交換即isa-swizzling亭枷。isa指針就是指向?qū)ο蟮念悾趯ο鬄閷傩宰訩VO的時候搀崭,將修改觀察對象的isa指針叨粘,指向中間類而不是真實(shí)類,所以isa指針的值不一定反映實(shí)例的實(shí)際類瘤睹,不能依靠isa指針來確定類成員升敲,應(yīng)該使用class類方法來確定對象實(shí)例的類。

為了驗(yàn)證這個說法默蚌,還是用上一篇文章介紹的Person類來冻晤,然后打斷點(diǎn),用po的指令绸吸,得到的如下圖所示鼻弧。其中,Person在添加觀擦者之前的self.person對象的類名和class類方法是一樣的锦茁。

添加觀察者之前

在有注冊了觀察者之后self.person對象的類名變成了NSKVONotifying_Person這一個中間類名了攘轩。

添加觀察者之后

所以中間生成的是一個動態(tài)類NSKVONotifying_Person,但是修改的是原對象的isa码俩。

7.2中間類與原類的關(guān)系

對于動態(tài)的生成的中間類NSKVONotifying_PersonPerson這個類的它們之間的關(guān)系是怎樣的暫時還是不清楚的度帮。為了搞明白它們之間的關(guān)系,就寫了一個方法來探究一下兩者之間的關(guān)系

#pragma mark - 遍歷類以及子類
- (void)printClasses:(Class)cls{
    // 注冊類的總數(shù)
    int count = objc_getClassList(NULL, 0);
    // 創(chuàng)建一個數(shù)組稿存, 其中包含給定對象
    NSMutableArray *mArray = [NSMutableArray arrayWithObject:cls];
    // 獲取所有已注冊的類
    Class* classes = (Class*)malloc(sizeof(Class)*count);
    objc_getClassList(classes, count);
    for (int i = 0; i<count; i++) {
        if (cls == class_getSuperclass(classes[i])) {
            [mArray addObject:classes[i]];
        }
    }
    free(classes);
    NSLog(@"classes = %@", mArray);
}

并且在Person類對屬性注冊觀察者之前和之后分別打印當(dāng)前的注冊的類和子類列表笨篷,得到的結(jié)果

2020-04-18 22:56:39.522750+0800 KVODemo[7411:334483] classes = (
    Person
)
2020-04-18 22:56:42.953337+0800 KVODemo[7411:334483] classes = (
    Person,
    "NSKVONotifying_Person"
)

由此可知,對象Person與動態(tài)生成的類NSKVONotifying_Person之間的關(guān)系是繼承關(guān)系瓣履。NSKVONotifying_PersonPerson類的子類率翅。但是,并不是說KVO是對所有的要被觀察的類的屬性和變量都是可以觀察的監(jiān)聽的袖迎,因?yàn)樵赑erson類中添加成員變量冕臭,并且修改成員變量的值,發(fā)現(xiàn)回調(diào)中并沒有值返回燕锥。

7.2中間類的內(nèi)部

對于動態(tài)生成的中間類NSKVONotifying_xxxx是不是很好奇內(nèi)部的方法到底是怎樣的辜贵?下面就添加了這個方法來遍歷出類的全部方法

#pragma mark - 遍歷方法
- (void)printClassAllMethod:(Class)cls{
    NSLog(@"*********************");
    unsigned int count = 0;
    Method *methodList = class_copyMethodList(cls, &count);
    for (int i = 0; i<count; i++) {
        Method method = methodList[i];
        SEL sel = method_getName(method);
        IMP imp = class_getMethodImplementation(cls, sel);
        NSLog(@"%@-%p",NSStringFromSelector(sel),imp);
    }
    free(methodList);
}

并且分別在添加觀察name之前和之后打印出來,只對name屬性觀察

 [self printClasses:[Person class]];
 [self printClassAllMethod:[Person class]];
 [self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:PersonNameContext];
 [self printClasses:[Person class]];
 [self printClassAllMethod:NSClassFromString(@"NSKVONotifying_Person")];

打印的結(jié)果

2020-04-23 10:06:22.678886+0800 KVODemo[3414:59220] classes = (
    Person
)
2020-04-23 10:06:28.331048+0800 KVODemo[3414:59220] *********************
2020-04-23 10:06:28.331264+0800 KVODemo[3414:59220] setDateArray:-0x10fcf3bc0
2020-04-23 10:06:28.331381+0800 KVODemo[3414:59220] .cxx_destruct-0x10fcf3c00
2020-04-23 10:06:28.331506+0800 KVODemo[3414:59220] name-0x10fcf3ac0
2020-04-23 10:06:28.331631+0800 KVODemo[3414:59220] setName:-0x10fcf3af0
2020-04-23 10:06:28.331757+0800 KVODemo[3414:59220] dateArray-0x10fcf3ba0
2020-04-23 10:06:28.331873+0800 KVODemo[3414:59220] fullName-0x10fcf39b0
2020-04-23 10:06:28.332025+0800 KVODemo[3414:59220] setFullName:-0x10fcf3a80
2020-04-23 10:06:28.332420+0800 KVODemo[3414:59220] nickName-0x10fcf3b30
2020-04-23 10:06:28.332799+0800 KVODemo[3414:59220] setNickName:-0x10fcf3b60
2020-04-23 10:06:28.336781+0800 KVODemo[3414:59220] classes = (
    Person,
    "NSKVONotifying_Person"
)
2020-04-23 10:06:28.336950+0800 KVODemo[3414:59220] *********************
2020-04-23 10:06:28.337077+0800 KVODemo[3414:59220] setName:-0x110079c7a
2020-04-23 10:06:28.337197+0800 KVODemo[3414:59220] class-0x11007873d
2020-04-23 10:06:28.337296+0800 KVODemo[3414:59220] dealloc-0x1100784a2
2020-04-23 10:06:28.337383+0800 KVODemo[3414:59220] _isKVOA-0x11007849a

從打印出來的結(jié)果可以看到NSKVONotifing_Person的類的方法分別有class,delloc,_isKVOAsetName:方法,因?yàn)橹粚ame屬性觀察归形,所以只有setName方法托慨,就是說NSKVONotifing_Person雖然是Person類的子類,但是并不是將Person類的全部方法都加進(jìn)去的连霉,只重寫了觀察屬性的setter方法榴芳。

在delloc方法對被觀察的屬性銷毀之后嗡靡,中間動態(tài)類的isa會重新指向原來的對象,并且當(dāng)生成了一次中間類之后窟感,這個中間類就會一直存在緩存中讨彼,并不會被銷毀的。

8.最后

至此有關(guān)KVO的基礎(chǔ)與原理相關(guān)的就介紹到這里了柿祈,如果想了解更多的詳細(xì)有關(guān)KVO的知識哈误,可以閱讀蘋果的官方文檔鍵值觀察編程指南

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市躏嚎,隨后出現(xiàn)的幾起案子蜜自,更是在濱河造成了極大的恐慌,老刑警劉巖卢佣,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件重荠,死亡現(xiàn)場離奇詭異,居然都是意外死亡虚茶,警方通過查閱死者的電腦和手機(jī)戈鲁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來嘹叫,“玉大人婆殿,你說我怎么就攤上這事≌稚龋” “怎么了婆芦?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長喂饥。 經(jīng)常有香客問我消约,道長,這世上最難降的妖魔是什么员帮? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任荆陆,我火速辦了婚禮,結(jié)果婚禮上集侯,老公的妹妹穿的比我還像新娘。我一直安慰自己帜消,他們只是感情好棠枉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著泡挺,像睡著了一般辈讶。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上娄猫,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天贱除,我揣著相機(jī)與錄音生闲,去河邊找鬼。 笑死月幌,一個胖子當(dāng)著我的面吹牛碍讯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播扯躺,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼捉兴,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了录语?” 一聲冷哼從身側(cè)響起倍啥,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎澎埠,沒想到半個月后虽缕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蒲稳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年氮趋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片弟塞。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡凭峡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出决记,到底是詐尸還是另有隱情摧冀,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布系宫,位于F島的核電站索昂,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏扩借。R本人自食惡果不足惜椒惨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望潮罪。 院中可真熱鬧康谆,春花似錦、人聲如沸嫉到。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽何恶。三九已至孽锥,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背惜辑。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工唬涧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人盛撑。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓碎节,卻偏偏與公主長得像,于是被迫代替她去往敵國和親撵彻。 傳聞我的和親對象是個殘疾皇子钓株,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

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