Source Code Learning - YYKit

微博 Demo 性能的优化

抛弃了以往在 Controller 中保存 Model 数组的传统方法,而是添加了用于存放布局和数据对象的数组,其中的布局是从 API 获取到数据后,在后台线程中计算后并保存的,这样当 Cell 需要显示时直接从内存中获取,不需要再进行计算。优点显而易见,缺点是 UIKit 的控件不是线程安全的,所以依赖 YYText 中的控件实现。

头像图片的圆角实现方式是在图片下载后,使用 Core Graphic 将图片用贝塞尔路径切成圆角后放到内存中使用。YYKit 的 Base 下有常用的 Category 实现,其中包括了切圆角的实现。

Macros 宏

TARGET_INTERFACE_BUILDER 宏用于区分代码是否需要在 IB 中执行

#if !TARGET_INTERFACE_BUILDER
    // this code will run in the app itself
#else
    // this code will execute only in IB
#endif

事务机制

在非 IB 中设置的 YYTextView 需要更新布局时使用了事务机制

[[YYTransaction transactionWithTarget:self selector:@selector(_updateIfNeeded)] commit];

事务提交时调用 YYTransactionSetup

static void YYTransactionSetup() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        transactionSet = [NSMutableSet new];
        CFRunLoopRef runloop = CFRunLoopGetMain();
        CFRunLoopObserverRef observer;

        observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
                                           kCFRunLoopBeforeWaiting | kCFRunLoopExit,
                                           true,      // repeat
                                           0xFFFFFF,  // after CATransaction(2000000)
                                           YYRunLoopObserverCallBack, NULL);
        CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes);
        CFRelease(observer);
    });
}
static void YYRunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    if (transactionSet.count == 0) return;
    NSSet *currentSet = transactionSet;
    transactionSet = [NSMutableSet new];
    [currentSet enumerateObjectsUsingBlock:^(YYTransaction *transaction, BOOL *stop) {
        [transaction.target performSelector:transaction.selector];
    }];
}

YYTransactionSetup 中创建一个RunLoop 的监听,当 Runloop 空闲或者即将退出时执行事务的 selector 即上面的 _updateIfNeeded 方法。

全局并发控制

使用 YYDispatchQueuePool 会创建一个指定优先级,MAX_QUEUE_COUNT 为 32 个串行队列分发池代替并行队列。使用串行队列可以控制线程数量,避免创建过多线程影响性能,但是串行队列又不能利用多核 CPU 优势,所以使用队列池来管理队列。

获取队列的实现如下

static dispatch_queue_t YYDispatchContextGetQueue(YYDispatchContext *context) {
    int32_t counter = OSAtomicIncrement32(&context->counter);
    // 如果越界,变为负数,则转为正数
    if (counter < 0) counter = -counter;
    void *queue = context->queues[counter % context->queueCount];
    return (__bridge dispatch_queue_t)(queue);
}

参数 YYDispatchContext 定义如下

typedef struct {
const char name; // 分发名称
void *
queues; // 队列数组指针
uint32_t queueCount; // 队列个数
int32_t counter; // 获取队列的次数
} YYDispatchContext;

C 语言的知识

strdup / strcpy / memcpy 的区别

if (name) {
     context->name = strdup(name);
}

strcpy 从源字符串中查找不是结束符 ‘\0’ 的内存都复制过去,如果目标字符串空间不够则会导致崩溃;memcpy 内存拷贝,不考虑字符串结束符,strdup 先用 malloc 函数分配与源字符串相同大小空间,然后将源字符串的内容复制到该内存地址,再将该地址返回。

好用的 YYFPSLabel

使用 Instruments 查看帧率不如直接在应用中查看来的直接,YYFPSLabel 可以直接显示帧率。

CADisplayLink _link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
// CADisplayLink 刷新执行的函数
- (void)tick:(CADisplayLink *)link {
    if (_lastTime == 0) {
        _lastTime = link.timestamp;
        return;
    }
    // 计算 fps
    _count++;
    NSTimeInterval delta = link.timestamp - _lastTime;
    // 不够 1s 不处理
    if (delta < 1) return;
    _lastTime = link.timestamp;
    float fps = _count / delta;
    _count = 0;
    // 显示 fps 的值 ...
}

CADisplayLink 可以以屏幕刷新率相同的频率将内容绘制到屏幕上的定时器,精确度相当高,但是如果调用的方法比较耗时,超过了屏幕刷新周期,就会导致跳过若干次回调调用机会。

NSTimer 的精确度稍低,比如 NSTimer 触发时间到的时候,如果 runloop 出在阻塞状态,触发时间就会被延迟到下一个 runloop 周期。

CADisplayLink 适合 UI 不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer 适合需要单次或循环定时处理的任务。

  • timestamp:当前时间的时间戳

  • frameInterval:NSInteger类型的值,用来设置间隔多少帧调用一次 selector方法 ,默认值是1,即每帧都调用一次。

  • duration:readOnly 的 CFTimeInterval 值,表示两次屏幕刷新之间的时间间隔。需要注意的是,该属性在 target 的 selector 被首次调用以后才会被赋值。selector 的调用间隔时间计算方式是:调用间隔时间 = duration × frameInterval

Timer 内存泄漏的避免

YYWeakProxy,利用消息转发机制,将消息 转发给 target,解决了 self 被 timer 强引用,timer 又被 runloop 强引用的问题。调用方法如下

_link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

实现机制

@property (nullable, nonatomic, weak, readonly) id target;

- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}

- (id)forwardingTargetForSelector:(SEL)selector {
    return _target;
}

target 为 weak,如果 _target 为空 就会调用如下方法,不实现如下方法,则会崩溃,所以作者应该是随便调用两个函数,保证不崩溃

- (void)forwardInvocation:(NSInvocation *)invocation {
    void *null = NULL;
    [invocation setReturnValue:&null];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    return [NSObject instanceMethodSignatureForSelector:@selector(init)];
} 

奇技淫巧

人都趋向于犯懒的,只是有的人偷懒是靠动脑子的

{
    NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"Shadow"];
    // ...
}
{
    NSMutableAttributedString *one = [[NSMutableAttributedString alloc] initWithString:@"Inner Shadow"];
    // ...
}

\uFFFC 为对象占位符,目的是当富文本中有图像时只复制文本信息

NSString *const YYTextAttachmentToken = @"\uFFFC”; 

成员变量访问修饰符 @package

@implementation YYTextContainer {
    @package
    BOOL _readonly;
}

@package 是框架级的,对于本框架内相当于 @protect,框架外相当于 @private

@interface TestClass : NSObject {
    @package NSString *testPackage;
}
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    TestClass *testClass = [[TestClass alloc] init];
    NSLog(@"%@", testClass->testPackage);   // OK
}
@end

使用方法

如果想要返回某个类的子类,可以实现 modelCustomClassForDictionary。例如

@implementation YYShape
+ (Class)modelCustomClassForDictionary:(NSDictionary*)dictionary {
        if (dictionary[@"radius"] != nil) {
            return [YYCircle class];
        } else if (dictionary[@"width"] != nil) {
            return [YYRectangle class];
        } else if (dictionary[@"y2"] != nil) {
            return [YYLine class];
        } else {
            return [self class];
        }
 }

参考文章

IBInspectable / IBDesignable

请我喝汽水儿