iOS-SDK开发经验分享

作者: Yochi | 来源:发表于2017-07-12 16:06 被阅读574次
    本文目标:掌握封装SDK的全部技巧及跳坑指南

    本文目标:掌握封装及开发SDK的全部技巧

    文章比较长,建议分模块阅读

    内容提要:不同场景下如何封装及开发SDK

    • .a静态库创建
    • 直接创建Framework库
    • 在已有工程中创建
    • 创建Framework工程进行封装
    • 创建Bundle资源库文件
    • 含界面SDK如何进行依赖开发
    • 使用脚本创建Framework库,解决合并的烦恼
    • Swift 如何创建Framework库

    知识准备

    • 终端命令
    真机和模拟器上的库文件合并 
    Framework库合并的是Framework内包含的二进制文件,合并后替换库中的文件,没有.a后缀
    lipo -create xxx.a(真机) xxx.a(模拟器) -output 新名字.a
    查看SDK支持的架构
    lipo -info XXX.a  输出: i386 armv7 x86_64 arm64
    

    arm7: 在最老的支持iOS7的设备上使用
    arm7s: 在iPhone5和5C上使用
    arm64: 运行于iPhone5S的64位 ARM 处理器 上
    i386: 32位模拟器上使用
    x86_64: 64为模拟器上使用
    注意: 高位兼容地位(32位兼容16位),arm7版本可以在arm7s上运行
    需要在对应架构设备上运行,才能生成对应架构的包

    • category的处理

    category是项目开发中经常用到的,把category打包成静态库是没有问题的,但是在使用这个静态库时,
    调用category中的方法时会发生找不到该方法的运行时错误(selector not recognized),
    解决的办法是在使用静态库的工程中配置other linker flags的值为 -ObjC -all_load

    • 对图片资源和UI界面xib或nib文件的处理

    .a和.framework两种静态库,通常都是把需要用的到图片或者xib文件存放在一个bundle文件中,而该bundle文件的名字和.a或.framework的名字相同。
    .a文件中无法存放图片或xib文件,很容易理解,但是.framework从本质上说也是一个bundle文件,为什么不把图片或者xib文件直接放在.framework中而单独再创建个bundle文件呢?
    那是因为iOS系统不会去扫描.framework下的图片等资源文件,也不会在项目中显示,也就是说即使放在 .framework目录下,系统根本就不会去扫描,因此也无法发现使用

    • Debug和Release

    Debug和Release,在我看来主要是针对其面向的目标不同的而进行区分的。
    Debug通常称为调试版本,通过一系列编译选项的配合,编译的结果通常包含调试信息,而且不做任何优化,以为开发人员提供强大的应用程序调试能力。
    Release通常称为发布版本,是为用户使用的,一般客户不允许在发布版本上进行调试。所以不保存调试信息,同时,它往往进行了各种优化,以期达到代码最小和速度最优。为用户的使用提供便利

    开发指南

    网上找了一个动画工程,作为我们开刀的对象
    下载原始工程
    如果你有耐性,可以和我一起走完整个流程。当然每个模块都是独立的,你可以进行针对性的阅读

    一、.a静态库创建

    • 创建静态库工程 >> 删除自动创建的.m文件 >> 清空头文件里的信息 >> 导入你要封装的系统库文件
    创建静态库工程 libLoader(名字你高兴就好) 删除自动创建的.m文件,清空头文件里的信息 导入你要封装的系统库文件
    • 点击目标工程 >> Build Phases >> Editor >> add build Phases(是否公开头文件选项) >> 设置公开访问的头文件(或在Target Membership中直接设置)

      目标工程 > Build Phases > 点击左侧加号 > add build Phases(是否公开头文件选项) > 设置公开访问的头文件(或在Target Membership中直接设置)
    添加头文件选项 设置私有和暴露头文件 设置私有和暴露头文件 选择sdk支持的系统版本
    • 参数设置
      • Build Settings >> Dead Code Stripping设置为NO
      • Build Settings >> Strip Debug Symbol During Copy 全部设置为NO
      • Build Settings >> Strip Style设置为Non-Global Symbols
      • Build Settings >> Base SDK >> Latest iOS(iOS 选择最新的)
    Dead Code Stripping设置为NO Strip Debug Symbol During Copy 全部设置为NO Strip Style设置为Non-Global Symbols Base SDK >> Latest iOS(iOS 选择最新的)
    • 在设备 和 模拟器 下分别按下command + B进行编译 >> 查看Product目录 >> Show in Finder 查看编译成功的静态库
    • debug模式下运行生成 Debug-iphoneos 和 Debug-iphonesimulator两个文件夹
    • release模式下运行生成 Release-iphoneos 和 Release-iphonesimulator文件夹
    切换编译模式 分别查看.a库文件
    • 合并Debug模式下的真机和模拟器下的静态库文件
    使用终端进行合并
    cd 文件保存目录
    lipo -create 模拟器.a(路径) 真机.a(路径) -output 重命名.a
    查看架构模式
    lipo -info XXX.a  查看是否满足运行要求
    
    • 使用.a库文件
      创建文件夹libAdvanced用于保存静态库信息 >> 替换刚刚合并的.a文件 >> 添加用到的图片等资源文件 >> 导入工程验证
    静态库完成
    • 创建新工程验证
    • 如果架构报错 Build Settings >> BuildActiveArchitecture Only Debug改为NO

    二、直接创建Framework库

    <p id="jump"></p>

    Framework是资源的集合,将静态库和其头文件包含到一个结构中,让Xcode可以方便地把它纳入到你的项目中。本质也是一个bundle文件

    • 在已有工程中创建
      • 创建Framework
        点击目标工程 >> 点击下面左下角加号 >> 创建


        创建Framework
      • 参数配置
          点击目标工程 >> 选择你创建的Framework >> 点击工程设置 >> 做出如下修改
    
          Build Settings >> Dead Code Stripping >> 设置为NO
    
          Build Settings >> Strip Debug Symbol During Copy >> 全部设置为NO
    
          Build Settings >> Strip Style >> 设置为Non-Global Symbols
    
          Build Settings >> Base SDK >> Latest iOS(iOS 选择最新的)
    
          Build Settings >>  Link With Standard Libraries >> 设置为 NO
    
          Build Settings >>  Mach-O Type >> Static Library
    
          对于Mach-O Type有两种情况:(1)选择 Static Library 打出来的是静态库;(2)选择 Relocatable Object File 打出来是动态库。
    
    其他参数修改类似
    • 选择framework支持的系统版本

    • 将需要打包的文件拖入到Framework中


      将需要打包的文件拖入到Framework中
    • 设置需要公开的文件


      设置暴露头文件
    • 选择运行模式(debug 或 Release)分别在真机和模拟器下common + B 编译生成对应的Framework库

    • 合并二进制文件并替换

    cd 到保存文件目录下
    lipo -create xxx/Debug-iphoneos/LibLoaderFramework.framework/LibLoaderFramework xxx/Debug-iphonesimulator/LibLoaderFramework.framework/LibLoaderFramework -output LibLoaderFramework
    lipo -info LibLoaderFramework
    输出
    Architectures in the fat file: LibLoaderFramework are: i386 arm64 
    然后替换二进制文件
    
    image.png
    • 删除Framework


      image.png
    • 如果没有用到info.plist文件可以删除,避免在工程中发生冲突
    • 验证
      导入完整Framework到工程,移除打包前的代码,对库进行验证
    • 建立Framework工程进行创建
      • 使用xcode直接创建Framework工程

      • 把需要编译的文件导入到工程中

      • 设置需要公开的头文件


        image.png
      • 选择Framework支持的系统版本

      • 配置参数

     参数配置基本都一样
    点击目标工程 >> 选择你创建的Framework >> 点击工程设置 >> 做出如下修改
      Build Settings >> Dead Code Stripping >> 设置为NO
      Build Settings >> Strip Debug Symbol During Copy >> 全部设置为NO
      Build Settings >> Strip Style >> 设置为Non-Global Symbols
      Build Settings >> Base SDK >> Latest iOS(iOS 选择最新的)
      Build Settings >>  Link With Standard Libraries >> 设置为 NO
      Build Settings >>  Mach-O Type >> Static Library
      对于Mach-O Type有两种情况:(1)选择 Static Library 打出来的是静态库;(2)选择 Relocatable Object File 打出来是动态库。
    
    -  选择Debug(或Release)模式分别在模拟器和真机上 command + B 编译
    
    - 合并真机和模拟器下编译的二进制文件
    
    cd到你想要保存合并后文件的目录下
    lipo -create xxx.framework/xxx ooo.framework/ooo -output ooo。
    查看文件支持的架构
    lipo -info LibLoaderFramework
    将合并成功的二进制文件替换为framework中的二进制文件,如果没有用到info.plist文件,可以删除,避免在工程中发生冲突
    
    • 创建新的工程,导入Framework进行验证
    如果工程无法联想出Framework头文件,导入路径形式如下:
    #import <LibLoaderFramework/PublicHeader.h>
    

    三、创建Bundle资源库文件

    • 创建Bundle工程


      创建Bundle工程
    • 配置几个编译设置

    因为你正在创建一个在iOS上使用的bundle,这与默认的OS X不同。
     Build Settings >> Base SDK >> Latest iOS (iOS 10.2选择最新)
     Build Settings >> Product Name >> ${TARGET_NAME}替换为你的工程名XXXX(直接写工程名就好)
    ################注意事项######################
    默认情况下,有两种resolutions(分辨率)的图片可以产生一些有趣的现象。
    例如,当你导入一个retina@2x版本的图片时,普通版的和Retina版的将会合并成一个多resolution的TIFF(标签图像文件格式,Tagged Image File Format)。
    Build Settings > COMBINE_HIDPI_IMAGES设置为NO
    
    • 如何添加资源文件

      • 直接拖入
      • 选择图片或其他资源文件 > Target Membership > 选择bundle目标
    • 编译工程并查看


      编译工程并查看
    • Bundle文件使用时需要真实路径

        NSBundle *bundle = [NSBundle bundleWithURL:[[NSBundle mainBundle] URLForResource:@"LoaderBundle" withExtension:@"bundle"]];
        NSString *resourceStr = [bundle pathForResource:@"IMG_0017" ofType:@"JPG"];
        我们可以创建NSBundle分类避免重复书写    
    
    • 创建工程验证

    四、含界面SDK如何进行依赖开发

    在无法看到真实效果的情况下为iOS开发一个UI控件库是极其困难的,所以我们需要掌握依赖开发的知识

    • 创建Framework工程
    • 参数设置:参考如上第二章节
    • 创建验证工程
    • 关闭Framework工程
    • 添加Framework工程的xxxx.xcodeproj到验证工程并连接到静态库如图操作:若未找到库,对库进行编译
      添加Framework工程的xxxx.xcodeproj到验证工程并连接到静态库
    • 导入库的公开头文件,对验证工程进行编译
    如果工程无法联想出Framework头文件,导入路径形式如下:
    #import <LibLoaderFramework/PublicHeader.h>
    

    像这样使用嵌套工程的好处是你可以对库本身做出修改,而不用离开示例工程,即使你同时改变两个地方的代码也一样。每次你编译工程,你都要检查是否将头文件的public/project关系设置正确。如果实例工程中缺失了任何需要的头文件,它都不能被编译。

    五、使用脚本创建Framework库

    • 创建.a的静态库工程
      (创建方式与参数配置参照第一节不再赘述)
    • 使用脚本创建Framework目录结构,此时不包含二进制文件
    • 添加 New Run Script Phases
    • 双击面板标题栏Run Script,重命名为Build Framework。
      • 这个面板允许你在构建时运行一个Bash脚本
      • 你希望让脚本在build的过程中何时执行,就把这个面板拖动到列表中相对应的那一位置。
      • 对于该framework工程来说,脚本最后执行,因此你可以让它保留在默认的位置即可。


        使用脚本创建Framework目录结构
    #set –e确保脚本的任何地方执行失败,则整个脚本都执行失败。
    set -e
    
    #导出framework路径
    export FRAMEWORK_LOCN="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework"
    
    # 创建当前版本真实头文件夹
    mkdir -p "${FRAMEWORK_LOCN}/Versions/A/Headers"
    
    # 创建引用路径
    /bin/ln -sfh A "${FRAMEWORK_LOCN}/Versions/Current"
    
    /bin/ln -sfh Versions/Current/Headers "${FRAMEWORK_LOCN}/Headers"
    
    /bin/ln -sfh "Versions/Current/${PRODUCT_NAME}" \
    "${FRAMEWORK_LOCN}/${PRODUCT_NAME}"
    
    # 拷贝公共头文件到framework中
    /bin/cp -a "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/" \ 
    "${FRAMEWORK_LOCN}/Versions/A/Headers"
    
    #######################简化目录也可用使用如下脚本#######################
    
    #set –e确保脚本的任何地方执行失败,则整个脚本都执行失败。
    set -e
    
    #导出的文件路径
    export FRAMEWORK_LOCN="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework"
    
    # 创建真是文件路径
    mkdir -p "${FRAMEWORK_LOCN}/Headers"
    
    # 拷贝公共头文件到framework中
    /bin/cp -a "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/" \
    "${FRAMEWORK_LOCN}/Headers"
    
    这个脚本做了如下三个操作:
    1.创建了libLoader.framework/Versions/A/Headers目录
    2.创建了一个framework所需要的三个连接符号([symbolic links](http://en.wikipedia.org/wiki/Symbolic_link))
    
    当前版本文件夹
    Versions/Current => A
    头文件夹
    Headers => Versions/Current/Headers
    二进制文件
    libLoader => Versions/Current/libLoader
    
    3.将公共头文件从你之前定义的公共头文件路径拷贝到Versions/A/Headers目录下,-a参数确保修饰次数作为拷贝的一部分不会改变,防止不必要的重新编译。
    
    构建成功后的Framework目录
    • 多架构(Multi-Architecture)编译
      解决Framework或.a库合并的烦恼,此处用的是.a工程,使用Framework工程类似
    • iOS app需要在许多不同的CPU架构下运行:
     arm7: 在最老的支持iOS7的设备上使用
     arm7s: 在iPhone5和5C上使用 
     arm64: 运行于iPhone5S的64位 ARM 处理器 上
     i386: 32位模拟器上使用
     x86_64: 64为模拟器上使用
    

    每个CPU架构都需要不同的二进制数据,当你编译一个应用时,无论你目前正在使用那种架构,Xcode都会正确地依照对应的架构编译。例如,如果你想跑在虚拟机上,Xcode只会编译i386版本(或者是64位机的x86_64版本)。
    这意味着编译会尽可能快地进行,当你归档一款app或者构建app的发布版本(release mode)时,Xcode会构建上述三个用于真机的ARM架构。因此这样app就可以跑在所有设备上了。不过,其他的编译架构又如何呢?让我们一起往下走

    • 创建 Aggregate 集合
      点击目标工程 > 添加新目标 > Cross-Platform > Aggregate > next > 命名为Framework


      命名为Framework

    为什么使用集合(Aggregate)目标来创建一个framework呢?为什么这么不直接?因为OS X对库的支持更好一些,事实上,Xcode直接为每一个OS X工程提供一个Cocoa Framework编译目标。基于此,你将使用集合编译目标,作为Bash脚本的连接串来创建神奇的framework目录结构。
    你是不是开始觉得这个方法有些懵逼了?

    • 添加依赖库
      为了确保每当这个新的framework目标被创建时,静态链接库都会被编译,你需要往静态库目标中添加依赖(Dependency)。在库工程中选择Framework目标,在Build Phases中添加一个依赖。展开Target Dependencies面板,添加


      添加依赖库
    • 添加多平台编译脚本
      这个目标的主要编译部分是多平台编译,你将使用一个脚本来做到这一点。和你之前做的一样
      选择Framework目标 >> Build Phases >> 左侧 “+” 按钮 >> New Run Script Phases >> 命名MultiPlatform Build
      ![MultiPlatform Build


    • 写入编译framewo编译脚本

        #set –e确保脚本的任何地方执行失败,则整个脚本都执行失败。
        set -e
    
        #标示 如果已经插入脚本 退出
        if [ -n "$IYQ_MULTIPLATFORM_BUILD_IN_PROGRESS" ]; then
        exit 0
        fi
        export IYQ_MULTIPLATFORM_BUILD_IN_PROGRESS=1
    
        # 自定义变量
        IYQ_FRAMEWORK_NAME=${PROJECT_NAME}
        IYQ_INPUT_STATIC_LIB="lib${PROJECT_NAME}.a"
        IYQ_FRAMEWORK_LOCATION="${BUILT_PRODUCTS_DIR}/${IYQ_FRAMEWORK_NAME}.framework"
    
        #构建静态库 传参 "${1}"
        function build_static_library {
        # 重新构建库
            xcrun xcodebuild -project "${PROJECT_FILE_PATH}" \
            -target "${TARGET_NAME}" \
            -configuration "${CONFIGURATION}" \
            -sdk "${1}" \
            ONLY_ACTIVE_ARCH=NO \
            BUILD_DIR="${BUILD_DIR}" \
            OBJROOT="${OBJROOT}" \
            BUILD_ROOT="${BUILD_ROOT}" \
            SYMROOT="${SYMROOT}" $ACTION
        }
    
        #合并
        function make_fat_library {
    
            xcrun lipo -create "${1}" "${2}" -output "${3}"
        }
    
        # 1 正则判断 真机还是模拟器 (iphoneos/iphonesimulator)
        if [[ "$SDK_NAME" =~ ([A-Za-z]+) ]]; then
        IYQ_SDK_PLATFORM=${BASH_REMATCH[1]}
        else
        echo "Could not find platform name from SDK_NAME: $SDK_NAME"
        exit 1
        fi
    
        # 2 SDK版本
        if [[ "$SDK_NAME" =~ ([0-9]+.*$) ]]; then
        IYQ_SDK_VERSION=${BASH_REMATCH[1]}
        else
        echo "Could not find sdk version from SDK_NAME: $SDK_NAME"
        exit 1
        fi
    
        # 3 其他平台判断 如果 则 否则
        if [ "$IYQ_SDK_PLATFORM" == "iphoneos" ]; then
        IYQ_OTHER_PLATFORM=iphonesimulator
        else
        IYQ_OTHER_PLATFORM=iphoneos
        fi
    
        # 4 其他平台路径
        if [[ "$BUILT_PRODUCTS_DIR" =~ (.*)$IYQ_SDK_PLATFORM$ ]]; then
        IYQ_OTHER_BUILT_PRODUCTS_DIR="${BASH_REMATCH[1]}${IYQ_OTHER_PLATFORM}"
        else
        echo "Could not find other platform build directory."
        exit 1
        fi
    
        #调用上面构建函数 如果当前运行的是真机则构建模拟器
        build_static_library "${IYQ_OTHER_PLATFORM}${IYQ_SDK_VERSION}"
    
        # 如果你现在正在为模拟器编译,那么Xcode会默认只在该系统对应的结构下编译,例如i386 或 x86_64。为了在这两个结构下都进行编译,这里调用了build_static_library,基于iphonesimulator SDK重新编译,确保这两个结构都进行了编译。
        if [ "$RW_SDK_PLATFORM" == "iphonesimulator" ]; then
        build_static_library "${SDK_NAME}"
        fi
    
        # 合并库
        make_fat_library "${BUILT_PRODUCTS_DIR}/${IYQ_INPUT_STATIC_LIB}" \
        "${IYQ_OTHER_BUILT_PRODUCTS_DIR}/${IYQ_INPUT_STATIC_LIB}" \
        "${IYQ_FRAMEWORK_LOCATION}/${IYQ_FRAMEWORK_NAME}"
    
        # 确保文件存在 相当于-dpR,保持文件的连接(d),保持原文件的属性(p)并作递归处理(R)
        cp -a "${IYQ_FRAMEWORK_LOCATION}/${IYQ_FRAMEWORK_NAME}" \
        "${IYQ_OTHER_BUILT_PRODUCTS_DIR}/${IYQ_FRAMEWORK_NAME}.framework/${IYQ_FRAMEWORK_NAME}"
    
        # 拷贝到指定目录下
        ditto "${IYQ_FRAMEWORK_LOCATION}" "${SRCROOT}/BuildFramework/${IYQ_FRAMEWORK_NAME}.framework"
    
    • 在工程目录下的BuildFramework文件下查看,并导入工程验证
      #import <libLoader/LoaderProgressView.h>

      BuildFramework在工程目标下
    • SDK存在图片,xib等资源文件的情况

    • 添加bundle目标工程

    • bundle创建详细操作参考第三节内容

    • bundle目标工程进行编译

    • 添加bundle资源库依赖


      添加bundle资源库依赖
    • 如果想把你的编译包copy到指定位置,在脚本后面加入如下代码

      # 拷贝bundle到指定目录下
      ditto "${BUILT_PRODUCTS_DIR}/${IYQ_FRAMEWORK_NAME}.bundle" \
      "${SRCROOT}/BuildFramework/${IYQ_FRAMEWORK_NAME}.bundle"
    

    六、swift 工程如何创建Framework库

    • swift支持静态库吗?

    • iOS 8已经开放了动态库的权限。

    • Swift 不在支持静态库,这也就间接放弃了 iOS 7。

    • 项目中如果使用了自制的动态库,能否上传到AppStore?

    • 创建的动态库需要在 General >> Embedded Binaries 中添加才能使用

    • 苹果把这种 Framework 称为 Embedded Framework。

    • 我们创建的这个动态库其实也不能给其他程序使用,只能是在我们的 App Extension 和 APP 之间共用。所有这种情况对 AppStore 上架没有影响,可以正常发布。

    • 创建swift语言的Framework项目

    • 对外公开的 方法和类 需要添加 public 前缀

      对外公开的方法需要添加 `public` 前缀
      注意:如果还允许 **override **和继承的话,可以使用 **open **关键字。(关于访问控制的详细说明,可以参考我之前的这篇文章:Swift - 访问控制(fileprivate,private,internal,public,open)
    • 分别在真机和模拟器上编译
      为客户提供使用的正式包请选择release模式下编译

    • 合并Framework二进制文件

    终端命令
    cd 到Products目录
    真机和模拟器上的库文件合并 
    lipo -create xx/aa.framework/aa oo/aa.framework/aa -output aa
    查看SDK支持的架构
    lipo -info LibLoaderFramework
    
    • 合并Mudules目录下的内容
    • 最后Framework的目录结果


      image.png
    • 导入工程进行验证
      因为是动态库,此处也需要导入,否则工程会崩溃
      image.png
      获取全部源码

    相关文章

      网友评论

      • bb418d1b7333:多谢作者的文章,收获很多。
      • 清辉_:我修改了整个项目名字后 就报这个错误了 *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Could not load NIB in bundle: 'NSBundle </var/mobile/Containers/Bundle/Application/A5B479BB-C63D-4129-8CF7-191552347CA1/gzqytestdemo.app> (loaded)' with name 'GZQYLoginViewController''
        *** First throw call stack:
        清辉_:@Yochi 还是要谢谢你
        清辉_:@Yochi 已经弄好了 是没有正确的引用bundle 要在Build Phases 的Copy Bundle Resources 中引入自己的资源包
        Yochi:@luzongyuan 查看GZQYLoginViewController这个有没有关联上
      • 祖国的栋梁:楼主写的很详细, 赞一个, 顺便求更新^_^
      • 认真看书的傻子:请问楼主,cocoapods怎样进入动态库的?
      • 胖次有毒zZ:看了你的文章,成功了
        Yochi:文章有点长,后面几节还在挤时间更新。。。

      本文标题:iOS-SDK开发经验分享

      本文链接:https://www.haomeiwen.com/subject/kqhyfxtx.html