当前位置: 首页 > 工具软件 > Gem Framework > 使用案例 >

从已有项目中抽取 framework

李永寿
2023-12-01

一般步骤

  1. 在现有项目中,新建 target,选择: Cocoa Touch Framework。mach-o type 设置为 Static Library(否则苹果商店审核不过)。

  2. 编辑 Podfile 文件为:

    source 'https://github.com/CocoaPods/Specs.git'
    
    platform :ios, "9.0"
    
    targets = ['ParkButler-Owner','MCCSframework']
    
    targets.each do |t|
    
    target t do
    
    pod 'JSONModel'
    pod 'AFNetworking'
    ...
    
    end
    
    end
    

    这里的 targets 数组中,将所有 target 的名字加入,包括原项目的 target (也就是这里的ParkButler-Owner)和新建的 framework(也就是这里的MCCSframework)。然后用一个循环,向两个 target 中都安装同样的 pod。如果你不想在 framework 中安装所有 pod,则需要单独为 framework 导入框架:

    target 'MCCSframework' do
    pod 'IGListKit', '~> 2.0.0'
    pod 'UICollectionViewLeftAlignedLayout'
    end
    
  3. 然后关闭 Xcode,删除原项目所用的 .xcworkspace,用 pod intall 命令重新生成新的 .xcworkspace 文件,然后打开这个 .xcworkspace 文件。

    注:这一步很重要,否则 pod 不会安装到新的 framework 的 target 中。要验证 framework 中已经正确安装 pod,可以查看 framework 的 target 的 Build Phases 中是否已包含 [CP] Check Pods Manifest.lock 和 [CP] Copy Pods Resources 项。

  4. 在项目导航器中,将 .h/.m 文件从原项目拖到 framework 文件夹中,注意需要将 .h/.m 文件的 Target Membership 都修改为 framework 的 target。这样原项目对 .h/.m 的依赖就转移到了 framework 上。

    如果要公开这个类给外部使用,可以在 Target Membership 中将 .h 文件设置为 Public,默认是 Project。建议,将所有 .h 文件设置为 Public。
    同时为了使框架便于使用,可以在 <框架名>.h 头文件中将所有 .h 文件 import,形式如:

    #import "MCCSframework/IGListBaseVC.h"
    #import "MCCSframework/UIView+loadFromNib.h"
    #import "MCCSframework/NibView.h"
    
  5. 在 app 项目中全局搜索 import "刚才移动的头文件.h" (快捷键 command+option+shift+F),替换为 import <框架名称/刚才移动的头文件.h>。注意,如果是 framework 自身的类 import 这个头文件就无需替换。

    认真来说,这一步不是必须的,也可以在集成 pod 时再做。但是也可以提前到这里做,要稍微省事一些。

  6. build framework,看是否报错。同时,build 原项目,看是否报错。这是很有可能的,当你移动源文件后,framework 没有报错,但原项目却编译不了了。所以最好在每次移动文件后,都运行一下 app,看是否有什么问题。

编译 Universal Framework

  1. 选择框架的 scheme,选择模拟器,command+B,打出模拟器的 release 包,注意,打出的包位于 Release-iphonesimulator 目录。

    记住, Scheme 的 Build Configuration 要改成 release。

  2. 选择真机,command+B,打出真机的 release 包。注意打出的包位于 Release-iphoneos 目录。

  3. 在 Products 目录下创建一个 Release-universal 目录。

  4. 拷贝 lipo-create.sh 到 Products 目录下,编辑脚本内容为:

    lipo -create "./Release-iphonesimulator/MCCSframework.framework/MCCSframework"  "./Release-iphoneos/MCCSframework.framework/MCCSframework"  -output "./Release-universal/MCCSframework"
    

    将脚本中的 MCCSframework 替换成你自己的框架名。

  5. 执行 lipo-create.sh,将两个包合并成 universal 的包(放在 Release-universal 目录下)。注意,如果文件无法执行,可能需要用 chmod 777 lipo-create.sh 命令。

使用 Aggregate 创建 Universal 包

如果不想自己在终端执行脚本,可以创建 Aggregate Target:

  1. 添加 Target,选择 Cross-platform -> other 下的 Aggreagate。
  2. 打开 target 的 Build Phases,New Run Script Phase,粘贴以下脚本:
#!/bin/sh
#要build的target名(若一个工程有多个target,最好手动指定需要打包的目标,如TARGET_NAME="framework名")
TARGET_NAME="MCCSframework"
if [[ $1 ]]
then
TARGET_NAME=$1
fi
UNIVERSAL_OUTPUT_FOLDER="${SRCROOT}/Products/"

#创建输出目录,并删除之前的framework文件
mkdir -p "${UNIVERSAL_OUTPUT_FOLDER}"
rm -rf "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework"

#分别编译模拟器和真机的Framework
xcodebuild -target "${TARGET_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
xcodebuild -target "${TARGET_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphonesimulator BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build

#拷贝framework到univer目录
cp -R "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework" "${UNIVERSAL_OUTPUT_FOLDER}"

#合并framework,输出最终的framework到build目录
lipo -create -output "${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/${TARGET_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${TARGET_NAME}.framework/${TARGET_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${TARGET_NAME}.framework/${TARGET_NAME}"

#删除编译之后生成的无关的配置文件
dir_path="${UNIVERSAL_OUTPUT_FOLDER}/${TARGET_NAME}.framework/"
for file in ls $dir_path
do
if [[ ${file} =~ ".xcconfig" ]]
then
rm -f "${dir_path}/${file}"
fi
done

#判断build文件夹是否存在,存在则删除
if [ -d "${SRCROOT}/build" ]
then
rm -rf "${SRCROOT}/build"
fi

rm -rf "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator" "${BUILD_DIR}/${CONFIGURATION}-iphoneos"

#打开合并后的文件夹
open "${UNIVERSAL_OUTPUT_FOLDER}"

然后 Build。这个是最新脚本,亲测在 Xcode 10.3 下可用。

Bundle 不区分 iphonesimulator 和 iphoneos,不需要创建二合一版本。直接拷贝 Products 下的 .bundle 文件即可。

创建 pod 工程

  1. 在 git 上新建代码仓库后,用 git clone 命令 check out 到本地。

  2. 将打包出来的 .framework(任意一个 .framework,模拟器或者真机的包都可)拷贝到项目目录,然后将 .framework 中的静态库文件(即 .a 文件,虽然这个文件并没有后缀名)替换成 universal 版本。

  3. 如果框架中的资源以 bundle 形式封装,记得也拷贝 .bundle 文件到 pod 项目中来。

  4. add、commit、push。

  5. 在 pod 项目目录下运行:

    pod spec create

  6. 打开 xxxframework.podspec,修改内容。注意:

    1. s.version 必须和 tag 保持一致。
    2. 这个库除了 .framework 文件之外,没有任何源文件,不用指定 s. source_files。但是需要指定 s.verdored_frameworks,这个属性用于指定 pod 库中包含的所有非系统的 framework,我们用 xcode 编译出来的那个 framework 当然也是其中之一。
    3. 如果使用了第三方 pod,需要添加 s.dependency(可以有多条)。
  7. 打标签:

    git tag ‘<你的 tag 版本>’ -m ‘<标签信息>’

    git push --tags

删除一个 tag 使用 git tag -d '<tag 名>' 命令。

上传至 pod 索引库

  1. 切换到 pod 项目所在目录(即.podspec文件所在目录),比如 /Users/qq/YHYRepos/MCCSframework

  2. 本地验证 pod lib lint --no-clean,远程验证 pod spec lint,如果出现 passed validation. 表示校验通过。

  3. pod repo push <pod 索引库名称> <podspec文件名>,比如:

     pod repo push YHYSpecs MCCSframework.podspec
    

    这里以上传到远程私有库为例。如果你想上传至 cocospods 官方索引库,请用 pod trunk push 命令替代。

在项目中使用 framework

  1. 打开 app 项目,删除 framework 的 target 和源文件夹。

  2. 将 Scheme 中 framework 所对应的 target 删除。

  3. 编辑 Podfile ,将 framework 所对应的的 target 删除。同时将 Pods 目录下多余的两个 .xcconfig 文件删除。

  4. pod repo 命令查看<远程私有索引库 url>,比如 https://gitee.com/kmyhy/YHYSpecs.git,复制此 url。

  5. 修改 Podfile 文件,添加私有库:

      source '<远程私有索引库 Url>'
      source 'https://github.com/CocoaPods/Specs.git'
     其中,第二个 source 是官方索引库的 url,这是必须的,否则所有官方 pod 都安装不了。
    
  6. 修改 PodFile,在其中 pod ‘远程私有库名称’,比如:

     pod 'MCCSframework'
    
  7. pod install

  8. command + b,编译 app。会报一堆错误,但大部分是头文件找不到错误,需要修改引用 .h 文件的方式。因为有一部分头文件从 APP 中挪到 framework 中了。比如原来引用 #import "Utils.h" 的地方,全部都需要修改为 #import <MCCSframework/NibView.h>

    可以用 command+shift+option+F 进行全局查找替换。

  9. build & run,分别在真机和模拟器上测试,查看 app 功能是否正常。

这里还是以远程私有库为例。

加载 nib

如果 framework 中包含了 nib,那么在加载这些 nib 时,需要注意几件事情:

  1. app 打包后,如果需要加载 framework 中的资源,必须将 framework 也打包到 app 中(也可以选择打包成单独的 Bundle)。因此,需要在 app 的 Build Phases -> Copy Bundle Resouces 中,将 .framework 文件加入,否则 nib 不可用。

  2. 由于 nib 是在 xxx.framework 目录下,而不是在 xxx.app 目录下,所以在加载 nib 时需要指定 bundle,同时在 bundle 中指定 .framework 的路径:

     NSBundle *bundle = [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"MCCSframework" ofType:@"framework"]];
     UINib* nib = [UINib nibWithNibName:@"SimpleDatePicker" bundle:bundle];
    

    这样,才能正确加载 nib。

  3. 这样在分发框架时,注意将 .a 文件和 .nib 文件一起拷贝。

使用 bundle

虽然也可以直接在 framework 中包含 nib 和 png 等 UI 资源,但更好的办法是将它们单独打包到一个 bundle 进行分发。这样如果调用者仅仅是换图片或换 nib 文件只需要修改这个 bundle 即可。而且也不必因为框架中使用了 framework 中的 nib 文件或图片,将整个 .framework 文件打包进 app 中(即添加进 Copy Bundle Resources)。

  1. 创建一个 Target,选择 -> macOS -> Framework & Library -> Bundle,命名为 xxxBundle

    这种方式的 Bundle 在打包时可能会签名失败,此时需要手动签名,将签名证书从 Mac Developer 修改为 iOS Developer。

  2. 在 target 的 Build Settings 中,将 BaseSDK、Support Platforms 改为 iOS,iOS Deployment Target 修改为 10。“COMBINE_HIDPI_IMAGES” 设置为 “NO”。

  3. 将 .xib 文件拖到这个 xxxBundle 目录里面,command+B,生成一个 xxxBundle.bundle 文件(这个文件中将是编译好的 .nib 文件),将这个文件拖到 app 的 Copy Bundle Resources 中。

  4. 在代码中,这样加载 bundle 中的 .nib 文件:

     NSBundle *bundle = getBundl(@"MCCS");
    

    其中 getBundle 是一个实用函数:

     // 获取指定 bundle
     NSBundle* getBundle(NSString* bundleName){
     	return [NSBundle bundleWithPath: [[NSBundle mainBundle] 				pathForResource:bundleName ofType: @"bundle"]];
     }
    
  5. 在代码中,这样加载 bundle 中的图片:

    // 指定图片文件名、bundle 文件名
    self.imageView.image = getBundleImage(@"cart@3x", @"MCCS");
    
    

    注意,其中图片文件名中的 @3x 不能省略。

    其中 getBundleImage 是一个实用函数:

    // 获取指定 bundle 下的图片
    UIImage* getBundleImage(NSString *name,NSString* bundleName){
    return [UIImage imageWithContentsOfFile:[getBundle(bundleName) pathForResource:name ofType:@"png"]];
    }
    
    

注意,如果在框架中使用了 bundle,则上传 Pod 库时,记得在 podspec 文件中添加 s.resouces 属性。

常见错误

  1. include of non-modular header inside XXX.h

    通常在编译 framework 时并不会报此错误,而是在使用该框架的 application 中会报此错误。此错误表明,在 framework 的 XXX.h 头文件中暴露了一个非此模块的头文件,这个头文件是 umbrella 的,你不应该在自己框架的头文件中暴露它,而是要把它隐藏起来。
    怎么隐藏呢,其实就是把那个头文件的 import 语句移到 .m 文件中即可。如果在 XXX.h 中需要使用此头文件的定义,可以用 @class 语句(向前声明),比如:

    
    // 在 .h 中
    
    @class IGListSectionController;
    
    @interface IGListVC : IGListSectionController
    	
    // 在 .m 中
    
    #import <IGListKit.h>
    
    

    还有一种情况,就是你正在继承或者扩展第三方框架中类(Category),此时你只能在扩展的 .h 文件中 import 这个类,因为扩展一个类时必须明确类的定义,此时向前声明是无效的。
    解决办法就是将 Allow Non-module Include In Framework Modules 设置为 Yes(不管 framework 还是 app 都需要设置)。

  2. 不能加载框架中的图片资源

    将图片资源拷贝到框架文件夹的 .xcassets 中。

  3. could not load nib in bundle

    要加载位于框架中的 nib 文件,需要在初始化 NSBundle 时指定框架的 Bundle Id:

    NSString* const frameworkBundleID  = @"com.xxx.MCCSframework";
    
    NSBundle* bundle = [NSBundle bundleWithIdentifier:frameworkBundleID];
    
    NSString* nibName = NSStringFromClass([self class]);
    UINib* nib = [UINib nibWithNibName:nibName bundle:bundle];
    
  4. unrecognized selector sent to class

    如果框架中包含了 Category,那么当 app 中调用到 Category 中的方法时就会出现此错误。这是因为链接器在处理包含Category(类别)方法"静态库" 时,没有将Category的方法链接到 APP 中。具体可参考这里

    解决办法是,将 framework 的 Perform Single-Object Prelink 设置为 Yes。将 framework 中所有对象文件合并成单一文件,这样所有的代码都会被链接到 app 中。

  5. Undefined symbols for architecture x86_64 ‘xxx’

    将 xxx 类删除重建,将原来的代码拷回重新编译。

  6. pod lib lint 时出错:Could not find a ios simulator

    更新 rubygem,更新 cocoapods 后再执行:

     pod lib lint --verbose --use-libraries --allow-warnings
    
  7. 更新 rubygem 出错:You don’t have write permissions for the /Library/Ruby/Gems/2.3.0 directory.

    rubygem 更新至 2.7.7 时出错(Mac mojava)。解决办法:

     sudo gem update -n /usr/local/bin —system
    
  8. 更新 cocoapods 出错: ERROR: While executing gem … (Gem::FilePermissionError) You don’t have write permissions for the /usr/bin directory.

    cocoapods 更新至 1.7.5 时出错。解决办法:

     sudo gem install cocoapods -n /usr/local/bin
    
  9. 更新 cocoapod 出错:Unable to download data from https://gems.ruby-china.org/

    替换https://gems.ruby-china.org/为https://gems.ruby-china.com/:

     gem sources --add https://gems.ruby-china.com/  --remove https://gems.ruby-china.org/
    

    确保只有 gems.ruby-china.com

 类似资料: