流霞地

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迁移到自研小程序方案。

四、附录

  1. https://github.com/flexih/Snake
  2. https://github.com/aidansteele/osx-abi-macho-file-format-reference
  3. https://checkra.in/
  4. https://unc0ver.dev/
  5. https://github.com/AloneMonkey/frida-ios-dump