美文网首页iOS开发知识点收集
iOS-APP启动优化(二)

iOS-APP启动优化(二)

作者: 泽泽伐木类 | 来源:发表于2020-11-23 15:26 被阅读0次
前言

iOS-APP启动优化(一)中,我们在文章中提到二进制重排的优化方式,对它的概念做了详细的介绍,同时我们也做了一些简单的应用,即通过编辑order file来实现二进制的重排,但是问题是我们如何将启动阶段的所有调用符号全覆盖到呐?通过我们这样一行行的手写,一定不是最佳实现方式,而且也不能做到真正的全覆盖。本片文章,我们就通过Clang 插桩 的方式来编写一个完整的,全覆盖的order file

Clang 插桩

Clang 插桩?这又是个什么牛逼的东西呐? 简单理解就是一个更加高级的Hook方式,比如我们通过Method swizling方式来实现运行时的hook,而Clang 插桩则是在编译器层面对二进制文件的操作,它会在所有的方法(包括OC,C/C++,Swift)边缘插入一些函数,当方法被调用时,插入的函数也会执行。我们通过实现这些函数,来达到更高级的Hook。 具体来看下官方文档:clang-llvm-SanitizerCoverage-tracing-pcs

LLVM has a simple code coverage instrumentation built in (SanitizerCoverage). It inserts calls to user-defined functions on function-, basic-block-, and edge- levels. Default implementations of those callbacks are provided and implement simple coverage reporting and visualization, however if you need just coverage visualization you may want to use SourceBasedCodeCoverage instead.

使用-fsanitize-coverage=trace-pc-guard编译器将在每个边缘插入以下代码:

__sanitizer_cov_trace_pc_guard(&guard_variable)

编译器还将插入对模块构造函数的调用:

// The guards are [start, stop).
// This function will be called at least once per DSO and may be called
// more than once with the same values of start/stop.
__sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop);

在每个间接调用中都会插入一个附加...=trace-pc,indirect-calls标志。__sanitizer_cov_trace_pc_indirect(void *callee)方法由开发者来实现。
这里我们直接使用文档中的示例代码,添加到我们项目中的任意的一个.m文件中,同时配置编译器添加-fsanitize-coverage=trace-pc-guard
1.配置编译器添加-fsanitize-coverage=trace-pc-guard
Target -> Build Settings -> Custom Compiler Flags

Custom Compiler Flags
此时Command + B 会报错:
Undefined symbol
这里跟文档说明是一致的,当配置完编译器后,需要开发者实现这2个函数。
  1. 实现2个插桩函数(这里我们的实现写在ViewController.m
@implementation ViewController

void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,uint32_t *stop){
    // Counter for the guards.
    static uint64_t N;
    // Initialize only once.
    if (start == stop || *start) return;
    printf("INIT: %p %p\n", start, stop);
    for (uint32_t *x = start; x < stop; x++)
    // Guards should start from 1.
    *x = ++N;
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  //__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

这段代码直接参考了文档中的示例代码,并做了一些简化。
__sanitizer_cov_trace_pc_guard_init () 该方法主要是统计了被插桩的符号数量,启动时会执行一次。当没添加一个方法时,数量就会加1。通过startstop读取数据:

INIT: 0x1047b2100 0x1047b21dc
(lldb) x/4gx 0x1047b2100
0x1047b2100: 0x0000000200000001 0x0000000400000003
0x1047b2110: 0x0000000600000005 0x0000000800000007
(lldb) x/4gx 0x1047b21dc-4
0x1047b21d8: 0x0000000000000037 0x0000000104aa4380
0x1047b21e8: 0x0000000000000000 0x00000001047af187

(这里的stop指针-4,是由于从startstop是一个完整的数据段,而stop指向的是数据端的末端,通过减4来读取数据段中最后一个元素)。这里是0x37即十进制55,当前统计到有55个符号数量。
如果这个方法只能获取到符号数量的话,对我们想要完善order File来说意义就不大了。
__sanitizer_cov_trace_pc_guard(): 当有方法调用时,会被该函数Hook;
比如这里我们通过添加一个touchesBegan:withEvent:来验证一下:

@implementation ViewController
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;
    void *PC = __builtin_return_address(0);
    char PcDescr[1024];
    //  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self func1];
}
//C函数
void test1(){

}
//OC方法
- (void)func1
{
    block();
}
//Block
void (^block)(void) = ^{
    test1();
};

运行代码,触发touchesBegan:withEvent:,查看printf()信息:

guard: 0x10442a14c 1a PC \220\244\337\377
guard: 0x10442a154 1c PC �
guard: 0x10442a158 1d PC �
guard: 0x10442a150 1b PC \SB��

这里来了4次,对应的也就是:
a. touchesBegan:withEvent:
b. func1
c. block()
d. test1()
这也就是说,当触发调用,就会触发__sanitizer_cov_trace_pc_guard,而且oc方法,C函数,Block 都能被它hook住。通过点点看下test1()的汇编指令:

test()汇编
这也印证了文档里面所说的,当配置了-fsanitize-coverage=trace-pc-guard后,Clang会在所有的方法边缘插入(生成IR文件前)__sanitizer_cov_trace_pc_guard

那在 __sanitizer_cov_trace_pc_guard() 函数中,能否拿到我们想要的东西呐?
这里,我们要看一下函数内部的一个重要的方法:

void *PC = __builtin_return_address(0);

通过断点,来看下这个方法PC的输出,执行touchesBegan:withEvent::进入断点,输出PC ,同时看下目前的堆栈信息:

PC指针与调用堆栈
这也就是说,__builtin_return_address(0)返回了函数的调用地址, 比如
frame #3: 0x0000000102c392ec TestApp`-[ViewController touchesBegan:withEvent:](self=0x0000000104602870, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x0000000281cd4140) at ViewController.m:48:5

那么,是否可以通过PC指针来拿到对应的符号信息呐?这里要介绍一个库:<dlfcn.h>, 这里面有个核心的方法:

 * Structure filled in by dladdr().
 */
typedef struct dl_info {
        const char      *dli_fname;     /* Pathname of shared object */
        void            *dli_fbase;     /* Base address of shared object */
        const char      *dli_sname;     /* Name of nearest symbol */
        void            *dli_saddr;     /* Address of nearest symbol */
} Dl_info;

extern int dladdr(const void *, Dl_info *);

通过PC指针来获取符号相关信息,这里返回给了一个Dl_info结构体。下面修改函数,来获取符号信息:

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (!*guard) return;
    void *PC = __builtin_return_address(0);
    /*
     const char      *dli_fname;
     void            *dli_fbase;
     const char      *dli_sname;
     void            *dli_saddr;
    */
    Dl_info info;
    dladdr(PC, &info); 
     printf("dli_fname:%s\ndli_fbase:%s\ndli_sname:%s\ndli_saddr:%s\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
//    char PcDescr[1024];
    //  __sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
//    printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}

执行结果:

dli_fname:/private/var/containers/Bundle/Application/E40E41B2-7D51-494D-99A5-07AA98DED1AD/TestApp.app/TestApp
dli_fbase:\317\372\355\376
dli_sname:-[ViewController touchesBegan:withEvent:]
dli_saddr:\377C�\321\375{�\251\375��\221(
guard: 0x102bee154 1a PC 4\264�\227\201�<�\320�\200�
dli_fname:/private/var/containers/Bundle/Application/E40E41B2-7D51-494D-99A5-07AA98DED1AD/TestApp.app/TestApp
dli_fbase:\317\372\355\376
dli_sname:-[ViewController func1]
dli_saddr:\377\303
guard: 0x102bee15c 1c PC 
dli_fname:/private/var/containers/Bundle/Application/E40E41B2-7D51-494D-99A5-07AA98DED1AD/TestApp.app/TestApp
dli_fbase:\317\372\355\376
dli_sname:block_block_invoke
dli_saddr:\377\303
guard: 0x102bee160 1d PC 
dli_fname:/private/var/containers/Bundle/Application/E40E41B2-7D51-494D-99A5-07AA98DED1AD/TestApp.app/TestApp
dli_fbase:\317\372\355\376
dli_sname:test1
dli_saddr:\375{\277\251\375�
guard: 0x102bee158 1b PC �\223\276��

这里很明显,其实对我们有价值的信息是dli_sname

INIT: 0x102abe0e0 0x102abe1b8
dli_sname:main
dli_sname:-[AppDelegate application:didFinishLaunchingWithOptions:]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate setWindow:]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate scene:willConnectToSession:options:]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate window]
dli_sname:-[ViewController viewDidLoad]
dli_sname:-[ViewController func1]
dli_sname:block_block_invoke
dli_sname:test1
dli_sname:-[ViewController loadOtherObject]
dli_sname:-[TestObject toDoSomething]
dli_sname:-[SceneDelegate sceneWillEnterForeground:]
dli_sname:-[SceneDelegate sceneDidBecomeActive:]
dli_sname:-[SceneDelegate window]
dli_sname:-[SceneDelegate window]

到此,我们通过Clang插桩就能获取到启动时的所有符号信息了,剩下的就是将符号写入到Order File中去,写入的方法就非常多了,可以通过写文件的方式写入,也可以直接控制台输出复制粘贴都可以。需要注意的是,在写入前要对符号做一些简单的处理:

  1. 方法可能重复调用,这就意味着存在重复的符号,需要去重;
  2. order file写入格式要求,C函数需要在方法前添加下划线_;

接下来就举例演示一下(这并不是唯一方式)

编写完整Order File

这里先避开一个坑点:
while循环,for循环的执行会触发__sanitizer_cov_trace_pc_guard()的Hook,
这是我们不希望的,这里修改下Custom Compiler Flags的配置:
-fsanitize-coverage=trace-pc-guard修改为-fsanitize-coverage=func,trace-pc-guard,来屏蔽掉whilefor的干扰。

#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#include <dlfcn.h>
#include <libkern/OSAtomic.h>

//符号结构体
typedef struct{
    void *pc;
    void *next;
}SymbolNode;
//原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//    if (!*guard) return; //该判断会屏蔽掉load方法
    void *PC = __builtin_return_address(0);
    //创建结构体
    SymbolNode *node = malloc(sizeof(SymbolNode));
    *node = (SymbolNode){PC,NULL};
    //加入AtomicQueue
    OSAtomicEnqueue(&symbolList, node, offsetof(SymbolNode, next));
}
//-fsanitize-coverage=trace-pc-guard
//-fsanitize-coverage=func,trace-pc-guard
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    NSMutableArray *nodes = [NSMutableArray array];
    while (YES) {
        SymbolNode *node = OSAtomicDequeue(&symbolList, offsetof(SymbolNode, next));
        if(node == NULL) break;
        Dl_info info = {0};
        dladdr(node->pc, &info);
        NSString *sName = @(info.dli_sname);
        BOOL isOC = ([sName hasPrefix:@"+["] || [sName hasPrefix:@"-["]) ? YES : NO;
        if(isOC){
            if(![nodes containsObject:sName]){
                [nodes addObject:sName];
            }
            continue;
        }
        sName = [@"_" stringByAppendingString:sName];
        if(![nodes containsObject:sName]){
            [nodes addObject:sName];
        }
    }
    NSMutableArray *newNodes = (NSMutableArray *)[[nodes reverseObjectEnumerator]allObjects];
    NSString *nodesStr =  [newNodes componentsJoinedByString:@"\n"];
    NSLog(@"nodesStr == \n%@",nodesStr);
    NSData *strData = [nodesStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager]createFileAtPath:[NSString stringWithFormat:@"%@app.order",NSTemporaryDirectory()] contents:strData attributes:nil];
}
总结

一切的未知,都只是我们认知范围内的未知,其问题本身都是有答案的;而我们要做的就是在有限的时间里,找到它,让我们认知中的未知,变成已知。

相关文章

  • iOS-APP启动优化(二)

    前言 在iOS-APP启动优化(一)[https://www.jianshu.com/p/5b41cb0c70ba...

  • iOS-App启动优化

    手动目录Main之前Main 之后二进制重排系统默认加载方式1)、查看PageFault 次数2)、查看系统默认的...

  • iOS-APP启动优化

    App的启动可以分为2种: 冷启动(Cold Launch)从零开始启动APP 热启动(Warm Launch)A...

  • iOS-APP性能优化-启动优化

    APP的启动可以分为2种: 冷启动(Cold Launch):从零开始启动APP。 热启动(Warm Launch...

  • iOS-APP启动优化(一)

    前言 APP的启动优化,对开发者来说是一个永无止境的过程。开发者们在追求更快的路上,实现了一次又一次的突破(这里也...

  • App启动优化(三)启动优化方案

    系列文章 App启动优化(一)冷启动和热启动 App启动优化(二)启动时间测量 App启动优化(三)启动优化方案 ...

  • 重拾iOS-App启动性能优化

    关键词:冷启动,热启动,dyld,+load,+initialize,Time Profiler 一、App启动流...

  • Android系统原理

    Android性能优化(一)App启动原理分析及启动时间优化 - CSDN博客 Android性能优化(二)布局渲...

  • Android性能优化之启动优化(实战篇)

    目录 一、启动优化的意义 二、启动时间检测 三、启动优化工具---traceview 四、优化方案1.异步初始化2...

  • iOS 性能优化三

    主要讲解APP冷启动的优化 iOS 性能优化一iOS 性能优化二iOS 性能优化三 1. APP 启动的分类 冷...

网友评论

    本文标题:iOS-APP启动优化(二)

    本文链接:https://www.haomeiwen.com/subject/pstjiktx.html