iOS代码规范 (好代码其实不如“好看”的代码^^)

编写的公司iOS端代码规范 (已经脱敏了)

注释规范

  • 好的注释可以大大的提高代码的阅读性、也能让自己快速的看懂自己几个月前写的代码(……_)

  • 类注释需要在 interface 之上,这样可以在其他地方看到该类的注释

    /******************************************************************************************************************************
     这里写注释
     ******************************************************************************************************************************/
     @interface ViewController : UIViewController
    
  • UI类注释 在.h文件头部添加描述和当前UI的雏形,"X" 代表不隐藏, “O”代码隐藏;当该cell有多种排版时,需要一一画出来

    /**
     列表模板视图2:
      --------------------------
     | XXXXXXXXXXXXX    -----   |
     | XXXXX           |     |  |
     | xxxxxxxxxxx     |     |  |
     |                  -----   |
     | xx OO           OOO xxx  |
      --------------------------
     */
    
  • 功能类注释 除了UI界面外,其他的类最好在.h文件头部中添加一个描述,阐述一下功能和思路,如下

    //
    //  xxxxxx.h
    //  bookclub
    //
    //  Created by youhui on 2018/11/21.
    //  Copyright © 2018年 luke.chen. All rights reserved.
    //
    /******************************************************************************************************************************
     功能:
     用来解决vc生命周期已经执行,但是却被其他图层挡住,导致无法准确的拿到页面“可见化”的时机
     通过传入target 在debut、each 两个block中可以分别拿到首次、每次的“可见化”回调
     思路:
     1、采用CADisplayLink 这个和屏幕同频率的定时器,监听keywindow 是否被占用.通过block 传回给调用方的页面 debut(初次出现),each(每次出现),eachDismiss(每次消失)的生命周期时机
     2、判定keywindow是否被占用: 存在不是tabBar的容器但却覆盖屏幕的图层,则判定为window被占用 (有可能存在两个视图拼接起来覆盖屏幕的图层,当前版本尚未发现,后续优化)
     ******************************************************************************************************************************/
    
  • 函数注释--.h文件

    /**
     开始监听位置(一次),并在block中把 地址/经纬度传回来
     
     @param needAreaId NO:不会请求区域码 
     @param addressBlock callBackBlock
     */
    - (void)xxxxxx:(BOOL)needAreaId addressBlock:(BCLocationManagerAddressBlock)addressBlock;
    
  • 函数注释--.m文件 一般情况下,在.m文件中,不需要那么详细的注释,简单注释能看明白函数的作用即可,太长会影响可读性,需要查看详细的细节,可以到.h中查看

    //开始监听位置(一次),并在block中把 地址/经纬度传回来
    - (void)xxxxxx:(BOOL)needAreaId addressBlock:(BCLocationManagerAddressBlock)addressBlock {
    
    }
    
  • 属性注释--.h文件 如果必须要传,要在注释的首行加上 “@required”

    /**
    @required
     每次block
    
     @param times “可见化”次数
     */
    typedef void(^BCLifeCircleManagerEachBlock)(long long times);
    
  • 属性注释--.m文件 紧贴着属性的定义后面注释

    @property (nonatomic, readwrite, strong) CADisplayLink *displayLink;//屏幕刷新同步器
    
  • 业务代码注释 在编写业务代码时,往往会是一串又长又臭的代码,(原则上,一个函数最好不要超过100行)这个时候,需要对代码进行模块话的注释,同一类功能添加一个注释.如下所示

    //用户角色改变
    - (void)xxxxx:(NSNotification *)noti {
        //重置
        xxxxx
    
        //重新请求
        xxxxx
    
        //清除数据
        xxxxx
        xxxxx
    
        //音频重置
        xxxx
        xxxx
    
        //视频重置
        xxxxx
        xxxxx
    }
    

页面规范

  • 推荐使用懒加载自动懒加载插件,具有高可阅读性的优点
  • 复制以下代码 在Xcode 中 添加一个用户自定义代码块,支持快捷缩写关联出页面脚手架
    • ViewController

      //生命周期
      #pragma mark - Lifecycle
      - (void)dealloc {
          NSLog(@"%@dealloc",self);
      }
      
      - (void)viewDidLoad {
          [super viewDidLoad];
          [self loadSubviews];
          // Do any additional setup after loading the view.
      }
      
      //视图的初始化,层级结构都在这里
      #pragma mark - LoadSubViews
      - (void)loadSubviews {
      
      }
      
      //视图的布局,刷新
      #pragma mark - LayoutSubViews
      - (void)layout {
      
      }
      
      //重写父类放这里
      #pragma mark - Overwrite
      
      //公开方法
      #pragma mark - Public Method
      
      //隐私方法
      #pragma mark - Privacy Method
      
      //网络请求
      #pragma mark - Request <#netRequest#>
      
      //通知方法
      #pragma mark - Notification Obverser Method
      
      //代理
      #pragma mark - Delegate
      
      //get set 懒加载
      #pragma mark - Get and Set
      
    • View

      //生命周期
      #pragma mark - Lifecycle
      - (void)dealloc {
          NSLog(@"%@dealloc",self);
      }
      
      //视图的初始化,层级结构都在这里
      #pragma mark - LoadSubViews
      - (void)loadSubviews {
      
      }
      
      //视图的布局,刷新
      #pragma mark - LayoutSubViews
      // tell UIKit that you are using AutoLayout
      + (BOOL)requiresConstraintBasedLayout {
          return YES;
      }
      
      // this is Apple's recommended place for adding/updating constraints
      - (void)updateConstraints {
          [self layout];
          [super updateConstraints];
      }
      
      //布局
      - (void)layout {
      
      }
      
      //重写父类放这里
      #pragma mark - Overwrite
      
      //公开方法
      #pragma mark - Public Method
      
      //隐私方法
      #pragma mark - Privacy Method
      
      //网络请求
      #pragma mark - Request <#netRequest#>
      
      //通知方法
      #pragma mark - Notification Obverser Method
      
      //代理
      #pragma mark - Delegate
      
      //get set 懒加载
      #pragma mark - Get and Set
      
    • Model

      //生命周期
      #pragma mark - Lifecycle
      
      //重写父类放这里
      #pragma mark - Overwrite
      
      //公开方法
      #pragma mark - Public Method
      
      //隐私方法
      #pragma mark - Privacy Method
      
      //网络请求
      #pragma mark - Request <#netRequest#>
      
      //代理
      #pragma mark - Delegate
      
      //get set 懒加载
      #pragma mark - Get and Set
      

属性规范 (属性方面,主要关注 .h 暴露出来的属性。)

  • 写代码中最难的一个步骤之一可以说是命名,在给属性命名时,在不是非常的长的情况下可以适当的做到尽量做到顾名思义,如需要给view 传递一个数据源,下面给出了三种命名:
array
dataArray
interfaceDataArray(**最优,能一眼就看出该数组的作用**)
  • 在.h文件的头尾分别加上 NS_ASSUME_NONNULL_BEGIN / NS_ASSUME_NONNULL_END, 在这两个宏之间的代码,所有简单指针对象都被假定为nonnull
  • "," ")" "@property" 之后需要加 空格 ; "*****"之前需要加 空格
@property空格(nonatomic,空格readwrite,空格strong)空格NSArray空格*interfaceDataArray;
  • 不可变的属性使用Copy, 可变的属性使用Strong(在需要保护封装性的前提下)
  • 如果必须要赋值,要在注释中使用 @required注明
  • 功能上不需要就不暴露,暴露了就需要足够的信息或备注来说明,如下,interfaceDataArray 属性可以说明
  • 尽量使用Fundation 框架的数据类型,如使用CGFloat 来替换float
    • 第一档:只有一个简单的nonatomic 和 strong 的来修饰

      @property (nonatomic, strong) NSArray *interfaceDataArray;
      
    • 第二档:除了nonatomic 和 strong 之外 还多了一个readOnly字段,表明了该属性为“只读”属性,你并不需要费劲心思的给他赋值

      @property (nonatomic, readOnly, strong) NSArray *interfaceDataArray; 
      
    • 第三档:nullable类型的属性可以说明支持 nil

      @property (nonatomic, readwrite, strong, nullable) NSArray *interfaceDataArray; 
      
    • 第四档:容器属性的 内部值类型,最好能一目了然,这样写就可以很直观的看出该属性是一个“包含了字符串数组的一个大数组”

      @property (nonatomic, readwrite, strong, nullable) NSArray<NSArray<NSString *> *> *interfaceDataArray; 
      
    • 第五档:这里用copy替换掉了strong,被其他数组赋值时,会重新拷贝一份,不会和赋值的数组内存地址一致,从而保护了该属性的封装性。

      @property (nonatomic, readwrite, copy, nullable) NSArray<NSArray<NSString *> *> *interfaceDataArray; 
      
    • 第六档:如果你有一个很详细的注释,那么你完全可以忘记之前的五种类型~

      /**
       @required
       外部传入数据源
       在外部数据更新时需要对该属性进行赋值刷新
       */
      @property (nonatomic, readwrite, copy, nullable) NSArray<NSArray<NSString *> *> *interfaceDataArray;
      

函数规范

  • 遵守驼峰命名
  • 一个文件里面统一使用一种括号模式
- (void)aaa {

}
  • 不使用 with、and 来连接各个参数
  • 尽量采用 instancetype 来替换 id
  • 提供初始化方法时,尽量考虑到使用方的便利,可以提供多个不同入参的方法 例如AF框架中的AFHTTPSessionManager 是这样定义初始化函数的:
- (instancetype)init {
    return [self initWithBaseURL:nil];
}

- (instancetype)initWithBaseURL:(NSURL *)url {
    return [self initWithBaseURL:url sessionConfiguration:nil];
}

- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
    return [self initWithBaseURL:nil sessionConfiguration:configuration];
}

- (instancetype)initWithBaseURL:(NSURL *)url sessionConfiguration:(NSURLSessionConfiguration *)configuration {
......
}

宏定义规范

  • NSUserDefault (BC_ + UDF_ + 命名),如下所示
#define BC_UDF_xxxxx @"BC_UDF_xxxxx"//购买专辑成功
  • 通知 (BC_ + NOTI_ + 命名),如下所示
#define BC_NOTI_aaaaa @"BC_NOTI_aaaaa"//节目列表页面购买成功
  • 常数 (BC_ + K_ + 命名),如下所示 (包括字符串、数字等固定常量)
#define BC_K_bbbbb @"2"//每日荐听
  • 计算 存在计算的宏定义,需要对计算部分添加括号,否则在用该宏参与计算的时候会因为符号优先级不一定能得到想要的结果,如下所示
#define BC_K_GetSum(a,b) a + b
BC_K_GetSum(1,2) * 5 //11
    
//所以应该加上括号
#define BC_K_GetSum(a,b) (a + b)
BC_K_GetSum(1,2) * 5 //得到想要的15
  • 生产环境禁止Log (需要在xcode中添加DEBUG 配置,并复制以下代码到项目对应的文件中)
#ifdef DEBUG
#define NSLog(...) NSLog(__VA_ARGS__)
#define debugMethod() NSLog(@"%s", __func__)
#else
#define NSLog(...)
#define debugMethod()
#endif
  • Weak / Strong
#define WeakObj(o) autoreleasepool{} __weak typeof(o) o##Weak = o;
#define StrongObj(o) autoreleasepool{} __strong typeof(o) o = o##Weak;

//例如
@WeakObj(self);
self.block =  ^{
    @StrongObj(self);
    [self xxxx];
}];

枚举规范

  • NS_ENUM

    • NSInteger/NSUInteger 类型,直接赋值
    typedef NS_ENUM(NSInteger, BCHttpResponseType) {
            BCHttpResponseTypeA = 0, //这里是注释
            BCHttpResponseTypeB = 1, //这里是注释
            BCHttpResponseTypeC = -1, //这里是注释
            BCHttpResponseTypeD = -2, //这里是注释
            BCHttpResponseTypeE = -3, //这里是注释
            BCHttpResponseTypeF = -4, //这里是注释
            BCHttpResponseTypeG = -5, //这里是注释
    };
    
  • NS_OPTIONS

    • 固定 NSUInteger 类型,一般用来进行二进制左右位移计算,当然也可以直接赋值.使用 NS_OPTIONS 的优点是可以通过 “|” 符号来链接多个枚举,达到多选的效果.
    //颗粒化视频遮罩层的组件,可用|自由组合不同层,高度可定制
    typedef NS_OPTIONS(NSUInteger, BCVideoMaskLayer) {
        BCVideoMaskLayerA = 0,//这里是注释
        BCVideoMaskLayerB = 1 << 0,//这里是注释
        BCVideoMaskLayerC = 1 << 1,//这里是注释
        BCVideoMaskLayerD = 1 << 2,//这里是注释
        BCVideoMaskLayerE = 1 << 3,//这里是注释
        BCVideoMaskLayerF = 1 << 4,//这里是注释
        BCVideoMaskLayerG = 1 << 5,//这里是注释
        BCVideoMaskLayerH = 1 << 6,//这里是注释
        BCVideoMaskLayerI = 1 << 8,//这里是注释
        BCVideoMaskLayerJ = 1 << 9,//这里是注释
        BCVideoMaskLayerK = 1 << 10,//这里是注释
        BCVideoMaskLayerL = 1 << 11,//这里是注释
        BCVideoMaskLayerM = 1 << 12,//这里是注释
        BCVideoMaskLayerN = 1 << 13,//这里是注释
        BCVideoMaskLayerO = 1 << 14,//这里是注释
        BCVideoMaskLayerP = 1 << 15,//这里是注释
        BCVideoMaskLayerQ = 1 << 16,//这里是注释
        BCVideoMaskLayerR = 1 << 17,//这里是注释
        BCVideoMaskLayerS = 1 << 18,//这里是注释
        BCVideoMaskLayerT = 1 << 19,//这里是注释
    };
    

内存规范

  • Dealloc 一定要执行
    页面:在页面的脚手架中的Lifecycle分类,需要实现dealloc 函数,并打印当前的类,在页面功能开发完成之后,最好要测试一下页面的dealloc 是否被调用.如果没有被及时调用,那么就存在内存泄漏,需要对页面的代码进行排查.

  • 添加的监听需要在合适的地方进行释放

    • 如通知监听,定时器等
  • block的循环引用.

    • 原因 在block中很容易因为当前类持有该block,而block中的代码又引用当前类,产生循环引用,无法释放.
    • 处理 在block外部定义弱引用(self),在block内部强引用(self)
  • delegate 没有主动释放

    • 原因 如果当前类持有了如友盟单例等代理,那么会因为单例一直存在,导致单例会一直对当前类进行持有,导致无法释放.
    • 处理 需要在页面退出时或者其他合适的时机,手动注销delegate
    • (例子)正常点击 A 的返回按钮退出
      这里想要如期所愿的释放 B、C,那么需要移除B、C 身上的所有引用,如上图所示,B身上有两个箭头,代表两个引用.那么当A释放时,B身上只剩下一个引用,因为A释放,D身上的唯一引用也被移除,D也随之被释放了,所以间接移除了D对B的引用,B在A释放的同时可以正常的被释放;
      那么问题来了,那么当A释放时,按照上面的逻辑,D和A对C的引用也会被随之移除,但是因为E还保持着对C的引用,所以C并没有随A的释放而释放.所以,想要让C也跟随A一起释放,可以在A的dealloc 中手动解除E对C的持有即可.
    graph LR
      A(SuperVC-A) -->|持有|B((ChildVC-B))
      A -->|持有|C((ChildVC-C))
      A -->|持有|D((View-D))
      D -->|持有|B
      D -->|持有|C
      E(ShareInstance-E) -->|持有|C
    
  • 因系统或者其他情况导致的特殊情况.

    • (例子)在子页面内点击按钮直接返回到RootVc,且切换到其他tab分栏,结构图如下所示
      这里和上面点击返回情况不同的是,因在pop到RootVc的同时切换到其他tab,导致系统无法及时对RootVc下的一些内存进行回收.导致在切回到RootVc所在分栏之前,D一直没有被回收,也就是通常所见的野指针.那么这种情况下,如果还想让B、C随A一直释放,可以在A释放的同时,额外手动释放D对的B、C的持有即可
    graph LR
      A(SuperVC-A) -->|持有|B((ChildVC-B))
      A -->|持有|C((ChildVC-C))
      A -->|持有|D((View-D))
      D -->|持有|B
      D -->|持有|C
      E(ShareInstance-E) -->|持有|C
      H(野指针) -->|在点击按钮直接退出到root且切换tab的情况下,导致的没有被释放|D
    
  • 当以上的一些方法都找不出问题时,可以通过profile的instruments的来进行调试定位到具体的代码.

布局规范

  • RunLoop相关参考

  • 布局相关参考

  • RunLoop 中 苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数,_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面,而约束布局的视图一般和这几个函数相关,从而进行约束、布局、绘制 (具体)
    [图片上传失败...(image-6a68ff-1560592762925)]

  • 优先使用自动布局(masonry)

    • ViewController : 可以在viewDidLoad 生命周期中/或者接口返回后手动调用layout
    - (void)viewDidLoad {
     [super viewDidLoad];
     [self layout];
    }
        
    - (void)layout {
    //此处为布局代码
    }
    
    • View : 使用上面提到的脚手架 ,其中重写了 requiresConstraintBasedLayout 和 updateConstraints 方法,并在layout 函数中 进行代码自动布局,在需要更新布局时,执行[self setNeedsUpdateConstraints] 即可,无需在其他地方主动执行 layout函数
    #pragma mark - LayoutSubViews
    // tell UIKit that you are using AutoLayout
    + (BOOL)requiresConstraintBasedLayout {
        return YES;
    }
    
    // this is Apple's recommended place for adding/updating constraints
    - (void)updateConstraints {
        [self layout];
        [super updateConstraints];
    }
    
    //布局
    - (void)layout {
    //此处为布局代码
    }
    
    //在需要更新布局时,调用setNeedsUpdateConstraints 即可
    - (void)xxx {
            [self setNeedsUpdateConstraints];
    }
    
  • 在某些场景可以使用frame布局,如下

    • layer层参与布局 (因为layer 不支持自动布局)
    • 动画和自动布局冲突时,如下,这样使用就会发生一些奇怪的问题
    [UIView animateWithDuration:.5 animations:^{
        [xxx mas_updateConstraints:^(MASConstraintMaker *make) {
           xxxx
        }];
        //[xxx layoutIfNeeded];
     }];
    

资源规范

  • Assets 中应该建有和模块相应的文件夹,存放该模块中需要的icon
  • icon名字需要统一改为英文名字,并在文件名结尾标明 @2x,@3x ,系统能自动识别两种资源.获取对应的资源
  • gif、plist 等其他文件,因为不能存放在Assets中,需要在对应的下模块新建Resource文件夹存放
  • 放入资源之前,需要提前检索是否已经有同名的资源
  • 不需要的资源,需要定期删除(可以在该版本结束时,进行删除,包括不需要的代码)

第三方库规范

  • 使用cocoaPods进行管理
  • 在cocoaPods 的 ".gitignore" 屏蔽文件中,需要加上 Pods/ 、Podfile.lock (pod只在本地管理,有其他同事新增时可以直接pod udpate 新的)
  • pods中的每个三方库都需要注明作用,如下
# Uncomment the next line to define a global platform for your project
platform :ios, '8.0'
target 'xxxx' do
    # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
    # use_frameworks!
    # Pods for xxx
    pod 'AFNetworking', '3.2.0'         #网络
    pod 'MBProgressHUD', '1.1.0'        #提示框
    ....
    target 'xxxTest' do
    inherit! :search_paths
    # Pods for testing
    end

    target 'xxxUITests' do
    inherit! :search_paths
    # Pods for testing
    end
end

MVVM规范

  • 文件夹


    m_4752a2380b3175fcbb6d92de2dd9fbbd_r.jpg
  • 基本规则

    • view 或 vc 持有 viewModel 、viewModel 持有 model
    • viewModel 不持有view,且最好不引入UIKit框架
    • view 不持有 model
graph LR
A(View/ViewController)-->|持有|B(ViewModel)
B-->|持有|C(Model)
B---|不引入|D(UIKit框架)

解析规范

  • 尽量使用model 来承载数据,使用 ParseToModel 工具类来解析数据(如下代码所示)
- (xxxxActivityModel *)xxxxModelWithResp:(NSDictionary *)response {
    NSDictionary *activityInfo = [response objectForKey:@"bbbb"];
    xxxxActivityModel *activityModel = [ParseToModel parseDictionary:activityInfo byClassName:@"xxxxActivityModel"];
    activityModel.aaa = [[response objectForKey:@"aaaa"] longLongValue];
    return activityModel;
}
  • 解析出的数据可能是null,在需要使用时,需要进行判空处理(如下代码所示)
NSString *xxxx = [hotBook bc_jsonString:@"xxxx"]?[hotBook bc_jsonString:@"xxxx"]:@"";
NSString *aaaa = [hotBook bc_jsonString:@"aaaa"]?[hotBook bc_jsonString:@"aaaa"]:@"";
return @{@"xxxx":xxxx, @"aaaa":aaaa, @"cccc": @"c"};

跳转规范

  • 在路由中有配置的跳转使用路由跳转
  • 如果路由中没有配置,那么放在对应的vc中,尽量别放在view/viewModel/model 中

ps:喜欢的点个赞哈,或者赞赏支持~