iOS 技术

3,177 阅读13分钟

1. 请问前后台切换,会发生些什么,系统哪些方法会被调用,viewcontroller哪些方法会被调用

在不考虑 APP 在后台被 kill 的情况: 进入后台:

方法 作用
applicationWillResignActive 点击 Home 键,app开始准备进入后台,这个时候会进入该回调,意味着 app 被挂起,进程即将失去活跃。经过不严谨的测试,大约有 10 分钟左右的时间用来处理事务。
applicationDidEnterBackground 当 applicationWillResignActive回调方法完全执行完毕后,会进入 applicationDidEnterBackground 。

进入前台:

方法 作用
applicationWillEnterForeground 在 app 未被杀死的情况下,点击 icon再次进入 app,重新回到前台之前会先进入 applicationWillEnterForeground 回调
applicationDidBecomeActive applicationWillEnterForeground执行完毕后,会进入 applicationDidBecomeActive 回调,正式回归活跃。

前后台切换,主要的坑点在于:VC中并没函数会调用,尤其注意:VC 相关的 Appear 和 Disappear 函数并不会被调用。想在VC中监听切换,只能监听通知,每个在appdelegate的生命代理方法都有对应的通知。

如果考虑 APP 在后台被 kill 的情况:

进入后台后,如果没有后台运行权限及功能,可能在一段时间后被系统 kill 掉,再次进入app后,会重新进入启动流程。

方法 作用
main() 函数: 这个阶段一般是 可执行 .o 文件,动态库加载,objc类注册,category 类注册,selector 唯一性检查,+(void)load 方法,C++ 静态全局变量的创建等。
didFinishLaunchingWithOptions 用户点击 icon 启动 app,或者被 kill 后以任何方式进入 app,在 main() 执行后,会进入didFinishLaunchingWithOptions回调,处理首屏渲染,以及其他业务相关的事件,例如监听事件,配置文件读写或者 SDK 初始化等等。
applicationDidBecomeActive 在didFinishLaunchingWithOptions方法作用域结束后,会进入 applicationDidBecomeActive 回调,也正式意味着 app 已经处于活跃状态。
rootViewController 的相关的 Appear 函数 注意:此时rootViewController 的相关的 Appear 函数会被调用。

参考链接:WWDC 2016 - Session 406-Optimizing App Startup Time

2. 请问对无序的Array排序,有什么好的方法,代码越少,API越高级越好。有无原生方法可以办到。

苹果为我们提供了很多 Array 的排序方法,但原理上可以看到就是 Comparator (比较器) 和 Descriptor (描述器) 两种,像是 Selector 和 Function ,最终也是使用 Comparator 在做排序,只是响应方法不同。 其中 Swift 也有方法: array.sort(),见参考链接:Apple Documentation-Swift-Array-sorted 先说说 Comparator ,如果数组中元素是 String 或 Number,首选 Comparator,可以将 compare: 方法的返回值直接作为 NSComparisonResult 返回值。实际的排序代码三行就可以搞定。当然用 Selector 和 Function,也是一样的效果,但需要写更多的代码。 例子一:

NSArray *sortedArray = [array sortedArrayUsingComparator:^NSComparisonResult(NSString *obj1, NSString *obj2) {
               if ([obj1 compare:obj2] == NSOrderedAscending) {
                   return NSOrderedAscending;
               } else if ([obj1 compare:obj2] == NSOrderedDescending){
                   return NSOrderedDescending;
               }else {
                   return NSOrderedSame;
               }
           }];

例子二:

- (void)arraySortUsingCompare {
   // 比较器 排序
   
   NSMutableArray *arr = [NSMutableArray array];
   for (int i = 0; i < 10; i ++) {
       int n = arc4random() % (10 - 0) + 1;
       [arr addObject:@(n)];
   }
   NSLog(@"排序前 ===== %@", arr);
   
   [arr sortUsingComparator:^NSComparisonResult(NSNumber *num1, NSNumber *num2) {
       //        return [num1 compare:num2];  // 正序
       return [num2 compare:num1]; // 倒序
       
   }];
   
   NSLog(@"排序后 %@", arr);
   
   
   arr = [NSMutableArray array];
   [arr addObject:@"Kobe Bryant"];
   [arr addObject:@"LeBorn James"];
   [arr addObject:@"Steve Nash"];
   [arr addObject:@"Stephen Curry"];
   [arr addObject:@"Monkey D Luffy"];
   [arr addObject:@"Roronoa Zoro"];
   
   NSLog(@"排序前 ==== %@", arr);
   
   [arr sortUsingComparator:^NSComparisonResult(NSString *str1, NSString *str2) {
       //        return [str1 compare:str2];  // 正序
       return [str2 compare:str1]; // 倒序
   }];
   
   NSLog(@"排序后 %@", arr);
}

参考链接:Objective-C中的排序及Compare陷阱

但是如果需要针对一个对象的几个属性作为不同的维度去做排序,那选择 Descriptor,因为不需要根据利用属性对排序优先级写一大堆的逻辑判断。主要将所有参与比较的属性都放入描述器中即可,如果想对球员的年龄和号码(优先级分先后)进行排序,只需要依次加入描述器组,三行代码就可以完成。

- (void)arraySortUsingDescriptor {
   NSMutableArray *arr = [NSMutableArray array];
   
   Person *person = [[Person alloc] init];
   person.name = @"Ingram";
   person.age = 21;
   person.number = 14;
   
   [arr addObject:person];
   
   person = [[Person alloc] init];
   person.name = @"Ball";
   person.age = 21;
   person.number = 2;
   
   [arr addObject:person];
   
   person = [[Person alloc] init];
   person.name = @"Zubac";
   person.age = 21;
   person.number = 15;
   
   [arr addObject:person];
   
   NSSortDescriptor *ageDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES];
   NSSortDescriptor *numberDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"number" ascending:YES];
   [arr sortUsingDescriptors:@[numberDescriptor, ageDescriptor]];
   
   for (Person *person in arr) {
       NSLog(@"\n 球员姓名: %@ \n 球员号码: %d \n 球员年龄: %d \n -------- \n", person.name, person.number, person.age);
       
   }
}

3. 请问APNs推送如何区分设备,如何将设备的信息传给Apple,你上传的时机时怎样的,猜想这个设备信息是如何生成的

设备信息传递给apple

post请求; Use HTTP/2 and TLS 1.2 or later to establish a connection between your provider server and one of the following servers:

  • Development server: api.sandbox.push.apple.com:443
  • Production server: api.push.apple.com:443

也就是: 设备信息是通过一个POST请求将DeveiceToken和其他信息发送给APNS,需要用 HTTP/2 和 TLS 1.2或以上的版本,在自己提供的服务和以上服务之间建立连接。

开发环境:api.sandbox.push.apple.com:443

生产环境:api.push.apple.com:443

当然,还可以用一台机器的 2197 端口让 APNS 通过防火墙

请求示例:

HEADERS
 - END_STREAM
 + END_HEADERS
 :method = POST
 :scheme = https
 :path = /3/device/00fc13adff785122b4ad28809a3420982341241421348097878e577c991de8f0
 host = api.sandbox.push.apple.com
 authorization = bearer eyAia2lkIjogIjhZTDNHM1JSWDciIH0.eyAiaXNzIjogIkM4Nk5WOUpYM0QiLCAiaWF0I
    jogIjE0NTkxNDM1ODA2NTAiIH0.MEYCIQDzqyahmH1rz1s-LFNkylXEa2lZ_aOCX4daxxTZkVEGzwIhALvkClnx5m5eAT6
    Lxw7LZtEQcH6JENhJTMArwLf3sXwi
 apns-id = eabeae54-14a8-11e5-b60b-1697f925ec7b
 apns-expiration = 0
 apns-priority = 10
 apns-topic = com.example.MyApp
DATA
 + END_STREAM
 { "aps" : { "alert" : "Hello" } }

上传时机

didRegisterForRemoteNotificationsWithDeviceToken方法,回调内处理设备信息上传的业务。但有些情况是,我们希望根据用户账号来做推送,例如即时通讯应用。那么我们就要在登录或自动登录后,上传deviceToken,和用户信息绑定并处理替换逻辑,避免推送错乱。

设备信息

This address takes the form of a device token unique to both the device and your app.

猜测:UDID+bundleId+生产/开发环境+时间戳。

其中注意带时间戳hash是为什么频繁上传device token的主要原因。长期不活跃app,比如用户一个月或者两个月没打开过该app,该服务器后端就再也推不到了。

3. 谨慎iOS黑魔法 - Method Swizzling

优点:

区别于⼿动为每⼀个类编写埋点⽅法或者写⼀个基类来做统⼀的埋点,前两者在某些场景下⼯ 作量都不算⼩。可以做⼀个UIViewController的Category,置换原⽣⽅法,在置换⽅法中将写⼊埋点代码,这样可以直接⼀键埋点完成。之后新增的UIViewController类也不需要再关⼼这些的埋点代码。

- (void)cyl_APOViewDidLoad {
Class class = [self class];
if (!([class isEqual:[UIViewController class]] || [class isEqual: [UINavigationController class]])) {
NSLog(@"统计该⻚⾯ %@", class);
}
}

置换 NSDictionary-setObject:forKey: 方法,用于防止 crashNSArray 同理。

- (void)cyl_safeSetObject:(id)object forKey:(id<NSCopying>)key {
if (object && key) {
[self safe_setObject:object forKey:key];
}
}

缺点:

总结:一时hook一时爽,debug火葬场。

原因:

以下为What are the Dangers of Method Swizzling in Objective-C? 中列举出的7个问题:

  • Method swizzling is not atomic
  • Changes behavior of un-owned code
  • Possible naming conflicts
  • Swizzling changes the method's arguments
  • The order of swizzles matters
  • Difficult to understand (looks recursive)
  • Difficult to debug

可见,其没有类似注解的东⻄,⽅法置换没有有效声明。如果滥⽤,反⽽会增加维护成本。若擅⾃使⽤未同步其他同学,会成为极⼤的项⽬隐患。尤其是⼀些封装的模块。

这里着重说明几个场景:

场景:(iTeaTime(技术清谈)@国家一级保护废物 提供答案)

如果多次hook了同一个类的同一个方法, 跟分类重名的表现是一样:表现为无法控制执行的先后顺序,与编译器build的顺序有关,但编译器顺序有不可控性。

比如下面的实现方法,可能出现方法覆盖的问题:

+ (void)load {
static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewDidLoad); SEL swizzledSelector = @selector(XK_ViewDidLoad);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

场景:(iTeaTime(技术清谈)@molon 提供) Hook了具有继承关系的相同方法。

以下场景:

如果子类并没有重写父类的方法,拿父类的implement去swizzling本来就是错误的行为。

A<—继承---B<—继承---C (B是A的子类,C又是B的子类)

A 里有 test 方法,但是 B 和 C 都没有重写。 通常如果要对 B 或者 C 的 test 进行hook的话,很多开发者都喜欢去给 B 或者 C add A.test 的 implemention。 那如果先hook的C,又hook的B,似乎就形成了C与A直接打交道的局面。但是以面向对象来说,C的原实现应该是B的当前实现才合理。 所以不应该hook当前类没有重写的方法,这种其实直接继承(或者加category方法)就可以做了,不需要hook,需要调用原实现直接[super test]即可。

4. iPhone在无耳机状态下,通过实体按键设置静音后,以下路径比如: 微信主tab-朋友圈-点开feed流中的小视频,可以播放声音。通过点击头像-个人朋友圈主页,点开视频无法播放声音。即使按声音增加键也无法播放。请问这个表现不一致的现象,是feature还是bug,如果是bug你觉得是代码哪里写的有问题。写出修复代码

视频播放器默认静音模式下是没有声音的,但可以控制即使是静音模式下依然有声音,显然前者设置了,后者没有设置。推测前者是被提交了bug所以fix掉了,后者使用场景比较少,所以没有被注意到。

//忽略静音按钮
   AVAudioSession *session =[AVAudioSession sharedInstance];
   [session setCategory:AVAudioSessionCategoryPlayback error:nil];

完整代码:

- (AVAudioPlayer *)player {
if (!_player) {
NSURL *URL = [[NSBundle mainBundle] URLForResource:@"xxxx.wav"
withExtension:nil];
_player = [[AVAudioPlayer alloc] initWithContentsOfURL:URL error:nil];
AVAudioSession *autioSession = [AVAudioSession sharedInstance];
[autioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
[autioSession setActive:YES error:nil];
[_player prepareToPlay];
}

耳机场景下,统一做了处理,都可以播放视频带声音。 比如以下代码用于判断耳机状态,因为AVAudioSession是单例,对耳机优先处理即可。

- (BOOL)isHeadsetPluggedIn {  
   AVAudioSessionRouteDescription* route = [[AVAudioSession sharedInstance] currentRoute];  
   for (AVAudioSessionPortDescription* desc in [route outputs]) {  
       if ([[desc portType] isEqualToString:AVAudioSessionPortHeadphones])  
           return YES;  
   }  
   return NO;  
} 

5. 【iOS-autolayout】一个ScrollView上有3个UILabel,每个label字数不固定,类似字数很多的那种,要求上下依次排列,当文字超出ScrollView的时候可以滑动,左右不能滚动,上下可滚动。【难度🌟🌟🌟】【出题人群内大佬:@起点】

出题人提示

就是label的宽度设置跟scrollView等宽,最底下的label底部要跟scrollView的底部约束上就可以了。 考察的主要是scrollView的约束问题。scrollView的约束主要是从内部撑开宽度跟高度。

答案

三个label 那个,就是放了个scrollview然后里面放三个label,从上往下边距全部约束为0,然后label宽度与scrollview相同,最下面那个label距离底部scrollview为0。(在内部无需多放view)

  1. 在 Scrollview 添加⼀个 ContainView
  2. ContentView 完全覆盖 Scrollview
  3. ContainView 上添加了三个 Label。View 的 bottom 和 第三个 Label 的 bottom 做约束
  4. 三个 Label 互相做间距和宽的约束,不约束⾼

5. 通过将 Scrollview 的 ContentSize 和 ContainView 的 size 保持⼀致。

- (void)viewDidAppear:(BOOL)animated {
       [super viewDidAppear:animated];
       [self.contentView layoutIfNeeded];
        self.scrollview.contentSize =
       CGSizeMake(CGRectGetWidth(self.contentView.frame), CGRectGetHeight(self.contentView.frame));
}

DEMO

6. 如何用一行代码,互换两个变量的值,且不产生第三个变量。

  • 利用Swift元组特性:

可以在定义的同时就取出元祖中的值

// 相当于同时定义了三个变量

let (name, age, score) = (“a”, 30, 99.9)

根据这一特性,我们可以这样互换值: (a, b) = (b, a)

  • 异或或者加减

(a = a ^ b) && (b = a ^ b) && (a = a ^ b)

或者这样

a = a ^ b;b = a ^ b;a = a ^ b;

(a = a + b) && (b = a - b) && (a = a - b)

(a = a x b) && (b = a / b) && (a = a / b)

7. 如何给view同时加上圆角和阴影?至少给出两种实现方法,使用到的API越高级越好。

【提示】两种方法,答案提示:UIBezierPath,和iOS11 layer有个新的方法 【答案】iOS11的layer是maskedCorners,CACornerMask。

参考链接:ios 圆角 cornerRadius 对性能的影响究竟多大? 你测试过吗?

8. 猜想dequeueReusableCellWithIdentifier的实现是怎样的,给出示例代码。注意边界条件:相邻cell的identifier相等时。你的实现中该函数的时间复杂度是多少。为什么?【难度🌟🌟🌟🌟】【出题人 微博@iOS程序犭袁】

cell复用机制的实现猜想,见GitHub-Chameleon:

- (UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier
{
   for (UITableViewCell *cell in _reusableCells) {
       if ([cell.reuseIdentifier isEqualToString:identifier]) {
           UITableViewCell *strongCell = cell;
           
           // the above strongCell reference seems totally unnecessary, but without it ARC apparently
           // ends up releasing the cell when it's removed on this line even though we're referencing it
           // later in this method by way of the cell variable. I do not like this.
           [_reusableCells removeObject:cell];

           [strongCell prepareForReuse];
           return strongCell;
       }
   }
   
   return nil;
}

时间复杂度为: O(n)

注意:

NSArray / NSMutableArray

containsObject:containsObject:``,indexOfObject*removeObject:会遍历里面元素查看是否与之匹对,所以复杂度等于或大于 O(n)。

这里 _reusableCells 使用的是NSMutableSet,而 NSSet / NSMutableSet / NSCountedSet

这些集合类型是无序没有重复元素。这样就可以通过 hash table 进行快速的操作。比如 addObject:, removeObject:, containsObject: 都是按照 O(1) 来的。需要注意的是将数组转成 Set 时会将重复元素合成一个,同时失去排序。

加之 for 循环,可以得到复杂度计算结果。

参考:深入剖析 iOS 性能优化

7. 【在IM开发中】app 接收到一个message,上层UI刷新一次,要求考虑到CPU和电量消耗,解决短时间内接收到很多条消息的问题。怎么解决?有几种方案?【出题人:远之²³³³-free zone-北】【 难度🌟🌟】

方案一:利用联结(在异步线程上调用dispatch_source_merge_data后,就会执行 dispatch source事先定义好的handler)、DISPATCH_SOURCE_TYPE_DATA_ADD,将刷新UI的工作拼接起来,短时间内做尽量少次数的刷新。

方案二:自己实现队列、确定一个合适的时间阈值,在阈值时间到达时、主动取消息或者被动接受消息,最后刷新UI,达到消息限流的作用。举例:假设我们消息的获取都是通过长连接推送过来的,而不是主动拉取的。可以用消息队列来做,消费者定期去队列取数据进行数据展示。或者假设前一条消息和后一条消息间隔只在0.2s以内,就可以认为是频繁收到消息。然后把这0.2s内的消息刷新相关操作,比如做个动画效果。

8. 如图label1在containerView上,containerView、label2在cell.contentView上问题:label1与label2的字数不固定,需求是,无论label2字数多少,label1都不能被拉伸或者压缩:【 难度🌟🌟🌟】【出题人:记忆、搁浅】

效果图见:

【答案】需要给label1设置一下优先级,设置平行的的Content compression resistance priority。

系统 Autolayout 参考 :Apple-Documentation-UIView-setContentHuggingPriority(_:for:)

Masonry 参考以下属性:

static const MASLayoutPriority MASLayoutPriorityRequired = UILayoutPriorityRequired;
   static const MASLayoutPriority MASLayoutPriorityDefaultHigh = UILayoutPriorityDefaultHigh;

9.【计算机常识】如果你一直在用GitLab开发,现在公司要切换到GitHub开发,可以两个邮箱不一样,你自己的提交记录,GitHub无法识别,签到数据也没了,请问如何让GitHub能够识别你整个仓库中所有的提交记录。【难度🌟🌟】【出题人 微博@iOS程序犭袁】

【注】“签到数据”指的是下图:

【答案】参考:

Git 实战手册(一): 批量修改log中的提交信息

使用git迁移git项目并保留提交记录

文章来源

iTeaTime(技术清谈)