1.单例的写法
-
(instancetype)sharedInstance { static id sharedInstance = nil;
static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedInstance = [[self alloc] init]; });
return sharedInstance; } 2.规范写法: @property (weak, nonatomic) id analyticsDelegate;
@property (assign, nonatomic) NSInteger statusCode; @synthesize statusCode = _statusCode;
3.GCD 延时操作 dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC); dispatch_after(timer, dispatch_get_main_queue(), ^{
});
复制代码
4.设置父视图的透明度,添加的子视图不显示解决方法 设置父视图的颜色: _bottomView.backgroundColor = [[UIColor whiteColor] colorWithAlphaComponent:0.5];
5.计算时间
NSDate *date = [NSDate dateWithTimeIntervalSince1970:[finance.buildDate longLongValue] / 1000];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
NSString *strDate = [dateFormatter stringFromDate:date];
复制代码
6、NSString为何要用copy?而不是strong?
我这样说你就明白了 A->B A中的一个MutableString给B中的一个Property(NSString类型)赋值 首先是能接受的,父类可以接受子类,如果是retain,仅仅是生成一个指针,计数器加一,然后指向那个MutableString。如果MString改变,B中那个跟着改变,因为是同一块内存区域。而选择Copy相当于又生成了一个NSString,与A中的MutableString独立。
NSArray 和 NADictionary同理
7、对象回收时Weak指针自动被置为nil的实现原理 Runtime维护了一个Weak表,用于存储指向某个对象的所有Weak指针。Weak表其实是一个哈希表,Key是所指对象的地址,Value是Weak指针的地址(这个地址的值是所指对象的地址)的数组。 在对象被回收的时候,经过层层调用,会最终触发下面的方法将所有Weak指针的值设为nil。
8、响应者链 事件的传递和响应的区别: 事件的传递是从上到下(父控件到子控件),事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件。 通过hitTest:withEvent:层层找到最佳响应的视图,然后通过touchesBegan:withEvent:响应事件,如果不能响应再找父视图然后从下到上 直到UIApplication如果不能响应则废弃该事件。 http://www.jianshu.com/p/c294d1bd963d 传递过程详解: keyWindow会在它的内容视图上调用hitTest:withEvent:(该方法返回的就是处理此触摸事件的最合适view)来完成这个找寻过程。 hitTest:withEvent:在内部首先会判断该视图是否能响应触摸事件,如果不能响应,返回nil,表示该视图不响应此触摸事件。然后再调用pointInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内)。如果pointInside:withEvent:返回NO,那么hiteTest:withEvent:也直接返回nil。 如果pointInside:withEvent:返回YES,则向当前视图的所有子视图发送hitTest:withEvent:消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews数组的末尾向前遍历。直到有子视图返回非空对象或者全部子视图遍历完毕;若第一次有子视图返回非空对象,则 hitTest:withEvent:方法返回此对象,处理结束;如所有子视图都返回非,则hitTest:withEvent:方法返回该视图自身。 hitTest:底层实现 // point是该视图的坐标系上的点
- (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]) return nil; // 3.从后往前遍历自己的子控件,看是否有子控件更适合响应此事件 int count = self.subviews.count; for (int i = count - 1; i >= 0; i--) { UIView *childView = self.subviews[i]; CGPoint childPoint = [self convertPoint:point toView:childView]; UIView *fitView = [childView hitTest:childPoint withEvent:event]; if (fitView) { return fitView; } } // 没有找到比自己更合适的view return self; } http://www.jianshu.com/p/2f664e71c527 http://www.cocoachina.com/ios/20160630/16868.html
9、结合响应者链扩大button的点击范围 重写pointInside: withEvent:方法 给button添加一个范围 #import <UIKit/UIKit.h> @interface UIButton (test) @property(nonatomic, assign) UIEdgeInsets hitTestEdgeInsets; @end
#import "UIButton+test.h" #import <objc/runtime.h>
@implementation UIButton (test) @dynamic hitTestEdgeInsets;
static const NSString *KEY_HIT_TEST_EDGE_INSETS = @"HitTestEdgeInsets";
-(void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets { NSValue *value = [NSValue value:&hitTestEdgeInsets withObjCType:@encode(UIEdgeInsets)]; objc_setAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }
-(UIEdgeInsets)hitTestEdgeInsets { NSValue *value = objc_getAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS); if(value) { UIEdgeInsets edgeInsets; [value getValue:&edgeInsets]; return edgeInsets; }else { return UIEdgeInsetsZero; } }
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { if(UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero) || !self.enabled || self.hidden) { return [super pointInside:point withEvent:event]; } CGRect relativeFrame = self.bounds; CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets); //判断这个点是否在这个范围内 return CGRectContainsPoint(hitFrame, point); } @end
10、面试题 http://www.cocoachina.com/programmer/20151019/13746.html
11、观察者模式 () http://www.jb51.net/article/76122.htm http://www.cnblogs.com/goodboy-heyang/p/5265675.html kvo: 一对多, 观察者模式, 是键值监听,键值观察机制, KVO的本质是当观察者为一个对象的属性进行了注册,被观察对象的isa指针被修改的时候,isa指针就会指向一个中间类,而不是真实的类。所以 isa指针其实不需要指向实例对象真实的类。所以我们的程序最好不要依赖于isa指针。在调用类的方法的时候,最好要明确对象实例的类名 当你观察一个对象时,会动态的创建该对象类的子类,这个子类重写了被观察属性的 setter 方法,同时将该对象的 isa 指针指向了新创建的子类。在 Objective-C 中对象是通过 isa 指针来查找对应类中的方法列表的,所以这里可以把该对象看为新子类的实例对象。重写的 setter 方法会在调用原 setter 方法之后,通知观察者对象属性值的更改。 addObserver:forKeyPath:options:context: observeValueForKeyPath:ofObject:change:context
当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。 派生类在被重写的 setter 方法中实现真正的通知机制,就如前面手动实现键值观察那样。这么做是基于设置属性会调用 setter 方法,而通过重写就获得了 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。 同时派生类还重写了 class 方法以“欺骗”外部调用者它就是起初的那个类。然后系统将这个对象的 isa 指针指向这个新诞生的派生类,因此这个对象就成为该派生类的对象了,因而在该对象上对 setter 的调用就会调用重写的 setter,从而激活键值通知机制。此外,派生类还重写了 dealloc 方法来释放资源。 //自定义kvo @implementation NSObject (WSG)
-
(void)wsg_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context{
//1、自定义子类 //2、给子类重写setName方法 调用父类的setName方法,通知观察者 //3、修改当前对象的isa指针,指向自定义的子类 //Person类 NSString *oldClass = NSStringFromClass([self class]); //新类 NSString *newClass = [@"wsgKVO_" stringByAppendingString:oldClass]; //创建的子类对象 Class myClass = objc_allocateClassPair([self class], [newClass UTF8String], 0); //注册类 objc_registerClassPair(myClass);
//给类添加setName方法 class_addMethod(myClass, @selector(setName:), (IMP)setName, "v@:@");
//修改isa指针 指向新建的子类 object_setClass(self, myClass);
//保存观察者对象 objc_setAssociatedObject(self, @"objc", observer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); //保存keypath objc_setAssociatedObject(self, @"wsgkeypath", keyPath, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }
//调用父类的setName方法 通知观察者 observer void setName(id self,SEL _cmd,NSString *newName){ //调用父类的set方法 变量是一个结构体 比较负责 //objc_msgSendSuper()
//改用 修改self的isa指针指向父类 调用setName方法 然后通知观察者 值发生了改变 然后再改回self的isa指针指向 子类
id class = [self class];
//修改self的isa指向 父类
object_setClass(self, class_getSuperclass(class));
//调用父类的setName方法 /* id self, SEL op, ... */ 要编译通过 要去build-setting 搜索objc_msg 设置enable为NO
objc_msgSend(self,@selector(setName:),newName);
//通知observer 值发生了改变
id objc = objc_getAssociatedObject(self, @"objc");
id keypath = objc_getAssociatedObject(self, @"wsgkeypath");
objc_msgSend(objc, @selector(observeValueForKeyPath:ofObject:change:context:),self,keypath,nil,nil);
//把self的isa指针改为指向子类类型
object_setClass(self, class);
复制代码
} @end
KVC(Key-Value-Coding)内部的实现:是键值编码, 一个对象在调用setValue的时候,(1)首先根据方法名找到运行方法的时候所需要的环 境参数。(2)他会从自己isa指针结合环境参数,找到具体的方法实现的接口。(3)再直接查找得来的具体的方法实现。
Delegate: 通常发送者和接收者的关系是直接的一对一的关系。代理的目的是改变或传递控制链。允许一个类在某些特定时刻通知到其他类,而不需要获取到那些类的指针。可以减少框架复杂度。消息的发送者(sender)告知接收者(receiver)某个事件将要发生,delegate同意然然后发送者响应事件,delegate机制使得接收者可以改变发送者的行为。
Notification: 观察者模式, 通常发送者和接收者的关系是间接的多对多关系。 消息的发送者告知接收者事件已经发生或者将要发送,仅此而已,接收者并不能反过来影响发送者的行为。
1). 效率肯定是delegate比NSNotification高。
2). delegate方法比notification更加直接,最典型的特征是,delegate方法往往需要关注返回值,也就是delegate方法的结果。比如-windowShouldClose:,需要关心返回的是yes还是no。所以delegate方法往往包含should这个很传神的词。也就是好比你做我的delegate,我会问你我想关闭窗口你愿意吗?你需要给我一个答案,我根据你的答案来决定如何做下一步。相反的,notification最大的特色就是不关心接受者的态度,我只管把通告放出来,你接受不接受就是你的事情,同时我也不关心结果。所以notification往往用did这个词汇,比如NSWindowDidResizeNotification,那么nswindow对象放出这个notification后就什么都不管了也不会等待接受者的反应。
1)两个模块之间联系不是很紧密,就用notification传值,例如多线程之间传值用notificaiton。
2)delegate只是一种较为简单的回调,且主要用在一个模块中,例如底层功能完成了,需要把一些值传到上层去,就事先把上层的函数通过delegate传到底层,然后在底层call这个delegate,它们都在一个模块中,完成一个功能,例如说NavgationController 从 B 界面到A 点返回按钮 (调用popViewController方法) 可以用delegate比较好。
12、delegate为什么用weak @protocol LYSunDelegate @end
@interface LYSun : NSObject @property (nonatomic, weak) iddelegate; @end
#import "LYPerson.h" #import "LYSun.h"
@interface LYPerson() /** 强引用dog*/ @property (nonatomic, strong) LYSun *sun; @end
@implementation LYPerson
- (instancetype)init { self = [super init]; if (self) { // 实例化dog self.sun = [[LYSun alloc] init]; // sun的delegate引用self,self的retainCount,取决于delegate修饰,weak:retainCount不变,strong:retainCount + 1 self.sun.delegate = self; } return self; }
- (void)dealloc { NSLog(@"LYPerson----销毁"); } @end
// 实例化person, self对person弱引用,person的retainCount不变 LYPerson *person = [[LYPerson alloc] init]; 当delegate 用strong时 :当viewController不对person引用后想释放person的时候,发现这时sun.delegate对person还强引用着呢,person的retainCount为1,所以person不会释放,sun固然也不会释放,这就是造成循环引用导致的内存泄漏的原因 当delegate用weak时:当viewController 不对person引用之后,person的retainCount 为 0 ,当然可以释放啦,那么person就被释放了,sun也就被释放啦
13、retain、copy、assign对于get、set方法的影响 //assain修饰的属性 生成的set get方法 -(void)setAge:(NSInteger)age { _age =age; } -(NSInteger )age { return _age; } //retain 修饰的属性 -(void)setArray:(NSArray *)array { if (_array !=array) { [_array release]; _array =[array retain]; } }
-(NSArray*)array{ return [[_array retain]autorelease]; } //copy修饰的属性 -(void)setName:(NSString *)name { if (_name !=name) { [_name release]; _name = [name copy]; } } -(NSString *)name { return [[_name retain]autorelease]; } 14:SEL http://www.imlifengfeng.com/blog/?p=398 又叫选择器,是表示一个方法的selector的指针 本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度 IMP 是方法的实现 SEL 和IMP是一一对应的
15:tableview计算cell高度 1:专门的layout 来计算各个控件的位置:例如YYKit里https://github.com/ibireme/YYKit; 在子线程里计算好之后 在主线程刷新 提高刷新效率。 2、autolayout自动适应高度; 3、全代码在cell里的layoutSubviews里代码计算高度。 http://www.imlifengfeng.com/blog/?p=501 tableview优化:
16、coretext绘制文本
CTFramesetter是由CFAttributedString(NSAttributedString)初始化而来,可以认为它是CTFrame的一个Factory,通过传入CGPath生成相应的CTFrame并使用它进行渲染:直接以CTFrame为参数使用CTFrameDraw绘制或者从CTFrame中获取CTLine进行微调后使用CTLineDraw进行绘制。
一个CTFrame是由一行一行的CLine组成,每个CTLine又会包含若干个CTRun(既字形绘制的最小单元),通过相应的方法可以获取到不同位置的CTRun和CTLine,以实现对不同位置touch事件的响应。
http://blog.devtang.com/2015/06/27/using-coretext-1/ http://blog.devtang.com/2015/06/27/using-coretext-2/ http://blog.csdn.net/hopedark/article/details/50174157
17、runloop runloop只能在一个mode下 执行完一个mode 会休眠 等待下一个。 NSRunLoopCommonModes:占位模式 UI&默认 UITrackingRunLoopMode:UI模式 NSDefaultRunLoopMode:默认模式 每个mode都对应着 source、timer、observer source: source0:非系统内核 source1:系统内核
18、GCD 多线程 队列: 并行队列(Concurrent Dispatch Queue):可以让多个任务并行(同时)执行(自动开启多个线程同时执行任务)并行功能只有在异步(dispatch_async)函数下才有效 串行队列(Serial Dispatch Queue):让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务) 任务: 同步执行(sync):只能在当前线程中执行任务,不具备开启新线程的能力 异步执行(async):可以在新的线程中执行任务,具备开启新线程的能力
//并行队列 dispatch_queue_t queue = dispatch_queue_create("com.sunny.test", DISPATCH_QUEUE_CONCURRENT); //串行队列 dispatch_queue_t queue = dispatch_queue_create("com.test", DISPATCH_QUEUE_SERIAL); //全局并行队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); //全局队列的优先级 DISPATCH_QUEUE_PRIORITY_HIGH DISPATCH_QUEUE_PRIORITY_DEFAULT DISPATCH_QUEUE_PRIORITY_LOW DISPATCH_QUEUE_PRIORITY_BACKGROUND
创建任务: // 同步执行任务创建方法 dispatch_sync(queue, ^{ NSLog(@"%@",[NSThread currentThread]); // 这里放任务代码 }); // 异步执行任务创建方法 dispatch_async(queue, ^{ NSLog(@"%@",[NSThread currentThread]); // 这里放任务代码 });
// 回到主线程 dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"2-------%@",[NSThread currentThread]); });
//延时操作 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 2秒后异步执行这里的代码... NSLog(@"run-----"); });
//只执行一次 static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 只执行1次的代码(这里面默认是线程安全的) });
dispatch_barrier_async 需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作 dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
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]);
});
复制代码
队列组 dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 执行1个耗时的异步操作 });
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 执行1个耗时的异步操作 });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 等前面的异步操作都执行完毕后,回到主线程... });
//信号量 dispatch_semaphore //创建信号量 dispatch_semaphore_create //发送信号量 dispatch_semaphore_signal //等待信号量 dispatch_semaphore_wait
//创建一个并行队列 dispatch_queue_t queque = dispatch_queue_create("GoyakodCreated", DISPATCH_QUEUE_CONCURRENT); //异步执行 dispatch_async(queque, ^{ dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); [self getToken:semaphore]; dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); [self request]; }); 收到信号量之后 才执行接下来的任务 在getToken方法里有发送信号量方法:dispatch_semaphore_signal(semaphore); 然后会往下执行request方法 //计时 比NSTImer更精准 self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0)); dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 1000000000, 0); dispatch_source_set_event_handler(self.timer, ^{ NSLog(@"---------%@",[NSThread currentThread]); }); dispatch_resume(self.timer);
19、NSString内存分配 方法1.直接赋值: NSString *testStr1 = @"a"; 方法2.类函数初始化生成:
NSString *testStr2 = [NSString stringWithString:@"b"]; NSString *testStr3 = [NSString stringWithFormat:@"c"]; 方法3.实例方法初始化生成: NSString *testStr4 = [[NSString alloc] initWithString:@"d"]; NSString *testStr5 = [[NSString alloc] initWithFormat:@"e"]; test1,test2,test4都是在一个内存区域,也就是上文所说的常量内存区。test3,test5在一个内存区,也就是堆区 test1,test2,test4这三种的话建议用=@“字符串”来使用,因为本来就是一样的。test3,test5这两种的话,建议用texst3这种
20、dsYM文件 分析 xcode 打包 archive 之后 会生成文件. xcarchive 路径(~/Library/Developer/Xcode/Archives) 每一个xx.app 和 xx.app.dSYM都有对应的UUID,crash文件也有自己的UUID,只要这三个文件的UUID一致,通过命令行解析出错误的函数地址, 查看xx.app.dSYM 的UUID dwarfdump --uuid xx.app.dSYM
查看crash地址对应的函数 atos -arch arm64 -o Zeus -l 0x10063000 0x00000001009795d4
21、HMAC加密 替代MD5加密 MD5加密的加盐加密 在APP中存在危险 如果盐泄露 将导致整个加密流程收到威胁 修改盐 影响所有用户。 // HMACMD5加密方法 每个用户有一个key 从服务端获取 可以更新key
- (NSString *)hmacMD5StringWithKey:(NSString *)key { const char *keyData = key.UTF8String; const char *strData = self.UTF8String; uint8_t buffer[CC_MD5_DIGEST_LENGTH]; CCHmac(kCCHmacAlgMD5, keyData, strlen(keyData), strData, strlen(strData), buffer); return [self stringFromBytes:buffer length:CC_MD5_DIGEST_LENGTH]; } HMAC加密之后 再把密码+201711302229 md5加密 到服务器校验 服务器也是把HMAC密码加上时间(到分) 跟APP传过来的进行匹配 。
22、成员变量 属性
@interface Person : NSObject{ @public NSString *_name;//成员变量 } @property (nonatomic,copy) NSString *age;//属性 @end Person *p = [[Person alloc] init]; p->_name = @"sunny";//实例变量赋值采用箭头 kvo观察不到值得变化 p.age = @"18";//实例变量赋值采用set方法 kvo可以观察到值得变化
23、递归没有return 会栈溢出 调用函数 就会分配栈空间 没有return 会一直分配栈空间 。
24、2、在iOS 11上运行tableView向下偏移64px或者20px,因为iOS 11废弃了automaticallyAdjustsScrollViewInsets,而是给UIScrollView增加了contentInsetAdjustmentBehavior属性。避免这个坑的方法是要判断 if (@available(iOS 11.0, *)) { _tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; }else { self.automaticallyAdjustsScrollViewInsets = NO; }
25、设置导航栏为透明 self.navigationController.navigationBar.translucent = YES; self.navigationController.navigationBar.barTintColor = clear_color; [self.navigationController.navigationBar setBackgroundImage:[UIImage new] forBarMetrics:UIBarMetricsDefault]; [self.navigationController.navigationBar setShadowImage:[UIImage new]];
26、设置导航栏为不透明 self.navigationController.navigationBar.translucent = NO; self.navigationController.navigationBar.barTintColor = RGBCOLOR(64, 98, 153); [self.navigationController.navigationBar setBackgroundImage:nil forBarMetrics:UIBarMetricsDefault]; [self.navigationController.navigationBar setShadowImage:nil];
27、CFBundleURLType:当前APP的scheme记录, 通过schema可在其它App中打开当前App LSApplicationQueriesSchemes是从iOS9时被引入的。 用意:当前APP允许访问的APP有哪些,即白名单,需要通信双方 均设置为对方的scheme,否则当调用对方App时,系统会告诉你This app is not allowed to query for scheme。 调用者和被调用者均需要设置白名单,一方想调用,另一方需要也知道将被你调用 ,更为安全