注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

LeetCode刷题-合并区间

一、题目描述 难度:中等~ 以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。 示例1: 输入:...
继续阅读 »

一、题目描述


难度:中等~

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。


示例1:


输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例2:


输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

提示:
  1 <= intervals.length <= 10^4
  intervals[i].length == 2
  0 <= starti <= endi <= 10^4


作者:力扣 (LeetCode)
链接:leetcode-cn.com/leetbook/re…
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


二、题目解析


思路:
直接代码里注释!


三、代码


1.Python实现



初见的第一思路:
1.按左端点从小到大排序



2.有交集,更新右端点;无交集,则保存当前区间


class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
//将二维数组intervals按照其内每个子数组第一个元素从小到大排序
intervals.sort()
result = list()
for i in intervals:
//如果result中没有子数组或者当前两个数组无交集
//直接保存当前区间
if not result or result[-1][1] < i[0]:
result.append(i)
//否则有交集,取两个数组中第一个元素最大的值作为当前数组的第一个元素(即合并操作)
else:
result[-1][1] = max(result[-1][1], i[1])
return result

复杂度分析




  • 时间复杂度:O(n log n),其中 n 为区间的数量。除去排序的开销,我们只需要一次线性扫描,所以主要的时间开销是排序的 O(n log n)。




  • 空间复杂度:O(log n),其中 n 为区间的数量。这里计算的是存储答案之外,使用的额外空间。O(log n) 即为排序所需要的空间复杂度。




2.C实现


留空,等变再牛B点再来手写快排加合并!


3.C++实现


class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.size() == 0) {
return {};
}
sort(intervals.begin(), intervals.end());
vector<vector<int>> merge;
for (int i = 0; i < intervals.size(); ++i) {
int L = intervals[i][0], R = intervals[i][1];
if (!merge.size() || merge.back()[1] < L) {
merge.push_back({L, R});
}
else {
merge.back()[1] = max(merge.back()[1], R);
}
}
return merge;
}
};

🔆In The End!


请添加图片描述








从现在做起,坚持下去,一天进步一小点,不久的将来,你会感谢曾经努力的你!

作者:孤寒者
链接:https://juejin.cn/post/7023000958921605127
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Swift 枚举

枚举简单的说也是一种数据类型,只不过是这种数据类型只包含自定义的特定数据,它是一组有共同特性的数据的集合。Swift 的枚举类似于 Objective C 和 C 的结构,枚举的功能为:它声明在类中,可以通过实例化类来访问它的值。枚举也可以定义构造函数(ini...
继续阅读 »

枚举简单的说也是一种数据类型,只不过是这种数据类型只包含自定义的特定数据,它是一组有共同特性的数据的集合。

Swift 的枚举类似于 Objective C 和 C 的结构,枚举的功能为:

  • 它声明在类中,可以通过实例化类来访问它的值。

  • 枚举也可以定义构造函数(initializers)来提供一个初始成员值;可以在原始的实现基础上扩展它们的功能。

  • 可以遵守协议(protocols)来提供标准的功能。

语法

Swift 中使用 enum 关键词来创建枚举并且把它们的整个定义放在一对大括号内:

enum enumname {
// 枚举定义放在这里
}

例如我们定义以下表示星期的枚举:

import Cocoa

// 定义枚举
enum DaysofaWeek {
case Sunday
case Monday
case TUESDAY
case WEDNESDAY
case THURSDAY
case FRIDAY
case Saturday
}

var weekDay = DaysofaWeek.THURSDAY
weekDay = .THURSDAY
switch weekDay
{
case .Sunday:
print("星期天")
case .Monday:
print("星期一")
case .TUESDAY:
print("星期二")
case .WEDNESDAY:
print("星期三")
case .THURSDAY:
print("星期四")
case .FRIDAY:
print("星期五")
case .Saturday:
print("星期六")
}

以上程序执行输出结果为:

星期四

枚举中定义的值(如 SundayMonday……Saturday)是这个枚举的成员值(或成员)。case关键词表示一行新的成员值将被定义。

注意: 和 C 和 Objective-C 不同,Swift 的枚举成员在被创建时不会被赋予一个默认的整型值。在上面的DaysofaWeek例子中,SundayMonday……Saturday不会隐式地赋值为01……6。相反,这些枚举成员本身就有完备的值,这些值是已经明确定义好的DaysofaWeek类型。

var weekDay = DaysofaWeek.THURSDAY 

weekDay的类型可以在它被DaysofaWeek的一个可能值初始化时推断出来。一旦weekDay被声明为一个DaysofaWeek,你可以使用一个缩写语法(.)将其设置为另一个DaysofaWeek的值:

var weekDay = .THURSDAY 

weekDay的类型已知时,再次为其赋值可以省略枚举名。使用显式类型的枚举值可以让代码具有更好的可读性。

枚举可分为相关值与原始值。

相关值与原始值的区别

相关值原始值
不同数据类型相同数据类型
实例: enum {10,0.8,"Hello"}实例: enum {10,35,50}
值的创建基于常量或变量预先填充的值
相关值是当你在创建一个基于枚举成员的新常量或变量时才会被设置,并且每次当你这么做得时候,它的值可以是不同的。原始值始终是相同的

相关值

以下实例中我们定义一个名为 Student 的枚举类型,它可以是 Name 的一个字符串(String),或者是 Mark 的一个相关值(Int,Int,Int)。

import Cocoa

enum Student{
case Name(String)
case Mark(Int,Int,Int)
}
var studDetails = Student.Name("Runoob")
var studMarks = Student.Mark(98,97,95)
switch studMarks {
case .Name(let studName):
print("学生的名字是: \(studName)。")
case .Mark(let Mark1, let Mark2, let Mark3):
print("学生的成绩是: \(Mark1),\(Mark2),\(Mark3)。")
}

以上程序执行输出结果为:

学生的成绩是: 98,97,95。

原始值

原始值可以是字符串,字符,或者任何整型值或浮点型值。每个原始值在它的枚举声明中必须是唯一的。

在原始值为整数的枚举时,不需要显式的为每一个成员赋值,Swift会自动为你赋值。

例如,当使用整数作为原始值时,隐式赋值的值依次递增1。如果第一个值没有被赋初值,将会被自动置为0。

import Cocoa

enum Month: Int {
case January = 1, February, March, April, May, June, July, August, September, October, November, December
}

let yearMonth = Month.May.rawValue
print("数字月份为: \(yearMonth)。")

以上程序执行输出结果为:

数字月份为: 5。
收起阅读 »

Swift 闭包

闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的 匿名函数比较相似。全局函数和嵌套函数其实就是特殊的闭包。闭包的形式有...
继续阅读 »

闭包(Closures)是自包含的功能代码块,可以在代码中使用或者用来作为参数传值。

Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的 匿名函数比较相似。

全局函数和嵌套函数其实就是特殊的闭包。

闭包的形式有:

全局函数嵌套函数闭包表达式
有名字但不能捕获任何值。有名字,也能捕获封闭函数内的值。无名闭包,使用轻量级语法,可以根据上下文环境捕获值。

Swift中的闭包有很多优化的地方:

  1. 根据上下文推断参数和返回值类型
  2. 从单行表达式闭包中隐式返回(也就是闭包体只有一行代码,可以省略return)
  3. 可以使用简化参数名,如$0, $1(从0开始,表示第i个参数...)
  4. 提供了尾随闭包语法(Trailing closure syntax)
  5. 语法

    以下定义了一个接收参数并返回指定类型的闭包语法:

    {(parameters) -> return type in
    statements
    }

    实例

    import Cocoa

    let studname = { print("Swift 闭包实例。") }
    studname
    ()

    以上程序执行输出结果为:

    Swift 闭包实例。

    以下闭包形式接收两个参数并返回布尔值:

    {(Int, Int) -> Bool in
    Statement1
    Statement 2
    ---
    Statement n
    }

    实例

    import Cocoa

    let divide = {(val1: Int, val2: Int) -> Int in
    return val1 / val2
    }
    let result = divide(200, 20)
    print (result)

    以上程序执行输出结果为:

    10

    闭包表达式

    闭包表达式是一种利用简洁语法构建内联闭包的方式。 闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。


    sorted 方法

    Swift 标准库提供了名为 sorted(by:) 的方法,会根据您提供的用于排序的闭包函数将已知类型数组中的值进行排序。

    排序完成后,sorted(by:) 方法会返回一个与原数组大小相同,包含同类型元素且元素已正确排序的新数组。原数组不会被 sorted(by:) 方法修改。

    sorted(by:)方法需要传入两个参数:

    • 已知类型的数组
    • 闭包函数,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回 true,反之返回 false

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    // 使用普通函数(或内嵌函数)提供排序功能,闭包函数类型需为(String, String) -> Bool。
    func backwards
    (s1: String, s2: String) -> Bool {
    return s1 > s2
    }
    var reversed = names.sorted(by: backwards)

    print(reversed)

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    如果第一个字符串 (s1) 大于第二个字符串 (s2),backwards函数返回true,表示在新的数组中s1应该出现在s2前。 对于字符串中的字符来说,"大于" 表示 "按照字母顺序较晚出现"。 这意味着字母"B"大于字母"A",字符串"S"大于字符串"D"。 其将进行字母逆序排序,"AT"将会排在"AE"之前。


    参数名称缩写

    Swift 自动为内联函数提供了参数名称缩写功能,您可以直接通过$0,$1,$2来顺序调用闭包的参数。

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    var reversed = names.sorted( by: { $0 > $1 } )
    print(reversed)

    $0和$1表示闭包中第一个和第二个String类型的参数。

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    如果你在闭包表达式中使用参数名称缩写, 您可以在闭包参数列表中省略对其定义, 并且对应参数名称缩写的类型会通过函数类型进行推断。in 关键字同样也可以被省略.


    运算符函数

    实际上还有一种更简短的方式来撰写上面例子中的闭包表达式。

    Swift 的String类型定义了关于大于号 (>) 的字符串实现,其作为一个函数接受两个String类型的参数并返回Bool类型的值。 而这正好与sort(_:)方法的第二个参数需要的函数类型相符合。 因此,您可以简单地传递一个大于号,Swift可以自动推断出您想使用大于号的字符串函数实现:

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    var reversed = names.sorted(by: >)
    print(reversed)

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    尾随闭包

    尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。

    func someFunctionThatTakesAClosure(closure: () -> Void) {
    // 函数体部分
    }

    // 以下是不使用尾随闭包进行函数调用
    someFunctionThatTakesAClosure
    ({
    // 闭包主体部分
    })

    // 以下是使用尾随闭包进行函数调用
    someFunctionThatTakesAClosure
    () {
    // 闭包主体部分
    }

    实例

    import Cocoa

    let names = ["AT", "AE", "D", "S", "BE"]

    //尾随闭包
    var reversed = names.sorted() { $0 > $1 }
    print(reversed)

    sort() 后的 { $0 > $1} 为尾随闭包。

    以上程序执行输出结果为:

    ["S", "D", "BE", "AT", "AE"]

    注意: 如果函数只需要闭包表达式一个参数,当您使用尾随闭包时,您甚至可以把()省略掉。

    reversed = names.sorted { $0 > $1 }

    捕获值

    闭包可以在其定义的上下文中捕获常量或变量。

    即使定义这些常量和变量的原域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。

    Swift最简单的闭包形式是嵌套函数,也就是定义在其他函数的函数体内的函数。

    嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。

    看这个例子:

    func makeIncrementor(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    一个函数makeIncrementor ,它有一个Int型的参数amout, 并且它有一个外部参数名字forIncremet,意味着你调用的时候,必须使用这个外部名字。返回值是一个()-> Int的函数。

    函数体内,声明了变量 runningTotal 和一个函数 incrementor。

    incrementor函数并没有获取任何参数,但是在函数体内访问了runningTotal和amount变量。这是因为其通过捕获在包含它的函数体内已经存在的runningTotal和amount变量而实现。

    由于没有修改amount变量,incrementor实际上捕获并存储了该变量的一个副本,而该副本随着incrementor一同被存储。

    所以我们调用这个函数时会累加:

    import Cocoa

    func makeIncrementor
    (forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    let incrementByTen = makeIncrementor(forIncrement: 10)

    // 返回的值为10
    print(incrementByTen())

    // 返回的值为20
    print(incrementByTen())

    // 返回的值为30
    print(incrementByTen())

    以上程序执行输出结果为:

    10
    20
    30

    闭包是引用类型

    上面的例子中,incrementByTen是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量值。

    这是因为函数和闭包都是引用类型。

    无论您将函数/闭包赋值给一个常量还是变量,您实际上都是将常量/变量的值设置为对应函数/闭包的引用。 上面的例子中,incrementByTen指向闭包的引用是一个常量,而并非闭包内容本身。

    这也意味着如果您将闭包赋值给了两个不同的常量/变量,两个值都会指向同一个闭包:

    import Cocoa

    func makeIncrementor
    (forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementor
    () -> Int {
    runningTotal
    += amount
    return runningTotal
    }
    return incrementor
    }

    let incrementByTen = makeIncrementor(forIncrement: 10)

    // 返回的值为10
    incrementByTen
    ()

    // 返回的值为20
    incrementByTen
    ()

    // 返回的值为30
    incrementByTen
    ()

    // 返回的值为40
    incrementByTen
    ()

    let alsoIncrementByTen = incrementByTen

    // 返回的值也为50
    print(alsoIncrementByTen())
收起阅读 »

Swift 函数

Swift 函数用来完成特定任务的独立的代码块。Swift使用一个统一的语法来表示简单的C语言风格的函数到复杂的Objective-C语言风格的方法。函数声明: 告诉编译器函数的名字,返回类型及参数。函数定义: 提供了函数的实体。Swift 函数包含了参数类型...
继续阅读 »

Swift 函数用来完成特定任务的独立的代码块。

Swift使用一个统一的语法来表示简单的C语言风格的函数到复杂的Objective-C语言风格的方法。

  • 函数声明: 告诉编译器函数的名字,返回类型及参数。

  • 函数定义: 提供了函数的实体。

Swift 函数包含了参数类型及返回值类型:


函数定义

Swift 定义函数使用关键字 func

定义函数的时候,可以指定一个或多个输入参数和一个返回值类型。

每个函数都有一个函数名来描述它的功能。通过函数名以及对应类型的参数值来调用这个函数。函数的参数传递的顺序必须与参数列表相同。

函数的实参传递的顺序必须与形参列表相同,-> 后定义函数的返回值类型。

语法

func funcname(形参) -> returntype
{
Statement1
Statement2
……
Statement N
return parameters
}

实例

以下我们定义了一个函数名为 runoob 的函数,形参的数据类型为 String,返回值也为 String:

import Cocoa

func runoob
(site: String) -> String {
return (site)
}
print(runoob(site: "www.runoob.com"))

以上程序执行输出结果为:

www.runoob.com

函数调用

我们可以通过函数名以及对应类型的参数值来调用函数,函数的参数传递的顺序必须与参数列表相同。

以下我们定义了一个函数名为 runoob 的函数,形参 site 的数据类型为 String,之后我们调用函数传递的实参也必须 String 类型,实参传入函数体后,将直接返回,返回的数据类型为 String。

import Cocoa

func runoob
(site: String) -> String {
return (site)
}
print(runoob(site: "www.runoob.com"))

以上程序执行输出结果为:

www.runoob.com

函数参数

函数可以接受一个或者多个参数,这些参数被包含在函数的括号之中,以逗号分隔。

以下实例向函数 runoob 传递站点名 name 和站点地址 site:

import Cocoa

func runoob
(name: String, site: String) -> String {
return name + site
}
print(runoob(name: "菜鸟教程:", site: "www.runoob.com"))
print(runoob(name: "Google:", site: "www.google.com"))

以上程序执行输出结果为:

菜鸟教程:www.runoob.com
Googlewww.google.com

不带参数函数

我们可以创建不带参数的函数。

语法:

func funcname() -> datatype {
return datatype
}

实例

import Cocoa

func sitename
() -> String {
return "菜鸟教程"
}
print(sitename())

以上程序执行输出结果为:

菜鸟教程

元组作为函数返回值

函数返回值类型可以是字符串,整型,浮点型等。

元组与数组类似,不同的是,元组中的元素可以是任意类型,使用的是圆括号。

你可以用元组(tuple)类型让多个值作为一个复合值从函数中返回。

下面的这个例子中,定义了一个名为minMax(_:)的函数,作用是在一个Int数组中找出最小值与最大值。

import Cocoa

func minMax
(array: [Int]) -> (min: Int, max: Int) {
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin
= value
} else if value > currentMax {
currentMax
= value
}
}
return (currentMin, currentMax)
}

let bounds = minMax(array: [8, -6, 2, 109, 3, 71])
print("最小值为 \(bounds.min) ,最大值为 \(bounds.max)")

minMax(_:)函数返回一个包含两个Int值的元组,这些值被标记为min和max,以便查询函数的返回值时可以通过名字访问它们。

以上程序执行输出结果为:

最小值为 -6 ,最大值为 109

如果你不确定返回的元组一定不为nil,那么你可以返回一个可选的元组类型。

你可以通过在元组类型的右括号后放置一个问号来定义一个可选元组,例如(Int, Int)?或(String, Int, Bool)?

注意
可选元组类型如(Int, Int)?与元组包含可选类型如(Int?, Int?)是不同的.可选的元组类型,整个元组是可选的,而不只是元组中的每个元素值。

前面的minMax(_:)函数返回了一个包含两个Int值的元组。但是函数不会对传入的数组执行任何安全检查,如果array参数是一个空数组,如上定义的minMax(_:)在试图访问array[0]时会触发一个运行时错误。

为了安全地处理这个"空数组"问题,将minMax(_:)函数改写为使用可选元组返回类型,并且当数组为空时返回nil

import Cocoa

func minMax
(array: [Int]) -> (min: Int, max: Int)? {
if array.isEmpty { return nil }
var currentMin = array[0]
var currentMax = array[0]
for value in array[1..<array.count] {
if value < currentMin {
currentMin
= value
} else if value > currentMax {
currentMax
= value
}
}
return (currentMin, currentMax)
}
if let bounds = minMax(array: [8, -6, 2, 109, 3, 71]) {
print("最小值为 \(bounds.min),最大值为 \(bounds.max)")
}

以上程序执行输出结果为:

最小值为 -6,最大值为 109

没有返回值函数

下面是 runoob(_:) 函数的另一个版本,这个函数接收菜鸟教程官网网址参数,没有指定返回值类型,并直接输出 String 值,而不是返回它:

import Cocoa

func runoob
(site: String) {
print("菜鸟教程官网:\(site)")
}
runoob
(site: "http://www.runoob.com")

以上程序执行输出结果为:

菜鸟教程官网:http://www.runoob.com

函数参数名称

函数参数都有一个外部参数名和一个局部参数名。

局部参数名

局部参数名在函数的实现内部使用。

func sample(number: Int) {
println
(number)
}

以上实例中 number 为局部参数名,只能在函数体内使用。

import Cocoa

func sample
(number: Int) {
print(number)
}
sample
(number: 1)
sample
(number: 2)
sample
(number: 3)

以上程序执行输出结果为:

1
2
3

外部参数名

你可以在局部参数名前指定外部参数名,中间以空格分隔,外部参数名用于在函数调用时传递给函数的参数。

如下你可以定义以下两个函数参数名并调用它:

import Cocoa

func pow
(firstArg a: Int, secondArg b: Int) -> Int {
var res = a
for _ in 1..<b {
res
= res * a
}
print(res)
return res
}
pow
(firstArg:5, secondArg:3)

以上程序执行输出结果为:

125

注意
如果你提供了外部参数名,那么函数在被调用时,必须使用外部参数名。


可变参数

可变参数可以接受零个或多个值。函数调用时,你可以用可变参数来指定函数参数,其数量是不确定的。

可变参数通过在变量类型名后面加入(...)的方式来定义。

import Cocoa

func vari
<N>(members: N...){
for i in members {
print(i)
}
}
vari
(members: 4,3,5)
vari
(members: 4.5, 3.1, 5.6)
vari
(members: "Google", "Baidu", "Runoob")

以上程序执行输出结果为:

4
3
5
4.5
3.1
5.6
Google
Baidu
Runoob

常量,变量及 I/O 参数

一般默认在函数中定义的参数都是常量参数,也就是这个参数你只可以查询使用,不能改变它的值。

如果想要声明一个变量参数,可以在参数定义前加 inout 关键字,这样就可以改变这个参数的值了。

例如:

func  getName(_ name: inout String).........

此时这个 name 值可以在函数中改变。

一般默认的参数传递都是传值调用的,而不是传引用。所以传入的参数在函数内改变,并不影响原来的那个参数。传入的只是这个参数的副本。

当传入的参数作为输入输出参数时,需要在参数名前加 & 符,表示这个值可以被函数修改。

实例

import Cocoa

func swapTwoInts
(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a
= b
b
= temporaryA
}


var x = 1
var y = 5
swapTwoInts
(&x, &y)
print("x 现在的值 \(x), y 现在的值 \(y)")

swapTwoInts(_:_:) 函数简单地交换 a 与 b 的值。该函数先将 a 的值存到一个临时常量 temporaryA 中,然后将 b 的值赋给 a,最后将 temporaryA 赋值给 b。

需要注意的是,someInt 和 anotherInt 在传入 swapTwoInts(_:_:) 函数前,都加了 & 的前缀。

以上程序执行输出结果为:

x 现在的值 5, y 现在的值 1

函数类型及使用

每个函数都有种特定的函数类型,由函数的参数类型和返回类型组成。

func inputs(no1: Int, no2: Int) -> Int {
return no1/no2
}

inputs 函数类型有两个 Int 型的参数(no1、no2)并返回一个 Int 型的值。

实例如下:

import Cocoa

func inputs
(no1: Int, no2: Int) -> Int {
return no1/no2
}
print(inputs(no1: 20, no2: 10))
print(inputs(no1: 36, no2: 6))

以上程序执行输出结果为:

2
6

以上函数定义了两个 Int 参数类型,返回值也为 Int 类型。

接下来我们看下如下函数,函数定义了参数为 String 类型,返回值为 String 类型。

func inputstr(name: String) -> String {
return name
}

函数也可以定义一个没有参数,也没有返回值的函数,如下所示:

import Cocoa

func inputstr
() {
print("菜鸟教程")
print("www.runoob.com")
}
inputstr
()

以上程序执行输出结果为:

菜鸟教程
www
.runoob.com

使用函数类型

在 Swift 中,使用函数类型就像使用其他类型一样。例如,你可以定义一个类型为函数的常量或变量,并将适当的函数赋值给它:

var addition: (Int, Int) -> Int = sum

解析:

"定义一个叫做 addition 的变量,参数与返回值类型均是 Int ,并让这个新变量指向 sum 函数"。

sum 和 addition 有同样的类型,所以以上操作是合法的。

现在,你可以用 addition 来调用被赋值的函数了:

import Cocoa

func sum
(a: Int, b: Int) -> Int {
return a + b
}
var addition: (Int, Int) -> Int = sum
print("输出结果: \(addition(40, 89))")

以上程序执行输出结果为:

输出结果: 129

函数类型作为参数类型、函数类型作为返回类型

我们可以将函数作为参数传递给另外一个参数:

import Cocoa

func sum
(a: Int, b: Int) -> Int {
return a + b
}
var addition: (Int, Int) -> Int = sum
print("输出结果: \(addition(40, 89))")

func another
(addition: (Int, Int) -> Int, a: Int, b: Int) {
print("输出结果: \(addition(a, b))")
}
another
(addition: sum, a: 10, b: 20)

以上程序执行输出结果为:

输出结果: 129
输出结果: 30

函数嵌套

函数嵌套指的是函数内定义一个新的函数,外部的函数可以调用函数内定义的函数。

实例如下:

import Cocoa

func calcDecrement
(forDecrement total: Int) -> () -> Int {
var overallDecrement = 0
func decrementer
() -> Int {
overallDecrement
-= total
return overallDecrement
}
return decrementer
}
let decrem = calcDecrement(forDecrement: 30)
print(decrem())

以上程序执行输出结果为:

-30
收起阅读 »

Swift 字典

Swift 字典用来存储无序的相同类型数据的集合,Swift 字典会强制检测元素的类型,如果类型不同则会报错。Swift 字典每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。和数组中的数据项不同,字典中的数据项并没有具体顺序。我...
继续阅读 »

Swift 字典用来存储无序的相同类型数据的集合,Swift 字典会强制检测元素的类型,如果类型不同则会报错。

Swift 字典每个值(value)都关联唯一的键(key),键作为字典中的这个值数据的标识符。

和数组中的数据项不同,字典中的数据项并没有具体顺序。我们在需要通过标识符(键)访问数据的时候使用字典,这种方法很大程度上和我们在现实世界中使用字典查字义的方法一样。

Swift 字典的key没有类型限制可以是整型或字符串,但必须是唯一的。

如果创建一个字典,并赋值给一个变量,则创建的字典就是可以修改的。这意味着在创建字典后,可以通过添加、删除、修改的方式改变字典里的项目。如果将一个字典赋值给常量,字典就不可修改,并且字典的大小和内容都不可以修改。


创建字典

我们可以使用以下语法来创建一个特定类型的空字典:

var someDict =  [KeyType: ValueType]()

以下是创建一个空字典,键的类型为 Int,值的类型为 String 的简单语法:

var someDict = [Int: String]()

以下为创建一个字典的实例:

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

访问字典

我们可以根据字典的索引来访问数组的元素,语法如下:

var someVar = someDict[key]

我们可以通过以下实例来学习如何创建,初始化,访问字典:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var someVar = someDict[1]

print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

修改字典

我们可以使用 updateValue(forKey:) 增加或更新字典的内容。如果 key 不存在,则添加值,如果存在则修改 key 对应的值。updateValue(_:forKey:)方法返回Optional值。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var oldVal = someDict.updateValue("One 新的值", forKey: 1)

var someVar = someDict[1]

print( "key = 1 旧的值 \(oldVal)" )
print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 旧的值 Optional("One")
key
= 1 的值为 Optional("One 新的值")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

你也可以通过指定的 key 来修改字典的值,如下所示:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var oldVal = someDict[1]
someDict
[1] = "One 新的值"
var someVar = someDict[1]

print( "key = 1 旧的值 \(oldVal)" )
print( "key = 1 的值为 \(someVar)" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 旧的值 Optional("One")
key
= 1 的值为 Optional("One 新的值")
key
= 2 的值为 Optional("Two")
key
= 3 的值为 Optional("Three")

移除 Key-Value 对

我们可以使用 removeValueForKey() 方法来移除字典 key-value 对。如果 key 存在该方法返回移除的值,如果不存在返回 nil 。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

var removedValue = someDict.removeValue(forKey: 2)

print( "key = 1 的值为 \(someDict[1])" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 nil
key
= 3 的值为 Optional("Three")

你也可以通过指定键的值为 nil 来移除 key-value(键-值)对。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

someDict
[2] = nil

print( "key = 1 的值为 \(someDict[1])" )
print( "key = 2 的值为 \(someDict[2])" )
print( "key = 3 的值为 \(someDict[3])" )

以上程序执行输出结果为:

key = 1 的值为 Optional("One")
key
= 2 的值为 nil
key
= 3 的值为 Optional("Three")

遍历字典

我们可以使用 for-in 循环来遍历某个字典中的键值对。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

for (key, value) in someDict {
print("字典 key \(key) - 字典 value \(value)")
}

以上程序执行输出结果为:

字典 key 2 -  字典 value Two
字典 key 3 - 字典 value Three
字典 key 1 - 字典 value One

我们也可以使用enumerate()方法来进行字典遍历,返回的是字典的索引及 (key, value) 对,实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

for (key, value) in someDict.enumerated() {
print("字典 key \(key) - 字典 (key, value) 对 \(value)")
}

以上程序执行输出结果为:

字典 key 0 -  字典 (key, value)  (2, "Two")
字典 key 1 - 字典 (key, value) (3, "Three")
字典 key 2 - 字典 (key, value) (1, "One")

字典转换为数组

你可以提取字典的键值(key-value)对,并转换为独立的数组。实例如下:

import Cocoa

var someDict:[Int:String] = [1:"One", 2:"Two", 3:"Three"]

let dictKeys = [Int](someDict.keys)
let dictValues = [String](someDict.values)

print("输出字典的键(key)")

for (key) in dictKeys {
print("\(key)")
}

print("输出字典的值(value)")

for (value) in dictValues {
print("\(value)")
}

以上程序执行输出结果为:

输出字典的键(key)
2
3
1
输出字典的值(value)
Two
Three
One

count 属性

我们可以使用只读的 count 属性来计算字典有多少个键值对:

import Cocoa

var someDict1:[Int:String] = [1:"One", 2:"Two", 3:"Three"]
var someDict2:[Int:String] = [4:"Four", 5:"Five"]

print("someDict1 含有 \(someDict1.count) 个键值对")
print("someDict2 含有 \(someDict2.count) 个键值对")

以上程序执行输出结果为:

someDict1 含有 3 个键值对
someDict2
含有 2 个键值对

isEmpty 属性

Y我们可以通过只读属性 isEmpty 来判断字典是否为空,返回布尔值:

import Cocoa

var someDict1:[Int:String] = [1:"One", 2:"Two", 3:"Three"]
var someDict2:[Int:String] = [4:"Four", 5:"Five"]
var someDict3:[Int:String] = [Int:String]()

print("someDict1 = \(someDict1.isEmpty)")
print("someDict2 = \(someDict2.isEmpty)")
print("someDict3 = \(someDict3.isEmpty)")

以上程序执行输出结果为:

someDict1 = false
someDict2
= false
someDict3
= true
收起阅读 »

使用 Kotlin Flow 优化你的网络请求框架,减少模板代码

一、以前封装的遗憾点 主要集中在如下2点上: Loading的处理 多余的LiveData 总而言之,就是需要写很多模板代码。 不必编写模版代码的一个最大好处就是: 写的代码越少,出错的概率越小. 1.1 Loading的处理 对于封装二,虽然...
继续阅读 »

一、以前封装的遗憾点


主要集中在如下2点上:




  • Loading的处理




  • 多余的LiveData




总而言之,就是需要写很多模板代码。



不必编写模版代码的一个最大好处就是: 写的代码越少,出错的概率越小.



1.1 Loading的处理


对于封装二,虽然解耦比封装一更彻底,但是关于Loading这里我觉得还是有遗憾。


试想一下:如果Activity中业务很多、逻辑复杂,存在很多个网络请求,在需要网络请求的地方都要手动去showLoading() ,然后在 observer() 中手动调用 stopLoading()


假如Activity中代码业务复杂,存在多个api接口,这样Activity中就存在很多个与loading有关的方法。


此外,如果一个网络请求的showLoading()方法和dismissLoading()方法相隔很远。会导致一个顺序流程的割裂。


请求开始前showLoading() ---> 请求网络 ---> 结束后stopLoading(),这是一个完整的流程,代码也应该尽量在一起,一目了然,不应该割裂存在。


如果代码量一多,以后维护起来,万一不小心删除了某个showLoading()或者stopLoading(),也容易导致问题。


还有就是每次都要手动调用这两个方法,麻烦。


1.2 重复的LiveData声明


个人认为常用的网络请求分为两大类:




  • 用完即丢




  • 需要监听数据变化




举个常见的例子,看下面这个页面:


image.png


用户一进入这个页面,绿色框里面内容基本不会变化,(不去纠结微信这个页面是不是webview之类的),这种ui其实是不需要设置一个LiveData去监听的,因为它几乎不会再更新了。


典型的还有:点击登录按钮,成功后就进去了下一个页面。


但是红色的框里面的ui不一样,需要实时刷新数据,也就用到LiveData监听,这种情况下观察者订阅者模式的好处才真正展示出来。并且从其他页面过来,LiveData也会把最新的数据自动更新。


对于用完即丢的网络请求,LoginViewModel会存在这种代码:


// LoginViewModel.kt
val loginLiveData = MutableLiveData<User?>()
val logoutLiveData = MutableLiveData<Any?>()
val forgetPasswordLiveData = MutableLiveData<User?>(

并且对应的Activity中也需要监听这3个LiveData。


这种模板代码让我写的很烦。


用了Flow优化后,完美的解决这2个痛点。



“Talk is cheap. Show me the code.”



二、集成Flow之后的用法


2.1 请求自带Loading&&不需要监听数据变化


需求:




  • 不需要监听数据变化,对应上面的用完即丢




  • 不需要在ViewModel中声明LiveData成员对象




  • 发起请求之前自动showLoading(),请求结束后自动stopLoading()




  • 类似于点击登录按钮,finish 当前页面,跳转到下一个页面




TestActivity 中示例代码:


// TestActivity.kt
private fun login() {
launchWithLoadingAndCollect({mViewModel.login("username", "password")}) {
onSuccess = { data->
showSuccessView(data)
}
onFailed = { errorCode, errorMsg ->
showFailedView(code, msg)
}
onError = {e ->
e.printStackTrace()
}
}
}

TestViewModel 中代码:


// TestViewModel中代码
suspend fun login(username: String, password: String): ApiResponse<User?> {
return repository.login(username, password)
}

2.2 请求不带Loading&&不需要声明LiveData


需求:




  • 不需要监听数据变化




  • 不需要在ViewModel中声明LiveData成员对象




  • 不需要Loading的展示




// TestActivity.kt
private fun getArticleDetail() {
launchAndCollect({ mViewModel.getArticleDetail() }) {
onSuccess = {
showSuccessView()
}
onFailed = { errorCode, errorMsg ->
showFailedView(code, msg)
}
onDataEmpty = {
showEmptyView()
}
}
}

TestViewModel 中代码和上面一样,这里就不写了。


是不是非常简单,一个方法搞定,将Loading的逻辑都隐藏了,再也不需要手动写 showLoading()stopLoading()


并且请求的结果直接在回调里面接收,直接处理,这样请求网络和结果的处理都在一起,看起来一目了然,再也不需要在 Activity 中到处找在哪监听的 LiveData


同样,它跟 LiveData 一样,也会监听 Activity 的生命周期,不会造成内存泄露。因为它是运行在ActivitylifecycleScope 协程作用域中的。


2.3 需要监听数据变化


需求:




  • 需要监听数据变化,要实时更新数据




  • 需要在 ViewModel 中声明 LiveData 成员对象




  • 例如实时获取最新的配置、最新的用户信息等




TestActivity 中示例代码:


// TestActivity.kt
class TestActivity : AppCompatActivity(R.layout.activity_api) {

private fun initObserver() {
mViewModel.wxArticleLiveData.observeState(this) {

onSuccess = { data: List<WxArticleBean>? ->
showSuccessView(data)
}

onDataEmpty = { showEmptyView() }

onFailed = { code, msg -> showFailedView(code, msg) }

onError = { showErrorView() }
}
}

private fun requestNet() {
// 需要Loading
launchWithLoading {
mViewModel.requestNet()
}
}
}

ViewModel 中示例代码:


class ApiViewModel : ViewModel() {

private val repository by lazy { WxArticleRepository() }

val wxArticleLiveData = StateMutableLiveData<List<WxArticleBean>>()

suspend fun requestNet() {
wxArticleLiveData.value = repository.fetchWxArticleFromNet()
}
}

本质上是通过FLow来调用LiveDatasetValue()方法,还是LiveData的使用。虽然可以完全用 Flow 来实现,但是我觉得这里用 Flow 的方式麻烦,不容易懂,还是怎么简单怎么来。


这种方式其实跟上篇文章中的封装二差不多,区别就是不需要手动调用Loading有关的方法。


三、拆封装


如果不抽取通用方法是这样写的:


// TestActivity.kt
private fun login() {
lifecycleScope.launch {
flow {
emit(mViewModel.login("username", "password"))
}.onStart {
showLoading()
}.onCompletion {
dismissLoading()
}.collect { response ->
when (response) {
is ApiSuccessResponse -> showSuccessView(response.data)
is ApiEmptyResponse -> showEmptyView()
is ApiFailedResponse -> showFailedView(response.errorCode, response.errorMsg)
is ApiErrorResponse -> showErrorView(response.error)
}
}
}
}

简单介绍下Flow


Flow类似于RxJava,操作符都跟Rxjava差不多,但是比Rxjava简单很多,kotlin通过flow来实现顺序流和链式编程。


flow关键字大括号里面的是方法的执行,结果通过emit发送给下游。


onStart表示最开始调用方法之前执行的操作,这里是展示一个 loading ui


onCompletion表示所有执行完成,不管有没有异常都会执行这个回调。


collect表示执行成功的结果回调,就是emit()方法发送的内容,flow必须执行collect才能有结果。因为是冷流,对应的还有热流。


更多的Flow知识点可以参考其他博客和官方文档。


这里可以看出,通过Flow完美的解决了loading的显示与隐藏。


我这里是在Activity中都调用flow的流程,这样我们扩展BaseActivity即可。


为什么扩展的是BaseActivity?


因为startLoading()stopLoading()BaseActivity中。😂


3.1 解决 flow 的 Loading 模板代码


fun <T> BaseActivity.launchWithLoadingGetFlow(block: suspend () -> ApiResponse<T>): Flow<ApiResponse<T>> {
return flow {
emit(block())
}.onStart {
showLoading()
}.onCompletion {
dismissLoading()
}
}

这样每次调用launchWithLoadingGetFlow方法,里面就实现了 Loading 的展示与隐藏,并且会返回一个 FLow 对象。


下一步就是处理 flow 结果collect里面的模板代码。


3.2 声明结果回调类


class ResultBuilder<T> {
var onSuccess: (data: T?) -> Unit = {}
var onDataEmpty: () -> Unit = {}
var onFailed: (errorCode: Int?, errorMsg: String?) -> Unit = { _, _ -> }
var onError: (e: Throwable) -> Unit = { e -> }
var onComplete: () -> Unit = {}
}

各种回调按照项目特性删减即可。


3.3 对ApiResponse对象进行解析


private fun <T> parseResultAndCallback(response: ApiResponse<T>, 
listenerBuilder: ResultBuilder<T>.() -> Unit) {
val listener = ResultBuilder<T>().also(listenerBuilder)
when (response) {
is ApiSuccessResponse -> listener.onSuccess(response.response)
is ApiEmptyResponse -> listener.onDataEmpty()
is ApiFailedResponse -> listener.onFailed(response.errorCode, response.errorMsg)
is ApiErrorResponse -> listener.onError(response.throwable)
}
listener.onComplete()
}

上篇文章这里的处理用的是继承LiveDataObserver,这里就不需要了,毕竟继承能少用就少用。


3.4 最终抽取方法


将上面的步骤连起来如下:


fun <T> BaseActivity.launchWithLoadingAndCollect(block: suspend () -> ApiResponse<T>, 
listenerBuilder: ResultBuilder<T>.() -> Unit) {
lifecycleScope.launch {
launchWithLoadingGetFlow(block).collect { response ->
parseResultAndCallback(response, listenerBuilder)
}
}
}

3.5 将Flow转换成LiveData对象


获取到的是Flow对象,如果想要变成LiveDataFlow原生就支持将Flow对象转换成不可变的LiveData对象。


val loginFlow: Flow<ApiResponse<User?>> =
launchAndGetFlow(requestBlock = { mViewModel.login("UserName", "Password") })
val loginLiveData: LiveData<ApiResponse<User?>> = loginFlow.asLiveData()

调用的是 Flow 的asLiveData()方法,原理也很简单,就是用了livedata的扩展函数:


@JvmOverloads
fun <T> Flow<T>.asLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
collect {
emit(it)
}
}

这里返回的是LiveData<ApiResponse<User?>>对象,如果想要跟上篇文章一样用StateLiveData,在observe的回调里面监听不同状态的callback


以前的方式是继承,有如下缺点:



  • 必须要用StateLiveData,不能用原生的LiveData,侵入性很强

  • 不只是继承LiveData,还要继承Observer,麻烦

  • 为了实现这个,写了一堆的代码


这里用 Kotlin 扩展实现,直接扩展 LiveData


@MainThread
inline fun <T> LiveData<ApiResponse<T>>.observeState(
owner: LifecycleOwner,
listenerBuilder: ResultBuilder<T>.() -> Unit
) {
val listener = ResultBuilder<T>().also(listenerBuilder)
observe(owner) { apiResponse ->
when (apiResponse) {
is ApiSuccessResponse -> listener.onSuccess(apiResponse.response)
is ApiEmptyResponse -> listener.onDataEmpty()
is ApiFailedResponse -> listener.onFailed(apiResponse.errorCode, apiResponse.errorMsg)
is ApiErrorResponse -> listener.onError(apiResponse.throwable)
}
listener.onComplete()
}
}

感谢Flywith24开源库提供的思路,感觉自己有时候还是在用Java的思路在写Kotlin。


3.6 进一步完善


很多网络请求的相关并不是只有 loading 状态,还需要在请求前和结束后处理一些特定的逻辑。


这里的方式是:直接在封装方法的参数加 callback,默认用是 loading 的实现。


fun <T> BaseActivity.launchAndCollect(
requestBlock: suspend () -> ApiResponse<T>,
startCallback: () -> Unit = { showLoading() },
completeCallback: () -> Unit = { dismissLoading() },
listenerBuilder: ResultBuilder<T>.() -> Unit
)

四、针对多数据来源


虽然项目中大部分都是单一数据来源,但是也偶尔会出现多数据来源,多数据源结合Flow的操作符,也非常的方便。


示例


假如同一份数据可以从数据库获取,可以从网络请求获取,TestRepository的代码如下:


// TestRepository.kt
suspend fun fetchDataFromNet(): Flow<ApiResponse<List<WxArticleBean>>> {
val response = executeHttp { mService.getWxArticle() }
return flow { emit(response) }.flowOn(Dispatchers.IO)
}

suspend fun fetchDataFromDb(): Flow<ApiResponse<List<WxArticleBean>>> {
val response = getDataFromRoom()
return flow { emit(response) }.flowOn(Dispatchers.IO)


Repository中的返回不再直接返回实体类,而是返回flow包裹的实体类对象。


为什么要这么做?


为了用神奇的flow操作符来处理。


flow组合操作符



  • combine、combineTransform


combine操作符可以连接两个不同的Flow。



  • merge


merge操作符用于将多个流合并。



  • zip


zip操作符会分别从两个流中取值,当一个流中的数据取完,zip过程就完成了。


关于 Flow 的基础操作符,徐医生大神的这篇文章已经写的很棒了,这里就不多余的写了。


根据操作符的示例可以看出,就算返回的不是同一个对象,也可以用操作符进行处理。


几年前刚开始学RxJava时,好几次都是入门到放弃,操作符太多了,搞的也很懵逼,Flow 真的比它简单太多了。


五、flow的奇淫技巧


flowWithLifecycle


需求:
Activity 的 onSume() 方法中请求最新的地理位置信息。


以前的写法:


// TestActivity.kt
override fun onResume() {
super.onResume()
getLastLocation()
}

override fun onDestory() {
super.onDestory()
// 释放获取定位的代码,防止内存泄露
}

这种写法没问题,也很正常,但是用了 Flow 之后,有一种新的写法。


用了 flow 的写法:


// TestActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
getLastLocation()
}

@ExperimentalCoroutinesApi
@SuppressLint("MissingPermission")
private fun getLastLocation() {
if (LocationPermissionUtils.isLocationProviderEnabled() && LocationPermissionUtils.isLocationPermissionGranted()) {
lifecycleScope.launch {
SharedLocationManager(lifecycleScope)
.locationFlow()
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
.collect { location ->
Log.i(TAG, "最新的位置是:$location")
}
}
}
}

onCreate中书写该函数,然后 flow 的链式调用中加入:


.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)


flowWithLifecycle能监听 Activity 的生命周期,在 Activity 的onResume开始请求位置信息,onStop 时自动停止,不会导致内存泄露。



flowWithLifecycle 会在生命周期进入和离开目标状态时发送项目和取消内部的生产者。



这个api需要引入 androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-rc01依赖库。


callbackFlow


有没有发现5.1中调用获取位置信息的代码很简单?


SharedLocationManager(lifecycleScope)
.locationFlow()
.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
.collect { location ->
Log.i(TAG, "最新的位置是:$location")
}

几行代码解决获取位置信息,并且任何地方都直接调用,不要写一堆代码。


这里就是用到callbackFlow,简而言之,callbackFlow就是将callback回调代码变成同步的方式来写。


这里直接上SharedLocationManager的代码,具体细节自行 Google,因为这就不是网络框架的内容。


这里附上主要的代码:


@ExperimentalCoroutinesApi
@SuppressLint("MissingPermission")
private val _locationUpdates: SharedFlow<Location> = callbackFlow<Location> {
val callback = object : LocationCallback() {
override fun onLocationResult(result: LocationResult?) {
result ?: return
Log.d(TAG, "New location: ${result.lastLocation}")
trySend(result.lastLocation)
}

}
Log.d(TAG, "Starting location updates")

fusedLocationClient.requestLocationUpdates(
locationRequest,callback,Looper.getMainLooper())
.addOnFailureListener { e ->close(e)}

awaitClose {
Log.d(TAG, "Stopping location updates")
fusedLocationClient.removeLocationUpdates(callback)
}
}.shareIn(
externalScope,
replay = 0,
started = SharingStarted.WhileSubscribed()
)

完整代码见:GitHub


总结


上一篇文章# 两种方式封装Retrofit+协程,实现优雅快速的网络请求


加上这篇的 flow 网络请求封装,一共是三种对Retrofit+协程的网络封装方式。


对比下三种封装方式:




  • 封装一 (对应分支oneWay) 传递ui引用,可按照项目进行深度ui定制,方便快速,但是耦合高




  • 封装二 (对应分支master) 耦合低,依赖的东西很少,但是写起来模板代码偏多




  • 封装三 (对应分支dev) 引入了新的flow流式编程(虽然出来很久,但是大部分人应该还没用到),链式调用,loading 和网络请求以及结果处理都在一起,很多时候甚至都不要声明 LiveData 对象。




第二种封装我在公司的商业项目App中用了很长时间了,涉及几十个接口,暂时没遇到什么问题。


第三种是我最近才折腾出来的,在公司的新项目中(还没上线)使用,也暂时没遇到什么问题。


如果某位大神看到这篇文章,有不同意见,或者发现封装三有漏洞,欢迎指出,不甚感谢!


项目地址


FastJetpack


项目持续更新...


作者:零先生
链接:https://juejin.cn/post/7022823222928211975
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

MVVM 进阶版:MVI 架构了解一下~

MVI
前言 Android开发发展到今天已经相当成熟了,各种架构大家也都耳熟能详,如MVC,MVP,MVVM等,其中MVVM更是被官方推荐,成为Android开发中的显学。 不过软件开发中没有银弹,MVVM架构也不是尽善尽美的,在使用过程中也会有一些不太方便之处,而...
继续阅读 »

前言


Android开发发展到今天已经相当成熟了,各种架构大家也都耳熟能详,如MVC,MVP,MVVM等,其中MVVM更是被官方推荐,成为Android开发中的显学。

不过软件开发中没有银弹,MVVM架构也不是尽善尽美的,在使用过程中也会有一些不太方便之处,而MVI可以很好的解决一部分MVVM的痛点。

本文主要包括以下内容



  1. MVC,MVP,MVVM等经典架构介绍

  2. MVI架构到底是什么?

  3. MVI架构实战



需要重点指出的是,标题中说MVI架构是MVVM的进阶版是指MVIMVVM非常相似,并在其基础上做了一定的改良,并不是说MVI架构一定比MVVM适合你的项目

各位同学可以在分析比较各个架构后,选择合适项目场景的架构



经典架构介绍


MVC架构介绍


MVC是个古老的Android开发架构,随着MVPMVVM的流行已经逐渐退出历史舞台,我们在这里做一个简单的介绍,其架构图如下所示:



MVC架构主要分为以下几部分



  1. 视图层(View):对应于xml布局文件和java代码动态view部分

  2. 控制层(Controller):主要负责业务逻辑,在android中由Activity承担,同时因为XML视图功能太弱,所以Activity既要负责视图的显示又要加入控制逻辑,承担的功能过多。

  3. 模型层(Model):主要负责网络请求,数据库处理,I/O的操作,即页面的数据来源


由于androidxml布局的功能性太弱,Activity实际上负责了View层与Controller层两者的工作,所以在androidmvc更像是这种形式:



因此MVC架构在android平台上的主要存在以下问题:



  1. Activity同时负责ViewController层的工作,违背了单一职责原则

  2. Model层与View层存在耦合,存在互相依赖,违背了最小知识原则


MVP架构介绍


由于MVC架构在Android平台上的一些缺陷,MVP也就应运而生了,其架构图如下所示



MVP架构主要分为以下几个部分



  1. View层:对应于ActivityXML,只负责显示UI,只与Presenter层交互,与Model层没有耦合

  2. Presenter层: 主要负责处理业务逻辑,通过接口回调View

  3. Model层:主要负责网络请求,数据库处理等操作,这个没有什么变化


我们可以看到,MVP解决了MVC的两个问题,即Activity承担了两层职责与View层与Model层耦合的问题


MVP架构同样有自己的问题



  1. Presenter层通过接口与View通信,实际上持有了View的引用

  2. 但是随着业务逻辑的增加,一个页面可能会非常复杂,这样就会造成View的接口会很庞大。


MVVM架构介绍


MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向数据绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然

MVVM架构图如下所示:



可以看出MVVMMVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP


MVVM的双向数据绑定主要通过DataBinding实现,不过相信有很多人跟我一样,是不喜欢用DataBinding的,这样架构就变成了下面这样



  1. View观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定,所以其实MVVM的这一大特性我其实并没有用到

  2. View通过调用ViewModel提供的方法来与ViewMdoel交互


小结



  1. MVC架构的主要问题在于Activity承担了ViewController两层的职责,同时View层与Model层存在耦合

  2. MVP引入Presenter层解决了MVC架构的两个问题,View只能与Presenter层交互,业务逻辑放在Presenter

  3. MVP的问题在于随着业务逻辑的增加,View的接口会很庞大,MVVM架构通过双向数据绑定可以解决这个问题

  4. MVVMMVP的主要区别在于,你不用去主动去刷新UI了,只要Model数据变了,会自动反映到UI上。换句话说,MVVM更像是自动化的MVP

  5. MVVM的双向数据绑定主要通过DataBinding实现,但有很多人(比如我)不喜欢用DataBinding,而是View通过LiveData等观察ViewModle的数据变化并自我更新,这其实是单一数据源而不是双向数据绑定


MVI架构到底是什么?


MVVM架构有什么不足?


要了解MVI架构,我们首先来了解下MVVM架构有什么不足

相信使用MVVM架构的同学都有如下经验,为了保证数据流的单向流动,LiveData向外暴露时需要转化成immutable的,这需要添加不少模板代码并且容易遗忘,如下所示


class TestViewModel : ViewModel() {
//为保证对外暴露的LiveData不可变,增加一个状态就要添加两个LiveData变量
private val _pageState: MutableLiveData<PageState> = MutableLiveData()
val pageState: LiveData<PageState> = _pageState
private val _state1: MutableLiveData<String> = MutableLiveData()
val state1: LiveData<String> = _state1
private val _state2: MutableLiveData<String> = MutableLiveData()
val state2: LiveData<String> = _state2
//...
}

如上所示,如果页面逻辑比较复杂,ViewModel中将会有许多全局变量的LiveData,并且每个LiveData都必须定义两遍,一个可变的,一个不可变的。这其实就是我通过MVVM架构写比较复杂页面时最难受的点。

其次就是View层通过调用ViewModel层的方法来交互的,View层与ViewModel的交互比较分散,不成体系


小结一下,在我的使用中,MVVM架构主要有以下不足



  1. 为保证对外暴露的LiveData是不可变的,需要添加不少模板代码并且容易遗忘

  2. View层与ViewModel层的交互比较分散零乱,不成体系


MVI架构是什么?


MVIMVVM 很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,架构图如下所示



其主要分为以下几部分



  1. Model: 与MVVM中的Model不同的是,MVIModel主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态

  2. View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Intent的变化实现界面刷新(注意:这里不是ActivityIntent

  3. Intent: 此Intent不是ActivityIntent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求


单向数据流


MVI强调数据的单向流动,主要分为以下几步:



  1. 用户操作以Intent的形式通知Model

  2. Model基于Intent更新State

  3. View接收到State变化刷新UI。


数据永远在一个环形结构中单向流动,不能反向流动:


上面简单的介绍了下MVI架构,下面我们一起来看下具体是怎么使用MVI架构的


MVI架构实战


总体架构图




我们使用ViewModel来承载MVIModel层,总体结构也与MVVM类似,主要区别在于ModelView层交互的部分



  1. Model层承载UI状态,并暴露出ViewStateView订阅,ViewState是个data class,包含所有页面状态

  2. View层通过Action更新ViewState,替代MVVM通过调用ViewModel方法交互的方式


MVI实例介绍


添加ViewStateViewEvent


ViewState承载页面的所有状态,ViewEvent则是一次性事件,如Toast等,如下所示


data class MainViewState(val fetchStatus: FetchStatus, val newsList: List<NewsItem>)  

sealed class MainViewEvent {
data class ShowSnackbar(val message: String) : MainViewEvent()
data class ShowToast(val message: String) : MainViewEvent()
}


  1. 我们这里ViewState只定义了两个,一个是请求状态,一个是页面数据

  2. ViewEvent也很简单,一个简单的密封类,显示ToastSnackbar


ViewState更新


class MainViewModel : ViewModel() {
private val _viewStates: MutableLiveData<MainViewState> = MutableLiveData()
val viewStates = _viewStates.asLiveData()
private val _viewEvents: SingleLiveEvent<MainViewEvent> = SingleLiveEvent()
val viewEvents = _viewEvents.asLiveData()

init {
emit(MainViewState(fetchStatus = FetchStatus.NotFetched, newsList = emptyList()))
}

private fun fabClicked() {
count++
emit(MainViewEvent.ShowToast(message = "Fab clicked count $count"))
}

private fun emit(state: MainViewState?) {
_viewStates.value = state
}

private fun emit(event: MainViewEvent?) {
_viewEvents.value = event
}
}

如上所示



  1. 我们只需定义ViewStateViewEvent两个State,后续增加状态时在data class中添加即可,不需要再写模板代码

  2. ViewEvents是一次性的,通过SingleLiveEvent实现,当然你也可以用Channel当来实现

  3. 当状态更新时,通过emit来更新状态


View监听ViewState


    private fun initViewModel() {
viewModel.viewStates.observe(this) {
renderViewState(it)
}
viewModel.viewEvents.observe(this) {
renderViewEvent(it)
}
}

如上所示,MVI 使用 ViewStateState 集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码。


View通过Action更新State


class MainActivity : AppCompatActivity() {
private fun initView() {
fabStar.setOnClickListener {
viewModel.dispatch(MainViewAction.FabClicked)
}
}
}
class MainViewModel : ViewModel() {
fun dispatch(action: MainViewAction) =
reduce(viewStates.value, action)

private fun reduce(state: MainViewState?, viewAction: MainViewAction) {
when (viewAction) {
is MainViewAction.NewsItemClicked -> newsItemClicked(viewAction.newsItem)
MainViewAction.FabClicked -> fabClicked()
MainViewAction.OnSwipeRefresh -> fetchNews(state)
MainViewAction.FetchNews -> fetchNews(state)
}
}
}

如上所示,View通过ActionViewModel交互,通过 Action 通信,有利于 ViewViewModel 之间的进一步解耦,同时所有调用以 Action 的形式汇总到一处,也有利于对行为的集中分析和监控


总结


本文主要介绍了MVC,MVP,MVVMMVI架构,目前MVVM是官方推荐的架构,但仍然有以下几个痛点



  1. MVVMMVP的主要区别在于双向数据绑定,但由于很多人(比如我)并不喜欢使用DataBindg,其实并没有使用MVVM双向绑定的特性,而是单一数据源

  2. 当页面复杂时,需要定义很多State,并且需要定义可变与不可变两种,状态会以双倍的速度膨胀,模板代码较多且容易遗忘

  3. ViewViewModel通过ViewModel暴露的方法交互,比较零乱难以维护


MVI可以比较好的解决以上痛点,它主要有以下优势



  1. 强调数据单向流动,很容易对状态变化进行跟踪和回溯

  2. 使用ViewStateState集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码

  3. ViewModel通过ViewStateAction通信,通过浏览ViewStateAciton 定义就可以理清 ViewModel 的职责,可以直接拿来作为接口文档使用。


当然MVI也有一些缺点,比如



  1. 所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀

  2. state是不变的,因此每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销


软件开发中没有银弹,所有架构都不是完美的,有自己的适用场景,读者可根据自己的需求选择使用。

但通过以上的分析与介绍,我相信使用MVI架构代替没有使用DataBindingMVVM是一个比较好的选择~


Sample代码


github.com/shenzhen201…


作者:RicardoMJiang
链接:https://juejin.cn/post/7022624191723601928
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android IPC 之 Messenger

绑定服务(Bound Services)概述 绑定服务是client-server接口中的服务器。它允许组件(例如活动)绑定到服务、发送请求、接收响应和执行进程间通信(IPC)。 绑定服务通常仅在它为另一个应用程序组件提供服务时才存在,并且不会无限期地在后台运...
继续阅读 »

绑定服务(Bound Services)概述


绑定服务是client-server接口中的服务器。它允许组件(例如活动)绑定到服务、发送请求、接收响应和执行进程间通信(IPC)。 绑定服务通常仅在它为另一个应用程序组件提供服务时才存在,并且不会无限期地在后台运行


💥 基础知识


绑定服务是 Service 类的实现,它允许其他应用程序绑定到它并与之交互。 要为服务提供绑定,你必须实现 onBind() 回调方法。 此方法返回一个 IBinder 对象,该对象定义了客户端可用于与服务交互的编程接口。


🔥 Messenger


💥 概述


一提到IPC 很多人的反应都是 AIDL,其实如果仅仅是多进程单线程,那么你可以使用 Messenger 为你的服务提供接口。


使用 Messenger 比使用 AIDL 更简单,因为 Messenger 会将所有对服务的调用排入队列


对于大多数应用程序,该服务不需要执行多线程,因此使用 Messenger 允许该服务一次处理一个调用。如果你的 服务多线程很重要,那你就要用到ALDL了。


💥 使用 Messenger 步骤




  • 1、该 Service 实现了一个 Handler,该 Handler 接收来自客户端的每次调用的回调。




  • 2、该服务使用 Handler 创建一个 Messenger 对象(它是对 Handler 的引用)。




  • 3、Messenger 创建一个 IBinder,该服务从 onBind() 返回给客户端。




  • 4、客户端使用 IBinder 来实例化 Messenger(引用服务的Handler),客户端使用 Handler 来向服务发送 Message 对象。




  • 5、服务在其 Handler 的 handleMessage() 中接收每个消息。




💥 实例(Client到Server数据传递)


🌀 MessengerService.java


public class MessengerService extends Service {
public static final int MSG_SAY_HELLO = 0;
//让客户端向IncomingHandler发送消息。
Messenger messenger = null;

//当绑定到服务时,我们向我们的Messenger返回一个接口,用于向服务发送消息。
public IBinder onBind(Intent intent) {
MLog.e("MessengerService:onBind");
//创建 Messenger 对象(对 Handler 的引用)
messenger = new Messenger(new IncomingHander(this));
//返回支持此Messenger的IBinder。
return messenger.getBinder();
}
//实现了一个 Handler
static class IncomingHander extends Handler {
private Context appliacationContext;
public IncomingHander(Context context) {
appliacationContext = context.getApplicationContext();
}

@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MSG_SAY_HELLO:
Bundle bundle = msg.getData();
String string = bundle.getString("name");
//处理来自客户端的消息
MLog.e("handleMessage:来自Acitvity的"+string);
break;
case 1:

break;
default:
super.handleMessage(msg);
}
}
}
}

🌀 AndroidMainfest.xml


        <service android:name=".ipc.MessengerService"
android:process="com.scc.ipc.messengerservice"
android:exported="true"
android:enabled="true"/>

使用 android:process 属性 创建不同进程。


🌀 MainActivity.class


public class MainActivity extends ActivityBase implements View.OnClickListener {
Messenger mService = null;
Messenger messenger = null;
private boolean bound;
private ViewStub v_stud;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
}

ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//从原始 IBinder 创建一个 Messenger,该 IBinder 之前已使用 getBinder 检索到。
mService = new Messenger(service);
bound = true;
}

@Override
public void onServiceDisconnected(ComponentName name) {
bound = false;
}
};

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_bind_service:
bindService(new Intent(MainActivity.this, MessengerService.class), connection, Context.BIND_AUTO_CREATE);
break;
case R.id.btn_send_msg:
Message message = Message.obtain(null, MessengerService.MSG_SAY_HELLO);
Bundle bundle = new Bundle();
bundle.putString("name","Scc");
message.setData(bundle);
try {
mService.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
break;

}
}
@Override
protected void onStop() {
super.onStop();
if (bound) {
unbindService(connection);
bound = false;
}
}
}

🌀 运行效果如下



两个进程也存在着,也完成了进程间的通信,并把数据传递过去了。


💥 实例(Server将数据传回Client)


我不仅想将消息传递给 Server ,还想让 Server 将数据处理后传会Client。


🌀 MessengerService.java


public class MessengerService extends Service {
/** 用于显示和隐藏我们的通知。 */
ArrayList<Messenger> mClients = new ArrayList<Messenger>();
/** 保存客户端设置的最后一个值。 */
int mValue = 0;

/**
* 数组中添加 Messenger (来自客户端)。
* Message 的 replyTo 字段必须是应该发送回调的客户端的 Messenger。
*/
public static final int MSG_REGISTER_CLIENT = 1;

/**
* 数组中删除 Messenger (来自客户端)。
* Message 的 replyTo 字段必须是之前用 MSG_REGISTER_CLIENT 给出的客户端的 Messenger。
*/
public static final int MSG_UNREGISTER_CLIENT = 2;
/**
* 用于设置新值。
* 这可以发送到服务以提供新值,并将由服务发送给具有新值的任何注册客户端。
*/
public static final int MSG_SET_VALUE = 3;
//让客户端向IncomingHandler发送消息。
Messenger messenger = null;

//当绑定到服务时,我们向我们的Messenger返回一个接口,用于向服务发送消息。
public IBinder onBind(Intent intent) {
MLog.e("MessengerService-onBind");
//创建 Messenger 对象(对 Handler 的引用)
messenger = new Messenger(new IncomingHander(this));
//返回支持此Messenger的IBinder。
return messenger.getBinder();
}
//实现了一个 Handler
class IncomingHander extends Handler {
private Context appliacationContext;
public IncomingHander(Context context) {
appliacationContext = context.getApplicationContext();
}

@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MSG_REGISTER_CLIENT:
mClients.add(msg.replyTo);
break;
case MSG_UNREGISTER_CLIENT:
mClients.remove(msg.replyTo);
break;
case MSG_SET_VALUE:
mValue = msg.arg1;
for (int i=mClients.size()-1; i>=0; i--) {
try {
mClients.get(i).send(Message.obtain(null,
MSG_SET_VALUE, mValue, 0));
} catch (RemoteException e) {
// 客户端没了。 从列表中删除它;
//从后往前安全,从前往后遍历数组越界。
mClients.remove(i);
}
}
default:
super.handleMessage(msg);
}
}
}
}

🌀 MainActivity.java


public class MainActivity extends ActivityBase implements View.OnClickListener {
Messenger mService = null;
Messenger messenger = null;
private boolean bound;
private ViewStub v_stud;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
}

ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//从原始 IBinder 创建一个 Messenger,该 IBinder 之前已使用 getBinder 检索到。
mService = new Messenger(service);
bound = true;
}

@Override
public void onServiceDisconnected(ComponentName name) {
bound = false;
}
};
static class ReturnHander extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MessengerService.MSG_SET_VALUE:
//我要起飞:此处处理
MLog.e("Received from service: " + msg.arg1);
break;
default:
super.handleMessage(msg);
}
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_bind_service:
bindService(new Intent(MainActivity.this, MessengerService.class), connection, Context.BIND_AUTO_CREATE);
break;
case R.id.btn_send_msg:
try {
mMessenger = new Messenger(new ReturnHander());
Message msg = Message.obtain(null,
MessengerService.MSG_REGISTER_CLIENT);
msg.replyTo = mMessenger;
//先发一则消息添加Messenger:msg.replyTo = mMessenger;
mService.send(msg);

// Give it some value as an example.
msg = Message.obtain(null,
MessengerService.MSG_SET_VALUE, this.hashCode(), 0);
//传入的arg1值:this.hashCode()
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
break;

}
}
@Override
protected void onStop() {
super.onStop();
if (bound) {
unbindService(connection);
bound = false;
}
}
}

🌀 运行效果如下



我们在MainActivity 的 Handler.sendMessger()中接收到了来自 MesengerService 的消息 。


本次 Messenger 进程间通信齐活,这只是个简单的Demo。最后咱们看一波源码。


🔥 Messenger 源码


Messenger.java


public final class Messenger implements Parcelable {
private final IMessenger mTarget;
public Messenger(Handler target) {
mTarget = target.getIMessenger();
}
public void send(Message message) throws RemoteException {
mTarget.send(message);
}
public IBinder getBinder() {
return mTarget.asBinder();
}
...
public Messenger(IBinder target) {
mTarget = IMessenger.Stub.asInterface(target);
}
}

然后你会发现 只要代码还是在 IMessenger 里面,咱们去找找。


IMessenger.aidl


package android.os;

import android.os.Message;

/** @hide */
oneway interface IMessenger {
void send(in Message msg);
}

new Messenger(Handler handelr)


这里其实是用Handler 调用 getIMessenger() 。咱们去Handler.class里面转转。


    @UnsupportedAppUsage
final IMessenger getIMessenger() {
synchronized (mQueue) {
if (mMessenger != null) {
return mMessenger;
}
mMessenger = new MessengerImpl();
return mMessenger;
}
}
//创建了Messenger实现类
private final class MessengerImpl extends IMessenger.Stub {
public void send(Message msg) {
msg.sendingUid = Binder.getCallingUid();
//Messenger调用send()方法,通过Handler发送消息。
//然后在服务端通过Handler的handleMessge(msg)接收这个消息。
Handler.this.sendMessage(msg);
}
}

new Messenger(IBinder target)


package android.os;
/** @hide */
public interface IMessenger extends android.os.IInterface
{
/** Default implementation for IMessenger. */
public static class Default implements android.os.IMessenger
{
@Override public void send(android.os.Message msg) throws android.os.RemoteException
{
}
@Override
public android.os.IBinder asBinder() {
return null;
}
}
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements android.os.IMessenger
{
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an android.os.IMessenger interface,
* generating a proxy if needed.
*/
public static android.os.IMessenger asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
//判断是否在同一进程。
if (((iin!=null)&&(iin instanceof android.os.IMessenger))) {
//同一进程
return ((android.os.IMessenger)iin);
}
//代理对象
return new android.os.IMessenger.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
...
}
public void send(android.os.Message msg) throws android.os.RemoteException;
}

看了上面代码你会发现这不就是个aidl吗? 什么是aidl,咱们下一篇继续讲到。


作者:Android帅次
链接:https://juejin.cn/post/7022881022438015007
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

一天高中的女同桌突然问我是不是程序猿

背景 昨天一个我高中的女同桌突然发微信问我“你是不是程序猿 我有问题求助”, 先是激动后是茫然再是冷静,毕业多年不见联系,突然发个信息求助,感觉大脑有点反应不过来... 再说我一个搞Android的也不咋会python啊(不是说Java不能实现,大家懂的,人...
继续阅读 »

背景


昨天一个我高中的女同桌突然发微信问我“你是不是程序猿 我有问题求助”,


image-20211015101843733.png


先是激动后是茫然再是冷静,毕业多年不见联系,突然发个信息求助,感觉大脑有点反应不过来... 再说我一个搞Android的也不咋会python啊(不是说Java不能实现,大家懂的,人生苦短,我用python),即使如此,
为了大家的面子,为了程序猿们的脸,不就简单的小Python嘛,必须答应!


梳理需求


现有excel表格记录着 有效图片的名字,如:


image-20211015103418631.png


要从一个文件夹里把excel表格里记录名字的图片筛选出来;


需求也不是很难,代码思路就有了:



  1. 读取Excel表格第一列的信息并放入A集合

  2. 遍历文件夹下所有的文件,判断文件名字是否存在A集合

  3. 存在A集合则拷贝到目标文件夹


实现(Python 2.7)


读取Excel表格

加载Excel表格的方法有很多种,例如pandasxlrdopenpyxl,我这里选择openpyxl库,
先安装库



pip install openpyxl



代码如下:


from openpyxl import load_workbook

def handler_excel(filename=r'C:/Users/xxx/Desktop/haha.xlsx'):
   # 根据文件路径加载一个excel表格,这里包含所有的sheet
   excel = load_workbook(filename)
   # 根据sheet名称加载对应的table
   table = excel.get_sheet_by_name('Sheet1')
   imgnames = []
   # 读取所有列
   for column in table.columns:
       for cell in column:
           imgnames.append(cell.value+".png")
# 选择图片
   pickImg(imgnames)

遍历文件夹读取文件名,找到target并拷贝

使用os.listdir 方法遍历文件,这里注意windows环境下拿到的unicode编码,需要GBK重新解码


def pickImg(pickImageNames):
   # 遍历所有图片集的文件名
   for image in os.listdir(
           r"C:\Users\xxx\Desktop\work\img"):
       # 使用gbk解码,不然中文乱码
       u_file = image.decode('gbk')
       print(u_file)
       if u_file in pickImageNames:
           oldname = r"C:\Users\xxx\Desktop\work\img/" + image
           newname = r"C:\Users\xxx\Desktop\work\target/" + image
           # 文件拷贝
           shutil.copyfile(oldname, newname)

简单搞定!没有砸程序猿的招牌,豪横的把成果发给女同桌,结果:


image-20211015112550343.png


换来有机会请你吃饭,微信都不带回的,哎 ,xdm,小丑竟是我自己!
小丑竟是我自己什么梗-小丑竟是我自己是什么意思出自什么-55手游网


作者:李诺曹
链接:https://juejin.cn/post/7019167108185456677
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

美团面试官问我一个字符的String.length()是多少,我说是1,面试官说你回去好好学一下吧

public class testT { public static void main(String [] args){ String A = "hi你是乔戈里"; System.out.println(A.lengt...
继续阅读 »


public class testT {
public static void main(String [] args){
String A = "hi你是乔戈里";
System.out.println(A.length());
}
}复制代码


以上结果输出为7。







小萌边说边在IDEA中的win环境下选中String.length()函数,使用ctrl+B快捷键进入到String.length()的定义。


    /**
* Returns the length of this string.
* The length is equal to the number of <a href="Character.html#unicode">Unicode
* code units</a> in the string.
*
* @return the length of the sequence of characters represented by this
* object.
*/
public int length() {
return value.length;
}复制代码


接着使用google翻译对这段英文进行了翻译,得到了大体意思:返回字符串的长度,这一长度等于字符串中的 Unicode 代码单元的数目。


小萌:乔戈里,那这又是啥意思呢?乔哥:前几天我写的一篇文章:面试官问你编码相关的面试题,把这篇甩给他就完事!)里面对于Java的字符使用的编码有介绍:


Java中 有内码和外码这一区分简单来说



  • 内码:char或String在内存里使用的编码方式。
  • 外码:除了内码都可以认为是“外码”。(包括class文件的编码)



而java内码:unicode(utf-16)中使用的是utf-16.所以上面的那句话再进一步解释就是:返回字符串的长度,这一长度等于字符串中的UTF-16的代码单元的数目。




代码单元指一种转换格式(UTF)中最小的一个分隔,称为一个代码单元(Code Unit),因此,一种转换格式只会包含整数个单元。UTF-X 中的数字 X 就是各自代码单元的位数。


UTF-16 的 16 指的就是最小为 16 位一个单元,也即两字节为一个单元,UTF-16 可以包含一个单元和两个单元,对应即是两个字节和四个字节。我们操作 UTF-16 时就是以它的一个单元为基本单位的。


你还记得你前几天被面试官说菜的时候学到的Unicode知识吗,在面试官让我讲讲Unicode,我讲了3秒说没了,面试官说你可真菜这里面提到,UTF-16编码一个字符对于U+0000-U+FFFF范围内的字符采用2字节进行编码,而对于字符的码点大于U+FFFF的字符采用四字节进行编码,前者是两字节也就是一个代码单元,后者一个字符是四字节也就是两个代码单元!


而上面我的例子中的那个字符的Unicode值就是“U+1D11E”,这个Unicode的值明显大于U+FFFF,所以对于这个字符UTF-16需要使用四个字节进行编码,也就是使用两个代码单元!


所以你才看到我的上面那个示例结果表示一个字符的String.length()长度是2!



来看个例子!


public class testStringLength {
public static void main(String [] args){
String B = "𝄞"; // 这个就是那个音符字符,只不过由于当前的网页没支持这种编码,所以没显示。
String C = "\uD834\uDD1E";// 这个就是音符字符的UTF-16编码
System.out.println(C);
System.out.println(B.length());
System.out.println(B.codePointCount(0,B.length()));
// 想获取这个Java文件自己进行演示的,可以在我的公众号【程序员乔戈里】后台回复 6666 获取
}
}复制代码



可以看到通过codePointCount()函数得知这个音乐字符是一个字符!




几个问题:0.codePointCount是什么意思呢?1.之前不是说音符字符是“U+1D11E”,为什么UTF-16是"uD834uDD1E",这俩之间如何转换?2.前面说了UTF-16的代码单元,UTF-32和UTF-8的代码单元是多少呢?



一个一个解答:


第0个问题:


codePointCount其实就是代码点数的意思,也就是一个字符就对应一个代码点数。


比如刚才音符字符(没办法打出来),它的代码点是U+1D11E,但它的代理单元是U+D834和U+DD1E,如果令字符串str = "u1D11E",机器识别的不是音符字符,而是一个代码点”/u1D11“和字符”E“,所以会得到它的代码点数是2,代码单元数也是2。


但如果令字符str = "uD834uDD1E",那么机器会识别它是2个代码单元代理,但是是1个代码点(那个音符字符),故而,length的结果是代码单元数量2,而codePointCount()的结果是代码点数量1.


第1个问题




上图是对应的转换规则:



  • 首先 U+1D11E-U+10000 = U+0D11E
  • 接着将U+0D11E转换为二进制:0000 1101 0001 0001 1110,前10位是0000 1101 00 后10位是01 0001 1110
  • 接着套用模板:110110yyyyyyyyyy 110111xxxxxxxxxx
  • U+0D11E的二进制依次从左到右填入进模板:110110 0000 1101 00 110111 01 0001 1110
  • 然后将得到的二进制转换为16进制:d834dd1e,也就是你看到的utf-16编码了



第2个问题




  • 同理,UTF-32 以 32 位一个单元,它只包含这一种单元就够了,它的一单元自然也就是四字节了。
  • UTF-8 的 8 指的就是最小为 8 位一个单元,也即一字节为一个单元,UTF-8 可以包含一个单元,二个单元,三个单元及四个单元,对应即是一,二,三及四字节。






作者:程序员乔戈里
链接:https://juejin.cn/post/6844904036873814023
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

领导:谁再用定时任务实现关闭订单,立马滚蛋!

在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢? 一般的做法有如下几种定时任务关闭订单rocketmq延迟...
继续阅读 »

在电商、支付等领域,往往会有这样的场景,用户下单后放弃支付了,那这笔订单会在指定的时间段后进行关闭操作,细心的你一定发现了像某宝、某东都有这样的逻辑,而且时间很准确,误差在1s内;那他们是怎么实现的呢?


一般的做法有如下几种

定时任务关闭订单

rocketmq延迟队列

rabbitmq死信队列

时间轮算法

redis过期监听


一、定时任务关闭订单(最low)


一般情况下,最不推荐的方式就是关单方式就是定时任务方式,原因我们可以看下面的图来说明


image.png


我们假设,关单时间为下单后10分钟,定时任务间隔也是10分钟;通过上图我们看出,如果在第1分钟下单,在第20分钟的时候才能被扫描到执行关单操作,这样误差达到10分钟,这在很多场景下是不可接受的,另外需要频繁扫描主订单号造成网络IO和磁盘IO的消耗,对实时交易造成一定的冲击,所以PASS


二、rocketmq延迟队列方式


延迟消息
生产者把消息发送到消息服务器后,并不希望被立即消费,而是等待指定时间后才可以被消费者消费,这类消息通常被称为延迟消息。
在RocketMQ开源版本中,支持延迟消息,但是不支持任意时间精度的延迟消息,只支持特定级别的延迟消息。
消息延迟级别分别为1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h,共18个级别。


发送延迟消息(生产者)


/**
* 推送延迟消息
*
@param topic
*
@param body
*
@param producerGroup
*
@return boolean
*/

public boolean sendMessage(String topic, String body, String producerGroup)
{
try
{
Message recordMsg = new Message(topic, body.getBytes());
producer.setProducerGroup(producerGroup);

//设置消息延迟级别,我这里设置14,对应就是延时10分钟
// "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h"
recordMsg.setDelayTimeLevel(14);
// 发送消息到一个Broker
SendResult sendResult = producer.send(recordMsg);
// 通过sendResult返回消息是否成功送达
log.info("发送延迟消息结果:======sendResult:{}", sendResult);
DateFormat format =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("发送时间:{}", format.format(new Date()));

return true;
}
catch (Exception e)
{
e.printStackTrace();
log.error("延迟消息队列推送消息异常:{},推送内容:{}", e.getMessage(), body);
}
return false;
}

消费延迟消息(消费者)


/**
* 接收延迟消息
*
* @param topic
* @param consumerGroup
* @param messageHandler
*/

public void messageListener(String topic, String consumerGroup, MessageListenerConcurrently messageHandler)
{
ThreadPoolUtil.execute(() ->
{
try
{
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
consumer.setConsumerGroup(consumerGroup);
consumer.setVipChannelEnabled(false);
consumer.setNamesrvAddr(address);
//设置消费者拉取消息的策略,*表示消费该topic下的所有消息,也可以指定tag进行消息过滤
consumer.subscribe(topic, "*");
//消费者端启动消息监听,一旦生产者发送消息被监听到,就打印消息,和rabbitmq中的handlerDelivery类似
consumer.registerMessageListener(messageHandler);
consumer.start();
log.info("启动延迟消息队列监听成功:" + topic);
}
catch (MQClientException e)
{
log.error("启动延迟消息队列监听失败:{}", e.getErrorMessage());
System.exit(1);
}
});
}

实现监听类,处理具体逻辑


/**
* 延迟消息监听
*
*/

@Component
public class CourseOrderTimeoutListener implements ApplicationListener
{

@Resource
private MQUtil mqUtil;

@Resource
private CourseOrderTimeoutHandler courseOrderTimeoutHandler;

@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent)
{
// 订单超时监听
mqUtil.messageListener(EnumTopic.ORDER_TIMEOUT, EnumGroup.ORDER_TIMEOUT_GROUP, courseOrderTimeoutHandler);
}
}

/**
* 实现监听
*/

@Slf4j
@Component
public class CourseOrderTimeoutHandler implements MessageListenerConcurrently
{

@Override
public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt msg : list)
{
// 得到消息体
String body = new String(msg.getBody());
JSONObject userJson = JSONObject.parseObject(body);
TCourseBuy courseBuyDetails = JSON.toJavaObject(userJson, TCourseBuy.class);

// 处理具体的业务逻辑,,,,,

DateFormat format =new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.info("消费时间:{}", format.format(new Date()));

return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}

这种方式相比定时任务好了很多,但是有一个致命的缺点,就是延迟等级只有18种(商业版本支持自定义时间),如果我们想把关闭订单时间设置在15分钟该如何处理呢?显然不够灵活。


三、rabbitmq死信队列的方式


Rabbitmq本身是没有延迟队列的,只能通过Rabbitmq本身队列的特性来实现,想要Rabbitmq实现延迟队列,需要使用Rabbitmq的死信交换机(Exchange)和消息的存活时间TTL(Time To Live)


死信交换机
一个消息在满足如下条件下,会进死信交换机,记住这里是交换机而不是队列,一个交换机可以对应很多队列。


一个消息被Consumer拒收了,并且reject方法的参数里requeue是false。也就是说不会被再次放在队列里,被其他消费者使用。
上面的消息的TTL到了,消息过期了。


队列的长度限制满了。排在前面的消息会被丢弃或者扔到死信路由上。
死信交换机就是普通的交换机,只是因为我们把过期的消息扔进去,所以叫死信交换机,并不是说死信交换机是某种特定的交换机


消息TTL(消息存活时间)
消息的TTL就是消息的存活时间。RabbitMQ可以对队列和消息分别设置TTL。对队列设置就是队列没有消费者连着的保留时间,也可以对每一个单独的消息做单独的设置。超过了这个时间,我们认为这个消息就死了,称之为死信。如果队列设置了,消息也设置了,那么会取值较小的。所以一个消息如果被路由到不同的队列中,这个消息死亡的时间有可能不一样(不同的队列设置)。这里单讲单个消息的TTL,因为它才是实现延迟任务的关键。


byte[] messageBodyBytes = "Hello, world!".getBytes();  
AMQP.BasicProperties properties = new AMQP.BasicProperties();
properties.setExpiration("60000");
channel.basicPublish("my-exchange", "queue-key", properties, messageBodyBytes);

可以通过设置消息的expiration字段或者x-message-ttl属性来设置时间,两者是一样的效果。只是expiration字段是字符串参数,所以要写个int类型的字符串:当上面的消息扔到队列中后,过了60秒,如果没有被消费,它就死了。不会被消费者消费到。这个消息后面的,没有“死掉”的消息对顶上来,被消费者消费。死信在队列中并不会被删除和释放,它会被统计到队列的消息数中去


处理流程图


image.png


创建交换机(Exchanges)和队列(Queues)


创建死信交换机


image.png


如图所示,就是创建一个普通的交换机,这里为了方便区分,把交换机的名字取为:delay


创建自动过期消息队列
这个队列的主要作用是让消息定时过期的,比如我们需要2小时候关闭订单,我们就需要把消息放进这个队列里面,把消息过期时间设置为2小时


image.png


创建一个一个名为delay_queue1的自动过期的队列,当然图片上面的参数并不会让消息自动过期,因为我们并没有设置x-message-ttl参数,如果整个队列的消息有消息都是相同的,可以设置,这里为了灵活,所以并没有设置,另外两个参数x-dead-letter-exchange代表消息过期后,消息要进入的交换机,这里配置的是delay,也就是死信交换机,x-dead-letter-routing-key是配置消息过期后,进入死信交换机的routing-key,跟发送消息的routing-key一个道理,根据这个key将消息放入不同的队列


创建消息处理队列
这个队列才是真正处理消息的队列,所有进入这个队列的消息都会被处理


image.png


消息队列的名字为delay_queue2
消息队列绑定到交换机
进入交换机详情页面,将创建的2个队列(delayqueue1和delayqueue2)绑定到交换机上面


image.png
自动过期消息队列的routing key 设置为delay
绑定delayqueue2


image.png


delayqueue2 的key要设置为创建自动过期的队列的x-dead-letter-routing-key参数,这样当消息过期的时候就可以自动把消息放入delay_queue2这个队列中了
绑定后的管理页面如下图:


image.png


当然这个绑定也可以使用代码来实现,只是为了直观表现,所以本文使用的管理平台来操作
发送消息


String msg = "hello word";  
MessageProperties messageProperties = newMessageProperties();
messageProperties.setExpiration("6000");
messageProperties.setCorrelationId(UUID.randomUUID().toString().getBytes());
Message message = newMessage(msg.getBytes(), messageProperties);
rabbitTemplate.convertAndSend("delay", "delay",message);

设置了让消息6秒后过期
注意:因为要让消息自动过期,所以一定不能设置delay_queue1的监听,不能让这个队列里面的消息被接受到,否则消息一旦被消费,就不存在过期了


接收消息
接收消息配置好delay_queue2的监听就好了


package wang.raye.rabbitmq.demo1;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
publicclassDelayQueue{
/** 消息交换机的名字*/
publicstaticfinalString EXCHANGE = "delay";
/** 队列key1*/
publicstaticfinalString ROUTINGKEY1 = "delay";
/** 队列key2*/
publicstaticfinalString ROUTINGKEY2 = "delay_key";
/**
* 配置链接信息
* @return
*/

@Bean
publicConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = newCachingConnectionFactory("120.76.237.8",5672);
connectionFactory.setUsername("kberp");
connectionFactory.setPassword("kberp");
connectionFactory.setVirtualHost("/");
connectionFactory.setPublisherConfirms(true); // 必须要设置
return connectionFactory;
}
/**
* 配置消息交换机
* 针对消费者配置
FanoutExchange: 将消息分发到所有的绑定队列,无routingkey的概念
HeadersExchange :通过添加属性key-value匹配
DirectExchange:按照routingkey分发到指定队列
TopicExchange:多关键字匹配
*/

@Bean
publicDirectExchange defaultExchange() {
returnnewDirectExchange(EXCHANGE, true, false);
}
/**
* 配置消息队列2
* 针对消费者配置
* @return
*/

@Bean
publicQueue queue() {
returnnewQueue("delay_queue2", true); //队列持久
}
/**
* 将消息队列2与交换机绑定
* 针对消费者配置
* @return
*/

@Bean
@Autowired
publicBinding binding() {
returnBindingBuilder.bind(queue()).to(defaultExchange()).with(DelayQueue.ROUTINGKEY2);
}
/**
* 接受消息的监听,这个监听会接受消息队列1的消息
* 针对消费者配置
* @return
*/

@Bean
@Autowired
publicSimpleMessageListenerContainer messageContainer2(ConnectionFactory connectionFactory) {
SimpleMessageListenerContainer container = newSimpleMessageListenerContainer(connectionFactory());
container.setQueues(queue());
container.setExposeListenerChannel(true);
container.setMaxConcurrentConsumers(1);
container.setConcurrentConsumers(1);
container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //设置确认模式手工确认
container.setMessageListener(newChannelAwareMessageListener() {
publicvoid onMessage(Message message, com.rabbitmq.client.Channel channel) throwsException{
byte[] body = message.getBody();
System.out.println("delay_queue2 收到消息 : "+ newString(body));
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); //确认消息成功消费
}
});
return container;
}
}

这种方式可以自定义进入死信队列的时间;是不是很完美,但是有的小伙伴的情况是消息中间件就是rocketmq,公司也不可能会用商业版,怎么办?那就进入下一节


四、时间轮算法


image.png


(1)创建环形队列,例如可以创建一个包含3600个slot的环形队列(本质是个数组)


(2)任务集合,环上每一个slot是一个Set
同时,启动一个timer,这个timer每隔1s,在上述环形队列中移动一格,有一个Current Index指针来标识正在检测的slot。


Task结构中有两个很重要的属性:
(1)Cycle-Num:当Current Index第几圈扫描到这个Slot时,执行任务
(2)订单号,要关闭的订单号(也可以是其他信息,比如:是一个基于某个订单号的任务)


假设当前Current Index指向第0格,例如在3610秒之后,有一个订单需要关闭,只需:
(1)计算这个订单应该放在哪一个slot,当我们计算的时候现在指向1,3610秒之后,应该是第10格,所以这个Task应该放在第10个slot的Set中
(2)计算这个Task的Cycle-Num,由于环形队列是3600格(每秒移动一格,正好1小时),这个任务是3610秒后执行,所以应该绕3610/3600=1圈之后再执行,于是Cycle-Num=1


Current Index不停的移动,每秒移动到一个新slot,这个slot中对应的Set,每个Task看Cycle-Num是不是0:
(1)如果不是0,说明还需要多移动几圈,将Cycle-Num减1
(2)如果是0,说明马上要执行这个关单Task了,取出订单号执行关单(可以用单独的线程来执行Task),并把这个订单信息从Set中删除即可。
(1)无需再轮询全部订单,效率高
(2)一个订单,任务只执行一次
(3)时效性好,精确到秒(控制timer移动频率可以控制精度)


五、redis过期监听


1.修改redis.windows.conf配置文件中notify-keyspace-events的值
默认配置notify-keyspace-events的值为 ""
修改为 notify-keyspace-events Ex 这样便开启了过期事件


2. 创建配置类RedisListenerConfig(配置RedisMessageListenerContainer这个Bean)


package com.zjt.shop.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;


@Configuration
public class RedisListenerConfig {

@Autowired
private RedisTemplate redisTemplate;

/**
*
@return
*/

@Bean
public RedisTemplate redisTemplateInit() {

// key序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());

//val实例化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

return redisTemplate;
}

@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}

}

3.继承KeyExpirationEventMessageListener创建redis过期事件的监听类


package com.zjt.shop.common.util;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.zjt.shop.modules.order.service.OrderInfoService;
import com.zjt.shop.modules.product.entity.OrderInfoEntity;
import com.zjt.shop.modules.product.mapper.OrderInfoMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.stereotype.Component;


@Slf4j
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}

@Autowired
private OrderInfoMapper orderInfoMapper;

/**
* 针对redis数据失效事件,进行数据处理
*
@param message
*
@param pattern
*/

@Override
public void onMessage(Message message, byte[] pattern) {
try {
String key = message.toString();
//从失效key中筛选代表订单失效的key
if (key != null && key.startsWith("order_")) {
//截取订单号,查询订单,如果是未支付状态则为-取消订单
String orderNo = key.substring(6);
QueryWrapper queryWrapper = new QueryWrapper<>();
queryWrapper.eq("order_no",orderNo);
OrderInfoEntity orderInfo = orderInfoMapper.selectOne(queryWrapper);
if (orderInfo != null) {
if (orderInfo.getOrderState() == 0) { //待支付
orderInfo.setOrderState(4); //已取消
orderInfoMapper.updateById(orderInfo);
log.info("订单号为【" + orderNo + "】超时未支付-自动修改为已取消状态");
}
}
}
} catch (Exception e) {
e.printStackTrace();
log.error("【修改支付订单过期状态异常】:" + e.getMessage());
}
}
}

4:测试
通过redis客户端存一个有效时间为3s的订单:


image.png


结果:


image.png


总结:
以上方法只是个人对于关单的一些想法,可能有些地方有疏漏,请在公众号直接留言进行指出,当然如果你有更好的关单方式也可以随时沟通交流


作者:程序员阿牛
链接:https://juejin.cn/post/6987233263660040206
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

搜索历史记录的实现-Android

前言最近一个客户想要实现搜索中搜索历史的功能,其实这个功能听起来很简单,实际上里面有很多逻辑在里面,一开始写的时候脑子蒙蒙的,最后提给客户的时候一堆毛病,这一次来详细梳理一下,也分享一下我的思路主要逻辑搜索后保存当前内容将最新的搜索记录在最前面搜索历史记录可以...
继续阅读 »

前言

最近一个客户想要实现搜索中搜索历史的功能,其实这个功能听起来很简单,实际上里面有很多逻辑在里面,一开始写的时候脑子蒙蒙的,最后提给客户的时候一堆毛病,这一次来详细梳理一下,也分享一下我的思路

主要逻辑

  1. 搜索后保存当前内容
  2. 将最新的搜索记录在最前面
  3. 搜索历史记录可以点击并执行搜索功能,并将其提到最前面

我里面使用了ObjectBox作为数据存储,因为实际项目用的Java所以没用Room,而且Room好像第一次搜索至少要200ms,不过可以在某个activity随便搜索热启动一下.GreenDao使用有点麻烦,查询条件没有什么太大需求,直接用ObjectBox了,而且使用超级简单

Code

ObjectBox的工具类

public class ObjectBoxUtils {
public static BoxStore init() {
BoxStore boxStore = null;
try {
boxStore = MyApplication.getBoxStore();
if (boxStore == null) {
boxStore = MyObjectBox.builder().androidContext(MyApplication.applicationContext).build();
MyApplication.setBoxStore(boxStore);
}
} catch (Exception e) {
}
return boxStore;
}


public static <T> List<T> getAllData(Class clazz) {
try {
BoxStore boxStore = init();
if (boxStore != null && !boxStore.isClosed()) {
Box<T> box = boxStore.boxFor(clazz);

return box.getAll();
}
} catch (Exception e) {
}
return new ArrayList<>();
}


/**
* 添加数据
*/
public static <T> long addData(T o, Class c) {
try {
BoxStore boxStore = init();
if (boxStore != null && !boxStore.isClosed()) {
return boxStore.boxFor(c).put(o);
}
} catch (Throwable e) {
}
return 0;
}


public static HistoryBean getHistroyBean(String name) {
try {
BoxStore boxStore = init();
if (boxStore != null && !boxStore.isClosed()) {
Box<HistoryBean> box = boxStore.boxFor(HistoryBean.class);
HistoryBean first = box.query().equal(HistoryBean_.name, name).build().findFirst();
return first;
}
} catch (Exception e) {
}
return null;
}
}

其实我在Application就初始化了ObjectBox,但是实际项目中有时候会初始化失败,导致直接空指针,所有每次调用我都会判断一下是否初始化了,没有的话就进行相应操作

Activity

class HistoryActivity : AppCompatActivity() {
private var list: MutableList<HistoryBean>? = null
private var inflate: ActivityHistoryBinding? = null
private var historyAdapter: HistoryAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
inflate = ActivityHistoryBinding.inflate(layoutInflater)
setContentView(inflate?.root)
list = ObjectBoxUtils.getAllData(HistoryBean::class.java)
list?.sort()
inflate!!.rv.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
historyAdapter = HistoryAdapter(this, list)
inflate!!.rv.adapter = historyAdapter


inflate!!.et.setOnEditorActionListener(object : TextView.OnEditorActionListener {
override fun onEditorAction(v: TextView?, actionId: Int, event: KeyEvent?): Boolean {
saveHistory(inflate!!.et.text.toString())
return true
}

})
}

/**
* 保存搜索历史
*
*/
fun saveHistory(keyWord: String) {
//查询本地是否有name为参数中的数据
var histroyBean: HistoryBean? = ObjectBoxUtils.getHistroyBean(keyWord)
val currentTimeMillis = System.currentTimeMillis()
//没有就新创建一个
if (histroyBean == null) {
histroyBean = HistoryBean(currentTimeMillis, keyWord, currentTimeMillis)
} else {
//有的话就更新时间,也就说明了两种情况,第一 重复搜索了,搜索肯定要排重嘛,第二就是我们点击历史记录了,因此更新下时间
histroyBean.setTime(currentTimeMillis)
}
//把新/旧数据保存到本地
ObjectBoxUtils.addData(histroyBean, HistoryBean::class.java)
//每一次操作都从数据库拿取数据,性能消耗很低,就这么一个小模块没必要上纲上线
list?.clear()
list?.addAll(ObjectBoxUtils.getAllData(HistoryBean::class.java))
//实体Bean重写了Comparable,排序一下
list?.sort()
historyAdapter?.notifyDataSetChanged()
}
}

相应注释都在代码里,说实话kotlin用的好难受啊,还是自己语法学的不行,一个小东西卡我好久,导致我Application里面直接删除用Java重写了

实体类

@Entity
public class HistoryBean implements Comparable<HistoryBean> {
@Id(assignable = true)
public long id;

public HistoryBean(long id, String name, long time) {
this.id = id;
this.name = name;
this.time = time;
}

public String name;

public long time;

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public long getTime() {
return time;
}

public void setTime(long time) {
this.time = time;
}


@Override
public int compareTo(HistoryBean o) {
return (int) (o.time-time);
}
}

实体类重写了CompareTo,因为集合的sort实际上也是调用了ComparteTo,我们直接重写相应逻辑就简化业务层很多代码

效果

历史记录.gif

嗯,效果还不错,继续学习令人脑壳痛的自定义View去了


收起阅读 »

动态代理的使用-功能增强

背景接手某项目时碰到切换主线程的逻辑, 原项目代码流程如下:xxPresenter 会创建observer直接用于二方库的 SDKService (通常在子线程中回调),记为 innerObserverxxActivit...
继续阅读 »

背景

接手某项目时碰到切换主线程的逻辑, 原项目代码流程如下:

切换主线程时序图.png

  1. xxPresenter 会创建observer直接用于二方库的 SDKService (通常在子线程中回调),记为 innerObserver
  2. xxActivity 也需要创建observer用于主线程回调, 记为 uiObserver
  3. xxPresenter 在收到 innerObserver 的回调后通过主线程handler进行线程切换, 最终触发 uiObserver 的对应方法
  4. 业务需求回调方法都在 xxActivity 主线程中执行后续操作, innerObserver 几乎仅用于线程切换而已

存在的问题

  1. 如图第2/3步, 对于同一类型的observer, 需要在 activity , presenter中各实现一次, presenter中会产生大量模板代码
  2. 如图第6步, 收到 SDKService 回调后, presenter需要构建Message, 设置各回调实参, 这完全依赖开发人员手动配置, 效率低下且易发生错误, 灵活度低
  3. 对应的, 第11步通过handler线程切换时, 又需要从 message 中依次还原各实参, 这一步同样依赖开发人员手动处理
  4. observer变化时(如形参列表顺序/类型发生变更), 均需要同步更新 prenter 和 handler
  5. 我司项目最多时, 某个SDKService有将近100个observer需要设置, 部分observer的方法数甚至超过45个, 导致单纯在 Presenter 中创建observer的空白匿名内部类时, 代码就超过100行, 模板代码过多
  6. ...

改造思路

根据已知条件:

  1. 各observer均为接口 interface 类型
  2. presenter 中实现的 innerObserver 仅用于进行线程切换,最终触发UI层创建的observer而已 --> 即:有统一的功能增强逻辑

自然联想到 代理模式 中的动态代理:

代理模式-图侵删,来源于C语言中文网

  1. 创建一个 ThreadSwitcher 辅助类, 可根据传入的 observer 的类型Class,自动生成动态代理类对象,即之前的 innerObserver, 然后作用于sdk中 --> 此步骤可节省prsetner中因 new observer(){} 产生的大量模板代码, 且在observer接口发生变更时, 也不需要修改代码,自动完成适配, 伪代码如下:
    Observer innerOb = ThreadSwitcher.generateInnerObserver(Observer.class)

  2. ThreadSwitcher 类同时透出接口供UI层传入用于主线程的observer, 缓存在 Map<Class,IObserver> 中, 供后续切换主线程时使用

  3. 当下层sdk回调动态代理对象时, 最终都会触发 InvocationHandler#invoke 方法, 其方法签名如下, 我们只需要在其方法体中构造runnable, 按需post到主线程中即可:

// package java.lang.reflect.InvocationHandler.java
/**
* @param method 接口中被触发的回调方法
* @param args 方法实参列表
*/

public Object invoke(Object proxy, Method method, Object[] args);
  1. 构造的runnable时, 需查找UI层注入的observer,并触发对应的方法, 而由于 InvocationHandler中已告知我们方法 method 及其实参 args , 因此可直接通过 method.invoke(uiObserver,args) 来触发 uiObserver 的对应方法, 具体代码见下一节

动态代理的使用

import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method
import java.lang.reflect.Proxy

object ThreadSwitcher {
// ui层注入的observer, 会在主线程中回调
val uiObserverMap = mutableMapOf<Class<*>, Any>()
val targetHandler: Handler = Handler(Looper.mainLooper())

private fun runOnUIThread(runnable: Runnable) {
// 此处省略切换主线程代码,创建一个mainLooper的handler, post Runnable即可
}

// 生成代理类
fun <O> generateInnerObserver(clz: Class<O>): O? {
// 固定写法, 传入classLoader 和 待实现的接口列表, 以及核心的 InvocationHandler 的实现, 在其内部进行功能增强
return Proxy.newProxyInstance(clz.classLoader, arrayOf(clz), object : InvocationHandler {
override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {

// 1. 构造runnable, 用于主线程切换
val runnable = Runnable {

// 3. 查找 uiObserver, 若存在则触发
uiObserverMap[clz]?.let { uiObserver ->
val result = method?.invoke(uiObserver, args)
result
}
}

// 2. 将runnable抛主线程
runOnUIThread(runnable)

// 4. 触发method方法得到的返回值, 根据实际类型构造, void时返回null, 此处仅做示意
return null
}
}) as O // 按需强转为实现的接口类型
}
}

具体封装实现可参考如下链接:

改造后的流程如下:

改造后的时序图.png

源码分析

动态代理的实现很简单, 两三行代码就可以搞定, 系统肯定做了很多封装, 把脏活累活给做了, 我们简单看下

从入口方法开始: java.lang.reflect.Proxy#newProxyInstance

// package java.lang.reflect.Proxy.java  基于api 29
private static final Class<?>[] constructorParams = { InvocationHandler.class };

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h){
final Class<?>[] intfs = interfaces.clone();

// 从缓存中查找已生成过的class类型,若不存在则进行生成
Class<?> cl = getProxyClass0(loader, intfs);

// 反射调用构造方法 Proxy(InvocationHandler), 创建并返回实例
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
cons.setAccessible(true);
}
return cons.newInstance(new Object[]{h});
}

private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

/**
* 创建代理类class
*/

private static Class<?> getProxyClass0(ClassLoader loader,Class<?>... interfaces) {
// 接口方法数限制
if (interfaces.length > 65535) { throw new IllegalArgumentException("interface limit exceeded"); }

// 优先从缓存中获取已创建过的代理类, 若不存在, 则创建
return proxyClassCache.get(loader, interfaces);
}

关键的 proxyClassCache 是个二级缓存类(WeakCache), 通过调用其 get 方法得到最终的实现类, 其构造方法签名如下:

// java.lang.reflect.WeakCache.java

/**
* Construct an instance of {@code WeakCache}
*
* @param subKeyFactory a function mapping a pair of
* {@code (key, parameter) -> sub-key}
* @param valueFactory a function mapping a pair of
* {@code (key, parameter) -> value}
* @throws NullPointerException if {@code subKeyFactory} or
* {@code valueFactory} is null.
*/

public WeakCache(BiFunction<K, P, ?> subKeyFactory, BiFunction<K, P, V> valueFactory) {

通过参数名也可以猜到最终是通过 valueFactory 生成的, 我们回到 Proxy 类看下:

// java.lang.reflect.Proxy.java

private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());

/**
* A factory function that generates, defines and returns the proxy class given
* the ClassLoader and array of interfaces.
*/

private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{
// 所有动态代理类名的前缀
private static final String proxyClassNamePrefix = "$Proxy";

// 每一个动态代理类类名中唯一的数字,可猜测最终是分层的代理类名就是: $Proxy+数字
private static final AtomicLong nextUniqueNumber = new AtomicLong();

@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
// 省略部分代码: 对传入的接口数组进行一些校验

String proxyPkg = null; // 最终实现类所在的包路径
int accessFlags = Modifier.PUBLIC | Modifier.FINAL; // 生成的代理类默认访问权限是: public final

// 对接口数组校验: 若待实现的接口是非public的, 则最终实现的代理类也是非public的,并且非public的接口需要在同一个包下
for (Class<?> intf : interfaces) {
int flags = intf.getModifiers();
if (!Modifier.isPublic(flags)) {
accessFlags = Modifier.FINAL;
String name = intf.getName();
int n = name.lastIndexOf('.');
String pkg = ((n == -1) ? "" : name.substring(0, n + 1));
if (proxyPkg == null) {
proxyPkg = pkg;
} else if (!pkg.equals(proxyPkg)) {
throw new IllegalArgumentException(
"non-public interfaces from different packages");
}
}
}

// 若待实现的接口均为 public, 则使用默认的包路径
if (proxyPkg == null) { proxyPkg = ""; }

{
List<Method> methods = getMethods(interfaces); // 递归获取所有接口(包括其父接口)的方法,并手动添加了 equals/hashCode/toString 三个方法
Collections.sort(methods, ORDER_BY_SIGNATURE_AND_SUBTYPE); // 对所有接口方法排序
validateReturnTypes(methods); // 校验接口方法: 确保同名方法得返回类型一致
List<Class<?>[]> exceptions = deduplicateAndGetExceptions(methods); // 去除重复的方法,并获取每个方法对应的异常值信息

Method[] methodsArray = methods.toArray(new Method[methods.size()]);
Class<?>[][] exceptionsArray = exceptions.toArray(new Class<?>[exceptions.size()][]);

long num = nextUniqueNumber.getAndIncrement(); // 生成当前代理实现类的数字信息
String proxyName = proxyPkg + proxyClassNamePrefix + num; // 拼接生成代理类名,默认为: $Proxy+数字

return generateProxy(proxyName, interfaces, loader, methodsArray, exceptionsArray); // 通过native方法生成代理类Class
}
}

@FastNative
private static native Class<?> generateProxy(String name, Class<?>[] interfaces, ClassLoader loader, Method[] methods, Class<?>[][] exceptions);

/**
* 根据传入的接口class信息,获取所有的接口方法,并额外添加 equals/hashCode/toString 三个方法
*/

private static List<Method> getMethods(Class<?>[] interfaces) {
List<Method> result = new ArrayList<Method>();
try {
result.add(Object.class.getMethod("equals", Object.class));
result.add(Object.class.getMethod("hashCode", EmptyArray.CLASS));
result.add(Object.class.getMethod("toString", EmptyArray.CLASS));
} catch (NoSuchMethodException e) {
throw new AssertionError();
}

getMethodsRecursive(interfaces, result); // 通过递归反射的方式一次获取接口所有的方法
return result;
}
}

动态代理生成的类长啥样?

上面我们简单分析了下动态代理的源码, 我们可以知道/推测得到以下信息:

  1. 生成的代理类叫做 $ProxyN 其中 N 是一个数字,随代理类的增加而递增
  2. $ProxyN 实现了所有接口方法,并自动添加了 equals/hashCode/toString 三个方法,因此: --> a. 动态代理生成类应可以强转为任何传入的接口类型 --> b. 额外增加的三个方法通常会影响对象的比较,需要手动赋值区分
  3. 触发动态代理类的方法最终都会回调 InvocationHandler#invoke 方法,而 InvocationHandler 是通过 Proxy#newProxyInstance 传入的,因此: --> 猜测生成 $ProxyN 类应是继承自 Proxy 类

猜测归猜测, 最好能导出生成的 $ProxyN 看下实际代码:

  1. 网上查到的通常是使用 JVM 提供的 sun.misc.ProxyGenerator 类, 但这个类在android中不存在,手动拷贝对应jar包到android中使用也有问题
  2. 尝试使用字节码操作库或者 Class#getResourceAsStream 等方式也失败了, 终究是JVM上的工具, 在android虚拟机上无法直接使用
  3. 最终退而求其次, 先通过反射获取 $ProxyN 的类结构, 至于方法的调用则通过 InvocationHandler#invoke 方法中打印堆栈来查看
// 1. 自定义一个接口如下
package org.lynxz.utils.observer
interface ICallback {
fun onCallback(a: Int, b: Boolean, c: String?)
}

// 2. 通过反射获取类结构
package org.lynxz.utils.reflect.ReflectUtilTest
@Test
fun oriProxyTest() {
val proxyObj = Proxy.newProxyInstance(
javaClass.classLoader,
arrayOf(ICallback::class.java)
) { proxy, method, args -> // InvocationHandler#invoke 方法体
RuntimeException("===> 调用堆栈:${method?.name}").printStackTrace() // 3. 打印调用堆栈信息
args?.forEachIndexed { index, any -> // 4. 打印方法得实参
LoggerUtil.w(TAG, "===> 方法参数: $index - $any")
}
ReflectUtil.generateDefaultTypeValue(method!!.returnType) // 根据方法返回类型生成对应数据
}

// ProxyGeneratorImpl 是自定义的通过反射获取类结构的实现类, 具体代码请查看上面给出的github仓库
LoggerUtil.w(TAG, "===>类结构:\n${ProxyGeneratorImpl(proxyObj.javaClass).generate()}")
if (proxyObj is ICallback) { // 强转生成的动态代理类为自定义的接口
proxyObj.onCallback(1, true, "hello") // 触发接口方法,以便触发 InvocationHandler#invoke 方法, 进而打印堆栈
}
}

最终得到日志如下, 验证了之前的猜测:

// ===>类结构:
public final class $Proxy6 extends java.lang.reflect.Proxy implements ICallback{
public static final Class[] NFC;
public static final Class[][] NFD;
public $Proxy6(Class){...}
public final boolean equals(Object){...} // 方法体的内容不可知, 此处用省略号替代
public final int hashCode(){...}
public final String toString(){...}
public final void onCallback(int,boolean,String){...}
}

// 调用堆栈:
===> 调用堆栈:onCallback
at org.lynxz.utils.reflect.ReflectUtilTest$oriProxyTest$proxyObj$1.invok(ReflectUtilTest.kt:86) // 对应上方代码: RuntimeException("===> 调用堆栈:${method?.name}").printStackTrace()
at java.lang.reflect.Proxy.invoke(Proxy.java:913) // 触发 Proxy#invoke 方法, 其内部直接触发 InvocationHandler#invoke 方法
at $Proxy6.onCallback(Unknown Source) // 对应上方代码: proxyObj.onCallback(1, true, "hello")

// 打印方法实参数据, 序号 - 值, 与我们传入的相同
===> 方法参数: 0 - 1
===> 方法参数: 1 - true
===> 方法参数: 2 - hello

Proxy#invoke 源码, 就是简单的触发 InvocationHandler#invoke 而已

// java.lang.reflect.Proxy.java
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}

// 直接触发 invocationHandler 方法
// 而 InvocationHandler 是通过 Proxy#newProxyInstance 传入的, 最终传到 $Proxy6 的构造方法
private static Object invoke(Proxy proxy, Method method, Object[] args) throws Throwable {
InvocationHandler h = proxy.h; // 此处的proxy就是上面动态代理生成 `$Proxy6` 类
return h.invoke(proxy, method, args);
}

收起阅读 »

smali语言之locals和registers的区别

介绍对于dalviks字节码寄存器都是32位的,它能够表示任何类型,2个寄存器用于表示64位的类型(Long and Double)。作用声明于方法内部(必须).method public getName()V .registers 6 retu...
继续阅读 »

介绍

对于dalviks字节码寄存器都是32位的,它能够表示任何类型,2个寄存器用于表示64位的类型(Long and Double)。

作用

声明于方法内部(必须)

.method public getName()V
.registers 6

return-void
.end method

.registers和locals基本区别

在一个方法(method)中有两中方式指定有多少个可用的寄存器。指令.registers指令指定了在这个方法中有多少个可用的寄存器,

指令.locals指明了在这个方法中非参(non-parameter)寄存器的数量。然而寄存器的总数也包括保存方法参数的寄存器。

参数是如何传递的?

1.如果是非静态方法

例如,你写了一个非静态方法LMyObject;->callMe(II)V。这个方法有2个int参数,但在这两个整型参数前面还有一个隐藏的参数LMyObject;也就是当前对象的引用,所以这个方法总共有3个参数。 假如在一个方法中包含了五个寄存器(V0-V4),如下:

.method public callMe(II)V
const-string v0,"1"
const-string v1,"1"

return-void
.end method

那么只需用.register指令指定5个,或者使用.locals指令指定2个(2个local寄存器+3个参数寄存器)。如下:

.method public callMe(II)V
.registers 5
const-string v0,"1"
const-string v1,"1"
v3==>p0
V4==>P1
V5==>P2

return-void
.end method

或者
.method public callMe(II)V
.locals 2
const-string v0,"1"
const-string v1,"1"
return-void
.end method

该方法被调用的时候,调用方法的对象(即this引用)会保存在V2中,第一个参数在V3中,第二个参数在v4中。

2.如果是静态方法

那么参数少了对象引用,除此之外和非静态原理相同,registers为4 locals依然是2

关于寄存器命名规则

v命名法

上面的例子中我们使用的是v命名法,也就是在本地寄存器后面依次添加参数寄存器,

但是这种命名方式存在一种问题:假如我后期想要修改方法体的内容,涉及到增加或者删除寄存器,由于v命名法需要排序的局限性,那么会造成大量代码的改动,有没有一种办法让我们只改动registers或者locals的值就可以了呢, 答案是:有的

v命名法之外,还有一种命名法叫做p命名法

p命名法

p命名法只能给方法参数命名,不能给本地变量命名

假如有一个非静态方法如下:

.method public print(Ljava/lang/String;Ljava/lang/String;I)V

以下是p命名法参数对应表:

p0this
p1第一个参数Ljava/lang/String;
p2第二个参数Ljava/lang/String;
p3第三个参数I

如前面提到的,long和double类型都是64位,需要2个寄存器。当你引用参数的时候一定要记住,例如:你有一个非静态方法

LMyObject;->MyMethod(IJZ)V

方法的参数为int、long、bool。所以这个方法的所有参数需要5个寄存器。

p0this
p1I
p2, p3J
p4Z

另外当你调用方法后,你必须在寄存器列表,调用指令中指明,两个寄存器保存了double-wide宽度的参数。

注意:在默认的baksmali中,参数寄存器将使用P命名方式,如果出于某种原因你要禁用P命名方式,而要强制使用V命名方式,应当使用-p/--no-parameter-registers选项。

总结

  • locals和registers都可以表示寄存器数量,locals指定本地局部变量寄存器个数,registers是locals和参数寄存器数量的总数,两者使用任选其一
  • 同时,寄存器命名一共分两种,一种是v命名法,另一种是p命名法
v0the first local register
v1the second local register
v2p0the first parameter register
v3p1the second parameter register
v4p2the third parameter register

收起阅读 »

Swift 数组

iOS
Swift 数组Swift 数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。Swift 数组会强制检测元素的类型,如果类型不同则会报错,Swift 数组应该遵循像Array<Element>这样的形式,其中Elem...
继续阅读 »

Swift 数组

Swift 数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在一个数组的不同位置中。

Swift 数组会强制检测元素的类型,如果类型不同则会报错,Swift 数组应该遵循像Array<Element>这样的形式,其中Element是这个数组中唯一允许存在的数据类型。

如果创建一个数组,并赋值给一个变量,则创建的集合就是可以修改的。这意味着在创建数组后,可以通过添加、删除、修改的方式改变数组里的项目。如果将一个数组赋值给常量,数组就不可更改,并且数组的大小和内容都不可以修改。


创建数组

我们可以使用构造语法来创建一个由特定数据类型构成的空数组:

var someArray = [SomeType]()

以下是创建一个初始化大小数组的语法:

var someArray = [SomeType](repeating: InitialValue, count: NumbeOfElements)

以下实例创建了一个类型为 Int ,数量为 3,初始值为 0 的空数组:

var someInts = [Int](repeating: 0, count: 3)

以下实例创建了含有三个元素的数组:

var someInts:[Int] = [10, 20, 30]

访问数组

我们可以根据数组的索引来访问数组的元素,语法如下:

var someVar = someArray[index]

index 索引从 0 开始,即索引 0 对应第一个元素,索引 1 对应第二个元素,以此类推。

我们可以通过以下实例来学习如何创建,初始化,访问数组:

import Cocoa

var someInts = [Int](repeating: 10, count: 3)

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 10
第二个元素的值 10
第三个元素的值 10

修改数组

你可以使用 append() 方法或者赋值运算符 += 在数组末尾添加元素,如下所示,我们初始化一个数组,并向其添加元素:

import Cocoa

var someInts = [Int]()

someInts
.append(20)
someInts
.append(30)
someInts
+= [40]

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 20
第二个元素的值 30
第三个元素的值 40

我们也可以通过索引修改数组元素的值:

import Cocoa

var someInts = [Int]()

someInts
.append(20)
someInts
.append(30)
someInts
+= [40]

// 修改最后一个元素
someInts
[2] = 50

var someVar = someInts[0]

print( "第一个元素的值 \(someVar)" )
print( "第二个元素的值 \(someInts[1])" )
print( "第三个元素的值 \(someInts[2])" )

以上程序执行输出结果为:

第一个元素的值 20
第二个元素的值 30
第三个元素的值 50

遍历数组

我们可以使用for-in循环来遍历所有数组中的数据项:

import Cocoa

var someStrs = [String]()

someStrs
.append("Apple")
someStrs
.append("Amazon")
someStrs
.append("Runoob")
someStrs
+= ["Google"]

for item in someStrs {
print(item)
}

以上程序执行输出结果为:

Apple
Amazon
Runoob
Google

如果我们同时需要每个数据项的值和索引值,可以使用 String 的 enumerate() 方法来进行数组遍历。实例如下:

import Cocoa

var someStrs = [String]()

someStrs
.append("Apple")
someStrs
.append("Amazon")
someStrs
.append("Runoob")
someStrs
+= ["Google"]

for (index, item) in someStrs.enumerated() {
print("在 index = \(index) 位置上的值为 \(item)")
}

以上程序执行输出结果为:

 index = 0 位置上的值为 Apple
index = 1 位置上的值为 Amazon
index = 2 位置上的值为 Runoob
index = 3 位置上的值为 Google

合并数组

我们可以使用加法操作符(+)来合并两种已存在的相同类型数组。新数组的数据类型会从两个数组的数据类型中推断出来:

import Cocoa

var intsA = [Int](repeating: 2, count:2)
var intsB = [Int](repeating: 1, count:3)

var intsC = intsA + intsB

for item in intsC {
print(item)
}

以上程序执行输出结果为:

2
2
1
1
1

count 属性

我们可以使用 count 属性来计算数组元素个数:

import Cocoa

var intsA = [Int](count:2, repeatedValue: 2)
var intsB = [Int](count:3, repeatedValue: 1)

var intsC = intsA + intsB

print("intsA 元素个数为 \(intsA.count)")
print("intsB 元素个数为 \(intsB.count)")
print("intsC 元素个数为 \(intsC.count)")

以上程序执行输出结果为:

intsA 元素个数为 2
intsB
元素个数为 3
intsC
元素个数为 5

isEmpty 属性

我们可以通过只读属性 isEmpty 来判断数组是否为空,返回布尔值:

import Cocoa

var intsA = [Int](count:2, repeatedValue: 2)
var intsB = [Int](count:3, repeatedValue: 1)
var intsC = [Int]()

print("intsA.isEmpty = \(intsA.isEmpty)")
print("intsB.isEmpty = \(intsB.isEmpty)")
print("intsC.isEmpty = \(intsC.isEmpty)")

以上程序执行输出结果为:

intsA.isEmpty = false
intsB
.isEmpty = false
intsC
.isEmpty = true
收起阅读 »

Swift 字符(Character)

iOS
Swift 的字符是一个单一的字符字符串字面量,数据类型为 Character。以下实例列出了两个字符实例:import Cocoa let char1: Character = "A" let char2: Character = "B" print("...
继续阅读 »

Swift 的字符是一个单一的字符字符串字面量,数据类型为 Character。

以下实例列出了两个字符实例:

import Cocoa

let char1: Character = "A"
let char2: Character = "B"

print("char1 的值为 \(char1)")
print("char2 的值为 \(char2)")

以上程序执行输出结果为:

char1 的值为 A
char2
的值为 B

如果你想在 Character(字符) 类型的常量中存储更多的字符,则程序执行会报错,如下所示:

import Cocoa

// Swift 中以下赋值会报错
let char: Character = "AB"

print("Value of char \(char)")

以上程序执行输出结果为:

error: cannot convert value of type 'String' to specified type 'Character'
let char: Character = "AB"

空字符变量

Swift 中不能创建空的 Character(字符) 类型变量或常量:

import Cocoa

// Swift 中以下赋值会报错
let char1: Character = ""
var char2: Character = ""

print("char1 的值为 \(char1)")
print("char2 的值为 \(char2)")

以上程序执行输出结果为:

 error: cannot convert value of type 'String' to specified type 'Character'
let char1: Character = ""
^~
error
: cannot convert value of type 'String' to specified type 'Character'
var char2: Character = ""

遍历字符串中的字符

Swift 的 String 类型表示特定序列的 Character(字符) 类型值的集合。 每一个字符值代表一个 Unicode 字符。

Swift 3 中的 String 需要通过 characters 去调用的属性方法,在 Swift 4 中可以通过 String 对象本身直接调用,例如:

Swift 3 中:

import Cocoa

for ch in "Runoob".characters {
print(ch)
}

Swift 4 中:

import Cocoa

for ch in "Runoob" {
print(ch)
}

以上程序执行输出结果为:

R
u
n
o
o
b

字符串连接字符

以下实例演示了使用 String 的 append() 方法来实现字符串连接字符:

import Cocoa

var varA:String = "Hello "
let varB:Character = "G"

varA
.append( varB )

print("varC = \(varA)")

以上程序执行输出结果为:

varC  =  Hello G
收起阅读 »

Swift 字符串

iOS
Swift 字符串是一系列字符的集合。例如 "Hello, World!" 这样的有序的字符类型的值的集合,它的数据类型为 String。创建字符串你可以通过使用字符串字面量或 String 类的实例来创建一个字符串:import Cocoa //...
继续阅读 »

Swift 字符串是一系列字符的集合。例如 "Hello, World!" 这样的有序的字符类型的值的集合,它的数据类型为 String


创建字符串

你可以通过使用字符串字面量或 String 类的实例来创建一个字符串:

import Cocoa

// 使用字符串字面量
var stringA = "Hello, World!"
print( stringA )

// String 实例化
var stringB = String("Hello, World!")
print( stringB )

以上程序执行输出结果为:

Hello, World!
Hello, World!

空字符串

你可以使用空的字符串字面量赋值给变量或初始化一个String类的实例来初始值一个空的字符串。 我们可以使用字符串属性 isEmpty 来判断字符串是否为空:

import Cocoa

// 使用字符串字面量创建空字符串
var stringA = ""

if stringA.isEmpty {
print( "stringA 是空的" )
} else {
print( "stringA 不是空的" )
}

// 实例化 String 类来创建空字符串
let stringB = String()

if stringB.isEmpty {
print( "stringB 是空的" )
} else {
print( "stringB 不是空的" )
}

以上程序执行输出结果为:

stringA 是空的
stringB 是空的

字符串常量

你可以将一个字符串赋值给一个变量或常量,变量是可修改的,常量是不可修改的。

import Cocoa

// stringA 可被修改
var stringA = "菜鸟教程:"
stringA += "http://www.runoob.com"
print( stringA )

// stringB 不能修改
let stringB = String("菜鸟教程:")
stringB += "http://www.runoob.com"
print( stringB )

以上程序执行输出结果会报错,因为 stringB 为常量是不能被修改的:

error: left side of mutating operator isn't mutable: 'stringB' is a 'let' constant
stringB += "http://www.runoob.com"

字符串中插入值

字符串插值是一种构建新字符串的方式,可以在其中包含常量、变量、字面量和表达式。 您插入的字符串字面量的每一项都在以反斜线为前缀的圆括号中:

import Cocoa

var varA = 20
let constA = 100
var varC:Float = 20.0

var stringA = "\(varA) 乘于 \(constA) 等于 \(varC * 100)"
print( stringA )

以上程序执行输出结果为:

20 乘于 100 等于 2000.0

字符串连接

字符串可以通过 + 号来连接,实例如下:

import Cocoa

let constA = "菜鸟教程:"
let constB = "http://www.runoob.com"

var stringA = constA + constB

print( stringA )

以上程序执行输出结果为:

菜鸟教程:http://www.runoob.com

字符串长度

字符串长度使用 String.count 属性来计算,实例如下:

Swift 3 版本使用的是 String.characters.count

import Cocoa

var varA = "www.runoob.com"

print( "\(varA), 长度为 \(varA.count)" )

以上程序执行输出结果为:

http://www.runoob.com, 长度为 14

字符串比较

你可以使用 == 来比较两个字符串是否相等:

import Cocoa

var varA = "Hello, Swift!"
var varB = "Hello, World!"

if varA == varB {
print( "\(varA) 与 \(varB) 是相等的" )
} else {
print( "\(varA) 与 \(varB) 是不相等的" )
}

以上程序执行输出结果为:

Hello, Swift! 与 Hello, World! 是不相等的

Unicode 字符串

Unicode 是一个国际标准,用于文本的编码,Swift 的 String 类型是基于 Unicode建立的。你可以循环迭代出字符串中 UTF-8 与 UTF-16 的编码,实例如下:

import Cocoa

var unicodeString = "菜鸟教程"

print("UTF-8 编码: ")
for code in unicodeString.utf8 {
print("\(code) ")
}

print("\n")

print("UTF-16 编码: ")
for code in unicodeString.utf16 {
print("\(code) ")
}

以上程序执行输出结果为:

UTF-8 编码: 
232
143
156
233
184
159
230
149
153
231
168
139
UTF-16 编码:
33756
40479
25945
31243

字符串函数及运算符

Swift 支持以下几种字符串函数及运算符:

序号函数/运算符 & 描述
1

isEmpty

判断字符串是否为空,返回布尔值

2

hasPrefix(prefix: String)

检查字符串是否拥有特定前缀

3

hasSuffix(suffix: String)

检查字符串是否拥有特定后缀。

4

Int(String)

转换字符串数字为整型。 实例:

let myString: String = "256"
let myInt: Int? = Int(myString)

5

String.count

Swift 3 版本使用的是 String.characters.count

计算字符串的长度

6

utf8

您可以通过遍历 String 的 utf8 属性来访问它的 UTF-8 编码

7

utf16

您可以通过遍历 String 的 utf8 属性来访问它的 utf16 编码

8

unicodeScalars

您可以通过遍历String值的unicodeScalars属性来访问它的 Unicode 标量编码.

9

+

连接两个字符串,并返回一个新的字符串

10

+=

连接操作符两边的字符串并将新字符串赋值给左边的操作符变量

11

==

判断两个字符串是否相等

12

<

比较两个字符串,对两个字符串的字母逐一比较。

13

!=

比较两个字符串是否不相等。

收起阅读 »

Swift 实战技巧

iOS
Swift实战技巧从OC转战到Swift,差别还是蛮大的,本文记录了我再从OC转到Swift开发过程中遇到的一些问题,然后把我遇到的这些问题记录形成文章,大体上是一些Swift语言下面的一些技巧,希望对有需要的人有帮助OC调用方法的处理给OC调用的方法需要添加...
继续阅读 »

Swift实战技巧

从OC转战到Swift,差别还是蛮大的,本文记录了我再从OC转到Swift开发过程中遇到的一些问题,然后把我遇到的这些问题记录形成文章,大体上是一些Swift语言下面的一些技巧,希望对有需要的人有帮助

  • OC调用方法的处理

给OC调用的方法需要添加@objc标记,一般的action-target的处理方法,通知的处理方法等需要添加@objc标记

@objc func onRefresh(){
self.refreshCallback?()
}

  • 处理SEL选择子

使用方法型如 #selector(方法名称)
eg.

`#selector(self.onRefresh))`  

更加详细的介绍可以看这篇文章: http://swifter.tips/selector/

下面是使用MJRefresh给mj_headermj_footer添加回调处理函数的例子

self.mj_header.setRefreshingTarget(self, refreshingAction: #selector(self.onRefresh))
self.mj_footer.setRefreshingTarget(self, refreshingAction: #selector(self.onLoadMore))

  • try关键字的使用

可能发生异常的方法使用try?方法进行可选捕获异常

let jsonStr=try?String(contentsOfFile: jsonPath!)

  • 类对象参数和类对象的参数值

AnyClass作为方法的形参,类名称.self(modelClass.self)作为实参

    func registerCellNib(nib:UINib,modelClass:AnyClass){
self.register(nib, forCellReuseIdentifier: String(describing:modelClass.self))
}

...
self.tableView?.registerCellNib(nib: R.nib.gameCell(), modelClass: GameModel.self)

  • 线程间调用

主线程使用DispatchQueue.main,全局的子线程使用DispatchQueue.global(),方法可以使用syncasyncasyncAfter等等

下面是网络请求线程间调用的例子

let _  = URLSession.shared.dataTask(with: url, completionHandler: { [weak self] (data, response, error) in
guard let weakSelf = self else {
return
}
if error == nil {
if let json = try? JSONSerialization.jsonObject(with: data!, options: .mutableContainers) {
let data = json as! [Any]
DispatchQueue.main.async {
weakSelf.suggestions = data[1] as! [String]
if weakSelf.suggestions.count > 0 {
weakSelf.tableView.reloadData()
weakSelf.tableView.isHidden = false
} else {
weakSelf.tableView.isHidden = true
}
}
}
}
}).resume()

  • 闭包中使用weak防止循环引用的语法
URLSession.shared.dataTask(with: requestURL) {[weak self] (data, response, error) in
guard let weakSelf = self else {
return
}
weakSelf.tableView.reloadData()
}

  • 逃逸闭包和非逃逸闭包
    逃逸闭包,在方法执行完成之后才调用的闭包称为逃逸闭包,一般在方法中做异步处理耗时的任务,任务完成之后把结果使用闭包进行回调处理使用的闭包为逃逸闭包,需要显示的使用@escaping关键字修饰
    非逃逸闭包,在方法执行完成之前调用的闭包称为逃逸闭包,比如snapkit框架使用的闭包是在方法执行完成之后就已经处理完毕了
    Swift3之后闭包默认都是非逃逸(@noescape,不能显示声明),并且这种类型是不能显示使用@noescape关键字修饰的
    // 模拟网络请求,completion闭包是异步延迟处理的,所以需要添加`@escaping`进行修饰
class func fetchVideos(completion: @escaping (([Video]) -> Void)) {
DispatchQueue.global().async {
let video1 = Video.init(title: "What Does Jared Kushner Believe", channelName: "Nerdwriter1")
let video2 = Video.init(title: "Moore's Law Is Ending. So, What's Next", channelName: "Seeker")
let video3 = Video.init(title: "What Bill Gates is afraid of", channelName: "Vox")
var items = [video1, video2, video3]
items.shuffle()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.init(uptimeNanoseconds: 3000000000), execute: {
completion(items)
})
}
}

  • Notification.Name的封装处理

swift3中Notification的名字是一种特殊的Notification.Name类型,下面使用enum进行封装处理,并且创建一个NotificationCenter的扩展,处理通知消息的发送

// 定义Notification.Name枚举
enum YTNotification: String {
case scrollMenu
case didSelectMenu
case openPage
case hideBar

var stringValue: String {
return "YT" + rawValue
}

// 枚举成员返回对应的Notification.Name类型
var notificationName: NSNotification.Name {
return Notification.Name.init(stringValue)
}
}

extension NotificationCenter {
func yt_post(custom notification: YTNotification, object anObject: Any?, userInfo aUserInfo: [AnyHashable : Any]? = nil) {
self.post(name: notification.notificationName, object: anObject, userInfo: aUserInfo)
}
}

使用方法
添加通知观察者使用的是YTNotification枚举成员的notificationName返回的Notification.Name类型的值
发送消息使用的是YTNotification枚举成员

// 添加通知观察
NotificationCenter.default.addObserver(self, selector: #selector(self.changeTitle(notification:)), name: YTNotification.scrollMenu.notificationName, object: nil)

// 发送消息
NotificationCenter.default.yt_post(custom: YTNotification.scrollMenu, object: nil, userInfo: ["length": scrollIndex])

lazy惰性加载属性,只有在使用的时候才初始化变量

    // 闭包的方式
let menuTitles = ["History", "My Videos", "Notifications", "Watch Later"]
lazy var menuItems : [MenuItem] = {
var tmenuItems = [MenuItem]()
for menuTitle in menuTitles {
let menuItem = MenuItem(iconImage: UIImage.init(named: menuTitle)!, title: menuTitle)
tmenuItems.append(menuItem)
}
return tmenuItems
}()

// 普通方式,
lazy var titles = ["A", "B"]

  • 类型判断

使用is判断类型以及使用if-let和as?判断类型

// MARK:- 类型检查例子
let sa = [
Chemistry(physics: "固体物理", equations: "赫兹"),
Maths(physics: "流体动力学", formulae: "千兆赫"),
Chemistry(physics: "热物理学", equations: "分贝"),
Maths(physics: "天体物理学", formulae: "兆赫"),
Maths(physics: "微分方程", formulae: "余弦级数")]

var chemCount = 0
var mathsCount = 0
for item in sa {
// 如果是一个 Chemistry 类型的实例,返回 true,相反返回 false。 相当于isKindOfClass
if item is Chemistry {
chemCount += 1
} else if item is Maths {
mathsCount += 1
}
}

// 使用if-let和as?判断类型
for item in sa {
// 如果是一个 Chemistry 类型的实例,返回 true,相反返回 false。 相当于isKindOfClass
if let _ = item as? Chemistry {
chemCount += 1
} else if let _ = item as? Maths {
mathsCount += 1
}
}

使用switch-case和as判断类型

// Any可以表示任何类型,包括方法类型
var exampleany = [Any]()
exampleany.append(12)
exampleany.append(3.14159)
exampleany.append("Any 实例")
exampleany.append(Chemistry(physics: "固体物理", equations: "兆赫"))

// 使用switch-case和as判断类型
for item2 in exampleany {
switch item2 {
case let someInt as Int:
print("整型值为 \(someInt)")
case let someDouble as Double where someDouble > 0:
print("Pi 值为 \(someDouble)")
case let someString as String:
print("\(someString)")
case let phy as Chemistry:
print("主题 '\(phy.physics)', \(phy.equations)")
default:
print("None")
}
}

  • Swift使用KVC,执行KVC操作的变量需要添加@objc标记
class Feed: NSObject, HandyJSON  {
// 使用KVC添加@objc关键字
@objc var id = 0
var type = ""
var payload: PayLoad?
var user: PostUser?

required override init() {}
}

  • swift中CGRect类型的操作

swift中简化了CGRect类型的操作,比如有一个CGRect的类型实例为frame,以下例举了OC中对应的在swift下的语法

OCSwift
CGRectGetMaxX(frame)frame.maxX
CGRectGetMinY(frame)frame.minY
CGRectGetMidX(frame)frame.midX
CGRectGetWidth(frame)frame.width
CGRectGetHeight(frame)frame.height
CGRectContainsPoint(frame, point)frame.contains(point)
  • Swift中指针的处理

详细的介绍可以查看这篇文章:http://swifter.tips/unsafe/
下面是一个使用OC库RegexKitLite中的一个例子,block中返回值是指针类型的,需要转换为对应的swift对象类型

func composeAttrStr(text: String) -> NSAttributedString {
// 表情的规则
let emotionPattern = "\\[[0-9a-zA-Z\\u4e00-\\u9fa5]+\\]";
// @的规则
let atPattern = "@[0-9a-zA-Z\\u4e00-\\u9fa5-_]+";
// #话题#的规则
let topicPattern = "#[0-9a-zA-Z\\u4e00-\\u9fa5]+#";
// url链接的规则
let urlPattern = "\\b(([\\w-]+://?|www[.])[^\\s()<>]+(?:\\([\\w\\d]+\\)|([^[:punct:]\\s]|/)))";
let pattern = "\(emotionPattern)|\(atPattern)|\(topicPattern)|\(urlPattern)"

var textParts = [TextPart]()

(text as! NSString).enumerateStringsMatched(byRegex: pattern) { (captureCount: Int, capString: UnsafePointer<NSString?>?, capRange: UnsafePointer<NSRange>?, stop: UnsafeMutablePointer<ObjCBool>?) in
let captureString = capString?.pointee as! String
let captureRange = capRange?.pointee as! NSRange

let part = TextPart()
part.text = captureString
part.isSpecial = true
part.range = captureRange
textParts.append(part)
}
// ...
}

  • 只有类才能实现的protocol 有一种场景,protocol作为delegate,需要使用weak关键字修饰的时候,需要指定delegate的类型为ptotocol型,这个ptotocol需要添加class修饰符,比如下面的这个protocol,因为类类型的对象才有引用计数,才有weak的概念,没有引用计数的struct型是没有weak概念的
/// ImageViewer和ImageCell交互使用的协议
protocol YTImageCellProtocol : class {
// Cell的点击事件,处理dismiss
func imageCell(_ imageCell: YTImageCell, didSingleTap: Bool);
}
收起阅读 »

手摸手教你用webpack搭建TS开发环境

前言 最近在学习typescript,也就是我们常说的TS,它是JS的超集。具体介绍就不多说了,今天主要是带大家用webpack从零搭建一个TS开发环境。直接用传统的tsc xx.ts文件进行编译的话太繁琐,不利于我们开发,经过这次手动配置,我们也能知道vue...
继续阅读 »

前言


最近在学习typescript,也就是我们常说的TS,它是JS的超集。具体介绍就不多说了,今天主要是带大家用webpack从零搭建一个TS开发环境。直接用传统的tsc xx.ts文件进行编译的话太繁琐,不利于我们开发,经过这次手动配置,我们也能知道vue3内部对TS的webpack进行了怎样的配置,废话不多说进入正题。


Node 编译TS


先讲讲如何运行ts文件吧,最传统的方式当然是直接输入命令



tsc xxx.ts



当然你必须得先安装过ts,如果没有请执行以下命令



npm install typescript -g



安装后查看下版本



tsc --version



这样我们就能得到编译后的js文件了,然后我们可以通过node指令



node xxx.js



进行查看,当然也可以新建一个HTML页面引入编译后的js文件


我们从上可以发现有点小复杂,那可不可以直接通过Node直接编译TS呢?接来下就是介绍这种方法

使用ts-node 就可以得到我们想要的效果

安装



npm install ts-node -g



另外ts-node需要依赖 tslib 和 @types/node 两个包,也需要下载



npm install tslib @types/node -g



现在,我们可以直接通过 ts-node 来运行TypeScript的代码



ts-node xxx.ts



如果遇到很多ts文件,那我们用这种方法也会觉得繁琐,所以我们最好是用webpack搭建一个支持TS开发环境,这样才是最好的解决方案。


webpack搭建准备工作


先新建一个文件夹

下载 webpack webpack-cli



npm install webpack webpack-cli -D



下载 ts tsloader(编译ts文件)



npm install typescript ts-loader -D



下载 webpack-dev-server(搭建本地服务器)



npm install webpack-dev-server -D



下载 html模板插件



npm install html-webpack-plugin -D



初始化webpack



npm init



初始化ts



tsc --init



新建配置文件 webpack.config.js


初始化后文件结构如下图所示,当然还有一些测试ts和html需要自己手动创建下
image.png


webpack 配置


配置之前我们先去package.json中添加两个运行和打包指令


image.png


webpack.config.js


代码中有详细说明哦


const path = require('path')//引入内置path方便得到绝对路径
const HtmlWebpackPlugin = require('html-webpack-plugin')//引入模板组件


module.exports = {
mode: 'development',//开发模式
entry: './src/main.ts',//入口文件地址
output: {
path: path.resolve(__dirname, "./dist"),//出口文件,即打包后的文件存放地址
filename: 'bundle.js' //文件名
},
devServer: {

},
resolve: {
extensions:['.ts', '.js', '.cjs', '.json'] //配置文件引入时省略后缀名
},
module: {
rules: [
{
test: /\.ts$/, //匹配规则 以ts结尾的文件
loader: 'ts-loader' //对应文件采用ts-loader进行编译
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html' //使用模板地址
})
]
}

配置完成我们可以进行测试了,执行指令



npm run serve



打包指令



npm run build



End


看完的话点个赞吧~~


QQ图片20200210181218.jpg



收起阅读 »

用 JS 写算法时你应该知道的——数组不能当队列使用!!

在初学 JS 时,发现数组拥有 shift()、unshift()、pop()、push() 这一系列方法,而不像 Java 或 CPP 中分别引用队列、栈等数据结构,还曾偷偷窃喜。现在想想,这都是以高昂的复杂度作为代价的QAQ。 举个例子 - BFS 一般队...
继续阅读 »

在初学 JS 时,发现数组拥有 shift()unshift()pop()push() 这一系列方法,而不像 Java 或 CPP 中分别引用队列、栈等数据结构,还曾偷偷窃喜。现在想想,这都是以高昂的复杂度作为代价的QAQ。


举个例子 - BFS


一般队列的应用是在 BFS 题目中使用到。BFS(Breath First Search)广度优先搜索,作为入门算法,基本原理大家应该都了解,这里不再细说。


LeetCode 1765. 地图中的最高点



给你一个大小为 m x n 的整数矩阵 isWater ,它代表了一个由 陆地 和 水域 单元格组成的地图。


如果 isWater[i][j] == 0 ,格子 (i, j) 是一个 陆地 格子。
如果 isWater[i][j] == 1 ,格子 (i, j) 是一个 水域 格子。
你需要按照如下规则给每个单元格安排高度:



  • 每个格子的高度都必须是非负的。

  • 如果一个格子是是 水域 ,那么它的高度必须为 0 。

  • 任意相邻的格子高度差 至多 为 1 。当两个格子在正东、南、西、北方向上相互紧挨着,就称它们为相邻的格子。(也就是说它们有一条公共边)


找到一种安排高度的方案,使得矩阵中的最高高度值 最大 。


请你返回一个大小为 m x n 的整数矩阵 height ,其中 height[i][j] 是格子 (i, j) 的高度。如果有多种解法,请返回 任意一个 。



常规 BFS 题目,从所有的水域出发进行遍历,找到每个点离水域的最近距离即可。常规写法,三分钟搞定。


/**
* @param {number[][]} isWater
* @return {number[][]}
*/
var highestPeak = function(isWater) {
// 每个水域的高度都必须是0
// 一个格子离最近的水域的距离 就是它的最大高度
let n = isWater.length, m = isWater[0].length;
let height = new Array(n).fill().map(() => new Array(m).fill(-1));
let q = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < m; j++) {
if (isWater[i][j] === 1) {
q.push([i, j]);
height[i][j] = 0;
}
}
}
let dir = [[0, 1], [0, -1], [1, 0], [-1, 0]];
while (q.length) {
for (let i = q.length - 1; i >= 0; i--) {
let [x, y] = q.shift();
for (let [dx, dy] of dir) {
let nx = x + dx, ny = y + dy;
if (nx < n && nx >= 0 && ny < m && ny >= 0 && height[nx][ny] === -1) {
q.push([nx, ny]);
height[nx][ny] = height[x][y] + 1;
}
}
}
}
return height;
};

然后,超时了……


调整一下,


/**
* @param {number[][]} isWater
* @return {number[][]}
*/
var highestPeak = function(isWater) {
// 每个水域的高度都必须是0
// 一个格子离最近的水域的距离 就是它的最大高度
let n = isWater.length, m = isWater[0].length;
let height = new Array(n).fill().map(() => new Array(m).fill(-1));
let q = [];
for (let i = 0; i < n; i++) {
for (let j = 0; j < m; j++) {
if (isWater[i][j] === 1) {
q.push([i, j]);
height[i][j] = 0;
}
}
}
let dir = [[0, 1], [0, -1], [1, 0], [-1, 0]];
while (q.length) {
let tmp = [];
for (let i = q.length - 1; i >= 0; i--) {
let [x, y] = q[i];
for (let [dx, dy] of dir) {
let nx = x + dx, ny = y + dy;
if (nx < n && nx >= 0 && ny < m && ny >= 0 && height[nx][ny] === -1) {
tmp.push([nx, ny]);
height[nx][ny] = height[x][y] + 1;
}
}
}
q = tmp;
}
return height;
};

ok,这回过了,而且打败了 90% 的用户。


image.png


那么问题出在哪里呢?shift()!!!


探究 JavaScript 中 shift() 的实现


在学习 C++ 的时候,队列作为一个先入先出的数据结构,入队和出队肯定都是O(1)的时间复杂度,用链表


让我们查看下 V8 中 shift() 的源码


简单实现就是


function shift(arr) {
let len = arr.length;
if (len === 0) {
return;
}
let first = arr[0];
for (let i = 0; i < len - 1; i++) {
arr[i] = arr[i + 1];
}
arr.length = len - 1;
return first;
}

所以,shift()O(N) 的!!! 吐血 QAQ


同理,unshift() 也是 O(N) 的,不过,pop()push()O(1),也就是说把数组当做栈是没有问题的。


我就是想用队列怎么办!


没想到作为一个 JSer,想好好地用个队列都这么难……QAQ


找到了一个队列实现,详情见注释。


/*

Queue.js

A function to represent a queue

Created by Kate Morley - http://code.iamkate.com/ - and released under the terms
of the CC0 1.0 Universal legal code:

http://creativecommons.org/publicdomain/zero/1.0/legalcode

*/

/* Creates a new queue. A queue is a first-in-first-out (FIFO) data structure -
* items are added to the end of the queue and removed from the front.
*/
function Queue(){

// initialise the queue and offset
var queue = [];
var offset = 0;

// Returns the length of the queue.
this.getLength = function(){
return (queue.length - offset);
}

// Returns true if the queue is empty, and false otherwise.
this.isEmpty = function(){
return (queue.length == 0);
}

/* Enqueues the specified item. The parameter is:
*
* item - the item to enqueue
*/
this.enqueue = function(item){
queue.push(item);
}

/* Dequeues an item and returns it. If the queue is empty, the value
* 'undefined' is returned.
*/
this.dequeue = function(){

// if the queue is empty, return immediately
if (queue.length == 0) return undefined;

// store the item at the front of the queue
var item = queue[offset];

// increment the offset and remove the free space if necessary
if (++ offset * 2 >= queue.length){
queue = queue.slice(offset);
offset = 0;
}

// return the dequeued item
return item;

}

/* Returns the item at the front of the queue (without dequeuing it). If the
* queue is empty then undefined is returned.
*/
this.peek = function(){
return (queue.length > 0 ? queue[offset] : undefined);
}

}

把最初代码中的数组改为 Queue,现在终于可以通过了。:)



收起阅读 »

如何“优雅”地修改 node_modules 下的代码?

在实际开发过程中当我们遇到 node_modules 中的 A 包有 bug 时候,通常开发者有几个选择: 方法一:给 A 包提 issue 等待他人修复并发布:做好石沉大海或修复周期很长的准备。 方法二:给 A 包提 mr 自行修复并等待发布:很棒,不过你最...
继续阅读 »

在实际开发过程中当我们遇到 node_modules 中的 A 包有 bug 时候,通常开发者有几个选择:


方法一:给 A 包提 issue 等待他人修复并发布:做好石沉大海或修复周期很长的准备。


方法二:给 A 包提 mr 自行修复并等待发布:很棒,不过你最好祈祷作者发版积极,并且新版本向下兼容。


方法三:把 A 包的源码拖出来自己维护:有点暴力且事后维护成本较高,不过应急时也能勉强接受。


等等,可如果出问题的包是“幽灵依赖”呢,比如项目的依赖链是: A -> B -> C,此时 C 包有 bug。那么上面三个方法的改动需要同时影响到 A、B、C 三个包,修复周期可能就更长了,可是你今晚就要上线啊,这可怎么办?


1


上线要紧,直接手动修改 node_modules 下的代码给缺陷包打个临时补丁吧,可问题又来了,改动只能在本地生效,构建却在云端, 积极的同学开始写起了脚本,然后陷入一个个坑里...


上述场景下即可考虑使用 patch-package 这个包,假设我们现在的源码结构如下所示:


├── node_modules  
│ └── lodash
│ └── toString.js
├── src
│ └── app.js // 依赖 lodash 的 toString 方法
└── package.json

node_modules/lodash/toString.js


var baseToString = require('./_baseToString')

function toString(value) {
return value == null ? '' : baseToString(value);
}

module.exports = toString;

src/app.js


const toString = require('lodash/toString')
console.log(toString(123));

假设现在需要修改 node_modules/lodash/toString.js 文件,只需要遵循以下几步即可“优雅”完成修改:


第一步:安装依赖


yarn add patch-package postinstall-postinstall -D

第二步:修改 node_modules/lodash/toString.js 文件


function toString(value) {
console.log('it works!!!'); // 这里插入一行代码
return value == null ? '' : baseToString(value);
}

module.exports = toString;

第三步:生成修改文件


npx patch-package lodash

这一步运行后会生成 patches/lodash+4.17.21.patch,目录结构变成下面这样:


├── node_modules  
│ └── lodash
│ └── toString.js
├── patches
│ └── lodash+4.17.21.patch
├── src
│ └── app.js
└── package.json

其中 .patch 文件内容如下:


diff --git a/node_modules/lodash/toString.js b/node_modules/lodash/toString.js
index daaf681..8308e76 100644
--- a/node_modules/lodash/toString.js
+++ b/node_modules/lodash/toString.js
@@ -22,6 +22,7 @@ var baseToString = require('./_baseToString');
* // => '1,2,3'
*/
function toString(value) {
+ console.log('it works!!!');
return value == null ? '' : baseToString(value);
}

第四步:修改 package.json 文件


"scripts": {
+ "postinstall": "patch-package"
}

最后重装一下依赖,测试最终效果:


rm -rf node_modules
yarn
node ./src/app.js

// it works!!!
// 123

可以看到,即便重装依赖,我们对 node_modules 下代码的修改还是被 patch-package 还原并最终生效。


至此我们便完成一次临时打补丁的操作,不过这并非真正优雅的长久之计,长期看还是需要彻底修复第三方包缺陷并逐步移除项目中的 .patch 文件。


作者:王力国
链接:https://juejin.cn/post/7022252841116893215

收起阅读 »

封装一个底部导航

前言 在我们日常项目开发中,我们在做移动端的时候会涉及到地步导航功能,所以封装了这个底部导航组件。 底部导航 BottomNav组件属性 1. value选中值(即选中BottomNavPane的name值)值为字符串类型非必填默认为第一个BottomNavP...
继续阅读 »

前言


在我们日常项目开发中,我们在做移动端的时候会涉及到地步导航功能,所以封装了这个底部导航组件。

底部导航


BottomNav组件属性


1. value
选中值(即选中BottomNavPane的name值)
值为字符串类型
非必填默认为第一个BottomNavPane的name

2. lazy
未显示的内容面板是否延迟渲染
值为布尔类型
默认为false

样式要求
组件外面需要包裹可以相对定位的元素,增加样式:position: relative

BottomNavPane组件属性


1. name
英文名称
值为字符串类型
必填

2. icon
导航图标名称
值为字符串类型
值需要与src/assets/icon目录下svg文件的名称一致(name值不含“.svg”后缀)
必填

3. label
导航图标下面显示的文字
值为字符串类型
必填

4. scroll
是否有滚动条
值为布尔类型
默认值为:true

示例


<template>
<div class="bottom-nav-wrap">
<BottomNav v-model="curNav" :lazy="true">
<BottomNavPane name="home" label="首页" icon="home">
<h1>首页内容</h1>
</BottomNavPane>
<BottomNavPane name="oa" label="办公" icon="logo">
<h1>办公内容</h1>
</BottomNavPane>
<BottomNavPane name="page2" label="我的" icon="user">
<h1>个人中心</h1>
</BottomNavPane>
</BottomNav>
</div>
</template>

<script>
import { BottomNav, BottomNavPane } from '@/components/m/bottomNav'

export default {
name: 'BottomNavDemo',
components: {
BottomNav,
BottomNavPane
},
data () {
return {
curNav: ''
}
}
}
</script>

<style lang="scss" scoped>
.bottom-nav-wrap {
position: absolute;
top: $app-title-bar-height;
bottom: 0;
left: 0;
right: 0;
}
</style>

BottomNav.vue


<template>
<div class="bottom-nav">
<div class="nav-pane-wrap">
<slot></slot>
</div>
<div class="nav-list">
<div class="nav-item"
v-for="info in navInfos"
:key="info.name"
:class="{active: info.name === curValue}"
@click="handleClickNav(info.name)">
<Icon class="nav-icon" :name="info.icon"></Icon>
<span class="nav-label">{{info.label}}</span>
</div>
</div>
</div>
</template>
<script>
import { generateUUID } from '@/assets/js/utils.js'
export default {
name: 'BottomNav',
props: {
// 选中导航值(导航的英文名)
value: String,
// 未显示的内容面板是否延迟渲染
lazy: {
type: Boolean,
default: false
}
},
data () {
return {
// 组件实例的唯一ID
id: generateUUID(),
// 当前选中的导航值(导航的英文名)
curValue: this.value,
// 导航信息数组
navInfos: [],
// 导航面板vue实例数组
panes: []
}
},
watch: {
value (val) {
this.curValue = val
},
curValue (val) {
this.$eventBus.$emit('CHANGE_NAV' + this.id, val)
this.$emit('cahnge', val)
}
},
mounted () {
this.calcPaneInstances()
},
beforeDestroy () {
this.$eventBus.$off('CHANGE_NAV' + this.id)
},
methods: {
// 计算导航面板实例信息
calcPaneInstances () {
if (this.$slots.default) {
const paneSlots = this.$slots.default.filter(vnode => vnode.tag &&
vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'BottomNavPane')
const panes = paneSlots.map(({ componentInstance }) => componentInstance)
const navInfos = paneSlots.map(({ componentInstance }) => {
// console.log(componentInstance.name, componentInstance)
return {
name: componentInstance.name,
label: componentInstance.label,
icon: componentInstance.icon
}
})
this.navInfos = navInfos
this.panes = panes
if (!this.curValue) {
if (navInfos.length > 0) {
this.curValue = navInfos[0].name
}
} else {
this.$eventBus.$emit('CHANGE_NAV' + this.id, this.curValue)
}
}
},
// 导航点击事件处理方法
handleClickNav (val) {
this.curValue = val
}
}
}
</script>
<style lang="scss" scoped>
.bottom-nav {
display: flex;
flex-direction: column;
height: 100%;
.nav-pane-wrap {
flex: 1;
}
.nav-list {
flex: none;
display: flex;
height: 90px;
background-color: #FFF;
align-items: center;
border-top: 1px solid $base-border-color;
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
line-height: 1;
text-align: center;
color: #666;
.nav-icon {
font-size: 40px;/*yes*/
}
.nav-label {
margin-top: 6px;
font-size: 24px;/*yes*/
}
&.active {
position: relative;
color: $base-color;
}
}
}
}
</style>

BottomNavPane.vue


<template>
<div v-if="canInit" class="bottom-nav-pane" v-show="show">
<Scroll v-if="scroll">
<slot></slot>
</Scroll>
<slot v-else></slot>
</div>
</template>
<script>
import Scroll from '@/components/base/scroll'

export default {
name: 'BottomNavPane',
components: {
Scroll
},
props: {
// 页签英文名称
name: {
type: String,
required: true
},
// 页签显示的标签
label: {
type: String,
required: true
},
// 图标名称
icon: {
type: String,
required: true
},
// 是否有滚动条
scroll: {
type: Boolean,
default: true
}
},
data () {
return {
// 是否显示
show: false,
// 是否已经显示过
hasShowed: false
}
},
computed: {
canInit () {
return (!this.$parent.lazy) || (this.$parent.lazy && this.hasShowed)
}
},
created () {
this.$eventBus.$on('CHANGE_NAV' + this.$parent.id, val => {
if (val === this.name) {
this.show = true
this.hasShowed = true
} else {
this.show = false
}
})
}
}
</script>
<style lang="scss" scoped>
.bottom-nav-pane {
height: 100%;
position: relative;
}
</style>

/**
* 底部图标导航组件
*/
import BaseBottomNav from './BottomNav.vue'
import BaseBottomNavPane from './BottomNavPane.vue'
export const BottomNav = BaseBottomNav
export const BottomNavPane = BaseBottomNavPane


「欢迎在评论区讨论」



收起阅读 »

如何优雅的集成Google pay到你的项目中

官方集成文档 官方集成文档 官方集成文档第一步:javadependencies { def billing_version = "3.0.0" implementation 'com.android.billingcli...
继续阅读 »

官方集成文档 官方集成文档 官方集成文档

第一步:

java

dependencies {
def billing_version = "3.0.0"

implementation 'com.android.billingclient:billing:$billing_version'
}

kotlin

dependencies {
def billing_version = "3.0.0"

implementation 'com.android.billingclient:billing-ktx:$billing_version'
}

第二部:

private PurchasesUpdatedListener purchaseUpdateListener = new PurchasesUpdatedListener() {
@Override
void onPurchasesUpdated(BillingResult billingResult, List<Purchase> purchases) {
// To be implemented in a later section.
}
};

private BillingClient billingClient = BillingClient.newBuilder(activity)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build();

第三部:

billingClient.startConnection(new BillingClientStateListener() {
@Override
public void onBillingSetupFinished(BillingResult billingResult) {
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
//链接成功
}
}
@Override
public void onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
// 链接失败触发,触发重连机制
}
});

第四部: 请求自己服务器,拿到对应的商品列表,这里拿到的商品列表要和Google后台配置的商品列表ID一致。

List<String> skuList = new ArrayList<> ();
skuList.add("premium_upgrade");
SkuDetailsParams.Builder params = SkuDetailsParams.newBuilder();
params.setSkusList(skuList).setType(SkuType.INAPP);
billingClient.querySkuDetailsAsync(params.build(),
new SkuDetailsResponseListener() {
@Override
public void onSkuDetailsResponse(BillingResult billingResult,
List<SkuDetails> skuDetailsList) {
// Process the result.
}
});

第五步: 调起Google 支付界面

// An activity reference from which the billing flow will be launched.
Activity activity = ...;

// Retrieve a value for "skuDetails" by calling querySkuDetailsAsync().
BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
.setSkuDetails(skuDetails)
.build();
int responseCode = billingClient.launchBillingFlow(activity, billingFlowParams).getResponseCode();

// Handle the result.

在这里插入图片描述在这里插入图片描述

此处说明你的支付已经完成,回调到你最开始初始化的onPurchasesUpdated方法里边。

if (billingResult.getResponseCode() == BillingResponseCode.OK
&& purchases != null) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
}
} else if (billingResult.getResponseCode() == BillingResponseCode.USER_CANCELED) {
// Handle an error caused by a user cancelling the purchase flow.
} else {
// Handle any other error codes.
}

第六步: Google 支付分为两个部分,购买和验证,比如我们的产品是虚拟货币,是一次性消耗产品,使用下边方法去验证消耗。 最好的处理方法是请求后台服务器,让后台做一个验证,然后我们再去验证消费(保证安全性)

void handlePurchase(Purchase purchase) {
// Purchase retrieved from BillingClient#queryPurchases or your PurchasesUpdatedListener.
Purchase purchase = ...;

// Verify the purchase.
// Ensure entitlement was not already granted for this purchaseToken.
// Grant entitlement to the user.

ConsumeParams consumeParams =
ConsumeParams.newBuilder()
.setPurchaseToken(purchase.getPurchaseToken())
.build();

ConsumeResponseListener listener = new ConsumeResponseListener() {
@Override
public void onConsumeResponse(BillingResult billingResult, String purchaseToken) {
if (billingResult.getResponseCode() == BillingResponseCode.OK) {
// Handle the success of the consume operation.
}
}
};

billingClient.consumeAsync(consumeParams, listener);
}

因为网络原因可能会出现,掉单的问题,所以就会出现补单的逻辑。

调起支付之前

BillingResult billingResult = billingClient.launchBillingFlow(this, billingFlowParams);
if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) {
queryHistory();
}
/**
* 查询历史记录,有没校验的开始支付验证流程
*/
public void queryHistory() {
if (billingClient == null) {
return;
}
//google消费失败的补单
List<Purchase> purchases = billingClient.queryPurchases(BillingClient.SkuType.INAPP).getPurchasesList();
if (purchases != null && !purchases.isEmpty()) {
for (Purchase purchase : purchases) {
handlePurchase(purchase);
return;
}
}
}

收起阅读 »

android 如何优雅的集成 Razorpay

请在您的应用build.gradle文件中添加以下依赖项:repositories { mavenCentral() } dependencies { implementation 'com.razorpay:checkout...
继续阅读 »
  1. 请在您的应用build.gradle文件中添加以下依赖项:
repositories {   
mavenCentral()
}
dependencies {
implementation 'com.razorpay:checkout:1.5.16'
}

在这里插入图片描述

  1. Checkout并将付款详细信息和选项作为传递JSONObject。确保您添加了order_id在步骤1中生成的(一般是后台生成)
 public void startPayment() {
/*
You need to pass current activity in order to let Razorpay create CheckoutActivity
*/
final Activity activity = this;
final Checkout co = new Checkout();

try {
JSONObject options = new JSONObject();
options.put("name", "Razorpay Corp");
options.put("description", "Demoing Charges");
//You can omit the image option to fetch the image from dashboard
options.put("image", "https://s3.amazonaws.com/rzp-mobile/images/rzp.png");
options.put("order_id", "order_DBJOWzybf0sJbb");//这一部很重要,是后台调用Razorpay的接口生成的,否则支付成功的状态不对

options.put("currency", "INR");
options.put("amount", "100");
options.put("payment_capture", "1");

JSONObject preFill = new JSONObject();
preFill.put("email", "test@razorpay.com");
preFill.put("contact", "9876543210");

options.put("prefill", preFill);

co.open(activity, options);
} catch (Exception e) {
Toast.makeText(activity, "Error in payment: " + e.getMessage(), Toast.LENGTH_SHORT)
.show();
e.printStackTrace();
}
}
  1. 回调处理支付状态,处理成功和错误事件
  public class PaymentActivity extends Activity implements PaymentResultListener{
@Override
public void onPaymentSuccess(String s, PaymentData paymentData) {
}

@Override
public void onPaymentError(int i, String s, PaymentData paymentData) {

}
}
  1. 混淆
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}

-keepattributes JavascriptInterface
-keepattributes *Annotation*

-dontwarn com.razorpay.**
-keep class com.razorpay.** {*;}

-optimizations !method/inlining/*

-keepclasseswithmembers class * {
public void onPayment*(...);
}

收起阅读 »

Android-关于设备唯一ID的奇技淫巧

前言最近在二开项目国际版客户的功能,我们项目中默认是有一个游客登录的,一般大家都是取Android设备的唯一ID上传服务器,然后服务器给你分配一个用户信息.但是Google在高版本对于设备唯一Id的获取简直限制到了极点.以前我都是直接获取IMEI来作为设备的唯...
继续阅读 »

前言

最近在二开项目国际版客户的功能,我们项目中默认是有一个游客登录的,一般大家都是取Android设备的唯一ID上传服务器,然后服务器给你分配一个用户信息.但是Google在高版本对于设备唯一Id的获取简直限制到了极点.

以前我都是直接获取IMEI来作为设备的唯一标识

var imei: String = ""
val tm: TelephonyManager =
context.getSystemService(Service.TELEPHONY_SERVICE) as TelephonyManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
imei = tm.imei
} else {
imei = tm.deviceId
}
Log.e("TAG","$imei")

imei和deviceId都有一个重载函数,主要是区别双卡的一个情况

image.png

Android6.0以后我们加一个动态权限即可,但是用户只要拒绝就没办法获取了,不过一般来说我们会有个弹框来引导用户同意

<uses-permission android:name="android.permission.READ_PHONE_STATE"/>

Android 10.0 谷歌再一次收紧权限

image.png

<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE" />
如果你把他放到AndroidManifest会报错

image.png

官方也说了,你要是弟弟(9.0 以下)我给你报null,你要是10.0 还敢用我就直接抛异常. 后面在stackoverflow上面找到了一个办法

public class DeviceUuidFactory {
protected static final String PREFS_FILE = "device_id.xml";
protected static final String PREFS_DEVICE_ID = "device_id";
protected static UUID uuid;

public DeviceUuidFactory(Context context) {
if( uuid ==null ) {
synchronized (DeviceUuidFactory.class) {
if( uuid == null) {
final SharedPreferences prefs = context.getSharedPreferences( PREFS_FILE, 0);
final String id = prefs.getString(PREFS_DEVICE_ID, null );
if (id != null) {
// Use the ids previously computed and stored in the prefs file
uuid = UUID.fromString(id);
} else {
final String androidId = Secure.getString(context.getContentResolver(), Secure.ANDROID_ID);
// Use the Android ID unless it's broken, in which case fallback on deviceId,
// unless it's not available, then fallback on a random number which we store
// to a prefs file
try {
if () {
uuid = UUID.nameUUIDFromBytes(androidId.getBytes("utf8"));
} else {
@SuppressLint("MissingPermission") final String deviceId = ((TelephonyManager) context.getSystemService( Context.TELEPHONY_SERVICE )).getDeviceId();
uuid = deviceId!=null ? UUID.nameUUIDFromBytes(deviceId.getBytes("utf8")) : UUID.randomUUID();
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
// Write the value out to the prefs file
prefs.edit().putString(PREFS_DEVICE_ID, uuid.toString() ).commit();
}
}
}
}
}
/**
* Returns a unique UUID for the current android device. As with all UUIDs, this unique ID is "very highly likely"
* to be unique across all Android devices. Much more so than ANDROID_ID is.
*
* The UUID is generated by using ANDROID_ID as the base key if appropriate, falling back on
* TelephonyManager.getDeviceID() if ANDROID_ID is known to be incorrect, and finally falling back
* on a random UUID that's persisted to SharedPreferences if getDeviceID() does not return a
* usable value.
*
* In some rare circumstances, this ID may change. In particular, if the device is factory reset a new device ID
* may be generated. In addition, if a user upgrades their phone from certain buggy implementations of Android 2.2
* to a newer, non-buggy version of Android, the device ID may change. Or, if a user uninstalls your app on
* a device that has neither a proper Android ID nor a Device ID, this ID may change on reinstallation.
*
* Note that if the code falls back on using TelephonyManager.getDeviceId(), the resulting ID will NOT
* change after a factory reset. Something to be aware of.
*
* Works around a bug in Android 2.2 for many devices when using ANDROID_ID directly.
*

*
* @return a UUID that may be used to uniquely identify your device for most purposes.
*/
public String getDeviceUuid() {
return uuid.toString();
}
}

这个类的意思是,首先他会去SharedPreferences查询有没有,没有的话再去查询ANDROID_ID,后面判断了是否是9774d56d682e549c,因为有的厂商手机好多ANDROID_ID都是这个,所以判断一下,防止好几万个人用一个账号,不然那就笑嘻嘻了,后面如果真等于9774d56d682e549c了,就通过下面的

@SuppressLint("MissingPermission") final String deviceId = ((TelephonyManager) context.getSystemService( Context.TELEPHONY_SERVICE )).getDeviceId();

来获取DeviceId,但是这个AndroidId虽然可以是获取了,但是会受限于签名文件,如果在相同设备上运行但是应用签名不一样,获取到的ANDROID_ID就会不一样,比如谷歌商店会二次签名apk,他获取的id可能就是159951,后面我们要测试时,上传到内部测试的包好像会再次签名,这次获取的可能是951159,然后我们用android提供的签名文件可能就是147258,我们自己新建一个签名文件就可能是258369,总之这个ANDROID_ID会受制于签名文件

反正最后我们国际版用到了Mob的推送服务,推送中有一个只推送单个设备,然后我们就设想,直接用Mob的唯一设备Id和我们服务器绑定如何,后面一经测试,效果很好,直接跳过大堆测试和寻找时间

//阿里云唯一设备id
val deviceId = PushServiceFactory.getCloudPushService().deviceId

//Mob
CloudPushService pushService = PushServiceFactory.getCloudPushService();
pushService.register(applicationContext, new CommonCallback() {
@Override
public void onSuccess(String response) {
Log.e("TAG", "onSuccess: "+response);
}

@Override
public void onFailed(String errorCode, String errorMessage) {
}
});

//友盟唯一设备ID
val pushAgent = PushAgent.getInstance(context)
pushAgent.register(object : UPushRegisterCallback {
override fun onSuccess(deviceToken: String) {
//注册成功会返回deviceToken deviceToken是推送消息的唯一标志
Log.i(TAG, "注册成功:deviceToken:--> $deviceToken")
}

override fun onFailure(errCode: String, errDesc: String) {
Log.e(TAG, "注册失败:--> code:$errCode, desc:$errDesc")
}
})

这是常用的第三方服务获取唯一设备ID的方法,其实有的人可能用的跟我不一样,基本上文档里面都有,真找不到可以去问问客服

终于解决一个让人头疼的问题了,下班,回家

收起阅读 »

一条SQL查询语句是如何执行的

sql
背景我们执行一条查询语句时,对客户端是一个很简单的过程,但对服务端(MySQL)内部却涉及到很复杂的组件和逻辑,当出现一些比较复杂的SQL问题时,如果不理解其内部执行的原理,将会很难去定位和解决问题正文先聊聊MySQL的逻辑架构大体来说,MySQL可以分为 S...
继续阅读 »

背景

我们执行一条查询语句时,对客户端是一个很简单的过程,但对服务端(MySQL)内部却涉及到很复杂的组件和逻辑,当出现一些比较复杂的SQL问题时,如果不理解其内部执行的原理,将会很难去定位和解决问题

正文

先聊聊MySQL的逻辑架构

image.png

大体来说,MySQL可以分为 Server层和存储引擎层两部分

Server层

  • 包括连接器、查询缓存、分析器、优化器、执行器
  • 实现了MySQL 的大多数核心服务功能,所有的包括查询解析、分析、优化、缓存以及所有的内置函数,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图

存储引擎层

  • 负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。也就是说,你执行 create table 建表的时候,如果不指定引擎类型,默认使用的就是 InnoDB。不过,你也可以通过指定存储引擎的类型来选择别的引擎,比如在 create table 语句中使用 engine=memory, 来指定使用内存引擎创建表。
  • 不同存储引擎的表数据存取方式不同,支持的功能也不同,不同的存储引擎共用一个 Server 层
  • Server 层通过存储引擎API来与它们交互,这些接口屏蔽了不同存储引擎之间的差异,使得这些差异对上层的查询尽可能的透明。这些API包含几十个底层函数,用于执行诸如"开始一个事务"或者"根据主键提取一行记录"等操作,存储引擎不能解析SQL,互相之间也不能通信。只是简单地响应上层服务器的请求

SQL查询语句的执行流程

step1:使用连接器与客户端建立连接

首先客户端会先连接到指定数据库上,这时候接待的就是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接

我们在Linux上会通过以下方式与MySQL建立连接,连接命令中的"mysql"是客户端工具

mysql -h$ip -P$port -u$user -p

在完成经典的TCP 握手后,连接器就要开始认证你的身份,这个时候用的就是你输入的用户名和密码:

  • 如果用户名或密码不对,你就会收到一个"Access denied for user"的错误,然后客户端程序结束执行
  • 如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。这就意味着一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置

连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,可以输入show processlist 命令看到全部的连接,Command 列显示为“Sleep”的这一行,就表示现在系统里面有一个空闲连接

企业微信截图_16348234821060.png

客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query,这时候如果你要继续,就需要重连,然后再执行请求了

数据库的连接分为有长连接和短连接:

  • 长连接:指连接成功后,如果客户端持续有请求,则一直使用同一个连接
  • 短连接:指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个

建立连接的过程通常是比较复杂的,因此建议在使用中要尽量减少建立连接的动作,也就是尽量使用长连接

但是全部使用长连接后,你可能会发现,有些时候 MySQL 占用内存涨得特别快,这是因为 MySQL 在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是 MySQL 异常重启了

怎么解决这个问题呢?你可以考虑以下两种方案:

  • 定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连
  • 如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态

step2:查询缓存,有就直接返回查询结果

连接建立完成后,你就可以执行 select 语句了。执行逻辑就会来到第二步:查询缓存

MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端

如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高

但是大多数情况下我会建议你不要使用查询缓存,为什么呢?因为查询缓存往往弊大于利。查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。除非你的业务就是有一张静态表,很长时间才会更新一次。比如,一个系统配置表,那这张表上的查询才适合使用查询缓存

好在 MySQL 也提供了这种“按需使用”的方式。你可以将参数 query_cache_type 设置成 DEMAND,这样对于默认的 SQL 语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定,像下面这个语句一样:

select SQL_CACHE * from T where ID=10;

注意:MySQL 8.0 版本直接将查询缓存的整块功能删掉了

step3:使用分析器解析你的SQL,知道你要做什么

分析器如果没有命中查询缓存,就要开始真正执行语句了。MySQL首先需要知道你要做什么,因此需要对 SQL 语句做解析

分析器先会做“词法分析”:你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。MySQL 从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名 T”,把字符串“ID”识别成“列 ID”

做完了这些识别以后,就要做“语法分析”:根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果你的语句不对,就会收到“You have an error in your SQL syntax”的错误提醒,比如下面这个语句 select 少打了开头的字母“s”

mysql> elect * from t where ID=1;

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect * from t where ID=1' at line 1,

一般语法错误会提示第一个出现错误的位置,所以你要关注的是紧接“use near”的内容

step4:使用优化器确定语句的执行方案

经过了分析器,MySQL 就知道你要做什么了。在开始执行之前,还要先经过优化器的处理

优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序

比如执行下面这样的语句,这个语句是执行两个表的 join:

select * from t1 join t2 using(ID) where t1.c=10 and t2.d=20;
  • 既可以先从表 t1 里面取出 c=10 的记录的 ID 值,再根据 ID 值关联到表 t2,再判断 t2 里面 d 的值是否等于 20。
  • 也可以先从表 t2 里面取出 d=20 的记录的 ID 值,再根据 ID 值关联到 t1,再判断 t1 里面 c 的值是否等于 10。

这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。优化器阶段完成后,这个语句的执行方案就确定下来了

step5:使用执行器执行语句

MySQL 通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句

开始执行的时候,要先判断你对这个表 T 有没有执行查询的权限

  • 如果没有,就会返回没有权限的错误,如下所示 (在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。查询也会在优化器之前调用 precheck 验证权限)。
mysql> elect * from t where ID=1; 

ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'elect * from t where ID=1' at line 1
  • 如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口

比如我们这个例子中的表 T 中,ID 字段没有索引,那么执行器的执行流程是这样的:

  1. 调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果是则将这行存在结果集中;
  2. 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
  3. 执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。

对于有索引的表,执行的逻辑也差不多:

  1. 第一次调用的是“取满足条件的第一行”这个接口,
  2. 之后循环取“满足条件的下一行”这个接口,这些接口都是引擎中已经定义好的。

你会在数据库的慢查询日志中看到一个 rows_examined 的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。但在有些场景下,执行器调用一次,在引擎内部则扫描了多行,因此引擎扫描行数跟 rows_examined 并不是完全相同的

总结

  • MySQL的逻辑架构包含两部分,server层存储引擎层

  • SQL查询语句的执行依赖于这些核心组件:先通过连接器与客户端进行连接,随后查询是否可以应用缓存,可以直接返回结果,不可以则使用解析器分析SQL,然后利用优化器确定执行方案,最终利用执行器存储引擎执行SQL获取结果


收起阅读 »

熬夜再战Android之修炼Kotlin-【findView】篇

前提 前面我们学了Kotlin语言,趁热打铁我们试试Kotlin在Android中的应用。 如果是新手,请先学完Android基础。 推荐先看小空之前写的熬夜Android系列,再来尝试。 👉实践过程 😜方式一 使用扩展,如果你第一次创建项目的时候选择的是Ko...
继续阅读 »

前提


前面我们学了Kotlin语言,趁热打铁我们试试Kotlin在Android中的应用。


如果是新手,请先学完Android基础。


推荐先看小空之前写的熬夜Android系列,再来尝试。


👉实践过程


😜方式一


使用扩展,如果你第一次创建项目的时候选择的是Kotlin语言,则默认带有该插件,如果选择的默认是Java语言,则需要手动添加。


该方式的优点就是对编程人员来说可以直接拿到View的id,不需要定义变量和findViewById。


在项目的build文件开头添加【app->build.gradle】


apply plugin: 'kotlin-android-extensions'


image.png
之后在Activity中添加import,固定格式的。


import kotlinx.android.synthetic.main.修改为你的布局名称.*


【*】代表的是该布局下的所有控件,如果只需要指定控件,将【*】改为控件名即可,如下示例


import kotlinx.android.synthetic.main.activity_main.*


import kotlinx.android.synthetic.main.activity_main.mytextview


之后就可以直接在代码中使用控件的id来进行响应操作了。


<TextView
        android:id="@+id/myText"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="8dp"
        android:text="芝麻粒儿和空名先生"
        android:textStyle="bold"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

但是方便的同时,问题的隐患也存在着,在底层仍然回归原始使用的是findViewById,所以会对性能有影响


😜方式二


使用findViewById,这个仍然有两种方式,方式一是【lateinit】关键字,但是存在坑,详情看这(在 Kotlin 代码中慎用 lateinit 属性zhuanlan.zhihu.com/p/31297995
推荐方式二使用【lazy】,如下:



private val myText: TextView by lazy { findViewById<TextView>(R.id.myText) }
private val myImg: ImageView by lazy { findViewById<ImageView>(R.id.myImg) }
private val imageView: ImageView by lazy { findViewById<ImageView>(R.id.imageView) }
private val myBtn: Button by lazy { findViewById<Button>(R.id.myBtn) }

上面是在Activity中的使用,而在Fragment中又怎么用呢?


class LoginFragment : Fragment() {
  private var myText: TextView? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        //设置布局
        return inflater.inflate(R.layout.login_fragment, container, false)
    }
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
   myText = view.findViewById(R.id.myText)
        myText?.setText("动态修改文本")
    }
}

除了上面的写法,Android官方给了我们更好的解决方案:


class LoginFragment : Fragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        //设置布局
        return inflater.inflate(R.layout.login_fragment, container, false)
    }

    private lateinit var myText: TextView
    private lateinit var myImg: ImageView
    private lateinit var imageView: ImageView
    private lateinit var myBtn: Button

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        //查找view
        myText = view.findViewById(R.id.myText)
        myImg = view.findViewById(R.id.myImg)
        imageView = view.findViewById(R.id.imageView)
        myBtn = view.findViewById(R.id.myBtn)
    }
}

还有另外方式就是使用自动化插件,在【File-Setting-Plugins】市场搜索关键字【findview】,看最新的几个,挑选自己用的顺手的使用即可。


作者:芝麻粒儿
链接:https://juejin.cn/post/7021457050496401415
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

翻车了,字节一道 Fragment面试题

一道面试题 前段时间面试,面试官先问了一下fragment的生命周期,我一看这简单呀,直接按照下图回答 面试官点点头,然后问,如果Activity里面有一个fragment,那么启动他们时,他们的生命周期加载顺序是什么? 所以今天,我们好好了解了解这个用得...
继续阅读 »

一道面试题


前段时间面试,面试官先问了一下fragment的生命周期,我一看这简单呀,直接按照下图回答


img


面试官点点头,然后问,如果Activity里面有一个fragment,那么启动他们时,他们的生命周期加载顺序是什么?


在这里插入图片描述


所以今天,我们好好了解了解这个用得非常多,但是对底层不是很理解的fragment吧


首先回答面试官的问题,Fragment 的 start与activity 的start 的调用时机



调用顺序:


D/MainActivity: MainActivity:


D/MainActivity: onCreate: start


D/MainFragment: onAttach:


D/MainFragment: onCreate:




D/MainActivity: onCreate: end


D/MainFragment: onCreateView:


D/MainFragment: onViewCreated:


D/MainFragment: onActivityCreated:


D/MainFragment: onViewStateRestored:


D/MainFragment: onCreateAnimation:


D/MainFragment: onCreateAnimator:


D/MainFragment: onStart:




D/MainActivity: onStart:


D/MainActivity: onResume:


D/MainFragment: onResume:



可以看到Activity 在oncreate开始时,Fragment紧接着attach,create,然后activity执行完毕onCreate方法


此后都是Fragment在执行,直到onStart方法结束


然后轮到Activity,执行onStart onResume


也就是,Activity 创建的时候,Fragment一同创建,同时Fragment优先在后台先展示好,最后Activity带着Fragment一起展示到前台。


是什么?


Fragment中文翻译为”碎片“,在手机中,每一个Activity作为一个页面,有时候太大了,尤其是在平板的横屏下,我们希望左半边是一根独立模块,右半边是一个独立模块,比如一个新闻app,左边是标题栏,右边是显示内容


此时就非常适合Fragment


Fragment是内嵌入Activity中的,可以在onCreateView中加载自定义的布局,使用LayoutInflater,然后Activity持有FragmentManager对Fragment进行控制,下图是他的代码框架


img


我们的Activity一般是用AppCompatActivity,而AppCompatActivity继承了FragmentActivity


public class AppCompatActivity extends FragmentActivity implements AppCompatCallback,
TaskStackBuilder.SupportParentable, ActionBarDrawerToggle.DelegateProvider {

也就是说Activity之所支持fragment,是因为有FragmentActivity,他内部有一个FragmentController,这个controller持有一个FragmentManager,真正做事的就是这个FragmentManager的实现类FragmentManagerImpl


整体架构


回到我们刚才的面试题,关于生命周期绝对是重中之重,但是实际上,生命周期本质只是被其他地方的方法被动调用而已,关键是Fragment自己的状态变化了,才会回调生命周期方法,所以我们来看看fragment的状态转移


static final int INITIALIZING = 0;     初始状态,Fragment 未创建
static final int CREATED = 1; 已创建状态,Fragment 视图未创建
static final int ACTIVITY_CREATED = 2; 已视图创建状态,Fragment 不可见
static final int STARTED = 3; 可见状态,Fragment 不处于前台
static final int RESUMED = 4; 前台状态,可接受用户交互

fragment有五个状态,


调用过程如下


img


Fragment的状态转移过程主要受到宿主,事务的影响,宿主一般就是Activity,在我们刚刚的题目中,看到了Activity与Fragment的生命周期交替执行,本质上就是,Activity执行完后通知了Fragment进行状态转移,而Fragment执行了状态转移后对应的回调了生命周期方法


下图可以更加清晰


img


宿主改变Fragment状态


那么我们不禁要问,Activity如何改变Fragment的状态?


我们知道Activity继承于FragmentActivity,最终是通过持有的FragmentManager来控制Fragment,我们去看看


FragmentActivity
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
mFragments.attachHost(null /*parent*/);

super.onCreate(savedInstanceState);
...
mFragments.dispatchCreate();
}

可以看到,onCreate方法中执行了mFragments.dispatchCreate();,看起来像是通知Fragment的onCreate执行,这也印证了我们开始时的周期回调顺序


D/MainActivity: MainActivity: 
D/MainActivity: onCreate: start // 进入onCreate
D/MainFragment: onAttach: // 执行mFragments.dispatchCreate();
D/MainFragment: onCreate:
D/MainActivity: onCreate: end // 退出onCreate

类似的FragmentActivity在每一个生命周期方法中都做了相同的事情


@Override
protected void onDestroy() {
super.onDestroy();

if (mViewModelStore != null && !isChangingConfigurations()) {
mViewModelStore.clear();
}

mFragments.dispatchDestroy();
}

我们进入dispatchCreate看看,


Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}

//内部修改了两个状态
public void dispatchCreate() {
mStateSaved = false;
mStopped = false;
dispatchStateChange(Fragment.CREATED);

private void dispatchStateChange(int nextState) {
try {
mExecutingActions = true;
moveToState(nextState, false);// 转移到nextState
} finally {
mExecutingActions = false;
}
execPendingActions();
}
//一路下来会执行到
void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {
// Fragments that are not currently added will sit in the onCreate() state.
if ((!f.mAdded || f.mDetached) && newState > Fragment.CREATED) {
newState = Fragment.CREATED;
}
if (f.mRemoving && newState > f.mState) {
if (f.mState == Fragment.INITIALIZING && f.isInBackStack()) {
// Allow the fragment to be created so that it can be saved later.
newState = Fragment.CREATED;
} else {
// While removing a fragment, we can't change it to a higher state.
newState = f.mState;
}
}
...
}

可以看到上面的代码,最终执行到 moveToState,通过判断Fragment当前的状态,同时newState > f.mState,避免状态回退,然后进行状态转移


状态转移完成后就会触发对应的生命周期回调方法


事务管理


如果Fragment只能随着Activity的生命周期变化而变化,那就太不灵活了,所以Android给我们提供了一个独立的操作方案,事务


同样由FragManager管理,具体由FragmentTransaction执行,主要是添加删除替换Fragment等,执行操作后,需要提交来保证生效


FragmentManager fragmentManager = ...
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.setReorderingAllowed(true);

transaction.replace(R.id.fragment_container, ExampleFragment.class, null); // 替换Fragment

transaction.commit();// 这里的commit是提交的一种方法

Android给我们的几种提交方式


image-20211020143006614


FragmentTransaction是个挂名抽象类,真正的实现在BackStackState回退栈中,我们看下commit


@Override
public int commit() {
return commitInternal(false);
}
int commitInternal(boolean allowStateLoss) {
if (mCommitted) throw new IllegalStateException("commit already called");
...
mCommitted = true;
if (mAddToBackStack) {
mIndex = mManager.allocBackStackIndex(this);//1
} else {
mIndex = -1;
}
// 入队操作
mManager.enqueueAction(this, allowStateLoss);//2
return mIndex;
}

可以看到,commit的本质就是将事务提交到队列中,这里出现了两个数组,注释1处


ArrayList<BackStackRecord> mBackStackIndices;
ArrayList<Integer> mAvailBackStackIndices;
public int allocBackStackIndex(BackStackRecord bse) {
synchronized (this) {
if (mAvailBackStackIndices == null || mAvailBackStackIndices.size() <= 0) {
if (mBackStackIndices == null) {
mBackStackIndices = new ArrayList<BackStackRecord>();
}
int index = mBackStackIndices.size();
mBackStackIndices.add(bse);
return ind
} else {
int index = mAvailBackStackIndices.remove(mAvailBackStackIndices.size()-1);
mBackStackIndices.set(index, bse);
return index;
}
}
}

mBackStackIndices数组,每个元素是一个回退栈,用来记录索引。比如说,当有五个BackStackState时,移除掉1,3两个,就是在mBackStackIndices将对应元素置为null,然后mAvailBackStackIndices会添加这两个回退栈,记录被移除的回退栈


当下次commit时,就判定mAvailBackStackIndices中的索引,对应的BackStackState一定是null,直接写到这个索引即可


而一组操作都commit到同一个队列里面,所以要么全部完成,要么全部不做,可以保证原子性


注释二处是一个入队操作


public void enqueueAction(OpGenerator action, boolean allowStateLoss
synchronized (this) {
...
mPendingActions.add(action);
scheduleCommit(); // 真正的提交
}
}
public void scheduleCommit() {
synchronized (this) {
boolean postponeReady =
mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
if (postponeReady || pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit); // 发送请求
}
}

这里最后 mHost.getHandler()是拿到了宿主Activity的handler,使得可以在主线程执行,mExecCommit本身是一个线程


我们继续看下这个mExecCommit


Runnable mExecCommit = new Runnable() {
@Override
public void run() {
execPendingActions();
}
};
public boolean execPendingActions() {
ensureExecReady(true);
...
doPendingDeferredStart();
burpActive();
return didSomething;
}
void doPendingDeferredStart() {
if (mHavePendingDeferredStart) {
mHavePendingDeferredStart = false;
startPendingDeferredFragments();
}
}
void startPendingDeferredFragments() {
if (mActive == null) return;
for (int i=0; i<mActive.size(); i++) {
Fragment f = mActive.valueAt(i);
if (f != null) {
performPendingDeferredStart(f);
}
}
}
public void performPendingDeferredStart(Fragment f) {
if (f.mDeferStart) {
f.mDeferStart = false;
moveToState(f, mCurState, 0, 0, false); // 最终到了MoveToState
}
}

还记得我们在宿主改变Fragment状态,里面的最终路径吗?是的,就是这个moveToState,无论是宿主改变Fragment状态,还是事务来改变,最终都会执行到moveToState,然后call对应的生命周期方法来执行,这也是为什么我们要将状态转移作为学习主线,而不是生命周期。


除了commit,可以看到FragmentTransaction有众多对Fragment进行增删改查的方法


image-20211020143442569


都是由BackStackState来执行,最后都会执行到moveToState中


具体是如何改变的,有很多细节,这里不再赘述。


小结


本节我们讲了Fragment在android系统中的状态,那就是通过自身状态转移来回调对应生命周期方法,这块是自动实现的,我们开发时不太需要关注状态转移,只要知道什么时候执行某个生命周期方法,然后再在对应方法中写业务逻辑即可


有两个方法可以让Fragment状态转移,



  • 宿主Activity生命周期内自动修改Fragment状态,回调Fragment的生命周期方法

  • 通过手动提交事务,修改Fragment状态,回调Fragment的生命周期方法

作者:神鹰梦泽
链接:https://juejin.cn/post/7021398731056480269
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Android模块化设计之组件开发规范

最近一直在做基础建设方面的工作,面对这三十多个完全没有规范可言的组件,气的我直接打了一套闪电五连鞭,但打工还得继续,于是想对这些组件建立一套规范,来降低够用、使用、维护以及扯皮成本,本想在网上白嫖一套,可找到的都是一些基础的代码规范,用处不大,于是乎根据自己的...
继续阅读 »

最近一直在做基础建设方面的工作,面对这三十多个完全没有规范可言的组件,气的我直接打了一套闪电五连鞭,但打工还得继续,于是想对这些组件建立一套规范,来降低够用、使用、维护以及扯皮成本,本想在网上白嫖一套,可找到的都是一些基础的代码规范,用处不大,于是乎根据自己的工作经验,总结出一套规范/约定出来,希望能抛砖引玉,各位大佬多多指点和补充~

规范本身就是开发人员之间的约定,没有最权威的,只有最适合的。

版本规范

为保证组件在各个项目中的兼容性问题,约定组件开发版本如下:

AndroidSdk版本

minSdkVersion:21

targetSdkVersion:28

语言环境

开发语言:Kotlin

Kotlin版本:1.4.x

JDK版本:1.8

其他建议版本

AndroidStuido:4.2

gradle tools:4.1.x

gradle:6.7.x

组件命名规范

根据组件的功能不同,约定组件分为三个类型:

基础组件

为项目提供与业务无关基础支持的组件库,如提供MVVM架构的lib_basic,提供依赖注入的lib_basic_koin,这类组件统一命名方式为lib_basic_xxx。这些基础组件也可以被工具组件和业务组件依赖,而不仅仅是只被项目依赖。

工具组件

对项目中常用的业务无关功能封装的组件库,如Dialog弹窗,相册选择等,这类组件统一命名方式为lib_util_xxx

业务组件

在某个项目的需求中出现的在其他项目中可能也会用到的功能的封装,比如A业务员版中扫描拍照在A商业版中也有用到,于是单独封装为一个业务组件进行管理,这类组件统一命名方式为lib_tool_xxx

GroupId

所有组件统一GroupId为com.company.android,方便在Maven仓库中进行索引和管理。

版本号

对于迭代的版本号,不做强制性的要求,只需合理进行升级即可,但以下情况需特殊注意:

1、对于上传的开发版本,需要在版本中进行体现,如1.1.0-dev02,与正式版本做区分,在项目中验证无风险后,合并到主迭代版本中。

2、对于紧急修复A项目中的问题而临时发版的,需要在版本中进行体现,如1.1.0-hotfix-A,此次修改在其他使用的项目中验证无风险后,合并到主迭代版本中。

开发原则

开闭原则

在组件升级时,应对新增开放,对修改关闭,即做加法不做减法,目的是为了保证对项目中调用老版本API的兼容问题。对于不再建议使用的API,应使用 @Deprecated注解进行标注,并新增建议使用的API进行代替,而不是直接删除旧API。在完成多个稳定版本的迭代之后,可以所有组件使用者讨论删除旧版API的事宜。

向下兼容原则

所有组件的版本迭代必须向下兼容,即1.2.0版本须兼容1.1.0,在使用者升级版本之后,无需修改业务层代码。

如果因组件前期设计不合理导致升级必须修改业务代码时,组件开发者需要与所有组件使用者商讨技术实现方案,看能否以最小的改动完成组件的升级,并在组件功能开发完毕后,以书面形式告知使用者并提供完整的升级文档

三方隔离原则

我们在封装组件的时候,难免会使用到第三方依赖,为了避免更换三方依赖或依赖升级造成的影响,需要对三方依赖库进行二次封装,避免直接使用。比如我们要使用glide框架加载图片,可以创建一个代理类对glide的功能进行代理,在业务代码中使用代理类进行操作而不是直接调用glide,这样我们将来替换glide框架时,能最小的改动业务代码。

当然,并不是所有的三方库都通过代理模式进行隔离,比如retrofit、RxJava等,毕竟我们的隔离原则是为了以后更简单的迭代而不是自寻烦恼。

最少依赖原则

为了减少项目中依赖的类库,在组件封装中遇到如下情况,应对组件进行拆分工作:

现封装一图片加载类库lib_util_imageloader,对GlidePicasso进行了二次封装,由于两个类库提供了类似的功能,在项目中只需要使用一套就能满足业务需求,没有必要把两个图片加载框架都进行依赖,所以应该对lib_util_imageloader进行拆分为如下结构:

lib_util_imageloader_core:图片加载核心库,把加载图片的方法抽象为接口,面向项目,不做具体的实现。

lib_util_imageloader_glide:对glide进行二次封装,实现core中的接口。

lib_util_imageloader_picasso:对picasso进行二次封装,实现core中的接口。

在项目使用图片加载库时,除了必须要依赖的core外,只需要从glide和picasso中挑选一个即可,这样就不会把用不到的类库也打包到项目中去了。

这样的做法还有一个好处,就是容易拓展。如果我想要使用Fresco,只需要再新增一个lib_util_imageloader_fresco,并实现core的接口即可,在切换组件时,只需要改变gradle文件中的依赖,无需变更业务代码,因为业务代码都是基于core组件的。

最少可见原则

组件应该尽可能少的对外暴露类、接口、方法等。可以通过外观模式对使用者统一提供API,降低使用者的理解难度。

支持开发模式

组件需要预留开发者模式,可以让使用者自行选择开启关闭。

打开开发者模式:组件运行的关键节点需进行日志输出,方便使用者进行调试,可以在运行时抛出异常。

关闭开发者模式:组件不再对外输出Error级别以下的日志,禁止在运行时抛出异常、ANR,如发生异常需要在组件内捕获并通过错误回调或打印Error级别日志等方式告知使用者。

可拓展性

部分组件(视具体情况而定,多数为对三方类库的封装组件)应该有一定的拓展性,应支持使用者自定义实现覆盖默认实现。

比如Dialog类库有默认弹窗样式,需要支持使用者自定义弹窗样式而不是只能在默认样式中进行选择,图片加载框架也是相同的逻辑。

开发规范

包名规范

为了避免无意中导致的包名冲突问题,约定组件的包名为4级,除去前两级的com.company为固定写法以外,后两级可根据具体的组件功能进行命名,并要求有良好的可读性。

资源规范

组件中定义资源文件时,要以组件名称为前缀,避免资源冲突导致的打包问题,需要在gradle文件的android节点下新增如下代码强制进行资源名称前缀检查:

android{
 resourcePrefix = "${your_component_name}_"
}
复制代码

代码规范

应当遵循Java开发代码规范,这里不再赘述。

可见域规范

Kotlin中新增关键字internal,可用于修饰类名、方法名和成员变量名,限制所修饰的对象模块内可见,对于无需对外暴露的类、方法和变量,应当降低其可见域。

内联函数

Kotlin中新增关键字inline修饰方法,可以减少方法栈的进栈方法数。在封装组件时善用inline以提高组件的运行效率。

内存泄漏

所有组件在发布之间,必须进行内存泄漏检测,禁止存在内存泄漏的组件上线,应在开发阶段修复所有泄漏问题。

混淆

组件开发者需要确认自己的组件在打包混淆后是否可以正常工作,如有用到运行时注解、反射、Json转换等功能,需要在接入文档中声明避免混淆的规则。

文档

组件的接入、使用、升级和注意事项等需要在开发文档中有明确的体现,没有接入文档或者文档不完善一律不许通过验收。

Demo

组件最好有配套的演示工程,用来最直观的体现出组件所提供的功能,也方便使用者进行参考。

最后

目前只能想起这么多,以后有新增的会继续补充。

规范好定制,落实起来却难,路漫漫其修远兮,加油吧,打工人!


作者:王远道
链接:https://juejin.cn/post/7021434072652054565
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

消失性进度条

效果&使用 图例分别为: 修改读条起点为y轴正方向 消失性读条 正常读条 使用: 1 在xml中添加控件 <com.lloydfinch.ProgressTrackBar android:id="@+id/progress_tr...
继续阅读 »

效果&使用


效果


图例分别为:



  • 修改读条起点为y轴正方向

  • 消失性读条

  • 正常读条


使用:



  • 1 在xml中添加控件


<com.lloydfinch.ProgressTrackBar
android:id="@+id/progress_track_bar"
android:layout_width="62dp"
android:layout_height="62dp"
app:p_second_color="#E91E63"
app:p_width="3dp" />

<com.lloydfinch.ProgressTrackBar
android:id="@+id/progress_track_bar2"
android:layout_width="62dp"
android:layout_height="62dp"
app:p_first_color="#18B612"
app:p_second_color="#00000000"
app:p_width="3dp" />

<com.lloydfinch.ProgressTrackBar
android:id="@+id/progress_track_bar3"
android:layout_width="62dp"
android:layout_height="62dp"
app:p_first_color="#ffd864"
app:p_second_color="#1C3F7C"
app:p_width="3dp" />


  • 2 在代码中启动倒计时


val trackBar = findViewById<ProgressTrackBar>(R.id.progress_track_bar)
trackBar.setStartAngle(-90F) // 从-90度开始读条
trackBar.setOnProgressListener { // 进度回调
Log.d("ProgressTrackBar", "progress is $it")
}
trackBar.startTask(0) { // 开始计时,传入读条结束的回调
Log.d("ProgressTrackBar", "progress run finish")
}

// 从0开始计时
findViewById<ProgressTrackBar>(R.id.progress_track_bar2).startTask(0)

// 从20开始计时
findViewById<ProgressTrackBar>(R.id.progress_track_bar3).startTask(20)

思路&编码


核心思路就一个: 画原环。我们要画两个圆环,一个下层的完整圆环作为底色,一个上层的圆弧作为进度。重点就是计算圆弧弧度的问题了。


假设当前进度是current,最大进度是max,那么当前圆弧进度就是:(current/max)*360,然后我们直接调用:


// oval: 放置圆弧的矩形
// startAngle: 开始绘制的起点角度,方向是顺时针计算的。0就x正半轴,90就是y轴负半轴
// sweepAngle: 要绘制的圆弧的弧度,就是上述: (current/max)x360
// false: 表示不连接到圆心,表示绘制一个圆弧
canvas.drawArc(oval, startAngle, sweepAngle, false, mPaint);

就能绘制出对应的圆弧。


所以,我们这样:


// 绘制下层: 圆形
mPaint.setColor(firstLayerColor);
canvas.drawCircle(x, y, radius, mPaint);

// 绘制上层: 圆弧
mPaint.setColor(secondLayerColor);
float sweepAngle = (currentProgress / maxProgress) * 360;
canvas.drawArc(oval, startAngle, sweepAngle, false, mPaint);

我们先用下层颜色绘制一个圆形,然后用上层颜色绘制个圆弧,然后不断触发重绘,就能得到想要的效果。


但是,如果我们想要的是: 随着进度变大,圆弧越来越短呢?比如示例图的第二个效果。说白了就是让上层随着时间流逝而变小,直到消失,怎么实现呢?


其实,说白了就是时间越长,弧度越小,我们做减法即可,我们用(max-current)来作为已读进度,这样随着时间流逝,进度就越来越小。


有人说,这样不对啊,这样(max-current)不就越读越小了吗,这样画出来的弧度就越来越短了,最后完全漏出了底层,给人的感觉是倒着读的。没错,所以,我们只绘制一层,我们用下层颜色来绘制圆弧!这样,随着时间流逝,弧度越来越小,因为圆弧是用下层颜色绘制的,所以视觉上就是: 下层越来越少。给人的感觉就是: 上层越来越大以至于盖住了下层。


逻辑如下:


// 用下层颜色 绘制 剩下的弧度
mPaint.setColor(firstLayerColor);
float leaveAngle = ((maxProgress - currentProgress) / maxProgress) * 360;
canvas.drawArc(oval, startAngle, leaveAngle, false, mPaint);

可以看到,这里只绘制一层,随着时间流逝,圆弧越来越短,给人的感觉就是: 圆弧消失。就达到了示例图中 第二个圆弧的效果。


整体代码如下:


public class ProgressTrackBar extends View {


private static final int DEFAULT_FIRST_COLOR = Color.WHITE;
private static final int DEFAULT_SECOND_COLOR = Color.parseColor("#FFA12F");

private static final int PROGRESS_WIDTH = 6;
private static final float MAX_PROGRESS = 360F;
private static final int DEFAULT_SPEED = 1;

private Paint mPaint;
private float startAngle = 0;
private int firstLayerColor = DEFAULT_FIRST_COLOR;
private int secondLayerColor = DEFAULT_SECOND_COLOR;
private final RectF oval = new RectF(); // 圆形轨迹
private float maxProgress = MAX_PROGRESS; // 最大进度:ms
private float currentProgress = 0F; // 当前进度:ms
private int speed = DEFAULT_SPEED; // 速度(多长时间更新一次UI):ms
private int progressWidth = PROGRESS_WIDTH; // 进度条宽度

private OnProgressFinished onProgressFinished;

private Handler taskHandler;
private OnProgress runnable; //进度回调

// 顶层颜色是否是透明
private boolean isSecondColorTransparent = false;

public ProgressTrackBar(Context context) {
super(context);
init();
}

public ProgressTrackBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);

TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.ProgressTrackBar);
firstLayerColor = typedArray.getColor(R.styleable.ProgressTrackBar_p_first_color, DEFAULT_FIRST_COLOR);
secondLayerColor = typedArray.getColor(R.styleable.ProgressTrackBar_p_second_color, DEFAULT_SECOND_COLOR);
startAngle = typedArray.getFloat(R.styleable.ProgressTrackBar_p_start, 0F);
progressWidth = typedArray.getDimensionPixelSize(R.styleable.ProgressTrackBar_p_width, PROGRESS_WIDTH);
maxProgress = typedArray.getDimension(R.styleable.ProgressTrackBar_p_max_progress, MAX_PROGRESS);

typedArray.recycle();

init();
}

public ProgressTrackBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
refresh();
mPaint = new Paint();
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setStrokeWidth(progressWidth);
}

public void setFirstLayerColor(int firstLayerColor) {
this.firstLayerColor = firstLayerColor;
}

public void setSecondLayerColor(int secondLayerColor) {
this.secondLayerColor = secondLayerColor;
refresh();
}

public void setMaxProgress(float maxProgress) {
this.maxProgress = maxProgress;
}

public void setSpeed(int speed) {
this.speed = speed;
}

public void setStartAngle(float startAngle) {
this.startAngle = startAngle;
}

public void setProgressWidth(int progressWidth) {
this.progressWidth = progressWidth;
}

public void setOnProgressListener(OnProgress runnable) {
this.runnable = runnable;
}

public void setOnProgressFinished(OnProgressFinished onProgressFinished) {
this.onProgressFinished = onProgressFinished;
}

private void initTask() {
taskHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
if (currentProgress < maxProgress) {
currentProgress += speed;
postInvalidate();
if (runnable != null) {
runnable.onProgress(currentProgress);
}
taskHandler.sendEmptyMessageDelayed(0, speed);
} else {
stopTask();
}
}
};
}

private void refresh() {
isSecondColorTransparent = (secondLayerColor == Color.parseColor("#00000000"));
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int x = getWidth() >> 1;
int y = getHeight() >> 1;
int center = Math.min(x, y);
int radius = center - progressWidth;

int left = x - radius;
int top = y - radius;
int right = x + radius;
int bottom = y + radius;
oval.set(left, top, right, bottom);

// 这里需要处理一下上层是透明的情况
if (isSecondColorTransparent) {
// 用下层颜色 绘制 剩下的弧度
mPaint.setColor(firstLayerColor);
float leaveAngle = ((maxProgress - currentProgress) / maxProgress) * 360;
canvas.drawArc(oval, startAngle, leaveAngle, false, mPaint);
} else {
// 绘制下层
mPaint.setColor(firstLayerColor);
canvas.drawCircle(x, y, radius, mPaint);

// 绘制上层
mPaint.setColor(secondLayerColor);
float sweepAngle = (currentProgress / maxProgress) * 360;
canvas.drawArc(oval, startAngle, sweepAngle, false, mPaint);
}
}

public void startTask(int progress) {
currentProgress = progress;
initTask();
taskHandler.sendEmptyMessage(0);
}

public void startTask(int progress, OnProgressFinished onProgressFinished) {
this.onProgressFinished = onProgressFinished;
currentProgress = progress;
initTask();
taskHandler.sendEmptyMessage(0);
}

public void stopTask() {
if (onProgressFinished != null) {
onProgressFinished.onFinished();
}
if (taskHandler != null) {
taskHandler.removeCallbacksAndMessages(null);
}
}

@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
stopTask();
}

public interface OnProgressFinished {
void onFinished();
}

public interface OnProgress {
void onProgress(float progress);
}
}

总结


核心思路就一个: 如果上层要用透明盖住下层,这是不可能的,所以不如用上层的相对值去绘制下层


作者:奔波儿灞取经
链接:https://juejin.cn/post/7021448361488154631
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

你知道为何跨域中会发送 options 请求?

同源策略 同源策略是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。 简单说,当我们访问一个网站时,浏览器会对源地址的不同部分(协议://域名:端口)做检查...
继续阅读 »

同源策略



同源策略是一个重要的安全策略,它用于限制一个 origin 的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。



简单说,当我们访问一个网站时,浏览器会对源地址的不同部分(协议://域名:端口)做检查。比如防止利用它源的存储信息(Cookies...)做不安全的用途。


跨域 CORS


但凡被浏览器识别为不同源,浏览器都会认为是跨域,默认是不允许的。


比如:试图在 http://127.0.0.1:4000 中,请求 http://127.0.0.1:3000 的资源会出现如下错误:


跨域错误


这也是前端 100% 在接口调试中会遇到的问题。


同源和跨域的判断规则


简单请求和复杂请求



相信都会在浏览器的 Network 中看到两个同样地址的请求,有没有想过这是为什么呢?这是因为在请求中,会分为 简单请求复杂请求


简单请求:满足如下条件的,将不会触发跨域检查:



  • 请求方法为:GETPOSTHEAD

  • 请求头:AcceptAccept-LanguageContent-LanguageContent-Type


其中 Content-Type 限定为 :text/plain、multipart/form-data、application/x-www-form-urlencoded


我们可以更改同源规则,看下如下示例:



http://127.0.0.1:4000/ 下,请求 http://127.0.0.1:3000 不同端口的地址



简单请求


域名不同,这已经跨域了。但由于请求方法为 GET,符合 简单请求,请求将正常工作。


复杂请求:不满足简单请求的都为复杂请求。在发送请求前,会使用 options 方法发起一个 预检请求(Preflight) 到服务器,以获知服务器是否允许该实际请求。


模拟一个跨域请求:


// 端口不同,content-type 也非限定值
axios.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
},
}
);

能看到在请求之前浏览器会事先发起一个 Preflight 预检请求


Preflight


这个 预检请求 的请求方法为 options,同时会包含 Access-Control-xxx 的请求头:


options请求信息


当然,此时服务端没有做跨域处理(示例使用 express 起的服务,预检请求默认响应 200),就会出现浏览器 CORS 的错误警告。


跨域错误


如何解决跨域


对于跨域,前端再熟悉不过,百度搜索能找到一堆解决方法,关键词不是 JSONP,或者添加些 Access-Control-XXX 响应头。


本篇将详细说下后一种方式,姑且称为:服务端解决方案。


为 options 添加响应头


express 举例,首先对 OPTIONS 方法的请求添加这些响应头,它将根据告诉浏览器根据这些属性进行跨域限制:


app.use(function (req, res, next) {
if (req.method == 'OPTIONS') {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'content-type');
res.status(200).end();
}
});

如果你不对 预检接口 做正确的设置,那么后续一切都是徒劳。


打个比方:如果 Access-Control-Allow-Methods 只设置了 POST,如果客户端请求方法为 PUT,那么最终会出现跨域异常,并会指出 PUT 没有在预检请求中的 Access-Control-Allow-Methods 出现:


跨域方法错误
所以,以后读懂跨域异常对于正确的添加服务端响应信息非常重要。另外:GET、POST、HEAD 属于简单请求的方法,所以即使不在 Access-Control-Allow-Methods 定义也不碍事(如果不对请指出)


正式的跨域请求


随后对我们代码发出的请求额外添加跨域响应头(这需要和前面的预检接口一致)


if (req.method == 'OPTIONS') {
//...
} else {
// http://127.0.0.1:3000/test/cors
res.setHeader('Access-Control-Allow-Origin', '*');
next();
}

最后能看到我们等请求正常请求到了:


跨域请求


对于跨域请求头的说明


上例出现了我们经常见到的三个:Access-Control-Allow-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-Headers


参考 cors 库,另外还有其他用于预检请求的响应头:



下面将对上面这些头做个说明。


Access-Control-Allow-Origin


预检请求正常请求 告知浏览器被允许的源。支持通配符“*”,但不支持以逗号“,”分割的多源填写方式。


如果尝试些多个域名,则会出现如下错误:



Response to preflight request doesn't pass access control check: The 'Access-Control-Allow-Origin' header contains multiple values 'aaa,bbb', but only one is allowed.



多源错误


另外,也不建议 Access-Control-Allow-Origin 以通配符方式定义,这样会增加安全隐患,最好以请求方的 origin 来赋值。


const origin = req.headers.origin;
res.setHeader('Access-Control-Allow-Origin', origin || '*');
// 因为会随着客户端请求的 Origin 变化,所以标识 Vary,让浏览器不要缓存
res.setHeader('Vary', 'Origin');

Access-Control-Allow-Methods


被允许的 Http 方法,按照需要填写,支持多个,例如: GET , HEAD , PUT , PATCH , POST , DELETE


由于判断 简单请求 之一的 HTTP 方法默认为 GETPOSTHEAD ,所以这些即使不在 Access-Control-Allow-Methods 约定,浏览器也是支持的。


比如:如果服务端定义 PUT 方法,而客户端发送的方法为 DELETE,则会出现如下错误:


res.setHeader('Access-Control-Allow-Methods', 'PUT');


Method DELETE is not allowed by Access-Control-Allow-Methods in preflight response.



方法错误


Access-Control-Allow-Headers


预检接口 告知客户端允许的请求头。


简单请求 约定的请求头默认支持: AcceptAccept-LanguageContent-LanguageContent-Typetext/plain、multipart/form-data、application/x-www-form-urlencoded


如果客户端的请求头不在定义范围内,则会报错:



Request header field abc is not allowed by Access-Control-Allow-Headers in preflight response.



请求头错误


需要将此头调整为:


res.setHeader('Access-Control-Allow-Headers', 'content-type, abc');

Access-Control-Max-Age


定义 预检接口 告知客户端允许的请求头可以缓存多久。


默认时间规则:



  • 在 Firefox 中,上限是 24 小时 (即 86400 秒)。

  • 在 Chromium v76 之前, 上限是 10 分钟(即 600 秒)。

  • 从 Chromium v76 开始,上限是 2 小时(即 7200 秒)。

  • Chromium 同时规定了一个默认值 5 秒。

  • 如果值为 -1,表示禁用缓存,则每次请求前都需要使用 OPTIONS 预检请求。


比如设置为 5 秒后,客户端在第一次会发送 预检接口 后,5 秒内将不再发送 预检接口


res.setHeader('Access-Control-Max-Age', '5');

缓存示例


Access-Control-Allow-Credentials


跨域的请求,默认浏览器不会将当前地址的 Cookies 信息传给服务器,以确保信息的安全性。如果有需要,服务端需要设置 Access-Control-Allow-Credentials 响应头,另外客户端也需要开启 withCredentials 配置。


// 客户端请求
axios.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
abc: '123',
},
withCredentials: true,
}
);

// 所有请求
res.setHeader('Access-Control-Allow-Credentials', 'true');

需要注意的是,Access-Control-Allow-Origin 不能设置通配符“*”方式,会出现如下错误:


不支持通配符
这个 Access-Control-Allow-Origin 必须是当前页面源的地址。


Access-Control-Expose-Headers


Access-Control-Allow-Credentials 类似,如果服务端有自定义设置的请求头,跨域的客户端请求在响应信息中是接收不到该请求头的。


// 服务端
res.setHeader('def', '123');

axios
.post(
'http://127.0.0.1:3000/test/cors',
{},
{
headers: {
'content-type': 'application/json',
abc: '123',
},
withCredentials: true,
}
)
.then((data) => {
console.log(data.headers.def); //undefined
});

需要在服务端设置 Access-Control-Expose-Headers 响应头,并标记哪些头是客户端能获取到的:


res.setHeader('Access-Control-Expose-Headers', 'def');
res.setHeader('def', '123');

Access-Control-Request-Headers


我试了半天没找到 Access-Control-Request-Headers 的使用示例,其实它是根据当前请求的头拼接得到的。


如果客户端的请求头为:


{
"content-type": "application/json",
"abc": "123",
"xyz": "123",
},

那么浏览器最后会在 预检接口 添加一个 Access-Control-Request-Headers 的头,其值为:abc,content-type,xyz。然后服务端再根据 Access-Control-Allow-Headers 告诉浏览器服务端的请求头支持说明,最后浏览器判断是否会有跨域错误。


另外,对于服务端也需要针对 Access-Control-Request-HeadersVary 处理:


res.setHeader('Vary', 'Origin' + ', ' + req.headers['access-control-request-headers']);

如此,对于跨域及其怎么处理头信息会有个基本的概念。希望在遇到类似问题能有章法的解决,而非胡乱尝试。


作者:Eminoda
链接:https://juejin.cn/post/7021077647417409550

收起阅读 »

移动端常见问题汇总,拿来吧你!

1px适配方案 某些时候,设计人员希望 1px在手机显示的就是1px,这也是....闲的,但是我们也要满足他们的需求, 这时候我们可以利用缩放来达到目的 .border_1px:before{    content: '';  ...
继续阅读 »

1px适配方案


某些时候,设计人员希望 1px在手机显示的就是1px,这也是....闲的,但是我们也要满足他们的需求,


这时候我们可以利用缩放来达到目的


.border_1px:before{
   content: '';
   position: absolute;
   top: 0;
   height: 1px;
   width: 100%;
   background-color: #000;
   transform-origin: 0% 0%;
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
   .border_1px:before{
       transform: scaleY(0.5);
  }
}
@media only screen and (-webkit-min-device-pixel-ratio:3){
   .border_1px:before{
       transform: scaleY(0.33);
  }
}


设置一个专门的class来处理1px的问题,利用伪类给其添加



  • -webkit-min-device-pixel-ratio 获取像素比

  • transform: scaleY(0.5) 垂直方向缩放,后面的数字是倍数


图片模糊问题


.avatar{
   background-image: url(conardLi_1x.png);
}
@media only screen and (-webkit-min-device-pixel-ratio:2){
   .avatar{
       background-image: url(conardLi_2x.png);
  }
}
@media only screen and (-webkit-min-device-pixel-ratio:3){
   .avatar{
       background-image: url(conardLi_3x.png);
  }
}

根据不一样的像素比,准备不一样的图片,正常来说是1px图片像素 对应1px物理像素,图片的显示就不会模糊啦,但是这样的情况不多,不是非常重要,特殊需求的图,我们不这么做。


滚动穿透问题


wt1.gif


移动端的网站,我们是经常会有一些弹出框出现的,这样的弹出框,在上面滑动,会导致我们后面的整个页面发生移动,这个问题怎么解决呢??


body{
   position:fixed;
   width:100%;
}

给body添加position:fixed就可以使滚动条失效,这里弹框的显示和隐藏,我们利用JS进行控制,而且添加上position:fixed的一瞬间,可以看到页面一下回到0,0的位置,因为fixed是根据可视区定位的。


键盘唤起


main{
   padding: 2rem 0;
   /* height: 2000px; */
   position: absolute;
   top: 60px;
   bottom: 60px;
   overflow-y: scroll;
   width: 100%;
   -webkit-overflow-scrolling: touch;
}

当底部根据页面进行fixed定位的时候,键盘弹出一瞬间,fixed会失效,变成类似absoult,让main的内容无滚动,就不会连带fixed一起动了


并且为了保证如丝般顺滑:



  • -webkit-overflow-scrolling: touch;


移动端的神奇操作


IOS下的一些设置 和 安卓下的一些设置


添加到主屏幕后的标题


<meta name="apple-mobile-web-app-title" content="标题"> 

image.png


添加到主屏后的APP图标


<link href="short_cut_114x114.png" rel="apple-touch-icon-precomposed">


  • 一般我们只需要提供一个114*114的图标即可


启用webApp全屏模式


<meta name="apple-mobile-web-app-capable" content="yes" /> 
<meta name="apple-touch-fullscreen" content="yes" />



  • apple-mobile-web-app-capable


    删除默认的苹果工具栏和菜单栏,默认为no




  • apple-touch-fullscreen


    全屏显示




移动端手机号码识别


<meta name="format-detection" content="telephone=no" />


  • safari会对一些可能是手机号码的数字,进行识别,我们可以利用上面的方式,禁止识别


手动开启拨打电话功能


<a href="tel:13300000000">13300000000</a>


  • 在手机上点击这个链接,可以直接拨打电话


手动开启短信功能


<a href="sms:13300000000">13300000000</a>


  • 在手机上点击这个链接,可以跳转去短信页面,给该手机号发送消息


移动端邮箱识别


<meta name="format-detection" content="email=no" /> 

手动开启邮箱发送功能


<a href="mailto:854121000@qq.com">发送邮件</a>


  • 调用邮箱发送功能


优先启用最新版本IE和chrome


<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> 

移动端默认样式




  • 移动端默认字体



    1. 数字 和 英文字体 可使用Helvetica字体,IOS 和 Android都有这个字体

    2. 手机系统都有自己默认的字体,直接使用默认的


    body{
       font-family:Helvetica;
    }



  • 字体大小


    如果只是适配手机,可以使用px




  • IOS系统中,链接、按钮等点击会有灰色遮罩


    a,button,input,textarea{-webkit-tap-highlight-color: rgba(0,0,0,0)}



  • 去除圆角


    button,input{
       -webkit-appearance:none;
       border-radius: 0;
    }



  • 禁止文本缩放


    html{
        -webkit-text-size-adjust: 100%;
    }
收起阅读 »

你真的了解border-radius吗?

水平半径和垂直半径 现在很多人都不知道我们平常使用的圆角值是一种缩写,例如我们平常写的top圆角10px就是一种缩写: border-top-left-radius:10px; 等同于 border-top-left-radius:10px 10px; 其中...
继续阅读 »

水平半径和垂直半径


现在很多人都不知道我们平常使用的圆角值是一种缩写,例如我们平常写的top圆角10px就是一种缩写:


border-top-left-radius:10px; 等同于 border-top-left-radius:10px 10px; 

其中,第一个值表示水平半径,第二个值表示圆角垂直半径;


例如:


    <style>
.talk-dialog {
position: relative;
background: deepskyblue;
width: 100px;
height: 100px;
margin: 0 auto;
border-top-left-radius: 30px 80px;
}
</style>

 <div class="talk-dialog"></div>

image.png


那么border-radius的写法应该怎么去写呢??它的水平半径和垂直半径是通过 斜杠 区分。 例如:


border-radius: 30px / 40px;


表示四个角的圆角水平半径都是30px,垂直半径是40px;


border-radius斜杠前后都支持1-4个值,以下多个值得写法为:


border-radius:10px 20px / 5% 20% 3% 10%;(左上+右下,右上+左下, / 左上,右上,右下,左下)


重叠问题


难道你认为这就完了,border-radius你彻底搞懂了??其实不然!


我们来看看下面一个列子:


<!DOCTYPE html>
<html lang="en">
<head>

<style>
.talk-dialog {
position: relative;
background: red;
width: 100px;//重点关注
height: 100px;//重点关注
border-radius: 30px / 80px; //重点关注
margin: 50px auto;
}

.talk-dialog1 {
position: relative;
background: deepskyblue;
width: 100px;//重点关注
height: 100px;//重点关注
border-top-left-radius: 30px 80px; //重点关注
margin: 10px auto;
}
</style>

</head>

<body>
<div class="talk-dialog"></div>
<div class="talk-dialog1"></div>
</body>
</html>

我们的容器大小宽为100px,高为100px, 问大家一个问题!


border-radius: 30px / 80px; 与 border-top-left-radius: 30px 80px; 两个不同的容器的 top-left的圆角大小一样吗???


image.png


大家或许这样看不出来,我们修改为绝对布局,两个元素重叠在一起看看是否左上角可以完美重叠?


image.png


答案揭晓: 圆角效果是不一样的,因为我们容器的垂直高度为100px,我们border-radius:30px / 80px设置以后,我们元素的高度不足以放下两个半轴为80px(80+80=160)的椭圆,如果这种场景不做约束,曲线就会发生一定的重叠,因此css 规范对圆角曲线重叠问题做了额外的渲染设定,具体算法如下:


f=min(L宽度/S宽度,L高度/S高度),L为容器宽高,S为半径之和,


这里计算我们的例子:f=min(100/60,100/160)=0.625 , f的值小于1,则所有的圆角值都要乘以f


因此:border-radius: 30px / 80px;


左上角值等同于:


border-top-left-radius:18.75px 50px;


细节



  • border-radius 不支持负值

  • 圆角以外的区域不可点击

  • border-radius没有继承性,因此父元素设置了border-radius,子元素依旧是直角效果,要想达到圆角效果,需要加overflow:hidden。(重要,工作中常用)

  • border-radius 也支持transition过渡效果


高级用法案例:


image.png


代码:


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<title></title>
<link rel="icon" href="data:;base64,=" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" />
<meta name=" theme-color" content="#000000" />
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<style>
.radius {
width: 150px;
height: 150px;
border-radius: 70% 30% 30% 70% / 60% 40% 60% 40%;
object-fit: cover;
object-position: right;
}

.demo {
position: relative;
width: 150px;
height: 150px;
margin: 10px auto;
}

.radius-1 {
width: 150px;
height: 150px;
object-fit: cover;
object-position: right;
background: deepskyblue;
color: #fff;
font-size: 40px;
text-align: center;
line-height: 120px;
border-bottom-right-radius: 100%;
}

.talk {
padding: 10px;
background: deepskyblue;
border-radius: .5em;
color: #fff;
position: relative;
z-index: 0;
}

.talk::before {
content: "";
position: absolute;
width: 15px;
height: 10px;
color: deepskyblue;
border-top: 10px solid;
border-top-left-radius: 80%;
left: 0;
bottom: 0;
margin-left: -12px;
-ms-transform: skewX(-30deg) scaleY(1.3);
transform: skewX(-30deg) scaleY(1.3);
z-index: -1;
}
</style>

</head>

<body>
<div class="demo demo1">
<img class="radius" src="./1.jpg" />
</div>
<div class="demo demo2">
<div class="radius-1">1</div>
</div>
<div class="demo demo3">
<div class="talk">border-radius圆角效果实现。</div>
</div>
</body>

</html>

结语:


欢迎大家多提宝贵意见,一赞一回,如果本文让你get 到知识,请不要吝啬你的star!



收起阅读 »

写给vue转react的同志们(5)

写给vue转react的同志们(4)我们知道 React 中使用高阶组件(下面简称HOC)来复用一些组件的逻辑。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件...
继续阅读 »

写给vue转react的同志们(4)
我们知道 React 中使用高阶组件(下面简称HOC)来复用一些组件的逻辑。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。具体而言,高阶组件是参数为组件,返回值为新组件的函数。


组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件。


const EnhancedComponent = higherOrderComponent(WrappedComponent);

上面出自 React 官方文档。


那在 Vue 中 复用组件逻辑实际上比较简单,利用 Mixins 混入复用组件逻辑,当 Mixins 中的逻辑过多时(比如方法和属性),在项目当中使用时追述源代码会比较麻烦,因为他在混入后没有明确告诉你哪个方法是被复用的。


//mixins.js(Vue 2 举例)
export defalut {
data() {
return {
text: 'hello'
}
}
}
// a.vue
import mixins from './mixins.js'
export defalut {
mixins: [mixins]
computed: {
acitveText() {
return `来自mixins的数据:${this.text}`
}
}
}
复制代码

可以看到除了在开头引入并挂载混入,并没有看到this.text是从哪里来的,混入虽然好用,但当逻辑复杂时,其阅读起来是有一定困难的。


那你想在 Vue 中强行使用像 React 那样的高阶组件呢?那当然可以。只是 Vue 官方不怎么推崇 HOC,且 Mixins 本身可以实现 HOC 相关功能。


简单举个例子:


// hoc.js
import Vue from 'Vue'

export default const HOC = (component, text) => {
return Vue.component('HOC', {
render(createElement) {
return createElement(component, {
on: { ...this.$listeners },
props: {
text: this.text
}
})
},
data() {
return {
text: text,
hocText: 'HOC'
}
},
mounted() {
// do something ...
console.log(this.text)
console.log(this.hocText)
}
})
}

使用高阶组件:


// user.vue
<template>
<userInfo/>
</template>

<script>
import HOC from './hoc.js'
// 引入某个组件
import xxx from './xxx'

const userInfo = HOC(xxx, 'hello')

export default {
name: 'user',
components: {
userInfo
}
}
</script>



是不是相比 Mixins 更加复杂一点了?在 Vue 中使用高阶组件所带来的收益相对于 Mixins 并没有质的变化。不过话又说回来,起初 React 也是使用 Mixins 来完成代码复用的,比如为了避免组件的非必要的重复渲染可以在组件中混入 PureRenderMixin


const PureRenderMixin = require('react-addons-pure-render-mixin')
const component = React.createClass({
mixins: [PureRenderMixin]
})


后来 React 使用shallowCompare 来 替代 PureRenderMixin


const shallowCompare = require('react-addons-shallow-compare')
const component = React.createClass({
shouldComponentUpdate: (nextProps, nextState) => {
return shallowCompare(nextProps, nextState)
}
})


这需要你自己在组件中实现 shouldComponentUpdate 方法,只不过这个方法具体的工作由 shallowCompare 帮你完成,我们只需调用即可。


再后来 React 为了避免总是要重复调用这段代码,React.PureComponent 应运而生,总之 React 在慢慢将 Mixins 脱离开来,这对他们的生态系统并不是特别的契合。当然每种方案都各有千秋,只是是否适合自己的框架。


那我们回归 HOC,在 React 中如何封装 HOC 呢?


实际上我在往期篇幅有提到过:
点击传送


但是我还是简单举个例子:


封装 HOC:


// hoc.js
export default const HOC = (WrappedComponent) => {
return Class newComponent extends WrappedComponent {
constructor(props) {
super(props)
// do something ...
this.state = {
text: 'hello'
}
}
componentDidMount() {
super.componentDidMount()
// do something ...
console.log('this.state.text')
}
render() {
// init render
return super.render()
}
}
}



使用 HOC:


// user.js
import HOC from './hoc.js'
class user extends React.Component {
// do something ...
}
export defalut HOC(user)


装饰器写法更为简洁:


import HOC from './hoc.js'
@HOC
class user extends React.Component {
// do something ...
}
export defalut user


可以看到无论 Vue 还是 React 亦或是 HOC 或 Mixins 他们都是为了解决组件逻辑复用应运而生的,具体使用哪一种方案还要看你的项目契合度等其他因素。


技术本身并无好坏,只是会随着时间推移被其他更适合的方案取代,技术迭代也是必然的,相信作为一个优秀的程序员也不会去讨论一个技术的好或坏,只有适合与否。


作者:饼干_
链接:https://juejin.cn/post/7020215941422137381

收起阅读 »

Android学习指南 — Android进阶篇

ARTART 代表 Android Runtime,其处理应用程序执行的方式完全不同于 Dalvik,Dalvik 是依靠一个 Just-In-Time (JIT) 编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高...
继续阅读 »

ART

ART 代表 Android Runtime,其处理应用程序执行的方式完全不同于 Dalvik,Dalvik 是依靠一个 Just-In-Time (JIT) 编译器去解释字节码。开发者编译后的应用代码需要通过一个解释器在用户的设备上运行,这一机制并不高效,但让应用能更容易在不同硬件和架构上运 行。ART 则完全改变了这套做法,在应用安装时就预编译字节码到机器语言,这一机制叫 Ahead-Of-Time (AOT)编译。在移除解释代码这一过程后,应用程序执行将更有效率,启动更快。

ART 功能

预先 (AOT) 编译

ART 引入了预先编译机制,可提高应用的性能。ART 还具有比 Dalvik 更严格的安装时验证。在安装时,ART 使用设备自带的 dex2oat 工具来编译应用。该实用工具接受 DEX 文件作为输入,并为目标设备生成经过编译的应用可执行文件。该工具应能够顺利编译所有有效的 DEX 文件。

垃圾回收优化

垃圾回收 (GC) 可能有损于应用性能,从而导致显示不稳定、界面响应速度缓慢以及其他问题。ART 通过以下几种方式对垃圾回收做了优化:

  • 只有一次(而非两次)GC 暂停
  • 在 GC 保持暂停状态期间并行处理
  • 在清理最近分配的短时对象这种特殊情况中,回收器的总 GC 时间更短
  • 优化了垃圾回收的工效,能够更加及时地进行并行垃圾回收,这使得 GC_FOR_ALLOC 事件在典型用例中极为罕见
  • 压缩 GC 以减少后台内存使用和碎片

开发和调试方面的优化

  • 支持采样分析器

一直以来,开发者都使用 Traceview 工具(用于跟踪应用执行情况)作为分析器。虽然 Traceview 可提供有用的信息,但每次方法调用产生的开销会导致 Dalvik 分析结果出现偏差,而且使用该工具明显会影响运行时性能

ART 添加了对没有这些限制的专用采样分析器的支持,因而可更准确地了解应用执行情况,而不会明显减慢速度。KitKat 版本为 Dalvik 的 Traceview 添加了采样支持。

  • 支持更多调试功能

ART 支持许多新的调试选项,特别是与监控和垃圾回收相关的功能。例如,查看堆栈跟踪中保留了哪些锁,然后跳转到持有锁的线程;询问指定类的当前活动的实例数、请求查看实例,以及查看使对象保持有效状态的参考;过滤特定实例的事件(如断点)等。

  • 优化了异常和崩溃报告中的诊断详细信息

当发生运行时异常时,ART 会为您提供尽可能多的上下文和详细信息。ART 会提供 java.lang.ClassCastExceptionjava.lang.ClassNotFoundException 和 java.lang.NullPointerException 的更多异常详细信息(较高版本的 Dalvik 会提供 java.lang.ArrayIndexOutOfBoundsException 和 java.lang.ArrayStoreException 的更多异常详细信息,这些信息现在包括数组大小和越界偏移量;ART 也提供这类信息)。

ART GC

ART 有多个不同的 GC 方案,这些方案包括运行不同垃圾回收器。默认方案是 CMS(并发标记清除)方案,主要使用粘性 CMS 和部分 CMS。粘性 CMS 是 ART 的不移动分代垃圾回收器。它仅扫描堆中自上次 GC 后修改的部分,并且只能回收自上次 GC 后分配的对象。除 CMS 方案外,当应用将进程状态更改为察觉不到卡顿的进程状态(例如,后台或缓存)时,ART 将执行堆压缩。

除了新的垃圾回收器之外,ART 还引入了一种基于位图的新内存分配程序,称为 RosAlloc(插槽运行分配器)。此新分配器具有分片锁,当分配规模较小时可添加线程的本地缓冲区,因而性能优于 DlMalloc。

与 Dalvik 相比,ART CMS 垃圾回收计划在很多方面都有一定的改善:

  • 与 Dalvik 相比,暂停次数从 2 次减少到 1 次。Dalvik 的第一次暂停主要是为了进行根标记,即在 ART 中进行并发标记,让线程标记自己的根,然后马上恢复运行。
  • 与 Dalvik 类似,ART GC 在清除过程开始之前也会暂停 1 次。两者在这方面的主要差异在于:在此暂停期间,某些 Dalvik 环节在 ART 中并发进行。这些环节包括 java.lang.ref.Reference 处理、系统弱清除(例如,jni 弱全局等)、重新标记非线程根和卡片预清理。在 ART 暂停期间仍进行的阶段包括扫描脏卡片以及重新标记线程根,这些操作有助于缩短暂停时间。
  • 相对于 Dalvik,ART GC 改进的最后一个方面是粘性 CMS 回收器增加了 GC 吞吐量。不同于普通的分代 GC,粘性 CMS 不移动。系统会将年轻对象保存在一个分配堆栈(基本上是 java.lang.Object 数组)中,而非为其设置一个专属区域。这样可以避免移动所需的对象以维持低暂停次数,但缺点是容易在堆栈中加入大量复杂对象图像而使堆栈变长。

ART GC 与 Dalvik 的另一个主要区别在于 ART GC 引入了移动垃圾回收器。使用移动 GC 的目的在于通过堆压缩来减少后台应用使用的内存。目前,触发堆压缩的事件是 ActivityManager 进程状态的改变。当应用转到后台运行时,它会通知 ART 已进入不再“感知”卡顿的进程状态。此时 ART 会进行一些操作(例如,压缩和监视器压缩),从而导致应用线程长时间暂停。目前正在使用的两个移动 GC 是同构空间压缩和半空间压缩。

  • 半空间压缩将对象在两个紧密排列的碰撞指针空间之间进行移动。这种移动 GC 适用于小内存设备,因为它可以比同构空间压缩稍微多节省一点内存。额外节省出的空间主要来自紧密排列的对象,这样可以避免 RosAlloc/DlMalloc 分配器占用开销。由于 CMS 仍在前台使用,且不能从碰撞指针空间中进行收集,因此当应用在前台使用时,半空间还要再进行一次转换。这种情况并不理想,因为它可能引起较长时间的暂停。
  • 同构空间压缩通过将对象从一个 RosAlloc 空间复制到另一个 RosAlloc 空间来实现。这有助于通过减少堆碎片来减少内存使用量。这是目前非低内存设备的默认压缩模式。相比半空间压缩,同构空间压缩的主要优势在于应用从后台切换到前台时无需进行堆转换。

Hook

基本流程

1、根据需求确定 要 hook 的对象
2、寻找要hook的对象的持有者,拿到要 hook 的对象
3、定义“要 hook 的对象”的代理类,并且创建该类的对象
4、使用上一步创建出来的对象,替换掉要 hook 的对象

使用示例

/**
* hook的核心代码
* 这个方法的唯一目的:用自己的点击事件,替换掉 View 原来的点击事件
*
* @param view hook的范围仅限于这个view
*/
@SuppressLint({"DiscouragedPrivateApi", "PrivateApi"})
public static void hook(Context context, final View view) {//
try {
// 反射执行View类的getListenerInfo()方法,拿到v的mListenerInfo对象,这个对象就是点击事件的持有者
Method method = View.class.getDeclaredMethod("getListenerInfo");
method.setAccessible(true);//由于getListenerInfo()方法并不是public的,所以要加这个代码来保证访问权限
Object mListenerInfo = method.invoke(view);//这里拿到的就是mListenerInfo对象,也就是点击事件的持有者

// 要从这里面拿到当前的点击事件对象
Class<?> listenerInfoClz = Class.forName("android.view.View$ListenerInfo");// 这是内部类的表示方法
Field field = listenerInfoClz.getDeclaredField("mOnClickListener");
final View.OnClickListener onClickListenerInstance = (View.OnClickListener) field.get(mListenerInfo);//取得真实的mOnClickListener对象

// 2. 创建我们自己的点击事件代理类
// 方式1:自己创建代理类
// ProxyOnClickListener proxyOnClickListener = new ProxyOnClickListener(onClickListenerInstance);
// 方式2:由于View.OnClickListener是一个接口,所以可以直接用动态代理模式
// Proxy.newProxyInstance的3个参数依次分别是:
// 本地的类加载器;
// 代理类的对象所继承的接口(用Class数组表示,支持多个接口)
// 代理类的实际逻辑,封装在new出来的InvocationHandler内
Object proxyOnClickListener = Proxy.newProxyInstance(context.getClass().getClassLoader(), new Class[]{View.OnClickListener.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.d("HookSetOnClickListener", "点击事件被hook到了");//加入自己的逻辑
return method.invoke(onClickListenerInstance, args);//执行被代理的对象的逻辑
}
});
// 3. 用我们自己的点击事件代理类,设置到"持有者"中
field.set(mListenerInfo, proxyOnClickListener);
} catch (Exception e) {
e.printStackTrace();
}
}

// 自定义代理类
static class ProxyOnClickListener implements View.OnClickListener {
View.OnClickListener oriLis;

public ProxyOnClickListener(View.OnClickListener oriLis) {
this.oriLis = oriLis;
}

@Override
public void onClick(View v) {
Log.d("HookSetOnClickListener", "点击事件被hook到了");
if (oriLis != null) {
oriLis.onClick(v);
}
}
}

Proguard

Proguard 具有以下三个功能:

  • 压缩(Shrink): 检测和删除没有使用的类,字段,方法和特性
  • 优化(Optimize) : 分析和优化Java字节码
  • 混淆(Obfuscate): 使用简短的无意义的名称,对类,字段和方法进行重命名

规则

  • 关键字
关键字描述
keep保留类和类中的成员,防止被混淆或移除
keepnames保留类和类中的成员,防止被混淆,成员没有被引用会被移除
keepclassmembers只保留类中的成员,防止被混淆或移除
keepclassmembernames只保留类中的成员,防止被混淆,成员没有引用会被移除
keepclasseswithmembers保留类和类中的成员,防止被混淆或移除,保留指明的成员
keepclasseswithmembernames保留类和类中的成员,防止被混淆,保留指明的成员,成员没有引用会被移除
  • 通配符
通配符描述
匹配类中的所有字段
匹配类中所有的方法
匹配类中所有的构造函数
*匹配任意长度字符,不包含包名分隔符(.)
**匹配任意长度字符,包含包名分隔符(.)
***匹配任意参数类型
  • 指定混淆时可使用字典
-applymapping filename 指定重用一个已经写好了的map文件作为新旧元素名的映射。
-obfuscationdictionary filename 指定一个文本文件用来生成混淆后的名字。
-classobfuscationdictionary filename 指定一个混淆类名的字典
-packageobfuscationdictionary filename 指定一个混淆包名的字典
-overloadaggressively 混淆的时候大量使用重载,多个方法名使用同一个混淆名(慎用)

公共模板

#############################################
#
# 对于一些基本指令的添加
#
#############################################
# 代码混淆压缩比,在 0~7 之间,默认为 5,一般不做修改
-optimizationpasses 5

# 混合时不使用大小写混合,混合后的类名为小写
-dontusemixedcaseclassnames

# 指定不去忽略非公共库的类
-dontskipnonpubliclibraryclasses

# 这句话能够使我们的项目混淆后产生映射文件
# 包含有类名->混淆后类名的映射关系
-verbose

# 指定不去忽略非公共库的类成员
-dontskipnonpubliclibraryclassmembers

# 不做预校验,preverify 是 proguard 的四个步骤之一,Android 不需要 preverify,去掉这一步能够加快混淆速度。
-dontpreverify

# 保留 Annotation 不混淆
-keepattributes *Annotation*,InnerClasses

# 避免混淆泛型
-keepattributes Signature

# 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable

# 指定混淆是采用的算法,后面的参数是一个过滤器
# 这个过滤器是谷歌推荐的算法,一般不做更改
-optimizations !code/simplification/cast,!field/*,!class/merging/*


#############################################
#
# Android开发中一些需要保留的公共部分
#
#############################################

# 保留我们使用的四大组件,自定义的 Application 等等这些类不被混淆
# 因为这些子类都有可能被外部调用
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService


# 保留 support 下的所有类及其内部类
-keep class android.support.** { *; }

# 保留继承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**

# 保留 R 下面的资源
-keep class **.R$* { *; }

# 保留本地 native 方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}

# 保留在 Activity 中的方法参数是view的方法,
# 这样以来我们在 layout 中写的 onClick 就不会被影响
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}

# 保留枚举类不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}

# 保留我们自定义控件(继承自 View)不被混淆
-keep public class * extends android.view.View {
*** get*();
void set*(***);
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}

# 保留 Parcelable 序列化类不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}

# 保留 Serializable 序列化的类不被混淆
-keepnames class * implements java.io.Serializable
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
!private <fields>;
!private <methods>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}

# 对于带有回调函数的 onXXEvent、**On*Listener 的,不能被混淆
-keepclassmembers class * {
void *(**On*Event);
void *(**On*Listener);
}

# webView 处理,项目中没有使用到 webView 忽略即可
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
public *;
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.webView, java.lang.String);
}

# js
-keepattributes JavascriptInterface
-keep class android.webkit.JavascriptInterface { *; }
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}

# @Keep
-keep,allowobfuscation @interface android.support.annotation.Keep
-keep @android.support.annotation.Keep class *
-keepclassmembers class * {
@android.support.annotation.Keep *;
}

常用的自定义混淆规则

# 通配符*,匹配任意长度字符,但不含包名分隔符(.)
# 通配符**,匹配任意长度字符,并且包含包名分隔符(.)

# 不混淆某个类
-keep public class com.jasonwu.demo.Test { *; }

# 不混淆某个包所有的类
-keep class com.jasonwu.demo.test.** { *; }

# 不混淆某个类的子类
-keep public class * com.jasonwu.demo.Test { *; }

# 不混淆所有类名中包含了 ``model`` 的类及其成员
-keep public class **.*model*.** {*;}

# 不混淆某个接口的实现
-keep class * implements com.jasonwu.demo.TestInterface { *; }

# 不混淆某个类的构造方法
-keepclassmembers class com.jasonwu.demo.Test {
public <init>();
}

# 不混淆某个类的特定的方法
-keepclassmembers class com.jasonwu.demo.Test {
public void test(java.lang.String);
}

aar中增加独立的混淆配置

build.gralde

android {
···
defaultConfig {
···
consumerProguardFile 'proguard-rules.pro'
}
···
}

检查混淆和追踪异常

开启 Proguard 功能,则每次构建时 ProGuard 都会输出下列文件:

  • dump.txt
    说明 APK 中所有类文件的内部结构。
  • mapping.txt
    提供原始与混淆过的类、方法和字段名称之间的转换。
  • seeds.txt
    列出未进行混淆的类和成员。
  • usage.txt
    列出从 APK 移除的代码。

这些文件保存在 /build/outputs/mapping/release/ 中。我们可以查看 seeds.txt 里面是否是我们需要保留的,以及 usage.txt 里查看是否有误删除的代码。 mapping.txt 文件很重要,由于我们的部分代码是经过重命名的,如果该部分出现 bug,对应的异常堆栈信息里的类或成员也是经过重命名的,难以定位问题。我们可以用 retrace 脚本(在 Windows 上为 retrace.bat;在 Mac/Linux 上为 retrace.sh)。它位于 /tools/proguard/ 目录中。该脚本利用 mapping.txt 文件和你的异常堆栈文件生成没有经过混淆的异常堆栈文件,这样就可以看清是哪里出问题了。使用 retrace 工具的语法如下:

retrace.bat|retrace.sh [-verbose] mapping.txt [<stacktrace_file>]

架构

MVC

在 Android 中,三者的关系如下:

由于在 Android 中 xml 布局的功能性太弱,所以 Activity 承担了绝大部分的工作,所以在 Android 中 mvc 更像:

总结:

  • 具有一定的分层,model 解耦,controller 和 view 并没有解耦
  • controller 和 view 在 Android 中无法做到彻底分离,Controller 变得臃肿不堪
  • 易于理解、开发速度快、可维护性高

MVP

通过引入接口 BaseView,让相应的视图组件如 Activity,Fragment去实现 BaseView,把业务逻辑放在 presenter 层中,弱化 Model 只有跟 view 相关的操作都由 View 层去完成。

总结:

  • 彻底解决了 MVC 中 View 和 Controller 傻傻分不清楚的问题
  • 但是随着业务逻辑的增加,一个页面可能会非常复杂,UI 的改变是非常多,会有非常多的 case,这样就会造成 View 的接口会很庞大
  • 更容易单元测试

MVVM

在 MVP 中 View 和 Presenter 要相互持有,方便调用对方,而在 MVP 中 View 和 ViewModel 通过 Binding 进行关联,他们之前的关联处理通过 DataBinding 完成。

总结:

  • 很好的解决了 MVC 和 MVP 的问题
  • 视图状态较多,ViewModel 的构建和维护的成本都会比较高
  • 但是由于数据和视图的双向绑定,导致出现问题时不太好定位来源

Jetpack

架构

CMake 构建 NDK 项目

CMake 是一个开源的跨平台工具系列,旨在构建,测试和打包软件,从 Android Studio 2.2 开始,Android Sudio 默认地使用 CMake 与 Gradle 搭配使用来构建原生库。

启动方式只需要在 app/build.gradle 中添加相关:

android {
···
defaultConfig {
···
externalNativeBuild {
cmake {
cppFlags ""
}
}

ndk {
abiFilters 'arm64-v8a', 'armeabi-v7a'
}
}
···
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}

然后在对应目录新建一个 CMakeLists.txt 文件:

# 定义了所需 CMake 的最低版本
cmake_minimum_required(VERSION 3.4.1)

# add_library() 命令用来添加库
# native-lib 对应着生成的库的名字
# SHARED 代表为分享库
# src/main/cpp/native-lib.cpp 则是指明了源文件的路径。
add_library( # Sets the name of the library.
native-lib

# Sets the library as a shared library.
SHARED

# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp)

# find_library 命令添加到 CMake 构建脚本中以定位 NDK 库,并将其路径存储为一个变量。
# 可以使用此变量在构建脚本的其他部分引用 NDK 库
find_library( # Sets the name of the path variable.
log-lib

# Specifies the name of the NDK library that
# you want CMake to locate.
log)

# 预构建的 NDK 库已经存在于 Android 平台上,因此,无需再构建或将其打包到 APK 中。
# 由于 NDK 库已经是 CMake 搜索路径的一部分,只需要向 CMake 提供希望使用的库的名称,并将其关联到自己的原生库中

# 要将预构建库关联到自己的原生库
target_link_libraries( # Specifies the target library.
native-lib

# Links the target library to the log library
# included in the NDK.
${log-lib})
···

常用的 Android NDK 原生 API

支持 NDK 的 API 级别关键原生 API包括
3Java 原生接口#include <jni.h>
3Android 日志记录 API#include <android/log.h>
5OpenGL ES 2.0#include <GLES2/gl2.h> #include <GLES2/gl2ext.h>
8Android 位图 API#include <android/bitmap.h>
9OpenSL ES#include <SLES/OpenSLES.h> #include <SLES/OpenSLES_Platform.h> #include <SLES/OpenSLES_Android.h> #include <SLES/OpenSLES_AndroidConfiguration.h>
9原生应用 API#include <android/rect.h> #include <android/window.h> #include<android/native_activity.h> ···
18OpenGL ES 3.0#include <GLES3/gl3.h> #include <GLES3/gl3ext.h>
21原生媒体 API#include <media/NdkMediaCodec.h> #include <media/NdkMediaCrypto.h> ···
24原生相机 API#include <camera/NdkCameraCaptureSession.h> #include <camera/NdkCameraDevice.h> ···
···

类加载器

双亲委托模式

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子 ClassLoader 再加载一次。如果不使用这种委托模式,那我们就可以随时使用自定义的类来动态替代一些核心的类,存在非常大的安全隐患。

DexPathList

DexClassLoader 重载了 findClass 方法,在加载类时会调用其内部的 DexPathList 去加载。DexPathList 是在构造 DexClassLoader 时生成的,其内部包含了 DexFile。

DexPathList.java
public Class findClass(String name) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext);
if (clazz != null) {
return clazz;
}
}
}
return null;
}
收起阅读 »

Android自定义控件六边形

Android自定义六边形控件一.效果图原文地址: https://blog.csdn.net/oMengHui/article/details/45540645二.核心算法平面内一个坐标点是否在多边形内判断,使用射线法判断。从目标点出发引一条射线,...
继续阅读 »



Android自定义六边形控件

一.效果图

原文地址: https://blog.csdn.net/oMengHui/article/details/45540645

20150506195536825.gif

二.核心算法
平面内一个坐标点是否在多边形内判断,使用射线法判断。从目标点出发引一条射线,看这条射线和多边形所有边的交点数目。如果是奇数个交点,则说明点在多边形内部;如果是偶数个交点,则说明在外部。

20150506195740393.jpeg

算法图解:

20150506195748111.jpeg

参考代码:

int pnpoly(int nvert, float *vertx, float *verty, float testx, float testy)
{
int i, j, c = 0;
for (i = 0, j = nvert-1; i < nvert; j = i++)
{
if ( ((verty[i]>testy) != (verty[j]>testy)) &&
(testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) )
c = !c;
}
return c;
}
复制代码

更多参考信息

三.知识点
1.控件属性自定义和使用 在values->attrs->declare-styleable中定义属性;在布局中引入(格式xmls:sec=”schemas.android.com/apk/res/程序包…;

2.Paint画笔使用 class继承View后重写onDraw方法,Paint paint=new Paint().setStyle(Style.FILL); canvas.drawText(“Hello”,x,y,paint);

3.Path路径使用 Path path=new Path(); path.moveTo(x1,y1); path.lineTo(x2,y2); path.close(); canvas.drawPath(path,paint);

4.图片缩放平铺居中 六边形视图显示为正方形,如属性设置图片宽高不相等直接使用图片会被拉伸变形。通过逻辑处理以图片宽高较小值居中裁剪图片。

 /**
* 按宽/高缩放图片到指定大小并进行裁剪得到中间部分图片
*
* @param bitmap 源bitmap
* @param w 缩放后指定的宽度
* @param h 缩放后指定的高度
* @return 缩放后的中间部分图片
*/
public static Bitmap zoomBitmap(Bitmap bitmap, int w, int h) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
float scaleWidht, scaleHeight, x, y;
Bitmap newbmp;
Matrix matrix = new Matrix();
if (width > height) {
scaleWidht = ((float) h / height);
scaleHeight = ((float) h / height);
x = (width - w * height / h) / 2;// 获取bitmap源文件中x做表需要偏移的像数大小
y = 0;
} else if (width < height) {
scaleWidht = ((float) w / width);
scaleHeight = ((float) w / width);
x = 0;
y = (height - h * width / w) / 2;// 获取bitmap源文件中y做表需要偏移的像数大小
} else {
scaleWidht = ((float) w / width);
scaleHeight = ((float) w / width);
x = 0;
y = 0;
}
matrix.postScale(scaleWidht, scaleHeight);
try {
newbmp = Bitmap.createBitmap(bitmap, (int) x, (int) y,
(int) (width - x), (int) (height - y), matrix, true);// createBitmap()方法中定义的参数x+width要小于或等于bitmap.getWidth(),y+height要小于或等于bitmap.getHeight()
} catch (Exception e) {
e.printStackTrace();
return null;
}
return newbmp;
}
复制代码

5.动画(ScaleAnimation)

Animation scaleAnimation = new ScaleAnimation(start, end, start, end,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
scaleAnimation.setDuration(30);
scaleAnimation.setFillAfter(true);
this.startAnimation(endAnimation);
复制代码

6.监听实现

HexagonView.java ->
public interface OnHexagonViewClickListener {
public void onClick(View view);
}
public void setOnHexagonClickListener(OnHexagonViewClickListener listener) {
this.listener = listener;
}
OnHexagonViewClickListener hexagonListener=new OnHexagonViewClickListener();//实例化
/**
*系统onTouchEvent
*/
public boolean onTouchEvent(MotionEvent event){
if(!isOn){//未点中六边形
break;
}
switch(event.getAction()){
case MotionEvent.ACTION_UP:
if(hexagonListener!=null){
hexagonListener.click(this);
}
break;
}
}

MainActivity->
public class MainActivity extends Activity implements HexagonView.OnHexagonViewClickListener{
HexagonView hexagonViewHello;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
hexagonViewHello=(HexagonView)this.findViewById(R.id.hexagonviewhello);
hexagonViewHello.setOnHexagonClickListener(this);
}
/**
* 事件监听
*/
public void onClick(View view){
Log.d(TAG,"onClick()");
switch (view.getId()){
case R.id.hexagonviewhello:
Toast.makeText(this,"Hello",Toast.LENGTH_SHORT).show();
break;
}
}

}
收起阅读 »

Kotlin协程的取消和异常传播机制

1.协程核心概念回顾结构化并发(Structured Concurrency)作用域(CoroutineScope /SupervisorScope)作业(Job/SupervisorJob)开启协程(launch/async)2.协程的取消2.1 协程的取消...
继续阅读 »

1.协程核心概念回顾

结构化并发(Structured Concurrency)

作用域(CoroutineScope /SupervisorScope)

作业(Job/SupervisorJob)

开启协程(launch/async)

2.协程的取消

2.1 协程的取消操作

Job生命周期

  • 作用域或作业的取消

示例代码

   suspend fun c01_cancle() {
val scope = CoroutineScope(Job())
val job1 = scope.launch { }
val job2 = scope.launch { }
//取消作业
job1.cancel()
job2.cancel()
//取消作用域
scope.cancel()

}

注意:不能在已取消的作用域中再开启协程

2.2确保协程可以被取消

  • 协程的取消只是标记了协程的取消状态,并未真正取消协程

示例代码:

  val job = launch(Dispatchers.Default) {
var i = 0
while (i < 5) {
println("Hello ${i++}")
Thread.sleep(100)
}
}
delay(200)
println("Cancel!")
job.cancel()

打印结果://未真正取消,直接检查

Hello 0
Hello 1
Hello 2
Cancel!
Hello 3
Hello 4

  • 可以用 isActive ensureActive() yield来在关键位置做检查,确保协程可以正常关闭
   val job = launch(Dispatchers.Default) {
var i = 0
while (i < 5 && isActive) {//方法1
ensureActive()//方法2
yield()//方法3
println("Hello ${i++}")
Thread.sleep(100)
}
}
delay(200)
println("Cancel!")
job.cancel()

2.3 协程取消后的资源关闭

  • try/finally可以关闭资源
 launch {
try {
openIo()//开启文件io
delay(100)
throw ArithmeticException()
} finally {
println("协程结束")
closeIo()//关闭文件io
}
}
  • 注意:finally中不能调用挂起函数(如果一定要调用,需要用withContext(NonCancellable),不推荐使用)
   launch {
try {
work()
} finally {
//withContext(NonCancellable)可以执行,不然不会再被执行
withContext(NonCancellable) {
delay(1000L) // 挂起方法
println("Cleanup done!")
}
}
}

2.4 CancellationException 会被忽略

  val job = launch {
try {
delay(Long.MAX_VALUE)
} catch (e: Exception) {
println("捕获到一个异常$e")
//打印:捕获到一个异常java.util.concurrent.CancellationException: 我是一个取消异常
}
}
yield()
job.cancel(CancellationException("我是一个取消异常"))
job.join()

3.协程的异常传播机制

3.1 捕捉协程异常

3.1.1 try/catch

  • try/catch业务代码
 launch {
try {
throw ArithmeticException("计算错误")
} catch (e: Exception) {
println("捕获到一个异常$e")
}
}
//打印:捕获到一个异常java.lang.ArithmeticException: 计算错误
  • try/catch协程
  try {
launch {
throw ArithmeticException("计算错误")
}
} catch (e: Exception) {
println("捕获到一个异常$e")
}

//无法捕捉到 error日志
Exception in thread "main" java.lang.ArithmeticException: 计算错误
at com.jinbo.kotlin.coroutine.C05_Exception$testDemo$2$1.invokeSuspend(C05_Exception.kt:65)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:84)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.jinbo.kotlin.coroutine.C05_Exception.main(C05_Exception.kt:17)
  • 无法通过外部try-catch语句来捕获协程异常

3.1.2 CoroutineExceptionHandler 捕捉异常

  supervisorScope {
val exceptionHandler = CoroutineExceptionHandler { _, e ->
println("捕获到一个异常$e")
}
launch(exceptionHandler) {
throw ArithmeticException("计算错误")
}
}
//捕获到一个异常java.lang.ArithmeticException: 计算错误

3.1.3 runCatching 捕捉异常

  val catching = kotlin.runCatching {
"hello"
throw ArithmeticException("我是一个异常")
}
if (catching.isSuccess) {
println("正常结果是${catching.getOrNull()}")
} else {
println("失败了,原因是:${catching.exceptionOrNull()}")
}

这时,就要介绍协程的异常传播机制

3.2 协同作用域的传播机制

3.2.1 特性

  • 双向传播,取消子协程,取消自己,向父协程传播

[协同作用域传播特性] 示意图

  coroutineScope {
launch {
launch {
//子协程的异常,会向上传播
throw ArithmeticException() }
}
launch {
launch { }
}
}

3.2.2 子协程无法捕获自己的异常,只有父协程才可以


val scope = CoroutineScope(Job())
//父协程(根协程)才可以捕获异常
scope.launch(exceptionHandler) {
launch {
throw ArithmeticException("我是一个子异常")
}
//这时不会捕获到,会向上传播
// launch(exceptionHandler) {
// throw ArithmeticException("我是另外一个子异常")
// }
}

3.2.3 当父协程的所有子协程都结束后,原始的异常才会被父协程处理

val handler = CoroutineExceptionHandler { _, exception ->
println("捕捉到异常: $exception")
}
val job = GlobalScope.launch(handler) {
launch { // 第一个子协程
try {
delay(Long.MAX_VALUE)
} finally {
withContext(NonCancellable) {
println("第一个子协程还在运行,所以暂时不会处理异常")
delay(100)
println("现在子协程处理完成了")
}
}
}
launch { // 第二个子协程
delay(10)
println("第二个子协程出异常了")
throw ArithmeticException()
}
}
job.join()

//打印结棍:

第二个子协程出异常了
第一个子协程还在运行,所以暂时不会处理异常
现在子协程处理完成了
捕捉到异常: java.lang.ArithmeticException

3.2.4 异常聚合

第 1 个发生的异常会被优先y处理,在此之后发生的所有其他异常会被添加到最先发生的异常上, 作为被压制(suppressed)的异常

  val handler = CoroutineExceptionHandler { _, exception ->
println("捕捉到异常: $exception ${exception.suppressed.contentToString()}")
}
val job = GlobalScope.launch(handler) {
launch {
delay(100)
throw IOException() // 第一个异常
}
launch {
try {
delay(Long.MAX_VALUE) // 当另一个同级的协程因 IOException 失败时,它将被取消
} finally {
throw ArithmeticException() // 同时抛出第二个异常
}
}

delay(Long.MAX_VALUE)
}
job.join()

输出:
捕捉到异常: java.io.IOException [java.lang.ArithmeticException]

3.2.5 launch 和 async异常处理

  • launch 直接抛出异常,无等待
  launch {
throw ArithmeticException("launch异常")
}

//打印
Exception in thread "main" java.lang.ArithmeticException: launch异常
  • async预期会在用户调用await()时,再反馈异常

直接在根协程(GlobalScope) 或 supervisor子协程时,async会在await()时抛出异常

  supervisorScope {
val deferred = async {
throw ArithmeticException("异常")
}
}
//打印结果:空
  • 在await()时才抛出异常
  supervisorScope {
val deferred = async {
throw ArithmeticException("异常")
}
try {
deferred.await()
} catch (e: Exception) {
println("捕获到一个异常$e")
}
}
//打印结果:
捕获到一个异常java.lang.ArithmeticException: 异常
  • tips: 如果不是直接在根协程(GlobalScope) 或 supervisor子协程时,async 和 launch表现一致,直接抛出异常,不会在await()时,再抛出异常
  supervisorScope {
launch {
val deferred = async {
throw ArithmeticException("异常")
}
}
}

3.2.6 coroutineScope外部可以用try-catch捕获(supervisor不可以)

 try {
coroutineScope {
launch {
throw ArithmeticException("异常")
}
}
} catch (e: Exception) {
println("捕捉到异常:$e")
}

//打印结果:
捕捉到异常:java.lang.ArithmeticException: 异常

3.3 监督作用域的传播机制

3.3.1 特性 单向向下传播

  • 监督作用域的传播机制 (独立决策的权利?)

示意图

- supervisor的示例代码

3.3.2 子协程可以单独设置CoroutineExceptionHandler

 supervisorScope {
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}

打印结果:
发现了异常java.lang.ArithmeticException: 异常出现了

3.3.3 监督作业只对它直接的子协程有用

  supervisorScope {
//监督作业只对它直接的子协程有用
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}
-无效示例代码
supervisorScope {
launch {
//监督作业的子子协程无法独立处理异常,向上抛异常
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}
}

//打印结果:
Exception in thread "main" java.lang.ArithmeticException: 异常出现了
at com.jinbo.kotlin.coroutine.C05_Exception$testDemo$2$1$1$1.invokeSuspend(C05_Exception.kt:1039)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:84)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.jinbo.kotlin.coroutine.C05_Exception.main(C05_Exception.kt:17)

3.4 正确使用coroutineExceptionHandler

3.4.1 根协程(GlobalScope)//TODO 确认

  GlobalScope.launch(exceptionHandler) {  }

3.4.2 supervisorScope 直接子级

 supervisorScope {
launch(exceptionHandler) {
throw ArithmeticException("异常出现了")
}
}

3.4.3 手动创建的Scope(Job()/SupervisorJob())

 val scope = CoroutineScope(Job())
scope.launch(exceptionHandler) {
throw ArithmeticException("异常")
}

4 思考

4.1 android 的协同

  • viewmodelScope lifecycleScope

收起阅读 »

DiffUtil 让 RecyclerView 更好用

DiffUtil 让 RecyclerView 更好用前几天在写局部刷新RecyclerView时,评论区有掘友提到了DiffUtil,说实话,确实没有在项目中用到过,查了资料,DiffUtil帮我们做了很多刷新很多工作,真香。DiffUtil是什么DiffU...
继续阅读 »

DiffUtil 让 RecyclerView 更好用

前几天在写局部刷新RecyclerView时,评论区有掘友提到了DiffUtil,说实话,确实没有在项目中用到过,查了资料,DiffUtil帮我们做了很多刷新很多工作,真香。

DiffUtil是什么

DiffUtil 是来自recycleview-v7下的工具类,Diff 直接翻译过来是 差异、对比,所以这个工具类主要帮助我们对比两个数据集,寻找出最小的变化量。那么它和RecyclerView有什么关系呢,实际上我们只要把新旧数据集给到DiffUtil,那么它就会自动帮我们对比数据,并且刷新适配器,而不用我们判断,是增加了删除了等等,DiffUtil对比之后自动帮我们搞定,这就是它非常好用的地方了

常规的适配器

我们先用RecyclerView写一个常规的列表。
它拥有刷新item和item局部刷新的功能。
代码如下:

MainActivity: 主界面

public class MainActivity extends AppCompatActivity {

private RecyclerView recyclerView;

private List<PersonInfo> mDatas;
private PersonAdapter personAdapter;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

recyclerView = findViewById(R.id.recyclerView);
initData();
recyclerView.setLayoutManager(new LinearLayoutManager(this));
personAdapter = new PersonAdapter(this, mDatas);
recyclerView.setAdapter(personAdapter);
}

private void initData() {
mDatas = new ArrayList<>();
mDatas.add(new PersonInfo(1, "姓名1"));
mDatas.add(new PersonInfo(2, "姓名2"));
mDatas.add(new PersonInfo(3, "姓名3"));
mDatas.add(new PersonInfo(4, "姓名4"));
mDatas.add(new PersonInfo(5, "姓名5"));
mDatas.add(new PersonInfo(6, "姓名6"));
mDatas.add(new PersonInfo(7, "姓名7"));
mDatas.add(new PersonInfo(8, "姓名8"));
mDatas.add(new PersonInfo(9, "姓名9"));
mDatas.add(new PersonInfo(10, "姓名10"));
mDatas.add(new PersonInfo(11, "姓名11"));
}

public void ADD(View view) {
int position = mDatas.size();
List<PersonInfo> tempData = new ArrayList<>();

tempData.add(new PersonInfo(12, "姓名12"));
tempData.add(new PersonInfo(13, "姓名13"));
tempData.add(new PersonInfo(14, "姓名114"));

mDatas.addAll(tempData);
personAdapter.notifyItemRangeInserted(position, tempData.size());
}

public void DELETE(View view) {
mDatas.remove(1);
personAdapter.notifyItemRemoved(1);
}

public void UPDATE(View view) {
mDatas.get(1).setName("姓名:我被更新了");
personAdapter.notifyItemChanged(1);
}

public void UPDATE2(View view) {
mDatas.get(1).setName("姓名:我被更新了");

Bundle payload = new Bundle();
payload.putString("KEY_NAME", mDatas.get(1).getName());
personAdapter.notifyItemChanged(1, payload);
}
}
复制代码

PersonAdapter: 适配器

public class PersonAdapter extends RecyclerView.Adapter<PersonAdapter.DiffVH> {
private List<PersonInfo> mDatas;
private LayoutInflater mInflater;

public PersonAdapter(Context context, List<PersonInfo> mDatas) {
this.mDatas = mDatas;
mInflater = LayoutInflater.from(context);
}

public void setDatas(List<PersonInfo> mDatas) {
this.mDatas = mDatas;
}

@Override
public DiffVH onCreateViewHolder(ViewGroup parent, int viewType) {
return new DiffVH(mInflater.inflate(R.layout.item_person, parent, false));
}

@Override
public void onBindViewHolder(final DiffVH holder, final int position) {
PersonInfo personInfo = mDatas.get(position);
holder.tv_index.setText(String.valueOf(personInfo.getIndex()));
holder.tv_name.setText(String.valueOf(personInfo.getName()));
}

@Override
public void onBindViewHolder(DiffVH holder, int position, List<Object> payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position);
} else {
Bundle payload = (Bundle) payloads.get(0);
PersonInfo bean = mDatas.get(position);
for (String key : payload.keySet()) {
switch (key) {
case "KEY_INDEX":
holder.tv_index.setText(String.valueOf(bean.getIndex()));
break;
case "KEY_NAME":
holder.tv_name.setText(String.valueOf(bean.getName()));
break;
default:
break;
}
}
}
}

@Override
public int getItemCount() {
return mDatas != null ? mDatas.size() : 0;
}

class DiffVH extends RecyclerView.ViewHolder {
TextView tv_index;
TextView tv_name;

public DiffVH(View view) {
super(view);
tv_index = view.findViewById(R.id.tv_index);
tv_name = view.findViewById(R.id.tv_name);
}
}
}

复制代码

PersonInfo: 实体类

public class PersonInfo {
private int index;
private String name;

public PersonInfo(int index, String name) {
this.index = index;
this.name = name;
}

public int getIndex() {
return index;
}

public void setIndex(int index) {
this.index = index;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
复制代码

activity_main

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="增加"
android:onClick="ADD"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除"
android:onClick="DELETE"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="修改"
android:onClick="UPDATE"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="局部更新"
android:onClick="UPDATE2"/>

</LinearLayout>


<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />


</LinearLayout>

复制代码

item_person

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:context=".MainActivity">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@color/purple_200"
android:orientation="horizontal"
android:padding="5dp">

<TextView
android:id="@+id/tv_index"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />

<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/tv_index" />

</RelativeLayout>


</LinearLayout>
复制代码

引入DiffUtil

我们创建一个DiffUtil类,在需要更新的时候,用DiffUtil中的方法去代替原本的刷新方法。

用新增举例,这样就能达到更新的目的

public void ADD(View view) {
List<PersonInfo> newData = new ArrayList<>();
newData.addAll(mDatas);
newData.add(new PersonInfo(12, "姓名12"));
newData.add(new PersonInfo(13, "姓名13"));
newData.add(new PersonInfo(14, "姓名114"));

DiffUtil.calculateDiff(new DiffUtilCallBack(newData,mDatas), true).dispatchUpdatesTo(personAdapter);
mDatas = newData;
personAdapter.setDatas(mDatas);
}

复制代码

DiffUtilCallBack

public class DiffUtilCallBack extends DiffUtil.Callback {
private List<PersonInfo> newlist;
private List<PersonInfo> oldlist;

public DiffUtilCallBack(List<PersonInfo> newlist, List<PersonInfo> oldlist) {
this.newlist = newlist;
this.oldlist = oldlist;
}

@Override
public int getOldListSize() {
return oldlist.size();
}

@Override
public int getNewListSize() {
return newlist.size();
}

@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
//判断是否是同一个item,可以在这里处理 判断是否是相同item的逻辑,比如id之类的
return newlist.get(newItemPosition).getIndex() == oldlist.get(oldItemPosition).getIndex();
}

@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
//判断数据是否发生改变,这个 方法会在上面的方法返回true时调用, 因为虽然item是同一个,但有可能item的数据发生了改变
return newlist.get(newItemPosition).getName().equals(oldlist.get(oldItemPosition).getName());
}
}
复制代码

万能适配器中的DiffUtil

配合RecyclerView,我一直在使用万能适配器(BaseRecyclerViewAdapterHelper),如果你也习惯了使用万能适配器,在它的3.0方法中引入了对DiffUtil的支持。

直接看官方文档吧。

代码下载:https://github.com/CymChad/BaseRecyclerViewAdapterHelper/archive/refs/heads/master.zip

收起阅读 »

iOS 上的 WebSocket 框架 Starscream

iOS
Starscream实现Websocket通讯1.Starscream 简介2.Starscream 使用2.1 Starscream基本使用2.2 Starscream高阶使用2.2.1 判断是否连接2.2.2 自定义头文件2.2.3 自定义HTTP方法2....
继续阅读 »

Starscream实现Websocket通讯
1.Starscream 简介
2.Starscream 使用
2.1 Starscream基本使用
2.2 Starscream高阶使用
2.2.1 判断是否连接
2.2.2 自定义头文件
2.2.3 自定义HTTP方法
2.2.4 协议
2.2.5 自签名 SSL
2.2.5.1 SSL引脚
2.2.5.2 SSL密码套件
2.2.6 压缩扩展
2.2.7 自定义队列
2.2.8 高级代理
3.Starscream 使用Demo
1.Starscream 简介

Starscream的特征:

Conforms to all of the base Autobahn test suite.
Nonblocking. Everything happens in the background, thanks to GCD.
TLS/WSS support.
Compression Extensions support (RFC 7692)
Simple concise codebase at just a few hundred LOC.
什么是websocket:
WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。
在 WebSocket API,浏览器和服务器只需要要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

HTTP 第一次出现是 1991 年,它设计为一种请求/响应式的通讯机制。Web 浏览器用这种机制工作良好,用户请求 web 页,服务器返回内容。但某些时候,需要有新数据时不经过用户请求就通知用户——也就是,服务器推。
HTTP 协议无法很好地解决推模型。在 websocket 出现前,web 服务通过一系列浏览器刷新机制来实现推模型,但效率无法让人满意。
webSocket 实现了服务端推机制。新的 web 浏览器全都支持 WebSocket,这使得它的使用超级简单。通过 WebSocket 能够打开持久连接,大部分网络都能轻松处理 WebSocket 连接。
WebSocket 通常应用在某些数据经常性或频繁改变的场景。例如 Facebook 中的 web 通知、Slack 中的实时聊天、交易系统中的变化的股票价格
socket通讯过程:


集成Websocket:

开发中推荐使用Starscream框架。通过pod 方式导入:

pod 'Starscream'
1
Starscream 使用swift版本为4.2
2.Starscream 使用
2.1 Starscream基本使用
import UIKit
import Starscream
@objc public protocol DSWebSocketDelegate: NSObjectProtocol{
/**websocket 连接成功*/
optional func websocketDidConnect(sock: DSWebSocket)
/**websocket 连接失败*/
optional func websocketDidDisconnect(socket: DSWebSocket, error: NSError?)
/**websocket 接受文字信息*/
func websocketDidReceiveMessage(socket: DSWebSocket, text: String)
/ **websocket 接受二进制信息*/
optional func websocketDidReceiveData(socket: DSWebSocket, data: NSData)
}
public class DSWebSocket: NSObject,WebSocketDelegate {
var socket:WebSocket!
weak var webSocketDelegate: DSWebSocketDelegate?
//单例
class func sharedInstance() -> DSWebSocket
{
return manger
}
static let manger: DSWebSocket = {
return DSWebSocket()
}()

//MARK:- 链接服务器
func connectSever(){
socket = WebSocket(url: NSURL(string: 你的URL网址如:ws://192.168.3.209:8080/shop))
socket.delegate = self
socket.connect()
}

//发送文字消息
func sendBrandStr(brandID:String){
socket.writeString(brandID))
}
//MARK:- 关闭消息
func disconnect(){
socket.disconnect()
}

//MARK: - WebSocketDelegate
//客户端连接到服务器时,websocketDidConnect将被调用。
public func websocketDidConnect(socket: WebSocket){
debugPrint("连接成功了: \(error?.localizedDescription)")
webSocketDelegate?.websocketDidConnect!(self)
}
//客户端与服务器断开连接后,将立即调用 websocketDidDisconnect。
public func websocketDidDisconnect(socket: WebSocket, error: NSError?){
debugPrint("连接失败了: \(error?.localizedDescription)")
webSocketDelegate?.websocketDidDisconnect!(self, error: error)
}
//当客户端从连接获取一个文本框时,调用 websocketDidReceiveMessage。
//注:一般返回的都是字符串
public func websocketDidReceiveMessage(socket: WebSocket, text: String){
debugPrint("接受到消息了: \(error?.localizedDescription)")
webSocketDelegate?.websocketDidReceiveMessage!(self, text: text)
}
public func websocketDidReceiveData(socket: WebSocket, data: NSData){
debugPrint("data数据")
webSocketDelegate?.websocketDidReceiveData!(self, data: data)
}
}

编写一个pong框架
writePong方法与writePing相同,但发送一个pong控制帧。
socket.write(pong: Data()) //example on how to write a pong control frame over the socket!
1
Starscream会自动响应传入的ping 控制帧,这样你就不需要手动发送 pong。

但是,如果出于某些原因需要控制这个 prosses,你可以通过禁用 respondToPingWithPong 来关闭自动 ping 响应。

socket.respondToPingWithPong=false//Do not automaticaly respond to incoming pings with pongs.
1
当客户端从连接获得一个pong响应时,调用 websocketDidReceivePong。 你需要实现WebSocketPongDelegate协议并设置一个额外的委托,例如: socket.pongDelegate = self

funcwebsocketDidReceivePong(socket: WebSocketClient, data: Data?) {
print("Got pong! Maybe some data: (data?.count)")
}

2.2 Starscream高阶使用
2.2.1 判断是否连接
if socket.isConnected {
// do cool stuff.
}

2.2.2 自定义头文件
你可以使用自己自定义的web socket标头覆盖默认的web socket标头,如下所示:
var request = URLRequest(url: URL(string: "ws://localhost:8080/")!)
request.timeoutInterval = 5
request.setValue("someother protocols", forHTTPHeaderField: "Sec-WebSocket-Protocol")
request.setValue("14", forHTTPHeaderField: "Sec-WebSocket-Version")
request.setValue("Everything is Awesome!", forHTTPHeaderField: "My-Awesome-Header")
let socket = WebSocket(request: request)

2.2.3 自定义HTTP方法
你的服务器在连接到 web socket时可能会使用不同的HTTP方法:
var request = URLRequest(url: URL(string: "ws://localhost:8080/")!)
request.httpMethod = "POST"
request.timeoutInterval = 5
let socket = WebSocket(request: request)

2.2.4 协议
如果需要指定协议,简单地将它的添加到 init:
//chat and superchat are the example protocols here
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])
socket.delegate = self
socket.connect()

2.2.5 自签名 SSL
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])

//set this if you want to ignore SSL cert validation, so a self signed SSL certificate can be used.
socket.disableSSLCertValidation = true

2.2.5.1 SSL引脚
Starscream还支持SSL固定。
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])
let data = ... //load your certificate from disk
socket.security = SSLSecurity(certs: [SSLCert(data: data)], usePublicKeys: true)
//socket.security = SSLSecurity() //uses the .cer files in your app's bundle

你可以加载证书的Data 小区,否则你可以使用 SecKeyRef,如果你想要使用 public 键。 usePublicKeys bool是使用证书进行验证还是使用 public 键。 如果选择 usePublicKeys,将自动从证书中提取 public 密钥。

2.2.5.2 SSL密码套件
要使用SSL加密连接,你需要告诉小红你的服务器支持的密码套件。
socket = WebSocket(url: URL(string: "wss://localhost:8080/")!, protocols: ["chat","superchat"])

// Set enabled cipher suites to AES 256 and AES 128
socket.enabledSSLCipherSuites = [TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256]

如果你不知道服务器支持哪些密码套件可以查看:SSL Labs

2.2.6 压缩扩展
Starscream支持压缩扩展( RFC 7692 )。 默认情况下,压缩是启用的,但是只有当服务器支持压缩时才会使用压缩。 你可以通过 .enableCompression 属性启用或者禁用压缩:
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!)
socket.enableCompression = false

如果应用程序正在传输已经压缩。随机或者其他uncompressable数据,则应禁用压缩。
2.2.7 自定义队列
调用委托方法时可以指定自定义队列。 默认使用 DispatchQueue.main,因此使所有委托方法调用都在主线程上运行。 重要的是要注意,所有 web socket处理都是在后台线程上完成的,只有修改队列时才更改委托方法。 实际的处理总是在后台线程上,不会暂停你的应用程序。
socket = WebSocket(url: URL(string: "ws://localhost:8080/")!, protocols: ["chat","superchat"])
//create a custom queue
socket.callbackQueue = DispatchQueue(label: "com.vluxe.starscream.myapp")

2.2.8 高级代理
socket.advancedDelegate = self

websocketDidReceiveMessage
func websocketDidReceiveMessage(socket: WebSocketClient, text: String, response: WebSocket.WSResponse) {
print("got some text: \(text)")
print("First frame for this message arrived on \(response.firstFrame)")
}

websocketDidReceiveData
func websocketDidReceiveData(socket: WebSocketClient, data: Date, response: WebSocket.WSResponse) {
print("got some data it long: \(data.count)")
print("A total of \(response.frameCount) frames were used to send this data")
}

websocketHttpUpgrade
当发送HTTP升级请求后,会返回下面回调

func websocketHttpUpgrade(socket: WebSocketClient, request: CFHTTPMessage) {
print("the http request was sent we can check the raw http if we need to")
}

func websocketHttpUpgrade(socket: WebSocketClient, response: CFHTTPMessage) {
print("the http response has returned.")
————————————————

原文链接:https://blog.csdn.net/kyl282889543/article/details/100655005

收起阅读 »

iOS 15-适配要点

iOS
增加UISheetPresentationController,通过它可以控制 Modal 出来的 UIViewController 的显示大小,且可以通过拖拽手势在不同大小之间进行切换。只需要在跳转的目标 UIViewController 做如下处理:if ...
继续阅读 »

  1. 增加UISheetPresentationController,通过它可以控制 Modal 出来的 UIViewController 的显示大小,且可以通过拖拽手势在不同大小之间进行切换。只需要在跳转的目标 UIViewController 做如下处理:

    if let presentationController = presentationController as? UISheetPresentationController {
    // 显示时支持的尺寸
    presentationController.detents = [.medium(), .large()]
    // 显示一个指示器表示可以拖拽调整大小
    presentationController.prefersGrabberVisible = true
    }
  2. UIButton支持更多配置。UIButton.Configuration是一个新的结构体,它指定按钮及其内容的外观和行为。它有许多与按钮外观和内容相关的属性,如cornerStyle、baseForegroundColor、baseBackgroundColor、buttonSize、title、image、subtitle、titlePadding、imagePadding、contentInsets、imagePlacement等。

    // Plain
    let plain = UIButton(configuration: .plain(), primaryAction: nil)
    plain.setTitle("Plain", for: .normal)
    // Gray
    let gray = UIButton(configuration: .gray(), primaryAction: nil)
    gray.setTitle("Gray", for: .normal)
    // Tinted
    let tinted = UIButton(configuration: .tinted(), primaryAction: nil)
    tinted.setTitle("Tinted", for: .normal)
    // Filled
    let filled = UIButton(configuration: .filled(), primaryAction: nil)
    filled.setTitle("Filled", for: .normal)

    Snipaste_2021-07-11_15-26-55.png16259886795922.png

  3. 推出CLLocationButton用于一次性定位授权,该内容内置于CoreLocationUI模块,但如果需要获取定位的详细信息仍然需要借助于CoreLocation

    let locationButton = CLLocationButton()
    // 文字
    locationButton.label = .currentLocation
    locationButton.fontSize = 20
    // 图标
    locationButton.icon = .arrowFilled
    // 圆角
    locationButton.cornerRadius = 10
    // tint
    locationButton.tintColor = UIColor.systemPink
    // 背景色
    locationButton.backgroundColor = UIColor.systemGreen
    // 点击事件,应该在在其中发起定位请求
    locationButton.addTarget(self, action: #selector(getCurrentLocation), for: .touchUpInside)
  4. URLSession 推出支持 async/await 的 API,包括获取数据、上传与下载。

    // 加载数据
    let (data, response) = try await URLSession.shared.data(from: url)
    // 下载
    let (localURL, _) = try await session.download(from: url)
    // 上传
    let (_, response) = try await session.upload(for: request, from: data)

  5. 系统图片支持多个层,支持多种渲染模式。

    // hierarchicalColor:多层渲染,透明度不同
    let config = UIImage.SymbolConfiguration(hierarchicalColor: .systemRed)
    let image = UIImage(systemName: "square.stack.3d.down.right.fill", withConfiguration: config)
    // paletteColors:多层渲染,设置不同风格
    let config2 = UIImage.SymbolConfiguration(paletteColors: [.systemRed, .systemGreen, .systemBlue])
    let image2 = UIImage(systemName: "person.3.sequence.fill", withConfiguration: config2)
  6. UINavigationBar、UIToolbar 和 UITabBar 设置颜色,需要使用 UIBarAppearance APIs。

    // UINavigationBar
    let navigationBarAppearance = UINavigationBarAppearance()
    navigationBarAppearance.backgroundColor = .red
    navigationController?.navigationBar.scrollEdgeAppearance = navigationBarAppearance
    navigationController?.navigationBar.standardAppearance = navigationBarAppearance
    // UIToolbar
    let toolBarAppearance = UIToolbarAppearance()
    toolBarAppearance.backgroundColor = .blue
    navigationController?.toolbar.scrollEdgeAppearance = toolBarAppearance
    navigationController?.toolbar.standardAppearance = toolBarAppearance
    // UITabBar
    let tabBarAppearance = UITabBarAppearance()
    toolBarAppearance.backgroundColor = .purple
    tabBarController?.tabBar.scrollEdgeAppearance = tabBarAppearance
    tabBarController?.tabBar.standardAppearance = tabBarAppearance
  7. UITableView 新增了属性 sectionHeaderTopPadding,会给每一个section 的 header 增加一个默认高度。

    tableView.sectionHeaderTopPadding = 0
  8. UIImage 新增了几个调整尺寸的方法。

    // preparingThumbnail
    UIImage(named: "sv.png")?.preparingThumbnail(of: CGSize(width: 200, height: 100))
    // prepareThumbnail,闭包中直接获取调整后的UIImage
    UIImage(named: "sv.png")?.prepareThumbnail(of: CGSize(width: 200, height: 100)) { image in
    // 需要回到主线程更新UI
    }
    // byPreparingThumbnail
    await UIImage(named: "sv.png")?.byPreparingThumbnail(ofSize: CGSize(width: 100, height: 100))

文中代码已在 Xcode 13 Beta3 中测试通过, 案例源代码下载地址

收起阅读 »

iOS Runtime (四)Runtime的消息机制

iOS
引言 iOS的消息转发机制,在我们开发中有时候忘记实现某个声明的方法,从而在运行过程中调用该方法出现崩溃, 当然这类问题是可以解决的,在当前对象或者父类对象中添加对象的方法实现,再重新运行,调用该方法就能解决这个问题,又或者在我们运行的时候动态的去添加接收者中...
继续阅读 »

引言


iOS的消息转发机制,在我们开发中有时候忘记实现某个声明的方法,从而在运行过程中调用该方法出现崩溃,


当然这类问题是可以解决的,在当前对象或者父类对象中添加对象的方法实现,再重新运行,调用该方法就能解决这个问题,又或者在我们运行的时候动态的去添加接收者中未知方法实现,这就是这篇重点要学习的内容。


错误异常实例


创建Game对象代码

#import <Foundation/Foundation.h>
@interface Game : NSObject
- (void)Play;
- (void)DoThings:(NSString *)Str Num:(NSInteger)num;
@end

#import "Game.h"
@implementation Game
- (void)Play{
NSLog(@"the game is play");
}
@end

调用该对象中没有实现的方法 DoThings: Num:


Game *game = [[Game alloc]init];
[game DoThings:@"wodenidetade" Num:10];

当我们运行的时候会报当调用的对象的方法不存在,即便是消息转发过后还是不存在的时候,就会抛出这个异常


解决方案


第一种方式

遇到这种情况通常第一个想法就是在该对象或继承树中的实现文件中添加该方法并实现,这种形式,就是你必须要去实现方法,需要开发者主动去写代码。


第二种方式

消息转发在运行时:


1.动态方法解析

+(BOOL)resolveInstanceMethod:(SEL)sel 实例方法解析
+(BOOL)resolveClassMethod:(SEL)sel 类方法解析

当运用消息转发运行时,根据调用的方法类型调用这两个方法其中一个,返回值BOOL类型,告诉系统该消息是否被处理,YES处理 NO 未处理



  • resolveInstanceMethod实例方法调用

  • resolveClassMethod类方法调用


这样的作用是: 当接受者接受到的消息方法并没有找到的情况下,系统会调用该函数,给予这个对象一次动态添加该消息方法实现的机会,如果该对象动态的添加了这个方法的实现,就返回YES,告诉系统这个消息我已经处理完毕。再次运行该方法。


注意:

当这个对象在实现了resolveInstanceMethod,resolveClassMethod两个方法,并没有对该对象消息进行处理,那么该方法会被调用两次:


一次是没有找到该方法需要对象解析处理;第二次是告诉系统我处理完成需要再次调用该方法但实际上并没有处理完成,所以会调用第二次该方法崩溃


2.后备接收者对象

-(id)forwardingTargetForSelector:(SEL)aSelector

在消息转发第一次方法解析中没有处理方法,并告诉系统本对象无法处理,需另寻办法,那么系统会给予另外一个办法,就是让别的对象B来处理该问题,如果对象B能够处理该消息,那么该消息转发结束。


将未知SEL作为参数传入,寻找另外对象处理,如果可以处理,返回该对象


3.以其他形式实现该消息方法

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
-(void)forwardInvocation:(NSInvocation *)anInvocation

当我们在前面两个步骤都没有处理该未知SEL时,就会到第三个步骤,上述两个方法是最后寻找IML的机会



  • 将未知SEL作为参数传入methodSignatureForSelector,在该方法中处理该消息,一旦能够处理,返回方法签名(自由的修改方法签名,apple签名),让后续forwardInvocation来进行处理



  • forwardInvocation中我们可以做很多的操作,这个方法比forwardingTargetForSelector更灵活



    • 也可以做到像forwardingTargetForSelector一样的结果,不同的是一个是让别的对象去处理,后者是直接切换调用目标,也就是该方法的Target

    • 我们也可以修改该方法的SEL,就是重新替换一个新的SEL

    • ....




4.直到最后未处理,抛出异常

-(void)doesNotRecognizeSelector:(SEL)aSelector

作为找不到函数实现的最后一步,NSObject实现这个函数只有一个功能,就是抛出异常。


虽然理论上可以重载这个函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。


流程图


代码实现

/*
* 第一步 实例方法专用 方法解析
**/
+ (BOOL)resolveInstanceMethod:(SEL)sel{

NSLog(@"%@",NSStringFromSelector(sel));

if(sel == @selector(DoThings:Num:)){
class_addMethod([self class], sel, (IMP)MyMethodIMP, "v@:");
return YES;
}

return [super resolveInstanceMethod:sel];
}

/*
* 第二步 如果第一步未处理,那么让别的对象去处理这个方法
**/
-(id)forwardingTargetForSelector:(SEL)aSelector{
if([NSStringFromSelector(aSelector) isEqualToString:@"DoThings:Num:"]){
return [[Tools alloc]init];
}
return [super forwardingTargetForSelector:aSelector];
}

/*
* 第三步 如果前两步未处理,这是最后处理的机会将目标函数以其他形式执行
**/
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSString *SelStr = NSStringFromSelector(aSelector);
if([SelStr isEqualToString:@"DoThings:Num:"]){
[NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
//改变消息接受者对象
[anInvocation invokeWithTarget:[[Tools alloc]init]];

//改变消息的SEL
anInvocation.selector = @selector(flyGame);
[anInvocation invokeWithTarget:self];
}

- (void)flyGame{
NSLog(@"我要飞翔追逐梦想!");
}

/*
* 作为找不到函数实现的最后一步,NSObject实现这个函数只有一个功能,就是抛出异常。
* 虽然理论上可以重载这个函数实现保证不抛出异常(不调用super实现),但是苹果文档着重提出“一定不能让这个函数就这么结束掉,必须抛出异常”。
*
***/
- (void)doesNotRecognizeSelector:(SEL)aSelector{

}


作者:响彻天堂
链接:https://www.jianshu.com/p/019bce1e6253

上一篇链接:https://www.imgeek.org/article/825358865

收起阅读 »

Java正则表达式语法大全

在我们日常开发项目中经常用到正则表达式/比如邮箱/电话手机号/域名/ip等)都会经常用到其实一个字符串就是一个简单的正则表达式,例如 Hello World 正则表达式匹配 "Hello World" 字符串。.(点号)也是一个正则表达式,...
继续阅读 »

在我们日常开发项目中经常用到正则表达式/比如邮箱/电话手机号/域名/ip等)都会经常用到

其实一个字符串就是一个简单的正则表达式,例如 Hello World 正则表达式匹配 "Hello World" 字符串。

.(点号)也是一个正则表达式,它匹配任何一个字符如:"a" 或 "1"。

下表列出了一些正则表达式的实例及描述:

正则表达式描述
this is text匹配字符串 "this is text"
this\s+is\s+text注意字符串中的 \s+ 。匹配单词 "this" 后面的 \s+  可以匹配多个空格,之后匹配 is 字符串,再之后 \s+  匹配多个空格然后再跟上 text 字符串。可以匹配这个实例:this is text
^\d+(.\d+)?^ 定义了以什么开始\d+ 匹配一个或多个数字? 设置括号内的选项是可选的. 匹配 "."可以匹配的实例:"5", "1.5" 和 "2.21"。

java 正则表达式和 Perl 的是最为相似的。

java.util.regex 包主要包括以下三个类:

  • Pattern 类:

    pattern 对象是一个正则表达式的编译表示。Pattern 类没有公共构造方法。要创建一个 Pattern 对象,你必须首先调用其公共静态编译方法,它返回一个 Pattern 对象。该方法接受一个正则表达式作为它的第一个参数。

  • Matcher 类:

    Matcher 对象是对输入字符串进行解释和匹配操作的引擎。与Pattern 类一样,Matcher 也没有公共构造方法。你需要调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象。

  • PatternSyntaxException:

    PatternSyntaxException 是一个非强制异常类,它表示一个正则表达式模式中的语法错误。 以下实例中使用了正则表达式  .runoob.  用于查找字符串中是否包了 runoob 子串:

import java.util.regex.*;
class RegexExample1{
public static void main(String[] args){
String content = "I am noob " + "from runoob.com.";
String pattern = ".*runoob.*";
boolean isMatch = Pattern.matches(pattern, content);
System.out.println("字符串中是否包含了 'runoob' 子字符串? " + isMatch);
}
}

最终打印字符串中是否包含了 'runoob' 子字符串? true

正则表达式语法大全

在其他语言中,\ 表示:我想要在正则表达式中插入一个普通的(字面上的)反斜杠,请不要给它任何特殊的意义。

在 Java 中,\ 表示:我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。 所以,在其他的语言中(如 Perl),一个反斜杠 \ 就足以具有转义的作用,而在 Java 中正则表达式中则需要有两个反斜杠才能被解析为其他语言中的转义作用。也可以简单的理解在 Java 的正则表达式中,两个 \ 代表其他语言中的一个 \,这也就是为什么表示一位数字的正则表达式是 \d,而表示一个普通的反斜杠是 \。

System.out.print("\");    // 输出为 \
System.out.print("\\"); // 输出为 \
字符说明
\将下一字符标记为特殊字符、文本、反向引用或八进制转义符。例如, n匹配字符 n。\n 匹配换行符。序列 \\ 匹配 \ ,\( 匹配 (。
^匹配输入字符串开始的位置。如果设置了 RegExp 对象的 Multiline 属性,^ 还会与"\n"或"\r"之后的位置匹配。
$匹配输入字符串结尾的位置。如果设置了 RegExp 对象的 Multiline 属性,$ 还会与"\n"或"\r"之前的位置匹配。
*零次或多次匹配前面的字符或子表达式。例如,zo* 匹配"z"和"zoo"。* 等效于 {0,}。
+一次或多次匹配前面的字符或子表达式。例如,"zo+"与"zo"和"zoo"匹配,但与"z"不匹配。+ 等效于 {1,}。
?零次或一次匹配前面的字符或子表达式。例如,"do(es)?"匹配"do"或"does"中的"do"。? 等效于 {0,1}。
{n}n 是非负整数。正好匹配 n 次。例如,"o{2}"与"Bob"中的"o"不匹配,但与"food"中的两个"o"匹配。
{n,}n 是非负整数。至少匹配 n 次。例如,"o{2,}"不匹配"Bob"中的"o",而匹配"foooood"中的所有 o。"o{1,}"等效于"o+"。"o{0,}"等效于"o*"。
{n,m}m 和 n 是非负整数,其中 n <= m。匹配至少 n 次,至多 m 次。例如,"o{1,3}"匹配"fooooood"中的头三个 o。'o{0,1}' 等效于 'o?'。注意:您不能将空格插入逗号和数字之间。
?当此字符紧随任何其他限定符(*、+、?、{n}、{n,}、{n,m})之后时,匹配模式是"非贪心的"。"非贪心的"模式匹配搜索到的、尽可能短的字符串,而默认的"贪心的"模式匹配搜索到的、尽可能长的字符串。例如,在字符串"oooo"中,"o+?"只匹配单个"o",而"o+"匹配所有"o"。
.匹配除"\r\n"之外的任何单个字符。若要匹配包括"\r\n"在内的任意字符,请使用诸如"[\s\S]"之类的模式。
(pattern)匹配 pattern 并捕获该匹配的子表达式。可以使用  0…9 属性从结果"匹配"集合中检索捕获的匹配。若要匹配括号字符 ( ),请使用"("或者")"。
(?:pattern)匹配 pattern 但不捕获该匹配的子表达式,即它是一个非捕获匹配,不存储供以后使用的匹配。这对于用"or"字符 (
(?=pattern)执行正向预测先行搜索的子表达式,该表达式匹配处于匹配 pattern 的字符串的起始点的字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,'Windows (?=95
(?!pattern)执行反向预测先行搜索的子表达式,该表达式匹配不处于匹配 pattern 的字符串的起始点的搜索字符串。它是一个非捕获匹配,即不能捕获供以后使用的匹配。例如,'Windows (?!95
xy
[xyz]字符集。匹配包含的任一字符。例如,"[abc]"匹配"plain"中的"a"。
[^xyz]反向字符集。匹配未包含的任何字符。例如,"[^abc]"匹配"plain"中"p","l","i","n"。
[a-z]字符范围。匹配指定范围内的任何字符。例如,"[a-z]"匹配"a"到"z"范围内的任何小写字母。
[^a-z]反向范围字符。匹配不在指定的范围内的任何字符。例如,"[^a-z]"匹配任何不在"a"到"z"范围内的任何字符。
\b匹配一个字边界,即字与空格间的位置。例如,"er\b"匹配"never"中的"er",但不匹配"verb"中的"er"。
\B非字边界匹配。"er\B"匹配"verb"中的"er",但不匹配"never"中的"er"。
\cx匹配 x 指示的控制字符。例如,\cM 匹配 Control-M 或回车符。x 的值必须在 A-Z 或 a-z 之间。如果不是这样,则假定 c 就是"c"字符本身。
\d数字字符匹配。等效于 [0-9]。
\D非数字字符匹配。等效于 [^0-9]。
\f换页符匹配。等效于 \x0c 和 \cL。
\n换行符匹配。等效于 \x0a 和 \cJ。
\r匹配一个回车符。等效于 \x0d 和 \cM。
\s匹配任何空白字符,包括空格、制表符、换页符等。与 [ \f\n\r\t\v] 等效。
\S匹配任何非空白字符。与 [^ \f\n\r\t\v] 等效。
\t制表符匹配。与 \x09 和 \cI 等效。
\v垂直制表符匹配。与 \x0b 和 \cK 等效。
\w匹配任何字类字符,包括下划线。与"[A-Za-z0-9_]"等效。
\W与任何非单词字符匹配。与"[^A-Za-z0-9_]"等效。
\xn匹配 n,此处的 n 是一个十六进制转义码。十六进制转义码必须正好是两位数长。例如,"\x41"匹配"A"。"\x041"与"\x04"&"1"等效。允许在正则表达式中使用 ASCII 代码。
*num*匹配 num,此处的 num 是一个正整数。到捕获匹配的反向引用。例如,"(.)\1"匹配两个连续的相同字符。
*n*标识一个八进制转义码或反向引用。如果 *n* 前面至少有 n 个捕获子表达式,那么 n 是反向引用。否则,如果 n 是八进制数 (0-7),那么 n 是八进制转义码。
*nm*标识一个八进制转义码或反向引用。如果 *nm* 前面至少有 nm 个捕获子表达式,那么 nm 是反向引用。如果 *nm* 前面至少有 n 个捕获,则 n 是反向引用,后面跟有字符 m。如果两种前面的情况都不存在,则 *nm* 匹配八进制值 nm,其中 n 和 m 是八进制数字 (0-7)。
\nml当 n 是八进制数 (0-3),m 和 l 是八进制数 (0-7) 时,匹配八进制转义码 nml。
\un匹配 n,其中 n 是以四位十六进制数表示的 Unicode 字符。例如,\u00A9 匹配版权符号 (©)。

注意:根据 Java Language Specification 的要求,Java 源代码的字符串中的反斜线被解释为 Unicode 转义或其他字符转义。因此必须在字符串字面值中使用两个反斜线,表示正则表达式受到保护,不被 Java 字节码编译器解释。例如,当解释为正则表达式时,字符串字面值 "\b" 与单个退格字符匹配,而 "\b" 与单词边界匹配。字符串字面值 "(hello)" 是非法的,将导致编译时错误;要与字符串 (hello) 匹配,必须使用字符串字面值 "\(hello\)"。


作者:java李杨勇
链接:https://juejin.cn/post/7020303920966664222
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

熬夜再战Android之修炼Kotlin-【Get和Set】、【继承】、【抽象类/嵌套类/内部类】篇

前提 当前环境 2021年10月8日最新下载2020.3.1 Patch 2 版本 👉实践过程 😜Get和Set 其实Kotlin声明实体类之后,里面的变量默认就带有set和get的属性功能了。除非想要特殊业务内容。 比如set需要结合项目进行其他业务处理,g...
继续阅读 »

前提


当前环境


2021年10月8日最新下载2020.3.1 Patch 2 版本


👉实践过程


😜Get和Set


其实Kotlin声明实体类之后,里面的变量默认就带有set和get的属性功能了。除非想要特殊业务内容。


比如set需要结合项目进行其他业务处理,get也是同样的道理。


【filed】是系统内置的一个关键字,算是中间变量


除了这些


var name: String? = null
        set(value) { //value随意起名
            field = value  //这个field是系统内置的 用在get
        }
        get() {
            return field + "这是返回"
        }
var urlJUEJIIN: String? = null
        get() =field+"这是只有get"
var urlCSDN: String? = null
var urlList: List<String>? = null

😜继承


在Java中可以说所有的类都继承自Object,而在Kotlin中可以说所有的是继承自Any类。


在Java中继承使用关键字【extends】,而在Kotlin中使用【:】(英文冒号)


除此之外,不管是方法重写还是属性变量重写,前面都加上【override】关键字,这一点和Java一样


class EntityTwo : Entity {
    constructor() {

    }

    constructor(name: String) : this(name, 0) {

    }

    //不同参数的次要构造函数
    constructor(name: String, age: Int) : super(name, age) {
        Log.e("TAG,", "执行了子类构造器$name===$age")
    }
}

😜接口


这点也和Java类似,使用【interface】定义,使用上也没差距


修饰类的关键字有



  • abstract    // 说明该类为抽象类 

  • final       // 说明该类为类不可继承,默认属性

  • enum        // 说明该类为枚举类

  • open        // 说明该类为类可继承,类默认是final的

  • annotation  // 说明该类为注解类


访问权限的修饰符有:



  • private    // 访问权限-仅在同一个文件中可见

  • protected  // 访问权限-同一个文件中或子类可见

  • public     // 访问权限-所有调用的地方都可见

  • internal   // 访问权限-同一个模块中可见


经过学习和试验验证,小空决定还是用Java的实体类吧,反正他们有互操作性。


😜抽象类/嵌套类/内部类


小空带大家直接用实例来看明白


abstract class EntityThree {
    abstract fun methonOne()
}

//嵌套类实例
class One {                  // 这是外部类
    private val age: Int = 1
    class Two {             // 这是在类里面的类,叫做嵌套类
        fun hello() {

        }

        fun hi() = 3
    }
}

//内部类使用关键字inner
class Three {
    inner class Four { //这个Four类是内部类
        fun hello() {

        }
        fun hi() = 3
    }
}
//这是引用示例
var one = Three().Four().hello()

作者:芝麻粒儿
链接:https://juejin.cn/post/7020944054665871391
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

使用BlackHook(黑钩) 可以Hook一切java或者kotlin方法

前言 之前做内存优化的时候,为了实现对线程的使用监控,借助了一个第三方的hook框架(epic),这个框架可以hook一切java方法,使用也简单,但是最大的问题是它有较严重的兼容性问题,部分机型会出现闪退的现象,这就导致它不能被带到线上使用,只能在线下使用,...
继续阅读 »

前言


之前做内存优化的时候,为了实现对线程的使用监控,借助了一个第三方的hook框架(epic),这个框架可以hook一切java方法,使用也简单,但是最大的问题是它有较严重的兼容性问题,部分机型会出现闪退的现象,这就导致它不能被带到线上使用,只能在线下使用,为了实现在线上监控线程的使用,于是我便开发了BlackHook插件,也可以hook一切java方法,而且很稳定,没有兼容性问题,真是十足的黑科技


简介


BlackHook 是一个实现编译时插桩的gradle插件,基于ASM+Tranfrom实现,理论上可以hook任意一个java方法或者kotlin方法,只要代码对应的字节码可以在编译阶段被Tranfrom扫描到,就可以使用ASM在代码对应的字节码处插入特定字节码,从而hook该方法


优点



  1. 用DSL(领域特定语言)使用该插件,使用简单,配置灵活,而且插入的字节码可以使用
    ASM Bytecode Viewer Support Kotlin 插件自动生成,上手难度低

  2. 理论上可以hook任意一个java方法,只要代码对应的字节码可以在编译阶段被Tranfrom扫描到

  3. 基于ASM+Tranfrom实现,在编译阶段直接修改字节码,效率高,没有兼容性问题


使用


在app下面的build.gradle文件添加如下代码


apply plugin: 'com.blackHook'

/**
* 返回hook线程构造函数的字节码,Hook 线程的构造函数,让每次在调用Thread的构造函数的时候就会调用
* ThreadCheck类的 printThread方法,从而在控制台打印线程的构造函数的调用堆栈,这些代码可以借助
* ASM Bytecode Viewer Support Kotlin生成,MethodVisitor是ASM提供的一个类,用于修改字节码
*/
void createHookThreadByteCode(MethodVisitor mv, String className) {
mv.visitTypeInsn(Opcodes.NEW, "com/quwan/tt/asmdemoapp/ThreadCheck")
mv.visitInsn(Opcodes.DUP)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "<init>", "()V", false)
mv.visitLdcInsn(className)
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "printThread", "(Ljava/lang/String;)V", false)
}

/**
* 返回需要被hook的方法,需要被hook的方法是Thread的构造函数
*/
List<HookMethod> getHookMethods() {
List<HookMethod> hookMethodList = new ArrayList<>()
hookMethodList.add(new HookMethod("java/lang/Thread", "<init>", "()V", { MethodVisitor mv -> createHookThreadByteCode(mv, "java/lang/Thread") }))
return hookMethodList
}

blackHook {
//表示要处理的数据类型是什么,CLASSES 表示要处理编译后的字节码(可能是 jar 包也可能是目录),RESOURCES 表示要处理的是标准的 java 资源
inputTypes BlackHook.CONTENT_CLASS
//表示Transform 的作用域,这里设置的SCOPE_FULL_PROJECT代表作用域是全工程
scopes BlackHook.SCOPE_FULL_PROJECT
//表示是否支持增量编译,false不支持
isIncremental false
//表示hook的方法
hookMethodList = getHookMethods()
}

以上的代码其实是hook的Thread的构造函数,将ThreadCheck的printThread方法hook到了Thread的构造函数中,每次调用线程的构造函数的时候就会调用ThreadCheck的printThread方法,这个方法会打印出Thread的构造函数的调用堆栈,从而可以在控制台知道哪个页面的哪行代码实例化了Thread,ThreadCheck的代码如下


class ThreadCheck {

var isCanAppendLog = false
private val tag = "====>ThreadCheck"

fun printThread(name : String){

println("====>printThread:${name}")

val es = Thread.currentThread().stackTrace

val normalInfo = StringBuilder(" \nThreadTrace:")
.append("\nthreadName:${name}")
.append("\n====================================threadTraceStart=======================================")

for (e in es) {

if (e.className == "dalvik.system.VMStack" && e.methodName == "getThreadStackTrace") {
isCanAppendLog = false
}

if (e.className.contains("ThreadCheck") && e.methodName == "printThread") {
isCanAppendLog = true
} else {
if (isCanAppendLog) {
normalInfo.append("\n${e.className}(lineNumber:${e.lineNumber})")
}
}
}
normalInfo.append("\n=====================================threadTraceEnd=======================================")

Log.i(tag, normalInfo.toString())
}

}

上面的代码获取了调用堆栈,并且打印到控制台


实现原理


首先它是一个gradle 的自定义Plugin,其次它是通过在编译阶段修改字节码实现Hook,在编译阶段通过Tranfrom扫描所有的字节码,然后根据在使用插件的时候设置的需要被Hook的方法,插入需要被插入的字节码,
需要被插入的字节码也是在使用的时候设置的,例如下面的代码


/**
* 返回hook线程构造函数的字节码,Hook 线程的构造函数,让每次在调用Thread的构造函数的时候就会调用
* ThreadCheck的 printThread方法,从而在控制台打印线程的构造函数的调用堆栈,这些代码可以借助
* ASM Bytecode Viewer Support Kotlin生成,MethodVisitor是ASM提供的一个类,用于修改字节码
*/
void createHookThreadByteCode(MethodVisitor mv, String className) {
mv.visitTypeInsn(Opcodes.NEW, "com/quwan/tt/asmdemoapp/ThreadCheck")
mv.visitInsn(Opcodes.DUP)
mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "<init>", "()V", false)
mv.visitLdcInsn(className)
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "com/quwan/tt/asmdemoapp/ThreadCheck", "printThread", "(Ljava/lang/String;)V", false)
}

准备过程


实现这个gradle插件需要我们有足够的预备知识,如下:



实现过程


1.自定义gradle plugin


因为这是一个gradle插件,所以需要我们自定义一个gradle的plugin


1. 新建一个模块


在工程中新建一个模块,命名为"buildSrc",注意,一定要命名为buildSrc,否则在工程中必须要将代码发布到本地或者远程maven仓库中才能正常使用,这样调试不方便,如下所示:


image.png


2. 然后配置gradle脚本,代码如下所示:


plugins {
id 'java-library'
id 'maven'
id 'groovy'
}

java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

dependencies {
implementation gradleApi()//gradle sdk
implementation localGroovy()
implementation "com.android.tools.build:gradle:3.4.1"
implementation 'org.ow2.asm:asm:9.1'
implementation 'org.ow2.asm:asm-commons:9.1'
}

3. 实现Plugin类


新建groovy文件夹,新建BlackHookPlugin类,继承Transform类,实现Plugin接口


image.png


BlackHookPlugin代码如下所示:


package com.blackHook.plugin

class BlackHookPlugin extends Transform implements Plugin<Project> {

....此处省略了很多代码

@Override
void apply(Project target) {
println("注册了")
project = target
target.extensions.getByType(BaseExtension).registerTransform(this)
target.extensions.create("blackHook", BlackHook.class)
}

....此处省略了很多代码
}

新建resources文件夹,新建com.blackHook.properties文件,如下所示


image.png


com.blackHook.properties文件的代码如下:


implementation-class=com.blackHook.plugin.BlackHookPlugin

implementation-class的值即是BlackHookPlugin的完整路径,另外,com.blackHook.properties文件的文件名既是使用插件的时候的插件名,如下代码:


apply plugin: 'com.blackHook'

2. 实现BlackHook扩展类


新建BlackHook类,代码如下


public class BlackHook {

Closure methodHooker;

List<HookMethod> hookMethodList = new ArrayList<>();

public static final String CONTENT_CLASS = "CONTENT_CLASS";
public static final String CONTENT_JARS = "CONTENT_JARS";
public static final String CONTENT_RESOURCES = "CONTENT_RESOURCES";

public static final String SCOPE_FULL_PROJECT = "SCOPE_FULL_PROJECT";
public static final String PROJECT_ONLY = "PROJECT_ONLY";

String inputTypes = CONTENT_CLASS;

String scopes = SCOPE_FULL_PROJECT;

boolean isNeedLog = false;

boolean isIncremental = false;

public Closure getMethodHooker() {
return methodHooker;
}

public void setMethodHooker(Closure methodHooker) {
this.methodHooker = methodHooker;
}

public List<HookMethod> getHookMethodList() {
return hookMethodList;
}

public void setHookMethodList(List<HookMethod> hookMethodList) {
this.hookMethodList = hookMethodList;
}

public String getInputTypes() {
return inputTypes;
}

public void setInputTypes(String inputTypes) {
this.inputTypes = inputTypes;
}

public String getScopes() {
return scopes;
}

public void setScopes(String scopes) {
this.scopes = scopes;
}

public boolean getIsIncremental() {
return isIncremental;
}

public void setIsIncremental(boolean incremental) {
isIncremental = incremental;
}

public boolean getIsNeedLog() {
return isNeedLog;
}

public void setIsNeedLog(boolean needLog) {
isNeedLog = needLog;
}
}

这个类用于接收开发人员使用插件的时候设置的参数和需要被Hook的方法以及参与Hook的字节码,我们在使用blackHook插件的时候可以使用DSL的方式来使用,如下代码所示:


blackHook {
//表示要处理的数据类型是什么,CLASSES 表示要处理编译后的字节码(可能是 jar 包也可能是目录), RESOURCES 表示要处理的是标准的 java 资源
inputTypes BlackHook.CONTENT_CLASS
//表示Transform 的作用域,这里设置的SCOPE_FULL_PROJECT代表作用域是全工程
scopes BlackHook.SCOPE_FULL_PROJECT
//表示是否支持增量编译,false不支持
isIncremental false
//表示hook的方法
hookMethodList = getHookMethods()
}

之所以可以这么做是因为我们在BlackHookPlugin将BlackHook类添加到了target.extensions(扩展属性)中,
如下代码:


class BlackHookPlugin extends Transform implements Plugin<Project> {
@Override
void apply(Project target) {
target.extensions.create("blackHook", BlackHook.class)
}
}

3.开始实现扫描


需要在BlackHookPlugin的transform()方法中扫描全局代码,代码如下:


  @Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
Collection<TransformInput> inputs = transformInvocation.inputs
TransformOutputProvider outputProvider = transformInvocation.outputProvider
if (outputProvider != null) {
outputProvider.deleteAll()
}
if (blackHook == null) {
blackHook = new BlackHook()
blackHook.methodHooker = project.extensions.blackHook.methodHooker
blackHook.isNeedLog = project.extensions.blackHook.isNeedLog
for (int i = 0; i < project.extensions.blackHook.hookMethodList.size(); i++) {
HookMethod hookMethod = new HookMethod()
hookMethod.className = project.extensions.blackHook.hookMethodList.get(i).className
hookMethod.methodName = project.extensions.blackHook.hookMethodList.get(i).methodName
hookMethod.descriptor = project.extensions.blackHook.hookMethodList.get(i).descriptor
hookMethod.createBytecode = project.extensions.blackHook.hookMethodList.get(i).createBytecode
blackHook.hookMethodList.add(hookMethod)
}
}
inputs.each { input ->
input.directoryInputs.each { directoryInput ->
handleDirectoryInput(directoryInput, outputProvider)
}
//遍历jarInputs
input.jarInputs.each { JarInput jarInput ->
//处理jarInputs
handleJarInputs(jarInput, outputProvider)
}
}
super.transform(transformInvocation)
}

void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
if (directoryInput.file.isDirectory()) {
directoryInput.file.eachFileRecurse { file ->
String name = file.name
if (name.endsWith(".class") && !name.startsWith("R$drawable")
&& !"R.class".equals(name) && !"BuildConfig.class".equals(name)) {
ClassReader classReader = new ClassReader(file.bytes)
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor classVisitor = new AllClassVisitor(classWriter, blackHook)
classReader.accept(classVisitor, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
FileOutputStream fos = new FileOutputStream(
file.parentFile.absolutePath + File.separator + name)
fos.write(code)
fos.close()
}
}
}

//处理完输入文件之后,要把输出给下一个任务
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file, dest)
}

void handleJarInputs(JarInput jarInput, TransformOutputProvider outputProvider) {
if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
//重名名输出文件,因为可能同名,会覆盖
def jarName = jarInput.name

def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4)
}
JarFile jarFile = new JarFile(jarInput.file)
Enumeration enumeration = jarFile.entries()
File tmpFile = new File(jarInput.file.getParent() + File.separator + "classes_temp.jar")
//避免上次的缓存被重复插入
if (tmpFile.exists()) {
tmpFile.delete()
}
JarOutputStream jarOutputStream = new JarOutputStream(new FileOutputStream(tmpFile))
//用于保存
while (enumeration.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) enumeration.nextElement()
String entryName = jarEntry.getName()
ZipEntry zipEntry = new ZipEntry(entryName)
InputStream inputStream = jarFile.getInputStream(jarEntry)
//插桩class
if (entryName.endsWith(".class") && !entryName.startsWith("R$")
&& !"R.class".equals(entryName) && !"BuildConfig.class".equals(entryName)) {
//class文件处理
jarOutputStream.putNextEntry(zipEntry)
ClassReader classReader = new ClassReader(IOUtils.toByteArray(inputStream))
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
ClassVisitor cv = new AllClassVisitor(classWriter, blackHook)
classReader.accept(cv, EXPAND_FRAMES)
byte[] code = classWriter.toByteArray()
jarOutputStream.write(code)
} else {
jarOutputStream.putNextEntry(zipEntry)
jarOutputStream.write(IOUtils.toByteArray(inputStream))
}
jarOutputStream.closeEntry()
}
//结束
jarOutputStream.close()
jarFile.close()
def dest = outputProvider.getContentLocation(jarName + md5Name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(tmpFile, dest)
tmpFile.delete()
}
}

扫描的过程中会将扫描到的所有类的信息(包含类名,父类名,方法名等)交给AllClassVisitor类,AllClassVisitor类代码如下所示:


public class AllClassVisitor extends ClassVisitor {
private String className;
private BlackHook blackHook;
private String superClassName;

public AllClassVisitor(ClassVisitor classVisitor, BlackHook blackHook) {
super(ASM6, classVisitor);
this.blackHook = blackHook;
}

@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
className = name;
superClassName = superName;
}

// 扫描到每个类中的方法的时候会回调到这个方法
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
// 新建AllMethodVisitor类,将扫描到类和方法的信息以及BlackHook类存储的参数交给 AllMethodVisitor对象,由AllMethodVisitor来判断是否需要Hook指定的方法
return new AllMethodVisitor(blackHook, mv, access, name, descriptor, className, superClassName);
}

然后在AllClassVisitor类中会将将扫描到的类和方法的信息以及BlackHook扩展类存储的参数交给AllMethodVisitor对象,由AllMethodVisitor来判断是否需要Hook指定的方法,AllMethodVisitor代码如下:


class AllMethodVisitor extends AdviceAdapter {
private final String methodName;
private final String className;
private BlackHook blackHook;
private String superClassName;

protected AllMethodVisitor(BlackHook blackHook, org.objectweb.asm.MethodVisitor methodVisitor, int access, String name, String descriptor, String className, String superClassName) {
super(ASM5, methodVisitor, access, name, descriptor);
this.blackHook = blackHook;
this.methodName = name;
this.className = className;
this.superClassName = superClassName;
}

@Override
protected void onMethodEnter() {
super.onMethodEnter();
}

@Override
public void visitMethodInsn(int opcode, String owner, String methodName, String descriptor, boolean isInterface) {
super.visitMethodInsn(opcode, owner, methodName, descriptor, isInterface);
if (blackHook.isNeedLog) {
System.out.println("====>methodInfo:" + "className:" + owner + ",methodName:" + methodName + ",descriptor:" + descriptor);
}
if (blackHook != null && blackHook.hookMethodList != null && blackHook.hookMethodList.size() > 0) {
for (int i = 0; i < blackHook.hookMethodList.size(); i++) {
HookMethod hookMethod = blackHook.hookMethodList.get(i);
//这里根据开发人员设置的需要hook的方法以及扫描到的方法来判断是否需要hook
if ((owner.equals(hookMethod.className) || superClassName.equals(hookMethod.className) || className.equals(hookMethod.className)) && methodName.equals(hookMethod.methodName) && descriptor.equals(hookMethod.descriptor)) {
hookMethod.createBytecode.call(mv);
break;
}
}
}
}
}

在这个类中根据开发人员调用插件的时候设置的需要hook的方法以及扫描到的方法来判断是否需要hook


4.源码


github.com/18824863285…


作者:陈平大将
链接:https://juejin.cn/post/7021033161488334856
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

扒一扒Android的.9图

前言相信大家对.9图都不陌生,我们在开发当中当有控件的背景需要对内容的大小做自适应的时候,可能就需要用到.9图。如下图所示,就是一张.9图。官方是这么定义的:NinePatchDrawable 图形是一种可拉伸的位图,可用作视图的背景。Android...
继续阅读 »

前言

相信大家对.9图都不陌生,我们在开发当中当有控件的背景需要对内容的大小做自适应的时候,可能就需要用到.9图。如下图所示,就是一张.9图。官方是这么定义的:

NinePatchDrawable 图形是一种可拉伸的位图,可用作视图的背景。Android 会自动调整图形的大小以适应视图的内容。NinePatch 图形是标准 PNG 图片,包含一个额外的 1 像素边框。必须使用 9.png 扩展名将其保存在项目的 res/drawable/ 目录下。

ninepatch_raw.png

那么有人可能会说,这有什么好讲的,从做Andorid开始,我就一直用到现在了。但是,往往越简单的东西,我们越容易忽略它。下面我们就带着这几个问题,一步步来看:

  1. Android是怎么识别一张.9图的?
  2. .9图片一定要放在res/drawable目录下吗,Android是怎么处理它的,为什么在手机上显示出来这个黑色边线却不见了?
  3. 一定要用.9图才能达到自适应的效果吗,普通图片行不行?

PNG

定义

从官方介绍可以得知,.9图是一张标准的PNG图片,只不过是加了一些额外的像素而已,那么首先我们得了解一下什么是PNG。

便携式网络图形(英语:Portable Network Graphics,PNG)是一种支持无损压缩的位图图形格式,支持索引、灰度、RGB三种颜色方案以及Alpha通道等特性。PNG的开发目标是改善并取代GIF作为适合网络传输的格式而不需专利许可,所以被广泛应用于互联网及其他方面上。

文件结构

文件跟协议一样,都是用数据来呈现的。那么既然协议有协议头来标识是什么协议,文件也一样。PNG的文件标识(file signature)是由8个字节组成(89 50 4E 47 0D 0A 1A 0A, 十六进制),系统就是根据这8个自己来识别出PNG文件。

在文件头之后,紧跟着的是数据块。PNG的数据块分为两类,一类是PNG文件必须包含、读写软件也必须要支持的关键块(critical chunk);另一种叫做辅助块(ancillary chunks),PNG允许软件忽略它不认识的附加块。这种基于数据块的设计,允许PNG格式在扩展时仍能保持与旧版本兼容。

数据块的格式:

名称字节数说明
Length4字节指定数据块中数据域的长度,其长度不超过(2^{31}-1)字节
Chunk Type Code(数据块类型码)4字节数据块类型码由ASCII字母(A-Z和a-z)组成
Chunk Data(数据块实际内容)实际内容长度存储按照Chunk Type Code指定的数据
CRC(循环冗余检测)4字节存储用来检测是否有错误的循环冗余码

关键块中有4个标准的数据块:

  • 文件头数据块IHDR(header chunk):包含有图像基本信息,作为第一个数据块出现并只出现一次。
  • 调色板数据块PLTE(palette chunk):必须放在图像数据块之前。
  • 图像数据块IDAT(image data chunk):存储实际图像数据。PNG数据允许包含多个连续的图像数据块。
  • 图像结束数据IEND(image trailer chunk):放在文件尾部,表示PNG数据流结束。

当然关于PNG的信息不止这些,有兴趣了解更多的话,可以去阅读RFC 2083,这里不做过多赘述。

所以不难猜出,.9图是在PNG的辅助块加了自己可以识别的数据块,然后显示的时候对图片做特殊的处理

Android是怎么加载一张.9图的

在Android中,一张图片对应的是一个Bitmap,我们可以看看从怎么从文件读取一张Bitmap入手

//BitmapFactory.java

public static Bitmap decodeFile(String pathName) {
   return decodeFile(pathName, null);
}

private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
           Rect padding, Options opts, long inBitmapHandle, long colorSpaceHandle);

我们根据一个文件路径读取一张图片的话,需要调用BitmapFactorydecodeFile方法,这里我省略了一些过程,但最终都会调用到nativeDecodeStream这个方法,它是一个native方法,接着看C++那边是怎么实现的

//BitmapFactory.cpp

static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
       jobject padding, jobject options, jlong inBitmapHandle, jlong colorSpaceHandle) {
...

   if (stream.get()) {
      ...
       bitmap = doDecode(env, std::move(bufferedStream), padding, options, inBitmapHandle,
                         colorSpaceHandle);
  }
   return bitmap;
}

static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,
                       jobject padding, jobject options, jlong inBitmapHandle,
                       jlong colorSpaceHandle) {
...
   NinePatchPeeker peeker;
   std::unique_ptr<SkAndroidCodec> codec;
  {
      ...
       std::unique_ptr<SkCodec> c = SkCodec::MakeFromStream(std::move(stream), &result, &peeker);
      ...
  }
 
...
jbyteArray ninePatchChunk = NULL;
   if (peeker.mPatch != NULL) {
       size_t ninePatchArraySize = peeker.mPatch->serializedSize();
       ninePatchChunk = env->NewByteArray(ninePatchArraySize);
       jbyte* array = (jbyte*) env->GetPrimitiveArrayCritical(ninePatchChunk, NULL);
       memcpy(array, peeker.mPatch, peeker.mPatchSize);
       env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0);
  }
 
// now create the java bitmap
   return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
           bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}

doDecode方法很长,这里只提取关键部分。我们看到了关键的NinePatchPeeker,然后把它的指针传给MakeFromStream这个方法。接着copy出NinePatchPeekermPatchBitmap作为构造参数。我们接着往下看:

// SkCodec.cpp
// 刚才NinePatchPeeker传给了这个方法的第三个参数,NinePatchPeeker实际上是实现了SkPngChunkReader
std::unique_ptr<SkCodec> SkCodec::MakeFromStream(
       std::unique_ptr<SkStream> stream, Result* outResult,
       SkPngChunkReader* chunkReader, SelectionPolicy selectionPolicy) {
 
  ...
#ifdef SK_HAS_PNG_LIBRARY
   if (SkPngCodec::IsPng(buffer, bytesRead)) {
       return SkPngCodec::MakeFromStream(std::move(stream), outResult, chunkReader);
  } else
#endif
  ...
}

// SkPngCodec.cpp
std::unique_ptr<SkCodec> SkPngCodec::MakeFromStream(std::unique_ptr<SkStream> stream,
                                                   Result* result, SkPngChunkReader* chunkReader) {
   SkCodec* outCodec = nullptr;
   *result = read_header(stream.get(), chunkReader, &outCodec, nullptr, nullptr);
   if (kSuccess == *result) {
       // Codec has taken ownership of the stream.
       SkASSERT(outCodec);
       stream.release();
  }
   return std::unique_ptr<SkCodec>(outCodec);
}

static SkCodec::Result read_header(SkStream* stream, SkPngChunkReader* chunkReader,
                                  SkCodec** outCodec,
                                  png_structp* png_ptrp, png_infop* info_ptrp) {
...
#ifdef PNG_READ_UNKNOWN_CHUNKS_SUPPORTED
   // Hookup our chunkReader so we can see any user-chunks the caller may be interested in.
   // This needs to be installed before we read the png header. Android may store ninepatch
   // chunks in the header.
   if (chunkReader) {
       png_set_keep_unknown_chunks(png_ptr, PNG_HANDLE_CHUNK_ALWAYS, (png_byte*)"", 0);
       png_set_read_user_chunk_fn(png_ptr, (png_voidp) chunkReader, sk_read_user_chunk);
  }
#endif
...
}

这里重点看下png_set_read_user_chunk_fn这个方法,传了chunkReadersk_read_user_chunk方法进去

#ifdef PNG_READ_USER_CHUNKS_SUPPORTED
void PNGAPI
png_set_read_user_chunk_fn(png_structrp png_ptr, png_voidp user_chunk_ptr,
   png_user_chunk_ptr read_user_chunk_fn) {
  ...
  png_ptr->read_user_chunk_fn = read_user_chunk_fn;
  png_ptr->user_chunk_ptr = user_chunk_ptr;
}
#endif

这个方法主要是对png_ptr的两个变量进行赋值,png_ptr是一个PNG结构体的指针。之后read_user_chunk_fn这个方法会在pngrutil.c中被调用

// pngrutil.c
void png_handle_unknown(png_structrp png_ptr, png_inforp info_ptr,
   png_uint_32 length, int keep) {
...
# ifdef PNG_READ_USER_CHUNKS_SUPPORTED
  if (png_ptr->read_user_chunk_fn != NULL) {
     if (png_cache_unknown_chunk(png_ptr, length) != 0) {
        /* Callback to user unknown chunk handler */
        int ret = (*(png_ptr->read_user_chunk_fn))(png_ptr,
            &png_ptr->unknown_chunk);
    }
  }
...
}

这里看方法名就知道是libpng这个库在读取未知的数据块,调用了read_user_chunk_fn方法读取用户自己定义的数据块。而read_user_chunk_fn就是上面的sk_read_user_chunk

// SkPngCodec.cpp
#ifdef PNG_READ_UNKNOWN_CHUNKS_SUPPORTED
static int sk_read_user_chunk(png_structp png_ptr, png_unknown_chunkp chunk) {
   SkPngChunkReader* chunkReader = (SkPngChunkReader*)png_get_user_chunk_ptr(png_ptr);
   // readChunk() returning true means continue decoding
   return chunkReader->readChunk((const char*)chunk->name, chunk->data, chunk->size) ? 1 : -1;
}
#endif

// pngget.c
#ifdef PNG_USER_CHUNKS_SUPPORTED
png_voidp PNGAPI
png_get_user_chunk_ptr(png_const_structrp png_ptr) {
  return (png_ptr ? png_ptr->user_chunk_ptr : NULL);
}
#endif

拿到一个SkPngChunkReader,而它的具体实现上面有说到,就是NinePatchPeeker

// NinePatchPeeker.cpp
bool NinePatchPeeker::readChunk(const char tag[], const void* data, size_t length) {
   if (!strcmp("npTc", tag) && length >= sizeof(Res_png_9patch)) {
       Res_png_9patch* patch = (Res_png_9patch*) data;
       size_t patchSize = patch->serializedSize();
       if (length != patchSize) {
           return false;
      }
       // You have to copy the data because it is owned by the png reader
       Res_png_9patch* patchNew = (Res_png_9patch*) malloc(patchSize);
       memcpy(patchNew, patch, patchSize);
       Res_png_9patch::deserialize(patchNew);
       patchNew->fileToDevice();
       free(mPatch);
       mPatch = patchNew;
       mPatchSize = patchSize;
  } else if (!strcmp("npLb", tag) && length == sizeof(int32_t) * 4) {
       mHasInsets = true;
       memcpy(&mOpticalInsets, data, sizeof(int32_t) * 4);
  } else if (!strcmp("npOl", tag) && length == 24) { // 4 int32_ts, 1 float, 1 int32_t sized byte
       mHasInsets = true;
       memcpy(&mOutlineInsets, data, sizeof(int32_t) * 4);
       mOutlineRadius = ((const float*)data)[4];
       mOutlineAlpha = ((const int32_t*)data)[5] & 0xff;
  }
   return true;
}

找了这么久,我们的目的地终于找到了。可以看到.9图对应的数据块有三个:npTcnpLbnpOl,负责图片图片拉伸的是npTc这个数据块。它在这里用一个Res_png_9patch的结构体封装,我们可以从这个结构体的注释就可以知道很多事情了,懒得看注释的话可以跳过,直接看我下面的解释:

/**
* This chunk specifies how to split an image into segments for
* scaling.
*
* There are J horizontal and K vertical segments. These segments divide
* the image into J*K regions as follows (where J=4 and K=3):
*
*     F0   S0   F1     S1
*   +-----+----+------+-------+
* S2| 0 | 1 | 2   |   3   |
*   +-----+----+------+-------+
*   |     |   |     |       |
*   |     |   |     |       |
* F2| 4 | 5 | 6   |   7   |
*   |     |   |     |       |
*   |     |   |     |       |
*   +-----+----+------+-------+
* S3| 8 | 9 | 10 |   11 |
*   +-----+----+------+-------+
*
* Each horizontal and vertical segment is considered to by either
* stretchable (marked by the Sx labels) or fixed (marked by the Fy
* labels), in the horizontal or vertical axis, respectively. In the
* above example, the first is horizontal segment (F0) is fixed, the
* next is stretchable and then they continue to alternate. Note that
* the segment list for each axis can begin or end with a stretchable
* or fixed segment.
*
* ...
*
* The colors array contains hints for each of the regions. They are
* ordered according left-to-right and top-to-bottom as indicated above.
* For each segment that is a solid color the array entry will contain
* that color value; otherwise it will contain NO_COLOR. Segments that
* are completely transparent will always have the value TRANSPARENT_COLOR.
*
* The PNG chunk type is "npTc".
*/
struct alignas(uintptr_t) Res_png_9patch
{
int8_t wasDeserialized;
   uint8_t numXDivs, numYDivs, numColors;

   uint32_t xDivsOffset, yDivsOffset, colorsOffset;

// .9图右边和下边黑线描述的方位
   int32_t paddingLeft, paddingRight, paddingTop, paddingBottom;

   enum {
       // The 9 patch segment is not a solid color.
       NO_COLOR = 0x00000001,

       // The 9 patch segment is completely transparent.
       TRANSPARENT_COLOR = 0x00000000
  };

...
     
   inline int32_t* getXDivs() const {
       return reinterpret_cast<int32_t*>(reinterpret_cast<uintptr_t>(this) + xDivsOffset);
  }
   inline int32_t* getYDivs() const {
       return reinterpret_cast<int32_t*>(reinterpret_cast<uintptr_t>(this) + yDivsOffset);
  }
   inline uint32_t* getColors() const {
       return reinterpret_cast<uint32_t*>(reinterpret_cast<uintptr_t>(this) + colorsOffset);
  }
}

注释告诉我们几个信息:

  • 一张图片被分为几个区块,支持拉伸的区块坐标分别存储在xDivs和yDivs两个数组。

  • S开头的表示可以拉伸(其实就是做.9图时,旁边1像素的黑线标记的范围),F表示不能拉伸。

    按照注释中的例子,图片被分为12块,例如S0,它表示编号为1、5、9在横轴方向 上是可以拉伸的,S1则表示标号3、7、11是支持拉伸的。所以xDivs和yDivs存储的数据长下面这样:

    xDivs = [S0.start, S0.end, S1.start, S1.end]

    yDivs = [S2.start, S2.end, S3.start, S3.end]

  • colors 描述了各个区块的颜色,按照从左到右从上到下表示。通常情况下,赋值为源码中定义的NO_COLOR = 0x00000001就行了

    colors = [c1, c2, c3, .... c11]

  • 横向(或者纵向)有多个拉伸块的时候,他们的拉伸长度是按照他们标识的范围比例来算的。加入S0是1像素,S1是3像素,则他们拉伸长度按照1:3去拉伸

数据结构

那么,从Res_png_9patch的序列化方法,我们可以推断出这个chunk的数据结构

void Res_png_9patch::serialize(const Res_png_9patch& patch, const int32_t* xDivs,
                              const int32_t* yDivs, const uint32_t* colors, void* outData) {
   uint8_t* data = (uint8_t*) outData;
   memcpy(data, &patch.wasDeserialized, 4);     // copy wasDeserialized, numXDivs, numYDivs, numColors
   memcpy(data + 12, &patch.paddingLeft, 16);   // copy paddingXXXX
   data += 32;

   memcpy(data, xDivs, patch.numXDivs * sizeof(int32_t));
   data +=  patch.numXDivs * sizeof(int32_t);
   memcpy(data, yDivs, patch.numYDivs * sizeof(int32_t));
   data +=  patch.numYDivs * sizeof(int32_t);
   memcpy(data, colors, patch.numColors * sizeof(uint32_t));
}
名称字节长度说明
wasDeserialized1这个值为-1的话表示这个区块不是.9图
numXDivs1xDivs 数组长度
numYDivs1yDivs 数组长度
numColors1colors 数组长度
--4无意义
--4无意义
paddingLeft4横向内容区域的左边
paddingRight4横向内容区域的右边
paddingTop4纵向内容区域的顶部
paddingBottom4纵向内容区域的底部
--无意义
xDivsnumXDivs * 4横向拉伸区域(图片上方黑线)
yDivsnumYDivs * 4纵向拉伸区域(图片左边黑线)
colorsnumColors * 4各个区块颜色

小结

那么,到这里Android把一个.9图加载成Bitmap给理清楚了。先通过读取PNG到header信息,发现有npTc数据块到时候,把它到chunk数据读取出来,用来做Bitmap的构造参数。接下来我们看看绘制

绘制

.9图是用NinePatchDrawable做绘制的,使用方式是这样的:

val bitmap = BitmapFactory.decodeFile(absolutePath)
// 检查bitmap的ninePatchChunk是不是属于.9图的格式,其实就是判断这个chunk的wasDeserialized(第一个字节)是不是等于-1,
val isNinePatch = NinePatch.isNinePatchChunk(bitmap.ninePatchChunk)
if (isNinePatch) {
// 用bitmap以及bitmap.ninePatchChunk构造NinePatchDrawable
val background = NinePatchDrawable(context.resources, bitmap, bitmap.ninePatchChunk, Rect(), null)
imageView.background = background
}

NinePatchDrawable的绘制方法里,又会调用到native方法,由于篇幅原因,这里简单的列下调用栈,大家感兴趣的话可以去看源码:

NinePatchDrawable.java -> draw()
NinePatch.java -> draw()
Canvas.java -> drawPatch()
BaseCanvas.java -> drawPatch()
-> nDrawNinePatch() // 这里是一个native方法,从这里开始就都是native逻辑了

SkiaCanvas.cpp -> drawNinePatch() // Canvas所有的native方法都对应的native层的SkiaCanvas。这里会根据xDivs和yDivs的数据把图片分为N个格子
SkCanvas.cpp -> drawImageLattice()
SkDevice.cpp -> onDrawImageLattice() // 这里循环绘制每个格子
-> drawImageRect()
SkBitmapDevice.cpp -> drawBitmapRect() // 这里给Paint设置了BitmapShader去绘制图片,模式用的是CLAMP(拉伸模式)

到这里,从加载到绘制的过程都已经讲完了,但是还漏了一块,那就是编译。

编译

大家有没有疑问,.9图header里面,npTc这个数据块哪里来?官方介绍为什么叫我们要保存到res/drawable/里面?

其实在编译的时候,aapt会对res/drawable/的图片进行编译,发现是.9图,就把图片四周的黑色像素提取出来,整理成npTc数据块,放到PNG的header里面。

我们可以用Vim打开一张未编译的.9图看看

1.png

这里我们可以看到一些基本的数据块,例如IHDR以及IEND。接着我们用aapt编译一下这张.9图,具体命令如下:

./aapt s -i xxx_in.png -o xxx_out.png

2.png 用Vim打开之后,可以看到多了很多信息,也可以看到.9图对应的npTc数据块,打开图片也可以发现那些四周的黑线不见了。

最后

回答一下文章开头的几个问题:

.9图片一定要放在res/drawable目录下吗

这个不一定,如果你需要从assets目录、sdcard、或者网络读取.9图,也可以实现。只不过需要手动用aapt对图片做处理

一定要用.9图才能达到自适应的效果吗,普通图片行不行

通过了解.9图的原理之后,答案是肯定行的。我们可以自己手动构造ninePatchChunk, 然后传给NinePatchDrawable就可以了,这里就不写代码演示了。


收起阅读 »

在Android中使用Netty进行通讯,附带服务端代码

NettyNetty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。 Netty 是一个广泛使用的 Java 网络编程框架(Netty 在 2011 年获得了Duke's Choice Award...
继续阅读 »

Netty

Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。 Netty 是一个广泛使用的 Java 网络编程框架(Netty 在 2011 年获得了Duke's Choice Award,见http://www.java.net/dukeschoice… Facebook 和 Instagram 以及流行 开源项目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其强大的对于网络抽象的核心代码。

依赖引入

由于使用最新版本的话,发现有个类找不到,后面查了下是因为jdk版本,在android中的话,太高的jdk版本肯定不支持,所以我找了19年的发行版,测试ok。

implementation 'io.netty:netty-all:4.1.42.Final'

服务端代码实现

NettyServer

@Slf4j
public class NettyServer {

public void start(InetSocketAddress socketAddress) {
//new 一个主线程组
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
//new 一个工作线程组
EventLoopGroup workGroup = new NioEventLoopGroup(200);
ServerBootstrap bootstrap = new ServerBootstrap()
.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ServerChannelInitializer())
.localAddress(socketAddress)
//设置队列大小
.option(ChannelOption.SO_BACKLOG, 1024)
// 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
.childOption(ChannelOption.SO_KEEPALIVE, true);
//绑定端口,开始接收进来的连接
try {
ChannelFuture future = bootstrap.bind(socketAddress).sync();
log.info("服务器启动开始监听端口: {}", socketAddress.getPort());
// future.channel().writeAndFlush("你好啊");
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//关闭主线程组
bossGroup.shutdownGracefully();
//关闭工作线程组
workGroup.shutdownGracefully();
}
}
}

ServerChannelInitializer

public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//添加编解码
socketChannel.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
socketChannel.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
socketChannel.pipeline().addLast(new NettyServerHandler());
}
}

NettyServerHandler

@Slf4j
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 客户端连接会触发
*/

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("Channel active......");
}

@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
log.info("Channel Inactive......");
}

/**
* 客户端发消息会触发
*/

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//----------- 只改了这里 -----------
log.info("服务器收到消息1111: {}", msg.toString());

ctx.write("{\"data\":{\"taskData\":{\"collectionRule\":{\"id\":1,\"name\":\"IP主播直播室互动用户\",\"rule\":\"[{\\\"label\\\":\\\"抖音号\\\",\\\"key\\\":\\\"dyId\\\",\\\"type\\\":\\\"string\\\"},{\\\"key\\\":\\\"count\\\",\\\"label\\\":\\\"数量\\\",\\\"type\\\":\\\"string\\\"}]\",\"ruleType\":\"collect\",\"source\":\"collectLiveAudience\"},\"description\":\"粉丝列表-付鹏的财经世界3\",\"ruleId\":\"1\",\"ruleParam\":\"{\\\"dyId\\\":\\\"ghsys\\\",\\\"count\\\":\\\"140000\\\"}\"},\"taskId\":\"64\",\"taskType\":\"collection\"},\"devicesId\":\"5011bbdcd5006a93\",\"type\":\"task\"}");
ctx.flush();
}

/**
* 发生异常触发
*/

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}

启动服务

@SpringBootApplication
public class ServerApplication {

public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
//启动服务端
NettyServer nettyServer = new NettyServer();
nettyServer.start(new InetSocketAddress("192.18.52.95", 8091));
}
}

客户端代码实现

其实netty的使用客户端和服务器端整体上是差不多的,所以这里只列出来核心代码。

初始化操作

abstract class McnNettyTask : Runnable {

private var socketChannel: SocketChannel? = null
private var isConnected = false

override fun run() {
createConnection()
}

private fun createConnection() {
val nioEventLoopGroup = NioEventLoopGroup()
val bootstrap = Bootstrap()
bootstrap
.group(nioEventLoopGroup)
.option(ChannelOption.TCP_NODELAY, true) //无阻塞
.channel(NioSocketChannel::class.java)
.option(ChannelOption.SO_KEEPALIVE, true) //长连接
.option(ChannelOption.SO_TIMEOUT, 30_000) //收发超时
.handler(McnClientInitializer(object : McnClientListener {
override fun disConnected() {
isConnected = false
}

override fun connected() {
isConnected = true
}
}, object : McnEventListener {
override fun onReceiverMessage(messageRequest: MessageRequest) {
dispatchMessage(messageRequest)
}
}))
try {
val channelFuture = bootstrap.connect(McnNettyConfig.ip, McnNettyConfig.port)
.addListener(object : ChannelFutureListener {
override fun operationComplete(future: ChannelFuture) {
if (future.isSuccess) {
socketChannel = future.channel() as SocketChannel;
isConnected = true
CommonConsole.log("netty connect success (ip: ${McnNettyConfig.ip}, port: ${McnNettyConfig.port})")

sendMsg(MessageRequest.createDevicesState(0))
} else {
CommonConsole.log("netty connect failure (ip: ${McnNettyConfig.ip}, port: ${McnNettyConfig.port})")
isConnected = false
future.channel().close()
nioEventLoopGroup.shutdownGracefully()
}
}
}).sync()//阻塞,直到连接完成
channelFuture.channel().closeFuture().sync()
} catch (ex: Exception) {
ex.printStackTrace()
} finally {
//释放所有资源和创建的线程
nioEventLoopGroup.shutdownGracefully()
}
}

fun isConnected(): Boolean {
return isConnected
}

fun disConnected() {
socketChannel?.close()
}

abstract fun dispatchMessage(messageRequest: MessageRequest)

fun sendMsg(msg: String, nettyMessageListener: McnMessageListener? = null) {
if (!isConnected()) {
nettyMessageListener?.sendFailure()
return
}
socketChannel?.run {
writeAndFlush(msg + "###").addListener { future ->
if (future.isSuccess) {
//消息发送成功
CommonConsole.log("netty send message success (message: $msg")
nettyMessageListener?.sendSuccess()
} else {
//消息发送失败
CommonConsole.log("netty send message failure (message: $msg")
nettyMessageListener?.sendFailure()
}
}
}
}
}

加载handler和Initializer

class McnClientInitializer(
private val nettyClientListener: McnClientListener,
private val nettyEventListener: McnEventListener
) :
ChannelInitializer<SocketChannel>() {

override fun initChannel(socketChannel: SocketChannel) {
val pipeline = socketChannel.pipeline()
// pipeline.addLast("decoder", McnStringDecoder())
// pipeline.addLast("encoder", McnStringEncoder())
// pipeline.addLast(LineBasedFrameDecoder(1024))
pipeline.addLast("decoder", StringDecoder())
// pipeline.addLast("encoder", StringEncoder())
pipeline.addLast(DelimiterBasedFrameEncoder("###"))
pipeline.addLast(McnClientHandler(nettyClientListener, nettyEventListener))
}
}

核心数据接收处理handler

class McnClientHandler(
private val nettyClientListener: McnClientListener,
private val nettyEventListener: McnEventListener
) :
SimpleChannelInboundHandler<String>() {

override fun channelActive(ctx: ChannelHandlerContext?) {
super.channelActive(ctx)
CommonConsole.log("Netty channelActive.........")
nettyClientListener.connected()
}

override fun channelInactive(ctx: ChannelHandlerContext?) {
super.channelInactive(ctx)
nettyClientListener.disConnected()
}

override fun channelReadComplete(ctx: ChannelHandlerContext?) {
super.channelReadComplete(ctx)
CommonConsole.log("Netty channelReadComplete.........")
}

override fun exceptionCaught(ctx: ChannelHandlerContext?, cause: Throwable?) {
super.exceptionCaught(ctx, cause)
CommonConsole.log("Netty exceptionCaught.........${cause?.message}")
cause?.printStackTrace()
ctx?.close()
}

override fun channelRead0(ctx: ChannelHandlerContext?, msg: String?) {
CommonConsole.log("Netty channelRead.........${msg}")
msg?.run {
try {
val messageRequest =
Gson().fromJson<MessageRequest>(msg, MessageRequest::class.java)
nettyEventListener.onReceiverMessage(messageRequest)
} catch (ex: Exception) {
ex.printStackTrace()
}
ReferenceCountUtil.release(msg)
}
}
}

处理数据粘包 & 数据分包

如果使用netty,你肯定会碰到数据粘包和数据分包的问题的。所谓数据粘包就是当数据量比较小的情况下,相近时间内的多个发送数据会被作为一个数据包接收解析。而数据分包就是会将一个比较大的数据包分成为很多个小的数据包。不管是数据的分包还是粘包,都会导致我们使用的时候不能简单使用数据,所以我们要对粘包和分包数据做处理,让每次发送的数据都是独立且完整的。

对于数据编码,使用自定义的解析器处理,相当于是对数据使用特定字符串做拼接和反取操作。

服务端:

ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,
Unpooled.wrappedBuffer(delimiter.getBytes())));
// 将分隔之后的字节数据转换为字符串数据
ch.pipeline().addLast(new StringDecoder());
// 这是我们自定义的一个编码器,主要作用是在返回的响应数据最后添加分隔符
ch.pipeline().addLast(new DelimiterBasedFrameEncoder("###"));
// 最终处理数据并且返回响应的handler
ch.pipeline().addLast(new EchoServerHandler());

客户端:

/ 对服务端返回的消息通过_$进行分隔,并且每次查找的最大大小为1024字节
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,
Unpooled.wrappedBuffer(delimiter.getBytes())));
// 将分隔之后的字节数据转换为字符串
ch.pipeline().addLast(new StringDecoder());
// 对客户端发送的数据进行编码,这里主要是在客户端发送的数据最后添加分隔符
ch.pipeline().addLast(new DelimiterBasedFrameEncoder("###"));
// 客户端发送数据给服务端,并且处理从服务端响应的数据
ch.pipeline().addLast(new EchoClientHandler());

DelimiterBasedFrameEncoder

public class DelimiterBasedFrameEncoder extends MessageToByteEncoder<String> {

private String delimiter;

public DelimiterBasedFrameEncoder(String delimiter) {
this.delimiter = delimiter;
}

@Override
protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out)
throws Exception {
// 在响应的数据后面添加分隔符
ctx.writeAndFlush(Unpooled.wrappedBuffer((msg + delimiter).getBytes()));
}
}

如果是

对客户端 & 服务端通讯加入[暗号]

在客户端和服务端数据通讯的时候,为了确保数据的完整性和安全性,通常会加入一段暗号,作为安全校验。其实两端真实的通信结构就变成了如下:

完整数据 = 暗号字节 + 真实数据内容

客户端和服务端在获取到数据之后,按照约定将暗号数据移除之后,剩下的就是正式的数据。

对于暗号的处理,可以通过MessageToMessageEncoder来实现,通过对获取到的通讯字节码编解码来对数据处理。

McnStringEncoder

/**
* copy自 StringEncoder源码,进行修改,增加了业务处理暗号
*/

class McnStringEncoder : MessageToMessageEncoder<CharSequence> {

var charset: Charset? = null

constructor(charset: Charset?) {
if (charset == null) {
throw NullPointerException("charset")
} else {
this.charset = charset
}
}

constructor() : this(Charset.defaultCharset())

override fun encode(ctx: ChannelHandlerContext?, msg: CharSequence?, out: MutableList<Any>?) {
if (msg?.isNotEmpty() == true) {
out?.add(
ByteBufUtil.encodeString(
ctx!!.alloc(),
CharBuffer.wrap(McnNettyConfig.private_key + msg),
charset
)
)
}
}
}

McnStringDecoder

/**
* copy自 StringDecoder源码,进行修改,增加了业务处理暗号
*/

class McnStringDecoder : MessageToMessageDecoder<ByteBuf> {
var charset: Charset? = null

constructor(charset: Charset?) {
if (charset == null) {
throw NullPointerException("charset")
} else {
this.charset = charset
}
}

constructor() : this(Charset.defaultCharset())

override fun decode(ctx: ChannelHandlerContext?, msg: ByteBuf?, out: MutableList<Any>?) {
msg?.run {
Log.e("info", "decoder结果====>${msg.toString(charset)}")
//校验报文长度是否合法
if (msg.readableBytes() <= McnNettyConfig.keyLength) {
out?.add(ErrorData.creator(ErrorData.LENGTH_ERROR, "报文长度校验失败"))
return
}
val privateKey = this.readBytes(McnNettyConfig.keyLength)
//校验报文暗号是否匹配
if (privateKey.toString(charset) != McnNettyConfig.private_key) {
out?.add(ErrorData.creator(ErrorData.PRIVATE_KEY_ERROR, "报文暗号校验失败"))
return
}
//获取真实报文内容
out?.add(this.toString(charset))
}
}

data class ErrorData(
var errorCode: Int,
var errorMsg: String
) {

companion object {

//长度异常
const val LENGTH_ERROR = -10001

//报文校验失败
const val PRIVATE_KEY_ERROR = -10002

@JvmStatic
fun creator(errorCode: Int, message: String): String {
val errorData = ErrorData(errorCode, message)
return JSON.toJSONString(errorData)
}
}
}
}

最后的使用就很简单了。只需要将我们的处理加到处理链就行了。

val pipeline = socketChannel.pipeline()
pipeline.addLast("decoder", McnStringDecoder())
pipeline.addLast("encoder", McnStringEncoder())

收起阅读 »

再谈协程之第三者Flow基础档案

该来的还是来了,LiveData提供了响应式编程的基础,搭建了一套数据观察者的使用框架,但是,它相当于RxJava这类的异步框架来说,有点略显单薄了,这也是经常被人诟病的问题,因此,Flow这个小三就顺应而生了。Flow作为一套异步数据流框架,几乎可以约等于R...
继续阅读 »

该来的还是来了,LiveData提供了响应式编程的基础,搭建了一套数据观察者的使用框架,但是,它相当于RxJava这类的异步框架来说,有点略显单薄了,这也是经常被人诟病的问题,因此,Flow这个小三就顺应而生了。

Flow作为一套异步数据流框架,几乎可以约等于RxJava,但借助Kotlin语法糖和协程,以及Kotlin的DSL语法,可以让Flow的写法变得异常简洁,让你直面人性最善良的地方,一切的黑暗和丑陋,都被编译器消化了。而且,Flow作为LiveData的进化版本,可以很好的和JetPack结合起来,作为全家桶的一员,为统一架构添砖加瓦。

要理解FLow,首先需要了解Flow的各种操作符和基础功能,如果不理解这些,那么很难将Flow灵活运用,所以,本节主要来梳理Flow的基础。

Flow前言

首先,我们来看一个新的概念——冷流和热流,如果你看网上的Flow相关的文章,十有八九都会提到这个很冷门的名词。

Flow是早上冷的,到Channel才热起来。

一个异步数据流,通常包含三部分:

  • 上游
  • 操作符
  • 下游

所谓冷流,即下游无消费行为时,上游不会产生数据,只有下游开始消费,上游才从开始产生数据。

而所谓热流,即无论下游是否有消费行为,上游都会自己产生数据。

Flow操作符

Flow和RxJava一样,用各种操作符撑起了异步数据流框架的半边天。Flow默认为冷流,即下游有消费时,才执行生产操作。

所以,操作符也被分为两类——中间操作符和末端操作符,中间操作符不会产生消费行为,返回依然为Flow,而末端操作符,会产生消费行为,即触发流的生产。

Flow的创建

仅仅创建Flow,是不会执行Flow中的任何代码的,但我们首先,还是要看下如何创建Flow。

  • flow

通过flow{}构造器,可以快速创建Flow,在flow中,可以使用emit来生产数据(或者emitAll生产批量数据),示例如下。

flow {
for (i in 0..3) {
emit(i.toString())
}
}
  • flowOf

与listOf类似,Flow可以通过flowOf来产生有限的已知数据。

flowOf(1, 2, 3)
  • asFlow

asFlow用于将List转换为Flow。

listOf(1,2,3).asFlow()
  • emptyFlow

如题,创建一个空流。

末端操作符

末端操作符在调用之后,创建Flow的代码才会执行,这点和Sequence非常类似。

  • collect

collect是最常用的末端操作符,示例如下。

末端操作符都是suspend函数,所以需要运行在协程作用域中。

MainScope().launch {
val time = measureTimeMillis {
flow {
for (i in 0..3) {
Log.d("xys", "emit value---$i")
emit(i.toString())
}
}.collect {
Log.d("xys", "Result---$it")
}
}
Log.d("xys", "Time---$time")
}
  • collectIndexed

带下标的collect,下标是Flow中的emit顺序。

MainScope().launch {
val time = measureTimeMillis {
flow {
for (i in 0..3) {
Log.d("xys", "emit value---$i")
emit(i.toString())
}
}.collectIndexed { index, value ->
Log.d("xys", "Result in $index --- $value")
}
}
Log.d("xys", "Time---$time")
}
  • collectLatest

collectLatest用于在collect中取消未来得及处理的数据,只保留当前最新的生产数据。

flowOf(1, 2, 3).collectLatest {
delay(1)
Log.d("xys", "Result---$it")
}
  • toCollection、toSet、toList

这些操作符用于将Flow转换为Collection、Set和List。

  • launchIn

在指定的协程作用域中直接执行Flow。

flow {
for (i in 0..3) {
Log.d("xys", "emit value---$i")
emit(i.toString())
}
}.launchIn(MainScope())
  • last、lastOrNull、first、firstOrNull

返回Flow的最后一个值(第一个值),区别是last为空的话,last会抛出异常,而lastOrNull可空。

flow {
for (i in 0..3) {
emit(i.toString())
}
}.last()

状态操作符

状态操作符不做任何修改,只是在合适的节点返回状态。

  • onStart:在上游生产数据前调用
  • onCompletion:在流完成或者取消时调用
  • onEach:在上游每次emit前调用
  • onEmpty:流中未产生任何数据时调用
  • catch:对上游中的异常进行捕获
  • retry、retryWhen:在发生异常时进行重试,retryWhen中可以拿到异常和当前重试的次数
MainScope().launch {
Log.d("xys", "Coroutine in ${Thread.currentThread().name}")
val time = measureTimeMillis {
flow {
for (i in 0..3) {
emit(i.toString())
}
throw Exception("Test")
}.retryWhen { _, retryCount ->
retryCount <= 3
}.onStart {
Log.d("xys", "Start Flow in ${Thread.currentThread().name}")
}.onEach {
Log.d("xys", "emit value---$it")
}.onCompletion {
Log.d("xys", "Flow Complete")
}.catch { error ->
Log.d("xys", "Flow Error $error")
}.collect {
Log.d("xys", "Result---$it")
}
}
Log.d("xys", "Time---$time")
}

另外,onCompletion也可以监听异常,代码如下所示。

.onCompletion { exception ->
Log.d("xys", "Result---$exception")
}

Transform操作符

与RxJava一样,在数据流中,我们可以利用操作符对数据进行各种变换,以满足操作流的不同需求。

  • map、mapLatest、mapNotNull

map操作符将Flow的输入通过block转换为新的输出。

flow {
for (i in 0..3) {
emit(i)
}
}.map {
it * it
}
  • transform、transformLatest

transform操作符与map操作符有点一样,但又不完全一样,map是一对一的变换,而transform则可以完全控制流的数据,进行过滤、 重组等等操作都可以。

flow {
for (i in 0..3) {
emit(i)
}
}.transform { value ->
if (value == 1) {
emit("!!!$value!!!")
}
}.collect {
Log.d("xys", "Result---$it")
}
  • transformWhile

transformWhile的返回值是一个bool类型,用来控制流的截断,如果返回true,则流继续执行,如果false,则流截断。

flow {
for (i in 0..3) {
emit(i)
}
}.transformWhile { value ->
emit(value)
value == 1
}.collect {
Log.d("xys", "Result---$it")
}

过滤操作符

如题,过滤操作符用于过滤流中的数据。

  • filter、filterInstance、filterNot、filterNotNull

过滤操作符可以按条件、类型或者对过滤取反、取非空等条件进行操作。

flow {
for (i in 0..3) {
emit(i)
}
}.filter { value ->
value == 1
}.collect {
Log.d("xys", "Result---$it")
}
  • drop、dropWhile、take、takeWhile

这类操作符可以丢弃前n个数据,或者是只拿前n个数据。带while后缀的,则表示按条件进行判断。

  • debounce

debounce操作符用于防抖,指定时间内的值只接收最新的一个。

  • sample

sample操作符与debounce操作符有点像,但是却限制了一个周期性时间,sample操作符获取的是一个周期内的最新的数据,可以理解为debounce操作符增加了周期的限制。

  • distinctUntilChangedBy

去重操作符,可以按照指定类型的参数进行去重。

组合操作符

组合操作符用于将多个Flow的数据进行组合。

  • combine、combineTransform

combine操作符可以连接两个不同的Flow。

val flow1 = flowOf(1, 2).onEach { delay(10) }
val flow2 = flowOf("a", "b", "c").onEach { delay(20) }
flow1.combine(flow2) { i, s -> i.toString() + s }.collect {
Log.d("xys", "Flow combine: $it")
}

输出为:

D/xys: Flow combine: 1a
D/xys: Flow combine: 2a
D/xys: Flow combine: 2b
D/xys: Flow combine: 2c

可以发现,当两个Flow数量不同时,始终由Flow1开始,用其最新的元素,与Flow2的最新的元素进行组合,形成新的元素。

  • merge

merge操作符用于将多个流合并。

val flow1 = flowOf(1, 2).onEach { delay(10) }
val flow2 = flowOf("a", "b", "c").onEach { delay(20) }
listOf(flow1, flow2).merge().collect {
Log.d("xys", "Flow merge: $it")
}

输出为:

D/xys: Flow merge: 1
D/xys: Flow merge: 2
D/xys: Flow merge: a
D/xys: Flow merge: b
D/xys: Flow merge: c

merge的输出结果是按照时间顺序,将多个流依次发射出来。

  • zip

zip操作符会分别从两个流中取值,当一个流中的数据取完,zip过程就完成了。

val flow1 = flowOf(1, 2).onEach { delay(10) }
val flow2 = flowOf("a", "b", "c").onEach { delay(20) }
flow1.zip(flow2) { i, s -> i.toString() + s }.collect {
Log.d("xys", "Flow zip: $it")
}

输出为:

D/xys: Flow zip: 1a
D/xys: Flow zip: 2b

线程切换

在Flow中,可以简单的使用flowOn来指定线程的切换,flowOn会对上游,以及flowOn之前的所有操作符生效。

flow {
for (i in 0..3) {
Log.d("xys", "Emit Flow in ${Thread.currentThread().name}")
emit(i)
}
}.map {
Log.d("xys", "Map Flow in ${Thread.currentThread().name}")
it * it
}.flowOn(Dispatchers.IO).collect {
Log.d("xys", "Collect Flow in ${Thread.currentThread().name}")
Log.d("xys", "Result---$it")
}

这种情况下,flow和map的操作都将在子线程中执行。

而如果是这样:

flow {
for (i in 0..3) {
Log.d("xys", "Emit Flow in ${Thread.currentThread().name}")
emit(i)
}
}.flowOn(Dispatchers.IO).map {
Log.d("xys", "Map Flow in ${Thread.currentThread().name}")
it * it
}.collect {
Log.d("xys", "Collect Flow in ${Thread.currentThread().name}")
Log.d("xys", "Result---$it")
}

这样map就会执行在主线程了。

同时,你也可以多次调用flowOn来不断的切换线程,让前面的操作符执行在不同的线程中。

取消Flow

Flow也是可以被取消的,最常用的方式就是通过withTimeoutOrNull来取消,代码如下所示。

MainScope().launch {
withTimeoutOrNull(2500) {
flow {
for (i in 1..5) {
delay(1000)
emit(i)
}
}.collect {
Log.d("xys", "Flow: $it")
}
}
}

这样当输出1、2之后,Flow就被取消了。

Flow的取消,实际上就是依赖于协程的取消。

Flow的同步非阻塞模型

首先,我们要理解下,什么叫同步非阻塞,默认场景下,Flow在没有切换线程的时候,运行在协程作用域指定的线程,这就是同步,那么非阻塞又是什么呢?我们知道emit和collect都是suspend函数,所谓suspend函数,就是会挂起,将CPU资源让出去,这就是非阻塞,因为suspend了就可以让一让,让给谁呢?让给其它需要执行的函数,执行完毕后,再把资源还给我。

所以,我们来看下面这个例子。

flow {
for (i in 0..3) {
emit(i)
}
}.onStart {
Log.d("xys", "Start Flow in ${Thread.currentThread().name}")
}.onEach {
Log.d("xys", "emit value---$it")
}.collect {
Log.d("xys", "Result---$it")
}

输出为:

D/xys: Start Flow in main
D/xys: emit value---0
D/xys: Result---0
D/xys: emit value---1
D/xys: Result---1
D/xys: emit value---2
D/xys: Result---2
D/xys: emit value---3
D/xys: Result---3

可以发现,emit一个,collect拿一个,这就是同步非阻塞,互相谦让,这样谁都可以执行,看上去flow中的代码和collect中的代码,就是同步执行的。

异步非阻塞模型

假如我们给Flow增加一个线程切换,让Flow执行在子线程,同样是上面的代码,我们再来看下执行情况。

flow {
for (i in 0..3) {
emit(i)
}
}.onStart {
Log.d("xys", "Start Flow in ${Thread.currentThread().name}")
}.onEach {
Log.d("xys", "emit value---$it")
}.flowOn(Dispatchers.IO).collect {
Log.d("xys", "Collect Flow in ${Thread.currentThread().name}")
Log.d("xys", "Result---$it")
}

输出为:

D/xys: Start Flow in DefaultDispatcher-worker-1
D/xys: emit value---0
D/xys: emit value---1
D/xys: emit value---2
D/xys: emit value---3
D/xys: Collect Flow in main
D/xys: Result---0
D/xys: Collect Flow in main
D/xys: Result---1
D/xys: Collect Flow in main
D/xys: Result---2
D/xys: Collect Flow in main
D/xys: Result---3

这个时候,Flow就变成了异步非阻塞模型,异步呢,就更好理解了,因为在不同线程,而此时的非阻塞,就没什么意义了,由于flow代码先执行,而这里的代码由于没有delay,所以是同步执行的,执行的同时,collect在主线程进行监听。

除了使用flowOn来切换线程,使用channelFlow也可以实现异步非阻塞模型。


收起阅读 »

Hilt 扩展 | MAD Skills

案例: WorkManager 扩展Hilt 扩展是一个生成代码的库,常通过注解处理器实现。生成的代码作为构成 Hilt 依赖项注入关系图的模块或入口点。Jetpack 中 WorkManager 的集成库就是一个扩展的例子。WorkManager ...
继续阅读 »

案例: WorkManager 扩展

Hilt 扩展是一个生成代码的库,常通过注解处理器实现。生成的代码作为构成 Hilt 依赖项注入关系图的模块或入口点。

Jetpack 中 WorkManager 的集成库就是一个扩展的例子。WorkManager 扩展帮助我们减少向 worker 提供依赖项时所需的模板代码及配置。该库由两部分组成,分别为 androidx.hilt:hilt-work 和 androidx.hilt:hilt-compiler。第一部分包含 HiltWorker 注解以及一些运行时的辅助类,第二部分是一个注解处理器,根据第一部分中注解提供的信息生成模块。

扩展的使用非常简单,仅需在您的 worker 上添加 @HiltWorker 注解:

@HiltWorker
public class ExampleWorker extends Worker {
// ...
}

扩展编译器会生成一个添加了 @Module 注解的类:

@Generated("androidx.hilt.AndroidXHiltProcessor")
@Module
@InstallIn(SingletonComponent.class)
@OriginatingElement(
topLevelClass = ExampleWorker.class
)
public interface ExampleWorker_HiltModule {
@Binds
@IntoMap
@StringKey("my.app.ExmapleWorker")
WorkerAssistedFactory<? extends ListenableWorker> bind(
ExampleWorker_AssistedFactory factory);
}

该模块为 worker 定义了一个可以访问 HiltWorkerFactory 的绑定。然后,配置 WorkerManager 使用该 factory,从而使 worker 的依赖项注入可用。

Hilt 聚合

启用扩展的一个关键机制是 Hilt 能够从类路径中发现模块和入口点。这被称为聚合,因为模块和入口点被聚合到带有 @HiltAndroidApp 注解的 Application 中。

由于 Hilt 具有聚合能力,任何通过添加 @InstallIn 注解生成 @Module 及 @EntryPoint 的工具都可以被 Hilt 发现,并在编译期成为 Hilt DI 图中的一部分。这使得扩展可以轻松地以插件形式集成到 Hilt,无需开发者处理任何额外工作。

注解处理器

生成代码的常规途径是使用注解处理器。源文件转换为 class 文件之前,注解处理器会在编译器中运行。当资源带有处理器所声明的已支持的注解时,处理器会进行处理。处理器可以生成进一步需要被处理的方法,因此编译器会不断循环运行注解处理器,直到没有新的内容产生。一旦所有的环节都完成,编译器才会将源文件转换为 class 文件。

△ 注解处理示意图

△ 注解处理示意图

由于循环机制,处理器可以相互作用。这非常重要,因为这使得 Hilt 的注解处理器可以处理由其他处理器生成的 @Module 或 @EntryPoint 类。这也意味着您的扩展也可以建立在其他人编写的扩展之上!

WorkManager extension processor 根据带有 @HiltWorker 注解的类生成代码,同时验证注解用法并使用 JavaPoet 等库生成代码。

Hilt 扩展注解

Hilt API 中有两个重要的注解: @GeneratesRootInput 和 @OriginatingElement。扩展应该使用这些注解才能与 Hilt 正确集成。

扩展应该使用 @GeneratesRootInput 来启用代码生成的注解。这让 Hilt 注解处理器知道它应该在生成组件之前完成扩展注解处理器的工作。例如,@HiltWorker 注解本身是被 @GeneratesRootInput 注解修饰的:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
@GeneratesRootInput
public @interface HiltWorker {
}

所生成的带有 @Module、@EntryPoint 以及 @InstallIn 注解的类都需要添加 @OriginatingElement 注解,该注解的输入参数是触发模块或入口点生成的顶层类。这就是 Hilt 判断生成的模块和入口点是否在本地测试的依据。例如,在 Hilt 测试中定义了一个添加 @HiltWorker 注解的内部类,模块的初始元素就是测试值。

测试案例如下:

@HiltAndroidTest
class SampleTest {
@HiltWorker
class TestWorker extends Worker {
// …
}
}

生成的模块包含 @OriginatingElement 注解:

@Module
@InstallIn(SingletonComponent.class)
@OriginatingElement(
topLevelClass = SampleTest.class
)
public interface SampleTest_TestWorker__HiltModule {
// …
}

心得

Hilt 扩展支持多种可能性,以下是创建扩展的一些心得:

项目中的通用模式

如果您的项目中有创建模块或入口点的通用模式,那么它们很大概率可以通过使用 Hilt 扩展实现自动化。举个例子,如果每一个实现特定接口的类都必须创建一个具有多绑定的模块,那么可以创建一个扩展,只需在实现类上添加注解即可生成多重绑定模块。

支持非标准成员注入

对于那些 Framework 中已经支持带有实例化能力的成员注入类型,我们需要创建一个 @EntryPoint。如果有多种类型需要被成员注入,那么自动创建入口点的扩展会很有用。例如,需要通过 ServiceLoader 发现服务实现的库负责实例化发现的服务。为了将依赖项注入到服务实现中,必须创建一个 @EntryPoint。通过使用 Hilt 扩展,可以使用在实现类上添加注解完成自动生成入口点。扩展可以进一步生成代码以使用入口点,例如由服务实现扩展的基类。这类似于 @AndroidEntryPoint 为 Activity 创建 @EntryPoint,并创建使用生成的入口点在 Activity 中执行成员注入的基类。

镜像绑定

有时需要使用不同的限定符来镜像或重新声明绑定。当存在自定义组件时,这可能更常见。为了避免丢失重新声明的绑定,可以创建 Hilt 扩展以自动生成其他镜像绑定的模块。例如,考虑包含不同依赖项实现的应用中 "付费" 和 "免费" 订阅的情况。然后,每一层都有两个不同的自定义组件,这样您就可以确定依赖关系的作用域。当添加一个通用的未限定作用域的绑定时,定义绑定的模块可以在其 @InstallIn 中包含两个组件,也可以加载在父组件中,通常是单例组件。但是当绑定被限定作用域时,模块必须被复制,因为需要不同的限定符。实现一个扩展就可以生成两个模块,可以避免样板代码并确保不会遗漏通用绑定。

总结

Hilt 的扩展可以进一步增强代码库中的依赖项注入能力,因为它们可以实现与 Hilt 尚不支持的其他库集成。总而言之,扩展通常由两部分组成,包含扩展注解的运行时部分,以及生成 @Module 或 @EntryPoint 的代码生成器 (通常是注解处理器)。扩展的运行时部分可能有额外的辅助类,这些辅助类使用声明在生成的模块或入口点中绑定。代码生成器还可能生成与扩展相关的附加代码,它们无需专门生成模块和入口点。

扩展必须使用两个注解才能与 Hilt 正确交互:

  • @GeneratesRootInput 添加在扩展注解上。
  • @OriginatingElement 由扩展添加在生成的模块或入口点上。

最后,您可以查看 hilt-install-binding 项目,这是一个简单扩展的示例,它展示了本文中提到的概念。

以上便是 MAD Skills 系列关于 Hilt 的全部内容,如需观看视频全集,请移步到 Hilt - MAD Skills 播放列表。感谢阅读本文!

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!

收起阅读 »

面试官:Java从编译到执行,发生了什么?

面试官:今天从基础先问起吧,你是怎么理解Java是一门「跨平台」的语言,也就是「一次编译,到处运行的」?候选者:很好理解啊,因为我们有JVM。候选者:Java源代码会被编译为class文件,class文件是运行在JVM之上的。候选者:当我们日常开发安装JDK的...
继续阅读 »

面试官:今天从基础先问起吧,你是怎么理解Java是一门「跨平台」的语言,也就是「一次编译,到处运行的」?

候选者:很好理解啊,因为我们有JVM。

候选者:Java源代码会被编译为class文件,class文件是运行在JVM之上的。

候选者:当我们日常开发安装JDK的时候,可以发现JDK是分「不同的操作系统」,JDK里是包含JVM的,所以Java依赖着JVM实现了『跨平台』

候选者:JVM是面向操作系统的,它负责把Class字节码解释成系统所能识别的指令并执行,同时也负责程序运行时内存的管理。

面试官那要不你来聊聊从源码文件(.java)到代码执行的过程呗?

候选者:嗯,没问题的

候选者:简单总结的话,我认为就4个步骤:编译->加载->解释->执行

候选者:编译:将源码文件编译成JVM可以解释的class文件。

候选者:编译过程会对源代码程序做 「语法分析」「语义分析」「注解处理」等等处理,最后才生成字节码文件。

候选者:比如对泛型的擦除和我们经常用的Lombok就是在编译阶段干的。

候选者:加载:将编译后的class文件加载到JVM中。

候选者:在加载阶段又可以细化几个步骤:装载->连接->初始化

候选者:下面我对这些步骤又细说下哈。

候选者:【装载时机】为了节省内存的开销,并不会一次性把所有的类都装载至JVM,而是等到「有需要」的时候才进行装载(比如new和反射等等)

候选者:【装载发生】class文件是通过「类加载器」装载到jvm中的,为了防止内存中出现多份同样的字节码,使用了双亲委派机制(它不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上)

候选者:【装载规则】JDK 中的本地方法类一般由根加载器(Bootstrp loader)装载,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader )实现装载,而程序中的类文件则由系统加载器(AppClassLoader )实现装载。

候选者:装载这个阶段它做的事情可以总结为:查找并加载类的二进制数据,在JVM「堆」中创建一个java.lang.Class类的对象,并将类相关的信息存储在JVM「方法区」中

面试官:嗯…

候选者:通过「装载」这个步骤后,现在已经把class文件装载到JVM中了,并创建出对应的Class对象以及类信息存储至方法区了。

候选者:「连接」这个阶段它做的事情可以总结为:对class的信息进行验证、为「类变量」分配内存空间并对其赋默认值。

候选者:连接又可以细化为几个步骤:验证->准备->解析

候选者:1. 验证:验证类是否符合 Java 规范和 JVM 规范

候选者:2. 准备:为类的静态变量分配内存,初始化为系统的初始值

候选者:3. 解析:将符号引用转为直接引用的过程

面试官:嗯…

候选者:通过「连接」这个步骤后,现在已经对class信息做校验并分配了内存空间和默认值了。

候选者:接下来就是「初始化」阶段了,这个阶段可以总结为:为类的静态变量赋予正确的初始值。

候选者:过程大概就是收集class的静态变量、静态代码块、静态方法至()方法,随后从上往下开始执行。

候选者:如果「实例化对象」则会调用方法对实例变量进行初始化,并执行对应的构造方法内的代码。

候选者:扯了这么多,现在其实才完成至(编译->加载->解释->执行)中的加载阶段,下面就来说下【解释阶段】做了什么

候选者:初始化完成之后,当我们尝试执行一个类的方法时,会找到对应方法的字节码的信息,然后解释器会把字节码信息解释成系统能识别的指令码。

候选者:「解释」这个阶段它做的事情可以总结为:把字节码转换为操作系统识别的指令

候选者:在解释阶段会有两种方式把字节码信息解释成机器指令码,一个是字节码解释器、一个是即时编译器(JIT)。

候选者:JVM会对「热点代码」做编译,非热点代码直接进行解释。当JVM发现某个方法或代码块的运行特别频繁的时候,就有可能把这部分代码认定为「热点代码」

候选者:使用「热点探测」来检测是否为热点代码。「热点探测」一般有两种方式,计数器和抽样。HotSpot使用的是「计数器」的方式进行探测,为每个方法准备了两类计数器:方法调用计数器和回边计数器

候选者:这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。

候选者:即时编译器把热点方法的指令码保存起来,下次执行的时候就无需重复的进行解释,直接执行缓存的机器语言

面试官:嗯…

候选者:解释阶段结束后,最后就到了执行阶段。

候选者:「执行」这个阶段它做的事情可以总结为:操作系统把解释器解析出来的指令码,调用系统的硬件执行最终的程序指令。

候选者:上面就是我对从源码文件(.java)到代码执行的过程的理解了。

面试官:嗯…我还想问下你刚才提到的双亲委派模型…

候选者:下次一定!

本文总结:

  • Java跨平台因为有JVM屏蔽了底层操作系统

  • Java源码到执行的过程,从JVM的角度看可以总结为四个步骤:编译->加载->解释->执行

    • 「编译」经过 语法分析、语义分析、注解处理 最后才生成会class文件
    • 「加载」又可以细分步骤为:装载->连接->初始化。装载则把class文件装载至JVM,连接则校验class信息、分配内存空间及赋默认值,初始化则为变量赋值为正确的初始值。连接里又可以细化为:验证、准备、解析
    • 「解释」则是把字节码转换成操作系统可识别的执行指令,在JVM中会有字节码解释器和即时编译器。在解释时会对代码进行分析,查看是否为「热点代码」,如果为「热点代码」则触发JIT编译,下次执行时就无需重复进行解释,提高解释速度
    • 「执行」调用系统的硬件执行最终的程序指令


作者:Java3y
链接:https://juejin.cn/post/7020193184303185950
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

面试官:双亲委派模型你了解吗?

面试官:要不你今天来详细讲讲双亲委派机制? 候选者:嗯,好的。 候选者:上次提到了:class文件是通过「类加载器」装载至JVM中的 候选者:为了防止内存中存在多份同样的字节码,使用了双亲委派机制(它不会自己去尝试加载类,而是把请求委托给父加载器去完成,依次向...
继续阅读 »

面试官要不你今天来详细讲讲双亲委派机制?


候选者:嗯,好的。


候选者:上次提到了:class文件是通过「类加载器」装载至JVM中的


候选者:为了防止内存中存在多份同样的字节码,使用了双亲委派机制(它不会自己去尝试加载类,而是把请求委托给父加载器去完成,依次向上)


候选者:JDK 中的本地方法类一般由根加载器(Bootstrp loader)装载,JDK 中内部实现的扩展类一般由扩展加载器(ExtClassLoader )实现装载,而程序中的类文件则由系统加载器(AppClassLoader )实现装载。



候选者:这应该很好理解吧?


面试官:雀食(确实)!


面试官顺着话题,我想问问,打破双亲委派机制是什么意思?


候选者:很好理解啊,意思就是:只要我加载类的时候,不是从APPClassLoader->Ext ClassLoader->BootStrap ClassLoader 这个顺序找,那就算是打破了啊


候选者:因为加载class核心的方法在LoaderClass类的loadClass方法上(双亲委派机制的核心实现)


候选者:那只要我自定义个ClassLoader,重写loadClass方法(不依照往上开始寻找类加载器),那就算是打破双亲委派机制了。


面试官:这么简单?


候选者:嗯,就是这么简单


面试官那你知道有哪个场景破坏了双亲委派机制吗?


候选者:最明显的就Tomcat啊


面试官:详细说说?


候选者:在初学时部署项目,我们是把war包放到tomcat的webapp下,这意味着一个tomcat可以运行多个Web应用程序(:


候选者:是吧?


面试官:嗯..


候选者:那假设我现在有两个Web应用程序,它们都有一个类,叫做User,并且它们的类全限定名都一样,比如都是com.yyy.User。但是他们的具体实现是不一样的


候选者:那么Tomcat是如何保证它们是不会冲突的呢?


候选者:答案就是,Tomcat给每个 Web 应用创建一个类加载器实例(WebAppClassLoader),该加载器重写了loadClass方法,优先加载当前应用目录下的类,如果当前找不到了,才一层一层往上找(:


候选者:那这样就做到了Web应用层级的隔离



面试官嗯,那你还知道Tomcat还有别的类加载器吗?


候选者:嗯,知道的


候选者:并不是Web应用程序下的所有依赖都需要隔离的,比如Redis就可以Web应用程序之间共享(如果有需要的话),因为如果版本相同,没必要每个Web应用程序都独自加载一份啊。


候选者:做法也很简单,Tomcat就在WebAppClassLoader上加了个父类加载器(SharedClassLoader),如果WebAppClassLoader自身没有加载到某个类,那就委托SharedClassLoader去加载。


候选者:(无非就是把需要应用程序之间需要共享的类放到一个共享目录下嘛)


面试官:嗯..


候选者:为了隔绝Web应用程序与Tomcat本身的类,又有类加载器(CatalinaClassLoader)来装载Tomcat本身的依赖


候选者:如果Tomcat本身的依赖和Web应用还需要共享,那么还有类加载器(CommonClassLoader)来装载进而达到共享


候选者:各个类加载器的加载目录可以到tomcat的catalina.properties配置文件上查看


候选者:我稍微画下Tomcat的类加载结构图吧,不然有点抽象



面试官:嗯,还可以,我听懂了,有点意思。


面试官顺便,我想问下,JDBC你不是知道吗,听说它也是破坏了双亲委派模型的,你怎么理解的。


候选者:Eumm,这个有没有破坏,见仁见智吧。


候选者:JDBC定义了接口,具体实现类由各个厂商进行实现嘛(比如MySQL)


候选者:类加载有个规则:如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。


候选者:我们用JDBC的时候,是使用DriverManager进而获取Connection,DriverManager在java.sql包下,显然是由BootStrap类加载器进行装载


候选者:当我们使用DriverManager.getConnection()时,得到的一定是厂商实现的类。


候选者:但BootStrap ClassLoader会能加载到各个厂商实现的类吗?


候选者:显然不可以啊,这些实现类又没在java包中,怎么可能加载得到呢


面试官:嗯..


候选者:DriverManager的解决方案就是,在DriverManager初始化的时候,得到「线程上下文加载器」


候选者:去获取Connection的时候,是使用「线程上下文加载器」去加载Connection的,而这里的线程上下文加载器实际上还是App ClassLoader


候选者:所以在获取Connection的时候,还是先找Ext ClassLoader和BootStrap ClassLoader,只不过这俩加载器肯定是加载不到的,最终会由App ClassLoader进行加载



面试官:嗯..


候选者:那这种情况,有的人觉得破坏了双亲委派机制,因为本来明明应该是由BootStrap ClassLoader进行加载的,结果你来了一手「线程上下文加载器」,改掉了「类加载器」


候选者:有的人觉得没破坏双亲委派机制,只是改成由「线程上下文加载器」进行类加载,但还是遵守着:「依次往上找父类加载器进行加载,都找不到时才由自身加载」。认为”原则”上是没变的。


面试官:那我了解了


本文总结




  • 前置知识: JDK中默认类加载器有三个:AppClassLoader、Ext ClassLoader、BootStrap ClassLoader。AppClassLoader的父加载器为Ext ClassLoader、Ext ClassLoader的父加载器为BootStrap ClassLoader。这里的父子关系并不是通过继承实现的,而是组合。




  • 什么是双亲委派机制: 加载器在加载过程中,先把类交由父类加载器进行加载,父类加载器没找到才由自身加载。




  • 双亲委派机制目的: 为了防止内存中存在多份同样的字节码(安全)




  • 类加载规则: 如果一个类由类加载器A加载,那么这个类的依赖类也是由「相同的类加载器」加载。




  • 如何打破双亲委派机制: 自定义ClassLoader,重写loadClass方法(只要不依次往上交给父加载器进行加载,就算是打破双亲委派机制)




  • 打破双亲委派机制案例: Tomcat



    • 为了Web应用程序类之间隔离,为每个应用程序创建WebAppClassLoader类加载器

    • 为了Web应用程序类之间共享,把ShareClassLoader作为WebAppClassLoader的父类加载器,如果WebAppClassLoader加载器找不到,则尝试用ShareClassLoader进行加载

    • 为了Tomcat本身与Web应用程序类隔离,用CatalinaClassLoader类加载器进行隔离,CatalinaClassLoader加载Tomcat本身的类

    • 为了Tomcat与Web应用程序类共享,用CommonClassLoader作为CatalinaClassLoader和ShareClassLoader的父类加载器

    • ShareClassLoader、CatalinaClassLoader、CommonClassLoader的目录可以在Tomcat的catalina.properties进行配置




  • 线程上下文加载器: 由于类加载的规则,很可能导致父加载器加载时依赖子加载器的类,导致无法加载成功(BootStrap ClassLoader无法加载第三方库的类),所以存在「线程上下文加载器」来进行加载。


作者:Java3y
链接:https://juejin.cn/post/7020935051860770853
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »