最近在日常的开发过程中,发现了很多问题。思考了很多关于编写可维护性代码东西,以及一些关于程序的看法。

最早发现这个问题,起源于我想做一个需求,这个需求呢,很简单,就是点击一个按钮,弹出一个分享的界面,点击可以分享到 QQ,微信,微博等地方。作为一个已经持续了 2 年的项目,我觉得应该有现成的封装好的代码,只需要我调用一下,传一些分享的相关数据就可以了。可是当我找了一圈后发现,所有的关于分享的代码,全部都是复制粘贴的。我看了下,发现里边竟然有七八处,除了个别地方不一样,其他 95% 的代码都是一样的。当时感觉一脸懵逼,外加黑人问号。

依稀记得,当年实习刚入职 京东。接到的第一个需求就是优化一下分享,当时我就知道要把这个东西封装起来,让别人尽可能的调用起来方便。要考虑到分享渠道可配置,分享标题,内容,链接,图片等等,都要可传参,可配置。而这个工程对接的还是 友盟分享,一个功能上是相当齐全的库,结果还做成这个样子,每次使用分享,要复制粘贴一份。我头都大了。

第二个事情呢,就在前两天,我要做另外一个需求,还是一个点击事件,根据不同的参数,来决定跳转到那个页面,我当时问了一下,貌似有对应的功能,我当时还是很开心的,意味着我可以早早完成。结果,当我去调用的时候,发现跳不过去。而当我费尽心机搞定跳转的时候,我看了下里边的代码,发现了这样的代码。

这代码看的我真是虎躯一震

遍地的 if else,遍地的硬编码,遍地的随意拼接字符串,感觉要裂开了。

硬着头皮看完之后,我手贱,搜了一下,发现工程内,有六七处一样的代码,我的妈呀,这真的是酸爽啊。

写到这里,关于吐槽就完了。后边大致说一下这几天想的一些东西吧。

👉编写可维护性代码

最近看 代码简介之道 感觉挺不错的,里边的一些 tips 真的有用,可以让大家写出清晰,简介,易于维护的代码

我们在日常开发的过程中,大部分的程序员都不会编写很高深莫测的代码,都是常规的业务性逻辑。清晰易懂的业务代码,可以为后期的维护带来极大的便利性。

👉模块化

面向对象的三大要素,继承,封装,多态 这几点,看似简单,其实要用好挺难得,还有各种设计模式,尽量要做到高内聚,低耦合。

能封装的,就封装,做成独立的小模块,小而精,这样移植起来也更加方便,出错的可能性也就越小。

👉为了新技术

我一个前同事 DKH 入职了 TX 某部门,工程里边什么都有,一个 iOS 的工程,有 Objective-C,RAC,Swift, RXSwift,等等,各种技术,谁想用什么技术,觉得有现成的,直接搬过来,用的时候倒是挺爽,后来维护,修改,就变成了灾难。

我们前一段也尝试了 swift + oc 混编,想试一下新技术,用了一段时间后,就停下来主要原因有以下几点。

  1. 时间紧,任务重,陡然间上手 swift,效率较低
  2. 需要做大量的桥接工作
  3. 混编太慢

其实关于 1,2,经过长时间的迭代,是可以克服的,3 可以通过一些方式进行优化。但现在这个工程,已经是一艘将要倾覆的船了,意义已经不大。

去网上逛一圈,你会发现很有意思的一些事情,比如 文言文写程序,还有个一用 Dart 来写 Objective-C 代码,还有一个 用 Golang 写 iOS,还有这几年比较火的,就是 React-Native, Flutter 来开发 iOS Android 应用,以及各种热修复技术等等。诚然,这些技术是好的,有的可以极大的提升开发效率,节省时间。但是很显然,我们想的总是太美好,Airbnb 放弃了 RNDropbox 放弃 C++跨平台技术 等等,这些大厂的经验,也应该给我们一些警示,这些东西用的越多,可维护性也就越差,也会让招聘变得更难。这些是好是坏,不能一概而论,什么样的场景,配合什么样的技术,这无疑是重要的。

之前有看过一篇文章,写的是一个程序员吐槽 Oracle,虽然全世界都在用 Oracle,但不排除,它也是个屎山。软件工程发展了这么多年,程序员大佬们写了那么多的书来指导,可是,还是无法避免,一个项目,最终沦为屎山,网上有很多因为代码写的烂,导致迟迟不能交付,最终拖垮公司的。这种最终讨论,总会上升到哲学性的问题。尤其是在中国,赶紧上线,才是大部分公司的常态。优化,是不可能的。

随便写写吐个槽,不过作为一个程序员,我们还是要有自己的底线的,起码要对得起自己。

为了避免成为一个胖子(虽然已经是一个胖子了),我又决定健身了,这次很纯粹,只做波比跳,看能坚持多久,能减重多少斤

第一周

时间 次数 耗时 早饭 中饭 晚饭
2019年12月17日 6 * 10 14 分 苹果 豆芽 + 油麦菜 null
2019年12月18日 6 * 10 8 分 null 肉炒刀削 null
2019年12月19日 6 * 10 8 分 苹果 炒白菜 + 西葫芦鸡蛋 nil
2019年12月20日 6 * 10 8 分 苹果 聚餐了,吃的比较杂,荤素都有 nil

封装了 WKWebView,做一些记录

👉iOS 12 无法设置 UA

在其他 iOS 系统中,如果我们想设置 UA 的话,我们只需要这样做就可以了

1
2
3
[self.wkWebView.evaluateJavaScript:@"navigator.userAgent" completionHandler:^(NSString* _Nullable oldUA, NSError * _Nullable error) {
self.wkWebView.customUserAgent = newUA;
}];

可是在 iOS 12 上,这么设置,你会发现,在 js 中取 UA 还是原来的。

后来发现,只要你使用 wkWebView 调用了 navigator.userAgent,你再设置,会无法设置成功。也就是你用 A 调用了获取 UA 的 JS 方法,在设置 A 的 UA,将无法成功。所以我们在 iOS 12,需要先用一个假的 WKWebView 来获取,然后设置真正的 WKWebView 的 UA

1
2
3
4
5
self.fakeWKWebView = [[WKWebView alloc] init];
[self.fakeWKWebView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(NSString* _Nullable oldUA, NSError * _Nullable error) {
self.fakeWKWebView = nil;
self.realWKWebView.customUserAgent = newUA;
}];

👉Xcode 11.2 打开 WKWebView 疯狂输出 log

在 Xcode 11.2 打开一个 WKWebView 会输出一堆log

[Process] kill() returned unexpected error 1

这是一个 WebKit 的一个 bug

👉状态栏问题

如果 WKWebView 页面会有显示或者隐藏状态栏的需求,你会发现,在一些系统上,显示或隐藏状态栏,页面会跳动,这是 scrollView 的问题,只需要这么设置即可

1
2
3
if (@available(iOS 11.0, *)) {
self.wkWebView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}

两年前刷题,就遇见了,这个问题,当时没看懂,后来直接就不刷题了,现在,我又遇见了这个题,那它到底是个啥
其实这个题,说穿了,很简单。

给一个二维数组,

1
2
3
4
5
1, 2, 2, 1
2, 3, 4, 1
1, 3, 2, 1
2, 0, 3, 4

根据这个二维数组,转为 3D 的试图,每一个元素是一个小块,值代表了高度,位置就是在 3D 试图中的位置 ,然后看俯视图,左视图,正视图。求他们的面积和

如上数组所示

左视图,求每行中,最高的那个,然后累加
正视图,求每列中,最高的那个,然后累加
俯视图,求每个元素不为 0 的个数

思路很简单,就直接写代码了,Go 实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func projectionArea(grid [][]int) int {

var res int = 0

// 找出每一行的最大值,列的值
// 找出每一列的最大值,行的值

for I, iV := range grid {

var col = 0
var row = 0

for J, v := range iV {

if v > 0 {
res++
}
if v > col {
col = v
}

if grid[J][I] > row {
row = grid[J][I]
}
}
res = res + row + col
}
return res
}

遇到 leetCode 玄学问题,这次 for range 比 常规 for 遍历要快。。。也是无语了

557. Reverse Words in a String III

这道题比较简单,就是逆序字符串

  1. 按照空格分割字符串
  2. 字符串逆序
  3. 拼接输出

常规解法

  1. 分割字符串
  2. 倒序遍历字符串
  3. 拼接输出

go 语言实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func reverseWords(s string) string {
var res string

strArr := strings.Split(s, " ")

for idx, world := range strArr {
if idx != 0 {
res += " "
}
for i := len(world) - 1; i >= 0 ; i-- {
res = res + string(world[i])
}
}
return res
}

这样看似逻辑清晰,其实非常耗时,空间占用也大

原地逆序

  1. 分割字符串
  2. 原地逆序,交换字符
  3. 拼接输出

go 语言实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func reverseWords(s string) string {
strArr := strings.Split(s, " ")
for i, world := range strArr {
strArr[i] = revers(world)
}
return strings.Join(strArr, " ")
}

func revers(str string) string {
bytes := []byte(str)
i, j := 0, len(bytes) - 1
for i < j {
bytes[i], bytes[j] = bytes[j], bytes[i]
i++
j--
}
return string(bytes)
}

原地逆序时间复杂度为 O(N),空间复杂度为 O(1),这样可以大大节省空间

811. Subdomain Visit Count

这个题没什么好说的,就常规解法,放到一个 map 中,key 是 域名,value 是域名出现的次数,最后遍历这个 map,按照格式输出就行。

  1. 用空格分割字符串,找出数字和后边的域名,并把域名和数字作为 key-value 添加到 map 中
  2. 用点分割,点后边的字符串就是子域名,然后把子域名 和 数字 作为 key-value 添加到 map 中
  3. 最后遍历这个 map,按照格式输出就行。

go 实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func subdomainVisits(cpdomains []string) []string {
countMap := make(map[string]int)
var count int
var subStr string
for _, domain := range cpdomains {

for i, c := range domain {
if c == ' ' {
count, _ = strconv.Atoi(domain[:i])
subStr = domain[i+1:]
break
}
}
countMap[subStr] += count
for i, c := range subStr {
if c == '.' {
countMap[subStr[i+1:]] += count
}
}
}

res := make([]string, 0)

for key, value := range countMap {
res = append(res, strconv.Itoa(value) + " " + key)
}
return res
}

876. Middle of the Linked List

该题目给定一个链表,找出链表中间的那个节点,如果是偶数的话,返回中间后边的节点

1
2
3
4
5
Input: [1,2,3,4,5]
Output: [3,4,5]

Input: [1,2,3,4,5,6]
Output: [4,5,6]

常规解法

遍历这个链表,然后把每一个节点放在数组里边,取数组的中间值,返回。go 语言实现如下

1
2
3
4
5
6
7
8
9
10
11
12
func middleNode(head *ListNode) *ListNode {
array := make([]*ListNode, 0)
for {
if head != nil {
array = append(array, head)
head = head.Next
} else {
break
}
}
return array[len(array) / 2]
}

Time Complexity: O(N)

Space Complexity: O(N)

快慢指针

一个走一步,一个走两步,当走两步的到达末尾的时候,走一步的那个,就正好在中间。go 语言实现如下

1
2
3
4
5
6
7
8
func middleNode(head *ListNode) *ListNode {
var slow,fast = head,head
for fast != nil && fast.Next!=nil{
slow = slow.Next
fast = fast.Next.Next
}
return slow
}

Time Complexity: O(N)

Space Complexity: O(1)

顺便吐槽一下 LeetCode 的代码运行机制,如第一种方式,如果把 array 方在函数体外边,直接 Run Code 是没问题的,但是当 Submit 的时候,就会报错,提示 Wrong Answer。这个错误搞得我都要怀疑人生了,后来突然灵光一现,发现,只要把变量放在函数里边就 OK 了,也是醉了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
,--^----------,--------,-----,-------^--,
| ||||||||| `--------' | O
`+---------------------------^----------|
`\_,-------, _________________________|
/ XXXXXX /`| /
/ XXXXXX / `\ /
/ XXXXXX /\______(
/ XXXXXX /
/ XXXXXX /
(________(


,--^----------,--------,-----,-------^--,
| ||||||||| `--------' | O
`+---------------------------^----------|
`\_,-------, _________________________|
/ XXXXXX /`| /
/ XXXXXX / `\ /
/ XXXXXX /\______(
/ XXXXXX /
/ XXXXXX /
(________(
`------'

有时候我们会单独控制一个 VC 中状态栏的显示与隐藏,这里记录一下,如何正确的显示与隐藏状态栏

由于是单独控制,所以我们需要在 info.plist 中开启 View controller-based status bar appearanceYES。然后我们在自动控制的 VC 中,重写两个方法

1
2
3
4
5
6
7
8
9
- (BOOL)prefersStatusBarHidden {
// your logic for hide or show
return self.hiddenStatusBar;
}

- (UIStatusBarStyle)preferredStatusBarStyle {
// your logic for statusBar style
return UIStatusBarStyleDefault;
}

然后在我们需要的地方调用 [self setNeedsStatusBarAppearanceUpdate]; 即可,通常这可以满足大多数的场景。但是有一天我发现,有时候调用了 [self setNeedsStatusBarAppearanceUpdate]; 之后,prefersStatusBarHidden 并不会被回调。经过一番探索后发现,如果你有一个 未被隐藏的 UIWindow 的时候,调用 setNeedsStatusBarAppearanceUpdate 并不会触发 prefersStatusBarHidden,所以,只需要把对应的 UIWindow 隐藏就可以了。

现在项目中用的是 WKWebView,当显示或隐藏状态栏的时候,会导致 WKWebView 错位,而错位的原因就是因为,在 iOS 11 上,scrollView 新出了一个 API contentInsetAdjustmentBehavior 导致的,所以我们只需要对 WKWebView 的 scrollView 指定成如下即可。

1
2
3
if (@available(iOS 11.0, *)) {
_wkWebView.scrollView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;
}

而对于 iOS 11 以下的版本,我们需要在 对应的 VC 中指定 self.automaticallyAdjustsScrollViewInsets = NO;,这样 WKWebView 就不会受到 StatusBar 的显示和隐藏的影响了。

908. Smallest Range I

这道题一开始没看懂,其实不难

因为范围是 [-K, K],所以,找到 A 中的 最大值 maxB最小值 minB,然后我们计算差值就可以了,差值就只 maxB - K - (minB + K),也就是 res = maxB - minB - 2 * K,如果 res > 0 返回即可,小于 0 的话,返回 0 即可。

go 的实现代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func smallestRangeI(A []int, K int) int {
if len(A) == 0 {
return 0
}
minB := A[0]
maxB := A[0]

for _, value := range A {

if value < minB {
minB = value
}
if value > maxB{
maxB = value
}
}

res := maxB - minB - 2 * K
if res < 0 {
return 0
}
return res
}

在 Effective Objective-C 中 第 4 条,说要少用 #define 多用常量 ,这里就聊聊 const static extern 这些关键字

const

关于常量,我们有以下几种定义方式

1
2
3
4
5
6
7
8
9
int one = 1;

const int *i = &one; // 1

int const *i = &one; // 2

int * const i = &one; // 3

const int * const i = &one; // 4

我们知道当我们声明一个指针的时候

1
2
3
int one = 1;
int two = 2;
int *i = &one;

其实 i 本身有一个地址 A ,而 i 这个变量存储了一个指针,这个指针指向了 one 的地址,也就是 B,而这个地址中存放的是 2。

其实简单的来说,const 的位置,决定了 A 是 const 还是 B 是 const,下边逐一分析。

👉int const *i 和 const int *i

关于 1 和 2 其实是一样的,都等于 int const* i,也就是 *i 是被 const 修饰的,我们无法再次改变 *i ,但我们可以改变 i。也就是我们无法 *i = two 但我们可以 i = &two

1
2
3
4
5
6
int one = 1;
int two = 2;
int const* i = &one;

*i = two; // error: Read-only variable is not assignable
i = &two; // correct

👉int * const i

这种情况来说,i 是被 const 修饰的,我们是可以改变 *i,而无法改变 i。也就是我们可以 *i = two 但无法 i = &two

1
2
3
4
5
6
int one = 1;
int two = 2;
int * const i = &one;

*i = two; // correct
i = &two; // error: Cannot assign to variable 'i' with const-qualified type 'int *const'

👉const int * const i

这种情况就比较简单了,我们无法改变 *i,也无法改变 i 的地址。也就是我们无法 *i = two,也无法 i = &two

1
2
3
4
5
6
int one = 1;
int two = 2;
int const * const i = &one;

*i = two; // error: Read-only variable is not assignable
i = &two; // error: Cannot assign to variable 'i' with const-qualified type 'int *const'

👉再看

我们来看一个有意思的事情

1
2
3
4
5
6
7
8
9
10
11
int a = 10;
int b = 11;
NSLog(@"%p", &a); // 1
NSLog(@"%p", &b); // 2
int * p;
p = &a;
NSLog(@"%d %p %p", *p, p, &p); // 3
*p = b;
NSLog(@"%d %p %p", *p, p, &p); // 4

NSLog(@"%d", a); // 5 是多少

想想在往下看

1
2
3
4
5
1. 0x7ffee643b908
2. 0x7ffee643b8f8
3. 10 0x7ffee643b90c 0x7ffee643b8f8
4. 11 0x7ffee643b90c 0x7ffee643b8f8
5. 11

怎么样,和你想的一样么,我们发现 a 被偷偷的改成了 11,这是为什么,我们分析下地址就知道了。

我们先看下地址

我们知道 *p 是取地址的内容,当我们在执行 *p = b 的时候,其实就是 取 0x7ffee643b90c 的内容,并修改为 11 ,而这个叫 a 的变量,他的地址其实还是原来的。当我们输出 a 的时候,a 变成了 11。如果我们想改变 *p 的值,安全的做法,是 p = &someValue不要使用 *p = someValue 所以当我们 int const *p 的时候,我们是无法修改 *p 的值的。

当我们知道一个数据的地址的时候,如何取地址中存放的数据呢,例如上边有个地址是 0x7ffee643b90c,如何知道 这个地址存放的是什么。 可以这样做 *(int *)(0x7ffee643b90c) 这样,就能取到这个地址中存放的数据了,但是前提是你需要知道这个地址中的数据是什么类型的,不然取出来,也是不正确的。

在 OC 中,其实是一样的 当我们创建一个对象 NSObject *obj1 = [NSObject new] 的时候, obj1 是一个地址,指向了真正的系统分配的内存空间,当我们用 NSObject * const obj1 来修饰的时候,我们的 obj1 就再也无法被赋值为其他值了,因为这个 obj1 是被 const 修饰的,而 *obj1 是可以被改的,有兴趣可以试试。

👉小结

关于 const 的位置,我们可以这么理解,const 后边的哪个东西,就是我们修饰的常量,是不可变的。当 int const* p 的时候,也就是这个 *p 是常量,我们无法改变他,而当 int * const p 的时候,这个 p 是常量,我们无法改变他。

static

static 有以下几点特性

👉内部可见

在 OC 中,当在我们在 .m 中写一个被 static 修饰的变量或者函数的时候,我们只能在 这个文件 中使用,而不能再别的地方调用。在其他地方调用,将会提示,找不到该定义。但如果在 .h 中定义的话,那么是可以被外部使用的。

👉只初始化一次

如下代码,static 修饰的 count 只会初始化一次,每次调用 count 会累加,而 不用 static 修饰的话,每次会重新初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
int fun() 
{
static int count = 0;
count++;
return count;
}

int main()
{
printf("%d ", fun()); // 1
printf("%d ", fun()); // 2
return 0;
}

👉存储在 data segment 中

被 static 修饰的存储在 data segment,而在函数中,没有被 static 修饰的,存放在 stack segment,更详细的分配,可以参见这篇文章

👉未初始化自动赋值为默认值

被 static 修饰的变量,如果没有指定值,将会被赋值为默认值

1
2
3
static int x ;
int y;
NSLog(@"%d %d", x, y); // 0 1082916864

👉只能是特定值,不能是函数

static 修饰的只能被赋值为特定的值,而不能是函数

1
2
3
4
5
6
7
static int x = initializer(); //error: Initializer element is not a compile-time constant
NSLog(@"%d", x);

int initializer(void)
{
return 50;
}

extern

这个就比较简单了,就是一个标记,外部可用,当我们在 .m 中声明一个常量的时候,需要供外部使用就需要这么写。在 Foundation 框架中,我们经常能看到这种写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

// Test.m
NSString *const kNotificationTest1 = @"kNotificationTest1";

@implementation Test

@end

// Test.h
extern NSString * const kNotificationTest1;

@interface Test : NSObject

@end

inline

inline 编译器标识符相当于在执行这段代码的时候,把这段代码直接拿过来,而不是像调用函数那样直接调用(函数会跳转,有一些开销)。

1
2
3
4
5
6
7
8
9
10
11
12

// Test.m 中

__inline__ __attribute__((always_inline)) void xiaoma(void) {
NSLog(@"ddd");
}


// Test.h 中

void xiaoma(void);

0%