1、UI相关

1.1 说一下事情的传递和呼应

1.1.1 事情的发生进程

用户在运用手机的的进程中会发生许多“事情”,例如接触手机屏幕、摇晃手机、运用耳机上的按键控制手机等。这些事情大体上能够分为三类:“Touch Events”、“Motion Events”、“Remote Events”。
在iOS开发当中,咱们能接触到的有关该进程的相关类有三个:UIEventUITouchUIResponder

UIEvent描述了用户与手机的一次交互,包括类型、时刻等,还有许多UITouch。

UITouch表明了接触在屏幕上的方位、移动、巨细、压力。

UIResponder笼统了呼应和处理事情的接口。

iPhone接受到一个接触事情时,处理进程如下:

  1. 通过动作发生接触事情唤醒处于睡眠状态中的app;
  2. 运用IOKit.framework将事情封装为 IOHIDEvent 目标;
  3. 体系通过 mach portIOHIDEvent 目标转发给 SpringBoard.app 处理。SpringBorad 是iPhone手机的桌面办理程序,SpringBoard 能够找到能够处理该事情的app,然后将 IOHIDEvent 目标通过mach port转发给对应的App;
  4. App的主线程 Runloop 接纳到 SpringBoard 转发的音讯,触发对应 mach port 的source1回调 __IOHIDeventSystemClientQueueCallback()
  5. source1回调内部触发source0回调,__UIApplicationHandleEventQueue()
  6. source0内部将IOHIDEvent目标包装为UIEvent目标;
  7. source0内部回调 UIApplicationsendEvnet:办法,将 UIEvent 传递给 UIWindow
  8. UIWindow 接纳到 UIEvent 后,就开端寻觅适宜的 UIResponsder 处理。

1.1.2 事情的传递进程

UIWindow开端运用逆序深度优先遍历算法,查找到最适宜的 Responsder。呼应进程是顺着事情传递进程的路径反向进行的。view的 nextResponder 或许是控制器,也或许是view。

1.2 离屏烘托

1.2.1 什么是离屏烘托?

离屏烘托是指GPU在当时屏幕缓冲区(Frame Buffer)以外开辟一块新的缓冲区(Off- Screen Buffer)进行烘托作业。在当时屏幕缓冲区之外的烘托称之为离屏烘托。创立新的缓冲区是会消耗CPU和GPU资源的,并且创立和删去缓冲区都需求CPU和GPU同步,这会形成GPU烘托流水线中止。离屏烘托需求两次烘托作业,一次在当时屏幕缓冲区另一次在离屏缓冲区,这意味着GPU需求做更多的作业。触发离屏烘托时会从Frame Buffer切换到Off- Screen Buffer,烘托完毕后再切换回Frame Buffer,这一进程需求来回切换上下文,因而对功用有一定的影响。

以下办法会形成离屏烘托:

  1. 一起运用 cornerRadiusmasksToBounds ,且contents有内容状况下,比方UIButton设置了背景图,UIImageVIew设置了图片后再一起设置cornerRadius和masksToBounds;
  2. layer.shouldRasterize;
  3. layer.mask;
  4. layer.opacity 和 layer.allowsGroupOpacity = true;
  5. 运用了高斯含糊;

实践上能够理解为该视图需求多个图层兼并的状况下。

1.2.2 怎么防止离屏烘托?

  1. 防止一起设置 layer.cornerRadiuslayer.masksToBounds = YES,即设置圆角的一起又答应切开圆角;
  2. 需求运用圆角图片时,预先用 CoreGraphics 切好;
  3. 暗影运用 shadowPath
  4. 需求mask的状况下,能够运用自定义 UIView
  5. 需求进行含糊处理的时分尽量不用 UIVisualEffectView ,运用CoreImage供给的办法或许是Accelerate.framework

离屏烘托并不一定是不好的,合理运用离屏烘托也是提高app功用的一种办法。比方在视图包括图片切比较复杂的状况下,敞开光栅化虽然会形成离屏烘托,可是体系会将这一次烘托成果进行保存,下次需求烘托的时分之间就能够拿过来运用了,从而在一定程度上提高了功用。需求留意的是,该缓存只要100ms,且巨细不得超过屏幕像素数据的2.5倍

1.3 UITableView怎么优化?

UITableView 常用的优化思路如下:

  1. 减少cell图层的数量,假如子视图太多的话在drawRect:办法里运用CoreGraphics绘制;
  2. UITab了View在翻滚的时分不烘托,能够在tableView.dragging == NO && tableView.decelerating == NO的时分显现图片;
  3. 运用YYWebImage 等结构时,能够将其回来的图片预先切好圆角,直接替换内存缓存和磁盘缓存中的数据;
  4. 假如高度不固定的状况下,预先计算好高度并缓存;
  5. 重用特别类型的cell;
  6. 不要在cellForRowAtIndexPath:办法中绑定数据,由于此时cell还没有显现。在cellWillDisplay办法中绑定数据;
  7. 尽量不要给视图设置透明背景;
  8. 运用异步烘托结构或许思路AsyncDisplayKit

1.4 说一下UIViewController生命周期

能够从进入UIViewController和退出两个方面阐述。

当进入一个视图控制器时:

  1. +(instancetype)initialize;
  2. -(instancetype)init;
  3. -(void)loadView;
  4. -(void)viewDidload;
  5. -(void)viewWillAppear;
  6. -(void)viewWillLayoutSubviews
  7. -(void)viewDidLayoutSubviews
  8. -(void)viewDidAppear;

当退出一个视图时:

  1. -(void)viewWillDisappear;
  2. -(void)viewDidDisappear;
  3. -(void)dealloc;

1.5 UIView和CALayer

  1. UIView 承继自 UIResponder,是 UIKit结构里边的,CALayer是承继自 NSObject 的,是 QuartzCore结构中的一个类;
  2. CALayer 是 UIView 里边的一个特点;
  3. CALayer 担任烘托,UIView 能够处理呼应事情;
  4. UIView 的 layer 树形在体系内部保护了三份复制:第一棵树(逻辑树)是咱们能够操作的,比方设置圆角暗影等,第二棵树(动画树)是体系操作的,体系在这一层进行操作更改特点,第三棵树(显现树)便是当时屏幕显现的。

2、Objective-C言语特性

2.1 实例目标的实质

实例目标的底层实践上是一个结构体,比方一个 NSObject 的实例目标结构如下:

struct NSObject_IMP {
    Class isa;
};

2.2 类目标的实质

类目标的实质也是一个结构体。结构体里边保存了 isa、superclass 指针、办法缓存、类信息等。
实例目标的 isa 指向类目标。类目标的 isa 指向元类目标。类目标和元类目标的结构是相同的。
isa 从64 位今后是以共用体办法存在的。下面是简化版的结构:

struct objc_class {
    Class isa;
    Class superclass;
    cache_t cache; // 办法缓存
    class_data_bits_t bits; // 类信息。通过进行&FAST_DATA_MASK运算能够得到
    ...
}
struct class_rw_t { // 运行时可变
    uint32_t flags;
    uint32_t version;
    const class_ro_t *ro;
    method_list_t *methods; // 办法列表
    property_list_t *properties; // 特点列表
    const protocol_list_t *protocols; // 协议列表
    Class firstSubclass;
    Class nextSiblingClass;
    char *demangleName;
    ...
}
struct class_ro_t { // 运行时不可变。
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    uint32_t reversed;
    const uint8_t *ivarLayout;
    const char *name;
    method_list_t *baseMethods; // 办法列表
    property_list_t *baseProperties; // 特点列表
    protocol_list_t *baseProtocols; // 协议列表
    const ivar_list_t *ivars; // 成员变量信息列表
    const uint8_t *weakIvarsLayout;
    ...
}

2.3 分类和扩展有什么区别

  1. 分类在运行时决议,扩展在编译时决议。分类完成同名的办法会“覆盖”原来的办法;
  2. 分类不能够增加特点,扩展能够增加特点;
  3. 二者写法不相同;

OC 的分类在编译后实践上是一个结构体,里边包括着与类相似的信息;

struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
    struct property_list_t *classProperties;
    method_list_t *methodsForMeta(bool isMeta) {
        if (isMeta) return classMethods;
        else return instanceMethods;
    }
    property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
}

后编译的分类办法会被刺进到类的办法列表前面,因而会先调用分类办法。

2.3 load 办法、initialize 办法的调用次序

load 办法调用次序:

  1. 先调用类的 load 办法再调用分类的 load 办法;
  2. 先编译先调用;
  3. 子类的 load 办法调用之前先调用父类的。

initialize 的办法调用次序:

  1. 类第一次调用办法时会先初始化,即调用 initialize 办法;
  2. 先调用父类的再调用子类的,即先初始化父类再初始化子类。

两者的区别是 load 办法的调用是直接通过函数地址调用,而 initialize 办法是音讯发送。initialize 办法假如子类没有完成,那么父类的办法会被调用屡次。

2.5 KVO

KVO是 OC 里边的对类的特点变化监听的技能。其原理用到了 OC 的 runtime 底层技能,表现了OC 动态言语的强大之处。具体完成是这样的:
当咱们给一个实例目标XXX增加调查者调用addObserver:forKeyPath: 办法后,该办法内部会进行一系列的处理。其内部会生成一个XXX的子类 NSKVONotifing_XXX, 并且 XXX 的 isa 会指向新生成的子类,因而在调用方看来仍是跟之前相同。这个新派生的类重写了基类 NSObjectclass_isKVOAdealloc 和父类被调查特点的 setter 办法。

被调查特点的 setter 办法被重写后,里边会调用 _NSSetXValueAndNotifiy() , 其内部会调用 [super setXX:xx] 且之前之后分别刺进[self willChangeValueForKey][self didChangeValueForKey]。后者会调用调查者完成的observerValueForKeyPath:ofObject:change:context:办法;
运用KVO 的时分应该留意,在适宜的时机移除调查者,否则会触发 NSRangeException 反常;
KVC 会触发 KVO,可是直接给成员变量赋值是不会触发的;

2.6 KVC

KVC,说的官方一点便是“键值编码”,通过 KVC 这种技能能够给一个类的私有特点进行赋值,比方 UITextFieldplacholderLabel 修改文字色彩等等。运用办法是 setValue:forKey: 或许是 setValue:ForKeyPath:。其中 value 能够传nil。能够提一下的是,字典的办法 setObject:forKey:中的两个参数都不能够为空。

下面谈谈 KVC 的完成原理。
先去看有没有完成上面说到的两个赋值办法,假如有就直接赋值,假如没有就调用accessInstanceVariablesDirectly 办法,假如回来了 NO,就调用 valueForUndefineKey: 办法,并抛出反常。假如是回来了 YES,说明能够直接拜访成员变量,依照 _key, _isKey, key, isKey 的次序找成员变量,假如找到了就赋值,反之就调用 valueForUndefineKey: 并抛出反常。

KVC 的取值跟上面的相似,查找次序是 getKey, key, isKey, _Key ,假如找到了就回来,没有找到也是调用 accessInstanceVariableDirectly 办法判别是否能够直接拜访成员变量,假如能够的话,就依照 _key, _isKey, key, isKey这个次序查找。

2.7 告知

2.7.1 完成原理

告知中心保护了一个 table,table 里边包括了 named表、nameless表、wildcard链表这三个数据结构;当咱们调用 addObserver:selector:name:object:办法时,其内部大概是这样完成的:
1.构造一个 Observation目标,该目标里边保存着 object 和 selector,能够看做是一个链表的节点。
2. 判别传入的 name 是否为空。假如 name 不为空,以 name为 key 从 named 的字典中取出一个 n 字典,然后从 n 字典里边以 object 为 key 取出 observation,再然后把 observation 目标存入链表。
3. 判别传入的 object 是否为空。假如 object 不为空,以 object 为 key 从 namedless 字典中取出 observation 链表,将 observation 目标存入;
4. 假如name 和 object 都为空,则将Observertion 目标存入 wildcard 链表中。

发送告知的进程是先判别object,再判别 name。name 的优先级高于 object。

2.7.2 告知的发送是同步的仍是异步的?

同步的,会调用performSelector:withObject。可是有种状况能够不实时发送告知,而是在适宜的时机发送,并没有敞开线程,这种说法是指运用 NSOperationQueue,指定发送时机,能够依靠 Runloop 比及下一次循环开端时发送。

2.7.3 NSNotificationCenter 接纳音讯和发送音讯是在同一个线程吗,怎么异步发送告知?

是的,发送音讯在哪个线程,接纳音讯就在哪个线程。

2.7.4 NSNotificaionQueue 是异步仍是同步?在哪个线程呼应?

没有异步发送一说,仅仅运用了 Runloop 能够选择触发时机。

2.7.5 NSNotificationQueue 和 Runloop 的联系?

前者依靠后者。比方指定 postStyle 的时分 NSPostWhenIdle 表明在 Runloop 空闲的时分发送。
此外还有 NSPostASAP,尽或许快 发送,NSPostNow多个相同的告知兼并后马上发送。

2.7.6 怎么保证告知接纳的线程在主线程?

运用 block 办法注册告知,在主行列呼应。或许是在主线程注册 machPort,这是担任线程通讯的,当异步线程收到告知后,给 machport 发送音讯。
还能够在告知的回调办法里边,运用 GCD 主行列调度办法。

2.7.8 页面毁掉时,不移除告知会溃散吗?

iOS9 之后不会了,告知中心对 Observer是弱引证的。

2.7.9 屡次增加同一个告知和屡次移除同一个告知会是什么成果?

屡次增加会屡次呼应。移除没事儿。

2.8 音讯转发流程

当给某个实例发送一个音讯找不到办法时,就会进入所谓的音讯转发流程。音讯转发流程是这样的:

  1. 触发resoveInstanceMethod:,这个时分能够解决办法找不到的问题,俗称“动态办法解析”。该办法回来 YES 后,会持续走音讯发送流程,从办法缓存开端查找。咱们能够在这里运用 Runtime 的 api 动态增加一个办法完成;
    2 动态办法解析往后,会来到“音讯转发”阶段。该阶段有三个办法能够供咱们完成,在这三个办法里操作一番也能够解决办法找不到的问题。依照调用次序,首先是forwardingTargetForSelector:,该办法能够回来一个能够处理音讯的目标,在这里咱们能够创立一个能够处理音讯的实例目标,让这个目标去处理音讯。当这里没有完成或许回来 nil 时,会调用 methodSignaturaForSelector:和forwardInvocation:办法。完成这两个办法能够有最后一次机会处理找不到办法的问题。

2.9 super 调用办法的实质

虽然是通过 super 关键字调用办法,实质上仍是给当时目标发送音讯,只不过是办法查找的起点是从父类开端:objcMsgSendSuper2:这么一个函数调用。该函数接纳两个参数,第一个是一个结构体:

struct objc_super2 {
    id receiver; // 音讯接纳者,也便是 self
    Class current_class; // 办法查找起点,也便是父类
}

第二个参数是 SEL。
像下面这种调用办法肯定会死循环的:

@implementation** B
- (void)a {
  [super performSelector: @selector(a)];
}
@end

3. Block的实质

block 实质也是一种 OC 目标,其内部也有 isa,是封装了函数调用和函数调用环境的 OC 目标。有三种类型 block,即 NSGlobalBlockNSStackBlockNSMallocBlock,这三种 block 都承继自 NSBlock。没有拜访 auto 变量的 block 归于 Global 类型的,保存在数据区。在 ARC 环境下,block 作为回来值、usingBlock:办法传入的 block、block被__strong指针指向、GCD里边的 block,这些状况下编译器会主动识别并且会调用 copy办法复制到堆上。

block 的变量捕获:auto 类型的变量是值捕获,static 润饰的变量是指针捕获,大局变量不捕获。

4. Runtime

Runtime 是 OC的柱石,没有 Runtime 支持就没有 OC 、也没有 OC 的动态特性。通过 Runtime 的 api 能够完成许多功用,比方办法交流、动态生成类(KVO)、获取类的成员变量办法列表等信息、字典转模型、关联目标等等。
常用 api:

  • objc_allocateClass // 创立一个类
  • objc_registerClassPare // 注册一个类
  • object_getClass // 获取类目标
  • class_getSuperClass // 获取父类目标
  • class_getInstanceVariable // 获取一个实例变量
  • class_copyIvarList // 获取成员变量列表
  • class_addIvar // 增加成员变量(现已初始化往后的类是不能增加的)
  • class_copyMethodList // 获取办法列表
  • method_exchangeImplementation // 交流办法完成
  • class_replaceMethod // 替换办法完成
  • imp_implementationWithBlock // 运用 block 作为办法完成

5. Runloop

Runloop 是 APP 运行的保证,事情处理、NStimer、autoreleasePoll内存办理、GCD 回到主行列的执行、网络恳求、perforSelector、屏幕刷新都是根据 Runloop 的,Runloop 的底层实践上便是一个做了许多事情的 while 循环,前面说的那些事情便是在循环内部完成的。Runloop 和线程是一一对应联系,每个线程都能够获取一个 Runloop。

OC 里边有能够通过 NSRunloop 来运用 Runloop,也能够运用 CFRunloopRef 这套 C言语的 api 运用。

Runloop 和线程是以一一对应联系,保存在大局字典里边,线程目标作为 key,Runloop 为值。Runloop 会在第一次获取的时分创立。子线程的 Runloop 默认是不敞开的。Runloop 会在线程完毕后毁掉。

跟 Runloop 相关的五个类:CFRunloopRef、CFRunloopModeRef、CFRunloopSourceRef、CFRunloopTimerRef、CFRunloopObserverRef。CFRunloopModeRef 代表 Runloop 的运行办法。常见的运行办法有 defaultMode 和 UITrackingMode。

CFRunloopRef 是个结构体指针,结构体里边有三个集合分别是_commonModes、_commonModeItems、_modes,还有_currentMode和pthread。

每个 mode 里边包括source0、source1、timers、observers,假如没有这些东西,Runloop 会退出。observer 能够监听 Runloop 的六种状态,进入、退出、处理 source、处理 timer、开端等等、完毕等待,枚举值分别为kCFRunloopEntry、kCFRunloopExit、kCFRunloopBeforeSources、kCFRunloopBeforeTimers、kCFRunloopBeforeWating、kCFRunlooopAfterWating。

Runloop 的运行逻辑:

  1. 告知 observers,进入 Runloop;
  2. 告知 observers,处理 timers;
  3. 告知 observers,处理 sources;
  4. 处理 block;
  5. 处理 sources0;
  6. 假如存在 source1 就处理source1 ,跳转第8 步;
  7. 告知 observers 开端等待;
  8. 告知 observers 完毕等待(被唤醒)-> 处理 timers、处理GCD调度主行列、处理 source1;
  9. 处理 block;
  10. 根据之前的处理成果判别是否从第二部进入循环,或许退出;
  11. 告知 observers 退出 Runloop;

6. 内存办理

自从 iOS5 开端,OC 的内存办理由 MRC 升级到了 ARC,即主动引证计数。ARC是编译器、Runtime 一起作用下完成的。OC 的内存办理是通过引证计数来完成的,当一个目标的引证计数为 0 时,该目标就会被开释。引证计数是被存储在 isa 指针或许是 SideTable 里边,当 isa 指针供给的19 位不够运用时就会存到 SideTable。

SideTable 是内存办理的计划之一,包括了引证计数表和弱引证表。__weak 润饰的目标,就会被增加到弱引证表里边。一个 iOS 项目会大局保护一个 SideTables,SideTables 里边又有多个 SideTable(真机状况下是 8 个)。SideTable 里边有线程锁、RefcountMap、weak_table_t三个核心特点。

6.1 autorelease

  1. AutoreleasePool 并没有独立的结构,而是由若干个 AutoreleasePoolPage 以双向链表的办法组合而成。
  2. AutoreleasePooPage 是按线程一一对应的。
  3. AutoreleasePoolPage 每个目标会开辟 4096 个字节(虚拟内存一页的巨细),除了上面的实例变量所占空间,剩下的空间全部用来存储 Autorelease 目标的地址。
  4. 一个 AutoreleasePoolPage 的空间被占满时,会新建一个 AutoreleasePoolPage 目标,追加到链表尾部,后来的 Autorelease 目标参加到新建的 AutoreleasePoolPage 里边,是以栈的办法参加。里边有个 next 指针指向栈顶的下一个方位。

6.2 Autorelease 目标什么时分开释?

没有手动增加 AutoreleasePool 的状况下,Autorelease 目标是在当时的 Runloop 循环完毕时开释的,而它能够开释的原因是体系在每个 Runloop 迭代中都参加了主动开释池 push 和 pop。这些都是跟 Runloop 相关的:iOS 在主线程的 Runloop 中注册了两个 Observer。第一个 Observer 监听了 kCFRunloopEntry 事情,这个监听会调用objc_autoreleasePoolPush()。第二个 Observer 监听了 kCFRunloopBeforeExit 事情,会调用objc_autoreleasePoolPop()。

AutoreleasePool实践用处:

  1. 开发命令行工具,没有 UI结构给咱们创立 Runloop 的时分,能够敞开一个;
  2. 短时刻内创立大量目标内存暴增的时分;

7. 多线程

iOS 里边常用的线程计划有 NSThread、NSOperation、GCD。NSThread 是根据 p_thread的高层次封装,NSOperation是根据 GCD 的封装。NSOpeartion 不能够直接运用,通常状况下咱们会运用NSBlockOperation 和 NSInvocationOperation,前者的回调是 block 办法的,后者是办法。
咱们还能够运用 NSOperationQueue 控制并发数、增加依靠、撤销使命等等多种操作。GCD 是面向 C言语的接口,通过 block 的办法执行使命,运用起来代码更加聚合。

运用 GCD 的时分需求留意死锁。运用 sync 函数向当时串行行列中增加使命时就会发生死锁

8. 网络

8.1 了解 TCP 吗,请你说一下 TCP 树立衔接和开释衔接的进程?

TCP树立衔接需求通过所谓的“三次握手”。

  1. 客户端发送恳求报文,标志位 SYN=1,序列号 seq=x,奉告服务端“我想跟你树立衔接”;
  2. 服务端收到客户端发送的恳求,服务端回应客户端,标志位 ACK=1,SYN=1,seq=y,ack=x+1,该进程表明服务端乐意跟客户端树立衔接,“我收到你的恳求了,能够树立衔接”;
  3. 客户端收到服务端的回应,奉告服务端“我收到你的音讯了”,其中标志位 ACK=1,seq=x+1,ack=y+1;

之所以是三次握手而不是两次,是由于两边都需求确定对方有发送音讯和接纳音讯的能力,假如只要两次的话服务端是不知道客户端是否有接纳音讯的能力的。

TCP 开释衔接需求阅历“四次握手”。

  1. 客户端发送标志位 FIN=1 的报文给服务端,奉告服务端“我要跟你断开衔接了”;
  2. 服务端回来 ACK=1 的报文,奉告客户端“知道了,可是等我会儿,我或许还有音讯没给你发完”;
  3. 过一会服务端的音讯也发送完了,给客户端发送FIN=1的报文,告知客户端“我的音讯发完了,我这边下线了”;
  4. 客户端收到服务端的断开恳求后,回应一个 ACK=1,告知服务端“好的我也下线了”。

客户端通过 MSL 的时刻后开释端口。为什么要等这么一段时刻呢?由于假如服务端没有收到客户端最后一次的 ACK 承认报文,或许会重发,假如不等待这段时刻,那么新的进程敞开后占用之前的端口,新建的衔接就收到了服务端重发的 FIN 报文,刚树立的衔接就被断开了。

10. 数据耐久化

iOS 开发中常见的数据耐久化计划有这么几种:

  1. UserDefaults。实践上是对 plist 文件的操作,运用起来非常便利,常常用于记录一些用户数据。
  2. 归档。运用归档办法能够将 OC 目标存储到磁盘当中。
  3. 文件。直接将数据转为 Data 以文件的办法存入磁盘。
  4. Bundle。运用 XCode 像增加文件相同增加一个 Bundle,用户能够在设置中看到需求存储的内容。
  5. Plist,能够将集合中的数据保存为 Plist 格局的,比方字典或许数组。
  6. Sqlite3,轻量级的数据库。
  7. CoreData,苹果面向目标的耐久化结构,一般状况下很少用。

10. 规划办法

10.1 SOLID准则

  1. S 单一功用,或许单一责任。目标各司其职,具有单一功用。
  2. O 开闭准则,软件应该是对于扩展开发的,可是修改关闭的。没问题的代码就最好不要修改。
  3. L 里氏替换,凡是之前用Person 类能够完成的功用,那么换成 Person 的子类也应该能够支持。
  4. I 接口阻隔,客户端不应该依靠那些它用不到的接口,应该仅仅依靠它实践运用的办法。假如一个接口具有了多个办法,那么完成类就会完成所有的办法,代码会臃肿。
  5. D 依靠倒置,高层不依靠底层,应该依靠笼统。笼统不依靠细节,细节依靠笼统。对应的规划办法有工厂办法、模板办法、战略。