前言
如何掌握高级编程语言
- 底层思维: 向下,如何把握机器底层从微观理解对象构造
- 语言构造
- 编译转换
- 内存模型
- 运行时机制
- 抽象思维:向上,如何将我们的周围世界为程序代码
- 面向对象
- 组件封装
- 设计模式
- 架构模式
两种开发方式
- Clang或GCC命令行
clang-fobjc-arc helloword.m
-
-fobjc-arc
支持ARC内存管理 - 适合调试、研究、微观探查
- Xcode项目
- 构建正规工程项目
- 使用大型框架,追求设计质量与代码组织
- 目前oc采用的LLVM-Clang的编译架构
类与对象
使用#import和#include的区别
- import是oc语音的头文件导入,它能避免重复导入。确保头文件只会被导入一次。
- include如果不注意很容易会重复导入,出现相互包含的编译错误。推荐使用import来导入头文件。
类和对象
- 类是对某一事物的描述,是抽象的,而对象是一个实实在在的个体,是类的一个实例。
- 类是对象的抽象,而对象是类的具体实例。
类型系统(不同的类型)
- 引用类型(复制的指针)
- 类 class
- 指针 pointer
- 块 block
- 值类型(直接复制的数值)
- 基础数据类型,int float等
- 结构 struct
- 枚举 enum
- 类型装饰
- 协议 protocol
- 类别 category
- 扩展 extension
类class和结构体struct的区别
- 从类型和实例关系来看
- class是类(类型)和对象(实例)
- struct是结构(类型)和值(实例)
- class是引用类型,位于栈上的是指针(引用),位于堆上的是实体对象
- struct是值类型,实例(值)内存直接位于栈中
栈与堆的区别
内存管理的范围
- 任何继承了NSObject的对象
- 对其他非对象类型无效
- 即只有oc对象需要进行内存管理,非oc对象类型比如基本的数据类型不需要进行内存管理
引入栈和堆的概念
栈(stack)
- 栈区(stack)是由编译器自动分配并释放,存放函数的参数值,局部变量等。栈是系统数据结构,对应线程/进程是唯一的。优点是快速高效,缺点是有限制,数据不灵活(先进后出)。
- 栈空间有两种分配方式:
- 静态分配是由编译器完成,比如局部变量的分配
- 动态分配由
alloca
函数完成,动态分配是无须释放的(自动释放)
堆(heap)
- 堆(heap)是由程序员分配和释放的,如果不释放,程序结束时,可能会由操作系统回收 ,比如在ios 中 alloc 都是存放在堆中。优点是灵活方便,数据适应面广泛,但是效率有一定降低。
- 堆空间的分配总是动态的。
- 操作系统使用stack 段中的指针值访问heap 段中的对象。如果stack 对象的指针没有了,则heap 中的对象就不能访问。这也是内存泄露的原因。
只有OC对象才需要进行内存管理的原因
- OC对象在内存中是以堆的方式分配空间的,并且堆内存是由程序员释放的,就是release。OC对象存放于堆里面(堆内存要程序员手动回收)。堆里面的内存是动态分配的,所以需要程序员手动的去添加内存、回收内存。
- 非OC对象一般放在栈里面,栈内存会被系统自动回收。
stack 对象的优点主要有两点,一是创建速度快,二是管理简单,它有严格的生命周期。stack 对象的缺点是它不灵活。创建时长度是多大就一直是多 大,创建时是哪个函数创建的,它的owner 就一直是它。不像heap 对象那样有多个owner ,其实多个owner 等同于引用计数。只有 heap 对象才是采用“引用计数”方法管理它。
总结区别
- 按管理方式分
- 对于栈,是由系统编译器自动管理,不需要程序员手动管理
- 对于堆,释放工作由程序员手动管理,不及时回收容易产生内存泄露
- 按分配方式分
- 堆是动态分配和回收内存的,没有静态分配的堆
- 栈有两种分配方式:静态分配和动态分配
- 静态分配由系统编译器完成的,比如局部变量的分配
- 动态分配由alloc函数进行分配的,但是栈的动态分配和堆不同,它的动态分配也由系统编译器进行释放,不是程序员手动管理。
两个运行内存图
数据成员:属性与实例变量
类型成员
- 数据成员 data member 描述对象状态
- 实例变量 instance variable
- 属性 propety
- 函数成员 function member 描述对象行为
- 方法 method
- 初始化器 init
- 析构器 dealloc
实例变量和属性的区别
- 大括号括起来的是instance variable(实例变量),只是简单的数值,不能绑定get/set方法,不能自动retain/copy/atomic。相当于一个简单的跟着instance走的局部变量
- propety则是对get/set方法的语法封装,将两个方法的声明变成一个propety声明,并且通过标注各种attribute来完成许多基本任务。
属性认识
- 属性表达实例状态,描述类型的对外接口。相比直接访问实例变量,属性可以做更多的控制。
- 默认情况下,编译器会为属性定义propetyName自动合成:
- 一个getter访问器方法:propertyName
- 一个setter访问器方法:setPropertyName
- 一个实例变量_propertyName
- 可以自定义访问器方法,也可以更改访问器方法名、或实例变量名
- 可以使用静态全局变量(C语言)+类方法,模拟类型属性
getter和setter方法
面向对象的语言一般都会有setter和getter器。在oc中一般这样写:
在.h中声明
-(void)setAge:(int)newAge;
-(int)age;
在.m文件中具体实现
-(void)setAge:(int)newAge {
age = newAge;
}
-(int)age {
return age;
}
- 调用方法,一般才用中括号[]和点.的方式调用。
- 之后引用了@property来改进setter和getter。但是必须声明与之对应的实例变量。
- 当编译器遇到@property的时候,会自动展开getter和setter的声明
- 在m文件中,@synthesize会自动生成getter和setter的实现。@synthesize会去访问str同名的变量,如果没有找到就会错。(所以必须声明与之对应的实例变量)
- 之后的版本中,不需要为属性声明实例变量,@synthesize如果找不到会自动生成一个同名的私有同名变量。
- 再之后的版本中,直接省略@synthesize,因为编译器可以自动为属性生成setter和getter方法以及以下划线开头的实例变量_propertyName。
- 目前,
@property (nonatomic, copy) NSString *str;
这句话完成了3个功能:
1)生成_str成员变量的get和set方法的声明;
2)生成_str成员变量set和get方法的实现;
3)生成一个_str的成员变量。(注意:这种方式生成的成员变量是private的) - 设置访问方法的名字,默认的名字是setPropertyName和propertyName,可以通过设置@property的getter和setter属性来修改setter和getter器的方法名。
@property (nonatomic,copy,getter = show1,setter = show2:) NSString *str
属性重写setter和getter方法
- 如果只重写setter和getter其中之一,可以直接重写
-(void)setStr:(NSString *)str {
_str = @"只重写setter方法";
}
- 如果要同时重写setter和getter。需要加上
@synthesize propertyName = _propertyName;
不然系统不认_str
。
如果同时重写了setter和getter方法,系统就不会帮你自动生成这个_str
。
@synthesize str = _str;
-(void)setStr:(NSString *)str {
_str = @"同时重写setter和getter";
}
-(NSString *)str {
return _str;
}
实例变量
- 可以定义实例变量,而不定义属性,只有实例变量没有类变量
- 如果自定义了getter和setter访问器方法,或者针对只读属性定义了getter访问器方法,编译器将不再合成实例变量,需要自己声明。
- 在类外一律使用属性来访问,类内大多也通过self使用属性访问,以下情况使用实例变量来访问:
- 初始化器init
- 析构器dealloc
- 自定义访问器方法
- 什么时候使用实例变量什么时候使用属性呢?参考这篇文章:iOS巩基之 不再纠结实例变量&属性
属性的描述特性
- 读写特性
- 读写属性 readwrite 默认
- 只读属性 readonly
- 多线程特性
- 原子性atomic(默认)
- 非原子性 nonatomic
- 内存管理特性
- ARC环境
- 强引用strong
- 弱引用 weak 阻止循环引用
- 拷贝属性 copy 创建独立拷贝
- 其他情况
- retain
- assign
- unsafe_unretained
- ARC环境
参考这篇文章关于属性的特性总结:
1.OC属性总结笔记
2.OC 属性特性assign,retain
函数成员:方法
- 方法是类的成员函数,表达实例行为或类型行为
- 所有方法默认为公有方法,没有private和protected方法
- 动态消息分发:方法调用通过运行时动态消息分发实现,在对象上调用方法又称为"向对象发送消息"
- OC 的方法调用就是编译器通过 objc_msgSend()进行消息分发。下面两行代码是等价的:
[array insertObject:foo atIndex:0];
objc_msgSend(array, @selector(insertObject:atIndex:), foo, 0);
- 动态消息分发属于oc runtime机制,比较复杂。单独在分析。
runtime
- Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时做的事放到了运行时来处理。
- 这种特性意味着OC不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。对于OC来说,这个运行时系统就像一个操作系统一样。这个运行时系统即Runtime,是用C和汇编写的,这个库使得C语言有了面向对象的能力。其中最主要的是消息机制。对于C语言,函数的调用在编译的会决定调用哪个函数,编译完成之后顺序执行,无任何二义性。OC的函数调用称为消息发送,属于动态调用过程。在编译的时候并不能决定真正的调用哪个函数(事实证明,在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。而C语言在编译阶段就会报错。)只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
- Runtime库主要做下面几件事:
1.封装:在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上一些额外的特性。这些结构体和函数被Runtime函数封装后,我们就可以在程序运行时创建、检查、修改类、对象和他们的方法了。
2.找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),Runtime会根据消息接收者是否能响应该消息而做出不同的反应。
实例方法和类型方法
- 实例方法的self指针代表当前实例对象,实例指针
- 类方法的self指针代表当前类
方法参数
- 注意值类型和引用类型的不同参数类型对外部的影响。
- 注意方法参数名,内部参数和外部参数,第二个参数名开始需要显示的提供外部参数名。
- 调用时,第一个参数名忽略,后面的参数名必须显示标明。
初始化器和析构器
认识初始化器和析构器
- 初始化器用于初始化对象实例或类型,是一个特殊的函数
- 对象初始化器
-(id) init
可以重载多个 - 类型初始化器
+(void)initialize
只能有一个
- 对象初始化器
- 析构器用于释放对象拥有的资源,无返回值的函数
- 对象析构器
-(void)dealloc
只能有一个 - 没有类型析构器
- 对象析构器
对象初始化器
初始化对象实例时,init通常和alloc搭配使用
-
alloc所做的事情-NSObject已实现
- 在堆上分配合适大小的内存
- 将属性或者实例变量的内存置0
-
init所做的事情 - 可以自定义
- 调用父类初始化器[super init](前置调用)
- 初始化当前对象实例变量(注意使用实例变量,不要使用属性)
new相等于调用alloc/init的无参数版本。
初始化器内部使用实例变量不使用属性。
id或者instancetype类型的返回值。
指定初始化器(真正实现的初始化),便捷初始化器(会调用指定初始化器)?
指定初始化方法和便利构造器
指定初始化方法
- 无论调用哪一个初始化方法都会调用的初始化方法,称为指定初始化方法。
- 一般会写一个传入参数最多的init方法,然后其他方法调用这个指定初始化方法即可。
- (id)initWithName:(NSString *)name
{
//调用指定初始化方法
return [self initWithName:name age:0 sex:'0' height:0];
}
//指定初始化方法
- (id)initWithName:(NSString *)name age:(NSInteger)age sex:(char)sex height:(CGFloat)height
{
self = [super init];
if (self) {
_name = name;
_age = age;
_sex = sex;
_height = height;
}
return self;
}
便利构造器
- 便利构造器就是一个类方法(+号方法),内部实现是封装了alloc和初始化操作,创建对象更加方便快捷。
- 定义便利构造器有以下规则:
- 便利构造器是“+”方法。
- 返回本类型的实例。
- ⽅法名以类名开头。
- 可以有0到多个参数。
+(id)PersonWithName:(NSString *)name age:(NSInteger)age sex:(char)sex height:(CGFloat)height
{
Person *p = [[Person alloc] initWithName:name age:age sex:sex height:height];
return p;
}
类初始化器
- 类初始化器initialize负责类型级别的初始化
- initialize在每个类使用之前被系统自动调用,且每个进程周期中,只被调用一次。
- 子类的initialize会自动调用父类的initialize(前置调用)
对象析构器
- 对象析构器dealloc负责释放对象拥有的动态资源
- 自动实现:ARC将对象属性引用计数减持
- 手动实现:释放不受ARC管理的动态内存,如malloc分配的内存
- 手动实现:关闭非内存资源,如文件句柄,网络端口
- dealloc由ARC根据对象引用计数规则,在释放对象内存前自动调用,无法手工调用
- 子类的dealloc会自动调用父类的dealloc(后置调用)
继承与多态
认识面向对象
- 封装 encapsulation
- 继承 inheritance
- 多态 polymorphism
继承
- 继承:每一个类只能有一个基类,子类自动继承基类
- 实例变量
- 属性
- 实例方法
- 类方法
- 所有类的根类:NSObject
- 继承的两层含义:
- 成员复用:子类复用基类成员
- 类型抽象:将子类当做父类来使用
运行时多态 polymorphism
- 多态:子类在父类统一行为接口下,表现不同的实现方式
- 对比重写和重载
- 子类重写父类同名同参数方法
- 方法名相同、参数不同,OC不支持方法的重载
- 在子类的代码中,可以使用super来调用基类的实现
- self具有多态性,可以指向不同的子类
- super没有多态性,仅指向当前父类
继承中的init和dealloc
- 初始化器init
- 子类自动继续基类的初始化器
- 子类也可以重写基类初始化器,此时子类初始化器必须首先调用基类的一个初始化器(手工调用)
- 析构器dealloc
- 子类可以选择继承基类析构器,或者重写基类析构器
- 子类析构器执行完毕后,会自动调用基类析构器(后置调用,且不支持手工调用)
- 子类析构器自动具有多态性
- Tips:尽量避免在父类init和dealloc调用子类重写的方法。
在我的博客中,点我