OciOS基本知识研究

OC&iOS

2018-04-22  本文已影响1764人  奇异果好补

OC语言基础

1.类与对象

类方法

OC的类方法只有2种:静态方法和实例方法两种

@interface Controller : NSObject
+ (void)thisIsAStaticMethod;静态方法
- (void)thisIsInstanceMethod;实例方法

在OC中,只要方法声明在@interface里,就可以认为是公有的。实际上,OC没有绝对的私有和保护成员方法,仅仅是对调用者隐藏某些方法
声明和实现都写在@inplementation里的方法,类的外部是看不到的。

!如果想通过Category来隐藏方法的时候,可以把实现方法放在主implementation里。如果是Extension类拓展,声明隐藏在.m文件中,调用者无法看到其声明,也就没法调用这个类拓展的方法,在ARC下会引发编译错误。

使用Extension的好处:

类变量

@property中有哪些属性关键字(后面有哪些修饰符)?

属性分为四类:

  1. 非原子性-- nonatomic特性:
    在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性。如果属性具备nonatomic特质,则不使用同步锁。请注意,尽管没有名为“atomic的特质(如果其属性不具备nonatomic特质那么都是原则子的),但是仍然可以在属性特质中写明这一点,编译器不会报错,若是自己定义存取方法,那么就应该从与属性特质相符的原子性”
  2. 读写权限-- readwrite(读写)readonly(只读)
  3. 内存管理权限--

@property 的说明可以有以下几种:

* readwrite 是可读可写特性;需要生成 getter 方法和 setter 方法
* readonly 是只读特性,只会生成 getter 方法 不会生成 setter 方法,不希望属性在类外改变时使用
* assign 是赋值特性,setter 方法将传入参数赋值给实例变量;仅设置变量时;
* retain 表示持有特性,setter 方法将传入参数先保留,再赋值,传入参数的 retain count 会+1;
* copy 表示拷贝特性,setter 方法将传入对象复制一份;需要完全一份新的变量时。
* nonatomic 和 atomic ,决定编译器生成的 setter getter是否是原子操作。 atomic 表示使用原子操作,可以在一定程度上保证线程安全。一般推荐使用 nonatomic ,因为 nonatomic 编译出的代码更快

4.方法-- getter=<name>setter=<name>
getter=<name>的样式: @property (nonatomic, getter=isOn) BOOL on;

5.ARC下默认关键字:

property的本质:

property有2大概念:ivar(实例变量)、存取方法(getter+setter=access method) @property会自动生成getter和setter方法,同时进行自动内存管理。

@synthesize@dynamic的认识

@property 背后使用synthesize来生成getter和setter,对于现代OC来说,编译器默认会进行自动synthesize,把ivar和属性绑定起来:
@synthesize propertyName = _propertyName 不需要我们写任何代码,就直接使用getter和setter了。

下面几种特殊情况不会自动synthesize

类拓展--Protocol,Category和Extension

Protocol
OC是单继承的,OC中的类可以实现多个协议protocol来实现类似C++中多重继承的效果。
Protocol类似Java中的interface接口,定义了一个方法列表,这个方法列表中的方法可以使用@required,@optional标注,以表示该方法是否是必须要实现的方法。一个protocol可以继承其他的protocol。

@protocol TestProtocol <NSObject> //NSObject也是一个protocol,这里继承NSObject里的方法
- (void)print;
@end

@interface B : NSObject<TestProtocol>
- (void)print;//默认方法是@required的,即必须实现
@end

Delegate(委托)是Cocoa中常见的一种设计模式,其实依赖于protocol这个语言特性。

当Protocol中含有property时,编译器不会自动synthesize,需要手动处理:
方法1:再次声明property
方法2:手动synthesize @synthesize item;

Category
Category是一种灵活的拓展原有类的机制,使用Category不需要访问原有类的代码,也无需继承。Category提供了一种简单的方式,来实现类的相关方法的模块化,把不同的类方法分配到不同的类文件中。

Extension一种匿名的Category :Extension是用来给类添加私有变量和方法,用在类的内部使用。
区别:

使用场景:

一般用在类的内部。例如在interface中定义readonly类型的属性,在实现中添加extension,将其重新定义为readwrite,这样我们在类的内部就可以直接修改它的值了,然而外部依然不能调用setter方法来修改.

Person.h

@interface Person : NSObject
@property (readonly) NSString *uniqueIdentifier;
@end

Person.m

@interface Person()
@property (readwrite) NSString *uniqueIdentifier;
@end

@implementation Person
....
@end

在类中添加全局变量:在.m文件中使用static变量,因为static变量在编译期就是确定的,因此对于NSObject对象来说,初始化的值只能是nil.

static NSOperationQueue * _personOperationQueue = nil;

如何进行init的初始化?通过重载initialize方法

@implementation Person
- (void)initialize{
    if (!_personOperationQueue){
        _personOperationQueue = [NSOperationQueue alloc] init];
    }
}
@end

类的导入

导入类可以使用#include,#import和class三种方法,区别如下:

@class是放在interface中的,只是在引用一个类,将这个被引用类作为一个类型使用。在实现文件中,如果需要引用到被引用类的实体变量或者方法时,还需要使用#import方法引入被引用类。

类的初始化

OC是建立在Runtime基础上的语言,类也不例外。OC中 类是初始化也是动态的。在OC中绝大部分类都继承自NSObject,它有两个非常特殊的类方法loadinitilize,用于类的初始化

+load

+load方法是当类或分类被添加到OC runtime时被调用的,实现这个方法可以在类加载的时候执行一些类相关的行为。子类的+load方法会在它的所有父类的+load方法之后执行,而分类的+load方法会在它的主类的+load方法之后执行。不同的类之间的+load方法的调用顺序是不确定的。

load方法不会被类自动继承,每一个类中的load方法都不需要像viewDidLoad方法一样调用父类的方法。子类、父类和分类中的 +load方法的实现是被区别对待的。也就是说如果子类没有实现+load方法,那么当它被加载时,runtime是不会去调用父类的+load方法的
当一个类和它的分类都实现了+load方法时,两个方法都会被调用。因此,我们常常可以利用这个特性做一些坏事,比如:混淆方法

+initialize

+initialize方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说+initialize方法是以懒加载的方法被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的+initialize方法是永远不会被调用的。

+initialize方法的调用与普通方法的调用是一样的,走的都是发送消息的流程。换而言之。如果子类没有实现+initialize方法,那么继承自父类的实现会被调用,如果一个类的分类实现了+initialize方法,那么就会对这个类中实现造成覆盖。

OC语法

对OC的理解与特性

区分:“super”和“self”

super是一个神奇的关键字,super本身是一个编译器标识符self是指向的同一个消息接受者
不同点在于:super会告诉编译器,调用class这个方法时,要去父类的方法,而不是本类的方法当使用self调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;
而当使用super时,则从父类的方法中开始寻找。
然后调用那个父类中对应的方法。

野指针

野指针不是NULL空指针,而是指向"垃圾"内存的指针。
其原因主要为:

runtime如何实现weak属性?

runtime对注册的类,会进行布局,对于weak对象会放入一个哈希表中,用weak指向的对象内存地址作为key,为此对象的引用计数为0时,会销毁。

如何实现将weak变量的自动设置为nil?

假如weak指向的对象内存地址是a,那么就会以a为键,去这个weak哈希表中寻找所有以a为键的weak对象,从而设置为nil

消息

代理

SET、Method、和IMP分别说下。再谈下对IMP的理解


 NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
        NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];
        NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil];
        IMP originalAFResumeIMP = method_getImplementation(class_getInstanceMethod([self class], @selector(af_resume)));
        Class currentClass = [localDataTask class];
        
        while (class_getInstanceMethod(currentClass, @selector(resume))) {
            Class superClass = [currentClass superclass];
            IMP classResumeIMP = method_getImplementation(class_getInstanceMethod(currentClass, @selector(resume)));
            IMP superclassResumeIMP = method_getImplementation(class_getInstanceMethod(superClass, @selector(resume)));
            if (classResumeIMP != superclassResumeIMP &&
                originalAFResumeIMP != classResumeIMP) {
                [self swizzleResumeAndSuspendMethodForClass:currentClass];
            }
            currentClass = [currentClass superclass];
        }
        
        [localDataTask cancel];
        [session finishTasksAndInvalidate];

isa指针的作用 --(帮助一个对象找到它的方法)

消息机制

OC是建立在Runtime基础上的语言,类也不例外。OC中类是初始化也是动态的。在OC中结大部分分类都继承自NSObject,它有两个特殊的类方法load,initialize,用于类的初始化。

在对象上调用方法是OC中经常使用的功能,也称作传递消息消息有”名称“或选择器”,可以接受参数,可以有返回值。

消息=【接收者+选择器+参数】构成。给某对象发送消息也就相当于在该对象上“调用方法”,发送给某对象的全部消息都要由动态消息派发系统来处理,该系统会查出对应的方法,并执行其代码

编译器在编译代码的时候就已经知道程序中有X和Y这2个函数了,会直接生成调用这些函数的指令。而函数地址实际上是硬编码值在指令之中。

消息传递实现原理:

在OC中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有的方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言。

给对象发送消息可以这样写:
id returnValue = [someObjectName:parameter];

someObject叫做“接收者” messageName叫做“选择器”。选择器与参数合起来称为"消息(message)"。

编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数仍是消息传递机制中的核心函数,叫做objc_msgSend
void objc_msgSend(id self,SEL cmd,...)
参数个数可变,第一个参数代表接收者,第二个参数代表选择器,后续参数表示消息中的那些参数,其顺序不变。

调用方法的过程:

接着下面的消息转发机制

总结如下:

objc是动态语言,每个方法在运行时会被动转为消息发送,即objc_msgSend(receiver,selector)

  1. Method resolution
    objc运行时调用+resolveInstanceMethod:或者+resolveClassMethod:往上级寻找备援函数,让你有机会提供一个函数的实现。如果添加了函数,那么运行时系统就会重新启动一次消息发送的过程,否则,运行时就会移动下一步,消息转发(Message Forwarding).
  2. Fast forwarding
    如果目标对象实现了-forwardingTargetForSelector:,Runtime这时就会调用这个方法,给你把这个消息转发给其他对象的机会。只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象也会变成返回的那个对象。否则,会继续Normal Fowarding(下一步的转发机制)。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快。
  3. Normal forwarding
    这一步是Runtime最后一次给你挽救的机会,首先它会发送 -methodSignatureForSelector:消息获得函数的参数和返回值类型。如果-methodSingnatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这是也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象。

具体实现:消息转发分为2大阶段

2.类的初始化

+load

+load方法是当类或分类被添加到OC runtime时被调用的,实现这个方法可以让我们在类加载的时候执行一些类相关的行为。子类的+load方法会在它的所有后类的+load方法之后执行,而分类的+load方法会在它的主类的+load方法之后执行。但是不同的类之间的+load方法的调用顺序是不确定的。

+load方法不会被类自动继承,每个类中的load方法都不需要像viewDidLoad方法一样调用父类的方法。子类、父类和分类的+load方法的实现是被区别对待的。

也就是说如果子类没有实现+load方法,那么当它被加载时runtime时不会去调用父类的+load方法的。同理,当一个类和它的分类都实现了+load方法时,两个方法都会被调用。

+initialize

+initialize方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法 和类方法的调用。
也就是+initialize方法是以懒加载的方式被调用的,如果程序一直没有某个类或它的子类发送消息,那么这个类的+initialize方法永远不会被调用。
+initialize方法的调用与普通方法的调用时一样的,走的都是发送消息的流程。换言之,如果子类没有实现+initialize方法,那么继承自父类的实现会被调用;

如果一个类的分类实现了+initialize方法,那么就会对这个类的实现造成覆盖。

2.Block编程

block:实际上是带有自动变量的匿名函数指向结构体的函数指针,编译器会将block的内部代码生成对应的函数。

百度大神的block技巧与底层解析

block和函数的相似性:可以在任何时候执行可以保存代码有返回值有形参调用方式一样

block访问外部变量:block内部可以访问外部变量
默认情况下,block内部不能修改外部的局部变量
但是给局部变量加上__block关键字,就可以在内部进行修改了

1.默认情况下,block的内存是在栈中的,它不会对所引用的对象进行任何操作

看下面这段代码:int a = 10;a是一般局部变量,传递过去的是值,所以后面的a不管赋值多少,在栈中的值10 是不改变的。

void test1()
{
    int a = 10;
    
    void (^block)() = ^{
        NSLog(@"a is %d", a);
    };
    
    a = 20;
    
    block(); // 打印出来: a is 10 
}

下面几种情况,变量就会传地址到堆中

//加上__block
void test2()
{
    __block int a = 10;
    
    void (^block)() = ^{
        NSLog(@"a is %d", a);
    };
    
    a = 20;
    
    block(); // 20
}

//静态局部变量
void test3()
{
    static int a = 10;
    
    void (^block)() = ^{
        NSLog(@"a is %d", a);
    };
    
    a = 20;
    
    block(); // 20
}


//全局变量
int a = 10;
void test4()
{
    void (^block)() = ^{
        NSLog(@"a is %d", a);
    };
    
    a = 20;
    
    block();
}



2.如果对block进行一次copy操作,block的内存就会在堆中,它会对所用的对象做一次retain操作,引用计数器加1.比如@property (nonatomic, copy) void (^block)();

非ARC:如果所引用的对象用了__blcok修饰,就不会做retain操作

- (id)init
{
    if (self = [super init]) {
//        __block Dog * dog = self;与下行意思一样:将自身的对象赋值给block对象
        __block typeof(self) dog = self;
        self.block = ^{
//            [dog run];
            NSLog(@"%d", dog->_age);
        };
    }
    return self;
}

//做了一次copy就要有释放
- (void)dealloc
{
    NSLog(@"Dog --- dealloc");
    
    Block_release(_block);
    
    [super dealloc];
}

- (void)run
{
    NSLog(@"run");
}

ARC:如果所引用的对象用了__unsafe_unretained或者__weak修饰,就不用做retain操作。

__unsafe_unretained__weak修饰的变量,因为自己生成并持有的对象不能继续为自己所有,使用生成的对象会立即释放。
把使用__unsafe_unretained修饰的变量,赋值给__strong强引用修饰的变量时 一定要保证被赋值的对象【也就是__unsafe_unretained修饰的对象】确实存在,不然程序会崩溃

__block是复制对象的引用地址来实现访问的,源码中多了一个结构体,用来保存我们捕获并修改的变量。

block的常见的三种类型:每种类型的block都有一个isa

3.block也经常使用copy关键字

block使用copy是从MRC遗留下来的“传统”,在MRC中,方法内部的block在栈区,使用copy可以把它放到堆区。
在ACR中,写不写都没关系。
block使用copy还是strong效果都一样,写上block能时刻提醒我们:编译器自动对block进行了copy操作,如果不写copy,该类的调用者有可能会忘记或者根本不知道”编译器会对block进行了copy操作“,他们有可能会在调用之前自行拷贝属性值。这种操作多余而且低效。

4.使用block时什么情况会发生引用循环,如何解决?

!一个对象中强引用了block,在block中又强引用了该对象,就会发生循环引用。【2个对象互相持有对方的强引用,并且2各对象的引用计数都不是0的时候,就会造成循环引用。】

解决方法:是将该对象使用_weak或者_block修饰符修饰之后再在block中使用
1.id weak weakSelf = self; 或者 weak__typeof(&*self)weakSelf = self该方法可以设置宏
2.id __block weakSelf = self;
3.实例变量完成工作后,其中一方强制置空 xxx = nil.
默认情况下,在block中访问的外部变量是复制过去的。即,写操作不对原变量生效,但是你可以加上_block来让其写操作生效

_block int a = 0;
void (^foo)(void) = ^{
    a = 1;
};
foo();

Block不允许修改外部变量的值(栈中指针的内存地址)。_block所起到的作用就是只要观察到该变量被block所持用,就将"外部变量"在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。

__block int a = 0;//声明
NSLog("定义前:%p",&a);
void (^foo)(void)=^{
    a = 1;
    NSLog(@"block内部:%p",&a);//堆区
};
NSLog(@"定义后:%p",&a);//堆区
foo();
2016-07-17 18:34:58.581 Block[28619:2454735] 定义前:0x7fff5fbff848
2016-07-17 18:34:58.582 Block[28619:2454735] 定义后:0x100600088
2016-07-17 18:34:58.582 Block[28619:2454735] block内部:0x100600088

“定义前”在栈区,只要进入了block区域,就变成了堆区。这就是__block关键字的真正作用。定义后和block内部两者的内存地址是一样的,我们都知道block内部变量会被copy到堆区,"block内部“打印的是堆地址,因而”定义后“打印的也是堆的地址
a由基本数据类型,变成了对象类型。block会对对象类型的指针进行copy,copy到堆中,但并不会改变该指针所指向的堆中的地址,所以在上面的示例代码中,block体内修改的实际是a指向堆中的内容。

Block不允许修改外部变量的值,这里所说的外部变量的值,是占中指针的内存地址,栈是红灯区,堆是绿灯区。

block底层的实现
c++里面的结构体相当于OC的类,c++里面的结构体拥有自己的属性以及构造方法和方法。

block底层实现分2种:
1.不加_block,会创建一个结构体[结构体内有个isa指针,isa指针指向&NSConcreateStackBlock栈地址,实现其构造方法,来接收三个参数

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;//将fp赋值给impl结构体的FuncPtr参数[存在的是形参的值]
    Desc = desc;
}

2.__block实现:其中多了一个forwarding 指针存放的是自己类型的结构体的指针,存放的是自己的地址。取值的时候,拿到的是结构体的地址,只要把地址传递过去,就有了最高的操作权限,到时候再去取值就可以取到内存中最新的值。
生成的结构体如下

  truct __Block_byref_a_0 {
    void *__isa;   //isa 类型的指针 自己的类型
    __Block_byref_a_0 *__forwarding;  //与自己结构体同名,是一个自己类型的结构体的指针,存放的是自己的地址
    int __flags;  // 标记
    int __size;  // 类型的大小
    int a;  // a 属性 保存变量的值
  };

5.使用系统的某些block api(如UIView的block版本写动画时),是否也考虑引用循环问题?


系统的某些block api中,UIView的block版本写动画不需要考虑,但也有一些api需要考虑,所谓”循环引用“是指双方的强引用,所以那些”单向的强引用“(block强引用了self)没有问题,
比如:[UIView animateWithDuration:duration animations:^{[self.superview layoutIfNeeded];}];不需要考虑强引用问题。

但如果你使用了一些参数中可能含有ivar的系统api,如GCD、NSNotificationCenter就要小心一点,如果GCD内部引用了self,而GCD的其他参数是ivar,则要考虑到循环引用:

__weak __typeof__(self) weakSelf = self;
dispatch_group_async(_operationsGroup,_operationsQueue,^{
    __typeof__(self) strongSelf = weakSelf;//强引用
    [strongSelf doSomething];//强引用
    [strongSelf doSomethingElse];
});

3.Runtime

9.1 什么是runtime(运行时机制)?

9.1.是什么?

runtime是一套比较底层的纯C语言的API,属于1个C语言库,包含了很多底层的C语言API。

如何实现?

我们平时写的OC代码,在程序运行过程中,其实最终都会转成runtime的C语言代码,调用相应的函数进行消息发送。
所以runtime是OC的幕后工作者
比如:在OC中:[[Person] alloc] init]

在runtime中会转换成:objc_msg(obj_msgSend("Person", "alloc"), "init")

9.2.runtime怎么用呢?

runtime属于OC的底层,可以进行一些非常底层的操作(是OC无法实现的)
runtime可以在程序的运行过程中,动态添加一个类(比如KVO底层的实现,其实整个OC都是)
runtime可以在程序运行过程中,动态地为某个类添加属性、方法,修改属性值、方法
runtime可以遍历一个类的所有成员变量(属性)、所有方法

9.3.相关的头文件和函数

头文件:<objc/runtime.h> <objc/message.h>
函数:Ivar:是成员变量Method:成员方法
objc_msgSend:给对象发送消息
class_copyIvarList:遍历某个类所有的成员变量
class_copyMethodList:遍历某个类所有的方法

9.4.相关应用:

(1)NSCoding归档和解档,利用runtime遍历模型对象的所有属性(用一个循环)
(2)字典转模型;利用runtime遍历模型对象的所有属性,根据属性名从字典中取出对应的值,设置到模型的属性上
(3)KVO:利用runtime动态产生一个类

KVO内部实现原理:是基于runtime机制实现的。
1.当我们观察一个类的对象时,系统就会在运行期间动态的创建这个类的派生类。
2.这个派生类继承自该对象的原本的类,派生类并重写了被观察属性的setter方法。
3.重写的setter方法会负责在调用原来的setter方法前后,通知所有观察对象:值的更改。
真正实现通知机制【Person->转换成NSKVONotifying_Person】
4.最后通过isa把这个对象的isa指针(isa指针告诉runtime系统这个对象的类是什么) ,指向这个新创建的派生类,对象就变成了派生类的实例。

(4)runtime 如何实现 weak 属性?runtime如何实现weak变量的自动置为nil?


要实现weak属性,首先要搞清楚weak属性的特点:

weak表示的是一个弱引用,这个引用不会增加对象的引用计数,并且当这个所指的对象被释放之后weak指针会被赋值为nil,而在OC中向为nil 的weak对象 发送消息是安全的

runtime如何实现weak变量的自动设置为nil?

runtime对注册的类,会进行布局,对于weak对象会放入一个哈希表中,用weak指向的对象内存地址作为key,为此对象的引用计数为0时,会销毁。假如weak指向的对象内存地址是a,那么就会以a为键,在这个weak哈希表里搜索,找到所有以a为键的weak对象,从而设置为nil。

下面是KVO内部实现的监听代码
目的:让self.dog监听self.person的age属性的改变
@property (nonatomic, strong) MJPerson *person;
@property (nonatomic, strong) MJDog *dog;
//分配内存空间
self.person = [[Person alloc] init];
self.dog = [[Dog alloc] init];

//添加键值观察:
addObserver:观察者,负责处理监听事件的对象
forKeypath:观察的属性
options:观察的选项  NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
context:上下文


//让self.dog监听self.person的age属性的改变
[self.person addObserver:self.dog forKeypath:@"age" options:0 context:nil];


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    self.person.age = 30;
}


!!!系统还会以某种方式在中间代码插入:`[self willChangeValueForKey:@"属性名"];`、`[self didChangeValueForkey:@"变量名"];`

`KVO在调用存取方法之前总是通过isa指针调用``willChangeValueForKey:`,这会记录旧的值,而当改变发生后,``observeValueForKey:ofObject:change:context:`会被调用,之后总是调用`didChangeValueForkey:`
如果需要实现手动触发一个值value的KVO,那么在实现上面的3个调用,就可以实现手动触发了。【手动触发一般只在希望能控制回调的调用时机时才这么做】


注意:所有的kvo监听到事件,都会调用此方法
1.观察的属性
2.观察的对象
3.change属性变化字典(新、旧)
4.上下文,与监听的时候传递的一致


//所以必须在Dog 类的.m文件里实现获取监听的方法
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"狗监听了%@对象的%@属性改变了",object,keyPath);
}

(4)用于封装框架:想怎么改就怎么改
比如如下示例代码:一个person模型

MJPerson.h
#import <Foundation/Foundation.h>
@interface MJPerson : NSObject <NSCoding>
@property (nonatomic, assign) int age;
@property (nonatomic, assign) int height;
@property (nonatomic, copy) NSString *name;

#import "MJPerson.h"
#import <objc/runtime.h>

@implementation MJPerson

- (void)encodeWithCoder:(NSCoder *)encoder
{
   unsigned int count = 0;
   //copy
   Ivar *ivars = class_copyIvarList([MJPerson class], &count);
   //循环遍历
   for (int i = 0; i<count; i++) {
       // 取出i位置对应的成员变量
       Ivar ivar = ivars[i];
       
       // 查看成员变量
       const char *name = ivar_getName(ivar);
       
       // 归档:用UTF8的格式
       NSString *key = [NSString stringWithUTF8String:name];
       //kvc键值编码 传入key获取key对应的值
       id value = [self valueForKey:key];
       [encoder encodeObject:value forKey:key];
   }
   //释放这个指针变量
   free(ivars);
}

- (id)initWithCoder:(NSCoder *)decoder
{
   if (self = [super init]) {
       unsigned int count = 0;
       Ivar *ivars = class_copyIvarList([MJPerson class], &count);
       
       for (int i = 0; i<count; i++) {
           // 取出i位置对应的成员变量
           Ivar ivar = ivars[i];
           
           // 查看成员变量
           const char *name = ivar_getName(ivar);
           
           // 解档
           NSString *key = [NSString stringWithUTF8String:name];
           id value = [decoder decodeObjectForKey:key];
           
           // 设置到成员变量身上
           [self setValue:value forKey:key];
       }
       
       free(ivars);
   }
   return self;
}

@end

!!!区分:键路径(keyPath)、键值编码(KVC)、键值观察(KVO)

键路径:

键值编码KVC:

4.内存管理

自动引用计数ARC

ARC由编译器在编译时自动生成releaseretain代码,自动管理内存,ARC编译器由两部分:前端编译器和优化器

在ARC下,不需要手动管理内存,编译器会根据引用计数来帮我们管理内存,会在合适的位置帮我们releaseautorelease
ARC的出现降低了出现崩溃、内存泄露等风险,减少了开发者的工作量。
ARC通过什么方式帮助开发者管理内存?

通过retainCount机制 来决定对象是否需要释放,每次runloop的时候,都会检查对象的retainCount,如果retainCount为0,说明该对象不需要继续使用,就可以释放了。

ARC相对于MRC,不是在编译时添加retain/release/autorelease这么简单,应该是编译期和运行期两部分共同帮助开发者管理内存。

在编译期,ARC用的是底层的C接口实现的retain/release/autorelease,这样做性能更好,也是为什么不能在ARC环境下手动retain/release/autorelease,同时对同一上下文的同一对象的成对retain/release操作进行优化(即忽略掉不必要的操作) ARC也包含运行期组件,这个地方做的优化比较复杂,但也不能被忽略。

在ARC中与内存管理有关的4种标识符

我对ARC的看法:

ARC原则:

7.2引用计数的存储

isa的指针会拿出一部分空间来存储引用计数,就相对64位环境来说,就会拿出19位来存储引用计数,毕竟19bit(iOS系统)保存引用计数一定够,这时引用计数会由专门的SideTable类来存储。

SideTable类用来管理引用计数表和weak

//保证原子操作的自选锁
spinlock_t slock;
//保存引用计数的散列表
RefcountMap refcnts;
//保存weak引用的全局散列表
weak_table_t weak_table;

isa指针不仅可以指向它的类对象,,还会指向一个isa_t结构体,这个结构体包含类的信息以及引用计数等消息

autoreleasepool自动释放池

8.1自动释放池 autoreleasepool 底层怎么实现?

自动释放池以栈的形式实现:当我们创建一个新的自动释放池时,它将添加到栈顶。当一个对象收到发送autorelease消息时,它被添加到当前线程的处于栈顶的自动释放池,当自动释放池被回收时,它们从栈中删除,并且会给自动释放池里面所有的对象都做一次release操作。

也就是说自动释放池NSAutoreleasePool内部包含一个可变数组NSMutableArray,用来保存对象的声明。
如果一个对象声明为autorelease,那么,系统就会把这个对象加入到自动释放池中的这个可变数组中。
自动释放池NSAutoreleasePool自身在销毁的时候,会遍历一遍这个数组,release数组中的每个成员。

总结:autoreleasepool 以一个队列的形式出现,主要通过三个函数完成。 对autorelease执行了push和pop操作,销毁对象时执行release操作。

8.2自动释放池的手动创建和自动创建

手动创建

1.不是基于Application Kit的程序,比如命令行工具,则没有自动释放池的内置支持,需要手动创建
2.生成一个从属线程(子线程),一旦该线程开始执行,必须立即创建自动释放池,否则会泄露对象
3.写了一个循环,其中创建很多的临时变量,可以在循环内部创建一个自动释放池,
在达到一定数量的时候销毁这些对象,再创建一个自动释放池,帮助减少应用程序的最大内存占有量

自动创建

一般情况下,系统自动创建自动释放池--Application Kit会在一个事件周期(或事件循环迭代)的开端,
【比如点击事件,自动创建一个自动释放池,并且在事件周期的结尾释放它。】

8.3objc使用使用什么机制管理对象内存?

通过retainCount机制来决定对象是否需要释放,每次runloop的时候,都会检查对象的retainCount,如果retainCount为0,说明该对象没有地方需要继续使用,就可以释放掉了。

8.4不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)

基本信息:autorelease机制是iOS开发管理对象内存的好伙伴,在MRC中,调用[obj autorelease]来延迟内存的释放,ARC中不需要管理。

概念:NSAutoreleasePool内部包含一个数组NSMutableArray,用来保存声明,如果一个对象声明为autorelease,系统所做的工作就是把这个对象加入到这个数组中。NSAutoreleasePool自身在销毁的时候,会遍历一遍这个数组,release数组中的每个成员。
分两种情况:手动干预释放时机,系统自动去释放

1.手动干预释放时机:指定autoreleasepool就是在当前作用域大括号结束时释放。

2.系统自动去释放:不手动指定autoreleasepool
Autorelease对象除了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的runloop迭代结束时释放。

从程序启动到加载完成是一个完整的运行循环,然后会停下来,等待用户交互,用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件、触摸事件。

所有的autorelease的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。

8.5什么情况下回帮我们加autorelease,放进自动释放池

1.当使用alloc/new/copy/mutableCopy方法进行初始化时,会生成并持有对象(也就是不需要pool池子管理了),系统会自动在合适的位置对这些对象进行release

对于id类型id obj = [NSMutableArray array];
这种情况会自动将返回值的对象注册到autoreleasepool

@autoreleasepool{
    id _autoreleasing obj = [NSMutableArray array];
}

2._weak修饰符只持有对象的弱引用,而在访问引用对象的过程中,该对象可能被废弃。所以必须把对象注册到autoreleasepool中,那么在@autoreleasepool块结束之前都能确保对象的存在。

id __weak objweak = obj0;
NSLog(@"class=%@",[objweak class]);

对应的源代码为:

id __weak objweak = obj0;
id __autoreleasing tmp = objweak;
NSLog(@"class%@",[tmp class]);

3.id的指针或对象的指针在没有显式指定时会被附加上_autoreleasing修饰符

8.6怎么保证多人开发进行内存泄露的检查

1.使用Product、Analyze静态分析工具,进行代码的静态分析。【如果有内存泄露危险会提示蓝色标识信息"Prtential leak of an object stored into “变量名”】

2.为了避免不必要的麻烦,多人开发时尽量使用ARC

8.7什么情况下会发生内存泄露和内存溢出?

1.内存泄露:
(1)当程序在申请内存后,无法释放已申请的内存空间【例如一个对象或者变量,使用完后没有释放,这个对象一直占着内存】,
一次内存泄露危害可以忽略,但是内存泄露堆积后果严重,无论多少内存,迟早都会被耗光。内存泄露最终会导致内存溢出。

(2)当2个对象互相引用的时候。【解决方法:让其中一个成为weak】

(3)循环引用导致内存泄露:
1)block中若引用对象,需将该对象修饰为【在ARC】__unsafe_unretain或者__weak
2)代理delegate不要使用retain属性,要用weak属性避免循环。

2.内存溢出:当程序在申请内存时,没有给足够的空间供其使用,出现out of memory【例如申请了一个int类型的空间,但给变量存了long类型才能存下的数,这就是内存溢出了】`

8.8针对内存管理,有没有遇到内存暴增的时候

1.当列表滑动的时候内存莫名增长

原因:
1.没有使用UITableView的reuse重用机制,导致每显示一个cell都用autorelease的方式重新alloc一次,导致cell的内存不断的增加`

2.每个cell会显示一个单独的UIView,在UIView发生内存泄露,导致cell的内存不断增长

5.runloop

5.1 runloop内部实现原理

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出,如果我们需要一个机制而这个机制就是NSRunLoop的实现机制,让线程能随时处理事件但并不退出。

int main(int argc, char * argv[]){
    //程序一直运行状态
    while(AppIsRuning){
        //睡眠状态,等待唤醒事件
        id whoWakesMe = SleepForWakingUp();
        //得到唤醒事件
        id event = GetEvent(whoWakesMe);
        //开始处理事件
        HandleEvent(event);
    
    }
    return 0;
}

5.2 runloop和线程有什么关系?

总的来说,Run loop 一直运行着的循环。实际上,run loop和线程是紧密相连的。runloop是为了线程而生,没有线程,它就没有存在的必要。
Run loops是线程的基础架构部分,Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run loop,每个线程保证程序的主线程(main thread)都有与之对应的run loop对象。

1.主线程的run loop默认是启动的。

iOS应用程序里,程序启动后会有一个main()函数
重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。

int main(int argc, char *argv[]){
@autoreleasepool{
    return UIApplicationMain(argc,argv,nil,NSStringFromClass([AppDelegate class]));
}
}

2.对其他线程来说,run loop默认是没有启动的。如果你需要更多的线程交互,需要手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要
3.在任何一个Cocoa程序的线程中,都可以通过以下代码来获得当前线程runloop
NSRunLoop *runloop = [NSRunLoop currentRunLoop];

5.3 NSRunLoop的实现机制,及在多线程中如何使用

1.实现机制:NSRunLoop是iOS消息机制的处理模式

(1)NSRunLoop的主要作用:控制NSRunLoop里面线程的执行和休眠,
在有事情做的时候让当前的NSRunLoop控制线程工作,没事让当前的NSRunLoop控制的线程休眠
(2)NSRunLoop就是一直在循环检测,从线程start到线程end,
检测inputsource(如点击,双击等操作)异步事件,检测timesource同步事件,检测到输入源会执行处理函数
首先会产生通知,Corefunction向线程添加runloop observes来监听事件,意在监听事件发生时来做处理

(3)runloopmode 是一个集合,包括监听:事件源和定时器,以及需通知的runloop observes

2.在多线程中使用:

(1)只有在为程序创建子线程的时候,才需要运行runloop。对于程序的主线程而言,runloop是关键部分。Cocoa提供了运行主线程runloop的代码,同时也会自动运行runloop
iOS程序UIApplication中的run方法在程序正常启动的时候会启动runloop。 如果使用xcode提供的模板创建的程序,那永远不需要自己去启动runloop
(2)在多线程中,需要判断是否是runloop。如果需要runloop,那么就要负责配置runloop并启动,不需要任何情况下都去启动runloop。比如,使用线程去处理一个预先定义好的耗时极长的任务时,就可以启动runloop了。
runloop 只在要和线程交互时才需要

5.4 runloop定时源和输入源

1.创建的程序不需要显式创建runloop;

每个线程,包括程序的主线程(main thread)都有与之相关的runloop对象,主线程会自行创建并运行runloop

2.runloop 处理的输入事件有2种不同的来源:输入源(input source)定时源(timer source)

3.区别:

输入源传递异步信息,通常来自其他线程或程序。

定时源则传递同步信息,在特定的事件或者一定的时间间隔发生

5.5 runloop的mode作用是什么?


倒计时如何实现 dispatch_source_t _timer

/**
 倒计时的实现
 */
- (void)startTimer {
    //倒计时时间
    __block int timeout = 30;
    //全局队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //创建定时器
    dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), 1.0 * NSEC_PER_SEC, 0);//每秒执行
    //设置事件处理
    dispatch_source_set_event_handler(_timer, ^{
        if (timeout <= 0) { //倒计时结束关闭
            dispatch_source_cancel(_timer);
            dispatch_async(dispatch_get_main_queue(), ^{
                //设置界面上按钮显示,根据自己的需求设置
                [_timeButton setTitle:@"发送验证码" forState:UIControlStateNormal];
                _timeButton.userInteractionEnabled = YES;
            });
        } else {//倒计时还没结束
            int seconds = timeout % 60;
            NSString *timerStr = [NSString stringWithFormat:@"%.2d", seconds];
            dispatch_async(dispatch_get_main_queue(), ^{
                //设置界面的按钮显示,根据自己需求设置
                [_timeButton setTitle:[NSString stringWithFormat:@"%@秒后重新发送", timerStr] forState:UIControlStateNormal];
                _timeButton.userInteractionEnabled = NO;
            });
            timeout --;
        }
    });
    //将定时器挂起
    dispatch_resume(_timer);
}


Cocoa Touch

1.事件处理

iOS的响应者链UIResponder工作原理

对于iOS来说事件类型有:3种。

响应者链=事件传递+事件接收。

事件传递:

当view接收到事件时就要开始响应了(向上抛)

系统是怎么找到接收触摸事件发生的视图的?

在传递过程中:

2.UIApplication

UIApplication:提供iOS运行期间的控制和协作工作。【处理用户事件】

每一个程序在运行期间必须有且仅有一个UIApplication的一个实例。在程序开始运行的时候,UIApplicationMain函数是程序进入点,这个函数做了很多工作,其中一个重要的就是出创建了一个UIApplication的单例实例,可以通过调用[UIApplication sharedApplication]来得到这个单例实例的指针

UIApplication的一个主要工作是处理用户事件,它会挂起一个队列,把所有用户事件都放入队列,逐个处理,在处理的时候,它会发送当前事件到一个合适的目标控件来处理事件。

UIApplication实例会被赋予一个代理对象,以处理应用程序的生命周期(比如程序启动和关闭)、系统事件(比如来电)。

UIApplication生命周期。

一个UIApplication可以有如下几种状态:

3.UIView

iOS中图形显示原理

在iOS系统中所有显示的视图都是从基类UIView继承而来的,同时UIView负责接收用户交互,但是实际上你所看到的视图内容,包括图形等,都是由UIView的一个实例图层属性来绘制和渲染的,那就是CALayer.
CALayer是UIView的内部实现细节。

在每个UIView实例当中,都有一个默认的支持图层,UIView负责创建并且管理这个图层。实际上这个CALayer图层才是真正用来显示屏幕的,UIView仅仅是对它的一层封装,实现了CALaerdelegate,提供了处理事件交互的具体功能,还有动画底层方法的实现API。

UIWindow、UIView、CALayer的关系

简单说一下APP的启动过程,从main文件开始说起

程序启动分为2类:1.有storyboard 2.没有storyboard
有storyboard情况下:

没有storyboard情况下

XIB与Storyboard的优缺点

XIB:

Storyboard

4.UIViewController

ViewController视图控制器生命周期

实际场景

viewDidUnload什么时候调用?

当内存警告时,系统可能会释放view,将view赋值为nil,并且调用viewDidUnload方法。

2.@property中有哪些属性关键字(后面有哪些修饰符)?


@property 的说明可以有以下几种:

* readwrite 是可读可写特性;需要生成 getter 方法和 setter 方法
* readonly 是只读特性,只会生成 getter 方法 不会生成 setter 方法,不希望属性在类外改变时使用
* assign 是赋值特性,setter 方法将传入参数赋值给实例变量;仅设置变量时;
* retain 表示持有特性,setter 方法将传入参数先保留,再赋值,传入参数的 retain count 会+1;
* copy 表示拷贝特性,setter 方法将传入对象复制一份;需要完全一份新的变量时。
* nonatomic 和 atomic ,决定编译器生成的 setter getter是否是原子操作。 atomic 表示使用原子操作,可以在一定程度上保证线程安全。一般推荐使用 nonatomic ,因为 nonatomic 编译出的代码更快

属性分为四类:

  1. 非原子性-- nonatomic特性:

在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性。如果属性具备nonatomic特质,则不使用同步锁。请注意,尽管没有名为“atomic的特质(如果其属性不具备nonatomic特质那么都是原子的),但是仍然可以在属性特质中写明这一点,编译器不会报错,若是自己定义存取方法,那么就应该从与属性特质相符的原子性”

  1. 读写权限-- readwrite(读写)readonly(只读)
  2. 内存管理权限--

4.方法-- getter=<name>setter=<name>
getter=<name>的样式: @property (nonatomic, getter=isOn) BOOL on;

5.ARC下默认关键字:

2.1property的本质:

property有2大概念:ivar(实例变量)、存取(getter+setter=access method)

2.2@synthesize@dynamic的认识

2.3有了自动合成属性autosynthesis,@synthsize还有哪些使用场景?

不会自动合成的情况:

当你想手动管理@property的所有内容时,你就会尝试通过实现@property的所有存取方法(the accessor methods)或者使用@dynamic来达到这个目的,这时编译器就会认为你打算手动管理@property,于是编译器就禁用了autosynthesis(自动合成)。这时需要借助@synthesize来手动合成ivar。

3.客户端优化

3.1iOS安装包优化

3.2内存优化

3.3性能优化

SDWebImage的原理实现机制。

https://github.com/DevDragonLi/Dev-Repo/blob/master/interview-Set(iOS)/interview-iOS-2.md

2.SDImageCache 是如何做数据管理的?

SDImageCache分为两个部分,一个是内存层面的,一个是硬盘层面的。
内存层面:相当于是个缓存器。以key-value形式存储图片,当内存不够的时候,会清除所有缓存图片。文件替换方式是以时间为单位,删除时间大于7天的图片文件。
SDWebImageManagerSDImageCache要资源师,先搜素内存层面的数据,如果有直接返回。
若没,访问磁盘,将图片从磁盘读取出来,做Decoder(图片解码),将图片对象放到内存层面做备份,再返回调用层。

3.内部做Decoder的原因(以空间换时间)

由于UIImageImageWithData函数是每次画图时才将NSData解压成ARGB图像的,这样效率低,但是只有瞬间的内存需求。

为了提高效率通过SDWebImageDecoder将包装在Data下的资源解压,然后画在另一张图片上,这样这张新图片就不再需要重复解压了。

单元格cell重用机制的理解:

对于多变多种类型的cell,这种重用机制会导致内容出错,解决方法:

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"重用标示"];
修改为:
UITableViewCell *cell = [tableView cellForRowIndexPath:indexPath];

离屏渲染:带来界面卡顿问题 【当前屏幕渲染、离屏渲染、CPU渲染】

在OpenGL中,GPU屏幕渲染有2种方式:

设置了以下属性就会触发【离屏渲染】

为了避免卡顿问题,应尽可能使用当前屏幕渲染,可以不适用离屏渲染则尽量不用,
应当尽量避免使用layer的border,corner,shadow,mask等技术。必须离屏渲染时,简单的视图使用CPU渲染,相对复杂的视图使用一般的离屏渲染。

使用drawRect有什么影响

CPU渲染与离屏渲染的区别

由于GPU的浮点运算能力 比 CPU强,CPU渲染的效率可能不如离屏渲染。但如果仅仅是实现一个简单的效果,直接使用 CPU 渲染的效率又可能比离屏渲染好,毕竟普通的离屏渲染要涉及到缓冲区创建和上下文切换等耗时操作。对一些简单的绘制过程来说,这个过程有可能用CoreGraphics,全部用CPU来完成反而会比GPU做得更好。一个常见的 CPU 渲染的例子是:重写 drawRect 方法,并且使用任何 Core Graphics 的技术进行了绘制操作,就涉及到了 CPU 渲染。整个渲染过程由 CPU 在 App 内同步地完成,渲染得到的bitmap最后再交由GPU用于显示。总之,具体使用 CPU 渲染还是使用 GPU 离屏渲染更多的时候需要进行性能上的具体比较才可以。

一个常见的性能优化的例子就是如何给 UIView/UIImageView 加圆角。

如下是三种加圆角的方式:

如下是这三种方法的比较:

cornerRadius

view.layer.cornerRadius = 6.0;
view.layer.masksToBounds = YES;

这种方式会触发两次离屏渲染,如果在滚动页面中这么做的话就会遇到性能问题。当然我们可以进行缓存以优化性能,如下:

view.layer.shouldRasterize = YES;
view.layer.rasterizationScale = [UIScreen mainScreen].scale;

shouldRasterize = YES 会使视图渲染内容被缓存起来,下次绘制的时候可以直接显示缓存,当然要在视图内容不改变的情况下。

注意:png 图片 在 UIImageView 这样处理圆角是不会产生离屏渲染的。(ios9.0之后不会离屏渲染,ios9.0之前还是会离屏渲染)。

UIBezierPath

- (void)drawRect:(CGRect)rect {
  CGRect bounds = self.bounds;
  [[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:8.0] addClip];

  [self.image drawInRect:bounds];
}

这种方法会触发一次离屏渲染,很多资料推崇这种写法,但是这种方式会导致内存暴增,并且同样会触发离屏渲染。

Core Graphics(为 UIView 加圆角)与直接截取图片(为 UIImageView 加圆角)

正如你所期待的那样,这种方法应该是极具效率的正确的姿势。这里将为 UIView 添加圆角与为 UIImageView 添加圆角进行区分。

使用 Core Graphics 为 UIView 加圆角

这种做法的原理是利用 Core Graphics 自己画出了一个圆角矩形。

func kt_drawRectWithRoundedCorner(radius radius: CGFloat,  
                                  borderWidth: CGFloat,
                                  backgroundColor: UIColor,
                                  borderColor: UIColor) -> UIImage {    
    UIGraphicsBeginImageContextWithOptions(sizeToFit, false, UIScreen.mainScreen().scale)
    let context = UIGraphicsGetCurrentContext()

    CGContextMoveToPoint(context, 开始位置);  // 开始坐标右边开始
    CGContextAddArcToPoint(context, x1, y1, x2, y2, radius);  // 这种类型的代码重复四次

    CGContextDrawPath(UIGraphicsGetCurrentContext(), .FillStroke)
    let output = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return output
}

这个方法返回的是 UIImage,有了这个图片后,就可以创建一个 UIImageView 并插入到视图层级的底部:

extension UIView {  
    func kt_addCorner(radius radius: CGFloat,
                      borderWidth: CGFloat,
                      backgroundColor: UIColor,
                      borderColor: UIColor) {
        let imageView = UIImageView(image: kt_drawRectWithRoundedCorner(radius: radius,
                                    borderWidth: borderWidth,
                                    backgroundColor: backgroundColor,
                                    borderColor: borderColor))
        self.insertSubview(imageView, atIndex: 0)
    }
}

在调用时 只需要像这样写:

let view = UIView(frame: CGRectMake(1,2,3,4))  
view.kt_addCorner(radius: 6) 

直接截取图片为 UIImageView 加圆角

这里的实现思路是直接截取图片:

extension UIImage {  
    func kt_drawRectWithRoundedCorner(radius radius: CGFloat, _ sizetoFit: CGSize) -> UIImage {
        let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: sizetoFit)

        UIGraphicsBeginImageContextWithOptions(rect.size, false, UIScreen.mainScreen().scale)
        CGContextAddPath(UIGraphicsGetCurrentContext(),
            UIBezierPath(roundedRect: rect, byRoundingCorners: UIRectCorner.AllCorners,
                cornerRadii: CGSize(width: radius, height: radius)).CGPath)
        CGContextClip(UIGraphicsGetCurrentContext())

        self.drawInRect(rect)
        CGContextDrawPath(UIGraphicsGetCurrentContext(), .FillStroke)
        let output = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        return output
    }
}

圆角路径直接用贝塞尔曲线绘制。这个函数的效果是将原来的 UIImage 剪裁出圆角。配合着这函数,我们可以为 UIImageView 拓展一个设置圆角的方法:

extension UIImageView {  
    /**
     / !!!只有当 imageView 不为nil 时,调用此方法才有效果

     :param: radius 圆角半径
     */
    override func kt_addCorner(radius radius: CGFloat) {
        self.image = self.image?.kt_drawRectWithRoundedCorner(radius: radius, self.bounds.size)
    }
}

在调用时只需要像如下这样写:

let imageView = let imgView1 = UIImageView(image: UIImage(name: ""))  
imageView.kt_addCorner(radius: 6)  

注意:需要小心使用背景颜色。因为没有设置 masksToBounds,因此超出圆角的部分依然会被显示。因此不应该再使用背景颜色,可以在绘制圆角矩形时设置填充颜色来达到类似效果。

总结

参考链接

4.持久化方案(数据存储)

FMDatabase用来执行SQL语句FMResultSet执行查询后的结果集

iOS中常用的数据存储方式有哪些?

4.1简单描述客户端缓存机制?

1.缓存可以分为:内存数据缓存、数据库缓存、沙盒文件缓存
2.当获取数据的时候:

4.2SQLite数据存储怎么用?

1.添加SQLite动态库 libsqlite3.dylib
2.导入头文件:#import<sqlite3.h>
3.利用C语言函数创建、打开数据库,编写SQL语句
4.第三方框架 FMDB对SQLite3封装

4.3CoreData:对SQLite数据库的封装

1.NSManagedObject:实体对象(1个类对应一张表,1个对象对应表中的一条记录)
2.NSPersistentStoreCoordinator:相当于存储器,决定了你的数据存储在什么地方(SQLite、XML、其他文件)
3.NSManagedObjectContext:操作数据库。在多线程中安全,是一个单例。
要想在多线程中访问CoreData,最好的办法是一个线程一个NSManagedObjectContext

NSManagedObjectContext会在使用NSPersistentStoreCoordinator前上锁,
每个NSManagedObjectContext对象实例都可以使用同一个NSPersistentStoreCoordinator实例。

5.对沙盒的理解

6.设计模式

6.1通知NSNotificationKVO``观察键值监听的区别和用法是什么?

NSNotification通知模式:对于跨模块的类交互,可以给多个对象传递数据消息。一个通知能被多个对象接收,一个对象能接收多个通知。比如:每个应用程序都有一个通知中心,键盘

6.2KVO观察者模式:键值监听,监听某个类的属性值的变化,当发生变化时,而做出相应的改变。比如:监听模型里面某个属性值的变化,发生变化时,更新UI显示

KVO性能不好,底层会动态产生新的类。【在运行期会动态地创建这个类的派生类,在这个派生类中重写基类的setter方法,去观察属性的变化。】
特点:一个对象的属性能被多个对象监听,1个对象能监听多个对象的其他属性

6.3通知模式

在iOS中广播通知(broadcast notification)、本地通知(local notification)、推送通知(push notification)。
区别:

广播通知的实现原理:
在通知机制中对某个通知感兴趣的所有对象都可以成为接收者

6.4Delegate代理模式

实现原理:

示例:UITextField为例,

6.5单例模式

什么是单例模式?
单例模式的作用?

如何实现一个单例模式:

有三种方式:

1.简单 单例模式:可以延迟加载,按需分配内存来节省开销。但这并不是一个线程安全的写法,比如2个或多个线程并发的调用sharedInstance方法,可能会得到多个实例。

Cocoa 库本身在一些地方也使用了单例模式,例如[NSNotificationCenter defaultCenter],[UIColor redColor]

Singleton.h
#import "Foundation/Foundation.h"
@interface Singleton:NSObject
+ (Singleton *)sharedInstance;
@end
Singleton.m
#import "Singleton.h"
static Singleton *instance = nil;

@implementation Singleton
+(Singleton *)sharedInstance{
    if(!instance){
        instance = [[super allocWithZone:NULL] init];
    }
    return instance;
}

2.加锁 @synchronized

可以使用@synchronized进行加锁。这种写法是懒加载,保证了线程安全,但是锁的存在当多线程访问时,性能会降低。

+ (Singleton *)sharedInstance{
    @synchronized (self){
        if(!instance){
            instance = [[super alloc] init];
        }
    }
}

3.GCD,推荐使用的。满足了线程安全问题,GCD可以确保以更快的方式完成这些检测,它可以保证block中的代码在任何线程通过dispatch_once 调用之前被执行,但它不会强制每次调用这个函数都让代码进行同步控制

函数原型如下:

void dispatch_once(
    dispatch_once_t *predicate,
    dispatch_block_t block);

实现思路:

*当一个变量初始化为alloc时,内部会自动调用allocWithZone方法*
+ (id)allocWithZone:(struct_NSZone *)zone{
    if(_instace == nil){
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken,^{
            _instace = [super allocWithZone:zone];
        });
    }
    return _instace;
}

*重写init初始化方法*
- (id)init{
    static dispatch_once_t once_Token;
    dispatch_once(&onceToken,^{
        _instace = [super init];
    });
    return _instace;
}

当关键字 使用copy的时候,会自动调用这个方法,把对象分配到zone里

+ (id)copyWithZone:(struct _NSZone *)zone 
{ 
return _instace; 
} 

+ (id)mutableCopyWithZone:(struct _NSZone *)zone 
{ 
return _instace; 
}



重写retain,保证这个对象一直到在,不会创建一次就被释放
- (id)retain{
   return self;
}
保证对象的计数器一直都为1
- (NSUInteger)retainCount{
    return 1;
 }
释放对象
- (void)release 
{ 

} 

6.6分类模式category

iOS中的category是在运行时决定的,在已经存在的类中添加方法,在不修改原来模型的基础上扩充方法。

区分:extension类扩展:是在编译时决定的,属于类的 一部分。

在编译时和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它随着类产生和消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension。【所以无法为系统的类比如NSString添加extension

区分OC中类的扩展--Protocol,Category和Extension

Protocol:Delegate(委托)是Cocoa中常见的一种设计模式,是依赖于protocol这个语言特性的。

OC是单继承的,OC中的类可以实现多个protocol来实现类似C++中多重继承的效果。
Protocol译为协议。定义了一个方法列表,这个方法列表中的方法可以使用@required,@optional标注,以表示该方法是否是客户类必须要实现的方法。一个protocol可以继承其他的 protocol.

@protocol test<NSObject>//NSObject也就是一个protocol,这里即继承NSObject里的犯法
- (void)Print;
@end

@interface B:NSObject<test>
- (void)Print;//默认方法是@required的,必须实现的
@end

@protocol和category中如何让使用@property


objc_setAssociatedObject
objc_getAssociatedObject

反射

6.7工厂模式(Factory)

让一个类的实例化延迟到子类中进行
比如:生成控件的API,封装成一套全是扩展的方法

12.多线程

12.1基本介绍

多线程的原理:

同一时间,CPU只能处理一条线程,只有一条线程在工作。多线程并发【同时】执行,其实就是CPU快速地在多条线程之间调度切换,如果CPU调度线程的时间够快,就造成了多线程并发执行的假象。
如果线程非常多,CPU会在N多条线程之间调度,CPU会累死,消耗大量的CPU资源,每条线程被调度执行的频次会降低。

多线程底层的实现:

(1)搞清楚什么是线程、多线程?
(2)Mach是第一个以多线程方式处理任务的系统,【Mach是一个系统,比Linux、unix都要早】,因此多线程的底层实现机制是基于Mach的线程
(3)开发中很少用到Mach级的线程,因为Mach级的线程没有提供多线程的基本特征,线程实现是独立的。
(4)开发中的多线程方案:

多线程的应用:

一个程序运行后,默认会开启一条线程,称为"主线程或父线程"
主线程的主要作用:处理事件【点击事件、滚动事件、拖拽事件】
主线程的使用注意:别把比较耗时的操作放到主线程中,耗时操作会卡住主线程,严重影响流畅度,给用户一种“卡”的坏体验。

种类

1.NSThread:轻量级的线程。一个NSThread对象表示一条线程.缺点:需要自己管理线程的生命周期,线程同步,线程同步对数据的加锁会有一定的系统开销。

2.NSOperation:优点不需要关心线程管理,数据同步的事情。 NSOperation和NSOperationQueue也能实现多线程编程

因为NSOperation是一个抽象类,不能封装操作,必须使用它的子类,NSInvocationOperation和NSBlockOperation来创建操作对象,开启执行操作。

下面看NSInvocationOperation封装操作 默认情况下。操作对象在主线程中执行,是同步执行,只有将NSInvocationOperation添加到NSOperationQueue队列中才会异步执行操作

实现具体步骤:

(1)封装对象:先将需要执行的操作封装到一个NSOperation对象中
(2)添加对象到队列:将NSOperation对象添加到NSOperationQueue中
(3)系统自动取出对象:系统会自动将NSOperationQueue中的NSOperation对象取出来
(4)将取出的NSOperation封装的操作放到一条新的线程中执行

NSOperationQueue操作队列。

作用:NSOperation可以调用start方法来执行任务,但默认是同步执行的,

如果将NSOperation添加到NSOperationQueue操作队列中,
系统会自动异步执行NSOperation中的操作,添加操作到NSOperationQueue中,自动执行操作,自动开启线程。

3.GCD(Grand Central Dispatch)伟大的中枢调度器--异步执行任务的技术之一

前言

GCD是纯C语言的APT,我们在编写GCD相关代码的时候,面对的是函数而不是方法,GCD函数大部分都是以dispatch开头,因为GCDlibdispatch这个库中,这个调度库包含了GCD所有的东西,在程序运行的过程中,程序会自动加载这个库,不需要手动导入。

3.1GCD内部怎么实现的?

3.2GCD的优势:

3.3GCD的核心:任务和队列

基本常识:任务:执行什么操作队列:用来存放任务
GCD的使用主要就2个步骤:
(1)定制任务
(2)确定想做的事情

将任务添加到队列中,GCD会自动将队列中的任务取出,放到对应的线程中执行。
GCD任务block的取出只支持FIFO队列【Dispatch Queue通过结构体和链表,实现FIFO
NSOperationQueue可以方便调整执行顺序,设置最大并发数量

3.3.1GCD的核心--任务

1.GCD:有2个用来执行任务的函数: 把右边的(block)任务提交给左边的(queue)队列执行。

基本常识:同步异步--决定要不要开启新的线程

同步:在当前线程中执行任务,不具备开启新线程的能力
异步:在新的线程中执行任务,具备开启新线程的能力

(1)同步方式执行任务:在当前线程中执行``dispatch_sync(dispatch_queue_t queue, ^(void)block);
(2)异步方式执行任务:在另一条线程中执行`` dispatch_async(<#dispatch_queue_t queue#>, ^(void)block);

总结:

dispatch_sync中block的执行线程和dispatch_sync上下文线程是同一个线程;
dispatch_async中的block的执行线程和dispatch_async上下文不是同一个线程
主队列中的异步任务还是在主队列中执行
串行队列中的任务,无论是同步执行还是异步执行,执行顺序都是FIFO

3.3.2GCD的核心--队列

GCD队列分为串行队列【任务一个接一个执行】并行队列:让多个线程并发同时执行,自动开启多个线程同时执行任务 只在异步(dispatch_async)函数下才有效

基本常识:串行并发--决定了任务的执行方式

串行:一个任务执行完后再执行下一个
并发:多个任务并发同时执行

串行队列:

(1)利用dispatch_queue_create创建串行队列
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t);或者
dispatch_queue_t queue = dispatch_queue_create("alisa", NULL);

(2)使用主队列(跟主线程相关的队列)
主队列是GCD自带的一种特殊的串行队列,放在队列中的任务,都会放到主线程中执行, 如果把任务放到主队列中执行,不论处理函数是异步还是同步都不会开启新的线程
dispatch_queue_t queue = dispatch_get_main_queue();

并发队列:

GCD默认提供了全局的并发队列,供整个应用使用,不需要手动创建
(1)获得全局的并发队列,priority是优先级。 参数 flags暂时传0
dispatch_queue_t dispatch_get_global_queue(dispatch_queue_priority_t priority,unsigned long flages);

(2)获得全局并发队列 优先级默认是一个全局的并发队列。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

//全局并发数列的优先级:都是宏
DISPATCH_QUEUE_PRIORITY_HIGH 高

DISPATCH_QUEUE_PRIORITY_DEFAULT 默认

DISPATCH_QUEUE_PRIORITY_LOW 低

DISPATCH_QUEUE_PRIORITY_BACKGROUND 后台

代码示例
文顶顶的GCD代码示例

//用异步函数往并发队列中添加任务:同时开启2个子线程
- (void)async{
    
    //1.获得全局的并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //2.添加任务到队列中:用异步函数:具有开启新线程的能力
    dispatch_async(queue, ^{
       NSLog(@"下载图片1------%@",[NSThread currentThread]) ;
    });
    
    dispatch_async(queue, ^{
        NSLog(@"下载图片2------%@",[NSThread currentThread]) ;
    });
    //打印主线程
    NSLog(@"主线程------%@",[NSThread mainThread]);
    
}

3.3.3与NSOperationQueue的区别

(1)GCD是纯C语言的API,NSOperationQueue是基于GCD的OC版本的封装
(2)GCD只支持FIFO队列,NSOperationQueue可以方便地调整执行顺序,设置最大并发数量
(3)NSOperationQueue可以在Operation间设置依赖关系,而GCD需要写很多的代码才能实现
(4)NSOperationQueue支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceld)
(5)GCD的执行速度比NSOperationQueue快

任务之间不太互相依赖:GCD。
任务之间有依赖或者要监听任务的执行情况用:NSOperationQUeue。

12.4应用:用NSOperation 异步下载图片

1.用NSOperation 异步下载图片,创建一个全局的queue来管理下载图片的操作。

//存放所有下载操作的队列
@property (nonatomic, strong) NSOperationQueue *queue;

2.创建2个字典来存放所有的下载操作和下载完成的图片

//存放所有的下载操作(url是key,operation的对象是value)
@property (nonatomic, strong) NSMutableDictionary *operations;

//存放所有下载完成的图片,用于内存缓存,同样用url作为key
@property (nonatomic, strong) NSMutableDictionary *images;

3.具体思路:

刷新当前行的图片数据
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];

//存放到沙盒中的代码
if(image){//防止下载失败为空赋值导致崩溃
    vc.images[app.icon] = image;
    //下载完成的图片存入沙盒中
    //UIImage-->NSData-->File(文件)
    NSData *ImageData = UIImagePNGRepresentation(image);
    
    NSString *CachesPath = [NSSearchPathForDirectoriesInDomains(NSCacheDirectory,NSUserDomainMask,YES) lastObject];
    NSString *filePath = [CachesPath stringByAppendingPathComponent:[app.icon lastPathComponent]];
    
    [ImageData writeToFile:filePath atomically:YES];
}


3.4使用GCD来替代performSelector的原因

3.4.1 performSelector会导致内存泄露问题

performSelector:调用一个方法,编译器并不知道将要调用的selector是什么,就不了解其方法签名及返回值,甚至是否有返回值也不知道。
由于编译器不知道方法名,所以没法用ARC的内存规则来判定返回值是不是该释放。
因此,ARC采用了比较谨慎的做法,就是不加释放操作,那么这就可能导致内存泄露了,因为方法在返回值对象时已经将其保留了。

3.4.2performSelector返回值只能是void或者对象类型(id类型)

如果想返回整数或浮点数等scalar类型(标量)值,那么就需要执行一些复杂的转换操作,而这种转换操作很容易出错。
由于id类型表示指向任何OC对象的指针,所以,只要返回的大小和指针所占大小相同就行。
也就是说,在32位架构的计算机上,可以返回任意32位大小的类型,而64位架构的计算机上,则可以返回任意64位大小的类型。
可以返回NSNumber进行转换,若返回的类型为C语言结构体,则不可使用performSelector方法

3.4.3performSelector提供的方法局限性大


- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;

具备延迟功能的方法无法处理带有2个参数的选择子。而能够指定执行线程的哪些方法,则与之类似,所以不是特别通用。如果要用这些方法,就得把很多参数打包到字典中,然后在被调用的方法中将这些参数提取出来,这样会增加开销,同时也提高了产生bug的可能性。

3.5 GCD中block的使用问题

3.5.1 GCD会对block进行复制

Dispatch Queue对添加的Block会进行复制,在完成执行后自动释放。不需要手动添加block到queue时 显式复制

3.5.2 GCD中的autorelease pool自动缓存池

GCD dispatch queue 有自己的autorelease pool来管理内存对象,但是不保证在什么时候会进行回收,如果在block中创建了大量的对象,可以添加自己的autorelease pool来进行管理。

3.5.3 GCD中国在开新的线程执行任务一定比较快吗?

如果对于工作量小的block,切换线程的开销大于直接在原来线程上执行block的开销
这样会导致新的线程没有原来的线程执行的快

3.5.4 GCD中的block会造成循环引用吗?

会。如果控制器持有block,会造成循环引用 如果只持有队列queue是不会造成循环引用的。

3.6GCD常见用法

3.6.1延迟执行

- (void)delayOperation
{
    //延迟执行:5秒钟之后,执行block中的代码段
    //1.主队列中,在主队列中执行
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)( 5* NSEC_PER_SEC)) , queue, ^{
        //5秒钟后要执行的操作;
    });
    
    //2.在并发队列中
    dispatch_queue_t Bqueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //设置时间
    dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC));
    //开启一条新的线程执行
    dispatch_after(when, Bqueue, ^{
        NSLog(@"并发延迟执行的操作");
    });
}

3.6.2只执行一次的代码

- (void)onlyOnetime
{
    //不用dispatch_once的写法
    if (_log == NO) {
        NSLog(@"该行代码执行一次");
        _log = YES;
    }
    
    //使用dispatch_once
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"该行代码执行一次");
    });
    
}

3.6.3变更优先级dispatch_set_target_queue

- (void)setTargetqueue{
    //默认优先级为Global Dispatch Queue
    dispatch_queue_t mySerial = dispatch_queue_create("串行", NULL);
    dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
    //变更优先级
    dispatch_set_target_queue(mySerial, globalDispatchQueueBackground);
}

3.6.4Dispatch Group:追加到Dispatch Queue中的多个处理全部结束后,执行结束处理。

比如:【如何用GCD同步若干个异步调用?(如根据若干个rul异步加载多张图片,然后在都下载完成后合成一张整图)】

使用Dispatch Group,追加3个block到Dispatch Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中处理结束的block

- (void)dispatchGroup
{
    //创建一个全局队列
    dispatch_queue_t  queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //穿件一个组队列
    dispatch_group_t group = dispatch_group_create();
    
    //将block任务添加到队列中
    dispatch_group_async(group, queue, ^{
        NSLog(@"block1");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"block2");
        
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"block3");
    });
    
    //dispatch_group_notify 追加结束处理的函数
    //dispatch_group_notify函数会将执行的block追加到Dispatch Queue中
    dispatch_group_notify(group, queue, ^{
        NSLog(@"done");
    });
    
    //等待group的处理,DISPATCH_TIME_FOREVER表示只要处理尚未执行结束,就会一直等待,中途不能取消。DISPATCH_TIME_NOW,则不用任何等待即可判断属于Dispatch Group的处理是否执行结束。
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    //主线程的Runloop的每次循环中,可检查执行是否结束,从而不耗费多余的等待时间,虽然这样也可以,但一般这种情况下,还是用dispatch_group_notify函数,【追加结束处理到Main Dispatch Queue主队列】中。
    
    
    //释放
    dispatch_release(group);
}

3.6.5Dispatch_barrier_async:的作用:解决数据竞争问题


- (void)dispatch_barrier_async{
    //当访问数据库户文件的时候,写入处理由串行的主队列处理,读取处理追加到并发队列中,写入处理在任一个读取处理没有执行的状态下,追加到串行队列中即可。【在写入处理结束之前,读取处理是不能执行的】
    //使用barrier来等待之前任务完成,避免数据竞争问题。
    //dispatc_barrier_async函数同dispatch_queue_create函数生成的并发队列一起使用
    dispatch_queue_t queue = dispatch_queue_create("dispatch_barrier_async", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue,^{
        NSLog(@"block1");
    });
    dispatch_async(queue,^{
        NSLog(@"block2");
    });
    
    dispatch_barrier_sync(queue, ^{
        NSLog(@"处理数据竞争问题");
    });
    dispatch_async(queue, ^{
        NSLog(@"block3");
    });
}

GCD的暂停和继续

3.6.6Dispatch_suspend:不执行已追加的处理,即挂起已追加的队列,当可以执行时再回复

dispatch_suspend函数挂起指定的 Dispatch Queue
dispatch_suspend(queue);
dispatch_resume函数恢复指定的 Dispatch Queue
dispatch_resume(queue);
这些处理对已经执行的处理没有影响,挂起后,追加到Dispatch Queue中但尚未执行的处理在这之后停止执行。dispatch_rusume函数则使得这些处理能够执行

3.6.7实际运用:有些图片加载的比较慢怎么处理?如何优化程序的性能?

(1)图片下载放在异步线程
(2)图片下载过程中使用占位图片
(3)如果图片较大,可以考虑多线程断点下载 【开多条线程同时下载,设置http的请求头信息,比如 字节数:bytes = 0-1024个字节 1024-5120个字节】

3.7 Dispatch Source分派源--基础数据类型,协调特定底层系统事件的处理

是基于BSD系内核惯有功能kqueue的包装
GCD 支持以下 dispatch source :

3.7.1Dispatch Source的实现

Dispatch source替代了异步回调函数,来处理系统相关的事件,当你配置一个dispatch source时,你指定要监测的事件。
dispatch queue以及处理事件的代码 (block或函数)当事件发生时,dispatch source会提交 block或函数到指定的queue去执行。

·dispatch source提交的block或函数和手工提交到queue的任务不同,dispatch source为应用提供连续的事件源。除非你显式地取消,dispatch source会一直 保留与dispatch queue的关联。只要相关的事件发生,就会提交关联的代码到dispatch queue去执行。

为了防止事件积压到 dispatch queue,dispatch source实现了事件合并机制
如果新事件在上一个事件处理器出列并执行之前到达,dispatch source会将新旧事件的数据合并。根据事件类型的不同,合并操作可能会替换旧事件,或者更新旧事件的信息。

3.7.2Dispatch Source和Dispatch Queue两者在线程执行上的关系

两者线程上没有关系,独立运行
Dispatch Queue 相当于任务生产者
Dispatch Source 相当于处理任务的消费者。可以一边异步生产,一边异步消费
可以在任意线程上调用dispatch_source_merge_data以触发dispatch_source_set_event_handler.而句柄的执行线程,取决于你创建句柄时所指定的线程
自定义源也需要一个队列,用来处理所有的响应句柄block。那么就有2个队列了,一个队列用来执行自定义源,另一个队列用来执行句柄。

13.AFNetWorking的认识

13.1从NSRLConnection到NSURLSession

14 应用程序类

1414.1在iOS7之前,后台执行内容有几种形式都是什么?

一般的应用在进入后台的时候 可以 获取一定时间来运行相关任务,也就是说可以在后台运行一小段时间(10s左右)
1.后台音乐播放
2.后台GPS跟踪
3.后台voip支持 电话
iOS7之后在plist文件中可以修改

14.2AFN与ASI的区别

ASIHTTPRequest外号"HTTP终结者",功能强大,可已停止更新。
AFNetworking简单易用,提供了基本够用的常用功能。
原理分析:
ASI的性能优于AFN,ASI是基于CFNetwork框架开发的,而AFN是基于NSURL.

ASI是基于CFHTTP开发的一个组件,而AFN的基础是NSURL。ASI更加底层。

在使用NSOperation和NSOperationQueue上的实现

AFHTTPClient是一个封装了一系列操作方法的“工具类”,处理请求的操作类是一系列单独的,基于NSOperation封装的,AFURLConnectionOperation的子类。
AFN的示例代码中通过一个静态方法,使用dispatch_once()的方式创建AFHTTPClient的共享实例,这也是官方建议的使用方法。

在创建AFHTTPClient的初始化方法中,创建了OperationQueue并设置一系列参数默认值。在getPath:parameters:success:failure方法中创建NSURLRequest,以NSURLRequest对象实例作为参数,创建一个NSOperation,并加入在初始化发方中创建的NSOperationQueue。

以上操作都是在主线程中完成的。在NSOperation的start方法中,以此前创建的NSURLRequest对象为参数创建NSURLConnection并开启连结。

14.3程序自己关掉和程序进入后台,远程推送的区别

1.程序自己关掉:关掉后不执行任何代码,不能处理事件
2.程序进入后台:程序进入后台状态不久后转入挂起状态。

在这种状态下,应用程序不执行任何代码,并有可能在任意时候从内存中删除。
只有当用户再次运行此应用,应用才会从挂起状态唤醒,代码才能继续执行
或者进入后台时开启多线程状态,保留在内存中,这样就可以执行系统允许的动作

3.远程推送是由远程服务器上的程序发送到APNS[苹果的一个与远程推送相关的机制,主要是获取到用户的设备id [device Token],根据device Token来推送消息]
--> 再由APNS把消息推送至设备上的程序,当应用程序收到推送的消息会自动调用特定的方法执行实现写好的代码

14.4本地通知和远程推送通知的基本概念和用法?

概念:本地推送和远程推送通知都可以向不在前台运行的应用发送消息,这种消息可能是即将发送的事件,也可能是服务器的新数据。不管是本地通知还是远程通知,它们在程序界面的显示效果相同,都可能显示为一段警告信息或者应用程序图标上的徽章。
目的:都是让应用程序能够通知用户某些事情,而且不需要应用程序在前台运行。
区别:本地通知由本应用负责调用,只能从当前设备上的iOS发出。
而远程通知由远程服务器上的程序发送到APNS,再由APNS把消息推送至用户设备上的程序。【国内有极光推送jpush】

14.5客户端安全性处理方式?

传输数据和保存数据方面

14.6sip是什么?

SIP(Session Initiation Protocol),会话发起协议

SIP是建立VOIP连接的IETE标准,IETE是全球互联网最具权威的技术标准化组织
VOIP,就是网络电话,直接用互联网打电话,不用耗费手机话费

14.7如何实现一个框架或者库给别人使用?如果设想和设计框架的public的API,并指出大概需要如何做,需要注意一些什么方面,来使别人容易地使用你的框架。

(1)提供给外界的接口功能是否实用,够用

(2)别人使用我的框架时,能不能根据类名、方法名就能猜出接口的具体作用

(3)别人调用接口时,提供的参数是否够用,调用起来是否简单

(4)别人使用我的框架时,要不要导入依赖其他的框架

14.8手机架构与性能调试?

手机架构:项目一般分层,分为UI层-->``业务层(处理业务逻辑)-->``工具层(处理业务的手段,网络,存储)

(1)刚接手公司的旧项目时,模块比较多,而且代码几乎写在控制器里面,比如UI控件代码,网络请求代码,数据存储代码
(2)接下来才去MVC模式进行封装、重构。这也会造成Controller很臃肿
比如封装,重构:自定义UI控件封装内部的业务逻辑
封装网络请求工具类
封装数据存储工具类

14.9怎么实现原子锁

iOS中一般用nonatomic,在Mac中用atomic。自己加一把锁,在调用getter,setter方法时加锁。

@property (atomic, assign) int age;

    - (void)setAge:(int)age{
    @synchronized(self) {
        _age = age;
    }
}
    

14.10即时通讯中的大数据处理

即时通讯需要长时间连接(长连接,一般用socket实现),发送put,post,https请求上传大数据到文件服务器,然后利用TCP\IP发带url的自定义格式给对方,对方接收到之后下载。

Socket 建立网络连接的步骤

建立Socket连接至少需要一对套接字,其中一个运行于客户端,称为ClientSocket,另一个运行于服务器端,称为ServerSocket

套接字之间的连接过程分为三个步骤:

#import "AsyncSocket/AsyncSocket.h"
@interface ViewController ()
@property (nonatomic) AsyncSocket *socket;
@property (nonatomic, copy) NSString *socketHost;
@property (nonatomic, assign) uint16_t socketPort;
@property (nonatomic) NSTimer *connectTimer;
@end

//socketConnect
- (void)socketConnectHost {
    self.socket = [[AsyncSocket alloc] initWithDelegate:self];
    NSError *error = nil;
    [self.socket connectToHost:self.socketHost onPort:self.socketPort withTimeout:-1 error:&error];
}


- (void)onSocket:(AsyncSocket *)sock didConnectToHost:(NSString *)host port:(UInt16)port {
    NSLog(@"连接成功");
    //每个3s 向服务器发送心跳包
    self.connectTimer = [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(longConnectToSocket) userInfo:nil repeats:YES];
    
    [self.connectTimer fire];
}


- (void)longConnectToSocket {
    NSLog(@"在longConnectToSocket方法中进行长连接需要向服务器发送的讯息");
    // socket发送数据是以栈的形式存放,所有数据放在一个栈中,存取时会出现粘包的现象,所以很多时候服务器在收发数据时是以先发送内容字节长度,再发送内容的形式,得到数据时也是先得到一个长度,再根据这个长度在栈中读取这个长度的字节流,如果是这种情况,发送数据时只需在发送内容前发送一个长度,发送方法与发送内容一样
    NSData   *dataStream ; //[@8 dataUsingEncoding:NSUTF8StringEncoding];
    
    
    [self.socket writeData:dataStream withTimeout:1 tag:1];
    // 接收数据
}

- (void)onSocket:(AsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag {
    //对得到的data值进行解析与转换即可
    [self.socket readDataWithTimeout:30 tag:0];
}

网络安全

计算机网络面临的安全性威胁

网络安全协议

运输层(TCP)安全协议:安全套接层SSL(Secure Socket Layer)。

SSL对客户端端与服务器之间传送数据进行加密和鉴别。在双方联络阶段(握手阶段)对将要使用的加密算法(RSA)和双方共享的会话密钥进行协商,完成客户端与服务端之间的鉴别。
不同角度来看

SSL三大功能

SSL工作原理 以浏览器和服务器通信为例

网络通讯中加密方式有哪些,各自原理?

用户需要上传和下载一个重要的资料文件,应该如何判断操作成功?

ReactiveCocoa(RAC)如何防止UIButton短时间内多次重复点击,大概思路?

你一般是如何调试Bug的?

界面多个网络请求,如何处理刷新

**界面同时有多个网络请求,这时还没得到数据,已经提前刷新导致界面空白 **

1. 调度组
dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"netWorking_Frist");
        
    });
    
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"netWorking_Second");
    });
    dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"netWorking_Three");
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"complete");
    });
    
        
2. 信号量

- (void)netWorkingimplementation{ // 基于自身项目的网络请求
//    1. 发起请求
    dispatch_semaphore_t  semaphore = dispatch_semaphore_create(0);
//    2. 成功/失败回调标记
    dispatch_semaphore_signal(semaphore);
//    3. 计数为0 ,则一直等待
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
    

如何是init方法私有化

- (instancetype)init {
   // 抛出不识别,没有说明真的原因
    [super doesNotRecognizeSelector:_cmd];
    return nil;
}
- (instancetype)init1{
    NSAssert(false,@"unavailable, use sharedInstance instead");
    return nil;
}
- (instancetype)init2{
    [NSException raise:NSGenericException format:@"Disabled. Use +[%@ %@] instead",
     NSStringFromClass([self class]),
     NSStringFromSelector(@selector(sharedInstance))];
    
    return nil;
}

如果tableView界面网络请求有缓存数据逻辑

代理(Delegate)--软件设计模式

  1. 委托方---->(要求代理方需要实现的接口方法)协议。
  2. 协议----> (协议中可以定义方法和属性)--->
  3. 代理方---->(按照协议去实现方法,可能会有返回值。)
  4. 代理方---->委托方(代理方返回一个处理结果给委托方。)
  5. 委托方---->代理方(委托方调用代理方遵从的协议方法 。)

注意

上一篇下一篇

猜你喜欢

热点阅读