Skip to content

Latest commit

 

History

History
286 lines (218 loc) · 16.9 KB

19新浪公司iOS面试题2019年6月.md

File metadata and controls

286 lines (218 loc) · 16.9 KB

新浪公司iOS面试题2019年6月

作者:路飞_Luck&&jianshu.com/p/f9ec208b00b6

对象引用计数放哪里?

参考内容
  • 可查阅objc相关源码:struct objc_class -> isa

  • isa 结构如下

     /** isa_t 结构体 */
     union isa_t {
         Class cls;
         uintptr_t bits;
         struct {
             uintptr_t nonpointer        : 1;
             uintptr_t has_assoc         : 1;
             uintptr_t has_cxx_dtor      : 1;
             uintptr_t shiftcls          : 33;
             uintptr_t magic             : 6;
             uintptr_t weakly_referenced : 1;
             uintptr_t deallocating      : 1;
             uintptr_t has_sidetable_rc  : 1;
             uintptr_t extra_rc          : 19;
         };
     };
    
  • extra_rc:表示该对象的引用计数值,实际上是引用计数值减 1,例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10,则需要使用到下面的 has_sidetable_rc。

  • has_sidetable_rc:当对象引用计数大于 10 时,则has_sidetable_rc 的值为 1,那么引用计数会存储在一个叫 SideTable 的类的属性中,这是一个散列表。

  • 对象引用计数就存放在extra_rc中

MVVM和MVC的区别

参考内容较长,可直接看总结

参考内容
  • MVC:V(<Update&&Action>)C (Update> &&<Notify) M

  • MVC的弊端

    • 厚重的View Controller

      • M:模型model的对象通常非常的简单。根据Apple的文档,model应包括数据和操作数据的业务逻辑。而在实践中,model层往往非常薄,不管怎样,model层的业务逻辑不应被拖入到controller。
      • V:视图view通常是UIKit控件(component,这里根据习惯译为控件)或者编码定义的UIKit控件的集合。View的如何构建(PS:IB或者手写界面)何必让Controller知晓,同时View不应该直接引用model(PS:现实中,你懂的!),并且仅仅通过IBAction事件引用controller。业务逻辑很明显不归入view,视图本身没有任何业务。
      • C:控制器controller。Controller是app的“胶水代码”:协调模型和视图之间的所有交互。控制器负责管理他们所拥有的视图的视图层次结构,还要响应视图的loading、appearing、disappearing等等,同时往往也会充满我们不愿暴露的model的模型逻辑以及不愿暴露给视图的业务逻辑。网络数据的请求及后续处理,本地数据库操作,以及一些带有工具性质辅助方法都加大了Massive View Controller的产生。
      • 遗失(无处安放)的网络逻辑 苹果使用的MVC的定义是这么说的:所有的对象都可以被归类为一个model,一个view,或是一个controller。
    • 你可能试着把它放在Model对象里,但是也会很棘手,因为网络调用应该使用异步,这样如果一个网络请求比持有它的model生命周期更长,事情将变的复杂。显然View里面做网络请求那就更格格不入了,因此只剩下Controller了。若这样,这又加剧了Massive View Controller的问题。若不这样,何处才是网络逻辑的家呢?

    • 较差的可测试性 由于View Controller混合了视图处理逻辑和业务逻辑,分离这些成分的单元测试成了一个艰巨的任务。

  • MVVM

一种可以很好地解决Massive View Controller问题的办法就是将 Controller 中的展示逻辑抽取出来,放置到一个专门的地方,而这个地方就是 viewModel 。MVVM衍生于MVC,是对 MVC 的一种演进,它促进了 UI 代码与业务逻辑的分离。它正式规范了视图和控制器紧耦合的性质,并引入新的组件。他们之间的结构关系如下:

  • MVVM 基本概念

    • 在MVVM 中,view 和 view controller正式联系在一起,我们把它们视为一个组件 view 和 view controller 都不能直接引用model,而是引用视图模型(viewModel) viewModel 是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他代码的地方 使用MVVM会轻微的增加代码量,但总体上减少了代码的复杂性
  • MVVM 的注意事项

    • view 引用viewModel ,但反过来不行(即不要在viewModel中引入#import UIKit.h,任何视图本身的引用都不应该放在viewModel中)(PS:基本要求,必须满足)
    • viewModel 引用model,但反过来不行* MVVM 的使用建议
    • MVVM 可以兼容你当下使用的MVC架构。
    • MVVM 增加你的应用的可测试性。
    • MVVM 配合一个绑定机制效果最好(PS:ReactiveCocoa你值得拥有)。
    • viewController 尽量不涉及业务逻辑,让 viewModel 去做这些事情。
    • viewController 只是一个中间人,接收 view 的事件、调用 viewModel 的方法、响应 viewModel 的变化。
    • viewModel 绝对不能包含视图 view(UIKit.h),不然就跟 view 产生了耦合,不方便复用和测试。
    • viewModel之间可以有依赖。
    • viewModel避免过于臃肿,否则重蹈Controller的覆辙,变得难以维护。
  • MVVM 的优势

    • 低耦合:View 可以独立于Model变化和修改,一个 viewModel 可以绑定到不同的 View 上
    • 可重用性:可以把一些视图逻辑放在一个 viewModel里面,让很多 view 重用这段视图逻辑
    • 独立开发:开发人员可以专注于业务逻辑和数据的开发 viewModel,设计人员可以专注于页面设计
    • 可测试:通常界面是比较难于测试的,而 MVVM 模式可以针对 viewModel来进行测试
  • MVVM 的弊端

    • 数据绑定使得Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
    • 对于过大的项目,数据绑定和数据转化需要花费更多的内存(成本)。主要成本在于:
    • 数组内容的转化成本较高:数组里面每项都要转化成Item对象,如果Item对象中还有类似数组,就很头疼。
    • 转化之后的数据在大部分情况是不能直接被展示的,为了能够被展示,还需要第二次转化。
    • 只有在API返回的数据高度标准化时,这些对象原型(Item)的可复用程度才高,否则容易出现类型爆炸,提高维护成本。
    • 调试时通过对象原型查看数据内容不如直接通过NSDictionary/NSArray直观。
    • 同一API的数据被不同View展示时,难以控制数据转化的代码,它们有可能会散落在任何需要的地方。
  • 总结

    • MVC的设计模式也并非是病入膏肓,无药可救的架构,最起码目前MVC设计模式仍旧是iOS开发的主流框架,存在即合理。针对文章所述的弊端,我们依旧有许多可行的方法去避免和解决,从而打造一个轻量级的ViewController。

    • MVVM是MVC的升级版,完全兼容当前的MVC架构,MVVM虽然促进了UI 代码与业务逻辑的分离,一定程度上减轻了ViewController的臃肿度,但是View和ViewModel之间的数据绑定使得 MVVM变得复杂和难用了,如果我们不能更好的驾驭两者之间的数据绑定,同样会造成Controller 代码过于复杂,代码逻辑不易维护的问题。

    • 一个轻量级的ViewController是基于MVC和MVVM模式进行代码职责的分离而打造的。MVC和MVVM有优点也有缺点,但缺点在他们所带来的好处面前时不值一提的。他们的低耦合性,封装性,可测试性,可维护性和多人协作便利大大提高了开法效率。

    • 同时,我们需要保持的是一个拥抱变化的心,以及理性分析的态度。在新技术的面前,不盲从,也不守旧,一切的决策都应该建立在认真分析的基础上,这样才能应对技术的变化。

UIButton防止多次点击

参考内容 - userInteractionEnabled - 通过UIButton的enabled属性和userInteractionEnabled属性控制按钮是否可点击。 - 此方案在逻辑上比较清晰、易懂,**但具体代码书写分散**,常常涉及多个地方。
  • cancelPreviousPerformRequestsWithTarget:selector:object

    • 总结:会出现延时现象,并且需要对大量的UIButton做处理,工作量大,不方便。
     /** 方法一 */
     - (void)tapBtn:(UIButton *)btn {
         NSLog(@"按钮点击了...");
         // 此方法会在连续点击按钮时取消之前的点击事件,从而只执行最后一次点击事件
         [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(buttonClickedAction:) object:btn];
         // 多长时间后做某件事情
         [self performSelector:@selector(buttonClickedAction:) withObject:btn afterDelay:2.0];
     }
     
     - (void)buttonClickedAction:(UIButton *)btn {
         NSLog(@"真正开始执行业务 - 比如网络请求...");
     }
     
    
  • 通过Runtime交换UIButton的响应事件方法,从而控制响应事件的时间间隔。

    • 创建一个UIButton的分类,使用runtime增加public属性cs_eventInterval和private属性cs_eventInvalid。
    • 在+load方法中使用runtime将UIButton的-sendAction:to:forEvent:方法与自定义的cs_sendAction:to:forEvent:方法进行交换
    • 使用cs_eventInterval作为控制cs_eventInvalid的计时因子,用cs_eventInvalid控制UIButton的event事件是否有效。
    
     	@interface UIButton (Extension)
     	
     	/** 时间间隔 */
     	@property(nonatomic, assign)NSTimeInterval cs_eventInterval;
     	
     	@end
     	
     	static char *const kEventIntervalKey = "kEventIntervalKey"; // 时间间隔
     	static char *const kEventInvalidKey = "kEventInvalidKey";   // 是否失效
     	
     	@interface UIButton()
     	
     	/** 是否失效 - 即不可以点击 */
     	@property(nonatomic, assign)BOOL cs_eventInvalid;
     	
     	@end
     	
     	@implementation UIButton (Extension)
     	
     	+ (void)load {
     	    // 交换方法
     	    Method clickMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
     	    Method cs_clickMethod = class_getInstanceMethod(self, @selector(cs_sendAction:to:forEvent:));
     	    method_exchangeImplementations(clickMethod, cs_clickMethod);
     	}
     	
     	
     	
     	- (void)cs_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
     	    if (!self.cs_eventInvalid) {
     	        self.cs_eventInvalid = YES;
     	        [self cs_sendAction:action to:target forEvent:event];
     	        [self performSelector:@selector(setCs_eventInvalid:) withObject:@(NO) afterDelay:self.cs_eventInterval];
     	    }
     	}
     	
     	
     	
     	- (NSTimeInterval)cs_eventInterval {
     	    return [objc_getAssociatedObject(self, kEventIntervalKey) doubleValue];
     	}
     	
     	- (void)setCs_eventInterval:(NSTimeInterval)cs_eventInterval {
     	    objc_setAssociatedObject(self, kEventIntervalKey, @(cs_eventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
     	}
     	
     	- (BOOL)cs_eventInvalid {
     	    return [objc_getAssociatedObject(self, kEventInvalidKey) boolValue];
     	}
     	
     	- (void)setCs_eventInvalid:(BOOL)cs_eventInvalid {
     	    objc_setAssociatedObject(self, kEventInvalidKey, @(cs_eventInvalid), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
     	}
    
    

弱网相关

参考内容 - 合适的超时时间,针对不同网络设定不同的超时时间,加快超时,尽快重试 - 按子模块多请求去拉取数据,避免一次性加载,导致数据太多请求返回慢; - 缓存和增量请求 - 优化DNS查询:应尽量减少DNS查询,做DNS缓存,避免域名劫持、DNS污染,同时把用户调度到“最优接入点”。 - 减小数据包大小和优化包量:通过压缩、精简包头、消息合并等方式,来减小数据包大小和包量。 - 优化ACK包:平衡冗余包和ACK包个数,达到降低延时,提高吞吐量的目的。(这些难度有点高) - 断线重连,因为我们是 socket 通信的,所以需要做断线重连,重连时间可以递增 - 减少数据连接的创建次数,由于创建连接是一个非常昂贵的操作,所以应尽量减少数据连接的创建次数,且在 - 一次请求中应尽量以批量的方式执行任务。如果多次发送小数据包,应该尽量保证在2秒以内发送出去。在短时间内访问不同服务器时,尽可能地复用无线连接。 - 用户 UI 体验优化,加载一些动画什么的分散下注意力 - 根据不同网络情况动态调节方案,比如图片下载和视频流就可以按照 3G 4G 和 WIFI 进行区分返回 - HTTP3.0

卡顿检测

  • 通过监测Runloop的kCFRunLoopAfterWaiting,用一个子线程去检查,一次循环是否时间太长。
  • 如果超出预定时间,则可以dump堆栈,确定卡顿函数

NSCache,NSDictionary,NSArray的区别

参考内容 - NSArray - 在数组的开头和结尾插入/删除元素通常是一个O(1)操作,而随机的插入/删除通常是 O(N)的。 - NSDictionary - NSDictionary中的键是被拷贝的并且需要是恒定的。如果在一个键在被用于在字典中放入一个值后被改变,那么这个值可能就会变得无法获取了。一个有趣的细节,在NSDictionary中键是被拷贝的,而在使用一个toll-free桥接的CFDictionary时却只被retain。CoreFoundation类没有通用对象的拷贝方法,因此这时拷贝是不可能的(*)。这只适用于使用CFDictionarySetValue()的时候。如果通过setObject:forKey使用toll-free桥接的CFDictionary,苹果增加了额外处理逻辑来使键被拷贝。反过来这个结论则不成立 — 转换为CFDictionary的NSDictionary对象,对其使用CFDictionarySetValue()方法会调用回setObject:forKey并拷贝键。 - NSCache - 当系统资源将要耗尽时,NSCache可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统通知时手动删减缓存,NSCache会先行删减 时间最久为被使用的对象 - NSCache 并不会拷贝键,而是会保留它。此行为用NSDictionary也可以实现,但是需要编写比较复杂的代码。NSCache对象不拷贝键的原因在于,很多时候键都是不支持拷贝操作的对象来充当的。因此NSCache对象不会自动拷贝键,所以在键不支持拷贝操作的情况下,该类比字典用起来更方便 - NScache是线程安全的,NSDictionary不是。在开发者自己不编写加锁代码的前提下,多个线程可以同时访问NSCache。对缓存来说,线程安全通常是很重要的,因为开发者可能在某个线程中读取数据,此时如果发现缓存里找不着指定的键,那么就要下载该键对应的数据了

SDWebImage里面用了哪种缓存策略?

  • 使用了NSCache做缓存策略

self + weakSelf + strongSelf ?

参考内容 - 在 Block 内如果需要访问 self 的方法、变量,建议使用 weakSelf。 - 如果在 Block 内需要多次 访问 self,则需要使用 strongSelf。
1.使用__weak __typeof是在编译的时候,另外创建一个局部变量weak对象来操作self,引用计数不变。
block 会将这个局部变量捕获为自己的属性,
访问这个属性,从而达到访问 self 的效果,因为他们的内存地址都是一样的。

2.因为weakSelf和self是两个变量,doSomething有可能就直接对self自身引用计数减到0了.
  所以在[weakSelf doSomething]的时候,你很难控制这里self是否就会被释放了.weakSelf只能看着.

3.__strong __typeof在编译的时候,实际是对weakSelf的强引用.
  指针连带关系self的引用计数会增加.但是你这个是在block里面,生命周期也只在当前block的作用域.
  所以,当这个block结束, strongSelf随之也就被释放了.不会影响block外部的self的生命周期.

__weak __typeof(self)weakSelf = self;    //1

[self.context performBlock:^{      
    [weakSelf doSomething];          //2
     __strong __typeof(weakSelf)strongSelf = weakSelf;  //3
    [strongSelf doAnotherSomething];        
}];

链接

赞赏一下旺仔(收集整理不易,且赞且珍惜)