[编写高质量iOS代码的52个有效方法](二)对象

[编写高质量iOS代码的52个有效方法](二)对象

参考书籍:《Effective Objective-C 2.0》 【英】 Matt Galloway

先睹为快

6.理解“属性”这一概念
7.在对象内部尽量直接访问实例变量
8.理解“对象等同性”这一概念
9.以“类簇模式”隐藏实现细节
10.在既有类中使用关联对象存放自定义数据

目录

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

属性是Objective-C的一项特性,用于封装对象中的数据,编译器会自动编写访问属性所需的方法(getter方法和setter方法)。除此之外,编译器还会自动向类中添加适当类型的实例变量,并且在属性名前加下划线,以此作为实例变量的名字。也可以在类的实现代码里通过@synthesize语法来指定实例变量的名字:

// EOCPerson.h 
@interface EOCPerson : NSObject 
// 声明属性 
@property NSString *firstName; 
@property NSString *lastName; 
@end  
// EOCPerson.m 
@implementation EOCPerson 
// 通过@synthesize语法修改默认的实例变量名 
@synthesize firstName = _firstName; 
@synthesize lastName = _lastName; 
@end

若不想令编译器自动合成存取方法,则可以自己实现。还有一种方法是使用@dynamic关键字阻止编译器自动合成存取方法和实例变量。这样的话,就需要在运行期动态创建存取方法。(12条中会有具体示例)

// EOCPerson.h 
@interface EOCPerson : NSObject 
@property NSString *firstName; 
@property NSString *lastName; 
@end  
// EOCPerson.m 
@implementation EOCPerson 
// 通过@dynamic关键字阻止编译器自动合成存取方法和实例变量 
@dynamic firstName, lastName; 
@end

属性特质:
1. 原子性:默认情况下,由编译器合成的方法是atomic(原子的),如果指定了nonatomic(非原子的)特质,则不会在方法中使用同步锁。由于iOS中使用同步锁的开销较大,因此iOS程序一般都会使用nonatomic属性。
2. 读/写权限:默认情况下,属性为readwrite(读写)特质,@synthesize实现属性时会自动生成getter方法和setter方法。如果属性是readonly(只读)特质,则@synthesize实现属性时只会生成getter方法。
3. 内存管理语义
(1)assign 设置方法只会针对纯量类型(如CGFloat,NSInteger等)的简单赋值操作
(2)strong 属性定义为一种拥有关系,为这种属性设置新值时,设置方法会保留新值,并释放旧值,再将新值设置上去。
(4)unsafe_unretained 此特质的语义和assign相同,但它适用于对象类型,该特质也表达一种非拥有关系,当目标对象遭到摧毁时,属性值不会自动清空,这一点与weak有区别。
(5)copy 此特质所表达的所属关系与strong类似,然而设置方法并不保留新值,而是将其拷贝,多用来保护NSString* 类型属性的封装性。
4. 方法名 getter= 和 setter=分别用来指定存取方法的方法名,其中指定setter方法名不常见。

// 声明一个非原子性,只读,用assign方式管理内存,获取方法名为“isON”的布尔类型属性 
@property(nonatomic, readonly, assign, getter = isOn) Bool on;

在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义:

// EOCPerson.h 
@interface EOCPerson : NSObject 
// 声明属性 
@property (copy, readonly) NSString *firstName; 
@property (copy, readonly) NSString *lastName;  
-(id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName; 
@end 
 // EOCPerson.m 
-(id)initWithFirstName:(NSString *)firstName lastName:(NSString *)lastName{                 
    if((self = [super init])){         
        // 实现初始化方法时,遵循属性定义中的copy语义         
        _firstName = [firstName copy];         
        _lastName = [lastName copy];
    } 
} 
@end

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

在对象之外访问实例变量时,总是应该通过属性来做。在对象内部访问实例变量时,除了几种特殊情况之外,最好在读取实例变量时采用直接访问的形式,而在设置实例变量时通过属性来做:

// EOCPerson.h 
@interface EOCPerson : NSObject
@property (nonatomic, copy) NSString *firstName; 
@property (nonatomic, copy) NSString *lastName;  
-(NSString *)fullName; 
-(void)setFullName:(NSString *)fullName; 
@end  
// EOCPerson.m @implementation EOCPerson  
// 直接访问实例变量
-(NSString *)fullName{     
    return [NSString stringWithFormat:@"%@ %@", _firstName, _lastName]; 
}  
// 通过属性访问实例变量(存取方法) 
-(void)setFullName:(NSString *)fullName{
     NSArray *components = [fullName componentsSeparatedByString:@" "];
     self.firstName = [components objectAtIndex:0];
     self.lastName = [components objectAtIndex:1]; 
 } 
@end

两种写法的区别:
1. 直接访问实例变量速度比较快,因为不经过Objective-C的方法派发。
2. 直接访问实例变量时,不会调用其setter方法,这就绕过了为相关属性所定义的内存管理语义。
3. 如果直接访问实例变量,那么就不会触发KVO(键值观察)通知。
4. 通过属性来访问有助于排查与之相关的错误,因为可以给getter方法或setter方法中新增断点来进行监控。

综上:进行写操作时通过属性访问,进行读操作时直接访问实例变量。这种折中方案既能提高读取速度,又能控制对属性的写入操作。

特殊场景1:在初始化方法(init)中总是应该直接访问实例变量,因为子类可能会重写setter方法。只有一种特殊情况,待初始化的实例变量声明在超类中,而我们又无法在子类中直接访问此实例变量的话,那么就需要调用setter方法了。

特殊场景2:惰性初始化。这种情况下,必须通过getter方法来访问属性,否则,实例变量永远不会初始化。例如EOCPerson类中用到了一个用于表示人脑中信息的属性,这个属性创建成本较高且不常用,所以在getter方法中对其执行惰性初始化:

// brain属性的getter方法 
-(EOCBrain *)brain{
     if(!_brain){
          _brain = [EOCBrain new];
     }
     return _brain; 
}

若没有调用getter方法就直接访问实例变量,则会看到尚未设置好的brain,所以采用惰性初始化时,必须通过getter方法来访问实例变量。

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

根据等同性来比较对象是一个非常有用的功能。==操作符比较的是两个指针本身,并不是其所指的对象。应该使用NSObject协议中声明的isEqual:方法来判断两个对象的等同性:

NSString *foo = @"Badger 123"; 
NSString *bar = [NSString stringWithFormat:@"Badger %i",123]; 
Bool equalA = (foo == bar);  // equalA为NO 
Bool equalB = [foo isEqual:bar]; // equalB为YES 
Bool equalC = [foo isEqualToString:bar]; // equalC为YES

在确定比较对象为NSString类时,可以使用isEqualToString:方法,调用速度比isEqual:快,因为后者需要执行额外的步骤来确定受测对象的类型。 NSObject协议中有两个用于判断等同性的关键方法:

-(BOOL)isEqual:(id)object; 
-(NSUInteger)hash;

NSObject类对这两个方法的默认实现是,当且仅当其指针值完全相等时,这两个对象才相等。如果想正确地重写这两个方法,需要注意的是:如果isEqual方法判定两个对象相等,那么其hash方法也必须返回同一个值。但是如果两个对象的hash方法返回同一个值,那么isEqual方法未必认为两者相等。 下面是一个重写EOCPerson类,isEqual方法和hash方法的示例,EOCPerson类共有3个属性,如果两个EOCPerson对象的这3个属性都相等,则认为两个对象相等:

// EOCPerson.h 
@interface EOCPerson : NSObject 
@property (nonatomic, copy) NSString *firstName; 
@property (nonatomic, copy) NSString *lastName; 
@property (nonatomic, assign) NSInteger age; 
@end  
// EOCPerson.m 
@implementation EOCPerson 
-(BOOL)isEqual:(id)object{
     // 如果指针相等,则指向同一对象,所以相等     
     if(self == object) return YES;     
     // 如果不属于同一个类,则对象也必定不相等(不过有时可能认为:可以与子类对象相等)     
     if([self class] != [object class]) return NO;      
     EOCPerson *otherPerson = (EOCPerson*)object;     
     // 只有每个属性都相等,才认为两个对象相等。     
     if(![_firstName isEqualToString:otherPerson.firstName]) return NO;     
     if(![_lastName isEqualToString:otherPerson.lastName]) return NO;     
     if(_age != otherPerson.age) return NO;     
     return YES 
}  
// 哈希码生成方法只要能保证两个相同的对象生成的是两个相同的哈希码就行了(相同哈希码的对象不一定相等),写法有很多种。 
// 下面是一种比较好的哈希码生成方式,既能保持高效率,又能使生成的哈希码至少位于一定范围之内,而不会过于频繁地重复。 
-(NSUInteger)hash{
     NSUInteger firstNameHash = [_firstName hash];     
     NSUInteger lastNameHash = [_lastName hash];     
     NSUInteger ageHash = _age;     
     return firstNameHash ^ lastNameHash ^ ageHash;
} 
@end

如果自定义的类经常需要判断等同性,那么最好自己来编写一个与isEqualToString:方法类似的判断方法。既能提升检测速度,又提升代码的可读性。

-(BOOL)isEqualToPerson:(EOCPerson*)otherPerson{
     // 如果指针相等,则指向同一对象,所以相等     
     if(self == object) return YES;          
     // 只有每个属性都相等,才认为两个对象相等。     
     if(![_firstName isEqualToString:otherPerson.firstName]) return NO;     
     if(![_lastName isEqualToString:otherPerson.lastName]) return NO;     
     if(_age != otherPerson.age) return NO;     
     return YES 
}  
-(BOOL)isEqual:(id)object{
     // 如果两个对象属于同一个类,则由自己的方法判定,否则交由超类判定     
     if([self class] != [object class]){
          return [self isEqualToPerson:(EOCPerson*)obj];    
      }else{    
          return [super isEqual:object];     
      } 
} 

在容器中放入可变类对象后,就不应该改变其哈希码了。要解决这个问题,需要确保哈希码不是根据对象的可变部分计算出来的,或是保证放入容器后就不再改变对象内容了。 下面是一个错误示例:

NSMutableSet *set = [NSMutableSet new]; 

NSMutableArray *arrayA = [@[@1, @2]mutableCopy]; 
[set addObject:arrayA]; 
NSLog(@"set = %@", set); // set = {(1,2)}  

NSMutableArray *arrayB = [@[@1, @2]mutableCopy]; 
[set addObject:arrayB]; 
NSLog(@"set = %@", set); // set = {(1,2)} NSSet或NSMutableSet中不会保存重复的对象 

NSMutableArray *arrayC = [@[@1]mutableCopy]; 
[set addObject:arrayC]; 
NSLog(@"set = %@", set); // set = {(1),(1,2)} arrayC与set里已有对象不等,可以添加到set中  

[arrayC addObject:@2]; // 改变arrayC中的内容,使其与set中的对象相等 
NSLog(@"set = %@", set); // set = {(1,2),(1,2)} 出现问题,set中出现两个相等的对象,与set的语义冲突  

NSSet *setB = [set copy]; 
NSLog(@"setB = %@", setB); // setB = {(1,2)} 拷贝之后重复的对象又消失了 

如果将某对象放入容器之后又修改其内容将会造成难以预料的行为! 进行等同性判定的时候还需要注意:不要盲目地逐个检测每条属性,而是应该按照具体需求来制定检测方案。

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

类簇是一种很有用的模式,可以隐藏抽象基类背后的实现细节。Objective-C的系统框架中普遍使用此模式。比如,iOS的用户界面框架UIKit中的UIButton类,创建按钮调用的是下面的类方法:

+(UIButton*)buttonWithType:(UIButtonType)type;

该方法返回的对象取决于传入的按钮类型,所有的返回类型的对象都继承自同一个基类:UIButton。这样做的意义在于,UIButton类的使用者无须关心创建出来的按钮具体属于哪个子类,也不用考虑按钮的绘制方式等实现细节。只需要知道明白如何创建按钮,如何设置title等属性,增加触摸动作的目标对象等问题。 下面是一个创建类簇的示例:

// EOCEmployee.h 
// 用枚举类型表示雇员的类型 
typedef NS_ENUM(NSUInteger, EOCEmployeeType){
     EOCEmployeeTypeDeveloper,     
     EOCEmployeeTypeDesigner,     
     EOCEmployeeTypeFinance, 
};  
@interface EOCEmployee : NSObject  
@property (copy) NSString *name; 
@property NSUInteger salary;  
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type;
- (void)doADayWork;
@end  

// EOCEmployee.m 
@implementation EOCEmployee  
// 使用工厂模式创建类簇
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type{
     // 根据传入的雇员类型参数返回不同的子类对象     
     switch (type) {     
         case EOCEmployeeTypeDeveloper:             
         return [EOCEmployeeDeveloper new];             
         break;         
         case EOCEmployeeTypeDesigner:             
         return [EOCEmployeeDesigner new];             
         break;         
         case EOCEmployeeTypeFinance:             
         return [EOCEmployeeFinance new];             
         break;         
     } 
}  
-(void)doADayWork{
     // 由子类实现 
} 
@end  
// EOCEmployeeTypeDeveloper枚举类型对应的子类,其它两个子类类似 
// EOCEmployeeDeveloper.h 
@interface EOCEmployeeDeveloper : EOCEmployee 
@end  
// EOCEmployeeDeveloper.m 
@implementation EOCEmployeeDeveloper 
- (void)doADayWork{
     // 具体实现代码 
} 
@end

系统框架中有许多类簇。大部分容器都是类簇,例如NSArray与NSMutableArray,虽然有两个抽象基类,但仍然可以合起来算作一个类簇。两个类共属于同一类簇,这意味着二者在实现各自类型的数组时可以共用实现代码,此外还可以把可变数组复制为不可变数组,反之亦然。

id maybeAnArray = /* . . . */; 
// b1永远为NO,因为[maybeAnArray class]返回的是隐藏在NSArray类簇公共接口后面的某个内部类型,而不是NSArray类本身 
bool b1 = ([maybeAnArray class] == [NSArray class]); 
// 可以用isKindOfClass:方法来判定maybeAnArray所属的类是否位于NSArray类簇中 
bool b2 = [maybeAnArray isKindOfClass:[NSArray class]];

对于Cocoa中NSArray这样的类簇来说,可以新增子类,但是需要遵守下面几条规则:
1. 子类应该继承自类簇中的抽象基类。
2. 子类应该定义自己的数据存储方式。
3. 子类应当重写超类文档中指明需要重写的方法(一般会定义在基类的开发文档中)。

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

有时需要在对象中存放相关信息。这时我们通常会从对象所属的类中继承一个子类,然后改用这个子类对象。有时候类的实例可能是由某种机制所创建的,而开发者无法令这种机制创建出自己所写的子类实例。之中情况就需要用关联对象来解决问题。 可以给某关联对象关联许多其他对象,这些对象通过键来区分。存储对象值的时候可以指明存储策略,用以维护相应的内存管理语义。

关联类型 等效的属性特质
OBJC_ASSOCIATION_ASSIGN assgin
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic,retain
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic,copy
OBJC_ASSOCIATION_RETAIN retain
OBJC_ASSOCIATION_COPY copy

下列方法可以管理关联对象

// 以给定的键和策略为某对象设置关联对象值 
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) 
// 根据给定的键从某对象中获取关联对象值 
id objc_getAssociatedObject(id object, void *key) 
// 移除指定对象的全部关联对象
void objc_removeAssociatedObjects(id object)

对关联对象的操作与对NSDictionary的操作类似,但有个重要差别,设置关联对象时用到的键是void*类型的。如果在两个键上调用isEqual方法返回的是YES,那么NSDictionary就认为二者相等,而设置关联对象时,若想要两个键匹配到同一个值,则二者必须是完全相同的指针才行,所以通常使用静态全局变量作为关联对象的键。 下面是一个关联对象用法的示例,示例使用的是UIAlertView类(这个类现在已经被废弃了,推荐使用UIAlertController类,这里仅作演示使用)

// 导入运行时系统框架 
#import <objc/runtime.h>  
// 定义全局静态变量做键 
static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";  
- (void)askUserAQuestion{
     UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question" message:@"What do you want to do?" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Continue", nil];          
     // 定义一个处理按钮逻辑的块     
     void (^block)(NSUInteger) = ^(NSUInteger buttonIndex){      
        if (buttonIndex == 0) {        
           // 停止         
        }         
        else{        
           // 继续         
        }     
     };          
     // 将块设置为alert的关联对象     
     objc_setAssociatedObject(alert, EOCMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);          
     [alert show]; 
}  

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
     // 获取关联对象中的块并执行     
     void (^block)(NSUInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey);     
     block(buttonIndex); 
}

这样做的好处是,将创建警告视图与处理操作结果的代码放到了一起,这样能增强代码的可读性。需要注意的是,块可能要捕获某些变量,这也许会造成保留环。因为关联对象之间的关系并没有正式的定义,其内存管理语句是在关联的时候才定义的,而不是接口中预先定好的。使用块作关联对象的时候一定要小心,更好的做法是将块保存为子类中的属性。

本页内容版权归属为原作者,如有侵犯您的权益,请通知我们删除。
插件其实是Apk安装包,如果要使用必须先要安装和解析,以便知道插件Apk的相关信息。而从Demo中我们知道插件的安装和卸载是通过调用PluginManager的installPackage()和deletePackage()来实现的。就先从PluginManager.installPackage()开始分析插件Apk的安装过程。 第一步:PluginManager. getInstance().installPackage(apkPath,flag); 此函数中只是调用了mPluginManager.in
分享截屏已经是很多游戏应用必备的功能了,找到了一个国内的插件,虽然用起来还行,但是,还是想吐槽下,跟老外的插件比,真的有差距啊有差距啊有差距啊,啊啊啊。 ShareSDK的官方网站:http://www.mob.com/,使用插件需要注册账号获得key,不过,至少现在是免费的。 我的Unity版本是5.3,xcode版本是7.2,ShareSDK版本是2.7.4 Unity sdk下载地址:https://github.com/MobClub/New-Unity-For-ShareSDK 新建一个简单的工
正常情况下启动一个Activity,首先需要在AndroidManifest文件中声明,其次需要把该应用安装到手机系统中。 而插件apk是没有正在安装到手机系统中的,也就按照正常的启动流程插件Activity是不能启动的。另外插件apk的类需要加载进来是需要指定ClassLoader。前面的文章也大概讲过,当启动一个插件Activity时,先是用预定义的代理Activity替换目标Activity(及插件Activity)去启动,当AMS处理完回调到应用空间时(及回到运行Activity的进程空间时)再用
从DroidPlugin的官方文档中我们知道。 2 在AndroidManifest.xml中使用插件的com.morgoo.droidplugin.PluginApplication: 或者在自定义的Application的onCreate()函数中,调用PluginHelper.getInstance().applicationOnCreate(getBaseContext()); 在Application的attachBaseContext()函数中,调用 PluginHelper.getInsta

字母雨的实现 - 2016-07-23 14:07:06

有段时间没写博文了,前段时间比较忙,这几天闲下来,想着写点东西,脑袋一下就闪过以前学习Android的时候见到的别人实现的黑客帝国的字母雨效果,当时对于小菜鸟的自己,那叫一个膜拜啊,时隔几年,自己实现一下,算是对以前的自己一个交代吧。 先看效果: 一、实现原理 在实现过程中,主要考虑整个界面由若干个字母组成的子母线条组成,这样的话把固定数量的字母封装成一个字母线条,而每个字母又封装成一个对象,这样的话,就形成了如下组成效果: 字母对象--》字母线条对象--》界面效果 每个字母都应该知道自己的位置坐标,自己

DroidPlugin源码分析Hook过程 - 2016-07-23 14:07:06

插件运行环境初始化过程中我们知道,Hook的初始化是在PluginHelper的initPlugin函数中通过调用PluginProcessManager.installHook来实现的。而在分析DroidPlugin Hook过程之前需要先简单了解一下Java的动态代理。 Java动态代理与之相关的一个类Proxy,一个接口InvocationHandler,一个函数invoke他们之间的关系。就通过DroidPlugin 的BinderHook类的部分代码来解释一下他们的关系; abstract cl
大概半年之前,看过鸿洋大神的一篇博客 Android 自定义控件玩转字体变色 打造炫酷ViewPager指示器 他说大概想了32秒就知道了实现思路,这深深的刺痛了我。最近又看了一遍,决定做点什么 我要自定义的控件是一个盖世英雄, 它不仅仅是一个 Loading控件 ,同时还支持 进度条 (ProgressBar) 功能 。 它会在你需要的时候出现, 它支持 left , top , right , bottom 四个方向加载(变色),最重要的是,它可以是 文字 ,也可以是 图片 ,能够满足开发者一切需求。
首先为权限: uses-permission android:name="android.permission.INTERNET" / uses-permission android:name="com.android.vending.BILLING" / uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" / uses-permission android:name="android.permission.AC
Day01 html与css基础入门 1.html的常见标签和实战 1.1 a标签 a href=#> 1.2 img标签 img src="plmm.jpg" width="100px" height="200px" alt="图片信息丢失!"/// alt属性的值表示当图片找不到时显示的文字信息 1.3 列表标签 ol type="I" start="1" li我是天才1号/li li我是天才2号/li li我是天才3号/li li我是天才4号/li/olul type="circle" li我是逗逼

EventBus 源码分析 - 2016-07-23 14:07:10

0. 前言 EventBus 是一款针对Android优化的发布/订阅事件总线。主要功能是替代 Intent , Handler , BroadCast 在 Fragment , Activity , Service ,线程之间传递消息。优点是开销小,代码更优雅,以及将发送者和接收者解耦。此文将对最新的 EventBus 3.0 的源码进行简要的分析。 1. 用法回顾 EventBus 3.0 的用法较之前的版本有所变化,它使用了最近较为流行的注解形式取代以前的 onEvent 开头作为方法名,但使用步骤