事件传递和响应链
事件传递:
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(核心图形)
- 它是iOS的核心图形库,包含Quartz2D绘图API接口,常用的是point,size,rect等这些图形,都定义在这个框架中,类名以CG开头的都属于CoreGraphics框架,它提供的都是C语言函数接口,是可以在iOS和mac OS 通用的。
- iOS系统本身提供了两套绘图的框架,即UIBezierPath 和 Core Graphics。而前者所属UIKit,其实是对Core Graphics框架关于path的进一步封装,所以使用起来比较简单。但是毕竟Core Graphics更接近底层,所以它更加强大。
CoreAnimation(核心动画)
CoreAnimation翻译过来就是核心动画,一组非常强大的API,用来做动画的,非常的简单,但是效果非常绚丽。
- CoreAnimation是跨平台的,既可以支持IOS,也支持MAC OS。
- CoreAnimation执行动画是在后台,不会阻塞主线程。
- CoreAnimation作用在CALayer,不是UIView。
- CoreGraphics和CoreAnimation的关系:它们都是跨iOS和Mac OS 使用的,这点区别于UIKit,并且CoreAnimation中大量使用到CoreGraphics中的类,因为实现动画要用到图形库中的东西。
- 可以看出,CoreGraphics是底层绘制框架,我们实际会用到的也就是CG开头的一些底层绘制函数和变量,这是一个纯C语言框架。
- 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修饰符
- 需声明该类遵从 NSCopying 协议
- 实现 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函数会多次执行。
- 当父类和子类都实现load函数时,父类的load方法执行顺序要优先于子类
- 类中的load方法执行顺序要优先于类别(Category)
- 当有多个类别(Category)都实现了load方法,这几个load方法都会执行,但执行顺序不确定(其执行顺序与类别在Compile Sources中出现的顺序一致)
- 当然当有多个不同的类的时候,每个类load 执行顺序与其在Compile Sources出现的顺序一致
注意:
load调用时机比较早,当load调用时,其他类可能还没加载完成,运行环境不安全.
load方法是线程安全的,它使用了锁,我们应该避免线程阻塞在load方法.
+initialize方法要点
initialize在类或者其子类的第一个方法被调用前调用。即使类文件被引用进项目,但是没有使用,initialize不会被调用。由于是系统自动调用,也不需要显式的调用父类的initialize,否则父类的initialize会被多次执行。假如这个类放到代码中,而这段代码并没有被执行,这个函数是不会被执行的。
- 父类的initialize方法会比子类先执行
- 当子类不实现initialize方法,会把父类的实现继承过来调用一遍。在此之前,父类的方法会被优先调用一次
- 当有多个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开发中方法延迟执行的几种方式
- performSelector方法
- NSTimer定时器
- NSThread线程的sleep
- 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];
- 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(扩展)和继承的区别
- 分类是运行时决议,扩展是编译时决议
- 分类有名字,扩展没有名字
- 分类包含声明和实现,扩展只有实现
- 可为系统的类添加分类,但不能添加扩展
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或数据单例类)
- extern (在上一个控制器中声明一个熟悉为外部已定义的属性)