yuhanle / blogbag

我会不断更新这个仓库中的文章
https://latehorse.github.io/
14 stars 1 forks source link

iOS 集成 Cronet 的可能方案 #17

Open yuhanle opened 3 years ago

yuhanle commented 3 years ago

image

准备 Cronet

编译好的 Cronet 可在 https://console.cloud.google.com/storage/browser/chromium-cronet/ios?pli=1 找到。因为不同版本太多,我们可按照前缀过滤,如 77,https://console.cloud.google.com/storage/browser/chromium-cronet/ios?pli=1&prefix=77

由于数据存在 Google Cloud 上,我们不能直接下载,但利用 gsutil(其安装文档见 https://cloud.google.com/storage/docs/gsutil_install)工具,我们可以将预先编译好的 Cronet 下载下来。

我们选择一个版本,如 77.0.3865.120(最新版本已到 79,这里按照 Chrome 浏览器当前的版本),以此名建立文件夹,然后分别下载其模拟器版和真机版:

gsutil cp -r gs://chromium-cronet/ios/77.0.3865.120/Release-iphonesimulator/ ./
gsutil cp -r gs://chromium-cronet/ios/77.0.3865.120/Release-iphoneos/ ./

注意:目前 Cronet 不支持 Bitcode,根据搜索到的一些信息,它有实验性的支持(参考 https://bugs.chromium.org/p/chromium/issues/detail?id=723816),但会极大的增加二进制的体积

静态库方案

下载完成后,我们利用 lipo 命令,将其中的静态库二进制合并在一起(这样才能支持模拟器开发和真机打包):

xcrun lipo -create -output Cronet ./Release-iphoneos/cronet/Static/Cronet.framework/Cronet ./Release-iphonesimulator/cronet/Static/Cronet.framework/Cronet

然后,我们手工准备一个 Cronet.framework,其结构如下:

$ tree Cronet.framework/

Cronet.framework/

├── Cronet

├── Headers

│  ├── Cronet.h

│  ├── bidirectional_stream_c.h

│  ├── cronet.idl_c.h

│  ├── cronet_c.h

│  └── cronet_export.h

└── Modules

  └── module.modulemap

我们只需要创建一个 Cronet.framework 文件夹,然后将之前合并生成的 Cronet 放入其中,然后从 Release-iphoneos/ 中拷贝 Headers 和 Modules 文件夹,注意后者不在 Static 里。

然后,我们可将 Cronet.framework 放入 Moya,建立依赖。根据编译错误提示,再引用libc++.tbdlibresolv.tbdMobileCoreServices.framework 以及 SystemConfiguration.framework,以通过编译。此外,为了通过 Carthage 检查 carthage build Moya --platform ios --cache-builds --no-skip-current,需要调整 Simulator 的 Arch(有可能是预编译的二进制的 i386 支持有问题。)

注意:根据测试,Release 模式的静态库会使 app 体积增加约 20MB。业务方 app 要关闭 Bitcode。

动态库方案

其准备过程和结构都类似静态库,但需要将二进制上传到 s3。

Fastfile

fastlane_version "2.100.0"
default_platform :ios

import_from_git(url: 'git@git.llsapp.com:client-infra/ci_scripts.git',
                path: 'fastlane/Fastfile')

platform :ios do
  lane :publish do |options|
    tag = options[:tag]
    upload_binaries(frameworks: ["Cronet"], tag: tag)
    key_url_map = Actions.lane_context[SharedValues::S3_FILES_PUBLIC_URLS_MAP]
    UI.message key_url_map
    if key_url_map && key_url_map.count > 0
      update_versions(url_map: key_url_map, tag: tag)
    else
      UI.user_error! "No binary!"
    end
  end
end

注意:根据测试,Release 模式的动态库会使 app 体积增加约 10MB(拷贝完整的 Cronet.framework)。业务方 app 要关闭 Bitcode。

定制

理论上,我们可以通过裁剪协议并自行编译 Cronet 来减少体积,但目前的研究程度不够,而且因为“墙”的关系,编译过程中的一个 sync 步骤并未成功,还没有实际编译过。不过这可以是未来的一个可能的改进方向。

整合 API

在 iOS 上, Cronet 只暴露了有限的 API(现有 25 个方法,而且大部分是配置类的),而且它本质是通过单例实现的。以 start() 方法为分水岭,一些设置方法需要在其之前调用(例如设置是否开启 HTTP2,设置 User-Agent 等),一些配置方法则需要在其之后调用(例如配置 URLSessionConfiguration 或设置 HostResolverRules),其余的则可在任意时刻调用。

阅读源码可知,Cronet 在 iOS 上是通过配置 URLSessionConfiguration 的 protocolClasses,进而利用 URLProtocol 来实现拦截请求的。这样做的好处是,对于用户方,依然使用 URLSession 配套的 API 即可,也就是几乎所有的上层 API 都可以保持不变。也就是说,我们准备一个特殊的 URLSessionConfiguration 让 Cronet 配置,然后利用这个 URLSessionConfiguration 生成 URLSession 即可。

在 Moya 中,我们没有直接使用 URLSession ,而是使用了 AFNetworking,它可被看成是 URLSession 的包装,也就是其内部依然使用 URLSession。这样一来,我们只需要修改生成 AFURLSessionManager 的地方,传入特殊的 URLSessionConfiguration 即可,而 AFURLSessionManager 内部会生成对应的 URLSession。这样,其它使用 AFURLSessionManager 的地方也就无需修改。

但目前 Moya 暴露的 API 是可以生成多个实例的,并且其配置 QuicksilverURLSessionConfiguration 里的 urlSessionConfiguration 可能发生改变。理论上,我们可以通过 useHTTPDNS 的值来决定是否要使用 Cronet,如果需要使用,再准备一个 URLSessionConfiguration 单例给 Cronet 配置,然后生成 AFURLSessionManager。之后可能会对 QuicksilverURLSessionConfiguration 做一些变动(应该会有 API 变更)。

因为 Cronet 的 start() 需要特殊对待(最好只在主线程调用一次),我们可在准备 AFURLSessionManager 之前判断,只在必要的时候调用它。注意,一旦调用了它,Cronet 的一些设置方法就不能再调用了(不会再生效)。

注意,因为目前 Cronet 没有暴露 WebSocket 相关的 API,因此 WebSocket 相关的代码不会改动。

配置 Cronet

开启 HTTP2,开启 Metrics,使用 HTTP 内存缓存。

使用 HTTPDNS

Cronet 通过 API setHostResolverRulesForTesting 方法接受一个特殊格式的字符串来生成 DNS 映射规则。如果我们用 Cronet 接管 useHTTPDNS 为 true 的请求,那在实现上,我们需要在 Moya 内部的 getTargetRequest 里,在请求发起之前,将来自 HTTPDNS 的 host/IP 的映射注入 Cronet,而不需要像之前一样修改请求 URL 的 host 为 IP。

不过因为 setHostResolverRulesForTesting 的方法比较难用,我们最好维护一个 host/IP 映射字典,做一个中间层,以提供友好的 API。

目前在 Demo 中测试过 Cronet 的 SNI (IP based) 支持,应该是可用的。不过如 setHostResolverRulesForTesting 的名字所暗示,这个 API 原本的用途是测试,不知道在实际使用中会有什么坑。我尝试跟踪过其内部代码,最后也会到达 HostResolver 这一层。

对 Plugin 的影响

目前 Moya 利用插件机制来让开发者有机会在请求发起前修改请求,或者在请求结束后做进一步的数据处理。

根据目前的简单测试,集成 Cronet 不会影响目前的插件机制。

对 Thanos 的影响

因为目前 Thanos 使用了 Objective-C Runtime 相关技术去实现网络监控,而我们使用 Cronet 之后,网络代码的执行路径发生改变,可能影响 Thanos 的收集过程,需要进一步评估。

初步在 iOS 13 测试了一下,可以收集到信息(需要 Cronet.setMetricsEnabled(true),不然影响下载任务的数据收集),但可能缺少某些字段。

itdongbaojun commented 3 years ago

Cronet的bitcode问题是个头疼的问题

yuhanle commented 3 years ago

Cronet的bitcode问题是个头疼的问题

没错。接入成本还是有的,看收益吧

itdongbaojun commented 3 years ago

Cronet的bitcode问题是个头疼的问题

没错。接入成本还是有的,看收益吧

收益是明显的,我们这边基于Cronet的DNS这一整套解决方案在线上全量运行一年多了,关闭了bitcode,最近有同学在做宝体积缩减的项目,又聊到了bitcode这个问题。

heaoven commented 2 years ago

Cronet的bitcode问题是个头疼的问题

没错。接入成本还是有的,看收益吧

收益是明显的,我们这边基于Cronet的DNS这一整套解决方案在线上全量运行一年多了,关闭了bitcode,最近有同学在做宝体积缩减的项目,又聊到了bitcode这个问题。

bitcode在armv7中编译出现问题 [1897/1912] SOLINK obj/components/cron...j/components/cronet/ios/arm/Cronet.TOC FAILED: obj/components/cronet/ios/arm/Cronet obj/components/cronet/ios/arm/Cronet.TOC if [ ! -e "obj/components/cronet/ios/arm/Cronet" -o ! -e "obj/components/cronet/ios/arm/Cronet.TOC" ] || otool -l "obj/components/cronet/ios/arm/Cronet" | grep -q LC_REEXPORT_DYLIB ; then TOOL_VERSION=1560752681 ../../build/toolchain/mac/linker_driver.py clang++ -shared -Xlinker -install_name -Xlinker @rpath/Cronet.framework/Cronet -Xlinker -objc_abi_version -Xlinker 2 -arch armv7 -Werror -Wl,-dead_strip -isysroot /Applications/Xcode11.7.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.7.sdk -stdlib=libc++ -miphoneos-version-min=9.0 -fembed-bitcode -Wl,-ObjC -o "obj/components/cronet/ios/arm/Cronet" -Wl,-filelist,"obj/components/cronet/ios/arm/Cronet.rsp" -framework UIKit -framework CoreFoundation -framework CoreGraphics -framework CoreText -framework Foundation -framework Security -framework CFNetwork -framework MobileCoreServices -framework SystemConfiguration -lresolv && { otool -l "obj/components/cronet/ios/arm/Cronet" | grep LC_ID_DYLIB -A 5; nm -gP "obj/components/cronet/ios/arm/Cronet" | cut -f1-2 -d' ' | grep -v U$$; true; } > "obj/components/cronet/ios/arm/Cronet.TOC"; else TOOL_VERSION=1560752681 ../../build/toolchain/mac/linker_driver.py clang++ -shared -Xlinker -install_name -Xlinker @rpath/Cronet.framework/Cronet -Xlinker -objc_abi_version -Xlinker 2 -arch armv7 -Werror -Wl,-dead_strip -isysroot /Applications/Xcode11.7.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.7.sdk -stdlib=libc++ -miphoneos-version-min=9.0 -fembed-bitcode -Wl,-ObjC -o "obj/components/cronet/ios/arm/Cronet" -Wl,-filelist,"obj/components/cronet/ios/arm/Cronet.rsp" -framework UIKit -framework CoreFoundation -framework CoreGraphics -framework CoreText -framework Foundation -framework Security -framework CFNetwork -framework MobileCoreServices -framework SystemConfiguration -lresolv && { otool -l "obj/components/cronet/ios/arm/Cronet" | grep LC_ID_DYLIB -A 5; nm -gP "obj/components/cronet/ios/arm/Cronet" | cut -f1-2 -d' ' | grep -v U$$; true; } > "obj/components/cronet/ios/arm/Cronet.tmp" && if ! cmp -s "obj/components/cronet/ios/arm/Cronet.tmp" "obj/components/cronet/ios/arm/Cronet.TOC"; then mv "obj/components/cronet/ios/arm/Cronet.tmp" "obj/components/cronet/ios/arm/Cronet.TOC" ; fi; fi ld: section __bundle (address=0x00304000, size=928344742) would make the output executable exceed available address range for architecture armv7 各位有遇到过吗

brownppfeng commented 1 year ago

现在apple 都干掉 bitcode了. 现在还有包大小问题么