iOS技术选型调研
一、背景
iOS开发的技术方案中既有苹果自家的Swift和Swift UI,也有第三方跨端方案React Native、Flutter和Weex等。如何做好技术选型?如果能参考市场上其他家的选型决策,是不是能辅助决策?那么如何能分析其他家选型方案呢,我们以iOS免费排行榜Top 100 中国区为例。
二、方案
2.1 认识Mach-O
Mach-O全称是Mach Object File Format,是iOS/macOS上的可执行文件格式,包括可执行文件、动态库、静态库和目标文件等。如图所示Mach-O文件主要有三部分组成,分别是Header,Load Commands和Segments数据。一个Mach-O文件可以包含一个或者多个CPU架构的数据。详细资料参考osx-abi-macho-file-format-reference
2.2 可执行文件类
在__DATA segment的__objc_classlist section下存储着可执行文件包含的所有Objective C类信息,格式如下:
struct objc_class {
uintptr_t isa;
uintptr_t superclass;
uintptr_t cache;
uintptr_t vtable;
uintptr_t bits;
};
可以通过读取该section的数据,获取到该二进制的所有类,注意不包含动态库里的类型。 读取全部Objective C类信息代码如下:
void Arch::handleClasslist() {
for (auto iter : allSections) {
if (strncmp(iter->sectname, "__objc_classlist", std::size(iter->sectname)) == 0) {
auto count = iter->size / sizeof(uintptr_t);
auto beg = OFFSET(iter->offset);
std::vector<uintptr_t> classRefs;
for (auto i = 0; i < count; ++i) {
uintptr_t classRef = *(uintptr_t *)(beg + i * sizeof(uintptr_t));
classRefs.push_back(classRef);
objc_class *oclass = (objc_class *)POINTER(classRef);
if (oclass->isa) {
classRefs.push_back(oclass->isa);
}
}
for (auto classRef : classRefs) {
objc_class *oclass = (objc_class *)POINTER(classRef);
class_ro_t *ro = (class_ro_t *)POINTER(oclass->bits & FAST_DATA_MASK);
auto isMeta = ro->flags & RO_META;
if (!isMeta) {
(void)ObjCClassForName(POINTER(ro->name));
}
}
break;
}
}
}
2.3 动态链接库
类型为LC_LOAD_DYLIB的Load Command存储的是动态链接库信息,定义如下:
struct dylib {
union lc_str name; /* library's path name */
uint32_t timestamp; /* library's build time stamp */
uint32_t current_version; /* library's current version number */
uint32_t compatibility_version; /* library's compatibility vers number*/
};
使用MachOView截图: name字段的值@rpath/Flutter.framework/Flutter,代表和二进制文件同级的Frameworks文件夹下的Flutter.framework。 对于Swift应用,Swift运行时库名称是@rpath/libswiftCore.dylib。 读取加载的动态库代码如下:
std::vector<std::string> Arch::handleDyld() {
std::vector<std::string> result;
size_t offset = sizeof(mach_header);
for (auto loadcommand : allLoadCommdands) {
if (loadcommand->cmd == LC_LOAD_DYLIB) {
auto dylib_command = (struct dylib_command *)loadcommand;
auto name = OFFSET(offset) + dylib_command->dylib.name.offset;
result.push_back(name);
}
offset += loadcommand->cmdsize;
}
return result;
}
2.4 实现
2.4.1 方案
有了上面的理论知识,我们来分析一个App是否采用Swift/React Native/Flutter/Weex。 1.Swift 加载的动态库里是否包含libswiftCore.dylib。 2.React Native 否包含RCTView类。 3.Flutter 加载的动态库里是否包含Flutter.framework。 4.Weex 否包含WXSDKInstance类。 对二进制和二进制包含的动态库,以及动态库引用的动态库,分别执行1-4步检查。 代码如下:
void checkFeatures(std::string &path, bool checkAll, Info &info) {
Bin bin;
bin.read(path);
auto arch = bin.arch();
if (!arch) {
std::cout << "Error: " << path << std::endl;
return;
}
bool swift = false, rn = false, weex = false, flutter = false, encrypt = false, xamarin = false;
auto classlist = arch->ObjCClasses();
for (auto &name : classlist) {
if (name.compare("RCTView") == 0) {
rn = true;
} else if (name.compare("WXSDKInstance") == 0) {
weex = true;
} else if (name.compare(0, 7, "Xamarin") == 0) {
xamarin = true;
}/* else if (name.compare("FlutterAppDelegate") == 0) {
flutter = true;
}*/
}
size_t count = 0;
if (!rn && !weex) {
for (auto &name : classlist) {
for (auto i = 0; i < name.length(); ++i) {
auto ch = name[i];
if (!isalnum(ch) && ch != '_' && ch != '$') {
if (count++ > classlist.size() / 2) {
encrypt = true;
}
break;
}
}
}
}
std::string frameworkDir;
if (auto i = path.find(".app/Frameworks/"); i != std::string::npos) {
frameworkDir = path.substr(0, i + 15);
} else {
frameworkDir = path.substr(0, path.find_last_of('/') + 1) + "Frameworks";
}
std::vector<std::string> frameworks;
for (auto dylib : arch->handleDyld()) {
std::vector<std::string> components;
split(dylib, components, '/');
if (strcasestr(components.back().c_str(), "flutter") != nullptr) {
flutter = true;
} else if (components.back().find("libswiftCore") == 0) {
swift = true;
}
if (components.front() == "@rpath" && components.back().rfind(".dylib") == std::string::npos) {
auto copy = frameworkDir + dylib.substr(dylib.find_first_of('/'));
frameworks.push_back(copy);
}
}
info.swift |= swift;
info.flutter |= flutter;
info.rn |= rn;
info.weex |= weex;
info.encrypt |= encrypt;
info.xamarin |= xamarin;
for (auto &framework : frameworks) {
checkFeatures(framework, false, info);
}
}
2.4.2 实施
从App Store下载的IPA里的可执行文件是经过加密的,通过以下命令可以查看Mach-O文件是否是加密的:
otool -l path_to_macho | grep -A 4 LC_ENCRYPTION_INFO
需要对先对可执行文件解密才能对Mach-O进行以上处理。这一解密过程也被叫做脱壳。脱壳需要在越狱的设备上进行,可以使用checkra或者unc0ver越狱,推荐使用frida-ios-dump脱壳。对于脱壳后的IPA,使用zip命令解压,遍历文件找到可执行文件。 判断文件是否是可执行文件代码如下:
bool Bin::isMachO(std::string &path) {
int fd = open(path.c_str(), O_RDONLY, S_IRUSR | S_IWUSR);
if (fd < 0) {
return false;
}
uint32_t magic = 0;
if (::read(fd, &magic, sizeof(magic)) != sizeof(magic)) {
close(fd);
return false;
}
close(fd);
return magic == FAT_MAGIC
|| magic == FAT_CIGAM
|| magic == FAT_MAGIC_64
|| magic == FAT_CIGAM_64
|| magic == MH_MAGIC_64
|| magic == MH_CIGAM_64;
}
三、结论
3.1 iOS中国区免费排行榜TOP 100 Swift/React Native/Flutter/Weex应用现状
通过对对Top 100应用做分析,得出如下数据:
时间 | Swift | React Native | Flutter | Weex |
---|---|---|---|---|
2020.03.08 | 26 | 23 | 13 | 14 |
2021.03.18 | 55 | 28 | 30 | 13 |
变化 | +29 | +5 | +17 | -1 |
从表中对比可以看到,Swift的普及有了较大的提升,Flutter发展比较迅速。 典型应用变化情况
App | Swift | React Native | Flutter | Weex |
---|---|---|---|---|
微信 | 新增 | 新增 | ||
支付宝 | 新增 | 删除 | ||
手机淘宝 | 保持 | 保持 |
PS:经了解支付宝动态化方案从React Native迁移到自研小程序方案。