我的iOS开发小屋iOS干货iOS 代码规范 && 效率

《52个有效方法》笔记1——熟悉Objective-C

2015-12-12  本文已影响1651人  Wang66

前言

若想深刻地理解一门学科,或者学习一门手艺,必须经过严谨的,成系统,成理论地学习。无疑,一本一本地去读关于它们的著作是最好的方式,因为书籍中的文字流露出的思维逻辑是最细腻最连贯的。在浩瀚的文字中你不仅能知其然,且能知其所以然。而碎片化阅读仿佛阅读一片片的小纸片,是散乱的。你得到的信息是不成系统的,不能融会贯通的,感觉就像盲人摸象似是而非,而深度阅读后仿佛站在了上帝视角,以前所有的疑问和谜团也拨云见日,逐渐清晰了。
这几天在阅读《编写高质量iOS与OS X代码的52个有效方法》,发现书中有很多干货,而且有很多重要的细节是以前不曾认真思考的。所以想记录下来。


在类的头文件中尽量少引入其他头文件

如下我们在.h文件中定义类的属性或者定义方法时,需要引入其有关类的头文件(StudentModel.h)。

#import <UIKit/UIKit.h>
#import "StudentModel.h"

@interface YWViewController : UIViewController

@property (nonatomic, strong)StudentModel       *studentModel;

- (void)studentStudy:(StudentModel *)studentModel;

@end

但这样不太完美,我们知道#import "StudentModel.h"在编译时其实是拷贝动作,把导入的StudentModel.h文件也拷贝进了该类进行编译,如果在.h文件中泛滥导入其他文件,无疑会增加编译时间。
其实在该YWViewController.h文件中仅仅需要声明StudentModel是个类就行了,所以在此我们应该使用@class关键词声明它是一个类,而在YWViewController.m文件中实现定义的方法时我们需要StudentModel类的细节,所以这时得用#import "StudentModel.h"导入头文件。

规范的写法应该像下面这样:

YWViewController.h

#import <UIKit/UIKit.h>
@class StudentModel; // 在.h文件中仅声明其确实是一个类

@interface YWViewController : UIViewController

@property (nonatomic, strong)StudentModel       *studentModel;

- (void)studentStudy:(StudentModel *)studentModel;

@end

YWViewController.m

#import "YWViewController.h"
#import "StudentModel.h"  // 在.m文件中才正式导入文件,因为需要知道该类细节

@interface YWViewController ()

@end

@implementation YWViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

}


- (void)studentStudy:(StudentModel *)studentModel
{
    NSLog(@"----%@----%@----",studentModel.studentId,studentModel.name);
}

@end

OC中类里的常量和全局常量

即在.m文件中用static和const关键词同时来修饰,static表示只在该“编译单元”内有效,const表示其为常量,不可修改,若类里对该常量值进行了修改,编译时会报错。而且一般命名时以k开头表示只对类内部有效的常量。

// YWViewController.m文件

#import "YWViewController.h"

static NSString *const kMyURL = @"http://www.jianshu.com/users/8b79c6535a4b/latest_articles";


@interface YWViewController ()

@end

@implementation YWViewController

- (void)viewDidLoad{
    [super viewDidLoad];
}

@end

** 千万需要注意的是:const*的位置前后关系有着非常重要的意义,const修饰的是它右边的部分。**在上面的例子中const右边的部分是kMyURL,因此不可变的是kMyURL。而若const处在下面这样的位置,则代表就代表另外的意思了!

    static NSString const *kMyURL = @"http://www.jianshu.com/users/8b79c6535a4b/latest_articles";
    static const NSString *kMyURL = @"http://www.jianshu.com/users/8b79c6535a4b/latest_articles";

** 因为const修饰的是它后面的部分,而此时它后面的是(* kMyURL )"*"是指针指向符号 ,也就是说此时kMyURL指向的内存地址不可变,而内存块中保存的内容是可变的。 **

用下面代码来验证:

    static NSString const *kUserName = @"wang66";
    NSLog(@"%@----%x",kUserName,&kUserName);
    kUserName = @"wang77";
    NSLog(@"%@----%x",kUserName,&kUserName);
    
//  2016-04-09 17:57:06.199 OCDemo[1860:994583] wang66----87f6190
//  2016-04-09 17:57:06.199 OCDemo[1860:994583] wang77----87f6190

kUserName赋值时编译器没报错,赋值成功,打印的内存地址一样。由此验证上面的结论是正确的。

** 先在.h文件中通过extern关键词声明此全局常量,然后在.m文件中定义该常量。一般命名全局常量时会加上该类的前缀已提高可读性。**

比如在登录成功后我们需要发送一个已登录的通知,这个通知的名字一般得定义成全局常量,其他地方需要判断是否已登录时需要通过该通知名来观察该通知,即需要使用该全局常量。

// YWViewController.h文件

#import <UIKit/UIKit.h>

extern NSString *const YWHasLoginedNotifition;  // 先在.h文件中声明此全局常量

@interface YWViewController : UIViewController

@end
// YWViewController.m文件

#import "YWViewController.h"

NSString *const YWHasLoginedNotifition = @"YWHasLoginedNotifition"; // 然后在.m文件中定义该常量


@interface YWViewController ()

@end

@implementation YWViewController

- (void)viewDidLoad{
    [super viewDidLoad];
}
@end

关于数组和字典的初始化

数组和字典初始化系统提供了下面几个方法:
方式1:

    NSArray *arr1 = [NSArray arrayWithObjects:@"a", @"b", @"c", nil];
    NSArray *arr2 = [[NSArray alloc] initWithObjects:@"a", @"b", @"c", nil];
    
    NSDictionary *dict1 = [NSDictionary dictionaryWithObjectsAndKeys:
                           @"valueA", @"keyA",
                           @"valueB", @"keyB",
                           @"valueC", @"keyC", nil];
    
    NSDictionary *dict2 = [[NSDictionary alloc] initWithObjectsAndKeys:
                           @"valueA", @"keyA",
                           @"valueB", @"keyB",
                           @"valueC", @"keyC", nil];

这几种初始化方法是最基本的,但是有更简洁的初始化方式:
方式2:

    NSArray *arr3 = @[@"a", @"b", @"c"];
    
    NSDictionary *dict3 = @{@"keyA":@"valueA",
                            @"keyB":@"valueB",
                            @"keyC":@"valueC"};

显然这种写法更简洁,而且语义更符合人的思维习惯,更简单易懂(代码1里字典初始化时value在key前,这和人们平时的思维是相反的)。
这两种初始化方法并不仅仅是写法不同而已,需要注意的是通过方式2初始化时,元素不能为空,否则会crash。初始化时放入集合的元素对象必须保证是非空的。
而通过方式1初始化时若有元素是nil,会怎样呢?

    NSArray *arr1 = [NSArray arrayWithObjects:@"a", nil, @"c", nil];

    for(int i=0; i<arr1.count; i++)
    {
        NSLog(@"arr1_____%@",arr1[i]);
    }

2015-12-12 20:40:36.341 WangDemo[8254:3133600] arr1_____a

可以从上面代码中看到:通过方式1初始化时,若遇到某元素是nil,则就在该处终止,后面的元素不再放入集合中。上面的代码中arr1在初始化时第二个元素是nil,则在此终止初始化,所以最终arr1里只有一个元素@“a”。

另外。
通过方式2创建的数组和字典都是不可变的,若想变成可变的,那你可以拷贝一份使其成为可变数组:

    NSMutableArray *mutArr = [@[@"a", @"b", @"c"] mutableCopy];

不过拷贝新份对象和一开始就这样初始化并无多大差异。总之,具体场景具体选择吧!

    NSMutableArray *mutArr = [NSMutableArray arrayWithObjects:@"a", @"b", @"c", nil];

善用枚举表示类型,状态、选项,组合等

枚举的写法:

最常见的枚举定义形式是:

typedef enum{ 
   MainList_Ads=0,          //轮播广告
   MainList_Topic,          //专题
   MainList_Classify,       //课程分类
   MainList_Teacher,        //推荐老师 
   MainList_Celebrity,      //名师推荐
   MainList_Agency,         //推荐机构   
}MainListType;

不过OC中对这种定义形式稍稍进行了封装。使其可以指明该枚举底层的数据类型,其实默认都是整形的。既然枚举类型是整形的,那在定义属性时就得是这样的:

@property (nonatomic, assign) MainListType mainListType;

指定第一项为0,则后面项的值会递增。其实通过枚举判断类型和定义一个整形变量判断没有区别。枚举好就好在它的语义一看就明白代表什么意思。

typedef NS_ENUM(NSUInteger, MainListType)
{
    MainList_Ads=0,          //轮播广告
    MainList_Topic,          //专题
    MainList_Classify,       //课程分类
    MainList_Teacher,        //推荐老师
    MainList_Celebrity,      //名师推荐
    MainList_Agency,         //推荐机构
};
枚举的组合:

用枚举表示选项时,选项是可以组合多选的。比如自动布局的条件是枚举,而且需要多选组合使用。

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

既然枚举的每项元素要组合使用以表示不同的情况,那我们就得保证元素组合后的唯一性,不能A和B和C和D组合后表示一个值。
那怎么确定枚举每项组合后值的唯一性呢?
普通的枚举每项值是递增的,此时&&或者||的结果是不唯一的。为了保证组合后的唯一性,人类就给每项元素赋值时做了一点改动:枚举每项的值执行按位或操作(第一项是1的0次方,第二项是1的1次方,第三项是1的2次方·····)。
看下面这张图有助于理解:

屏幕快照 2015-12-12 21.47.42.png

“对象等同性”判断

有时我们有比较两对象是否相等的需求。首先想到的是通过等号“==”判断,但它** 仅仅比较的是两个指针本身,而不是其所指向的对象。 **即使两个不同的指针,也有可能都指向同一个对象。
然后我们想想两个对象到底怎样就会相等呢?...
** 首先 **,若他俩是同一个指针的话,那不用再费劲去比较了,两个对象毫无疑问是相等的,一个对象可以被多个指针指向,但一个指针不可能指向多个对象吧;
** 然后 **,俩对象若要相等,他俩肯定同属于一种类型吧,若它们的类型不同,则两个对象肯定不相等。
** 最后 **,在同属于一种类型的基础上,两个对象对应的属性得完全相等。

NSObject协议中有两个用于判断对象等同性的关键方法:
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
** 通过isEqual:方法判断的两对象若相等,则它们的hash值肯定也是相等的;但两个对象的hash相等,并不能说明他们相等。
对于我们自定义的对象判断等同性,我们可以实现上面两个方法,实现isEqual:方法来定义我们自定义对象比较的逻辑。实现hash方法来定义自定义对象哈希值的算法。编写hash方法时,应该使用计算速度快而且哈希吗碰撞几率低的算法。(hash方法的编写这个日后补充) **
StudentModel类的有比较对象等同性的需求,则实现isEqual:方法。

// StudentModel.m

#import "StudentModel.h"

@implementation StudentModel


- (BOOL)isEqual:(id)object
{
    if(self == object) return YES;  // 若俩指针相同,则肯定是同一个对象。
    if([self class] != [object class]) return NO; // 若俩对象不属于同一类型,则肯定不相等。
    StudentModel *otherStuModel = (StudentModel *)object;
    if(![_studentId isEqualToString:otherStuModel.studentId])
        return NO;
    if(![_name isEqualToString:otherStuModel.name])  // 若有一个属性不相等,则俩对象不相等
        return NO;
    
    return YES;
}

@end

像NSString,NSArray,NSDictionary等系统对象,还提供了诸如isEqualToString:isEqualToArray:等针对这些类特定的比较方法,这样就不用先判断两个对象是否是同一类型了,这样速度更快。我们的自定义对象也完全可以借鉴。

// StudentModel.m

#import "StudentModel.h"

@implementation StudentModel


- (BOOL)isEqual:(id)object
{
    if(self == object) return YES;
    
    if([self class] == [object class]){
        return [self isEqualToStudentModel:(StudentModel *)object]; // 此时只需要调用isEqualToStudentModel:方法即可
    }else{
        return [super isEqual:object];
    }
}

// 已知俩对象同为StudentModel类型时
- (BOOL)isEqualToStudentModel:(StudentModel *)stuModel
{
    if(![_studentId isEqualToString:stuModel.studentId])
        return NO;
    if(![_name isEqualToString:stuModel.name])
        return NO;
    
    return YES;
}

@end

** 注意:** 判断俩对象相等,并不一定都要判断它们对应的每个属性是否都相等,要根据具体情况而定,若在仅通过“主键”等就可以判断对象是否相等的情况下,完全没必要一个一个去判断属性。

容器中可变类的等同性:

关于copy

** 拷贝的目的:改变原对象不影响副本,改变副本不影响原对象。**
并不是说只要拷贝了就一定会生成一个新对象,而要根据上面拷贝的目的来分析。比如,若一个不可变的对象进行了copy操作是不会生成新对象的,因为原始对象就是不可变的,拷贝后的对象也是不可变的,两者均不可变,互不干扰,互不影响,并不需要生成一个新的对象,只需要进行指针拷贝就行了。
同理,若对一个对象进行了mutableCopy操作,不管原始对象是可变不可变,均会生成新对象。
** 容器类对象无论进行什么拷贝,其元素对象均是指针拷贝。但可以通过归档再解档的方式实现元素对象的神拷贝。
** ||----修饰NSString型的属性时为什么要用copy关键字?----||

这篇文章解释得很好:
什么时候用copy,什么时候用strong

** ||----修饰block属性时为什么要用copy关键字?----||**

首先这涉及到MRC时代。因为MRC时期,为了防止block内用到的变量提前释放导致程序崩溃,使用copy将block存放到堆中,此时block会对内部变量进行一次retain操作,从而防止意外清空。同时block放入堆中也会带来一个新的问题,self持有block的引用,如果在block中使用self就会产生循环引用,所以不论MRC还是ARC,我们都分别用blcok和weak来修饰self。

这篇文章解释得很全面:
认识copy关键字

上一篇下一篇

猜你喜欢

热点阅读