架构师之路iOS 底层原理

OC底层原理三十三:启动优化(二进制重排)

2020-11-22  本文已影响0人  markhetao

OC底层原理 学习大纲

  1. 冷启动和热启动
  2. 启动性能检测和分析
  3. 虚拟内存与物理内存
  4. 二进制重排原理
  5. PageFault检测
  6. 体验二进制重排

1. 冷启动和热启动

首次启动应用、kill应用后重新打开应用、应用置于后台隔一段时间再返回前台等情况,都是应用启动

有时启动,有时启动。这是冷启动热启动的原因:

冷启动:

热启动:

冷启动热启动区别场景

【区别】内存是否有加载的数据

  • 有:热启动无需重新加载数据速度快
  • 无:冷启动需要磁盘读取数据加载内存中,耗时,速度慢

【场景】

  • 首次启动: 一定冷启动。(内存中无数据
  • kill后启动:冷启动热启动 (取决于内存中是否有数据
  • 置于后台再回到前台: 冷启动热启动 (取决于内存中是否有数据)
    (ps: 如果其他应用需要更多内存空间系统可能自动覆盖你的内存空间提供给其他应用使用,此时你的数据就被覆盖了,回到前台时,应用自动重启

2. 启动性能检测和分析

测试APP启动,分为两个阶段:

系统处理,我们从dyld应用加载的流程来优化。(借助系统工具分析耗时)

通过检测业务流程优化main函数打个时间点第一个页面渲染完成打个时间点。测算耗时)

2.1 main函数前

大家可以使用自己的项目作为观察对象,此处是以砸包后的某个应用为测试对象仅供观察学习

ps: 此处记录下砸壳后的包重签名过程:(看官们可忽略此处 😂)

    1. 新建APP文件夹,放入砸壳后的包
      image.png
    1. 加入appSign.sh重签名脚本:
# ${SRCROOT} 它是工程文件所在的目录
TEMP_PATH="${SRCROOT}/Temp"
#资源文件夹,我们提前在工程目录下新建一个APP文件夹,里面放ipa包
ASSETS_PATH="${SRCROOT}/APP"
#目标ipa包路径
TARGET_IPA_PATH="${ASSETS_PATH}/*.ipa"
#清空Temp文件夹
rm -rf "${SRCROOT}/Temp"
mkdir -p "${SRCROOT}/Temp"

#----------------------------------------
# 1. 解压IPA到Temp下
unzip -oqq "$TARGET_IPA_PATH" -d "$TEMP_PATH"
# 拿到解压的临时的APP的路径
TEMP_APP_PATH=$(set -- "$TEMP_PATH/Payload/"*.app;echo "$1")
# echo "路径是:$TEMP_APP_PATH"

#----------------------------------------
# 2. 将解压出来的.app拷贝进入工程下
# BUILT_PRODUCTS_DIR 工程生成的APP包的路径
# TARGET_NAME target名称
TARGET_APP_PATH="$BUILT_PRODUCTS_DIR/$TARGET_NAME.app"
echo "app路径:$TARGET_APP_PATH"

rm -rf "$TARGET_APP_PATH"
mkdir -p "$TARGET_APP_PATH"
cp -rf "$TEMP_APP_PATH/" "$TARGET_APP_PATH"

#----------------------------------------
# 3. 删除extension和WatchAPP.个人证书没法签名Extention
rm -rf "$TARGET_APP_PATH/PlugIns"
rm -rf "$TARGET_APP_PATH/Watch"

#----------------------------------------
# 4. 更新info.plist文件 CFBundleIdentifier
#  设置:"Set : KEY Value" "目标文件路径"
/usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier >$PRODUCT_BUNDLE_IDENTIFIER" "$TARGET_APP_PATH/Info.plist"

#----------------------------------------
# 5. 给MachO文件上执行权限
# 拿到MachO文件的路径
APP_BINARY=`plutil -convert xml1 -o - $TARGET_APP_PATH/Info.plist|grep -A1 Exec|tail -n1|cut -f2 -d\>|cut -f1 -d\<`
#上可执行权限
chmod +x "$TARGET_APP_PATH/$APP_BINARY"

#----------------------------------------
# 6. 重签名第三方 FrameWorks
TARGET_APP_FRAMEWORKS_PATH="$TARGET_APP_PATH/Frameworks"
if [ -d "$TARGET_APP_FRAMEWORKS_PATH" ];
then
for FRAMEWORK in "$TARGET_APP_FRAMEWORKS_PATH/"*
do

#签名
/usr/bin/codesign --force --sign "$EXPANDED_CODE_SIGN_IDENTITY" "$FRAMEWORK"
done
fi

#注入
#yololib "$TARGET_APP_PATH/$APP_BINARY" >"Frameworks/HankHook.framework/HankHook"
    1. Demo工程添加脚本指令./appSign.sh
      image.png
Total pre-main time: 1.2 seconds (100.0%)
         dylib loading time: 326.38 milliseconds (25.4%)
        rebase/binding time: 146.54 milliseconds (11.4%)
            ObjC setup time:  40.49 milliseconds (3.1%)
           initializer time: 767.04 milliseconds (59.9%)
           slowest intializers :
             libSystem.B.dylib :   6.86 milliseconds (0.5%)
    libMainThreadChecker.dylib :  38.26 milliseconds (2.9%)
          libglInterpose.dylib : 447.73 milliseconds (34.9%)
             marsbridgenetwork :  48.86 milliseconds (3.8%)
                          mars :  30.85 milliseconds (2.4%)
                       砸壳应用 : 212.00 milliseconds (16.5%)

2.2 分析DYLD耗时元素:

2.2 main函数后

  1. 启动用不到的类和页面,移到启动后创建
  2. 耗时操作使用多线程处理
  3. 启动页面,尽量不用XIBStoryBoard

在讲二进制重排前,必须知道虚拟内存物理内存

3. 虚拟内存与物理内存

物理内存容易理解,就是真实内存条容量。但虚拟内存是个

3.1 虚拟内存

  • 早期计算机,没有虚拟内存概念,只有物理内存每个应用都直接全部信息写入内存条中的。当内存条空间不够时(被其他应用占据了),就会报内存警告。这时我们只能手动关闭一些应用腾出内存来让当前应用运行。
    image.png
  1. 内存不够: 每个应用打开,就把所有信息加载进去,占用太多资源大软件直接无法加载
  2. 不安全: 每次加载应用,内存地址固定了,很容易被人直接通过内存地址篡改数据
    • 早期本地外挂,就是通过内存地址篡改数据
      (如:游戏中捡到500金币时,搜索所有内存地址,有记录500金币的,就是金币计数地址。直接通过这个地址修改金额)
  • 后来,经过研究,发现每个应用内存中使用的部分,仅占该应用小部分(活跃部分)。于是聪明的前辈们,将内存均匀分割很多页
  • 应用不用一启动就全部加载进去,而是每个启动的应用,都分配一个虚拟内存大小,里面也跟物理内存一样切割成一样大小的的内存页

现在就变成了这样:


image.png

补充:

  1. 内存管理单元
  • MMU:(Memory Management Unit) 内存管理单元,有时称作PMMUpaged memory management unit分页内存管理单元
  • 负责处理中央处理器(CPU)的内存访问请求计算机硬件
  1. 内存页大小
  • LinuxMacOS系统:每页4K
  • iOS系统: 每页16K
  1. 页表
    应用的虚拟内存物理内存地址映射关系

  2. 五大分区

  • 栈区堆区常量区代码区全局静态区都是指的虚拟内存区域。都依赖于进程(启动的应用)
    比如应用A,有个地址0x00000666, 如果应用A关闭了,应用B也有0x00000666。他们指向的完全不一样。
    应用访问的都是虚拟内存空间
  1. 虚拟空间大小
  • 每个应用(进程)默认可以分配4G大小。但它实际只是一张页表记录映射关系就可以。

  • 页表存放在操作系统内存区域

  • 应用用到的,都是物理内存实际占有物理内存大小应用运行时决定的。

    比如你1T空间百度网盘。你本地只是个地址链接而已,并不会占用电脑空间。你用了200M,它就在数据库给你200M空间资源,然后将这个资源地址和你的网盘地址 关联起来。剩余800M需要的时候,它再分配空间资源给你。
    你的所有资料,都是在它的数据库中。而你的网盘,只是记录了每个资料资料存放地址映射关系而已。

4.二进制重排原理

本节我们研究的就是APP启动优化,所以这里也是一个优化点

  • iOS每一页16K大小,但是16K中,可能真正在启动时刻需要用到的,可能不到1K启动需要访问到这1K数据,不得不整页加载
  • 我们的二进制重排,就是为了启动用到的这些数据整合到一起,然后进行内存分页。这样启动用到的数据都在前几页中了。启动时只需加载几页数据就可以了。
image.png
  • 知道了优化原理,但是有几个问题:
  1. 二进制重排中的二进制是啥?
  2. 二进制数据原来是什么顺序?
  3. 二进制如何重排?

4.1 二进制重排中的二进制

二进制: 只有01的两个数的数制。是机器识别进制

4.2 二进制数据顺序

#import "ViewController.h"

@interface ViewController ()
@end

@implementation ViewController

void test1() {
    printf("1");
}

void test2() {
    printf("2");
}

- (void)viewDidLoad {
    [super viewDidLoad];

    printf("viewDidLoad");
    test1();
}

+(void)load {
    printf("load");
    test2();
}

@end
image.png image.png

总结

我们要做的,就是把启动用到函数排列在一起

5.PageFault检测

大家可以用自己项目检测

6.体验二进制重排

二进制重排,关键是order文件

  • 前面讲objc源码时,会在工程中看到order文件:

    image.png
  • 打开.order文件,可以看到内部都是排序好函数符号

    image.png
  • 这是因为苹果自己的都进行了二进制重排

  1. Demo项目根目录创建一个.order文件

    image.png
  2. ht.order文件中手动顺序写入函数(还写了个不存在的hello函数)

    image.png
  1. Build Settings中搜索order file,加入./ht.order

    image.png
  2. Command + B编译后,再次去查看link map文件

    image.png
  • 发现order文件不存在的函数(hello),编译器直接跳过
  • 其他函数符号,完全按照我们order顺序排列。
  • order没有的函数,按照默认顺序接在order函数后面

此时此刻,还有谁!!宝剑在手,天下我有 哈哈哈 😃

目标: 拿到启动完成后的某个时刻之前的所有被调用函数劳烦你们自己排队入我的order文件中。

下一节 Clang插桩 教你宝剑口诀(函数~ 函数~ ,快到我的碗里来 😂 )

上一篇 下一篇

猜你喜欢

热点阅读