iOS 开发Objective-C的使用技巧

此文是《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》的阅读笔记

目录

第1章 熟悉Objective-C

  • 第1条:了解Objective-C语言的起源
  • 第2条:在类的头文件中尽量少引入其他头文件
  • 第3条:多用字面量语法,少用与之等价的方法
  • 第4条:多用类型常量,少用#define预处理指令
  • 第5条:用枚举表示状态、选项、状态码

第2章 对象、消息、运行期

  • 第6条:理解“属性”这一概念
  • 第7条:在对象内部尽量直接访问实例变量
  • 第8条:理解“对象等同性”这一概念
  • 第9条:以“类族模式”隐藏实现细节
  • 第10条:在既有类中使用关联对象存在自定义数据
  • 第11条:理解objc_msgSend的作用
  • 第12条:理解消息转发机制(推荐看原文
  • 第13条:用“方法调配技术”调试“黑盒方法”
  • 第14条: 理解“类对象”的用意

第3章 接口与API设计

  • 第15条:用前缀避免命名空间冲突
  • 第16条:提供“全能初始化方法”
  • 第17条:实现description方法
  • 第18条:尽量使用不可变对象
  • 第19条:使用清晰而协调的命名方法
  • 第20条:为私有方法名加前缀
  • 第21条:理解Objective-C错误模型
  • 第22条:理解NSCopying协议

第4章 协议与分类

  • 第23条:通过委托与数据源协议进行对象间通信
  • 第24条:将类的实现代码分散到便于管理的数个分类之中
  • 第25条:总是为第三方类的分类名称加前缀
  • 第26条:勿在分类中声明属性
  • 第27条:使用"class-continuation分类"隐藏实现细节
  • 第28条:通过协议提供匿名对象

第5章 内存管理

  • 第29条:理解引用计数
  • 第30条:以ARC简化引用计数
  • 第31条:在dealloc方法中只释放引用并解除监听
  • 第32条:编写“异常安全代码”时留意内存管理问题
  • 第33条:以弱引用避免保留环
  • 第34条:以“自动释放池块”降低内存峰值
  • 第35条:用“僵尸对象”调试内存管理问题(推荐看原文
  • 第36条:不要使用retainCount

第6章 块与大中枢派发

  • 第37条:理解“块”这一概念
  • 第38条:为常用的块类型创建typedef
  • 第39条:用handler块降低代码分散程度
  • 第40条:用块引用其所属对象时不要出现保留环
  • 第41条:多用派发队列,少用同步锁
  • 第42条:多用GCD,少用performSelector系列方法
  • 第43条:掌握GCD及操作队列的使用时机
  • 第44条:通过Dispatch Group机制,根据系统资源状况来执行任务
  • 第45条:使用dispatch_once来执行只需运行一次的线程安全代码
  • 第46条:不要使用dispatch_get_current_queue

第7章 系统框架

  • 第47条:熟悉系统框架
  • 第48条:多用块枚举,少用for循环
  • 第49条:对自定义其内存管理语义的collection使用无缝桥接
  • 第50条:构建缓存时选用NSCache而非NSDictionary
  • 第51条:精简initialize与load的实现代码
  • 第52条:别忘了NSTimer会保留其目标对象

===================================

第一章 熟悉Objective-C

===================================

第1条:了解Objective-C语言的起源

  • Objective-C由Smalltalk(消息型语言的鼻祖)演化而来,使用的是“消息结构”(messageing structure)而非"函数调用"(functioncalling),这两者之间的区别就像这样:
 // Messaging (Objective-C)
Object *obj = [Object new];
[obj performWith: parameter1 and: parameter2];

// Function calling (C++)
Object *obj = new Object;
obj->perform(parameter1, parameter2);
  • Objective-C 为C语言添加了对象特性,是其超集。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。
  • Objective-C语言中的指针是用来指示对象的,声明一个变量,令其指代某个对象的语法基本上是照搬C语言的。
NSString *someString = @"The string";

变量someString指向分配在堆里的某块内存,其中含有一个NSString的对象"The string"。另外Objective-C的对象分配总是分配在"堆空间"(heap space)中,有些不含有*的变量,它们可能会使用栈空间(stack space),例如CGRect类型的分配。而这些分配在"堆空间"的对象,需要内存管理(现在不用手动管理了)。

第2条:在类的头文件中尽量少引入其他的头文件

  • 当不需要知道某个类的全部细节时,用@class TheClass,来替代#import "TheClass.h", 这叫做“向前声明”(forward declaring)该类。@class的使用既可以减少不必要的导入,也可以避免头文件因互相import而编译通不过。
// EOCPerson.m
# import "EOCPerson.h"
// # import "EOCEmployer.h"

@class EOCEmployer;

@interface EOCPerson : NSObject
@property (nonatomic, strong) EOCEmployer *employer;

@end

将引入的头文件时机尽量延后,只在确有需要时才引入,这样就可以减少类的使用者所需引入的头文件数量, 减少编译时间。

# import "ViewController.h"
# import "EOCPerson.h"
# import "EOCEmployer.h"

@interface ViewController()

@end

@implementation

- (void)viewDidload {
     [super viewDidload];
     EOCPerson *person = [EOCPerson new];
     EOCEmployer *employer = [EOCEmployer new];
     employer.name = "Coder"; // 如果没有 # import "EOCEmployer.h"会编译报错
     person.employer = employer;
}

@end

  • 声明类遵循某个协议时,最好把协议单独放在一个头文件中,再来导入。然而有些协议,例如“委托协议”(delegate protocol),就不用单独写一个头文件了。在那种情况下,协议只有与接收协议委托的类放在一起定义才有意义。

第3条:多用字面量语法,少用与之等价的方法

  • 字面量语法(literal syntax)实际上只是一种“语法糖”(syntactic sugar),例如普通的创建NSNumber对象是:NSNumber *someNumber = [NSNumber numberWithInt:1];, 字面量的语法是NSNumber *someNumber = @1;
  • 应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
  • 应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
  • 用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此务必确保值里不含nil。
  • 字面量语法的局限性:除了字符串之外,所创建出来的对象必须属于Foundation框架才行。如果定义了这些类的子类,则无法用字面量创建对象。而且使用字面量语法创建的字符串、数组、字典对象都是不可变的(immutable)。

第4条:多用类型常量,少用#define预处理指令

  • 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量不一致。
  • 在实现文件中使用static const来定义“只在编译单元内可见的常量”(translation-unit-specitfic constant)。另外,在Objective-C的语境下,“编译单元”一词通常指每个类的实现文件(以.m为后缀名)。通常命名时以"k"作为前缀,然后驼峰命名。
static const NSTimeInterval kAnimationDuration = 0.3
  • 在头文件中使用extern来声明全局变量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名加前缀。Objective-C没有“名称空间”(namespace)这一概念,以类名作为前缀,是为了避免命名冲突。例如UIKit就按照这种方式来声明用作通知名称的全局常量。其中有类似UIApplicationDidEnterBackgroundNotification与UIApplicationWillEnerForegroundNotification这样的常量名。
// EOCLoginManager.h
#import <Foundation/Foundation.h>

// 此常量需放在“全局符号表”(global symbol table)中,以便可以在定义该常量的编译单元之外使用。
extern NSString *const EOCLoginManagerDidLoginNotification;

@interface EOCLoginManager : NSObject
- (void)login;
@end

// EOCLoginManager.m
#import "EOCLoginManager.h"

NSString *const EOCLoginManagerDidLoginNotification = @"EOCLoginManagerDidLoginNotification";

@implementation EOCLoginManager

@end

第5条:用枚举表示状态、选项、状态码

  • 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
  • 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将个选项值定义为2的幂,以便通过按位或操作将其组合起来。
  • 用NS_ENUM与NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。
  • 在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch 语句并未处理所有枚举。

===================================

第二章 对象、消息、运行期

===================================

第6条:理解“属性”这一概念

  • “属性”(property)是Objective-C的一项特性,用于封装对象中的数据。利用@property声明属性,可以快速方便得为实例变量创建存取器set和get方法。@property还允许我们用点语法使用存取器。
@interface Person : NSObject

@property NSString *name;

@end
  • @synthesize 的作用:是为属性添加一个实例变量名,或者说别名。同时会为该属性生成 setter/getter 方法。
/*
_name是实例变量,name是属性。告诉编译器name属性为_name实例变量生成setter and getter方法的实现
*/
@synthesize name = _name
  • 属性特质
@property (nonatomic, readonly, assign, getter=isOpen) BOOL open;

  1. 【原子性】默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备nonatomic特质,则不使用同步锁。
  2. 【读、写权限】a.具备readwirte特质的属性拥有“获取方法(getter)“与“设置方法(setter)”。若该属性由@synthesize实现,则编译器会自动生成这两个方法。b.具备readonly特质的属性仅拥有获取方法,只有当该属性有@synthesize实现时,编译器才会为其合成获取方法。你可以用此特质把某个属性对外公开为只读属性,然后在"class-continuation分类"中将其重新定义为读写属性。
  3. 【内存管理语义】
    3.1 assign "设置方法"只会执行针对“纯量类型”(scalar type,例如CGFloat或NSInteger等)的简单赋值操作。
    3.2 strong 此特质表明该属性定义了一种“拥有关系”(owning relationship)。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去
    3.3 weak 表明了一种“非拥有关系”(nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似,然后在属性所指的对象遭到摧毁时,属性值也会清空。
    3.4 unsafe_unretained 语义和assign相同,但是它适用于“对象类型”,该特质表达一种“非拥有关系”(“不保留”,unretained),当目标对象遭到摧毁时,属性值不会自动清空(“不安全”,unsafe),这一点与weak有区别。
    3.5 copy 此特质所表达的属性关系与strong类似。然而设置方法并不保留新值,而是将其"拷贝"(copy)。
  4. 【方法名】
    • getter=<name> 指定“获取方法”的方法名。
    • setter=<name> 指定“设置方法”的方法名。
  • 在设置属性对应的实例变量时,一定要遵从该属性所声明的语义。
- (id)initWithzName: (NSString *)name {
    if (self = [super init]) {
        _name = [name copy];
    }
    return self;
}
  • 开发iOS程序时应该使用nonatomic,因为atomic属性会严重影响性能(而且atomic并不能保证线程安全,若要实现“线程安全”的操作,还需要更为深层的锁定机制才行)。

第7条:在对象内部尽量直接访问实例变量

  • 在对象内部读取数据时,应该直接通过实例变量来读(不经过Objective-C“方法派发”,访问实例变量速读比较快),而写入数据时,则应通过属性来写。
  • 在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据。
  • 有时会使用惰性初始化(lazy initialization)技术配置某份数据,这种情况下,需要通过属性来读取数据。

第8条:理解“对象等同性”这一概念

  • 若想检测对象的等同性,请提供“isEqual”与hash方法。
  • 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
  • 不要盲目地逐个检测每条属性,而是应该按照具体需求来指定检测方案。
  • 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
    例如
- (NSUInteger) hash {
    NSUInteger firstNameHash = [_firstName hash];
    NSUInteger lastNameHash = [_lastName hash];
    NSUInteger ageHash = _age;
    return firstNameHash ^ lastNameHash ^ ageHash;
}

第9条:以“类族模式”隐藏实现细节

  • “类族(class cluster)”是一种很有用的模式(pattern),可以隐藏“抽象基类”(abstract base class)背后的实现细节。Objective-C的系统框架中普遍使用此模式。比如UIKit的UIButton类。创建按钮,需要调用类方法:+ (UIButton *)buttonWithType: (UIButtonType)type;。还有,NSnumber类也是类族。
  • 大部分collection类都是类族,例如NSArray与其可变版本NSMutableArray。
  • 子类应该继承自类族中的抽象基类。(可惜Objective-C这门语言没办法指明某个基类是“抽象的”,通常是在文档中写明)。
  • 子类应该定义自己的数据存储方式。
    开发者编写NSArray子类时,经常在这个问题上受阻。子类必须用一个实例变量来存放数组中的对象。这似乎与大家预想的不同,我们以为NSArray自己肯定会保存那些对象,所以在子类中就无须再保存一份了。但是大家要记住,NSArray本身只不过是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都具备的一些接口。对于这个自定义的数组子类来说,可以用NSArray来保存其实例。
  • 子类应该覆写超类文档中指明需要覆写的方法。
  • 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。

第10条:在既有类中使用关联对象存放自定义数据

  • “关联对象”, 可以给某个对象关联许多其他对象,这些对象通过来“键”来区分。存储对象的时候,可以指明“存储策略”(storage policy),用以维护相应的“内存管理语义”。存储策略由名为objc_AssociationPolicy的枚举所定义:
关联类型 等效的@property属性
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, retain
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy
OBJC_ASSOCIATION_RETAIN retain
OBJC_ASSOCIATION_COPY copy
  • 通过下列方法可以管理关联对象:
    1. void objc_setAssociateObject(id object, void *key, id value, objc_AssociationPolicy policy)
      此方法以给定的键和策略为某对象设置关联对象
    2. id objc_getAssociateObject(id object, void *key)
      此方法根据给定的键从某对象中获取相应的关联对象值。
    3. void objc_removeAssociatedObjects(id object)
      此方法移除指定对象的全部关联对象。
  • 在设置关联对象时,若想令两个键匹配到同一个值,则二者必须是完全相同的指针才行。鉴于此,在设置关联对象值时,通常使用静态全局变量做键。
  • 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug。

第11条:理解objc_msgSend的作用

  • 给某个对象“调用方法”(call a method),相当于给某个对象“发送消息”(invoke a message)。
  • 给对象发送消息可以这样写:id returnValue = [someObject messageName:paramter];。someObject叫做“接收者”(receiver),messagename叫做“选择子”(seletor)。选择子与参数合起来称为“消息”(message)。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,其“原型”: void objc_msgSend(id self, SEL cmd, ...)
  • 消息传递机制:objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”(list of methods),如果能找到与选择子名称相符的方法,就跳转至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”(message forwarding)操作。
  • 消息传递机制:

第12条: 理解消息转发机制

消息转发分为两大阶段。

  • 第一阶段:动态方法解析:先征询接收者,所属的类,看是否能动态添加方法,以处理当前这个“未知的选择子”(unknown selector)。这个过程是调用类方法: + (BOOL)resolveInstanceMethod: (SEL)selector (如果需要新增方法,就要在这个方法里面进行处理并返回YES)
  • 第二阶段:第一阶段执行完后,接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。这时,运行期系统会请求接收者以其他手段来处理消息相关的方法调用。分为两小步:
    1. 寻找备援接收者。调用- (id)forwardingTargetForSelector:(SEL)selector,运行期系统会问它:能不能把这条消息转给其他接收者来处理。在一个对象内部,可能还有一系列其他对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,好像是该对象亲自处理了这些消息似的。
    2. 完整的消息转发。到这一步,系统会调用- (void)forwardInvocation: (NSInvocation *)invocation,其中invocation对象携带了那条尚未处理的消息有关的全部细节。在这里将消息指派给目标对象,然而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以比较少用。
      消息转发全流程

第13条:用“方法调配技术”调试“黑盒方法”

  • 类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式表示,这种指针叫做IMP。
  • 方法调配(method swizzing):在运行期,向类中新增或替换选择子对应的方法实现,常用来向原有实现中添加新功能。

第14条:理解“类对象”的用意

  • Objective-C对象本质是一个结构体,该对象结构体的首个成员是Class类的变量。
typedef struct objc_object {
     Class isa;
} *id;
  • Class对象也是一个结构体,此结构体存放类的“元数据”(metadata),首个变量也是isa指针,这说明Class本身亦为Objective-C对象。结构体里还有个变量叫做super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做“元类”(metaclass),用来表述类对象本身所具备的元数据。“类方法”就定义与此处,因为这些方法可以理解成类对象的实例方法。每个Class仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。
typedef struct objc_class *Class;
struct objc_class {
     Class isa;
     Class super_class;
     const char *name;
     long version;
     long info;
     struct objc_ivar_list *ivars;   
     struct objc_method_list **methodLists;
     struct objc_cache *cache;
     struct objc_protocol_list *protocols;
}
继承体系图(假设有个名为SomeClass的子类从NSObject中继承而来)

super_class指针确立了继承关系,而isa指针描述了实例所属的类。通过这张布局关系图即可执行“类型信息查询”。

  • 可以用类型信息查询方法来检视类继承体系。"isMemberOfClass":能够判断出对象是否为某个特定类的实例,而"isKindOfClass":则能够判断出对象是否为某类或其派生类的实例。
  • 如果对象类型无法在编译器确定,那么就应该使用类型信息查询方法来探知。
  • 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了转发功能。

===================================

第三章 接口与API设计

===================================

第15条:用前缀避免命名空间冲突

  • Objective-C没有其他语言那种内置的命名空间(namespace)机制。所以为了避免命名冲突的唯一办法就是变相实现命名空间:为所有名称都加上适当前缀。Apple宣称其保留使用所有"两字母前缀"(two-letter prefix)的权利; 可以选择与你的公司、应用程序或二者皆有关联之名称作为类名的前缀。

第16条:提供“全能初始化方法”

  • “全能初始化方法”(designated initializer):可为对象提供必要信息以便其能完成工作的初始化方法。例如:
- (id)init
- (id)initWithString:(NSString *)string
- (id)initWithTimeIntervalSingNow:(NSTimeInterval)seconds
- (id)initWithTimeInterval:(NSTimeInterval)seconds
                       sinceDate:(NSDate *)refDate
- (id)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)seconds
- (id)initWithTimeIntervalSince1970:(NSTimeInterval)seconds

其中的- (id)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)seconds是全能初始化方法。也就是,其余的初始化方法都要调用它。于是,只有在全能初始化方法中,才会存储内部数据。

  • 在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法。
  • 若全能初始化方法与超类不同,则需覆写超类中的对应方法。
  • 如果超类的初始化方法不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。

第17条:实现description方法

  • 实现description方法返回一个有意义的字符串,用以描述该实例
  • 若想在调试时打印更详尽的对象描述信息,则应实现。debugDescripiton方法(在NSObject类的默认实现中,此方法只是直接调用了description)。

第18条:尽量使用不可变对象

  • 尽量创建不可变的对象。
  • 若某属性仅可于对象内部修改,则在“class-continuation分类”中将readonly属性扩展为readwrite属性。
  • 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的可变collection。

第19条:使用清晰而协调的命名方式

  • 方法名要言简意赅,从左至右读起来更像个日常用语中的句子才行好。
  • 方法命名:
    1. 如果方法的返回值是新创建的,那么方法的首个词应是返回值的类型,除非前面还有修饰语,例如localizedString。属性的存取方法不遵守这种命名方式,因为一般认为这些方法不会创建新对象,即便有时返回内部对象的一份拷贝,我们也认为那相当于原有的对象。这些存取方法应该按照其所对应的属性来命名。
    2. 应该把表示参数类型的名词放在参数前面。
    3. 如果方法要在当前对象上执行操作,那么应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个名词。
    4. 不要使用str这种简称,应该用string这样的全称。
    5. Boolean属性应加is前缀。如果某方法返回非属性的Boolean值,那么应该根据其功能,选用has或is当前缀。
    6. 将get这个前缀留给那些由“输出参数”(out-parameter)来保存返回值的方法,比如说,把返回值填充到“C语言式数组”(C-style array)里的那种方法就可以使用这个词做前缀。
    - (void)getCharacter:(unichar *)buffer range:(NSRange)aRange
    
  • 类与协议的命名:如果要从其他框架中继承子类,那么务必遵守其命名惯例。比方说,要从UIView类中继承自定义的子类,那么类名 末尾的词必须是view。同理,若要创建自定义的委托协议,则其名称中应该包含委托发起方的名称,后面再跟上Delegate一词。

第20条:为私有方法名加前缀

  • 把私有方法标出来,这样很容易就能看出哪些方法可以随意修改,哪些不应轻易改动。
  • 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开。但是不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的(Apple文档),可以考虑用“p_”做前缀,或者用基于公司或者项目的“xx_”形式。

第21条:理解Objective-C错误模型

  • Objective-C在默认情况下不是“异常安全的”(exception safe)。具体来说,这意味着:如果抛出异常,那么本应在作用域末尾释放的对象现在却不会自动释放了。
  • 如果想生成“异常安全”的代码,可以通过设置编译器的标志来实现,不过这将引入一些额外代码,在不抛出异常时,也照样执行这部分代码。需要打开的编译器标志叫做 -fobjc-arc-exceptions。换句话说在ARC中异常可能会导致对象的内存泄露。如果开启了该选项,则ARC会额外为异常中的对象申请和释放操作添加代码,保证异常中ARC管理的对象也不会造成内存泄露。当然这样一来缺点就是可能会生成大量平常可能根本用不到的代码。(只有发生异常才会执行)
  • 在出现非致命错误(nonfatal error)时,Objective-C语言所用的编程范式为:令方法返回nil/0,或是使用NSError,以表明其中 有错误发生。
  • NSError对象里封装了三条信息:
    1. Error domain(错误范围,其类型为字符串)。也就是产生错误的根源,通常用一个特有的全局变量来定义。
    2. Error code(错误码,其类型为整数)独有的错误代码,用以指明在某个范围内具体发生了何种错误。
    3. User info(用户信息,其类型为字典)有关此错误的额外信息,其中或许包含一段“本地化的描述”(localized description)。
  • 在处理错误时,除了可以指派“委托方法”(delegate method)来处理错误,也可以把错误信息放在NSError对象里,经由“输出参数”返回给调用者。
NSError *error = nil;
Bool ret = [object doSomething: &error];
if (error) {
     // There was an error
}

第22条:理解NSCopying协议

  • 若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。
  • 如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying与NSMutableCopying协议。
  • 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝。
  • 如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。

===================================

第四章 协议与分类

===================================

第23条:通过委托与数据源协议进行对象间通信

  • 委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象。
  • 将委托对象应该支持的接口定义成协议,在协议中把可能需要处理的事件定义成方法。
  • 当某对象需要从另外一个对象中获取数据时,可以使用委托模式。这种情境下,该模式亦成“数据源协议”(data source protocal)。
  • 若有必要,可实现含有位段的结构体(位段结构既能够节省空间,又方便于操作),将委托对象是否能相应相关协议方法这一信息缓存至其中。
    struct {
        unsigned int didReceiveData: 1; 
        unsigned int didFailWithError: 1; 
        unsigned int didUpdateProgressTo: 1; 
    } _delegateFlags;
 

第24条:将类的实现代码分散到便于管理的数个分类之中

  • 使用分类把类的实现代码划分成易于管理的小块
  • 将应该视为“私有”的方法归入名叫Private的分类中,以隐藏实现细节

第25条:总是为第三方类的分类名称加前缀

  • 分类机制通常用于向无源码的既有分类中新增功能。
  • 向第三方类中添加分类时,总应给其名称加上你专用的前缀。例如NSString分类,NSString(ABC_HTTP)
  • 向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀。例如:- (NSString *)abc_urlDecodedString;

第26条:勿在分类中声明属性

  • 属性是用来封装数据的,所要表达的意思是,类中有数据在支撑着它。
  • 在"class-continuation分类"之外的其他分类中,可以定义存取方法(或者readonly属性),但尽量不要定义属性。可以把封装数据所用的全部属性定义在主接口中。

第27条:使用"class-continuation分类"隐藏实现细节

  • 通过"class-continuation分类"向类中新增实例变量。
  • 如果属性在主接口中声明为“只读”,而类的内部又要用设置方法修改此属性,那么就在"class-continuation分类"中将其扩展为“可读写”。
  • 把私有方法的原型声明在"class-continuation分类"里面。
  • 若想使类所遵守的协议不为人所知,则可于"class-continuation分类"中声明。

第28条:通过协议提供匿名对象

  • 协议可在某种程度上提供匿名类型。具体的对象类型可以淡化遵从某协议的id类型,协议里规定了对象所应实现的方法。
- (id<EOCDatabaseConnection>)connectionWithIdentifiler:(NSString *)indentifier;
  • 使用匿名对象来隐藏类型名称(或类名)。
  • 如果类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。

===================================

第五章 内存管理

===================================

第29条:理解引用计数

  • Objective-C语言使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器。如果想使某个对象继续存活,那就递增其引用计数;用完了之后,就递减其计数。计数变为0名,就表示没人关注此对象了,于是,就可以把它销毁。
  • 在ARC工程中使用MRC: 在Project的Build Phase里面的Compile Source找到需要特殊处理的文件,加上编译选项(Compiler Flags),在文件后用-fno-objc-arc修饰就行了。
  • “悬挂指针”:为了避免在不经意间使用了无效对象,一般调用完release之后都清空指针。
NSNumber *number = [[NSNumber alloc] initWithInt:1337];
[array addObject: number];
[number release];
number = nil;
  • 属性存取方法中的内存管理。若属性为"strong关系"(strong relationship),则设置的属性会保留。
- (void)setFoo:(id)foo {
  [foo retain];
  [_foo release];
  _foo = foo;
}

此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要!

  • 自动释放池。在Objective-C的引用计数架构中,自动释放池是一项重要特性。调用release会立刻递减对象的保留计数(而且还可能令系统回收对象),然而有时候可以不调用它,改为调用autorelease,此方法会在稍后递减计数,通常是在下一次“事件循环”(event loop时递减),除非你有自己的自动释放池。
- (NSString *)stringValue {
     NSString *str = [[NSString alloc] initWithFormat:@"I am this: %@", self];
    return [str autorelease];
}
  • 通常采用“弱引用”(weak refefence)来打破保留环(retain cycle)。

第30条:以ARC简化引用计数

  • ARC环境下,引用计数实际上还是要执行的,只不过保留与释放操作现在是由ARC自动为你添加。ARC会自动执行retain、release、autorelease等操作。因为ARC自动调用这些方法时,并不通过普的通Objective-C消息派发机制,而是直接调用其底层C语言版本,所以不能覆写这些方法,这些方法从来不会被直接调用。
  • 在ARC管理对象生命期的办法基本上就是:在合适的地方插入“保留”及“释放”操作。在ARC环境下,变量的内存管理语义可以通过修饰符(_ _strong 、_ _weak、_ _unsafe_unretained、_ _atutoreleasing)指明,而原来则需要手工执行“保留”及“释放”操作。
  • ARC只负责管理Objective-C对象的内存。尤其要注意:CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease

第31条:在dealloc方法中只释放引用并解除监听

  • 在dealloc方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观测”(KVO)或NSNotificationCenter等通知,不要做其他事情。
  • 如果对象持有文件描述等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用close方法。
  • 执行异步任务的方法不应在dealloc里调用;只能在正常状态下执行的那些方法也不应在dealloc里调用,因为此时对象已处于正在回收的状态了。

第32条:编写“异常安全代码”时留意内存管理问题

  • 应用程序因异常状况而终止时才抛出异常,因此,如果应用程序即将终止,那么是否还会发生内存泄漏就已经无关紧要了。 但如果捕获异常,那么一定要注意将try块内所创立的对象清理干净。
@try {
     EOCSomeClass *object = [[EOCSomeClass alloc] init];
     [object doSomethingThatMayThrow];
}
@catch (..) {
      NSLog(@"Whoops, there was an error. Oh well..");
}

上面代码情况是会发生内存泄漏的。

  • 在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志(-fobjc-arc-exceptions)后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。不过有种情况编译器会自动把-fobjc-arc-exceptions标志打开,就是处于Objective-C++模式时。因为C++处理异常所用的代码与ARC实现的附加代码类似,所以令ARC加入自己的代码以安全处理异常,其性能损失并不太大。

第33条:以弱引用避免保留环

  • 将某些引用设为weak,可避免出现“保留环”。
  • weak引用可以自动清空,也可以不自动清空。自动清空(autonilling)是随着ARC而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。

第34条: 以“自动释放池块”降低内存峰值

  • 释放对象由两种方式:一种是调用release方法,使其保留计数立即递减;另一种是调用autorelease方法,将其加入“自动释放池”中。自动释放池用于存放那些需要在稍后某个时刻释放的对象。清空(drain)自动释放池时,系统会向其中的对象发送release消息。
  • 一般情况下无须担心自动释放池的创建问题。系统会自动创建一些线程,这些线程默认都有自动释放池,每次执行“事件循环”(event loop)时,就会将其清空。通常只有一个地方需要创建自动释放池,那就是在main函数里,我们用自动释放池来包裹应用程序的主入口点(main application entry point)。
  • 内存峰值(high-memory waterline)是指应用程序在某个特定时段内的最大内存用量(highest memory footproint)。
  • 自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里。
  • @autoreleasepool这种新式写法能创建出更为轻便的自动释放池。

第35条:用“僵尸对象”调试内存管理问题

  • 向已回收的对象发送消息是不安全的。这么做有时可以,有时不可以,可以与否,取决于对象所占内存有没有为其他内容所覆写。而这块内存有没有移作他用,无法确定,因此应用程序只是偶尔崩溃。在没有崩溃的情况下,可能那块内存只复用了一部分,所以对象的某些二进制数据依然有效。还有另外一种可能,就是那块内存恰好为另外一个有效且存活的对象所占据。在这种情况下,运行期系统会把消息发到新对象那里,而此对象也许能应答,也许不能。
  • Xcode的“僵尸对象”(Zombie Object)调试功能打开时,运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”,而不会真正回收它们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。给僵尸对象发送消息后,控制台会打印消息,应用程序则会终止。打印消息就像这样:
*** -[CFString respondsToSelector:]: message sent to deallocated instance 0x7ff9e9c0808e0
  • 系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量NSZombieEnabled可开启此功能。
  • 系统会修改对象的isa指针,令其指向特殊的僵尸类(例如由原来的EOCClass变为_NSZombile_EOCClass),从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。

第36条:不要使用retainCount

  • retainCount的方法之所以无用,其首要原因在于:它所返回的保留计数只是某个给定时间点上的值。该方法并未考虑到系统稍后会把自动释放池清空(参见第34条),因而不会将后续的释放操作从返回值里减去,这样的话,此值就未必能真实反应实际的保留计数了。
  • 引入ARC之后,retainCount方法就正式废止了,在ARC下调用该方法会导致编译器报错。

===================================

第六章 块与大中枢派发

===================================

第37条:理解“块”这一概念

  • 块其实就是个值,而且自有其相关类型。与int、float或Objective-C对象一样,也可以把块赋给变量,然后像使用其他变量那样使用它。块类型的语法与函数指针近似。
// 语法结构
return_type (^block_name)(parameters)

// 初始化赋值
int (^addBlock)(int a, int b) = ^(int a, int b) {
    return a + b;
}
  • 默认情况下,为块所捕获的变量,是不可以在块里修改的, 需要在声明变量的时候加上_ _block修饰符才可以。
  • 块本身可视为对象,有引用计数。当最后一个指向块的引用移走之后,块就回收了。回收时也会释放块所捕获的变量,以便平衡捕获时所执行的保留操作。
  • 块的内部结构:


    块对象的内存布局

    块存放在块对象的内存区域中,首个变量是指向Class对象的指针,该指针叫做isa。其余内存里含有块对象正常运转所需的各种信息。
    在内存中 最重要的就是invoke变量,这个函数指针,指向块的实现代码。函数原型至少要接受一个void *型的参数,此参数代表块。descriptor变量是指向结构体的指针,每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了copy与dispose这两个辅助函数所对应的函数指针。辅助函数在拷贝及丢弃块对象时运行,其中会执行一些操作,比如说,前者要保留捕获的对象,而后者则将之释放。
    块还会把它所捕获的所有变量都拷贝一份。这些拷贝放在descriptor变量后面,捕获了多少个变量,就要占据多少内存空间。请注意,拷贝的并不是对象本身,而是指向这些对象的指针变量。invoke函数为何需要把块对象作为参数传进来呢?原因在于,执行块时,要从内存中把这些捕获到的变量读出来。

  • 块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的Objective-C对象一样,具备引用计数了。

第38条:为常用的块类型创建typedef

  • 以typedef重新定义块类型,可令块变量用起来更加简单。
  • 定义新类型时应遵守现有的命名习惯,勿使其名称与别的类型相冲突。
  • 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应typedef中的块签名即可,无须改动其他typedef。

第39条:用handler块降低代码分散程度

  • 在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明。
  • 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用handler块来实现,则可直接将块与相关对象放在一起。
  • 设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。

第40条:用块引用其所属对象时不要出现保留环

  • 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。
  • 一定要找个适当的时机解除保留环,而不能把责任推给API的调用者。

第41条:多用派发队列,少用同步锁

  • 滥用“同步块”@synchronized(self)会降低代码效率,因为共用同一个锁的那些同步块,都必须按顺序执行。若是在self对象上频繁加锁,那么程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码。
  • 使用NSLock对象,在极端情况下,同步块会导致死锁,另外效率也不见得很高,而如果直接使用锁对象的话,一旦遇到死锁,就会非常麻烦。
  • 派发队列可用来表述同步语义(synchronization semantic),这种做法要比使用@synchronized块或NSLock对象更简单。
  • 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程。
  • 使用同步队列及栅栏块,可以令同步行为更加高效。

第42条:多用GCD,少用performSelector系列方法

  • performSelector系列方法在内存管理方面容易疏失。它无法确定将要执行的选择子具体是什么,返回什么,因而ARC编译器也就无法插入适当的内存管理方法。
  • persormSelector系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受到限制。
  • 如果想把任务放在另一个线程上执行,那么最好不要用performSelector系列方法,而是应该把任务封装在到块里,然后调用大中枢派发机制的相关方法来实现。

第43条:掌握CGD及操作队列的使用时机

  • 在解决多线程与任务管理问题时,派发队列并非唯一方案。
  • 操作队列(NSOperationQueue)提供了一套高级Objective-C API,能实现纯GCD所具备的绝大部分功能,而且还能完成一些更为复杂的操作,那些操作若改用GCD来实现,则需另外编写代码。

第44条:通过Dispatch Group机制,根据系统资源状况来执行任务

  • 一些列任务可归入一个dispatch group之中。开发者可以在这组任务执行完毕时获得通知。
  • 通过dispatch group,可以在并发式派发队列里同时执行多项任务。此时GCD会根据系统资源状况来调度这些并发执行的任务。开发者若自己来实现功能,则需编写大量代码。

第45条:使用dispatch_once来执行只需运行一次的线程安全代码

  • 经常需要编写“只需执行一次的线程安全代码”(thread-safe single-code execution)。通过GCD所提供的dispatch_once函数,很容易就能实现此功能。
  • 标记应该声明在static或global作用域中,这样的话,在把只需执行一次的块传给dispatch_once函数时,传进去的标记也是相同的。

第46条:不要使用dispatch_get_current_queue(推荐看原文

  • dispatch_get_current_queue函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试只用。
  • 由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。
  • dispatch_get_current_queue函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用“队列特定数据”来解决。

===================================

第七章 系统框架

===================================

第47条:熟悉系统框架

  • 系统框架都是动态库
  • 在为Mac OS X或iOS系统开发“带图形界面的应用程序”(graphical application)时,会用到名为Cocoa的框架,在iOS上称为Cocoa Touch。其实Cocoa本身并不是框架,但是里面集成了一批创建应用程序时经常用到的框架。
  • 许多系统框架都可以直接使用。其中最重要的是Foundation与CoreFoundation,这两个框架提供了构建应用程序所需的许多核心功能。
  • 很多常见任务都能用框架来做,例如音频与视频处理、网络通信、数据管理等。
  • 请记住:用纯C写成的框架与用Objective-C写成的一样重要,若想成为优秀的Objective-C开发者,应该掌握C语言的核心概念。

第48条:多用块枚举,少用for循环

  • 遍历collection有四种方式。最基本的方法是for循环,其次是NSEnumerator遍历法及快速遍历法,最新、最先进的方式则是“块枚举法”。
  • “块枚举法”本身就能通过GCD来并发执行遍历操作,无须另行编写代码。而采用其他遍历方式则无法轻易实现这一点。
  • 若提前知道待遍历的collection含有何种对象,则应修改块签名,指出对象的具体类型。

第49条:对自定义其内存管理语义的collection使用无缝桥接

  • 通过无缝桥接技术,可以在Foundation框架中的Objective-C对象与CoreFoundation框架中的C语言数据结构之间来回转换。
  • 在CoreFoundation层面创建collection时,可以指定许多回调函数,这些函数表示此collection应如何处理其元素。然后,可运用无缝桥接技术,将其转换成具备特殊内存管理语义的Objective-C collection。

第50条:构建缓存时选用NSCache而非NSDictionary

  • 实现缓存时应选用NSCache而非NSDictionary对象。因为NSCache可以提供优雅的自动删减功能,而且是“线程安全的”,此外,它与字典不同,并不会拷贝键。
  • 可以给NSCache对象设置上限,用以限制缓存中的对象总个数及“总成本”,而这些尺度则定义了缓存删减其中对象的时机。但是绝对不要把这些尺度当成可靠的“硬限制”(hard limit),它们仅对NSCache起指导作用。
  • 将NSPurgeableData与NSCache搭配使用,可实现自动清除数据的功能,也就是说,当NSPurgeableData对象所占内存为系统所丢弃时,该对象自身也会从缓存中移除。
  • 如果缓存使用得当,那么应用程序的响应速度就能提高。只有那种“重新计算起来很费事的”数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据。

第51条:精简initialize与load的实现代码(推荐看原文)

  • 在加载阶段(通常指应用程序启动的时候),如果类实现了load方法,那么系统就会调用它。分类里也可以定义此方法,类的load方法要比分类中的先调用。与其他方法不同, load方法不参与覆写机制。
  • load方法中使用其他类是不安全的,因为不能却确定其他类是否已经加载好了。
  • 首次使用某个类之前,系统会向其发送initialize消息。由于此方法遵从普通的覆写规则,所以通常应该在里面判断当前初始化的是哪个类。
  • load与initialize方法都应该实现得精简一些,这有助于保持应用程序的响应能力,也能减少引入"依赖环"(interdependency cycle)的几率。
  • 无法在编译器设定的全局变量,可以放在initialize方法里初始化。

第52条:别忘了NSTimer会保留其目标对象(推荐看原文)

  • NSTimer 对象会保留其目标,直到计时器本身失效为止,调用invalidate方法可令计时器失效,另外,一次性的计时器在触发完任务之后也会失效。
  • 反复执行任务的计时器(repeating timer),很容易引入保留环,如果这种计时器的目标对象又保留了计时器本身,那肯定会导致保留环。这种环状保留关系,可能是直接发生的,也可能是通过对象图里的其他对象间接发生的。
  • 可以扩充NSTimer的功能,用“块”来打破保留环。不过,除非NSTimer将来在公共接口里提供功能,否则必须创建分类,将相关实现代码加入其中。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容