iOS --- 使用runtime解决3D Touch导致UII

作者: icetime17 | 来源:发表于2016-03-21 20:12 被阅读477次

    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内容, 请参考博客:
    http://blog.csdn.net/icetime17

    相关文章

      网友评论

      • feng_dev:NSAllocateMemoryPages(15129172) failed 调用了相机后 上传图片 ,然后报错,求解
        icetime17:@Developer_峰 这个就不太清楚了诶. 目前还没有做过上传图片的内容呢.
      • feng_dev:UIImagePickerControllerReferenceURL
        比如这个 key 是不是 必须导入一个 assets library 框架 才有效啊,这个url 上传图片的时候 管用吗,我试了不行,不知道我试的对不
        icetime17:@Developer_峰 嗯嗯。估计用photokit也是类似的URL吧
        feng_dev:@icetime17 可是 现在 assets library 这个框架 不都放弃了吗? 怎么还通过这个框架来获取图片
        icetime17:@Developer_峰 这个key不用AssetsLibrary啊, 不过说了是通过AssetsLibrary来获取相册图片的时候, 这个图片对应的url. 上传图片时候使用这个应该不行的吧. 不过我没有试过呢, 大概猜测的.
      • feng_dev:大神,能具体解释下 图片选择的info里面的信息吗? 那些key 具体代表的是啥,能获取什么啊
        feng_dev:@icetime17 十分感谢!!
        icetime17:@Developer_峰 打印info的信息, 可以看到啊.
        (lldb) po info
        {
        UIImagePickerControllerMediaType = "public.image”;
        UIImagePickerControllerOriginalImage = "<UIImage: 0x7fe752f03bc0> size {1472, 2208} orientation 0 scale 1.000000”;
        UIImagePickerControllerReferenceURL = "assets-library://asset/asset.JPG?id=20C77916-4A13-486C-ABAC-9D2AF6049355&ext=JPG”; // an NSURL that references an asset in the AssetsLibrary framework
        }
      • feng_dev:UIImagePickerControllerDelegate
        这个代理方法 第一个 放弃了,第三个好像不写也行吗,只有第二个有用了
        icetime17:通常使用第二个即可。第三个用于在照片选取页点击取消的delegate方法

      本文标题:iOS --- 使用runtime解决3D Touch导致UII

      本文链接:https://www.haomeiwen.com/subject/ytwklttx.html