• 0

  • 509

iOS 一些基础概念

1星期前

类别和类扩展 结构体 KVO、KVC、Notification 单例 指针 其他 面试必读

类别和类扩展

类别(Category)

  • 作用
    增强已有类的功能。使用分类,我们可以在不破坏原有类的结构的前提下,对原有类进行模块化的扩展。
    为继承自某类的所有子类添加公有的方法(因为类别中的方法可以被所以子类继承)
    向对象添加非正式协议。
  • 局限性
    1. 无法向现有的类中添加新的实例变量(类别没有位置容纳实例变量);
    2. 方法名称冲突,即类别中的新方法的名称与现有类中方法名称重名,类别具有更高的优先级,类别中的方法将完全取代现有类中的方法(再也无法访问现有类中的同名方法)。
  • Category使用和注意
    1. Category中的方法如果和当前类中已有方法完全相同,那么Category相当于重写覆盖掉类中的方法。因为Category优先级最高,当重写了类的方法时,系统会调用Category里的重写方法。
    2. Category拓展的方法按照有没有重写当前类中的方法,分为未重写的拓展方法和重写拓展方法,如果类要引用Category中未重写的拓展方法,必须引入Category的声明文件。且类引用自己的Category时,只能在.m实现文件中引用(如果在.h声明文件中引用自己的类别,会因为头文件原因造成死循环),子类引用父类的类别,在.h或.m文件中引用皆可。如果类引用Category中的重写方法,不用引入Category声明文件,系统会自动调用Category的重写方法
    3. Category中如果重写了A类从父类继承来的方法s,理论上不会影响同级类(比如B类,A、B继承了同一个父类)中的方法s
    4. 子类会不会继承父类的Category:Category中的重写方法会对子类造成影响,但是子类不会主动继承父类的Category中的非重写拓展方法。但是在子类中引入父类Category的声明文件后,子类就会继承Category里的非重写拓展方法。注意,是继承。而继承的具体表现就是:当子类里的方法和父类Category中的方法完全相同(这里的相同只除了方法体外的其他地方相同)时,那么子类里的方法会覆盖掉父类Category,因为这相当于子类重写了继承自父类的方法
    5. Category 的影响是向下有效的,即会影响到该类所有子类。比如A类和B类继承自Super类,为A添加了一个类别Gtr,那么能使用该类别的仅限A类以及A的所有子类。B类是不能使用类别Gtr的扩展(Extension)
    6. 当为一个类存在多个Category的时候,子类对象调用方法的时候真正执行的方法实现由编译顺序决定。
    7. 类别的方法中,不可以调用super方法。--类别的局限

类别使用

(1)对框架提供类的扩展(没有源码,不能修改)。 (2)不想生成一个新的子类的情况下,比如对 NSArray 的扩展。 (3)方便做项目管理,可以将一份源码在多个地方共享或者做方法版本管理、多人协作开发、用本地版本替换公共版本实现。 (4)用于把一个比较庞大的类,分割开来,具有相同功能的方法放到一个分类中 把太多的功能封装到一个类中,导致类文件过于庞大.

类别(Category)和扩展(Extension)的区别

  1. 类别只能扩充方法,不能扩展属性和成员变量(如果包含成员变量会直接报错);如果分类中声明了一个属性,那分类只会生成这个属性的set、get方法声明,不会生成setter/getter方法,也就是不会有实现
  2. 类扩展一般只针对自定义的类,对于系统类增加类扩展(无法去直接改变系统文件里的代码,也就是属性无 setter && getter ,方法也不能实现)没什么意义。
  3. 分类是有名称的,类扩展没有名称

swift中的类扩展(无类别)

swift中没有分类这种写法。swif中只有扩展(Extensions),而且具有很强大的功能。

  • swift中扩展(Extensions)的说明:
    扩展就是向一个已有的类、结构体、枚举类型或者协议类型添加新功能。这包括在没有权限获取原始源代码的情况下扩展类型的能力(即逆向建模)。扩展和OC中的分类类似,不过更加强大,而且与OC不同的是,Swift 的扩展没有名字。
  • swift中扩展(Extensions)的优点:
    1. 可以把代码进行模块化区分,把功能性相同的代码放到一个扩展(Extensions)中。例如把VC中关于网络请求的处理单独放到一个扩展中,把表相关的代理方法、其他视图控件的代理方法、私有方法、公开方法、视图的创建处理等等都可以区分开放到各自的扩展(Extensions)中,这样控制器中的代码层次就非常的清晰明了。
    2. 与OC中的扩展不同,在Swift中,类的扩展里的方法、属性,在外部都可以使用。而且支持被继承。
    3. 注意扩展中不能有存储类型的属性,只可以添加计算型实例属性和计算型类型属性,或者也可以利用runtime给继承与OC中的类添加属性。

结构体

常用结构体

typedef struct _NSRange {
    NSUInteger location;
    NSUInteger length;
} NSRange;
复制代码
struct CGPoint {
  CGFloat x;
  CGFloat y;
};
typedef struct CGPoint CGPoint;
typedef CGPoint NSPoint;
复制代码

构建函数: NSMakePoint(10,9); CGPointMake(10,9);(常用) 快速打印使用函数: NSStringFromPoint(point)

struct CGSize {
  CGFloat width;
  CGFloat height;
};
typedef struct CGSize CGSize;
typedef CGSize NSSize;
复制代码

表示宽度和高度,同样有如下方法: NSMakeSize(10, 8); CGSizeMake(10, 8);(常用) NSStringFromSize(size);

struct CGRect {
  CGPoint origin;
  CGSize size;
};
typedef struct CGRect CGRect;
typedef CGRect NSRect;
复制代码

存储位置和尺寸,也就是一个矩形范围。
创建:
NSMakeRect(10, 10, 80, 80);
CGRectMake(10, 10, 80, 80);(常用)
打印:
NSStringFromRect(rect)

结构体和类有什么区别

  1. 结构体只能封装属性,而类不仅能封装属性还可以封装方法。
  2. 结构体分配在栈,类分配在堆(栈效率高,空间小,堆反之)。
  3. 结构体赋值是直接赋值,类(对象)赋值是指针赋值(对象的地址)。

KVO、KVC、Notification

KVC

KVC是键值编码(NSKeyValueCoding),一个非正式的 Protocol,提供一种机制来间接访问对象的属性。特点是通过指定表示要访问的属性名字的字符串标识符,可以进行类的属性读取和设置;

  • 对于kvc机制如何通过key寻找到value:
    当通过KVC调用对象时,比如:[self valueForKey:@”someKey”]时,程序会自动试图通过几种不同的方式解析这个调用。首先查找对象是否带有 someKey 这个方法,如果没找到,会继续查找对象是否带有someKey这个实例变量(iVar),如果还没有找到,程序会继续试图调用 -(id) valueForUndefinedKey:这个方法。如果这个方法还是没有被实现的话,程序会抛出一个NSUndefinedKeyException异常错误。(注:Key-Value Coding查找方法的时候,不仅仅会查找someKey这个方法,还会查找getsomeKey这个方法,前面加一个get,或者_someKey以及_getsomeKey这几种形式。)
    设计valueForUndefinedKey:方法的主要目的是当你使用-(id)valueForKey方法从对象中请求值时,对象能够在错误发生前,有最后的机会响应这个请求。

KVO

KVO是键值观察,就是基于 KVC 实现的关键技术之一。特点是利用键值观察可以注册成为一个对象的观察者,在该对象的某个属性变化时收到通知。
KVO使用:观察者添加 addobserver:forkeyPath:options:context:后,被观察者的keypath值发生变化(注意单纯改变值不会调用此方法,只有通过getter和sett来改变值才会触发KVO),就会在观察者里调用方法observerValueForKeyPath:ofobject:change:context:因此实现此方法来对KVO发出的通知做出响应.

具体用到过的一个地方是对于按钮点击变化状态的的监控。
比如我自定义的一个button
[self addObserver:self forKeyPath:@"highlighted"options:0 context:nil];
#pragma mark KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if([keyPath isEqualToString:@"highlighted"] ) {
        [self setNeedsDisplay];
    }
}
对于系统是根据keypath去取的到相应的值发生改变,理论上来说是和kvc机制的道理是一样的。
复制代码
  • KVO 内部实现原理:

    1. KVO是基于runtime实现的
    2. 当某个类的对象第一次被观察时,系统会在运行期动态的创建类的一个派生类,在这个派生类中重写基类中的任何被观察属性的setter方法。派生类在被重写的setter方法实现真正的通知机制(Person -> NSKVONotifying_Person)
  • 使用步骤:

    1. 注册观察者(为被观察这指定观察者以及被观察者属性)
    2. 实现回调方法
    3. 触发回调方法
    4. 移除观察者
  • KVO崩溃原因:
    被观察的对象销毁掉了(被观察的对象是一个局部变量)。
    观察者被释放掉了,但是没有移除监听(如pop等)。
    注册的监听没有移除掉,又重新注册了一遍监听。

KVO 代码举例
- (void)testKvo {
    HMPerson *p = [[HMPerson alloc] init];
    p.age = 20;
    
    [p addObserver:self forKeyPath:@"age" options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew context:nil];
    
    p.age = 30;
    p.age = 40;
    
    self.p = p;
}

- (void)dealloc {
    [self.p removeObserver:self forKeyPath:@"age"];
}

/**
 *  当监控的某个属性的值改变了就会调用
 *
 *  @param keyPath 属性名(哪个属性改了?)
 *  @param object  哪个对象的属性被改了?
 *  @param change  属性的修改情况(属性原来的值、属性最新的值)
 *  @param context void * == id
 */
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    NSLog(@"%@对象的%@属性改变了:%@", object, keyPath, change);
}
复制代码

Notification

观察者模式,controller向defaultNotificationCenter添加自己的notification,其他类注册这个 notification就可以收到通知,这些类可以在收到通知时做自己的操作(多观察者默认随机顺序发通知给观察者们,而且每个观察者都要等当前的某个观察者的操作做完才能轮到他来操作,可以用NotificationQueue的方式安排观察者的反应顺序,也可以在添加观察者中设定反映时间,取消观察需要在viewDidUnload跟dealloc中都要注销)。

是否可以把比较耗时的操作放在通知中?

如果是在异步线程发的通知,那么就可以执行比较耗时的操作;
如果在主线程发的通知,那么就不可以执行比较耗时的操作。
因为在哪个线程发通知就在哪个线程执行操作。

协议(Protocol)和代理(delegate)

协议: 相当于一个方法列表,多个类可以共享的方法的列表。
代理: 它是一个设计模式,本质就是两个对象之间的调用id的范型。

协议(Protocol)
  • 作用

    1. 用来声明一些方法。
    2. 也就说,一个Protocol是由一系列的方法声明组成的。
    3. 任何类只要遵守了Protocol, 就相当于拥有了Protocol的所有方法声明。
  • 书写格式

    @protocol 协议名称
    // 方法声明列表
    // 协议中有2个关键字可以控制方法是否要实现(默认是@required,在大多数情况下,用途在于程序员之间的交流)
    @required // 这个方法必须要实现(若不实现,编译器会发出警告)
    @optional // 这个方法不一定要实现
    @end
    复制代码

    类遵守协议:

    @interface 类名 : ⽗父类 <协议名称1, 协议名称2,...>
    @end
    复制代码

    协议遵守协议:
    一个协议可以遵守其他多个协议,一个协议遵守了其他协议,就相当于拥有了其他协议中的方法声明。

    @protocol 协议名称 <协议1, 协议2>
    @end
    复制代码
  • 基协议
    NSObject是一个基类,最根本最基本的类,任何其他类最终都要继承它。还有名字也叫NSObject的协议,它是一个基协议,最根本最基本的协议。 NSObject协议中声明很多最基本的方法,descriptionretainrelease,建议每个新的协议都要遵守NSObject协议。

代理(delegate)

  • 代理的目的
    1. 两个对象之间传递数据和消息,代理的目的是改变或传递控制链。允许一个类在某些特定时刻通知到其他类,而不需要获取到那些类的指针。可以减少框架复杂度。另外一点,代理可以理解为java中的回调监听机制的一种类似。
    2. 解耦,拆分业务逻辑

代码举例

反向代理传值实现过程

委托方:.h
//制定协议
@protocol ViewController4Delegate <NSObject>
- (void)labelFontSize: (CGFloat)fontSize;
@end
@interface ViewController4 : FatherViewController
//拥有一个代理人
@property (assign, nonatomic)id <ViewController4Delegate>delegate;
@end
委托方:.m
//传值 如果拥有方法就执行方法
    if ([self.delegate respondsToSelector:@selector(labelFontSize:)]) {
        [self.delegate labelFontSize:_fontSize];
    }
代理方:.h
@interface ViewController3 : FatherViewController <ViewController4Delegate>//遵守协议
@end
代理方.m
#pragma mark 实现ViewController4Delegate的协议方法
- (void)labelFontSize:(CGFloat)fontSize{
    _label.font = [UIFont systemFontOfSize:fontSize];
}
事件
vc4.delegate = self;//设置代理人
复制代码

Notification、KVO、代理 对比

通知和协议的不同之处?
  1. 协议有控制链(has-a)的关系,通知没有。
  2. 通过NSNotification可以给多个对象传递数据和消息。例如将Module层的变化,通知到多个Controller对象时,可以使用 NSNotification;
    protocol(代理模式)只能给一个对象传递数据和消息。在通知模式中,往往是一对多的关系。
  3. delegate 效率比 notification 高。
  4. delegate方法比notification更加直接,最典型的特征是,delegate方法往往需要关注返回值,也就是delegate方法的结果。
Notification和KVO的区别和用法是什么?

相同: 两者都是观察者模式

区别: KVO只能对属性做出反应,不会用来对方法或者动作做出反应。如果是只需要观察某个对象的某个属性,可以使用KVO。
KVO是被观察者直接发送消息给观察者,是对象间的直接交互。只能检测类中属性,并且属性名都是通过NSString来查找,编译器不会帮你检测对错和补全,纯手敲会比较容易出错。
通知则是两者都和通知中心对象交互,对象之间不知道彼此。NSNotification的特点,就是需要被观察者先主动发出通知,然后观察者注册监听后,再来进行响应,比KVO多了发送通知的一步,但是其优点是监听不局限与属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,使用灵活。

notification优缺点:
优势:

  1. 不需要写多少代码,实现比较简单
  2. 一个对象发出的通知,多个对象能进行反应,一对多的方式实现很简单

缺点:

  1. 编译期不会接茬通知是否能被正确处理
  2. 释放注册的对象时候,需要在通知中心取消注册
  3. 调试的时候,程序的工作以及控制流程难跟踪
  4. 需要第三方来管理controller和观察者的联系
  5. controller和观察者需要提前知道通知名称、UserInfo dictionary keys。如果这些没有在工作区间定义,那么会出现不同步的情况
  6. 通知发出后,发出通知的对象不能从观察者获得任何反馈。

KVO优缺点:
优点:

  1. 提供一个简单地方法来实现两个对象的同步
  2. 能对非我们创建的对象做出反应
  3. 能够提供观察的属性的最新值和先前值
  4. 用keypaths来观察属性,因此也可以观察嵌套对象

缺点:

  1. 观察的属性必须使用string来定义,因此编译器不会出现警告和检查
  2. 对属性的重构将导致观察不可用
  3. 复杂的“if”语句要求对象正在观察多个值,这是因为所有的观察都通过一个方法来指向
Notification、KVO、代理 选择

通知比较灵活(1个通知能被多个对象接收, 1个对象能接收多个通知);
代理比较规范,但是代码多(默认是1对1);
KVO性能不好(底层会动态产生新的类),只能监听某个对象属性的改变, 不推荐使用(1个对象的属性能被多个对象监听, 1个对象能监听多个对象的其他属性)

文章推荐

深度解析三者的不同
手动触发KVO

其他

循环引用
  • 委托和委托方双方的property声明用什么属性?为什么?
    委托和委托方双方的property声明属性都是assign而不是retain。
    为了避免循环引用造成的内存泄露。

  • 为什么很多内置类,如UITableViewController的delegate属性都是assign而不是retain?
    会引起循环引用。
    tableView的代理一般都是它所属的控制器,控制器会对它内部的view做一次retain操作,假设tableView也对代理(控制器)做一次retain操作,那么就出现循环retain问题。
    这里delegate我们只是想得到实现了它delegate方法的对象,然后拿到这个对象的指针就可以了,我们不期望去改变它或者做别的什么操作,所以我们只要用assign拿到它的指针就可以了。而用retain的话,计数器加1。我们有可能在别的地方期望释放掉delegate这个对象,然后通过一些判断比如说它是否已经被释放,做一些操作。但是实际上它retainCount还是1,没有被释放掉,要在UITableViewController的dealloc里面才被释放掉(这里我只是举个例子,一般retain的对象都是在dealloc里被释放)。这里就会造成一些问题出现。而如果你确定不会有冲突的问题出现的话,或者你也希望用到delegate的这个对象,直到你不用它为止,那么用retain也未尝不可,只是需要最后release一次。

  • 循环引用的问题这样理解:
    创建了两个类A和B,现在引用计数都是1。现在让A和B互相引用(A有一个属性是B对象,属性说明是retain;B有一个属性是A对象,属性说明是retain),现在两个对象的引用计数都增加了1,都变成了2。现在执行[A release]; [B release];,这时你发现A和B将无法释放,因为要想释放A必须先释放B,在B的dealloc方法中再释放A。同理,要想释放B必须先释放A,在A的dealloc方法中再释放B。所以这两个对象将一直存在在内存中而不释放。这就是所谓的循环引用的问题。要想解决这个问题,一般的方法可以将引用的属性设置为assign,而不是retain来处理。

iOS中回调机制,并作简单的比较
  1. 目标动作对:当两个对象之间有比较紧密的关系时,如视图控制器与其下的某个视图。
  2. 代理:也叫委托,当某个对象收到多个事件,并要求同一个对象来处理所有事件时。委托机制依赖于某个协议定义的方法来发送消息。
  3. 通知:当需要多个对象或两个无关对象处理同一个事件时。
  4. Block:适用于回调只发生一次的简单任务。

单例

Foundation 和 Application Kit 框架中的一些类只允许创建单件对象,即这些类在当前进程中的唯一实例。举例来说,NSFileManager 和 NSWorkspace 类在使用时都是基于进程进行单件对象的实例化。当向这些类请求实例的时候,它们会向您传递单一实例的一个引用,如果该实例还不存在,则首先进行实例的分配和初始化。单件对象充当控制中心的角色,负责指引或协调类的各种服务。如果类在概念上只有一个实例(比如NSWorkspace),就应该产生一个单件实例,而不是多个实例。

怎样实现一个单例模式的类,给出思路,不写代码。 · 首先必须创建一个全局实例,通常存放在一个全局变量中,此全局变量设置为nil · 提供工厂方法对该全局实例进行访问,检查该变量是否为nil,如果nil就创建一个新的实例,最后返回全局实例 · 全局变量的初始化在第一次调用工厂方法时会在+allocWithZone:中进行,所以需要重写该方法,防止通过标准的alloc方式创建新的实例 · 为了防止通过copy方法得到新的实例,需要实现-copyWithZone方法 · 只需在此方法中返回本身对象即可,引用计数也不需要进行改变,因为单例模式下的对象是不允许销毁的,所以也就不用保留 · 因为全局实例不允许释放,所以retain,release,autorelease方法均需重写

使用dispatch_once实现单例

很多人实现单例会这样写:

@implementation XXClass
+ (id)sharedInstance {
    static XXClass *sharedInstance = nil;
    @synchronized(self) {
        if (!sharedInstance) {
            sharedInstance = [[self alloc] init];
        }
    }
    return sharedInstance;
}
复制代码

相比之下:

@implementation XXClass
+ (id)sharedInstance {
    static XXClass *sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        if (!sharedInstance) {
            sharedInstance = [[self alloc] init];
        }
    });
    return sharedInstance;
}
复制代码

使用dispatch_once可以保证线程安全,开发者无需担心加锁或同步。此外,dispatch_once更高效,它没有使用重量级的同步机制。相反,此函数采用“原子访问”来查询标记,以判断其所对应的代码原来是否已经执行过。在64位Mac OS X上测试,后者的执行速度要比前者快一倍。

指针

指针本身不可变,但指针指向的数据结构可以改变。

堆和栈上的指针

指针所指向的这块内存是在哪里分配的,在堆上称为堆上的指针,在栈上为栈上的指针。
在堆上的指针,可以保存在全局数据结构中,供不同函数使用访问同一块内存。
在栈上的指针,在函数退出后,该内存即不可访问。

几种指针类型

  • 数据结构中的指针?
    其实就是指向一块内存的地址,通过指针传递,可实现复杂的内存访问。
  • 函数指针?
    指向一块函数的入口地址。
  • 指向指针的指针?
    指针指向的变量是一个指针,即具体内容为一个指针的值,是一个地址。此时指针指向的变量长度也是4位。

指针与地址的区别

  1. 指针意味着已经有一个指针变量存在,他的值是一个地址。指针变量本身也存放在一个长度为四个字节的地址当中,而地址概念本身并不代表有任何变量存在。
  2. 指针的值,如果没有限制,通常是可以变化的,也可以指向另外一个地址。
    地址表示内存空间的一个位置点,他是用来赋给指针的。
  3. 地址本身是没有大小概念。
    指针指向变量的大小,取决于地址后面存放的变量类型。

怎样防止指针的越界使用问题?

必须让指针指向一个有效的内存地址,

  1. 防止数组越界
  2. 防止向一块内存中拷贝过多的内容
  3. 防止使用空指针
  4. 防止改变const修改的指针
  5. 防止改变指向静态存储区的内容
  6. 防止两次释放一个指针
  7. 防止使用野指针

nil Nil null NSNull 区别

nil 是宏,OC对象使用,表示对象为空;
Nil 是宏,OC类使用,表示类指向空;
NULL 是宏,C语言使用,表示空指针(比如SEL等);
NSNull 是类的类型,表示空的占位对象(比如NSArray NSDictionary中charity非nil的空值)

id和instancetype的区别

  1. instancetype 只能作为函数或者方法的返回值
  2. id 能作为函数或者方法的返回值、参数类型,也能用来定义变量
  3. instancetype对比id的好处:能精确地限制返回值的具体类型

面试题

一些声明

  • @property
    @property 实际上是getter和setter,@synthesize是合成这2个方法。

  • @class作用
    在头文件中,一般只需要知道被引用的类的名称就可以了,不需要知道其内部的实体变量和方法,所以在头文件中一般使用@class来声明这个名称是类的名称。而在实现类里面,因为会用到这个引用类的内部的实体变量和方法,所以需要使用#import来包含这个被引用类的头文件。
    @class 的作用是告诉编译器,有这么一个类。
    @class 还可以解决循环依赖的问题,例如 A.h 导入了 B.h,而 B.h 导入了 A.h,每一个头文件的编译都要让对象先编译成功才行,使用@class就可以避免这种情况的发生。

  • @interface
    创建某个特定类的对象之前,objective-c编译器需要一些有关该类的信息。他必须知道该对象的数据成员和它提供的特性。可以使用@interface指令把这种信息传递给编译器。

  • @implementation
    编译器指令,表明将为某个类提供代码。类名出现 @implementation 之后。

load、initialize 方法

  • +(void)load;

    1. 当某个类第一次装载到OC运行时系统(内存)时,就会调用
    2. 程序一启动就会调用
    3. 程序运行过程中只会调用一次
  • +(void)initialize;

    1. 当某个类第一次使用时(比如调用了类的某个方法),就会调用
    2. 并非程序一启动就会调用

static

  • 函数体内 static
    变量的作用范围为该函数体,不同于 auto 变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
  • 模块内的 static
    全局变量,可以被模块内所用函数访问,但不能被模块外其它函数访问,这个函数的使用范围被限制在声明它的模块内;
  • 类中的 static
    成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因而只能访问类的 static 成员变量。

static定义静态变量,只初始化一次,直到程序销毁时才释放,即为该变量分配的空间在整个程序的执行期内都始终存在;声明外部变量,该变量的作用只限于本文件模块。
extern定义外部变量,作为类的拓展供其他外部类访问;也可作为传值使用。

const

const意味着“只读”,属于修饰符。合理地使用关键字 const 可以设置参数不可修改,防止其被无意的代码修改。这样可以减少bug的出现。
在定义 const 变量时,通常需要对它进行初始化,因为以后就没有机会再去改变它了;
对指针来说,可以指定指针本身为 const,也可以指定指针所指的数据为 const,或二者同时指定为 const;
在一个函数声明中,const 可以修饰形参,表明它是一个输入参数,在函数内部不能改变其值;
对于类的成员函数,若指定其为 const 类型,则表明其是一个常函数,不能修改类的成员变量;
对于类的成员函数,有时候必须指定其返回值为 const 类型,以使得其返回值不为“左值”。

  • const int aint const a
    作用一样,a不能被修改。

  • const int *a
    const修饰int,而int定义的又是一个整型数,a是一个指向常整型数的指针。所以整型数不能被修改,即*a不能被重新赋值,而指针可以被修改,使其指向另一个地址空间。

    const int *a = 0;//此时输出*a,结果为0
    int b = 1;
    a = &b; // 此时输出*a,结果为1
    b = 20; // 此时输出*a,结果为20
    *a = 20; // 这是不被允许的,会报错,因为*a被const修饰,不能重新赋值
    复制代码
  • int *const a
    const修饰a,a是一个指向整型数的常指针。所以指针不能被修改,但所指向的地址的值是可以被修改的。

    int b = 1;
    int *const a = &b; // 此时输出*a,结果为1
    b = 20; // 此时输出*a,结果为20。可以使用*a = 20来代替
    a = &b; // 这是不被允许的,会报错,因为指针a被const修饰,指针不能被修改
    复制代码
  • int const * const a
    a是一个指向常整型数的常指针,所以指针不可被修改,指针指向的整型数也是不可以被修改的。

    int b = 1;
    int c = 2;
    float d = 3.0;
    int const * const a = &b;//此时输出*a,结果为1
    b = 20; // 此时输出*a,结果为20
    a = &c; // 这是不被允许的,会报错,因为指针a不能被改变
    *a =  d; // 这也是不被允许的,会报错,因为指针a指向的整型数也是不能被修改的
    int const * const a也可以写成const int* const a,它们的作用是一样的。
    复制代码
  • 修饰字符串比较

    // " *coder "不能被修改, "coder"能被修改
    const NSString *coder = @"Hello world!";
    // " *coder "不能被修改, "coder"能被修改
    NSString const *coder = @"Hello world!";
    // "coder"不能被修改," *coder "能被修改
    NSString * const coder = @"Hello world!";
    复制代码

define与const的区别?

  1. 编译器处理方式不同
    define宏是在预处理阶段展开。
    const常量是编译运行阶段使用。
  2. 类型和安全检查不同
    define宏没有类型,不做任何类型检查,仅仅是展开。
    const常量有具体的类型,在编译阶段会执行类型检查。
  3. 存储方式不同
    define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存。
    const常量会在内存中分配(可以是堆中也可以是栈中)。
  4. const可以节省空间,避免不必要的内存分配。
    例如: const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而 #define定义的常量在内存中有若干个拷贝。
  5. 提高了效率
    编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
  6. const相比#define的优点:
    const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。

atomic 锁是怎么加的

@synthesize name = _name;
- (void)setName:(NSString *)name {
    @synchronized(self) {
        _name = [name copy];
    }
}
- (NSString *)name {
    @synchronized(self) {
        return _name;
    }
}
复制代码

推送机制

  • iOS消息推送的工作机制
    iOS消息推送的工作机制可以简单的用下图来概括:
    Provider是指某个iPhone软件的Push服务器;
    APNS是Apple Push Notification Service的缩写,是苹果的服务器。
    第一阶段:应用程序把要发送的消息、目的iPhone的标识打包,发给APNS。
    第二阶段:APNS在自身的已注册Push服务的iPhone列表中,查找有相应标识的iPhone,并把消息发送到iPhone。
    第三阶段:iPhone把发来的消息传递给相应的应用程序,并且按照设定弹出Push通知。

  • 整个推送流程

    从上图我们可以看到:

    1. 应用程序注册消息推送。
    2. iOS从APNS Server获取device token,应用程序接收device token。
    3. 应用程序将device token发送给PUSH服务端程序。
    4. 服务端程序向APNS服务发送消息。
    5. APNS服务将消息发送给iPhone应用程序。

堆和栈的区别

首先堆和栈都是一块内存区域。
堆是不连续的内存区域,是动态分配的,没有静态分配的堆,他的释放是由程序员控制,容易产生存储泄露,使程序效率降低。但是堆获得的空间比较灵活,也比较大。
栈是一块连续的内存区域,有静态分配(如局部变量的分配)和动态分配(用alloc函数进行分配),但是栈的动态分配是由编译器进行释放,无需我们手工实现。能从栈获得的空间较小。 具体来说:

  1. 申请大小:

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的。在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小。
堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。
2. 碎片问题: 对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。
对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出。
3. 分配方式:
堆都是动态分配的,没有静态分配的堆。
栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。
4. 分配效率:
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
堆则是C/C++函数库提供的,它的机制是很复杂的。

面试必读

阿里数据iOS端启动速度优化的一些经验
iOS中 性能优化之浅谈load与initialize 韩俊强的博客

免责声明:文章版权归原作者所有,其内容与观点不代表Unitimes立场,亦不构成任何投资意见或建议。

ios

509

相关文章推荐

未登录头像

暂无评论