【iOS】Effective Objective-C 一时失言乱红尘 2022-08-21 04:12 108阅读 0赞 \[\]方法名长 令许多人觉得此语言较为冗长 但是是易读的。 1.了解OC语言的起源 OC使用消息结构 而非函数调用 OC由smallTalk演化而来,后者是消息型语言的鼻祖。 使用消息结构的语言 其运行时所应执行的代码由运行环境决定 而使用函数调用的语言 则由编译器决定。,如果范例代码中调用函数为多态的,。那么在运行时就要按照“虚方法表”来查找到底应该执行哪个函数实现。而采用消息结构的语言 无论函数是否多态,总是在运行时才会去查找索要执行的方法。实际上 编译器甚至不关心接收消息的对象是何种类型,接收消息的对象问题也要在运行时处理。其过程叫做“动态绑定”。(dynamic binding。) 普通在编译时确定的语言调用例子: Object o = new Object(); o.method();编译时就确定了方法名和方法类型 参数等,以Java例,编译成二进制字节码(.class文件)直接将方法拷贝入class文 运行时语言例 NSSet set = \[NSSet setWithInt:2\];其中alloc init等只有在运行时才会知道方法和方法名 参数 参数类型等。 Objective-C的重要工作都由运行时组件而非编译器来完成 使用Objective-C的面向对象特性需要的全部数据结构以及函数都在运行时组件里面。举例子 运行期组件中含有全部的内存管理办法,运行期组件本质上就是一种与开发者所编代码相连接的动态库。dynamic lib 其代码能把开发者编写的所有程序粘合起来,这样的话,只需要更新运行期组件,即可提升程序的性能。而那种许多工作都在编译时完成语言,如果需要获得类似的性能提升则要重新编译应用程序的代码。 NSString \*someString = @"the string"; 创建字符串对象 放在堆内存中,在栈中开辟内存空间存储该堆内存地址。 NSString \*somestring = @"the string"; NSString \*anotherString = someString; 只有一个NSString的实例,然而有两个变量指向这个实例,两个变量都是NSString类型。这说明当前“栈帧”里分配了两块内存,每块内存的大小都能容下一枚指针 在32位机上是32位/8=4字节,64/8=8字节。内存内存放字符串地址 在OC代码中有时候会遇到定义里面不含\*的变量,它们可能会使用“栈空间(stack space)”。这些变量所保存的不是Objective-C对象,比如 CoreGraphics框架中的CGRect就是个例子 CGRect framel frame.origin.x = 0.0f; frame.origin.y = 10.0f; frame.size.width = 100.0f; frame.size.height = 150.0f; CGrect是C结构体 其定义为 struct CGRect \{ CGPoint origin; CGSize size; \} typedef struct CGRect CGRect; * 整个系统框架都在使用这种结构体,因为如果改用Objective-C对象来做的话,性能会受到影响,与创建结构体相比,创建对象还需要额外的开销,例如分配和释放内存等。如果只需要保存int float double char等“非对象类型” 那么 使用CGRect这种结构体就可以了。 ” 问题:结构体在内存中是如何分配的?结构体出现的原因? 内存对齐。 2.在类的头文件中尽量少引入其他头文件 和C C++一样 OC也是用头文件 header file 与实现文件 来区分代码 用OC语言编写类 在编译一个使用了外部类的文件时,不需要知道类内部的细节,只需要知道一个类名就好。OC中提供了一个@class的修饰符 被称为“前向声明 forward declaring” 3.多用字面量语法,少用与之等价的方法 编写OC程序时,总会用到几个类,他们属于Fundation框架,虽然从技术上讲 不用Fundation框架也可写出OC代码 但是实际上却经常要用到这个框架。如NSString NSNumber NSArray NSDictionary OC以语法繁杂著称,事实的确这样,不过从OC1.0开始 有一种非常简单地方式能创建NSString对象 这就是字符串字面量。其语法如下 NSString \*someString = @"effective oc 2.0"; 如果不用 需要用alloc init来分配并初始化NSString对象。在版本较新的编译器中 也能用这种字面量语法来声明NSNumber NSArray NSDictionary等。使用字面量语法可以缩减源代码长度 使其更为易读。 字面数值 NSNumber \*someNumber - @1; NSNumber \* floatNumber = @2.5f; NSNumber \*doubleNumber = @YES; int x = 5; float y = 6.23f; NSNumber \*expressionNumber = @(x+y); 以字面量来表示数值十分有用 这样做可以令NSNumber对象变得非常整洁,因为声明中只包含数值,而没有多余的语法部分。 字面量数组 不适用字面量语法: NSArray \*animals = \[NSArray arrayWithObjects:@"cat",@"dog",@"mouse"\]; 而使用了之后: NSArray \* animals = @\[@"cat",@"dog",@"mouse",@"mine"\]; 上面做法不仅简单 而且利于操作数组 数组的常见操作取某个下表的对象,如果不用字面量 NSString \*dog = \[animal objectAtIndex:1\]; 如果使用字面量 则为: NSString \*dog = animal\[1\]; 不过 用字面量语法创建数组时要注意 如果数组对象中有nil 则会抛出异常,因为字面量语法实际上只是一种“ 语法糖” 其效果等于是先创建了一个数组,然后把方括号内的所有对象都加入数组中去。抛出的异常如下: \*\*\* Terminating app due to uncaught exception 'NSInvalidArgumentException',reason: '\[\_\_NSPlaceholderArray initWithObjects:count:\]'attemp to insert nil object from objects\[0\] 语法糖:计算机语言中与另外一套语法等效但是开发者用起来却很方便的语法,语法糖可以让程序更加易读,减少代码的出错几率。 在改用字面量语法来创建数组时就会遇到这个问题,下面这段代码分别以两种语法来创建数组 id object1 = ; id object2 = ; id object3 = ; NSArray \*arrayA = \[NSArray arrayWithObjects:object1,object2,object3\]; NSArray \*arrayB = @\[object1,object2,object3\]; 如果1,3都指向了有效的oc对象 而2是nil那么会出现什么情况?按照字面量语法创建数组B会抛出异常 A虽然能创建出对象 但是其中却只有一个对象 原因在于 arrayWithObjects方法会依次处理各个参数,直到发现nil为止。由于2为nil 所以该方法会提前结束。 这个微妙的差别表明 是用字面量语法更安全,抛出异常让程序终止运行 比创建好数组之后才发现元素个数少了要好。向数组中插入nil通常说明程序有错误,而通过异常可以更快发现错误。 字面量字典 “字典”Dictionary是一种映射型数据结构 可以向其中添加键值对,与数组一样 OC代码也经常用到字典,其创建方式如下 NSDictionary \*d = \[NSDicitonary dictionaryWithObjectsAndkeys:@"a",@"1",@"b",@"2",@"c",@"3"\]; 这样写令人困惑,因为顺序为对象,键值 对象 键值 这个与通常理解的顺序相反。我们一般认为是把键映射到对象 因此不好读。 字面量语法改写:NSDictionary \*d = @\{@"1":@"a",@"2":@"b"\}; 上面这种写法更简明,而且键出现在对象之前 理解起来比较顺畅,此范例代码还说明使用字面量数值的好处 字典中的对象和键值必须都是oc对象,所以不能把整数28直接放入 而要封装在NSNumber实例中 使用字面量语法很容易,只需要在数字前加@就可以啦。 与数组一样,一旦对象nil,抛出异常。 可变数组和字典 通过取下标操作,可以访问数组中某个下标或者字典中某个键值所对应的元素 如果数组和字典对象是可变的,那么也能通过下标来修改其中的元素值。修改可变数组和字典内容的标准做法是 \[mutableArray replaceObjectAtIndex 1 withObject:@"dog"\] \[mutableDictionary setObject:@"ddd" forKey:@"lastName"\] 如果换用下标操作来写 则为 mutableArray\[1\] = @"dog" mutableDictionary\[@"name"\] = @"dkdkkd"; 局限性 字面量语法有一个限制 就是除了字符串之外 所创建出来的对象必须属于Fundation框架才行 如果自定义了这些类的子类,则无法用字面量语法创建其对象 要想创建自定义子类的实例,必须采用“非字面量语法” 然而由于NSArray NSDictionary NSNumber都是业已成型的“子族” 因此很少有人会从中自定义子类。真要那么做会比较麻烦。而且一般来说,标准实现已经很好了 无需再改动。创建字符串时可以使用自定义的子类 然而必须要修改编译器的选项才行。除非你明白后果 否则不鼓励子类化。 使用字面量语法创建出来的字符串等对象不可变,如果想要可变版本对象 则需要复制一份。 NSMutableArray \* array = \[@\[@1,@2,@3\] mutableCopy\]; 这么做会调用一个方法 而且还要创建一个对象 不过使用字面量语法带来好处还是多的。 第四条 多用类型常量 少用\#define 预处理指令 编写代码时经常要定义常量 例如 要写一个UI视图类 此类图显示出来之后就播放动画然后消失 可能想把动画的时间提取为常量 掌握了OC和其语言基础的人 也许会这么做 \#define ANIMATION\_DURATION 0.3 上述预处理指令会将源代码中字符串替换成0.3 这样的话 假设此指令声明在某个头文件中,那么所有引入了这个头文件的代码 其该变量均会被替换。 要想解决问题,应该设法利用编译器的某些特性 有个办法比用预处理指令来定义常量更好,比如 下面这行代码定义了一个类型为NSTimerInterval常量 static const NSTimeInterval kAnimationDuration = 0.3; 用这个方法定义常量包含类型信息,其好处是清除地描述常量的含义,由此可知 该变量类型为NSTimeInterval如果要定义许多常量 那么这种方式能令后来的人更容易理解意图。 # OC中的extern,static,const # ## const的作用: ## * const仅仅用来修饰右边的变量(基本数据变量p,指针变量\*p)。 * 被const修饰的变量是只读的。 ## static的作用: ## * 修饰局部变量: 1.延长局部变量的生命周期,程序结束才会销毁。 2.局部变量只会生成一份内存,只会初始化一次。 3.改变局部变量的作用域。 * 修饰全局变量 1.只能在本文件中访问,修改全局变量的作用域,生命周期不会改 2.避免重复定义全局变量 ## Java中final关键字的作用 ## 被final修饰的变量不可被修改 被final修饰的方法不可被重写 但是可被继承。 被final修饰的类不可被继承。 还需要注意常量名称,常用命名方法为 如果常量局限于某“编译单元”也就是实现文件中 则在前面加字母K 如果常量在类之外可见,则以类名为前缀。参见19条命名习惯。 定义常量的位置 定义常量的位置很重要 我们总喜欢在头文件中声明预处理命令,这么做很糟糕,当常量名称有可能互相冲突时更如此,例如,ANIMATION\_DURATION这个常量名就不该出现在头文件中,因为所有引入这份头文件的其他文件都会出现这个名字,其实就连static const 定义的常量也不应该出现在头文件中 因为OC没有命名空间,所以那么做相当于声明了一个名叫kAnimationDuration的全局变量,此名称应该加上前缀 以表明其所属的类,如可改为EOCViewClassAnimationDuration 如果不打算公开,则可以在.m文件的实现内写全局。 如: @implement ClassA : NSObject static const NSString \*name; @end 变量一定要同时用static const来声明 如果试图修改const修饰符所声明的变量 那么编译器就会报错。在本例中我们正是希望这样,因为动画播放时间为定值,所以不应该修改 而static修饰符则意味着该变量仅在定义此变量的编译单元中可见。编译器每收到一个编译单元就会输出一份“目标文件” 在OC的语境下 编译单元一词通常指每个类的实现文件(以.m为后缀名)因此 在上述代码中声明的kAnimationDuration变量其作用域仅仅限于EOCAnimatedView.m所生成的目标文件中。假如声明此变量时不加static 则编译器会为他创建一个“外部符号”(external symbol)。此时如果在另外一个编译单元也声明了同名变量那么编译器就会抛出一条错误信息, duplicate\_symbol \_kAnimationDuration in: EOCAnimatedView.o EOCAnimatedView.o 实际上如果一个变量既声明为static 又声明为const 那么编译器根本不会创建符号,而是会像\#define预处理指令一样 把所有的遇到变量都替换成定值。不过还是要记住 用这种方式定义的常量带有类型信息。 有时候需要对外公开某个常量 比方说 你可能需要在类代码中调用NSNotificationCenter以通知他人 用一个对象来派发通知 令其他想接受通知的对象向该对象注册 这样就能实现此功能了。派发通知时。需要使用字符串来表示此通知的名称 而这个名字就可以被声明为一个外界可见的常值变量。这样的话 注册者无需知道实际的字符串值 只需要以常值变量来注册自己想要接收的通知即可。 此类常量需要放在全局符号表中 以便可以在定义该常量的编译单元之外使用,因此 其定义方式与上例演示的static const不同 如下定义 //header file extern NSString \*const EOCStringConstant; //在实现.m文件中 NSString \* const EOCStringConstant = @"Value"; 该常量在头文件中声明 且在实现文件中定义 注意 const修饰符在常量类型中的位置 常量定义应该从右到左解读,所以 EOCStringConstant就是一个常量 而这个常量是指针,指向NSString对象 这与需求相符 我们不希望有人改变此指针常量 使其指向另外一个对象。 编译器看到头文件extern关键字 就明白如何在引入此头文件的代码中处理该常量了。这个关键字告诉编译器,在全局符号表中将会有一个名为EOCStringConstant的符号。也就是说 编译器无需查看其定义 即允许代码使用此常量。因为他知道 当连接成二进制文件之后 肯定能找到这个变量。 此类常量必须定义 而且只能定义一次 通常将其定义在与声明该常量的头文件相关的实现文件中。由实现文件成目标文件时 编译器会在数据段为字符串分配存储空间 链接器会把此目标文件与其他目标文件链接 以生成最终二进制文件 凡是用到EOCStringConstant符号的地方 链接器均可解析 因为符号要放在全局符号表中所以命名常量时需要谨慎,例如 某应用程序中有个处理登录操作的类 在登录完成后会发出通知 。派发通知时所用代码如下 \#import<FUndation/Fundation.h> extern NSString \*const EOCLoginManagerDidLoginNotification; @interface EOCLoginManager : NSObject \-(void)login; @end .m文件 \#import “EOCLoginManager.m” NSString \*const EOCLoginManagerDidLoginNotification = @"EOCLoginManagerDIdLoginNotification"; @implementation EOCLoginManager \-(void)login\{ 异步登录 然后调用didLogin \} \-(void)didLogin\{ \[NSNotificationCenter defaultCenter\] postNotificationName:EOCLoginManagerDidLoginNotification Object:nil\]; \} @end 注意常量的名字。为避免冲突 最好使用相关的类名做前缀 系统框架中一般都这么做。 例如 UIKit 就是按照这种方式来声明用作通知名称的全局变量。其中有类似UIApplicationDidEnterBackgroundNotification与UIApplicationWillEnterNotification这样的常量名。 其他类型的常量也是如此。假如要把潜力中 EOCAnimatedView类中动画播放时长对外公布 那么可以这样声明 .h extern const NSTimeInterval EOCAnimatedViewAnimationDuration; .m const NSTimeInterval EOCAnimatedViewAnimationDuration = 0.3; 这样定义优于使用\#define预处理指令 因为编译器会确保常量值不变。一旦在EOCAnimatedView.m中定义好 即可随处使用 而采用预处理指令所定义的常量 可能会无意中被修改 从而导致应用程序各部分所使用的值互不相同。 总之 不要使用预处理指令来定义常量 而应该借助编译器来确保常量正确 比方说后可以在实现文件中用static const来声明常量 也可以声明一些全局变量。 要点: 1.不要使用预处理指令定义常量 这样定义出来的常量不含类型信息 编译器只是会在变以前据此执行查找和替换操作,即使有人重新定义了常量值编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。 在实现文件中使用static const来定义“只在编译单元内可见的常量” 由于此类常量不在全局符号表中 所以无需为其名称前加前缀。 在头文件中使用extern来声明全局变量 并在相关实现文件中定义其值,这种常量要出现在全局符号表中,所以其名称应该加以分割 通常用与之相关的类名作为前缀, 第五条 用枚举表示状态 选项 状态码 由于OC是c的超集,所以C语言有的功能都有 其中一个就是枚举类型。enum.系统框架中频繁用到此类型,然而开发者容易忽视它。在以一系列常量来表示错误状态码或者可组合的选项时,极适合用枚举来命名。由于C++11标准扩充了枚举特性,所以最新版系统框架使用了强类型的枚举。没错 OC也得益于C++标准。 枚举是一种常量命名方式。某个对象所经历的各种状态就可以定义为一个简单地枚举集。比如 可以用下列枚举表示“套接字连接”的状态 enum EOCConnectionState\{ EOCConnectionStateDisconnected. EOCConnectionStateConnecting, EOCConnectionStateConnected, \}; 由于每种状态都用一个便于理解的值来表示 所以这样写出来的代码更容易读懂,编译器会为枚举分配一个独有的编号 0开始 每个枚举+1 枚举变量的定义 enum EOCConnectionState state = EOCConnectionStateDisconnected; 如果每次不写enum 而只需要写入EOCConnectionState 就好了 如果想这么做 则需要使用typedef关键字重新定义枚举类型。 enum EOCConnectionState\{ EOCConnectionStateDisconnected, EOCConnectionStateConnected, EOCConnectionStateConnected \}. typedef enum EOCConnectionState EOCConnectionState; 现在可以使用简写的EOCConnectionState来代替完整的enum EOCConnectionState了。 EOCConnectionState state = EOCConnectionStateDisconnected; C++11 指明 可以指明用何种 底层数据雷系个 来保存枚举类型的变量 这样做的好处是 可以向前声明枚举变量了,若不指定底层数据类型 则无法向前声明枚举类型,因为编译器不知道底层数据类型的大小 语法: enum EOCConnectionStateConnectionState : NSInteger\{\}; 上面这行代码确保枚举的底层数据类型NSInteger 还可以不使用编译器所分配的序号 enum EOCConnectionStateConnectionState\{ EOCConnectionStateDisconnected = 1, EOCConnectionStateConnecting, EOCConnectionStateConnected \}; 还有一种情况应该使用枚举 就是定义选项的时候。如果这些选项可以彼此组合 则更应该如此。只要枚举定义的对 各选项之间就可以通过“按位或操作符” 来组合。例如 iOS UI框架中有如下枚举类型 用来表示某个视图应该如何在水平或垂直方向上调整大小。 enum UIViewAutoresing \{ UIViewAutoresingNone =0. UIViewAutoresingFlexibleLeftMargin =1 <<0, UIViewAutoresingFlexibleWidth =1 <<1, UIViewAutoresingFlexibleRightMargin =1 <<2, UIViewAutoresingFlexibleTopMargin =1 <<3, UIViewAutoresingFlexibleHeight =1 <<4, UIViewAutoresingFlexibleBottomMargin=1<<5 \}; 每个选项都可以启动和禁用。使用上述方式来定义枚举值即可保证这一点。因为在每个枚举值所对应的二进制表示中只有一个二进制位是1 用“按位或操作符” 可组合多个选项 如 UIViewAutoResizingFlexibleWidth | UIViewAutoResizingFlexibleHeight 用 “按位与操作符”就可判断出是否已经启动某个选项。 enum UIViewAutoResizing resizing = UIViewAutoresizingFlexibleWidth | UIViewAutoresizinggFlexibleHeight; if(resizing & UIViewAutoresizingFlexibleWidth) \{\} 系统库中多次使用这个办法 iOS UI框架中UIKit还有个例子 用枚举值来告诉系统视图所支持的设备显示方向。这个枚举类型叫做UIInterfaceOrientationMask 开发者需要实现一个名为supportedInterfaceOrientations的方法 将视图所支持的显示方向告诉系统, \-(NSUInteger)supportedInterfaceOrientations\{ return UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft; \} Fundation框架中定义了一些辅助的宏 用这些宏来定义枚举类型时 也可以指定 用于保存枚举值的底层数据类型,这些宏具备向后兼容的能力,\#define语法定义的。其中一个用于定义像EOCConnectionState这种普通的枚举类型,另一个用于定义像UIViewAutoresizing这种包含一系列选项的枚举类型,其用法如下 typedef NS\_ENUM(NSUInteger,EOCConnectionState) \{ EOCConnectionStateDisconnected, EOCConnectionStateConnecting, EOCConnectionStateConnected, \}; typedef NS\_OPTIONS(NSUInteger,EOCPermittedDirection) \{ EOCPermittedDirectionUp =1 <<0, EOCPermittedDirectionDown =1 <<1, EOCPermittedDirectionLeft =1 <<2, EOCPermittedDIrectionRight =1 <<3 \}; 这些宏的定义如下 ![Center][] ![Image 1][] 由于需要分别处理不同情况 所以上述代码用多种方式来定义这两个宏。第一个\#if判断编译器是否支持新式枚举,其中所用逻辑看上去很复杂 不过其意思就是想判断编译器是否支持新特性。如果不支持 就用老式语法定义枚举。 typedef enum EOCConnectionState : NSUinteger EOCConnectionState; enum EOCConnectionState : NSUInteger \{ EOCConnectionStateDisconnected, EOCConnectionStateConnecting, EOCConnectionStateConnected \}; 要点: 1.应该用枚举表示状态机的状态 传递给方法的选项以及状态码等值。给这些值起个易懂的名字。 2.如果把传递给某个方法的选项表示为枚举类型 而多个选项又可以同时使用,那么就将各选项值定义为2的幂,以便通过按位或操作将其组合。 3.用NS\_ENUM与NS\_OPTIONS宏来定义枚举类型 并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。 4.在处理枚举类型的switch语句中不要实现default分支,这样的话 加入新的枚举之后 编译器就会提示开发者:switch语句并没有处理所有枚举。 [Center]: /images/20220731/e0684c2f7cc247f19bd9c6b986f2578e.png [Image 1]:
还没有评论,来说两句吧...