面试相关

iOS面试了20几家总结出来的面试题(二)

2020-07-21  本文已影响0人  一意孤行的程序猿

收录:原文地址
原作者:执笔续春秋

27. SDWebImage是怎么做缓存的?

二、如何获取图片的? 1、在内存缓存中找 2、如果内存中找不到, 会去默认磁盘目录中寻找, 如果找不到,在去自定义磁盘目录中寻找 3、如果磁盘也找不到就会下载图片 4、获取图片数据之后, 将图片数据从NSData转化UIImage。其中转化根据图片的类型进行转化 5、默认对图片进行解压缩,生成位图图片 6、将位图图片返回

三、图片是如何被解压缩的? 1、判断图片是否是动态图片,如果是,不能解压缩 2、判断图片是否透明,如果是,不能解压缩 3、判断图片的颜色空间是不是RGB如果不是、不能解压缩 4、根据图片的大小创建一个上下文 5、将图片绘制在上下文中 6、从上下文中读取一个不透明的位图图像,该图像就是解压缩后的图像 7、将位图图像返回

接上说 NSCache

//名称
@property (copy) NSString *name;

//NSCacheDelegate代理
@property (nullable, assign) id<NSCacheDelegate> delegate;

//通过key获取value,类似于字典中通过key取value的操作
- (nullable ObjectType)objectForKey:(KeyType)key;

//设置key、value
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost

/*
设置key、value
cost表示obj这个value对象的占用的消耗?可以自行设置每个需要添加进缓存的对象的cost值
这个值与后面的totalCostLimit对应,如果添加进缓存的cost总值大于totalCostLimit就会自动进行删除
感觉在实际开发中直接使用setObject:forKey:方法就可以解决问题了
*/
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;

//根据key删除value对象
- (void)removeObjectForKey:(KeyType)key;

//删除保存的所有的key-value
- (void)removeAllObjects;

/*
当NSCache缓存的对象的总cost值大于这个值则会自动释放一部分对象直到占用小于该值
非严格限制意味着如果保存的对象超出这个大小也不一定会被删除
这个值就是与前面setObject:forKey:cost:方法对应
*/
@property NSUInteger totalCostLimit;    // limits are imprecise/not strict

/*
缓存能够保存的key-value个数的最大数量
当保存的数量大于该值就会被自动释放
非严格限制意味着如果超出了这个数量也不一定会被删除
*/
@property NSUInteger countLimit;    // limits are imprecise/not strict
/*
这个值与NSDiscardableContent协议有关,默认为YES
当一个类实现了该协议,并且这个类的对象不再被使用时意味着可以被释放
*/
@property BOOL evictsObjectsWithDiscardedContent;

@end

//NSCacheDelegate协议
@protocol NSCacheDelegate <NSObject>
@optional
//上述协议只有这一个方法,缓存中的一个对象即将被删除时被回调
- (void)cache:(NSCache *)cache willEvictObject:(id)obj;
@end**

countLimit注意一下这个属性, 这个属性就是设置最大缓存数量,啥意思呢? 这玩意就和栈差不多, 先进先出(叫什么FIFO?)原则。比如你countLimit设置为5 那么当你缓存第6个对象的时候, 原本第一个就被移除了。 所以这便就有有一个风险,也可能会是面试点,为什么,通过key去取值的时候,一定要判断一个获取的对象是否为nil?答:就因为很有可能某些对象被释放(顶)掉了。

又又又可能出现的面试题!NSCache里面缓存的对象,在什么场景下会被释放?

28.SDWebImage实现原理是什么? 它是如何解决tableView的复用时出现图片错乱问题的呢

29. 为什么刷新UI要在主线程操作

30. RunTime

类的结构体:

//Class也表示一个结构体指针的类型
typedef struct objc_class *Class;

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

分类结构体

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods; // 对象方法
    struct method_list_t *classMethods; // 类方法
    struct protocol_list_t *protocols; // 协议
    struct property_list_t *instanceProperties; // 属性
    // Fields below this point are not always present on disk.
    struct property_list_t *_classProperties;

    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }

    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};

引申1. class_copyIvarList与class_copyPropertyList的区别?

引申2. class_ro_t和class_rw_t的区别?

31. NSNotification

// 比如
   dispatch_async(dispatch_get_main_queue(), ^{
       // 刷新UI
   });

32. 什么情况使用 weak 关键字,相比 assign 有什么不同?(轮回系列)

引申 1.关键字copy 的用法?

引申 2.@property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的。

33. 说说内存管理?

杂项

互斥锁:如果共享数据已经有了其他线程加锁了,线程会进行休眠状态等待锁,一旦被访问的资源被解锁,则等待资源的线程会被唤醒。信号量dispatch_semaphore 为互斥锁   @synchronized是NSLock的封装 属于互斥锁  互斥锁一般用于等待时间较长的情况
**适用于**:线程等待锁的时间较长

自旋锁:如果共享数据已经有其他线程加锁了,线程会以死循环的方式等待锁,一旦被访问的资源被解锁,则等待资源的线程会立即执行。OSSpinLock 属于自旋锁   自旋锁一般用于时间较短的情况,OSSpinLock
**适用于**:线程等待锁的时间较端

drawRect调用时机

  • 一般是第一次想要为项目添加pod的时候使用的,当然也可以在添加和移除库使用
  • 每次pod install的时候,pod install 回为每一个安装的pod库在Podfile.lock文件中写入其版本号,并且锁定当前版本号。
  • 如果pod install的时候,不会更新其版本库,而是去下载新的或者移除当前版本 pod update
  • 当执行了pod update的时候,cocoaPods不会考虑Podfile.lock中的版本。直接去更新当前所有的库到最新,然后Podfile.lock会更新这一次的版本号。
  • 有的相对布局最终都会转换成Frame绝对布局 中间多了一层转换的操作

iOS9

从HTTP升级到HTTPS App瘦身 下面有讲 这里不赘述( App瘦身 ) 新增UIStackView

iOS10

新增通知推送相关的操作。自定义通知弹窗,自定义通知类型(地理位置,时间间隔,日历等)

iOS11

无线 调试 齐刘海儿,导航条,安全距离等

iOS12

启动速度优化 应用启动速度提升40% 键盘响应速度提升50% 相机启动速度提升70%

iOS13

黑暗模式 详情请查阅 www.jianshu.com/p/0da3b107f…


二、App包以及启动过程

App瘦身

1、App如何瘦身?

Slicing: 这个过程是iOS9出来之后 不需要程序员干预的一个瘦身的过程,简单来说就是我们再上传IPA包到iTunes Connect,然后AppStore会对app进行切片,切成特定的机型想要的数据,比如@3x给max用,@2x就自动剔除了。 是一个自动的过程Bitcode:是一种中间码,如果配置了Bitcode(Xcode7以后默认开启)的程序会在App Store Connect上被重新编译等一系列操作,进而苹果内部会对可执行文件进行优化,也就是说不需要我们干预什么东西,也操作不了, 如果后面苹果有更牛逼的优化操作,也是苹果的事情, 跟我们个人开发者一毛钱关系没有。 On Demand Resources 按需加载, 是程序员自己手动操作,说白了就是在用的时候去下载某些资源, 但是我们自己在配置的时候都需要配置,要额外写一些代码啥的,等我们提交到市场的时候, 苹果内部会把我们按需加载的资源从包里面做了一些抽离操作啥的, 让我们的包在下载的时候更小,举个例子,就是吃鸡里面沙漠地图如果玩家不自己下载, 就玩不了沙漠。

on-demond resource(ODR)具体请查看原理版本:www.jianshu.com/p/bacedd8a3…
或者详细使用版本:www.cocoachina.com/articles/12…

关于 slicing, bitcode, on-demond resource(ODR)的参考资源https://blog.csdn.net/zhuod/article/details/70051514?utm_source=blogxgwz6


2、app启动时候都经历了什么?

启动分为两种。 一种是之前启动过,按了一下home键,然后再点启动,这个启动叫热启动,另外就是第一次启动app,或者启动杀死之后的app 叫做冷启动

根据info.plist里面的设置加载,建立沙箱,权限检查等 加载可执行文件 加载动态库 objc运行时的初始化处理(类的注册,category注册,selector唯一性检查等等) 初始化,包括+load方法 执行main函数 Application 初始化,到 applicationDidFinishLaunchingWithOptions 执行完 渲染屏幕,到viewDidAppear 执行完毕,展现给用户

根据info.plist里面的设置加载,建立沙箱,权限检查等 加载可执行文件 加载动态库 objc运行时的初始化处理(类的注册,category注册,selector唯一性检查等等) 初始化,包括+load方法


3、优化启动时间

注意:启动时间在小于400ms是最佳的,因为从点击图标到显示Launch Screen,到Launch Screen消失这段时间是400ms。启动时间不可以大于20s,否则会被系统杀掉。

mian函数之前的启动优化


mian函数之后的启动优化 首先分析一下从main函数开始执行,到第一个页面显示, 这段时间做了哪些事情

  1. 执行didFinishLaunchingWithOptions方法
  2. 初始化Window,初始化基础ViewContreoller(一般是UINavigationController+UITabViewController)
  3. 获取数据(本地和远程)
  4. 最后展示给用户
  5. 减少创建线程(高性能iOS开发一书中提到,线程不仅仅有创建时的时间开销,还会消耗内核的内存,即应用的内存空间。 每个线程大约消耗 1KB 的内核内存空间线程创建的耗时(不包含启动时间),其区间范围在 4000~5000 微秒,即 4~5 毫秒。创建线程后启动线程的耗时区间为 5~100 毫秒,平均大约在 29 毫秒。这是很大的时间开销,若在应用启动时开启多个线程,则尤为明显。线程的启动时间之所以如此之长,是因为多次的上下文切换所带来的开销。所以线程在开发过程中也避免滥用)
  6. 合并或者删减不必要的类(或者分类)和函数objc的类越多,函数越多启动越慢
  7. 在设计师可接受的范文尽量使用小的图片
didFinishLaunchingWithOptions
applicationDidBecomeActive

优化的核心思想就是,能延时的延时, 不能延时的尽量放到后台去优化。

- 日志、统计等必须在 APP 一启动就最先配置的事件。仍然把它留在 didFinishLaunchingWithOptions 里启动。
- 项目配置、环境配置、用户信息的初始化 、推送、IM等事件,这些功能在用户进入 APP 主体的之前是必须要加载完的,把他放到广告页面的viewDidAppear启动。
- 其他 SDK 和配置事件,由于启动时间不是必须的,所以我们可以放在第一个界面的 viewDidAppear 方法里,这里完全不会影响到启动时间。
- 每次用NSLog方式打印会隐式的创建一个Calendar,因此需要删减启动时各业务方打的log,或者仅仅针对内测版输出log
- 尽量不要在didFinishLaunchingWithOptions 里面创建和开启多线程

参考文献https://www.jianshu.com/p/f40fdd8799b8 其文章内部作者谈到了美团关于启动优化的相关分析,看似似曾相似,没记错的画《高性能iOS应用开发》这本书就是美团这几个哥们儿翻译的吧,实现方式和书中颇为相似。


3、App电量消耗

定位优化

1.尽量不要实时更新 2.定位精度尽量不要太高

网络优化

1.减少、压缩网络数据 2.能使用缓存就使用缓存,减少网络请求 3.断点续传 4.批量传输 5.设置适合的超时时间,用户可以取消耗时的网络请求 6.网络不可用时就不要再执行网络请求了

CPU/GPU优化

相关离屏渲染操作尽量避免 内存管理处理好 使用懒加载 使用绘制 图片与imageView相同大小避免多余运算 Timer的时间间隔不宜太短,满足需求即可 线程适量,不宜过多,不要阻塞主线程 适当使用多线程 减少视图刷新:确保必要的时候才刷新,能刷新1行cell最好只刷新一行;

为了优化耗电我们还可以做: 1.尽量不要使用定时器 2.优化I/O操作(文件的读写操作) 2.1最好不要频繁读写小数据,最好批量读写 2.2数据量比较大的时候可以考虑使用数据库 2.3读写大量重要数据时,考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API。用dispatch_io系统会优化磁盘访问

高性能iOS应用开发中提到一下几点

  • 数据处理(例如文本格式优化)
  • 待处理的数据大小----更大的显示屏允许软件在单个视图中展示更多的信息,但这也意味着要处理更多的数据
  • 处理数据的算法和数据结构
  • 执行更新的次数,尤其是在数据更新之后,触发应用的状态或者UI进行更新(比如刷新单行cell)
  • 服务器中的数据尽量不要在客户端上处理(例如服务器字符串,在客户端进行拆分操作)
  • 按需加载(例如tableViewcell 不需要一下子全部渲染,快速滑动的时候 过程中的留白处理。)
  • 在进行网络请求之前,先检查是否有网络连接。(没网络的时候,不要请求网络)
  • 避免没有连接WiFi的情况下进行高带宽的消耗操作(因为3G、4G等手机网络耗电量远大于WIFi信号),例如视频流在4G或者非Wifi情况下应该给出响应的提示。
  • 尽量不要实时更新
  • 定位精度尽量不要太高

三、算法

定义相关

  1. 链表和数组的区别是什么? 链表和字典的区别是什么?

数组在内存中是逐个存放的,链表每隔节点没有相对固定的位置关系 数组被声明后,大小就固定了,不能进行动态扩充。 链表可以动态生成节点,并且添加到已有的链表后面 数组存在越界问题,链表则不存在 数组的插入删除的时间复杂度是O(n),链表O(1) 数组的查询下标时间复杂度为O(1), 链表为O(n) 根据值查询的时间复杂度,链表和数组都是O(n)

  1. 如何检测链表中是否有环?

思路 假设有两个学生A和B在跑道上跑步,两人从相同起点出发,假设A的速度为2m/s,B的速度为1m/s,结果会发生什么? 答案很简单,A绕了跑道一圈之后会追上B! 将这个问题延伸到链表中,跑道就是链表,我们可以设置两个指针,a跑的快,b跑的慢,如果链表有环,那么当程序执行到某一状态时,a==b。如果链表没有环,程序会执行到a==NULL,结束。

1、 调换A和B

//    int a = 10;
//    int b = 20;
//
//    a = a + b;
//    b = a - b;
//    a = a - b;
//
//    NSLog(@"a = %d , b = %d", a, b);
//
//
//    a = a*b;
//    b = a/b;
//    a = a/b;
//
//    NSLog(@"1 =====    : a = %d , b = %d", a, b);

2、最大公约数

//    int n = 20,v = 30,temp = 0,max,min;
//
//    if (n>v) {
//        max = n;
//        min = v;
//    } else {
//        max = v;
//        min = n;
//    }
//
//
//    while (min != 0) {
//        temp = max - min;
//        max = min;
//        min = temp;
//    }
//
//    NSLog(@"%d", max);

3、打印2 - 100 的素数(质数) 除了1和自身被整除的.

//    NSMutableArray *primeNumberArray =[NSMutableArray array];
//    for(int i=2; i<=100; i++) {
//
//        NSInteger n = 0;
//
//        for(int j = 1; j <= i; j++) {
//
//            if(i % j == 0) {
//                n = n + 1;
//            }
//        }
//
//        if(n == 2) {
//            [primeNumberArray addObject:@(i)];
//        }
//    }
//
//    NSLog(@"primeNumber = %@",primeNumberArray);
//

4、 字符串倒叙

//    NSString *string = @"hei Son 我是你father";
//    NSMutableString *string1 = [NSMutableString string];
//    for (NSInteger i = string.length; i>0; i--) {
//        [string1 appendString:[string substringWithRange:NSMakeRange(i -1,1)]];
//    }
//
//    NSLog(@"%@", string1);
//

5、 寻找出字符串中有那些中文

//    for (int i = 0; i < string.length; i++) {
//        NSString *str1 = [string substringWithRange:NSMakeRange(i, 1)];
//        const char *cStr = [str1 UTF8String];
//        if (strlen(cStr) == 3 ) {  // oc中 中文三个字节
//            NSLog(@"%@", str1);
//        }
//    }

6. 排序

比较相邻的元素。如果第一个比第二个大,就交换他们两个。 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。 针对所有的元素重复以上的步骤,除了最后一个。 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较

for (int i = 0; i<result.count-1; i++) {
        for (int j = 0; j<result.count-1-i; j++) {
            NSInteger left = [result[j] integerValue];
            NSInteger right = [result[j+1] integerValue];
            if (left>right) {
                [result exchangeObjectAtIndex:j withObjectAtIndex:j+1];
            }
        }
    }
NSLog(@"%@",result);
时间复杂度O(n^2)

选择排序就是通过遍历数组找出每次遍历数组的最小元素的下标,然后将其按顺序从第一位依次排列

     //self.array = @[@2,@4,@3,@1];
     NSMutableArray *mutableArray = [self.array mutableCopy];//oc数组中不能存储基本数据类型,所以快速赋值完成后,系统默认数组元素为NSNumber类型
    if (mutableArray == nil || [mutableArray count] == 0)
    {
        return;
    }
    for (int i = 0; i < [mutableArray count]; i++)
    {
        NSInteger minIdx = i;//默认最小值的索引为i
        for (int j = i+1; j < [mutableArray count]; j++)//通过循环寻找当前数组中最小值的索引值
        {
            if (NSOrderedAscending == [mutableArray[j] compare:mutableArray[minIdx]])//NSNumber类判断大小方法,这句话的意思是当mutableArray[j] <mutableArray[minIdx]时
            {
                minIdx = j;//更新数组中最小值的索引值
            }
        }
        [mutableArray exchangeObjectAtIndex:i withObjectAtIndex:minIdx];//将每次循环结束后找到的最小值交换到数组的第i位
        NSLog(@"%@",mutableArray);
    }
}
时间复杂度为O(n)。最坏情况下仍为O(n^2)
NSMutableArray *priceArray = [NSMutableArray arrayWithObjects:@"0.2",@"5",@"44",@"67",@"98.5",@"1.55", nil];

[priceArray sortUsingComparator:^NSComparisonResult(id _Nonnull obj1, id _Nonnull obj2)  {
     if ([obj1 integerValue] < [obj2 integerValue]){
         return NSOrderedAscending;
     } else {
       return NSOrderedDescending;
     }
}];

这里再次得到的priceArray即为升序排列的数组;
若果想要得到降序的调换一下return的位置即可

7 寻找最近的公共View

// 所有View的父View
+ (NSArray *)superViews:(UIView *)view{
    if (view==nil) {
        return @[];
    }
    NSMutableArray *result = [NSMutableArray array];
    while (view!=nil) {
        [result addObject:view];
        view = view.superview;
    }
    return [result copy];
}

// 相互对比两个节点中的view,出现一样就返回
+ (UIView *)commonView_1:(UIView *)viewA  andView:(UIView *)viewB{
    NSArray *arr1 = [self superViews:viewA];
    NSArray *arr2 = [self superViews:viewB];
    for (NSUInteger i =0; i<arr1.count; ++i) {
        UIView *targetView = arr1[i];
        for (NSUInteger j=0; j<arr2.count; ++j) {
            if (targetView == arr2[j]) {
                return targetView;
            }
        }
    }
    return nil;
}

// 利用NSSet中的hash表,可以将上面代码进行进一步优化
+ (UIView *)commomView_2:(UIView *)viewA andView:(UIView *)viewB{
    NSArray *arr1 = [self superViews:viewA];
    NSArray *arr2 = [self superViews:viewB];
    NSSet *set = [NSSet setWithArray:arr2];
    for (NSUInteger i =0; i<arr1.count; ++i) {
        UIView *targetView = arr1[i];
        if ([set containsObject:targetView]) {
            return targetView;
        }
    }
   return nil;
}

8.数组题:如何在有序数组中找出和等于给定值的两个元素?

NSArray *arr = @[@"1", @"12", @"13", @"23", @"31", @"43", @"52", @"66", @"88", @"111", @"127", @"199"];
[self confirmNumbers:arr total:199];
----

- (void)confirmNumbers:(NSArray *)array total:(NSInteger)totalNmuber  {
    if (array.count <= 1) { return; }

    NSInteger tempAddCount = 0;
    NSInteger tempDeleltCount = 0;

    for (int i = 0; i <= array.count; i++) {
        NSInteger tNamber = [array[tempAddCount] integerValue] + [array[array.count-1-tempDeleltCount] integerValue];
        if (tNamber == totalNmuber) {
            NSLog(@"%ld, %ld,  第一个元素%@ - 后面的元素%@", (long)tempAddCount, (long)tempDeleltCount, array[tempAddCount] ,array[array.count-tempDeleltCount-1]);
            break;
        } else if (tNamber < totalNmuber) {
            tempAddCount ++;
        } else {
            tempDeleltCount ++;
        }
        if (i == array.count - 1) {
            NSLog(@"啥都没匹配着");
            break;
        }
    }

}

9 用递归写一个算法,计算从1到100的和。

NSLog(@"%ld", [self getSumResult:100]);

- (NSInteger)getSumResult:(NSInteger)number {
    if (number <=0 ) {
        return number;
    }

    return number + [self getSumResult:number - 1];
}
// 递归效率差的原因是 每一次调用函数(自己)都是要有内存开销的,影响CUP的效率

10.打乱一个数组

  NSArray* arr = @[@"1",@"2",@"3"];
    arr = [arr sortedArrayUsingComparator:^NSComparisonResult(NSString *str1, NSString *str2) {
        int seed = arc4random_uniform(2);
        if (seed) {
            return [str1 compare:str2];
        } else {
            return [str2 compare:str1];
        }
    }];

三、软技术篇 ###1.开发过程中, 你碰到那些技术难点?是怎么解决的?


 - SD4.X的解决办法
[[SDImageCache sharedImageCache] setShouldDecompressImages:NO];
[[SDWebImageDownloader sharedDownloader] setShouldDecompressImages:NO];

- SD5.0 及以上的解决办法
SDWebImageAvoidDecodeImage添加了这个枚举,意思是在子线程程解压缩图片
[self.imageView sd_setImageWithURL:self.url placeholderImage:[UIImage imageNamed:@"logo"] options:SDWebImageAvoidDecodeImage];

2.过程中, 你用过什么调试工具?

Leaks(泄漏):一般的查看内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录;
- locations(内存分配):跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史; 

结语

这是陆陆续续面试中总结出来的, 总之,有的问题回答的比较捡漏, 有的问题模棱两可,有些面试官的反馈比较扎实,有些面试官说回答的泰国笼统不够细致,反正这东西见仁见智吧。

此文章基本上不会添加新的问题了,如果有错误我会及时改正,避免耽误有需要帮助的小伙伴! 最后一次更新时间:2020年07月21日

推荐👇:

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:789143298 ,不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

上一篇下一篇

猜你喜欢

热点阅读