Skip to content

gozelus/iOSReview

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 

Repository files navigation

一.objc对象内存模型

二.Runtime

三.Category

四. KVO 原理

五. iOS App 启动过程

六. Block原理

七. GCD

八. Runloop

九. ARC

十. Tableview 优化

十一. 开源库

十二. Weex

十三. Tableview 优化

十. TCP/IP

十一. HTTP/HTTPS

一、对象内存模型

  • isa指针的作用

  • 对象的isa指向类对象,类对象的isa指向元类,元类的isa指向根元类,根元类的isa指向自己。

  • 类对象的superClass指针指向父类对象,直到指向根类对象,根类对象的superClass指向nil,元类也如此,直到根元类,根元类的superClass指向根类对象

  • 对象的内存分布

    • 对象的变量表

    • 对象大小

    • 对象方法表

    • cache?

    • 协议

    • char *name?

       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;
       /* Use `Class` instead of `struct objc_class *` */
      

Runtime

reference:objc runtime

  1. 消息接收

    • 编译时间确定接受到的消息,运行时间通过@selector找到对应的方法。
    • 消息接受者如果能直接找到@selector则直接执行方法,否则转发消息。若最终找不到,则运行时崩溃。
  2. 术语

    • SEL
      • 方法名的C字符串,用于辨别不同的方法。
      • 用于传递给objc_msgSend(self, SEL)函数
    • Class
      • Class是指向类对象的指针,继承于objc_object
    • categoty
      • categoty是结构体``categoty_t的指针:`typedef struct `categoty`_t *`categoty`; `
      • categoty是在app启动时加载镜像文件,通过read_imgs函数向类对象中的class_rw_t中的某些分组中添加指针,完成categoty的属性添加
    • Method
      • 方法包含以下
        • IMP:函数指针,方法的具体实现
        • types:char*,函数的参数类型,返回值等信息
        • SEL:函数名
  3. 消息转发

    • objc_msgSend()函数并不返回数据,而是它转发消息后,调用了相关的方法返回了数据。
    • 整个流程: 1. 检测这个 selector 是不是要忽略的。比如 Mac OS X 开发,有了垃圾回收就不理会 retain, release 这些函数了。 2. 检测这个 target 是不是 nil 对象。ObjC 的特性是允许对一个nil对象执行任何一个方法不会 Crash,因为会被忽略掉。 3. 如果上面两个都过了,那就开始查找这个类的 IMP,先从 cache 里面找,完了找得到就跳到对应的函数去执行。 4. 如果 cache 找不到就找一下方法分发表。 5. 如果分发表找不到就到超类的分发表去找,一直找,直到找到NSObject类为止。 6. 进入动态解析:resolveInstanceMethod:resolveClassMethod:方法 7. 若上一步返回NO,进入重定向:- (id)forwardingTargetForSelector:(SEL)aSelector+ (id)forwardingTargetForSelector:(SEL)aSelector 8. 若上一步返回的对象或者类对象仍然没能处理消息或者返回NO,进入消息转发流程:forwardInvocation
    • 重定向和消息转发都可以用于实现多继承
  4. Objective-C Associated Objects关联对象

    • OS X 10.6 之后,Runtime系统让Objc支持向对象动态添加变量。涉及到的函数有以下三个:
    void objc_setAssociatedObject ( id object, const void *key, id value, objc_AssociationPolicy policy );
    id objc_getAssociatedObject ( id object, const void *key );
    void objc_removeAssociatedObjects ( id object );
    • 这些方法以键值对的形式动态地向对象添加、获取或删除关联值。其中关联政策是一组枚举常量:
    enum {
          OBJC_ASSOCIATION_ASSIGN  = 0,
          OBJC_ASSOCIATION_RETAIN_NONATOMIC  = 1,
          OBJC_ASSOCIATION_COPY_NONATOMIC  = 3,
          OBJC_ASSOCIATION_RETAIN  = 01401,
          OBJC_ASSOCIATION_COPY  = 01403
    };
  5. Method Swizzling 方法混淆

    • 作用是修改SEL对应的IMP指针
    • 用于debug,避免数组越界等问题.

Categoty

reference:美团技术博客:深入理解Objective-C:Category

categoty 简介

  • categoty开头动态地给类添加属性及方法,通过这个特性,使得objc可以模拟多继承。也可以把一个类拆分到各个文件中,使用方可以按需加载。也可以多人协作共同完成一个类。

categoty 类比 extension

  • extension看起来很像一个匿名的categoty,但是extension和有名字的categoty几乎完全是两个东西。 extension在编译期决议,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension所以你无法为系统的类比如NSString添加extension

  • 但是categoty则完全不一样,它是在运行期决议的。 就categotyextension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而categoty是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的)。

categoty 内存结构

typedef struct `categoty`_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;
} `categoty`_t;

我们知道,所有的OC类和对象,在runtime层都是用struct表示的,categoty也不例外,在runtime层,categoty用结构体categoty_t(在objc-runtime-new.h中可以找到此定义),它包含了

  1. 类的名字(name)
  2. 类(cls)
  3. categoty中所有给类添加的实例方法的列表(instanceMethods
  4. categoty中所有添加的类方法的列表(classMethods
  5. categoty实现的所有协议的列表(protocols
  6. categoty中添加的所有属性(instanceProperties

category 的加载

category是在runtime进入入口时被添加进类的,runtime通过遍历编译后产生的DATA端上的category_t数组动态地将内容添加到类上。

需要注意的时,category中的方法其实并没有覆盖原类中的同名方法,只是单纯地添加到方法列表中而已。但是由于objc在方法调用中查找到类对象中的方法列表时,只要找到了第一个对应消息中的选择子的方法,就认为是找到了对应的方法并调用。所以形成了category中的方法会覆盖原类中的同名方法的假象。

根据这个原理,我们可以得知,如果有多个类别中同时都复写了类中的某个方法,那么最终调用的是最后被加载的category,也就是最后被编译的category文件。

category 中 Load 方法

category中也可以复写原类中的+Load方法,因为类的Load方法永远发生在categoryLoad方法之前。如果多个category都复写了Load方法,则categoryLoad执行顺序取决于对应category文件的编译顺序。

category 与关联对象

由于category中无法动态添加实例变量,所以可以通过关联对象的手段为对象增加实例变量:

#import "MyClass+Category1.h"
#import <objc/runtime.h>

@implementation MyClass (Category1)

+ (void)load
{
    NSLog(@"%@",@"load in Category1");
}

- (void)setName:(NSString *)name
{
    objc_setAssociatedObject(self,
                             "name",
                             name,
                             OBJC_ASSOCIATION_COPY);
}

- (NSString*)name
{
    NSString *nameObject = objc_getAssociatedObject(self, "name");
    return nameObject;
}

@end

关联对象存储在一张全局的map里,其中mapkey是被关联的对象的指针地址,该mapvalue存储的是另外一张map,此处称为AssociationsHashMap,该AssociationsHashMap的kv分别是设置关联对象时的kv。

KVO

  • 原理:

    1. isa swizzling方法,通过runtime动态创建一个中间类,继承自被监听的类。
    2. 使原有类的isa指针指向这个中间类,同时重写改类的Class方法,使得该类的Class方法返回自己而不是isa的指向。
    3. 复写中间类的对应被监听属性的setter方法,调用添加进来的方法,然后给当前中间类的父类也就是原类的发送setter消息。
  • 自定义KVO:

    1. 通过给NSObject添加分类的方法,添加新的对象方法:
        #import <Foundation/Foundation.h>
        @interface NSObject (ACoolCustom_KVO)
        /**
         *    自定义添加观察者
         *
         *    @param oberserver 观察者
         *    @param keyPath    要观察的属性
         *    @param block      自定义回调
         */
        - (void)zl_addOberver:(id)oberserver
                  forKeyPath:(NSString *)keyPath
                       block:(void(^)(id oberveredObject,NSString *keyPath,id newValue,id oldValue))block;
    1. 动态创建中间类, 更改原有类的isa指向,同时重写中间类的Class方法:
     Class originalClass = object_getClass(self);
     //创建中间类 并使其继承被监听的类
     Class kvoClass = objc_allocateClassPair(originalClass, kvoClassName.UTF8String, 0);
     //向runtime动态注册类
     objc_registerClassPair(kvoClass);
     object_setClass(self, kvoClass);
     //替换被监听对象的class方法...
     //原始类的class方法的实现
     //原始类的class方法的参数等信息
     Method clazzMethod = class_getInstanceMethod(originalClass, @selector(class));
     const char *types = method_getTypeEncoding(clazzMethod);
     class_addMethod(kvoClass, @selector(class), (IMP)new_class, types);
    1. 利用runtime给对象动态增加关联属性保存外部传进来的回调block:
     objc_setAssociatedObject(self, kObjectPropertyKey ,block, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    1. 利用runtime替换中间类的setter方法,在新的方法中,调用block后再向父类发送原消息:
     // 利用函数指针强制转换
     void (*objc_msgSendSuperCasted)(void *, SEL, id) = (void *)objc_msgSendSuper;
     // 给父类 发送原消息
     objc_msgSendSuperCasted(&superclass, _cmd, newValue);
     // 调用block
     ZLObservingBlock block = objc_getAssociatedObject(self, kObjectPropertyKey);
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         block(self, nil, newValue);
     });

Block

Block 基本原理

int main() {
    void (^blk)(void) = ^{
        (printf("hello world!"));
    };
    blk();
    return 0;
}

如上,通过clang转换为cpp源码,截取关键部分:

// block 的真面目
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

// ..... 一大坨无关代码

// 通过这个结构体包装block,用于快速构造block
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

        (printf("hello world!"));
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main() {
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

比较核心的是以下内容:__block_impl结构体,__mian_block_impl_0结构体,__main_block_func_0函数,&_NSConcreteStackBlock__main_block_desc_0结构体。

  • __block_impl结构体:

    该结构体定义在源文件上方,其中isa指针指向当前block对象类对象,FuncPtr指向block保存的函数指针。

  • __main_block_desc_0结构体:

    • 用于保存__main_block_impl_0 结构体大小等信息。
  • __main_block_func_0静态函数:

    • 用于存储当前block的代码块。
  • __main_block_impl_0结构体:

    该结构体包装了__block_impl结构体,同时包含__main_block_desc_0结构体。对外提供一个构造函数,构造函数需要传递函数指针(__main_block_func_0静态函数)、__main_block_desc_0实例。

由此我们可知,上述一个简单的block定义及调用过程被转换为了:

  1. 定义block变量相当于调用__main_block_impl_0构造函数,通过函数指针传递代码块进__main_block_impl_0实例。

  2. 构造函数内部,将外部传递进来的__main_block_func_0函数指针,设置内部实际的block变量(__block_impl类型的结构体)的函数指针。

  3. 调用block时,取出__mian_block_impl_0类型结构体中的__block_impl类型的结构体的函数指针(__main_block_func_0)并调用。

至此,一个简单的block原理描述完毕。

__block 捕获原理

block内部可以直接使用外部变量,但是在不加__block修饰符的情况下,是无法修改的。比如下面这段代码:

int main() {
    int a = 1;
    void (^blk)(void) = ^{
        printf("%d", a);
    };
    blk();
    return 0;
}

经过cpp重写后:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

        printf("%d", a);
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main() {
    int a = 1;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

经过上一节的讨论,我们知道代码块是由__main_block_impl_0构造函数传通过函数指针传递进来的。在这节的例子中,可以发现构造函数中多了一个int _a参数。同时__main_block_impl_0结构体也多了一个int a属性用于保存block内部使用的变量a

由于构造函数的参数为整形,在c++中,函数的形参为值拷贝,也就是说__main_block_impl_0结构体中的属性a,是外部a变量的拷贝。在代码块内部(也就是__main_block_func_0 函数)我们通过__cself指针拿到a变量。故我们在代码块中是无法修改a变量的,同时如果外部a变量被修改了,那么block内部也是无法得知的。

如果想要在内部修改a变量,可以通过__block关键字:

int main() {
    __block int number = 1;
    void (^blk)(void) = ^{
        number = 3;
        printf("%d", number);
    };
    blk();
    return 0;
}
struct __Block_byref_number_0 {
  void *__isa;
__Block_byref_number_0 *__forwarding;
 int __flags;
 int __size;
 int number;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_number_0 *number; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_number_0 *_number, int flags=0) : number(_number->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_number_0 *number = __cself->number; // bound by ref

        (number->__forwarding->number) = 3;
        printf("%d", (number->__forwarding->number));
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->number, (void*)src->number, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->number, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
int main() {
    __attribute__((__blocks__(byref))) __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 1};
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_number_0 *)&number, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

可以看到整形number变量变成了__Block_byref_number_0结构体实例,在__main_block_impl_0构造函数中,将该实例的指针传递进来。__Block_byref_number_0结构体中,通过__forwarding指向自己,整形number存储具体的值。

上节提到,构造函数中的参数是值拷贝,故此处代码块内部拿到的指针拷贝一样可以操作外部的__Block_byref_number_0结构体中的值,通过这种方式实现了block内部修改外部值。

block 引起的循环引用原理

RunLoop

reference:深入理解Runloop

Runloop 基本原理

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

iOS中,Runloop与线程是一一对应的关系,这种关系存在一张全局字典里:

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
 
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

苹果是不允许直接创建Runloop的,只能通过:

CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

这两个函数来获得当前或者某个线程的Runloop。根据上面的源码可以得出结论:

Runloop创建是发生在第一次获取之前,若从未获取,则永远不会创建。 Runloop销毁是在线程结束前,除主线程外,所有Runloop只能在当前线程中获取,主线程的Runloop可以在任意线程中获取。

Models in Runloop

runloop

一个RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

  1. Source:

    • Souce是事件发生的地方。
    • Souce分为Source0Source1Source0只包含一个函数指针做为回调,且不能主动唤醒Runloop,使用时需要先手动将其标为待处理,然后手动唤醒RunloopSource1也包含函数指针,同时还包含一个mach_por。它可以主动唤醒Runloop,用于内核与其他线程发送消息。
  2. Timer

    • Timer是基于时间的触发器,它和 NSTimertoll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
  3. Observer

    • Obserer是观察者,用于观察Runloop各种状态。它也包含函数指针,用于状态改变时回调通知外部。
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
        kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
        kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
        kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
        kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
        kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
    };
    

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

CFRunLoopMode CFRunLoop 的结构大致如下:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
 
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

这里有个概念叫 “CommonModes”:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultModeUITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。

有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去

Runloop 处理事件的逻辑

Runloop逻辑

/// 用DefaultMode启动
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
 
/// 用指定的Mode启动,允许设置RunLoop超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
 
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    
    /// 首先根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
    /// 如果mode里没有source/timer/observer, 直接返回。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;
    
    /// 1. 通知 Observers: RunLoop 即将进入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
    /// 内部函数,进入loop
    __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
        
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
 
            /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
            /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
            /// 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
            /// 4. RunLoop 触发 Source0 (非port) 回调。
            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
            /// 执行被加入的block
            __CFRunLoopDoBlocks(runloop, currentMode);
 
            /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
            if (__Source0DidDispatchPortLastTime) {
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }
            
            /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
            if (!sourceHandledThisLoop) {
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }
            
            /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
            /// • 一个基于 port 的Source 的事件。
            /// • 一个 Timer 到时间了
            /// • RunLoop 自身的超时时间到了
            /// • 被其他什么调用者手动唤醒
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }
 
            /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
            
            /// 收到消息,处理消息。
            handle_msg:
 
            /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
            if (msg_is_timer) {
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
 
            /// 9.2 如果有dispatch到main_queue的block,执行block。
            else if (msg_is_dispatch) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
 
            /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
            else {
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            
            /// 执行加入到Loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);
            
 
            if (sourceHandledThisLoop && stopAfterHandle) {
                /// 进入loop时参数说处理完事件就返回。
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
                /// 超出传入参数标记的超时时间了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
                /// 被外部调用者强制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                /// source/timer/observer一个都没有了
                retVal = kCFRunLoopRunFinished;
            }
            
            /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
        } while (retVal == 0);
    }
    
    /// 10. 通知 Observers: RunLoop 即将退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

Runloop 常见用法

Autorelease Pool

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool

事件响应

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和Exit(即将退出Loop) 事件,回调去执行一个很长的函数: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

定时器

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个TimerTimer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。

PerformSelecter

当调用 NSObjectperformSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

GCD

GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

AFN 保活

AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

AsyncDisplayKit

AsyncDisplayKitFacebook推出的用于保持界面流畅性的框架,其原理大致如下:

UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。

  1. 排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。
  2. 绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。
  3. UI对象操作通常包括 UIView/CALayer UI 对象的创建、设置属性和销毁。

其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。

为此,ASDK 创建了一个名为ASDisplayNode的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 framebackgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过Node来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的UIView/CALayer去。

ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的RunLoop中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

ARC

reference:深入理解Objective C的ARC机制Objective-C 引用计数原理黑幕背后的Autorelease

ARC 基本原理

使用ARC,开发者不再需要手动的retain/release/autorelease. 编译器会自动插入对应的代码,再结合Objective-Cruntime,实现自动引用计数。

如:

NSObject * obj;
{
    obj = [[NSObject alloc] init]; //引用计数为1
}
NSLog(@"%@",obj);
NSObject * obj;
{
    obj = [[NSObject alloc] init]; //引用计数为1
    [obj relrease]
}
NSLog(@"%@",obj);

其中,retain,release的方法实现原理:

- (id)retain {
    return ((id)self)->rootRetain();
}
inline id objc_object::rootRetain()
{
    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}

#if SUPPORT_MSB_TAGGED_POINTERS
#   define TAG_MASK (1ULL<<63)
#else
#   define TAG_MASK 1

inline bool 
objc_object::isTaggedPointer() 
{
#if SUPPORT_TAGGED_POINTERS
    return ((uintptr_t)this & TAG_MASK);
#else
    return false;
#endif
}

retain方法的实现可知,有些对象如果支持使用 TaggedPointer,苹果会直接将其指针值作为引用计数返回;如果当前设备是 64 位环境并且使用 Objective-C 2.0,那么“一些”对象会使用其 isa 指针的一部分空间来存储它的引用计数;否则 Runtime会使用一张散列表来管理引用计数。

id objc_object::sidetable_retain()
{
    //获取table
    SideTable& table = SideTables()[this];
    //加锁
    table.lock();
    //获取引用计数
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
         //增加引用计数
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    //解锁
    table.unlock();
    return (id)this;
}

retain类似,release方法也是如此的逻辑,只是增加了若引用计数为0,则销毁对象:

SideTable& table = SideTables()[this];
bool do_dealloc = false;
table.lock();
//找到对应地址的
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) { //找不到的话,执行dellloc
    do_dealloc = true;
    table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {//引用计数小于阈值,dealloc
    do_dealloc = true;
    it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
//引用计数减去1
    it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc  &&  performDealloc) {
    //执行dealloc
    ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;

再介绍下 SideTable 这个类,它用于管理引用计数表和 weak 表,并使用 spinlock_lock 自旋锁来防止操作表结构时可能的竞态条件。它用一个 64*128 大小的 uint8_t 静态数组作为 buffer 来保存所有的 SideTable 实例。并提供三个公有属性。

spinlock_t slock;//保证原子操作的自选锁
RefcountMap refcnts;//保存引用计数的散列表
weak_table_t weak_table;//保存 weak 引用的全局散列表

weak 表的作用是在对象执行 dealloc 的时候将所有指向该对象的 weak 指针的值设为 nil,避免悬空指针。这是 weak 表的结构:

struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};

苹果使用一个全局的 weak 表来保存所有的 weak 引用。并将对象作为键,weak_entry_t 作为值。weak_entry_t 中保存了所有指向该对象的 weak 指针。

autoreleasepool 原理

Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池PushPop

Autorelease对象的各种方法都是对类AutoreleasePoolPage的封装,其中AutoreleasePoolPage类结构如下:

Autorelease结构

  • AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)
  • AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
  • AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址
  • 上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置
  • 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入

所以,若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下图:

图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。

所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置

每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPageadd进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子:

objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:

  • 根据传入的哨兵对象地址找到哨兵对象所处的page
  • 在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置
  • 从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page

Tableview 优化

引用自保持界面流畅的技巧YYAsyncLayer源码分析

预排版

当获取到 API JSON 数据后,把每条 Cell 需要的数据都在后台线程计算并封装为一个布局对象 CellLayoutCellLayout 包含所有文本的 CoreText 排版结果、Cell 内部每个控件的高度、Cell 的整体高度。每个 CellLayout 的内存占用并不多,所以当生成后,可以全部缓存到内存,以供稍后使用。这样,TableView 在请求各个高度函数时,不会消耗任何多余计算量;当把 CellLayout 设置到 Cell 内部时,Cell 内部也不用再计算布局了。

避免离屏渲染

异步绘制

图像的绘制通常是指用那些以 CG 开头的方法把图像绘制到画布中,然后从画布创建图片并显示这样一个过程。这个最常见的地方就是 [UIView drawRect:] 里面了。由于CoreGraphic方法通常都是线程安全的,所以图像的绘制可以很容易的放到后台线程进行。一个简单异步绘制的过程大致如下(实际情况会比这个复杂得多,但原理基本一致):

- (void)display {
    dispatch_async(backgroundQueue, ^{
        CGContextRef ctx = CGBitmapContextCreate(...);
        // draw in context...
        CGImageRef img = CGBitmapContextCreateImage(ctx);
        CFRelease(ctx);
        dispatch_async(mainQueue, ^{
            layer.contents = img;
        });
    });
}

十一. 开源库

SDWebImg

引用自sdwebimg源码解析

简介

基本原理

YYKit

ASDK

runloop章节有所简介。

asdk 基本原理

asdk设计 ASDK 认为,阻塞主线程的任务,主要分为上面这三大类。文本和布局的计算、渲染、解码、绘制都可以通过各种方式异步执行,但UIKit Core Animation 相关操作必需在主线程进行。ASDK 的目标,就是尽量把这些任务从主线程挪走,而挪不走的,就尽量优化性能。

ASDK 为此创建了 ASDisplayNode 类,包装了常见的视图属性(比如 frame/bounds/alpha/transform/backgroundColor/superNode/subNodes 等),然后它用 UIView->CALayer 相同的方式,实现了 ASNode->UIView 这样一个关系: Asdk node

当不需要响应触摸事件时,ASDisplayNode 可以被设置为 layer backed,即 ASDisplayNode 充当了原来UIView的功能,节省了更多资源: 不需要事件触摸的情况下

UIViewCALayer 不同,ASDisplayNode 是线程安全的,它可以在后台线程创建和修改。Node 刚创建时,并不会在内部新建 UIViewCALayer,直到第一次在主线程访问 view layer 属性时,它才会在内部生成对应的对象。当它的属性(比如frame/transform)改变后,它并不会立刻同步到其持有的 viewlayer 去,而是把被改变的属性保存到内部的一个中间变量,稍后在需要时,再通过某个机制一次性设置到内部的 viewlayer

通过模拟和封装 UIView/CALayer,开发者可以把代码中的UIView替换为 ASNode,很大的降低了开发和学习成本,同时能获得 ASDK 底层大量的性能优化。为了方便使用, ASDK 把大量常用控件都封装成了 ASNode 的子类,比如 Button、Control、Cell、Image、ImageView、Text、TableView、CollectionView 等。利用这些控件,开发者可以尽量避免直接使用 UIKit 相关控件,以获得更完整的性能提升。

asdk 中 runloop的应用

iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)。iOS 图形服务接收到 VSync 信号后,会通过 IPC 通知到 App 内。AppRunloop 在启动后会注册对应的CFRunLoopSource通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个App的动画与显示。

Core AnimationRunLoop 中注册了一个 Observer,监听了 BeforeWaitingExit 事件。这个 Observer 的优先级是 2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置UIView frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完整)。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。

ASDK 在此处模拟了 Core Animation 的这个机制:所有针对 ASNode 的修改和提交,总有些任务是必需放入主线程执行的。当出现这种任务时,ASNode 会把任务用 ASAsyncTransaction(Group) 封装并提交到一个全局的容器去。ASDK 也在 RunLoop 中注册了一个 Observer,监视的事件和 CA 一样,但优先级比CA要低。当 RunLoop 进入休眠前、CA 处理完事件后,ASDK 就会执行该 loop 内提交的所有任务。具体代码见这个文件:ASAsyncTransactionGroup

通过这种机制,ASDK 可以在合适的机会把异步、并发的操作同步到主线程去,并且能获得不错的性能。

十二. Weex

本节不讲解如何使用weex,只关注weex iOS部分原理。如有兴趣,可自行去官网学习基本知识

Weex 基本结构

Weex目前最新版本支持RaxVueJS,其中Rax即为React语法,目的使前端开发者更易上手。

Weex SDK结构主要有三部分组成:

weex sdk

  1. 顶层framework

    • 此层的作用是编译目标文件至js,比如如果使用的.vue文件,就将此文件通过framework编译成对应的jsBundle。此层其实是weex集成了vuejs或者react
  2. 中间层js runtime

    • 此层的作用是承接上层的jsbundle文件,在iOS上,weex sdk通过使用系统JSCore来跑jsbundle文件。通过理解js文件,开始执行各种指令。比如在vuejs中的creatElement指令,会被转换为creatNative指令发送给下一层。通过这种方式,隔离了前端开发者与客端户开发工作,使得前端开发者也可以通过常用的js语法生成native组件,调用原生的各种方法。
  3. 底层Render Engine

    • 此层承接weex runtime传来的各种指令,开始执行weex sdk中"预置"的native代码。

Weex JS Framework 详解

Weex JS Framework主要分为以下几个功能:

  1. 适配前端层
  2. 构建渲染指令树
  3. JS-Native 通信
  4. JS Service
  5. 准备环境接口

适配前端层

前端框架在 Weex 和浏览器中的执行过程不一样,这个应该不难理解。如何让一个前端框架运行在 Weex 平台上,是 JS Framework 的一个关键功能。

Vue.js 为例,在浏览器上运行一个页面大概分这么几个步骤:首先要准备好页面容器,可以是浏览器或者是WebView,容器里提供了标准的 Web API。然后给页面容器传入一个地址,通过这个地址最终获取到一个 HTML 文件,然后解析这个 HTML 文件,加载并执行其中的脚本。想要正确的渲染,应该首先加载执行 Vue.js 框架的代码,向浏览器环境中添加 Vue 这个变量,然后创建好挂载点的DOM元素,最后执行页面代码,从入口组件开始,层层渲染好再挂载到配置的挂载点上去。

Weex 里的执行过程也比较类似,不过 Weex 页面对应的是一个 js 文件,不是 HTML 文件,而且不需要自行引入 Vue.js 框架的代码,也不需要设置挂载点。过程大概是这样的:首先初始化好 Weex 容器,这个过程中会初始化 JS FrameworkVue.js 的代码也包含在了其中。然后给 Weex 容器传入页面地址,通过这个地址最终获取到一个 js 文件,客户端会调用 createInstance 来创建页面。

构建渲染指令树

不同的前端框架里 Virtual DOM 的结构、patch 的方式都是不同的,这也反应了它们开发理念和优化策略的不同,但是最终,在浏览器上它们都使用一致的 DOM APIVirtual DOM 转换成真实的 HTMLElement。在 Weex 里的逻辑也是类似的,只是在最后一步生成真实元素的过程中,不使用原生 DOM API,而是使用 JS Framework 里定义的一套 Weex DOM API 将操作转化成渲染指令发给客户端。 渲染指令树

JS Framework 提供的 Weex DOM API 和浏览器提供的 DOM API 功能基本一致,在 VueRax 内部对这些接口都做了适配,针对Weex和浏览器平台调用不同的接口就可以实现跨平台渲染。

JS-Native 通信原理

通信 首先,页面的 js 代码是运行在 js 线程上的,然而原生组件的绘制、事件的捕获都发生在 UI 线程。在这两个线程之间的通信用的是 callNativecallJS 这两个底层接口(现在已经扩展到了很多个),它们默认都是异步的,在 JS Framework 和原生渲染器内部都基于这两个方法做了各种封装。

callNative 是由客户端向 JS 执行环境中注入的接口,提供给 JS Framework 调用,界面的节点(上文提到的渲染指令树)、模块调用的方法和参数都是通过这个接口发送给客户端的。为了减少调用接口时的开销,其实现在已经开了更多更直接的通信接口,其中有些接口还支持同步调用(支持返回值),它们在原理上都和 callNative 是一样的。

callJS 是由 JS Framework 实现的,并且也注入到了执行环境中,提供给客户端调用。事件的派发、模块的回调函数都是通过这个接口通知到 JS Framework,然后再将其传递给上层前端框架。

JS Service

Weex 是一个多页面的框架,每个页面的 js bundle 都在一个独立的环境里运行,不同的 Weex 页面对应到浏览器上就相当于不同的“标签页”,普通的 js 库没办法实现在多个页面之间实现状态共享,也很难实现跨页通信。

JS Framework 中实现了 JS Service 的功能,主要就是用来解决跨页面复用和状态共享的问题的,例如 BroadcastChannel 就是基于 JS Service 实现的,它可以在多个 Weex 页面之间通信。

Weex 渲染过程及事件响应

Weex 的页面渲染

执行过程

先描述渲染前的流程:Weex会先拉取一个JSBundle,这个JSBundle就是之前提到的VueJS编译后的js打包文件。再收到JSBunlde后,Weex SDK会通过native代码创建intance,之后再将一些配置及环境变量、JSBundle一起传给JS Framework,其目的是通过JS Framwork生成之前提到过的渲染指令树

在此之后就可以开始正式的渲染页面流程:

页面渲染

Weex 的事件响应

事件响应

如上图所示,如果在 Vue.js 里某个标签上绑定了事件,会在内部执行 addEventListener 给节点绑定事件,这个接口在 Weex 平台下调用的是 JS Framework 提供的 addEvent 方法向元素上添加事件,传递了事件类型和处理函数。JS Framework 不会立即向客户端发送添加事件的指令,而是把事件类型和处理函数记录下来,节点构建好以后再一起发给客户端,发送的节点中只包含了事件类型,不含事件处理函数。客户端在渲染节点时,如果发现节点上包含事件,就监听原生 UI 上的指定事件。

当原生 UI 监听到用户触发的事件以后,会派发 fireEvent 命令把节点的 ref、事件类型以及事件对象发给 JS FrameworkJS Framework 根据 ref 和事件类型找到相应的事件处理函数,并且以事件对象 event 为参数执行事件处理函数。目前 Weex 里的事件模型相对比较简单,并不区分捕获阶段和冒泡阶段,而是只派发给触发了事件的节点,并不向上冒泡,类似 DOM 模型里 level 0 级别的事件。

Weex 中的 iOS: JavaScriptCore

此节简单介绍下Weex中的JavaScriptCore

OC、JS互相调用

 var addElement = function(){
    return 'addElement';
 }

比如上述js文件

//1.加载上面js文件到JSContext
JSContext *context = [[JSContext alloc]init];
[context evaluateScript:jsFilePath];

// 2.调用addElement方法
JSValue *jsFunction = context[@"addElement"];
JSValue *addElement = [jsFunction callWithArguments:nil];

js文件加载进objcJSContext后,就可以调用js文件里定义的方法了,这个就是objc调用js的原理。

//注册native方法给js调用
- (void)regiestNativeFunction {
    //注册一个callCreateBody方法给js调用
    self.jsContext[@"callCreateBody"] = ^(NSString *msg){
        NSLog(@"js:msg:%@",msg);
    };
    //注册一个callAddEvent方法给js调用
    self.jsContext[@"callAddEvent"] = ^(NSString *msg){
        NSLog(@"js:msg:%@",msg);
    };
     //注册一个callAddElement方法给js调用
   self.jsContext[@"callAddElement"] = ^(NSString *msg){
        NSLog(@"js:msg:%@",msg);
    };
    //使用js调用objc
    [self.jsContext evaluateScript:@"callAddEvent('hello,i am js side')"];
}

weex封装的全局方法

上面的代码native端注册了三个方法给js端调用,weex基于这种能力封装了一系列的方法,下面列出的就是weex提供的操作UI的一些的全局方法。

global.callCreateBody,
global.callAddElement,
global.callRemoveElement,
global.callMoveElement,
global.callUpdateAttrs,
global.callUpdateStyle,
global.callAddEvent,
global.callRemoveEvent,
global.callNative

通信数据格式

weex有了JS-Native相互通信的能力后,再按照一定格式发送数据给native端就可以渲染UI了。这个格式要与native端协商好,目前weex接受的格式类似下面的json格式
{ 
	ref: "", //js端随机生成的数字id
	type: "", //native端支持的component类型
	          //比如text div list等
	attr: {}, //component支持的属性
	          //比如text的value
	style: {} //元素的样式,native端基于Facebook的yoga     
	          //框架的前身cssLayout来生成native UI

}

About

常见iOS面试中考察的知识点整理

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published