笔记

一、UI视图相关

1、UITableView数据源同步

(1)并发访问,数据拷贝
例如:在列表删除一个cell数据,同时还有一个loadmore加载,那么就需要先记录删除的数据,在加载完成后再判断一次,删除已经删除的数据。

(2)串行访问


数据源同步-串行访问

视图刷新

1、layoutSubviews

这个方法,默认没有做任何事情,需要子类进行重写 。 系统在很多时候会去调用这个方法:

1.初始化不会触发layoutSubviews,但是如果设置了不为CGRectZero的frame的时候就会触发。

2.addSubview会触发layoutSubviews

3.设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化

4.滚动一个UIScrollView会触发layoutSubviews

5.旋转Screen会触发父UIView上的layoutSubviews事件

6.改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件

setNeedsLayout

标记为需要重新布局,不立即刷新,配合layoutIfNeeded会立即更新。

layoutIfNeeded

如果有需要刷新的标记,立即调用layoutSubviews进行布局。

drawRect

不能直接调用drawRect。drawRect方法使用注意点:

1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect,让系统自动调该方法。

2、若使用calayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法

3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕。

2、事件传递、事件响应

事件传递:UIApplication->UIWindow->viewcontroller->view->subviews
事件响应:subviews->view->viewcontroller->UIWindow->UIApplication->没有响应者就抛弃


事件传递

3、图像显示原理

图像显示原理1

CPU工作:

Layout:UI布局计算、文本计算

Display:绘制drawRect方法

Prepare:图片编解码,UIImage是不能直接显示的需要先解码

Commit:提交位图给GPU处理

CPU显示原理

GPU显示原理

4、UI卡顿掉帧

(1)卡顿掉帧原因:

在规定的16.7毫秒内,在下一帧到来前CPU、GPU没有共同完成下一帧图像,就会出现卡顿和掉帧


image

(2)解决

CPU层级以下在子线程中完成:

  • 对象的创建、调整、销毁
  • 预排版(布局计算,文本计算)
  • 预渲染(文本等异步绘制,图片编解码)

GPU层级

  • 纹理渲染:避免离屏渲染、依托CPU异步绘制减轻GPU压力
  • 减少视图层级

5、异步绘制

(1)UIView的绘制原理:

调用setNeedsDislay时候,(实际上是这个view的layer调用setNeedsDisplay方法,之后相当于在这个layer上打上一个脏标记),然后并没有立即发生当前视图的绘制工作,而是在当前runloop快要结束的时候调用CALayer的display方法,进入到当前视图真正的绘制工作的流程当中。
原因是由于要减少绘制次数,提升性能,所以要在当前runloop快要结束的时候调用CALayer的display方法。

绘制原理

(2)系统绘制流程
[UIView drawRect:]是系统开给我们的异步绘制口子,让我们可以做一些操作。


系统绘制流程

(3)异步绘制原理
通过子线程的切换,借助Global queue,在子线程中进行位图的绘制,此时主线程可以做其它的工作。等子线程绘制位图完毕,再回到主队列中提交位图,设置给CALayer的contents属性,完成一个UI控件的异步绘图过程。


异步绘制原理

1、某个时机调用setNeedsDisplay;

2、runloop将要结束时调用[CALayer display];

3、若代理实现了displayLayer将会调用此方法,在子线程中做异步绘制的工作;

4、在子线程中创建上下文、绘制控件并生成图片;

5、在主线程中设置layer.contents,将生成的视图展示在layer上。

例如我们创建了一个叫AsyncDrawLabel的UIView,实现其异步绘制关键代码在dispalyLayer:layer

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface AsyncDrawLabel : UIView

@property (nonatomic, copy) NSString *text;
@property (nonatomic, strong) UIFont *font;

@end

NS_ASSUME_NONNULL_END
#import "AsyncDrawLabel.h"
#import <CoreText/CoreText.h>

@implementation AsyncDrawLabel

- (void)setText:(NSString *)text {
    _text = text;
}

- (void)setFont:(UIFont *)font {
    _font = font;
}


// 除了在drawRect方法中, 其他地方获取context需要自己创建[https://www.jianshu.com/p/86f025f06d62] coreText用法简介:[https://www.cnblogs.com/purple-sweet-pottoes/p/5109413.html]
 
- (void)displayLayer:(CALayer *)layer {
    CGSize size = self.bounds.size;
    CGFloat scale = [UIScreen mainScreen].scale;
    // 异步绘制,切换至子线程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        UIGraphicsBeginImageContextWithOptions(size, NO, scale);
        // 获取当前上下文
        CGContextRef context = UIGraphicsGetCurrentContext();
        [self draw:context size:size];
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        // 子线程完成工作,切换至主线程显示
        dispatch_async(dispatch_get_main_queue(), ^{
            self.layer.contents = (__bridge id)image.CGImage;
        });
    });
}

- (void)draw:(CGContextRef)context size:(CGSize)size {
    // 将坐标系上下翻转,因为底层坐标系和 UIKit 坐标系原点位置不同。
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    // 文本沿着Y轴移动
    CGContextTranslateCTM(context, 0, size.height); // 原点为左下角
    // 文本反转成context坐标系
    CGContextScaleCTM(context, 1, -1);
    // 创建绘制区域
    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
    // 创建需要绘制的文字
    NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
    [attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];
    // 根据attStr生成CTFramesetterRef
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
    // 将frame的内容绘制到content中
    CTFrameDraw(frame, context);
}

@end

6、离屏渲染

离屏渲染意思是在当前屏幕缓冲区外,创建了一个新的缓冲区,使得GPU触发了openGL的多通道渲染管线,产生了额外开销。可能造成CPU+GPU在一帧的时间内无法完成对应操作,造成卡顿和掉帧。

为什么会产生离屏渲染

有些效果不能直接呈现到屏幕,而需要在缓冲区以外做额外的处理预合成。如图层属性的混合体没有预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染。

(1)常见触发场景

  • 圆角(必须要和maskToBounds一起使用时才会触发)
  • 光栅化(shouldRasterize)
  • 阴影(shadow) layer.shadowColor、layer.shadowOffset、layer.shadowRadius、layer.shadowOpacity,如果设置了 layer.shadowPath 就不会产生离屏渲染
  • 图层蒙版(mask)
  • group opacity(组透明度)
  • UIBlurEffect(毛玻璃)

(2)离屏渲染导致卡顿掉帧原理

离屏渲染是发生在GPU层面,使得GPU触发了OpenGL的多通道渲染管线,产生了额外的开销。

  • 创建新的渲染缓冲区
    增加了内存的开销,包括上下文切换,因为有多通道渲染管线,需要把多通道的渲染结果做一个最终的合成,就需要上下文切换,这就造成了GPU额外的开销。
  • 离屏渲染增加了GPU的工作量,使得CPU+GPU的工作时间超出了16.7ms的总耗时,可能会导致UI的卡顿和掉帧。

(3)UITableView等列表滑动优化
CPU层面

  • 对象创建销毁调整在子线程中完成、视图位置计算文本计算、图片的编解码、异步绘制、减少视图层级
    GPU
  • 避免离屏渲染

二、Object-C相关

1、分类(category)

(1)分类做了哪些事?

  • 声明私有方法
  • 分解体积庞大的文件
  • 将framework中的私有方法公开

(2)分类的特点

  • 运行时决议
  • 为系统类添加分类

(3)分类中添加哪些内容

  • 实例方法
  • 类方法
  • 协议
  • 属性(只是添加get set 方法)
  • 实例变量(使用关联对象)

(4)分类总结

  • 分类添加的方法可以“覆盖”原类方法(这里的覆盖只不过是,在分类方法拼接到原类方法列表中,在前面插入。导致方法查询是优先查询到了分类方法)
  • 同名分类方法谁能生效取决于编译顺序(优先执行最后编译的分类方法)
  • 名字相同的分类会引起编译报错

2、关联对象

关键方法

(1)关联对象本质,关联对象存储在哪里

关联对象本质

3、扩展

  • 编译时决议
  • 只以声明形式存在,多数情况寄生于宿主类的.m文件中
  • 不能为系统类添加扩展

4、代理、通知

  • 代理是代理模式实现、通知是观察者模式实现的跨层级消息传递机制
  • 代理一对一、通知一对多

(1)通知实现机制
全局创建一个map表,key为notificationName,value为observes_list观察者列表


通知实现机制

(2)没移除通知/kvo导致的奔溃
在iOS9 之前,通知中心对通知观察者做了unsafe_unretain引用,而iOS
9 之后做了weak 引用,区别就是,unsafe_unretain引用就是对象释放之后,指针不会置为nil,
会造成野指针,而weak 引用,对象释放之后,指针也会置为nil,不会造成野指针的问题。

(3)通知和kvo区别

  • 通知需要主动发送
  • KVO只能监听属性的变化,并且触发条件是通过setter/getter方法调用。
  • KVO是被观察者直接发送消息给观察者,是对象间的直接交互,通知则是两者都和通知中心对象交互

5、KVO

  • kvo是objective-c对观察者设计模式的又一实现
  • isa-swizzling实现kvo
    当你观察一个类时,系统会同时创建一个新类NSKVONotifying_A继承自原类,并且将原类的isa指针指向新创建的类,并且重写属性的setter方法。
willChangeValueForKey:
didChangeValueForKey:
kvo实现机制
  • 由kvc改变值,会引起kvo
  • 直接改变成员变量,则不会引起kvo。除非手动添加willChangeValueForKey和didChangeValueForKey方法
6、KVC

KVC会破坏面向对象编程的封装特性。

KVC这里的key是没有任何限制的,如果已知某个类或者实例的内部某个私有成员变量名称的话,我们在外界是可以通过已知的key来访问、设置。即破坏了面向对象的编程思想。

-(id)valueForKey:(NSString *)key
-(void)setValue:(id)value forKey:(NSString*)key
valueForKey查找相似方法

valueForKey查找相似变量
setValueForKey
7、属性关键字

读写相关:readOnly、readWrite
原子性:atomic、nonatomic
引用计数相关:retain、strong、assign、weak、copy

(1)weak/assign区别

  • weak只能修饰对象,释放后自动将对象指针设置为nil
  • assign能修饰对象和基础属性(int bool),但是释放对象后,会出现野指针,再次调用会出现内存泄漏
  • 都不改变引用计数

(2)__weak和__strong

在多线程环境下在block中还需要使用__strong强引用一下对象。__strong会在block执行完成后对象的引用计数-1。这样就可以避免循环引用。

(3)__weak的实现:

简单来说,系统有一个全局的 CFMutableDictionary 实例,来保存每个对象的 weak 指针列表,因为每个对象可能有多个 weak 指针,所以这个实例的值是 CFMutableSet(Array) 类型。
剩下我们要做的,就是在引用计数变成 0 的时候,去这个全局的字典里面,找到所有的 weak 指针,将其值设置成 nil。如何做到这一点呢?Friday QA 上介绍了一种类似 KVO 实现的方式。当对象存在 weak 指针时,我们可以将这个实例指向一个新创建的子类,然后修改这个子类的 release 方法,在 release 方法中,去从全局的。

CFMutableDictionary 字典中找到所有的 weak 对象,并且设置成 nil。

(4)copy/mutableCopy

  • 可变对象的copy和mutableCopy都是深拷贝,会创建新的内存空间
  • 不可变对象的copy是浅拷贝,mutableCopy是深拷贝
  • copy方法返回的都是不可变对象
@property(copy) NSMutableArray *array
这样声明会有何问题?
答案:copy后得到的是NSArray不可变对象,但是声明的是一个NSMutableArray,会使用append,remove等操作数组就会发生奔溃。

(5)weak修饰String相关问题

    __weak NSString *str1 = [[NSString alloc] initWithFormat:@"First Name"];
    __weak NSString *str2 = [[NSString alloc] initWithFormat:@"joealzhou"];
    __weak NSString *str3 = @"joeal zhou";
    __weak NSString *str4 = [NSString stringWithString:@"joeal zhou"];
    __weak NSString *str5 = [[NSString alloc] initWithString:@"joeal zhou"];
    NSLog(@"str1:%@ str2:%@ str3:%@ str4:%@ str5:%@", str1, str2, str3, str4, str5);
打印结果:
   str1:(null) str2:joealzhou str3:joeal zhou str4:joeal zhou str5:joeal zhou

使用str3、str4、str5方式直接生成的字符是存储在常量区。对引用计数不影响。
而使用initWithFormat:方式生成的字符串这里有两种区别:

  • 字符串长度大于9:在堆区会影响引用计数,所以使用__weak会释放。
  • 字符串长度不大于9:在常量区,也是不影响引用计数。
    Tip:如果是中文或者非ASII字符时,那么就只能存储为String指针。

(6)swift中的copy on write 写时复制

指的是Swift 中的值类型,并不会在一开始赋值的时候就开辟新的内存空间,只有在需要改变这个值的时候才去开辟新的内存空间,以达到优化内存的目的.

var arr1 = [1,2,3]
var arr2 = arr1//[1,2,3]
print1(address: arr1)
print1(address: arr2)
arr2.append(4)// 改变数组 arr2
print1(address: arr1)
print1(address: arr2)
 
arr1的地址:0x137e2f930
arr2的地址:0x137e2f930
改变arr2 之后:
arr1的地址:0x137e2f930
arr2的地址:0x139901670

如上 初始化数组1,在把数组1赋值给数组2.这时候两个数组的指向的内存地址是一样的,共享了他们的存储部分.当我们改变arr2的时候,共享会被检测到,这时候arr2会重新开辟内存空间,在这个新的内存空间把值复制过去.然后在对arr2 进行操作.. 即 元素复制操作只是在必要的时候发生.

扩展:

OC 中数组字典是引用类型. Swift 里数组字典是值类型.
Swift 中采用了如上所述的写时复制技术. 即当一个结构体发生了写入行为时才会有复制行为.

三、runtime相关

1、runtime数据结构

  • objc_object
  • objc_class
  • isa指针
  • method_t
objc_object
objc_object
objc_class

cache_t里面缓存了之前查找到的方法,是用一张hashmap来实现的,为的就是能快速查找。key对应的是SEL,value对应函数地址IMP。

objc_class

isa

关于对象isa指向的是类对象,关于类对象,指向元类对象。

在oc的方法查找中,如果是实例方法则根据对象的isa指针找到其类对象,再从类对象中的方法列表中查找。如果是类方法则根据类对象的isa指针找到其元类对象,再从元类对象的方法列表中查找。


isa
Type Encodings
type encodings
整体数据结构

2、类对象元类对象、消息传递

经典题分析:


面试题

[self class]转化成objc_msgsend

[super class]转化成objc_msgsendsuper。super是一个结构体,里面的receiver就是当前对象self。所以这道题应该都是打印Phone。

super结构体
类对象元类对象关系图

特别注意最后,根元类对象的superclass指向的是根类对象。

面试题:如果有一个类方法在元类方法及元类的父类中都没有方法实现。但是有实例方法实现,会报错吗?
解析:不会,并且会调用该同名实例方法。因为如果在根元类方法列表中没有找到,就会从根类方法类别中查找同名实例方法。因为根元类的父类是根类。
类对象-元类对象关系图
消息传递
消息传递

消息转发流程:

1、调用动态解析方法resolveClassMethod:(SEL)sel,如果动态添加方法(调用class_addMethod函数)并返回YES,则结束流程

2、如果上一步没有实现动态添加方法,无论返回Yes还是No,都会调用消息接受者重定向forwardingTargetForSelector方法,如果返回重定向接受者,则当前流程结束

3、如果返回上一步nil,则会调用methodSignatureForSelector获取函数的参数和返回值类型,同时调用forwardInvocation消息通知当前对象。

4、如果上一步返回nil,消息无法处理,App crash。

即消息转发三步骤:消息动态解析、消息接受者重定向、消息重定向 如果预防崩溃,在第二步进行消息转发


消息转发

)

1、[obj foo]在编译后会转成objc_msgSend(obj, @selector(foo)),消息转发机制。

2、第二题考察的是方法的查找:先查找类的方法缓存列表,再从父类的方法缓存及方法列表中查找,都没有找到就走消息转发流程。

3、不能向编译后的类添加实例变量,因为内存布局已经结束了。但可以向动态运行时添加的类,添加实例变量。

runtime

+load

1.当父类和子类都实现load函数时,父类的load方法执行顺序要优先于子类

2.当子类未实现load方法时,不会调用父类load方法

3.类中的load方法执行顺序要优先于类别(Category)

4.当有多个类别(Category)都实现了load方法,这几个load方法都会执行,
但执行顺序不确定(其执行顺序与类别在Compile Sources中出现的顺序一致)

5.当然当有多个不同的类的时候,每个类load 执行顺序与其在Compile Sources出现的顺序一致

+initalize

当使用到这个类时就会调用:类方法、实例方法、runtime发送消息

1.父类的initialize方法会比子类先执行

2.当子类未实现initialize方法时,会调用父类initialize方法,
子类实现initialize方法时,会覆盖父类initialize方法.

3.当有多个Category都实现了initialize方法,会覆盖类中的方法,
只执行一个(会执行Compile Sources 列表中最后一个Category 的initialize方法)

四、内存管理相关

1、内存管理方案

内存管理方案:

  • TaggedPointer, 对于一些小对象, 例如NSNumber
  • NONPOINTER_ISA 非指针型isa 对于64位架构下iOS应用程序采用这种内存管理防范; 在64位架构下, 这种isa指针占据32位或者40位比特位, 剩余的比特位存储内存管理的数据内容
  • 散列表包括引用计数表和弱引用技术标

散列表方式:SideTables()结构,由多个SideTable组成。
散列表实现内存管理涉及到的数据结构:

  • spinlock_t 自旋锁
  • refcountMap 引用计数表
  • weak_table_t 弱引用表

自旋锁概念:

  • 是“忙等”的锁,如果该锁已被其它线程获取,那么当前线程就会不断探测这个锁是否被释放,如果被释放则第一时间获取该锁;其它的锁,比如信号量,当线程获取不到锁的时候,会阻塞等待;待其它线程释放锁了之后,唤醒该线程获取锁;
  • 适用于轻量访问

分离锁的概念:

系统为了解决只有一张SideTable造成的效率低下的问题,引入了分离锁的概念:

把内存对象的引用计数表分拆成多个部分,比如分拆成8个,就需要对这8个表分别加锁,比如对象A在ptr(1)~ptr(8)的表里,对象B在ptr(57)~ptr(64)的表里,当A和B同时进行引用计数操作时就可以并发操作,提高访问效率。

分离锁

2、MRC与ARC区别

mrc
arc

引用计数retain实现

通过对象指针地址hash查找到SideTable,再hash找到存储地址。


retain

对象可以直接释放的五个判断条件(需要全部满足)

1.当前对象不是非指针类型的isa指针。

2.无弱引用

3.无关联对象

4.无c++、没采用ARC

5.没有使用side table

释放条件

3、如何添加弱引用对象

一个被声明为 __weak 的对象指针经过编译器的编译之后,会调用相应的 objc_initWeak(),然后经过一系列的函数调用栈,最终在 weak_register_no_lock() 进行弱引用的添加,具体添加的位置是通过一个哈希算法来进行位置查找的

如果我们查找的对应位置已经有了这个当前对象所对应的弱引用数组,就把新的弱引用变量添加到数组当中
如果没有当前对象所对应的弱引用数组,就创建一个,然后把第0个位置添加上最新的weak指针,后面的都初始化为nil


添加weak

4、当一个对象被释放之后,weak变量是如何处理的?

当一个对象被dealloc之后,在dealloc() 的内部实现当中会去调用弱引用清除的相关函数,然后在weak_clear_no_lock()当中,会根据当前对象指针查找弱引用表,把当前对象相对应的弱引用都取出来得到一个entry数组,然后遍历这个数组当中的弱引用指针,分别置为nil

5、autoreleasepool

实现:是以栈为节点,通过双向链表的形式组合而成的。并且和线程是一一对应的。

autoreleasepool为何可以多层嵌套?

多层嵌套实际上是多次插入哨兵对象。AutoReleasePool的嵌套实际上就是AutoReleasePoolPage::Push调用 ,这个调用实质上就是插入一个哨兵 ,而是否增加链表节点 取决于当下的Page是否是满的。

6、图片加载

使用imageNamed:加载图片:

  • 加载到内存中后,会一直停留在内存中,不会随着对象销毁而销毁
  • 加载进图片后,占用的内存归系统管理,我们无法管理
  • 相同的图片,图片不会重新加载
  • 加载到内存中后,占据内存空间较大

使用 imageWithContentOfFile:加载图片:

  • 加载到内存中后,占据内存空间比较小
  • 相同的图片会被重复加载到内存中
  • 对象销毁的时候,加载到内存中得图片会被一起销毁

结论:

如果图片较小,并且频繁使用的图片,使用imageName:来加载图片(按钮图片/主页图片/占位图)

如果图片较大,并且使用次数较少,使用 imageWithContentOfFile:来加载(相册/版本新特性)

五、Block

什么是block?

block是封装了函数及其上下文的一个对象。

1、Block截获变量规则

  • 对于基本类型的局部变量截获其值
  • 对于对象类型的局部变量连同所有权修饰符(block外定义的是strong截获的就是strong)一起截获
  • 对于局部静态变量以指针形式截获
  • 对于全局变量全局静态变量不截获

2、什么时候使用__block修饰符?

在block中对被截获变量进行赋值时需要添加__block。

{
  NSMutableArray *array = [NSMutableArray array];
void (^block)(void) = ^{
// 下面这种情况下就可以不用添加__block
  [array addObject:@123];
// 赋值就需要添加__block
  array = [NSMutableArray array];
  };
block();
}

3、block有哪几类

  • NSConcreteGlobalBlock
  • NSConcreteStackBlock
  • NSConcreteMallocBlock

4、block中的__forwarding指针

当block在栈上时__forwarding指针指向的是自身,当发生copy操作时指针指向的是堆上的block变量。__forwarding存在的意义是:不管block在任何内存位置,都可以顺利访问同一个__block变量


block

5、block的循环引用问题

解决这种循环引用需要在使用完blockSelf变量后主动置为nil,但是这样也会导致一个问题,只有调用了block才会断开,没调用该block时,循环引用一直存在。


image

六、多线程

  • GCD
  • NSOperation
  • NSThread

1、GCD

以同步方式提交任务,无论在哪个队列都将在当前线程中执行。


同步并行队列

因为viewDidLoad也是在主队列中,将和gcd中的任务引发队列死锁。如果换成自定义串行队列就没问题。


队列引发的死锁

队列死锁

GCD底层创建的一个线程没有开启runloop,所以performSelector不会执行。


image

那么如何在GCD线程中开启runloop呢?

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
  [self performSelector:@selector(test) withObject:nil afterDelay:2];
  // 这句代码要在performSelector后面执行
  [[NSRunLoop currentRunLoop] run];
});

因为run方法只是尝试想要开启当前线程中的runloop,但是如果该线程中并没有任何事件(source、timer、observer)的话,并不会成功的开启。

2、GCD实现多读单写dispatch_barrier_async

image

3、dispatch_group_async()

应用到具体实例中就是,下载多张小图拼成一张大图。


image

4、NSOperation

任务状态:

  • isReady
  • isExecuting
  • isFinished
  • isCancelled

状态控制:

  • 如果只重写main方法,底层控制变更任务执行状态,以及任务退出。
  • 如果重写了start方法,那么要自行控制任务状态。

系统是如何移除状态isFinished=YES的NSOperation?

是通过kvo来实现的。

5、NSThread

如何启动的?

NSThread的启动流程:
首先调用start,创建pthread调用main方法,再调用performSelector,最后调用exit。
涉及到NSThread的考察都是结合常驻线程的,一般在对应的入口函数selector中添加一个runloop,来达到实现一个常驻线程的目的。

用过哪些锁?

1、synchronize
一般创建单例对象时使用的。
2、automic
修饰属性关键字,只作用于赋值,不包括操作对象。
3、NSLock
4、NSRecursiveLock
递归锁
5、OSSpinlock
自旋锁,常用于轻量级数据访问。
6、dispatch_semaphore_t
信号量访问。
dispatch_semaphore_wait()内部实现:
if value-1<0,将当前进程状态设置为等待。将该进程的PCB插入响应等待队列。
dispatch_semaphore_singal()内部实现:
if value+1<=0,则会唤醒一个等待队列中的一个进程,改变其状态为就绪状态,并将其插入就绪队列。

六、Runloop

runloop是在内部维护事件循环来对事件/消息处理的一个对象。内核态和用户态的相互切换。被唤醒就是内核态切换到用户态。休眠时就是用户态到内核态。

事件循环Event Loop:

1)没有消息需要处理时,进程或者线程会进入休眠状态,而休眠状态的过渡相当于把当前线程的控制权转移给了内核态。

2)有消息需要处理时,就会有一个从用户态到内核态的状态切换。

3)维护的事件循环可以用来不断的处理消息或事件,对他们进行管理,如果没有消息进行处理,会从用户态切换到内核态,进行资源的休眠避免资源占用;当有消息进行处理时,会发生从内核态到用户态的切换,当前用户线程会被唤醒;

状态的切换是回答该问题的关键点。


image

main函数为什么能保持一直运行的状态而不退出?

在main函数中所调用的UIApplicationMain函数内部会启动主线程的runloop,而runloop又是对事件循环的一种维护机制,可以做到有事做的时候做事,没有事情做的时候会通过用户态到内核态的切换,避免资源占用,使当前线程处于休眠状态。

注意:等待不等于死循环

CFRunLoopSource:

source0:需要手动唤醒线程,在我们添加一个source0到对应runloop中,并不会主动唤醒当前线程,需要手动唤醒,把当前线程从内核态切换到用户态。

source1:具备唤醒线程的能力

CommonMode的特殊性:

NSRunLoopCommonModes字符串常量来表达CommonMode。

1)CommonMode并不是实际存在的mode。

2)是同步source、timer、observer到多个mode的一个技术方案。

runloop和线程一一对应,runloop有多个mode,mode有多个source/observer/timer。

image

RunLoop事件循环机制

从屏幕上点击开始系统发生了什么?

调用了main函数之后,会调用UIApplicationMain,在内部会启动主线程的runloop,进过一系列的处理runloop处于休眠状态。如果此时点击屏幕产生了mach-port,最终转成source1事件,把主线程唤醒,运行处理。当我们把程序杀死时,会触发kCFRunloopExit通知,即将退出runloop,线程被销毁。

唤醒操作有:Source1、timer事件、外部手动唤醒。

image

RunLoop总结

1、怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?

用户滑动操作时runloop是运行在kCFRunLoopUITrackingMode下,网络请求一般放在子线程中,子线程返回给主线程的数据要抛回给主线程进行UI更新,把这部分的逻辑包装起来提交到主线程defaultMode下,这样进行mode隔离就避免了问题。

2、如何实现一个常驻线程:

1)创建一个runoop。

2)给runloop添加source/timer/observer事件以及port。

3)调用run方法。

注意:

运行的模式和资源添加的模式必须是同一个,否则可能由于外部使用while循环会导致死循环。

image

3、runLoop与线程是怎样的关系?

1)两者一一对应的关系

2)一个线程默认是没有runloop,需要手动加上runloop。

七、HTTP网络相关

HTTP协议

image

image

1、请求方法

GET POST HEAD DELETE PUT OPTIONS

2、GET和POST方式的区别 标准答案:从语义的角度来回答

  • get是获取资源。安全的,幂等的(一次或多次请求不会造成资源状态修改),可缓存的。
  • post是处理资源。非安全的,非幂等的,不可缓存的。

3、连接建立流程:

1)通过TCP的三次握手建立连接。

2)在这条连接上进行http的请求和响应。

3)经历TCP的四次挥手进行连接的释放。

image

4、HTTP的特点:

  • 无连接(需要建立连接、释放连接的过程) ,可用HTTP持久连接方案解决
  • 无状态(多次发送HTTP请求,SERVER端不知道是同一个用户),用COOKIE/SESSION方案解决

5、HTTP的持久连接和非持久连接

非持久连接的定义:

每次进行http请求都是重新创建一个连接,经历三次握手和四次挥手。

持久连接的定义:

打开一条tcp通道,多个http请求在同一条tcp通道上进行,在一段time后关闭。

image

持久连接头部字段:

  • connection:keep-alive,表示客户端期许采用持久连接。
  • time:20,持久连接持续多久有效。
  • max:10,这条连接最多发生多少个http请求。

持久连接中,怎样判断一个请求是否结束?

1)通过响应中的content-length字段的值来判断。

2)chunked,比如通过post请求server端给客户端可能会多次响应返回数据,当有多个块通过http的tcp连接传给客户端时,每一个报文都会带有chunked字段,而最后一个块是一个空的chunked。所以可以通过判断哪个chunked是空的来判断前一个网络请求是否结束。

6、charles抓包原理是怎样的?

利用了http的中间人攻击这个漏洞。

中间人攻击的定义:

当client发送一个http请求时,是由中间人进行hold住,然后中间人假冒client的身份向server端进行同样的请求,然后server端返回结果给中间人,再由中间人返给client。
如果使用http进行请求或者响应时,中间人可以篡改我们发起的请求参数,server端发回的数据也可以被篡改之后再发给client。

image

7、HTTPS

HTTPS其实是HTTP加上了ssl/tls加密,是一种安全的通信方式。

8、HTTPS建立连接流程

HTTPS建立流程描述1:

Client端:发送TLS版本号,random number C,所有支持的加密算法给服务器进行协商;

Server端:商定好加密算法,random number S,server证书发给客户端。

Client端:

1)验证server证书;

2)利用预主密钥(对称加密中用到的密钥),random numberC,andom numberS三个值通过一定的算法合成会话密钥后,通过server的公钥进行加密传输。

HTTPS建立流程描述2:

Server端:

1)通过私钥解密收到的会话密钥,得到预主密钥。

2)利用预主密钥(对称加密中用到的密钥),random numberC,andom numberS三个值通过一定的算法合成会话密钥。

HTTPS建立流程描述3:

Client端 发送一条经过会话密钥加密的握手消息给server端;

Server端 发送一条经过会话密钥加密的握手消息给client端;

来验证安全通道是否已经建立完成。

image

9、HTTPS都使用了哪些加密手段?为什么

1)连接建立过程中使用非对称加密,非对称加密很耗时。

2)后续通信过程使用对称加密

非对称加密:使用的是一对私钥公钥进行加密算法。

对称加密:使用的是同一个密钥进行加密算法。

10、为什么需要进行三次握手?

【为了应对网络中存在的延迟或者重复数据的问题。】

比如:客户端发送syn同步报文时,超时了,会触发超时重传策略,又发送一条syn报文,服务端收到了重传的,回复了syn同步报文和ack确认报文,此时超时的syn同步报文被服务器收到,服务端也会回复syn同步报文和ack确认报文,客户端实际只需要建立一次连接,通过第三次握手确认,即回复服务端ack确认报文,服务端会忽略没有确认报文的连接

11、为什么断开连接需要四次挥手?

客户端和服务端建立的TCP通道是双向通道:

即:一条通道双方都可以接收和发送。

正是因为这样的双通道机制所以需要双方面的连接释放,所以需要四次挥手。先断开的是客户端发送到服务端,第二次是断开服务端到客户端。

image

12、UDP,用户数据报协议:

1)无连接 不用在数据传输之前进行连接和释放连接。

2)尽最大努力交付

3)面向报文 既不合并,也不拆分。

13、TCP可靠传输表现在哪些方面:

  • 无差错
  • 不丢失
  • 不重复
  • 按序到达

14、TCP可靠传输是通过停止等待协议实现的:

四方面理解:

  • 无差错情况
  • 超时重传
  • 确认丢失
  • 确认迟到

15、TCP流量控制

基于滑动窗口协议。接收方可以根据可接收的大小动态修改发送方的窗口大小,这里就体现了流量控制。
流量控制:

基于滑动窗口协议:

1)发送窗口

  • 发送窗口第一个字节是最后被确认的字节;最后一个字节是最后发送的字节;
  • 发送窗口比发送缓存小;
  • 接收窗口也有固定的大小,如果超出会溢出;
  • 接收方可以动态调整发送窗口的大小来决定发送速率

2)接收窗口

  • 序号从左到右是增大的;
  • 接收缓存中左侧部分是按序到达的,最后一个字节是下一个期望收到的字节。
  • 缓冲区中往上层应用程序提交的都是已经按序到达的字节;未按序到达的字节必须等到前部分的字节都到达了,保证有序了才能向上层提交
  • 接收窗口第一个字节是下一个读取的字节,最后一个字节是下一个期望收到的字节。
  • 接收窗口的大小制约发送窗口的大小。
image

16、TCP拥塞控制

  • 慢开始、拥塞避免
    以指数增长发送,到达门限值后采用拥塞避免加法增大,到达拥塞窗口后乘法减小。重新以一个新的门限值开始慢开始。
  • 快恢复、快重传


    image

17、cookie和session

1)cookie性质:

  • 客户端发送的cookie在http请求报文的cookie首部字段中。
  • 服务器端设置http响应报文的set-cookie首部字段。

2)session工作流程:

  • 客户端发送http请求报文,服务器端会进行2步,比如记录用户状态,密码和用户名,同时会生成sesssionid,再把sessionid用setCookie设置给http响应报文的头部发给客户端;
  • 客户端在后续的请求过程中,在http请求头部字段的cookie中,设置所接收到的sessionid,这样server就可以通过sessionid来识别用户。

3)清除cookie

  • 新cookie覆盖旧cookie;
  • 覆盖规则:name,path,domain等需要与原cookie一致。
  • 设置cookie的expires=过去的一个时间点,或者maxAge = 0,相当于说明这个cookie是无效的;

DNS相关

1、DNS解析

将需要访问的域名发送给DNS服务器,DNS服务器会返回给客户端对应的IP地址,然后客户端用这个IP地址采用HTTP连接该域名对应的服务器进行交互。

2、DNS解析查询方式

  • 递归查询
    客户端先去本地DNS服务器查询,如果没有结果会往上一级DNS服务器中查询。
    顺序为本地DNS---》根域DNS---》顶级DNS---》权限DNS。
  • 迭代查询

3、DNS解析存在的问题

  • DNS劫持
    在DNS解析过程中因为采用的是UDP并且是明文的方式。容易被钓鱼网站利用返回一个错误的IP地址。
  • DNS解析转发

DNS解析转发:

客户端在询问本地DNS服务器来获取某一域名的IP地址时,比如是通过手机访问慕课网,经过XX移动DNS服务器解析域名,有时会不遵从一些协议规范,比如一些小的运营商,为了节省资源,把请求转发给其它的某一个XX电信DNS,来帮助XX移动DNS服务器完成解析;

XX电信的DNS会向权威DNS请求解析对应域名,权威DNS会根据不同运营商的请求进行流量的调度分发,返回IP地址给XX移动DNS,再交给客户端。所以可能会造成跨网访问的问题:请求缓慢,效率低下。

4、解决DNS劫持问题

  • httpDNS
    DNS解析是通过DNS协议向DNS服务器的53端口进行请求; httpDNS是使用http协议向DNS服务器的80端口进行请求。 这样就不涉及DNS解析,自然不会被劫持。
  • 长连接

客户端和Server建立一个长连server,可以理解为一个代理服务器; 客户端和长连server中可以建立一个TCP的长连通道; 长连server和server可以通过内网专线来进行http请求和响应。在这里关于请求的关于域名解析是长连server通过内网专线通过内网DNS的解析得到的,就规避了公网DNS解析涉及到的劫持问题。

5、DNS和HTTP的关系?

没关系。DNS解析是发生在HTTP连接之前。DNS解析请求使用UDP数据报,端口号53(httpDNS是使用http协议向DNS服务器的80端口进行请求。)

八、设计模式相关

一、六大设计原则

1:单一职责原则 一个类只负责一件事,例如:UIView与CALayer

2:依赖倒置原则 抽象不应该依赖于具体实现,具体实现可以依赖于抽象 (即上层业务调用时,不关心具体实现,只关心接口)

3:开闭原则 对修改关闭,对扩展开放, 例如:类的定义

4:里氏替换原则 父类可以被子类无缝替换,且原有功能不受任何影响 例如:KVO,当开始监听变量的时候,系统在动态运行时已自动生成并指向子类,对外暴露的还是在操作父类,实际上系统内部是对子类的操作

5:接口隔离原则 使用多个专门的协议,而不是一个庞大臃肿的协议, 例如:UITableView代理

6:迪米特法则 一个对象对其他对象应尽可能少的了解,高内聚,低耦合

二、六大设计模式

1、责任链模式

比如:事件响应链

责任链模式就是为一个请求或者一个动作创建一个接收者对象的链,这条链上的每一个对象都可以去响应和处理这个请求和动作,把发送者和接收者进行解耦,在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。

2、桥接模式

ClassA、ClassB为抽象类,A1、A2、A3和B1、B2、B3分别为对应的子类

image

3、适配器模式

  • 类适配器
  • 对象适配器

比如:项目中一个很稳定的类文件ClassA,我们需要为它增加功能并且要使用到旧逻辑,如果直接在里面修改的话风险较高。则可以新建一个类,添加一个属性为ClassA,然后去调用相关旧逻辑。


截屏2021-03-02 16.31.39.png

4、单例模式

单例重写方法必不可少

原因:主要为了规避外部调用单例时没有调用share方法而用alloc的方法或者copy的方法,外部可能会对单例对象进行copy操作来创建一个新的对象

image

5、命令模式

命令模式是做行为参数化的,作用是降低代码重合度

九、算法

有序数组合并

创建两个指针分别指向初始两个有序数组,对比两个值,小的放入新数组同时移动小的指针。一直循环下去直到指针移动到末尾,那么把剩余值全部放入新数组。

截屏2021-03-05 16.03.04.png

字符串反转

定义两个指针分别指向开头和结尾,替换其值,再begin+1 end-1直到中间说明全部交换完了。

截屏2021-03-05 16.06.42.png

两个视图寻找共同父视图

image

无序数组的中位数

使用快速排序,当左右长度一样说明当前值就是数组的中位数。


image

十、swift各版本

Swift2

  • Error handling增强;
  • guard语法;
  • 协议支持扩展。

Swift3

  • 新的GCD和Core Graphics;
  • NS前缀从老的Foundation类型中移除;
  • 内联序列函数sequence;
  • 新增fileprivate和open两个权限控制;
  • 移除了诸多弃用的特性,比如++、–运算符等。

Swift4

  • extension中可以访问private的属性;
  • 类型和协议的组合类型;
  • Associated Type可以追加Where约束语句;
  • 新的Key Paths语法;
  • 下标支持泛型;
  • 字符串增强。

Swift5

  • ABI稳定;
  • Raw strings;用#号包裹String
let qutoedString = #"如果句子里面有"双引号"就很尴尬"#    
  • 标准库新增Result;
  • 定义了与Python或Ruby等脚本语言互操作的动态可调动类型。

十一、xcode打包编译过程

1、写入辅助文件:将项目的文件结构对应表、将要执行的脚本、项目依赖库的文件结构对应表写成文件,方便后面使用;并且创建一个 .app 包,后面编译后的文件都会被放入包中;

2、运行预设脚本:Cocoapods 会预设一些脚本,当然你也可以自己预设一些脚本来运行。这些脚本都在 Build Phases 中可以看到;

3、编译文件:针对每一个文件进行编译,生成可执行文件 Mach-O,这过程 LLVM 的完整流程,前端、优化器、后端;

4、链接文件:将项目中的多个可执行文件合并成一个文件;

5、拷贝资源文件:将项目中的资源文件拷贝到目标包;

6、编译 storyboard 文件:storyboard 文件也是会被编译的;

7、链接 storyboard 文件:将编译后的 storyboard 文件链接成一个文件;

8、编译 Asset 文件:我们的图片如果使用 Assets.xcassets 来管理图片,那么这些图片将会被编译成机器码,除了 icon 和 launchImage;

9、运行 Cocoapods 脚本:将在编译项目之前已经编译好的依赖库和相关资源拷贝到包中。

10、生成 .app 包

11、将 Swift 静态库拷贝到包中

12 、对包进行签名

13、完成打包

在上述流程中:2 - 9 步骤的数量和顺序并不固定,这个过程可以在 Build Phases 中指定。Phases:阶段、步骤。这个 Tab 的意思就是编译步骤。其实不仅我们的整个编译步骤和顺序可以被设定,包括编译过程中的编译规则(Build Rules)和具体步骤的参数(Build Settings),在对应的 Tab 都可以看到。

本地存储的方式

1、NSKeyedArchiver归档(NSCoding)序列化

2、NSUserDefaults:用来保存应用程序设置和属性、用户保存的数据。

3、NSFileManager write 的方式直接写入磁盘

4、SQLite:采用SQLite数据库来存储数据

5、coredata

6、keychain:跨APP

NSUserDefaults与plist的区别

  • NSUserDefaults是在Library/Preferences目录下的一个plist文件,其它plist一般存储在documents文件夹下
  • NSUserDefaults写入数据后并不是第一时间存入,使用[userDefaults synchronize]可以同步写入。

优化相关

1、包优化

  • 检测未使用的图片、ImageOptim图片无损压缩
  • 减少未使用到的文件

2、性能优化

  • instruments leaks:内存泄漏检测
  • Color Blended Layers:检测透明图层间的混合,红色区域为混合区域。

解决:设置opaque=true,或者设置一个不透明的颜色

  • Color Misaligned Images:图片大小和UIImageView的大小不匹配,黄色区域为不匹配的图片。
  • Color Offscreen-Rendered Yellow:离屏渲染区域

3、错误处理

  • 利用category+runtime处理数组数组越界问题。

swift闭包

1、自动闭包

//函数声明 参数是一个自动闭包 
func removeNameAuto(nameIndex: @autoclosure ()->String){
    print("auto ameArray first name is \(nameIndex())")
}
//函数调用并传入一个普通的表达式,因为是自动的闭包,会将该普通的表达式自动转化为闭包传入。

removeNameAuto(nameIndex: nameArray.remove(at: 0))

swift消息派发方式

一、直接派发
  • 全局函数
  • static修饰的函数
  • final修饰的函数或者类里面的所有方法
  • 使用private修饰的方法或者属性,会隐式声明final
  • 值类型方法,struct enum都是值类型
  • extension中没有使用@objc修饰的实例方法
二、函数表派发

类的方法默认使用函数派发的方式

三、消息派发

方法前面加上dynamic来支持消息派发(注意@objc只是用于把方法暴露给ObjectiveC,使用的还是函数表派发方式)

四、协议Protocol

协议所指向的对象,只有在运行时才能确定类型,Swift对于协议默认都使用函数表派发

五、NSObject
  • 对于普通的实例方法,使用函数表派发
  • 对于使用@objc声明的方法,会暴露给ObjectiveC,还是使用函数表派发
  • 对于overrideOC方法,使用消息派发
  • 对于extension方法,默认使用直接派发
  • 使用dynamic修饰的方法使用消息派发

String和NSString区别

  • string采用Unicode编码对表情支持更好,NSString采用ASCII
  • string是struct实现的是值类型,NSString是NSObject类型
  • string实现了CollectionType协议

Swift中Struct和Class的区别

  • struct是值类型、class是对象类型
  • struct定义成员是可以不用设置初始值
  • struct有默认初始化方法
  • 在struct中的方法中修改属性需要添加mutating标识
  • class可以实现继承

swift中的闭包和oc的block

  • oc默认截获值,swift默认截获变量的引用。都会强引用被截获的变量。
  • swift没有__block,但多了截获列表,通过将截获的变量置为weak。例如:[weak self]

静态库和动态库

1、释义

静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。

动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。

2、存在形式

静态库:.a.framework

动态库:.framework.dylib

自己创建的.framework库是静态库,系统的.framework是动态库。

3、.a .framework区别

.a是一个纯二进制文件,.framework中除了有二进制文件之外还有资源文件。

.a文件不能直接使用,至少要有.h文件配合,.framework文件可以直接使用。

.a + .h + sourceFile = .framework

建议用.framework.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容