iOS热重载实现

前言

每次从开发Flutter开发切回到原生开发时候最不习惯的就是原生没有热重载功能。
简单地调一下字体颜色,view大小都要重新编译,既耗时又费力。

所以想了一下可不可以让原生开发也可以享受到热重载功能,在UI调试下可以做到 "即写即看"。

iOS端的热更新主要可以分成俩大块。

一种是基于JSCore。
它建立起了Objective-C 与 JavaScript通的桥梁。
代表框架有,React Native, Weex,JSPatch  等等优秀框架。

还有一种是更为小众一些的。
自己实现了一个 OC 语法的简单解释器,包含了基础的词法分析与语法分析,从而能够在运行期将 OC 代码生成抽象语法树 AST 然后进行执行。再通过 runtime 进行方法替换 方法添加等操作,进而实现了动态化的效果。
代表框架有,OCEval, OCRunner 等框架 (据我观察貌似都是独立开发者,目前暂时还无法做到商用级别。但也可以拿他们的基础框架进行魔改,大部分基础工作已经做完了)。

React Native 与 Weex 比较重,需要项目级支持。
JSPatch 是需要下发js代码进行热修复,虽然可以满足需求,但弊端也比较明显,那就是需要将OC代码强行翻译成JS语法,大段落使用时候不是很方便。
用OC语法解释器,虽然可以做到OC代码不转译, 但还是需要对框架做一些魔改才可以按照自己习惯方便使用  (自己做语法分析, 生成抽象语法树 这块还是值得研究的。理论上可以做到 下发OC代码, 可以基于 解释器+runtime 做到整个app都动态化)。

我设想的场景是只是在debug环境下,即写即看。
线上修复不想打擦边球, 以及引入一些不可靠的因素。
所以才去的方案是通过动态库来在开发阶段做到动态化。

众所周知Objective-C 这门语言天生具有动态性,可以任意的在运行时替换方法, 成员变量等等。那这样就联想到可不可以下发动态库,在运行时加载这个动态库并替换新加载进来的动态库里类对象/元类对象的一些信息。这样就可以再开发环境下做到 "即写即看"的效果。


热重载demo


实践

流程图

项目搭建

项目分俩部分。

第一部分:  macOS项目(监听热重载项目文件变化)

第二部分:  iOS项目(热重载目标项目)

第一部分 (监听文件变化项目)

首先我们需要有一个mac端程序监听指定文件夹下文件的变化,从而将保存变化后的.m文件通过运行预先编写好的 shell脚本进行 编译 成 .o 文件并打包成 一个dylib,发送给app。

shell脚本整体流程思路 :
1) 接收 要编译的 .m 文件路径 以及 .m引用的其他类的 .o文件路径 (这一步是生成动态库必要的,不然会因为LinkFileList 引用出错而无法编译出一个dylib)
2) 通过clang 先将 .m 编译成 .o 并储存起来
3) 再将重新编译后的 .o 文件编译成dylib
4) 这时候可以多加一个参数判断是向真机还是模拟器发送动态库。如果向真机发送的话需要做个签名操作,不然真机无法dlopen 这个 dylib
5) 向目标传输编译好的dylib

编译脚本

监听软件部分

这部分可以通过 FSEventStream 来实现监听文件变化。
shell脚本方面可以使用 NSTask 来执行脚本名。
* 我是在这里生成了LinkFileList。是从变化文件字符串中提取出 引入文件并遍历拼接成一个有效 依赖链接。不知道有没有更好的方法生成该文件需要依赖的 LinkFileList。

最终成型后的mac端监听软件

mac端监听程序

这里勾选中真机 并 键入手机ip地址将会热重载手机端app。解除勾选将会热重载模拟器。这样的话俩者都可以兼顾了。

第一个部分这样就差不多可以了。

第二部分 (热重载目标项目)

首先项目里面要搭建一个http服务,我们这里选择的是用 GCDWebServer。

GCDWebServer 是一个基于GCD 可以用于macOS & iOS 上的一个轻量的HTTP server,该库实现了基于web的文件上传等功能。

然后要开始编写解析mac上编译并连接好的dylib了。

① 通过 dlopen 打开传进来的 dylib
    dlopen(dylibPatch,RTLD_NOW)
② 获取内存中所有镜像
    int32_t images= _dyld_image_count();    // 所有内存中镜像
③ 循环镜像获取刚刚注入的动态库镜像。(这个步骤是必须的,不然会踩坑)
        for(uint32_ti =0; i < images; i++) {
            pszModName =_dyld_get_image_name(i);
            if(!strcmp(pszModName, dylibPatch)){  // 判断镜像地址是否与传进来的dylib地址一致           
                base  = (void*)_dyld_get_image_header(i);
                slide =_dyld_get_image_vmaddr_slide(i);
            }
        }
④ 获取注入动态库结构体地址
        Dl_infoinfo;
        dladdr((mach_header_t*)base, &info);
        machHeader1 = (structmach_header_64*)info.dli_fbase;
⑤ mach-O 文件里面的 class列表信息存在Data断。获取data段 classList 信息 (这个节列出了所有的class,包括元类对象)
        uint64_tsize =0;
        char*referencesSection =getsectdatafromheader_64(machHeader1,            "__DATA","__objc_classlist", &size );

用烂苹果解析dylib后的DATA段 Class_list信息

⑥ 获取注入dylib 类对象
    Class class = classReferences[i];
⑦ 对象替换
    // 获取要替换类名称
    constchar*className = class_getName(newClass);
    // 获取当前内存中类对象
    Class oldClass = objc_getClass(className);
    // 判断是否是注入进来的类对象
    if  ( newClass != oldClass ) {
        开始进行方法替换
    }
⑧ 删除掉传上来的动态库
    [[NSFileManager defaultManager] removeItemAtPath:patch error:nil];
⑨ 发送广播
    dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:@"DWHotReload" object:nil];
    });
   * 这一步可以优化成通过消息转发来调用。demo图省事直接发了个广播

待解决问题

上述步骤就是个大概的一个解析流程。
如果想要做到真正项目应用级的话,需要润色点是shell脚本, 引用三方库时候链接编译问题。
支持swift。

期待最后的落地场景

最终期望落地是,测试的同学在debug页面开启接收dylib开关,开发同学只要本地修复问题后直接下发dylib,直接在测试同学设备上修正好。 因为是直接编写的OC/swift 代码所以,不会像是使用jspatch 需要最终回归一下正式代码。这条路很漫长。。。 慢慢走。

推荐框架

强烈推荐injection的框架。oc项目无侵入的可以直接热重载。但swift 项目在有些bridge时候会发生异常。

误区

网上很多文章都再说dlopen只能在模拟器上使用,其实并不正确的。
真机上无法dlopen加载dylib,大体是犯了俩个错误。
一,编译时target依赖的是x86架构
二,打包成dylib后没有做有效签名
如果这俩点都做了其实dylib可以在真机上dlopen加载成功 (逆向开发后的插件就是dylib,它能注入到真机二进制文件里咱们自己的也必然可以)

* 本demo也参考了部分injection思路,并使用了该工程里的方法互换方法。

demo地址

https://github.com/378804441/DWHotReload
demo  分为三个部分
① 本地监听文件改变工程 (FSEventStreamDemo)
② 编译shell脚本 (shell)
③ Demo工程 (DWDebugHR)
注意: 工程直接跑不起来,要想跑起来的话可以按照错误提示自己配置下路径。开发阶段我都写得我本地绝对路径 哈哈哈哈

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容