开发小知识

作者: ZhengYaWei | 来源:发表于2018-12-26 23:16 被阅读82次

    前言和目录

    该文章主要整理一些小知识点,主要涉及 iOS 以及计算基础相关知识点,某些知识点暂时只有标题,后续会持续更新。笔者最近一段时间面试过程中发现一些普遍现象,对于一些很不起眼的问题,很多开发者都只停留在知道听说过的层面,但是一旦问 是什么为什么 ,很多应试者回答的并不理想,比如下面的几个问题:

    • 数组的下标为什么从零开始?
    • 经常听到深拷贝和浅拷贝,为什么会存在深拷贝和浅拷贝这一对概念?
    • block 和 函数指针有什么区别?
    • 引用的本质是什么?引用和指针有什么关系?
    • UI 性能优化的时候,很多面试者会提到用CALayer代替视图组件,如果某天产品改需求,要求添加触发事件,那么CALayer上怎么添加触发事件?
    • 和 H5 交互的时候,经常会用到userAgent, 请问 userAgent 是什么?(问过几次,纯 iOS 开发者没几人知道只说有印象)
    • 标准的 MVC 架构模式中,ViewModel是完全独立开来的,很多开发者都说自己使用的是 MVC 模式,当问起:为什么实际开发中自定义视图组件时通常都会引入 Model ,并重写 setModel 方法?这还是不是 MVC ?
    • 面试过程中笔者偶尔会问多线程的相关问题,印象中有两位应试者脱口而出 自旋锁 ,当问及什么是 互斥锁 ?什么是 自旋锁 ?应试者一脸懵,明明是自己给自己挖坑。此外还会问到:为什么线程会不安全?也没几个应试者能完整回答出。
    • 很多应试者都知道,http 和 https 的区别在于多了 SSL 层,但是 SSL 层里面有什么,做了什么,位于网络模型什么位置?
    • 很多人都知道内存(堆内存)回收,但是内存(堆内存)回收后发生了什么?是把内存从堆空间清空了吗?还是重置为 0 ?还是说做了其他什么操作?
    • MD5 安全吗?如果不安全,有什么替代的方案?MD5算是加密算法的一种吗?如果不是,和加密算法有什么区别?
    • pods 经常用吧,pods 命令后面的参数--verbose 和 --no-repo-update 是什么意思?
    • 令笔者比较惊讶的是,响应链流程算是 iOS 入门基础知识。笔者问了一道相关问题百分之七八十的面试者都很难回答上来。A 为父视图,依次执行[A addSubView:B][A addSubView:C]C.userInteractionEnabled = NO,其中 B 视图和 C 视图有重叠,请问:B 视图添加点击事件能否响应?多数应试者第一反应是不能,结合响应链流程来看,答案显然是错误的。
    • super 经常用,请问 super 调用方法和 self 调用方法有什么本质区别?

    以上仅是部分典型小知识点,更多内容请详看此文。

    目录

    一、CALayer如何添加点击事件

    两种方法: convertPointhitTest:hitTest: 返回的顺序严格按照图层树的图层顺序。

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        CGPoint point = [[touches anyObject] locationInView:self.view];
        CGPoint redPoint = [self.redLayer convertPoint:point fromLayer:self.view.layer];
        if ([self.redLayer containsPoint:redPoint]) {
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"point red" message:@"" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
            [alert show];
        }
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        CGPoint point = [[touches anyObject] locationInView:self.view];
        CALayer *layer = [self.view.layer hitTest:point];
        if (layer == self.redLayer) {
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"point red" message:@"" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
            [alert show];
        }else if (layer == self.yellowLayer){
            UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"point yellow" message:@"" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
            [alert show];
        }
    }
    

    二、为什么会存在堆空间

    堆空间的存在主要是为了延长对象的生命周期,并使得对象的生命周期可控。

    • 如果试图用栈空间取代堆空间,显然是不可行的。栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,如果申请的空间超过栈的剩余空间时,将出现栈溢出,发生未知错误。因此,能从栈获得的空间较小。而堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。 但是栈空间比堆空间响应速度更快,所以一般类似int、NSInteger等占用内存比较小的通常放在栈空间,对象一般放在堆空间。
    • 如果试图用数据区(全局区)取代堆空间,显然也是不可行的。因为全局区的生命周期会伴随整个应用而存在,比较消耗内存,生命周期不向在堆空间那样可控,堆空间中可以随时创建和销毁。
    • 代码区就不用想了,如果能够轻易改变代码区,一个应用就无任何安全性可言了。

    三、Tagged Pointer 是什么?

    从 64bit 开始,iOS 引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储。在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值;使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中。当指针不够存储数据时,会使用动态分配内存的方式来存储数据。

    四、iOS平台跨域访问漏洞

    UIWebView 默认开启了WebKitAllowUniversalAccessFromFileURLsWebKitAllowFileAccessFromFileURLs 属性。利用这个漏洞给某个 App 下发一个 HTML 文件,当 UIWebView 使用 file 协议打开这个 HTML 文件, HTML 文件中含有一段窃取用户数据的 JS 代码,就会导致用户数据泄露。

    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
    _webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
    [_webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:filePath]]];
    
    <!DOCTYPE html>
    <html>
        <body>
            <script>
                // 这个可以是手机任意一个文件地址
                var localfile = "/etc/passwd"
                var xhr = new XMLHttpRequest();
                xhr.onreadystatechange = function() {
                    if (xhr.readyState == 4) {
                        alert(xhr.responseText);
                    }
                }
               try {
                  xhr.open("GET", localfile, true);
                  xhr.send();
               } catch (ex) {
                  alert(ex.message);
               }
            </script>
        </body>
    </html>
    

    上面代码可以读取出手机端 /etc/passwd 的文件。这个漏洞访问其他应用的数据,而不必需要用户的许可。但WKWiebViewWebKitAllowUniversalAccessFromFileURLsWebKitAllowFileAccessFromFileURLs 默认是关闭的(可以手动控制),不会存在这样的风险。

    补充:针对 https 请求UIWebView需要做额外处理,借助NSURLConnection做证书验证,而WKWebView无需做过多额外处理。

    五、缓存 NSDateFormatter

    缓存原因参考苹果官方文档:

    Creating a date formatter is not a cheap operation. If you are likely to use a formatter frequently, it is typically more efficient to cache a single instance than to create and dispose of multiple instances. One approach is to use a static variable.

    六、iOS 9 以后通知不再需要手动移除

    通知 NSNotification 在注册者被回收时需要手动移除,是一直以来的使用准则。原因是在 MRC 时代,通知中心持有的是注册者的 unsafe_unretained 指针,在注册者被回收时若不对通知进行手动移除,则指针指向被回收的内存区域,变为野指针。此时发送通知会造成 crash 。而在 iOS 9 以后,通知中心持有的是注册者的 weak 指针,这时即使不对通知进行手动移除,指针也会在注册者被回收后自动置空。因为向空指针发送消息是不会有问题的。

    七、UIImage 名称为空的警告(符号断点解决)

    [UIImage imageNamed:] 传了 nil 或者传入@"",控制台会输出[framework] CUICatalog: Invalid asset name supplied: '(null)'。通过符号断点可定位。

    八、NSUserDefaults 存储字典的一个坑

    NSDictionary *dict = @{@1: @"1",
                               @2: @"2",
                               @3: @"3",
                               @4: @"4"};
    
    [[NSUserDefaults standardUserDefaults] setObject:dict forKey:@"key"];
    [[NSUserDefaults standardUserDefaults] synchronize];
    

    执行上述代码会报如下错误:

    [User Defaults] Attempt to set a non-property-list object {
        3 = "3";
        2 = "3";
        1 = "1";
        4 = "4";
    } as an NSUserDefaults/CFPreferences value for key `key`
    

    The value parameter can be only property list objects: NSData, NSString, NSNumber, NSDate, NSArray, or NSDictionary. For NSArray and NSDictionary objects, their contents must be property list objects.
    ......
    And although NSDictionary and CFDictionary objects allow their keys to be objects of any type, if the keys are not string objects, the collections are not property-list objects.

    苹果官网有上述这样一段话,能往 NSUserDefaults 里存储的对象只能是 property list objects,包括 NSData,NSString, NSNumber, NSDate, NSArray, NSDictionary,且对于 NSArrayNSDictionary 这两个容器对象,它们所包含的内容也必需是 property list objects。重点看最后一句话,虽然 NSDictionaryCFDictionary 对象的 Key 可以为任何类型(只要遵循 NSCopying 协议即可),但是如果当Key 不为字符串 string 对象时,此时这个字典对象就不能算是property list objects了,所以不能往 NSUserDefaults 中存储,不然就会报错。

    九、performSelector:afterDelay:的坑

    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        dispatch_async(queue, ^{
            NSLog(@"1");
            [self performSelector:self withObject:@selector(test) afterDelay:.0];
            NSLog(@"3");
        });
    
    - (void)test{
        NSLog(@"2");
    }
    

    上述代码的执行结果并非 1 2 3 ,而是 1 3。原因是performSelector: withObject: afterDelay:的本质是往 RunLoop中添加定时器,而子线程默认是没有启动RunLoopperformSelector: withObject: afterDelay:接口虽然和performSelector:系列接口长得很类似。但前者存在于RunLoop相关文件,后者存在于NSObject相关文件。

    performSelector: withObject: afterDelay:接口
    performSelector:系列接口

    十、 @autoreleasepool

    autoreleasepool 使用

    每次遍历的时候生成了很多占内存大的对象,如果交于默认的 autoreleasepool 去管理生命周期,会有因为内存飙升产生crash的风险,遍历过程中,可在适当的位置上去使用@autoreleasepool,一旦出了@autoreleasepool作用域,该作用域内的变量会立马释放。如:

    for(int i = 0; i < 10000; i++){
            @autoreleasepool {   
            }
      }
    

    但并不是所有的遍历方法都要加上@autoreleasepool,比如enumerateObjectsUsingBlock:方法,仔细阅读苹果官方文档,可发现该方法内部已经添加过@autoreleasepool处理。

    autoreleasepool 底层

    autoreleasepool 底层是个C++结构体,创建和销毁的时候分别会调用构造函数和析构函数。

    struct __AtAutoreleasePool {
        __AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
            atautoreleasepoolobj = objc_autoreleasePoolPush();
        }
        ~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
            objc_autoreleasePoolPop(atautoreleasepoolobj);
        }
        void * atautoreleasepoolobj;
     };
    

    系统默认 autoreleasepool

    iOS 中有个默认的autoreleasepool,主线程的 Runloop 中注册了 2 个 Observer:

    • 第1个Observer监听kCFRunLoopEntry事件,会调系统默认autoreleasepool的 objc_autoreleasePoolPush() ;
    • 第2个Observer:
      监听kCFRunLoopBeforeWaiting事件,会调系统默认autoreleasepoolobjc_autoreleasePoolPop()objc_autoreleasePoolPush();
      监听了kCFRunLoopBeforeExit事件,会调系统默认autoreleasepoolobjc_autoreleasePoolPop();

    autorelease和autoreleasepool

    内存管理中调用alloc、new、copy、mutableCopy方法返回对象,在不需要这个对象时,要调用 release 或autorelease 来释放它,MRC 中通常会使用 release 和 autorelease。autorelease 一般是仅用在 MRC 中。

    十一、如何对 NSMutableArray 进行 KVO

    一般情况下只有通过调用 set 方法对值进行改变才会触发 KVO。但是在调用NSMutableArrayaddObjectremoveObject 系列方法时,并不会触发它的 set 方法。所以为了实现NSMutableArray的 KVO,官方为我们提供了如下方法:

    - (NSMutableArray *)mutableArrayValueForKey:(NSString *)key
    

    在增删元素时,使用上述方法来获取要操作的可变数组,然后再执行添加或删除元素的操作,便能实现 KVO 机制。如:

    @property (nonatomic, strong) NSMutableArray *arr;
    
    //添加元素操作
    [[self mutableArrayValueForKey:@"arr"] addObject:item];
    //移除元素操作
    [[self mutableArrayValueForKey:@"arr"] removeObjectAtIndex:0];
    

    十二、被忽略的UIViewController两对API

    如何判断一个页面的viewWillAppear方法是 push 或 present 进来是调用的,还是 pop 或 dismiss 是调用的?一种比较笨拙的方法是通过添加属性标记是进入还是返回调用viewWillAppear方法。还有一种最简单的方法,是直接调用苹果提供的两对 API 。
    针对 Push 和 Pop 或 add childViewController 和 remove childViewController 的 API:

    @property(nonatomic, readonly, getter=isMovingToParentViewController) BOOL movingToParentViewController NS_AVAILABLE_IOS(5_0);
    @property(nonatomic, readonly, getter=isMovingFromParentViewController) BOOL movingFromParentViewController NS_AVAILABLE_IOS(5_0);
    

    针对 Present 和 Dismiss 的 API:

    @property(nonatomic, readonly, getter=isBeingPresented) BOOL beingPresented NS_AVAILABLE_IOS(5_0);
    @property(nonatomic, readonly, getter=isBeingDismissed) BOOL beingDismissed NS_AVAILABLE_IOS(5_0);
    

    十三、抗压缩优先级

    两个水平布局的label,两边间隔分别是12,中间间隔为8(懂意思就行)。如果两个label 都不设置宽度,则左边 label 会拉长,右边 label 自适应。

       UILabel *label1 = [[UILabel alloc] initWithFrame:CGRectZero];
        label1.backgroundColor = [UIColor redColor];
        label1.text = @"我是标题";
        [self.view addSubview:label1];
        [label1 mas_makeConstraints:^(MASConstraintMaker *make) {
            make.centerY.equalTo(self.view);
            make.left.equalTo(@(12));
        }];
        UILabel *label2 = [[UILabel alloc] initWithFrame:CGRectZero];
        label2.backgroundColor = [UIColor redColor];
        label2.text = @"我是描述";
        [self.view addSubview:label2];
        [label2 mas_makeConstraints:^(MASConstraintMaker *make) {
            make.centerY.equalTo(label1);
            make.left.equalTo(label1.mas_right).offset(8);
            make.right.equalTo(self.view).offset(-12);
        }];
    

    如果想让左边 label 自适应,右边 label 拉升,可以设置控件拉升阻力(即抗拉升),拉升阻力越大越不容易被拉升。所以只要 label1 的拉升阻力比 label2 的大就能达到效果。

    //UILayoutPriorityRequired = 1000 
        [label1 setContentHuggingPriority:UILayoutPriorityRequired
                                  forAxis:UILayoutConstraintAxisHorizontal];
    //    //UILayoutPriorityDefaultLow = 250
        [label2 setContentHuggingPriority:UILayoutPriorityDefaultLow
                                  forAxis:UILayoutConstraintAxisHorizontal];
    
    • Content Hugging Priority:拉伸阻力,即抗拉伸。值越大,越不容易被拉伸。
    • Content Compression Resistance Priority:压缩阻力,即抗压缩。值越大,越不容易被压缩。

    十四、约束优先级

    从左到右依次为红、蓝、黄三个视图三等分,蓝色视图布局依赖红色,黄色视图布局依赖蓝色,如果突然将中间的蓝色视图移除,红色和黄色视图的宽度就无法计算。此种情况可以设置最后一个黄色视图的做约束优先级,移除中间蓝色视图后,红色和黄色视图二等分。

     //红  left bottom height
        [redView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.mas_equalTo(self.view.mas_left).with.offset(20);
            make.bottom.mas_equalTo(self.view.mas_bottom).with.offset(-80);
            make.height.equalTo(@50);
        }];
        //蓝   left bottom height       width=红色
        [blueView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.mas_equalTo(redView.mas_right).with.offset(40);
            make.height.width.bottom.mas_equalTo(redView);
        }];
        //黄 left right height           width=红色
        [yellowView mas_makeConstraints:^(MASConstraintMaker *make) {
            make.left.mas_equalTo(blueView.mas_right).with.offset(40);
            make.right.mas_equalTo(self.view.mas_right).with.offset(-20);
            make.height.width.bottom.mas_equalTo(redView);
            //优先级
            //必须添加这个优先级,否则blueView被移除后,redView 和 yellowView 的宽度就不能计算出来
            make.left.mas_equalTo(redView.mas_right).with.offset(20).priority(250);
        }];
        //移除蓝色
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [blueView removeFromSuperview];
            [UIView animateWithDuration:3 animations:^{
                //不加这行代码就直接跳到对应的地方,加这行代码就可以执行动画。
                //另外还要注意调用layoutIfNeeded的对象必须是执行动画的父视图。
                //[blueView.superview layoutIfNeeded];
                [self.view layoutIfNeeded];
            }];
        });
    

    十五、设置代码只在 Debug 下起效

    • 源代码中的测试代码一般可以通过#ifdef DEBUG ... #endif
    • .a 静态库或 .framework 动态库,可以通过设置 Library Search PathsFramework Search Paths,分别移除Release环境对应的路径,Debug环境对应的路径保持不变。
    • 对于 CocoaPods 引入的测试库,可以配置 configurations 选项让对应的库只在 Debug 模式下生效,如:
    pod 'RongCloudIM/IMKit', '~> 2.8.3',:configurations => ['Debug']
    

    十六、为什么会有深拷贝和浅拷贝之分


    上图中观察可知只有不可变 + 不可变组合的时候才出现浅拷贝,其他三种情况都是深拷贝。原因在于,两个不可变对象内容一旦确定都是不可变的,所以不会彼此干扰,为了节省内容空间,两个对象可以指向同一块内存。而其他三种情况,都有可变对象的存在,为了避免两个对象之间的彼此干扰,所有会开辟额外的空间。

    十七、为什么交叉方法出现"死循环"

    因为交换了方法的实现 IMP ,如果alert_replaceInitWithString方法内部调用initWithString会出现真正的死循环。下面代码的死循环只是一个假象。

    @implementation NSAttributedString (Exception)
    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            @autoreleasepool {
                [objc_getClass("NSConcreteAttributedString") swizzleMethod:@selector(initWithString:) swizzledSelector:@selector(alert_replaceInitWithString:)];
            }
        });
    }
    -(instancetype)alert_replaceInitWithString:(NSString*)aString{
        if (!aString) {
            NSString *string = [NSString stringWithFormat:@"[%s:%d行]",[[[NSString stringWithUTF8String:__FILE__] lastPathComponent] UTF8String],__LINE__];
            [[[ExceptionAlert alloc]init]showAlertWithString:string];
            ;
            return nil;
        }
        return [self alert_replaceInitWithString:aString];
    }
    @end
    

    十八、为什么数组下标从零开始

    数组下标最确切的定义应该偏移(offset),如果用 a 来表示数组的首地址,a[0] 就是偏移为 0 的位置,也就是首地址,a[k] 就表示偏移 k 个 type_size 的位置,所以计算 a[k] 的内存地址只需要用这个公式:

    a[k]_address = base_address + k * type_size
    

    但是,如果数组从 1 开始计数,那我们计算数组元素 a[k]的内存地址就会变为:

    a[k]_address = base_address + (k-1)*type_size
    

    对比两个公式,不难发现,从 1 开始编号,每次随机访问数组元素都多了一次减法运算,对于 CPU 来说,就是多了一次减法指令。数组作为非常基础的数据结构,通过下标随机访问数组元素又是其非常基础的编程操作,效率的优化就要尽可能做到极致。所以为了减少一次减法操作,数组选择了从 0 开始编号,而不是从 1 开始。

    十九、copy修饰符引发崩溃问题

    可变数组或字典经过 copy 修饰符修饰后,变成不可变数组或字典,此时再去执行添加或插入元素的时候会发生崩溃。

    二十、为什么量子密码学会有取代传统加密方法的趋势

    传统的加密方式存在两个问题:

    • 如在非对称加密 RSA 体系中存在私钥,只要对方获取到私钥就能破解,为此会有针对私钥泄露相关的吊销证书检测机制。所以只能说相对安全不能说绝对安全。
    • RSA 加密是基于互质关系实现的,不是没有破解的可能,只是需要时间,如果并发机器足够多,时间足够多(几十年或几百年),RSA 加密依然是可以破解的。

    关于互质关系
    如果两个正整数,除了1以外,没有其他公因数,我们就称这两个数是互质关系(coprime)。
    量子密码学是基于量子形态做加解密,如果想破解必须要介入到量子状态中,但是量子传输过程中可监听到监听者的介入。目前量子密码仍处于研究阶段,并没有成熟的应用,量子很容易收到外界的干扰而改变状态。

    二十一、引用计数是怎么管理的

    在arm64架构之前,isa 就是一个普通的指针,存储着Class、Meta-Class对象的内存地址。从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存储更多的信息。 isa 的结构如下:


    • extra_r:里面存储的值是引用计数器减1
    • has_sidetable_rc表示引用计数器是否过大无法存储在isa中,如果为1,那么引用计数会存储在一个叫SideTable的类的属性中。
      SideTable 结构如下,其中 refcnts 是一个存放着对象引用计数的散列表,用当前对象的地址值作为 key ,对象的引用计数作为 Value。

    二十二、weak 原理

    - (void)viewDidLoad {
        [super viewDidLoad];
        __strong Person *person1;
        __weak Person *person2;
        Person *person3;
        NSLog(@"111");
        {
            Person *person = [[MJPerson alloc] init];
            //如果只开启该代码,person在111,222 之后释放,调用dealloc
            //person1 = person;
            //如果只开启该代码,person会在111,222 中间释放
            //person2 = person;
            //person3 = person;
        }
        NSLog(@"222");
    }
    
    @implementation Person
    - (void)dealloc{
        NSLog(@"%s", __func__);
    }
    @end
    

    上述代码如果开启了person1 = person person 会在输出111,222 之后释放,调用dealloc;如果开启了person2 = person person 会在111,222 中间释放;如果开启person3 = person,效果同第一种。

    • __strong是强引用,所以只有离开了viewDidLoad方法后 person 对象才被释放。
    • 所谓的 weak 指针原理是指:如何做到对象被销毁之后,指向对象的 weak 指针立马被清空,置位nil。
    • 默认是强引用。
    weak 原理说明

    一个对象可能会被多次弱引用,当这个对象被销毁时,我们需要找到这个对象的所有弱引用,所以我们需要将这些弱引用的地址(即指针)放在一个容器里(比如数组)。当对象不再被强引用时需要销毁的时候,可以在SideTable中 通过这个对象的地址找到引用值,清空引用值。同时, SideTable结构中还有weak_table_t结构,该结构也是一个散列表,key 为对象地址,value 为一个数组,里面保存着指向该对象的所有弱指针。当对象释放的时候,先清空引用哈希表RefcountMap对应的引用值,遍历弱指针数组,依次将各个弱指针置为 nil。

    二十三、加盐的意义

    用户设置的密码复杂度可能不够高,同时不同的用户极有可能会使用相同的密码,那么这些用户对应的密文也会相同,这样,当存储用户密码的数据库泄露后,攻击者会很容易便能找到相同密码的用户,从而也降低了破解密码的难度,因此,在对用户密码进行加密时,需要考虑对密码进行掩饰,即使是相同的密码,也应该要保存为不同的密文,即使用户输入的是弱密码,也需要考虑进行增强,从而增加密码被攻破的难度,而使用带盐的加密hash值便能满足该需求。笔者实际项目开发中,为了网络安全,请求参数按照一定的规则拼接成字符串,然后在字符串中加盐,最后 MD5 签名。后端依照同样的规则校验签名,若签名值一致则通过校验。

    二十四、Shell 脚本

    请点击此链接

    二十五、什么是User Agent

    User Agent中文名为用户代理,简称 UA,它是一个特殊字符串头,使得服务器能够识别客户使用的操作系统及版本、CPU 类型、浏览器及版本、浏览器渲染引擎、浏览器语言、浏览器插件等。网站在手机端 app 打开和直接在浏览器中打开看到的内容可能不一样,是因为网页可以根据 UA 判断是 app 打开的还是浏览器打开的。

    navigator 可以获取到浏览器的信息:navigator.userAgent。webView中获取 User Agent 方式如下:

    +(void)initialize{
        if ([NSThread isMainThread]) {
            [self getUserAgent];
        }else{
            dispatch_async(dispatch_get_main_queue(), ^{
                [self getUserAgent];
            });
        }
    }
    +(void)getUserAgent{
        UIWebView *webView = [[UIWebView alloc]initWithFrame:CGRectZero];
        NSString *userAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
        NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:[NSString stringWithFormat:@"%@meicaiMallIOS",userAgent],@"UserAgent",nil];
        [[NSUserDefaults standardUserDefaults] registerDefaults:dict];
    }
    

    二十六、JS和OC通信方式汇总

    JS 调 OC

    JS 调 OC ⽬目前主要的⽅方式有两种:

    • 通过 JSCore 中的 block
    • 通过 JSCore 中的 JSExport

    在 JS 执⾏行行环境中添加⼀一个 _OC_catch 的 block,那么在 JS 代码中就可以直接调⽤用 _OC_catch 这 个函数,当在 JS 中调⽤用 _OC_catch 这个函数后,我们刚才注册的 block 就会被执行。也就是通过 JS 成功的调⽤了 OC 代码。

    context[@"_OC_catch"] = ^(JSValue *msg, JSValue *stack) {
     
    }; 
    

    JSExport 可以导出 Objective-C 的属性、实例方法、类方法和初始化⽅方法到 JS 环境,这样就可 以通过 JS 代码直接调⽤用 Objective-C 。通过 JSExport 不仅可以导出⾃自定义类的方法、属性,也可以导出已有类的⽅方法、属性。在导出过程中,类的方法名会被转换成 JS 类型命名,第二个参数的第一个字⺟会被大写,比如- (void)addX:(int)x andY:(int)y;被转为addXAndY(x, y)。除此,JSExport还可以导出已有类的⽅方法、属性。

    OC 调 JS

    OC 调 JS 主要有 UIWebView 、WKWebView 和 JSCore 这三种⽅方式。⽽ UIWebView 的方式其实可以看作是 JSCore 的⽅方式。

    • JSCore 方式
    // 要执行的 JS 代码,定义一个 add 函数并执⾏行行
    NSString *addjs = @"function add(a, b) {return a + b;};add(1,3)";
    // sumValue 为执⾏行行后的结果
    JSValue *sumValue = [self.context evaluateScript:addjs];
    
    • UIWebView 方式
      这种⽅方式说⽩白了了就是使⽤用 JSCore ,通过 UIWebView 来获取 JSContext ,这样直接通过获取到 context 来执⾏行行 JS 代码。
    //通过 UIWebView 获取 context
    JSContext *context = [_webView
    valueForKeyPath:@"documentView.webView.mainFrame.JSContext"]; 
    // 要执行的 JS 代码,定义一个 add 函数并执⾏行行
    NSString *addjs = @"function add(a, b) {return a + b;};add(1,3)";
    // sumValue 为执⾏行行后的结果
    JSValue *sumValue = [self.context evaluateScript:addjs];
    
    • WKWebView 方式
      WKWebView 没有提供获取 JSContext 的方法,但是它提供了执行 JS 的方法 evaluateJS: ,通过下面方法来执行 JS 代码。
    [self.webView evaluateJS:@"function add(a, b) {return a + b;};add(1,3)" completionHandler:^(id _Nullable msg, NSError * _Nullable error) {  
        NSLog(@"evaluateJS add: %@, error: %@", msg, error);
    }]; 
    

    二十七、UIScrollView 原理

    UIScrollView继承自UIView,内部有一个 UIPanGestureRecongnizer手势。 frame 是相对父视图坐标系来决定自己的位置和大小,而bounds是相对于自身坐标系的位置和尺寸的。改视图 boundsorigin 视图本身没有发生变化,但是它的子视图的位置却发生了变化,因为 boundsorigin 值是基于自身的坐标系,当自身坐标系的位置被改变了,里面的子视图肯定得变化, boundspanGestureRecognize 是实现 UIScrollView 滑动效果的关键技术点。

    二十八、--verbose 和 --no-repo-update

    • verbose意思为 冗长的、啰嗦的,一般在程序中表示详细信息。此参数可以显示命令执行过程中都发生了什么。
    • pod installpod update可能会卡在Analyzing dependencies步骤,因为这两个命令会升级 CocoaPods 的 spec 仓库,追加该参数可以省略此步骤,命令执行速度会提升。

    二十九、dataSource 和 delegate 的本质区别

    普遍开发者得理解是:一个是数据,一个是操作。如果从数据传递方向的角度来看,两者的本质是数据传递的方向不同。dataSource 是外部将数据传递到视图内,而 delegate 是将视图内的数据和操作等传递到外部。实际开发封装自定义视图,可以参照数据传递方向分别设置 dataSourcedelegate

    三十、变种 MVC

    真正的 MVC 应该是苹果提供的经典UITableView的使用,实际开发中经常在 Cell 中引入Model,本质上来说不算是真正的 MVC ,只能算是 MVC 的变种。

    三十一、函数指针和 Block

    相同点:

    • 二者都可以看成是一个代码片段。
    • 函数指针类型和 Block 类型都可以作为变量和函数参数的类型(typedef定义别名之后,这个别名就是一个类型)。

    不同点:

    • 函数指针只能指向预先定义好的函数代码块,函数地址是在编译链接时就已经确定好的。从内存的角度看,函数指针只不过是指向代码区的一段可执行代码,而 block 本质是 OC对象,是 NSObject的子类,是程序运行过程中在栈内存动态创建的对象,可以向其发送copy消息将block对象拷贝到堆内存,以延长其生命周期。
    补充:指针函数和函数指针的区别

    指针函数是指带指针的函数,即本质是一个函数,函数返回类型是某一类型的指针。它是一个函数,只不过这个函数的返回值是一个地址值。

    int *f(x,y);
    

    函数指针是指向函数的指针变量,即本质是一个指针变量。

    int (*f) (int x); /*声明一个函数指针 */
    f = func; /* 将func函数的首地址赋给指针f */
    

    三十二、内存(堆内存)回收是什么意思

    NSObject *obj = [[NSObject alloc] init];
    

    代码对应的内存布局如下,obj 指针存在于栈取,obj 对象存在于堆区。obj 指针的回收由栈区自动管理,堆区的内存需要开发者自己管理(MRC)情况。所谓的堆内存回收并不是指将 obj 对象占有的内存给挖去或是将空间数据清空为0,而是指 obj 对象原本占有的空间可以被其他人利用(即其他指针可以指向该空间)。其他指针指向该空间时,重新初始化该空间,将空间原有数据清零。

    三十三、IP 和 MAC

    IP 是地址,有定位功能;MAC 是身份唯一标识,无定位功能;有了 MAC 地址为什么还要有 IP 地址?举个例子,现在我要和你通信(写信给你),地址用你的身份证号,信能送到你手上吗? 明显不能!身份证号前六位能定位你出生的县,MAC 地址前几位也可以定位生产厂家。但是你出生后会离开这个县(IP 地址变动),哪怕你还在这个县,我总不能满大街喊着你的身份证号去问路边人是否认识这个身份证号的主人,所以此刻需要借助 IP 的定位功能。

    三十四、MD5 相关小知识

    具体可参考笔者之前文章 iOS 签名机制,文章中可以找到答案。

    三十五、响应链问题

    这是文章前言的问题,请参考 这篇文章

    三十六、什么是线程不安全?线程不安全的本质原因?

    不能确定代码的运行顺序和结果,是线程不安全的。线程安全是相对于多线程而言的,单线程不会存在线程安全问题。因为单线程代码的执行顺序是唯一确定的,进而可以确定代码的执行结果。

    线程不安全的本质原因在于:表面展现在我们眼前的可能是一行代码,但转换成汇编代码后可能对应多行。当多个线程同时去访问代码资源时,代码的执行逻辑就会发生混乱。如数据的写操作,底层实现可能是先读取,再在原有数据的基础上改动。如果此时有一个读操作,原本意图是想在写操作完毕之后再读取数据,但不巧的这个读操作刚好发生在写操作执行的中间步骤中。虽然读操作后与写操作执行,但数据读取的值并不是写操作的结果值,运气不好时还可能发生崩溃。

    - (void)viewDidLoad {
        [super viewDidLoad];
        int a = 100;
        a += 200;
        NSLog(@"%d",a);
    }
    

    如上述代码中的int a = 100;a += 200;转换的汇编代码,为下面中间八行汇编代码。

    0x1098e7621 <+49>: callq  0x1098e7a32               ; symbol stub for: objc_msgSendSuper2
        0x1098e7626 <+54>: leaq   0x1a33(%rip), %rax        ; @"%d"
        0x1098e762d <+61>: movl   $0x64, -0x24(%rbp)
        0x1098e7634 <+68>: movl   -0x24(%rbp), %ecx
        0x1098e7637 <+71>: addl   $0xc8, %ecx
        0x1098e763d <+77>: movl   %ecx, -0x24(%rbp)
    ->  0x1098e7640 <+80>: movl   -0x24(%rbp), %esi
        0x1098e7643 <+83>: movq   %rax, %rdi
        0x1098e7646 <+86>: movb   $0x0, %al
        0x1098e7648 <+88>: callq  0x1098e7a14               ; symbol stub for: NSLog
    

    三十七、App 启动流程

    APP 启动分为冷启动和热启动,这里主要说下冷启动过程。冷启动分为三阶段: dyld 阶段、runtime阶段、main函数阶段,一般启动时间的优化也是从这三大步着手。

    • dyld阶段:dyld(dynamic link editor)是Apple的动态链接器,可以用来装载 Mach-O 文件(可执行文件、动态库等)。启动APP时,dyld 首先装载可执行文件,同时会递归加载所有依赖的动态库。
    • runtime 阶段:首先解析可执行文件,之后调用所有类和分类的+load方法,并进行各种objc结构的初始化(注册Objc类 、初始化类对象等等)。到此为止,可执行文件和动态库中所有的符号(Class、Protocol、Selector、IMP …)都已经按格式成功加载到内存中,被runtime 所管理。
    • main函数阶段:所有初始化工作结束后,dyld就会调用main函数。

    三十八、包体积优化中的内联函数

    在关于 App 包体积优化的一些博客文章中,偶尔看到包体积的优化可以从 C++ 入手,其中有一条是减少内联函数的使用。问题来了,什么是内联函数?为什么要减少内联函数的使用?它和一般函数有什么异同点?和宏相比有什么异同点?

    内联函数关键字是 inline ,C++ 中普通函数使用的申明或实现使用inline 修饰后,即为内联函数。注意:递归函数即使被 inline 修饰后也不是内联函数,依然是普通函数。

    inline int sum(int a, int b){
        return a + b;
    }
    

    普通函数调用会开辟一段栈空间执行相关代码,函数执行完再将对应的栈空间回收。而内联函数调用中,编译器会将函数调用直接展开为函数代码。如cout << sum(1, 2) << endl会直接转换为cout << 1 + 2<< endl,由此可见内联函数和一般的宏很类似,都是直接替换相关代码。同宏相比,内联函数只是多了一些函数特性和语法检测功能。

    综上,内联函数或宏可以减少函数调用的开销,但是会增加代码体积,所以减少内联函数或宏的使用一定程度上可以减少包体积。但并不是说为了减小包体积完全不去使用内联函数,建议经常会被调用的代码,且代码量不是很多的时候(不超过10行),为减少函数调用的开销,可适当使用内联函数。

    三十九、super 本质

    有两个类 Animal 和 Cat ,其中 Cat 继承自 Animal 类,在 Cat 类实现如下代码,试问打印结果是什么?

    @implementation Cat
    - (instancetype)init{
        self = [super init];
        if (self) {
            NSLog(@"%@",[self class]);//Cat
            NSLog(@"%@",[self superclass]);//Animal
            NSLog(@"%@",[super class]);//Cat
            NSLog(@"%@",[super superclass]);//Animal
        }
        return self;
    }
    @end
    

    上述代码打印结果一次为: Cat Animal Cat Animal,前两个结果不足为奇,后两个结果似乎有点费解。

    super 调用底层会转换为objc_msgSendSuper函数的调用,objc_msgSendSuper 函数接收 2 个参数 objc_super 结构体和 SELobjc_super结构如下:

    struct objc_super {
        __unsafe_unretained _Nonnull id receiver; // 消息接收者
        __unsafe_unretained _Nonnull Class super_class; // 消息接收者的父类
    };
    

    [super class] 在调用过程中,底层转化为 objc_msgSendSuper({self, [Animal class]}, @selector(class)); ,同 objc_msgSend 函数相比相当于多了第二个参数,但消息接收者仍然是 self ,所以打印结果为 Cat

    the superclass at which to start searching for the method implementation.

    objc_msgSendSuper 方法中的第二个参数主要作用是告诉从哪里开始搜索方法实现,一般传入的是父类。这也是实际开发中 [super superClassMethod] 直接调用父类方法的原因。

    四十、引用的本质(引用和指针的区别)

    待更新。。。。。。

    四十一、渲染框架分类

    待更新。。。。。。

    四十二、NSProxy & NSObject

    待更新。。。。。。

    四十三、如何给百万数据排序

    待更新。。。。。。

    四十四、自旋锁 & 互斥锁

    待更新。。。。。。

    四十五、应用 Crash 时系统做了什么操作

    待更新。。。。。。

    相关文章

      网友评论

        本文标题:开发小知识

        本文链接:https://www.haomeiwen.com/subject/dfjjgxtx.html