注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

来聊聊 关于SwiftUI State的一些细节

本文转载自:onevcat.com/2021/01/swi…,本文转载出于传递更多信息之目的,版权归原作者或者来源机构所有。@State 基础在 SwiftUI 中,我们使用 @State 进行私有状态管理,并驱动 View&nb...
继续阅读 »


本文转载自:onevcat.com/2021/01/swi…,本文转载出于传递更多信息之目的,版权归原作者或者来源机构所有。

@State 基础

在 SwiftUI 中,我们使用 @State 进行私有状态管理,并驱动 View 的显示,这是基础中的基础。比如,下面的 ContentView 将在点击加号按钮时将显示的数字 +1:

struct ContentView: View {
@State private var value = 99
var body: some View {
VStack(alignment: .leading) {
Text("Number: (value)")
Button("+") { value += 1 }
}
}
}

当我们想要将这个状态值传递给下层子 View 的时候,直接在子 View 中声明一个变量就可以了。下面的 View 在表现上来说完全一致:

struct DetailView: View {
let number: Int
var body: some View {
Text("Number: (number)")
}
}

struct ContentView: View {
@State private var value = 99
var body: some View {
VStack(alignment: .leading) {
DetailView(number: value)
Button("+") { value += 1 }
}
}
}

在 ContentView 中的 @State value 发生改变时,ContentView.body 被重新求值,DetailView 将被重新创建,包含新数字的 Text 被重新渲染。一切都很顺利。

子 View 中自己的 @State

如果我们希望的不完全是这种被动的传递,而是希望 DetailView 也拥有这个传入的状态值,并且可以自己对这个值进行管理的话,一种方法是在让 DetailView 持有自己的 @State,然后通过初始化方法把值传递进去:

struct DetailView0: View {
@State var number: Int
var body: some View {
HStack {
Text("0: (number)")
Button("+") { number += 1 }
}
}
}

// ContentView
@State private var value = 99
var body: some View {
// ...
DetailView0(number: value)
}

这种方法能够奏效,但是违背了 @State 文档中关于这个属性标签的说明:

… declare your state properties as private, to prevent clients of your view from accessing them.

如果一个 @State 无法被标记为 private 的话,一定是哪里出了问题。一种很朴素的想法是,将 @State 声明为 private,然后使用合适的 init 方法来设置它。更多的时候,我们可能需要初始化方法来解决另一个更“现实”的问题:那就是使用合适的初始化方法,来对传递进来的 value 进行一些处理。比如,如果我们想要实现一个可以对任何传进来的数据在显示前就进行 +1 处理的 View:

struct DetailView1: View {
@State private var number: Int

init(number: Int) {
self.number = number + 1
}
//
}

但这会给出一个编译错误!

Variable ‘self.number’ used before being initialized

在最新的 Xcode 中,上面的方法已经不会报错了:对于初始化方法中类型匹配的情况,Swift 编译时会将其映射到内部底层存储的值,并完成设置。 不过,对于类型不匹配的情况,这个映射依然暂时不成立。比如下面的 var number: Int? 和输入参数的 number: Int就是一个例子。因此,我决定 还是把下面的讨论再保留一段时间。

一开始你可能对这个错误一头雾水。我们会在本文后面的部分再来看这个错误的原因。现在先把它放在一边,想办法让编译通过。最简单的方式就是把 number 声明为 Int?

struct DetailView1: View {
@State private var number: Int?

init(number: Int) {
self.number = number + 1
}

var body: some View {
HStack {
Text("1: (number ?? 0)")
Button("+") { number = (number ?? 0) + 1 }
}
}
}

// ContentView
@State private var value = 99
var body: some View {
// ...
DetailView1(number: value)
}

问答时间,你觉得 DetailView1 中的 Text 显示的会是什么呢?是 0,还是 100?

如果你回答的是 100 的话,恭喜,你答错掉“坑”里了。比较“出人意料”,虽然我们在 init中设置了 self.number = 100,但在 body 被第一次求值时,number 的值是 nil,因此 0会被显示在屏幕上。

@State 内部

问题出在 @State 上:SwiftUI 通过 property wrapper 简化并模拟了普通的变量读写,但是我们必须始终牢记,@State Int 并不等同于 Int,它根本就不是一个传统意义的存储属性。这个 property wrapper 做的事情大体上说有三件:

  1. 为底层的存储变量 State<Int> 这个 struct 提供了一组 getter 和 setter,这个 State struct 中保存了 Int 的具体数字。
  2. 在 body 首次求值前,将 State<Int> 关联到当前 View 上,为它在堆中对应当前 View 分配一个存储位置。
  3. 为 @State 修饰的变量设置观察,当值改变时,触发新一次的 body 求值,并刷新屏幕。

我们可以看到的 State 的 public 的部分只有几个初始化方法和 property wrapper 的标准的 value:

struct State<Value> : DynamicProperty {
init(wrappedValue value: Value)
init(initialValue value: Value)
var wrappedValue: Value { get nonmutating set }
var projectedValue: Binding<Value> { get }
}

不过,通过打印和 dump State 的值,很容易知道它的几个私有变量。进一步地,可以大致猜测相对更完整和“私密”的 State 结构如下:

struct State<Value> : DynamicProperty {
var _value: Value
var _location: StoredLocation<Value>?

var _graph: ViewGraph?

var wrappedValue: Value {
get { _value }
set {
updateValue(newValue)
}
}

// 发生在 init 后,body 求值前。
func _linkToGraph(graph: ViewGraph) {
if _location == nil {
_location = graph.getLocation(self)
}
if _location == nil {
_location = graph.createAndStore(self)
}
_graph = graph
}

func _renderView(_ value: Value) {
if let graph = _graph {
// 有效的 State 值
_value = value
graph.triggerRender(self)
}
}
}

SwiftUI 使用 meta data 来在 View 中寻找 State 变量,并将用来渲染的 ViewGraph 注入到 State 中。当 State 发生改变时,调用这个 Graph 来刷新界面。关于 State 渲染部分的原理,超出了本文的讨论范围。有机会在后面的博客再进一步探索。

对于 @State 的声明,会在当前 View 中带来一个自动生成的私有存储属性,来存储真实的 State struct 值。比如上面的 DetailView1,由于 @State number 的存在,实际上相当于:

struct DetailView1: View {
@State private var number: Int?
private var _number: State<Int?> // 自动生成
// ...
}

这为我们解释了为什么刚才直接声明 @State var number: Int 无法编译:

struct DetailView1: View {
@State private var number: Int

init(number: Int) {
self.number = number + 1
}
//
}

Int? 的声明在初始化时会默认赋值为 nil,让 _number 完成初始化 (它的值为 State<Optional<Int>>(_value: nil, _location: nil));而非 Optional 的 number 则需要明确的初始化值,否则在调用 self.number 的时候,底层 _number 是没有完成初始化的。

于是“为什么 init 中的设置无效”的问题也迎刃而解了。对于 @State 的设置,只有在 View 被添加到 graph 中以后 (也就是首次 body 被求值前) 才有效。

当前 SwiftUI 的版本中,自动生成的存储变量使用的是在 State 变量名前加下划线的方式。这也是一个代码风格的提示:我们在自己选择变量名时,虽然部分语言使用下划线来表示类型中的私有变量,但在 SwiftUI 中,最好是避免使用 _name 这样的名字,因为它有可能会被系统生成的代码占用 (类似的情况也发生在其他一些 property wrapper 中,比如 Binding 等)。

几种可选方案

在知道了 State struct 的工作原理后,为了达到最初的“在 init 中对传入数据进行一些操作”这个目的,会有几种选择。

首先是直接操作 _number

struct DetailView2: View {
@State private var number: Int

init(number: Int) {
_number = State(wrappedValue: number + 1)
}

var body: some View {
return HStack {
Text("2: (number)")
Button("+") { number += 1 }
}
}
}

因为现在我们直接插手介入了 _number 的初始化,所以它在被添加到 View 之前,就有了正确的初始值 100。不过,因为 _number 显然并不存在于任何文档中,这么做带来的风险是这个行为今后随时可能失效。

另一种可行方案是,将 init 中获取的 number 值先暂存,然后在 @State number 可用时 (也就是在 body ) 中,再进行赋值:

struct DetailView3: View {
@State private var number: Int?
private var tempNumber: Int

init(number: Int) {
self.tempNumber = number + 1
}

var body: some View {
DispatchQueue.main.async {
if (number == nil) {
number = tempNumber
}
}
return HStack {
Text("3: (number ?? 0)")
Button("+") { number = (number ?? 0) + 1 }
}
}
}

不过,这样的做法也并不是很合理。State 文档中明确指出:

You should only access a state property from inside the view’s body, or from methods called by it.

虽然 DetailView3 可以按照预期工作,但通过 DispatchQueue.main.async 中来访问和更改 state,是不是推荐的做法,还是存疑的。另外,由于实际上 body 有可能被多次求值,所以这部分代码会多次运行,你必须考虑它在 body 被重新求值时的正确性 (比如我们需要加入 number == nil 判断,才能避免重复设值)。在造成浪费的同时,这也增加了维护的难度。

对于这种方法,一个更好的设置初值的地方是在 onAppear 中:

struct DetailView4: View {
@State private var number: Int = 0
private var tempNumber: Int

init(number: Int) {
self.tempNumber = number + 1
}

var body: some View {
HStack {
Text("4: (number)")
Button("+") { number += 1 }
}.onAppear {
number = tempNumber
}
}
}

虽然 ContentView中每次 body 被求值时,DetailView4.init 都会将 tempNumber 设置为最新的传入值,但是 DetailView4.body 中的 onAppear 只在最初出现在屏幕上时被调用一次。在拥有一定初始化逻辑的同时,避免了多次设置。

如果一定要从外部给 @State 一个初始值,这种方式是笔者比较推荐的方式:从外部在 initializer 中直接对 @State 直接进行初始化, 是反模式的做法:一方面它事实上违背了 @State 应该是纯私有状态这一假设,另一方面由于 SwiftUI 中 View 只是一个“虚拟”的结构,而非真实的渲染 对象,即使表现为同一个视图,它在别的 view 的 body 中是可能被重复多次创建的。在初始化方法中做 @State 赋值,很可能导致已经改变的现有状态 被意外覆盖,这往往不是我们想要的结果。

State, Binding, StateObject, ObservedObject

@StateObject 的情况和 @State 很类似:View 都拥有对这个状态的所有权,它们不会随着新的 View init 而重新初始化。这个行为和 Binding 以及 ObservedObject 是正好相反的:使用 Binding 和 ObservedObject 的话,意味着 View 不会负责底层的存储,开发者需要自行决定和维护“非所有”状态的声明周期。

当然,如果 DetailView 不需要自己拥有且独立管理的状态,而是想要直接使用 ContentView中的值,且将这个值的更改反馈回去的话,使用标准的 @Bining 是毫无疑问的:

struct DetailView5: View {
@Binding var number: Int
var body: some View {
HStack {
Text("5: (number)")
Button("+") { number += 1 }
}
}
}

状态重设

对于文中的情景,想要对本地的 State (或者 StateObject) 在初始化时进行操作,最合适的方式还是通过在 .onAppear 里赋值来完成。如果想要在初次设置后,再次将父 view 的值“同步”到子 view 中去,可以选择使用 id modifier 来将子 view 上的已有状态清除掉。在一些场景下,这也会非常有用:

struct ContentView: View {
@State private var value = 99

var identifier: String {
value < 105 ? "id1" : "id2"
}

var body: some View {
VStack(alignment: .leading) {
DetailView(number: value)
Button("+") { value += 1 }
Divider()
DetailView4(number: value)
.id(identifier)
}
}

被 id modifier 修饰后,每次 body 求值时,DetailView4 将会检查是否具有相同的 identifier。如果出现不一致,在 graph 中的原来的 DetailView4 将被废弃,所有状态将被清除,并被重新创建。这样一来,最新的 value 值将被重新通过初始化方法设置到 DetailView4.tempNumber。而这个新 View 的 onAppear 也会被触发,最终把处理后的输入值再次显示出来。

总结

对于 @State 来说,严格遵循文档所预想的使用方式,避免在 body 以外的地方获取和设置它的值,会避免不少麻烦。正确理解 @State 的工作方式和各个变化发生的时机,能让我们在迷茫时找到正确的分析方向,并最终对这些行为给出合理的解释和预测。

iOS相关资料下载

收起阅读 »

详细分析iOS启动页广告

iOS
最近公司有个需求,需要添加启动页广告,查了不少资料,基本上有2种说法。一种是实时展示广告,另外一种是先保存,下次再展示本地的。对于这两种说法,仔细了研究下,有可取之处,也有一些小缺点。下面就和大家慢慢探讨下。1.先下载后展示方案先说下我采用的方案,APP首次启...
继续阅读 »


最近公司有个需求,需要添加启动页广告,查了不少资料,基本上有2种说法。一种是实时展示广告,另外一种是先保存,下次再展示本地的。对于这两种说法,仔细了研究下,有可取之处,也有一些小缺点。下面就和大家慢慢探讨下。

1.先下载后展示方案

先说下我采用的方案,APP首次启动,加载引导页,然后进入首页,这第一次不展示启动页广告。可以选择在didFinishLaunchingWithOptions里面,先网络请求广告,判断本地是有已经存储了相同的广告信息,如果是,则不用理会。不是,则存储到本地上。

等下次进来,可以判断是否有本地存储的广告信息,有则直接展示,没有就直接进入首页。

优点:启动流程流畅,无影响,不会影响用户启动体验。

缺点:广告不是实时的。例如本地广告已经下架了,这时候启动还加载本地的是不是就出问题了。对于这点,我觉得还是要看公司实际运营情况来确定,如果有后台返回的有效期,就能避免这种情况。

想了下,无伤大雅,影响也不是很大。采用这种方式感觉也不错。

2.实时展示方案

这个方案,有研究过,也是一种不错的做法。APP启动,直接网络请求广告,我们直接跳到广告页,这里也分成2种情况。

一种情况,先加载本地固定的广告,1S内有广告数据返回,倒计时开启,直接展示广告,没有广告,或者网络请求失败,直接结束倒计时,进入首页。

另外一种情况,和一开始说的先下载后展示的有点雷同,这时候就是先加载本地下载的广告,1S内有广告数据返回,倒计时开启,直接展示广告,并把广告下载到本地。如果网络请求失败,就倒计时本地下载的广告,如果没有广告,也是直接结束倒计时,进入首页。

优点:实时更新启动广告,保证每次都是最新的。

缺点:广告可以会延迟展示,用户体验可能会差点。

还是想了下,其实感觉都行,毕竟要看注重点在哪里。用户体验嘛,对于我来说,肯定是能不展示倒计时是更好的,直接进入首页。但既然有启动页这广告东西,我觉得展示也行,不要太频繁就好,不要弄得每次打开都有。这只是我的一个小小期望而已。

3.多Windows实现

对于实现这个启动广告功能,又有两种做法,其中一种是利用多windows来实现。

我们在didFinishLaunchingWithOptions里面,先添加2个window。

      // 多window实现,相当于又2个window,1个在下面,1个在上面
self.window = UIWindow(frame: UIScreen.main.bounds)
self.window?.tintColor = .darkGray;
let nav1 = UINavigationController(rootViewController: ViewController())
window?.rootViewController = nav1
window?.makeKeyAndVisible()

// self.splashWindow = UIWindow(frame: CGRect(x: 0, y: 100, width: 300, height: 500))
self.splashWindow = UIWindow(frame: UIScreen.main.bounds)
let splashVC = SplashViewViewController()
let nav = UINavigationController(rootViewController: splashVC)
splashWindow?.rootViewController = nav
splashWindow?.makeKeyAndVisible()

splashWindow是展示广告的,window是展示首页的,window在splashwindow的下面,所以我们先看到的上面是展示广告的,这种做法的好处是在倒计时广告的时候,首页其实已经在请求加载页面了,等倒计时结束,这时候首页也已经加载好了。

4.单window实现

单window的话,无非就是看rootViewController是哪个页面,我们直接由广告页,变成首页就好。这里无非要注意的就是过渡的动画。这里看自己想怎样的效果了。

这种单window用法,我们常见的有登录页,首页互相切换,还有引导页和首页切换等等,实现起来倒是不难。也打算细说了。

5.效果图

按照国际惯例,提供一下GitHubDemo:github.com/wenweijia/S…

6.总结

对于方案提出了2个,都是文字类的,听起来的确是文绉绉的,本来想弄个流程图的,有点懒,也比较忙,后面有时间再补吧

主要是抛砖引玉,还是想看下各位大佬的看法,例如有没有更好的方案,哪些方案需要完善一下,欢迎留意,谢谢!

收起阅读 »

Swift系列 -- 可选类型

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」前言好记性不如烂笔头,学习过后还是要总结输出才能更有利于对知识的消化吸收。因此对于Swift的学习作了一个系列总结:Swift中的函数盘点本篇作为Swift学习总结的第二篇文章,主要探...
继续阅读 »


前言

好记性不如烂笔头,学习过后还是要总结输出才能更有利于对知识的消化吸收。因此对于Swift的学习作了一个系列总结:

本篇作为Swift学习总结的第二篇文章,主要探索的是关于Swift可选项的内容。

还记得在使用OC开发时,对于一个对象而没有初始化时,其默认值为nil,并且可以在后续的操作中将该对象重新赋值为nil。因此我们经常会遇到因为对象为nil造成的程序错误,例如向数组中插入了nil造成闪退。

但是Swift是一门类型安全语言,Swift的类型是不允许变量值为nil的,而不论是引用类型,还是值类型。但是,在实际的开发中,确实存在变量可能为nil的情况,因此Swfit中还提供了一种特殊的类型 -- 可选类型,用于处理这种情况,下面就一起探索下可选项的相关内容。

一、可选类型的本质

Swift的类型安全是指,定义一个变量时,给定了类型,那么就不能再将其它类型的值赋给该变量。当然,这也不是说Swift中定义变量时,必须显式的指定变量类型。Swift还有类型推断,即根据给定的值,自动确定变量类型。如下代码所示:

let a:Int
a = "123" => Cannot assign value of type 'String' to type 'Int'

var b = 20 // 赋值为20,类型推断为Int
b = "swift" => Cannot assign value of type 'String' to type 'Int'

var c:Int = nil => 'nil' cannot initialize specified type 'Int'
var str: String = nil => 'nil' cannot initialize specified type 'String'


在上述事例代码中,a显式指定了类型为Intb通过类型推断也被指定为Int,将String赋值给这两个变量时都报错Cannot assign value of type 'String' to type 'Int' 。

对于cstr两个变量,将其初始值赋值为nil,结果会报错nil无法为指定类型赋初始化值。需要注意的一点是,变量c虽然为Int类型,但是其初始值并不为0,即 var c:Int 和 var c:Int = 0 并不等价

不过在实际的开发过程中,我们经常会遇到无法确定一个变量是否有值的情况,比如在一个相机App中,当我们获取当前摄像头时,不能确定摄像头是否正在被其它App使用,或者摄像头硬件本身有什么问题,因此无法确定是否可以获取成功,那么此时我们就可能得到一个nil值。此时,就需要有一种类型可以接收nil,又可以接收正常的值。在Swift中,用以实现这种类型的就是可选类型

Swift的可选类型的定义方式为类型+?,具体代码如下:Xnip2021-11-17_10-57-25.png可选类型的变量可以给定一个对应类型的初始值,若不给定,则其默认值为nil

当可选类型有具体的值时,与其对应的类型也是有区别的,不能做等价处理,以Int为例:Xnip2021-11-17_14-14-51.pnga为可选类型的Int,b为普通的Int类型,虽然值都为20,但是a的类型打印出来是Optional(20)。在LLDB中po一下查看ab,结果如下:Xnip2021-11-17_14-32-30.png可以发现,b是一个纯粹的Int值20,而a是在20外面包了一层,这一点类似于一个盒子,如下图所示:Xnip2021-11-17_14-51-56.png

图中红色部分表示存储的值,如果可选类型中存储有值,则盒子中存储具体的值,本例中为Int值2,如果可选类型为nil,则盒子为空。

那么如果是多重可选项呢?即可选项能否包裹一层可选项呢?代码如下:

let result:Int?? = 20

通过LLDB调试,可以看到其实际结构如下所示:

Xnip2021-11-18_17-55-34.png

画图可表示为:

Xnip2021-11-18_17-52-31.png

通过LLDB打印出来可以看到可选类型是由一个Optional包裹的类型,那么Optional是什么呢?其实Optional是一个枚举类型,可以发现其定义如下所示:Xnip2021-11-18_23-34-47.png因此如下图所示的代码是等价的:Xnip2021-11-18_23-40-20.pngOptional通过泛型来指定其要包装的类型,并且Optional遵守了ExpressibleByNilLiteral协议,遵守该协议的枚举、结构体或类初始化时允许值为nil。

Optional枚举内部包含nonesome两个case,如果值为nil,则属于none,有值的话则包装为some,由此也可看出Swift枚举的强大。

二、强制解包

既然可选类型是将对应类型的值包在一个盒子中,那么是否可以将可选类型的值赋值给对应类型的变量呢?可以简单做个测试,结果如下:Xnip2021-11-17_17-02-06.png答案显然是否定的,编译器在编译时就会报错Value of optional type 'Int?' must be unwrapped to a value of type 'Int',Int?必须解包成一个 Int类型。

Swift中可选类型的强制解包使用一个!即可,代码如下所示:

let a:Int? = 20
var b:Int = 20
b = a!

代码第三行 b = a!中,可选类型Int a即解包为了Int,并赋值给b。当然a依然是一个可选类型,其值依然为20。

强制解包需要注意以下几点:

  • 1、强制解包后,对于原可选变量的值没有影响,其依然为可选类型
  • 2、值为nil的可选类型,强制解包会发生闪退,因此在使用强制解包时,需要确定可选类型中的值不为nil

与强制解包一起的还有一种类型,隐式解包的可选项,代码表现为类型 + !。例子如下:

let result:Int! = 20
let realInt:Int = result

可以发现result可以直接赋值给一个Int的realInt,因为隐式解包的可选项会隐式的将变量解包,而不会有明显的感知。不过需要明确的一点是,隐式解包依然是可选项,如果不是确定变量会一直有值,使用需要谨慎。

三、可选项绑定

使用可选项强制解包时,为例防止值为nil的闪退,我们可能会像下面这样写代码:

   let a:Int? = 20
   var b:Int

if a != nil {
b = a!
}

相对于直接解包,这样写安全性确实提高了一些,不过Swift提供了一种更加优雅的方式来解决这一问题,即可选项绑定,代码如下:

let a:Int? = 20
var b:Int
if let value = a {
b = value
} else {
print("a的值为nil")
}

如同代码中if后面的条件所示,可选项绑定的语法是let value = 可选项变量。可选项绑定使用在条件判断等地方,如果可选项变量为nil,则条件为false,如果可选项变量不为nil,则会自动解包并赋值给value,只是value的作用域仅限if条件后的{},不能用在else后的{}。

如果有多个可选项绑定,中间需要用,隔开,而不能使用&&,如图所示:

Xnip2021-11-17_18-49-11.png

Xnip2021-11-17_18-51-01.png

四、空合并运算符

Swift中还提供了空运算符 ??,其定义为如下代码

public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?

public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T

空合并运算符是一个二元运算符,假定有两个变量 a 和 b,使用空合并运算符方式为 a ?? b,使用时有以下注意事项:

  • a??b,如果a为nil,则返回b,否则返回a自身
  • a需要为可选项,否则虽然编译器不会报错,但是没有意义
  • b可以是可选项,也可以不是可选项
  • 不管a、b是否都是可选项,两者存储的类型要对应,例如Int?<=> Int 或 Int?<=> Int?
  • 如果b不是可选项,a的值不为nil,则在返回 a 时,会自动解包,事实上b决定了返回值是否解包

4.1 空合并运算符使用举例

以下为空合并运算符的几个例子,假定a、b存储皆为Int值:

  • a为nil,b为Int值2
let a:Int? = nil

let b:Int = 2

let result = a ?? b // result为b的值,且为Int类型

  • a为nil,b为可选类型Int值2
let a:Int? = nil

let b:Int? = 2

let result = a ?? b // result为b的值 Optional(2)

  • a不为nil,b为Int类型
let a:Int? = 3

let b:Int = 2

let result = a ?? b // result为a的值,并且已经解包为3

  • a不为nil,b为可选Int类型
let a:Int? = 3

let b:Int? = 2

let result = a ?? b // result为a的值,并且依然为Optional(3)

还有多个空合并运算符连接使用的情况,如下代码:

let a:Int? = 2
let b:Int? = nil
let c:Int = 4

let result = a ?? b ?? c // result值为2,是 a 解包后的值

如例所示,当多个??连接使用时,决定result值的依然是最后一个变量c,前面 a??b 得到了可选Int?值2,因为c为int类型,所以得到解包后的Int值2

4.2 空合并运算符与可选项绑定

??还可以与可选项绑定结合在一起使用,如下代码所示:

  • 类似于 a != nil || b != nil
let a:Int? = 2
let b:Int? = nil

if let result = a ?? b { // 只要a和b中有一个不为空,就可以进入该条件判断
let c = result
    print(c)
}

  • 类似于 a != nil && b != nil
let a:Int? = 2
let b:Int? = nil

if let c = a, let d = b { // 只有a和b都不为nil时,才会进入条件判断
    print(c)
    print(d)
}

通过上述两种方式,可以更加精简的进行多个可选项的nil值判断,并且在条件为真的情况下可以自动解包,直接使用解包后的值。

五、guard语句

guard语句与if语句类似,都是条件判断语句,其语法规则为:

guard 条件 else {
// 执行代码
}

不过与if语句不同的是,guard语句是条件为false时,进入{}执行代码。如下面的例子所示:

let a:Int? = 20

guard a != nil else {
print("a的值为nil")
return
}
print("a的值为\(a!)")

并且guard语句的代码块中,必须有return或者抛出异常,否则编译器会报错如下:Xnip2021-11-18_15-51-34.png

在最初接触到guard语句时,可能想已经有了if语句,为什么还需要guard呢?并且guard实现的功能,if也可以实现,会觉得其有些多余。但是经过一段时间开发,对两者做出对比后,可以发现在一定程度上,guard表达的语义更加明确,代码的可读性更高。例如,上面的代码改成if语句如下:

if a == nil {
print("a的值为nil")
return
}

print("a的值为\(a!)")

对比两段代码,语义上我们符合我们预期的值 a != nil,如果使用if语句是要判断a==nil,使用guard就判断a != nil,不符合就return即可,因此guard更加适合做容错判断。

六、总结

  • Swift可选类型的本质是Optional枚举,其包含nonesome两个case,none表示当前变量值为nil,some表示当前变量值不为nil
  • Swift可选项不能直接赋值给其包装的类型所对应的变量,应该在解包后赋值,但是需要注意的是,要在保证有值的情况下强制解包,否则会Crash
  • 对于可选类型的判空处理,可以使用可选项绑定来做,这样更加优雅

与OC不同,Swift更加注重安全性,尤其是对于nil的处理上,Swift更加的严谨,虽然在刚接触时会有些不适应,但是在开发过程中却可以省去很多对nil的容错处理,由此也可以看出Swift的强大与设计精妙。以上即为对于Swift中的可选类型的总结,欢迎大家指正。

收起阅读 »

面试官:请你实现一下JS重载?可不是TS重载哦!

一位同学:“如何实现JS重载?”我:“JS有重载吗?不是TS才有吗?”一位同学:“有的,这是网易一道面试题”我:“好吧我想想哈!”什么是重载我第一次看到重载这个词还是在以前学习Java的时候,我一直觉得JavaScript是没有重载的,直到TypeScript...
继续阅读 »
  • 一位同学:“如何实现JS重载?”
  • 我:“JS有重载吗?不是TS才有吗?”
  • 一位同学:“有的,这是网易一道面试题”
  • 我:“好吧我想想哈!”

image.png

什么是重载

我第一次看到重载这个词还是在以前学习Java的时候,我一直觉得JavaScript是没有重载的,直到TypeScript的出现,所以我一直觉得JavaScript没有重载,TypeScript才有,但是现在看来我是错的。

我理解的重载是:同样的函数,不同样的参数个数,执行不同的代码,比如:

/*
* 重载
*/
function fn(name) {
console.log(`我是${name}`)
}

function fn(name, age) {
console.log(`我是${name},今年${age}岁`)
}

function fn(name, age, sport) {
console.log(`我是${name},今年${age}岁,喜欢运动是${sport}`)
}

/*
* 理想结果
*/
fn('林三心') // 我是林三心
fn('林三心', 18) // 我是林三心,今年18岁
fn('林三心', 18, '打篮球') // 我是林三心,今年18岁,喜欢运动是打篮球

但是直接在JavaScript中这么写,肯定是不行的,咱们来看看上面代码的实际执行结果,可以看到,最后一个fn的定义,把前面两个都给覆盖了,所以没有实现重载的效果

我是林三心,今年undefined岁,喜欢运动是undefined
我是林三心,今年18岁,喜欢运动是undefined
我是林三心,今年18岁,喜欢运动是打篮球

我的做法

其实,想要实现理想的重载效果,我还是有办法的,我可以只写一个fn函数,并在这个函数中判断arguments类数组的长度,执行不同的代码,就可以完成重载的效果

function fn() {
switch (arguments.length) {
case 1:
var [name] = arguments
console.log(`我是${name}`)
break;
case 2:
var [name, age] = arguments
console.log(`我是${name},今年${age}岁`)
break;
case 3:
var [name, age, sport] = arguments
console.log(`我是${name},今年${age}岁,喜欢运动是${sport}`)
break;
}
}

/*
* 实现效果
*/
fn('林三心') // 我是林三心
fn('林三心', 18) // 我是林三心,今年18岁
fn('林三心', 18, '打篮球') // 我是林三心,今年18岁,喜欢运动是打篮球

但是那位同学说,网易的面试官好像觉得这么实现可以是可以,但是还有没有更好的实现方法,我就懵逼了。

高端做法

image.png

经过了我的一通网上查找资料,发现了一种比较高端的做法,可以利用闭包来实现重载的效果。这个方法在JQuery之父John Resig写的《secrets of the JavaScript ninja》中,这种方法充分的利用了闭包的特性!

function addMethod(object, name, fn) {
var old = object[name]; //把前一次添加的方法存在一个临时变量old里面
object[name] = function () { // 重写了object[name]的方法
// 如果调用object[name]方法时,传入的参数个数跟预期的一致,则直接调用
if (fn.length === arguments.length) {
return fn.apply(this, arguments);
// 否则,判断old是否是函数,如果是,就调用old
} else if (typeof old === "function") {
return old.apply(this, arguments);
}
}
}

addMethod(window, 'fn', (name) => console.log(`我是${name}`))
addMethod(window, 'fn', (name, age) => console.log(`我是${name},今年${age}岁`))
addMethod(window, 'fn', (name, age, sport) => console.log(`我是${name},今年${age}岁,喜欢运动是${sport}`))

/*
* 实现效果
*/

window.fn('林三心') // 我是林三心
window.fn('林三心', 18) // 我是林三心,今年18岁
window.fn('林三心', 18, '打篮球') // 我是林三心,今年18岁,喜欢运动是打篮球

结语

如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。或者可以加入我的摸鱼群 想进学习群,摸鱼群,请点击这里摸鱼,我会定时直播模拟面试,简历指导,答疑解惑

image.png


作者:Sunshine_Lin
链接:https://juejin.cn/post/7031525301414805518

收起阅读 »

建议收藏!!VueRouter原理和ReactRouter原理

简述 其实Vue和React在很多地方,底层原理和语法上差别并不是很大。底层原理更多的是相同的。就比如说React有JSX,Vue有Template。其实就可以理解成一个东西,就是写法不同。文章 【今天学习了吗?hash 路由和 history 路由简介】简单...
继续阅读 »

简述


其实Vue和React在很多地方,底层原理和语法上差别并不是很大。底层原理更多的是相同的。就比如说React有JSX,Vue有Template。其实就可以理解成一个东西,就是写法不同。文章 【今天学习了吗?hash 路由和 history 路由简介】简单介绍了vue的两种路由模式,但是其背后的原理是什么呢?这里和React路由一起介绍一下!希望对读者有所帮助 ~~~


更新视图但不重新请求页面,是前端路由原理的核心之一!!


Hash模式

hash 虽然出现在 url 中,但不会被包括在 http 请求中,它是用来指导浏览器动作的,对服务器端完全无用,因此,改变 hash 不会重新加载页面。

可以为 hash 的改变添加监听事件:window.addEventListener('hashchange',callBack)

每一次改变 hash(window.localtion.hash),都会在浏览器访问历史中增加一个记录。利用 hash 的以上特点,就可以来实现前端路由"更新视图但不重新请求页面"的功能了。



我们就可以通过 hashchange 去处理一些特殊的操作,执行一些情况下才会执行的代码。而 Vue / React 应用的正是这一原理。通过不同的 路由去调用不同的 函数/JS 去生成不同的页面代码。



举个栗子:


// 这是一个hash模式的网址例子
http://www.xxx.com/#/abcd123

function callBack(e) {
// 通过event对象去获取当前的路由,来判断下一步要进行的一些操作,当然这里不止包含Dom,
// 其他的操作也是可以的
console.log(e.oldURL)
console.log(e.newURL)
}
window.addEventListener('hashchange',callBack)


目前hash模式支持最低版本是IE8,这也就是为什么都说hash模式的兼容性更好了。其实 React 和 Vue 的hash模式的路由底层就是这么简单的。



History模式


History模式,即http://www.xxxx.com/abc/dcd


这种模式会造成浏览器重新请求服务器路由,首先去获取服务器相应的path下的文件。若没有则会造成 404 Not Found! 当然这种形式需要服务端进行配合,将路由重新重定向到我们的打包出来的index.html文件上。


History模式其实就是ES6中的新增BOM对象History。Vue 和 React 设计的也很巧妙,完美的使用了ES6的新增属性。ES6新增的BOM对象History如下:


20316322-a9b1585b2694c9b6.webp


proto里面包含了replaceState 和 pushState方法。replaceState 和 pushState 其实就是vue中的 replace 和 push ,不过就是Vue的作者将其再进行了封装了。


History 存储历史记录是 队列存储 的,也可以理解为一个数组。它也是有 length 属性的。
我们平时操作 go(num) 其实调用的就是这个History队列里面的历史数据,并找到相应的索引进行一个跳转。


因为IE9才支持ES6,所以History模式并不支持IE9以下版本。所以说Hash模式的兼容更好。


以上就是 Vue 和 React 两种路由的底层原理了。


作者:不是Null
链接:https://juejin.cn/post/7031820537676611614
收起阅读 »

iOS App上架技能:不更新版本的情况下删除App Store非主语言的方法、app上架后的事项(ASO及ASA)

iOS App上架技能:不更新版本的情况下删除App Store非主语言的方法、app上架后的事项(ASO及ASA)这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战。前言iOS上架前的准备:kunnan.blog.csdn.net/a...
继续阅读 »

iOS App上架技能:不更新版本的情况下删除App Store非主语言的方法、app上架后的事项(ASO及ASA)

这是我参与11月更文挑战的第17天,活动详情查看:2021最后一次更文挑战

前言

  • iOS上架前的准备:kunnan.blog.csdn.net/article/det…
  • 上架技巧(不更新版本的情况下删除App Store非主语言的方法)
  • 常见上架问题及解决方案(上传ipa包被吃掉、已上架app在AppStore搜不到)
  • app上架后的事项(ASO、ASA)

I、AppStore 上架技巧

1.1 上传构建版本

archive之后通过 Xcode、macOS 版 Transporter 或 altool 上传构建版本

help.apple.com/app-store-c…

  • Xcode 上传 在这里插入图片描述
  • Transporter 在这里插入图片描述
  • 通过 altool 上传您 App 的二进制文件

您可以使用 xcrun(包含在 Xcode 中)来调用 altool,该命令行工具用于公证、验证并上传您 App 的二进制文件至 App Store。在“终端”的命令行中指定以下命令之一:

$ xcrun altool --validate-app -f file -t platform -u username [-p password] [--output-format xml]
$ xcrun altool --upload-app -f file -t platform -u username [-p password] [—output-format xml]

【注】如果您使用自动构建系统,则可以将公证过程集成到现有构建脚本中。Xcode 中的 altool 和 stapler 命令行工具可将您的软件上传至 Apple 公证服务,并将生成的凭证附加到您的可执行文件中。altool 位于:/Applications/Xcode.app/Contents/Developer/usr/bin/altool。

有关更多信息,请参见《altool 指南》

help.apple.com/asc/appsalt…

1.2 不更新版本的情况下删除App Store非主语言的方法

1、由于AppStore缓存原因导致已上架app在AppStore上搜不到的解决方案2、不更新版本的情况下删除App Store非主语言的方法(应用场景:马甲包)

blog.csdn.net/z929118967/…

1.3 对开发权限和上架权限进行分离管理

在大公司通常苹果开发账号归数据中心人管,如果没有专门测试的开发者账号,只能在公司开发者下面添加一个新用户用于测试开发;选择对应职能即可。

在这里插入图片描述 通过添加开发职能账号,方便其他开发者知道app的审核状态。 当然你也可以采用邮件转发来同步信息(当发件人是>no_reply@email.apple.com时,就转发给特定人员 ) 在这里插入图片描述

具体流程举例

苹果版本升级先发邮件给市场管理部邮箱scglb@xxx.com,由对应人员走oa申请流程,审批完成后开发同事邮件发送审批截图+具体事宜给总部研发对应同事,然后总部这边就操作后面的上架流程(打包+上架)。

II、常见上架问题及解决方案

2.1 iOS app因蓝牙功能隐蔽而导致上架被拒绝的解决方案

相关的公众号文章:https://mp.weixin.qq.com/s?__biz=MzI0MjU5MzU5Ng==&mid=2247484133&idx=1&sn=1d50f59ea026c1b4a9d540c9c1222695&chksm=e978b8b6de0f31a0bbcff38495e858d4db16a854828c1c80719df820826d8405f93b3662ef29&mpshare=1&scene=1&srcid=0114rQ5AKSFyy8QxZoG4Jrmf&sharer_sharetime=1610606706852&sharer_shareid=38c24777c9b84b8b44c56026b3aa9bd7&version=3.0.36.2330&platform=mac#rd

2.2 info.plist 的权限配置问题导致的app被吃掉了

如果上传ipa包之后,app被吃掉了,大部分是权限问题。

 <key>NSAppleMusicUsageDescription</key>
 <string>App需要您的同意,才能访问媒体资料库</string>
 <key>NSBluetoothPeripheralUsageDescription</key>
 <string>App需要您的同意,才能访问蓝牙</string>
 <key>NSCalendarsUsageDescription</key>
 <string>App需要您的同意,才能访问日历</string>
 <key>NSCameraUsageDescription</key>
 <string>App需要您的同意,才能访问相机</string>
 <key>NSLocationAlwaysUsageDescription</key>
 <string>App需要您的同意,才能始终访问位置</string>
 <key>NSLocationUsageDescription</key>
 <string>App需要您的同意,才能访问位置</string>
 <key>NSLocationWhenInUseUsageDescription</key>
 <string>App需要您的同意,才能在使用期间访问位置</string>
 <key>NSMicrophoneUsageDescription</key>
 <string>App需要您的同意,才能访问麦克风</string>
 <key>NSPhotoLibraryAddUsageDescription</key>
 <string>To save the conversion results to the phone, you need to open the album permissions.</string>
 <key>NSPhotoLibraryUsageDescription</key>
 <string>To save the conversion results to the phone, you need to open the album permissions.</string>
 <key>NSRemindersUsageDescription</key>
 <string>App需要您的同意,才能访问提醒事项</string>

  • other
 <key>NSAppleMusicUsageDescription</key>
 <string></string>
 <key>NSCalendarsUsageDescription</key>
 <string></string>
 <key>NSCameraUsageDescription</key>
 <string>是否允许此App使用你的相机?</string>
 <key>NSContactsUsageDescription</key>
 <string>是否允许此App访问你的通讯录?</string>
 <key>NSLocationWhenInUseUsageDescription</key>
 <string></string>
 <key>NSMicrophoneUsageDescription</key>
 <string>是否允许此App使用你的麦克风?</string>
 <key>NSPhotoLibraryUsageDescription</key>
 <string>是否允许此App访问你的媒体资料库?</string>
 <key>NSRemindersUsageDescription</key>
 <string></string>

III 、app上架之后的事项

3.1 ASO

blog.csdn.net/z929118967/…

3.2 管理符号表

  • 上传app上线版本的dSYMs文件到bugly,用于后续的app日志文件符号化

3.3 管理代码分支

blog.csdn.net/z929118967/…

3.4 申请iOS App上线爱思助手应用市场

iOS App如何在爱思助手应用市场上架?

blog.csdn.net/z929118967/…

3.5 Apple search ads(ASA)

searchads.apple.com/cn/

时隔五年,ASA(Apple Search Ads,即苹果搜索广告)终于上线中国大陆地区的App Store。 在这里插入图片描述

使用 Apple Search Ads Advanced,你可以在两个位置展示你的 app:

1、一个是“搜索”标签广告,在用户搜索前展示; 2、另一个是搜索结果顶部广告,在用户搜索时展示。

ITC后台和苹果广告这两者是两个不同的体系,两个账号是不同的,单独的一个苹果广告账号可以给多个App进行投放

如果公司下有多个开发者账号,可将这些账号的包授权给同一个投放账号,这样这个投放账号就可以投放不同主体的App。

Q1.目前ASA账户充值是预充值还是后付呢?

现在是要预充值的,因为苹果可能会随时根据你的消耗情况进行扣款。扣款条件主要是分两种情况,分别是满500美金或者7天扣一次,当这两个条件哪个先触达了就按哪个来。

Q2.公司注册的个人小号没有营业执照,这个号下面的App应该怎么推广?

按目前苹果在国内市场的政策来看,要使用苹果广告都需要营业执照,所以这样的小号大概率是没办法推广的。

see also

(高校学生于教育商店选购新款 iPad /Mac 可享受优惠)【修订版】

mp.weixin.qq.com/s/rkRMVUoYK…

更多内容请关注#小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域;更多服务和咨询请关注#公众号:iOS逆向

收起阅读 »

(转载)元宇宙想要玩起来,需要哪些技术支撑?

我以前是做云计算的,现在是做云原生的,要是问我以后是做什么的,我会告诉你——元宇宙。 有人会说,你这个连游戏都不打的人有啥资格谈元宇宙?我会说,你out了,如果现在还只把元宇宙当成个跟游戏沾边的概念,那是你该提高了。 虽然现在元宇宙很火,但我来谈元宇宙绝对不是...
继续阅读 »

我以前是做云计算的,现在是做云原生的,要是问我以后是做什么的,我会告诉你——元宇宙


有人会说,你这个连游戏都不打的人有啥资格谈元宇宙?我会说,你out了,如果现在还只把元宇宙当成个跟游戏沾边的概念,那是你该提高了。


虽然现在元宇宙很火,但我来谈元宇宙绝对不是跟着小札后面炒冷饭,小札把FB改名这事之前没和我商量,算英雄所见略同吧。注意Metaverse 的Meta的英语发音不是“妈他”,而是“麦嗒”,清音“嗒”。


云计算解决了计算资源按需分别的问题,云原生解决了业务应用智能交付的问题,而元宇宙是大数据、人工智能、虚拟现实等热点技术集成后再应用回现实生活的终极形态


电影工作者超级想象力建立了大众对元宇宙的初级认识。几年前,VR眼镜刚火时被资本炒上了天,有个投资界的朋友跟我说,现在还早了点,要是VR能和生产生活场景联动起来就更有价值了。


我非常同意他的观点,VR只是解决了生态链上的一个展现环节,其他配套场景设备没有完善之前只能用来“自嗨”。在这件事上,电影就做得比较好,例如《头号玩家》中全套穿戴设备就考虑得比较周到。


当然元宇宙想要玩起来还要包含很多其他技术要素支撑,除了云计算、大数据和虚拟现实以外,第一位要解决的是万物互联,这里说的不是物联网那种设备接入,而是“******”,这里买个关子,后面是巨大的商机。第二个要解决的是数字资产,数字资产是构建元宇宙的“元”,这里最难的是制定“标准”。有了数字资产以后就可以结合数字孪生技术做初步的场景呈现,数字孪生是工业元宇宙的第一阶段,也是最容易实现元宇宙现实落地的场景,数字化工厂、数字化制造、智能智造都是这个话题的衍生话题,先不展开讨论。


近日,看到著名“十一维”主人在他的文章中对元宇宙提出了一个观点我较为赞同,元宇宙必须要解决如何生存的问题,说白就是如何挣钱养活自己,投资可以解决启动的问题,成长和不断迭代就要靠生态系统的良性运转才能实现。


转自:达叔怎么看


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

Swift组件化如何解耦

组件化如何解耦把同一模块的代码放到一起代码是两个模块的代码,不能放在同一模块的怎么办。问题1很简单,就是从代码层面做好按模块分开。 如A模块的代码全部放到A模块里面,然后要对外的时候,A模块放出对外的接口给其他模块调用。 比如日志模块,他能够独立成一个模块,他...
继续阅读 »

组件化如何解耦

  1. 把同一模块的代码放到一起

  2. 代码是两个模块的代码,不能放在同一模块的怎么办。

问题1很简单,就是从代码层面做好按模块分开。 如A模块的代码全部放到A模块里面,然后要对外的时候,A模块放出对外的接口给其他模块调用。 比如日志模块,他能够独立成一个模块,他不依赖别的模块,所以只需要把负责写日志等的代码放到一个日志模块里面,这样别人想要输出日志。就可以引入日志模块并用日志模块的接口输出日志就行。这里面没有耦合,也就不需要解耦。

问题2,比如A模块会用到B模块的方法,然后B模块又有可能用到A模块的代码,但是又不能把A和B合并为一个模块的时候怎么办。总不能A模块编译都编译不过,因为编译的时候会提示缺少B模块的方法。同样B模块也一样,缺少A模块,他编译都编译不了。

这里就需要对A模块做B模块的解耦,同样B模块也一样要做A模块的解耦。

关于解耦的方法,网上也有挺多。比较有代表性的两种如下:

  1. CTMediator的target-action模式

  2. BeeHive的用protocol实现模块间调用

这里参考BeeHive的思想,用swift实现了一个解耦的例子。

定义

  1. A模块
  2. B模块
  3. Interface模块(保存各个模块的对外接口,比如A或者B模块的对外接口,都放在这里)
  4. 主工程

先看最后的依赖图

image.png

A模块要调用B模块的接口,这里他不需要依赖B模块,他只需要依赖Interface模块,然后A模块要调用B模块的功能,他只需要调用B模块放到Interface模块的接口就行。

如何实现:

Interface模块里面有一个公共类,比如叫:ModuleInterface 然后他里面有两个方法, 一个注册函数是让别的模块把自己实例注册到这里来的 一个是获取函数,通过某个key,获取到对应的模块的实例

public class ModuleInterface {
public static let shared = ModuleInterface()
public var protocols: [String: BaseProtocol] = [:] // 维护一个字典
// 注册函数
public func registProtocol(by name: String, instance: BaseProtocol) {
self.protocols[name] = instance
}
// 获取实例
public func getProtocol(by name: String) -> BaseProtocol? {
return self.protocols[name]
}
}

这里的核心是维护一个字典,通过对应的key,找到对应的实例。

然后B模块的公开接口也放在这个Interface模块里面,如:

extension ModuleInterface: BProtocol {
// b对外的接口
public func getBModuleValue(b: String, callback: ((Int)->Void)) -> Int {
if let pro = self.getProtocol(by: "BProtocol") as? BProtocol {
return pro.getBModuleValue(b: b, callback: callback)
} else {
print("no found BProtocol instance")
callback(0)
return 0
}
}
}

这里有两个技巧

  1. 使用extension ModuleInterface: BProtocol, 这样可以把BProtocol里面定义的方法,实现到ModuleInterface类里面,这样别的模块调用的时候,统一用ModuleInterface来调用就行,入口简单
  2. 使用self.getProtocol(by: "BProtocol") as? BProtocol,通过转类型的方式得到BProtocol的实例,就可以调用B模块的方法了。而且是运行时检查,这样也解决了,没有引用B模块也能编译通过。

如上:这样每个模块只需要在初始化的时候把自己的实例添加到这个字典里面去。然后想调用其他模块的时候,只需要从这个字典拿出对应模块的实例,再去调用别的模块就行。

然后A模块要想使用B模块的getBModuleValue的方法时,他只需要引入Interface模块,然后从ModuleInterface里面去调用如下:

let a = ModuleInterface.shared.getBModuleValue(b: "a call b") { value in
print("==callBModule=result==", value)
}

整个代码实现非常简单。

具体pod的代码如下:

A模块的podspec的定义
s.dependency 'Interface'

A模块的podfile的定义
use_frameworks!

platform :ios, '9.0'

target 'AModule_Example' do
pod 'AModule', :path => '../'
pod 'Interface', :path => '../../Interface'
end

B模块的podspec的定义
s.dependency 'Interface'
B模块的podfile的定义
use_frameworks!

platform :ios, '9.0'

target 'BModule_Example' do
pod 'BModule', :path => '../'
pod 'Interface', :path => '../../Interface'
end

主工程Demo的podfile的定义
use_frameworks!

platform :ios, '9.0'

target 'Demo' do
pod 'AModule', :path => '../AModule'
pod 'BModule', :path => '../BModule'
pod 'Interface', :path => '../Interface'
end

具体代码看例子: github.com/yxh265/Modu…

收起阅读 »

拒绝编译等待 - 动态研发模式 ARK

iOS
拒绝编译等待 - 动态研发模式 ARK作者:字节跳动终端技术——徐纪光背景iOS 业界研发模式多为 CocoaPods + Xcode + Git 的多仓组件化开发模型。为追求极致的研发体验、提升研发效率,对该研发模式进行了大量优化,但目前遇到了以下瓶颈,亟需...
继续阅读 »

拒绝编译等待 - 动态研发模式 ARK

作者:字节跳动终端技术——徐纪光

背景

iOS 业界研发模式多为 CocoaPods + Xcode + Git 的多仓组件化开发模型。为追求极致的研发体验、提升研发效率,对该研发模式进行了大量优化,但目前遇到了以下瓶颈,亟需突破:

  • pod install 时间长:编译优化绝大部分任务放在了 CocoaPods 上,CocoaPods 承担了更多工作,执行时间因此变长。
  • 编译时间长:虽然现阶段绝大部分工程已经从源码编译转型成二进制编译,但编译耗时依旧在十分钟左右,且现有工程基础上已无更好优化手段。
  • 超大型工程通病:Xcode Index 慢、爆内存、甚至卡死,链接时间长。

如何处理这些问题?

究其本质,产生这些问题的原因在于工程规模庞大。据此我们停下了对传统模式各节点的优化工作,以"缩小工程规模"为切入点,探索新型研发模式——动态研发模式 ARK。

ARK[1] 是全链路覆盖的动态研发模式,旨在保证工程体验的前提下缩小工程规模:通过基线构建的方式,提供线下研发所需物料;同时通过实时的动态库转化技术,保证本地研发仅需下载和编译开发仓库。

Show Case

动态研发模式本地研发流程图如下。接下来就以抖音产品为例,阐述如何使用 ARK 做一次本地开发。

演示基于字节跳动本地研发工具 MBox[2] 。

流程图

  1. 仓库下载

ARK 研发模式下,本地研发不再拉取主仓代码,取而代之的是 ARK 仓库。ARK 仓库含有与主仓对应的所有配置,一次适配接入后期不需要持续维护。

相较传统 APP 仓库动辄几个 GB 的大小,ARK 仓库贯彻了缩减代码规模这一概念。仓库仅有应用配置信息,不包含任何组件代码。ARK 仓库大小仅 2 MB,在 1 s 内可以完成仓库下载 。

在 MBox 中的使用仅需几步点击操作。首先选择要开发的产品,然后勾选 ark 模式,选择开发分支,最后点击 Create 便可以数秒完成仓库下载。

  1. 开发组件

CocoaPods 下进行组件开发一般是将组件仓库下载到本地,修改 Podfile 对应组件 A 为本地引用 pod A, :path =>'./A' ,之后进行本地开发。而在 MBox 和 ARK 的研发流程中,仅需选择要开发的组件点击 Add 便可进行本地开发。

动态研发模式 ARK 通过解析 Podfile.lock 支持了 Checkout From Commit 功能,该功能根据宿主的组件依赖信息自动拉取相应的组件版本到本地,带来便捷性的同时也保证了编译成功率。

  1. pod install

传统研发模式下 pod install 必须要经历 解析 Podfile 依赖、下载依赖、创建 Pods.xcodeproj 工程、集成 workspace 四个步骤,其中依赖解析和下载依赖两个步骤尤为耗时。

ARK 研发模式下 Podfile 中没有组件,因此依赖解析、下载依赖这两个环节耗时几乎为零。其次由于工程中仅需开发组件步骤中添加的组件,在创建 Pods 工程、集成工程这两个环节中代码规模的降低,对提升集成速度的效果非常显著。

没有依赖信息,编译、链接阶段显然不能成功。ARK 解决方案通过自研 cocoapods-ark 及配套工具链来保证编译、链接、运行的成功,其原理后续会在系列文章中介绍。

  1. 开发组件编译&调试

和传统模式一样通过 Xcode 打开工程的 xcworkspace ,即可正常开发、调试完整的应用。

工程中仅保留开发组件,但是依然有变量、函数、头文件跳转能力;参与 Index、编译的规模变小,Xcode 几乎不存在 loading 状态,大型工程也可以秒开;编译速度大幅提升。在整个动态研发流程中,通过工具链将组件从静态库转化成动态库,链接时间明显缩短。

  1. 查看全源码

ARK 工程下默认只有开发组件的源码,查看全源码是开发中的刚需。动态研发流程提供了 pod doc 异步命令实现该能力,此命令可以在开发时执行,命令执行完成后重启工程即可通过 Document Target 查看工程中其他组件源码。

pod doc 优点:

  • 支持异步和同步,执行过程中不影响本地开发。
  • 执行指令时跳过依赖解析环节,从服务端获取依赖信息,下载源码。
  • 通过 xcodegen 异步生成 Document 工程,大幅降低 pod install 时间。
  • 仅复用 pod installer 中的资源下载、缓存模块。
  • 支持仓库统一鉴权,自动跳过无权限组件仓库。

收益

体验上: 与传统模式开发流程一致,零成本切换动态研发模式。

工具上: 站在巨人的肩膀上,CocoaPods 工具链相关优化在 ARK 同样生效。

时间上: 传统研发模式中,历经各项优化后虽然能将全链路开发时间控制在 20 分钟左右,但这样的研发体验依旧不够友好。开发非常容易在这个时间间隔内被其他事情打断,良好的研发体验应该是连贯的。结合本地开发体验我们认为,一次连贯的开发体验应该将工程集成时间控制在分钟级,当前研发模式成功做到了这一点,将全链路开发时间控制在 5 分钟以内。

成功率: 成功率一直是研发效率中容易被忽视的一个指标。据不完全数据统计,集团内应用的本地全量编译成功率不足五成。一半的同学会在首次编译后执行重试。显然,对于工程新手来说就是噩梦,这意味着很长时间将在这一环节中浪费。而 ARK 从平台基线到本地工具链贯彻 Sandbox 的理念,整体上提高编译成功率。

写在最后

ARK 当前已经在字节跳动内部多业务落地使用。从初期技术方案的探索到实际落地应用,遇到了很多技术难点,也对研发模式有了新的思考。

相关技术文章将陆续分享,敬请期待。

扩展阅读

[1] ARK: github.com/kuperxu/Kwa…

[2] MBox: mp.weixin.qq.com/s/5_IlQPWnC…

字节跳动终端技术团队(Client Infrastructure)是大前端基础技术的全球化研发团队(分别在北京、上海、杭州、深圳、广州、新加坡和美国山景城设有研发团队),负责整个字节跳动的大前端基础设施建设,提升公司全产品线的性能、稳定性和工程效率;支持的产品包括但不限于抖音、今日头条、西瓜视频、飞书、懂车帝等,在移动端、Web、Desktop等各终端都有深入研究。

火山引擎应用开发套件MARS是字节跳动终端技术团队过去九年在抖音、今日头条、西瓜视频、飞书、懂车帝等 App 的研发实践成果,面向移动研发、前端开发、QA、 运维、产品经理、项目经理以及运营角色,提供一站式整体研发解决方案,助力企业研发模式升级,降低企业研发综合成本。

收起阅读 »

iOS使用addChildViewController

iOS
「这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战」。iOS早在iOS5的时候为了解耦、更加清晰的处理页面View的逻辑,UIViewController提供了addChildViewController方法,将ViewControl...
继续阅读 »

「这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战」。

iOS早在iOS5的时候为了解耦、更加清晰的处理页面View的逻辑,UIViewController提供了addChildViewController方法,将ViewController作为容器处理视图控制器的切换,将比较复杂的UI使用子ViewController来管理。

iOS5.0之前只能在ViewControllerview中不断的通过addSubView添加subViewVCview视图层级中。这样使得主ViewController中的内容越来越混乱,代码越来越多,subView的管理越来越困难。

iOS5.0之后按照MVC的原则,每个ViewController只需要管理一个view视图层次结构,因此我们可以使用childViewController来拆分开发中比较复杂的View。并且此时的childViewController拥有了与父ViewController同步的声明周期。

项目中使用:

在我们项目的APP首页的实现中使用到了,首页内容展示位推荐分类菜单,以及每个菜单下的内容展示,不同的分类下的内容view展示的UI多样化。

相关方法:

///子视图控制器数组
@property(nonatomic,readonly) NSArray *childViewControllers

///向父VC中添加子VC
- (void)addChildViewController:(UIViewController *)childController

///将子VC从父VC中移除
- (void) removeFromParentViewController

///fromViewController 当前显示在父视图控制器中的子视图控制器
///toViewController 将要显示的姿势图控制器
///duration 动画时间
/// options 动画效果(渐变,从下往上等等,具体查看API)
///animations 转换过程中得动画
///completion 转换完成
- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion

///当向父VC添加子VC之后,该方法会自动调用;
- (void)willMoveToParentViewController:(UIViewController *)parent

///从父VC移除子VC之后,该方法会自动调用
- (void)didMoveToParentViewController:(UIViewController *)parent

如何使用?:

  • 如果在view上添加的只是简单的控件的话,那么使用addSubView添加到父ViewController上;
  • 如果子视图是比较复杂的视图集合,功能丰富,就选择使用addChildViewController来添加新的子ViewController,但也需要通过addSubview将子ViewControllerview添加到父视图的视图层级中;
  • iOS5之后使用addChildViewController时的原则,我们在使用addSubview的时候,同时调用addChildViewController方法将subView对应的viewController也加到当前viewController的管理中;
  • 对于那些不需要显示的subView,只需通过addChildViewControllersubVC添加到父控制器中,需要显示时再调用transitionFromViewController方法将其显示出来;
  • 当收到系统的 Memory Warning 的时候,系统也会自动把当前没有显示的 subview 销毁掉 掉,以节省内存;
  • 优点:
  1. 使页面逻辑更加清晰明了,遵循MVC模式,每个View对应相应的ViewController;
  2. 当存在不需显示的view时,将不会被加载,减少内尺使用;
  3. 当收到内存警告时,会将没有加载出的view率先释放,优化了程序的内存释放机制;

系统方法解释:

  • addChildViewController

[A父视图控制器 addChildViewController:B子视图控制器]在视图控制器A中添加了子视图控制器B.调用这个方法时如果子视图控制器已经有父视图控制器了,那么调用该方法会先把子视图控制器从之前的父视图控制器中移除,然后再添加到当前的视图控制器上作为子视图控制器。

注意:调用addChildViewController后会自动调用willMoveToParentViewController:superVC方法;

  • removeFromParentViewController

将子视图控制器从父视图控制器中移除,移除之后将自动调用didMoveToParentViewController

注意:调用removeFromParentViewControlle后会调用didMoveToParentViewController:nil方法

  • willMoveToParentViewController

当一个视图控制器从视图控制器容器中被添加或者被删除之前,该方法被调用parent:父视图控制器,如果没有父视图控制器,将为nil;当调用removeFromParentViewController方法是必须先手动调用该方法,且parent参数为nil。

  • didMoveToParentViewController

当从一个视图控制容器中添加或者移除viewController后,该方法被调用;当调用addChildViewController方法时必须手动调用该方法,且parent参数为父控制器。

代码展示:

  • 添加子VC
//自动调用,可以省略 
//[childVC willMoveToParentViewController: superVC];
[superVC addChildViewController:childVC];
[superVC.view addSubview:childVC.view];
[childVC didMoveToParentViewController:superVC];

  • 删除子VC
[childVC willMoveToParentViewController];
[childVC removeFromParentViewController];
//自动调用,可以省略
//[childVC didMoveToParentViewController:nil];

  • 切换子VC
[self addChildViewController:newController];
[self transitionFromViewController:oldController toViewController:newController duration:1.5f options:UIViewAnimationOptionCurveEaseOut animations:^{

} completion:^(BOOL finished) {
if (finished) {
[newController didMoveToParentViewController:self];
[oldController willMoveToParentViewController:nil];
[oldController removeFromParentViewController];
self.currentVC = newController;
}
else{
self.currentVC = oldController;
}
}];

总结:

  1. addChildViewController向父视图控制器中添加子视图控制器时,添加之后自动调用willMoveToParentViewController,需要手动调用didMoveToParentViewController
  2. removeFromParentViewController将子视图控制器从父视图控制器中移除,移除之后自动调用didMoveToParentViewController: nil参数为nil,需要在移除前手动调用willMoveToParentViewController
  3. transitionFromViewController:toViewController在调用这个方法之前先调用[fromViewController willMoveToParentViewController:nil]然后在completion后调用[toViewController didMoveToParentViewController:self]方法;
  4. 在切换子视图控制器显示的时候需要保证切换的子视图控制器已经被添加到父视图控制器中;
  5. 当某个子视图控制器将从父视图控制器中删除时,parent参数为nil,即:[将被删除的VC willMoveToParentViewController:nil];
  6. 当某个子试图控制器将加入到父视图控制器时,parent参数为父视图控制器,即:[将被加入的VC didMoveToParentViewController:superVC];

参考文献

blog.csdn.net/yongyinmg/a…


作者:麻蕊老师
链接:https://juejin.cn/post/7031466347410718727

收起阅读 »

(转载)人民日报评论:理性看待当前的元宇宙热潮

近来,“元宇宙”成为热门话题,越来越频繁地出现在人们的视野里。从英伟达宣布推出为元宇宙建立提供基础的模拟和协作平台,到日本社交平台GREE开展元宇宙业务,从微软正努力打造“企业元宇宙”,到脸书改名为元宇宙(Metaverse)一词中的Meta……不仅科技公司在...
继续阅读 »

近来,“元宇宙”成为热门话题,越来越频繁地出现在人们的视野里。从英伟达宣布推出为元宇宙建立提供基础的模拟和协作平台,到日本社交平台GREE开展元宇宙业务,从微软正努力打造“企业元宇宙”,到脸书改名为元宇宙(Metaverse)一词中的Meta……不仅科技公司在元宇宙赛道上争相布局,一些机构也积极参与其中。今天,我们就来聊聊元宇宙这个话题。

图源网络图源网络  

  不得不说的是,尽管已经耳熟能详,但元宇宙概念迄今仍没有清晰准确的定义。近30年前,科幻小说《雪崩》这样描述元宇宙:戴上耳机和目镜,找到连接终端,就能够以虚拟分身的方式进入由计算机模拟、与真实世界平行的虚拟空间。有人认为元宇宙会让人更有身临其境之感,用户将置身“实体互联网”之中;有人概括出元宇宙的几大特征,称元宇宙不仅是与真实世界平行的虚拟空间,更“和现实世界相互影响”,甚至拥有与现实世界相互联通的经济系统……多元的声音不一而足,可以明确的一点是:虽然元宇宙似乎拥有广阔空间和多种可能,但目前还是一个尚未成型的新兴事物。

  元宇宙概念的走红,背后有着相应的技术支撑和社会生活因素。一方面,经过多年的发展,虚拟现实、人工智能、区块链、5G通讯、可穿戴设备等底层技术的应用日渐成熟;另一方面,因为疫情等原因,线上办公、线上课程逐渐普及,人们在虚拟空间的停留时间更长,线上生活所占的比例不断升高。在一些具体的场景中,人们捕捉到元宇宙可能给生活带来的改变。从在游戏中参加虚拟演唱会,到在虚拟空间以虚拟形象参加会议,且会上可以用语音和动作进行实时交互,这些已经成为现实的案例,一定程度上打破了虚拟与现实的界线。尽管如此,有业内人士指出,元宇宙产业还远远达不到全产业覆盖和生态开放、经济自洽、虚实互通的理想状态,在技术层面、法律层面、道德伦理层面,都还有很长一段路要走。

图源网络图源网络  

  我们离元宇宙的世界有多远?这个问题可能短期内不会有答案,但各类打着元宇宙旗号的套路与骗局已经有滋生的苗头。一些知识付费项目把元宇宙包装成一夜暴富的机会,声称“未来只有元宇宙这一条路”,以贩卖焦虑的方式借机敛财。一些人言必称元宇宙,没有任何与之相关的实体内容却热衷于抢注各种相关商标,挖空心思从元宇宙概念中分得一杯“流量羹”。这就提示我们,对待新鲜事物,保持好奇和探索的同时,也要保留一份审慎和理性。即便元宇宙有可能成为真实世界的延伸与拓展,潜在的机遇和可能带来的变革值得期待,每个人仍需理性看待当前的元宇宙热潮,警惕任何以科技和未来为名义的忽悠。

  关于元宇宙的讨论仍在继续,有人充满乐观与向往,也有不少怀疑的声音。是镜花水月还是触摸得到的未来,是资本炒作还是新的赛道,是新瓶装旧酒还是科技新突破,下结论前不妨“让子弹飞一会儿”。不过可以明确的是,一些新概念承载着人们对技术发展的信心,以及对未来美好生活的期待。推动新概念及其产业逐步走向成熟需要时间,通向令人神往的科技未来需要脚踏实地、打好发展地基。正如不论虚拟现实、增强现实还是混合现实,中心词都是“现实”,这也预示着离开了现实的支撑,终归是海市蜃楼无本之木。“基础不牢地动山摇”,这样的道理不论在真实宇宙还是元宇宙,应该都是适用的。

  这正是:

  万物皆可元宇宙?理性常在少烦忧。

原文链接:https://baijiahao.baidu.com/s?id=1716732148118786628&wfr=spider&for=pc

收起阅读 »

「设计模式」iOS 中的适配器模式 Adapter

iOS
1. 生活中的适配器 提到适配器,最先想到什么?莫过于 电源适配器 了,日常使用的电脑、手机等电子设备都会有个电源适配器,作用是将插座里输出的高压交流电转换为电子设备所需的低压直流电。另外,世界各地区除了标准电压不同以外,大部分电源插头形状也不同,所以还有一类...
继续阅读 »

适配器模式.png


1. 生活中的适配器


提到适配器,最先想到什么?莫过于 电源适配器 了,日常使用的电脑、手机等电子设备都会有个电源适配器,作用是将插座里输出的高压交流电转换为电子设备所需的低压直流电。另外,世界各地区除了标准电压不同以外,大部分电源插头形状也不同,所以还有一类适配器,用于连接插头,例如香港的标准插座是三角方头的,就需要一个适配器来连接转换。


概括起来,适配器的功能是让原本不能一起工作的多个设备在不改变自身行为的前提下能一起工作。发散一下的话,笔记本电脑上各种接口使用的扩展坞、在国外旅游可能用到的语言翻译器、各种显卡 / 声卡 / 硬盘的驱动程序等等都可以理解为适配器。


2. 适配器模式


2.1 适配器模式定义


在《Head First 设计模式》中的定义如下:



适配器模式:将一个类的接口,转换成客户期望的另一个接口。适配器让原本接口不兼容的类可以合作无间。



适配器模式中主要有三个角色:



  • Target 目标接口 / 对象

  • Adaptee 被适配的对象

  • Adapter 适配器


即:通过适配器 Adapter 将被适配对象 Adaptee 包装成支持目标接口 Target 的对象,使原来 Target 能够完成的任务现在通过适配器包装后的对象也能支持,所以适配器也称作包装器 Wrapper


例如国标三角插座一般是三角扁头的,而港版电源适配器是三角方头的,在内地就不好使,需要弄一个转换器,把港版电源适配器插在转换器上再把转换器插在国标插座上,就可以正常工作了。上面的国标插座就对应为 Target 目标接口角色,港版三角方头插头是被适配的对象,额外的专用适配器将国标三角扁头转换港版三角方头。


2.2 适配器的类型


按照实现适配器的方式可以分为两种类型:类适配器(继承)  和 对象(组合)适配器。类图如下:


适配器模式类图pure.png


类适配器通过继承,也就是子类化,然后在子类中实现目标接口。在支持多重继承的语言中(C++、Python),类适配器同时继承父类以及 Target,由于在 Objective-C 以及 Java 这类语言不支持多重继承,所以目标接口一般为 协议 Protocol / 接口 Interface


对象适配器通过组合 - 将被适配对象作为适配器的属性,在实现目标接口相关方法中根据需要访问被适配对象。


类适配器 vs 对象适配器



























类适配器对象适配器
实现方式继承组合
作用范围仅被适配者类被适配者类及其子类
其他+ 易于重载,必要时可以覆盖被适配者的行为。+ 结构上更简单,不需要额外属性指向被适配者。+ 可以选择将部分工作委托给被适配者,更具弹性。 - 需要额外属性指向被适配者。

2.3 适配器的优缺点



  • 使用者(客户)与接口绑定,而不是与实现绑定,实现解耦。

  • 让没有关联的类能一起工作,不侵入原有代码,隔离原系统的影响。

  • 过多使用适配器会导致代码结构混乱(任何模式过度使用都会有问题吧:)


2.4 适配器应用场景



  • 面对遗留代码,期望项目统一使用新特性同时兼容已有类。→ eg.《Head First 设计模式》ch 7. 关于迭代器与枚举的示例。

  • 扩展新功能,方便接入新的第三方库。→ eg. 《人人都懂设计模式》电子阅读器中通过适配第三方 PDF 解析库来扩展支持 PDF 阅读。


3. iOS 中的适配器模式


在 iOS 系统上,苹果一般通过协议(可以理解为 Target 为接口)来实现适配器。例如常用的 UITableViewDataSourceUITableViewDelegate,将一个原本不能为 UITableView 提供数据 / 响应相关事件的类包装成数据源 / 代理,显然这是类适配器。详细实践介绍参考 Raywenderlich


3.1 属性包装器 @propertyWrapper


在 Swift 中当需要为属性添加相同的逻辑代码时使用属性包装器会大大减少工作量。属性包装器可以应用于结构体、枚举或者类。


如 Swift 官方文档中的示例,期望整型属性值始终小于 12,可以定义如下 TwelveOrLess 属性包装器:


// 定义 *TwelveOrLess* 属性包装器
@propertyWrapper
struct TwelveOrLess {
// 私有存储属性 number
private var number = 0
// 包装值
var wrappedValue: Int {
get { return number }
set { number = min(newValue, 12) }
}
}

// 使用 *TwelveOrLess* 来定义一个小矩形,长宽都小于等于一定值。
struct SmallRectangle {
@TwelveOrLess var height: Int
@TwelveOrLess var width: Int
}

var rectangle = SmallRectangle()
print(rectangle.height) // 打印 "0"

rectangle.height = 10
print(rectangle.height) // 打印 "10"

rectangle.height = 24
print(rectangle.height) // 打印 "12"


*以下为个人理解,不一定正确。


可以将 @propertWrapper 也理解为一个协议,这个协议要求对象实现包装属性的 Set/Get 方法:


protocol PropertyWrapperProtocol {
var wrappedValue : Int { set get }
}


即通过 TwelveOrLess 实现 PropertyWrapperProtocol 协议,来实现一个适配器,这个适配器的作用是返回一个限定范围内的数,如果尝试设置超过预设最大值的数,也只会保存为最大值。类比于电源适配器将输入的高电压适配器低电压。当然 Swift 中的属性适配器更强大也更灵活,参考 Swift GG 翻译文档 - 属性包装器


3.2 应用代理适配器 UIApplicationDelegateAdaptor


iOS 14 中新增了 UIApplicationDelegateAdaptor 用于包装原来 UIKit 中的应用代理UIApplicationDelegateNSApplicationDelegateAdaptor for AppKit、WKExtensionDelegateAdaptor for WatchKit),以便在 SwiftUI 中访问应用代理。


@propertyWrapper struct UIApplicationDelegateAdaptor<DelegateType> where DelegateType : NSObject, DelegateType : UIApplicationDelegate


从 @propertyWrapper 可以看出实际上是属性包装器的一个具体应用场景。通过泛型 DelegateType 传入一个 NSObject 类型且遵循 UIApplicationDelegate 协议的对象,猜测内部一些操作是通过转交给这个代理对象来执行的,显然是一个 对象适配器。这样使用(参考 HackingWithSwiftstackoverflow: swiftui-app-life-cycle-ios14-where-to-put-appdelegate-code):


class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
print("do something")
return true
}
}

@main
struct testApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

var body: some Scene {
WindowGroup {
ContentView()
}
}
}


有其他 iOS 相关的适配器应用实例欢迎分享讨论。


以上就是目前学习总结的 适配器模式 相关知识了。(有些地方配合图示理解更直观,似乎还差点什么,过些日子一起补上示例代码:)


参考



  1. Raywenderlich - How To Use the Adapter Pattern. 包含一个完整的例子演示如何利用协议(数据源&委托代理)实现通用水平滚动视图。

  2. Bloodline - iOS中的设计模式 - 适配器(Adapter) 介绍挺全面的。

  3. 《Head First 设计模式》ch 7. 适配器模式与外观模式。①用插座作为示例解析适配器;②面向对象适配器小节中的 ‘现有系统 → 适配器 -) 厂商类’ 例子比较形象;③示例:Java 中通过 EnumerationIterator 枚举迭代(适配)器遵循新的 迭代器 Iterator 接口来替代早期的 枚举 Enumeration(除了判断是否还有元素及访问下一个元素,迭代器还支持移除元素) 。

  4. 《人人都懂设计模式 - 从生活中领悟设计模式》第 13 章。①中国古建筑中的榫卯结构例子,不同榫头与榫槽配合工作;②示例:一个支持 .txt 及 .epub 格式的电子阅读器项目,通过适配器适配第三方 PDF 解析库支持 PDF 阅读。


扩展



  • 一般一个适配器只包装一个被适配对象,有没有一个适配器‘包装’多个被适配对象的场景?有!那就是 外观 / 门面模式 Facade Pattern

  • 有的电源适配器除了改变插头形状/电流电压外,还会提供一些额外功能,例如状态指示灯、扩展 USB 接口等,这类特性通过 装饰者模式 Decorator Pattern 实现。(装饰者主要 添加特性,适配器主要 转换接口

链接:https://juejin.cn/post/7031011469189709838
收起阅读 »

Android App 卡顿分析

Android App 反应卡顿,从技术上将就是UI 渲染慢。 UI渲染是从您的应用程序生成一个框架并将其显示在屏幕上的行为。 为了确保用户与您的应用程序的交互顺利,您的应用程序应该在16ms内渲染帧数达到每秒60帧(为什么60fps?)。 如果您的应用程序因...
继续阅读 »

Android App 反应卡顿,从技术上将就是UI 渲染慢。


UI渲染是从您的应用程序生成一个框架并将其显示在屏幕上的行为。 为了确保用户与您的应用程序的交互顺利,您的应用程序应该在16ms内渲染帧数达到每秒60帧(为什么60fps?)。 如果您的应用程序因UI渲染速度缓慢而受到影响,那么系统将被迫跳过帧,用户将感觉到您的应用程序中出现卡顿。 我们把这个叫做jank


本篇文章主要介绍 Android 开发中的部分知识点,通过阅读本篇文章,您将收获以下内容:



1.UI 渲染简介
2.识别Jank
3.Fix Jank
4.引起Jank 通用问题举例



1.UI 渲染简介


为了帮助您提高应用程序质量,Android会自动监视您的应用程序是否有空,并在Android生命危险仪表板中显示信息。 有关如何收集数据的信息,请参阅Play Console文档。


如果您的应用程序出现问题,本页提供诊断和解决问题的指导。


Android生命危险仪表板和Android系统会跟踪使用UI Toolkit的应用程序的渲染时间统计信息(应用程序的用户可见部分是从CanvasView hierarchy绘制的)。


如果您的应用程序不使用UI Toolkit,就像使用VulkanUnityUnrealOpenGL构建的应用程序一样,则在Android Vitals仪表板中不提供时间统计信息。


您可以通过运行
adb shell dumpsys gfxinfo <package name>
来确定您的设备是否正在记录您的应用的渲染时间指标。


2.识别Jank


在您的应用程序中定位引起jank的代码可能很困难。 本部分介绍了三种识别jank的方法:



  • 1.Visual inspection


通过视觉检查,您可以在几分钟内快速浏览应用程序中的所有用例use-cases,但不能提供与Systrace相同的详细信息。



  • 2.Systrace


Systrace提供了更多的细节,但是如果你运行Systrace来处理应用程序中的所有用例,那么就会被大量的数据淹没,难以分析。



  • 3.Custom performance monitoring


Visual inspectionSystrace都会在你的本地设备上检测到。


如果不能在本地设备上重现,则可以构建自定义性能监视器Custom performance monitoring,以测量在现场运行的设备上应用的特定部分。


##1. Visual inspection


目视检查可以帮助您识别正在生产结果的使用案例。 要执行视觉检查,请打开您的应用程序并手动检查应用程序的不同部分,然后查看非常粗糙的UI。 以下是进行目视检查时的一些提示:



  • 1.运行release 版本


运行您release应用程序的版本(或至少不可调试)的版本。ART运行时为了支持调试功能而禁用了一些重要的优化,所以确保你正在寻找类似于用户将看到的东西。





    1. 开启GPU渲染




开启步骤:
Settings -->Developer options -->Profile GPU rending


开启配置文件GPU渲染,会在屏幕上显示条形图,可以快速直观地显示相对于每帧16毫秒基准测试渲染UI窗口帧所花费的时间。
每个条都有着色的组件映射到渲染管道中的一个舞台,所以你可以看到哪个部分花费的时间最长。
例如,如果框架花费大量时间处理输入,则应该查看处理用户输入的应用程序代码。


开启 GPU 渲染效果图





    1. 留意特殊组件




有一些组件,如RecyclerView,是Jank普遍的来源。 如果您的应用程序使用这些组件,那么运行应用程序的这些部分是一个好idea





    1. App 冷启动导致




有时候,只有当应用程序从冷启动启动(Clod start)时,才能复制jank



  • 5.低内存情况下jank 比较容易出现


一旦你发现产生jank的用例,你可能会有一个很好的想法是什么导致你的应用程序的结果。 但是,如果您需要更多信息,则可以使用Systrace进一步深入研究。


##2. Systrace


Systrace是一个显示整个设备在做什么的工具,并且它可以用于识别应用程序中的JankSystrace的系统开销很小,所以在仪器使用过程中你会感受到app卡顿的存在。


Systrace记录跟踪,同时在设备上执行janky用例。 有关如何使用Systrace的说明,请参阅Systrace演练。 systrace被进程和线程分解。 在Systrace中查找应用程序的过程,应该如图所示。


Systrace分析应用程序


上面3个标注点解释



  1. 当卡顿时,会有掉帧发生,如上图1所示


Systrace显示何时绘制每个框架,并对每个框架进行颜色编码以突出显示较慢的渲染时间。 这可以帮助您查找比视觉检查更准确的单个janky框架。 有关更多信息,请参阅Inspecting Frames.




  1. 掉帧提示,如上图 2所示
    Systrace检测应用程序中的问题,并在各个框架和警报面板中显示警报。 警报中的以下指示是您的最佳选择。




  2. systrace timeline 如上图3 所示




Android框架和库的一部分(如RecyclerView)包含跟踪标记。 因此,systrace时间线会显示何时在UI线程上执行这些方法,以及执行多长时间。


如果systrace没有向您显示有关长时间使用UI线程工作的详细信息,则需要使用Android CPU Profiler来记录采样或检测的方法跟踪。 一般来说,method方法痕迹不适合用于识别排队,因为由于开销太大而产生假jank,并且无法看到线程何时被阻塞。 但是,method方法跟踪可以帮助您识别应用中花费最多时间的方法。 在识别这些方法后,add Trace markers a
标记并重新运行systrace,以查看这些方法是否引起混乱。
当记录systrace时,每个跟踪标记(执行的开始 Trace.beginSection();和结束Trace.endSection();对)会增加大约10μs的开销。 为了避免假Jank结局,不要将追踪标记添加到在一帧中被称为几十次的方法中,或者短于200us左右。


如需获取更多内容,请查看Systrace详解


##3. Custom performance monitoring


如果您无法在本地设备上再现突发事件,则可以在您的应用中构建自定义性能监控,以帮助识别现场设备上的突发源。


为此,请使用FrameMetricsAggregator从应用程序的特定部分收集帧渲染时间,并使用Firebase性能监控记录和分析数据。


要了解更多信息,请参阅使用Use Firebase Performance Monitoring with Android Vitals.


#3.Fix Jank


为了解决这个问题,请检查哪些帧在16.7ms内没有完成,并寻找出错的地方。Record View#draw在一些帧中抽取异常长度,或者可能是Layout? 查看下面4这些问题的常见来源,以及其他问题。


为了避免乱码,长时间运行的任务应该在UI线程之外异步运行。 一定要注意你的代码正在运行在哪个线程上,并且在向主线程发布不重要的任务时要小心。


如果您的应用程序有一个复杂而重要的主UI(可能是中央滚动列表),请考虑编写可自动检测缓慢渲染时间的测试测试,并经常运行测试以防止出现回归。 有关更多信息,请参阅自动化性能测试代码实验室。


#4.引起Jank 通用问题举例


以下部分解释了应用程序中常见Jank问题 的来源,以及解决这些问题的最佳方案。


滑动 List


ListView和特别是RecyclerView通常用于复杂的滚动列表,这些列表最容易被忽略。 他们都包含Systrace标记,所以你可以使用Systrace来弄清楚他们是否有助于在你的应用程序jank。 一定要传递命令行参数-a <your-package-name>来获取RecyclerView中的跟踪部分(以及添加的任何跟踪标记)以显示出来。 如果可用,请遵循systrace输出中生成的警报的指导。 在Systrace里面,你可以点击RecyclerView-traced部分查看RecyclerView正在做的工作的解释。


RecyclerView: notifyDataSetChanged


如果您看到RecyclerView中的每个项目在一个框架中被反弹(并因此重新布局和重新绘制),请确保您没有调用notifyDataSetChanged()setAdapter(Adapter)swapAdapter(Adapter,boolean)为小更新。 这些方法表示整个列表内容已经改变,并且将在Systrace中显示为RV FullInvalidate。 而是在内容更改或添加时使用SortedListDiffUtil生成最小更新。


例如,考虑从服务器接收新闻内容列表的新版本的应用程序。 当您将该信息发布到适配器时,可以调用notifyDataSetChanged(),如下所示:


RecyclerView: notifyDataSetChanged

但是这带来了一个很大的缺点 - 如果它是一个微不足道的变化(也许单个项目添加到顶部),RecyclerView不知道 - 它被告知放弃所有的缓存项目状态,因此需要重新绑定一切。


最好使用DiffUtil,它将为您计算和分配最小的更新。

DiffUtil 使用


只需将您的MyCallback定义为DiffUtil.Callback实现,以通知DiffUtil如何检查您的列表。


RecyclerView: Nested RecyclerViews


嵌套RecyclerView是很常见的,特别是水平滚动列表的垂直列表(如Play Store主页上的应用程序的网格)。 这可以很好的工作,但也有很多意外四处移动。 如果在第一次向下滚动页面时看到很多内部项目膨胀,则可能需要检查是否在内部(水平)RecyclerViews之间共享RecyclerView.RecycledViewPools


默认情况下,每个RecyclerView将拥有自己的物品池。 如果在屏幕上同时显示一打itemViews,那么当itemViews不能被不同的水平列表共享的时候,如果所有的行都显示了相似类型的视图,那么这是有问题的。


RecyclerView: Nested RecyclerViews


如果要进一步优化,还可以在内部RecyclerViewLinearLayoutManager上调用setInitialPrefetchItemCount(int)
例如,如果您总是在一行中可见3.5项,请调用innerLLM.setInitialItemPrefetchCount(4);. 这将告诉RecyclerView,当一个水平行即将出现在屏幕上时,如果UI线程上有空闲时间,它应该尝试预取内部的项目


RecyclerView: Too much inflation / Create taking too long


UI线程则处于闲置状态下,RecyclerView中的预取功能应该有助于在大多数情况下通过提前完成工作来解决inflation Layout的成本问题。
如果您在一帧中看到inflation Layout(而不是标记为RV Prefetch的部分),请确保您正在测试最近的设备(Prefetch目前仅在Android 5.0 API Level 21及更高版本上支持),并使用最近版本的Support Library.


如果经常看到inflation Layout导致屏幕上出现新的Jank,验证出问题,请移除多余的ViewRecyclerView内容中的视图类型越少,当新的项目类型出现在屏幕上时,需要完成的inflation Layout就越少。


如果可能的话,将视图类型合并到合理的位置 - 如果只有图标,颜色或文本块在类型之间改变,则可以在绑定时间进行更改,并避免inflation Layout(同时减少应用程序的内存占用)。


如果您的视图类型看起来还不错,请考虑减少inflation Layout的成本。减少不必要的容器和结构视图可以帮助 - 考虑使用ConstraintLayout构建itemView,这可以很容易地减少结构视图。如果你想真正优化性能,你的项目层次结构是简单的,并且你不需要复杂的themingstyle的功能,请考虑自己调用构造函数 - 但请注意,它往往是不值得的损失的简单性和功能的权衡XML。


RecyclerView: Bind taking too long


绑定(即onBindViewHolder(VH,int))应该是非常简单的,除了最复杂的项目之外的所有项目都要花费少于一毫秒的时间。 它只需从adapter's的内部项目数据中获取POJO项目,然后在ViewHolder中的视图上调用setter。 如果RV OnBindView需要很长时间,请确认您在绑定代码中做了最少的工作。


如果您使用简单的POJO对象来保存适配器中的数据,则可以完全避免使用Data Binding l
库来将绑定代码写入onBindViewHolder


RecyclerView or ListView: layout / draw taking too long


有关绘制和布局的问题,请参阅 Layout and Rendering Performance.


ListView: Inflation


如果你不小心,ListView很容易会被意外回收。 如果每次屏幕显示项目时都看到inflation Layout,请检查Adapter.getView()的实现是否正在使用,重新绑定并返回convertView参数。 如果你的getView()实现总是inflation Layout,你的应用程序将无法从ListView中获得回收的好处。 你的getView()的结构几乎总是类似于下面的实现:


复用 convertView


Layout performance


如果Systrace显示Choreographer#doFrame的布局部分工作太多,或者工作频繁,这意味着您遇到了布局性能问题。 您的应用的布局性能取决于View层次结构的哪个部分具有更改布局参数或输入。


Layout performance: Cost


如果段长度超过几毫秒,则可能是针对RelativeLayoutsweighted-LinearLayouts.
的最差嵌套性能。


这些布局中的每一个都可以触发其子项的多个measure/layout传递,因此嵌套它们会导致嵌套深度上的O(n ^ 2)行为。 请尝试避免使用RelativeLayoutLinearLayoutweight特征,除了层次结构的最低叶节点之外的所有特征。 有几种方法可以做到这一点:



  • 优化View结构

  • 使用自定义View

  • 尝试转换到ConstraintLayout,它提供了类似的功能,并且没有性能上的缺陷。


Layout performance: Frequency


当新内容出现在屏幕上时,将会发生新的Layout,例如,当一个新项目在RecyclerView中滚动查看时。 如果在每个框架上都发生重要的布局,则可能是在布局上进行动画处理,这很可能导致丢帧。 通常,动画应该在View的绘图属性(例如setTranslationX / Y / Z()setRotation(),setAlpha()等)上运行。 这些都可以比Layout属性(如填充或边距)更好地更改。 通常通过调用触发invalidate()setter,然后在下一帧中绘制(Canvas),来更改视图的绘制属性。 这将重新记录无效的视图的绘图操作,并且通常也比布局好得多。


Rendering performance渲染性能


Android UI在两个阶段工作 - 在UI线程Record View#draw,在RenderThread上绘制DrawFrame。 第一次运行在每个无效的View上绘制(Canvas),并可能调用自定义视图或代码。 第二个在本地RenderThread上运行,但是将根据Record View#draw阶段生成的工作进行操作。


Rendering performance: UI Thread


如果Record View#draw需要很长时间,则通常是在UI线程上绘制位图的情况。 绘制位图需要使用CPU渲染,一般应该避免在主线程中绘制。 您可以使用Android CPU分析器的方法跟踪来查看这是否是问题。


绘制位图通常是在应用程序想要在显示位图之前修饰位图的时候完成的。 有时候像装饰圆角的装饰:

绘制圆角图片

如果这是您在UI线程上所做的工作,则可以在后台的解码线程上执行此操作。 在这样的一些情况下,你甚至可以在绘制时做这个工作,所以如果你的Drawable或View代码看起来像这样:
性能差代码


可以将上面代码优化为如下:


优化性能的代码


请注意,这通常也可以用于后台保护(在位图顶部绘制渐变)和图像过滤(使用ColorMatrixColorFilter),以及修改位图的其他两种常见操作。


如果由于其他原因(可能将其用作缓存)绘制到位图,则尝试绘制直接传递到ViewDrawable的硬件加速硬件,如有必要,可考虑使用LAYER_TYPE_HARDWARE调用setLayerType()来缓存复杂的渲染 输出,并仍然利用GPU渲染。


Rendering performance: RenderThread


一些canvas操作是便小的消耗,但触发RenderThread昂贵的计算。 Systrace通常会通知这些。


Canvas.saveLayer()


避免Canvas.saveLayer() - 它可以触发昂贵的,未缓存的,离屏渲染每一帧。 尽管Android 6.0的性能得到了提高(当进行优化以避免GPU上的渲染目标切换时),但是如果可能的话,避免使用这个昂贵的API仍然是好事,或者至少确保您通过CLIP_TO_LAYER_SAVE_FLAG(或者调用一个变体 不带标志)。


Animating large Paths


当硬件加速Canvas传递给Views时,Canvas.drawPath()被调用,Android首先在CPU上绘制这些路径,然后将它们上传到GPU。 如果路径较大,请避免逐帧编辑,以便高速缓存和绘制。 drawPoints(),drawLines()和drawRect / Circle / Oval / RoundRect()更有效率 - 即使最终使用更多的绘制调用,最好使用它们。


Canvas.clipPath


clipPath(Path)触发了昂贵的裁剪行为,通常应该避免。 如果可能,选择绘制形状,而不是剪裁到非矩形。 它性能更好,支持抗锯齿。 例如,下面的clipPath调用:


Canvas.clipPath


Bitmap uploads


Android将位图显示为OpenGL纹理,并且首次在一帧中显示位图时,将其上传到GPU。您可以在Systrace中将此视为上传宽度x高度纹理。这可能需要几个毫秒(见下图),但是有必要用GPU显示图像。

位图绘制


如果这些花费很长时间,请首先检查轨迹中的宽度和高度数字。确保正在显示的位图不比显示的屏幕区域大得多。如果是,则浪费上传时间和内存。通常位图加载库提供了简单的方法来请求适当大小的位图。


Android 7.0中,位图加载代码(通常由库完成)可以在需要之前调用prepareToDraw()来及早触发上传。这样上传发生的早,而RenderThread空闲。这可以在解码之后完成,也可以在将位图绑定到View时进行,只要知道位图即可。理想情况下,你的位图加载库会为你做这个,但是如果你正在管理你自己的,或者想确保你没有在新设备上点击上传,你可以在你自己的代码中调用prepareToDraw()


Thread scheduling delays


线程调度程序是Android操作系统的一部分,负责决定系统中哪些线程应该运行,何时运行以及运行多长时间。 有时候,因为你的应用程序的UI线程被阻塞或者没有运行,就会发生JankSystrace使用不同的颜色来指示线程正在Sleep(灰色)Runnable(蓝色:可以运行,但调度程序还没有选择它运行)正在运行(绿色)中断(红色或橙色)。 这对于调试线程调度延迟导致的Jank`问题非常有用。



注意:
旧版本的Android更频繁地遇到不是应用程序故障的调度问题。 在这方面进行了不断的改进,所以考虑在最近的操作系统版本上更多的调试线程调度问题,在这些版本中,被调度的线程更可能是应用程序的错误。



线程渲染过程


UI线程RenderThread预计不会运行时,有框架的一部分。 例如,UI线程RenderThreadsyncFrameState正在运行并且上传位图时被阻塞 - 这是因为RenderThread可以安全地复制UI线程所使用的数据。 另一个例子是,RenderThread在使用IPC时可以被阻塞:在帧的开始处获取缓冲区,从中查询信息,或者通过eglSwapBuffers将缓冲区传回给合成器。


在您的应用程序的执行中经常会有很长时间的暂停,这些都是由Android上的进程间通信(IPC)机制进行的。 在最近的Android版本中,这是UI线程停止运行的最常见原因之一。 一般来说,修正是为了避免调用函数来调用binder; 如果这是不可避免的,那么应该缓存该值,或将工作移动到后台线程。 随着代码库变得越来越大,如果不小心的话,通过调用一些低级别的方法,很容易意外地添加了一个binder调用,但是使用跟踪来发现和修复它们也是很容易的。


如果您有绑定事务,则可以使用以下adb命令来捕获其调用堆栈:

adb 命令捕获堆栈信息


有时像getRefreshRate()这样的无害的表面调用可能会触发绑定事务,并在频繁调用时导致严重的问题。 定期跟踪可以帮助您快速找到并解决这些问题。


UI Thread sleeping due to binder transactions in a RV fling


如果你没有看到绑定Activity,但仍然没有看到你的UI线程运行,请确保你没有等待来自另一个线程的锁定或其他操作。 通常,UI线程不应该等待来自其他线程的结果 - 其他线程应该向其发布信息post message.


Object allocation and garbage collection


对象分配和垃圾回收(GC)已经成为一个问题,因为ARTAndroid 5.0中默认运行时引入的,但是仍然有可能通过这些额外的工作来减轻你的线程负担。 对于每秒钟不会发生多次的罕见事件(如用户单击按钮)进行分配是很好的做法,但要记住,每次分配都需要付出一定的代价。 如果它处于一个频繁调用的紧密循环中,请考虑避免分配来减轻GC上的负载。


Systrace会告诉你GC是否频繁运行,Android Memory Profiler可以显示你的分配来自哪里。 如果你可以避免分配,特别是在紧密的循环中,你应该没有问题。

shows a 94ms GC on the HeapTaskDaemon thread


在最新版本的Android上,GC通常在名为HeapTaskDaemon的后台线程上运行。 请注意,大量的分配可能意味着更多的CPU资源花费在GC上.



至此,本篇已结束,如有不对的地方,欢迎您的建议与指正。同时期待您的关注,感谢您的阅读,谢谢!


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

Android组件化基础

前言 公司包含三大业务线,每条业务线都有独立的app。功能模块难免会有重合~举个栗子,直播功能本来只在业务线A使用,但是由于业务拓展,现在业务线B和C也需要使用直播功能。这时候就有必要将直播功能做成一个独立的直播组件供三条业务线使用。 构思 既然要将直播做成组...
继续阅读 »

前言


公司包含三大业务线,每条业务线都有独立的app。功能模块难免会有重合~举个栗子,直播功能本来只在业务线A使用,但是由于业务拓展,现在业务线B和C也需要使用直播功能。这时候就有必要将直播功能做成一个独立的直播组件供三条业务线使用。


构思


既然要将直播做成组件,需要考虑哪些方面呢?



  1. 既可独立运行,单独测试该组件功能;也可作为sdk,被其他项目使用

  2. 统一管理:部署到私有化仓库,其他项目可配置引用


基础实践


全局控制配置


在gradle.properties中的配置可以在项目中直接使用


# 是否作为module使用
isModule=true

build.gradle的配置



  1. 配置android构建插件


if(isModule.toBoolean()){
// lib
apply plugin: 'com.android.library'
}else{
// 独立运行的app
apply plugin: 'com.android.application'
}


  1. 禁用applicationId配置


作为library不能带有配置,否则编译会报错:Library projects cannot set applicationId. applicationId is set to 'com.example.live' in default config.


android {
...
defaultConfig {
if(!isModule.toBoolean()){
applicationId "com.example.live"
}
...
}

AndroidManifest.xml的配置


1. 独立运行


为了可独立运行,需要配置application和启动Activity


// 正常模板
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.TestAndroidManifest">
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

2. module形式使用


假如被其他项目作为组件使用,则需要修改application和启动入口配置


// 去除application不必要的属性配置
<application>
// 去除intent-filter
<activity
android:name=".MainActivity"
android:exported="true">
</activity>
</application>

这里有两个问题:



  1. application里边的属性配置可以不去掉吗?


其实在编译后,所有module的AndroidManifest会被合并到一起,假如相同属性配置不同会报错


Manifest merger failed : Attribute application@name value=(com.example.moduledemo.MainApplication) from AndroidManifest.xml:7:9-40
is also present at [:live] AndroidManifest.xml:11:9-56 value=(com.example.live.LiveApplication).
Suggestion: add 'tools:replace="android:name"' to <application> element at AndroidManifest.xml:6:5-23:19 to override.

这里我分别给和app-module和live-module指定了自定义appliation,提示合并失败了,解决方案需要通过在app-module配置tools:replace="android:name"。这里通过不同配置然后rebuild查看下输出的AndroidManifest.xml文件可以总结以下规律:



  • 假如只有一个module配置了自定义application,则直接使用该application

  • 假如每个module都配置了自定义application,则需要解决冲突。解决后会使用最后编译的那个module的application(举个例子:demo中,app-module依赖于live-module,假如都配置了自定义application,因为app后编译,所以最后会使用app-module里边定义的)



  1. activity里边的intent-filter可以不去掉吗?


合并.png


看到合并后的文件,里边包含了两个包含启动信息的activity。安装app时你会发现在桌面会有两个启动图标,并且点击他们的行为是一致的:打开第一个配置了MAIN和LAUNCHER的activity。因此是没有必要保留该配置的。


3. 动态配置AndroidManifest


根据上述的分析发现,作为module使用和独立app运行,相应的AndroidManifest.xml也需要相应的进行调整。那我们就有必要根据配置来配置使用不同的AndroidManifest文件了



  1. 在live-module增加用于sdk的AndroidManifest.xml


module.png
3. 在live-module的build.gradle配置动态引用不同的AndroidManifest.xml


android {
...
sourceSets {
main {
if(isModule){
manifest.srcFile 'src/main/module/AndroidManifest.xml'
}else{
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}

总结


至此,你已经可以通过修改gradle.properties里边的liModule来控制是否以library的形式使用live组件了。这里可以思考个问题,假如我们项目中有好几个类似于live这样的组件,是否每个组件都需要做这么繁琐的配置呢?能否将这些配置抽出来,统一管理?


优化


1. 抽取独立app构建脚本


在项目根目录创建一个common_app_build.gradle


apply plugin: 'com.android.application'

android {
compileSdk 31
defaultConfig {
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

sourceSets {
main {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}

2. 抽取构建library脚本


在项目根目录创建一个common_library_build.gradle


apply plugin: 'com.android.library'

android {
compileSdk 31
defaultConfig {
minSdk 21
targetSdk 31
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}

sourceSets {
main {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
}
}
}

3. 在创建一个的course module(用于验证)


4. 修改live和course两个module的build.gradle


下边以live module为例


// 直接通过配置引用不同的gradle文件,前边涉及的配置都可以去掉
if (isModule.toBoolean()) {
apply from: '../common_library_build.gradle'
} else {
apply from: '../common_app_build.gradle'
}

android {
defaultConfig {
if(!isModule.toBoolean()){
applicationId "com.example.live"
}
}
}

后续类似的组件只需要进行简单的配置,即可实现第一点的构思


module发布


这里以live module为例进行实践,# google文档:使用 Maven Publish 插件


发布live module到本地仓库


再live module的build.gradle增加以下配置


afterEvaluate {
publishing {
repositories {
maven {
url uri("../repo")
}
}
publications {
maven(MavenPublication) {
from components.release
groupId "com.example.live"
artifactId "modulelive"
version "1.0.0"
}
}
}
}

上述配置,指定将live发布到 项目/repo/ 目录下。sync完成后,会在live出现publish task


maven.png


双击publish,即会在repo生成相应的aar文件


aar.png


配置根build.gradle


为了可以使用repo里边的aar,需要增加配置


buildscript {
repositories {
...
maven {
url('repo')
}
}
...
}

app中使用:配置build.gradle


dependencies {
...
// 不直接引用project
// api project(':live')
// 改为该配置
implementation 'com.example.live:modulelive:1.0.0'
...
}

重新rebuild就可以正常使用到live组件。


发布到远程仓库


因为不同业务线项目环境不同,发布到本地项目目录下,使用比较不方便吗。所以可以考虑将组件发布到公司内部的私有仓库,供所有项目组使用:


publishing {
...
repositories {
maven {
// 仓库地址
url = "http://...."
// 仓库用户名及密码
credentials {
username ''
password ''
}
}
}
}

总结


上述主要是讲述了Android组件化的一些基础以及如何发布组件的一些流程。当然,组件化的内容不止这些内容,包括:



  • 组件间通信

  • 组件间跳转

  • 组件化混淆

  • 组件资源冲突

  • .....


这些方面都是在进行组件化设计需要思考与处理的~后续逐渐完善这块的内容


gitee:Demo地址


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

Android加载长图方案

背景介绍 在某些特定场景下,我们需要考虑加载长图的需求,比如加载一幅《清明上河图》,这个好像有点过分了,那就加载1/2的《清明上河图》吧... 那TMD还不是一样道理。 言归正传说一下我这边遇到的情况,之前有图片或大图的模块是划分为H5来实现的,现在需求变更划...
继续阅读 »

背景介绍


在某些特定场景下,我们需要考虑加载长图的需求,比如加载一幅《清明上河图》,这个好像有点过分了,那就加载1/2的《清明上河图》吧... 那TMD还不是一样道理。


言归正传说一下我这边遇到的情况,之前有图片或大图的模块是划分为H5来实现的,现在需求变更划分为原生开发,那么问题就来了。


图片尺寸为


image.png


图片大小为


image.png


这一刻我是懵逼的,哪个端图片上传的时候没限制尺寸和压缩?mdzz,
吐槽归吐槽,还是要撸起袖子解决加载长图大图的问题。
先提供几个技术方案来对比一下:


方案1:WebView加载渲染

因为图片本身也是一个URL地址,也是被WebView渲染,并且支持缩放。这是一种实现方案,遇到几M的大图WebView也是会崩溃Crash,所以这种投机的方式并不推荐。


方案2:BitmapRegionDecoder

分片加载,使用系统BitmapRegionDecoder去加载本地的图片,调用bitmapRegionDecoder.decodeRegion解析图片的矩形区域,返回bitmap,最终显示在ImageView上。这种方案需要手动处理滑动、缩放手势,网络图片还要处理缓存策略等问题。实现方式比较繁琐也不是很推荐。


方案3:SubsamplingScaleImageView

一款封装BitmapRegionDecoder的三方库,已经处理了滑动,缩放手势。我们可以考虑选择这个库来进行加载长图,但是官方上的Demo示例加载的长图均为本地图片。这可能并不符合我们的网络场景需求,所以对于网络图片,我们还要考虑不同的加载框架,


SubsamplingScaleImageView Git传送门

方案4:Glide+SubsamplingScaleImageView混合加载渲染

对于图片加载框架,Glide当然是首选,我们使用Glide进行网络图片的下载和缓存管理,FileTarget作为桥梁,SubsamplingScaleImageView进行本地资源图片的分片加载,看起来很靠谱,那么一起来实现吧。


Glide Git传送门

SubsamplingScaleImageView Git传送门

fun loadLargeImage(context: Context, res: String, imageView: SubsamplingScaleImageView) {
imageView.isQuickScaleEnabled = true
imageView.maxScale = 15F
imageView.isZoomEnabled = true
imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)

Glide.with(context).load(res).downloadOnly(object : SimpleTarget<File?>() {
override fun onResourceReady(resource: File, glideAnimation: Transition<in File?>?) {

val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(resource.absolutePath, options)
val sWidth = options.outWidth
val sHeight = options.outHeight
options.inJustDecodeBounds = false
val wm = ContextCompat.getSystemService(context, WindowManager::class.java)
val width = wm?.defaultDisplay?.width ?: 0
val height = wm?.defaultDisplay?.height ?: 0
if (sHeight >= height
&& sHeight / sWidth >= 3) {
imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP)
imageView.setImage(ImageSource.uri(Uri.fromFile(resource)), ImageViewState(0.5f, PointF(0f, 0f), 0))
} else {
imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
imageView.setImage(ImageSource.uri(Uri.fromFile(resource)))
imageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER_IMMEDIATE)
}
}
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
}
})

}

这是我封装起来的一个方法,就很简单就能理解了, 包括SubsamplingScaleImageView的缩放设置,默认展示状态、缩放、位置,计算当前图片高宽比为3倍进行长图渲染处理,否则按正常图片渲染处理。


最后快用下面的这张完整版《清明上河图》来试一试效果吧~ 赞


清明上河图_简书_爱吃大蒜.jpeg


如果我帮助你成功实现了加载长图的需求,千万要记得回来点赞哦,ღ( ´・ᴗ・` )比心

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

关于web中的颜色表示方法,你知道多少?

想要表示web中的各种颜色,大家首先想到的大概就是用十六进制或者RGB来表示。但在实际web中,是远不止这两种的。今天这篇文章就和大家聊一聊,在web中颜色的各种表示方法。 以如下代码为例,大家可以复制代码看看效果: HTML <div class="b...
继续阅读 »

想要表示web中的各种颜色,大家首先想到的大概就是用十六进制或者RGB来表示。但在实际web中,是远不止这两种的。今天这篇文章就和大家聊一聊,在web中颜色的各种表示方法。


以如下代码为例,大家可以复制代码看看效果:


HTML


<div class="box">
<div class="one"></div>
<div class="two"></div>
<div class="three"></div>
</div>

CSS


.box {
width: 200px;
height: 200px;
padding: 20px 20px;
display: flex;
justify-content: space-between;
}
.box > div {
width: 50px;
height: 50px;
border-radius: 4px;
}

英文单词


HTML 和 CSS 颜色规范中预定义了 140+ 个颜色名称,可以点进这里进行查看。直接用英文单词的好处是直接明了,缺点是140+个单词确实难记,也不能包含所有的颜色。


.one { background-color: red; }
.two { background-color: green; }
.three { background-color: blue; }

十六进制


十六进制表示颜色:#RRGGBB ,这里的十六进制实质就是RGB的十六进制表示法,每两位表示RR(红色)、GG(绿色)和 BB(蓝色)三色通道的色阶。所有值必须在 00 到 FF 之间。


.one { background-color: #00FFFF; }
.two { background-color: #FAEBD7; }
.three { background-color: #7FFFD4; }

对于类似于 #00FFFF 的颜色格式也可以缩写为 #0FF


.one { background-color: #0FF; }

如果需要带上透明度,还可以像下面这样增加两个额外的数字:


.one { background-color: #00FFFF80; }

RGB


rgb() 函数中,CSS语法如下:


rgb(red, green, blue)

每个参数 red, green, blue 定义颜色的强度,可以是 0 到 255 之间的整数或百分比值(从 0% 到 100%)


.one { background-color: rgb(112,128,144); }
.two { background-color: rgb(30%,10%,60%); }
.three { background-color: rgb( 0,139,139); }
复制代码

十六进制和RGB的原理都是利用了光的三原色:红色,绿色,蓝色。利用这三种颜色就能组合出上千万种颜色。简单的计算一下,256级的RGB色彩总共能组合出约1678万种色彩,即256×256×256=16777216种。至于为什么是256级,因为 0 也是数值之一。


RGBA


RGBA就是在RGB之上扩展了一个 Alpha 通道 ,指定对象的不透明度。


.one { background-color: rgba(112,128,144, 0.5); }
.two { background-color: rgb(30%,10%,60%, 0.2); }
.three { background-color: rgb( 0,139,139, 0.5); }

HSL


HSL 分别代表 色相(hue)、饱和度(saturation)和亮度(lightness),是一种将RGB色彩模型中的点在圆柱坐标系中的表示法


CSS语法如下:


hsl(hue, saturation, lightness)


  • 色相:色轮上的度数(从 0 到 360)- 0(或 360)是红色,120 是绿色,240 是蓝色。

  • 饱和度:一个百分比值; 0% 表示灰色阴影,而 100% 是全彩色。

  • 亮度:一个百分比; 0% 是黑色,100% 是白色。


例子:


.one { background-color: hsl(20, 100%, 50%); }
.two { background-color: hsl(130, 100%, 25%); }
.three { background-color: hsl(240, 80%, 80%); }

HSLA


HSLA 和 HSL 的关系与 RGBA 和 RGB 的关系类似,HSLA 颜色值在 HSL 颜色值上扩展 Alpha 通道 - 指定对象的不透明度。


CSS语法如下:


hsla(hue, saturation, lightness, alpha)

例子:


.one { background-color: hsla(20, 100%, 50%, 0.5); }
.two { background-color: hsla(130, 100%, 25%, 0.75); }
.three { background-color: hsla(240, 80%, 80%,0.4); }
复制代码

opacity


opacity 属性设置一个元素了透明度级别。


CSS语法如下:


opacity: value|inherit;

它与 RGBA 中的 A 在行为上有一定的区别:opacity 同时影响子元素的样式,而 RGBA 则不会。感兴趣的可以试一试。


关键字


除了 <color>s 的各种数字语法之外,CSS还定义了几组关于颜色的关键字,这些关键字都有各自的有点和用例。这里介绍一下两个特殊的关键字 transparentcurrentcolor


transparent


transparen 指定透明黑色,如果一个元素覆盖在另外一个元素之上,而你想显示下面的元素;或者你不希望某元素拥有背景色,同时又不希望用户对浏览器的颜色设置影响到您的设计。 transparent 就能派上用场了。


在CSS1中,transparent 是作为 background-color 的一个值来用的,在后续的 CSS2 和 CSS3 中, transparent 可以用在任何一个有 color 值的属性上了。


.one { 
background-color: transparent;
color: transparent;
border-color: transparent;
}

currentcolor


currentcolor 关键字可以引用元素的 color 属性值。


.one { 
color: red;
border: 1px solid currentcolor;
}

相当于


.one { 
color: red;
border: 1px solid red;
}

下面介绍的这些目前主流浏览器还没有很好的支持,但是已经列为CSS4标准了,所以了解一下也是挺好的。


HWB


hwb() 函数表示法根据颜色的色调、白度和黑度来表示给定的颜色。也可以添加 alpha 组件来表示颜色的透明度。


语法如下:


hwb[a](H W B[/ A])

例子:


hwb(180 0% 0%)
hwb(180 0% 0% / .5)
hwb(180, 0%, 0%, .5); /* 使用逗号分隔符 */

目前只有Safari支持。


Lab、Lch


lab() 函数表示法表示 CIE L * a * b * 颜色空间中的给定颜色,L* 代表亮度,取值范围是[0,100]; a* 代表从绿色到红色的分量,取值范围是[127,-128]; b* 代表从蓝色到黄色的分量 ,取值范围是[127,-128]。理论上可以展示出人类可以看到的全部颜色范围。


语法如下:


lab(L a b [/ A])

例子:


lab(29.2345% 39.3825 20.0664);
lab(52.2345% 40.1645 59.9971);

lch() 函数表示法表示CIE LCH 颜色空间中给定的颜色,采用了同 L * a * b * 一样的颜色空间,但它采用L表示明度值,C表示饱和度值,H表示色调角度值的柱形坐标。


语法如下:


lch(L C H [/ A])

例子:


lch(29.2345% 44.2 27);
lch(52.2345% 72.2 56.2);

关于常用颜色空间的概念,可以自行查询,或者点击这篇文章进行了解。


color()


color() 函数表示法允许在特定的颜色空间中指定颜色。


语法如下:


color( [ [<ident> | <dashed-ident>]? [ <number-percentage>+ | <string> ] [ / <alpha-value> ]? ] )

例子:


color(display-p3 -0.6112 1.0079 -0.2192);
color(profoto-rgb 0.4835 0.9167 0.2188);这里可以了解一下色域标准

CMYK


CMYK印刷四色模式



印刷四色模式,是彩色印刷时采用的一种套色模式,利用色料的三原色混色原理,加上黑色油墨,共计四种颜色混合叠加,形成所谓“全彩印刷”。四种标准颜色是:C:Cyan = 青色,又称为‘天蓝色’或是‘湛蓝’M:Magenta = 品红色,又称为‘洋红色’;Y:Yellow = 黄色;K:blacK=黑色。此处缩写使用最后一个字母K而非开头的B,是为了避免与Blue混淆。CMYK模式是减色模式,相对应的RGB模式是加色模式。



电脑显示屏使用 RGB 颜色值显示颜色,而打印机通常使用 CMYK 颜色值显示颜色。在CSS4标准中,计划利用 device-cmyk() 函数来实现。


语法如下:


device-cmyk() = device-cmyk( <cmyk-component>{4} [ / <alpha-value> ]? , <color>? )
<cmyk-component> = <number> | <percentage>

例子:


device-cmyk(0 81% 81% 30%);
device-cmyk(0 81% 81% 30% / .5);
作者:xmanlin
链接:https://juejin.cn/post/7031700587120951310

收起阅读 »

使用这11个代码,可以大大地简化我们的代码。

1.避免 if 过长 如果判断值满足多个条件,我们可能会这么写: if (value === 'a' || value === 'b' || value === 'c') { ... } 像这样如果有多个条件,if 条件就会很我,可读性降低,我们可以这样简化:...
继续阅读 »

1.避免 if 过长


如果判断值满足多个条件,我们可能会这么写:


if (value === 'a' || value === 'b' || value === 'c') { ... }

像这样如果有多个条件,if 条件就会很我,可读性降低,我们可以这样简化:


if (['a', 'b', 'c'].includes(value)) { ... }

2.双!操作符将任何变量转换为布尔值


!(NOT)运算符可以使用两次!!,这样可以将任何变量转换为布尔值(像布尔函数),当你需要在处理它之前检查某个值时非常方便。


const toto = null

!!toto // false
Boolean(toto) // false

if (!!toto) { } // toto is not null or undefined

3.可选项 (?)


在 JS 中,我们需要经常检查对象的某些属性是否存在,然后才能再处理它,不然会报错。 早期我们可能会这么干:


const toto = { a: { b: { c: 5 } } }

if (!!toto.a && !!toto.a.b && !!toto.a.b.c) { ... } // toto.a.b.c exist

如果对象嵌套很深,我们这写法就难以阅读,这时可以使用?来简化:



if (!!toto.a?.b?.c) { ... } // toto.a.b.c exist

// 如果键不存在,返回 `undefined`。
const test = toto.a?.b?.c?.d // undefined

4. 如果if中返回值时, 就不要在写 else


经常会看到这种写法:


if (...) {
return 'toto'
} else {
return 'tutu'
}

如果if有返回值了,可以这样写:


if (...) {
return 'toto'
}

return 'tutu'

5.避免forEach,多使用filtermapreduceeverysome


作为初学者,我们使用了很多forEach函数,但 JS 为我们提供了很多选择,而且这些函数是FP(函数式编程)。


filter


filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。


const toto = [1, 2, 3, 4]

// 过滤奇数
const evenValue = toto.filter(currentValue => {
return currentValue % 2 == 0
}) // [2, 4]

map


map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。


const toto = [1, 2, 3, 4]

const valueMultiplied = toto.map(currentValue => {
return currentValue * 2
}) // [2, 4, 6, 8]

reduce


reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。


const toto = [1, 2, 3, 4]

const sum = toto.reduce((accumulator, currentValue) => {
return accumulator += currentValue
}, 0) // 10

Some & Every


some() 方法测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值。


every() 方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。


什么时候使用?


所有项目都符合一个条件可以用 every


const toto = [ 2, 4 ]

toto.every(val => val % 2 === 0) // true

const falsyToto = [ 2, 4, 5 ]

falsyToto.every(val => val % 2 === 0) // false

只要一个符合条件就行,用some


const toto = [ 2, 4, 5 ]

toto.some(val => val % 2 !== 0) // return true

6.不要使用 delete 来删除属性


从一个对象中 delete 一个属性是非常不好的(性能不好),此外,它还会产生很多副作用。


但是如果你需要删除一个属性,你应该怎么做?


可以使用函数方式创建一个没有此属性的新对象,如下所示:


const removeProperty = (target, propertyToRemove) => {
const { [propertyToRemove]: _, ...newTarget } = target
return newTarget
}
const toto = { a: 55, b: 66 }
const totoWithoutB = removeProperty(toto, 'b') // { a: 55 }

7.仅当对象存在时才向其添加属性


有时,如果对象已经定义了属性,我们需要向对象添加属性,我们可能会这样写:


const toto = { name: 'toto' }
const other = { other: 'other' }
// The condition is not important
const condition = true

if (condition) {
other.name = toto.name
}

❌不是很好的代码


✅ 可以用一些更优雅的东西!


const condition = true

const other = {
other: 'other',
...condition && { name: 'toto' }
}

8. 使用模板字符串


在 JS 中学习字符串时,我们需要将它们与变量连接起来


const toto = 'toto'
const message = 'hello from ' + toto + '!' // hello from toto!

如果还有其它变量,我们就得写很长的表达式,这时可以使用模板字符串来优化。


const toto = 'toto'
const message = `hello from ${toto}!` // hello from toto!

9. 条件简写


当条件为 true 时,执行某些操作,我们可能会这样写:


if(condition){
toto()
}

这种方式可以用 && 简写:


condition && toto()

10.设置变量的默认值


如果需要给一个变量设置一个默认值,可以这么做:


let toto

console.log(toto) //undefined

toto = toto ?? 'default value'

console.log(toto) //default value

toto = toto ?? 'new value'

console.log(toto) //default value

11.使用 console timer


如果需要知道一个函数的执行时间,可以这么做:


for (i = 0; i < 100000; i++) {
// some code
}
console.timeEnd() // x ms




作者:前端小智
链接:https://juejin.cn/post/7031691510533849124

收起阅读 »

(转载)领投第三代计算机图形引擎,高瓴持续开发元宇宙

本报记者 谢岚第三代计算机图形引擎开发商粒界科技11月16日宣布完成千万美元级B轮融资,由高瓴创投领投。这也是短短数月里,高瓴创投投资的第5家元宇宙公司了。成立于2015年的粒界科技,由毕业于上海交大的计算机博士吴小毛创立,从事智能化渲染及数字建模技术的产品研...
继续阅读 »

本报记者 谢岚

第三代计算机图形引擎开发商粒界科技11月16日宣布完成千万美元级B轮融资,由高瓴创投领投。

这也是短短数月里,高瓴创投投资的第5家元宇宙公司了。

成立于2015年的粒界科技,由毕业于上海交大的计算机博士吴小毛创立,从事智能化渲染及数字建模技术的产品研发,致力于“让数字世界人机交互更简单”和打造第三代图形引擎GritGene。

在粒界看来,“元宇宙”即未来世界的人机交互将出现在无限多的3D场景中。如何将这些高维、多领域的数据转换成人们方便理解的可视化数据、并与之流畅地交互,急需新一代的图形引擎作为基础技术和工具来支撑。

粒界打造的第三代图形引擎正是这样一种底层技术。区别于Unreal、Unity为代表的第一、二代图形技术,第三代图形引擎的一个重要特征,是通过高性能的LBS(基于位置的服务)接口,跳脱出前两代图形引擎仅仅为游戏开发者服务的场景,为更多场景下的专业与非专业开发者创造参与数字内容创作的可能。

高瓴则认为,数字内容对于物理世界的加速渗透,正在颠覆人与现实的交互模式。未来世界将出现巨量、无限场景的“虚拟现实”和‘混合现实’,而这需要一个强有力的数字引擎作为基础。

“粒界科技自主研发的实时图形引擎正是这样一种底层技术,其不但打通了云-边-端,更将原本只服务于游戏的渲染技术在非游戏的众多场景一一验证与落地,为虚拟现实和混合现实的发展打开了更大的可想象空间。”高瓴创投合伙人李强表示。

投云、投边缘计算:元宇宙基础设施

从公开数据可以看到,几个月来高瓴在元宇宙概念下出手连连,布局覆盖从入口、云、底层技术、2B应用到Verse等。

元宇宙作为现实世界的平行空间,其入口类似黑洞,在未来可视范围大约3-5年内,可以成为元宇宙入口的首先就是AR/VR眼镜及交互设备。在这一领域,高瓴创投投资了增强现实(AR)科技公司Nreal。

再来看云。元宇宙有先天的云属性,是云端的上层建筑,因此云基础架构的重要性不言而喻。而算力、存储、网络传输是云计算的“三驾马车”。

2021年8月,高瓴创投领投了云原生数据库「DatafuseLabs」。这一领域的代表性企业Snowflake去年上市,市值高达700亿美元,而中国云原生数据仓库的发展还倚赖一个更大优势:即庞大的数据体量和数据分析需求。

「DatafuseLabs」创始团队成员来自阿里云、Google、青云等国内外知名云计算公司,在云原生数据库领域有着丰富的工程经验,同时也是数据库开源社区活跃贡献者。

在高瓴看来,Datafuse作为一家云中立开源海量数据分析平台,以对云资源的精细调度为基础,打造了一款在数据处理量、分析速度和易用性上兼备的领先产品。“技术的发展已逐渐演化成一个超链接环境:无数用户跨多种设备地使用着海量应用,这令传统数据库面临巨大挑战,也让我们确信基于云原生的数据服务必是未来。”

除了云原生,元宇宙对算力提出了极高的要求,当前的算力架构已无法满足元宇宙对于低门槛高体验的需求,而边缘计算一定程度上能够推动算力发展,为元宇宙发展扫清障碍。高瓴创投连续投资两轮的分布式边缘云计算公司秒如科技,就是聚焦于分布式边缘云InfraSoftware底层基础设施软件研发,致力于将复杂的边缘IT基础设施简单化、自动化、智能化地提供给全球客户。

发力应用层布局:物理引擎、动作引擎到

在偏向于底层技术的toB应用层,除了图形引擎粒界科技,今年8月,高瓴创投还投资了物理引擎公司Motphys(谋先飞)和元象唯思。

元象唯思瞄准全真互联网,专注于将人工智能、云渲染、视频编解码与大系统工程等前沿技术,引入数字世界生成的过程中,致力于使互联网全面地融入并与现实结合,实现全面地“脱虚向实”。谋先飞则自主研发的物理动作引擎Motphys,拥有将角色动作展示与融合、并模拟真实物理效果等功能。该引擎省去了开发者敲击大量代码的工作,开发团队只需设置好参数,就可通过动作物理引擎将这些效果模拟出来。

既然Metaverse被看作平行宇宙,那么Verse即为最远之境。高瓴迄今为止在元宇宙概念中最重要的一笔投资小冰(原微软小冰)正接近于这一概念。小冰公司以“少女小冰”的形象为人们熟知,这与最近元宇宙中大热的虚拟人概念很接近,但其实公司的核心目标完成“全球交互量最大的人工智能框架”。

按照扎克伯格的说法,元宇宙将是互联网的升级版:一个人们可以“置身其中”而不仅仅是“观看使用”的互联网。从这个意义上说,也可以说元宇宙是下一代互联网,甚至是终极互联网。面对新一代“互联网”的崛起,不同的投资机构和科技巨头也做出了不同的选择。譬如最近正在猛投元宇宙的字节跳动,围绕的就是VR设备、沉浸式社交平台和游戏连连出手。

而高瓴在成立高瓴创投后,硬科技明显成了其主攻方向。据悉在其内部,项目的“含tech量”是决定投资的重要依据。这也不难理解其在元宇宙板块、以底层技术革新作为选择了。

(编辑 张明富)


原文链接:https://baijiahao.baidu.com/s?id=1716654323678083060&wfr=spider&for=pc

收起阅读 »

(转载)入局·元宇宙 | 清华大学沈阳:元宇宙不是游戏,其发展需获得全社会认同

近段时间,元宇宙概念炙手可热。嗅觉最为灵敏的资本市场早已闻风而动,各路产业纷纷将自己嵌套进元宇宙的蓝图,甚至有机构卖“元宇宙网课”已收入百万。那么,元宇宙和我们的生活有什么关系?又会给现实世界带来哪些影响?11月17日,清华大学新媒体研究中心执行主任沈阳在接受...
继续阅读 »

近段时间,元宇宙概念炙手可热。嗅觉最为灵敏的资本市场早已闻风而动,各路产业纷纷将自己嵌套进元宇宙的蓝图,甚至有机构卖“元宇宙网课”已收入百万。那么,元宇宙和我们的生活有什么关系?又会给现实世界带来哪些影响?

11月17日,清华大学新媒体研究中心执行主任沈阳在接受封面新闻专访时表示,元宇宙是移动互联网之后,互联网的下一种形态,将突破当下互联网产业平台形态内卷化的瓶颈。不过目前,元宇宙的产业生态系统还处于亚健康状态,要通过技术创新引领和制度创新推动其健康发展。

截至目前,市场上似乎并未给出元宇宙的准确定义,在沈阳看来,这仍是一个不断发展、演变的概念。他认为,元宇宙是整合多种新技术而产生的新型虚实相融的互联网应用和社会形态, 并且允许每个用户进行内容生产和世界编辑。“总体而言,就是对人生存维度和感官维度的拓展。”

很多影视作品都对元宇宙有过想象。比如在电影《头号玩家》、《黑客帝国》里,呈现了一个戴上眼镜,连上脑机就能脱离现实的数字游戏世界。那元宇宙是更高级的游戏吗?

对此,沈阳给出了否定的答复,他提到,元宇宙可以被理解为在现有大型多人在线游戏的基础上,加上了开放式任务、可编辑世界、XR入口、去中心化认证系统等融合形成的,是虚拟世界和现实世界的交融。

谈及元宇宙会对人类社会带来哪些颠覆性改变,沈阳认为,元宇宙将在物理层面有选择地解放人类天性。比如,超越现实世界中的时空规定,让人跳得更高、跑得更快;同时,元宇宙所具有的庞大地理空间供用户选择和探索。

就像是《头号玩家》中的“绿洲”,看似美好的背后其实也存在利益斗争、不平等和安全危机。“元宇宙也具有新兴产业不成熟、不稳定的特征。”沈阳表示,元宇宙要进一步发展,需面对算力压力、伦理舆论、隐私保护、资本操控以及知识产权等多方面潜在风险。“就像是一个人在VR头盔里面待得越久,可能越发习惯一个人的状态,元宇宙想要健康发展,用户们还需要经历一道心理建设的关卡,从认知走向认同。”


收起阅读 »

Android自定义view,实现电子签名

首先new一个类继承于Viewpublic class SignatureView extends View 自定义view,采用画笔绘制一张图片 定义一个画笔滑动的宽度 还需要对画笔进行跟踪,以便内容区域可以容纳笔划private static final...
继续阅读 »

首先new一个类继承于View

public class SignatureView extends View

自定义view,采用画笔绘制一张图片 定义一个画笔滑动的宽度 还需要对画笔进行跟踪,以便内容区域可以容纳笔划

private static final float STROKE_WIDTH = 5f;
private static final float HALF_STROKE_WIDTH = STROKE_WIDTH / 2;

通过使最小可能区域无效来优化绘制

private Paint paint = new Paint();
private Path path = new Path();

private float lastTouchX;
private float lastTouchY;
private final RectF dirtyRect = new RectF();

构造方法里面初始化画笔

public SignatureView(Context context, AttributeSet attrs) {
super(context, attrs);

paint.setAntiAlias(true);
paint.setColor(Color.BLACK);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeWidth(STROKE_WIDTH);
}

处理手势,触发画笔路径

@Override
public boolean onTouchEvent(MotionEvent event) {
float eventX = event.getX();
float eventY = event.getY();

switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
path.moveTo(eventX, eventY);
lastTouchX = eventX;
lastTouchY = eventY;
// 现在还没有终点,所以不要浪费周期使其失效。
return true;

case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
//开始跟画笔区域。
resetDirtyRect(eventX, eventY);

// 当硬件跟踪事件的速度快于事件的交付速度时
// 事件将包含这些跳过点的历史记录。
int historySize = event.getHistorySize();
for (int i = 0; i < historySize; i++) {
float historicalX = event.getHistoricalX(i);
float historicalY = event.getHistoricalY(i);
expandDirtyRect(historicalX, historicalY);
path.lineTo(historicalX, historicalY);
}

// 回放历史记录后,将线路连接到触点。
path.lineTo(eventX, eventY);
break;

default:

return false;
}

// 包括一半笔划宽度以避免剪裁
invalidate(
(int) (dirtyRect.left - HALF_STROKE_WIDTH),
(int) (dirtyRect.top - HALF_STROKE_WIDTH),
(int) (dirtyRect.right + HALF_STROKE_WIDTH),
(int) (dirtyRect.bottom + HALF_STROKE_WIDTH));

lastTouchX = eventX;
lastTouchY = eventY;

return true;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawPath(path, paint);
}

抬起时调用,以确包含所有路径

private void expandDirtyRect(float historicalX, float historicalY) {
if (historicalX < dirtyRect.left) {
dirtyRect.left = historicalX;
} else if (historicalX > dirtyRect.right) {
dirtyRect.right = historicalX;
}
if (historicalY < dirtyRect.top) {
dirtyRect.top = historicalY;
} else if (historicalY > dirtyRect.bottom) {
dirtyRect.bottom = historicalY;
}
}

到这里,手写功能就已经能使用了,接下来是做一些处理

在运动事件发生时重置
lastTouchX和lastTouchY是在动作结束时设置的

private void resetDirtyRect(float eventX, float eventY) {
dirtyRect.left = Math.min(lastTouchX, eventX);
dirtyRect.right = Math.max(lastTouchX, eventX);
dirtyRect.top = Math.min(lastTouchY, eventY);
dirtyRect.bottom = Math.max(lastTouchY, eventY);
}

清除签名

那写错了怎么办,肯定要有清除签名啦

public void clear() {
path.reset();
// 重新绘制整个视图
invalidate();
}

获取图片缓存

public Bitmap getBitmapFromView(){
this.setDrawingCacheEnabled(true); //开启图片缓存
buildDrawingCache(); //构建图片缓存
Bitmap bitmap = Bitmap.createBitmap(getDrawingCache());
setDrawingCacheEnabled(false); //关闭图片缓存
return bitmap;
}

效果图

微信图片_20211115231808.jpg


收起阅读 »

(转载)漫画:什么是“元宇宙”?

什么是更高的自由度呢?或许有人觉得,我们在网络游戏当中,不是也很自由吗?想怎么玩就怎么玩。但是,无论一款网络游戏的元素有多么丰富,游戏当中的角色、任务、职业、道具、场景,都是游戏设计师预先设计好的。就拿大型网络游戏《魔兽世界》来举例,你在里面可以当一名法师,也...
继续阅读 »


什么是更高的自由度呢?或许有人觉得,我们在网络游戏当中,不是也很自由吗?想怎么玩就怎么玩。

但是,无论一款网络游戏的元素有多么丰富,游戏当中的角色、任务、职业、道具、场景,都是游戏设计师预先设计好的。

就拿大型网络游戏《魔兽世界》来举例,你在里面可以当一名法师,也可以当一名猎人,但你要是想当一名“星际战士”,那对不起,游戏里没设计这个职业。

同样的,尽管《魔兽世界》里有着各式各样的武器装备,但你想要锻造出一把“倚天剑”,那也不能,暴雪设计师没听说过这东西。

但是,在元宇宙当中,整个虚拟世界是大家共建的。每一个用户不但可以参与其中,更可以改造世界,创造出全新的元素和规则。

《我的世界》当中,玩家们的作品

如果说,在网络游戏的世界中,游戏设计师是创世的神灵。那么在元宇宙中,不再有一个权威的“神”,所有的用户都是共同参与和改造世界的“人”。

对于这样的模式,业界有一个专门的术语:UGC(User Generated Content),翻译过来就是“用户生产内容”。

网络游戏当中,也有自己的经济系统,有虚拟货币,有各种物品的交易。那么元宇宙当中的经济系统,又有什么特别之处呢?

稀缺性,是经济学的基石。在现实世界里,无论是一个馒头,还是一栋别墅,数量都是有限的。要想做出更多的馒头,建造更多的别墅,就需要更多的材料、能源、人力成本。

但是,在网络游戏当中,一切物品装备都是数字。在理论上,游戏服务商可以把一件物品无限复制,而不需要任何额外的成本。因此,在网络游戏当中,经济系统并非独立的。

但是,在元宇宙当中,每一件物品都是独一无二,不可被大量复制的。并且,这些物品在一切交易过程,都有安全的数字合约保障,使得元宇宙当中的数字资产就像现实里人们的资产一样,稀缺且稳定存在。

稀缺与稳定,是元宇宙独立经济系统的基石。

数字资产“无聊猿猴”系列

元宇宙的实现,都涉及到了哪些技术呢?

首先,是众多互联网产品和游戏当中共通的技术,比如云计算5G通信VR虚拟现实技术等等。这些技术的细节,我们就不过多阐述了。

元宇宙能够实现去中心化,底层依赖的则是区块链技术这十年来的发展进步。在区块链当中,没有一个中心服务器,每一个用户都是一个独立且平等的节点,可以自由提交自己的内容,在达成共识的前提下更新整个区块链。这也是元宇宙的用户们可以改造虚拟世界的基础。

此外,最近流行的NFT,为元宇宙的独立经济系统提供了保障。

NFT,全称Non-Fungible Token,翻译过来叫做非同质化代币。NFT同样是基于区块链实现,与之前的比特币同样属于数字代币。但不同的是,每一个比特币都是同质化的,且可以分割,比如你可以拥有0.01个比特币;而每一个NFT代币都是独一无二且不可分割的,就好比是一幅蒙娜丽莎的原画,或者你家的房子。

在元宇宙当中,每一件物品的生产和交易,都是基于NFT实现,保障了虚拟世界中一切数字物品的稀缺与稳定。

Roblox,中文音译为罗布乐思,是世界最大的多人在线创作游戏。在这个游戏平台上,玩家们既可以参与游戏本身,也可以开发创造自己的游戏。

玩家们创造出的游戏种类,包括射击、格斗、竞速、生存等丰富的类型,充分发挥了所有人的想象力。

2021年3月,Roblox在美股上市,被称为元宇宙第一股,2021年也被称为元宇宙元年。

对于元宇宙这片蓝海,各方大佬也纷纷做出了积极的反应。其中Facebook创始人扎克伯格就是元宇宙的热衷支持者,甚至把公司品牌都更名为“Meta”。

至于国内,腾讯公司投资了炙手可热的项目Roblox,而字节跳动也投资了号称“中国版Roblox”的游戏开发商代码乾坤。

如今,元宇宙成为了多家互联网巨头进行角逐的全新战场。

原文链接:https://juejin.cn/post/7029142002109267976

收起阅读 »

【白话前端】从一个故事说明白“浏览器缓存”

一则小故事 小明常去图书馆借阅英文杂志回家看,由于单词量少,他同时需要借阅一本《英汉词典》; 起初,和图书管理员不熟,每次他都要在图书馆借英文杂志和《英汉词典》,放在书包里背回家;这个过程,暂且将其称为“不缓存”; 后来,小明发现图书管理员竟是妈妈的...
继续阅读 »

一则小故事



小明常去图书馆借阅英文杂志回家看,由于单词量少,他同时需要借阅一本《英汉词典》;




起初,和图书管理员不熟,每次他都要在图书馆借英文杂志和《英汉词典》,放在书包里背回家;这个过程,暂且将其称为“不缓存”;




后来,小明发现图书管理员竟是妈妈的好朋友和好邻居王叔叔,经过相认后,王叔叔对小明说:“你每次都要借阅《英汉词典》,我直接借你一整年,在一年内你可以将它放在家里,不需要每次到图书馆来借阅。”小明听了非常高兴,因为他的书包可以轻上一大截;可以持有《英汉词典》一整年的过程,暂且称为“强缓存”;




再后来,小明发现图书管理员王叔叔经常去家里做客,两人关系也愈发亲密;小明问:“王叔叔,英文杂志的更新总是很不规律,我经常去了图书馆,英文杂志却未更新,我借到的依然是上一期的杂志,有啥办法让我少跑路吗?”




王叔叔笑着说:“这还不简单?每次你准备去借阅之前,先把你手里当前持有的杂志期号(etag)用短信发给我,如果图书馆没有更新,我就给你一个304的暗号,你就还是接着读家里那本;如果有了更新,我给你一个200的暗号,你再来图书馆拿书就行;”这个过程,暂且被称为“协商缓存”



逐渐装逼


不缓存


不缓存是最容易理解的缓存策略,也最不容易出错,只要每一次刷新页面都去服务器取数据即可;但同样的,不缓存意味着页面加载速度会更慢


要设置不缓存也很容易,只需要将资源文件的Response Header中的Cache-Control设为no-store即可;



Cache-Control: no-store



cache-control 属性之一:可缓存性



强缓存


对于已知的几乎不会发生变化的资源,可以通过调整策略,使得浏览器在限定时间内,直接从本地缓存获取,这就是所谓的强缓存;

要配置静态资源的强缓存,通常需要发送的缓存头如下:



Cache-Control:public, max-age=31536000



以下是强缓存常用的两种属性↓;


cache-control 属性之:可缓存性



cache-control 属性之: 到期



协商缓存


其实上面故事里关于协商缓存的描述,有一点是非常不准确的,那就是对于浏览器而言,小明发送给王叔叔的不是所谓的“杂志期号”,而是杂志的散列(hash);而这个hash,自然也是王叔叔(服务器端)告诉小明(客户端)的;


在真实情况下,浏览器的协商缓存要触发,只有两种情况:



1.Cache-Control 的值为 no-cache (不强缓存)



or



2.max-age 过期了 (强缓存,但总有过期的时候)



只有在这两种情况下满足其中至少一种时,才会进入协商缓存的过程;


因此,常规的协商缓存,通常分为以下几步:



step1

浏览器第一次发起请求,request上并没有相应请求头;

(小明第一次去图书馆借书)




step2

服务器第一次返回资源,response上带上了两个属性:
etag: "33a64df"

last-modified: Mon, 12 Dec 2020 12:12:12 GMT

(王叔叔借给小明一本书,并告诉小明这本杂志的编号,以及它的发刊日期)




step3

浏览器第二次发起请求,request上携带了上一次请求返回的内容:

if-none-matched: "33a64df"

if-modified-since: Mon, 12 Dec 2020 12:12:12 GMT

(小明第二次借书,先进行了询问:上一次借我的那本的编号和上一次更改后是否有变动?)




step4

服务器发现资源没有改变,于是返回了304状态码;

浏览器直接在本地读取缓存;

(王叔叔说:还没来新货,你先读着上次借的那本吧)




step5

浏览器第三次发起请求,request上携带了上一次请求返回的内容:

if-none-matched: "33a64df"

if-modified-since: Mon, 12 Dec 2020 12:12:12 GMT

(小明第三次借书,先进行了询问:上一次借我的那本的编号和上一次更改后是否有变动?)




step6

服务器检查之后发现,文件已经发生了变化,于是将新的资源、编号、最后变更时间一起返回给了客户端;并返回了200状态码;
if-none-matched: "sd423dss"

if-modified-since: Mon, 30 Dec 2020 12:12:12 GMT

(王叔叔说:来了来了,最新一期的杂志编号、发刊日期如下,这是杂志本身,也一起给你;)



上面过程展示了一次协商缓存生效的过程;


如何在项目中使用?


正常来说,一个前端单页应用(SPA)的项目结构大概如下:


├─favicon.ico
├─index.html

├─css
│ └───app.fb0c6e1c.css

├─img
│ └───logo.82b9c7a5.png

└─js
├───app.febf7357.js
└───chunk-vendors.5a5a5781.js

从命名上可以发现,文件大概分两类:



  1. index.html & favicon.ico 都属于固定命名,通常情况下名称不会再发生改变;

  2. css/js/image/ttf 等文件,则通常会以 {name}.{hash}.{suffix}的方式进行命名;


name-with-hash.png


当文件发生变化时,其命名规则,可天然保证文件hash跟着发生变化,从而保证文件的路径发生变化;


因此,针对以上场景,通常情况下可以按以下方式制定缓存策略



  1. index.html 和 favicon.ico 设置为“不缓存”或者“协商缓存”(必要不大);

  2. 名称中带hash的文件(如css/js/image/ttf),可以直接使用“强缓存”策略

作者:春哥的梦想是摸鱼
链接:https://juejin.cn/post/7030781324650610695

收起阅读 »

2021 年你需要知道的 CSS 工程化技术

目前整个 CSS 工具链、工程化领域的主要方案如下: 而我们技术选型的标准如下: 开发速度快 开发体验友好 调试体验友好 可维护性友好 扩展性友好 可协作性友好 体积小 有最佳实践指导 目前主要需要对比的三套方案: Less/Sass + PostCS...
继续阅读 »

目前整个 CSS 工具链、工程化领域的主要方案如下:


image.png


而我们技术选型的标准如下:



  • 开发速度快

  • 开发体验友好

  • 调试体验友好

  • 可维护性友好

  • 扩展性友好

  • 可协作性友好

  • 体积小

  • 有最佳实践指导


目前主要需要对比的三套方案:



  • Less/Sass + PostCSS 的纯 CSS c侧方案

  • styled-components / emotion 的纯 CSS-in-JS 侧方案

  • TailwindCSS 的以写辅助类为主的 HTML 侧方案


纯 CSS 侧方案


介绍与优点




维护状态:一般




Star 数:16.7K




支持框架:无框架限制




项目地址:github.com/less/less.j…



Less/Sass + PostCSS 这种方案在目前主流的组件库和企业级项目中使用很广,如 ant-design 等


它们的主要作用如下:



  • 为 CSS 添加了类似 JS 的特性,你也可以使用变量、mixin,写判断等

  • 引入了模块化的概念,可以在一个 less 文件中导入另外一个 less 文件进行使用

  • 兼容标准,可以快速使用 CSS 新特性,兼容浏览器 CSS 差异等


这类工具能够与主流的工程化工具一起使用,如 Webpack,提供对应的 loader 如 sass-loader,然后就可以在 React/Vue 项目中建 .scss 文件,写 sass 语法,并导入到 React 组件中生效。


比如我写一个组件在响应式各个断点下的展示情况的 sass 代码:


.component {

width: 300px;

@media (min-width: 768px) {

width: 600px;

@media (min-resolution: 192dpi) {

background-image: url(/img/retina2x.png);

}

}

@media (min-width: 1280px) {

width: 800px;

}

}

或导入一些用于标准化浏览器差异的代码:


@import "normalize.css"; 



// component 相关的其他代码

不足


这类方案的一个主要问题就是,只是对 CSS 本身进行了增强,但是在帮助开发者如何写更好的 CSS、更高效、可维护的 CSS 方面并没有提供任何建议。



  • 你依然需要自己定义 CSS 类、id,并且思考如何去用这些类、id 进行组合去描述 HTML 的样式

  • 你依然可能会写很多冗余的 Less/Sass 代码,然后造成项目的负担,在可维护性方面也有巨大问题


优化



  • 可以引入 CSS 设计规范:BEM 规范,来辅助用户在整个网页的 HTML 骨架以及对应的类上进行设计

  • 可以引入 CSS Modules,将 CSS 文件进行 “作用域” 限制,确保在之后维护时,修改一个内容不会引起全局中其他样式的效果


BEM 规范


B (Block)、E(Element)、M(Modifier),具体就是通过块、元素、行为来定义所有的可视化功能。


拿设计一个 Button 为例:


/* Block */

.btn {}



/* 依赖于 Block 的 Element */

.btn__price {}



/* 修改 Block 风格的 Modifier */

.btn--orange {}

.btn--big {}

遵循上述规范的一个真实的 Button:


<a href="#">

<span>$3</span>

<span>BIG BUTTON</span>

</a>

可以获得如下的效果:



CSS Modules


CSS Modules 主要为 CSS 添加局部作用域和模块依赖,使得 CSS 也能具有组件化。


一个例子如下:


import React from 'react';

import style from './App.css';



export default () => {

return (

<h1 className={style.title}>

Hello World

</h1>

);

};

.title {

composes: className;

color: red;

}

上述经过编译会变成如下 hash 字符串:


<h1>

Hello World

</h1>

._3zyde4l1yATCOkgn-DBWEL {

color: red;

}

CSS Modules 可以与普通 CSS、Less、Sass 等结合使用。


纯 JS 侧方案


介绍与优点




维护状态:一般




Star 数:35.2K




支持框架:React ,通过社区支持 Vue 等框架




项目地址:github.com/styled-comp…



使用 JS 的模板字符串函数,在 JS 里面写 CSS 代码,这带来了两个认知的改变:



  • 不是在根据 HTML,然后去写 CSS,而是站在组件设计的角度,为组件写 CSS,然后应用组件的组合思想搭建大应用

  • 自动提供类似 CSS Modules 的体验,不用担心样式的全局污染问题


同时带来了很多 JS 侧才有的各种功能特性,可以让开发者用开发 JS 的方式开发 CSS,如编辑器自动补全、Lint、编译压缩等。


比如我写一个按钮:


const Button = styled.button`

/* Adapt the colors based on primary prop */

background: ${props => props.primary ? "palevioletred" : "white"};

color: ${props => props.primary ? "white" : "palevioletred"};



font-size: 1em;

margin: 1em;

padding: 0.25em 1em;

border: 2px solid palevioletred;

border-radius: 3px;

`;



render(

<div>

<Button>Normal</Button>

<Button primary>Primary</Button>

</div>

);

可以获得如下效果:



还可以扩展样式:


// The Button from the last section without the interpolations

const Button = styled.button`

color: palevioletred;

font-size: 1em;

margin: 1em;

padding: 0.25em 1em;

border: 2px solid palevioletred;

border-radius: 3px;

`;



// A new component based on Button, but with some override styles

const TomatoButton = styled(Button)`

color: tomato;

border-color: tomato;

`;



render(

<div>

<Button>Normal Button</Button>

<TomatoButton>Tomato Button</TomatoButton>

</div>

);

可以获得如下效果:



不足


虽然这类方案提供了在 JS 中写 CSS,充分利用 JS 的插值、组合等特性,然后应用 React 组件等组合思想,将组件与 CSS 进行细粒度绑定,让 CSS 跟随着组件一同进行组件化开发,同时提供和组件类似的模块化特性,相比 Less/Sass 这一套,可以复用 JS 社区的最佳实践等。


但是它仍然有一些不足:



  • 仍然是是对 CSS 增强,提供非常大的灵活性,开发者仍然需要考虑如何去组织自己的 CSS

  • 没有给出一套 “有观点” 的最佳实践做法

  • 在上层也缺乏基于 styled-components 进行复用的物料库可进行参考设计和使用,导致在初始化使用时开发速度较低

  • 在 JS 中写 CSS,势必带来一些本属于 JS 的限制,如 TS 下,需要对 Styled 的组件进行类型注释

  • 官方维护的内容只兼容 React 框架,Vue 和其他框架都由社区提供支持


整体来说不太符合团队协作使用,需要人为总结最佳实践和规范等。


优化



  • 寻求一套写 CSS 的最佳实践和团队协作规范

  • 能够拥有大量的物料库或辅助类等,提高开发效率,快速完成应用开发


偏向 HTML 侧方案


介绍与优点




维护状态:积极




Star 数:48.9K




支持框架:React、Vue、Svelte 等主流框架




项目地址:github.com/tailwindlab…



典型的是 TailwindCSS,一个辅助类优先的 CSS 框架,提供如 flexpt-4text-centerrotate-90 这样实用的类名,然后基于这些底层的辅助类向上组合构建任何网站,而且只需要专注于为 HTML 设置类名即可。


一个比较形象的例子可以参考如下代码:


<button>Decline</button>

<button>Accept</button>

上述代码应用 BEM 风格的类名设计,然后设计两个按钮,而这两个类名类似主流组件库里面的 Button 的不同状态的设计,而这两个类又是由更加基础的 TailwindCSS 辅助类组成:


.btn {

@apply text-base font-medium rounded-lg p-3;

}



.btn--primary {

@apply bg-rose-500 text-white;

}



.btn--secondary {

@apply bg-gray-100 text-black;

}

上面的辅助类包含以下几类:



  • 设置文本相关: text-basefont-mediumtext-whitetext-black

  • 设置背景相关的:bg-rose-500bg-gray-100

  • 设置间距相关的:p-3

  • 设置边角相关的:rounded-lg


通过 Tailwind 提供的 @apply 方法来对这些辅助类进行组合构建更上层的样式类。


上述的最终效果展示如下:



可以看到 TailwindCSS 将我们开发网站的过程抽象成为使用 Figma 等设计软件设计界面的过程,同时提供了一套用于设计的规范,相当于内置最佳实践,如颜色、阴影、字体相关的内容,一个很形象的图片可以说明这一点:



TailwindCSS 为我们规划了一个元素可以设置的属性,并且为每个属性给定了一组可以设置的值,这些属性+属性值组合成一个有机的设计系统,非常便于团队协作与共识,让我们开发网站就像做设计一样简单、快速,但是整体风格又能保持一致。


TailwindCSS 同时也能与主流组件库如 React、Vue、Svelte 结合,融入基于组件的 CSS 设计思想,但又只需要修改 HTML 上的类名,如我们设计一个食谱组件:


// Recipes.js

import Nav from './Nav.js'

import NavItem from './NavItem.js'

import List from './List.js'

import ListItem from './ListItem.js'



export default function Recipes({ recipes }) {

return (

<div className="divide-y divide-gray-100">

<Nav>

<NavItem href="/featured" isActive>Featured</NavItem>

<NavItem href="/popular">Popular</NavItem>

<NavItem href="/recent">Recent</NavItem>

</Nav>

<List>

{recipes.map((recipe) => (

<ListItem key={recipe.id} recipe={recipe} />

))}

</List>

</div>

)

}



// Nav.js

export default function Nav({ children }) {

return (

<nav className="p-4">

<ul className="flex space-x-2">

{children}

</ul>

</nav>

)

}



// NavItem.js

export default function NavItem({ href, isActive, children }) {

return (

<li>

<a

href={href}

className={`block px-4 py-2 rounded-md ${isActive ? 'bg-amber-100 text-amber-700' : ''}`}

>

{children}

</a>

</li>

)

}



// List.js

export default function List({ children }) {

return (

<ul className="divide-y divide-gray-100">

{children}

</ul>

)

}



//ListItem.js

export default function ListItem({ recipe }) {

return (

<article className="p-4 flex space-x-4">

<img src={recipe.image} alt="" className="flex-none w-18 h-18 rounded-lg object-cover bg-gray-100" width="144" height="144" />

<div className="min-w-0 relative flex-auto sm:pr-20 lg:pr-0 xl:pr-20">

<h2 className="text-lg font-semibold text-black mb-0.5">

{recipe.title}

</h2>

<dl className="flex flex-wrap text-sm font-medium whitespace-pre">

<div>

<dt className="sr-only">Time</dt>

<dd>

<abbr title={`${recipe.time} minutes`}>{recipe.time}m</abbr>

</dd>

</div>

<div>

<dt className="sr-only">Difficulty</dt>

<dd> · {recipe.difficulty}</dd>

</div>

<div>

<dt className="sr-only">Servings</dt>

<dd> · {recipe.servings} servings</dd>

</div>

<div className="flex-none w-full mt-0.5 font-normal">

<dt className="inline">By</dt>{' '}

<dd className="inline text-black">{recipe.author}</dd>

</div>

<div>

<dt className="text-amber-500">

<span className="sr-only">Rating</span>

<svg width="16" height="20" fill="currentColor">

<path d="M7.05 3.691c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.372 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.539 1.118l-2.8-2.034a1 1 0 00-1.176 0l-2.8 2.034c-.783.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.363-1.118L.98 9.483c-.784-.57-.381-1.81.587-1.81H5.03a1 1 0 00.95-.69L7.05 3.69z" />

</svg>

</dt>

<dd>{recipe.rating}</dd>

</div>

</dl>

</div>

</article>

)

}

上述食谱的效果如下:



可以看到我们无需写一行 CSS,而是在 HTML 里面应用各种辅助类,结合 React 的组件化设计,既可以轻松完成一个非常现代化且好看的食谱组件。


除了上面的特性,TailwindCSS 在响应式、新特性支持、Dark Mode、自定义配置、自定义新的辅助类、IDE 方面也提供非常优秀的支持,除此之外还有基于 TailwindCSS 构建的物料库 Tailwind UI ,提供各种各样成熟、好看、可用于生产的物料库:



因为需要自定的 CSS 不多,而需要自定义的 CSS 可以定义为可复用的辅助类,所以在可维护性方面也是极好的。


不足



  • 因为要引入一个额外的运行时,TailwindCSS 辅助类到 CSS 的编译过程,而随着组件越来越多,需要编译的工作量也会变大,所以速度会有影响

  • 过于底层,相当于给了用于设计的最基础的指标,但是如果我们想要快速设计网站,那么可能还需要一致的、更加上层的组件库

  • 相当于引入了一套框架,具有一定的学习成本和使用成本


优化



  • Tailwind 2.0 支持 JIT,可以大大提升编译速度,可以考虑引入

  • 基于 TailwindCSS,设计一套符合自身风格的上层组件库、物料库,便于更加快速开发

  • 提前探索、学习和总结一套教程与开发最佳实践

  • 探索 styled-components 等结合 TailwindCSS 的开发方式



作者:程序员巴士
链接:https://juejin.cn/post/7030790310590447630

收起阅读 »

如何在TS里使用命名空间,来组织你的代码

前言 关于命名空间,官方有个说明,大概是这么个意思: 为了与ECMAScript 2015里的术语保持一致,从TypeScript 1.5开始,“外部模块”称为“模块”,而“内部模块”称做“命名空间”。 为了避免新的使用者被相似的名称所迷惑,建议: 任何使用...
继续阅读 »

前言


关于命名空间,官方有个说明,大概是这么个意思:


为了与ECMAScript 2015里的术语保持一致,从TypeScript 1.5开始,“外部模块”称为“模块”,而“内部模块”称做“命名空间”。


为了避免新的使用者被相似的名称所迷惑,建议:



任何使用 module关键字来声明一个内部模块的地方都应该使用namespace关键字来替换



具体的使用下面会讲到


使用命名空间


使用命名空间的方式,其实非常简单,格式如下:


namespace X {}

具体的使用可以看看下面这个例子(例子来源TS官方文档)


我们定义几个简单的字符串验证器,假设会使用它们来验证表单里的用户输入或验证外部数据


interface StringValidator {
isAcceptable(s: string): boolean;
}

let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;

class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}

class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
let isMatch = validators[name].isAcceptable(s);
console.log(`'${ s }' ${ isMatch ? "matches" : "does not match" } '${ name }'.`);
}
}

现在我们是把所有的验证器都放在一个文件里


但是,随着更多验证器的加入,我们可能会担心与其它对象产生命名冲突。因此我们使用命名空间来组织我们的代码


如下使用命名空间:


namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}

const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;

export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}

export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
}
}

如上代码,把所有与验证器相关的类型都放到一个叫做Validation的命名空间里。 因为我们想让这些接口和类在命名空间之外也是可访问的,所以需要使用 export。 相反的,变量 lettersRegexpnumberRegexp是实现的细节,不需要导出,因此它们在命名空间外是不能访问的


有个问题是,如果只是一个文件,当应用变得越来越大的时候,会变得难以维护,因此我们根据需要,可选的将单文件分离到不同的文件中


下节我们会继续讲到这个问题,关于多文件的命名空间,并且我们会将上例中的单文件分割成多个文件。欢迎关注


END


以上就是本文的所有内容,如有问题,欢迎指正~


作者:LBJ
链接:https://juejin.cn/post/7031021973966684191

收起阅读 »

5 个让 Swift 更优雅的扩展——Pt.1

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战引言作为开发者,应该编写具有高可维护性和可扩展性的代码。我们可以通过扩展原有的功能,写出更易读,更简洁的代码。下面就介绍 5 个日常开发中非常实用的扩展。1. 自定义下标来安全访问数组我想...
继续阅读 »

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战


引言

作为开发者,应该编写具有高可维护性和可扩展性的代码。我们可以通过扩展原有的功能,写出更易读,更简洁的代码。

下面就介绍 5 个日常开发中非常实用的扩展。

1. 自定义下标来安全访问数组

我想每个开发人员都至少经历过一次index-out-of-bounds的报错。就是数组越界,这个大家都懂,就不过多介绍了。下面是个数组越界的例子:

let values = ["A", "B", "C"]
values[0] // A
values[1] // B
values[2] // C
values[3] // Fatal error: Index out of range

既然是下标超过了数组的大小,那我们在取值之前,先检查下标是否超过数组大小。让我们来看下面的几种方案:

  • 通过 if 来判断下标
if 2 < values.count {
values[2] // "C"
}
if 3 < values.count {
values[3] // 不会走到这里
}

虽然也可以,但显的就很重复繁琐,每次取值之前都要判断一遍下标。

  • 定义公共函数

既然每次都要检查下标,那就把检查下标的逻辑放在一个函数里

func getValue<T>(in elements: [T], at index: Int) -> T? {
guard index >= 0 && index < elements.count else {
return nil
}
return elements[index]
}

let values = ["A", "B", "C"]
getValue(in: values, at: 2) // "C"
getValue(in: values, at: 3) // nil

不仅使用泛型支持了任何类型的元素,当数组越界时,还很贴心的返回了 nil,防止崩溃。

虽然很贴心,但每次取值都要把原数组传进去,显的就很冗余。

  • extension

既然每次都要传入数组很冗余,那就把数组的参数给去掉。我们知道 Swift 一个很强大的特性就是 extension,我们给 Array定义个 extension,并把这个函数添加进去。

extension Array {
func getValue(at index: Int) -> Element? {
guard index >= 0 && index < self.count else {
return nil
}
return self[index]
}
}

let values = ["A", "B", "C"]
values.getValue(at: 2) // "C"
values.getValue(at: 3) // nil

  • subscript

虽然看起来好很多了,但可不可以像原生的取值一样, 一个[]就搞定了呢?of course!

extension Array {
subscript (safe index: Int) -> Element? {
guard index >= 0 && index < self.count else {
return nil
}
return self[index]
}
}

values[safe: 2] // "C"
values[safe: 3] // nil

自定义的[safe: 2]和原生的 [2]非常的接近了。但自定义的提供了数据越界保护机制。

  • 应用到 Collection

既然这么棒,岂能数组一人独享,我们把它应用到所有 Collection 协议。看起来是不是很优雅~😉

extension Collection {
public subscript (safe index: Self.Index) -> Iterator.Element? {
(startIndex ..< endIndex).contains(index) ? self[index] : nil
}
}


2. 平等的处理 nil 和空字符串

在处理可选值时,我们通常需要将它们与 nil 进行比较进行空检查。当为 nil 时,我们会提供一个默认值让程序继续执行。比如下面这个例子:

func unwrap(value: String?) -> String {
return value ?? "default value"
}

unwrap(value: "foo") // foo
unwrap(value: nil) // default value

但是还有种情况就是空字符串,有时,我们需要把空字符串当做 nil 的情况来处理。此时,不仅要坚持 nil,还要检查空字符串的情况

func unwrap(value: String?) -> String {
let defaultValue = "default value"
guard let value = value else {
return defaultValue
}
if value.isEmpty {
return defaultValue
}
return value
}

unwrap(value: "foo") // foo
unwrap(value: "") // default value
unwrap(value: nil) // default value

虽然也能解决问题,但依然看起来很臃肿,我们把他简化一下:

func unwrapCompressed(value val: String?) -> String {
return val != nil && !val!.isEmpty ? val! : "default value"
}

unwrapCompressed(value: "foo") // foo
unwrapCompressed(value: "") // default value
unwrapCompressed(value: nil) // default value

虽然简化了很多,但不易读,可维护性略差。

可以把空字符串先转化为 nil,再进行处理,这样就和处理 nil 的情况一致了。

public extension String {
var nilIfEmpty: String? {
self.isEmpty ? nil : self
}
}

let foo: String? = nil

if let value = foo?.nilIfEmpty {
print(value) //不会调用
}

if let value = "".nilIfEmpty {
print(value) //不会调用
}

if let value = "ABC".nilIfEmpty {
print(value) //ABC
}


总结

这里先介绍 5 个常用扩展中的其中 2 个,剩下 3 个且听下回分解啦~

  • 给集合增加扩展,防止取值越界造成崩溃
  • 给字符串增加扩展,让空字符串变为 nil

如果觉得对你有帮助,不妨在项目中试试吧~

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

收起阅读 »

iOS App - 从编译到运行

iOS
在iOS开发中,app是被直接编译成机器码后在CPU上运行的,而不是使用解释器编译成字节码再运行。从app的编译到运行的过程中,要经过编译、链接、启动几个步骤。而在iOS中,编译阶段分为前端和后端,前端使用Apple开发的Clang,后端使用LLVM。 编译...
继续阅读 »

在iOS开发中,app是被直接编译成机器码后在CPU上运行的,而不是使用解释器编译成字节码再运行。从app的编译到运行的过程中,要经过编译、链接、启动几个步骤。而在iOS中,编译阶段分为前端和后端,前端使用Apple开发的Clang,后端使用LLVM。



编译


编译过程


编译过程主要有



  • 预处理

  • 词法分析

  • 语法分析

  • 静态分析

  • 中间代码生成

  • 汇编生成

  • 链接生成可执行文件


预处理


在预处理的阶段中,编译器Clang首先预处理我们代码,做一些比如将宏替换到代码中、删除注释、处理预编译命令等工作


词法分析


在此阶段词法分析器读入预处理过的代码字节流,将其中的字符处理成有意义的词素序列,对于每个词素产生词法单元并标记位置,处理完成后进入下一步。这个过程主要是为了在下一步生成语法树做基础工作。


语法分析


这一步中使用在词法分析中生成的词法单元,抽象生成一个语法树(AST,Abstract syntax tree)。抽象语法树上的每个节点也标记了它在源代码的位置。抽象语法树的遍历比起源代码块很多,这一步主要是为了后面的静态分析。
抽象语法树AST


静态分析 | 中间代码生成


将源代码转化为抽象语法树后,编译器就可以遍历整个树来做静态分析。**常见的类型检查、语法错误、方法未定义等都是在静态分析中发现并处理的,当然静态分析能做的事情还有非常多。**在静态分析结束后,编译器会生成IR。IR是整个编译链接系统的中间产物,是一种比较接近机器码的形式,但他与平台无关,通过IR可以生成多个平台的机器码。IR是在iOS编译系统中,前端Clang和后端LLVM的分界点。Clang的任务在生成IR后结束,将IR交付给LLVM后LLVM开始工作。


汇编生成


在获得到IR后,LLVM可以根据优化策略对IR进行一些优化,如尾递归优化、循环优化、全局变量优化。在优化完成后,LLVM会调用汇编生成器将IR转化成汇编代码。此时,生成产物就是.o文件了(二进制文件)。
在生成二进制文件后,我们可以通过二进制重排的方式对我们的编译产物进行更进一步的优化,已达到缩小编译产物大小、优化启动速度等目的


链接


在将源代码编译成.o文件后,就开始链接。链接其实就是一个打包的过程,将编译出的所有.o文件和一些如dylib,.a,tbd文件链接起来,一起合并生成一个Mach-o文件。到这里,编译过程全部结束,可执行文件mach-o已生成。在链接前,符号是未跟内存地址、寄存器绑定的,尤其是一些被定义在其他模块的符号。而在链接阶段,链接器完成了上述工作,进行了除动态库符号外的符号绑定,同时将这些目标文件链接成一个可执行文件


Mach-o文件结构


-w313



  • Header

    • Header 包含该二进制文件的一般信息 字节顺序、架构类型、加载指令的数量等。 使得可以快速确认一些信息,比如当前文件用于32位还是64位,对应的处理器是什么、文件类型是什么



  • Load Commands

    • 是一张包含很多内容的表。内容包括区域的位置、符号表、动态符号表等。这一段紧跟Header,加载Mach-O文件时会使用这里的数据来确定内存的分布



  • Data

    • Data 通常是对象文件中最大的部分,包含Segement的具体数据,如静态C字符串,带参数/不带参数的OC方法,带参数/不带参数的C函数。当运行一个可执行文件时,虚拟内存 (virtual memory) 系统将 segment 映射到进程的地址空间上。

    • Segment __PAGEZERO 规定进程地址空间的前多少空间不可读写

    • Segment __TEXT 包含可执行的二进制代码

    • Segment __DATA 包含了将被更改的数据

    • Segment __LINKEDIT 包含了方法和变量的元数据,代码签名等信息。




静态链接


编译主要分为静态链接动态链接。在编译器阶段进行的是静态链接,也就是在上文中提到的过程。这一阶段是将在前面生成的各种目标文件和各种库(or module in swift)链接起来,生成一个可执行文件mach-o。




运行


装载


一个程序从可执行文件到运行,基本都要经过装载和动态库链接两个阶段。由于在可执行文件生成前已经完成了静态库链接,所以在装载时所有的源代码和静态库已经完成了装载,而动态库链接则需要下文提到的动态链接来完成。


可执行文件,或者说程序,是一个静态的概念,而进程是一个动态的概念。每个程序在运行起来后,他对应的进程都会拥有独立的地址空间,而这个地址空间是由计算机硬件(CPU的位数)决定的,当然,进程只是以为自己拥有计算机整个的地址空间,实际上他是与其他的进程共享计算机的内存(虚拟化)


装载,就是把硬盘上的可执行文件映射到虚拟内存上的过程。


装载的过程,也可以当作是进程建立的过程,一般来说有以下几个步骤。



  • 创建一个独立的虚拟地址空间

  • 读取可执行文件头,建立虚拟地址空间与可执行文件之间的映射关系。(将可执行文件中的相对地址与虚拟地址空间的地址进行绑定)

  • 将CPU的指令寄存器设为可执行文件的入口地址,交与CPU启动运行


动态链接


静态链接是链接静态库,需要链接进Mach-o文件中,如果需要更新就需要重新编译一次,所以无法动态更新和加载。而动态链接是使用dyld动态加载动态库,可以实现动态地加载和更新。并且其他的进程、框架链接的都是同一个动态库,节省了内存。


iOS中我们常用的一些如UIKitFoundation等框架都是使用动态链接的,而为了节省内存,系统将这些库放在动态库共享缓存区(Dyld shared cache)


mach-o文件中,属于动态库的符号会被标记为未定义,但他们的名字与路径会被记录下来。在运行时dyld会通过dlopendlsym导入动态库,并通过记录的路径找到对应的动态库,通过记录的名字找到对应的地址,进行符号与地址的绑定。


dlopen会将动态库映射到进程的虚拟地址空间中,由于载入的动态库中可能也会存在未定义的符号,也就是说该动态库还依赖了其他的动态库,这时会触发更多的动态库被载入,但dlopen可以决定是立刻载入这些依赖库还是延后载入。


dlopen打开动态库后返回的是引用的指针,dlsym的作用就是通过dlopen返回的动态库指针和函数符号,得到函数的地址然后使用。


动态链接解决了静态链接内存占用过多只要有库修改就要重新编译打包的缺点,但同时也引入了新的问题。



  • 结构复杂,动态链接将重定位推迟到运行时进行。

  • 引入了安全问题,这也是我们能够进行PLT HOOK的基础

  • 性能问题


而提到动态库链接,在iOS领域就必须提到我们的dyld


dyld - Dynamic Link Editor



dyld是苹果开发的动态链接器,是苹果系统的一个重要组成部分。它负责mach-o文件的动态库链接和程序的启动。相关代码已开源




  • 启动流程


main方法前的调用栈


启动工程,在_objc_init处设置一个symbolic breakpoint,Xcode会帮我们在main方法执行前设置断点。进入lldb后使用bt命令,我们就可以看到_objc_init方法前的调用栈。


可以看到,dyld是最先被启动的。_dyld_start后,首先调用的是dyldbootstrap命名空间里的start函数,dyld:bootstrap意义为dyld进行自举工作。由于动态链接器本身也是一个共享对象,那么它自己也需要重定向工作。那么为了避免循环重定向的问题,动态链接器相对于其他的共享对象需要有一些特性。第一个就是它不可以依赖于其他的共享对象,第二个是它的重定向工作可以由自己完成。这种具有一定限制条件的启动代码称为自举(bootstrap)


由于dyld比较复杂,在这里就先不详细展开,留待另一篇文章中细讲。启动的大体流程为



  • dyld 开始将程序二进制文件初始化

  • 交由 ImageLoader 读取image,其中包含了我们的类、方法等各种符号

  • 由于 runtime 向 dyld 绑定了回调,当image 加载到内存后,dyld会通知runtime进行处理

  • runtime接手后调用map_images做解析和处理,接下来load_images中调用call_load_methods方法,遍历所有加载进来的Class,按继承层级依次调用Class的 +load 方法和其 Category 的 +load 方法


所以动态链接器的工作流程为



  1. 动态链接器自举 (动态链接器的地址在可执行文件的.interp段) ->

  2. 装载共享对象(在这个步骤合并生成全局符号表)->

  3. 重定位(遍历可执行文件和每个共享对象的重定位表将GOT/PLT中需要重定位的位置进行修正)->

  4. 初始化(执行共享对象.init段中的代码,进程的.init段由程序初始化代码执行)->

  5. 将控制权交还给程序的入口


写在最后


在写这篇的过程中系统地学习了一下app从编译到运行的过程。在编译阶段,静态链接动态链接这种编译原理相关的知识很重要,有时间可以读一下编译原理那本书。运行阶段,dyld在main函数执行前做了非常多工作,其实现也很复杂,待仔细学习后再写一篇聚焦于dyld的笔记。


链接:https://juejin.cn/post/7030435738944536607
来源:稀土掘金
收起阅读 »

重要!后面几个月,iOS开发需要注意的3件事情

iOS
这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战前言这里非常感谢@恋猫de小郭,大佬的一篇文章让我醍醐灌顶。通过大佬的这篇文章对开发者而言《个人信息保护法》更新究竟是什么?如何应对适配?,我回过头,老老实实看了苹果开发的一些新闻,有一些...
继续阅读 »


这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

前言

这里非常感谢@恋猫de小郭,大佬的一篇文章让我醍醐灌顶。

通过大佬的这篇文章对开发者而言《个人信息保护法》更新究竟是什么?如何应对适配?,我回过头,老老实实看了苹果开发的一些新闻,有一些非常重要的信息。

如果你从事iOS开发,抑或针对Flutter开发,有需要上架App Store,我建议各位都来了解这3件事情。

自2022年1月31日起,需在app内提供帐户删除的功能

截屏2021-11-12 15.31.56.png

撇开11月1日颁布的《中华人民共和国个人信息保护法》,早上10月6日,Apple就发布了新闻说明:

需要在App中提供账户删除的功能,并且对于在2022年1月31日开始提交的App生效。

这说明了一个什么事呢?

如果你有一个App,后面会持续迭代,那么这个账户删除功能必须加上!!!

这事最好和项目、技术、后台一起讨论一下,我个人认为在App端无非多了一个交互,多了一个接口,多了一个逻辑,但是这个删除对后端来说,可能需要删除的东西就多了。

另外,从这字面上看,这应该是一个硬删除吧(软删除大家都懂的)。

自2022年4月起,必须使用Xcode 13和iOS 15 SDK构建App,提交至App Store

截屏2021-11-12 15.42.35.png


说简单点:

说白了,这是一波强行的让你升级Xcode的做法,没办法,在此道上混,就只能这么走。

迟早都要升级Xcode13,早点升级早踩坑。


另外需要注意的是,有些项目可能会在Xcode12上运行的很好,但是在Xcode13上一运行就报错,需要提前做好准备,我手上就有一个这样的项目。

年末假期接受app提交

截屏2021-11-12 15.56.06.png

大家都知道的,由于11月和12月都有西方的传统节日,所以一般情况下,在一些时间点提交App到App Store审核会异常缓慢。

今年Apple自己内卷了一把,在年末的假期也接受app提交了,虽然不知道具体速度如何,不过这也算是迎合国内的市场需求吧。

因为有些app就是在年末或者过节的时候迭代的非常频繁。毕竟大家都有剁手嘛~😁

参考文档

“需在 app 内提供帐户删除”的要求将于 1 月 31 日生效

将iOS和iPadOS app提交至App Store

年末假期接受app提交

macOS Monterey 与以下电脑兼容

总结

今天就想讲这么几件事情,其中第一件和第二件事情在我看来还是挺重要的,大家自己也掂量一下呗。

一周有空去Apple Developer网站看看新闻,有些对开发还是非常重要的。

Apple Developer新闻与更新

收起阅读 »

iOS App 的最佳架构,存在么?

iOS
iOS App 的最佳架构,存在么?本文翻译自 The best architecture for the iOS app, does it even exist?,建议参考原文阅读,也可查看这里前一段时间,我偶然发现了有关 iOS 体系结构模式的文...
继续阅读 »

iOS App 的最佳架构,存在么?

本文翻译自 The best architecture for the iOS app, does it even exist?,建议参考原文阅读,也可查看这里

前一段时间,我偶然发现了有关 iOS 体系结构模式的文章,标题颇具挑衅性:“唯一可行的 iOS 架构”。标题中问题的答案实际上是 MVC。简而言之,MVC 是 iOS 应用程序唯一可行的也是最好的架构。

该文章的主要思想是人们只是以错误的方式去理解 MVC。该 ViewController 实际上是表示层的一部分,而 Model 部分则代表整个 Domain Model,而不仅仅是某些数据实体。总的来说,我同意那个帖子的想法,但是如果我同意那个帖子的每一个陈述,我就不会写这篇文章了,不是吗?

我注意到作者基本上没有涉及格式良好的应用程序体系结构的一个非常重要的方面:使用单元测试(UT)覆盖了应用程序业务逻辑(BL)。对我而言,这是明智的应用程序体系结构的最重要因素之一。如果无法提取应用程序 BL 并以足够的覆盖范围实现 UT,那么这种架构简直糟透了。

此外,如果一个应用没有 UT,那么证明上述观点是不可行的,因此其架构最有可能出现问题。您可以向自己保证,在你从紧张工作中的片刻休息时间可以轻松实现UT,或者仅仅是因为您在 XCode 项目中拥有专用的“Tests”目标以及一些模板 UT。相信我,但这不过是一种幻想。我坚信,如果单元测试未随功能一起实施或在交付后不久就将无法实施。

以该声明为公理,应用程序体系结构必须提供将 BL 与 UI 表示分离并使其可测试的功能。很明显,由于 UIViewController 子类对生命周期的依赖性,因此它不是该角色的最佳候选人。这意味着负责 BL 的类必须位于 UIViewController 和 Services 之间,或者换句话说,位于 View 和 Domain Model 之间。

值得注意的是,这里的 Services 是指负责联网,与数据库、传感器、蓝牙、钥匙串、第三方服务等进行通信的逻辑。换句话说,是应用中多个位置、页面的共享部分。而图上的业务逻辑部分仅对应于一个页面或一个由视图控制器表示的页面组件。在开头提到的有关 MVC 的文章中,作者将 BL 和 Domain Model 部分结合在一起,同时接受 UIViewController 是表示逻辑(即视图)的一部分。

现在,当确定了将表示和业务逻辑分离的需要时,让我们考虑一下这两个部分如何相互通信。 这就是那些著名的架构模式出现的地方。

MVP

在 MVP 模式中,Presenter 和 View 通过协议相互链接。Presenter 被注入了 View 协议的实例,反之亦然,View 的协议必须具有足够的接口才能在 UI 中呈现原始数据,而Presenter 的协议必须具有传输从用户或系统接收到的事件(如触摸,手势,摇动等)的接口。UIViewController 子类在此处表示 View 部分,而 Presenter 类不能依赖UIKit(例如,有时需要导入 UIKit 才能对 UIImage 等数据类进行操作)。

在 iOS 上,由于 UIViewController 生命周期的工作方式,它必须具有对 Presenter 实例的强引用,而最后一个必须是弱引用,以避免循环引用。此配置使人联想到委托模式。 在大多数情况下,UIViewController 可能具有对 Presenter 的直接类型化引用,但在某些情况下,最后一个角色也可以通过协议注入到第一个中。如果 presentation 用于不同的业务逻辑,这可能会很有用。Presenter 到 UIViewController 的链接必须通过协议才能进行模拟,并用 UT 覆盖。在 Service 部分,我不会做太多具体说明,但是为了测试 Presenter,还必须将其与协议一起注入。

有关 MVP 的更多详细信息以及基本示例,请参见此处1

MVVM

在 MVVM 模式中,表示和业务部分使用响应性绑定相互通信,它们分别称为 View 和 ViewModel。在 iOS 中,通常会使用 ReactiveCocoa,RxSwift 或现代的 Combine 框架进行响应性绑定,它们通常位于 ViewModel 类中,并且也由 ViewController 通过协议使用。在与 Services 或 Domain Model 进行通信的一部分中,MVP 并没有太大的区别,但人们可能更喜欢在这里使用绑定或响应性事件。与前面的模式一样,必须在协议中注入依赖项,以便在 UT 中模拟它们。

可以在此处2找到有关 MVVM 的更多详细信息以及基本示例。

MVVM+Router

这里独立的主题是路由。在 iOS 中,以模态方式显示新屏幕或推送到导航堆栈是通过 UIViewController 子类来实现的。但是,这些操作可能是 BL 的一部分,并且可能会被 UT 覆盖,例如如果发生特定事件,则必须关闭屏幕。在这种情况下,将应用程序逻辑的这一部分分为一个称为 Router 的类是有意义的。因此,模式变为 MVP+R 或 MVVM+R。在某些来源中,您可能会发现此部分分别命名为 Coordinator 和 MVVP+C 或 MVVM+C。尽管协调器可能具有除路由之外的其他一些逻辑,但我更喜欢在概念上将它们等同。ViewModel 和 Router 之间的链接必须通过协议,并且最后一个必须仅负责屏幕操作,所有 BL 必须仍然集中在第一个中。因此,Router 不是 UT 的主题。

具有 MVVM+R 架构模式实现的示例项目可以在我的 GitHub3 上找到。

其它

VIPER iOS 体系结构模式是 MVVM+R 的扩展,其中 ViewModel 分为两部分:Interactor 和 Presenter。第一个负责与实体(即域模型)的通信。第二部分准备要在视图中呈现的模型类。老实说,我从未使用过这种模式,因为对我而言,它似乎过于分散和复杂。 MVVM+R 关注点分离对我而言总是足够的。

在 MVVM+R 中,每个模块(屏幕)必须至少显示 3 个类:ViewController,ViewModel 和 Router。并且必须有一个实例化所有这些部分并将它们彼此链接的位置,即模块构建的关键。最合适的位置是 Router,因为它没有与 iOS UIViewController 生命周期耦合,并且必须知道如何显示页面才能正确关闭它。但是,在某些情况下,将这一部分移到名为 Builder 的单独的类中会更方便,这就是 RIB(Uber的架构模式)中发生的情况。ViewModel 重命名为 Interactor,其余部分保持不变。这种模式具有 Uber 引入的一些更有趣的想法和技术,您可以在 RIB Wiki 上阅读。但是,我在 RIBs 代码库中发现的最实用的东西是 XCode 模板,当在项目中引入新的 RIBlet 时,它可以帮助避免样板编码。这些模板也可以很容易地用于 MVVM+R 类。为 Uber 的 iOS 工程师👏。

最后简单聊聊关于 iOS 上的单向数据流架构模式。如果看一下以上模式的方案,它们在组件之间都具有双向连接。在 Redux 中不是这种情况。这种架构模式最初是从 Web 迁发到移动应用的,尤其是 React 框架。在 ReSwift 框架中,此概念是 iOS 开发中最受欢迎的实现。我不会详细介绍,因为尚未在生产应用中使用此架构。但是,很明显,从 Web 开发进入 iOS 的人们发现这种架构模式最为直观和熟悉。

结论

什么才是最好的应用程序架构始终是一个热门的主题,所以现在我更倾向于约翰·桑德尔在他的最近一次演讲中提出的想法:

最好的架构是您和您的团队共同创建的架构,通过将标准模式和技术与系统设计相结合来适合您的项目。

参考

[1]https://medium.com/@saad.eloulladi/ios-swift-mvp-architecture-pattern-a2b0c2d310a3
[2]https://medium.com/flawless-app-stories/practical-mvvm-rxswift-a330db6aa693
[3]https://github.com/OlexandrStepanov/MVVM-RouterDemo

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

收起阅读 »

滴滴DoKit For Flutter正式开源,还是熟悉的"配方"

社区的小伙伴们,大家好啊,艰难的2020已经过去,随着2021的到来,在新的一年里希望大家都能够需求不变更、准点上下班、代码0 Bug。同时我们DoKit团队也给大家准备了一份特殊的“新年礼物”,没错它就是DoKit For Flutter,它来啦,它带着熟悉...
继续阅读 »

社区的小伙伴们,大家好啊,艰难的2020已经过去,随着2021的到来,在新的一年里希望大家都能够需求不变更、准点上下班、代码0 Bug。同时我们DoKit团队也给大家准备了一份特殊的“新年礼物”,没错它就是DoKit For Flutter,它来啦,它带着熟悉的配方来啦。接下来就让我们揭开它神秘的面纱吧。


背景


Flutter是Google开源的跨端技术框架。凭借其区别于RN/Weex的自渲染模式,在社区里引起了广泛关注,不管是终端还是前端的小伙伴都趋之若鹜,大有一统大前端江湖的气势。而国内大厂如闲鱼、字节、美团等,也都在其核心业务上完成了落地。


滴滴做为国内最大的出行平台,早在两年前就有多个内部团队开始在Flutter领域进行尝试。但是在开发过程中,我们遇到了很多调试性问题,如日志、帧率、抓包等。为了解决这些开发测试过程中遇到的各类问题,我们DoKit团队联合滴滴代驾和货运团队,把平时工作过程中沉淀下来的效率工具进行业务剥离和脱敏,并最终打造出DoKit For Flutter,在服务内部业务的同时,也为社区贡献一份力量,这也是滴滴的开源精神。


介绍


DoKit For Flutter是一个DoKit针对Flutter环境的产研工具包,内部集成了各种丰富的小工具,UI、网络、内存、监控等等。DoKit始终站在用户的角度,为用户提供最便利的产研工具。


Github地址


Pub仓库地址


操作文档



图片名称

那么接下来就让我来列举一下DoKit For Flutter的功能以及核心实现。


工具详解


基本信息


基本信息模块会展示当前dart虚拟机进程、CPU、Flutter版本信息、当前App包名和dart工程构建版本信息;



图片名称

VM信息通过VMService获取。Flutter版本实际上是通过Devtools服务注入的"flutterVersion"方法获取到的,在flutter attach后,本地会起一个websocket服务,连接VMService并注入flutterVersion和其余方法(HotReload、HotRestart等),通过VMService调用flutterVersion方法,会从本地flutter sdk目录下解析version文件返回版本号。


路由信息



图片名称

在Flutter中,每个页面对应一个Route,通过Navigator管理Route。Navigator内部会包含一个Overlay Widget,每个Route最终都转化成一个_OverlayEntryWidget添加到Overlay上。这个地方可以把Overlay理解为Android中的FrameLayout,内部子View上下叠加。每打开一个新的Route,都相当于往FrameLayout添加一个新的子View。Navigator会存在嵌套的情况,即Route所创建的页面本身也包含一个Navigator,比如App的根Widget是MaterialApp(自带Navigator),Route页面也用MaterialApp包裹,就会形成Navigator嵌套的情况。还是以FrameLayout来理解,这也就相当于嵌套的FrameLayout。
路由信息功能会打印出当前栈顶页面所处的Route信息,如果存在Navigator嵌套的情况,也会向上遍历打印出每层Navigator的信息。具体的实现方式是,先获取当前根app根Element,可以使用WidgetsBinding.instance.renderViewElement作为根Element,再通过递归调用element的visitChildElements方法,向下遍历整棵树找到最后一个RenderObejctElement,该RenderObejctElement即为当前显示的页面上的元素。然后使用ModalRoute.of(element)方法即可获取到当前页面的路由信息。


至于嵌套的路由信息,则可以通过找到的RenderObejctElement的findAncestorStateOfType方法,反向向上递归遍历,获得所处的Navigator的NavigatorState,再调用ModalRoute.of(navigatorState.context),如果返回不为空则表示存在嵌套。


方法通道



图片名称

Flutter的Method Channel调用最终都会经过ServiceBinding.instance._defaultBinaryMessenger这个对象,类型为BinaryMessenger,由于这个对象是个私有对象,无法动态进行修改。不过查看ServiceBinding的源码可以发现这个对象是通过ServiceBinding.createBinaryMessenger方法创建的,通过使用flutter的mixins,可以实现对该方法的重写。
我们知道,ServiceBinding实际也是通过mixins在WidgetsFlutterBinding.ensureInitialized方法中一起被初始化的,所以只要在WidgetsFlutterBinding这个类额外mixin一个继承于ServiceBinding并且重写了createBinaryMessenger方法的类,就能实现对ServiceBinding中createBinaryMessenger的覆盖,代码如下:


class DoKitWidgetsFlutterBinding extends WidgetsFlutterBinding
with DoKitServicesBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null) DoKitWidgetsFlutterBinding();
return WidgetsBinding.instance;
}
}

mixin DoKitServicesBinding on BindingBase, ServicesBinding {
@override
BinaryMessenger createBinaryMessenger() {
return DoKitBinaryMessenger(super.createBinaryMessenger());
}
}

接下去把runApp的入口调用改成如下,就能实现BinaryMessenger的替换
static void _runWrapperApp(DoKitApp wrapper) {
DoKitWidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(wrapper)
..scheduleWarmUpFrame();
}
至于Method Channel具体信息的捕获,只要hook住BinaryMessenger.handlePlatformMessage和BinaryMessenger.send两个方法就行了,具体可看DoKitBinaryMessenger这个类


控件检查



图片名称

和路由功能类似,通过从根element向下遍历,在遍历过程中记录和选中的View有交集的所有RendereObjectElement,并且记录用以标志当前页面的RendereObjectElement,获取它的Route信息。遍历完成后,遍历记录下来的RendereObjectElement,过滤掉Route信息和当前页面不一致的,这些Element属于被遮盖住的页面。然后通过比对RendereObjectElement和选中View的交叉区域面积占RendereObjectElement面积的比例,占比最大的为当前选中的组件。
在Debug模式下可以获取选中组件在工程中的代码位置,将WidgetInspectorService.instance.selection.current赋值为选中element的renderObject,再调用WidgetInspectorService.instance.getSelectedSummaryWidget方法,会返回一个json字符串,解析这个字符串就能获取源码文件名、行列信息等。


日志查看



图片名称

日志查看功能比较简单,只要使用runZoned方法替代runApp,传入zoneSpecification,就能为日志输出设置一个代理函数,在这个代理函数内进行日志捕获,同时,还可以为onError设置一个代理函数,在这里将捕获的异常也会传入到日志当中。


帧率



图片名称

使用WidgetsBinding.instance.addTimingsCallback可以统计帧率信息,在每帧渲染完成时会触发回调,包含该帧渲染的信息。


内存



图片名称

同VM信息,使用VMService可以获取到内存详细使用信息。


网络请求



图片名称

Flutter自带的网络请求通过HttpClient类发送,只要hook住HttpClient的创建就可以hook整个网络请求的过程。查看HttpClient的构造函数可以发现,如果存在HttpOverrides,就会使用HttpOverrids来创建HttpClient


factory HttpClient({SecurityContext? context}) {
HttpOverrides? overrides = HttpOverrides.current;
if (overrides == null) {
return new _HttpClient(context);
}
return overrides.createHttpClient(context);
}
所以这里重写了一个HttpOverrids
class DoKitHttpOverrides extends HttpOverrides {
final HttpOverrides origin;

DoKitHttpOverrides(this.origin);

@override
HttpClient createHttpClient(SecurityContext context) {
if (origin != null) {
return DoKitHttpClient(origin.createHttpClient(context));
}
// 置空,防止递归调用,使得_HttpClient可以被初始化
HttpOverrides.global = null;
HttpClient client = DoKitHttpClient(new HttpClient(context: context));
// 创建完成后继续置回DoKitHttpOverrides
HttpOverrides.global = this;
return client;
}
}

替换HttpOverrides


HttpOverrides origin = HttpOverrides.current;
HttpOverrides.global = new DoKitHttpOverrides(origin);

hook住HttpClient方法后,对于请求和返回结果的hook过程就和Android中的HttpUrlConnection类似了,具体可以看DoKitHttpClient、DoKitHttpClientRequest、DoKitHttpClientResponse三个类。


版本API兼容


Flutter版本更新还是比较快的,每一个大版本更新都会带来一些API的变更,目前DoKit的方案需要重写一些framework层的类,在兼容多版本时就会有一些问题。以上面的BinaryMessager为例,1.17版本只有四个方法,用来hook的DoKitBinaryMessager是这么写的


class DoKitBinaryMessenger extends BinaryMessenger {
final MethodCodec codec = const StandardMethodCodec();
final BinaryMessenger origin;

DoKitBinaryMessenger(this.origin);

@override
Future<void> handlePlatformMessage(String channel, ByteData data, callback) {
ChannelInfo info = saveMessage(channel, data, false);
PlatformMessageResponseCallback wrapper = (ByteData data) {
resolveResult(info, data);
callback(data);
};
return origin.handlePlatformMessage(channel, data, wrapper);
}

@override
Future<ByteData> send(String channel, ByteData message) async {
ChannelInfo info = saveMessage(channel, message, true);
ByteData result = await origin.send(channel, message);
resolveResult(info, result);
return result;
}

@override
void setMessageHandler(
String channel, Future<ByteData> Function(ByteData message) handler) {
origin.setMessageHandler(channel, handler);
}

@override
void setMockMessageHandler(
String channel, Future<ByteData> Function(ByteData message) handler) {
origin.setMockMessageHandler(channel, handler);
}
}

用来hook的wrapper类需要调用oring对象的同名方法。但在1.20版本BinaryMessager增加了两个新方法checkMessageHandler和checkMockMessageHandler,如果使用1.17.5版本的flutter sdk去编译,就无法调用origin.checkMessageHandler方法,因为不存在;如果使用1.20.4版本的flutter sdk去编译,编译和发布没问题,但编出来的sdk在1.17.5的工程被引用后,也会因为checkMessageHandler方法不存在导致编译失败。
针对这种多个Flutter版本API不同导致的兼容性问题,可以使用扩展方法extension关键字来解决。
建立一个_BinaryMessengerExt类如下:


extension _BinaryMessengerExt on BinaryMessenger {
bool checkMessageHandler(String channel, MessageHandler handler) {
return this.checkMessageHandler(channel, handler);
}

bool checkMockMessageHandler(String channel, MessageHandler handler) {
return this.checkMockMessageHandler(channel, handler);
}
}

在1.17.5版本,调用origin.checkMessageHandler会走到扩展方法的checkMessageHandler中,编译能通过,由于这个方法在1.17.5中是绝对不会被调用到的,虽然会形成递归调用,但没影响。而在1.20版本,BinaryMessenger本身实现了checkMessageHandler方法,所以调用checkMessageHandler方法会走到BinaryMessenger的checkMessageHandler方法中,也能正常使用。
通过extentsion,只要以最低兼容版本的类作为基础,在扩展类中定义新版本中新增的API,就能解决多版本API兼容的问题。


总结


以上就是DoKit For Flutter的现有功能以及工具的基本原理介绍。 我们知道当前它的功能还不是完善,后续我们会继续不断深入的挖掘业务中的痛点并持续输出各种提高用户效率的工具,努力让DoKit For Flutter变得更加优秀,符合大家的期望。


DoKit一直追求给开发者提供最便捷和最直观的开发体验,同时我们也十分欢迎社区中能有更多的人参与到DoKit的建设中来并给我们提出宝贵的意见或PR。 DoKit的未来需要大家共同的努力。


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

什么?你还不会用位运算来操作状态?

回顾首先来回顾一下位运算,什么是位运算呢?位运算就是直接对整数在内存中的二进制位进行操作。在 Java 语言中,位运算有如下这些:左移(<<)。右移(>>)。无符号右移(>>>)。与(&)。或(|)。非(~)。...
继续阅读 »

回顾

首先来回顾一下位运算,什么是位运算呢?

位运算就是直接对整数在内存中的二进制位进行操作。

在 Java 语言中,位运算有如下这些:

  • 左移(<<)。

  • 右移(>>)。

  • 无符号右移(>>>)。

  • 与(&)。

  • 或(|)。

  • 非(~)。

  • 异或(^)。

在本篇文章中,我们所需要用到的有如下几个(其他的后续文章再讲):

  • &(与运算):只有当两方都为 true 时,结果才是 true,否则为 false。

  • |(或运算):只要当一方为 true 时,结果就是 true,否则为 false。

  • ^(异或运算):只要两方不同,结果就是 true,否则为 false。

以 true、false 为例:


true & true = true

true & false = false



true | false = true;

false | false = false;



true ^ true = false;

true ^ false = true;

以数字运算为例:


6 & 4 = ?

6 | 4 = ?

6 ^ 4 = ?

当以数字运算时,我们首先需要知道这些数字的二进制,假设 6 是 int 类型,那么其二进制如下:

00000000 00000000 00000000 00000110

在 Java 中,int 占了 4 个字节(Byte),一个字节呢又等于 8 个 Bit 位。所以 int 类型的二进制表现形式如上。

在这里为方便讲解,直接取后 8 位:00000110。

4 的二进制码如下:


00000100

在二进制码中,1 为 true,0 为 false,根据这个,我们再来看看 6 & 4 的运算过程:


00000110

00000100

-----------

00000100

对每位的数进行运算后,结果为 4。

再来看看 | 运算:


6 | 4 = ?

6 和 4 的二进制上面已经说了:


00000110

00000100

-----------

00000110

可以发现最后的结果是 6。

最后再来看看 ^ 运算:


6 ^ 4 = ?


00000110

00000100

-----------

00000010

结果是 2。

应用

通过上面的例子,我们已经回顾了 & 、 | 以及 ^ 运算。现在来将它应用到实际的应用中。

假如我们现在要定义一个人的模型,这个人可能会包含有多种性格,比如说什么乐观型、内向型啦...

要是想要知道他包含了哪种性格,那么我们该如何判断呢?

可能在第一时间会想到:


if(这个人是乐观性){

....

}else if(这个人是内向型){

...

}

那么如果有很多种性格呢?一堆判断写起来真的是很要命..

下面就来介绍一种更简单的方式。首先来定义一组数:


public static final int STATUS_NORMAL = 0;
public static final int STATUS_OPTIMISTIC = 1;
public static final int STATUS_OPEN = 2;
public static final int STATUS_CLOSE = 4;

把它们转换为二进制:


0000 0000 0000 0000
0000 0000 0000 0001
0000 0000 0000 0010
0000 0000 0000 0100

发现其中二进制的规律没有?都是 2 的次幂,并且二进制都只有一个为 1 位,其他都是 0 !

然后再来定义一个变量,用于存储状态(默认值是 0):


private static int mStatus = STATUS_NORMAL;

当我们要保存状态时,直接用 | 运算即可:


mStatus |= STATUS_OPTIMISTIC;

保存的运算过程如下:


00000000

执行 | 运算(只要有 1 则为 1)

00000001

-----------

00000001 = 1

相当于就把这个 1 存储到 0 的二进制当中了。

那么如果要判断 mStatus 中是否有某个状态呢?使用 & 运算:


System.out.println((mStatus & STATUS_OPTIMISTIC) != 0);// true,代表有它

计算过程如下:


00000001

执行 & 运算(都为 1 才为 1)

00000001

-----------

00000001 = 1

再来判断一个不存在的状态 mStatus & STATUS_OPEN


System.out.println((mStatus & STATUS_OPEN) != 0);// false,代表没有它

计算过程如下:


00000001

00000010

-----------

00000000 = 0

可以发现,因为 STATUS_OPEN 这个状态的二进制位,1 的位置处,mStatus 的二进制并没有对于的 1,而又因为其他位都是 0,导致全部归 0,计算出来的结果自然也就是 0 了。

这也就是为什么定义状态的数字中,是 1、2、4 这几位数了,因为他们的特定就是二进制只有一个为 1 的位,其他位都是 0,并同其他数位 1 的位不冲突。

如果换成其他的数,就会有问题了。比如说 3:


mStatus |= 3

计算过程:


00000000

00000011

-----------

00000011 = 3

运算完毕,这时候 mStatus 中已经存储了 3 这个值了,我们再来判断下是否存在 2:


System.out.println((mStatus & 2) != 0);// true,代表有它,但是其实是没有的

00000011

00000010

-----------

00000010 = 2

结果是 true,但是其实我们只存储了 3 到 mStatus 中,结果肯定是错误的。

所以我们在定义的时候,一定不要手滑定义错了数字。

存储和判断已经说了,那么如何取出呢?这时候就要用到 ^ 运算了。

假如现在 mStatus 中已经存储了 STATUS_OPTIMISTIC 状态了,要把它给取出来,这样写即可:


mStatus ^= STATUS_OPTIMISTIC

其中的运算过程:


00000001

执行 ^ 运算,两边不相同,则为 true

00000001

-----------

00000000

可以看到状态又回到了最初没有存储 STATUS_OPTIMISTIC 状态的时候了。

最后再来看一个取出的例子,这次是先存储两个状态,然后再取出其中一个:


mStatus |= STATUS_OPTIMISTIC

mStatus |= STATUS_OPEN

存储完后,mStatus 的二进制为:


00000011

再来取出 STATUS_OPEN 这个状态:


mStatus ^= STATUS_OPEN

运算过程:


00000011

00000010

-----------

00000001

mStatus 现在就只有 STATUS_OPTIMISTIC 的状态了。

总结

通过 |、^、& 运算,我们可以很方便快捷的对状态值进行操作。当然,位运算的应用不仅限于状态值,知道了其中的二进制运算原理后,还有更多的其他应用场景,等着你去发现。


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

收起阅读 »

Android 多渠道打包看这一篇就够了

Android 多渠道打包看这一篇就够了 本文三个流程 一、多渠道配置 1、多渠道配置 2、不同渠道不同签名配置 3、不同渠道不同资源文件配置 4、不同渠道不同依赖配置 二、注意事项 三、打包 1、命令行打包 2、IDE 打包 多渠道配置(2 种方式) 1、可...
继续阅读 »

Android 多渠道打包看这一篇就够了


本文三个流程


一、多渠道配置


1、多渠道配置

2、不同渠道不同签名配置

3、不同渠道不同资源文件配置

4、不同渠道不同依赖配置

二、注意事项


三、打包


1、命令行打包


2、IDE 打包


多渠道配置(2 种方式)


1、可写在主模块(app)的 build.gradle 下


android {  
compileSdkVersion 29
buildToolsVersion "29.0.3"

defaultConfig {
applicationId "com.test.moduledemo"
minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
}

flavorDimensions "versionCode"

productFlavors {
xiaomi{
applicationId = “com.test.xiaomi"
//不同渠道配置不同参数
buildConfigField "int", "TEST_VALUE", "1"
buildConfigField "String", "TEST_NAME", "\"xiaomi\""
}
huawei{
applicationId = "com.test.huawei"
//不同渠道配置不同参数
buildConfigField "int", "TEST_VALUE", "2"
buildConfigField "String", "TEST_NAME", "\"huawei\""
}
productFlavors.all {//遍历productFlavors多渠道,设置渠道号(xiaomi 、huawei)
flavor -> flavor.manifestPlaceholders.put("CHANNEL", name)
}
}
applicationVariants.all { variant ->
// 打包完成后输出路径
def name = ((project.name != "app") ? project.name : rootProject.name.replace(" ", "")) +
"_" + variant.flavorName +
"_" + variant.buildType.name +
"_" + variant.versionName +
"_" + new Date().format('yyyyMMddhhmm') + ".apk"
//相对路径app/build/outputs/apk/huawei/release/
def path = "../../../../../apk/" //相当于路径 app/apk/
variant.outputs.each { output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith('.apk')) {
//指定路径输出
output.outputFileName = new File(path, name)
}
}
// 在打包完成后还可以做一些别的操作,可以复制到指定目录,或者移动文件到指定目录
variant.assemble.doLast {
File out = new File(“${project.rootDir}/apk”)
variant.outputs.forEach { file ->
//复制apk到指定文件夹
//copy {
// from file.outputFile
// into out
//}
//把文件移动到指定文件夹
ant.move file: file.outputFile,
todir: "${project.rootDir}/apk"
}
}
}
//多渠道签名的配置
signingConfigs {
test {
storeFile file("../test.keystore")
storePassword 'test'
keyAlias 'test'
keyPassword 'test'
v1SigningEnabled true
v2SigningEnabled true
}
xiaomi {
storeFile file("../xiaomi.keystore")
storePassword 'xiaomi'
keyAlias 'xiaomi'
keyPassword 'xiaomi'
v1SigningEnabled true
v2SigningEnabled true
}
huawei {
storeFile file("../huawei.keystore")
storePassword 'huawei'
keyAlias 'huawei'
keyPassword 'huawei'
v1SigningEnabled true
v2SigningEnabled true
}
}
buildTypes {
debug {
// debug这里设置不起作用,可能是编译器的问题?
// productFlavors.xiaomi.signingConfig signingConfigs.test
// productFlavors.huawei.signingConfig signingConfigs.test
}
release {
productFlavors.xiaomi.signingConfig signingConfigs.xiaomi
productFlavors.huawei.signingConfig signingConfigs.huawei
}
}
//不同渠道不同资源文件配置
sourceSets{
xiaomi.res.srcDirs 'src/main/res-xiaomi'
huawei.res.srcDirs 'src/main/res-huawei'
}
//不同渠道不同的依赖文件
dependencies {
xiaomiApi('xxxxxxx')
huaweiImplementation('xxxxxxxx')
}
}

2、在项目根目录下(与settings.gradle同目录)新建 flavors.gradle 文件


 android {  
flavorDimensions "versionCode"

productFlavors {
xiaomi{
applicationId = "com.test.xiaomi"
//不同渠道配置不同参数
buildConfigField "int", "TEST_VALUE", "1"
buildConfigField "String", "TEST_NAME", "\"xiaomi\""
}
huawei{
applicationId = "com.test.huawei"
//不同渠道配置不同参数
buildConfigField "int", "TEST_VALUE", "2"
buildConfigField "String", "TEST_NAME", "\"huawei\""
}
productFlavors.all {//遍历productFlavors多渠道,设置渠道号(xiaomi 、huawei)
flavor -> flavor.manifestPlaceholders.put("CHANNEL", name)
}
}
// ............ 更多配置
}

在主模块(app)的 build.gradle 下引用该 flavors.gradle 文件即可
apply from: rootProject.file('flavors.gradle')


注意


如果项目较为复杂,有可能通过 buildConfigField 设置不同的渠道包,不同的信息字段有可能失效,则把
buildConfigField "int", "TEST_VALUE", "1"
换成
manifestPlaceholders.put("TEST_VALUE", 1)
然后再 AndroidManifest.xml 里添加


<application>
<meta-data
android:name="TEST_VALUE"
android:value="${TEST_VALUE}" />
</application>

在 代码通过一下操作获取其值:


ApplicationInfo applicationInfo = getPackageManager().getApplicationInfo(getPackageName(),  
PackageManager.GET_META_DATA);
int testValue = applicationInfo.metaData.getInt("TEST_VALUE");

打包


命令行打包:


Windows下: gradlew assembleRelease
Mac 下:./gradlew assembleRelease
assembleRelease 是打所有渠道的 Release 包
assembleDebug 是打所有渠道的 Debug 包
还可以打指定渠道的包:
gradlew assembleXiaoMiRelease assembleHuaWeiRelease
(空格隔开要打的渠道包的任务名称即可,任务名称可以通过点击 android studio 右边的 Gradle 根据图中目录查看)



编译器打包





当渠道很多的时候,不同渠道不同配置就会变得相当繁琐了,欢迎查看我的下一篇推文多渠道打包-进阶


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

【灵魂拷问】当面试官问你JavaScript预编译

(一) 前言 在腾讯字节等其他大厂的面试中,JavaScript预编译是经常会被问到的问题,本文将带大家了解JS预编译中的具体过程 (二)编译执行步骤 传统编译语言编译步骤 对传统编译型语言来说,其编译步骤一般为:词法分析->语法分析->代码生成,...
继续阅读 »

(一) 前言


在腾讯字节等其他大厂的面试中,JavaScript预编译是经常会被问到的问题,本文将带大家了解JS预编译中的具体过程


(二)编译执行步骤


传统编译语言编译步骤


对传统编译型语言来说,其编译步骤一般为:词法分析->语法分析->代码生成,下面让我们来分别介绍这3个过程。



  1. 词法分析


这个过程会将代码分隔成一个个语法单元,比如var a = 520;这段代码通常会被分解为vara=520这4个词法单元。



  1. 语法分析


这个过程是将词法单元整合成一个多维数组,即抽象语法树(AST),以下面代码为例


if(typeof a == "undefined" ){ 
a = 0;
} else {
a = a;
}
alert(a);

语法树.jpg


当JavaScript解释器在构造语法树的时候,如果发现无法构造,就会报语法错误(syntaxError),并结束整个代码块的解析。



  1. 代码生成


这个过程是将抽象语法树AST转变为可执行的机器代码,让计算机能读懂执行。


JavaScript编译步骤


比起传统的只有3个步骤的语言的编译器,JavaScript引擎要复杂的多,但总体来看,JavaScript编译过程只有下面三个步骤:
1. 语法分析
2. 预编译
3. 解释执行


(三)预编译详解


预编译概述


JavaScript预编译发生在代码片段执行前的几微秒(甚至更短!),预编译分为两种,一种是函数预编译,另一种是全局预编译,全局预编译发生在页面加载完成时执行,函数预编译发生在函数执行的前一刻。预编译会创建当前环境的执行上下文。


函数的预编译执行四部曲



  1. 创建Activation Object(以下简写为AO对象);

  2. 找形参和变量声明,将变量声明和形参作为AO的属性名,值为underfined;

  3. 将实参和形参值统一;

  4. 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体。


案例代码


//请问下面的console.log()输出什么?
function fn(a) {
//console.log(a);
var a = 123//变量赋值
//console.log(a);
function a() { }//函数声明
//console.log(a);
var b = function () { }//变量赋值(函数表达式)
//console.log(b);
function d() { }//函数声明
}
fn(1)//函数调用

根据上面的四部曲,对代码注解后,我们可以很轻松的知道四个console.log()输出什么,让我们来看下AO的变化



  1. 创建AO对象


AO{
//空对象
}


  1. 找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined;


AO{
a: undefined
b: undefined
}


  1. 将实参和形参值统一;


AO{
a: 1,
b: undefined
}


  1. 在函数体里找函数声明,将函数名作为AO对象的属性名,值赋予函数体。


AO{
a: function(){}
b: undefined
d: function(){}
}

最后,下面是完整的预编译过程


AO:{
a:undefined -> 1 -> function a(){}
b:undefined
d:function d(){}
}


全局的预编译执行三部曲



  1. 创建Global Object(以下简写为GO对象);

  2. 找形参和变量声明,将变量声明和形参作为GO的属性名,值为undefined;

  3. 在全局里找函数声明,将函数名作为GO对象的属性名,值赋予函数体。


案例代码


global = 100;
function fn() {
//console.log(global);
global = 200;
//console.log(global);
var global = 300;
}
fn();

根据全局预编译三部曲我们可以知道他的GO变化过程



  1. 创建GO对象


GO{
// 空对象
}


  1. 找形参和变量声明,将变量声明和形参作为GO的属性名,值为underfined


GO: {
global: undefined
}


  1. 在全局里找函数声明,将函数名作为GO对象的属性名,值赋予函数体


GO: {
global: undefined
fn: function() { }
}


注意这里函数声明会带来函数自己的AO,预编译过程继续套用四部曲即可



(四)总结


当遇到面试官问你预编译过程时,可以根据上面的内容轻松解答,同时面试时也会遇到很多问你console.log()输出值的问题,也可以用上面的公式获取正确答案。


作者:橙玉米
链接:https://juejin.cn/post/7030370931478364196

收起阅读 »

面试题:实现小程序平台的并发双工 rpc 通信

前几天面试的时候遇到一道面试题,还是挺考验能力的。 题目是这样的: rpc 是 remote procedure call,远程过程调用,比如一个进程调用另一个进程的某个方法。很多平台提供的进程间通信机制都封装成了 rpc 的形式,比如 electron 的 ...
继续阅读 »

前几天面试的时候遇到一道面试题,还是挺考验能力的。


题目是这样的:


rpc 是 remote procedure call,远程过程调用,比如一个进程调用另一个进程的某个方法。很多平台提供的进程间通信机制都封装成了 rpc 的形式,比如 electron 的 remote 模块。


小程序是双线程机制,两个线程之间要通信,提供了 postMessage 和 addListener 的 api。现在要在两个线程都会引入的 common.js 文件里实现 rpc 方法,支持并发的 rpc 通信。


达到这样的使用效果:


const res = await rpc('method', params);

这道题是有真实应用场景的题目,比一些逻辑题和算法题更有意思一些。


实现思路


两个线程之间是用 postMessage 的 api 来传递消息的:



  • 在 rpc 方法里用 postMessage 来传递要调用的方法名和参数

  • 在 addListener 里收到调用的时候,调用 api,然后通过 postMessage 返回结果或者错误


我们先实现 rpc 方法,通过 postMessage 传递消息,返回一个 promise:


function rpc(method, params) {
postMessage(JSON.stringify({
method,
params
}));

return new Promise((resolve, reject) => {

});
}

这个 promise 什么时候 resolve 或者 reject 呢? 是在 addListener 收到消息后。那就要先把它存起来,等收到消息再调用 resolve 或 reject。


为了支持并发和区分多个调用通道,我们加一个 id。


let id = 0;
function genId() {
return ++id;
}

const channelMap = new Map();

function rpc(method, params) {
const curId = genId();

postMessage(JSON.stringify({
id: curId,
method,
params
}));

return new Promise((resolve, reject) => {
channelMap.set(curId, {
resolve,
reject
});
});
}

这样,就通过 id 来标识了每一个远程调用请求和与它关联的 resolve、reject。


然后要处理 addListener,因为是双工的通信,也就是通信的两者都会用到这段代码,所以要区分一下是请求还是响应。


addListener((message) => {
const { curId, method, params, res}= JSON.parse(message);
if (res) {
// 处理响应
} else {
// 处理请求
}
});

处理请求就是调用方法,然后返回结果或者错误:


try {
const data = global[method](...params);
postMessage({
id
res: {
data
}
});
} catch(e) {
postMessage({
id,
res: {
error: e.message
}
});
}

处理响应就是拿到并调用和 id 关联的 resolve 和 reject:


const { resolve, reject  } = channelMap.get(id);
if(res.data) {
resolve(res.data);
} else {
reject(res.error);
}

全部代码是这样的:


let id = 0;
function genId() {
return ++id;
}

const channelMap = new Map();

function rpc(method, params) {
const curId = genId();

postMessage(JSON.stringify({
id: curId,
method,
params
}));

return new Promise((resolve, reject) => {
channelMap.set(curId, {
resolve,
reject
});
});
}

addListener((message) => {
const { id, method, params, res}= JSON.parse(message);
if (res) {
const { resolve, reject } = channelMap.get(id);
if(res.data) {
resolve(res.data);
} else {
reject(res.error);
}
} else {
try {
const data = global[method](...params);
postMessage({
id
res: {
data
}
});
} catch(e) {
postMessage({
id,
res: {
error: e.message
}
});
}
}
});

我们实现了最开始的需求:



  • 实现了 rpc 方法,返回一个 promise

  • 支持并发的调用

  • 两个线程都引入这个文件,支持双工的通信


其实主要注意的有两个点:



  • 要添加一个 id 来关联请求和响应,这在 socket 通信的时候也经常用

  • resolve 和 reject 可以保存下来,后续再调用。这在请求取消,比如 axios 的 cancelToken 的实现上也有应用


这两个点的应用场景还是比较多的。


总结


rpc 是远程过程调用,是跨进程、跨线程等场景下通信的常见封装形式。面试题是小程序平台的双线程的场景,在一个公共文件里实现双工的并发的 rpc 通信。


思路文中已经讲清楚了,主要要注意的是 promise 的 resolve 和 reject 可以保存下来后续调用,通过添加 id 来标识和关联一组请求响应。


作者:zxg_神说要有光
链接:https://juejin.cn/post/7030803556282155022

收起阅读 »

localStorage灵魂五问。 5M?? 10M !!!

灵魂五问 localStorage 存储的键值采用什么字符编码 5M 的单位是什么 localStorage 键占不占存储空间 localStorage的键的数量,对写和读性能的影响 写个方法统计一个localStorage已使用空间 我们挨个解答,之后给...
继续阅读 »

灵魂五问



  1. localStorage 存储的键值采用什么字符编码

  2. 5M 的单位是什么

  3. localStorage 键占不占存储空间

  4. localStorage的键的数量,对写和读性能的影响

  5. 写个方法统计一个localStorage已使用空间


我们挨个解答,之后给各位面试官又多了一个面试题。


我们常说localStorage存储空间是5M,请问这个5M的单位是什么?


localStorage 存储的键值采用什么字符编码?


打开相对权威的MDN localStorage#description



The keys and the values stored with localStorage are always in the UTF-16 DOMString format, which uses two bytes per character. As with objects, integer keys are automatically converted to strings.



翻译成中文:



localStorage 存储的键和值始终采用 UTF-16 DOMString 格式,每个字符使用两个字节。与对象一样,整数键将自动转换为字符串。



答案: UTF-16


MDN这里描述的没有问题,也有问题,因为UTF-16,每个字符使用两个字节,是有前提条件的,就是码点小于0xFFFF(65535), 大于这个码点的是四个字节。


这是全文的关键。


5M 的单位是什么


5M的单位是什么?


选项:



  1. 字符的个数

  2. 字节数

  3. 字符的长度值

  4. bit 数

  5. utf-16编码单元


以前不知道,现代浏览器,准确的应该是 选项3,字符的长度 ,亦或 选项5, utf-16编码单元


字符的个数,并不等于字符的长度,这一点要知道:


"a".length // 1
"人".length // 1
"𠮷".length // 2
"🔴".length // 2

现代浏览器对字符串的处理是基于UTF-16 DOMString


但是说5M字符串的长度,显然有那么点怪异。


而根据 UTF-16编码规则,要么2个字节,要么四个字节,所以不如说是 10M 的字节数,更为合理。


当然,2个字节作为一个utf-16的字符编码单元,也可以说是 5M 的utf-16的编码单元。


我们先编写一个utf-16字符串计算字节数的方法:非常简单,判断码点决定是2还是4


function sizeofUtf16Bytes(str) {
var total = 0,
charCode,
i,
len;
for (i = 0, len = str.length; i < len; i++) {
charCode = str.charCodeAt(i);
if (charCode <= 0xffff) {
total += 2;
} else {
total += 4;
}
}
return total;
}

我们再根绝10M的字节数来存储


我们留下8个字节数作为key,8个字节可是普通的4个字符换,也可是码点大于65535的3个字符,也可是是组合。


下面的三个组合,都是可以的,



  1. aaaa

  2. aa🔴

  3. 🔴🔴


在此基础上增加任意一个字符,都会报错异常异常。


const charTxt = "人";
let count = (10 * 1024 * 1024 / 2) - 8 / 2;
let content = new Array(count).fill(charTxt).join("");
const key = "aa🔴";
localStorage.clear();
try {
localStorage.setItem(key, content);
} catch (err) {
console.log("err", err);
}

const sizeKey = sizeofUtf16Bytes(key);
const contentSize = sizeofUtf16Bytes(content);
console.log("key size:", sizeKey, content.length);
console.log("content size:", contentSize, content.length);
console.log("total size:", sizeKey + contentSize, content.length + key.length);

现代浏览器的情况下:


所以,说是10M的字节数,更为准确,也更容易让人理解。


如果说5M,那其单位就是字符串的长度,而不是字符数。


答案: 字符串的长度值, 或者utf-16的编码单元


更合理的答案是 10M字节空间。


localStorage 键占不占存储空间


我们把 key和val各自设置长 2.5M的长度


const charTxt = "a";
let count = (2.5 * 1024 * 1024);
let content = new Array(count).fill(charTxt).join("");
const key = new Array(count).fill(charTxt).join("");
localStorage.clear();
try {
console.time("setItem")
localStorage.setItem(key, content);
console.timeEnd("setItem")
} catch (err) {
console.log("err code:", err.code);
console.log("err message:", err.message)
}

执行正常。


我们把content的长度加1, 变为 2.5 M + 1, key的长度依旧是 2.5M的长度


const charTxt = "a";
let count = (2.5 * 1024 * 1024);
let content = new Array(count).fill(charTxt).join("") + 1;
const key = new Array(count).fill(charTxt).join("");
localStorage.clear();
try {
console.time("setItem")
localStorage.setItem(key, content);
console.timeEnd("setItem")
} catch (err) {
console.log("err code:", err.code);
console.log("err message:", err.message)
}

image.png


产生异常,存储失败。 至于更多异常详情吗,参见 localstorage_功能检测


function storageAvailable(type) {
var storage;
try {
storage = window[type];
var x = '__storage_test__';
storage.setItem(x, x);
storage.removeItem(x);
return true;
}
catch(e) {
return e instanceof DOMException && (
// everything except Firefox
e.code === 22 ||
// Firefox
e.code === 1014 ||
// test name field too, because code might not be present
// everything except Firefox
e.name === 'QuotaExceededError' ||
// Firefox
e.name === 'NS_ERROR_DOM_QUOTA_REACHED') &&
// acknowledge QuotaExceededError only if there's something already stored
(storage && storage.length !== 0);
}
}

答案: 占空间


键的数量,对读写的影响


我们500 * 1000键,如下


let keyCount = 500 * 1000;

localStorage.clear();
for (let i = 0; i < keyCount; i++) {
localStorage.setItem(i, "");
}

setTimeout(() => {
console.time("save_cost");
localStorage.setItem("a", "1");
console.timeEnd("save_cost");
}, 2000)


setTimeout(() => {
console.time("read_cost");
localStorage.getItem("a");
console.timeEnd("read_cost");

}, 2000)

// save_cost: 0.05615234375 ms
// read_cost: 0.008056640625 ms

你单独执行保存代码:


localStorage.clear();    
console.time("save_cost");
localStorage.setItem("a", "1");
console.timeEnd("save_cost");
// save_cost: 0.033203125 ms

可以多次测试, 影响肯定是有的,也仅仅是数倍,不是特别的大。


反过来,如果是保存的值表较大呢?


const charTxt = "a";
const count = 5 * 1024 * 1024 - 1
const val1 = new Array(count).fill(charTxt).join("");

setTimeout(() =>{
localStorage.clear();
console.time("save_cost_1");
localStorage.setItem("a", val1);
console.timeEnd("save_cost_1");
},1000)


setTimeout(() =>{
localStorage.clear();
console.time("save_cost_2");
localStorage.setItem("a", "a");
console.timeEnd("save_cost_2");
},1000)

// save_cost_1: 12.276123046875 ms
// save_cost_2: 0.010009765625 ms

可以多测试很多次,单次值的大小对存的性能影响非常大,读取也一样,合情合理之中。


所以尽量不要保存大的值,因为其是同步读取,纯大数据,用indexedDB就好。


答案:键的数量对读取性能有影响,但是不大。值的大小对性能影响更大,不建议保存大的数据。


写个方法统计一个localStorage已使用空间


现代浏览器的精写版本:


function sieOfLS() {
return Object.entries(localStorage).map(v => v.join('')).join('').length;
}

测试代码:


localStorage.clear();
localStorage.setItem("🔴", 1);
localStorage.setItem("🔴🔴🔴🔴🔴🔴🔴🔴", 1111);
console.log("size:", sieOfLS()) // 23
// 🔴*9 + 1 *5 = 2*9 + 1*5 = 23

html的协议标准


WHATWG 超文本应用程序技术工作组 的localstorage 协议定了localStorage的方法,属性等等,并没有明确规定其存储空间。也就导致各个浏览器的最大限制不一样。


其并不是ES的标准。


页面的utf-8编码


我们的html页面,经常会出现 <meta charset="UTF-8">
告知浏览器此页面属于什么字符编码格式,下一步浏览器做好解码工作。


<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>容器</title>
</head>

这和localStorage的存储没有半毛钱的关系。


localStorage扩容


localStorage的空间是 10M的字节数,一般情况是够用,可是人总是有贪欲。
真达到了空间限制,怎么弄?


localStorage扩容就是一个话题。


作者:云的世界
链接:https://juejin.cn/post/7030585901524713508

收起阅读 »

(转载)网文也要元宇宙!虚拟看书,这也行?

“元宇宙”概念股热度持续。此刻,已经包括了来自网文领域的元宇宙。例如中文在线,自从拉上和元宇宙的关系,就一度飞涨。从10月底至今,中文在线在投资者互动平台上发布了超过6000字的与元宇宙相关的内容。面投资者对于其蹭热点的质疑,中文在线11月9日在投资者互动平台...
继续阅读 »

“元宇宙”概念股热度持续。

此刻,已经包括了来自网文领域的元宇宙。

例如中文在线,自从拉上和元宇宙的关系,就一度飞涨。

从10月底至今,中文在线在投资者互动平台上发布了超过6000字的与元宇宙相关的内容。

面投资者对于其蹭热点的质疑,中文在线11月9日在投资者互动平台表示,“公司已申请如中文元宇宙、IP元宇宙、17K元宇宙、四月天元宇宙等商标。”

中文在线表示,“借助5G、AI、AR/VR等技术的发展,公司的沉浸式互动阅读将会借助技术的赋能加速延展,构建一个互动性更强的平行世界。作为数字内容公司,本质是生产内容、打造IP、连接人群、丰富娱乐、学习和社交。文字创作和阅读仅存在于2D,但随着元宇宙的到来通过AR/VR等技术可以让人沉浸式体验新世界,而公司拥有海量的库存将会有力地支持构建平行的互动阅读世界。”

据了解,中文在线2000年成立于清华大学,2015年1月21日在深交所创业板上市,成为中国“数字出版第一股”。

其拥有数字内容资源超过460万种,签约版权机构600余家,签约知名作家、畅销书作者2000余位。

旗下拥有17K小说网、四月天小说网、汤圆创作三大网络文学原创平台,驻站网络作者超过390万名。

作为网络文学公司,除中文在线外,阅文集团、掌阅科技均未披露与元宇宙相关的信息。

而海量IP支持构建平行的互动元宇宙场景又是否立得住脚呢?

对此,《华夏时报》记者于玉金、《南方都市报》记者张洁莹分别和书乐进行了一番交流。

贫道以为:

中文在线目前而言,技术实力尚不足以支撑起以VR为底色的元宇宙。

但宣布进军元宇宙,某种意义上是表现出自身的目标与追求,后期则看是否投入海量资金进行研发,才能确定是实干还是蹭热点。

整个资本市场上的元宇宙热,则可以视作一个爆炒蹭热点,毕竟元宇宙真正要形成战斗力,至少还要十年。

事实上,这种概念炒作并不新鲜。

此前每一轮风口,都会有各种游资散户进入和蹭一波热点,尤其是在互联网科技巨头集体追捧的大风口下。

但这一波风口本身需要强大的技术支撑,而非此前O2O、比特币那种低门槛、以人力驱动的概念。

因此,随着市场对云宇宙的认知更加深刻,这样的蹭热点行径因为能量不足和,势必消歇。

事实上,所有互联网科技和游戏公司,都可以和元宇宙有关,不足为奇。

所以宣布进军元宇宙,哪怕未来只是在别人的元宇宙里开个“书店”,也是一种进军。

当然,此刻之所以蹭热点有用,还在于,元宇宙包含广阔,而且还处在待开发的史前时代,因此,任何方向进击都是可能的路径。

也因此,此前9月份,元宇宙概念也曾一度火热,遭遇市场降温后现下热度重燃,就在于真的巨头开始进击,如facebook、微软和其他。

真正互联网科技巨头本身认知到元宇宙必然是未来的大趋势(名称不重要,存在形式才是关键点),纷纷进击之后,导致了死灰复燃。

不过,贫道判断,由于技术门槛过高,蹭热点者很快会知难而退,让热度在年底前被新的热点所取代。

作者:张书乐(人民网、人民邮电报专栏作者,互联网和游戏产业观察者)

原文链接:https://new.qq.com/omn/20211115/20211115A06C8Z00.html

收起阅读 »

(转载)元宇宙的经济逻辑

继脸书(Facebook)把自己公司的名字改成了Meta之后,一贯对新概念保持低调态度的微软也终于放下了矜持,高调地加入了元宇宙的开拓大军。有意思的是,微软带给元宇宙的第一款产品叫做Mesh for Microsoft Teams。这是个什么样的产品呢?微软官...
继续阅读 »

继脸书(Facebook)把自己公司的名字改成了Meta之后,一贯对新概念保持低调态度的微软也终于放下了矜持,高调地加入了元宇宙的开拓大军。有意思的是,微软带给元宇宙的第一款产品叫做Mesh for Microsoft Teams。这是个什么样的产品呢?微软官方给出的说明是,这款产品要实现的主要是混合现实功能,把办公协作功能带入元宇宙。也就是说,现在人们在元宇宙也能用Excel制表、用PPT做幻灯片了!看着微软的这款新产品,打工人们不禁感叹:果然是“打工人,打工魂”,就算是躲进了元宇宙,终究还是逃不开打工的宿命啊!

脸书、微软对于元宇宙的青睐当然不是个例。事实上,自从元宇宙的概念爆火之后,就不断有公司宣称自己是元宇宙公司。例如,很多原来做VR的企业就说自己做的是元宇宙,因为人们通过他们的VR眼镜就能看到区别于现实的另一个世界;很多原来做游戏的企业也说自己做的是元宇宙,因为在玩游戏时,人的精神就会飘移到游戏所创造的新世界,而且还是“沉浸式”的。我不知道这些公司在进行类似的宣传时是否相信自己的说辞,但至少我身边的大多数朋友对这些说法的反应是:“就这?”

那么,问题出在哪儿呢?为什么这些企业确实在某些意义上把人们带入了一个新的世界,但人们却直观地认为,这些并不足以作为一个元宇宙呢?答案很简单——很多自我标榜为元宇宙的项目其实都有形无实。

从构词法上看,元宇宙是Meta和Verse的合体词。Meta是希腊语中超越的意思,而Verse则是宇宙(Universe)的简写。很显然,这样的构词结果已经明明白白地告诉了我们,元宇宙要成其为元宇宙,就必须满足两个层面的要求:一是要超越;二是要复刻。

所谓超越,要求的就是元宇宙必须要和现实世界不一样,在元宇宙里面,人们一定要能够做到某些现实世界中做不到的事情。而所谓复刻,指的则是需要在元宇宙里面建立起与现实世界类似的社会运行逻辑。从具体的设计上看,元宇宙的规则不必和现实世界完全一致,否则元宇宙也就失去了它存在的必要。但是,决定现实世界运行的一些关键逻辑和规则必须要在元宇宙中找到对应,否则人们就很难将元宇宙认可为一个真正的世界。

而从操作上看,建立一套与现实世界对应的运行逻辑,恐怕要比创造出各种令人着迷的VR特效难上不知道多少个数量级。从这个意义上看,如果我们要想创造出一个真正意义上的元宇宙,最艰巨的挑战,恐怕还是在元宇宙运行规则的复刻上。

元宇宙的运行规则可能是多层次的。在诸多的规则当中,最重要的应该是其中的经济逻辑。那么,在未来可能到来的元宇宙当中,经济逻辑究竟会怎样?或者说,它们应该怎样?抱着这些问题,我们不妨来作一番畅想吧。

稀缺的构建

在思考元宇宙的经济逻辑之前,我们不妨先对现实世界中的经济逻辑进行一些思考。在现实世界中,经济逻辑的起点是什么?似乎每一本关于经济学的教科书都有不同的回答。有的说是效用,有的说是价格,也有的说是产权。在我看来,其实以上所有这些概念都可以从一个更为原初的概念——“稀缺”(Scarcity),里面衍生出来。

所谓“稀缺”,指人欲望的无限性和现实条件有限性之间的矛盾。我们每个人都有各种各样的欲望,并且这种欲望是无限膨胀的。没吃饱时想吃饱,吃饱了后想吃好,吃得好了,又想穿得更好……欲望的膨胀总是无穷无尽。但相比于欲望,现实条件总是有限的,随心所欲的“买买买”很快就会把我们的钱袋掏空。面对有限的预算条件,就必须收敛无限的欲望。在进行购物决策时,我们就要给所有的欲望排一个序,确认出哪个更重要,而哪个相对次要一些,哪个要先买,哪个则可以先放一放。

在经济学的教科书里面,这个心理的排序被抽象出来,就是效用的概念。由于各种物品都可以给人带来各种各样的效用,因而人们就会对它们进行争夺。这时,谁拥有某物,而谁不拥有,谁可以支配某物,而谁不可以等问题就变得很重要了。于是,产权的概念也就应运而生。有了产权,人们就可以定分止争,从而免于为获取某物而诉诸暴力。这时,市场的手段,也就是用自己拥有的去换取自己所想要的,就成了配置资源的最主要方式。如果一个人是理性的,那么他在购买商品时总会不断在自己的所欲与所有之间精打细算。他们需要尽力地在市场上讨价还价,以便让自己所占有的东西能够尽可能换到更多自己所要的东西。通过市场上无数个人的交换,就形成了对世间万物的供给和需求,而供给和需求一起,就决定了世间万物的价格。

通过以上的简单推演,我们可以看到,无论是效用、价格,还是产权,其实都可以还原到稀缺这个概念。在现实的世界为什么会有稀缺,这一点似乎是不言自明的,因为物理的规律决定了这一切,原子化的世界决计不可能支撑起我们所有的欲望。但是在元宇宙,稀缺这个概念本身可能就会成为一个问题。元宇宙是一个比特的世界,里面的万事万物,归根到底就是一串代码,都可以简单地修改代码来得到。想要什么,就可以有什么。即使是在真实世界里千金难买的时间,人们也可以调整对于脑部的刺激来实现。在这样的情况下,稀缺本身可能就不存在了。

那么,在元宇宙当中,真的不会有稀缺了吗?情况当然不会是这样。事实上,即使在元宇宙,稀缺也会存在。而且,它必须有。“虚拟经济学”(virtual economics)领域的先驱、美国印第安纳大学教授爱德华·卡斯特罗诺瓦(Edward Castronova)曾经对数字条件下稀缺性存在的必然性给出过一个解释。他认为,稀缺性的存在,其实是人们为了提升在虚拟世界中的体验而作出的一种人为设定。从人性上看,我们每个人都喜欢拥有自己的个性,而拥有差异化的物品,就是个性在外界的一种投射。试想,如果所有的人只能够吃一样的东西、穿一样的衣服、住一样的房子,那么这个世界将是多么无聊啊。正是由于这个原因,即使从技术上看,人们完全可以在虚拟世界中获得任何自己想要的东西,他们也必须人为地制造出差异化和稀缺来。

应该说,卡斯特罗诺瓦的以上观点是颇具吸引力的。作为特殊的虚拟世界,在元宇宙当中,这个逻辑当然也依然成立。不过,在我看来,除了这个理由之外,在元宇宙当中设定稀缺性,其实还有一个重要的理由,就是要确立起人们建设元宇宙的激励基础。

在大多数的想象版本中,元宇宙都不是一个一次性整体成型的世界。相反,和现实世界一样,它本身可以不断地演化。而这种演化,则需要参与其中的每一个人的共同努力。例如,在一些具有元宇宙要素的游戏当中,整个世界的搭建需要所有人的协力,大家配合得越好,游戏中的世界就发展越好。而对于一些更为开放式的元宇宙,则不仅需要参与者在元宇宙内部通力合作,更需要他们在游戏之外改进游戏的代码,甚至更新硬件。参与者奉献得越多,这个元宇宙发展得也就越好。在这种情况下,如何激励好每一个参与者,让他们保持充足的奉献精神就成了一个问题。

经验告诉我们,在合作参与的人数较少时,要协调每一个人的激励是相对容易的。在大多数时候,人们依靠自己内心的道德准则,或者“为爱发电”的冲动,就可以解决所有问题。然而,当合作规模扩大的时候,单纯依靠自愿和利他的道德冲动就很难持久维系每一个合作者的参与热情。例如,互联网早年的很多论坛,在规模发展到一定程度之后,就会陷入混乱或者沉寂。因此,要维护社区的长期活力,就必须能够构建起一套相应的激励,让每一个人的付出都能获得相应的回报。为了达到这个目的,人们就需要在合作中引入能够被用于激励的价值,而为了实现这一点,就必须首先创造出它的逻辑起点,也就是稀缺。

尽管看起来有点不可思议:在类似元宇宙这样的一个虚拟世界,稀缺并非像真实世界那样,源自于物理规律的限制,而是来自于人们的建构,但现实确实是如此。事实上,现在人们所做的很多工作,就是试图在元宇宙当中构建起稀缺性。例如,现在的数字水印、数字权利管理(Digital Rights Management,简称DRM),以及非同质化通证(Non-Fungible Token,简称NFT)等重要技术,其实都是为了构建稀缺的技术。以前一段时间十分火爆的NFT为例,很多人都认为,这将是支撑未来元宇宙发展的一项关键技术。但是,NFT究竟有什么用呢?究其本质,就是它可以在元宇宙内创造出差异化、创造出稀缺。在元宇宙当中,人们完全可以对数码造物实现无限的复制,稀缺本来可以不存在。而借助于NFT技术,每一个物品都可以被打上独有的标签,或者赋予特殊的涵义,从而成为独一无二的东西。这样一来,稀缺就被制造了出来。

价值的确定

有了稀缺性之后,元宇宙经济系统就有了自己的逻辑起点。从理论上看,一切在现实世界中有的经济概念,都可以随之演化出来。

和现实中一样,人们在元宇宙当中通过不断地交互,可以逐步摸索出各种物品的相对价值。类似的实践,其实已经可以在不少大型的网络游戏中看到了。事实上,早在十多年前,“远古”游戏《暗黑破坏神2》中就自发演化出了一个价值交换体系。游戏中的一种符文被玩家作为了刻画价值的一般等价物,被人们用来交换各种道具。这里的交换策略并不是游戏开发者给定的,但大批的玩家在长期的交互过程中就形成了一种约定俗成的规律。而在现在有元宇宙概念的游戏当中,由于一般都引入了通证(Token)体系,所以这种价值的自发演化就会变得更快。在游戏中,一件物品可以值多少个通证,可以和其他的什么物品进行交换,都可以在自发当中被安排得明明白白。我想,游戏既然如此,未来更大规模的元宇宙实践当然也可以实现类似的过程。

和现实世界不同的是,在元宇宙的经济系统发展之初,现实的世界就已经是一个前定的存在了。因此,现实世界对元宇宙的影响可能会成为元宇宙价值决定的一个重要影响因素。

事实上,现在很多元宇宙当中的资产都是直接通过直接拍卖来进行初次配置的。比如,在Axie Infinity、Decentraland、Sandbox等有元宇宙概念的游戏当中,就都有土地拍卖的概念。参与其中的玩家可以像参与真实世界的土地拍卖一样,购买虚拟世界当中的地产,而荷式拍卖则是实现这种交易的最重要手段。从交易的结果来看,这些虚拟土地的价格通常都价格不菲。例如,2021年6月,Axie Infinity的9块虚拟土地以888.25以太坊(ETH)的高价出售,根据以太坊当时的价格,这批虚拟土地的成交价格约为150万美元;而2021年7月,Sandbox上面积超过530万“平方米”(注:这里的一个平方米指的是一个24*24的点阵)的虚拟土地以近88万美元的价格出售。

很显然,通过上述的拍卖,元宇宙中物品的价值就可以很容易地与现实世界建立一定的锚定。而根据每种物品与现实世界之间的价值比值,这些物品在元宇宙内部的交换价值也就可以更容易被确定了。

货币的引入

当各种物品的价值被确定后,元宇宙内的交易就可以开展起来了。当然,就像在现实世界一样,当交易的规模扩展到一定程度之后,它就不可能持续地以一种以货易货的形式存在了,基于货币的交易将会成为发展的必然。

那么,在元宇宙当中,货币会采用一种什么样的形式呢?我想,要思考这个问题,不妨先看一下现实世界当中的货币是怎么演化的。马克思在《资本论》当中曾经有一句名言,叫做“金银天生不是货币,但货币天生是金银”。这是什么意思呢?大致上就是讲,金银天生具有能够充当固定的一般等价物,也就是货币的各种良好性质:它们不仅质地均匀,容易分割,价值稳定,而且便于携带——虽然金银本身分量并不轻,但由于它们代表的价值很高,所以在现实中,人们携带很轻的一部分金银就足以满足大部分的交易需要了——用更为现代的经济学语言讲,就是用金银来充当货币,是可以最大幅度降低交易成本的。当然,后来随着交易规模的不断扩大,金银也不再适合作为流通货币存在于市场上了。于是人们就发明了纸币,而金银则作为储备,用以支撑纸币的价值。

参考现实世界中的货币演化,我们就可以重新来思考元宇宙中的货币问题。在元宇宙中,如果有货币,那么它本身就是以数字形态存在的,它的质地、分割等,显然不是问题。问题的关键是,它到底能否有效保证价值稳定,以及能否有效地达到节约交易成本的目的。在审视可行的方案时,这两个问题就可以作为检验的指标。

一些人认为,在元宇宙当中,以比特币为代表的加密货币可能会扮演货币的角色。我对此是有些怀疑的。一方面,加密货币的币值太不稳定,这很难满足作为流通货币的需要。另一方面,至少从目前看,加密货币的交易效率非常低。以比特币为例,其设计要求每完成一笔交易就需要对全网进行广播验证,因而每笔交易都需要花费很长的时间,花费很大的算力。从这个意义上看,要用加密货币来直接作为元宇宙的货币恐怕并不合适——尤其是当这个元宇宙的规模比较大时,这种设定的效率就会非常低。

一种或许更为可取的方式是,在每一个元宇宙内部都开发独立的通证。为了实现交易的效率,这些通证未必需要和比特币一样建筑于区块链技术之上。而为了保证币值的稳定,这些通证可以采用某些资产锚定,以资产作为储配的方式来发行。这里的资产可以是现实世界当中的货币,也可以是一篮子加密货币,选取的标准应当以它们有相对稳定的价值为标准。这样,元宇宙内的货币体系就可以建立起来了。

各种设计必然是有利也有弊的。如果使用了区块链技术来作为支持,尽管交易效率较低,但交易的安全性却可以获得比较好的保证。而反过来,如果放弃了直接使用区块链技术,那么交易的效率固然高,但交易的安全性则可能会受到影响。考虑到这点,我们或许可以引入一种抽检制度,在所有的交易当中按照一定比例抽取部分交易作为检查,一旦发现交易有造假,则给予重罚。从理论上讲,只要处罚设置足够高,就可以有效抑制人们的造假动机——这种惩罚策略在现实世界当中或许不成立,但在元宇宙这样的虚拟世界,可能就会更加适应。通过引入这样的机制,我们就可以在让交易安全达到足够程度保证的基础上,尽可能实现交易的高效率。

需要指出的是,当把元宇宙内部的通证和外部的资产直接挂钩时,也会引起一些副作用。因为在这种设定下,就相当于取消了元宇宙的货币发行独立性。如果有人将大批外部资产(如美元、人民币)兑换为元宇宙内部的通证时,元宇宙就可能面临急速的通货膨胀,并伴随着巨大的分配失衡。这就好像在一个网游当中,一下子引入了一批满身神装的“RMB玩家”,那么其他玩家的游玩体验就会大幅降低。考虑到这种情况,或许可以设计一个机制,当外部的货币流入达到一定值后,就启动“汇率”的浮动,让元宇宙内的通证对外补货币升值。通过这种机制,就可以比较好的保证在元宇宙内部货币价值的稳定性。

生产要素的交易

下面讨论一下元宇宙的生产要素交易。在现实世界,劳动、资本和土地是最为重要的生产要素。那么,元宇宙当中,有哪些是重要的生产要素呢?在我看来,比较关键的可能有两样,一是劳动,二是算力。至于元宇宙中所谓的“土地”,正如前面所说的,它们更多是一种人为构建出来的稀缺资源,如果有了足够的劳动和算力,它们本身是可以被构建出来的。由于这个原因,我更愿意把它作为一种一般意义上的商品来看待,而不认为它是一种独立的生产要素。

与元宇宙相关的劳动可以分为很多种:

第一种是在元宇宙经济体系内的劳动。作为一个虚拟的空间,元宇宙的价值很大程度上取决于其给人的体验。而为了保证这种体验,元宇宙中就需要安排一些专门用于和人交互的NPC。当然,这种NPC由谁来当,就是一个选择。一个方案找一些AI来当NPC。但是,从现在看,这些AI给人的交互体验绝对达不到《失控玩家》里面那样的水平,因而很难满足人们的需要。而另一个方案,就是专门找一些人来扮演NPC。如果采用这种方案,那么NPC和人的交互活动就形成了一种劳动。和真实世界当中一样,这样的劳动也需要得到报偿。

除此之外,在元宇宙当中,很多任务可能是需要多人协同完成的。例如,在被称为“元宇宙第一股”的Roblox游戏中,人们就需要一起建设社区,一起建设城市。这种共同的建设如果是出于所有玩家自愿的,那么这就是一种协作。但如果这种共同建设是某人要求其他人做的,那么它就成为了一种劳动的雇佣关系。这个时候,城市的建设也就成了一种在元宇宙内的打工劳动。

第二种是支撑元宇宙的劳动。如前所述,元宇宙要运转好,需要很多相应的技术支撑。比如,程序的底层需要有人开发,bug需要有人来处理,这些劳动,尽管不发生在元宇宙内部,但它们对元宇宙的发展却是必不可少的。

第三种则是发生在元宇宙内部的劳动。例如,真实世界的打工人转战元宇宙,在里面用微软的MeshforMi-crosoftTeams写Word,做PPT,然后拿着PPT开会。这些活动只是发生在元宇宙内部,但是从本质上来讲,它们依然是真实世界劳动的延伸。

对于以上三类劳动,第一种毫无疑问应该用元宇宙内部的通证来激励。事实上,在AxieInfinity等具有元宇宙概念游戏中,已经提出了“玩中赚”(play toearn,简称P2E)的概念。在这些游戏中,玩家或可以根据其游戏的时长来获取相应的通证,或可以通过打怪升级来获得独有的装备NFT,所有的这些,都可以作为他们的报酬。值得一提的是,在AxieInfinity上,有不少玩家都是因疫情而失业的人。对于他们来讲,P2E就成了获得收入的一个重要来源。

除了第一类劳动,第二、三类劳动严格来说都是元宇宙之外的,因而他们的报酬可以通过真实世界的货币,也可以通过通证来结算。当然,从促进元宇宙的发展来说,以通证结算或许是比较有利的。这可以促使人们以更高的频率使用元宇宙,从而可以从多个方面促进其发展。

至于元宇宙发展所需要的算力,则可以通过仿照比特币网络的做法,以工作量证明来分配一定的通证作为回报。当然,在现实中,为了吸引普通用户进行分布式的算力供应,也有一些产品试图将算力的供应包装成某种形式的游戏。这样,人们就可以在游戏当中就实现了算力提供,并且同时获得了相应的报酬。

“跨宇宙”的价值交换

从构建看,元宇宙绝对不可能是一蹴而就的,它更可能以一个个独立项目的形式发展起来——这就好像互联网的发展并不是源自于一个统一的设计,而是源自于一个个独立建立的网站。不过,就像一个个孤立的网站并没有价值一样,如果每一个元宇宙项目都是彼此独立的,而不能彼此联通,那么它们的价值也终将是有限的。唯有把各个元宇宙打通,让人们在它们之间自由穿梭,元宇宙的价值才能得到真正的体现。

然而,一旦我们要让彼此独立发展的元宇宙实现互联互通,就会涉及很多的问题。这些问题中,除了技术的困难外,还有一些经济问题恐怕是需要关心的:由于各个元宇宙项目是独立打造的,其建造者和建造规范各不相同,那么它们之间的通证价值如何兑换?一个人在A宇宙当中拥有的财富,应该通过什么比例折算到B宇宙?

如果从现实世界的经济理论看,这会是一个十分复杂的汇率决定问题。所幸的是,随着去中心化金融(DeFi)的发展,这个问题的解决就有了很好的解决基础。现在,借助于Uniswap等DeFi产品,人们已经可以很容易在不同的区块链项目之间实现兑换。所有的一切,程序都可以根据供求状况,通过可编程(Programmable)的方式来实现。在未来,这套系统应该可以被用到跨元宇宙的通证兑换当中去。不过,在应用到类似系统时,我们也需要对DeFi产品的运行风险做好监管和预防。比如,在现实当中,已经有人利用不同区块链产品之间的设计差异,综合使用多种DeFi产品来设计出了套利方案,公然套取了大量的财富。对于类似的情况,应当要做好提前的预防工作。否则,一不小心就可能酿成“跨宇宙”的金融风险,其影响可能是十分巨大的。

元宇宙治理和分配政策

通过以上的分析,我们已经可以大致看到一个类似于现实世界的元宇宙经济运作框架。但是,和现实世界一样,元宇宙也会遭遇很多的经济和社会问题。在现实世界中,很多问题可以诉诸于政府来进行解决,而在元宇宙当中,可能没有类似现实世界当中这样强力的政府组织,因此各种问题的解决恐怕需要依靠元宇宙参与者的自发治理来实现。

从现阶段看,区块链上“分布式自主组织”(DAO)的治理实践或许可以为未来的元宇宙治理提供借鉴。DAO有很多重要的特点,例如,它具有自动化执行的统一规则,具有很强的透明度,权益相关者都可以表达自己的利益诉求,并且可以通证来对参与者进行有效的激励。实践证明,只要DAO的设计比较得当,就可以充分地调动各方面的积极性,比较好地达成组织治理的目的。我想,在元宇宙的场景下,类似的经验也可以得到有效的复制。

这里需要指出的是,一个组织的有效治理,必须是以地位平等为前提的。在现实中,随着贫富分化的日益加深,人与人之间的经济地位会形成重大的差异。而经济地位上的差异则会反过来影响政治参与的热情——事实上,很多国家的实践都告诉我们,赤贫的人是几乎没有参政的积极性的。而反过来,由于他们的声音无法被听到,诉求无法被表达,因而也就很难被政府关注,其境况也就很难得到改善。和现实类似,在元宇宙当中,人与人之间也可能会形成贫富分化,那么如何类似以上的现象出现呢?我想,一个可能的做法是制定最低收入制度,为所有人发放能够在元宇宙生存的基本收入。唯有如此,才可能调动哪些元宇宙当中那些“穷人”的积极性,让他们也参与到对于元宇宙的治理当中来。

结语

好了,我想是时候结束这次对于元宇宙经济逻辑的畅想了。尽管我自认为在进行以上畅想时已经秉承了比较严谨的态度,但我几乎可以肯定,未来的元宇宙发展几乎不可能和上面的推测一样。

现实永远要比想象更为精彩!

原文链接:https://finance.ifeng.com/c/8BBhOmKdq9Z

收起阅读 »

(转载)10个问题说清楚,什么是元宇宙

作者: 赵颖2021-11-12 19:14英伟达、微软、阿里云早已布局,建立起元宇宙的底座?区块链、NFT将迎来怎样的发展?去中心化又将如何实现?人们对于元宇宙的构想十分多元且抽象,这十个问题将抽象的元宇宙具象化,帮助人们更好地理解。一、什么是元宇宙?1)元...
继续阅读 »


作者: 赵颖
英伟达、微软、阿里云早已布局,建立起元宇宙的底座?区块链、NFT将迎来怎样的发展?去中心化又将如何实现?

人们对于元宇宙的构想十分多元且抽象,这十个问题将抽象的元宇宙具象化,帮助人们更好地理解。

一、什么是元宇宙?

1)元宇宙概念的提出

元宇宙在很长一段时间内仅存在于文学与影视作品中。元宇宙(Metaverse)由Meta和Verse两个词根组成,Meta表示“超越”“元”,verse表示“宇宙Universe”。Metaverse一词最早来自1992年的科幻小说《雪崩》。小说描绘人们在虚拟现实世界中通过控制自己的数字化身相互竞争以提升社会地位。在其后的接近30年间,元宇宙的概念在《黑客帝国》《头号玩家》《西部世界》等影视作品,《模拟人生》等游戏中有所呈现。在这一阶段,元宇宙的概念比较模糊,更多地被理解为平行的虚拟世界

2)元宇宙八大要素

根据元宇宙概念上市公司Roblox的定义,元宇宙应具备身份、朋友、沉浸感、低延迟、多元化、随地、经济系统、文明等八大要素。元宇宙的表现形式大多以游戏为起点,并逐渐整合互联网、数字化娱乐、社交网络等功能,长期来看甚至可以整合社会经济与商业活动。

3)元宇宙第一股Roblox上市

2021年3月10日,Roblox采取直接挂牌模式(DPO)在纽约证券交易所上市。Roblox认为元宇宙用于描述虚拟宇宙中持久的、共享的、三维虚拟空间的概念。

4)元宇宙总结了前期科技的发展方向

东方证券分析师张颖指出,在迸发元宇宙概念前,5G基础设施、用于智能终端的显示屏、AI芯片等技术不断发展演进,同时工业互联网、产业互联网、数字孪生、VR游戏等概念均不断成熟。而在元宇宙概念诞生后,可以很好地总结这一时期大部分技术的发展方向。

二、元宇宙的架构?

东方证券认为,元宇宙的载体与内容这两个概念十分宽泛,主要分三部分:

  • 元宇宙的底层由基础设施与终端硬件设备组成:包括但不限于人机交互、3D引擎、GIS、设计工具、游戏渲染、画面渲染、隐私计算、AI、操作系统、工业互联网、内容分发、应用商店以及智能合约;
  • 在此基础上,元宇宙还需要大量的软件与技术协同:包括但不限于:基础设施端的5G、6G、云计算、区块链节点、边缘计算节点、DPU;用户端的路由器、传感器、芯片、VR头显、显示器、脑机接口;
  • 基于此,元宇宙可以衍生出相应的应用,并基于元宇宙各类应用发展出潜在的内容载体。

三、元宇宙的发展模式?

1)元宇宙的发展是循序渐进的过程,技术端、内容端、载体端都在不断演变

  • 技术端,区块链技术在不断演进,以以太坊为代表的社区在探索区块链应用如何丰富化,以Coinbase、Uniswap以及Opesea为代表的交易所也在为区块链经济提供更好的交易能力;
  • 内容端,元宇宙概念的游戏不断增加,生态不断加强,用户数也随之增长。以Roblox、Sandbox为代表的UGC元宇宙概念游戏得益于玩家的参与而不断丰富自己游戏的内容;
  • 载体端,通信技术、虚拟现实、芯片等底层技术也在不断演进。

四、元宇宙的渗透路径?

这个问题也可以理解成我们距离元宇宙的距离,东方证券张颖表示,元宇宙的内容短期将集中于游戏端与艺术端(NFT艺术藏品),长期来看,元宇宙的渗透路径预计将为“游戏/艺术-工作-生活”

1)游戏端:以Roblox为代表

Roblox平台主要由三个产品构成:

  • 客户端:允许用户探索3D数字世界的应用程序。(面向用户);
  • 工作室:允许开发人员和创作者构建、发布和操作Roblox客户端访问的3D体验和其他内容的工具群。(面向开发者)
  • Roblox云:为共同体验平台提供动力的服务和基础设施。

Roblox的主要营收来源为用户的游戏内支出。玩家需要充值换取游戏中的代币Robux获取Roblox的各种功能,这也是Roblox的营收来源。

并且,Roblox的激励机制十分明确,除掉25%支付给APPStore的营收以及用于平台各种费用的营收(约26%),剩下约49%的营收基本由公司和开发者平分。

其实,游戏UGC平台的概念可以追溯至魔兽争霸3:魔兽争霸3WE地图编辑器支持开发者创造出许多RTS(即时战略游戏)、MOBA(多人在线战术竞技)的游戏地图和游戏类型。但魔兽3对于开发者的奖励机制缺失,导致整体商用化程度不高。

2)艺术端:NFT构建元宇宙经济基础

非同质化代币(NFT)具有不可互换性、独特性、不可分性、低兼容性以及物品属性。并且产品流通渠道单一,市场透明度、价格发现能力均有较高提升空间。

目前,多家互联网大厂正试水NFT领域:2021年6月,阿里巴巴发售支付宝付款码皮肤NFT,2021年8月,腾讯围绕NFT进行一系列战略布局。

3)工作端:Facebook与英伟达的布局

Infinite office是Facebook元宇宙战略中重要环节。2020年9月,Facebook宣布推出VR虚拟办公应用InfiniteOffice,支持用户们创建虚拟办公空间,提高工作效率。

英伟达推出了NVIDIA Omniverse,一个专为虚拟协作和物理属性准确的实时模拟打造的开放式平台。并且已经开始投入使用,宝马公司正在内部推进NVIDIAOmniverse平台,以协调全球31座工厂的生产。而根据英伟达官网披露的信息,NVIDIA Omniverse将宝马的生产规划效率提高30%。

4)生活端:面向体验场景

东方证券认为,元宇宙的未来在于探索其应用场景的共性。这些应用场景均需考量用户的体验,元宇宙未来的商业模式与智能手机类似,即通过体验感增加用户的使用时间,进而提高用户粘性。这些时间(体验)成为元宇宙中各项服务的基础。

五、元宇宙时代有哪些确定性趋势?

元宇宙主要的载体(基础设施)主要包括如下几部分

1)网络(通信)

5G作为具有高速率、低时延和大连接特点的新一代宽带移动通信技术,是实现人机物互联的网络基础设施。

2)芯片(算力)

元宇宙的内容、网络、区块链、图形显示等功能均需要更为强大的算力。

云端算力方面,DPU芯片(数据处理芯片)通过分流、加速和隔离各种高级网络、存储和安全服务,为云、数据中心或边缘等环境中的各种工作负载提供安全的加速基础设施。

终端算力方面,异构芯片可以让SoC中的CPU、GPU、FPGA、DPU、ASIC等芯片协同工作,不断提升算力以提升用户体验。

3)云与边缘计算

云计算与边缘计算为用户提供所需的计算资源,降低用户触达元宇宙的门槛。

4)AI

AI在元宇宙中应用渗透较广泛。AI可以帮助创建元宇宙资产、艺术品和其他内容(AIGC),并可以改进我们用来构建所有这些内容的软件和流程。

六、元宇宙在产业端如何发挥价值?

全球范围内许多互联网企业、工业软件企业就工业元宇宙的相关技术已有长期的布局,其中包括数字孪生、工业互联网、仿真测试、数字化工厂、CAD、CAE、EDA等工业软件。

1)英伟达:Omniverse平台

Omniverse的架构包括Connect、Nucleus、Kit、Simulation、RTXrenderer等五个部分,他们与第三方数字内容创建工具(DCC)以及基于Omniverse的微服务构成了Omniverse的生态。

黄仁勋在参加Computex2021线上会议表示未来虚拟世界与现实世界将产生交叉融合,元宇宙与NFT将在其中扮演重要角色。其中Omniverse平台主要面向建筑、工程和施工;制造业;媒体和娱乐以及超级计算场景

根据英伟达官网对于Omniverse平台的介绍,通过Omniverse平台,用户可以完成实时虚拟协作、模拟现实的设计、模拟环境以及搭建未来工厂等操作。

2)微软:数字孪生探索

2021年9月,微软CEO Satya Nadella在Inspire2021演讲中提出全新“企业元宇宙”概念。微软的元宇宙计划中期望元宇宙可以打破现在的通信和业务流程之间的障碍,把他们融合在一起,让工业场景更为便捷。在宣布“企业元宇宙”概念之前,微软就已通过Azure数字孪生及AI等技术建立了工业元宇宙的底座。

Azure数字孪生是一个物联网(IoT)平台,可用于创建真实物品、地点、业务流程和人员的数字表示形式。通用电气航空的数字集团使用Azure数字孪生构建了一个实时和自动演变的模型,因此客户将随时可以访问其飞机的更新、准确和可用的数据模型。并且,通过内置数字可追溯性,可以实时记录每架飞机上每一个实物资产和部件。

3)能科股份:布局仿真与测试

能科股份主要业务包括智能制造、智能电气两个板块,其中公司智能制造业务基于数字孪生理念,整合业内先进工业软件和数字化IOT设备,虚拟世界内定义生产力中台并为客户开发个性化的工业微应用,物理世界内建立数字化、智能化的生产线和测试台,满足制造业企业产品全生命周期的数据与业务协同需求,帮助企业实现其自主创新、运营成本、生产效率、不良品率和客户满意度等业务目标。

4)阿里云:数字工厂“新基建”

根据德国工程师协会的定义,数字工厂(DF)是由数字化模型、方法和工具构成的综合网络,包含仿真和3D虚拟现实可视化,通过连续的没有中断的数据管理集成在一起。

阿里云工业互联网平台助力制造企业数字化转型,打造工厂内、供应链、产业平台全面协同的新基建,将工厂的设备、产线、产品、供应链、客户紧密地连接协同起来,为企业提供可靠的基础平台和上层丰富的工业应用,结合全面的产业支撑,助力企业完成数字化转型。

七、元宇宙是否需要区块链?

首先来理解一下区块连的技术特性,区块链是一种按时间顺序将不断产生的信息区块以顺序相连方式组合而成的一种可追溯的链式数据结构,是一种以密码学方式保证数据不可篡改、不可伪造的分布式账本。区块链借助自身的特性可以用于数字资产、内容平台、游戏平台、共享经济与社交平台的应用。基于自身的技术特性,天然适配元宇宙的关键应用场景。

东方证券认为,在元宇宙的整体架构中需要一套完善、缜密且成熟的技术系统支撑元宇宙的治理与激励。凭借区块链技术,元宇宙参与者可以根据在元宇宙的贡献度(时间、金钱、内容创造)等获得奖励。

八、元宇宙是否需要去中心化?

1)个人信息可携带权

在个人信息可携带权的时代,用户成为关键参与者,由用户主动发起个人信息数据传输并自行上传,从而实践个人数据可携带权,去中心化成为必不可少的条件。

2)去中心化不等于没有中心、没有监管

在去中心化概念下,仍有较为高级的节点参与治理或运营,这与分布式架构完全舍弃中心的概念不同。在去中心化概念下,有效的监管和治理仍可存在。

3)去中心化如何践行?参考DAO

去中心化自治组织(DecentralizedAutonomousOrganization,DAO)是基于区块链核心思想理念,由达成同一个共识的群体自发产生的共创、共建、共治、共享的协同行为衍生出来的一种组织形态,是区块链解决信任问题后的附属产物。

DAO将组织的管理和运营规则以智能合约的形式编码在区块链上,从而在没有集中控制或第三方干预的情况下自主运行。DAO具有充分开放、自主交互、去中心化控制、复杂多样以及涌现等特点,可成为应对不确定、多样、复杂环境的有效组织。

4)去中心化如何交易?参考DEX

去中心化交易所(DEX)在没有任何形式的中央权力的情况下以分散的方式运作。DEX不需要第三方来管理用户的资产,并允许用户随时保留对私人密钥的控制。由于DEX交易是点对点交易,它们提供了更高的透明度水平。

全球主流区块链去中心化交易所包括基于以太坊网络的Uniswap、Sushiswap、IDEX、Bancor、Kyber,基于币安智能链的Pancakeswap,基于Heco链上的MDEX等。

九、元宇宙是否需要NFT?

1)什么是NFT?区块链的主流资产之一。

NFT(Non-fungibleToken)代表不可替代的代币,是可以用来表示独特物品所有权的代币。NFT让艺术品、收藏品甚至房地产等事物标记化。他们一次只能拥有一个正式所有者,并且他们受到以太坊等区块链的保护,没有人可以修改所有权记录或复制/粘贴新的NFT。

NFT具有不可互换性、独特性、不可分性、低兼容性以及物品属性,可应用于流动性挖矿、艺术品交易、游戏/VR以及链下资产NFT化等场景,大幅提升数据流转效率。

2)NFT应用:一种潜在的元宇宙经济模式

NFT由于自身的数字稀缺性被率先运用于收藏、艺术品以及游戏场景。

东方证券认为,NFT在元宇宙中将扮演关键角色。首先,区块链是连接元宇宙概念的重要技术;其次,在元宇宙的整体架构中,在基础设施、数据和算法层之上、应用层之下,需要一套完善、缜密且成熟的技术系统支撑元宇宙的治理与激励;而NFT可以充当元宇宙激励环节的媒介

十、元宇宙时代,互联网形态是否会发生变化?

东方证券认为,基于元宇宙的发展,互联网的协议可能发生改变,互联网会针对于打造可信化的数字底座进行演进。而区块链技术也在攻克自身的缺陷:交易吞吐量低、与外界沟通困难等。

注:本文内容主要摘录自东方证券研报《十问元宇宙:如何将抽象的概念具象化?》

原文链接:https://wallstreetcn.com/articles/3644791
收起阅读 »

Vue新玩具VueUse

vue
什么是 VueUse VueUse 是一个基于 Composition API 的实用函数集合。通俗的来说,这就是一个工具函数包,它可以帮助你快速实现一些常见的功能,免得你自己去写,解决重复的工作内容。以及进行了基于 Composition API 的封装。让...
继续阅读 »

什么是 VueUse


VueUse 是一个基于 Composition API 的实用函数集合。通俗的来说,这就是一个工具函数包,它可以帮助你快速实现一些常见的功能,免得你自己去写,解决重复的工作内容。以及进行了基于 Composition API 的封装。让你在 vue3 中更加得心应手。


简单上手


安装 VueUse


npm i @vueuse/core

使用 VueUse


// 导入
import { useMouse, usePreferredDark, useLocalStorage } from '@vueuse/core'

export default {
setup() {
// tracks mouse position
const { x, y } = useMouse()

// is user prefers dark theme
const isDark = usePreferredDark()

// persist state in localStorage
const store = useLocalStorage(
'my-storage',
{
name: 'Apple',
color: 'red',
},
)

return { x, y, isDark, store }
}
}

上面从 VueUse 当中导入了三个函数, useMouseusePreferredDarkuseLocalStorageuseMouse 是一个监听当前鼠标坐标的一个方法,他会实时的获取鼠标的当前的位置。usePreferredDark 是一个判断用户是否喜欢深色的方法,他会实时的判断用户是否喜欢深色的主题。useLocalStorage 是一个用来持久化数据的方法,他会把数据持久化到本地存储中。


还有我们熟悉的 防抖节流


import { throttleFilter, debounceFilter, useLocalStorage, useMouse } from '@vueuse/core'

// 以节流的方式去改变 localStorage 的值
const storage = useLocalStorage('my-key', { foo: 'bar' }, { eventFilter: throttleFilter(1000) })

// 100ms后更新鼠标的位置
const { x, y } = useMouse({ eventFilter: debounceFilter(100) })

还有还有在 component 中使用的函数


<script setup>
import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'

const el = ref()

function close () {
/* ... */
}

onClickOutside(el, close)
</script>

<template>
<div ref="el">
Click Outside of Me
</div>
</template>

上面例子中,使用了 onClickOutside 函数,这个函数会在点击元素外部时触发一个回调函数。也就是这里的 close 函数。在 component 中就是这么使用


<script setup>
import { OnClickOutside } from '@vueuse/components'

function close () {
/* ... */
}
</script>

<template>
<OnClickOutside @trigger="close">
<div>
Click Outside of Me
</div>
</OnClickOutside>
</template>


注意⚠️ 这里的 OnClickOutside 函数是一个组件,不是一个函数。需要package.json 中安装了 @vueuse/components



还还有全局状态共享的函数


// store.js
import { createGlobalState, useStorage } from '@vueuse/core'

export const useGlobalState = createGlobalState(
() => useStorage('vue-use-local-storage'),
)

// component.js
import { useGlobalState } from './store'

export default defineComponent({
setup() {
const state = useGlobalState()
return { state }
},
})

这样子就是一个简单的状态共享了。扩展一下。传一个参数,就能改变 store 的值了。


还有关于 fetch, 下面👇就是一个简单的请求了。


import { useFetch } from '@vueuse/core'

const { isFetching, error, data } = useFetch(url)

它还有很多的 option 参数,可以自定义。


// 100ms超时
const { data } = useFetch(url, { timeout: 100 })

// 请求拦截
const { data } = useFetch(url, {
async beforeFetch({ url, options, cancel }) {
const myToken = await getMyToken()

if (!myToken)
cancel()

options.headers = {
...options.headers,
Authorization: `Bearer ${myToken}`,
}

return {
options
}
}
})

// 响应拦截
const { data } = useFetch(url, {
afterFetch(ctx) {
if (ctx.data.title === 'HxH')
ctx.data.title = 'Hunter x Hunter' // Modifies the resposne data

return ctx
},
})

作者:我只是一个小菜鸡
链接:https://juejin.cn/post/7029699344596992031

收起阅读 »

token过期自动跳转到登录页面

vue
这几天项目提测,测试给我提了个bug,说token过期,路由应该自动跳转到登陆页面,让用户重新登录。先说下一些前置条件, 1:我公司的token时效在生产环境设置为一个小时,当token过期,所有接口都直接返回 2:每次路由跳转都会对token进行判断,设置了...
继续阅读 »

这几天项目提测,测试给我提了个bug,说token过期,路由应该自动跳转到登陆页面,让用户重新登录。先说下一些前置条件,
1:我公司的token时效在生产环境设置为一个小时,当token过期,所有接口都直接返回
2:每次路由跳转都会对token进行判断,设置了一个全局的beforeEach钩子函数,如果token存在就跳到你所需要的页面,否则就直接跳转到登录页面,让用户登录重新存取token


接口返回的信息
{
code:10009,
msg:'token过期',
data:null
}
全局的路由钩子函数
router.beforeEach(async(to, from, next) => {
//获取token
// determine whether the user has logged in
const hasToken = getToken()

if (hasToken) {
//token存在,如果当前跳转的路由是登录界面
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done()
} else {
//在这里,就拉去用户权限,判断用户是否有权限访问这个路由
} catch (error) {
// remove token and go to login page to re-login
await store.dispatch('user/resetToken')
Message.error(error || 'Has Error')
next(`/login?redirect=${to.path}`)
NProgress.done()
}
} else {
//token不存在
if (whiteList.indexOf(to.path) !== -1) {
//如果要跳转的路由在白名单里,则跳转过去
next()
} else {
//否则跳转到登录页面
next(`/login?redirect=${to.path}`)
NProgress.done()
}
}
})

所以我直接在对所有的请求进行拦截,当响应的数据返回的code是10009,就直接清空用户信息,重新加载页面。我对代码简化了下,因为用户在登录时就会把token,name以及权限信息存在store/user.js文件里,所以只要token过期,把user文件的信息清空。这样,在token过期后,刷新页面或者跳转组件时,都会调用全局的beforeEach判断,当token信息不存在就会直接跳转到登录页面


import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
import { getToken } from '@/utils/auth'

const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
//发送请求时把token携带过去
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['sg-token'] = getToken()
}
return config
},
error => {
console.log(error)
return Promise.reject(error)
}
)

service.interceptors.response.use(
response => {
console.log(response.data)
const res = response.data

// token过期,重返登录界面
if (res.code === 10009) {
store.dispatch('user/logout').then(() => {
location.reload(true)
})
}
return res
},
error => {
console.log('err' + error) // for debug
Message({
message: error.msg,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)

export default service

好啦,关于token的分享就到这里了,以上代码根据你们项目的情况换成你们的数据,有错误欢迎指出来!


作者:阿狸要吃吃的
链接:https://juejin.cn/post/6947970204320137252

收起阅读 »

Vue3,我决定不再使用Vuex

vue
在开发基于Vue3的项目中发现我们可以不再依赖Vuex也能很方便的来管理数据,只需要通过Composition Api可以快捷的建立简单易懂的全局数据存储. 创建State 通过reactive我们来创建state,暴露的IState是用来方便其他文件来接受S...
继续阅读 »

在开发基于Vue3的项目中发现我们可以不再依赖Vuex也能很方便的来管理数据,只需要通过Composition Api可以快捷的建立简单易懂的全局数据存储.


创建State


通过reactive我们来创建state,暴露的IState是用来方便其他文件来接受State对象


import { reactive } from 'vue'

export interface IState {
code: string
token: string
user: any
}

export const State: IState = {
code: '',
token: '',
user: {}
}

export function createState() {
return reactive(State)
}


创建Action


我们来创建Action来作为我们修改State的方法


import { reactive } from 'vue'
import { IState } from './state'

function updateCode(state: IState) {
return (code: string) => {
state.code = code
}
}

function updateToken(state: IState) {
return (token: string) => {
state.token = token
}
}

function updateUser(state: IState) {
return (user: any) => {
state.user = user
}
}

/**
* 创建Action
* @param state
*/
export function createAction(state: IState) {
return {
updateToken: updateToken(state),
updateCode: updateCode(state),
updateUser: updateUser(state)
}
}

通过暴露的IState我们也可以实现对State的代码访问.


创建Store


创建好StateAction后我们将它们通过Store整合在一起.


import { reactive, readonly } from 'vue'
import { createAction } from './action'
import { createState } from './state'

const state = createState()
const action = createAction(state)

export const useStore = () => {
const store = {
state: readonly(state),
action: readonly(action)
}

return store
}

这样我们就可以在项目中通过调用useStore访问和修改State,因为通过useStore返回的State是通过readonly生成的,所以就确认只有Action可以对其进行修改.


// 访问state
const store = useStore()
store.state.code

// 调用action
const store = useStore()
store.action.updateCode(123)

这样我们就离开了Vuex并创建出了可是实时更新的数据中心.


持久化存储


很多Store中的数据还是需要实现持久化存储,来保证页面刷新后数据依然可用,我们主要基于watch来实现持久化存储


import { watch, toRaw } from 'vue'

export function createPersistStorage<T>(state: any, key = 'default'): T {
const STORAGE_KEY = '--APP-STORAGE--'

// init value
Object.entries(getItem(key)).forEach(([key, value]) => {
state[key] = value
})

function setItem(state: any) {
const stateRow = getItem()
stateRow[key] = state
const stateStr = JSON.stringify(stateRow)
localStorage.setItem(STORAGE_KEY, stateStr)
}

function getItem(key?: string) {
const stateStr = localStorage.getItem(STORAGE_KEY) || '{}'
const stateRow = JSON.parse(stateStr) || {}
return key ? stateRow[key] || {} : stateRow
}

watch(state, () => {
const stateRow = toRaw(state)
setItem(stateRow)
})

return readonly(state)
}

通过watchtoRaw我们就实现了statelocalstorage的交互.


只需要将readonly更换成createPersistStorage即可


export const useStore = () => {
const store = {
state: createPersistStorage<IState>(state),
action: readonly(action)
}

return store
}

这样也就实现了对Store数据的持久化支持.


作者:程序员紫菜苔
链接:https://juejin.cn/post/6898504898380464142

收起阅读 »

如何优雅的在java中统计代码块耗时

在我们的实际开发中,多多少少会遇到统计一段代码片段的耗时的情况,我们一般的写法如下 long start = System.currentTimeMillis(); try { // .... 具体的代码段 } finally { System...
继续阅读 »

在我们的实际开发中,多多少少会遇到统计一段代码片段的耗时的情况,我们一般的写法如下


long start = System.currentTimeMillis();
try {
// .... 具体的代码段
} finally {
System.out.println("cost: " + (System.currentTimeMillis() - start));
}

上面的写法没有什么毛病,但是看起来就不太美观了,那么有没有什么更优雅的写法呢?



1. 代理方式


了解 Spring AOP 的同学可能立马会想到一个解决方法,如果想要统计某个方法耗时,使用切面可以无侵入的实现,如


// 定义切点,拦截所有满足条件的方法
@Pointcut("execution(public * com.git.hui.boot.aop.demo.*.*(*))")
public void point() {
}

@Around("point()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try{
return joinPoint.proceed();
} finally {
System.out.println("cost: " + (System.currentTimeMillis() - start));
}
}

Spring AOP 的底层支持原理为代理模式,为目标对象提供增强功能;在 Spring 的生态体系下,使用 aop 的方式来统计方法耗时,可以说少侵入且实现简单,但是有以下几个问题



2. AutoCloseable


在 JDK1.7 引入了一个新的接口AutoCloseable, 通常它的实现类配合try{}使用,可在 IO 流的使用上,经常可以看到下面这种写法


// 读取文件内容并输出
try (Reader stream = new BufferedReader(new InputStreamReader(new FileInputStream("/tmp")))) {
List<String> list = ((BufferedReader) stream).lines().collect(Collectors.toList());
System.out.println(list);
} catch (IOException e) {
e.printStackTrace();
}

注意上面的写法中,最值得关注一点是,不需要再主动的写stream.close了,主要原因就是在try(){}执行完毕之后,会调用方法AutoCloseable#close方法;


基于此,我们就会有一个大单的想法,下一个Cost类实现AutoCloseable接口,创建时记录一个时间,close 方法中记录一个时间,并输出时间差值;将需要统计耗时的逻辑放入try(){}代码块


下面是一个具体的实现:


public static class Cost implements AutoCloseable {
private long start;

public Cost() {
this.start = System.currentTimeMillis();
}

@Override
public void close() {
System.out.println("cost: " + (System.currentTimeMillis() - start));
}
}

public static void testPrint() {
for (int i = 0; i < 5; i++) {
System.out.println("now " + i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
try (Cost c = new Cost()) {
testPrint();
}
System.out.println("------over-------");
}

执行后输出如下:


now 0
now 1
now 2
now 3
now 4
cost: 55
------over-------

如果代码块抛异常,也会正常输出耗时么?


public static void testPrint() {
for (int i = 0; i < 5; i++) {
System.out.println("now " + i);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (i == 3) {
throw new RuntimeException("some exception!");
}
}
}

再次输出如下,并没有问题


now 0
now 1
now 2
now 3
cost: 46
Exception in thread "main" java.lang.RuntimeException: some exception!
at com.git.hui.boot.order.Application.testPrint(Application.java:43)
at com.git.hui.boot.order.Application.main(Application.java:50)

3. 小结


除了上面介绍的两种方式,还有一种在业务开发中不太常见,但是在中间件、偏基础服务的功能组件中可以看到,利用 Java Agent 探针技术来实现,比如阿里的 arthas 就是在 JavaAgent 的基础上做了各种上天的功能,后续介绍 java 探针技术时会专门介绍


下面小结一下三种统计耗时的方式


基本写法


long start = System.currentTimeMillis();
try {
// .... 具体的代码段
} finally {
System.out.println("cost: " + (System.currentTimeMillis() - start));
}

优点是简单,适用范围广泛;缺点是侵入性强,大量的重复代码


Spring AOP


在 Spring 生态下,可以借助 AOP 来拦截目标方法,统计耗时


@Around("...")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
try{
return joinPoint.proceed();
} finally {
System.out.println("cost: " + (System.currentTimeMillis() - start));
}
}

优点:无侵入,适合统一管理(比如测试环境输出统计耗时,生产环境不输出);缺点是适用范围小,且粒度为方法级别,并受限于 AOP 的使用范围


AutoCloseable


这种方式可以看做是第一种写法的进阶版


// 定义类
public static class Cost implements AutoCloseable {
private long start;

public Cost() {
this.start = System.currentTimeMillis();
}

@Override
public void close() {
System.out.println("cost: " + (System.currentTimeMillis() - start));
}
}

// 使用姿势
try (Cost c = new Cost()) {
...
}

优点是:简单,适用范围广泛,且适合统一管理;缺点是依然有代码侵入


说明


上面第二种方法看着属于最优雅的方式,但是限制性强;如果有更灵活的需求,建议考虑第三种写法,在代码的简洁性和统一管理上都要优雅很多,相比较第一种可以减少大量冗余代码


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

Flutter -项目架构篇

介绍 多姿的青春,迷茫的青春,懵懂的青春,落泪的青春,责任的青春,青春的婀娜,青春的美妙全部撒播在了沿途的风景之中,迷茫,酸楚,欢声笑语在记忆的天空中承载着梦想而飞翔,青春才成了心中的永恒 本文带你一步一步搭建flutter项目架构,方便你项目直接集成使用...
继续阅读 »

介绍



多姿的青春,迷茫的青春,懵懂的青春,落泪的青春,责任的青春,青春的婀娜,青春的美妙全部撒播在了沿途的风景之中,迷茫,酸楚,欢声笑语在记忆的天空中承载着梦想而飞翔,青春才成了心中的永恒



本文带你一步一步搭建flutter项目架构,方便你项目直接集成使用。项目主要用到以下技术栈,小编秉着分享的宗旨,为你讲解


1.全局捕获异常


2.路由(Route)


3.Dio(网络)


4.OverlayEntry


5.网络dio抓包工具配置(ALice)


6.状态管理(Provider)


7.通知(这个是小编自己写的, 很方便,类似EventBus)


全局捕获异常


在Flutter中 ,有些异常是可以捕获到的,有些则是捕获不到的。那么,我们要做到错误日志上报给服务器,方便线上跟踪问题,怎么办呢?有个东西了解一下,捕获不到的用runZoned。代码如下,代码中有详细的注释,这里就不一一解释了。


void main() {
/// 捕获flutter能try catch 捕获的异常
/// 还有一些异常是try catch 捕获不到的 用runZoned
FlutterError.onError = (FlutterErrorDetails errorDetails) {
if (Application.debug) {
/// 测试环境 日志直接打印子啊控制台
FlutterError.dumpErrorToConsole(errorDetails);
} else {
/// 在生产环境上 重定向到runZone 处理
Zone.current
.handleUncaughtError(errorDetails.exception, errorDetails.stack);
}
reportErrorAndLog(errorDetails);
};
WidgetsFlutterBinding.ensureInitialized();
GlobalKey<NavigatorState> globalKey = new GlobalKey<NavigatorState>();
Application.globalKey = globalKey;

/// dio 网络抓包工具配置
Alice alice = Alice(
showInspectorOnShake: true,
showNotification: true,
navigatorKey: globalKey);
Application.alice = alice;

/// 初始化网络配置
HttpManager.initNet();

/// 捕获try catch 捕获不到的异常
runZoned(
() => runApp(MultiProvider(
providers: [
///注册通知
/// 这个是相当于通知 的作用 用于 这个类中的属性改变 然后通知到用到的页面 进行刷新
ChangeNotifierProvider(create: (_) => CounterProvider()),
],
child: MyApp(),
)), zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
/// 这里捕获所有print 日志
},
), onError: (Object obj, StackTrace stack) {
var detail = makeDetails(obj, stack);
reportErrorAndLog(detail);
});
}

void reportErrorAndLog(FlutterErrorDetails errorDetails) {
/// 错误日志上报 服务器
}

/// 构建错误信息
FlutterErrorDetails makeDetails(Object obj, StackTrace stack) {
FlutterErrorDetails details =
FlutterErrorDetails(stack: stack, exception: obj);
return details;
}

路由相关


路由跳转配置


跳转有2种方式。一种是直接用Widget, 另一种是用routeName。 这里小编为你讲解routeName跳转


先附上路由跳转封装类



import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';

/// Created by zhengxiangke
/// des:
class NavigatorUtil {
///直接跳转
static void push(BuildContext context, Widget widget) {
Navigator.push(context, MaterialPageRoute(builder: (context) => widget));
}

///根据路径跳转 可传参数
static void pushName(BuildContext context, String name, {Object arguments}) {
Navigator.pushNamed(context, name, arguments: arguments);
}

///销毁页面
static void pop(BuildContext context) {
Navigator.of(context).pop(context);
}

/// 推到 指定路由页面 这指定路由页面上的页面全部销毁
/// 注意: 若果没有指定路由 会报错
static void popUntil(BuildContext context, String routeName) {
Navigator.popUntil(context, ModalRoute.withName(routeName));
}


/// 把当前页面在栈中的位置替换为跳转的页面, 当新的页面进入后,之前的页面将执行dispose方法
static void pushReplacementNamed(BuildContext context, String routeName,
{Object arguments}) {

Navigator.of(context).pushReplacementNamed(routeName, arguments: arguments);
}
}


我们可以看到代码中的routeName, routeName这个是我们自己可以配置的 ,简单而言,就是根据路径去跳到指定的页面。路由配置如下


class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
localeResolutionCallback:
(Locale locale, Iterable<Locale> supportedLocales) {
//print("change language");
return locale;
},
navigatorKey: Application.globalKey,

/// 这个routes 不能写 如果写了的话 就不能传递参数
// routes: routes,
/// 这个既可以传递参数 也可以不传递参数 用这一个就够了 无须用这个routes
onGenerateRoute: onGenerateRoute,
navigatorObservers: [
/// 路由监听 作用:对用户行为流的埋点监测
GLObserver()
],
home: MyHomePage(),
);
}
}

final routes = {
'/second' : (context) => SecondPage(),
'/NestedScrollViewDemo' : (context, {arguments}) => NestedScrollViewDemo(value: arguments['value'] as String),
'/ProviderDemo': (context) => ProviderDemo()
};

// ignore: missing_return, top_level_function_literal_block
final onGenerateRoute = (settings) {
Function pageContentBuilder = routes[settings.name];
Route route;
if (pageContentBuilder != null) {
if (settings.arguments != null) {
/// 传递参数
route = MaterialPageRoute(
settings: settings,
builder: (context) => pageContentBuilder(context, arguments: settings.arguments));
return route;
} else {
/// 不传递参数 只管跳
route = MaterialPageRoute(
settings: settings,
builder: (context) => pageContentBuilder(context));
return route;
}

}
};

观察者


页面跳转添加观察者,能获取用户行为数据(GLObserver)


  @override
Widget build(BuildContext context) {
return MaterialApp(
localeResolutionCallback:
(Locale locale, Iterable<Locale> supportedLocales) {
//print("change language");
return locale;
},
navigatorKey: Application.globalKey,

/// 这个routes 不能写 如果写了的话 就不能传递参数
// routes: routes,
/// 这个既可以传递参数 也可以不传递参数 用这一个就够了 无须用这个routes
onGenerateRoute: onGenerateRoute,
navigatorObservers: [
/// 路由监听 作用:对用户行为流的埋点监测
GLObserver()
],
home: MyHomePage(),
);
}

Dio相关


dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等...


dependencies:
dio: ^3.0.9 // 请使用pub上3.0.0分支的最新版本

基本配置


小编带你写个Dio单例, 在这个单例中配置Dio基本配置


/// 这个是域名  请书写自己项目中的域名
const String BASEURL = '';
class HttpConfig {
static const baseUrl = BASEURL;
static const timeout = 5000;

static const codeSuccess = 10000;

}

class HttpManager {
factory HttpManager() => getInstance();
static HttpManager get install => getInstance();
static HttpManager _install;
static Dio dio;
HttpManager._internal() {
// 初始化
}
static HttpManager getInstance() {
if (_install == null) {
_install = HttpManager._internal();
}
return _install;
}
/// 初始化网络配置
static void initNet() {
dio = Dio(BaseOptions(
baseUrl: HttpConfig.baseUrl,
contentType: 'application/x-www-form-urlencoded',
connectTimeout: HttpConfig.timeout,
receiveTimeout: HttpConfig.timeout
));
}

}

设置代理


有这么个需求背景, 有一天,测试来问,怎么抓网络信息。Dio 为我们提供了代理, 测试可以根据chanles等抓包工具进行查看网络信息


    if (Application.proxy) {
/// 用于代理 抓包
(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
client.findProxy = (uri) {
//// PROXY 是固定 后面的localhost:8888 指的是别人的机器ip
return 'PROXY localhost:8888';
};
};
}

拦截器


我们可以在拦截器中添加一些公共的参数,如用户信息,手机信息,App版本信息等等, 也可以打印请求的url, 请求头,请求体信息。也可以进行参数签名。这里签名就不一一说了,


  /// 添加拦截器
dio.interceptors.add(CustomInterceptors());

///  des:  这里的api 规范是 200成功
class CustomInterceptors extends InterceptorsWrapper {
@override
Future onRequest(RequestOptions options) {
/// 在拦截里设置公共请求头
options.headers = {HttpHeaders.authorizationHeader: '这是token'};
if (Application.debug) {
try {
print("请求url:${options.path}");
print('请求头: ' + options.headers.toString());
/// 可以在这里个性定制 如签名 key value 从小到大排序 options.data 再次赋值即可
print('请求体: ' + options.data);
} catch (e) {
print(e);
}
}
return super.onRequest(options);
}
@override
Future onResponse(Response response) async{
LoadingUtil.closeLoading();
if (Application.debug) {
print('code=${response.statusCode.toString()} ==data=${response.data
.toString()}');
}
return super.onResponse(response);
}
@override
Future onError(DioError err) {
// TODO: implement onError
LoadingUtil.closeLoading();
if (err.type == DioErrorType.CONNECT_TIMEOUT
|| err.type == DioErrorType.RECEIVE_TIMEOUT
|| err.type == DioErrorType.SEND_TIMEOUT) {
Fluttertoast.showToast(msg: '请求超时');
} else {
Fluttertoast.showToast(msg: '服务异常');
}
return super.onError(err);
}
}

ALice


这是一个网络请求查看库,有了这个就不需要指定代理了,很方便。下面为dio 进行Alice 拦截,以便查看Dio 发出的请求


dependencies:
alice: 0.1.4
dio.interceptors.add(Application.alice.getDioInterceptor());

注意:ALice 一定要配置navigatorKey


  GlobalKey<NavigatorState> globalKey = new GlobalKey<NavigatorState>();
Application.globalKey = globalKey;
/// dio 网络抓包工具配置
Alice alice = Alice(
showInspectorOnShake: true,
showNotification: true,
navigatorKey: globalKey);
Application.alice = alice;

  class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp( navigatorKey:Application.globalKey,
],
home: MyHomePage(),
);
}
}

Provider


Flutter 状态管理,实际来说就是数据和视图的绑定和刷新; 这块对应到 H5,就比较好理解,这个概念也是从前端来到; 对应到 客户端,就是监听回调,类似事件总线(EventBus)


简而言之,就是监听的类中的变量属性发生变化就会刷新用到这个变量的Widget 页面


dependencies:
provider: ^4.3.2+2

说说这个库的中心思想


1.注册


2.定义类


3.赋值


4.取值


注册


runApp(MultiProvider(
providers: [
///注册通知
/// 这个是相当于通知 的作用 用于 这个类中的属性改变 然后通知到用到的页面 进行刷新
ChangeNotifierProvider(create: (_) => CounterProvider()),
],
child: MyApp(),
))

定义类


class CounterProvider with ChangeNotifier {
int count = 0;
void addCount() {
count ++;
notifyListeners();
}
}

赋值


 Provider.of<CounterProvider>(context, listen: false).addCount()

取值


context.watch<CounterProvider>().count

通知


小编之前在做Android开发,就用到了这个通知。后来做了Flutter开发一年来头了, 借鉴其思想,创下了这个通知


先说说通知原理:


我们知道EventBus有一个action事件和一个可以传递的数据对象。在页面初始化生命周期中注册通知,在页面销毁生命周期中销毁该通知。在需要发送通知 刷新数据地方, 调用发送通知 ,一个Action 对应发送到哪个通知,通知数据是一个泛型的Object, 可以发送字符串,对象,数组等任何数据


通知管理类


在这个类中提供几个方法


1.注册通知


2.销毁通知


3.发送通知


///这是一个例子
///这个在initState() 注册 <> 里的类型 要跟 发送的数据类型对应 可以是String int bool 还有model list
/// IUpdateViewManager.instance.registerIUPdateView(UpdateView(NoticeAction.action1,
/// IUpdateView<List<String>>(
/// callback: (List<String> msg) {
/// print(msg);
/// }
/// )));、
///发送通知 注意 这里可以发送任何类型的数据 因为范型
///IUpdateViewManager.instance.notifyIUpdateView(NoticeAction.action1, ['xx', 'vvv']);
///在dispose中 取消注册 注意: 有注册就有取消 这是对应的 否则会出现功能不正常请客
///IUpdateViewManager.instance.unRegistIUpdateView(NoticeAction.action1);
class IUpdateViewManager{
List<UpdateView> updateViews = [];

// 工厂模式
factory IUpdateViewManager() =>_getInstance();
static IUpdateViewManager get instance => _getInstance();
static IUpdateViewManager _instance;
IUpdateViewManager._internal() {
// 初始化
}
static IUpdateViewManager _getInstance() {
_instance ??= IUpdateViewManager._internal();
return _instance;
}
///注册通知 在initstatus 注册
void registerIUPdateView(UpdateView updateView) {
///在数组中不能存在多个相同的action
updateViews.insert(0, updateView);
}
///发送通知 在业务场景需要的地方 调用这个方法
void notifyIUpdateView <T>(String action, T t) {
if (updateViews != null && updateViews.isNotEmpty) {
for (var item in updateViews) {
if (item.action == action) {
item.iUpdateView.updateView(t);
break;
}
}
}
}
///通知解绑 在dispose方法中解绑 注意 有注册 就有解绑 这是一定必须的
void unRegistIUpdateView(String action) {
if (updateViews != null && updateViews.isNotEmpty) {
updateViews.remove(UpdateView(action, null));
}
}
}
///这个类是时间action 用到这个类的通知的action 在这里定义常量
class NoticeAction {
static const String action1 = 'action1';
static const String action2 = 'action2';
}

其次,通知类如下


class UpdateView {
String action;
IUpdateView iUpdateView;


UpdateView(this.action, this.iUpdateView);

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is UpdateView &&
runtimeType == other.runtimeType &&
action == other.action;

@override
int get hashCode => action.hashCode;
}

class IUpdateView <T>{
Function(T msg) callback;
void updateView (T t) {
if (callback != null) {
callback(t);
}
}

IUpdateView({@required this.callback});
}

注册通知


这个在initState() 注册 <> 里的类型 要跟 发送的数据类型对应 可以是String int bool 还有model list


   IUpdateViewManager.instance.registerIUPdateView(UpdateView(NoticeAction.action1,
IUpdateView<List<String>>(
callback: (List<String> msg) {
print(msg);
}
)));

发送通知


IUpdateViewManager.instance.notifyIUpdateView(NoticeAction.action1, ['xx', 'vvv']);

销毁通知


在dispose中 取消注册 注意: 有注册就有取消 这是对应的 否则会出现功能不正常


IUpdateViewManager.instance.unRegistIUpdateView(NoticeAction.action1);

架构篇


小编先说说搭建项目的总体思想,




  1. 我们知道每一个页面刚进去的时候都会有一个loading,因此小编用一个widget的基类,所有的页面都会继承这个基类。在这个基类中提供了Appbar的方法,加载试图显示和隐藏,加载失败重试,网络请求的方法,另外还有个buildBody方法,所有继承该基类的widget都必须重写这个方法,详见BasePage




  2. 网络: 本文采用Dio,并添加了拦截器,可在拦截器中打印请求信息,有个HttpManager管理单例的dio实例,并添加了Alice网络查看器,方便测试人员查看请求信息。HttpRequest里有个请求方法, 可定义请求方式,传递方式,失败回调,成功回调。并在回调中返回ResultData(这是一个返回的数据结构封装类)




  3. 对于埋点上报,新增了一个GLObserver路由观察者,在这里可以进行简单的用户行为进行捕获




  4. 错误日志上报 详见main.dart




  5. 由于复杂的页面交互,那么通知也是少不了的,一个页面的某个行为会影响上个页面的展现内容或者刷新数据,那么 这里小编定义了2中方式:1.Provider 2.IupdateViewManager 大家可以任选其一即可




  6. 基于SmartRefresher刷新封装的CustomerSmartRefresh


    最后,代码已上传github , 欢迎下载阅读
    如有疑问 加QQ群 883130953


    github.com/zhengxiangk…


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

关于Android架构,你是否还在生搬硬套?

前言 关于Android架构,可能在很多人心里一直都是虚无缥缈的存在,似懂非懂、为了用而用、处处生搬硬套,这种情况使用的意义真的很有限。本人有多个项目重构的经验,恰好对设计领域较为感兴趣,今天我将毫无保留的将自己对架构、设计的理解分享给大家。 本文不会具体去讲...
继续阅读 »

前言


关于Android架构,可能在很多人心里一直都是虚无缥缈的存在,似懂非懂、为了用而用、处处生搬硬套,这种情况使用的意义真的很有限。本人有多个项目重构的经验,恰好对设计领域较为感兴趣,今天我将毫无保留的将自己对架构、设计的理解分享给大家。


本文不会具体去讲什么是MVC、MVP、MVVM,但我描述的点应该都是这些模式的基石,从本质上讲明白为什么这样做,这样做的好处是什么,有了这些底层思想的支持再去看对应的架构模式,相信会让你有一种焕然一新的感觉。


知识储备:需掌握Java面向对象、六大设计原则,如果不理解也无妨,我尽量将用到的设计原则加以详细描述


目录



  • 1. 模块化的意义何在?

    • 1.1 基本概念以及底层思想

    • 1.2 我们要基于哪些特性去做模块化划分?

    • 1.3 Android如何做分层处理?

    • 1.4 Data Mapper或许是解药

    • 1.5 无处安放的业务逻辑



  • 2. 合理分层是给 数据驱动UI 做铺垫

    • 2.1 什么是 控制反转?

    • 2.2 什么是数据驱动UI?

    • 2.3 为什么说数据驱动UI底层思想是控制反转?

    • 2.4 为什么引入Diff?



  • 3. 为什么我建议使用 函数式编程

    • 3.1 什么是 函数式编程?

    • 3.2 Android视图开发可以借鉴函数式编程思想




1. 模块化的意义何在?


1.1 基本概念以及底层思想



所有的模块化都是为了满足单一设计原则 (字面意思理解即可),一个函数或者一个类再或者一个模块,职责越单一复用性就越强,同时能够间接降低耦合性



在软件工程的背景下,改动就会有出错的可能,不要说"我注意一点就不会出错"这种话,因为人不是机器。我们能做的就是尽可能让模块更加单一,职责越单一影响到外层模块的可能性就越小,这样出错的概率也就越低。


所以模块化核心思想即:单一设计原则


1.2 我们要基于哪些特性去做模块化划分?


做模块化处理的时候尽量基于两种特性进行功能特性业务特性


功能特性



网络、图片加载等等都可称之为功能特性。比如网络:我们可以将网络框架的集成、封装等等写到同一个模块(module、package等)当中,这样可以增强可读性(同一目录一目了然)、降低误操作概率,方便于维护也更加安全。同时也可将模块托管至远程如maven库,可供多个项目使用,进一步提升复用性



业务特性



业务特性字面意思理解即可,就是我们常常编写的业务,需要以业务的特性进行模块划分



为什么说业务特性优先级要高于功能特性


举个例子如下图:


image.png


相信很多人见过或者正在使用这种分包方式,在业务层把所有的AdapterPresenterActivity等等都放在对应的包中,这种方式合理吗?先说答案不合理,首先这已经是在业务层,我们做的所有事情其实都在为业务层服务,所以业务的优先级应该是最高的,我们应当优先根据业务特性将对应的类放入到同一个包中。


功能模块核心是功能,应当以功能进行模块划分。业务模块核心是业务,应当优先以业务进行模块划分,其次再以功能进行模块划分。


1.3 Android如何做分层处理?


前端开发其实就是做数据搬运,再展示到视图中。数据视图是两个不同的概念,为了提高复用性以及可维护性,我们应当根据单一设计原则我们应当将二者进行分层处理,所以无论是MVCMVP还是MVVM最核心的点都是将数据视图进行分层。


绊脚石:



通常来讲,我们通过网络请求拿到数据结构都是后端定义的,这也就意味着视图层不得不直接使用后端定义的字段,一旦后端进行业务调整会迫使我们前端从数据层-->视图层都会进行对应的改动,如下伪代码所示:



//原始逻辑
数据层
Model{
title
}
UI层
View{
textView = model.title
}

//后端调整后
数据层
Model{
title
prefix
}
UI层
View{
textView = model.prefix + model.title
}

起初我们的textView显示的是model中的title,但后端调整后我们需要在model中加一个prefix字段,同时textView显示内容也要做一次字符串拼接。视图层因为数据层的改动而被动做了修改。既然做了分层我们想要的肯定是视图、数据互不干扰,如何解决?往下看...


1.4 Data Mapper或许是解药


Data Mapper是后端常用的一个概念,一般情况下他们是不会直接使用数据库里面的字段,而是加一个Data Mapper(数据映射)将数据库表转按需换成Java Bean,这样做的好处也很明显,表结构甭管怎么折腾都不会影响到业务层代码。


对于前端我觉得可以适当引入Data Mapper,将后端数据转换成本地模型,本地模型只与设计图对应,将后端业务视图完全隔离。这也就解决了 1.3 面临的问题,具体方式如下:


数据层
Model{
title
prefix
}
本地模型(与设计图一一对应)
LocalModel{
//将后端模型转换为本地模型
title = model.prefix + model.title
}
UI层
View{
textView = localModel.title
}

LocalModel相当于一个中间层,通过适配器模式将数据层与视图层做隔离。


前端引入Data Mapper后可以脱离后端进行开发,只要需求明确就可以做视图层的开发,完全不需要担心后端返回什么结构字段。并且这种做法是一劳永逸的,比如后端需要对某些字段做调整,我们可以不暇思索直奔数据层,涉及到的调整100%不会影响到视图层


注意点:



当下有一部分公司为了将前后端分离更彻底,由前端开发人员提供Java Bean(相当于LocalModel)的结构,好处也很明显,更多的业务内聚到后端,很大程度提升了业务的灵活性,毕竟App发一次版成本还是比较大的。面对这种情况我们其实没必要再编写Data Mapper。所以任何架构设计都要结合实际情况,适合自己的才是最好的。



1.5 无处安放的业务逻辑


关于业务逻辑其实是一个很笼统的概念,甚至可以将任意一行代码称之为业务逻辑,如此宽泛的概念我们该如何去理解?我先大致将它分为两个方面:




  • 界面交互逻辑:视图层的交互逻辑,比如手势控制、吸顶悬浮等等都是根据业务需要实现的,所以严格来说这部分也属于业务逻辑。但这部分业务逻辑一般在视图层实现。

  • 数据逻辑:这部分是大家常说的业务逻辑,属于强业务逻辑,比如根据不同用户类型获取不同数据、展示不同界面,加上Data Mapper一系列操作其实就是给后端兜底,帮他们补全剩余逻辑而已。为了方便大家理解下文我将数据逻辑统称为业务逻辑



前面我们说到,Android开发应该具备数据层视图层,那业务逻辑放在哪一层比较合适呢?比如MVVM模式下大家都说将业务逻辑放到ViewModel处理,这么说也没有太大的问题,但如果一个界面足够复杂那对应的ViewModel代码可能会有成百上千行,看起来会很臃肿可读性也非常差。最重要的一点这些业务很难编写单元测试用例


关于业务逻辑我建议单独写一个use case处理。


use case通常放在ViewModel/Presenter数据层之间,业务逻辑以及Data Mapper都应该放在use case中,每一个行为对应一个use case。这样就解决了ViewModel/Presenter臃肿的问题,同时更方便编写测试用例。


注意点:



好的设计都是特定场景解决特定问题,过度设计不仅解决不了任何问题反而会增加开发成本。以我目前经验来看Android开发至少一半的场景都很简单:请求-->拿数据-->渲染视图最多再加个Data Mapper,流程很单一并且后期改动的可能也不太大,这种情况就没必要写一个use case,Data Mapper扔到数据层即可。



2. 合理分层是给 数据驱动UI 做铺垫


先说结论:数据驱动UI的本质是控制反转


2.1 什么是 控制反转?


控制即对程序流程的控制,一般由我们开发者承担,此过程为控制。但开发者是人所以不可避免出现错误,此时可以将角色做一个反转由成熟的框架负责整个流程,程序员只需要在框架预留的扩展点上,添加跟自己的业务代码,就可以利用框架来驱动整个程序流程的执行,此过程为反转


控制反转概念和设计原则中的依赖倒置很相似,只是少了一个依赖抽象


打个比方:



现有一个HTTP请求的需求,如果想自己维护HTTT链接、自己管理TCP Socket、自己处理HTTP缓存.....就是整个HTTP协议全部自己封装,先不说这个工程能不能靠个人实现,就算实现也是漏洞百出,此时可以换个思路:通过OkHttp去实现,OkHttp是一个成熟的框架用它基本上不会出错。个人封装HTTP协议到使用OkHttp框架,这个过程在控制HTTP的角色上发生了一个反转个人--->成熟的框架OkHttp即控制反转,好处也很明显,框架出错的概率远低于个人。



2.2 什么是数据驱动UI?


通俗一点说就是当数据改变时对应的UI也要跟着变,反过来说当需要改变UI只需要改变对应的数据即可。现在比较流行的UI框架如FlutterComposeVue其本质都是基于函数式编程实现数据驱动UI,它们共同的目的都是为了解决数据,UI一致性问题。


在当前的Android中可以使用DataBinding实现同样的效果,以Jetpack MVVM为例:ViewModelRepository拿到数据暂存到ViewModel对应的ObservableFiled即可实现数据驱动UI,但前提是从Repository拿到的数据可以直接用,如果在Activity或者Adapter做数据二次处理再notify UI,已经违背数据驱动UI核心思想。所以想实现数据驱动UI必须要有合理的分层(UI层拿到的数据无需处理,可以直接用)Data Mapper恰好解决这一问题,同时也可规避大量编写BindAdapter的现状。



DataBinding并非函数式编程,它只是通过AbstractProcessor生成中间代码,将数据映射到XML中



2.3 为什么说数据驱动UI底层思想是控制反转?


当前Android生态能实现数据绑定UI的框架只有两个:DataBinding、Compose(暂不讨论)


在引入DataBinding之前渲染一条数据通常需要两步,如下:


var title = "iOS"
fun setTitle(){
//第一步更改数据源
title = "Android"
//第二个更改UI
textView = title
}

共需要两步更改数据源、更改UI,数据源UI有一个忘记修改便会出现BUG,千万不要说:“两个我都不会忘记修改”,当面临复杂的逻辑以及十几个甚至几十个的数据源很难保证不出错。这种问题可以通过DataBinding解决,只需更改对应的ObservableFiledUI便会同步修改,控制UI状态也从个人反转到的DataBinding,个人疏忽的事情DataBinding可不会。


所以说数据驱动UI底层思想是控制反转


2.4 为什么引入Diff?


引入diff之前:



RecyclerView想要实现动态删除、添加、更新需要分别手动更新数据和UI,这样在中间插了一道并且分别更新数据和UI已经违背了前面所说的数据驱动UI,而我们想要的是不管删除、添加或者更新只有一个入口,只要改变数据源就会驱动UI做更新,想要满足这一原则只能改变数据源后对RecyclerView做全部刷新,但这样会造成性能问题,复杂的界面会感到明显的卡顿。



引入diff之后:



Diff算法通过对oldItemnewItem做差异化比对,会自动更新改变的item,同时支持删除、添加的动画效果,这一特性解决了RecyclerView需要实现数据驱动UI的性能问题



3 为什么我建议使用 函数式编程


3.1 什么是 函数式编程?



  • 一个入口,一个出口。

  • 不在函数链内部执行与运算本身无关的操作

  • 不在函数链内部使用外部变量(实际上这一条很难遵守,可以适当突破)


说的通俗点就是给定一个初始值,经过函数链的运行会得到一个目标值,运算的过程中外部没有插手的权限,同时不做与本身无关的操作,从根本上解决了不可预期错误的产生。


举个例子:


//Kotlin代码

listOf(10, 20).map {
it + 1
}.forEach {
Log.i("list", "$it")
}

上面这种链式编程就是标准的函数式编程,输入到输出之间开发者根本没有插手的机会(即Log.i(..)之前开发者没有权限处理list),所以整个流程是100%安全的,RxJavaFlow链式高阶函数都是标准的函数式编程,它们从规范层面解决数据安全问题。所以我建议在Kotlin中 碰到数据处理尽量使用链式高阶函数(RxJava、Kotlin Flow亦然)


其实函数式编程的核心思想就是 门面模式 以及 迪米特法则


3.2 Android视图开发可以借鉴函数式编程思想


Android视图开发大都遵循如下流程:请求-->处理数据-->渲染UI,这一流程可以借鉴函数式编程,将请求作为入口,渲染做为出口,在这个流程中尽量不做与当前行为无关的事(这也要求ViewModel,Repository中的函数要符合单一原则)。这样说有点笼统,下面举个反例:


    View{
//刷新
fun refresh(){
ViewModel.load(true)
}
//加载更多
fun loadMore(){
ViewModel.load(false)
}
}

ViewModel{
//加载数据
load(isRefresh){
if (isRefresh){
//刷新
}else{
//加载更多
}
}
}

View层有刷新、加载更多两种行为,load(isRefresh)一个入口,两个出口。面临的问题很明显,修改刷新加载更多都会对对方产生影响,违反开闭原则中的闭(对修改关闭:行为没变不准修改源代码),导致存在不可预期的问题产生。可以借鉴函数式编程思想对其进行改进,将ViewModelload函数拆分成refreshloadMore,这样刷新加载更多两种行为、两个入口、两个出口互不干涉,通过函数的衔接形成两条独立的业务链条。


函数式编程可以约束我们写出规范的代码,面对不能使用函数式编程的场景,我们可以尝试自我约束往函数式编程方向靠拢,大致也能实现相同的效果。


综上所述



  • 合理的分层可以提升复用性、降低模块间耦合性

  • Data Mapper 可以让视图层脱离于后端进行开发

  • 复杂的业务逻辑应该写到use case中

  • 数据驱动UI的本质是控制反转

  • 通过函数式编程可以写出更加安全的代码

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

iOS-应用程序的加载

iOS
资料准备: 1、dyld源码下载opensource.apple.com/ 2、libdispatch源码下载opensource.apple.com/ 3、libSystem源码下载opensource.apple.com/ 前情提要: 在探索分析app启动...
继续阅读 »

资料准备:


1、dyld源码下载opensource.apple.com/

2、libdispatch源码下载opensource.apple.com/

3、libSystem源码下载opensource.apple.com/


前情提要:


在探索分析app启动之前,我们需要先了解iOS中App代码的编译过程以及动态库和静态库。


编译过程


1预编译:处理代码中的#开头的预编译指令,比如删除#define并展开宏定义,将#include包含的文件插入到该指令位置等(即替换宏,删除注释,展开头文件,产生.i文件)

2编译:对预编译处理过的文件进行词法分析、语法分析和语义分析,并进行源代码优化,然后生成汇编代码(即将.i文件转换为汇编语言,产生.s文件)

3汇编:通过汇编器将汇编代码转换为机器可以执行的指令,并生成目标文件.o文件

4链接:将目标文件链接成可执行文件.这一过程中,链接器将不同的目标文件链接起来,因为不同的目标文件之间可能有相互引用的变量或调用的函数,如我们经常调用Foundation框架和UIKit框架中的方法和变量,但是这些框架跟我们的代码并不在一个目标文件中,这就需要链接器将它们与我们自己的代码链接起来


流程如下



Foundation和UIKit这种可以共享代码、实现代码的复用统称为库——它是可执行代码的二进制文件,可以被操作系统写入内存,它又分为静态库和动态库



静态库


链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。
.a.lib、非系统framework都是静态库


动态库


链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。如.dylib.framework都是动态库


dyld:


简介


dyld(The dynamic link editor)是苹果的动态链接器,负责程序的链接及加载工作,是苹果操作系统的重要组成部分,存在于MacOS系统的(/usr/lib/dyld)目录下.在应用被编译打包成可执行文件格式的Mach-O文件之后 ,交由dyld负责链接,加载程序。

整体流程如下


image.png


dyld_shared_cache


由于不止一个程序需要使用UIKit系统动态库,所以不可能在每个程序加载时都去加载所有的系统动态库.为了优化程序启动速度和利用动态库缓存,苹果从iOS3.1之后,将所有系统库(私有与公有)编译成一个大的缓存文件,这就是dyld_shared_cache,该缓存文件存在iOS系统下的/System/Library/Caches/com.apple.dyld/目录下


dyld加载流程:


load方法处加一个断点,点击函数调用栈/使用LLDB——bt指令打印,都能看到最初的起点_dyld_start


_dyld_start


可以看到_dyld_start是汇编写的,从注释中可以看出dyldbootstrap::start方法就是最开始的start方法。


image.png


dyldbootstrap::start


dyldbootstrap::start其实C++的语法,其中dyldbootstrap代表命名空间,start则是这个命名空间中的方法。


image.png


image.png
可以看到start这个方法的核心是dyld::main


dyld::main


_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue) {

代码省略......

/// 环境变量的配置
// Grab the cdHash of the main executable from the environment
/// 从环境变量中获取主要可执行文件的cdHash
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
if ( const char* mainExeCdHashStr = _simple_getenv(apple, "executable_cdhash") ) {
unsigned bufferLenUsed;
if ( hexStringToBytes(mainExeCdHashStr, mainExecutableCDHashBuffer, sizeof(mainExecutableCDHashBuffer), bufferLenUsed) )
mainExecutableCDHash = mainExecutableCDHashBuffer;
}
/// 根据Mach-O头部获取当前运行的架构信息
getHostInfo(mainExecutableMH, mainExecutableSlide);

代码省略......

/// 检查共享缓存是否开启,在iOS中必须开启
// load shared cache
checkSharedRegionDisable((dyld3::MachOLoaded*)mainExecutableMH, mainExecutableSlide);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
#if TARGET_OS_SIMULATOR
if ( sSharedCacheOverrideDir)
mapSharedCache(mainExecutableSlide);
#else
/// 检查共享缓存是否映射到了共享区域
mapSharedCache(mainExecutableSlide);
#endif

代码省略......

/// 加载可执行文件,并生成一个ImageLoder实例对象
// instantiate ImageLoader for main executable
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

代码省略......

/// 加载所有DYLD_INSERT_LIBRARIES指定的库
// load any inserted libraries
if ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib)
loadInsertedDylib(*lib);
}

代码省略......

/// link主程序
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);

代码省略......

/// link动态库
// link any inserted libraries
// do this after linking main executable so that any dylibs pulled in by inserted
// dylibs (e.g. libSystem) will not be in front of dylibs the program uses
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
if ( gLinkContext.allowInterposing ) {
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->registerInterposing(gLinkContext);
}
}
}

代码省略......

sMainExecutable->recursiveBindWithAccounting(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true);
uint64_t bindMainExecutableEndTime = mach_absolute_time();
ImageLoaderMachO::fgTotalBindTime += bindMainExecutableEndTime - bindMainExecutableStartTime;
gLinkContext.notifyBatch(dyld_image_state_bound, false);

// Bind and notify for the inserted images now interposing has been registered
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->recursiveBind(gLinkContext, sEnv.DYLD_BIND_AT_LAUNCH, true, nullptr);
}
}

代码省略......

/// 弱符号绑定
// <rdar://problem/12186933> do weak binding only after all inserted images linked
sMainExecutable->weakBind(gLinkContext);

代码省略......

/// 执行初始化方法
// run all initializers
initializeMainExecutable();

代码省略......

/// 寻找m目标可执行文件ru入口并执行
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getEntryFromLC_UNIXTHREAD();

}#### 1 环境变量配置


通过以上代码分析大体流程如下


1 环境变量配置



  • 平台,版本,路径,主机信息的确定

  • 从环境变量中获取主要可执行文件的cdHash

  • checkEnvironmentVariables(envp)检查设置环境变量

  • defaultUninitializedFallbackPaths(envp)DYLD_FALLBACK为空时设置默认值

  • getHostInfo(mainExecutableMH, mainExecutableSlide)获取程序架构


image.png
image.png
image.png


Xcode设置了这两个环境变量参数,在App启动时就会打印相关参数、环境变量信息
如下


image.png


image.png


2 共享缓存



  • checkSharedRegionDisable检查是否开启共享缓存(在iOS中必须开启)

  • mapSharedCache加载共享缓存库,其中调用loadDyldCache函数有这么几种情况:

    • 仅加载到当前进程mapCachePrivate(模拟器仅支持加载到当前进程)

    • 共享缓存是第一次被加载,就去做加载操作mapCacheSystemWide

    • 共享缓存不是第一次被加载,那么就不做任何处理




image.png


image.png


3 主程序的初始化


调用instantiateFromLoadedImage函数实例化了一个ImageLoader对象
image.png
通过instantiateMainExecutable方法创建ImageLoader实例对象
image.png
这里主要是为主可执行文件创建映像,返回一个ImageLoader类型的image对象,即主程序.其中sniffLoadCommands函数会获取Mach-O类型文件的Load Command的相关信息,并对其进行各种校验
image.png


4 插入动态库


遍历DYLD_INSERT_LIBRARIES环境变量,调用loadInsertedDylib加载,通过该环境变量我们可以注入自定义的一些动态库代码从而完成安全攻防,loadInsertedDylib内部会从DYLD_ROOT_PATHLD_LIBRARY_PATHDYLD_FRAMEWORK_PATH等路径查找dylib并且检查代码签名,无效则直接抛出异常
image.png
image.png


5 link主程序
image.png


5 link动态库
image.png


6 弱符号绑定
image.png


7 执行初始化方法


image.png
initializeMainExecutable源码主要是循环遍历,都会执行runInitializers方法
image.png
runInitializers(cons其核心代码是processInitializers函数的调用
image.png
processInitializers函数对镜像列表调用recursiveInitialization函数进行递归实例化
image.png
recursiveInitialization函数其作用获取到镜像的初始化,核心方法有两个notifySingledoInitialization
image.png
notifySingle函数,其重点是(*sNotifyObjCInit)(image->getRealPath(), image->machHeader());这句
image.png
sNotifyObjCInit只有赋值操作
image.png
registerObjCNotifiers发现在_dyld_objc_notify_register进行了调用,这个函数只在运行时提供给objc使用
image.png
objc4源码中查找_dyld_objc_notify_register,发现在_objc_init源码中调用了该方法,并传入了参数,所以sNotifyObjCInit的赋值的就是objc中的load_images所以综上所述,notifySingle是一个回调函数
image.png
load_images中可以看到call_load_methods方法调用
image.png
call_load_methods方法其核心是通过do-while循环调用call_class_loads方法
image.png
call_class_loads这里调用的load方法就是类的load方法
image.png
至此也证实了load_images调用了所有的load函数。


doInitialization中调用了两个核心方法doImageInitdoModInitFunctions
image.png
doImageInit其核心主要是for循环加载方法的调用,这里需要注意的一点是libSystem的初始化必须先运行
image.png
doModInitFunctions中加载了所有Cxx文件,这里需要注意的一点是libSystem的初始化必须先运行
image.png
通过doImageInitdoModInitFunctions方法知道libSystem初始化必须先运行,这里也和堆栈信息相互验证一致性
image.png
libSystem库中的初始化函数libSystem_initializer中调用了libdispatch_init函数
image.png
libdispatch_init方法中调用了_os_object_init函数
image.png
_os_object_init方法中调用了_objc_init函数
image.png
结合上面的分析,从初始化_objc_init注册的_dyld_objc_notify_register的参数2,即load_images,到sNotifySingle --> sNotifyObjCInie=参数2sNotifyObjcInit()调用,形成了一个闭环


8 寻找主程序入口
image.png
dyld汇编源码实现
image.png


dyld加载流程


image.png




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

收起阅读 »

谈谈iOS项目的多环境配置

iOS
在项目中配置多环境,需要了解的三个芝士点: Project: 包含了项目所有的代码,资源文件,所有信息。 Target: 对指定代码和资源文件的具体构建方式。 Scheme: 对指定Target的环境配置。 配置多环境的三种方案 多Target 先复制一...
继续阅读 »

在项目中配置多环境,需要了解的三个芝士点:



  • Project: 包含了项目所有的代码,资源文件,所有信息。

  • Target: 对指定代码和资源文件的具体构建方式。

  • Scheme: 对指定Target的环境配置。


配置多环境的三种方案


多Target



  1. 先复制一份一样的Target


image.png



  1. 对其进行重新命名,此时对于项目会增加一个新的info.plist文件


image.png



  1. 设置其对应的info.plist文件


image.png
4. 对于这个新的Target修改其对应BundleID


image.png



  1. 设置宏定义来实现多环境配置



  • Objc:在Objc中通过在Preprocessor Macros中配置宏定义


image.png



  • Swift:在Swift中通过在Other Swfit Flags中增加配置


image.png


总结
通过多Target方案会有两个缺点,第一每生成一个Target都会产生一个Info.plist文件,会比较冗余,第二就是比较麻烦,因为每次都会要设置宏定义,故不建议采纳。


通过Scheme实现多环境配置



  1. 添加新的Configuration


image.png



  1. 增加新的Scheme


image.png



  1. schemeBuild Configuration一一对应


image.png



  1. 新增定义设置(这里以区分不同环境需要访问的域名来举例)


image.png


image.png



  1. Info.plist中新增访问接口


image.png



  1. 在项目中进行访问


image.png


image.png


image.png


image.png


可以看到实现了不同的scheme访问了不同的值,实现了多环境配置,不过这个方案依然不够方便,因为有些Build Settings里针对不同环境需要做不同设置,这样还是不够方便。


xcconfig


1.在项目中创建自己的xcconfig文件,这里分别创建debugreleaserc对应的文件


image.png


2.在ProjectConfigurations进行对应


image.png


3.在xcconfig文件中进行配置(同样以不同环境的域名为例子)


image.png


image.png


image.png


4.在plist文件中提供接口
image.png


5.运行程序发现报错


image.png


这里涉及使用pod,如果另外创建xcconfig文件会导致这个错误,如果不涉及pod则不会报错,来看下控制台的报错


image.png


6.引入pods工程下的xcconfig相关文件
仅举例debug.xcconfig文件,其余操作均如下


image.png


7.选中不同的scheme运行,即可实现多环境配置


image.png


image.png


image.png


注意
在自己创建的xcconfig进行设置一些Build Settings里的参数时,可能会覆盖掉pods里的设置,这时需要加上关键字$(inherited),这样就会继承pods文件中的设置。


链接:https://juejin.cn/post/7030327656738455565
收起阅读 »

iOS autorelease与自动释放池

iOS
autorelease、autorelease pool以及原理 autorelease与MRC、ARC autorelease:在MRC下,内存管理允许有三个操作,分别是release,retain,autorelease。release会使对象的引用计数...
继续阅读 »

autorelease、autorelease pool以及原理


autorelease与MRC、ARC



  • autorelease:在MRC下,内存管理允许有三个操作,分别是release,retain,autoreleaserelease会使对象的引用计数立刻-1,retain使对象的引用计数立刻+1,autorelease也会让对象的引用计数-1,但不是立刻-1.调用autorelease的对象会被加入到autorelease pool中,在合适的时间autorelease pool向对象调用release,也就是说,对象被延迟释放了。

  • 而在ARC下,Apple禁止了手动调用autorelease方法。使用@autoreleaseblock创建自动释放池后,runtime会自动向在block中的对象加上autorelease


autorelease pool



A thread's autorelease pool is a stack of pointers. Each pointer is either an object to release, or POOL_BOUNDARY which is an autorelease pool boundary. A pool token is a pointer to the POOL_BOUNDARY for that pool. When the pool is popped, every object hotter than the sentinel is released. The stack is divided into a doubly-linked list of pages. Pages are added and deleted as necessary. Thread-local storage points to the hot page, where newly autoreleased objects are stored.



以上是objc-781源码中NSObject.mm对于自动释放池的定义。从定义里面可以得知,自动释放池实际上是一个存放了指针的栈,栈中的指针有两类,一类是等待释放的对象指针,一类是名为POOL_BOUNDARY的哨兵指针。释放池之间以链表的形式相连,一个Page通常是4096个字节的大小(虚拟内存中的一页)。而前面提到的POOL_BOUNDARY哨兵指针的作用就是标示每个池子的尾端。


当在MRC中调用autorelease方法或者在ARC中将对象编写在@autoreleaseblock中,对象将会被注册到自动释放池中,当合适的时机到来自动释放池将会向这些对象调用release方法,以释放对象。



The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, it may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.



以上是Developer Documentation中Apple对于autorelease pool的一个介绍。可以看到在主线程中,每一个事件循环Runloop的开始,Appkit框架都会为程序创建一个自动释放池,并且在每次Runloop结束时释放所有在池中的对象。
需要注意的是,在这里Apple提到了一点:如果程序中临时创建了大量的autorelease对象,那么更好的做法是开发者自行新增一个释放池来最小化内存峰值的发生。


原理


First of all,最简单的代码


我们先来写出最常见的@autorelease代码。
-w461
当我们在Xcode中建立一个macOS项目时,通常模版中的main函数就包含了这样一段类似的代码。其中的@autorelease pool就是在ARC环境下使用自动释放池的API。


OC to C++


在终端中使用clang -rewrite-objc main.m命令将main.m文件转换成C++代码文件。
转换出来的代码会很多,我们挑重点的看。


-w511
在C++代码的main函数中,@autoreleasepool{}已经被转换成了如上代码。我们可以看到熟悉的objc_msgSeng,这是OC的灵魂-消息发送。
同时,@autoreleasepool{}变成了__AtAutoreleasePool,看来自动释放池的真实结构就是这个。我们再找一下它的定义在哪里。


通过搜索关键字,我们找到了它的定义语句。
-w543


可以看到,__AtAutoreleasePool结构体中定义了一个构造函数和一个析构函数,并调用了objc_autoreleasePoolPush()objc_autoreleasePoolPop()两个函数。


objc源码


这一步,我们到objc4-781代码中找上述两个方法的实现。
-w397
可以看到,这两个方法实际上是调用了AutoreleasePoolPage类的push()pop()方法,也就是说,自动释放池在runtime中的实际结构其实是AutoreleasePoolPage,这就是它的最终面目了。


-w596


AutoreleasePoolPage类继承于AutoreleasePoolPageData


-w766


从这里我们可以看到,autoreleasepool 是与线程一一对应的。同时线程池之间以双向链表相连。


这里引用网上一位同学分享的内存分布图
AutoreleasePoolPage


接着我们来看一下几个关键方法的具体实现。


autorelease


首先是autorelease方法。调用该方法会将对象加入到自动释放池中。
autorelease


第一行和第二行代码分别对传入参数做了一些检验,从第二行代码可以见到,如果传入的对象是TaggedPointer类型的,比如由小于等于9个字符的字面量字符串创建的NSString,将会有其他的处理操作。


autoreleaseFast-w465


该方法是autorelease方法的关键方法。可以看到第一行通过hotPage()方法拿到一个最近使用过的Page,然后来到流程控制。



  • 如果获取到了该hotPage并且Page还没有满,那么将对象加入到该Page中;

  • 如果Page满了,则调用autoreleaseFullPage方法创建一个新的page,将对象加入到新创建的page后并将新建立的page与通过hotPage()获取到的page相连接。

  • 如果没有获取到hotpage,那么将会调用autoreleaseNoPage方法建立并初始化自动释放池。


AutoreleasePoolPage::push()


在前面我们提到将对象加入到自动释放池时首先调用objc_autoreleasePoolPush方法,而该方法只起到了调用AutoreleasePoolPage::push()方法的作用。
-w688


其中,if-else的if分支是当出错时Debug会执行的流程,正常将会执行else分支里的代码。而autoreleaseFast()方法的实现在上一小段中已给出,这里传入的POOL_BOUNDARY就是哨兵对象。当创建一个自动释放池时,会调用该push()函数。


_objc_autoreleasePoolPrint()


使用_objc_autoreleasePoolPrint();方法可以打印出目前自动释放池中的对象,当然在使用前要先extern void _objc_autoreleasePoolPrint(void);.
该方法会调用AutoreleasePoolPage::printAll();打印出自动释放池中的相关信息。


-w553


从打印出的信息来看,自动释放池确实是跟线程一一对应的,并且在创建时会将一个哨兵对象加入到池中,这与我们在上文的代码分析结果相互映证。


写在最后


关于autoreleaseautoreleasePool的原理和代码的分析大概就是这些,当然还有很多具体实现是本文没有提到的,有兴趣的读者也可以自行到objc4-781的源码里找到NSObject.mm文件更加详细地研究。
总得来说,自动释放池机制延迟了对象的生命周期,并且可以为开发者自动释放需要被释放的对象,减少了内存泄漏发生的可能。


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

iOS 简单模拟 https 证书信任逻辑

iOS
废话开篇:https 证书是什么?如何进行认证呢?带着这些疑问来简单的实现一下验证过程简单的了解一下 https 在数据传输前的一些操作,如图:这里总结一下上面的流程图关键的步骤:1、认证网络请求的安全性:服务器会在建立真正的数据传输之前返回一个公钥数字证书。...
继续阅读 »

废话开篇:https 证书是什么?如何进行认证呢?带着这些疑问来简单的实现一下验证过程

简单的了解一下 https 在数据传输前的一些操作,如图:

image.png

这里总结一下上面的流程图关键的步骤:

1、认证网络请求的安全性

服务器会在建立真正的数据传输之前返回一个公钥数字证书。这里客户端需要在 URLSession 进行认证挑战方法回调里进行判断然后确定是否要继续进行请求。代理方法如下:

- (void)URLSession:(NSURLSession *)session

              task:(NSURLSessionTask *)task

didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge

 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler

可以这样理解,URLSession 做 https 网络请求的时候其实会把请求鉴权的权限通过代理的方法给暴露出来,是否信任并继续建立连接可以按照特定规则去执行(如自签证书),只有 https 请求会走代理方法,http 则不进行回调,这也是为什么 iOS系统 为什么提倡使用 https 的原因。

2、认证通过,通过公私钥非对称加密方式对最后的对称加密密钥进行加、解密:

这话听起来有点绕,基于第一步的公钥数字证书信任,那么,生成一个用于请求数据对称加密的密钥(对称加密更快),用这个公钥进行非对称加密,在由服务器的私钥进行解密,得到这个密钥,那么,真正建立的数据传输就以此密钥进行加、解密。

下面,模拟一下如何进行的公钥证书受信

创建 公钥.der 及 证书.cer 文件

在终端依次输入如下命令:


//生成私钥
openssl genrsa -out private_key.pem 1024

//获取 证书.cer
openssl req -new -key private_key.pem -out rsaCertReq.csr

openssl x509 -req -days 3650 -in rsaCertReq.csr -signkey private_key.pem -out rsaCert.crt

//将 .crt 格式证书转换为 .cer 格式证书,后面iOS程序里需要 .cer格式证书
openssl x509 -in rsaCert.crt -out rsaCert.cer -outform der

//获得 公钥.der
openssl x509 -outform der -in rsaCert.crt -out public_key.der



过程中会有一些简单信息输入,这里没有特别的要求,文件创建后目录如图:

image.png

把 .cer 格式证书 和 公钥.der 格式证书 全部拖到工程里:

image.png

下面输出一段代码,用 .cer 证书去验证 公钥.der 是否可信。


- (void)trustIsVaild

{
//获取工程下所有cer证书(https 网络请求鉴权必需证书)
    NSArray *paths = [[NSBundle mainBundle] pathsForResourcesOfType:@"cer" inDirectory:@"."];

//保存工程内的所有 cer 证书(并在后面设置为鉴权锚点)
    NSMutableArray *pinnedCertificates = [NSMutableArray array];

    for (NSString *path in paths) {

        NSData *certificateData = [NSData dataWithContentsOfFile:path];

        [pinnedCertificates addObject:( __bridge_transfer id)SecCertificateCreateWithData(NULL, ( __bridge CFDataRef)certificateData)];

    }

//获取工程下的公钥数字证书(在https网络请求认证挑战中由服务器返回)
    NSString * publicKeyPath = [[NSBundle mainBundle] pathForResource:@"public_key" ofType:@"der"];

    NSData *derData = [[NSData alloc] initWithContentsOfFile:publicKeyPath];

//证书资源
    SecCertificateRef myCertificate = SecCertificateCreateWithData(kCFAllocatorDefault, ( __bridge CFDataRef)derData);

//验证政策设置
    SecPolicyRef myPolicy = SecPolicyCreateBasicX509();

    SecTrustRef myTrust;

//SecTrust 赋值
    OSStatus status = SecTrustCreateWithCertificates(myCertificate,myPolicy,&myTrust);

    if (status == noErr) {
//设置证书锚点(这里的意思就是如果鉴权到指定的证书是有效的,那么,就信任此公钥数字签名,这里如果不设置,那么就会一直找向根证书,由于工程里的公钥数字证书是自签的,所以,一定不会受信)
        SecTrustSetAnchorCertificates(myTrust, ( __bridge CFArrayRef)pinnedCertificates);

        SecTrustResultType result;

        if (SecTrustEvaluate(myTrust, &result) == 0) {

//kSecTrustResultUnspecified 隐式信任
//kSecTrustResultProceed 可继续进行
            if ((result == kSecTrustResultUnspecified || result == kSecTrustResultProceed)) {

                NSLog(@"受信任的证书");

            } else {

                NSLog(@"未受信任的证书");

            }

        } else {

            NSLog(@"未受信任的证书初始化操作失败");

        }

    }

}

运行如下:

image.png

顺便输出一下不设置 证书锚点 控制台内容:

if (status == noErr) {
//不设置锚点
//SecTrustSetAnchorCertificates(myTrust, (__bridge CFArrayRef)pinnedCertificates);

        SecTrustResultType result;

        if (SecTrustEvaluate(myTrust, &result) == 0) {

            if ((result == kSecTrustResultUnspecified || result == kSecTrustResultProceed)) {

                NSLog(@"受信任的证书");

            } else {

                NSLog(@"未受信任的证书");
            }
        } else {

            NSLog(@"未受信任的证书初始化操作失败");
        }
    }

image.png

到这里,公钥证书如果受信,那么,下一步就规定一个 对称加密 session key 用这个公钥加密,发送到服务器,然后用对应的私钥解密,供以后的数据传输进行 对称加密 操作。

所以,移动端在做自定义证书鉴权的时候就需要存储服务器生成的 .cer 证书文件!

AFNetworking 下的鉴权方式处理相对复杂,因为 URLSession 的认证挑战回调是允许程序员全部无条件开启的,所以,AFNetworking 在默认鉴权行为的基础上添加了几种自定义鉴权方式:

typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {

    AFSSLPinningModeNone,//无条件开启

    AFSSLPinningModePublicKey,//认证公钥内容

    AFSSLPinningModeCertificate,//认证证书

};

而且,在此之前 AFNetworking 通过

@property (readwrite, nonatomic, copy) AFURLSessionTaskAuthenticationChallengeBlock authenticationChallengeHandler;

暴露给外界闭包进行自定义鉴权逻辑及处理结果。

- (void)URLSession:(NSURLSession *)session

              task:(NSURLSessionTask *)task

didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
BOOL evaluateServerTrust = NO;
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    NSURLCredential *credential = nil;

//AFNetworking 暴露给程序员自定义处理入口
if (self.authenticationChallengeHandler) {
id result = self.authenticationChallengeHandler(....);
... (解析处理结果)
}
...(证书认证处理代码)

//最后调用 completionHandler 继续执行操作
    if (completionHandler) {

        completionHandler(disposition, credential);

    }
}

disposition: 可以设置继续鉴权挑战(NSURLSessionAuthChallengeUseCredential) 或者中断鉴权挑战(NSURLSessionAuthChallengeCancelAuthenticationChallenge

credential: 如果证书认证通过则直接进行赋值,

credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];

否则为 nil

这里只是简单的梳理一下证书信任逻辑,就不再赘述 AFNetworking 源码部分。代码拙劣,大神勿笑。


作者:头疼脑胀的代码搬运工
链接:https://juejin.cn/post/7030345610704191501
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Dart中async和async*有什么区别?

水文我在行 在Dart中两个关键字,长得很像async和async*,可能还有的朋友还不知道他们两个有什么区别。现在简单介绍一下。 简单答案 简单回答这个问题就是: async返回Future. async*返回Stream. async async不必多...
继续阅读 »

水文我在行


在Dart中两个关键字,长得很像asyncasync*,可能还有的朋友还不知道他们两个有什么区别。现在简单介绍一下。


简单答案


简单回答这个问题就是:



  • async返回Future.

  • async*返回Stream.


async


async不必多言,有了解的都知道这是异步调用。
当一个函数被标记成async的时候,意味这个方法可能要从事耗时工作,比如说网络请求、处理图片等等。被async标记的方法会把返回值用Future包裹一下。


Future<int> doSomeLongTask() async {
await Future.delayed(const Duration(seconds: 1));
return 42;
}

我们可以通过await来获取Future里的返回值:


main() async {
int result = await doSomeLongTask();
print(result); // 等待一分钟后打印 '42'
}

async*


async*比多了一个*,加上*其实是函数生成器的意思。
async*标记的函数会在返回一组返回值,这些返回值会被包裹在Stream中。async*其实是为yield关键字发出的值提供了一个语法糖。


Stream<int> countForOneMinute() async* {
for (int i = 1; i <= 60; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i;
}
}

上面的其实就是异步生成器了。我们可以使用yield替代return返回数据,因为这个是时候我们的函数还在执行中。
此时,我们就可以使用await for去等待Stream发出的每一个值了。


main() async {
await for (int i in countForOneMinute()) {
print(i); // 打印 1 到 60,一个秒一个整数
}
}

应用


初一看,好像并没有什么用。因为自从我使用Flutter以来,我几乎没有使用过async*。但是现在假使我们有这样的一个需求,我们需要每一秒钟请求一次接口,一共请求10次,来看看京东还剩多少茅台。


首先看看使用async的代码:


  getMaoTai() async{
for (int i = 0; i <10; i++){
await Future.delayed(Duration(seconds: 1), ()async {
MaoTaiData data = await fetchMaoTaiData();
setState(){
//更新UI
};
});
}

上面的代码里使用了循环,然后每一秒钟请求依次接口,返回数据后调用setState()更新UI。这样做会导致你每隔一两秒就setState()一次,如果不怕性能问题,不怕产品经理打你,你这么玩玩。这个时候async*就应该上场了:


    Stream<MaoTaiData> getData() async* {
for (int i = 0; i <10; i++) {
await Future.delayed(Duration(seconds: 1));
yield await fetchMaoTaiData();
}
}

这样我们就可以使用StreamBuilder包裹下Widget,就不必每次都去setState()了。


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

Flutter + Rust 高性能的跨端尝试

稍作配置,同一份代码横跨 Android & IOS,相比于 React Native 方案更加高性能。除此之外,得益于 Rust 跨平台加持,Rust 部分的代码可在种种场合复用。 这篇文章旨在记录作者尝试结合 Rust 和 Flutter 的过程,...
继续阅读 »

稍作配置,同一份代码横跨 Android & IOS,相比于 React Native 方案更加高性能。除此之外,得益于 Rust 跨平台加持,Rust 部分的代码可在种种场合复用。


这篇文章旨在记录作者尝试结合 Rust 和 Flutter 的过程,且仅为初步尝试。不会涉及诸如:



  • 如何搭建一个 Flutter 开发环境,以及 Dart 语言怎么用

  • 如何搭建一个 Rust 开发环境,以及 Rust 语言怎么学


Environment



  • Flutter: Android, IOS 工具配置妥当
    -w672

  • Rust: Stable 就好
    -w513


Rust Part


Prepare cross-platform toolchains & deps


IOS


# Download targets for IOS ( 64 bit targets (real device & simulator) )
rustup target add aarch64-apple-ios x86_64-apple-ios

# Install cargo-lipo to generate the iOS universal library
cargo install cargo-lipo

Android


这里有一些行之有效的辅助脚本用于更加快捷配置交叉编译工具。




  1. 获取 Android NDK


    sdkmanager --verbose ndk-bundle

    如果已经准备好了 Android NDK ,则设置环境变量 $ANDROID_NDK_HOME


    # example:
    export ANDROID_NDK_HOME=/Users/yinsiwei/Downloads/android-ndk-r20b


  2. Create the standalone NDK


    # $(pwd) == ~/Downloads
    git clone https://github.com/kennytm/rust-ios-android.git
    cd rust-ios-android
    ./create-ndk-standalone.sh


  3. 在 Cargo default config VS 配置 Android 交叉编译工具


    cat cargo-config.toml >> ~/.cargo/config

    执行上述命令后会在 Cargo 默认配置中,增加有关 Android 跨平台目标 (targets, aarch64-linux-android, armv7-linux-androideabi, i686-linux-android) 的工具信息,指向刚刚创建的 standalone NDK


    [target.aarch64-linux-android]
    ar = ...
    linker = ..

    [target.armv7-linux-androideabi]
    ...

    [target.i686-linux-android]
    ..


  4. 下载 Rust 支持 Android 交叉编译的依赖





rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android
```


Start a simple rust library




  1. 创建一个 Rust 项目





cargo init my-app-base --lib
```




  1. 编辑 Cargo.toml 修改 crate-type





[lib]
name = "my_app_base"
crate-type = ["staticlib", "cdylib"]
```
Rust 构建出来的二进制库,在 IOS 中是静态链接进最终的程序之中,需要对构建 staticlib 的支持;在 Android 是通过动态链接在运行时装在进程序运行空间的,需要对构建 cdylib 的支持。




  1. 写一些符合 C ABI 的函数 src/lib.rs


    use std::os::raw::c_char;
    use std::ffi::CString;

    #[no_mangle]
    pub unsafe extern fn hello() -> *const c_char {
    let s = CString::new("world").unwrap();
    s.into_raw()
    }

    在上述代码中,每次当外部调用 hello 函数时,会在晋城堆空间中创建一个字符串 ( CString ),并将所有权 ( 释放该字符串所占堆空间的权利 ) 移交给调用者




Build libraries


# IOS
cargo lipo --release

# Android
cargo build --target aarch64-linux-android --release
cargo build --target armv7-linux-androideabi --release
cargo build --target i686-linux-android --release

然后在 target 目录下会得到以下有用的物料。


target
├── aarch64-linux-android
│   └── release
│   ├── libmy_app_base.a
│   └── libmy_app_base.so
├── armv7-linux-androideabi
│   └── release
│   ├── libmy_app_base.a
│   └── libmy_app_base.so
├── i686-linux-android
│   └── release
│   ├── libmy_app_base.a
│   └── libmy_app_base.so
├── universal
│   └── release
│   └── libmy_app_base.a

至此, Rust 部分就告于段落了。


Flutter Part


Copy build artifacts to flutter project


from: target/universal/release/libmy_app_base.a 
to: ios/

from: target/aarch64-linux-android/release/libmy_app_base.so
to: android/app/src/main/jniLibs/arm64-v8a/

from: target/armv7-linux-androideabi/release/libmy_app_base.so
to: android/app/src/main/jniLibs/armeabi-v7a/

from: target/i686-linux-android/release/libmy_app_base.so
to: android/app/src/main/jniLibs/x86/

Call FFI function in Dart




  1. 添加依赖


    pubspec.yaml -> dev_dependencies: += ffi: ^0.1.3




  2. 添加代码


    (直接在生成的项目上修改,暂不考虑代码设计问题,就简简单单的先把项目跑起来 )


    import 'dart:ffi';
    import 'package:ffi/ffi.dart';

    // ...
    final dylib = Platform.isAndroid ? DynamicLibrary.open('libmy_app_base.so') :DynamicLibrary.process();
    var hello = dylib.lookupFunction<Pointer<Utf8> Function(),Pointer<Utf8> Function()>('hello');

    // ...
    hello();
    // -> world


Build Android Project


flutter run # 如果连接着 Android 设备就直接运行了起来

Build IOS Project


( 复杂了许多 )



  1. 跟随 Flutter 官方文档,配置 XCode 项目。

  2. Build PhasesLink Binary With Libraries 添加 libmy_app_base.a 文件
    (按照图上箭头点...)
    -w1140

  3. Build SettingsOther Linker Flags 中添加 force_load 的参数。
    -w855


这是由于在 Dart 中通过动态的方式调用了该库的相关函数,但在编译期间静态分析的时候,这些都是未曾被调用过的无用函数,就被剪裁掉了。要通过 force_load 方式解决这个问题。


Result


2020-02-15 12.39.59-w300


ezgif-6-785f61b1b53b


Troubleshooting


XCode & IOS


Error getting attached iOS device: ideviceinfo could not find device


sudo xattr -d com.apple.quarantine ~/flutter/bin/cache/artifacts/libimobiledevice/ideviceinfo

将后面的路径替换成你的


dyld: Library not loaded


dyld: Library not loaded: /b/s/w/ir/k/homebrew/Cellar/libimobiledevice-flutter/HEAD-398c120_3/lib/libimobiledevice.6.dylib
Referenced from: /Users/hey/flutter/bin/cache/artifacts/libimobiledevice/idevice_id
Reason: image not found

删除&重新下载


rm -rf /Users/hey/flutter/bin/cache && flutter doctor -v

真机无法启动 Flutter 程序


参见 github.com/flutter/flu…
不要升级到 IOS 13.3.1 系统


What's next




  • 如何高效的实现 Rust & Dart 部分的通信


    我们知道 Flutter 和广大 GUI 库类似,属于单线程模型结合事件系统,因此在主线程中使用 FFI 调用 Rust 部分的代码不能阻塞线程。Dart 语言提供 async/await 语法特性用于在 Flutter 中处理网络请求等阻塞任务。而 Rust 也在最近版本中提供了 async/await 语法支持,如何优雅的把两部分结合起来,这是一个问题。




  • 对 MacOS Windows Linux 桌面端的支持


    Flutter 已经有了对桌面端的实验性支持,可以研究下如何结合在一起,实现跨 6 个端共享代码。




References



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

(转载)中国新观察|疯狂的元宇宙,暴富的新机会?

中新网客户端北京11月12日电(记者 李金磊)近期,元宇宙掀起疯狂热潮,众多中外科技公司竞相涌入,资本市场上的相关概念股暴涨,A股已有股票在20个交易日内涨幅翻倍。如此火爆的元宇宙,也勾起了民众的好奇心。元宇宙到底是什么?会颠覆未来的人类社会吗?又会催生哪些机...
继续阅读 »

中新网客户端北京11月12日电(记者 李金磊)近期,元宇宙掀起疯狂热潮,众多中外科技公司竞相涌入,资本市场上的相关概念股暴涨,A股已有股票在20个交易日内涨幅翻倍。

如此火爆的元宇宙,也勾起了民众的好奇心。元宇宙到底是什么?会颠覆未来的人类社会吗?又会催生哪些机遇?会否是下一个暴富机会?对于人类文明意味着什么?

对此,中国社会科学院数量经济与技术经济研究所信息化与网络经济研究室副主任、中国社会科学院信息化研究中心秘书长左鹏飞接受中新网“中国新观察”栏目专访进行了解读。

访谈实录摘编如下:

元宇宙基本场景的实现需10-20年时间

中新网:元宇宙大火,但是这个概念对于普通人来说还比较陌生,很多人认为元宇宙就是游戏,能否通俗地解释一下到底什么是元宇宙?目前市场上对于元宇宙的认识存在哪些误区?

左鹏飞:当前,元宇宙、区块链等重要概念缺乏公认的定义,在前沿科技和产业领域是一种普遍现象。元宇宙,作为一个正在快速发展的新生事物,从技术、感知、经济、行业等不同视角去分析,就如同从不同角度去观察一个多面体,必然会得出不同的结论。

例如,从技术视角看,元宇宙就是一个3D虚拟空间;从感知视角看,元宇宙是神经元感知的延伸和具化;从经济视角看,元宇宙是跨越实体和虚拟的新型数字经济形态;从认知视角看,元宇宙是一种机器视角对现实世界的认识,等等。其实,通俗来讲,元宇宙是一种可以大规模连接的虚拟现实应用场景。

除了认为元宇宙就是VR之外,目前对于元宇宙的认识误区,主要包括三类:

一是认为元宇宙是一个伪命题。在当前元宇宙热潮涌起的同时,也有不少人认为元宇宙只不过是一场新的割韭菜资本骗局,其实,在技术演进和人类需求的共同推动下,随着时间的不断推进,元宇宙应用的成熟只是一个时间问题。

二是认为元宇宙场景的实现将非常漫长。其实从目前的算力条件、网络技术和虚拟现实技术的发展现状来看,元宇宙基本场景的实现大概只需要10-20年的时间。

三是认为元宇宙只是大企业的游戏。大企业在元宇宙竞争赛道上虽然存在一定优势,但是作为新生事物,中小企业在这个新领域同样具备“从0到1”的突破能力。同时,初创企业更容易在元宇宙发展过程中,做到快速适应和融入创新。

美国Facebook公司宣布更名为Meta。中新社记者 刘关关 摄

庞大网民规模是中国在元宇宙赛道的最大优势

中新网:众多科技巨头纷纷布局元宇宙,脸书(Face book)甚至直接为元宇宙改名“Meta”,为什么元宇宙概念突然如此火爆?中国企业发展元宇宙有哪些优势?

左鹏飞:元宇宙概念在2021年突然爆发,主要有三方面原因:

第一,从规律和趋势来看,支持元宇宙场景落地的相关技术正在接近技术奇点,技术跃变效应正在形成;

第二,从互联网发展阶段来看,当前已经进入后互联网时代,传统网络红利已经到顶并开始消退,元宇宙作为虚拟世界和现实世界融合的载体,对企业来说,蕴含着巨大发展机遇;

第三,从市场先行者来看,罗布乐思(Roblox)作为元宇宙第一股,今年3月份上市以来,业绩远超预期,市场的正反馈效应明显。

中国企业发展元宇宙的优势主要包括三方面:第一,庞大的网民规模。当前,我国网民规模已经超过10亿,庞大的人口基数是丰富和完善元宇宙应用场景的重要力量,这将是中国企业在元宇宙赛道上最大的优势。

第二,丰富的创新经验。近20年来,互联网行业是中国竞争最激烈的行业之一,企业之间竞争长期白热化,只有勇于创新、善于创新、乐于创新的企业才能生存下来。这些实战积累形成的创新经验也是中国企业优势之一。

第三,有力的政策支持。从我国数字经济发展所取得的成就来看,无论是中央还是地方,都积极支持企业在数字经济领域探索新技术、新业态,并通过具体政策进行支持。今年以来,已有多个国家部委和地方政府出台(支持)VR产业发展的相关政策。

抖音打造的虚拟博主柳夜熙爆红。

元宇宙将打破人们所习惯的现实世界物理规则

中新网:从电影《头号玩家》中打造的避世虚拟游戏世界绿洲,到今年上映的《失控玩家》里虚拟NPC(非玩家角色)的自我觉醒,再到抖音打造的虚拟博主柳夜熙爆红,您怎么看这种现象?元宇宙真的如脸书创始人扎克伯格所言会颠覆未来的人类社会吗?

左鹏飞: 近几年,伴随虚拟现实技术的不断进步,各类平台和机构纷纷打造虚拟偶像。今年由于元宇宙的突然爆发,部分虚拟偶像顺势爆红,柳夜熙就是其中之一。

这一现象发生的背后,具有更深层涵义:一方面,从用户角度来说,高度拟人化、共情化的虚拟偶像,满足了用户的新鲜体验,增强了用户对于元宇宙场景的初步认知,具有较为深远的意义;另一方面,从企业角度来说,虚拟偶像日益成为企业价值和理念的载体,未来有可能成为企业最显著的标志,因此越来越多的企业推出自己的虚拟偶像。

元宇宙是一把双刃剑。一方面,元宇宙的发展,会让更多的人沉浸在虚拟世界中。从上网时间来看,人类平均上网时间呈现逐年增长趋势,而且青少年一代表现更加明显,元宇宙带来全新的视觉冲击,肯定会进一步延长人类的“在网时间”。

另一方面,元宇宙的发展,将打破我们所习惯的现实世界物理规则,在虚拟空间,重新定义我们绝大部分的生产生活方式,以全新的生产方式和合作方式提高全社会生产效率。元宇宙对于人类社会的影响,取决于人类本身。

因此,我们需要前瞻性研究元宇宙发展的原则规范、技术伦理等一系列内容,让元宇宙更好地服务人类,而不是成为时间黑洞,吞噬人类的未来。

资料图:游客体验VR技术 梁婷 摄

元宇宙产业崛起将带来新财富格局

中新网:普华永道预计元宇宙市场规模在2030年将达到1.5万亿美元。从现实看,元宇宙发展处于什么阶段?会催生哪些机遇?是否为下一个暴富机会?

左鹏飞:元宇宙的发展大致可以分为三个阶段:萌芽阶段,由企业创造一些简单的虚拟现实内容(提供)给用户,用户在虚拟现实场景下可以进行简单的人与人交互;成长阶段,用户在元宇宙场景下,可以进行复杂的人与人交互,以及简单的人机交互;成熟阶段,用户、机器在元宇宙场景下可以进行高度互动。

我们目前处于萌芽阶段,大概需要10-20年时间,才能进入成长阶段。

从产业链角度来看,元宇宙发展将催生六个方面的机遇:

首先,影响元宇宙应用场景(实现)的直接相关技术产业,如XR、AI、空间映射、数字孪生等领域;其次,支撑元宇宙场景运行的底层技术产业,元宇宙需要实现大量底层技术进行深度融合,如区块链、云计算、大数据、未来网络、半导体等领域;第三,虚拟与现实连接(技术)产业。即把虚拟世界与现实世界连接在一起的相关技术,如可穿戴设备、脑机接口、微传感器等领域;第四,元宇宙(应用)场景下的产业“元化”,如购物、娱乐、社交、学习、办公等领域;第五,视觉仿真下的创意产业,一种虚拟世界的新型创造的经济;第六,助力元宇宙系统健康有序运行的支付、安全等辅助产业。

数字化革命正在重塑传统产业格局,而新产业的崛起,往往会带来新的财富格局。目前,已有多个国际知名咨询机构,对元宇宙市场前景表示乐观。我们认为,各行各业,只要能与元宇宙的发展做到良好衔接,也就抓住了一次新的财富增长机遇。

谨防借元宇宙概念的名义催生科技泡沫

中新网:现在资本市场热炒元宇宙概念,相关股票暴涨。从当下看,元宇宙市场估值是否被高估了,已产生了泡沫?

左鹏飞:今年以来,全球主要投资机构纷纷投资元宇宙领域,推高了元宇宙的关注度和热度,带来了相关概念股的大涨,在一定程度上增加了元宇宙泡沫。资本市场习惯热捧新概念,在此之前,也热炒过区块链、人工智能、大数据等概念,因此很多人担心元宇宙会成为一个新的泡沫。

与以往概念有所不同,元宇宙承载着真实世界的延伸与拓展,企业和用户对其场景实现有着较为明确的目标和预期,是由技术进步与人类需求共同推进的。正因如此,我们更应保持理性的行为认知和正确的市场导向,推动其产业逐步走向成熟,谨防借技术和概念的名义催生科技泡沫。

儿童戴着VR眼镜体验“太空飞行”。中新社记者 任东 摄

元宇宙发展对反垄断和隐私保护是个大挑战

中新网:我们距离真正的元宇宙世界还有多远,面临哪些挑战和难题?若真的实现,它将给我们的生活和社会经济发展带来哪些巨变?

左鹏飞:我们距离元宇宙基本场景的实现大概需要10-20年时间,除了技术瓶颈以外,我们主要面临四个方面的挑战:

第一,如何确立元宇宙运行的基本框架?元宇宙是现实经济社会的场景模拟,其中涉及到价值观念、制度设计和法律秩序等一系列基本框架选择问题。

第二,如何避免形成高度垄断?元宇宙场景的实现,需要巨大的人力和物力投入,同时又要实现超大规模的连接,因此元宇宙具有一种内在垄断基因。我们需要避免元宇宙被少数力量所垄断。

第三,如何维系现实世界和元宇宙之间的正面互动关系?也就是用好元宇宙这把双刃剑,谨防人们沉浸在元宇宙场景中不能自拔,要发挥元宇宙的积极作用。

第四,如何保护隐私和数据安全?元宇宙的发展,需要搜集人们更多的个人信息,保护个人隐私和数据安全将是一个非常大的挑战。

元宇宙将给我们的生活和社会经济发展带来五个方面的巨变:一是从技术创新和协作方式上,进一步提高社会生产效率;二是催生出一系列新技术新业态新模式,促进传统产业变革;三是推动文创产业跨界衍生,极大刺激信息消费;四是重构工作生活方式,大量工作和生活将在虚拟世界发生;五是推动智慧城市建设,创新社会治理模式。

资料图:参观者体验高科技眼镜。 中新社记者 许少峰 摄

元宇宙或让人类衍生出“双世界”文明形态

中新网:有人认为,如果人类在走向太空文明之前就实现了元宇宙世界,这将是一场灾难。您是否认同这个观点?您如何看待元宇宙对人类文明走向的影响?

左鹏飞:技术的本质是实现与所处环境更好相融的一种手段。走向太空文明和进入元宇宙世界,分别反映了人类对外部世界探索和内心世界需求的两种不同技术追求。认为人类在走向太空文明之前实现元宇宙(世界)将是一场灾难,其根源是担心人类沉浸于虚拟世界不能自拔,失去了探索精神。这种担忧具有一定的合理性,但是用“灾难”来形容可能并不合适。

第一,这两种技术追求,本身并不是一种对立关系,而是可以并行的。最基本的考虑是,并非所有人都会喜欢虚拟世界,就如同并非所有人都喜欢游戏一样,所以不用担心所有人都会沉浸在虚拟世界;第二,我们发展元宇宙过程中,必然会注重形成一种现实世界和元宇宙之间的正面互动关系,对负向发展会进行必要纠正,如同现在的游戏防沉迷系统。第三,探索太空文明很重要,我们与地球和谐相处也很重要,元宇宙的发展会促进我们与地球环境更好相容。

元宇宙会引导人类积极探索内心世界,在虚拟世界创造理想生活,这在一定程度上影响人类现实社会活动。因此,伴随元宇宙发展,未来可能形成一种虚拟世界与现实世界高度互动,衍生出一种在观念、习惯、技术、思维等层面相互补和平衡的“双世界”文明形态。

受访嘉宾简介:

左鹏飞,博士、副研究员,中国社会科学院数量经济与技术经济研究所信息化与网络经济研究室副主任、中国社会科学院信息化研究中心秘书长,主要研究领域:信息技术经济、互联网经济、信息化。


收起阅读 »