注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一个Vue3可使用的JSON转excel组件

JSON to Excel for VUE3在浏览器中将JSON格式数据以excel文件的形式下载。该组件是基于this thread 提出的解决方案。支持Vue3.2.25及以上版本使用重要提示! Microsoft Excel中的额外提示此组件中实现的方法...
继续阅读 »



JSON to Excel for VUE3

在浏览器中将JSON格式数据以excel文件的形式下载。该组件是基于this thread 提出的解决方案。支持Vue3.2.25及以上版本使用

重要提示! Microsoft Excel中的额外提示

此组件中实现的方法使用HTML表绘制。在xls文件中,Microsoft Excel不再将HTML识别为本机内容,因此在打开文件之前会显示警告消息。excel的内容已经完美呈现,但是提示信息无法避免,请不要在意!

Getting started

安装依赖:

npm install vue3-json-excel

在vue3的应用入口处有两种注册组件的方式:

import Vue from "vue"
import {vue3JsonExcel} from "vue3-json-excel"

Vue.component("vue3JsonExcel", vue3JsonExcel)

或者

import Vue from "vue"
import vue3JsonExcel from "vue3-json-excel"

Vue.use(vue3JsonExcel)

在template文件中直接使用即可

<vue3-json-excel :json-data="json_data">
Download Data
</vue3-json-excel>

Props List

NameTypeDescriptionDefaultremark
json-dataArray即将导出的数据

fieldsObject要导出的JSON对象内的字段。如果未提供任何属性,将导出JSON中的所有属性。

export-fields (exportFields)Object用于修复使用变量字段的其他组件的问题,如vee-validate。exportFields的工作原理与fields完全相同

typestringMime 类型 [xls, csv]xls1.0.x版本暂时只支持xls,csv会在下个版本迭代
namestringFile 导出的文件名jsonData.xls
headerstring/Array数据的标题。可以是字符串(一个标题)或字符串数组(多个标题)。

title(deprecated)string/Array与header相同,title是出于追溯兼容性目的而维护的,但由于与HTML5 title属性冲突,不建议使用它。

License

MIT

Status

该项目处于早期开发阶段。欢迎参与共建。
有好的产品建议可以联系我!!!!

npm地址

vue3-json-excel

作者:小章鱼
来源:https://segmentfault.com/a/1190000041117522

收起阅读 »

kotlin函数

1.概念函数是执行操作并可以返回值的离散代码块。在 Kotlin 中,函数是使用 fun 关键字声明的,并且可以使用接收具名值或默认值的参数。与特定类关联的函数称为方法。一个用于执行特定任务的代码块它可以将大型的程序分解为小型的模块使用关键字 fun 来声明可...
继续阅读 »

1.概念

函数是执行操作并可以返回值的离散代码块。在 Kotlin 中,函数是使用 fun 关键字声明的,并且可以使用接收具名值或默认值的参数。与特定类关联的函数称为方法。

  • 一个用于执行特定任务的代码块
  • 它可以将大型的程序分解为小型的模块
  • 使用关键字 fun 来声明
  • 可以通过参数接收具名值或默认值


2.函数的组成部分

我们可以使用 fun 关键字,并紧跟一个函数名来定义一个函数。例如:
fun sayHello(text:String) :Unit{
println("hello world!")
}

其中 sayHello就是函数名小括号()内text是函数参数大括号{}中是函数具体代码Unit是返回值(当函数没有任何返回的时候那它的返回值便是UnitUnit 类型只包含一个值,即:Unit 。返回Unit类型的声明是可选的。)。

以上函数等价于:

fun sayHello(text:String){
println("hello world!")
}

2.1函数参数

函数可能会有:

  • 默认参数

  • 必选参数

  • 具名参数

我们重新编写一个 sayHello 函数,这个函数接收一个 String 类型的参数: name,并且会打印出输入的姓名。name 的默认值为 "张三"。

fun sayHello(name: String = "张三") {
println("你好:${name}")
}

sayHello() =>你好:张三

sayHello("李四") =>你好:李四

sayHello(name="王五") =>你好:王五

在 main() 函数中,有三种方法可以调用 sayHello() 函数:

  • 在调用函数时,使用默认参数;
  • 调用函数时,不使用参数名传入 speed;
  • 调用函数时,传入名为 speed 的参数。

如果没有为某个参数指定默认值,则该参数为必选参数。

fun sayHello(name: String) {
println("你好:${name}")
}

sayHello() =>编译错误:Kotlin: No value passed for parameter 'name'

sayHello("李四") =>你好:李四

sayHello(name="王五") =>你好:王五

为了提升可读性,可以向必选参数传入具名值。就是我们上述代码中的 sayHello(name="王五")调用方式 。

函数可以同时拥有默认参数和必选参数。我们改造一下sayHello函数 。

fun sayHello(name: String, age: Int = 6, sex: String = "男") {
println("你好:${name} ,你的年龄:$age ,你的性别:$sex")
}
sayHello(name = "王五") =>你好:王五,你的年龄:6,你的性别:男

sayHello("王五",sex = "女") =>你好:王五 ,你的年龄:6 ,你的性别:女

参数可以通过它们的名称进行传递 (具名参数)。在函数包含大量参数或者默认参数时,具名参数非常方便。默认参数和具名参数可以减少重载并优化代码可读性。


3.紧凑函数

如果一个函数只返回了一个表达式,那么它的大括号可以省略,而函数体可以在 “=” 符号后指定。

fun getSum(x: Int, y: Int): Int {          =>完整版
return x + y
}

fun getSum(x: Int, y: Int): Int = x + y =>紧凑版


4.Lambda 表达式与高阶函数

Kotlin 中函数是 first-class(头等函数)

  • Kotlin 的函数可以储存在变量和数据结构中
  • 函数可以作为其他高阶函数的参数和返回值
  • 可以使用高阶函数创建新的 “内建” 函数

4.1 Lambda 函数

除了传统的命名函数外,Kotlin 还支持 lambda 表达式。lambda 是用来创建函数的表达式,但不同于声明已命名的函数,以这种方式声明的函数没有名称。lambda 很好用的一点便是可以作为数据传递。在其他编程语言中,lambda 也被称为匿名函数、函数字面量或其他类似的名称。
像具名函数一样,lambda 也可以有参数。对于 lambda 来说,参数 (及其类型,如果需要声明的话) 位于函数箭头 -> 的左侧。要执行的代码在函数箭头的右侧。将 lambda 赋值给变量后,就可以像函数一样调用它。

kotlin可以声明一个存储函数的变量

val getSum = { x: Int, y: Int -> x + y }
println(getSum(1, 2)) =>3

其中x,y就是参数。->为函数箭头 ,需要执行的代码再其右侧。其中getSum就是变量名。

4.2高阶函数

高阶函数接收函数作为参数,或以函数作为返回值。

fun getSum(x: Int, y: Int, sum: (Int, Int) -> Int): Int {
return sum(x, y)
}
val sum: (Int, Int) -> Int = { x, y -> x + y }
println(getSum(1, 2, sum)) =>3

此段代码中函数体调用了作为第三个参数传入的函数,并将第一个第二个参数传递给该函数。

lambda 的真正作用在于可以使用它们创建高阶函数,高阶函数接收另一个函数作为参数。

使用函数类型可将其实现与使用处分离。

4.3传递函数引用

 使用:: 操作符将具名函数作为参数传入另一个函数。

fun sum(x: Int, y: Int): Int {
return x + y
}
println(getSum(1, 2, ::sum))

:: 操作符让 Kotlin 知道我们正在将函数引用作为参数传递,这样一来 Kotlin 便不会尝试调用该函数。

在使用高阶函数时,Kotlin 倾向于将接收函数的参数作为函数的最后一个参数。Kotlin 有一个特别的语法,叫做尾随参数调用语法,可以让我们的代码更加简洁。在下面代码中中,我们可以为函数参数传递一个 lambda,而不必将 lambda 放在括号中。

println(getSum(1, 2,{ x, y -> x + y }) )

等价于
println(getSum(1, 2) { x, y -> x + y })

Kotlin 内建的许多函数,其声明方式都遵循了尾随参数调用语法。

inline fun repeat(times: Int, action: (Int) -> Unit)

repeat(3) {
println("hello world!")
}
收起阅读 »

小程序框架对比(Taro VS uni-app)

前前段时间,要开发一个小程序,需要选一个跨平台的框架,为此做了一些调研,在这里记录一下。 目前的跨平台方案大致是以下三种类型,各有优劣。 结合项目自身情况,我选择了第三种类型的框架,再结合支持多平台的要求,重点锁定在了Taro和uni-app之间。 ...
继续阅读 »

前前段时间,要开发一个小程序,需要选一个跨平台的框架,为此做了一些调研,在这里记录一下。


目前的跨平台方案大致是以下三种类型,各有优劣。


image.png


结合项目自身情况,我选择了第三种类型的框架,再结合支持多平台的要求,重点锁定在了Tarouni-app之间。















































框架技术栈微信小程序H5App支付宝/百度小程序
TaroReact/Vue
uni-appVue
WePYVue
mpvueVue

Taro


开发者:


京东


优缺点:


Taro在App端使用的是React Native的渲染引擎,原生的UI体验较好,但据说在实时交互和高响应要求的操作方面不是很理想。


微信小程序方面,结合度感觉没有那么顺滑,有一些常见功能还是需要自己去封装。


另外就是开发环境难度稍高,需要自己去搭建iOS和Android的环境,对于想要一处开发到处应用的傻瓜式操作来讲,稍显繁琐。


但Taro 3的出现,支持了React 和 Vue两种DSL,适合的人群会更多一点,并且对快应用的支持也更好。


案例:


image.png


学习成本:


React、RN、小程序、XCode、Android Studio


uni-app


开发者:


DCloud


优缺点:


uni-app在App渲染方面,提供了原生渲染引擎和小程序引擎的双选方案,加上自身的一些技术优化(renderjs),对于高性能和响应要求的场景展现得更为流畅。


另外它整体的开发配套流程也做得很容易上手。比如有丰富的插件市场,使用简单,支持大量常用场景。


还比如它的定制IDE——HBuilder,提供了强大的整合能力。在用HBuilder之前,我心想:“还要多装一个编辑器麻烦,再好用能有VS Code好用?”用过之后:“真香!”


虽然用惯了VS Code对比起来还是有一些痛点没有解决,但是对于跨平台开发太友好了,其他缺点都可以忍受。HBuilder里支持直接跳转到微信开发者工具调试,支持真机实时预览,支持直接打包小程序和App,零门槛上手。


image.png


不过,uni-app也还是不够成熟,开发中也存在一些坑,需要不时到论坛社区去寻找答案。


代表产品:


image.png


学习成本:


Vue、小程序


总结


跨平台方案目前来看都不完善,适合以小程序、H5为主,原生APP(RN)为辅,不涉及太过复杂的交互的项目。


uni-app 开发简单,小项目效率高,入门容易debug难,不适合中大型项目。
Taro 3 开发流程稍微复杂一点,但复杂项目的支持度会稍好,未来可以打通React和Vue,已经开始支持RN了。



  1. 不考虑原生RN的话二者差不多,考虑RN目前Taro3不支持,只能选uni-app;

  2. 开发效率uni-app高,有自家的IDE(HBuilderX),编译调试打包一体化,对原生App开发体验友好;

  3. 个人技术栈方面倾向于Taro/React,但项目角度uni-app/Vue比较短平快,社区活跃度也比较高。

作者:sherryhe
链接:https://juejin.cn/post/6974584590841167879

收起阅读 »

前端重新部署后,领导跟我说页面崩溃了..

背景: 每次前端更新,重新部署后,用户还停留在更新之前的页面,当请求页面数据时,会导致页面白屏,报错信息如下: Uncaught ChunkLoadError: Loading chunk {n} failed. 原因 每次更新后,用户端的html文件中的 j...
继续阅读 »

背景:


每次前端更新,重新部署后,用户还停留在更新之前的页面,当请求页面数据时,会导致页面白屏,报错信息如下:


Uncaught ChunkLoadError: Loading chunk {n} failed.


原因


每次更新后,用户端的html文件中的 js 和 css 名称就和服务器上的不一致导致,导致加载失败。


解决方案


1.对error事件进行监听,检测到错误之后重新刷新


      window.addEventListener(
'error',
function (event) {
if (
event.message &&
String(event.message).includes('Loading chunk') &&
String(event.message).includes('failed')
) {
window.location.replace(window.location.href);
}
},
true,
);

2.对window.console.error事件监听,效果同上


      window.console.error = function () {
console.log(JSON.stringify(arguments), 'window.console.error'); // 自定义处理
};

3.其他方案


如:HTTP2.0推送机制 / fis3 构建 /webSocket通知等,未尝试


注:有好的方案可以在下面评论讨论哈


本篇收录在个人工作记录专栏中,专门记录一些比较有意思的场景和问题。


后记


在之后的某一天,该问题再次暴露出来。源于一位同事在使用过程中,会不定页面的出现报错情况,报错如下:


image.png


很明显,还是资源加载问题,按道理讲应该可以走入我们逻辑进行刷新。但是当时用户反馈:刷新也不能解决问题,强制刷新才可以解决。


分析:刷新为什么不能解决问题?其实还是因为当用户第一次因为网络原因未成功加载到资源,后续刷新均走的缓存,因此让用户手动去刷新解决不了问题。


解决方案:


不知道大家发现没有,上面对error事件进行监听代码中,并没有包括css失败的情况,因此匹配上css加载失败的情况即可。


更新代码如下:


      window.addEventListener(
'error',
function (event) {
if (
event.message &&
String(event.message).includes('chunk') &&
String(event.message).includes('failed')
) {
window.location.replace(window.location.href);
}
},
true,
);

可能有人会问,既然刷新解决不了问题,那window.location.replace(window.location.href)可以解决吗?


其实刷新的方法有很多,根据MDN的说法,Location.replace() 方法会以给定的URL替换当前的资源,因此可解决此问题。



作者:纵有疾风起
链接:https://juejin.cn/post/6981718762483679239

收起阅读 »

上一个程序员提桶跑路了!我接手后用这些方法优化了项目

平常我们在开发和维护项目的过程中,如果我们跑的项目有点大啊,或者数据太多,导致项目跑起来弊蜗牛还要慢,然后用户体验还不友好,对于新手程序员来说!老板天天都要你加班改!你还不敢辞职!这种时候,就很让人头痛了,怎么办! 但是!也不是没有办法的!骚年!你当时学vue...
继续阅读 »

平常我们在开发和维护项目的过程中,如果我们跑的项目有点大啊,或者数据太多,导致项目跑起来弊蜗牛还要慢,然后用户体验还不友好,对于新手程序员来说!老板天天都要你加班改!你还不敢辞职!这种时候,就很让人头痛了,怎么办!


但是!也不是没有办法的!骚年!你当时学vue的时候可不是这样说的!


接下来我来给你浓重介绍几个优化性能的小技巧,让你的项目蹭蹭蹭的飞起来!老板看了都直呼内行!


1.v-if和v-show的使用场景要区分


v-if 是条件渲染,当条件满足了,那肯定是渲染哇!如果你需要设置一个元素随时隐藏或者消失,然后用v-if是非常的浪费性能的,因为它不停的创建然后销毁。


但是它也是惰性的,如果你开始一给它条件为 false,它就害羞不出来了,跟你家女朋友一样天天晚上都不跟你回家,甚至你家里都没有你女朋友的衣物!然后结构页面里也不会渲染出来查不到这个元素,不知情的朋友以为你谈了个虚拟女友,直到你让它条件为 true ,它才开始渲染,也就是拿了结婚证才跟你回家。


v-show 就很简单,他的原理就是利用 css display 的属性,让他隐藏或者出现,所以一开始渲染页面哪怕我们看不到这个元素,但是它在文档的话,是确确实实存在的,只是因为 display:none; 隐藏了。




就像是你的打气女朋友,平常有人你肯定不敢打气哇!肯定是等夜深人静的时候,才偷偷打气,然后早上又继续放气藏起来,这样是不是很方便咧!所以这个元素你也就是你打气女朋友每天打气放气,是不是也没有那么费力咧!白天就可以藏起来快乐的上班啦!


好啦划重点啦!不要瞎鸡巴想什么女朋友了,女朋友只会影响我码项目的速度!


所以这样看来,如果是很少改变条件来渲染或者销毁的,建议是用 v-if ,如果是需要不断地切换,不断地隐藏又出现又隐藏这些场景的话, v-show 更适合使用!所以要在这些场景里合适的运用 v-if v-show 会节省很多性能以达到性能优化。


2.v-if和v-for不能同时使用


v-if v-for 一起使用时, v-for **** v-if 更高的优先级。这样就意味着 v-if 将分别重复运行于每一个 v-for 循中,那就是先运行 v-for 的循环,然后在每一个 v-for 的循环中,再进行 v-if 的条件对比,会造成性能浪费,影响项目的速度




如果按照下面的写法(我是用vue3写的),好家伙!直接不工作了,没有报错也没有页面,诡异的很


<template>
<div id="app">
<div v-for="item in list" v-if="list.flag" :key="item">{{item.color}}</div>
</div>
</template>
<script>
export default {
data(){
return{
list:[
{
color:"red",
flag:true
},
{
color:"green",
flag:false
},
{
color:"blue",
flag:true
},
],
}
}}
</script>

当你真的需要根据条件渲染页面的时候,建议是采用计算属性,这样及高效且美观,又不会出问题,如下面的代码展示


<template>
<div id="app">
<div v-for="item in newList" :key="item">{{item.color}}</div>
</div>
</template>
<script>
export default {
data(){
return{
list:[
{
color:"red",
flag:true
},
{
color:"green",
flag:false
},
{
color:"blue",
flag:true
},
],
}
},
computed:{
newList(){
return this.list.filter(list => {
return list.flag
})
}
}
}
</script>

3.computed和watch使用要区分场景


先看一下计算属性computed,它支持缓存,当依赖的数据发生变化的时候,才会重新计算,但是它并不支持异步操作,它无法监听数据变化。而且计算属性是基于响应式依赖进行缓存的。


再看一下侦听属性watch,它不支持缓存,它支持异步操作,当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。这是和computed最大的区别。


所以说,如果你的需求是写像购物车那种的,一个属性受其他属性影响的,用计算属性 computed 就像是你家的二哈,你不带它出去玩,你一回家就发现你家能拆的都被二哈拆掉了,因为你不带它出去跟你女朋友逛街!


如果是像写那种像模糊查询的,可以用侦听属性 watch ,因为可以一条数据影响多条数据。 比如你双12给你女朋友买了很多东西,那双十二之后,是不是很多机会回不去宿舍咧!


用好这两个,可以让你的代码更加高效,看起来也更加简洁优雅,让项目蹭蹭跑起来!这样都是一种优化性能的方式!


4.路由懒加载


当Vue项目是单页面应用的时候,可能会有很多路由引入 ,这样的话,使用 webpcak 打包后的文件会非常的大,当第一次进入首页的时候,加载的资源很多多,页面有时候会出现白屏的情况,对用户很不友好,体验也差。


但是,当我们把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应的组件,这样就很高效了。会大大提升首屏加载显示的速度,但是可能其他的页面的速度就会降下来。有利有弊吧,根据自己业务需求来使用,实现效果也非常的简单,在 router index.js 文件下,如下所示


import Home from '../views/Home'

const routes = [
{
path:'/home',
name:"Home", //这里没有用懒加载
component:Home
},
{
path:'/about',
name:'About',
component:()=>import(/*webpackChunkName:"about"*/ '../views/About.vue') //这里用了懒加载
}
]

打开浏览器运行,当我没有点击进入 about 组件的时候包的大小就如蓝色框住的那些,当我点击了 about 组件进入后,就增加了后面红色圈住的包,总的大小是增加了



所以,使用路由懒加载可以降低首次加载的时候的性能消耗,但是后面打开这些组件可能会有所减慢,建议是如果体积不大的又不用马上展示的页面可以使用路由懒加载降低性能消耗,从而做到性能优化!


5.第三方插件按需引入


比如我们做一个项目,如果是全局引入第三方插件,打包构建的时候,会将别人整个插件包也一起打包进去,这样的话文件是非常庞大的,然后我们就需要将第三方插件按需引入,这个就需要自己去根据每个插件的官方文档在项目配置,始终就是一句话,用什么引什么!


6.优化列表的数据


当我们遇到哪些,一开始取的数据非常的庞大,然后还要渲染在页面上的时候,比如一下子给你传回来10w条数据还要渲染在页面上,项目一下子渲染出来是非常的有难度的。


这个时候,我们就 需要采用窗口化的技术来优化性能,只需要渲染少部分的内容(比如一下子拿多少条数据),这样就可以减少重新渲染组件和创建dom节点的时间。可以看看下面代码


<template>
<div>
<h3>列表的懒加载</h3>
<div>
<div v-for="item in list">
<div>{{ item }}</div>
</div>
</div>
<div>
<div v-if="moreShowBoolen">滚动加载更多</div>
<div v-else>已无更多</div>
</div>
</div>
</template>
<script>
export default {
name: 'List',
data() {
return {
list: [],
moreShowBoolen: false,
nowPage: 1,
scrollHeight: 0,
};
},
mounted() {
this.init();
// document.documentElement.scrollTop获取当前页面滚动条的位置,documentElement对应的是html标签,body对应的是body标签
// document.compatMode用来判断当前浏览器采用的渲染方式,CSS1Compat表示标准兼容模式开启
window.addEventListener("scroll", () => {
let scrollY=document.documentElement.scrollTop || document.body.scrollTop; // 滚动条在Y轴上的滚动距离
let vh=document.compatMode === "CSS1Compat"?document.documentElement.clientHeight:document.body.clientHeight; // 页面的可视高度
let allHeight = Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
); // 整个页面的高度
if (scrollY + vh >= allHeight) {
// 当滚动条滑到页面底部的时候触发这个函数继续添加数据
this.init2();
}
});
},
methods: {
init() {
//一开始就往list添加数据
for (let i = 0; i < 100; i++) {
this.list.push(i);
}
},
init2() {
for (let i = 0; i < 200; i++) {
// 当滑动到底部的时候,继续触发这个函数
this.list.push(i);
}
},
},
};
</script>

这样的话,就可以做到数据懒加载啦,根据需要,逐步添加数据进去,减少一次性拉取所有数据,因为数据是非常庞大的,这样就可以优化很多性能了!


作者:零零后程序员小三
链接:https://juejin.cn/post/7041471019327946759

收起阅读 »

axios 封装,API接口统一管理,支持动态API!

vue
分享一个自己封装的 axios 网络请求 主要的功能及其优点: 将所有的接口放在一个文件夹中管理(api.js)。并且可以支持动态接口,就是 api.js 文件中定义的接口可以使用 :xx 占位,根据需要动态的改变。动态接口用法模仿的是vue的动态路由,如果你...
继续阅读 »

分享一个自己封装的 axios 网络请求


主要的功能及其优点:


将所有的接口放在一个文件夹中管理(api.js)。并且可以支持动态接口,就是 api.js 文件中定义的接口可以使用 :xx 占位,根据需要动态的改变。动态接口用法模仿的是vue的动态路由,如果你不熟悉动态路由可以看看我的这篇文章:Vue路由传参详解(params 与 query)


1.封装请求:



  1. 首先在 src 目录下创建 http 目录。继续在 http 目录中创建 api.js 文件与 index.js 文件。

  2. 然后再 main.js 文件中导入 http 目录下的 index.js 文件。将请求注册为全局组件。

  3. 将下面封装所需代码代码粘到对应的文件夹


2.基本使用:


//示例:获取用户列表
getUsers() {
 const { data } = await this.$http({
   url: 'users' //这里的 users 就是 api.js 中定义的“属性名”
})
},

3.动态接口的使用:


//示例:删除用户
deleteUser() {
 const { data } = await this.$http({
   method: 'delete',
   //动态接口写法模仿的是vue的动态路由
   //这里 params 携带的是动态参数,其中 “属性名” 需要与 api 接口中的 :id 对应
   //也就是需要保证携带参数的 key 与 api 接口中的 :xx 一致
   url: {
     // 这里的 name 值就是 api.js 接口中的 “属性名”
     name: 'usersEdit',
     params: {
       id: userinfo.id,
    },
  },
})
},

4.不足:


封装的请求只能这样使用 this.$http() 。不能 this.$http.get()this.$http.delete()


由于我感觉使用 this.$http() 这种就够了,所以没做其他的封装处理


如果你有更好的想法可以随时联系我


如下是封装所需代码:



  • api.js 管理所有的接口


// 如下接口地址根据自身项目定义
const API = {
 // base接口
 baseURL: 'http://127.0.0.1:8888/api/private/v1/',
 // 用户
 users: '/users',
 // “修改”与“删除”用户接口(动态接口)
 usersEdit: '/users/:id',
}

export default API


  • index.js 逻辑代码


// 这里请求封装的主要逻辑,你可以分析并将他优化,如果有更好的封装方法欢迎联系我Q:2356924146
import axios from 'axios'
import API from './api.js'

const instance = axios.create({
 baseURL: API.baseURL,
 timeout: '8000',
 method: 'GET'
})

// 请求拦截器
instance.interceptors.request.use(
 config => {
   // 此处编写请求拦截代码,一般用于加载弹窗,或者每个请求都需要携带的token
   console.log('正在请求...')
   // 请求携带的token
   config.headers.Authorization = sessionStorage.getItem('token')
   return config
},
 err => {
   console.log('请求失败', err)
}
)

// 响应拦截器
instance.interceptors.response.use(
 res => {
   console.log('响应成功')
   //该返回对象会绑定到响应对象中
   return res
},
 err => {
   console.log('响应失败', err)
}
)

//options 接收 {method, url, params/data}
export default function(options = {}) {
 return instance({
   method: options.method,
   url: (function() {
     const URL = options.url

     if (typeof URL === 'object') {
       //拿到动态 url
       let DynamicURL = API[URL.name]

       //将 DynamicURL 中对应的 key 进行替换
       for (const key of Object.keys(URL.params)) {
         DynamicURL = DynamicURL.replace(':' + key, URL.params[key])
      }

       return DynamicURL
    } else {
       return API[URL]
    }
  })(),
   //获取查询字符串参数
   params: options.params,
   //获取请求体字符串参数
   data: options.data
})
}



  • main.js 将请求注册为全局组件


import Vue from 'vue'

// 会自动导入 http 目录中的 index.js 文件
import http from './http'

Vue.prototype.$http = http

作者:寸头男生
链接:https://juejin.cn/post/7006508579595223070

收起阅读 »

AES 前后端加解密方案

AES
AES 前后端加解密方案 背景 最近有一个需求:后端对敏感数据进行加密传输给前端,由前端解密后进行回显。在讨论之后,定下了AES加解密方案 概念 AES: 密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijn...
继续阅读 »

AES 前后端加解密方案


背景


最近有一个需求:后端对敏感数据进行加密传输给前端,由前端解密后进行回显。在讨论之后,定下了AES加解密方案


概念


AES: 密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准,是最为常见的对称加密算法


密码说明


AES算法主要有四种操作处理,分别是:



  1. 密钥轮加(Add Round Key)

  2. 字节代换层(SubBytes)

  3. 行位移层(Shift Rows)

  4. 列混淆层(Mix Column)


主要是讲使用方案,所以这里不说太多废话了,对算法感兴趣的同学移步这里, 讲的非常详细,不过文章里的代码是使用C语言写的,为此找到了github上aes.js 的源码,感兴趣的同学移步这里


前端实现


现在简单说一下前端的实现:


我先找到了github上的源码,看了一下大概800行的样子。本来打算直接改吧改吧,封装成一个加解密的工具方法,直接扔在utils目录里的。本来也很成功的改好了,本地加解密试了一下,效果也很不错。根据github链接上的readme文档说明,封装了如下函数:


// 省略了改完的aes.js的代码。。。

// 加密 text 需要加密的文本 key 密钥
const toAESBytes = (text, key) => {
const textBytes = aesjs.utils.utf8.toBytes(text);
const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
const encryptedBytes = aesCtr.encrypt(textBytes);
const encryptedHex = aesjs.utils.hex.fromBytes(encryptedBytes);
console.log('加密后的文本:', encryptedHex);
return encryptedHex;
};

// 解密
const fromAESBytes = (text, key) => {
const encryptedBytes = aesjs.utils.hex.toBytes(text);
const aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(5));
const decryptedBytes = aesCtr.decrypt(encryptedBytes);
const decryptedText = aesjs.utils.utf8.fromBytes(decryptedBytes);
console.log('解密后的文本:', decryptedText);
return decryptedText;
}

但是这个方法在和后端对接的时候出现了一点偏差,死活也不能将后端加密后的数据成功解密。于是又向后端同学请教了一下,发现原因如下:


在AES加解密算法中,除了加解密的密文,也就是key需要一样之外,还有几样东西也非常重要:




  • AES 的算法模式需要保持一致


    关于算法模式,主要有以下几种:


      1. 电码本模式 Electronic Codebook Book (ECB);
    2. 密码分组链接模式 Cipher Block Chaining (CBC)
    3. 计算器模式Counter (CTR)
    4. 密码反馈模式(Cipher FeedBack (CFB)
    5. 输出反馈模式Output FeedBack (OFB)

    这么看的话,我上面的demo应该使用的就是计算器模式了!关于算法模式的介绍,感兴趣的同学请移步这里




  • 补码方式保持一致


    关于补码方式,我查到的以下几种:


      1. PKCS5Padding PKCS7Padding的子集,块大小固定为8字节
    2. PKCS7Padding 假设数据长度需要填充n(n>0)个字节才对齐,那么填充n个字节,每个字节都是n;如果数据本身就已经对齐了,则填充一块长度为块大小的数据,每个字节都是块大小。
    3. ZeroPadding 数据长度不对齐时使用0填充,否则不填充



  • 密钥长度保持一致


    AES算法一共有三种密钥长度:128、192、256。这个前后端的密钥长度确实是保持一致的。




  • 加密结果编码方式保持一致


    一般情况下,AES加密结果有两种编码方式:base64 和 16进制




所以到底是哪里出了问题呢?后端同学好心发给了我他后端的代码:


/**
* aes 加密 Created by xingxiping on 2017/9/20.
*/
public class AesUtils {
private static final String CIPHER_ALGORITHM = "AES"; // optional value AES/DES/DESede

private AesUtils(){

}
/**
* 加密
*
* @param content
* 源内容
* @param key
* 加密密钥
* @return
*/
public static String encrypt(String content, String key) throws Exception {
Cipher cipher = getCipher(key, Cipher.ENCRYPT_MODE);
byte[] byteContent = content.getBytes(StandardCharsets.UTF_8);
byte[] result = cipher.doFinal(byteContent);
return Base64Utils.encode(result);
}

/**
* 解密
*
* @param content
* 内容
* @param key
* 解密密钥
* @return
*/
public static byte[] decrypt(String content, String key) throws Exception {
Cipher cipher = getCipher(key, Cipher.DECRYPT_MODE);
byte[] bytes = Base64Utils.decode(content);
bytes = cipher.doFinal(bytes);
return bytes;
}

private static Cipher getCipher(String key, int cipherMode) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException {
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES");
cipher.init(cipherMode, secretKey);
return cipher;
}
}

破案了,后端老哥对加密后的结果进行了base64编码,然后我又仔细去看了一下aes.js源码,根本没有找到base64的影子啊!


于是在查找一翻资料以后,决定使用crypto-j,使用 Crypto-JS 可以非常方便地在 JavaScript 进行 MD5、SHA1、SHA2、SHA3、RIPEMD-160 哈希散列,进行 AES、DES、Rabbit、RC4、Triple DES 加解密。真是方便呀,老规矩,感兴趣的同学可以移步这里


以下是我又一轮的解决步骤:




  1. npm install crypto-js




  2. 在utils目录下新建一个文件aes.js




  3. 封装如下代码:


    // aes 解密
    import CryptoJS from 'crypto-js';

    // 解密 encryptedStr待解密字符串 pass 密文
    export const aesDecode = (encryptedStr, pass) => {
    const key = CryptoJS.enc.Utf8.parse(pass); // 通过密钥获取128位的key
    const encryptedHexStr = CryptoJS.enc.Base64.parse(encryptedStr); // 解码base64编码结果
    const encryptedBase64Str = CryptoJS.enc.Base64.stringify(encryptedHexStr);
    const decryptedData = CryptoJS.AES.decrypt(encryptedBase64Str, key, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7
    });
    return decryptedData.toString(CryptoJS.enc.Utf8);
    }



  4. 然后就可以正常调用了!




最后,终于成功解密!


一点点小感悟


在日常工作中真的很少使用算法,对称加密在学校里听起来好像非常简单的样子,但是真的应用到生活中,特别是安全领域,还是非常复杂的。哎,学无止境吧~


感谢大家的阅读!


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

聊一聊ThreadLocal,终于搞明白了

ThreadLocal是什么? 试想以下情况: 在多线程的情况下,对一同一个变量操作可能会出现冲突,解决的办法就是对这个变量加锁。但是我们有时候其实就是想要一个全局变量,不想让多个线程去干扰。那么能不能有一个变量,名字相同,但是多个线程操作的时候又不会相互影响...
继续阅读 »

ThreadLocal是什么?


试想以下情况:


在多线程的情况下,对一同一个变量操作可能会出现冲突,解决的办法就是对这个变量加锁。但是我们有时候其实就是想要一个全局变量,不想让多个线程去干扰。那么能不能有一个变量,名字相同,但是多个线程操作的时候又不会相互影响呢?


从另外一个角度来说,对于一个变量,在一个线程的任何一个地方都可能需要用到,但是通过传参的方式又比较麻烦,有没有一个变量是贯穿整个线程,我们想取就能取到的呢。


ThreadLocal就是这么一个变量,那么这个变量是怎么实现的呢?


ThreadLocal源码分析


ThreadLocal github地址


首先看用法


public class Client {
private static final ThreadLocal<String> myThreadLocal = ThreadLocal.withInitial(() -> "This is the initial value");

public static void main(String[] args) {

for (int i = 0; i < 6; i++){
new Thread(new MyRunnable(), "线程"+i).start();
}

}

public static class MyRunnable implements Runnable {

@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name + "的threadLocal"+ ",设置为" + name);
myThreadLocal.set(name);
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {}
System.out.println(name + ":" + myThreadLocal.get());
}

}
}

------

线程0的threadLocal,设置为线程0
线程3的threadLocal,设置为线程3
线程4的threadLocal,设置为线程4
线程2的threadLocal,设置为线程2
线程1的threadLocal,设置为线程1
线程5的threadLocal,设置为线程5
线程0:线程0
线程4:线程4
线程5:线程5
线程2:线程2
线程1:线程1
线程3:线程3

例子中有六个线程,myThreadLocal里存的都是自己线程独有的变量。这样就实现了变量的线程隔离,而且如果不向传参数,在另外一个函数里直接就能get到这个变量,这对于很多场景下都非常有用。


我们下面来分析一下源码:



  1. 首先每一个Thread,都有一个ThreadLocalMap,变量名字叫做threadLocas,里面保存的是多个ThreadLocal,所以每一个线程才能保存属于自己线程的值。

  2. ThreadLocal封装了getMap()、Set()、Get()、Remove()4个核心方法。主要是对ThreadLocalMap来进行操作。

  3. ThreadLocalMap是一个ThreadLocal的内部类,它实现了一个自定义的Map,ThreadLocalMap中的Entry[]数组存储数据。

  4. Entry的键是threadLocal变量本身,值就是设置的变量的值。Entry的key是对ThreadLocal的弱引用,当ThreadLocal不再有强引用的时候,就会清理掉这个key,防止内存泄漏(然而并不能,后面会说)


5abe86d1459c394b7552c1ef9d7e370c.png


get方法


public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}



  1. 获取当前的Thread对象,通过getMap获取Thread内的ThreadLocalMap,ThreadLocalMap的定义如下:ThreadLocal.ThreadLocalMap threadLocals = null;

  2. 如果map已经存在,以当前的ThreadLocal为键,获取Entry对象,并从从Entry中取出值

  3. 否则,调用setInitialValue进行初始化。


setInitialValue


private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}



  1. 首先是调用initialValue生成一个初始的value值,深入initialValue函数,我们可知它就是返回一个null;

  2. 然后还是在get以下Map,如果map存在,则直接map.set,这个函数会放在后文说;

  3. 如果不存在则会调用createMap创建ThreadLocalMap,这里正好可以先说明下ThreadLocalMap了。


ThreadLocalMap


createMap


void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;

private Entry[] table;

private int size = 0;

private int threshold; // Default to 0

private void setThreshold(int len) {
threshold = len * 2 / 3;
}

private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
...
}


  1. 首先是Entry的定义,前面已经说过;

  2. 初始的容量为INITIAL_CAPACITY = 16

  3. 主要数据结构就是一个Entry的数组table;

  4. size用于记录Map中实际存在的entry个数;

  5. threshold是扩容上限,当size到达threashold时,需要resize整个Map,threshold的初始值为len * 2 / 3

  6. nextIndex和prevIndex则是为了安全的移动索引,后面的函数里经常用到。


map.getEntry


private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}



  1. 计算索引位置

  2. 获取Entry,如果Entry存在,且key和threadLocal相等,那么返回

  3. 否则,调用getEntryAfterMiss。


getEntryAfterMiss


private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}


  1. 如果k==key,那么代表找到了这个所需要的Entry,直接返回;

  2. 如果k==null,那么证明这个Entry中key已经为null,那么这个Entry就是一个过期对象,这里调用expungeStaleEntry清理该Entry。



为什么会需要清理呢?


如果说ThreadLocal变量被人为的置为null了,ThreadLocal对象只有一个弱引用指着,就会被GC,Entry的key没有了,value可能会内存泄漏。ThreadLocal在每一个get,set的时候都会清理这种过期的key。


为什么需要循环查找key?


这是一种解决hash冲突的手段,这里用的是开放地址法,既有冲突之后,把要插入的元素放在要插入的位置后面为null的地方。HashMap则采用的是链地址法。



expungeStaleEntry


private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;

// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}


  1. expunge entry at staleSlot:这段主要是将i位置上的Entry的value设为null,Entry的引用也设为null,那么系统GC的时候自然会清理掉这块内存;

  2. Rehash until we encounter null: 这段就是扫描位置staleSlot之后,null之前的Entry数组,清除每一个key为null的Entry,同时若是key不为空,做rehash,调整其位置。



这里rehash的作用是什么?


我们清理的过程中会把某个值设置为null,如果之前这个值后面的区域是和前两连起来的,那么下次循环查找的时候,就会只查到null为止。比如三个hash值碰撞的key,中间的那个被删除了,那么第三个key在查找的时候会从第一个开始查找,查找到第二个就停止了,第三个就查不到了。



set


public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

map.set


private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}


  1. 首先还是根据key计算出位置i,然后查找i位置上的Entry,

  2. 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值。

  3. 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry

  4. 不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。

  5. 最后调用cleanSomeSlots,这个函数就不细说了,你只要知道内部还是调用了上面提到的expungeStaleEntry函数清理key为null的Entry就行了,最后返回是否清理了Entry,接下来再判断sz>thresgold,这里就是判断是否达到了rehash的条件,达到的话就会调用rehash函数。


rehash


private void rehash() {
expungeStaleEntries();

// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}


private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;

for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}

setThreshold(newLen);
size = count;
table = newTab;
}


  1. 首先,size大于threshold的时候才会rehash。

  2. 清理空key,如果size大于3/4的threshold,调用resize()函数。

  3. 每次扩容大小扩展为原来的2倍,然后再一个for循环里,清除空key的Entry,同时重新计算key不为空的Entry的hash值,把它们放到正确的位置上,再更新ThreadLocalMap的所有属性。


remove


private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}

先计算出hash值,若是第一次没有命中,就循环直到null,在此过程中也会调用expungeStaleEntry清除空key节点。


什么是内存泄漏?


当程序分配了空间但是却忘了回收导致以后的程序都无法或暂时无法使用这块空间,就发生了内存泄漏。和内存溢出不一样,内存溢出是内存不足的时候出现的。这块要理解清楚,才能明白ThreadLocal为什么会导致内存泄漏。


Java 引用类型


要说到ThreadLocal引起内存泄漏,还得从java的四种引用类型说起。


java中有四种引用类型,分别是强软弱虚。


强引用


一个对象被强引用,那么他就不会被回收。


软引用


如果一个对象只具有软引用,那么它的性质属于可有可无的那种。如果此时内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。


软引用可以和一个引用队列联合使用,如果软件用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。


    Object obj = new Object();
ReferenceQueue queue = new ReferenceQueue();
SoftReference reference = new SoftReference(obj, queue);
//强引用对象滞空,保留软引用
obj = null;
当内存不足时,软引用对象被回收时,reference.get()为null,此时软引用对象的作用已经发挥完毕,这时将其添加进ReferenceQueue 队列中

如果要判断哪些软引用对象已经被清理:


    SoftReference ref = null;
while ((ref = (SoftReference) queue.poll()) != null) {
//清除软引用对象
}

弱引用


弱引用和软引用的区别就是,如果一个对象只有弱引用,那么只要GC,不管内存够不够,都会回收他的内存。注意这里的”只有弱引用“。如果这个对象还被其他变量强引用,那么他是不会被回收的。


虚引用


如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动。





































引用类型被垃圾回收时间用途生存时间
强引用从来不会对象的一般状态JVM停止运行时终止
软引用在内存不足时对象缓存内存不足时终止
弱引用在垃圾回收时对象缓存垃圾回收时终止
虚引用UnkonwnUnkonwnUnkonwn


为什么要有四种引用类型?



  1. 可以让程序员通过代码的方式来决定某个对象的生命周期。

  2. 有利于垃圾回收

  3. 能够实现一些复杂的数据结构。



ThreadLocal什么情况下会出现内存泄漏?


threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露。


如果这个时候还会去调用get set方法,那么这块内存可能会被清理掉。


如果没有去调用get set方法,如果这个线程很快销毁了,那么也不会内存泄漏。


最坏的情况就是,threadLocal对象设置成null了,然后使用线程池,这个线程被重复使用了,但是有一直没有调用get set方法,这个期间就会发生真正的内存泄漏。


其实ThreadLocal发生内存泄露的条件还是比较苛刻的,只要是使用规范,那么就没有什么问题。


ThreadLocal最佳实践



  1. 每次使用完手动调用remove函数,删除不再使用的ThreadLocal.

  2. 可以将ThreadLocal设置成private static的,这样ThreadLocal会尽量和线程本身一起消亡。


ThreadLocal应用案例



管理数据库连接。


  假如A类的方法a中,会调用B类的方法b和C类的方法c,a方法开启了事务,b方法和c方法会去操作数据库。我们知道,要想实现事务,那么b方法和c方法中所使用的的数据库连接一定是同一个连接,那怎么才能实现所用的是同一个数据库连接呢?答案就是通过ThreadLocal来管理。


MDC日志链路追踪。


MDC(Mapped Diagnostic Contexts)主要用于保存每次请求的上下文参数,同时可以在日志输出的格式中直接使用 %X{key} 的方式,将上下文中的参数输出至每一行日志中。而保存上下文信息主要就是通过ThreadLocal来实现的。
假如在交易流程每个环节的日志中,你都想打印全局流水号transId,流程可能涉及多个系统、多个线程、多个方法。有一些环节中,全局流水号并不能当做参数传递,那你怎么才能获取这个transId参数呢,这里就是利用了Threadlocal特性。每个系统或者线程在接收到请求时,都会将transId存放到ThreadLocal中,在输出日志时,将transId获取出来,进行打印。这样,我们就可以通过transId,在日志文件中查询全链路的日志信息了。



InheritableThreadLocal


使用ThreadLocal时,子线程获取不到父线程通过set方法保存的数据,要想使子线程也可以获取到,可以使用InheritableThreadLocal类。


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

【墙裂推荐】球球了,RPC之间调用别再使用 if-else做参数校验了

RPC
RPC调用时使用 @Validated进行参数校验不起作用 球球了,RPC之间调用别再使用 if-else做参数校验了。众所周知,@Validated 是一款非常好用的参数校验工具。但在RPC调用时不可用,在当前的微服务大环境下,微服务之间的调用怎么做到优雅...
继续阅读 »

RPC调用时使用 @Validated进行参数校验不起作用


球球了,RPC之间调用别再使用 if-else做参数校验了。众所周知,@Validated 是一款非常好用的参数校验工具。但在RPC调用时不可用,在当前的微服务大环境下,微服务之间的调用怎么做到优雅的参数校验呢?


话不多说,直接上干货


引包


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

1. 参数校验 这里我们先要定义一个注解来代替来继承 @Validated


import org.springframework.validation.annotation.Validated;

@Validated
public @interface RPCValidated {
}

2. 然后使用AOP来解析参数,进行参数校验。


import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
import org.hibernate.validator.resourceloading.PlatformResourceBundleLocator;
import org.springframework.stereotype.Component;
import wangjubao.base.common.extend.Response;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Aspect
@Component
@Slf4j
public class ValidatedAop {
private static Validator validator;

static {
validator = Validation.byDefaultProvider().configure()
.messageInterpolator(new ResourceBundleMessageInterpolator(
new PlatformResourceBundleLocator("validationMessages")))
.buildValidatorFactory().getValidator();
}

@Around("@annotation(com.qiaoba.annotation.RPCValidated))")
public Object around(ProceedingJoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 执行方法参数的校验
Set<ConstraintViolation<Object>> constraintViolations = validator.forExecutables().validateParameters(joinPoint.getThis(), signature.getMethod(), args);
List<String> messages = new ArrayList<>();
for (ConstraintViolation<Object> error : constraintViolations) {
messages.add(error.getMessage());
}
if (!messages.isEmpty()) {
return Response.paramError("参数错误:", messages.get(0));
}
try {
return joinPoint.proceed(args);
} catch (Throwable e) {
e.printStackTrace();
return Response.error("操作失败:", e.getMessage());
}
}
}

3. 使用方法,在接口Impl实现类加上定义的@RPCValidated


@Override
@RPCValidated
public Response create(MessageSmsRechargeDto dto) {
//todo:业务逻辑......
}

4. 在Interfaces接口层加上@Valid注解


Response create(@Valid MessageSmsRechargeDto params);

5. 实体类


@Data
public class MessageSmsRechargeDto implements Serializable {


/**
* 充值公司
*/
@NotNull(message = "充值公司不能为空 ")
private Long companyId;

/**
* 充值备注
*/
@NotEmpty(message = "充值备注不能为空 ")
private String rechargeRemark;
}

--- 至此,整个流程完成,加上自定义的参数校验注解@RPCValidated后,就可以优雅的进行参数校验,不用再写各种if-else 做参数校验了


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

Flutter APP 前期准备工作

组件库可参考:flutter.dev、bruno(贝壳开源组件库) 以下从部分GetX文档转载 用于记录。 框架: Flutter GetX GetX 是 Flutter 上的一个轻量且强大的解决方案:高性能的状态管理、智能的依赖注入和便捷的路由管理。 Get...
继续阅读 »

组件库可参考:flutter.dev、bruno(贝壳开源组件库)


以下从部分GetX文档转载 用于记录。
框架: Flutter GetX


GetX 是 Flutter 上的一个轻量且强大的解决方案:高性能的状态管理、智能的依赖注入和便捷的路由管理。


GetX 有3个基本原则:


性能: GetX 专注于性能和最小资源消耗。GetX 打包后的apk占用大小和运行时的内存占用与其他状态管理插件不相上下。效率: GetX 的语法非常简捷,并保持了极高的性能,能极大缩短你的开发时长。 结构: GetX 可以将界面、逻辑、依赖和路由完全解耦,用起来更清爽,逻辑更清晰,代码更容易维护。 GetX 并不臃肿,却很轻量。


三大功能


状态管理


目前,Flutter有几种状态管理器。但是,它们中的大多数都涉及到使用ChangeNotifier来更新widget,这对于中大型应用的性能来说是一个很糟糕的方法。你可以在Flutter的官方文档中查看到,ChangeNotifier应该使用1个或最多2个监听器,这使得它们实际上无法用于任何中等或大型应用。


Get 并不是比任何其他状态管理器更好或更差,而是说你应该分析这些要点以及下面的要点来选择只用Get,还是与其他状态管理器结合使用。


Get不是其他状态管理器的敌人,因为Get是一个微框架,而不仅仅是一个状态管理器,既可以单独使用,也可以与其他状态管理器结合使用。


Get有两个不同的状态管理器:简单的状态管理器(GetBuilder)和响应式状态管理器(GetX)。


响应式状态管理器


响应式编程可能会让很多人感到陌生,因为觉得它很复杂,但是GetX将响应式编程变得非常简单。



  • 你不需要创建StreamControllers.

  • 你不需要为每个变量创建一个StreamBuilder。

  • 你不需要为每个状态创建一个类。

  • 你不需要为一个初始值创建一个get。


使用 Get 的响应式编程就像使用 setState 一样简单。


让我们想象一下,你有一个名称变量,并且希望每次你改变它时,所有使用它的小组件都会自动刷新。


这就是你的计数变量。


var name = 'Jonatas Borges';

要想让它变得可观察,你只需要在它的末尾加上".obs"。


var name = 'Jonatas Borges'.obs;

而在UI中,当你想显示该值并在值变化时更新页面,只需这样做。


Obx(() => Text("${controller.name}"));

这就是全部,就这么简单。


关于状态管理的更多细节


关于状态管理更深入的解释请查看这里。在那里你将看到更多的例子,以及简单的状态管理器和响应式状态管理器之间的区别


你会对GetX的能力有一个很好的了解。


路由管理


如果你想免上下文(context)使用路由/snackbars/dialogs/bottomsheets,GetX对你来说也是极好的,来吧展示:


在你的MaterialApp前加上 "Get",把它变成GetMaterialApp。


GetMaterialApp( // Before: MaterialApp(
home: MyHome(),
)

导航到新页面


Get.to(NextScreen());

用别名导航到新页面。查看更多关于命名路由的详细信息这里


Get.toNamed('/details');

要关闭snackbars, dialogs, bottomsheets或任何你通常会用Navigator.pop(context)关闭的东西。


Get.back();

进入下一个页面,但没有返回上一个页面的选项(用于闪屏页,登录页面等)。


Get.off(NextScreen());

进入下一个页面并取消之前的所有路由(在购物车、投票和测试中很有用)。


Get.offAll(NextScreen());

注意到你不需要使用context来做这些事情吗?这就是使用Get路由管理的最大优势之一。有了它,你可以在你的控制器类中执行所有这些方法,而不用担心context在哪里。


关于路由管理的更多细节


关于别名路由,和对路由的低级控制,请看这里


依赖管理


Get有一个简单而强大的依赖管理器,它允许你只用1行代码就能检索到与你的Bloc或Controller相同的类,无需Provider context,无需inheritedWidget。


Controller controller = Get.put(Controller()); // 而不是 Controller controller = Controller();


  • 注意:如果你使用的是Get的状态管理器,请多注意绑定api,这将使你的界面更容易连接到你的控制器。


你是在Get实例中实例化它,而不是在你使用的类中实例化你的类,这将使它在整个App中可用。 所以你可以正常使用你的控制器(或类Bloc)。


提示:  Get依赖管理与包的其他部分是解耦的,所以如果你的应用已经使用了一个状态管理器(任何一个,都没关系),你不需要全部重写,你可以使用这个依赖注入。


controller.fetchApi();

想象一下,你已经浏览了无数条路由,现在你需要拿到一个被遗留在控制器中的数据,那你需要一个状态管理器与Provider或Get_it一起使用来拿到它,对吗?用Get则不然,Get会自动为你的控制器找到你想要的数据,而你甚至不需要任何额外的依赖关系。


Controller controller = Get.find();
//是的,它看起来像魔术,Get会找到你的控制器,并将其提供给你。你可以实例化100万个控制器,Get总会给你正确的控制器。

然后你就可以恢复你在后面获得的控制器数据。


Text(controller.textFromApi);

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

cocoapods-binary工作原理及改进

iOS
「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」 在iOS开发中,如果能够对一些稳定的组件能够二进制化,那么将大大的缩减我们在开发过程中的编译时间。在基于Cocaopods工程,快速实现Swift组件二进制一文中,我们讲述了,借助P...
继续阅读 »

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


iOS开发中,如果能够对一些稳定的组件能够二进制化,那么将大大的缩减我们在开发过程中的编译时间。在基于Cocaopods工程,快速实现Swift组件二进制一文中,我们讲述了,借助Pods工程和Shell脚本,一步实现二进制打包,但需要我们手动更改podspec文件,采用这种方式,如果作为依赖项加入到其他工程中,还会出现二进制源码共存的情况,今天介绍一个cocoapods插件 cocoapods-binary,可以实现组件预编译,该工程已经两年多没维护了,随着pods更新,有了一些小bug。基于源码,我对该插件做了几点更改,又可以开心的玩耍了.在了解该插件之前,我们先大概了解下,输入pod install之后发生了什么?


Pod install


如果你想调试cocoapods工程,可以查看之前的文章Ruby和Cocoapods文章合集


一图胜千言,当输入Pod install


Pod install.png



  • 1,首先校验是否有Podfile文件,并解析为Podfile对象

  • 2,准备阶段,安装插件,调用pre_install阶段的插件

  • 3,解析依赖,通过 当前的Podfile文件 和 上一次的Pofile.Lock,Manifest.Lock文件进行比对,确认哪些文件需要更新。

  • 4,下载依赖,更新需要更改的文件,并执行Podfile里面定义的pre_installhook函数。

  • 5,集成阶段,生成新的Pods.xcodeproj工程文件,并执行Podfile里面的post_installhook函数。

  • 6,写入新的Lockfile信息。

  • 7,执行 post_install阶段的插件,并输出安装信息。


cocoapods-binary是以插件的形式,在Pod工程的pre_install阶段进行预编译的,


cocoapods-binary工作流


pre_install 插件


cocoapods-binary中的,通过HookManager来注册插件的执行时机


Pod::HooksManager.register('cocoapods-binary', :pre_install) do |installer_context|

end


主流程


主要流程如下图所示


截屏2021-11-28 下午7.20.16.png



  • 1,必须使用framework的形式,也就是use_frameworks

  • 2,在Pods文件夹下,创建一个名为_Prebuild的文件夹,作为预编译沙箱

  • 3,在当前环境下,读取Podfile文件,并创建Podfile对象。

  • 4,读取Podflie.lock文件,创建 Lockfile对象。

  • 5,创建预编译安装器,沙箱地址为 预编译沙箱

  • 6,对比PodfilePodfile.lock,得到已更改的pod_name

  • 7,使用预编译安装器pod_name的源代码下载到预编译沙箱中,并生成新的Pods.xcodeproj文件。开始编译需要更新的framework

  • 8,回到主工程,继续执行 pod install的后续流程,在这一步修该需要二进制文件的podspec文件。


解析自定义参数


在插件中,有两个自定义的参数 :binaryall_binary!,是通过自定义DSL来实现的,有对这一块不熟悉的,可以参考我的这篇文章Cocoapods之 Podfile文件


Podfile.png
创建Podfile对象时,通过 method swizzling来hook:parse_inhibit_warnings方法,拿到我们在Podfile文件中写入的配置选项。将需要预编译pod,保存到数组中。


old_method = instance_method(:parse_inhibit_warnings)
define_method(:parse_inhibit_warnings) do |name, requirements|
variables = requirements
parse_prebuild_framework(name, requirements)
old_method.bind(self).(name, variables)
end
复制代码

Ruby中,Method Swizzling主要分为三步:



  • 1,获取parse_inhibit_warnings实例方法。

  • 2,定义一个相同名字的方法。

  • 3,调用原来的方法。


对比Lockfile


lockfile.png
在这里 Podfle.lock预编译沙箱Manifest.lock是一样的,通过对比可以一个Hash对象


<Pod::Installer::Analyzer::SpecsState:0x00007f83370c61a8 @added=#<Set: {}>, @deleted=#<Set: {}>, @changed=#<Set: {}>, @unchanged=#<Set: {"Alamofire", "SnapKit"}>>


可以很清楚的知道哪些pod库发生了更改。如果有改动,则就在预编译沙箱进行install


binary_installer.install!


pre_install.png
在这一阶段,主要是在预编译沙箱中拉取framework源码和修改Pods.xcodeproj文件,在 Manifest.lock成功写入预编译沙箱,通过hook run_plugins_post_install_hooks函数,在预编译沙箱中,使用 xcodebuild命令,编译每一个需要更新的pod_target,并将编译好的framework放至GeneratedFrameworks目录下。


回到主工程执行pod install


截屏2021-11-28 下午8.22.44.png


编译完成后,就回到了主工程里面的 Pod install流程中。对:resolve_dependencies方法进行Method Swizzling,对需要更改的pod_target进行修改。通过修改内存中的Pod::Specification对象的vendored_frameworkssource_filesresource_bundlesresources属性,来引用已经编译好的framework


工作流总结


通过对每一个阶段的了解,我们了解了作者的思路是这样的:
1,先将源码和Pods.project安装到预编译沙箱中 。
2,借助于Pods.project工程,使用xcodebuild编译需要预编译的scheme
3,巧妙的利用Method Swizzling,在分析依赖阶段,修改Pod::Specification对象,完成二进制的引用等工作。


现有问题


1,:binary => true 无效


ruby 2.6.8p205 (2021-07-07 revision 67951) [universal.x86_64-darwin21]版本中,使用:binary => true无效,无法编译为framework。在D ebug模式下生效,在发布后就失效了。


def set_prebuild_for_pod(pod_name, should_prebuild)
Pod::UI.puts("pod_name: #{pod_name} prebuild:#{should_prebuild}")
if should_prebuild == true
@prebuild_framework_pod_names ||= []
@prebuild_framework_pod_names.push pod_name
else
@should_not_prebuild_framework_pod_names ||= []
@should_not_prebuild_framework_pod_names.push pod_name
end
end


ruby 2.6.8中,release模式下,执行了两次,参数还不一致,导致 @prebuild_framework_pod_names@should_not_prebuild_framework_pod_names相等,最终需要预编译的数组为[]


pod_name: SnapKit, should_prebuild:true
pod_name: SnapKit, should_prebuild:


2,pod update没有及时更新最新的framework


frameworkA依赖frameworkB,在 Podfile中,只引入了 frameworkA


target xxx do 
pod "frameworkA", :binary => true
end


frameworkB有新版本时,没有更新最新的frameworkB,对于我们自己的组件,我们期望有新版本发布时,能及时更新。


解决办法:
在检测更新的方法中,读取最新的Manifest.lock文件,读取已编译的frameworkplist文件,比较两个版本号是否一致,不一致则重新编译。


cocoapods-binary-bel


为了能充分利用该插件,我根据实际产生的问题,对源码进行了一些修改,增加了 plist版本校验。使用方式和cocoapods-binary一致。
源码github链接cocoapods-binary-bel


安装


sudo gem install cocoapods-binary-bel


流程图


cocoapods-binary的基础上增加了版本校验工程,总的流程图如下所示:


cocoapods-binary-bel.png


一键转源码


1,:binary => false,指定某一个framework为源码


2, 新增 --hsource选项,输入 pod install --hsource,可以将所有的framework转为源码。


去除了依赖分析


在实际运用的工程中,如果 A 依赖 BC, B又依赖D,如果 A需要预编译,那么 BCD都需要重新编译,实际上此时BCD已经有了已经编译好的版本,无需重新编译。在Podfile指明 BCD即可。


pod "A"
pod "B"
pod "C"
pod "D"


增加静态库的resource处理


在 cocoapods中,如果使用 resource_bundle 处理资源文件,会生成一个相对应的target来处理资源文件,如果是动态库,会在动态库Target,添加资源工程依赖,使用 xcodebuild命令制作二进制会将bundle文件编译至xxx.framework目录下,使用 resources同样也会将资源文件编译至xxx.framework目录下。


对于静态库而言,如果是使用resource_bundle,也同样生成一个会生成一个相对应的target来处理资源文件,但对资源文件的拷贝,是由主工程做的,无需静态库工程处理, 如果使用 resources,则需要将资源文件从源码中,拷贝到 xxx.framework下,在主工程编译时,由主工程处理即可。


作者:Bel李玉
链接:https://juejin.cn/post/7035628418972516360

收起阅读 »

iOS 简单封装一个新用户功能模块引导工具类小玩儿意

iOS
废话开篇:新手引导功能就是简单的告诉用户某一模块下能够进行什么样的操作,起到指引用户的作用,那么就简单的实现一下这样的功能模块。一、实现效果展示可以从效果图中看到,对新用户的必要模块都会进行简单的功能解释。二、调用代码添加待引导功能视图到管理类管理类进行展示三...
继续阅读 »

废话开篇:新手引导功能就是简单的告诉用户某一模块下能够进行什么样的操作,起到指引用户的作用,那么就简单的实现一下这样的功能模块。

一、实现效果展示

屏幕录制2021-12-08 上午11.04.57.gif

可以从效果图中看到,对新用户的必要模块都会进行简单的功能解释。

二、调用代码

添加待引导功能视图到管理类

image.png

管理类进行展示

image.png

三、工具类解析

image.png

1、KDSGuideMannager 类

(1)统一管理全局下需要进行 “引导” 的功能区域(UIView)的保存。

(2)控制引导界面的显示与消失。

(3)控制下一个功能区域(UIView)圈定及描述展示。

2、KDSGuideView 类

(1)整体的蒙板视图层。

(2)对当前所选引导功能区域(UIView)进行镂空标注

(3)调整气泡(KDSGuideBubbleView)位置。

3、KDSGuideBubbleView 类

(1)气泡标注视图。

4、KDSGuideModel 类

(1)保存功能区域(UIView)视图及功能描述文字

四、实现代码

1、KDSGuideMannager 类

KDSGuideMannager.h

image.png

KDSGuideMannager.m

image.png

image.png

image.png

2、KDSGuideView 类

KDSGuideView.h

image.png

KDSGuideView.m

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

3、KDSGuideBubbleView 类

KDSGuideBubbleView.h

image.png

KDSGuideBubbleView.m

image.png

image.png

4、KDSGuideModel 类

KDSGuideModel.h

image.png

KDSGuideModel.m

image.png

五、其他效果展示

屏幕录制2021-12-08 下午1.54.31.gif

个人总结,代码拙劣,大神勿笑。

收起阅读 »

SDWebImage从小白到大师蜕变

iOS
简介SDWebImage提供的简洁的获取远程URL图片的API;平时开发中使用最多场景就是列表中的cell中要显示远程图片的需求,在具体的实现中要避免加载图片造成的界面卡顿,列表卡顿等现象的出现;所以需要编码实现如下功能:使用占位图片显示UI界面,异步线程加载...
继续阅读 »

简介

SDWebImage提供的简洁的获取远程URL图片的API;平时开发中使用最多场景就是列表中的cell中要显示远程图片的需求,在具体的实现中要避免加载图片造成的界面卡顿,列表卡顿等现象的出现;所以需要编码实现如下功能:

  • 使用占位图片显示UI界面,异步线程加载图片成功后刷新控件
  • 缓存机制,下载过的图片做内存缓存和磁盘缓存
  • app内存吃紧的状态下移除缓存的内容

SDWebImage的框架结构

SDWebImage的框架结构

SDWebImage的图片下载分类,只要一行代码就可以实现图片异步下载和缓存功能。

功能简介

  1. 一个添加了web图片加载和缓存管理的UIImageView分类
  2. 一个异步图片下载器
  3. 一个异步的内存加磁盘综合存储图片并且自动处理过期图片
  4. 支持动态gif图
    • 4.0 之前的动图效果并不是太好
    • 4.0 以后基于 FLAnimatedImage加载动图
  5. 支持webP格式的图片
  6. 后台图片解压处理
  7. 确保同样的图片url不会下载多次
  8. 确保伪造的图片url不会重复尝试下载
  9. 确保主线程不会阻塞

实现原理

  1. 架构图(UML 类图)

架构图(UML 类图)

  1. 流程图(方法调用顺序图)

1559217862563-364c0d60-3f2a-4db9-b5c5-e81f01cd125e.png

目录结构

  • Downloader
    ○ SDWebImageDownloader
    ○ SDWebImageDownloaderOperation

  • Cache
    ○ SDImageCache

  • Utils
    ○ SDWebImageManager
    ○ SDWebImageDecoder
    ○ SDWebImagePrefetcher

  • Categories
    ○ UIView+WebCacheOperation
    ○ UIImageView+WebCache
    ○ UIImageView+HighlightedWebCache
    ○ UIButton+WebCache
    ○ MKAnnotationView+WebCache
    ○ NSData+ImageContentType
    ○ UIImage+GIF
    ○ UIImage+MultiFormat
    ○ UIImage+WebP

  • Other
    ○ SDWebImageOperation(协议)
    ○ SDWebImageCompat(宏定义、常量、通用函数)

相关类名与功能描述

SDWebImageDownloader:是专门用来下载图片和优化图片加载的,跟缓存没有关系

SDWebImageDownloaderOperation:继承于 NSOperation,用来处理下载任务的

SDImageCache:用来处理内存缓存和磁盘缓存(可选)的,其中磁盘缓存是异步进行的,因此不会阻塞主线程

SDWebImageManager:作为 UIImageView+WebCache 背后的默默付出者,主要功能是将图片下载(SDWebImageDownloader)和图片缓存(SDImageCache)两个独立的功能组合起来

SDWebImageDecoder:图片解码器,用于图片下载完成后进行解码

SDWebImagePrefetcher:预下载图片,方便后续使用,图片下载的优先级低,其内部由

SDWebImageManager :来处理图片下载和缓存

UIView+WebCacheOperation:用来记录图片加载的 operation,方便需要时取消和移除图片加载的 operation

UIImageView+WebCache:集成 SDWebImageManager 的图片下载和缓存功能到 UIImageView 的方法中,方便调用方的简单使用

UIImageView+HighlightedWebCache:跟 UIImageView+WebCache 类似,也是包装了 SDWebImageManager,只不过是用于加载 highlighted 状态的图片

UIButton+WebCache:跟 UIImageView+WebCache 类似,集成 SDWebImageManager 的图片下载和缓存功能到 UIButton 的方法中,方便调用方的简单使用

MKAnnotationView+WebCache:跟 UIImageView+WebCache 类似

NSData+ImageContentType:用于获取图片数据的格式(JPEG、PNG等)

UIImage+GIF:用于加载 GIF 动图

UIImage+MultiFormat:根据不同格式的二进制数据转成 UIImage 对象

UIImage+WebP用于解码并加载 WebP 图片

工作流程

工作流程

  • 入口 setImageWithURL:placeholderImage:options: 会先把 placeholderImage 显示,然后 SDWebImageManager 根据 URL 开始处理图片。

  • 进入 SDWebImageManager-downloadWithURL:delegate:options:userInfo:交给 SDImageCache 从缓存查找图片是否已经下载 queryDiskCacheForKey:delegate:userInfo:。

  • 先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo: 到 SDWebImageManager。

  • SDWebImageManagerDelegate 回调 webImageManager:didFinishWithImage: 到 UIImageView+WebCache 等前端展示图片。

  • 如果内存缓存中没有,生成 NSInvocationOperation 添加到队列开始从硬盘查找图片是否已经缓存。

  • 根据 URLKey 在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。

  • 如果从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小,会先清空内存缓存)。SDImageCacheDelegate 回调 imageCache:didFindImage:forKey:userInfo:进而回调展示图片。

  • 如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片,回调 imageCache:didNotFindImageForKey:userInfo:。

  • 共享或重新生成一个下载器 SDWebImageDownloader 开始下载图片。

  • 图片下载由 NSURLConnection(3.8.0之后使用了NSURLSession),实现相关 delegate 来判断图片下载中、下载完成和下载失败。

  • connection:didReceiveData: 中利用 ImageIO 做了按图片下载进度加载效果。connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder 做图片解码处理。

  • 图片解码处理在一个 NSOperationQueue 完成,不会拖慢主线程 UI。如果有需要对下载的图片进行二次处理,最好也在这里完成,效率会好很多。

  • 在主线程 notifyDelegateOnMainThreadWithInfo: 宣告解码完成,imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader。

  • imageDownloader:didFinishWithImage: 回调给 SDWebImageManager 告知图片下载完成。

  • 通知所有的 downloadDelegates 下载完成,回调给需要的地方展示图片。

  • 将图片保存到 SDImageCache 中,内存缓存和硬盘缓存同时保存。写文件到硬盘也在以单独 NSInvocationOperation 完成,避免拖慢主线程。

  • SDImageCache 在初始化的时候会注册一些消息通知,在内存警告或退到后台的时候清理内存图片缓存,应用结束的时候清理过期图片。

  • SDWebImagePrefetcher 可以预先下载图片,方便后续使用。

常见面试题

  1. 图片文件缓存的时间有多长:1周

_maxCacheAge = kDefaultCacheMaxCacheAge

  1. SDWebImage 的内存缓存是用什么实现的?

NSCache

  1. SDWebImage 的最大并发数是多少?

maxConcurrentDownloads = 6

  • 是程序固定死了,可以通过属性进行调整!
  1. SDWebImage 支持动图吗?GIF
1. #import <ImageIO/ImageIO.h>
2. [UIImage animatedImageWithImages:images duration:duration];
复制代码
  1. SDWebImage是如何区分不同格式的图像的

    • 根据图像数据第一个字节来判断的!
    • PNG:压缩比没有JPG高,但是无损压缩,解压缩性能高,苹果推荐的图像格式!
    • JPG:压缩比最高的一种图片格式,有损压缩!最多使用的场景,照相机!解压缩的性能不好!
    • GIF:序列桢动图,特点:只支持256种颜色!最流行的时候在1998~1999,有专利的!

6.SDWebImage 缓存图片的名称是怎么确定的!

  • md5

  • 如果单纯使用 文件名保存,重名的几率很高!

  • 使用 MD5 的散列函数!对完整的 URL 进行 md5,结果是一个 32 个字符长度的字符串!

  1. SDWebImage 的内存警告是如何处理的!
    • 利用通知中心观察
    • - UIApplicationDidReceiveMemoryWarningNotification 接收到内存警告的通知
    • 执行 clearMemory 方法,清理内存缓存!
    • - UIApplicationWillTerminateNotification 接收到应用程序将要终止通知
    • 执行 cleanDisk 方法,清理磁盘缓存!
    • - UIApplicationDidEnterBackgroundNotification 接收到应用程序进入后台通知
    • 执行 backgroundCleanDisk 方法,后台清理磁盘!
    • 通过以上通知监听,能够保证缓存文件的大小始终在控制范围之内!
    • clearDisk 清空磁盘缓存,将所有缓存目录中的文件,全部删除!

实际工作,将缓存目录直接删除,再次创建一个同名空目录!

青山不改,绿水长流,后会有期,感谢每一位佳人的支持!

收起阅读 »

闲鱼正在悄悄放弃 Flutter 吗?

iOS
闲鱼技术阿里巴巴集团采访嘉宾 | 于佳(宗心)编辑 | Tina闲鱼在 2017 年引入 Flutter,当时的 Flutter 还远未成熟,行业内也没有把 Flutter 放入已有工程体系进行开发的先例。之后这支不到 15 人的闲鱼团队从工程架构、混合栈调用...
继续阅读 »

闲鱼技术lv-4阿里巴巴集团

采访嘉宾 | 于佳(宗心)

编辑 | Tina

闲鱼在 2017 年引入 Flutter,当时的 Flutter 还远未成熟,行业内也没有把 Flutter 放入已有工程体系进行开发的先例。

之后这支不到 15 人的闲鱼团队从工程架构、混合栈调用、打包构建、协同模式上都做了一些创新,保证了 Flutter 能融入到闲鱼已有的客户端工程体系内。在 2017 年到 2019 年期间,闲鱼也不断的修正 Bug 提高 Flutter 的稳定性并同步给 Google,并在实践中沉淀出一套自己的混合技术方案,开源了 Flutter Boost 引擎。

2019 年,闲鱼开始大规模落地,推进 Flutter 在闲鱼的应用。2020 年,闲鱼线上的主链路几乎已经完全拥抱 Flutter。这两年,Flutter 也逐渐在其他企业里落地,但同时也不断有质疑的声音发出。甚至有传言表示“闲鱼的新业务已经放弃 Flutter”、“相信闲鱼遇到了很大的难题”......

那么,作为 Flutter 先驱和探路者,闲鱼在过去几年的摸索过程中是否有走弯路?闲鱼现在到底面临着什么样的挑战?是否会放弃 Flutter?新业务选择了什么技术?对应的技术选型原则是什么?针对这些疑问,闲鱼技术团队客户端负责人于佳(宗心)逐一给了我们解答。

国内第一个引进 Flutter 的团队

InfoQ:闲鱼当时引进 Flutter 时主要是为了解决什么问题?

于佳(宗心):闲鱼在 17 年调研的时候,客户端团队只有不到 15 人,而闲鱼的业务场景可以称得上是一个 “小淘宝”,相对比较复杂。这种场景下我们首先需要解决的是多端人力共享的问题。多端人力带来的好处不只是可以一人开发双端,也代表着更好的研发资源调配灵活性(这意味着团队的 iOS:Android 的比例不再需要 1:1,而市面上 Android 的工程师基数远大于 iOS)。

另外我们希望这个技术是贴合移动端研发技术栈的,而非前端技术栈,本身对于 RN 和 Weex 来说,工具链和研发习惯还是有比较大的差异的。最后我们希望这个技术的体验可以做到接近原生,AOT 下的 Flutter 基本满足我们当时的要求,在实际测试过程中,同样未深度优化的详情页面,Flutter 在低端机的表现比 Native 更好。因此当时基于这三个条件选择了 Flutter。

2018 年的尝试投入过程中,整个基建和探索带来了一定的成本。2019 年,团队开始正式大量使用 Flutter 进行研发,目前整个团队 70% 的 commit 来自 Dart,可以说基本完成了我们当初的期望。在实际的研发过程中,基本可以完成一个需求一个客户端投入的目标。

InfoQ:很多人质疑 Dart 语言,认为这个语言独特小众,还存在比如说多层嵌套的问题,您们怎么看待新语言的应用?

于佳(宗心):语言是我们选择技术方案的其中一个因素,但是相对比较弱的因素。

我们会从几个角度去看:

  • 语言的背景,从我们的角度来看 Dart 是大厂研发的,也有比较久的历史。

  • 语言的学习成本,从语法糖和学习曲线上来看,Dart 成本都比较低,首先 Android 同学的上手率很快。另外熟悉 swift 的 iOS 同学,上手也很快。现代语言的特性有很多是相通的。这部分是它的优势。

  • 语言带来的其他优势,如编译产物支持 AOT 和 JIT,比较灵活。AOT 有明显的性能优势。

  • 语言的未来的趋势。Dart 在 2020 年第四季度 Github Pull Request 的排名已经到了全网第 13 位,超过了 Kotlin(15 位),Swift(16 位),Objective-C(18 位)。作为移动技术领域的新语言成长性还是非常不错的。

对于像多层嵌套的问题,可以通过进一步抽象一些控件类或方法解决,并不是特别大的问题。

InfoQ:闲鱼引入 Flutter 之后做了哪些关键创新?在使用 Flutter 上有哪些收益?

于佳(宗心):闲鱼在这部分创新非常多,并在内部申请了非常多专利。

  • 我们的开源项目 Flutter Boost 彻底改变了 Flutter 官方的一些 RoadMap。目前 Add2ExistApp 是行业最主流的研发方式。混合开发一方面帮助了业务更平滑的迁移到了新的技术栈,另一方面可以更好的利用已有的 Native 能力,大幅减少了重复开发的工作。

  • 针对音视频的外接纹理方案,也是目前行业大厂常见的解决方案,在外接纹理方案下,Native 和 Flutter 侧的缓存管理得到了统一,在性能上也有一定的提升。

  • Flutter APM,基于 Flutter 技术栈的性能稳定性数据采集和加工方案,目前在集团内部也是跟多个 BU 一起共建,为大的 AliFlutter 组织提供服务。

  • Flutter 相关的动态模版方案,Flutter DX,兼容集团的已有的 Native 模版,保证了业务的平滑迁移,并为 Flutter 提供了部分业务动态性。

  • 其他还有很多,包括内部的高性能长列表容器 PowerScrollView,动画框架 Fish-Lottie,游戏引擎 Candy,我们现在还有一些新的方向在沉淀,在基于 Flutter 的研发流程和研发工具上也有投入,未来大家如果感兴趣可以去 InfoQ 组织的行业大会与我们交流。

闲鱼有想过放弃 Flutter 吗?

InfoQ:最近一两年,您们在 Flutter 开发上,遇到的最大挑战是什么?跟最初使用 Flutter 时的挑战一样吗?

于佳(宗心):早先几年闲鱼作为整个行业的先驱,主要的挑战是整个技术生态太差,都需要自己做。另外就是前期引擎的稳定性有比较大的问题。

最近几年随着整个技术的深度使用,以及闲鱼这两年业务快速发展背后,越来越多的体验问题被大家提及,因此我们从去年开始进行了整个产品的大改版,同时客户端的目标就是全面优化,打造更好的用户端产品体验。

因此在生态逐渐完善后,我们的挑战是,怎么通过 Flutter 来实现更加精细化的用户体验。去年,这部分确实花了我们比较多的精力。基于这个命题,我们在内存和卡顿上内部也开发了较多的基于 Flutter 的检测工具,在内存优化和卡顿优化上也有一些比较具体的方法,但不得不说,所有的细节优化都是比较耗人力的,不管是 Native 还是 Flutter 都要投入相当的精力,所以我们目前也面向全行业进行客户端的招聘,希望有志在 Flutter 领域进行探索的同学联系我。

InfoQ:在混合研发体系下,闲鱼还进行了引擎定制,那么官方提供的方案主要问题是什么?对于一般小企业来说,混合开发复杂度会不会太高?

于佳(宗心):闲鱼在前期有不少修改引擎的动作,我针对当时有一些 自己的反思,一方面是确实因为 Flutter 不太完善,另一方面在 18 年左右,我们自己引擎的理解也不够深刻,很多时候可以通过更上层的方案解决,这也间接导致了我们的很多引擎定制修改难以合入主干。

所以这部分我想说的是,目前官方的方案可以解决 90% 的问题,如果一定要说定制,目前在性能侧还是有一些问题的。比如闲鱼目前首页还是 native 没有使用 Flutter,就是因为替换以后启动加载体验不佳,另外在长列表侧大家一直诟病的卡顿问题,我们有尝试通过上层框架解决了一部分,接下来可能还需要底层引擎帮忙优化。另外一些包括双端字体不一致的问题,还有输入框体验不一致的问题,都需要官方进行长期的优化。

目前我们主要还是希望跟随主干分支,尽量不修改 Flutter 的代码,闲鱼团队会储备一些引擎侧的专家,同时也会依靠集团 AliFlutter 的生态做事情。在整个 AliFlutter 的组织里不同的 BU 擅长的也不同,如 UC 同学更擅长引擎定制,闲鱼团队有大量的上层应用框架,淘宝团队提供基于构建相关的基础设施。这样在大型公司中通过内部开源社区的方式就可以解决大部分的问题,放心开发了。

对于中小企业来说,要明确下大家面临的场景,如果前期快速迭代跑起来,对细节问题可以有一部分妥协,选择 Flutter 是一个比较明确的路径。今天大家所处的环境比闲鱼当年所处的环境要完善的多。推荐使用 Flutter Boost 进行混合开发,在部分场景下遇到问题无法快速响应时,也可以通过混合工程使用 native 进行兜底。复杂度方面,单纯引入混合栈能力,整体复杂度一般。

InfoQ:有传言,闲鱼有新业务没采用 Flutter,这给很多人造成了闲鱼放弃 Flutter 的观念,那么您们在新业务的技术选型上,考虑了哪些因素?

于佳(宗心):作为技术决策者,是应该避免自己被某一个技术绑架而在落地过程中产生谬误的。Flutter 和其他技术一样,最终是为了帮助团队实现业务价值,同时它也只是移动端的一种技术,捧杀和谩骂都是不合适的。这也是我特别不想在公众面前回应这个事情的原因,因为 技术本身要看适用场景。

从目前闲鱼的人员规模和业务规模来看。对于架构设计,我的理念是尽量追求一致性和架构的简洁。

整个客户端组织未来从语言的方向来看是 Dart First,尽量减少双端的研发投入。而对其他容器的选择,主要以 H5 为主,在未来的路径上尽量减少其他容器的接入,让前端开发也回归到标准路线来。

这里有两个好处:

  1. 组织成本最低,组织成本包括了同学们的学习成本、协同成本等等,多技术栈和容器多会带来额外的成本,这是我不愿意看到的。

  2. 架构的一致性对研发效能和质量都有帮助。举个例子,随着业务复杂性加大,多容器带来的内存飙升和包大小的问题是非常致命的,而且几乎是无解的,这就需要架构师作出决策,干掉什么留下什么。回到研发效能上,配套的工具,流程一定是围绕一类容器和语言来扩展的,如果方案特别多,每个方向都需要做额外的配套设施,成本收益很低,研发的幸福感也很低。

从这个设计的角度出发,我们会有几个明确的选择

  • 在默认场景下使用 Flutter 作为首选的方案;

  • 在投放活动、前台导购、非常不确定的新业务、以及管理后台等使用 H5 作为首选实现方案;

  • 在极少场景下,比如已有完整的 SDK 附带 UI 的支持如直播,以及未来中台的拍摄功能 SDK 也是自带 UI 的部分,如要切换,Native 成本最低,选择 Native。另外目前 Flutter 在首页加载还有一定的性能问题,因此还在使用 Native。从长远发展来看,未来到一定程度可能随改版直接改为 Flutter。

关于未来发展

InfoQ:使用 Flutter 多年后,现在回过头去看,您认为哪些公司哪些场景适合 Flutter?

于佳(宗心):目前看起来有几个典型场景比较适合:

  • 中台战略下的小前台产品,从大公司的组织里看阿里、头条、美团都有相对完善的 Flutter 组织或内部技术社区可以提供一些基础服务,保证了基于 Flutter 基础设施在前期投入过程中的成本均摊,在未来落地过程中,业务团队可以更加专注于业务研发,而更少的担心过程中填坑的成本。

  • 中小型企业的初创 App,在人力成本资源都不够的情况下,希望先跑通流程上线验证的团队,可以尝试使用 Flutter 进行研发,在我自己实际的面试和行业交流过程中,这一类情况也比较典型。这种方式可以避免前期成本过度投入,在人员调配上也更加灵活。

  • 另外这个观点还没有验证,但是逻辑上应该可行。未来面向企业内部流程工具,政府部门的部分工具属性较强的 App,可以尝试使用 Flutter。因为目前我了解的情况来看,在企业这边的应用来看,整体 ToB(美团商家端)和 ToD(比如饿了么骑手端)的场景的 App 特别多。横向比较来看,场景比较类似,也就是说更多中长尾应用有可能是 Flutter 技术的主要场景。

InfoQ:您认为未来 Flutter 急需改善的地方是什么?

于佳(宗心):从 Flutter 2.0 发布后我跟一些一线开发者交流的感受来看,Flutter 还是需要推进跨端性能和细节体验的优化。去年一年在大的战略方向上(跨终端),Flutter 做的不错,在 PC 和 Web 侧都有建树,跟车企以及操作系统厂商合作都有一定进展。但回归到产品体验和开发者体验上,还有不少路要走,很多时候对于一个严苛的业务方来说,小到字体和控件的体验都会成为最后不选择这门技术的原因。这部分希望整个开源社区在新的一年能有一些进步。我们 AliFlutter 组织内部,以 UC 内核团队为首的同学们,在这方面就有非常多的沉淀以及 PR,在内部引擎制定上有很多体验的提升。未来在 AliFlutter 组织内,我们也会除了完善整个公司的基建外,进一步关注细节体验,沉淀一些最佳实践给到其他的开发同学。大家会在2个月内看到我们最新出版的书籍,欢迎交流。

InfoQ:Flutter2.0 来了,那么 Flutter 会成为主流选择吗?

于佳(宗心):可以讲一下我对 Flutter 未来的判断。一方面在未来操作系统有可能走向分裂,多终端的场景下,Flutter 会有比较不错的发展,跨平台本身的对企业来说在成本侧是有很大的诉求的,尤其是互联网公司。但是从历史的经验来看,Flutter 只是渲染引擎,即使今天的游戏开发,在游戏引擎和配套工具完善的情况下,有部分的功能模块(比如社区 / 直播的功能)依然还是混合的框架,所以混合开发最后一定是一直存在的。能不能成为未来整个移动研发的主流这件事情上看,我无法给出答案,但可以肯定的是,在生态更加完善后,会在一定的历史阶段成为客户端研发的另一种常见的技术选择。

嘉宾介绍:

于佳,花名 宗心,闲鱼技术团队客户端负责人。2012 年应届毕业加入阿里巴巴,经历集团无线化转型的重要时期,参与过集团多款重量级 App 以及移动中间件的设计与开发,多年客户端老兵。2014 年参与了手机淘宝的 iOS 客户端的架构升级,该架构首次完成了对百人团队并行开发的支持,同年主导了手机天猫客户端基础架构以及交易链路向手淘架构的归一,为手机淘宝作为未来集团无线中台奠定了坚实的基础。2015 年加入闲鱼客户端团队负责端架构和团队建设,工作期间完成了基于 Flutter 混合架构的闲鱼客户端的整体架构设计,在工程体系上完善了针对 Flutter 的持续集成以及高可用体系的支撑,同时推进了闲鱼主链路业务的 Flutter 化。未来将持续关注终端技术的演变及发展趋势。

收起阅读 »

如何用 GPU硬件层加速优化Android系统的游戏流畅度

作为一款VR实时操作游戏App,我们需要根据重力感应系统,实时监控手机的角度,并渲染出相应位置的VR图像,因此在不同 Android 设备之间,由于使用的芯片组和不同架构的GPU,游戏性能会因此受到影响。举例来说:游戏在 Galaxy S20+ 上可能以 6...
继续阅读 »

作为一款VR实时操作游戏App,我们需要根据重力感应系统,实时监控手机的角度,并渲染出相应位置的VR图像,因此在不同 Android 设备之间,由于使用的芯片组和不同架构的GPU,游戏性能会因此受到影响。举例来说:游戏在 Galaxy S20+ 上可能以 60fps 的速度渲染,但它在HUAWEI P50 Pro上的表现可能与前者大相径庭。 由于新版本的手机具有良好的配置,而游戏需要考虑基于底层硬件的运行情况。

如果玩家遇到帧速率下降或加载时间变慢,他们很快就会对游戏失去兴趣。
如果游戏耗尽电池电量或设备过热,我们也会流失处于长途旅行中的游戏玩家。
如果提前预渲染不必要的游戏素材,会大大增加游戏的启动时间,导致玩家失去耐心。
如果帧率和手机不能适配,在运行时会由于手机自我保护机制造成闪退,带来极差的游戏体验。

基于此,我们需要对代码进行优化以适配市场上不同手机的不同帧率运行。

所遇到的挑战

首先我们使用Streamline获取在 Android 设备上运行的游戏的配置文件,在运行测试场景时将 CPU 和 GPU性能计数器活动可视化,以准确了解设备处理 CPU 和 GPU 工作负载,从而去定位帧速率下降的主要问题。

以下的帧率分析图表显示了应用程序如何随时间运行。

在下面的图中,我们可以看到执行引擎周期与 FPS 下降之间的相关性。显然GPU 正忙于算术运算,并且着色器可能过于复杂。

为了测试在不同设备中的帧率情况,使用友盟+U-APM测试不同机型上的卡顿状况,发现在onSurfaceCreated函数中进行渲染时出现卡顿, 应证了前文的分析,可以确定GPU是在算数运算过程中发生了卡顿:

因为不同设备有不同的性能预期,所以需要为每个设备设置自己的性能预算。例如,已知设备中 GPU 的最高频率,并且提供目标帧速率,则可以计算每帧 GPU 成本的绝对限制。

数学公式: $ 每帧 GPU 成本 = GPU 最高频率 / 目标帧率 $

CPU 到 GPU 的调度存在一定的约束,由于调度上存在限制所以我们无法达到目标帧率。

另外,由于 CPU-GPU 接口上的工作负载序列化,渲染过程是异步进行的。
CPU 将新的渲染工作放入队列,稍后由 GPU 处理。

数据资源问题

CPU 控制渲染过程并且实时提供最新的数据,例如每一帧的变换和灯光位置。然而,GPU 处理是异步的。这意味着数据资源会被排队的命令引用,并在命令流中停留一段时间。而程序中的OpenGL ES 需要渲染以反映进行绘制调用时资源的状态,因此在引用它们的 GPU 工作负载完成之前无法修改资源。

调试过程

我们曾做出尝试,对引用资源进行代码上的编辑优化,然而当我们尝试修改这部分内容时,会触发该部分的新副本的创建。这将能够一定程度上实现我们的目标,但是会产生大量的 CPU 开销。

于是我们使用Streamline查明高 CPU 负载的实例。在图形驱动程序内部libGLES_Mali.so路径函数, 视图中看到极高的占用时间。

由于我们希望在不同手机上适配不同帧率运行,所以需要查明libGLES_Mali.so是否在不同机型的设备上都产生了极高的占用时间,此处采用了友盟+U-APM来检测用户在不同机型上的函数占用比例。

友盟+ U-APM自定义异常测试,下列机型会产生高libGLES_Mali.so占用的问题,因此我们需要基于底层硬件的运行情况来解决流畅性问题,同时由于存在问题的机型不止一种,我们需要从内存层面着手,考虑如何调用较少的内存缓存区并及时释放内存。

解决方案及优化

基于前文的分析,我们首先尝试从缓冲区入手进行优化。

单缓冲区方案
• 使用glMapBufferRange和GL_MAP_UNSYNCHRONIZED.然后使用单个缓冲区内的子区域构建旋转。这避免了对多个缓冲区的需求,但是这一方案仍然存在一些问题,我们仍需要处理管理子区域依赖项,这一部分的代码给我们带来了额外的工作量。

多缓冲区方案
• 我们尝试在系统中创建多个缓冲区,并以循环方式使用缓冲区。通过计算我们得到了适合的缓冲区的数目,在之后的帧中,代码可以去重新使用这些循环缓冲区。由于我们使用了大量的循环缓冲区,那么大量的日志记录和数据库写入是非常有必要的。但是有几个因素会导致此处的性能不佳:

1. 产生了额外的内存使用和GC压力
2. Android 操作系统实际上是将日志消息写入日志而并非文件,这需要额外的时间。
3. 如果只有一次调用,那么这里的性能消耗微乎其微。但是由于使用了循环缓冲区,所以这里需要用到多次调用。
我们会在基于c#中的 Mono 分析器中启用内存分配跟踪函数用于定位问题:

$ adb shell setprop debug.mono.profile log:calls,alloc

我们可以看到该方法在每次调用时都花费时间:

Method call summary Total(ms) Self(ms) Calls Method name 782 5 100 MyApp.MainActivity:Log (string,object[]) 775 3 100 Android.Util.Log:Debug (string,string,object[]) 634 10 100 Android.Util.Log:Debug (string,string)

在这里定位到我们的日志记录花费了大量时间,我们的下一步方向可能需要改进单个调用,或者寻求全新的解决方案。

log:alloc还让我们看到内存分配;日志调用直接导致了大量的不合理内存分配:

Allocation summary Bytes Count Average Type name 41784 839 49 System.String 4280 144 29 System.Object[]

硬件加速

最后尝试引入硬件加速,获得了一个新的绘图模型来将应用程序渲染到屏幕上。它引入了DisplayList 结构并且记录视图的绘图命令以加快渲染速度。

同时,可以将 View渲染到屏幕外缓冲区并随心所欲地修改它而不用担心被引用的问题。此功能主要适用于动画,非常适合解决我们的帧率问题,可以更快地为复杂的视图设置动画。

如果没有图层,在更改动画属性后,动画视图将使其无效。对于复杂的视图,这种失效会传播到所有的子视图,它们反过来会重绘自己。

在使用由硬件支持的视图层后,GPU 会为视图创建纹理。因此我们可以在我们的屏幕上为复杂的视图设置动画,并且使动画更加流畅。

代码示例:

// Using the Object animator view.setLayerType(View.LAYER_TYPE_HARDWARE, null); ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 20f); objectAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { view.setLayerType(View.LAYER_TYPE_NONE, null); } }); objectAnimator.start(); // Using the Property animator view.animate().translationX(20f).withLayer().start();

另外还有几点在使用硬件层中仍需注意:

(1)在使用之后进行清理:

硬件层会占用GPU上的空间。在上面的 ObjectAnimator代码中,侦听器会在动画结束时移除图层。在 Property animator 示例中,withLayers()方法会在开始时自动创建图层并在动画结束时将其删除。

(2)需要将硬件层更新可视化:

使用开发人员选项,可以启用“显示硬件层更新”。
如果在应用硬件层后更改视图,它将使硬件层无效并将视图重新渲染到该屏幕外缓冲区。

硬件加速优化

但是由此带来了一个问题是,在不需要快速渲染的界面,比如滚动栏, 硬件层也会更快地渲染它们。当将 ViewPager 滚动到两侧时,它的页面在整个滚动阶段会以绿色突出显示。

因此当我滚动ViewPager时,我使用DDMS运行 TraceView
,按名称对方法调用进行排序,搜索“android/view/View.setLayerType”,然后跟踪它的引用:

ViewPager#enableLayers(): private void enableLayers(boolean enable) { final int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { final int layerType = enable ? ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE; ViewCompat.setLayerType(getChildAt(i), layerType, null); } }

该方法负责为 ViewPager的孩子启用/禁用硬件层。它从 ViewPaper#setScrollState() 调用一次:

private void setScrollState(int newState) { if (mScrollState == newState) { return; } mScrollState = newState; if (mPageTransformer != null) { enableLayers(newState != SCROLL_STATE_IDLE); } if (mOnPageChangeListener != null) { mOnPageChangeListener.onPageScrollStateChanged(newState); } }

正如代码中所示,当滚动状态为IDLE时硬件被禁用,否则在DRAGGINGSETTLING时启用。PageTransformer 旨在“使用动画属性将自定义转换应用于页面视图”(Source)。

基于我们的需求,只在渲染动画的时候启用硬件层,所以我想覆盖ViewPager 方法,但由于它们是私有的,我们无法修改这个方法。

所以我采取了另外的解决方案:在 ViewPage#setScrollState() 上,在调用 enableLayers()之后,我们还会调用

OnPageChangeListener#onPageScrollStateChanged()

。所以我设置了一个监听器,当 ViewPager的滚动状态不同于 IDLE时,它将所有ViewPager的孩子的图层类型重置为 NONE

@Override public void onPageScrollStateChanged(int scrollState) { // A small hack to remove the HW layer that the viewpager add to each page when scrolling. if (scrollState != ViewPager.SCROLL_STATE_IDLE) { final int childCount = <your_viewpager>.getChildCount(); for (int i = 0; i < childCount; i++) <your_viewpager>.getChildAt(i).setLayerType(View.LAYER_TYPE_NONE, null); } }

这样,在ViewPager#setScrollState()为页面设置了一个硬件层之后——我将它们重新设置为NONE,这将禁用硬件层,因此而导致的帧率区别主要显示在Nexus上。

作者:六一
来源:https://segmentfault.com/a/1190000040864118

收起阅读 »

黄仁勋要造“第二颗地球”?对“元宇宙”意味着什么

随着元宇宙概念的爆火,英伟达正凭借其Omniverse平台及在AI、高性能计算等方面的建树,迅速成长为市值高达7700亿美元的AI顶级玩家。作为科技圈极具个性的大佬“黄教主”——NVIDIA创始人兼CEO黄仁勋,每年都会在行业科技展会上拿出一些有趣的东西。在刚...
继续阅读 »


随着元宇宙概念的爆火,英伟达正凭借其Omniverse平台及在AI、高性能计算等方面的建树,迅速成长为市值高达7700亿美元的AI顶级玩家。

作为科技圈极具个性的大佬“黄教主”——NVIDIA创始人兼CEO黄仁勋,每年都会在行业科技展会上拿出一些有趣的东西。

在刚刚结束的 NVIDIA GTC 2021 大会上,黄仁勋惊喜亮相,表达了对今年爆火的元宇宙的痴迷,更宣布要造“第二颗地球”!

一时间,引发“全网”热议。

“元宇宙”格局打开:Omniverse Avatar

此次大会上,英伟达推出了全球最小、功能强大、能效最高的新一代AI超级计算机NVIDIA Jetson AGX Orin、NVIDIA Triton推理服务器及NVIDIA A2 Tensor Core GPU加速器、NeMo Megatron、NVIDIA Modulus及新自动驾驶平台DRIVE Hyperion 8 GA等一系列重磅新技术。

同时,英伟达还推出了3个新加速库:一、针对运筹优化问题的加速求解器——NVIDIA ReOpt,可实现实时路线规划优化。二、cuQuantum DGX设备,配备有针对量子计算工作流的加速库,可用态矢量和张量网络的方法来加速量子电路模拟。三、在PyData和NumPy生态系统的大规模加速计算cuNumeric,属于NVIDIA RAPIDS开源Python数据科学套件。

除此之外,会上黄仁勋还带来了承载着其“元宇宙”愿景的全新虚拟化身平台——Omniverse Avatar,彻底将元宇宙格局打开。

作为NVIDIA一系列先进AI技术的集大成者,Omniverse Avatar可以将Metropolis的感知能力、Riva的语音识别能力、Merlin的推荐能力、Omniverse的动画渲染能力等交汇于一体。

Omniverse Avatar能帮助开发者能构建出一个完全交互式的虚拟化身,它足够生动,能对语音和面部提示做出反应,能理解多种语言,能给出智能的建议。

通过Omniverse Avatar平台,用户可以为视频会议和协作平台、客户支持平台、内容创建、应用收益和数字孪生、机器人应用等等构建定制的AI助理。

英伟达的“元宇宙”探索之路

对于英伟达来说,早在2020年10月份,该公司就10月推出了面向企业的实时仿真和协作平台Omniverse的测试版,当时就吸引了包括宝马、爱立信、沃尔沃、Adobe、Epic Games在内的众多公司与之合作。

而今年4月份, Omniverse 的正式版一经推出后,便被称为“工程师的元宇宙”的虚拟工作平台。

黄仁勋评价称,“Omniverse可以让个人模拟制造出遵从物理规律的共享3D虚拟世界。”

自此,英伟达彻底掀起了国内“元宇宙”的浪潮。

而据英伟达相关负责人透露,英伟达已经为此花费了数年时间和数亿美元。

“人类需要‘在为时已晚之前采取行动缓解和适应’”。本次GTC大会上,黄仁勋谈到了许多关于未来的畅想:英伟达未来的规划,将主要着力于生物合成、气候预测等与人类未来息息相关的方面。

”使用物理原理以及源自原理型模型和观测结果的数据 Physics-ML 模型,经过优化在多 GPU 和多节点上进行训练,以超实时方式预测气候变化、创造地球的数字孪生模型或者将其数据传输到元宇宙中。”

最后,黄仁勋正式宣布了“Earth-two第二颗地球”即将到来。

据悉,英伟达上一台超级计算机名为 Cambridge-1,即 C-1,接下来英伟达将着手研发新的超级计算机“E-2”,即“Earth-two”第二颗地球。

黄仁勋强调:“我们目前所发明的所有技术均是实现元宇宙所必不可少的,我想象不出比这更宏伟更重要的用途。”

元宇宙火爆背后也需要警醒

其实除了英伟达,对于元宇宙的突然爆火,今年以来已经有不少业内不少科技公司都纷纷跟进布局,乘此风口发力突围。

比如Facebook(现更名为Meta)早在2019年就同意收购了专门从事云游戏开发的PlayGiga;今年早些时候,微软(Microsoft Corp.)以75亿美元收购电子游戏公司ZeniMax Media Inc.等等,再比如从过去到现在一直在对元宇宙进行探索的英伟达,这些都让VR/AR及相关衍生产业热度大涨。

当然,也并不是所有科技公司都认同元宇宙这个概念。也有内人士评论称,“目前构成“元宇宙”概念的技术力量薄弱,商品化程度低,处于发展的初级阶段”。

Facebook (现更名为Meta)的老对手Snap Inc.,就对此有不同看法,尽管该公司目前也在投资增强现实技术。

据《华尔街日报》报道,此前Snap Inc.公司首席执行官埃文·斯皮格尔(Evan Spiegel)在Tech Live大会上曾表示,“我们之所以对增强现实如此感兴趣,因为它的立足点是我们共享的这个世界。”

所以,看到这里,我们也会思考,如果能够让这个世界变的更好,那么这些技术这个发展趋势某些方面会值得人们追捧。但同时,我们也要时刻保持警醒,避免被“元宇宙”概念中所描绘的巨无霸虚拟世界所“迷惑而上瘾”。

作者:MissD
来源:https://segmentfault.com/a/1190000040937338


收起阅读 »

关于组件文档从编写到生成的那些事

前言说到前端领域的组件,Vue 技术体系下有 Element UI,React 技术体系下有 Ant Design,这些都是当前的前端攻城狮们都免不了要实际使用到的基础组件库。而在实际工作中,我们也总免不了要根据自己的工作内容,整理一些适合自己业务风格的一套组...
继续阅读 »



前言

说到前端领域的组件,Vue 技术体系下有 Element UI,React 技术体系下有 Ant Design,这些都是当前的前端攻城狮们都免不了要实际使用到的基础组件库。而在实际工作中,我们也总免不了要根据自己的工作内容,整理一些适合自己业务风格的一套组件库,基础组件部分可以基于上面开源的组件库以及 less 框架等多主题样式方案做自己的定制,但更多的是一些基于这些基础组件整理出适合自己业务产品的一套业务组件库。

而说到开发组件库,我们或选择 Monorepo 单仓库多包的形式(参考网文 https://segmentfault.com/a/11... 等)或其他 Git 多仓库单包的形式来维护组件代码,最终都免不了要将组件真正落到一个文档中,提供给其他同事去参考使用。

本篇文章就产出组件文档这件事,聊聊我在产出文档过程中的一系列思考过程,解决组件开发这「最后一公里」中的体验问题。

组件文档的编写

规范与搭建的调研

组件文档是要有一定的规范的,规范是任何软件工程阶段的第一步。对于组件来说,文档透出的内容都应包含哪些内容,决定着组件开发者和使用者双方的所有的体验。确定这些规范并不难,参考开源的组件库的文档,我们会有一些初步的印象。

因为我们团队使用的是 React 技术栈,这里我们参考 Ant Design 组件库。

比如这个最基本的 Button 组件,官方文档从上至下的内容结构是这样:

  1. 显示组件标题,下跟组件的简单描述。

  2. 列出组件的使用场景。

  3. 不同使用场景下的效果演示、代码案例。可外跳 CodeSandbox、CodePen 等在线源码编辑器网站编辑实时查看效果。

  4. 列出组件可配置的属性、接口方法列表。列表中包含属性/方法名、字段类型、默认值、使用描述等。

  5. 常见问题的 FAQ。

  6. 面向设计师的一些 Case 说明链接。

这些文档内容很丰富,作为一个开放的组件库,几乎考虑到的从设计到开发视角的方方面面,使用体验特别好。而在好奇心驱使下,我去查看了官网源码方库,比如 Button 组件:https://github.com/ant-design...。在源码库下,放置了和组件入口文件同名的分别以 .zh-CN.md.en-US.md 后缀命名的 Markdown 文件,而在这些 Markdown 文件中,便是我们看到的官网文档内容...咦?不对,好像缺少了什么,案例演示和示例代码呢?

难道 AntD 官网文档是另外自己手动开发维护的?这么大的一个项目肯定不至于,根据其官网类似 docs/react/introduce-cn 这种访问路径在源码库中有对应的 Markdown 文件来看,官网的文档肯定是官方仓库根据一种规范来生成的。那么是怎么生成的呢?第一次做组件文档规范的我被挑起了兴趣。

而作为一个前端工程老手,我很熟练地打开了其 package.json 文件,通过查看其中的 scripts 命令,轻易便发现了其下的 site 命令(源码仓库 package.json):

npm run site:theme && cross-env NODE_ICU_DATA=node_modules/full-icu ESBUILD=1 bisheng build --ssr -c ./site/bisheng.config.js

原来如此,网站的构建使用了 bisheng。通过查阅了解 bisheng 这个工具库,发现它确实是一个文档系统的自动生成工具,其下有一个插件 bisheng-plugin-react,可以将 Markdown 文档中的 JSX 源码块转换成可以运行演示的示例。而每个组件自身的示例代码文档,则在每个组件路径下的
demo 目录下维护。

Emmm,bisheng 确实是很好很强大,还能支持多语言,结合一定的文档规范约束下,能够快速搭建一个文档的主站。但在深入了解 bisheng 的过程中,发现其文档相对来说比较缺乏,包装的东西又那么多,使用过程中黑盒感严重,而我们团队的组件库其实要求很简单,一是能做到方便流通,而是只在内部流通使用,不会开源。那么,有没有更简单的搭建文档的方式呢?

更多的文档工具库的调研

在谷歌搜索中敲入如 React Components Documentation 等关键字,我们很快便能搜索出很多与 React 组件文档相关的工具库,这里我看到了如下这些:DoczStoryBookReact Styleguidist 、UMI 构建体系下的 dumi 等等。

这些工具库都支持解析 Markdown 文档,其中 DoczStoryBook 还支持使用 mdx 格式(Markdown 和 JSX 的混合写法),且在文档内容格式都能支持到组件属性列表、示例代码演示等功能。

接下来,我们分别简单看下这些工具库对于组件文档的支持情况。

Docz

在了解过程中,发现 Docz 其实是一个比较老牌的文档系统搭建工具了。它本身便主推 MDX 格式的文档,基本不需要什么配置便能跑起来。支持本地调试和构建生成可发布产物,支持多包仓库、TypeScript 环境、CSS 处理器、插件机制等,完全满足功能需要。

只是 Docz 貌似只支持 React 组件(当然对于我们来说够用),且看其 NPM 包最近更新已经是两年之前。另外 MDX 格式的文档虽然理解成本很少但对于使用不多的同事来说还是有一定的接受和熟练上手的成本。暂时备选。

StoryBook

在初次了解到 StoryBook 时便被其 66.7K 的 Star 量惊到了(Docz 是 22K),相对 Docz 来说,StoryBook 相关的社区内容非常丰富,它不依赖组件的技术栈体系,现在已经支持 React、Vue、Angular、Web Components 等数十个技术栈。

StoryBook 搭建文档系统的方式不是去自动解析 Markdown 文件,而是暴露一系列搭建文档的接口,让开发者自己为组件手动编写一个个的 stories 文件,StoryBook 会自动解析这些 stories 文件来生成文档内容。这种方式会带来一定的学习和理解接口的成本,但同时也基于这种方式实现了支持跨组件技术栈的效果,并让社区显得更为丰富。

官方示例:https://github.com/storybookj...

StoryBook 的强大毋庸置疑,但对于我们团队的情况来说还是有些杀鸡用牛刀了。另外,其需要额外理解接口功能并编写组件的 stories 文件在团队内很难推动起来:大家都很忙,组件开发分布在团队几十号人,情况比较复杂,将文档整理约束到一个人身上又不现实。继续调研。

React Styleguidist

React Styleguidist 的 Star 量没有 StoryBook 那么耀眼(10K+),但包体的下载量也比较大,且近期的提交也是相当活跃。由名字可知,它支持的是 React 组件的环境。它是通过自动解析 Mardown 文件的形式来生成文档的,实现方式是自动解析文档中 JSX 声明代码块,按照名称一一对应的规则查找到组件源码,然后将声明的代码块通过 Webpack 打包产生出对应的演示示例。

而在继续试用了 React Styleguidist 的一些基础案例后,它的一个功能让我眼前一亮:它会自动解析组件的属性,并解析出其类型、默认值、注释描述等内容,然后将解析到的内容自动生成属性表格放置在演示示例的上方。这就有点 JSDOC 的意思了,对于一个组件开发者来说,TA 确实需要关心组件属性的透出、注释以及文档案例的编写,但编写完也就够了,不用去考虑怎么适应搭建出一个文档系统。

另外, React Styleguidist 解析组件属性是基于解析 AST 以及配合工具 react-docgen 来实现的,并且还支持配合 react-docgen-typescript 来实现解析 TypeScript 环境下的组件,另外还能很多配置项支持更改文档站点相关的各个部分的展示样式、内容格式等,配置自定义支持相当灵活。

当然,它也有一些缺点,比如内嵌 Webpack,对于已经将编译组件库的构建工具换为 Rollup.js 的情况是一个额外的配置负担。

总的来说,React Styleguidist 在我看来是一个小而美的工具库,很适合我们团队协作参与人多、且大都日常开发工作繁重的情况。暂时备选。

dumi

了解到 dumi 是因为我们团队内已经有部分组件文档站点是基于它来搭建的了。dumi 一样是通过自动解析 Markdown 文档的方式来实现搭建文档系统,同样基本零配置,也有很多灵活的配置支持更改文档站点一些部分的显示内容、(主题)样式等,整体秉承了 UMI 体系的风格:开箱即用,封装极好。它能单独使用,也能结合 UMI 框架一起配置使用。

只是相比于上面已经了解到的 React Styleguidist 来说,并未看到有其他明显的优势,且貌似没有看到有自动解析组件属性部分的功能,对于我来说没有 React Styleguidist 下得一些亮点。可以参考,不再考虑。

组件文档的生成

在多方对比了多个文档搭建的工具库后,我最终还是选用了 React Styleguidist。在我看来,自然是其基于 react-docgen 来实现解析组件属性、类型、注释描述等的功能吸引到了我,这个功能一方面能在较少的额外时间付出下规范团队同事开发组件过程中一系列规范,另一方面其 API 接口的接入形式能够通过统一构建配置而统一产出文档内容格式和样式,方便各业务接入使用。

决定了技术方案后,便是如何具体实现基于其封装一个工具,便于各业务仓库接入了。

我们团队有自己统一的 CLI 构建工具,再多一个 React Styleguidist 的 CLI 配置会在理解上有一定的熟悉成本,但我可以基于 React Styleguidist 的 Node API 接入形式,将 React Styleguidist 的功能分别融入我们自身 CLI 的 devbuild 命令。

首先,基于 React Styleguidist API 的形式,统一一套配置,将生成 React Styleguidist 示例的代码抽象出来:

// 定义一套统一的配置,生成 react-styleguidist 实例
import styleguidist from 'react-styleguidist/lib/scripts/index.esm';
import * as docgen from 'react-docgen';
import * as docgenTS from 'react-docgen-typescript';

import type * as RDocgen from 'react-docgen';

export type DocStyleguideOptions = {
 cwd?: string;
 rootDir: string;
 workDir: string;
 customConfig?: object;
};

const DOC_STYLEGUIDE_DEFAULTS = {
 cwd: process.cwd(),
 rootDir: process.cwd(),
 workDir: process.cwd(),
 customConfig: {},
};

export const createDocStyleguide = (
 env: 'development' | 'production',
 options: DocStyleguideOptions = DOC_STYLEGUIDE_DEFAULTS,
) => {
 // 0. 处理配置项
 const opts = { ...DOC_STYLEGUIDE_DEFAULTS, ...options };
 const {
   cwd: cwdPath = DOC_STYLEGUIDE_DEFAULTS.cwd,
   rootDir,
   workDir,
   customConfig,
} = opts;

 // 标记:是否正在调试所有包
 let isDevAllPackages = true;

 // 解析工程根目录包信息
 const pkgRootJson = Utils.parsePackageSync(rootDir);

 // 1. 解析指定要调试的包下的组件
 let componentsPattern: (() => string[]) | string | string[] = [];
 if (path.relative(rootDir, workDir).length <= 0) {
   // 选择调试所有包时,则读取根路径下 packages 字段定义的所有包下的组件
   const { packages = [] } = pkgRootJson;
   componentsPattern = packages.map(packagePattern => (
     path.relative(cwdPath, path.join(rootDir, packagePattern, 'src/**/[A-Z]*.{js,jsx,ts,tsx}'))
  ));
} else {
   // 选择调试某个包时,则定位至选择的具体包下的组件
   componentsPattern = path.join(workDir, 'src/**/[A-Z]*.{js,jsx,ts,tsx}');
   isDevAllPackages = false;
}

 // 2. 获取默认的 webpack 配置
 const webpackConfig = getWebpackConfig(env);

 // 3. 生成 styleguidist 配置实例
 const styleguide = styleguidist({
   title: `${pkgRootJson.name}`,
   // 要解析的所有组件
   components: componentsPattern,
   // 属性解析设置
   propsParser: (filePath, code, resolver, handlers) => {
     if (/\.tsx?/.test(filePath)) {
       // ts 文件,使用 typescript docgen 解析器
       const pkgRootDir = findPackageRootDir(path.dirname(filePath));
       const tsConfigParser = docgenTS.withCustomConfig(
         path.resolve(pkgRootDir, 'tsconfig.json'),
        {},
      );
       const parseResults = tsConfigParser.parse(filePath);
       const parseResult = parseResults[0];
       return (parseResult as any) as RDocgen.DocumentationObject;
    }
     // 其他使用默认的 react-docgen 解析器
     const parseResults = docgen.parse(code, resolver, handlers);
     if (Array.isArray(parseResults)) {
       return parseResults[0];
    }
     return parseResults;
  },
   // webpack 配置
   webpackConfig: { ...webpackConfig },
   // 初始是否展开代码样例
   // expand: 展开 | collapse: 折叠 | hide: 不显示;
   exampleMode: 'expand',
   // 组件 path 展示内容
   getComponentPathLine: (componentPath) => {
     const pkgRootDir = findPackageRootDir(path.dirname(componentPath));
     try {
       const pkgJson = Utils.parsePackageSync(pkgRootDir);
       const name = path.basename(componentPath, path.extname(componentPath));
       return `import ${name} from '${pkgJson.name}';`;
    } catch (error) {
       return componentPath;
    }
  },
   // 非调试所有包时,不显示 sidebar
   showSidebar: isDevAllPackages,
   // 日志配置
   logger: {
     // One of: info, debug, warn
     info: message => Utils.log('info', message),
     warn: message => Utils.log('warning', message),
     debug: message => console.debug(message),
  },
   // 覆盖自定义配置
   ...customConfig,
});

 return styleguide;
};

这样,在 devbuild 命令下可以分别调用实例的 server 接口方法和 build 接口方法来实现调试和构建产出文档静态资源。

// dev 命令下启动调试
// 0. 初始化配置
const HOST = process.env.HOST || customConfig.serverHost || '0.0.0.0';
const PORT = process.env.PORT || customConfig.serverPort || '6060';

// 1. 生成 styleguide 实例
const styleguide = createDocStyleguide(
 'development',
{
   cwd: cwdPath,
   rootDir: pkgRootPath,
   workDir: workPath,
   customConfig: {
     ...customConfig,
     // dev server host
     serverHost: HOST,
     // dev server port
     serverPort: PORT,
  },
},
);

// 2. 调用 server 接口方法启动调试
const { compiler } = styleguide.server((err, config) => {
 if (err) {
   console.error(err);
} else {
   const url = `http://${config.serverHost}:${config.serverPort}`;
   Utils.log('info', `Listening at ${url}`);
}
});
compiler.hooks.done.tap('done', (stats: any) => {
 const timeStr = stats.toString({
   all: false,
   timings: true,
});

 const statStr = stats.toString({
   all: false,
   warnings: true,
   errors: true,
});

 console.log(timeStr);

 if (stats.hasErrors()) {
   console.log(statStr);
   return;
}
});
// build 命令下执行构建

// 生成 styleguide 实例
const styleguide = MonorepoDev.createDocStyleguide('production', {
 cwd,
 rootDir,
 workDir,
 customConfig: {
   styleguideDir: path.join(pkgDocsDir, 'dist'),
},
});
// 构建文档内容
await new Promise<void>((resolve, reject) => {
 styleguide.build(
  (err, config, stats) => {
     if (err) {
       reject(err);
    } else {
       if (stats != null) {
         const statStr = stats.toString({
           all: false,
           warnings: true,
           errors: true,
        });
         console.log(statStr);
         if (stats.hasErrors()) {
           reject(new Error('Docs build failed!'));
           return;
        }
         console.log('\n');
         Utils.log('success', `Docs published to ${path.relative(workDir, config.styleguideDir)}`);
      }
       resolve();
    }
  },
);

最后,在组件多包仓库的每个包下的 package.json 中,分别配置 devbuild 命令即可。实现了支持无感启动调试和构建产出文档资源。

小结

本文主要介绍了我在调研实现组件文档规范和搭建过程中的一个思考过程,诚如文中介绍其他文档系统搭建工具时所说,有很多优秀的开源工具能够支持实现我们想要的效果,这是前端攻城狮们的幸运,也是不幸:我们可以站在前人的肩膀上,但要在这么多优秀库中选择一个适合自己的,更需要多做一些了解和收益点的权衡。一句老话经久不衰:适合自己的才是最好的。

希望这篇文章对看到这里的你能有所帮助。

作者:ES2049 / 靳志凯
链接:https://segmentfault.com/a/1190000041097170

收起阅读 »

驳“低代码开发取代程序员”论 为什么专业开发者也需要低代码?

低代码又火了。近几年,腾讯、阿里、百度等互联网大厂纷纷入局,国内外低代码平台融资动辄数千万甚至数亿,以及伴随着热度而来的巨大争议……无不说明“低代码”的火爆。事实上,低代码并非新概念,它可以追溯到上世纪80年代的“第四代编程语言”。2014年,Forreste...
继续阅读 »

低代码又火了。

近几年,腾讯、阿里、百度等互联网大厂纷纷入局,国内外低代码平台融资动辄数千万甚至数亿,以及伴随着热度而来的巨大争议……无不说明“低代码”的火爆。

事实上,低代码并非新概念,它可以追溯到上世纪80年代的“第四代编程语言”。2014年,Forrester正式提出低代码的概念。低代码是一种软件开发技术,衍生于软件开发的高级语言,让使用者通过可视化的方式,以更少的编码,更快速地构建和交付应用软件,全方位降低软件的开发成本。与传统软件开发方式相比,低代码开发平台整合了软件开发和部署所需的 IDE(集成开发环境)、服务器和数据库管理工具,覆盖软件开发的全生命周期,我们可以将其理解为 Visual Studio + IIS + SQL Management Studio(.NET 技 术)或 Eclipse + Tomcat + MySQL Workbench(Java 技术)的组合。

编码更少、交付更快、成本更低,还覆盖软件开发全生命周期,怎么看低代码都可以说是不错的软件开发工具。那么,它又为什么引发争议,甚至被其主要用户群体之一——程序员所诟病呢?“低代码开发会取代程序员” 这一观点大行其是,它说得对吗?

为什么低代码引起专业开发者的反感?

技术浪潮引发巨大变革,也带来了无数“取代论”,比如机器翻译是否取代人类翻译、机器人记者是否取代人类记者,以及低代码开发是否取代程序员。

低代码虽然火爆,但程序员对此抱有不同的心态:

  • 轻视:低代码技术的诸多优势只是炒作,该技术更适合初学者,解决不了复杂的技术问题;

  • 恐惧:担心被低代码取代;

  • 抵触:低代码开发平台能够覆盖所有需求吗;大量封装组件使得低代码开发平台更像一个黑盒子,可能导致难以debug、难以修改和迭代升级等技术问题;低代码开发平台配置有大量组件,简单的拖拉拽动作即可完成大量开发工作,程序员不再需要厉害的技术能力。

那么,上述理由真的站得住脚吗?我们一一来看。

低代码的门槛真的低吗?

低代码开发过程常被比作拼积木:像拼搭积木一样,以可视化的方式,通过拖拉拽组件快速开发出数据填报、流程审批等应用程序,满足企业里比较简单的办公需求。

但这并不意味着低代码开发平台只能做到这些。

Gartner在2020年9月发布的《企业级低代码开发平台的关键能力报告》(Critical Capabilities for Enterprise Low-Code Application Platforms)中,列举了低代码的11项关键能力。

图源:http://www.gartner.com/en/document…

这里我们着重来看其中三项关键能力。

  • 数据建模和管理:该指标就是通常所讲的 “模型驱动” 。相比于表单驱动,模型驱动能够提供满足数据库设计范式的数据模型设计和管理能力。开发的应用复杂度越高,系统集成的要求越高,这个能力就越关键。

  • 流程和业务逻辑:流程应用与业务逻辑开发能力和效率。这个能力有两层,第一层是指使用该低代码开发平台能否开发出复杂的工作流和业务处理逻辑;第二层是开发这些功能时的便利性和易用性程度有多高。

  • 接口和集成:编程接口与系统集成能力。为了避免“数据孤岛”现象,企业级应用通常需要与其他系统进行集成,协同增效。此时,内置的集成能力和编程接口就变得至关重要。除非确认可预期的未来中,项目不涉及系统集成和扩展开发,开发者都应该关注这个能力。

这些关键能力表明低代码平台在建模与逻辑方面具备较强的能力,而接口和集成能力可使专业开发人员完成低代码无法实现的部分,通过低代码与专业代码开发的协作实现复杂应用的开发。 在涉及高价值或复杂的核心业务时,专业开发人员需要理解业务需求,厘清业务逻辑。从这个层面上看,低代码开发的门槛并不低。事实也是如此:海比研究在《2021 年中国低代码/无代码市场研究报告》中提到,截至 2020 年底,技术人员在低代码使用者中的比例超 75%,占主体地位。

低代码什么都能做吗?

程序员的工作围绕开发需求展开。在选择开发工具时,程序员通常考虑的首要问题是:这款工具能否覆盖所有需求?如果需求增加或变更,该工具是否支持相关操作?这些问题同样适用于低代码平台的选型。

在实际项目交付过程中,如果我们仅可以满足99%的需求,另外1%的需求满足不了,那么真实用户大概率是不会买单的。因此,在评估低代码产品的时候,我们一定要保证该平台可以支撑所有系统模块类型的开发,同时也要具备足够的扩展性,确保使用纯代码开发出的模块能够与低代码模块进行无缝集成,而这离不开编程接口。

以国内主流低代码开发平台活字格为例。该平台提供开箱即用的开发组件,同时为系统的各个分层均提供编程扩展能力,以满足企业级应用开发对扩展性的高要求。借助分层编程接口,开发者可以用纯代码的方式实现新增功能,无需受限于低代码开发平台的版本和现有功能。


活字格的编程扩展能力

当然,就具体应用领域而言,低代码开发平台也有其擅长和不擅长的地方。目前,低代码开发更多地被应用于2B企业应用开发,而对于用户量特大的头部互联网应用、对算法和复杂数据结构要求较高的应用,低代码平台则不太适合。

低代码开发不可控?

“低代码开发平台是个黑盒子,内部出问题无法排查和解决。开发过程中发现有问题怎么办?迭代升级难以实现怎么办?”很多程序员会有这种疑惑。

但我们需要注意的是,低代码开发平台本质上仍是软件开发工具,用户模型与软件开发周期支持是其关键能力之一。也就是说,成熟的低代码开发平台具备软件开发全生命周期所需的各项功能,从而大大简化开发者的技术栈,进一步提高开发效率。

具体而言,在面对频繁的需求变更、棘手的问题排查时,低代码开发平台引入了版本管理机制,从而更高效地进行代码审查、版本管理与协调,以及软件的迭代升级。至于debug,日志分析无疑是个好办法。例如,活字格把执行过程及细节以日志方式输出,方便程序员高效debug。

对程序员而言,低代码平台是限制还是助力?

“低代码”意味着更少的代码。代码都不怎么写了,程序员又该怎么成长,怎么获得职业成就感呢?

其实不然。

首先,开发 ≠ 写代码。低代码平台可以减少大量重复工作,提升开发效率,把专业开发人员从简单、重复的开发需求中解放出来,把精力投入到更有价值的事情上,比如精进技术、理清业务逻辑。

其次,低代码平台的组件化和拖拽式配置降低了开发门槛,新手程序员能够借助此类平台快速入门,加速升级打怪;有经验的程序员也有机会参与更多项目,甚至带团队,积累更多经验值,实现快速成长。

宁波聚轩就是一个例子。这家公司自2009年起就专注于智能制造、工业4.0、系统方案集成等领域的探索研究。在接触了低代码之后,项目负责人发现开发效率得到极大提升,采用传统方式需要一个月开发量的项目,现在需要半个月甚至更短的时间就可以完成。此外,其实践经验表明,低代码开发的学习成本较低,毕业新生经过一周学习,两周就可做项目,一个月就能熟练开发。

该公司在2021企业级低代码应用大赛中获得了应用创新奖,获奖作品是一套轴承行业数字化智造系统。这套系统主要集成了ERP、MES、WMS和设备机联网系统,覆盖了销售、采购、仓库、计划、生产、财务等全流程功能,且已经在生产现场投入使用。在开发过程中,宁波聚轩的开发团队利用低代码平台成功解决了定制化要求高、多终端需求等难题,及时完成项目交付。

结语

当迷雾散尽,低代码开发平台重新露出高效率开发工具的本色时,你会选择它吗?


作者:SegmentFault思否
来源:https://juejin.cn/post/7023579572096466974

收起阅读 »

2021年爆火的“元宇宙”,是真风口还是资本骗局下的割韭菜?

Metaverse——元宇宙。一个出自1992年科幻小说「雪崩」(Snow Crash)的概念,在2021年突然火爆了起来。各路资本也纷纷下场,似乎这个「元宇宙」马上就要实现了一样。甚至有人称2021年是「元宇宙」元年。 就在今年,字节跳动、Facebook...
继续阅读 »

Metaverse——元宇宙。一个出自1992年科幻小说「雪崩」(Snow Crash)的概念,在2021年突然火爆了起来。各路资本也纷纷下场,似乎这个「元宇宙」马上就要实现了一样。甚至有人称2021年是「元宇宙」元年。


2021年爆火的“元宇宙”,是真风口还是资本骗局下的割韭菜?


就在今年,字节跳动、Facebook、腾讯、英伟达等从国外到国内的科技巨头纷纷宣布自己的元宇宙未来发展战略,把元宇宙视为公司未来存亡的关键。


Facebook创始人扎克伯格,更是雄心勃勃的表示,未来5年Facebook要变成「元宇宙公司」,可以说,在资本的涌入之下,元宇宙已经成为互联网公司的第一赛道。


一、什么是元宇宙?


各位有没有看过前两年大火的电影《头号玩家》,影片讲述2045年,由于现实生活无趣,无数年轻人迷失在一款超级火爆的游戏《绿洲》,当你穿上一身装备,站在一个可以自由奔跑的平台上时,一个崭新的世界便呈现了出来,这个新世界,就是元宇宙。


2021年爆火的“元宇宙”,是真风口还是资本骗局下的割韭菜?


在这个绿洲世界,你既可以升级打怪,也可以修仙摸鱼;既可以感受江湖恩怨、武侠情仇;又可以躲在悠远的溪谷,享受采菊东篱下的恬静。在这个世界,你可以为所欲为,体验你幻想中的一切。


在极具娱乐性的同时,这个世界并非完全脱离现实世界,例如游戏世界的金钱可以和现实世界互通共用,例如服务可以同向切换,例如你在虚拟世界也可以叫滴滴代驾,这个虚拟的世界与现实世界在很多方面重叠互通,你可以随时通过VR设备、电脑、手机登陆这个线上世界,又可以通过线上世界的行为影响到真实生活,在它脱胎于现实世界,又与现实世界平行。


这就是一个完美的元宇宙构想,是互联网高度发展后的一种延伸。


2021年爆火的“元宇宙”,是真风口还是资本骗局下的割韭菜?


很多朋友要问,我们已经有互联网了,为什么还要探索元宇宙呢?难道仅仅是为了一款游戏吗?


那肯定不是,元宇宙的思想,其实是为了提升世界的互联互通,在现实世界中,互联网技术和协议本身是开放的,但商业世界却大多是封闭的,《头号玩家》里人们在各个虚拟世界穿梭,但有着统一的账号身份、好友关系、实时联通的数据流动、资产流转。


这在今天的互联网世界里就是不存在的,比如,你在央行的征信状况和支付宝信用分就不是互通的,你的比特币和微信钱宝更不互通,人民币和美元汇率也不是实时对等,这种世界处处存在着摩擦,公司与公司之间,国际之间的信息都具有极高的壁垒。


元宇宙正是要打破这种壁垒,把所有公司和人都链接起来,构建起新的商业、金融、贸易、交流规则,实现更广阔互联网。


乍一听,这样的想法简直是天方夜谈,根本不可能实现,但是这几年区块链和VR技术的发展,让这种构想逐渐成为现实。


二、已经处于技术爆发的前夜


凌乐作为一个保守派的投资者,当看到这个概念的时候我也是一脸懵逼?这不就是一个VR游戏吗?有什么值得吹的?VR概念都炒作这么久了,无论是产品力还是实用程度,都离现实差了很远,凭什么现在来炒作?怎么看,都是一种虚假的概念。


但是仔细去研究之后,我发现,技术框架已经逐渐成型,这个飘渺的未来,或许正在实现。


区块链让框架规则出现了一定雏形:


在头号玩家中,最大的BOSS是游戏公司,也就是那个世界的神,可以制定规则,改变规则。但是在现实中,我们不需要这么一个神,这么庞大的工程,需要很多企业和用户共同搭建,需要让主流科技公司彼此合作,需要说服全球的监管机构,让他们相信互联网公司们打造的世界对社会是有利的,更需要让用户相信,这个世界没有潜规则,在部分规则方面绝对平等。


如果是在以前,根本不存在这种技术,但是区块链的产生,这种开始产生使用场景,NFT(Non-fungible Token)技术可以让区块链标识成为新的价值承载物,使虚拟物品资产化,从而实现数据内容的价值流转,规避黑箱操作,比特币的成功,又让世界意识到去中心化真的可以实现。


可以说,区块链技术,让这个绝对公平的商业规则出现了基本雏形。


VR设备快速升级,已处于爆发前夜:


2021年爆火的“元宇宙”,是真风口还是资本骗局下的割韭菜?


不管新世界的内容有多么丰富,都需要一道门来链接人和新世界,目前来看,VR设备就是开启这道新宇宙的门。


VR概念早在2016年就开始炒作了,不过因为技术水平低,实际体验感并不好,Facebook也是意识到VR设备的重要性,它们在 2016 年以 20 亿美元买下VR设备企业Oculus,并且每年在VR业务上的研发投入上百亿美元研发VR,在多家大厂入局的情况,现在VR设备技术突飞猛进,我专门去淘宝看了销量,Facebook下的这款VR眼镜居然都有四千多条评价,京东上面的销量更高。


扎克伯格认为,当产品销量突破1000万的时候,就标志着VR市场开始爆发,根据市场统计,Facebook旗下的VR设备Quest 2今年年底的销量就会突破1000万台,工信部预计2021年VR年复合增长率达91.2%,这个市场已经处于爆发前夜。


作者:IT技术管理的那些事儿
链接:https://juejin.cn/post/7008408876961759262
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

一篇完整的Swift属性参考,轻松让你提高一个档次!

iOS
属性 提供了更多关于声明和类型的信息。在 Swift 中有两种类型的属性,一种用于声明,一种用于类型。例如,required 属性-当用于类的指定或者便利初始化声明时-指明每个子类都必须实现它的初始化函数。noreturn 属性-当用于函数或者方法的类型时-指...
继续阅读 »

属性 提供了更多关于声明和类型的信息。在 Swift 中有两种类型的属性,一种用于声明,一种用于类型。例如,

required 属性-当用于类的指定或者便利初始化声明时-指明每个子类都必须实现它的初始化函数。noreturn 属性

-当用于函数或者方法的类型时-指明函数或者方法无需返回值。

咋们一起好好看,好好学

你可以用字符 @ 加上属性名和属性参数来指定一个属性:

@属性名
@属性名(属性参数)

含有参数的声明属性可以为属性指定更多的信息,可以用于特殊的声明。这些属性参数 被包含在圆括号里,参数的格式由属性决定。

声明属性

声明属性只能用于声明,当然,你也可以使用 noreturn 属性作为函数或者方法的类型。

assignment

此属性可用于修饰重载复合赋值运算符的函数。这个重载复合赋值运算符的函数必须用 inout 来标记初始输入参数。assignment属性示例参见复合赋值运算符

class_protocol

此属性可用于定义类类型协议。

如果你使用 objc 属性的协议, 那么这个协议就隐式含有 class_protocol 属性,你无需显式标记 class_protocol 属性。

exported

此属性可用于内部声明,可以将当前模块的内部模块、子模块或者声明暴露给外部其他模块。如果另一个模块引用了当前模块,那么这个模块就可以访问当前模块中暴露出来的部分。

final

此属性可用于修饰类或者类的属性、方法或成员下标运算符。用于一个类的时候,表明这个类是不能被继承的。用于类的属性、方法或成员下标运算符的时候,表明这个类的这些成员函数不能在任何子类中重写。

lazy

此属性可用于修饰类或者结构体中的存储变量属性,表明这个属性在第一次被访问时,其初始值最多只能被计算和存储一次。lazy 属性示例参见惰性存储属性

noreturn

此属性用于函数或者方法的声明,表明其函数或者方法相应的类型T是@noreturn T。当一个函数或者方法无需返回其调用者时,你可以用这个属性来修饰其类型。

你可以重写没有标示noreturn属性的函数或者方法。也就是说,你不能够重写有noreturn属性的函数或者方法。当你实现此类型的协议方法时,也有相似的规则。

NSCopying

此属性可用于修饰类中的存储变量属性。被修饰的这个属性的赋值函数是由这个属性值的拷贝组成-由copyWithZone方法返回-而不是这个属性本身的值。此属性类型必须符合NSCopying协议。
NSCopying属性类似于Objective-C中的copy属性。

NSManaged

用于修饰类中的存储变量属性,此类继承于NSManagedObject,表明这个属性的存储和实现是由Core Data基于相关的实体描述实时动态提供的。

objc

此属性可用于能用Objective-C表示的任何声明中-例如,非嵌套的类、协议、类和协议的属性和方法(包括取值函数和赋值函数)、初始化函数、析构函数以及下标运算符。objc属性告诉编译器此声明在Objective-C代码中可用。

如果你使用objc属性修饰类或者协议,它会显式的应用于这个类或者协议中的所有成员。当一个类继承于标注objc属性的另一类时,编译器会显式的为这个类添加objc属性。标注objc属性的协议不能够继承于不含有objc属性的协议。

objc属性可以接受由标识符组成的单个属性参数。当你希望暴露给Objective-C的部分是一个不同的名字时,你可以使用objc属性。你可以使用这个参数去命名类、协议、方法、取值函数、赋值函数以及初始化函数。下面的示例就是ExampleClass的enabled属性的取值函数,它暴露给Objective-C代码的是isEnabled,而不是这个属性的原名。

1.  @objc
2. class ExampleClass {
3. var enabled: Bool {
4. @objc(isEnabled) get {
5. // Return the appropriate value
6. }
7. }
8. }

optional

此属性可用于协议的属性、方法或者成员下标运算符,用来表明实现那些成员函数时,此类型的不是必需实现的。

optional属性只能用于标注objc属性的协议。因此,包含可选成员的协议只有类类型适用。更多的关于怎样使用optional属性,以及怎样访问可选协议成员的指导-例如,当你不确定它们是否实现了此类型时-参见可选协议需求

required

此属性用于类的指定或者便利初始化函数时,表明这个类的每个子类都必须实现这个初始化函数。

需求的指定初始化函数必须被显式的包含。当子类直接实现所有超类的指定初始化函数时(或者子类使用便利初始化函数重写了指定初始化函数时),需求的便利初始化函数必须被显式的包含或者继承。

使用Interface Builder声明属性

Interface Builder属性就是使用Interface Builder声明属性以与Xcode同步。Swift提供了如下几种Interface Builder属性:IBAction,IBdesignable,IBInspectable以及IBOutlet。这些属性理论上与Objective-C中相应的属性一样。

IBOutlet和IBInspectable属性可用于类的属性声明,IBAction属性可用于类的方法声明,IBDesignable属性可用于类的声明。

类型属性

类型属性可以只用于类型。当然noreturn属性也可以用于函数或者方法的声明。

auto_closure

此属性用于延迟表达式的赋值计算,将表达式自动封装成一个无参数的闭包。此属性还可作为函数或者方法的类型,此类型无参数并且其返回的是表达式类型。auto_closure属性示例参见函数类型

noreturn

此属性用于函数或者方法时表明此函数或者方法无返回值。你也可以用此属性标记函数或者方法的声明,以表明其函数或者方法相应的类型T是@noreturn T。

属性语法
attribute → @­attribute-name attribute-argument-clause opt
attribute-name → identifier
attribute-argument-clause → (balanced-tokens­ opt)
attributes → attribute­ attributes­ opt­
balanced-tokens → balanced-token ­balanced-tokens­ opt­
balanced-token → (­balanced-tokens­ opt­)­
balanced-token → [balanced-tokens­ opt­]­
balanced-token → {balanced-tokens­ opt­­}­
balanced-token → 任意标识符,关键字,常量,或运算符
balanced-token → 任意的标点符号 (­, )­, [­, ]­, {­, 或 }­

由于文章篇幅有限,只能点到即止地介绍当前一些工作成果和思考,各个 Swift 还有一些新的方向在探索,如果你对 iOS 底层原理、架构设计、构建系统、如何面试有兴趣了解,你也可以关注我及时获取最新资料以及面试相关资料。如果你有什么意见和建议欢迎给我留言!

写的不好的地方欢迎大家指出,希望大家多留言讨论,让我们共同进步!

喜欢iOS的小伙伴可以关注我,一起学习交流!!!

链接:juejin.cn/post/698169…


作者:在做开发的信哥
链接:https://juejin.cn/post/6988459235797368862

收起阅读 »

啥?iOS长列表还可以这么写

iOS
一般说,iOS界面的一些长列表,比如首页,活动页,长的会比较长,那么写起来总感觉没有那么优雅,那么如何才能做到优雅呢? 我在实践工作利用swift枚举的关联值和自定义组模型方法来实现了 下面是gif图效果 可以看到,有些组是杂乱无章的排列着,而且运营那边...
继续阅读 »

一般说,iOS界面的一些长列表,比如首页,活动页,长的会比较长,那么写起来总感觉没有那么优雅,那么如何才能做到优雅呢?
我在实践工作利用swift枚举的关联值和自定义组模型方法来实现了



  • 下面是gif图效果



可以看到,有些组是杂乱无章的排列着,而且运营那边要求,他们可以在后台自定义这些组的顺序
这可怎么办!🥺
下面看我的实现方式


定义一个组模型枚举



  • 包含可能的定义,每个枚举关联当前组需要显示的数据模型,有可能是一个对象数组,也有可能是一个对象


/// 新版首页组cell的类型
enum OriginGroupCellType {
case marquee(list: [MarqueeModel]) // 跑马灯
case beltAndRoad(list: [GlobalAdModel]) // 一带一路广告位
case shoppingCarnival(list: [GlobalAdModel]) // 购物狂欢节
case walletCard(smallWelfare: WelfareSmallResutlModel) // 钱包卡片
case wallet(list: [HomeNavigationModel]) // 钱包cell
case otc(list: [GlobalAdModel]) // OTC
case hxPrefecture(list: [GlobalAdModel]) // HX商品专区
case middleNav(list: [HomeNavigationModel]) // 中部导航
case bottomNav(list: [HomeNavigationModel]) // 底部导航
case broadcast(topSale: HomeNavigationModel, hot: OriginBroadcastModel, choiceness: OriginBroadcastModel) // 直播cell
case middleAd(list: [GlobalAdModel]) // 中间广告cell
case localService(list: [LocalServiceModel]) // 本地服务cell
case bottomFloat(headerList: [OriginBottomFloatHeaderModel]) // 底部悬停cell
}


  • 考虑到要下拉刷新等问题,可以这些枚举都得遵守Equatable协议


  extension OriginGroupCellType: Equatable {
public static func == (lhs: OriginGroupCellType, rhs: OriginGroupCellType) -> Bool {
switch (lhs, rhs) {
case (.marquee, .marquee): return true
case (.beltAndRoad, .beltAndRoad): return true
case (.shoppingCarnival, .shoppingCarnival): return true
case (.walletCard, .walletCard): return true
case (.wallet, .wallet): return true
case (.otc, .otc): return true
case (.hxPrefecture, .hxPrefecture): return true
case (.middleNav, .middleNav): return true
case (.bottomNav, .bottomNav): return true
case (.broadcast, .broadcast): return true
case (.middleAd, .middleAd): return true
case (.localService, .localService): return true
case (.bottomFloat, .bottomFloat): return true
default:
return false
}
}
}

接下来就是组模型的定义



  • 同时我抽取一个协议GroupProvider,方便复用


protocol GroupProvider {
/// 占位
associatedtype GroupModel where GroupModel: Equatable

/// 是否需要往组模型列表中添加当前组模型
func isNeedAppend(with current: GroupModel, listMs: [GroupModel]) -> Bool
/// 获取当前组模型在组模型列表的下标
func index(with current: GroupModel, listMs: [GroupModel]) -> Int
}

extension GroupProvider {
func isNeedAppend(with current: GroupModel, listMs: [GroupModel]) -> Bool {
return !listMs.contains(current)
}

func index(with current: GroupModel, listMs: [GroupModel]) -> Int {
return listMs.firstIndex(of: current) ?? 0
}
}




  • OriginGroupModel,同样也遵守Equatable协议,防止重复添加


func addTo(listMs: inout [OriginGroupModel]) 



  • 这个方法是方便于下拉刷新时,替换最新数据所用


public struct OriginGroupModel: GroupProvider {
typealias GroupModel = OriginGroupModel

/// 组模型的类型
var cellType: OriginGroupCellType
/// 排序
var sortIndex: Int

/// 把groupModel添加或替换到listMs中
func addTo(listMs: inout [OriginGroupModel]) {
if isNeedAppend(with: self, listMs: listMs) {
listMs.append(self)
} else {
let index = self.index(with: self, listMs: listMs)
listMs[index] = self
}
}
}

extension OriginGroupModel: Equatable {
public static func == (lhs: OriginGroupModel, rhs: OriginGroupModel) -> Bool {
return lhs.cellType == rhs.cellType
}
}


  • 考虑要自定义顺序,所以需要定义一个排序的实体


// MARK: - 新版首页组模型的排序规则模型
struct OriginGroupSortModel {
/// 搜索历史的排序
var marqueeIndex: Int
var beltAndRoadIndex: Int
var shoppingCarnivalIndex: Int
var walletCardIndex: Int
var walletIndex: Int
var otcIndex: Int
var hxPrefectureIndex: Int
var middleNavIndex: Int
var bottomNavIndex: Int
var broadcastIndex: Int
var middleAdIndex: Int
var localServiceIndex: Int
var bottomFloatIndex: Int

static var defaultSort: OriginGroupSortModel {
return OriginGroupSortModel(
marqueeIndex: 0,
beltAndRoadIndex: 1,
shoppingCarnivalIndex: 2,
walletCardIndex: 3,
walletIndex: 4,
otcIndex: 5,
hxPrefectureIndex: 6,
middleNavIndex: 7,
bottomNavIndex: 8,
broadcastIndex: 9,
middleAdIndex: 10,
localServiceIndex: 11,
bottomFloatIndex: 99)
}
}


控制器里定义一个 组模型数组



  • 这里有关键代码是


listMs.sort(by: { return $0.sortIndex < $1.sortIndex }) 



  • 所有的数据加载完毕后,会根据我们的自定义排序规则去排序


    /// 组模型数据
public var listMs: [OriginGroupModel] = [] {
didSet {
listMs.sort(by: {
return $0.sortIndex < $1.sortIndex
})
collectionView.reloadData()
}
}

/// 组模型排序规则(可以由后台配置返回,在这里我们先给一个默认值)
/// 需要做一个请求依赖,先请求排序接口,再请求各组的数据
public lazy var sortModel: OriginGroupSortModel = OriginGroupSortModel.defaultSort


网络请求代码


func loadData(_ update: Bool = false, _ isUHead: Bool = false) {
// 定义队列组
let queue = DispatchQueue.init(label: "getOriginData")
let group = DispatchGroup()

// MARK: - 文字跑马灯
group.enter()
queue.async(group: group, execute: {
HomeNetworkService.shared.getMarqueeList { [weak self] (state, message, data) in
guard let `self` = self else { return }
self.collectionView.uHead.endRefreshing()

defer { group.leave() }
let groupModel = OriginGroupModel(cellType: .marquee(list: data), sortIndex: self.sortModel.marqueeIndex)
guard !data.isEmpty else { return }

/// 把groupModel添加到listMs中
groupModel.addTo(listMs: &self.listMs)
}
})

/// .... 此处省略其它多个请求

group.notify(queue: queue) {
// 队列中线程全部结束,刷新UI
DispatchQueue.main.sync { [weak self] in
self?.collectionView.reloadData()
}
}
}


collectionView的代理方法处理


    func numberOfSections(in collectionView: UICollectionView) -> Int {
return listMs.count
}

func collectionView(_: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let groupModel = listMs[section]
switch groupModel.cellType {
case .marquee, .beltAndRoad, .walletCard, .wallet, .otc, .hxPrefecture, .shoppingCarnival, .middleAd:
return 1
case .middleNav(let list):
return list.count
case .bottomNav(let list):
return list.count
case .broadcast:
return 1
case .localService(let list):
return list.count
case .bottomFloat:
return 1
}
}



  • 同理,collectionView的代理方法中,都是先拿到 cellType 来判断,达到精准定位, 举个栗子


    /// Cell大小
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let groupModel = listMs[indexPath.section]
let width = screenWidth - 2 * margin
switch groupModel.cellType {
case .marquee:
return CGSize(width: screenWidth, height: 32)
case .beltAndRoad:
return CGSize(width: width, height: 46)
case .walletCard:
return CGSize(width: width, height: 85)
case .wallet:
return CGSize(width: width, height: OriginWalletCell.eachHeight * 2 + 10)
case .otc, .hxPrefecture:
return CGSize(width: width, height: 60)
case .middleNav:
let row: CGFloat = 5
let totalWidth: CGFloat = 13 * (row - 1) + 2 * margin
return CGSize(width: (screenWidth - totalWidth) / row, height: CGFloat(98.zh(80).vi(108)))
case .bottomNav:
let isFirstRow: Bool = indexPath.item < 2
let row: CGFloat = isFirstRow ? 2 : 3
let totalWidth: CGFloat = 4 * (row - 1) + 2 * margin
let width = (screenWidth - totalWidth) / row
return CGSize(width: floor(Double(width)), height: 70)
case .shoppingCarnival:
return CGSize(width: width, height: 150)
case .broadcast:
return CGSize(width: screenWidth - 20, height: 114)
case .middleAd:
return CGSize(width: width, height: 114)
case .localService:
let width = (82 * screenWidth) / 375
return CGSize(width: width, height: 110)
case .bottomFloat:
let h = bottomCellHeight > OriginBottomH ? bottomCellHeight : OriginBottomH
return CGSize(width: screenWidth, height: h)
}
}


总结一下这种写法的优势




  • 方便修改组和组之前的顺序问题,甚至可以由服务器下发顺序




  • 方便删减组,只要把数据的添加组注释掉




  • 用枚举的方式,定义每个组,更清晰,加上swift的关联值优势,可以不用在控制器里定义多个数组




  • 考虑到要下拉刷新,所以抽取了一个协议 GroupProvider,里面提供两个默认的实现方法



    • 方法一:获取当前cellType在listMs中的下标

    • 方法二:是否要添加到listMs中




  • 界面长什么样,全部由数据来驱动,这组没有数据,界面就对应的不显示(皮之不存,毛将焉附),有数据就按预先设计好的显示




源码地址(源码内容和gif图中有差异,但是思路是一致的)


github.com/XYXiaoYuan/…


作者:Bruceyuan
链接:https://juejin.cn/post/6939767696846225421

收起阅读 »

Awesome metaverse projects (元宇宙精选资源汇总)

Awesome Metaverse 关于 Metaverse 的精彩项目和信息资源列表。由于关于 Metaverse 是什么存在许多相互竞争的想法,请随时以拉取请求、问题和评论的形式留下反馈。WebXRWebXR Explainer - 什么是 WebXR,有...
继续阅读 »



Awesome Metaverse

关于 Metaverse 的精彩项目和信息资源列表。

由于关于 Metaverse 是什么存在许多相互竞争的想法,请随时以拉取请求、问题和评论的形式留下反馈。

WebXR

社交虚拟现实(Social VR)

开源

非免费

  • Cryptovoxels - 用户拥有的虚拟世界

  • Somnium Space - 基于区块链的持久虚拟世界

  • NeosVR - 旨在加速社交 VR 应用程序开发的引擎

  • VRChat

    - 最大的社交 VR 平台,拥有世界和头像的 UGC

    • Awesome VRChat - 有兴趣为 VRchat 开发内容的人的一站式商店

  • Roblox - 大型在线多人 UGC 游戏平台

  • Omniverse - 3D 生产流水线的实时模拟和协作平台

  • dot bigbang - 基于 Web 的多人 UGC 游戏平台,内置工具和 Typescript 脚本

  • Helios - 基于虚幻引擎的 UGC 世界、头像和游戏平台

  • Meta - The Metaverse 的 Meta(前 Facebook)公告视频

头像提供者

协议和标准

书籍

科幻

  • Neuromancer - (80s) 定义了赛博朋克流派和赛博空间一词

  • Snow Crash - (90 年代) 创造术语 Metaverse 作为互联网的继承者

  • Ready Player One - (2011) 后来成为斯皮尔伯格电影的热门书

  • Ready Player Two - (2020) 准备好的玩家一的续集

  • Rainbows End - (2006) 越来越多的数字/虚拟世界与无处不在的计算

  • Idoru - 虚拟名人和分散的虚拟世界,桥梁三部曲中的第 2 册

非小说

  • 空间网络 - Web 3.0 将如何连接人类、机器和人工智能以改变世界

电影

  • The Matrix 矩阵

  • The Thirteenth Floor 十三楼

  • Existenz 存在

  • Free Guy 自由人

  • Tron 创

  • Wreck it Ralph 2 破坏它拉尔夫 2

  • Ready Player One 准备好球员一

文章和博客

加密

白皮书

链接

翻译文章

元节入门

元节入门

后期 ROAD-MAP

  • 整理相关的资源到当前仓库。

  • 白皮书翻译

作者:houbb
来源:https://github.com/houbb/awesome-metaverse-zh

收起阅读 »

到2030年将存在的10个元宇宙工作

还记得2016年么? 宝可梦GO(Pokémon GO)席卷全球,当时许多人认为我们正站在增强现实技术(AR)革命的风口上。 显然, 这并没有实现。快进到今天,我们再次就Facebook/Meta 疯狂投资为创建一个元宇宙(Metaverse)——人人都生活在...
继续阅读 »



还记得2016年么? 宝可梦GO(Pokémon GO)席卷全球,当时许多人认为我们正站在增强现实技术(AR)革命的风口上。 显然, 这并没有实现。快进到今天,我们再次就Facebook/Meta 疯狂投资为创建一个元宇宙(Metaverse)——人人都生活在其中的完全沉浸式数字世界,展开了类似的谈论。

来自如此多参与者的这种投资往往会创造一个自我实现的预言:无论我们喜欢与否,我们都可能很快就拥有一个运转的元宇宙,只因科技霸主想这样做。

因为我的工作是规划(而非预测)未来的工作,所以我研究了这种可能性并自我发问:“元宇宙将创造什么类型的工作?” 以下是一些初步想法。

*快速定义

  • 虚拟现实 (VR):全人工环境;完全沉浸在虚拟环境中。

  • 增强现实 (AR):虚拟物体与真实世界环境重叠;数字物体增强了真实世界。

  • 混合现实 (MR):虚拟环境融合真实世界;现实世界和虚拟环境皆可交互。

扩展现实 (XR) / 元宇宙:上述一切的混合。


1. 元宇宙研究科学家

AR 和 VR 研究科学家已经是顶尖大学和大型科技公司的主要人才。但在元宇宙(或者您对“物质世界和数字世界的无缝交织”的任意称呼) 慢慢成为一个被广泛接受的观点同时,我们需要更多的智慧

开发一些真实世界的基本数字模型,让企业将能够在这些模型中吸引客户和合作伙伴,元宇宙研究科学家的工作可不仅仅是如此。这已经存在了。未来会有更大的变化。元宇宙研究科学家需要构建的是一种类似于万物理论的东西,其中整个世界都是可见的和可数字化操作。(想象一下没有乐趣的《头号玩家》)。该架构将是所有其他用例构建的基础;游戏、广告、工厂质量控制、互联健康、DeFi……等等。

这是一项极度复杂的任务,元宇宙研究科学家们需要能够使用计算机视觉算法融合的技术构建并缩放原型,用于3D计算摄影神经渲染场景重构计算成像视觉惯性里程计状态估计传感器融合,绘图与定位……这些原型将随着时间推移而变大

  • 如何成为元宇宙研究科学家:获得深度学习、计算机视觉、计算机图形学或计算成像的博士学位。你还需要了解c++,祝你好运。

2. 元宇宙规划师

一步实际行动比一打纲领更重要。一旦我们有了一个运行着的元宇宙,将所有功能规划和实施到一个完全虚拟的世界中的能力绝对是大多数公司的关键。在这个不断扩张的数字世界中选择正确的事情亦是如此。

这就是元宇宙规划师的用武之地。随着CEO为创造和增长其公司的元宇宙收入而制定愿景和战略时,规划师们需要推动从概念验证到试点再到部署的战略性机遇组合。这意指识别市场机会、建立商业案例、影响工程路线图、制定关键指标等……

你知道的…有趣的事物

这似乎并不吸引人,但您如何决定汽车公司是应该专注于创建虚拟驾驶测试,还是应实施一个数字孪生业务来预测故障?我不知道,但是规划师们肯定会给出答案。

  • 如何成为元宇宙规划师:拥有多年的管理经验,了解硬件/软件/SaaS/PaaS营销和商业模式,以及良好的创业心态。

3. 生态系统开发者

元宇宙不会靠扎克伯格的意志自行实现,需要围绕它建立一个健全的生态系统。传感器、CPU、GPU、KYC流程、数据湖(data-lakes)、绿色电能生产、边缘计算、法律、法规……世界是复杂的,因此进一步数字化(比现在更复杂)并非易事。我们可以将这一困难同目前汽车行业向电动汽车转型所面临的困难对比。电动汽车就在那里,但它们被采用的最大障碍是街道和道路上缺乏广泛分布的充电站,以及电池容量的不断变化。同样,我们可能拥有实现元宇宙的软件和硬件,但是仍然缺乏……其他一切

元宇宙生态系统开发者将负责协调合作伙伴和政府,以确保创建的各种功能能够大规模实现。他们将推动政府对基础建设的投资,并激发大型社区的活力。

元宇宙生态系统开发者们需要关注的一个关键问题是互操作性,以确保元宇宙客户能够在不同的体验中使用他们的虚拟道具。毕竟,如果你不能在商场里也穿着它,那么在迷你游戏中获得炫酷皮肤又有什么意义呢?其他游说努力将面向金融机构,它们需要支持分布式账本技术,以及在平台上交换商品和服务的智能合约。

  • 如何成为生态系统开发者:拥有多年的政务/游说经验以及对蓬勃发展的XR行业的深刻理解。

4. 元宇宙安全经理

您确信互联网对每个人来说都很安全吗? 是的,我也不。任何声称元宇宙会更安全的人都是在自欺欺人。当然,它有很多机会成为一个更安全、包容的地方,但是这不会自己发生。

隐私。虚拟身份认证。安全帽。足够的传感器……我们需要能在设计、验证和量产阶段提供指导和监督的人员,确保我们的数字世界是安全的,满足或超过适用的监管安全要求。显然,这一切都未牺牲尖端功能或设计,或削尖收入。这个人员即是元宇宙安全经理。

这也不是一个简单活。他们需要准确地预测元宇宙功能将如何被使用或者被滥用,并识别与这些预测相关的安全关键组件(safety-critical components)、系统和制造步骤。这十足的复杂性和移动部件的数量光是想想就足以让我头晕目眩。

  • 如何成为XR安全经理:拥有工程学位和消费电子产品/制造经验。

5. 元宇宙硬件制造商

元宇宙不会(仅仅)构建在代码之上,它(也)将构建在传感器、摄像头和耳机上。 这样的传感器会让你感受到被触摸就仿佛有人在网上挤压你的手臂。摄像头可以查看你是否心情不好,这样AI就不会过多打扰你。而耳机则可以感受到你周围的太阳,并在数字世界中投射出夏日,以增加真实感。这甚至还没有涉及到无聊的东西,譬如用以辅助跟踪、绘图和定位的惯性测量单元、视觉光相机、深度相机……

目前,最好的传感器是为工业作业和汽车行业制造的。这些都是拥有大量资本的行业。因此,作为一个额外的挑战,无论谁制造元宇宙的硬件,都需要确保他们足够廉价且安全,以便元宇宙就不会成为富人们的专属玩物。

  • 如何成为传感器制造商:拥有一家能够制造复杂消费电子产品的工厂。嘿,我可从没说过这很容易

6. 元宇宙作家(storyteller)

随着体验经济和游戏化概念的不断发展,我们要求我们的扩展现实体验具有很棒的并可汲取教训的故事情节,这是非常符合逻辑的。我们想笑;我们想哭;我们想要学习;我们想要从数字世界中看到一些稍微稀奇古怪点的东西。这就是元宇宙作家的用武之地.

元宇宙作家将负责为用户设计沉浸式的任务以探索元宇宙,如军事训练方案、以公司叙述的方式进行难以发现的营销机会、心理学会议(为什么杀死内心的恶魔当你可以在数字世界中模拟杀他们)……例子不胜枚举。

他们不会得到很好的报酬,除非你把为他们打call也算作报酬,但你没有。但至少他们会出售能够吸引数百万人来追的故事线,以助其摆脱枯燥的日常生活。这难道不是元宇宙作家们的梦想吗?

  • 如何成为元宇宙作家:主修文学,辅修市场营销,在游戏公司开始你的职业生涯,然后转向科技公司。

7. 世界建造者

在我们建立了我们的架构、我们的硬件和我们的故事情节后,我们仍需要创建整个世界(想想《盗梦空间》中的Ellen Page —或者是Elliot,老实说当时她仍在使用她的弃名)。我的意思不是为世界编码,我是指,想象世界

这个角色需要许多与电子游戏设计师相同的技能,尽管规则可能云泥之别。世界建设者将需要具有前瞻性并迎接未来,因为他们梦想的许多东西还没有以技术或产品解决方案的形式存在。

他们还需要考虑规章和道德。当数字世界变得真实时,在其中杀害可以吗?犯下战争罪行?我们已经自我发问这些问题了,但讨论还离得出结论很远呢。

  • 如何成为世界建造者:半战争诗人,半平面设计师。擅长我的世界(Minecraft)也没有什么坏处。

8. 广告拦截专家

Facebook…噢不好意思,是Meta,它如何赚钱?通过向它的虚假信息工厂出售订阅?通过摘取和出售器官?通过接受独裁者的捐款?当然不是(?)。他们卖广告。让我告诉你,元宇宙可能会以非常相似的方式运行。我想我们可以称之为DNA。你认为Instagram的广告很具有针对性且很烦人吗?那是因为你还没有看到他们可以用整个数据集做什么事,并且能够真正地在无处不在地关注到你。

想象一下,你在一个数字空间中走来走去,此时在现实世界的你很饿。不知不觉中,你盯着沿途的数字咖啡厅和餐馆看的时间长了很久。然后您猜怎么着,一分钟后,你开始收到食物的广告。 一开始听起来挺有趣的,但是从长远角度来看,这很干扰。因此,一旦我们丧失了新鲜感,我们就会希望广告拦截软件足够先进以发现嵌入现实中的广告,这就是广告拦截专家发挥作用的地方。

很像AdBlock Plus模式,我猜他们会开发插件来阻止广告弹出。他们不会得到太多报酬,但通过捐赠和获取数据,他们或许能维持生计。

  • 如何成为广告拦截专家:拥有基础编程知识,并能访问元宇宙的源代码。

9. 元宇宙网络安全

元宇宙是网络攻击和诈骗的完美目标:被黑的虚拟形象(avatars)、NFT 盗窃、生物识别/生理数据泄露(脑电波模式,有人在吗?),被黑的耳机……出现问题的可能几乎是无数的。

这就是我们需要元宇宙网络安全专家的原因。他们将实时阻止攻击,并确保法律和协议被重新考虑和修改,甚至再创造法律和协议,以囊括元宇宙所有可能的风险。

笔者对网络安全不甚了解,所以在这一段给出开放式的想法:虚拟世界违规成为现实世界的法案只是时间问题

  • 如何成为元宇宙网络安全专家:具有常规网络安全知识和/或具有技术倾向的法学学位。

10. 无薪实习生

笔者知道,为了得到一个漂亮整数而凑数,有点不光彩。但是请听我说完,虽然这个角色已经存在,但需要强调的是,它将是元宇宙未来的核心。

无薪实习生不仅能喝咖啡,他们研磨数据;他们制作风险投资的附录(VC’s decks’ appendices);他们为岩石和树木写代码;他们是科技帝国赖以建立的必要素材。

我们应该向他们的牺牲致敬。

而且,我们应该付钱给他们。


安全、舒适、引人入胜的叙事曲线、可定制的发型、家长控制、智力游戏、附加组件、小部件、通知、优化、意识形态一致性、娱乐和惩罚门户以及发出低沉隆隆声的传感器(beta版)……我们将在元宇宙中拥有一切!

但这不是一蹴而就的,元宇宙将需要无数新技术、协议、公司、创新和发现才能运作。并且,根据定义,它需要无数人从事不可量计的工作才能实现其宏伟目标。

如果您有幸在不久的将来得到上述之一的工作,只要记住一件事:: 世界是异常黯淡的,请你不要将其黑暗带入元宇宙


作者:用户8309268629399
来源:https://juejin.cn/post/7032552320483524645

收起阅读 »

如果将元宇宙逐层拆解,你会发现内核是“云”

2021 最火的新概念,莫过于元宇宙。2021 年 10 月 29 日,Facebook 宣布改名 Meta;2021 年 11 月 1 日,“元宇宙第一股” Roblox 经过短暂调整,宣布重新上线。接下来关于元宇宙的线下 / 线上讨论如火如荼,元宇宙概...
继续阅读 »

2021 最火的新概念,莫过于元宇宙。2021 年 10 月 29 日,Facebook 宣布改名 Meta;2021 年 11 月 1 日,“元宇宙第一股” Roblox 经过短暂调整,宣布重新上线。接下来关于元宇宙的线下 / 线上讨论如火如荼,元宇宙概念的热度可见一斑。

逐层拆解元宇宙

清华大学新闻学院教授、博士生导师沈阳教授在一场活动中分享道,元宇宙,英文是 Metaverse,从字面来理解,由 Meta(超越) 和 Universe(宇宙) 两部分组成。而沈阳教授团队也给元宇宙下了一个相对精确的定义:

元宇宙是整合多种新技术而产生的新型虚实相融的互联网应用和社会形态,它基于扩展现实技术提供沉浸式体验,基于数字孪生技术生成现实世界的镜像,基于区块链技术搭建经济体系,将虚拟世界与现实世界在经济系统、社交系统、身份系统上密切融合,并且允许每个用户进行内容生产和世界编辑。

Beamable 公司创始人 Jon Radoff 则在产业层面对元宇宙的概念做了拆解:“元宇宙构造的七个层面:体验;发现;创作者经济;空间计算;去中心化;人机互动;基础设施。”

体验层面相对最容易理解,目前我们常见到的游戏、社交等领域企业,都是在体验层面开展工作。著名游戏 《Second Life》 尤为经典。在这个游戏里,用户叫做"居民",可以通过可运动的虚拟化身互相交互。这套程序还在一个通常的元宇宙的基础上提供了一个高层次的社交网络服务。居民们可以四处逛逛,会碰到其他的居民,社交,参加个人或集体活动,制造和相互交易虚拟财产和服务。而典型《头号玩家》则是人们对于元宇宙在体验层面的自由畅想。

发现层面是用户了解到体验层的重要途径,其中包括各种应用商店,主要参与者是大型互联网公司;

创作者经济层 (Creator Economy): 帮助元宇宙创作者的成果货币化,其中包括设计工具 、 货币化技术、动画系统、图形工具等;

空间计算层 (Spital Computing): 对创作者经济层的赋能,具体包括 3D 引擎、手势识别、空 间映射和人工智能等,主要参与者是 3D 软硬件厂商;

去中心化层 (Decentralization): 这个层面的公司主要是帮助元宇宙生态系统构建分布式架 构,从而形成民主化结构;

人机交互层 (Human Interface): 人机交互层主要是大众接触元宇宙的媒介工具,主要体现在 触觉、姿势、声音、神经等层面,其中产品包括 AR/VR、手机、电脑、汽车、智能眼镜等可穿戴设备,主要参与者是 3D 软硬件厂商 ;

基础设施层 (Infrastructure):5G、半导体芯片、新型材料、云计算和电信网络等。基础设施层大概率是巨头之间的游戏,大部分是基础硬件公司。

可以说,元宇宙是整个人类经济体未来需求的一个集中出口,包含了用户对新体验的渴望,资本对新出口的渴望,技术对新领域的渴望,它是科技发展到一定阶段的必然新构想。即便 2021 没有出现“元宇宙”,可能也会出现“元世界”、“元矩阵”等其他概念。

元宇宙的关键支撑技术

以上关于元宇宙概念和产业分层方面的定义,是最近被很多人所熟知的概念,但这仍然没有解释元宇宙的实现路径,说到底,我们最想搞清楚的是,究竟该如何实现梦想中的元宇宙。

从技术维度来看,元宇宙的各部分关键支撑可以简称为:“HNCBD”,分别是硬件体验设备 (Hardware)、网络与算力 (Networking and Computing)、内容及应用生态 (Content)、区块链和 NFT(Blockchain),数字孪生(Digital Twin)。当然,这些核心技术在不同人眼里可能有细微区别,但总体相差不大。

在“HNCBD”中,H 属于硬件,不在软件开发者的常规讨论范围内;C 依赖百花齐放的应用社区;而网络与算力、区块链和 NFT、数字孪生,其实都存在一个统一的承载形式,就是云计算。

明眼人早已看出,如果排除因商业竞争而重复造轮子的问题,其实实现元宇宙最好的通路就是云。某种意义上讲,云不光承载的是元宇宙对于算力和基础设施的空前庞大的需求,更是各类在基础设施之上的 PaaS 、SaaS 服务。在元宇宙的发展过程中,如果每一家应用提供商、内容提供商,都要重构基础设施,包括基础的数据湖仓服务、数字孪生服务、机器学习服务,那成本将是不可想象的。

而当下阶段的云计算,除了提供基础的算力支撑,最关键的就是在游戏、AI 算法及 VR 三个方向上,提供了足够成熟的技术产品,其中最具代表性的就是亚马逊云科技。

回顾《头号玩家》的电影画面,演员们戴上眼镜,即进入了游戏世界,这其实是典型的云游戏场景。

目前大型游戏采用服务器 + 客户端的实现模式,对客户端硬件要求比较高,尤其是 3D 图形的渲染,基本完全依赖于终端运算。随着 5G 时代的到来,游戏将会在云端 GPU 上完成大规模渲染,游戏画面压缩后通过 5G 高速网络传送给用户。

在客户端,用户的游戏设备不需要任何高端处理器和显卡,只需要基本的视频解压能力。从游戏开发角度来看,游戏平台可以更加快速地部署新游戏功能,减少启动游戏所需的构建和测试工作量,满足玩家需求。

2020 年 9 月,亚马逊云科技就推出了自己的云游戏平台 Luna,兼容 PC、Mac、Fire TV、iPad 和 iPhone 和 Android 系统,知名游戏和平台厂商 Epic Games 也在利用 Amazon EC2 等 亚马逊云科技 服务及时扩展容量并支持远程创建者。Amazon G4 实例就是通过 GPU 来驱动云游戏渲染,通过 NVIDIA Video Codec SDK 传输最复杂的云游戏。Amazon G4 实例所搭载的 NVIDIA T4 GPU,也是云上第一款提供了 RT 核心、支持 NVIDIA RTX 实时光线追踪的 GPU 实例。

而元宇宙的体验又不仅限于云游戏,云游戏只是场景,VR 才是路径。

传统 VR 应用的局限性主要体现在四个方面,其中包括:购置主机和终端硬件成本高、设备使用率低、内容分散、移动性受限。

云计算和 VR 的结合,可以将 GPU 渲染功能从本地迁移到云端,从而使得终端的设计变得更加轻便与高性价比,降低了用户购买硬件设备的成本。VR 开发者可以 在云上进行快速的内容迭代发布,用户即点即玩、无需下载,解决内容不集中问题。

以 Amazon Sumerian 为例,开发者可以轻松创建 3D 场景并将其嵌入到现有网页中。Amazon Sumerian 编辑器则提供了现成的场景模板和 直观的拖放工具,使内容创建者、设计师和开发人员都可以构建交互式场景。Amazon Sumerian 采用最新的 WebGL 和 WebXR 标准,可直接在 Web 浏览器中创建沉浸式体验,并可通过简单的 URL 在几秒钟内进行访 问,同时能够在适用于 AR/VR 的主要硬件平台上运行。

除了云游戏和 VR,元宇宙的实现还有一个关键变量,就是 AI。AI 可以缩短数字创作时间,为元宇宙提供底层支持,主要体现在计算机视觉、智能语音语义、机器学习。三者都需要巨大的算力和存储,云计算为人工智能提供了无限的算力和存储支持。有一家叫做 GE Healthcare 的公司,就是使用 Amazon P4d 实例,将定制化 AI 模型处理时间从几天缩短为几小时,使训练模型的速度提高了两三倍,从而提供各类远程医疗、诊断服务。

AI 在虚拟形象上的价值更明显,亚马逊云科技的 AI 服务在此领域有很多的应用实践包括图像 AI 生成(自动上色、场景调整、图像二次元化)、模型自动生成(动画自动生成、场景道具生成)、游戏机器人(游戏 AI NPC、文本交互、语音驱动口型动画、动作补抓、表情迁移)、偶像营销运营(聊天观察、流行搭配、反外挂)等。

云计算企业在元宇宙领域的核心工作

如果说,以上拆解更多还属于理论分析,那么,如果我们仔细看看头部云计算企业的近期动态,就会发现关于元宇宙的种种技术支撑正在云端成为现实。

2021 亚马逊云科技 re:Invent,亚马逊云科技发布了 Amazon IoT TwinMaker 与 Amazon Private 5G。

前者让开发人员可以轻松汇集来自多个来源(如设备传感器、摄像机和业务应用程序)的数据,并将这些数据结合起来创建一个知识图谱,对现实世界环境进行建模,是实现工业元宇宙的组成技术之一。

后者则可自动设置和部署企业专有 5G 网络,并按需扩展容量以支持更多设备和网络流量,重点服务了以工业 4.0 为主的庞大传感器和端侧设备集群,前文提到的工业元宇宙、车联网自然也在同一序列。

更不用说 Amazon SageMaker Canvas,用无代码理念构建机器学习模型,做模型预测,保证在脱离数据工程团队的情况下,依然可以提供服务,进一步降低了未来元宇宙内容生产的门槛,保证了内容的多样性。

同样在 2021 亚马云科技 re:Invent 全球大会期间,元宇宙公司 Meta 宣布深化与亚马逊云科技的合作,将亚马逊云科技作为其战略云服务提供商。

据介绍,Meta 使用亚马逊云科技可靠的基础设施和全面的功能,补充其现有的本地基础设施,并将使用更多亚马逊云科技的计算、存储、数据库和安全服务,获得云端更好的隐私保护、可靠性和扩展性,包括将在亚马逊云科技上运行第三方合作应用,并使用云服务支持其收购的已经在使用亚马逊云科技的企业。

Meta 还将使用亚马逊云科技的计算服务来加速 Meta AI 部门人工智能项目的研发工作。另外,亚马逊云科技和 Meta 双方还将合作帮助客户提高在亚马逊云科技上运行深度学习计算框架 PyTorch 的性能,并助力开发人员加速构建、训练、部署和运行人工智能和机器学习模型的机制。

亚马逊全球副总裁、亚马逊云科技大中华区执行董事张文翊认为,这是云计算可以大量赋能的一个领域。她表示:“我们认为元宇宙一定是云计算可以大量赋能的一个领域。元宇宙本身需要的就是计算、存储、机器学习等,这些都离不开云计算。”

未来仍在描绘中

未来元宇宙的技术栈是否会扩展,元宇宙的呈现形式是否会出现大幅变化?

答案几乎是肯定的,就像在 4G 手机普及以前,我们完全无法想象 4G 生态下主要的应用类型。从建设到成熟,仅在算力层面,元宇宙也至少还有十余年时间的路程要走。

但万变不离其宗,关注云计算领域关于元宇宙支撑技术的更新迭代,可能是我们抛开泡沫,观察元宇宙生态进展的重要方法。


作者:SegmentFault思否
来源:https://juejin.cn/post/7041125661465346062

收起阅读 »

前端 4 种渲染技术的计算机理论基础

前端可用的渲染技术有 html + css、canvas、svg、webgl,我们会综合运用这些技术来绘制页面。有没有想过这些技术有什么区别和联系,它们和图形学有什么关系呢?本文我们就来谈一下网页渲染技术的计算机理论基础。渲染的理论基础人眼的视网膜有视觉暂留机...
继续阅读 »

前端可用的渲染技术有 html + css、canvas、svg、webgl,我们会综合运用这些技术来绘制页面。有没有想过这些技术有什么区别和联系,它们和图形学有什么关系呢?

本文我们就来谈一下网页渲染技术的计算机理论基础。

渲染的理论基础

人眼的视网膜有视觉暂留机制,也就是看到的图像会继续保留 0.1s 左右,图形界面就是根据这个原理来设计的一帧一帧刷新的机制,要保证 1s 至少要渲染 10 帧,这样人眼看到画面才是连续的。

每帧显示的都是图像,它是由像素组成的,是显示的基本单位。不同显示器实现像素的原理不同。

我们要绘制的目标是矩形、圆形、椭圆、曲线等各种图形,绘制完之后要把它们转成图像。图形的绘制有一系列的理论,比如贝塞尔曲线是画曲线的理论。图形转图像的过程叫做光栅化。这些图形的绘制和光栅化的过程,都是图形学研究的内容。

图形可能做缩放、平移、旋转等变化,这些是通过矩阵计算来实现的,也是图形学的内容。

除了 2D 的图形外,还要绘制 3D 的图形。3D 的原理是把一个个三维坐标的顶点连起来,构成一个一个三角形,这是造型的过程。之后再把每一个三角形的面贴上图,叫做纹理。这样组成的就是一个 3D 图形,也叫 3D 模型。

3D 图形也同样需要经历光栅化变成二维的图像,然后显示出来。这种三维图形的光栅化需要找一个角度去观察,就像拍照一样,所以一般把这个概念叫做相机。

同时,为了 3D 图形更真实,还引入了光线的概念,也就是一束光照过来,3D 图形的每个面都会有什么变化,怎么反射等。不同材质的物体反射的方式不同,比如漫反射、镜面反射等,也就有不同的计算公式。一束光会照射到一些物体,到物体的反射,这个过程需要一系列跟踪的计算,叫做光线追踪技术。

我们也能感受出来,3D 图形的计算量比 2D 图形大太多了,用 CPU 计算很可能达不到 1s 大于 10 帧,所以后面出现了专门用于 3D 渲染加速的硬件,叫做 GPU。它是专门用于这种并行计算的,可以批量计算一堆顶点、一堆三角形、一堆像素的光栅化,这个渲染流程叫做渲染管线。

现在的渲染管线都是可编程的,也就是可以控制顶点的位置,每个三角形的着色,这两种分别叫做顶点着色器(shader)、片元着色器。

总之,2D 或 3D 的图形经过绘制和光栅化就变成了一帧帧的图像显示出来。

变成图像之后其实还可以做一些图像处理,比如灰度、反色、高斯模糊等各种滤镜的实现。

所以,前端的渲染技术的理论基础是计算机图形学 + 图像处理。

不同的渲染技术的区别和联系

具体到前前端的渲染技术来说,html+css、svg、canvas、webgl 都是用于图形和图像渲染的技术,但是它们各有侧重:

html + css

html + css 是用于图文布局的,也就是计算文字、图片、视频等的显示位置。它提供了很多计算规则,比如流式布局很适合做图文排版,弹性布局易于做自适应的布局等。但是它不适合做更灵活的图形绘制,这时就要用其他几种技术了。

canvas

canvas 是给定一块画布区域,在不同的位置画图形和图像,它没有布局规则,所以很灵活,常用来做可视化或者游戏的开发。但是 canvas 并不会保留绘制的图形的信息,生成的图像只能显示在固定的区域,当显示区域变大的时候,它不能跟随一起放缩,就会失真,如果有放缩不失真的需求就要用其他渲染技术了。

svg

svg 会在内存中保留绘制的图形的信息,显示区域变化后会重新计算,是一个矢量图,常用于 icon、字体等的绘制。

webgl

上面的 3 种技术都是用于 2D 的图形图像的绘制,如果想绘制 3D 的内容,就要用 webgl 了。它提供了绘制 3D 图形的 api,比如通过顶点构成 3D 的模型,给每一个面贴图,设置光源,然后光栅化成图像等的 api。它常用于通过 3D 内容增强网站的交互效果,3D 的可视化,3D 游戏等,再就是虚拟现实中的 3D 交互。

所以,虽然前端渲染技术的底层原理都是图形学 + 图像处理,但上层提供的 4 种渲染技术各有侧重点。

不过,它们还是有很多相同的地方的:

  • 位置、大小等的变化都是通过矩阵的计算

  • 都要经过图形转图像,也就是光栅化的过程

  • 都支持对图像做进一步处理,比如各种滤镜

  • html + css 渲染会分不同图层分别做计算,canvas 也会根据计算量分成不同的 canvas 来做计算

因为他们底层的图形学原理还是一致的。

除此以外,3D 内容,也就是 webgl 的内容会通过 GPU 来计算,但 css 其实也可以通过 GPU 计算,这叫做 css 的硬件加速,有四个属性可以触发硬件加速:transform、opacity、filter、will-change。(更多的 GPU 和 css 硬件加速的内容可以看这篇文章:这一次,彻底搞懂 GPU 和 css 硬件加速

编译原理的应用

除了图形学和图像技术外,html+css 还用到了编译技术。因为 html、css 是一种 DSL( domin specific language,领域特定语言),也就是专门为界面描述所设计的语言。用 html 来表达 dom 结构,用 css 来给 dom 添加样式都只需要很少的代码,然后运行时解析 html 和 css 来创建 dom、添加样式。

DSL 可以让特定领域的逻辑更容易表达,前端领域还有一些其他技术也用到了 DSL,比如 graphql。

总结

因为人眼的视觉暂留机制,只要每帧绘制不超过 0.1s,人看到的画面就是连续的,这是显示的原理。每帧的绘制要经过图形绘制和图形转图像的光栅化过程,2D 和 3D 图形分别有不同的绘制和光栅化的算法。此外,转成图像之后还可以做进一步的图像处理。

前端领域的四种渲染技术:html+css、canvas、svg、webgl 各有侧重点,分别用于不同内容的渲染:

  • html+ css 用于布局

  • canvas 用于灵活的图形图像渲染

  • svg 用于矢量图渲染

  • webgl 用于 3D 图形的渲染

但他们的理论基础都是计算机图形学 + 图像处理。(而且,html+css 为了方便逻辑的表达,还设计了 DSL,这用到了编译技术)

这四种渲染技术看似差别很大,但在理论基础层面,很多东西都是一样的。这也是为什么我们要去学计算机基础,因为它可以让我们对技术有一个更深入的更本质的理解。


作者:zxg_神说要有光
来源:https://juejin.cn/post/7041157165024804895

收起阅读 »

如何用 docker 打造前端开发环境

用 docker 做开发环境的好处 保持本机清爽 做开发的都知道,电脑一买回来就要安装各种各样的环境,比如前端开发需要安装 node、yarn、git 等,为了使用某些工具或者包,可能还需要安装 python 或者 java 等(比如 jenkins 就依赖了...
继续阅读 »

用 docker 做开发环境的好处


保持本机清爽


做开发的都知道,电脑一买回来就要安装各种各样的环境,比如前端开发需要安装 node、yarn、git 等,为了使用某些工具或者包,可能还需要安装 python 或者 java 等(比如 jenkins 就依赖了 java),久而久之,本机环境会非常乱,对于一些强迫证患者或者有软件洁癖的人来说多少有点不爽。


使用 docker 后,开发环境都配置在容器中,开发时只需要打开 docker,开发完后关闭 docker,本机不用再安装乱七八糟的环境,非常清爽。


隔离环境


不知道大家在开发时有没有遇到这种情况:公司某些项目需要在较新的 node 版本上运行(比如 vite,需要在 node12 或以上),某些老的项目需要在较老的 node 版本上运行,切换起来比较麻烦,虽然可以用 nvm 来解决,但使用 docker 可以更方便的解决此问题。


快速配置环境


买了新电脑,或者重装了系统,又或者换了新的工作环境,第一件事就是配置开发环境。下载 node、git,然后安装一些 npm 的全局包,然后下载 vscode,配置 vscode,下载插件等等……


使用 docker 后,只需从 docker hub 中拉取事先打包好的开发环境镜像,就可以愉快的进行开发了。


安装 docker


到 docker 官网(http://www.docker.com)下载 docker desktop 并安装,此步比较简单,省略。


安装完成,打开 docker,待其完全启动后,打开 shell 输入:


docker images

截屏2021-09-29 22.16.13.png


显示上述信息即成功!


配置开发环境


假设有一个项目,它必须要运行在 8.14.0 版本的 node 中,我们先去 docker hub 中将这个版本的 node 镜像拉取下来:


docker pull node:8.14.0

拉取完成后,列出镜像列表:


docker images

截屏2021-09-29 23.30.16.png


有了镜像后,就可以使用镜像启动一个容器:


docker run -it --name my_container 3b7ecd51 /bin/bash

上面的命令表示以命令行交互的模式启动一个容器,并将容器的名称指定为 my_container。


截屏2021-10-08 23.16.11.png


此时已经新建并进入到容器,容器就是一个 linux 系统,可以使用 linux 命令,我们尝试输入一些命令:


截屏2021-10-08 23.18.11.png


可以看到这个 node 镜像除了预装了 node 8.14.0,还预装了 git 2.11.0。



镜像和容器的的关系:镜像只预装了最基本的环境,比如上面的 node:8.14.0 镜像可以看成是预装了 node 8.14.0 的 linux 系统,而容器是基于镜像克隆出来的另一个 linux 系统,可以在这个系统中安装其它环境比如 java、python 等,一个镜像可以建立多个容器,每个容器环境都是相互隔离的,互不影响(比如在容器 A 中安装了 java,容器 B 是没有的)。



使用命令行操作项目并不方便,所以我们先退出命令行模式,使用 exit 退出:


截屏2021-10-08 23.20.49.png


借助 IDE 可以更方便的玩 docker,这里我们选择 vscode,打开 vscode,安装 Remote - Containers 扩展,这个扩展可以让我们更方便的管理容器:


截屏2021-10-08 23.03.53.png


安装成功后,左下角会多了一个图标,点击:


截屏2021-10-08 23.23.00.png


在展开菜单中选择“Attach to Running Container”:


截屏2021-10-08 23.25.02.png


此时会报一个错“There are no running containers to attach to.”,因为我们刚刚退出了命令行交互模式,所以现在容器是处理停止状态的,我们可以使用以下命令来查看正在运行的容器:


docker ps

# 或者
docker container ls

截屏2021-10-08 23.30.51.png


发现列表中并没有正在运行的容器,我们需要找到刚刚创建的容器并将其运行起来,先显示所有容器列表:


# -a 可以显示所有容器,包括未运行的
docker ps -a

# 或者
docker container ls -a

截屏2021-10-08 23.29.25.png


运行指定容器:


# 使用容器名称
docker start my_container

# 或者使用容器 id,id 只需输入前几位,docker 会自动识别
docker start 8ceb4

再次运行 docker ps 命令后,就可以看到已运行的容器了。然后回到 vscode,再次选择"Attach to Running Container",就会出现正在运行的容器列表:


截屏2021-10-08 23.36.14.png


选择容器进入,添加一个 bash 终端,就可以进入我们刚刚的命令行模式:


截屏2021-10-08 23.40.14.png


我们安装 vue-cli,并在 /home 目录下创建一个项目:


# 安装 vue-cli
npm install -g @vue/cli

# 进入到 home 目录
cd /home

# 创建 vue 项目
vue create demo

在 vscode 中打开目录,发现打开的不再是本机的目录,而是容器中的目录,找到我们刚刚创建的 /home/demo 打开:


截屏2021-10-09 00.01.13.png


输入 npm run serve,就可以愉快的进行开发啦:


截屏2021-10-09 00.03.32.png


上面我们以 node 8.14.0 镜像为例创建了一个开发环境,如果想使用新版的 node 也是一样的,只需要将指定版本的 node 镜像 pull 下来,然后使用这个镜像创建一个容器,并在容器中创建项目或者从 git 仓库中拉取项目进行开发,这样就有了两个不同版本的 node 开发环境,并且可以同时进行开发。


使用 ubuntu 配置开发环境


上面这种方式使用起来其实并不方便,因为 node 镜像只安装了 node 和 git,有时我们希望镜像可以内置更多功能(比如预装 nrm、vue-cli 等 npm 全局包,或者预装好 vscode 的扩展等),这样用镜像新建的容器也包含这些功能,不需要每个容器都要安装一次。


我们可以使用 ubuntu 作为基础自由配置开发环境,首先获取 ubuntu 镜像:


# 不输入版本号,默认获取 latest 即最新版
docker pull ubuntu

新建一个容器:


docker run -itd --name fed 597ce /bin/bash

这里的 -itd 其实是 -i -t -d 的合写,-d 是在后台中运行容器,相当于新建时一并启动容器,这样就不用使用 docker start 命令了。后面我们直接用 vscode 操作容器,所以也不需要使用命令行模式了。


我们将容器命名为 fed(表示 front end development),建议容器的名称简短一些,方便输入。


截屏2021-10-10 00.11.40.png


ubuntu 镜像非常纯净(只有 72m),只具备最基本的能力,为了后续方便使用,我们需要更新一下系统,更新前为了速度快一点,先换到阿里的源,用 vscode 打开 fed 容器,然后打开 /etc/apt/sources.list 文件,将内容改为:


deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse

截屏2021-10-10 00.18.57.png


在下面的终端中依次输入以下命令更新系统:


apt-get update
apt-get upgrade

安装 sudo:


apt-get install sudo

安装 git:


apt-get install git

安装 wget(wget 是一个下载工具,我们需要用它来下载软件包,当然也可以选择 axel,看个人喜好):


apt-get install wget

为了方便管理项目与软件包,我们在 /home 目录中创建两个文件夹(projects 与 packages),projects 用于存放项目,packages 用于存放软件包:


cd /home
mkdir projects
mkdir packages

由于 ubuntu 源中的 node 版本比较旧,所以从官网中下载最新版,使用 wget 下载 node 软件包:


# 将 node 放到 /home/packages 中
cd /home/packages

# 需要下载其它版本修改版本号即可
wget https://nodejs.org/dist/v14.18.0/node-v14.18.0-linux-x64.tar

解压文件:


tar -xvf node-v14.18.0-linux-x64.tar

# 删除安装包
rm node-v14.18.0-linux-x64.tar

# 改个名字,方便以后切换 node 版本
mv node-v14.18.0-linux-x64 node

配置 node 环境变量:


# 修改 profile 文件
echo "export PATH=/home/packages/node/bin:$PATH" >> /etc/profile

# 编译 profile 文件,使其生效
source /etc/profile

# 修改 ~.bashrc,系统启动时编译 profile
echo "source /etc/profile" >> ~/.bashrc

# 之后就可以使用 node 和 npm 命令了
node -v
npm -v

安装 nrm,并切换到 taobao 源:


npm install nrm -g
nrm use taobao

安装一些 vscode 扩展,比如 eslint、vetur 等,扩展是安装在容器中的,在容器中会保留一份配置文件,到时打包镜像会一并打包进去。当我们关闭容器后再打开 vscode,可以发现本机的 vscode 中并没有安装这些扩展。


至此一个简单的前端开发环境已经配置完毕,可以根据自己的喜好自行添加一些包,比如 yarn、nginx、vim 等。


打包镜像


上面我们通过 ubuntu 配置了一个简单的开发环境,为了复用这个环境,我们需要将其打包成镜像并推送到 docker hub 中。


第一步:先到 docker 中注册账号。


第二步:打开 shell,登录 docker。


截屏2021-10-10 01.44.26.png


第三步:将容器打包成镜像。


# commit [容器名称] [镜像名称]
docker container commit fed fed

第四步:为镜像打 tag,因为镜像推送到 docker hub 中,要用 tag 来区分版本,这里我们先设置为 latest。tag 名称加上了用户名做命名空间,防止与 docker hub 上的镜像冲突。


docker tag fed huangzhaoping/fed:latest

第五步:将 tag 推送至 docker hub。


docker push huangzhaoping/fed:latest

第六步:将本地所有关于 fed 的镜像和容器删除,然后从 docker hub 中拉取刚刚推送的镜像:


# 拉取
docker pull huangzhaoping/fed

# 创建容器
docker run -itd --name fed huangzhaoping/fed /bin/bash

用 vscode 打开容器,打开命令行,输入:


node -v
npm -v
nrm -V
git --version

然后再看看 vscode 扩展,可以发现扩展都已经安装好了。


如果要切换 node 版本,只需要下载指定版本的 node,解压替换掉 /home/packages/node 即可。


至此一个 docker 开发环境的镜像就配置完毕,可以在不同电脑,不同系统中共享这个镜像,以达到快速配置开发环境的目的。


注意事项



  • 如果要将镜像推送到 docker hub,不要将重要的信息保存到镜像中,因为镜像是共享的,避免重要信息泄露。

  • 千万不要在容器中存任何重要的文件或信息,因为容器一旦误删这些文件也就没了。

  • 如果在容器中开发项目,记得每天提交代码到远程仓库,避免容器误删后代码丢失。

作者:Rise_
链接:https://juejin.cn/post/7017129520649994253

收起阅读 »

【手写代码】面试官:请你手写防抖和节流

一、前言当用户高频触发某一事件时,如窗口的resize、scroll,输入框内容校验等,此时这些事件调用函数的频率如果没有限制,可能会导致响应跟不上触发,出现页面卡顿,假死现象。此时,我们可以采用 防抖(debounce) 和 节...
继续阅读 »

一、前言

当用户高频触发某一事件时,如窗口的resize、scroll,输入框内容校验等,此时这些事件调用函数的频率如果没有限制,可能会导致响应跟不上触发,出现页面卡顿,假死现象。此时,我们可以采用 防抖(debounce) 和 节流(throttle) 的方式来减少调用频率,同时又不影响实际效果。

二、防抖

假设你用手压住一个弹簧,那么弹簧不会弹起来,除非你松手。

函数防抖,就是指触发事件后,函数在 n 秒后只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数的执行时间。

简单的说,当一个函数连续触发,只执行最后一次。

函数防抖一般用在什么情况之下呢?一般用在,连续的事件只需触发一次回调的场合。具体有:

  1. 搜索框搜索输入。只需用户最后一次输入完,再发送请求;
  2. 用户名、手机号、邮箱输入验证;
  3. 浏览器窗口大小改变后,只需窗口调整完后,再执行resize事件中的代码,防止重复渲染。

代码实现

在下面这段代码中,我们实现了最简单的一个防抖函数,我们设置一个定时器,你重复调用一次函数,我们就清除定时器,重新定时,直到在设定的时间段内没有重复调用函数。

// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
// timer是一个定时器
let timer = null;
// 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
return function () {
// 调用一次就清除上一次的定时器
clearTimeout(timer);
// 开启这一次的定时器
timer = setTimeout(() => {
fn();
}, delay)
}
}

代码优化

仔细一想,上面的代码是不是有什么问题?

问题一: 我们返回的fn函数,如果需要事件参数e怎么办?事件参数被debounce函数保存着,如果不把事件参数给闭包函数,若fn函数需要e我们没给,代码毫无疑问会报错。

问题二: 我们怎么确保调用fn函数的对象是我们想要的对象?你发现了吗,在上面这段代码中fn()函数的调用者是fn所定义的环境,这里涉及this指向问题,想要了解为什么可以去了解下js中的this。

为了解决上述两个问题,我们对代码优化如下

// fn是你要调用的函数,delay是防抖的时间
function debounce(fn, delay) {
// timer是一个定时器
let timer = null;
// 返回一个闭包函数,用闭包保存timer确保其不会销毁,重复点击会清理上一次的定时器
return function () {
// 保存事件参数,防止fn函数需要事件参数里的数据
let arg = arguments;
// 调用一次就清除上一次的定时器
clearTimeout(timer);
// 开启这一次的定时器
timer = setTimeout(() => {
// 若不改变this指向,则会指向fn定义环境
fn.apply(this, arg);
}, delay)
}
}

三、节流

当水龙头的水一直往下流,这十分的浪费水,所以我们可以把龙头关小一点,让水一滴一滴往下流,每隔一段时间掉下来一滴水。

节流就是限制一个函数在一段时间内只能执行一次,过了这段时间,在下一段时间又可以执行一次。应用场景如:

  1. 输入框的联想,可以限定用户在输入时,只在每两秒钟响应一次联想。
  2. 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
  3. 表单验证
  4. 按钮提交事件。

代码实现1(时间戳版)

// 方法一:时间戳
function throttle(fn, delay = 1000) {
// 记录第一次的调用时间
var prev = null;
console.log(prev);
// 返回闭包函数
return function () {
// 保存事件参数
var args = arguments;
// 记录现在调用的时间
var now = Date.now();
// console.log(now);
// 如果间隔时间大于等于设置的节流时间
if (now - prev >= delay) {
// 执行函数
fn.apply(this, args);
// 将现在的时间设置为上一次执行时间
prev = now;
}
}
}

触发事件时立即执行,以后每过delay秒之后才执行一次,并且最后一次触发事件若不满足要求不会被执行

代码实现2(定时器版)

// 方法二:定时器
function throttle(fn, delay) {
// 重置定时器
let timer = null;
// 返回闭包函数
return function () {
// 记录事件参数
let args = arguments;
// 如果定时器为空
if (!timer) {
// 开启定时器
timer = setTimeout(() => {
// 执行函数
fn.apply(this, args);
// 函数执行完毕后重置定时器
timer = null;
}, delay);
}
}
}

第一次触发时不会执行,而是在delay秒之后才执行,当最后一次停止触发后,还会再执行一次函数。

代码实现3(时间戳 & 定时器)

// 方法三:时间戳 & 定时器
function throttle(fn, delay) {
// 初始化定时器
let timer = null;
// 上一次调用时间
let prev = null;
// 返回闭包函数
return function () {
// 现在触发事件时间
let now = Date.now();
// 触发间隔是否大于delay
let remaining = delay - (now - prev);
// 保存事件参数
const args = arguments;
// 清除定时器
clearTimeout(timer);
// 如果间隔时间满足delay
if (remaining <= 0) {
// 调用fn,并且将现在的时间设置为上一次执行时间
fn.apply(this, args);
prev = Date.now();
} else {
// 否则,过了剩余时间执行最后一次fn
timer = setTimeout(() => {
fn.apply(this, args)
}, delay);
}
}
}


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


收起阅读 »

vue工程师必须学会封装的埋点指令思路

vue
前言 最近项目中需要做埋点功能,梳理下产品的埋点文档,发现点击埋点的场景比较多。因为使用的是阿里云sls日志服务去埋点,所以通过使用手动侵入代码式的埋点。定好埋点的形式后,技术实现方法也有很多,哪种比较好呢? 稍加思考... 决定封装个埋点指令,这样使用起来...
继续阅读 »

前言


最近项目中需要做埋点功能,梳理下产品的埋点文档,发现点击埋点的场景比较多。因为使用的是阿里云sls日志服务去埋点,所以通过使用手动侵入代码式的埋点。定好埋点的形式后,技术实现方法也有很多,哪种比较好呢?


稍加思考...



决定封装个埋点指令,这样使用起来会比较方便,因为指令的颗粒度比较细能够直击要害,挺适合上面所说的业务场景。


指令基础知识


在此之前,先来复习下vue自定义指令吧,这里只介绍常用的基础知识。更全的介绍可以查看官方文档


钩子函数




  • bind:只调用一次,指令第一次绑定到元素时调用。




  • inserted:被绑定元素插入父节点时调用。




  • update:所在组件的 VNode 更新时调用。




钩子函数参数



  • el:指令所绑定的DOM元素。

  • binding:一个对象,包含以下 property:

    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。

    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"。



  • vnode:指令所绑定的当前组件vnode。


在这里分享个小技巧,钩子函数参数中没有可以直接获取当前实例的参数,但可以通过 vnode.context 获取到,这个在我之前的vue技巧文章中也有分享到,有兴趣可以去看看。


正文


进入正题,下面会介绍埋点指令的使用,内部是怎么实现的。


用法与思路


一般我在封装一个东西时,会先确定好它该怎么去用,然后再从用法入手去封装。这样会令整个思路更加清晰,在定义用法时也可以思考下易用性,不至于封装完之后因为用法不理想而返工。


埋点上报的数据会分为公共数据(每个埋点都要上报的数据)和自定义数据(可选的额外数据,和公共数据一起上报)。那么公共数据在内部就进行统一处理,对于自定义数据则需要从外部传入。于是有了以下两种用法:



  • 一般用法


<div v-track:clickBtn></div>


  • 自定义数据


<div v-track:clickBtn="{other:'xxx'}"></div>

可以看到埋点事件是通过 arg 的形式传入,在此之前也看到有些小伙伴封装的埋点事件是在 value 传入。但我个人比较喜欢 arg 的形式,这种更能让人一目了然对应的埋点事件是什么。


另外上报数据结构大致为:


{   
eventName: 'clickBtn'
userId: 1,
userName: 'xxx',
data: {
other: 'xxx'
}
}

eventName 是埋点对应的事件名,与之同级的是公共数据,而自定义数据放在 data 内。


实现


定义一个 track.js 的文件


import SlsWebLogger from 'js-sls-logger'

function getSlsWebLoggerInstance (options = {}) {
return new SlsWebLogger({
host: '***',
project: '***',
logstore: `***`,
time: 10,
count: 10,
...options
})
}

export default {
install (Vue, {baseData = {}, slsOptions = {}) {
const slsWebLogger = getSlsWebLoggerInstance(slsOptions)
// 获取公共数据的方法
let getBaseTrackData = typeof baseData === 'function' ? baseData : () => baseData
let baseTrackData = null
const Track = {
name: 'track',
inserted (el, binding) {
el.addEventListener('click', () => {
if (!binding.arg) {
console.error('Track slsWebLogger 事件名无效')
return
}
if (!baseTrackData) {
baseTrackData = getBaseTrackData()
}
baseTrackData.eventName = binding.arg
// 自定义数据
let trackData = binding.value || {}
const submitData = Object.assign({}, baseTrackData, {data: trackData})
// 上报
slsWebLogger.send(submitData)
if (process.env.NODE_ENV === 'development') {
console.log('Track slsWebLogger', submitData)
}
})
}
}
Vue.directive(Track.name, Track)
}
}

封装比较简单,主要做了两件事,首先是为绑定指令的 DOM 添加 click 事件,其次处理上报数据。在封装埋点指令时,公共数据通过baseData传入,这样可以增加通用性,第二个参数是上报平台的一些配置参数。


在初始化时注册指令:


import store from 'src/store'
import track from 'Lib/directive/track'

function getBaseTrackData () {
let userInfo = store.state.User.user_info
// 公共数据
const baseTrackData = {
userId: userInfo.user_id, // 用户id
userName: userInfo.user_name // 用户名
}
return baseTrackData
}

Vue.use(track, {baseData: getBaseTrackData})

Vue.use 时会自动寻找 install 函数进行调用,最终在全局注册指令。


加点通用性


除了点击埋点之外,如果有停留埋点等场景,上面的指令就不适用了。为此,可以增加手动调用的形式。


export default {
install (Vue, {baseData = {}, slsOptions = {}) {
// ...
Vue.directive(Track.name, Track)
// 手动调用
Vue.prototype.slsWebLogger = {
send (trackData) {
if (!trackData.eventName) {
console.error('Track slsWebLogger 事件名无效')
return
}
const submitData = Object.assign({}, getBaseTrackData(), trackData)
slsWebLogger.send(submitData)
if (process.env.NODE_ENV === 'development') {
console.log('Track slsWebLogger', submitData)
}
}
}
}

这种挂载到原型的方式可以在每个组件实例上通过 this 方便进行调用。


export default {
// ...
created () {
this.slsWebLogger.send({
//...
})
}
}

总结


本文分享了封装埋点指令的过程,封装并不难实现。主要有两种形式,点击埋点通过绑定 DOM click 事件监听点击上报,而其他场景下提供手动调用的方式。主要是想记录下封装的思路,以及使用方式。埋点实现也是根据业务做了一些调整,比如注册埋点指令可以接受上报平台的配置参数。毕竟人是活的,代码是死的。只要能满足业务需求并且能维护,怎么使用舒服怎么来嘛。


作者:出来吧皮卡丘
链接:https://juejin.cn/post/7040649951923142687

收起阅读 »

Android中的类加载器

类的生命周期加载阶段加载阶段可以细分如下加载类的二进制流数据结构转换,将二进制流所代表的静态存储结构转化成方法区的运行时的数据结构生成java.lang.Class对象,作为方法区这个类的各种数据的访问入口加载类的二进制流的方法从zip包中读取。我们常见的JA...
继续阅读 »

类的生命周期

image.png

加载阶段

加载阶段可以细分如下

  • 加载类的二进制流
  • 数据结构转换,将二进制流所代表的静态存储结构转化成方法区的运行时的数据结构
  • 生成java.lang.Class对象,作为方法区这个类的各种数据的访问入口

加载类的二进制流的方法

  • 从zip包中读取。我们常见的JAR、AAR依赖
  • 运行时动态生成。我们常见的动态代理技术,在java.reflect.Proxy中就是用ProxyGenerateProxyClass来为特定的接口生成代理的二进制流
验证

验证是连接阶段的第一步,这一阶段的目的是为了确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

  1. 文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。
    此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求。
  2. 元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。
    第二阶段,保证不存在不符合 Java 语言规范的元数据信息。
  3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上。
  4. 符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用。

可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

类变量分配内存并设置类变量初始值,这些变量所使用的内存都将在方法区中进行分配。

解析

虚拟机将常量池内的符号引用替换为直接引用的过程。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行

初始化

到初始化阶段,才真正开始执行类中定义的 Java 程序代码,此阶段是执行 () 方法的过程。

类加载的时机

虚拟机规范规定了有且只有 5 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始)

  1. 遇到new、getstatic 和 putstatic 或 invokestatic 这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。对应场景是:使用 new 实例化对象、读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)、以及调用一个类的静态方法。

  2. 对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  3. 当初始化类的父类还没有进行过初始化,则需要先触发其父类的初始化。(而一个接口在初始化时,并不要求其父接口全部都完成了初始化)

  4. 虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),
    虚拟机会先初始化这个主类。

  5. 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

注意:

  1. 通过子类引用父类的静态字段,不会导致子类初始化。
  2. 通过数组定义来引用类,不会触发此类的初始化。MyClass[] cs = new MyClass[10];
  3. 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

类加载器

把实现类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的代码模块称为“类加载器”。

将 class 文件二进制数据放入方法区内,然后在堆内(heap)创建一个 java.lang.Class 对象,Class 对象封装了类在方法区内的数据结构,并且向开发者提供了访问方法区内的数据结构的接口。

类的唯一性

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。

即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。
这里所指的“相等”,包括代表类的 Class 对象的 equals() 方法、 isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况

双亲委托机制

image.png

如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

    protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
//先从缓存中加没加载这个类
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//从parent中加载
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//加载不到,就自己加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}


好处
  • 避免重复加载,当父加载器已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
  • 安全性考虑,防止核心API库被随意篡改。

Android中ClassLoader

image.png

  • ClassLoader是一个抽象类,定义了ClassLoader的主要功能
  • BootClassLoader是ClassLoader的子类(注意不是内部类,有些材料上说是内部类,是不对的),用于加载一些系统Framework层级需要的类,是Android平台上所有的ClassLoader的最终parent
  • SecureClassLoader扩展了ClassLoader类,加入了权限方面的功能,加强了安全性
  • URLClassLoader继承SecureClassLoader,用来通过URI路径从jar文件和文件夹中加载类和资源,在Android中基本无法使用
  • BaseDexClassLoader是实现了Android ClassLoader的大部分功能
  • PathClassLoader加载应用程序的类,会加载/data/app目录下的dex文件以及包含dex的apk文件或者java文件(有些材料上说他也会加载系统类,我没有找到,这里存疑)
  • DexClassLoader可以加载自定义dex文件以及包含dex的apk文件或jar文件,支持从SD卡进行加载。我们使用插件化技术的时候会用到
  • InMemoryDexClassLoader用于加载内存中的dex文件

ClassLoader的加载流程源码分析

-> ClassLoader.java 类

protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized(this.getClassLoadingLock(name)) {
//先查找class是否已经加载过,如果加载过直接返回
Class c = this.findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();

try {
if (this.parent != null) {
//委托给parent加载器进行加载 ClassLoader parent;
c = this.parent.loadClass(name, false);
} else {
//当执行到顶层的类加载器时,parent = null
c = this.findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException var10) {
}

if (c == null) {
long t1 = System.nanoTime();
c = this.findClass(name);
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
//如果parent加载器中没有找到,
PerfCounter.getFindClasses().increment();
}
}

if (resolve) {
this.resolveClass(c);
}

return c;
}
}

由子类实现

protected Class findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}

BaseDexClassLoader类中findClass方法

protected Class findClass(String name) throws ClassNotFoundException {
List suppressedExceptions = new ArrayList();
// pathList是DexPathList,是具体存放代码的地方。
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException(
"Didn't find class "" + name + "" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}

public Class findClass(String name, List suppressed) {
for (Element element : dexElements) {
Class clazz = element.findClass(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}

if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}

public Class findClass(String name, ClassLoader definingContext,
List suppressed) {
return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed) : null;
}

public Class loadClassBinaryName(String name, ClassLoader loader, List suppressed) {
return defineClass(name, loader, mCookie, this, suppressed);
}

private static Class defineClass(String name, ClassLoader loader, Object cookie,
DexFile dexFile, List suppressed) {
Class result = null;
try {
result = defineClassNative(name, loader, cookie, dexFile);
} catch (NoClassDefFoundError e) {
if (suppressed != null) {
suppressed.add(e);
}
} catch (ClassNotFoundException e) {
if (suppressed != null) {
suppressed.add(e);
}
}
return result;
}

// 调用 Native 层代码
private static native Class defineClassNative(String name, ClassLoader loader, Object cookie, DexFile dexFile)

image.png

本文转自 juejin.cn/post/703847…,如有侵权,请联系删除。


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

收起阅读 »

Android 多线程-IntentService详解

IntentService 一、IntentService概述   上一篇我们聊到了HandlerThread,本篇我们就来看看HandlerThread在IntentService中的应用,看本篇前建议先看看上篇的HandlerThread,有助于我们更好掌...
继续阅读 »

IntentService


一、IntentService概述


  上一篇我们聊到了HandlerThread,本篇我们就来看看HandlerThread在IntentService中的应用,看本篇前建议先看看上篇的HandlerThread,有助于我们更好掌握IntentService。同样地,我们先来看看IntentService的特点:



  • 它本质是一种特殊的Service,继承自Service并且本身就是一个抽象类

  • 它可以用于在后台执行耗时的异步任务,当任务完成后会自动停止

  • 它拥有较高的优先级,不易被系统杀死(继承自Service的缘故),因此比较适合执行一些高优先级的异步任务

  • 它内部通过HandlerThread和Handler实现异步操作

  • 创建IntentService时,只需实现onHandleIntent和构造方法,onHandleIntent为异步方法,可以执行耗时操作


二、IntentService的常规使用套路


  大概了解了IntentService的特点后,我们就来了解一下它的使用方式,先看个案例:

IntentService实现类如下:


package com.zejian.handlerlooper;

import android.app.IntentService;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.IBinder;
import android.os.Message;

import com.zejian.handlerlooper.util.LogUtils;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;

/**
* Created by zejian
* Time 16/9/3.
* Description:
*/
public class MyIntentService extends IntentService {
public static final String DOWNLOAD_URL="download_url";
public static final String INDEX_FLAG="index_flag";
public static UpdateUI updateUI;


public static void setUpdateUI(UpdateUI updateUIInterface){
updateUI=updateUIInterface;
}

public MyIntentService(){
super("MyIntentService");
}

/**
* 实现异步任务的方法
* @param intent Activity传递过来的Intent,数据封装在intent中
*/
@Override
protected void onHandleIntent(Intent intent) {

//在子线程中进行网络请求
Bitmap bitmap=downloadUrlBitmap(intent.getStringExtra(DOWNLOAD_URL));
Message msg1 = new Message();
msg1.what = intent.getIntExtra(INDEX_FLAG,0);
msg1.obj =bitmap;
//通知主线程去更新UI
if(updateUI!=null){
updateUI.updateUI(msg1);
}
//mUIHandler.sendMessageDelayed(msg1,1000);

LogUtils.e("onHandleIntent");
}
//----------------------重写一下方法仅为测试------------------------------------------
@Override
public void onCreate() {
LogUtils.e("onCreate");
super.onCreate();
}

@Override
public void onStart(Intent intent, int startId) {
super.onStart(intent, startId);
LogUtils.e("onStart");
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
LogUtils.e("onStartCommand");
return super.onStartCommand(intent, flags, startId);

}

@Override
public void onDestroy() {
LogUtils.e("onDestroy");
super.onDestroy();
}

@Override
public IBinder onBind(Intent intent) {
LogUtils.e("onBind");
return super.onBind(intent);
}


public interface UpdateUI{
void updateUI(Message message);
}


private Bitmap downloadUrlBitmap(String urlString) {
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;
Bitmap bitmap=null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
bitmap= BitmapFactory.decodeStream(in);
} catch (final IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (in != null) {
in.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return bitmap;
}

}

  通过代码可以看出,我们继承了IntentService,这里有两个方法是必须实现的,一个是构造方法,必须传递一个线程名称的字符串,另外一个就是进行异步处理的方法onHandleIntent(Intent intent) 方法,其参数intent可以附带从activity传递过来的数据。这里我们的案例主要利用onHandleIntent实现异步下载图片,然后通过回调监听的方法把下载完的bitmap放在message中回调给Activity(当然也可以使用广播完成),最后通过Handler去更新UI。下面再来看看Acitvity的代码:


activity_intent_service.xml


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">

<ImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

IntentServiceActivity.java


package com.zejian.handlerlooper.util;

import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.widget.ImageView;

import com.zejian.handlerlooper.MyIntentService;
import com.zejian.handlerlooper.R;

/**
* Created by zejian
* Time 16/9/3.
* Description:
*/
public class IntentServiceActivity extends Activity implements MyIntentService.UpdateUI{
/**
* 图片地址集合
*/
private String url[] = {
"https://img-blog.csdn.net/20160903083245762",
"https://img-blog.csdn.net/20160903083252184",
"https://img-blog.csdn.net/20160903083257871",
"https://img-blog.csdn.net/20160903083257871",
"https://img-blog.csdn.net/20160903083311972",
"https://img-blog.csdn.net/20160903083319668",
"https://img-blog.csdn.net/20160903083326871"
};

private static ImageView imageView;
private static final Handler mUIHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
imageView.setImageBitmap((Bitmap) msg.obj);
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_intent_service);
imageView = (ImageView) findViewById(R.id.image);

Intent intent = new Intent(this,MyIntentService.class);
for (int i=0;i<7;i++) {//循环启动任务
intent.putExtra(MyIntentService.DOWNLOAD_URL,url[i]);
intent.putExtra(MyIntentService.INDEX_FLAG,i);
startService(intent);
}
MyIntentService.setUpdateUI(this);
}

//必须通过Handler去更新,该方法为异步方法,不可更新UI
@Override
public void updateUI(Message message) {
mUIHandler.sendMessageDelayed(message,message.what * 1000);
}
}

  代码比较简单,通过for循环多次去启动IntentService,然后去下载图片,注意即使我们多次启动IntentService,但IntentService的实例只有一个,这跟传统的Service是一样的,最终IntentService会去调用onHandleIntent执行异步任务。这里可能我们还会担心for循环去启动任务,而实例又只有一个,那么任务会不会被覆盖掉呢?其实是不会的,因为IntentService真正执行异步任务的是HandlerThread+Handler,每次启动都会把下载图片的任务添加到依附的消息队列中,最后由HandlerThread+Handler去执行。好~,我们运行一下代码:



每间隔一秒去更新图片,接着我们看一组log:



从Log可以看出onCreate只启动了一次,而onStartCommand和onStart多次启动,这就证实了之前所说的,启动多次,但IntentService的实例只有一个,这跟传统的Service是一样的,最后任务都执行完成后,IntentService自动销毁。以上便是IntentService德使用方式,怎么样,比较简单吧。接着我们就来分析一下IntentService的源码,其实也比较简单只有100多行代码。


三、IntentService源码解析


我们先来看看IntentService的onCreate方法:


@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.

super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}

  当第一启动IntentService时,它的onCreate方法将会被调用,其内部会去创建一个HandlerThread并启动它,接着创建一个ServiceHandler(继承Handler),传入HandlerThread的Looper对象,这样ServiceHandler就变成可以处理异步线程的执行类了(因为Looper对象与HandlerThread绑定,而HandlerThread又是一个异步线程,我们把HandlerThread持有的Looper对象传递给Handler后,ServiceHandler内部就持有异步线程的Looper,自然就可以执行异步任务了),那么IntentService是怎么启动异步任务的呢?其实IntentService启动后还会去调用onStartCommand方法,而onStartCommand方法又会去调用onStart方法,我们看看它们的源码:


@Override
public void onStart(Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}

/**
* You should not override this method for your IntentService. Instead,
* override {@link #onHandleIntent}, which the system calls when the IntentService
* receives a start request.
* @see android.app.Service#onStartCommand
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

  从源码我们可以看出,在onStart方法中,IntentService通过mServiceHandler的sendMessage方法发送了一个消息,这个消息将会发送到HandlerThread中进行处理(因为HandlerThread持有Looper对象,所以其实是Looper从消息队列中取出消息进行处理,然后调用mServiceHandler的handleMessage方法),我们看看ServiceHandler的源码:


private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}

  这里其实也说明onHandleIntent确实是一个异步处理方法(ServiceHandler本身就是一个异步处理的handler类),在onHandleIntent方法执行结束后,IntentService会通过 stopSelf(int startId)方法来尝试停止服务。这里采用stopSelf(int startId)而不是stopSelf()来停止服务,是因为stopSelf()会立即停止服务,而stopSelf(int startId)会等待所有消息都处理完后才终止服务。最后看看onHandleIntent方法的声明:


protected abstract void onHandleIntent(Intent intent);

  到此我们就知道了IntentService的onHandleIntent方法是一个抽象方法,所以我们在创建IntentService时必须实现该方法,通过上面一系列的分析可知,onHandleIntent方法也是一个异步方法。这里要注意的是如果后台任务只有一个的话,onHandleIntent执行完,服务就会销毁,但如果后台任务有多个的话,onHandleIntent执行完最后一个任务时,服务才销毁。最后我们要知道每次执行一个后台任务就必须启动一次IntentService,而IntentService内部则是通过消息的方式发送给HandlerThread的,然后由Handler中的Looper来处理消息,而Looper是按顺序从消息队列中取任务的,也就是说IntentService的后台任务时顺序执行的,当有多个后台任务同时存在时,这些后台任务会按外部调用的顺序排队执行,我们前面的使用案例也很好说明了这点。最后贴一下到IntentService的全部源码,大家再次感受一下:


/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package android.app;

import android.annotation.WorkerThread;
import android.content.Intent;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;

/**
* IntentService is a base class for {@link Service}s that handle asynchronous
* requests (expressed as {@link Intent}s) on demand. Clients send requests
* through {@link android.content.Context#startService(Intent)} calls; the
* service is started as needed, handles each Intent in turn using a worker
* thread, and stops itself when it runs out of work.
*
* <p>This "work queue processor" pattern is commonly used to offload tasks
* from an application's main thread. The IntentService class exists to
* simplify this pattern and take care of the mechanics. To use it, extend
* IntentService and implement {@link #onHandleIntent(Intent)}. IntentService
* will receive the Intents, launch a worker thread, and stop the service as
* appropriate.
*
* <p>All requests are handled on a single worker thread -- they may take as
* long as necessary (and will not block the application's main loop), but
* only one request will be processed at a time.
*
* <div>
* <h3>Developer Guides</h3>
* <p>For a detailed discussion about how to create services, read the
* <a href="{@docRoot}guide/topics/fundamentals/services.html">Services</a> developer guide.</p>
* </div>
*
* @see android.os.AsyncTask
*/
public abstract class IntentService extends Service {
private volatile Looper mServiceLooper;

private volatile ServiceHandler mServiceHandler;
private String mName;
private boolean mRedelivery;

private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}

@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}

/**
* Creates an IntentService. Invoked by your subclass's constructor.
*
* @param name Used to name the worker thread, important only for debugging.
*/
public IntentService(String name) {
super();
mName = name;
}

/**
* Sets intent redelivery preferences. Usually called from the constructor
* with your preferred semantics.
*
* <p>If enabled is true,
* {@link #onStartCommand(Intent, int, int)} will return
* {@link Service#START_REDELIVER_INTENT}, so if this process dies before
* {@link #onHandleIntent(Intent)} returns, the process will be restarted
* and the intent redelivered. If multiple Intents have been sent, only
* the most recent one is guaranteed to be redelivered.
*
* <p>If enabled is false (the default),
* {@link #onStartCommand(Intent, int, int)} will return
* {@link Service#START_NOT_STICKY}, and if the process dies, the Intent
* dies along with it.
*/
public void setIntentRedelivery(boolean enabled) {
mRedelivery = enabled;
}

@Override
public void onCreate() {
// TODO: It would be nice to have an option to hold a partial wakelock
// during processing, and to have a static startService(Context, Intent)
// method that would launch the service & hand off a wakelock.

super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();

mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}

@Override
public void onStart(Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}

/**
* You should not override this method for your IntentService. Instead,
* override {@link #onHandleIntent}, which the system calls when the IntentService
* receives a start request.
* @see android.app.Service#onStartCommand
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}

@Override
public void onDestroy() {
mServiceLooper.quit();
}

/**
* Unless you provide binding for your service, you don't need to implement this
* method, because the default implementation returns null.
* @see android.app.Service#onBind
*/
@Override
public IBinder onBind(Intent intent) {
return null;
}

/**
* This method is invoked on the worker thread with a request to process.
* Only one Intent is processed at a time, but the processing happens on a
* worker thread that runs independently from other application logic.
* So, if this code takes a long time, it will hold up other requests to
* the same IntentService, but it will not hold up anything else.
* When all requests have been handled, the IntentService stops itself,
* so you should not call {@link #stopSelf}.
*
* @param intent The value passed to {@link
* android.content.Context#startService(Intent)}.
*/
@WorkerThread
protected abstract void onHandleIntent(Intent intent);
}

此IntentService的源码就分析完了,嗯,本篇完结。


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

Android onSaveInstanceState/onRestoreInstanceState 原来要这么理解

前些天,有位小伙伴兴匆匆地跑过来给我展示一个现象:Activity 里有个EditText,点击该EditText 输入一些文字。此时,转动手机方向,Activity 变成横屏了,而EditText 上的文字依然保留。 问我:为啥EditText上文字能够恢复...
继续阅读 »

前些天,有位小伙伴兴匆匆地跑过来给我展示一个现象:Activity 里有个EditText,点击该EditText 输入一些文字。此时,转动手机方向,Activity 变成横屏了,而EditText 上的文字依然保留。

问我:为啥EditText上文字能够恢复?

我说:你Activity 配置了横竖屏切换时不重建Activity。

他立马给我展示了:Activity 重建的日志。

我说:系统会在重建Activity 的时候恢复整个ViewTree吧。

他又给我展示了:ImageView 横竖屏时没有恢复之前的图像。

我:...

不服输的我开始了默默地研究,于是有了这篇总结以解心中困惑。

通过本篇文章,你将了解到:



1、onSaveInstanceState/onRestoreInstanceState 作用。

2、onSaveInstanceState/onRestoreInstanceState 原理分析

3、onSaveInstanceState/onRestoreInstanceState 触发场景。

4、onSaveInstanceState/onRestoreInstanceState 为啥不能存放大数据?

5、与Jetpack ViewModel 区别。



1、onSaveInstanceState/onRestoreInstanceState 作用


EditText/ImageView 横竖屏地表现



tt0.top-423136.gif


可以看出,从竖屏到横屏再恢复到竖屏,EditText 内容没有变化。而从竖屏到横屏时,ImageView 内容已经丢失了。

都是系统控件,咱们也没有进行其它的额外区别处理,为啥表现不一致呢?

View.java 里有两个方法:


#View.java
protected Parcelable onSaveInstanceState() {...}

protected void onRestoreInstanceState(Parcelable state){...}

官方注释上写的比较清楚了:



1、onSaveInstanceState 是个钩子方法,View.java 的子类可以重写该方法,在方法里面存储一些子类的内部状态,用以下次重建时恢复。

2、onRestoreInstanceState 也是个钩子方法,用以恢复在onSaveInstanceState 里保存的状态。



既然是View的方法,分别查看EditText 与ImageView 对它们的重写情况:


#TextView.java
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();
...
if (freezesText || hasSelection) {
SavedState ss = new SavedState(superState);

if (freezesText) {
if (mText instanceof Spanned) {
final Spannable sp = new SpannableStringBuilder(mText);
...
ss.text = sp;
} else {
//将TextView 内容存储在SavedState里
ss.text = mText.toString();
}
}
...
return ss;
}

//返回存储的对象
return superState;
}

public void onRestoreInstanceState(Parcelable state) {
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}

SavedState ss = (SavedState) state;
super.onRestoreInstanceState(ss.getSuperState());

if (ss.text != null) {
//取出TextView 内容,并设置
setText(ss.text);
}
...
}

由此可见,TextView 重写这俩方法,先是在onSaveInstanceState 里存储文本内容,再在onRestoreInstanceState 里恢复文本内容。

而通过查看ImageView 发现它并没有重写这俩方法,当然就不能恢复了。其实这也比较容易理解,毕竟对于ImageView,Bitmap 是它的内容,暂存这个Bitmap 很耗内存。


需要注意的是:想要onSaveInstanceState 被调用,则需要给该控件设置id。因为系统是根据View id将状态存储在SparseArray 里


Activity 横竖屏的处理


现在的问题是:谁调用了View 的onSaveInstanceState/onRestoreInstanceState ? 在前一篇分析过Activity 和View的关系:Android Activity 与View 的互动思考

可知,Activity 通过Window 控制View,我们子类继承自EditText,并重写 onSaveInstanceState/onRestoreInstanceState,然后在横竖屏切换时查看这俩方法的调用栈:



image.png


第一个红色框表示EditText子类里的方法(onSaveInstanceState),而第二个红框表示Activity 子类里重写的方法(onSaveInstanceState)。

由此可知,当横竖屏切换时调用了Activity.onSaveInstanceState(xx) 方法。


#Activity.java
protected void onSaveInstanceState(@NonNull Bundle outState) {
//saveHierarchyState 调用整个ViewTree 的onSaveInstanceState 方法
outState.putBundle(WINDOW_HIERARCHY_TAG, mWindow.saveHierarchyState());
...
//告知生命周期回调方法状态已保存
dispatchActivitySaveInstanceState(outState);
}

同样的对于onRestoreInstanceState:


#Activity.java
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
if (mWindow != null) {
Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
if (windowState != null) {
//恢复整个ViewTree 状态
mWindow.restoreHierarchyState(windowState);
}
}
}


当横竖屏切换时,会调用到Activity onSaveInstanceState/onRestoreInstanceState 方法,进而会调用整个ViewTree onSaveInstanceState/onRestoreInstanceState 方法来保存与恢复必要的状态。



Activity 数据保存与恢复


Activity 的onSaveInstanceState/onRestoreInstanceState 方法 除了触发View 的状态保存与恢复外,还可以将Activity 用到的一些重要的数据保存下来,待下次Activity 重建时恢复。

重写两者:


    @Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("say", "hello world");
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
String restore = savedInstanceState.getString("say");
Log.d("fish", restore);
}

此时我们注意到onSaveInstanceState 的入参是Bundle 类型,往outState 写入数据,在onRestoreInstanceState 将数据取出,outState/savedInstanceState 必然不为空。


总结 onSaveInstanceState/onRestoreInstanceState 作用



1、保存与恢复View 的状态。

2、保存与恢复Activity 自定义数据。



2、onSaveInstanceState/onRestoreInstanceState 原理分析。


onSaveInstanceState 调用时机


之前 Android Activity 生命周期详解及监听 有详细分析了Activity 各个阶段的调用情况,现在结合生命周期来分析onSaveInstanceState(xx)在生命周期中的哪个阶段被调用的。

调用栈如下:



image.png


看看上图标黄色的方法,这方法很眼熟,在Activity 生命周期中分析过,它是Activity.onStop()方法的调用者:


#ActivityThread.java
private void callActivityOnStop(ActivityClientRecord r, boolean saveState, String reason) {
// Before P onSaveInstanceState was called before onStop, starting with P it's
// called after. Before Honeycomb state was always saved before onPause.
//这句话翻译过来:
//如果目标设备是Android 9之前,那么onSaveInstanceState 在onStop 之前调用
//如果在Android 9 之后,那么onSaveInstanceState 在onStop 之后调用
//Honeycomb 指的是Android 3.0 现在基本可以忽略了。
//r.activity.mFinished 表示Activity 是否即将被销毁
final boolean shouldSaveState = saveState && !r.activity.mFinished && r.state == null
&& !r.isPreHoneycomb();
final boolean isPreP = r.isPreP();
//Android p 之前先于onStop 之前执行
if (shouldSaveState && isPreP) {
callActivityOnSaveInstanceState(r);
}
try {
//最终执行到Activity.onStop()方法
r.activity.performStop(r.mPreserveWindow, reason);
} catch (SuperNotCalledException e) {
...
}
//标记Stop状态
r.setState(ON_STOP);

if (shouldSaveState && !isPreP) {
//调用onSave 保存
callActivityOnSaveInstanceState(r);
}
}

以上注释比较详细了,小结一下:



1、在Android 9之前,onSaveInstanceState 在onStop 之前调用(至于在onPause 之前还是之后调用,这个时机不确定);在Android 9(包含)之后,onSaveInstanceState 在onStop 之后调用。

2、如果Activity 即将被销毁,则onSaveInstanceState 不会被调用。



对于第二句的理解,举个简单例子:



Activity 在前台时,此时按Home键回到桌面,会执行onSaveInstanceState;若是按back键/主动finish,此时虽然会执行到onStop,但是不会执行onSaveInstanceState。



onRestoreInstanceState 调用时机


现在已经弄清楚onSaveInstanceState 调用时机,接着来分析 onRestoreInstanceState 什么时候执行。

调用栈如下:



image.png


黄色部分的方法也很眼熟,它是Activity.onStart()方法的调用者:


    public void handleStartActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions) {
final Activity activity = r.activity;
...
//最终执行到Activity.onStart()
activity.performStart("handleStartActivity");
r.setState(ON_START);
...
if (pendingActions.shouldRestoreInstanceState()) {
if (r.isPersistable()) {
//从持久化存储里恢复数据
if (r.state != null || r.persistentState != null) {
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
r.persistentState);
}
} else if (r.state != null) {
//从内存里恢复数据
mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
}
}
...
}

小结:



1、onRestoreInstanceState 在onStart()方法之后执行。

2、pendingActions.shouldRestoreInstanceState() 返回值是执行 onRestoreInstanceState()方法的关键,它是在哪赋值的呢?接下来会分析。

3、r.state 不能为空,毕竟没数据无法恢复。



通过以上分析,结合Activity生命周期,onSaveInstanceState /onRestoreInstanceState 调用时机如下:



image.png


onRestoreInstanceState 与onCreate 参数差异


onCreate参数也是Bundle类型,实际上这个参数就是onSaveInstanceState里保存的Bundle,这个Bundle分别传递给了onCreate和onRestoreInstanceState,而onCreate里的Bundle可能为空(新建非重建的情况下),onRestoreInstanceState 里的Bundle必然不为空。

官方注释也说了在onRestoreInstanceState里处理数据的恢复更灵活。


3、onSaveInstanceState/onRestoreInstanceState 触发场景


横竖屏触发的场景


在前面的分析中,与Activity 生命周期关联可能会让人有种印象:

onSaveInstanceState 调用之后onRestoreInstanceState 就会被调用。

而事实并非如此,举个简单例子:

Activity 处在前台时,此时退回到桌面,onSaveInstanceState 会被执行。而后再让Activity 回到前台,onStart()方法执行后,发现onRestoreInstanceState 并没有被调用。



也就是说onSaveInstanceState/onRestoreInstanceState 的调用不一定是成对出现的。



还记得在分析onRestoreInstanceState 遗留了个问题: pendingActions.shouldRestoreInstanceState() 返回值如何确定的 ?

在横竖屏切换时,onRestoreInstanceState 被调用了,说明 pendingActions.shouldRestoreInstanceState() 在横竖屏切换时返回了true,接着来看看其来龙去脉:


#PendingTransactionActions.java
//判断是否需要执行onRestoreInstanceState 方法
public boolean shouldRestoreInstanceState() {
return mRestoreInstanceState;
}

//设置标记
public void setRestoreInstanceState(boolean restoreInstanceState) {
mRestoreInstanceState = restoreInstanceState;
}

只需要找到setRestoreInstanceState()在何处调用即可。

直接说结论:



ActivityThread.handleLaunchActivity() 里设置了setRestoreInstanceState(true)



而handleLaunchActivity()在两种情况下被调用:



image.png


横竖屏时属于重建 Activity,因此onRestoreInstanceState 能被调用。

而从后台返回到前台,并没有新建Activity也没有重建Activity,因此onRestoreInstanceState 不会被调用。

又引申出另一个问题:为啥新建Activity 时onRestoreInstanceState 没被调用?

答案:因为新建Activity 时,ActivityClientRecord 是全新的对象,它所持有的Bundle state 对象为空,因此不会调用到onRestoreInstanceState。


其它配置项更改的场景


除了横竖屏切换时会重建Activity,还有以下配置项更改会重建Activity:



image.png


当然,还有一些不常涉及的配置项,比如所在地区更改等。


重建Activity 的细节



image.png


当需要重建Activity 时,AMS 发出指令,会执行到 ActivityThread.handleRelaunchActivity()方法。


#ActivityThread.java
public void handleRelaunchActivity(ActivityClientRecord tmp,
PendingTransactionActions pendingActions) {
...
//从Map 里获取缓存的ActivityClientRecord
ActivityClientRecord r = mActivities.get(tmp.token);
...
//将ActivityClientRecord 传递下去
handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,
pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");
...
}

mActivities 定义如下:


final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();

以IBinder 为key,存储ActivityClientRecord。

当新建Activity 时,存入ActivityClientRecord,当销毁Activity 时,移除 ActivityClientRecord。


再来分析handleRelaunchActivityInner():


#ActivityThread.java
private void handleRelaunchActivityInner(...) {
...
if (!r.paused) {
//最终执行到onPause
performPauseActivity(r, false, reason, null /* pendingActions */);
}
if (!r.stopped) {
//最终执行到onStop
callActivityOnStop(r, true /* saveState */, reason);
}
//最终执行到onDestroy
handleDestroyActivity(r.token, false, configChanges, true, reason);
//创建新的Activity 实例
handleLaunchActivity(r, pendingActions, customIntent);
}

通过分析Activity 重建的细节,有以下结论:



1、Activity 重建过程中,先将原来的Activity 进行销毁(从onResume--onStop-->onDestroy 的生命周期)。

2、虽然是不同的Activity 对象,但重建时使用的ActivityClientRecord 却是相同的,而ActivityClientRecord 最终是被ActivityThread 持有,它是全局的。这也是 onSaveInstanceState/onRestoreInstanceState 能够存储与恢复数据的本质原因。



当然也可以通过配置告诉系统在配置项变更时不重建Activity:


<activity android:name=".viewmodel.ViewModelActivity" android:configChanges="orientation|screenSize"></activity>

比如以上配置,当横竖屏切换时,不会重建Activity,而配置项的变更会通过 Activity.onConfigurationChanged()方法回调。


4、onSaveInstanceState/onRestoreInstanceState 为啥不能存放大数据?


onSaveInstanceState/onRestoreInstanceState 的参数都是Bundle 类型,思考一下为什么需要定义为Bundle类型呢?

Android IPC 精讲系列 中有提到过,Android 进程间通信方式大多时候使用的是Binder,而要想自定义数据能够通过Binder传输则需要实现Parcelable 接口,Bundle 实现了Parcelable 接口。


由此我们推测,onSaveInstanceState/onRestoreInstanceState 可能涉及到进程间通信,才会用Bundle 来修饰形参。但之前说的ActivityClientRecord是存储在当前进程的啊,貌似和其它进程没有关联呢?

要分析这个问题,实际上只需要在onSaveInstanceState 存储一个比较大的数据,看看报错时的堆栈。


    protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("say", "hello world");
//存储2M 数据
outState.putByteArray("big", new byte[1024*1024*2]);
}

保存2M 的数据,通常来说这是超出了Binder的限制,当调用onSaveInstanceState 时会有报错信息:



image.png


果然还是crash了。

找到 PendingTransactionActions ,它实现了Runnable 接口,在其run方法里:


#PendingTransactionActions.java
public void run() {
try {
//提交给ActivityTaskManagerService 处理,属于进程间通信
//mState 即是onSaveInstanceState 保存的数据
ActivityTaskManager.getService().activityStopped(
mActivity.token, mState, mPersistentState, mDescription);
} catch (RemoteException ex) {
...
}
}

而在ActivityThread.java 里有个方法:


    public void reportStop(PendingTransactionActions pendingActions) {
mH.post(pendingActions.getStopInfo());
}

该方法用于告知系统,咱们的Activity 已经变为Stop状态了,最终会执行到PendingTransactionActions.run()方法。

小结一下:



onSaveInstanceState 存储的数据,在onStop执行后,当前进程需要将Stop状态传递给ATM(ActivityTaskManagerService 运行在system_server进程),因为跨进程传递(Binder)有大小限制,因此onSaveInstanceState 不能传递大量数据。



5、与Jetpack ViewModel 区别


onSaveInstanceState 与 ViewModel 都是将数据放在ActivityClientRecord 的不同字段里。



image.png



1、onSaveInstanceState 用Bundle存储数据便于跨进程传递,而ViewModel 是Object存储数据,不需要跨进程,因此它没有大小限制。

2、onSaveInstanceState 在onStop 之后调用,比较频繁。而ViewModel 存储数据是onDestroy 之后。

3、onSaveInstanceState 可以选择是否持久化数据到文件里(该功能由ATM 实现,存储到xml里),而ViewModel 没有这功能。



更多的区别后续分析 ViewModel 时会提到。


本文基于Android 10.0。


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

Spring Boot + Redis 解决重复提交问题,还有谁不会??

前言 在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下手段: 1、数据库...
继续阅读 »

前言


在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下手段:


1、数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据。


2、token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token。


3、悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)


4、先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。


redis 实现自动幂等的原理图:



搭建 Redis 服务 API


1、首先是搭建redis服务器。


2、引入springboot中到的redis的stater,或者Spring封装的jedis也可以,后面主要用到的api就是它的set方法和exists方法,这里我们使用springboot的封装好的redisTemplate。


推荐一个 Spring Boot 基础教程及实战示例:
github.com/javastacks/…


/**
* redis工具类
*/
@Component
public class RedisService {

@Autowired
private RedisTemplate redisTemplate;

/**
* 写入缓存
* @param key
* @param value
* @return
*/
public boolean set(finalString key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

/**
* 写入缓存设置时效时间
* @param key
* @param value
* @return
*/
public boolean setEx(finalString key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

/**
* 判断缓存中是否有对应的value
* @param key
* @return
*/
public boolean exists(finalString key) {
return redisTemplate.hasKey(key);
}

/**
* 读取缓存
* @param key
* @return
*/
public Objectget(finalString key) {
Object result = null;
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
result = operations.get(key);
return result;
}

/**
* 删除对应的value
* @param key
*/
public boolean remove(finalString key) {
if (exists(key)) {
Boolean delete = redisTemplate.delete(key);
return delete;
}
returnfalse;

}

}

自定义注解 AutoIdempotent


自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,使用元注解ElementType.METHOD表示它只能放在方法上,etentionPolicy.RUNTIME表示它在运行时。


@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {

}

token 创建和检验


token服务接口:我们新建一个接口,创建token服务,里面主要是两个方法,一个用来创建token,一个用来验证token。创建token主要产生的是一个字符串,检验token的话主要是传达request对象,为什么要传request对象呢?主要作用就是获取header里面的token,然后检验,通过抛出的Exception来获取具体的报错信息返回给前端。


publicinterface TokenService {

/**
* 创建token
* @return
*/
public String createToken();

/**
* 检验token
* @param request
* @return
*/
public boolean checkToken(HttpServletRequest request) throws Exception;

}

token的服务实现类:token引用了redis服务,创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,最后返回这个token值。checkToken方法就是从header中获取token到值(如果header中拿不到,就从paramter中获取),如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。


@Service
publicclass TokenServiceImpl implements TokenService {

@Autowired
private RedisService redisService;

/**
* 创建token
*
* @return
*/
@Override
public String createToken() {
String str = RandomUtil.randomUUID();
StrBuilder token = new StrBuilder();
try {
token.append(Constant.Redis.TOKEN_PREFIX).append(str);
redisService.setEx(token.toString(), token.toString(),10000L);
boolean notEmpty = StrUtil.isNotEmpty(token.toString());
if (notEmpty) {
return token.toString();
}
}catch (Exception ex){
ex.printStackTrace();
}
returnnull;
}

/**
* 检验token
*
* @param request
* @return
*/
@Override
public boolean checkToken(HttpServletRequest request) throws Exception {

String token = request.getHeader(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// header中不存在token
token = request.getParameter(Constant.TOKEN_NAME);
if (StrUtil.isBlank(token)) {// parameter中也不存在token
thrownew ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
}
}

if (!redisService.exists(token)) {
thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}

boolean remove = redisService.remove(token);
if (!remove) {
thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
}
returntrue;
}
}

拦截器的配置


web配置类,实现WebMvcConfigurerAdapter,主要作用就是添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中。


@Configuration
publicclass WebConfiguration extends WebMvcConfigurerAdapter {

@Resource
private AutoIdempotentInterceptor autoIdempotentInterceptor;

/**
* 添加拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(autoIdempotentInterceptor);
super.addInterceptors(registry);
}
}

拦截处理器:主要的功能是拦截扫描到AutoIdempotent到注解到方法,然后调用tokenService的checkToken()方法校验token是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端。


/**
* 拦截器
*/
@Component
publicclass AutoIdempotentInterceptor implements HandlerInterceptor {

@Autowired
private TokenService tokenService;

/**
* 预处理
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

if (!(handler instanceof HandlerMethod)) {
returntrue;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
//被ApiIdempotment标记的扫描
AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
if (methodAnnotation != null) {
try {
return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
}catch (Exception ex){
ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
throw ex;
}
}
//必须返回true,否则会被拦截一切请求
returntrue;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

}

/**
* 返回的json值
* @param response
* @param json
* @throws Exception
*/
private void writeReturnJson(HttpServletResponse response, String json) throws Exception{
PrintWriter writer = null;
response.setCharacterEncoding("UTF-8");
response.setContentType("text/html; charset=utf-8");
try {
writer = response.getWriter();
writer.print(json);

} catch (IOException e) {
} finally {
if (writer != null)
writer.close();
}
}

}

测试用例


模拟业务请求类,首先我们需要通过/get/token路径通过getToken()方法去获取具体的token,然后我们调用testIdempotence方法,这个方法上面注解了@AutoIdempotent,拦截器会拦截所有的请求,当判断到处理的方法上面有该注解的时候,就会调用TokenService中的checkToken()方法,如果捕获到异常会将异常抛出调用者,下面我们来模拟请求一下:


@RestController
publicclass BusinessController {

@Resource
private TokenService tokenService;

@Resource
private TestService testService;

@PostMapping("/get/token")
public String getToken(){
String token = tokenService.createToken();
if (StrUtil.isNotEmpty(token)) {
ResultVo resultVo = new ResultVo();
resultVo.setCode(Constant.code_success);
resultVo.setMessage(Constant.SUCCESS);
resultVo.setData(token);
return JSONUtil.toJsonStr(resultVo);
}
return StrUtil.EMPTY;
}

@AutoIdempotent
@PostMapping("/test/Idempotence")
public String testIdempotence() {
String businessResult = testService.testIdempotence();
if (StrUtil.isNotEmpty(businessResult)) {
ResultVo successResult = ResultVo.getSuccessResult(businessResult);
return JSONUtil.toJsonStr(successResult);
}
return StrUtil.EMPTY;
}
}

使用postman请求,首先访问get/token路径获取到具体到token:



利用获取到到token,然后放到具体请求到header中,可以看到第一次请求成功,接着我们请求第二次:



第二次请求,返回到是重复性操作,可见重复性验证通过,再多次请求到时候我们只让其第一次成功,第二次就是失败:



总结


本篇博客介绍了使用springboot和拦截器、redis来优雅的实现接口幂等,对于幂等在实际的开发过程中是十分重要的,因为一个接口可能会被无数的客户端调用,如何保证其不影响后台的业务处理,如何保证其只影响数据一次是非常重要的,它可以防止产生脏数据或者乱数据,也可以减少并发量,实乃十分有益的一件事。而传统的做法是每次判断数据,这种做法不够智能化和自动化,比较麻烦。而今天的这种自动化处理也可以提升程序的伸缩性。


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

FlutterWeb初体验

FlutterWeb初体验 [toc] 背景 因为最近业务需求的变动,在APP的某一部分页面会经常性发生变动,一般情况下来说,这种不稳定的页面不应该由原生来承担,修改发版的成本太大了,最合理的做法是由H5来承担,由原生提供必要的bridge来调用原生方法,但是...
继续阅读 »

FlutterWeb初体验


[toc]


背景


因为最近业务需求的变动,在APP的某一部分页面会经常性发生变动,一般情况下来说,这种不稳定的页面不应该由原生来承担,修改发版的成本太大了,最合理的做法是由H5来承担,由原生提供必要的bridge来调用原生方法,但是由于种种历史债务,还是没有如此实现,经历了痛苦的发版以及等待审核后,我在想flutterWeb是不是可以解决这个问题?


想法


页面进入流程


screenshot-20211210-211936.png


项目架构想法


整个项目转为支持FlutterWeb


整个项目转为flutterweb,可以打包成web文件直接部署在服务器,而app依旧打包成apk和ipa,但是在路由监听处留下开关,当有页面需要紧急修复或者紧急更改的情况下,下发配置,跳转的时候根据路由配置跳转WebView或者原生页面。


抽离出某个模块,单个模块支持web


抽离出一个module,由一个壳工程引用,这个壳工程用于把该module打包成web;同时该模块依然被app工程引用,作为一个功能模块,而部署的时候只部署了这个模块的web产物。


因为目前app集成了一定数量的原生端的第三方sdk,直接支持flutterweb工程量较大,所以先尝试第二个方法。


壳工程结构图

1924616-18f5d8ee85f0f330.png


其中


flutter_libs 是基础的lib库,封装了基础的网络请求,持久化存储,状态管理等基础,壳工程和app工程也会引用


ly_income是功能module,也是我们主要开发需求的模块,它会被壳工程引用作为web的打包内容,也会被app工程引用作为原生的页面展示。


实践


打包问题处理


因为是新建的项目工程,打包成flutterWeb并不会有那么多障碍。


开启web支持


执行 flutter config查看目前的配置信息,如果看到


Settings:
enable-web: true
enable-macos-desktop: true

那就是已经开启了,如果还没,可以使用flutter config --enable-web开启配置


打包模式选择

而flutterWeb打包也有两种模式可以选择:html模式和CanvasKit模式


它们两者各自的特别是:


html模式


flutter build web --web-renderer html


当我们采用html渲染模式时,flutter会采用HTML的custom element,CSS,Canvas和SVG来渲染UI元素


优点是:体积比较小


缺点是:渲染性能比较差,跨端一致性可能不受保障



CanvasKit模式


flutter build web --web-renderer canvaskit


当我们采用canvaskit渲染模式时,flutter将 Skia 编译成 WebAssembly 格式,并使用 WebGL 渲染。应用在移动和桌面端保持一致,有更好的性能,以及降低不同浏览器渲染效果不一致的风险。但是应用的大小会增加大约 2MB。


优点是:跨端一致性受保障,渲染性能更好


缺点是:体积比较大,load页面时间会更久



跨域问题处理


之前一直是做app开发,跨域这个词只听过,还没见识过。


了解跨域

跨域是指浏览器的不执行其他网站脚本的,由于浏览器的同源策略造成,是对JavaScript的一种安全限制


说白点理解,当你通过浏览器向其他服务器发送请求时,不是服务器不响应,而是服务器返回的结果被浏览器限制了。


而什么是同源策略的同源



同源指的是协议、域名、端口 都要保持一致


http://www.123.com:8080/index.html (http协议,http://www.123.com 域名、8080 端口 ,只要这三个有一项不一样的都是跨域,这里不一一举例子)


http://www.123.com:8080/matsh.html(…


http://www.123.com:8081/matsh.html(…


注意:localhost 和127.0.0.1 虽然都指向本机,但也属于跨域。



而跨域的解决方法也暂时不适用我:



  1. JSONP方式 (我们项目的请求都是post请求)

  2. 反向代理,ngixn (ngixn小白)

  3. 配置浏览器 (好像不太适用,应该,大概,也许,可能,或许)

  4. 项目配置跨域 (因为只是尝试项目,需要后台和运维支持的话,需要跨部门沟通,太麻烦了)


摘自网络 什么是跨域,侵删歉


常规做法



  1. 本地调试的时候修改代码,支持跨域请求


    在上图红框中添加代码--disable-web-security




1924616-e444ef62f7776b1e.png


1924616-fddf6a72c3a43965.png


然后删除以下两个文件,执行flutter doctor生成新的一份,再尝试run起来,你会发现浏览器已经支持跨域了,你可以很开心地在浏览器run接口了。但是仅支持本地调试!!!



  1. ngixn做转发,但是这个... 我没有怎么用过ngixn,而且需要在周末做完调研给出可行性报告,也没有时间去学习,先搁置,后续再拿起来看看

  2. 后端和运维同学帮忙调试跨域,因为是尝试而已,没有必要用到其他部门的资源,先搁置,后续如果可实际应用,再要求他们协助。


骚操作


保命前提:



  1. 这个其实就是配置转发的做法,但是这块我没什么经验,时间紧任务重所以就先这么尝试做了

  2. 其实这个就是类似于openfeign之类的想法,但是我并不知道后台开发的FeignClient,而且也有点危险,还是调用开发的接口更加稳妥

  3. 纯个人做法,肯定还会有更好的方法,但是这个是我当时最快的达成方案,勿喷。



如果说我要求不了后台服务做跨域,那可不可以我自己要求我自己做跨域呢?


比如:


我请求我的服务器,我的服务器再去请求后台服务,我访问后台服务跨域而已,我的服务器访问后台服务可不跨域,我的服务器跨域又咋样,自己的东西随便拿捏。



  1. 新建一个springboot项目

  2. 搭建一个controller,参数是url全路径以及参数json字符串,配置好header之后请求后台服务并返回信息


@CrossOrigin
@RestController
@RequestMapping("api/home")
public class GatewayController {

@PostMapping("/gatewayApi")
public String gatewayApi(@RequestParam("url") String url, @RequestParam("params") String json) {
try {
JSONObject jsonObject = JSONObject.parseObject(json);
JSONObject result = doPost(jsonObject, url);
if (result != null) {
return result.toString();
} else {
return errMsg().toString();
}
} catch (Exception e) {
return errMsg(e.getMessage()).toString();
}
}
}


  1. 配置跨域信息


@SpringBootConfiguration
public class WebGlobalConfig {

@Bean
public CorsFilter corsFilter() {

//创建CorsConfiguration对象后添加配置
CorsConfiguration config = new CorsConfiguration();
//设置放行哪些原始域
config.addAllowedOriginPattern("*");
//放行哪些原始请求头部信息
config.addAllowedHeader("*");
//暴露哪些头部信息
config.addExposedHeader("*");
//放行哪些请求方式
config.addAllowedMethod("GET"); //get
config.addAllowedMethod("PUT"); //put
config.addAllowedMethod("POST"); //post
config.addAllowedMethod("DELETE"); //delete
//corsConfig.addAllowedMethod("*"); //放行全部请求

//是否发送Cookie
config.setAllowCredentials(true);

//2. 添加映射路径
UrlBasedCorsConfigurationSource corsConfigurationSource =
new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**", config);
//返回CorsFilter
return new CorsFilter(corsConfigurationSource);
}
}


  1. 打包后部署到服务器

  2. module里的接口不再请求后台服务,而是请求我的服务器,因为只是转发,所以没有改动任何数据结构,只需要请求地址改动下

  3. 可以跨域了


与原生交互问题


设想中web的页面可以有三种方式:



  1. 集成在app里面作为原生页面,这个的交互没什么好说的。

  2. 打包成web项目,通过webview进行加载,那需要额外处理持久化信息的获取与写入,以及与原生页面的跳转交互

  3. 只有url,测试人员可以通过url路径传参之类的切换账号,方便测试


针对业务来说,页面的加载流程应该是这样的:


screenshot-20211210-211914.png


不同场景做不同的操作

原生

通过持久化工具类获取用户基础信息,然后读取接口判断身份,根据身份去做不同展示,点击跳转时间也是直接的通过路由跳转


通过webview加载

通过js交互,从原生模块拿到用户基础信息(存疑,是否直接读接口?,这样避免对原生api的依赖,如果有需求修改的话可以尽量不依赖),然后读取接口判断身份,根据身份不同去做不同展示,如果是dialog之类的交互可以直接实现,如果是跳转页面之类的,可以通过js交互进行原生操作


通过url加载的

通过url的参数串获取到对应的用户id,读取接口获取用户信息,其他操作如上,但是页面没有跳转之类的交互


实现

从链接上面获取参数

比如url为:```xxx.yyy.zzz/value


要如何拿到value值?


因为项目里刚好使用了Get做状态管理,而刚好Get已经实现了这一块,世间上的事情就是这么刚好。(好像navigator2已经支持这个了,不过还没仔细看过)




  1. 配置路由表



    class RouterConf {
    static const String appIncomeArgs = '/app/inCome/:fromApp';
    static const String appIncome = '/app/inCome/';
    static List<GetPage> _getPages = [];
    static List<GetPage> get getPages {
    _getPages = [
    GetPage(name: appIncomeArgs, page: () => const StoreKeeperInComePage()),
    ];
    return _getPages;
    }
    }

    这里appIncome配置了两个路由名


    但是实际使用时以没带**:fromApp为准的,fromApp我觉得可以理解成一个占位符,也就是fromApp=value**




  2. 获取对应的value


    在base类里面定义一个bool值,在init的回调里面去做获取操作


      bool ifFromApp = false;
    Map<String, String?> _args = Get.parameters;
    if (_args.isNotEmpty && _args.containsKey('fromApp')) {
    String? _fromAppFlag = Get.parameters['fromApp'];
    if ((_fromAppFlag?.isNotEmpty ?? false)) {
    ifFromApp = _fromAppFlag == "1";
    }
    }



根据不同情景做操作

以在webview打开为例,在页面加载时通过js交互获取用户信息,拿到用户信息后替换cache类里缓存的id,token之类的,因为拦截器里面会读取这些值用于拼接通用参数


  @override
void onReady() {
if (ifFromApp) {
initUserInfo();
js.context['getUserInfoCallback'] = getUserInfoCallback;
}else{
_loadInterface();
}

super.onReady();
}

void initUserInfo() {
js.context.callMethod("callFlutterMethod", [
json.encode({
"api": "getUserInfo",
"data": {
"name": 'getUserInfo',
"needCallback": true,
"needToken": true,
"callbackName": 'getUserInfoCallback',
"callbackArgs": 'info'
},
})
]);
}

void getUserInfoCallback(msg, info) {
Map<String, dynamic> _args = {};
if (info != null) {
if (info is String) {
_args = jsonDecode(info);
} else {
_args = info;
}
if (_args.containsKey("info")) {
dynamic _realInfo = _args['info'];
if (_realInfo is String) {
_args = jsonDecode(_realInfo);
} else {
_args = _realInfo;
}
}
if (_args.containsKey('name')) {
debugPrint(' _args[name]---------${_args['name']}');
CacheManager.instance.oName = _args['name'];
}
if (_args.containsKey('uId')) {
debugPrint(' _args[uId]---------${_args['uId']}');

CacheManager.instance.userId = _args['uId'];
}
if (_args.containsKey('oId')) {
debugPrint(' _args[oId]---------${_args['oId']}');
CacheManager.instance.userOId = _args['oId'];
}
if (_args.containsKey('token')) {
debugPrint(' _args[token]---------${_args['token']}');

CacheManager.instance.userToken = _args['token'];
}
if (_args.containsKey('headImg')) {
debugPrint(' _args[headImg]---------${_args['headImg']}');
CacheManager.instance.headImgUrl = _args['headImg'];
}
state.userName = CacheManager.instance.oName;
state.userHeaderImg = CacheManager.instance.headImgUrl;
_loadInterface();
}
}

每次都做这个判断是真的恶心,应该把这些东西抽离出来,通过中间件去实现,避免页面上耦合了这个判断。


接下去就是正常的请求接口渲染页面的流程了。


与原生的交互

这里借鉴的是这位大佬的文章 flutterweb与flutter的交互 侵删歉


唯一需要注意的就是在web项目里面增加一个js


1924616-af650f09d9300f88.png
在app里面也要做一点操作:


class NativeBridge implements JavascriptChannel {
BuildContext context; //来源于当前widget, 便于操作UI
Future<WebViewController> _controller; //当前webView 的 controller

NativeBridge(this.context, this._controller);

// api 与具体函数的映射表,可通过 _functions[key](data) 调用函数
get _functions => <String, Function>{
"getUserInfo": _getUserInfo,
"incomeDetail": _incomeDetail,
"incomeHistory": _incomeHistory,
};

@override
String get name =>
"nativeBridge"; // js 通过 nativeBridge.postMessage(msg); 调用flutter

// 处理js请求
@override
get onMessageReceived => (msg) async {
// 将收到的string数据转为json
Map<String, dynamic> message = json.decode(msg.message);
// 异步是因为有些api函数实现可能为异步,如inputText,等待UI相应
// 根据 api 字段,调用具体函数
final data = await _functions[message["api"]](message["data"]);
};

//拿token
_getUserInfo(data) async {
handlerCallback(data);
} //拿token

_incomeDetail(data) async {
Get.toNamed(RouterConf.OLD_STOREKEEPER_INCOME_LIST);
}

_incomeHistory(data) async {
Get.toNamed(RouterConf.STORE_KEEPER_INCOME_HISTORY);
}

handlerCallback(data) async {
LoginModel? _login = await UserManager.getLoginModel();
UserInfoModel? _user = await UserManager.getUserInfo();
String? _name = _user?.resultData?.organization?.organizationName;
String? _uId = _user?.resultData?.user?.userId?.toString() ?? "";
String? _oId =
_user?.resultData?.organization?.organizationId?.toString() ?? "";
String? _token = _login?.resultData?.xAUTHTOKEN;
String? _img = _user?.resultData?.user?.portraitUrl;
_img = ImgSize.getImgUrlThumbnail(_img);
Map<String, dynamic> _infos = {
"name": _name,
"uId": _uId,
"oId": _oId,
"token": _token,
"headImg": _img,
};

if (data['needCallback']) {
var args = data['callbackArgs'];
if (data['needToken']) {
args = "'${data['callbackArgs']}','${jsonEncode(_infos)}'";
}
doCallback(data['callbackName'], args);
}
}

doCallback(name, args) {
_controller.then((value) => value.evaluateJavascript("$name($args)"));
}
}

在webview里面设置channels:


 javascriptChannels: <JavascriptChannel>[
NativeBridge(context, widget.controller!.future)
].toSet(),

结尾


目前来说好像这个方案是可行的,把一个app页面通过网页跑起来确实是挺爽的,但是慢也是真的慢,


也可能因为我的服务器是丐版中的丐版,加载起来是真的慢:


1924616-5860a92dde710996.png


1924616-71b2de0857dcbc1e.png


但是挺好玩的,虽然代码很烂,但是开心就是了。


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

一步一步完成Flutter应用开发-掘金App文章详情, 悬浮,标题动画

这边文章主要将掘金app的文章详情界面的内容构造和效果的实现,也是完结篇,或者有兴趣的同学们可以谈论想要去实现哪个页面的或者功能都可以谈论起来。一起进步。一个人终究没有一群人会走得远。 标题部分 看了一下掘金app文章详情的效果,我的思路是自定义一个appba...
继续阅读 »

这边文章主要将掘金app的文章详情界面的内容构造和效果的实现,也是完结篇,或者有兴趣的同学们可以谈论想要去实现哪个页面的或者功能都可以谈论起来。一起进步。一个人终究没有一群人会走得远。


标题部分


看了一下掘金app文章详情的效果,我的思路是自定义一个appbar然后,左半部分是一个返回按钮,右部分是点击弹出分享的悬浮窗口,中间部分根据内容列表的滑动进行改变,大体思路就是通过pageView构建中间部分,禁止手势滑动,使用主动触发滑动效果,触发机制是内容滑动改变的距离
效果如下:


tutieshi_640x1343_4s.gif


import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

class DetailPage extends StatefulWidget {
DetailPage({Key key}) : super(key: key);

@override
_DetailPageState createState() => _DetailPageState();
}

class _DetailPageState extends State<DetailPage> {
PageController controller = new PageController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Container(
width: Get.width,
height: 80,
decoration: BoxDecoration(color: Colors.white),
padding: EdgeInsets.only(
top: Get.context.mediaQueryPadding.top, left: 10, right: 10),
child: Row(
children: [
InkWell(
child: Icon(
CupertinoIcons.back,
color: Color.fromRGBO(38, 38, 40, 1),
),
onTap: () {
print('点击了返回');
Get.back();
},
),
Expanded(
child: PageView.builder(
itemBuilder: (context, index) {
if (index == 0) {
return Container(
alignment: Alignment.center,
child: Text('一步一步完成Flutter应用开发-掘金文章详情页面'),
);
}
return Container(
child: Row(
children: [
Container(
height: 20,
width: 20,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10)),
)
],
),
);
},
itemCount: 2,
scrollDirection: Axis.vertical,
physics: NeverScrollableScrollPhysics(),
controller: controller,
)),
InkWell(
child: Icon(
Icons.list,
color: Color.fromRGBO(38, 38, 40, 1),
),
onTap: () {
controller.animateTo(40,
duration: Duration(milliseconds: 500),
curve: Curves.ease);
},
),
],
),
)
],
));
}
}

基于这个思想接下来完成下面的内容


内容部分的构建


这部分想要知道掘金内容的返回格式是什么,是markdown内容或者是html内容,如果是html内容传送门,可以参考一下。
效果:


tutieshi_640x1343_5s.gif
这块主要是通过markdown形式展示详情内容引入


flutter_markdown: ^0.5.2

在上述代码中加入详情内容代码


ScrollController _scrollController = new ScrollController();

@override
void initState() {
super.initState();
_scrollController
..addListener(() {
setState(() {
if (_scrollController.offset > 88 && _scrollController.offset < 100) {
controller.animateTo(30,
duration: Duration(milliseconds: 500), curve: Curves.ease);
} else if (_scrollController.offset <= 0) {
controller.animateTo(0,
duration: Duration(milliseconds: 500), curve: Curves.ease);
}
});
});
}

@override
Widget build(BuildContext context) {
...省略上述代码
//_markdownData为内容常量
Expanded(
child: Markdown(
data: _markdownData,
controller: _scrollController,
))
}


悬浮弹窗


使用getX的Get.dialog进行展示悬浮弹窗
效果:


tutieshi_640x1343_4s.gif


代码如下:


renderItem(title) {
return Column(
children: [
Container(
height: 40,
width: 40,
margin: EdgeInsets.only(top: 10),
decoration: BoxDecoration(
color: Colors.red, borderRadius: BorderRadius.circular(20)),
),
Padding(padding: EdgeInsets.only(top: 8)),
Material(
child: Text(title),
)
],
);
}

//调用方法
Get.dialog(
UnconstrainedBox(
alignment: Alignment.bottomCenter,
child: Container(
width: Get.width,
height: Get.height * 0.6,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20))),
child: Column(
children: [
Padding(padding: EdgeInsets.only(top: 40)),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
renderItem('卡片分享'),
renderItem('微信分享'),
renderItem('朋友圈分享'),
renderItem('微博分享'),
],
),
Padding(padding: EdgeInsets.only(top: 20)),
Divider(
height: 2,
color: Colors.grey,
),
Padding(padding: EdgeInsets.only(top: 20)),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
renderItem('卡片分享'),
renderItem('微信分享'),
renderItem('朋友圈分享'),
renderItem('微博分享'),
],
),
Row(
mainAxisAlignment:
MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
renderItem('卡片分享'),
renderItem('微信分享'),
renderItem('朋友圈分享'),
renderItem('微信分享'),
],
),
Expanded(child: Container()),
GestureDetector(
onTap: () {
Get.back();
},
child: Container(
margin: EdgeInsets.only(
bottom: Get
.context.mediaQueryPadding.bottom),
width: Get.width,
height: 40,
alignment: Alignment.center,
child: Material(
child: Text(
'取消',
),
),
),
),
],
)),
),
useRootNavigator: false,
useSafeArea: false);

over ~~~~


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

SwiftUI与Swift的区别

iOS
引言 SwiftUI 于 2019 年度 WWDC 全球开发者大会上发布,它是基于 Swift 建立的声明式框架。该框架可以用于 watchOS、tvOS、macOS、iOS 等平台的应用开发,等于说统一了苹果生态圈的开发工具。 本人最早开始 iOS 开发时选...
继续阅读 »

引言


SwiftUI 于 2019 年度 WWDC 全球开发者大会上发布,它是基于 Swift 建立的声明式框架。该框架可以用于 watchOS、tvOS、macOS、iOS 等平台的应用开发,等于说统一了苹果生态圈的开发工具。


本人最早开始 iOS 开发时选择了 OC(Objective-C,一种编程语言),当时 OC 不但拥有各种知名的第三方库和完善的社区支持,同时 Swift 语言本身都还在不断颠覆性改进中。但当我看了 2020 年 WWDC 关于 SwiftUI 一系列课程之后,便从 Swift 语言的学习开始,逐步了解并掌握 SwiftUI,并果断抛弃了OC,将新项目全部迁移到了 SwiftUI 框架。


SwiftUI 到底有没有苹果宣传的那么理想化?资深的 iOS 开发者是否有必要转型,以及如何转型?SwiftUI 在实际使用的过程中真实体验如何?这些问题就是这篇文章希望探讨的话题。


在最后,我会分享一些自己的学习心得和材料顺序,借开源的精神与大家共同进步。


什么是 SwiftUI


对于 Swift UI,官方的定义很好找到,也写得非常明确:



SwiftUI is a user interface toolkit that lets us design apps in a declarative way.

可以理解为 SwiftUI 就是⼀种描述式的构建 UI 的⽅式。



单单通过描述,大部分人其实很难对抽象的编程方法,和其中的改进有直观的认识。这篇文章也希望通过尽量口语化的叙述,减少专业词汇和代码的出现来降低阅读门槛,让更多人了解计算机科学,了解程序的世界。


下面是我手头正在做的一个项目,定位是一个原生全平台的电子阅读应用,正在使用 SwiftUI 构建用户界面。


为什么苹果要推出 SwiftUI


SwiftUI 的两个组成部分,Swift + UI,即是这个问题的答案。


Swift:编程语言和体验的一致性


Swift 代表苹果推出的一种现代编程语言


很多苹果用户之所以喜欢苹果的产品,其中一个原因,是不同产品之间由内而外的统一感和协调感。这一点在硬件层面的感知是最明显的,从早期开始苹果出的硬件就是「果味十足」的。即使是新品迭代或者是开发全新的品类,也一定会带有烙印很深的「果味」工业设计。


仅仅外观的统一还不够,苹果真正追求的是内外一致,也就是体验的统一。


然而工业设计可以交给自家精英团队,系统可以相互借鉴,但用户使用的软件是由广大的开发者自由创造的。让人意外的是,这一点苹果做的也很不错,与别家相比,质量精良是很多人对苹果系统上软件的印象。


为了实现这一目标苹果做了大量不为普通消费者所感知的工作。


在设计上,苹果提供了一整套不断在更新的 Human Interface Guidelines,详细规定了与视觉相关的各个方面。在完成开发,准备上架分发之前,苹果的审核团队会对每一款应用进行审核,根据 App Store Review Guidelines 的条款判断应用是否允许上架 App Store,即使是知名的应用违反规定也是说下架就下架,绝不含糊。对于不越狱的移动设备而言,App Store 是唯一可以安装应用的途径,控制了其中的准入也就等于替整个平台做了筛选。


除了控制终端以外,苹果也在想方设法增加开发者的数量,提升单个应用质量。方式也非常符合第一性思维原则——降低开发的难度。所以先有了Swift,紧接着又推出了 SwiftUI。苹果希望直接优化语言本身,并统一所有设备的开发体验,让开发者更容易上手,也更容易将心里的想法转化为运行的程序。


虽说在 2015 年推出 Swift2.0 的时候就进行了开源。但这些年 Swift 在后端或是跨平台的发展上并不是非常顺利。提起 Swift,圈内还是会被默认为特指苹果平台内使用的编程语言,地位有些类似 OC 的接班者。其实为了推广这门语言,苹果本身也做了非常多的工作,像是推出SwiftUI,基本就可以看做苹果在推广新语言的过程中一个里程碑式的节点。


SwiftUI 使用了大量 Swift 的语言特性,特别是 5.0 之后新增的特性。Swift 5.1 的很多特性几乎可以说都是为了 SwiftUI 量身定制的,比如 Opaque return types、Property Delegate 和 Function builder 等。


UI:开发的困局


在 SwiftUI 出现之前,苹果不同的设备之前的开发框架并不互通,移动端的⼯程师和桌⾯端的⼯程师需要掌握的知识,有很⼤⼀部分是差异化的。


从 iOS SDK2.0 开始,移动端的开发者⼀直使⽤ UIKit 进⾏⻚⾯部分的开发。UIKit 的思想继承了成熟的 AppKit 和MVC(Model-View-Controller)模式,作出了⼀些改进,但本质上改动不⼤。UI 包括了⽤⼾能看到的⼀切,包括静⽌的显⽰和动态的动画。


再到后来苹果推出了Apple Watch,在这块狭小屏幕上,又引入了一种新的布局方式。这种类似堆叠的逻辑,在某种程度上可以看做 SwiftUI 的未完全体。


截止此时,macOS 的开发需要使用 AppKit,iOS 的开发需要使用 UIKit,WatchOS 的开发需要使用堆叠,这种碎片化的开发体验无疑会大大增加开发者所需消耗的时间精力,也不利于构建跨平台的软件体验。


即使单看 iOS 平台,UIKit 也不是完美的开发⽅案。


UIKit 的基本思想要求 ViewController 承担绝⼤部分职责,它需要协调 model,view 以及⽤⼾交互。这带来了巨⼤的 sideeffect 以及⼤量的状态,如果没有妥善安置,它们将在 ViewController 中混杂在⼀起,同时作⽤于 view 或者逻辑,从⽽使状态管理愈发复杂,最后甚⾄不可维护⽽导致项⽬崩溃。换句话说,在不断增加新的功能和⻚⾯后,同⼀个ViewControlle r会越来越庞杂,很容易在意想不到的地⽅产⽣ bug。⽽且代码纠缠在⼀起后也会⼤⼤降低可读性,伤害到维护和协作的效率。


SwiftUI的特点


在很多地方都能看到 SwiftUI 针对现有问题的一些解决思路,而且现在的编程思想经过不断以来的演化,也一直就软件工程在开发过程中的各种问题在寻找答案。


近年来,随着编程技术和思想的进步,使用声明式或者函数式的方式来进行界面开发,已经越来越被接受并逐渐成为主流。SwiftUI 不是第一个,也不会是最后一个使用声明式界面开发的框架。


声明式的界面开发方式


在计算机科学的世界内,抽象是一个很重要的概念。从底层的二进制逻辑门,到人类可以阅读和理解的编程语言之间,是由很多层的抽象将它们关联起来的。所谓抽象,简单解释就是通过封装组件,将底层细节打包并隐藏起来,从而明确逻辑降低复杂度。就像把晶体管打包成逻辑门,以及软件工程中的函数对象。在软件开发的过程中,工程师只需负责某个具体功能的实现,而其他人则通过开放的 api 使用该功能。


与曾经的布局方式相比,声明式的页面开发无疑又加了一层抽象。


在 UIKit 框架中,界面上的每一个元素都需要开发者进行布置,期间有不少计算工作,例如长宽的改变或是屏幕可视面积的变化等。这种线性的方式被称为指令式 (imperative) 编程。以一行文字为例,放置在哪个坐标、宽度多少、在哪里换行、怎么断句、字形字号是多少、最终高度多少、是否需要缩小字号来完全显示等,这些都是开发者在制作界面时要考虑和计算妥当的问题。到了第二年,用户可能会换更大屏幕的手机,系统支持动态字体调节等新功能,此时原先的程序不进行适配就可能出现显示问题,开发者就需要回头进行程序的重新调试。


换做 SwiftUI 之后,上述的很多变量就被系统接管了。开发者要做的就是直观的告诉系统放置一个图像,上面加一行文字,右边加一个按钮。系统会根据屏幕大小、方向等自动渲染这个界面,开发者也不再需要像素级的进行计算。这被称为声明式 (declarative) 编程


对比同一个场景界面的实现


作为常用的列表视图,在UIKit中被称为 TableView,而在 SwiftUI 中被叫做 List 或 Form。同样是实现一个列表,在 SwiftUI 仅需要声明这里有一个 List 以及每个单元的内容即可。而在UIKit 中,需要使用委托代理的方式定制每个单元的内容,还需要事无巨细的设置行和组的数量、对触摸的反应、点击过程等各方面。


在我的另一个早期项目 Amos 时间志中就可以看到,为了绘制主页就需要几千行代码。


智能适配不同尺寸的屏幕


除了不同尺寸的屏幕,SwiftUI 还能根据运行平台的区别,将按钮、对话框、设置项等渲染成匹配的样式。由于声明的留白是很大的,当开发者不需要事无巨细的安排好每一个细节时,系统可操作的空间也会变大。


可以想象,假如苹果推出新品例如眼镜,或许同样的界面代码会被展示成与 iPhone 中完全不同的样式。


提高了解决问题时所需要着手的层级,这可以让开发者可以将更多的注意力集中到更重要的创意方面。


链式调用修改属性


链式调用是 Swift 语言的一种特性,就是用来使用函数方法的一种方式。可以像链条那样不断地调用函数,中间不需要断开。使用这种方式开发者可以给界面元素添加各种属性,只要愿意,同样能够事无巨细的安排页面元素的各种细节。


除了系统预制的属性可以调节外,开发者也可以进行自定义。例如将不同字体、字号、行间距、颜色等属性统合起来,可以组合成为一个叫「标题」的文字属性。之后凡是需要将某一行文字设置成标题,直接添加这个自定义的属性即可。


使用这种方式进行开发无疑能够极大的避免无意义的重复工作,更快的搭建应用界面框架。


界面元素组件化


理论上来讲,每一个复杂的视图,都是由大量简单的单元视图构成。但是函数方法可以包装起来,做到仅在有需要的时候进行调取使用。在 UIKit 框架下的页面元素解耦却不太容易,一般都是针对某种特定情境,很难进行移植。有时候可能手机横屏就会让页面元素混乱,就更别论页面元素的组件化了。


不过 SwiftUI 在布局上的特点,却可以便捷的拆分复杂的视图组件。单一的组件不仅可以自由组合,而且在苹果的任意平台上都可以使用该组件,达到跨平台的实现。


一般我个人会将视图组件区分为基础组件、布局组件和功能组件。因为 SwiftUI 的界面不再像 UIKit 那样,用 ViewController 承载各种 UIVew 控件,而是一切都是视图。这种视图的拼装方式提高了界面开发的灵活性和复用性。


响应式编程框架 Combine


在构建复杂界面的过程中,数据的流通一直是指令式编程中相当让人头疼的部分。


在 UIKit 框架下时,会配合 Target-Action 或者 protocol-delegate 模式来交换信息,使用 Key-Value Observing (KVO) 或者 Key-Value Coding (KVC) 来监测变化和读写属性。但即便开发者熟练地使用这些工具,面对日益增长的应用复杂性,掉坑里的可能性还是非常大。因为有太多需要开发者妥善处理的数据流动,例如数据改动后需要通知相关的页面进行刷新,或是让关联数据重新计算等。


像是 React Native 和 Flutter 这样的移动端跨平台方案,由于采用了声明式 UI 的编写方式和严格的数据流动方向,就能够大幅减轻开发者的思考负担。


SwiftUI 很明显也吸收了这些现代的编程思想,在另一个重量级系统框架 Combine 的协助下,实现了单一数据源的管理。


响应式编程的核心是将所有事件转化成为异步的数据流,这刚好就是 Combine 的主要功能。Combine 采用观察者模式,对应多个观察者,可以分别订阅感兴趣的内容。在 SwiftUI 的界面布局过程中,不同的 View 就是观察者,分别订阅了相关联的属性,并在数据发生变化之后就能够自动的重新渲染。


单一数据源


在 WWDC 的介绍视频中,「Source of truth」这个词反复出现,中文可以将这个词理解为单一数据源。


一直以来复杂的UI结构都会创造更为复杂的数据和逻辑管理需求,每次在用户交互,或是数据来源发生变化的时候,能否及时更新相关界面组件,不然就会引起显示问题。


但是在 SwiftUI 中,只要在属性声明时加上 @State 等关键词,就可以将该属性和界面元素联系起来,在每次数据改动后,都有机会决定是否更新视图。这样就可以将所有的属性都集中到一起进行管理和计算,也不再需要手写刷新的逻辑。因为在 SwiftUI 中,页面渲染前会将开发者描述的界面状态储存为结构体,更新界面就是将之前状态的结构体销毁,然后生成新的状态。而在绘制界面的过程中,会自动比较视图中各个属性是否有变化,如果发生变化,便会更新对应的视图,避免全局绘制和资源浪费。


使用这种方式,读和写都集中在一处,开发者就能够更好地设计数据结构,比较方便的增减类型和排查问题。而不用再考虑线程、原子状态、寻找最新数据等各种细节,再决定通知相关的界面进行刷新


与UIKit彼此相容


一般开发者学习新技术有一个最大的障碍就是原先的项目怎么办。但 SwiftUI 在这一点上做的相当不错。由于是一个新发布的框架,UI 组件并不齐全,当 SwiftUI 中并没有提供类似的功能时,就可以把 UIKit 中已有的部分进行封装,提供给 SwiftUI 使用。需要做的仅仅是遵循UIViewRepresentable协议即可。相反,在已有的项目中,也可以仅用 SwiftUI 制作一部分的 UI 界面。


当然两种代码的风格是截然不同的,但在使用上却基本没有性能的损失。到最终成品时,用户也无法分辨出两种界面框架的不同。


从开发者的⻆度看 SwiftUI


回到开头的问题:SwiftUI 到底有没有苹果宣传的那么理想化?


在 WWDC 发布 SwiftUI 时,有一句话让我印象深刻:「不论多复杂,原先布局的 99% 现在都可以使用 SwiftUI 进行构建」。当我查询 SwiftUI 是否可以承担大型项目开发时,又一次从资深开发者那里看到了这句话。


在我实际体验一段时间,并最终将一款全 SwiftUI 开发的应用上架后,认为这句话并没有什么问题,但前提是对编程这件事需要有比较基础且深入的理解。


这有点像我们学习一些优秀的第三方库时候的感受,同样是用 Xcode 写代码,有些人写出来就是白开水,而另一些人就是黑魔法。学习同样的语言特性,但由于理解的深刻程度不同,在使用时也会大不一样。仅仅依靠一些标准的自带组件无法做出一款出色的应用,即使如 UIKit 那样拥有如此丰富的组件也不行。很多时候还是要根据业务需要,或者是一些独特的脑洞做出最合适的界面。


对于个⼈开发者而言,意味着什么?


SwiftUI 的上限有多高,还要看未来一年一度的更新。但与之前的 UIKit 相比,下限被大大拉低已是不争的事实。这里的所谓下限,指的是学习的难度。由于描述性的布局方式与我们平时的阅读习惯非常接近,告诉系统在页面中间放一个图片就像告诉别人在桌子中间放一个苹果那么直观。我认识的好多 UI 设计师就通过短时间的自学掌握了 SwiftUI,并且搭建起可以直接在真机上使用的 Demo。


降低学习成本这件事是非常有意义的,不仅可以增加开发者数量,降低学习门槛,而且就学习本身而言,让初学者感受到成就感和明确学习方向,长久而言是比短时间的学习效率更重要的事情。只有开始的时候培养足够的兴趣,在后期才可能自主自发的研究更深入更困难的问题。


SwiftUI 和 Combine 大量借助了 Swift 的语法特性,尤其是 5.0 之后的几个更新,新特性就仿佛是为了这两个系统及框架量身定做的一般。虽然将 Swift 开源,但苹果无疑还是牢牢地把握着这门语言的发展。这两个框架和编程语言之间的配合默契,也仿佛让开发者体会到了软硬件一体带来的发展潜力。无论 Swift 出圈后的成果能有多少,在苹果的体系内,无疑是能够将各种消费层面的软件体验整合统一。


只要使用 SwiftUI,系统会默认支持白天和黑夜模式的自动切换;在各种尺寸的屏幕间自动适配;为任意控件添加 Haptic Touch 或是动画;在 Apple Watch 上带来独立而完整的体验;将iOS 的应用转换为 macOS 的原生应用,会以最快的速度支持第一方的各种新特性。这种对苹果硬件的深入支持是那些跨平台方案无论如何无法实现的。可以看一些采用第三方框架的知名应用,像是横屏、黑夜模式、小组件等基础的特性,到现在都迟迟没有适配。


所以 SwiftUI 对小工作室或是独立开发者来说是件好事,可以让新的想法快速落地并且接受市场验证,真正的做到敏捷开发。以这种方式在市场中的细分领域获得一席之地,也能让更多人体会到编写程序的感受,甚至是创造财富。


开源我的学习心得


在最后的部分我会分享一些自己学习 SwiftUI 的过程和介绍相关的资源,给一些也对开发感兴趣的小伙伴们做个参考。


首先要学习的是 Swift 编程语言,它与 OC 之间的差别还是挺大的,学习也没有什么捷径,直接阅读官方教程,对照着实例自己写一遍就行。国内有几个非常好的汉化网站,可以一起对照学习。基本上没有必要特意买书,反而不如直接电脑上看了就敲来的方便。



  1. 官网

  2. SwiftGG

  3. GitHub 汉化库


对语言有了大概的了解后,就可以开始对 SwiftUI 的学习,假如遇到问题可以反复回去查看之前的资料。很多被忽略的细节,或是当时初看没概念的部分,结合具体的案例就能够有比较透彻的理解。



  1. 斯坦福公开课 CS193P·2020 年春:该课程强推,我当年学习 OC 看的就是它,现在到SwiftUI了还是先看这个,系统且细致,结合案例和编程过程中的小技巧介绍,是很好的入门课程。

  2. 苹果官方 SwiftUI 课程:打开Xcode,照着官方的教学,从头到尾学着做一遍应用。

  3. Hacking with swift:这是国外一个程序员用业余时间搭建的分享网站,有大量的文章可以阅读,还有推荐初学者跟着做的「100 Days of SwiftUI」课程。

  4. 苹果官方文档:文档是必读的,虽然很多文档缺乏工程细节,但是文档涉及很多概念性的内容,你可以知道官方是怎么思考的,并且有很多具体的机制参数。我本人有一个习惯,要是工程涉及某个框架,会把相关的文档都翻译一遍。

  5. Stack Overflow:有问题查询专用,在谷歌中搜索错误代码或者关键词基本都会由该网站给答案。

  6. 阅读 SwiftUI 库的源代码。


基本到此假如能够顺利完成下来,就可以开启自己的项目。开发想要提高的关键就是亲自写代码和不断地阅读学习。初期学习的关键能力就是英语,而到后期需要的就是真正的兴趣和一些数学能力。


作者:洋仔
链接:https://juejin.cn/post/6997313521067229214
收起阅读 »

Swift:基石库——R.swift

iOS
这是我参与更文挑战的第4天,活动详情查看: 更文挑战何为基石库?做一个App无外乎两大要素:获取数据通过数据驱动页面也许你的App没有网络请求或者网络请求少,你可以不需要Alamofire。也许你的App的UI不是特别复杂,简单的xib和storyb...
继续阅读 »

这是我参与更文挑战的第4天,活动详情查看: 更文挑战

何为基石库?

做一个App无外乎两大要素:

  • 获取数据

  • 通过数据驱动页面

也许你的App没有网络请求或者网络请求少,你可以不需要Alamofire。

也许你的App的UI不是特别复杂,简单的xib和storyboard就可以胜任。

但是在当下一个App中,图片资源、字符串资源等,作为一个App开发者,你是不得不用的。

举个栗子,传统的获取一个image资源我们都是这么写:

let image = UImage(named: "saber")

这么写的最大弊端就是saber这是一个字符串硬编码,靠的的是纯手工敲打,一旦出错,界面就会出现异常。

在开发中,需要尽量避免这种硬编码,如何高效将这种硬编码的表达方式更换为高效安全的方式,就由本次的主角出场了--R.swift

统和所有的资源,以现代化的方式引用资源,项目中使用它,虽然不会让你的App上升一个层次,不过却给你的编码极度舒适。

let image = R.image.saber()

同样是Ex咖喱棒,味道却完全不同,哈哈。

基石库就是那些你无法避免不得不用的库,而R.swift恰恰就是。

R.swift

何为R,即Resource的缩写,我们先看看官方给出的一些例子:

使用R.swift函数前:

let icon = UIImage(named: "settings-icon")
let font = UIFont(name: "San Francisco", size: 42)
let color = UIColor(named: "indicator highlight")
let viewController = CustomViewController(nibName: "CustomView", bundle: nil)
let string = String(format: NSLocalizedString("welcome.withName", comment: ""), locale: NSLocale.current, "Arthur Dent")

使用R.swift函数后:

let icon = R.image.settingsIcon()
let font = R.font.sanFrancisco(size: 42)
let color = R.color.indicatorHighlight()
let viewController = CustomViewController(nib: R.nib.customView)
let string = R.string.localizable.welcomeWithName("Arthur Dent")

所有的资源都函数化后,编写过程想出错都难,特别需要注意的是最后一个涉及国际化的函数R.string.localizable.welcomeWithName("Arthur Dent"),Arthur Dent这个字符串需要自己具体制定,可以通过在做国际化时,通过info.strings进行处理。

R.swift目前支持下面这些资源文件管理:

  • Images
  • Fonts
  • Resource files
  • Colors
  • Localized strings
  • Storyboards
  • Segues
  • Nibs
  • Reusable cells

基本上覆盖了绝大多数的App中的资源管理。

安装和使用

安装

R.swift使用其他特别舒服,不过它的安装确实比其他的第三方库稍微麻烦一点,正所谓工欲善其事必先利其器,这一点麻烦是值得的。

1.添加'R.swift' 在工程的Podfile文件中,并运行pod install。 2.如下图所示。添加脚本:

image.png

3.如下图所示,移动脚本的位置,让它在Compile Sources phase和Check Pods Manifest.lock之间:

image.png

4.添加脚本:

image.png

在shell,下面这一栏添加"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/R.generated.swift"

在input Files通过+号添加$TEMP_DIR/rswift-lastrun

在Output Files通过+号添加$SRCROOT/R.generated.swift

5.运行添加R.generated.swift:

完成第4步后,进行command + B编译,然后在工程的根目录下面会找到R.generated.swift文件: image.png

将这个文件拖入到工程中,并且不要勾选Copy items if needed

image.png

这样,R.swift就安装完成啦。

使用

每一次添加了新的资源文件,就运行一次command + B一次,这样R.generated.swift文件就将新加入的资源文件更新,使用使用的时候只用通过R.来进行引用了。

更多用法,参考上面写的例子,以及官方文档

明天周末怎么破?

最怕周末更文,因为作为一个奶爸,休息都不是自己的,我争取做到不水文,至少讲一些知识点,明日继续,大家加油。


收起阅读 »

Swift:解包的正确姿势

iOS
嗯,先来一段感慨 在掘金里面看见iOS各路大神各种底层与runtime,看得就算工作了好几年的我也一脸蒙圈,于是只好从简单的入手。 文章最初发布在简书上面,有段时间了,考虑以后大部分时间都会在掘金学习,于是把文章搬过来了。稍微做了点润色与排版。 对于Swift...
继续阅读 »

嗯,先来一段感慨


在掘金里面看见iOS各路大神各种底层与runtime,看得就算工作了好几年的我也一脸蒙圈,于是只好从简单的入手。


文章最初发布在简书上面,有段时间了,考虑以后大部分时间都会在掘金学习,于是把文章搬过来了。稍微做了点润色与排版。


对于Swift学习而言,可选类型Optional是永远绕不过的坎,特别是从OC刚刚转Swift的时候,可能就会被代码行间的?与!,有的时候甚至是??搞得稀里糊涂的。


这篇文章会给各位带来我对于可选类型的一些认识以及如何进行解包,其中会涉及到Swift中if let以及guard let的使用以及思考,还有涉及OC部分的nullablenonnull两个关键字,以及一点点对两种语言的思考。


var num: Int? 它是什么类型?


在进行解包前,我们先来理解一个概念,这样可能更有利于对于解包。


首先我们来看看这样一段代码:



var num: Int?

num = 10

if num is Optional<Int> {

print("它是Optional类型")

}else {

print("它是Int类型")

}



请先暂时不要把这段代码复制到Xcode中,先自问自答,num是什么类型,是Int类型吗?


好了,你可以将这段代码复制到Xcode里去了,然后在Xcode中的if上一定会出现这样一段话:



'is' test is always true



num不是Int类,它是Optional类型


那么Optional类型是啥呢--可选类型,具体Optional是啥,Optional类型的本质实际上就是一个带有泛型参数的enum类型,各位去源码中仔细看看就能了解到,这个类型和Swift中的Result类有异曲同工之妙。


var num: Int?这是一个人Optional的声明,意思不是“我声明了一个Optional的Int值”,而是“我声明了一个Optional类型,它可能包含一个Int值,也可能什么都不包含”,也就是说实际上我们声明的是Optional类型,而不是声明了一个Int类型!


至于像Int!或者Int?这种写法,只是一种Optional类型的糖语法写法。


以此类推String?是什么类型,泛型T?是什么类型,答案各位心中已经明了吧。


正是因为num是一个可选类型。所以它才能赋值为nil, var num: Int = nil。这样是不可能赋值成功的。因为Int类型中没有nil这个概念!


这就是Swift与OC一个很大区别,在OC中我们的对象都可以赋值为nil,而在Swift中,能赋值为nil只有Optional类型!


解包的基本思路,使用if let或者guard let,而非强制解包


我们先来看一个简单的需求,虽然这个需求在实际开发中意义不太大:


我们需要从网络请求获取到的一个人的身高(cm为单位)以除以100倍,以获取m为单位的结果然后将其结果进行返回。


设计思路:


由于实际网络请求中,后台可能会返回我们的身高为空(即nil),所以在转模型的时候我们不能定义Float类型,而是定义Float?便于接受数据。


如果身高为nil,那么nil除以100是没有意义的,在编译器中Float?除以100会直接报错,那么其返回值也应该为nil,所以函数的返回值也是Float?类型


那么函数应该设计成为这个样子是这样的:



func getHeight(_ height: Float?) -> Float?



如果一般解包的话,我们的函数实现大概会写成这样:



func getHeight(_ height: Float?) -> Float? {

if height != nil {

return height! / 100

}

return nil

}



使用!进行强制解包,然后进行运算。


我想说的是使用强制解包固然没有错,不过如果在实际开发中这个height参数可能还要其他用途,那么是不是每使用一次都要进行强制解包?


强制解包是一种很危险的行为,一旦解包失败,就有崩溃的可能,也许你会说这不是有if判断,然而实际开发中,情况往往比想的复杂的多。所以安全的解包行为应该是通过if let 或者guard let来进行。



func getHeight(_ height: Float?) -> Float? {

if let unwrapedHeight = height {

return unwrapedHeight / 100

}

return nil

}



或者:



func getHeight(_ height: Float?) -> Float? {

guard let unwrapedHeight = height else {

return nil

}

return unwrapedHeight / 100

}



那么if let和guard let 你更倾向使用哪个呢?


在本例子中,其实感觉二者的差别不大,不过我个人更倾向于使用guard let。




原因如下:


在使用if let的时候其大括号类中的情况才是正常情况,而外部主体是非正常情况的返回的nil;


而在使用guard let的时候,guard let else中的大括号是异常情况,而外部主体返回的是正常情况。


对于一个以返回结果为目的的函数,函数主体展示正常返回值,而将异常抛出在判断中,这样不仅逻辑更清晰,而且更加易于代码阅读。




解包深入


有这么一个需求,从本地路径获取一个json文件,最终将其转为字典,准备进行转模型操作。


在这个过程中我们大概有这么几个步骤:


1. 获取本地路径 


func path(forResource name: String?, ofType ext: String?) -> String?


2. 将本地路径读取转为Data 


init(contentsOf url: URL, options: Data.ReadingOptions = default) throws


3. JSON序列化


class func jsonObject(with data: Data, options opt: JSONSerialization.ReadingOptions = []) throws -> Any


4. 是否可以转为字典类型


我们可以看到以上几个函数中,获取路径获取返回的路径结果是一个可选类型而转Data的方法是抛出异常,JSON序列化也是抛出异常,至于最后一步的类型转换是使用as? [Sting: Any]这样的操作


这个函数我是这来进行设计与步骤分解的:


函数的返回类型为可选类型,因为下面的4步中都有可能失败进而返回nil。


虽然有人会说第一步获取本地路径,一定是本地有的才会进行读取操作,但是作为一个严谨操作,凡事和字符串打交道的书写都是有隐患的,所以我这里还是用了guard let进行守护。


这个函数看起来很不简洁,每一个guard let 后面都跟着一个异常返回,甚至不如使用if let看着简洁


但是这么写的好处是:在调试过程中你可以明确的知道自己哪一步出错



func getDictFromLocal() -> [String: Any]? {

/// 1 获取路径

guard let path = Bundle.main.path(forResource: "test", ofType:"json") else {

return nil

}

/// 2 获取json文件里面的内容

guard let jsonData = try? Data.init(contentsOf: URL.init(fileURLWithPath: path)) else {

return nil

}

/// 3 解析json内容

guard let json = try? JSONSerialization.jsonObject(with: jsonData, options:[]) else {

return nil

}

/// 4 将Any转为Dict

guard let dict = json as? [String: Any] else {

return nil

}

return dict

}



当然,如果你要追求简洁,这么写也未尝不可,一波流带走



func getDictFromLocal() -> [String: Any]? {

guard let path = Bundle.main.path(forResource: "test", ofType:"json"),

let jsonData = try? Data.init(contentsOf: URL.init(fileURLWithPath: path)),

let json = try? JSONSerialization.jsonObject(with: jsonData, options:[]),

let dict = json as? [String: Any] else {

return nil

}

return dict

}



guard let与if let不仅可以判断一个值的解包,而且可以进行连续操作


像下面这种写法,更加追求的是结果,对于一般的调试与学习,多几个guard let进行拆分,未尝不是好事。


至于哪种用法更适合,因人而异。


可选链的解包


至于可选链的解包是完全可以一步到位,假设我们有以下这个模型。



class Person {

var phone: Phone?

}

class Phone {

var number: String?

}



Person类中有一个手机对象属性,手机类中有个手机号属性,现在我们有位小明同学,我们想知道他的手机号。


小明他不一定有手机,可能有手机而手机并没有上手机号码。



let xiaoming = Person()

guard let number = xiaoming.phone?.number else {

return

}

print(number)



这里只是抛砖引玉,更长的可选链也可以一步到位,而不必一层层进行判断,因为可选链中一旦有某个链为nil,那么就会返回nil。


nullable和nonnull


我们先来看这两个函数,PHImageManager在OC与Swift中通过PHAsset实例获取图片的例子



[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:size contentMode:PHImageContentModeDefault options:options resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {

//、 非空才进行操作 注意_Nullable,Swift中即为nil,注意判断

if (result) {

}

}];




PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .default, options: options, resultHandler: { (result: UIImage?, info: [AnyHashable : Any]?) in

guard let image = result else { return }

})



在Swift中闭包返回的是两个可选类型,result: UIImage?与info: [AnyHashable : Any]? 


而在OC中返回的类型是 UIImage * _Nullable result, NSDictionary * _Nullable info


注意观察OC中返回的类型UIImage * 后面使用了_Nullable来修饰,至于Nullable这个单词是什么意思,我想稍微有点英文基础的应该一看就懂--"可以为空",这不恰恰和Swift的可选类型呼应吗?


另外还有PHFetchResult遍历这个函数,我们再来看看在OC与Swift中的表达



PHFetchResult *fetchResult;

[fetchResult enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

}];




let fetchResult: PHFetchResult

fetchResult.enumerateObjects({ (obj, index, stop) in

})



看见OC中Block中的回调使用了Nonnull来修饰,即不可能为空,不可能为nil,一定有值,对于使用这样的字符修饰的对象,我们就不必为其做健壮性判断了。


这也就是nullable与nonnull两个关键字出现的原因吧--与Swift做桥接使用以及显式的提醒对象的状态


一点点Swift与OC的语言思考


我之前写过一篇文章,是说有关于一个字符串拼接函数的


从Swift来反思OC的语法


OC函数是这样的:



- (NSString *)stringByAppendingString:(NSString *)aString;



Swift中函数是这样的:



public mutating func append(_ other: String)



仅从API来看,OC的入参是很危险的,因为类型是NSString *


那么nil也可以传入其中,而传入nil的后果就是崩掉,我觉得对于这种传入参数为nil会崩掉的函数需要特别提醒一下,应该写成这样:



- (NSString *)stringByAppendingString:(NSString * _Nonnull)aString;

/// 或者下面这样

- (NSString *)stringByAppendingString:(nonnull NSString *)aString;



以便告诉程序员,入参不能为空,不能为空,不能为空,重要的事情说三遍!!!


反观Swift就不会出现这种情况,other后面的类型为String,而不是String?,说明入参是一个非可选类型。


基于以上对于代码的严谨性,所以我才更喜欢使用Swift进行编程。


当然,Swift的严谨使得它失去部分的灵活性,OC在灵活性上比Swift卓越。


作者:season_zhu
链接:https://juejin.cn/post/6931154052776460302

收起阅读 »

iOS 无感知上拉

iOS
本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!RxSwift编写wanandroid客户端现已开源目前RxSwift编写wanandroid客户端已经开源了——项目链接。记得给个star喔!附上一张效果图片:本篇文章是从6月更...
继续阅读 »

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

RxSwift编写wanandroid客户端现已开源

目前RxSwift编写wanandroid客户端已经开源了——项目链接。记得给个star喔!

附上一张效果图片:

RPReplay_Final1625472730.2021-07-05 16_13_58.gif

本篇文章是从6月更文中热心网友的留言中进行的开发与探索:

Snip20210709_1.png

6月确实因为日更的原因,这个功能没有实现,趁着7月的时候,解决了。

废话了这么多,那么我们进入主题吧。

什么是无感知上拉加载更多

什么是无感知,这个这样理解:在网络情况正常的情况下,用户对列表进行连续的上拉时,该列表可以无卡顿不停出现新的数据。

如果要体验话,Web端很多已经做到了,比如掘金的首页,还有比如掘金iOS的App,列表都是无感知上拉加载更多。

说来惭愧,写了这久的代码,还真的没有认真思考这个功能怎么实现。

如何实现无感知上拉加载更多

我在看见这位网友留言的时候,就开始思考了。

在我看来,有下面几个着手点:

  • 列表滑动时候的是如何知道具体滑动的位置以触发接口请求,添加更多数据?

  • 从UIScrollView的代理回调中去找和scrollView的位置(contentOffset)大小(contentSize)关系密切的回调。

  • 网络上有没有比较成熟的思路?

顺着这条线,我先跑去看了UIScrollViewDelegate的源码:

public protocol UIScrollViewDelegate : NSObjectProtocol {


@available(iOS 2.0, *)
optional func scrollViewDidScroll(_ scrollView: UIScrollView) // any offset changes

@available(iOS 3.2, *)
optional func scrollViewDidZoom(_ scrollView: UIScrollView) // any zoom scale changes

.
.
.
.
.
.
/// 代码很多,这里就不放上来,给大家压力了。
}

直接上结论吧:看了一圈,反正没有和contentSize或者位置相关的回调代理。scrollViewDidScroll这个回调里面虽然可以回参scrollView,但是对于我们需要的信息还不够具体。

思考:既然UIScrollViewDelegate的代理没有现成的代理回调,自己使用KVO去监听试试?

网上的思路(一)

就在我思考的同时,我也在网络上需求实现这个功能的答案,让后看到这样一个思路:

实现方法很简单,需要用到tableView的一个代理方法,就可轻松实现。- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath就是这个方法,自定义显示cell。这个方法不太常用。但是这个方法可在每个cell将要第一次出现的时候触发。然后我们可设置当前页面第几个cell将要出现时,触发请求加载更多数据。

我看了之后,心想着,多写一个TableView的代理,总比写KVO的代码少,先试试再说,于是代码撸起:

extension SwiftCoinRankListController: UITableViewDelegate {

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let row = indexPath.row
let distance = dataSource.count - 25
print("row: \(row), distance:\(distance) ")
if row == distance {
loadMore()
}
}
}

本代码可以在开源项目中的SwiftCoinRankListController.swift文件查看具体的逻辑,其主要就是通过cell显示的个数去提前请求加载数据,然后我们看看效果:

620A94AE4920C54C6E1B85E1776AC83C.2021-07-09 17_47_45.gif

Gif可能看起来还好,我说我调试的感受:

虽然做到了上拉无感知,但是当手滑的速度比较快的时候,到底了新的数据没有回来,就会在底部等一段时间。

功能达到了,但是感受却不理想,果然还是监听的细腻程度不够。

网上的思路(二)

然后在继续的搜索中,我看到了另外一个方案:

很多时候我们上拉刷新需要提前加载新数据,这时候利用MJRefreshAutoFooter的属性triggerAutomaticallyRefreshPercent就可以实现,该属性triggerAutomaticallyRefreshPercent默认值为1,然后改成0的话划到底部就会自动刷新,改成-1的话,在快划到底部44px的时候就会自动刷新。

MJRefresh?使用MJRefreshAutoFooter,这个简单,我直接把基类的footer给替换掉就可以了,本代码可以在开源项目中的BaseTableViewController.swift文件查看:

/// 设置尾部刷新控件,更新为无感知加载更多
let footer = MJRefreshAutoFooter()
footer.triggerAutomaticallyRefreshPercent = -1
tableView.mj_footer = footer

再来看看效果:

992BC78FBAC7B8CB36A6DC679897DA21.2021-07-09 18_04_09.gif

直接说感受:

代码改动性少,编写简单,达到预期效果,爽歪歪。比方案一更丝滑,体验好。

到此,功能就实现,难道就完了?

当然,不会,我们去看看源码吧。

MJRefresh代码的追根朔源

首先我们看看MJRefreshAutoFooter.h文件:

image.png

这里有个专门的属性triggerAutomaticallyRefreshPercent去做自动刷新,那么我们去MJRefreshAutoFooter.m中去看看吧:

image.png

注意看喔,这个.m文件有一个- (void)scrollViewContentOffsetDidChange:(NSDictionary *)change方法,并且还调用了super,从这个方法名中我们可以明显的得到当scrollView的contentOffset变化的时候进行回调的监听。,我们顺藤摸瓜,看看super是什么,会不会有新的发现:

image.png

稍微跟着一下源代码,MJRefreshAutoFooter的继承关系如下:

MJRefreshAutoFooter => MJRefreshFooter => MJRefreshComponent

所以这个super的调用我们就去MJRefreshComponent.m里面去看看吧:

image.png

通过上面的截图我们可以得到下面的一些信息与结论:

  • MJRefreshComponent是通过KVO去监听scrollView的contentOffset变化,思路上我们对齐一致了。

  • 该类并没有实现其具体方法,而是将其交由其子类去实现,这一点通过看MJRefreshComponent.h的注释可以得到:

image.png

  • MJRefreshComponent从本质上更像虚基类。

总结

如果不是掘友提出这个问题,我可能都不会太仔细的去研究这个功能,也许继续普普通通的使用一般的上拉加载更多就够了。

这次的实践,其实是从思路到寻找方法,最后再到源码阅读。

思路也许不困难,但是真正一点点实现并完善功能,每一步都并不容易,这次我也仅仅是继续使用了MJRefresh这个轮子。

想起有一天,在群里吹水看见的一张图:

云程序员来了.jpeg

灵魂拷问,直击人心,大部分时间我们不也是云程序员呢?

知行合一方能开拓新的天地。


收起阅读 »

JVM整体结构

JVM结构图类加载子系统加载连接初始化使用卸载运行时数据区域栈帧数据结构动态链接内存管理Java内存区域从逻辑上分为堆区和非堆区,Java8以前,非堆区又称为永久区,Java8以后统一为原数据区,堆区按照分代模型分为新生代和老年代,其中新生代分为Eden、so...
继续阅读 »

Java虚拟机主要负责自动内存管理、类加载与执行、主要包括执行引擎、垃圾回收器、PC寄存器、方法区、堆区、直接内存、Java虚拟机栈、本地方法栈、及类加载子系统几个部分,其中方法区与Java堆区由所有线程共享、Java虚拟机栈、本地方法栈、PC寄存器线程私有,宏观的结构如下图所示:

JVM结构图

类加载子系统

从文件或网络中加载Class信息,类信息存放于方法区,类的加载包括加载->验证->准备->解析->初始化->使用->卸载几个阶段,详细流程后续文章会介绍。

  • 加载

从文件或网络中读取类的二进制数据、将字节流表示的静态存储结构转换为方法区运行时数据结构、并于堆中生成Java对象实例,类加载器既可以使用系统提供的加载器(默认),也可以自定义类加载器。

  • 连接

连接分为验证、准备、解析3个阶段,验证阶段确保类加载的正确性、准备阶段为类的静态变量分配内存,并将其初始化为默认值、解析阶段将类中的符号引用转换为直接引用

  • 初始化

初始化阶段负责类的初始化,Java中类变量初始化的方式有2种,声明类变量时指定初始值、静态代码块指定初始化,只有类被主动使用时才会触发类的初始化,类的初始化会先初始化父类,然后再初始化子类。

  • 使用

类访问方法区内的数据结构的接口,对象是堆区的数据

  • 卸载

程序执行了System.exit()、程序正常执行结束、JVM进程异常终止等

运行时数据区域

程序从静态的源代码到编译成为JVM执行引擎可执行的字节码,会经历类的加载过程,并于内存空间开辟逻辑上的运行时数据区域,便于JVM管理,其中各数据区域如下,其中垃圾回收JVM会自动自行管理

栈帧数据结构

Java中方法的执行在虚拟机栈中执行,为每个线程所私有,每次方法的调用和返回对应栈帧的压栈和出栈,其中栈帧中保存着局部变量表、方法返回地址、操作数栈及动态链接信息。

动态链接

Java中方法执行过程中,栈帧保存方法的符号引用,通过动态链接,将解析为符号引用。

内存管理

  • 内存划分(逻辑上)

Java内存区域从逻辑上分为堆区和非堆区,Java8以前,非堆区又称为永久区,Java8以后统一为原数据区,堆区按照分代模型分为新生代和老年代,其中新生代分为Eden、so、s1,so和s1是大小相同的2块区域,生产环境可以根据具体的场景调整虚拟机内存分配比例参数,达到性能调优的效果。

堆区是JVM管理的最大内存区域,由所有线程共享,采用分代模型,堆区主要用于存放对象实例,堆可以是物理上不连续的空间,逻辑上连续即可,其中堆内存大小可以通过虚拟机参数-Xmx、-Xms指定,当堆无法继续扩展时,将抛出OOM异常。

  • 运行时实例

假设存在如下的SimpleHeap测试类,则SimpleHeap在内存中的堆区、Java栈、方法区对应的映射关系如下图所示:

public class SimpleHeap {
   /**
    * 实例变量
    */
   private int id;

   /**
    * 构造器
    *
    * @param id
    */
   public SimpleHeap(int id) {
       this.id = id;
  }

   /**
    * 实例方法
    */
   private void displayId() {
       System.out.println("id:" + id);
  }

   public static void main(String[]args){
       SimpleHeap heap1=new SimpleHeap(1);
       SimpleHeap heap2=new SimpleHeap(2);
       heap1.displayId();
       heap2.displayId();
  }

}
复制代码


同理,建设存在Person类,则创建对象实例后,内存中堆区、Java方法栈、方法区三者关系如下图:

直接内存

Java NIO库允许Java程序使用直接内存,直接内存是独立于Java堆区的一块内存区域,访问内存的速度优于堆区,出于性能考虑,针对读写频繁的场景,可以直接操作直接内存,它的大小不受Xmx参数的限制,但堆区内存和直接内存总和必须小于系统内存。

PC寄存器

线程私有空间,又称之为程序计数器,任意时刻,Java线程总在执行一个方法,正在执行的方法,我们称之为:"当前方法”,如果当前方法不是本地方法,则PC寄存器指向当前正在被执行的指令;若当前方法是本地方法,则PC寄存器的值是undefined。

垃圾回收系统

垃圾回收系统是Java虚拟机中的重要组成部分,其主要对方法区、堆区、直接内存空间进行回收,与C/C++不同,Java中所有对象的空间回收是隐式进行的,其中垃圾回收会根据GC算法自动完成内存管理。


作者:洞幺幺洞
来源:https://juejin.cn/post/7040742081236566029

收起阅读 »

Apache Log4j 漏洞(JNDI注入 CVE-2021-44228)

本周最热的事件莫过于 Log4j 漏洞,攻击者仅需向目标输入一段代码,不需要用户执行任何多余操作即可触发该漏洞,使攻击者可以远程控制用户受害者服务器,90% 以上基于 java 开发的应用平台都会受到影响。通过本文特推项目 2 你也能近距离感受这个漏洞的“魅力...
继续阅读 »



本周最热的事件莫过于 Log4j 漏洞,攻击者仅需向目标输入一段代码,不需要用户执行任何多余操作即可触发该漏洞,使攻击者可以远程控制用户受害者服务器,90% 以上基于 java 开发的应用平台都会受到影响。通过本文特推项目 2 你也能近距离感受这个漏洞的“魅力”,而特推 1 则是一个漏洞检测工具,能预防类似漏洞的发生。

除了安全相关的 2 个特推项目之外,本周 GitHub 热门项目还有高性能的 Rust 运行时项目,在你不知道用何词时给你参考词的反向词典 WantWords,还有可玩性贼高的终端模拟器 Tabby。

以下内容摘录自微博@HelloGitHub 的 GitHub Trending 及 Hacker News 热帖(简称 HN 热帖),选项标准:新发布 | 实用 | 有趣,根据项目 release 时间分类,发布时间不超过 14 day 的项目会标注 New,无该标志则说明项目 release 超过半月。由于本文篇幅有限,还有部分项目未能在本文展示,望周知 🌝

  • 本文目录

      1. 本周特推

      • 1.1 JNDI 注入测试工具:JNDI-Injection-Exploit

      • 1.2 Apache Log4j 远程代码执行:CVE-2021-44228-Apache-Log4j-Rce

      1. GitHub Trending 周榜

      • 2.1 塞尔达传说·时之笛反编译:oot

      • 2.2 终端模拟器:Tabby

      • 2.3 反向词典:WantWords

      • 2.4 CPU 性能分析和调优:perf-book

      • 2.5 高性能 Rust Runtime:Monoio

      1. 往期回顾

1. 本周特推

1.1 JNDI 注入测试工具:JNDI-Injection-Exploit

本周 star 增长数: 200+

JNDI-Injection-Exploit 并非是一个新项目,它是一个可用于 Fastjson、Jackson 等相关漏洞的验证的工具,作为 JNDI 注入利用工具,它能生成 JNDI 链接并启动后端相关服务进而检测系统。

GitHub 地址→github.com/welk1n/JNDI…

1.2 Apache Log4j 远程代码执行:CVE-2021-44228-Apache-Log4j-Rce

本周 star 增长数: 1,500+

New CVE-2021-44228-Apache-Log4j-Rce 是 Apache Log4j 远程代码执行,受影响的版本 < 2.15.0。项目开源 1 天便标星 1.5k+ 可见本次 Log4j 漏洞受关注程度。

GitHub 地址→github.com/tangxiaofen…

2. GitHub Trending 周榜

2.1 塞尔达传说·时之笛反编译:oot

本周 star 增长数:900+

oot 是一个反编译游戏塞尔达传说·时之笛的项目,目前项目处于半成品状态,会有较大的代码变更,项目从 scratch 中重新构建代码,并用游戏中发现的信息以及静态、动态分析。如果你想通过这个项目了解反编译知识,建议先保存好个人的塞尔代资产。

GitHub 地址→github.com/zeldaret/oo…

2.2 终端模拟器:Tabby

本周 star 增长数:1,800+

Tabby(曾叫 Terminus)是一个可配置、自定义程度高的终端模拟器、SSH 和串行客户端,适用于 Windows、macOS 和 Linux。它是一种替代 Windows 标准终端 conhost、PowerShell ISE、PuTTY、macOS terminal 的存在,但它不是新的 shell 也不是 MinGW 和 Cygwin 的替代品。此外,它并非一个轻量级工具,如果你注重内存,可以考虑 ConemuAlacritty

GitHub 地址→github.com/Eugeny/tabb…

2.3 反向词典:WantWords

本周 star 增长数:1,000+

WantWords 是清华大学计算机系自然语言处理实验室(THUNLP)开源的反向字词查询工具,反向词典并非是查询反义词的词典,而是基于目前网络词官广泛使用导致部分场景下,我们表达某个意思未能找到精准的用词,所以它可以让你通过想要表达的意思来找寻符合语境的词汇。你可以在线体验反向词典:wantwords.thunlp.org/ 。下图分别为项目 workflow 以及查询结果。

GitHub 地址→github.com/thunlp/Want…

2.4 CPU 性能分析和调优:perf-book

本周 star 增长数:1,300+

perf-book 是书籍《现代 CPU 的性能分析和调优》开源版本,你可以通过 python.exe export_book.py && pdflatex book.tex && bibtex book && pdflatex book.tex && pdflatex book.tex 命令导出 pdf。

GitHub 地址→github.com/dendibakh/p…

2.5 高性能 Rust Runtime:Monoio

本周 star 增长数:1,250+

New Monoio 是字节开源基于 io-uring 的 thread-per-core 模型高性能 Rust Runtime,旨在为高性能网络中间件等场景提供必要的运行时。详细项目背景可以阅读团队的文章Monoio:基于 io-uring 的高性能 Rust Runtime

GitHub 地址→github.com/bytedance/m…

3. 往期回顾

以上为 2021 年第 50 个工作周的 GitHub Trending 🎉如果你 Pick 其他好玩、实用的 GitHub 项目,记得来 HelloGitHub issue 区和我们分享下哟 🌝

最后,记得你在本文留言区留下你想看的主题 Repo(限公众号),例如:AI 换头。👀 和之前的送书活动类似,留言点赞 Top5 的小伙伴(),小鱼干会努力去找 Repo 的^^

作者:HelloGitHub
来源:https://juejin.cn/post/7040980646423953439

收起阅读 »

又到公祭日,App快速实现“哀悼主题”方案

今天是南京大屠杀死难者国家公祭日,很多APP已经变成哀悼主题,怎么实现的呢?看看这个Android方案! 4月4日,今天是国家,为了纪念和哀悼为新冠疫情做出努力和牺牲的烈士以及在新冠中逝去的同胞“举行全国哀悼”的日子! 今天10时全国停止一切娱乐活动,并默...
继续阅读 »


今天是南京大屠杀死难者国家公祭日,很多APP已经变成哀悼主题,怎么实现的呢?看看这个Android方案!

原标题:App快速实现“哀悼主题”方案

4月4日,今天是国家,为了纪念和哀悼为新冠疫情做出努力和牺牲的烈士以及在新冠中逝去的同胞“举行全国哀悼”的日子!
今天10时全国停止一切娱乐活动,并默哀3分钟!至此,各大app(爱奇艺、腾讯、虎牙...)响应号召,统一将app主题更换至“哀悼色”...,本篇文章简谈Android端的一种实现方案。

系统api:saveLayer

saveLayer可以为canvas创建一个新的透明图层,在新的图层上绘制,并不会直接绘制到屏幕上,而会在restore之后,绘制到上一个图层或者屏幕上(如果没有上一个图层)。为什么会需要一个新的图层,例如在处理xfermode的时候,原canvas上的图(包括背景)会影响src和dst的合成,这个时候,使用一个新的透明图层是一个很好的选择

public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint, @Saveflags int saveFlags) {
       if (bounds == null) {
           bounds = new RectF(getClipBounds());
      }
       checkValidSaveFlags(saveFlags);
       return saveLayer(bounds.left, bounds.top, bounds.right, bounds.bottom, paint,
               ALL_SAVE_FLAG);
  }

public int saveLayer(@Nullable RectF bounds, @Nullable Paint paint) {
       return saveLayer(bounds, paint, ALL_SAVE_FLAG);
  }

ColorMatrix中setSaturation设置饱和度,给布局去色(0为灰色,1为原图)

/**
    * Set the matrix to affect the saturation of colors.
    *
    * @param sat A value of 0 maps the color to gray-scale. 1 is identity.
    */
   public void setSaturation(float sat) {
       reset();
       float[] m = mArray;

       final float invSat = 1 - sat;
       final float R = 0.213f * invSat;
       final float G = 0.715f * invSat;
       final float B = 0.072f * invSat;

       m[0] = R + sat; m[1] = G;       m[2] = B;
       m[5] = R;       m[6] = G + sat; m[7] = B;
       m[10] = R;      m[11] = G;      m[12] = B + sat;
  }

1.在view上的实践

自定义MourningImageVIew

public class MourningImageView extends AppCompatImageView {
   private Paint mPaint;
   public MourningImageView(Context context) {
       this(context,null);
  }
   public MourningImageView(Context context, @Nullable AttributeSet attrs) {
       this(context, attrs,0);
  }
   public MourningImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
       createPaint();
  }
   private void createPaint(){
       if(mPaint == null) {
           mPaint = new Paint();
           ColorMatrix cm = new ColorMatrix();
           cm.setSaturation(0);
           mPaint.setColorFilter(new ColorMatrixColorFilter(cm));
      }
  }
   @Override
   public void draw(Canvas canvas) {
       createPaint();
       canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
       super.draw(canvas);
       canvas.restore();
  }
   @Override
   protected void dispatchDraw(Canvas canvas) {
       createPaint();
       canvas.saveLayer(null, mPaint, Canvas.ALL_SAVE_FLAG);
       super.dispatchDraw(canvas);
       canvas.restore();
  }
}

和普通的ImageView对比效果:

举一反三在其他的view上也是可以生效的,实际线上项目不可能去替换所有view,接下来我们在根布局上做文章。

自定义各种MourningViewGroup实际替换效果:

xml version="1.0" encoding="utf-8"?>
<com.example.sample.MourningLinearlayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:app="http://schemas.android.com/apk/res-auto"
   xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent"
   tools:context=".MainActivity"
   android:orientation="vertical"
   >

   <ImageView
       android:layout_width="wrap_content"
       android:src="@mipmap/ic_launcher"
       android:layout_margin="20dp"
       android:layout_height="wrap_content"/>

   <com.example.sample.MourningImageView
       android:layout_width="wrap_content"
       android:src="@mipmap/ic_launcher"
       android:layout_margin="20dp"
       android:layout_height="wrap_content"/>
com.example.sample.MourningLinearlayout>

也是可以生效的,那接下来的问题就是如何用最少的代码替换所有的页面根布局的问题了。在Activity创建的时候同时会创建一个 Window,其中包含一个 DecoView,在我们Activity中调用setContentView(),其实就是把它添加到decoView中,可以统一拦截decoview并对其做文章,降低入侵同时减少代码量。

2.Hook Window DecoView

application中注册ActivityLifecycleCallbacks,创建hook点

public class MineApplication extends Application {

   @Override
   protected void attachBaseContext(Context base) {
       super.attachBaseContext(base);
       registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
           @Override
           public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
              //获取decoview
               ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
               if(decorView!= null && decorView.getChildCount() > 0) {
                   //获取设置的contentview
                   View child = decorView.getChildAt(0);
                   //从decoview中移除contentview
                   decorView.removeView(child);
                   //创建哀悼主题布局
                   MourningFramlayout mourningFramlayout = new MourningFramlayout(activity);
                   //将contentview添加到哀悼布局中
                   mourningFramlayout.addView(child);
                   //将哀悼布局添加到decoview中
                   decorView.addView(mourningFramlayout);
              }
...
}

找个之前做的项目试试效果:

效果还行,暂时没发现什么坑,但是可能存在坑....😄😄😄

作者:code_balance
来源:https://www.jianshu.com/p/abdebc2c508e


收起阅读 »

jsonp的原理是什么?它是怎么实现跨域的?

写在前面一说到javascript的跨域,很多人第一时间想到的就是jsonp(JSON with Padding),那么这种跨域方式的实现原理是什么? 我承认我使用了很长时间,但是还是现在才知道,原来是这样....问题,如果我在 本地 访问 api.com下面...
继续阅读 »



写在前面

一说到javascript的跨域,很多人第一时间想到的就是jsonp(JSON with Padding),那么这种跨域方式的实现原理是什么? 我承认我使用了很长时间,但是还是现在才知道,原来是这样....

问题,如果我在 本地 访问 api.com下面的接口,会出现跨域请求的问题,为什么jsonp能解决这个?

  • 1、script标签是用来加载什么的?

加载js脚本的,src写上一个脚本的地址,然后浏览器就能加载啊!

  • 2、那么本地jsonp.html的script标签可以加载api.com的域名下面的脚本文件吗?

可以啊!要不那些用CDN方式优化网页加载速度的,是不可能成功的如

<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
复制代码
  • 3、那么script能加载别的域名下面的脚本文件,与jsonp何干?

我们都知道,加载api.com的域名下面的js脚本是可以的,此时,api.com下面的js脚本文件为真实存在的静态资源。那么如果这个脚本文件是由后端语言生成的呢?实例使用 php ==>jsonp.php

<?php
echo 'alert("Hello world")';
?>
  • 4、那么问题来了,我们生成js脚本的文件为.php文件啊,怎么加载这个脚本?

答案是:我们的 script标签是能够加载.php文件的,也就是

<script type="text/javascript" src='http://localhost/jsonp.php'></script>

运行结果

以上证明,我们完全可以在服务器端生成一段脚本,然后html页面用script标签去加载然后执行脚本。

那么,我们可以在生成的脚本中执行html中定义的方法吗?我们来试一下

index.html

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <title>jsonp</title>
</head>
<body>
</body>
<script type="text/javascript">
 function execWithJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,传递过来的参数是==>'+para);
   console.log(para)
}
</script>
<script type="text/javascript" src='http://localhost/jsonp.php'></script>
</html>

jsonp.php

<?php
echo "execWithJsonp({status:'ok'})";
?>

运行结果

是的,我们发现完全没问题,我们平常调用接口就是要的后端返回的数据,上面的例子,后端生成脚本时已然给我们传递了参数,拿到数据之后,我们可以做任何我们想做的事。

问题:如果后端接口这么写,那么前端所有调用这个接口的地方,岂不是都要定义一个 execWithJsonp方法?

如果页面调用两次,处理逻辑还不一样,那么我们岂不是要区分是哪一次?我希望每次访问接口调用不同的处理数据函数,每次我来告诉后端用哪个函数来处理返回的数据。 当然可以,我们可以这么做

index.html

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="utf-8">
   <title>jsonp</title>
</head>
<body>
</body>
<script type="text/javascript">
 function execWithJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是execWithJsonp,传递过来的参数是==>'+para);
   console.log(para)
}
 function doExecJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是doExecJsonp,传递过来的参数是==>'+para);
   console.log(para)
}
</script>
<script type="text/javascript" src='http://localhost/jsonp.php?callback=doExecJsonp'></script>
<script type="text/javascript" src='http://localhost/jsonp.php?callback=execWithJsonp'></script>
</html>

jsonp.php

<?php
 $callback=$_GET['callback'];
 echo $callback."({status:'ok'})";
?>

运行结果

说到这儿,我好像还是没说原理是啥,其实你看完上面的也就理解了

jsonp实际上就是

  • 1、前端调用后端时传递给后端数据的处理函数callback

  • 2、后端收到处理函数callback之后,进行数据库查询等操作,将后端要传递给前端的数据(一般为json格式)放入callback函数的()中并返回【实际上就是由后端动态生成一个前端可用的js脚本】,

  • 3、html页面在脚本文件加载后,自动执行脚本

  • 4、完成了整个jsonp请求。

优缺点

优点:它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制;它的兼容性更好,在更加古老的浏览器中都 可以运行,不需要XMLHttpRequest或ActiveX的支持;并且在请求完毕后可以通过调用callback的方式回传结果。

缺点:它只支持GET请求而不支持POST等其它类型的HTTP请求;它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题,切很明显的需要后端工程师配合才能完成。

后记,发挥自己的想象吧,看这东西该怎么操作好

 function execWithJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是execWithJsonp,传递过来的参数是==>'+para);
   console.log(para)
}
 function doExecJsonp(para){
   console.log('我被jsonp.php中的脚本执行了,我是doExecJsonp,传递过来的参数是==>'+para);
   console.log(para)
}

doJsonp('doExecJsonp')

function doJsonp(callbackName){
 var script=document.createElement('script');
 script.src='http://localhost/jsonp.php?callback='+callbackName;
 document.body.appendChild(script);
}


作者:小枫学幽默
来源:https://juejin.cn/post/7040730836156547086

收起阅读 »

Vite为什么快呢?快在哪?说一下我自己的理解吧

前言大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。由于这几个月使用了Vue3 + TS + Vite进行开发,并且是真的被Vite强力吸粉了!!!Vite最大的优点就是:快!!!非常快!!!说实话,使用Vite开发...
继续阅读 »



前言

大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。

由于这几个月使用了Vue3 + TS + Vite进行开发,并且是真的被Vite强力吸粉了!!!Vite最大的优点就是:快!!!非常快!!!

说实话,使用Vite开发之后,我都有点不想回到以前Webpack的项目开发了,因为之前的项目启动项目需要30s以上,修改代码更新也需要2s以上,但是现在使用Vite,差不多启动项目只需要1s,而修改代码更新也是超级快!!!

那到底是为什么Vite可以做到这么快呢?官方给的解释,真的很官方。。所以今天我想用比较通俗易懂的话来讲讲,希望大家能看一遍就懂。

问题现状

ES模块化支持的问题

咱们都知道,以前的浏览器是不支持ES module的,比如:

// index.js

import { add } from './add.js'
import { sub } from './sub.js'
console.log(add(1, 2))
console.log(sub(1, 2))

// add.js
export const add = (a, b) => a + b

// sub.js
export const sub = (a, b) => a - b

你觉得这样的一段代码,放到浏览器能直接运行吗?答案是不行的哦。那怎么解决呢?这时候打包工具出场了,他将index.js、add.js、sub.js这三个文件打包在一个bundle.js文件里,然后在项目index.html中直接引入bundle.js,从而达到代码效果。一些打包工具,都是这么做的,例如webpack、Rollup、Parcel

项目启动与代码更新的问题

这个不用说,大家都懂:

  • 项目启动:随着项目越来越大,启动个项目可能要几分钟

  • 代码更新:随着项目越来越大,修改一小段代码,保存后都要等几秒才更新

解决问题

解决启动项目缓慢

Vite在打包的时候,将模块分成两个区域依赖源码

  • 依赖:一般是那种在开发中不会改变的JavaScript,比如组件库,或者一些较大的依赖(可能有上百个模块的库),这一部分使用esbuild来进行预构建依赖,esbuild使用的是 Go 进行编写,比 JavaScript 编写的打包器预构建依赖快 10-100倍

  • 源码:一般是哪种好修改几率比较大的文件,例如JSX、CSS、vue这些需要转换且时常会被修改编辑的文件。同时,这些文件并不是一股脑全部加载,而是可以按需加载(例如路由懒加载)。Vite会将文件转换后,以es module的方式直接交给浏览器,因为现在的浏览器大多数都直接支持es module,这使性能提高了很多,为什么呢?咱们看下面两张图:

第一张图,是以前的打包模式,就像之前举的index.js、add.js、sub.js的例子,项目启动时,需要先将所有文件打包成一个文件bundle.js,然后在html引入,这个多文件 -> bundle.js的过程是非常耗时间的。

第二张图,是Vite的打包方式,刚刚说了,Vite是直接把转换后的es module的JavaScript代码,扔给支持es module的浏览器,让浏览器自己去加载依赖,也就是把压力丢给了浏览器,从而达到了项目启动速度快的效果。

解决更新缓慢

刚刚说了,项目启动时,将模块分成依赖源码,当你更新代码时,依赖就不需要重新加载,只需要精准地找到是哪个源码的文件更新了,更新相对应的文件就行了。这样做使得更新速度非常快。

Vite 同时利用 HTTP 头来加速整个页面的重新加载(再次让浏览器为我们做更多事情):源码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。

生产环境

刚刚咱们说的都是开发环境,也说了,Vite在是直接把转化后的es module的JavaScript,扔给浏览器,让浏览器根据依赖关系,自己去加载依赖。

那有人就会说了,那放到生产环境时,是不是可以不打包,直接在开个Vite服务就行,反正浏览器会自己去根据依赖关系去自己加载依赖。答案是不行的,为啥呢:

  • 1、你代码是放在服务器的,过多的浏览器加载依赖肯定会引起更多的网络请求

  • 2、为了在生产环境中获得最佳的加载性能,最好还是将代码进行tree-shaking、懒加载和 chunk 分割、CSS处理,这些优化操作,目前esbuild还不怎么完善

所以Vite最后的打包是使用了Rollup


作者:Sunshine_Lin
来源:https://juejin.cn/post/7040750959764439048

收起阅读 »

轻量级安卓水印框架,支持隐形数字水印 AndroidWM

一个轻量级的 Android 图片水印框架,支持隐形水印和加密水印。 English version下载与安装Maven:<dependency>  <groupId>com.huangyz0918groupId> &n...
继续阅读 »



AndroidWM

一个轻量级的 Android 图片水印框架,支持隐形水印和加密水印。 English version

img

下载与安装

Gradle:

implementation 'com.huangyz0918:androidwm:0.1.9'

Maven:

<dependency>
 <groupId>com.huangyz0918groupId>
 <artifactId>androidwmartifactId>
 <version>0.1.9version>
 <type>pomtype>
dependency>

Lvy:

<dependency org='com.huangyz0918' name='androidwm' rev='0.1.9'>
 <artifact name='androidwm' ext='pom' >artifact>
dependency>

快速入门

新建一个水印图片

在下载并且配置好 androidwm 之后,你可以创建一个 WatermarkImage 或者是 WatermarkText 的实例,并且使用内置的诸多Set方法为创建一个水印做好准备。

    WatermarkText watermarkText = new WatermarkText(editText)
          .setPositionX(0.5)
          .setPositionY(0.5)
          .setTextColor(Color.WHITE)
          .setTextFont(R.font.champagne)
          .setTextShadow(0.1f, 5, 5, Color.BLUE)
          .setTextAlpha(150)
          .setRotation(30)
          .setTextSize(20);

对于具体定制一个文字水印或者是图片水印, 我们在接下来的文档中会仔细介绍。

当你的水印(文字或图片水印)已经准备就绪的时候,你需要一个 WatermarkBuilder来把水印画到你希望的背景图片上。 你可以通过 create 方法获取一个 WatermarkBuilder 的实例,注意,在创建这个实例的时候你需要先传入一个 Bitmap 或者是一个 Drawable 的资源 id 来获取背景图。

    WatermarkBuilder
          .create(context, backgroundBitmap)
          .loadWatermarkText(watermarkText) // use .loadWatermarkImage(watermarkImage) to load an image.
          .getWatermark()
          .setToImageView(imageView);

选择绘制模式

你可以在 WatermarkBuilder.setTileMode() 中选择是否使用铺满整图模式,默认情况下我们只会添加一个水印。

    WatermarkBuilder
          .create(this, backgroundBitmap)
          .loadWatermarkText(watermarkText)
          .setTileMode(true) // select different drawing mode.
          .getWatermark()
          .setToImageView(backgroundView);

咚! 带水印的图片已经绘制好啦:

img

获取输出图片

你可以在 WatermarkBuilder 中同时加载文字水印和图片水印。 如果你想在绘制完成之后获得带水印的结果图片,可以使用 Watermark.getOutputImage() 方法:

    Bitmap bitmap = WatermarkBuilder
          .create(this, backgroundBitmap)
          .getWatermark()
          .getOutputImage();

创建多个水印

你还可以一次性添加多个水印图片,通过创建一个WatermarkText 的列表 List<> 并且在水印构建器的方法 .loadWatermarkTexts(watermarkTexts)中把列表传入进去(图片类型水印同理):

    WatermarkBuilder
          .create(this, backgroundBitmap)
          .loadWatermarkTexts(watermarkTexts)
          .loadWatermarkImages(watermarkImages)
          .getWatermark();

加载资源

你还可以从系统的控件和资源中装载图片或者是文字资源,从而创建一个水印对象:

WatermarkText watermarkText = new WatermarkText(editText); // for a text from EditText.
WatermarkText watermarkText = new WatermarkText(textView); // for a text from TextView.
WatermarkImage watermarkImage = new WatermarkImage(imageView); // for an image from ImageView.
WatermarkImage watermarkImage = new WatermarkImage(this, R.drawable.image); // for an image from Resource.

WatermarkBuilder里面的背景图片同样可以从系统资源或者是 ImageView 中装载:

    WatermarkBuilder
          .create(this, backgroundImageView) // .create(this, R.drawable.background)
          .getWatermark()

如果在水印构建器中你既没有加载文字水印也没有加载图片水印,那么处理过后的图片将保持原样,毕竟你啥也没干 :)

隐形水印 (测试版)

androidwm 支持两种模式的隐形水印:

  • 空域 LSB 水印

  • 频域叠加水印

你可以通过WatermarkBuilder 直接构造一个隐形水印,为了选择不同的隐形方式,可以使用布尔参数 isLSB 来区分它们 (注:频域水印扔在开发中),而想要获取到构建成功的水印图片,你需要添加一个监听器:

     WatermarkBuilder
          .create(this, backgroundBitmap)
          .loadWatermarkImage(watermarkBitmap)
          .setInvisibleWMListener(true, 512, new BuildFinishListener<Bitmap>() {
               @Override
               public void onSuccess(Bitmap object) {
                   if (object != null) {
                      // do something...
                  }
              }

               @Override
               public void onFailure(String message) {
                  // do something...
              }
          });

setInvisibleWMListener 方法的第二个参数是一个整数,表示输入图片最大尺寸,有的时候,你输入的可能是一个巨大的图片,为了使计算算法更加快速,你可以选择在构建图片之前是否对图片进行缩放,如果你让这个参数为空,那么图片将以原图形式进行添加水印操作。无论如何,注意一定要保持背景图片的大小足以放得下水印图片中的信息,否则会抛出异常。

同理,检测隐形水印可以使用类WatermarkDetector,通过一个create方法获取到实例,同时传进去一张加过水印的图片,第一个布尔参数代表着水印的种类,true 代表着检测文字水印,反之则检测图形水印。

     WatermarkDetector
          .create(inputBitmap, true)
          .detect(false, new DetectFinishListener() {
               @Override
               public void onImage(Bitmap watermark) {
                   if (watermark != null) {
                        // do something...
                  }
              }

               @Override
               public void onText(String watermark) {
                   if (watermark != null) {
                       // do something...
                  }
              }

               @Override
               public void onFailure(String message) {
                      // do something...
              }
          });

LSB 隐形空域水印 Demo 动态图:

imgimg
隐形文字水印 (LSB)隐形图像水印 (LSB)

好啦!请尽情使用吧 😘

使用说明

水印位置

我们使用 WatermarkPosition 这个类的对象来控制具体水印出现的位置。

   WatermarkPosition watermarkPosition = new WatermarkPosition(double position_x, double position_y, double rotation);
  WatermarkPosition watermarkPosition = new WatermarkPosition(double position_x, double position_y);

在函数构造器中,我们可以设定水印图片的横纵坐标,如果你想在构造器中初始化一个水印旋转角度也是可以的, 水印的坐标系以背景图片的左上角为原点,横轴向右,纵轴向下。

WatermarkPosition 同时也支持动态调整水印的位置,这样你就不需要一次又一次地初始化新的位置对象了, androidwm 提供了一些方法:

     watermarkPosition
            .setPositionX(x)
            .setPositionY(y)
            .setRotation(rotation);

在全覆盖水印模式(Tile mode)下,关于水印位置的参数将会失效。

imgimg
x = y = 0, rotation = 15x = y = 0.5, rotation = -15

横纵坐标都是一个从 0 到 1 的浮点数,代表着和背景图片的相对比例。

字体水印的颜色

你可以在 WatermarkText 中设置字体水印的颜色或者是其背景颜色:

    WatermarkText watermarkText = new WatermarkText(editText)
          .setPositionX(0.5)
          .setPositionY(0.5)
          .setTextSize(30)
          .setTextAlpha(200)
          .setTextColor(Color.GREEN)
          .setBackgroundColor(Color.WHITE); // 默认背景颜色是透明的
imgimg
color = green, background color = whitecolor = green, background color = default

字体颜色的阴影和字体

你可以从软件资源中加载一种字体,也可以通过方法 setTextShadow 设置字体的阴影。

    WatermarkText watermarkText = new WatermarkText(editText)
          .setPositionX(0.5)
          .setPositionY(0.5)
          .setTextSize(40)
          .setTextAlpha(200)
          .setTextColor(Color.GREEN)
          .setTextFont(R.font.champagne)
          .setTextShadow(0.1f, 5, 5, Color.BLUE);
imgimg
font = champagneshadow = (0.1f, 5, 5, BLUE)

阴影的四个参数分别为: (blur radius, x offset, y offset, color).

字体大小和图片大小

水印字体和水印图片大小的单位是不同的:
- 字体大小和系统布局中字体大小是类似的,取决于屏幕的分辨率和背景图片的像素,您可能需要动态调整。
- 图片大小是一个从 0 到 1 的浮点数,是水印图片的宽度占背景图片宽度的比例。

imgimg
image size = 0.3text size = 40

方法列表

对于 WatermarkTextWatermarkImage 的定制化,我们提供了一些常用的方法:

方法名称备注默认值
setPosition水印的位置类 WatermarkPositionnull
setPositionX水印的横轴坐标,从背景图片左上角为(0,0)0
setPositionY水印的纵轴坐标,从背景图片左上角为(0,0)0
setRotation水印的旋转角度0
setTextColor (WatermarkText)WatermarkText 的文字颜色Color.BLACK
setTextStyle (WatermarkText)WatermarkText 的文字样式Paint.Style.FILL
setBackgroundColor (WatermarkText)WatermarkText 的背景颜色null
setTextAlpha (WatermarkText)WatermarkText 文字的透明度, 从 0 到 25550
setImageAlpha (WatermarkImage)WatermarkImage 图片的透明度, 从 0 到 25550
setTextSize (WatermarkText)WatermarkText 字体的大小,单位与系统 layout 相同20
setSize (WatermarkImage)WatermarkImage 水印图片的大小,从 0 到 1 (背景图片大小的比例)0.2
setTextFont (WatermarkText)WatermarkText 的字体default
setTextShadow (WatermarkText)WatermarkText 字体的阴影与圆角(0, 0, 0)
setImageDrawable (WatermarkImage)WatermarkImage的图片资源null

WatermarkImage 的一些基本属性和WatermarkText 的相同。

项目地址:https://github.com/huangyz0918/AndroidWM

作者:huangyz0918
来源:https://www.wanandroid.com/blog/show/2346

收起阅读 »

优秀的react框架的开源ui库 -- Pile.js

Pile.js是滴滴出行企业级前端组开发的一套基于 react 的移动端组件库,Pile.js组件库在滴滴企业级产品中极大提高了开发效率,也希望我们的产出能给广大前端开发者带来便捷。特性质量可靠 由滴滴企业级业务精简提炼而来,经历了一年多的考验,提供质量保障标...
继续阅读 »

Pile.js是滴滴出行企业级前端组开发的一套基于 react 的移动端组件库,Pile.js组件库在滴滴企业级产品中极大提高了开发效率,也希望我们的产出能给广大前端开发者带来便捷。

特性

质量可靠
由滴滴企业级业务精简提炼而来,经历了一年多的考验,提供质量保障

标准规范
代码规范严格按照eslint Airbnb编码规范,增加代码的可读性

优势

相对于同类型的移动端组件库,Pile.js有哪些优势?

组件数量多、体积小
Pile.js组件库包含52个组件,体积只有236k(未压缩),并且我们支持单个组件引用,除了常用的基础组件(比如:Button、Alert、Toast、Tip、Content等)外,我们还包含更为丰富的日期、时间、城市、车型组件,包括雷达图、环形加载、刻度尺组件等以及canvas动画图表等

样式定制
Pile.js设计规范上支持一定程度的样式定制,以满足业务和品牌上多样化的视觉需求

多语言
组件内文案提供统一的国际化支持,配置LocaleProvider组件,运用React的context特性,只需在应用外围包裹一次即可全局生效。

啰嗦一句,如果你有兴趣,不妨也参与到这个项目中来。

项目地址:https://github.com/didi/pile.js

Pile Issues:https://github.com/didi/pile.js/issues

文档: https://didi.github.io/pile.js/docs/

demo: https://didi.github.io/pile.js/demo/#/?_k=klfvmd

组件分类

作者:闫森
来源: https://www.cnblogs.com/yansen/p/9083173.html


收起阅读 »

又到年会抽奖的时候,这是你要的抽奖程序

原标题:公司年会用了我的抽奖程序,然后我中奖了…… 这是我去年写的代码和文章,眼看又到年底抽奖季了,翻出来洗洗还能再用背景临近年末,又到了各大公司举办年会的时候了。对于年会,大家最关心的应该就是抽奖了吧?虽然中奖概率通常不高,但总归是个机会,期待一下也是好...
继续阅读 »

原标题:公司年会用了我的抽奖程序,然后我中奖了……

这是我去年写的代码和文章,眼看又到年底抽奖季了,翻出来洗洗还能再用

背景

临近年末,又到了各大公司举办年会的时候了。对于年会,大家最关心的应该就是抽奖了吧?虽然中奖概率通常不高,但总归是个机会,期待一下也是好的。

最近,我们部门举办了年会,也有抽奖环节。临近年会的前几天,Boss 突然找到我,说要做一个抽奖程序,部门年会要用。我当时都懵了:就三天时间,万一做的程序有bug,岂不是要被现场百十号人的唾沫给淹死?没办法,Boss 看起来对我很有信心,我也只能硬着头皮上了。

需求

  1. 要一个设置页面,包括设置奖项、参与人员名单等。

  2. 如果单个奖项中奖人数过多,可分批抽取,每批人数可设置。

  3. 默认按奖项顺序抽奖,也可选定某个奖项开始。

  4. 可删除没到场的中奖者,同时可再次抽取以作替补。

  5. 可在任意奖项之间切换,可查中奖记录名单

  6. 支持撤销当前轮次的抽奖结果,重新抽取。

实现

身为Web前端开发,自然想到用Web技术来实现。本着不重复造轮子的原则,首先求助Google,Github。搜了一圈好像没有找到特别好用的程序能直接用的。后来看到一个Github上的一个项目,用 TagCanvas 做的抽奖程序,界面挺好,就是逻辑有问题,点几次就崩溃了。代码是不能拿来用了,标签云这种抽奖形式倒是可以借鉴。于是找来文档看了下基本用法,很快就集成到页面里了。

由于设置页面涉及多种交互,纯手写太费时间了,直接用框架。平时 Element UI 用得比较多,自然就用它了。考虑到年会现场可能没有网络,就把框架相关的JS和CSS都下载到本地,直接引用。为了快速开发,也没搭建webpack构建工具了,直接在浏览器里引入JS。

    <link rel="stylesheet" href="css/reset.css" />
  <link
    rel="stylesheet"
    href="js/element-ui@2.4.11/lib/theme-chalk/index.css"
  />
  <script src="js/polyfill.min.js"></script>
  <script src="js/vue.min.js"></script>
  <script src="js/element-ui@2.4.11/lib/index.js"></script>
  <script src="js/member.js"></script>
1.先设计数据结构。 奖项列表 awards
[{
  "name": "二等奖",
  "count": 25,
  "award": "办公室一日游"
}, {
  "name": "一等奖",
  "count": 10,
  "award": "BMW X5"
}, {
  "name": "特等奖",
  "count": 1,
  "award": "深圳湾一号"
}]
2.参与人列表 members
[{
"id": 1,
"name": "张三"
}, {
"id": 2,
"name": "李四"
}]
3.待抽奖人员列表players,是members 的子集
[{
"id": 1,
"name": "张三"
}]
4.抽奖结果列表result,按奖项顺序索引
[[{
  "id": 1,
  "name": "张三"
}], [{
  "id": 2,
  "name": "李四"
}]]
5.设置页面 包括奖项设置和参与人员列表。

6.抽奖页面

具体代码可以去我的Github项目 查看,方便的话可以点个 star。也可以现在体验一下。由于时间仓促,代码写得比较将就。

年会当天抽中了四等奖:1000元购物卡。我是不是该庆幸自己没中特等奖……

作者:KaysonLi
来源:https://juejin.cn/post/6844904033652572174



收起阅读 »

Hi~ 这将是一个通用的新手引导解决方案

本组件已开源,源码可见:github.com/bytedance/g…组件背景不管是老用户还是新用户,在产品发布新版本、有新功能上线、或是现有功能更新的场景下,都需要一定的指导。功能引导组件就是互联网产品中的指示牌,它旨在带领用户参观产品,帮助用户熟悉新的界面...
继续阅读 »



本组件已开源,源码可见:github.com/bytedance/g…

组件背景

不管是老用户还是新用户,在产品发布新版本、有新功能上线、或是现有功能更新的场景下,都需要一定的指导。功能引导组件就是互联网产品中的指示牌,它旨在带领用户参观产品,帮助用户熟悉新的界面、交互与功能。与 FAQs、产品介绍视频、使用手册、以及 UI 组件帮助信息不同的是,功能引导组件与产品 UI 融合为一体,不会给用户割裂的交互感受,并且不需要用户主动进行触发操作,就会展示在用户眼前。

图片比文字更加具象,以下是两种典型的新手引导组件,你是不是一看就明白功能引导组件是什么了呢?

img

img

功能简介

分步引导

Guide 组件以分步引导为核心,像指路牌一样,一节一节地引导用户从起点到终点。这种引导适用于交互流程较长的新功能,或是界面比较复杂的产品。它带领用户体验了完整的操作链路,并快速地了解各个功能点的位置。

img

img

呈现方式

蒙层模式

顾名思义,蒙层引导是指在产品上用一个半透明的黑色进行遮罩,蒙层上方对界面进行高亮,旁边配以弹窗进行讲解。这种引导方式阻断了用户与界面的交互,让用户的注意力聚焦在所圈注的功能点上,不被其他元素所干扰。

img

弹窗模式

很多场景下,为了不干扰用户,我们并不想使用蒙层。这时,我们可以使用无蒙层模式,即在功能点旁边弹出一个简单的窗口引导。

img

精准定位

初始定位

Guide 提供了 12 种对齐方式,将弹窗引导加载到所选择的元素上。同时,还允许自定义横纵向偏差值,对弹窗的位置进行调整。下图分别展示了定位为 top-left 和 right-bottom 的弹窗:

img

img

并且当用户缩放或者滚动页面时,弹窗的定位依然是准确的。

自动滚动

在很多情境中,我们都需要对距离较远的几个页面元素进行功能说明,串联成一个完整的引导路径。当下一步要圈注的功能点不在用户视野中时,Guide 会自动滚动页面至合适的位置,并弹出引导窗口。

1.gif

键盘操作

当 Guide 引导组件弹出时,我们希望用户的注意力被完全吸引过来。为了让使用辅助阅读器的用户也能够感知到 Guide 的出现,我们将页面焦点移动到弹窗上,并且让弹窗里的每一个可读元素都能够聚焦。同时,用户可以用键盘(tab 或 tab+shift)依次聚焦弹窗里的内容,也可以按 escape 键退出引导。

下图中,用户用 tab 键在弹窗中移动焦点,被聚焦的元素用虚线框标识出来。当聚焦到“下一步”按钮时,敲击 shift 键,便可跳至下一步引导。

2.gif

技术实现

总体流程

在展示组件的步骤前我们会先判断是否过期,判断是否过期的标准有两个:一个是该引导组件在localStorage中存储唯一 key 是否为 true,为 true 则为该组件步骤执行完毕。第二个是组件接收一个props.expireDate,如果当前时间大于expireDate则代表组件已经过期则不会继续展示。

img

当组件没有过期时,会展示传入的props.steps相应的内容,steps 结构如下:

interface Step {
   selector: string;
   title: string;
   content: React.Element | string;
   placement: 'top' | 'bottom' | 'left' | 'right'
       | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right',
   offset: Record<'top' | 'bottom' | 'left' | 'right', number>
}

const steps = Step[]

根据 step.selector 获取高亮元素,再根据 step.placement 将弹窗展示到高亮元素相关的具体位置。点击下一步会按序展示下个 step,当所有步骤展示完毕之后我们会将该引导组件在 localStorage 中存储唯一 key 置为 true,下次进来将不再展示。

下面来看看引导组件的具体细节实现吧。

蒙层模式

当前的引导组件支持有无蒙层两种模式,有蒙层的展示效果如下图所示。

img

蒙层很好实现,就是一个撑满屏幕的 div,但是我们怎么才能让它做到高亮出中间的 selector 元素并且还支持圆角呢?🤔 ,真相只有一个,那就是—— border-width

img

我们拿到了 selector 元素的offsetTop, offsetRight, offsetBottom, offsetLeft,并相应地设置为高亮框的border-width,再把border-color设置为灰色,一个带有高亮框的蒙层就实现啦!在给这个高亮框 div 加个pseudo-element ::after 来赋予它 border-radius,完美!

弹窗的定位

用户使用 Guide 时,传入了步骤信息,每一步都包括了所要进行引导说明的界面元素的 CSS 选择器。我们将所要标注的元素叫做“锚元素”。Guide 需要根据锚元素的位置信息,准确地定位弹窗。

每一个 HTML 元素都有一个只读属性 offsetParent,它指向最近的(指包含层级上的最近)包含该元素的定位元素或者最近的 table,td,th,body元素。每个元素都是根据它的 offsetParent 元素进行定位的。比如说,一个 absolute 定位的元素,是根据它最近的、非 static 定位的上级元素进行偏移的,这个上级元素,就是其的 offsetParent。

所以我们想到将弹窗元素放进锚元素的 offsetParent 中,再对其位置进行调整。同时,为了不让锚元素 offsetParent 中的其它元素产生位移,我们设定弹窗元素为 absolute 绝对定位。

定位步骤

弹窗的定位计算流程大致如下:

img

步骤 1. 得到锚元素

通过传给 Guide 的步骤信息中的 selector,即 CSS selector,我们可以由下述代码拿到锚元素:

const anchor = document.querySelector(selector);

如何拿到 anchor 的 offsetParent 呢?这一步其实并没有想象中那么简单。下面我们就来详细地讲一讲这一步吧。

步骤 2. 获取 offsetParent

一般来说,拿到锚元素的 offsetParent,也只需要简单的一行代码:

const parent = anchor.offsetParent;

但是这行代码并不能涵盖所有的场景,我们需要考虑一些特殊的情况。

场景一: 锚元素为 fixed 定位

并不是所有的 HTMLElement 都有 offsetParent 属性。当锚元素为 fixed 定位时,其 offsetParent 返回 null。这时,我们就需要使用其 包含块(containing block) 代替 offsetParent 了。

包含块是什么呢?大多数情况下,包含块就是这个元素最近的祖先块元素的内容区,但也不是总是这样。一个元素的包含块是由其 position 属性决定的。

  • 如果 position 属性是 fixed,包含块通常是 document.documentElement

  • 如果 position 属性是 fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:

    • transformperspective的值不是none

    • will-change 的值是 transformperspective

    • filter 的值不是 nonewill-change 的值是 filter(只在 Firefox 下生效).

    • contain 的值是 paint (例如: contain: paint;)

因此,我们可以从锚元素开始,递归地向上寻找符合上述条件的父级元素,如果找不到,那么就返回 document.documentElement

下面是 Guide 中用来寻找包含块的代码:

const getContainingBlock = node => {
 let currentNode = getDocument(node).documentElement;

 while (
   isHTMLElement(currentNode) &&
   !['html', 'body'].includes(getNodeName(currentNode))
) {
   const css = getComputedStyle(currentNode);

   if (
     css.transform !== 'none' ||
     css.perspective !== 'none' ||
    (css.willChange && css.willChange !== 'auto')
  ) {
     return currentNode;
  }
   currentNode = currentNode.parentNode;
}

 return currentNode;
};
场景二:在 iframe 中使用 Guide

在 Guide 的代码中,我们常常用到 window 对象。比如说,我们需要在 window 对象上调用 getComputedStyle()获取元素的样式,我们还需要 window 对象作为元素 offsetParent 的兜底。但是我们并不能直接使用 window 对象,为什么呢?这时,我们需要考虑 iframe 的情况。

想象一下,如果我们在一个内嵌了 iframe 的应用中使用 Guide 组件,Guide 组件代码在 iframe 外面,而被引导的功能点在 iframe 里面,那么在使用 Window 对象提供的方法是,我们一定是想在所圈注的功能点所在的 Window 对象上进行调用,而非当前代码运行的 Window。

因此,我们通过下面的 getWindow 方法,确保拿到的是参数 node 所在的 Window。

// Get the window object using this function rather then simply use `window` because
// there are cases where the window object we are seeking to reference is not in
// the same window scope as the code we are running. (https://stackoverflow.com/a/37638629)
const getWindow = node => {
 // if node is not the window object
 if (node.toString() !== '[object Window]') {
   // get the top-level document object of the node, or null if node is a document.
   const { ownerDocument } = node;
   // get the window object associated with the document, or null if none is available.
   return ownerDocument ? ownerDocument.defaultView || window : window;
}

 return node;
};

在 line 8,我们看到一个属性 ownerDocument。如果 node 是一个 DOM Element,那么它具有一个属性 ownerDocument,此属性返回的 document 对象是在实际的 HTML 文档中的所有子节点所属的主对象。如果在文档节点自身上使用此属性,则结果是 null。当 node 为 Window 对象时,我们返回 window;当 node 为 Document 对象时,我们返回了 ownerDocument.defaultView 。这样,getWindow 函数便涵盖了参数 node 的所有可能性。

步骤 3. 挂载弹窗

如下代码所示,我们常常遇到的使用场景是,在组件 A 中渲染 Guide,让其去标注的元素却在组件 B、组件 C 中。

 // 组件A
const A = props => (
   <>
       <Guide
           steps={[
              {
                   ......
                   selector: '#btn1'
              },
              {
                   ......
                   selector: '#btn2'
              },
              {
                   ......
                   selector: '#btn3'
              }
          ]}
       />
       <button id="btn1">Button 1</button>
   </>
)

// 组件B
const B = props => (<button id="btn2">Button 2</button>)

// 组件C
const C = props => (<button id="btn3">Button 3</button>)

上述代码中,Guide 会自然而然地渲染在 A 组件 DOM 结构下,我们怎样将其挂载到组件 B、C 的 offsetParent 中呢?这时候就要给大家介绍一下强大却少为人知的 React Portals 了。

React Portals

当我们需要把一个组件渲染到其父节点所在的 DOM 树结构之外时, 我们首先应该考虑使用 React Portals。Portals 最适用于这种需要将子节点从视觉上渲染到其父节点之外的场景了,在 Antd 的 Modal、Popover、Tooltip 组件实现中,我们也可以看到 Portal 的应用。

我们使用 ReactDOM.createPortal(child, container)创建一个 Portal。child 是我们要挂载的组件,container 则是 child 要挂载到的容器组件。

虽然 Portal 是渲染在其父元素 DOM 结构之外的,但是它并不会创建一个完全独立的 React DOM 树。一个 Portal 与 React 树中其它子节点相同,都可以拿到父组件的传来的 props 和 context,也都可以进行事件冒泡。

另外,与 ReactDOM.render 所创建的 React DOM 树不同,ReactDOM.createPortal 是应用在组件的 render 函数中的,因此不需要手动卸载。

在 Guide 中,每跳一步,上一步的弹窗便会卸载掉,新的弹窗会被加载到这一步要圈注的元素的 offsetParent 里。伪代码如下:

const Modal = props => (
ReactDOM.createPortal(
<div>
......
</div>,
offsetParent);
)

将弹窗渲染进 offsetParent 后,Guide 的下一步工作便是计算弹窗相对于 offsetParent 的偏移量。这一步非常复杂,并且要考虑一些特殊情况。下面就让我们就仔细地讲解这部分计算吧。

步骤 4. 偏移量计算

以一个 placement = left ,即需要在功能点左侧展示的弹窗引导为例。如果我们直接把弹窗通过 React Portal 挂载到锚元素的 offsetParent 中,并赋予其绝对定位,其位置会如下图所示——左上角与 offsetParent 的左上角对齐。

_下图中,用蓝色框表示的考拉图片是 Guide 需要标注的元素,即锚元素;红色框则标识出这个锚元素的 offsetParent 元素。

img

而我们预想的定位结果如下:

img

参考下图,将弹窗从初始位置移动至预期位置,我们需要在 y 轴上向下移动弹窗 offsetTop + h1/2 - h2/2 px。其中,h1 为锚元素的高度,h2 为弹窗的高度。

img

但是,上述计算依然忽略了一种场景,那就是当锚元素定位为 fixed 时。若锚元素定位为 fixed,那么无论锚元素所在的界面怎样滑动,锚元素相对于屏幕视口(viewport)的位置是固定的。自然,用来对 fixed 锚元素进行引导的弹窗也需要具有这些特性,即同样需要为 fixed 定位。

Arrow 实现及定位

arrowmodal 的子元素且相对于 modal 绝对定位,如下图所示有十二种展示位置,我们把十二种定位分为两类情况:

  1. 紫色的四种居中情况;

  2. 黄色的其余八种斜角。

img

对于第一类情况

箭头始终是相对弹窗边缘居中的位置,出对于 top、bottom,箭头的 right 值始终是(modal.width - arrow.diagonalWidth)/2 ,而 top 或 bottom 值始终为-arrow.diagonalWidth/2

对于 left、right,箭头的 top 值是(modal.height - arrow.diagonalWidth)/2 ,而 left 或 right 为-arrow.diagonalWidth/2

img

注:diagonalWidth为对角线宽度,getReversePosition\(placement\)为获取传入参数的 reverse 位置,top 对应 bottom,left 对应 right。

伪代码如下:

const placement = 'top' | 'bottom' | 'left' | 'right';
const diagonalWidth = 10;

const style = {
right: ['bottom', 'top'].includes(placement)
? (modal.width - diagonalWidth) / 2
: '',
top: ['left', 'right'].includes(placement)
? (modal.height - diagonalWidth) / 2
: '',
[getReversePosition(placement)]: -diagonalWidth / 2,
};

对于第二类情况

对于 A-B 的位置,通过下图可以发现,B 的位移总是固定值。比如对于 placement 值为 top-left 的弹窗,箭头 left 值总是固定的,而 bottom 值为-arrow.diagonalWidth/2

img

以下为伪代码:

const [firstPlacement, lastPlacement] = placement.split('-');
const diagonalWidth = 10;
const margin = 24;

const style = {
[lastPlacement]: margin,
[getReversePosition(placement)]: -diagonalWidth / 2,
}

Hotspot 实现及定位

引导组件支持 hotspot 功能,通过给一个 div 元素加上动画改变其 box-shadow 大小实现呼吸灯的效果,效果如下图所示,其中热点的定位是相对箭头的位置计算的,这里便不赘述了。

img

结语

在 Guide 的开发初期,我们并没有想到这样一个小组件需要考虑到以上这些技术点。可见,再小的组件,让其适用于所有场景,做到足够通用都是件难事,需要不断地尝试与反思。

作者:字节前端
来源:https://juejin.cn/post/6960493325061193735

收起阅读 »

领域驱动设计(DDD)能给前端带来什么

为什么需要 DDD在回答这个问题之前,我们先看下大部分软件都会经历的发展过程:频繁的变更带来软件质量的下降而这又是软件发展的规律导致的:软件是对真实世界的模拟,真实世界往往十分复杂人在认识真实世界的时候总有一个从简单到复杂的过程因此需求的变更是一种必然,并且总...
继续阅读 »



为什么需要 DDD

在回答这个问题之前,我们先看下大部分软件都会经历的发展过程:频繁的变更带来软件质量的下降

而这又是软件发展的规律导致的:

  • 软件是对真实世界的模拟,真实世界往往十分复杂

  • 人在认识真实世界的时候总有一个从简单到复杂的过程

  • 因此需求的变更是一种必然,并且总是由简单到复杂演变

  • 软件初期的业务逻辑非常清晰明了,慢慢变得越来越复杂

可以看到需求的不断变更和迭代导致了项目变得越来越复杂,那么问题来了,项目复杂性提高的根本原因是需求变更引起的吗?

根本原因其实是因为在需求变更过程中没有及时的进行解耦和扩展。

那么在需求变更的过程中如何进行解耦和扩展呢? DDD 发挥作用的时候来了。

什么是 DDD

DDD(领域驱动设计)的概念见维基百科:zh.wikipedia.org/wiki/\%E9\%…

可以看到领域驱动设计(domin-driven design)不同于传统的针对数据库表结构的设计,领域模型驱动设计自然是以提炼和转换业务需求中的领域知识为设计的起点。在提炼领域知识时,没有数据库的概念,亦没有服务的概念,一切围绕着业务需求而来,即:

  • 现实世界有什么事物 -> 模型中就有什么对象

  • 现实世界有什么行为 -> 模型中就有什么方法

  • 现实世界有什么关系 -> 模型中就有什么关联

在 DDD 中按照什么样的原则进行领域建模呢?

单一职责原则(Single responsibility principle)即 SRP:软件系统中每个元素只完成自己职责内的事,将其他的事交给别人去做。

上面这句话有没有什么哪里不清晰的?有,那就是“职责”两个字。职责该怎么理解?如何限定该元素的职责范围呢?这就引出了“限界上下文”的概念。

Eric Evans 用细胞来形容限界上下文,因为“细胞之所以能够存在,是因为细胞膜限定了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜。”这里,细胞代表上下文,而细胞膜代表了包裹上下文的边界。

我们需要根据业务相关性耦合的强弱程度分离的关注点对这些活动进行归类,找到不同类别之间存在的边界,这就是限界上下文的含义。上下文(Context)是业务目标,限界(Bounded)则是保护和隔离上下文的边界,避免业务目标的不单一而带来的混乱与概念的不一致。

如何 DDD

DDD 的大体流程如下:

  1. 建立统一语言

统一语言是提炼领域知识的产出物,获得统一语言就是需求分析的过程,也是团队中各个角色就系统目标、范围与具体功能达成一致的过程。

使用统一语言可以帮助我们将参与讨论的客户、领域专家与开发团队拉到同一个维度空间进行讨论,若没有达成这种一致性,那就是鸡同鸭讲,毫无沟通效率,相反还可能造成误解。因此,在沟通需求时,团队中的每个人都应使用统一语言进行交流。

一旦确定了统一语言,无论是与领域专家的讨论,还是最终的实现代码,都可以通过使用相同的术语,清晰准确地定义领域知识。重要的是,当我们建立了符合整个团队皆认同的一套统一语言后,就可以在此基础上寻找正确的领域概念,为建立领域模型提供重要参考。

举个例子,不同玩家对于英雄联盟(league of legends)的称呼不尽相同;国外玩家一般叫“League”,国内玩家有的称呼“撸啊撸”,有的称呼“LOL”等等。那么如果要开发相关产品,开发人员和客户首先需要统一对“英雄联盟”的语言模型。

  1. 事件风暴(Event Storming)

事件风暴会议是一种基于工作坊的实践方法,它可以快速发现业务领域中正在发生的事件,指导领域建模及程序开发。 它是 Alberto Brandolini 发明的一 种领域驱动设计实践方法,被广泛应用于业务流程建模和需求工程,基本思想是将软件开发人员和领域专家聚集在一起,相互学习,类似头脑风暴。

会议一般以探讨领域事件开始,从前向后梳理,以确保所有的领域事件都能被覆盖。

什么是领域事件呢?

领域事件是领域模型中非常重要的一部分,用来表示领域中发生的事件。一个领域事件将导致进一步的业务操作,在实现业务解耦的同时,还有助于形成完整的业务闭环。

领域事件可以是业务流程的一个步骤,比如投保业务缴费完成后,触发投保单转保单的动作;也可能是定时批处理过程中发生的事件,比如批处理生成季缴保费通知单,触发发送缴费邮件通知操作;或者一个事件发生后触发的后续动作,比如密码连续输错三次,触发锁定账户的动作。

  1. 进行领域建模,将各个模型分配到各个限界上下文中,构建上下文地图。

领域建模时,我们会根据场景分析过程中产生的领域对象,比如命令、事件等之间关系,找出产生命令的实体,分析实体之间的依赖关系组成聚合,为聚合划定限界上下文,建立领域模型以及模型之间的依赖。

上面我们大体了解了 DDD 的作用,概念和一般的流程,虽然前端和后端的 DDD 不尽相同,但是我们仍然可以将这种思想应用于我们的项目中。

DDD 能给前端项目带来什么

通过领域模型 (feature)组织项目结构,降低耦合度

很多通过 react 脚手架生成的项目组织结构是这样的:

-components
   component1
   component2
-actions.ts
...allActions
-reducers.ts
...allReducers

这种代码组织方式,比如 actions.ts 中的 actions 其实没有功能逻辑关系;当增加新的功能的时候,只是机械的往每个文件夹中加入对应的 component,action,reducer,而没有关心他们功能上的关系。那么这种项目的演进方向就是:

项目初期:规模小,模块关系清晰 ---> 迭代期:加入新的功能和其他元素 ---> 项目收尾:文件结构,模块依赖错综复杂。

因此我们可以通过领域模型的方式来组织代码,降低耦合度。

  1. 首先从功能角度对项目进行拆分。将业务逻辑拆分成高内聚松耦合的模块。从而对 feature 进行新增,重构,删除,重命名等变得简单 ,不会影响到其他的 feature,使项目可扩展和可维护。

  1. 再从技术角度进行拆分,可以看到 componet, routing,reducer 都来自等多个功能模块

可以看到:

  • 技术上的代码按照功能的方式组织在 feature 下面,而不是单纯通过技术角度进行区分。

  • 通常是由一个文件来管理所有的路由,随着项目的迭代,这个路由文件也会变得复杂。那么可以把路由分散在 feature 中,由每个 feature 来管理自己的路由。

通过 feature 来组织代码结构的好处是:当项目的功能越来越多时,整体复杂度不会指数级上升,而是始终保持在可控的范围之内,保持可扩展,可维护。

如何组织 componet,action,reducer

文件夹结构该如何设计?

  • 按 feature 组织组件,action 和 reducer

  • 组件和样式文件在同一级

  • Redux 放在单独的文件

  1. 每个 feature 下面分为 redux 文件夹 和 组件文件

  1. redux 文件夹下面的 action.js 只是充当 loader 的作用,负责将各个 action 引入,而没有具体的逻辑。 reducer 同理

  1. 项目的根节点还需要一个 root loader 来加载 feature 下的资源

如何组织 router

组织 router 的核心思想是把每个路由配置分发到每个 feature 自己的路由表中,那么需要:

  • 每个 feature 都有自己专属的路由配置

  • 顶层路由(页面级别的路由)通过 JSON 配置 1,然后解析 JSON 到 React Router

  1. 每个 feature 有自己的路由配置

  1. 顶层的 routerConfig 引入各个 feature 的子路由

import { App } from '../features/home';
import { PageNotFound } from '../features/common';
import homeRoute from '../features/home/route';
import commonRoute from '../features/common/route';
import examplesRoute from '../features/examples/route';

const childRoutes = [
 homeRoute,
 commonRoute,
 examplesRoute,
];

const routes = [{
   path: '/',
   componet: App,
   childRoutes: [
       ... childRoutes,
      { path:'*', name: 'Page not found', component: PageNotFound },
  ].filter( r => r.componet || (r.childRoutes && r.childRoutes.length > 0))
}]

export default routes
  1. 解析 JSON 路由到 React Router

import React from 'react';
import { Switch, Route } from 'react-router-dom';
import { ConnectedRouter } from 'connected-react-router';
import routeConfig from './common/routeConfig';

function renderRouteConfig(routes, path) {
   const children = []        // children component list
     const renderRoute = (item, routeContextPath) => {
   let newContextPath;
   if (/^\//.test(item.path)) {
     newContextPath = item.path;
  } else {
     newContextPath = `${routeContextPath}/${item.path}`;
  }
   newContextPath = newContextPath.replace(/\/+/g, '/');
   if (item.component && item.childRoutes) {
     const childRoutes = renderRouteConfigV3(item.childRoutes, newContextPath);
     children.push(
       <Route
         key={newContextPath}
         render={props => <item.component {...props}>{childRoutes}</item.component>}
         path={newContextPath}
       />,
    );
  } else if (item.component) {
     children.push(
       <Route key={newContextPath} component={item.component} path={newContextPath} exact />,
    );
  } else if (item.childRoutes) {
     item.childRoutes.forEach(r => renderRoute(r, newContextPath));
  }
};
   routes.forEach(item => renderRoute(item,path))
   return <Switch>children</Switch>
}


function Root() {
 const children = renderRouteConfig(routeConfig, '/');
 return (
     <ConnectedRouter>{children}</ConnectedRouter>
);
}

reference

Rekit:帮助创建遵循一般的最佳实践,可拓展的 Web 应用程序 rekit.js.org/


作者:字节前端
来源:https://juejin.cn/post/7007995442864586766

收起阅读 »

面试官对不起!我终于会了Promise...(一面凉经泪目)

面试题CSS 实现水平垂直居中flex的属性CSS transition的实现效果和有哪些属性CSS 实现沿Y轴旋转360度 (直接自爆了 CSS不行....麻了)好,那来点JS 基本数据类型有哪些 用什么判断数组怎么判断引用类型和基本类型的区别什么是栈?什么...
继续阅读 »

面试题

  • CSS 实现水平垂直居中
  • flex的属性
  • CSS transition的实现效果和有哪些属性
  • CSS 实现沿Y轴旋转360度 (直接自爆了 CSS不行....麻了)
  • 好,那来点JS 基本数据类型有哪些 用什么判断
  • 数组怎么判断
  • 引用类型和基本类型的区别
  • 什么是栈?什么是堆?
  • 手写 翻转字符串
  • 手写 Sum(1,2,3)的累加(argument)(我以为是柯里化,面试官笑了一下,脑筋不要这么死嘛)
  • 箭头函数和普通函数的区别(上题忘记了argument,面试官特意问这个问题提醒我,奈何基础太差救不起来了...泪目)
  • 数组去重的方法
  • 图片懒加载
  • 跨域产生的原因,同源策略是什么
  • 说说你了解的解决办法(只说了JSONP和CORS)
  • Cookie、sessionStorage、localStorage的区别
  • get 和 post 的区别 (只说了传参方式和功能不同,面试官问还有吗 其他的不知道了...)
  • 问了一下项目,react
  • 对ES6的了解 (Promise果真逃不了....)
  • let var const的区别
  • 知道Promise嘛?聊聊对Promise的理解?(说了一下Promise对象代表一个异步操作,有三种状态,状态转变为单向...)
  • 那它是为了解决什么问题的?(emmm当异步返回值又需要等待另一个异步就会嵌套回调,Promise可以解决这个回调地狱问题)
  • 那它是如何解决回调地狱的?(Promise对象内部是同步的,内部得到内部值后进行调用.then的异步操作,可以一直.then .then ...)
  • 好,你说可以一直.then .then ...那它是如何实现一直.then 的?(emmm... 这个.then链式调用就是...额这个...)
  • Promise有哪些方法 all和race区别是什么
  • 具体说一下 .catch() 和 reject (...我人麻了...)


结束环节

  • 问了面试官对CSS的理解(必须但非重要,前端的核心还是尽量一比一还原设计稿,只有写好了页面才能考虑交互)

  • 如何学习(基础是最重要的,CSS和JS要注重实践,盖房子最重要的还是地基,所有的框架源码,组件等都基于CSS和JS)

  • 曾经是如何度过这个过程的(多做项目,在项目中学习理解每个细节,再次告诫我基础的重要性)



Promise概述


Promise是ES6新增的引用类型,可以通过new来进行实例化对象。Promise内部包含着异步的操作。



new Promise(fn)




Promise.resolve(fn)



这两种方式都会返回一个 Promise 对象。



  • Promise 有三种状态: 等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected),且Promise 必须为三种状态之一只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

  • 状态只能由 Pending 变为 Fulfilled 或由 Pending 变为 Rejected ,且状态改变之后不会在发生变化,会一直保持这个状态。

  • Pending 变为 Fulfilled 会得到一个私有value,Pending 变为 Rejected会得到一个私有reason,当Promise达到了Fulfilled或Rejected时,执行的异步代码会接收到这个value或reason。


知道了这些,我们可以得到下面的代码:


实现原理


class Promise {
constructor() {
this.state = 'pending' // 初始化 未完成状态
// 成功的值
this.value = undefined;
// 失败的原因
this.reason = undefined;
}
}

基本用法


Promise状态只能在内部进行操作,内部操作在Promise执行器函数执行。Promise必须接受一个函数作为参数,我们称该函数为执行器函数,执行器函数又包含resolve和reject两个参数,它们是两个函数。



  • resolve : 将Promise对象的状态从 Pending(进行中) 变为 Fulfilled(已成功)

  • reject : 将Promise对象的状态从 Pending(进行中) 变为 Rejected(已失败),并抛出错误。


使用栗子


let p1 = new Promise((resolve,reject) => {
resolve(value);
})
setTimeout(() => {
console.log((p1)); // Promise {<fulfilled>: undefined}
},1)

let p2 = new Promise((resolve,reject) => {
reject(reason);
})
setTimeout(() => {
console.log((p2)); // Promise {<rejected>: undefined}
},1)

实现原理

  • p1 resolve为成功,接收参数value,状态改变为fulfilled,不可再次改变。
  • p2 reject为失败,接收参数reason,状态改变为rejected,不可再次改变。
  • 如果executor执行器函数执行报错,直接执行reject。


所以得到如下代码:


class Promise{
constructor(executor){
// 初始化state为等待态
this.state = 'pending';
// 成功的值
this.value = undefined;
// 失败的原因
this.reason = undefined;
let resolve = value => {
console.log(value);
if (this.state === 'pending') {
// resolve调用后,state转化为成功态
console.log('fulfilled 状态被执行');
this.state = 'fulfilled';
// 储存成功的值
this.value = value;
}
};
let reject = reason => {
console.log(reason);
if (this.state === 'pending') {
// reject调用后,state转化为失败态
console.log('rejected 状态被执行');
this.state = 'rejected';
// 储存失败的原因
this.reason = reason;
}
};
// 如果 执行器函数 执行报错,直接执行reject
try{
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
}

检验一下上述代码咯:


class Promise{...} // 上述代码

new Promise((resolve, reject) => {
console.log(0);
setTimeout(() => {
resolve(10) // 1
// reject('JS我不爱你了') // 2
// 可能有错误
// throw new Error('是你的错') // 3
}, 1000)
})

  • 当执行代码1时输出为 0 后一秒输出 10 和 fulfilled 状态被执行
  • 当执行代码2时输出为 0 后一秒输出 我不爱你了 和 rejected 状态被执行
  • 当执行代码3时 抛出错误 是你的错

.then方法



promise.then(onFulfilled, onRejected)

  • 初始化Promise时,执行器函数已经改变了Promise的状态。且执行器函数是同步执行的。异步操作返回的数据(成功的值和失败的原因)可以交给.then处理,为Promise实例提供处理程序。
  • Promise实例生成以后,可以用then方法分别指定resolved状态rejected状态的回调函数。这两个函数onFulfilled,onRejected都是可选的,不一定要提供。如果提供,则会Promise分别进入resolved状态rejected状态时执行。
  • 而且任何传给then方法的非函数类型参数都会被静默忽略。
  • then 方法必须返回一个新的 promise 对象(实现链式调用的关键)


实现原理

  • Promise只能转换最终状态一次,所以onFulfilledonRejected两个参数的操作是互斥
  • 当状态state为fulfilled,则执行onFulfilled,传入this.value。当状态state为rejected,则执行onRejected,传入this.reason

class Promise {
constructor(executor) {
this.state = 'pending' // 初始化 未完成状态
// 成功的值
this.value = undefined;
// 失败的原因
this.reason = undefined;

// .then 立即执行后 state为pengding 把.then保存起来
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];

// 把异步任务 把结果交给 resolve
let resolve = (value) => {
if (this.state === 'pending') {
console.log('fulfilled 状态被执行');
this.value = value
this.state = 'fulfilled'
// onFulfilled 要执行一次
this.onResolvedCallbacks.forEach(fn => fn());
}
}
let reject = (reason) => {
if (this.state === 'pending') {
console.log('rejected 状态被执行');
this.reason = reason
this.state = 'rejected'
this.onRejectedCallbacks.forEach(fn => fn());
}
}
try {
executor(resolve, reject)
}
catch (e) {
reject(err)
}
}
// 一个promise解决了后(完成状态转移,把控制权交出来)
then(onFulfilled, onRejected) {
if (this.state == 'pending') {
this.onResolvedCallbacks.push(() => {
onFulfilled(this.value)
})
this.onRejectedCallbacks.push(() => {
onRejected(this.reason)
})
}
console.log('then');
// 状态为fulfilled 执行成功 传入成功后的回调 把执行权转移
if (this.state == 'fulfiiied') {
onFulfilled(this.value);
}
// 状态为rejected 执行失败 传入失败后的回调 把执行权转移
if (this.state == 'rejected') {
onRejected(this.reason)
}
}
}
let p1 = new Promise((resolve, reject) => {
console.log(0);
setTimeout(() => {
// resolve(10)
reject('JS我不爱你了')
console.log('setTimeout');
}, 1000)
}).then(null,(data) => {
console.log(data, '++++++++++');
})

0
then
rejected 状态被执行
JS我不爱你了 ++++++++++
setTimeout


当resolve在setTomeout内执行,then时state还是pending等待状态 我们就需要在then调用的时候,将成功和失败存到各自的数组,一旦reject或者resolve,就调用它们。



现可以异步实现了,但是还是不能链式调用啊?
为保证 then 函数链式调用,then 需要返回 promise 实例,再把这个promise返回的值传入下一个then中。


链式调用及后续实现源码


这部分我也不会,还没看懂。后续再更。
先贴代码:


class Promise{
constructor(executor){
this.state = 'pending';
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
let resolve = value => {
if (this.state === 'pending') {
this.state = 'fulfilled';
this.value = value;
this.onResolvedCallbacks.forEach(fn=>fn());
}
};
let reject = reason => {
if (this.state === 'pending') {
this.state = 'rejected';
this.reason = reason;
this.onRejectedCallbacks.forEach(fn=>fn());
}
};
try{
executor(resolve, reject);
} catch (err) {
reject(err);
}
}
then(onFulfilled,onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
let promise2 = new Promise((resolve, reject) => {
if (this.state === 'fulfilled') {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
};
if (this.state === 'rejected') {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
};
if (this.state === 'pending') {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0)
});
};
});
return promise2;
}
catch(fn){
return this.then(null,fn);
}
}
function resolvePromise(promise2, x, resolve, reject){
if(x === promise2){
return reject(new TypeError('Chaining cycle detected for promise'));
}
let called;
if (x != null && (typeof x === 'object' || typeof x === 'function')) {
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, y => {
if(called)return;
called = true;
resolvePromise(promise2, y, resolve, reject);
}, err => {
if(called)return;
called = true;
reject(err);
})
} else {
resolve(x);
}
} catch (e) {
if(called)return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
//resolve方法
Promise.resolve = function(val){
return new Promise((resolve,reject)=>{
resolve(val)
});
}
//reject方法
Promise.reject = function(val){
return new Promise((resolve,reject)=>{
reject(val)
});
}
//race方法
Promise.race = function(promises){
return new Promise((resolve,reject)=>{
for(let i=0;i<promises.length;i++){
promises[i].then(resolve,reject)
};
})
}
//all方法(获取所有的promise,都执行then,把结果放到数组,一起返回)
Promise.all = function(promises){
let arr = [];
let i = 0;
function processData(index,data){
arr[index] = data;
i++;
if(i == promises.length){
resolve(arr);
};
};
return new Promise((resolve,reject)=>{
for(let i=0;i<promises.length;i++){
promises[i].then(data=>{
processData(i,data);
},reject);
};
});
}

Promise的各种方法


Promise.prototype.catch()


catch 异常处理函数,处理前面回调中可能抛出的异常。只接收一个参数onRejected处理程序。它相当于调用Promise.prototype.then(null,onRejected),所以它也会返回一个新的Promise



  • 栗子


let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(10)
}, 1000)
}).then(() => {
throw Error("1123")
}).catch((err) => {
console.log(err);
})
.then(() => {
console.log('异常捕获后可以继续.then');
})
复制代码

当第一个.then的异常被捕获后可以继续执行。


Promise.all()


Promise.all()创建的Promise会在这一组Promise全部解决后在解决。也就是说会等待所有的promise程序都返回结果之后执行后续的程序。返回一个新的Promise。



  • 栗子


let p1 = new Promise((resolve, reject) => {  
resolve('success1')
})

let p2 = new Promise((resolve, reject) => {
resolve('success1')
})
// let p3 = Promise.reject('failed3')
Promise.all([p1, p2]).then((result) => {
console.log(result) // ['success1', 'success2']

}).catch((error) => {
console.log(error)
})
// Promise.all([p1,p3,p2]).then((result) => {
// console.log(result)
// }).catch((error) => {
// console.log(error) // 'failed3'
//
// })
复制代码

有上述栗子得到,all的性质:



  • 如果所有都成功,则合成Promise的返回值就是所有子Promise的返回值数组。

  • 如果有一个失败,那么第一个失败的会把自己的理由作为合成Promise的失败理由。


Promise.race()


Promise.race()是一组集合中最先解决或最先拒绝的Promise,返回一个新的Promise。



  • 栗子


let p1 = new Promise((resolve, reject) => {  
setTimeout(() => {
resolve('success1')
},1000)
})

let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('failed2')
}, 1500)
})

Promise.race([p1, p2]).then((result) => {
console.log(result)
}).catch((error) => {
console.log(error) // 'success1'
})
复制代码

有上述栗子得到,race的性质:

无论如何,最先执行完成的,就执行相应后面的.then或者.catch。谁先以谁作为回调


总结


上面的Promise就总结到这里,讲的可能不太清楚,有兴趣的小伙伴可以看看链接呀,有什么理解也可以在下方评论区一起交流学习。


面试结束了,面试官人很好,聊的很开心,问题大概都能说上来一点,却总有关键部分忘了hhhhhh,结尾跟面试官聊了一下容易忘这个问题,哈哈哈哈他说我忘就是没学会,以后还是要多总结,多做项目...


面试可以让自己发现更多的知识盲点,从而促进自己学习,大家一起加油冲呀!!


作者:_清水
链接:https://juejin.cn/post/6952083081519955998

收起阅读 »

HashMap原理浅析及相关知识

一、初识Hashmap 作为集合Map的主要实现类;线程不安全的,效率高;存储null的key和value。 二、HashMap在Jdk7中实现原理 1、HashMap map = new HashMap() 实例化之后会在底层创建长度是16的一维数组Ent...
继续阅读 »

一、初识Hashmap


作为集合Map的主要实现类;线程不安全的,效率高;存储null的key和value。


image.png


二、HashMap在Jdk7中实现原理


1、HashMap map = new HashMap()


实例化之后会在底层创建长度是16的一维数组Entry[] table。


2、map.put(key1,value1)


调用Key1所在类的hashCode()计算key1哈希值,得到Entry数组中存放的位置                   ---比较存放位置

如果此位置为空,此时key1-value1添加成功 *情况1,添加成功*

此位置不为空(以为此位置存在一个或多个数据(以链表形式存在)),比较key1和已存在的数据的哈希值: --比较哈希值

如果key1的哈希值与存在数据哈希值都不相同,此时key1-value1添加成功 *情况2,添加成功*

如果key1的哈希值与某一存在数据(key2,value2)相同,继续调用key1类的equals(key2)方法 --equals比较

如果equals()返回false,此时key1-value1添加成功 *情况3,添加成功*

如果equals()返回true,此时value1替换value2 *情况4,更新原有key的值*

情况2和情况3状态下,key1-value1和原来的数据以链表方式存储。

添加过程中会涉及扩容,超出临界值(存放位置非空)时扩容。默认扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。




三、HashMap在Jdk8之后实现原理


1、HashMap map = new HashMap()


底层没创建一个长度为16的数组,而是在首次调用put()方法时,底层创建长度为16的数组。


2、map.put(key1,value1)


final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;//首次put,创建长度为16的数组
if ((p = tab[i = (n - 1) & hash]) == null)// 需要插入数据位置为空。注:[i = (n - 1) & hash]找到当前key应插入的位置
tab[i] = newNode(hash, key, value, null); //*情况1*
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//*情况4*
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//红黑树情况
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);//*情况2、3*
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//*情况4*
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

3、map.entrySet()


返回一个Set集合


public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

4、map.get(ket)


返回key对应的value值。


public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

5、常见参数:


DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16


DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75


threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12


TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8


MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64


四、涉及的基础知识


位运算符用来对二进制位进行操作,Java中提供了如下表所示的位运算符:位运算符中,除 ~ 以外,其余均为二元运算符。


操作数只能为整型和字符型数据。


C语言中六种位运算符:


<<左移


>>右移


| 按位或


& 按位与


~取反


^ 按位异或


左移符号<<:向左移动若干位,高位丢弃,低位补零,对于左移N位,就等于乘以2^n


带符号右移操作>>:向右移动若干位,低位进行丢弃,高位按照符号位进行填补,对于正数做右移操作时,高位补充0;负数进行右移时,高位补充1


不带符号的右移操作>>>:与右移操作类似,高位补零,低位丢弃,正数符合规律,负数不符合规律


键(key)经过hash函数得到的结果作为地址去存放当前的键值对(key-value)(这个是hashmap的存值方式),但是却发现该地址已经有人先来了,一山不容二虎,就会产生冲突。这个冲突就是hash冲突了。


简单来说:两个不同对象的hashCode相同,这种现象称为hash冲突。


HashMap的Put方法在第2、3情况添加前会产生哈希冲突,HashMap采用的链地址法(将所有哈希地址相同的都链接在同一个链表中 ,因而查找、插入和删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况)解决哈希冲突。


五、相关面试问题


1、HashMap原理?


见上


2、HashMap初始化时阈值默认为12(加载因子为0.75),会使HashMap提前进行扩容,那为什么不在HashMap满的时候再进行扩容?


若加载因子越大,填满的元素越多,好处是,空间利用率高了,但冲突的机会加大了.链表长度会越来越长,查找效率降低。
反之,加载因子越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)
冲突的机会越大,则查找的成本越高. 因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷.
这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷.
如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。


3、什么是哈希冲突?如何解决?


4、并发集合


以下均为java.util.concurrent - Java并发工具包中的同步集合


4.1、ConcurrentHashMap 支持完全并发的检索和更新,所希望的可调整并发的哈希表。此类遵守与 Hashtable 相同的功能规范,并且包括对应于 Hashtable 的每个方法的方法版本。不过,尽管所有操作都是线程安全的,但检索操作不必锁定,并且不支持以某种防止所有访问的方式锁定整个表。此类可以通过程序完全与 Hashtable 进行互操作,这取决于其线程安全,而与其同步细节无关。


4.2、ConcurrentSkipListMap 是基于跳表的实现,也是支持key有序排列的一个key-value数据结构,在并发情况下表现很好,是一种空间换时间的实现,ConcurrentSkipListMap是基于一种乐观锁的方式去实现高并发。


4.3、ConCurrentSkipListSet (在JavaSE 6新增的)提供的功能类似于TreeSet,能够并发的访问有序的set。因为ConcurrentSkipListSet是基于“跳跃列表(skip list)”实现的,只要多个线程没有同时修改集合的同一个部分,那么在正常读、写集合的操作中不会出现竞争现象。


4.4、CopyOnWriteArrayList 是ArrayList 的一个线程安全的变形,其中所有可变操作(添加、设置,等等)都是通过对基础数组进行一次新的复制来实现的。这一般需要很大的开销,但是当遍历操作的数量大大超过可变操作的数量时,这种方法可能比其他替代方法更 有效。在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时,它也很有用。“快照”风格的迭代器方法在创建迭代器时使用了对数组状态的引用。此数组在迭代器的生存期内绝不会更改,因此不可能发生冲突,并且迭代器保证不会抛出 ConcurrentModificationException。自创建迭代器以后,迭代器就不会反映列表的添加、移除或者更改。不支持迭代器上更改元素的操作(移除、设置和添加)。这些方法将抛出 UnsupportedOperationException。


4.5、CopyOnWriteArraySet 线程安全的无序的集合,可以将它理解成线程安全的HashSet。有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet;但是,HashSet是通过“散列表(HashMap)”实现的,而CopyOnWriteArraySet则是通过“动态数组(CopyOnWriteArrayList)”实现的,并不是散列表。


4.6、ConcurrentLinkedQueue 是一个基于链接节点的、无界的、线程安全的队列。此队列按照 FIFO(先进先出)原则对元素进行排序,队列的头部 是队列中时间最长的元素。队列的尾部 是队列中时间最短的元素。新的元素插入到队列的尾部,队列检索操作从队列头部获得元素。当许多线程共享访问一个公共 collection 时,ConcurrentLinkedQueue 是一个恰当的选择,此队列不允许 null 元素。


注:ArrayList和HashMap是非并发集合,迭代时不能进行修改和删除操作

注:CopyOnWriteArrayList和CopyOnWriteArraySet,最适合于读操作通常大大超过写操作的情况


5、线程安全集合及实现原理?


5.1 早期线程安全的集合


Vector:作为Collection->List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储


HashTable:作为Map古老的实现类;线程安全的,效率低;不能存储null的key和value(Properties为其子类:常用来处理配置文件。key和value都是String类型)


5.2 Collections包装方法


Vector和HashTable被弃用后,它们被ArrayList和HashMap代替,但它们不是线程安全的,所以Collections工具类中提供了相应的包装方法把它们包装成线程安全的集合


List<E> synArrayList = Collections.synchronizedList(new ArrayList<E>());

Set<E> synHashSet = Collections.synchronizedSet(new HashSet<E>());

Map<K,V> synHashMap = Collections.synchronizedMap(new HashMap<K,V>());

...

5.3 java.util.concurrent包中的集合


ConcurrentHashMap和HashTable都是线程安全的集合,它们的不同主要是加锁粒度上的不同。HashTable的加锁方法是给每个方法加上synchronized关键字,这样锁住的是整个Table对象。而ConcurrentHashMap是更细粒度的加锁
在JDK1.8之前,ConcurrentHashMap加的是分段锁,也就是Segment锁,每个Segment含有整个table的一部分,这样不同分段之间的并发操作就互不影响
JDK1.8对此做了进一步的改进,它取消了Segment字段,直接在table元素上加锁,实现对每一行进行加锁,进一步减小了并发冲突的概率


CopyOnWriteArrayList和CopyOnWriteArraySet
它们是加了写锁的ArrayList和ArraySet,锁住的是整个对象,但读操作可以并发执行


除此之外还有ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedQueue、ConcurrentLinkedDeque等,至于为什么没有ConcurrentArrayList,原因是无法设计一个通用的而且可以规避ArrayList的并发瓶颈的线程安全的集合类,只能锁住整个list,这用Collections里的包装类就能办到


6、HashMap和hashTable的区别?


HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value


Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value


7、hashCode的作用?如何重载hashCode方法?


hashCode的存在主要是用于查找的快捷性,如Hashtable,HashMap等,hashCode是用来在散列存储结构中确定对象的存储地址的;如果两个对象相同,就是适用于equals(Java.lang.Object) 方法,那么这两个对象的hashCode一定要相同;如果对象的equals方法被重写,那么对象的hashCode也尽量重写,并且产生hashCode使用的对象,一定要和equals方法中使用的一致,否则就会违反上面提到的第2点;两个对象的hashCode相同,并不一定表示两个对象就相同,也就是不一定适用于equals(java.lang.Object)方法,只能够说明这两个对象在散列存储结构中,如Hashtable,他们“存放在同一个篮子里”。


总结:再归纳一下就是hashCode是用于查找使用的,而equals是用于比较两个对象的是否相等的。


作者:求求了瘦10斤吧
链接:https://juejin.cn/post/7039596855012884510

收起阅读 »