iOS开发,面试有这些就足够了!(含答案)

iOS面试合集
这个栏目将持续更新--请iOS的小伙伴关注!

为什么我要写这篇文章呢?

首先是为了提升自己,因为自己在小公司,不想变成纯业务型程序员;
然后是公司闲暇时间多,有时间去了解并整理这些;
其实是自己有一个开发群,很容易收集问题;
最后是把整个文章当成一个笔记的作用,时不时的翻阅。
问题列表汇总,你可以复制其中的问题全文搜索找到对应的答案。

有记录面试题, 希望对你们有帮助~! 少走一些弯路!

问题目录

  1. 面向对象设计原则
  2. iOS应用导航模式有哪些
  3. iOS持久化方式有哪些NSClassFromString加载静态库中的类什么情况是nilid和NSObject*的区别
  4. 简单描述一下Runtime
  5. Runtime给类添加属性、成员变量
  6. KVO原理
  7. Property修饰符
  8. 程序内存分区extern的作用
  9. 指针函数/函数指针/Block
  10. __weak、__strong、__block理解
  11. 事件传递链/事件响应链
  12. 简述RunLoopNSTimer原理
  13. 简述GCD
  14. 自动释放池
  15. iOS中的定时器UIView/UILayer关系
  16. 简述你了解的锁
  17. ISO七层、TCP/IP四层协议
  18. 什么是ARC
  19. iOS类和结构体有什么区别
  20. iOS通知和协议的区别
  21. iOS内存使用注意事项和优化
  22. ViewController完整生命周期
  23. frame和bounds区别@synthesize和@dynamic的作用
  24. SDWebImage作用
  25. XML解析方式
  26. AFNetWorking作用
  27. Http协议特点,GET/POST请求区别
  28. Socket连接和Http连接区别
  29. Tcp三次握手、四次挥手
  30. performSelector传三个参数(未解答)
  31. main方法前过程
  32. 线程安全方法NSOperationQueue和GCD区别联系
  33. iOS常用设计模式
  34. 简述Block
  35. 消息动态处理/转发流程weak变量怎么置为nil对nil发消息会发生什么
  36. 安全区域的理解UITableView优化方法
  37. ssl/tls证书作用
  38. MVC、MVP、MVVM
    ———————————————————————————————————————————————

由于答案太多,我做了一个PDF文档,由于简书不能上传文件,需要答案可以加小编我的iOS交流群761407670,密码‘000‘’在里面获取文档,也欢迎招聘者,找工作的来,提供一个更大的平台

回答 答案↓↓↓↓↓

面向对象设计原则

单一职责原则,开闭原则,依赖倒置原则(面向接口编程),迪米特原则,里氏替换原则,接口隔离原则。

了解更多

谈谈面向对象设计(OOD)原则

iOS应用导航模式有哪些

这个问题更多是设计人员考虑的,不过我们也需要了解,不然我们都不知道UITabBarController和UINavigationController等存在的意义是啥。
iOS应用属于客户端应用,问题其实是问下面两个部分:

1:什么是导航模式?客户端导航模式有哪些常见的?
2:iOS中存在哪些导航模式?
导航模式

导航模式:将信息以最优的方式组织起来展现给用户。
客户端常见模式:tab、抽屉、列表、平铺/轮播、宫格和悬浮icon等。
注:不要太在意名称,你会在网上搜到一种模式有多种名称。

了解更多

移动端导航的七种设计模式
8种移动APP导航设计模式大对比

iOS中导航模式

这里并不是问你哪些控件/控制器对应这些导航模式,所以iOS具有上面提到的所有导航模式。

iOS持久化方式有哪些

首先这里的持久化指的是数据持久化,目前客户端的持久化也只有这一个含义。
为何要持久化:iOS开发可以没有持久化,持久化更多的是业务需求;比如记录用户是否登陆,下次进应用不需要再登陆。
因为iOS的沙盒机制,所以持久化分为两类:沙盒内和沙盒外。

沙盒内
NSKeyedArchiver

只要遵循了NSCoding协议并正确实现了initWithCoder和encodeWithCoder方法的类都可以通过NSKeyedArchiver来序列化。
归档使用archiveRootObject,解归档使用unarchiveObjectWithFile;需要指定文件路径。

NSUserDefaults

[NSUserDefaults standardUserDefaults]获取NSUserDefaults对象,以key-value方式进行持久化操作。

plist

写入使用writeToFile,读取使用xxxWithContentsOfFile;需要指定文件路径。

数据库

数据库无疑是大量数据最好的持久化方案,数据库目前有:sqlite、CoreData和Realm等。这里就不用回答FMDB它只是封装了sqlite而已。

文件

这里要和plist区分一下,plist方式是字典/数组数据格式写入文件;而这里的文件方式不限数据格式。

沙盒外
KeyChain

沙盒内的方式在应用被删除后数据都会丢失,如果想要不丢失则需要使用KeyChain。
KeyChain本质是一个sqlite数据库,其保存的所有数据都是加密过的。
KeyChain分为私有和公有,公有则需要指定group,一个group中的应用可以共享此KeyChain。
使用KeyChain过程中要理解下面几个问题:

1:自己使用的KeyChain和系统自带的KeyChain数据是隔离的,内部应该是不同数据库文件;
2:KeyChain数据可备份到iCloud中;
3:不需要联网,也不用登陆iCloud账号;一个设备一个sqlite数据库,但是不同应用组不共享数据;
4:要在另一台设备上使用当前设备存储的KeyChain信息,需要当前设备进行数据备份,
再在另一设备上复原数据;比较常用的是iCloud备份方式;
5:系统自带的KeyChain中账号密码分类数据可在系统设置->账号与密码里面看到,
你退出iCloud账号还是存在,只是iCloud会帮你备份如果你设置了的话;这个和照片是一样的道理。
了解更多

持久化
iOS 数据持久化的几种方法
聊聊iOS KeyChain

NSClassFromString加载静态库中的类什么情况是nil

NSClassFromString动态加载是OC中runtime的一个方法,用来从字符串得到一个class对象,当系统给应用分配的运行内存中没有这个类时会返回nil;静态库在链接阶段会被写入到执行文件,这里要注意了,如果工程中没有用到静态库中的某些类,那么这些类是不会写入到执行文件的,自然系统给应用分配的运行内存中没有这个类。所以NSClassFromString返回nil只在工程中没有使用到该类的情况下。
有人可能会问了,那我可以在运行的时候手动加载库到运行内存吗?动态库是可以的,这样就是插件化了;静态库因为最后打包的包里没有这个文件了,所以没办法获取到该静态库。而动态库在工程General的Embedded Binaries中加入该动态库,则打包后包内有一个framework文件夹专门放动态库,则可以实现手动加载。
又有人问了,General的Linked Frameworks and Libraries又是什么作用呢?好吧一般我们都忽略这个了,因为拖入库到工程默认就会把该库加入到此处,如果不加且你工程直接使用了该类则build通不过;使用workspace设置工程依赖实现组件化等为了解决相应问题而使用此实现思路情况下,要在适当的project的此处手动添加被依赖库,如果不加且你工程直接使用了该类则build通不过。

了解更多

NSClassFromString类的动态加载
iOS中的库

id和NSObject*的区别

这个问题没有固定的答案,只需要答到比较重要的点就可以了。
你可以从<objc .h="" style="box-sizing: border-box;">中看到如下的内容。
id的定义:</objc>

typedef struct objc_object {  
    struct objc_class *isa;  } *id;

NSObject的定义:

@interface NSObject  {    struct objc_class *isa;
}

开始分析得先知道这样一个事实,iOS中不是所有的类都继承自NSObject:

@interface NSProxy  {    struct objc_class *isa;
}

所以也就得出了答案:id可以指向oc中的任何对象,而NSObject*只能指向NSObject及子类对象。

了解更多

简单描述一下Runtime

Runtime是一个运行时系统,用来执行编译链接后的可执行文件;它将很多静态语言在编译和链接时期做的事放到了运行时来处理。这种我们写代码更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。
objc_class的定义:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;    const char * _Nonnull name                               OBJC2_UNAVAILABLE;    long version                                             OBJC2_UNAVAILABLE;    long info                                                OBJC2_UNAVAILABLE;    long instance_size                                       OBJC2_UNAVAILABLE;    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists  OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;};

从中可以看出,我们可以修改一些值达到运行时改变原有行为的目的;比如给对象调用方法是从methodLists查找方法实现等。
延伸内容举个例子:Runtime是怎么对对象发送消息的呢?
先需要了解object_class中isa和super_class指的是什么。

image

其中图最左边列表示类的实例对象,中间列表示类,右边列表示元类。

最左边实例对象只存在一个objc_object结构体;
类则有objc_object和objc_class两个结构体,因为类是元类的实例对象;
元类则只有一个objc_class结构体;
对类、实例对象调用方法都是从objc_object中isa查找。

接下来我们来分析MyClass *myClass = [[MyClass alloc] init];[myClass test]。
首先我们要拆分成MyClass *myClass = [MyClass alloc];myClass = [myClass init];[myClass test]。

1:先会执行[MyClass alloc]语句,这是对MyClass类调用类方法,MyClass也是MyClass元类的实例对象,
同样也有objc_object结构体,objc_object结构体中isa指向MyClass元类;MyClass元类也是一个类,
在其objc_class中的methodLists中并没有发现alloc方法;
2:则从MyClass元类的objc_class中super_class进行递归查找,最终在NSObject元类中找到alloc方法;
3:[MyClass alloc]返回MyClass的实例对象myClass,这样myClass就有了一个objc_object结构体,
objc_object是通过alloc中class_createInstance创建的,isa指针指向MyClass类;
4:对myClass调用init,因为myClass的objc_object中isa指向MyClass类,所以会在MyClass类的
objc_class中methodLists进行查找,发现没有,则去MyClass类的objc_class中
super_class进行递归查找,最终在NSObject类中找到init方法,init只是做一个初始化的操作,
返回自身;这里要注意是从类中不是元类中去查找,因为是对实例对象调用方法;
5:这样实例对象分配空间和初始化就完成了,接下来是对myClass调用test方法,
因为test就是MyClass类中定义的,前面说了myClass的objc_object中isa指向MyClass类,所以直接
就在MyClass的objc_class中methodLists找到了test方法,直接执行test;
6:整个过程执行完毕。

那么我怎么获取objc_class中isa和super_class指向谁呢?
可以用objc_getClass获取isa,用class_getSuperclass获取super_class,class方法则只会返回类本身。

    //ClassTwo : ClassOne,ClassOne : NSObject
    //获取ClassTwo对应的元类的super_class
    Class currentClass = objc_getMetaClass("ClassTwo");    //打印这些元类的super_class ClassTwo ClassOne NSObject NSObject nil 
    //基元类的super_calss指向基类 基类的super_class为nil 形成闭环
    for (int i = 0; i < 5; i++) {        NSLog(@"Following the super_class pointer %d times gives %p", i, currentClass);
        currentClass = class_getSuperclass(currentClass);
    }    //打印ClassTwo类的super_class指向 ClassTwo ClassOne NSObject nil
    currentClass = [ClassTwo class];    for (int i = 0; i < 4; i++) {        NSLog(@"Following the super_class pointer %d times gives %p", i, currentClass);
        currentClass = class_getSuperclass(currentClass);
    }    //打印ClassTwo的isa指向 ClassTwo nil nil nil 任何元类的isa指向基类的元类,就是nil
    currentClass = [ClassTwo class];    for (int i = 0; i < 4; i++) {        NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
        currentClass = objc_getClass((__bridge void *)currentClass);
    }
了解更多

Objective-C Runtime 运行时之一:类与对象
Objective-C对象模型及应用
iOS中isa的深层理解
Objective-C 中的元类(meta class)是什么?
iOS底层原理总结 - 探寻Class的本质

Runtime给类添加属性、成员变量

添加属性

可以给任意类添加属性,用class_addProperty。
这里需要注意了,如果我们在类定义中加的属性,那么编译器会默认生成一个成员变量和getter/setter方法,如果我们动态加属性,则只是表示这是一个属性罢了,我们需要再添加对应的成员变量和getter/setter方法才能正常使用。

添加成员变量

只能给动态添加类添加成员变量,用class_addIvar;该类不能是元类。

添加关联对象

可以给任意类添加关联对象达到添加"属性"作用,用objc_setAssociatedObject。

//设置objc_setAssociatedObject(self, (const void *)"key", @(1), OBJC_ASSOCIATION_ASSIGN);//获取id object =objc_getAssociatedObject(self, (const void *)"key");
了解更多

对象关联的使用objc_setAssociatedObject
Ivar 详解

KVO原理

要了解KVO,我们得知道KVC和KeyPath。
KeyPath:键路径;运行时系统根据键路径找到最后的属性/成员变量/关联对象进行相应的操作。
KVC:允许开发者通过Key名直接访问对象的属性/成员变量/关联对象;并有一组api供开发者使用,像操作字典一样操作对象属性/成员变量/关联对象。

...
- (void)setValue:(nullable id)value forKey:(NSString *)key
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath
...

为什么说是属性/成员变量/关联对象呢?

声明属性编译器默认会给我们生成对应的私有成员变量,其实属性就是私有成员变量+getter+setter罢了;
这里我们不考虑幺蛾子情况,比如声明了两个属性year和month,你又写了这样的代码@synthesize year = 
month;那么不好意思,这样的话编译器不会生成_month和_year成员变量了,也不会生成month的getter和setter方法,只会生成一个month成员变量和year的getter和setter方法,操作self.year
就相当于操作了month成员变量;

成员变量是自己写在类扩展、类定义或类实现后成员变量声明区中的变量;类定义中声明的默认是受保护
类型,类扩展和类实现声明的默认是私有类型;任何类型的变量都可以被子类继承;
也可用Runtime修改/获取值。

关联对象是如果在分类中声明了"属性"且本类中没声明,如果本类中声明了那么操作的将会是本类中的属性,
编译器不会生成对应的成员变量,只是生成了getter和setter罢了;KeyPath了分类中的"属性"其实是调用了分类中对应"属性"重写的getter和setter罢了,
内部实现一般是设置/返回关联对象的值。

这里需要注意了。如果我们声明一个属性为year,那么你下面两种方式都可以修改到year的值,理由上面已经说过了。

[xxx setValue:@(10) forKey:@"year"];
[xxx setValue:@(20) forKey:@"_year"];

如果你声明了属性year,你又添加了一个成员变量year,那么将会有year和_year两个成员变量,self.year操作的_year成员变量。你可以加@synthesize year = year告诉编译器year属性使用year成员变量而不用再生成_year;那么下面的这句话就会崩溃。

[xxx setValue:@(20) forKey:@"_year"];

KVO:键值对观察者,在监听属性值变化时发出一个通知给监听者。

...
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath 
    options:(NSKeyValueObservingOptions)options context:(nullable void *)context
...

系统提供的接口,在新/旧值一样时也会发出通知。
实现KVO方式很多种,我们看看系统KVO的内部大概实现思路。

假设有一个Person类,该类有一个name属性。执行下面语句的时候。
[person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:nil];
其实系统动态创建了一个子类NSKVONotifying_Person,并把person的isa指针指向了NSKVONotifying_Person;如果我们打印出NSKVONotifying_Person所以方
法,可以得到这么几个:setName:、class、dealloc和_isKVOA。重点是setName:方法,我们打印
setName:的内容为:(Foundation`_NSSetObjectValueAndNotify);这个是一个私有方法,不过我们
不难猜到通知是从这个方法发出来的。那么当我们改变person的name属性时,其实是走了NSKVONotifying_Person的setName:方法,该方法先调用Person的setName:方法给name赋值后
发出一个通知给监听者。

我看了一些网上自己实现KVO的做法,网上的大部分实现只能给实例添加一个监听对象、不支持实例变量添加KVO、不支持keyPath;要自己仿写KVO其实是非常困难。

了解更多

iOS开发技巧系列---详解KVC(我告诉你KVC的一切)
iOS KVO的底层实现原理
KVO 的内部实现

Property修饰符

这里我们讲ARC环境下;修饰符主要分为下面的几类。

原子性

nonatomic:原子性访问,对属性赋值的时候不加锁,多线程并发访问会提高性能。
atomic:属性默认为atomic,提供多线程安全,在多线程环境下,原子操作是必要的,否则有可能引起错误的结果。

在iOS开发中,几乎所有属性都声明为 nonatomic。
atomic的作用只是给getter和setter加了个锁,atomic只能保证代码进入getter或者setter函数内部时是安全的,一旦出了getter和setter,多线程安全只能靠程序员自己保障了。
读/写

readwrite:同时产生setter/getter方法。
readonly:只产生简单的getter,没有setter。
有的朋友会问怎么没有writeonly?因为写权限包含了读权限。

引用计数

copy:目标对象引用计数不变,拷贝一份引用计数为1的对象,该属性指向拷贝对象。
weak:目标对象引用计数不变,该属性指向目标对象地址,当目标对象销毁时,该属性置为nil。
strong:目标对象引用计数+1,该属性指向目标对象地址。
对于NSString对象需要单独考虑。

NSString *a = @"abc";//@"abc"被放到常量区,对a对象copy和strong引用计数不会变化,a是NSCFConstantString类型。NSString *a = [NSString stringWithFormat:@"abc"];//在堆上分配内存得到的a是NSCFString对象,对a对象copy和strong都只是引用计数+1。

对于可变对象赋值给copy属性时会变成不可变对象。

能否为空

nonnull:对象不应该为nil;当赋值为nil时编译器会给出警告。

NS_ASSUME_NONNULL_BEGIN//之间的属性都被认为是nonnull的NS_ASSUME_NONNULL_END

nullable:对象可以为nil;属性默认是nullable的。

了解更多

iOS中property的关键字(史上最详解)
iOS属性常用关键字解析
NSString特性分析学习
iOS多线程到底不安全在哪里?

程序内存分区

以下是比较常用的五分区方式,当然也不排除网上有其他的分区方式。

栈的大小在编译时就已经确定了,一般是2M;栈是一块从高到低地址的连续区域,存放临时变量和执行函数时的内存等。栈内存分配分为动态和静态,静态如自动变量(局部变量)等,动态如alloc等。

堆是从低到高地址的不连续区域,类似链表;用来存放malloc或new申请的内存。

全局/静态

存放静态/全局变量;全局区细分为未初始化/初始化区。

常量

存放常量;程序中使用的常量会到常量区获取。
可以看看这个例子来理解一下。

...int a;//a在全局未初始化区int a = 10;//a在全局初始化区 10在常量区static int a = 10;//a在静态区 10在常量区//程序入口int main(...) {   int a = 10;//a在栈区 10在常量区
   static int a = 10;//a在静态区 10在常量区
   char *a = (char *)malloc(10); //a在栈区 malloc后的内存在堆区
   ...
}
代码

存放二进制代码,运行程序就是执行代码,代码要执行就要加载进内存(RAM运行内存)。

了解更多

iOS程序中的内存分配分区
iOS基础全局变量·静态变量·局部变量·自动变量

extern的作用

告诉编译器,这个全局变量在本文件找不到就去其他文件去找。如有必要需要使用#import "x.h"这样编译器才知道到哪里去找。

//.hint age = 10;//error 不能.h此处声明全局非静态变量,.m中可以extern int age = 10;//error 和int age = 10;等价extern static int age = 10;//全局静态变量声明不和extern一起用@interface Class : NSObject...@end//.mextern static int age = 10;//全局静态变量声明不和extern一起用@implementation Class {   int age;//成员变量不能用作extern;}
- (void)test {    extern int age = 10;//error 因为这并不是全局变量
    static int age = 10;//error 因为这并不是全局变量
    extern int age;//error 因为这并不是全局变量}
...@end

使用extern前要保证对应变量被编译过

//.hextern int age;//error extern在声明前extern static int age;//error extern没有static@interface Class : NSObject...@end//.mstatic int age = 10;@implementation Class ...@end
//.hstatic int age = 10;extern int age;//正确 @interface Class : NSObject...@end//.m@implementation Class ...
- (void)test {    extern int age;//正确 声明和extern可以在不同文件中}@end

全局非静态变量

//.hextern int age;//正确@interface Class : NSObject...@end//.mint age = 10;@implementation Class ...@end
了解更多

iOS开发中的关键字const/static/extern

指针函数/函数指针/Block

指针函数

C语言的概念;本质是函数,返回指针。

char *fun() {    char *p = "";    return p;
}
函数指针

C语言的概念;本质是指针,指向函数。

int fun(int a,int b) {    return a + b;
}int (*func)(int,int);
func = fun;
func(1,2);//3
Block

OC语言的概念;表示一个代码块,OC中视为对象;挺像C函数指针的。

//typedeftypedef int (^SumBlock)(int a,int b);
SumBlock sumBlock = ^(int a,int b) {    return a + b;
};
sumBlock(1,2);//3//普通
 int (^sumBlock)(int a,int b) = ^(int a,int b) {    return a + b;
};
sumBlock(1,2);//3
了解更多

指针函数与函数指针(C语言)
iOS开发-由浅至深学习block

__weak、__strong、__block理解

我们基本都是ARC环境,所以回答以ARC角度。

__block

讲这个之前,我们需要搞清楚一个概念,这个block存在内存什么区域的?
如果这个block内部没有访问栈、堆的变量,那么这个block存在代码区;反之存在堆区。block内部修改栈区的变量,该变量需要加__block修饰,这样会将变量从栈上复制到堆上。栈上那个变量会指向复制到堆上的变量。block内部修改堆区的变量不用加__block。
因为堆区不断有变量创建和销毁,block作为属性时我们需要加copy或者strong修饰。

__weak

__weak我们就在block和声明属性中看到过。
如block是被self强引用的。

@property (nonatomic, copy) void (^Block)(void);

那么在Block内部使用self时,Block内部又会对self进行一次强引用;这就形成了循环引用,所以需要对self进行__weak。

__weak typeof(self) weakSelf = self;

弱引用不会影响对象的释放,当对象被释放时,所有指向它的弱引用都会自定被置为nil。
当然了,self没有强引用block时是不需要__weak的。

- (void)func() {    void (^Block)(void) = ^(void) {
        [self test];
    };
}
__strong

对self进行了__weak,那么在block执行时weakSelf随时可能被释放,所以内部需要对weakSelf进行__strong让self不被释放。

__strong typeof(self) strongSelf = weakSelf;

在block执行完成后,strongSelf会被释放,不会造成循环引用。

了解更多

iOS中block块的存储位置&内存管理
__block & __weak & __strong

事件传递链/事件响应链

事件传递链

当点击一个按钮的时候,事件如果传递到按钮这个第一响应者上,这就是事件传递链要做的事情。系统根据下面两个方法来传递事件。

//该点是否在本视图点击范围内 point已经被转换了成本视图对应frame- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {    //内部实现大概是这样
    return CGRectContainsPoint(self.bounds, point);
}//本视图/子视图是否能够传递本事件 point已经被转换了成本视图对应frame- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {    //内部实现大概是这样
    //用户交互为NO,不处理
    if(self.userInteractionEnabled == NO) {        return nil;
    }    if([self pointInside:point withEvent:event]) {        NSArray * superViews = self.subviews;        //倒序从最上面的一个视图开始查找
        for (NSUInteger i = superViews.count; i > 0; i--) {            UIView * subview = superViews[i - 1];            //转换坐标系 使坐标基于子视图
            CGPoint newPoint = [self convertPoint:point toView:subview];            //得到子视图 hitTest 方法返回的值
            UIView * view = [subview hitTest:newPoint withEvent:event];            //如果子视图返回一个view 就直接返回 不在继续遍历
            if (view) {                return view;
            }
        }        //所有子视图都没有返回 则返回自身
        return self;
    }    return nil;
}

当点击按钮的时候,其实事件是这样传递的:AppDelegate->UIApplication->UIWindow->xxx->UIViewController->UIView->UIButton。

事件响应链

当找到事件第一响应者之后,该事件如何响应,就是事件响应链要做的事情。
接着上面的例子,UIButton就是系统找出来的第一响应者,那么会执行如下方法:

//触摸事件开始- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {}//触摸事件移动- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {}//触摸事件结束- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {}//触摸事件取消- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {}

如果你自己不处理,你可以self.nextResponder让下一个响应者处理。

//触摸事件开始- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {    //让下一个响应者处理
    [self.nextResponder touchesBegan:touches withEvent:event];
}
...

响应者链也就是传递链的倒序。
这里需要注意的就是,如果给UIButton添加了target和UITapGestureRecognizer那么点击按钮只会执行UITapGestureRecognizer,也就是说如果手势和target同时满足条件则只会执行手势。target也是touchesxxx中判定的,你可以重写touchesxxx内部实现为空,你会发现并不影响手势但会影响target。

了解更多

iOS事件拦截和事件转发
UIView之userInteractionEnabled属性介绍
iOS触摸事件那点儿事

简述RunLoop

RunLoop是iOS中的Event Loop实现,简单来说是一个do while循环,需要GCD等协作执行;循环体内没事件需要处理就休眠,被mach_port唤醒之后处理相应事件后判断条件继续进入循环。一个线程只能有一个根RunLoop,RunLoop保存在TSD中;一次RunLoop执行只能指定一个RunLoopMode,mode有timer、source、common和observer等;几乎所有的操作都是通过Call out方法进行回调的,比如点击是通过source1到source0再到action回调;要切换mode必须退出当前RunLoop并指定新mode重新执行。

了解更多

iOS刨根问底-深入理解RunLoop
Run Loop 记录与源码注释
深入理解RunLoop
RunLoop 源码阅读

NSTimer原理

有容错值用gcd timer实现,反之用mk_timer实现,mk_timer更准确;触发点并不是一开始就计算出的,而是每次触发后动态计算;当RunLoop执行一个阻塞操作时,触发点可能延迟,可能会跳过中间的触发点。

了解更多

从RunLoop源码探索NSTimer的实现原理

简述GCD

iOS的一个多核调度器,用于优化应用程序以支持多核处理器;内部默认创建一个串行主队列和12种不同优先级的并发队列,可以自己创建默认优先级为Default的串行/并发队列;得到队列后可以向队列同步/异步方式添加任务,异步GCD会按需创建线程;向主队列添加的任务将由主线程RunLoop处理;GCD还可以实现定时器、延迟、栅栏、信号量和组等。

了解更多

iOS多线程:『GCD』详尽总结
iOS GCD之dispatch_semaphore(信号量 )
GCD源码分析

自动释放池

释放池是由n个page组成的双向链表,线程和释放池一一对应;释放池push时会放入哨兵对象,根据next指针放置添加进来的自动释放对象;释放池pop时会将hotpage中next指针依次向前移动,对所指对象调用release直到遇到结束标志,清理过程可跨越page;从main方法中知道iOS项目默认是包裹在大的释放池中;RunLoop开始循环、休眠和退出时会对释放池进行poolpush/poolpop操作。

了解更多

理解 iOS 的内存管理
iOS 自动释放池原理探究
黑幕背后的Autorelease
iOS中autorelease的那些事儿

iOS中的定时器

NSTimer、GCD定时器和CADisplayLink。
NSTimer根据容错值使用GCD定时器或mk_timer。
NSTimer和CADisplayLink依赖于RunLoop,GCD定时器不依赖。

了解更多

iOS定时器 NSTimer、CADisplayLink、GCD

UIView/CALayer关系

view是layer的代理对象;view负责管理layer,layer负责渲染;view初始化的时候默认会创建一个layer;设置view的frame和bounds等内部其实是修改layer对应属性。

了解更多

详解 CALayer 和 UIView 的区别和联系

简述你了解的锁

互斥锁:NSLock、pthread_mutex、@synchronized。

加锁后,其他加锁操作阻塞直到解锁。

递归锁:NSRecursiveLock。

一个线程可以多次加锁,相应的要对应多次解锁其他线程才可以加锁。

条件锁:NSCondition、NSConditionLock。

锁满足指定条件时才继续执行,否则阻塞。

信号量:dispatch_semaphore。

wait操作阻塞直到signal被调用。

读写锁:pthread_rwlock。

读模式占有锁时其他线程只能读;写模式占有锁时其他线程不能进行任何操作。
了解更多

iOS中的各种锁

ISO七层、TCP/IP四层协议

ISO七层协议

应用、表示、会话、传输、网络、数据链路、物理。

TCP/IP四层协议

应用、传输、网络、数据链路。

传输层单位:段;
网络层单位:报;
链路层单位:帧。
了解更多

TCP/IP 知识点整理

什么是ARC

ARC是引用计数,是一个简单而有效的管理对象生命周期的方式;编译器在代码合适的地方自动给我们加了一些关键字,比如:retain、release和autorelease等;这样我们就不用手动管理对象生命周期。

了解更多

理解 iOS 的内存管理

iOS类和结构体有什么区别

区别还是有很多的,答到核心的就可以了。
1:类指针赋值时只是复制了地址,结构体是复制内容;
2:类不能有同名同参数个数的方法,结构体可以;
3:结构体方法实现编译时就确定了,类方法实现可动态改变;
4:内存分配不一样,结构体在栈,类在堆;
5:结构体可以多重继承,类只能单继承。

网上很多文章说结构体不能有方法,结构体不能继承;
请看清楚题,说的是iOS中的结构体,不是C中的结构体。
了解更多

iOS通知和协议的区别

一个协议一时间只能有一个代理对象,而一个通知一时间可以有多个监听者。
通知的发送和监听依靠通知中心,协议则可以自己创建,通过setDelegate指定代理对象。

了解更多

IOS中的协议Protocol与代理Delegate以及通知

iOS内存使用注意事项和优化

使用注意事项

访问野指针:数据越界、对象已经释放但对其发送消息等;
内存泄漏:循环引用、imageNamed读取图片等;
触碰内存峰值:for循环声明变量等;
申请了不使用的内存:声明变量但未使用、只在某个逻辑分支用到某些变量但一开始就初始化等。

内存优化

访问野指针:访问前加判断;

这部分问题大多是多线程造成的,比如两个线程同时执行一个方法,方法内部对数组有更新操作。

内存泄漏:Instrument Leaks/Allocations检测、使用imageWithContentsOfFile读取图片等;

imageNamed会缓存加载的图片;imageWithContentsOfFile只是简单的加载图片。

触碰内存峰值:手动添加释放池;

for循环内大量创建局部变量,这些局部变量会等到RunLoop的下一个循环才释放,
而手动加入释放池则会提前释放。

申请了不使用的内存:懒加载。

了解更多

ViewController完整生命周期

init
loadView
viewDidLoad
viewWillAppear
viewDidAppear
viewWillDisappear
viewDisDisappear
viewWillUnload(Deprecated)
viewDidUnload(Deprecated)
delloc

这里要注意loadView方法,平时我们都没有去管这个事情;
这个方法执行后self.view才有值,也就是说这个方法完成了view的加载,内部会根据控制器名字、是否自己实现、xib等条件来加载view。

如果自己实现loadView方法,并且方法体为空,则self.view为nil,
并且viewDidLoad方法会调用两次;
这说明viewDidLoad是被loadView调起的,在viewDidLoad中如果self.view为nil会再调用一次loadView。

延伸了解。

viewWillUnload和viewDidUnload被弃用了,内部是在清理self.view。

storyboard加载的是控制器+View,而xib加载的是View,也就是说storyboard加载时会调用
ViewController的awakeFromNib方法。
awakeFromNib:表示ViewController/View从xib加载。
了解更多

iOS开发笔记(九):UIViewController的生命周期

frame和bounds区别

frame表示在superview坐标系中的位置和大小,bounds表示自身坐标系,默认左上角为0,0;
bounds给subview参考,其结合自身frame确定显示位置;
改变bounds不会改变自己在superview中的位置,但会改变subview的位置。

改变bounds时如果宽高和frame宽高不一致则会以center为中心缩放,此时将改变frame并重新显示。
了解更多

frame和bounds的区别

@synthesize和@dynamic的作用

@synthesize

如果属性没有自动生成getter/setter方法,则告诉编译器去生成。

@dynamic

告诉编译器不要生成此属性的getter/setter方法,开发者自己去实现。

了解更多

iOS @property、@synthesize和@dynamic

SDWebImage作用

先了解一下NSURLCache。

NSURLCache默认会对部分GET请求进行缓存;
请求一张图片,第一次请求成功后NSURLCache会缓存图片内容,第二次请求的时候直接从缓存中取就可以了,
并没有真正发起请求;NSURLCache系统也是默认启动的。

看了上面的介绍你发现这不就是你最终要实现的功能吗?那SDWebImage到底做了什么呢?

1:正在下载该url的图片,直接返回;2:处理多线程问题,比如cell复用造成的显示错乱问题,也就是下载之前先取消;3:增加一层内存缓存,直接从url得到image,NSURLCache存的是data;4:相同url请求时只加了一个finish回调;5:请求做了排队处理,控制资源。

所以可以把SDWebImage看成是NSURLCache的封装,等同于NSOperationQueue和GCD的关系。

了解更多

XML解析方式

SAX:基于事件驱动,逐行解析,采用协议回调;文件比较大时建议用此方式。
DOM:文档对象模型,解析时将整个文档读入并结构化成树,通过树状结构读取相关数据;文件比较小时建议用此方式。

了解更多

iOS - XML解析

AFNetWorking作用

我看的是AFNetWorking 3.0版本,是基于NSURLSession封装的,NSURLSession使用起来已经足够方便了,所以AFNetWorking做的内容相对来说少了很多。

1:设置请求/响应序列化对象,这样可以帮你检查请求/响应参数是否正确;
2:https验证功能,可以用于自签名证书等特殊情况,如果不启用则让系统帮你验证,
那么就只能是苹果官方认可的CA证书才能通过;
3:对于不同的请求方式和参数,帮你设置请求,比如头部的Content-type。
了解更多

Http协议特点,GET/POST请求区别

Http协议特点,基于Http2.0

快速:协议简单,通讯速度快;
灵活:通过Content-Type可以传递任何类型的数据;
长连接:一次连接处理多个请求,并支持管线(同时发出多个请求,不用等到前面请求响应)、多路复用(一个请求多次响应);
无状态:协议对于事务处理没有记忆能力,如果本次请求需要上次的数据则需要重传。

GET/POST请求区别

GET在URL后以?形式拼接参数,POST将参数放在body中;

GET也可以在body里面放数据,POST也可以在URL放数据,Http只是规定,你也可以不遵守啊。
至于服务器获不获取就看具体实现了。

POST比GET安全,因为GET参数在URL中,用户一眼就能看到;

POST当然也可以用工具看到参数。

语意理解,GET用来获取数据,POST用来上传数据;
客户端和服务器对URL数据一般有1kb的限制。

Http协议对URL并没有限制。
了解更多

iOS-网络编程(一)HTTP协议
http2.0的时代真的来了...

Socket连接和Http连接区别

Http是基于Tcp的,而Socket是一套编程接口让我们更方便的使用Tcp/Ip协议;Http是应用层协议,在Tcp/Udp上一层。
1:Http是基于"请求-响应"的,服务器不能主动向客户端推送数据,只能借助客户端请求到后向客户端推送数据,而Sokcet双方随时可以互发数据;
2:Http不是持久连接的,Socket用Tcp是持久连接;
3:Http基于Tcp,Socket可以基于Tcp/Udp;
4:Http连接是通过Socket实现的;
5:Http连接后发送的数据必须满足Http协议规定的格式:请求头、请求头和请求体,而Socket连接后发送的数据没有格式要求。

了解更多

TCP连接、HTTP连接与Socket连接的区别

Tcp三次握手、四次挥手

Tcp是传输层可靠传输协议,发送数据前要进行握手,数据发送完时可以挥手断开连接释放资源;挥手比握手多一次是因为断开需要双方单独断开,而握手被连接端是被动打开的。
首先,我们必须要明白一些重要的Tcp头部标志。

序号:每一个包都有一个序号,此序号mod2^32;
确认号:用来确认收到对方的包,此序号mod2^32;
SYN:为1时表示希望与对方建立连接;
FIN:为1时表示我已经没有数据发送了,希望断开连接;
ACK:为1时确认号有效,Tcp规定除了第一次建立连接的包ACK都要置为1。

这是我抓的访问百度握手/挥手过程,Http协议也是基于Tcp的。

10.10.9.141:客户端ip;180.97.33.107:百度ip。
第一次握手 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 64)
    10.10.9.141.50806 > 180.97.33.107.http: Flags [S], cksum 0x95b1 (correct), seq 589117916, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 815096723 ecr 0,sackOK,eol], length 0
第一次握手 SYN=1 seq=589117916 第二次握手 IP (tos 0x0, ttl 53, id 0, offset 0, flags [DF], proto TCP (6), length 64)
    180.97.33.107.http > 10.10.9.141.50806: Flags [S.], cksum 0x2cb0 (correct), seq 1775662715, ack 589117917, win 8192, options [mss 1408,nop,wscale 5,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,sackOK,eol], length 0
第二次握手 SYN=1 ACK=1 seq=1775662715 ack=589117917第三次握手 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 40)
    10.10.9.141.50806 > 180.97.33.107.http: Flags [.], cksum 0xa25d (correct), seq 589117917, ack 1775662716, win 8192, length 0
第三次握手 ACK=1 seq=589117917 ack=1775662716数据传输...
数据传输...

第一次挥手 IP (tos 0x0, ttl 50, id 43779, offset 0, flags [DF], proto TCP (6), length 40)
    180.97.33.107.http > 10.10.9.141.50806: Flags [F.], cksum 0xb3c8 (correct), seq 1775665497, ack 589118060, win 808, length 0
第一次挥手 FIN=1 ACK=1 seq=1775665497 ack=589118060第二次挥手 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 40)
    10.10.9.141.50806 > 180.97.33.107.http: Flags [.], cksum 0x973b (correct), seq 589118060, ack 1775665498, win 8117, length 0
第二次挥手 ACK=1 seq=589118060 ack=1775665498第三次挥手 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 40)
    10.10.9.141.50806 > 180.97.33.107.http: Flags [F.], cksum 0x96ef (correct), seq 589118060, ack 1775665498, win 8192, length 0
第三次挥手 ACK=1 FIN=1 seq=589118060 ack=1775665498第四次挥手 IP (tos 0x0, ttl 50, id 0, offset 0, flags [DF], proto TCP (6), length 40)
    180.97.33.107.http > 10.10.9.141.50806: Flags [.], cksum 0xb3c7 (correct), seq 1775665498, ack 589118061, win 808, length 0
第四次挥手 ACK=1 seq=1775665498 ack=589118061
了解更多

通俗大白话来理解TCP协议的三次握手和四次分手
tcpdump查看三次握手

performSelector传三个参数(未解答)

了解更多

performSelector多个参数

main方法前过程

1:系统内核做好准备工作,比如把应用数据从ROM移到RAM;
2:libdyld接管后续工作,加载动态库到内存等;
3:ImageLoader把动态库和可执行文件加载到内存;
4.1:Runtime解析文件符号表,注册类、调用load方法;
4.2:libdispath等其他库初始化;
5:libdyld调用main方法。

了解更多

iOS 程序 main 函数之前发生了什么

线程安全方法

线程安全:多线程环境下保证数据的完整性。

队列

把操作放入队列线性执行,可用GCD和NSOperationQueue。

锁/信号量

用锁/信号量形成操作互斥。

让操作原子化

让操作原子执行,系统提供了一些原子执行的方法。

了解更多

iOS-线程安全

NSOperationQueue和GCD区别联系

区别

NSOperationQueue没有串行/并发队列,但可以设置最大并发数;
NSOperationQueue支持方法和block,GCD只支持block;
NSOperationQueue可以暂停/取消操作;
NSOperationQueue支持更多的功能,比如KVO和自定义操作;
NSOperationQueue可以设置操作的优先级,GCD只能设置队列的优先级。

联系

提供的功能是相似的;
NSOperationQueue是GCD的封装。

了解更多

iOS多线程之NSOperationQueue

iOS常用设计模式

其实设计模式有很多,你只要答上6个就可以了。

装饰模式:分类;
代理模式:协议;
工厂模式:UIButton创建;
原型模式:[object copy];
观察者模式:KVO;
迭代器模式:数组的遍历;
单例模式:Appdelegate;
命令模式:给对象发消息;
职责链模式:事件传递链;
中介者模式:模块解耦;
解释器模式:Siri语意识别;
了解更多

IOS 设计模式探索:常用的 23 种设计模式

简述Block

Block本质就是函数,根据有无返回值/参数有4种Block;
在ARC下根据是否访问栈/堆变量可分为全局/堆Block;
Block内修改栈变量时需要__block修饰,__block的作用其实就是在堆上创建一个指向栈变量的指针达到修改栈变量值的目的。

了解更多

iOS Block 详解

消息动态处理/转发流程

动态处理

当向对象发送了一个不存在的消息时,会先走resolvexxxMethod动态处理流程,你可以在这之中用Runtime动态添加该方法;你也可以不处理,则进入转发流程。

转发

先走forwardingTargetForSelector,你可以在这里返回一个可以处理aSelector的对象,否则走forwardInvocation操作anInvocation;forwardInvocation需要实现methodSignatureForSelector得到一个方法签名。

了解更多

iOS runtime 之消息转发

weak变量怎么置为nil

系统维护了一个weak变量组成的hash表,key为weak指向的变量地址,value为weak变量的地址;当对象引用计数为0时,遍历hash表,设置对应的weak变量为nil。

了解更多

iOS开发 Runtime是如何实现weak属性的?

对nil发消息会发生什么

我们可以从Runtime的源码中看到,发消息最终会走objc_msgSend()并把nil最为第一个参数,其内部用汇编实现,里面会判断第一个参数是否为nil,如果为nil则返回0,所以iOS允许对nil发送消息;这个0针对不同的selector返回值有不同的表示,比如:0、nil和结构体等。

了解更多

Objective-C 消息发送与转发机制原理

安全区域的理解

SafeArea是View的属性,是iOS11出来用来代替bottomLayoutGuide/topLayoutGuide的,bottomLayoutGuide/topLayoutGuide是ViewController的属性;从这里可以看出,SafeArea更灵活,可以对每一个View进行配置;他们都是让控件不被父View遮挡系统自动计算的距离,你当然也可以关闭这个功能;iOS11前用automaticallyAdjustsScrollViewInsets,iOS11后用contentInsetAdjustmentBehavior。

了解更多

UITableView优化方法

cell重用,异步执行耗时操作这种就不用提了,这都是大家能想到的,我们可以说一点其他的。
1:减少视图层次;
2:正确使用api,比如设置rowHeight而不去取dataSource;
3:减少离屏渲染;

圆角图片
阴影
...

4:设置视图不透明减少渲染代价,如果有透明度则会根据多个视图才知道一个像素点显示什么颜色;
5:RunLoop空闲计算cell高度并缓存;
6:利用RunLoop的mode在滚动时不执行耗时操作。

了解更多

老生常谈之UITableView的性能优化

ssl/tls证书作用

如果没有ssl/tls证书,每个不同客户端访问服务器都需要生成一对密钥,这会造成服务器端存储的密钥太多等问题;让所用客户端使用统一的服务器ssl/tls证书则可以解决这些问题。
https=http+ssl/tls,拿我们客户端请求api来说,我们适配https时要后台给我们一个cer证书,这个证书里面包括了:颁发机构、有效期、RSA公钥和Hash指纹等信息;客户端会把这个证书交给请求库,请求库负责完成加/解密;为什么需要从颁发机构申请证书呢?因为我们的api和web可能在一个域名下,一个域名只能一个证书,也就是说如果只有api访问这个域名我们完全可以自己创建一个证书设置受信用;然而如果web也要访问该域名则浏览器不会认为这个证书受信用会拒绝访问。

这个栏目将持续更新--请iOS的小伙伴关注!

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

更多

1.直击2020——iOS 面试题大全(补充完整版)

2.“新”携程,阿里,腾讯iOS面试常见问题合集(附答案)

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