开发小知识(三)

2020-05-13  本文已影响0人  ZhengYaWei

开发小知识(一)

开发小知识(二)

仅做笔记使用。。。。。。。

目录

一、UIButton 继承链

UIButton > UIControl > UIView > UIResponder > NSObject

如果 UIButton 上的自带的 UIImageView 和 UILable 无法满足界面要求,且还想添加类似 UIButton 的点击事件,此时封装的视图组件可继承于 UIControl。初始化组件时可添加点击事件:

[self addTarget:self action:@selector(clickedAction:) forControlEvents:UIControlEventTouchUpInside];
补:
hidden = YES;
alphe = 0.0 ~ 0.01;
userInteractionEnabled = NO;

二、UIStackView

在iOS9中苹果在UIKit框架中引入了一个新的视图类 UIStackView。UIStackView 类提供了一个高效的接口用于平铺一行或一列的视图组合。Stack视图管理着所有在它的 arrangedSubviews 属性中的视图的布局。这些视图根据它们在 arrangedSubviews 数组中的顺序沿着 Stack 视图的轴向排列。虽然 UIStackView 是继承与 UIView,但是却没有继承 UIView 的渲染功能,UIStackView 是没有 UI的,也就是不显示本身。类似backgroundColor 的界面属性无效,重写layerClass, drawRect:甚至drawLayer:inContext:都是无效的。UIStackView 是一个纯粹的容器 View。参考文章:

三、中文 log 乱码问题

在网络请求的返回数据中,默认的 NSLog 打印的字典中汉字是因 UTF8 编码显示的"乱码",对调试不是很友好。此时可以在分类中重写 descriptionWithLocale 方法,数组也是如此。

@implementation NSDictionary (Log)
/**
 分类中重写方法解决字典输出中文乱码的问题
 
 @return 输出结果
 */
- (NSString *)descriptionWithLocale:(id)locale {
    //以下两种方法均能实现目的,第二中方法排版视觉效果更好
    
//    NSMutableString *string = [NSMutableString stringWithString:@"{\n"];
//    [self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
//        [string appendFormat:@"\t%@ = %@;\n", key, obj];
//        //去除转义字符
//        [string stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"];
//    }];
//    [string appendString:@"}\n"];
//    return string;
    NSString *output;
    @try {
        output = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:self options:NSJSONWritingPrettyPrinted error:nil] encoding:NSUTF8StringEncoding];
        output = [output stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"]; // 处理\/转义字符
    } @catch (NSException *exception) {
        output = self.description;
    } @finally {

    }
    return  output;
}
@end
@implementation NSArray (Log)
/**
 分类中重写方法解决数组输出中文乱码的问题
 
 @return 输出结果
 */
- (NSString *)descriptionWithLocale:(id)locale {
    NSMutableString *string = [NSMutableString stringWithString:@"(\n"];
    [self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        [string appendFormat:@"\t%@,\n", obj];
        //去除转义字符
        [string stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"];
    }];
    if ([string hasSuffix:@",\n"]) {
        [string deleteCharactersInRange:NSMakeRange(string.length - 2, 1)]; // 删除最后一个逗号
    }
    [string appendString:@")\n"];
    
    return string;
}
@end

四、锚点

参考

额外补充:
CGAffineTransformMakeScale是对单位矩阵进行缩放。
CGAffineTransformScale是对第一个参数的矩阵进行缩放。
默认是基于视图的中心去缩放。

_v.bounds = CGRectMake(0, 0, 100, 100);
_v.center = CGPointMake(100, 400);
//此时:(origin = (x = 50, y = 350), size = (width = 100, height = 100))
_v.transform = CGAffineTransformMakeScale(2, 2);
//此时:(origin = (x = 0, y = 300), size = (width = 200, height = 200))

五、动画分类

注意: CGAffineTransform 系列不同于 CAAnimation 系列,CGAffineTransform 会改变视图的 frame 相关属性,CAAnimation 只是制造假象。
参考

六、动画曲线

    UIViewAnimationCurveEaseInOut,         // slow at beginning and end
    UIViewAnimationCurveEaseIn,            // slow at beginning
    UIViewAnimationCurveEaseOut,           // slow at end
    UIViewAnimationCurveLinear,

七、Xcode中other linker flags 的作用

参考

八、GCD

注意:实际面试过程中有人会问到队列和线程的关系,实际回答过程中就针对下方的表达做回答即可。
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);dispatch_async(dispatch_queue_t queue, dispatch_block_t block);的前两个参数分别为队列和要执行的任务。


通过上图可以看出 2 条重要信息:

另外还有一个注意点:
sync 函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)。比如下面代码,任务1、dispatch_sync、以及任务 3 在主队列中,此时又往主队列中添加任务2,当前队列指的就是主队列,且主队列本身是串行队列。

- (void)ViewDidLoad{
  NSLog(@"1");// 任务1
  dispatch_sync(dispatch_get_main_queue(),^{
      NSLog(@"2");// 任务2
  });
  NSLog(@"3");// 任务3
}

参考

九、临界资源概念

临界资源指的是一些虽作为 共享资源却又无法同时被多个线程共同访问的资源 。当有进程在使用临界资源时,其他进程必须依据操作系统的同步机制等待占用进程释放共享资源才可重新竞争使用共享资源。

十、线程锁

十一、线程不安全是否会导致崩溃?

先用一幅图说明什么是线程不安全问题,如两个线程同时往里面写数据,就有可能存在互相覆盖的情况。



线程不安全自身是不会导致崩溃问题的,但是线程不安全引发的数据或逻辑上的错误可能引发崩溃问题。参照上图逻辑顺序,子线程1先删除了数组中的部分元素,但是子线程2并不知道,此时还是拿删除之前的数组长度去访问最后一个元素会发生崩溃。

十二、全局并发队列和手动创建的并发队列

 dispatch_queue_t queue1 = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue2 = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue3 = dispatch_queue_create("queu3", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue4 = dispatch_queue_create("queu4", DISPATCH_QUEUE_CONCURRENT);
//虽然第一个参数和queue4 对象一样,但最后创建出来也是不同的队列对象
    dispatch_queue_t queue5 = dispatch_queue_create("queu4", DISPATCH_QUEUE_CONCURRENT);

全局并发队列可以理解为一个全局对象,在整个应用内任何地方获取的都是同一个队列对象。手动创建的并发队列每次都是创建不同的对象,即使第一个参数一样(实际开发中不建议这样做),最终返回也是不同的队列对象。

十三、子线程中能否运行定时器

正常情况下来说子线程无法运行定时器,因为一旦出了子线程的生命周期,子线程已经被销毁,之后的代码将无法正常运行。但通过线程保活技术可以继续运行定时器。

十四、atomic 和线程安全

一般的所可以针对特定的代码片段去添加,保证对应代码片段的线程安全。而 atomic 仅用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁。但它并不能保证使用属性的过程是线程安全的。如 arr 是obj 对象中的 atomic 修饰的属性,当执行 [obj.arr addObject:@"1"];这行代码时,obj.arr 会调用 arr 的 getter 方法,是线程安全的。但 addObject 并不会触发 arr 的 getter 或 setter 方法,此时不是线程安全的。

因为 atomic对每一次 setter 和 getter 方法调用都加锁, 比较消耗性能,所以一般在 iOS 设备上很少使用,Mac 上偶尔会使用到。

十五、内存管理

十六、多级缓存的意义(寄存器、内存、磁盘)

。。。。。。。

十七、

。。。。。。。

十八、iOS 如何进行网络测速

https://www.jianshu.com/p/9f67b7716b9d
http://www.cocoachina.com/articles/27654

通过调研发现,目前常见的网络测速方案只有两种:

方案1:通过上传和下载数据包,使用 TotalSize / TotalTime 来计算真实的上传和下载速率是多少

方案2:通过读取网卡数据来计算(可以通过系统API 获取),读取上一秒的整体流量消耗 T1,然后读取当前的流量消耗 T2,那么 T2 - T1 其实可以表示为当前的一个网速情况。同时这个流量数据是可以区分蜂窝网络、Wi-Fi的,也可以区分哪些是上行流量,那些是下行流量。

两种方案各有优劣,可以在合适的场合来选择对应的方案

第一种方案感觉是比较准确,这个时候是真实的在下载或上传数据,比较充分的利用了当前的带宽,计算的网速也比较接近真实的网速值。但是蜂窝网络下,会消耗用户的少量流量。

第二种方案在下载和上传东西时,计算的值和第一种方案比较接近。但是如果当前系统内没有 App 在被使用,处于静止状态的话,其实当前读取的流量值是比较小的,无法反映出网速情况,但是可以实时反映流量消耗状况。

十九、泛型

iOS 强大的泛型
[iOS][OC] 理解并应用OC的泛型提高代码质量
http://blog.sunnyxx.com/2015/06/12/objc-new-features-in-2015/

二十、局部性原理

二十一、汇编语言种类

汇编语言的种类:

其中 x86 和 x64 汇编根据编译器的不同,有 2 中书写形式 Intel(Windows 派系) 和 AT&T(Unix 派系)。

iOS 开发者主要涉及两种汇编语言 AT&T 汇编(模拟器)和 ARM 汇编(真机)。

二十二、常用汇编指令

二十三、Swift 中结构体都存在占空间吗?

腾讯课堂视频第六节 1:00分前后,Class 内存一定存在堆空间,但是Class 对象可能存在栈区

二十四、dSYM解析

参考
参考

二十五、布隆过滤器

二十六、iOS 小组件widget开发

iOS的小组件widget开发参考

二十七、应用程序生命周期 watch dog 时间

二十八、捕获不到的异常

KVO 问题、NSNotification 线程问题、数组越界、野指针等崩溃信息,是可以通过信号捕获的。但是,像后台任务超时、内存被打爆、主线程卡顿超阈值等信息,是无法通过信号捕捉到的。

二十九、为何需要解压缩图片

三十、IQKeyboard 原理

IQKeyboardManager 在需要解决键盘遮挡时会去递归找可滚动的父视图进行偏移,如果没有就对 window 的 frame 做文章。核心方法是 adjustFrame,通过它解决键盘遮挡。IQKeyboardManager 考虑的非常全面,以至于里面很多的判断语句,主要是为了能使用于目前发现的任何情况

三十一、Symbol

知识小集 :深入理解
参考

三十二、查询所有应用bundle id 和 udid 命令

ideviceinstaller  -l
instruments -s

三十三、本地 commit 的代码撤销

git reset HEAD~   

三十四、查找静态库是否包含特定字符串

参考
参考
使用

./findlib.sh  ~/Desktop/iphone_ive_cook/SRC  "_DriftsandEn"

findlib.sh 内部代码

#!/bin/sh 

function func_log_print(){
    local level="$1"
    local msg="$2"
        case $level in
        error)  echo -e "\033[41;30;1m${msg}\033[0m";;
        warn)   echo -e "\033[43;30;1m${msg}\033[0m";;
          info) echo -e "\033[47;30;1m${msg}\033[0m";;
        concern)    echo -e "\033[42;30;1m${msg}\033[0m";;
    *)      echo "${msg}";;
    esac
}
## 暂时缺对framework的查询
find $1 -name '*.a' > .ddddtmp.bat

for line in `cat .ddddtmp.bat`
do
aa=`strings ${line} | grep $2 -I`
if [ -n "${aa}" ] ;then
  func_log_print warn "${line}"
  strings ${line} | grep $2 -I
fi

done

rm .ddddtmp.bat

三十五、curl 、postman

curl 参考
postman 参考

三十六、DYSM 解析流程记录

1、DYSMTools 解析

参考
mac 自带进制计算器使用
涉及进制计算,需要使用mac 自带计算器进行进制计算。

2、手机内部崩溃信息解析
1.首先在桌面创建个文件夹,如:crash
2.通过终端指令:find /Applications/Xcode.app -name symbolicatecrash -type f找到Xcode自带解析工具symbolicatecrash的路径,之后把这个文件复制到桌面crash文件夹中。

/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

3.在Archives下找到打包文件,右键进入finder打开显示包内容,之后找到xx.app和xx.app.dSYM文件,xx代表项目名
4.把xx.app.dSYM复制到桌面crash文件夹中
5.在Crashes中找到对应xx.app.dSYM的crash信息,右键把.crash文件复制到桌面crash文件夹中

  1. .app.dSYM和.crash文件的UUID要一致
    7.查看xx.app.dSYM的UUID,先终端cd 到放置xx.app.dSYM的文件夹,之后 dwarfdump --uuid xx.app.dSYM/ (xx代表App的项目名)
    8.查看xx.crash文件的UUID,先终端cd 到放置xx.crash的文件夹, 之后 grep "AppName arm64" xx.crash
    9.查看xx.app的UUID,先终端cd 到放置xx.app的UUID的文件夹,之后dwarfdump --uuid xx.app/xx (xx代表App的项目名)
    10.开始进行解析工作 cd 到桌面crash文件夹中
  2. 输入命令(解析核心命令): ./symbolicatecrash ./.crash ./.app.dSYM > symbol.crash
sfit-macmini-for-test:crash user$ ./symbolicatecrash CrashDemo0809-2018-08-09-143458.ips CrashDemo0809.app.dSYM > out.crash
sfit-macmini-for-test:crash user$ 
  1. 如果不成功用xcode-select -print-path 检查一下环境变量,正确返回/Applications/Xcode.app/Contents/Developer/ 如果返回的不是这个,用export DEVELOPER_DIR=/Applications/XCode.app/Contents/Developer 设置导出一下环境变量
    参考

三十七、SDWebImage性能优化

SDWebImage有个性能优化(不是问缓存流程那一套),咋优化的;
图片解析那块,就是先在子线程把解析的数据转成CGImage
1、SDWebImage提供了对图片进行了缓存,主要由SDImageCache完成。该类负责处理内存缓存以及一个可选的磁盘缓存,其中磁盘缓存的写操作是异步的,不会对UI造成影响。

2、图片解压缩
SDWebImage 在下载完图片之后,会调用decodedImageWithImage:image方法异步对图片进行解压缩后返回。
在我们使用 UIImage 的时候,创建的图片通常不会直接加载到内存,而是在渲染的时候再进行解压并加载到内存。这就会导致 UIImage 在渲染的时候效率上不是那么高效。为了提高效率通过 decodedImageWithImage方法把图片提前解压加载到内存,这样这张新图片就不再需要重复解压了,提高了渲染效率。这是一种空间换时间的做法。

参考

三十八、打包原理及脚本自动打包的实现

我们写出的代码经过llvm进行build, 编译完成后会生成.app文件, 之后进行Archive归档, 然后进行Export导出, 这就是最基本的原理。打包脚本可以借助苹果官方的 xcodebuild 命令。总结为如下三步:

xcodebuild -workspace "/Users/sam/Desktop/TestPackage-workspace/TestPackage.xcworkspace" -scheme "TestPackage" -configuration "Debug"

3、xcode 缓存路径下生成 .app 文件,显示包内容,会发现里面包含签名文件。说明应用签名成功。

执行如下命令,最终生成一个 .xcarchive 文件。

xcodebuild -workspace "/Users/sam/Desktop/TestPackage-workspace/TestPackage.xcworkspace" -scheme "TestPackage" -configuration "Debug" -archivePath "/Users/sam/Desktop/TestPackage.xcarchive" archive
-exportOptionsPlist "/Users/sam/Desktop/TestPackage 2018-12-07 17-29-35/ExportOptions.plist"

打包原理及脚本自动打包的实现

三十九、 安装 ipa 包

参考

四十、import "" 和 import <>

参考

四十一、抓包原理

https://www.jianshu.com/p/ca3204cb793a

四十二、iOS是否支持重载

概念:所谓方法的重写是指子类中的方法和父类中继承的方法有完全相同的返回值类型、方法名、参数个数和参数类型。这样就可以实现对父类方法的覆盖。只能发生在父类和子类之间,又叫函数覆盖。

所谓的方法重载是指函数名相同,函数的参数列表不同(包括参数个数和参数类型),至于返回类型可同可不同。重载既可以发生在同一个类的不同函数之间,也可发生在父类子类的继承关系之间,其中发生在父类子类之间时要注意与重写区分开。

结论:按照重载严格定义说OC不支持重载,但事实上OC支持参数个数不同的函数重载。

描述

//参数个数不同的函数重载。本质上是方法名不同,函数参数多少回影响到方法名。但在js 中不会出现此种的情况。
- (void)test:(int)one {
 NSLog(@"one parameter!");
}
- (void)test:(int)one andTwo:(int)two {
 NSLog(@"two parameters!");
}
//参数个数相同的函数不支持重载,后三个方法会报错。即使方法返回值不同,也依然报错,OC中只认方法名
- (void)testTwo:(int)two{}
- (void)testTwo:(NSInteger)two2{}
- (BOOL)testTwo:(NSInteger)two{}
- (BOOL)testTwo:(NSInteger)two{}

四十三、js里继承的实现原理;同样是面向对象的语言,和iOS有啥区别

https://juejin.im/post/5c60ca2ae51d45719046d73f

四十四、Mach-O

Mach-O 类型

通用二进制文件

Mach-O 结构

一个Mach-O文件包含3个主要区域,通过 MachOView图像可视化工具可以分析Mach-O 结构。

四十五、代码混淆

加固方式

加固是为了增加应用的安全性,防止应用被破解、盗版、二次打包、注入、反编译等。常见的加固方式有:

代码混淆

iOS 程序可以通过class-dump、Hopper、IDA等工具获取类名、方法名、以及分析程序的执行逻辑,方便别人逆向自己的应用。如果进行代码混淆,可以加大别人的分析难度。

代码混淆方案:
源码混淆方案

字符串加密

可执行文件中的字符串信息,对破解者来说,非常关键,是破解的捷径之一。为了加大破解、逆向难度,可以考虑对字符串进行加密。字符串的加密技术有很多种,可以根据自己的需要自行制定算法。

如:对每个字符进行异或()处理,当需要使用字符串时,对异或()过的字符再进行一次异或(^),就可以获得原字符。主要原理是两次异或操作最终得到其本身。

20^5//得到的不是20
20^5^5//最终得到20

四十六、 FPS 帧率监测原理

CADisplayLink 的调用频率始终和屏幕刷新的频率一致。所以 FPS = 刷新次数 / 间隔时间

四十七、layoutSubviews、setNeedsLayout、layoutIfNeeded

UI 刷新时机

如果打印App启动之后的主线程 RunLoop 可以发现另外一个 callout 为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 的Observer,这个监听专门负责UI变化后的更新,比如修改了frame、调整了UI层级(UIView/CALayer)或者手动设置了setNeedsDisplay / setNeedsLayout之后就会将这些操作提交到全局容器。而这个 Observer 监听了主线程 RunLoop 的即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的 UI 更新并提交进行实际绘制更新。

四十八、子线程修改 frame 会不会生效?

主线程刷新 UI 原因

像UIKit这样大的框架上确保线程安全是一个重大的任务,会带来巨大的成本。UIKit不是线程安全的,假如某一个线程中遍历找寻某个subView,然而在另一个线程中删除了该subView,那么就会造成错乱。apple有对大部分的绘图方法和诸如UIColor等类改写成线程安全可用,可还是建议将UI操作保证在主线程中。

答:不会生效。

四十九、视图是如何显示到界面上的

CALayer 和 UIView

CALayer 里有个 id <CALayerDelegate> delegate 属性,实际代码运行中,这个 delegate 被赋值为 UIView 类,即 UIView 是 CALayer 的代理。UIView控制大小和事件处理,CALayer控制渲染和内容展示。

@property(nullable, weak) id <CALayerDelegate> delegate;

渲染过程

参考

注意 drawRect 的消耗

首先要清楚,在 drawRect 中所调用的重绘功能是基于 Quartz 2D 实现的,Quartz 2D是CoreGraphics框架的一部分,因此其中的相关类及方法都是以CG为前缀。在drawRect重绘过程中最常用的就是CGContext类。CGContext又叫图形上下文,相当于一块画板,以堆栈形式存放,只有在当前context上绘图才有效。

CAShapeLayer 与 drawRect 重绘的一些比较:

五十、SEL 和 IMP 的区别

每个类对象都有一个方法列表,方法列表存储方法名、方法实现、参数类型,SEL是方法名(编号),IMP指向方法实现的首地址。

五十一、设计的6大原则

五十二、isKindOfClass 和 isMemberOfClass

isKindOfClass 和 [isMemberOfClass 内部实现逻辑。说明: object_getClass 方法如果传入实例对象则返回类对象;如果传入类对象则返回元类对象。

//object_getClass()取得的是对象的isa指针指向的对象,也就是判断传入的类对象的元类对象是否与传入的这个对象相等,所以这个cls应该是元类对象才有可能相等
       + (BOOL)isMemberOfClass:(Class)cls {
          return object_getClass((id)self) == cls;
       }
       //循环判断传入的类对象的元类对象及其父类的元类对象是否等于传入的cls
       + (BOOL)isKindOfClass:(Class)cls {
          for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
              if (tcls == cls) return YES;
          }
          return NO;
       }
                       
       //判断传入的实例对象的类对象是否与传入的对象相等,所以cls只有可能是类对象才有可能相等
       - (BOOL)isMemberOfClass:(Class)cls {
          return [self class] == cls;
       }
      //循环判断实例对象的父类的类对象是否等于传入的对象cls,也就是判断实例对象是否是cls及其子类的一种
       - (BOOL)isKindOfClass:(Class)cls {
          for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
              if (tcls == cls) return YES;
          }
          return NO;
       }
 [[NSObject class] isKindOfClass:[NSObject class]];//1
      [[Person class] isKindOfClass:[NSObject class]];//1
      [[NSObject class] isMemberOfClass:[NSObject class]];//0
      [[NSObject class] isMemberOfClass:object_getClass([NSObject class])];//1
      
      [[Person class] isKindOfClass:[Person class]];//0
      [[Person class] isKindOfClass:object_getClass([Person class])];//1
      [[Person class] isMemberOfClass:[Person class]];//0
      [[Person class] isMemberOfClass:object_getClass([Person class])];//1

按照 isKindOfClass 和 isMemberOfClass 实现逻辑,第一个和第二的打印结果让人费解。方法调用者是类对象,传入的参数也是类对象,理论有只有方法调用者为类对象,参数为元类对象时才返回 YES,其余情况为 NO。NSObject 能匹配成功,是因为 NSObject 元类(根元类)对象的 superClass 指针比较特殊,NSObject 元类的 superClass 指向 NSObject 的类对象,方法调用者和传入的参数都是 NSObject 类,所以最终会匹配成功。

扩展问题:下面代码打印结果

@interface NSObject (Test)
+(void)test;
@end
@implementation NSObject (Test)
-(void)test {
    NSLog(@"test");
}
@end

请问调用 [NSObject test]; 什么结果?答案是输出 test 。主要原因在于:NSObject 元类的 superClass 指针指向 NSobject 类,方法调用的过程中如果当前类没有找到方法会尝试到父类中查找。虽然一个是类方法一个是对象方法,但方法名都是一致的。所以能正常调用。结论:NSObject 的类方法和实例方法是可以用 任意实例对象任意类 随意调用的。如在ViewController类中调用上述分类 test 方法,结果也是一样的。[ViewController test];

五十三、成员变量和属性

1、成员变量是不与外界接触的变量,应用于类的内部,所以当用于类内部,属性为private时,就可以将变量定义为成员变量;
2、属性变量可以让其他对象访问,可以设置只读、只写等属性,当属性为public 时,定义属性在.h中;

五十四、通用缓存设计

一、磁盘+内存组合优化

这样就充分结合两者特性,利用内存读取快特性减少读取数据时间。

二、磁盘优化

四种磁盘缓存方案:
优化方案

三、内存优化

提高内存命中率(LRU)

LRU: 可以将链表看成一串数据链,每个数据是这个串上的一个节点,经常访问的数据移动到头部,等数据超出容量后从链表后面的一些节点销毁,这样经常访问数据在头部位置,还保留在内存中。

四、磁盘和内存读取线程安全

@interface dispatch_semaphoreDemo()
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
@end
@implementation dispatch_semaphoreDemo
- (instancetype)init{
if (self = [super init]) {
self.semaphore = dispatch_semaphore_create(1);
}
return self;
}
- (void)otherTest{
  for (int i = 0; i < 20; i++) {
    [[[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil] start];
  }
}
- (void)test{
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
sleep(2);
NSLog(@"test - %@", [NSThread currentThread]);
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);
}
@end

五、YYCache 使用

YYCache *cache = [YYCache cacheWithName:@"ResponseCache"];
  if ([cache containsObjectForKey:url] && networkErrow) {    // 如果有缓存且网络有问题
    id response = [cache objectForKey:url];
    success(response);
  return;
  }
   [[MNNetworkTool shareService] GET:url parameters:param progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
                if (mnnetSet.saveCache) {  // 如果需要缓存,进行缓存
                    [cache setObject:dic forKey:url];
                }
                success(responseObject);
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            failed();
        }];
//清除缓存
  YYCache *cache = [YYCache cacheWithName:@"ResponseCache"];
  [cache removeAllObjects]; // 移除所有缓存

六、YYCache 细节问题

1、整体结构
2、磁盘读写问题

五十五、@synchronized 锁

@synchronized 参数问题

@synchronized (self) 这种写法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。执行到这段代码结尾处,self 对应的锁就释放了。这样做的优点是:不需要在代码中显式的创建锁对象,便可以实现锁的机制。但是,滥用 @synchronized (self) 则会降低代码效率,因为公用同一个锁的那些同步块,都必须按顺序执行。若是在 self 对象上频繁加锁,程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码,这样效率就低了。
注:因为 @synchronized (self) 方法针对self只有一个锁,相当于对于 self 的所有用到同步块的地方都是公用同一个锁,所以如果有多个同步块,则其他的同步块都要等待当前同步块执行完毕才能继续执行。对于同一个类中要求出现多个锁的情况, @synchronized 中应该传入其它对象。

@synchronized 是递归锁

@synchronized 是对 mutex 递归锁的封装,@synchronized(obj) 内部会生成 obj 对应的递归锁,然后进行加锁、解锁操作。相比于互斥锁,递归锁需要多做一些额外操作,所以其开销会比互斥锁更大一些。

关于递归锁:

- (void)sellingTickets{
  pthread_mutex_lock(&_ticketMutex);
  [self sellingTickets2];
  pthread_mutex_unlock(&_ticketMutex);
}
- (void)sellingTickets2{
  pthread_mutex_lock(&_ticketMutex);
  NSLog(@"%s",__func__);
  pthread_mutex_unlock(&_ticketMutex);
}

上述代码 sellingTickets 和 sellingTickets2 共用了同一把锁,且 sellingTickets 调用了 sellingTickets2 方法,如果此锁只是一把普通的互斥锁,就会出现死锁,主要原因在于: 方法 sellingTickets 的结束需要 sellingTickets2 解锁,方法 sellingTickets2 的结束需要 sellingTickets 解锁,相互引用造成死锁。

但是上述代码中的 pthread_mutex 是递归锁,所以类似递归调用的场景不会出现死锁的情况。递归锁的定义是允许同一个线程对同一把锁进行重复加锁。

五十六、RN 和原生通信原理

1、整体结构

RN整体结构

从上图可知,在 RN 中 C++与JS部分的实现为多平台共用,在此基础上再分化为各平台实现。其中,iOS 平台特有的实现(Objective-C、C++)主要集中在 React 下,以C++语言实现的共有部分在 ReactCommon 下:


OC 和 JS 两端都保存了一份配置表,里面标记了所有 OC 暴露给 JS 的模块和方法。这样,无论是哪一方调用另一方的方法,实际上传递的数据只有 ModuleId、MethodId 和 Arguments。这三个元素,它们分别表示类、方法和方法参数,当 OC 接收到这三个值后,就可以通过 runtime 唯一确定要调用的是哪个函数,然后调用这个函数。

既然说到函数互调,那么就不得不提到回调了。对于 OC 来说,执行完 JS 再执行 Objective-C 回调毫无难度,难点依然在于 JS 代码调用 OC 之后,如何在 OC 的代码中,回调执行 JS 代码。目前的做法是:JS 调用 OC 时,JS 提前注册要回调的 Block,并且把 BlockId 作为参数发送给 OC,OC 收到参数时会创建 Block,调用完 OC 函数后就会执行这个刚刚创建的 Block。

2、JS 调 Native

通过 Apple 推出的 JavaScriptCore,要实现 JS to Native 的通信并非难事,主要有两条途径:

JS 端通信部分的核心代码不到两百行,主要由 NativeModules.js、MessageQueue.js、BatchedBridge.js三个文件构成。BatchedBridge 对象提供了 JS 触发 Native 调用的方法:

var enqueueNativeCall = function(moduleID, methodID, params, onFail, onSuccess) {
    if (onFail || onSuccess) {
        // 如果存在 callback 回调,添加到 callbacks 字典中
        // OC 根据 callbackID 来执行回调
        if (onFail) {
            params.push(this.callbackID);
            this.callbacks[this.callbackID++] = onFail;
        }
        if (onSuccess) {
            params.push(this.callbackID);
            this.callbacks[this.callbackID++] = onSuccess;
        }
    }   
        // 将 Native 调用存入消息队列中
        this.queue[MODULE_INDEX].push(moduleID);
        this.queue[METHOD_INDEX].push(methodID);
        this.queue[PARAMS].push(params);
        // 每次都有 ID
        this.callID++;
        const now = new Date().getTime();
        // 检测原生端是否为 global 添加过 nativeFlushQueueImmediate 方法
        // 如果有这个方法,并且 5ms 内队列还有未处理的调用,就主动调用 nativeFlushQueueImmediate 触发 Native 调用
        if (global.nativeFlushQueueImmediate && now - this.lastFlush > MIN_TIME_BETWEEN_FLUSHES_MS) {
            global.nativeFlushQueueImmediate(this.queue);
            // 调用后清空队列
            this.queue = [[], [], [], this.callID];
        }
}

该函数将调用 Native 实例模块 ID,方法 ID,以及回调 ID 分别存入三个队列中,this.queue 持有这三个队列。nativeFlushQueueImmediate 函数是关键,原生中提前中提前注册好 nativeFlushQueueImmediate 方法,js 执行 nativeFlushQueueImmediate 时会触发原生端 block 的调用,并传入参数 queue,触发 native 调用。。

self->_context[@"nativeFlushQueueImmediate"] = ^(NSArray<NSArray *> *calls) {
    AHJSExecutor *strongSelf = weakSelf;
    [strongSelf->_bridge handleBuffer:calls];
};

每个模块和方法,都会关联一个 ID,这个 ID 其实是模块和方法在各自列表中所处的下标。发起调用时,JS 端将 ID 存入消息队列,Native 拿到 ID,在 Native 端的配置表中,找到对应的原生类(实例)和方法,并进行调用。

对象和方法约定好了,还需要约定参数,将参数值按顺序放入一个数组中。使用者需要注意参数的个数和顺序,保持与 Native 端的方法匹配,否者会报错。

最后是 JS callback 的处理,JS 和 Native 通信是无法传递事件的,所以选择将事件序列化,给每个 callback 一个 ID,自己存一份,再将 ID 传给 Native,当 Native 要执行这个回调时,通过 invokeJSCallback 函数把这个 ID 回传给 JS,JS 再根据 ID 查找对应的 callback 并执行。

3、Native 调 JS

Native 调用 JS 依赖于 JavaScriptCore,该框架提供创建 JS 上下文环境,以及执行 JS 代码的接口,相对来说简单很多。

同步调用的场景非常少,因为它仅限于在 JS 线程调用,而实际情况是,Native 和 JS 的通信几乎都是跨线程的。因为页面刷新和事件回调都发生在主线程。对于 Native 端来说,JS 线程是普通的一个线程,跟其他线程没有区别,只不过是用这个线程来初始化 JS 的上下文环境,以及执行 JS 代码。

Native 异步调用 JS 主要由callFunctionReturnFlushedQueue函数分发,该函数定义在BatchedBridge对象,并由 global 的__batchedBridge属性所持有。

var callFunctionReturnFlushedQueue = function(module, method, args) {
    this.__callFunction(module, method, args);
    return this.flushedQueue();
}

为什么 JS 和 Native 的通信只能是异步?
本质原因是 JS 是单线程执行的。而 Native 端负责 UI 展示的又只能是主线程,跨线程通信如果使用同步会阻塞 UI。

为什么 JS 调 Native 要设计一个消息队列,等待 Native 调用时才执行,而不像 Native 调 JS 每次调用都直接去执行呢?
为了提高性能,批量处理 JS 的 Native 调用,可以减少 JS 与 Native 通信开销。

参考一
参考二

五十七、RN 和 Flutter

RN 和 Flutter 对比

RN

除了编程语言、页面框架和自动化工具以外,React Native 的表现就处处不如 Flutter 了。相比RN,Flutter 的优势最主要体现在性能、开发效率和体验这两大方面。

Flutter

WebView渲染和 RN 渲染对比

对于第一类 WebView 的大前端渲染,主要工作在 WebKit 中完成。WebKit 的渲染层来自以前 macOS 的 Layer Rendering 架构,而 iOS 也是基于这一套架构。所以,从本质上来看,WebKit 和 iOS 原生渲染差别不大。
第二类的类 React Native 更简单,渲染直接走的是 iOS 原生的渲染。

为什么 WebView 比 RN 慢?

Flutter 原理

以往最早的Hybrid开发,主要依赖于WebView。但是WebView是一个很重的控件,很容易产生内存问题,而且复杂的UI在WebView上显示的性能不好。react-native技术抛开了WebView,利用JavaScriptCore来做桥接,将js调用转为native调用,只牺牲了小部分性能获取的跨平台开发,这是一大进步。所以现在react-native很流行的原因。

Flutter实现跨平台采用了更为彻底的方案。它既没有采用WebView也没有采用JavaScriptCore,而是自己实现了一台UI框架,然后直接系统更底层渲染系统上画UI。所以它采用的开发语言不是JS,而Dart。据称Dart语言可以编译成原生代码,直接跟原生通信。
https://blog.csdn.net/wolfking0608/article/details/80769567

五十八、AOT 和 JIT

JIT,即Just-in-time,动态(即时)编译,边运行边编译;AOT,Ahead Of Time,指运行前编译,是两种程序的编译方式。

JIT优点:

可以根据当前硬件情况实时编译生成最优机器指令(ps. AOT也可以做到,在用户使用是使用字节码根据机器情况在做一次编译)
可以根据当前程序的运行情况生成最优的机器指令序列
当程序需要支持动态链接时,只能使用JIT
可以根据进程中内存的实际情况调整代码,使内存能够更充分的利用

JIT缺点:

编译需要占用运行时资源,会导致进程卡顿
由于编译时间需要占用运行时间,对于某些代码的编译优化不能完全支持,需要在程序流畅和编译时间之间做权衡
在编译准备和识别频繁使用的方法需要占用时间,使得初始编译不能达到最高性能

AOT优点:

在程序运行前编译,可以避免在运行时的编译性能消耗和内存消耗
可以在程序运行初期就达到最高性能
可以显著的加快程序的启动

AOT缺点:

在程序运行前编译会使程序安装的时间增加
牺牲Java的一致性
将提前编译的内容保存会占用更多的外

五十九、离屏渲染

离屏渲染需要在屏幕外开辟内存空间,提前使用 CPU 渲染复杂的视图,保证视频控制器能够及时地从缓存区读到新的渲染结果。它在 GPU 面临性能瓶颈时,将压力转移一部分给比较空闲的 CPU,然而 CPU 的渲染能力远没有 GPU 高效,有点杀鸡出牛刀的意思。视频控制器要读取离屏渲染的结果,需要把渲染上下文从当前屏幕缓存区切到屏幕外缓存区,当要显示非离屏渲染视图的时候又要切换回来,然而不可能在一屏上所有的元素都是离屏渲染的,所以视频控制器上下文需要不停地来回切换。而这种上下文切换的代价非常昂贵。

离屏渲染并不是一无是处的,虽然会造成很多额外的开销,但也是为了充分利用设备的资源来保证界面的流畅。发生离屏渲染时,是为了引起开发者对性能的关注,减少不必要的透明视图层级。如果不可避免的要触发离屏渲染,并且发生离屏渲染视图内容不会频繁的变化,可以利用 CALayer.shouldRasterize 开启光栅化,将离屏渲染的内容以位图的形式缓存,减少复杂视图频繁渲染的开销。然而,这个缓存的时效是 100ms,也就是刷新 6 帧的时间,如果视图内容更新频繁,缓存就会不停的刷新,导致无法命中,开启光栅化并没有什么作用。

https://www.jianshu.com/p/24dac847cfc4
https://zhuanlan.zhihu.com/p/72653360

经典::::::::::::::
https://mp.weixin.qq.com/s/RFIiwvEhdSBub7HFm3C1Ug

面试必考题:为什么圆角和裁剪后iOS绘制会触发离屏渲染? 答:默认情况下每个视图都是完全独立绘制渲染的。而当某个父视图设置了圆角和裁剪并且又有子视图时,父视图只会对自身进行裁剪绘制和渲染。当子视图绘制时就要考虑被父视图裁剪部分的绘制渲染处理,因此需要反复递归回溯和拷贝父视图的渲染上下文和裁剪信息,再和子视图做合并处理,以便完成最终的裁剪效果。这样势必产生大量的时间和内存的开销。解决的方法是当父视图被裁剪和有圆角并且有子视图时,就单独的开辟一块绘制上下文,把自身和所有子视图的内容都统一绘制在这个上下文中,这样子视图也不需要再单独绘制了,所有裁剪都会统一处理。当父视图绘制完成时再将开辟的缓冲上下文拷贝到屏幕上下文中去。这个过程就是离屏渲染!!所以离屏渲染其实和我们先将内容绘制在位图内存上下文然后再统一拷贝到屏幕上下文中的双缓存技术是非常相似的。使用离屏渲染主要因为iOS内部的视图独立绘制技术所导致的一些缺陷而不得不才用的技术。

六十、NSString用copy还是strong修饰?

六十一、动态链接库和静态链接库

在 iOS 8 之前,iOS 平台不支持开发者使用用户自己的动态 Framework,appstore不能上架,因为 iOS 应用都是运行在沙盒当中,不同的程序之间不能共享代码。但是,iOS 8/Xcode 6 推出之后,因为 Extension 和 App 是两个分开的可执行文件,同时需要共享代码,iOS添加了对动态库的支持。

六十二、引用计数加减

谁创建谁release ,谁retain谁release。需要注意的是:release并不代表销毁\回收对象,仅仅是计数器 -1。

一旦重写了dealloc方法,就必须调用[super dealloc],并且放在最后面调用

六十三、OC 的动态性体现

动态类型

动态类型是跟静态类型相对的。像内置的明确的基本类型都属于静态类型(int、NSString等)。静态类型在编译的时候就能被识别出来。所以,若程序发生了类型不对应,编译器就会发出警告。而动态类型就编译器编译的时候是不能被识别的,要等到运行时(run time),即程序运行的时候才会根据语境来识别。

动态绑定

OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去。这就是动态绑定,要实现他就必须用SEL变量绑定一个方法。最终形成的这个SEL变量就代表一个方法的引用。SEL变量只是一个整数,他是该方法的ID。动态绑定所做的,即是在实例所属类确定后,将某些属性和相应的方法绑定到实例上。

动态加载

根据需求加载所需要的资源,这个很容易理解;做个最简单的说明。因为 Retina 屏幕的出现, 图片经常要准备 @2x ,@3x,但是我们在使用的时候,并没有代码要求使用 2倍 3倍图,系统会动态自动完成加载。

六十四、强类型和弱类型语言

强类型语言

一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。强类型定义语言是类型安全的语言。

弱类型语言

数据类型可以被忽略的语言。它与强类型定义语言相反, 一个变量可以赋不同数据类型的值。强类型定义语言在速度上可能略逊色于弱类型定义语言,但是强类型定义语言带来的严谨性能够有效的避免许多错误。像vb,php,js 等就属于弱类型语言

六十五、UIWebView 和 WKWebView 的区别

在 iOS8 之后,使用 WKWebView 来取代 UIWebView,之后上架的应用不能使用 UIWebView。

六十六、ARC 上可能出现野指针的情况

六十七、NSHashTable(NSMutableArray) 和 NSMapTable(NSMutableDictory)

@interface ViewController ()
@property(nonatomic,strong)NSMutableArray *mArr;
@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    self.mArr = [NSMutableArray array];
    [self.mArr addObject:self];
}

上述代码会出现内存泄漏的情况,主要原因在于 mArr 内部强持有控制器。控制器又强持有 mArr。

NSHashTable

NSHashTable 类似 Foundation 框架中的 NSMutableArray,初始化时可以指定引用策略。即被添加的对象可以被 NSHashTable 强持有,也可以弱持有。

 NSHashTable *hashTable = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory];
    [hashTable addObject:human];

NSMapTable

NSMapTable 类似 Foundation 框架中的 NSMutableDictory。用法同 NSHashTable 类似。

六十八、UITableView 多图下载

实际用 SDWebImage 就可以实现,但是也可自己实现,主要考擦思路。
https://www.jianshu.com/p/32dd2c605d95

六十九、井盖为什么是圆的

七十、DNS(Domain Name System) 解析过程

  • 根 DNS 服务器 :返回顶级域 DNS 服务器的 IP 地址
  • 顶级域 DNS 服务器:返回权威 DNS 服务器的 IP 地址
  • 权威 DNS 服务器 :返回相应主机的 IP 地址

七十一、DNS 劫持

1、DNS劫持和HTTP 劫持概念

https://zhuanlan.zhihu.com/p/31027719

2、如何防止DNS 劫持

参考

七十二、URL 和 URI 的区别

URL 组成

.七十三、a 和 frameWork 区别

七十四、数据库知识

关系数据库一对多和多对多
参考

参考

七十五、IM SDK

传输协议 UDP 和 TCP 的选择

第三方服务商IM底层协议基本上都是TCP。CocoaAsyncSocket:
这个框架实现了两种传输协议TCP和UDP,分别对应GCDAsyncSocket类和GCDAsyncUdpSocket。
对于小公司或者技术不那么成熟的公司,IM一定要用TCP来实现,因为如果你要用UDP的话,需要做的事太多。当然QQ就是用的UDP协议,当然不仅仅是UDP,腾讯还用了自己的私有协议,来保证了传输的可靠性,杜绝了UDP下各种数据丢包,乱序等等一系列问题。
总之一句话,如果你觉得团队技术很成熟,那么你用UDP也行,否则还是用TCP为好。

关于传输格式的选择

推荐使用 Protobuf ,相比 JOSN、XML 具有如下优点:

心跳检测

心跳就是用来检测TCP连接的双方是否可用。那又会有人要问了,TCP不是本身就自带一个KeepAlive机制吗?这里我们需要说明的是TCP的 KeepAlive 机制只能保证连接的存在,但是并不能保证客户端以及服务端的可用性。

断线重连

理论上,我们自己主动去断开的Scoket连接(例如退出账号,APP退出到后台等等),不需要重连。其他的连接断开,我们都需要进行断线重连。一般解决方案是尝试重连几次,如果仍旧无法重连成功,那么不再进行重连。可参考这种方案:采用2的指数级别增长,第一次断线立刻重连,第二次2秒,第三次4秒,第四次8秒...直到大于64秒就不再重连。而任意的一次成功的连接,都会重置这个重连时间。避免快速或无限制的重连。

TCP和 UDP 粘包和断包问题

粘包概念及原因

由于数据传输过程中为数据流,经过TCP传输后,三条数据被合并成了一条,这就是数据粘包问题。TCP中会将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这么做的目的是为了减少广域网的小分组数目,从而减小网络拥塞的出现。UDP不会使用块的合并优化算法。但是 UDP 也会出现粘包问题。

除了数据块合并会导致粘包问题,下面两种情况都会造成TCP和UDP粘包:

断包解决方案

七十六、去除实际业务中数组中重复元素

七十七、ABTest SDK

七十八、YYCache

https://blog.ibireme.com/2015/10/26/yycache/
https://www.jianshu.com/p/b592ee20f09a

七十九、 YYModel

https://www.jianshu.com/p/5428552be6ce
1、整体结构
2、Type Encodings
3、有很多逻辑是调用底层 runtime API 实现

八十、ASyncDisplayKit

https://zhuanlan.zhihu.com/p/25371361

八十一、AFNetworking 2.0 和 3.0

https://www.jianshu.com/p/5572238f2039

八十二、OC 和 C 对比

八十三、 对OC 的理解

1、OC作为一门面向对象的语言,自然具有面向对象的语言特性,如:封装、多态、继承。

2、它具有静态语言的特性,又有动态语言的效率。

3、动态性主要体现在三个方面:动态类型、动态绑定、动态加载。之所以叫做动态,是因为必须到运行时才会做一些事情。

八十四、数组和链表区别

八十五、线程最大并发数实现方法(GCD)

NSOperationQueue 中一个属性可以很方便的设置线程最大并发数。GCD 中可以借助型号量来设置最大线程并发数。

dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
    dispatch_async(workConcurrentQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"1");
        sleep(5);
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(workConcurrentQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"2");
        sleep(3);
        dispatch_semaphore_signal(semaphore);
    });
      dispatch_async(workConcurrentQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"3");
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_async(workConcurrentQueue, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"4");
        dispatch_semaphore_signal(semaphore);
    });
理解线程和 CPU 的关系,为什么要控制线程最大并发数?

https://blog.csdn.net/shihuboke/article/details/76736743

八十六、 多个网络请求完成后执行下一步

信号量实现

NSString *str = @"http://www.jianshu.com/p/6930f335adba";
    NSURL *url = [NSURL URLWithString:str];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    NSURLSession *session = [NSURLSession sharedSession];
    
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    for (int i=0; i<10; i++) {
        
        NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSLog(@"%d---%d",i,i);
            count++;
            if (count==10) {
                //10个请求都回来后再放开线程
                dispatch_semaphore_signal(sem);
                count = 0;
            }
        }];
        [task resume];
    }
    //先卡主线程
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"end");
    });

NSMutableArray *imageURLs= [NSMutableArray array];
dispatch_group_t group = dispatch_group_create();                    // 1
for (UIImage *image in images) {
    dispatch_group_enter(group);                                    // 2
    sendPhoto(image, success:^(NSString *url) {
        [imageURLs addObject:url];
        dispatch_group_leave(group);                                 // 3
    });
}
dispatch_group_notify(group, dispatch_get_global_queue(), ^{         // 4
    postFeed(imageURLs, text);
});

上面这端代码可以理解为发朋友圈圈时上传多张图片的逻辑,即图片上传成功之后再去真正的发朋友圈。创建一个dispatch_group_t, 每次上传图片前先dispatch_group_enter,请求回调后再dispatch_group_leave,对于enter和leave必须配合使用,有几次enter就要有几次leave,否则group会一直存在。当所有enter的block都leave后,会执行dispatch_group_notify的block。

https://www.jianshu.com/p/e93fd15d93d3

https://www.jianshu.com/p/fb4fb80aefb8

https://www.cnblogs.com/bbqzsl/p/5287970.html

八十七、项目中分类同名怎么处理

1、避免措施

不同业务组分类命名以及分类方法命名添加在业务前缀。

2、出现此种情况如何检测

可以通过写一些检测工具去检测,参照下图和下文。关键点是使用 class_copyMethodList 方法

https://tech.meituan.com/2015/03/03/diveintocategory.html

八十八、按钮点击防抖处理

https://juejin.im/post/5b6be86d6fb9a04fbb114e02

八十九、通知实现原理

观察者模式是定义对象间一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会收到通知并自动更新。NSNotificationCenter 是使用观察者模式来实现的用于跨层传递消息。

https://www.jianshu.com/p/364d39651c2a

九十、instance和id的区别

相同点

九十一、指出如下代码不合理的地方

参照孙源面试题

九十二、流量监控

九十三、import 和 include

启动优化 https://www.jianshu.com/p/f40fdd8799b8

九十四、为什么有类对象和元类对象

http://www.zyiz.net/tech/detail-111585.html

http://www.leewong.cn/2018/05/02/why-metaclass/

九十五、垃圾回收和自动引用计数

https://www.jianshu.com/p/30efb565d42b

上一篇 下一篇

猜你喜欢

热点阅读