zhing 2016-10-19T13:07:27+00:00 zqwillseven@gmail.com 迷之“导航栏” 2016-10-12T00:00:00+00:00 zhing http://zhing.github.com/2016/10/12/迷之“导航栏” 对于初学者来说,IOS中的导航栏确实是一个让人困惑的知识点,我开始也是在项目预定的框架 下去设置导航栏的一些属性,直到我负责的模块在IOS10中出现了导航栏的bug的时候才不得不 去好好消化这一块,去理解系统在导航切换时的一些特效,去调研市面上常用APP对于导航栏 的一些处理,这里我就来分解一下导航栏的奥秘。

导航栏基础

我们见到的导航栏大致由两部分组成,分别是navigationBar和navigationItem。

  • navigationBar

豪无疑问navigationBar是属于navigationController的一部分,其不属于单个的UIViewController 我们在同一个navigationController中push或者pop的时候,看到的Bar其实都是同一个,也就是 说,在一个ViewController中修改了Bar在其它的ViewController中也是可见的。

在考虑设置导航栏的时候,我们通常也是一起设置了App的状态栏(也就是电池条),IOS有两个常用的 状态栏(statusBarStyle)属性,分别是:

// Dark content, for use on light backgrounds
UIStatusBarStyleDefault         = 0,
// Light content, for use on dark backgrounds
UIStatusBarStyleLightContent     NS_ENUM_AVAILABLE_IOS(7_0) = 1,

导航栏始终处于状态栏的下方,我们可以看到几乎所有的App导航栏和状态栏的颜色 都相同,这样的效果无法通过设置导航栏的backgroundColor得到,因为导航栏y坐标为20, 状态栏,设置了导航栏的背景色不会影响到状态栏。但是神奇的是,导航栏上面还有一层 View是navigationBarBackground(设置barTintColor将改变此背景色),其y坐标为-20,刚好把状态栏覆盖,所以我们使用setBackgroundImage就可以保证导航栏与状态栏同色,如下:

UIImage *colorImage = [UIImage imageWithColor:[UIColor clearColor] size:CGSizeMake(1, 1)];
[navc.navigationBar setBackgroundImage:colorImage forBarMetrics:UIBarMetricsDefault];
[navc.navigationBar setShadowImage:colorImage];
  • navigationItem

我们强调了navigationBar是属于navigationController的一部分,那么navigationItem 却是属于UIViewCOntroller的一部分,这一点在刚开始很容易造成困惑,这些个Item明明 就在Bar上,为什么偏偏属于ViewController,但是事实就是这样。navigationItem由三部分 组成,分别是:

  1. titleView

这又是一个让我们疑惑的属性,按照常理我们认为这是一个UILabel,通过设置navigationItem. titleView就是可以搞定title的一切属性,但是事实是我们想错了,这个属性默认为空。所以 当我们要单独考虑一个viewController的title的时候,就需要为其设置一个UILabel作为 titleView了,如下所示:

- (void)setTitle:(NSString *)title titleColor:(UIColor *)color{
UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 100, 44)];
titleLabel.text     = title;
titleLabel.font     = [UIFont boldSystemFontOfSize:20.f];
titleLabel.textAlignment  = NSTextAlignmentCenter;
titleLabel.textColor    = color;
self.navigationItem.titleView = titleLabel;
}
  1. leftNavigationItem

同上,其默认也为空(虽然系统会默认生成“返回”,但这个属性值依然为空)。所以我们一般 会设置可控的leftNavigationItem,如下:

- (void)setNavBarCustomBackButton:(NSString *)title target:(id)target action:(SEL)action {
UIImage *image = [UIImage imageSVGNamed:@"icon_arrow_back_white" size:CGSizeMake(20, 20) cache:YES];

UIButton *buttonItem = [UIButton buttonWithType:UIButtonTypeSystem];
buttonItem.tag = 1002;
buttonItem.titleLabel.font = [UIFont systemFontOfSize:16.0];
buttonItem.imageEdgeInsets = UIEdgeInsetsMake(0, -16, 0, 0);
buttonItem.titleEdgeInsets = UIEdgeInsetsMake(0, -19, 0, 0);
[buttonItem setImage:image forState:UIControlStateNormal];
[buttonItem setTitle:title forState:UIControlStateNormal];
[buttonItem setTitleColor:RGB(45, 45, 45) forState:UIControlStateNormal];
[buttonItem addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
[buttonItem sizeToFit];
buttonItem.frame = CGRectMake(0, 0, buttonItem.frame.size.width, 40);

self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithCustomView:buttonItem];
}
  1. rightNavigationItem

其设置与leftNavigationItem一样,这里不啰嗦了。

对于navigationItem的三个属性来说,其贯穿与整个App的开发过程中,我们为了代码的整洁性, 一般不会在viewController中直接设置,而已通过category来使用。

导航栏切换

iOS10中navigationController的push和pop,前后两个viewController如果导航栏颜色(或者透明度)不一致, 就会出现导航栏的切换动画,处理这种情况时应该特别小心,如果处理不当就会造成bug或者 App体验上的损失。

系统默认在导航栏切换的时候会有动画和毛玻璃效果,其实这个动画就是前后Controller的 导航栏颜色的渐变动画,而且title也会随着动画产生“隐去”和“隐现”的效果,这种效果在 一般情况下没有问题,而且看起来还很不错,但是下列两种情况下你应该单独考虑。

  • 一侧透明一侧不透明

这种情况下就会产生奇怪的效果,一侧根本看不见导航栏,而系统给我们的是一个渐变的动画, 然而这个动画仿佛在iOS10上有bug,会发生跳转闪烁的情况(在viewWillAppear中设置导航栏 背景Image的效果会推迟显示)。如果我们可以接受这个动画,但是绝不能接受这个闪烁发生, 可以尝试着修复这个bug;但是一些情况下我们并不期望这个动画产生,做法很简单把透明一侧 的导航栏直接隐藏掉,这样前后viewController的导航栏渐进动画就会消失,很多blog上面 说的就是这种方法。

如果在透明一侧的导航栏的透明度是随着scrollView的contentOffset来变化的,那么隐藏导航栏这种 解决办法就会失效,因为我们不能鲁莽地去隐藏掉一个透明度不为0的导航栏。这种情况下是 逼着我们去实现自定义的转场动画,或者修复ios10上的bug,这两种解决办法都不会太简单, 我会接下来讲解。

  • 两侧主题相差巨大

如果两侧导航栏的主题风格相差巨大,也就是说前后两个viewController根本不像是同一个 navigationController中的子congtoller,那么系统中的渐变动画就会显得很不协调,想想 在渐变动画中出现一个其它不相干的颜色是多么的抓狂。这里的解决办法也有两种,一种原理很简单 ,就是在一侧放弃使用系统的导航栏,而伪造一个假的导航栏,这样就不会产生渐变动画。另一种 方法还是自己实现转场动画,又回到了这个问题。

市面上一些大的App,在处理上面两种情况的时候都放弃了系统的渐变动画,而是采用一种泾渭分明的 转场方式,比如说今日头条、支付宝等,具体是使用伪导航栏还是自己实现的转场动画就不得而知了。 特别说明:以上情况可能不适用于ios10之前,小心调试吧。

Appearance主题

上面已经讨论过,为navigationBar上面的item设置属性往往会造成困扰,我们有时候无法直接 改变这些item的属性,所以需要设置自定义item。Appearance主题是为了设置整个App的默认属性, 特别是对于UINavigation有用,能够设置App导航栏的色彩基调。如下:

[[UINavigationBar appearance] setTintColor:RGB(0x51, 0x4e, 0x4e)];
[[UINavigationBar appearance] setBarTintColor:RGB(0, 191, 143)];
[[UINavigationBar appearance] setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys:[UIFont systemFontOfSize:15], NSFontAttributeName, [UIColor whiteColor], NSForegroundColorAttributeName, nil]];
[[UIBarButtonItem appearance] setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
                                                              [UIFont systemFontOfSize:15.0], NSFontAttributeName,
                                                              RGB(0x51, 0x4e, 0x4e), NSForegroundColorAttributeName,
                                                              nil]
                                                    forState:UIControlStateNormal];
[[UINavigationBar appearance] setBackIndicatorImage:[UIImage imageSVGNamed:@"icon_arrow_back_white" size:CGSizeMake(20, 20) cache:YES]];
[[UINavigationBar appearance] setBackIndicatorTransitionMaskImage:[UIImage imageSVGNamed:@"icon_arrow_back_white" size:CGSizeMake(20, 20) cache:YES]];

上述代码最好在App启动的时候就设置好,作为导航栏的默认设置,而且在项目的进行过程中不要轻易地再去更改。 如果确实有需求更改navigationBar上的属性,记得在viewWillAppear或者viewWillDisappear中修改 回去,或者直接使用自定义的item。

总结

这里我不打算去讲解IOS的转场动画了,因为这个题目还是很大的,我怕说不清楚;再者除非对细节有着很高的要求,一般APP也不会去做 导航栏的转场动画。针对,IOS10中出现的导航栏闪烁问题,我们组的大牛的解决办法是,放弃NavigationBar上面的一层backgroundView, 统一设置成clearColor,然后在navigationBar上自己加一层layer,来模拟渐变动画,这样就通过扩展NavigationBar,避开了系统导航栏 的一些默认处理效果。

]]>
由“点击头像放大”想开去 2016-08-31T00:00:00+00:00 zhing http://zhing.github.com/2016/08/31/由“点击头像放大”想开去 由“点击头像放大”想开去

在使用微信查看联系人头像的时候,可以点击头像使之放大、进而填充整个window,可以对大图 进行双击、缩放、平移等操作,总的来说这个过程的编码也不复杂,但是涉及到UIWindow、手势 识别方面的知识,有必要总结一下。

UIWindow

UIWindow是一种特殊的UIView,我们一般在程序中这样来指定App的UIWindow:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    UITabBarController * tabBarController = [[UITabBarController alloc] init];
    self.window.rootViewController = tabBarController;

    [self.window makeKeyAndVisible];
    return YES;
}

一个App中通常只有一个UIWindow,作为所有视图的容器。在编码的过程中这样来获得App的keyWindow:

UIWindow *window = [UIApplication sharedApplication].keyWindow;

当然,我们也可以手动创建多个UIWindow,多个Window的显示顺序是根据UIWindowLevel进行排序的, IOS默认定义了三个等级:

const UIWindowLevel UIWindowLevelNormal;    //0.0
const UIWindowLevel UIWindowLevelAlert;     //2000.0
const UIWindowLevel UIWindowLevelStatusBar; //1000.0

值得注意的是:UIWindow是严格按照Level来显示的,与keyWindow的设置顺序无关,在使用的 过程中常用的场景是需要全屏幕覆盖一个蒙层。

事件处理

任何一款移动设备都会有事件处理,允许操作系统能够对用户的操作进行响应,IOS中的用户事件 分为三种:

1. 触摸事件:通过触摸、手势进行触发
2. 运动事件:通过加速器进行触发,例如手机晃动
3. 远程控制事件:远程设备触发,例如耳机控制按钮

IOS中只有继承UIResponder类的对象才能处理事件,比如我们常用的UIView、UIViewController、 UIApplication。UIResponder中包含了对三种事件的处理方法,如果我们需要实现哪种事件的响应 逻辑,我们就需要覆盖这些方法:

触摸事件     
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;   一根或多根手指开始触摸屏幕时执行;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;   一根或多根手指在屏幕上移动时执行,注意此方法在移动过程中会重复调用;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;   一根或多根手指触摸结束离开屏幕时执行;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;   触摸意外取消时执行(例如正在触摸时打入电话);
运动事件     
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0);    运动开始时执行;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0);    运动结束后执行;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event NS_AVAILABLE_IOS(3_0);    运动被意外取消时执行;
远程控制事件   
- (void)remoteControlReceivedWithEvent:(UIEvent *)event NS_AVAILABLE_IOS(4_0);  接收到远程控制消息时执行;

手势处理

通过覆盖UIResponder的方法,我们可以处理触摸事件,然而我们在使用iphone的时候,每一个手势 都是一系列触摸事件的集合,所以苹果引入了手势识别,并封装成具体的类,这样开发者就不 需要编写识别算法了。

UITapGestureRecognizer       点按手势
UIPinchGestureRecognizer     捏合手势
UIPanGestureRecognizer       拖动手势
UISwipeGestureRecognizer     轻扫手势,支持四个方向的轻扫,但是不同的方向要分别定义轻扫手势
UIRotationGestureRecognizer  旋转手势
UILongPressGestureRecognizer 长按手势

手势处理是对触摸事件的集合,其中会有很多中间状态,比如说识别开始、识别中、识别失败或者 识别成功等等,所以其实现其实是一个有限状态自动机,常用的状态有:

UIGestureRecognizerStateBegan       //开始
UIGestureRecognizerStateChanged     //状态变化
UIGestureRecognizerStateEnded       //识别结束=识别成功

每一次状态的变更都会导致手势的响应selector被调用,所以我们一般在selector中对这几种 状态做区分处理。

坐标转换

在做动画的时候,可能会需要转换坐标系,比如,在cell中的button,我们要知道其在整个tableView 中的坐标值,或者是其在屏幕中的位置,我们就需要坐标转换,UIView提供了坐标转换的API, 不过我们得小心使用:

// 将像素point由point所在视图转换到目标视图view中,返回在目标视图view中的像素值
- (CGPoint)convertPoint:(CGPoint)point toView:(UIView *)view;
// 将像素point从view中转换到当前视图中,返回在当前视图中的像素值
- (CGPoint)convertPoint:(CGPoint)point fromView:(UIView *)view;

// 将rect由rect所在视图转换到目标视图view中,返回在目标视图view中的rect
- (CGRect)convertRect:(CGRect)rect toView:(UIView *)view;
// 将rect从view中转换到当前视图中,返回在当前视图中的rect
- (CGRect)convertRect:(CGRect)rect fromView:(UIView *)view;

上述实际上是坐标系的转换,toView是将当前坐标系中的矩形域转化为view坐标系中的矩形域, 而fromView则相反,将view坐标系中的矩形框转化为当前坐标系中的矩形框。

]]>
IOS基础图像处理 2016-08-24T00:00:00+00:00 zhing http://zhing.github.com/2016/08/24/IOS图像处理基础 移动应用中图像处理对于用户体验来说至关重要,也是考验app性能、网络速度的重要指标。 在开发和使用app的过程中,ScrollView经常作为UIImageView的载体,在滑动过程中Image是否 流畅加载和显示是移动开发中最基本也是最常见的优化场景。下面就从一些最基本的方向来 总结一下Image的处理套路。

圆角

从很多网络资料上都可以看到对于UIImageView的圆角设置会导致离屏渲染,从而损伤性能, 那我们在开发中应该怎样去平衡性能损失和编码复杂度呢?一般情况下给UIImageView设置 圆角都是通过如下两行代码实现:

imageView.layer.cornerRadius = 5
imageView.layer.masksToBounds = true

这两行代码很简单,通过设置UIView的layer层来实现圆角,在页面图片圆角不多的情况下, 这就是最简单的方式,也不会带来多少性能损耗。

如果是页面图片圆角较多的话,如上操作会带来严重的性能问题,故不可取。此时可行的处理 方法就是直接截取图片实现:

- (UIImage *)imageWithSize:(CGSize)size cornerRadius:(CGFloat)cornerRadius {
    UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
    [[UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, size.width, size.height) cornerRadius:cornerRadius] addClip];
    [self drawInRect:CGRectMake(0, 0, size.width, size.height)];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

直接将截取好的图片填入UIImageView中,所以对于移动App中feed流这种需要好的体验的地方, 一般是采取的这种方式。当然对于普通的画图,我们也可以使用Core Graphics这样来实现:

+ (UIImage *)imageWithFillColor:(UIColor *)fillColor strokeColor:(UIColor *)strokeColor size:(CGSize)size lineWidth:(CGFloat)lineWidth cornerRadius:(CGFloat)radius {
    UIGraphicsBeginImageContextWithOptions(size, NO, [UIScreen mainScreen].scale);
    [fillColor setFill];
    [strokeColor setStroke];
    CGRect roundedRect = CGRectMake(lineWidth/2, lineWidth/2, size.width-lineWidth, size.height-lineWidth);
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:roundedRect cornerRadius:radius];
    path.lineWidth = lineWidth;
    [path fill];
    [path stroke];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

当然我们可以重写一个UIView的drawRect方法来得到不同形状的UIView,但是调用drawRect 方法也会导致离屏渲染,实不可取。用上面的绘图方法绘出特定的图形,然后代替UIView才 是正确高效的实现方式。

主题色

每个app都有自己特有的tintColor,最明显的就是当我们选中一个TabBarItem的时候,图标 会变色,这种情况使用tintColor作为图标的选中色最合适不过了,那么我们怎样得到tintColor 背景的图片呢,可以为UIImage写一个扩展为UIImage+Tint.h,实现如下:

@implementation UIImage (Tint)
- (UIImage *) imageWithTintColor:(UIColor *)tintColor
{
    return [self imageWithTintColor:tintColor blendMode:kCGBlendModeDestinationIn];
}

- (UIImage *) imageWithGradientTintColor:(UIColor *)tintColor
{
    return [self imageWithTintColor:tintColor blendMode:kCGBlendModeOverlay];
}

- (UIImage *) imageWithTintColor:(UIColor *)tintColor blendMode:(CGBlendMode)blendMode
{
    //We want to keep alpha, set opaque to NO; Use 0.0f for scale to use the scale factor of the device’s main screen.
    if (tintColor) {
        UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0f);
        [tintColor setFill];
        CGRect bounds = CGRectMake(0, 0, self.size.width, self.size.height);
        UIRectFill(bounds);

        //Draw the tinted image in context
        [self drawInRect:bounds blendMode:blendMode alpha:1.0f];

        if (blendMode != kCGBlendModeDestinationIn) {
            [self drawInRect:bounds blendMode:kCGBlendModeDestinationIn alpha:1.0f];
        }

        UIImage *tintedImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        return tintedImage;
    }
    else {
        return self;
    }
}

@end

上面也是通过Core Graphics来为图片加载主题色的,至于代码中的blendMode,渲染模式对 透明度和渐变色产生影响,除非你对这个参数非常了解,否则请使用上面的方式。

透明度

设置一张图片或者UIView的透明度对于性能也会产生影响,设置图片透明度的正确姿势为:

- (UIImage *)imageByApplyingAlpha:(CGFloat) alpha {
    UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0f);

    CGContextRef ctx = UIGraphicsGetCurrentContext();
    CGRect area = CGRectMake(0, 0, self.size.width, self.size.height);

    CGContextScaleCTM(ctx, 1, -1);
    CGContextTranslateCTM(ctx, 0, -area.size.height);

    CGContextSetBlendMode(ctx, kCGBlendModeMultiply);

    CGContextSetAlpha(ctx, alpha);

    CGContextDrawImage(ctx, area, self.CGImage);

    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return newImage;
}

大小

很多时候我们会去单纯地改变一张图片的大小,而且这种要求会很频繁,这里也是推荐使用 Core Graphics来改变一张图片的大小:

- (UIImage *)resizeImageWithMaxSize:(CGSize)maxSize {
    if (maxSize.width < FLT_EPSILON || maxSize.height < FLT_EPSILON) {
        return nil;
    }
    CGSize size = self.size;
    if (size.width < maxSize.width && size.height < maxSize.height) {
        return self;
    }

    CGFloat widthRatio = maxSize.width / size.width;
    CGFloat heightRatio = maxSize.height / size.height;
    CGFloat ratio = widthRatio < heightRatio ? widthRatio : heightRatio;
    CGSize finalSize = CGSizeMake(size.width * ratio, size.height * ratio);

    UIGraphicsBeginImageContext(finalSize);
    [self drawInRect:CGRectMake(0, 0, finalSize.width, finalSize.height)];
    UIImage *resizedImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return resizedImage;
}

缓存

当图片需要重复使用的时候,我们有必要对于图像进行缓存,这样能够消除CPU的重复图像 处理时间,在进行图像缓存的时候,使用最简单的NSMutableDictionary就行,缓存的Value 是UIImage,Key为图像的四个基本属性(name、UIImageTransformMode、size和cornerRadius) 当然,并不是这四个属性都是必须,不过一般情况下以name和size作为key。

+ (UIImage *)imageSVGNamed:(NSString *)name transformMode:(LNImageTransformMode)mode size:(CGSize)size cornerRadius:(CGFloat)cornerRadius cache:(BOOL)needCache {
    NSDictionary *cacheKey = @{@"name" : name, @"mode": @(mode), @"size": [NSValue valueWithCGSize:size], @"cornerRadius": @(cornerRadius)};
    UIImage *image = [[SVGImageCache sharedImageCache] cachedImageWithKey:cacheKey];
    if (image == nil) {
        image = [[UIImage svg2Image:name size:size] imageWithTransformMode:mode size:size cornerRadius:cornerRadius];
        if (image && needCache) {
            [[SVGImageCache sharedImageCache] addImageToCache:image forKey:cacheKey];
        }
    }

    return image;
}

格式

判断图像的格式很容易,读取图像二进制码的第一个字节就可以得到图片的格式。一般在开发中 常用的图片格式是Jpg、PNG、SVG和GIF。其中

Jpg格式适合于从网络上下载的、像素丰富、体积略大的图片(压缩空间比较大);
png适合于存储于客户端的小图标,但是需要为不同屏幕的图标大小做适配(*1、*2、*3);
SVG为矢量图,可以避开屏幕大小适配的问题,也是使用很广泛的图片格式;
GIF为动态图,其带有时间轴。
]]>
IOS开发中的页面跳转 2016-08-10T00:00:00+00:00 zhing http://zhing.github.com/2016/08/10/IOS开发中的页面跳转 IOS开发中的页面跳转

不止在移动开发领域,甚至对于所有的前端开发,页面跳转都是最基本的场景转换,是开发过程 中最基本的问题。对于IOS初学者,push与present是两种最常用的方式,在开发的逐渐深入过程 中,需要接触到APN的推送、网络链接等等急于URLScheme的高级跳转方式,随着开发经验的不断 丰富,将APP中的跳转统一使用URLScheme来管理不失为一种良好的设计。

Push与Present

Push与Present是最基本的两种转场方式,也是URLScheme等高级封装的基础。两者最直观的区别 就是push的转场效果是自右向左的,其需要UINavigationController的配合使用,present模态 窗口的默认转场效果是自下而上的,两种的使用方式也有着本质区别。

UIViewController *nextViewController = [[UIViewController alloc] init];
nextViewController.title = @"第二个界面";
[self.navigationController pushViewController:nextViewController animated:YES];

UIViewController *nextViewController = [[UIViewController alloc] init];
nextViewController.title = @"第二个界面";
[self presentViewController:nextViewContrller animated:YES completed:nil];

Present方式经常被是用来进行字段的填充或者编辑页面,Present方式是默认没有NavigationBar 的,如果需要可以在外面包裹一层UINavigationViewController,如下:

[self presentViewController:[[UINavigationController alloc]
    initWithRootViewController:controller] animated:YES completion:^{
}];

URLScheme

我们在开发过程中,会遇到四种与跳转相关的场景,分别是:

  • 外部网页场景

    只能通过AppDelegate中事件去获取对应的URL进行匹配。

  • Push推送场景

    通过AppDelegate中的事件去获取数据,取出链接字段,然后自定义一个URL,就可以实现 跳转了。

  • 内部网页场景

    UIWebView在内部打开URL,如果要在网页中跳转APP页面,有两种方法:一种是使用JSBridge, js调用native的handler,跳到相关页面;二是直接使用url来跳转,原理同情景一。

  • 应用内点击跳转

    最普通的打开方式,也同样可以抽离一个URLScheme匹配器去匹配打开。

上面总结了四种场景,如果对于新手来说,可能会每一种场景写一遍跳转代码,这样即低效又 难以维护,高手会选择将这四种场景尽量融合为一种URLScheme,然后使用URL匹配器统一打开。

由于URLScheme的多功能性,需要给URLScheme定义一个类:

@interface MYURLObject : NSObject
    @property MYURLType type;
    NSDictionary *params;
@end

当然还需要一个Route类,该类最好能同时处理MYURLObject和urlString,其API如下:

+ (void)navigateWithURLObject:(LNURLObject *)obj;
+ (void)navigateToURL:(NSString *)url fromViewController:(UIViewController *)controller;

由于前面所说的前三个场景均是需要使用urlString来跳转,所以需要一个将urlString转化为 MYURLObject的类,其API为:

+ (MYURLObject *)convertToURLObject:(NSString *)url;

这样一个URL匹配器就搭建完成了,在写跳转代码的时候,请优先考虑URLScheme。

]]>
Objc文件读写 2016-08-07T00:00:00+00:00 zhing http://zhing.github.com/2016/08/07/Objc文件读写 Objc文件读写

在ios开发中处理文件的时候也很多,其作为一种数据持久化方式起到了至关重要的作用,各个 语言的文件处理都大同小异,但是IOS处理文件的方式却很特别,下面就来详细总结一下objc 处理文件的一般用法。

文件目录

IOS程序是运行在应用沙盒之中,每个APP均会有一个专属沙盒,这样各个APP就不会互相干扰, APP也不能访问沙盒之外的文件,一定程度上保证了APP运行的安全。那么,我们如何获得沙盒 中的文件目录?

//获取App主路径~
NSString *homePath =NSHomeDirectory();

//获取~/Documents
NSArray *documentArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, NO);
NSString *documentPath = [documentArray firstObject];

//获取~/Library
NSString *libraryPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) firstObject];

//获取~/Library/Caches
NSString *cachesPath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)firstObject];

//获取~/Library/Preferences
NSString *preferencePath =[libraryPath stringByAppendingString:@"/Preferences"];

//获取~/tmp
NSString *tmpPath = NSTemporaryDirectory();

上面是我们在编码过程中使用的常用路径,还有一点对于初学者来说容易混淆,就是我们在 代码中使用的一些资源文件,即静态文件,处于Bundle管理下,IOS8之后,Bundle不再处于 home目录之下。

//获得Bundle主目录
NSString *path = [NSBundle mainBundle].resourcePath;
NSString *path = [NSBundle mainBundle].bundlePath;

在App启动时,会将资源文件拷贝到Bundle主目录下,所以在使用过程中我们只需要关心文件 名即可。

简单对象写入

IOS开发者一定很熟悉NSUserDefaults,它可以让我们方便地对简单数据进行存储,其是一个 单例对象,文件实际存储的位置在于~/Library/Preferences目录下,适合于存储轻量级的本地 数据,比如用户名、密码之类的,支持的数据类型如下:

NSNumber ( Integer、Float、Double )
NSString
NSArray
NSDictionary
BOOL类型
NSDate

使用NSUserDefaults的本质还是写入文件,对于NSString、NSArray、NSDictionary和NSData 这四种对象的数据可以直接写入文件中:

[string writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
[array writeToFile:docuPath atomically:YES];
[dict writeToFile:prefePath atomically:YES];
[data writeToFile:homePath atomically:YES];

对于上述简单对象,其嵌套的类型也必须是简单类型。再者,Objc是c语言的超集,c语言中 一些处理文件的API在这里同样适用,

NSString *path = NSTemporaryDirectory();
FILE *file = fopen([path cStringUsingEncoding:NSASCIIStringEncoding], "rb");
UInt64 temp64;
fread(&temp64, sizeof(UInt64), 1, file);

UInt64 temp64 = [key longLongValue];
fwrite(&temp64, sizeof(UInt64), 1, file);

不要以为C中的繁琐,当我们需要向文件中追加数据或者定位数据的文件位置时,推荐使用c 的API,这样既简单又高效。

复杂对象归档

复杂对象并不是Foundation框架内的对象,自然也就无法使用writeFile对象来写入文件中, 但是我们可以将复杂对象转化为NSData,然后写入文件,读取的时候也是读取的NSData,然后 再转化为一般的对象,这种转化的过程即为归档。

使用归档的一般步骤为,遵守NSCoding协议,实现其中的两个方法。如下:

@interface Person:NSObject<NSCoding> 
     @property(nonatomic,copy) NSString *name 
     @property(nonatomic,assign) integer age; 
@end

// 对person对象进行归档时,此方法执行。
// 对person中想要进行归档的所有属性,进行序列化操作
-(void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:self.name forKey:@"name"];
    [aCoder encodeInteger:self.age forKey:@"age"];
}
// 对person对象进行反归档时,该方法执行。
// 创建一个新的person对象,所有属性都是通过反序列化得到的。
-(id)initWithCoder:(NSCoder *)aDecoder 
{
    self = [super init];
    if (self) {
        self.name = [aDecoder decodeObjectForKey:@"name"];
        self.age = [aDecoder decodeIntegerForKey:@"age"];
    }
return self;
}

然后使用NSKeyedArchiver类来进行归档,常用的API如下:

1. 方法一:
// 准备一个NSMutableData, 用于保存归档后的对象
NSMutableData *data = [NSMutableData data];
// 创建归档工具
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingMutableData:data];
// 归档
[archiver encodeObject:p] forKey:@"p1"];
// 结束
[archiver finishEncoding];
// 写入沙盒
[data writeToFile:filePath atomically:YES];

2.方法二:
BOOL archiveSuccess = [NSKeyedArchiver archiveRootObject:person toFile:path];

3. 方法三:
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:person];
[data writeToFile:filePath atomically:YES];

上述API中显然方法二最简单,所以一般使用方法二。反归档一般如下使用:

Person* person = [NSKeyedUnarchiver unarchiveObjectWithFile:path];

NSFileManager

苹果将文件操作封装成了一个单例类,其API如下:

//判断一个文件是否存在
BOOL fileExists = [fileManager fileExistsAtPath:filePath];

//创建一个目录
if (![fileManager fileExistsAtPath:imagesPath]) {
    [fileManager createDirectoryAtPath:imagesPath withIntermediateDirectories:NO attributes:nil error:nil];
}

//删除一个目录
if (![fileManager removeItemAtPath:filePath error:&error]) {
    NSLog(@"[Error] %@ (%@)", error, filePath);
}

当然文件操作API还有很多,比如遍历一个目录等等,请参考:http://nshipster.cn/nsfilemanager/

总结

文件操作是写代码必须掌握的部分,重要性无需累述。

]]>
AFNetworking使用总结 2016-07-31T00:00:00+00:00 zhing http://zhing.github.com/2016/07/31/AFNetworking使用总结 AFNetworking使用总结

IOS开源网络库AFNetworking已经成为了IOS程序开发的首选、亦可以说是必备,无数IOS 的“先哲”们撰文称赞此库良好的设计和功能的强大,以致后来的开发者在项目中都不会去 考虑其它的网络库,而直接选择AFNetworking。这里就来总结一下使用它的一般程式,在 总结过程中学习和成长。

HttpClient

我们在使用AFHTTPSessionManager的时候,一般均会对其进行封装,以满足App的各种要求。 所以这里选择对其进行扩展,设计如下:

@interface LNHttpClient : AFHTTPSessionManager

+ (instancetype)sharedClient;
+ (void)setTimeout:(NSTimeInterval)timeout;
+ (void)setResponseType:(LNHttpResponseType)type;
- (void)setHttpHeader;

@end

该继承类的实现需要注意如下几点:

  • 继承AFHTTPSessionManager免不了对initWithBaseURL的覆写,并在其中注册一些通知,用于 检测用户的登陆和登出,以便Client做相应的处理。
  • setHttpHeader可以设置Http头部,比如token、userId等等。
  • 中间两个方法使得开发者可以控制每一次请求的timeout和responseType。

APIService

APIService是所有网络请求的入口,所有Service的网络调用均使用该类来完成,我们项目中 使用proto-buf来作为数据交换的类型,其设计力求简介:

typedef void (^APISuccessHandler)(id responseObject);
typedef void (^APIFailureHandler)(NSInteger code, NSString *msg);

@interface APIService : NSObject


+ (NSURLSessionTask *)POST:(NSString *)relativePath
             protobuf:(NSData *)proto
           modelClass:(Class)modelClass
              success:(APISuccessHandler)success
              failure:(APIFailureHandler)failure;


+ (NSURLSessionTask *)GET:(NSString *)relativePath
             protobuf:(NSData *)proto
           modelClass:(Class)modelClass
              success:(APISuccessHandler)success
              failure:(APIFailureHandler)failure;

该类的设计是对于AFHTTPSessionManager的封装,是所有Service类的基类。实现要点:

  • 定义了两个block,分别用来处理成功和失败的调用。
  • modelClass用来解析ContentType的数据,此处是proto-buf。
  • 此类派生的各个Service来处理不同的业务场景。

AFHTTPRequestSerializer覆写

在客户端发送请求时,我们有时需要设置request的content-Type,以便于服务端能够根据 content-Type来处理不同格式的数据,比如AFNetworking中自带的AFJSONRequestSerializer, 就能够把请求的数据转化为JSON格式,并且把content-Type设置为application/json。这里 我们的请求数据格式为proto-buf,而AF库并没有给我们提供相关的默认实现,这时候就需要 我们自己来实现AFProtoRequestSerializer。

- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                           withParameters:(id)parameters
                                    error:(NSError *__autoreleasing *)error
{
NSParameterAssert(request);

if ([self.HTTPMethodsEncodingParametersInURI containsObject:[[request HTTPMethod] uppercaseString]]) {
    return [super requestBySerializingRequest:request withParameters:parameters error:error];
}

NSMutableURLRequest *mutableRequest = [request mutableCopy];

[self.HTTPRequestHeaders enumerateKeysAndObjectsUsingBlock:^(id field, id value, BOOL * __unused stop) {
    if (![request valueForHTTPHeaderField:field]) {
        [mutableRequest setValue:value forHTTPHeaderField:field];
    }
}];

if (parameters) {
    if (![mutableRequest valueForHTTPHeaderField:@"Content-Type"]) {
        [mutableRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    }

    [mutableRequest setHTTPBody:[NSJSONSerialization dataWithJSONObject:parameters options:self.writingOptions error:error]];
}

return mutableRequest;
}

上面的代码是AFJSONRequestSerializer的主要覆写方法。同理我们只需要仿照这个例子来 实现AFProtoRequestSerializer即可。

URL缓存

说起HTTP请求,就不得不聊到缓存,每次去请求相同的URL的数据显然是不划算的,所以将 每次URL请求的数据缓存起来,以后当有相同的URL请求时,直接使用缓存数据即可。使用 缓存一般有两种选择。

  • NSURLCache

    系统提供的默认缓存,使用该方式可以减少开发的难度,但是在使用过程中需要注意的 是

    • 该缓存只能用在GET请求上,并不支持Post。
    • 缓存方式尽量选择NSURLRequestReturnCacheDataDontLoad,如果有缓存直接返回数据 如果没有缓存则不发送请求,返回nil,我们手工来再发一次请求。这样做可以规避一 些苹果实现缓存的坑。
  • **URLCache

    自己实现的缓存,我们只需要扩展NSURLCache即可,使用扩展的cache来代替原生的实例。 这样我们就可以人为控制缓存的URL范围和数据存储了,简单实现如下:

      @implementation LNURLCache
    
      - (void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request {
          if ([self shouldManuallyCacheRequest:request]) {
              [[LNCache globalCache] setObject:cachedResponse forKey:request.URL.absoluteString withTimeoutInterval:kTimeOneYear];
          } else {
              [super storeCachedResponse:cachedResponse forRequest:request];
          }
      }
    
      - (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
          if ([self shouldManuallyCacheRequest:request]) {
              return (NSCachedURLResponse *)[[LNCache globalCache] objectForKey:request.URL.absoluteString];
          } else {
              return [super cachedResponseForRequest:request];
          }
      }
    
      - (BOOL)shouldManuallyCacheRequest:(NSURLRequest *)request {
          return [request.URL.host hasSuffix:kCDNHostName];
      }
    
      @end
    

总结

通过以上讲解,相信你可以从容地处理好网络请求模块的设计。

]]>
FMDB实现ORM 2016-07-30T00:00:00+00:00 zhing http://zhing.github.com/2016/07/30/FMDB实现ORM FMDB实现ORM

IOS开发者们在使用coreData和sqlite原生接口的问题上争论不休,使用coreData无疑更方便, 它也在变得更好,但是反对者们的枪口对准了其在处理大量数据时候的无力感,而且作为开发 者却束手无策。sqlite的鼓吹者们觉得自己手写sql更简介、更可控,还可以带来更好的性能。 FMDB这个简单易用的存储库,站在了以上两种观点的中间,基本上平衡了两种方式存在的种种 优缺点,获得了大量的开发者的青睐。

FMDB只是在sqlite上做了一层很薄的封装,但是却大大地提高了开发者的使用效率,其毕竟不 是大家耳熟能详的ORM,自然也不能享受到ORM带来的种种好处。下文中我们就来看看使用FMDB 来实现一些类似于ORM的类库。

LocalDBManager

LocalDBManager是直接与数据库交互的类,为了使代码更好维护,所有的持久化均要走这个类 的API才能写入数据库中,下面是其简要设计:

typedef void (^DBUpdateCallback)(NSError *error);
typedef void (^DBInsertCallback)(NSArray *ids, NSError *error);
typedef void (^DBQueryCallback)(FMResultSet *result, NSError *error);
typedef void (^DBExecuteCallback)(FMDatabase *db);

@interface LocalDBManager : NSObject

+ (instancetype)sharedInstance;
- (void)open;
- (void)close;

- (void)insertBatchAsync:(DBInsertCallback)callback insert:(NSString *)insert withArgsArray:(NSArray *)argsArray abortWhenError:(BOOL)abortWhenError;
- (void)updateBatchAsync:(DBUpdateCallback)callback update:(NSString *)update withArgsArray:(NSArray *)argsArray abortWhenError:(BOOL)abortWhenError;
- (void)queryAsync:(DBQueryCallback)callback query:(NSString *)query withArgs:(NSArray *)args;
- (void)execute:(DBExecuteCallback)callback;

@end

以上设计中,主要有如下要点:

  • 对于数据库的增、删、改、查操作,强制提供批量Batch操作的接口,以增加数据库操作的效率。
  • 在Batch操作中提供强制使用事务,提高Sqlite的速度。
  • 在open操作中处理数据库的migrate,留出execute方法来处理一些复杂场景。

BaseModel

BaseModel作为Model的基类,所有的Model均继承自此类,所以在该类的实现中完成一些基本 动作,设计如下:

typedef void (^ModelQueryCallback)(NSArray *items, NSError *error);
typedef void (^ModelCallback)(NSError *error);

@interface BaseModel : NSObject

+ (NSString *)tableName;
+ (NSString *)primaryKeyName;
- (id)primaryKeyValue;

+ (void)queryAll:(ModelQueryCallback)callback;
+ (void)queryByOffset:(NSInteger)offset withSize:(NSInteger)size callback:(ModelQueryCallback)callback;
+ (void)removeBatch:(NSArray *)models callback:(ModelCallback)callback;
+ (void)insertBatch:(NSArray *)models callback:(ModelCallback)callback;
+ (void)insert:(BaseModel *)model callback:(ModelCallback)callback;
+ (void)updateBatch:(NSArray *)models callback:(ModelCallback)callback;

- (void)insert:(ModelCallback)callback;
- (void)update:(ModelCallback)callback;
- (void)remove:(ModelCallback)callback;

+ (void)modelCallbackOnMainThread:(ModelCallback)callback withError:(NSError *)error;
+ (void)modelQueryCallbackOnMainThread:(ModelQueryCallback)callback withItems:(NSArray *)items withError:(NSError *)error;

@end

以上设计中,主要有如下要点:

  • tableName、primaryKeyName、primaryKeyValue需要子类来细化,所以必须被覆写。
  • 其它的需要子类必须覆写的API有queryByOffset、insertBatch、updateBatch,其它的方法 均可在此基类中实现。
  • 最后两个方法是对callback进行的优雅处理,由于其通用性,在基类中实现。

Model

Model即是我们需要持久化的模型,由于Model是重要的数据结构,所以一般习惯于把Model 写的很重,理论上可以在Model中实现所有的持久化操作,只是写数据库必须通过LocalDBManager 来完成,Model之间一些共有操作被提取到BaseModel中实现。再者并不是Model中所有的字段 与数据库中表字段一一对应,也可以在其中声明not in db的属性。下面来看看Model的一般设 计:

@interface User : BaseModel

+ (void)queryByOffset:(NSInteger)offset withSize:(NSInteger)size callback:(ModelQueryCallback)callback;
+ (void)updateBatch:(NSArray *)models callback:(ModelCallback)callback;
+ (void)insertBatch:(NSArray *)models callback:(ModelCallback)callback;

- (void)fillWith:(User *)user;
- (BOOL)isEqualToUser:(User *)aUser;
+ (NSArray *)convertToModel:(FMResultSet *)result

+ (void)query:(NSString *)sql withArgs:(NSArray *)args callback:(ModelQueryCallback)callback;
+ (void)queryById:(SInt64)userId callback:(ModelQueryCallback)callback;

@end

以上设计中,主要有一下要点:

  • 前三个方法是必须要覆写的父类中方法,后两个方法是Model的特色方法,各个Model之间各异。
  • 中间的两个方法是大部分Model中都会存在的方法。

总结

以上只是数据持久化写入数据库的一种通用方法,当然我们可以抽象出更多的protocal来让 Model去实现,使得Model封装的层次性更好,但是继承的深度不要再增加了,继承只会增加 程序的复杂度。

通过以上的讲解,相信你对于FMDB的使用方法和Model持久化有了更清醒的认识。

]]>
block详解 2016-07-19T00:00:00+00:00 zhing http://zhing.github.com/2016/07/19/block详解 毫不夸张地说,block让objc这门语言变得更有魅力,它就是在其它语言中常见的闭包的概念。 在block之前,objc重度依赖delegate来完成一些用户行为,是block让我们开发者多了一个 更简单的选择,本文就《objc高级编程》来总结一下block的实现原理。

什么是block

用书中的话来概括block,就是:带自动变量的匿名函数。block可以有很多理解,它可以被 看作一个代码块,这个代码块即是匿名函数的函数体,它和普通的函数一样可以有参数,可 以有返回值,由此可见,block在使用上除了没有函数名之外和普通的函数没有区别,但是在 这里要注意区分函数和方法的区别。其带有自动变量,即是它可以保持block声明作用域内变 量的值,无论把这个block拿到哪里去执行,这些变量的值都为你保存着。这些都是显然的闭 包的概念,通过block来深入理解objc(甚至是其它语言)的闭包未尝不是一个好的方法。

虽然使用和理解block给接触objc不久的开发者带来了困扰,但是block的语法却很简单,通 过下面三个实例概括一下block的使用:

  1. 定义:
typedef void (^RFAudioBasicBlock) (void);
typedef void (^RFAudioSuccessBlock) (BOOL flag);
typedef void (^RFAudioSuccessDetailBlock) (BOOL flag, NSURL *url, NSTimeInterval duration);
typedef void (^RFAudioSuccessURLBlock) (BOOL flag, NSURL *url);
  1. 作为参数:
- (void)playWithURL:(NSURL *)url finishedBlock:(RFAudioSuccessDetailBlock)block;
  1. 使用:
[[RFAudioManager defaultManager] playWithURL:url finishedBlock:^(BOOL flag, NSURL *url) {
NSLog(@"播放结束:%@", url);
}];

block的本质与实现

直接通过代码来分析block的实现比较有说服力:

  • objc源码
int main {
void (^blk)(void) = ^{printf("Block/n");};
blk();
return 0;
}
  • 编译之后的代码
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;

__main_block_impl_0 (void *fp, struct __main_block_desc_0 *desc, int flags=0){
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself){
printf("Block/n");
}

static struct __main_block_desc_0{
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};

int main() {
void (^blk)(void) = (void (*)(void))&__main_block_impl_0(
(void *)__main_block_func_0, &__main_block_desc_0_DATA
);

( (void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr )(
(struct __block_impl *)blk
);
}

这段代码是我照着PDF文档手打出来的,等我打出来之后也就明白了这段代码的意思,不信你 可以试试。由源代码我们可以清晰地看到:

  1. Block就是main_block_impl_0结构体指针类型的变量blk,即栈上生成的main_block_impl_0 结构体实例(对象)。
  2. block_impl即为Block的类信息,理解这里的类信息需要借助与objc对象与类之间的关系。 里面的isa变量我们很熟悉,标识了block_impl的元类型NSConcreteStackBlock(其实标识了 该对象所处的位置在stack中)。
  3. Block如何截获自动变量,再简单不过了,将自动变量保存在__main_block_impl_0对象中即可。

由此,我们可以下结论:Block就是一个Objective-c对象。

__block说明符

Block能够轻松地截获自动变量的值,并在其调用内使用它,然而却不能更改它,如果需要在 Block内部更改外部的变量值,需要加上__block修饰符。咋一看,这个要求很简单,在Block 对象中保存变量的指针不就完事了吗?然而实际情况却不是,在Block调用的时候,自动变量 可能早已超出其作用域被销毁了,也就不能通过指针来访问自动变量了。看代码:

  • objc代码
__block int val = 10;

*编译之后的代码

struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int flags;
int __size;
int val;
};

int main() {
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
}
}

由上,我们不难看出所有被block修饰的变量均会变成一个结构体(即对象),所以在block 中改变其值并不难,然而即便是对象也并没有解决超出作用域被收回的问题,下面我们来探讨 Block及block变量超出作用域而存在的道理。

Block存储域

分解上述的isa指针,Block分为三种类型:

  • _NSConcreteGlobalBlock 通过名称就可以判断出此Block为全局的,处于程序的数据区,生成此种类型的Block有 两种情况:
  • 记述全局变量的地方有Block语法
  • Block语法的表达式中不使用应该截获的自动变量时 此种类型的Block就如同全局变量一样,在全局环境下通过指针安全地使用。

  • _NSConcreteStackBlock 当Block截获自动变量的时候,其所处的区域在于Stack中。设置在栈上的Block,如果其 所属的作用域结束,该Block即会被废弃;同理block类型的变量也配置在栈上,如果 其所属的变量作用域结束,该block变量也会被废弃。

  • _NSConcreteMallocBlock 大部分Block的结构体实例均是在栈上,如果在实例作用域结束的时候,要保留这个Block 就需要将其复制到堆上,如果堆上的Block引用也过期,就会被收回。下面探讨将Block 从栈复制到堆的情形。

typedef int (^blk_t)(int);

blk_t func(int rate) {
return ^(int count){return rate * count;};
}

该Block即是属于Stack上的实例变量,当函数返回的时候,Block也相继被废弃,但是此Block 作为函数的返回值,编译器会自动生成将Block复制到堆上的代码。一般情况下,编译器会自行 判断需要将Block复制到堆上的情形,但是也存在编译器不能判断的场景:

  • 向方法或者函数的参数中传递Block时需要手动复制Block。这就是很好地解释了@property 后面的类型为Block时需要申明copy,当然申明strong/retain也不会出现问题,因为其对于 Block的默认行为也是copy。

还有两个我们不需要copy的场景:

  • Cocoa框架的方法且方法名中含有usingBlock等时;
  • 使用GCD的API时

至此我们可以稍微总结一下Block被复制的情形:

  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将Block赋值给附有__strong修饰符的id类型的类或者Block类型的成员变量时
  • 方法名中含有usingBlock的cocoa框架方法或者使用GCD时

__block变量作用域

由上文可知,__block变量被转化为结构体实例,其实就是对象,其随着持有它的Block一起被 复制到堆中或者被废弃。当其被复制之后,不管是栈中还是堆中,均可以访问到该对象,并对其 值进行的更改均有效,这是如何做到的呢?

在上文Block_byref_val_0结构体中有个_forwarding字段,其即为block变量对象的 指针,栈中对象的_forwarding实际上指向了堆中对象的地址,所以不管是在栈中还是 在堆中访问这个变量均指向了同一个地方即堆中的对象。所以在Block内外均能够对其 进行赋值和访问。

截获对象

有一种情况,如果非__block变量指向的是对象,那么我们依然得克服变量作用域失效而导致 对象被释放的困境,以达到Block截获自动变量的目的。这个问题也似乎很好解决,只需要在 Block中带入该自动变量的修饰符(strong/weak),于是Block对象中保持了对自动变量指向 的对象的强引用,那么目标对象即不会被释放,这样就达到了Block持有这个对象的目的。 实际上苹果也是这么做的,只是这个修饰符在Block被copy的时候才会生效,也即要求Block 对象处于堆中,如果该Block并没有被copy,那么自动变量的修饰符会丢失,自动变量所指 向的对象也会因为超出作用域而被释放。

如果__block变量指向的是对象,情况与非block变量类似。

最后

提醒:使用block是循环引用的高发区,所以要小心杜绝循环引用引起的内存泄露。

]]>
GCD的奥秘 2016-07-19T00:00:00+00:00 zhing http://zhing.github.com/2016/07/19/GCD的奥秘 GCD的奥秘

很多编程语言都会有多线程编程,抛开多线程编程的复杂性,它确实能够提升程序执行的效率 。特别是现在CPU都是多核,能够充分发挥多核的优势也是一些编程语言的追求,比如说golang, 熟悉Golang或者java的开发者,应该都对多线程很熟悉,然而在objc中,使用GCD来进行多线 程的编码要来得更优雅、更简单,下来就来揭开其神秘面纱。

API

开发者要做的只是将想执行的任务追加到适当的Dispatch Queue中去。

dispatch_async(queue, ^{
    /*
     *想执行的任务
     */
})

上面的代码是使用GCD的一般格式,其中的queue分为两种:

1. Serial Dispatch Queue (串行队列)
2. Concurrent Dispatch Queue (并行队列)

很好理解,串行队列中的任务会在一个线程中串行执行,并行队列中的任务会在多个线程中 并行执行。那么如何得到这两个队列呢?

1. dispatch_queue_create

    dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.serialDispatchQueue", Operation)

上述Operation指示生成队列的类型,NULL和DISPATCH_QUEUE_SERIAL为串行,DISPATCH_QUEUE_CONCURRENT
为并行。

2. 获取系统提供的队列
_queue = dispatch_get_main_queue();
_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

第一个为主线程队列,是串行的,后四个为全局并行队列,可以通过参数来区分其优先级

上面总结了GCD的最基本的用法,当然还有很多很实用的API,如下:

1. dispatch_set_target_queue
可以变更Queue的优先级,还可以将多个queue中的任务归并到某一个queue中。

2. dispatch_after
可以延迟某个任务的执行,但是要注意:这里的延迟并不是任务在指定时间之后执行,而是
延迟指定时间追加到队列中去。

3. dispatch_group_async
如果想要追加到queue中的多个处理结束后进行结束处理,是使用dispatch_group的绝佳场景。
当然将这些任务依次放入一个串行队列中就可以解决问题,但是使用并行队列时,就需要
使用dispatch group了。

4. dispatch_barrier_asyc
dispatch_barrier允许在一个并发队列中创建一个同步点,当在并发队列中遇到一个barrier,
它会等到在这个barrier之前提交的所有任务都执行完毕之后,再执行,而所有在barrier之
后提交的任务会等到barrier之后再执行。

5. dispatch_semaphore
是GCD的同步信号量,

上面所说的都是异步GCD的API,当然还有一些同步的API可以使用,也很重要。

1. dispatch_apply
该函数按指定的次数将指定的block加入到指定的queue中去,并等待全部处理执行结束。
由于此API是同步的,所以一般在dispatch_async中使用它比较常见。

2. dispatch_once
函数保证在应用程序中只执行一次任务,普遍应用于单例对象的初始化。

3. dispatch_semaphore
GCD中的同步信号量,能够比dispatch_group等提供更细粒度的同步控制,使用很广泛。

GCD使用案例

如果多个线程同时操作(读写)一个可变容器,就很有可能会出现线程安全的问题,当一个线程 正在读取时另一个线程正在修改就是一个不安全的行为,例如:

    - (void)addObject:(NSObject *)obj {
        if (obj) {
            [mutableArray addObject: obj];
            dispatch_async(dispatch_get_main_queue(), ^{
                [self postContentAddedNotification];
            });
        }
    }

    - (NSArray *)objects {
        return [NSArray arrayWithArray:mutableArray];
    }

如果使用GCD来改写这段不太安全的代码,效果将是这样的。

    - (void)addObject:(NSObject *)obj {
        if (obj) {
            dispatch_barrier_async(self.concurrentQueue, ^{
            [mutableArray addObject:obj];
            dispatch_async(dispatch_get_main_queue(), ^{
                [self postContentAddedNotification]; 
            });
        });
        }
    }

    - (NSArray *)objects {
        __block NSArray *array;
        dispatch_sync(self.concurrentQueue, ^{
            array = [NSArray arrayWithArray:_mutableArray];
        });
        return array;
    }

这段代码中,写入数据通过一个barrier来完成,因为barrierBlock永远不会和其它Block一 起执行,所以保证了写安全。在读的时候,使用同步调用,确保了函数返回。在写客户端代 码的时候不会像服务端那样变态地去考虑多线程问题,但是在编码过程中意识到哪些地方可能 会出错还是很重要的。

死锁

GCD相当好用,但用不好就会死锁,始终要记着这样一句秘籍: 不要在串行队列放dispatch_sync、 dispatch_apply,比如:

  • 案例一

    dispatch_sync(dispatch_get_main_queue(), ^{

          NSLog(@"test");
    
      });
    
  • 案例二

    //queue为串行队列 dispatch_async(queue, ^{

      dispatch_sync(queue, ^{  
    
          NSLog(@"1"); // 任务1
    
      });
    

    NSLog(@"2"); // 任务2

    });

上文中两个死锁的例子对于dispatch_apply同样适用。

]]>
objc的内存管理详解 2016-07-17T00:00:00+00:00 zhing http://zhing.github.com/2016/07/17/objc内存管理 objc作为极少见的以引用记数来管理内存的动态语言,对于从其它语言转过来的开发者来说有 点陌生,甚至是对于MRC或者ARC有点束手无策,网上的文章一般不能透彻的分析其中的奥妙, 还好《objc高级编程》这本书为我们打开了objc引用记数的神秘面纱,这里将这本书中关于的 内存管理方面的精髓做个总结。

MRC

MRC(Manual Reference Counting),顾名思义即手动管理对象的引用记数,在xcode4.2之前, ios开发者都需要自己来管理对象的生命周期,其有四个准则:

  1. 自己生成的对象自己持有

     {
         id obj1 = [[NSObject alloc] init];
         id obj2 = [[NSObject new];
         id obj3 = [[NSObject copy];
         id obj4 = [[NSObject mutableCopy];
     }
    

    这四种方式(alloc/new/copy/mutableCopy,以及以他们开头的函数簇)生成的对象是自己 生成并持有的对象。

  2. 非自己生成的对象自己也能持用

     {
         id obj = [NSArray array];
         [obj retain];
     }
    

    对于非自己生成的对象,需要调用retain来持有该对象。

  3. 不需要自己持有的对象时释放

     {
         id obj = [[NSObject alloc] init];
         [obj release];
     }
    

    如果自己持有的对象不再需要,使用release释放。

  4. 无法释放非自己持有的对象

     {
         id obj = [NSArray array];
         [obj release];
     }
    

    上述release调用会导致奔溃,因为释放非自己持有的对象是非法的。

实现上述引用记数(垃圾回收的一种)有两种通用的方式,如下:

  1. 将引用记数(retainCount)保存在对象头中,调用alloc或者retain之后,retainCount +1,调用release之后,retainCount-1,当引用记数为0时,调用对象的dealloc方法来释 放当前的对象。
  2. 将引用记数和内存块地址保存在散列表(即引用记数表)中,引用记数的作用同上,内存 块地址用来追溯对象的内存地址。

苹果的实现方式为第2种方式,显然这种方式有利于调试和追溯内存空间。

* 谈到objc的内存管理就不得不说autoRelease(自动release),其管理内存的方式如下:
{
    id obj = [[NSObject alloc] init];
    [obj autorelease];
}
当autorelease调用发生时,obj对象会被注册进对应的autoreleasePool中,NSRunloop
此次循环结束的时候将pool中的对象废弃。所以对于注册进autoreleasePool中的对象,
并不是立即废弃,而是需要等循环结束来释放pool。

ARC

ARC(Automatic Reference Counting),即是编辑器帮助我们自动管理引用记数,大大地提升 了开发者的效率,是现在xcode的默认管理内存方式,其是通过给对象加上所有权修饰符来实 现的,一共有四种修饰符号:

  1. __strong修饰符(id类型和对象类型默认的修饰符号)

     {
         id __strong obj0 = [[NSObject alloc] init];/*对象A*/
         id __strong obj1 = [[NSObject alloc] init];/*对象B*/
         obj0 = obj1;/*obj0持有对象B的强引用,因此原先的对A强引用失效,故而废弃对象A*/
     }
    

    由此可见,__strong不止在变量作用域,在赋值上也能正确管理对象所有者。那么ARC是如何 来实现自动管理的呢?

      {
          id obj = [[NSObject alloc] init];
    
          /*编译器模拟代码*/
          id obj = objc_msgSend(NSObject, @selector(alloc));
          objc_msgSend(obj, @selector(init));
          objc_release(obj);
      }
    
      {
          id obj = [NSArray array];
    
          /*编译器模拟代码*/
          id obj = objc_msgSend(NSArray, @selector(array));
          objc_retainAutoreleaseReturnValue(obj);
          objc_release(obj);
    
          + (id) array {
              return [[NSArray alloc] init];
          }
    
          /*编译器模拟代码*/
          + (id) array {
              id obj = objc_msgSend(NSArray, @selector(alloc));
              objc_msgSend(obj, @selector(init));
              return objc_autoreleaseReturnValue(obj);
          }
      }
    

    上面两个代码块,清晰得展现除了__strong的模拟实现过程,即ARC会在适当的地方自动插入 release代码。特别注意上述参数传递过程的实现,中间借助autoreleasePool来过度。

  2. __weak修饰符(为了避免环形引用)

     id __weak obj1;
     {
         id obj0 = [[NSObject alloc] init];
         obj1 = obj0;
     }
     /*
         此处obj1为nil,因为对象的强引用为0时即被释放
     */
    

weak修饰符使我们能够使用对象,但是不持有对象。ARC是通过用weak表来记录weak变 量的,weak表与上文中的引用记数表相似,都是散列表的实现。一个对象被收回时,其在 weak表中的所有weak记录全部被销毁,其weak变量被赋值为nil。所以大量的weak修饰必 然会损耗性能,所以只在有循环应用危险的时候使用weak才是正确姿势。

对于weak修饰符还有一个比较有意思的事情,就是:使用附有weak修饰符的变量就是使用 autoreleasePool中的变量,这一点描述如下:

```
{
    id __weak obj1 = obj;
    NSLog(@"%@", obj1);

    /*编译器模拟代码*/
    id obj1;
    objc_initWeak(&obj1, obj);
    id tmp = objc_loadWeakRetained(&obj1);
    objc_autorelease(tmp);
    NSLog(@"%@", tmp);
    objc_destoryWeak(&obj1);
}
```

由以上可以得知,所有附有weak修饰的变量的使用,均会被注册到autoreleasePool中,使用 几次就会被注册几次,所以过多的使用weak也会造成问题。所以恰当使用weak很重要。

  1. __unsafe_unretained修饰符

此修饰符与weak一样,不会持有对象,但其不属于编译器管理的对象,所以是不安全的, 使用较少(可以在ios5之前,代替weak的作用)。

  1. __autoreleasing修饰符

比较少用。其调用的效果和MRC下调用autorelease的效果相同。

bridge

Objective-c中的对象(Foundation)与Core Foundation的对象没有任何区别,在MRC中, 两者之间可以自由转换,不需要使用额外的cpu资源。但是使用ARC来管理对象的情况下, 需要使用显式的转换,而且转换的安全性有所下降。如下有几种bridge书写方式。

  1. __bridge转换(赋值)

     {
         id obj = [[NSObject alloc] init];
         void *p = (__bridge void *)obj;
         id o = (__bridge id)p;
     }
    
  2. bridge_retained与bridge_transfer

     {
         id obj = [[NSObject alloc] init];
         /*变量p(Core Foundation)也持有对象*/
         void *p = (__bridge_retained void *)obj;
    
         /*被转换变量在被赋值给转换变量之后随之释放*/
         id obj = (__bridge_transfer id)p;
    
     }
    

引用记数作为垃圾回收最原始的方式,在objc中得以发扬光大,由此可见“技术并没有优劣 之分,在进行足够的探索和优化之后也能发挥巨大的作用”。上面是总结了objc内存管理的 一些关键要领,只有从大的引用记数的方向去把握objc的实现方式,才能真正领悟它。

]]>