第一天视频课程:
Objective-C 语言简介
Objective-C 语言是一门在C语言基础上做了面向对象扩展的编程语言,1983年由Brad Cox 和Tom Love发明,是目前苹果开发平台的主力语言,与Cocoa 和Cocoa Touch框架高度集成,支持开发Mac OS X、iOS应用。在苹果开发平台上通过LLVM(LowLevelVirtualMachine)编译器架构支持与Swift语言双向互操作。
iOS开发平台
Cocoa框架部分包括了系统内核(Core OS)、内核服务(Core Service)、媒体(Media)、触摸(Cocoa Touch)这几个内库。通过LLVM编译框架和Objective-C运行时编译和运行。目前主要有Objective-C、Swift和C/C++这几种语言来进行编码,常用的开发工具为Xcode。
掌握高级编程语言的思维方式
底层思维:从微观、机器的层面理解语言的构造、编译转换、内存模型和运行时机制。
抽象思维:将我们周围世界抽象为程序代码,即面向对象的思维方式,组件封装、设计模式、架构模式。
“时空人”三位一体分析方法
时间分析,发生在什么时候?编译时还是运行时。
空间分析,变量放在那里?堆空间还是栈空间。
人物分析,代码哪里来的?程序员还是编译器、运行时、框架。
Objective-C 语言的两种开发方式
Clang 或 GCC命令行:适合调试、研究、微观探查。
Xcode项目:适合构建正规工程项目,使用大型框架,追求设计质量与代码组织。
Objective-C 语言代码学习
#import 导入头文件(类似C语言的#include),可以避免相同头文件的重复导入,推荐使用#import代替#include。
@autoreleasepool 支持ARC(Automatic Reference Counting)的一个池,用来表明启用了内存自动回收机制。
NSlog(@"Hello,world!"); NSlog类似C语言里的printf用来打印字符串,OC语言里的字符串前需要加@符号(@"Hello,world!")。
头文件的扩展名是.h,主程序文件的扩展名是.m
编译命名行:clang -fobjc-arc HelloWorld.m -o HelloWorld其中-fobjc-arc为ARC内存管理的开关命名,其中-o HelloWorld,-o 为output即输出的意思,一起为将输出的文件命名为HelloWorld。clang也可以换成gcc即用gcc编译器编译。(推荐用clang)
命名行:clang -help 用来显示clang的设置帮助文档,可以用来了解clang的设置。
命名行:./HelloWorld 执行生成的HelloWorld文件。
ObjC编译过程
目前主流为LLVM-Clang的编译过程,由OC、C\C++代码通过Clang前端再通过LLVM优化和LLVM代码生成器生成出X86-64机器码。
学习资源
苹果官方文档:https://developer.apple.com/library/
programming with Objective-C: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/
iOS专区:https://developer.apple.com/library/ios/
苹果开发者大会WWDC:https://developer.apple.com/videos/wwdc/2014/
WWDC:https://developer.apple.com/videos/wwdc/2015/
第二天视频课程:
类型系统
引用类型 reference type:包括class、pointer、block。
值类型 value type:包括基础数值类型、结构struct、枚举enum。
类型装饰:包括协议protocol、类别category、扩展extension
类 class VS. 结构 struct
类型与实例:类的实例是对象,结构的实例是值。
类是一个引用类型,是位于栈上的指针指向了一个位于堆上的实体对象。
结构是一个值类型,它直接位于栈中。
例子1:声明一个类(建立一个对外接口)
@interface RPoint:NSObject //声明一个类,类名RPoint,继承自类NSObject。:表示继承。
@property int x; //声明一个属性 int X
@property int y;
-(void)print; //声明一个实例方法 print
@end
在OC中创建类时要先在头文件中(.h)创建一个对外借口,@interface来表示对外接口的起始,用@end来对应结束。
@property 在类中定义属性的关键字,用来表示这个类的状态。
-(void)print 是在类中定义方法的语句,其中-表示是一个实例方法,方法的返回值是void,方法的名字是print.类中的方法来表示这个类的行为。
例子2:实现一个类
#import<Foundation/Foundation.h> //导入Foundation.h头文件
#import"rpoint.h" //导入rpoint.h头文件,即上面我们创建的类声明文件。
@implementation RPoint //定义一个类名为RPoint 的类
-(void)print{
NSLog(@"[%d,%d]", self.x, self.y); //print方法的实现
}
@end
OC的类需要在主文件中(.m)实现,@implementation 来表示实现的起始,用@end来对应结束。
NSLog(@"[%d,%d]", self.x, self.y); NSLog表示打印一个字符串,self表示当前的事例,整句语句表示将当前事例的属性x和y打印出来。
例子3:
#import<Foundation/Foundation.h>
#import"rpoint.h" //导入类所在的文件
#import"spoint.h" //导入结构体所在的文件
void process(RPoint* rp3, SPoint sp3); //函数process的声明
int main(int argc, const char * argv[]){ //入口函数
@autoreleasepool{
RPoint* rp1=[[RPoint alloc]init]; //生成一个RPoint类的对象rp1
rp1.x=10;
rp1.y=20;
[rp1 print]; //显示结果为10, 20
SPoint sp1; //生成一个SPoint结构体实例。
sp1.x=10;
sp2.y=20;
NSLog(@"拷贝----------");
RPoint* rp2=rp1;
rp2.x++;
rp2.y++;
[rp1 print]; //显示结果为11, 21
[rp2 print]; //显示结果为11, 21
SPoint sp2=sp1;
sp2.x++;
sp2.y++;
NSLog(@"[%d,%d]",sp1.x, sp1.y); //显示结果为10, 20
NSLog(@"[%d,%d]",sp2.x, sp2.y); //显示结果为11, 21
NSLog(@"传参----------")
process(rp1, sp1);
[rp1 print]; //显示结果为12, 22
NSLog(@"[%d,%d]",sp1.x, sp1.y); //显示结果为10, 20
}
return0;
}
void process(RPoint* rp3, SPoint sp3){
rp3.x++;
rp3.y++;
sp3.x++;
sp3.y++;
[rp3 print];
NSLog(@"[%d,%d]",sp3.x, sp3.y);
}
RPoint* rp1=[[RPoint alloc] init]; //在内存栈空间创建了一个名字为rp1的RPpoint类实例对象。[]为调用符号,调用也可以称为发送消息。alloc的用处是手动在堆空间申请内存空间,init为初始化所分配的空间。
堆(heap):堆空间用于存储引用类型对象,由程序员手动申请内存空间,释放由运行时ARC机制自动释放,函数之间通过拷贝引用(指针)传递。堆空间具有全局性,总体大小受制于系统内存整体大小。
由于rp1是一个类的实例对象,所以无论是复制还是传参,都是由一个栈中的指针指向堆中的实体对象,复制副本和传参参数的改变都能直接导致rp1指向的实际对象发生改变。
SPoint sp1; //在内存的堆空间创建了一个名字为sp1的结构体。
栈(stack):栈空间用于存储值类型,无ARC负担,由系统自动管理,以执行函数为单位。栈的空间大小在编译时确定(根据参数+局部变量来计算),在函数执行时由系统自动分配一个栈,函数执行结束系统立即自动收回该栈空间,函数之间通过拷贝值传递。栈空间具有局部性,大小有限额(编译工具可以设定栈的大小,一般为1M),超出会栈溢出(stack overflow)。
由于sp1是一个结构值,它存储于栈中,无论复制还是传参,都将在栈中复制一个sp1的副本,sp1的原始值保持不变。
第三天视频课程:
OC类的类型成员
OC类所包含的类型成员(Type Member)主要分为两大类,一类是数据成员(data member)用来描述对象的状态,还有一类是函数成员(function member)用来描述对象的行为。其中数据成员又有实例变量(instance variable)和属性(property)两种。函数成员分为方法(method)、初始化器(init)和析构器(dealloc)三种。
认识属性
属性表达实例状态,描述类型对外接口。相比直接访问实例变量,属性可以做更多控制。
默认情况下,编译器会为属性定义propertyName自动合成一个getter访问器方法:propertyName、一个setter访问器方法:setPropertyName还有一个实例变量_propertyName。
举例:
在OC类中声明一个属性:@property NSString* firstName; //声明一个类所包含的属性。
此时编译器会自动生成类似如下代码,但是并不显示出来:
-(NSString*)firstName{ //生成一个getter函数
return _firstName;
}
-(void)setFirstName:(NSString *)newValue{ //生成一个setter函数
_firstName=newValue;
}
NSString* _firstName; //创建一个以 _(下划线)+属性名的实例变量
由于编译器自动生成了类似如上代码,我们可以用编译器生成的getter方法和setter方法来访问或修改该属性的内容。
例如:
Employee* employee=[[Employee alloc] init];
[employee setFirstName: @"Tom"]; //将employee类的FirstName属性设置为“Tom”,setFirstName这个setter方法是编译器自动生成的。
NSLog(@"First Name: %@", [employee firstName]); //打印出employee类的FirstName属性,此处[employee firstName]是调用了编译器自动生成的getter方法firstName。
除了以上的方法来访问getter和setter方法外,可以用类名+.(点表达式)+属性名来访问属性的getter和setter方法。
例如:
employee.lastName=@"Chen"; //访问了lastName属性的setter表达式,等价于[employee setFirstName: @"Chen"];
NSLog(@"First Name: %@", employee.lastName); //访问了lastName属性的getter表达式,等价于NSLog(@"First Name: %@", [employee firstName]);
两种方法在本质上没有什么区别,推荐用点表达式,使用比较方便。
可自定义访问器方法,也可更改访问器方法名或实例变量名。
例如:
@property (readonly) NSString* fullName; //在头文件中定义了一个只读属性fullName
-(NSString *)fullName{
return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
} //在主文件中自定义了fullName属性的getter访问器方法。stringWithFormat函数是用来链接两个字符串类实例。
@property (getter=GivenName, setter=GivenName:) NSString* firstName; //声明firstName属性时将该属性的getter、setter访问器的名字都设置为GivenName。
@synthesize firstName=givenName; //在主文件中将firstName属性由系统自动生成的_firstName实例变量改名为givenName。
可以使用静态全局变量(C语言)+类方法,模拟类型属性。
static int _max=100; //在主文件中定义一个静态变量_max
+(int)max; //在头文件中声明一个类方法(getter访问器) +(int)max
+(void)setMax:(int)newValue; //在头文件中声明一个类方法(setter访问器) +(void)setMax
+(int)max{
return _max; //在主文件中实现类方法+(int)max
}
+(void)setMax:(int)newValue{
_max=newValue; //在主文件中实现类方法+(void)setMax
}
完成以上步骤后,可以实现访问类型属性。
[Employee setMax:400]; 等同于Employee.max=400; 模拟出来的类属性。
实例变量
可以定义实例变量,而不定义属性(这样外部不能访问到实例变量),只有实例变量,没有类变量(使用静态全局变量+类方法可以模拟出类变量的效果)。
如果同时自定义了getter和setter访问器方法,或者针对只读属性定义了getter访问器方法,编译器将不再合成实例变量。
引用类型的实例变量在类外一律使用属性来访问,类内大多也通过self使用属性访问。只有以下情就卡了况使用实例变量来访问:1、初始化器 init 2、析构器 dealloc 3、自定义访问器方法。
实例变量的生存周期
实例变量的储存:跟随对象实例存储在堆上。
值类型实例变量直接“内嵌”在对象实例中。跟随对象实例内存释放而被释放。
引用类型实例变量通过指针“引用”堆上的引用类型实例,ARC针对引用进行计数管理,自动释放引用计数为0的对象。
属性的描述特性(Attribute)
属性的描述特性可以指定属性不同环境下的不同功能。
读写特性,默认情况属性都是可读写(readwrite)的,也可以设置为只读(readonly)属性。
例如:
@property (readonly) NSString* fullName; //在头文件中定义了一个只读属性fullName
多线程特性,默认情况下是属性是原子性(atomic)的,表示多线程时原子性属性不能被线程抢走,要么没有运行,开始运行了必须运行到结束。也可以设置为非原子性(nonatomic).
例如:
@property (nonatomic) NSString* fullName; //在头文件中定义了一个非原子性属性fullName
内存管理特性
引用属性默认为强(strong)引用属性,弱(weak)引用阻止循环引用。当两个实例对象属性相互强引用时会形成循环引用,这时ARC内存管理将视这两个互引用的属性都占用,无法将这两个实例的引用属性释放。
为了避免上述情况发生把其中一个对象属性设置为弱引用,这样就不会出现不能释放的问题了。
拷贝(copy)属性创建独立拷贝,当引用属性不想被外界直接引用时,可以用拷贝属性,让外界引用一个拷贝副本,来确保原始数据不会被外界修改。
第四天视频课程:
认识方法 Method
代码段上的可执行指令序列就是函数,函数有全局函数(C语言函数),和成员函数(OC中也叫方法)。
方法是类的成员函数,表达实例行为或类型行为。
举例:
以下声明了4个方法
-(void) print; //-表示是实例方法,返回值是void, 方法名是print, 无参数
-(BOOL) isEqualToPoint: (BLNPoint*) point; //返回值是BOOL, 参数名是point, 参数类型是BLNPoint*
-(void) moveToX: (int)x toY: (int)y; //有两个(int)类型的参数x和y
+(BLNPoint*) getOriginPoint; //+表示getOriginPoint是一个类方法
所有方法默认为公有方法。没有private或protected方法(可以在接口文件中不要声明方法,而在实现文件中实现方法来做到类似private方法)。
动态消息发布:方法调用通过运行时动态消息分发实现,在对象上调用方法又称“向对象发送消息”。
例子:
[p1 print]; //实例p1调用了print方法,也可以说向对象p1发送了一个print消息
[p1 moveToX:100 toY:200]; //向对象p1发送了一个moveToX消息,其中还包含了两个参数,100和200
BLNPoint* origin=[BLNPoint getOriginPoint]; //向BLNPoint类发送了一个getOriginPoint消息,返回值赋值给BLNPoint的类型实例origin
实例方法或类型方法
实例方法用来表达实例的行为所以只能通过实例对象来调用,实例方法在内部实现时可以访问实例成员包括实例属性、实例变量和实例方法。也可以访问类型方法和静态变量。
类方法用来表达类的行为只能通过类来调用,类型方法在实现时可以访问类型方法和静态变量,不能访问实例成员包括实例属性、实例变量和实例方法。
编译器背后对实例方法和类方法的不同处理:self指针
例子:
实例方法实现
-(void) print{
NSLog(@"[%d, %d]", self.x, self.y);
}
上面的实例方法实现中self实际上是一个隐藏的指针参数,用来传递当前实例地址,编译器编译后用C语言的表达方式可以写成:
void print(BLNPoint* self){
NSLog(@"[%d, %d]", self.x, self.y);
}
调用该方法时:[p1 print]; //print(p1);
类方法实现
+(BLNPoint*) getOriginPoint{
BLNPoint* origin=[[BLNPoint alloc] init];
origin.x=0;
origin.y=0;
return origin;
}
上述的类方法实现编译后用C语言表示为
BLNPoint* getOriginPoint(){
BLNPoint* origin=[[BLNPoint alloc] init];
origin.x=0;
origin.y=0;
return origin;
}
类方法里面不能用self关键字来访问实例变量,但是依然可以使用self关键字,这时self关键字用来表示当前类。在类方法实现里面可以用[self process]来调用当前类中的process方法,也等同于[BLNPoint process]。
方法参数
如果参数类型是值类型,为传值方式,如果参数类型为引用类型,则为传指针方式。
方法可以没有参数,也可以没有返回值。
如果方法有参数,方法名约定包含第一个参数,从第二个参数开始需要显示提供外部参数名。
-(void) moveToX: (int)x toY: (int)y; //toY是第二个参数的外部参数名,x是第一个内部参数名,y是第二个内部参数名。
调用时,第一个参数名忽略,但后面的参数名必须显示标明。如: [p1 moveToX:100 toY:200];
动态方法调用机制--消息分发表
在OC里所有的对象类型都可以声明为id类型
例子:
id obj=[[BLNPoint alloc] init];
[obj moveToX:50 toY:60];
[obj print];
这里类型为id的对象obj可以调用类型为BLNPoint的对象方法,是由于OC的动态调用机制造成的。
OC中调用所有方法都会通过消息分发表,上图灰色的部分其中有一个指针,指向class再指向method list。这样做可以增加灵活性,支持在运行时向方法表添加新的方法。缺点是每次调用方法都要多次寻址有性能损失。
第五天视频课程:
初始化器与析构器
初始化器和析构器是类型的特殊函数成员,初始化器用于初始化对象实例或者类型。
对象初始化器:-(id)init 可以重载多个。
例子:
在头文件中可以声明多个有不同参数列表的-(id)init函数。
-(id)init;
-(id)initWithName:(NSString *)name;
-(id)initWithName:(NSString *)name WithPages:(int)pages;
-(id)initWithName:(NSString *)name WithPages:(int)pages WithCategory:(NSString *)category;
在主文件中实现声明的init方法
-(id)init{
self = [super init];
if(self){ //如果父类初始化失败self指针将等于Null
NSLog(@"Book Object init“); //在调用了父类的init方法后可以添加自己所需的内容。
}
return self;
}
-(id)initWithName:(NSString *)name WithPages:(int)pages{
return [self initWithName:name WithPages:pages WithCategory:@"General"];
}
-(id)initWithName:(NSString *)name WithPages:(int)pages WithCategory:(NSString *)category{ //由于该初始化器的参数是最多的,作为主初始化器,其他参数少的初始化器可以直接调用主初始化器。
self = [super init];
if(self){
_name = [name copy]; //在初始化器中使用实例变量而不要使用实例属性。
_pages = pages;
_category = [category copy];
}
return self:
}
初始化对象实例时,init通常和alloc搭配使用。
Book *b1 = [[Book alloc]init]; //alloc是一个从NSObject继承过来的类方法。Tips: 按住commond健再点击关键词可以查看其类库。
Book *b1 = [[Book alloc]init]; 也可以拆分为:
Book *b1 = [Book alloc];
b1 =[b1 init]; //这里等号左边的b1是不可以省略的,因为[b1 init]有一个返回值,是返回一个地址,OC在这里返回的地址有可能和上一行Book *b1 = [Book alloc];其中的b1地址不一样。
alloc所做的事情--NSObject已实现:1、在对上分配合适大小的内存。 2、将属性或者实例变量的内存置0。
init所做的事情--可以自定义:1、调用父类初始化器[super init](前置调用)。 2、初始化当前对象实例变量。
Book *b1= [Book new]; //new相当于调用 alloc/init的无参数版本,不能传递参数。
类初始化器
类型初始化器:+(void)initialize 只能有一个,负责类型级别初始化,初始化类里面的静态变量。
initialize在每个类使用之前被系统自动调用,且每个进程周期中只被调用一次。
子类的initalize会自动调用父类的initialize(前置调用)。
+(void)initialize{
if(self ==[Book class]){ //判断该类是否是Book类
NSLog(@"Book Class initialize");
}
}
对象析构器
对象析构器 -(void)dealloc 只能有一个,用于释放对象(没有类型析构器)拥有的动态资源,无返回值。
-(void)dealloc{
自动调用:1.ARC将对象属性引用计数减持(-1)
手动实现:2.释放不受ARC管理的动态内存,如malloc分配的内存,关闭非内存资源,如文件句柄、网络端口
自动调用:3.父类dealloc
}
dealloc由ARC根据对象引用计数规则,在释放对象内存前自动调用,无法手工调用。
子类的dealloc会自动调用父类的dealloc(后置调用)。
第六天视频课程:
认识面向对象
封装 encapsulation:隐藏对象内部实现细节,对外部仅提供公共接口访问。
继承 inheritance:一个类型在另外类型基础上进行的扩展实现。每一个类只能有一个基类,子类自动继承基类的:实例变量、属性、实例方法、类方法。NSObject类是所有类的根类,所有类向上追溯最上面的类都是NSObject类。继承有两层含义:1、成员复用,子类复用基类的成员。所有的成员都会被继承,就算是私有成员也被继承,只是成员访问不到。2、类型抽象,将子类当作父类来使用。
例子:
建立一个类Shape
@interface Shape : NSObject{ //Shape类继承于NSObject Tips:按住option键点击关键字可以看到相关的参考文档。
@public int _data; //定义了一个公开的实例变量
}
@property int no;
-(void)draw;
+(void)process;
@end
建立一个Shape的子类Circle
@interface Circle:Shape //Circle类继承了Shape类
@property int radius; //Circle类自己的实例属性
@end
由于Circle继承了Shape类,所以Shape类的实例变量、属性、实例方法、类方法Circle类都可以使用。
Circle* circle=[[Circle alloc]init];
circle.no=200;
circle->_data++; //在访问父类实例变量时用->来访问
[circle draw];
[Circle process]; //Circle类也可以调用父类的类方法。
第七天视频课程:
多态 polymorphism:不同类型针对同一行为接口的不同实现方式。
对比重写(override)与重载:子类重写父类同名同参数方法,子类只可以重写父类方法。方法名相同、参数不同,OC不支持方法的重载(重载指的是方法名相同,参数不同。用不同的参数传递给相同名字的方法,达到不同的运行效果)。
在子类的代码中,可以使用super类调用基类的实现,self具有多态性,可以指向不同子类,super没有多态性,仅指向当前父类。
例子:
建立一个Shape的子类Rectangle
@interface Rectangle:Shape
@property int width;
@property int length;
@end
基类Shape中有一个实例方法move:
-(void)move{
NSLog(@"Shape object move");
[self draw];
}
针对Rectangle类增加了两个属性(width和length)重写(override)继承过来的方法
@implementation Rectangle
-(id)init{
self = [super init]; //重写时子类初始化器中必须首先调用基类的初始化器
if(self){
_length = 10;
_width = 20;
}
return self;
}
-(void)draw{
NSLog(@"Rectangle object draw: length=%d, width=%d", self.length, self.width);
}
-(void)print{
NSLog(@"Rectangle Instance variable %d", _data);
}
+(void)process{
NSLog(@"Rectangle class process");
}
-(void)dealloc{
NSLog(@"Rectangle dealloc");
}
当实例对象调用时,Rectangle实例将调用重写的方法。Rectangle实例调用-(void)move时将调用父类Shape类的实例方法-(void)move,此方法中调用的draw方法[self draw],此时self有多态性,将调用Rectangle实例中的draw方法。
对于父类中的属性也可以在子类中重写。
父类Shape中的属性:@property int no;
在子类Rectangle中重写:
-(int)no{
return super.no; //super表示调用父类的方法
}
-(int)setNo:(int)no{
super.no = no;
}
属性的重写本质上就是getter和setter两个访问器的重写。
继承中的init和dealloc
初始化器 init:子类自动继承基类的初始化器,子类也可以重写初始化器,重写时子类初始化器中必须首先调用基类的初始化器(手工调用)再添加其他内容。
析构器 dealloc:子类可以选择继续继承基类析构器,或者重写基类析构器,子类析构器执行完毕后,会自动调用基类析构器(后置调用,且不支持手工调用)。子类析构器自动具有多态性。
Tips:尽量避免在父类init和dealloc中调用子类重写的方法。