I've spent the better part of a day and half now trying to debug this issue I'm seeing when trying to unarchive a Data blob stored locally (this issue also appears when retrieving it via iCloud, but since they run through the same codepath, I assume they're related).
Background
I originally built this app four years ago, and for reasons that have since been lost to time (but probably because I was more of a novice back then), I relied on the AutoCoding library to get objects in my data model to automagically adopt NSCoding (although I did implement the protocol myself in some places -- like I said, I was an novice) and FCFileManager for saving these objects to the local documents directory. The data model itself is fairly simple: custom NSObjects that have various properties of NSString, NSArray, and other custom NSObject classes (but I will note there are a number of circular references; most of them declared as strong and nonatomic in header files). This combination has (and still does) work well in the production version of the app.
However, in a future update, I'm planning on adding saving/loading files from iCloud. While I've been building that out, I've been looking to trim down my list of third-party dependencies and update older code to iOS 13+ APIs. It so happens that FCFileManager relies on the now-deprecated +[NSKeyedUnarchiver unarchiveObjectWithFile:] and +[NSKeyedArchiver archiveRootObject:toFile:], so I've focused on rewriting what I need from that library using more modern APIs.
I was able to get saving files working pretty easily using this:
#objc static func save(_ content: NSCoding, at fileName: String, completion: ((Bool, Error?) -> ())?) {
CFCSerialQueue.processingQueue.async { // my own serial queue
measureTime(operation: "[LocalService Save] Saving") { // just measures the time it takes for the logic in the closure to process
do {
let data: Data = try NSKeyedArchiver.archivedData(withRootObject: content, requiringSecureCoding: false)
// targetDirectory here is defined earlier in the class as the local documents directory
try data.write(to: targetDirectory!.appendingPathComponent(fileName), options: .atomicWrite)
if (completion != nil) {
completion!(true, nil)
}
} catch {
if (completion != nil) {
completion!(false, error)
}
}
}
}
}
And this works great -- pretty fast and can still be loaded by FCFileManager's minimal wrapper around +[NSKeyedUnarchiver unarchiveObjectWithFile:].
Problem
But loading this file back from the local documents directory has proved to be a massive challenge. Here's what I'm working with right now:
#objc static func load(_ fileName: String, completion: #escaping ((Any?, Error?) -> ())) {
CFCSerialQueue.processingQueue.async {// my own serial queue
measureTime(operation: "[LocalService Load] Loading") {
do {
// targetDirectory here is defined earlier in the class as the local documents directory
let combinedUrl: URL = targetDirectory!.appendingPathComponent(fileName)
if (FileManager.default.fileExists(atPath: combinedUrl.path)) {
let data: Data = try Data(contentsOf: combinedUrl)
let obj: Any? = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)
completion(obj, nil)
} else {
completion(nil, ServiceError.generic(message: "Data not found at URL \(combinedUrl)"))
}
} catch {
completion(nil, error)
}
}
}
}
I've replaced FCFileManager's +[NSKeyedUnarchiver unarchiveObjectWithFile:] with the new +[NSKeyedUnarchiver unarchiveTopLevelObjectWithData:], but I'm running into EXC_BAD_ACCESS code=2 crashes when getting execution flowing through that line. The stacktrace is never particularly helpful; it's usually ~1500 frames long and jumping between various custom -[NSObject initWithCoder:] implementations. Here's an example (comments added for context, clarity, and conciseness):
#implementation Game
#synthesize AwayKStats,AwayQBStats,AwayRB1Stats,AwayRB2Stats,AwayWR1Stats,AwayWR2Stats,AwayWR3Stats,awayTOs,awayTeam,awayScore,awayYards,awayQScore,awayStarters,gameName,homeTeam,hasPlayed,homeYards,HomeKStats,superclass,HomeQBStats,HomeRB1Stats,HomeRB2Stats,homeStarters,HomeWR1Stats,HomeWR2Stats,HomeWR3Stats,homeScore,homeQScore,homeTOs,numOT,AwayTEStats,HomeTEStats, gameEventLog,HomeSStats,HomeCB1Stats,HomeCB2Stats,HomeCB3Stats,HomeDL1Stats,HomeDL2Stats,HomeDL3Stats,HomeDL4Stats,HomeLB1Stats,HomeLB2Stats,HomeLB3Stats,AwaySStats,AwayCB1Stats,AwayCB2Stats,AwayCB3Stats,AwayDL1Stats,AwayDL2Stats,AwayDL3Stats,AwayDL4Stats,AwayLB1Stats,AwayLB2Stats,AwayLB3Stats,homePlays,awayPlays,playEffectiveness, homeStarterSet, awayStarterSet;
-(id)initWithCoder:(NSCoder *)aDecoder {
self = [super init];
if (self) {
// ...lots of other decoding...
// stack trace says the BAD_ACCESS is flowing through these decoding lines
// #property (atomic) Team *homeTeam;
homeTeam = [aDecoder decodeObjectOfClass:[Team class] forKey:#"homeTeam"];
// #property (atomic) Team *awayTeam;
// there's no special reason for this line using a different decoding method;
// I was just trying to test out both
awayTeam = [aDecoder decodeObjectForKey:#"awayTeam"];
// ...lots of other decoding...
}
return self;
}
Each Game object has a home and away Team; each team has an NSMutableArray of Game objects called gameSchedule, defined as such:
#property (strong, atomic) NSMutableArray<Game*> *gameSchedule;
Here's Team's initWithCoder: implementation:
-(id)initWithCoder:(NSCoder *)coder {
self = [super initWithCoder:coder];
if (self) {
if (teamHistory.count > 0) {
if (teamHistoryDictionary == nil) {
teamHistoryDictionary = [NSMutableDictionary dictionary];
}
if (teamHistoryDictionary.count < teamHistory.count) {
for (int i = 0; i < teamHistory.count; i++) {
[teamHistoryDictionary setObject:teamHistory[i] forKey:[NSString stringWithFormat:#"%ld",(long)([HBSharedUtils currentLeague].baseYear + i)]];
}
}
}
if (state == nil) {
// set the home state here
}
if (playersTransferring == nil) {
playersTransferring = [NSMutableArray array];
}
if (![coder containsValueForKey:#"projectedPollScore"]) {
if (teamOLs != nil && teamQBs != nil && teamRBs != nil && teamWRs != nil && teamTEs != nil) {
FCLog(#"[Team Attributes] Adding Projected Poll Score to %#", self.abbreviation);
projectedPollScore = [self projectPollScore];
} else {
projectedPollScore = 0;
}
}
if (![coder containsValueForKey:#"teamStrengthOfLosses"]) {
[self updateStrengthOfLosses];
}
if (![coder containsValueForKey:#"teamStrengthOfSchedule"]) {
[self updateStrengthOfSchedule];
}
if (![coder containsValueForKey:#"teamStrengthOfWins"]) {
[self updateStrengthOfWins];
}
}
return self;
}
Pretty simple other than for the backfilling of some properties. However, this class imports AutoCoding, which hooks into -[NSObject initWithCoder:] like so:
- (void)setWithCoder:(NSCoder *)aDecoder
{
BOOL secureAvailable = [aDecoder respondsToSelector:#selector(decodeObjectOfClass:forKey:)];
BOOL secureSupported = [[self class] supportsSecureCoding];
NSDictionary *properties = self.codableProperties;
for (NSString *key in properties)
{
id object = nil;
Class propertyClass = properties[key];
if (secureAvailable)
{
object = [aDecoder decodeObjectOfClass:propertyClass forKey:key]; // where the EXC_BAD_ACCESS seems to be coming from
}
else
{
object = [aDecoder decodeObjectForKey:key];
}
if (object)
{
if (secureSupported && ![object isKindOfClass:propertyClass] && object != [NSNull null])
{
[NSException raise:AutocodingException format:#"Expected '%#' to be a %#, but was actually a %#", key, propertyClass, [object class]];
}
[self setValue:object forKey:key];
}
}
}
- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
[self setWithCoder:aDecoder];
return self;
}
I did some code tracing and found that execution flows the -[NSCoder decodeObject:forKey:] call above. Based on some logging I added, it seems like propertyClass somehow gets deallocated before getting passed to -[NSCoder decodeObjectOfClass:forKey:]. However, Xcode shows that propertyClass has a value when the crash occurs (see screenshot: https://imgur.com/a/J0mgrvQ)
The property in question in that frame is defined:
#property (strong, nonatomic) Record *careerFgMadeRecord;
and has the following properties itself:
#interface Record : NSObject
#property (strong, nonatomic) NSString *title;
#property (nonatomic) NSInteger year;
#property (nonatomic) NSInteger statistic;
#property (nonatomic) Player *holder;
#property (nonatomic) HeadCoach *coachHolder;
// … some functions
#end
This class also imports AutoCoding, but has no custom initWithCoder: or setWithCoder: implementation.
Curiously, replacing the load method I’ve written with FCFileManager’s version also crashes in the same fashion, so this could be more of an issue with how the data is being archived than how it’s being retrieved. But again, this all works fine when using FCFileManager’s methods to load/save files, so my guess is that there’s some lower-level difference between the implementation of archives in iOS 11 (when FCFileManager was last updated) and iOS 12+ (when the NSKeyedArchiver APIs were updated).
Per some suggestions I've found online (like this one), I also tried this:
#objc static func load(_ fileName: String, completion: #escaping ((Any?, Error?) -> ())) {
CFCSerialQueue.processingQueue.async {
measureTime(operation: "[LocalService Load] Loading") {
do {
let combinedUrl: URL = targetDirectory!.appendingPathComponent(fileName)
if (FileManager.default.fileExists(atPath: combinedUrl.path)) {
let data: Data = try Data(contentsOf: combinedUrl)
let unarchiver: NSKeyedUnarchiver = try NSKeyedUnarchiver(forReadingFrom: data)
unarchiver.requiresSecureCoding = false;
let obj: Any? = try unarchiver.decodeTopLevelObject(forKey: NSKeyedArchiveRootObjectKey)
completion(obj, nil)
} else {
completion(nil, ServiceError.generic(message: "Data not found at URL \(combinedUrl)"))
}
} catch {
completion(nil, error)
}
}
}
}
However, this still throws the same EXC_BAD_ACCESS when trying to decode.
Does anyone have any insight into where I might be going wrong here? I’m sure it’s something simple, but I just can’t seem to figure it out. I have no problem providing more source code if needed to dive deeper.
Thanks for your help!
I (somehow) got past this by relying on AutoCoding to manage the NSCoding implementation for the Game class. Peeling back the layers, it seems like there was some issue with using -[NSMutableArray arrayWithObjects:] in -[Game initWithCoder:] that caused the EXC_BAD_ACCESS, but even that seemed to go away when falling back to AutoCoding. Not sure what effect this will have on backwards compatibility, but I guess I'll cross that bridge when I get there.
I have a service that I'm currently writing a unit test for. The code works as expected, but I'm getting a strange retain cycle warning.
[self.myService doSomethingCoolWithCompletionBlock:^(MyResponseObject *obj) {
XCTAssertNil(obj, #"obj should be nil");
}];
The XCTAssertNil(obj, #"obj should be nil"); line shows a warning in Xcode Capturing 'self' strongly in this block is likely to lead to a retain cycle.
If I change the code to the following, the warning is removed:
__weak MyService *weakService = self.myService;
[weakService doSomethingCoolWithCompletionBlock:^(MyResponseObject *obj) {
XCTAssertNil(obj, #"obj should be nil");
}];
I am using self.someService in other unit tests, and never had this issue. Anyone experienced this before?
EDIT
I have another test that has the following:
[self.myService doSomethingElseCoolWithCompletionBlock:(NSArray *results) {
XCTestAssertNotNil(results, #"results should not be nil");
}];
This doesn't give me a warning. The only difference I see is that this is checking an array, and the other is checking an object of a specific type.
assert it is macros and used self inside.
so you need create local variable with name self.
__weak id weakSelf = self;
self.fooBlock = ^{
id self = weakSelf;
XCTAssert(YES);
};
Don't do this:
#interface MyCoolTests : XCTestCase
#property (retain) id myService;
#end
#implementation MyCoolTests
-(void)testCoolness{
self.myService = [MyService new];
self.myService.callback = ^{
XCTAssert(YES);
};
// ...
}
#end
Do this:
#interface MyCoolTests : XCTestCase
#end
#implementation MyCoolTests
-(void)testCoolness{
id myService = [MyService new];
myService.callback = ^{
XCTAssert(YES);
};
// ...
}
#end
It's a limitation of XCTTestCase and it probably catches people when using the setup method.
I can not compile this code:
[verify(mockedContext) deleteObject:item1];
[verify(mockedContext) deleteObject:item2];
[verify(mockedContext) save:anything()];<--compilation error for conversion id to NSError**
However I'm able to pass compilation in similar case with given macros with additional syntax:
[[given([mockedContext save:nil]) withMatcher:anything()] willReturn:nil];
Are there anything to help me pass compilation with verify?
Here is compilation error:
Implicit conversion of an Objective-C pointer to 'NSError *__autoreleasing *' is disallowed with ARC
I assume the save: method on the 'mockedContext' takes a pointer-to-pointer to NSError.
So actually, the NSError must be seen as an extra return value of the save:method. This means that you should rather setup an expectation in the first place.
I worked out a small example:
We start with the Context protocol with a simple method taking an NSError**.
#protocol Context <NSObject>
- (id)doWithError:(NSError *__autoreleasing *)err;
#end
Next is a class using this protocol, much like your SUT. I called it ContextUsingClass
#interface ContextUsingClass : NSObject
#property (nonatomic, strong) id<Context> context;
#property BOOL recordedError;
- (void)call;
#end
#implementation ContextUsingClass
- (void)call {
NSError *error;
[self.context doWithError:&error];
if (error) {
self.recordedError = YES;
}
}
#end
As you can see, when the context method doWithError: returns an error, the recordedError property is set to YES. This is something we can expect to be true or false in our test. The only problem is, how do we tell the mock to result in an error (or to succeed without error)?
The answer is fairly straight forward, and was almost part of your question: we pass an OCHamcrest matcher to the given statement, which in turn will set the error for us through a block. Bear with me, we'll get there. Let's first write the fitting matcher:
typedef void(^ErrorSettingBlock)(NSError **item);
#interface ErrorSettingBlockMatcher : HCBaseMatcher
#property (nonatomic, strong) ErrorSettingBlock errorSettingBlock;
#end
#implementation ErrorSettingBlockMatcher
- (BOOL)matches:(id)item {
if (self.errorSettingBlock) {
self.errorSettingBlock((NSError * __autoreleasing *)[item pointerValue]);
}
return YES;
}
#end
This matcher will call the errorSettingBlock if it has been set, and will always return YES as it accepts all items. The matchers sole purpose is to set the error, when the test asks as much. From OCMockito issue 22 and it's fix, we learn that pointer-to-pointers are wrapped in NSValue objects, so we should unwrap it, and cast it to our well known NSError **
Now finally, here is how the test looks:
#implementation StackOverFlowAnswersTests {
id<Context> context;
ContextUsingClass *sut;
ErrorSettingBlockMatcher *matcher;
}
- (void)setUp {
[super setUp];
context = mockProtocol(#protocol(Context));
sut = [[ContextUsingClass alloc] init];
sut.context = context;
matcher = [[ErrorSettingBlockMatcher alloc] init];
}
- (void)testContextResultsInError {
matcher.errorSettingBlock = ^(NSError **error) {
*error = [NSError errorWithDomain:#"dom" code:-100 userInfo:#{}];
};
[[given([context doWithError:nil]) withMatcher:matcher] willReturn:nil];
[sut call];
assertThatBool(sut.recordedError, is(equalToBool(YES)));
}
- (void)testContextResultsInSuccess {
[[given([context doWithError:nil]) withMatcher:matcher] willReturn:nil];
[sut call];
assertThatBool(sut.recordedError, is(equalToBool(NO)));
}
#end
Conclusion
When you call methods within your SUT which are returning errors through pointer-to-pointers, you should probably test for the different possible outcomes, rather than just verifying if the method has been called.
If your SUT is ignoring the error, then let the block you pass into the matcher keep a boolean flag to indicate that it was called like so:
- (void)testNotCaringAboutTheError {
__block BOOL called = NO;
matcher.errorSettingBlock = ^(NSError **error) {
called = YES;
};
[[given([context doWithError:nil]) withMatcher:matcher] willReturn:nil];
[sut call];
assertThatBool(called, is(equalToBool(YES)));
}
Or with simple verification:
- (void)testWithVerifyOnly {
[sut call];
[[verify(context) withMatcher:matcher] doWithError:nil];
}
PS: Ignoring errors is probably something you don't want to do...
As Apple encourages the usage of blocks, and i wanted to do a series of animations, with sound output in between them which is basicly like a todolist, i wanted to implement this using blocks.
unfortunatly AVAudiosplayer doesnt appear to support onCompletion blocks, in the manner UIAnimation does.
So i thought it would be cool to add that support to the AVAudioplayer.
so what ive dont is this
header
#import <AVFoundation/AVFoundation.h>
#interface AVAudioPlayer (AVAudioPlayer_blockSupport)
typedef void(^AVPlaybackCompleteBlock)(void);
#property (nonatomic, copy) AVPlaybackCompleteBlock block;
-(id)initWithContentsOfURL:(NSURL*)pathURL error:(NSError**)error onCompletion:(AVPlaybackCompleteBlock) block;
-(void)setBlock:(AVPlaybackCompleteBlock)block;
-(AVPlaybackCompleteBlock)block;
-(void) executeBlock;
#end
and the m file
#import "AVAudioPlayer+blocks.h"
#implementation AVAudioPlayer (AVAudioPlayer_blockSupport)
-(id)initWithContentsOfURL:(NSURL *)pathURL error:(NSError **)error onCompletion:(AVPlaybackCompleteBlock )block {
self = [[AVAudioPlayer alloc] initWithContentsOfURL:pathURL error:error];
self.block = block;
return self;
}
-(void)setBlock:(AVPlaybackCompleteBlock)block {
self.block = block;
}
-(AVPlaybackCompleteBlock)block {
return self.block;
}
-(void) executeBlock {
if (self.block != NULL) {
self.block();
}
}
#end
after doing this, i thought i should be able to create a new audioplayer like this:
player = [[AVAudioPlayer alloc] initWithContentsOfURL:pathURL error:&error onCompletion:block];
this appears to be working.
now in the delegate will try to execute the block attached.
if (localPlayer.block) {
[localPlayer executeBlock];
}
unfortunately when i try to run the code, it appears to be looping infinitely. I wanted to use synthesize instead, but thats not for category use...
If i dont implement that Method im stuck with '-[AVAudioPlayer setBlock:]: unrecognized selector sent to instance which makes sense, since there is no method with that name.
i found this Block references as instance vars in Objective-C so i thought i should be able to attach the additional property(my Block) to the AudioPlayer.
I figured it out, i needed to use
objc_setAssociatedObject(self, &defaultHashKey, blocked, OBJC_ASSOCIATION_COPY_NONATOMIC);
to store and access the property. maybe thats what jere meant with, i have tot ake care of the memory management myself.
-(void)setBlock:(AVPlaybackCompleteBlock)blocked {
objc_setAssociatedObject(self, &defaultHashKey, blocked, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(AVPlaybackCompleteBlock)block {
return objc_getAssociatedObject(self, &defaultHashKey) ;
}
I'm currently bringing a legacy project up from iOS 5/6 to iOS 6/7.
Part of this project involves taking a picture using the GPUImage library, processing it with a crop filter, then optionally adding some saturation and blur effects. I am currently using version 0.1.2 installed via cocoa pods.
The problem I am having is that when I try to capture an image from the camera, I hit the following assert in GPUImageStillCamera.m line 254
if (CVPixelBufferGetPlaneCount(cameraFrame) > 0)
{
NSAssert(NO, #"Error: no downsampling for YUV input in the framework yet");
}
where cameraFrame is a CVImageBufferRef
I have reproduced the code where this is called and move it to another project, where it works perfectly.
Once I moved this reproduced class back into the main project, I was hitting the assert every time.
Things I've ruled out with my own debugging
64bit (it’s happening on both)
different lib version
initial object setup / code / usage
This has lead me to believe that perhaps it might be a project setting that I've over looked. Any help or even a pointer in the right direction would be very very welcome. I've spent a good 1-2 days on this now and am still entirely lost!
I've included the stripped down class below which shows the general use.
#import "ViewController.h"
#import "GPUImage.h"
#import "ImageViewController.h"
#interface ViewController ()
#property (nonatomic, strong) IBOutlet GPUImageView *gpuImageView;
#property (nonatomic, strong) GPUImageStillCamera *camera;
#property (nonatomic, strong) GPUImageCropFilter *cropFilter;
#end
#implementation ViewController
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self setupCameraCapture];
}
- (void)setupCameraCapture
{
if (self.camera) {
return;
}
self.cropFilter = [[GPUImageCropFilter alloc] initWithCropRegion:CGRectMake(0, 0, 1, 0.5625)];
if ([UIImagePickerController isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear]) {
self.camera = [[GPUImageStillCamera alloc] initWithSessionPreset:AVCaptureSessionPresetPhoto cameraPosition:AVCaptureDevicePositionBack];
}
else {
self.camera = [[GPUImageStillCamera alloc] initWithSessionPreset:AVCaptureSessionPresetPhoto cameraPosition:AVCaptureDevicePositionFront];
}
self.camera.outputImageOrientation = UIInterfaceOrientationPortrait;
NSError *error = nil;
[self.camera.inputCamera lockForConfiguration:&error];
[self.camera.inputCamera setExposureMode:AVCaptureExposureModeContinuousAutoExposure];
[self.camera.inputCamera setWhiteBalanceMode:AVCaptureWhiteBalanceModeContinuousAutoWhiteBalance];
if ([self.camera.inputCamera respondsToSelector:#selector(isLowLightBoostSupported)]) {
BOOL isSupported = self.camera.inputCamera.isLowLightBoostSupported;
if (isSupported) {
[self.camera.inputCamera setAutomaticallyEnablesLowLightBoostWhenAvailable:YES];
}
}
[self.camera.inputCamera unlockForConfiguration];
[self.camera addTarget:self.cropFilter];
[self.cropFilter addTarget:self.gpuImageView];
[self.camera startCameraCapture];
}
- (IBAction)capturePressed:(id)sender
{
[self.camera capturePhotoAsImageProcessedUpToFilter:self.cropFilter withCompletionHandler:^(UIImage *image, NSError *error) {
// do something with the image here
}];
}
#end
The actual culprit was a swizzled method found by my colleague Marek. Hidden away in the depths of the old codebase. The above code works fine.
Lesson: if you really have to swizzle something, make sure you leave proper documentation for the future devs.