冷启动是指内存中不包含该应用程序相关的数据,必须要从磁盘载入到内存中的启动过程。
注意:重新打开 APP, 不一定就是冷启动。
一般来讲,统计 APP 启动时长,以 main 函数为节点 ,分两个大阶段:
接下来看一下项目中的 pre-main 阶段的耗时。
下图是我项目的加载耗时:
耗时过程分为以下4部分:
以上方法,都是和自己的项目代码息息相关的优化方案。不同项目具体是实施动作不一样。
还有一个优化方法,不管是什么项目,实施动作都一样 ,对什么项目都有效,那就是二进制重排!
学习二进制重排,首先要知道数据是如何加载到内存中的 。
我们已经知道数据加载到内存的过程,当虚拟内存页还没有对应的物理内存页时,会出现缺页异常(PageFault)。
当冷启动时,物理内存中是没有数据的,这时会出现大量的缺页异常,在iOS生产环境的app,在发生Page Fault进行重新加载时,iOS系统还会对其做一次签名验证,因此 iOS 生产环境的 Page Fault 比Debug环境下所产生的耗时更多
这里有没有优化空间呢?接下来就是优化方案:二进制重排!
在了解二进制重排之前,再了解下在项目编译生成二进制文件的时候,类及其内部方法实现的排列顺序是什么样的呢?
接下来查看 Link Map文件查看符号顺序, 查看方式:
****
5.我们可以直观的看出 link map中符号的顺序,类是以源文件的编译顺序,从上到下按序排列。方法名是以类中方法的书写顺序,由上到下排序。
从源码的执行顺序上看,应该是 load -> test2 -> viewDidLoad -> test1.
但是二进制文件中符号的顺序是方法从上到下的书写顺序,没有按照调用顺序去排列。
在冷启动分页加载二进制文件时,发现很多页中都有启动时需要用到的方法,那么即使页里面也存在启动时不需要的方法,但是由于内存是分页管理的,要加载就要整页加载。这样就导致了大量不需要在 pre-main 阶段执行的方法,也会被加载到内存中,增加了启动的耗时。
\
例如,启动需要加载100个页,每个页可以包含20个方法。但是每个页里只有2个方法是启动时后用到的。这样实际上启动时必须要的方法是2 * 100 = 200个,如果将这200个方法紧挨着放在一起,那么只需要2页。比100个页,减少了98页。这样耗时就会大大降低。
在项目编译生成二进制文件的时候,找到启动时需要的方法,并且将它们放在一起 重新排序,这就是二进制重排。
两个关键点: 找到启动时需要方法 & 方法 的重排序
重排序其实很简单。xcode已经为我们提供了这个机制,它使用的链接器叫做 ld, ld有一个参数叫做Order File, 我们可以通过配置order文件,来使编译时生成的二进制的文件的Link Map种的符号顺序,按照我们指定的顺序排列生成。而且 libobjc 实际上也做了二进制重排 。
【第一步】在项目根目录下建一个xxx.order的文件,里面写上按照自己想排列的顺序,写上方法或者函数的名字。(如果写了一个不存在的符号,也不会报错,会被自动过滤掉~)
【第二步】在 Build Settings 搜索order file 的文件。将项目根目录创建的文件,设置上去。
【第三步】重新编译,查看 Link Map 文件的顺序,果然,按照我们指定的顺序排列啦!
接下来,需要做的就是写入 order 文件里的符号了,我们不可能手写上所有的启动时需要的执行的符号,这里的所有符号包括,调用的方法、函数、C++构造方法、swift方法、block。
这里使用 LLVM 内置的简单代码覆盖率检测工具(SanitizerCoverage)。它在边缘、 函数、基本块 级别上插入对用户定义函数的调用。
edge (默认):检测边缘(所有的指令跳转都会被插入对用户定义函数的调用, 如循环、分支判断、方法函数等)。bb:检测基本块。func:仅将检测每个 功能的输入块(这个就是我们要重排序的符号)。按照文档,
__sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t *stop);
通过打印start, stop 地址的内容,从 start 地址开始,到 stop 地址的前4位,存储的是 uint32 的 1-19的数字。
我们可以从这个函数中知道, 当前项目中自定义的功能输入块的数量。
__sanitizer_cov_trace_pc_guard(&guard_variable)
也就是说,每个方法在执行的时候,都会调用上面这个方法。 接下来:
// 获取到本方法结束后,要返回的地址去,这个地址包含在被hook的方法内部,但不是被hook 的方法的首地址
void *PC = __builtin_return_address(0);
// 将结构体存入到原子队列中。
// offsetof(type,member) 返回结构体中成员的偏移值,由于指针PC是8字节,所以这里返回8字节。
// 详见下图
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
每个 SYNode 首地址都距离上一个偏移 PC 所占的字节数。这样做的妙处就是,每个 SYNode 的 next 的地址,恰巧就是下一个结构体的地址。这样方便获取队列里面的所有数据。
typedef struct dl_info {
const char *dli_fname; /* 所在文件 */
void *dli_fbase; /* 文件地址 */
const char *dli_sname; /* 符号名称 */
void *dli_saddr; /* 函数起始地址 */
} Dl_info;
//这个函数能通过函数内部地址找到函数符号
int dladdr(const void *, Dl_info *);
完整代码如下:
//
// ViewController.m
// TraceDemo
//
// Created by hank on 2020/3/16.
// Copyright © 2020 hank. All rights reserved.
//
#import "ViewController.h"
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
#import "TraceDemo-Swift.h"
@interface ViewController ()
@end
@implementation ViewController
+(void)initialize
{
}
void(^block1)(void) = ^(void) {
};
void test(){
block1();
}
+(void)load
{
}
- (void)viewDidLoad {
[super viewDidLoad];
[SwiftTest swiftTestLoad];
test();
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSMutableArray <NSString *> * symbolNames = [NSMutableArray array];
while (YES) {
SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if (node == NULL) {
break;
}
Dl_info info;
dladdr(node->pc, &info);
NSString * name = @(info.dli_sname);
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name: [@"_" stringByAppendingString:name];
[symbolNames addObject:symbolName];
}
//取反
NSEnumerator * emt = [symbolNames reverseObjectEnumerator];
//去重
NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString * name;
while (name = [emt nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
//移除本方法
[funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
//将数组变成字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"demo.order"];
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
NSLog(@"%@",funcStr);
}
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}
//原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct {
void *pc;
void *next;
}SYNode;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// 会导致load 方法被return
// if (!*guard) return;
// 获取到本方法结束后,要返回的地址去,这个地址包含在被hook的方法内部,但不是被hook 的方法的首地址
void *PC = __builtin_return_address(0);
SYNode *node = malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL};
//进入
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
@end
3.选择真机、项目、点击启动,当第一个页面显示出来后,点击停止。
优化前:项目的缺页遗产数量是427
优化后:
优化前:项目的缺页遗产数量是286
减少了启动时大概40%的缺页异常~
随着代码迭代,order文件需要更新,每次手动更新很麻烦,所以需要自动更新。
brew install ios-deploy
APP_ORDER_DIR=appOrderDir
APP_ORDER=./$APP_ORDER_DIR/Documents/app.order
mkdir $APP_ORDER_DIR
ios-deploy --download=/Documents --bundle_id $PRODUCT_BUNDLE_IDENTIFIER --to ./$APP_ORDER_DIR
if [ -e $APP_ORDER ] ;then
cp -f $APP_ORDER ./Resource/app.order
fi
rm -r $APP_ORDER_DIR
【补充xcode13】查看缺页异常的方式
选择真机、项目、点击启动,当第一个页面显示出来后,点击停止。
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我正在尝试测试是否存在表单。我是Rails新手。我的new.html.erb_spec.rb文件的内容是:require'spec_helper'describe"messages/new.html.erb"doit"shouldrendertheform"dorender'/messages/new.html.erb'reponse.shouldhave_form_putting_to(@message)with_submit_buttonendendView本身,new.html.erb,有代码:当我运行rspec时,它失败了:1)messages/new.html.erbshou
我在从html页面生成PDF时遇到问题。我正在使用PDFkit。在安装它的过程中,我注意到我需要wkhtmltopdf。所以我也安装了它。我做了PDFkit的文档所说的一切......现在我在尝试加载PDF时遇到了这个错误。这里是错误:commandfailed:"/usr/local/bin/wkhtmltopdf""--margin-right""0.75in""--page-size""Letter""--margin-top""0.75in""--margin-bottom""0.75in""--encoding""UTF-8""--margin-left""0.75in""-
在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev