iOS概念知识

作者: xiny123 | 来源:发表于2018-08-07 13:41 被阅读161次

    面向对象的三大特征,并作简单的介绍。

    面向对象的三个基本特征是:封装、继承、多态。

    • 1.封装是面向对象的特征之一, 是对象和类概念的主要特性。 封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。隐藏对象的属性和实现细节,仅对外公开接口,提高代码安全性,封转程度越高,独立性越强,使用越方便
    • 2.继承是指这样一种能力: 它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。 通过继承创建的新类称为“子类”或“派生类”。 被继承的类称为“基类”、“父类”或“超类”
    • 3.多态性: 允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子 对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针

    When we call objective c is runtimelanguage what does it mean? 我们说的 obc是动态运行时语言是什么意思?

    多态。 主要是将数据类型的确定由编译时,推迟到了运行时。这个问题其实浅涉及到两个概念,运行时和多态。简单来说,运行时机制使我们直到运行时才去决定一个对象的类别,以及调用该类别对象指定方法。 多态:不同对象以自己的方式响应相同的消息的能力叫做多态。意思就是假设生物类(life)都用有一个相同的方法-eat; 那人类属于生物,猪也属于生物,都继承了 life 后,实现各自的 eat,但是调用是我们只需调用各自的 eat 方法。也就是不同的对象以自己的方式响应了相同的消息(响应了 eat 这个选择器)。 因此也可以说,运行时机制是多态的基础.

    static的作用

    • static修饰的函数是一个内部函数,只能在本文件中调用,其他文件不能调用
    • static修饰的全局变量是一个内部变量,只能在本文件中使用,其他文件不能使用
    • static修饰的局部变量只会初始化一次,并且在程序退出时才会回收内存

    堆和栈的区别

    • 堆空间的内存是动态分配的,一般存放对象,并且需要手动释放内存
    • 栈空间的内存由系统自动分配,一般存放局部变量等,不需要手动管理内存

    http协议的组成和特性

    • 组成:http 请求由三部分组成;分别是:请求行、消息报头、请求正文
      HTTP协议的主要特点
    • 1.支持客户/服务器模式
    • 2.简单快速:客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有 GET、HEAD、POST。每种方法规定了客户与服务器联系的类型不同。由于 HTTP 协议简单,使得 HTTP 服务器的程序规模小,因而通信速度很快
    • 3.灵活:HTTP 允许传输任意类型的数据对象。正在传输的类型由 Content-Type 加以标记
    • 4.无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间
    • 5.无状态:HTTP 协议是无状态协议。无状态是指协议对于事务处理没有记忆能力

    什么情况下会发生内存泄漏和内存溢出

    当程序在申请内存后,无法释放已申请的内存空间(例如一个对象或者变量使用完成后没有释放,这个对象一直占用着内存),一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。内存泄露会最终会导致内存溢出!当程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个int,但给它存了long才能存下的数,那就是内存溢出

    自动释放池是什么,如何工作

    当您向一个对象发送一个autorelease消息时,Cocoa就会将该对象的一个引用放入到最新的自动释放池。它仍然是个正当的对象,因此自动释放池定义的作用域内的其它对象可以向它发送消息。当程序执行到作用域结束的位置时,自动释放池就会被释放,池中的所有对象也就被释放

    • 1.ojc-c是通过一种"referring counting"(引用计数)的方式来管理内存的,对象在开始分配内存(alloc)的时候引用计数为1,以后每当碰到有copy,retain的时候引用计数都会加一,每当碰到release和autorelease的时候引用计数就会减1,如果此对象的计数变为了0, 就会被系统销毁
    • 2.NSAutoreleasePool就是用来做引用计数的管理工作的,这个东西一般不用你管的
    • 3.autorelease和release没什么区别,只是引用计数减一的时机不同而已,autorelease会在对象的使用真正结束的时候才做引用计数减一

    Swift中Struct和Class的区别

    • 主要的区别就在于class是类型引用,而struct是值引用,在Objective-C时代,我们对类型引用和值引用就有了一定的了解,例如在Objective-C中常用的NSArray, NSDictionary, NSString, UIKit等都是类型引用;而NSInteger, CGFloat, CGRect等则是值引用
    • swift Foundation框架的SDK,诸如String,Array,Dictionary都是基于struct实现的
    • 在swift中,类型引用和值引用的区别在于,对于类型引用(class reference),将变量a赋值给变量b,即b = a,这样的赋值语句仅仅将b的指针与a的指针一样,指向同一块内存区域,此时改变b的值,a也会跟着改变;而对于值引用(value reference),赋值语句b = a处理的过程是开辟一个新的内存b,将a变量的内容拷贝后存放到内存b,这时a和b完全没有关系的两个变量,对b的改变不会影响到a,反之亦然。

    证书传递、验证和数据加密、解密过程解析

    • 1.客户端发起HTTPS请求
    • 2.服务端的配置
      采用HTTPS协议的服务器必须要有一套数字证书,可以自己制作,也可以向组织申请。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用受信任的公司申请的证书则不会弹出提示页面(startssl就是个不错的选择,有1年的免费服务)。这套证书其实就是一对公钥和私钥。如果对公钥和私钥不太理解,可以想象成一把钥匙和一个锁头,只是全世界只有你一个人有这把钥匙,你可以把锁头给别人,别人可以用这个锁把重要的东西锁起来,然后发给你,因为只有你一个人有这把钥匙,所以只有你才能看到被这把锁锁起来的东西。
    • 3.传送证书
      这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等等。
    • 4.客户端解析证书
      这部分工作是有客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值。然后用证书对该随机值进行加密。就好像上面说的,把随机值用锁头锁起来,这样除非有钥匙,不然看不到被锁住的内容。
    • 5.传送加密信息
      这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。
    • 6.服务段解密信息
      服务端用私钥解密后,得到了客户端传过来的随机值(私钥),然后把内容通过该值进行对称加密。所谓对称加密就是,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。
    • 7.传输加密后的信息
      这部分信息是服务段用私钥加密后的信息,可以在客户端被还原
    • 8.客户端解密信息
      客户端用之前生成的私钥解密服务段传过来的信息,于是获取了解密后的内容。整个过程第三方即使监听到了数据,也束手无策。

    客户端发送https请求 ->服务器返回证书公钥 -> 客户端验证公钥并生成一个随机值,用证书对该随机值进行加密,传给服务器 -> 服务端用私钥解密后,得到了客户端传过来的随机值(私钥)->客户端发送加密后的信息->客户端用之前生成的私钥解密服务段传过来的信息

    TCP/IP 协议

    image.png

    UDP

    • UDP 不提供复杂的控制机制,利用 IP 提供面向无连接的通信服务
    • 并且它是将应用程序发来的数据在收到的那一刻,立即按照原样发送到网络上的一种机制。即使是出现网络拥堵的情况,UDP 也无法进行流量控制等避免网络拥塞行为
    • 此外,传输途中出现丢包,UDP 也不负责重发
    • 甚至当包的到达顺序出现乱序时也没有纠正的功能
    • 如果需要以上的细节控制,不得不交由采用 UDP 的应用程序去处理

    UDP 常用于一下几个方面:1.包总量较少的通信(DNS、SNMP等);2.视频、音频等多媒体通信(即时通信);3.限定于 LAN 等特定网络中的应用通信;4.广播通信(广播、多播

    TCP

    • TCP 与 UDP 的区别相当大。它充分地实现了数据传输时各种控制功能,可以进行丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。而这些在 UDP 中都没有。
    • 此外,TCP 作为一种面向有连接的协议,只有在确认通信对端存在时才会发送数据,从而可以控制通信流量的浪费

    根据 TCP 的这些机制,在 IP 这种无连接的网络上也能够实现高可靠性的通信( 主要通过检验和、序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现

    三次握手

    • TCP 提供面向有连接的通信传输。面向有连接是指在数据通信开始之前先做好两端之间的准备工作
    • 所谓三次握手是指建立一个 TCP 连接时需要客户端和服务器端总共发送三个包以确认连接的建立。在socket编程中,这一过程由客户端执行connect来触发


      image.png
    • 第一次握手:客户端将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给服务器端,客户端进入SYN_SENT状态,等待服务器端确认
    • 第二次握手:服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态
    • 第三次握手:客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了

    四次挥手

    • 四次挥手即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发
    • 由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭


      image.png

    中断连接端可以是客户端,也可以是服务器端。

    • 第一次挥手:客户端发送一个FIN=M,用来关闭客户端到服务器端的数据传送,客户端进入FIN_WAIT_1状态。意思是说"我客户端没有数据要发给你了",但是如果你服务器端还有数据没有发送完成,则不必急着关闭连接,可以继续发送数据
    • 第二次挥手:服务器端收到FIN后,先发送ack=M+1,告诉客户端,你的请求我收到了,但是我还没准备好,请继续你等我的消息。这个时候客户端就进入FIN_WAIT_2 状态,继续等待服务器端的FIN报文
    • 第三次挥手:当服务器端确定数据已发送完成,则向客户端发送FIN=N报文,告诉客户端,好了,我这边数据发完了,准备好关闭连接了。服务器端进入LAST_ACK状态
    • 第四次挥手:客户端收到FIN=N报文后,就知道可以关闭连接了,但是他还是不相信网络,怕服务器端不知道要关闭,所以发送ack=N+1后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。服务器端收到ACK后,就知道可以断开连接了。客户端等待了2MSL后依然没有收到回复,则证明服务器端已正常关闭,那好,我客户端也可以关闭连接了。最终完成了四次握手

    对MRC和ARC的理解

    程序在运行的过程中通常通过创建一个OC对象/定义一个变量/调用一个函数或者方法,来增加程序的内存暂用,而一个移动设备的内存是有限的,每个软件所能占用的内存也是有限的,所以我们需要合理的分配内存、清除内存,回收那些不再使用的对象,从而保证程序的稳定性

    • 继承了NSObject的对象存储在操作系统的堆里面。操作系统的堆:一般由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收,分配方式类似于链表。非OC对象一般放在操作系统的栈里面,由操作系统自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈(先进先出)。任何继承NSObject的对象需要进行内存管理,而其他非对象类型(int、char、float、double、struct、enum等)不需要进行内存管理
    • 为保证对象的存在,每当创建引用到对象需要给对象发送一条retain消息,可以使引用计数器+1;当不需要对象时,通过给对象发送一条release消息,可以使引用计数器值-1
    • 当对象的引用计数为0时,系统就知道这个对象不需要再使用了,所以可以释放它的内存,通过给对象发送dealloc消息发起这个过程
      需要注意的是:release并不代表销毁/回收对象,仅仅是计数器-1
      *使用ARC后,系统会检测出何时需要保存对象,何时需要自动释放对象,何时需要释放对象。编译器会管理好对象的内存,会在需要的地方插入retain、release、autorelease,通过生成正确的代码去自动释放或者保持对象。我们完全不用担心编译器会报错
    • ARC判断一个对象是否需要释放不是通过引用计数来判断的,而是通过强指针来判断的

    ARC下,不显式指定任何属性关键字时,默认的关键字都有哪些?

    • 对应基本数据类型默认关键字是:atomic,readwrite,assign

    • 对于普通的OC对象:atomic,readwrite,strong

    runtime实现的机制

    • 需要导入<objc/message.h><objc/runtime.h> runtime,运行时机制,它是一套C语言库。 实际上我们编写的所有OC代码,最终都是转成了runtime库的东西,比如类转成了runtime库里面的结构体等数据类型,方法转成了runtime库里面的C语言函数,平时调方法都是转成了objc_msgSend函数(所以说OC有个消息发送机制)因此,可以说runtime是OC的底层实现,是OC的幕后执行者##
    • 运行时机制,runtime库里面包含了跟类、成员变量、方法相关的API,比如获取类里面的所有成员变量,为类动态添加成员变量,动态改变类的方法实现,为类动态添加新的方法等。

    runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)

    每一个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现.

    谈谈消息转发机制实现

    简而言之,它允许未知的消息被困住并作出反应。换句话说,无论何时发送未知消息,它 都会以一个很好的包发送到您的代码中,此时您可以随心所欲地执行任何操作。

    消息转发的步骤

    • 首先检查这个selector是不是要忽略
    • 检测这个selector的target是不是nil,OC允许我们对一个nil对象执行任何方法都不会Crash,因为运行时会被忽略掉
    • 开始检查这个类的实现IMP,先从cache里查找,如果找到了就运行对应的函数去执行相应的代码
    • 如果cache中没有找,就到类的方法列表中找对应的方法
    • 如果类的方法列表中找不到,就到类的父类方法列表中查找,一直找到NSObject
    • 如果还是没有找到,就要进入动态方法解析和消息转发了

    没有方法实现时,程序会在运行时挂掉并抛出unrecognized selector send to _ 的异常。但在异常抛出前,Objective-C的运行会给你三次拯救程序的机会

      1. Method resolution
    * 首先,Objective-C运行时会调用+(BOOL)resolveInstanceMethod: 或者
    

    +(BOOL)resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数并返回YES,那运行时系统就会重新启动一次消息发送的过程。

      1. Fast forwarding
        - (id)forwardingTargetForSelector:(SEL)aSelector {
            if(aSelector == @selector(foo:)){
                return [[BackupClass alloc] init]
           }
            return [super forwardingTargetForSelector:aSelector];
        }
    
      1. Normal forwarding
    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
    {
        if (aSelector==@selector(run)) {
           return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector: aSelector];
    }
    -(void)forwardInvocation:(NSInvocation *)anInvocation
    {
        SEL selector =[anInvocation selector];
        RunPerson *RP1=[RunPerson new];
        RunPerson *RP2=[RunPerson new];
        if ([RP1 respondsToSelector:selector]) {
            [anInvocation invokeWithTarget:RP1];
        }
        if ([RP2 respondsToSelector:selector]) {
            [anInvocation invokeWithTarget:RP2];
        }    
    }
    

    Fast forwarding和Normal Forwarding区别:前者需要重载一个API,后者需要重载两个API;前者只能转发一个对象,后者可以连续转发给多个对象。

    image.png

    UIViewController 生命周期

    image.png

    loadView()
    viewDidLoad()
    viewWillAppear
    viewWillLayoutSubviews() - Optional((162.0, 308.0, 50.0, 50.0))
    viewDidLayoutSubviews() - Optional((67.0, 269.0, 241.0, 129.0))
    viewDidAppear
    viewWillDisappear
    viewDidDisappear
    deinit

    谈谈对自动释放池的理解

    自动释放池

    当我们不再使用一个对象的时候,应该将其空间释放,但是有时候,我们不知道何时应该将其释放。为了解决这个问题,Objective-C提供了autorelease方法。
    使用autorelease的好处:不用再关心对象释放的时间;不用再关心什么时候调用release
    autorelease的本质:只是把对release的调用延迟了,对于每一个autorelease,系统只是把该对象放入了当前的autorelease pool中,当该pool被释放时,该pool中所有对象会被调用release

    自动释放池在mrc和arc区别

    • @autorelease是自动释放池,让我们更自由的管理内存,在MRC环境如果有alloc、new、copy如果想延迟释放,我们都在自动释放池中加上autorelease来延迟释放
    • 当我们手动创建一个@autoreleasepool,里面创建了很多临时变量,当@autoreleasepool结束时,里面的内存就会回收
    • ARC时代,系统自动管理自己的autoreleasepool,RunLoop就是iOS中的消息循环机制,但是个RunLoop结束时系统才会一次性清理掉被autorelease处理过的对象,其实本质上说是在本次RunLoop迭代结束时清理掉本地迭代期间被放到autoreleasepool中的对象。至于何时RunLoop结束没有固定的duration。

    多层自动释放池嵌套的对象在哪一层释放

    • 自动释放池是以栈的形式存在的。 由于栈只有一个入口,所以调用autorelease会将对象放到栈顶的自动释放池,栈顶就是离autorelease方法最近的自动释放池

    对于block的理解、mrc和arc下有什么区别、使用注意事项

    • 在ARC中,__Block修饰的变量被引用到,引用计数+1;在MRC中__Block修饰的变量的引用计数是不变的
    • 在ARC中,__Block修饰,会引起循环引用;在MRC中__Block修饰的变量可以避免循环引用
    • __Block不管是ARC还是MRC的模式下都可以使用,可以修饰对象,还可以修饰基本数据类型;__weak只能在ARC模式下使用,只能修饰对象不能修饰基本数据类型;
    • __Block对象可以在block中被重新赋值,__weak不可以

    对于深拷贝和浅拷贝的理解

    浅拷贝:拷贝后只是指向了该对象的地址,这两个对象指向了同一个内存地址,通过任何一个对象修改值,这两个对象再次获取值都是获取修改后的值。

    深拷贝:拷贝了对象的内容然后重新开辟了新的内存空间,这是两个互相独立的对象,对任意一个修改值,另一个的值都不会改变。

    • 不可变字符串/对象: copy/浅拷贝 mutableCopy/深拷贝
    • 可变字符串/对象:copy/深拷贝 mutableCopy/深拷贝
    • 集合对象的值:这说明集合对象的深拷贝只是单层深拷贝,只是给该对象分配了一个新的内存地址而集合对象里面的值还都指向原来的内存地址。

    对于strong、weak理解

    strong

    • 当一个对象不再有strong类型引用指向它的时候,它就会被释放,即使该对象还有weak类型指针引用指向它,并且还指向该对象的weak引用会被置为nil,防止野指针的出现

    将一个对象类比为一只狗,释放对象类比为狗要跑掉。strong类型就像是栓住狗的绳子,只要有绳子拴住狗,它就不会跑掉。如果有5条绳子栓一只狗,除非5条绳子都脱落,否则狗是不会跑掉的,就相当于只有每个strong指针都被置为nil时,对象才会被释放。weak类型就像是看着狗的小孩子,只要狗一直被拴着,那么小孩子就能看到狗,会一直指向它,只要狗的绳子脱落,那么狗就会跑掉,不管有多少的小孩在看着它。最后,狗跑掉以后,小孩也就和狗之间没什么联系了。只要最后一个strong型指针不再指向对象,那么对象就会被释放,同时所有的weak型指针都将会被清除。

    weak

    • weak最主要的防止strong类型之间形成循环,使大家都不能释放造成内存泄漏。最明显的例子就是delegate,以一个viewcontroller和tableView为例,如果viewcontroller中有strong类型指向tableview,而tableView的delegate指向viewcontroller,如果delegate是strong类型,那么要销毁delegate就要销毁viewcontroller,而要销毁viewcontroller则要先销毁delegate,这样就形成了循环了。所以要将delegate声明为weak类型的,这样才能在viewcontroller需要销毁的时候进行销毁。

    retain和strong一致的(声明为强引用);assign和weak是基本一致的(声明为弱引用),之所以说它们是基本一致的是因为它们还是有所不同的。weak严格的说应当叫做“归零弱引用”,即当对象销毁后,会自动的把它的指针置为nil,这样可以防止野指针错误。而assign销毁对象后,不会把对象的指针置为nil,对象已经销毁,但指针还在痴痴的指向它,这就成了野指针,这是比较危险的。retain和assign都是ARC之前使用的,strong和weak都是在ARC下才加入的,strong是默认的修饰符

    weak原理

    Runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash表,key是所指对象的地址,value是weak指针地址(这个地址的值是所指对象的地址)数组

    weak的实现原理可以概括为以下三步:

      1. 初始化时,runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址
      1. 添加引用时:objc_initWeak函数会调用objc_storeWeak()函数,objc_storeWeak()的作用是更新指针指向,创建对应的弱引用表
      1. 释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址数组,然后遍历这个数组把其中的数据设置为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

    简述下block的实现

    image.png

    Http协议30x的错误是什么

    1xx 消息——请求已被服务器接收,继续处理
    2xx 成功——请求已成功被服务器接收、理解、并接受
    3xx 重定向——需要后续操作才能完成这一请求
    4xx 请求错误——请求含有词法错误或者无法被执行
    5xx 服务器错误——服务器在处理某个正确请求时发生错误

    谈谈你对runloop得理解:由浅入深

    • 一个线程一次只能执行一个任务,执行完成后线程就会退出。RunLoop可以让线程随时处理事件但不退出
    • 引用RunLoop机制的目的是利用RunLoop机制的特点实现整体省电的效果,并且让系统和应用可以流畅的运行,提高响应速度,达到极致的用户体验。

    进程是一家工厂,线程是一个流水线,RunLoop就是流水线主管;当工厂接到商家分配的订单分配给这个流水线时,RunLoop就会启动这个流水线,让流水线运动起来,生产产品;当产品生产完毕时,RunLoop就会暂停流水线,节约资源。RunLoop管理流水线,流水线才不会因为无所事事被工厂销毁;而不需要流水线时,就会辞退RunLoop这个主管,即退出线程,把所有资源释放

    RunLoop实质上是一个对象,这个对象管理了其需要处理的事件和消息,并提供了入口函数来执行Event Loop的逻辑。线程执行这个函数后,会一直处于这个函数内部,接受消息 -> 等待 -> 执行 的循环中,直到这个循环结束

    RunLoop的特性

    • 主线程的RunLoop在应用启动的时候就会自动创建
    • 其他线程则需要再该线程下自己启动
    • 不能直接创建RunLoop
    • RunLoop并不是线程安全的,所以需要避免在其他线程上嗲偶偶那个当前线程的RunLoop
    • RunLoop负责处理消息事件,即输入源事件和计时事件

    谈谈对多线程理解:由浅入深

    进程

    • 进程代表当前运行的一个程序
    • 是系统分配资源的基本单位
    • 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内
    • 比如同时打开QQ、Xcode,系统就会分别启动2个进程
    • 进程可以理解为一个工厂

    线程

    • 线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行
    • 一个进程含有一个线程或多个线程
    • 应用程序打开后会默认开辟一个线程叫做主线程或者UI线程
    • 线程可以理解为工厂里的工人

    串行

    • 多个任务按顺序执行
    • 类似于一个窗口办公排队
    • 也就是说,在同一时间内,1个线程只能执行1个任务
    • 比如在1个线程中下载3个文件(分别是文件A、文件B、文件C)就要依次执行

    并行

    • 多个任务同一时间一起执行
    • 似于多个窗口办公
    • 比如同时开启3条线程分别下载3个文件(分别是文件A、文件B、文件C),同时执行

    并发

    • 很多人容易认为并发和并行是一个意思,但实际上他们有本质的区别
    • 并发看起来像多个任务同一时间一起执行
    • 但实际上是CPU快速的轮转切换造成的假象

    多线程概念

    • 本质
    *  在一个进程中开启多个线程并发执行
    
    • 原理
    * 同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)
    * 多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)
    * 如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
    
    • 优点
    * 能适当提高程序的执行效率
    * 能适当提高资源利用率(CPU、内存利用率)
    
    • 缺点
    * 线程需要耗费系统资源
    * 线程需要消耗栈空间的1MB资源
    * 其他线程每个消耗512KB资源
    *  程序设计更加复杂:比如线程之间的通信、多线程的数据共享
    

    主线程

    • 概念
    * 一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”
    
    • 作用
    * 显示\刷新UI界面
    * 处理UI事件(比如点击事件、滚动事件、拖拽事件等)
    
    • 注意
    * 别将比较耗时的操作放到主线程中
    * 耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验
    

    多线程实现

    image.png
    • NSThread
    • GCD
     * 任务:即操作,你想要干什么,说白了就是一段代码,在 GCD 中就是一个 Block,所以添加任务十分方便。任务有两种执行方式: 同步执行 和 异步执行,他们之间的区别是 是否会创建新的线程。
     * 队列:用于存放任务。一共有两种队列, 串行队列 和 并行队列。
    
    • NSOperation
     * 是苹果公司对 GCD 的封装,完全面向对象,所以使用起来更好理解。 大家可以看到 NSOperation 和 NSOperationQueue 分别对应 GCD 的 任务 和 队列 。操作步骤也很好理解。
     * NSOperation也有两个概念,队列和任务。
    
    • NSOperation 对比 GCD
     * GCD效率更高,使用起来也很方便
     *  NSOperation面向对象,可读性更高,架构更清晰,对于复杂多线程场景,如并发中存在串行,和设置最大并发数,拥有现在的API,使用起来特别简单
    

    互斥锁

    @synchronized(self) {
      //需要执行的代码块
    }
    

    谈谈category和extension区别,系统如何底层实现category

    extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)

    • extension 在编译期决议,它是类的一部分,但是category则完全不一样,它在运行期决议的。extension在编译期和头文件里的@interface以及实现文件@implement一起形成一个完整的类。extension伴随类的产生而产生,亦随之一起消亡。
    • extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension,除非创建子类再添加extension。而category不需要有类的源码,我们可以给系统提供的类添加category。
    • extension可以添加实例变量,而category不可以。
    • extension和category都可以添加属性,但是category的属性不能生成成员变量和getter、setter方法的实现。

    Category可以添加属性,但是并不会自动生成成员变量及set/get方法。因为category_t结构体中并不存在成员变量。通过之前对对象的分析我们知道成员变量是存放在实例对象中的,并且编译的那一刻就已经决定好了。而分类是在运行时才去加载的。那么我们就无法再程序运行时将分类的成员变量中添加到实例对象的结构体中。因此分类中不可以添加成员变量

    objc中的类方法和实例方法有什么本质区别和联系?

    • 类方法:
    * 类方法是属于类对象的
    * 类方法只能通过类对象调用
    * 类方法中的self是类对象
    * 类方法可以调用其他的类方法
    * 类方法中不能访问成员变量
    * 类方法中不定直接调用对象方法
    
    • 实例方法:
     * 实例方法是属于实例对象的
     * 实例方法只能通过实例对象调用
     *  实例方法中的self是实例对象
     *  实例方法中可以访问成员变量
     *  实例方法中直接调用实例方法
     *  实例方法中也可以调用类方法(通过类名)
    

    能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

    • 不能向编译后得到的类中增加实例变量
    • 能向运行时创建的类中添加实例变量
    • 因为编译后的类已经注册在 runtime 中,类结构体中的 objc_ivar_list 实例变量的链表 和 instance_size 实例变量的内存大小已经确定,同时runtime 会调用 class_setIvarLayout 或 class_setWeakIvarLayout 来处理 strong weak 引用。所以不能向存在的类中添加实例变量
    • 运行时创建的类是可以添加实例变量,调用 class_addIvar 函数。但是得在调用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上
      答案是通过 isa 混写(isa-swizzling)

    KVC的底层实现

    • 检查是否存在相应的key的set方法,如果存在,就调用set方法
    • 如果set方法不存在,就会查找与key相同名称并且带下划线的成员变量,如果有,则直接给成员变量属性赋值
    • 如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值
    • 如果还没有找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。这些方法的默认实现都是抛出异常,我们可以根据需要重写它们

    iOS远程推送原理及详细实现过程

    • 1.装有我们应用程序的 iOS 设备,需要向 APNs 服务器注册
    • 2.注册成功后,APNs 服务器将会给我们返回一个 devicetoken,我们获取到这个 token 后会将这个 token 发送给我们自己的应用服务器。
    • 3.当我们需要推送消息时,我们的应用服务器将消息按照指定的格式进行打包,然后结合 iOS 设备的 devicetoken 一起发给 APNs 服务器
    • 4.我们的应用会和 APNs 服务器维持一个基于 TCP 的长连接,APNs 服务器将新消息推送到iOS 设备上,然后在设备屏幕上显示出推送的消息

    KVO内部实现原理

    KVO是基于runtime机制实现的
    当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的setter 方法。派生类在被重写的 setter 方法实现真正的通知机制(Person->NSKVONotifying_Person)

    apple用什么方式实现对一个对象的KVO?

    • 键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就会记录旧的值。而当改变发生后, observeValueForKey:ofObject:change:context: 会被调用,继而 didChangeValueForKey: 也会被调用。可以手动实现这些调用,但很少有人这么做。一般我们只在希望能控制回调的调用时机时才会这么做。大部分情况下,改变通知会自动调用。
    • 比如调用 setNow: 时,系统还会以某种方式在中间插入 wilChangeValueForKey: 、 didChangeValueForKey: 和 observeValueForKeyPath:ofObject:change:context: 的调用。大家可能以为这是因为 setNow: 是合成方法,有时候我们也能看到有人这么写代码:
    - (void)setNow:(NSDate *)aDate {
       [self willChangeValueForKey:@"now"]; // 没有必要
       _now = aDate;
       [self didChangeValueForKey:@"now"];// 没有必要
    }
    

    这完全没有必要,不要这么做,这样的话,KVO代码会被调用两次。KVO在调用存取方法之前总是调用 willChangeValueForKey: ,之后总是调用 didChangeValueForkey: 。怎么做到的呢?答案是通过 isa 混写(isa-swizzling)。第一次对一个对象调用 addObserver:forKeyPath:options:context: 时,框架会创建这个类的新的 KVO 子类,并将被观察对象转换为新子类的对象。在这个 KVO 特殊子类中, Cocoa 创建观察属性的 setter ,大致工作原理如下:

    - (void)setNow:(NSDate *)aDate {
       [self willChangeValueForKey:@"now"];
       [super setValue:aDate forKey:@"now"];
       [self didChangeValueForKey:@"now"];
    }
    

    这种继承和方法注入是在运行时而不是编译时实现的。这就是正确命名如此重要的原因。只有在使用KVC命名约定时,KVO才能做到这一点。
    KVO 在实现中通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。

    IBOutlet连出来的视图属性为什么可以被设置成weak?

    使用storyboard(xib不行)创建的vc,会有一个叫_topLevelObjectsToKeepAliveFromStoryboard的私有数组强引用所有top level的对象,所以这时即便outlet声明成weak也没关系

    dispatch_barrier_async的作用是什么?

    在并行队列中,为了保持某些任务的顺序,需要等待一些任务完成后才能继续进行,使用 barrier 来等待之前任务完成,避免数据竞争等问题。 dispatch_barrier_async 函数会等待追加到Concurrent Dispatch Queue并行队列中的操作全部执行完之后,然后再执行 dispatch_barrier_async 函数追加的处理,等 dispatch_barrier_async 追加的处理执行结束之后,Concurrent Dispatch Queue才恢复之前的动作继续执行。

    如何手动触发一个value的KVO

    • 自动触发是指类似这种场景:在注册 KVO 之前设置一个初始值,注册之后,设置一个不一样的值,就可以触发了。
    • 键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后, observeValueForKey:ofObject:change:context: 会被调用,继而 didChangeValueForKey: 也会被调用。如果可以手动实现这些调用,就可以实现“手动触发”了。

    RunLoop基本概念

    为什么引入RunLoop机制

    • 一个线程一次只能执行一个任务,执行完成后线程就会退出。RunLoop可以让线程随时处理事件但不退出。
    • 引入RunLoop机制目的是利用RunLoop机制的特点实现整体省点的效果,并且让系统和应用可以流畅的运行,提高响应速度,达到极致的用户体验

    RunLoop的本质

    • 进程是一家工厂,线程是一个流水线,RunLoop就是流水线上的主管;当工厂接到商家的订单分配给这个流水线时,RunLoop就启动这个流水线,让流水线动起来,生产产品;当产品生产完毕时,RunLoop就会暂时停下流水线,节约资源。RunLoop管理流水线,流水线才不会因为无所事事被工厂销毁;而不需要流水线时,就会辞退RunLoop这个主管,即退出线程,把所有资源释放。
    • RunLoop实质上是一个对象,这个对象管理了其需要处理的事情和消息,并提供了入口函数来执行Event Loop 的逻辑。线程执行这个函数后,会一直处于这个函数内部:接受消息 -> 等待 -> 执行 的循环中,知道这个循环结束。

    RunLoop的特性

    • 主线程的RunLoop在应用启动的时候就会自动创建
    • 其他线程则需要再该线程下自己启动
    • 不能直接创建RunLoop
    • RunLoop并不是线程安全的,所以需要在其他线程上调用当前线程的RunLoop
    • RunLoop负责管理autorelease pools
    • RunLoop负责处理消息事件,即输入源事件和计时器事件

    RunLoop与线程的关系

    线程和RunLoop之间是一一对应的,其关系是保存在一个全局的Dictionary里。线程刚创建时并没有RunLoop,如果不主动获取,一直不会有。RunLoop的创建时发生在第一次获取时。

    RunLoop的内部组件

    一个RunLoop可以包含多个Mode,每个Mode包含多个Source、Timer、Observer。


    image.png

    RunLoop 实现逻辑分析

    • 实际上 RunLoop 其内部是一个 do-while 循环。RunLoop的核心就是一个MachMessage的调用过程,RunLoop调用这个函数去接收消息,如果没有别人发送port消息过来,RunLoop会进入休眠状态。内核会将线程置于等待状态,这个时候whild循环是停止的,相比于一直运行的while循环,会很节省CPU资源,进而达到省电的目的。
    • 因为Source1注册了MachPort端口,当有Source1事件进来的时候会通过这个port端口唤醒RunLoop继续执行,当计时器到了时候也会唤醒RunLoop,RunLoop唤醒后会先通过Observer 当前已经处于唤醒状态,之后会先执行Source1事件,执行完成后再执行timer、Source0。执行完所有的Source0事件后,如果有Source1事件则执行Source1,如果没有则通知Observe 进入到休眠状态。完成一个循环。
    image.png

    总结

    RunLoop是一个do-while 循环,又不是一个do-while 循环。他的工作模式是一个循环,但是他基于mach_port和mach_msg的 休眠\唤醒 机制确保了他可以在无任务的时候休眠,有任务的时候及时唤醒,相比于一个普通循环,不会空转,不会浪费系统资源。RunLoop又通过不同的工作mode隔离了不同的事件源,使他们的工作互不影响。这才是RunLoop实现省电,流畅,响应速度快,用户体验好的根本原因;进而基于RunLoop的组件如计时器、GCD、界面更新、自动释放池能高效运转的根本原因。

    RunLoopModel

    • kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的
    • UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
    • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
    • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
    • kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用

    为什么引入Runloop机制,有什么作用或者好处?

    引入RunLoop机制的目的是利用RunLoop机制的特点实现整体省电的效果,并且让系统和应用可以流畅的运行,提高响应速度,达到极致用户体验。

    为什么省电

    主要有两点:1.因为不做任何操作的时候,主线程RunLoop会处于退出状态,不会执行任何空转逻辑,不执行代码自然不消耗GPU资源,自然省电。2.RunLoop提供一种班车制,限制如页面刷新等任务的频率,一次RunLoop只执行一次,防止多次重复执行代码带来的性能损耗。

    为什么可以流程运行

    • 一个app流畅与否的决定性因素是主线程的阻塞率,在iOS系统中runloop每秒执行60次,理论上主线程runloop达到55帧以上的刷新频率用户就感觉不到卡顿。
    • Mode机制,同一时间只执行一个Mode内的Source或者Timer,比如拖动的时候只指定拖动Mode,其他Mode 如Default Mode中的源不会被执行,确保了高速滑动的时候不会有其他逻辑阻碍主线程刷新。
    • Runloop做的是管理视图刷新频率,防止重复运算。由于视图更新必须在主线程,视图的重布局和重绘都会占用主线程的执行时间,一次Runloop循环只执行一次可以最大限度的防止重复运算导致的计算浪费
    • 管理核心动画。核心动画有三个树,其中render tree 是私有的,应用开发无法访问到。render tree在专用的render server 进程中执行,是真正用来渲染动画的地方,线程优先级高于主线程。所以即使app主线程阻塞,也不会影响到动画的绘制工作。既节省了主线程的计算资源,又使动画可以流畅的执行
    • 支持异步方法调用,将耗时操作分发到子线程中进行。RunLoop是performSelector的基础设施。我们使用 performSelector:onThread: 或者 performSelecter:afterDelay: 时,实际上系统会创建一个Timer并添加到当前线程的RunLoop中。

    如何提高响应速度

    当发生系统事件时,如触碰事件,系统通过Mach Port发送Mach消息主动唤醒RunLoop。Mach是抢占式操作系统内核,Mach系统IPC机制就是依靠消息机制实现的,所以效率非常高。

    iOS中的事件的产生和传递

    • 触摸事件的传递是从父控件传递子控件
    • 也就是UIApplication ->window ->寻找处理事件最合适的view

    如何找到最合适的控件来处理事件

    • 首先判断主窗口(keyWindow)自己能否接受触摸事件
    • 判断触摸事件是否在自己身上
    • 子控件数组中冲后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
    • view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止
    • 如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己的最合适的view
      注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。

    UIView不能接受触摸事件的三种情况

    • 不允许交互:userInteractionEnabled=NO
    • 隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
    • 透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明

    总结

    • 点击一个UIView或产生一个触摸事件,这个触摸事件A会被添加到由UIApplication管理的事件队列中
    • UIApplication会从事件队列中取出最前面的事件(此处假设为触摸事件),把时间A传递给应用程序的主窗口
    • 窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件

    响应者链

    • 响应者链条:在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。在iOS中响应者链的关系可以用下图表示:


      image.png
    响应者链的事件传递过程
    • 当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件传递,即寻找最合适的view过程
    • 接下来是事件的响应。首先看initial view能否处理这个事件。如果不能则会将事件传递给其上级视图;如果上级视图仍然无法处理则继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能,则继续往上传递;一直到window,如果window还是不能处理此事件则继续交给UIApplication处理,如果最后UIApplication还是不能处理此事件则将其丢弃
    • 在事件的响应中,如果某个空间实现了touches...方法,则这个事件将由该空间来接受,如果调用了[super touches...];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches...方法

    git add 和 git stage 有什么区别

    在回答这个问题之前需要先了解 git 仓库的三个组成部分:工作区(Working Directory)、暂存区(Stage)和历史记录区(History)

    • 工作区:在 git 管理下的正常目录都算是工作区,我们平时的编辑工作都是在工作区完成
    • 暂存区:临时区域。里面存放将要提交文件的快照
    • 历史记录区:git commit 后的记录区
      然后是这三个区的转换关系以及转换所使用的命令:


      image.png

    然后我们就可以来说一下 git add 和 git stage 了。其实,他们两是同义的,所以,惊不惊喜,意不意外?

    iOS NSDictionary(字典)~实现原理

    • NSDictionary(字典)是使用 hash表来实现key和value之间的映射和存储的, hash函数设计的好坏影响着数据的查找访问效率。
    • Objective-C 中的字典 NSDictionary 底层其实是一个哈希表,实际上绝大多数语言中字典都通过哈希表实现,

    哈希的原理

    哈希概念:哈希表的本质是一个数组,数组中每一个元素称为一个箱子(bin),箱子中存放的是键值对。

    哈希表的存储过程

    • 根据 key 计算出它的哈希值 h
    • 假设箱子的个数为 n,那么这个键值对应该放在第 (h % n) 个箱子中
    • 如果该箱子中已经有了键值对,就使用开放寻址法或者拉链法解决冲突

    iOS编译过程的原理和应用

    编译器前端

    编译器前端的任务是进行:语法分析,语义分析,生成中间代码(intermediate representation )。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。

    编译器后端

    编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化。

    NSURLSession & NSURLConnection

      1. 使用现状
        NSURLSession是NSURLConnection 的替代者,在2013年苹果全球开发者大会(WWDC2013)随ios7一起发布,是对NSURLConnection进行了重构优化后的新的网络访问接口
    • 2.普通任务和上传
      NSURLSession针对下载/上传等复杂的网络操作提供了专门的解决方案,针对普通、上传和下载分别对应三种不同的网络请求任务:NSURLSessionDataTask, NSURLSessionUploadTask和NSURLSessionDownloadTask.。创建的task都是挂起状态,需要resume才能执行。
    • 3.下载任务方式
      NSURLConnection下载文件时,先将整个文件下载到内存,然后再写入沙盒,如果文件比较大,就会出现内存暴涨的情况。而使用NSURLSessionUploadTask下载文件,会默认下载到沙盒中的tem文件夹中,不会出现内存暴涨的情况,但在下载完成后会将tem中的临时文件删除,需要在初始化任务方法时,在completionHandler回调中增加保存文件的代码。
    • 4.请求方法的控制
      NSURLConnection实例化对象,实例化开始,默认请求就发送(同步发送),不需要调用start方法。而cancel 可以停止请求的发送,停止后不能继续访问,需要创建新的请求。
      NSURLSession有三个控制方法,取消(cancel),暂停(suspend),继续(resume),暂停后可以通过继续恢复当前的请求任务。
    • 5.断点续传的方式
      NSURLConnection进行断点下载,通过设置访问请求的HTTPHeaderField的Range属性,开启运行循环,NSURLConnection的代理方法作为运行循环的事件源,接收到下载数据时代理方法就会持续调用,并使用NSOutputStream管道流进行数据保存。
      NSURLSession进行断点下载,当暂停下载任务后,如果 downloadTask (下载任务)为非空,调用 cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler 这个方法,这个方法接收一个参数,完成处理代码块,这个代码块有一个 NSData 参数 resumeData,如果 resumeData 非空,我们就保存这个对象到视图控制器的 resumeData 属性中。在点击再次下载时,通过调用 [ [self.session downloadTaskWithResumeData:self.resumeData]resume]方法进行继续下载操作。
      1. 配置信息
        NSURLSession的构造方法(sessionWithConfiguration:delegate:delegateQueue)中有一个 NSURLSessionConfiguration类的参数可以设置配置信息,其决定了cookie,安全和高速缓存策略,最大主机连接数,资源管理,网络超时等配置。NSURLConnection不能进行这个配置,相比于 NSURLConnection 依赖于一个全局的配置对象,缺乏灵活性而言,NSURLSession 有很大的改进了。

    相关文章

      网友评论

        本文标题:iOS概念知识

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