开发小知识(一)

开发小知识(一)

开发小知识(二)

前言和目录

该文章主要整理一些小知识点,主要涉及 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:系列接口

[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

如果在子线程中添加上述两行代码,启动RunLoop, 则代码逻辑可以正常执行。

         NSLog(@"1");
        [self performSelector:self withObject:@selector(test) afterDelay:.0];
        NSLog(@"3");

如果上述代码放在主线程,是可以正常执行的。因为主线程默认开启了 RunLoop。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");
//        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }];
    [thread start];
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}

同之前的分析比较,这里同样是在子线程 thread 上执行 performSelector: withObject: afterDelay: 方法,因为子线程没有开启 RunLoop,这里应该只输出 1。 但实际上会在输出 1 之后崩溃,原因在于执行完 thread 的block后,thread 会被释放。打开注释开启子线程 thread 的 RunLoop,代码可正常执行。

十、 @autoreleasepool

autoreleasepool 使用

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

for(int i = 0; i < 10000; i++){
        @autoreleasepool {   
            Person *p = [[Person alloc]init];
        }
  }

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

autoreleasepool 底层

自动释放池的主要底层数据结构是:__AtAutoreleasePool 和 AutoreleasePoolPage。

  • __AtAutoreleasePool : autoreleasepool 底层是个C++结构体__AtAutoreleasePool,创建和销毁的时候分别会调用构造函数和析构函数。即进入@autoreleasepool{}执行objc_autoreleasePoolPush ,出了@autoreleasepool{}执行objc_autoreleasePoolPop
struct __AtAutoreleasePool {
    __AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
    ~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    void * atautoreleasepoolobj;
 };
  • AutoreleasePoolPage: 调用了autorelease的对象最终由AutoreleasePoolPage 对象来管理。如 MRC 下Person *p = [[[Person alloc]init]autorelease]。每一个AutoreleasePoolPage对象占用4094字节内存,本身成员占用56字节,剩下的空间用来存放 autorelease对象 的地址和 POOL_BOUNDARY。所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。另外,每个 AutoreleasePoolPage 有个 thread ,说明 autoreleasepool 和线程是一一对应的。如下图:
  • POOL_BOUNDARY: 进入@autoreleasepool{}执行objc_autoreleasePoolPush后 ,会往 AutoreleasePoolPage 添加 POOL_BOUNDARY,并将 POOL_BOUNDARY 的内存地址作为 objc_autoreleasePoolPush的返回值记录下来;当出了@autoreleasepool{}时执行objc_autoreleasePoolPop,并将之前记录的 POOL_BOUNDARY 地址作为 objc_autoreleasePoolPop的参数,objc_autoreleasePoolPop内部会依次调用 autorelease对象 的 release 方法销毁对象,直到遇到 POOL_BOUNDARY 内存地址为止。

  • 双向链表:上述描述先进后出,实际上是栈的结构。每个AutoreleasePoolPage 的内存空间是连续的,理论上可以当做栈的形式处理,但是单个 AutoreleasePoolPage 容量有限, 所以需要借助链表结构去连接多个 AutoreleasePoolPage 扩容。之所以要使用双向链表,是因为当执行objc_autoreleasePoolPop时,POOL_BOUNDARY 可能在上一个 AutoreleasePoolPage 中,此时需要找到之前的 AutoreleasePoolPage,并释放掉中间的 autorelease 对象。

系统默认 autoreleasepool 和 RunLoop 的关系

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [[[Person alloc] init] autorelease];
    NSLog(@"%s",__FUNCTION__);
}
- (void)viewWillAppear{
    NSLog(@"%s",__FUNCTION__);
}
-(void)viewDidAppear{
    NSLog(@"%s",__FUNCTION__);
}

上述代码, MRC 下 Person 对象会在执行完viewDidLoadviewWillAppear 方法之后再释放,主要是和 Runloop 有关。iOS 中有个默认的autoreleasepool,主线程的 Runloop 中注册了 2 个 Observer:

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

上述代码执行结果说明了: viewDidLoadviewWillAppear在同一个运行周期内。

autorelease 和 release

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

autorelease 对象在什么时候释放 ?

分两种情况

  • main 函数自带的 autoReleasePool 内: 此种情况下,和 RunLoop 有关。
  • 手动创建的 autoReleasePool:此种情况下,出了 autoReleasePool 之后,autorelease 对象会依次释放。

ARC 下,方法里的局部对象什么时候释放?

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [[Person alloc] init];
}

上述代码如果 ARC 最终转换成Person *p = [[[Person alloc] init] autorelease];则该对象的释放和 RunLoop 有关;如果生成如下代码,则出了方法内部该对象会立马释放,实际验证中是出了方法立马释放。

- (void)viewDidLoad {
    [super viewDidLoad];
    Person *p = [[Person alloc] init];
    [p release];
}

十一、如何对 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 开始。同理 OC 中的 objc_msgSend 是直接基于汇编实现的,直接抛开 C 或 C++ 层面的代码调用,极可能的提升代码执行效率。

十九、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,person1 调用 person 的set  方法进行了retain 操作,所以 person 的生命周期同 person1。
        //person1 = person;

        //========第二种情况========
        //如果只开启该代码,person会在111,222 中间释放。此时 person2 没有强引用(retain) person。
//所谓的 weak 指针原理是指:如何做到对象(person)被销毁之后,指向对象的 weak 指针(person2)立马被清空,置位 nil。
        //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 指针原理是指:如何做到对象(person)被销毁之后(出了上述代码中内嵌的{ }之后), 指向对象的 weak 指针(person2)立马被清空,并被置位 nil
  • 默认是强引用。
weak 原理说明
image.png
  • weak_table 是一个散列表,key 为对象地址,value 为一个数组,数组里面保存着指向该对象的所有弱指针。
  • refcnts是一个存放着对象引用计数的散列表。

一个对象可能会被多次弱引用,当这个对象被销毁时,我们需要找到这个对象的所有弱引用,所以我们需要将这些弱引用的地址(即指针)放在一个容器里(比如数组)。当对象不再被强引用时需要销毁的时候,可以在 SideTable 中通过这个对象的地址找到引用值,首先清空引用值。同时, SideTable结构中还有weak_table,该结构也是一个散列表,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
  • 通过拦截 URL
    在 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还可以导出已有类的⽅方法、属性。

通过拦截 URL,这种方式是 Web 端通过某种方式发送 URLScheme 请求,之后 Native 拦截到请求并根据URL SCHEME(包括所带的参数)进行相关操作。类似于通过 SCHEME 唤起APP。这种方式的缺点是 url 长度有隐患,并且创建请求需要一定的耗时,比注入 API 的方式调用同样的功能。耗时会比较长。所以还是更推荐使用注入 API 的方式。

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 滑动效果的关键技术点。

frame和bounds对比:
参考

  • frame很简单,它的x、y就是以当前视图的父视图为参照确定当前视图的位置
  • bounds的x、y则是当前视图的坐标,并不影响当前视图的位置,但是对当前视图的子视图有影响。当前视图的坐标系统原点0,0默认为左上角,当更改了bounds.origin 坐标系统原点也会对应的被改变,由于当前试图的子视图都是参照当前视图的原点进行布局,当坐标系统原点位置改变,其子视图位置也会发生变化。

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

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

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

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

三十、变种 MVC

真正的 MVC 应该是苹果提供的经典UITableView的使用,实际开发中经常在 Cell 中引入Model,本质上来说不算是真正的 MVC ,只能算是 MVC 的变种。真正的 MVC 中 View 和 Model 应该是完全隔离的。苹果的 MVC 中正是因为 View 没有和任何 Model 绑定,所以 cell 的可冲拥堵高,但是缺点是代码过于臃肿。

苹果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 签名机制,文章中可以找到答案。

三十五、响应链问题

命中测试和响应链问题
手势、UIControl、UITouch系列事件关系

过程:

触屏事件的处理被分成两个阶段:查找响应者(a)和响应者处理(b、c、d)。

  • a.先将事件由上向下(从父控件向子控件)传递,找到最合适处理事件的控件。如果是同一级别的视图,先调用后添加的视图 hitTest,再调用先添加视图的 hitTest。寻找最合适的视图之所以从 Window 开始,是因为界面渲染本身是图层树的结构,遍历从树的根节点开始,才可以获取到各个子节点信息。
  • b.调用最合适控件的 touches 系列方法。
  • c.如果调用 [super touches] 方法,就会将事件顺着响应链条向上传递,传递给上一个响应者。
  • d.接着调用上一个响应者的touches方法
hitTest 内部实现代码还原
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    NSLog(@"-----%@",self.nextResponder.class);
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) return nil;
    //判断点在不在这个视图里
    if ([self pointInside:point withEvent:event]) {
        //在这个视图 遍历该视图的子视图
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            //转换坐标到子视图
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            //递归调用hitTest:withEvent继续判断
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                //在这里打印self.class可以看到递归返回的顺序。
                return hitTestView;
            }
        }
        //这里就是该视图没有子视图了 点在该视图中,所以直接返回本身,上面的hitTestView就是这个。
        NSLog(@"命中的view:%@",self.class);
        return self;
    }
    //不在这个视图直接返回nil
    return nil;
}
响应链是什么?

其实响应链就是在命中测试中,走通的路径。用上个章节的例子,整个命中测试的走向是:A✅ --> D❎ --> B✅ --> C❎,我们把没走通的❎的去掉,以第一响应者 B 作为头,依次连接,响应链就是:B -> A。(实际上 A 后面还有控制器等,但在该例子中没有展示控制器等,所以就写到 A)

默认来说,若该结点是 UIView 类型的话,这个 next 属性是该结点的父视图。但也有几个例外:

  • 如果是 UIViewController 的根视图,则下一个响应者是 UIViewController。
  • 如果是 UIViewController:
    如果 UIViewController 的视图是 UIWindow 的根视图,则下一个响应者是 UIWindow 对象;如果 UIViewController 是由另一个 UIViewController 呈现的,则下一个响应者是第二个 UIViewController。
  • UIWindow的下一个响应者是 UIApplication。
  • UIApplication 的下一个响应者是 app delegate。但仅当该 app delegate 是 UIResponder 的实例且不是 UIView、UIViewController 或 app 对象本身时,才是下一个响应者。

下面举个例子来说明。如下图所示,触摸点是,那根据命中测试,B 就成为了第一响应者。由于 C 是 B 的父视图、A 是 C 的父视图、同时 A 是 Controller 的根视图,那么按照规则,响应链就是这样的:视图 B -> 视图 C -> 根视图 A -> UIViewController 对象 -> UIWindow 对象 -> UIApplication 对象 -> App Delegate

获取到响应链后,触摸事件首先将会由第一响应者响应,首先触发第一响应者的touchBegin 方法。

应用1:子View超出父View的情况,子 View 依旧能响应事件。

重载父 view 的 -(UIView *)hitTest: withEvent: 方法,去掉点击必须在父 view 内的坐标判断逻辑,子 view 就能成为最合适的视图,用于响应事件了。注意内部的坐标转化:判断点击的点是包含子视图,如果包含子视图,则调用子视图的 hitTest 方法。

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
        return nil;
    }
    /**
     *  此注释掉的方法用来判断点击是否在父View Bounds内,
     *  如果不在父view内,就会直接不会去其子View中寻找HitTestView,return 返回
     */
//    if ([self pointInside:point withEvent:event]) {
        for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
            CGPoint convertedPoint = [subview convertPoint:point fromView:self];
            UIView *hitTestView = [subview hitTest:convertedPoint withEvent:event];
            if (hitTestView) {
                return hitTestView;
            }
        }
        return self;
//    }
    return nil;
}
应用2:穿透子 View 点击父 View。

子View覆盖在父View上,但是要实现穿透子View去响应父View点击事件。解决方法时,重写子 View 的 hitTest 方法。点击的是自身则返回nil, 此时最合适响应者转为父类。

-(UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    UIView *hitView =[super hitTest:point withEvent:event];
    if(hitView == self){
        //自动将事件传递到上一层(父视图),自身不做事件处理
        return nil;
    }
    return hitView;   
}
应用3:如何实现点击子视图,父视图和子视图同时响应?

方法一:子视图重写以下 touch 系列方法,通过 self.nextResponder 找到父视图,并调用父视图的 touch 系列方法。

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{   
    [self.nextResponder touchesBegan:touches withEvent:event];
    [super touchesBegan:touches withEvent:event];
}
 
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self.nextResponder touchesMoved:touches withEvent:event];
    [super touchesMoved:touches withEvent:event];
}
 
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self.nextResponder touchesEnded:touches withEvent:event];
    [super touchesEnded:touches withEvent:event];
}
 
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self.nextResponder touchesCancelled:touches withEvent:event];
    [super touchesCancelled:touches withEvent:event];
}

方法二: 如果子视图和父视图的收拾都是 UIGestureRecognizer 相关方法,也可以通过- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer; 方法,同时响应父视图和子视图的事件。此方法返回YES,手势事件会一直往下传递,不论当前层次是否对该事件进行响应。

应用4:扩大 button 热区

重载UIButton的-(BOOL)pointInside: withEvent:方法,让Point即使落在Button的Frame外围也返回YES。

- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event {
    return CGRectContainsPoint(HitTestingBounds(self.bounds, self.minimumHitTestWidth, self.minimumHitTestHeight), point);
}

CGRect HitTestingBounds(CGRect bounds, CGFloat minimumHitTestWidth, CGFloat minimumHitTestHeight) {
    CGRect hitTestingBounds = bounds;
    if (minimumHitTestWidth > bounds.size.width) {
        hitTestingBounds.size.width = minimumHitTestWidth;
        hitTestingBounds.origin.x -= (hitTestingBounds.size.width - bounds.size.width)/2;
    }
    if (minimumHitTestHeight > bounds.size.height) {
        hitTestingBounds.size.height = minimumHitTestHeight;
        hitTestingBounds.origin.y -= (hitTestingBounds.size.height - bounds.size.height)/2;
    }
    return hitTestingBounds;
}

参考一
参考二
参考三

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

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

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

- (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 阶段:首先解析可执行文件,进行各种objc结构的初始化(注册Objc类 、初始化类对象等等)。之后调用所有类和分类的+load方法,attribute((constructor)) 修饰的函数的调用、创建 C++ 静态全局变量。到此为止,可执行文件和动态库中所有的符号(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,由此可见内联函数和一般的宏很类似,都是直接替换相关代码。同宏相比,内联函数只是多了一些函数特性和语法检测功能。

OC 中可以通过关键字 NS_INLINE 使用内联函数。

NS_INLINE void log(int value) {
    NSLog(@"%d", value);
}

综上,内联函数或宏省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,可以减少函数调用的开销。但是会增加代码体积,所以减少内联函数或宏的使用一定程度上可以减少包体积。但并不是说为了减小包体积完全不去使用内联函数,建议经常会被调用的代码,且代码量不是很多的时候(不超过10行),为减少函数调用的开销,可适当使用内联函数。

参考

三十九、super 本质

objc_msgSend 方法参数

LGPerson *person = [LGPerson alloc];
[person sayHello];
//2.消息发送
objc_msgSend(person, sel_registerName("sayHello"));

有两个类 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] 直接调用父类方法的原因。但是如果按照这一点来看,依然和上述打印结果不符合。因此需要看 class 和 superClass 方法的内部实现,class 方法的内部实现返回消息接收者,上述代码消息接收者为 Cat ,因此打印结果仍然是 Cat。superClass 方法内部实现是返回消息接受者的父类,因此打印结果是 Animal。

@interface Person : NSObject
- (void)test;
@end
@implementation Person
- (void)test{
    NSLog(@"person: %@,%@,%@,%@",[self class],[self superclass],[super class],[super superclass]);
}
@end
@interface Student : Person
- (void)test;
@end
@implementation Student
- (void)test{
    NSLog(@"student:%@,%@,%@,%@",[self class],[self superclass],[super class],[super superclass]);
    [super test];
}
@end

调用 Student 的 test 方法,最终打印结果都是 Student,Person,Student,Person。因为实例对象一直是 Student 对应的实例对象,并非是 Person 的实例对象。

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

待更新。。。。。。

四十一、渲染框架分类

说实在的有时会对各种渲染框架感觉混乱,一会CA、一会CG等等,于是就把这些渲染框架简单汇总了下。

  • 1、UIKit & AppKit :这个不多说。
  • 2、Core Animation:UIView底下封装了一层CALayer树,Core Animation 层是真正的渲染层,我们之所以能在屏幕上看到内容,真正的渲染工作是在 Core Animation 层的。
  • 3、Core Graphics:用于运行时绘制图像。可以绘制路径、颜色,当开发者需要在运行时创建图像时,可以使用 Core Graphics 去绘制。
  • 4、 Core Image:用来处理已经创建的图像。该框架拥有一系列现成的图像过滤器,能对已存在的图像进行高效的处理。
  • 5、SceneKit & SpriteKit:普通开发者可能会对 SceneKit 和 SpriteKit 感到陌生,SceneKit 主要用于某些 3D 场景需求,而 SpriteKit 更多的用于游戏开发。SceneKit 和 SpriteKit 都包含了粒子系统、物理引擎等,即使是非游戏应用中也可以使用它们来完成一些比较炫酷的特效和物理模拟。
  • 6、Metal:Metal 存在于以上渲染框架的最底层。Core Animation、Core Image、SceneKit、SpriteKit等等渲染框架都是构建于 Metal 之上的。

四十二、耗时代码定位

实际开发中可能会遇到严重线程阻塞的情况,比如笔者之前就遇到过使用 MJ 下拉刷新,刷新完毕后 MJ 复位无动画效果,第一猜测就是有阻塞,于是借助 Product --> Profile-->TimeProfiler 工具 第一时间定位到耗时较多的代码。结果发现在渲染 Cell 的时候动态的调用了苹果接口中 html 转属性文本的方法,该方法的解析异常耗时。可按照下图设置 Call Tree ,方便定位耗时代码。

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

桶排序定义

给百万数据排序可以用"桶排序",核心思想是将数据分到几个有序的桶,每个桶里的数据再单独进行排序。桶内排完序之后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。

桶排序时间复杂度

如果要排序的数据为 n 个,均匀地划分到 m 个桶内,每个桶里就有 k = n/m 个元素。每个桶内部使用快速排序,则每个桶内时间复杂度为 O(k * logk)。m 个桶排序的时间复是 O(m * k * logk),因为 k = n/m,所以整个桶排序的时间复杂度就是 O(n*log(n/m))。当桶的个数 m 接近数据个数 n 时,log(n/m) 就是非常小的常量,这个时候桶排序的时间复杂度接近 O(n)

桶排序缺点

桶排序对要排序数据的要求是非常苛刻的。

  • 1、桶与桶之间有着大小顺序。这样每个桶内的数据都排序完之后,桶与桶之间的数据不需要再进行排序。
  • 2、数据在各个桶之间的分布是比较均匀的。如果数据经过桶的划分之后,有些桶里的数据非常多,有些非常少,很不平均。时间复杂度就不是常量级了。极端情况下,如果数据都被划分到一个桶里,那就退化为 O(nlogn) 的排序算法了。
内存不足时,如何排序?

假设有 10GB 的订单数据需要排序,内存有限,只有几百 MB,没办法一次性把 10GB 的数据都加载到内存中。先扫秒订单知道金额最小是 1 元,最大是 10 万元。可以将订单划到100个桶内,第一个桶我们存储金额在 1 元到 1000 元之内的订单,第二桶存储金额在 1001 元到 2000 元之内的订单,以此类推。但是订单的数据分布可能并不是非常均匀,某些桶内的数据依然是大于内存空间,此时可以将该桶内的数据再次进行划分,直到能加载到内存为止。

四十四、自旋锁 & 互斥锁

线程安全中为了实现线程阻塞,一般有两种方案:一种是让线程处于休眠状态,此时不会消耗 CPU 资源;另一种方案是让线程忙等或空转,此时会消耗一定的 CPU 资源。前者属于互斥,后者属于自旋。

自旋锁

自旋锁是一种特殊的互斥锁,自旋在线程加锁的情况下,会一直尝试是否解锁,如果没有解锁,会一直循环判断,如果锁已经放开,则继续执行,不再是空转状态。
优点

  • 循环检查资源持有者是否已经释放了资源,这样做的好处是减少了线程从睡眠到唤醒的资源消耗,但会一直占用CPU的资源。

缺点:

  • OSSpinLock 属于自旋锁,Pthred 库中相关的锁,以及 NSLock@synchronized 等都属于互斥锁。OSSpinLock目前已经不再安全,因为会出现优先级反转问题。 现代操作系统一般采用 时间片轮转算法 调度进程或线程,按照线程的优先级为不同的线程分配不同的时间,优先级越高分配的时间片越多。假设有两个线程 thread1 和 thread2,其中 thread1 的优先级高于 thread2,即thread1 分配的时间片多余 thread2。如果 thread2 正在锁内安全执行,一段时间后 thread1 执行任务时,发现锁未打开,于是会处于忙等状态。由于thread1 的优先级高于 thread2,此时系统会分配更多的时间片给 thread1,thread2 时间片减少,迟迟不能完成,thread1 却一直等待。如此就造成线程优先级反转。
  • 由于一直忙等,所以忙等的过程会消耗 CPU 资源。
自旋锁和互斥锁适用场景:

什么时候用自旋锁比较划算

  • 预计线程等待锁的时间很短 。如YYCache 中的内存缓存
  • 加锁的代码(加锁部分的代码也称为临界区)经常被调用,但竞争情况很少发生(如果竞争比较多自旋锁可能会发生优先级反转问题)。
  • CPU资源不紧张(如果 CPU 资源比较紧张,再加上自旋锁会一直占用 CPU 资源,会带来更差的体验)
  • 多核处理器

什么时候用互斥锁比较划算

  • 预计线程等待锁的时间较长
  • 单核处理器(为单核处理器时要避免占用更多的CPU资源,避免CPU空转轮询 )
  • 临界区有IO操作(因为 IO 操作比较占用资源,所以要使用占用资源更少的互斥锁)
  • 临界区代码复杂或者循环量大
  • 临界区竞争非常激烈(使用互斥锁可以减少资源占用)

四十五、应用 Crash 时为什么对操作系统无影响?

双模式、I/O 保护和内存保护、定时器三者是确保操作系统能够运行的关键技术,可以避免外界应用崩溃对操作系统的影响。

  • 双模式
    为了保证操作系统不受其它故障程序的影响,进而产生系统崩溃的可能。一种常用的办法是引入双重模式,即用户模式和内核模式。内核模式只能运行操作系统的程序。所有的用户应用程序只能在用户模式下运行。 双模式需要CPU的支持,如果CPU有模式位,则可以在操作系统中实现双模式,目前主流的CPU基本都有模式位。双模式允许操作系统不受其它故障应用程序的影响。特权指令是指可能引起崩溃的指令,该指令只能运行在内核模式中。 如果用户程序需要使用特权指令,可以通过系统提供的API调用。

  • I/O保护和内存保护
    定义所有I/O指令为特权指令,用户应用程序无法直接访问I/O指令,只能通过系统调用进行I/O操作,从而避免非法I/O操作。
    利用基址寄存器和限长寄存器隔离不同程序的内存地址。

  • 定时器
    如果用户程序死循环或用户程序不调用系统调用,此时操作系统将无法获得CPU并对系统进行管理。解决方法是引入定时器,在一段时间后发生中断,将CPU控制权返回给操作系统。

四十六、硬盘重量会随着存储数据大小而变化吗?

如果是磁盘重量不变,如果是 SSD硬盘(固态硬盘)会受到影响。

磁硬盘能存储数据靠的是里面的磁铁的方向改变。一个长条形状的磁铁有南极和北极两个端,一种端代表0,另一端代表1,然后通过 01 不同的组合代表不同的意义,只要磁铁足够多,就能用它们排列的顺序代表所有的信息,数据就是这样存在磁硬盘中的。所以磁硬盘重量不会收存储数据大小的影响。

SSD 内部有上万亿个小单元,每个单元表示 0 还是 1,取决于这个单元里装了多少个电子,比如装进去100个电子后,这个单元就代表 1 ,低于这个数值就代表 0。所以对SSD的重量会受到内部电子的影响。一个电子是0.000000……9公斤,30个零。2TB的数据至少要用2×10^13个电子。质量大约就是0.0000000000002公斤,12个零。

四十七、如何消除小数误差

小数误差的原因:

计算机之所以会出现运算错误的原因是因为一些小数无法转换二进制数,例如 0.1 就无法用二进制数正确表示。下图说明了小数的二进制小数表达方式,小数的表示方式和整数表示方式类似。


消除小数误差:

把小数扩大对应的倍数,转成整数进行计算。计算机在进行小数计算时可能会出错,但是在计算整数的时候,只要不超过可处理数值的范围一定不会出现问题。

四十八、运行时是否是 OC 的专利?

runtime 并非是 Objective-C 的专利,绝大多数语言都有这个概念,runtime 就是动态库(运行时库)的一部分。比如 C 语言中 glibc 动态链接库通常会被很多操作依赖,包括字符串处理(strlen、strcpy)、信号处理、socket、线程、IO、动态内存分配等等。由于每个程序都依赖于运行时库,这些库一般都是动态链接的。这样一来,运行时库可以存储在操作系统中,很多程序共享一个动态库,这样就可以节省内存占用空间和应用程序大小。

补充:链接一般分为静态链接和动态链接。一般说的预编译、编译、汇编、链接,其中的链接是指静态链接。所谓的动态链接是指: 链接过程被推迟到运行时再进行。

四十九、线程保活

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");
    }];
    [thread start];
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- (void)test{
    NSLog(@"2");
}

直接执行上述代码,在输出 1 之后,会直接崩溃。主要原因在于,执行完 [thread start] 后,线程立马被杀死。此时再次在线程中调用 test 方法会直接崩溃。 解决该问题的思路主要是保证线程的生命周期,即线程保活。AFN 中,异步网络发起请求,请求回来之后,线程依然没有被杀死,也是利用了线程保活技术。代码如下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"1");
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }];
    [thread start];
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- (void)test{
    NSLog(@"2");
}

五十、包体积优化总结

主要从以下四个方面作总结资源文件、源代码、编译参数配置以及苹果自身优化。

资源文件
  • 1、检测未使用的图片:开源项目LSUnusedResources脚本工具
  • 2、图片压缩处理imageoptim。顺便补充一些很多电商网站里面涉及大量的商品图片,为了节省流量通常可以使用 webp 格式图片,webp 格式图片不仅仅体积小,还支持 gif 格式,可参考这里
  • 3、简单的图片可以使用代码自动生成。很类似的图片,仅仅只有颜色不同,可以通过代码处理图片的颜色。
  • 4、启动图和伪启动图不要直接在资源文件中保留两份,伪启动图容器可以通过代码获取启动图资源。一次实际优化过程中,在该点下手,包体积立马减少了 4M 左右。
  • 5、Xcode 中也会有一些图片相关设置,Compress PNG Files 和 Remove Text Medadata From PNG Files。前者打包的时候自动对图片进行无损压缩,后者会移除 PNG 图像名称、作者、版权、创作时间、注释等信息。
  • 6、部分资源文件还可以通过后端下发方式。
  • 7、iconFont 替换部分图标和文字。可参考该Demo,iconFont 制作过程
  • 8、如果项目中包含各种动画效果,可以使用 Lottie 减少资源文件大小。
  • 9、苹果官方Symbol资源
-(UIImage*)imageChangeColor:(UIColor*)color{
    UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0f);//获取画布
    [color setFill];//画笔沾取颜色
    CGRect bounds = CGRectMake(0, 0, self.size.width, self.size.height);
    UIRectFill(bounds);
    [self drawInRect:bounds blendMode:kCGBlendModeOverlay alpha:1.0f];//绘制一次
    [self drawInRect:bounds blendMode:kCGBlendModeDestinationIn alpha:1.0f];//再绘制一次
    UIImage *img = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();//获取图片
    return img;
}
+ (UIImage *)getLaunchImage{
    CGSize viewSize = [UIScreen mainScreen].bounds.size;
    NSString *viewOr = @"Portrait";//垂直
    NSString *launchImage = nil;
    NSArray *launchImages =  [[[NSBundle mainBundle] infoDictionary] valueForKey:@"UILaunchImages"];
    for (NSDictionary *dict in launchImages) {
        CGSize imageSize = CGSizeFromString(dict[@"UILaunchImageSize"]);
        if (CGSizeEqualToSize(viewSize, imageSize) && [viewOr isEqualToString:dict[@"UILaunchImageOrientation"]]) {
            launchImage = dict[@"UILaunchImageName"];
        }
    }
    return [UIImage imageNamed:launchImage];
}
源代码
  • 1、使用fui工具检测没用到的 import 代码文件,因为运行时的原因,删除类之前做一下核对。
  • 2、SameCodeFinder 可以在源代码⽂文件中检测到相同的 function, 过多的类似方法,可以将其抽离出来提取为工具类。
  • 3、注意控制宏和内联函数。可看本文的第三十八个小知识点包体积优化中的内联函数
  • 4、LinkMap 可以得出每个类或者库所占用的空间大小(代码段+数据段),方便开发者快速定位需要优化的类或静态库。
  • 5、OC 中项目中 Debug 代码即使没有使用,也没有导入头文件,依然会增加包体积,因为 OC 是基于运行时机制,编译器无法确定哪些代码将来是否会使用。但是 Swift 不同 OC,Swift 是静态的,在编译阶段编译器优化就可以去除无用代码。
  • 6、使用轻量级三方库。
编译参数配置
  • 1、Optional Level-->Fastest,Smallest[-OS]:含义可以参照该篇文章 2.1 小节

  • 2、Link-Time Optimization : 它是 LLVM 编译器的一个特性,用于在 link 中间代码时,对全局代码进行优化。这个优化是自动完成的,因此不需要修改现有的代码。苹果使用了新的优化方式 Incremental,大大减少了链接的时间。笔者在实际的项目开发中开启这个配置后,包体积减少了 4 - 5M 左右。

  • 3、Deployment Postprocessing、Strip Linked Product、Strip Debug Symbols During Copy、Symbols hidden by default 四者设置为 YES 后可以去掉不必要的符号信息,减少可执行文件大小。但去除了符号信息之后我们就只能使用 dSYM 来进行符号化了,所以需要将 Debug Information Format 修改为 DWARF with dSYM file。

  • 其它、Dead Code Stripping(仅对静态语言有效):删除静态链接的可执行文件中未引用的代码。Debug 设置为 NO, Release 设置为 YES 可减少可执行文件大小。Xcode 默认会开启此选项,C/C++/Swift 等静态语言编译器会在 link 的时候移除未使用的代码,但是对于 Objective-C 等动态语言是无效的。因为 Objective-C 是建立在运行时上面的,底层暴露给编译器的都是 Runtime 源码编译结果,所有的部分应该都是会被判别为有效代码。

  • 其它、Generate Debug Symbols(有作用但不建议修改): 当 Generate Debug Symbol s选项设置为 YES时,每个源文件在编译成 .o 文件时,编译参数多了 -g 和 -gmodules 两项。打包会生成 symbols 文件。设置为 NO 则 ipa 中不会生成 symbol 文件,可以减少 ipa 大小。但会影响到崩溃的定位。保持默认的开启,不做修改。

苹果自身优化
  • 1、Slicing : 创建、分发不同变体以适应不同目标设备的过程,App Slicing 仅向设备传送与之相关的资源(取决于屏幕分辨率,架构等等)。如 2x 和 3x 的图片放在 Asset Catalog 中会自动管理仅保留合适的图片。但 Bundle 内则会同时包含2x 和 3x 。所以资源图片尽可能放在 Asset Catalog 中。代码资源会对应不同的设备生成不同的执行文件。如果用心的话,还会发现 AppStrore 中同一款应用在不同设备上显示的包体积大小不同。
    注意 : 代码架构的拆分主要是由 Slicing 完成的,并非是 Bitcode 。Bitcode 的优势更多体现在性能、以及后续的维护上。如果开启了 Bitcode,以后 Apple 推出了新的 CPU 架构(不是指新iPhone设备)或者以后 LLVM 推出了一系列优化,我们也不再需要为其发布新的安装包了,Apple Store 会为我们自动完成这步。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,265评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,274评论 1 288
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,087评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,479评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,782评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,218评论 1 207
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,594评论 2 309
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,316评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,955评论 1 237
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,274评论 2 240
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,803评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,177评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,732评论 3 229
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,953评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,687评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,263评论 2 267
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,189评论 2 258