2019 知识点总结

屏幕快照 2019-08-15 下午5.31.28.png

1、Block 释放

追问 (1)Block本质?

  • Block本质就是一个OC对象,内部有isa指针。
  • Block是封装了函数调用和函数调用的环境的OC对象。

追问 (2)Block为什么会产生循环引用?

  • 如果block对当前对象的某一成员变量进行截获,block会对对应变量有一个强引用;当前block又因为当前对象对其有个强引用,就产生了个自循环引用问题。
  • 解决方案:通过__weak或者__block来解决,
  • 注意:__block 在mrc下不会产生循环引用,在arc下会产生循环引用。但是在arc下可以通过断环的方式解决循环引用,断环前提条件:当前block必须调用,并且要置空

   __block ViewController *weakSelf = self;
    void (^myBlock)(void) = ^{
        NSLog(@"%@",weakSelf.textLab.text);
        weakSelf  = nil;
    };
    myBlock();

追问 (3)在block内如何修改block外部变量?

注释:我们都知道:Block不允许修改外部变量的值,这里所说的外部变量的值,指的是栈中指针的内存地址。__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。
__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。

追问 (4)__block 修饰的基本数据类型变量,变成了结构体对象?

屏幕快照 2019-08-14 上午9.16.56.png

追问(5)block 的属性修饰词为什么用 copy?

Block默认存放在栈中,可能随时被销毁,需要作用域在堆中,所以只有copy后的Block才会在堆中,栈中的Block的生命周期是和栈绑定的。

  • 总结
    block内部没有调用外部局部变量时存放在全局区(ARC和MRC下均是)

block使用了外部局部变量,这种情况也正是我们平时所常用的方式。MRC:Block的内存地址显示在栈区,栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃,在对block进行copy后,block存放在堆区.所以在使用Block属性时使用copy修饰。但是ARC中的Block都会在堆上的,系统会默认对Block进行copy操作

用copy,strong修饰block在ARC和MRC都是可以的,都是在堆区

这就是为什么我们要用copy来修饰block。因为不用copy修饰的访问外部变量的block,只在他所在的函数被调用的那一瞬间可以使用。之后就消失了。

2、 栅栏函数 dispatch_barrier_sync(concurrentQueue,^{});坑点

栅栏函数 一定要是自定义的并发队列才有效果。

  • 自定义:dispatch_queue_t concurrentQueue = dispatch_queue_create("cooci", DISPATCH_QUEUE_CONCURRENT);
    
  • 非自定义:dispatch_queue_t concurrentQueue = dispatch_get_global_queue(0, 0);
    

追尾1、栅栏函数特点?

  • 保证顺序执行
  • 保证线程安全
  • 一定是自定义并发队列
    注意点: 1、一定是自定义并发队列 2、必须要求在同一个队列 3、

追尾2、 dispatch_barrier_sync 与 dispatch_barrier_async 区别?

  • dispatch_barrier_sync将自己的任务插入到队列的时候,需要等待自己的任务结束之后才会继续插入被写在它后面的任务,然后执行它们。
  • dispatch_barrier_async将自己的任务插入到队列之后,不会等待自己的任务结束,它会继续把后面的任务插入到队列,然后等待自己的任务结束后才执行后面任务。

3、 图形异步绘制

https://www.jianshu.com/p/6634dbdf2964

  • UIView的绘制原理

流程图


image
  • 当我们调用[UIView setNeedsDisplay]这个方法时,其实并没有立即进行绘制工作,系统会立刻调用CALayer的同名方法,并且会在当前layer上打上一个标记,然后会在当前runloop将要结束的时候调用[CALayer display]这个方法,然后进入我们视图的真正绘制过程
  • 而在[CALayer display]这个方法的内部实现中会判断这个layer的delegate是否响应displayLayer:这个方法,如果不响应这个方法,就会进入到系统绘制流程中;如果响应这个方法,那么就会为我们提供异步绘制的入口
上面就是UIView的绘制原理,接下来我们看一下系统绘制流程是怎样的
image
  • 在CALayer内部会先创建backing store,我可以理解为CGContext,我们一般在drawRect:方法中通过上下文堆栈当中取出栈顶的context,也就是上下文
  • 然后这个layer会判断是否有代理,如果没有代理,那么就会调用[CALayer drawInCotext:];如果有代理,会调用代理的drawLayer:inContext:方法,然后做当前视图的绘制工作(这一步是发生在系统内部的),然后在一个合适的时机给与我们这个十分熟悉的[UIView drawRect:]方法的回调,[UIView drawRect:]这个方法默认是什么都不做,,系统给我们开这个口子是为了让我们可以再做一些其他的绘制工作
  • 然后无论是哪个分支,最终都会由CALayer上传对应的backing store(可以理解为位图)给GPU,然后就结束了系统默认的绘制流程
  • 1 在layer内部会创建一个backing store,我们可以理解为CGContextRef上下文。
  • 2 判断layer是否有delegate:
    • 2.1 如果有delegate,则会执行[layer.delegate drawLayer:inContext](这个方法的执行是在系统内部执行的),然后在这个方法中会调用view的drawRect:方法,也就是我们重写view的drawRect:方法才会被调用到。
    • 2.2 如果没有delegate,会调用layer的drawInContext方法,也就是我们可以重写的layer的该方法,此刻会被调用到。
  • 3 然后无论是哪个分支,最终都会由CALayer上传对应的backing store(可以理解为位图)给GPU,然后就结束了系统默认的绘制流程。w

那么问题来了,我们如何进行异步绘制呢?实际上我们就需要借用系统给开的这个口子,即[layer.delegate displayLayer:]

追问1、什么是异步绘制?

展示界面的过程中将创建上下文和控件的绘制工作放到子线程中, 子线程将那些工作完成渲染成图片后转回主线程然后将图片展示在界面上;

1、异步绘制的入口在[layer.delegate displayLayer]
2、异步绘制过程中代理负责生成对应的位图(bitmap);
3、将bitmap赋值给layer.content属性;

知识点:为什么调用UIView的setNeedsDisplay后界面并没有立即绘制?
在当前Runloop将要结束的时候才会开始界面的绘制;

YYAsyncLayer 异步绘制源码
YYAsyncLayer
它的主要处理流程如下:

1、在主线程的runLoop中注册一个observer,它的优先级要比系统的CATransaction要低,保证系统先做完必须的工作。
2、把需要异步绘制的操作集中起来。比如设置字体、颜色、背景这些,不是设置一个就绘制一个,把他们都收集起来,runloop会在observer需要的时机通知统一处理。
3、处理时机到时,执行异步绘制,并在主线程中把绘制结果传递给layer.contents。

@implementation YYAsyncLayer {
    YYSentinel *_sentinel;
}

#pragma mark - Override

+ (id)defaultValueForKey:(NSString *)key {
    if ([key isEqualToString:@"displaysAsynchronously"]) {
        return @(YES);
    } else {
        return [super defaultValueForKey:key];
    }
}

- (instancetype)init {
    self = [super init];
    static CGFloat scale; //global
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        scale = [UIScreen mainScreen].scale;
    });
    self.contentsScale = scale;
    _sentinel = [YYSentinel new];
    _displaysAsynchronously = YES;
    return self;
}

- (void)dealloc {
    [_sentinel increase];
}

- (void)setNeedsDisplay {
    [self _cancelAsyncDisplay];
    [super setNeedsDisplay];
}

- (void)display {
    super.contents = super.contents;
    [self _displayAsync:_displaysAsynchronously];
}

#pragma mark - Private

- (void)_displayAsync:(BOOL)async {
    __strong id<YYAsyncLayerDelegate> delegate = (id)self.delegate;
    YYAsyncLayerDisplayTask *task = [delegate newAsyncDisplayTask];
    if (!task.display) {
        if (task.willDisplay) task.willDisplay(self);
        self.contents = nil;
        if (task.didDisplay) task.didDisplay(self, YES);
        return;
    }
    
    if (async) {
        if (task.willDisplay) task.willDisplay(self);
        YYSentinel *sentinel = _sentinel;
        int32_t value = sentinel.value;
        BOOL (^isCancelled)() = ^BOOL() {
            return value != sentinel.value;
        };
        CGSize size = self.bounds.size;
        BOOL opaque = self.opaque;
        CGFloat scale = self.contentsScale;
        CGColorRef backgroundColor = (opaque && self.backgroundColor) ? CGColorRetain(self.backgroundColor) : NULL;
        if (size.width < 1 || size.height < 1) {
            CGImageRef image = (__bridge_retained CGImageRef)(self.contents);
            self.contents = nil;
            if (image) {
                dispatch_async(YYAsyncLayerGetReleaseQueue(), ^{
                    CFRelease(image);
                });
            }
            if (task.didDisplay) task.didDisplay(self, YES);
            CGColorRelease(backgroundColor);
            return;
        }
        
        dispatch_async(YYAsyncLayerGetDisplayQueue(), ^{
            if (isCancelled()) {
                CGColorRelease(backgroundColor);
                return;
            }
            UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
            CGContextRef context = UIGraphicsGetCurrentContext();
            if (opaque) {
                CGContextSaveGState(context); {
                    if (!backgroundColor || CGColorGetAlpha(backgroundColor) < 1) {
                        CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
                        CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
                        CGContextFillPath(context);
                    }
                    if (backgroundColor) {
                        CGContextSetFillColorWithColor(context, backgroundColor);
                        CGContextAddRect(context, CGRectMake(0, 0, size.width * scale, size.height * scale));
                        CGContextFillPath(context);
                    }
                } CGContextRestoreGState(context);
                CGColorRelease(backgroundColor);
            }
            task.display(context, size, isCancelled);
            if (isCancelled()) {
                UIGraphicsEndImageContext();
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (task.didDisplay) task.didDisplay(self, NO);
                });
                return;
            }
            UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            if (isCancelled()) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    if (task.didDisplay) task.didDisplay(self, NO);
                });
                return;
            }
            dispatch_async(dispatch_get_main_queue(), ^{
                if (isCancelled()) {
                    if (task.didDisplay) task.didDisplay(self, NO);
                } else {
                    self.contents = (__bridge id)(image.CGImage);
                    if (task.didDisplay) task.didDisplay(self, YES);
                }
            });
        });
    } else {
        [_sentinel increase];
        if (task.willDisplay) task.willDisplay(self);
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, self.opaque, self.contentsScale);
        CGContextRef context = UIGraphicsGetCurrentContext();
        if (self.opaque) {
            CGSize size = self.bounds.size;
            size.width *= self.contentsScale;
            size.height *= self.contentsScale;
            CGContextSaveGState(context); {
                if (!self.backgroundColor || CGColorGetAlpha(self.backgroundColor) < 1) {
                    CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
                    CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
                    CGContextFillPath(context);
                }
                if (self.backgroundColor) {
                    CGContextSetFillColorWithColor(context, self.backgroundColor);
                    CGContextAddRect(context, CGRectMake(0, 0, size.width, size.height));
                    CGContextFillPath(context);
                }
            } CGContextRestoreGState(context);
        }
        task.display(context, self.bounds.size, ^{return NO;});
        UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        self.contents = (__bridge id)(image.CGImage);
        if (task.didDisplay) task.didDisplay(self, YES);
    }
}

- (void)_cancelAsyncDisplay {
    [_sentinel increase];
}

4、什么是Block,Block的本质是什么?

  • block本质上也是一个OC对象,它内部也有个isa指针
  • block是封装了函数调用以及函数调用环境的OC对象
  • block是封装函数及其上下文的OC对象

5、iOS开发过程中的几种内存泄露情况?

  • Block循环引用
  • delegate循环引用问题
  • NSTimer循环引用
  • 非OC对象内存处理
    例子:
    • 常用的滤镜操作调节图片亮度
    • CGImageRef类型变量非OC对象,其需要手动执行释放操作CGImageRelease(ref),否则会造成大量的内存泄漏导致程序崩溃
    • CoreFoundation框架下的某些对象或变量需要手动释放、C语言代码中的malloc等需要对应free等都需要注意。
  • 地图类处理
  • 大次数循环内存暴涨问题
  • 数组越界
  • WKWebView 造成的内存泄漏

理由: 但是其实 “addScriptMessageHandler” 这个操作,导致了 wkWebView 对 self 进行了强引用,然后 “addSubview”这个操作,也让 self 对 wkWebView 进行了强引用,这就造成了循环引用。
解决方案: 解决方法就是在合适的机会里对 “MessageHandler” 进行移除操作。

  • NSNotification
@property (nonatomic, strong) id observer; //持有注册通知后返回的对象

__weak __typeof__(self) weakSelf = self;
 _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey"
                                                               object:nil
                                                                queue:nil
                                                           usingBlock:^(NSNotification *note) {
     __typeof__(self) strongSelf = weakSelf;
     [strongSelf dismissModalViewControllerAnimated:YES];
 }];

6、http链接的建立流程

  • 首先,通过tcp的三次握手建立链接。
  • 进行http请求与响应的传递。
  • 经历tcp的四次挥手断开链接

追问1、http长连接?

  • HTTP 1.0规定浏览器与服务器只保持短暂的连接
  • 为了克服HTTP 1.0的缺陷,HTTP 1.1支持持久连接(HTTP/1.1的默认模式使用带流水线的持久连接),在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。
    如下图
    http长连接会在响应头上添加Connection(对应keep-alive、close)、Keep-Alive(对应timeout,持久链接保持的时长)
    image

7、TCP为什么是三次握手?而不是两次握手?

  • 如果说Clinet第一次发送链接建立请求的SYN同步报文,在网络路由传输过程中发生了超时,此时Client(客户端)会启用超时重传策略,重新发送一份SYN同步报文;如果Server端收到SYN同步报文之后,会向Client端发送同步SYN确认ACK报文。如果是两次握手情况下,此时TCP链接已经建立,假设刚才超时的SYN同步报文,在Server端发送给Client端SYN,ACK之后,被server端接收到,此时Server端会认为Client再次发送了TCP链接。三次握手协议就可以规避这种情况的发生。
  • 答案:防止已失效的连接请求又传送到服务器端,因而产生错误

8、http的特点?对应特点的解决方案?

  • 无连接
    • 无连接解决方案:HTTP的持久链接
  • 无状态
    • 无状态解决方案:Cookie/Session
      ==================================
      补充
  • 持久链接:客户端和Server端进行多次交互的时候,在同一条tcp链接上进行,不会多次创建tcp链接。持久链接可以提升网络请求响应的效率
  • 非持久链接:客户端和Server端进行多次交互的时候,每次交互都重新建立新的tcp链接,进行数据交互。

追问1、cookie和session有什么区别?

  • 共同之处
    cookie和session都是用来跟踪浏览器用户身份的绘画方式

  • 不同之处

    • 1、存储位置不同

      cookie的数据信息存放在客户端浏览器上。

      session的数据信息存放在服务器上。

    • 2、存储容量不同

      单个cookie保存的数据<=4KB,一个站点最多保存20个Cookie。

      对于session来说并没有上限,但出于对服务器端的性能考虑,session内不要存放过多的东西,并且设置session删除机制

    • 3、存储方式不同

      cookie中只能保管ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据。

      session中能够存储任何类型的数据,包括且不限于string,integer,list,map等。

    • 4、隐私策略不同

      cookie对客户端是可见的,别有用心的人可以分析存放在本地的cookie并进行cookie欺骗,所以它是不安全的。

      session存储在服务器上,对客户端是透明对,不存在敏感信息泄漏的风险。

    • 5、有效期上不同

      开发可以通过设置cookie的属性,达到使cookie长期有效的效果。
      session不能达到长期有效的效果。

    • 6、跨域支持上不同

      cookie支持跨域名访问。

      session不支持跨域名访问。

(1)cookie数据存放在客户的浏览器上,session数据放在服务器上
(2)cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,如果主要考虑到安全应当使用session
(3)session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,如果主要考虑到减轻服务器性能方面,应当使用COOKIE
(4)单个cookie在客户端的限制是3K,就是说一个站点在客户端存放的COOKIE不能3K。
(5)所以:将登陆信息等重要信息存放为SESSION;其他信息如果需要保留,可以放在COOKIE中
屏幕快照 2019-09-12 上午9.18.19.png

9、https的链接建立流程?

屏幕快照 2019-08-15 下午5.31.28.png

追问1、https证书包含哪些信息?

  • 1、有关你的企业/组织(或个人)的信息。
  • 2、域名。
  • 3、证书特有的加密密钥(公私钥)

追问2、socket 连接和 Http 连接的区别?

  • HTTP协议:简单对象访问协议,对应于应用层,HTTP协议是基于TCP连接的
  • tcp协议: 对应于传输层
  • ip协议: 对应于网络层
    • TCP/IP是传输层协议,主要解决数据如何在网络中传输;
    • 而HTTP是应用层协议,主要解决如何包装数据。
  • Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。

10、TCP为什么要四次挥手?

因为TCP是全双工传输。所以链接断开是双方向的。

  • Client主动发起链接终止报文FIN,Server收到消息之后,往客户端发送ACK确认报文,此时客户端向server端的通信就断开,整个链接处于半断开状态。(此时如果server还是可以往Client发送数据)
  • Server发起终止确认的报文FIN、ACK到Client端,来断开server到client的链接,client发送ACK报文到server端,此时链接完全断开。
    • 全双工:允许数据在两个方向上同时传输。TCP
    • 半双工:允许数据在两个方向上传输,但是同一时间数据只能在一个方向上传输。UDP
    • 单工:数据只在一个方向上传输,不能实现双方通信。

11、class_ro_t 和 class_rw_t 的区别?

  • 底层源码
struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;
};

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    uint32_t reserved;

    const uint8_t * ivarLayout;

    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};
  • class_rw_t 中包括

    • method_array_t 方法数组
    • property_array_t 属性数组
    • protocol_array_t 代理数组
    • class_ro_t
  • class_ro_t 中包括

    • name 类名
    • method_list_t 方法列表
    • property_list_t 属性列表
    • protocol_list_t 代理列表
    • ivar_list_t 成员变量列表
  • class_rw_t结构体内有一个指向class_ro_t结构体的指针。

    每个类都对应有一个class_ro_t结构体和一个class_rw_t结构体。在编译期间,class_ro_t结构体就已经确定,objc_class中的bits的data部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtime的realizeClass 方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址。

  • OBJC2 class结构

图片

struct objc_class 的结构

12、iOS 定时器的创建方式?

  • NSTime

    • 缺点:不精准、target-action设计会产生循环引用

    • 不精准原因分析: 定时器被添加在主线程中,由于定时器在一个RunLoop中被检测一次,所以如果在这一次的RunLoop中做了耗时的操作,当前RunLoop持续的时间超过了定时器的间隔时间,那么下一次定时就被延后了。

    • 不精准解决方案:
      1、在子线程中创建timer,在主线程进行定时任务的操作
      2、在子线程中创建timer,在子线程中进行定时任务的操作,需要UI操作时切换回主线程进行操作

    • 循环引用原因分析:由于timer会添加到Runloop里面,RunLoop会强引用timer,timer会强引用Target(此处target一般为self,会形成runloop ===> timer === > self,此时runloop不退出,self和time就不会释放),容易造成循环引用、内存泄露等问题。

    • 循环引用解决方案:
      1、不使用计时器的时候把计时器销毁置空。
      2、消息转发
      循环引用消息转发 图解

  • ADisplayLink
    CADisplayLink是基于屏幕刷新的周期,所以其一般很准时,每秒刷新60次。其本质也是通过RunLoop,所以不难看出,当RunLoop选择其他模式或被耗时操作过多时,仍旧会造成延迟。

// 创建CADisplayLink
CADisplayLink *disLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(linkMethod)];
// 添加至RunLoop中
[disLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
// 终止定时器
[disLink invalidate];
// 销毁对象
disLink = nil;
  • dispatch_source_t
    • 缺点:Dispatch Source使用最多的就是用来实现定时器,source创建后默认是暂停状态,需要手动调用dispatch_resume启动定时器。
      • 1、循环引用:因为dispatch_source_set_event_handler回调是个block,在添加到source的链表上时会执行copy并被source强引用,如果block里持有了self,self又持有了source的话,就会引起循环引用。正确的方法是使用weak+strong或者提前调用dispatch_source_cancel取消timer。
      • 2、dispatch_resume和dispatch_suspend调用次数需要平衡,如果重复调用dispatch_resume则会崩溃,因为重复调用会让dispatch_resume代码里if分支不成立,从而执行了DISPATCH_CLIENT_CRASH("Over-resume of an object")导致崩溃。
      • 3、source在suspend状态下,如果直接设置source = nil或者重新创建source都会造成crash。正确的方式是在resume状态下调用dispatch_source_cancel(source)释放当前的source。
        https://blog.csdn.net/u013602835/article/details/87623497
  • 优点:精准
    我们知道,RunLoop是dispatch_source_t实现的timer,所以理论上来说,GCD定时器的精度比NSTimer只高不低。

13、Dealloc方法在哪个线程中执行?

一个对象的dealloc方法,会在该对象的引用计数变为0的线程被调用。

14、runtime如何通过selector找到对应的IMP地址?

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

15、iOS开发中实现多线程的方式?

  • pthread: 跨平台,适用于多种操作系统,可移植性强,是一套纯C语言的通用API,且线程的生命周期需要程序员自己管理,使用难度较大,所以在实际开发中通常不使用。
  • NSThread: 基于OC语言的API,使得其简单易用,面向对象操作。线程的声明周期由程序员管理,在实际开发中偶尔使用。优点:NSThread 比其他两个轻量级 缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销
  • GCD: 基于C语言的API,充分利用设备的多核,旨在替换NSThread等线程技术。线程的生命周期由系统自动管理,在实际开发中经常使用。有点:多核编程的解决方法,简单、高效
  • NSOperation: 基于OC语言API,底层是GCD,增加了一些更加简单易用的功能,使用更加面向对象。线程生命周期由系统自动管理,在实际开发中经常使用。优点:不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上。

追问1、pthread中的p代表什么意思?

p意思是POSIX可移植操作系统接口

16、如何在不使用GCD和NSOperation、NSThread的情况下,实现异步线程?

  • performSelectorInBackground 后台执行
  • [self performSelectorInBackground:@selector(test) withObject:nil];
    
  • performSelector:onThread:在指定线程执行
  • [self performSelector:@selector(test) onThread:[NSThread currentThread] withObject:nil waitUntilDone:YES];
    

这个方法有一个thread参数是指定执行的线程,但是很奇怪当我使用自己创建的线程 [[NSThread alloc] init];时,并不会执行test方法,只有当使用[NSThread currentThread]时才会执行:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [self performSelector:@selector(tests) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
});

链接

17、iOS 事件传递的流程?

当我们点击屏幕的某个位置时,该事件会传递给UIApplication,再由UIApplication传递给UIWindow,UIWindow里面会根据hitTest:withEvent:来返回响应的视图,在系统内部会调用pointInside:withEvent:判断当前点击的point是否在UIWindow内。如果是则继续依次调用其 subView的hitTest:withEvent:方法,直至没有更合适的view为止。


屏幕快照 2019-09-17 下午3.34.47.png

18、hitTest:withEvent:方法的内部实现?

  • 1、首先在hitTest:withEvent:方法内部会判断当前视图是否隐藏hidden、是否可交互userinteractionEnabled、透明度alpha是否大于0.01等属性,
  • 2、只有视图不隐藏、可以交互、透明度大于0.01才会调用pointInside:withEvent来判断点击的点是否在视图范围之内。
  • 3、否则他会返回nil,也就是说当前视图不作为事件响应者,再由父视图遍历同级的兄弟节点视图,调用hitTest:withEvent:方法。
  • 4、如果pointInside:withEvent返回NO判断不在当前视图内,再次进入流程3。
  • 5、如果pointInside:withEvent返回yes,则会倒叙遍历当前视图的子视图,调用其hitTest:withEvent:方法。如果某个子视图返回了事件响应的视图,就会把对应的视图作为最终事件响应的视图。
屏幕快照 2019-09-17 下午3.49.50.png

19、事件的响应过程?

事件响应会先从底层最合适的view开始,然后随着上一步找到的链一层一层响应touch事件。默认touch事件会传递给上一层。如果到了viewcontroller的view,就会传递给viewcontroller。如果viewcontroller不能处理,就会传递给UIWindow。如果UIWindow无法处理,就会传递给UIApplication。如果UIApplication无法处理,就会传递给UIApplicationDelegate。如果UIApplicationDelegate不能处理,则会丢弃该事件。

20、说一下 Runtime 的方法缓存?存储的形式、数据结构以及缓存查找的过程?

cache_t增量扩展的哈希表结构。哈希表内部存储的 bucket_t
bucket_t 中存储的是 SEL(方法名)IMP(方法实现)的键值对。

  • 如果是有序方法列表,采用二分查找。
  • 如果是无序方法列表,直接遍历查找。

21、方法缓存查找?

struct objc_class : objc_object {
    // 这里没写 isa,其实继承了 objc_object 的 isa , 在这里 isa 是一个指向元类的指针
    // Class ISA;
    Class superclass;           // 指向当前类的父类
    cache_t cache;              // formerly cache pointer and vtable
                                // 用于缓存指针和 vtable,加速方法的调用
    class_data_bits_t bits;     // class_rw_t * plus custom rr/alloc flags
                                // 相当于 class_rw_t 指针加上 rr/alloc 的标志
                                // bits 用于存储类的方法、属性、遵循的协议等信息的地方

    // 针对 class_data_bits_t 的 data() 函数的封装,最终返回一个 class_rw_t 类型的结构体变量
    // Objective-C 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中
    class_rw_t *data() { 
        return bits.data();
    }
    
    ...
};
  • 缓存查找

通过给定的方法选择器SEL,通过哈希查找,找到对应的bucket_t在数组中的位置。bucket_t 中存储的是 SEL(方法名)IMP(方法实现)的键值对。
Class内部结构中有一个方法缓存cache_t,用散列表(哈希表)来缓存之前调用过的方法,可以提高方法的查找速度.

struct  cache_t {
  struct bucket_t *_buckets;//散列表
  mask_t _mask;//散列表的长度 -1
  mask_t _occupied;//已经缓存的方法数量
};
struct bucket_t {
  cache_key_t _key;//SEL作为key
  IMP _imp;//函数的内存地址
}
  • 方法在当前类中的查找过程

    • 对于已排序好的方法列表,采用二分查找。
    • 对于没排序好的方法列表,直接遍历查找。

追问1、方法的数据结构

struct objc_method {
    // 方法名
    SEL method_name                                          OBJC2_UNAVAILABLE;
   // 方法类型
    char *method_types                                       OBJC2_UNAVAILABLE;
   //  方法实现
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;

typedef struct objc_method *Method;

22、使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?

无论在MRC下还是ARC下均不需要,被关联的对象在生命周期内要比对象本身释放的晚很多。它会在obj dealloc时候会调用object_dispose,检查有无关联对象,有的话_object_remove_assocations删除

23、Class、Category数据结构

  • Category
    如下Category包含实例方法链表、类方法链表、协议链表、属性链表。并没有实例变量链表,可以添加属性,但是没有生成对应的setter、getter方法。


    屏幕快照 2019-09-18 下午1.40.34.png
  • Class
    objc_class 数据结构中包含了实例变量链表(objc_ivar_list)、方法链表(objc_method_list)、方法缓存(objc_cache)、协议链表(objc_protocol_list)
//对象
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};
//类
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;

struct objc_class {

    struct objc_class * isa; /* 指向元类,元类里面存放这所有的类方法*/
    struct objc_class * super_class;  /*父类*/
    const char *name;                 /*类名字*/
    long version;                   /*版本信息*/
    long info;                        /*类信息*/
    long instance_size;               /*实例大小*/
    struct objc_ivar_list *ivars;     /*实例参数链表*/
    struct objc_method_list **methodLists;  /*方法链表*/
    struct objc_cache *cache;               /*方法缓存*/
    struct objc_protocol_list *protocols;   /*协议链表*/

};//  存放类的结构的对象 isa 也称为元类对象

24、_objc_msgForward函数是做什么的,直接调用它将会发生什么?

_objc_msgForward是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。

  • 1.调用resolveInstanceMethod:方法,允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回。如果仍没实现,继续下面的动作。

  • 2.调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。

  • 3.调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。

  • 4.调用forwardInvocation:方法,将地3步获取到的方法签名包装成Invocation传入,如何处理就在这里面了。
    消息转发地址

25、性能优化

  • 一、电池电量优化方案。(CPU处理、网络、定位、图像)

    • (一)、代码层面

      • 1、合理使用NSDateFormatter 和 NSCalendar这种高开销对象
        性能测试表明,NSDateFormatter的性能瓶颈是由于NSDate格式到NSString格式的转化,所以把NSDateFormatter创建单例意义不大.推荐的做法是,把最常用到的日期格式做缓存.
      • 2、不要频繁的刷新页面,能刷新1行cell最好只刷新一行,尽量不要使用reloadData.
      • 3、选择正确的集合
        • NSArray,使用index来查找很快(插入和删除很慢)
        • 字典,使用键来查找很快
        • NSSets,是无序的,用键查找很快,插入/删除很快
      • 4、少用运算获得圆角,不论view.maskToBounds还是layer.clipToBounds都会有很大的资源开销,必须要用圆角的话,不如把图片本身就做成圆角
      • 5、懒加载,不要一次性创建所有的subview,而是需要时才创建.
      • 6、重用
        可以模仿UITableView和UICollectionView,不要一次性创建所有的subview,而是需要时才创建.完成了使命,把他放入到一个可重用集合中
      • 7、图片处理
      • 8、cache,cache,cache(缓存所有需要的)
      • 9、尽量少用透明或半透明,会产生额外的运算.
      • 10、使用ARC减少内存失误,dealloc需要重写并对属性置为nil
      • 11、避免庞大的xib,storyBoard,尽量使用纯代码开发
      • 12、合理的地图定位标准制定以及后台功能使用
    • (二)、CPU层面

      • 1、Timer的时间间隔不宜太短,满足需求即可
      • 2、线程适量,不宜过多,不要阻塞主线程
      • 3、优化算法,减少循环次数
      • 4、定位和蓝牙按需取用,定位之后要关闭或降低定位频率
  • 二、APP启动优化。

    • (一)、main()函数执行前
      • 1、减少动态库静态库等Mach-O文件的加载
        (可以将多个动态库合并,非系统动态库最多6个合为一个)
      • **2、合并或者删减一些OC类,关于清理项目中没用到的类,可以借助AppCode代码检查工具
        **
      • 3、用+initialize方法和dispatch_once取代所有的ObjC的+load
      • 4、尽量不要用C++虚函数(创建虚函数表有开销)
      • 5、合并功能类似的类和扩展(Category)
      • 6、压缩资源图片
    • (二)、main()函数执行后
      • 1、尽量使用纯代码编写,减少xib的使用
      • 2、启动阶段的网络请求,是否都放到异步请求
      • 3、一些耗时的操作放到后面去执行,或异步执行等
      • 4、优化rootViewController加载,减少或延后加载不需要的视图及逻辑、网络请求的优化。。。
        数据本地缓存,先布局视图,加载本地缓存,再加载网络资源
      • 5、数据本地缓存,先布局视图,加载本地缓存,再加载网络资源

26、Category 在编译过后,是在什么时机与原有的类合并到一起的?

  • 1、程序启动后,通过编译之后,Runtime 会进行初始化,调用 _objc_init

  • 2、然后会调用 map_images

  • 3、接下来调用 map_images_nolock

  • 4、再然后就是 read_images,这个方法会读取所有的类的相关信息。

  • 5、最后是调用 reMethodizeClass:,这个方法是重新方法化的意思。

  • 6、在 reMethodizeClass: 方法内部会调用 attachCategories: ,这个方法会传入 Class 和 Category ,会将方法列表,协议列表等与原有的类合并。最后加入到 class_rw_t结构体中。
    注意:

  • attachCategories里面调用了rw->methods.attachLists(mlists, mcount); 把新增分类中的方法列表添加到实际运行时查询的方法列表头部。

  • 在进行方法调用时会从头部查询,一旦查到后就返回结果,因此最后参与编译的文件中的方法会被优先调用。


    屏幕快照 2019-09-27 上午11.00.59.png
static void attachCategories(Class cls, category_list *cats, bool flush_caches)
{
    if (!cats) return;

    bool isMeta = cls->isMetaClass();

    //新建数组指针
    method_list_t **mlists = (method_list_t **)
        malloc(cats->count * sizeof(*mlists));
    property_list_t **proplists = (property_list_t **)
        malloc(cats->count * sizeof(*proplists));
    protocol_list_t **protolists = (protocol_list_t **)
        malloc(cats->count * sizeof(*protolists));

    int mcount = 0;
    int propcount = 0;
    int protocount = 0;
    int i = cats->count;//倒序获取最新的分类
    bool fromBundle = NO;
    while (i--) {
        auto& entry = cats->list[i];
        //分别获取列表
        method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
        if (mlist) {
            mlists[mcount++] = mlist;
            fromBundle |= entry.hi->isBundle();
        }

        property_list_t *proplist = 
            entry.cat->propertiesForMeta(isMeta, entry.hi);
        if (proplist) {
            proplists[propcount++] = proplist;
        }

        protocol_list_t *protolist = entry.cat->protocols;
        if (protolist) {
            protolists[protocount++] = protolist;
        }
    }

    auto rw = cls->data();
    //加载列表到rw中
    prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
    rw->methods.attachLists(mlists, mcount);
    free(mlists);
    if (flush_caches  &&  mcount > 0) flushCaches(cls);

    rw->properties.attachLists(proplists, propcount);
    free(proplists);

    rw->protocols.attachLists(protolists, protocount);
    free(protolists);
}
void attachLists(List* const * addedLists, uint32_t addedCount) {
        if (addedCount == 0) return;

        if (hasArray()) {
            // many lists -> many lists
            uint32_t oldCount = array()->count;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
            array()->count = newCount;
            memmove(array()->lists + addedCount, array()->lists, 
                    oldCount * sizeof(array()->lists[0]));
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
        } 
        else {
            // 1 list -> many lists
            List* oldList = list;
            uint32_t oldCount = oldList ? 1 : 0;
            uint32_t newCount = oldCount + addedCount;
            setArray((array_t *)malloc(array_t::byteSize(newCount)));
            array()->count = newCount;
            if (oldList) array()->lists[addedCount] = oldList;
            memcpy(array()->lists, addedLists, 
                   addedCount * sizeof(array()->lists[0]));
        }
    }

27、属性关键字

https://www.2cto.com/kf/201901/790916.html

  • 属性关键字分为三类:
    • 原子操作: nonatomic、atomic
    • 内存管理语义:assign、weak、copy、strong
    • 读写权限:readwrite、readonly
  • 一、为什么NSString使用copy?

    • 不可变字符串:经过copy之后,还是不可变字符串,为浅copy,新对象和旧对象指针指向同一块内存地址。无影响
    • 可变字符串:经过copy之后,变成不可变字符串,为深copy,新对象和旧对象指针指向两块不同的内存地址(我们队原对象值进行修改,并不会影响新对象的值)。此时,如果我们使用strong、retain关键字修饰,会对原对象进行浅copy,新旧对象内存地址相同,对原对象修改的同时,也会影响新对象
  • 二、@property (copy) NSMutableArray *array;这写法有什么问题?

    • 如果赋值过来的是NSMutableArray,经过copy操作之后是NSArray。
    • 如果赋值过来的是NSArray,经过copy之后也是NSArray
      此时如果我们对array,进行add:、remove:等操作,就会crash
    • 使用了atomic属性会严重影响性能


      Snip20190930_1.png
  • 三、MRC下如何重写retain修饰变量的setter方法?

@property (nonatomic, retain) id obj;

- (void)setObj:(id)obj{
    if (_obj != obj) {
        [_obj release];
        _obj = [obj retain];
    }
}

追问1、为什么要做_obj != obj这个判断呢?

如果我们传递过来的obj对象就是_obj对象,当我们[_obj release]释放旧对象时,就是对传递过来的obj进行释放,此时obj对象已经被我们释放了,如果我们在通过ojb指针去访问一个被释放废弃的对象,就会crash。所以要进行判等。

28、ES6新特性

  • 不一样的变量声明:const和let
  • 模板字面量
  • 对象和数组解构
  • for...of
  • ES6箭头函数
  • 函数的参数默认值
  • 对象超类
  • for...of 和 for...in
  • ES6 中支持 class 语法,不过,ES6的class不是新的对象继承模型,它只是原型链的语法糖表现形式。
    参考链接

29、malloc_size、class_getInstanceSize、sizeof区别?

受限于内存分配的机制,一个 NSObject对象都会分配 16byte 的内存空间。

但是实际上在 64位 下,只使用了 8byte;
在32位下,只使用了4byte

一个 NSObject 实例对象成员变量所占的大小,实际上是 8 字节

    NSObject *obj = [NSObject new];
    // 分配的内存大小
    size_t size  = malloc_size((__bridge const void *)obj);
    // 实际占用的内存大小
    size_t size1 =  class_getInstanceSize([NSObject class]);
    size_t size2 =  sizeof([NSObject class]);

    NSLog(@"%zu",size);
    NSLog(@"%zu",size1);
    NSLog(@"%zu",size2);
打印结果:
2019-11-04 14:16:50.190274+0800 SortAlgorithm[1509:73507] 16
2019-11-04 14:16:50.755659+0800 SortAlgorithm[1509:73507] 8
2019-11-04 14:16:51.352050+0800 SortAlgorithm[1509:73507] 8

30、下面这段代码的执行结果是什么?怎么处理?

 NSMutableArray *arr = [NSMutableArray arrayWithObjects:@"2",@"3",@"6",@"9",@"7",@"5", nil];
    for (NSString *str in arr) {
        [arr removeObject:str];
    }
  • 结果
*** Terminating app due to uncaught exception 'NSGenericException', 
reason: '*** Collection <__NSArrayM: 0x600003f977e0> was mutated 
while being enumerated.'
  • 原因

当程序出现这个提示的时候,是因为你一边便利数组,又同时修改这个数组里面的内容,导致崩溃

  • 解决方案
    将arr拷贝出一份arr1,遍历arr1,操作arr删除对象
    这里使用copy或者mutableCopy都可以,但不可以直接arr1 = arr;
  NSMutableArray *arr  = [NSMutableArray arrayWithObjects:@"2",@"3",@"6",@"9",@"7",@"5", nil];
 // 这里使用copy或者mutableCopy都可以,但不可以直接arr1 = arr;
  NSMutableArray *arr1 = [arr mutableCopy];
    for (NSString *str in arr1) {
        [arr removeObject:str];
    }

31、判断两个NSString的字面量是否相同,为什么要用isEqualToString来判断,而不能用==来判断呢?

可能大多数人会回答:因为==判断的是两个指针是否相等,而NSString是分配到堆上的,每次创建的时候,指针指向的地址的不同的,所以不能用==来判断。但是这样的回答不完整。这个题感觉有点毛病,字面量本身就只有一种方式,@""; 直接创建等于赋值,这种方式创建的类型都是__NSCFConstantString,本身也不会有引用计数,所以它就是一个对象,这个是可以用==来判断的,我想题的意思应该是NSString创建的字符串,是否相等,要用isEqualToString来判断,因为字符串的创建方式不同,类型不同,地址不同,单纯从==来判断的话,不准确。

32、简述你了解的锁?

互斥锁:NSLock、pthread_mutex、@synchronized。

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

递归锁:NSRecursiveLock。

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

条件锁:NSCondition、NSConditionLock。

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

信号量:dispatch_semaphore。

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

读写锁:pthread_rwlock。

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,067评论 1 32
  • 一:base.h 二:block.h 1. dispatch_block_flags:DISPATCH_BLOCK...
    小暖风阅读 2,394评论 0 0
  • 设计模式是什么? 你知道哪些设计模式,并简要叙述? 设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型的...
    iOS菜鸟大大阅读 698评论 0 1
  • 1.设计模式是什么? 你知道哪些设计模式,并简要叙述?设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型...
    龍飝阅读 2,116评论 0 12
  • 儿子如是说 亲爱的爸爸: 您好! 首先,我要感谢的便是您在我来到这个世界上的15年中,对我的陪伴和呵护,您这15年...
    幺妹_d554阅读 582评论 2 4