iOS常用知识

事件传递和响应链

事件传递:
UIApplication->UIWindow->UIViewController->View->subView (从UIWindow开始会使用hitTest:withEvent方法倒序遍历来寻找最适合响应事件的view)
响应者链:
如果上面找到的最适合的view不响应事件,就会往下传递,传给下一个响应者。subView->View->UIViewController->UIWindow->UIApplication

 - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event  //此方法返回一个最适合响应事件的view,由最后添加的子视图开始遍历。(另:内部会调用pointInside:withEvent:方法)
 - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event  //此方法返回一个BOOL值,判断点击点是否在当前视图上

内部实现:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
 // 1.判断下窗口能否接收事件
 if (self.userInteractionEnabled == NO || self.hidden == YES ||  self.alpha <= 0.01) return nil;
 // 2.判断下点在不在窗口上
 // 不在窗口上
 if ([self pointInside:point withEvent:event] == NO) return nil;
 // 3.从后往前遍历子控件数组
 int count = (int)self.subviews.count;
 for (int i = count - 1; i >= 0; i--)     {
     // 获取子控件
     UIView *childView = self.subviews[i];
     // 坐标系的转换,把窗口上的点转换为子控件上的点
     // 把自己控件上的点转换成子控件上的点
     CGPoint childP = [self convertPoint:point toView:childView];
     UIView *fitView = [childView hitTest:childP withEvent:event];
     if (fitView) {
         // 如果能找到最合适的view
         return fitView;
     }
 }
 // 4.没有找到更合适的view,也就是没有比自己更合适的view
 return self;
}
// 作用:判断下传入过来的点在不在方法调用者的坐标系上
// point:是方法调用者坐标系上的点
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
 // 判断点在不在区域内
  if (CGRectContainsPoint(self.button.bounds, tempoint))
 {
     return YES ;
   }
return NO ;
}

View的刷新

setNeedsDisplay:会自动调用drawRect方法
setNeedsLayout:会默认调用layoutSubViews方法
setNeedsDisplay多用于绘图,setNeedsLayout多用于加载数据后更新UI

LayoutSubviews在以下情况下会被调用:
addSubview、改变view的Frame、滚动UIScrollView、旋转屏幕会触发父UIView的layoutSubviews方法、调用setLayoutSubviews、调用LayoutIfNeeded。以上情况都是会触发layoutSubviews方法的。(注:init初始化的时候是不会触发的)

LayoutIfNeeded 如果,有需要刷新的标记,立即调用layoutSubviews进行布局

扩大按钮响应区

重写- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event 方法

touch、gesture、action方法哪个优先调用

Button的响应优先级touch -> action(UIControlEventTouchDown) -> gesture

  • touchBegin会拦截事件,action不会

离屏渲染

概念:在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
导致离屏渲染的情况:光栅化、遮罩、圆角、阴影

  • 光栅化是将一个layer预先渲染成位图(bitmap),然后加入缓存中

  • 下面方法添加圆角会造成离屏渲染。(可通过绘图技术,绘制出圆角图片)

self.view.layer.cornerRadius = 5.0f;
self.view.layer.masksToBounds = YES;

TableView方法调用顺序

tableView:numberOfRowsInSection:
tableView:estimatedHeightForRowAtIndexPath:   //如果没有使用预估行高,此次会先调用tableView:heightForRowAtIndexPath:
tableView:cellForRowAtIndexPath:
tableView:heightForRowAtIndexPath:

UITableView 的优化

1). 正确的复用cell。
2). 设计统一规格的Cell
3). 提前计算并缓存好高度(布局),因为heightForRowAtIndexPath:是调用最频繁的方法;
4). 异步绘制,遇到复杂界面,遇到性能瓶颈时,可能就是突破口;
5). 减少子视图的层级关系
6). 尽量使所有的视图不透明化以及做切圆操作。(使用绘图技术来实现圆角)
7). 不要动态的add 或者 remove 子控件。最好在初始化时就添加完,然后通过hidden来控制是否显示。
8). 使用调试工具分析问题。(Time Profile)

如何实现cell的动态的行高

如果希望每条数据显示自身的行高:
设置预估行高 tableView.estimatedRowHeight = 200。
设置定义行高 tableView.rowHeight = UITableViewAutomaticDimension。
另外cell里面的自动布局如果要有高度的约束。

ViewController生命周期

按照执行顺序排列:
1. init 
2. awakeFromNib:nib文件被加载的时候,会发生一个awakeFromNib的消息到nib文件中的每个对象。  (当有xib文件时)    
3. loadView:开始加载视图控制器自带的view。
4. viewDidLoad:视图控制器的view被加载完成。  
5. viewWillAppear:视图控制器的view将要显示在window上。
6. updateViewConstraints:视图控制器的view开始更新AutoLayout约束。
7. viewWillLayoutSubviews:视图控制器的view将要更新内容视图的位置。
8. viewDidLayoutSubviews:视图控制器的view已经更新视图的位置。
9. viewDidAppear:视图控制器的view已经展示到window上。 
10. viewWillDisappear:视图控制器的view将要从window上消失。
11. viewDidDisappear:视图控制器的view已经从window上消失。
12. dealloc

CoreAnimation和CoreGraphics

CoreGraphics(核心图形)

  1. 它是iOS的核心图形库,包含Quartz2D绘图API接口,常用的是point,size,rect等这些图形,都定义在这个框架中,类名以CG开头的都属于CoreGraphics框架,它提供的都是C语言函数接口,是可以在iOS和mac OS 通用的。
  2. iOS系统本身提供了两套绘图的框架,即UIBezierPath 和 Core Graphics。而前者所属UIKit,其实是对Core Graphics框架关于path的进一步封装,所以使用起来比较简单。但是毕竟Core Graphics更接近底层,所以它更加强大。

CoreAnimation(核心动画)
CoreAnimation翻译过来就是核心动画,一组非常强大的API,用来做动画的,非常的简单,但是效果非常绚丽。

  1. CoreAnimation是跨平台的,既可以支持IOS,也支持MAC OS。
  2. CoreAnimation执行动画是在后台,不会阻塞主线程。
  3. CoreAnimation作用在CALayer,不是UIView。
  4. CoreGraphics和CoreAnimation的关系:它们都是跨iOS和Mac OS 使用的,这点区别于UIKit,并且CoreAnimation中大量使用到CoreGraphics中的类,因为实现动画要用到图形库中的东西。
  5. 可以看出,CoreGraphics是底层绘制框架,我们实际会用到的也就是CG开头的一些底层绘制函数和变量,这是一个纯C语言框架。
  6. QuartzCore(包含CoreAnimation)框架,是iOS系统的基本渲染框架,是一个OC语言框架,是一套基于CoreGraphics的OC语言封装,封装出了基本渲染类CALayer

有哪些属性关键字

属性可以拥有的特质分为四类:
原子性---atomic、nonatomic (atomic:系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。比如,线程 A 的 getter 方法运行到一半,线程 B 调用了 setter:那么线程 A 的 getter 还是能得到一个完好无损的对象,不会出现错乱数据。不过atomic可并不能保证线程安全。如果线程 A 调了 getter,与此同时线程 B 、线程 C 都调了 setter——那最后线程 A get 到的值,3种都有可能)

读/写权限---readwrite(读写)、readonly (只读)

内存管理---assign、strong、 weak、copy、unsafe_unretained (unsafe_unretained跟weak相似,但当对象销毁时,会依然指向之前的内存空间(造成野指针)。使用场景:当你明确对象的生命周期的时候,可以使用unsafe_unretained替代weak,可以稍微提高一些性能,虽然这点性能微乎其微。)
是否可为空 nonnull, nullable, null_resettable

属性默认的关键字是什么

对象: atomic,readwrite,strong
基本数据类型:atomic,readwrite,assign

weak的原理

系统会将weak的属性记录在一张hash表里,当该属性的引用计数为0的时候,则将nil值赋值给该属性。

__weak和__block

__weak 本身是可以避免循环引用的问题的,但是其会导致外部对象释放了之后,block 内部也访问不到这个对象的问题,我们可以通过在 block 内部声明一个 __strong 的变量来指向 weakObj,使外部对象既能在 block 内部保持住,又能避免循环引用的问题。

__block的作用是可以Block内部修改外边的变量的值,它其实提升了变量的作用域,它可以把放在栈区的局部变量拷贝到堆区。
__block 本身无法避免循环引用的问题,但是我们可以通过在 block 内部手动把 blockObj 赋值为 nil 的方式来避免循环引用的问题。

NSMutableArray能否用copy修饰

不能。NSMutableArray用copy属性修饰调用set方法后会变成NSArray。

原因: 不管是集合类对象(NSArray,NSDictionary,NSSet...),还是非集合类对象(NSString),接收到copy或者mutableCopy消息时,都需遵循以下准则:

  • copy返回的都是不可变对象
  • mutableCopy 返回的都是可变对象

copy关键字什么时候用

用途:
1. NSString、NSArray、NSDictionary 等经常使用copy关键字。
说明:
因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;(如果不用copy修饰时,当str(NSString) = mStr(NSMutableString),而mStr改变时,str会跟着改变)

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

如何让自己的类用copy修饰符

  1. 需声明该类遵从 NSCopying 协议
  2. 实现 NSCopying 协议的方法:- (id)copyWithZone:(NSZone *)zone;

@synthesize和@dynamic

@synthesize是合成实例变量,它作用是为属性添加一个实例变量名,或者说别名。同时会为该属性生成 setter/getter 方法。
@dynamic 声明属性是用来告知系统不要自动生成setter和getter方法

非ARC下的retain、copy、和assign的setter方法

- (void)setRetainStr:(NSString *)retainStr
{
    if (_retainStr != retainStr) {
        [_retainStr release];
        _retainStr = [retainStr retain];
    }
}

- (void)setCopStr:(NSString *)copStr
{
    if (_copStr != copStr) {
        [_copStr release];
        _copStr = [copStr copy];
    }
}

- (void)setAssignStr:(NSString *)assignStr
{
    _assignStr = assignStr;
}

@property 的本质是什么

@property = 实例变量 + getter + setter

+load和+initialize

load和initialize的共同点
如果父类和子类都被调用,父类的调用一定在子类之前

+load方法要点
当类被引用进项目的时候就会执行load函数(在main函数开始执行之前),与这个类是否被用到无关,每个类的load函数只会自动调用一次.由于load函数是系统自动加载的,因此不需要再调用[super load],否则父类的load函数会多次执行。

  1. 当父类和子类都实现load函数时,父类的load方法执行顺序要优先于子类
  2. 类中的load方法执行顺序要优先于类别(Category)
  3. 当有多个类别(Category)都实现了load方法,这几个load方法都会执行,但执行顺序不确定(其执行顺序与类别在Compile Sources中出现的顺序一致)
  4. 当然当有多个不同的类的时候,每个类load 执行顺序与其在Compile Sources出现的顺序一致

注意:
load调用时机比较早,当load调用时,其他类可能还没加载完成,运行环境不安全.
load方法是线程安全的,它使用了锁,我们应该避免线程阻塞在load方法.

+initialize方法要点
initialize在类或者其子类的第一个方法被调用前调用。即使类文件被引用进项目,但是没有使用,initialize不会被调用。由于是系统自动调用,也不需要显式的调用父类的initialize,否则父类的initialize会被多次执行。假如这个类放到代码中,而这段代码并没有被执行,这个函数是不会被执行的。

  1. 父类的initialize方法会比子类先执行
  2. 当子类不实现initialize方法,会把父类的实现继承过来调用一遍。在此之前,父类的方法会被优先调用一次
  3. 当有多个Category都实现了initialize方法,会覆盖类中的方法,只执行一个(会执行Compile Sources 列表中最后一个Category 的initialize方法)

注意:
在initialize方法收到调用时,运行环境基本健全。
initialize内部也使用了锁,所以是线程安全的。但同时要避免阻塞线程,不要再使用锁

使用场景:
load:如runtime交换两个方法的实现
initialize:如对一些static变量初始化(它不能在load中执行)

内存问题检查

内存检查:
1.Analyze静态分析 (command + shift + b),主要分析逻辑错误(访问空指针或未初始化的变量等)和内存管理错误(如内存泄漏)
2.使用Instruments工具的Leaks工具(product->profile ->leaks 打开工具主窗口)
3.观察Xcode的实时监测内存占用情况
4.第三方工具,如MLeaksFinder(查找未释放的类(在pop或dismiss的时候会调用自定义的willDealloc方法,如果2秒后未释放,就打印可能出现内存泄露的信息))

一般易造成泄漏的点:
1.循环引用,Block强引用,NSTimer释放不当
2.CoreFoundation或C语言的方式手动申请的内存,忘记释放

Instruments的常用工具

1). Time Profiler: 性能分析
2). Leaks:检查内存,看是否有内存泄露。
3). Zombies:检查是否访问了僵尸对象,但是这个工具只能从上往下检查,不智能。
4). Allocations:用来检查内存,写算法的那批人也用这个来检查。

Runtime

Runtime又叫运行时,是一套底层的C语言API,其为iOS内部的核心之一,我们平时编写的OC代码,底层都是基于它来实现的。

Runtime实现的机制是什么
1). 使用时需要导入的头文件
2). Runtime 运行时机制,它是一套C语言库。
3). 实际上我们编写的所有OC代码,最终都是转成了runtime库的东西。
比如:
类转成了 Runtime 库里面的结构体等数据类型,
方法转成了 Runtime 库里面的C语言函数,
平时调方法都是转成了 objc_msgSend 函数(所以说OC有个消息发送机制)

OC是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。
[stu show]; 在objc动态编译时,会被转意为:objc_msgSend(stu, @selector(show));
4). 因此,可以说 Runtime 是OC的底层实现,是OC的幕后执行者。

Method Swizzle什么时候用
它是一个在运行时候改变一个已存在的选择器对应的实现的过程
1). 可以用 method_exchangeImplementations 来交换2个方法中的IMP。
2). 可以用 class_replaceMethod 来修改类。
3). 可以用 method_setImplementation 来直接设置某个方法的IMP。
(注:IMP有点类似函数指针,指向具体的方法实现)

** _objc_msgForward**
_objc_msgForward是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。

其他
在#import 下能看到相关的方法,
objc_getClass()和class_copyMethodList()获取过私有API;
Method method1 = class_getInstanceMethod(cls, sel1);

runloop

它是运行循环,它内部就是do-while循环,在这个循环内部不断地处理各种任务。

runloop五个相关的类
a.CFRunloopRef
b.CFRunloopModeRef【Runloop的运行模式】
c.CFRunloopSourceRef【Runloop要处理的事件源】
d.CFRunloopTimerRef【Timer事件】
e.CFRunloopObserverRef【Runloop的观察者(监听者)】

CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer
每次RunLoop启动时,只能指定其中一个Mode,这个Mode被称作CurrentMode
如果需要切换Mode,只能退出Loop ,在重新指定一个Mode进入
这样做主要是为了分割开不同组的Source/Timer/Observer,让其互不影响

runloop模式 (系统默认注册了5个Mode常用的有3个)
kCFRynLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响 (正常情况下不会执行定时器,拖拽UIScrollview的时候才会执行定时器任务)
kCFRunLoopCommonModes:这是一个占位用的Mode,不是一种真正的Mode (NSRunLoopCommonModes = NSDefaultRunLoopMode && UITrackingRunLoopMode)
UIInitializationRunLoopMode:在刚启动App时进入的第一个Mode,启动完成后不再使用
GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到

runloop和线程的关系
主线程默认开启的runloop, 一个线程对应一个runloop,线程也可以没有runloop,runloop保证了线程的持续运行。

Runloop的启动方法

[[NSRunLoop mainRunLoop]run];
[[NSRunLoop mainRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];

可参考:https://www.jianshu.com/p/2d3c8e084205

NSTimer

如何让计时器调用一个类方法
计时器只能调用实例方法,但是可以在这个实例方法里面调用静态方法。

使用计时器需要注意
计时器一定要加入RunLoop中,并且选好model才能运行。scheduledTimerWithTimeInterval方法创建一个计时器并加入到RunLoop中所以可以直接使用。
如果计时器的repeats选择YES说明这个计时器会重复执行,一定要在合适的时机调用计时器的invalid。不能在dealloc中调用,因为一旦设置为repeats 为yes,计时器会强持有self,导致dealloc永远不会被调用,这个类就永远无法被释放。比如可以在viewDidDisappear中调用,这样当类需要被回收的时候就可以正常进入dealloc中了。

 [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];

-(void)timerMethod
{
//调用类方法
    [[self class] staticMethod];
}

-(void)invalid
{
    [timer invalid];
    timer = nil;
}

NSTimer创建后,会在哪个线程运行。
手动创建的Timer,加入到哪个线程的RunLoop中就运行在哪个线程。
但用scheduledTimerWithTimeInterval创建的,在哪个线程创建就会被加入哪个线程的RunLoop中就运行在哪个线程(因为它被加入当前线程的runloop里)

制作一个定时器,使它在滑动列表时不受影响
解决方法其一是将timer加入到NSRunloopCommonModes中。其二是将timer放到另一个线程中,然后开启另一个线程的runloop,这样可以保证与主线程互不干扰。

// 方法1
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
// 方法2
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(repeat:) userInfo:nil repeats:true];
    [[NSRunLoop currentRunLoop] run];
});

iOS开发中方法延迟执行的几种方式

  1. performSelector方法
  2. NSTimer定时器
  3. NSThread线程的sleep
  4. GCD (dispatch after 和 设置精准定时器)
//使用GCD制作精准定时器
- (void)gcdTime
{
    /** 创建定时器对象
     * para1: DISPATCH_SOURCE_TYPE_TIMER 为定时器类型
     * para2-3: 中间两个参数对定时器无用
     * para4: 最后为在什么调度队列中使用
     */
    _gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    /** 设置定时器
     * para2: 任务开始时间
     * para3: 任务的间隔
     * para4: 可接受的误差时间,设置0即不允许出现误差
     * Tips: 单位均为纳秒
     */
    dispatch_source_set_timer(_gcdTimer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
    /** 设置定时器任务
     * 可以通过block方式
     * 也可以通过C函数方式
     */
    dispatch_source_set_event_handler(_gcdTimer, ^{
        static int gcdIdx = 0;
        NSLog(@"GCD Method: %d", gcdIdx++);
        NSLog(@"%@", [NSThread currentThread]);
        
        if(gcdIdx == 5) {
            // 终止定时器
            dispatch_suspend(_gcdTimer);
        }
    });
    // 启动任务,GCD计时器创建后需要手动启动
    dispatch_resume(_gcdTimer);
}

多线程

iOS多线程技术有哪几种方式
pthread、NSThread、GCD、NSOperation

1. NSOperation

NSOperationQueue *queue = [[NSOperationQueue alloc] init];(子线程)  [NSOperationQueue mainQueue](主线程)
[queue addOperationWithBlock:^{}]; 添加operation
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{ }]
[queue addOperation:operation1];
queue.maxConcurrentOperationCount = 3; //最大并发数
[queue cancelAllOperations]; //取消
[operationB addDependency:operationA]; //线程之间的依赖

2. GCD

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    dispatch_async(dispatch_get_main_queue(), ^{

**3. GCD ground

- (void)gcdGroup
{
    // 使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。
    // 创建队列组
    dispatch_group_t group = dispatch_group_create();
    // 获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //dispatch_async(queue, ^{ });
    dispatch_group_async(group, queue, ^{
        for(int i = 30030; i > 0 ; i--) {
            for(int i = 30030; i > 0 ; i--) {
                
            }
        }
        NSLog(@"加载图片1");
        
    });
    dispatch_group_async(group, queue, ^{NSLog(@"加载图片2");});
    dispatch_group_async(group, queue, ^{NSLog(@"加载图片3");});
    
    // 当并发队列组中的任务执行完毕后才会执行这里的代码
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"合并图片");
    });
    
    
}

** 4. 栅栏函数**

- (void)gcdBarrier
{
    //dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    // 1.创建并发队列 (队列要用这种创建方式才可以)
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    
    // 2.向队列中添加任务
    dispatch_async(queue, ^{  // 1.2是并行的
        for(int i = 30030; i > 0 ; i--) {
            for(int i = 30030; i > 0 ; i--) {
                
            }
        }
        NSLog(@"任务1, %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任务2, %@",[NSThread currentThread]);
    });
    
    dispatch_barrier_async(queue, ^{
        NSLog(@"任务 barrier, %@", [NSThread currentThread]);
    });
    
    dispatch_async(queue, ^{   // 这两个是同时执行的
        NSLog(@"任务3, %@",[NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"任务4, %@",[NSThread currentThread]);
    });
    
    // 输出结果: 任务1 任务2 ——》 任务 barrier ——》任务3 任务4
    // 其中的任务1与任务2,任务3与任务4 由于是并行处理先后顺序不定。
}

5. 同步死锁

//使用sync函数在当前串行队列执行任务就会产生死锁
- (void)deadLock
{
    NSLog(@"1");
    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"2");
    });
    
    NSLog(@"3");
    
    //dispatch_sync:立马在当前线程执行
    dispatch_sync(dispatch_get_main_queue(), ^{  //执行到dispatch_sync同步方法,主线程进入等待,不往下执行,死锁
        NSLog(@"4");
    });
    NSLog(@"5");
    //但是如果是换一个队列,或者使用并发队列的话就不会死锁

    //dispatch_get_main_queue,队列先进先出,执行完当前任务后才能执行新的任务
    //dispatch_async,异步,不要求立马在当前线程执行
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"2");
    });
}

使用GCD制作精准定时器

- (void)gcdTime
{
    /** 创建定时器对象
     * para1: DISPATCH_SOURCE_TYPE_TIMER 为定时器类型
     * para2-3: 中间两个参数对定时器无用
     * para4: 最后为在什么调度队列中使用
     */
    _gcdTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    /** 设置定时器
     * para2: 任务开始时间
     * para3: 任务的间隔
     * para4: 可接受的误差时间,设置0即不允许出现误差
     * Tips: 单位均为纳秒
     */
    dispatch_source_set_timer(_gcdTimer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
    /** 设置定时器任务
     * 可以通过block方式
     * 也可以通过C函数方式
     */
    dispatch_source_set_event_handler(_gcdTimer, ^{
        static int gcdIdx = 0;
        NSLog(@"GCD Method: %d", gcdIdx++);
        NSLog(@"%@", [NSThread currentThread]);
        
        if(gcdIdx == 5) {
            // 终止定时器
            dispatch_suspend(_gcdTimer);
        }
    });
    // 启动任务,GCD计时器创建后需要手动启动
    dispatch_resume(_gcdTimer);

}

isa指针

isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调 用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。
同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass)。根元类的isa指针指向本身,这样形成了一个封闭的内循环。

iOS数据持久化方法

plist, NSUserDefault, NSKeyArchiver, SQLite, CoreData

iOS的沙盒目录结构

1). Application:存放程序源文件,上架前经过数字签名,上架后不可修改。
2). Documents:常用目录,iCloud备份目录,存放数据。(这里不能存缓存文件,否则上架不被通过)
3). Library:
Caches:存放体积大又不需要备份的数据。(常用的缓存路径)
Preference:设置目录,iCloud会备份设置信息。
4). tmp:存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除的可能。

SQLite基本语法:

创表:create table if not exist
增:insert;删:delete;改:update;查:query

SQLite FMDB基本语句

// 创建数据库实例对象
self.db = [FMDatabase databaseWithPath:filename];
// 创表
BOOL result = [self.db executeUpdate:@"create table if not exists t_Record (type int, title text, startTime date, endTime date, state text, Id long);"];
// 增
res = [self.db executeUpdate:@"insert into t_Record (type, title, startTime, endTime, state, Id) values (?, ?, ?, ?, ?, ?);",
       @(record.type), record.title, record.startTime, record.endTime, record.state, @(record.Id)];
// 改
res = [self.db executeUpdate:@"update t_Record set type = ? title = ?, startTime = ?, endTime = ?, state = ? where Id = ?;",
       @(record.type), record.title, record.startTime, record.endTime, record.state, @(record.Id)];
// 查询
//    FMResultSet *rs = [self.db executeQuery:@"select * from t_Schedule where creatTimeNum > ?;", @0];
FMResultSet *rs = [self.db executeQuery:@"select * from t_Record"];
// 删除
BOOL res = [_db executeUpdate:@"delete from t_Record where Id = ?", @(record.Id)];

//事务
__block BOOL success = YES;
[self.queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
    @try {
        success = [db executeUpdate:@"update t_student set age = ? where name = ?;", @20, @"jack"];
    }
    @catch (NSException *exception) {
        *rollback = YES;
        success = NO;
    }
    @finally {
        *rollback = !success;
    }
}];

或: [db beginTransaction]; [db rollback]; [db commit];

fmdb如何使用异步存储

把fmdb封装成一个单例,在单例力创建一个全局的信号量,操作数据库时都通过信号量控制,包括增删改查

CoreData基本使用

创建:
1.创建表Company.xcdatamodel,添加实体
2.创建实体关联类,继承自NSManagedObject
3.创建NSManagedObjectContext,包含NSPersistentStoreCoordinator
4.增:Employee *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:_context];
[_context save:&error];

  1. FectchRequest 抓取请求对象 , 谓语查找NSPredicate, 排序NSSortDescriptor
    NSArray *emps = [_context executeFetchRequest:request error:&error];

另外可以通过创建工程勾选coreData,APPDelegate会自动生成NSPersistentContainer类,包含ViewContext对象

优点:可视化操作(表、关联对象),不用写SQL语句;
缺点:笨重,增加、删除字段时要进行数据库升级,否则会造成闪退

注:
NSPersistentStoreCoordinator是持久化存储协调者,主要用于协调
托管对象上下文和持久化存储区之间的关系。
NSManagedObjectContext使用协调者的托管对象模型将数据保存到数
据库,或查询数据。

iOS常用设计模式

单例模式、代理模式、观察者模式(通知和KVO)、MVC、MVP、MVVM

单例

static id _instance;

@implementation Tool

+ (instancetype)shareTool
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc]init];
    });
    
    return _instance;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [super allocWithZone:zone];
    });
    
    return _instance;
}

- (id)copyWithZone:(NSZone *)zone
{
    return _instance;
}

@end

一般用分类好还是继承好

一般情况用分类好,用Category去重写类的方法,仅对本Category有效,不会影响到其他类与原有类的关系。分类还能精简原类的代码,使类结构更清晰,此外还能给系统的类添加分类

Category(类别)、 Extension(扩展)和继承的区别

  1. 分类是运行时决议,扩展是编译时决议
  2. 分类有名字,扩展没有名字
  3. 分类包含声明和实现,扩展只有实现
  4. 可为系统的类添加分类,但不能添加扩展

KVC

KVC是键值编码,一种通过字符串间接访问对象的方式(即给属性赋值)

KVC的底层实现
当一个对象调用setValue方法时,方法内部会做以下操作:
1). 检查是否存在相应的key的set方法,如果存在,就调用set方法。
2). 如果set方法不存在,就会查找与key相同名称并且带下划线的成员变量,如果有,则直接给成员变量属性赋值。
3). 如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值。
4). 如果还没有找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。
这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。

KVO

KVO(key-Value-Observing):键值观察机制,他提供了观察某一属性变化的方法。

使用KVO分为三个步骤:
1.通过addObserver:forKeyPath:options:context:方法注册观察者,观察者可以接收keyPath属性的变化事件。
2.在观察者中实现observeValueForKeyPath:ofObject:change:context:方法,当keyPath属性发生改变后,KVO会回调这个方法来通知观察者。
3.当观察者不需要监听时,可以调用removeObserver:forKeyPath:方法将KVO移除。需要注意的是,调用removeObserver需要在观察者消失之前,否则会导致Crash。

KVO的底层实现
KVO是基于runtime机制实现。
1.当一个类的属性首次被监听的时候,会在运行阶段生成一个新的派生类,对应的isa指针也会指向新的派生类,在这个派生类中被监听的属性的setter方法会增加willChangeValueForKey和didlChangeValueForKey方法。
2.当属性改变时,会调用willChangeValueForKey记录旧的值,然后属性值改变,接着调用didChangeValueForKey,这时kvo的observeValueForKeyPath就会被调用,实现监听的效果。

手动触发kvo

[self willChangeValueForKey:@"balance"];
 _balance = theBalance;
[self didChangeValueForKey:@"balance"];

KVC会触发KVO吗
会,KVC的底层实现就是会先调用setter方法的。

访问并修改一个类的私有属性

1). 一种是通过KVC获取。
2). 通过runtime访问并修改私有属性。

方法和选择器有何不同

selector是一个方法的名字,方法是一个组合体,包含了名字和实现

OC中的反射机制

1). class反射
    通过类名的字符串形式实例化对象。
        Class class = NSClassFromString(@"student"); 
        Student *stu = [[class alloc] init];
    将类名变为字符串。
        Class class =[Student class];
        NSString *className = NSStringFromClass(class);
2). SEL的反射
    通过方法的字符串形式实例化方法。
        SEL selector = NSSelectorFromString(@"setName");  
        [stu performSelector:selector withObject:@"Mike"];
    将方法变成字符串。
        NSStringFromSelector(@selector*(setName:));

通知的完整过程

a. app注册远程推送通知并向APNS请求device token
b. app接收到APNS返回的token
c. app发送token给服务器
d. 服务器将要推送的内容和对应token发送给APNS (另:如果是使用第三方推送如JPush,则是服务器调用JPush的API,JPush发送给APNS)
e. APNS发出推送到手机

1.应用程序注册APNs推送消息。
说明:
a.只有注册过的应用才有可能接收到消息,程序中通常通过UIApplication的registerUserNotificationSettings:方法注册,iOS8中通知注册的方法发生了改变,如果是iOS7及之前版本的iOS请参考其他代码。
b.注册之前有两个前提条件必须准备好:开发配置文件(provisioning profile,也就是.mobileprovision后缀的文件)的App ID不能使用通配ID必须使用指定APP ID并且生成配置文件中选择Push Notifications服务,一般的开发配置文件无法完成注册;应用程序的Bundle Identifier必须和生成配置文件使用的APP ID完全一致。

2.iOS从APNs接收device token,在应用程序获取device token。
说明:
a.在UIApplication的-(void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken代理方法中获取令牌,此方法发生在注册之后。
b.如果无法正确获得device token可以在UIApplication的-(void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error代理方法中查看详细错误信息,此方法发生在获取device token失败之后。
c.必须真机调试,模拟器无法获取device token。

3.iOS应用将device token发送给应用程序提供商,告诉服务器端当前设备允许接收消息。
说明:
a.device token的生成算法只有Apple掌握,为了确保算法发生变化后仍然能够正常接收服务器端发送的通知,每次应用程序启动都重新获得device token(注意:device token的获取不会造成性能问题,苹果官方已经做过优化)。
b.通常可以创建一个网络连接发送给应用程序提供商的服务器端, 在这个过程中最好将上一次获得的device token存储起来,避免重复发送,一旦发现device token发生了变化最好将原有的device token一块发送给服务器端,服务器端删除原有令牌存储新令牌避免服务器端发送无效消息。

4.应用程序提供商在服务器端根据前面发送过来的device token组织信息发送给APNs。
说明:
a.发送时指定device token和消息内容,并且完全按照苹果官方的消息格式组织消息内容,通常情况下可以借助其他第三方消息推送框架来完成。

5.APNs根据消息中的device token查找已注册的设备推送消息。
说明:
a.正常情况下可以根据device token将消息成功推送到客户端设备中,但是也不排除用户卸载程序的情况,此时推送消息失败,APNs会将这个错误消息通知服务器端以避免资源浪费(服务器端此时可以根据错误删除已经存储的device token,下次不再发送)。

BLE

1. CoreBluetooth框架
2. CBCentralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil]
3. CBCentralManagerDelegate:
        - (void)centralManagerDidUpdateState:(CBCentralManager *)central; //状态更新发现CBCentralManagerStatePoweredOn时,开始扫描[_centralManager scanForPeripheralsWithServices:nil options:nil];
        - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI; //发现外设,连接外设[_centralManager connectPeripheral:peripheral options:nil];
        - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral; //已连接外设,查找指定服务[peripheral discoverServices:@[[CBUUID UUIDWithString:notifyServiceUUIDStirng], [CBUUID UUIDWithString:readWriteServiceUUIDStirng]]];(有通知服务、读写服务等)
        - (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error; //找到服务,在对应服务上查找指定特性[peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:stepCharacteristicUUIDStirng], [CBUUID UUIDWithString:infoCharacteristicUUIDStirng]] forService:service];
        - (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error; //找到服务上得特征(可进行读、写、订阅通知)
        - (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error; //更新特征的value (notify或read后都会调用此方法)
4.
        [peripheral setNotifyValue:YES forCharacteristic:characteristic]; //订阅通知
        [peripheral readValueForCharacteristic:characteristic]; //读
        [self.connectedPeripheral writeValue:sendData forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse];// 写数据(超过20字节的data要分包发送)
5.RSSI //信号强度

BAD_ACCESS在什么情况下出现

访问了野指针,比如访问已经释放对象的成员变量或者发消息、死循环等。

第三方框架

AFNetworking

AFNetworking主要是对NSURLSession的封装, 其中主要有以下类:

会话:
AFURLSessionManager(核心类),创建和管理NSURLSession、NSURLSessionTask,实现NSURLSessionDelegate等协议
AFHTTPSessionManager (AFURLSessionManager的子类)
网络监听模块:AFNetworkReachabilityManager
网络安全模块:AFSecurityPolicy,主要是针对 HTTPS 服务
请求序列化:AFURLRequestSerialization
响应序列化:AFURLResponseSerialization
UIKit集成模块:UIButton、UIImageView的分类等

SDWebImage

描述下SDWebImage里面给UIImageView加载图片的逻辑
SDWebImage 中为 UIImageView 提供了一个分类UIImageView+WebCache.h, 这个分类中有一个最常用的接口sd_setImageWithURL:placeholderImage:,会在真实图片出现前会先显示占位图片,当真实图片被加载出来后再替换占位图片。

加载图片的过程大致如下:
1.首先会在 SDWebImageCache 中寻找图片是否有对应的缓存, 它会以url 作为数据的索引先在内存中寻找是否有对应的缓存
2.如果缓存未找到就会利用通过MD5处理过的key来继续在磁盘中查询对应的数据, 如果找到了, 就会把磁盘中的数据加载到内存中,并将图片显示出来
3.如果在内存和磁盘缓存中都没有找到,就会向远程服务器发送请求,开始下载图片
4.下载后的图片会加入缓存中,并写入磁盘中
5.整个获取图片的过程都是在子线程中执行,获取到图片后回到主线程将图片显示出来

ios开发逆向传值的几种方法

1.代理
2.block
3.通知
4.KVO
5.单例(如通过一个manager或数据单例类)

  1. extern (在上一个控制器中声明一个熟悉为外部已定义的属性)

你可能感兴趣的:(iOS常用知识)