1.面向过程与面向对象
OC中的类是面向对象,C语言中的结构体是面向过程。OC不能直接编译,需要由runtime转换成纯C在进行编译。
例如五子棋,面向过程的设计思路就是首先分析问题的步骤:1、开始游戏,2、黑子先走,3、绘制画面,4、判断输赢,5、轮到白子,6、绘制画面,7、判断输赢,8、返回步骤2,9、输出最后结果。把上面每个步骤用分别的函数来实现,问题就解决了。
而面向对象的设计思路是:可以分为 1、黑白双方,这两方的行为是一模一样的,2、棋盘系统,负责绘制画面,3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的i变化就要负责在屏幕上面显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。
由此可见,面向过程是以步骤来划分问题的,而面向对象是以功能来划分问题的。
2.预处理指令
预处理过程扫描源代码,对其进行初步的转换,产生新的源代码提供给编译器。预处理过程先于编译器对源代码进行处理。
预处理过程读入源代码,检查包含预处理指令的语句和宏定义,并对源代码进行响应的转换。预处理过程还会删除程序中的注释和多余的空白字符。
预处理指令是以#号开头的代码行。常见的预处理指令如下:
- #空指令,无任何效果
- #include 包含一个源代码文件
重复使用include会因为重复定义报错,但是重复使用import可以
会在b.h中报出重复定义错误。如下:
#include "b.h"
#include "b.h"
可以使用:
#import "b.h"
#import "b.h"
在较为复杂的情况下,例如类A和类B均include头文件C,那么在类D同时include类A和类B时也会出现上述错误,全部使用import则不会存在问题。
另一种解决办法就是:
#ifndef _C_H
#define _C_H
#include "c.h"
#endif
用import就不存在该问题,所以不必使用ifndef和define强调。当创建.h文件和.pch文件时系统会自动生成带有ifndef和define这对预处理命令组合,所以它们应该很少会被手动用到。
- #define 定义宏
#define SizeValueBase6Plus(value) ((value) / 414.0f * [UIScreen mainScreen].bounds.size.width)
#define FONTSIZE(size) [UIFont systemFontOfSize:SizeValueBase6Plus(size)]
- #undef 取消已定义的宏
- #if 如果给定条件为真,则编译下面代码
用来区分生产联调等各环境基本配置信息。更改宏定义的值即可
#define IS_RELEEASE 1
#if IS_RELEEASE
NSString *const APP_KEY = @"67585b150d92de590a5767c624533b88";
#else
NSString *const APP_KEY = @"366490c0efde1128c99fc634c51bca5f";
#endif
- #ifdef 如果宏已经定义,则编译下面代码
通过判断前面是否有#define DEBUG来判断是否打印,可在代码手动控制打开注销。
#ifdef DEBUG
print ("device_open(%p)\n", file);
#endif
在.pch文件中强调非OC语言不能调用。 防止代码中有.cpp,.mm文件时会报错。
#ifdef __OBJC__
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#endif
- #ifndef 如果宏没有定义,则编译下面代码
如果没有宏定义过该文件,则宏定义它,并执行以下代码。如果宏定义过该文件,则什么也不做。以此来保证内部代码只编译一次。在.h文件和.pch文件创建事系统会自动生成,保证内部引用和宏定义不会重复编译。
#ifndef HWPrefixHeader_pch
#define HWPrefixHeader_pch
#endif /* HWPrefixHeader_pch */
- #elif 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
- #endif 结束一个#if……#else条件编译块
- #error 停止编译并显示错误信息
- #pragma mark - 代码中段落方法注释
3.typedef和struct的使用
typedef主要用于把已知类型更换成自定义的名字。struct结构体在OC中很少使用,OC中主要采用对象。
typedef和struct的使用方式:
struct Date1{
int year;
int month;
int day;
};
- (void)example1{
struct Date1 *birthday = NULL;
birthday->day = 22;
birthday->month = 10;
NSLog(@"%d月%d日",birthday->month,birthday->day);
}
struct Date2{
int year;
int month;
int day;
};
typedef struct Date2 MyDate2;
- (void)example2{
MyDate2 *birthday = NULL;
birthday->month = 10;
birthday->day = 20;
NSLog(@"%d月%d日",birthday->month,birthday->day);
}
struct Date3{
int year;
int month;
int day;
};
typedef struct Date2 *MyDate3;
- (void)example3{
MyDate3 birthday = NULL;
birthday->month = 10;
birthday->day = 20;
NSLog(@"%d月%d日",birthday->month,birthday->day);
}
typedef和struct在系统中的应用,如在objc.h中:
typedef struct objc_class *Class;
struct objc_class : objc_object {
Class superclass;
const char *name;
uint32_t version;
uint32_t info;
uint32_t instance_size;
struct old_ivar_list *ivars;
struct old_method_list **methodLists;
Cache cache;
......
};
Class实际上是objc_class结构体的指针。
使用typedef定义block。如下:
typedef void(^block8)(int,int);
[self block8with:^(int a, int b) {
NSLog(@"结果为%d",a+b);
}];
- (void)block8with:(block8)block
{
block(5,5);
}
直接使用block8代替void(^)(int,int)。
使用typedef在C语言中定义函数指针。常见于OC底层代码实现。使用方法如下:
typedef int (*calFun)(int,int);
int add(int a,int b)
{
return a+b;
}
int sub(int a,int b)
{
return a-b;
}
void testfun(int type)
{
calFun fun;
if (type == 1) {
fun = add;
}else
{
fun = sub;
}
int result = (*fun)(10,5);
NSLog(@"结果为%d",result);
}
定义一个名为calfun,返回值为int参数为两个int的函数类型。
3.@synthesize
声明一个属性
@property(nonatomic,retain) NSString *name;
“属性”(property)有两大概念:ivar(实例变量)、存取方法(access method=getter),即@property = ivar + getter + setter。
默认情况下,编译器会为当前类自动生成一个NSString *_name的实例变量,同时提供访问该实例变量的getter和setter方法(简化工作量)。即LLVM会自动生成@synthesize name = _name
,也代表属性有name和_name两个变量名。同时可以使用@synthesize关键字改变实例变量的别名。而实际上,我们使用_name操作实例变量,使用self.name操作的是方法(等号左是set方法,等号右边是get方法),而非属性。
当setter和getter方法同时重写时,需要手动添加@synthesize关键字。
@dynamic关键字会不自动生成getter和setter方法。
4. 修饰词
atomic/nonatomic
指定合成存取方法是否为原子操作,可以理解为是否线程安全,但在iOS上即时使用atomic也不一定是线程安全的,要保证线程安全需要使用锁机制,超过本文的讲解范围,可以自行查阅。
可以发现几乎所有代码的属性设置都会使用nonatomic,这样能够提高访问性能,在iOS中使用锁机制的开销较大,会损耗性能。
readwrite/readonly
readwrite是编译器的默认选项,表示自动生成getter和setter,如果需要getter和setter不写即可。
readonly表示只合成getter而不合成setter。
assign
assign表示对属性只进行简单的赋值操作,不更改所赋的新值的引用计数,也不改变旧值的引用计数,常用于标量类型,如NSInteger,NSUInteger,CGFloat,NSTimeInterval等。
assign也可以修饰对象如NSString等类型对象,上面说过使用assign修饰不会更改所赋的新值的引用计数,也不改变旧值的引用计数,如果当所赋的新值引用计数为0对象被销毁时属性并不知道,编译器不会将该属性置为nil,指针仍旧指向之前被销毁的内存,这时访问该属性会产生野指针错误并崩溃,因此使用assign修饰的类型一定要为标量类型。
unsafe_unretained
使用unsafe_unretained修饰时效果与assign相同,不会增加引用计数,当所赋的值被销毁时不会被置为nil可能会发生野指针错误。unsafe_unretained与assign的区别在于,unsafe_unretained只能修饰对象,不能修饰标量类型,而assign两者均可修饰。
strong
strong表示属性对所赋的值持有强引用表示一种“拥有关系”(owning relationship),会先保留新值即增加新值的引用计数,然后再释放旧值即减少旧值的引用计数。只能修饰对象。如果对一些对象需要保持强引用则使用strong。
weak
weak表示对所赋的值对象持有弱引用表示一种“非拥有关系”(nonowning relationship),对新值不会增加引用计数,也不会减少旧值的引用计数。所赋的值在引用计数为0被销毁后,weak修饰的属性会被自动置为nil能够有效防止野指针错误。
weak常用在修饰delegate等防止循环引用的场景。
copy
copy修饰的属性会在内存里拷贝一份对象,两个指针指向不同的内存地址。
一般用来修饰有对应可变类型子类的对象。
如:NSString/NSMutableString,NSArray/NSMutableArray,NSDictionary/NSMutableDictionary等。
为确保这些不可变对象因为可变子类对象影响,需要copy一份备份,如果不使用copy修饰,使用strong或assign等修饰则会因为多态导致属性值被修改。
_block是用来修饰一个变量,这个变量就可以在block中被修改
_weak是用来修饰一个变量,这个变量不会在block代码块中被retain,用来避免循环引用。
5.堆和栈与数据类型
堆和栈的区别可以用如下的比喻来看出:
使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。
OC对象需要进行内存管理,而其它非对象类型比如基本数据类型就不需要进行内存管理
Objective-C的对象在内存中是以堆的方式分配空间的。堆里面的内存是动态分配的,所以也就需要程序员手动的去添加内存、回收内存。
非OC对象一般放在栈里面(栈内存会被系统自动回收)
举例说明:
int main(int argc, char *argv[])
{
@autoreleasepool {
int a = 10;
int b = 20;
Car *c = [[Car alloc] init];
}
}
当代码块一过,里面的a,b,*c指针都会被系统编译器自动回收,因为它存放在栈里面,而OC对象则不会被系统回收,因为它存放堆里面,堆里面的内存是动态存储的,所以需要程序员手动回收内存。
在C中,当使用
Studet stu;
创建对象时,创建的对象在栈中。使用Student *pStudent = new Student
创建时,对象在堆中,需要手动释放。
在堆上创建对象是匿名的,必须要有一个指针指向它,否则无法使用。而在栈上创建对象,都用名字,不必须用指针指向它,由系统管理内存。
6.基本类型与OC对象
int,bool等初始化不需要*的为基本类型即值类型,存在于栈,不需要管理内存,赋值与作为参数传递会进行拷贝,拷贝后的值与原值内容相同地址不同,修改其中一个不会影响另一个。
而array,dictionary等初始化需要*的为对象类型即引用类型,存在于堆,需要管理内存,赋值与作为参数传递不会进行拷贝而是会对引用计数进行加1处理,实际上地址相同,改变其中一个会影响另一个。
string是比较特殊的对象类型。
7.NSString与NSMutableString
NSString不同之处在于使用常量字符串去初始化一个NSString,而系统会对常量字符串进行优化,所有引用同一个常量字符串的NSString共享同一块内存,这块内存位于常量区,引用计数为7fffffff,表示不使用通常的引用计数管理机制,不会释放,所以会处于栈而不是堆.
NSString 的对象就是stack(栈) 中的对象,NSMutableString 的对象就是heap(堆) 中的对象。前者创建时分配的内存长度固定且不可修改;后者是分配内存长度是可变的,可有多个owner, 适用于计数管理内存管理模式。
两类对象的创建方法也不同,前者直接创建“NSString * str1=@"welcome"; “,而后者需要先分配再初始化“ NSMutableString * mstr1=[[NSMutableString alloc] initWithString:@"welcome"]; ”。
NSString对应的赋值与作为参数传递等所有操作都是进行拷贝并使用所返回新的string(值类型),而NSMutableString是根据地址是真正意义上的在原字符串上操作(对象类型)。
8.分类
例a.h代码如下:
@interface a : NSObject
@end
//这是定义的一个extension(扩展),在扩展中是可以添加属性的
//并且注意,扩展中添加的属性是private的,对外是不可访问的(当然是运行时和kvc除外)
//extension实际上是一个“匿名”的category
//extension可以定义在.h或者.m文件中都可以
@interface a()
@property (strong, nonatomic) NSString *name;
@property (strong, nonatomic) NSString *sex;
@end
//这里又定义一个extension
//一个class可以定义多个extension
//实际上运行时的时候extension和原文件没有区别
@interface a()
@property (strong, nonatomic) NSString *age;
@end
例a.m代码如下:
#import "a.h"
#import <UIKit/UIKit.h>
//这里定义了一个category,注意在category中可以为任何文件扩展函数,但是不能添加属性(使用运行是才可以)
//扩展里面定义的函数是public的,所有继承者都拥有定义的功能
//扩展的定义可以在.h或者.m中
//扩展在官方的各种框架中是很常用的,和swift中的extension类似
@interface a(handlerAccount)<UITableViewDelegate>
//定义一个接口
- (a*)getAccount;
@end
//这里是上面定义的扩展的实现
@implementation a (handlerAccount)
- (a *)getAccount
{
a *test = [[a alloc] init];
test.name = @"mark";
test.sex = @"man";
test.age = @"12";
return test;
}
@end
//这里直接定义一个category的实现
//可以用来分离代码
@implementation a (tableViewDelegate)
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
}
@end
分类也可以用来将一个复杂的类中的代码分块(swift的extension可以有相似的作用), 使得代码组织更好, 例如可以将tableView的delegate, 和Datasource在分类中实现。
但是在使用category来扩展Cocoa的原生类的时候, 要注意函数的命名如果是和原生已有的函数名相同,那么将会发生不可预料的结果(不能确定哪一个方法在运行时会被调用), 因此建议在自己的函数名前面加上前缀)。
9.继承
继承一个类可以拥有基类的方法的接口和实现
OC中是不支持多继承的,因为多继承的设计模式效率低下,那么如何在OC中实现多继承
- 组合方式
初始化想要继承类的实例,并使用该实例调用即可 - 协议方式
定义想要继承类方法的协议,并遵循该协议。不过这样只能继承该类的方法接口,不会继承方法的具体实现。