注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

swift基础语法(内部函数,外部函数)

内部函数: 默认情况下的参数都是内部参数外部函数: 如果有多个参数的情况, 调用者并不知道每个参数的含义,         只能通过查看头文件的形式理解参数的含义    ...
继续阅读 »
内部函数: 默认情况下的参数都是内部参数
外部函数: 如果有多个参数的情况, 调用者并不知道每个参数的含义,
         只能通过查看头文件的形式理解参数的含义
        那么能不能和OC一样让调用者直观的知道参数的含义呢? 使用外部参数
         外部参数只能外部用, 函数内部不能使用, 函数内部只能使用内部参数
func divisionOpertaion1(a: Double, b:Double) -> Double{
    return a / b
}
func divisionOpertaion2(dividend: Double, divisor:Double) -> Double{
    return dividend / divisor
}
func divisionOpertaion3(dividend a: Double, divisor b:Double) -> Double{
    return a / b
}
print(divisionOpertaion3(dividend: 10, divisor: 3.5))
func divisionOpertaion4(a: Double, divisor b:Double) -> Double{
    return a / b
}
print(divisionOpertaion4(10, divisor: 3.5))
输出结果:
2.85714285714286
2.85714285714286
 
 
func divisionOpertaion(dividend: Double, divisor:Double) -> Double{
    return dividend / divisor
}
print(divisionOpertaion(10, divisor: 3.5))
输出结果:2.85714285714286
 
默认参数:
可以在定义函数的时候给某个参数赋值, 当外部调用没有传递该参数时会自动使用默认值
func joinString(s1:String ,toString s2:String, jioner s3:String) ->String
{
    return s1 + s3 + s2;
}
func joinString2(s1:String ,toString
                 s2:String, jioner
                 s3:String = "❤️") ->String
{
    return s1 + s3 + s2;
}
print(joinString2("hi", toString:"beauty"))
输出结果:hi❤️beauty
 
如果指定了默认参数, 但是确没有声明外部参数时
系统会自动把内部参数名称既作为内部参数也作为外部参数名称
并且在调用时如果需要修改默认参数的值必须写上外部参数名称
func joinString3(s1:String ,toString
                 s2:String,
             jioner:String = "❤️") ->String
{
    return s1 + jioner + s2;
}
print(joinString3("hi", toString:"beauty", jioner:"🐔"))
输出结果: hi🐔beauty
 
在其它语言中默认参数智能出现在参数列表的最后面, 但是在Swift中可以出现在任何位置
func joinString4(s1:String ,
             jioner:String = "❤️",
        toString s2:String) ->String
{
    return s1 + jioner + s2;
}
print(joinString4("hi", jioner:"🐔", toString:"beauty"))
输出结果: hi🐔beauty
 
常量参数和遍历参数:
默认情况下Swift中所有函数的参数都是常量参数
如果想在函数中修改参数, 必须在参数前加上var
func swap(var a:Int, var b:Int)
{
    print("交换前 a = \(a) b = \(b)")
    let temp = a;
    a = b;
    b = temp;
    print("交换后 a = \(a) b = \(b)")
}
swap(10, b: 20)
输出结果:
交换前 a = 10 b = 20
交换后 a = 20 b = 10
 
inout参数
如果想在函数中修改外界传入的参数
可以将参数的var换成inout, 这回会传递参数本身而不是参数的值
func swap(inout a:Int, inout b:Int)
{
    let temp = a;
    a = b;
    b = temp;
}
var x1 = 10;
var y1 = 20;
print("交换前 a = \(x1) b = \(y1)")
swap(&x1, b: &y1)
print("交换后 a = \(x1) b = \(y1)")
输出结果:
交换前 a = 10 b = 20
交换后 a = 20 b = 10
 
 
变参函数
如果没有变参函数 , 并且函数的参数个数又不确定那么只能写多个方法或者用将函数参数改为集合
变参只能放到参数列表的最后一位, 变参必须指定数据类型, 变参只能是同种类型的数据
 
func add(num1:Int, num2:Int, num3:Int) -> Int
{
    let sum = num1 + num2 + num3
    return sum
}
print(add(1, num2: 2, num3: 3))
输出结果:6
 
func add(nums:[Int]) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add([1, 2, 3]))
输出结果:6
 
func add(nums:Int...) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add(1, 2, 3))
输出结果:6
 
func add(other:Int, nums:Int...) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add(99, nums: 1, 2, 3))
输出结果:6
收起阅读 »

swift基础语法(内部函数,外部函数)

内部函数: 默认情况下的参数都是内部参数外部函数: 如果有多个参数的情况, 调用者并不知道每个参数的含义,         只能通过查看头文件的形式理解参数的含义    ...
继续阅读 »
内部函数: 默认情况下的参数都是内部参数
外部函数: 如果有多个参数的情况, 调用者并不知道每个参数的含义,
         只能通过查看头文件的形式理解参数的含义
        那么能不能和OC一样让调用者直观的知道参数的含义呢? 使用外部参数
         外部参数只能外部用, 函数内部不能使用, 函数内部只能使用内部参数
func divisionOpertaion1(a: Double, b:Double) -> Double{
    return a / b
}
func divisionOpertaion2(dividend: Double, divisor:Double) -> Double{
    return dividend / divisor
}
func divisionOpertaion3(dividend a: Double, divisor b:Double) -> Double{
    return a / b
}
print(divisionOpertaion3(dividend: 10, divisor: 3.5))
func divisionOpertaion4(a: Double, divisor b:Double) -> Double{
    return a / b
}
print(divisionOpertaion4(10, divisor: 3.5))
输出结果:
2.85714285714286
2.85714285714286
 
 
func divisionOpertaion(dividend: Double, divisor:Double) -> Double{
    return dividend / divisor
}
print(divisionOpertaion(10, divisor: 3.5))
输出结果:2.85714285714286
 
默认参数:
可以在定义函数的时候给某个参数赋值, 当外部调用没有传递该参数时会自动使用默认值
func joinString(s1:String ,toString s2:String, jioner s3:String) ->String
{
    return s1 + s3 + s2;
}
func joinString2(s1:String ,toString
                 s2:String, jioner
                 s3:String = "❤️") ->String
{
    return s1 + s3 + s2;
}
print(joinString2("hi", toString:"beauty"))
输出结果:hi❤️beauty
 
如果指定了默认参数, 但是确没有声明外部参数时
系统会自动把内部参数名称既作为内部参数也作为外部参数名称
并且在调用时如果需要修改默认参数的值必须写上外部参数名称
func joinString3(s1:String ,toString
                 s2:String,
             jioner:String = "❤️") ->String
{
    return s1 + jioner + s2;
}
print(joinString3("hi", toString:"beauty", jioner:"🐔"))
输出结果: hi🐔beauty
 
在其它语言中默认参数智能出现在参数列表的最后面, 但是在Swift中可以出现在任何位置
func joinString4(s1:String ,
             jioner:String = "❤️",
        toString s2:String) ->String
{
    return s1 + jioner + s2;
}
print(joinString4("hi", jioner:"🐔", toString:"beauty"))
输出结果: hi🐔beauty
 
常量参数和遍历参数:
默认情况下Swift中所有函数的参数都是常量参数
如果想在函数中修改参数, 必须在参数前加上var
func swap(var a:Int, var b:Int)
{
    print("交换前 a = \(a) b = \(b)")
    let temp = a;
    a = b;
    b = temp;
    print("交换后 a = \(a) b = \(b)")
}
swap(10, b: 20)
输出结果:
交换前 a = 10 b = 20
交换后 a = 20 b = 10
 
inout参数
如果想在函数中修改外界传入的参数
可以将参数的var换成inout, 这回会传递参数本身而不是参数的值
func swap(inout a:Int, inout b:Int)
{
    let temp = a;
    a = b;
    b = temp;
}
var x1 = 10;
var y1 = 20;
print("交换前 a = \(x1) b = \(y1)")
swap(&x1, b: &y1)
print("交换后 a = \(x1) b = \(y1)")
输出结果:
交换前 a = 10 b = 20
交换后 a = 20 b = 10
 
 
变参函数
如果没有变参函数 , 并且函数的参数个数又不确定那么只能写多个方法或者用将函数参数改为集合
变参只能放到参数列表的最后一位, 变参必须指定数据类型, 变参只能是同种类型的数据
 
func add(num1:Int, num2:Int, num3:Int) -> Int
{
    let sum = num1 + num2 + num3
    return sum
}
print(add(1, num2: 2, num3: 3))
输出结果:6
 
func add(nums:[Int]) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add([1, 2, 3]))
输出结果:6
 
func add(nums:Int...) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add(1, 2, 3))
输出结果:6
 
func add(other:Int, nums:Int...) -> Int
{
    var sum = 0;
    for num in nums
    {
        sum += num
    }
    return sum
}
print(add(99, nums: 1, 2, 3))
输出结果:6
收起阅读 »

swift 基础语法(19-闭包,闭包函数回调,尾随闭包,闭包捕获值)

闭包:函数是闭包的一种类似于OC语言的block闭包表达式(匿名函数) -- 能够捕获上下文中的值语法: in关键字的目的是便于区分返回值和执行语句闭包表达式的类型和函数的类型一样, 是参数加上返回值, 也就是in之前的部分{   ...
继续阅读 »
闭包:
函数是闭包的一种
类似于OC语言的block
闭包表达式(匿名函数) -- 能够捕获上下文中的值

语法: in关键字的目的是便于区分返回值和执行语句
闭包表达式的类型和函数的类型一样, 是参数加上返回值, 也就是in之前的部分
{
    (参数) -> 返回值类型 in
    执行语句
}
完整写法
let say:(String) -> Void = {
    (name: String) -> Void in
    print("hi \(name)")
}
say("qbs")
输出结果:  hi qbs
 
没有返回值写法
let say:(String) ->Void = {
    (name: String) in
    print("hi \(name)")
}
say("qbs")
输出结果:  hi qbs
 
没有参数没有返回值写法
let say:() ->Void = {
    print("hi qbs")
}
say()
输出结果:  hi qbs
 
 
闭包表达式作为回调函数
传统数组排序写法:
缺点: 不一定是小到大, 不一定是全部比较, 有可能只比较个位数
           所以, 如何比较可以交给调用者决定
func bubbleSort(inout array:[Int])
{
    let count = array.count;
    for var i = 1; i < count; i++
    {
        for var j = 0; j < (count - i); j++
        {
            if array[j] > array[j + 1]
            {
                let temp = array[j]
                array[j] = array[j + 1]
                array[j + 1] = temp
            }
        }
    }
}
 
闭包写法:
func bubbleSort(inout array:[Int], cmp: (Int, Int) -> Int)
{
    let count = array.count;
    for var i = 1; i < count; i++
    {
        for var j = 0; j < (count - i); j++
        {
            if cmp(array[j], array[j + 1]) == -1
            {
                let temp = array[j]
                array[j] = array[j + 1]
                array[j + 1] = temp
            }
        }
    }
}
 
let cmp = {
    (a: Int, b: Int) -> Int in
    if a > b{
        return 1;
    }else if a < b
    {
        return -1;
    }else
    {
        return 0;
    }
}
var arr:Array<Int> = [31, 13, 52, 84, 5]
bubbleSort(&arr, cmp: cmp)
print(arr)
 
输出结果:
[84, 52, 31, 13, 5]
 
 
闭包作为参数传递
var arr:Array<Int> = [31, 13, 52, 84, 5]
bubbleSort(&arr, cmp: {
    (a: Int, b: Int) -> Int in
    if a > b{
        return 1;
    }else if a < b
    {
        return -1;
    }else
    {
        return 0;
    }
})
print(arr)
输出结果:
[84, 52, 31, 13, 5]
 
尾随闭包:
如果闭包是最后一个参数, 可以直接将闭包写到参数列表后面
这样可以提高阅读性. 称之为尾随闭包
bubbleSort(&arr) {
    (a: Int, b: Int) -> Int in
    if a > b{
        return 1;
    }else if a < b
    {
        return -1;
    }else
    {
        return 0;
    }
}
 
闭包表达式优化
 1.类型优化, 由于函数中已经声明了闭包参数的类型
   所以传入的实参可以不用写类型
 2.返回值优化, 同理由于函数中已经声明了闭包的返回值类型
   所以传入的实参可以不用写类型
 3.参数优化, swift可以使用$索引的方式来访问闭包的参数, 默认从0开始
bubbleSort(&arr){
   (a , b) -> Int in
   (a , b) in
    if $0 > $1{
        return 1;
    }else if $0 < $1
    {
        return -1;
    }else
    {
        return 0;
    }
}
 
 
如果只有一条语句可以省略return
let hehe = {
    "我是qbs"
}
闭包捕获值
func getIncFunc() -> (Int) -> Int
{
    var max = 10
    func incFunc(x :Int) ->Int{
        print("incFunc函数结束")
        max++
        return max + x
    }
    当执行到这一句时inc参数就应该被释放了
    但是由于在内部函数中使用到了它, 所以它被捕获了
    同理, 当执行完这一句时max变量就被释放了
    但是由于在内部函数中使用到了它, 所以它被捕获了
    print("getIncFunc函数结束")
    return incFunc
}
 
被捕获的值会和与之对应的方法绑定在一起
同一个方法中的变量会被绑定到不同的方法中
let incFunc = getIncFunc()
print("---------")
print(incFunc(5))
print("---------")
print(incFunc(5))
输出结果:
getIncFunc函数结束
---------
incFunc
函数结束
16
---------
incFunc
函数结束
17
 
 
let incFunc2 = getIncFunc(5)
print(incFunc2(5))
输出结果:
getIncFunc函数结束
incFunc函数结束
16
收起阅读 »

Kotlin infix 关键字与高阶函数的应用[第一行代码 Kotlin 学习笔记]

使用 infix 函数构建更可读的语法 在前面的 Kotlin 学习笔记中,我们已经多次使用过 A to B 这样的语法结构构建键值对,包括 Kotlin 自带的 mapOf() 函数。 这种语法结构的优点是可读性高,相比于调用一个函数,它更接近于使用英语...
继续阅读 »

使用 infix 函数构建更可读的语法


在前面的 Kotlin 学习笔记中,我们已经多次使用过 A to B 这样的语法结构构建键值对,包括 Kotlin 自带的 mapOf() 函数。


这种语法结构的优点是可读性高,相比于调用一个函数,它更接近于使用英语的语法来编写程序。可能你会好奇,这种功能是怎么实现的呢?to 是不是 Kotlin 语言中的一个关键字?本节我们就对这个功能进行深度解密。


首先,to 并不是 Kotlin 语言中的一个关键字,之所以我们能够使用 A to B 这样的语法结构,是因为 Kotlin 提供了一种高级语法糖特性:infix 函数。当然,infix 函数也并不是什么难理解的事物,它只是把编程语言函数调用的语法规则调整了一下而已,比如 A to B 这样的写法,实际上等价于 A.to(B) 的写法。


下面我们就通过两个具体的例子来学习一下 infix 函数的用法,先从简单的例子看起。


String 类中有一个 startsWith() 函数,你一定使用过,它可以用于判断一个字符串是否是以某个指定参数开头的。比如说下面这段代码的判断结果一定会是 true:


if ("Hello Kotlin".startsWith("Hello")) {
// 处理具体的逻辑
}

startsWith() 函数的用法虽然非常简单,但是借助 infix 函数,我们可以使用一种更具可读性的语法来表达这段代码。新建一个 infix.kt 文件,然后编写如下代码:


infix fun String.beginsWith(prefix: String) = startsWith(prefix)

首先,除去最前面的 infix 关键字不谈,这是一个 String 类的扩展函数。我们给 String 类添加了一个 beginsWith() 函数,它也是用于判断一个字符串是否是以某个指定参数开头的,并且它的内部实现就是调用的 String 类的 startsWith() 函数。


但是加上了 infix 关键字之后,beginsWith() 函数就变成了一个 infix 函数,这样除了传统的函数调用方式之外,我们还可以用一种特殊的语法糖格式调用 beginsWith() 函数,如下所示:


if ("Hello World" beginsWith "Hello") {
// 处理具体的逻辑
}

从这个例子就能看出,infix 函数的语法规则并不复杂,上述代码其实就是调用的 " HelloKotlin " 这个字符串的 beginsWith() 函数,并传入了一个 "Hello" 字符串作为参数。但是 infix 函数允许我们将函数调用时的小数点、括号等计算机相关的语法去掉,从而使用一种更接近英语的语法来编写程序,让代码看起来更加具有可读性。


另外,infix 函数由于其语法糖格式的特殊性,有两个比较严格的限制:首先,infix 函数是不能定义成顶层函数的,它必须是某个类的成员函数,可以使用扩展函数的方式将它定义到某个类当中;其次,infix 函数必须接收且只能接收一个参数,至于参数类型是没有限制的。只有同时满足这两点, infix 函数的语法糖才具备使用的条件,你可以思考一下是不是这个道理。


看完了简单的例子,接下来我们再看一个复杂一些的例子。比如这里有一个集合,如果想要判断集合中是否包括某个指定元素,一般可以这样写:


val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
if (list.contains("Banana")) {
// 处理具体的逻辑
}

很简单对吗?但我们仍然可以借助 infix 函数让这段代码变得更加具有可读性。在 infix.kt 文件中添加如下代码:


infix fun <T> Collections<T>.has(element: T) = contains(element)

可以看到,我们给 Collection 接口添加了一个扩展函数,这是因为 Collection 是 Java 以及 Kotlin 所有集合的总接口,因此给 Collection 添加一个 has() 函数,那么所有集合的子类就都可以使用这个函数了。


另外,这里还使用了泛型函数的定义方法,从而使得 has() 函数可以接收任意具体类型的参数。而这个函数内部的实现逻辑就相当简单了,只是调用了 Collection 接口中的 contains() 函数而已。也就是说,has() 函数和 contains() 函数的功能实际上是一模一样的,只是它多了一个 infix 关键字,从而拥有了 infix 函数的语法糖功能。


现在我们就可以使用如下的语法来判断集合中是否包括某个指定的元素:


val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
if (list has "Banana") {
// 处理具体的逻辑
}

好了,两个例子都已经看完了,你对于 infix 函数应该也了解得差不多了。但是或许现在你的心中还有一个疑惑没有解开,就是 mapOf() 函数中允许我们使用 A to B 这样的语法来构建键值对,它的具体实现是怎样的呢?为了解开谜团,我们直接来看一看 to() 函数的源码吧,如下所示:


public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

可以看到,这里使用定义泛型函数的方式将 to() 函数定义到了 A 类型下,并且接收一个 B 类型的参数。因此 A 和 B 可以是两种不同类型的泛型,也就使得我们可以构建出字符串 to 整型这样的键值对。


再来看 to() 函数的具体实现,非常简单,就是创建并返回了一个 Pair 对象。也就是说,A to B 这样的语法结构实际上得到的是一个包含 A、B 数据的 Pair 对象,而 mapOf() 函数实际上接收的正是一个 Pair 类型的可变参数列表,这样我们就将这种神奇的语法结构完全解密了。


本着动手实践的精神,其实我们也可以模仿 to() 函数的源码来编写一个自己的键值对构建函数。在 infix.kt 文件中添加如下代码:


infix fun <A, B> A.with(that: B): Pair<A, B> = Pair(this, that)

这里只是将 to() 函数改名成了 with() 函数,其他实现逻辑是相同的,因此相信没有什么解释的必要。现在我们的项目中就可以使用 with() 函数来构建键值对了,还可以将构建的键值对传入 mapOf() 方法中:


val map = mapOf("Apple" with 1, "Banana" with 2, "Orange" with 3, "Pear" with 4, "Grape" with 5)

是不是很神奇?这就是 infix 函数给我们带来的诸多有意思的功能,灵活运用它确实可以让语法变得更具可读性。


高阶函数的应用


高阶函数非常适用于简化各种 API 的调用,一些 API 的原有用法在使用高阶函数简化之后,不管是在易用性还是可读性方面,都可能会有很大的提升。


为了进行举例说明,我们在本节会使用高阶函数简化 SharedPreferences 和 ContentValues 这两种 API 的用法,让它们的使用变得更加简单。


简化 SharedPreferences 的用法


首先来看 SharedPreferences,在开始对它进行简化之前,我们先回顾一下 SharedPreferences 原来的用法。向 SharedPreferences 中存储数据的过程大致可以分为以下 3 步:



  1. 调用 SharedPreferences 的 edit() 方法获取 SharedPreferences.Editor 对象;

  2. 向 SharedPreferences.Editor 对象中添加数据;

  3. 调用 apply() 方法将添加的数据提交,完成数据存储操作。


对应的代码示例如下:


val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name", "Tom")
editor.putInt("age", 28)
editor.putBoolean("married", false)
editor.apply()

当然,这段代码其实本身已经足够简单了,但是这种写法更多还是在用 Java 的编程思维来编写代码,而在 Kotlin 当中我们明显可以做到更好。


接下来我们就尝试使用高阶函数简化 SharedPreferences 的用法,新建一个 SharedPreferences.kt 文件,然后在里面加入如下代码:


fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) {
val editor = edit()
editor.block()
editor.apply()
}

这段代码虽然不长,但是涵盖了高阶函数的各种精华,下面我来解释一下。


首先,我们通过扩展函数的方式向 SharedPreferences 类中添加了一个 open 函数,并且它还接收一个函数类型的参数,因此 open 函数自然就是一个高阶函数了。


由于 open 函数内拥有 SharedPreferences 的上下文,因此这里可以直接调用 edit() 方法来获取 SharedPreferences.Editor 对象。另外 open 函数接收的是一个 SharedPreferences.Editor 的函数类型参数,因此这里需要调用 editor.block() 对函数类型参数进行调用,我们就可以在函数类型参数的具体实现中添加数据了。最后还要调用 editor.apply() 方法来提交数据,从而完成数据存储操作。


定义好了 open 函数之后,我们以后在项目中使用 SharedPreferences 存储数据就会更加方便了,写法如下所示:


getSharedPreferences("data", Context.MODE_PRIVATE).open {
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}

可以看到,我们可以直接在 SharedPreferences 对象上调用 open 函数,然后在 Lambda 表达式中完成数据的添加操作。注意,现在 Lambda 表达式拥有的是 SharedPreferences.Editor 的上下文环境,因此这里可以直接调用相应的 put 方法来添加数据。最后我们也不再需要调用 apply() 方法来提交数据了,因为 open 函数会自动完成提交操作。


怎么样,使用高阶函数简化之后,不管是在易用性还是在可读性上,SharedPreferences 的用法是不是都简化了很多?这就是高阶函数的魅力所在。好好掌握这个知识点,以后在诸多其他 API 的使用方面,我们都可以使用这个技巧,让API变得更加简单。


当然,最后不得不提的是,其实 Google 提供的 KTX 扩展库中已经包含了上述 SharedPreferences 的简化用法,这个扩展库会在 Android Studio 创建项目的时候自动引入 build.gradle 的 dependencies 中。


因此,我们实际上可以直接在项目中使用如下写法来向 SharedPreferences 存储数据:


getSharedPreferences("data", Context.MODE_PRIVATE).edit {
putString("name", "Tom")
putInt("age", 28)
putBoolean("married", false)
}

可以看到,其实就是将 open 函数换成了 edit 函数,但是 edit 函数的语义性明显要更好一些。当然,我前面命名成 open 函数,主要是为了防止和 KTX 的 edit 函数同名,以免你在理解的时候产生混淆。


那么你可能会问了,既然 Google 的 KTX 库中已经自带了一个 edit 函数,我们为什么还编写这个 open 函数呢?这是因为我希望你对于高阶函数的理解不要仅仅停留在使用的层面,而是要知其然也知其所以然。KTX 中提供的功能必然是有限的,但是掌握了它们背后的实现原理,你将可以对无限的 API 进行更多的扩展。


简化 ContentValues 的用法


接下来我们开始学习如何简化 ContentValues 的用法。


ContentValues 的基本用法在 7.4 节中已经学过了,它主要用于结合 SQLiteDatabase 的 API 存储和修改数据库中的数据,具体的用法示例如下:


val values = ContentValues()
values.put("name", "Game of Thrones")
values.put("author", "George Martin")
values.put("pages", 720)
values.put("price", 20.85)
db.insert("Book", null, values)

你可能会说,这段代码可以使用 apply 函数进行简化。这当然没有错,只是我们其实还可以做到更好。


不过在正式开始我们的简化之旅之前,我还得向你介绍一个额外的知识点。还记得在 2.6.1 小节中学过的 mapOf() 函数的用法吗?它允许我们使用 "Apple" to 1 这样的语法结构快速创建一个键值对。这里我先为你进行部分解密,在 Kotlin 中使用 A to B 这样的语法结构会创建一个 Pair 对象,暂时你只需要知道这些就可以了,至于为什么,我们将在第 9 章的 Kotlin 课堂中学习。


有了这个知识前提之后,就可以进行下一步了。新建一个 ContentValues.kt 文件,然后在里面定义一个 cvOf() 方法,如下所示:


fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {

}

这个方法的作用是构建一个 ContentValues 对象,有几点我需要解释一下。首先,cvOf() 方法接收了一个 Pair 参数,也就是使用 A to B 语法结构创建出来的参数类型,但是我们在参数前面加上了一个 vararg 关键字,这是什么意思呢?其实 vararg 对应的就是 Java 中的可变参数列表,我们允许向这个方法传入 0 个、1 个、2 个甚至任意多个 Pair 类型的参数,这些参数都会被赋值到使用 vararg 声明的这一个变量上面,然后使用 for-in 循环可以将传入的所有参数遍历出来。


再来看声明的 Pair 类型。由于 Pair 是一种键值对的数据结构,因此需要通过泛型来指定它的键和值分别对应什么类型的数据。值得庆幸的是,ContentValues 的所有键都是字符串类型的,这里可以直接将 Pair 键的泛型指定成 String。但 ContentValues 的值却可以有多种类型(字符串型、整型、浮点型,甚至是 null),所以我们需要将 Pair 值的泛型指定成 Any?。这是因为 Any 是 Kotlin 中所有类的共同基类,相当于 Java 中的 Object,而 Any? 则表示允许传入空值。


接下来我们开始为 cvOf() 方法实现功能逻辑,核心思路就是先创建一个 ContentValues 对象,然后遍历 pairs 参数列表,取出其中的数据并填入 ContentValues 中,最终将 ContentValues 对象返回即可。思路并不复杂,但是存在一个问题:Pair 参数的值是 Any? 类型的,我们怎样让它和 ContentValues 所支持的数据类型对应起来呢?这个确实没有什么好的办法,只能使用 when 语句一一进行条件判断,并覆盖 ContentValues 所支持的所有数据类型。结合下面的代码来理解应该更加清楚一些:


fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues {
val cv = ContentValues()
for (pair in pairs) {
val key = pair.first
val value = pair.second
when (value) {
is Int -> cv.put(key, value)
is Long -> cv.put(key, value)
is Short -> cv.put(key, value)
is Float -> cv.put(key, value)
is Double -> cv.put(key, value)
is Boolean -> cv.put(key, value)
is String -> cv.put(key, value)
is Byte -> cv.put(key, value)
is ByteArray -> cv.put(key, value)
null -> cv.putNull(key)
}
}
return cv
}

可以看到,上述代码基本就是按照刚才所说的思路进行实现的。我们使用 for-in 循环遍历了 pairs 参数列表,在循环中取出了 key 和 value,并使用 when 语句来判断 value 的类型。注意,这里将 ContentValues 所支持的所有数据类型全部覆盖了进去,然后将参数中传入的键值对逐个添加到 ContentValues 中,最终将 ContentValues 返回。


另外,这里还使用了 Kotlin 中的 Smart Cast 功能。比如 when 语句进入 Int 条件分支后,这个条件下面的 value 会被自动转换成 Int 类型,而不再是 Any? 类型,这样我们就不需要像 Java 中那样再额外进行一次向下转型了,这个功能在 if 语句中也同样适用。


有了这个 cvOf() 方法之后,我们使用 ContentValues 时就会变得更加简单了,比如向数据库中插入一条数据就可以这样写:


val values = cvOf("name" to "Game of Thrones", "author" to "George Martin", "pages" to 720, "price" to 20.85)
db.insert("Book", null, values)

怎么样?现在我们可以使用类似于 mapOf() 函数的语法结构来构建 ContentValues 对象,有没有觉得很神奇?


当然,虽然 cvOf() 方法已经非常好用了,但是它和高阶函数却一点关系也没有。因为 cvOf() 方法接收的参数是 Pair 类型的可变参数列表,返回值是 ContentValues 对象,完全没有用到函数类型,这和高阶函数的定义不符。


从功能性方面,cvOf() 方法好像确实用不到高阶函数的知识,但是从代码实现方面,却可以结合高阶函数来进行进一步的优化。比如借助 apply 函数,cvOf() 方法的实现将会变得更加优雅:


fun cvOf(vararg pairs: Pair<String, Any?>) = ContentValues().apply {
for (pair in pairs) {
val key = pair.first
when (val value = pair.second) {
is Int -> put(key, value)
is Long -> put(key, value)
is Short -> put(key, value)
is Float -> put(key, value)
is Double -> put(key, value)
is Boolean -> put(key, value)
is String -> put(key, value)
is Byte -> put(key, value)
is ByteArray -> put(key, value)
null -> putNull(key)
}
}
}

由于 apply 函数的返回值就是它的调用对象本身,因此这里我们可以使用单行代码函数的语法糖,用等号替代返回值的声明。另外,apply 函数的 Lambda 表达式中会自动拥有 ContentValues 的上下文,所以这里可以直接调用 ContentValues 的各种 put 方法。借助高阶函数之后,你有没有觉得代码变得更加优雅一些了呢?


当然,虽然我们编写了一个非常好用的 cvOf() 方法,但是或许你已经猜到了,KTX 库中也提供了一个具有同样功能的 contentValuesOf() 方法,用法如下所示:


val values = contentValuesOf("name" to "Game of Thrones", "author" to "George Martin", "pages" to 720, "price" to 20.85)
db.insert("Book", null, values)

平时我们在编写代码的时候,直接使用 KTX 提供的 contentValuesOf() 方法就可以了,但是通过本小节的学习,你不仅掌握了它的用法,还明白了它的源码实现,有没有觉得收获了更多呢?


收起阅读 »

IOS-实例化讲解RunLoop(应用于子线程)

iOS
实例化讲解RunLoop之前看过很多有关RunLoop的文章,其中要么是主要介绍RunLoop的基本概念,要么是主要讲解RunLoop的底层原理,很少用真正的实例来讲解RunLoop的,这其中有大部分原因是由于大家在项目中很少能用到RunLoop吧。基于这种原...
继续阅读 »

实例化讲解RunLoop

之前看过很多有关RunLoop的文章,其中要么是主要介绍RunLoop的基本概念,要么是主要讲解RunLoop的底层原理,很少用真正的实例来讲解RunLoop的,这其中有大部分原因是由于大家在项目中很少能用到RunLoop吧。基于这种原因,本文中将用很少的篇幅来对基础内容做以介绍,然后主要利用实例来加深大家对RunLoop的理解,本文中的代码已经上传GitHub,大家可以下载查看,有问题欢迎Issue我。本文主要分为如下几个部分:

  • RunLoop的基础知识
  • 初识RunLoop,如何让RunLoop进驻线程
  • 深入理解Perform Selector
  • 一直"活着"的后台线程
  • 深入理解NSTimer
  • 让两个后台线程有依赖性的一种方式
  • NSURLConnetction的内部实现
  • AFNetWorking中是如何使用RunLoop的?
  • 其它:利用GCD实现定时器功能
  • 延伸阅读

一、RunLoop的基本概念:

什么是RunLoop?提到RunLoop,我们一般都会提到线程,这是为什么呢?先来看下官方对RunLoop的定义:RunLoop系统中和线程相关的基础架构的组成部分(和线程相关),一个RunLoop是一个事件处理环,系统利用这个事件处理环来安排事务,协调输入的各种事件。RunLoop的目的是让你的线程在有工作的时候忙碌,没有工作的时候休眠(和线程相关)。可能这样说你还不是特别清楚RunLoop究竟是用来做什么的,打个比方来说明:我们把线程比作一辆跑车,把这辆跑车的主人比作RunLoop,那么在没有'主人'的时候,这个跑车的生命是直线型的,其启动,运行完之后就会废弃(没有人对其进行控制,'撞坏'被收回),当有了RunLoop这个主人之后,‘线程’这辆跑车的生命就有了保障,这个时候,跑车的生命是环形的,并且在主人有比赛任务的时候就会被RunLoop这个主人所唤醒,在没有任务的时候可以休眠(在IOS中,开启线程是很消耗性能的,开启主线程要消耗1M内存,开启一个后台线程需要消耗512k内存,我们应当在线程没有任务的时候休眠,来释放所占用的资源,以便CPU进行更加高效的工作),这样可以增加跑车的效率,也就是说RunLoop是为线程所服务的。这个例子有点不是很贴切,线程和RunLoop之间是以键值对的形式一一对应的,其中key是thread,value是runLoop(这点可以从苹果公开的源码中看出来)其实RunLoop是管理线程的一种机制,这种机制不仅在IOS上有,在Node.js中的EventLoop,Android中的Looper,都有类似的模式。刚才所说的比赛任务就是唤醒跑车这个线程的一个source;RunLoop Mode就是,一系列输入的source,timer以及observerRunLoop Mode包含以下几种: NSDefaultRunLoopMode,NSEventTrackingRunLoopMode,UIInitializationRunLoopMode,NSRunLoopCommonModes,NSConnectionReplyMode,NSModalPanelRunLoopMode,至于这些mode各自的含义,读者可自己查询,网上不乏这类资源;

二、初识RunLoop,如何让RunLoop进驻线程

我们在主线程中添加如下代码:

  1. while (1) {
  2. NSLog(@"while begin");
  3. // the thread be blocked here
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  6. // this will not be executed
  7. NSLog(@"while end");
  8. }

这个时候我们可以看到主线程在执行完[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];之后被阻塞而没有执行下面的NSLog(@"while end");同时,我们利用GCD,将这段代码放到一个后台线程中:

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. while (1) {
  3. NSLog(@"while begin");
  4. NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
  5. [subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  6. NSLog(@"while end");
  7. }
  8. });

这个时候我们发现这个while循环会一直在执行;这是为什么呢?我们先将这两个RunLoop分别打印出来:

主线程的RunLoop


由于这个日志比较长,我就只截取了上面的一部分。
我们再看我们新建的子线程中的RunLoop,打印出来之后:

backGroundThreadRunLoop.png


从中可以看出来:我们新建的线程中:

 

  1. sources0 = (null),
  2. sources1 = (null),
  3. observers = (null),
  4. timers = (null),

我们看到虽然有Mode,但是我们没有给它soures,observer,timer,其实Mode中的这些source,observer,timer,统称为这个Modeitem,如果一个Mode中一个item都没有,则这个RunLoop会直接退出,不进入循环(其实线程之所以可以一直存在就是由于RunLoop将其带入了这个循环中)。下面我们为这个RunLoop添加个source:

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. while (1) {
  3. NSPort *macPort = [NSPort port];
  4. NSLog(@"while begin");
  5. NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
  6. [subRunLoop addPort:macPort forMode:NSDefaultRunLoopMode];
  7. [subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  8. NSLog(@"while end");
  9. NSLog(@"%@",subRunLoop);
  10. }
  11. });

这样我们可以看到能够实现了和主线程中相同的效果,线程在这个地方暂停了,为什么呢?我们明天让RunLoop在distantFuture之前都一直run的啊?相信大家已经猜出出来了。这个时候线程被RunLoop带到‘坑’里去了,这个‘坑’就是一个循环,在循环中这个线程可以在没有任务的时候休眠,在有任务的时候被唤醒;当然我们只用一个while(1)也可以让这个线程一直存在,但是这个线程会一直在唤醒状态,及时它没有任务也一直处于运转状态,这对于CPU来说是非常不高效的。
小结:我们的RunLoop要想工作,必须要让它存在一个Item(source,observer或者timer),主线程之所以能够一直存在,并且随时准备被唤醒就是应为系统为其添加了很多Item

三、深入理解Perform Selector

我们先在主线程中使用下performselector:

  1. - (void)tryPerformSelectorOnMianThread{
  2. [self performSelector:@selector(mainThreadMethod) withObject:nil]; }
  3. - (void)mainThreadMethod{
  4. NSLog(@"execute %s",__func__);
  5. // print: execute -[ViewController mainThreadMethod]
  6. }

这样我们在ViewDidLoad中调用tryPerformSelectorOnMianThread,就会立即执行,并且输出:print: execute -[ViewController mainThreadMethod];
和上面的例子一样,我们使用GCD,让这个方法在后台线程中执行

  1. - (void)tryPerformSelectorOnBackGroundThread{
  2. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  3. [self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
  4. });
  5. }
  6. - (void)backGroundThread{
  7. NSLog(@"%u",[NSThread isMainThread]);
  8. NSLog(@"execute %s",__FUNCTION__);
  9. }

同样的,我们调用tryPerformSelectorOnBackGroundThread这个方法,我们会发现,下面的backGroundThread不会被调用,这是什么原因呢?
这是因为,在调用performSelector:onThread: withObject: waitUntilDone的时候,系统会给我们创建一个Timer的source,加到对应的RunLoop上去,然而这个时候我们没有RunLoop,如果我们加上RunLoop:

  1. - (void)tryPerformSelectorOnBackGroundThread{
  2. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  3. [self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop run];
  6. });
  7. }

这时就会发现我们的方法正常被调用了。那么为什么主线程中的perfom selector却能够正常调用呢?通过上面的例子相信你已经猜到了,主线程的RunLoop是一直存在的,所以我们在主线程中执行的时候,无需再添加RunLoop。从Apple的文档中我们也可以得到验证:

Each request to perform a selector is queued on the target thread’s run loop and the requests are then processed sequentially in the order in which they were received. 每个执行perform selector的请求都以队列的形式被放到目标线程的run loop中。然后目标线程会根据进入run loop的顺序来一一执行。

小结:当perform selector在后台线程中执行的时候,这个线程必须有一个开启的runLoop

四、一直"活着"的后台线程

现在有这样一个需求,每点击一下屏幕,让子线程做一个任务,然后大家一般会想到这样的方式:

  1. @interface ViewController ()
  2. @property(nonatomic,strong) NSThread *myThread;
  3. @end
  4. @implementation ViewController
  5. - (void)alwaysLiveBackGoundThread{
  6. NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(myThreadRun) object:@"etund"];
  7. self.myThread = thread;
  8. [self.myThread start];
  9. }
  10. - (void)myThreadRun{
  11. NSLog(@"my thread run");
  12. }
  13. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  14. NSLog(@"%@",self.myThread);
  15. [self performSelector:@selector(doBackGroundThreadWork) onThread:self.myThread withObject:nil waitUntilDone:NO];
  16. }
  17. - (void)doBackGroundThreadWork{
  18. NSLog(@"do some work %s",__FUNCTION__);
  19. }
  20. @end

这个方法中,我们利用一个强引用来获取了后台线程中的thread,然后在点击屏幕的时候,在这个线程上执行doBackGroundThreadWork这个方法,此时我们可以看到,在touchesBegin方法中,self.myThread是存在的,但是这是为是什么呢?这就要从线程的五大状态来说明了:新建状态、就绪状态、运行状态、阻塞状态、死亡状态,这个时候尽管内存中还有线程,但是这个线程在执行完任务之后已经死亡了,经过上面的论述,我们应该怎样处理呢?我们可以给这个线程的RunLoop添加一个source,那么这个线程就会检测这个source等待执行,而不至于死亡(有工作的强烈愿望而不死亡):

  1. - (void)myThreadRun{
  2. [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
  3. [[NSRunLoop currentRunLoop] run]
  4. NSLog(@"my thread run");
  5. }

这个时候再次点击屏幕,我们就会发现,后台线程中执行的任务可以正常进行了。
小结:正常情况下,后台线程执行完任务之后就处于死亡状态,我们要避免这种情况的发生可以利用RunLoop,并且给它一个Source这样来保证线程依旧还在

五、深入理解NSTimer

我们平时使用NSTimer,一般是在主线程中的,代码大多如下:

  1. - (void)tryTimerOnMainThread{
  2. NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self
  3. selector:@selector(timerAction) userInfo:nil repeats:YES];
  4. [myTimer fire];
  5. }
  6. - (void)timerAction{
  7. NSLog(@"timer action");
  8. }

这个时候代码按照我们预定的结果运行,如果我们把这个Tiemr放到后台线程中呢?

  1. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  2. NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
  3. [myTimer fire];
  4. });

这个时候我们会发现,这个timer只执行了一次,就停止了。这是为什么呢?通过上面的讲解,想必你已经知道了,NSTimer,只有注册到RunLoop之后才会生效,这个注册是由系统自动给我们完成的,既然需要注册到RunLoop,那么我们就需要有一个RunLoop,我们在后台线程中加入如下的代码:

  1. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  2. [runLoop run];

这样我们就会发现程序正常运行了。在Timer注册到RunLoop之后,RunLoop会为其重复的时间点注册好事件,比如1:10,1:20,1:30这几个时间点。有时候我们会在这个线程中执行一个耗时操作,这个时候RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer,这就造成了误差(Timer有个冗余度属性叫做tolerance,它标明了当前点到后,容许有多少最大误差),可以在执行一段循环之后调用一个耗时操作,很容易看到timer会有很大的误差,这说明在线程很闲的时候使用NSTiemr是比较傲你准确的,当线程很忙碌时候会有较大的误差。系统还有一个CADisplayLink,也可以实现定时效果,它是一个和屏幕的刷新率一样的定时器。如果在两次屏幕刷新之间执行一个耗时的任务,那其中就会有一个帧被跳过去,造成界面卡顿。另外GCD也可以实现定时器的效果,由于其和RunLoop没有关联,所以有时候使用它会更加的准确,这在最后会给予说明。

六、让两个后台线程有依赖性的一种方式

给两个后台线程添加依赖可能有很多的方式,这里说明一种利用RunLoop实现的方式。原理很简单,我们先让一个线程工作,当工作完成之后唤醒另外的一线程,通过上面对RunLoop的说明,相信大家很容易能够理解这些代码:

  1. - (void)runLoopAddDependance{
  2. self.runLoopThreadDidFinishFlag = NO;
  3. NSLog(@"Start a New Run Loop Thread");
  4. NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];
  5. [runLoopThread start];
  6. NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside");
  7. dispatch_async(dispatch_get_global_queue(0, 0), ^{
  8. while (!_runLoopThreadDidFinishFlag) {
  9. self.myThread = [NSThread currentThread];
  10. NSLog(@"Begin RunLoop");
  11. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  12. NSPort *myPort = [NSPort port];
  13. [runLoop addPort:myPort forMode:NSDefaultRunLoopMode];
  14. [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  15. NSLog(@"End RunLoop");
  16. [self.myThread cancel];
  17. self.myThread = nil;
  18. }
  19. });
  20. }
  21. - (void)handleRunLoopThreadTask
  22. {
  23. NSLog(@"Enter Run Loop Thread");
  24. for (NSInteger i = 0; i < 5; i ++) {
  25. NSLog(@"In Run Loop Thread, count = %ld", i);
  26. sleep(1);
  27. }
  28. #if 0
  29. // 错误示范
  30. _runLoopThreadDidFinishFlag = YES;
  31. // 这个时候并不能执行线程完成之后的任务,因为Run Loop所在的线程并不知道runLoopThreadDidFinishFlag被重新赋值。Run Loop这个时候没有被任务事件源唤醒。
  32. // 正确的做法是使用 "selector"方法唤醒Run Loop。 即如下:
  33. #endif
  34. NSLog(@"Exit Normal Thread");
  35. [self performSelector:@selector(tryOnMyThread) onThread:self.myThread withObject:nil waitUntilDone:NO];
  36. // NSLog(@"Exit Run Loop Thread");
  37. }

七、NSURLConnection的执行过程

在使用NSURLConnection时,我们会传入一个Delegate,当我们调用了[connection start]之后,这个Delegate会不停的收到事件的回调。实际上,start这个函数的内部会获取CurrentRunloop,然后在其中的DefaultMode中添加4个source。如下图所示,CFMultiplexerSource是负责各种Delegate回调的,CFHTTPCookieStorage是处理各种Cookie的。如下图所示:

NSURLConnection的执行过程


从中可以看出,当开始网络传输是,我们可以看到NSURLConnection创建了两个新的线程:com.apple.NSURLConnectionLoader和com.apple.CFSocket.private。其中CFSocket是处理底层socket链接的。NSURLConnectionLoader这个线程内部会使用RunLoop来接收底层socket的事件,并通过之前添加的source,来通知(唤醒)上层的Delegate。这样我们就可以理解我们平时封装网络请求时候常见的下面逻辑了:

 

  1. while (!_isEndRequest)
  2. {
  3. NSLog(@"entered run loop");
  4. [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  5. }
  6. NSLog(@"main finished,task be removed");
  7. - (void)connectionDidFinishLoading:(NSURLConnection *)connection
  8. {
  9. _isEndRequest = YES;
  10. }

这里我们就可以解决下面这些疑问了:

  1. 为什么这个While循环不停的执行,还需要使用一个RunLoop? 程序执行一个while循环是不会耗费很大性能的,我们这里的目的是想让子线程在有任务的时候处理任务,没有任务的时候休眠,来节约CPU的开支。
  2. 如果没有为RunLoop添加item,那么它就会立即退出,这里的item呢? 其实系统已经给我们默认添加了4个source了。
  3. 既然[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];让线程在这里停下来,那么为什么这个循环会持续的执行呢?因为这个一直在处理任务,并且接受系统对这个Delegate的回调,也就是这个回调唤醒了这个线程,让它在这里循环。

八、AFNetWorking中是如何使用RunLoop的?

在AFN中AFURLConnectionOperation是基于NSURLConnection构建的,其希望能够在后台线程来接收Delegate的回调。
为此AFN创建了一个线程,然后在里面开启了一个RunLoop,然后添加item

  1. + (void)networkRequestThreadEntryPoint:(id)__unused object {
  2. @autoreleasepool {
  3. [[NSThread currentThread] setName:@"AFNetworking"];
  4. NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
  5. [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
  6. [runLoop run];
  7. }
  8. }
  9. + (NSThread *)networkRequestThread {
  10. static NSThread *_networkRequestThread = nil;
  11. static dispatch_once_t oncePredicate;
  12. dispatch_once(&oncePredicate, ^{
  13. _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
  14. [_networkRequestThread start];
  15. });
  16. return _networkRequestThread;
  17. }

这里这个NSMachPort的作用和上文中的一样,就是让线程不至于在很快死亡,然后RunLoop不至于退出(如果要使用这个MachPort的话,调用者需要持有这个NSMachPort,然后在外部线程通过这个port发送信息到这个loop内部,它这里没有这么做)。然后和上面的做法相似,在需要后台执行这个任务的时候,会通过调用:[NSObject performSelector:onThread:..]来将这个任务扔给后台线程的RunLoop中来执行。

  1. - (void)start {
  2. [self.lock lock];
  3. if ([self isCancelled]) {
  4. [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
  5. } else if ([self isReady]) {
  6. self.state = AFOperationExecutingState;
  7. [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
  8. }
  9. [self.lock unlock];
  10. }

GCD定时器的实现

  1. - (void)gcdTimer{
  2. // get the queue
  3. dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
  4. // creat timer
  5. self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
  6. // config the timer (starting time,interval)
  7. // set begining time
  8. dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
  9. // set the interval
  10. uint64_t interver = (uint64_t)(1.0 * NSEC_PER_SEC);
  11. dispatch_source_set_timer(self.timer, start, interver, 0.0);
  12. dispatch_source_set_event_handler(self.timer, ^{
  13. // the tarsk needed to be processed async
  14. dispatch_async(dispatch_get_global_queue(0, 0), ^{
  15. for (int i = 0; i < 100000; i++) {
  16. NSLog(@"gcdTimer");
  17. }
  18. });
  19. });
  20. dispatch_resume(self.timer);
  21. }

链接: https://blog.csdn.net/qq_22389025/article/details/85264178

收起阅读 »

看完 React 哲学,我悟了

前言 最近测试给我提的的 bug 终于少了很多, 在 codeReview 的时候同事们也很少指出我那个地方写的不对 反而对我整体的文件结构和组件的编写结构及状态的设计提出了更高的要求,不得不说我这代码水平还是有所提高的,表示在稳步提升的过程还有很大的进步空...
继续阅读 »

前言


最近测试给我提的的 bug 终于少了很多, 在 codeReview 的时候同事们也很少指出我那个地方写的不对


反而对我整体的文件结构和组件的编写结构及状态的设计提出了更高的要求,不得不说我这代码水平还是有所提高的,表示在稳步提升的过程还有很大的进步空间


但是当我在看到同事给我说的整个组件如何分离才能提高维护性和复用性,别人在看的时候也能更清晰的知道这部分的逻辑


当时我就好奇,为啥同事能有这种见解,难道只是因为经验比我多,思考比我深入吗?我的直觉告诉我没有这么简单。


如何在看到设计图的时候就想好如何划分这块业务的逻辑,如何设计自己想要的数据结构,我在脑中思索着,忽然我想起初学 React 的时候那个被我撇过一眼就速度滑过的概念 React 哲学


我立马去官网看着概念, 果然那些我以前我以前对于一个组件不知道证明设计结构和状态,这些组件写到后面状态不通,还有我几乎还会在每个子组件都请求一遍数据,对于一些想显示的数据都定义一个 state 来简单粗暴的解决


在写一个模块的时候我几乎是马上就着手去写,往往都是没有任何思考和设计只是想到什么就写什么,只想着赶快把功能学完,以至于写出很多缝缝补补不合理的代码让我踩过很多坑,收获很多 bug ,在看到之前的组件,我的第一想法就是重构。


说了这么多我的血泪史,我们看一下 React 哲学到底是说的什么,它都是如何解决我上述的痛点的,我又因此悟到了什么?


准备阶段


首先在我们写代码之前肯定会有的是会有的一是 PM 的产品设计图,二是后端同学返回的 JSON 数据


image-20210801203507483的副本.png


[
{category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
{category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
{category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
{category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
{category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
{category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];


先理解这样一个简单的产品设计图所包含的需求都有哪些,这是一个展示商品的列表,用户可以在对商品进行关键字搜索,并且通过点击复选框选择是是否展示现货,商品列表包含商品名和价格,商品支持分类显示,其中告罄的商品名为红色显示。


当我们基本了解产品图所表达的需求之后,就可以开始代码编写的第一步了


通过产品图划分组件层级


在一开始不太熟练划分的时候可以在产品设计稿上通过画方框来确定组件和子组件,可以报组件当成一个函数或者对象来看,组件同样遵照单一功能原则,也就是说一个组件只负责一个功能


同时一个好的 JSON 数据模型也应该是和组件一一对应的,组件与数据模型中的某个部分匹配


image-20210801205928730的副本.png


不同的颜色划分成不同的组件,可以分成五部分:



  1. FilterableProductTable (橙色): 是整个示例应用的整体

  2. SearchBar (蓝色): 接受所有的用户输入

  3. ProductTable (绿色): 展示数据内容并根据用户输入筛选结果

  4. ProductCategoryRow (天蓝色): 为每一个产品类别展示标题

  5. ProductRow (红色): 每一行展示一个产品


组件名应该能让人迅速 get 到这个组件的写的是什么(不得不说我之前的组件命名真的太糟糕了,过几天回头看都是一脸懵逼的那种


组件的层级划分:



  • FilterableProductTable

    • SearchBar

    • ProductTable

      • ProductCategoryRow

      • ProductRow






React 构建静态页面


当我们划分好了组件层级之后可以来写代码了,先利用已有数据模型来写一个不包含交互的UI渲染,这是因为UI渲染的代码比较多,交互要考虑的细节比较多,把这两个过程分开写不容易漏掉一些细节,整个思路也比较清晰


通过复用编写的组件,使用 props 来进行数据的传递,父组件把数据进行层层的传递,这也是 React 的一个特点就是单向数据流动,在这个过程中先不使用 state ,因为 state 表示的是会随着时间变化而变化的,所以在交互的过程中使用


构建应用的时候可以使用自上而下或者自上而下的方法,自上而下表示先写层级最高的组件,如FilterableProductTable 组件,这种比较适合简单的应用; 自下而上表示先写层级最低的组件,如 ProductRow 组件,这种方法比较适合大型的应用构建


const PRODUCTS = [
{category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
{category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
{category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
{category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
{category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
{category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

const FilterableProductTable = () => (
<div>
<SearchBar />
<ProductTable products={PRODUCTS} />
</div>
);

const SearchBar = () => (
<form>
<input type="text" placeholder="Search..." />
<p>
<input type="checkbox" /> Only show products in stock
</p>
</form>
);

const ProductTable = ({ products }) => {
const rows = [];
let lastCategory = null;

products.forEach((product) => {
if (product.category !== lastCategory) {
rows.push(
<ProductCategoryRow
category={product.category}
key={product.category}
/>
);
rows.push(<ProductRow product={product} key={product.name} />);
}
lastCategory = product.category;
});

return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
};

const ProductCategoryRow = ({ category }) => (
<tr>
<td colSpan="2">{category}</td>
</tr>
);

const ProductRow = ({ product }) => {
const name = product.stocked ? (
product.name
) : (
<span style={{ color: "red" }}>{product.name}</span>
);

return (
<tr>
<td>{name}</td>
<td>{product.price}</td>
</tr>
);
};


确定 state 的最小且完整的集合


当我们一些其他的数据来触发改变基础数据,让UI具有交互结果,在 React 中就可以使用 state 来表示


最小且完整的表示在于我们可以先找到一些会根据时间产生变化的全部数据,再从这些数据中选出最必要的数据作为 state ,其他数据能通过计算得到。


看一下当前应用有哪些数据:



  • 商品的原始数据

  • 用户的搜索数据

  • 复选框是否选中的值

  • 经过筛选后的数据


在确定这些数据能否成为 state 可以先问一下自己这几个问题



  • 数据是否能通过 props 来传递

  • 是否会通过时间而产生改变

  • 是否可以通过其他 stateprops 计算得到


那么最后我们就可以确认,原始数据可以通过 props 传递,用户搜索的数据和复选框的值可以作为 state ,筛选后的数据可以通过原始数据和用户搜索数据以及复选框数据计算得来。所以最后 state 可以是:



  • 用户的搜索数据

  • 复选框是否选中的值


确定 state 放置的位置


当确定了 state 的最小集合之后,接下来就该确定 state 应该放置在哪个组件里


在前面我们知道了 React 是单向的数据流,自上而下的流动,所以我们应把 state 写在共同所有者(也就是需要这些 state 的组件的共同父组件)


我们可以看到 ProductRow 组件需要筛选后的数据, SearchBar 组件需要搜索的数据和复选框的值, 所以就可以把 state 放在它们的共同所有者组件 FilterableProductTable 组件里,再通过 props 来进行 state 的传递


添加反向数据流


当我们要通过层级较低的组件改变层级较高的组件,就需要通过反向数据流的方式


React 中的反向数据流是通过需要高层级组件通过 props 把改变 state 的方法 (回调函数) 传递给层级较低的组件,子组件 state 的改变后的值传给这个回调函数。


在当前应用中如果想要拿到最新的 state 就需要FilterableProductTable 必须将一个能够触发 state 改变的回调函数(callback)传递给 SearchBar。我们可以使用输入框的 onChange 事件来监视用户输入的变化,并通知 FilterableProductTable 传递给 SearchBar 的回调函数。


const FilterableProductTable = () => {
const [filterText, setFilterText] = React.useState("");
const [inStockOnly, setInStockOnly] = React.useState(false);

return (
<div>
<SearchBar
filterText={filterText}
setFilterText={setFilterText}
inStockOnly={inStockOnly}
setInStockOnly={setInStockOnly}
/>
<ProductTable
products={PRODUCTS}
inStockOnly={inStockOnly}
filterText={filterText}
/>
</div>
);
};

const SearchBar = ({
filterText,
setFilterText,
inStockOnly,
setInStockOnly,
}) => {
const handleProductsSearch = (value) => {
setFilterText(value);
};

const handleStockCheck = (value) => {
setInStockOnly(value);
};

return (
<form>
<input
type="text"
placeholder="Search..."
value={filterText}
onChange={handleProductsSearch}
/>
<p>
<input
type="checkbox"
value={inStockOnly}
onChange={handleStockCheck}
/>{" "}
Only show products in stock
</p>
</form>
);
};

const ProductTable = ({ products, inStockOnly, filterText }) => {
const rows = [];
let lastCategory = null;

products.forEach((product) => {
if (product.name.indexOf(filterText) === -1) {
return;
}
if (inStockOnly && !product.stocked) {
return;
}
if (product.category !== lastCategory) {
rows.push(
<ProductCategoryRow
category={product.category}
key={product.category}
/>
);
rows.push(<ProductRow product={product} key={product.name} />);
}
lastCategory = product.category;
});

return (
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>{rows}</tbody>
</table>
);
};

总结


React 哲学并没有对深奥的道理,相反它更倡导我们把代码写得更加简洁清晰,更具有模块化,这一点在写大型的项目尤为重要,在写代码之前就把大致的结构和涉及的数据结构设计好,会减少 Bug 的产生,减少重构的时间,减少维护的成本。


链接:https://juejin.cn/post/6991650159935356935

收起阅读 »

淘宝详情页分发推荐算法总结:用户即时兴趣强化

商品详情页是手淘内流量最大的模块之一,它加载了数十亿级商品的详细信息,是用户整个决策过程必不可少的一环。这个区块不仅要承接用户对当前商品充分感知的诉求,同时也要能肩负起其他来源导流流量的留存,最终尽可能地激活平台内部流量以及外部流量在整个生态中的活跃度。同时,...
继续阅读 »

商品详情页是手淘内流量最大的模块之一,它加载了数十亿级商品的详细信息,是用户整个决策过程必不可少的一环。这个区块不仅要承接用户对当前商品充分感知的诉求,同时也要能肩负起其他来源导流流量的留存,最终尽可能地激活平台内部流量以及外部流量在整个生态中的活跃度。同时,商品详情页也是众多场景乃至平台链接的纽带,用户在平台中的行为轨迹总会在多场景和详情页间不断交替,并在详情页产生进一步的行为决策(加购/购买等)。因而详情页上除了具备承接用户的“了解更多”的诉求,也应同时满足平台“起承转合中间件”的诉求。


详情页内流量具备两个显著的特性:



  1. 流量大,常是用户购买决策环节;

  2. 承接了大量的外部引流。


出于这两个重要特性,同时也出于提升平台黏度,尽可能地提升用户行为的流畅度的产品设计考量,我们在详情页内部设立了一些全网分发场景,并基于这些场景特点进行了一些算法探索。


背景


信息爆炸导致用户对于海量信息的触达寥若晨星,对于有效信息的触达更是凤毛麟角。如果说社交媒体是无声者的发声者,那推荐系统俨然可以看作是海量信息的发声者,同时也是平台用户被曝光信息的制造者。所以我们有责任与义务做到推荐内容的保质与品控,这对于推荐系统是极大的诉求与挑战。当下的推荐系统通过深度挖掘用户行为,对用户进行个性化需求挖掘与实时兴趣捕捉,旨在于帮助用户在海量信息中快速,精准地定位,从而更好的完成智能化服务。


详情页的分发推荐肩负着【服务商家】,【提升用户使用体验】以及【利好平台分发效能】的重要责任。这给我们场景提出了三个方面不同侧重的需求,它们需要被统筹兼顾,以期能够打造出一个更好的流量分发阵地。我们解决这三个需求的方式是开辟同店商品推荐前置的全网分发模块,在极大程度保证商家权益的同时,让用户能够在一个聚焦的页面快速定位海量商品中“猜你喜欢”的商品。详情页内的推荐和公域推荐有一个最大的差异:每个详情页面都是主商品的信息衍生场,推荐内容受到它较强的约束。现有的大多数研究缺乏对具有先验信息的场景的探索:它们只强调用户的个性化兴趣。有一些重要的、直接相关的先验信息被直接忽略。我们观察到,在单个商品/主题唤醒的推荐页面上,用户的点击行为和主商品(唤醒推荐页面的商品/主题)是高度同质的。在这些场景下,用户已经通过主商品给模型传达了一个很聚焦很明确的意图,所以推荐的相关结果不能肆意泛化。但同时,一味的聚集又回降低分发的效能,使得用户在浏览过程中产生疲劳感。因而这些场景的推荐内容,应当遵循“意图明确,适度发散”的策略。当然,因为有主商品信息的加持,我们在模型调优时能够因地制宜地架构推荐策略,做出一些和其他场景相比,更明确更可解释的用户体验,这是我们写这篇文章的初衷。如果对这样的“以品推品”场景想要知道更多的细节,本篇文章将带您一起来看我们的探索问题——“用户即时兴趣强化与延伸”,以及模型解法和线上工程实践。


场景介绍


其中,全网流量分发场景主要包括详情页底部信息流(邻家好货),主图横滑(新增),加购弹层(新增)。这些场景打破了商家私域画地为牢的局面,充分地提升了私域全网分发的能效。当然为了兼顾商家利益,这些场景将分为两个部分(同店内容推荐模块和跨店内容推荐模块)。


image.png


image.png


技术探索


算法问题定义——即时兴趣强化


进入详情页是用户主动发起的行为,因而用户对于当前页面的主商品有着较强的兴趣聚焦。主商品的信息能够帮助我们快速地定位用户的即时兴趣,这对于推荐算法来说是至关重要的。虽然现在有很多方法将行为序列的末位替代即时兴趣,或是使用模型挖掘即时兴趣,但这些方法均是在不确定事件中进行推理,没有详情页天然带有主商品这样的强意图信息。基于此,我们的工作将从推荐技术的不同方面,将这部分信息建模并加以强化,以期使得详情页分发场景能够结合场景特点,尽可能地满足用户的即时需求。


召回


背景


随着深度学习技术在多个领域的普及以及向量检索技术的兴起,一系列基于类似思想的深度学习召回技术相继涌现。Youtube在2016年提出了DNN在推荐系统做召回的思路,它将用户历史行为和用户画像信息相结合,极大地提升了匹配范围的个性化和丰富性。我们的工作基于同组师兄的召回工作《SDM: 基于用户行为序列建模的深度召回》,《User-based Sequential Deep Match》 也是这一思路的一脉相承。SDM能够很好地建模用户兴趣的动态变化,并且能够综合长短期行为在不同维度进行用户表征,从而更好的使用低维向量表达用户和商品,最终借助大规模向量检索技术完成深度召回。SDM上线较base(多路i2i召回merge)ipv指标提升了2.80%。较SDM模型,CIDM模型IPV提升4.69%。在此基础上,为了契合详情页分发场景的特点,我们丰富并挖掘了主商品相关信息,并将其作为即时兴趣对召回模型进行结构改良。


模型——CIDM(Current Intention Reinforce Deep Match )


image.png


为了能够让模型SDM能够将主商品信息catch到并与用户行为产生交互,我们设计了如下的模型结构,其中trigger即为详情页中的主商品,我们从几个方面对它进行表征及强化:



  1. Trigger-Layer:启发于论文1,对主商品显式建模:除SDM中建模用户长、短期偏好之外,引入用户即时偏好层将主商品特征与长短期偏好融合作为用户最终表达;

  2. Trigger-Attention: 即将原模型中使用的self-attention改为由trigger作为目标的target-attention;

  3. Trigger-Lstm:借鉴论文2中的建模思路,我们将lstm的结构中引入了trigger信息,并添加trigger-gate让lstm倾向于记住更多关于主商品的内容;

  4. Trigger-filter-sequence:实验发现,使用主商品的叶子类目,一级类目过滤得到的序列作为原序列的补充进行召回建模,能够增加收益,故在数据源中添加了cate-filter-seq以及cat1-filter-sequece。


其中前两个点都是比较显而易见的,这里就不再赘述,我们将三四两个创新点详细阐述。


论文2中论证了添加时间门能够更好地捕捉用户的短期和长期兴趣,基于这个结论,我们尝试设计一个trigger-gate用于在模型捕获序列特征中引入trigger信息的影响。我们尝试了多种结构变体,比较work的两种方式(如图):



  1. 将trigger信息作为记忆门的一路输入,即通过sigmoid函数后与之前想要更新的信息相乘;

  2. 平行于第一个记忆门,添加一个新的即时兴趣门,其输入为细胞输入以及当前主商品,和记忆门结构一致。


这样的方式能够将主商品的信息保留的更充分。


image.png


第一种方法,仅是对记忆门进行了修改:


image.png


第二种方法,新加了一个即时兴趣门:


image.png


image.png


这两个实验在离线hr指标分别增长+1.07%. 1.37%,最优版本线上指标ipv+1.1%。


出于我们自己的实验结论:"使用主商品的叶子类目和一级类目过滤得到的序列作为原始序列的补充,作为模型输入能够提升预测准度“。这说明,主商品的结构信息是具有明显的效益的,以它为条件能够对序列样本产生正向约束。究其根本,原始序列中一些和当前主商品相关性较小的样本被过滤掉了,这相当于对数据进行去噪处理。沿着这个思路,联想到自编码机的主要应用为数据降噪与特征降维,故考虑采用基于AE结构的模型对序列进行处理,更多的,由于我们是定向去噪(即剔除与主商品不相关的行为),我们使用变分自编码机(VAE),借主商品信息在隐变量空间对序列表达进行约束,以确保隐层能较好抽象序列数据的特点。


变分自编码机是具有对偶结构(包括编码器和解码器)联合训练的系列模型,它借鉴变分推断的思路,在隐变量空间进行个性化定制,比较契合我们即使兴趣建模的需求。首先我们有一批数据样本图片,其似然分布可以表示为图片,最大化其对数似然时后验概率分布图片是不可知的,因而VAEs用自定义分布图片来近似真实的后验概率图片计算,使用KL散度作为两个分布的相似程度的度量。整体的优化函数可以表示为:


image.png


具体推导可以参见论文5。其中第一项作为使假设的后验分布图片和先验分布图片尽量接近,第二项为重构损失,保证自编码结构整体的稳定性。其中,先验分布图片是我们自定义的,这里想要将主商品的信息融入其中,因而我们假设图片,即使用主商品的表示作为高斯分布的均值,采样batch的二阶矩作为高斯分布的方差带入其中。因此,模型的优化函数变为:


image.png


启发于论文3、4, 我们将网络结构设计为如下形式,使用主商品的特征向量作为mu和sigma引入到变分自编码网络中,规范隐空间中序列特征的表达,并将学习得到的序列隐空间变量seq_hid作为用户的强意图序列表达trigger_emb,和长短期偏好融合。


image.png


这实验在离线hr指标增长+2.23%,线上未测试。


效果


较SDM模型,CIDM模型线上效果IPV提升4.69%。


精排


背景


精排模型基于DIN(Deep Interest Networks)进行探索与发展,我们的想法是在序列信息基础之上融入主商品更多的信息。序列信息挖掘和主商品信息强化其实是我们场景两个需求的外化,主商品信息强化能够很好地抓住用户即时意图,满足用户即时的聚焦需求;而序列信息挖掘是基于当前意图的延伸,能够一定程度上对意图进行发散,使推荐结果不会产生过于集中而带来体验疲劳。当然这两方面需要权衡,让模型识别其中“聚”,“散”的时机与程度。在此基础上,我们进行了1、挖掘主商品更多的语义信息;2、强化主商品信息对于序列特征抽取的指引与影响。


精排模型——DTIN(Deep Trigger-based Interest Network)


首先,我们希望能够挖掘主商品更多的语义信息,这一部分,我们将主商品(trigger)相关的特征和待打分商品(candidate)对齐,然后将这部分特征直接拼到模型的wide侧,让模型提升对于主商品表征的敏感度。


其次,由于DIN的motivation是引入注意力机制来更精准的捕获用户的兴趣点,作为比待打分商品更强的用户兴趣点体现,我们设计了一个双attention结构来强化这部分信息。如图所示,首先,将trigger和candidate商品特征concat,传入第一层attention结构中,学得第一层加权向量图片。这部分权值融合了trigger和candidate的信息,它可以被看作基于主商品及待打分商品交叉的用户兴趣提取。然后,仅使用主商品信息作为查询query传入第二层attention结构中,学得第二层加权向量图片,它可以被看作仅基于即时兴趣的延伸兴趣捕获。之后这两个权重向量按位相乘作为序列加权向量。模型结构设计这部分经历了大量的探索实验,如果有兴趣欢迎大家一起来讨论,这里只呈现我们实验中效果最佳版本。


image.png


效果


较DIN模型,DTIN模型IPV提升9.34%, 对应离线实验auc提升4.6%,gauc提升5.8%。


粗排


动机


粗排模型为的是解决推荐系统应用于工业界的特殊问题,在召回集合较大时,精排模型因复杂度太高而无法保证打分效率。因而粗排模型应运而生。由于详情页分发场景需要从全网亿级商品中进行商品召回,且召回阶段使用了多种召回方式的组合(包括i2i, 向量召回等)。这使得召回数量级较大,而且多路召回存在交叉使得匹配特征不在同一尺度上,这给后续的精排模型带来了较大的压力。基于此,我们开发了桥接召回和精排两部分的粗排模块,它的目标是对召回结果进行初筛,不仅需要兼顾效率与精度,也需要具有兼容多尺度召回方式的能力。基于我们的场景特点,在粗排初筛阶段进行了基于主商品的即时意图的建模。


模型——Tri-tower(Triple-tower Preparatory Ranking Framework)


出于粗排模型对于效率的要求不能构建过于复杂的结构,基于双塔粗排模型,我们针对强化即时兴趣的方向新添加了一个主商品塔trigger-tower,该塔和商品塔的特征保持一致,在顶端输出logits后和商品塔做交叉,作为之前双塔模型的补充添加在sigmoid函数的输入中。模型结构如下:


image.png


其中 Trigger net 和 Item net 使用 item 侧更轻量的一些统计类特征,User net也在deep match的基础上对大规模的id类特征进行了筛检。确保粗排模型轻量且服务快速。最终三塔粗排模型较无粗排模型,IPV指标提升3.96%。


总结


总体来看,详情页分发场景的优化思路比较统一,都是对主商品信息进行挖掘,并在模型中将用户历史行为进行关联加强。我们和传统的兴趣挖掘网络相比,附增了一道关口(即时兴趣强化),将那些明确的,和当前最相关的意图保留下来。通过这样的方式,推荐的结果就有一定程度的收敛。同时,多元兴趣在模型中并没有被完全抹去,只是通过attention网络动态调权来影响结果的发散程度,这也确保我们推荐结果一定的个性化和可发散性。


至此已阐述完“用户即时兴趣强化与延伸”课题在私域分发场景三个主要环节:召回-粗排-精排上面的有收益的尝试,当然这个过程也伴随着很多失败的探索,无论是模型优化和工程实践上的阻塞,都给我们带来了丰硕的实践经验。除了这三个主要模型外,我们在策略和其他环节的模型上也都针对该问题进行了优化,这里不再赘述。如果您对细节或者后续的优化方向感兴趣,欢迎与我们联系。


引用



  1. Tang, Jiaxi, et al. "Towards neural mixture recommender for long range dependent user sequences." The World Wide Web Conference. 2019.

  2. Zhu, Yu, et al. "What to Do Next: Modeling User Behaviors by Time-LSTM." IJCAI. Vol. 17. 2017

  3. Liang, Dawen, et al. "Variational autoencoders for collaborative filtering." Proceedings of the 2018 world wide web conference. 2018.

  4. Li, Xiaopeng, and James She. "Collaborative variational autoencoder for recommender systems." Proceedings of the 23rd ACM SIGKDD international conference on knowledge discovery and data mining. 2017.

  5. Zhao, Shengjia, Jiaming Song, and Stefano Ermon. "Towards deeper understanding of variational autoencoding models." arXiv preprint arXiv:1702.08658 (2017).


链接:https://juejin.cn/post/6992169847207493639

收起阅读 »

用three.js写一个3D地球

着色器的入门介绍 Webgl绘制图形是基于着色器(shader)的绘图机制,着色器提供了灵活且强大的绘制二维或三维图形的方法,所有Webgl程序必须使用它。 着色器语言类似于c语言,当我们写webgl程序时,着色器语言以字符串的形式嵌入在javascrip...
继续阅读 »

着色器的入门介绍



Webgl绘制图形是基于着色器(shader)的绘图机制,着色器提供了灵活且强大的绘制二维或三维图形的方法,所有Webgl程序必须使用它。



着色器语言类似于c语言,当我们写webgl程序时,着色器语言以字符串的形式嵌入在javascript语言中。


比如,要在屏幕上绘制一个点,代码如下:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
margin: 0
}
</style>
</head>
<body>
<canvas id="webgl"></canvas>
</body>
<script>
//将canvas的大小设置为屏幕大小
var canvas = document.getElementById('webgl')
canvas.height = window.innerHeight
canvas.width = window.innerWidth

//获取webgl绘图上下文
var gl = canvas.getContext('webgl')

//将背景色设置为黑色
gl.clearColor(0.0, 0.0, 0.0, 1.0)
gl.clear(gl.COLOR_BUFFER_BIT)

//顶点着色器代码(字符串形式)
var VSHADER_SOURCE =
`void main () {
gl_Position = vec4(0.5, 0.5, 0.0, 1.0); //点的位置:x: 0.5, y: 0.5, z: 0。齐次坐标
gl_PointSize = 10.0; //点的尺寸,非必须,默认是0
}`

//片元着色器代码(字符串形式)
var FSHADER_SOURCE =
`void main () {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); //点的颜色:四个量分别代表 rgba
}`

//初始化着色器
initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)

//绘制一个点,第一个参数为gl.POINTS
gl.drawArrays(gl.POINTS, 0, 1)

function initShaders(gl, vshader, fshader) {
var program = createProgram(gl, vshader, fshader);
if (!program) {
console.log('Failed to create program');
return false;
}
gl.useProgram(program);
gl.program = program;
return true;
}

function createProgram(gl, vshader, fshader) {
var vertexShader = loadShader(gl, gl.VERTEX_SHADER, vshader);
var fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fshader);
if (!vertexShader || !fragmentShader) {
return null;
}
var program = gl.createProgram();
if (!program) {
return null;
}
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
var linked = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linked) {
var error = gl.getProgramInfoLog(program);
console.log('Failed to link program: ' + error);
gl.deleteProgram(program);
gl.deleteShader(fragmentShader);
gl.deleteShader(vertexShader);
return null;
}
return program;
}

function loadShader(gl, type, source) {
// 创建着色器对象
var shader = gl.createShader(type);
if (shader == null) {
console.log('unable to create shader');
return null;
}
gl.shaderSource(shader, source);
gl.compileShader(shader);
var compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
if (!compiled) {
var error = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
return null;
}
return shader;
}
</script>
</html>

上面代码在屏幕右上区域绘制了一个点。


image.png


绘制这个点需要三个必要的信息:位置、尺寸和颜色。



  • 顶点着色器指定点的位置和尺寸。(下面的代码中,gl_Positiongl_PointSizegl_FragColor 都是着色器的内置全局变量。)


var VSHADER_SOURCE = 
`void main () {
gl_Position = vec4(0.5, 0.5, 0.0, 1.0); //指定点的位置
gl_PointSize = 10.0; //指定点的尺寸
}`


  • 片元着色器指定点的颜色。


var FSHADER_SOURCE = 
`void main () {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); //指定点的颜色
}`

attribute变量 和 uniform变量


上面的例子中,我们直接在着色器中指定了点的位置、尺寸和颜色。而实际操作中,这些信息基本都是由js传递给着色器。


用于 js代码 和 着色器代码 通信的变量是attribute变量uniform变量


使用哪一种变量取决于需要传递的数据本身,attribute变量用于传递与顶点相关的数据,uniform变量用于传递与顶点无关的数据。


下面的例子中,要绘制的点的坐标将由js传入。


  //顶点着色器
var VSHADER_SOURCE =
`attribute vec4 a_Position; //声明一个attribute变量a_Position,用于接受js传递的顶点位置
void main () {
gl_Position = a_Position; //将a_Position赋值给gl_Position
gl_PointSize = 10.0;
}`

//片元着色器
var FSHADER_SOURCE =
`void main () {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}`

initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)

//js代码中,获取a_Position的存储位置,并向其传递数据
var a_Position = gl.getAttribLocation(gl.program, 'a_Position')
gl.vertexAttrib3f(a_Position, 0.5, 0.5, 0.0)

gl.drawArrays(gl.POINTS, 0, 1)

varying变量


我们从js传给着色器的通常是顶点相关的数据,比如我们要绘制一个三角形,三角形的顶点位置和顶点颜色由js传入。三个顶点的位置可以确定三角形的位置,那么整个三角形的颜色由什么确定呢?


这就需要varying变量出场了。


webgl中的颜色计算:


顶点着色器中,接收js传入的每个顶点的位置和颜色数据。webgl系统会根据顶点的数据,插值计算出,顶点之间区域中,每个片元(可以理解为组成图像的最小渲染点)的颜色值。插值计算由webgl系统自动完成。


计算出的每个片元的颜色值,再传递给 片元着色器片元着色器根据每个片元的颜色值渲染出图像。


顶点着色器片元着色器,传递工作由varying变量完成。


image.png


代码如下。



  • 顶点着色器代码


var VSHADER_SOURCE = 
`attribute vec4 a_Position; //顶点位置
attribute vec4 a_Color; //顶点颜色
varying vec4 v_Color; //根据顶点颜色,计算出三角形中每个片元的颜色值,然后将每个片元的颜色值传递给片元着色器。
void main () {
gl_Position = a_Position;
v_Color = a_Color; // a_Color 赋值给 v_Color
}`


  • 片元着色器代码


var FSHADER_SOURCE = 
`precision mediump float;
varying vec4 v_Color; //每个片元的颜色值
void main () {
gl_FragColor = v_Color;
}`


  • js代码


var verticesColors = new Float32Array([     //顶点位置和颜色
0.0, 0.5, 1.0, 0.0, 0.0, // 第一个点,前两个是坐标(x,y; z默认是0),后三个是颜色
-0.5, -0.5, 0.0, 1.0, 0.0, // 第二个点
0.5, -0.5, 0.0, 0.0, 1.0 // 第三个点
])

//以下是通过缓冲区向顶点着色器传递顶点位置和颜色
var vertexColorBuffer = gl.createBuffer()

gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer)
gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW)

var FSIZE = verticesColors.BYTES_PER_ELEMENT
var a_Position = gl.getAttribLocation(gl.program, 'a_Position')
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 5, 0)
gl.enableVertexAttribArray(a_Position)

var a_Color = gl.getAttribLocation(gl.program, 'a_Color')
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 5, FSIZE * 2)
gl.enableVertexAttribArray(a_Color)

//绘制一个三角形,第一个参数为gl.TRIANGLES
gl.drawArrays(gl.TRIANGLES, 0, 3)

下面最终绘制出来的效果:


image.png


纹理映射的简单理解


在上面的例子中,我们是为每个顶点指定颜色值。


延伸一下,纹理映射是为每个顶点指定纹理坐标,然后webgl系统会根据顶点纹理坐标,插值计算出每个片元的纹理坐标。


然后在片元着色器中,会根据传入的纹理图像,以及每个片元的纹理坐标,取出纹理图像中对应纹理坐标上的颜色值(纹素),作为该片元的颜色值,并进行渲染。


纹理坐标的特点:



  • 纹理图像左下角为原点(0, 0)。

  • 向右为横轴正方向,横轴最大值为 1(图像右边缘)。

  • 向上为纵轴正方向,纵轴最大值为 1(图像上边缘)。


image.png
不管纹理图像的尺寸是多少,纹理坐标的范围都是: x轴:0-1,y轴:0-1


画一个3D地球


使用webgl进行绘制,步骤和API都比较繁琐,所幸我们可以借助three.js


three.js中的ShaderMaterial可以让我们自己定制着色器,直接操作像素。我们只需要理解着色器的基本原理。


开始画地球吧。


基础球体


基础球体的绘制比较简单,用three.js提供的材质就行。关于材质的基础,在 用three.js写一个反光球 有比较详细的介绍。


var loader = new THREE.TextureLoader() 
var group = new THREE.Group()

//创建本体
var geometry = new THREE.SphereGeometry(20,30,30) //创建球形几何体
var earthMaterial = new THREE.MeshPhongMaterial({ //创建材质
map: loader.load( './images/earth.png' ), //基础纹理
specularMap: loader.load('./images/specular.png'), //高光纹理,指定物体表面中哪部分比较闪亮,哪部分相对暗淡
normalMap: loader.load('./images/normal.png'), //法向纹理,创建更加细致的凹凸和褶皱
normalScale: new THREE.Vector2(3, 3)
})
var sphere = new THREE.Mesh(geometry, earthMaterial) //创建基础球体
group.add(sphere)

image.png


流动大气


使用ShaderMaterial自定义着色器。大气的流动,是通过每次在requestAnimationFrame渲染循环中改变纹理坐标实现。为了使流动更加自然,加入噪声。


//顶点着色器
var VSHADER_SOURCE = `
varying vec2 vUv;
void main () {
vUv = uv; //顶点纹理坐标
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4( position, 1.0 );
}
`

//片元着色器
var FSHADER_SOURCE = `
uniform float time; //时间变量
uniform sampler2D fTexture; //大气纹理图像
uniform sampler2D nTexture; //噪声纹理图像
varying vec2 vUv; //片元纹理坐标
void main () {
vec2 newUv= vUv + vec2( 0, 0.02 ) * time; //向量加法,根据时间变量计算新的纹理坐标

//利用噪声随机使纹理坐标随机化
vec4 noiseRGBA = texture2D( nTexture, newUv );
newUv.x += noiseRGBA.r * 0.2;
newUv.y += noiseRGBA.g * 0.2;

gl_FragColor = texture2D( fTexture, newUv ); //提取大气纹理图像的颜色值(纹素)
}
`

var flowTexture = loader.load('./images/flow.png')
flowTexture.wrapS = THREE.RepeatWrapping
flowTexture.wrapT = THREE.RepeatWrapping

var noiseTexture = loader.load('./images/noise.png')
noiseTexture.wrapS = THREE.RepeatWrapping
noiseTexture.wrapT = THREE.RepeatWrapping

//着色器材质
var flowMaterial = new THREE.ShaderMaterial({
uniforms: {
fTexture: {
value: flowTexture,
},
nTexture: {
value: noiseTexture,
},
time: {
value: 0.0
},
},
// 顶点着色器
vertexShader: VSHADER_SOURCE,
// 片元着色器
fragmentShader: FSHADER_SOURCE,
transparent: true
})
var fgeometry = new THREE.SphereGeometry(20.001,30,30) //创建比基础球体略大的球状几何体
var fsphere = new THREE.Mesh(fgeometry, flowMaterial) //创建大气球体
group.add(fsphere)
scene.add( group )

创建了group,基础球体和大气球体,都加入到group,作为一个整体,设置转动和位置,都直接修改group的属性。


var clock = new THREE.Clock()
//渲染循环
var animate = function () {
requestAnimationFrame(animate)
var delta = clock.getDelta()
group.rotation.y -= 0.002 //整体转动
flowMaterial.uniforms.time.value += delta //改变uniforms.time的值,用于片元着色器中的纹理坐标计算
renderer.render(scene, camera)
}

animate()

image.png


光晕


创建光晕用的是精灵(Sprite),精灵是一个总是面朝着摄像机的平面,这里用它来模拟光晕,不管球体怎么转动,都看上去始终处于光晕中。


var ringMaterial = new THREE.SpriteMaterial( {  //创建点精灵材质
map: loader.load('./images/ring.png')
} )
var sprite = new THREE.Sprite( ringMaterial ) //创建精灵,和普通物体的创建不一样
sprite.scale.set(53,53, 1) //设置精灵的尺寸
scene.add( sprite )

最终效果图:


earth-gif-l.gif


链接:https://juejin.cn/post/6992445067344478239

收起阅读 »

这种微前端设计思维听说过吗?

前言:最近有种感觉,好像微前端成为当下前端工程师的标配,从single-spa到qiankun,各种微前端架构解决方案层出不穷。那一夜,我在翻阅github时,留意到一个新的微前端框架,来自京东零售开源的MicroApp,号称无需像上面提到那两个框架一样需要对...
继续阅读 »

前言:最近有种感觉,好像微前端成为当下前端工程师的标配,从single-spa到qiankun,各种微前端架构解决方案层出不穷。那一夜,我在翻阅github时,留意到一个新的微前端框架,来自京东零售开源的MicroApp,号称无需像上面提到那两个框架一样需要对子应用的渲染逻辑调整,甚至还不用修改webpack配置。还有一个成功引起我注意的是:它把web-components的概念给用上了!让我们一探究竟!



1.饭后小菜 - Web Components 🍵


众所周知,Web Components 是一种原生实现可服用web组件的方案,你可以理解为类似在vue、React这类框架下开发的组件。不同的是,基于这个标准下开发的组件可以直接在html下使用,不用依赖其他第三方的库。



换句话说:部分现代浏览器提供的API使我们创建一个可复用的组件而无需依赖任何框架成为一种可能,不会被框架所限制



主要包括以下几个特征:



  • 使用custom elements自定义标签

  • 使用shadow DOM做样式隔离

  • 使用 templates and slots 实现组件拓展 (本期不拓展)


那 Web Components是如何创建一个组件的?我们来看下下面这个demo实践


1.1 实践



针对web components的实践, 我在github上找到一个demo。如下图所示,假设一个页面是由三个不同团队负责独立开发,A团队负责红色区域的整体展示功能,B团队和C团队分别负责蓝色和绿色区域(在红色区域内展示),那他们是怎么实现的?



image.png


我们以绿色区域的功能为示例,来看看demo的代码实例,本质上可以理解为定义一个组件green-recos


carbon (27).png


通过上图,我们来分析这段代码,主要包括以下几点信息:



  • 如何自定义元素?: 通过Api:window.customElements中的defind方法来定义注册好的实例

  • 如何定义一个组件实例?: 通过继承HTMLElement定义一个是实例类

  • 如何与外部通信的?:通过创建一个CustomEvent来自定义一个新的事件,然后通过addEventListener来监听以及element.dispatchEvent() 来分发事件

  • 如何控制组件的生命周期?: 主要是包括这几个生命周期函数,顺序如下 👇



constructor(元素初始化) -> attributeChangedCallback(当元素增加、删除、修改自身属性时,被调用) -> connectedCallback(当元素首次被插入文档DOM时,被调用) -> disconnectedCallback(当 custom element从文档DOM中删除时,被调用)`



拓展:



1.2 关于兼容性



👨‍🎓 啊乐同学:树酱,听说web component兼容性不太好?咋整?



image.png
你可以看上图👆 ,大部分浏览器新版本支持,如果想兼容旧版本,莫慌,可以通过引入polyfill来解决兼容问题 webcomponents/polyfills


你也可以通过坚挺WebComponentsReady这个事件来得知web components是否成功加载


1.3 关于样式冲突


关于样式,上面例子的样式是全局引用的,并没有解决样式冲突的问题,那如果想基于Web Components 开发组件,又担心各组件间存在样式冲突,这个时候你可以使用Shadow DOM来解决,有点类似vue中定义组件中的scoped处理



Shadow DOM: 也称影子DOM,它可以将一个隐藏的、独立的 DOM 附加到一个元素上。如下图MDN官方介绍图所示



image.png


那基于web component如何开发一个挂在#shadow-root的组件?


carbon (28).png


我们可以看到通过上图对比上一节的例子,多了attachShadow的方法使用。它是啥玩意?



官方介绍:通过attachShadow来将一个 shadow root 附加到任何一个元素上。它接受一个配置对象作为参数,该对象有一个 mode 属性。当mode为true,则表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM



🌲 扩展阅读:



1.4 注意细节



啊乐同学:树君,那我在vue中可以使用Web Component开发的自定义组件吗?



可以的,但是有一点要注意就是,Vue 组件开发很类似自定义元素,如果我们不做点“手段”处理,vue会把你基于Web Component开发的组件当作本身框架下的组件来看待,so 我们需要配置ignoredElements,下图是vue官网的示例


image.png


如果想了解更多关于Web Component的组件开发,可以看看下面这个开源的组件库



2 Mrcio-app



一不小心绕远了,言归正传,聊聊今日主角:micro-app



使用过qiankun的童鞋知道,我们要在基座集成一个微应用离不开下面👇 这三要素:



  • 在基座注册子应用

  • 需要在子应用定义好生命周期函数

  • 修改微应用的webpack打包方式


虽然改造成本不算特别高,但是能尽量降低对源代码的侵入性不香吗?


Mrcio-app 走的就是极简的路线,只要修改一丢丢代码就可以实现微应用的集成,号称是目前市面上接入微前端成本最低的方案。那它是如何做到的?


2.1 原理


本质上 micro-app 是基于类WebComponent + HTML Entry实现的微前端架构


image.png



官方介绍:通过自定义元素micro-app的生命周期函数connectedCallback监听元素被渲染,加载子应用的html并转换为DOM结构,递归查询所有js和css等静态资源并加载,设置元素隔离,拦截所有动态创建的script、link等标签,提取标签内容。将加载的js经过插件系统处理后放入沙箱中运行,对css资源进行样式隔离,最后将格式化后的元素放入micro-app中,最终将micro-app元素渲染为一个微前端的子应用。在渲染的过程中,会执行开发者绑定的生命周期函数,用于进一步操作。





  • 关于HTML Entry:相信用过qiankun 的童鞋应该都很熟悉,就是加载微应用的入口文件,一方面对微应用的静态资源js、CSS等文件进行fetch,一方面渲染微应用的dom




  • 类WebComponent: 我们在上一节学习web Component中了解到两个特征:CustomElementShadowDom,前者使得我们可以创建自定义标签,后者则促使我们可以创建支持隔离样式和元素隔离的阴影DOM。而首次提及的类WebComponent是个啥玩意?本质上就是通过使用CustomElement结合自定义的ShadowDom实现WebComponent基本一致的功能




换句话说:让微前端下微应用实现真正意义上的组件化


2.2 很赞的机制


micro-app 有这几个机制我觉得很赞:



  • 不用像qiankun一样在每个微应用都预先定义好生命周期函数,如:createdmounted等,而是另辟蹊径,当你在基座集成后,在基座可以直接定义,也可以进行全局监听。如下所示


carbon (29).png


上图的属性配置中name是微应用的名称配置,url是子应用页面地址配置,其他则是各个生命周期函数的定义



  • 资源地址自动补全:我们在基座加载微应用的时候,当微应用涉及图片或其他资源加载时,如果访问路径是相对地址,我们会发现会以基座应用所在域名地址补全静态资源,导致资源加载错误。而micro-app支持将子应用静态资源的相对地址补全为绝对地址,解决了上述的问题


image.png


2.3 实践


2.3.1 demo上手



上手也很简单,以vue2应用为例,具体参考 github文档。这里不做重复陈述



通过官方在线演示vue微应用Demo,我们来看看集成后的效果


image.png


在控制台我们可以看到,基座加载完微应用"vue2",在自定义标签micro-app渲染后就是一个完整子应用Dom,有点类似iframe的感觉,然后该子应用的css样式,都多了一个前缀 micro-app[name=vue2]。这是利用标签的name属性为每个样式添加前缀,将子应用的样式影响禁锢在当前标签区域,避免各个微应用之间的样式冲突。这是micro-app的默认隔离机制



啊乐同学:树酱,他这个元素隔离是怎么实现的?



你听我解释,看下一节源码分析


2.3.2 渲染微应用的过程


渲染微应用的过程主要流程图可以参照官方提供,主要包括以下流程


image.png



  • fetch 子应用HTMl: 获取html,然后转换为dom结构并递归处理每一个子元素,对不同元素做相应的处理 源码链接


目的是为了提取微应用的link和script,绑定style作用域。最后实现将微应用的style挂在micro-app-head中 核心源码如下
carbon (6).png


通过源码的阅读,当我们在微应用的初始化定义的app.scopecss配置时(默认开启),就会调用scopedCSS处理dom
,以此实现绑定微应用的css作用域,让我们看下这个方法的实现 源码链接


我在源码中看到scoped_css主要针对几种cssRule来做区分处理



啊恒同学:树酱,什么是Css Rule?



这是一个有历史的概念了,CSSRule 表示一条 CSS 规则。而一个 CSS 样式表包含了一组表示规则CSSRule对象。 CSSRule 有几种不同的规则类型,你可以在micro-app主要针对以下几种常规的cssRule区分处理



  • CSSRule.STYLE_RULE: 一般的style规则

  • CSSRule.MEDIA_RULE: CSS @media 媒体属性查询的规则

  • CSSRule.SUPPORTS_RULE: CSS @support 可以根据浏览器对CSS特性的支持情况来定义不同的样式的规则


carbon (7).png


最后将转化成功的style内容,append到micro-app-head中



啊恒同学:树酱,你说micro-app隔离元素支持shadowDom ?



是的,如果开启shadowDOM后,上面提到的默认的样式隔离将失效。 且兼容性会比较差


下面是个删减版:关于mircro-app通过Web Component + shadowDOM的实现子应用初始化的定义,具体的源码你可以阅读框架源码中关于micro_app_element的定义 源码链接
carbon (8).png


本质上开启shadowDom后,<micro-app>标签才算真正实现意义上的WebComponent



链接:https://juejin.cn/post/6992500880364797960

收起阅读 »

你可能不知道的动态组件玩法?

○ 背景 知道的大佬请轻锤😂。 这篇是作者在公司做了活动架构升级后,产出的主文的前导篇,考虑到本文相对独立,因此抽离出单独成文。 题目为动态组件,但为了好理解可以叫做远程加载动态组件,后面统一简化称为“远程组件”。 具体是怎么玩呢?别着急,听我慢慢道来,看...
继续阅读 »

○ 背景



知道的大佬请轻锤😂。


这篇是作者在公司做了活动架构升级后,产出的主文的前导篇,考虑到本文相对独立,因此抽离出单独成文。



题目为动态组件,但为了好理解可以叫做远程加载动态组件,后面统一简化称为“远程组件”。


具体是怎么玩呢?别着急,听我慢慢道来,看完后会感慨Vue组件还能这么玩🐶,还会学会一个Stylelint插件,配有DEMO,以及隐藏在最后的彩蛋。


作者曾所在我司广告事业部,广告承载方式是以刮刮卡、大转盘等活动页进行展示,然后用户参与出广告券弹层。


旁白说:远程组件其实在可视化低代码平台也有类似应用,而我们这里也是利用了类似思路实现解耦了活动页和券弹层。继续主题...


image.png


遗留系统早先版本是一个活动就绑定一个弹层,1对1的绑定关系。


image.png


现在的场景是一个活动可能出不同样式的弹层,这得把绑定关系解除。我们需要多对多,就是一个活动页面可以对应多个广告券弹层,也可以一个广告券弹层对应多个活动页面。


我们可以在本地预先写好几个弹层,根据条件选择不同的弹层,可以满足一个活动对多个弹层。


而我们的需求是让活动页面对应无数种弹层,而不是多种,所以不可能把所有弹层都写在本地。因此怎么办呢?


image.png


因此我们要根据所需,然后通过判断所需的弹层,远端返回对应的代码。其实就是我们主题要讲到的远程组件


讲得容易,该怎么做呢?


○ 远程组件核心


Pure版本


如果是Pure JS、CSS组成的弹层,很自然的我们想到,通过动态的插入JS脚本和CSS,就能组成一个弹层。因此把编译好的JS、CSS文件可以存放在远端CDN。


image.png


看上图,我们可以看到弹窗出来之前,浏览器把CSS、JS下载下来了,然后根据既定代码拼装成一个弹层。


// CSS插入
<link rel="stylesheet" href="//yun.xxx.com/xxx.css">

// JS的动态插入

<script type="text/javascript">
var oHead = document.querySelector('.modal-group');
var oScript = document.createElement('script');
oScript.type = "text/javascript";
oScript.src = "//yun.xxx.com/xxx.js";
oHead.appendChild(oScript);
</script>

通过上面可知,JS、CSS方式能实现Pure版本的远程组件,而在Vue环境下能实现吗。如果按照Pure JS、CSS动态插入到Vue活动下,也是可以很粗糙的实现的。


但有没有更优雅的方式呢?


image.png


Vue版本



选型这篇不细讨论了,后续的主篇会讲为什么选择Vue。



上述是遗留系统的方式,如果我们要技术栈迁移到Vue,也需要对远程组件迁移,我们需要改造它。


让我们来回顾下Vue的一些概念。


组件形式


「对象组件」


一个弹窗,其实我们可以通过一个Vue组件表示,我们想把这个组件放到CDN,直接下载这个文件,然后在浏览器环境运行它可行吗?我们来尝试下。


基于Vue官方文档,我们可以把如下的选项对象传入Vue,通过new Vue来创建一个组件。


{
mounted: () => {
console.log('加载')
},
template: "<div v-bind:style=\"{ color: 'red', fontSize: '12' + 'px' }\">Home component</div>"
}

借助于包含编译器的运行时版本,我们可以处理字符串形式的Template。



-- 运行时-编译器-vs-只包含运行时




如果你需要在客户端编译模板 (比如传入一个字符串给Template选项,或挂载到一个元素上并以其 DOM 内部的 HTML 作为模板),就将需要加上编译器,即完整版



似乎找到了新世界的大门。


image.png


我们确实是可以通过这种形式实现Template、Script、CSS了,但对于开发同学,字符串形式的Template、内嵌的CSS,开发体验不友好。


image.png


「单文件组件」


这个时候很自然地想到SFC - 单文件组件。



文件扩展名为.vue的**single-file components (单文件组件)**为以上所有问题提供了解决方法 -- Vue文档。




image.png




但怎么样才能让一个.vue组件从远端下载下来,然后在当前活动Vue环境下运行呢?这是个问题,由于.vue文件浏览器是识别不了的,但.js文件是可以的。


我们先想一下,.vue文件是最终被转换成了什么?


image.png
(图片来源:1.03-vue文件的转换 - 简书


通过转换,实际变成了一个JS对象。所以怎么才能把.vue转换成.js呢?


有两种方式,一种通过运行时转换,我们找到了http-vue-loader。通过Ajax获取内容,解析Template、CSS、Script,输出一个JS对象。


image.png


而考虑到性能和兼容性,我们选择预编译,通过CSS预处理器、HTML模版预编译器。


Vue的官方提供了vue-loader,它会解析文件,提取每个语言块,如有必要会通过其它 loader 处理,最后将他们组装成一个 ES Module,它的默认导出是一个 Vue.js 组件选项的对象。这指的是什么意思呢?官方提供选项对象形式的组件DEMO。


有了理论支持,现在需要考虑下实践啦,用什么编译?


image.png


怎么构建


由于webpack编译后会带了很多关于模块化相关的无用代码,所以一般小型的库会选择rollup,这里我们也选择rollup。


// rollup.config.js
import vue from 'rollup-plugin-vue'
import commonjs from 'rollup-plugin-commonjs'

export default {
input: './skin/SkinDemo.vue',
output: {
format: 'iife',
file: './dist/rollup.js',
name: 'MyComponent'
},
plugins: [
commonjs(),
vue()
]
}

通过rollup-plugin-vue,我们可以把.vue文件转成.js,
rollup编译输出的iife形式js。


image.png


可以看到script、style、template分别被处理成对应的片段,通过整合计算,这些片段会生成一个JS对象,保存为.js文件。下图就是一个组件选项的对象。


image.png


可以通过项目:github.com/fly0o0/remo…,尝试下rollup文件夹下的构建,具体看README说明。


我们已经有了一个 Vue.js 组件选项的对象,怎么去让它挂载到对应的Vue App上呢?


image.png


挂载方式


回想之前通读Vue入门文档,遇到一个动态组件的概念,但当时并不太理解它的使用场景。
image.png


动态组件是可以不固定具体的组件,根据规则替换不同的组件。从文档上看出,支持一个组件的选项对象。


最终实现


首先需要构建.vue文件,然后通过Ajax或动态Script去加载远端JS。由于Ajax会有跨域限制,所以这里我们选择动态Script形式去加载。


而我们刚才使用Rollup导出的方式是把内容挂载在一个全局变量上。那就知道了,通过动态Script插入后,就有一个全局变量MyComponent,把它挂载在动态组件,最终就能把组件显示在页面上了。


具体怎么操作?欠缺哪些步骤,首先我们需要一个加载远程.js组件的函数。


// 加载远程组件js

function cleanup(script){
if (script.parentNode) script.parentNode.removeChild(script)
script.onload = null
script.onerror = null
script = null
}

function scriptLoad(url) {
const target = document.getElementsByTagName('script')[0] || document.head

let script = document.createElement('script')
script.src = url
target.parentNode.insertBefore(script, target)

return new Promise((resolve, reject) => {
script.onload = function () {
resolve()
cleanup(script)
}
script.onerror = function () {
reject(new Error('script load failed'))
cleanup(script)
}
})
}

export default scriptLoad

然后把加载下来的组件,挂载在对应的动态组件上。


<!-- 挂载远程组件 -->

<template>
<component

:is="mode">
</component>
</template>

<script>
import scriptLoad from "./scriptLoad"

export default {
name: "Remote",
data() {
return {
mode: "",
};
},
mounted() {
this.mountCom(this.url)
},
methods: {
async mountCom(url) {
// 下载远程js
await scriptLoad(url)

// 挂载在mode
this.mode = window.MyComponent

// 清除MyComponent
window.MyComponent = null
},
}
}
</script>

基本一个Vue的远程组件就实现了,但发现还存在一个问题。


image.png


全局变量MyComponent需要约定好,但要实现比较好的开发体验来说,应该尽量减少约定。


导出方式


怎么解决呢?由于我们导出是使用的IIFE方式,其实Rollup还支持UMD方式,包含了Common JS和AMD两种方式。


我们通过配置Rollup支持UMD。


// rollup.config.js
import vue from 'rollup-plugin-vue'
import commonjs from 'rollup-plugin-commonjs'

export default {
input: './skin/SkinDemo.vue',
output: {
format: 'umd',
file: './dist/rollup.js',
name: 'MyComponent'
},
plugins: [
commonjs(),
vue()
]
}

可以看到构建完毕后,支持三种方式导出。
image.png


我们可以模拟node环境,命名全局变量exports、module,就可以在module.exports变量上拿到导出的组件。
image.png


具体实现核心代码如下。


<!-- 挂载远程组件 -->

<template>
<component

:is="mode">
</component>
</template>

<script>
import scriptLoad from "./scriptLoad"

export default {
name: "Remote",
data() {
return {
mode: "",
};
},
mounted() {
this.mountCom(this.url)
},
methods: {
async mountCom(url) {
// 模拟node环境
window.module = {}
window.exports = {}

// 下载远程js
await scriptLoad(url)

// 挂载在mode
this.mode = window.module.exports

// 清除
delete window.module
delete window.exports
},
}
}
</script>

终于搞定了Vue版本的远程组件加载的方式。


image.png


接下来得想一想,怎么处理远程组件(弹层)的设计了。


小结


通过使用Vue动态组件实现了远程组件功能,取代了老架构。image.png


可以通过以下地址去尝试一下远程组件弹层,按照项目的README操作一下。会得到以下远程组件弹层。


项目地址:github.com/fly0o0/remo…


image.png


○ 远程组件(弹层)设计



远程组件已达成,这部分主要是对远程弹层组件的一些设计。



对于远程单组件本身来说,只需要根据数据渲染视图,根据用户行为触发业务逻辑,整个代码逻辑是这样的。


需要考虑组件复用、组件通讯、组件封装、样式层级等方向。


首先我们先看看组件复用。


为了方便统一管理和减少冗余代码,我们一般写一些类似的组件会抽取一部分可以公共的组件,例如按钮等。


但远程单组件代码和页面端代码是分离的啊(可以理解为两个webpack入口打包出的产物),我们得想想公共组件需要放在哪里了。


image.png


组件复用


现在可以发现有三种情况,我们利用枚举法尝试想一遍。


打包 📦


公共组件和远程组件打包一起


放在一起肯定不合适,不仅会引起远程组件变大,还不能让其他远程组件复用。往下考虑再看看。


image.png


公共组件单独打包


远程组件、公共组件分别单独打包,这样也是不利的,由于远程组件抽离的公共组件少于5个,而且代码量较少,单独作为一层打包,会多一个后置请求,影响远程组件的第一时间展示。


继续考虑再看看。
image.png
公共组件和页面核心库打包一起


把公共组件和页面核心库打包到一起,避免后面远程组件用到时候再加载,可以提升远程组件的展示速度。
image.png


因此最终敲定选择最后种,把公共组件和页面核心库打包在一起。


如果把远程组件.js和公共组件分开了,那我们该怎么才能使用公共组件啊?😂


image.png


注册 🔑


回顾下Vue官方文档,Vue.component它可以提供注册组件的能力,然后在全局能引用到。我们来试试吧。


公共组件例如按钮、关闭等,需要通过以下途径去注册。


一个按钮组件


// 本地页面端(本地是相较于在远端CDN)

<!-- 按钮组件 -->
<template>
<button type="button" @click="use">
</button>
</template>

<script>
export default {
name: 'Button',
inject: ['couponUseCallback'],
methods: {
use() {
this.couponUseCallback && this.couponUseCallback()
}
}
}
</script>

一个关闭组件


// 本地页面端(本地是相较于在远端CDN)

<!-- 关闭组件 -->
<template>
<span @click="close"></span>
</template>

<script>
export default {
name: "CouponClose",
inject: ["couponCloseCallback"],
methods: {
close() {
this.couponCloseCallback && this.couponCloseCallback();
},
},
};
</script>

<style lang="less" scoped>
.close {
&.gg {
background-image: url("//yun.tuisnake.com/h5-mami/dist/close-gg.png") !important;
background-size: 100% !important;
width: 92px !important;
height: 60px !important;
}
}
</style>

通过Vue.component全局注册公共组件,这样在远程组件中我们就可以直接调用了。


// 本地页面端(本地是相较于在远端CDN)

<script>
Vue.component("CpButton", Button);
Vue.component("CpClose", Close);
</script>

解决了公共组件复用的问题,后面需要考虑下远程组件和页面容器,还有不同类型的远程组件之间的通讯问题。


image.png


组件通讯


可以把页面容器理解为父亲、远程组件理解为儿子,两者存在父子组件跨级双向通讯,这里的父子也包含了爷孙和爷爷孙的情况,因此非props可以支持。那怎么处理?


可以通过在页面核心库中向远程组件 provide 自身,远程组件中 inject 活动实例,实现事件的触发及回调。


那不同类型的远程组件之间怎么办呢,使用Event Bus,可以利用顶层页面实例作为事件中心,利用 on 和 emit 进行沟通,降低不同类别远程组件之间的耦合度。


image.png


组件封装


现在有个组件封装的问题,先看个例子,基本就大概有了解了。


现有3个嵌套组件,如下图。** **现在需要从顶层组件Main.vue给底层组件RealComponent的一个count赋值,然后监听RealComponent的input组件的事件,如果有改变通知Main.vue里的方法。怎么做呢?


image.png


跨层级通信,有多少种方案可以选择?



  1. 我们使用vuex来进行数据管理,对于这个需求过重。

  2. 自定义vue bus事件总线(如上面提到的),无明显依赖关系的消息传递,如果传递组件所需的props不太合适。

  3. 通过props一层一层传递,但需要传递的事件和属性较多,增加维护成本。


而还有一种方式可以通过attrsattrs和listeners,实现跨层级属性和事件“透传”。


主组件


// Main.vue

<template>
<div>
<h2>组件Main 数据项:{{count}}</h2>
<ComponentWrapper @changeCount="changeCount" :count="count">
</ComponentWrapper>
</div>
</template>
<script>
import ComponentWrapper from "./ComponentWrapper";
export default {
data() {
return {
count: 100
};
},
components: {
ComponentWrapper
},
methods: {
changeCount(val) {
console.log('Top count', val)
this.count = val;
}
}
};
</script>

包装用的组件


有的时候我们为了对真实组件进行一些功能增加,这时候就需要用到包装组件(特别是对第三方组件库进行封装的时候)。


// ComponentWrapper.vue

<template>
<div>
<h3>组件包裹层</h3>
<RealComponent v-bind="$attrs" v-on="$listeners"></RealComponent>
</div>
</template>
<script>
import RealComponent from "./RealComponent";
export default {
inheritAttrs: false, // 默认就是true
components: {
RealComponent
}
};
</script>

真正的组件


// RealComponent.vue

<template>
<div>
<h3>真实组件</h3>
<input v-model="myCount" @input="inputHanlder" />
</div>
</template>
<script>
export default {
data() {
return {
myCount: 0
}
},
created() {
this.myCount = this.$attrs.count; // 在组件Main中传递过来的属性
console.info(this.$attrs, this.$listeners);
},
methods: {
inputHanlder() {
console.log('Bottom count', this.myCount)
this.$emit("changeCount", this.myCount); // 在组件Main中传递过来的事件,通过emit调用顶层的事件
// this.$listeners.changeCount(this.myCount) // 或者通过回调的方式
}
}
};
</script>

从例子中回归本文里来,我们要面对的场景是如下这样。


远程组件其实有两层,一层是本地(页面内),一层是远端(CDN)。本地这层只是做封装用的,可以理解为只是包装了一层,没有实际功能。这时候可以理解为本地这一层组件就是包装层,包装层主要做了导入远程组件的功能没办法去除,需要利用上面的特性去传递信息给远程组件。


样式层级


远程组件在本文可以简单理解为远端的弹层组件,公司业务又涉及到不同的弹层类别,每种弹层类别可能会重叠。


约定z-index


因此划分 0~90 为划分十层,后续可根据实际情况增加数值,设定各远程组件容器只能在规定层级内指定 z-index。


// const.js
const FLOOR = {
MAIN: 0, // 主页面容器
COUPON_MODAL: 20, // 广告弹层
OTHER_MODAL: 30, // 其他弹层
ERROR_MODAL: 90,
...
}

设置每种远程组件即弹层的包裹层。



// CouponModalWrapper.vue
<script>
<template>
<div :style="{'z-index': FLOOR.COUPON_MODAL}" @touchmove.prevent>
<slot></slot>
</div>
</template>

// OtherModalWrapper.vue
<template>
<div :style="{'z-index': FLOOR.OTHER_MODAL}" @touchmove.prevent>
<slot></slot>
</div>
</template>

// 这里只是为了表意简单,实际上两个Wrapper.vue可以合并

然后每类别各自引入对应的弹层包裹层。


// 每类别公共组件有一个

// CouponModal2.vue
<template>
<CouponModalWrapper>
...
</CouponModalWrapper>
</template>

// OtherModal2.vue
<template>
<OtherModalWrapper>
...
</OtherModalWrapper>
</template>

通过这种约定的方式,可以避免一些问题,但假如真的有人想捣乱怎么办?


image.png


别着急,有办法的。


借助stylelint


思路是这样的,每类别的远程组件是单独有对应的主文件夹,可以为这个文件夹定义最高和最小可允许的z-index,那该怎么做呢?


不知道大家有使用过自动加-webkit等前缀的插件 - autoprefixer没有,它其实是基于一款postcss工具做的。而我们经常用作css校验格式的工具stylelint也是基于它开发的。


这时候我们想到,能不能通过stylelint的能力,进行约束呢,我们发现找了官方文档并没有我们想要的API。


我们需要自己开发一个stylelint插件,来看看一个基本的stylelint插件的插件。


image.png


stylelint通过stylelint.createPlugin方法,接受一个函数,返回一个函数。


const stylelint = require('stylelint');
const ruleName = 'plugin/z-index-range-plugin';

function rule(options) {
// options传入的配置
return (cssRoot, result) => {
// cssRoot即为postcss对象
};
}

module.exports = stylelint.createPlugin(
ruleName,
rule
);

函数中可以拿到PostCSS对象,可以利用PostCSS对代码进行解析成AST、遍历、修改、AST变代码等操作。


有一些我们可用的概念。



  • rule,选择器,比如.class { z-index: 99 }。

  • decl,属性,比如z-index: 99。


我们需要检查z-index的值,因此需要遍历CSS检查z-index。我们可以调用cssRoot.walkDecls对做遍历:


// 遍历
cssRoot.walkDecls((decl) => {
// 获取属性定义
if (decl) {
// ...
}
});

前置基础知识差不多够用了。


image.png


假如我们要检测一个两个文件夹下的.css文件的z-index是否合乎规矩。


我们设置好两个模块stylelint配置文件下的z-index范围。


这里我们可以看到stylelint配置文件,两个css文件。


├── .stylelintrc.js
├── module1
│ └── index.css
├── module2
│ └── index2.css

stylelint配置文件


// .stylelintrc.js
module.exports = {
"extends": "stylelint-config-standard",
// 自定义插件
"plugins": ["./plugin.js"],
"rules": {
// 自定义插件的规则
"plugin/z-index-range-plugin": {
// 设置的范围,保证各模块不重复
"module1": [100, 199],
"module2": [200, 299]
}
}
}

CSS测试文件


/* module1/index.css */
.classA {
color: red;
width: 99px;
height: 100px;
z-index: 99;
}

/* module2/index.css */
.classB {
color: red;
width: 99px;
height: 100px;
z-index: 200;
}


我们要达到的目的是,运行如下命令,会让module1/index.css报错,说z-index小于预期。


npx stylelint "*/index.css"

于是乎我们完成了如下代码,达成了预期目的。


const stylelint = require('stylelint');
const ruleName = 'plugin/z-index-range-plugin';

function ruleFn(options) {
return function (cssRoot, result) {

cssRoot.walkDecls('z-index', function (decl) {
// 遍历路径
const path = decl.source.input.file
// 提取文件路径里的模块信息
const match = path.match(/module\d/)
// 获取文件夹
const folder = match?.[0]
// 获取z-index的值
const value = Number(decl.value);
// 获取设定的最大值、最小值
const params = {
min: options?.[folder]?.[0],
max: options?.[folder]?.[1],
}

if (params.max && Math.abs(value) > params.max) {
// 调用 stylelint 提供的report方法给出报错提示
stylelint.utils.report({
ruleName,
result,
node: decl,
message: `Expected z-index to have maximum value of ${params.max}.`
});
}

if (params.min && Math.abs(value) < params.min) {
// 调用 stylelint 提供的report方法给出报错提示
stylelint.utils.report({
ruleName,
result,
node: decl,
message: `Expected z-index to have minimum value of ${params.min}.`
});
}
});
};
}

module.exports = stylelint.createPlugin(
ruleName,
ruleFn
);

module.exports.ruleName = ruleName;

可以尝试项目:github.com/fly0o0/styl…,试一试感受一下🐶。


这样基本一个远程弹层的设计就完成了。


但还是遇到了些问题,艰难😂。


image.png


○ 遇到的问题


我们兴冲冲的打算发上线了,结果报错了🐶。报的错是webpackJsonp不是一个function。


不要慌,先吃个瓜镇静镇静。webpackJsonp是做什么的呢?


异步加载的例子


先看下以下例子,通过import的按需异步加载特性加载了test.js,以下例子基于Webpack3构建。


// 异步加载 test.js
import('./test').then((say) => {
say();
});

然后生成了异步加载文件 0.bundle.js。


// 异步加载的文件,0.bundle.js
webpackJsonp(
// 在其它文件中存放着的模块的 ID
[0],
// 本文件所包含的模块
[
// test.js 所对应的模块
(function (module, exports) {
function ;(content) {
console.log('i am test')
}

module.exports = say;
})
]
);

和执行入口文件 bundle.js。


// 执行入口文件,bundle.js
(function (modules) {
/***
* webpackJsonp 用于从异步加载的文件中安装模块。
*
*/
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
var moduleId, chunkId, i = 0, resolves = [], result;
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (installedChunks[chunkId]) {
resolves.push(installedChunks[chunkId][0]);
}
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
while (resolves.length) {
resolves.shift()();
}
};

// 模拟 require 语句
function __webpack_require__(moduleId) {
}

/**
* 用于加载被分割出去的,需要异步加载的 Chunk 对应的文件
*/
__webpack_require__.e = function requireEnsure(chunkId) {
// ... 省略代码
return promise;
};

return __webpack_require__(__webpack_require__.s = 0);
})
(
[
// main.js 对应的模块
(function (module, exports, __webpack_require__) {
// 通过 __webpack_require__.e 去异步加载 show.js 对应的 Chunk
__webpack_require__.e(0).then(__webpack_require__.bind(null, 1)).then((show) => {
// 执行 show 函数
show('Webpack');
});
})
]
);

可以看出webpackJsonp的作用是加载异步模块文件。但为什么会报webpackJsonp不是一个函数呢?


开始排查问题


我们开始检查构建出的源码,发现我们的webpackJsonp并不是一个函数,而是一个数组(现已知Webpack4,当时排查时候不知道)。


我们发现异步文件加载的时候确实是变成了数组,通过push去增加一个异步模块到系统里。


// 异步加载的文件

(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[/* chunk id */ 0], {
"./src/async.js": (function(module, __webpack_exports__, __webpack_require__) {

//...

}))

且在执行入口文件也发现了webpackJsonp被定义为了数组。


// 执行入口文件,bundle.js中的核心代码  

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;

确实我们构建出的源码的webpackJsonp是一个数组,确实不是一个函数了,感觉找到了一点线索。但为什么会webpackJsonp会函数形式去使用呢?


我们怀疑报错处有问题,开始排查报错处,发现对应的文件确实是用webpackJsonp当作函数去调用的,这是什么情况?🤔️


这时我们注意到报错的都是老架构下的远程组件,是不是在老架构的项目里会有什么蛛丝马迹?


我们开始探索老架构,这时候发现老架构是使用的webpack3,而我们新架构是使用webpack4构建的。难道是这里出了问题?💡


于是我们用webpack3重新构建了下老架构的远程组件,发现webpackJsonp对应的确实是函数,如上一节“异步加载的例子”里所示。


所以定位到了原因,webpack4和webpack3分别构建了新老两种的异步远程组件,webpackJsonp在版本4下是数组,而在版本3下面是函数。


image.png


细心的同学可能已经发现上面的图在之前出现过,webpack4构建的入口文件去加载webpack3构建的异步组件,就出现了章节头出现的webpackJsonp不是函数的错误。


image.png


好好想一想,大概有几个方案。



  1. 批量去修改webpack3构建出来的异步组件中webpackJsonp的命名,然后在容器页面入口里自定义异步加载能力(webpackJsonp功能)的函数。

  2. 重新去用webpack4构建所有遗留的老架构webpack3构建出来的异步组件。

  3. 搜寻是否有官方支持,毕竟这是一个webpack4从webpack3的过来的breack changes。


第一个方案工作量有点大,且怎么保证异步组件和入口文件同步修改完毕呢?
第二个方案工作量也很大,对于所有老架构的异步组件都得更新,且更新后的可靠性堪忧,万一有遗漏。
第三个方案看起来是最靠谱的。


image.png


于是在第三个方案的方向下,开始做了搜寻。


我们通过webpack4源码全局搜寻webpackJsonp,发现了jsonpFunction。通过官方文档找到了jsonpFunction是可以自定义webpack4的webpackJsonp的名称。比如可以改成如下。


output: {
// 自定义名称
jsonpFunction: 'webpack4JsonpIsArray'
},

这样后,webpackJsonp就不是一个数组了,而是未定义了。因此我们需要在我们的公共代码库里提供webpackJsonp函数版本的定义。如异步加载的例子小节所提到的。


// webpackJsonp函数版
!(function (n) {
window.webpackJsonp = function (t, u, i) {
//...
}
}([]))

以此来提供入口页面能加载webpack3构建的异步文件的能力。


○ 演进



我们还对远程组件弹层做了一些演进,由于跟本文关联度不大,只做一些简单介绍。



图片压缩问题


券弹层的券有PNG、JPG、GIF格式,需要更快的展现速度,因此我们做了图片压缩的统一服务。


image.png


gif处理策略:github.com/kornelski/g…
png处理策略:pngquant.org


效率问题


有规律的远程组件,可通过搭建工具处理,因此我们构建了可视化低代码建站工具,有感兴趣的同学留言,我考虑写一篇😂 。


image.png



链接:https://juejin.cn/post/6992483283187531789

收起阅读 »

AFNetWorking为何在发起请求时要通过runloop!OC 中常用关键字的区别!

最近几天经历了多场面试,由于简历上写了runloop,跟AFNetworing的字眼。面试官好像特别喜欢问这个问题。一连几场都遇到。可惜平时开发过程中,知识的累计跟沉淀不足。都不能回答的很好..趁着现在有时间。查阅一下资料 在这里进行一个总结。。Questio...
继续阅读 »

最近几天经历了多场面试,由于简历上写了runloop,跟AFNetworing的字眼。面试官好像特别喜欢问这个问题。一连几场都遇到。

可惜平时开发过程中,知识的累计跟沉淀不足。都不能回答的很好..


趁着现在有时间。查阅一下资料 在这里进行一个总结。。

Question:AFNetworking 2.x怎么开启常驻子线程?为何需要常驻子线程?对应以下代码:


+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}

+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});

return _networkRequestThread;
}

首先,我们要了解为何要开启常驻子线程?

NSURLConnection的接口是异步的,然后会在发起的线程回调。而一个子线程,在同步代码执行完成之后,一般情况下,线程就退出了。那么想要接收到NSURLConnection的回调,就必须让子线程至少存活到回调的时机。而AF让线程常驻的原因是,当发起多个http请求的时候,会统一在这个子线程进行回调的处理,所以干脆就让其一直存活下来。

上面说的一般情况,子线程执行完任务就会退出,那么什么情况下,子线程能够继续存活呢?这就涉及到第二个问题了,AF是如何开启常驻线程的,这里实际上考察的是runloop的基础知识。

这里简单来说,当runloop发现还有source/timer/observer的时候,runloop就不会退出。所以AF这里就通过给当前runloop添加一个NSMachPort,这个port实际上相对于添加了一个source事件源,这样子线程的runloop就会一直处于循环状态,等待别的线程向这个port发送消息,而实际上AF这里是没有消息发送到这个port的。


OC 中 strong, weak, assign, copy 的区别

strong

强引用,只可以修饰对象,属性的默认修饰符,其修饰的对象引用计数增加1

weak

弱引用,只可以修饰对象,指向但不拥有对象,其修饰的对象引用计数不增加,可以避免循环引用,weak修饰的对象释放后,指针会被系统置为nil,此时向对象发送消息不会奔溃

assign

可以修饰对象和基本数据类型,如果修饰对象,其修饰的对象引用计数不增加,可以避免循环引用,但assign修饰的对象释放后,指针不会被系统置为nil,这会产生野指针的问题,此时向对象发送消息会奔溃。所以assign通常用于基本数据类型,如int ,float, NSInteger, CGFloat ,这是因为基本数据类型放在栈区,先进先出,基本数据类型出栈后,assign修饰的变量就不存在了,不用担心指针的问题。

copy

引用,修饰不可变的对象,比如NSString, NSArray, NSDictionary。copy和strong类似,不同之处在于,copy修饰的对象会先在内存中拷贝一个新的对象,copy会指向那个新的对象的内存地址,这样避免了多个指针指向同一对象,而导致的其中一个指针改变了对象,其他指针指向的对象跟着改变,举个例子:

@property(strong) NSString *name1;
@property(copy) NSString *name2;

NSMutableString *name3 = [NSMutableString stringWithString:@"Danny"];
self.name1 = name3;
self.name2 = name3;
[name3 setString:@"Liming"];
NSLog(@"%@", self.name1); // Liming
NSLog(@"%@", self.name2); // Danny

我们可以看到使用strong的属性name1会跟着name3变,因为他们都指向同一个NSMutableString的对象,而name2预先拷贝了name1,从而避免了和name1一起变化。

copy的原则就是,把一个对象赋值给一个属性变量,当这个对象变化了,如果希望属性变量变化就使用strong属性,如果希望属性变量不跟着变化,就是用copy属性。






收起阅读 »

iOS一些容易被忽略的基础面试题

什么是对象 ,OC中的对象有哪些?对象是类的实例;是通过一个类创建出来的实例,一般称之为实例对象;OC中的常见对象有实例对象、类对象、元类对象;什么是类?什么是元类?类对象和类,元类对象和元类有什么区别?类: 是面向对象程序设计(OOP,Object-Orie...
继续阅读 »




什么是对象 ,OC中的对象有哪些?

对象是类的实例;是通过一个类创建出来的实例,一般称之为实例对象;OC中的常见对象有实例对象、类对象、元类对象;

什么是类?什么是元类?类对象和类,元类对象和元类有什么区别?

类: 是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础。类是一种用户定义的引用数据类型,也称类类型。每个类包含数据说明和一组操作数据或传递消息的函数。类的实例称为对象
元类:以类作为其实例的类;
类对象:类本身也是一个对象,所以就有类对象;类对象可以通过实例对象的ISA指针获得
元类对象:元类本身也是一个对象,所以就有元类对象;元类对象可以通过类对象的ISA指针获得
区分二者:

  • 类、元类是面向对象编程中的一种类型
  • 类对象、元类对象是一种对象

什么是分类?

分类也是一个类,其底层结构和类稍有不同;给分类添加的方法会在运行时合并到原有类的方法列表(二维数组)中
分类多用来给类做扩展使用;在OC开发中应用广泛





什么是类扩展?
类扩展:用来给类扩充私有属性、方法

什么是数组?
数组可表示为占用一块连续的内存空间用来存储元素的数据结构;OC中的数组有可变和不可变两种;可变数组做了优化利用环形缓冲区技术提高增删改查时的性能

什么是字典?
字典以键值的形式存储数据,底层实现是哈希表;OC对象作为字典的Key需要遵守NSCopying协议并且实现hash和isEqual两个方法。比如:NSNumber、NSArray 、NSDictionary、自定义OC对象 都可以作为key

什么是集合?
集合是一种用来存储数据的数据结构,内部存储的数据时无序的,其他和数组相同

OC语法有哪些?
OC中的语法有点语法.,这里的点一般转化为setter、getter方法调用

什么是Method?
Method是method_t的结构体,是对一个方法的描述:

struct method_t{
SEL name; //函数名/方法名
const char *types;//编码(返回值类型、参数类型)
IMP imp; //指向函数的指针(函数地址)
}



什么是内敛函数?

  • 内联函数基本概念
    在c++中,预定义宏的概念是用内联函数来实现的,而内联函数本身也是一个真正的函数。内联函数具有普通函数的所有行为。唯一不同之处在于内联函数会在适当的地方像预定义宏一样展开,所以不需要函数调用的开销。因此应该不使用宏,使用内联函数。

在普通函数(非成员函数)函数前面加上inline关键字使之成为内联函数。但是必须注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待。

inline void func(int a);

以上写法没有任何效果,仅仅是声明函数,应该如下方式来做:

inline int func(int a){return ++;}

注意: 编译器将会检查函数参数列表使用是否正确,并返回值(进行必要的转换)。

这些事预处理器无法完成的。
内联函数的确占用空间,但是内联函数相对于普通函数的优势只是省去了函数调用时候的压栈,跳转,返回的开销。我们可以理解为内联函数是以空间换时间。

  • 类内部的内联函数

    为了定义内联函数,通常必须在函数定义前面放一个inline关键字。但是在类内部定义内联函数时并不是必须的。任何在类内部定义的函数自动成为内联函数。

什么是构造函数?
在一个类中定义一个和类名相同的函数,这个函数就是构造函数

面向对象的设计原则是什么 ?
单一责任原则开闭原则接口隔离原则依赖倒置原则里式替换原则迪米特原则

  • 单一责任原则
    一个类只负责一件事情,CALayer只负责动画和视图的显示,UIView只负责事件的传递、事件的响应
  • 开闭原则
    对修改关闭,对扩展开放; 要考虑API的后续扩展,而不是在原有基础上来回修改
  • 接口隔离原则
    使用多协议的方式来定义接口,而不是一个臃肿的协议;比如delagate, datesource
  • 依赖倒置原则
    抽象不应该依赖具体实现,具体实现依赖于抽象
  • 里式替换原则
    父类和子类无缝衔接,且原有功能不受影响;比如:KVO, 用完就走不留痕迹
  • 迪米特原则
    高内聚,低耦合

面向对象语言的三大特性是什么 ?
封装、继承、多态

OC的继承体系





关键字的使用标准?
ARC环境修饰OC对象用strong、copy、weak,修饰基本数据类型用assign;
静态变量用static, 修饰为常量用const

常用设计模式?

设计模式分为四类:结构型模式、创建型模式、行为型模式、软件设计原则

常用的结构型模式有:代理、装饰
常用的创建型模式有:单利、工厂
常用的行为型模式有:观察者、发布订阅模式

代理:是一种消息传递方式,一个完整的代理模式包括:委托对象、代理对象和协议。

  • 请代理三部曲:
    1 定义代理协议
    2 声明delegate对象
    3 调用代理方法
  • 当别人代理的三部曲
    1 遵循协议
    2 设置代理
    3 实现方法

装饰:动态地给一个类添加一些额外的职责;Category 就是实现了装饰的设计模式;Category是一个特殊的类,通过它可以给类添加方法的接口与实现;
观察者:包含通知和KVO
单利:单:唯一,例:实例;即唯一的一个实例,该实例自创建开始到程序退出由系统自动释放;单利常被当做共有类使用;

系统常见单利类
UIApplication(应用程序实例类)
NSNotificationCenter(消息中心类)
NSFileManager(文件管理类)
NSUserDefaults(应用程序设置)
NSURLCache(请求缓存类)
NSHTTPCookieStorage(应用程序cookies池)


工厂模式:分为简单工厂模式、工厂模式、抽象工厂模式
简单工厂模式:简单工厂模式是由一个工厂对象决定创建出哪一种产品类的实例。简单工厂模式是工厂模式家族中最简单实用的模式,可以理解为是不同工厂模式的一个特殊实现。
工厂模式:抽象了工厂接口的具体产品,应用程序的调用不同工厂创建不同产品对象。(抽象产品)
抽象工厂模式:在工厂模式的基础上抽象了工厂,应用程序调用抽象的工厂发发创建不同产品对象。(抽象产品+抽象工厂)

懒加载:把初始化逻辑通过重写的方式封装起来,到需要时直接调用的方式
懒加载的优点

  • 相对来说,如果代码量不是很多,可读性略强
  • 相对来说,防止为nil,减少了后续使用时安全检查的后顾之忧
  • 使用适当,可节省内存资源
  • 一定程度上,节省了某一个期间内的时间
  • 使用得当,优化性能,提高用户体验
    懒加载的缺点
  • 使用太泛滥,导致可读性变差
  • 使用不得当,可能会造成死循环,导致crash
  • 代码量增多(每增加一个懒加载,代码会平均多出3-4行)

什么时候使用懒加载?

一般情况下,不需要使用懒加载,懒加载未必能增强可读性、独立性,滥用反而让可读性适得其反。简言之,就是在逻辑上,觉得现在不需要加载,而在后面某一时间段内可能会加载,就可以考虑懒加载

生产者消费者:
在编码中,有时会遇到一个模块产生数据,另外一个模块处理数据的情况,不论是为了模块间的结偶或是并发处理还是忙闲不均,我们都会在产生和处理数据的模块之间放置缓存区,作为生产和处理数据的仓库。以上的模型就是生产者消费者模型
生产者-消费者

中介者:

  • 中介者模式又叫做调停者模式,其实就是中间人或者调停者的意思
  • 概念:中介者模式(Mediator),用一个中介者对象来封装一系列的对象交互。中介者使各个对象不需要显式地相互引用,从而使其耦合松散,而且可 以独立地改变他们之间的交互
  • UINavigationViewController就是属于一个中介者
  • 中介者模式的优缺点
    中介者模式很容易在系统中应用,也很容易在系统中误用。当系统出现了多对多交互复杂的对象群时,不要急于使用中介者模式,而要先反思你在系统上设计是否合理。
    优点就是集中控制,减少了对象之间的耦合度。缺点就是太过于集中
  • 应用场景
    对象间的交互虽定义明确然而非常复杂,导致一组对象彼此相互依赖而且难以理解。
    因为对象引用了许多其他对象并与其通信,导致对象难以复用。
    想要定制一个分布在多个类中的逻辑或者行为,又不想生成太多子类

发布订阅模式

其实基本的设计模式中并没有发布订阅模式,上面也说了,他只是观察者模式的一个别称。但是经过时间的沉淀,似乎他已经强大了起来,已经独立于观察者模式,成为另外一种不同的设计模式。
在现在的发布订阅模式中,称为发布者的消息发送者不会将消息直接发送给订阅者,这意味着发布者和订阅者不知道彼此的存在。在发布者和订阅者之间存在第三个组件,称为消息代理或调度中心或中间件,它维持着发布者和订阅者之间的联系,过滤所有发布者传入的消息并相应地分发它们给订阅者。

举一个例子,你在微博上关注了A,同时其他很多人也关注了A,那么当A发布动态的时候,微博就会为你们推送这条动态。A就是发布者,你是订阅者,微博就是调度中心,你和A是没有直接的消息往来的,全是通过微博来协调的(你的关注,A的发布动态

观察者模式和发布订阅模式有什么区别?

观察者模式: 观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。

发布订阅模式: 订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。常见的是用协议的方式来做

  • 观察者模式是不是发布订阅模式

网上关于这个问题的回答,出现了两极分化,有认为发布订阅模式就是观察者模式的,也有认为观察者模式和发布订阅模式是真不一样的。

其实我不知道发布订阅模式是不是观察者模式,就像我不知道辨别模式的关键是设计意图还是设计结构(理念),虽然《JavaScript设计模式与开发实践》一书中说了分辨模式的关键是意图而不是结构。

如果以结构来分辨模式,发布订阅模式相比观察者模式多了一个中间件订阅器,所以发布订阅模式是不同于观察者模式的;如果以意图来分辨模式,他们都是实现了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新,那么他们就是同一种模式,发布订阅模式是在观察者模式的基础上做的优化升级。

不过,不管他们是不是同一个设计模式,他们的实现方式确实有差别,我们在使用的时候应该根据场景来判断选择哪个

block

  • block是封装了函数调用和函数调用环境的OC对象,block分为3种类型:NSGlobalBlock、NSStackBlock、NSMallocBlock; 其都继承自NSBlock,NSBlock 继承自NSObject;
  • block使用需注意循环引用问题;一般需要使用强弱引用、__block来解决问题
  • block声明为属性时需要使用copy或strong来修饰;因为block最初是被分配在栈空间,内存由系统管理;但一般使用block是需要在运行时的某一个时机使用,所以需要开发者自己管理block的内存,使用copy和strong修饰会把block的内存复制到堆空间,这样就达到了自己管理内存的目的

为什么block一开始的内存会被分配在栈空间?
block使用会有两种情况:局部变量typedef声明
局部变量申请的内存肯定在栈空间

对象的本质?
对象的本质是结构体;
内存分配原理:以16个字节为单位且遵循了内存对齐原则向堆内存申请内存空间

isa指针?
isa指针是OC对象的第一个成员变量;isa是一个联合体结构,通过位域来存储数据;
isa最重要的作用是用于消息发送;

位域宏定义(真机环境arm64)
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; 拿二进制的1位来存储 \
uintptr_t has_assoc : 1; \
uintptr_t has_cxx_dtor : 1; \
uintptr_t shiftcls : 33;
/*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
uintptr_t magic : 6; \
uintptr_t weakly_referenced : 1; \
uintptr_t deallocating : 1; \
uintptr_t has_sidetable_rc : 1; \
uintptr_t extra_rc : 19
# define RC_ONE (1ULL<<45)
# define RC_HALF (1ULL<<18)



OC的内存管理原则 ?
OC中内存管理是通过引用计数管理实现的,当一个对象的引用计数为0时就会进入释放流程;ARC利用LLVM编译器动态的在合适的位置添加内存管理代码的方式帮助开发者管理内存,同时又通过runtime管理weak修饰的弱引用表;基本实现了不用开发者关心内存问题就可以进行开发;

  • block、定时器时需要注意循环引用问题
  • 声明属性时需要注意强弱引用的使用

多线程?

即 multithreading , 是指从软件或者硬件上实现多个线程并发执行的技术。
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”。
多线程的调度原理可以认为是:时间片轮转调度算法,每个线程都会分配一个时间片然后大家轮着做任务,多线程执行时会快速切换时间片来完成多线程任务的执行;其实操作系统对进程、线程都是按照这种调度逻辑实现的

程序、进程、线程、例程、协程是什么?

  • 程序:全称 计算机程序(Computer Program),是一组计算机能识别和执行的指令,又称计算机软件
    是指为了得到某种结果而可以由计算机等具有信息处理能力的装置执行的代码化指令序列,用某些程序设计语言编写,如:C、C++、OC等;它运行于电子计算机

  • 进程:是计算机中的程序关于某数据集合上的一次运行活动;是独立运行、独立分配资源和独立接受调度的基本单位;是操作系统结构的基础

在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体

  • 线程:线程是计算机调度的最小单位,用来处理不同的任务;

  • 例程:即函数,一个函数就可以看做是一个例程

  • 协程:利用单线程执行多任务的技术解决方案,性能上避免线程间切换要优于线程调度;是线程的更小拆分,又称之为“微线程”,是一种用户太的轻量级线程;
    和线程的区别:
    线程是系统级别的,它们由操作系统调度;同时是可被调度的最小单位;
    协程则是程序级别的,由程序员根据需要自己调度
    子程序:函数
    在一个线程中会有很多子程序,在子程序执行过程中可以中断去执行别的子程序,而别的子程序也可以中断回来继续执行之前的子程序,这个过程及称为协程。也就是说在同一线程内一段代码在执行过程中会中断然后跳转执行别的代码,接着在之前中断的地方继续开始执行,类似于yield操作

实例的生命周期?

  • alloc、new、copy、mutableCopy
  • 引用计数变化
  • 引用计数为0
  • dealloc


作者:9523_it
链接:https://www.jianshu.com/p/7646a2e8165f





收起阅读 »

直播APP开发搭建,直播APP源码涉及方方面面

最近直播APP源码的热度持续上升,究其原因还是因为直播市场的繁荣。直播APP开发搭建到底能做哪些事呢?1. 主播用户开启手机直播 可以设置只有签约主播才能开启手机直播,也可以设置所有注册用户都有权限 开启手机直播的时候可以选择手机前后手机摄像头设备,也可以在直...
继续阅读 »

最近直播APP源码的热度持续上升,究其原因还是因为直播市场的繁荣。

直播APP开发搭建到底能做哪些事呢?

1. 主播用户开启手机直播 可以设置只有签约主播才能开启手机直播,也可以设置所有注册用户都有权限 开启手机直播的时候可以选择手机前后手机摄像头设备,也可以在直播过程中进行切换。

2. 玩家用户观看手机直播 玩家可以通过手机APP观看主播的直播。

3. 用户之间聊天互动 用户可以通过聊天窗口进行文字和表情的聊天,聊天部分使用node.js实现。

4. 用户之间礼物赠送 玩家可以充值后购买赠送礼物给主播,用户和主播分别可以在用户中心可以看到自己赠送和获得礼物的详细列表。

播放列表

直播APP开发的媒体播放器提供下面的方法来访问播放列表中的剪辑:

Next 方法,跳到节目(播放列表)中的下一个剪辑;

Previous 方法,跳回到节目中的上一个剪辑;

媒体播放器的一个特性是能够预览节目中的每一个剪辑,使用如下属性:

PreviewMode 属性,决定媒体播放器当前是否处于预览模式;

直播APP开发搭建,直播APP源码涉及方方面面

了解流媒体(直播APP开发搭建需要用到流媒体)

流媒体开发:网络层(socket或st)负责传输,协议层(rtmp或hls)负责网络打包,封装层(flv、ts)负责编解码数据的封装,编码层(h.264和aac)负责图像,音频压缩。

帧:每帧代表一幅静止的图像。

GOP:(Group of Pictures)画面组,一个GOP就是一组连续的画面,每个画面都是一帧,一个GOP就是很多帧的集合。

直播的数据,其实是一组图片,包括I帧、P帧、B帧,当用户第一次观看的时候,会寻找I帧,而播放器会到服务器寻找到最近的I帧反馈给用户。因此,GOP Cache增加了端到端延迟,因为它必须要拿到最近的I帧。

直播APP的开发说难也难,说容易也相当容易。

难,是因为搞个直播要考虑的东西太多了,根本不是一个人能够搞定的。因为直播中运用到的技术难点非常之多,视频/音频处理,图形处理,视频/音频压缩,CDN分发,即时通讯等技术,每一个技术都够你学几年的。

从采集编码传输到解码播放这个过程要处理的东西太多了:单就视频的采集来说就涉及两方面数据的采集:音频采集和图像采集,涉及到各种算法,这是一个很庞大的工作量。


收起阅读 »

iOS Swift 高阶函数

iOS
在Swift的集合类型中,有许多十分便捷的函数。相比于Objective-C,这些高阶函数会引起你的极度舒适。因为在Swift的许多函数中引入了闭包元素,这就直接造就了它的灵活性,简直就是极致的便捷。下面就来对Swift集合类中的这些高阶函数进行总结。// 全...
继续阅读 »

在Swift的集合类型中,有许多十分便捷的函数。相比于Objective-C,这些高阶函数会引起你的极度舒适。因为在Swift的许多函数中引入了闭包元素,这就直接造就了它的灵活性,简直就是极致的便捷。

下面就来对Swift集合类中的这些高阶函数进行总结。

// 全文的基础数据
let numbers = [7, 6, 10, 9, 8, 1, 2, 3, 4, 5]
1
2
1.sort函数
对原集合进行给定条件排序。
无返回值,直接修改原集合,所以这个集合应该是可变类型的。

var sortArr = numbers
numbers.sort { a, b in
return a < b
}
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
1
2
3
4
5
另外,系统还定义了一个sort()函数,即对集合进行升序排序的函数。但这个函数并不是上面函数不传入缺省值的情况,而是另外一个函数。

var sortArr2 = numbers
numbers.sort()
// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
1
2
3
2.sorted函数
sorted函数与sort函数对应。
将集合进行给定条件排序,返回一个新的集合,不修改原集合。

let sortedArr = numbers.sorted { a, b in
return a > b
}
// [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

// sorted()函数
let sortedArr2 = numbers.sorted()
// [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

// 闭包简写
let sortedArr3 = sortedArr2.sorted(by: >)
// [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
1
2
3
4
5
6
7
8
9
10
11
12
闭包的省略写法
因为在高阶函数中大部分都使用了闭包,所以我认为有必要做一个铺垫,以更好地理解本文。清楚闭包简写的请跳过本段,直奔第3条。

由于sort函数使用了闭包,所以自主定义的闭包可以简写为如下格式:

numbers.sort(by: >)
// [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
1
2
以上述方法为例,一个完整的闭包应该是这样的:

numbers.sorted { (a: Int, b: Int) -> Bool in
return a > b
}
1
2
3
然后,可以省略闭包中的返回值。

numbers.sorted { (a: Int, b: Int) in
return a > b
}
1
2
3
然后,再可以省略形参的类型,让编译器去自主推断。

numbers.sorted { a, b in
return a > b
}
1
2
3
再然后,还可以让$0,$1…来代替第一个,第二个形参,以此类推。

numbers.sorted { return $0 > $1 }
1
再然后,省略return。一般的,到这里也就足够简化了。毕竟在实际开发中我们需要使用闭包中的参数进行一些复杂的判断。

numbers.sorted { $0 > $1 }
1
如果你不需要复杂的判断,那么还可以写成下面这样,代表降序排序。

numbers.sorted(by: >)
1
3.map函数
按照闭包中的返回结果,将集合中对应元素进行替代,也就是映射函数。

// 数组数值转换为其各自平方
let mapArr = numbers.map { $0 * $0 }
// [49, 36, 100, 81, 64, 1, 4, 9, 16, 25]
1
2
3
可选类型的 map, flatMap函数
另外,不仅CollectionType有map和flatMap函数,在Optional类型中,也存在这两个函数。
它们的作用是对可选类型就行解包操作,若有值则进入闭包,并返回一个 Optional类型;若为nil,则直接返回当前可选类型的nil。

let num1: Int? = 3
let num2: Int? = nil

let numMap1 = num1.map {
$0 * 2
}
numMap1 // 6
type(of: numMap1) // Optional<Int>.Type

let numMap2 = num2.map {
$0 == 0
}
numMap2 // nil
type(of: numMap2) // Optional<Bool>.Type


let numFlatMap1 = num1.flatMap {
$0 * $0
}
numFlatMap1 // 9
type(of: numFlatMap1) // Optional<Int>.Type

let numFlatMap2 = num2.flatMap {
$0 == 0
}
numFlatMap2 // nil
type(of: numFlatMap2) // Optional<Bool>.Type
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
还有一种应用场景,就是解析可选类型的时候,map和flatMap函数会让你的代码更加优雅。

举个例子,当解析并判断可选类型的时候,你可能会经过一堆if或者guard判断,如下所示:

func loadURL(url: URL) {
print(url.absoluteString)
}

let urlStr: String? = "https://github.com/wangyanchang21"
guard let siteStr = urlStr else {
assert(false)
}
guard let url = URL(string: siteStr) else {
assert(false)
}
loadURL(url: url)
1
2
3
4
5
6
7
8
9
10
11
12
如果使用map和flatMap函数的话,就会有十分优雅的感觉。

// 这行优雅的代码代替上面的代码
urlStr.flatMap(URL.init).map(loadURL)
1
2
但有一点需要注意,这里 map替换 flatMap会报错, 原因在于 flatMap闭包可以返回 nil, 而 map闭包不可以。就如下面的代码编译不会通过:

// compile error
// urlStr.map(URL.init).map(loadURL)
1
2
再举一个例子:

let date: Date? = Date()
let format = date.map(DateFormatter().string)
1
2
我在函数的闭包形式中也写过这种优雅的写法具体是怎么回事。有兴趣可以了解一下。

4.flatMap函数
也是一种映射函数,这个函数具有多重功能,所以也就造成了这个函数有一个历史问题,稍后会解释。

第一种情况,解析首层元素,若有nil则过滤,就不会降维

let optLatticeNumbers = [[1, Optional(2), 3], [3, nil, 5], nil]
// 解析首层元素, 若有nil则过滤, 就不会降维
let flatMapArr2 = optLatticeNumbers.flatMap { $0 }
// [[1, 2, 3], [3, nil, 5]]
1
2
3
4
第二种情况,解析首层元素,若没有nil,则会降维

let latticeNumbers = [[1, Optional(2), 3], [3, nil, 5]]
// 解析首层元素, 若没有nil, 则会降维
let flatMapArr = latticeNumbers.flatMap { $0 }
// [1, 2, 3, 3, nil, 5]
1
2
3
4
所以flatMap的功能就有两个了,一个功能是解析并过滤首层元素为nil的元素,一个功能是对多维集合进行降维。原因是,其实这是两个功能是两个函数,只是在调用时代码上没有区别。

flatMap和compactMap的关系
但从表面上看,flatMap函数违背了单一功能原则,将过滤nil和降维两个功能于隐藏条件中进行判定。这也就是那个历史问题。

因此,为了将过滤nil和降维两个功能于区分开,swift4.1开始,就只保留了降维的flatMap函数,并弃用了过滤nil的flatMap函数,又用开放的新函数compactMap来替代弃用的函数。

所以,当需要过滤nil的时候,请使用compactMap函数;当需要进行降维时,请使用flatMap函数。这也就是flatMap和compactMap之间的区别。

5.compactMap函数
Swift4.1开始开放的一种映射函数,会解析并过滤首层元素为nil的元素。

let compactMapArr = optLatticeNumbers.compactMap { $0 }
// [[1, 2, 3], [3, nil, 5]]
let compactMapArr2 = latticeNumbers.compactMap { $0 }
// [[1, 2, 3], [3, nil, 5]]
1
2
3
4
compactMap函数作为过滤nil的flatMap函数的替代函数。当集合中的元素为一个一维集合,他们之间的功能是没有差别的。

let flatNumbers = [1, Optional(2), 3, nil, Optional(5), nil]

let flatMapArr = latticeNumbers.flatMap { $0 }
// [1, 2, 3, 5]
let compactMapArr = optLatticeNumbers.compactMap { $0 }
// [1, 2, 3, 5]
1
2
3
4
5
6
6.filter函数
按照条件进行元素过滤。

let filterArr = numbers.filter { num in
return num < 3 || num > 8
}
// [10, 9, 1, 2]
1
2
3
4
7.reduce函数
以指定参数为基础,按照条件进行拼接

let reduceNumber = numbers.reduce(100) { result, num in
return result + num
}
// 155

let reduceString = ["C", "O", "D", "E"].reduce("word: ") { result, num in
return result + num
}
// "word: CODE"
1
2
3
4
5
6
7
8
9
8.prefix函数
正向取满足条件的元素,进行新集合创建。一旦出现不满足条件的元素,则跳出循环,不再执行。

let prefixArr = numbers.prefix { $0 < 10 }
// [7, 6]
1
2
prefix相关函数:
upTo: 正向取元素创建数组, 包含小于指定index的元素

let prefixUpToArr = numbers.prefix(upTo: 5)
// [7, 6, 10, 9, 8]
1
2
through: 正向取元素创建数组, 包含小于等于指定index的元素

let prefixThroughArr = numbers.prefix(through: 2)
// [7, 6, 10]
1
2
maxLength: 正向取元素创建数组, 包含指定的元素个数

let prefixMaxLengthArr = numbers.prefix(6)
// [7, 6, 10, 9, 8, 1]
1
2
9.drop函数
与prefix函数对应。正向跳过满足条件的元素,进行新集合创建。一旦出现不满足条件的元素,则跳出循环,不再执行。

let dropArr = numbers.drop { $0 < 10 }
// [10, 9, 8, 1, 2, 3, 4, 5]
1
2
drop相关函数:
dropFirst: 正向跳过元素创建数组, 跳过指定元素个数, 缺省值为1

let dropFirstArr = numbers.dropFirst(3)
// [7, 6, 10, 9, 8]
1
2
dropLast: 返向跳过元素创建数组, 跳过指定元素个数, 缺省值为1

let dropLastArr = numbers.dropLast(5)
// [7, 6, 10, 9, 8]
1
2
10.first函数
正向找出第一个满足条件的元素。

let first = numbers.first { $0 < 7 }
// 6
1
2
11.last函数
与first函数对应。反向找出第一个满足条件的元素。

let last = numbers.last { $0 > 5 }
// 8
1
2
12.firstIndex函数
正向找出第一个满足条件的元素下标。

let firstIndex = numbers.firstIndex { $0 < 7 }
// 1
1
2
13.lastIndex函数
反向找出第一个满足条件的元素下标。

let lastIndex = numbers.lastIndex { $0 > 5 }
// 4
1
2
14.partition函数
按照条件进行重新排序,不满足条件的元素在集合前半部分,满足条件的元素后半部分,但不是完整的升序或者降序排列。
返回值为排序完成后集合中第一个满足条件的元素下标。

var partitionNumbers = [20, 50, 30, 10, 40, 20, 60]
let pIndex = partitionNumbers.partition { $0 > 30 }
// partitionNumbers = [20, 20, 30, 10, 40, 50, 60]
// pIndex = 4
1
2
3
4
15.min函数
按条件排序后取最小元素。

let min = numbers.min { $0 % 5 < $1 % 5 }
// 10
1
2
min()函数,自然升序取最小。

let minDefault = numbers.min()
// 1
1
2
16.max函数
按条件排序后取最大元素。

let maxDictionary = ["aKey": 33, "bKey": 66, "cKey": 99]
let max = maxDictionary.max { $0.value < $1.value }
// (key "cKey", value 99)
1
2
3
max()函数,自然升序取最大。

let maxDefault = numbers.max()
// 10
1
2
17.removeAll函数
移除原集合中所有满足条件的元素。
无返回值,直接修改原集合,所以这个集合应该是可变类型的。

var removeArr = numbers
removeArr.removeAll { $0 > 6 }
// [6, 1, 2, 3, 4, 5]
1
2
3
18.集合遍历
forEach函数:

numbers.forEach { num in
print(num)
}
1
2
3
for-in函数:

for num in numbers where num < 5 {
print(num)
}
1
2
3
与enumerated()函数配合使用:

for (index, num) in numbers.enumerated() {
print("\(index)-\(num)")
}
1
2
3
关于集合遍历的性能问题,可以看这里enumerated() 和 enumerateObjectsUsingBlock。

19.shuffled函数
shuffled函数,打乱集合中元素的的顺序。

let ascendingNumbers = 0...9
let shuffledArr = ascendingNumbers.shuffled()
// [3, 9, 2, 6, 4, 5, 0, 1, 7, 8]
1
2
3
20.contains函数
contains函数,判断集合中是否包含某元素。

let containsBool = numbers.contains(8)
let containsBool1 = numbers.contains(11)
// true
// false
1
2
3
4
21.split和joined函数
split函数,字符串的函数,按条件分割字符串,为子字符串创建集合。与Objective-C中的componentsSeparatedByString:方法类似。

let line = "123Hi!123I'm123a123coder.123"
let splitArr = line.split { $0.isNumber }
// ["Hi!", "I'm", "a", "coder."]

// 也可指定字符
let splitArr2 = line.split(separator: "1")
// ["23Hi!", "23I'm", "23a", "23coder.", "23"]
1
2
3
4
5
6
7
joined函数,数组元素连接指定字符拼接成一个字符串。与Objective-C中的componentsJoinedByString:方法类似。

let joined = splitArr.joined(separator: "_")
// "Hi!_I'm_a_coder."

// 也可以只传入字符
let joined2 = splitArr2.joined(separator: "#")
// "23Hi!#23I'm#23a#23coder.#23"
1
2
3
4
5
6
22.zip函数
将两个数组合并为一个元组组成的数组。

let titles = ["aaa", "bbb", "ccc"]
let numbers = [111, 222, 333]
let zipA = zip(titles, numbers)
for (title, num) in zipA {
print("\(title)-\(num)")
}
1
2
3
4
5
6
打印结果:

aaa-111
bbb-222
ccc-333

原文链接:https://blog.csdn.net/wangyanchang21/article/details/89955249

收起阅读 »

iOS 八种经典排序算法

iOS
一、冒泡排序(Bubble Sort)冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会...
继续阅读 »

一、冒泡排序(Bubble Sort)

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。

1.1算法复杂度

时间平均复杂度:O(n^2) 最坏复杂度:O(n^2) 最好复杂度: O(n) 空间复杂度: O(1) 稳定

1.2算法过程描述

  • <1>比较相邻的元素。如果第一个比第二个大,就交换它们两个;

  • <2>对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;

  • <3>针对所有的元素重复以上的步骤,除了最后一个;

  • <4>重复步骤1~3,直到排序完成。

1.3代码实现

冒泡排序

1.4执行Log信息

冒泡排序Log1

冒泡排序Log2

二、选择排序(Selection Sort)

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

2.1算法复杂度

时间平均复杂度:O(n^2) 最坏复杂度:O(n^2) 最好复杂度: O(n^2) 空间复杂度: O(1) 不稳定

2.2算法过程描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • <1>初始状态:无序区为R[1..n],有序区为空;

  • <2>第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;

  • <3>n-1趟结束,数组有序化了。

2.3代码实现

选择排序

2.4执行Log信息

选择排序Log

三、插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

3.1算法复杂度

时间平均复杂度:O(n^2) 最坏复杂度:O(n^2) 最好复杂度: O(n) 空间复杂度: O(1) 稳定

3.2算法过程描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  • <1>从第一个元素开始,该元素可以认为已经被排序;

  • <2>取出下一个元素,在已经排序的元素序列中从后向前扫描;

  • <3>如果该元素(已排序)大于新元素,将该元素移到下一位置;

  • <4>重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;

  • <5>将新元素插入到该位置后;

  • <6>重复步骤2~5。

3.3代码实现

插入排序

3.4执行Log信息

插入排序Log

四、希尔排序(Shell Sort)

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。

4.1算法复杂度

时间平均复杂度:O(n^1.3) 最坏复杂度:O(n^2) 最好复杂度: O(n) 空间复杂度: O(1) 不稳定

4.2算法过程描述

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • <1>选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;

  • <2>按增量序列个数k,对序列进行k 趟排序;

  • <3>每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

4.3代码实现

希尔排序

4.4执行Log信息

希尔排序Log

五、归并排序(Merge Sort)

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

5.1算法复杂度

时间平均复杂度:O(nlog2^n) 最坏复杂度:O(nlog2^n) 最好复杂度: O(nlog2^n) 空间复杂度: O(n) 稳定

5.2算法过程描述

  • <1>把长度为n的输入序列分成两个长度为n/2的子序列;

  • <2>对这两个子序列分别采用归并排序;

  • <3>将两个排序好的子序列合并成一个最终的排序序列。

5.3代码实现

归并排序1

归并排序2

5.4执行Log信

归并排序Log

六、快速排序(Quick Sort)

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

6.1算法复杂度

时间平均复杂度:O(nlog2^n) 最坏复杂度:O(n^2) 最好复杂度: O(nlog2^n) 空间复杂度: O(nlog2^n) 不稳定

6.2算法过程描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • <1>从数列中挑出一个元素,称为 “基准”(pivot);

  • <2>重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;

  • <3>递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

6.3代码实现

快速排序1

快速排序2

6.4执行Log信息

快速排序Log

七、堆排序(Heap Sort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

7.1算法复杂度

时间平均复杂度:O(nlog2^n) 最坏复杂度:O(nlog2^n) 最好复杂度: O(nlog2^n) 空间复杂度: O(1) 不稳定

7.2算法过程描述

  • <1>将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;

  • <2>将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];

  • <3>由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

7.3代码实现

堆排序1

堆排序2

7.4执行Log信息

堆排序Log1

堆排序Log2

八、计数排序(Counting Sort)

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

8.1算法复杂度

时间平均复杂度:O(n+k) 最坏复杂度:O(n+k) 最好复杂度: O(n+k) 空间复杂度: O(n+k) 稳定

8.2算法过程描述

  • <1>找出待排序的数组中最大和最小的元素;

  • <2>统计数组中每个值为i的元素出现的次数,存入数组C的第i项;

  • <3>对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);

  • <4>反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

8.3代码实现

计数排序

8.4执行Log信息

计数排序Log

终于结束了,最后附两张快速排序和堆排序的动态展示图!!!!觉得不错的记得点个喜欢/关注哦!

附:

快速排序动态图

 堆排序动态图


链接 http://www.cocoachina.com/cms/wap.php?action=article&id=22988

收起阅读 »

iOS OC项目转Swift指南

iOS
运行环境:Xcode 11.1 Swift5.0最近参与的一个项目需要从Objective-C(以下简称OC)转到Swift,期间遇到了一些坑,于是有了这篇总结性的文档。如果你也有将OC项目Swift化的需求,可以作为参考。OC转Swift有一个大前提就是你要...
继续阅读 »

运行环境:Xcode 11.1 Swift5.0

最近参与的一个项目需要从Objective-C(以下简称OC)转到Swift,期间遇到了一些坑,于是有了这篇总结性的文档。如果你也有将OC项目Swift化的需求,可以作为参考。

OC转Swift有一个大前提就是你要对Swift有一定的了解,熟悉Swift语法,最好是完整看过一遍官方的Language Guide

转换的过程分自动化和手动转译,鉴于自动化工具的识别率不能让人满意,大部分情况都是需要手动转换的。

自动化工具

有一个比较好的自动化工具Swiftify,可以将OC文件甚至OC工程整个转成Swift,号称准确率能达到90%。我试用了一些免费版中的功能,但感觉效果并不理想,因为没有使用过付费版,所以也不好评价它就是不好。

Swiftify还有一个Xcode的插件Swiftify for Xcode,可以实现对选中代码和单文件的转化。这个插件还挺不错,对纯系统代码转化还算精确,但部分代码还存在一些识别问题,需要手动再修改。

手动Swift化

桥接文件

如果你是在项目中首次使用Swift代码,在添加Swift文件时,Xcode会提示你添加一个.h的桥接文件。如果不小心点了不添加还可以手动导入,就是自己手动生成一个.h文件,然后在Build Settings > Swift Compiler - General > Objective-C Bridging Header中填入该.h文件的路径。

image.png

这个桥接文件的作用就是供Swift代码引用OC代码,或者OC的三方库。

#import "Utility.h"
#import
复制代码

Bridging Header的下面还有一个配置项是Objective-C Generated Interface Header Name,对应的值是ProjectName-Swift.h。这是由Xcode自动生成的一个隐藏头文件,每次Build的过程会将Swift代码中声明为外接调用的部分转成OC代码,OC部分的文件会类似pch一样全局引用这个头文件。因为是Build过程中生成的,所以只有.m文件中可以直接引用,对于在.h文件中的引用下文有介绍。

Appdelegate(程序入口)

Swift中没有main.m文件,取而代之的是@UIApplicationMain命令,该命令等效于原有的执行main.m。所以我们可以把main.m文件进行移除。

系统API

对于UIKit框架中的大部分代码转换可以直接查看系统API文档进行转换,这里就不过多介绍。

property(属性)

Swift没有property,也没有copynonatomic等属性修饰词,只有表示属性是否可变的letvar

注意点一 OC中一个类分.h.m两个文件,分别表示用于暴露给外接的方法,变量和仅供内部使用的方法变量。迁移到Swift时,应该将.m中的property标为private,即外接无法直接访问,对于.h中的property不做处理,取默认的internal,即同模块可访问。

对于函数的迁移也是相同的。

注意点二 有一种特殊情况是在OC项目中,某些属性在内部(.m)可变,外部(.h)只读。这种情况可以这么处理:

private(set) var value: String
复制代码

就是只对valueset方法就行private标记。

注意点三 Swift中针对空类型有个专门的符号?,对应OC中的nil。OC中没有这个符号,但是可以通过在nullablenonnull表示该种属性,方法参数或者返回值是否可以空。

如果OC中没有声明一个属性是否可以为空,那就去默认值nonnull

如果我们想让一个类的所有属性,函数返回值都是nonnull,除了手动一个个添加之外还有一个宏命令。

NS_ASSUME_NONNULL_BEGIN
/* code */
NS_ASSUME_NONNULL_END
复制代码

enum(枚举)

OC代码:

typedef NS_ENUM(NSInteger, PlayerState) {
PlayerStateNone = 0,
PlayerStatePlaying,
PlayerStatePause,
PlayerStateBuffer,
PlayerStateFailed,
};

typedef NS_OPTIONS(NSUInteger, XXViewAnimationOptions) {
XXViewAnimationOptionNone = 1 << 0,
XXViewAnimationOptionSelcted1 = 1 << 1,
XXViewAnimationOptionSelcted2 = 1 << 2,
}
复制代码

Swift代码

enum PlayerState: Int {
case none = 0
case playing
case pause
case buffer
case failed
}
struct ViewAnimationOptions: OptionSet {
let rawValue: UInt
static let None = ViewAnimationOptions(rawValue: 1<<0)
static let Selected1 = ViewAnimationOptions(rawValue: 1<<0)
static let Selected2 = ViewAnimationOptions(rawValue: 1 << 2)
//...
}
复制代码

Swift没有NS_OPTIONS的概念,取而代之的是为了满足OptionSet协议的struct类型。

懒加载

OC代码:

- (MTObject *)object {
if (!_object) {
_object = [MTObject new];
}
return _object;
}
复制代码

Swift代码:

lazy var object: MTObject = {
let object = MTObject()
return imagobjecteView
}()
复制代码

闭包

OC代码:

typedef void (^DownloadStateBlock)(BOOL isComplete);
复制代码

Swift代码:

typealias DownloadStateBlock = ((_ isComplete: Bool) -> Void)
复制代码

单例

OC代码:

+ (XXManager *)shareInstance {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
复制代码

Swift对单例的实现比较简单,有两种方式:

第一种

let shared = XXManager()// 声明在全局命名区(global namespace)
Class XXManager {
}
复制代码

你可能会疑惑,为什么没有dispatch_once,如何保证多线程下创建的唯一性?其实是这样的,Swift中全局变量是懒加载,在AppDelegate中被初始化,之后所有的调用都会使用该实例。而且全局变量的初始化是默认使用dispatch_once的,这保证了全局变量的构造器(initializer)只会被调用一次,保证了shard原子性

第二种

Class XXManager {
static let shared = XXManager()
private override init() {
// do something
}
}
复制代码

Swift 2 开始增加了static关键字,用于限定变量的作用域。如果不使用static,那么每一个shared都会对应一个实例。而使用static之后,shared成为全局变量,就成了跟上面第一种方式原理一致。可以注意到,由于构造器使用了 private 关键字,所以也保证了单例的原子性。

初始化方法和析构函数

对于初始化方法OC先调用父类的初始化方法,然后初始自己的成员变量。Swift先初始化自己的成员变量,然后在调用父类的初始化方法。

OC代码:

// 初始化方法
@interface MainView : UIView
@property (nonatomic, strong) NSString *title;
- (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title NS_DESIGNATED_INITIALIZER;
@end

@implementation MainView
- (instancetype)initWithFrame:(CGRect)frame title:(NSString *)title {
if (self = [super initWithFrame:frame]) {
self.title = title;
}
return self;
}
@end
// 析构函数
- (void)dealloc {
//dealloc
}
复制代码

上面类在调用时

Swift代码:

class MainViewSwift: UIView {
let title: String
init(frame: CGRect, title: String) {
self.title = title
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
//deinit
}
}
复制代码

函数调用

OC代码:

// 实例函数(共有方法)
- (void)configModelWith:(XXModel *)model {}
// 实例函数(私有方法)
- (void)calculateProgress {}
// 类函数
+ (void)configModelWith:(XXModel *)model {}
复制代码
// 实例函数(共有方法)
func configModel(with model: XXModel) {}
// 实例函数(私有方法)
private func calculateProgress() {}
// 类函数(不可以被子类重写)
static func configModel(with model: XXModel) {}
// 类函数(可以被子类重写)
class func configModel(with model: XXModel) {}
// 类函数(不可以被子类重写)
class final func configModel(with model: XXModel) {}
复制代码

OC可以通过是否将方法声明在.h文件表明该方法是否为私有方法。Swift中没有了.h文件,对于方法的权限控制是通过权限关键词进行的,各关键词权限大小为: private < fileprivate < internal < public < open

其中internal为默认权限,可以在同一module下访问。

NSNotification(通知)

OC代码:

// add observer
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(method) name:@"NotificationName" object:nil];
// post
[NSNotificationCenter.defaultCenter postNotificationName:@"NotificationName" object:nil];
复制代码

Swift代码:

// add observer
NotificationCenter.default.addObserver(self, selector: #selector(method), name: NSNotification.Name(rawValue: "NotificationName"), object: nil)
// post
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "NotificationName"), object: self)
复制代码

可以注意到,Swift中通知中心NotificationCenter不带NS前缀,通知名由字符串变成了NSNotification.Name的结构体。

改成结构体的目的就是为了便于管理字符串,原本的字符串类型变成了指定的NSNotification.Name类型。上面的Swift代码可以修改为:

extension NSNotification.Name {
static let NotificationName = NSNotification.Name("NotificationName")
}
// add observer
NotificationCenter.default.addObserver(self, selector: #selector(method), name: .NotificationName, object: nil)
// post
NotificationCenter.default.post(name: .NotificationName, object: self)
复制代码

protocol(协议/代理)

OC代码:

@protocol XXManagerDelegate 
- (void)downloadFileFailed:(NSError *)error;
@optional
- (void)downloadFileComplete;
@end

@interface XXManager: NSObject
@property (nonatomic, weak) id delegate;
@end
复制代码

Swift中对protocol的使用拓宽了许多,不光是class对象,structenum也都可以实现协议。需要注意的是structenum为指引用类型,不能使用weak修饰。只有指定当前代理只支持类对象,才能使用weak。将上面的代码转成对应的Swift代码,就是:

@objc protocol XXManagerDelegate {
func downloadFailFailed(error: Error)
@objc optional func downloadFileComplete() // 可选协议的实现
}
class XXManager: NSObject {
weak var delegate: XXManagerDelegate?
}
复制代码

@objc是表明当前代码是针对NSObject对象,也就是class对象,就可以正常使用weak了。

如果不是针对NSObject对象的delegate,仅仅是普通的class对象可以这样设置代理:

protocol XXManagerDelegate: class {
func downloadFailFailed(error: Error)
}
class XXManager {
weak var delegate: XXManagerDelegate?
}
复制代码

值得注意的是,仅@objc标记的protocol可以使用@optional

Swift和OC混编注意事项

函数名的变化

如果你在一个Swift类里定义了一个delegate方法:

@objc protocol MarkButtonDelegate {
func clickBtn(title: String)
}
复制代码

如果你要在OC中实现这个协议,这时候方法名就变成了:

- (void)clickBtnWithTitle:(NSString *)title {
// code
}
复制代码

这主要是因为Swift有指定参数标签,OC却没有,所以在由Swift方法名生成OC方法名时编译器会自动加一些修饰词,已使函数作为一个句子可以"通顺"。

在OC的头文件里调用Swift类

如果要在OC的头文件里引用Swift类,因为Swift没有头文件,而为了让在头文件能够识别该Swift类,需要通过@class的方法引入。

@class SwiftClass;

@interface XXOCClass: NSObject
@property (nonatomic, strong) SwiftClass *object;
@end
复制代码

对OC类在Swift调用下重命名

因为Swift对不同的module都有命名空间,所以Swift类都不需要添加前缀。如果有一个带前缀的OC公共组件,在Swift环境下调用时不得不指定前缀是一件很不优雅的事情,所以苹果添加了一个宏命令NS_SWIFT_NAME,允许在OC类在Swift环境下的重命名:

NS_SWIFT_NAME(LoginManager)
@interface XXLoginManager: NSObject
@end
复制代码

这样我们就将XXLoginManager在Swift环境下的类名改为了LoginManager

引用类型和值类型

  • struct 和 enum 是值类型,类 class 是引用类型。
  • StringArray和 Dictionary都是结构体,因此赋值直接是拷贝,而NSStringNSArray 和NSDictionary则是类,所以是使用引用的方式。
  • struct 比 class 更“轻量级”,struct 分配在栈中,class 分配在堆中。

id类型和AnyObject

OC中id类型被Swift调用时会自动转成AnyObject,他们很相似,但却其实概念并不一致。Swift中还有一个概念是Any,他们三者的区别是:

  • id 是一种通用的对象类型,它可以指向属于任何类的对象,在OC中即是可以代表所有继承于NSObject的对象。
  • AnyObject可以代表任何class类型的实例。
  • Any可以代表任何类型,甚至包括func类型。

从范围大小比较就是:id < AnyObject < Any

其他语法区别及注意事项(待补充)

1、Swift语句中不需要加分号;

2、关于Bool类型更加严格,Swift不再是OC中的非0就是真,真假只对应truefalse

3、Swift类内一般不需要写self,但是闭包内是需要写的。

4、Swift是强类型语言,必须要指定明确的类型。在Swift中IntFloat是不能直接做运算的,必须要将他们转成同一类型才可以运算。

5、Swift抛弃了传统的++--运算,抛弃了传统的C语言式的for循环写法,而改为for-in

6、Swift的switch操作,不需要在每个case语句结束的时候都添加break

7、Swift对enum的使用做了很大的扩展,可以支持任意类型,而OC枚举仅支持Int类型,如果要写兼容代码,要选择Int型枚举。

8、Swift代码要想被OC调用,需要在属性和方法名前面加上@objc

9、Swift独有的特性,如泛型,struct,非Int型的enum等被包含才函数参数中,即使添加@objc也不会被编译器通过。

10、Swift支持重载,OC不支持。

11、带默认值的Swift函数再被OC调用时会自动展开。

语法检查

对于OC转Swift之后的语法变化还有很多细节值得注意,特别是对于初次使用Swift这门语言的同学,很容易遗漏或者待着OC的思想去写代码。这里推荐一个语法检查的框架SwiftLint,可以自动化的检查我们的代码是否符合Swift规范。

可以通过cocoapods进行引入,配置好之后,每次Build的过程,Lint脚本都会执行一遍Swift代码的语法检查操作,Lint还会将代码规范进行分级,严重的代码错误会直接报错,导致程序无法启动,不太严重的会显示代码警告(⚠️)。

如果你感觉SwiftLint有点过于严格了,还可以通过修改.swiftlint.yml文件,自定义属于自己的语法规范。

链接:https://juejin.im/post/5e5a4f20518825495a277aa7

收起阅读 »

Android 面试题及答案

15、 说说mvc模式的原理,它在android中的运用,android的官方建议应用程序的开发采用mvc模式。何谓mvc? mvc是model,view,controller的缩写,mvc包含三个部分:   模型(model)对象:是应用程序的主体部分,所有...
继续阅读 »

15、 说说mvc模式的原理,它在android中的运用,android的官方建议应用程序的开发采用mvc模式。何谓mvc?


mvc是model,view,controller的缩写,mvc包含三个部分:


  模型(model)对象:是应用程序的主体部分,所有的业务逻辑都应该写在该层。


  视图(view)对象:是应用程序中负责生成用户界面的部分。也是在整个mvc架构中用户唯一可以看到的一层,接收用户的输入,显示处理结果。


  控制器(control)对象:是根据用户的输入,控制用户界面数据显示及更新model对象状态的部分,控制器更重要的一种导航功能,响应用户出发的相关事件,交给m层处理。


  android鼓励弱耦合和组件的重用,在android中mvc的具体体现如下:


  1)视图层(view):一般采用xml文件进行界面的描述,使用的时候可以非常方便的引入,当然,如果你对android了解的比较的多了话,就一定可以想到在android中也可以使用JavaScript+html等的方式作为view层,当然这里需要进行java和javascript之间的通信,幸运的是,android提供了它们之间非常方便的通信实现。


  2)控制层(controller):android的控制层的重任通常落在了众多的acitvity的肩上,这句话也就暗含了不要在acitivity中写代码,要通过activity交割model业务逻辑层处理,这样做的另外一个原因是android中的acitivity的响应时间是5s,如果耗时的操作放在这里,程序就很容易被回收掉。


  3)模型层(model):对数据库的操作、对网络等的操作都应该在model里面处理,当然对业务计算等操作也是必须放在的该层的。


16、 什么是ANR 如何避免它?


答:ANR:Application Not Responding。在Android中,活动管理器和窗口管理器这两个系统服务负责监视应用程序的响应,当用户操作的在5s内应用程序没能做出反应,BroadcastReceiver在10秒内没有执行完毕,就会出现应用程序无响应对话框,这既是ANR。


避免方法:Activity应该在它的关键生命周期方法(如onCreate()和onResume())里尽可能少的去做创建操作。潜在的耗时操作,例如网络或数据库操作,或者高耗时的计算如改变位图尺寸,应该在子线程里(或者异步方式)来完成。主线程应该为子线程提供一个Handler,以便完成时能够提交给主线程。


17、 什么情况会导致Force Close ?如何避免?能否捕获导致其的异常?


答:程序出现异常,比如nullpointer。


避免:编写程序时逻辑连贯,思维缜密。能捕获异常,在logcat中能看到异常信息


18、 描述一下android的系统架构


android系统架构分从下往上为linux 内核层、运行库、应用程序框架层、和应用程序层。


linuxkernel:负责硬件的驱动程序、网络、电源、系统安全以及内存管理等功能。


libraries和 android runtime:libraries:即c/c++函数库部分,大多数都是开放源代码的函数库,例如webkit(引擎),该函数库负责 android网页浏览器的运行,例如标准的c函数库libc、openssl、sqlite等,当然也包括支持游戏开发2dsgl和 3dopengles,在多媒体方面有mediaframework框架来支持各种影音和图形文件的播放与显示,例如mpeg4、h.264、mp3、 aac、amr、jpg和png等众多的多媒体文件格式。android的runtime负责解释和执行生成的dalvik格式的字节码。


 applicationframework(应用软件架构),java应用程序开发人员主要是使用该层封装好的api进行快速开发。


  applications:该层是java的应用程序层,android内置的googlemaps、e-mail、即时通信工具、浏览器、mp3播放器等处于该层,java开发人员开发的程序也处于该层,而且和内置的应用程序具有平等的位置,可以调用内置的应用程序,也可以替换内置的应用程序。


  上面的四个层次,下层为上层服务,上层需要下层的支持,调用下层的服务,这种严格分层的方式带来的极大的稳定性、灵活性和可扩展性,使得不同层的开发人员可以按照规范专心特定层的开发。


android应用程序使用框架的api并在框架下运行,这就带来了程序开发的高度一致性,另一方面也告诉我们,要想写出优质高效的程序就必须对整个 applicationframework进行非常深入的理解。精通applicationframework,你就可以真正的理解android的设计和运行机制,也就更能够驾驭整个应用层的开发。


19、 请介绍下ContentProvider是如何实现数据共享的。


一个程序可以通过实现一个Content provider的抽象接口将自己的数据完全暴露出去,而且Content providers是以类似数据库中表的方式将数据暴露。Content providers存储和检索数据,通过它可以让所有的应用程序访问到,这也是应用程序之间唯一共享数据的方法。


要想使应用程序的数据公开化,可通过2种方法:创建一个属于你自己的Content provider或者将你的数据添加到一个已经存在的Content provider中,前提是有相同数据类型并且有写入Content provider的权限。


如何通过一套标准及统一的接口获取其他应用程序暴露的数据?


Android提供了ContentResolver,外界的程序可以通过ContentResolver接口访问ContentProvider提供的数据。


20、 Service和Thread的区别?


答:servie是系统的组件,它由系统进程托管(servicemanager);它们之间的通信类似于client和server,是一种轻量级的ipc通信,这种通信的载体是binder,它是在linux层交换信息的一种ipc。而thread是由本应用程序托管。1). Thread:Thread 是程序执行的最小单元,它是分配CPU的基本单位。可以用 Thread 来执行一些异步的操作。


2). Service:Service 是android的一种机制,当它运行的时候如果是Local Service,那么对应的 Service 是运行在主进程的 main 线程上的。如:onCreate,onStart 这些函数在被系统调用的时候都是在主进程的 main 线程上运行的。如果是Remote Service,那么对应的 Service 则是运行在独立进程的 main 线程上。


既然这样,那么我们为什么要用 Service 呢?其实这跟 android 的系统机制有关,我们先拿 Thread 来说。Thread 的运行是独立于 Activity 的,也就是说当一个 Activity 被 finish 之后,如果你没有主动停止 Thread 或者 Thread 里的 run 方法没有执行完毕的话,Thread 也会一直执行。因此这里会出现一个问题:当 Activity 被 finish 之后,你不再持有该 Thread 的引用。另一方面,你没有办法在不同的 Activity 中对同一 Thread 进行控制。  


举个例子:如果你的 Thread 需要不停地隔一段时间就要连接服务器做某种同步的话,该 Thread 需要在 Activity 没有start的时候也在运行。这个时候当你 start 一个 Activity 就没有办法在该 Activity 里面控制之前创建的 Thread。因此你便需要创建并启动一个 Service ,在 Service 里面创建、运行并控制该 Thread,这样便解决了该问题(因为任何 Activity 都可以控制同一 Service,而系统也只会创建一个对应 Service 的实例)。  


因此你可以把 Service 想象成一种消息服务,而你可以在任何有 Context 的地方调用 Context.startService、Context.stopService、Context.bindService,Context.unbindService,来控制它,你也可以在 Service 里注册 BroadcastReceiver,在其他地方通过发送 broadcast 来控制它,当然这些都是 Thread 做不到的。


21、 Android本身的api并未声明会抛出异常,则其在运行时有无可能抛出runtime异常,你遇到过吗?诺有的话会导致什么问题?如何解决?


答:会,比如nullpointerException。我遇到过,比如textview.setText()时,textview没有初始化。会导致程序无法正常运行出现forceclose。打开控制台查看logcat信息找出异常信息并修改程序。


22、 IntentService有何优点?


答:Acitivity的进程,当处理Intent的时候,会产生一个对应的Service; Android的进程处理器现在会尽可能的不kill掉你;非常容易使用


23、 如果后台的Activity由于某原因被系统回收了,如何在被系统回收之前保存当前状态?


答:重写onSaveInstanceState()方法,在此方法中保存需要保存的数据,该方法将会在activity被回收之前调用。通过重写onRestoreInstanceState()方法可以从中提取保存好的数据


24、 如何将一个Activity设置成窗口的样式。


答:中配置:android :theme="@android:style/Theme.Dialog" 


另外android:theme="@android:style/Theme.Translucent" 是设置透明


25、 如何退出Activity?如何安全退出已调用多个Activity的Application?


答:对于单一Activity的应用来说,退出很简单,直接finish()即可。当然,也可以用killProcess()和System.exit()这样的方法。


对于多个activity,1、记录打开的Activity:每打开一个Activity,就记录下来。在需要退出时,关闭每一个Activity即可。2、发送特定广播:在需要结束应用时,发送一个特定的广播,每个Activity收到广播后,关闭即可。3、递归退出:在打开新的Activity时使用startActivityForResult,然后自己加标志,在onActivityResult中处理,递归关闭。为了编程方便,最好定义一个Activity基类,处理这些共通问题。


在2.1之前,可以使用ActivityManager的restartPackage方法。


它可以直接结束整个应用。在使用时需要权限android.permission.RESTART_PACKAGES。


注意不要被它的名字迷惑。


可是,在2.2,这个方法失效了。在2.2添加了一个新的方法,killBackground Processes(),需要权限 android.permission.KILL_BACKGROUND_PROCESSES。可惜的是,它和2.2的restartPackage一样,根本起不到应有的效果。


另外还有一个方法,就是系统自带的应用程序管理里,强制结束程序的方法,forceStopPackage()。它需要权限android.permission.FORCE_STOP_PACKAGES。并且需要添加android:sharedUserId="android.uid.system"属性。同样可惜的是,该方法是非公开的,他只能运行在系统进程,第三方程序无法调用。


因为需要在Android.mk中添加LOCAL_CERTIFICATE := platform。


而Android.mk是用于在Android源码下编译程序用的。


从以上可以看出,在2.2,没有办法直接结束一个应用,而只能用自己的办法间接办到。


现提供几个方法,供参考:


1、抛异常强制退出:


该方法通过抛异常,使程序Force Close。


验证可以,但是,需要解决的问题是,如何使程序结束掉,而不弹出Force Close的窗口。


2、记录打开的Activity:


每打开一个Activity,就记录下来。在需要退出时,关闭每一个Activity即可。


3、发送特定广播:


在需要结束应用时,发送一个特定的广播,每个Activity收到广播后,关闭即可。


4、递归退出


在打开新的Activity时使用startActivityForResult,然后自己加标志,在onActivityResult中处理,递归关闭。


除了第一个,都是想办法把每一个Activity都结束掉,间接达到目的。但是这样做同样不完美。你会发现,如果自己的应用程序对每一个Activity都设置了nosensor,在两个Activity结束的间隙,sensor可能有效了。但至少,我们的目的达到了,而且没有影响用户使用。为了编程方便,最好定义一个Activity基类,处理这些共通问题。


26、 AIDL的全称是什么?如何工作?能处理哪些类型的数据?


答:全称是:Android Interface Define Language


在Android中, 每个应用程序都可以有自己的进程. 在写UI应用的时候, 经常要用到Service. 在不同的进程中, 怎样传递对象呢?显然, Java中不允许跨进程内存共享. 因此传递对象, 只能把对象拆分成操作系统能理解的简单形式, 以达到跨界对象访问的目的. 在J2EE中,采用RMI的方式, 可以通过序列化传递对象. 在Android中, 则采用AIDL的方式. 理论上AIDL可以传递Bundle,实际上做起来却比较麻烦。


AIDL(AndRoid接口描述语言)是一种借口描述语言; 编译器可以通过aidl文件生成一段代码,通过预先定义的接口达到两个进程内部通信进程的目的. 如果需要在一个Activity中, 访问另一个Service中的某个对象, 需要先将对象转化成AIDL可识别的参数(可能是多个参数), 然后使用AIDL来传递这些参数, 在消息的接收端, 使用这些参数组装成自己需要的对象.


AIDL的IPC的机制和COM或CORBA类似, 是基于接口的,但它是轻量级的。它使用代理类在客户端和实现层间传递值. 如果要使用AIDL, 需要完成2件事情: 1. 引入AIDL的相关类.; 2. 调用aidl产生的class.


AIDL的创建方法:


AIDL语法很简单,可以用来声明一个带一个或多个方法的接口,也可以传递参数和返回值。 由于远程调用的需要, 这些参数和返回值并不是任何类型.下面是些AIDL支持的数据类型:


1. 不需要import声明的简单Java编程语言类型(int,boolean等)


2. String, CharSequence不需要特殊声明


3. List, Map和Parcelables类型, 这些类型内所包含的数据成员也只能是简单数据类型, String等其他比支持的类型.


(另外: 我没尝试Parcelables, 在Eclipse+ADT下编译不过, 或许以后会有所支持)


27、 请解释下Android程序运行时权限与文件系统权限的区别。


答:运行时权限Dalvik( android授权) 


文件系统 linux 内核授权


28、 系统上安装了多种浏览器,能否指定某浏览器访问指定页面?请说明原由。


通过直接发送Uri把参数带过去,或者通过manifest里的intentfilter里的data属性
29、 android系统的优势和不足



答:Android平台手机 5大优势: 


一、开放性 


在优势方面,Android平台首先就是其开发性,开发的平台允许任何移动终端厂商加入到Android联盟中来。显著的开放性可以使其拥有更多的开发者,随着用户和应用的日益丰富,一个崭新的平台也将很快走向成熟。开放性对于Android的发展而言,有利于积累人气,这里的人气包括消费者和厂商,而对于消费者来讲,随大的受益正是丰富的软件资源。开放的平台也会带来更大竞争,如此一来,消费者将可以用更低的价位购得心仪的手机。


二、挣脱运营商的束缚 


在过去很长的一段时间,特别是在欧美地区,手机应用往往受到运营商制约,使用什么功能接入什么网络,几乎都受到运营商的控制。从去年iPhone 上市 ,用户可以更加方便地连接网络,运营商的制约减少。随着EDGE、HSDPA这些2G至3G移动网络的逐步过渡和提升,手机随意接入网络已不是运营商口中的笑谈,当你可以通过手机IM软件方便地进行即时聊天时,再回想不久前天价的彩信和图铃下载业务,是不是像噩梦一样?互联网巨头Google推动的Android终端天生就有网络特色,将让用户离互联网更近。


三、丰富的硬件选择 


这一点还是与Android平台的开放性相关,由于Android的开放性,众多的厂商会推出千奇百怪,功能特色各具的多种产品。功能上的差异和特色,却不会影响到数据同步、甚至软件的兼容,好比你从诺基亚 Symbian风格手机 一下改用苹果 iPhone ,同时还可将Symbian中优秀的软件带到iPhone上使用、联系人等资料更是可以方便地转移,是不是非常方便呢?


四、不受任何限制的开发商 


Android平台提供给第三方开发商一个十分宽泛、自由的环境,不会受到各种条条框框的阻扰,可想而知,会有多少新颖别致的软件会诞生。但也有其两面性,血腥、暴力、情色方面的程序和游戏如可控制正是留给Android难题之一。


五、无缝结合的Google应用 


如今叱诧互联网的Google已经走过10年度历史,从搜索巨人到全面的互联网渗透,Google服务如地图、邮件、搜索等已经成为连接用户和互联网的重要纽带,而Android平台手机将无缝结合这些优秀的Google服务。


再说Android的5大不足:


一、安全和隐私 


由于手机 与互联网的紧密联系,个人隐私很难得到保守。除了上网过程中经意或不经意留下的个人足迹,Google这个巨人也时时站在你的身后,洞穿一切,因此,互联网的深入将会带来新一轮的隐私危机。


二、首先开卖Android手机的不是最大运营商 


众所周知,T-Mobile在23日,于美国纽约发布 了Android首款手机G1。但是在北美市场,最大的两家运营商乃AT&T和Verizon,而目前所知取得Android手机销售权的仅有 T-Mobile和Sprint,其中T-Mobile的3G网络相对于其他三家也要逊色不少,因此,用户可以买账购买G1,能否体验到最佳的3G网络服务则要另当别论了!


三、运营商仍然能够影响到Android手机 


在国内市场,不少用户对购得移动定制机不满,感觉所购的手机被人涂画了广告一般。这样的情况在国外市场同样出现。Android手机的另一发售运营商Sprint就将在其机型中内置其手机商店程序。


四、同类机型用户减少 


在不少手机论坛都会有针对某一型号的子论坛,对一款手机的使用心得交流,并分享软件资源。而对于Android平台手机,由于厂商丰富,产品类型多样,这样使用同一款机型的用户越来越少,缺少统一机型的程序强化。举个稍显不当的例子,现在山寨机泛滥,品种各异,就很少有专门针对某个型号山寨机的讨论和群组,除了哪些功能异常抢眼、颇受追捧的机型以外。


五、过分依赖开发商缺少标准配置 


在使用PC端的Windows Xp系统的时候,都会内置微软Windows Media Player这样一个浏览器程序,用户可以选择更多样的播放器,如Realplay或暴风影音等。但入手开始使用默认的程序同样可以应付多样的需要。在 Android平台中,由于其开放性,软件更多依赖第三方厂商,比如Android系统的SDK中就没有内置音乐 播放器,全部依赖第三方开发,缺少了产品的统一性。


30、 Android dvm的进程和Linux的进程, 应用程序的进程是否为同一个概念 


答:DVM指dalivk的虚拟机。每一个Android应用程序都在它自己的进程中运行,都拥有一个独立的Dalvik虚拟机实例。而每一个DVM都是在Linux 中的一个进程,所以说可以认为是同一个概念。


31、 sim卡的EF文件是什么?有何作用


答:sim卡的文件系统有自己规范,主要是为了和手机通讯,sim本 身可以有自己的操作系统,EF就是作存储并和手机通讯用的


32、 嵌入式操作系统内存管理有哪几种, 各有何特性


  页式,段式,段页,用到了MMU,虚拟空间等技术


33、 什么是嵌入式实时操作系统, Android 操作系统属于实时操作系统吗?


嵌入式实时操作系统是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统作出快速响应,并控制所有实时任务协调一致运行的嵌入式操作系统。主要用于工业控制、 军事设备、 航空航天等领域对系统的响应时间有苛刻的要求,这就需要使用实时系统。又可分为软实时和硬实时两种,而android是基于linux内核的,因此属于软实时。


34、 一条最长的短信息约占多少byte?


  中文70(包括标点),英文160,160个字节。  


35、 如何将SQLite数据库(dictionary.db文件)与apk文件一起发布


解答:可以将dictionary.db文件复制到Eclipse Android工程中的res aw目录中。所有在res aw目录中的文件不会被压缩,这样可以直接提取该目录中的文件。可以将dictionary.db文件复制到res aw目录中


36、 如何将打开res aw目录中的数据库文件?


解答:在Android中不能直接打开res aw目录中的数据库文件,而需要在程序第一次启动时将该文件复制到手机内存或SD卡的某个目录中,然后再打开该数据库文件。


复制的基本方法是使用getResources().openRawResource方法获得res aw目录中资源的 InputStream对象,然后将该InputStream对象中的数据写入其他的目录中相应文件中。在Android SDK中可以使用SQLiteDatabase.openOrCreateDatabase方法来打开任意目录中的SQLite数据库文件。


37、 DDMS和TraceView的区别? 


DDMS是一个程序执行查看器,在里面可以看见线程和堆栈等信息,TraceView是程序性能分析器 。


38、 java中如何引用本地语言 


可以用JNI(java native interface  java 本地接口)接口 。


39、 谈谈Android的IPC(进程间通信)机制 


IPC是内部进程通信的简称, 是共享"命名管道"的资源。Android中的IPC机制是为了让Activity和Service之间可以随时的进行交互,故在Android中该机制,只适用于Activity和Service之间的通信,类似于远程方法调用,类似于C/S模式的访问。通过定义AIDL接口文件来定义IPC接口。Servier端实现IPC接口,Client端调用IPC接口本地代理。


40、 NDK是什么


NDK是一些列工具的集合,NDK提供了一系列的工具,帮助开发者迅速的开发C/C++的动态库,并能自动将so和java 应用打成apk包。


NDK集成了交叉编译器,并提供了相应的mk文件和隔离cpu、平台等的差异,开发人员只需简单的修改mk文件就可以创建出so


作者:优弧同学
链接:https://juejin.cn/post/6844903441039360007
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android 面试题及答案

‍‍8、跟activity和Task 有关的 Intent启动方式有哪些?其含义?‍‍ ‍‍核心的Intent Flag有:‍‍  ‍‍FLAG_ACTIVITY_NEW_TASK‍‍  ‍‍FLAG_ACTI...
继续阅读 »

‍‍8、跟activity和Task 有关的 Intent启动方式有哪些?其含义?‍‍


‍‍核心的Intent Flag有:‍‍ 


‍‍FLAG_ACTIVITY_NEW_TASK‍‍ 


‍‍FLAG_ACTIVITY_CLEAR_TOP ‍‍


‍‍FLAG_ACTIVITY_RESET_TASK_IF_NEEDED ‍‍


‍‍FLAG_ACTIVITY_SINGLE_TOP‍‍


‍‍FLAG_ACTIVITY_NEW_TASK‍‍ 


  ‍‍如果设置,这个Activity会成为历史stack中一个新Task的开始。一个Task(从启动它的Activity到下一个Task中的 Activity)定义了用户可以迁移的Activity原子组。Task可以移动到前台和后台;在某个特定Task中的所有Activity总是保持相同的次序。‍‍ 


 ‍‍ 这个标志一般用于呈现“启动”类型的行为:它们提供用户一系列可以单独完成的事情,与启动它们的Activity完全无关。 


 使用这个标志,如果正在启动的Activity的Task已经在运行的话,那么,新的Activity将不会启动;代替的,当前Task会简单的移入前台。参考FLAG_ACTIVITY_MULTIPLE_TASK标志,可以禁用这一行为。 


  这个标志不能用于调用方对已经启动的Activity请求结果。


FLAG_ACTIVITY_CLEAR_TOP 


  如果设置,并且这个Activity已经在当前的Task中运行,因此,不再是重新启动一个这个Activity的实例,而是在这个Activity上方的所有Activity都将关闭,然后这个Intent会作为一个新的Intent投递到老的Activity(现在位于顶端)中。 


  例如,假设一个Task中包含这些Activity:A,B,C,D。如果D调用了startActivity(),并且包含一个指向Activity B的Intent,那么,C和D都将结束,然后B接收到这个Intent,因此,目前stack的状况是:A,B。 


  上例中正在运行的Activity B既可以在onNewIntent()中接收到这个新的Intent,也可以把自己关闭然后重新启动来接收这个Intent。如果它的启动模式声明为 “multiple”(默认值),并且你没有在这个Intent中设置FLAG_ACTIVITY_SINGLE_TOP标志,那么它将关闭然后重新创建;对于其它的启动模式,或者在这个Intent中设置FLAG_ACTIVITY_SINGLE_TOP标志,都将把这个Intent投递到当前这个实例的onNewIntent()中。 


  这个启动模式还可以与FLAG_ACTIVITY_NEW_TASK结合起来使用:用于启动一个Task中的根Activity,它会把那个Task中任何运行的实例带入前台,然后清除它直到根Activity。这非常有用,例如,当从Notification Manager处启动一个Activity。 


FLAG_ACTIVITY_RESET_TASK_IF_NEEDED 


    如果设置这个标志,这个activity不管是从一个新的栈启动还是从已有栈推到栈顶,它都将以the front door of the task的方式启动。这就讲导致任何与应用相关的栈都讲重置到正常状态(不管是正在讲activity移入还是移除),如果需要,或者直接重置该栈为初始状态。


FLAG_ACTIVITY_SINGLE_TOP 


  如果设置,当这个Activity位于历史stack的顶端运行时,不再启动一个新的


FLAG_ACTIVITY_BROUGHT_TO_FRONT 


  这个标志一般不是由程序代码设置的,如在launchMode中设置singleTask模式时系统帮你设定。 


FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET 


  如果设置,这将在Task的Activity stack中设置一个还原点,当Task恢复时,需要清理Activity。也就是说,下一次Task带着 FLAG_ACTIVITY_RESET_TASK_IF_NEEDED标记进入前台时(典型的操作是用户在主画面重启它),这个Activity和它之上的都将关闭,以至于用户不能再返回到它们,但是可以回到之前的Activity。 


  这在你的程序有分割点的时候很有用。例如,一个e-mail应用程序可能有一个操作是查看一个附件,需要启动图片浏览Activity来显示。这个 Activity应该作为e-mail应用程序Task的一部分,因为这是用户在这个Task中触发的操作。然而,当用户离开这个Task,然后从主画面选择e-mail app,我们可能希望回到查看的会话中,但不是查看图片附件,因为这让人困惑。通过在启动图片浏览时设定这个标志,浏览及其它启动的Activity在下次用户返回到mail程序时都将全部清除。 


FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 


  如果设置,新的Activity不会在最近启动的Activity的列表中保存。 


FLAG_ACTIVITY_FORWARD_RESULT 


  如果设置,并且这个Intent用于从一个存在的Activity启动一个新的Activity,那么,这个作为答复目标的Activity将会传到这个新的Activity中。这种方式下,新的Activity可以调用setResult(int),并且这个结果值将发送给那个作为答复目标的 Activity。 


FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY 


  这个标志一般不由应用程序代码设置,如果这个Activity是从历史记录里启动的(常按HOME键),那么,系统会帮你设定。 


FLAG_ACTIVITY_MULTIPLE_TASK 


  不要使用这个标志,除非你自己实现了应用程序启动器。与FLAG_ACTIVITY_NEW_TASK结合起来使用,可以禁用把已存的Task送入前台的行为。当设置时,新的Task总是会启动来处理Intent,而不管这是是否已经有一个Task可以处理相同的事情。 


  由于默认的系统不包含图形Task管理功能,因此,你不应该使用这个标志,除非你提供给用户一种方式可以返回到已经启动的Task。 


  如果FLAG_ACTIVITY_NEW_TASK标志没有设置,这个标志被忽略。 


FLAG_ACTIVITY_NO_ANIMATION 


  如果在Intent中设置,并传递给Context.startActivity()的话,这个标志将阻止系统进入下一个Activity时应用 Acitivity迁移动画。这并不意味着动画将永不运行——如果另一个Activity在启动显示之前,没有指定这个标志,那么,动画将被应用。这个标志可以很好的用于执行一连串的操作,而动画被看作是更高一级的事件的驱动。 


FLAG_ACTIVITY_NO_HISTORY 


  如果设置,新的Activity将不再历史stack中保留。用户一离开它,这个Activity就关闭了。这也可以通过设置noHistory特性。 


FLAG_ACTIVITY_NO_USER_ACTION 


  如果设置,作为新启动的Activity进入前台时,这个标志将在Activity暂停之前阻止从最前方的Activity回调的onUserLeaveHint()。 


  典型的,一个Activity可以依赖这个回调指明显式的用户动作引起的Activity移出后台。这个回调在Activity的生命周期中标记一个合适的点,并关闭一些Notification。 


  如果一个Activity通过非用户驱动的事件,如来电或闹钟,启动的,这个标志也应该传递给Context.startActivity,保证暂停的Activity不认为用户已经知晓其Notification。 


FLAG_ACTIVITY_PREVIOUS_IS_TOP 


  If set and this intent is being used to launch a new activity from an existing one, the current activity will not be counted as the top activity for deciding whether the new intent should be delivered to the top instead of starting a new one. The previous activity will be used as the top, with the assumption being that the current activity will finish itself immediately. 


FLAG_ACTIVITY_REORDER_TO_FRONT 


  如果在Intent中设置,并传递给Context.startActivity(),这个标志将引发已经运行的Activity移动到历史stack的顶端。 


  例如,假设一个Task由四个Activity组成:A,B,C,D。如果D调用startActivity()来启动Activity B,那么,B会移动到历史stack的顶端,现在的次序变成A,C,D,B。如果FLAG_ACTIVITY_CLEAR_TOP标志也设置的话,那么这个标志将被忽略。 


9、 请描述下Activity的生命周期。


答:activity的生命周期方法有:onCreate()、onStart()、onReStart()、onResume()、onPause()、onStop()、onDestory();


可见生命周期:从onStart()直到系统调用onStop()


前台生命周期:从onResume()直到系统调用onPause()


10、 activity在屏幕旋转时的生命周期


答:不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次;设置Activity的android:configChanges="orientation"时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次;设置Activity的android:configChanges="orientation|keyboardHidden"时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法


11、 如何启用Service,如何停用Service。


服务的开发比较简单,如下:


第一步:继承Service类


public class SMSService extends Service {}

第二步:在AndroidManifest.xml文件中的节点里对服务进行配置:


服务不能自己运行,需要通过调用Context.startService()或Context.bindService()方法启动服务。这两个方法都可以启动Service,但是它们的使用场合有所不同。使用startService()方法启用服务,调用者与服务之间没有关连,即使调用者退出了,服务仍然运行。使用bindService()方法启用服务,调用者与服务绑定在了一起,调用者一旦退出,服务也就终止,大有“不求同时生,必须同时死”的特点。


如果打算采用Context.startService()方法启动服务,在服务未被创建时,系统会先调用服务的onCreate()方法,接着调用onStart()方法。如果调用startService()方法前服务已经被创建,多次调用startService()方法并不会导致多次创建服务,但会导致多次调用onStart()方法。采用startService()方法启动的服务,只能调用Context.stopService()方法结束服务,服务结束时会调用onDestroy()方法。


如果打算采用Context.bindService()方法启动服务,在服务未被创建时,系统会先调用服务的onCreate()方法,接着调用onBind()方法。这个时候调用者和服务绑定在一起,调用者退出了,系统就会先调用服务的onUnbind()方法,接着调用onDestroy()方法。如果调用bindService()方法前服务已经被绑定,多次调用bindService()方法并不会导致多次创建服务及绑定(也就是说onCreate()和onBind()方法并不会被多次调用)。如果调用者希望与正在绑定的服务解除绑定,可以调用unbindService()方法,调用该方法也会导致系统调用服务的onUnbind()-->onDestroy()方法。


服务常用生命周期回调方法如下: 


onCreate() 该方法在服务被创建时调用,该方法只会被调用一次,无论调用多少次startService()或bindService()方法,服务也只被创建一次。


onDestroy()该方法在服务被终止时调用。


与采用Context.startService()方法启动服务有关的生命周期方法


onStart() 只有采用Context.startService()方法启动服务时才会回调该方法。该方法在服务开始运行时被调用。多次调用startService()方法尽管不会多次创建服务,但onStart() 方法会被多次调用。


与采用Context.bindService()方法启动服务有关的生命周期方法


onBind()只有采用Context.bindService()方法启动服务时才会回调该方法。该方法在调用者与服务绑定时被调用,当调用者与服务已经绑定,多次调用Context.bindService()方法并不会导致该方法被多次调用。


onUnbind()只有采用Context.bindService()方法启动服务时才会回调该方法。该方法在调用者与服务解除绑定时被调用


12、 注册广播有几种方式,这些方式有何优缺点?请谈谈Android引入广播机制的用意。


答:首先写一个类要继承BroadcastReceiver


第一种:在清单文件中声明,添加




   
复制代码

‍‍第二种使用代码进行注册如:‍‍


IntentFilter filter =  new IntentFilter("android.provider.Telephony.SMS_RECEIVED");
IncomingSMSReceiver receiver = new IncomgSMSReceiver();
registerReceiver(receiver.filter);复制代码

两种注册类型的区别是:


1)第一种不是常驻型广播,也就是说广播跟随程序的生命周期。


2)第二种是常驻型,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行。


13、 请解释下在单线程模型中Message、Handler、Message Queue、Looper之间的关系。


答:简单的说,Handler获取当前线程中的looper对象,looper用来从存放Message的MessageQueue中取出Message,再有Handler进行Message的分发和处理.


Message Queue(消息队列):用来存放通过Handler发布的消息,通常附属于某一个创建它的线程,可以通过Looper.myQueue()得到当前线程的消息队列


Handler:可以发布或者处理一个消息或者操作一个Runnable,通过Handler发布消息,消息将只会发送到与它关联的消息队列,然也只能处理该消息队列中的消息


Looper:是Handler和消息队列之间通讯桥梁,程序组件首先通过Handler把消息传递给Looper,Looper把消息放入队列。Looper也把消息队列里的消息广播给所有的


Handler:Handler接受到消息后调用handleMessage进行处理


Message:消息的类型,在Handler类中的handleMessage方法中得到单个的消息进行处理


在单线程模型下,为了线程通信问题,Android设计了一个Message Queue(消息队列), 线程间可以通过该Message Queue并结合Handler和Looper组件进行信息交换。下面将对它们进行分别介绍:


1. Message 


    Message消息,理解为线程间交流的信息,处理数据后台线程需要更新UI,则发送Message内含一些数据给UI线程。


2. Handler 


    Handler处理者,是Message的主要处理者,负责Message的发送,Message内容的执行处理。后台线程就是通过传进来的 Handler对象引用来sendMessage(Message)。而使用Handler,需要implement 该类的 handleMessage(Message)方法,它是处理这些Message的操作内容,例如Update UI。通常需要子类化Handler来实现handleMessage方法。


3. Message Queue 


    Message Queue消息队列,用来存放通过Handler发布的消息,按照先进先出执行。


    每个message queue都会有一个对应的Handler。Handler会向message queue通过两种方法发送消息:sendMessage或post。这两种消息都会插在message queue队尾并按先进先出执行。但通过这两种方法发送的消息执行的方式略有不同:通过sendMessage发送的是一个message对象,会被 Handler的handleMessage()函数处理;而通过post方法发送的是一个runnable对象,则会自己执行。


4. Looper 


    Looper是每条线程里的Message Queue的管家。Android没有Global的Message Queue,而Android会自动替主线程(UI线程)建立Message Queue,但在子线程里并没有建立Message Queue。所以调用Looper.getMainLooper()得到的主线程的Looper不为NULL,但调用Looper.myLooper() 得到当前线程的Looper就有可能为NULL。对于子线程使用Looper,API Doc提供了正确的使用方法:这个Message机制的大概流程:


    1. 在Looper.loop()方法运行开始后,循环地按照接收顺序取出Message Queue里面的非NULL的Message。


    2. 一开始Message Queue里面的Message都是NULL的。当Handler.sendMessage(Message)到Message Queue,该函数里面设置了那个Message对象的target属性是当前的Handler对象。随后Looper取出了那个Message,则调用 该Message的target指向的Hander的dispatchMessage函数对Message进行处理。在dispatchMessage方法里,如何处理Message则由用户指定,三个判断,优先级从高到低:


    1) Message里面的Callback,一个实现了Runnable接口的对象,其中run函数做处理工作;


    2) Handler里面的mCallback指向的一个实现了Callback接口的对象,由其handleMessage进行处理;


    3) 处理消息Handler对象对应的类继承并实现了其中handleMessage函数,通过这个实现的handleMessage函数处理消息。


    由此可见,我们实现的handleMessage方法是优先级最低的!


    3. Handler处理完该Message (update UI) 后,Looper则设置该Message为NULL,以便回收!


    在网上有很多文章讲述主线程和其他子线程如何交互,传送信息,最终谁来执行处理信息之类的,个人理解是最简单的方法——判断Handler对象里面的Looper对象是属于哪条线程的,则由该线程来执行! 


    1. 当Handler对象的构造函数的参数为空,则为当前所在线程的Looper; 


2. Looper.getMainLooper()得到的是主线程的Looper对象,Looper.myLooper()得到的是当前线程的Looper对象。


14、 简要解释一下activity、 intent 、intent filter、service、Broadcase、BroadcaseReceiver


答:一个activity呈现了一个用户可以操作的可视化用户界面;一个service不包含可见的用户界面,而是在后台运行,可以与一个activity绑定,通过绑定暴露出来接口并与其进行通信;一个broadcast receiver是一个接收广播消息并做出回应的component,broadcast receiver没有界面;一个intent是一个Intent对象,它保存了消息的内容。对于activity和service来说,它指定了请求的操作名称和待操作数据的URI,Intent对象可以显式的指定一个目标component。如果这样的话,android会找到这个component(基于manifest文件中的声明)并激活它。但如果一个目标不是显式指定的,android必须找到响应intent的最佳component。它是通过将Intent对象和目标的intent filter相比较来完成这一工作的;一个component的intent filter告诉android该component能处理的intent。intent filter也是在manifest文件中声明的。


作者:优弧同学
链接:https://juejin.cn/post/6844903441039360007
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android 面试题及答案

1、 Android的四大组件是哪些,它们的作用? 答:Activity:Activity是Android程序与用户交互的窗口,是Android构造块中最基本的一种,它需要为保持各界面的状态,做很多持久化的事情,妥善管理生命周期以及一些跳转逻辑 service...
继续阅读 »

1、 Android的四大组件是哪些,它们的作用?


答:Activity:Activity是Android程序与用户交互的窗口,是Android构造块中最基本的一种,它需要为保持各界面的状态,做很多持久化的事情,妥善管理生命周期以及一些跳转逻辑


service:后台服务于Activity,封装有一个完整的功能逻辑实现,接受上层指令,完成相关的事物,定义好需要接受的Intent提供同步和异步的接口


Content Provider:是Android提供的第三方应用数据的访问方案,可以派生Content Provider类,对外提供数据,可以像数据库一样进行选择排序,屏蔽内部数据的存储细节,向外提供统一的借口模型,大大简化上层应用,对数据的整合提供了更方便的途径


BroadCast Receiver:接受一种或者多种Intent作触发事件,接受相关消息,做一些简单处理,转换成一条Notification,统一了Android的事件广播模型


2、 请介绍下Android中常用的五种布局。


常用五种布局方式,分别是:FrameLayout(框架布局),LinearLayout (线性布局),AbsoluteLayout(绝对布局),RelativeLayout(相对布局),TableLayout(表格布局)。


一、FrameLayout:所有东西依次都放在左上角,会重叠,这个布局比较简单,也只能放一点比较简单的东西。二、LinearLayout:线性布局,每一个LinearLayout里面又可分为垂直布局(android:orientation="vertical")和水平布局(android:orientation="horizontal" )。当垂直布局时,每一行就只有一个元素,多个元素依次垂直往下;水平布局时,只有一行,每一个元素依次向右排列。三、AbsoluteLayout:绝对布局用X,Y坐标来指定元素的位置,这种布局方式也比较简单,但是在屏幕旋转时,往往会出问题,而且多个元素的时候,计算比较麻烦。四、RelativeLayout:相对布局可以理解为某一个元素为参照物,来定位的布局方式。主要属性有:相对于某一个元素android:layout_below、      android:layout_toLeftOf相对于父元素的地方android:layout_alignParentLeft、android:layout_alignParentRigh;五、TableLayout:表格布局,每一个TableLayout里面有表格行TableRow,TableRow里面可以具体定义每一个元素。每一个布局都有自己适合的方式,这五个布局元素可以相互嵌套应用,做出美观的界面。


3、 android中的动画有哪几类,它们的特点和区别是什么  


答:两种,一种是Tween动画、还有一种是Frame动画。Tween动画,这种实现方式可以使视图组件移动、放大、缩小以及产生透明度的变化;另一种Frame动画,传统的动画方法,通过顺序的播放排列好的图片来实现,类似电影。


4、 android 中有哪几种解析xml的类?官方推荐哪种?以及它们的原理和区别。


答:XML解析主要有三种方式,SAX、DOM、PULL。常规在PC上开发我们使用Dom相对轻松些,但一些性能敏感的数据库或手机上还是主要采用SAX方式,SAX读取是单向的,优点:不占内存空间、解析属性方便,但缺点就是对于套嵌多个分支来说处理不是很方便。而DOM方式会把整个XML文件加载到内存中去,这里Android开发网提醒大家该方法在查找方面可以和XPath很好的结合如果数据量不是很大推荐使用,而PULL常常用在J2ME对于节点处理比较好,类似SAX方式,同样很节省内存,在J2ME中我们经常使用的KXML库来解析。


5、 ListView的优化方案


答:1、如果自定义适配器,那么在getView方法中要考虑方法传进来的参数contentView是否为null,如果为null就创建contentView并返回,如果不为null则直接使用。在这个方法中尽可能少创建view。


  2、给contentView设置tag(setTag()),传入一个viewHolder对象,用于缓存要显示的数据,可以达到图像数据异步加载的效果。


  3、如果listview需要显示的item很多,就要考虑分页加载。比如一共要显示100条或者更多的时候,我们可以考虑先加载20条,等用户拉到列表底部的时候再去加载接下来的20条。


6、 请介绍下Android的数据存储方式。


答:使用SharedPreferences存储数据;文件存储数据;SQLite数据库存储数据;使用ContentProvider存储数据;网络存储数据;


Preference,File, DataBase这三种方式分别对应的目录是/data/data/Package Name/Shared_Pref, /data/data/Package Name/files, /data/data/Package Name/database 。


一:使用SharedPreferences存储数据


首先说明SharedPreferences存储方式,它是 Android提供的用来存储一些简单配置信息的一种机制,例如:登录用户的用户名与密码。其采用了Map数据结构来存储数据,以键值的方式存储,可以简单的读取与写入,具体实例如下:


void ReadSharedPreferences(){
String strName,strPassword;
SharedPreferences   user = getSharedPreferences(“user_info”,0);
strName = user.getString(“NAME”,””);
strPassword = user getString(“PASSWORD”,””);
}
void WriteSharedPreferences(String strName,String strPassword){
SharedPreferences   user = getSharedPreferences(“user_info”,0);
uer.edit();
user.putString(“NAME”, strName);
user.putString(“PASSWORD” ,strPassword);
user.commit();
}复制代码

‍‍数据读取与写入的方法都非常简单,只是在写入的时候有些区别:先调用edit()使其处于编辑状态,然后才能修改数据,最后使用commit()提交修改的数据。实际上SharedPreferences是采用了XML格式将数据存储到设备中,在DDMS中的File Explorer中的/data/data//shares_prefs下。使用SharedPreferences是有些限制的:只能在同一个包内使用,不能在不同的包之间使用。‍‍


‍‍二:文件存储数据‍‍


‍‍文件存储方式是一种较常用的方法,在Android中读取/写入文件的方法,与 Java中实现I/O的程序是完全一样的,提供了openFileInput()和openFileOutput()方法来读取设备上的文件。具体实例如下:‍‍


String fn = “moandroid.log”;
FileInputStream fis = openFileInput
(fn);
FileOutputStream fos = openFileOutput
(fn,Context.MODE_PRIVATE);复制代码

‍‍三:网络存储数据‍‍


‍‍网络存储方式,需要与Android 网络数据包打交道,关于Android 网络数据包的详细说明,请阅读Android SDK引用了Java SDK的哪些package?。‍‍


‍‍四:ContentProvider‍‍


‍‍1、ContentProvider简介‍‍


‍‍当应用继承ContentProvider类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据。虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同,如:采用文件方式对外共享数据,需要进行文件操作读写数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读写数据。而使用ContentProvider共享数据的好处是统一了数据访问方式。‍‍


‍‍2、Uri类简介‍‍


‍‍Uri代表了要操作的数据,Uri主要包含了两部分信息:1.需要操作的ContentProvider ,2.对ContentProvider中的什么数据进行操作,一个Uri由以下几部分组成:‍‍


‍‍1.scheme:ContentProvider(内容提供者)的scheme已经由Android所规定为:content://…‍‍


‍‍2.主机名(或Authority):用于唯一标识这个ContentProvider,外部调用者可以根据这个标识来找到它。‍‍


‍‍3.路径(path):可以用来表示我们要操作的数据,路径的构建应根据业务而定,如下:‍‍


要操作contact表中id为10的记录,可以构建这样的路径:/contact/10


要操作contact表中id为10的记录的name字段, contact/10/name


要操作contact表中的所有记录,可以构建这样的路径:/contact?


要操作的数据不一定来自数据库,也可以是文件等他存储方式,如下:


要操作xml文件中contact节点下的name节点,可以构建这样的路径:/contact/name


如果要把一个字符串转换成Uri,可以使用Uri类中的parse()方法,如下:


Uri uri = Uri.parse("content://com.changcheng.provider.contactprovider/contact")


3、UriMatcher、ContentUrist和ContentResolver简介


因为Uri代表了要操作的数据,所以我们很经常需要解析Uri,并从 Uri中获取数据。Android系统提供了两个用于操作Uri的工具类,分别为UriMatcher 和ContentUris 。掌握它们的使用,会便于我们的开发工作。


UriMatcher:用于匹配Uri,它的用法如下:


‍‍1.首先把你需要匹配Uri路径全部给注册上,如下:‍‍


//常量UriMatcher.NO_MATCH表示不匹配任何路径的返回码(-1)。
UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
//如果match()方法匹配content://com.changcheng.sqlite.provider.contactprovider /contact路径,返回匹配码为1
uriMatcher.addURI(“com.changcheng.sqlite.provider.contactprovider”, “contact”, 1);//添加需要匹配uri,如果匹配就会返回匹配码
//如果match()方法匹配 content://com.changcheng.sqlite.provider.contactprovider/contact/230路径,返回匹配码为2
uriMatcher.addURI(“com.changcheng.sqlite.provider.contactprovider”, “contact/#”, 2);//#号为通配符复制代码

‍‍2.注册完需要匹配的Uri后,就可以使用uriMatcher.match(uri)方法对输入的Uri进行匹配,如果匹配就返回匹配码,匹配码是调用 addURI()方法传入的第三个参数,假设匹配 content://com.changcheng.sqlite.provider.contactprovider/contact路径,返回的匹配码为1。‍‍


‍‍ContentUris:用于获取Uri路径后面的ID部分,它有两个比较实用的方法:‍‍


‍‍withAppendedId(uri, id)用于为路径加上ID部分‍‍


‍‍parseId(uri)方法用于从路径中获取ID部分‍‍


‍‍ContentResolver:当外部应用需要对ContentProvider中的数据进行添加、删除、修改和查询操作时,可以使用 ContentResolver 类来完成,要获取ContentResolver 对象,可以使用Activity提供的getContentResolver()方法。 ContentResolver使用insert、delete、update、query方法,来操作数据。‍‍


‍‍7、activity的启动模式有哪些?是什么含义?‍‍


‍‍答:在android里,有4种activity的启动模式,分别为: ‍‍


‍‍“standard” (默认) ‍‍


‍‍“singleTop” ‍‍


‍‍“singleTask” ‍‍


‍‍“singleInstance”


‍‍它们主要有如下不同:‍‍


‍‍1. 如何决定所属task ‍‍


‍‍“standard”和”singleTop”的activity的目标task,和收到的Intent的发送者在同一个task内,除非intent包括参数FLAG_ACTIVITY_NEW_TASK。‍‍ 


‍‍如果提供了FLAG_ACTIVITY_NEW_TASK参数,会启动到别的task里。 ‍‍


‍‍“singleTask”和”singleInstance”总是把activity作为一个task的根元素,他们不会被启动到一个其他task里。‍‍


‍‍2. 是否允许多个实例 ‍‍


‍‍“standard”和”singleTop”可以被实例化多次,并且存在于不同的task中,且一个task可以包括一个activity的多个实例; ‍‍


‍‍“singleTask”和”singleInstance”则限制只生成一个实例,并且是task的根元素。 singleTop要求如果创建intent的时候栈顶已经有要创建的Activity的实例,则将intent发送给该实例,而不发送给新的实例。‍‍


‍‍3. 是否允许其它activity存在于本task内 ‍‍


‍‍“singleInstance”独占一个task,其它activity不能存在那个task里;如果它启动了一个新的activity,不管新的activity的launch mode 如何,新的activity都将会到别的task里运行(如同加了FLAG_ACTIVITY_NEW_TASK参数)。 ‍‍


‍‍而另外三种模式,则可以和其它activity共存。‍‍


‍‍4. 是否每次都生成新实例‍‍ 


‍‍“standard”对于没一个启动Intent都会生成一个activity的新实例; ‍‍


‍‍“singleTop”的activity如果在task的栈顶的话,则不生成新的该activity的实例,直接使用栈顶的实例,否则,生成该activity的实例。 ‍‍


‍‍比如现在task栈元素为A-B-C-D(D在栈顶),这时候给D发一个启动intent,如果D是 “standard”的,则生成D的一个新实例,栈变为A-B-C-D-D。‍‍ 


‍‍如果D是singleTop的话,则不会生产D的新实例,栈状态仍为A-B-C-D ‍‍


‍‍如果这时候给B发Intent的话,不管B的launchmode是”standard” 还是 “singleTop” ,都会生成B的新实例,栈状态变为A-B-C-D-B。‍‍


‍‍“singleInstance”是其所在栈的唯一activity,它会每次都被重用。‍‍


‍‍“singleTask”如果在栈顶,则接受intent,否则,该intent会被丢弃,但是该task仍会回到前台。‍‍


‍‍当已经存在的activity实例处理新的intent时候,会调用onNewIntent()方法 如果收到intent生成一个activity实例,那么用户可以通过back键回到上一个状态;如果是已经存在的一个activity来处理这个intent的话,用户不能通过按back键返回到这之前的状态。


作者:优弧同学
链接:https://juejin.cn/post/6844903441039360007
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

为数不多的人知道的 Kotlin 技巧

Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的团队,在项目使用 Kotlin。众所周知 xml 十分耗时,因此在 Android 10.0 ...
继续阅读 »

Google 引入 Kotlin 的目的就是为了让 Android 开发更加方便,自从官宣 Kotlin 成为了 Android 开发的首选语言之后,已经有越来越多的团队,在项目使用 Kotlin。

众所周知 xml 十分耗时,因此在 Android 10.0 上新增加 tryInflatePrecompiled 方法,这是一个在编译期运行的一个优化,因为布局文件越复杂 XmlPullParser 解析 XML 越耗时, tryInflatePrecompiled 方法根据 XML 预编译生成 compiled_view.dex, 然后通过反射来生成对应的 View,从而减少 XmlPullParser 解析 XML 的时间,但是目前一直处于禁用状态。源码解析请查看 Android 资源加载源码分析一

因此一些体量比较大的应用,为了极致的优化,缩短一点时间,对于简单的布局,会使用 Kotlin 去重写这部分 UI,但是门槛还是很高,随着 Jetpack Compose 的出现,其目的是让您更快、更轻松地构建原生 Android 应用,前不久 Google 正式发布了 Jetpack Compose 1.0。

Kotlin 优势已经体现在了方方面面,结合着 Kotlin 的高级函数的特性可以让代码可读性更强,更加简洁,但是如果使用不当会对性能造成一些损耗,更多内容可前往查看。

以上两篇文章,主要分享了 Kotlin 在实际项目中使用的技巧,以及如果使用不当会对 性能 和 内存 造成的那些影响以及如何规避这些问题等等。

通过这篇文章你将学习到以下内容:

  • 什么是 Contract,以及如何使用?
  • Kotlin 注解在项目中的使用?
  • 一行代码接受 Activity 或者 Fragment 传递的参数?
  • 一行代码实现 Activity 之间传递参数?
  • 一行代码实现 Fragment 之间传递参数?
  • 一行代码实现点击事件,避免内存泄露?

KtKit 仓库

这篇文章主要围绕一个新库 KtKit 来介绍一些 Kotlin 技巧,正如其名 KtKit 是用 Kotlin 语言编写的工具库,包含了项目中常用的一系列工具,是 Jetpack ktx 系列的补充,涉及到了很多从 Kotlin 源码、Jetpack ktx、anko 等等知名的开源项目中学习到的技巧,包含了 Kotlin 委托属性、高阶函数、扩展函数、内联、注解的使用等等。

如果想要使用文中的 API 需要将下列代码添加在模块级 build.gradle 文件内, 最新版本号请查看 版本记录

implementation "com.hi-dhl:ktkit:${ktkitVersion}"

因为篇幅原因,文章中不会过多的涉及源码分析,源码部分将会在后续的文章中分享。

什么是 Contract,以及如何使用

众所周知 Kotlin 是比较智能的,比如 smart cast 特性,但是在有些情况下显得很笨拙,并不是那么智能,如下所示。

public inline fun String?.isNotNullOrEmpty(): Boolean {
return this != null && !this.trim().equals("null", true) && this.trim().isNotEmpty()
}

fun testString(name: String?) {
if (name.isNotNullOrEmpty()) {
println(name.length) // 1
}
}

正如你所见,只有字符串 name 不为空时,才会进入注释 1 的地方,但是以上代码却无法正常编译,如下图所示。

编译器会告诉你一个编译错误,经过代码分析只有当字符串 name 不为空时,才会进入注释 1 的地方,但是编译器却无法正常推断出来,真的是编译器做不到吗?看看官方文档是如何解释的。

However, as soon as these checks are extracted in a separate function, all the smartcasts immediately disappear:

将检查提取到一个函数中, smart cast 所带来的效果都会消失

编译器无法深入分析每一个函数,原因在于实际开发中我们可能写出更加复杂的代码,而 Kotlin 编译器进行了大量的静态分析,如果编译器去分析每一个函数,需要花费时间分析上下文,增加它的编译耗时的时间。

如果要解决上诉问题,这就需要用到 Contract 特性,Contract 是 Kotlin 提供的非常有用的特性,Contract 的作用就是当 Kotlin 编译器没有足够的信息去分析函数的情况的时候,Contracts 可以为函数提供附加信息,帮助 Kotlin 编译器去分析函数的情况,修改代码如下所示。

inline fun String?.isNotNullOrEmpty(): Boolean {
contract {
returns(true) implies (this@isNotNullOrEmpty != null)
}

return this != null && !this.trim().equals("null", true) && this.trim().isNotEmpty()
}

fun testString(name: String?) {
if (name != null && name.isNotNullOrEmpty()) {
println(name.length) // 1
}
}

相比于之前的代码,在 isNotNullOrEmpty() 函数中添加了 contract 代码块即可正常编译通过,这行代码的意思就是,如果返回值是 true ,this 所指向对象就不为 null。 而在 Kotlin 标准库中大量的用到 contract 特性。 上述示例的使用可前往查看 KtKit/ProfileActivity.kt

Kotlin 注解在项目中的使用

contract 是 Kotlin 1.3 添加的实验性的 API,如果我们调用实验性的 API 需要添加 @ExperimentalContracts 注解才可以正常使用,但是如果添加 @ExperimentalContracts 注解,所有调用这个方法的地方都需要添加注解,如果想要解决这个问题。只需要在声明 contract 文件中的第一行添加以下代码即可。

@file:OptIn(ExperimentalContracts::class)

在上述示例中使用了 inline 修饰符,但是编译器会有一个黄色警告,如下图所示。

编译器建议我们将函数作为参数时使用 Inline,Inline (内联函数) 的作用:提升运行效率,调用被 inline 修饰符的函数,会将方法内的代码段放到调用处。

既然 Inline 修饰符可以提升运行效率,为什么还给出警告,因为 Inline 修饰符的滥用会带来性能损失,更多内容前往查看 Inline 修饰符带来的性能损失

Inline 修饰符常用于下面的情况,编译器才不会有警告:

  • 将函数作为参数(例如:lambda 表达式)
  • 结合 reified 实化类型参数一起使用

但是在普通的方法中,使用 Inline 修饰符,编译会给出警告,如果方法体的代码段很短,想要通过 Inline 修饰符提升性能(虽然微乎其微),可以在文件的第一行添加下列代码,可消除警告。

@file:Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")

然后在使用 Inline 修饰符的地方添加以下注解,即可愉快的使用。

@kotlin.internal.InlineOnly

注解 @kotlin.internal.InlineOnly 的作用:

  • 消除编译器的警告
  • 修改内联函数的可见性,在编译时修改成 private
// 未添加 InlineOnly 编译后的代码
public static final void showShortToast(@NotNull Context $this$showShortToast, @NotNull String message) {
......
Toast.makeText($this$showShortToast, (CharSequence)message, 0).show();
}


// 添加 InlineOnly 编译后的代码
@InlineOnly
private static final void showShortToast(Context $this$showShortToast, String message) {
......
Toast.makeText($this$showShortToast, (CharSequence)message, 0).show();
}

关于注解完整的使用案例,可前往仓库 KtKit 查看。

一行代码接受 Activity 或者 Fragment 传递的参数

如果想要实现一行代码接受 Activity 或者 Fragment 传递的参数,可以通过 Kotlin 委托属性来实现,在仓库 KtKit 中提供了两个 API,根据实际情况使用即可。案例可前往查看 KtKit/ProfileActivity.kt

class ProfileActivity : Activity() {
// 方式一: 不带默认值
private val userPassword by intent<String>(KEY_USER_PASSWORD)

// 方式二:带默认值:如果获取失败,返回一个默认值
private val userName by intent<String>(KEY_USER_NAME) { "公众号:ByteCode" }
}

一行代码实现 Activity 之间传递参数

这个思路是参考了 anko 的实现,同样是提供了两个 API , 根据实际情况使用即可,可以传递 Android 支持的任意参数,案例可前往查看 KtKit/ProfileActivity.kt

// API:
activity.startActivity<ProfileActivity> { arrayOf( KEY_USER_NAME to "ByteCode" ) }
activity.startActivity<ProfileActivity>( KEY_USER_NAME to "ByteCode" )

// Example:
class ProfileActivity : Activity() {
......
companion object {
......

// 方式一
activity.startActivity<ProfileActivity> {
arrayOf(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}

// 方式二
activity.startActivity<ProfileActivity>(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}
}

Activity 之间传递参数 和 并回传结果

// 方式一
context.startActivityForResult<ProfileActivity>(KEY_REQUEST_CODE,
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)

// 方式二
context.startActivityForResult<ProfileActivity>(KEY_REQUEST_CODE) {
arrayOf(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}

回传结果

// 方式一
setActivityResult(Activity.RESULT_OK) {
arrayOf(
KEY_RESULT to "success",
KEY_USER_NAME to "ByteCode"
)
}

// 方式二
setActivityResult(
Activity.RESULT_OK,
KEY_RESULT to "success",
KEY_USER_NAME to "ByteCode"
)

一行代码实现 Fragment 之间传递参数

和 Activity 一样提供了两个 API 根据实际情况使用即可,可以传递 Android 支持的任意参数,源码前往查看 KtKit/LoginFragment.kt

// API: 
LoginFragment().makeBundle( KEY_USER_NAME to "ByteCode" )
LoginFragment().makeBundle { arrayOf( KEY_USER_NAME to "ByteCode" ) }

// Example:
class LoginFragment : Fragment(R.layout.fragment_login) {
......
companion object {
......
// 方式一
fun newInstance1(): Fragment {
return LoginFragment().makeBundle(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}

// 方式二
fun newInstance2(): Fragment {
return LoginFragment().makeBundle {
arrayOf(
KEY_USER_NAME to "ByteCode",
KEY_USER_PASSWORD to "1024"
)
}
}
}
}

一行代码实现点击事件,避免内存泄露

KtKit 提供了常用的三个 API:单击事件、延迟第一次点击事件、防止多次点击

单击事件

view.click(lifecycleScope) { showShortToast("公众号:ByteCode" }

延迟第一次点击事件

// 默认延迟时间是 500ms
view.clickDelayed(lifecycleScope){ showShortToast("公众号:ByteCode" }

// or
view.clickDelayed(lifecycleScope, 1000){ showShortToast("公众号:ByteCode") }

防止多次点击

// 默认间隔时间是 500ms
view.clickTrigger(lifecycleScope){ showShortToast("公众号:ByteCode") }

// or
view.clickTrigger(lifecycleScope, 1000){ showShortToast("公众号:ByteCode") }

但是 View#setOnClickListener 造成的内存泄露,如果做过性能优化的同学应该会见到很多这种 case。

根本原因在于不规范的使用,在做业务开发的时候,根本不会关注这些,那么如何避免这个问题呢,Kotlin Flow 提供了一个非常有用的 API callbackFlow,源码如下所示。

fun View.clickFlow(): Flow<View> {
return callbackFlow {
setOnClickListener {
safeOffer(it)
}
awaitClose { setOnClickListener(null) }
}
}

callbackFlow 正如其名将一个 callback 转换成 flow,awaitClose 会在 flow 结束时执行。

那么 flow 什么时候结束执行

源码中我将 Flow 通过 lifecycleScope 与 Activity / Fragment 的生命周期绑定在一起,在 Activity / Fragment 生命周期结束时,会结束 flow , flow 结束时会将 Listener 置为 null,有效的避免内存泄漏,源码如下所示。

inline fun View.click(lifecycle: LifecycleCoroutineScope, noinline onClick: (view: View) -> Unit) {
clickFlow().onEach {
onClick(this)
}.launchIn(lifecycle)
}

收起阅读 »

iOS研发助手DoraemonKit技术实现(一)

一、前言一个比较成熟的App,经历了多个版本的迭代之后,为了方便调式和测试,往往会积累一些工具来应付这些场景。最近我们组就开源了一款适用于iOS App线下开发、测试、验收阶段,内置在App中的工具集合。使用DoraemonKit,你无需连接电脑,就可以对于A...
继续阅读 »

一、前言

一个比较成熟的App,经历了多个版本的迭代之后,为了方便调式和测试,往往会积累一些工具来应付这些场景。最近我们组就开源了一款适用于iOS App线下开发、测试、验收阶段,内置在App中的工具集合。使用DoraemonKit,你无需连接电脑,就可以对于App的信息进行快速的查看和修改。一键接入、使用方便,提高开发、测试、视觉同学的工作效率,提高我们App上线的完整度和稳定性。

目前DoraemonKit拥有的功能大概分为以下几点:

  1. 常用工具 : App信息展示,沙盒浏览、MockGPS、H5任意门、子线程UI检查、日志显示。
  2. 性能工具 : 帧率监控、CPU监控、内存监控、流量监控、自定义监控。
  3. 视觉工具 : 颜色吸管、组件检查、对齐标尺。
  4. 业务专区 : 支持业务测试组件接入到DoraemonKit面板中。

拿我们App接入效果如下:




面两行是业务线自定义的工具,接入方可以自定义。除此之外都是内置工具集合。

因为里面功能比较多,大概会分三篇文章介绍DoraemonKit的使用和技术实现,这是第一篇主要介绍常用工具集中的几款工具实现。

二、技术实现

2.1:App信息展示




我们要看一些手机信息或者App的一些基本信息的时候,需要到系统设置去找,比较麻烦。特别是权限信息,在我们app装的比较多的时候,我们很难快速找到我们app的权限信息。而这些信息从代码角度都是比较容易获取的。我们把我们感兴趣的信息列表出来直接查看,避免了去手机设置里查看或者查看源代码的麻烦。

获取手机型号

我们从手机设置里面是找不到我们的手机具体是哪一款的文字表述的,比如我的手机是iphone8 Pro,在手机型号里面显示的是MQ8E2CH/A。对于iPhone不熟悉的人很难从外表对iphone进行区分。而手机型号,我们从代码角度就很好获取。


+ (NSString *)iphoneType{
struct utsname systemInfo;
uname(&systemInfo);
NSString *platform = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];

//iPhone
if ([platform isEqualToString:@"iPhone1,1"]) return @"iPhone 1G";
...
//其他对应关系请看下面对应表
return platform;
}

iPhone设备类型与通用手机类型一一对应关系表

设备类型通用类型
iPhone1,1iPhone 1G
iPhone1,2iPhone 3G
iPhone2,1iPhone 3GS
iPhone3,1iPhone 4
iPhone3,2iPhone 4
iPhone4,1iPhone 4S
iPhone5,1iPhone 5
iPhone5,2iPhone 5
iPhone5,3iPhone 5C
iPhone5,4iPhone 5C
iPhone6,1iPhone 5S
iPhone6,2iPhone 5S
iPhone7,1iPhone 6 Plus
iPhone7,2iPhone 6
iPhone8,1iPhone 6S
iPhone8,2iPhone 6S Plus
iPhone8,4iPhone SE
iPhone9,1iPhone 7
iPhone9,3iPhone 7
iPhone9,2iPhone 7 Plus
iPhone9,4iPhone 7 Plus
iPhone10,1iPhone 8
iPhone10.4iPhone 8
iPhone10,2iPhone 8 Plus
iPhone10,5iPhone 8 Plus
iPhone10,3iPhone X
iPhone10,6iPhone X
iPhone11,8iPhone XR
iPhone11,2iPhone XS
iPhone11,4iPhone XS Max
Phone11,6iPhone XS Max

获取手机系统版本

//获取手机系统版本
NSString *phoneVersion = [[UIDevice currentDevice] systemVersion];

获取App BundleId

一个app分为测试版本、企业版本、appStore发售版本,每一个app长得都一样,如何对他们进行区分呢,那就要用到BundleId这个属性了。


//获取bundle id
NSString *bundleId = [[NSBundle mainBundle] bundleIdentifier];

获取App 版本号


//获取App版本号
NSString *bundleVersionCode = [[[NSBundle mainBundle]infoDictionary] objectForKey:@"CFBundleVersion"];

权限信息查看


当我们发现App运行不正常,比如无法定位,网络一直失败,无法收到推送信息等问题的时候,我们第一个反应就是去手机设置里面去看我们app相关的权限有没有打开。DoraemonKit集成了对于地理位置权限、网络权限、推送权限、相机权限、麦克风权限、相册权限、通讯录权限、日历权限、提醒事项权限的查询。

由于代码比较多,这里就不一一贴出来了。大家可以去DorameonKit/Core/Plugin/AppInfo中自己去查看。这里讲一下,权限查询结果几个值的意义。

  • NotDetermined => 用户还没有选择。
  • Restricted => 该权限受限,比如家长控制。
  • Denied => 用户拒绝使用该权限。
  • Authorized => 用户同意使用该权限。

2.2:沙盒浏览




以前如果我们要去查看App缓存、日志信息,都需要访问沙盒。由于iOS的封闭性,我们无法直接查看沙盒中的文件内容。如果我们要去访问沙盒,基本上有两种方式,第一种使用Xcode自带的工具,从Windows-->Devices进入设备管理界面,通过Download Container的方式导出整个app的沙盒。第二种方式,就是自己写代码,访问沙盒中指定文件,然后使用NSLog的方式打印出来。这两种方式都比较麻烦。

DoraemonKit给出的解决方案:就是自己做一个简单的文件浏览器,通过NSFileManager对象对沙盒文件进行遍历,同时支持对于文件和文件夹的删除操作。对于文件支持本地预览或者通过airdrop的方式或者其他分享方式发送到PC端进行更加细致的操作。

怎么用NSFileManager对象遍历文件和删除文件这里就不说了,大家可以参考DorameonKit/Core/Plugin/Sanbox中的代码。这里讲一下:如何将手机中的文件快速上传到Mac端?刚开始我们还绕了一点路,我们在手机端搭了一个微服务,mac通过浏览器去访问它。后来和同事聊天的时候知道了UIActivityViewController这个类,可以十分便捷地吊起系统分享组件或者是其他注册到系统分享组件中的分享方式,比如微信、钉钉。实现代码非常简单,如下所示:


- (void)shareFileWithPath:(NSString *)filePath{

NSURL *url = [NSURL fileURLWithPath:filePath];
NSArray *objectsToShare = @[url];

UIActivityViewController *controller = [[UIActivityViewController alloc] initWithActivityItems:objectsToShare applicationActivities:nil];
NSArray *excludedActivities = @[UIActivityTypePostToTwitter, UIActivityTypePostToFacebook,
UIActivityTypePostToWeibo,
UIActivityTypeMessage, UIActivityTypeMail,
UIActivityTypePrint, UIActivityTypeCopyToPasteboard,
UIActivityTypeAssignToContact, UIActivityTypeSaveToCameraRoll,
UIActivityTypeAddToReadingList, UIActivityTypePostToFlickr,
UIActivityTypePostToVimeo, UIActivityTypePostToTencentWeibo];
controller.excludedActivityTypes = excludedActivities;

[self presentViewController:controller animated:YES completion:nil];
}

2.3:MockGPS




我们有些业务会根据地理位置不同,而有不同的业务处理逻辑。而我们开发或者测试,当然不可能去每一个地址都测试一遍。这种情况下,测试同学一般会找到我们让我们手动改掉系统获取经纬度的回调,或者修改GPX文件,然后再重新打一个包。这样也非常麻烦。

DoraemonKit给出的解决方案:提供一套地图界面,支持在地图中滑动选择或者手动输入经纬度,然后自动替换掉我们App中返回的当前经纬度信息。这里的难点是如何不需要重新打包自动替换掉系统返回的当前经纬度信息?

CLLocationManager的delegate中有一个方法如下:

/*
* locationManager:didUpdateLocations:
*
* Discussion:
* Invoked when new locations are available. Required for delivery of
* deferred locations. If implemented, updates will
* not be delivered to locationManager:didUpdateToLocation:fromLocation:
*
* locations is an array of CLLocation objects in chronological order.
*/

- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations API_AVAILABLE(ios(6.0), macos(10.9));

我们通常是在这个函数中获取当前系统的经纬度信息。我们如果想要没有侵入式的修改这个函数的默认实现方式,想到的第一个方法就是Method Swizzling。但是真正在实现过程中,你会发现Method Swizzling需要当前实例和方法,方法是- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations 我们有了,但是实例,每一个app都有自己的实现,无法做到统一处理。我们就换了一个思路,如何能获取该实现了该定位方法的实例呢?就是使用Method Swizzling Hook住CLLocationManager的setDelegate方法,就能获取具体是哪一个实例实现了- (void)locationManager:(CLLocationManager *)manager
didUpdateLocations:(NSArray *)locations 方法。

具体方法如下:

第一步: 生成一个CLLocationManager的分类CLLocationManager(Doraemon),在这个分类中,实现- (void)doraemon_swizzleLocationDelegate:(id)delegate这个方法,用来进行方法交换。

- (void)doraemon_swizzleLocationDelegate:(id)delegate {
if (delegate) {
//1、让所有的CLLocationManager的代理都设置为[DoraemonGPSMocker shareInstance],让他做中间转发
[self doraemon_swizzleLocationDelegate:[DoraemonGPSMocker shareInstance]];
//2、绑定所有CLLocationManager实例与delegate的关系,用于[DoraemonGPSMocker shareInstance]做目标转发用。
[[DoraemonGPSMocker shareInstance] addLocationBinder:self delegate:delegate];

//3、处理[DoraemonGPSMocker shareInstance]没有实现的selector,并且给用户提示。
Protocol *proto = objc_getProtocol("CLLocationManagerDelegate");
unsigned int count;
struct objc_method_description *methods = protocol_copyMethodDescriptionList(proto, NO, YES, &count);
NSMutableArray *array = [NSMutableArray array];
for(unsigned i = 0; i < count; i++)
{
SEL sel = methods[i].name;
if ([delegate respondsToSelector:sel]) {
if (![[DoraemonGPSMocker shareInstance] respondsToSelector:sel]) {
NSAssert(NO, @"你在Delegate %@ 中所使用的SEL %@,暂不支持,请联系DoraemonKit开发者",delegate,sel);
}
}
}
free(methods);

}else{
[self doraemon_swizzleLocationDelegate:delegate];
}
}


在这个函数中主要做了三件事情,1、将所有的定位回调统一交给[DoraemonGPSMocker shareInstance]处理 2、[DoraemonGPSMocker shareInstance]绑定了所有CLLocationManager与它的delegate的一一对应关系。3、处理[DoraemonGPSMocker shareInstance]没有实现的selector,并且给用户提示。

第二步:当有一个定位回调过来的时候,我们先传给[DoraemonGPSMocker shareInstance],然后[DoraemonGPSMocker shareInstance]再转发给它绑定过的所有的delegate。那我们App为例,绑定关系如下:


{
"0x2800a07a0_binder" = "";
"0x2800a07a0_delegate" = "";
"0x2800b59a0_binder" = "";
"0x2800b59a0_delegate" = "";
}

由此可见,我们App的统一定位KDDriverLocationManager和苹果地图的定位MAMapLocationManager都是使用都是CLLocationManager提供的。

具体 DoraemonGPSMocker这个类如何实现,请参考DorameonKit/Core/Plugin/GPS中的代码。


2.4:H5任意门





有的时候Native和H5开发同时开发一个功能,H5依赖native提供入口,而这个时候Native还没有开发好,这个时候H5开发就没法在App上看到效果。再比如,有些H5页面处于的位置比较深入,就像我们代驾司机端,做单流程比较多,有的H5界面需要很繁琐的操作才能展示到App上,不方便我们查看和定位问题。
这个时候我们可以为app做一个简单的浏览器,输入url,使用自带的容器进行跳转。因为每一个app的H5容器基本上都是自定义过得,都会有自己的bridge定制化,所以这个H5容器没有办法使用系统原生的UIWebView或者WKWebView,就只能交给业务方自己去完成。我们在DorameonKit初始化的时候,提供了一个回调让业务方用自己的H5容器去打开这个Url:

[[DoraemonManager shareInstance] addH5DoorBlock:^(NSString *h5Url) {
//使用自己的H5容器打开这个链接
}];

这个工具实现比较简单,就不多说了,代码路径在DorameonKit/Core/Plugin/H5.


2.5:子线程UI检查






在iOS中是不允许在子线程中对UI进行操作和渲染的,不然会造成未知的错误和问题,甚至会导致crash。我们在最近几个版本中发现新增了一些crash,调查原因就是在子线程中操作UI导致的。为了对于这种情况可以提早被我们发现,我在在DorameonKit中增加了子线程UI渲染检查查询。

具体事项思路,我们hook住UIView的三个必须在主线程中操作的绘制方法。1、setNeedsLayout 2、setNeedsDisplay 3、setNeedsDisplayInRect:。然后判断他们是不是在子线程中进行操作,如果是在子线程进行操作的话,打印出当前代码调用堆栈,提供给开发进行解决。具体代码如下:

@implementation UIView (Doraemon)

+ (void)load{
[[self class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsLayout) swizzledSel:@selector(doraemon_setNeedsLayout)];
[[self class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsDisplay) swizzledSel:@selector(doraemon_setNeedsDisplay)];
[[self class] doraemon_swizzleInstanceMethodWithOriginSel:@selector(setNeedsDisplayInRect:) swizzledSel:@selector(doraemon_setNeedsDisplayInRect:)];
}

- (void)doraemon_setNeedsLayout{
[self doraemon_setNeedsLayout];
[self uiCheck];
}

- (void)doraemon_setNeedsDisplay{
[self doraemon_setNeedsDisplay];
[self uiCheck];
}

- (void)doraemon_setNeedsDisplayInRect:(CGRect)rect{
[self doraemon_setNeedsDisplayInRect:rect];
[self uiCheck];
}

- (void)uiCheck{
if([[DoraemonCacheManager sharedInstance] subThreadUICheckSwitch]){
if(![NSThread isMainThread]){
NSString *report = [BSBacktraceLogger bs_backtraceOfCurrentThread];
NSDictionary *dic = @{
@"title":[DoraemonUtil dateFormatNow],
@"content":report
};
[[DoraemonSubThreadUICheckManager sharedInstance].checkArray addObject:dic];
}
}
}

@end

完整代码实现请参考DorameonKit/Core/Plugin/SubThreadUICheck


2.6:日志显示



这个主要是方便我们查看本地日志,以前我们如果要查看日志,需要自己写代码,访问沙盒导出日志文件,然后再查看。也是比较麻烦的。

DoraemonKit的解决方案是:我们每一次触发日志的时候,都把日志内容显示到界面上,方便我们查看。
如何实现的呢?因为我们这个工具并不是一个通用性的工具,只针对于底层日志库是CocoaLumberjack的情况。稍微讲一下的CocoaLumberjack原理,所有的log都会发给DDLog对象,其运行在自己的一个GCD队列中,之后,DDLog会将log分发给其下注册的一个或者多个Logger中,这一步在多核下面是并发的,效率很高。每一个Logger处理收到的log也是在它们自己的GCD队列下做的,它们询问其下的Formatter,获取Log消息格式,然后根据Logger的逻辑,将log消息分发到不同的地方。系统自带三个Logger处理器,DDTTYLogger,主要将日志发送到Xcode控制台;DDASLLogger,主要讲日志发送到苹果的日志系统Console.app; DDFileLogger,主要将日志发送到文件中保存起来,也是我们开发用到最多的。但是自带的Logger并不满足我们的需求,我们的需求是将日志显示到UI界面中,所以我们需要新建一个类DoraemonLogger,继承于DDAbstractLogger,然后重写logMessage方法,将每一条传过来的日志打印到UI界面中。





这个工具参考LumberjackConsole这个开源项目完成,因为刚出iOS11的时候,作者没有适配,所以我们自己拷贝一份代码出来,自己维护了。 完整代码实现请参考DorameonKit/WithLogger中.




作者:景铭巴巴
链接:https://www.jianshu.com/p/00763123dbc4







收起阅读 »

iOS - 极其强大的性能库DoraemonKit

每一个稍微有点规模的 App,总会自带一些线下的测试功能代码,比如环境切换功能、帧率查看功能等等,这些功能的切换入口往往放在各式各样的入口中,比如一些特殊的手势,双击 statusBar,双击某一个功能区块,或者新建一个 keyWindow 始终至于 App ...
继续阅读 »


每一个稍微有点规模的 App,总会自带一些线下的测试功能代码,比如环境切换功能、帧率查看功能等等,这些功能的切换入口往往放在各式各样的入口中,比如一些特殊的手势,双击 statusBar,双击某一个功能区块,或者新建一个 keyWindow 始终至于 App 最上方等等,而且每一个 App 里面的线下附带功能模块很多是相似的,比如帧率查看、内存和 CPU 监控等等,但是现在基本上都是每个 App 都是自己实现了一份,经历了以上的问题之后,DoKit 就有了它存在的意义。

DoKit 是一个功能平台,能够让每一个 App 快速接入一些常用的或者你没有实现的一些辅助开发工具、测试效率工具、视觉辅助工具,而且能够完美在 Doraemon 面板中接入你已经实现的与业务紧密耦合的一些非通有的辅助工具,并搭配我们的dokit平台,让功能得到延伸,接入方便,便于扩展。


一、平台工具(http://www.dokit.cn)

  1. 【数据Mock】 App接口Mock解决方案,提供一套基于App网络拦截的接口Mock方案,无需修改代码即可完成对于接口数据的Mock。
  2. 【健康体检】 一键式操作,整合DoKit多项工具,数据可视化,快速准确定位问题,让你对app的性能了如指掌。
  3. 【文件同步助手】 通过终端服务,让你的终端空间在平台端完整的展现并提供强大的文件以及数据库操作能力。
  4. 【一机多控】 主从同步,释放人力,让研发测试效率提升看得见

二、常用工具

  1. 【App 信息查看】 快速查看手机信息,App 基础信息、签名相关、权限信息的渠道,避免去手机设置查找或者查看项目源代码的麻烦;
  2. 【开发者选项 Android特有】 一键跳转开发者选项,避免安卓由于平台差异导致的入口不一致
  3. 【本地语言】 一键跳转本地语言,避免安卓由于平台差异导致的入口不一致
  4. 【沙盒浏览】 App 内部文件浏览的功能,支持删除和预览, 并且能通过 AirDrop 或者其他分享方式上传到 PC 中,进行更加细致的操作;
  5. 【MockGPS】 App 能定位到全国各地,支持地图地位和手动输入经纬度;
  6. 【H5任意门】 开发测试同学可以快速输入 H5 页面地址,查看该页面效果;
  7. 【Crash查看】 方便本地打印出出现 Crash 的堆栈;
  8. 【子线程UI】 快速定位哪一些 UI 操作在非主线程中进行渲染,避免不必要的问题;(iOS独有)
  9. 【清除本地数据】 一键删除沙盒中所有数据;
  10. 【NSLog】 把所有 NSLog 信息打印到UI界面,避免没有开发证书无法调试的尴尬;
  11. 【Lumberjack】 每一条 CocoaLumberjack 的日志信息,都在在 App 的界面中显示出来,再也不需要导出日志这么麻烦;(iOS独有)
  12. 【DBView】 通过网页方便快捷的操作应用内数据库,让数据库的调试变得非常优雅;
  13. 【模拟弱网】 限制网速,模拟弱网环境下App的运行情况。(android独有)

三、性能检测

  1. 【帧率】 App 帧率信息提供波形图查看功能,让帧率监控的趋势更加明显;
  2. 【CPU】 App CPU 使用率信息提供波形图查看功能,让 CPU 监控的趋势更加形象;
  3. 【内存】 App 内存使用量信息提供波形图查看功能,让内存监控的趋势更加鲜明;
  4. 【流量监控】 拦截 App 内部流量信息,提供波形图展示、流量概要展示、流量列表展示、流量筛选、流量详情,对流量信息统一拦截,成为我们 App 中自带的 "Charles";
  5. 【卡顿】 锁定 App 出现卡顿的时刻,打印出对应的代码调用堆栈;
  6. 【大图检测】 通过流量监测,找出所有的大小超标的图片,避免下载大图造成的流量浪费和渲染大图带来的CPU消耗。
  7. 【启动耗时】 无侵入的统计出App启动过程的总共耗时;
  8. 【UI层级检查】 检查出每一个页面中层级最深的元素;
  9. 【函数耗时】 从函数级别分析app性能瓶颈;
  10. 【Load】 找出所有的Load方法,并给出耗时分析;(iOS独有)
  11. 【内存泄漏】 找出App中所有的内存泄漏的问题。

四、视觉工具

  1. 【颜色吸管】 方便设计师 UI 捉虫的时候,查看每一个组件的颜色值是否设置正确;
  2. 【组件检查】 可以抓取任意一个UI控件,查看它们的详细信息,包括控件名称、控件位置、背景色、字体颜色、字体大小;
  3. 【对齐标尺】 参考 Android 系统自带测试工具,能够实时捕获屏幕坐标,并且可以查看组件是否对齐;
  4. 【元素边框线】 绘制出每一个 UI 组件的边框,对于组件布局有一定的参考意义。

五、Weex专项工具(CML专项工具)

  1. 【console日志查看】 方便在端上查看每一个Weex文件中的console日志,提供分级和搜索功能;
  2. 【storage缓存查看】 将Weex中的storage模块的本地缓存数据可视化展示;
  3. 【容器信息】 查看每一个打开的Weex页面的基本信息和性能数据;
  4. 【DevTool】 快速开启Weex DevTool的扫码入口。

tips : 如果使用我们滴滴优秀的开源跨端方案 chameleon 也可以集成该工具集合

六、支持自定义的业务工具集成到面板中

统一维护和管理所有的测试模块,详见接入手册

七、微信小程序专项工具


收起阅读 »

微信开源框架-崩溃、卡顿和爆内存Matrix

当前工具监控范围包括:崩溃、卡顿和爆内存,包含以下两款插件:WCCrashBlockMonitorPlugin: 基于 KSCrash 框架开发,具有业界领先的卡顿堆栈捕获能力,同时兼备崩溃捕获能力。WCMemoryStatPlu...
继续阅读 »

当前工具监控范围包括:崩溃、卡顿和爆内存,包含以下两款插件:

  • WCCrashBlockMonitorPlugin: 基于 KSCrash 框架开发,具有业界领先的卡顿堆栈捕获能力,同时兼备崩溃捕获能力。

  • WCMemoryStatPlugin: 一款性能优化到极致的爆内存监控工具,能够全面捕获应用爆内存时的内存分配以及调用堆栈情况。

特性

WCCrashBlockMonitorPlugin

  • 接入简单,代码无侵入
  • 通过检查 Runloop 运行状态判断应用是否卡顿,同时支持 iOS/macOS 平台
  • 增加耗时堆栈提取,卡顿线程快照日志中附加最近时间最耗时的主线程堆栈

WCMemoryStatPlugin

  • 在应用运行期间获取对象存活以及相应的堆栈信息,在检测到应用爆内存时进行上报
  • 使用平衡二叉树存储存活对象,使用 Hash Table 存储堆栈,将性能优化到极致

使用方法

安装

  • 通过 Cocoapods 安装

    1. 先安装 CocoaPods
    2. 通过 pod repo update 更新 matrix 的 Cocoapods 版本;
    3. 在 Podfile 对应的 target 中,添加 pod 'matrix-wechat',并执行 pod install;
    4. 在项目中使用 Cocoapods 生成的 .xcworkspace运行工程;
    5. 在你的代码文件头引入头文件 #import ,就可以接入微信的性能探针工具了!
  • 通过静态库安装

    1. 获取 Matrix 源码;
    2. 打开命令行,在 matrix/matrix-iOS 代码目录下执行 make 进行编译生成静态库;编译完成后,iOS 平台的库在 matrix/matrix-iOS/build_ios 目录下,macOS 平台的库在 matrix/matrix-iOS/build_macos目录下;
    3. 工程引入静态库:
    • iOS 平台:使用 matrix/matrix-iOS/build_ios 路径下的 Matrix.framework,将 Matrix.framework以静态库的方式引入工程;
    • macOS 平台:使用 matrix/matrix-iOS/build_macos 路径下的 Matrix.framework,将 Matrix.framework 以静态库的方式引入工程。
    1. 添加头文件 #import ,就可以接入微信的性能探针工具了!

启动监控

在以下地方:

  • 程序 main 函数入口;
  • AppDelegate 中的 application:didFinishLaunchingWithOptions:
  • 或者其他应用启动比较早的时间点。

添加类似如下代码,启动插件:

#import 

Matrix *matrix = [Matrix sharedInstance];
MatrixBuilder *curBuilder = [[MatrixBuilder alloc] init];
curBuilder.pluginListener = self; // pluginListener 回调 plugin 的相关事件

WCCrashBlockMonitorPlugin *crashBlockPlugin = [[WCCrashBlockMonitorPlugin alloc] init];
[curBuilder addPlugin:crashBlockPlugin]; // 添加卡顿和崩溃监控

WCMemoryStatPlugin *memoryStatPlugin = [[WCMemoryStatPlugin alloc] init];
[curBuilder addPlugin:memoryStatPlugin]; // 添加内存监控功能

[matrix addMatrixBuilder:curBuilder];

[crashBlockPlugin start]; // 开启卡顿和崩溃监控
// [memoryStatPlugin start];
// 开启内存监控,注意 memoryStatPlugin 开启之后对性能损耗较大,建议按需开启

接收回调获得监控数据

设置 MatrixBuilder 对象中的 pluginListener,实现 MatrixPluginListenerDelegate。

// 设置 delegate

MatrixBuilder *curBuilder = [[MatrixBuilder alloc] init];
curBuilder.pluginListener = <一个遵循 MatrixPluginListenerDelegate 的对象>;

// MatrixPluginListenerDelegate

- (void)onInit:(id)plugin;
- (void)onStart:(id)plugin;
- (void)onStop:(id)plugin;
- (void)onDestroy:(id)plugin;
- (void)onReportIssue:(MatrixIssue *)issue;

各个添加到 MatrixBuilder 的 plugin 会将对应的事件通过 pluginListener 回调。

重要:通过 onReportIssue: 获得 Matrix 处理后的数据,监控数据格式详见:Matrix for iOS/macOS 数据格式说明

Demo

至此,Matrix 已经集成到应用中并且开始收集崩溃、ANR、卡顿和爆内存数据,如仍有疑问,请查看示例:samples/sample-apple/MatrixDemo


常见问题及源码下载:https://github.com/Tencent/matrix#matrix_ios_cn



收起阅读 »

iOS 界面优化

卡顿原因计算机通过CPU、GPU、显示器三者协同工作将试图显示到屏幕上1、CPU将需要显示的内容计算出来,提交到GPU2、GPU将内容渲染完成后将渲染后的内容存放到FrameBuffer(帧缓冲区)3、视频控制器根据VSync(垂直同步)信号来读取FrameB...
继续阅读 »

卡顿原因

计算机通过CPUGPU显示器三者协同工作将试图显示到屏幕上

  • 1、CPU将需要显示的内容计算出来,提交到GPU
  • 2、GPU将内容渲染完成后将渲染后的内容存放到FrameBuffer(帧缓冲区)
  • 3、视频控制器根据VSync(垂直同步)信号来读取FrameBuffer中的数据
  • 4、将转换的数模传递给显示器显示


iOS设备中采用双缓存区+VSync

在收到VSync信号后,系统的图形服务通过CADisplayLink等机制通知App,在主程序中调度CPU计算显示的内容,随后将计算好的内容提交到GPU变换、合成、渲染,GPU将渲染结果提交帧缓冲区,等待下一个VSync信号到来时显示到屏幕上。由于垂直同步机制的原因,如果再一个VSync时间内,CPU或者GPU没有完成内容的处理,就会导致当前处理的帧丢弃,此时屏幕会保持上一帧的显示,造成掉帧


卡顿检测

  • FPS监控:因为iOS设备屏幕的刷新时间是60次/秒,一次刷新就是一次VSync信号,时间间隔是1000ms/60 = 16.67ms,所有如果咋16.67ms内下一帧数据没有准备好,就会产生掉帧
  • RunLoop监控:通过子线程检测主线程的RunLoop的状态,kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting两个状态之间的耗时是否达到一定的阈值

FPS监控

参照YYKit中的YYFPSLabel,其中通过CADisplayLink来实现,通过刷新次数/时间差得到刷新频率


class YPFPSLabel: UILabel {

fileprivate var link: CADisplayLink = {
let link = CADisplayLink.init()
return link
}()

fileprivate var count: Int = 0
fileprivate var lastTime: TimeInterval = 0.0
fileprivate var fpsColor: UIColor = {
return UIColor.green
}()
fileprivate var fps: Double = 0.0

override init(frame: CGRect) {
var f = frame
if f.size == CGSize.zero {
f.size = CGSize(width: 80.0, height: 22.0)
}

super.init(frame: f)

self.textColor = UIColor.white
self.textAlignment = .center
self.font = UIFont.init(name: "Menlo", size: 12)
self.backgroundColor = UIColor.lightGray
//通过虚拟类
link = CADisplayLink.init(target: CJLWeakProxy(target:self), selector: #selector(tick(_:)))
link.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

deinit {
link.invalidate()
}

@objc func tick(_ link: CADisplayLink){
guard lastTime != 0 else {
lastTime = link.timestamp
return
}

count += 1
//时间差
let detla = link.timestamp - lastTime
guard detla >= 1.0 else {
return
}

lastTime = link.timestamp
//刷新次数 / 时间差 = 刷新频次
fps = Double(count) / detla
let fpsText = "\(String.init(format: "%.2f", fps)) FPS"
count = 0

let attrMStr = NSMutableAttributedString(attributedString: NSAttributedString(string: fpsText))
if fps > 55.0 {
//流畅
fpsColor = UIColor.green
}else if (fps >= 50.0 && fps <= 55.0){
//一般
fpsColor = UIColor.yellow
}else{
//卡顿
fpsColor = UIColor.red
}

attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: fpsColor], range: NSMakeRange(0, attrMStr.length - 3))
attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], range: NSMakeRange(attrMStr.length - 3, 3))

DispatchQueue.main.async {
self.attributedText = attrMStr
}
}

}

RunLoop监控

开辟子线程,通过监听主线程的kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting两个Activity之间的差值


#import "YPBlockMonitor.h"

@interface YPBlockMonitor (){
CFRunLoopActivity activity;
}

@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) NSUInteger timeoutCount;

@end

@implementation YPBlockMonitor

+ (instancetype)sharedInstance {
static id instance = nil;
static dispatch_once_t onceToken;

dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}

- (void)start{
[self registerObserver];
[self startMonitor];
}

static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
LGBlockMonitor *monitor = (__bridge LGBlockMonitor *)info;
monitor->activity = activity;
// 发送信号
dispatch_semaphore_t semaphore = monitor->_semaphore;
dispatch_semaphore_signal(semaphore);
}

- (void)registerObserver{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
//NSIntegerMax : 优先级最小
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
NSIntegerMax,
&CallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}

- (void)startMonitor{
// 创建信号
_semaphore = dispatch_semaphore_create(0);
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 所有的任务
long st = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
if (st != 0)
{
if (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)
{
if (++self->_timeoutCount < 2){
NSLog(@"timeoutCount==%lu",(unsigned long)self->_timeoutCount);
continue;
}
// 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
NSLog(@"检测到超过两次连续卡顿");
}
}
self->_timeoutCount = 0;
}
});
}

@end

界面优化

UIView和CALayer的关系

  • UIView是基于UIKit框架,继承自UIResponder,可以处理事件,管理子视图
  • CALayer是基于CoreAnimation的,继承自NSObject,只负责显示,不能处理事件
  • UIKit组件最终都会分解为layer,存储到图层树
  • UIView中的部分属性,frame、bounds、transform等,来自CALayer的映射
  • CALayer内部没有属性,在调用属性时,内部通过运行时resolveInstanceMethod方法为对象临时添加一个方法,并将对应属性值保存到内部的Dictionary,同时通知delegate、创建动画等

CPU层面的优化

  • 1、对于不需要触摸的控件使用CALayer代替UIView

  • 2、减少UIViewCALayer的属性修改

  • 3、大量对象释放时,移动到后台线程释放

  • 4、预排版:在异步子线程中提前计算好视图的大小

  • 5、Autolayout在简单页面情况下们可以很好的提升开发效率,但是对于复杂视图而言,会产生严重的性能问题,随着视图数量的增长,Autolayout带来的CPU消耗是呈指数上升的。所以尽量使用代码布局

  • 6、文本处理

    • 对于文本没有特殊要求的,可以使用UILabel内部实现方法,放在子线程中执行
      • 计算文本宽高:[NSAttributedString boundingRectWithSize:options:context:]
      • 文本绘制:[NSAttributedString drawWithRect:options:context:]
    • 使用自定义文本控件,通过TextKit或者CoreText进行异步文本绘制。CoreText对象创建后,可以直接获取文本宽高等信息。CoreText直接使用了CoreGraphics占用内存小,效率高
  • 7、图片优化
    在使用UIImage或者CGImageSource方法创建图片时,图片数据不会立即解码,而是在设置到UIImageView/CALayer.contents中,然后由CALayer提交到GPU渲染前才在主线程进行解码,可以参考SDWebImage中对图片的处理,在子线程中先将图片绘制到CGBitmapContext,然后从Bitmap直接创建图片

    • 使用PNG图片,而非JPGE图片
    • 子线程中解码,主线程渲染,即通过Bitmap创建图片,在子线程赋值image
    • 优化图片大小,避免动态缩放
    • 多图合成一张图片显示
  • 8、避免使用透明View,会导致GPU在计算像素时,会将下层图层的像素也计算进来,颜色混合处理

  • 9、按需加载:例如通过RunLoop分发任务,ScrollView滚动时不加载

  • 10、少使用addView 给cell动态添加view

GPU层面优化

GPU主要是接收CPU提交的纹理+顶点,经过一系列transform,最终混合并渲染,输出到屏幕上
1、避免短时间显示大量图片,可以将多张图片合成一张
2、控制图片尺寸不超过4096x4096,因为图片超过这个尺寸,CPU会先进行预处理再提交给GPU
3、减少视图层级和数量
4、避免离屏渲染
5、异步渲染,例如可以将cell中的所有控件、视图合成一张位图进行显示



作者:木扬音
链接:https://www.jianshu.com/p/2f9a06932879

收起阅读 »

iOS开发制作同时支持armv7,armv7s,arm64,i386,x86_64的静态库.a

iOS
一、概要平时项目开发中,可能使用第三方提供的静态库.a,如果.a提供方技术不成熟,使用的时候就会出现问题,例如:在真机上编译报错:No architectures to compile for (ONLY_ACTIVE_ARCH=YES, active arc...
继续阅读 »

一、概要

平时项目开发中,可能使用第三方提供的静态库.a,如果.a提供方技术不成熟,使用的时候就会出现问题,例如:

在真机上编译报错:No architectures to compile for (ONLY_ACTIVE_ARCH=YES, active arch=x86_64, VALID_ARCHS=i386).

在模拟器上编译报错:No architectures to compile for (ONLY_ACTIVE_ARCH=YES, active arch=armv7s, VALID_ARCHS=armv7 armv6).

要解决以上问题,就要了解一下Apple移动设备处理器指令集相关的一些细节知识。

 

二、几个重要概念

1、ARM

ARM处理器,特点是体积小、低功耗、低成本、高性能,所以几乎所有手机处理器都基于ARM,在嵌入式系统中应用广泛。

 

2、ARM处理器指令集

armv6|armv7|armv7s|arm64都是ARM处理器的指令集,这些指令集都是向下兼容的,例如armv7指令集兼容armv6,只是使用armv6的时候无法发挥出其性能,无法使用armv7的新特性,从而会导致程序执行效率没那么高。

还有两个我们也很熟悉的指令集:i386|x86_64 是Mac处理器的指令集,i386是针对intel通用微处理器32架构的。x86_64是针对x86架构的64位处理器。所以当使用iOS模拟器的时候会遇到i386|x86_64,iOS模拟器没有arm指令集。

 

3、目前iOS移动设备指令集

arm64:iPhone5S| iPad Air| iPad mini2(iPad mini with Retina Display)

armv7s:iPhone5|iPhone5C|iPad4(iPad with Retina Display)

armv7:iPhone3GS|iPhone4|iPhone4S|iPad|iPad2|iPad3(The New iPad)|iPad mini|iPod Touch 3G|iPod Touch4

armv6 设备: iPhone, iPhone2, iPhone3G, 第一代、第二代 iPod Touch(一般不需要去支持)

 

4、Xcode中指令集相关选项(Build Setting中)

(1)Architectures

Space-separated list of identifiers. Specifies the architectures (ABIs, processor models) to which the binary is targeted. When this build setting specifies more than one architecture, the generated binary may contain object code for each of the specified
architectures.

指定工程被编译成可支持哪些指令集类型,而支持的指令集越多,就会编译出包含多个指令集代码的数据包,对应生成二进制包就越大,也就是ipa包会变大。

(2)Valid Architectures

Space-separated list of identifiers. Specifies the architectures for which the binary may be built. During the build, this list is intersected with the value of ARCHS build setting; the resulting list specifies the architectures the binary can run on. If
the resulting architecture list is empty, the target generates no binary.

限制可能被支持的指令集的范围,也就是Xcode编译出来的二进制包类型最终从这些类型产生,而编译出哪种指令集的包,将由Architectures与Valid Architectures(因此这个不能为空)的交集来确定,例如:
比如,你的Valid Architectures设置的支持arm指令集版本有:armv7/armv7s/arm64,对应的Architectures设置的支持arm指令集版本有:armv7s,这时Xcode只会生成一个armv7s指令集的二进制包。

再比如:将Architectures支持arm指令集设置为:armv7,armv7s,对应的Valid Architectures的支持的指令集设置为:armv7s,arm64,那么此时,XCode生成二进制包所支持的指令集只有armv7s

 

在Xcode6.1.1里的 Valid Architectures  设置里, 默认为 Standard architectures(armv7,arm64),如果你想改的话,自己在other中更改。

原因解释如下:
使用 standard architectures (including 64-bit)(armv7,arm64) 参数,则打的包里面有32位、64位两份代码,在iPhone5s( iPhone5s的cpu是64位的 )下,会首选运行64位代码包, 其余的iPhone( 其余iPhone都是32位的,iPhone5c也是32位 ),只能运行32位包,但是包含两种架构的代码包,只有运行在ios6,ios7系统上。
这也就是说,这种打包方式,对手机几乎没要求,但是对系统有要求,即ios6以上。
而使用 standard architectures (armv7,armv7s) 参数, 则打的包里只有32位代码, iPhone5s的cpu是64位,但是可以兼容32位代码,即可以运行32位代码。但是这会降低iPhone5s的性能。 其余的iPhone对32位代码包更没问题, 而32位代码包,对系统也几乎也没什么限制。
所以总结如下:

要发挥iPhone5s的64位性能,就要包含64位包,那么系统最低要求为ios6。 如果要兼容ios5以及更低的系统,只能打32位的包,系统都能通用,但是会丧失iPhone5s的性能。

(3)Build Active Architecture Only

指定是否只对当前连接设备所支持的指令集编译

当其值设置为YES,这个属性设置为yes,是为了debug的时候编译速度更快,它只编译当前的architecture版本,而设置为no时,会编译所有的版本。 编译出的版本是向下兼容的,连接的设备的指令集匹配是由高到低(arm64 > armv7s > armv7)依次匹配的。比如你设置此值为yes,用iphone4编译出来的是armv7版本的,iphone5也可以运行,但是armv6的设备就不能运行。  所以,一般debug的时候可以选择设置为yes,release的时候要改为no,以适应不同设备。

1)

Architectures:  armv7, armv7s, arm64
ValidArchitectures:  armv6, armv7s, arm64
生成二进制包支持的指令集: arm64

2)

Architectures: armv6, armv7, armv7s
Valid Architectures:  armv6, armv7s, arm64
生成二进制包支持的指令集: armv7s

3)

Architectures: armv7, armv7s, arm64
Valid Architectures: armv7,armv7s

这种情况是报错的,因为允许使用指令集中没有arm64。

注:如果你对ipa安装包大小有要求,可以减少安装包的指令集的数量,这样就可以尽可能的减少包的大小。当然这样做会使部分设备出现性能损失,当然在普通应用中这点体现几乎感觉不到,至少不会威胁到用户体检。

 

三、制作静态库.a是指令集选择

现在回归到正题,如何制作一个“没有问题”的.a静态库,通过以上信息了解到,当我们做App的时候,为了追求高效率,并且减小包的大小,Build Active Architecture Only设置成YES,Architectures按Xcode默认配置就可以,因为arm64向前兼容。但制作.a静态库就不同了,因为要保证兼容性,包括不同iOS设备以及模拟器运行不出错,所以结合当前行业情况,要做到最大的兼容性。

ValidArchitectures设置为:armv7|armv7s|arm64|i386|x86_64

Architectures设置不变(或根据你需要):  armv7|arm64

然后分别选择iOS设备和模拟器进行编译,最后找到相关的.a进行合包,使用lipo -create 真机库.a的路径 模拟器库.a的的路径 -output 合成库的名字.a(详情可以参考http://blog.csdn.net/lizhongfu2013/article/details/12648633)

这样就制作了一个通用的静态库.a

链接:https://www.jishudog.com/30423/html

收起阅读 »

从XML到View显示在屏幕上,都发生了什么

前言 View绘制可以说是Android开发的必备技能,但是关于View绘制的的知识点也有些繁杂。 如果我们从头开始阅读源码,往往千头万绪,抓不住要领。 目前当我们写页面时,布局都是写在XML里的,我们可以思考下:布局从XML到显示到屏幕上,都发生了什么,可...
继续阅读 »

前言


View绘制可以说是Android开发的必备技能,但是关于View绘制的的知识点也有些繁杂。
如果我们从头开始阅读源码,往往千头万绪,抓不住要领。
目前当我们写页面时,布局都是写在XML里的,我们可以思考下:布局从XML到显示到屏幕上,都发生了什么,可以分为哪几个部分?
我们将整个显示流程分解为以下几个部分



  1. 代码是怎么从XML转换成View的?

  2. View是怎么添加到页面上的?

  3. 在内存中View到底是怎么绘制的?

  4. View绘制完成后是怎么显示到屏幕上的?


本文目录如下所示:


1. XML是怎么转换成View的?


我们都知道,在android中写布局一般是通过XML,然后通过setContentView方法配置到页面中
看来XML转换成View就是在这个setContentView中了


1.1 setContentView中做了什么


    public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}

可以看到resId传给了我们熟悉的LayoutInflater,看来xml转化成View就是在LayoutInflater方法中实现的了


1.2 LayoutInflater中做了什么?


    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
//预编译直接返回view,目前还未启用
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
//真正将`XML`转化为`View`
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}

代码也比较简单,我们一起来分析下



  1. 首先我们需要明确,将XML转化为View牵涉到一些耗时操作,比如XML解析是一个io操作,将XML转化为View涉及到反射,这也是耗时的

  2. 我们可以看到在解析前有个tryInflatePrecompiled方法,这个方法就是希望可以在编译阶段直接预编译XML,在运行时直接返回构建好的View,看起来Google希望通过这种方式解决XML的性能问题。不过这个功能目前还没有启用,因此此方法直接返回null,目前生效的还是下面的方法

  3. 真正将XML解析为View的还是在inflate方法中,将标签名转化为View的名称,XML中的各种属性转化为AttributeSet对象,然后通过反射生成View对象


由于篇幅原因,这里就不再粘贴inflate方法的源码了,里面主要需要注意下setFactorysetFactory2方法
在真正进行反射前,会先调用这两个方法尝试创建一下View,而且系统开放了API,我们可以自定义解析XML方式
这就给了我们一些面向切面编程的空间,可以利用这两个API实现换肤,替换字体,替换 View,提升View构建速度等操作
希望进一步了解的同学可参考:探究 LayoutInflater setFactory


1.3 小结


XML转化为View转化为主要是通过LayoutInflator来完成的,将标签名转化为View的名称,XML中的各种属性转化为AttributeSet对象,然后通过反射生成View对象
这个过程中存在一些耗时操作,比如解析XMLIO操作,通过反射生成View等,我们可以通过多种方式优化这个过程,比如将反向的耗时转移到编译期,有兴趣的同学可以参阅:Android "退一步"的布局加载优化


2. View是怎么添加到页面上的?


经过上面这步,View已经被创建出来了,但是View又是怎么添加到页面(Activity)上的呢?
我们再来看下setContentView方法


    public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}

LayoutInflater有两个参数,第二个参数就是root,即创建出的view要被添加的父view
所以答案也就呼之欲出了,创建出来的view被添加到了contentParent上,即R.id.content
那么问题来了,这个R.id.content是哪来的呢?


2.1 R.id.content从何而来?


我们看到,setContentView开头调用了ensureSubDecor方法,一起来看下它的源码


    private void ensureSubDecor() {
if (!mSubDecorInstalled) {
mSubDecor = createSubDecor();
}
}
private ViewGroup createSubDecor() {
// Now let's make sure that the Window has installed its decor by retrieving it
ensureWindow();
mWindow.getDecorView();

final LayoutInflater inflater = LayoutInflater.from(mContext);
ViewGroup subDecor = null;

//省略其他样式subDecor布局的实例化
//包含 actionBar floatTitle ActionMode等样式
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(R.id.action_bar_activity_content);

final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
// 把`contentView`的id设置为android.R.id.content,把windowContentView的id设置为View.NO_ID
windowContentView.setId(View.NO_ID);
contentView.setId(android.R.id.content);

//将subDecor添加到window
mWindow.setContentView(subDecor);
return subDecor;
}

可以看出,主要工作是创建subDecor并添加到window



  • 步骤一:确认windowattach(设置背景等操作)

  • 步骤二:获取DecorView,因为是第一次调用所以会installDecor(创建DecorViewwindowContentView)

  • 步骤三:从xml中实例化出subDecor布局

  • 步骤四:将subDecorcontentViewid设置为R.id.content

  • 步骤四:将subDecor添加到window


现在我们已经知道R.id.content从何而来了,并且知道了subDecor最终会添加到window
那么问题来了,window又是什么呢?


2.2 window到底是什么?


我们上文提到,我们创建的view会被添加到subDecor上,最后会被添加到window中,那么window是什么?为什么要有window?


我们在应用中有多个页面,手机上也有多个应用,这么多页面同时只能有一个页面显示在手机上,这个时候就需要有一个机制来管理当前显示哪个页面
于是Android在系统进程中创建了一个系统服务WindowManagerService(WMS)专门用来管理屏幕上的窗口,而View只能显示在对应的窗口上,如果不符合规定就不开辟窗口进而对应的View也无法显示



window机制就是为了管理屏幕上的view的显示以及触摸事件的传递问题



值得注意的事,上面的window窗口很容易混淆,Android SDK中的Window是一个抽象类,它有一个唯一实现类PhoneWindowPhoneWindow内部会持有一个DecorView(根View),它的职责就是对DecorView做一些标准化的处理,比如标题、背景、导航栏、事件中转等,很显然与我们前面所说的窗口概念不符合


总得来说PhoneWindow只是提供些标准的UI方案,与窗口不等价
窗口是一个抽象概念,即当前应该显示哪个页面,系统通过WindowManagerService(WMS)来管理
关于窗口机制,想了解更加详细的同学,可参考:通俗易懂 Android视图系统的设计与实现,写得非常通俗易懂,有兴趣的可以了解下


2.3 View什么时候真正可见?


上面提到PhoneWindow只是提供些标准的UI方案,并不是真正的窗口
那么我们的View到底什么时候添加到窗口上,什么时候真正对用户可见?


#ActivityThread
public void handleResumeActivity(...) {
//...
//注释1
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
...
//注释2
wm.addView(decor, l);
...
}

#ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
//记录DecorView
mView = view;
//省略
//开启View的三大流程(measure、layout、draw)
requestLayout();
try {
//添加到WindowManagerService里,这里是真正添加window到底层
//这里的返回值判断window是否成功添加,权限判断等。
//比如用Application的context开启dialog,这里会添加不成功
// 注释3
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
setFrame(mTmpFrame);
} catch (RemoteException e) {
}
//省略
//输入事件接收
}
}
}



  1. 注释1处会从Activity中取出PhoneWindow,DecorView,WindowManager

  2. 注释2处调用了WindowManageraddView方法,顾名思义就是将DecorView添加至窗口当中

  3. 最后会调到ViewRootImpl中注释3处,这里才是真正的通过WMS在屏幕上开辟一个窗口,到这一步我们的View也就可以显示到屏幕上了


可以看出,当我们打开一个Activity时,界面真正可见是在onResume之后


2.4 Activity,PhoneWindow,View的关系



  1. Phonewindowactivity 的一个成员变量,会在Activity.attatch时初始化

  2. PhoneWindowView的容器,对DecorView做一些标准化的处理,比如标题、背景、导航栏、事件中转等

  3. Activity则提供了窗口的生命周期,屏蔽了窗口机制的复杂细节,开发者只需要基于模板方法开发即可


如下图所示


2.5 小结


View添加到页面上,主要经过了这么几个过程



  • 1.启动activity

  • 2.创建PhoneWindow

  • 3.设置布局setContentView,将layoutId转化为View

  • 4.确认subDecorView的初始化,将subDecorView添加到PhoneWindow

  • 5.添加layoutId转化后的Viewandroid.R.id.content

  • 6.在onResume中将DecorViewView添加到WindowManager

  • 7.View真正显示到屏幕上了


3. View到底是怎么绘制的?


经过上一步,View已经添加到window上了,接下来就是View本身的绘制了
View的绘制主要经过以下几步
1、首先需要确定View占的空间尺寸(measure)
2、确定了空间尺寸,就需要确定摆放在哪个位置(layout)
3、确认了摆放位置,就需要确定在上面展示些什么东西(draw)


这几个阶段,View已经封装了模板方法给我们,我们直接重写onMeasure,onLayout,onDraw这几个方法就好了
而绘制的入口,就是上面ViewRootImpl.setView中的requestLayout


3.1 requestLayout如何触发绘制


上文说到requestLayout会触发绘制,我们一起来看下源码


ViewRootImpl.java
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//检查是否是主线程,如果不是则直接抛出异常,ViewRootImpl创建的时候生成一个主线程引用
//用当前线程和引用比较,如果是同一个则是主线程
//这也是为什么在子线程对View进行更新、绘制会报错的原因
checkThread();
//用来标记需要进行layout
mLayoutRequested = true;
//绘制请求
scheduleTraversals();
}
}

void scheduleTraversals() {
if (!mTraversalScheduled) {
//标记一次绘制请求,用来屏蔽短时间内的重复请求
mTraversalScheduled = true;
//往主线程Looper队列里放同步屏障消息,用来控制异步消息的执行
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//放入mChoreographer队列里
//主要是将mTraversalRunnable放入队列
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
//省略
}
}

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}

void doTraversal() {
//没有取消绘制的话则开始绘制
if (mTraversalScheduled) {
mTraversalScheduled = false;
//移除同步屏障
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

//真正开始执行measure、layout、draw等方法
performTraversals();
}
}

requestLayout中其实主要也是做了以下几件事



  1. 检查绘制的线程与View创建的线程是否是同一个线程

  2. 通过Handler同步屏障机制,保证UI绘制消息优先级是最高的

  3. mTraversalRunnable传入Choreographer,监听vsync信号。

  4. 收到vsync信号后会回调TraversalRunnable,移除同步屏障并开始真正的measure,layout,draw


View绘制流程图如下:


3.2 MeasureSpec分析


在测量过程中,会传入一个MeasureSpec参数,MeasureSpec封装了View的规格尺寸参数,包括View的宽高以及测量模式。
它的高2位代表测量模式,低30位代表尺寸。其中测量模式总共有3中。



  • UNSPECIFIED:未指定模式不对子View的尺寸进行限制。

  • AT_MOST:最大模式对应于wrap_content属性,父容器已经确定子View的大小,并且子View不能大于这个值。

  • EXACTLY:精确模式对应于match_parent属性和具体的数值,子View可以达到父容器指定大小的值。


普通viewMeasureSpec创建规则如下


结合这个表,我们可以一起来看一个问题


<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/red"
xmlns:android="http://schemas.android.com/apk/res/android">
<View
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/blue"/>
</FrameLayout>

请问这样一个布局,最后是什么颜色呢?
答案是蓝色,并且占满屏幕


简单来说,当我们自定义View 时,如果没有对MODE做处理,设置wrap_contentmatch_content结果其实是一样的,View 的宽高都是取父 View 的宽高
本问题的详细解析可见:一道滴滴面试题


3.3 小结



  1. View的绘制需要定位,测量,绘制三个步骤,为了简化自定义View的过程,官方已经提供了模板方法,我们重写相关方法即可

  2. ViewRootImpl中的requestLayout是绘制的入口,当然我们在View中调用invalidate或者requestLayout也会触发重绘

  3. 绘制过程本质上也是通过Handler发送消息,为了提高绘制消息的优先级,会开启同步屏蔽机制

  4. mTraversalRunnable传入Choreographer,监听vsync信号。注意,vsync信号注册了才会监听。

  5. 收到vsync信号后会回调TraversalRunnable,移除同步屏障并开始真正的measure,layout,draw过程

  6. 接下来就是回调各个ViewonMeasure,onLayout,onDraw过程


4 View绘制完成后是怎么显示到屏幕上的?


目前我们已经知道了,从XML到调用View.onDraw的过程,但是从onDraw到显示到屏幕上似乎还有些距离
我们知道,View最后要显示在屏幕上,CPU负责计算帧数据,把计算好的数据交给GPUGPU会对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)里存起来,然后Display(屏幕或显示器)负责把buffer里的数据呈现到屏幕上
那么问题来了,canvas.draw是怎么转化成Graphic Buffer的呢?


其大概流程如图所示:

可以看出,这个过程还是相当复杂的,由于篇幅原因,这里就不展开了,感兴趣的同学可以参阅苍耳叔叔的系列文章:Android图形系统综述(干货篇)


总结


XMLView显示到屏幕上主要涉及到以下知识点



  1. Activity的启动

  2. LayoutInflater填充View的原理

  3. PhoneWindow,Activity,View的关系

  4. Android窗口机制与WindowManagerService管理窗口

  5. View的绘制流程,measure,layout,draw等与Handler同步屏障机制

  6. Android屏幕刷新机制,VSync信号监听,三级缓冲等

  7. Android图形绘制,包括SurfaceFinger工作流程,软件绘制,硬件加速等


这篇文章其实已经比较长了,但是要完全了解从XML到显示到屏幕上的过程,还是不够详细,有很多地方只做了简述,如果有什么错误或者需要补充的地方,欢迎在评论区提出
由于篇幅原因,有一些知识点没有写得很详细,下面列出一些更好的文章供参考:
Android窗口机制:通俗易懂 Android视图系统的设计与实现
Android屏幕刷新机制: “终于懂了” 系列:Android屏幕刷新机制—VSync、Choreographer 全面理解!
Android图形系统: Android图形系统综述(干货篇)
Handler同步屏障机制:关于Handler同步屏障你可能不知道的问题


参考资料


【Android进阶】这一次把View绘制流程刻在脑子里!!
Android Activity创建到View的显示过程



作者:RicardoMJiang
链接:https://juejin.cn/post/6991483318625632286
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Android AGP 7.0 适配,开始强制 JDK 11

本次跟随 Arctic Fox 更新的其中一个重点就是 AGP 7.0 的调整,估计很多直接升级到 AGP 7.0 的开发者都会发现项目出现一些异常,本篇主要结合官方简单介绍 AGP 7.0 下的主要调整内容。 跳过版本 5 和 6 直接进入 AGP 7...
继续阅读 »

本次跟随 Arctic Fox 更新的其中一个重点就是 AGP 7.0 的调整,估计很多直接升级到 AGP 7.0 的开发者都会发现项目出现一些异常,本篇主要结合官方简单介绍 AGP 7.0 下的主要调整内容。



跳过版本 5 和 6 直接进入 AGP 7.0.0 的原因,是为了和 Gradle 的版本 匹配,这意味着 AGP 7.x 就是和 Gradle 7.x API 一一对应。



通过此次版本号的更改,AGP 版本号将与 Android Studio 版本号分开,不过目前的情况看 Android Studio 和 Android Gradle 插件会同时发布。一般来说使用稳定版 AGP 的项目是应该可以在较新版本的 Android Studio 中打开。



运行 AGP 7 需要 JDK 11


是的,不要惊讶,使用 Android Gradle plugin 7.0 构建时需要 JDK 11 才能运行 Gradle


但是也请不必过多当心,这里的 JDK 11 是免费的 OpenJDK 11 ,并且只要你更新到 Android Studio Arctic Fox ,它是直接捆绑了 JDK 11 并将 Gradle 配置为默认使用它,所以大多数情况下,如果你本地配置正常,是可以直接使用 AGP 7.0 的升级。


当然,你也可以手动选择配置,在 Project StructureSDK Location 栏目,可以看到 JDK 的配置位置已经被移动到 Gradle Settings



在打开的 Gradle projects 可以看到 Gradle 对应的配置选项,并且有 Gradle JDK 等可选的参数,你可以选择自己的 Java SDK ,也可以选择 AS 自带的 JDK




比如 Mac 下可以看到捆绑的 JDK 位置在 /Applications/Android\ Studio.app/Contents/jre/Contents/Home/bin/java





如果是需要抛开 Android Studio 来配置运行的 AGP 时,通过会使用 JAVA_HOME 环境变量 或 -Dorg.gradle.java.home 命令行选项 设置为 JDK 11 的安装目录来升级 JDK 版本。



Variant API stable


新的 Variant API 现在已经是稳定的版本, 可以查看 com.android.build.api.variant 包中的新接口,以及 gradle-recipes GitHub 项目中的示例。


作为新 Variant API 的一部分,通过 Artifacts 接口提供了许多称为 artifacts 的中间文件,如合并的清单功能,可以通过使用第三方插件和代码安全地获取和定制。



后续将通过添加新功能和增加可用于定制的中间件的数量来继续扩展 Variant API。



Lint 的行为变化


改进了库依赖项的 lint


运行 lint with checkDependencies = true 现在比以前更快了,对于库依赖的 Android App 的项目,建议使用 checkDependencies = true 的方式,和运行 ./gradlew :app:lint,这将并行分析的所有依赖模块,并且提供一份单独包含有 App 和依赖的 issues 文件。


// build.gradle

android {
  ...
  lintOptions {
    checkDependencies true
  }
}

// build.gradle.kts

android {
  ...
  lint {
    isCheckDependencies = true
  }
}

Lint 任务现在可以 UP-TO-DATE


如果模块的源和资源没有改变,则不需要再次运行该模块的 lint 分析任务,当出现这种情况时,任务的执行在 Gradle 输出中显示为“UP-TO-DATE”。


通过此次的变换,当在带有 checkDependencies = true 的应用程序模块上运行 lint 时,只有发生更改的模块需要运行分析,因此 Lint 可以运行得更快。



如果输入没有发生更改,则 Lint 报告任务也不需要运行,这里有一个相关的已知问题是,当 lint 任务为 UP-TO-DATE 时,没有打印到 stdout 的 lint 文本输出(问题 #191897708)。



在动态功能模块上运行 lint


AGP 不再支持从动态功能模块运行 lint,从相应的模块运行 lint 将在其动态功能模块上运行 lint,并将所有问题包含在应用程序的 lint 报告中。


一个相关的已知问题 是,当 checkDependencies = true 时,从模块运行 lint 不再会检查动态功能库依赖项,除非它们也是应用程序的依赖项(问题 #191977888)。


仅在默认 variant 上运行 lint


运行 ./gradlew :app:lint 现在只运行默认的 variant , 在以前版本的 AGP 中,它将为所有 variants 运行 lint。


Missing class warnings in R8 shrinker


R8 可以更精确和更一致地处理丢失类和 -dontwarn 的选项,因此开发者应该开始针对 R8 发出的缺失类警告进行处理。


当 R8 遇到未在 App 或其依赖项之一中定义的类引用时,它将发出警告,并显示在您的构建输出中。例如:


R8: Missing class: java.lang.instrument.ClassFileTransformer

此警告意味着 java.lang.instrument.ClassFileTransformer 在分析代码时找不到类定义,虽然这些经过可能存在错误,所以开发者可能希望可以忽略此警告,忽略警告的两个常见原因是:





    1. 以 JVM 为目标的库和缺少的类是 JVM 库类型(如上例所示)。




    1. 依赖项之一使用仅限编译时的 API。



所以可以通过向文件添加 -dontwarn 规则来向 proguard-rules.pro 忽略缺少的类警告,例如:


-dontwarn java.lang.instrument.ClassFileTransformer

为方便起见,AGP 将生成一个包含所有可能丢失的规则的文件,将它们写入如下文件路径  app/build/outputs/mapping/release/missing_rules.txt, 这样可以方便地将规则添加到 proguard-rules.pro 文件以忽略警告。



在 AGP 7.0 中缺少类消息将显示为警告,当然你可以通过 android.r8.failOnMissingClasses = truegradle.properties 讲他们变成 errors 。



在 AGP 8.0 中,这些警告将变为破坏构建的 errors,你可以通过将选项添加 -ignorewarnings配置到 proguard-rules.pro 文件来保持 AGP 7.0 行为,但不建议这样做


移除了 Android Gradle 插件构建缓存


AGP 构建缓存已在 AGP 4.1 中删除,之前在 AGP 2.3 中引入是为了补充 Gradle 构建缓存,AGP 构建缓存被 AGP 4.1 中的 Gradle 构建缓存完全取代,这个变换其实不会影响构建时间。


在 AGP 7.0 中 android.enableBuildCache 属性、android.buildCacheDir属性和cleanBuildCache 任务已经被删除。


在项目中使用 Java 11


现在可以项目中使用 Java 11 去编译你的项目代码了,开发者能够使用更新后的语言功能,例如私有接口方法、匿名类的 diamond 运输符和 lambda 参数的局部变量语法。


要启用此功能,请设置 compileOptions 为所需的 Java 版本并设置 compileSdkVersion 为 30 或更高版本:


// build.gradle

android {
    compileSdkVersion 30

    compileOptions {
      sourceCompatibility JavaVersion.VERSION_11
      targetCompatibility JavaVersion.VERSION_11
    }

    // For Kotlin projects
    kotlinOptions {
      jvmTarget = "11"
    }
}

// build.gradle.kts

android {
    compileSdkVersion(30)

    compileOptions {
      sourceCompatibility(JavaVersion.VERSION_11)
      targetCompatibility(JavaVersion.VERSION_11)
    }

    kotlinOptions {
      jvmTarget = "11"
    }
}

已知的问题


与 1.?4.?x 的 Kotlin 多平台插件兼容


Android Gradle 插件 7.0.0 与 Kotlin 多平台插件 1.5.0 及更高版本兼容。


使用 Kotlin 多平台支持的项目需要更新到 Kotlin 1.5.0 才能使用 Android Gradle 插件 7.0.0。


缺少 lint 输出


当 lint 任务是最新的(问题 #191897708)时,文本输出没有打印到 stdout 的 lint ,此问题将在 Android Gradle 插件 7.1 中修复。


并非所有动态功能库依赖项都经过 lint 检查


checkDependencies = true 从应用程序模块运行 lint 时,不会检查动态功能库依赖项,除非它们也是应用程序依赖项(问题 #191977888)。


收起阅读 »

【Flutter 组件集录】Switch 是怎样炼成的

一、 Switch 组件使用详解 可能有人会觉得 Switch 组件非常简单,有什么好说的呢?其实 Switch 组件源码洋洋洒洒 近千行 ,其中关于主题处理、平台适配、事件处理、动画处理、绘制处理 都有值得我们学习的地方。那么废话不多说,来一起看看 Swi...
继续阅读 »
一、 Switch 组件使用详解

可能有人会觉得 Switch 组件非常简单,有什么好说的呢?其实 Switch 组件源码洋洋洒洒 近千行 ,其中关于主题处理平台适配事件处理动画处理绘制处理 都有值得我们学习的地方。那么废话不多说,来一起看看 Switch 是怎么炼成的吧。





1. Switch 最简使用:valueonChanged

Switch 组件的使用中注意:该组件是 StatelessWidget ,表示本身并不维护 开关状态。这也就意味着,我把只能通过 重新构建 Switch组件 来切换 开关状态 。在构建 Switch 时必须传入 valueonChanged 两个参数,其中 value 表示 Switch 开关的状态,onChanged 是状态变化回调函数。


如下,在 _SwitchDemoState 中定义状态 _value 用于表示 Switch 开关的状态,在 _onChanged 回调中改变状态值,并 重新构建 Switch 组件,这样就能达到点击进行开关的效果。


class SwitchDemo extends StatefulWidget {
const SwitchDemo({Key? key}) : super(key: key);

@override
_SwitchDemoState createState() => _SwitchDemoState();
}

class _SwitchDemoState extends State<SwitchDemo> {
bool _value = false;

@override
Widget build(BuildContext context) {
return Switch(
value: _value,
onChanged: _onChanged,
);
}

void _onChanged(bool value) {
setState(() {
_value = value;
});
}
}

其实这里可能很让人疑惑 Switch 为什么不自己维护 开关状态,要将改状态交由外界指定呢?既然 SwitchStatelessWidget ,为什么可以执行滑动的动画?还有 onChanged 方法又是何时触发的?带着这些问题我们来逐渐去认识这个属性而陌生的 Switch 组件。




2. Switch 的四个主要颜色

Switch 的构造方法中可以看出,其中定义了非常多的颜色相关属性。



先看前四个颜色属性:



  • inactiveThumbColor 代表关闭时圆圈的颜色。

  • inactiveTrackColor 代表关闭时滑槽的颜色。




  • activeColor 代表打开时圆圈的颜色。

  • inactiveTrackColor 代表打开时滑槽的颜色。



Switch(
activeColor: Colors.blue,
activeTrackColor: Colors.green,
inactiveThumbColor: Colors.orange,
inactiveTrackColor: Colors.pinkAccent,
value: _value,
onChanged: _onChanged,
);



3. hoverColor 、 mouseCursor 和 splashRadius

前两个属性一般只能在桌面或web 端起作用,hoverColor 顾名思义是鼠标悬浮时,外层的大圈颜色,splashRadius 表示大圈的半径,如果不想要外圈的悬浮效果,可以将半径设为 0 。另外, mouseCursor 代表鼠标的样式,比如下面的小拳头是 SystemMouseCursors.grabbing



Switch(
activeColor: Colors.blue,
activeTrackColor: Colors.green,
inactiveThumbColor: Colors.orange,
inactiveTrackColor: Colors.pinkAccent,
hoverColor: Colors.blue.withOpacity(0.2),
mouseCursor: SystemMouseCursors.grabbing,
value: _value,
onChanged: _onChanged,
);

mouseCursor 属性的类型为 MouseCursor ,其中 SystemMouseCursors 中定义了非常多的鼠标指针类型以供使用。下面给出几个效果:




















contextMenu copy forbidden text



5. 指定图片

通过 activeThumbImageinactiveThumbImage 可以指定小圆中开启/关闭 时的图片。另外 onActiveThumbImageErroronInactiveThumbImageError 两个回调用于图片加载错误的监听。




当小圆同时指定 图片颜色 属性时,会显示 图片


Switch(
activeColor: Colors.blue,
activeThumbImage: AssetImage('assets/images/icon_head.png'),
inactiveThumbImage: AssetImage('assets/images/icon_8.jpg'),
activeTrackColor: Colors.green,
inactiveThumbColor: Colors.orange,
inactiveTrackColor: Colors.pinkAccent,
hoverColor: Colors.blue.withOpacity(0.2),
mouseCursor: SystemMouseCursors.move,
splashRadius: 15,
value: _value,
onChanged: _onChanged,
);



6.主题相关属性: thumbColor 和 trackColor

一些具有交互性的 Material 组件会通过有 MaterialState 枚举定义交互行为,有如下 7 个元素。


enum MaterialState {
hovered,
focused,
pressed,
dragged,
selected,
disabled,
error,
}

可以看出这两个成员都是 MaterialStateProperty 类型,那这种类型的对象如何创建,又有什么特点呢?


---->[Switch 成员声明]----
final MaterialStateProperty<Color?>? thumbColor;
final MaterialStateProperty<Color?>? trackColor;



简单来说通过 MaterialStateProperty.resolveWith 方法,传入一个函数返回对应泛型数据。如下回调函数为 getThumbColor ,回调参数为 Set<MaterialState> 。也仅仅说,会根据 MaterialState 集合,来返回泛型数据。从 thumbColor 属性源码注释中可以看出,Switch 有如下四种 MaterialState



getThumbColor 中根据 states 的情况,分别对几种状态返回不同颜色,这样 Switch 在不同的状态下,就会自动使用对应颜色。比如下面的 onChanged: null 代表 Switch 不可用,在 getThumbColor 中当为 disabled ,会返回红色。



thumbColor 代表小圆颜色,trackColor 代表滑槽颜色,使用方式是一样的。这里可能有人会问:有三个属性可以设置小圆,那它们同时存在,优先级怎么样?结果测试发现,inactiveThumbImage 会优先显示,优先级如下:


inactiveThumbImage > thumbColor > inactiveThumbColor > 默认 Switch 主题



上面提到了 默认 Switch 主题 ,这里就来说一下 SwitchTheme ,它是一个 InheritedWidget,维护 SwitchThemeData 类型数据,具体内容如下:



我们可以通过在上层嵌套 SwitchTheme 来为子树中的 Switch 指定默认样式,由于 MaterialApp 内部继承了 SwitchTheme 组件,我们可以在 theme 中指定 Switch 的主题样式。这样在指定 Switch 的相关颜色属性,就会使用默认的主题样式:






7. Switch 的焦点: focusColor 与 autofocus

Switch 组件是拥有焦点的,焦点相关的处理被封装在组件内部。focusColor 表示聚焦时的颜色,可被聚焦的组件有个特点:在桌面或 web 平台中可以通过 Tab 键,切换焦点。如下是六个 Switch 通过 Tab 键切换焦点的效果:



@override
Widget build(BuildContext context) {
return
Wrap(
children: List.generate(6, (index) => Switch(
value: _value,
focusColor: Colors.blue.withOpacity(0.1),
onChanged: _onChanged,
))
);
}



8. Switch 的尺寸相关: materialTapTargetSize

MaterialTapTargetSize 是一个枚举类型,有两个元素。该属性可以影响 Switch 的大小,如下分布是 paddedshrinkWrap 的效果。通过调试可知,默认是 padded 。下面在源码分析中会详细介绍该属性的作用。


enum MaterialTapTargetSize {
padded,
shrinkWrap,
}




二、 挖掘 Switch 源码中的一些细节


1. 类型 _SwitchType

Switch 类中有一个 _SwitchType 类型成员,该成员完全被封装在 Switch 内部,我们是无法直接操作的。 _SwitchType 是只有两个元素的枚举类。


enum _SwitchType { material, adaptive }

---->[Switch 成员声明]----
final _SwitchType _switchType;

既然是成员变量,必然会在类内部被初始化,一般来说对 成员变量 初始化的地方在 构造方法 中。如下, Switch 的普通构造 中,会将 _switchType 设为 _SwitchType.material





一般来说,枚举对象就是为了分类处理,在 Switch#build 方法中,会根据 _switchType 的值进行不同的构建逻辑,如果是 material ,则所有的平台都使用Material风格的 Switch 。 如果是 adaptive 会根据平台的不同,使用不同的风格的 Switch 。在 androidfuchsialinuxwindows 中会使用 Material 风格;在 iOSmacOS 中会使用 Cupertino 风格。



到这里,可能有人会问, _SwitchType 成员完全被封装在 Switch 内部,那如何设置 adaptive 类型呢?仔细查看源码可以看出 Switch 还有一个 adaptive 构造,此处会将 _switchType 设为 _SwitchType.adaptive





2. 两种风格的 Switch 构建

_buildCupertinoSwitch 是当模式为 adaptive 时,用于构建 iOSmacOS 平台 Switch 组件构建,可以看出其内部是通过 CupertinoSwitch 进行构建,效果如下:






_buildMaterialSwitch 用于构建 Material 风格的 Switch 组件构建,可见其内部通过 _MaterialSwitch 组件进行构建。到这里我们就可以回答:既然 SwitchStatelessWidget ,为什么可以执行滑动的动画?因为 _MaterialSwitch 组件是 StatefulWidget ,它可以在内部改变组件状态。





3.Switch 尺寸的确定

从上面可以看出,两种风格的 Switch 都是通过 _getSwitchSize 获取 Size 尺寸的。如下代码中,可以看出,尺寸是通过 MaterialTapTargetSize 对象控制的。如果未指定 materialTapTargetSize 则会通过主题获取,调试可以看出,主题中 materialTapTargetSize 默认是 padded


Size _getSwitchSize(ThemeData theme) {
final MaterialTapTargetSize effectiveMaterialTapTargetSize = materialTapTargetSize
?? theme.switchTheme.materialTapTargetSize
?? theme.materialTapTargetSize;
switch (effectiveMaterialTapTargetSize) {
case MaterialTapTargetSize.padded:
return const Size(_kSwitchWidth, _kSwitchHeight);
case MaterialTapTargetSize.shrinkWrap:
return const Size(_kSwitchWidth, _kSwitchHeightCollapsed);
}
}

下面分别是 paddedshrinkWrap 的调试信息,可以很清楚地看出尺寸情况。






到这里 Switch 组件的源码就已经面面俱到了,我们可以发现,它作为一个 StatelessWidget 并不能做太多的事,只是定义了很多属性,并通过别的组件进行构建。也就是说,它本身起到平台差异的统筹、封装的作用,目的就是方便用户使用。




4. onChanged 方法触发的时机

通过调试可以发现,onChanged 方法 的触发是 ToggleableStateMixin#_handleTap 中触发的。如下是 buildToggleable 的源码,可以看出其中通过 GestureDetector 监听点击事件。



_MaterialSwitchState.build 方法中,可以看到其中通过 GestureDetector 监听了水平拖拽事件,这也是为什么 Switch 可以支持拖动的原因,同时 child 属性是 buildToggleable ,也就是上面的组件,支持点击事件。这是一个很好的多事件监听的案例。





5.动画的创建与触发

仔细看一下滑动的过程,可以看出其中有 位移动画透明度渐变动画。 首先来说一下动画的来源:






这些动画器都定义在 ToggleableStateMixin 中。而 _MaterialSwitchState 混入了 ToggleableStateMixin





和隐式动画一样, _MaterialSwitchState 中的动画触发也是通过重构组件,执行 didUpdateWidget 。如果你了解隐式动画,就不难理解 Switch 的动画触发机制。



最后,绘制是通过 _SwitchPainter 画出来的,这个画板是比较复杂的,这里就不展开了,有兴趣的可以自己研究一下。



Switch 组件的使用方式到这里就完全介绍完毕,那本文到这里就结束了,谢谢观看,明天见~


作者:张风捷特烈

链接:https://juejin.cn/post/6991998231458611231
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS 界面渲染流程分析

iOS
前言本文阅读建议 1.一定要辩证的看待本文. 2.本文所表达观点并不是最终观点,还会更新,因为本人还在学习过程中,有什么遗漏或错误还望各位指出. 3.觉得哪里不妥请在评论留下建议~ 4.觉得还行的话就点个小心心鼓励下我吧~ 在最近的面试中,我发现一道面试题,其...
继续阅读 »

前言

本文阅读建议
1.一定要辩证的看待本文.
2.本文所表达观点并不是最终观点,还会更新,因为本人还在学习过程中,有什么遗漏或错误还望各位指出.
3.觉得哪里不妥请在评论留下建议~
4.觉得还行的话就点个小心心鼓励下我吧~

在最近的面试中,我发现一道面试题,其考点是:围绕iOS App中一个视图从添加到完全渲染,在这个过程中,iOS系统都做了什么?

在进行了大量的文章查阅以及学习以后,将所有较为可靠的资料总结一下供大家参考。


面试题

本文可为以下面试题提供参考:

  1. app从点击屏幕(硬件)到完全渲染,中间发生了什么?越详细越好 要求讲到进程间通信?出处
  2. 一个UIImageView添加到视图上以后,内部是如何渲染到手机上的,请简述其流程?
  3. 在一个表内有很多cell,每个cell上有很多个视图,如何解决卡顿问题?
  4. UIView与CALayer的区别?

简答

iOS渲染视图的核心是Core Animation
其渲染层次依次为:图层树->呈现树->渲染树

  1. CPU阶段
    1. 布局(Frame)
    2. 显示(Core Graphics)
    3. 准备(QuartzCore/Core Animation)
    4. 通过IPC提交(打包好的图层树以及动画属性)
  2. OpenGL ES阶段
    1. 生成(Generate)
    2. 绑定(Bind)
    3. 缓存数据(Buffer Data)
    4. 启用(Enable)
    5. 设置指针(Set Pointers)
    6. 绘图(Draw)
    7. 清除(Delete)
  3. GPU阶段
    1. 接收提交的纹理(Texture)和顶点描述(三角形)
    2. 应用变换(transform)
    3. 合并渲染(离屏渲染等)

其iOS平台渲染核心原理的重点主要围绕前后帧缓存、Vsync信号、CADisplayLink

文字简答:

  1. 首先一个视图由CPU进行Frame布局,准备视图和图层的层级关系,查询是否有重写drawRect:drawLayer:inContext:方法,注意:如果有重写的话,这里的渲染是会占用CPU进行处理的
  2. CPU会将处理视图和图层的层级关系打包,通过IPC(内部处理通信)通道提交给渲染服务,渲染服务由OpenGL ES和GPU组成。
  3. 渲染服务首先将图层数据交给OpenGL ES进行纹理生成和着色。生成前后帧缓存,再根据显示硬件的刷新频率,一般以设备的VSync信号CADisplayLink为标准,进行前后帧缓存的切换。
  4. 最后,将最终要显示在画面上的后帧缓存交给GPU,进行采集图片和形状,运行变换,应用纹理和混合。最终显示在屏幕上。

以上仅仅是对该题简单回答,其中的原理以及瓶颈和优化,后面会详细介绍。


知识点

  1. 重新认识Core Animation
  2. CPU渲染职能
  3. OpenGL ES渲染职能
  4. GPU渲染职能
  5. IPC内部通信(进程间通信)
  6. 前后帧缓存&Vsync信号
  7. 视图渲染优化&卡顿优化
  8. Metal渲染引擎
  9. 事件响应链&Runloop原理
  10. CALayer的职能

重新认识Core Animation

苹果官方文档-Core Animation
Core Animation并仅仅是字面意思的核心动画,而是整个显示核心都是围绕QuartzCore框架中的Core Animation

Core Animation是依赖于OpenGL ES做GPU渲染,CoreGraphics做CPU渲染,但在本文中,以及官方文档都是将OpenGL与GPU分开说明。

Core Animation 在 RunLoop 中注册了一个 Observer,监听了 BeforeWaiting 和 Exit 事件。这个 Observer 的优先级是 2000000,低于常见的其他 Observer。当一个触摸事件到来时,RunLoop 被唤醒,App 中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView 的 frame、修改 CALayer 的透明度、为视图添加一个动画;这些操作最终都会被 CALayer 捕获,并通过 CATransaction 提交到一个中间状态去(CATransaction 的文档略有提到这些内容,但并不完整)。当上面所有操作结束后,RunLoop 即将进入休眠(或者退出)时,关注该事件的 Observer 都会得到通知。这时 CA 注册的那个 Observer 就会在回调中,把所有的中间状态合并提交到 GPU 去显示;如果此处有动画,CA 会通过 DisplayLink 等机制多次触发相关流程。

CPU渲染职能

在这里推荐大家去阅读落影loyinglin的文章iOS开发-视图渲染与性能优化

  • 显示逻辑
    • CoreAnimation提交会话,包括自己和子树(view hierarchy)的layout状态等;
    • RenderServer解析提交的子树状态,生成绘制指令
    • GPU执行绘制指令
    • 显示渲染后的数据
  • 提交流程
    • 布局(Layout)
      • 调用layoutSubviews方法
      • 调用addSubview:方法
    • 显示(Display)
      • 通过drawRect绘制视图;
      • 绘制string(字符串);
    • 准备提交(Prepare)
      • 解码图片;
      • 图片格式转换;
    • 提交(Commit)
      • 打包layers并发送到渲染server;
      • 递归提交子树的layers;
      • 如果子树太复杂,会消耗很大,对性能造成影响;

CPU渲染职能主要体现在以下5个方面:

布局计算
如果你的视图层级过于复杂,当视图呈现或者修改的时候,计算图层帧率就会消耗一部分时间。特别是使用iOS6的自动布局机制尤为明显,它应该是比老版的自动调整逻辑加强了CPU的工作。

视图懒加载
iOS只会当视图控制器的视图显示到屏幕上时才会加载它。这对内存使用和程序启动时间很有好处,但是当呈现到屏幕上之前,按下按钮导致的许多工作都会不能被及时响应。比如控制器从数据库中获取数据,或者视图 从一个nib文件中加载,或者涉及IO的图片显示,都会比CPU正常操作慢得多。

Core Graphics绘制
如果对视图实现了drawRect:drawLayer:inContext:方法,或者 CALayerDelegate 的 方法,那么在绘制任何东 西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后, 必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。

解压图片
PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。但是在图片绘制到屏幕上之前,必须把它扩展成完整的未解压的尺寸(通常等同于图片宽 x 长 x 4个字节)。为了节省内存,iOS通常直到真正绘制的时候才去解码图片。根据你加载图片的方式,第一次对 图层内容赋值的时候(直接或者间接使用 UIImageView )或者把它绘制到 Core Graphics中,都需要对它解压,这样的话,对于一个较大的图片,都会占用一定的时间。

图层打包
当图层被成功打包,发送到渲染服务器之后,CPU仍然要做如下工作:为了显示 屏幕上的图层,Core Animation必须对渲染树种的每个可见图层通过OpenGL循环 转换成纹理三角板。由于GPU并不知晓Core Animation图层的任何结构,所以必须 要由CPU做这些事情。这里CPU涉及的工作和图层个数成正比,所以如果在你的层 级关系中有太多的图层,就会导致CPU没一帧的渲染,即使这些事情不是你的应用 程序可控的。

OpenGL ES渲染职能

这里推荐大家去看《OpenGL ES应用开发实践指南:iOS卷》,因为篇幅过长,就不赘述OpenGL的原理。

简单来说,OpenGL ES是对图层进行取色,采样,生成纹理,绑定数据,生成前后帧缓存。

纹理的概念:纹理是一个用来保存图像的颜色元􏰈值的 OpenGL ES 缓存,可以简单理解为一个单位。

1)生成(Generate)— 请 OpenGL ES 为图形处理器制的缓存生成一个独一无二的标识符。 
2)绑定(Bind)— 告诉 OpenGL ES 为接下来的运算使用一个缓存。
3)缓存数据(Buffer Data)— 让 OpenGL ES 为当前定的缓存分配并初始化 够的内存(通常是从 CPU 制的内存复制数据到分配的内存)。
4)启用(Enable)或者(Disable)— 告诉 OpenGL ES 在接下来的渲染中是 使用缓存中的数据。
5)设置指(Set Pointers)— 告诉 Open-GL ES 在缓存中的数据的类型和所有需 要的数据的内存移值。
6)绘图(Draw) — 告诉 OpenGL ES 使用当前定并启用的缓存中的数据渲染 整个场景或者某个场景的一部分。
7)删除除(Delete)— 告诉 OpenGL ES 除以前生成的缓存并释相关的资源。

当显示一个UIImageView时,Core Animation会创建一个OpenGL ES纹理,并确保在这个图层中的位图被上传到对应的纹理中。当你重写-drawInContext方法时,Core Animation会请求分配一个纹理,同时确保Core Graphics会将你在-drawInContext中绘制的东西放入到纹理的位图数据中。

iOS 操作系统不会让应用直接向前帧缓存或者 后帧缓存绘图,也不会让应用直接复制前帧缓存和后帧缓存之间的切换。操作系统为自 己保留了这些操作,以便它可以随时使用 Core Animation 合成器来控制显示的最终外观

最终,生成前后帧缓存会再交由GPU进行最后一步的工作。

GPU渲染职能

GPU会根据生成的前后帧缓存数据,根据实际情况进行合成,其中造成GPU渲染负担的一般是:离屏渲染,图层混合,延迟加载。

  • 普通的Tile-Based渲染流程
    • CommandBuffer,接受OpenGL ES处理完毕的渲染指令;
    • Tiler,调用顶点着色器,把顶点数据进行分块(Tiling);
    • ParameterBuffer,接受分块完毕的tile和对应的渲染参数;
    • Renderer,调用片元着色器,进行像素渲染;
      -RenderBuffer,存储渲染完毕的像素;
  • 离屏渲染 —— 遮罩(Mask)
    • 渲染layer的mask纹理,同Tile-Based的基本渲染逻辑;
    • 渲染layer的content纹理,同Tile-Based的基本渲染逻辑;
    • Compositing操作,合并1、2的纹理;
  • 离屏渲染 ——UIVisiualEffectView
  • 渲染等待
  • 光栅化
  • 组透明度

GPU用来采集图片和形状,运行变换,应用文理和混合,最终把它们输送到屏幕上。

太多的几何结构会影响GPU速度,但这并不是GPU的瓶颈限制原因,但由于图层在显示之前要通过IPC发送到渲染服务器的时候(图层实际上是由很多小物体组成的特别重量级的对象),太多的图层就会引起CPU的瓶颈。

重绘。主要由重叠的半透明图层引起。GPU的填充比率(用颜色填充像素的比率)是有限的,所以要避免重绘。


IPC内部通信(进程间通信)

在研究这个问题的过程中,我有想过去看一下源码,试着去理解在视图完全渲染之前,IPC是如何调度的,可惜苹果并没有开源绘制过程中的代码。这里推荐官方文章给大家了解一下iOS中IPC是如何运作的。

苹果官方文档-Mach内核编程 IPC通信

前后帧缓存&Vsync信号

虽然我们不能看到苹果内部是如何实现的,但是苹果官方也提供了我们可以参考的对象,也就是VSync信号CADisplayLink对象。

iOS 的显示系统是由 VSync 信号驱动的,VSync 信号由硬件时钟生成,每秒钟发出 60 次(这个值取决设备硬件,比如 iPhone 真机上通常是 59.97)。iOS 图形服务接收到 VSync 信号后,会通过 IPC 通知到 App 内。App 的 Runloop 在启动后会注册对应的 CFRunLoopSource 通过 mach_port 接收传过来的时钟信号通知,随后 Source 的回调会驱动整个 App 的动画与显示。

帧缓存:接收渲染结果的缓冲区,为GPU指定存储渲染结果的区域

帧缓存可以同时存在多个,但是屏幕显示像素受到保存在前帧缓存(front frame buffer)的特定帧缓存中的像素颜色元素的控制。
程序的渲染结果通常保存在后帧缓存(back frame buffer)在内的其他帧缓存,当渲染后的后帧缓存完成后,前后帧缓存会互换。(这部分操作由操作系统来完成)

前帧缓存决定了屏幕上显示的像素颜色,会在适当的时候与后帧缓存切换。

Core Animation的合成器会联合OpenGL ES层和UIView层、StatusBar层等,在后帧缓存混合产生最终的颜色,并切换前后帧缓存;
OpenGL ES坐标是以浮点数来存储,即使是其他数据类型的顶点数据也会被转化成浮点型;


视图加载

那么在了解iOS视图渲染流程以后,再来看一下第二题:
一个UIImageView添加到视图上以后,内部是如何渲染到手机上的,请简述其流程?

图片的显示分为三步:加载、解码、渲染。
通常,我们操作的只有加载,解码和渲染是由UIKit进行。
以UIImageView为例。当其显示在屏幕上时,需要UIImage作为数据源。
UIImage持有的数据是未解码的压缩数据,能节省较多的内存和加快存储。
当UIImage被赋值给UIImage时(例如imageView.image = image;),图像数据会被解码,变成RGB的颜色数据。
解码是一个计算量较大的任务,且需要CPU来执行;并且解码出来的图片体积与图片的宽高有关系,而与图片原来的体积无关。
此处引用-->iOS性能优化——图片加载和处理

我查看了较为流行的第三方库源码,例如YYImage、SDWebImage、FastImageCache,其中加载一个图片的流程大致为:

  1. 查看UIImageView的API我们可以发现,UIImage封装了一个CoreGraphics/CGImage的对象。
    1.+[UIImage imageWithContentsOfFile:]使用Image I/O创建CGImageRef内存映射数据。此时,图像尚未解码。
  2. 返回的图像被分配给UIImageView。
  3. 如果图像数据为未解码的PNG/JPG,解码为位图数据
  4. 隐式CATransaction捕获到UIImageView layer树的变化
  5. 在主运行循环的下一次迭代中,Core Animation提交隐式事务,这会涉及创建已设置为层内容的所有图像的副本,根据图像:
    1. 缓冲区被分配用于管理文件IO和解压缩操作。
    2. 文件数据从磁盘读入内存。
    3. 压缩的图像数据被解码成其未压缩的位图形式
    4. Core Animation使用未压缩的位图数据来渲染图层。

再看一下YYImage的源码,其流程也大致为:

  1. 获取图片二进制数据
  2. 创建一个CGImageRef对象
  3. 使用CGBitmapContextCreate()方法创建一个上下文对象
  4. 使用CGContextDrawImage()方法绘制到上下文
  5. 使用CGBitmapContextCreateImage()生成CGImageRef对象。
  6. 最后使用imageWithCGImage()方法将CGImage转化为UIImage。

当然YYImage不止做了这些,还有解码器编码器,支持webP等多种格式,并且还写了自定义的操作队列,对网络加载图片进行了优化。在此不赘述。

推荐文章:
苹果官方文档-CGImage位图
iOS图片加载速度极限优化—FastImageCache解析
Image I/O详解的文章
在这里同时推荐Y大的两篇文章
移动端图片格式调研
iOS 处理图片的一些小 Tip

视图渲染优化&卡顿优化

接下来我们看一下第三题:在一个表内有很多cell,每个cell上有很多个视图,如何解决卡顿问题?

什么是卡顿?苹果官方文章-显示帧率

当你的主线程操作卡顿超过16.67ms以后,你的应用就会出现掉帧,丢帧的情况。也就是卡顿。

一般来说造成卡顿的原因,就是CPU负担过重,响应时间过长。主要原因有以下几种:

  • 隐式绘制 CGContext
  • 文本CATextLayer 和 UILabel
  • 光栅化 shouldRasterize
  • 离屏渲染
  • 可伸缩图片
  • shadowPath
  • 混合和过度绘制
  • 减少图层数量
  • 裁切
  • 对象回收
  • Core Graphics绘制
  • -renderInContext: 方法

其中最常见的问题就是离屏渲染

离屏渲染:离屏绘制发生在基于CPU或者是GPU的渲染,或者是为离屏图 片分配额外内存,以及切换绘制上下文,这些都会降低GPU性能。对于特定图 层效果的使用,比如圆角,图层遮罩,阴影或者是图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。

如果视图绘制超出GPU支持的2048x2048或者4096x4096尺寸的 纹理,就必须要用CPU在图层每次显示之前对图片预处理,同样也会降低性能。

那么如何在需要渲染大量视图的情况下,还能保证流畅度,也就是保证FPS。
在这里推荐阅读郭曜源前辈的iOS 保持界面流畅的技巧
以及indulge_in的YYAsyncLayer剖析
我参考了YYAsyncLayer,他其中的原理大致是这样的:

YYAsyncLayer原理

YYAsyncLayer 是 CALayer 的子类,当它需要显示内容(比如调用了 [layer setNeedDisplay])时,它会向 delegate,也就是 UIView 请求一个异步绘制的任务。在异步绘制时,Layer 会传递一个 BOOL(^isCancelled)() 这样的 block,绘制代码可以随时调用该 block 判断绘制任务是否已经被取消。

当 TableView 快速滑动时,会有大量异步绘制任务提交到后台线程去执行。但是有时滑动速度过快时,绘制任务还没有完成就可能已经被取消了。如果这时仍然继续绘制,就会造成大量的 CPU 资源浪费,甚至阻塞线程并造成后续的绘制任务迟迟无法完成。我的做法是尽量快速、提前判断当前绘制任务是否已经被取消;在绘制每一行文本前,我都会调用 isCancelled() 来进行判断,保证被取消的任务能及时退出,不至于影响后续操作。

AsyncDisplayKit原理

ASDK 在此处模拟了 Core Animation 的这个机制:所有针对 ASNode 的修改和提交,总有些任务是必需放入主线程执行的。当出现这种任务时,ASNode 会把任务用 ASAsyncTransaction(Group) 封装并提交到一个全局的容器去。ASDK 也在 RunLoop 中注册了一个 Observer,监视的事件和 CA 一样,但优先级比 CA 要低。当 RunLoop 进入休眠前、CA 处理完事件后,ASDK 就会执行该 loop 内提交的所有任务。

Tips

优化方案围绕着 使用多线程调用,合理利用CPU计算位置,布局,层次,解压等,再合理调度GPU进行渲染,GPU负担常常要比CPU大,合理调度CPU进行计算可以减轻GPU渲染负担,使应用更加流畅。


Metal渲染引擎

当你现在再去查阅官方文档时,你会发现苹果官方已经使用Metal去替代OpenGL ES作为Core Animation的渲染。

苹果将Metal作为新的渲染引擎,更好的利用了GPU的性能,同时保证了低内存占用和省电,但我个人并没有深入研究Metal,这里可以有兴趣的同学可以看一下落影前辈的文章:
Metal入门教程总结
Metal入门教程(八)Metal与OpenGL ES交互
OpenGL 专题


事件响应链&原理

最后一题:UIView和CALayer的区别?

如果你已经做了几年iOS开发,相比对于这道题可能已经很熟悉。
最直接的回答就是UIView可以响应用户事件,而CALayer不能处理事件

首先要讲一下App中的事件响应链,它分为两部分:Hit-Testing事件传递 & Runloop原理

当用户对屏幕进行了操作,产生了一个用户事件。

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue()进行应用内部的分发。

_UIApplicationHandleEventQueue()会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel事件都是在这个回调中完成的。
此处引用-->深入理解Runloop-事件响应


当前前台运行中应用接收到UIEvent以后,当用户对屏幕进行了操作,系统先循环调用Hit-test遍历视图栈里的视图,顺序为视图层次的逆顺序,用Responder Chain响应链传递一层层给根视图AppDelegate处理。-->苹果官方文档-使用响应者和响应者链来处理事件

推荐两篇文章:
iOS 事件处理机制与图像渲染过程
iOS事件响应链中Hit-Test View的应用

CALayer的职能

CALayer 并不清楚具体的响应链,所以不能直接处理触摸事件或者手势。但是它提供了-containsPoint:-hitTest:来判断是否一个触点在图层的范围之内。

与UIView不同,CALayer着重于图层的绘制,大致为以下职能:

  • 阴影、圆角、边框、蒙版、拉伸、transform、动画。
  • 寄宿图:你可以给CALayer.contents传递一个CGImage来进行渲染,也可以调用- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;方法进行绘制。但通常我们会使用UIView的drawRect方法
  • CATextLayer:直接将字符串使用Core Graphics写入图层
  • CATransformLayer:能够用于构造一个层级的3D结构

CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性。对此你在应用中,应该尽量减少不必要的属性修改。

当视图层次调整时,UIView、CALayer 之间会出现很多方法调用与通知,所以在优化性能时,应该尽量避免调整视图层次、添加和移除视图。

使用图层关联的视图而不是单独使用 CALayer 的好处在于,你能在使用所
有 CALayer 底层特性的同时,也可以使用 UIView 的高级API(比如自动排版, 布局和事件处理)。做一些对性能特别挑剔的工作,比如对 UIView 一些可忽略不计的操作都会引 起显著的不同

关于UIView动画以及CALayer的动画这里推荐两篇文章:
iOS-UIView与CALayer动画原理
CALayer与iOS动画 讲解及使用

参考

本文大量借助了引用文章的文字描述,在此感谢各位作者的文章对本问题的理解起了很大的帮助。也希望各位能去原文发表自己的看法。谢谢~

总结

iOS开发要学的东西还有很多,因为时间的推移,每年的iOS岗位要求都在提高,导致我们在iOS开发岗位的同学要学习很多知识。例如Runtime、Runloop、音视频处理、视图渲染等,各位一起加油吧。

链接:https://www.jianshu.com/p/39b91ecaaac8

收起阅读 »

【SpringBoot + Mybatis系列】插件机制 Interceptor

【SpringBoot + Mybatis系列】插件机制 Interceptor 在 Mybatis 中,插件机制提供了非常强大的扩展能力,在 sql 最终执行之前,提供了四个拦截点,支持不同场景的功能扩展 Executor (update, q...
继续阅读 »



【SpringBoot + Mybatis系列】插件机制 Interceptor



在 Mybatis 中,插件机制提供了非常强大的扩展能力,在 sql 最终执行之前,提供了四个拦截点,支持不同场景的功能扩展



  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)

  • ParameterHandler (getParameterObject, setParameters)

  • ResultSetHandler (handleResultSets, handleOutputParameters)

  • StatementHandler (prepare, parameterize, batch, update, query)


本文将主要介绍一下自定义 Interceptor 的使用姿势,并给出一个通过自定义插件来输出执行 sql,与耗时的 case


I. 环境准备


1. 数据库准备


使用 mysql 作为本文的实例数据库,新增一张表


CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

2. 项目环境


本文借助 SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发


pom 依赖如下


<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>

db 配置信息 application.yml


spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:

II. 实例演示


关于 myabtis 的配套 Entity/Mapper 相关内容,推荐查看之前的系列博文,这里就不贴出来了,将主要集中在 Interceptor 的实现上


1. 自定义 interceptor


实现一个自定义的插件还是比较简单的,试下org.apache.ibatis.plugin.Interceptor接口即可


比如定义一个拦截器,实现 sql 输出,执行耗时输出


@Slf4j
@Component
@Intercepts(value = {@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
})

public class ExecuteStatInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// MetaObject 是 Mybatis 提供的一个用于访问对象属性的对象
MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
BoundSql sql = statement.getBoundSql(invocation.getArgs()[1]);

long start = System.currentTimeMillis();
List<ParameterMapping> list = sql.getParameterMappings();
OgnlContext context = (OgnlContext) Ognl.createDefaultContext(sql.getParameterObject());
List<Object> params = new ArrayList<>(list.size());
for (ParameterMapping mapping : list) {
params.add(Ognl.getValue(Ognl.parseExpression(mapping.getProperty()), context, context.getRoot()));
}
try {
return invocation.proceed();
} finally {
System.out.println("------------> sql: " + sql.getSql() + "\n------------> args: " + params + "------------> cost: " + (System.currentTimeMillis() - start));
}
}

@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}

@Override
public void setProperties(Properties properties) {

}
}

注意上面的实现,核心逻辑在intercept方法,内部实现 sql 获取,参数解析,耗时统计


1.1 sql 参数解析说明


上面 case 中,对于参数解析,mybatis 是借助 Ognl 来实现参数替换的,因此上面直接使用 ognl 表达式来获取 sql 参数,当然这种实现方式比较粗暴


// 下面这一段逻辑,主要是OGNL的使用姿势
OgnlContext context = (OgnlContext) Ognl.createDefaultContext(sql.getParameterObject());
List<Object> params = new ArrayList<>(list.size());
for (ParameterMapping mapping : list) {
params.add(Ognl.getValue(Ognl.parseExpression(mapping.getProperty()), context, context.getRoot()));
}

除了上面这种姿势之外,我们知道最终 mybatis 也是会实现 sql 参数解析的,如果有分析过源码的小伙伴,对下面这种姿势应该比较熟悉了


源码参考自: org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters


BoundSql sql = statementHandler.getBoundSql();
DefaultParameterHandler handler = (DefaultParameterHandler) statementHandler.getParameterHandler();
Field field = handler.getClass().getDeclaredField("configuration");
field.setAccessible(true);
Configuration configuration = (Configuration) ReflectionUtils.getField(field, handler);
// 这种姿势,与mybatis源码中参数解析姿势一直
//
MetaObject mo = configuration.newMetaObject(sql.getParameterObject());
List<Object> args = new ArrayList<>();
for (ParameterMapping key : sql.getParameterMappings()) {
args.add(mo.getValue(key.getProperty()));
}

但是使用上面这种姿势,需要注意并不是所有的切点都可以生效;这个涉及到 mybatis 提供的四个切点的特性,这里也就不详细进行展开,在后面的源码篇,这些都是绕不过去的点


1.2 Intercepts 注解


接下来重点关注一下类上的@Intercepts注解,它表明这个类是一个 mybatis 的插件类,通过@Signature来指定切点


其中的 type, method, args 用来精确命中切点的具体方法


如根据上面的实例 case 进行说明


@Intercepts(value = {@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
})


首先从切点为Executor,然后两个方法的执行会被拦截;这两个方法的方法名分别是query, update,参数类型也一并定义了,通过这些信息,可以精确匹配Executor接口上定义的类,如下


// org.apache.ibatis.executor.Executor

// 对应第一个@Signature
<E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;

// 对应第二个@Signature
int update(MappedStatement var1, Object var2) throws SQLException;

1.3 切点说明


mybatis 提供了四个切点,那么他们之间有什么区别,什么样的场景选择什么样的切点呢?


一般来讲,拦截ParameterHandler是最常见的,虽然上面的实例是拦截Executor,切点的选择,主要与它的功能强相关,想要更好的理解它,需要从 mybatis 的工作原理出发,这里将只做最基本的介绍,待后续源码进行详细分析



  • Executor:代表执行器,由它调度 StatementHandler、ParameterHandler、ResultSetHandler 等来执行对应的 SQL,其中 StatementHandler 是最重要的。

  • StatementHandler:作用是使用数据库的 Statement(PreparedStatement)执行操作,它是四大对象的核心,起到承上启下的作用,许多重要的插件都是通过拦截它来实现的。

  • ParameterHandler:是用来处理 SQL 参数的。

  • ResultSetHandler:是进行数据集(ResultSet)的封装返回处理的,它非常的复杂,好在不常用。


借用网上的一张 mybatis 执行过程来辅助说明




原文 blog.csdn.net/weixin_3949…



2. 插件注册


上面只是自定义插件,接下来就是需要让这个插件生效,也有下面几种不同的姿势


2.1 Spring Bean


将插件定义为一个普通的 Spring Bean 对象,则可以生效


2.2 SqlSessionFactory


直接通过SqlSessionFactory来注册插件也是一个非常通用的做法,正如之前注册 TypeHandler 一样,如下


@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(
// 设置mybatis的xml所在位置,这里使用mybatis注解方式,没有配置xml文件
new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*.xml"));
// 注册typehandler,供全局使用
bean.setTypeHandlers(new Timestamp2LongHandler());
bean.setPlugins(new SqlStatInterceptor());
return bean.getObject();
}

2.3 xml 配置


习惯用 mybatis 的 xml 配置的小伙伴,可能更喜欢使用下面这种方式,在mybatis-config.xml全局 xml 配置文件中进行定义


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//ibatis.apache.org//DTD Config 3.1//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
<settings>
<!-- 驼峰下划线格式支持 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.git.hui.boot.mybatis.entity"/>
</typeAliases>

<!-- type handler 定义 -->
<typeHandlers>
<typeHandler handler="com.git.hui.boot.mybatis.handler.Timestamp2LongHandler"/>
</typeHandlers>

<!-- 插件定义 -->
<plugins>
<plugin interceptor="com.git.hui.boot.mybatis.interceptor.SqlStatInterceptor"/>
<plugin interceptor="com.git.hui.boot.mybatis.interceptor.ExecuteStatInterceptor"/>
</plugins>
</configuration>

3. 小结


本文主要介绍 mybatis 的插件使用姿势,一个简单的实例演示了如果通过插件,来输出执行 sql,以及耗时


自定义插件实现,重点两步



  • 实现接口org.apache.ibatis.plugin.Interceptor

  • @Intercepts 注解修饰插件类,@Signature定义切点


插件注册三种姿势:



  • 注册为 Spring Bean

  • SqlSessionFactory 设置插件

  • myabtis.xml 文件配置



作者:一灰灰
链接:https://juejin.cn/post/6992001465162137637
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

前端这个工种未来会继续拆分么?

作为前端,你和UI撕过逼么?脑中的场景前端:“上线日期定死了,你什么时候出设计稿?你不出稿子后面开发、测试都得加班!”UI:“快了快了,别催~”前端:“做好的先给我吧,我画静态页面”UI:“快了快了,别催~”前端流泪,后端沉默,终究测试承担了所有......你...
继续阅读 »

作为前端,你和UI撕过逼么?

脑中的场景

前端:“上线日期定死了,你什么时候出设计稿?你不出稿子后面开发、测试都得加班!”

UI:“快了快了,别催~”

前端:“做好的先给我吧,我画静态页面”

UI:“快了快了,别催~”

前端流泪,后端沉默,终究测试承担了所有......

你遇到过这种情况么?

您觉得本质原因是什么?如何才能最高效解决这个问题?

本文会提供一种思路以及可借鉴的产品。

欢迎文末就这个问题讨论

问题原因

现代 Web 开发困境与破局一文中,作者牛岱谈到当前前端与UI的配合模式如下:

图片来自“现代 Web 开发困境与破局”

UI在设计软件上完成设计逻辑、绘制页面样式,交付给前端。

前端根据UI绘制的样式重现用CSS+HTML在网页中再绘制一遍样式,绘制完毕后再添加功能逻辑。

为什么UI用设计软件绘制的页面样式,前端还需要重复绘制一次?仅仅因为UI用设计软件,而前端需要编程么?

所以,理想的分工应该如下:

图片来自“现代 Web 开发困境与破局”

UI完成设计逻辑与页面样式(通过设计软件),软件根据规范生成前端可用的静态页面代码,前端基于生成的代码编写功能逻辑。

大白话讲就是:

前端不用画静态页了

虽然这套流程有诸多难点需要解决,比如:

  • 对于UI来说,页面是一张张图层,对于前端则是一个个组件,怎么对齐这两者差异
  • 需要UI了解基本的页面布局(浮动、flex、绝对定位...),才能生成符合响应式规范的静态页

但是,瑕不掩瑜,如果能跑通这套流程,开发效率将极大提升。

mitosis就是这方面的一次大胆尝试。

一次大胆尝试

BuilderIO是一家低代码平台,主做拖拽生成页面。mitosis的作者是BuilderIOCEO

用一张图概括mitosis的定位:

左起第一排分别是:sketchFigmaBuilderIO,前两者是知名设计软件,后者是低代码平台。

UI使用这些软件完成页面设计,经由插件输出到mitosis后,mitosis能将其输出成多种知名前端框架代码。

设计图一步到位变成前端框架代码,前端就不用画静态页了。

他是怎么做到的?

现代前端框架都是以组件作为逻辑、视图的分割单元。而组件是可以被描述的。

比如ReactFiberVueVNode,都是描述组件信息的节点类型。

mitosis将设计图转化为框架无关的JSON,类似这样:

{
"@type": "@builder.io/mitosis/component",
"state": {
"name": "Steve"
},
"nodes": [
{
"@type": "@builder.io/mitosis/node",
"name": "div",
"children": [
{
"@type": "@builder.io/mitosis/node",
"bindings": {
"value": "state.name",
"onChange": "state.name = event.target.value"
}
}
]
}
]
}


这段JSON描述的是一个component类型(即组件),其包含状态namenodes代表组件对应的视图。

如果输出目标是React,那么代码如下:

export function MyComponent() {
const [name, updateName] = useState('Steve');

return (
<div>
<input
value={name}
onChange={(e) => updateName(e.target.value)}
/>
div>
);
}


小小心机

如果你仔细看这张图会发现,mitosis还能反向输出到设计软件。

是的,mitosis本身也是个框架。有意思的是,他更像是个前端框架缝合怪

他采用了:

  • ReactHooks语法
  • Vue的响应式更新
  • Solid.js的静态JSX
  • Svelte的预编译技术
  • Angular的规范

上面的代码例子,如果用mitosis语法写:

export function MyComponent() {
const state = useState({
name: 'Steve',
});

return (
<div>
<input
value={state.name}
onChange={(e) => (state.name = e.target.value)}
/>
div>
);
}

未曾设想的道路?

我们在开篇谈到阻碍前端直接使用设计软件生成静态代码的两个痛点:

  • 对于UI来说,页面是一张张图层,对于前端则是一个个组件,怎么对齐这两者差异
  • 需要UI了解基本的页面布局(浮动、flex、绝对定位...),才能生成复合响应式规范的静态页

我们设想一下,当使用mitosis开启一个新项目,流程如下:

  1. 由懂设计的前端基于mitosis开发初始代码
  2. 代码输出为设计稿
  3. 专业UI基于设计稿(符合组件规范、响应式规范)润色
  4. 设计稿经由mitosis输出为任意前端框架代码
  5. 前端基于框架代码开发

这样,就解决了以上痛点。

总结

在项目开发过程中,前端需要与后端配合。久而久之,一部分前端同学涉足接口转发的中间层,成为业务+Node工程师。

同样,前端也需要与UI配合,会不会如上文所设想,未来会出现一批UI+前端工程师呢?

收起阅读 »

【Web动画】科技感十足的暗黑字符雨动画

本文将使用纯 CSS,带大家一步一步实现一个这样的科幻字符跳动背景动画。类似于这样的字符雨动画: 或者是类似于这样的: 运用在一些类似科技主题的背景之上,非常的添彩。 文字的竖排 首先第一步,就是需要实现文字的竖向排列: 这一步非常的简单,可能方法也很多...
继续阅读 »

本文将使用纯 CSS,带大家一步一步实现一个这样的科幻字符跳动背景动画。类似于这样的字符雨动画:


Digital Char Rain Animation


或者是类似于这样的:


CodePen Home<br />
Matrix digital rain (animated version) By yuanchuan


运用在一些类似科技主题的背景之上,非常的添彩。


文字的竖排


首先第一步,就是需要实现文字的竖向排列:



这一步非常的简单,可能方法也很多,这里我简单罗列一下:



  1. 使用控制文本排列的属性 writing-mode 进行控制,可以通过 writing-mode: vertical-lr 等将文字进行竖向排列,但是对于数字和英文,将会旋转 90° 展示:


<p>1234567890ABC</p>
<p>中文或其他字符ォヶ</p>

p {
writing-mode: vertical-lr;
}


当然这种情况下,英文字符的展示不太满足我们的需求。



  1. 控制容器的宽度,控制每行只能展示 1 个中文字符。


这个方法算是最简单便捷的方法了,但是由于英文的特殊性,要让连续的长字符串自然的换行,我们还需要配合 word-break: break-all


p {
width: 12px;
font-size: 10px;
word-break: break-all;
}

效果如下,满足需求:



使用 CSS 实现随机字符串的选取


为了让我们的效果更加自然。每一行的字符的选取最好是随机的。


但是要让 CSS 实现随机生成每一行的字符可太难了。所以这里我们请出 CSS 预处理器 SASS/LESS 。


而且由于不太可能利用 CSS 给单个标签内,譬如 <p> 标签插入字符,所以我们把标签内的字符展示,放在每个 <p> 元素的伪元素 ::beforecontent 当中。


我们可以提前设置好一组字符串,然后利用 SASS function 随机生成每一次元素内的 content,伪代码如下:


<div>
<p></p>
<p></p>
<p></p>
</div>

$str: 'ぁぃぅぇぉかきくけこんさしすせそた◁▣▤▥▦▧♂♀♥☻►◄▧▨♦ちつってとゐなにぬねのはひふへほゑまみむめもゃゅょゎをァィゥヴェォカヵキクケヶコサシスセソタチツッテトヰンナニヌネノハヒフヘホヱマミムメモャュョヮヲㄅㄉㄓㄚㄞㄢㄦㄆㄊㄍㄐㄔㄗㄧㄛㄟㄣㄇㄋㄎㄑㄕㄘㄨㄜㄠㄤㄈㄏㄒㄖㄙㄩㄝㄡㄥabcdefghigklmnopqrstuvwxyz123456789%@#$<>^&*_+';
$length: str-length($str);

@function randomChar() {
$r: random($length);
@return str-slice($str, $r, $r);
}

@function randomChars($number) {
$value: '';

@if $number > 0 {
@for $i from 1 through $number {
$value: $value + randomChar();
}
}
@return $value;
}

p:nth-child(1)::before {
content: randomChars(25);
}
p:nth-child(2)::before {
content: randomChars(25);
}
p:nth-child(3)::before {
content: randomChars(25);
}

简单解释下上面的代码:



  1. $str 定义了一串随机字符串,$length 表示字符串的长度

  2. randomChar() 中利用了 SASS 的 random() 方法,每次随机选取一个 0 - $length 的整形数,记为 $r,再利用 SASS 的 str-slice 方法,每次从 $str 中选取一个下标为 $r 的随机字符

  3. randomChars() 就是循环调用 randomChar() 方法,从 $str 中随机生成一串字符串,长度为传进去的参数 $number


这样,每一列的字符,每次都是不一样的:




当然,上述的方法我认为不是最好的,CSS 的伪元素的 content 是支持字符编码的,譬如 content: '\3066'; 会被渲染成字符 ,这样,通过设定字符区间,配合 SASS function 可以更好的生成随机字符,但是我尝试了非常久,SASS function 生成的最终产物会在 \3066 这样的数字间添加上空格,无法最终通过字符编码转换成字符,最终放弃...



使用 CSS 实现打字效果


OK,继续,接下来我们要使用 CSS 实现打字效果,就是让字符一个一个的出现,像是这样:


纯 CSS 实现文字输入效果


这里借助了 animation 的 steps 的特性实现,也就是逐帧动画。


从左向右和从上向下原理是一样的,以从左向右为例,假设我们有 26 个英文字符,我们已知 26 个英文字符组成的字符串的长度,那么我们只需要设定一个动画,让它的宽度变化从 0 - 100% 经历 26 帧即可,配合 overflow: hidden,steps 的每一帧即可展出一个字符。


当然,这里需要利用一些小技巧,我们如何通过字符的数量知道字符串的长度呢?


划重点:通过等宽字体的特性,配合 CSS 中的 ch 单位



如果不了解什么是等宽字体族,可以看看我的这篇文章 -- 《你该知道的字体 font-family》



CSS 中,ch 单位表示数字 “0” 的宽度。如果字体恰巧又是等宽字体,即每个字符的宽度是一样的,此时 ch 就能变成每个英文字符的宽度,那么 26ch 其实也就是整个字符串的长度。


利用这个特性,配合 animation 的 steps,我们可以轻松的利用 CSS 实现打字动画效果:


<h1>Pure CSS Typing animation.</h1>

h1 {
font-family: monospace;
width: 26ch;
white-space: nowrap;
overflow: hidden;
animation: typing 3s steps(26, end);
}

@keyframes typing {
0{
width: 0;
}
100% {
width: 26ch;
}
}

就可以得到如下结果啦:


纯 CSS 实现文字输入效果


完整的代码你可以戳这里:


CodePen Demo -- 纯 CSS 实现文字输入效果


改造成竖向打字效果


接下来,我们就运用上述技巧,改造一下。将一个横向的打字效果改造成竖向的打字效果。


核心的伪代码如下:


<div>
<p></p>
<p></p>
<p></p>
</div>

$str: 'ぁぃぅぇぉかきくけこんさしすせそた◁▣▤▥▦▧♂♀♥☻►◄▧▨♦ちつってとゐなにぬねのはひふへほゑまみむめもゃゅょゎをァィゥヴェォカヵキクケヶコサシスセソタチツッテトヰンナニヌネノハヒフヘホヱマミムメモャュョヮヲㄅㄉㄓㄚㄞㄢㄦㄆㄊㄍㄐㄔㄗㄧㄛㄟㄣㄇㄋㄎㄑㄕㄘㄨㄜㄠㄤㄈㄏㄒㄖㄙㄩㄝㄡㄥabcdefghigklmnopqrstuvwxyz123456789%@#$<>^&*_+';
$length: str-length($str);

@function randomChar() {
$r: random($length);
@return str-slice($str, $r, $r);
}

@function randomChars($number) {
$value: '';

@if $number > 0 {
@for $i from 1 through $number {
$value: $value + randomChar();
}
}
@return $value;
}

p {
width: 12px;
font-size: 10px;
word-break: break-all;
}

p::before {
content: randomChars(20);
color: #fff;
animation: typing 4s steps(20, end) infinite;
}

@keyframes typing {
0% {
height: 0;
}
25% {
height: 100%;
}
100% {
height: 100%;
}
}

这样,我们就实现了竖向的打字效果:



当然,这样看上去比较整齐划一,缺少了一定的随机,也就缺少了一定的美感。


基于此,我们进行 2 点改造:



  1. 基于动画的时长 animation-time、和动画的延迟 animation-delay,增加一定幅度内的随机

  2. 在每次动画的末尾或者过程中,重新替换伪元素的 content,也就是重新生成一份 content


可以借助 SASS 非常轻松的实现这一点,核心的 SASS 代码如下:


$n: 3;
$animationTime: 3;
$perColumnNums: 20;

@for $i from 0 through $n {
$content: randomChars($perColumnNums);
$contentNext: randomChars($perColumnNums);
$delay: random($n);
$randomAnimationTine: #{$animationTime + random(20) / 10 - 1}s;

p:nth-child(#{$i})::before {
content: $content;
color: #fff;
animation: typing-#{$i} $randomAnimationTine steps(20, end) #{$delay * 0.1s * -1} infinite;
}

@keyframes typing-#{$i} {
0% {
height: 0;
}
25% {
height: 100%;
}
100% {
height: 100%;
content: $contentNext;
}
}
}

看看效果,已经有不错的改观:



当然,上述由横向打字转变为竖向打字效果其实是有一些不一样的。在现有的竖向排列规则下,无法通过 ch 配合字符数拿到实际的竖向高度。所以这里有一定的取舍,实际放慢动画来看,没个字的现出不一定是完整的。


当然,在快速的动画效果下几乎是察觉不到的。


增加光影与透明度变化


最后一步,就是增加光影及透明度的变化。


最佳的效果是要让每个新出现的字符保持亮度最大,同时已经出现过的字符亮度慢慢减弱。


但是由于这里我们无法精细操控每一个字符,只能操控每一行字符,所以在实现方式上必须另辟蹊径。


最终的方式是借用了另外一个伪元素进行同步的遮罩以实现最终的效果。下面我们就来一步一步看看过程。


给文字增添亮色及高光


第一步就是给文字增添亮色及高光,这点非常容易,就是选取一个黑色底色下的亮色,并且借助 text-shadow 让文字发光。


p::before {
color: rgb(179, 255, 199);
text-shadow: 0 0 1px #fff, 0 0 2px #fff, 0 0 5px currentColor, 0 0 10px currentColor;
}

看看效果,左边是白色字符,中间是改变字符颜色,右边是改变了字体颜色并且添加了字体阴影的效果:



给文字添加同步遮罩


接下来,就是在文字动画的行进过程中,同步添加一个黑色到透明的遮罩,尽量还原让每个新出现的字符保持亮度最大,同时已经出现过的字符亮度慢慢减弱。


这个效果的示意图大概是这样的,这里我将文字层和遮罩层分开,并且底色从黑色改为白色,方便理解:


蒙层遮罩原理图


大概的遮罩的层的伪代码如下,用到了元素的另外一个伪元素:


p::after {
content: '';
background: linear-gradient(rgba(0, 0, 0, .9), transparent 75%, transparent);
background-size: 100% 220%;
background-repeat: no-repeat;
animation: mask 4s infinite linear;
}

@keyframes mask {
0% {
background-position: 0 220%;
}
30% {
background-position: 0 0%;
}
100% {
background-position: 0 0%;
}
}

好,合在一起的最终效果大概就是这样:



通过调整 @keyframes mask 的一些参数,可以得到不一样的字符渐隐效果,需要一定的调试。


完整代码及效果


OK,拆解了一下主要的步骤,最后上一下完整代码,应用了 Pug 模板引擎和 SASS 语法。


完整代码加起来不过 100 行。


.g-container
-for(var i=0; i<50; i++)
p

@import url('https://fonts.googleapis.com/css2?family=Inconsolata:wght@200&display=swap');

$str: 'ぁぃぅぇぉかきくけこんさしすせそた◁▣▤▥▦▧♂♀♥☻►◄▧▨♦ちつってとゐなにぬねのはひふへほゑまみむめもゃゅょゎをァィゥヴェォカヵキクケヶコサシスセソタチツッテトヰンナニヌネノハヒフヘホヱマミムメモャュョヮヲㄅㄉㄓㄚㄞㄢㄦㄆㄊㄍㄐㄔㄗㄧㄛㄟㄣㄇㄋㄎㄑㄕㄘㄨㄜㄠㄤㄈㄏㄒㄖㄙㄩㄝㄡㄥabcdefghigklmnopqrstuvwxyz123456789%@#$<>^&*_+';
$length: str-length($str);
$n: 50;
$animationTime: 4;
$perColumnNums: 25;

@function randomChar() {
$r: random($length);
@return str-slice($str, $r, $r);
}

@function randomChars($number) {
$value: '';

@if $number > 0 {
@for $i from 1 through $number {
$value: $value + randomChar();
}
}
@return $value;
}

body, html {
width: 100%;
height: 100%;
background: #000;
display: flex;
overflow: hidden;
}

.g-container {
width: 100vw;
display: flex;
justify-content: space-between;
flex-wrap: nowrap;
flex-direction: row;
font-family: 'Inconsolata', monospace, sans-serif;
}

p {
position: relative;
width: 5vh;
height: 100vh;
text-align: center;
font-size: 5vh;
word-break: break-all;
white-space: pre-wrap;

&::before,
&::after {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 100%;
overflow: hidden;
}
}

@for $i from 0 through $n {
$content: randomChars($perColumnNums);
$contentNext: randomChars($perColumnNums);
$delay: random($n);
$randomAnimationTine: #{$animationTime + random(20) / 10 - 1}s;

p:nth-child(#{$i})::before {
content: $content;
color: rgb(179, 255, 199);
text-shadow: 0 0 1px #fff, 0 0 2px #fff, 0 0 5px currentColor, 0 0 10px currentColor;
animation: typing-#{$i} $randomAnimationTine steps(20, end) #{$delay * 0.1s * -1} infinite;
z-index: 1;
}

p:nth-child(#{$i})::after {
$alpha: random(40) / 100 + 0.6;
content: '';
background: linear-gradient(rgba(0, 0, 0, $alpha), rgba(0, 0, 0, $alpha), rgba(0, 0, 0, $alpha), transparent 75%, transparent);
background-size: 100% 220%;
background-repeat: no-repeat;
animation: mask $randomAnimationTine infinite #{($delay - 2) * 0.1s * -1} linear;
z-index: 2;
}

@keyframes typing-#{$i} {
0% {
height: 0;
}
25% {
height: 100%;
}
100% {
height: 100%;
content: $contentNext;
}
}
}

@keyframes mask{
0% {
background-position: 0 220%;
}
30% {
background-position: 0 0%;
}
100% {
background-position: 0 0%;
}
}

最终效果也就是题图所示:


Digital Char Rain Animation


完整的代码及演示效果你可以戳这里:


CodePen Demo -- Digital Char Rain Animation



链接:https://juejin.cn/post/6991657194282450951
收起阅读 »

前端button组件之涟漪效果

前言 在前端项目中,我们常常会使用到button组件进行事件的触发,而一些项目为了更好的交互效果,加入了一系列的动画,例如:脉冲、果冻、涟漪、滑箱等特效。 今天我们来讲讲如何使用HTML CSS和JavaScript来实现涟漪效果,我们先看下成品: 看完是...
继续阅读 »

前言


在前端项目中,我们常常会使用到button组件进行事件的触发,而一些项目为了更好的交互效果,加入了一系列的动画,例如:脉冲、果冻、涟漪、滑箱等特效。


今天我们来讲讲如何使用HTML CSSJavaScript来实现涟漪效果,我们先看下成品:


1.gif


5.png


看完是不是也想给自己项目整一个这样子的效果😎😎


原理


如图,我们需要两个元素来实现这个涟漪效果,当button被点击时,在button元素中放置一个元素,执行一个绽开动画效果,执行完毕后把buttion里的元素移除。


2.png


用码实现


码出基本样式

先创建一对div标签,作为一个基础按钮元素。后面我们将这对div称之为按钮。


<div id="btn" class="button">Click me</div>

为按钮添加基本样式,这里需要给按钮设定position:relative,后续我们涟漪效果是通过绝对定位来实现的。


.button {
   -webkit-user-select: none;
   -moz-user-select: none;
   -ms-user-select: none;
   user-select: none;
   position: relative;
   display: inline-block;
   color: #fff;
   padding: 14px 40px;
   background: linear-gradient(90deg, #0bc7f1, #c471ed);
   border-radius: 45px;
   margin: 0 15px;
   font-size: 24px;
   font-weight: 400;
   text-decoration: none;
   overflow: hidden;
   box-shadow: 1px 1px 3px #7459e9;
}

3.png


当样式写完之后我们按钮的样式就跟效果图上的按钮一模一样了,由于我们JavaScript部分还没有写以及实现涟漪效果还没有实现,此时我们点击按钮是没有涟漪效果的,接下来我们要就添加涟漪效果了。


👇 👇 👇 继续往下看 👇 👇 👇


码出链漪

给按钮添加一个涟漪效果,在按钮div中添加一个span标签,并绑定一个overlay


<div id="btn" class="button">
  Click me
   <span class="overlay"></span>
</div>

这个span标签是我们要实现涟漪效果的元素,给元素设置绝对定位,让元素脱离文件流,不为该元素预留出空间。默认我们定义在top:0left:0,再通过transform属性将元素偏移居中对齐。透明度设置0.5,绑定一个blink帧动画函数。


.overlay {
   position: absolute;
   height: 400px;
   width: 400px;
   background-color: #fff;
   top: 0;
   left: 0;
   transform: translate(-50%, -50%);
   border-radius: 50%;
   opacity: .5;
   animation: blink .5s linear infinite;
}

添加一个帧动画,命名为blink,将span元素的宽度,高度从0px过渡到400px,及透明度从设定的0.5过渡到0,渐渐向外绽开,这样子就形成了涟漪效果了,当我们把span元素挂载上去我们可以看下效果,接下来我们将通过JavaScript来获取鼠标点击位置来决定绽开的位置。


4.gif


注意


div中的span标签删除或者注释掉,后面我们将使用JavaScript来添加这个span标签


div中的span标签删除或者注释掉,后面我们将使用JavaScript来添加这个span标签


div中的span标签删除或者注释掉,后面我们将使用JavaScript来添加这个span标签


码出点击效果

这里我们先引入jQuery这个库,为了方便使用,这里我就使用cdn方式来引入。



这里给大家推荐一个国内的CDN库:http://www.bootcdn.cn



<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js"></script>

创建一个addRipple方法,先创建一个绑定overlay类的span标签,获取鼠标点击页面的xy值,绑定对应的left值和top值,绑定之后把span元素添加到div中。


设定一个定时器,当动画执行完毕后把span元素移除掉,减少内存的占用。


const addRipple = function (e) {
   let overlay = $("<span></span>")
   const x = e.clientX - e.target.offsetLeft
   const y = e.clientY - e.target.offsetTop;
   overlay.css(
      {
           left: x + 'px',
           top: y + 'px'
      }
  )
   $(this).append(overlay)
   setTimeout(() => {
       overlay.remove()
  }, 500)
}

div绑定addRipple事件,按钮就实现跟开头效果图一样的页面啦!


$('#btn').click(addRipple);

1.gif


5.png


链接:https://juejin.cn/post/6991752850644664333

收起阅读 »

我给公司封装的组件帮公司提效了60%

前置内容 在公司开发中或多或少都会有几个管理系统的项目,而对于这些系统大多页面都是表单、表格组成,为了不花费太多精力在这些不那么需要定制化的页面上,一般都会选择去用组件库完成,这个时候就如果将这些简单、枯燥的事情用配置项完成,把精力放在更有挑战的事情上,那么工...
继续阅读 »

前置内容


在公司开发中或多或少都会有几个管理系统的项目,而对于这些系统大多页面都是表单、表格组成,为了不花费太多精力在这些不那么需要定制化的页面上,一般都会选择去用组件库完成,这个时候就如果将这些简单、枯燥的事情用配置项完成,把精力放在更有挑战的事情上,那么工作摸鱼的时间又多了不少。下面就分享下我花了近一个月的时间为公司封装的组件。


涉及到的技术:



  • vue

  • element-ui


就基于上面两个来实现的,使用起来也非常简单,并不需要你去记太多prop,就是element哪些。你只需要这么简单的配置,表单项就出来了







1.gif


选择之后, testFormModel对象就会是这样


{
 treeProp: '三级 1-1-1',
}

为了方便使用, 我需要下拉提供这么几个功能:



  • 根据url动态请求数据(可携带参数,且参数变动之后重新发起请求)

  • 需要获取动态请求回来的数据

  • 需要提供数据格式化的能力(数据格式化显示是为了不变动数据的情况正确显示页面)

  • 需要实现过滤功能


{
 label: '树形下拉',
 // formModel绑定的属性
 prop: 'treeProp',
 type: 'treeSelect',
 url: 'xxxx',
 params: {
   query: 'all',
},
 resolveData: (data) => {
   this.xxx = data
}
 nodeKey: 'dyId',
 props: {
   label: 'name',
   children: 'sublevel'
},
 multiple: true,
 checkStrictly: false,
 filterable: true,
}

效果图💗


2.gif



简单的配置下这树形功能就非常的强大了,同样也支持用户自己去配置懒加载数据



{
 lazy: true,
 load: this.loadNode,
}

这个时候就不需要去配置动态请求的哪些的配置,由用户自己实例接口的懒加载数据请求


我们需要完成什么样的东西?


整体效果图💗


3.gif



  • 配合侧边栏,校验失败右侧对应label标红

  • 点击右侧label视图自动滚动到对应表单项,并激活表单项

  • 侧边栏可配置(可以需要,也可以不配置)



这就是后面我们要实现的组件库,本文只是抛转引玉,对后面要做的事件大概说下,后续文章会很详细的分享整个组件封装的架构和思路,对于表单的一些「原子组件」的实现,之前也分享过一些,感兴趣的可以关注我的专栏:组件封装最佳实践



表单支持的组件



  • el-input/el-autocomplete

  • el-select

  • treeSelect 「集成」

  • el-switch

  • el-checkbox/el-radio/el-raiod-group/el-checkbox-group

  • el-date-picker/el-time-picker

  • el-cascader/el-cascader-panel

  • table 「集成」


有些功能还在完善,暂时就没有共享代码。后续会发布到npm提供下载,通过Vue.use()使用插件的方式使用


4.png


如何使用?


main.js


// 引用插件
import './plugins'

plugins.js


import './element-ui'
import './dynamic-ui'


这套组件是依赖element-ui封装的,所以前提是需要使用element



dynamic-ui.js


import Vue from 'vue'
import dynamicUI from 'dynamic-ui'
import 'dynamic-ui/lib/index.scss'
// 向表单添加组件类型
import DynamicTable from '@/components/DynamicTable/src/index.vue'

import { getToken } from '@/utils/auth'
import request from '@/utils/request'

Vue.component(DynamicTable.name, DynamicTable)
Vue.use(dynamicUI, {
 request, // 动态请求数据的方法
 baseURI: process.env.VUE_APP_BASE_API,
 parseData: () => {}, // 解析接口返回数据的方法
 requestHeaders: { // 请求头
   Authorization: getToken()
},
 // 需要动态添加到表单组件的类型
 addFormComponent: [
  {
     type: 'table',
     name: DynamicTable.name
  }
]
})

组件库提供的功能



  • 传入[全局, 局部]的request

  • 传入[全局, 局部]的parseData

  • 传入requestHeaders请求头参数

  • 动态添加组件作为表单项


支持动态请求的数据组件



  • select

  • treeSelect

  • checkbox/radio

  • table

  • cascader/cascader-panel


以上几个组件类型在element基础上进行了扩展,允许用户动态请求数据,统一prop这样



三者的使用场景


这里分别说下parseData/formatter/resoveData的使用场景


parsseData


一般我们在使用axios都会封装响应拦截器,做业务码的统一处理,但一般不会去变动data,现在有个问题是这样的,后端返回的数据是这样的


{
code: '200',
message: 'xxx',
data: {
...
pageData: [] // 这个才是我们要的数据
}
}

只有个别数据是这样的格式,这个时候全局配置就不去动,可以允许去传递单个组件的parseData来解决这个问题


formatter


用于在不影响原有数据的情况下格式化数据以正确显示页面,比如这样


formatter: (value) => {
return `dy-${value}`
},

5.png


resolveData


获取响应式的数据:直接变动数据,可直接影响页面。


resolveData: (data) => {
console.log(data)
data[0].name = '欢迎关注:前端自学驿站'
},

6.png



以上所有类型都会在请求参数变动之后重新请求数据,所以如果后端提供分页接口,前端也就能实现分页懒加载的功能



表格组件支持的类型


表格会支持



  • 展示模式

  • 编辑模式(支持所有表单组件类型,包括用户动态添加的)



本来打算将组件的功能大致的过一遍,但是写到这太晚了,有点肝不动了~,目前就是表格编辑模式这块还有些地方需要完善,后续完善测试通过之后就会立马发布,欢迎大家持续关注~



写在最后


如果文章中有那块写的不太好或有问题欢迎大家指出,我也会在后面的文章不停修改。也希望自己进步的同时能跟你们一起成长。喜欢我文章的朋友们也可以关注一下,我会很感激第一批关注我的人。此时,年轻的我和你,轻装上阵;而后,富裕的你和我,满载而归。


业精于勤,荒于嬉



链接:https://juejin.cn/post/6991481121540145160

收起阅读 »

面对 this 指向丢失,尤雨溪在 Vuex 源码中是怎么处理的

1. 前言简单再说说 this 指向和尤大在 Vuex 源码中是怎么处理 this 指向丢失的。 2. 对象中的this指向 var person = { name: '若川', say: function(text){ console.log...
继续阅读 »

1. 前言简单再说说 this 指向和尤大在 Vuex 源码中是怎么处理 this 指向丢失的。


2. 对象中的this指向


var person = {
name: '若川',
say: function(text){
console.log(this.name + ', ' + text);
}
}
console.log(person.name);
console.log(person.say('在写文章')); // 若川, 在写文章
var say = person.say;
say('在写文章'); // 这里的this指向就丢失了,指向window了。(非严格模式)

3. 类中的this指向


3.1 ES5


// ES5
var Person = function(){
this.name = '若川';
}
Person.prototype.say = function(text){
console.log(this.name + ', ' + text);
}
var person = new Person();
console.log(person.name); // 若川
console.log(person.say('在写文章'));
var say = person.say;
say('在写文章'); // 这里的this指向就丢失了,指向 window 了。

3.2 ES6


// ES6
class Person{
construcor(name = '若川'){
this.name = name;
}
say(text){
console.log(`${this.name}, ${text}`);
}
}
const person = new Person();
person.say('在写文章')
// 解构
const { say } = person;
say('在写文章'); // 报错 this ,因为ES6 默认启用严格模式,严格模式下指向 undefined

4. 尤大在Vuex源码中是怎么处理的


先看代码


class Store{
constructor(options = {}){
this._actions = Object.create(null);
// bind commit and dispatch to self
// 给自己 绑定 commit 和 dispatch
const store = this
const { dispatch, commit } = this
// 为何要这样绑定 ?
// 说明调用commit和dispach 的 this 不一定是 store 实例
// 这是确保这两个函数里的this是store实例
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
}
this.commit = function boundCommit (type, payload, options) {
return commit.call(store, type, payload, options)
}
}
dispatch(){
console.log('dispatch', this);
}
commit(){
console.log('commit', this);
}
}
const store = new Store();
store.dispatch(); // 输出结果 this 是什么呢?

const { dispatch, commit } = store;
dispatch(); // 输出结果 this 是什么呢?
commit(); // 输出结果 this 是什么呢?

输出结果截图


结论:非常巧妙的用了calldispatchcommit函数的this指向强制绑定到store实例对象上。如果不这么绑定就报错了。


4.1 actions 解构 store


其实Vuex源码里就有上面解构const { dispatch, commit } = store;的写法。想想我们平时是如何写actions的。actions中自定义函数的第一个参数其实就是 store 实例。


这时我们翻看下actions文档https://vuex.vuejs.org/zh/guide/actions.html


const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})

也可以用解构赋值的写法。


actions: {
increment ({ commit }) {
commit('increment')
}
}

有了Vuex源码构造函数里的call绑定,这样this指向就被修正啦~不得不说祖师爷就是厉害。这一招,大家可以免费学走~


接着我们带着问题,为啥上文中的context就是store实例,有dispatchcommit这些方法呢。继续往下看。


4.2 为什么 actions 对象里的自定义函数 第一个参数就是 store 实例。


以下是简单源码,有缩减,感兴趣的可以看我的文章 Vuex 源码文章


class Store{
construcor(){
// 初始化 根模块
// 并且也递归的注册所有子模块
// 并且收集所有模块的 getters 放在 this._wrappedGetters 里面
installModule(this, state, [], this._modules.root)
}
}

接着我们看installModule函数中的遍历注册 actions 实现


function installModule (store, rootState, path, module, hot) {
// 省略若干代码
// 循环遍历注册 action
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
}

接着看注册 actions 函数实现 registerAction


/**
* 注册 mutation
* @param {Object} store 对象
* @param {String} type 类型
* @param {Function} handler 用户自定义的函数
* @param {Object} local local 对象
*/
function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = [])
// payload 是actions函数的第二个参数
entry.push(function wrappedActionHandler (payload) {
/**
* 也就是为什么用户定义的actions中的函数第一个参数有
* { dispatch, commit, getters, state, rootGetters, rootState } 的原因
* actions: {
* checkout ({ commit, state }, products) {
* console.log(commit, state);
* }
* }
*/
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload)
// 源码有删减
}

比较容易发现调用顺序是 new Store() => installModule(this) => registerAction(store) => let res = handler.call(store)


其中handler 就是 用户自定义的函数,也就是对应上文的例子increment函数。store实例对象一路往下传递,到handler执行时,也是用了call函数,强制绑定了第一个参数是store实例对象。


actions: {
increment ({ commit }) {
commit('increment')
}
}

这也就是为什么 actions 对象中的自定义函数的第一个参数是 store 对象实例了。


好啦,文章到这里就基本写完啦~相对简短一些。应该也比较好理解。


5. 最后再总结下 this 指向


摘抄下面试官问:this 指向文章结尾。


如果要判断一个运行中函数的 this 绑定, 就需要找到这个函数的直接调用位置。 找到之后
就可以顺序应用下面这四条规则来判断 this 的绑定对象。



  1. new 调用:绑定到新创建的对象,注意:显示return函数或对象,返回值不是新创建的对象,而是显式返回的函数或对象。

  2. call 或者 apply( 或者 bind) 调用:严格模式下,绑定到指定的第一个参数。非严格模式下,nullundefined,指向全局对象(浏览器中是window),其余值指向被new Object()包装的对象。

  3. 对象上的函数调用:绑定到那个对象。

  4. 普通函数调用: 在严格模式下绑定到 undefined,否则绑定到全局对象。


ES6 中的箭头函数:不会使用上文的四条标准的绑定规则, 而是根据当前的词法作用域来决定this, 具体来说, 箭头函数会继承外层函数,调用的 this 绑定( 无论 this 绑定到什么),没有外层函数,则是绑定到全局对象(浏览器中是window)。 这其实和 ES6 之前代码中的 self = this 机制一样。




链接:https://juejin.cn/post/6991701614612447239


收起阅读 »

搞懂Objective-C中的ARC

写这篇文章的背景前段时间招人,面试了一个多月,有关内存的基础问题,能完全答出来的竟无一人,回答出百分之80的人也寥寥无几,于是决定写这篇文章,简单业务流水线道友们一般都能写出符合需求,可以正常工作的代码,稍微复杂点的也许也不再话下,一旦涉及到性能、鲁棒性等要求...
继续阅读 »

写这篇文章的背景

前段时间招人,面试了一个多月,有关内存的基础问题,能完全答出来的竟无一人,回答出百分之80的人也寥寥无几,于是决定写这篇文章,简单业务流水线道友们一般都能写出符合需求,可以正常工作的代码,稍微复杂点的也许也不再话下,一旦涉及到性能、鲁棒性等要求很高的项目,不能真正理解内存的程序员将给整个项目带来灾难和隐藏的坑,所以本文旨在让道友们真正理解内存,这是再基础不过的东西,然而又是必须知道的东西,让我们一起,重温下基础吧,本文不打算大量罗列源码,而是从显而易见的东西开始

先从一个小问题开始

面试官:alloc的对象都存储在堆上是吗?
候选人:是的
面试官:好的,静态变量存储在数据段是吗?
候选人:是的,未初始化的存储在bss段,初始化的存储在data段
面试官:很好,不错,看一段代码,这两行代码可以写成一行吗

static NSObject *obj = nil;
obj = [[NSObject alloc] init];

像这样:

static NSObject *obj = [[NSObject alloc] init];
候选人:应该可以吧(get不到问题的点)
面试官:那内存是如何分布的呢?
候选人:不可能同时存储在数据段和堆区吧(小声嘀咕)
面试官:顺着这个思路再思考下
候选人:。。。(过去三分钟)
面试官:好的,换种问法,单例很常见吧,那么它可以手动释放吗?
候选人:既然用单例来实现,说明整个程序生命周期需要共享一个实例,不会存在需要释放的场景
面试官:比如一个app里面有很多业务线,业务线退出的时候,需要清理业务线所占内存,如若有单例存在,这个时候可以手动释放吗?
候选人:把指针置为nil?,应该可以吧(试图得到面试官的提示)
面试官:那这以后呢,单例就不占用内存了吗?
候选人:。。。(彻底卡住)

Objective-C中的指针

可曾听过一句话,一切OC对象皆指针,嗯~这句话很对,我想开讲之前有必要说下究竟OC中的指针是什么,对象又是什么,它们不是一个东西嘛?iOS操作系统是基于unix的一个分支开发的,自然继承了unix内核的部分功能,内存分区为:栈、堆、数据段、常量区、代码段,本文不打算枯燥的讲理论,我们设计几个demo直接从表象出发,去探寻理论,会不会记忆更深刻呢?!

还是从上面的代码出发

static NSObject *obj = [[NSObject alloc] init];

这样是无法通过编译的,编译器提示Initializer element is not a compile-time constant,意思是初始化的元素不是编译期分配的常量,obj指针即是分配在数据段的变量,在编译时就需要分配内存,alloc的对象内存开辟在堆上并且是运行时分配的,用运行时的对象去初始化编译期的指针是没有办法做到的,所以编译器提示我们这样做是不对的
以上得出个结论:

  • 栈、堆内存是运行时分配的
  • 数据段内存是编译时分配的(这么说并不完全准确,往下看)

(注:app的可执行文件二进制里面包括静态库和源码,动态库和资源文件会单独存储,系统的动态库整个操作系统共享一份)
注意我们讲的是内存,我们的可执行程序是以二进制的形式存在于手机上的,这里说的代码段并非用于存储二进制文件,而是存储程序启动时候被载入内存中的可执行代码,紧随其后,操作系统会为程序中的全局变量和静态变量在数据段开辟内存(起初会存储在bss段,初始化后会清空bss段,存储在data段),常量的内存空间开辟和初始化是一起执行的,初始化后不再有机会改变,所以准确的说:

  • 代码段内存是装载时分配的,数据段和常量区紧随其后,这些都发生在动态链接之前

看一个demo:环境是x86模拟器,嗯~64位架构

@interface Person : NSObject

@property (nonatomic, assign) int a;
@property (nonatomic, assign) int b;
@property (nonatomic, assign) int c;

@end
Person *obj = nil;
NSLog(@"%lu", sizeof(obj));
NSLog(@"%lu", malloc_size((__bridge const void *)obj));
obj = [[Person alloc] init];
NSLog(@"%lu", malloc_size((__bridge const void *)obj));
NSLog(@"%lu", class_getInstanceSize(Person.class));

2021-06-05 16:41:36.982538+0800 test[69294:38540223] 8
2021-06-05 16:41:36.982640+0800 test[69294:38540223] 0
2021-06-05 16:41:36.982712+0800 test[69294:38540223] 32
2021-06-05 16:41:36.982772+0800 test[69294:38540223] 24

首先我们看Person的实例对象有哪些成员需要在堆上开辟内存空间,一个isa指针8个字节,三个int类型变量12个字节,总共20个字节
控制台输出sizeof是8,证明指针本身在64位系统占用8个字节,紧接着malloc_size输出0,证明只是一个指向nil的指针,还没有在堆区分配内存,malloc_size然后输出32证明在堆区开辟了32字节的内存,16字节为一个开辟单元是iOS系统的规范,所以要想存储20个字节就需要开辟两个单元的大小,就是32字节,最后class_getInstanceSize输出24证明对象实际占用24字节,是因为iOS系统内存存储是按照8字节对齐的,所以20个字节之后需要补齐4个字节的0用于内存对齐,无论是开辟空间对齐,还是存储对齐都是操作系统设计之初的效率考虑

好的~我们回到上面说的单例释放问题,是否可手动释放呢?答案是部分可以,部分不能,原因是,堆栈的内存动态分配,动态释放,而数据段、常量区、代码段内存直到app进程退出才会释放,所以单例指针置为nil的时候,堆区对象的引用计数为0会自动释放,而还有一个指针存储在数据段,占用8个字节

什么是ARC,引用计数存储在哪里,哪些对象是通过引用计数来管理内存的

面试官:如下代码在MRC环境会有内存泄漏,为什么?

- (void)viewDidLoad {
[super viewDidLoad];
NSObject *obj = [[NSObject alloc] init];
}
候选人:因为obj没有调用release或者autorelease
面试官:嗯,那还是MRC环境,下面的代码会泄露吗?

- (void)viewDidLoad {
[super viewDidLoad];
static NSObject *obj = nil;
obj = [[NSObject alloc] init];
}

候选人:嗯~~~会吧
面试官:你怎样理解内存泄漏,什么叫内存泄漏
候选人:就是一个对象,没有释放掉,就泄露了
面试官:啊~~~,那么能在ARC环境举个内存泄漏的例子吗
候选人:比如block是self的属性,然后里面引用了self,没有加__weak
面试官:这个是循环引用吧,所以循环引用会内存泄漏是吗
候选人:是的(over)

只有堆区对象才有引用计数,引用计数存储在对象本身的结构里,嗯~可以通过isa指针辗转访问到
ARC是自动引用计数,即编译器在编译期在合适的位置自动插入release或是autorelease

回到上面问题

- (void)viewDidLoad {
[super viewDidLoad];
NSObject *obj = [[NSObject alloc] init];
}

MRC下如上代码泄漏的根本原因是,obj是声明在栈上的指针,作用域之外自动释放,即大括号之外指针已经不存在了,但是alloc的对象引用计数是1,但是已经没有指针引用它了,所以这块堆内存将没有机会释放了,这就是内存泄漏

那么下面的代码在MRC下为什么就没有泄漏呢

- (void)viewDidLoad {
[super viewDidLoad];
static NSObject *obj = nil;
obj = [[NSObject alloc] init];
}

原因是这个obj指针声明在数据段,生命周期和app进程生命周期一致,虽然alloc的对象引用计数也始终为1,但是有个static指针一直引用它,所以这块堆内存没有泄漏

以上明确几个常见内存问题概念:

  • 野指针:堆内存已经释放,但是还有指针指向这块内存,就是野指针,访问野指针crash
  • 内存泄漏:堆区内存引用计数不为0,但是没有指针指向这块内存,内存碎片
  • 循环引用:堆内存之间存在相互强引用,并且没有第三种力量打破这个环,内存碎片
  • OOM:堆内存开辟大小不固定,超过系统的限制,crash
  • 栈溢出:栈内存大小是固定的,超过系统限制,crash

ARC下哪些对象是autorelease对象

面试官:ARC下除了__autoreleasing显式创建autorelease对象的方式,还有哪些情况会生成autorelease对象
候选人:alloc和new出来的对象都会加入默认的autoreleasePool中,所以都是autorelease对象
面试官:哦?那ARC下release关键字是被弃用了吗?
候选人:是的(斩钉截铁)

这么回答的人占3成,有些恐怖~,我们还是通过一个demo来探索下,__autoreleasing显式的创建autorelease对象比较明显,我们来聊下隐式的情况


__weak NSString *weak_String;
__weak NSString *weak_StringRelease;
__weak NSString *weak_StringAutorelease;

- (void)testArc {
[self createString];
NSLog(@"------%s------", __func__);
NSLog(@"%@", weak_String);
NSLog(@"%@\n\n", weak_StringRelease);
NSLog(@"%@\n\n", weak_StringAutorelease);
}

- (void)createString {
NSString *constAreaString = @"字面量string";
NSString *heapAreastring = [[NSString alloc] initWithFormat:@"堆区string-release"];
NSString *stringAutorelease = [NSString stringWithFormat:@"堆区string-autorelease"];
NSLog(@"%lu", malloc_size((__bridge const void *)constAreaString));
NSLog(@"%lu", malloc_size((__bridge const void *)heapAreastring));
NSLog(@"%lu", malloc_size((__bridge const void *)stringAutorelease));

weak_String = constAreaString;
weak_StringRelease = heapAreastring;
weak_StringAutorelease = stringAutorelease;

NSLog(@"------%s------", __func__);
NSLog(@"%@", weak_String);
NSLog(@"%@\n\n", weak_StringRelease);
NSLog(@"%@\n\n", weak_StringAutorelease);
}

- (void)viewDidLoad {
[super viewDidLoad];
[self testArc];
}

- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(@"------%s------", __func__);
NSLog(@"%@", weak_String);
NSLog(@"%@\n\n", weak_StringRelease);
NSLog(@"%@\n\n", weak_StringAutorelease);
}

结果如下:

2021-06-06 00:25:44.981838+0800 test[81234:39339960] 0
2021-06-06 00:25:44.981936+0800 test[81234:39339960] 64
2021-06-06 00:25:44.982022+0800 test[81234:39339960] 64
2021-06-06 00:25:44.982116+0800 test[81234:39339960] -------[ViewController createString]------
2021-06-06 00:25:44.982213+0800 test[81234:39339960] 字面量string
2021-06-06 00:25:44.982278+0800 test[81234:39339960] 堆区string-release
2021-06-06 00:25:44.982357+0800 test[81234:39339960] 堆区string-autorelease
2021-06-06 00:25:44.982428+0800 test[81234:39339960] -------[ViewController testArc]------
2021-06-06 00:25:44.982508+0800 test[81234:39339960] 字面量string
2021-06-06 00:25:44.982578+0800 test[81234:39339960] (null)
2021-06-06 00:25:44.982656+0800 test[81234:39339960] 堆区string-autorelease
2021-06-06 00:25:44.992637+0800 test[81234:39339960] -------[ViewController viewDidAppear:]------
2021-06-06 00:25:44.992753+0800 test[81234:39339960] 字面量string
2021-06-06 00:25:44.992830+0800 test[81234:39339960] (null)
2021-06-06 00:25:44.992899+0800 test[81234:39339960] (null)
首先看字面量的方式创建的字符串malloc_size为0,说明它不在堆上,嗯~在常量区,另外两个malloc_size都是正数,证明是堆区对象,createString函数三个对象都有值,而当testArc的时候weak_StringRelease的值已经为空,即离开了createString函数的作用域就释放了,此时weak_StringAutorelease还有值,直到viewDidAppear的时候只有字面量创建的对象才能够打印出来,这个结果说明了什么呢,说明编译器做了如下优化:

- (void)createString {
//这行类型变成了__NSCFConstantString
__NSCFConstantString *constAreaString = @"字面量string";
NSString *heapAreastring = [[NSString alloc] initWithFormat:@"堆区string-release"];
//这行在末尾插入了autorelease
NSString *stringAutorelease = [[NSString stringWithFormat:@"堆区string-autorelease"] autorelease];
NSLog(@"%lu", malloc_size((__bridge const void *)constAreaString));
NSLog(@"%lu", malloc_size((__bridge const void *)heapAreastring));
NSLog(@"%lu", malloc_size((__bridge const void *)stringAutorelease));

weak_String = constAreaString;
weak_StringRelease = heapAreastring;
weak_StringAutorelease = stringAutorelease;

NSLog(@"------%s------", __func__);
NSLog(@"%@", weak_String);
NSLog(@"%@\n\n", weak_StringRelease);
NSLog(@"%@\n\n", weak_StringAutorelease);

//在作用域末尾插入了release
[heapAreastring release];
}

注意关键点,有三处变化__NSCFConstantString *constAreaString、[[NSString stringWithFormat:@"堆区string-autorelease"] autorelease]、[heapAreastring release];

  • 字面量创建的直接存储在常量区
  • alloc出来的存储在堆区并且作用域结束前直接插入release
  • 通过stringWithFormat工厂方法创建的对象则在其后插入autorelease,这是因为工厂方法里面通过alloc分配堆内存,到返回出来以后其作用域已经结束,所以只能延迟释放了,否则没有办法返回非空对象

同样是这个demo把字符串的长度缩短,结果会很不一样

NSString *constAreaString = @"字面量";
NSString *heapAreastring = [[NSString alloc] initWithFormat:@"release"];
NSString *stringAutorelease = [NSString stringWithFormat:@"autorelease"];

2021-06-06 00:51:27.827647+0800 test[82761:39451059] 0
2021-06-06 00:51:27.827750+0800 test[82761:39451059] 0
2021-06-06 00:51:27.827843+0800 test[82761:39451059] 0
2021-06-06 00:51:27.827941+0800 test[82761:39451059] -------[ViewController createString]------
2021-06-06 00:51:27.828040+0800 test[82761:39451059] 字面量
2021-06-06 00:51:27.828139+0800 test[82761:39451059] release
2021-06-06 00:51:27.828224+0800 test[82761:39451059] autorelease
2021-06-06 00:51:27.828292+0800 test[82761:39451059] -------[ViewController testArc]------
2021-06-06 00:51:27.828369+0800 test[82761:39451059] 字面量
2021-06-06 00:51:27.828444+0800 test[82761:39451059] release
2021-06-06 00:51:27.828535+0800 test[82761:39451059] autorelease
2021-06-06 00:51:27.838554+0800 test[82761:39451059] -------[ViewController viewDidAppear:]------
2021-06-06 00:51:27.838684+0800 test[82761:39451059] 字面量
2021-06-06 00:51:27.838765+0800 test[82761:39451059] release
2021-06-06 00:51:27.838853+0800 test[82761:39451059] autorelease
我们发现他们已经都不在堆区了,而是存储在常量区,这是一项优化叫做NSTagged Pointer,即指针和对象存储在一起,这项技术是苹果公司对小对象做的优化NSString、NSNumber、NSDate。所以上述代码经过编译器的优化,就变成了下面这样

__NSCFConstantString *constAreaString = @"字面量";
NSTaggedPointerString *heapAreastring = [[NSString alloc] initWithFormat:@"release"];
NSTaggedPointerString *stringAutorelease = [[NSString stringWithFormat:@"autorelease"];

以上结论:

  • 字面量创建的直接存储在常量区
  • alloc出来的存储在堆区并且作用域结束前直接插入release(符合NSTagged Pointer的会直接分配在常量区,类型是NSTaggedPointer_接类型名,标识指针和对象存储在一起)
  • 通过stringWithFormat工厂方法创建的对象则在其后插入autorelease,这是因为工厂方法里面通过alloc分配堆内存,到返回出来以后其作用域已经结束,所以只能延迟释放了,否则没有办法返回非空对象(符合NSTagged Pointer的会直接分配在常量区,类型是NSTaggedPointer_接类型名,标识指针和对象存储在一起)

objc_autoreleaseReturnValue 和 objc_retainAutoreleasedReturnValue的纠正

值得一提的是,即便编译器插入autorelease关键字,也不一定会将这个对象放入autoreleasePool,为了减轻autoreleasePool的负担,苹果做了一项优化,objc_autoreleaseReturnValue 和 objc_retainAutoreleasedReturnValue,这里不分析源码,直接给出上层的解释,如下,对象在加入autoreleasePool之前会调用objc_autoreleaseReturnValue,这个方法会检测后面串行的代码是否调用了objc_retainAutoreleasedReturnValue(就是一次引用计数+1的操作),如果有则不加入autoreleasePool,直接返回对象,❌引用计数不会+1并且在当前线程存储区域做个标记,待到执行到objc_retainAutoreleasedReturnValue的时候检测标志位,如在优化流程中则直接返回对象并且重置标志位,否则加入autoreleasePool,❌引用计数+1

注意:上面一段话❌部分都是错误的理解,实际上引用计数在对象初始化后就已经存在,是对象相关联的东西,+1与否和自动释放池没有半点关系,-1与否才有关系

错误的理解如下:
优化前

id obj = objc_msgSend(objc_msgSend(NSMutableString, @selector(string)));
objc_autorelease(obj);
objc_retain(obj);
// 这里引用计数为2
objc_release(obj);

优化后

id obj = objc_msgSend(objc_msgSend(NSMutableString, @selector(string)));
// 这里引用计数为1
objc_release(obj);

而实际上呢:

NSMutableString *str = [NSMutableString string];
NSMutableString *strRetain = str;
NSLog(@"%li", CFGetRetainCount((__bridge CFTypeRef)str));

优化后retainCount的结果是2

021-06-07 00:53:55.610395+0800 test[92513:40083306] 2

所以结论是:

编译器优化后,会在执行到objc_retainAutoreleasedReturnValue的时候,不会将对象加入autoreleasePool,而是在这次引用计数+1操作之后作用域结束之前再加入一个release操作


对象是何时被加入autoreleasepool的

运行时,对象在调用autorelease的时候就开始检测后续串行代码是否有引用计数加1操作,没有的话就会直接调用AutoreleasePoolPage的add函数添加到双向链表
源码级别的回答:

id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}

在NSObject.mm可以查到源码,AutoreleasePoolPage定义了一系列工具函数,其中添加到autoreleasePool中的操作是add函数,这个函数的调用栈如下:

  • add
  • autoreleaseFast
  • autorelease
  • objc_autorelease
  • objc_autoreleaseReturnValue

程序的最小执行流是线程,iOS系统为每个线程定义了一系列的数据结构,在线程初始化的时候就初始化相关结构,一个栈、一个autoreleasepool、一个runloop还有一个线程局部存储区域(很小),运行时执行到autorelease语句的时候,会优先检测对象是否符合NSTaggedPointer,如果符合就抛出异常,证明程序不应该进入autorelease环节,如果不符合就往下走流程,调用objc_autoreleaseReturnValue函数,优先进行检测后续串行代码是否调用了objc_retainAutoreleasedReturnValue函数,如果没有调用,就会直接调用add函数添加到双向链表,如果有引用计数+1操作,则会把一个标记存储在TLS(线程局部存储),待到执行到objc_retainAutoreleasedReturnValue的时候检测标志位,如在优化流程中则直接返回对象并且重置标志位



作者:野码道人
链接:https://www.jianshu.com/p/ed84101e0efe
收起阅读 »

iOS 控制器生命周期

1,单个viewController的生命周期①,initWithCoder:(NSCoder *)aDecoder:(如果使用storyboard或者xib)②,loadView:加载view③,viewDidLoad:view加载完毕④,viewWillA...
继续阅读 »

1,单个viewController的生命周期

①,initWithCoder:(NSCoder *)aDecoder:(如果使用storyboard或者xib)

②,loadView:加载view

③,viewDidLoad:view加载完毕

④,viewWillAppear:控制器的view将要显示

⑤,viewWillLayoutSubviews:控制器的view将要布局子控件

⑥,viewDidLayoutSubviews:控制器的view布局子控件完成

这期间系统可能会多次调用viewWillLayoutSubviews 、 viewDidLayoutSubviews 俩个方法

⑦,viewDidAppear:控制器的view完全显示

⑧,viewWillDisappear:控制器的view即将消失的时候

这期间系统也会调用viewWillLayoutSubviews 、viewDidLayoutSubviews 两个方法

⑨,viewDidDisappear:控制器的view完全消失的时候

⑩,didReceiveMemoryWarning(内存满时)

当程序发出一个内存警告--->

系统询问控制器有View吗--->如果有View

系统询问这个View能够销毁吗---->通过判断View是否在Windown上面,如果不在,就表示可以销毁

如果可以销毁,就执行viewWillUnLoad()----->对你的View进行一次release,此时View就为nil

然后调用viewDidUnLoad()----->一般还会在这个方法里将一些不需要属性清空

2,多个viewControllers跳转

当我们点击push的时候首先会加载下一个界面然后才会调用界面的消失方法

initWithCoder:(NSCoder *)aDecoder:ViewController2(如果用xib创建的情况下)

loadView:ViewController2

viewDidLoad:ViewController2

viewWillDisappear:ViewController1将要消失

viewWillAppear:ViewController2将要出现

viewWillLayoutSubviewsViewController2

viewDidLayoutSubviewsViewController2

viewWillLayoutSubviews:ViewController1

viewDidLayoutSubviews:ViewController1

viewDidDisappear:ViewController1完全消失

viewDidAppear:ViewController2完全出现

3,相关解释

①,loadView()

若控制器有关联的 Nib 文件,该方法会从 Nib 文件中加载 view;如果没有,则创建空白 UIView 对象。

自定义实现不应该再调用父类的该方法。

②,viewDidLoad()

view 被加载到内存后调用viewDidLoad()。

重写该方法需要首先调用父类该方法。

该方法中可以额外初始化控件,例如添加子控件,添加约束。

该方法被调用意味着控制器有可能(并非一定)在未来会显示。

在控制器生命周期中,该方法只会被调用一次。

③,viewWillAppear()

该方法在控制器 view 即将添加到视图层次时以及展示 view 时所有动画配置前被调用。

重写该方法需要首先调用父类该方法。

该方法中可以进行操作即将显示的 view,例如改变状态栏的取向,类型。

该方法被调用意味着控制器将一定会显示。

在控制器生命周期中,该方法可能会被多次调用。

注意:

如果A控制器present到B控制器,B控制器dismiss的时候,A的viewWillAppear不会被调用

④,viewWillLayoutSubviews()

该方法在通知控制器将要布局 view 的子控件时调用。

每当视图的 bounds 改变,view 将调整其子控件位置。

该方法可重写以在 view 布局子控件前做出改变。

该方法的默认实现为空。

该方法调用时,AutoLayout 未起作用。

在控制器生命周期中,该方法可能会被多次调用。

⑤,viewDidLayoutSubviews()

该方法在通知控制器已经布局 view 的子控件时调用。

该方法可重写以在 view 布局子控件后做出改变。

该方法的默认实现为空。

该方法调用时,AutoLayout 已经完成。

在控制器生命周期中,该方法可能会被多次调用。

⑥,viewDidAppear()

该方法在控制器 view 已经添加到视图层次时被调用。

重写该方法需要首先调用父类该方法。

该方法可重写以进行有关正在展示的视图操作。

在控制器生命周期中,该方法可能会被多次调用。

⑦,viewWillDisappear()

该方法在控制器 view 将要从视图层次移除时被调用。

类似 viewWillAppear()。

该方法可重写以提交变更,取消视图第一响应者状态。

⑧,viewDidDisappear()

该方法在控制器 view 已经从视图层次移除时被调用。

类似 viewDidAppear()

该方法可重写以清除或隐藏控件。

⑨,didReceiveMemoryWarning()

当内存预警时,该方法被调用。

不能直接手动调用该方法。

该方法可重写以释放资源、内存。

⑩,deinit

控制器销毁时(离开堆),调用该方法。

可以移除通知,调试循环测试

总结:

当屏幕旋转,view 的 bounds 改变,其内部的子控件也需要按照约束调整为新的位置,因此也调用了 viewWillLayoutSubviews() 和 viewDidLayoutSubviews()。

当在一个控制器内 Present 新的控制器,原先的控制器并不会销毁,但会消失,因此调用了viewWillDisappear和viewDidDisappear方法。

如果新的控制器 Dismiss,即清除自己,原先的控制器会再一次出现,因此调用了其中的viewWillAppear和viewDidAppear方法。

若 loadView() 没有加载 view,viewDidLoad() 会一直调用 loadView() 加载 view,因此构成了死循环,程序即卡死。原文

附加面试题:load和initialize的区别

******:load是只要类所在的文件被引用就会被调用,而initialize是在类或者其子类的第一个方法被调用前调用。所以如果类没有被引用进项目,就不会调用load方法,即使类文件被引用进来,如果没有使用,那么initialize不会被调用。

调用方式
1、load是根据函数地址直接调用
2、initialize是通过objc_msgSend调用
调用时刻
1、load是runtime加载类、分类的时候调用(只会调用一次)
2、initialize是类第一次接收到消息的时候调用, 每一个类只会initialize一次(如果子类没有实现initialize方法, 会调用父类的initialize方法, 所以父类的initialize方法可能会调用多次)

load和initializee的调用顺序

1、load:
先调用类的load, 在调用分类的load
先编译的类, 优先调用load, 调用子类的load之前, 会先调用父类的load
先编译的分类, 优先调用load

2、initialize
先初始化分类, 后初始化子类
通过消息机制调用, 当子类没有initialize方法时, 会调用父类的initialize方法, 所以父类的initialize方法会调用多次


作者:大宝的爱情
链接:https://www.jianshu.com/p/bd2197d5e547



收起阅读 »

View事件分发

所谓View的事件分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。分发过程由三个重要的方法共同完成:dispatchTouchEvent、...
继续阅读 »

所谓View的事件分发,其实就是对MotionEvent事件的分发过程,即当一个MotionEvent产生了以后,系统需要把这个事件传递给一个具体的View,而这个传递的过程就是分发过程。

分发过程由三个重要的方法共同完成:dispatchTouchEvent、onInterceptTouchEvent和onTouchEvent,下面先简单介绍一下这些方法:

1、public boolean dispatchTouchEvent(MotionEvent ev) 用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。

2、public boolean onInterceptTouchEvent(MotionEvent event) 在上述方法内部调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。

3、public boolean onTouchEvent(MotionEvent event) 在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
当一个点击事件产生后,事件总是先传递给Activity,由Activity的dispatchTouchEvent()来进行事件分发,源码如下,路径android.app.Activity#dispatchTouchEvent:


   public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
根据Activity中的dispatchTouchEvent方法可以看到,里边会调用到getWindow()的superDispatchTouchEvent(ev),这里的getWindow()返回的是一个Window对象,而PhoneWindow又是抽象类Window在Android中唯一的实现,因此可以直接看PhoneWindow的superDispatchTouchEvent(ev),源码如下,路径com.android.internal.policy.PhoneWindow#superDispatchTouchEvent:

    @Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
里边又调用到mDecor的superDispatchTouchEvent(event),源码如下,路径com.android.internal.policy.DecorView#superDispatchTouchEvent:

    public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}


mDecor是一个DecorView对象,而DecorView继承自FrameLayout,FrameLayout继承自ViewGroup,FrameLayout本身没有重写dispatchTouchEvent方法,因此super.dispatchTouchEvent(event)最终就调用到了ViewGroup里,所以事件分发的核心部分是ViewGroup,我们暂时先放下这里,后边重点分析。

再回过头看下上述Activity的dispatchTouchEvent方法,最终会根据ViewGroup的dispatchTouchEvent的返回值决定后续流程,如果getWindow().superDispatchTouchEvent(ev)返回true,代表ViewGroup处理了该事件,直接返回,反之代表正在事件分发过程中没人处理这个事件,则会调用Activity的onTouchEvent方法,这就是整体的事件分发流程。

下面我们详细分析ViewGroup,刚才看到事件传递到了ViewGroup的dispatchTouchEvent (event),那我们就看下它的源码,源码如下,路径android.view.ViewGroup#dispatchTouchEvent,代码略长,以下是精简了剩下核心流程的代码:


        @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;// 方法最后的返回值,该变量值表示是否处理了该事件。
if (onFilterTouchEventForSecurity(ev)) {// 出于安全原因,会过滤点击事件,在该方法中会对event的一些标志位进行处理,返回FALSE表示丢弃事件,返回TRUE表示继续处理。
final int action = ev.getAction();
// 在安卓源码中有大量的位操作,通过进行位操作限定标志位范围,再对其做判断,此处位操作的作用是保留action的末尾8bit位,其余置0,作为actionMasked。
final int actionMasked = action & MotionEvent.ACTION_MASK;

// Handle an initial down.
// 因为事件流是以按下事件开始的,为此当按下事件到来时,会做一些初始化工作
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
cancelAndClearTouchTargets(ev);// 清空mFirstTouchTarget
resetTouchState();// 5、重置状态,如disallowIntercept
}

// Check for interception.
final boolean intercepted;// 该标志位表示是否拦截事件
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {// 1若该事件是ACTION_DOWN事件,或者事件已经被某个组件(mFirstTouchTarget)处理过
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;// 3允许拦截disallowIntercept标志位
if (!disallowIntercept) {// 4没设置不允许拦截disallowIntercept标志位
intercepted = onInterceptTouchEvent(ev);// 这里决定拦截intercepted为true,后续就会执行注释6处
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;//就是为了不拦截后续的非ACTION_DOWN事件
}
} else {// 2没有mFirstTouchTarget处理过,并且也不是初始的ACTION_DOWN事件,则拦截。
// There are no touch targets and this action is not an initial down
// so this view group continues to intercept touches.
intercepted = true;
}

if (!canceled && !intercepted) {// 7没拦截,由子View进行处理
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {//注意这里不执行ACTION_MOVE和ACTION_UP事件
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {// 8遍历ViewGroup所有子View
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {// 9调用dispatchTransformedTouchEvent,内部调用子View的dispatchTouchEvent()
newTouchTarget = addTouchTarget(child, idBitsToAssign);// 10给mFirstTouchTarget赋值
break;// 11跳出遍历子View
}
}
}
}
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {// 6可能没有子View或者所有的子View都不处理该事件或者是ViewGroup拦截了事件

// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {//有子View处理了ACTION_DOWN,ACTION_MOVE和ACTION_UP会执行这里
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {// alreadyDispatchedToNewTouchTarget为true就说明了mFirstTouchTarget被赋值了,所以事件已经交给子View处理了,这里就返回true
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {//设置了FLAG_DISALLOW_INTERCEPT,后续ACTION_MOVE和ACTION_UP会执行子View的dispatchTouchEvent()
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
return handled;
}


首先看下注释1处的mFirstTouchTarget,那么mFirstTouchTarget != null是什么意思呢?这个从后面的代码逻辑可以看出来,当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素。

也就是说,当ViewGroup不拦截事件并将事件交由子元素处理时mFirstTouchTarget才不为null。反之,一旦事件由当前ViewGroup拦截时,mFirstTouchTarget就是null,这样当ACTION_MOVE和ACTION_UP事件到来时,由于mFirstTouchTarget是null,将导致执行注释2处代码,ViewGroup的onInterceptTouchEvent不会再被调用,并且同一序列中的其他事件都会默认交给它处理。

当然,这里有一种特殊情况,就是上述注释3处的FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过ViewGroup的requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件。为什么说是除了ACTION_DOWN以外的其他事件呢?这是因为ViewGroup在分发事件时,如果是ACTION_DOWN就会重置FLAG_DISALLOW_INTERCEPT这个标记位,具体代码是执行注释5处代码,将导致子View中设置的这个标记位无效。因此,当面对ACTION_DOWN事件时,ViewGroup总是会调用自己的onInterceptTouchEvent方法来询问自己是否要拦截事件,也就是一定会执行注释4,。因此子View调用requestDisallowInterceptTouchEvent方法并不能影响ViewGroup对ACTION_DOWN事件的处理,所以FLAG_DISALLOW_INTERCEPT就是为了不拦截后续的非ACTION_DOWN事件设计的。

如果ViewGroup决定拦截ACTION_DOWN事件,就会执行注释6处的dispatchTransformedTouchEvent方法,暂时先不管dispatchTransformedTouchEvent内部,下面统一分析,mFirstTouchTarget就是null,后续的非ACTION_DOWN事件将会默认交给它处理并且不再调用它的onInterceptTouchEvent方法

如果ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理,具体执行注释7处,然后在注释8处,遍历ViewGroup的所有子元素,然后判断子元素是否能够接收到点击事件。是否能够接收点击事件主要由两点来衡量:子元素是否在播动画和点击事件的坐标是否落在子元素的区域内。如果某个子元素满足这两个条件,那么事件就会传递给它来处理,然后也会调用到dispatchTransformedTouchEvent方法,暂时先不管dispatchTransformedTouchEvent内部,下面统一分析,如果dispatchTransformedTouchEvent返回了true,就会执行注释10处,调用addTouchTarget()方法,内部会完成mFirstTouchTarget的赋值,并且在注释11处终止对子元素的遍历,那我们就看下addTouchTarget()的源码,源码如下,路径android.view.ViewGroup#addTouchTarget


    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;// 单链表结构
mFirstTouchTarget = target;// 这里就会完成mFirstTouchTarget的赋值
return target;
}

mFirstTouchTarget是否被赋值,将直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一序列中所有的点击事件,这一点在前面已经做了分析。

如果遍历所有的子元素后事件都没有被合适地处理,这包含两种情况:第一种是ViewGroup没有子元素;第二种是子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent中返回了false。在这两种情况下,ViewGroup会自己处理点击事件,同样也会调用到注释6处代码执行dispatchTransformedTouchEvent方法,就如同ViewGroup自己拦截了事件,自己来处理。

上面提到ViewGroup自身拦截或子View没有处理,还是需要ViewGroup自身处理,都会调用dispatchTransformedTouchEvent(),传入的参数child为null,如果是没拦截,遍历到了子View,也会调用dispatchTransformedTouchEvent(),传入的参数child就是遍历出来的子View本身,因此下面看下dispatchTransformedTouchEvent(),源码如下,路径android.view.ViewGroup#dispatchTransformedTouchEvent


    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;

// Canceling motions is a special case. We don't need to perform any transformations
// or filtering. The important part is the action, not the contents.
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}

// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}

// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);

handled = child.dispatchTouchEvent(event);

event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}

// Perform any necessary transformations and dispatch.
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}

handled = child.dispatchTouchEvent(transformedEvent);
}

// Done.
transformedEvent.recycle();
return handled;
}

提取核心代码可以看到主要执行了

        if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;

也就是说如果传入的child为null,会return super.dispatchTouchEvent(),也就是会调用ViewGroup的父类View的dispatchTouchEvent(),这样点击事件开始交由View来处理;如果child不为null,就调用子view本身的dispatchTouchEvent(),如果子View不是ViewGroup,则也会交由View的dispatchTouchEvent()处理这个事件,如果子View是ViewGroup,那就开启新一轮的事件分发,就又回到了上面分析的ViewGroup的dispatchTouchEvent()方法

然后事件到了View的dispatchTouchEvent (MotionEvent event),直接上源码如下,路径android.view.View#dispatchTouchEvent



    public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}

boolean result = false;

if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}

if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {// 1判断有mOnTouchListener并且mOnTouchListener返回true
result = true;
}

if (!result && onTouchEvent(event)) {// 2如果上述result就不会执行onTouchEvent(event)
result = true;
}
}

if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}

// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}

return result;
}


从代码可以看出View对点击事件的处理过程就比较简单了,因为View(这里不包含ViewGroup)是一个单独的元素,它没有子元素因此无法向下传递事件,所以它只能自己处理事件。注释1处,可以看出View对点击事件的处理过程,首先会判断有没有设置OnTouchListener,如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener的优先级高于onTouchEvent,这样做的好处是方便在外界处理点击事件。如果没设置OnTouchListener,就会执行View的onTouchEvent(MotionEvent event)方法,代码精简如下,源码路径android.view.View#onTouchEvent

onTouchEvent首先在注释1处判断View是否是clickable状态,包括点击和长按,然后注释2处,View处于不可用状态下点击事件的处理过程,可以看到,不可用状态下的View照样会消耗点击事件,尽管它看起来不可用。

然后在注释3处,判断是clickable的,也就是说只要View的CLICKABLE和LONG_CLICKABLE有一个为true,那么它就会消耗这个事件,即onTouchEvent方法返回true,也就是注释5,结合上述描述,不管View是不是DISABLE状态,都会消耗这个事件。

然后就是当ACTION_UP事件发生时,会触发performClick方法,这就是给如果View设置了OnClickListener,会执行注释4处代码,那么performClick方法内部会调用它的onClick方法。



作者:冬日毛毛雨
链接:https://www.jianshu.com/p/e7b5c56d5c9d

收起阅读 »

UIKit -大话 iOS Layout

大话 iOS Layout在iOS的开发中,我们绝大部分的时间都是在跟UI打交道,例如UI怎么布局,UI怎么刷新,以及对复杂UI的优化,使我们的APP更加流畅。对于UI的布局,xcode提供了可视化的布局方式:xib、storyboard,这是非常便捷的布局方...
继续阅读 »

大话 iOS Layout

在iOS的开发中,我们绝大部分的时间都是在跟UI打交道,例如UI怎么布局,UI怎么刷新,以及对复杂UI的优化,使我们的APP更加流畅。

对于UI的布局,xcode提供了可视化的布局方式:xib、storyboard,这是非常便捷的布局方式,所见即所得,门槛也非常低,但占用的资源相对代码来说更多,而且在多人协作开发的过程中,处理xml格式的文件冲突是非常困难的,所以很多团队都不推荐使用这类方式的布局,适合需求相对简单的团队、需要快速迭代的项目。

纯代码方式的布局是我们必修课,苹果有提供 Frame 和 Auto Layout 两种方式的布局。Auto Layout是苹果为我们提供的一整套布局引擎(Layout Engine),这套引擎会将视图、约束、优先级、大小通过计算转化成对应的 frame,而当约束改变的时候,会再次触发该系统重新计算。Auto Layout本质就是一个线性方程解析Engine。基于Auto Layout,不再需要像frame时代一样,关注视图的尺寸、位置相关参数,转而关注视图之间的关系,描述一个表示视图间布局关系的约束集合,由Engine解析出最终数值。

在混合开发的布局中,同样都会有一个虚拟DOM机制,当布局发生改变的时候,框架会将修改提交到虚拟DOM虚拟DOM先进行计算,计算出各个节点新的相对位置,最后提交到真实DOM,以完成渲染,当多个修改同时被提交的时候,框架也会对这些修改做一个合并,避免每次修改都要刷新。这种机制跟iOS中的run loop的渲染机制非常类似。Layout Engine计算出视图的frame,等到下一次run loop到来的时候,将结果提交到渲染层以完成渲染,同样,也会对一些修改进行“合并”,直到下一次运行循环到来时,才将结果渲染出来。

这篇文章主要讲解在布局的过程中,视图分别在Auto Layout以及Frame的方式下,如何完成刷新。

Main run loop of an iOS app

主运行循环是iOS应用中用来处理所有的用户输入和触发合适的响应事件。iOS应用的所有用户交互都会被添加到一个事件队列(event queue)里面。UIApplication object会将所有的事件从这个队列中取出,然后分发到应用中的其他对象。它通过解释用户的输入事件并在application’s core objects中调用相似的处理,以执行运行循环。这些处理事件可以被开发者重写。一旦这这些方法调用结束,程序就会回到运行循环,然后开始更新周期(update cycle)更新周期(update cycle)负责视图的布局和重绘



Update cycle

更新周期(update cycle)开始的时间点:应用程序执行完所有事件处理后,控制权返回main run loop。在这个时间点后,系统开始布局、显示和约束。如果在系统正在执行事件处理时需要更改某个视图,系统会将这个视图标记为需要重绘。在下一个更新周期,系统将会执行这个视图的所有修改。为了让用户交互和布局更新之间的延迟不被用户察觉到。iOS应用程序以60fps的帧率刷新界面,也就是每一个更新周期的时间是1/60秒。由于更新周期存在一定的时间间隔,所以我们在布局界面的过程中,会遇到某些视图并不是我们想要实现的效果,拿到的其实是上一次运行循环的效果。(这里YY一下,大家可能都会在业务代码中遇到过这种问题,某个视图布局不对,我们加个0.5的延迟,然后就正确了,或者是异步添加到主队列,界面布局也正常了。这些都是取巧的操作,刷新相关的问题仍然存在,可能在你的这个界面不会出问题,但有可能会影响到别人的、或者其他的界面。)所以说,“出来混,迟早都是要还的”,问题也迟早都是要解决的。接下来将会介绍如何准确的知道视图的布局、绘制、约束触发的时间点,如何正确的去刷新视图。

60fps 的 1/60秒
1/60秒CPU+GPU整个计算+绘制的时间间隔,如果在这个时间段内,并没有完成显示数据的准备,那iOS应用将显示上一帧画面,这个就是所谓的掉帧。CPU中有大量的逻辑控制单元,而GPU中有大量的数据计算单元,所以GPU的计算效率远高于GPU。为了提高效率,我们可以尽量将计算逻辑交给GPU。关于具体GPU的绘制流程相关的文章,可以参考OpenGL专题

下图可以看出来,在 main run loop 结束后开始 更新周期(update cycle)



Layout

视图的布局指的它在屏幕上的位置和大小。每一个视图都有一个frame,用于定义它在父视图坐标系中的位置和大小。UIView会提供一些方法以通知视图的布局发生改变,同样也提供一系列方法供开发者重写,用来处理视图布局完成之后的操作。

layoutSubviews()

  • 这个方法用来处理一个视图和它所有的子视图的重新布局(位置、大小),它提供了当前视图和所有子视图的frame。

  • 这个方法的开销是昂贵的,因为它作用于所有的子视图,并逐级调用所有子视图的layoutSubviews()方法

  • 系统在重新计算frame的时候会调用这个方法,所以当我们需要设置特定的frame的时候,可以重写这个方法。

  • 永远不要直接调用这个方法来刷新frame。在运行循环期间,我们可以通过调用其他方法来触发这个方法,这样造成的开销会小的多。

  • 当UIView的layoutSubviews()调用完成之后,就会调用它的ViewController的viewDidLayoutSubviews()方法,而layoutSubviews()方法是视图布局更新之后的唯一可靠方法。所以对于依赖视图frame相关的逻辑代码应该放在viewDidLayoutSubviews()方法中,而不是viewDidLoadviewDidAppear中,这样就可以避免使用陈旧的布局信息。

layoutSubviews 是在系统重新计算frame之前调用,还是在重新计算frame之后调用。(初步估计是计算之后)

Automatic refresh triggers

有很多事件会自动标记一个视图已经更改了布局,以便于layoutSubviews()方法在下一次执行的时候调用,而不是由开发者手动去做这些事。

一些自动通知系统布局已经更改的方法有:

  • 调整视图的大小
  • 添加一个子视图
  • 用户滑动UIScrollView(UIScrollView和它的父视图将会调用layoutSubviews()
  • 用户旋转设备
  • 更新视图的约束

这些方法都会告诉系统需要重新计算视图的位置,而且最终也会自动调用到layoutSubviews()方法。除此之外,有方法可以直接触发layoutSubviews()的调用。

setNeedsLayout()

  • setNeedsLayout() 是触发 layoutSubviews() 造成开销最小的方法。它会直接告诉系统,view 的布局需要重新计算。setNeedsLayout()会立即执行并返回,而且在返回之前,是不会去更新 view。
  • 当系统逐级调用 layoutSubviews() 之后,view 会在下一个 更新周期(update cycle) 更新。尽管在 setNeedsLayout() 与视图的重绘和布局之间有一定的时间间隔,但这个时间间隔不会长到影响到用户交互。

setNeedsLayout() 是在什么时候 return

layoutIfNeeded()

  • 执行 layoutIfNeeded() 之后,如果 view 需要更新布局,系统会立刻调用 layoutSubviews() 方法去更新,而不是将 layoutSubviews() 方法加入队列,等待下一次 更新周期(update cycle) 再去调用;
  • 当我们在调用 setNeedsLayout() 或者是其他自动触发刷新的事件之后,执行 layoutIfNeeded() 方法,可以立即触发 layoutSubviews() 方法。
  • 如果一个 view 不需要更新布局,执行 layoutIfNeeded() 方法也不会触发 layoutSubviews() 方法。例如,当我们连续执行两次 layoutIfNeeded() 方法,第二次执行将不会触发 layoutSubviews() 方法。

使用 layoutIfNeeded() 方法,子视图的布局和重绘将立即发生,并且在该方法返回之前就可以完成(除非视图正在做动画)。如果需要依赖一个新的视图布局,并且不想等视图的更新到下一个 更新周期(update cycle) 才完成,使用 layoutIfNeeded() 方法时非常有用的。除了这种场景,一般使用 setNeedsLayout() 方法等到下一个 更新周期(update cycle) 去更新视图就可以了,这样可以保证在一次 run loop 里面只更新视图一次。

在使用约束动画的时候,这个方法是非常有用的。一般操作是,在动画开始前调用一次 layoutIfNeeded() 方法,以保证在动画之前布局的更新都已经完成。配置完我们的动画后,在 animation block 里面,再调一次 layoutIfNeeded() 方法,就可以更新到新的状态。

Display

视图的展示包含的属性不涉及视图及子视图的 size 和 position,例如:颜色、文本、图片和 Core Graphic drawing。显示通道包含于触发更新的布局通道类似的方法,它们都是当系统检测到有变更时,由系统调用,而且我们也都能手动的去触发刷新。

draw(_:)

UIView 的 draw(Objective-C里面是drawRect)方法,作用于视图的内容,就像 layoutSubviews() 作用于视图的 size 和 position。但是,这个方法不会触发子视图的 draw(Objective-C里面是drawRect)方法。这个方法也不能由开发者直接调用,我们应该在 run loop 期间,调用可以触发 draw方法的其他方法来触发draw方法。

setNeedsDisplay()

setNeedsDisplay() 方法等同于 setNeedsLayout() 方法。它会设置一个内部的标记来标记这个视图的内容需要更新,但它在视图重绘之前返回。然后,在下一个 更新周期(update cycle) 系统会遍历所有有这个标记的视图,然后调用它们的 draw 方法。如果只需要在下一个更新周期(update cycle)重绘视图的部分内容,可以调用setNeedsDisplay() 方法,并通过rect属性来设置我们需要重绘的部分。

大多数情况下,想要在下一个更新周期(update cycle)更新一个视图上的UI组件,通过自动设置内部的内容更新标记而不是手动调用setNeedsDisplay()方法。但是,如果一个视图(aView)并不是直接绑定到UI组件上的,但是我们又希望每次更新的时候都可以重绘这个视图,我们可以通过观察视图(aView)属性的setter方法(KVO),来调用setNeedsDisplay()方法以触发适当的视图更新。

当需要执行自定义绘制时,可以重写draw方法。下面可以通过一个例子来理解。

  • numberOfPointsdidSet方法中调用setNeedsDisplay()方法,可以触发draw方法。
  • 通过重写draw方法,以达到在不同情况下,绘制不同的样式的效果。
class MyView: UIView {
var numberOfPoints = 0 {
didSet {
setNeedsDisplay()
}
}

override func draw(_ rect: CGRect) {
switch numberOfPoints {
case 0:
return
case 1:
drawPoint(rect)
case 2:
drawLine(rect)
case 3:
drawTriangle(rect)
case 4:
drawRectangle(rect)
case 5:
drawPentagon(rect)
default:
drawEllipse(rect)
}
}
}


不像layoutIfNeeded可以立刻触发layoutSubviews那样,没有方法可以直接触发一个视图的内容更新。视图内容的更新必须等到下一个更新周期去重绘视图。

Constraints

在自动布局中,视图的布局和重绘需要三个步骤:

  1. 更新约束:系统会计算并设置视图上所有必须的约束。
  2. 布局阶段layout engine计算视图的frame,并将它们布局。
  3. 显示过程:结束更新循环并重绘视图内容,如果有必要,会调用draw方法。

updateConstraints()

  • updateConstraints在自动布局中的作用就像layoutSubviews在frame布局、以及draw在内容重绘中的作用一样。
  • updateConstraints方法只能被重写,不能被直接调用。
  • updateConstraints方法中一般只实现那些需要改变的约束,对于不需要改变的约束,我们尽可能的别写在里面。
  • Static constraints也应该在接口构造器、视图的初始化方法、或viewDidLoad()方法中实现,而不是放在updateConstraints方法中实现。

有以下一些方式可以自动触发约束的更新。在视图内部设置一个update constraints的标志,该标志会在下一个update cycle中,触发updateConstraints方法的调用。

  • 激活或停用约束;
  • 更改约束的优先级、常量值;
  • 移除约束;

除了自动触发约束的更新之外,同样也有以下方法可以手动触发约束的更新。

setNeedsUpdateConstraints()

调用setNeedsUpdateConstraints方法可以保证在下一个更新周期进行约束的更新。它触发updateConstraints方法的方式是通过标记视图的某个约束已经更新。这个方法的工作方式跟setNeedsLayoutsetNeedsDisplay类似。

updateConstrainsIfNeeded()

这个方法等同于layoutIfNeeded,但是在自动布局中,它会检查constraint update标记(这个标记可以被自动设置、也可以通过setNeedsUpdateConstraintsinvalidateInstinsicContentSize方法手动设置)。如果它确定约束需要更新,就会立即触发updateConstraints方法,而不是等到 run loop 结束。

invalidateInstinsicContentSize()

自动布局中某些视图拥有intrinsicContentSize属性,这是视图根据它的内容得到的自然尺寸。一个视图的intrinsicContentSize通常由所包含的元素的约束决定,但也可以通过重载提供自定义行为。调用invalidateIntrinsicContentSize()会设置一个标记表示这个视图的intrinsicContentSize已经过期,需要在下一个布局阶段重新计算。

How it all connects

布局、显示和约束都遵循着相似的模式,例如:他们更新的方式以及如何在 run loop 的不同时间点上强制更新。任一组件都有一个实际去更新的方法(layoutSubviewsdraw, 和updateConstraints),这些方法可以通过重写来手动操作视图,但任何情况下都不要显式调用。这个方法只在 run loop 的末端会被调用,如果视图被标记了告诉系统该视图需要被更新的标记话。有一些操作会自动设置这个标记,也有一些方法允许显式地设置它。对于布局和约束相关的更新,如果等不到在 run loop 结束才更新的话(例如:其他行为依赖于新布局),也有方法可以让你立即更新,并保证 update layout能被正确标记。下面的表格列出了任意组件会怎样更新及其对应方法。

LayoutDisplayConstraints方法意图
layoutSubviewsdrawupdateConstraints执行更新的方法,可以被重
写,但不能被调用
setNeedsLayoutsetNeedDisplaysetNeedsUpdateConstaints
invalidateInstrinsicContentSize
显示的标记视图需要在下一个更新循环更新
layoutIfNeeded--updateConstraintsIfNeeded立刻更新被标记的视图
添加视图
重设size
设置frame(需要改变bounds)
滑动ScrollView
旋转设备
发生在视图的bounds内部的改变激活、停用约束
修改约束的优先级和常量值
移除约束
隐式触发视图更新的事件

下图总结了更新周期(update cycle)事件循环(event loop)之间的交互,并且指示了上面这些方法在周期中下一步指向的位置。你可以现实的调用layoutIfNeededupdateConstraintsIfNeeded在run loop的任何地方,但需要注意的是,这两个方式是有潜在开销的。如果update constrintsupdate layoutneeds display标记被设置,在 run loop 的结尾处的更新周期就会更新约束、布局、显示内容。一但这些更新全部完成,run loop就会重新开始。






作者:修_远
链接:https://www.jianshu.com/p/98dec55a06c8

收起阅读 »

短视频源码开发,短视频源码应该从哪些方面进行优化?

短视频作为更加符合移动互联网时代用户触媒习惯的视频内容形式,在内容上和功能上本身就具有很大的想象空间。通过“短视频+”的方式现在有不少平台上搭建和嵌入短视频源码,是一个不错的入局途径。短视频压缩短视频的压缩问题是短视频源码的难点之一。视频拍摄、上传完成后,要在...
继续阅读 »

短视频作为更加符合移动互联网时代用户触媒习惯的视频内容形式,在内容上和功能上本身就具有很大的想象空间。通过“短视频+”的方式现在有不少平台上搭建和嵌入短视频源码,是一个不错的入局途径。

短视频压缩

短视频的压缩问题是短视频源码的难点之一。视频拍摄、上传完成后,要在不影响用户体验的情况下完成短视频帧率的统一、格式统一、分辨率处理、短视频压缩等处理。

短视频不进行压缩处理的情况下上传,会造成资源的浪费,对运营商来说,长期以往的带宽浪费会非常费钱。

实现秒播

短视频app源码中的短视频列表在打开时,就会主动扣留列表中的最后一个视频到内存中,然后再对其他视频进行预加载,当进行下拉刷新操作时,会将上次扣留的短视频作为刷新后的第一个视频进行展示,再去预加载其他视频内容,通过这样的方式,优化小视频app源码中短视频秒播的问题。

短视频源码开发,短视频源码应该从哪些方面进行优化?

大数据分析

短视频源码的数据分析功能非常重要,在推荐上、获取用户喜好上都能发挥很大的作用。百万级的用户,想要做到精准的推荐,就离不开对用户数据的分析,通过平时观看的喜好,充分了解用户的观看习惯。

短视频的录制

短视频录制功能通常能够设置视频的录制时长,可以录制的时间长一点或者短一点,配置各类的视频参数,像视频分辨率、码率等等。此外,短视频之所以这么火爆,还在于短视频中加了很多特效和滤镜,像有一个可爱的、搞怪的、清新的等等,还有很多动态的贴纸,面具,动态的在线染发,更关键的话还有很多小游戏,让人沉迷在短视频中。

除了以上短视频源码开发功能之外,管理后台也在全套方案中扮演着重要的角色。除了能够配置APP客户端相对应的功能外,还需要有良好的操作体验,方便短视频平台运营商的各项实际操作。

消息也是短视频源码当中重要的一环,是内容互动的前提条件。除了系统及时推送消息之外,粉丝和主播之间、粉丝和粉丝之间都可以进行私信的互动,也可以针对自己喜欢的视频进行评论。

收起阅读 »

iOS开发UIView的setNeedsLayout, layoutIfNeeded 和 layoutSubviews

iOS layout机制相关方法(CGSize)sizeThatFits:(CGSize)size(void)sizeToFit(void)layoutSubviews(void)layoutIfNeeded(void)setNeedsLayout(void)...
继续阅读 »

iOS layout机制相关方法

  • (CGSize)sizeThatFits:(CGSize)size
  • (void)sizeToFit
  • (void)layoutSubviews
  • (void)layoutIfNeeded
  • (void)setNeedsLayout
  • (void)setNeedsDisplay
  • (void)drawRect
    layoutSubviews在以下情况下会被调用:

1、init初始化不会触发layoutSubviews

但是是用initWithFrame 进行初始化时,当rect的值不为CGRectZero时,也会触发

2、addSubview会触发layoutSubviews

3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化

4、滚动一个UIScrollView会触发layoutSubviews

5、旋转Screen会触发父UIView上的layoutSubviews事件

6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件

在苹果的官方文档中强调:

You should override this method only if the autoresizing behaviors
of the subviews do not offer the behavior you want.
layoutSubviews, 当我们在某个类的内部调整子视图位置时,需要调用。

反过来的意思就是说:如果你想要在外部设置subviews的位置,就不要重写。

刷新子对象布局

-layoutSubviews方法:这个方法,默认没有做任何事情,需要子类进行重写
-setNeedsLayout方法: 标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定会被调用
-layoutIfNeeded方法:如果,有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)

如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局

在视图第一次显示之前,标记总是“需要刷新”的,可以直接调用[view layoutIfNeeded]

重绘

-drawRect:(CGRect)rect方法:重写此方法,执行重绘任务
-setNeedsDisplay方法:标记为需要重绘,异步调用drawRect
-setNeedsDisplayInRect:(CGRect)invalidRect方法:标记为需要局部重绘

sizeToFit会自动调用sizeThatFits方法;

sizeToFit不应该在子类中被重写,应该重写sizeThatFits

sizeThatFits传入的参数是receiver当前的size,返回一个适合的size

sizeToFit可以被手动直接调用

sizeToFit和sizeThatFits方法都没有递归,对subviews也不负责,只负责自己

layoutSubviews对subviews重新布局

layoutSubviews方法调用先于drawRect

setNeedsLayout在receiver标上一个需要被重新布局的标记,在系统runloop的下一个周期自动调用layoutSubviews

layoutIfNeeded方法如其名,UIKit会判断该receiver是否需要layout.根据Apple官方文档,layoutIfNeeded方法应该是这样的

layoutIfNeeded遍历的不是superview链,应该是subviews链

drawRect是对receiver的重绘,能获得context

setNeedDisplay在receiver标上一个需要被重新绘图的标记,在下一个draw周期自动重绘,iphone device的刷新频率是60hz,也就是1/60秒后重绘



作者:道哥_d5a0
链接:https://www.jianshu.com/p/f714642d6340

收起阅读 »

iOS - 图片显示类似LED的效果

iOS
LED灯的效果展示。整理了一下,自己所了解的知识。通过一些其他方式。在App界面展示出现LED的效果。屏幕快照 2020-10-27 上午9.43.30.png1.绘制图片 (或者是图片)2.通过获取到像素点的颜色去进行展示。每一个像素点有 RGB A 这个四...
继续阅读 »

LED灯的效果展示。
整理了一下,自己所了解的知识。通过一些其他方式。
在App界面展示出现LED的效果。



屏幕快照 2020-10-27 上午9.43.30.png

1.绘制图片 (或者是图片)
2.通过获取到像素点的颜色去进行展示。每一个像素点有 RGB A 这个四个
3.通过获取到的 bitMap 展示出LED的效果

第一步随意找一张图片 方便获取到他的像素点

第二步

//2.获取到图片的 bitMap
/**
- 传入图片的信息,返回图片的像素点信息
- 返回的数据排列。 R G B ,A :亮度, row:行数, col 列数
*/

func getImagePixel(_ image:UIImage) -> Array<Any>{
//存储像素的数据
let grayScale: [Pixel] = (image.pixelData.map {
//将RGB的颜色记录下来
return $0

})
//返回像素的数据
return grayScale
}


//MARK: 创建接受像素点的model
struct Pixel {
var r: Float
var g: Float
var b: Float
var a: Float
var row: Int
var col: Int
init(r: UInt8, g: UInt8, b: UInt8, a: UInt8, row: Int, col: Int) {
self.r = Float(r)
self.g = Float(g)
self.b = Float(b)
self.a = Float(a)
self.row = row
self.col = col
}
var color: UIColor {
return UIColor(
red: CGFloat(r/255.0),
green: CGFloat(g/255.0),
blue: CGFloat(b/255.0),
alpha: CGFloat(a/255.0)
)
}
var description: String {
return "\(r), \(g), \(b), \(a) ,\(row) ,\(col)"
}
}


//MARK: 读取像素点的方法
extension UIImage{

var pixelData: [Pixel] {

var pixelS = [Pixel]()
for row in 0 ..< Int(self.size.width){
for col in 0 ..< Int(self.size.height){
let coloR = self.cxg_getPointColor(withImage: self, point: CGPoint(x: row, y: col))
pixelS.append(Pixel(r: coloR![0], g: coloR![1], b: coloR![2], a: coloR![3], row: row, col: col))
}
}
//返回取出颜色的数组 返回RGB 亮度 行数、列数
return pixelS
}
/// - Parameters:
/// - image: 要获取颜色的图片
/// - point: 每一次要获取到的点的颜色
/// - Returns: 获取到的颜色
func cxg_getPointColor(withImage image: UIImage, point: CGPoint) -> [ UInt8]? {
guard CGRect(origin: CGPoint(x: 0, y: 0), size: image.size).contains(point) else {
return nil
}
let pointX = trunc(point.x);
let pointY = trunc(point.y);

let width = image.size.width;
let height = image.size.height;
let colorSpace = CGColorSpaceCreateDeviceRGB();
var pixelData: [UInt8] = [0, 0, 0, 0]

pixelData.withUnsafeMutableBytes { pointer in
if let context = CGContext(data: pointer.baseAddress, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue), let cgImage = image.cgImage {
context.setBlendMode(.copy)
context.translateBy(x: -pointX, y: pointY - height)
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
}
}
return pixelData
}
}

第三步

//3.通过获取到的 点阵的位置绘制出来点阵图
class TestView:UIView{
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
}
var x:CGFloat = 0.0
var y:CGFloat = 0.0

var imagePixel_Array:Array<Pixel>?
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
//获取绘图上下文
guard let context = UIGraphicsGetCurrentContext() else {
return
}
self.backgroundColor = #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0)
if imagePixel_Array != nil{
//控制小格子有多大
let size: CGSize = .init(width: self.frame.size.width / 18, height: self.frame.size.height/12)
for (index, pixel) in (imagePixel_Array?.enumerated())! {

//创建一个矩形,它的所有边都内缩3点
let drawingRect = CGRect(x: x, y: y, width: size.width, height: size.height)
//创建并设置路径
let path = CGMutablePath()
//绘制矩形
path.addRect(drawingRect)
//添加路径到图形上下文
context.addPath(path)
//设置填充颜色
context.setFillColor(UIColor.init(red: CGFloat(pixel.r / 255), green: CGFloat(pixel.g / 255), blue: CGFloat(pixel.b / 255), alpha: CGFloat(pixel.a / 255)).cgColor)
//绘制路径并填充
context.drawPath(using: .fillStroke)
print("---\(pixel.description)")

y += size.height
if index % 12 == 0 && index != 0{
x += size.width
y = 0
}
}
}
}
}

如何调用

        let image_Png = UIImage.init(named: "1111")!
let imagePixel_Array_2 = self.getImagePixel(image_Png!)
let testView_2 = TestView.init(frame: CGRect(x: 100, y: 400, width: 18 * 12, height: 12 * 12))
testView_2.imagePixel_Array = (imagePixel_Array_2 as! Array<Pixel>)
testView_2.setNeedsDisplay()
self.view.addSubview(testView_2)

现在只是效果只是完成了一丢丢
后面接着去研发,去研究这个展示 led效果的功能
如果有疑惑,评论。我就会去和大家讨论


1人点赞
收起阅读 »

iOS-Cocoapods 的正确安装姿势

iOS
在安装过程中出现curl: (7) Failed to connect to raw.githubusercontent.com port 443: Connection refused 问题访问我的处理方式可能会对你有帮助. 文末附带rvm 无法在线安装的解...
继续阅读 »

在安装过程中出现curl: (7) Failed to connect to raw.githubusercontent.com port 443: Connection refused 问题访问我的处理方式可能会对你有帮助.

文末附带rvm 无法在线安装的解决办法.

文末还提供了pod install或者serach 过程中[!]CDN: trunk URL couldn't be downloaded:的解决办法.


1. Mac环境下 Cocoapods 的安装


1.1 总体步骤



下载Xcode —>安装rvm —>安装ruby —>安装home-brew —>安装cocoapods


1.2 安装前,先检查是否有安装残留


1. 如果之前装过cocopods,最好先卸载掉,卸载命令:
$ sudo gem uninstall cocoapods

2. 先查看本地安装过的cocopods相关东西,命令如下:
$ gem list --local | grep cocoapods
会显示如下:
cocoapods (1.7.2)
cocoapods-core (1.7.2)
cocoapods-deintegrate (1.0.4)
cocoapods-downloader (1.2.2)
cocoapods-plugins (1.0.0)
cocoapods-search (1.0.0)
cocoapods-stats (1.1.0)
cocoapods-trunk (1.3.1)
cocoapods-try (1.1.0)

3. 使用删除命令, 逐个删除:
$ sudo gem uninstall cocoapods-core

1.3 Mac文件夹的显示隐藏命令行:


隐藏:defaults write com.apple.finder AppleShowAllFiles -bool true
显示:defaults write com.apple.finder AppleShowAllFiles -bool false

这里选择将隐藏文件显示出来; 退出终端,重启Finder. 如果不确定,可以把主目录下的隐藏文件都给删了.

1.4. RVM



  • Ruby Version Manager,Ruby版本管理器,包括Ruby的版本管理和Gem库管理(gemset)


1. 安装RVM
$ curl -sSL https://get.rvm.io | bash -s stable
期间可能需要管理员密码, 以及自动通过homebrew安装依赖包,等待一段时间就安装好了.

2. 载入 RVM 环境
$ source ~/.rvm/scripts/rvm

3. 检查一下是否安装正确
$ rvm -v
会显示如下:
rvm 1.29.8 (latest) by Michal Papis, Piotr Kuczynski, Wayne E. Seguin [https://rvm.io]
表示安装正确.

注意: 也可使用 ($ rvm -v) 来判断是否安装了rvm
// 结果类似如下代表没有安装rvm
zsh: command not found: rvm

1.5 用RVM安装Ruby环境


1. 列出已知的ruby版本
$ rvm list known

2. 选择最新版本进行安装(这里以2.6.0为例)
$ rvm install 2.6.0

同样继续等待漫长的下载,编译过程,完成以后,Ruby, Ruby Gems 就安装好了。

3. 查询已经安装的ruby
$ rvm list

卸载一个已安装版本的命令
$ rvm remove + 要卸载的版本号

4. RVM 装好以后,需要执行下面的命令将指定版本的 Ruby 设置为系统默认版本
$ rvm 2.6.0 --default

5. 测试操作是否正确(分 2 步)
$ ruby -v
会显示如下:
ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-darwin18]

$ gem -v
会显示如下:
3.0.4

注: RubyGems(简称 gems)是一个用于对 Ruby组件进行打包的 Ruby 打包系统。 它提供一个分发 Ruby 程序和库的标准格式,还提供一个管理程序包安装的工具。

1.6 更换镜像源


1. 查看当前镜像源
$ gem sources -l
会显示如下:
*** CURRENT SOURCES ***
http://rubygems.org/

2. 先删除, 再添加
$ gem sources --remove https://rubygems.org/
$ gem sources -a https://gems.ruby-china.com/

3. 再次查看, 测试是否成功
$ gem sources -l
会显示如下:
*** CURRENT SOURCES ***
https://gems.ruby-china.com/

到这里就已经把Ruby环境成功的安装到了Mac OS X上,接下来就可以进行相应的开发使用了。

1.7 安装home-brew




  • 也可选择跳过这步, 直接安装cocoapods, 引入库文件时, 会提示你自动安装home-brew

  • Homebrew: 是一个包管理器,用于在Mac上安装一些OS X没有的UNIX工具。

  • 官方网址: https://brew.sh/index_zh-cn

  • Homebrew是完全基于 Git 和 ruby.



1. 安装
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)”
安装过程中需要按回车键确认

2. 检测是否存在冲突
$ brew doctor

3. 检查是否安装成功, 出现版本号就成功了.
$ brew --version

1.8 安装Cocoapods (步骤有点慢,不要急)


1. 坑点:
使用$ sudo gem install cocoapods安装cocoapods 极有可能报error: RPC failed / early EOF

2. 正确的使用方法:
A. 看到报这个错之后,需要在终端执行$ sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer 这句,具体如下: 先找到xcode,显示包内容,在Contents里找到Developer文件,然后在终端输入sudo xcode-select -switch ,把找到的Developer文件夹拖进终端,就得到后边的路径啦,然后执行。因为xcode位置和版本安装的不一样,可能路径会有所不同。我的最终是sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer 这个。
B. 执行$ sudo gem install -n /usr/local/bin cocoapods
C. Git clone https://git.coding.net/CocoaPods/Specs.git ~/.cocoapods/repos/master

然后就等待吧,下载完就ok了.

2. 离线安装RVM方式



// 离线包
curl -sSL https://github.com/rvm/rvm/tarball/stable -o rvm-stable.tar.gz
// 创建文件夹
mkdir rvm && cd rvm
// 解包
tar --strip-components=1 -xzf ../rvm-stable.tar.gz
// 安装
./install --auto-dotfiles
// 加载
source ~/.rvm/scripts/rvm
// if --path was specified when instaling rvm, use the specified path rather than '~/.rvm'

// 查询 ruby的版本
rvm list known


在查询 ruby的版本时可能会出现下面的错误:A RVM version () is installed yet 1.25.14 (master) is loaded.Please do one of the following:* 'rvm reload'* open a new shell* 'echo rvm_auto_reload_flag=1 >> ~/.rvmrc' # for auto reload with msg.* 'echo rvm_auto_reload_flag=2 >> ~/.rvmrc' # for silent auto reload.




解决办法: sudo rm -rf /users/your_username/.rvmThen close and reopen the terminal.

然后重新打开终端即可.



3. [!] CDN: trunk URL couldn't be downloaded:


CocoaPods 1.8 版本之后的一些说明!



我的解决方法



// 在podfile 文件中添加 (选一个就行)
source 'https://github.com/CocoaPods/Specs.git'

source 'https://cdn.cocoapods.org/'

.End

链接:https://www.jianshu.com/p/d80b06f6e4e7
收起阅读 »

JetpackMVVM七宗罪(之一)拿Fragment当LifecycleOwner

首先承认这个系列有点标题党,Jetpack 的 MVVM 本身没有错,错在开发者的某些使用不当。本系列将分享那些 AAC 中常见的错误用法,指导大家打造更健康的应用架构 Fragment 作为 LifecycleOwner 的问题 MVVM 的核心是数...
继续阅读 »

首先承认这个系列有点标题党,Jetpack 的 MVVM 本身没有错,错在开发者的某些使用不当。本系列将分享那些 AAC 中常见的错误用法,指导大家打造更健康的应用架构



Fragment 作为 LifecycleOwner 的问题


MVVM 的核心是数据驱动UI,在 Jetpack 中,这一思想体现在以下场景:Fragment 通过订阅 ViewModel 中的 LiveData 以驱动自身 UI 的更新


关于订阅的时机,一般会选择放到 onViewCreated 中进行,如下:


override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

viewModel.liveData.observe(this) { // Warning : Use fragment as the LifecycleOwner
updateUI(it)
}

}

我们知道订阅 LiveData 时需要传入 LifecycleOwner 以防止泄露,此时一个容易犯的错误是使用 Fragment 作为这个 LifecycleOwner,某些场景下会造成重复订阅的Bug。


做个实验如下:


val handler = Handler(Looper.getMainLooper())

class MyFragment1 : Fragment() {
val data = MutableLiveData<Int>()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

tv.setOnClickListener {
parentFragmentManager.beginTransaction()
.replace(R.id.container, MyFragment2())
.addToBackStack(null)
.commit()

handler.post{ data.value = 1 }
}

data.observe(this, Observer {
Log.e("fragment", "count: ${data.value}")
})

}

当跳转到 MyFragment2 然后再返回 MyFragment1 中时,会打出输出两条log


E/fragment: count: 1
E/fragment: count: 1

原因分析


LiveData 之所以能够防止泄露,是当 LifecycleOwner 生命周期走到 DESTROYED 的时候会 remove 调其关联的 Observer


//LiveData.java

@Override
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());

}

前面例子中,基于 FragmentManager#replace 的页面跳转,使得 MyFragment1 发生了从 BackStack 的出栈/入栈,由于 Framgent 实例被复用并没有发生 onDestroy, 但是 Fragment的 View 的重建导致重新 onCreateView, 这使得 Observer 被 add 了两次,但是没有对应的 remove。


所以归其原因, 是由于 Fragment 的 Lifecycle 与 Fragment#mView 的 Lifecycle 不一致导致我们订阅 LiveData 的时机和所使用的 LivecycleOwner 不匹配,所以在任何基于 replace 进行页面切换的场景中,例如 ViewPager、Navigation 等会发生上述bug



解决方法


明白了问题原因,解决思路也就清楚了:必须要保证订阅的时机和所使用的LifecycleOwner相匹配,即要么调整订阅时机,要么修改LifecycleOwner


在 onCreate 中订阅


思路一是修改订阅时机,讲订阅提前到 onCreate, 可以保证与 onDestory 的成对出现,但不幸的是这会带来另一个问题。


当 Fragment 出入栈造成 View 重建时,我们需要重建后的 View 也能显示最新状态。但是由于 onCreate 中的订阅的 Observer 已经获取过 LiveData 的最新的 Value,如果 Value 没有新的变化是无法再次通知 Obsever 的



在 LiveData 源码中体现在通知 Obsever 之前对 mLastVersion 的判断:


//LiveData.java

private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}

if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {// Value已经处于最新的version
return;
}

observer.mLastVersion = mVersion;
//noinspection unchecked
observer.mObserver.onChanged((T) mData);
}

正是为了保证重建后的 View 也能刷新最新的数据, 我们才在 onViewCreated 中完成订阅。因此只能考虑另一个思路,替换 LifecycleOwner


使用 ViewLifecycleOwner


Support-28 或 AndroidX-1.0.0 起,Fragment 新增了 getViewLifecycleOwner 方法。顾名思义,它返回一个与 Fragment#mView 向匹配的 LifecycleOwner,可以在 onDestroyView 的时候走到 DESTROYED ,删除 onCreateView 中注册的 Observer, 保证了 add/remove 的成对出现。



看一下源码,原理非常简单


//Fragment.java
void performCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
//...

mViewLifecycleOwner = new LifecycleOwner() {
@Override
public Lifecycle getLifecycle() {
if (mViewLifecycleRegistry == null) {
mViewLifecycleRegistry = new LifecycleRegistry(mViewLifecycleOwner);
}
return mViewLifecycleRegistry;
}
};
mViewLifecycleRegistry = null;
mView = onCreateView(inflater, container, savedInstanceState);
if (mView != null) {
// Initialize the LifecycleRegistry if needed
mViewLifecycleOwner.getLifecycle();
// Then inform any Observers of the new LifecycleOwner
mViewLifecycleOwnerLiveData.setValue(mViewLifecycleOwner); //mViewLifecycleOwnerLiveData在后文介绍
} else {
//...
}
}

基于 mViewLifecycleRegistry 创建 mViewLifecycleOwner,


     @CallSuper
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {// called when onCreateView
if (mView != null) {
mViewLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
}
}


@CallSuper
public void onDestroyView() {
if (mView != null) {
mViewLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
}
}

然后在 onCreateViewonDestroyView 时,推进到合适的生命周期。


getViewLifecycleOwnerLiveData


顺道提一下,与 getViewLifecycleOwner 同时新增的还有 getViewLifecycleOwnerLiveData。 从前面贴的源码中对 mViewLifecycleOwnerLiveData 的使用,应该可以猜出它的作用: 它是前文讨论的思路1的实现方案,即使在 onCreate 中订阅,由于在 onCreateView 中对 LiveData 进行了重新设置,所以重建后的 View 也可以更新数据。


  // Then inform any Observers of the new LifecycleOwner
mViewLifecycleOwnerLiveData.setValue(mViewLifecycleOwner);

需要特别注意的是,根据 MVVM 最佳实践,我们希望由 ViewModel 而不是 Fragment 持有 LiveData,所以不再推荐使用 getViewLifecycleOwnerLiveData


最后: StateFlow 与 lifecycleScope


前面都是以 LiveData 为例介绍对 ViewLifecycleOwner 的使用, 如今大家也越来越多的开始使用协程的 StateFlow , 同样要注意不要错用 LifecycleOwner


订阅 StateFlow 需要 CoroutineScope, AndroidX 提供了基于 LifecycleOwner 的扩展方法


val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope

当我们在 Fragment 中获取 lifecycleScope 时,切记要使用 ViewLifecycleOwner


class MyFragment : Fragment() {

val viewModel: MyViewModel by viewModel()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

//使用 viewLifecycleOwner 的 lifecycleScope
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.someDataFlow.collect {
updateUI(it)
}
}
}
}
}

注意此处出现了一个 repeatOnLifecycle(...), 这跟本文无关,但是将涉及到第二宗罪的剧情,敬请期待。

收起阅读 »

Android Studio Arctic Fox 大版本更新,快来了解下吧

Android Studio 的新大版本又来了,大家快来躺坑。原本链接: developer.android.com/studio/rele… Android Studio Arctic Fox 是属于大版本更新,其中包含各种新功能和改进,其中主要有:版...
继续阅读 »

Android Studio 的新大版本又来了,大家快来躺坑。原本链接: developer.android.com/studio/rele…



Android Studio Arctic Fox 是属于大版本更新,其中包含各种新功能和改进,其中主要有:版本号规则变更,支持新的测试模式,更高级的调试跟踪,更方便的导出数据库,支持 Compose 等等


新版本号


更新了 Android Studio 的版本号


本次更改了 Android Studio 的版本号规则,与 IntelliJ IDEA(Android Studio 所基于的 IDE)更加一致。


在之前的版本号中,版本的编号规则为 Android Studio 4.3 或版本 4.3.0.1 ,而有了新的版本编号规则后,以后会是 Android Studio - Arctic Fox | 2020.3.12020.3.1 版本。



以下是未来确定 Android Studio 版本号的方式:


<Year of IntelliJ Version>.<IntelliJ major version>.<Studio major version>.<Studio minor/patch version>



  • 前两个数字组代表特定 Android Studio 所基于的 IntellIj 平台的版本,此次的版本为 2020.3

  • 第三个数字组代表 Studio 的主要版本,从 1 开始, 每个主要版本递增 1。

  • 第四组数字代表 Studio 次要/补丁版本,从 1 开始,每个次要版本递增 1。

  • 此次还为每个主要版本提供了一个版本名称,根据动物名称从 A 到 Z 递增,此版本名为 Arctic Fox


更新了 Android Gradle 插件的版本编号


此次更改 Android Gradle 插件 (AGP) 的版本号,以更紧密地匹配底层 Gradle 构建工具,因此 AGP 4.2 之后的下一个版本是 AGP 7.0 。



有关更多详细信息,请参阅 AGP 中的 版本控制更改



Android Gradle 插件 7.0.0


单元测试现在使用 Gradle 测试运行器


为了提高测试执行的整体一致性,Android Studio 现在默认使用 Gradle 运行所有单元测试,当然在一般情况下,此更改不会影响在 IDE 中的测试工作流。


例如,当单击上下文菜单中的Run命令(在右键单击某个测试类时可见)或其对应的 gutter action 时,Android Studio 将默认使用 Gradle 运行配置来运行单元测试。



但是 Android Studio 不再识别现有的 Android JUnit 运行配置,因此需要将项目文件的 Android JUnit 运行配置迁移到 Gradle 运行配置。


要创建 Gradle 测试配置,请按照创建新的运行/调试配置中的说明选择 Gradle 模板,创建新配置后它将出现在 Gradle 部分的 Edit Configurations 对话框中:



如果要检查不再被识别的 Android JUnit 配置,有以下两种选择:



  • 在文本编辑器中打开手动保存的配置,这些文件的位置由用户指定,但文件通常出现在 <my-app>/.idea/runConfigurations/

  • <my-app>/.idea/workspace.xml临时配置和在 <component name="RunManager" ...> 节点中查找, 例如:


<component name="RunManager" selected="Gradle.PlantTest">

<configuration name="PlantTest" type="AndroidJUnit" factoryName="Android JUnit" nameIsGenerated="true">
      <module name="Sunflower.app" />
      <useClassPathOnly />
      <extension name="coverage">
        <pattern>
          <option name="PATTERN" value="com.google.samples.apps.sunflower.data.*" />
          <option name="ENABLED" value="true" />
        </pattern>
      </extension>
      <option name="PACKAGE_NAME" value="com.google.samples.apps.sunflower.data" />
      <option name="MAIN_CLASS_NAME" value="com.google.samples.apps.sunflower.data.PlantTest" />
      <option name="METHOD_NAME" value="" />
      <option name="TEST_OBJECT" value="class" />
      <option name="PARAMETERS" value="" />
      <option name="WORKING_DIRECTORY" value="$MODULE_DIR$" />
      <method v="2">
        <option name="Android.Gradle.BeforeRunTask" enabled="true" />
      </method>
    </configuration>

新的后台任务检查器


可以使用新的 后台任务检查器 来可视化、监控和调试应用程序的后台工作人员


首先将应用程序部署到运行 WorkManager Library 2.5.0 或更高版本的设备,然后从菜单栏中选择View > Tool Windows > App Inspection



你可以通过单击 worker 查看更多详细信息,例如可以看到 worker 的描述,它是如何执行的,它的 worker 链的细节,以及执行的结果。



你还可以通过从表中选择一个 worker 并单击工具栏中的 Show Graph View 来 查看 worker 链的可视化,然后可以选择链中的任何工作程序以查看其详细信息,或者如果它当前正在排队或正在运行,你也可以选择停止它。


如果要返回表格,请单击 Show List View



为了帮助调查执行失败的工作线程问题,开发者可以通过从表中选择并单击工具栏中的 Cancel Selected Worker 线程来停止当前正在运行或排队的工作线程,还可以使用 All tags 下拉菜单,通过标签过滤来选择表中的 workers。


从数据库检查器导出数据


现在开发者可以轻松地从 Database Inspector 导出数据库、表和查询结果,以在本地保存、共享或重新创建。


当你在 Android Studio 中打开一个应用程序项目并在 Database Inspector 中检查 该项目的应用程序时,你可以通过以下方式之一开始导出数据:



  • Databases 面板中选择一个数据库或表,然后单击面板顶部附近的 Export to file

  • 右键单击 Databases 面板中的数据库或表,然后从上下文菜单中选择 Export to file

  • 在选项卡中检查表或查询结果时,单击表或查询结果上方的 Export to file


选择导出操作后,可以使用 Export 对话框来帮助完成最后的步骤,如下所示,你可以选择以下列一种或多种格式导出数据:DB、SQL 或 CSV。



Updated UI for recording in Memory Profiler


我们为不同的记录活动整合了 Memory Profiler 用户界面 (UI),例如捕获堆转储和记录 Java、Kotlin 和本机内存分配。



Memory Profiler 提供了以下选项:



  • Capture heap dump:查看应用程序中在特定时间点使用内存的对象。

  • Record native allocations:查看每个 C/C++ 对象在一段时间内是如何分配的。

  • Record Java/Kotlin allocations:查看每个 Java/Kotlin 对象在一段时间内是如何分配的。


以下是如何使用这三个选项:



  • 要捕获堆转储,请选择 Capture heap dump,然后选择 Record ,在分析器完成对堆转储的捕获后,内存分析器 UI 将转换到显示堆转储的单独页面。




  • 要在运行 Android 10 及更高版本的设备上使用 Record native allocations,请选择 Record native allocations ,然后选择 Record ,而后记录将保持到单击 Stop 为止,之后 Memory Profiler UI 将转换为显示 native 记录的单独页面。



在 Android 9 及更低版本上,Record native allocations 选项不可用。




  • 要记录 Java 和 Kotlin 分配,请选择 Record Java / Kotlin allocations,然后选择 Record。 如果设备运行的是 Android 8 或更高版本,Memory Profiler UI 将转换为显示正在进行的记录的单独页面,开发者可以与纪律上方的迷你时间线进行交互(例如,更改选择范围),而如果要完成录制,可以选择 Stop




在 Android 7.1 及更低版本上,内存分析器使用传统分配记录,它会在时间线上显示记录,直到单击 Stop



更新链接的 C++ 项目


新版本已将与配置无关的 .cxx/ 文件从文件夹移动到 build/ 文件夹中。


CMake C++ 构建需要一个在配置阶段用于执行编译和链接步骤的 Ninja 项目,通过 CMake 生成的项目成本比较高,所以有望在 gradle clean 中不被清理。


因此,它们存储在文件夹.cxx/ 旁边的一个名为的 build/ 文件夹中,通常 Android Gradle 插件会注意到配置更改并自动重新生成 Ninja 项目。但是并非所有情况都可以检测到,发生这种情况时,可以使用 “Refresh Linked C++ Project” 选项手动重新生成 Ninja 项目。


用于多设备测试的新测试矩阵


Instrumentation tests 现在可以在多个设备上并行运行,并且可以使用专门的 Instrumentation tests 结果面板进行调查。使用此面板可以确定测试是否由于 API 级别或硬件属性而失败。



在各种 API 级别和形式因素上测试应用程序,是确保所有用户在使用您的应用程序时获得出色体验的最佳方法之一。


要利用此功能:



  • 1、在 IDE 顶部中心的目标设备下拉菜单中选择 Select Multiple Devices




  • 2、选择目标设备并单击OK




  • 3、运行测试。


要在 Run 面板中查看测试结果,请转到 View > Tool Windows > Run


新的测试结果面板允许按状态、设备和 API 级别过滤测试结果。此外可以通过单击标题对每列进行排序,通过单击单个测试单独查看每个设备的日志和设备信息。


StateFlow 支持数据绑定


对于使用协程的 Kotlin 应用程序,现在可以使用 StateFlow 对象作为数据绑定源来自动通知 UI 数据的变化。数据绑定将具有生命周期感知能力,并且只会在 UI 在屏幕上可见时触发。


要在 StateFlow 绑定类中使用对象,需要指定生命周期所有者来定义 StateFlow 对象的范围,并在布局中 ViewModel 使用绑定表达式将组件的属性和方法分配给相应的视图,如下所示例子:


class ViewModel() {
   val username: StateFlow<String>
}

<TextView
    android:id="@+id/name"
    android:text="@{viewmodel.username}" />


如果在使用 AndroidX 的 Kotlin 应用程序中 StateFlow,数据绑定的功能中会自动包含支持,包括协程依赖项。


要了解更多信息,请参阅使用可观察数据对象


改进了建议的导入


改进了建议导入功能支持的库数量,并更频繁地更新索引。


建议导入可帮助开发者快速轻松地将某些 Google Maven 工件导入类和 Gradle 项目,当 Android Studio 从某些 Google 库中检测到未解析的符号时,IDE 会建议将库导入到类和项目中。


支持构建分析器中的配置缓存


Build Analyzer现在可识别项目何时未启用配置缓存,并将其作为优化提供。Build Analyzer 运行兼容性评估,以在启用之前通知项目中的配置缓存是否存在任何问题。



改进的 AGP 升级助手


Android Gradle 插件升级助手 现在有一个持久的工具窗口,其中包含将要完成的步骤列表。


附加信息也显示在工具窗口的右侧,如果需要还可以选择不同版本的 AGP 进行升级,单击Refresh 按钮更新相应的更新步骤。



非传递性 R 类的重构


可以将非传递性 R 类与 Android Gradle 插件结合使用,为具有多个模块的应用程序实现更快的构建。


这样做有助于防止资源重复,确保每个模块的 R 类只包含对其自身资源的引用,而不从其依赖项中提取引用。这会带来更多最新的构建以及避免编译的相应好处。


可以通过转到 Refactor > Migrate to Non-transitive R Classes 来访问此功能。


支持 Jetpack Compose 工具


我们现在为预览和测试使用Jetpack Compose 的 应用程序提供额外支持。


为了获得使用 Jetpack Compose 开发的最佳体验,应该使用最新版本的 Android Studio Arctic Fox 以开发者可以体验 smart editor features,例如新项目模板和立即预览 Compose UI 的能力。


Compose preview


@Preview 方法 的以下参数现在可用:



  • showBackground:打开和关闭预览的背景。

  • backgroundColor:设置仅在预览表面中使用的颜色。

  • uiMode:这个新参数可以采用任何 Configuration.UI_* 常量,并允许您更改预览的行为,例如将其设置为夜间模式以查看主题的反应。



Interactive preview


可以使用此功能与你的 UI 组件交互,单击它们,然后查看状态如何更改,这是获取有关 UI 反应和预览动画的反馈的快速方法。启用它可单击 Interactive 图标预览将切换模式。



要停止时单击顶部工具栏中的 Stop Interactive Preview


image.png


Deploy to device


可以使用此功能将 UI 片段部署到设备,这有助于测试设备中代码的一小部分而无需启动整个应用程序。


单击 @Preview 注释旁边或预览顶部的 Deploy to Device 图标 ,Android Studio 会部署到连接的设备或模拟器。



Live Edit of literals


我们添加了文字的实时编辑预览,以帮助使用 Compose 的开发人员快速编辑其代码中的文字(字符串、数字、布尔值)并立即查看结果而无需等待编译。


此功能的目标是通过在预览、模拟器或物理设备中近乎即时地显示代码更改来帮助提高开发者的工作效率。



Compose support in the Layout Inspector


Layout Inspector 可以让开发这看到连接设备应用程序布局的丰富细节,应用程序交互并查看工具中的实时更新,以快速调试可能出现的问题。


开发者可以检查使用新的 Android 声明式 UI 框架 Jetpack Compose 编写的布局,无论应用程序使用完全由 Compose 编写的布局,还是使用 Compose 和 Views 的混合布局,布局检查器都 可以帮助开发者了解布局在运行设备上的呈现方式。


开始


首先,将应用程序部署到连接的设备,然后通过选择 View > Tool Windows > Layout Inspector 打开 Layout Inspector 窗口。


如果 Layout Inspector 没有自动连接到应用程序进程,请从进程下拉列表中选择所需的应用程序进程,应该很快就会在工具窗口中看到应用程序布局。


要开始检查 Compose 布局,请选择渲染中可见的布局组件 Component Tree 中 选择它。



Attributes 窗口将显示目前所选择的组合功能的详细信息。在此窗口中可以检查函数的参数及其值,包括修饰符和 lambda 表达式。


对于 lambda 表达式,检查器提供了一个快捷方式来帮助导航到源代码中的表达式。


Layout Inspector 显示调用堆栈的所有功能,组件到应用的布局。在许多情况下,这包括 Compose 库在内部调用的 Compose 函数。如果只想查看应用程序直接调用的 Component Tre中的 Compose 函数,可单击过滤器操作,这可能有助于将树中显示的节点数量减少到可能想要检查的数量。



改进部署下拉菜单


设备下拉列表现在可以区分选择的设备配置中的不同类型的错误。


图标和样式更改现在区分 错误(导致配置损坏的设备选择)和 警告(可能导致意外行为但仍可运行的设备选择)。


此外如果尝试将项目启动到出现错误或相关警告的设备,Android Studio 现在会发出警告。


新的 Wear OS 配对助手


新的 Wear OS 配对助手可指导开发人员直接在 Android Studio 中将 Wear OS 模拟器与物理或虚拟手机配对。


该助手可以帮助在手机上安装正确的 Wear OS Companion 应用,并在两台设备之间建立连接,你可以通过转到设备下拉菜单 > Wear OS Emulator Pairing Assistant



响应式布局模板


Android Studio Arctic Fox 现在包含一个新的布局模板,可适应各种显示尺寸和应用调整大小,例如手机、可折叠设备、平板电脑和分屏模式。


创建新项目或模块时,选择响应式活动模板以创建具有动态调整大小的组件的布局。



通过 File > New,选择 New Project 或 New Module,然后选择 Responsive Activity 模板。



补丁不适用于 Windows for v3.6-v4.1


Windows 平台上 v3.6-v4.1 到 Android Studio Arctic Fox 稳定版的补丁可能不起作用。

收起阅读 »

Flutter 2 Router 从入门到放弃 - 基本使用、区别&优势

前言 Flutter 2 主要带来的新特性有 Null 安全性趋于稳定,桌面和 Web 支持也正式宣布进入 stable 渠道,最受大家关注的就是 Add-to-App 相关的更新,从而改善 Flutter2 之前的版本混合开发体验不好缺点。所谓的 Add-...
继续阅读 »

前言


Flutter 2 主要带来的新特性有 Null 安全性趋于稳定,桌面和 Web 支持也正式宣布进入 stable 渠道,最受大家关注的就是 Add-to-App 相关的更新,从而改善 Flutter2 之前的版本混合开发体验不好缺点。所谓的 Add-to-App 就是将 Flutter 添加到现有的 iOS 和 Android 应用程序中来利用 Flutter,在两个移动平台上复用 Flutter 代码同时仍保留现有本机代码库的绝佳方法。在此方案出现之前,类似的第三方支持有 flutter_boost 、flutter_thrio 等,但是这些方案都要面对的问题是:非官方的支持必然存在每个版本需要适配的问题,而按照 Flutter 目前更新的速度,很可能每个季度的版本都存在较大的变动,所以如果开发者不维护或者维护不及时,那么侵入性极强的这类框架很容易就成为项目的瓶颈。


Flutter2 多引擎混合开发基本用法


1、先创建一个 Android 原生工程,( Android 原生项目工程创建过程略过)


2、Android 项目创建引入 Flutter Module,使用 File -> New- > New Module … -> 选择 Flutter Module ,然后指定一个 module name,填写相关息,最后点击确定,等待 Gradle sync 完成。


3、Android 项目集成 Flutter Module


1)、创建 FlutterEngineGroup 对象,FlutterEngineGroup 可以用来管理多个 FlutterEngine 对象,多个 FlutterEngine 之间是可以共享资源的,这样多个 FlutterEngine 占用的资源相对会少一些,FlutterEngineGroup 需要在 Application onCreate 方法中创建。


package com.zalex.hybird;

import android.app.Application;
import io.flutter.embedding.engine.FlutterEngineGroup;

public class WYApplication extends Application {
public FlutterEngineGroup engineGroup;
@Override
public void onCreate() {
super.onCreate();
// 创建 FlutterEngineGroup 对象
engineGroup = new FlutterEngineGroup(this);
}
}

2)、创建 WYFlutterEngineManager 缓存管理类,通过 FlutterEngineCache 缓存类,先从中获取缓存的 FlutterEngine,如果没有取到,通过 findAppBundlePath 和 entrypoint 创建出 DartEntrypoint 对象,这里的 findAppBundlePath 主要就是默认的 flutter_assets 目录;而 entrypoint 其实就是 dart 代码里启动方法的名称;也就绑定了在 dart 中 runApp 的方法,再通过 createAndRunEngine 方法创建一个 FlutterEngine,然后缓存起来。


public class WYFlutterEngineManager {
public static FlutterEngine flutterEngine(Context context, String engineId, String entryPoint) {
// 1. 从缓存中获取 FlutterEngine
FlutterEngine engine = FlutterEngineCache.getInstance().get(engineId);
if (engine == null) {
// 如果缓存中没有 FlutterEngine
// 1. 新建 FlutterEngine,执行的入口函数是 entryPoint
WYApplication app = (WYApplication) context.getApplicationContext();
DartExecutor.DartEntrypoint dartEntrypoint = new DartExecutor.DartEntrypoint(FlutterInjector.instance().flutterLoader().findAppBundlePath(), entryPoint);
engine = app.engineGroup.createAndRunEngine(context, dartEntrypoint);
// 2. 存入缓存
FlutterEngineCache.getInstance().put(engineId, engine);
}
return engine;
}
}

Activity 绑定 flutter 引擎入口


public class WYFlutterActivity extends FlutterActivity implements EngineBindingsDelegate {
private WYFlutterBindings flutterBindings;

@Override
protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
flutterBindings = new WYFlutterBindings(this,SingleFlutterActivity.class.getSimpleName(),"main",this);
flutterBindings.attach();
}
@Override
protected void onDestroy() {
super.onDestroy();
flutterBindings.detach();
}

@Override
public FlutterEngine provideFlutterEngine(@NonNull @NotNull Context context) {
return flutterBindings.getEngine();
}

@Override
public void onNext() {
Intent flutterIntent = new Intent(this, MainActivity.class);
startActivity(flutterIntent);
}
}

Fragment 绑定 flutter 引擎入口


int engineId = engineId ;//自定义引擎 Id
int fragmentId = 1233444;//自定义 FragmentId

FrameLayout flutterContainer = new FrameLayout(this);
root.addView(flutterContainer);
flutterContainer.setId(containerId);
flutterContainer.setLayoutParams(new LinearLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT,
1.0f
));

WYFlutterBindings flutterBindings = new WYFlutterBindings(this,"WYTopFragment","fragmentMain",this);

FlutterEngine engine = bottomBindings.getEngine();

FlutterEngineCache.getInstance().put(engineId+"", engine);
Fragment flutterFragment =FlutterFragment.withCachedEngine(engineId+"").build();
fragmentManager
.beginTransaction()
.add(containerId, flutterFragment)
.commit();

3)、flutter 模块引擎入口绑定,除了 main 入口,其他引擎入口都需要加上@pragma('vm:entry-point')注解


void main() => runApp(MyApp(Colors.blue));

@pragma('vm:entry-point')
void fragmentMain() => runApp(CustomApp(Colors.green));


Flutter2 多引擎混合开发与单引擎混合开发比较


1、 Flutter 多引擎方案是 flutter api 一直都是可以支持的,可以创建多个引擎,也可以渲染多个不同的界面,也是独立的,但是每次启动一个 flutter 引擎,都会占一个独立的引擎,通过测试可以发现,一个引擎 40M,创建 10 个引擎消耗了 235M,对内存的占用很大,在开发中是不可接受的。


2、由于 Flutter 2 之前版本多引擎的缺陷,业内的做法一般是对 isolate 或 engine 进行复用来解决。影响力较大的是以 FlutterBoost 和 Thrio 为代表的单引擎浏览器方案。即把 Activity/ViewController 作为承载 Dart 页面的浏览器,在页面切换时对单引擎进行 detach/attach,同时通知 Dart 层页面切换,来实现 Engine 的复用。由于只持有了一个 Engine 单例,仅创建一份 isolate,Dart 层是通信和资源共享的,内存损耗也得以有显著的降低。但是单引擎实现依赖于修改官方的 io.flutter 包,对 flutter 框架做出比较大的结构性修改。


3、从 Flutter 2 开始,多引擎下使用 FlutterEngineGroup 来创建新的 Engine,官方宣称内存损耗仅占 180K,其本质是使 Engine 可以共享 GPU 上下文、字形和 isolate group snapshot,从而实现了更快的初始速度和更低的内存占用。


4、Flutter 2 与 Flutter 1 创建引擎的区别:


Flutter1 引擎创建


//Android
val engine = FlutterEngine(this)
engine.dartExecutor.executeDartEntrypoin(DartExecutor.DartEntrypoint.createDefault())
FlutterEngineCache.getInstance().put(1,engine)
val intent = FlutterActivity.withCacheEngine(1).build(this)

//iOS
let engine = FlutterEngine()
engine.run()
let vc = FlutterViewController(engine:engine,nibName:nil,bundle:nil)

Fluter2 引擎创建


//Android
val engineGroup = FlutterEngineGroup(context)
val engine1 = engineGroup.createAndRunDefaultEngine(context)
val engine2 = engineGroup.createAndRunEngine(context,DartExecutor.DartEntrypoint(FlutterInjector.instance().flutterLoader().findAppBundlePath(),"anotherEntrypoint"))

//iOS
let engineGroup = FlutterEngineGroup(name:"example",project:nil)
let engine1 = engineGroup.makeEngine(withEntrypoint:nil,libraryURI:nil)
let engine2 = engineGroup.makeEngine(withEntrypoint:"anotherEntrypoint",libraryURI:nil)

5、Flutter 混合开发方案比较



6、Flutter 轻量级多引擎和单引擎优缺点比较



后记


本文通过代码和表格,我们讲述了 Flutter 2 多引擎使用、多引擎混合开发与单引擎混合开发区别和优缺点比较,下一节我们将一起去学习 Flutter 2 多引擎的实现原理。



作者:微医前端团队
链接:https://juejin.cn/post/6991619205523062821
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【Flutter组件集录】Dismissible

一、认识 Dismissible 组件 今天来看一个和滑动相关的组件:Dismissible 。如下图效果,该组件可以通过滑动来使条目移除。先来看一下它最简单的使用。 左滑 右滑 ...
继续阅读 »
一、认识 Dismissible 组件

今天来看一个和滑动相关的组件:Dismissible 。如下图效果,该组件可以通过滑动来使条目移除。先来看一下它最简单的使用。















左滑 右滑



_HomePageState 中通过 ListView 展示 60 个条目。如下 tag1 处,在构建条目时在条目外层包裹 Dismissible 组件。 构造中传入 keychild 入参。其中 key 用于标识条目,child 为条目组件。onDismissed 回调是在条目被移除时被调用。


指定注意的是:Dismissible 组件滑动移除只是 UI 的效果,实际的数据并未被移除。为了保证数据UI 的一致性,我们一般在移除后,会同时移除对应的数据,并进行重建,如下 tag2


class HomePage extends StatefulWidget {
const HomePage({Key? key}) : super(key: key);

@override
_HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
List<String> data = List.generate(60, (index) => '第$index个');

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Dismissible 测试'),),
body: ListView.builder(
itemCount: data.length,
itemBuilder: _buildItems,
),
);
}

Widget _buildItems(BuildContext context, int index) {
return Dismissible( //<---- tag1
key: ValueKey<String>(data[index]),
child: ItemBox(
info: data[index],
),
onDismissed: (direction) =>_onDismissed(direction,index),
);
}

void _onDismissed(DismissDirection direction,int index) {
setState(() {
data.removeAt(index); //<--- tag 2
});
}
}

其中 ItemBox 就是个高度为 56Container ,其中显示一下文字信息。


class ItemBox extends StatelessWidget {
final String info;

const ItemBox({Key? key, required this.info}) : super(key: key);

@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
height: 56,
child: Text(
info,
style: TextStyle(fontSize: 20),
),
);
}
}



二、详细了解 Dismissible 组件


上面我们已经简单认识了 Dismissible 组件的使用。如下源码中可以看出,keychild 属性是必选项,除此之外,还有很多其他的属性。下面我们来逐一认识一下:





1、 background 和 secondaryBackground

Dismissible 组件滑动时,我们可以指定背景组件。如果只设置 background ,那么左滑和右滑背景都是一样的,如下左图绿色背景。如果设置 backgroundsecondaryBackground ,则左滑背景为 background ,右滑背景为 secondaryBackground ,如下右图。















单背景 双背景

代码实现也 很简单,指定 backgroundsecondaryBackground 对于组件即可。如下 tag1tag2 处理。


Widget _buildItems(BuildContext context, int index) {
return Dismissible(
key: ValueKey(data[index]),
background: buildBackground(), // tag1
secondaryBackground: buildSecondaryBackground(), // tag2
child: ItemBox(
info: data[index],
),
onDismissed: (direction) =>_onDismissed(direction,index),
);
}

Widget buildBackground(){
return Container(
color: Colors.green,
alignment: Alignment(-0.9, 0),
child: Icon(
Icons.check,
color: Colors.white,
),
);
}

Widget buildSecondaryBackground(){
return Container(
alignment: Alignment(0.9, 0),
child: Icon(
Icons.close,
color: Colors.white,
),
color: Colors.red,
);
}



2. confirmDismiss 回调

从源码中可以看出 confirmDismiss 的类型为 ConfirmDismissCallback 。它是一个函数类型,可以回调出 DismissDirection 对象,返回 bool 值。可以看出这个回调是一个异步方法,所以我们可以处理一下异步事件。


---->[Dismissible#confirmDismiss 声明]----
final ConfirmDismissCallback? confirmDismiss;

typedef ConfirmDismissCallback = Future<bool?> Function(DismissDirection direction);

如下左图中,滑动结束后,等待两秒再执行后续逻辑。效果上来看条目会在两秒后移除。也就说明 onDismissed 是在 confirmDismiss 异步方法完成后才被调用的。


该回调有一个 Future<bool?> 类型的返回值,返回 false 则表示不移除条目。如下右图中,绿色背景下不会移除条目,红色背景下移除条目。就可以通过该返回值进行控制。















执行异步事件 返回值的功效

代码实现如下, tag1 处设置 confirmDismiss 属性。返回值是看 direction 是否不是 startToEnd,即 从左向右滑动 。也就是说, 从左向右滑动 时,会返回 false ,即不消除条目。


Widget _buildItems(BuildContext context, int index) {
return Dismissible(
key: ValueKey(data[index]),
background: buildBackground(),
secondaryBackground: buildSecondaryBackground(),
child: ItemBox(
info: data[index],
),
onDismissed: (direction) =>_onDismissed(direction,index),
confirmDismiss: _confirmDismiss, // tag1
);
}

Future<bool?> _confirmDismiss(DismissDirection direction) async{
await Future.delayed(Duration(seconds: 2));
print('_confirmDismiss:$direction');
return direction!=DismissDirection.startToEnd;
}



3. direction 滑动方向

direction 表示滑动的方向,类型是 DismissDirection 是枚举,有 7 元素。


enum DismissDirection {
vertical,
horizontal,
endToStart,
startToEnd,
up,
down,
none
}

如下左图中,设置 startToEnd ,那么从右往左就无法滑动。如下右图中,设置 vertical ,那条目就只能在竖直方向响应滑动。不过和列表同向滑动有个问题,条目响应了竖直拖拽手势,那列表的拖拽手势就会竞技失败,所以列表是滑不动的。一般来说不会让 Dismissible 和列表滑动方向相同,当列表是水平方向滑动, Dismissible 可以使用竖直方向滑动。















startToEnd vertical



4. onResize 和 resizeDuration

在竖直列表中,滑动消失时,下方条目会有一个 上移 的动画。resizeDuration 就代表动画时长,而 onResize 会在动画执行的每帧中进行回调。















默认时长 2s

源码中可以看出 resizeDuration 的默认时长为 300 ms



在深入瞄一眼,可以看出会监听动画器执行 _handleResizeProgressChanged 。而 onResize 就是在此触发的。另外这个动画的曲线是 _kResizeTimeCurve



void _handleResizeProgressChanged() {
if (_resizeController!.isCompleted) {
widget.onDismissed?.call(_dismissDirection);
} else {
widget.onResize?.call();
}
}

const Curve _kResizeTimeCurve = Interval(0.4, 1.0, curve: Curves.ease);



5.dismissThresholds 和 movementDuration

dismissThresholds 表示消失的阈值,类型为Map<DismissDirection, double> 映射,也就是说我们可以设置不同滑动方向的容忍度, 默认是 0.4 。而 movementDuration 代表滑动方向上移动的动画时长。


const double _kDismissThreshold = 0.4;

final Map<DismissDirection, double> dismissThresholds;














默认效果 本案例效果

下面代码的效果如上图右侧,当 startToEnd 的宇宙设置为 0.8 , 就会比默认的 难触发移除事件 。其中 movementDuration 设置为 3 s 可以很明显地看出,水平移动的慢速。


Widget _buildItems(BuildContext context, int index) {
return Dismissible(
key: ValueKey(data[index]),
background: buildBackground(),
secondaryBackground: buildSecondaryBackground(),
onResize: _onResize,
resizeDuration: const Duration(seconds: 2),
dismissThresholds: {
DismissDirection.startToEnd: 0.8,
DismissDirection.endToStart: 0.2,
},
movementDuration: const Duration(seconds: 3),
child: ItemBox(
info: data[index],
),
direction: DismissDirection.horizontal,
onDismissed: (direction) => _onDismissed(direction, index),
confirmDismiss: _confirmDismiss,
);
}



6. crossAxisEndOffset 交叉轴偏移

如下图,是 crossAxisEndOffset-2 的效果,在滑动过程中,原条目在交叉轴(此处为纵轴)会发生偏移,偏移量就是 crossAxisEndOffset * 组件高 。右图所示,滑动到一般时, 条目 4 已经上移了一个条目高度。















1 2

最后 dragStartBehaviorbehavior 就不说了,这种通用的属性大家应该非常清楚。




三、从 Dismissible 源码中可以学到什么


Dismissible 组件中的 confirmDismissonDismissed 两个回调打的一个组合拳,还是非常巧妙的,在实际开发中我们也可以通过异步回调来处理一些界面效果。我们来看一下源码中的实现: confirmDismiss 回调在 _confirmStartResizeAnimation 方法中进行调用,



在拖拽结束,会先等待 _confirmStartResizeAnimation 的执行,且返回 true ,才会执行 _startResizeAnimation



另外一处是在 _moveController 动画器执行完毕,如果动画完成,也会执行类似逻辑。



最后 onDismissed 回调会在 _startResizeAnimation 中触发。这也就是如何通过一个异步方法,来控制另一个回调的触发。





Dismissible 组件的使用方式到这里就完全介绍完毕,那本文到这里就结束了,谢谢观看,明天见~

收起阅读 »

我在几期薅羊毛活动中学到了什么~

前言 为什么突然想写一篇总结了呢,其实也是被虐的。今年 3 月份初期,我们商城接了一个 XX 银行的一分购活动(说白点就是薅羊毛),那时候是活动第一期,未曾想到活动入口开放时,流量能直接将 cpu 冲至 100%,导致服务短暂的 502 了。。期间采取了紧急方...
继续阅读 »

前言


为什么突然想写一篇总结了呢,其实也是被虐的。今年 3 月份初期,我们商城接了一个 XX 银行的一分购活动(说白点就是薅羊毛),那时候是活动第一期,未曾想到活动入口开放时,流量能直接将 cpu 冲至 100%,导致服务短暂的 502 了。。期间采取了紧急方案到活动结束,但未曾想到还有活动二期,以及上周刚上线的活动三期。想着最近这段时间也做了一些事情,还有遇到的一些坑点,趁此机会,就不偷懒记录一下吧。


活动一期到三期具体做了些什么


技术背景&瓶颈


项目是基于 Vue+SSR 架构的,且没有做缓存处理,没做缓存的主要原因第一个是原本应用 tps 比较低,改造动力不强,并且页面渲染结果中包含了用户数据以及服务端时间,没法在不经过改造的情况下直接上缓存。所以当一期活动大流量冲击时,高并发情况下很容易将 cpu 打至 100%。


一期在未知情况下,服务直接扛不住了,当时为了活动能正常进行,首要方案就是先加机器扛住部分压力,紧接着就是加缓存,目前有两种缓存方案,缓存页面或缓存组件,但由于我们的需要缓存的商品详情页组件涉及到动态信息,可维护性太差,心智成本高,最终选择了前者。我们整理了一下商详页有关动态变化的信息数据(与时间/用户相关等类型的数据),在活动期间,紧急屏蔽了部分不影响功能的动态内容,然后页面上 CDN。


活动结束后,我们做了下复盘,要像应用要能保障大流量情况下稳定运行,性能优化处理是避免不了的了。为此我们做了以下四大方案:




  1. 对数据做动静分离: 我们可以将数据分类成动静两类,静态数据即是一段时间内,不随时间/用户改变,动态数据则是相反的,经常变动,与时间有关,有用户相关等类型的数据都可以归类为动态数据。原页面无法上缓存的最大阻碍就是,就是在 node 渲染模板时,会默认获取用户数据,或是在 asyncData 中调用用户相关的接口;此外,还会设置服务端时间等动态数据。所以思路就是将静态数据放在 node 获取,将动态数据放到客户端(浏览器读取 asyncData、mounted 等浏览器生命周期里)获取保证服务端的清洁。




  2. 页面接入 CDN: 经过动静态分离的改造后,已经可以将其路径加入 cdn。但需要注意路径上 query 等参数是否会影响渲染,如果影响,需要把逻辑搬到客户端,同时需要注意一下过期时间(如 10 分钟)是否会对业务产生影响




  3. 应用缓存: 如果在比较糟糕的情况下,cdn 失效了导致回源率上升,应用本身还是需要做好准备。这就要根据项目需要去选择内存缓存/redis 缓存。




  4. 自动降级: 在极端的情况下,前面的缓存都没挡住流量,就需要终极方案:降级渲染。所谓降级渲染,就是不进入路由,直接将空模板返回,完全交给浏览器去做渲染。这样做的最大好处就是完全避免了 node 压力,将一个 ssr 应用变成了静态应用。缺点也是显而易见的,用户看到的是空模板,需要等待初始化。那如何自动降级呢,可以通过定时器时时检测 cpu、负载等压力,来确定当前机器的负载,从而决定是否降级;也可以通过 url 的 query 上是否携带特定标识,显式地决定是否降级。




对项目方案做了以上性能优化接下来就是压测,也算是顺利上线了。🤪


二期活动没过多久又来了,不过如我们预期,项目很稳定地扛住了压力,期间也增加了流量接口,并加友好提示等优化。但其中一个痛点时需要针对几个特殊商品去做个文案处理,这几个文案非接口返回,也是临时性的一些醒目提示,没必要放在详情页接口中返回。由于时间也很紧急,我们不确定后面还有没有这种特定的文案需求(和具体的页面以及特定的区域关联),决定还是暂时写一些 low code:针对特定的活动商品 id,临时添加文案,活动下线之后,把文案去除。这样做的风险性是有的,毕竟代码是临时性的,需要上下线,并且有时间延迟。但好在活动结束时是周末,最后一天流量访问并不大,给了相对应的引导文案以及售后处理,评估下来影响不大,尚可接受。


以下图片商品详情页和商品购买页需要加的特定文案:


WechatIMG61.png


WechatIMG62.png


薅羊毛活动是真香现场吗~~6 月底产品就和我打了个招呼,说 XX 活动又要有三期了,但整体方案依旧和二期一样不变。我内心:还来???(小声说句打工人太苦了),由于最终时间没定下来,也有了二期的教训之后,和后端同学也一起商量了一下,把活动商品往配置化方向考虑,放在我们配置后台中文案模块且是可行的。针对商品详情页,考虑到不破坏动静分离,先确定下配置化接口返回的数据是静态的,可以放在服务端获取。以下具体三期做的事情:



  1. 将参与活动商品的文案做成配置化,从配置接口获取,去除 low code

  2. 整理大流量活动页(例如商详页)的接口,放在客户端的接口需要做限流,接口达到一定的 tps 后,返回 429 状态,前端要做容错处理,页面功能正常访问,屏蔽限流接口错误。

  3. 针对购买限流接口,需要给 busy 提示(活动太火爆了,请稍后再试)


// 统一在 getResponseErrorInterceptor 处针对 429 状态做处理
export const getResponseErrorInterceptor = ({ errorCallback }) => (error) => {
if (!isClient) {
...
} else {
// 429 Code 服务报错需要支持不弹出错误提示
if (+error.response.status === 429) {
// 针对限流接口,且需要 busy 提示时增加 needBusyMsg 属性
errorCallback(error.config.needBusyMsg ? '活动太火爆了,请稍后再试' : null);
} else {
...
);
}
}

return throwError(error);
};

结束了上周一周忙碌的压测和测试,三期终于上线了。👏👏👏👏


想了解更多 Vue SSR 性能优化方案可以移步到这里: Vue SSR 性能优化实践


实际过程中遇到的一些 Coding Question



  1. 本地项目(vue-ssr 架构)里,一个动态接口放在服务端获取时,有一段代码很 easy,一个是否是会员的标识去开通会员按钮的显隐,代码如下(代码有简化):


<redirect
v-if="!isVip"
:link="link"
type="h5"
>
开通会员<ui-icon name="arrow-right" />
</redirect>

本地中虽然运行正常,但是会有如下警告:
vue.esm.js:6428 Mismatching childNodes vs. VNodes: NodeList(2) [div, a.DetailVip-right.Redirect] (2) [VNode, VNode]


[Vue warn]: The client-side rendered virtual DOM tree is not matching server-rendered content. This is likely caused by incorrect HTML markup, for example nesting block-level elements inside <p>, or missing <tbody>. Bailing hydration and performing full client-side render.


但更新到测试环境中,页面会失效,点击失效等。会报有如下错误:
Failed to execute 'appendChild' on 'Node': This node type does not support this method


分析


Vue SSR 指南在客户端激活里刚好说到了一些需要注意的坑,使用「SSR + 客户端混合」时,需要了解的一件事是,浏览器可能会更改的一些特殊的 HTML 结构。例如 table 中漏写<tbody>,像以下几种情况也会导致导致服务端返回的静态 HTML 和浏览器端渲染的内容不一致:



  1. 无效的HTML(例如:<p><p>Text</p></p>

  2. 服务器与客户端的不同状态

  3. 例如日期、时间戳和随机化等不确定变量的影响

  4. 第三方脚本影响到了组件的渲染

  5. 需要身份验证相关时


当然确定原因之后对症下药,总结有几种办法可以解决此问题:



  1. 检查相关代码,确保 HTML 有效

  2. 最简单粗暴的一个方法就是:用v-show去代替v-if,要知道构建时生成的HTML是无状态的,应用程序中与身份验证相关的所有部分应该只在客户端呈现,具体可以 diff 下获取数据以及在服务器/客户端呈现的内容,解决服务器和客户端之间的状态不一致

  3. 面对第三方脚本这类的,可以通过将组件包装在标签中来避免在服务器端渲染组件

  4. .....(欢迎补充)


针对此类问题,还可以看看这篇文章:blog.lichter.io/posts/vue-h…


2: 同样的 h5 页面,在浏览器中打开配置生效,而在公众号&小程序中打开却失效了?


三期的时候,我们把活动商品 id 和对应文案做成了配置化处理。
配置方式如下:


WechatIMG64.png
获取商品配置内容经过 JSON.stringify()之后,毋庸置疑会得到如下字符串:


455164527672033280|龙支付 立减 10 元|满 40 立减 10(仅限 XX 卡)#\\n623841656577658880|龙支付 立减 10 元(仅限 XX 卡)|满 40 立减 10(仅限 XX 卡)#\\n350947143063699456|龙支付测试 立减 10 元(仅限 XX 卡)|满 40 立减 10 测试(仅限 XX 卡)#

在详情页获取所有的商品 id 列表信息,我们用的#做区分,写了一个简单的正则如下:


activityItems() {
return this.getFieldValue('activity_item')?.split('#\\n');
},

但在公众号里面打开我们的 h5 链接,会将#自动转义成\,内容会变成:


455164527672033280|龙支付 立减 10 元(仅限 XX 卡)|满 40 立减 10(仅限 XX 卡)\\\n623841656577658880|龙支付 立减 10 元(仅限 XX 卡)|满 40 立减 10(仅限 XX 卡)\\\n350947143063699456|龙支付测试 立减 10 元(仅限 XX 卡)|满 40 立减 10 测试(仅限 XX 卡)\

啊,这,,,不是吧??(发现时内心几乎是崩溃的)😩 解决方式立马把#字符换成不被转移的字符;


另外在小程序中打开失效是因为延用二期的方案,当时做了限制判断,只需要在主站和主 app 中打开有效,小程序设有自己单独的 appid,三期活动有多方入口,把该限制放开即可。


总结


薅羊毛参与了三期,也是积累了一些经验,踩了一些坑吧,想着太久没写了该记录一下了,先总结到这里,还有忘记的再补充~



链接:https://juejin.cn/post/6989618653998088228

收起阅读 »

webpack5 和 webpack4 的区别有哪些 ?

1、Tree Shaking 作用: 如果我们的项目中引入了 lodash 包,但是我只有了其中的一个方法。其他没有用到的方法是不是冗余的?此时 tree-shaking 就可以把没有用的那些东西剔除掉,来减少最终的bundle体积。 usedExports...
继续阅读 »

1、Tree Shaking


作用: 如果我们的项目中引入了 lodash 包,但是我只有了其中的一个方法。其他没有用到的方法是不是冗余的?此时 tree-shaking 就可以把没有用的那些东西剔除掉,来减少最终的bundle体积。



usedExports : true, 标记没有用的叶子




minimize: true, 摇掉那些没有用的叶子



  // webpack.config.js中
module.exports = {
optimization: {
usedExports: true, //只导出被使用的模块
minimize : true // 启动压缩
}
}

由于 tree shaking 只支持 esmodule ,如果你打包出来的是 commonjs,此时 tree-shaking 就失效了。不过当前大家都用的是 vue,react 等框架,他们都是用 babel-loader 编译,以下配置就能够保证他一定是 esmodule


image.png


webpack5的 mode=“production” 自动开启 tree-shaking。


2、压缩代码


1.webpack4


webpack4 上需要下载安装 terser-webpack-plugin 插件,并且需要以下配置:



const TerserPlugin = require('terser-webpack-plugin')

module.exports = {
// ...other config
optimization: {
minimize: !isDev,
minimizer: [
new TerserPlugin({
extractComments: false,
terserOptions: {
compress: {
pure_funcs: ['console.log']
}
}
}) ]
}

2.webpack5

内部本身就自带 js 压缩功能,他内置了 terser-webpack-plugin 插件,我们不用再下载安装。而且在 mode=“production” 的时候会自动开启 js 压缩功能。



如果你要在开发环境使用,就用下面:



  // webpack.config.js中
module.exports = {
optimization: {
usedExports: true, //只导出被使用的模块
minimize : true // 启动压缩
}
}

3.js 压缩失效问题

当你下载 optimize-css-assets-webpack-plugin ,执行 css 压缩以后,你会发现 webpack5 默认的 js 压缩功能失效了。先说 optimize-css-assets-webpack-plugin 的配置:



npm install optimize-css-assets-webpack-plugin -D



module.exports = { 
optimization: {
minimizer: [
new OptimizeCssAssetsPlugin()
]
},
}


此时的压缩插件 optimize-css-assets-webpack-plugin 可以配置到 plugins 里面去,也可以如图配置到到 optimization 里面。区别如下:



配置到 plugins 中,那么这个插件在任何情况下都会工作。 而配置在 optimization 表示只有 minimize 为 true 的时候才能工作。



当安装 optimize-css-assets-webpack-plugin 以后你去打包会发现原来可以压缩的 js 文件,现在不能压缩了。原因是你指定的压缩器是



optimize-css-assets-webpack-plugin 导致默认的 terser-webpack-plugin 就会失效。解决办法如下:



npm install terser-webpack-plugin -D



 optimization: {
minimizer: [
new TerserPlugin({
extractComments: false,
terserOptions: {
compress: { pure_funcs: ['console.log'] },
},
}),
new OptimiazeCssAssetPlugin(),
]
}

即便在 webpack5 中,你也要像 webpack4 中一样使用 js 压缩。


4.注意事项

在webpack5里面使用 optimize-css-assets-webpack-plugin 又是会报错,因为官方已经打算要废除了,请使用替换方案:



npm i css-assets-webpack-plugin -D



3、合并模块



普通打包只是将一个模块最终放到一个单独的立即执行函数中,如果你有很多模块,那么就有很多立即执行函数。concatenateModules 可以要所有的模块都合并到一个函数里面去。



optimization.concatenateModules = true


配置如下:


module.exports = {
optimization: {
usedExports: true,
concatenateModules: true,
minimize: true
}
}

此时配合 tree-shaking 你会发现打包的体积会减小很多。


4、副作用 sideEffects



webpack4 新增了一个 sideEffects 的功能,容许我们通过配置来标识我们的代码是否有副作用。




这个特性只有在开发 npm 包的时候用到



副作用的解释: 在utils文件夹下面有index.js文件,用于系统导出utils里面其他文件,作用就是写的少, 不管 utils 里面有多少方法,我都只需要引入 utils 即可。


// utils/index.js
export * from './getXXX.js';
export * from './getAAA.js';
export * from './getBBB.js';
export * from './getCCC.js';

 // 在其他文件使用 getXXX 引入
import {getXX} from '../utils'

此时,如果文件 getBBB 在外界没有用到,而 tree-shaking 又不能把它摇掉咋办?这个 getBBB 就是副作用。你或许要问 tree-shaking 为什么不能奈何他?原因就是:他在 utils/index.js 里面使用了。只能开启副作用特性。如下:


// package.json中
{
name:“项目名称”,
....
sideEffects: false
}

// webpack.config.js

module.exports = {
mode: 'none',
....
optimization: {
sideEffects: true
}
}

副作用开启:



(1)optimization.sideEffects = true 开启副作用功能




(2)package.json 中设置 sideEffects : false 标记所有模块无副作用



说明: webpack 打包前都会检查项目所属的 package.json 文件中的 sideEffects 标识,如果没有副作用,那些没有用到的模块就不需要打包,反之亦然。此时,在webpack.config.js 里面开启 sideEffects。


5、webpack 缓存


1.webpack4 缓存配置

支持缓存在内存中



npm install hard-source-webpack-plugin -D



const HardSourceWebpackPlugin = require('hard-source-webpack-plugin') 

module.exports = {
plugins: [
// 其它 plugin...
new HardSourceWebpackPlugin(),
] }

2. webpack5 缓存配置

webpack5 内部内置了 cache 缓存机制。直接配置即可。



cache 会在开发模式下被设置成 type: memory 而且会在生产模式把cache 给禁用掉。



// webpack.config.js
module.exports= {
// 使用持久化缓存
cache: {
type: 'filesystem',
cacheDirectory: path.join(__dirname, 'node_modules/.cac/webpack')
}
}


type 的可选值为: memory 使用内容缓存,filesystem 使用文件缓存。




当 type=filesystem的时候设置cacheDirectory才生效。用于设置你需要的东西缓存放在哪里?



6、对loader的优化



webpack 4 加载资源需要用不同的 loader




  • raw-loader 将文件导入为字符串

  • url-loader 将文件作为 data url 内联到 bundle文件中

  • file-loader 将文件发送到输出目录中


image.png



webpack5 的资源模块类型替换 loader




  • asset/resource 替换 file-loader(发送单独文件)

  • asset/inline 替换 url-loader (导出 url)

  • asset/source 替换 raw-loader(导出源代码)

  • asset


image.png


webpack5


7、启动服务的差别


1.webpack4 启动服务

通过 webpack-dev-server 启动服务


2.webpack5 启动服务

内置使用 webpack serve 启动,但是他的日志不是很好,所以一般都加都喜欢用 webpack-dev-server 优化。


8. 模块联邦(微前端)



webpack 可以实现 应用程序和应用程序之间的引用。



9.devtool的差别


sourceMap需要在 webpack.config.js里面直接配置 devtool 就可以实现了。而 devtool有很多个选项值,不同的选项值,不同的选项产生的 .map 文件不同,打包速度不同。


一般情况下,我们一般在开发环境配置用“cheap-eval-module-source-map”,在生产环境用‘none’。


devtool在webpack4和webpack5上也是有区别的



v4: devtool: 'cheap-eval-module-source-map'




v5: devtool: 'eval-cheap-module-source-map'



10.热更新差别



webpack4设置



image.png



webpack5 设置



如果你使用的是bable6,按照上述设置,你会发现热更新无效,需要添加配置:


  module.hot.accept('需要热启动的文件',(source)=>{
//自定义热启动
})

当前最新版的babel里面的 babel-loader已经帮我们处理的热更新失效的问题。所以不必担心,直接使用即可。


如果你引入 mini-css-extract-plugin 以后你会发现 样式的热更新也会失效。


只能在开发环境使用style-loader,而在生产环境用MinicssExtractPlugin.loader。 如下:


image.png


11、使用 webpack-merge 的差别



webpack4 导入



const merge = require('webpack-merge);



webpack 5 导入



const {merge} = require('webpack-merge');


12、 使用 copy-webpack-plugin 的差别


//webpack.config.js
const CopyWebpackPlugin = require('copy-webpack-plugin');

module.exports = {
plugins: [
// webpack 4
new CopyWebpackPlugin(['public']),

// webpack 5
new CopyWebpackPlugin({
patterns: [{
from: './public',
to: './dist/public'
}]
})
]
}

webpack5 支持的新版本里面需要配置的更加清楚。


链接:https://juejin.cn/post/6990869970385109005

收起阅读 »

与大厂面试官的高端博弈、顶级拉扯

前言 最近是跳槽季,发现有小伙伴在一些非技术的软性问题上答的不是很好。 众所周知,程序员情商偏低,而这些软性问题,恰恰都具有一定欺骗性和吹牛皮成分在里边,对于演技不好的直男癌,简直就是天生克星。 其实不用太担心,软性问题往往就那几个,稍加训练和准备,你就可以成...
继续阅读 »

前言


最近是跳槽季,发现有小伙伴在一些非技术的软性问题上答的不是很好。


众所周知,程序员情商偏低,而这些软性问题,恰恰都具有一定欺骗性和吹牛皮成分在里边,对于演技不好的直男癌,简直就是天生克星。


其实不用太担心,软性问题往往就那几个,稍加训练和准备,你就可以成为一位高端名猿。


题目


第 1 题:说一下你自己的缺点


演技考验:4星


这题处处暗藏杀鸡,很多小伙伴会推心置腹,诚实的将自己所有缺点说出来,比如:“我脾气不好”、“我学习东西慢”、“我贪财好.色”。


这种缺点,人人都有,但大可不必说出来。


很多小伙伴说,我就要做real man,你爱喜欢不喜欢。外面的那些人都是虚伪装哔的妖艳剑货,我就要做真实的自己。


其实,这根本不是真实的自己,这是情商低。不懂得逢场作戏,不懂得在特定的场合说正确的话。


所以,如何说一个缺点,但又不完全是缺点的缺点,还能让人觉得这是优点的缺点


你应该这么演:


唉声叹气,微笑、头部倾斜45度看向地面,说:我偶尔会因为专研技术问题,而搞到深夜,把自己弄得很累。今后我会多注意,把控好技术学习和工作状态的平衡。


这里几个细节注意以下



  • “头部倾斜45度看向地面”从行为学上让人感觉你没有架子,谦虚好相处。

  • 回答中多次用了“搞”、“弄”、“累”等动词,让人觉得你不做作,接地气。

  • 我说我的缺点是因为太爱学习,就好像是说,我的缺点是因为我太有钱,这根本不是缺点,而是优点,只是换了一种说法,直接上天日龙


这就是传说中的反套路学,程序猿和面试官之间的高端博弈,顶级拉扯


如果你有更好的答案或想法,欢迎在题目对应的github下留言:第一名的小蝌蚪


第 2 题:你对加班什么看法?


演技考验:3星


这题真的是炒鸡容易被问到。首先,没人有会喜欢加班,但是,但凡出现“敏捷开发”、“谈加班看法”的,大概率都是经常加班的公司。


我觉得只要不是业界那几个著名黑工厂,正常的加班,都是可以接受的


在大厂工作了8年,总结了一下导致我加班的几个原因:



  • 不爱思考

  • 能力不行

  • 贪玩好.色


  • 第五点不能说,懂的都懂


这8年里,我对加班的心态,从早期的抵触,到中期的忍耐,到现在的主动,我发现,你越不想加班,加班就越来找你,还不如主动进攻,主动学习技术,主动思考工作中优化点。


将重复性工作,通过开发一些工程化、自动化工具去代替,慢慢的你会发现,工作会事半功倍,不再那么被动,真的比被按住头逼你加班要好受很多。


撸迅曾说过:“工作就像强J,与其反抗,不如闭上眼睛好好享受”,很悲哀,但也是中年屌丝生存之道


所以,这道题有一个很不错的答案:
如果是工作需要我会义不容辞加班。但同时,我也会思考工作中的优化点,将重复性工作,通过开发一些工程化、自动化工具去代替,提高工作效率,减少不必要的加班


如果你有更好的答案或想法,欢迎在这题目对应的github下留言:第一名的小蝌蚪


第 3 题:你为什么从上家离职


演技考验:2星


有几个注意点,在说离职原因的时候,有几个大忌:



  • 不能说你跟上家leader闹掰

  • 不能说自己是被开除的

  • 不能说上家公司黄了,所以我跳槽了


反正千万记住,不要说任何上家公司和leader的坏话。


正确的表演应该是,不管上家对你怎么样,都要一副感恩戴德的态度去展示给面试官。是因为某种不可抗拒因素所以才导致你离职


有一个常规回答:上家公司平台趋于稳定,想到一家更大的平台去开阔视野,更好的展现自己的实力,让自己创造更大的价值


见过几个比较搞笑的回答:“我老婆要生了,想去一家能准点下班的公司”、“我失恋了,想去一个地方重新开始”,嘻嘻,但老铁还是别这么回答就好


如果你有更好的答案或想法,欢迎在这题目对应的github下留言:第一名的小蝌蚪


第 4 题:面对hr和面试官刁难,如何应对


演技考验:5星


这个情况是我真实遇到的,我职业生涯第二家公司(某知名大厂),就被当时的hr刁难了


她当时很高傲,还很藐视我,说过几句话让我印象深刻



  • 你背景很一般啊,上家公司是个小公司,我们一般只要有大厂经历的候选人

  • 我们都招985的研究生,你的综合素质没有我想象的好

  • 你简历里面写的那几个项目经历,都太一般了,没有任何知名度,你凭什么觉得我们会要你呢?


当时面完以后,我其实很生气,觉得那个hr人品有问题,但从头到尾都忍住了。


最后诡异的是。。。我还拿到了offer,拿到了期望的薪资和职级。。。。。


很久以后问过她为什么要这么面试候选人


她的回答,大意是:“hr一般这么刁难你,都是想压你的气势,让你受挫,让你觉得配不上我们,增加你接收offer的成功率,还能增加将来谈判薪资的主动权。”


这绝对是一种对人性心理的操控了。。。。


第二点,尤为重要“测试你的情商,那些随便刺激一下,就暴怒、就疯狂反驳、情绪失控的候选人,是绝对不能要的。能全程忍下来,且不卑不亢,保持自信的人,证明了你的抗压能力,确保你今后在工作中能处理好各种工作关系和极端情况”


她的回答令我醍醐灌顶,可能是代码敲多了,从来没有想过,原来人性也可以和代码一样,被度量,被证明


从那以后,对大厂的hr专业度,还是挺认可的


当然,这种刁难也可能是pua的早期萌芽,细思极恐。。。


所以针对候选人在面试过程中被刁难情况,应该如何表现和回答呢?


大家可以先自行思考一下


我把答案放在了github中,稍后公布,如果你有更好的想法,也可以给我留言:第一名的小蝌蚪


总结


以上4题,揭露了些面试中的套路和反套路,展示了逢场作戏和演技的技巧,很多人可能会觉得这样很虚伪


这里又要引用撸迅的一句名言:“当混浊变成一种常态,清白也会是一种罪行”


虚伪的人,有时候也是一种自我保护。


也许这并不虚伪,而是一种生存法则,因为不这么做,就会被这么做的人弄si。


希望大家针对上面提出的问题,和对应的答案,触发一些思考,总结自身,完善自己。不仅仅是面试,在工作也是要这样。


由于篇幅限制,下期会公布另外几道软性问题的答案:



  • 1.职场上,你的技术方案和同事不合,如何处理?

  • 2.如果你的方案和领导不合,如何处理?

  • 3.你未来五年的规划是什么

  • 4.你如何看待ppt文化

  • 5.你的入职,能给我们带来什么价值



链接:https://juejin.cn/post/6990533805786431524

收起阅读 »