游戏助手技术细节:结合Shell Script快速建立大量相似iOS App

游戏助手技术细节:结合Shell Script快速建立大量相似iOS App

背景

游戏助手项目是由一系列的游戏助手App组成,需要在现有基础功能上,通过换皮肤、渠道识别以及新增功能,快速制作出小差异化的App,例如:炉石传说助手、迷你西游助手,大话西游2助手等。

游戏助手系列

如何复制?

目前,有两种可行方式:Targets(编译目标)与Subprojects(子工程)

  • Targets - 同一项目工程里,通过复制多个target,利用target与scheme的配合编译不同的资源文件,得到多个App;
  • Subprojects - 把主框架独立成库项目,再复制出多个子工程得到多个App。

下面,先从Targets方式说起,分别介绍一下两个方式在我们项目中的具体实践,并讲述为什么我们放弃使用targets的方式,以及使用子项目方式都有哪些难点和关键点。

Targets方式

话不多说,先直接上图给大家看看:

工程结构

虽然xcode的文件组是虚拟的,但真实文件结构也差不多,就不截图上来了,下面直入实现细节。

实现细节:

  1. 每个target指定不同的Info.plist文件;
  2. Build Phases的Copy Bundle Resources中仅添加对应资料夹的资源;
  3. Build Settings -> Preprocessing添加Preprocessing Macros预处理宏常量以区分各App的其它实现细节;
  4. 复制同名带_TS的为测试服数据target,区别在于bundle id添加测试服标识,以及Build Settings -> Custom Compiler Flags设置预编译常量表明为测试服数据源;
  5. 预编译常量:每个资料夹下独立的AppBuilder.h(例如开放平台id与统计id等);
  6. 服务端通过渠道标识区分来自哪个助手App;
  7. 指定target对应的scheme进行编译,最终得到不同App。

使用Run Script

  1. Run Script脚本:放置bundle id等基础信息,每次跑都修改Info.plist,而不是直接修改Info.plist;
  2. Run Script处理AppBuilder.h的使用,通过拷贝至基础代码替换文件的方式实现xcode唯一引用,而非直接引用资料夹下的。
脚本流程

复制助手的步骤:

  1. 右击某个助手target,Duplicate(通过复制新建)这个target;
  2. 移除target内上一个助手的资源,例如几百个上个助手的皮肤图片,一些不可重用有代码等,但注意不要碰到基础代码内的东西;
  3. 建立资源资料夹,准备Info.plist,AppBuilder.h,以及相关资源;
  4. 资源文件仅添加进xcode的这个新建target;
  5. 修改AppBuilder.h,Run Script的内容;
  6. 修改xcode中新target的Product Name;
  7. 使用新的Info.plist为target的Info.plist
  8. 修改target内的其它预编译常量
  9. 通过对这个新的target 进行duplicate,修改类似相关的点得到测试服target

以上步骤缺一不可,当然,还是有进一步整理空间的,但主要问题不在这,请继续往下看。

Targets的优缺点:

优点是工程结构简单,清晰,统一,另外唐巧《使用多target来构建大量相似App》对此进行了很好的诠释,最初我们的项目工程也是这种方式,但当助手数量增加后,但缺点也越来越明显:

  • 工程文件(xcodeproj)日益增大,如上面的Copy Bundle Resource就有1700+资源文件,compile的文件就300+,每一个资源文件的引用就是1行记录,14个助手App,就有28个target,共28x2000 = 56000行记录,单个文件就20MB;
  • 因为上一条,导致xcode处理效率下降,甚至卡死,哪怕只修改target内的一个字母,2012年i5 MBA直接卡顿30秒以上;
  • 在协作与版本管理上,会造成多个人多次反复修改同一项目核心文件project.pbxproj,出现冲突机率很大,而且该文件不适合人工修改,一旦冲突出现,如果修改得多,他人根本就无从下手,xcode也无法打开,只好无奈revert了。

在SNS工具上也为此向大牛唐巧请教过,他也无奈表示目前还没什么办法解决,于是我们决定用子项目的方式把target分出来。

子项目方式:利用脚本批处理复制

实现计划

  1. 确定基础功能明确,确立框架,最终基础库由Foundation与UI两库组成,以.a文件形式提供;
  2. 代码重构,把注入式的配置文件AppBuilder.h、预编译常量全部分离,脱出基础库,基础库不再预编译进任何助手信息;
  3. Run Script改造,不再耦合任何助手信息,转为读取另外配置文件,并独立成文件;
  4. 精简target,不再储存资源文件引用以外的信息
  5. 所有信息,包括需要预编译信息,统一由运行期设置
  6. 建立Template项目,使用xcode workspace组织各子项目
  7. 编写脚本,从Template生成新项目,目的把上面繁琐的复制步骤去人工化,减负并减少出错机率

执行难点

大量的预编译常量使用

因为AppBuilder.h使用预编译常量记录信息,在基础库里散落各地,需要逐一整理出来,并用运行时的单例来取代它们。这部分没什么办法,只能慢慢挑鱼骨头。

原代码:

#if !kOpenPlatformEnabled
    self.shareButton.hidden = YES;
    //codes...
#else
    self.shareButton.hidden = NO;
    //codes else...
#endif

修改为

if (![BuildInfo shareInstance].openPlatformEnabled]){
    self.shareButton.hidden = YES;
    //codes...
} else {
    self.shareButton.hidden = NO;
    //codes else...
}

因为#define常量在编译期就固定值,不包含常量类型信息,xcode不能辅助识别和Refactor,不利于维护和debug,所以常量定义尽量使用const而非#define

// File.h
extern NSString *const MyKey;

// File.m
NSString *const MyKey = @"MyKey";

预编译 vs 运行时

预编译可以选择性的编译代码或打包资源,例如正式包不会包含任何测试服信息,即使逆向工程也无法从中找到任何信息,在一定程序上起到数据保护作用。

#if BUILD_FOR_ONLINE_APP
    return @"https://myonlinehost.163.com";
#else
    return @"http://123.123.123.123/";
#endif

project.pbxproj文件

利用子工程的方式与target方式一样需要进行项目配置,但最大的问题还在于xcode工程文件本身的组织方式,

  • xcode文件的组是虚拟组,文件引用并非扫描当下目录文件,不关心实体文件路径只关心文件名,并且隐藏了具体路径细节;
  • 通过Scheme指定Target进行编译,target参数众多,人工复制修改需要好记性与细心,是个绣花活。
上图:需要把左边的模板修改成为右边的样子

.xcodeproj是一个包,显示包内容后会发现最核心的是project.pbxproj文件,这个文件其实是一个类似JSON形式的文本,但并非JSON,我们希望从中找出规律,让脚本在复制的时候自动修改对应的内容

pbxproj文件

文件内容非常庞大,但仔细观察,还是能发现一些规律的,例如:

/* Begin PBXBuildFile section */
    94EDEA9C1A13500E00AAEA5F /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 94EDEA9B1A13500E00AAEA5F /* AppDelegate.m */; };
    ....
/* End PBXBuildFile section */

类似注释号的/* Begin PBXBuildFile section */明确了需要Build的文件;而类似94EDEA9B1A13500E00AAEA5F为索引key,xcode通过key来检索所以信息点。

当然,我们并不需要太过关心这些,毕竟这文件不是给人去修改的。

Hack掉project.pbxproj

  1. 首先,利用xcode做好一个Template工程,并把所有文件路径设置为Relative to Group,这样能简化路径搜寻;
relativetogroup
  1. 准备好要引用的代码、资源、Info.plist、Framework等;
  2. 修改项目名字,scheme name,target name为好标记的字母,例如统一使用Sample为名字替换目标;
  3. 准备target内其它需要的内容;
  4. 关于AppDelegate.m,上面说了,xcode并不关心文件放哪里,所以你可以用A项目的AppDelegate.m放到B项目里,只要没BUG,一样能运行起来,所以最终的子项目只是借用了原AppDelegate.m,而不是每次都用新的文件,.h文件也如是。换句话说,xcode工程文件只是一个虚拟文件组织器,任何认为不需要独立出来的文件都可以指向同一个,甚至是main.m文件!
  5. 让工程正确运行起来,通过测试;

自动脚本Shell Script

当上面的操作准备好后,再打开project.pbxproj,会发现一切都如此清晰,只需要用shell来替换埋设好的关键字即可。

在Mac,用来替换文件字符串,并拷贝到目标目录,可以用到sed命令:

sed -e "s/TemplateString/NewString/g" Template/template.xcodeproj/project.pbxproj > NewPath/NewNameFile/project.pbxproj

# 如果有多个要替换,可以多个集合多个替换字符串:
sed -e "s/TemplateString/NewString/g"  -e "s/TemplateString2/NewString2/g" Template/template.xcodeproj/project.pbxproj > NewPath/NewNameFile/project.pbxproj

我们的脚本必须能接受几个基本的参数,然后把参数转成对应的信息:

例如名为create.sh,执行时这样:

./create.sh -n ZGMH -b com.abc.zgmh -c zgmh_channel -p 1004

脚本开头可以这样写:

while [ "$1" != "" ]; do
case $1 in
    -n | --name )           shift
                            name=$1
                            ;;
    -b | -bundleid )        shift
                            bundleid=$1
                            ;;
    -c | --channel )        shift
                            channel=$1
                            ;;
    -p | --pushid )         shift
                            pushid=$1
                            ;;
    * )                     usage
                            exit 1
esac
shift
done    

这样就能接受参数了

更多设置:settingVars.txt

在前文,我们看到在Run Script中进行了Info.plist修改,我们把Run Script单独抽出来,并且用第三个文件来定义更多参数。

settingVars.txt:

# 配置信息 有空格必须用英文半角双引号括起来
CHANNEL=sampleChannel           #渠道名与目录
# 以下为info.plist内对应的内容

APP_BUNDLE_ID=appBundleId   #CFBundleIdentifier
CFBUNDLE_URL_NAME=zsUrlName #CFBundleURLName
CFBUNDLE_URL_SCHEMES=zsScmeme       #CFBundleURLSchemes
CFBUNDLE_DISPLAY_NAME=某某游戏助手        #CFBundleDisplayName

如何读进去脚本里?

#read setting vars
varsContent=$(

简单粗暴到连自己都不相信[捂脸]。

如何读写plist文件?

/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName ${CFBUNDLE_DISPLAY_NAME}" ${INFO_PLIST_FILE_PATH}

通过PlistBuddy来设置和读取plist,把接下来的字段补充完成即可,全部代码就不贴出来了。

小结一下

  1. 在xcode中配置好一切,埋设标识符;
  2. 编写Shell Script,用sed命令替换标识符并输出成新的project.pbxproj文件,
  3. 复制Template下所有文件到新目录;
  4. 编写统一runscript,读取新助手资料夹下的settingVars.txt内容,修改Info.plist;
  5. 复制助手准备完成

以上的步骤只需要一次准备,以后的助手复制只需要一条命令:

./create.sh -n ZGMH -b com.abc.zgmh -c zgmh_channel -p 1004

项目使用了workspace来管理,只在需要的助手才放进去:

总结

善于运用Shell Script,能帮人自动化执行一些重复劳动,结合xcode的run script,可以最大限度解放劳动力,提高效率减少人工错误率。上文内容包括了大部分助手复制技术实现关键点,事实上我们还可以给run script传参数,用来生成不同的内部测试包。下一步,还会把自动化打包部分脚本再完善,自动生成多个包,并进行签名。

【完】

你可能感兴趣的:(游戏助手技术细节:结合Shell Script快速建立大量相似iOS App)