移动开发技术

Jason's Blog

在现有iOS项目中集成Flutter方案

在现有项目中继承Flutter,可以用module的模式,官方已经支持。详细见文章Add Flutter to existing apps

官方文章中的集成有一个问题,native工程和flutter工程耦合性很强,导致如果一个项目组负责原生开发和flutter开发是不同人的话,原生开发必须安装flutter环境,才能跑起来原生工程。所以更好的办法是将flutter工程生成的framework集成进native工程里,这样native工程可以脱离flutter环境执行。有赞用了这样的方法,详见文章有赞 Flutter 混编方案

看了有赞的文章,知道了这样的事情,需要将App.framework和Flutter.framework这两个文件集成到native工程里,就可以跑起来了,并且Flutter.framework有分不同的版本,jit和aot,所以要根据需要集成不同的framework。但是这篇文章还是没有将清楚,从哪里获取这两个framework,通过实践提取验证了下,所以有了这篇文章。

有赞的文章还给了一个非常重要的灵感,framework可以放入pod私有库里进行管理,这样方便切换不同的framework库。于是我们的项目有了这样的架构。

Natvie分别依赖两个私有库,FlutterFrameworkDebug和FlutterFrameworkRelease,两个私有库分别包含debug和release版本的App.framework和Flutter.framework。

下边说一下这些Framework从哪里取。

  • Flutter.framework

(1)Debug版本

[Flutter工程Path]/.ios/Flutter/engine/Flutter.framework

(2)Release版本

[Flutter安装版本Path]/bin/cache/artifacts/engine/ios-release/Flutter.framework

  • App.framework

(1)Debug版本

[Flutter工程Path]/.ios/Flutter/App.framework

(2)Release版本(先运行flutter build ios)

[Flutter工程Path]/build/ios/iphoneos/Runner.app/Frameworks/App.framework

取到相应的framework后,就可以部署好FlutterFrameworkDebug和FlutterFrameworkRelease私有库。搞好之后在native工程里修改Podfile

1
2
#pod 'FlutterFrameworkDebug',  '0.1.1'    #在模拟器上和真机上都能运行
pod 'FlutterFrameworkRelease',  '0.1.1'   #只能在真机上运行

切换一下重新pod install就可以了。

__bridge __bridge retained __bridge transfer的区别

iOS开发中,经常会接触到两种对象,Objective-C对象和Core Foundation对象,他们之间有所不同,可以互相转换。最大的不同之处在于,在ARC模式下,前者不用开发者手动管理内存,后者需要开发者手动管理内存,即调用CFRelease方法释放对象,否则会造成内存泄漏。转换的话主要会用到以下3个方法:

  • __bridge,
  • __bridge_retained
  • __bridge_transfer

__bridge可以用于OC对象和CF对象互转,例如

1
2
3
NSObject *obj = [[NSObject alloc] init];    //retain count 1
CFTypeRef cfObj1 = (__bridge CFTypeRef)obj; //retain count 1
NSObject *obj1 = (__bridge id)cfObj1;       //retain count 2

在这种转换方式下,如果是OC对象转换成CF对象,引用计数不变。如果是CF对象转换成OC对象,因为OC对象的默认修饰符是__strong,引用计数会+1,即以下两种写法是一样的。

1
2
NSObject *obj1 = (__bridge id)cfObj1;
NSObject __strong *obj1 = (__bridge id)cfObj1;

__bridge_retained用于OC对象转换为CF对象,例如

1
2
3
4
5
6
NSObject *obj = [[NSObject alloc] init];                //retain count 1
CFTypeRef cfObj1 = (__bridge_retained CFTypeRef)obj;    //retain count 2

//等价写法
NSObject *obj = [[NSObject alloc] init];                //retain count 1
CFTypeRef cfObj1 = (CFTypeRef)CFBridgingRetain(obj);    //retain count 2

这种情况下,obj的引用计数会+1,obj的释放不会影响到cfObj1的使用

__bridge_transfer用于CF对象转换为OC对象,例如

1
2
3
4
5
6
7
8
NSObject *obj = [[NSObject alloc] init];                //retain count 1
CFTypeRef cfObj1 = (__bridge_retained CFTypeRef)obj;    //retain count 2
NSObject *obj1 = (__bridge_transfer id)cfObj1;          //retain count 2

//等价写法
NSObject *obj = [[NSObject alloc] init];                  //retain count 1
CFTypeRef cfObj1 = (__bridge_retained CFTypeRef)obj;      //retain count 2
NSObject *obj1 = (NSObject *)CFBridgingRelease(cfObj1);   //retain count 2

最后来做个练习,看下以下代码输出什么

1
2
3
4
5
6
7
8
9
10
11
12
13
NSObject *obj = [[NSObject alloc] init];                //retain count 1
CFTypeRef cfObj = (__bridge_retained CFTypeRef)obj;     //retain count 2
CFTypeRef cfObj1 = (__bridge CFTypeRef)obj;             //retain count 2
CFTypeRef cfObj2 = (__bridge_retained CFTypeRef)obj;    //retain count 3

NSObject *obj1 = (__bridge_transfer id)cfObj1;          //retain count 3
NSObject *obj2 = (__bridge id)cfObj2;                   //retain count 4

NSLog(@"obj retainCount %ld", (long)CFGetRetainCount(cfObj));

NSString *str = [NSString stringWithFormat:@"testStr"];
CFStringRef cfStr = (__bridge CFStringRef)str;
NSLog(@"str retainCount %ld", (long)CFGetRetainCount(cfStr));   //retain count 9223372036854775807 = 0x7FFFFFFFFFFFFFFF

答案是

1
2
obj retainCount 4
str retainCount 9223372036854775807

str特殊一点,会被当做是字符串常量,retainCount是一个最大值,保证不会被系统回收。

iOS 中的 NSProxy

在日常开发中,NSObject 经常会被使用到。但是 NSProxy 却很少用。这个类顾名思义,是用来做代理的,任何消息都可以对它发送,在它内部,再指向具体的实现。

如上图所示,调用者可以给 NSProxy 发送消息,而不用关心内部实现,NSProxy 则可以根据具体的 SEL 去调用真正的实现着 ClassA 或者 ClassB。需要说明的是,NSProxy 不能直接使用,需要自己写一个类继承它。

接下来我们来看实例代码。

先定义一个类 MyProxy 继承自 NSProxy:

1
2
3
4
5
6
//MyProxy.h
#import <Foundation/Foundation.h> 

@interface MyProxy : NSProxy
-(instancetype)init;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//MyProxy.m
#import "MyProxy.h"
#import <objc/runtime.h>
#import "Real.h"

@interface MyProxy()

@property (nonatomic, strong) Real *real;

@end
@implementation MyProxy

-(instancetype)init{
    self.real = [[Real alloc] init];
    return self;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    SEL sel = anInvocation.selector;
    if([self.real respondsToSelector:sel]){
        [anInvocation invokeWithTarget:self.real];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    if([self.real respondsToSelector:sel]){
        return [self.real methodSignatureForSelector:sel];
    }
    return NULL;
}

@end

MyProxy 需要实现两个方法 forwardInvocation: 和 methodSignatureForSelector:

在 forwardInvocation: 方法中,将真实的实现转发给了 Real 的实例,由其去实现。

1
2
3
4
5
6
//Real.h
#import <Foundation/Foundation.h>

@interface Real : NSObject
-(void)hello;
@end
1
2
3
4
5
6
7
8
9
10
//Real.m
#import "Real.h"

@implementation Real

-(void)hello{
    NSLog(@"hello11");
}

@end

下边是调用方代码:

1
2
MyProxy *proxy = [[MyProxy alloc] init];
[proxy performSelector:@selector(hello)];

运行后,在控制台输出 hello11,表明 Real 实例的 hello 方法得到执行。

NSProxy 可以用来实现双继承,更多好玩的玩法可以去探索。

通过源码理解Autorelease Pool原理

1. Autorelease Pool 是什么

iOS 的内存管理使用引用计数机制。当对象被初始化或者被强引用赋值时,对象的引用计数 +1,当对象离开所在函数作用域或者被设置为 nil 后,引用计数 -1。当对象的引用计数为 0 时,操作系统会释放掉对象所占用的内存。

我们先来看一下这段代码:

1
2
3
4
-(NSObject *)getObj{
    NSObject *obj = [NSObject alloc] init];
    return obj;
}

在 getObj 执行完后,obj 的作用域已经结束,obj 的引用计数为 0,应该马上被系统回收。那么问题就出现了,obj 是作为函数的返回给调用者的,被回收后调用者拿到的对象就是nil了,明显不符合调用者的预期。这时候 Autorelease Pool 就派上用场了,当 getObj 函数结束时,obj 并没有进行引用计数 -1 操作,而是将 obj 放入了 Autorelease Pool。Autorelease Pool 是一个可以存放多个对象指针的对象池,当 Autorelease Pool 被销毁时,会对所有 Autorelease Pool 中的对象执行引用计数 -1 操作,这时候才会回收 obj。相当于放入 Autorelease Pool 的对象被延迟释放了。这样的机制能够保证调用者能够正常拿取到 obj 的引用。

那么 Autorelease Pool 是什么时候被创建和销毁的呢?对于 ARC 来讲,大多数情况下,是不需要开发人员自己创建和销毁 Autorelease Pool 的(后面再讲少数情况)。Autorelease Pool 是在 Runloop 的一次循环中,被创建和释放的,是系统自己做的,开发人员不能控制创建和释放的时机,所以开发人员也不能知道 Autorelease Pool 里的对象什么时候被释放的。下边是网上看到的一个图,说明了 Autorelease Pool 创建和释放的时机。

2. AutoRelease Pool如何使用

在 ARC 情况下,AutoRelease Pool 的使用非常简单,以 iOS 工程里的 main.m 代码为例:

1
2
3
4
5
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

UIApplicationMain 的调用被 @autoreleasepool{} 整个包裹起来,表示 UIApplicationMain 函数执行之前,创建了一个 AutoRelease Pool,在函数返回之后,释放了之前创建的 AutoRelease Pool。在此期间,如果有对象要加入 AutoRelease Pool,就是加入的这个创建的 AutoRelease Pool。

上边提到,在大多数情况下,开发人员不需要自己创建和销毁自动释放池,现在谈一下少数情况。开发人员需要自己使用 AutoRelease Pool 的情形,通常是如下情况:

1
2
3
4
5
for (int i = 0; i < 1000000; i++) {
    @autoreleasepool {
        NSString *str = [NSString stringWithFormat:@"hi + %d", i];
    }
}

如果不加上 @autoreleasepool{} 代码块,循环里的临时变量 str 会被加入到当前的 AutoRelease Pool,而这个 AutoRelease Pool 的释放时机,如上所说,是需要等到当前 Runloop 一个循环后才会释放,而这个时机我们并不能控制。这样,在 Runloop 一个循环结束前,就会出现很多临时变量 str 不用了,但是占用内存的情况。所以这里手动加上 @autoreleasepool{} 代码块,每次循环都创建一个新的 AutoRelease Pool, str 会被加入到这个新的 AutoRelease Pool,在每次 for 循环结束时,AutoRelease Pool 被释放,从而 str 也被及时释放,内存能够得到及时的清理。

3. Autorelease Pool的实现原理

我们从系统使用 @autoreleasepool{} 的代码入手,将 main.m 代码编译成 main.cpp 代码进行进一步分析,在 main.m 文件目录执行下面的编译命令:

1
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m

执行完后会生成文件 main.cpp,在文件最后会看到如下代码:

1
2
3
4
5
int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
    }
}

可以看到,UIApplicationMain 执行前,增加了一行代码 AtAutoreleasePool autoreleasepool,这里声明了一个类型为 AtAutoreleasePool 的对象。在文件里搜索 AtAutoreleasePool,发现如下代码:

1
2
3
4
5
struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

__AtAutoreleasePool 是一个结构体,在构造函数和析构函数里,分别调用了 objc_autoreleasePoolPush() 和 objc_autoreleasePoolPop(atautoreleasepoolobj) 方法。也就是说,在 UIApplicationMain 执行前,首先先执行了 objc_autoreleasePoolPush 方法,然后执行了 objc_autoreleasePoolPop 方法,objc_autoreleasePoolPush 是在创建 Autorelease Pool,objc_autoreleasePoolPop 是在销毁 Autorelease Pool。接下来我们通过源码分析创建和销毁 Autorelease Pool 都做了什么。

这两个方法的代码在NSObject.mm里,代码是开源的,可以到 https://opensource.apple.com/release/macos-10141.html 下载,笔者查看的是最新的 objc4-750.1 版本。所有的历史版本可以在这里浏览 https://opensource.apple.com/source/objc4/

3.1 创建Autorelease Pool

首先看 objc_autoreleasePoolPush 的实现:

1
2
3
4
5
void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

objc_autoreleasePoolPush 的实现很简单,直接调用了AutoreleasePoolPage::push() 。先来看下 AutoreleasePoolPage 是什么:

1
2
3
4
5
6
7
8
9
10
class AutoreleasePoolPage
{
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
}

省去其他的宏定义、常量定义和方法,AutoreleasePoolPage 有如上属性,parent 和 child 同样指向AutoreleasePoolPage, 很容易猜测 AutoreleasePoolPage 是双向链表中的一个节点,后续的代码会印证这个猜测。next 是一个指针,是一个比较重要的属性,先留意一下,后边会讲。其余的属性对理解 Autorelease Pool 原理不是特别重要,暂时先都忽略。

AutoreleasePoolPage 对象分配内存方法如下:

1
2
3
static void * operator new(size_t size) {
    return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
}

SIZE 被定义为 PAGE_MAX_SIZE,PAGE_MAX_SIZE 是虚拟内存一页的大小,网上查资料说是0x1000字节。

1
2
3
4
5
6
    static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
        PAGE_MAX_SIZE;  // must be multiple of vm page size
#else
        PAGE_MAX_SIZE;  // size and alignment, power of 2
#endif

所以,一个 AutoreleasePoolPage 对象所占用的内存大小是 PAGE_MAX_SIZE。

看到这里我们已经清楚 AutoreleasePoolPage 的内部结构,用一张图来表示:

除了存储 AutoreleasePoolPage 的成员变量外,其余空间会用来存储加入到 Autorelease Pool 的对象指针。

继续看 AutoreleasePoolPage::push 方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
static inline void *push()
{
    id *dest;
    if (DebugPoolAllocation) {
        // Each autorelease pool starts on a new pool page.
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
    return dest;
}

会调用 autoreleaseFast 方法,方法的参数是 POOL_BOUNDARY ,关于 POOL_BOUNDARY 是什么,这个之后再说:

1
2
3
4
5
6
7
8
9
10
11
static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        return page->add(obj);
    } else if (page) {
        return autoreleaseFullPage(obj, page);
    } else {
        return autoreleaseNoPage(obj);
    }
}

首先拿到当前的 hotPage,hotPage 可以理解为正在使用的 AutoreleasePoolPage,也就是双向链表末端的 AutoreleasePoolPage。然后分为三种情况:

  1. 如果有 hotPage,并且 hotPage 没有满的时候,调用 page->add(obj)

  2. 如果有 hotPage,但是 hotPage 已经满的时候,调用 autoreleaseFullPage(obj, page)

  3. 如果没有 hotPage,调用 autoreleaseNoPage(obj)

以下对 3 种情况分别进行说明:

第 1 种情况,查看 AutoreleasePoolPage 的 add 方法:

1
2
3
4
5
6
7
8
9
id *add(id obj)
{
    assert(!full());
    unprotect();
    id *ret = next;  // faster than `return next-1` because of aliasing
    *next++ = obj;
    protect();
    return ret;
}

将 next 指针指向 obj, 然后next++,返回obj。所以,这里我们可以知道,AutoreleasePoolPage 的 next 指针是指向下一个空位置,当有对象要被加入到 AutoreleasePoolPage 的时候,会加入到这个位置。

第 2 种情况,查看 autoreleaseFullPage 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    // The hot page is full. 
    // Step to the next non-full page, adding a new page if necessary.
    // Then add the object to that page.
    assert(page == hotPage());
    assert(page->full()  ||  DebugPoolAllocation);

    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

新建一个 page,将新建的 page 设置为 hotPage,并且将 obj 加入到此 page 中,通过进一步查看 AutoreleasePoolPage 的构造函数会发现,新 page 的 parent 指针会设置成这个函数传入的老 page,新老 page 就形成了双向链表的结构。

第 3 种情况,查看 autoreleaseNoPage 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
id *autoreleaseNoPage(id obj)
{
    bool pushExtraBoundary = false;

    // Install the first page.
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    // Push a boundary on behalf of the previously-placeholder'd pool.
    if (pushExtraBoundary) {
        page->add(POOL_BOUNDARY);
    }

    // Push the requested object or pool.
    return page->add(obj);
}

新建一个 AutoreleasePoolPage ,然后再加入 obj ,创建 Autorelease Pool 的时候,obj 的值是 POOL_BOUNDARY。

我们用一张图来表示 Autorelease Pool 创建时候的情况:

在这里我们来说一下 POOL_BOUNDARY 是什么。我们可以发现其定义是为 nil

1
#define POOL_BOUNDARY nil

从字面意义上来讲,这是一个边界标记,当每次创建一个新的 Autorelease Pool 时,我们都会首先加入一个 POOL_BOUNDARY 标记在内存中,这样我们就知道了不同 Autorelease Pool 的分割位置在哪里。当我们需要最后创建的 Autorelease Pool 中的所有对象时,我们就只要释放这个 POOL_BOUNDARY 位置之后的对象。

3.2 将对象加入Autorelease Pool

创建 Autorelease Pool 的代码到此就基本看完了,我们马上再来看下将一个对象加入 Autorelease Pool 会干些什么。将对象加入 Autorelease Pool 会调用 NSObject 的 autorelease 方法,实现如下:

1
2
3
4
5
6
7
8
static inline id autorelease(id obj)
{
    assert(obj);
    assert(!obj->isTaggedPointer());
    id *dest __unused = autoreleaseFast(obj);
    assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
    return obj;
}

实际上是在调用 autoreleaseFast 方法。原来,创建一个 Autorelease Pool 和将一个 obj 加入 Autorelease Pool 其实代码流程是一样的,不同的是创建时候添加的是 POOL_BOUNDARY,添加时候添加的是 obj。

通过以上代码,我们知道往 Autorelease Pool 里添加多个对象后是什么情况了,用一张图来表示:

假设我们有 obj0 到 obj4 一共 5 个对象需要添加进 Autorelease Pool。第一个 AutorelasePoolPage 没有用满时,直接往里边加,满了之后,新建一个 AutorelasePoolPage,在往里边继续加。所以,obj0、obj1、obj2、obj3 被添加到了第 1 个 AutorelasePoolPage 中,obj4 被添加到了第 2 个 AutorelasePoolPage 中。真实情况下,AutorelasePoolPage 当然不只存储 4 个对象,这里只是方便举例说明。

如果在 Autorelase Pool 没有销毁的时候,再新建一个 Autorelase Pool,则往 AutorelasePoolPage 的 next 位置加入 POOL_BOUNDARY。如果又有对象要添加进新的 Autorelase Pool,则往 AutorelasePoolPage 继续添加 obj5 和 obj6,如下图:

可以看到,POOL_BOUNDARY 是边界对象,标识了多个 Autorelease Pool 的分割边界。

3.3 销毁Autorelease Pool

前边提到,销毁 Autorelease Pool 会调用 objc_autoreleasePoolPop 方法:

1
2
3
4
5
void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

直接查看 AutoreleasePoolPage::pop 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static inline void pop(void *token)
{
    AutoreleasePoolPage *page;
    id *stop;

    page = pageForPointer(token);
    stop = (id *)token;

    if (PrintPoolHiwat) printHiwat();

    page->releaseUntil(stop);

    // memory: delete empty children
    if (DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        // special case: delete everything for pop(top) 
        // when debugging missing autorelease pools
        page->kill();
        setHotPage(nil);
    }
    else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

回顾下之前的代码,token 为创建 Autorelease Pool 时返回的 POOL_BOUNDARY,这个会作为 pageForPointer 的输入参数。 pageForPointer 函数的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static AutoreleasePoolPage *pageForPointer(const void *p)
{
    return pageForPointer((uintptr_t)p);
}

static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
    AutoreleasePoolPage *result;
    uintptr_t offset = p % SIZE;

    assert(offset >= sizeof(AutoreleasePoolPage));

    result = (AutoreleasePoolPage *)(p - offset);
    result->fastcheck();

    return result;
}

通过 POOL_BOUNDARY 的内存地址和 AutoreleasePoolPage 的内存占用 SIZE,可以算出 POOL_BOUNDARY 相对于 AutoreleasePoolPage 起始地址的偏移量,从而计算出创建 Autorelease Pool 时候的那个 AutoreleasePoolPage 的内存起始地址。所以,pageForPointer 函数返回当前 Autorelease Pool 创建时候的 AutoreleasePoolPage。

接下来看 page->releaseUntil(stop) 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void releaseUntil(id *stop)
{
    // Not recursive: we don't want to blow out the stack 
    // if a thread accumulates a stupendous amount of garbage

    while (this->next != stop) {
        // Restart from hotPage() every time, in case -release 
        // autoreleased more objects
        AutoreleasePoolPage *page = hotPage();

        // fixme I think this `while` can be `if`, but I can't prove it
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

        if (obj != POOL_BOUNDARY) {
            objc_release(obj);
        }
    }

    setHotPage(this);
}

从当前的 hotPage 开始,依次对 AutoreleasePoolPage 里的对象执行 objc_release 操作,直到遇到 POOL_BOUNDARY 对象。这就是对当前 Autorelease Pool 里的所有对象进行释放操作。用一张图来表示这个过程会更加直观:

我们可以思考一下为什么要这么设计 Autorelease Pool。由于要加入 Autorelease Pool 的对象个数是不固定的,所以系统只能一次分配固定大小的内存,也就是一个 AutoreleasePoolPage的大小。当加满了之后,再在双向链表的最后加上一个 AutoreleasePoolPage。这里其实跟操作系统给应用程序分配内存空间是一样的,也是按页分配。而如何区分多个 Autorelease Pool,就是用了 POOL_BOUNDARY 来做边界标记。

4. 总结

到此位置,我们已经分析完了创建 Autorelease Pool,往 Autorelease Pool 里添加对象,释放 Autorelease Pool 的主要代码。其中还有一些分支代码和异常情况的处理被省略,感兴趣的同学可以自行查看其余源码。

最后我们总结一下 Autorelease Pool 的实现原理:

  1. Autorelease Pool 是由多个 AutoreleasePoolPage 对象以双向链表的方式组织起来的数据结构。

  2. 每个 AutoreleasePoolPage 只能存储有限个对象指针。当新的对象加入 Autorelease Pool 的时候,如果当前的 AutoreleasePoolPage 存储空间不够,会新初始化一个 AutoreleasePoolPage,加入到链表末端。

  3. Autorelease Pool 可以被嵌套创建。创建一个新的 Autorelease Pool 的时候,会在当前 AutoreleasePoolPage 中插入边界对象 POOL_BOUNDARY,以和上一个 Autorelease Pool 以区分。

  4. 当 Autorelease Pool 销毁的时候,对 AutoreleasePoolPage 里存储的所有对象依次从后往前调用 release,直到遇到对象 POOL_BOUNDARY,表明当前 Autorelease Pool 中的对象已经被全部释放。

5. 参考资料

https://juejin.im/post/5a66e28c6fb9a01cbf387da1

记一次数据库被攻击的经历

1. 问题出现

之前为了练手做了一个基于nodejs的后台系统。有一天突然发现http api访问没有数据了,赶快打开浏览器看了下报错信息,发现数据库的表找不到了,于是觉得问题可能有点大,马上登录服务器查看。

登录服务器后一看,发现数据库里的表被删掉了,新建了一个库叫PLEASE_READ_ME_XMG,里边有个表叫WARNING,WARNING表的结构如下图

猜测warning是警告信息,Bitcoin_Address是比特币钱包地址,Email是黑客联系邮箱。不过发现这个表里并没有数据,真不知道黑客是什么目的,还是只是恶作剧。上网搜索关键字“PLEASE_READ_ME_XMG”也印证了跟比特币勒索相关。

2. 查找问题

然后开始查找问题,先去mysql数据文件夹查看表文件WARNING.frmWARNING.ibd的生成时间,确定了攻击发生的时间点。

用命令查看linux机器登录记录

1
who /var/log/wtmp

发现在攻击发生的时间点,没有登录日志。那就怀疑用户是远程登录mysql直接操作数据库。于是登录数据库查看授权情况

1
show grants;

显示

1
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%'

%代表可以从任意机器登录。

3. 解决问题

3.1 修改mysql权限

首先把数据库授权改为localhost。

1
grant all privileges on *.* to 'root'@'localhost' with grant option

3.2 增加binlog

其次,由于mysql没有开启binlog,所以无法查看数据库的改动记录,也就无法看到删除数据库表的那条命令信息,经过这次教训,将binlog打开。

找到mysql配置文件my.cnf

1
2
[mysqld]
log-bin=/***/***/logs

在mysqld项下增加log-bin的配置,指定binlog的输出路径,注意这里要修改下logs文件夹的权限,保证可以正常写入。

然后重新启动mysql

1
systemctl restart mysqld

再尝试进行mysql update操作,通过命令查看binlog,发现日志正常写入。

1
mysqlbinlog -d xxx logs.000001

其中xxx是数据库名。

3.3 定时备份数据

除此之外,为了更加保险,写了备份脚本,通过mysqldump进行备份

1
2
3
4
#! /bin/sh

time=$(date "+%Y%m%d-%H%M")
mysqldump -uxxx -pxxx --databases dbname > /path/to/bak/$time.sql

将脚本保存,修改执行权限,加入crontab中定时每个小时执行备份

1
0 * * * * /path/to/back/bak_db.sh > /dev/null 2>&1 &

4. 总结

通过这次的攻击,数据全部丢失,虽然通过其他途径找回了信息,但是mysql数据本身并没有得到恢复。得到的教训如下:

  1. mysql操作权限要严格限制,通过白名单的方式放开,其他全部拒绝

  2. mysql要打开binlog功能,可以用来恢复数据和查看操作记录

  3. 数据要定时备份,以防丢失后能够恢复

iOS越狱以及cycript的安装

本篇文章向iOS逆向初学者介绍如何越狱手机,并且安装cycript工具来调试第三方App。

1. 准备

  • 1台iOS越狱手机

  • mac电脑

  • 苹果开发者账号

2. 手机如何越狱

根据当前网上查到的资料,iOS 12.1.4以下(包含)是可以越狱的,如果你的手机系统版本升级到了12.1.4之上,目前来讲没有办法,可以去淘宝买一部越狱手机。

笔者的手机系统版本是iOS 11.2.5,这个版本是可以非完美越狱的。非完美越狱的意思是,手机越狱成功后,如果重启,则会恢复到未越狱状态,需要重新越狱。不过这个是可以接受的,毕竟可以用了不是。以下以iOS 11.2.5为例,介绍越狱步骤。

2.1 越狱工具的选择

笔者越狱的时候尝试了两个工具Unc0ver和Electra,用了之后发现Electra好用一点,Unc0ver的问题是,越狱之后Cydia总是出现网络问题连接不上。用Electra越狱后Cydia可以使用,打开Cydia后会提示升级,正常升级后Cydia会变成Sileo,这个可以理解为新版的Cydia。

2.2 如何安装Electra

首先要在mac上安装Cydia Impactor。Cydia Impactor安装后,用数据线连接手机和电脑,将Electra.ipa拖入Impactor。安装的时候会要求输入账号名和密码。这里需要输入苹果开发者账号(每年699元那个),输入的密码并不是开发者账号的密码,而是临时生成的密码,需要到https://appleid.apple.com生成App专用密码。这样Electra就顺利安装到手机上了。

2.3 越狱

打开手机上的Electra App,点击Jailbroken按钮,注意底下会有个开关选项"Tweaks",这里需要把选项关掉,否则后边使用cycript会出错。顺利的话,手机会黑屏,然后重新启动桌面。再打开Electra App,刚才的Jailbroken按钮变成了Already Jailbroken,说明越狱已经成功了。手机桌面应该也出现了Cydia或Sileo图标。

3. 如何ssh到手机

有两种方式可以使mac ssh到手机上,一种是通过同一局域网WIFI,另一种是通过手机直接连接到mac。为了获取更快的连接速度和更稳定,笔者选择了后一种方式。

首先在mac上安装usbmuxd

1
brew install usbmuxd

usbmuxd自带了一个工具iproxy,这个工具的作用是,在mac和手机之间架上一个代理,mac ssh手机本来是走22端口,需要用这个工具将其他端口,比如5678,转发到22端口上。运行命令

1
iproxy 5678 22

运行后正常情况下会显示

1
waiting for connection

这时候,在mac上打开另一个终端窗口,输入命令

1
ssh -p 5678 root@127.0.0.1

输入默认的root密码alpine,就能成功ssh到手机上了。关于如何不每次都输入密码,这里可以自行搜索下其他相关资料,这里不再累述。

除了能够使用ssh,还可以使用scp向手机传文件,这里需要注意同样需要指定端口号,例如

1
scp -P 5678 filexxx root@127.0.0.1:/tmp

ssh到手机上之后,如果我们需要其他的软件,可以使用apt-get,例如安装vim

1
apt-get install vim

4. cycript的安装和使用

能够以root身份ssh到手机之后,就可以做很多事情了,比如查看当前手机正在运行的App的进程号,以及各个App的沙盒路径,以及随意浏览里边的文件,很多有趣的事情可以去探索和尝试。这篇文章主要介绍cycirpt工具的使用。

4.1 cycript介绍

cycript是一个脚本语言,通过编写脚本代码,可以在任意第三方App运行的时候,执行任何逻辑,通常情况下可以用来查看App的UI结构,某个页面的实现方式。其他的使用目的等待你去探索和发现。

4.2 安装和使用cycript

在网上查了些资料,说cycript可以直接安装在iOS手机上的,这样只要ssh到手机上,执行cycript命令,就可以使用了。但是经过尝试,iOS 11.2.5版本不能直接安装cycript,取而代之的是一个叫bfinject的工具。bfinject工具先安装到手机上,然后启动cycript监听服务,然后mac上安装cycript客户端,通过无线的方式连接手机暴露出来的ip和端口,就可以使用cycript编写和执行脚本了。

4.2.1 在手机上安装bfinject

https://github.com/BishopFox/bfinject下载bfinject,scp到手机上,然后解压,在解压目录运行bash bfinject启动服务。

通常情况下,会报错Unknown jailbrek。我们通过查看bfinject的代码,可以发现问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#
# Detect LiberiOS vs Electra
#
if [ -f /bootstrap/inject_criticald ]; then
    # This is Electra
    echo "[+] Electra detected."
    cp jtool.liberios /bootstrap/usr/local/bin/
    chmod +x /bootstrap/usr/local/bin/jtool.liberios
    JTOOL=/bootstrap/usr/local/bin/jtool.liberios
    cp bfinject4realz /bootstrap/usr/local/bin/
    INJECTOR=/bootstrap/usr/local/bin/bfinject4realz
elif [ -f /jb/usr/local/bin/jtool ]; then
    # This is LiberiOS
    echo "[+] Liberios detected"
    JTOOL=jtool
    INJECTOR=`pwd`/bfinject4realz
else
    echo "[!] Unknown jailbreak. Aborting."
    exit 1
fi

由以上代码第1个if分支可以判断,/bootstrap/inject_criticald目录不存在,这个是由于软件的目录变了,在不改变bfinject代码的前提下,需要修改一下目录,执行以下命令

1
2
3
4
ln -s /electra /bootstrap
mkdir /bootstrap/usr
mkdir /bootstrap/usr/local
mkdir /bootstrap/usr/local/bin

重新执行bash bfinject,还会出现一个错误md5: command not found,这个是由于需要运行md5程序,但是ios系统自带的程序叫md5sum,所以简单的办法是修改bfinject代码,将其中的md5改成md5sum就可以了。

4.2.2 在手机上启动bfinject服务

要查看哪个App,需要先在手机上启动,然后找到App的进程号,例如我们要查看微信的进程号,可以用以下命令

1
ps aux | grep containers | grep WeChat

假设微信的进程号为1026,运行

1
bash bfinject -p 1026 -L cycript

成功的话,在微信App的页面内会弹出一个框,上边的意思是正在监听xx.xx.xx.xx:1337端口(xx.xx.xx.xx为手机的ip地址),可以连接这个端口来进行cycript调试了。

4.2.3 在mac上安装cycript客户端

首先去http://www.cycript.org/下载代码,运行./cycript的时候,会报错,仔细查看是缺少ruby2.0的库,在mac上先安装ruby2.0

1
brew install ruby@2.0

安装后将/usr/local/Cellar/ruby@2.0/2.0.0-p648_2/lib/libruby.2.0.0.dylib拷贝到cycript/Cycript.lib目录下。顺利的话就可以用了。

在mac上执行

1
cycript -r xx.xx.xx.xx:1337

xx.xx.xx.xx为手机的ip,这里注意一下,因为是以无线连接的,所以手机和电脑需要在同一个局域网里,如果出现

1
cy#

代表cycript运行成功,可以通过写cycript脚本执行命令了。

5. 总结

本篇文章介绍了如何越狱iOS手机,以及逆向工具cycript的安装。关于cycript的详细使用,限于篇幅原因不介绍了,请自行查看相关资料。总之,这个工具非常强大,可以更深入的分析目标App的实现方式。

SDWebImage源码解读

SDWebImage是最流行的iOS第三方图片加载库,也是github上star数目最多的objective-c第三方库。这篇文章对SDWebImage的源码进行简单的分析,主要是分析代码的执行流程。源码版本是目前最新的稳定版本4.4.4。

源码目录

SDWebImage下分为以下几个目录

  • Downloader: 负责图片的下载,基于NSURLSession实现,主要类是SDWebImageDownloader
  • Cache: 负责图片的缓存,主要是对内存和磁盘进行读写,实现二级缓存功能,主要类是SDImageCache
  • Decoder: 负责图片的解码功能,以支持不同格式的图片,例如SDWebImageGIFCoder是对GIF图片的支持
  • Utils: 封装的工具类,主要类是SDWebImaegeManger,控制整个图片下载和缓存流程
  • Categories: 一些扩展支持,比如如果要对GIF图片进行下载展示,需要引入UIImage+GIF.h(框架已默认引入)
  • WebCache Categories: 对UIView的扩展支持,最常用的是UIImageView+WebCache.h,实现了UIImageView的图片加载和缓存功能
  • FLAnimatedImage: 对FLAnimatedImage进行了扩展,可以对动态图片进行加载和缓存

调用时序图

以官方Demo详情页加载图片为例,加载图片的时序图如下:

  1. 调用sd_internalSetImageWithURL:placeholderImage:options:operationKey:internalSetImageBlock:progress:completed:context:
  2. 调用loadImageWithURL:options:progress:completed:
  3. 调用queryCacheOperationForKey:cacheOptions:done查询缓存
  4. 调用imageFromMemoryCacheForKey查内存, 调用diskImageDataBySearchingAllPathsForKey查磁盘
  5. 调用downloadImageWithURL:options:progress:completed:从网络下载
  6. 调用storeImage:imageData:forKey:toDisk:completion:将结果缓存

源码分析

在官方Demo中,有一个列表页和详情页,我们从更简单的详情页来分析,详情页只有一个FLAnimatedImage控件,功能就是加载了一张图片,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
__weak typeof(self) weakSelf = self;
[self.imageView sd_setImageWithURL:self.imageURL
                  placeholderImage:nil
                           options:SDWebImageProgressiveDownload
                          progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL *targetURL) {
                              dispatch_async(dispatch_get_main_queue(), ^{
                                  float progress = 0;
                                  if (expectedSize != 0) {
                                      progress = (float)receivedSize / (float)expectedSize;
                                  }
                                  weakSelf.progressView.hidden = NO;
                                  [weakSelf.progressView setProgress:progress animated:YES];
                              });
                          }
                         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
                             weakSelf.progressView.hidden = YES;
                             [weakSelf.activityIndicator stopAnimating];
                             weakSelf.activityIndicator.hidden = YES;
                         }];

对外暴露的接口比较简单,传递需要加载图片的url,placeholder图片,加载选项options,加载过程回调progressBlock,完成回调completedBlock就可以了。

接下来到时序图的第(1)步,调用UIView+WebCachesd_internalSetImageWithURL:方法,代码如下:

1
2
3
4
5
6
7
8
9
10
[self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:nil
               internalSetImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
                           ……
                       }
                            progress:progressBlock
                           completed:completedBlock
                             context:@{SDWebImageInternalSetImageGroupKey: group}];

继续跟进这个方法,时序图到第(2)步,调用SDWebImageManagerloadImageWithURL:方法,代码如下:

1
2
3
4
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
  //加载完图片的回调
  ……
}

时序图来到第(3)步,SDWebImageManager会调用SDImageCachequeryCacheOperationForKey来进行缓存查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//SDImageCache.m
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {

    // 读内存缓存操作
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    void(^queryDiskBlock)(void) =  ^{
      //读磁盘缓存
      ……
    };

    if (options & SDImageCacheQueryDiskSync) {
        queryDiskBlock();
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }

    return operation;
}

可以看到,在上边的代码中,调用imageFromMemoryCacheForKey先从内存里查询是否有图片缓存,调用dispatch_async(self.ioQueue, queryDiskBlock)从磁盘里查询是否有图片缓存。这是时序图中的第(4)步。如果命中了缓存,则直接回调完成block,不再走下边的流程了。如果没有命中缓存,那么继续下边。

流程来到时序图中的第(5)步,查询完缓存返回后,SDWebImageManager会调用SDWebImageDownloaderdownloadImageWithURL:方法从网络下载图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//SDWebImageDownloader.m
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {

    LOCK(self.operationsLock);
    NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
    // There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
    if (!operation || operation.isFinished || operation.isCancelled) {
        operation = [self createDownloaderOperationWithUrl:url options:options];
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            LOCK(sself.operationsLock);
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        [self.URLOperations setObject:operation forKey:url];
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        //执行下载操作
        [self.downloadQueue addOperation:operation];
    }
    UNLOCK(self.operationsLock);

    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];

    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

从网络下载图片的代码如上,第8行生成NSOperation,第26行加入叫做downloadQueueNSOperationQueue执行。

下载完成后,时序图流程来到第(6)步,SDWebImageManager会调用SDImageCachestoreImage:方法将结果进行缓存,以便下次使用,然后再进行成功的回调,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

if (transformedImage && finished) {
    BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
    NSData *cacheData;
    // pass nil if the image was transformed, so we can recalculate the data from the image
    if (self.cacheSerializer) {
        cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
    } else {
        cacheData = (imageWasTransformed ? nil : downloadedData);
    }
    [self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
}

[self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];

到这里图片加载流程结束。

SDWebImage里还有很多实现细节,比如多线程的控制和加锁,各种控制下载行为的选项,取消正在下载的操作,URL参数的容错,PlaceHolder的设置,加载IndicatorView的显示控制,下载过程Progress的控制等。由于篇幅原因在本文中省略,等以后有时间再细致分析。

Aspects源码解读

Aspects是iOS面向切面编程的第三方库,它可以在不改变原有代码的情况下,在任意函数之前或之后插入代码,也可以替换掉函数原有的代码。它的原理是基于oc语言的runtime,这篇文章对Aspects进行源码解读,并阐述其原理。

调用方式

首先我们下载官方demo,从入口代码开始看:

1
2
3
4
AspectsViewController *aspectsController = [AspectsViewController new];
[aspectsController aspect_hookSelector:@selector(buttonPressed:) withOptions:0 usingBlock:^(id info, id sender) {
    NSLog(@"Button was pressed by: %@", sender);
} error:NULL];

这段代码就是Aspects的调用方式之一,表示在对象aspectsController的buttonPressed函数执行之后,再执行block里的代码,打印一行日志。withOptions的参数写的0,这里是一个枚举值,可以控制block代码怎样执行,具体的定义如下:

1
2
3
4
5
6
7
typedef NS_OPTIONS(NSUInteger, AspectOptions) {
    AspectPositionAfter   = 0,            /// Called after the original implementation (default)
    AspectPositionInstead = 1,            /// Will replace the original implementation.
    AspectPositionBefore  = 2,            /// Called before the original implementation.

    AspectOptionAutomaticRemoval = 1 << 3 /// Will remove the hook after the first execution.
};

hook过程

我们从入口函数进入开始跟踪代码,最后发现无论是对实例方法还是类方法进行hook,都会调用aspect_add函数,省略了一些无关代码后如下:

1
2
3
4
5
6
7
8
9
10
11
12
static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) {
    __block AspectIdentifier *identifier = nil;
    AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
    identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
    if (identifier) {
        [aspectContainer addAspect:identifier withOptions:options];

        // Modify the class to allow message interception.
        aspect_prepareClassAndHookSelector(self, selector, error);
    }
    return identifier;
}

这段代码做了两件事情。

首先生成AspectIdentifier,然后将AspectIdentifier加入到AspectsContainer中。AspectIdentifier的定义如下,它描述了一个Ascpect切片代码的信息。

1
2
3
4
5
6
7
@interface AspectIdentifier : NSObject
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end

AspectsContainer的定义如下,它负责容纳AspectIdentifier,可以在before,instead,after数组里放入多个AspectIdentifier,从名称可以看出这些AspectIdentifier所执行的时机。AspectsContainer将在后边取出并执行。

1
2
3
4
5
@interface AspectsContainer : NSObject
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end

其次调用aspect_prepareClassAndHookSelector函数,这是最关键的部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
    Class klass = aspect_hookClass(self, error);
    Method targetMethod = class_getInstanceMethod(klass, selector);
    IMP targetMethodIMP = method_getImplementation(targetMethod);
    if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
        // Make a method alias for the existing method implementation, it not already copied.
        const char *typeEncoding = method_getTypeEncoding(targetMethod);
        SEL aliasSelector = aspect_aliasForSelector(selector);
        if (![klass instancesRespondToSelector:aliasSelector]) {
            __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
        }
        // We use forwardInvocation to hook in.
        class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
    }
}

这个函数分为两部分,第2行aspect_hookClass和后边的部分。我们先来看aspect_hookClass函数,省略后的代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static Class aspect_hookClass(NSObject *self, NSError **error) {
  Class statedClass = self.class;
  Class baseClass = object_getClass(self);
  NSString *className = NSStringFromClass(baseClass);

    // Default case. Create dynamic subclass.
  const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
  Class subclass = objc_getClass(subclassName);

  if (subclass == nil) {
      subclass = objc_allocateClassPair(baseClass, subclassName, 0);
      aspect_swizzleForwardInvocation(subclass);
      aspect_hookedGetClass(subclass, statedClass);
      aspect_hookedGetClass(object_getClass(subclass), statedClass);
      objc_registerClassPair(subclass);
  }

  object_setClass(self, subclass);
  return subclass;
}

第11行代码通过运行时的函数objc_allocateClassPair定义了一个新的子类。如果是demo执行到这里的话,生成的子类叫AspectsViewController_Aspects。第12行,将子类的forwardInvocation替换为了自定义的实现函数__ASPECTS_ARE_BEING_CALLED__。第18行,将AspectsViewController实例的isa指针指向了子类AspectsViewController_Aspects。

接着,我们继续看aspect_prepareClassAndHookSelector函数的后半部分。第10行在AspectsViewController_Aspects类添加了一个方法aliasSelector,demo中就是aspect_buttonPressed,它的实现指向了原来AspectsViewController类的buttonPressed的实现。第13行,将AspectsViewController_Aspects类的buttonPressed实现指向了_objc_msgForward,这样调用就会启动oc的消息转发机制。

到这里,Aspects的hook流程就执行完了,我们用下边这个图来描述下当前类和方法实现之间的关系。

Aspects的实现为什么要生成一个原有类的子类,个人理解是为了对原有类产生的影响尽可能小。

hook后的执行流程

hook完成后,我们来看下hook后代码的执行流程。

这一段很重要!!!往AspectsViewController实例发送buttonPressed消息的时候,首先应该去查找实例所对应的类的方法列表,由于AspectsViewController的isa指向了AspectsViewController_Aspects类,就会去AspectsViewController_Aspects类中查找,结果是查找不到buttonPressed实现,然后会去查找父类AspectsViewController的方法列表,这时候查找到了buttonPressed的实现,但是实现是指向了_msg_forward,这样就进入了消息转发流程。按照消息转发流程,系统会调用AspectsViewController_Aspects类的forwardInvocation方法,forwardInvocation方法被我们替换成了自定义实现__ASPECTS_ARE_BEING_CALLED__,最终就进入了这个方法。

__ASPECTS_ARE_BEING_CALLED__的省略后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// This is the swizzled forwardInvocation: method.
static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
    SEL originalSelector = invocation.selector;
    SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
    invocation.selector = aliasSelector;
    AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
    AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
    AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
    NSArray *aspectsToRemove = nil;

    // Before hooks.
    aspect_invoke(classContainer.beforeAspects, info);
    aspect_invoke(objectContainer.beforeAspects, info);

    // Instead hooks.
    BOOL respondsToAlias = YES;
    if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
        aspect_invoke(classContainer.insteadAspects, info);
        aspect_invoke(objectContainer.insteadAspects, info);
    }else {
        Class klass = object_getClass(invocation.target);
        do {
            if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
                [invocation invoke];
                break;
            }
        }while (!respondsToAlias && (klass = class_getSuperclass(klass)));
    }

    // After hooks.
    aspect_invoke(classContainer.afterAspects, info);
    aspect_invoke(objectContainer.afterAspects, info);
}

第7行,对于hook的实例方法,先拿到之前设置的切片代码信息,存储在classContainer里。第24行,通过invocation调用AspectsViewController_Aspects的aspect_buttonPressed方法,由于这个方法已经指向了原来的实现buttonPressed,所以就调用了原始的代码。在这之后,如果Container里有afterAspects,就调用切片的block。beforeAspects同理。

到此为止,就实现了在原来的实例方法执行后,再执行hook插入的block代码。

总结

oc语言的runtime是黑魔法,运用起来可以做很多强大的功能。总的来讲,Aspects利用了method swizzling和消息转发机制forwordInvocation,实现了对函数进行切面hook。

对JSPatch原理的理解

JSPatch利用OC语言的动态特性,让OC语言根据传入的JS代码,进行动态行为修改,以达到热更新的目的。

项目中根据JSPatch的原理,自己实现了一套简单的热更新方案。以替换方法实现为例,热更新运行的步骤如下:

1.补丁下发阶段

应用启动的时候,会加载下发的补丁js文件,以下这一段热更新代码会被执行。

1
2
3
4
replaceMethod("IGTabBarController", "onNaviBarTaskBoxClick:", false, function (invocation) {
    log("origin method");
    callOriginMethod(invocation, "origin_onNaviBarTaskBoxClick:");
});

OC中的JSContext在初始化的时候加载过replaceMethod函数,所以会调用到OC代码

1
ocReplaceMethod:(NSString *)className selectorName:(NSString *)selectorName isClass:(BOOL)isClass func:(JSValue *)func

这个函数里,做了一个重要的逻辑(这里参考了JSPatch的实现方式),将IGTabBarController的实例方法onNaviBarTaskBoxClick:指向了forwardInvocation:,然后自定义实现PMDForwardInvocation替换forwardInvocation:的行为

2.用户调用阶段

用户操作点击后,IGTabBarController的onNaviBarTaskBoxClick:会被执行,从而PMDForwardInvocation被执行,根据OC的函数转发特性,PMDForwardInvocation会拿到所有的函数参数信息invocation。然后调用jsfunc(@[invocation])。这样就将所有原生参数通过invocation对象传回给了js代码。js代码拿到这些参数就可以去实现任何逻辑了,以达到替换原方法的目的。

总结:这里是一个很重要的技巧,如何将需要动态更新的OC方法的参数全部传给js代码,JSPatch是利用了forwardInvocation的特性。

Express学习笔记(二)-路由

这一篇主要介绍路由的用法

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var express = require('express');
var app = express();

// GET method route
app.get('/', function (req, res) {
  res.send('GET request to the homepage');
});

// POST method route
app.post('/', function (req, res) {
  res.send('POST request to the homepage');
});

app.all('/secret', function (req, res, next) {
  console.log('Accessing the secret section ...');
  next(); // pass control to the next handler
});

all表示接受所有的http方法

路由路径

支持正则表达式

1
2
3
app.get('/ab*cd', function(req, res) {
  res.send('ab*cd');
});

此路由路径将匹配 abcd、abxcd、abRABDOMcd、ab123cd 等。

路由处理顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var cb0 = function (req, res, next) {
  console.log('CB0');
  next();
}

var cb1 = function (req, res, next) {
  console.log('CB1');
  next();
}

app.get('/example/d', [cb0, cb1], function (req, res, next) {
  console.log('the response will be sent by the next function ...');
  next();
}, function (req, res) {
  res.send('Hello from D!');
});

可以写数组,也可以写多个回调函数参数,挨着执行,前一个函数必须执行next(),否则http请求会挂起

模块封装

可以封装一个bird.js模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var express = require('express');
var router = express.Router();

// middleware that is specific to this router
router.use(function timeLog(req, res, next) {
  console.log('Time: ', Date.now());
  next();
});
// define the home page route
router.get('/', function(req, res) {
  res.send('Birds home page');
});
// define the about route
router.get('/about', function(req, res) {
  res.send('About birds');
});

module.exports = router;

使用模块后,url从外部使用模块开始,接着模块定义的路径,就可以访问

1
2
3
var birds = require('./birds');
...
app.use('/birds', birds);

此时,可以响应/birds/birds/about