分享

JackZhou

 Tornador 2016-08-12

天气不错哦

给你的AppDelegate减减肥

我们在开发过程中,总是不可避免的需要加入第三方的SDK,比如第三方的推送、分享、统计、IM等功能。这些SDK大多数需要在AppDelegate的回调中设置响应方法。特别是以下几个方法最为常用:

//这个回调中通常需要设置第三方SDK的AppKey等初始化信息

- application:didFinishLaunchingWithOptions:

//这两个回调中通常需要回传给SDK,供SDK处理回传参数

- application:handleOpenURL:
- application:openURL:sourceApplication:annotation:

//如果使用第三方推送,这两个回调中通常需要传给第三方SDK

- application:didRegisterForRemoteNotificationsWithDeviceToken:
- application:didFailToRegisterForRemoteNotificationsWithError:

还有其他许多一些回调,也是第三方SDK需要关心的,全部写在AppDelegate里,那么AppDelegate会显得十分臃肿杂乱并且与各个SDK耦合特别严重。(如果集成了多个SDK,简直不能忍)

解决方法

实际上部分回调是有通知的比如上边的

- application:didFinishLaunchingWithOptions:

对应会有一个名为

UIApplicationDidFinishLaunchingNotification

的通知,那么针对这样的有通知的回调就可以解耦并且剥离到另外一个类中。

新建一个类名为 SDKLaunch的类,为了解耦在+load 方法中加入Observer,

#import "SDKLaunch.h"
#import <UIKit/UIKit.h>
@implementation SDKLaunch
+ (void)load
{
    [[NSNotificationCenter defaultCenter]addObserver:self
                                            selector:@selector(applicationDidFinishLaunchingNotification:)
                                            name:UIApplicationDidFinishLaunchingNotification
                                          object:nil];
}


+ (void)didFinishLaunchingNotification:(NSNotification *)notif
{
    UIApplication *application = notif.object;
    NSDictionary *launchOptions = notif.userInfo;
    //TODO: setup SDK
}
@end

这样只需要在项目中加入SDKLaunch 就可以自动设置好了,这样那些只需要在有通知的回调中设置的SDK(如crash统计类的SDK), 在AppDelegate 一行代码都没有并且已经解耦,SDKLaunch加入项目就可以配置完成。

##进一步(需要SEL、 IMP等知识)

但是很多SDK还是需要在没有通知的回调中设置的(比如上面说的第三方推送),难道就没有办法了么?不是的,还有大杀器 runtime。我们可以用runtime中方法去hook AppDelegate中的回调方法来实现。 

那就在+load方法的时候(此时AppDelegate还实例未创建)把需要的回调替换成SDKLaunch中的方法。这样当AppDelegate创建并调用回调方法的时候后自然就跑到SDKLaunch的方法中去了

以第三方推送举例需要的回调为例:

- application:didRegisterForRemoteNotificationsWithDeviceToken:

我们需要在SDKLaunch拿到回调,首先要

#import <objc/runtime.h>

获取 AppDelegate的Class targetClass

Class targetClass = objc_getClass("AppDelegate");

然后我们需要3个SEL:
newSEL、originalSEL、defaultSEL

//获取新的方法SEL
SEL newSEL =@selector(newApplication:didRegisterForRemoteNotificationsWithDeviceToken:);
//获取原始的需要Hook的方法SEL
SEL originalSEL =@selector(application:didRegisterForRemoteNotificationsWithDeviceToken:);
//获取一个默认的SEL
SEL defaultSEL =@selector(defaultApplication:didRegisterForRemoteNotificationsWithDeviceToken:);

以及2个IMP:
newIMP、defaultIMP

IMP newIMP = class_getMethodImplementation([SDKLaunch class], newSEL);
IMP defaultIMP = class_getMethodImplementation([SDKLaunch class], defaultSEL);

在SDKLaunch实现这个2个IMP

//添加默认方法和新的方法
- (BOOL)newApplication:(UIApplication *)applicationdidRegisterForRemoteNotificationsWithDeviceToken:(NSData *)token {
   //TODO: setup Token To SDK
   return [self newApplication:application didRegisterForRemoteNotificationsWithDeviceToken:token];
}
- (BOOL)defaultApplication:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)token {
return YES;
}

然后把对应的SEL 和 IMP加入到AppDelegate中

class_addMethod(targetClass, newSEL, newIMP ,nil);    
class_addMethod(targetClass, originalSEL, defaultIMP ,nil);

再交换originalSELnewSEL

Method oldMethod = class_getInstanceMethod(targetClass, originalSEL);
Method newMethod = class_getInstanceMethod(targetClass, newSEL);    
method_exchangeImplementations(oldMethod, newMethod);

需要注意的是 这里的 defaultSEL、defaultIMP  很重要,当AppDelegate没有实现回调时,可以让hook中的方法来代替 AppDelegate 实现,不至于找不到方法而Crash。

这样无论是用到什么样回调的SDK 都可以不在AppDelegate加一行代码,实现 解耦并且自动初始化。

甚至可以分模块如分成:ShareLaunch、ChatLaunch等到不同的类中实现不同的SDK初始化、回调等工作。

另外要做的就是把hook的方法封装封装下用起来更方便。

用枚举类型优雅的控制View布局

在iOS 开发过程中经常会遇到类似这样的页面:

这3个按钮(按钮宽度不同,会受到按钮文字数量不同的影响)在不同的场景下,会有一个或者几个隐藏(如:只有“咨询”和“留言”,为了页面的美观一般都是居左对齐,也就是“留言”会到“反馈”的位置)


常见的做法:(三颗按钮对应成 button0、button1、button2)

先定义2个常量

static float paddingLeft = 12.0f;//边缘距离
static float paddingInside = 8.0f;//按钮间隔

把按钮按设置好宽度高度放入一个数组中,再设置之前全部Hidden,然后把需要显示的按钮进行布局,代码类似如下:

- (instancetype)initWithFrame:(CGRect)frame
    self = [super initWithFrame:frame];
    if (self) {
        self.buttons = @[self.button0,self.button1,self.button2];
    }
    return self;
}
- (void)setupDisplayButtons:(NSArray)displayButtons
{
    //先全部隐藏
    [self.buttons enumerateObjectsUsingBlock:^(UIButton *obj, 
                                               NSUInteger idx, 
                                               BOOL *stop) {
        obj.hidden = YES;
    }];
    //把需要显示的布局
    __block float left = paddingLeft;
    [displayButtons enumerateObjectsUsingBlock:^(UIButton *obj, 
                                                NSUInteger idx, 
                                                 BOOL *stop) {
        obj.hidden = NO;
        obj.left = left;
        left+=obj.width+paddingInside;
    }];
}

这种方式的缺点是:

1、需要把按钮暴露在外面。很多时候仅仅是展示用,并不想把按钮暴露出去。

2、按钮的从左到右的排列顺序完全依赖外部传入。(如果需要内部控制要加额外代码,虽然可以实现但是显得不干净)

优化下,调用显示按钮的时候只传入需要显示按钮的index数组:

- (void)setupDisplayButtonIndexs:(NSArray)buttonIndexs
{
    //先全部隐藏
    [self.buttons enumerateObjectsUsingBlock:^(UIButton *obj, 
                                               NSUInteger idx, 
                                               BOOL *stop) {
        obj.hidden = YES;
    }];
    //把需要显示的布局
    __block float left = paddingLeft;
    __weak type(self)weakSelf = self;
    [buttonIndexs enumerateObjectsUsingBlock:^(id obj, 
                                               NSUInteger idx, 
                                               BOOL *stop) {
        NSInteger index = [obj interValue];
        UIButton *button = self.buttons[index];
        button.hidden = NO;
        button.left = left;
        left+=button.width+paddingInside;
    }];
}

这样虽然不需要把按钮暴露出去了并且按钮的排序顺序也是由内部决定的。但是导致了接口不够清晰,需要传什么不明确,正真调用的时候还要进到内部看下按钮的顺序T_T,还是不优雅。


2.使用枚举类型

定义一个枚举类型如下:

typedef NS_OPTIONS(NSInteger, DisplayType) {
    DisplayType_Button0 = 1,
    DisplayType_Button1 = 1<<2,
    DisplayType_Button2 = 1<<3,
};

然后定义一个变量,开放给外部

@property(nonatomic,assign)DisplayType displayType;

调用如下,非常简单明确:

//把需要的按钮用位或相连
viiew.displayType = DisplayType_Button0|DisplayType_Button2;

内部在重写的setter中做处理

- (void)setDisplayButtonTypes:(DisplayType)type
{
    _displayType = type;
    float left = paddingLeft;
    //用位与判断是否显示这个按钮,这里的判断顺序就是排序的优先级
    if (type & DisplayType_Button0) {
        [self addSubview:self.button0];
        self.button0.left = left;
        left+=self.button0.width+paddingInside;
    }
    if (type & DisplayType_Button1) {
        [self addSubview:self.button1];
        self.button1.left = left;
        left+=self.button1.width+paddingInside;
    }
    if (type & DisplayType_Button2) {
        [self addSubview:self.button2];
        self.button2.left = left;
        left+=self.button2.width+paddingInside;
    }
}

是不是感觉好了许多呢?:)

我在这个公司的最后一天,迎来新的开始。

我在这个公司的最后一天,迎来新的开始。

使用TodayExtension 导致意外登出的问题

一、问题

最近项目上使用了TodayExtension后发现,原本已经登录的用户会在很“诡异”的情况下登出。(开始并未找到重现规则所以这么认为) 但是调试状态下却从未出现。

二、分析下

结合业务逻辑仔细整理了下,登出的三种情况:

1、用户手动登出 (排除)

2、Token过期并且RefreshToken也过期(这个也排除,因为我们 RefreshToken过期时间得好几个月,却有上午登录下午就意外登出了的情况)

3、从KeyChain读取或者存入失败。

看来只有第三种情况导致的,但是调试状态下却抓不到,只能把KeyChain读取与写入时的错误信息存成Log文件,出现问题时查看Log。

出现后抓到一条不寻常的错误

errSecInteractionNotAllowed -25308

Apple的注解

/* User interaction is not allowed. */

用户操作不允许。。看来是被系统阻止了, 为什么被阻止了呢,翻了半天Security framewoeks的头文件终于在 SecItem.h发现了这么样一段:

@enum kSecAttrAccessible Value Constants
@discussion Predefined item attribute constants used to get or set values
    in a dictionary. The kSecAttrAccessible constant is the key and its
    value is one of the constants defined here.
    When asking SecItemCopyMatching to return the item's data, the error
    errSecInteractionNotAllowed will be returned if the item's data is not
    available until a device unlock occurs.

大意是说 这几个value 是给 kSecAttrAccessible 这个key 使用的,如果不符合value权限规定会返回 errSecInteractionNotAllowed。官方对这个key的说明大致是

当你需要访问KeyChain时应该选择满足你程序要求的、最严格的权限,以便iOS最大限度保护你的项目

这是KeyChain另外一种更加细致的权限分配。

看看它的几个值

kSecAttrAccessibleWhenUnlocked
kSecAttrAccessibleAfterFirstUnlock
kSecAttrAccessibleAlways
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly
kSecAttrAccessibleWhenUnlockedThisDeviceOnly
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
kSecAttrAccessibleAlwaysThisDeviceOnly

其实大致含义从他的命名上能看出来,而Apple的注释中写的更为详细

* kSecAttrAccessibleWhenUnlocked

只有设备解锁的情况下有效,并且当迁移到其他设备时会加密备份到别的设备。

* kSecAttrAccessibleAfterFirstUnlock

只有设备在重启后的一次解锁后的情况下有效,并且当迁移到其他设备时会加密备份到别的设备。

* kSecAttrAccessibleAlways

任何情况下都能访问无论设备是否解锁,并且当迁移到其他设备时会加密备份到别的设备。除了给系统使用不推荐使用

* kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly

这个是iOS8、OSX10.10后新增的属性,只有设备设置了密码并在解锁状态才有效,如果关闭设备密码在这里存储的数据会被删除。数据只保留在本机,如果迁移到其他设备这里的数据不会被迁移。这个权限显然比较严格,推荐给非常重要的数据使用。

* kSecAttrAccessibleWhenUnlockedThisDeviceOnly

只有设备解锁的情况下有效,数据只保留在本机,如果迁移到其他设备这里的数据不会被迁移。

* kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly

只有设备在重启后的一次解锁后的情况下有效,如果迁移到其他设备这里的数据不会被迁移。

* kSecAttrAccessibleAlwaysThisDeviceOnly

任何情况下都能访问无论设备是否解锁,如果迁移到其他设备这里的数据不会被迁移。

三、解决

了解了这些就可以回过头来看看 ,之前的问题就可以大致猜测了,应该是在锁定状态打开了TodayExtension,访问了KeyChain,导致发生errSecInteractionNotAllowed 错误,用户登出。而调试状态下都是没有锁定设备的,所以一直没有出现。根据猜测尝试操作后就重现了这问题。只需要在设置成 kSecAttrAccessibleAlways就可以了。但是需要注意,因为之前的数据已经是 WhenUnlocked 有效的,所以当有权限的时候要把原来的数据重新存成Always 这样才能正常访问。

四、总结下

这些权限实际上是这样几种权限的组合

1、是否单台设备
2、是否解锁
3、是否第一次解锁

4、是否有密码(iOS8)

官方并没有说明kSecAttrAccessible默认值,但是大致可以从现象上猜测下:是需要解锁中的其中一种。但一定不需要是第一次解锁,所有默认值只有三者之一: kSecAttrAccessibleWhenUnlocked kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly kSecAttrAccessibleWhenUnlockedThisDeviceOnly

具体是哪个没有具体考证,但是从他家讨论的来看应该是kSecAttrAccessibleWhenUnlocked,这也符合苹果一贯把第一个作为默认值的风格。

——-2014.12.30 补充

权衡了下,这个方式只能说从技术上解决了问题,但其实仔细从产品或者用户角度考虑当用户的设备被锁定时,其实并不应该去访问用户的登录信息的,这样可以起到保护用户信息的作用(因为TodayExtension是可以在锁定状态下打开的)。我觉得更好的方式是从产品上提示用户解锁设备后才能执行一些需要获取用户登录信息才能继续的操作。这应该也是 Apple设计这样的权限的初衷吧。

Extension下的单例生命周期

最近项目中用到了 ShareExtension ,发现了个的现象,如果在Extension 中使用了单例,其生命周期是跟随Host app的,也就是说如果在Safari下打开ShareExtension,那么只要Safari不被Kill掉,无论如何关闭打开Extension,其使用的单例永远是同一个。TodayExtension 经过测试也是一样(这样的话就不知道TodayExtension下的单例什么时候会生成新的,重启iOS?!)。

一开始感到挺惊讶的,但是细想下来,确实应该这样,Extension 对Host app来说只是一个插件(页面)而已。只是之前把Extension想象的太过独立。

测试过程很简单: 看下项目目录

InstanceObject 

是一个单例里面只有1个类方法,返回一个单例

+ (instancetype)sharedManager
{
    static dispatch_once_t onceToken;
    static InstanceObject *instance;
    dispatch_once(&onceToken, ^{
        instance = [[InstanceObject alloc] init];
     });
    return instance;
}

然后新建Extension的Target,并且把 InstanceObject 加入到Extension的编译中

然后在Extension的ViewController的 -(void)viewDidLoad 中加入打印

NSLog(@"\nExtensionVC Address:%p \nInstanceObj Address:%p",self,[InstanceObject sharedManager]);

然后在不关闭Safari的前提下打开、关闭Extension,可以看到InstanceObj Address 是同一个,而ExtensionViewController一直在变化

初识iOS8 Today Extension

iOS8正式版发布已经有段时间了,今天看了下Extension 相关的新功能接口的使用。

Apple 提供了6个位置(类型)的Extension:

其中最强大实用的应该就是 Custom Keyboard(自定义键盘)和 Today Extension(通知中心的“Today”Tab扩展)

对TodayExtension比较感兴趣就从它下手了

一、 创建带有Extension的项目

首先使用Xcode6新建一个项目叫“MyApplication”的项目,为了简单起见就使用 Single View Application。然后新增一个Target (File->New->Target),选择Application Extension 的 Today Extension 类型。

填上Product Name,就可以了这样就一个带有TodayExtension的项目建好了。

二、Hello,Extension!

项目建立后看到的目录是这样的

上下2个目录结构是不是很像呢~TodayExtension下的MainInterface.storyboard 就是会展示在通知中心的“Today”Tab下的视图。 Target选择TodayExtension Run!通知中心中已经呈现刚才的视图了:

如果这个默认的视图不够高不能显示下全部内容可以通过设置TodayViewController的preferredContentSize来调整。

三、研究下

1、

首先刚才注意到“MyApplication”和“TodayExtension”目录很像,都有个info.plist 文件,看看TodayExtension下的info.plist:

它拥有独立的一个APP必要的基本信息 和 特有的NSExtension信息。从这点上看出Extension其实就是一个高度独立APP。 为什么说是高度而不是完全独立呢?

第一、是因为它虽然拥有独立的Bundle identifier,但必须要以它的主程序的Bundle identifier为前缀,如果尝试修改掉这个前缀就不能Run起来了(猜测iOS是通过Bundle identifier判断从属关系的)。

第二、是因为他们使用的都是同一个沙盒,即主程序的沙盒,因此Extension可以随意读写主程序沙盒内文件以及KeyChain。

2、

看看TodayExtension生命周期,在以下2个方法中加入打印

-(id)initWithCoder:(NSCoder *)aDecoder{
    NSLog(@"===> initWithCoder");
    return [super initWithCoder:aDecoder];
}

-(void)dealloc
{
    NSLog(@"===> dealloc");
}

有两个特性:

1、 当通知中心的Tab是选中Today的时候: 拉出通知中心时,通知中心拉到底才会生成这个TodayExtension实例 收起通知中心时,只要一移动通知中心就会把TodayExtension实例释放 如下图:

所以当下拉通知中心的时候其实显示的是通知中心上一次的截图,而当向上划收起的时候显示的是滑动之前的截图。这也是动画效果的常用做法。

2、 只要通知中心的Today Tab不显示就不会生成这个TodayExtension实例或已经生成的就会释放。

也就是说Extension 的生命周期和主程序是无关的,是和Extension的宿主程序(这里是通知中心的Today Tab)相关。

结合这次发现的Extension特点,对Extension 、主程序、宿主程序之间的关系打个比喻

一个城市只有一个市公安局(主程序),但是会在不同的地方(宿主程序)设置不同的派出所(Extension),没有市公安局就不会设立派出所(从属关系),派出所可以访问市公安局的数据库(沙盒),公安局下班了派出所还是可以加班的(高度的独立性),但是如果公安局没有设立派出所工作效率就不高、不方便(插件的扩展功能)

先这么点吧。

iOS APP 重签名的问题

之前有朋友想实现ipa包内的资源文件修改,并且根据不同的资源生成各自新的ipa包。

想想有2种方式:

1、修改完资源文件后编译打包

2、直接对ipa包内的修改


第一种显然不是最优方案有2个缺点

1、需要在服务端存放客户端代码

2、消耗性能过程时间长

所以第一种PASS

那就只剩下第二种了。


一、为什么要重签名

为了校验资源文件的完整性,在编译打包成ipa后,会/Payload/xxx.app/_CodeSignature/目录下生成一个CodeResources文件 内容类似:

key是文件的名称,data 是文件的hash值应该使用的是SHA1+BASE64的方式加密(猜测)。

所以如果只修改资源文件 不修改这个CodeResources的话,安装ipa的时候就会验证失败导致安装不了。

二、如何重签名

使用 Xcode Command Line Tools

有2种方式:

1、使用xcrun命令

这种方式是最方便的只要对xxx.app文件执行,就能输出一个重新签名好的.ipa文件。

命令如下:

xcrun -sdk iphoneos PackageApplication -v "YourAppName.app" -o "/Users/YourAppName.ipa" --sign "iPhone Developer: XXXXX (6NG3S0RRCG)" --embed "xxx.mobileprovision"

2、使用codesign命令

这种方式也是网上流传最广的方式:

codesign -f -s "iPhone Developer: XXXXX (6NG3S0RRCG)" "xx/YourAppName.app"

不过这种方式输出的还是个.app文件,需要把这个YourAppName.app放到 名为Payload的文件夹,然后用zip压缩Payload文件夹,把生成的Payload.zip后缀改成.ipa,才能使用。

三、有什么问题

使用xcrun命令来重签名打包完美,没有任何问题。

使用codesign签名的包实际测试中,可以安装成功运行也没有问题, 但是使用NSKeyedUnarchiver、NSKeyedArchiver时 读取写入信息都会失败报错误号为-34018的错误。

四、怎么办

为什么用xcrun 就可以 用codesign就不行呢?

尝试使用:

 xcrun -sdk iphoneos PackageApplication -v "YourAppName.app" -o "/Users/YourAppName.ipa" --sign "iPhone Developer: XXXXX (6NG3S0RRCG)" --embed "xxx.mobileprovision"  > codeSign.log

把xcrun的日志输出看看它时怎么做的。

其实xcrun内部也是使用codesign来签名的,不同的是它多了个参数

--preserve-metadata=identifier,entitlements,resource-rules

所以如果要用codesign来签名应该是这样的:

codesign --preserve-metadata=identifier,entitlements,resource-rules -f -s "iPhone Developer: XXXXX (6NG3S0RRCG)" "xx/YourAppName.app"

经测试已经解决KeyChain不能用的问题。

五、如果你的需求也是这样(重新签名->打成ipa包),那还是使用xcrun吧!方便快捷。

几种可能引起[UIImage imageNamed:@“imageName”] == null 的情况

几种可能引起[UIImage imageNamed:@“imageName”] == null 的情况

资源为 imageName.png

  • 1 大小写:

代码:

[UIImage imageNamed:@"imagename"] 

造成模拟器中正常显示真机中返回null。

  • 2 图片格式

代码:

 [UIImage imageNamed:@"imageName"]

资源后缀是png,其实真实格式并不是png,造成模拟器中正常显示真机中返回null。

使用CocoPods&Git Branch方便构建多渠道的IPA包(二)

在前面一篇《使用CocoPods&Git Branch方便构建多渠道的IPA包(一)》 中介绍了思路及实现了更换颜色的简单效果,当然如果在配置的项目中放入不同的图片、Icon资源,就可以换肤了。

这次再来点稍微高级点的。

实现不同的包不同的 APP DisplayName 以及 Bundle Identifier

其实原理上相同 , 不同的是 这次建的文件是个Xcode 能读懂的配置文件,这个文件就是 Demo-Info.plist 位置如下:

img

看看里面的信息 应有尽有:

img

好了 我们只要拷贝一分,分别放到配置项目Configuration 不同分支下,然后修改成 不同的值。 然后pod update 下这个配置文件就被引入进来到项目里了。

额外需要做的就是把Xcode 读取配置文件的路径改成Configuration 项目下的 plist 文件,这个设置值位于 Targets->Demo->Build Setting 下:

img

把它修改为:

Pods/Configuration/ConfigFiles/Demo-Info.plist

并删除项目Supporting Files 下的 Demo-Info.plist

这样引入不同分支的 Demo项目 就有了不同的 APP DisplayName 以及 Bundle Identifier,总之只要是 Demo-Info.plist 下的配置都可以定制。

接下来就是我们的终极目的批量打包。

当需要3个以上不同IPA包的时候 ,Xcode打包的工作就显得很痛苦了,根本无法忍受

Xcode 提供 Command Tools 有打包功能这样我们需要的批量打包功能就可以用bash 来实现了。

思路:

根据设定的 branch 列表 做循环,单个循环过程是 修改Podfile 中引入配置项目的 branch 并 执行编译打包。

为了批量打包 先定义一些必要的参数:

//branch 的列表

branchs=("branchA" "branchB") 

//证书的列表

codeSignIdentities=("iPhone Distribution: Tom Zhang (SPAR8982YR)" "iPhone Distribution: Kitty Liu (7P89F6ZQS4)")

//配置文件的列表 profiles=(“branchA_appStore” “branchB_appStore”)

//需要Archive的scheme scheme=“Demo”

//项目workspace的名称 workspaceName=“Demo”

//打包输出的目录 outPutDir=“build/Release-iphoneos/”

//配置项目的名称(Podfile 中的那个配置项目的名字) configurationProjectName=“Configuration”

这个几个参数不多说了 和 用Xcode 打包是一样的。

首先使用 xcodebuild 命令 Archive 项目生成 .xcarchive :

 xcodebuild -workspace ${workspaceName}.xcworkspace CODE_SIGN_IDENTITY="${codeSignName}" -scheme ${schemeName} archive  -archivePath ${xcarchiveName} 

然后使用 xcarchive 文件 打包成 .ipa 文件:

xcodebuild -exportArchive CODE_SIGN_IDENTITY="${codeSignName}" -exportFormat IPA -archivePath  "${xcarchiveFilePath}".xcarchive -exportPath "${ipaName}".ipa -exportProvisioningProfile "${profile}"

完整脚本 点击 这里下载

打包时只要执行 sh Packaging

img

然后就可以去泡咖啡啦,即使时20个包也就喝一杯咖啡的时间!

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多