浅谈开发中提升工作效率的姿势


简介


回想起来,从毕业到现在在iOS这个行业也努(hua)力(shui)了好几年,每每看到同事加班到深夜,于心不忍,故写这篇博客,总结自己这几年写代码的感悟,希望能帮助到那些加班到深夜的程序猿们.这篇博客主要有两个主题,一是代码规范,而是提升效率.虽然两者看似风牛马不相及,但其中的联系可是大大的存在,当你注重了代码规范,那么你的代码质量对应的提升,反正,最终你加班的次数减少就对了~

文章也是随意写的,没有什么顺序,也就想到哪写到哪,各位大佬就当做饭后茶资来看吧~


正确的理解什么叫做写代码,理解业务逻辑的重要性


对于什么叫写代码,什么叫程序猿?首先来谈谈我自己对写代码的观点,开发一个项目其中 50%-70%的工作量为理解业务逻辑,剩余的部分为编写代码,而在编写代码部分70%的工作量为处理异常情况,只有30%的工作量是开发程序.所以理解项目的业务逻辑是非常有必要的,因为业务逻辑决定UI和UE.例如,某个新增数据的Button 当用户没有权限的时候,是不允许点击的,当你不管这些,允许没有权限的用户点击操作该Button,那么就很有可能出现Bug,这是最常见的例子.所以,对于项目中业务逻辑,虽然不用做到倒背如流,但是最少要做到熟读于心.


合理架构代码,提高工作效率


在写代码之前,一定要先去架构自己的代码结构,让它尽量变的合理起来,灵活合理的代码结构会让你更高效的工作,切忌先实现,后优化的理念去架构代码,有些程序猿(这几年遇到不少)就是喜欢使用先实现,后优化的理念去架构代码,或者是连优化都没有,想到一种实现方式就立马开始码代码,结果一堆Bug存在了自己写的代码里面.反正各种隐患,日积月累,Bug越来越多.到最后自己都不想去处理了,而且很多时候还是拆了东墙补西墙的情况,反正种种情况不断. 下面我就分享一下我写代码的两种架构方式.

  • 正所谓业务逻辑决定代码逻辑,所以我们可以通过业务逻辑来架构我们的代码结构.例如,现在业务逻辑中的帖子列表,只有展示和新增的逻辑,你就要立马去想会不会在后面的版本有删除或者修改的功能,或者还有分享的功能呢?是否需要给这些功能预留接口或者位置?用户会不会有其他想法或者操作?每一种业务情景都可能对应着Bug,架构代码之前多考虑业务逻辑是很有必要的.

  • 当你需要修改某个代码模块的时候,这时候你也要先去思考当你修改这个业务逻辑会不会对其他模块造成影响,这里主要可以通过耦合性来去联想其他模块,然后去思考如何架构代码才能让兼容性更好.这样修改代码是否会对后面的代码迭代造成影响?

当然了上面的只是简单的举例而已,有自己认为合理的架构方式欢迎评论.....😂


合理复用代码,业务逻辑代码尽量复用,UI逻辑代码少复用.


复用代码,在很大程度上可以减少代码的重复率,一个重复率很高的代码工程不是一个合格的工程,所以,复用代码是非常有必要的.

但是我们一定要去合理的复用代码,不合理的复用代码会造成的最常见问题就是代码臃肿,耦合度高.例如,我们一个ViewController视图控制器在UI的展现形式上在每一个地方都是一致的,但是每一个地方都需要不同的逻辑,有的是只展示,有的是既展示有可以跳转,有的是只跳转不展示种种逻辑.如果我们都复用这个视图控制器的话,那么这个视图控制器的逻辑代码会非常的多,各个使用这个控制器的模块也会因此变得耦合度高了起来.

那么我们应该遵循一个怎样的复用规律呢?那就是业务逻辑代码尽量复用,UI逻辑代码少复用(PS:安卓的布局文件尽量复用,不涉及逻辑代码,尽量复用).为什么这么说呢?这是因为业务逻辑决定着UI的展现,业务逻辑发生改变,UI一般就发生了改变,相反,只要业务逻辑不发生改变,业务逻辑代码也不需要发生改变.所以,业务逻辑代码尽量多复用,例如网络请求方法,我们写在一个统一的文件中,谁用谁调用即可.只有当后台发生变化的时候,我们才需要修改代码,大大的提高效率.


合理理解'闭环'现象,任何入口代码在用户使用过程中都需要出口代码.


这里我称之为'闭环'现象,也就是说任何入口代码都需要出口代码.当然了,这是我的个人感觉,与其说这是代码习惯,不如说它是我的一种思考习惯.而且我常常通过这种形式来完善我的代码,比如我Push一个界面,我就会想到底有多少种方式Pop到上一个界面?每一种方式会不会有其他的分支情况等等,再例如用户进入了某个状态,怎么样才能回到初始状态?需不需要回到初始状态(当然,在想这种问题都是假设能回到初始状态,完成一个'闭环'现象.)等等, 还有就是下面写到的if 和switch 的完整性问题,我也是常常用到这种思考方式,来验证我的代码是否完整,一个不是'闭环'的代码多多少少都会有点Bug.太深层次的我还没有体验,比如一个对象的创建必然会有对应的销毁过程,等等.


利用百度和Google解决日常问题和Bug.


程序猿日常开发过程中不免遇到这样或者那样的问题或者Bug,那么正确解决问题的姿势是什么呢?

一般情况下,我会分下面几步步骤操作.

  • 一、回想自己以前是否遇到过类似问题或者Bug,自己的博客是否有记录过这种问题(博客是程序猿很好的解决问题途径).有没有听说过类似的问题.
  • 二、回想发现没有该类似问题,那就思考问题可能出现的原因,仔细检查自己的代码逻辑,寻找问题可能出现的位置,打断点验证正确性.
  • 三、还是没有发现问题,这时候,我们就要百度或者Google了,我们要把具体的问题尽量提取出关键字来查询,提高查询效率.比如,日志的错误码或者错误信息等等,都是关键信息.
  • 四、其实上面的三步就已经差不多把问题给解决了,但是还是有一些很具体的问题,怎么办?我们要去回想我们身边的大佬有没有谈及这块的内容,如果有,我们去询问,尽量去询问解决思路,而不是解决方法.比如,当时我学习Java的时候,我就问当时我们老大,我说'老大,有没有相关的书籍或者学习网站呢?',而不是去问'老大,你教教我Java吧' 尔尔之语.最后想别人请教的时候,最好是有偿的,比如发个红包什么的,数量不用太大,这样做有两个原因,一,让别人知道你愿意为知识付费,这样别人以后更喜欢帮助你.二,提醒自己,都TM是钱呐,别随便去请教别人问题,自己动手,丰衣足食.....

说一下反面教材,我曾经碰到不止一个人问我问题,"你好,大佬,我这里有个问题,我把代码发你,你给我看看吧",''大佬,可不可给我解决这个问题?(其实连文章都没看,就让我解决)"如此尔尔,还有很多的人觉得在工作中向别人提问问题是一种好学的体现,但是我要说的是醒醒吧,你已经不在学生时代了,醒醒吧你的老师已经不在你身边了,你去向别人提问问题,让别人给你解决,就有可能是浪费他的工作时间,来帮助你,那他的工作可能就完成不了,被老板骂是他,被老板喷是他.当然了,对于骚栋自己而言,我还是很喜欢帮助别人的,只是不喜欢伸手党而已.


善用 return 和 break 关键词


returnbreak 代码中常用的关键词,其实还有一个关键词continue,这里简单的说明一下三者的作用以及不同之处.return是用来结束一个方法,break是来结束一个循环体,continue是来结束某个循环体中的一次循环.

那么为什么要善于运用 returnbreak 呢? 这主要是当数组遍历的时候,我们已经寻找到了我们想要的数据的时候,我们就可以停止循环体,或者停止函数了,具体是选择return 还是break ,要根据获取到我们想要的数据后续是否还有操作来作为依据.下面我们就举例来说明.

例: 返回数组中元素值为"test"的下标(有且只有一个),并且组成"第x个元素为test"返回,没有则返回nil

  • 在未做优化代码之前, 我们一般会想到我们要在循环体的外部创建一个字符串空对象,然后遍历数组,找到符合条件的下标,组装字符串,然后在循环体外返回.但是这样做就会可能造成性能的浪费,比如要是数组元素个数为10个,符合下标的元素是在第一位,也就是说后面九次的循环都是毫无意义的,从而造成资源的浪费.
- (NSString *)returnThirdItemWithArray:(NSArray *)array {

    NSString *thirdItem = nil;
    for (int i = 0; i < array.count; i++) {
        NSString *item = array[i];
        if ([item isEqualToString:@"test"]) {
            thirdItem = [NSString stringWithFormat:@"第%d个元素为test",i + 1];
        }
    }
    return thirdItem;
}
  • 下面为优化过后的代码, 我们直接把return放在了if当中,这样当在数组中找到合适的元素的时候就会立马跳出函数.不会有过多的性能浪费,我们要把握的时机就是只要当函数满足我们的需求时就停止函数的进行即可.
- (NSString *)returnThirdItemWithArray:(NSArray *)array {

    for (int i = 0; i < array.count; i++) {
        NSString *item = array[i];
        if ([item isEqualToString:@"test"]) {
            return [NSString stringWithFormat:@"第%d个元素为test",i + 1];
        }
    }
    return nil;
}

break关键词和上面的基本一致,主要是用于在当前函数当中跳出循环体时还需要做其他操作.这里就不多细说了.看例子吧~

//返回数组中元素值为"test"的下标(有且只有一个),并且组成"内容为test的元素的下一个下标为xxx".

  • 代码优化之前
- (void)findItemWithArray:(NSArray *)array {
    
    int index = 0;
    for (int i = 0; i < array.count; i++) {
        NSString *item = array[i];
        if ([item isEqualToString:@"test"]) {
            index = i + 1;
        }
    }
    NSLog(@"内容为test的元素的下一个下标为%d",index);
}
  • 代码优化之后
//返回数组中元素值为"test"的第一个下标,并且组成"第x个元素为test"返回,没有则返回nil
- (void)findItemWithArray:(NSArray *)array {
    
    int index = 0;
    for (int i = 0; i < array.count; i++) {
        NSString *item = array[i];
        if ([item isEqualToString:@"test"]) {
            index = i + 1;
            break;
        }
    }
    NSLog(@"内容为test的元素的下一个下标为%d",index);
}


关于 if 和 switch 产生 Bug 的思考


我可说在很多的初级小白百分之五十的Bug都是由于情况考虑不全导致的,那么在体现在代码上是什么样呢?在代码上主要就是ifswitch写的不完整造成情况考虑不全从而产生各种Bug.

我们先说一下if判断语句,什么叫完整的if,什么叫不完整的if,如下代码所示.

  • 不完整的if语句写法
    if (条件) {
        操作
    }

    或者

    if (条件) {
        操作
    } else if (条件) {
        操作
    }
  • 完整的if语句写法
    if (条件) {
        操作
    } else {
        操作
    }

    或者

    if (条件) {
        操作
    } else if (条件) {
        操作
    } else {
        操作
    }

关于完整性的if语句这种做法,很多书很多文章都称之为if语句的穷举法(自己看过<<Effective Objective -C 2.0>>中就有说到),也就是把if所有的情况都列举出啦,哪怕它不需要任何的代码操作.

<<Effective Objective -C 2.0>>PDF版传送门

对比上面的两种if,很多看官又会说到,卧槽,你这是侮辱我智商呢?我刚刚学习编程就会了,只是后面为了方便,所以就不写完整了,其实我工作以来也是基本上很少写完整的if语句,能少些就少些,代码同时整洁易懂.何乐而不为?但是要注意的是,在代码层面上你可以不写完整,但是你在心中一定要去把if语句的所有情况进行穷举,因为每一个if分支情况都可能是一个隐藏的Bug,这可能是业务逻辑方面的,也可能是代码逻辑方面的.所以对if语句进行穷举操作是很有必要的.

那么对于switch是一样的情况,switch中有default关键词,很多时候,我们并不写default部分,但是default部分也算是一个情况分支,这是我们所需要注意.但是有一种情况例外,那么就是switch的判断条件为枚举值的时候,这时候,情况总体个数已经根据枚举值的多少而定下了,所以不需要写default部分了.


列表视图能局部刷新绝对不全部刷新.


对于列表刷新是我们日常开发中最常见的一个操作,例如数据的删除,新增,变动,修改等等都需要我们去刷新列表,很多时候我们都是直接使用[self.mainTableView reloadData];来刷新数据,但是我们仔细想想假定就只有一个或者有限的Cell需要刷新,你使用上面的那句话,那不是白白造成了许多的内存资源浪费吗?所以我们能使用局部刷新绝对不使用全部刷新.

举例子说明,iOS这边我们能使用下面方法就使用下面方法进行局部刷新,虽然在代码量会有所提升,但是不会造成大量的资源浪费.

- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;

- (void)reloadRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;


善用宏定义和枚举,减少魔法数字的使用.


何为魔法数字?就是根本没有任何的解释,随心所欲的写在代码之中的数字,反正就是让人不明觉厉的那种就对了~ 魔法数字的危害性主要会体现在项目后期的维护上,在开发阶段的时候,你根据随手写上了一个魔法数字,可能是宽高信息,可能是边距信息,但是你没有写任何的注释来表明这个数字是怎么来的,是做什么用的,我相信不出三个月,连你去看这个你当前的魔法数字都会觉得很神秘.所以,在你的代码中减少魔法数字扥出现是很有必要的.

那么如何去消除魔法数字这种危害性呢?

一,增加合理注释,解释这个魔法数字是如何产生的,在代码当中有着怎么样的作用,虽然这样可以在一定程度上解决了魔法数字的危害,注释却多的一皮,还有就是后期维护非常麻烦,假设很多的魔法数字分布在你的项目各个角落中,后期你要改的话,需要先去找到这个魔法数字的位置,然后再去修改,是你自己写的代码还好,如果是别人的代码,光这个找的时间,就够自己喝一壶的了~

二,既然使用注释的方式不能完全解决魔法数字问题,我们就看一下使用宏定义和枚举如何解决魔法数字问题.(其实当某一种魔法数字少量的时候,使用注释是完全可行的~,酌情而定)

  • 宏定义方式来定义魔法数字,全局都可能用到的魔法数字,我们就放在pch文件中,如果只是某几个类可能用到的文件,我们就直接创建一个.h文件,然后需要的导入即可,对于单个类使用的魔法数字宏定义,我们直接放在头部即可.这样的方式不但方便管理魔法数字,而且简介明了,后期维护起来也是非常的方便.具体代码示例如下所示.
//pch 文件中的全局宏定义
#define NavigationBarHeight  (44.0f)

#define TabBarHeight  (49.0f)

#define KNormalEdgeDistance  (16.0f)

#define KNormalViewDistance  (10.0f)
#ifndef HomeHeader_h
#define HomeHeader_h

//HomeHeader是所有帖子列表的所需信息主要包含内容高度,Cell图片部分的尺寸

//Cell左右边距
#define EdgeDistance (15.0f * 4)

//Cell顶部边距
#define TopEdgeDistance (15.0f)

//头部分组信息高度
#define HeaderInfoHeight (27.0f)

#endif /* HomeHeader_h */
  • 再来说一下枚举的问题.枚举值也是很好的解决魔法数字的方式,注释是用于状态的展示,如果有两种状态,我们一个布尔值就可以解决了,如果是多种状态,如果不用枚举的话,到时候代码中各种if (style == 1) {}等等魔法数字,完全让人摸不到头脑,各种翻文档,各种翻接口找到对应的业务意义.大大浪费了时间.但是我们如果定了枚举类型了呢? 我们就可以快速的通过字面的意思推测出类型的意义,例如if (style == DrawStyleLine) {},我们可以清楚的明白我们的绘制的样式为线性,定义的枚举类型如下所示.
typedef enum : NSUInteger {
    DrawStyleLine,
    DrawStyleSquare,
    DrawStyleCircle,
    DrawStyleArrow,
    DrawStyleHand,
} DrawStyle;

当然,宏定义和枚举除了能解决魔法数字问题,还能解决书写错误问题,比如我们因为不小心把if (style == 1) {}写成if (style == 10) {}在编译过程中是没有任何错误的,只有在运行过程中才可能暴露出其对应的Bug来,但是我们如果使用宏定义或者枚举,我们书写不全,在编译过程中就直接显示错误,例如把DrawStyleLine写成DrawStyleLina,编译器会直接提示我们书写错误,这样也会有助于避免我们在这些小问题上翻车.


多利用 位移枚举 的位运算实现业务逻辑中多选操作


我们经常会在iOS中的.h看到这样的枚举,例如对于贝塞尔曲线的指定角进行切边操作,用到的枚举类型,如下所示.

typedef NS_OPTIONS(NSUInteger, UIRectCorner) {
    UIRectCornerTopLeft     = 1 << 0,
    UIRectCornerTopRight    = 1 << 1,
    UIRectCornerBottomLeft  = 1 << 2,
    UIRectCornerBottomRight = 1 << 3,
    UIRectCornerAllCorners  = ~0UL
};

这时候我们会发现枚举类型的值并不是我们常见的0,1,2,3等等,而是1 << 0,1 << 2,1 << 3等等,如下图所示,这代表着位运行的表示形式, 示例解释如下所示.

1 << 0 代表着 十进制的 1 左移 0 位 那么就是 0001 (十进制为1,具体运算为1(2^0)),
1 << 1 代表着 十进制的 1 左移 1 位 那么就是 0010 (十进制为2,具体运算为1
(2^1)),
1 << 2 代表着 十进制的 1 左移 2 位 那么就是 0100(十进制为4,具体运算为1*(2^2)),

再来给各位小白恶补一下位运算的几种运算符号的意义

位运算的几种常用运算符号的意义
  • << 左移运算符,就是将某一个整数的二进制整体左移n位,例如 整数5(二进制表示为0101)的位运算 5 << 1,那么结果就是整数10 (二进制为1010);
  • >> 右移运算符,就是将某一个整数的二进制整体右移n位,和左移运算符类似.
  • & 按位与运算符,只有对应的两个二进位均为1时,结果位才为1,否则为0, 例如5&9=1,解释为0101&1001=0001,转化成整数就是1.
  • | 按位或运算符,只要对应的二个二进位有一个为1时,结果位就为1,否则为0, 例如5|9=13,解释为0101|1001=1101,转化成整数就是13.

那么说了这么多,位移枚举的位运算到底有什么的用途呢?其实,这样的枚举任意几个枚举值相加的值(用其 按位或运算即可~) 都是不一样的,不信可以试验一下~我们也就是说可以对枚举值的任意组合进行判断,我们就用UIRectCorner来说明一下,假设我们当我们选择的是UIRectCornerTopLeft和UIRectCornerTopRight的时候,我们就让view的背景色为红色,当我们选择的是UIRectCornerTopLeft和UIRectCornerBottomLeft我们就为黑色,其他的都为白色,示例如下.

if (value == UIRectCornerTopLeft|UIRectCornerTopRight) {
    view.backgroundColor = [UIColor redColor];
} else if (value == UIRectCornerTopLeft|UIRectCornerBottomLeft) {
    view.backgroundColor = [UIColor blackColor];
} else {
    view.backgroundColor = [UIColor whiteColor];
}

有人就会问我们用普通的枚举来做多选会有什么问题,下面我来定义一个枚举类型,大家来看一下,仍然用UIRectCorner来做说明.

// 错误演示
typedef NS_OPTIONS(NSUInteger, UIRectCorner) {
    UIRectCornerTopLeft     = 1 ,
    UIRectCornerTopRight    = 2,
    UIRectCornerBottomLeft  = 3,
    UIRectCornerBottomRight = 4,
    UIRectCornerAllCorners  = 5
};

当我们选择 UIRectCornerTopLeft|UIRectCornerTopRight的时候计算出来的值为3,也就是说选择 UIRectCornerTopLeft|UIRectCornerTopRight和选择UIRectCornerBottomLeft是没有任何区别的.因为我们的判断依据只能是枚举所代表的值.这样就会出现了问题,做不成多选操作,这种类型的枚举只能来做单选操作.

当然了,还是会有人比比用下面的例子说,这样不是也能多选吗?但是 1 就是 1 << 0, 2就是 1 << 1,其他的都是等同的,这里就不多比比了~

typedef NS_OPTIONS(NSUInteger, UIRectCorner) {
    UIRectCornerTopLeft     = 1 ,
    UIRectCornerTopRight    = 2,
    UIRectCornerBottomLeft  = 4,
    UIRectCornerBottomRight = 8,
    UIRectCornerAllCorners  = ~0UL
};


合理理解高内聚,低耦合 控制单个文件的代码量


在上一家公司的时候,那时候的我还是那么天真单纯,当我接手iOS项目时,再一次刷新了我的三观,这是为什么呢?因为这个项目被上一伙人解耦解到支离破碎的,逻辑分散的各个角落中了,简直是惨不忍睹.最后一问原来是有后台开发大佬参与了开发~ 后来我接触了Java后台,我才明白为什么会写的支离破碎,在Java的前后端不分离的webApp中,View和Controller就是完全分离的~但是在iOS中,View和Controller的逻辑在一定程度上是内聚的.当然了,埋怨当时的后台开发人员,毕竟每一种编程语言都有一定的规则.

好了,言归正传,我们来说说高内聚,低耦合的问题,高内聚,低耦合这个概念我相信在学编程之初,你的老师就一定提过,高内聚就是让我们要把相关度比较高的部分尽可能的集中,不要分散.但是一旦过分高内聚,就会造成代码臃肿不堪,业务逻辑混乱复杂的情况,而低耦合就是让我们把两个相关的模块尽可以能把依赖的部分降低到最小,不要让两个系统产生强依赖.但是如果过度低耦合,那么就会造成上面的那种情况,代码逻辑支离破碎,代码可读性非常差.所以具体的高内聚,低耦合的概念如何在你的代码中体现,是需要一定的编程经验的~ 当然,高内聚低耦合这个概念的标准,什么时候该内聚,什么该解耦,在每一个程序猿眼里,我相信都是不一样,有的人认为这个部分应该解耦,认为逻辑堆在这里过于臃肿,但是有的人却认为这里的代码根据业务逻辑就应该堆在这里,可以提高代码的可读性,所以这个标准是只可意会不可言传的,哈哈.只要心中有这个概念,不用刻意去追求,水到渠成即可~

通过内聚和解耦,我们可以合理的控制单个代码文件的代码量,其实我不建议一个代码文件中的代码量太多.这样会造成代码非常的臃肿,可读性也是很差的.比如我以前写过一个列表的九宫格Cell(每一种情况都是一个新的UI),里面的代码超过两千多行,着实是臃肿不堪~维护起来非常的麻烦.这时候,我们就可以把部分的代码抽出来,写在一个新的文件中.当然了,如果实在是解耦不了,我们一定要去加注释,注明这里的代码是干什么用的,为什么要这么做等等,为后期维护或者二次开发做好铺垫工作.


合适使用注释 ,相对于“做了什么”,更应该说明“为什么这么做”


代码千万行,注释第一行;编程不规范,同事两行泪
通过上面的诗句,我们就可以深刻的体会到注释的重要性,从我们开始写第一行的代码时候,很多的大佬就会教导我们一定要把注释写好,写明白,我想大多数程序猿会有这种感觉,如果不写注释,当我们自己会看自己三个月前的代码时,我们都会大声的说一句,"我擦,这是写的什么鬼~ ,这肯定不是我写的",所以写注释,写好注释,这是一种利人利己的行为,何乐为不为?网上很多有很多写注释的重要性的博客或者文章,我想都是深受其害的程序猿~

在写注释的时候,既不能像大姨妈一样拖拖拉拉的写一堆,也不能为了成为卫生标兵就一点也不做注释,合理的注释会让你的代码可读性更高(其实,就算你把注释注的再详细,我想别人也不愿意去看你写的代码,通病而已,😂),我们在写注释的时候,不但要写明这代码是干什么用的,有时候更应该写为什么要去这么写,你当时所想的想法是什么,比如有个计算Cell的高度的时候,由于里面可能有固定高度也可能有可变高度,我一般会在Cell的.h文件中如下图进行类似注释(虽然这个类是用来做tableViewHeaderView的,但是是一样的.).详细的注明,Cell的高度是怎么来的,哪怕是日后再也看这些代码也是很轻松的.要不然,真的就成了"编程不规范,同事两行泪"了.


利用 状态机 和 枚举 完成一个控制器多种状态UI展示效果


状态机的这个概念我第一次接触是在Untiy 3D 做游戏用到的,其实就是一个监控状态流转的模块,例如,一个人有三种操作,一个是停止吃喝,一个是吃饭,一个是喝水.我们可以让一个人从吃饭到停止吃喝,或者从停止吃喝到吃饭,但是我们不能让一个人吃饭直接切换到喝水.因为我们要停止了吃饭,再去喝水,当然了,你非要一手吃饭一手喝水也行~ 请出门右转不送.状态的流转,什么样的状态可以流转到什么状态,不可以流转到什么样的状态,我们做一总结,这就是初步的状态机.

当我们在一个页面中我们有多种UI要展示,而我们要放在同一个控制器中,那么我们就要考虑状态的流转了,其实也就是状态机的问题.如果不做好状态的流转,可能会导致逻辑代码分散到各个位置,反正就是一个字,乱.

这时候,我们要先去理清UI的状态是如何流转的,然后我们要定义枚举,有多少种状态就定义多少种枚举值.例如我做过的一个关于Socket的项目中就有一个界面根据socket的不同状态需要展示不同的UI,枚举代码如下所示.

typedef enum : int {
    ConnectedStateWIFINotConnect,//wifi还未连接
    ConnectedStateWIFIContented,//wifi已连接
    ConnectedStateSocketConnecting,//socket连接中
    ConnectedStateSocketNotConnect,//socket连接失败
    ConnectedStateSocketContented,//socket已经连接
    ConnectedStateSocketDisconnect//socket断开连接
} ConnectedState;

接着,我们需要用switch 来做状态的流转之后的逻辑代码, 有人说为什么不用if else做,我不多说,自行体会去.部分代码(已经做了删减了)如下所示.

- (void)loadSubViewStateAction {
    
    // 删除所有的子控件,然后重新添加,我用的是懒加载的形式,所以不太用考虑性能问题.
    [self.view.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];

    switch ([SocketClinetManager defaultManager].connectedState) {
            
        case ConnectedStateWIFINotConnect:{
            // 当前WIfI未连接  分为未绑定盒子和已绑定盒子
            if ([UserManager defaultManager].connectBoxName == nil) {
                [self.view addSubview:self.bindButton];
            } else {
                [self.view addSubview:self.reloadConnectButton];
                [self.view addSubview:self.boxConnectWifiView];
            }
        }break;
            
        case ConnectedStateWIFIContented:{
            // 当前WIFI已连接
            _socketContentInfoLabel.text = @"设备未连接";
            [self.view addSubview:self.connectBoxButton];
            [self.view addSubview:self.reloadBindButton];
        }break;
            
        case ConnectedStateSocketConnecting:{
            _socketContentInfoLabel.text = @"连接中...";
            [self.view addSubview:self.reloadBindButton];
            [self.view addSubview:self.reloadConnectButton];
            [self socketStateImageViewStartAnimationAction];
        }break;
            
        case ConnectedStateSocketNotConnect:{
            // socket未连接
            _socketContentInfoLabel.text = @"设备连接失败";
            [self.view addSubview:self.reloadBindButton];
            [self.view addSubview:self.reloadConnectButton];
        } break;
        case ConnectedStateSocketContented:{
            // scoket连接成功
            _socketContentInfoLabel.text = @"连接状态正常";
            [self.view addSubview:self.reloadBindButton];
            [self.view addSubview:self.disconnectButton];
        }break;

        case ConnectedStateSocketDisconnect:{
            _socketContentInfoLabel.text = @"设备未连接";
            [self.view addSubview:self.connectBoxButton];
        }break;
    }
}

然后我们只需要修改[SocketClinetManager defaultManager].connectedState的值,然后调用loadSubViewStateAction这个方法就可以得到我们所需要的UI了.其实,这个模块算的上是一个开发经验吧,如果有这种需要可以用上这种模式,这样写我个人感觉把UI状态流转放到一个地方更方便去管理,在代码的可读性上也会有更大的提高.


合理使用懒加载.尤其是在 removeFromSuperView时候,就是两字,真香.


按需加载,是优化代码的一个重要的途径.先说说我自己吧,写了这么多年,一直在使用懒加载的形式创建控件,其实在那些视图要出现的就需要加载完成的控件身上,懒加载并没有提高什么效率,反而让代码量上升了,这种情形最好的好处也就是代码规范整洁,不用所以的控件初始化都挤在一个方法里面,其他别无用途.但是当我们有弹窗这种用户主观调出的视图的时候,我们就可以用懒加载的形式,这样当用户需要的时候,我们才去分配内存,初始化控件,不用在父类初始过程中就要分配内存空间,降低了程序内存峰值,提高了效率.

但是我要说的时候,什么时候使用懒加载是最爽的?那就是当Cell中有个控件,有的数据需要展示,有的数据则不需要展示,我们在配合上下划线直接访问属性的形式,就是两字,真香.

说的再多,也可能是白扯,我们看一下例子~ 这里有一个班级选择列表,当用户选择某个班级的时候,后面才会会出现选中按钮,否则不会出现选中按钮,如下图所示.

Cell中部分代码如下所示.初始化过程中不做任何操作,懒加载还是平常的懒加载.


// 初始化过程中不做任何操作
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
        self.selectionStyle = UITableViewCellSeparatorStyleNone;
    }
    return self;
}

//选中图片的懒加载
- (UIImageView *)selectImageView {
    
    if (_selectImageView == nil) {
        _selectImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"common_class_select_icon"]];
        _selectImageView.frame = CGRectMake(KmainWidth - 48.0f, 0, 48.0f, 48.0f);
        _selectImageView.contentMode = UIViewContentModeCenter;
    }
    return _selectImageView;
}

但是在赋值的时候就是提高性能的时候,我们的想法是Cell上是否含有图片控件,我们都删除.然后重新添加图片控件,这就是我们为什么在这里使用的是_selectImageView的原因,_selectImageView可以不通过set方法直接访问成员属性,所以假设图片控件没有被创建,那么就是[nil removeFromSuperview]了,为什么不用[self.selectImageView removeFromSuperview];呢?因为一旦self.selectImageView就会调用get方法从而创建控件,懒加载也就失去了意义.通俗讲法就是,删除控件的时候,控件没有初始化我就不进行删除,如果有,那么我就进行删除控件操作.从而提高代码效率.

//赋值数据时
- (void)setDataModel:(ClassModel *)dataModel {
    
    _dataModel = dataModel;
    
    [_selectImageView removeFromSuperview];

    if (dataModel.isSelect) {
        [self.contentView addSubview:self.selectImageView];
    }
}


及时解决代码冗余问题


代码冗余这种问题说大不大,说小不小,代码冗余每一个工程都或多或少有这样的问题,其实冗余的代码在业务逻辑上并不会有太大的影响,但是在后期代码维护上是存在着一定的问题,一定程度上增加了阅读难度,所以当你发现自己的代码冗余的时候,一定要及时删除冗余的代码.


学习并使用优秀的三方组件,尝试自己封装一些侵入性低的组件


首先说明一点,虽然以前的我造过不少的轮子,我不太提倡在工作中重复的去造轮子,这主要是因为自己造的轮子可能由于开发时间过短会存在各种问题或者Bug.而且还浪费你的工作时间,降低了工作效率.很多的优秀的三方迭代的较多,所以稳定性很好,如果你在工作中需要某一个三方,我建议先去网上找找看,如果有合适的,尽量使用那么稳定的三方来解决工作中的问题.提高自己的工作效率,这样就不用天天加班到深夜了.

很多优秀的三方组件都是值得我们去学习的,我们可以通过查看组件源码的形式学习开发者当时的开发逻辑,看多了,自然也就懂了.

当然了,在我们业余的时间,我们可以去尝试封装一些入侵性较低的组件,下面有侵入性的解释,侵入性就伴随着耦合问题,所以在封装组件的时候,组件的侵入性是一个很好的衡量组件优劣的方式.

当你的代码引入了一个组件,导致其它代码或者设计,要做相应的更改以适应新组件.这样的情况我们就认为这个新组件具有侵入性.


做好释放工作,完成"闭环"现象


当一块内存被分配的时候,你就需要想这块内存需不需要你自己来释放它(也就是上面提到的"闭环"现象).当你确定这块内存需要你来释放,不妨提前写好释放代码,防止自己遗忘.大大减少因为内存释放问题所造成的Bug或者问题的数量.反正牢记"创建就要销毁"的理念就行了.


不炫技,简单易懂才是最屌的代码


自己学到了某一个新的框架或者组件,总是想着把它使用到我们的项目当中,虽然这样是没有问题的,但是我们忽略了它的稳定性和可读性,一个新的框架可能会存在很多的问题或者Bug,所以代码的稳定性是一个很大的问题,再加上当别人来接手你的代码时,很有可能因为这些新的框架而需要额外的学习时间,从而造成了工作效率的降低,这都是一些潜在的风险.


保持函数的功能单一性,控制每个函数的代码量


我们在构建一个函数之前,我们要思考这个函数到底在我们程序中扮演着什么样的功能模块,从而保持函数的功能单一性,从代码结构上来说,一个功能单一的函数更利于阅读,同时,由于我们需要保持每个函数的功能单一性就必然会去抽离代码,重新组装新的函数,这样每一个函数的代码量都不会有太多.阅读起来相当的轻松.

例如,我写的自定义UITableViewCell都是通过懒加载的方式抽离出代码,如下图所示,这样的代码层次感就出现了,使人更容易理解代码逻辑.而不是把所有的控件初始化都放在init方法中,如果这样做的话,虽然没有任何的问题,但是到底是什么控件添加了什么控件就需要仔细阅读代码了,增加了阅读的难度.


浅谈NSTimer的释放问题


很多iOS初级开发者在使用NSTimer做定时器功能的时候,往往一个不消息就会造成了内存泄露问题,当然了,这也包括我在内,我们来举例说明一下NSTimer的释放问题.

首先,我们来举例子说明一下NSTimer的循环引用.首先我们在ViewController分类中定义一个NSTimer的成员变量.如下所示.

#import "ViewController.h"

@interface ViewController ()

@property(nonatomic,strong)NSTimer *timer;

@end

然后我们在下面的delloc中释放该NSTimer对象.

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
}

- (void)timerAction {
    
}

- (void)dealloc {
    
    if (_timer != nil) {
        [_timer invalidate];
        _timer = nil;
    }
}

@end

然后我们就会发现dealloc方法根本不走,也就是说我们释放不了这个NSTimer对象,这样就循环引用了 ,然后有人就说,你用strong强引用NSTimer对象了.所以释放不了,但是我要告诉就算我改成下面哪种方式,循环引用依然是存在的.

@interface ViewController ()

@property(nonatomic,weak)NSTimer *timer;

@end
@interface ViewController ()
{
    NSTimer *timer;
}

@end

那么NSTimer循环问题到底出现在哪里呢?这个循环引用的根源是在Target上,其实NSTimer的target参数会被RunLoop所持有(此时ViewController对象引用计数为2),也就是说销毁界面的时候,ViewController对象引用计数依然是1,故不能被释放,也就不能走dealloc方法.所以造成了循环引用.

既然知道了问题所在,我们只要打破环中一个位置即可,这里常见的方式就是在用户主动调用的方法中释放NSTimer,先释放NSTimer,然后RunLoop释放了对ViewController对象的持有,ViewController对象的引用计数变为1,然后销毁界面ViewController对象的引用计数变成0,对象被成功销毁,如下所示.

//假设是导航控制器 Push的界面
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    if (_timer == nil) {
        [_timer invalidate];
        _timer = nil;
    }
    [self.navigationController popViewControllerAnimated:YES];
}

// dealloc不做操作
- (void)dealloc {

}

在iOS 10 出现了一个新的NSTimer构建方法,那就是使用block的形式,虽然能解决上的问题,但是依然需要注意block中self的循环引用问题,具体方法如下图所示.

总结NSTimer释放秘诀就是下面的这句话.

通过用户主动操作调用的方法中来释放NSTimer,任何时候都不要在dealloc中释放NSTimer.


不建议在if中添加过长的判断语句,如果需要,那么就分行显示,提高代码的可读性


if分支语句中的判断条件有时候很多,如下所示.如果我们顺着写,代码不但臃肿了~ 阅读起来及其的不方便.这时候我们就可以使用分行显示的形式,来展示我们的判断条件.提高阅读效率.如下所示.

        // 未优化之前
        if ([test isEqualToString:@"条件1"] || [test isEqualToString:@"条件2"] || [test isEqualToString:@"条件3"] ) {
            
        }
        // 优化之后
        if ([test isEqualToString:@"条件1"] ||
            [test isEqualToString:@"条件2"] ||
            [test isEqualToString:@"条件3"]) {
            
        }

当然假设某一个条件过长的时候,我们也可以利用抽离的方式,让代码看起来整洁大方.具体例子如下所示.

        // 未优化之前
        if ([test isEqualToString:@"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"] ||
            [test isEqualToString:@"条件2"] ||
            [test isEqualToString:@"条件3"]) {
            
        }
        // 优化之后

        BOOL firstCondition = [test isEqualToString:@"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"];
        if (firstCondition ||
            [test isEqualToString:@"条件2"] ||
            [test isEqualToString:@"条件3"]) {
            
        }


代码风格要规范统一,做到自己现有水平的最好标准.


自己的代码一定要有统一的风格,一个良好的代码风格是一个程序猿最基本的要求,一个良好的代码风格可以让别人在阅读自己写的代码时候更加轻松.万万不可在每一个代码文件中都有着不同的风格,这样在阅读代码的时候肯定是非常难受的.写代码的时候我们要做到自己现在水平的最好,要像对待自己的孩子一样对待自己的代码,你只有呵护它,它更好的才能回报于你.下面我们就谈一下几种常见的代码规范.可以稍微参考.

参考于<<如何提高代码的可读性? - 读《编写可读代码的艺术》>>

  • 命名规范问题


命名尽量使用驼峰命名法,命名的时候不可随意取名,例如 action1 ,尽量要做到见名知意.


我们知道驼峰命名可以很清晰地体现变量的含义,但是当驼峰命名中的单元超过了3个之后,就会很影响阅读体验:
userFriendsInfoModel
memoryCacheCalculateTool
是不是看上去很吃力?因为我们大脑同时可以记住的信息非常有限,尤其是在看代码的时候,这种短期记忆的局限性是无法让我们同时记住或者瞬间理解几个具有3~4个单元的变量名的。所以我们需要在变量名里面去除一些不必要的单元.(PS:这一点我还真没做到....😂)


不能使用大家不熟悉的缩写
有些缩写是大家熟知的:
doc 可以代替document
str 可以代替string
但是如果你想用BEManager来代替BackEndManager就比较不合适了。因为不了解的人几乎是无法猜到这个名称的意义的。
所以类似这种情况不能偷懒,该是什么就是什么,否则会起到相反的效果。因为它看起来非常陌生,跟我们熟知的一些缩写规则相去甚远。


  • 提高代码的美观性


在声明一组变量的时候,由于每个变量名的长度不同,导致了在变量名左侧对齐的情况下,等号以及右侧的内容没有对齐:

NSString *name = userInfo[@"name"];
NSString *sex = userInfo[@"sex"];
NSString *address = userInfo[@"address"];

而如果使用了列对齐的方法,让等号以及右侧的部分对齐的方式会使代码看上去更加整洁:

NSString *name    = userInfo[@"name"];
NSString *sex     = userInfo[@"sex"];
NSString *address = userInfo[@"address"];

这二者的区别在条目数比较多以及变量名称长度相差较大的时候会更加明显。


当涉及到相同变量(属性)组合的存取都存在的时候,最好以一个有意义的顺序来排列它们:

  • 让变量的顺序与对应的HTML表单中<input>字段的顺序相匹配
  • 从最重要到最不重要排序
  • 按照字母排序

举个例子:相同集合里的元素同时出现的时候最好保证每个元素出现顺序是一致的。除了便于阅读这个好处以外,也有助于能发现漏掉的部分,尤其当元素很多的时候:

//给model赋值
model.name    = dict["name"];
model.sex     = dict["sex"];
model.address = dict["address"];

 ...
  
//拿到model来绘制UI
nameLabel.text    = model.name;
sexLabel.text     = model.sex;
addressLabel.text = model.address;


有些时候,你的某些代码风格可能与大众比较容易接受的风格不太一样。但是如果你在你自己所写的代码各处能够保持你这种独有的风格,也是可以对代码的可读性有积极的帮助的。

比如一个比较经典的代码风格问题:

if(condition){

}

or:

if(condition)
{

}

对于上面的两种写法,每个人对条件判断右侧的大括号的位置会有不同的看法。但是无论你坚持的是哪一个,请在你的代码里做到始终如一。因为如果有某几个特例的话,是非常影响代码的阅读体验的。


熟悉常用的颜色以及颜色的表示方式


作为一个程序猿,日常开发中在UI方面说的最多的可能就是"RGB值","#f5f5f5","RGBA"等等,每一天都和各种各样的颜色打交道,但是我见过很多的程序猿对颜色这块的知识实在太少了,今天,我们就简单地聊聊关于颜色一些常识.

而我们在日常中UI给我们最多的就是RGB值表示颜色,例如下下图所表示的 "#F500CC",那么其中F5代表着红色的十六进制值,00代表绿色的十六进制值,CC代表着蓝色的十六进制值,我们知道十六进制是从0到F,所以两位的十六进制最大值为16*16 = 256 (十进制),这也就是256颜色值的来由.三者全都是256的值那就是#FFFFFF(白色),三者都是0的值那就是#000000(黑色).

那么,现在我们就可以创建一个简单的颜色,比如我只想要红色,那么我们就让红色的值不为0,然后绿色和蓝色的值都是0即可,比如#FF0000,就是最满的红色,当红色的值变小时,颜色逐渐趋于黑色,我们可以通过下面来来了解这种变化.

再例如,我们可以通过本模块的第一个图来调出黄色,黄色就是红色加上绿色,那么RGB十六进制表示方式就为#FFFF00.其他的颜色以此类似.

我们接下来说一个比较有意思的颜色,那就是灰色,很多专业属于称之为中性灰,灰色是怎么来的呢?灰色其实就是RGB三个值是一样的即可. 例如 #C0C0C0 , #F5F5F5等等,只有是#ABABAB或者#AAAAAA的形式都是中性灰.这里还要说一个中性灰叫做 #808080 ,很多人称之为绝对中性灰.


写博客是一个快速提高的姿势


从我刚开始工作开始,我就一直在写博客,虽然也是中间也是断断续续,但是我绝对要说写博客是让一个程序猿快速成长的良好途径.当然了,我说的写博客并不是让你去抄网上的博客,一顿CV完了之后就没事了,写博客主要是写自己的学习代码和记录问题,而不是去抄袭,就算是抄袭同一个问题,你也要加上自己的观点,谈谈你对这个问题的理解.而不是抄一顿就以为万事大吉了,这种想法是万万要不得的.

可能一开始学博客会觉得很困难,语句组织不行,没有什么要写的,这时候你可以分析一下当时你对这个问题或者Bug的理解,你是怎么想的,怎么思考的,怎么解决的,然后有什么感悟,这些都是可以写上去.完全可以用大白话来说这些问题,我敢保证,你这样写十篇博客之后,你就知道你该如何组织你的语言了~

有人会问你比比这么多,那么写博客到底有什么好处?其实我在以前的博客中有写到过,这里我再总结一下,主要有以下的三点好处.

  • 博客是日常开发的笔记,一个程序猿是不能记太多的代码的,这从我们成为程序猿的第一天就知道,所以我们要做的就是知道这个问题如何解决,当我们发现了一个新的问题或者Bug,可能第一次花了我们很长时间才解决这个问题,如果我们不做记录的话,那么下一次我们虽然知道思路,但是依然需要做很长时间的修正,可能比第一次提高20%-30%的效率,但是当我们写博客做笔记呢?下次遇到这样的问题我们就会立马知道我们的博客中对应的解决方案,然后我们可以快速的找到解决方案,这样工作效率最少提升50%-60%,这就是小时候我们常说的"好记性不如烂笔头"的道理~~~~~

  • 如果出去面试,如何看出一个人的能力?这里有两个程序猿,水平差不多,一个有着写博客习惯,另外一个不写博客习惯,我相信很多人会选择前者,博客虽然不能概括出你所有的能力,但是最少能从其中窥其一二,所以,坚持写博客是很有必要的.

  • 写博客还有个好处就是帮助别的程序猿,当你发表了一篇关于某个问题解决方案的博客,可能更多的程序猿因此而解决问题,这是一个很好的过程,因为我的博客帮助很多人,虽然这是无偿的,但是我依然非常高兴.再就是可能有人可能对你的博客有着不同的理解,他们可能提出一些更好的方案或者解决办法.通过这样的方式你可了解更多更优的解决方案,助人即助己...


结语


这篇博客写了好几天,算的上是技术杂谈吧,如果有任何问题,欢迎随时在下面评论,谢谢~


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,050评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,538评论 1 306
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,673评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,622评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,047评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,974评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,129评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,893评论 0 209
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,654评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,828评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,297评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,619评论 3 262
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,326评论 3 243
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,176评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,975评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,118评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,909评论 2 278

推荐阅读更多精彩内容