带你快速了解IOS开发之OC语言

948 阅读18分钟

前言

本文阅读耗时15分钟,主要是介绍OC的一些语法,类如何写以及内存管理。不懂OC的看完也能看懂实际含义了。
文章目的方向其实还是研究Rust在端上的应用,下一篇会记录Swift的语法,再是IOS开发,Rust 在IOS上的表现。

目录

带你快速了解IOS开发之OC语言.png

一、OC概述

Next(Next也是上任苹果计算机CEOSteve Jobs1985年离开苹果计算机后所创立的公司)研发的一种操作系统NextStep
后来,在1996年,苹果公司出于战略考虑收购了Next公司,自然乔帮主又回到了苹果公司。基于NextStep code library,苹果公司把它集成到了自己的系统中去,也就是现在的MAC OS X可以说NextStep 为苹果mac系统的发展奠定了基础。
NextStep核心code就是Object-C写的, 所以你能看到OC中命名都是NS开头。

OCC有啥区别?

  • OC基于C,新增了一小部分面向对象语法
  • C中复杂的语法封装的更简单
  • OC里面直接写C语言,也是没有问题的
  • OC程序的源文件后缀名是.mC程序是.c
  • main函数依旧是两者的入口和出口
  • OC#import 类似C语言的#include ,但是#import只会导入一次头文件,再也不用像C一样,要对导头文件进行处理

二、OC的一些基本语法

OC编译

cc -c xxx.m //编译源文件得到.o文件
cc xxx.o  // 或者cc xxx.o -framework(框架名称)  和c一样,生成.out文件,执行即可运行

OC数据类型

该有的都有,如基本类型(intdoublefloatchar)、构造类型(数组、结构体、枚举)、指针类型、空类型(void)typedef 自定义类型、BOOL类型、Boolean类型、class类型、id类型、指针、nilSELblock代码段

typedef

将长类型定义为短类型

typedef unsigned long long int  bigdata;
bigdata test = 10;

BOOL、Boolean类型

BOOL b = YES;
BOOL c = NO;
Boolean d = true;

具体的定义如下

#define YES ((BOOL)1)
#define NO ((BOOL)0)

typedef unsigned char Boolean
#define true 1
#define false 0

所以YES就是1Boolean其实就是 char

class

说到class,就要体OC中的类以什么形式存储在代码段中了
任何存储在内存中的数据都有一个数据类型
先在代码段中创建一个Class对象,ClassFoundation框架的1个类。将类的信息存储在这个Class对象之中。 包含基本的类名、属性、方法、isa。这个Class对象,我们称为类对象。 如何拿到类对象?

Class c1 = [Person class];
或者
Person *p1 = [Person new];
Class c2 = [p1 class];

id

Animal *obj = [Animal new];
[obj sayHi];
id id2 = [Animal new];//id是 typedef自定义类型
[id2 sayHi];

nil和NULL

NULL就是0, nil 只能作为指针变量的值,代表指针变量不指向内存的任何空间。((void *)0)也等价于0
一般C使用NULL,OC使用nil,并且在OC中,Person *p1 = nil, 通过p1访问属性会报错,但是访问方法不会报错,就是不会执行而已。

SEL

这个主要是用来找这个类中是否有这个方法,方法的本质就是selector选择器,主要通过selector来查找。

SEL s1 = @selector(syaHi); //拿到SEL对象

block

block是一个数据类型,专门存储一段代码,这段代码可以有参数和返回值

返回类型(^block变量名称)(参数列表);
1.声明
    void (^myBlock1)();
    int (^myBlock2)(int num1,int num2);
    代码段格式
    ^返回值类型(参数列表){

    };
2.实现
    myBlock1 = ^void(){
      NSLog(@"hello");  
    };
    和变量一样定义就行
    myBlock1();//执行
3.简单的定义和使用
    int (^myBlock3)(int a, int b) = ^int(int a, int b){
        return a + b;
    };
    int result = myBlock3(2, 4);
4.block简写
    4.1 代码段没有返回值可以省略void,没有参数可以省略()
    void (^block)() = ^{};
    代码段没有参数
    int (^block2)() = ^int{
        return 1;
    };
    4.2声明block的时候,参数可以不写名字
    int (^block3)(int, int) = ^int(int a, int b){
        return 1;
    };
    4.3无论代码段是否有返回值,都可以不写返回值
    int (^block3)(int, int) = ^(int a, int b){
        return 1;
    };

static

C语言中的static 可以修饰局部变量,全局变量、函数
OC中,static不能修饰属性和方法,可以修饰方法中的局部变量(存储在常量区,不会回收,下次执行这个方法的时候会直接使用,不会声明)
这里可以看看后面类的单例章节怎么写的。因为它存储在常量区,不会被回收,所以下次的时候直接用。

异常

OC的异常处理不是万能的,C语言的异常是无法处理,以及一些nil对象的属性调用也无法处理。

@try{

}
@catch(NSException *ex){

    NSLog(@"catch的错误是 %@", ex);

}@finally{ 
}

三、类与方法

类与方法实现

@interface Person:NSObject
{
    NString *_name;
    @public
    int _age;
}
- (void) run; //对象方法
-(void)run:(int)name;
- (int)sum:(int)num1 :(int)num2;
+ (instancetype)person;//类方法,就比如java的静态方法
@end

//类的实现
@implementation Person

- (void) run{
}
- (void) run:(int)name{
}
- (int)sum:(int)num1 :(int)num2
{
    return num1 + num2;
}
+ (instancetype)person
{
    return [self new];
}
@end

//调用
Person *p1 = [Person new];
[p1 run];//方法调用
[p1 run:@"one-thread"];//有参数的方法调用
p1-> _age;//方法属性,_name是私有的,访问不到
[p1 sum:1:2];

Person *p2 = [Person person];//类方法

匿名对象

[[Person new] run]; //和其他语言一样

修饰符

修饰符有四种类型,分别是@private@protected@public@package (当前框架中访问)。 默认是私有的,且修饰符只能修饰属性。
下面两个属性ab都是公有的,c是私有的

@interface Person : NSObject{
    @public
    int a;
    int b;
    @private
    int c;
}
@end

你肯定会问,既然只能修饰属性,那方法私有怎么处理啊?我们直接在类的声明中把方法声明去掉就可以了,这就代表只能本类中使用。

//类的声明
@interface Person:NSObject
@end

//类的实现
@implementation Person
-(void) run{
}
@end

可能你还会问,如何实现类的打印?

Person *p1 = [Person new];
NSLog(@"%@", p1); //这样打印出来的不是我们想要的对象中的属性数据

只需要将Person实现NSObjectdescription方法即可,类似重写javatoString方法

- (NSString *)description
{
    return [NSString stringWithFormat:@"姓名:%@ 年龄%d", _name, _age];
}

你可能又会问NSLog 是干嘛的?

NSLog

NSLogFoudation框架里的,是printf函数的增强版,会引入一些调试相关的信息,就像AndroidLog一样,有时间、TAG信息,并且在输出后会自动换行,针对新增的数据类型,需要使用NSLog
它与C的字符串相比,OC需要加前缀@,NSString *str = @"jack";包括字符串输出,NSLog(@"hello, %@", s);
它的定义是 @interface NSString:NSObject<NSCoping, NSMutableCoping,NSSecureCoding>所以本质上字符串是用NSString对象来存储的。也可以通过NSString *str =[NSString new],内容是@"",或者 NSString *str1 = [NSString string];

常用的类方法有
(instancetype)stringWithUTF8String(const char *) //将c语言的字符串转换成OC字符串对象
(instancetype)stringWithFormat:(NSString *)format, ... //拼接OC字符串

了解了NSLog,你可能会比较好奇,OC中类是怎么存储的。

类以什么形式存储在代码段中

任何存储在内存中的数据都有一个数据类型
先在代码段中创建一个Class对象,ClassFoundation框架的1个类。将类的信息存储在这个Class对象之中。
包含基本的类名、属性、方法、isa。这个Class对象,我们称为类对象。

Class c1 = [Person class];
或者
Person *p1 = [Person new];
Class c2 = [p1 class];

其实对象中isa就是Class对象地址。并且c1 c2 就代表Person类,类方法完全可以通过c1c2调用。

oc_1.PNG

方法的本质

Person *p1 = [Person new];
[p1 sayHi];

先拿到sayHi方法的SEL对象。再将SEL消息发送给p1对象,p1 接收到SEL消息后,就知道要调用方法。
然后根据对象的isa指针找到存储类的类对象
找到类对象后,在这个类对象中去查找是否有和传入的SEL数据匹配的,有就执行,没有就从父类去找,直到NSObject

set、get方法

我们会使用点语法来调用getset方法

@interface Person: NSObject
{
    NSString *_name;
}
- (void)setName(NSString  *)name;
- (NSString *)name;
@end
@implementation Person
- (void) setName:(NSString *)name{
    _name = name;
}
- (NSString *)name
{
    return name;
}
@end

我们就可以使用点语法来调用,来读取属性了,它的本质就是调用getset方法

Person *p = [Person new];
p.name = @"张三";
NSString * testName = p.name;

那有没有办法让代码自动生成getset方法呢?

@property

@interface Person : NSObject
{
	NSString *_name;
	int _age;
	int _sex;
}
- (void)setName:(NSString *)name;
- (NSString *)name;
@property int age;
@property int sex;//标记了,会对应有一个_sex属性
@end
@implementation Person
{
- (void)setName:(NSString *)name
{
    _name = name;
}
 - (NSString *)name
 {
 	return name;
 }
 @synthesize age;//不用定义age也可以,它也会自动生成一个私有属性
 @synthesize sex = _sex; //这样写没有私有的属性
}
@end

我们可以利用 @property(相当于对get、set方法的定义) + @synthesize(类视线中,对属性的get、set方法的实现)
还是太麻烦了,声明和实现中都要写一遍。
怎么办?XCode 4.4 后,只需要写一个@property就会自动生成私有属性以及get、set方法声明和实现了。

@interface Person:NSObject
@property NSString *name; //自动生成的属性是_name
@end

对象的其他创建方式

Person *p1 = [Person new];
等价于
Person *p2 = [[Person alloc]init];

类如何继承

@interface Father:NSObject
@end
@interface Son:Father
@end

之前了解过了类如何存储的,并且有一个isa指针,那么子类也有的,子类的isa指针指向了Son Class代码段,Son Class中也有一个isa指针,指向了 Father Class
和其他语言一样,子类当中拥有父类的所有属性及方法。

协议

协议也可以理解为接口,专门声明一大堆方法的,不要认为是泛型,因为加了这个,子类要遵守协议,要实现协议里面的方法。

@protocol MyProtocol <NSObject>
//方法声明
-(void)run;
- (void)sleep;  //想要子类一定要实现,可以强制编译器报警告,可以使用@required -(void) sleep;另一个@optional 可以不报警告
@end
@interface Dog:NSObject<MyProtocol>
@end

//不实现也不会报错
@implementation Dog
- (void)run{
    
}
- (void)sleep{
    
}
@end

单例

单例,顾名思义,只能有一个对象,在OC里也是一样的。

非安全写法

@implementation Person
+ (instancetype)allocWithZone(struct _NSZone *)zone
{
	static id instance = nil;
	if(instances == nil){
        instance = [super allocWithZone:zone];
	}
    return instance;
}
+ (instancetype)defaultPerson{
  return [self new];  
}
+ (instancetype)sharedPerson{
    return [self new];
}
@end
Person *p = [Person new];
Person *p1 = [Person defaultPerson];

安全写法

+ (instancetype)allocWithZone(struct _NSZone *)zone
{
	static id instance = nil;
	@synchronized(self){
        if(instances == nil){
            instance = [super allocWithZone:zone];
        }
	}
    return instance;
}

推荐GCD安全写法

对象的代码在整个程序运行期间只被执行一次

+ (instancetype)sharedPerson()
{
	static id instance = nil;
	static dispatch_once_t onceToken;//声明一个静态的gcd单次任务
	dispatch_once(&onceToken, ^{
        if(instances == nil){
            instance = [[Perosn alloc]init];
        }
	});
    return instance;
}

也可以直接仿照系统[NSFileManager defaultManager] 单例实现。

四、内存管理

针对类的介绍,基本可以让你看懂OC代码了,但是其中的一些细节原理,还是要多写写代码。肯定要了解一下内存管理

内存的五大区域

  • : 局部变量,出了作用域后会被立马被系统回收
  • OC对象,使用C函数申请的动态空间。也是不会自动回收,直到程序结束的时候才会回收
  • BSS段:未初始化的全局变量和静态变量。一旦初始化就会回收,并转存到数据段之中。
  • 数据段:已经初始化的全局变量、静态变量。直到程序结束才会被回收
  • 代码段:代码。程序结束的时候,系统会自动回收存储在代码段中的数据。

引用计数

对象内部持有retainCount属性,即引用计数 unsigned long 8个字节,代表有多少人使用这个对象。这个引用计数器也是可以操作的,为对象发送retain消息,引用计数加1,为对象发送release消息,引用计数减1。当为0的时候,对象就会被系统立即回收,自动调用对象的dealloc方法。为对象发送retainCount消息,就可以获取到值。

MRC

Manual Reference Couting 手动内存管理,这个在XCode上,可以打开关闭这个功能,现在基本都是ARC了吧。ARC也是从XCode 4.2 后支持的。
内存管理原则

  • 有对象的创建,就要匹配一个release
  • retain次数和release要匹配

对象内存释放的时候

//给Person加个打印
@implementation Person
-(void)dealloc
{
	NSLog(@"回收Person自己的内存方法重写");
	[super dealloc];//这行一定要放在最后一行
}
@end
Person *p1 = [Person new];//创建的时候自己会reatin
[p1 release];//释放的时候,发现引用计数器为0了,就会调用dealloc

野指针

  • C语言:定义一个指针变量,没有初始化,指向1块随机的空间,这个就叫野指针。
  • OC: 指针指向的对象已经被回收了,这样的指针叫做野指针。

对象回收本质

申请一个变量,实际上就是向系统申请指定字节数量的空间。这些空间系统不再分配给别人了
当变量被回收时候,代表变量占用的字节空间从此以后系统就可以分配给别人使用了。
但是字节空间中存储的数据还在。
回收对象:所谓对象的回收,指的是对象占用的空间可以分配给别人。当这个对象没有分配给别人之前,其实对象数据还在。

僵尸对象

一个已经被释放的对象,但是这个对象所占的空间还没有分配给别人,这样的对象叫做僵尸对象。我们通过野指针去访问僵尸对象的时候,有可能没问题,有可能有问题,并且无法复活僵尸对象。

僵尸对象的实时检查机制,可以将这个机制打开(XCode 配置中),打开之后,只要访问的是僵尸对象,无论空间是否分配,就会报错。但是打开后,每访问一个对象的时候都会检查这个对象是否是1个僵尸对象,非常消耗性能的。

内存泄漏

内存没有被及时的回收,主要表现在有对象创建而没有对象的release。或者MRC的时候,retain次数和relase次数不匹配。并且在没有调用release的时候,直接赋值后,也会导致内存泄漏。
例如给Person加个Car

- (void)setCar(Car *)car{
    if(_car != car){//将之前的对象先release后,再reatin
        [_car release];
        _car = [car retain];
    }
}
//重写dealloc
- (void)dealloc
{
    [_car release];
    [super delloc];
}

Person *p = [Person new];
Car *car = [Car new];
p.car = car;

[car release];
[p release];

针对上面示例代码的写法(属于标准的MRC内存管理写法),@property也为我们准备了自动实现

property

@interface Person : NSObject{
	Car *_car;
}
@propperty car;
@end

上面这种写法默认的set方法就是直接将 _car = car;,所以@property提供了参数给我们实现标准的MRC管理。它有四组参数,分别是
atomic、nonatomic
默认值atomic,生成的set方法会被加上线程安全锁,当然对应的右nonatomic(不加锁)

@interface Person : NSObject
@property(nonatomic) Car * car;
@property(atomic) Car * car2;
@end

assign、retain
默认值assign,就是直接赋值, retain生成的实现,就符合标准的MRC内存管理

@interface Person : NSObject
@property(nonatomic, retain) Car * car;
@end
//自己还要实现相关的销毁策略
@implementation Person
- (void)dealloc
{
    [_car release];
}
@end

readonly、readwrite
默认值readwrite,对应的getset方法都会生成,readonly就只会生成get方法

@interface Person : NSObject
@property(nonatomic, retain, readonly) Car * car;
@end

get、set
默认情况下,生成的getset方法的名字都是标准的。

@interface Person : NSObject
@property(nonatomic, retain, readonly, getter=xxx, setter=yyy:) Car * car; //对应的get方法的方法名就是xxx, setter方法名也是一样的。
@end

循环引用

当两个类相互包含的时候,就会出现循环引用,造成无限递归问题。
比如下面的Book持有PersonPerson持有Book
解决方法第一步,先避免编译失败问题,就是不要import.h头文件,直接使用@class定义。

//book.h 文件
//#import "person.h"
@interface Book : NSObject
@property(nonatomic,retain)Person *person;
@end

//person.h 文件
//#import "book.h" 在person.m实现中再引入 book.h,这样方法也能有提示
@class Book
@interface Person : NSObject
@property(nonatomic,retain)Book *book;
@end

import是会将指定文件的内容拷贝到写指令的地方,而@class并不会拷贝内容,只是告诉编译器这是一个类。
解决第二步就是,将其中一个改为assign

Person的dealloc方法
{
    [_book relase];
    [super dealloc];
}
Book的dealloc方法
{
	[_person release];
	[super dealloc];
}
//下面是都是retain的示例过程
Person *p = [Person new];
Book *book1 = [Book new];
//这时候book和 p 引用计数都是1
p.book = book1;   // book1的引用计数变成了2
book1.person = p;//这时候p的引用计数也变成了2

[book1 release]; //book1 计数变成1
[p release]; // p的计数变成1
//最后对象都没销毁,都没有执行到dealloc方法

两个对象相互引用的时候,只要一端使用retain ,另一端使用assign,就不会循环引用。

autorelase

存入到自动释放池中的对象,在自动释放池被销毁的时候,会自动调用存储在该自动释放池中的所有对象的release方法。
注意每次对象只能加入一次自动释放池中。

@autoreleasepool{
    Person *p = [Person new];
    [p autorelease];//将p对象存储到当前的自动释放池中
    Person *p1 = [[Person new] autorelease];//自动释放池中调用1个对象的autorelease多少次,就会将这个对象存储到自动释放池多少次。并发多少次的release消息
}

当然不想写autorelease的话,可以为类,写一个同名的类方法,用来快速生成一个类对象,在类方法中实现autorelase,则通过类方法创建的对象自动会被加入到autorelease

ARC

Automatic Reference Couting 自动内存管理,系统自动的在合适的地方发送retainrelease消息。
本质也是引用计数器为0的时候,自动释放。对于我们开发而言,只要没有强指针指向这个对象,这个对象就会立即回收。
ARC下,不要写retainreleaseautorelease,以及手动调用dealloc函数。但是有时候避免不了,在维护项目的时候,会发现有些类的编译模式是MRC机制,一般就会造成MRCARC共存,在开发的时候需要看清楚了。

强、弱对象

Person *p1;这就是强类型指针
__strong Person *p1;显示标记这是强类型指针
__weak Person *p2;显示标记这个是弱指针

举个例子

int main(int argc, const char * argv[]){
	__waak Person *p2;
	{
    __strong Person *p1 = [Person new];
    p2 = p1;
    }//这里就释放了,出了作用域就会被释放
    //也会被回收,因为p2是弱指针
}

如果我们想针对属性指定强弱类型,可以这样做

@property(nonatomic,) Car *car;
等价于
@property(nonatomic, strong)Car *car;

弱类型可以这样写
@property(nonatomic, weak)Car *car;

五、分类

看到这样的代码也不要惊讶,例如下面的,Student是类名,company是分类名。通过XCode可以创建。它肯定还写了一个Student类在其他定义了。可以找找看。
分类相当于所属模块的意思,将一个类分成多个模块。但是记得不要在分类中使用@property属性,因为它不会自动生成实现。而且分类之间属性只能通过get、set方法来相互访问。针对分类和本类同名方法,会优先调用分类的方法。如果多个分类有同名方法,会优先调用最后编译的分类中的方法
并且分类中只能新增方法,不能加属性。

@interface Student(company)
@property(nonatomic, assign)int age;//只会生成属性的get 、set声明
@end
@implementation Student(company)
@end

延展

延展这个特殊的分类没有名字。只有声明没有实现。和本类共享一个实现。

@interface 本类名()
@end

通过XCode创建的文件名是 : 本类名_文件名.h
作用:相当于java的接口,属性和方法都可以添加,可以用来写私有方法和属性
一般写在.m文件里,和实现类放在一起。但是延展可以用@property,它会在本类生成set实现。
例如

//本类
@interface Person :NSObject
@end
//延展
@interface Person()
@property(nonatomic,assign)float weight; //延展会在本类的实现中生成
- (void)run
- (void)sleep();
@end

@implementation Person
- (void)run
{}
- (void)sleep
{}
@end

Person *p1 = [Person new];
[p1 run];
[p1 sleep];

如果要实现私有属性,外部访问不了,可以这样实现

//放到一个文件内
@interface Person()
@property(nonatomic,assign)float weight;  
- (void)run
- (void)sleep();
@end

@implementation Person
- (void)run
{}
- (void)sleep
{}
@end

六、常见的一些集合

NSMutableString

对象来存储字符串,内容可以修改
NSMutableString  *str = [NSMUtableString string];
[str appendString:@"jack"];
[str appendString:@"rose"];
[str appendFormat:@"&d岁", age];

copy

NSString copy 浅拷贝,引用计数器加1, mutableCopy是可变对象,深拷贝
NSMutableString copy 新对象,深拷贝,但是copy后是一个不可变的字符串NSString类型, mutableCopy也是深拷贝,也是可变对象
针对NSMutableString等可变的对象属性,我们可以利用@property(copy) 自动会在set中调用copy函数

针对自定义类没有copy方法,我们就需要实现NSCopying的copy方法

NSArray、NSMutableArray

NSArray * arr = [NSArray new];
NSArray *arr1 = [[NSArray alloc] init];
NSArray *arr2 = [NSArray arrayWithObject:@"jack"];
NSArray *arr3 = @[@"jack",@"data"];
子类 NSMutableArray,元素可以动态增加和删除
NSMutableArray *arr4 = @[@10,@20,@30]; //@10其实就是NSNumber
NSDictionary与NSMutableDictionary,例如
NSDictionary *dict = @{@"name":@"rose", @"age":@"18"};

使用@[] 或者@{} 创建的集合已经是被atuorelease的了
直接调用和类同名的类方法创建的对象也是被autorelease的了

NSDictionary、NSMutableDictionary

这个和Map一样,一个是不可变的,一个是可变的。