ronniesong

ronniesong 查看完整档案

北京编辑  |  填写毕业院校腾讯  |  后端 编辑填写个人主网站
编辑

Seek the truth!

个人动态

ronniesong 收藏了文章 · 9月24日

如何提升代码质量

何谓代码质量?

代码是给人看的

1. 书写规范:遵照自己公司制定的编程语言书写规范。
2. 易阅读。
3. 易修改。
4. 易测试。

代码是给机器运行的

1. 安全
2. 快速
3. 稳定

代码质量的标准?

对于机器来说,标准是恒定的,但不可兼得。

比如:

  • 锁机制:

    • 安全、慢
  • 指针:

    • 快、不稳定
  • 改内存地址:

    • 快、不安全

总的来说,这就像 CAP 理论一样,不同场景下的需求不一样,根据当下业务需求去做出取舍即可。

对于人来说,标准是变化的,因为习惯不同、工期不同、目的不同。

易阅读

表意明确

名词要准确

类、结构体、变量、常量等名词要能直观地描述这是个什么东西,一般 1-5 个单词组成为宜。

英文单词里没有官方缩写的就尽量不用缩写,像 result 就 6 个字母,也有人给缩写成 res、ret,temp 缩写成 tmp,更有甚者写 cnt,它到底是 count 还是 content 呢?除了歧义,完全没有任何好处。

动词要精简

方法名、函数名等动词要能保证只做一件事。一个方法不写太长的前提是它的功能本身就不多。

形容词要归约

属性、校验等形容词要归约为方法,业务逻辑关键点大多在判断上,预留扩展点会让阅读难度不随着加需求而快速增大。

单词统一

不要有歧义,因为人是有思维惯性的,比如同样的业务逻辑,有的写 add,有的写 append,有的写 insert,会严重影响阅读效率。

描述业务

有意义的名字要专注于描述业务,使读者通过阅读代码理解业务逻辑。不要在起名中掺杂数据结构。

例如这么几个场景:列表(集合)、配置映射

  • good case:users(用户集合),articles(文章列表)、siteNameToSiteId(映射)
  • bad case:userSet、articleList、siteMap

加上类型,并不会对理解业务逻辑有帮助,读者看到 list,map 这些关键词还会联想到数据结构中,很容易打断思维。

避免赘述

具有包含关系或从属关系的时候,不要重复,不要表达累赘的语境。

关注作用域和生命周期

当一个变量的作用域很窄,或生命周期很短的时候,可以用单字母命名,一般来讲,单字母意味着临时使用,读者在了解逻辑的时候可以不用关注这部分。

变量要接近使用的地方声明,不要开头声明一堆变量,隔几十行才使用。

写有用的注释

“好的代码是自描述的” 即读代码就和读文章一样。注释不应该用来解释代码逻辑,而应该是用来说明为什么这么写。

写什么样的注释

  1. 公共的、全局的变量和常量:说明用在哪,提供给谁用。
  2. 函数,方法:说明函数功能是什么。
  3. 行注释:xx 产品在 x 年 x 月 x 日提出什么需求,做此修改。

注释不是用来删代码的!!!

代码不用了就彻底删除,怕以后还有用就从 git 里找回来,如果一个函数 100 行,其中 50 行都被注释掉了,这种会很容易分散读者的注意力。

易修改

一值一用

不要把一个变量重复赋值使用。虽然类型一样,但这样做会让修改的人非常头疼,所谓牵一发而动全身。例如:

  • bad case:

result, err := a.Get()
result, err = b.Get()

  • good case:

resultA, err := a.Get()
resultB, err = b.Get()

少写参数

当你发现一个函数的功能需要传入七八个参数才能完成的时候,一定是函数干的事太多了,逻辑写太长了。需要适当拆分。

正确使用逻辑运算符

&&、||、!这些逻辑运算符是用来做逻辑判断的,不是用来控制执行流程的。

例如这样一段逻辑:

if (isA()) {
    doB()
}

不要写成 isA() && doB(),尽管结果是一样的。

适当化简

if ((condition1() && condition2()) || !condition1()) {
    return true
}
return false

取反后化简为:

if(condition1() && !condition2()) {
    return false
}
return true

降低圈复杂度

圈复杂度的定义:https://zh.wikipedia.org/wiki/循环复杂度

增加圈复杂度的关键词:

if、else、while、for、case、||、&& 等

圈复杂度的合格标准:

大部分标准在 10-20 之间。
这也是一个平均值,不是要求每一个函数都在 20 以下。
个别超标,是可以接受的。

如何降低

1. 提炼函数
2. 抽象配置,使用 map
3. 合并返回值相同的函数

多写函数少写变量

实现同样的功能,并不是代码越少越好。

因为代码越少往往意味着耦合度越高,修改扩展起来会更麻烦,就是爽了自己,给别人留坑。

但是,每一行代码都要有价值。

如果说逻辑节点之间需要一个东西来充当桥梁,变量就是独木桥,函数像隧道。
防杠精:有人说要考虑性能开销啊,多写一个函数比一个栈内的变量开销大啊,我觉得业务代码不差这一星半点的,自行斟酌。

易测试

TDD

测试驱动开发:写一个函数之前先考虑写出来之后能不能测试,好不好测试。

实现方式

1

第一步:先写单元测试。不必关心如何实现函数功能。

第二步:写目标函数,以刚好能通过单元测试的逻辑代码为目的。

第三步:重构函数,合理命名,优化结构,抽象设计。

如此循环,保证每次改动代码都能完好地通过所有测试用例。

这样做的目的是让错误尽早的暴露出来,在 10 行代码中解决 bug 要比在 100 行代码中解决 bug 更加容易和快速。

理想与现实

看起来 TDD 的理论和可操作性还不错,但实际开发中,如果真的严格按照此理论去开发。对开发效率是一个比较大的影响。
而且一旦形成惯性思维和盲从依赖,会降低对代码的灵感和熟练度。
测试用例跑过了就没问题了吗?不一定。因为测试用例也是人写的,隐藏的坑才最致命。
其实,当做到易阅读和易修改之后,易测试就是水到渠成的事情。

IoC 模式

将对象、接口、非固定值(如系统时间、随机数)等作为依赖注入,先构造条件,再执行函数。而不是由函数内部去构造。

全局变量单一写入方

当有两个以上的函数控制同一个全局变量的时候,会相互影响,即局部形成了一个状态机,使测试难度陡升。

封装外部依赖

有外部依赖的,尤其涉及 IO 通信的,要单独封装,哪怕只有几行代码也要封装成一个独立的函数,不要把对外部依赖的调用混合在自身的逻辑代码中。

最后

阅读 → 修改 → 测试

这是一个递进的关系,环环相扣,并且前一步做好了都能有利于后一步的完善。

查看原文

ronniesong 发布了文章 · 9月24日

如何提升代码质量

何谓代码质量?

代码是给人看的

1. 书写规范:遵照自己公司制定的编程语言书写规范。
2. 易阅读。
3. 易修改。
4. 易测试。

代码是给机器运行的

1. 安全
2. 快速
3. 稳定

代码质量的标准?

对于机器来说,标准是恒定的,但不可兼得。

比如:

  • 锁机制:

    • 安全、慢
  • 指针:

    • 快、不稳定
  • 改内存地址:

    • 快、不安全

总的来说,这就像 CAP 理论一样,不同场景下的需求不一样,根据当下业务需求去做出取舍即可。

对于人来说,标准是变化的,因为习惯不同、工期不同、目的不同。

易阅读

表意明确

名词要准确

类、结构体、变量、常量等名词要能直观地描述这是个什么东西,一般 1-5 个单词组成为宜。

英文单词里没有官方缩写的就尽量不用缩写,像 result 就 6 个字母,也有人给缩写成 res、ret,temp 缩写成 tmp,更有甚者写 cnt,它到底是 count 还是 content 呢?除了歧义,完全没有任何好处。

动词要精简

方法名、函数名等动词要能保证只做一件事。一个方法不写太长的前提是它的功能本身就不多。

形容词要归约

属性、校验等形容词要归约为方法,业务逻辑关键点大多在判断上,预留扩展点会让阅读难度不随着加需求而快速增大。

单词统一

不要有歧义,因为人是有思维惯性的,比如同样的业务逻辑,有的写 add,有的写 append,有的写 insert,会严重影响阅读效率。

描述业务

有意义的名字要专注于描述业务,使读者通过阅读代码理解业务逻辑。不要在起名中掺杂数据结构。

例如这么几个场景:列表(集合)、配置映射

  • good case:users(用户集合),articles(文章列表)、siteNameToSiteId(映射)
  • bad case:userSet、articleList、siteMap

加上类型,并不会对理解业务逻辑有帮助,读者看到 list,map 这些关键词还会联想到数据结构中,很容易打断思维。

避免赘述

具有包含关系或从属关系的时候,不要重复,不要表达累赘的语境。

关注作用域和生命周期

当一个变量的作用域很窄,或生命周期很短的时候,可以用单字母命名,一般来讲,单字母意味着临时使用,读者在了解逻辑的时候可以不用关注这部分。

变量要接近使用的地方声明,不要开头声明一堆变量,隔几十行才使用。

写有用的注释

“好的代码是自描述的” 即读代码就和读文章一样。注释不应该用来解释代码逻辑,而应该是用来说明为什么这么写。

写什么样的注释

  1. 公共的、全局的变量和常量:说明用在哪,提供给谁用。
  2. 函数,方法:说明函数功能是什么。
  3. 行注释:xx 产品在 x 年 x 月 x 日提出什么需求,做此修改。

注释不是用来删代码的!!!

代码不用了就彻底删除,怕以后还有用就从 git 里找回来,如果一个函数 100 行,其中 50 行都被注释掉了,这种会很容易分散读者的注意力。

易修改

一值一用

不要把一个变量重复赋值使用。虽然类型一样,但这样做会让修改的人非常头疼,所谓牵一发而动全身。例如:

  • bad case:

result, err := a.Get()
result, err = b.Get()

  • good case:

resultA, err := a.Get()
resultB, err = b.Get()

少写参数

当你发现一个函数的功能需要传入七八个参数才能完成的时候,一定是函数干的事太多了,逻辑写太长了。需要适当拆分。

正确使用逻辑运算符

&&、||、!这些逻辑运算符是用来做逻辑判断的,不是用来控制执行流程的。

例如这样一段逻辑:

if (isA()) {
    doB()
}

不要写成 isA() && doB(),尽管结果是一样的。

适当化简

if ((condition1() && condition2()) || !condition1()) {
    return true
}
return false

取反后化简为:

if(condition1() && !condition2()) {
    return false
}
return true

降低圈复杂度

圈复杂度的定义:https://zh.wikipedia.org/wiki/循环复杂度

增加圈复杂度的关键词:

if、else、while、for、case、||、&& 等

圈复杂度的合格标准:

大部分标准在 10-20 之间。
这也是一个平均值,不是要求每一个函数都在 20 以下。
个别超标,是可以接受的。

如何降低

1. 提炼函数
2. 抽象配置,使用 map
3. 合并返回值相同的函数

多写函数少写变量

实现同样的功能,并不是代码越少越好。

因为代码越少往往意味着耦合度越高,修改扩展起来会更麻烦,就是爽了自己,给别人留坑。

但是,每一行代码都要有价值。

如果说逻辑节点之间需要一个东西来充当桥梁,变量就是独木桥,函数像隧道。
防杠精:有人说要考虑性能开销啊,多写一个函数比一个栈内的变量开销大啊,我觉得业务代码不差这一星半点的,自行斟酌。

易测试

TDD

测试驱动开发:写一个函数之前先考虑写出来之后能不能测试,好不好测试。

实现方式

1

第一步:先写单元测试。不必关心如何实现函数功能。

第二步:写目标函数,以刚好能通过单元测试的逻辑代码为目的。

第三步:重构函数,合理命名,优化结构,抽象设计。

如此循环,保证每次改动代码都能完好地通过所有测试用例。

这样做的目的是让错误尽早的暴露出来,在 10 行代码中解决 bug 要比在 100 行代码中解决 bug 更加容易和快速。

理想与现实

看起来 TDD 的理论和可操作性还不错,但实际开发中,如果真的严格按照此理论去开发。对开发效率是一个比较大的影响。
而且一旦形成惯性思维和盲从依赖,会降低对代码的灵感和熟练度。
测试用例跑过了就没问题了吗?不一定。因为测试用例也是人写的,隐藏的坑才最致命。
其实,当做到易阅读和易修改之后,易测试就是水到渠成的事情。

IoC 模式

将对象、接口、非固定值(如系统时间、随机数)等作为依赖注入,先构造条件,再执行函数。而不是由函数内部去构造。

全局变量单一写入方

当有两个以上的函数控制同一个全局变量的时候,会相互影响,即局部形成了一个状态机,使测试难度陡升。

封装外部依赖

有外部依赖的,尤其涉及 IO 通信的,要单独封装,哪怕只有几行代码也要封装成一个独立的函数,不要把对外部依赖的调用混合在自身的逻辑代码中。

最后

阅读 → 修改 → 测试

这是一个递进的关系,环环相扣,并且前一步做好了都能有利于后一步的完善。

查看原文

赞 7 收藏 5 评论 0

ronniesong 收藏了文章 · 2019-09-16

Go 1.13 errors 基本用法

Go 最新版本 1.13 中新增了 errors 的一些特性,有助于我们更优雅的处理业务逻辑中报错的问题。
本文主要展示 errors 包中新增方法的用法。

核心思想:套娃

啥意思呢?这玩意就像套娃一样,从上往下扒,拿走一个还有一个,再拿走一个,诶还有一个,如果你愿意,可以一直扒到最底下没有了为止。

基本用法

1. 创建一个被包装的 error

方式一:fmt.Errorf

使用 %w 参数返回一个被包装的 error

err1 := errors.New("new error")
err2 := fmt.Errorf("err2: [%w]", err1)
err3 := fmt.Errorf("err3: [%w]", err2)
fmt.Println(err3)
// output
err3: [err2: [new error]]

err2 就是一个合法的被包装的 error,同样地,err3 也是一个被包装的 error,如此可以一直套下去。

方式二:自定义 struct

type WarpError struct {
    msg string
    err error
}

func (e *WarpError) Error() string {
    return e.msg
}

func (e *WrapError) Unwrap() error {
    return e.err
}

之前看过源码的同学可能已经知道了,这就是 fmt/errors.go 中关于 warp 的结构。
就,很简单。自定义一个实现了 Unwrap 方法的 struct 就可以了。

2. 拆开一个被包装的 error

errors.Unwrap

err1 := errors.New("new error")
err2 := fmt.Errorf("err2: [%w]", err1)
err3 := fmt.Errorf("err3: [%w]", err2)

fmt.Println(errors.Unwrap(err3))
fmt.Println(errors.Unwrap(errors.Unwrap(err3)))
// output
err2: [new error]
new error

3. 判断被包装的 error 是否是包含指定错误

errors.Is

当多层调用返回的错误被一次次地包装起来,我们在调用链上游拿到的错误如何判断是否是底层的某个错误呢?
它递归调用 Unwrap 并判断每一层的 err 是否相等,如果有任何一层 err 和传入的目标错误相等,则返回 true。

err1 := errors.New("new error")
err2 := fmt.Errorf("err2: [%w]", err1)
err3 := fmt.Errorf("err3: [%w]", err2)

fmt.Println(errors.Is(err3, err2))
fmt.Println(errors.Is(err3, err1))
// output
true
true

4. 提取指定类型的错误

errors.As

这个和上面的 errors.Is 大体上是一样的,区别在于 Is 是严格判断相等,即两个 error 是否相等。
As 则是判断类型是否相同,并提取第一个符合目标类型的错误,用来统一处理某一类错误。

type ErrorString struct {
    s string
}

func (e *ErrorString) Error() string {
    return e.s
}

var targetErr *ErrorString
err := fmt.Errorf("new error:[%w]", &ErrorString{s:"target err"})
fmt.Println(errors.As(err, &targetErr))
// output
true

扩展

IsAs 两个方法已经预留了口子,可以由自定义的 error struct 实现并覆盖调用。

源码也没什么可说的,太简单了,一眼就能看懂的。
查看原文

ronniesong 发布了文章 · 2019-09-16

Go 1.13 errors 基本用法

Go 最新版本 1.13 中新增了 errors 的一些特性,有助于我们更优雅的处理业务逻辑中报错的问题。
本文主要展示 errors 包中新增方法的用法。

核心思想:套娃

啥意思呢?这玩意就像套娃一样,从上往下扒,拿走一个还有一个,再拿走一个,诶还有一个,如果你愿意,可以一直扒到最底下没有了为止。

基本用法

1. 创建一个被包装的 error

方式一:fmt.Errorf

使用 %w 参数返回一个被包装的 error

err1 := errors.New("new error")
err2 := fmt.Errorf("err2: [%w]", err1)
err3 := fmt.Errorf("err3: [%w]", err2)
fmt.Println(err3)
// output
err3: [err2: [new error]]

err2 就是一个合法的被包装的 error,同样地,err3 也是一个被包装的 error,如此可以一直套下去。

方式二:自定义 struct

type WarpError struct {
    msg string
    err error
}

func (e *WarpError) Error() string {
    return e.msg
}

func (e *WrapError) Unwrap() error {
    return e.err
}

之前看过源码的同学可能已经知道了,这就是 fmt/errors.go 中关于 warp 的结构。
就,很简单。自定义一个实现了 Unwrap 方法的 struct 就可以了。

2. 拆开一个被包装的 error

errors.Unwrap

err1 := errors.New("new error")
err2 := fmt.Errorf("err2: [%w]", err1)
err3 := fmt.Errorf("err3: [%w]", err2)

fmt.Println(errors.Unwrap(err3))
fmt.Println(errors.Unwrap(errors.Unwrap(err3)))
// output
err2: [new error]
new error

3. 判断被包装的 error 是否是包含指定错误

errors.Is

当多层调用返回的错误被一次次地包装起来,我们在调用链上游拿到的错误如何判断是否是底层的某个错误呢?
它递归调用 Unwrap 并判断每一层的 err 是否相等,如果有任何一层 err 和传入的目标错误相等,则返回 true。

err1 := errors.New("new error")
err2 := fmt.Errorf("err2: [%w]", err1)
err3 := fmt.Errorf("err3: [%w]", err2)

fmt.Println(errors.Is(err3, err2))
fmt.Println(errors.Is(err3, err1))
// output
true
true

4. 提取指定类型的错误

errors.As

这个和上面的 errors.Is 大体上是一样的,区别在于 Is 是严格判断相等,即两个 error 是否相等。
As 则是判断类型是否相同,并提取第一个符合目标类型的错误,用来统一处理某一类错误。

type ErrorString struct {
    s string
}

func (e *ErrorString) Error() string {
    return e.s
}

var targetErr *ErrorString
err := fmt.Errorf("new error:[%w]", &ErrorString{s:"target err"})
fmt.Println(errors.As(err, &targetErr))
// output
true

扩展

IsAs 两个方法已经预留了口子,可以由自定义的 error struct 实现并覆盖调用。

源码也没什么可说的,太简单了,一眼就能看懂的。
查看原文

赞 17 收藏 10 评论 3

ronniesong 收藏了文章 · 2019-03-04

Weex系列(7) ——踩坑填坑的总总

目录

使用weex已经一年半了,踩了很多坑,也流了很多泪填上,总结一波,希望对大家有所帮助。

LaunchImage

这是今年来的第一个调整,需要把 iOS8.0 and Later勾上,不然iPhone XR/XS Max默认会走iPhone X的尺寸375ptx812pt。

clipboard.png

build.gradle

这个文件设置还挺多的,先说一点吧,比如配置打包信息,是debug还是release版本,这个对微博的分享签名配置是有影响的。

clipboard.png
clipboard.png

image

1、必须指定样式中的宽度和高度
2、Android 默认的Image Adapter不支持 gif,需要自己封装,我是用的GifDrawable
3、安卓图片太大太长,我是在安卓设置了属性hardwareAccelerated,但是内存好像会升高,最好还是避免出现又长又大的图,现在发现出来了个autoBitmapRecycleAndroid
大家可以试一下

refresh

refresh和pullingdown事件是在这个组件上不是加在list和scroller上,真的刚开始接触的时候,list和scroller用的又多,有一次就犯了这个错误,找了半天,手动dog吧。

list和scroller

1、尽量不要在list的cell上做处理,比如宽高啊、position定位啊,可能会不生效,还有可能会导致滚动加载不正常
2、我遇到过scroller在安卓上下拉刷新不正常,跟初始加载数据,div绘制有关,上来一滚动就下拉刷新,最后是用list解决的,所以建议大家列表还是多用list。

slider

官网上没有像image那样强调一定要设置宽高,但是还是建议大家给个值,不然有时候会遇到点异常。

picker

picker的pick方法在安卓底下会崩溃,结果竟然是要在AndroidManifest.xml里面设置正确的android:theme,因为我是用官网的脚手架搭起来的项目,不知道大家会不会遇到,改一下android:theme就可以解决问题了。

css相关

1、Weex对于长度值目前只支持像素值,不支持相对单位(em、rem),也不支持百分比。
2、遇到一些奇怪的现象的时候,可以找找是否有position:relative/fixed/absolute,比如slider嵌套list,和slider并列后面用了position:relative的div等,我这边就遇到了加载tab乱跳,还有封装了最外层position:fixed的
3、Weex 目前不支持 z-index 设置元素层级关系,但靠后的元素层级更高,因此,对于层级高的元素,可将其排列在后面

最后还是感谢大家,如果喜欢欢迎点赞收藏啊~

查看原文

ronniesong 赞了文章 · 2019-03-04

Weex系列(7) ——踩坑填坑的总总

目录

使用weex已经一年半了,踩了很多坑,也流了很多泪填上,总结一波,希望对大家有所帮助。

LaunchImage

这是今年来的第一个调整,需要把 iOS8.0 and Later勾上,不然iPhone XR/XS Max默认会走iPhone X的尺寸375ptx812pt。

clipboard.png

build.gradle

这个文件设置还挺多的,先说一点吧,比如配置打包信息,是debug还是release版本,这个对微博的分享签名配置是有影响的。

clipboard.png
clipboard.png

image

1、必须指定样式中的宽度和高度
2、Android 默认的Image Adapter不支持 gif,需要自己封装,我是用的GifDrawable
3、安卓图片太大太长,我是在安卓设置了属性hardwareAccelerated,但是内存好像会升高,最好还是避免出现又长又大的图,现在发现出来了个autoBitmapRecycleAndroid
大家可以试一下

refresh

refresh和pullingdown事件是在这个组件上不是加在list和scroller上,真的刚开始接触的时候,list和scroller用的又多,有一次就犯了这个错误,找了半天,手动dog吧。

list和scroller

1、尽量不要在list的cell上做处理,比如宽高啊、position定位啊,可能会不生效,还有可能会导致滚动加载不正常
2、我遇到过scroller在安卓上下拉刷新不正常,跟初始加载数据,div绘制有关,上来一滚动就下拉刷新,最后是用list解决的,所以建议大家列表还是多用list。

slider

官网上没有像image那样强调一定要设置宽高,但是还是建议大家给个值,不然有时候会遇到点异常。

picker

picker的pick方法在安卓底下会崩溃,结果竟然是要在AndroidManifest.xml里面设置正确的android:theme,因为我是用官网的脚手架搭起来的项目,不知道大家会不会遇到,改一下android:theme就可以解决问题了。

css相关

1、Weex对于长度值目前只支持像素值,不支持相对单位(em、rem),也不支持百分比。
2、遇到一些奇怪的现象的时候,可以找找是否有position:relative/fixed/absolute,比如slider嵌套list,和slider并列后面用了position:relative的div等,我这边就遇到了加载tab乱跳,还有封装了最外层position:fixed的
3、Weex 目前不支持 z-index 设置元素层级关系,但靠后的元素层级更高,因此,对于层级高的元素,可将其排列在后面

最后还是感谢大家,如果喜欢欢迎点赞收藏啊~

查看原文

赞 1 收藏 1 评论 0

ronniesong 收藏了文章 · 2019-03-01

Weex系列(6) —— web组件和webview

目录

不知不觉就3月1号了,这段时间在想怎么来收尾这个系列,打算把css小结放在踩坑那一章,那一章以后估计也会不定时更新。最后一章就简单分析一下流程原理。

还是言归正传吧,webview是一个基于webkit引擎、展现web页面的控件,app里面是经常用到的,weex官方提供了web组件。

webview这块是比较复杂的,所以官方提供的远远不够,但是对原生又不是很熟悉,就找到组件源码,在此基础上再进行二次封装,上一章也是有很详细的提到的。

进行了二次封装,我们想添加功能配置什么的就方便很多了。

iOS

iOS端的webview坑要少一些,几乎没怎么改过,主要就是html和原生的交互。
1、可以用到URL Schemes来拦截做一些简单的跳转处理
2、实在绕不过,就用到了一个比较复杂的WebViewJavascriptBridge,我用的就是谷歌搜出来第一个,参照例子加在我们自己封装的组件里面了,我这边直接就加在了viewWillAppear方法里面,同理也需要在html里面配置,最后app就能监听到html里面的点击等交互动作了。

Android

安卓要麻烦许多,网上大多也都是安卓的webview讲解,我也是遇到了好多坑。
我把网上需要配置的基本都加上了,每个设置的说明看方法能猜出一二。
1、然后就是shouldOverrideUrlLoading,页面跳转遇到的无限加载、白屏等都需要在这个方法里面做处理,由于这块涉及业务处理,也就不截出来了,我也是参照网上的方案解决的,需要耐心,多试几次,会解决的。

private void initWebView(WebView wv) {
        WebSettings settings = wv.getSettings();
        settings.setAppCacheEnabled(true);
        settings.setAllowFileAccess(true);//设置启用或禁止访问文件数据
        settings.setDomStorageEnabled(true);
        settings.setLoadsImagesAutomatically(true);
        //适应屏幕
        settings.setUseWideViewPort(true);
        // 设置可以支持缩放
        settings.setSupportZoom(true);
        // 设置出现缩放工具
        settings.setBuiltInZoomControls(true);
        //不显示webview缩放按钮
        settings.setDisplayZoomControls(false);
        settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
        settings.setLoadWithOverviewMode(true);

        // 设置与Js交互的权限
        settings.setJavaScriptEnabled(true);
        // 设置允许JS弹窗
        settings.setJavaScriptCanOpenWindowsAutomatically(true);
        //设置字体大小
        settings.setTextZoom(100);

        wv.setWebViewClient(new WebViewClient() {

            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {

2、接着就是安卓的上传图片文件,用到了如下的方法,最后用到的是WXWebView.mUploadCallbackAboveL 回传图片的

// For Android < 3.0
            public void openFileChooser(ValueCallback<Uri> valueCallback) {
                mUploadMessage = valueCallback;
                openImageChooserActivity();
            }

            // For Android  >= 3.0
            public void openFileChooser(ValueCallback valueCallback, String acceptType) {
                mUploadMessage = valueCallback;
                openImageChooserActivity();
            }

            //For Android  >= 4.1
            public void openFileChooser(ValueCallback<Uri> valueCallback, String acceptType, String capture) {
                //Log.e(TAG, "onShowFileChooser: "+acceptType);
                mUploadMessage = valueCallback;
                openImageChooserActivity();
            }

            // For Android >= 5.0
            @Override
            public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
                //Log.e(TAG, "onShowFileChooser: "+fileChooserParams);
                mUploadCallbackAboveL = filePathCallback;
                openImageChooserActivity();
                return true;
            }

3、还有一个点是webview里面音视频播放,退出页面,还会有声音等,可以在原生.java的onPause方法里面做处理,我记得这个没处理的时候有的安卓应用商店都是审核不过的。

@Override
    public void onPause() {
        super.onPause();
        if(WXWebView.mWebView != null){
            WXWebView.mWebView.pauseTimers();
            //这里只对页面中只有一个音频的情况做了处理,如果有多个音频需要遍历整个数组记录状态
            WXWebView.mWebView.loadUrl(
                    "javascript:audioEty = document.getElementsByTagName('audio')[0]; audioEty.pause();"
            );
            WXWebView.mWebView.loadUrl(
                    "javascript:videoEty = document.getElementsByTagName('video')[0]; videoEty.pause();"
            );

            WXWebView.mWebView.onPause();
        }
        
    }

页面宽高适配

最后本来是打算想把app的宽高适配等问题放在css那个小结的,但是现在归类在了踩坑里面,就把这个放在这儿讲吧。
1、iOS我是在viewDidLayoutSubviews里面重新绘制了一下,在适配iPhoneX、XR、XMAX的时候会用到,安卓倒是没有怎么处理。

_instance.frame = CGRectMake(safeArea.left, safeArea.top, self.view.frame.size.width-safeArea.left-safeArea.right, _weexHeight-safeArea.top);

2、weex官网config这一章里面有讲到
这个例子,大家也可以扫码看一下,主要就是weex.config.env.deviceWidth和weex.config.env.deviceHeight。

clipboard.png

最后这个系列就剩下两章了,下一篇也会尽快发布出来,感谢大家,如果喜欢欢迎收藏点赞啊~

查看原文

ronniesong 赞了文章 · 2019-03-01

Weex系列(6) —— web组件和webview

目录

不知不觉就3月1号了,这段时间在想怎么来收尾这个系列,打算把css小结放在踩坑那一章,那一章以后估计也会不定时更新。最后一章就简单分析一下流程原理。

还是言归正传吧,webview是一个基于webkit引擎、展现web页面的控件,app里面是经常用到的,weex官方提供了web组件。

webview这块是比较复杂的,所以官方提供的远远不够,但是对原生又不是很熟悉,就找到组件源码,在此基础上再进行二次封装,上一章也是有很详细的提到的。

进行了二次封装,我们想添加功能配置什么的就方便很多了。

iOS

iOS端的webview坑要少一些,几乎没怎么改过,主要就是html和原生的交互。
1、可以用到URL Schemes来拦截做一些简单的跳转处理
2、实在绕不过,就用到了一个比较复杂的WebViewJavascriptBridge,我用的就是谷歌搜出来第一个,参照例子加在我们自己封装的组件里面了,我这边直接就加在了viewWillAppear方法里面,同理也需要在html里面配置,最后app就能监听到html里面的点击等交互动作了。

Android

安卓要麻烦许多,网上大多也都是安卓的webview讲解,我也是遇到了好多坑。
我把网上需要配置的基本都加上了,每个设置的说明看方法能猜出一二。
1、然后就是shouldOverrideUrlLoading,页面跳转遇到的无限加载、白屏等都需要在这个方法里面做处理,由于这块涉及业务处理,也就不截出来了,我也是参照网上的方案解决的,需要耐心,多试几次,会解决的。

private void initWebView(WebView wv) {
        WebSettings settings = wv.getSettings();
        settings.setAppCacheEnabled(true);
        settings.setAllowFileAccess(true);//设置启用或禁止访问文件数据
        settings.setDomStorageEnabled(true);
        settings.setLoadsImagesAutomatically(true);
        //适应屏幕
        settings.setUseWideViewPort(true);
        // 设置可以支持缩放
        settings.setSupportZoom(true);
        // 设置出现缩放工具
        settings.setBuiltInZoomControls(true);
        //不显示webview缩放按钮
        settings.setDisplayZoomControls(false);
        settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
        settings.setLoadWithOverviewMode(true);

        // 设置与Js交互的权限
        settings.setJavaScriptEnabled(true);
        // 设置允许JS弹窗
        settings.setJavaScriptCanOpenWindowsAutomatically(true);
        //设置字体大小
        settings.setTextZoom(100);

        wv.setWebViewClient(new WebViewClient() {

            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {

2、接着就是安卓的上传图片文件,用到了如下的方法,最后用到的是WXWebView.mUploadCallbackAboveL 回传图片的

// For Android < 3.0
            public void openFileChooser(ValueCallback<Uri> valueCallback) {
                mUploadMessage = valueCallback;
                openImageChooserActivity();
            }

            // For Android  >= 3.0
            public void openFileChooser(ValueCallback valueCallback, String acceptType) {
                mUploadMessage = valueCallback;
                openImageChooserActivity();
            }

            //For Android  >= 4.1
            public void openFileChooser(ValueCallback<Uri> valueCallback, String acceptType, String capture) {
                //Log.e(TAG, "onShowFileChooser: "+acceptType);
                mUploadMessage = valueCallback;
                openImageChooserActivity();
            }

            // For Android >= 5.0
            @Override
            public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) {
                //Log.e(TAG, "onShowFileChooser: "+fileChooserParams);
                mUploadCallbackAboveL = filePathCallback;
                openImageChooserActivity();
                return true;
            }

3、还有一个点是webview里面音视频播放,退出页面,还会有声音等,可以在原生.java的onPause方法里面做处理,我记得这个没处理的时候有的安卓应用商店都是审核不过的。

@Override
    public void onPause() {
        super.onPause();
        if(WXWebView.mWebView != null){
            WXWebView.mWebView.pauseTimers();
            //这里只对页面中只有一个音频的情况做了处理,如果有多个音频需要遍历整个数组记录状态
            WXWebView.mWebView.loadUrl(
                    "javascript:audioEty = document.getElementsByTagName('audio')[0]; audioEty.pause();"
            );
            WXWebView.mWebView.loadUrl(
                    "javascript:videoEty = document.getElementsByTagName('video')[0]; videoEty.pause();"
            );

            WXWebView.mWebView.onPause();
        }
        
    }

页面宽高适配

最后本来是打算想把app的宽高适配等问题放在css那个小结的,但是现在归类在了踩坑里面,就把这个放在这儿讲吧。
1、iOS我是在viewDidLayoutSubviews里面重新绘制了一下,在适配iPhoneX、XR、XMAX的时候会用到,安卓倒是没有怎么处理。

_instance.frame = CGRectMake(safeArea.left, safeArea.top, self.view.frame.size.width-safeArea.left-safeArea.right, _weexHeight-safeArea.top);

2、weex官网config这一章里面有讲到
这个例子,大家也可以扫码看一下,主要就是weex.config.env.deviceWidth和weex.config.env.deviceHeight。

clipboard.png

最后这个系列就剩下两章了,下一篇也会尽快发布出来,感谢大家,如果喜欢欢迎收藏点赞啊~

查看原文

赞 2 收藏 2 评论 0

ronniesong 赞了文章 · 2019-02-12

Go 语言编译过程概述

Golang 是一门需要编译才能运行的编程语言,也就说代码在运行之前需要通过编译器生成二进制机器码,随后二进制文件才能在目标机器上运行,如果我们想要了解 Go 语言的实现原理,理解它的编译过程就是一个没有办法绕过的事情。

这一节会先对 Go 语言编译的过程进行概述,从顶层介绍编译器执行的几个步骤,随后的章节会分别剖析各个步骤完成的工作和实现原理,同时也会对一些需要预先掌握的知识进行介绍和准备,确保后面的章节能够被更好的理解。

目录

预备知识

想要深入了解 Go 语言的编译过程,需要提前了解一下编译过程中涉及的一些术语和专业知识。这些知识其实在我们的日常工作和学习中比较难用到,但是对于理解编译的过程和原理还是非常重要的。这一小节会简单挑选几个常见并且重要的概念提前进行介绍,减少后面章节的理解压力。

抽象语法树

抽象语法树(AST)是源代码语法的结构的一种抽象表示,它用树状的方式表示编程语言的语法结构。抽象语法树中的每一个节点都表示源代码中的一个元素,每一颗子树都表示一个语法元素,例如一个 if else 语句,我们可以从 2 * 3 + 7 这一表达式中解析出下图所示的抽象语法树。

abstract-syntax-tree

作为编译器常用的数据结构,抽象语法树抹去了源代码中不重要的一些字符 - 空格、分号或者括号等等。编译器在执行完语法分析之后会输出一个抽象语法树,这棵树会辅助编译器进行语义分析,我们可以用它来确定结构正确的程序是否存在一些类型不匹配或不一致的问题。

静态单赋值

静态单赋值(SSA)是中间代码的一个特性,如果一个中间代码具有静态单赋值的特性,那么每个变量就只会被赋值一次,在实践中我们通常会用添加下标的方式实现每个变量只能被赋值一次的特性,这里以下面的代码举一个简单的例子:

x := 1
x := 2
y := x

根据分析,我们其实能够发现上述的代码其实并不需要第一个将 1 赋值给 x 的表达式,也就是这一表达式在整个代码片段中是没有作用的:

x1 := 1
x2 := 2
y1 := x2

从使用 SSA 的『中间代码』我们就可以非常清晰地看出变量 y1 的值和 x1 是完全没有任何关系的,所以在机器码生成时其实就可以省略第一步,这样就能减少需要执行的指令来优化这一段代码。

根据 Wikipedia 对 SSA 的介绍来看,在中间代码中使用 SSA 的特性能够为整个程序实现以下的优化:

  1. 常数传播(constant propagation)
  2. 值域传播(value range propagation)
  3. 稀疏有条件的常数传播(sparse conditional constant propagation)
  4. 消除无用的程式码(dead code elimination)
  5. 全域数值编号(global value numbering)
  6. 消除部分的冗余(partial redundancy elimination)
  7. 强度折减(strength reduction)
  8. 寄存器分配(register allocation)

从 SSA 的作用我们就能看出,因为它的主要作用就是代码的优化,所以是编译器后端(主要负责目标代码的优化和生成)的一部分;当然,除了 SSA 之外代码编译领域还有非常多的中间代码优化方法,优化编译器生成的代码是一个非常古老并且复杂的领域,这里就不会展开介绍了。

指令集架构

最后要介绍的一个预备知识就是指令集的架构了,很多开发者都会遇到在生产环境运行的结果和本地不同的问题,导致这种情况的原因其实非常复杂,不同机器使用不同的指令也是可能的原因之一。

我们大多数开发者都会使用 x86_64 的 Macbook 作为工作上主要使用的硬件,在命令行中输入 uname -m 就能够获得当前机器上硬件的信息:

$ uname -m
x86_64

x86_64 是目前比较常见的指令集架构之一,除了 x86_64 之外,还包含其他类型的指令集架构,例如 amd64、arm64 以及 mips 等等,不同的处理器使用了大不相同的机器语言,所以很多编程语言为了在不同的机器上运行需要将源代码根据架构翻译成不同的机器代码。

复杂指令集计算机(CISC)和精简指令集计算机(RISC)是目前的两种 CPU 区别,它们的在设计理念上会有一些不同,从名字我们就能看出来这两种不同的设计有什么区别,复杂指令集通过增加指令的数量减少需要执行的质量数,而精简指令集能使用更少的指令完成目标的计算任务;早期的 CPU 为了减少机器语言指令的数量使用复杂指令集完成计算任务,这两者之前的区别其实就是设计上的权衡,我们会在后面的章节 机器码生成 中详细介绍指令集架构,当然各位读者也可以自行搜索和学习。

编译原理

Go 语言编译器的源代码在 cmd/compile 目录中,目录下的文件共同构成了 Go 语言的编译器,学过编译原理的人可能听说过编译器的前端和后端,编译器的前端一般承担着词法分析、语法分析、类型检查和中间代码生成几部分工作,而编译器后端主要负责目标代码的生成和优化,也就是将中间代码翻译成目标『机器』能够运行的机器码。

complication-process

Go 的编译器在逻辑上可以被分成四个阶段:词法与语法分析、类型检查和 AST 转换、通用 SSA 生成和最后的机器代码生成,在这一节我们会使用比较少的篇幅分别介绍这四个阶段做的工作,后面的章节会具体介绍每一个阶段的具体内容。

词法与语法分析

所有的编译过程其实都是从解析代码的源文件开始的,词法分析的作用就是解析源代码文件,它将文件中的字符串序列转换成 Token 序列,方便后面的处理和解析,我们一般会把执行词法分析的程序称为词法解析器(lexer)。

而语法分析的输入就是词法分析器输出的 Token 序列,这些序列会按照顺序被语法分析器进行解析,语法的解析过程就是将词法分析生成的 Token 按照语言定义好的文法(Grammar)自下而上或者自上而下的进行规约,每一个 Go 的源代码文件最终会被归纳成一个 SourceFile 结构:

SourceFile = PackageClause ";" { ImportDecl ";" } { TopLevelDecl ";" } .

标准的 Golang 语法解析器使用的就是 LALR(1) 的文法,语法解析的结果其实就是上面介绍过的抽象语法树(AST),每一个 AST 都对应着一个单独的 Go 语言文件,这个抽象语法树中包括当前文件属于的包名、定义的常量、结构体和函数等。

golang-files-and-ast

如果在语法解析的过程中发生了任何语法错误,都会被语法解析器发现并将消息打印到标准输出上,整个编译过程也会随着错误的出现而被中止。

我们会在这一章后面的小节 词法与语法分析 中介绍 Go 语言的文法和它的词法与语法解析过程。

类型检查

当拿到一组文件的抽象语法树 AST 之后,Go 语言的编译器会对语法树中定义和使用的类型进行检查,类型检查分别会按照顺序对不同类型的节点进行验证,按照以下的顺序进行处理:

  1. 常量、类型和函数名及类型;
  2. 变量的赋值和初始化;
  3. 函数和闭包的主体;
  4. 哈希键值对的类型;
  5. 导入函数体;
  6. 外部的声明;

通过对每一棵抽象节点树的遍历,我们在每一个节点上都会对当前子树的类型进行验证保证当前节点上不会出现类型错误的问题,所有的类型错误和不匹配都会在这一个阶段被发现和暴露出来。

类型检查的阶段不止会对树状结构的节点进行验证,同时也会对一些内建的函数进行展开和改写,例如 make 关键字在这个阶段会根据子树的结构被替换成 makeslice 或者 makechan 等函数。

golang-keyword-make

我们其实能够看出类型检查不止做了验证类型的工作,还做了对 AST 进行改写,处理 Go 语言内置关键字的活,所以,这一过程在整个编译流程中还是非常重要的,没有这个步骤很多关键字其实就没有办法工作,后面的章节 类型检查 会介绍这一步骤。

中间代码生成

当我们将源文件转换成了抽象语法树、对整棵树的语法进行解析并进行类型检查之后,就可以认为当前文件中的代码基本上不存在无法编译或者语法错误的问题了,Go 语言的编译器就会将输入的 AST 转换成中间代码。

Go 语言编译器的中间代码使用了 SSA(Static Single Assignment Form) 的特性,如果我们在中间代码生成的过程中使用这种特性,就能够比较容易的分析出代码中的无用变量和片段并对代码进行优化。

在类型检查之后,就会通过一个名为 compileFunctions 的函数开始对整个 Go 语言项目中的全部函数进行编译,这些函数会在一个编译队列中等待几个后端工作协程的消费,这些 Goroutine 会将所有函数对应的 AST 转换成使用 SSA 特性的中间代码。

中间代码生成 这一章节会详细介绍中间代码的生成过程并简单介绍 Golang 是如何在中间代码中使用 SSA 的特性的,在这里就不展开介绍其他的内容了。

机器码生成

Go 语言源代码的 cmd/compile/internal 中包含了非常多机器码生成相关的包,不同类型的 CPU 分别使用了不同的包进行生成 amd64、arm、arm64、mips、mips64、ppc64、s390x、x86 和 wasm,也就是说 Go 语言能够在上述的 CPU 指令集类型上运行,其中比较有趣的就是 WebAssembly 了。

作为一种在栈虚拟机上使用的二进制指令格式,它的设计的主要目标就是在 Web 浏览器上提供一种具有高可移植性的目标语言。Go 语言的编译器既然能够生成 WASM 格式的指令,那么就能够运行在常见的主流浏览器中。

$ GOARCH=wasm GOOS=js go build -o lib.wasm main.go

我们可以使用上述的命令将 Go 的源代码编译成能够在浏览器上运行的『汇编语言』,除了这种新兴的指令之外,Go 语言还支持了几乎全部常见的 CPU 指令集类型,也就是说它编译出的机器码能够在使用上述指令集的机器上运行。

机器码生成 一节会详细介绍将中间代码翻译到不同目标机器的过程,在这个章节中也会简单介绍不同的指令集架构的区别。

编译器入口

Go 语言的编译器入口在 src/cmd/compile/internal/pc 包中的 main.go 文件,这个 600 多行的 Main 函数就是 Go 语言编译器的主程序,这个函数会先获取命令行传入的参数并更新编译的选项和配置,随后就会开始运行 parseFiles 函数对输入的所有文件进行词法与语法分析得到文件对应的抽象语法树:

func Main(archInit func(*Arch)) {
    // ...

    lines := parseFiles(flag.Args())

接下来就会分九个阶段对抽象语法树进行更新和编译,就像我们在上面介绍的,整个过程会经历类型检查、SSA 中间代码生成以及机器码生成三个部分:

  1. 检查常量、类型和函数的类型;
  2. 处理变量的赋值;
  3. 对函数的主体进行类型检查;
  4. 决定如何捕获变量;
  5. 检查内联函数的类型;
  6. 进行逃逸分析;
  7. 将闭包的主体转换成引用的捕获变量;
  8. 编译顶层函数;
  9. 检查外部依赖的声明;

了解了剩下的编译过程之后,我们重新回到词法和语法分析后的具体流程,在这里编译器会对生成语法树中的节点执行类型检查,除了常量、类型和函数这些顶层声明之外,它还会对变量的赋值语句、函数主体等结构进行检查:

    for i := 0; i < len(xtop); i++ {
        n := xtop[i]
        if op := n.Op; op != ODCL && op != OAS && op != OAS2 && (op != ODCLTYPE || !n.Left.Name.Param.Alias) {
            xtop[i] = typecheck(n, ctxStmt)
        }
    }

    for i := 0; i < len(xtop); i++ {
        n := xtop[i]
        if op := n.Op; op == ODCL || op == OAS || op == OAS2 || op == ODCLTYPE && n.Left.Name.Param.Alias {
            xtop[i] = typecheck(n, ctxStmt)
        }
    }

    for i := 0; i < len(xtop); i++ {
        n := xtop[i]
        if op := n.Op; op == ODCLFUNC || op == OCLOSURE {
            typecheckslice(Curfn.Nbody.Slice(), ctxStmt)
        }
    }

    checkMapKeys()

    for _, n := range xtop {
        if n.Op == ODCLFUNC && n.Func.Closure != nil {
            capturevars(n)
        }
    }

    escapes(xtop)

    for _, n := range xtop {
        if n.Op == ODCLFUNC && n.Func.Closure != nil {
            transformclosure(n)
        }
    }

类型检查会对传入节点的子节点进行遍历,这个过程会对 make 等关键字进行展开和重写,类型检查结束之后并没有输出新的数据结构,只是改变了语法树中的一些节点,同时这个过程的结束也意味着源代码中已经不存在语法错误和类型错误,中间代码和机器码也都可以正常的生成了。

    initssaconfig()

    peekitabs()

    for i := 0; i < len(xtop); i++ {
        n := xtop[i]
        if n.Op == ODCLFUNC {
            funccompile(n)
        }
    }

    compileFunctions()

    for i, n := range externdcl {
        if n.Op == ONAME {
            externdcl[i] = typecheck(externdcl[i], ctxExpr)
        }
    }

    checkMapKeys()
}

在主程序运行的最后,会将顶层的函数编译成中间代码并根据目标的 CPU 架构生成机器码,不过这里其实也可能会再次对外部依赖进行类型检查以验证正确性。

总结

Go 语言的编译过程其实是非常有趣并且值得学习的,通过对 Go 语言四个编译阶段的分析和对编译器主函数的梳理,我们能够对 Golang 的实现有一些基本的理解,掌握编译的过程之后,Go 语言对于我们来讲也不再是一个黑盒,所以学习其编译原理的过程还是非常让人着迷的。

相关文章

Reference

查看原文

赞 24 收藏 14 评论 0

ronniesong 回答了问题 · 2019-01-16

golang sql如何才能摆脱Scan的参数必须和查询返回的列一一对应的问题?

先获取表结构,再反射

关注 4 回答 3

认证与成就

  • 获得 567 次点赞
  • 获得 9 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 7 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2014-08-22
个人主页被 3.7k 人浏览