iOS - 如果仅在设备上运行,我的应用程序会因内存错误而崩溃

Posted

技术标签:

【中文标题】iOS - 如果仅在设备上运行,我的应用程序会因内存错误而崩溃【英文标题】:iOS - My app crashes with memory Error if only runs on Device 【发布时间】:2015-07-30 23:19:01 【问题描述】:

我的应用在模拟器上完美运行。 但是当我在设备上运行它进行测试时,应用程序崩溃了。

显示的错误:

"malloc: * 对象 0x17415d0c0 的错误:从空闲列表中出列的无效指针 * 在 malloc_error_break 中设置断点以进行调试";

有时还会发生另一个错误:

“线程6 com.apple.NSURLConnectionLoader:程序接收到的信号:EXC_BAD_ACCESS”

这两个错误是随机的,会在应用运行两三分钟后发生。

我搜索了谷歌并找到了一些解决方案。 它说在 malloc_error_break 中设置断点进行调试,但这仅适用于模拟器。即使我设置了 malloc_error_break,应用程序仍然崩溃并且它不显示任何内容,我只能看到错误消息。

这是一个大项目,我真的不知道哪部分代码导致了问题,我需要发布哪部分代码。 但我对其中一门课,我的“SynchroningView”有疑问。 如果只有我的应用程序与服务器同步,将显示此视图。这意味着这个视图包含在我项目中几乎所有的 Viewcontroller 中。

这是我的“SynchroningView”类:

“SynchroningView.h”

#import <UIKit/UIKit.h>
@class SynchroningView;

@protocol SynchroningViewDelegate
@optional
- (void)SynchroningViewCancelButtonDidClicked:(SynchroningView *)synchroningView;
- (void)SynchroningViewStartTherapyButtonDidClicked:(SynchroningView *)synchroningView;
@end



@interface SynchroningView : UIView <DataManagerDelegate>

@property (nonatomic, assign) id<SynchroningViewDelegate>delegateCustom;

@property (nonatomic, retain) UIButton *containerButton;

@property (nonatomic, retain) Patient *singlePatient;





- (instancetype)initWithFrame:(CGRect)frame;

- (void)showInView:(UIView *)view;

- (void)dismissMenuPopover;

@end

SynchroningView.m

#import "SDDemoItemView.h"
#import "SDPieLoopProgressView.h"



#define Directory_Document  [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0]

#define FileName_Image_SynchroningView_BackGroundImageName @"SynchroningView_BackGroundImage"

#define FilePath_Image_SynchroningView_BackGroundImageName [Directory_Document stringByAppendingPathComponent:FileName_Image_SynchroningView_BackGroundImageName]



@interface SynchroningView ()

@property   (nonatomic, strong) DataManager*    dataManager;

@property   (nonatomic, retain) UILabel*    messageLabel;

@property   (nonatomic, retain) UIButton*   CancelButton;
@property   (nonatomic, retain) CoolButton* ConfirmButton;
@property   (nonatomic, retain) CoolButton* startTherapyButton;

@property   (nonatomic, strong) SDDemoItemView* demoProgressView;

@end

@interface SynchroningView ()

    BOOL    _blinking;

    NSTimer *timer;

@end

@implementation SynchroningView

-(void)dealloc



//get the background Image, make it blur and save it. In this way I do not have to use Blur function everytim, I use the blured image directly
- (UIImage *)getBackGroundImage

    NSFileManager *fileManager = [NSFileManager defaultManager];
    if ([fileManager fileExistsAtPath:FilePath_Image_SynchroningView_BackGroundImageName]) 
        return [UIImage imageWithContentsOfFile:FilePath_Image_SynchroningView_BackGroundImageName];
    
    else
        UIImage *backImage = [UIImage imageWithContentsOfFile:kBackgroundImagePath];
        backImage = [backImage blurWithFloat:20.0f];

        NSData * binaryImageData = UIImagePNGRepresentation(backImage);

        if ([binaryImageData writeToFile:FilePath_Image_SynchroningView_BackGroundImageName atomically:YES]) 
            return backImage;
        
    


    return [UIImage imageWithContentsOfFile:kBackgroundImagePath];


-(instancetype)initWithFrame:(CGRect)frame

    self = [super initWithFrame:frame];

    if (self) 

        _blinking = NO;
        self.dataManager = [[DataManager alloc] init];
        self.dataManager.DelegateCustom = self;



        self.backgroundColor = [UIColor clearColor];






        self.containerButton = [[UIButton alloc] init];
        [self.containerButton setBackgroundColor:RGBA_Custom(0, 0, 0, 0.6f)];
        [self.containerButton setAutoresizingMask:UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleBottomMargin];




        UIView *shadowView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.frame.size.width, self.frame.size.height)];
        [self addSubview:shadowView];
        shadowView.backgroundColor = RGBA_Custom(230, 249, 251, 0.96f);
//        shadowView.layer.borderWidth = 2*IPAD;
//        shadowView.layer.borderColor = [RGBA_Custom(199, 199, 199, 1.0f) CGColor];
        shadowView.layer.shadowColor = [UIColor blackColor].CGColor;
        shadowView.layer.shadowOpacity = 0.4;
        shadowView.layer.shadowRadius = 4*IPAD;
        shadowView.layer.shadowOffset = CGSizeMake(4.0f*IPAD, 4.0f*IPAD);
        shadowView.layer.cornerRadius = 6*IPAD;
        shadowView.userInteractionEnabled = NO;




        UIImageView *backGround = [[UIImageView alloc] initWithImage:[self getBackGroundImage]];
        backGround.frame = CGRectMake(0, 0, shadowView.frame.size.width, shadowView.frame.size.height);
        backGround.backgroundColor = [UIColor clearColor];
        [shadowView addSubview:backGround];
        backGround.layer.cornerRadius = shadowView.layer.cornerRadius;
        backGround.layer.masksToBounds = YES;




        ImageWIDTH_ = self.frame.size.height*0.3;
        self.demoProgressView =[SDDemoItemView demoItemViewWithClass:[SDPieLoopProgressView class]];
        self.demoProgressView.frame = CGRectMake((self.frame.size.width - ImageWIDTH_)/2,
                                                 kFromLeft,
                                                 ImageWIDTH_,
                                                 ImageWIDTH_);
        [self addSubview:self.demoProgressView];




        self.messageLabel = [[UILabel alloc] initWithFrame:CGRectMake(kFromLeft,
                                                                  self.demoProgressView.frame.origin.y + self.demoProgressView.frame.size.height + kFromLeft,
                                                                  self.frame.size.width - kFromLeft*2,
                                                                  self.frame.size.height - (self.demoProgressView.frame.origin.y + self.demoProgressView.frame.size.height + kFromLeft*2))];
        self.messageLabel.backgroundColor = [UIColor clearColor];
        self.messageLabel.textColor = [UIColor blackColor];
        self.messageLabel.textAlignment = NSTextAlignmentCenter;
        self.messageLabel.numberOfLines = 0;
        self.messageLabel.lineBreakMode = NSLineBreakByWordWrapping;
        self.messageLabel.font = [UIFont boldSystemFontOfSize:kIPAD ? 20:12];
        self.messageLabel.text = @"";
        self.messageLabel.adjustsFontSizeToFitWidth = YES;
        [self addSubview:self.messageLabel];









        CGFloat BtnHeight = kIPAD ? 40  : 30;
        self.CancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
        self.CancelButton.frame = CGRectMake(self.frame.size.width - kFromLeft*0.5 - BtnHeight,
                                             kFromLeft*0.5,
                                             BtnHeight,
                                             BtnHeight);
        self.CancelButton.backgroundColor = [UIColor clearColor];
        [self.CancelButton setImage:[[UIImage alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"utilities_close" ofType:@"png"]] forState:UIControlStateNormal];
        [self.CancelButton addTarget:self action:@selector(pressCancelButton:) forControlEvents:UIControlEventTouchUpInside];
        [self addSubview:self.CancelButton];




        BtnHeight = kIPAD ? 70  : 50;
        CGFloat BtnWidth  = self.frame.size.width/4;
        NSString*   ConfirmButtonStr = NSLocalizedString(@"Confirm", nil);
        ExpectedSize = [ConfirmButtonStr sizeWithFont:self.messageLabel.font constrainedToSize:CGSizeMake(VIEW_WIDTH, BtnHeight) lineBreakMode:NSLineBreakByWordWrapping];
        ExpectedSize = CGSizeMake(ExpectedSize.width + 30*IPAD, ExpectedSize.height);
        self.ConfirmButton = [CoolButton buttonWithType:UIButtonTypeCustom];
        self.ConfirmButton.frame = CGRectMake((self.frame.size.width - ExpectedSize.width)/2,
                                              self.frame.size.height - BtnHeight - kFromLeft,
                                              ExpectedSize.width,
                                              BtnHeight);
        self.ConfirmButton.titleLabel.font = self.messageLabel.font;
        [self.ConfirmButton setTitle:ConfirmButtonStr forState:UIControlStateNormal];
        [self.ConfirmButton addTarget:self action:@selector(pressConfirmButton:) forControlEvents:UIControlEventTouchUpInside];
        [self addSubview:self.ConfirmButton];








        BtnWidth = self.frame.size.width*0.6;
        NSString*   startTherapyButtonStr = NSLocalizedString(@"StartTerapia", nil);
        ExpectedSize = [startTherapyButtonStr sizeWithFont:self.messageLabel.font constrainedToSize:CGSizeMake(VIEW_WIDTH, BtnHeight) lineBreakMode:NSLineBreakByWordWrapping];
        ExpectedSize = CGSizeMake(ExpectedSize.width + 30*IPAD, ExpectedSize.height);
        self.startTherapyButton = [CoolButton buttonWithType:UIButtonTypeCustom];
        self.startTherapyButton.frame = CGRectMake((self.frame.size.width - ExpectedSize.width)/2,
                                                   self.ConfirmButton.frame.origin.y,
                                                   ExpectedSize.width,
                                                   self.ConfirmButton.frame.size.height);
        self.startTherapyButton.titleLabel.font = self.messageLabel.font;
        [self.startTherapyButton setTitle:startTherapyButtonStr forState:UIControlStateNormal];
        [self.startTherapyButton addTarget:self action:@selector(startTherapyButtonDidClicked:) forControlEvents:UIControlEventTouchUpInside];
        [self addSubview:self.startTherapyButton];





        self.CancelButton.alpha         = 0.0;
        self.ConfirmButton.alpha        = 0.0;
        self.startTherapyButton.alpha   = 0.0;





        if (self.dataManager.firstSynchronizationAgain == 1 && [AccessedInfo getAccessedInfo]) 
            UILabel *alertInfoLabel = [[UILabel alloc] initWithFrame:CGRectMake(kFromLeft,
                                                                                0,
                                                                                self.frame.size.width - kFromLeft*2,
                                                                                VIEW_HEIGHT)];
            alertInfoLabel.backgroundColor = [UIColor clearColor];
            alertInfoLabel.textColor = COLOR_CIRCLE;
            alertInfoLabel.textAlignment = NSTextAlignmentCenter;
            alertInfoLabel.numberOfLines = 0;
            alertInfoLabel.lineBreakMode = NSLineBreakByWordWrapping;
            alertInfoLabel.font = [UIFont systemFontOfSize:self.startTherapyButton.titleLabel.font.pointSize-2];
            alertInfoLabel.text = NSLocalizedString(@"SynchronizingNew", nil);
            ExpectedSize = [alertInfoLabel.text sizeWithFont:alertInfoLabel.font constrainedToSize:alertInfoLabel.frame.size lineBreakMode:NSLineBreakByWordWrapping];
            alertInfoLabel.frame = CGRectMake(alertInfoLabel.frame.origin.x,
                                              self.startTherapyButton.frame.origin.y - ExpectedSize.height - IPAD*2,
                                              alertInfoLabel.frame.size.width,
                                              ExpectedSize.height);
            [self addSubview:alertInfoLabel];

        





        [self.containerButton addSubview:self];
    

    return self;

//show the synchronization result message
- (void)setMessageLabelString:(NSString *)labelName

    self.messageLabel.text = labelName;
    ExpectedSize = [labelName sizeWithFont:self.messageLabel.font constrainedToSize:CGSizeMake(self.messageLabel.frame.size.width, VIEW_HEIGHT) lineBreakMode:NSLineBreakByWordWrapping];
    self.messageLabel.frame = CGRectMake(self.messageLabel.frame.origin.x,
                                     self.messageLabel.frame.origin.y,
                                     self.messageLabel.frame.size.width,
                                     ExpectedSize.height);


    timer = [NSTimer scheduledTimerWithTimeInterval:0.8 target:self selector:@selector(MessageLabelBlinking) userInfo:nil repeats:YES];

//    self.messageLabel.adjustsFontSizeToFitWidth = YES;

- (void)MessageLabelBlinking

    if (_blinking) 
        NSString *threeDot = @".....";
        if ([self.messageLabel.text rangeOfString:threeDot].location == NSNotFound) 
            self.messageLabel.text = [self.messageLabel.text stringByAppendingString:@"."];
        
        else
            self.messageLabel.text = [self.messageLabel.text stringByReplacingOccurrencesOfString:threeDot withString:@""];
        
    
    else
        [timer invalidate];
        timer = nil;
    

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect 
    // Drawing code

*/
//init the data processing block
- (void)initCustomOtherParameters

    self.dataManager.DelegateCustom = self;

    self.dataManager.blockProcessingData = ^(float allCommand, float restCommand)
        if (allCommand == 0) 
            [[SingletonOperationQueue mainQueue] addOperationWithBlock:^
                self.demoProgressView.progressView.progress = 1;
                NSString *messageStr = [NSLocalizedString(@"SuccessfulSynchronized", nil) stringByAppendingFormat:@"\n%@", NSLocalizedString(@"EmptyTherapy", nil)];
                [self setMessageLabelString:messageStr];
                _blinking = NO;
                self.CancelButton.alpha         = 1.0;
            ];
        
        else
            float percenAger = (restCommand/allCommand);
            percenAger = 1 - percenAger;

            [[SingletonOperationQueue mainQueue] addOperationWithBlock:^
                self.demoProgressView.progressView.progress = percenAger;
                if (restCommand == 0) 
                    [self downloadZipFile];
                
            ];
        
    ;

    [Patient RemovePatient];

    [self.singlePatient SavePatient];

- (void)showInView:(UIView *)view

    self.transform = CGAffineTransformScale(self.transform, 0.8, 0.8);

    self.containerButton.alpha = 0.0f;
    self.containerButton.frame = view.bounds;

    [view addSubview:self.containerButton];

    [UIView animateWithDuration:0.15 animations:^
        self.containerButton.alpha = 1.0f;
        self.transform = CGAffineTransformScale(self.transform, 1.4, 1.4);

    completion:^(BOOL finished) 
        if (finished) 
            [UIView animateWithDuration:0.20 animations:^
                self.transform = CGAffineTransformIdentity;
            ];

            [self sendConnectionTestRequest];
        
        else
            self.transform = CGAffineTransformIdentity;
            [self sendConnectionTestRequest];
        
    ];

- (void)dismissMenuPopover

    [self hide];

- (void)hide

    [UIView animateWithDuration:0.25 animations:^
        self.transform = CGAffineTransformScale(self.transform, 0.8, 0.8);
        self.containerButton.alpha = 0.0f;
    completion:^(BOOL finished) 
        if (finished) 
            [self.containerButton removeFromSuperview];
        
    ];


- (void)pressCancelButton:(UIButton *)button

    [self.dataManager CommunicationCancel];

    [self.delegateCustom SynchroningViewCancelButtonDidClicked:self];

- (void)startTherapyButtonDidClicked:(UIButton *)button

    [self.delegateCustom SynchroningViewStartTherapyButtonDidClicked:self];

- (void)pressConfirmButton:(UIButton *)button

    [UIView animateWithDuration:0.25 animations:^
        self.CancelButton.alpha  = 0.0;
        self.ConfirmButton.alpha = 0.0;
    ];



    [self sendRegistrationRequestConfirm:YES];

#pragma mark - Communication
- (void)sendConnectionTestRequest

    [self initCustomOtherParameters];

    _blinking = YES;

    [self setMessageLabelString:NSLocalizedString(@"StartRegistration", nil)];

    [self.dataManager sendRequestCommand:kConnectionTest];

- (void)sendRegistrationRequestConfirm:(BOOL)Confirm

    [self setMessageLabelString:NSLocalizedString(@"StartRegistration", nil)];
    NSString *registrationResult = [self.dataManager RegisterTheUSER_Confirm:Confirm];
    [self processLogInResult:registrationResult];

- (void)processLogInResult:(NSString *)logInResult

    if ([logInResult isEqual:@"1"]) 
        if ([self.dataManager DataInitialization]) 
            [self.dataManager DataInitializationForFakeUser];

            [self.singlePatient SavePatient];

            [self startTherapyButtonDidClicked:nil];
        
    
    else if ([logInResult isEqual:@"2"]) 
        if ([self.dataManager DataInitialization]) 
            self.singlePatient.record7_AuthenticatedUser = YES;

            [self.singlePatient SavePatient];

            [self.dataManager sendRequestCommand:kNewDataAvailable];
        ;
    
    else if ([logInResult intValue] == DEVICE_NOT_REGISTERED)
        logInResult = NSLocalizedString(@"105", nil);
        _blinking = NO;
        [self setMessageLabelString:logInResult];
        [UIView animateWithDuration:0.25 animations:^
            self.CancelButton.alpha  = 1.0;
            self.ConfirmButton.alpha = 1.0;
        ];
    
    else if ([logInResult isEqualToString:kNO])
        _blinking = NO;
        [self setMessageLabelString:@"Cannot find the Error "];
        [UIView animateWithDuration:0.25 animations:^
            self.CancelButton.alpha  = 1.0;
        ];
    
    else
        _blinking = NO;
        [self setMessageLabelString:logInResult];
        [UIView animateWithDuration:0.25 animations:^
            self.CancelButton.alpha  = 1.0;
        ];
    


- (void)downloadZipFile

    self.demoProgressView.progressView.progress = 1.0;

    sleep(0.4);

    [self setMessageLabelString:NSLocalizedString(@"Loading Data", nil)];
    self.demoProgressView.progressView.progress = 0.0;
    self.dataManager.blockProcessingDataPercentAge = ^(float percentAge, NSString *fileName)

        if ([fileName isEqualToString:kaderenzainfo]) 
            [[SingletonOperationQueue mainQueue] addOperationWithBlock:^
                self.demoProgressView.progressView.progress = percentAge;
                NSLog(@"%f", percentAge);
                if (percentAge == 0.5) 
                    sleep(0.4);
                    NSString *aderenzainfoFilePath =  [[NSUserDefaults standardUserDefaults] objectForKey:kaccertamentiinfo];
                    [self.dataManager downloadZipFile:aderenzainfoFilePath fileName:kaccertamentiinfo];
                
            ];
        
        else if ([fileName isEqualToString:kaccertamentiinfo])
            [[SingletonOperationQueue mainQueue] addOperationWithBlock:^
                self.demoProgressView.progressView.progress = 0.5 + percentAge;
                if (0.5 + percentAge >= 1.0) 

                    _blinking = NO;
                    self.CancelButton.alpha = 1.0;

                    if ([Patient getPatient].record_patientStatus == PatientStatusSuspendedType) 
                        [self setMessageLabelString:NSLocalizedString(@"SuspendedPatient", nil)];
                    
                    else if ([Patient getPatient].record_patientStatus == PatientStatusDeletedType)
                        [self setMessageLabelString:NSLocalizedString(@"DeletedPatient", nil)];
                    
                    else if ([Patient getPatient].record_patientStatus == PatientStatusActiveType)
                        [self setMessageLabelString:NSLocalizedString(@"SuccessfulSynchronized", nil)];
                        self.startTherapyButton.alpha = 1.0;
                    
                
            ];
        
    ;



    NSString *aderenzainfoFilePath =  [[NSUserDefaults standardUserDefaults] objectForKey:kaderenzainfo];
    [self.dataManager downloadZipFile:aderenzainfoFilePath fileName:kaderenzainfo];

#pragma mark - DataManagerDelegate
- (void)CommunicationConnectionTest:(BOOL)connected

    if (connected) 
        [self sendRegistrationRequestConfirm:NO];
    
    else
        [self setMessageLabelString:NSLocalizedString(@"connectionTestFail", nil)];
        [UIView animateWithDuration:0.25 animations:^
            self.CancelButton.alpha  = 1.0;
        ];
        _blinking = NO;
    

- (void)CommunicationSendRequestCommandName:(NSString *)commandName

    if ([commandName isEqualToString:kNewDataAvailable]) 
        [self setMessageLabelString:NSLocalizedString(@"Synchronizing", nil)];
    

- (void)CommunicationReceiveResponseWithOKCommandName:(NSString *)commandName



- (void)CommunicationReceiveResponseWithRequestError:(NSString *)commandName

    [self setMessageLabelString:NSLocalizedString(@"NetworkConnectionError", nil)];
    _blinking = NO;
    [UIView animateWithDuration:0.25 animations:^
        self.CancelButton.alpha  = 1.0;
    ];

- (void)CommunicationReceiveResponseCommandName:(NSString *)commandName WithErrorCode:(int)errorCode withErrorDescription:(NSString *)errorDescription

    [self setMessageLabelString:NSLocalizedString(@"NewDataAvaiableErrorCode", nil)];
    _blinking = NO;
    [UIView animateWithDuration:0.25 animations:^
        self.CancelButton.alpha  = 1.0;
    ];

-(void)CommunicationZipFileFailDownload

    [self setMessageLabelString:NSLocalizedString(@"ZipFileDownloadFail", nil)];
    _blinking = NO;
    [UIView animateWithDuration:0.25 animations:^
        self.CancelButton.alpha  = 1.0;
    ];

-(void)CommunicationZipFileIsNotReadAble

    [self setMessageLabelString:NSLocalizedString(@"ZipFileUnzippedFail", nil)];
    _blinking = NO;
    [UIView animateWithDuration:0.25 animations:^
        self.CancelButton.alpha  = 1.0;
    ];

@end

【问题讨论】:

这通常表明您有内存泄漏或仍在使用的已释放对象。您是否尝试过使用泄漏仪器? 是的,我试过了。但是如果我尝试使用模拟器,我没有任何问题。如果我尝试使用设备,当应用程序崩溃时,仪器会停止并且它不会告诉我是哪一部分代码导致了问题。似乎仪器从构建运行项目,而不是从代码。 您是否使用诸如保护 malloc、scribble 等调试选项配置了您的调试方案? 是的,我编辑了方案。几天来我一直在研究这个内存问题。但我仍然没有找到任何解决方案。项目完成,这是最后一个问题。内存错误太难找了,如果我能找到导致问题的代码部分就好了。 能贴出相关代码吗? 【参考方案1】:

我终于解决了

在我的加密和解密函数中,我有这个:

Byte *buffer    = (Byte*)malloc(asciiDataLength);

用缓冲区处理后,我将其转换为 NSData:

NSData *plainData = [NSData dataWithBytesNoCopy:buffer length:asciiDataLength freeWhenDone:YES];

此代码导致我的应用程序不断崩溃,我将其更改为

NSData *plainData = [NSData dataWithBytes:buffer length:asciiDataLength];
free(buffer);

然后我的应用再也不会崩溃了。

所以我必须自己释放字节,ARC不会为我释放它。

【讨论】:

ARC 适用于 objects,而不是“C”级函数,例如 malloc。 SO上有一些解决方案不使用malloc,只使用NSMutableData【参考方案2】:

模拟器的内存 = PC 的 RAM,即 4 或 6 GB 等。 并且设备最大只有 1 GB。在Xcode中运行app时,需要点击Debug Navigator,然后点击Memory,会显示app的内存消耗情况。

要查看代码中的内存泄漏 - 您必须使用内存工具,例如 Instruments。

【讨论】:

以上是关于iOS - 如果仅在设备上运行,我的应用程序会因内存错误而崩溃的主要内容,如果未能解决你的问题,请参考以下文章

仅在物理设备而非模拟器上运行 Android 应用程序

IOS模拟器问题应用程序仅在第二次启动时运行..

Flutter 无法仅在物理 iOs 设备中构建和运行 iOS 应用程序

我可以在 Xcode Server 中的所有设备上运行单元测试并仅在 iOS 9.x 设备上运行 UI 测试吗?

设备检测是不是是android?

确保 iOS 4 应用程序仅在设备满足特定密码条件时运行