神刀安全网

iOS — 使用runtime解决3D Touch导致UIImagePicker崩溃的问题

UIImagePickerController是iOS中自带的系统相册选择器, 使用起来非常简便.

UIImagePickerController

UIImagePickerController *imagePickerController = [[UIImagePickerController alloc] init];
// 设置sourceType为系统相册, 如果使用Camera请对应修改该属性.
imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
imagePickerController.allowsEditing = YES;
imagePickerController.delegate = self;
[self presentViewController:imagePickerController animated:YES completion:nil];

UIImagePickerController本身继承自UINavigationController, 因此不能将其简单视作一个UIViewController来看待.了UINavigationControllerDelegate和UIImagePickerControllerDelegate协议, 包含了由三个ViewController组成的导航视图, 我们通过打印其viewControllers即可看出来:

(lldb) po [picker viewControllers]
<__NSArrayI 0x146b31140>(
<PUUIAlbumListViewController: 0x14609a200>,
<PUUIPhotosAlbumViewController: 0x1458e5a00>,
<PUUIImageViewController: 0x1468f1ad0>
)

UIImagePickerControllerDelegate

UIImagePickerControllerDelegate协议有如下三个代理方法.

__TVOS_PROHIBITED @protocol UIImagePickerControllerDelegate<NSObject>
@optional
// The picker does not dismiss itself; the client dismisses it in these callbacks.
// The delegate will receive one or the other, but not both, depending whether the user
// confirms or cancels.
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingImage:(UIImage *)image editingInfo:(nullable NSDictionary<NSString *,id> *)editingInfo NS_DEPRECATED_IOS(2_0, 3_0);
- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info;
- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker;

其中info包含了所选取照片的基本信息, 如下:

// info dictionary keys
UIKIT_EXTERN NSString *const UIImagePickerControllerMediaType __TVOS_PROHIBITED; // an NSString (UTI, i.e. kUTTypeImage)
UIKIT_EXTERN NSString *const UIImagePickerControllerOriginalImage __TVOS_PROHIBITED; // a UIImage
UIKIT_EXTERN NSString *const UIImagePickerControllerEditedImage __TVOS_PROHIBITED; // a UIImage
UIKIT_EXTERN NSString *const UIImagePickerControllerCropRect __TVOS_PROHIBITED; // an NSValue (CGRect)
UIKIT_EXTERN NSString *const UIImagePickerControllerMediaURL __TVOS_PROHIBITED; // an NSURL
UIKIT_EXTERN NSString *const UIImagePickerControllerReferenceURL NS_AVAILABLE_IOS(4_1) __TVOS_PROHIBITED; // an NSURL that references an asset in the AssetsLibrary framework
UIKIT_EXTERN NSString *const UIImagePickerControllerMediaMetadata NS_AVAILABLE_IOS(4_1) __TVOS_PROHIBITED; // an NSDictionary containing metadata from a captured photo
UIKIT_EXTERN NSString *const UIImagePickerControllerLivePhoto NS_AVAILABLE_IOS(9_1) __TVOS_PROHIBITED; // a PHLivePhoto
}

当在系统相册中选取了照片之后, 会调用imagePickerController:didFinishPickingMediaWithInfo:方法, 在其中可以添加对该照片的一些操作, 如写入相册等.

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary<NSString *,id> *)info {
UIImage *originalImage = info[UIImagePickerControllerOriginalImage];
UIImage *editedImage = info[UIImagePickerControllerEditedImage];
UIImage *savedImage = editedImage ?: originalImage;

if (picker.sourceType == UIImagePickerControllerSourceTypeCamera) {
UIImageWriteToSavedPhotosAlbum(savedImage, nil, nil, nil);
}

__weak ViewController *weakSelf = self;
[picker dismissViewControllerAnimated:YES completion:^{
weakSelf.imageView.image = savedImage;
}];
}

并且UIImagePickerController自身并不会dismiss, 需要我们自己调用, 并且通过回调的方式将照片进行相应的处理.上边的代码, 即在UIImagePickerController的dismiss操作之后, 将照片放置到UIImageView中.

自定义UIImagePickerController

创建一个UIViewController, 继承自UIImagePickerController, 并实现相关协议方法, 即可以自定义一个相册选择器.

不过实际上依然只是对UIImagePickerController进行了一次包装而已.

请参考demo:

https://github.com/icetime17/Playground/tree/master/DemoUIImagePicker

另外, 可以使用PhotoKit框架, 结合UICollectionView来实现真正意义上的自定义相册.

参考博客:

iOS — 使用PhotoKit代替ALAssetsLibrary来管理相册资源

问题

3D Touch

3D Touch是iPhone 6s/6splus设备才有的特点, 在系统相册中长按一个照片, 可触发3D Touch相关的操作.而在没有3D Touch的设备中, 在系统相册中长按一个照片, 会导致crash. 这看起来像是iOS系统的一个bug.

原因在于:触发3D Touch操作后, PUPhotosGridViewController的previewingContext:viewControllerForLocation:未实现, 所以导致crash.

解决方法:

使用runtime来实现method swizzling, 即在runtime中将该方法替换.

使用method_exchangeImplementations(originalMethod, replacementMethod);方法即可实现.

首先, 封装一个方法用于实现method swizzling

- (void)replaceSelectorForClass:(Class)cls
SelectorOriginal:(SEL)original
SelectorReplace:(SEL)replacement
withBlock:(id)block {

IMP implementation = imp_implementationWithBlock(block);
Method originalMethod = class_getInstanceMethod(cls, original);
class_addMethod(cls, replacement, implementation, method_getTypeEncoding(originalMethod));
Method replacementMethod = class_getInstanceMethod(cls, replacement);
if (class_addMethod(cls, original, method_getImplementation(replacementMethod), method_getTypeEncoding(replacementMethod))) {
class_replaceMethod(cls, replacement, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, replacementMethod);
}

}

在AppDelegate的application:didFididFinishLaunchingWithOptions:方法中, 进行method swizzling:

- (void)preventImagePickerCrashOn3DTouch {
// Load PhotosUI and bail if 3D Touch is unavailable.
// (UIViewControllerPreviewing may be redundant,
// as PUPhotosGridViewController only seems to exist on iOS 9,
// but I'm being cautious.)
NSString *photosUIPath = @"/System/Library/Frameworks/PhotosUI.framework";
NSBundle *photosUI = [NSBundle bundleWithPath:photosUIPath];
Class photosClass = [photosUI classNamed:@"PUPhotosGridViewController"];
if (!(photosClass && objc_getProtocol("UIViewControllerPreviewing"))) {
return;
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"

SEL selector = @selector(ab_previewingContext:viewControllerForLocation:);
[self replaceSelectorForClass:photosClass
SelectorOriginal:@selector(previewingContext:viewControllerForLocation:)
SelectorReplace:selector
withBlock:^UIViewController *(id self, id<UIViewControllerPreviewing> previewingContext, CGPoint location) {

// Default implementation throws on iOS 9.0 and 9.1.
@try {
MTLog(@"Replace method at runtime to prevent UIImagePicker crash on 3D Touch.");
return ((UIViewController *(*)(id, SEL, id, CGPoint))objc_msgSend)(self, selector, previewingContext, location);
} @catch (NSException *e) {
return nil;
}
}];

#pragma clang diagnostic pop
}

这样, 即可成功解决3D Touch导致系统的UIImagePickerController崩溃的问题.

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » iOS — 使用runtime解决3D Touch导致UIImagePicker崩溃的问题

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮