Jasen Huang's Blog

The Climb


  • 首页

  • 关于

  • 归档

  • 公益404

How to fishhook with block

发表于 2021-01-30   |  

fishhook function with ObjC block :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 原函数
int testFunc1(char* a, struct test_t b) {
return b.x + 1;
}
// hook
kk_fish_hook(@"testFunc1", (kk_replacement_function)^int(void *replaced, char* name, struct test_t rect) {
// 调用原函数
int(*origin)(char*,struct test_t) = (int(*)(char*,struct test_t))replaced;
int sum = origin(name, rect);
return sum;
});

// var_lit
kk_fish_hook(@"printf", (kk_replacement_function)^int(void *replaced, char * format, KKTypeList){
int(*origin)(char*,...) = (int(*)(char*,...))replaced;
return origin(format, KKVarList);
});

实现原理

  1. block signature

    • 通过获取block的signature(block的参数要与Hook的函数匹配)确定hook的函数原型ffi_cif
  2. libffi

    • 构造ffi_closure返回_kk_ffi_closure_func
    • 用fishhook rebind_symbols把函数调用重定向到_kk_ffi_closure_func
    • _kk_ffi_closure_func里构造NSInvocation,把原函数replaced 作为第一个参数传给NSInvocation,对block进行invokeWithTarget实现block调用
  3. 如何hook带可变参数的函数 printf

    • 因为可变参数无法传递,插桩代码后要调用回原函数直接枚举多个参数
      KKTypeList + KKVarList

      另外通过汇编,保存栈指针和寄存器 + jmp 的方法也可以实现,然而就相对复杂很多了

源码

KernelKit 是源自于QQ邮箱/微信读书移动客户端项目多年开发经验提炼了出来的框架,封装了iOS、Macosx常用的底层API,涉及动态库,线程,内存,Crash, Hook, I/O, ObjC-ABI等内容,致力于为iOS、Macosx底层操作系统接口提供更好的API封装,持续更新中。

NSAttributedString的autorelease内存风暴

发表于 2020-11-11   |  

前言

  微信读书iOS客户端也有几年历史了,和人一样,项目代码老了或多或少总有一些毛病很难根治。微信读书iOS客户端就有一个这样的历史代码模块:阅读器排版引擎。想来这个引擎代码还是和 @bang哥 共事年代的代码。基于DTCoreText的HTML树排版引擎,总体思路就是把HTML文本通过libxml2解析,再用DFS遍历树结点,处理CSS后转成NSAttributedString,最终利用CoreText渲染出来,当然少不了在源码基础上,针对业务进行各种魔改。

顽疾

  回到微信读书排版引擎的问题,经常有微信读书的线上用户反馈过来,阅读的时候经常卡死闪退,但是我们的内部监控系统(bugly/matrix)一直没有抓到crash堆栈上报。这就很神奇了,根据以往经验,没有crash堆栈上报很大概率是被系统的WatchDog强杀了,一般是OOM或者wakelock太频繁。针对用户反馈的日志定位到相应的场景,发现有一个共性:HTML里<p>标签特别多,具体用户的场景里是遇到单个章节有2w+的<p>标签,大胆盲猜,这里是OOM了,果然用instruments一跑,发现了有个神奇的东西:


Allocation Summary里多了很多@autoreleasepool content,每一个占去了4k的内存,App的内存在短时间内暴涨到1个G以上,触发系统WatchDog强杀。

定位原因

  这里大小为4k@autoreleasepool content是怎么产生的?而且NSAttributedString在整个iOS开发生态里使用非常多,UIKit系统控件都会用到,为什么单单这个场景会引发这个问题? 难道又是打开方式不对?长文预警,想看结论的同学可以直接跳到后面

我们带着这两个疑问继续往下分析,我们先来看下调用链:

  1. DFS遍历树结点[DTHtmlElement attributeString]
  2. 在<p>标签结束处理段落appendEndOfParagraph,代码可以简化为:

    1
    2
    3
    4
    5
    6
    7
    8
    - (void)appendEndOfParagraph {
    ....
    // create a temp attributed string from the appended part
    NSAttributedString *appendString =
    [[NSAttributedString alloc] initWithString:@"\n" attributes:attributes];

    [self appendAttributedString:appendString];
    }
  3. 生成NSAttributedString对象,并设置属性attributes

  4. NSConcreteHashTable触发autoreleaseFullPage去申请内存_malloc_zone_memalign

单纯看项目代码,是很普通的函数调用,为什么会用到HashTable,HashTable又为什么会触发autoreleaseFullPage?autorelease pool没有及时释放吗?
关于autorelease,网上有很多文章,大家可以自行google或参考文章最后的文章链接,基本结论可以归结为:

  1. autorelease对象什么时候产生?

    • 以alloc/new/copy/mutableCopy开头的函数调用,编译器会自动插入release语句,否则返回的对象会加到autorelease池子中
    • 函数调用会根据objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue进行TLS优化判断,避免autorelease过多
  2. autorelease对象什么时候释放?

    • @autoreleasepool{} -> 退出作用域的时候
    • NSThread -> 线程退出调用tls_dealloc的时候
    • GCD -> 处理完队列任务后调用 _dispatch_last_resort_autorelease_pool_pop

不过GCD任务存在多线程切换的时机问题,释放时机有随机性,按照Apple文档的说法:

If your block creates more than a few Objective-C objects, you might want to enclose parts of your block’s code in an @autorelease block to handle the memory management for those objects. Although GCD dispatch queues have their own autorelease pools, they make no guarantees as to when those pools are drained. If your application is memory constrained, creating your own autorelease pool allows you to free up the memory for autoreleased objects at more regular intervals.
大概的意思就是GCD会自动添加autorelease池,但释放时机不能保证,创建大量对象时需要自行添加@autoreleasepool{}保证及时释放

回到微信读书排版引擎的代码实现本身,逻辑上讲并没有什么问题,可以简化为:

1
2
3
4
5
6
dispatch_group_async(queue, ^{
...
assembleString = [node attributeString];
[assembleString appendEndOfParagraph];
...
})

  那这里是不是加上@autoreleasepool{}就万事大吉了呢?结果一顿操作,内存是降下来了,但排版速度却严重变慢,看来还不是这么简单,这里先盗张图:

autorelease池子会不断pop对象并调用[obj release],查询共用的sizetable重新计算refCnt,refCnt为0的时候要调[obj dealloc],autorelease对象多的话这个过程还是挺耗时的。

  既然不能直接加@autoreleasepool{},那回过头来看,可不可以减少加到autorelease池里对象呢?首先我们要搞清楚这里加入aurelease池是哪些对象?为什么会产生大量的对象?从代码上看,NSSAttributeString直接调用alloc方法,返回对象经过TLS优化也不存在加入aurelease池的情况(可以在xcode里通过Debug->DebugWorkflow->Disassembly查看汇编代码,再用LLDB进去看TLS优化生效了没有)没办法只能祭出最后的武器:汇编

  通过反编译Foundation/UIFoundation/libobjc.A.dylib,我们大概理清了一下NSAttributedString的初始化过程:

  1. NSConcreteAttributedString初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    int -[NSConcreteAttributedString initWithString:attributes:]() {
    r14 = rcx;
    rbx = [rdi initWithString:rdx];
    if ((r14 != 0x0) && (rbx != 0x0)) {
    r15 = [[NSMutableRLEArray allocWithZone:[rbx zone]] init];
    r14 = [__NSAttributeDictionaryClass() newWithDictionary:r14]; //r14 是传入属性NSDictionary
    [rbx length];
    [r15 insertObject:r14 range:0x0];
    [r14 release];
    rbx->attributes = r15;//r15 是 NSMutableRLEArray
    }
    rax = rbx;
    return rax;
    }
  2. NSDictionary -> NSAttributeDictionary过程可以简化为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    + (NSAttributeDictionary*)newWithDictionary:(NSDictionary*)dict {
    if (!dict) {
    return _emptyAttributeDictionary;
    }

    os_unfair_lock_lock_with_options(_attributeDictionaryLock, 0x10000);
    NSAttributeDictionary* rax = [_attributeDictionaryTable getItem:dict];//NSConcreteHashTable
    if (!rax){
    os_unfair_lock_unlock(_attributeDictionaryLock);
    ...
    NSZoneMalloc();//找不到重新new一个
    ...
    [dict getObjects:andKeys:count:];// 遍历dict取出kv对,copy到NSAttributeDictionary
    ...
    }else{
    [rax retain];//找到直接返回对象复用
    os_unfair_lock_unlock(_attributeDictionaryLock);
    }
    return rax;
    }

_attributeDictionaryTable是全局共用的NSConcreteHashTable,操作的时候需要加上os_unfair_lock,NSAttributeDictionary存放在NSConcreteHashTable,看逻辑应该是为了在初始化NSAttributedString的attributes的时候进行对象复用。

  1. NSConcreteHashTable如何查找NSDictionary

    1
    2
    3
    4
    5
    6
    - (void)getItem:(NSDictionary*)dict {
    if (dict != 0x0) {
    rax = _hashProbe(dict);//关键函数
    }
    return rax;
    }
  2. hashProbe函数简化版:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    id hashProbe(NSDictionary* dict) {
    void* location = attributeDictionaryHash(dict);// hash函数:遍历kv值进行hash累加
    do {
    ...
    location = (location + 1) & (capicity - 1);// hash值偏移0x1 线性探测
    obj = readARCWeakAtWithSentinel(location, 0);
    if (!obj){
    break; // hash桶的位置数据为空
    }
    } while (!isEqualFuntion(obj, dict));

    return obj;
    }
  3. readARCWeakAtWithSentinel从HashTable里取出NSAttributeDictionay

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    id readARCWeakAtWithSentinel(void* location, int sentinel){
    ...
    rax = objc_loadWeak(locaction);// 关键函数
    return rax;
    }

    id objc_loadWeak(void* location){
    if (!*location) return nil;
    return objc_autorelease(objc_loadWeakRetained(location));//autorelease在这里
    }

看到这里终于理清了这些autorelease对象是哪里产生: NSConcreteHashTable进行hash探测的时候,会不断读出hash值所在location的对象,放到autorelease池并进行isEqualFunction比较,如果不相等,hash值会先偏移0x1继续while查找。问题就在这里:如果NSConcreteHashTable里存了大量的对象,那这个while过程会不断产生autorelease对象,造成AutoreleasePoolPage::autoreleaseFullPage不断重新申请4k的内存

实践出真理,我们直接fishhook相应的函数来验证:

1
2
3
4
5
6
7
8
NSUInteger attributeDictionaryCount = 0;
static id (*orig_objc_loadWeak)(id *location);
id my_objc_loadWeak(id *location) {
if ([(*location) isKindOfClass:NSClassFromString(@"NSAttributeDictionary")]){
++attributeDictionaryCount;// 计数
}
return orig_objc_loadWeak(location);
}

排版一个有2w个<p>标签的HTML章节,objc_loadWeak会加载6747w个NSAttributeDictionary对象

解决方案

  到此,NSAttributedString内存暴涨的原因算是找到了,解决方案其实很简单:就是如何避免hash冲突。这里有两个前提:

  1. 直接去改NSDictionary的hash函数有点困难
  2. 加@autoreleasepool{} 影响效率

其实从场景上分析,HTML的<p>标签是有限个(实际场景中超1w个的都是极少数),而且<p>对应的attributes里只保存的对象的pointer,内存占用大小也只是NSAttributeDictionay本身,attributes复用的意义并不是很大。这里直接在attributes里加上一个 随机因子,减少 hashProbe 的命中数,NSConcreteHashTable没命中会直接跳过objc_loadWeak的调用,也就不会产生autorelease对象了。

1
2
3
4
5
6
7
8
9
10
- (void)appendEndOfParagraph {
....
// create a temp attributed string from the appended part
NSAttributedString *appendString =
[[NSAttributedString alloc] initWithString:@"\n" attributes:attributes];

[attributes setObject:@(arc4random()) forKey:@"random"];//随机因子

[self appendAttributedString:appendString];
}

优化后解析同一个HTML章节,objc_loadWeak只加载了 27w个NSAttributeDictionary对象

当然这种解决方案并不是最好的,只是衡量投入产出比后折中的方案,如果有更好的解决方案,欢迎私信交流!

最后打个广告:微信读书iOS/Android客户端大量招人,欢迎大家加入
简历直接发:jasenhuang@rdgz.org

友情链接

  1. 黑幕背后的Autorelease
  2. 自动释放池的前世今生
  3. Autorelease 之不经意间可能被影响的优化
  4. Revisit iOS Autorelease

Swizzle Foundation容器的正确姿势

发表于 2016-01-04   |  

Nil Crash

这事还得从一个crash 讲起。。。
在开发iOS App的时候,很多时候会遇到下面这种场景:

1
2
3
4
5
6
7
8
//从数据库读取
NSString* item = [NSString stringWithUTF8String:sqlite3_column_text(statement, idx)];

//长长的逻辑
...

NSMutableArray* items = [NSMutableArray array];
[items addObject:item]

上面的代码存在两个地方可能引发crash,没办法,项目进度紧张,先保护一下再说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//从数据库读取
NSString* item;
const char* name = sqlite3_column_text(statement, idx);
if (name != NULL){
item = [NSString stringWithUTF8String:name];
}

//长长的逻辑
...

NSMutableArray* items = [NSMutableArray array];
if (item.length){
[items addObject:item]
}

但很快你就会发现,如果项目中要加保护的地方数不胜数,而且很容易在开发阶段把问题隐藏起来。
但不加的话,如果修复不及时,很容易把crash带到线上版本,严重影响产品的质量。
这个时候最好的办法就是Hook掉 addObject:和 stringWithUTF8String:这两个函数,
然后在Debug模式下Assert,Release模式就打Log 帮助定位。

阅读全文 »

wbxml for microsoft activesync

发表于 2013-10-16   |  

WBXML for Microsoft Exchange ActiveSync

Microsoft Exchange ActiveSync是适用于移动设备的同步协议,它采用的数据传输的格式是wbxml。
本来用了libwbxml 来为xml数据 encode, 结果死活每个命令都是返回 [102(invalid wbxml)][status code]
[status code]: http://msdn.microsoft.com/en-us/library/ee218647(v=exchg.80).aspx
没有办法, 只能自己手动写 wbxml的 encode decode 了。
查了下官网,activesync 对 [WAP Binay XML algorithm][wbxml]
[wbxml]: http://msdn.microsoft.com/en-us/library/ee237245(v=exchg.80).aspx
有详细的说明,microsoft activesync官网 有 c#版本的[sample code][sample]
[sample]:http://msdn.microsoft.com/en-us/library/hh361570(v=exchg.140).aspx
这里借助 tinyxml 将 这个sample code 翻译成c++
有需要的同学请猛戳 这里

django+mongodb web 应用

发表于 2013-07-18   |  

django + mongodb 快速搭建web 应用

接近python时间最不算短,但直到最近才真正用python去做一些项目,说来实在惭愧。
刚好想做一个移动互联网的项目,没人来做后台,那就自己来吧,发现python真的好多web 框架,对于我这种拿来主义的人来说,是再好不过了。
google了一下,决定 用django + mongodb ,嗯,这货确实受欢迎,django 框架 功能丰富,用来做应用的原型再合适不过了。
django 标准的MVC结构,简洁的GRUD模型用起来很方便 , 很pythonic,具体的使用方法就不赘述了,有兴趣的朋友请猛戳https://www.djangoproject.com/
django的database backends还不支持mongodb , 不过可以利用mongoengine进行GRUD的无缝转接,附上settings.py的部分设置

1
2
3
4
5
6
7
8
9
10
11
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.dummy', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
'NAME': '', # Or path to database file if using sqlite3.
# The following settings are not used with sqlite3:
'USER': '',
'PASSWORD': '',
'HOST': '', # Empty for localhost through domain sockets or '127.0.0.1' for localhost through TCP.
'PORT': '', # Set to empty string for default.
}
}

有一点要请意的是,django本身自带一个User Document, 想要自定义的话比较麻烦,需要重写

1
2
3
4
5
6
7
8
9
AUTHENTICATION_BACKENDS和MONGOENGINE_USER_DOCUMENT

AUTHENTICATION_BACKENDS = (
'gamelab.webapp.models.authmodel.MyMongoEngineBackend',
)

MONGOENGINE_USER_DOCUMENT = 'xx.xx.xx.MyCUser'

SESSION_ENGINE = 'mongoengine.django.sessions'

具体可以参与django源码mongoengine.django.auth做扩展

用python 写简单的网络爬虫 抓取google play的app 信息

发表于 2013-07-18   |  

最近在做一个项目,需要抓取 google play 的应用信息,包括app的包名,icon, 截图,评论等。
又因为一直被python的优雅语法所吸引,所以决定用python 做做项目,当练手都好。
分析了一下google play 的网页格式,发现该要信息静态页面基本都有了,好嗨森,不用去分析javascript。
其实只要拿到了应用的包名(com.xxx.xxx之类的) 就可以直接跳到应用的detail页面抓信息了。
不过有一点是比较麻烦的是怎么拿到尽可能多的包名?
首先来看一下:
https://play.google.com/store/apps/category/GAME/collection/topselling_free?start=480&num=26
像这样的一个url ,如果start超过了499,google 会自动屏蔽的。
这里用了两个小技巧:

  • 就是在每个app的detail页面会有相关的其它app的推荐,可以由此递归找下去。
  • 用关键字search出相应的app,再递归找下去(这个还没做,不过策略这种东西什么时候加都可以)
    按照这种想法,用python建立一个生产者消费者的模型,把新找到的包名加到队列,再依次读出来去请求detail页抓取app的信息.
    python的生产者消费者模型实现起来就有很多种方式了:

生产者

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
class QueueState:
stop = 0
running = 1

class GLCManager(object):
__metaclass__ = Singleton #网上找到python singleton的写法,要实现Singleton的类, 以后有机会再研究
taskQueue = Queue()
consumers = []
queueState = QueueState.stop
#game id Manager for consumer
def __init__(self):
self.queueState = QueueState.stop;
for i in range(1, 5):
consumer = GLCConsumer()
self.consumers.append(consumer)
def __del__(self):
self.stop();
def addGameTask(self , id):
self.taskQueue.put(id)
def start(self):
if self.queueState == QueueState.stop:
for consumer in self.consumers:
consumer.start()
self.queueState = QueueState.running
def stop(self):
if self.queueState == QueueState.running:
for consumer in self.consumers:
consumer.isStop = True
consumer.join()
self.queueState = QueueState.stop
阅读全文 »

c++仿object-c的performselector 实现类对象与函数的分离

发表于 2012-09-09   |  

C++ performSelector

我们知道object-c是一个类动态语言,它可以直接通过类似objc_send()这样的一个函数,给内存中一个对象发出函数调用的消息,从面实现对象与函数的分离,这个实际中的编程带来很大的灵活性。
那么在C++中能不能也这样呢?
在最近的一个项目中,我尝试一个办法来模拟这样的过程,实现对象与函数的分离,仅供作为个人随笔,大神们请无视。
其实很简单,只需设置一个辅助基类Base就可以,不啰嗦,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Base {};
class A: public Base{
public:
void func1(const char* szEvent , void * object){
printf("A call func1\n");
}
};
class B: public Base{
public:
void func2(const char* szEvent , void * object){
printf("A call func1\n");
}
};

这里定义一下通用的类对象指针 Handler

1
2
3
4
5
6
7
8
9
10
11
12
typedef void (Base::*Handler)(const char* , void *);
int main()
{
A a;
B b;
Handler handler1 = (Handler)&A::func1;
Handler handler2 = (Handler)&B::func2;
//这样就可以把类对象和函数指针归一化,分开存储,然后在适 当的时候进行调用。
(a.*handler1)("" , NULL);
(b.*handler2)("" , NULL);
return 0;
}

Conclusion

当然,object-c 是一门动态语言,它内部实现performSelector 是通过运行时的消息传递来实现的,
具体可以打开 runtime.h 看下。 object-c 运行时还可以动态 创建类,添加function, 修改function的实现等等 以后有时间再写吧

Jasen Huang

Jasen Huang

7 日志
1 分类
15 标签
GitHub Weibo
© 2015 - 2021 Jasen Huang
由 Hexo 强力驱动
主题 - NexT.Mist