注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Kotlin浅析之Contract

在进行kotlin的项目开发中,我们依赖kotlin语法糖相比java可以更高效地产出,kotlin的彩蛋众多,这篇文章着重跟大家聊一聊Contract,其实Contract在官方函数中其实也有被多次使用,比如我们常用的let、apply、also、isNul...
继续阅读 »

在进行kotlin的项目开发中,我们依赖kotlin语法糖相比java可以更高效地产出,kotlin的彩蛋众多,这篇文章着重跟大家聊一聊Contract,其实Contract在官方函数中其实也有被多次使用,比如我们常用的let、apply、also、isNullOrEmpty等函数:


@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block(this)
return this
}

@kotlin.internal.InlineOnly
public inline fun CharSequence?.isNullOrEmpty(): Boolean {
contract {
returns(false) implies (this@isNullOrEmpty != null)
}

return this == null || this.length == 0
}

接下来,我们来了解一下contract到底是什么以及怎么用?


一、Contract是什么?


contract翻译过来意思是"契约",那么既然是"契约",约定的双方又是谁?


“我”和"你",心连心,同住地球村?搞叉了,再来!


契约的双方实际上是"开发者'和"编译器" ,我们都知道,kotlin编译器有着智能推断自动类型转换的功能。但实际上,它的智能推断有时候并不那么智能,下面会讲到,而官方为开发者预留了一个通道去与编译器沟通,这就是contract存在的意义。


二、Contact怎么用?


首先,我们定义一个String常规的判空扩展函数


/**
* 字符串扩展函数判空,常规方式
* @receiver String? 接收类型
* @return Boolean 是否为空
*/
fun String?.isNullOrEmptyWithoutContract(): Boolean {
return this == null || this.isEmpty()
}

然后,我们来调用看看


/**
* 问题示例1 使用自定义函数判空,编译器无感知
* @param name String? 传入的姓名字符串
*/
private fun problemNull(name: String?) {
// 用常规方式的自定义扩展函数对局部变量判空
if (!name.isNullOrEmptyWithoutContract()) {
//name.length报错,自定义扩展函数中的判空逻辑未同步到编译器 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?
Log.d(TAG, "name:$name,length:${name.length}")
}
}

结果,在函数内部调用外部自定义字符串判空函数,不起作用,这是因为编译器并不知道这种间接的判空是不是有效的,而这时候,我们请出contract来表演看看:


判空扩展函数contract returns改造


/**
* 字符串扩展函数判空,contract方式
* @receiver String? 接收类型
* @return Boolean 是否为空
*/
@ExperimentalContracts
fun String?.isNullOrEmptyWithContract(): Boolean {
contract {
returns(false) implies (this@isNullOrEmptyWithContract != null)
}
return this == null || this.isEmpty()
}

/**
* 解决问题1 自定义函数判空后结果同步编译器
* @param name String? 传入的姓名字符串
*/
@ExperimentalContracts
fun fixProblemNull(name: String?) {
// 用contract方式的自定义扩展函数对局部变量判空
if (!name.isNullOrEmptyWithContract()) {
//运行正常
Log.d(TAG, "name:$name,length:${name.length}")
}
}

可以看到,判空扩展函数加入了contract之后,编译器就懂事了,但编译器是如何懂事的呢?contract内部到底跟编译器说了什么悄悄话?咱们先分析下判空扩展函数的代码


contract {
returns(false) implies (this@isNullOrEmptyWithContract != null)
}

contract所包裹的语句,实际上就是我们要告诉编译器的逻辑,这里的returns(false) 代表当前函数isNullOrEmptyWithContract()的返回值也就是 return this == null || this.isEmpty()如果是false,那么会告知编译器implies后面的表达式也就是this@isNullOrEmptyWithContract != null成立,也就是调用者对象String不为空,那么后面在打印name.length的时候编译器就知道name不为空拉,这就是开发者与编译器的契约!


其次,我们发现除了resturns的用法外,常用的apply扩展函数里面的contract是callsInPlace形式,那么callsInPlace又是什么意思?


/**
* 定义apply函数,常规方式
* @receiver T 接收类型
* @param block [@kotlin.ExtensionFunctionType] Function1<T, Unit> 函数入参
* @return T 返回类型
*/
fun <T> T.applyWithoutContract(block: T.() -> Unit): T {
block()
return this
}

/**
* 问题示例2 函数执行变量初始化,编译器无感知
*/
fun problemInit() {
var name: String
// 用常规方式的自定义扩展函数对局部变量赋值
applyWithoutContract {
// 编译器实际上不知道这个函数入参有没有被调用
name = "WenChangJi"
}
// 报错 'Variable 'name' must be initialized'
Log.d(TAG, "name:${name}")
}

这里我们给间接给局部变量name去赋值,但是后续使用时编译器报错声称name没有初始化,采取以往经验,我们加入contract去改造试试:


/**
*
* 定义apply函数,contract方式
* @receiver T 接收类型
* @param block [@kotlin.ExtensionFunctionType] Function1<T, Unit> 函数入参
* @return T 返回类型
*/
@ExperimentalContracts
fun <T> T.applyWithContract(block: T.() -> Unit): T {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
block()
return this
}

/**
* 解决问题2 函数执行变量初始化后同步编译器
*/
@ExperimentalContracts
fun fixProblemInit() {
var name: String
// 用contract方式的自定义扩展函数对局部变量赋值
applyWithContract {
// applyWithContract内部契约告知编译器,这里绝对会调用一次的,也就一定会初始化
name = "WenChangJi"
}
// 运行正常
Log.d(TAG, "name:${name}")

}

这里我们并没有采用returns告知编译器在满足什么条件下什么表达式成立,而是采用callsInPlace方式告知编译器入参函数block的调用规则,callsInPlace(block, InvocationKind.EXACTLY_ONCE)即是告诉编译器block在内部会被调用一次,也就是后续调用时的语句name = "WenChangJi"会被调用一次进行赋值,那么在使用name时编译器就不会说没有初始化之类的问题拉!


callsInPlace内部次数的常量值由以下几种:



























常量值含义
- InvocationKind.AT_MOST_ONCE最多调用一次
InvocationKind.AT_LEAST_ONCE最少调用一次
InvocationKind.EXACTLY_ONCE调用一次
InvocationKind.UNKNOWN未知

最后,咱们这边文章只是讲解了Contract是什么和怎么用的部分场景,还有更多的场景以及具体的原理有兴趣的同学可以深挖~


感谢大家的观看!!!


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

如果有机会,你会选择脱产学习深造吗?

因为有同学让我帮忙写封情书,所以我最近在看朱生豪写给他爱人宋清如的情书,其中有这样一句:要是有人问你,你愿意做快乐的猪呢,还是愿意做苦恼的哲学家?你就回答:我愿意做快乐的哲学家,这样可以显出你的聪明。还有一个提问,当你手上拿着一杯水,接下来你要做什么?答案很简...
继续阅读 »

因为有同学让我帮忙写封情书,所以我最近在看朱生豪写给他爱人宋清如的情书,其中有这样一句:

要是有人问你,你愿意做快乐的猪呢,还是愿意做苦恼的哲学家?你就回答:我愿意做快乐的哲学家,这样可以显出你的聪明。

还有一个提问,当你手上拿着一杯水,接下来你要做什么?

答案很简单,做你自己该做的事情,和水没关。

对于这个提问的回答,其实我也是一样的。“如果有机会,你会选择脱产学习吗?”,脱离现实的题目,也不用进入这个设定进行回答,反正也没有什么参考意义,这个机会真出现的时候,情况往往是复杂的,你之前想的再齐全,真到了选择的时候,你还是可能做出截然不同的选择,最后喊上一句,真香。

在我看来,这个机会,就是我手里的这杯水,我如果不渴,为什么要喝水?该干什么你就干什么。

我相信很多同学面对这个问题,都会选择脱产学习。如果是一个穿越的故事背景,比如你回到了你大一刚入学的时候,说真的,都已经预知未来了,我还学习干什么,无限种可能,光是想想,都能写成万字爽文。如果是父母支持,读研深造的故事背景,那我不一定愿意,尽管大学生活让人向往,但我要考试呀,我还是要学习一堆我觉得可能没什么用的东西,然后准备考试,可能还要给导师打工,我不是很想做。我真正想做的是,自由的学习我想学习的内容,然后结束后,一鸣惊人,从此走上人生巅峰之类的。

你看,在脱产这个选择下,我想过的都是爽番的生活。

而站在不选择脱产的角度来看的话,多是出于现实的角度考虑。我也有些朋友天马行空说是要花钱考研,说真的,在互联网行业中,其实学历的影响并不大,从本科提到硕士,收益并不高,主要是图个自由散漫的生活状态、贪恋校园年轻帅气的师兄学长,本质上还是逃避心理。

真想对工作有用,就下班回到家里,充电学习工作上用到的技能,但很不巧,这些技能在大学往往并不教授,还是要靠自学。至于找对象,这个理由我觉得是成立的,你可以因此去脱产学习,我支持。

可是人生真的会有这样的机会,让你能够脱产学习吗?

大概率是不会主动出现的,但也许我们可以主动创造出来。如果成家了,抛家弃子,一心搞梦想,《月亮与六便士》的翻版。没成家的,工作攒几年积蓄,然后离职过个自由的生活。想学习?想学什么学什么,想运动,办了卡可以天天去。可是我们真的会这么自律吗?

其实想一想我们过往的寒暑假,有几次是真的好好学习度过的呢?那不就是我们自由生活的缩影吗?也许前几天还能自律,后几天就开始散漫,散漫久了,又开始想学习,就这样反复横跳,很可能也做不出什么。所以才感叹,有些人真的很厉害,为了梦想,拼尽全力。你以为你差的是一个梦想,但你的思想觉悟却可能已经差了一大截。

其实我们人生中还会遇到很多这种两难的选择,是考研还是工作?是学后端还是前端?是接受还是拒绝?是分手还是继续?等等

我认为两难选择中,真正难的并不是问题本身,而是有问题的这个人,当然很多时候,这个人就是我自己。人并不是一个绝对理性的生物,人是很容易被各种情绪压的不能动弹。我们很可能遇到过这种情况,当有好友向我们咨询问题的时候,你发现,不管你给他什么建议,他都能找到很好的理由来解释为什么这个方法不行。

有的时候我觉得这种人就是在作,左右都不行,然后还逼逼赖赖,就在这里不做决定,耽搁生命,让事情越来越糟糕。不过转念一想,我们很多时候不也是这样的吗?

真正让我们无法做出正确决定的是我们的恐惧。基本上所有的恐惧都是害怕会失去某些东西。

要突破这种左右为难的困境其实也很简单,那就是遵循内心的声音,还有就是坦然接受两个选择的任何一个,或者两个都不选。这一点之所以重要,是因为处在两难的境地中,你往往会相信你就只有两个选择,再没有第三条路可以选。什么样的选择是最好的,答案就在心中,可我们听不到,是因为恐惧、焦虑、压力等已经扰乱了我们的思想。如果我们愿意两个选择中选择一个,或都不选的话,我们就能得到平静、平息恐惧,并听到自己内心的声音。两难的境地会让人既排斥现有的选择,同时又不肯放弃它们。这也就是让我们陷入困境的原因。

无论做出什么样的选择,有一件事是确定的,那就是跟随着自己的心去做的事,不管造成多大的骚动,都将会为每个人带来好的影响。所以每一次左右为难的处境,你都可以理解为考验你追寻自我的决心。

但还要记得一件事情,那就是:拖延做决定是最差的决定!

每个人都害怕受伤,为了避免让自己受伤,也为了避免让他人受伤(当然往往这是个借口),人总是拖到不得不做决定的时候才做决定,最后到了不得不决定的时候,于人于己都是更大的伤害,于是又懊恼过往的时光怎么不早早做出决定,但下次同样的问题出现时又是同样的行为模式,人总是这样不长教训,可笑而又无奈。

有的时候我们即便不断地听到那个内心的声音,我们还是会把它掩盖,用其他事物转移自己的注意力,我们还会寄希望于寻找所谓的真理,希望能够降维解决当下的难题,然后自己就会有做出决定的勇气,怀着这样一种不切实际的幻想投入所谓的行动,虽然在其他方面真的有所长进,实际上也只是不断地缓解自己拖延决定的痛苦而已。

你说有什么解决方法吗?没有。你不张开嘴,说出那句话,你不做出那个小小的行动,一切都是于事无补,一切都不会有大的改变,一切的想法都只是镜花水月。

我们常说人生只有一次,我们常说开心快乐就好,我们希望这样的想法能给自己勇气,人们常以为勇气就是有了它你就不恐惧的东西,但勇气从来都不是无知者无畏,而是当你还未开始就已知道自己会输,可你依然要去做,而且无论如何都要把它坚持到底。引用《存在的勇气》:

什么是勇气?概言之,就是不顾非存在的威胁而对存在进行自我肯定。

有了勇气问题就能解决吗?问题会尘埃落地,但不一定就能如我们期望那样解决,“你很少能赢,但有时也会”,而你所做的,不过是对无力的自己的一点抗争,我们能掌控的从来不是事情的结果,而是那个能够选择做出抗争的自己,听起来有些无奈,但仅是如此,我们也拥有了自由。

不为他人,不为自己,仅是为了与被推着自己往前走的命运抗争,仅为了证明你有掌控命运一角的可能,仅为了守住一份自由,你也值得向前迈出一步。

作者:冴羽

来源:juejin.cn/post/7103474675455361037

收起阅读 »

在uni-app中使用微软的文字转语音服务

前言尝试过各种TTS的方案,一番体验下来,发现微软才是这个领域的王者,其Azure文本转语音服务的转换出的语音效果最为自然,但Azure是付费服务,注册操作付费都太麻烦了。但在其官网上竟然提供了一个完全体的演示功能,能够完完整整的体验所有角色语音,说话风格.....
继续阅读 »

前言

尝试过各种TTS的方案,一番体验下来,发现微软才是这个领域的王者,其Azure文本转语音服务的转换出的语音效果最为自然,但Azure是付费服务,注册操作付费都太麻烦了。但在其官网上竟然提供了一个完全体的演示功能,能够完完整整的体验所有角色语音,说话风格...


但就是不能下载成mp3文件,所以有一些小伙伴逼不得已只好通过转录电脑的声音来获得音频文件,但这样太麻烦了。其实,能在网页里看到听到的所有资源,都是解密后的结果。也就是说,只要这个声音从网页里播放出来了,我们必然可以找到方法提取到音频文件。

本文就是记录了这整个探索实现的过程,请尽情享用~

本文大部分内容写于今年年初一直按在手里未发布,我深知这个方法一旦公之于众,可能很快会迎来微软的封堵,甚至直接取消网页体验的入口和相关接口。

解析Azure官网的演示功能

使用Chrome浏览器打开调试面板,当我们在Azure官网中点击播放功能时,可以从network标签中监控到一个wss://的请求,这是一个websocket的请求。


两个参数

在请求的URL中,我们可以看到有两个参数分别是AuthorizationX-ConnectionId


有意思的是,第一个参数就在网页的源码里,使用axios对这个Azure文本转语音的网址发起get请求就可以直接提取到


const res = await axios.get("https://azure.microsoft.com/en-gb/services/cognitive-services/text-to-speech/");

const reg = /token: \"(.*?)\"/;

if(reg.test(res.data)){
  const token = RegExp.$1;
}

通过查看发起请求的JS调用栈,加入断点后再次点击播放



可以发现第二个参数X-ConnectionId来自一个createNoDashGuid的函数

this.privConnectionId = void 0 !== t ? t : s.createNoDashGuid(),

这就是一个uuid v4格式的字符串,nodash就是没有-的意思。

三次发送

请求时URL里的两个参数已经搞定了,我们继续分析这个webscoket请求,从Message标签中可以看到


每次点击播放时,都向服务器上报了三次数据,明显可以看出来三次上报数据各自的作用

第一次的数据:SDK版本,系统信息,UserAgent

Path: speech.config
X-RequestId: 818A1E398D8D4303956D180A3761864B
X-Timestamp: 2022-05-27T16:45:02.799Z
Content-Type: application/json

{"context":{"system":{"name":"SpeechSDK","version":"1.19.0","build":"JavaScript","lang":"JavaScript"},"os":{"platform":"Browser/MacIntel","name":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36","version":"5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36"}}}

第二次的数据:转语音输出配置,从outputFormat可以看出来,最终的音频格式为audio-24khz-160kbitrate-mono-mp3,这不就是我们想要的mp3文件吗?!

Path: synthesis.context
X-RequestId: 091963E8C7F342D0A8E79125EA6BB707
X-Timestamp: 2022-05-27T16:48:43.340Z
Content-Type: application/json

{"synthesis":{"audio":{"metadataOptions":{"bookmarkEnabled":false,"sentenceBoundaryEnabled":false,"visemeEnabled":false,"wordBoundaryEnabled":false},"outputFormat":"audio-24khz-160kbitrate-mono-mp3"},"language":{"autoDetection":false}}}

第三次的数据:要转语音的文本信息和角色voice name,语速rate,语调pitch,情感等配置

Path: ssml
X-RequestId: 091963E8C7F342D0A8E79125EA6BB707
X-Timestamp: 2022-05-27T16:48:49.594Z
Content-Type: application/ssml+xml

<speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" xmlns:emo="http://www.w3.org/2009/10/emotionml" version="1.0" xml:lang="en-US"><voice name="zh-CN-XiaoxiaoNeural"><prosody rate="0%" pitch="0%">我叫大帅,一个热爱编程的老程序猿</prosody></voice></speak>

接收的二进制消息

既然从前三次上报的信息已经看出来返回的格式就是mp3文件了,那么我们是不是把所有返回的二进制数据合并就可以拼接成完整的mp3文件了呢?答案是肯定的!

每次点击播放后接收的所有来自websocket的消息的最后一条,都有明确的结束标识符



turn.end代表转换结束!

用Node.js实现它

既然都解析出来了,剩下的就是在Node.js中重新实现这个过程。

两个参数

  1. Authorization,直接通过axios的get请求抓取网页内容后通过正则表达式提取

const res = await axios.get("https://azure.microsoft.com/en-gb/services/cognitive-services/text-to-speech/");

const reg = /token: \"(.*?)\"/;

if(reg.test(res.data)){
  const Authorization = RegExp.$1;
}
  1. X-ConnectionId,直接使用uuid库即可

//npm install uuid
const { v4: uuidv4 } = require('uuid');

const XConnectionId = uuidv4().toUpperCase();

创建WebSocket连接

//npm install nodejs-websocket
const ws = require("nodejs-websocket");

const url = `wss://eastus.tts.speech.microsoft.com/cognitiveservices/websocket/v1?Authorization=${Authorization}&X-ConnectionId=${XConnectionId}`;
const connect = ws.connect(url);

三次发送

第一次发送

function getXTime(){
  return new Date().toISOString();
}

const message_1 = `Path: speech.config\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/json\r\n\r\n{"context":{"system":{"name":"SpeechSDK","version":"1.19.0","build":"JavaScript","lang":"JavaScript","os":{"platform":"Browser/Linux x86_64","name":"Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0","version":"5.0 (X11)"}}}}`;

connect.send(message_1);

第二次发送

const message_2 = `Path: synthesis.context\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/json\r\n\r\n{"synthesis":{"audio":{"metadataOptions":{"sentenceBoundaryEnabled":false,"wordBoundaryEnabled":false},"outputFormat":"audio-16khz-32kbitrate-mono-mp3"}}}`;

connect.send(message_2);

第三次发送

const SSML = `
  <speak xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="http://www.w3.org/2001/mstts" xmlns:emo="http://www.w3.org/2009/10/emotionml" version="1.0" xml:lang="en-US">
      <voice name="zh-CN-XiaoxiaoNeural">
          <mstts:express-as style="general">
              <prosody rate="0%" pitch="0%">
              我叫大帅,一个热爱编程的老程序猿
              </prosody>
          </mstts:express-as>
      </voice>
  </speak>
  `

const message_3 = `Path: ssml\r\nX-RequestId: ${XConnectionId}\r\nX-Timestamp: ${getXTime()}\r\nContent-Type: application/ssml+xml\r\n\r\n${SSML}`

connect.send(message_3);

接收二进制消息拼接mp3

当三次发送结束后我们通过connect.on('binary')监听websocket接收的二进制消息。

创建一个空的Buffer对象final_data,然后将每一次接收到的二进制内容拼接到final_data里,一旦监听到普通文本消息中包含Path:turn.end标识时则将final_data写入创建一个mp3文件中。

let final_data=Buffer.alloc(0);
connect.on("text", (data) => {
  if(data.indexOf("Path:turn.end")>=0){
      fs.writeFileSync("test.mp3",final_data);
      connect.close();
  }
})
connect.on("binary", function (response) {
  let data = Buffer.alloc(0);
  response.on("readable", function () {
      const newData = response.read()
      if (newData)data = Buffer.concat([data, newData], data.length+newData.length);
  })
  response.on("end", function () {
      const index = data.toString().indexOf("Path:audio")+12;
      final_data = Buffer.concat([final_data,data.slice(index)]);
  })
});

这样我们就成功的保存出了mp3音频文件,连Azure官网都不用打开!

命令行工具

我已经将整个代码打包成一个命令行工具,使用非常简单

npm install -g mstts-js
mstts -i 文本转语音 -o ./test.mp3

已全部开源: github.com/ezshine/mst…

在uni-app中使用

新建一个云函数

新建一个云函数,命名为mstts


由于mstss-js已经封装好了,只需要在云函数中npm install mstts-js然后require即可,代码如下

'use strict';
const mstts = require('mstts-js')

exports.main = async (event, context) => {
  const res = await mstts.getTTSData('要转换的文本','CN-Yunxi');
 
  //res为buffer格式
});

下载播放mp3文件

要在uniapp中播放这个mp3格式的文件,有两种方法

方法1. 先上传到云存储,通过云存储地址访问

exports.main = async (event, context) => {
  const res = await mstts.getTTSData('要转换的文本','CN-Yunxi');
 
  //res为buffer格式
  var uploadRes = await uniCloud.uploadFile({
      cloudPath: "xxxxx.mp3",
      fileContent: res
  })
   
  return uploadRes.fileID;
});

前端用法:

uniCloud.callFunction({
  name:"mstts",
  success:(res)=>{
      const aud = uni.createInnerAudioContext();
      aud.autoplay = true;
      aud.src = res;
      aud.play();
  }
})
  • 优点:云函数安全

  • 缺点:文件上传到云存储不做清理机制的话会浪费空间

方法2. 利用云函数的URL化+集成响应来访问

这种方法就是直接将云函数的响应体变成一个mp3文件,直接通过audio.src赋值即可访问`

exports.main = async (event, context) => {
const res = await mstts.getTTSData('要转换的文本','CN-Yunxi');

return {
mpserverlessComposedResponse: true,
isBase64Encoded: true,
statusCode: 200,
headers: {
'Content-Type': 'audio/mp3',
'Content-Disposition':'attachment;filename=\"temp.mp3\"'
},
body: res.toString('base64')
}
};

前端用法:

const aud = uni.createInnerAudioContext();
aud.autoplay = true;
aud.src = 'https://ezshine-274162.service.tcloudbase.com/mstts';
aud.play();
  • 优点:用起来很简单,无需保存文件到云存储

  • 缺点:URL化后的云函数如果没有安全机制,被抓包后可被其他人肆意使用

作者:大帅老猿
来源:juejin.cn/post/7103720862221598757

收起阅读 »

百度95后程序员删库跑路被判刑,原因竟是工作内容变动及对领导不满

删库一时爽,后果很严重!近日,记者自北京裁判文书网上获悉,百度某“95后”校招员工金某某在任职期间,私自建立隧道进入数据库“删表”。最终因犯破坏计算机信息系统罪,被判处有期徒刑九个月。而究其动机,居然是出于对工作内容变动及领导的不满。为了显示自己在项目中的重要...
继续阅读 »
删库一时爽,后果很严重!

近日,记者自北京裁判文书网上获悉,百度某“95后”校招员工金某某在任职期间,私自建立隧道进入数据库“删表”。最终因犯破坏计算机信息系统罪,被判处有期徒刑九个月。



而究其动机,居然是出于对工作内容变动及领导的不满。为了显示自己在项目中的重要性,遂对平台数据进行破坏。事后,百度职业道德建设部很快接到了内部安全部门的举报,开始进行数据恢复及调查。同月,民警在百度将该金某某抓获。

来看详情——

“95后”员工愤然删库

“删库跑路”往往只是程序员们之间排解压力的调侃之语,但当更注重实现自我价值的“Z世代”们走上工作岗位,一时头脑发热删库泄愤,最终导致严重后果。

据裁定书显示,金某某出生于1996年,大学文化,在“删库”被抓时还不满25岁。据金某某供述,他毕业后就在百度网讯科技有限公司工作,负责测试开发,工作内容是测试公司的平台与写程序。2020年8月,公司派其他员工来接手项目,金某某对该安排感到不满意。为了显示自己在这个项目里还有作用,就对平台的数据进行了破坏。

具体而言,金某某使用链接内网的工具,打通外部与公司服务器之间的链接,然后在家中使用手机登录隧道进入到公司内网服务器,用内网服务器做跳板去访问可视化项目服务器,分次将可视化项目程序数据库内的项目表进行了删除、锁定、修改。

每对数据库删除、锁定、修改一次,公司就需要修复一遍。在公司修好后,金某某再次进行删改。在多次“折腾”后,金某某主动申请更换部门,再未对数据库进行删改。

金某某的工作实际情况如何?对此,他同部门的同事赵某介绍,金某某通过校招入职百度,到公司后大部分时间在商业质量效能部部门工作,2020年10月23日被调到ACG部门。

商业质量效能部门自己开发了一套用于测试服务的系统,金某某当时参与了这套系统的开发,他将这套系统数据库的数据部分篡改、删除,导致系统的算法无法正常运行,得不到要的结论。赵某证实,金某某的行为在一段时间内影响了部门工作的正常开展。

删库造成严重后果

一时泄愤“删库”,还没等到“跑路”,金某某的行为就已被发现。

据安全工程师李某证实,2020年8月,百度商业质量效能部向其部门反馈ff.baidu-int.com平台数据库近期频繁被篡改,且数据库管理软件Adminer被不明人员连接,安全部门遂按照公司要求配合开展核查工作。经调查发现,事发时间段相关IP地址由金某某使用。

百度在线出具的数据库删除数据操作日志情况说明显示:结合业务反馈与安全排查共发现16次疑似恶意操作,其中10次操作关联到内部员工。

证人艾某则证实,2021年3月,其公司职业道德建设部接内部安全部门举报称,员工金某某在商业质量效能部任职期间,由于对部门领导不满,使用隧道违规从外网接入百度IDC并对商业质量效能部ff.baidu-int.com平台数据库内表进行清空、篡改、锁定,造成严重后果。

艾某介绍,金某某的行为一方面导致平台数据不一致或丢失,无法使用快捷操作功能,数据通过脚本回溯快速恢复,共计影响50个项目使用平台快捷操作能力;另一方面数据库的异常变化会带来用户对百度产品使用体验的误解,严重影响公司形象及经济效益。

“删库一时爽”,但恢复被删除数据则需要大量的人力和时间成本。经北京中海义信信息技术有限公司司法鉴定所鉴定,并对删除的表数据进行恢复,共计花费16300元。

2021年3月,北京市公安局海淀分局警务支援大队对涉案被破坏计算机信息系统服务器日志信息等数据进行远程提取。同日,民警在百度公司将金某某抓获。

被判有期徒刑9个月

年轻人犯错,总是很容易被原谅。在家属的帮助下,金某某赔偿百度7万元并获得了谅解,但仍将面临法律的惩处。

一审海淀区法院认为,被告人金某某违反国家规定,对计算机信息系统中存储的数据进行删除、修改操作,后果严重,其行为已构成破坏计算机信息系统罪,应予惩处。鉴于被告人金某某能够如实供述自己的主要犯罪事实,并在家属的帮助下赔偿百度公司经济损失并获得谅解,依法对其从轻处罚并适用缓刑。

一审法院判决:金某某犯破坏计算机信息系统罪,判处有期徒刑九个月,缓刑一年。

对此,金某某提出上诉称,自己做的的确不对,但其行为没有造成这么大的损失,修复数据16300元不是必要费用,故不构成犯罪。

其辩护人亦要求改判金某某无罪或裁定发回重审,认为百度委托鉴定无必要,且并非由公安机关委托,费用不应被认定为经济损失。金某某具有自首情节,涉案行为情节轻微显著,且社会危害性不大。

对此,二审法院指出,破坏计算机信息系统犯罪中“经济损失”的计算范围,具体包括危害计算机信息系统犯罪行为给用户直接造成的经济损失,以及用户为恢复数据、功能而支出的必要费用。本案中,百度公司在其相关数据库被金某某破坏后,委托北京中海义信信息技术有限公司司法鉴定所进行恢复相关数据,并无不妥。

而对于金某某是否具有自首情节,法院指出,金某某系被公安民警在百度公司抓获到案,其行为不符合自首的法律规定。金某某对计算机信息系统中存储的数据进行删除、修改操作,后果严重,不属于情节显著轻微的情况。考虑到金某某如实供述并获得谅解,已对其从轻量刑。最终,二审法院裁定驳回上诉,维持原判。

“删库跑路”者屡现

“别惹程序员!”业内删库报复的案例不断增加。

最有名的案例,莫过于微盟集团程序员的“删库”。2020年2月,有商户发现微盟的SaaS业务服务突然宕机,微盟旗下300万商户的线上业务全部停止,商铺后台的所有数据被清零。此后,微盟集团发布公告解释这次事故,称数据库遭遇“人为破坏”。

据事后了解,该员工一直深陷网络贷,还曾有过轻生举动。最终,该员工被判有期徒刑6年,自称系因生活不如意、无力偿还网贷等个人原因导致作出“删库”行为。

据中国裁判文书网公布的案例,2018年6月,链家数据库管理员韩某利用其担任并掌握该公司财务系统“root”权限的便利,登录该公司财务系统,并将系统内的财务数据及相关应用程序删除,致使该公司财务系统彻底无法访问。

对于韩某的删库行为,同样是因积怨所致。韩冰于2018年2月开始在公司负责财务系统维护,但5月被调整至技术保障部,工作地点也产生变动。韩冰对组织调整有意见,觉得自己不受重视,这也是他后来删库的重要原因之一。最终,韩某被判有期徒刑七年。

另外,据上海市杨浦区法院近期披露的一则刑事判决书显示,92年出生的程序员录某负责京东到家平台的代码研发工作。在2021年6月离职后,录某未经许可用本人账户登录服务器的代码控制平台,将其在职期间所写京东到家平台优惠券、预算系统以及补贴规则等代码删除,导致原定按期上线项目延后。与金某某情况类似,录某在积极赔偿后取得了公司谅解,最终被判有期徒刑十个月。

除了知名大厂外,小公司程序员“删库泄愤”的情况发生更加频繁。例如,2021年10月公布的一份裁判文书显示,某集团邯郸客运总站售票系统计算机编程人员因薪酬等问题离职心生不满,遂利用自己的苹果笔记本电脑远程接入网上自助售票系统的接口地址,删除了售票员表、网络售票表、结算单表、售票数据表、手持机表等,造成该站当日约5小时所有售票渠道全部无法正常使用,当日部分售票数据丢失。

放眼海外,今年年初,知名开源库Faker.js和colors.js的作者MarakSquires主动恶意破坏了自己的项目,不仅“删库跑路”,还注入了导致程序死循环的恶意代码,影响甚众。

对于企业来说,程序员删库跑路带来的不止是经济上的损失,还有顾客信任度的丧失以及对企业形象的负面影响。因此,公司在平时就应完善相应的安全机制和管理制度,做好备份恢复和权限管理,防患于未然。而对于程序员来说,“删库一时爽”,但短暂的宣泄情绪后,将面临的是法律的惩处。

来源:中国基金报记者 颜颖

收起阅读 »

Mac修改hosts,域名与ip绑定,vue Invalid Host header

在移动开发过程中,有时候需要使用域名进行访问(如微信网页开发)本地ip地址服务,或者使用域名访问本地ip地址服务等。这时候可以修改host进行实现。1. 修改host文件在命令终端,使用root用户修改host文件。域名使用root用户打开/etc/hosts...
继续阅读 »

在移动开发过程中,有时候需要使用域名进行访问(如微信网页开发)本地ip地址服务,或者使用域名访问本地ip地址服务等。

这时候可以修改host进行实现。

1. 修改host文件

在命令终端,使用root用户修改host文件。域名使用root用户打开/etc/hosts host文件进行修改。添加
ip及对应的域名

$ sudo vi /etc/hosts
127.0.0.1       localhost
127.0.0.1 zhangguoyedeMacBook-Pro.local
255.255.255.255 broadcasthost
::1 localhost
::1 zhangguoyedeMacBook-Pro.local

# 在这里添加上ip及对应的域名并保存退出
#(这里假设你设置的是本机ip是 127.0.0.1 访问域名是 guoye.com)
127.0.0.1 guoye.com

2. 通过域名访问项目

现在可以在浏览器上访问你设置的域名guoye.com,跟直接通过ip访问127.0.0.1的内容是一致的。
通常你的项目会加上端口号,域名也需要加上端口号,如http://guoye.com:4201

3. vue (Invalid Host header)

在vue项目开发时,直接通过ip地址访问正常,但通过上面host域名方式访问,浏览器会显示一段文字:Invalid Host header
这是由于新版webpack-dev-server出于安全考虑,默认检查hostname,如果hostname 没有配置在内的,将中断访问。

解决方法:
vue.config.jsdevServer配置文件加上 disableHostCheck: true

devServer: {
port: 4201, // 端口配置
proxy: {
// 代理配置
},
disableHostCheck: true, // 这是由于新版的webpack-dev-server出于安全考虑,默认检查hostname,如果hostname 不是配置内的,将中断访问。
}

4. 手机端也通过域名进行访问

移动开发时,可以使用Charles软件进行代理。
此时手机端也能通过域名访问本机电脑的应用。

原文:https://segmentfault.com/a/1190000023077264

收起阅读 »

iOS-底层原理 02:alloc & init & new 源码分析

在分析alloc源码之前,先来看看一下3个变量 内存地址 和 指针地址 区别:分别输出3个对象的内容、内存地址、指针地址,下图是打印结果结论:通过上图可以看出,3个对象指向的是同一个内存空间,所以其内容 和 内存地址是相同的,但是对象的指针...
继续阅读 »

在分析alloc源码之前,先来看看一下3个变量 内存地址 和 指针地址 区别:


分别输出3个对象的内容、内存地址、指针地址,下图是打印结果


结论:通过上图可以看出,3个对象指向的是同一个内存空间,所以其内容 和 内存地址相同的,但是对象的指针地址是不同的

%p -> &p1:是对象的指针地址,
%p -> p1: 是对象指针指向的的内存地址

这就是本文需要探索的内容,alloc做了什么?init做了什么?

准备工作

alloc 源码探索

alloc + init 整体源码的探索流程如下


  • 【第一步】首先根据main函数中的LGPerson类的alloc方法进入alloc方法的源码实现(即源码分析开始),

  • 【第二步】跳转至_objc_rootAlloc的源码实现

  • 【第三步】跳转至callAlloc的源码实现

如上所示,在calloc方法中,当我们无法确定实现走到哪步时,可以通过断点调试,判断执行走哪部分逻辑。这里是执行到_objc_rootAllocWithZone

slowpath & fastpath

其中关于slowpathfastpath这里需要简要说明下,这两个都是objc源码中定义的,其定义如下



其中的__builtin_expect指令是由gcc引入的,
1、目的:编译器可以对代码进行优化,以减少指令跳转带来的性能下降。即性能优化
2、作用:允许程序员将最有可能执行的分支告诉编译器。
3、指令的写法为:__builtin_expect(EXP, N)。表示 EXP==N的概率很大。
4、fastpath定义中__builtin_expect((x),1)表示 x 的值为真的可能性更大;即 执行if 里面语句的机会更大
5、slowpath定义中的__builtin_expect((x),0)表示 x 的值为假的可能性更大。即执行 else 里面语句的机会更大
6、在日常的开发中,也可以通过设置来优化编译器,达到性能优化的目的,设置的路径为:Build Setting --> Optimization Level --> Debug --> 将None 改为 fastest 或者 smallest

cls->ISA()->hasCustomAWZ()

其中fastpath中的 cls->ISA()->hasCustomAWZ() 表示判断一个类是否有自定义的 +allocWithZone 实现,这里通过断点调试,是没有自定义的实现,所以会执行到 if 里面的代码,即走到_objc_rootAllocWithZone


【第四步】跳转至_objc_rootAllocWithZone的源码实现


【第五步】跳转至_class_createInstanceFromZone的源码实现,这部分是alloc源码的核心操作,由下面的流程图及源码可知,该方法的实现主要分为三部分
cls->instanceSize:计算需要开辟的内存空间大小
calloc:申请内存,返回地址指针
obj->initInstanceIsa:将 类 与 isa 关联


根据源码分析,得出其实现流程图如下所示:


alloc 核心操作

核心操作都位于calloc方法中

cls->instanceSize:计算所需内存大小

计算需要开辟内存的大小的执行流程如下所示


  • 1、跳转至instanceSize的源码实现

通过断点调试,会执行到cache.fastInstanceSize方法,快速计算内存大小

  • 2、跳转至fastInstanceSize的源码实现,通过断点调试,会执行到align16

  • 3、跳转至align16的源码实现,这个方法是16字节对齐算法

内存字节对齐原则

在解释为什么需要16字节对齐之前,首先需要了解内存字节对齐的原则,主要有以下三点

数据成员对齐规则:struct 或者 union 的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如数据、结构体等)的整数倍开始(例如int在32位机中是4字节,则要从4的整数倍地址开始存储)
数据成员为结构体:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储(例如:struct a里面存有struct b,b里面有char、int、double等元素,则b应该从8的整数倍开始存储)
结构体的整体对齐规则:结构体的总大小,即sizeof的结果,必须是其内部做大成员的整数倍,不足的要补齐
为什么需要16字节对齐

需要字节对齐的原因,有以下几点:

通常内存是由一个个字节组成的,cpu在存取数据时,并不是以字节为单位存储,而是以块为单位存取,块的大小为内存存取力度。频繁存取字节未对齐的数据,会极大降低cpu的性能,所以可以通过减少存取次数来降低cpu的开销
16字节对齐,是由于在一个对象中,第一个属性isa占8字节,当然一个对象肯定还有其他属性,当无属性时,会预留8字节,即16字节对齐,如果不预留,相当于这个对象的isa和其他对象的isa紧挨着,容易造成访问混乱
16字节对齐后,可以加快CPU读取速度,同时使访问更安全,不会产生访问混乱的情况
字节对齐-总结

在字节对齐算法中,对齐的主要是对象,而对象的本质则是一个 struct objc_object的结构体,
结构体在内存中是连续存放的,所以可以利用这点对结构体进行强转。
苹果早期是8字节对齐,现在是16字节对齐
下面以align(8) 为例,图解16字节对齐算法的计算过程,如下所示

首先将原始的内存 8 与 size_t(15)相加,得到 8 + 15 = 23
将 size_t(15) 即 15进行~(取反)操作,~(取反)的规则是:1变为0,0变为1
最后将 23 与 15的取反结果 进行 &(与)操作,&(与)的规则是:都是1为1,反之为0,最后的结果为 16,即内存的大小是以16的倍数增加的

calloc:申请内存,返回地址指针

通过instanceSize计算的内存大小,向内存中申请 大小 为 size的内存,并赋值给obj,因此 obj是指向内存地址的指针


这里我们可以通过断点来印证上述的说法,在未执行calloc时,po objnil,执行后,再po obj法线,返回了一个16进制的地址


在平常的开发中,一般一个对象的打印的格式都是类似于这样的<LGPerson: 0x01111111f>(是一个指针)。为什么这里不是呢?

  • 主要是因为objc 地址 还没有与传入 的 cls进行关联,
  • 同时印证了 alloc的根本作用就是 开辟内存
obj->initInstanceIsa:类与isa关联

经过calloc可知,内存已经申请好了,类也已经出入进来了,接下来就需要将 类与 地址指针 即isa指针进行关联,其关联的流程图如下所示


主要过程就是初始化一个isa指针,并将isa指针指向申请的内存地址,在将指针与cls类进行 关联

同样也可以通过断点调试来印证上面的说法,在执行完initInstanceIsa后,在通过po obj可以得出一个对象指针


总结

  • 通过对alloc源码的分析,可以得知alloc的主要目的就是开辟内存,而且开辟的内存需要使用16字节对齐算法,现在开辟的内存的大小基本上都是16的整数倍
  • 开辟内存的核心步骤有3步:计算 -- 申请 -- 关联

init 源码探索

alloc源码探索完了,接下来探索init源码,通过源码可知,inti的源码实现有以下两种

类方法 init


这里的init是一个构造方法 ,是通过工厂设计(工厂方法模式),主要是用于给用户提供构造方法入口。这里能使用id强转的原因,主要还是因为 内存字节对齐后,可以使用类型强转为你所需的类型

实例方法 init

  • 通过以下代码进行探索实例方法 init

  • 通过main中的init跳转至init的源码实现

  • 跳转至_objc_rootInit的源码实现

有上述代码可以,返回的是传入的self本身。

new 源码探索

一般在开发中,初始化除了init,还可以使用new,两者本质上并没有什么区别,以下是objc中new的源码实一般在开发中,初始化除了init,还可以使用new,两者本质上并没有什么区别,以下是objc中new的源码实现,通过源码可以得知,new函数中直接调用了callAlloc函数(即alloc中分析的函数),且调用了init函数,所以可以得出new 其实就等价于 [alloc init]的结论


但是一般开发中并不建议使用new,主要是因为有时会重写init方法做一些自定义的操作,例如 initWithXXX,会在这个方法中调用[super init],用new初始化可能会无法走到自定义的initWithXXX部分。

例如,在CJLPerson中有两个初始化方法,一个是重写的父类的init,一个是自定义的initWithXXX方法,如下图所示


使用 alloc + init 初始化时,打印的情况如下


使用new 初始化时,打印的情况如下

总结

如果子类没有重写父类的init,new会调用父类的init方法
如果子类重写了父类的init,new会调用子类重写的init方法
如果使用 alloc + 自定义的init,可以帮助我们自定义初始化操作,例如传入一些子类所需参数等,最终也会走到父类的init,相比new而言,扩展性更好,更灵活。

补充

【问题】为什么无法断点到obj->initInstanceIsa(cls, hasCxxDtor);

主要是因为断点断住的不是 自定义类的流程,而是系统级别的


作者:style_月月
链接:https://blog.csdn.net/lin1109221208/article/details/108427260

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

iOS底层原理01:源码探索的三种方式

iOS
本文主要介绍下源码探索的三种方法1、符号断点直接跟流程2、通过按住control+step into3、汇编跟流程下面详细讲下这三种方法是如何查找到函数所在的源码库,以alloc为例1、符号断点直接跟流程通过下alloc的符号断点选择断点Symbolic Br...
继续阅读 »

本文主要介绍下源码探索的三种方法

  • 1、符号断点直接跟流程
  • 2、通过按住control+step into
  • 3、汇编跟流程

下面详细讲下这三种方法是如何查找到函数所在的源码库,以alloc为例

1、符号断点直接跟流程

  • 通过下alloc的符号断点

    • 选择断点Symbolic Breakpoint


符号断点中输入 alloc


main中的CJLPerson处 加一个断点
在走到这部分断点之前,需要关闭上面新增的符号断点,原因是因为alloc的调用有很多,如果开启了就不能准确的定位到CJLPerson的alloc方法


以下为符号断点的关闭状态


运行程序, 断在CJLPerson部分

  • 打开 alloc符号断点 ,断点状态为


    继续执行


    以下为alloc符号断点断住的堆栈调用情况,从下图可以看出 alloc 的源码位于libobjc.A.dylib库(需要去Apple 相应的开源网址下载 objc源码进行更深入的探索)


    2、通过按住control+step into

    • main中的CJLPerson处 加一个断点,运行程序,会断在CJLPerson位置


  • 按住 control键,选择 step into ⬇️键


进去后,显示为以下内容


再下一个objc_alloc符号断点,符号断点后显示了 objc_alloc所在的源码库
(需要去Apple 相应的开源网址下载 objc源码进行更深入的探索)


3、汇编跟流程

main中的CJLPerson处 加一个断点,运行程序,会断在CJLPerson位置


xcode 工具栏 选择 Debug --> Debug Workflow --> Always Show Disassembly,这个 选项表示 始终显示反汇编 ,即 通过汇编 跟流程


按住control,点击 step into ⬇️键,执行到下图的callq ,对应 objc_alloc


  • 按住control,点击 step into ⬇️键进入,看到断点断在objc_alloc部分


  • 同样通过objc_alloc的符号断点,得知源码所在库
    (需要去Apple 相应的开源网址下载 objc源码进行更深入的探索)



作者:style_月月
链接:https://blog.csdn.net/lin1109221208/article/details/108425742
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

web网页基础知识

浮动元素重叠1、行内元素与浮动元素发生重叠,边框、背景、内容都会显示在浮动元素之上2、块级元素与浮动元素发生重叠,边框、背景会显示在浮动元素之下,内容会显示在浮动元素之上3、若不浮动的是块级元素,那么浮动的元素将显示在其上方4、若不浮动的是行内元素或者行内块元...
继续阅读 »

浮动元素重叠
1、行内元素与浮动元素发生重叠,边框、背景、内容都会显示在浮动元素之上
2、块级元素与浮动元素发生重叠,边框、背景会显示在浮动元素之下,内容会显示在浮动元素之上
3、若不浮动的是块级元素,那么浮动的元素将显示在其上方
4、若不浮动的是行内元素或者行内块元素,那么浮动的元素不会覆盖它,而是将其挤往左方、、

表单里面enctype 属性的默认值是“application/x-www-form-urlencoded”,



Boolean.FALSE与new Boolean(false)的区别

因为Boolean的 构造函数Boolean(String s) 参数只有为" true "(忽略大小写,比如TRUE,tRue都行)时候才是创建为真的Boolean值。其他情况都为假

 JavaScript的其他数据类型都可以转换成Boolean类型,注意!!!只有这几种类型会转换为false

undefined
null
0
-0
NaN
"" (空字符串)

  其他的都会转换为true。空对象{},空数组[] , 负数 ,false的对象包装等

  重点,new Boolean(false)是布尔值的包装对象 typeof (new Boolean(false)) // 'object' ,所以 转换为boolean是true,而不是false

内联元素是不可以控制宽和高、margin等;并且在同一行显示,不换行。
块级元素时可以控制宽和高、margin等,并且会换行。
行内元素不可以设置宽高,但是可以设置 左右padding、左右margin
1. inline : 使用此属性后,元素会被显示为内联元素,元素则不会换行
inline是行内元素,同行可以显示,像span、font、em、b这些默认都是行内元素,不会换行,无法设置宽度、高度、margin、border
2. block : 使用此属性后,元素会被现实为块级元素,元素会进行换行。
block,块元素,div、p、ul、li等这些默认都是块元素,会换行,除非设置float
3. inline-block : 是使元素以块级元素的形式呈现在行内。意思就是说,让这个元素显示在同一行不换行,但是又可以控制高度和宽度,这相当于内敛元素的增强。(IE6不支持)
inline-block,可以同行显示的block,想input、img这些默认就是inline-block,出了可以同行显示,其他基本block一样
一.h1~h6标签:有默认margin(top,bottom且相同)值,没有默认padding值。

在chrome中:16,15,14,16,17,19;

在firefox中:16,15,14,16,17,20;

在safari中:16,15,14,16,17,19;

在opera中:16,15,14,14,17,21;

在maxthon中:16,14,14,15,16,18;

在IE6.0中:都是19;

在IE7.0中:都是19;

在IE8.0中:16,15,14,16,17,19;
二.dl标签:有默认margin(top,bottom且相同)值,没有默认padding值。

在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中:margin:12px 0px;

在IE6.0,7.0中:margin:19px 0px;

dd标签有默认margin-left:40px;(在所有上述浏览器中)。
三.ol,ul标签:有默认margin-(top,bottom且相同)值,有默认padding-left值

在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中:margin:12px 0px;

在IE6.0,7.0中:margin:19px 0px;

默认padding-left值:在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中都是padding-left:40px;在IE6.0,7.0中没有默认padding值,因为ol,ul标签的边框不包含序号。
四.table标签没有默认的margin,padding值;th,td标签没有默认的margin值,有默认的padding值。

在Chrome,Firefox,Safari,Opera,Maxthon中:padding:1px;

在IE8.0中:padding:0px 1px 1px;

在IE7.0中:padding:0px 1px;

相同内容th的宽度要比td宽,因为th字体有加粗效果。
五.form标签在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中没有默认的margin,padding值,但在IE6.0,7.0中有默认的margin:19px 0px;
六.p标签有默认margin(top,bottom)值,没有默认padding值。

在Chrome,Firefox,Safari,Opera,Maxthon,IE8.0中:margin:12px 0px;

在IE6.0,7.0中:margin:19px 0px;
七.textarea标签在上述所有浏览器中:margin:2px;padding:2px;
八.select标签在Chrome,Safari,Maxthon中有默认的margin:2px;在Opera,Firefox,IE6.0,7.0,8.0没有默认的margin值。

option标签只有在firefox中有默认的padding-left:3px;

###属性继承

1. 不可继承的:display、margin、border、padding、background、height、min-height、max-height、width、min-width、max-width、overflow、position、left、right、top、bottom、z-index、float、clear、table-layout、vertical-align、page-break-after、page-bread-before和unicode-bidi。
2. 所有元素可继承:visibility和cursor。
3. 内联元素可继承:letter-spacing、word-spacing、white-space、line-height、color、font、font-family、font-size、font-style、font-variant、font-weight、text-decoration、text-transform、direction。
4. 终端块状元素可继承:text-indent和text-align。
5. 列表元素可继承:list-style、list-style-type、list-style-position、list-style-image。

收起阅读 »

uniapp里面可以使用的单利定时器

主要代码 var HashMap = require('../tools/HashMap') /** * 使用说明: * 1、引入 var timeTool=require("../utils/timeTool.js") ...
继续阅读 »


主要代码
var HashMap = require('../tools/HashMap')
/**
* 使用说明:
* 1、引入 var timeTool=require("../utils/timeTool.js")
* 2、onload 里面实例化并调用start方法:
* mtimeTool = new timeTool( this); mtimeTool.start();
添加监听函数:keykey随意写不要重复就好
mPKGame.addCallBack("keykey", () => {})
*/
//

class PKGame {
constructor(handler) {
this.mNetTool = netTool;
this.mHandler = handler;
this.mHandler;
this.commonTimer;
this.callBackListener = new HashMap();
this.instance;
// uni.setStorageSync("token",handler.appInfo.token)
}

destroy() {
this.clearInterval(commonTimer)
this.commonTimer = null;
}
start() {
if (!this.commonTimer) {
this.commonTimer = setInterval(() => {
var values = this.callBackListener.values();
for (var i in values) {
typeof values[i] == "function" && values[i]();
}
}, 1000);
}
// this.createRoom();
}

addCallBack(listenerKey, listener) {
this.callBackListener.put(listenerKey, listener)
}
removeCallBack(listenerKey) {
this.callBackListener.remove(listenerKey)
}

static getInstance = function (handler) { //静态方法
return this.instance || (this.instance = new PKGame(handler))
}
}

module.exports = ( handler) => {
return PKGame.getInstance( handler)
};

hashmap工具类

/**
* ********* 操作实例 **************
* var map = new HashMap();
* map.put("key1","Value1");
* map.put("key2","Value2");
* map.put("key3","Value3");
* map.put("key4","Value4");
* map.put("key5","Value5");
* alert("size:"+map.size()+" key1:"+map.get("key1"));
* map.remove("key1");
* map.put("key3","newValue");
* var values = map.values();
* for(var i in values){
* document.write(i+":"+values[i]+" ");
* }
* document.write("<br>");
* var keySet = map.keySet();
* for(var i in keySet){
* document.write(i+":"+keySet[i]+" ");
* }
* alert(map.isEmpty());
*/

function HashMap(){
//定义长度
var length = 0;
//创建一个对象
var obj = new Object();

/**
* 判断Map是否为空
*/
this.isEmpty = function(){
return length == 0;
};

/**
* 判断对象中是否包含给定Key
*/
this.containsKey=function(key){
return (key in obj);
};

/**
* 判断对象中是否包含给定的Value
*/
this.containsValue=function(value){
for(var key in obj){
if(obj[key] == value){
return true;
}
}
return false;
};

/**
*向map中添加数据
*/
this.put=function(key,value){
if(!this.containsKey(key)){
length++;
}
obj[key] = value;
};

/**
* 根据给定的Key获得Value
*/
this.get=function(key){
return this.containsKey(key)?obj[key]:null;
};

/**
* 根据给定的Key删除一个值
*/
this.remove=function(key){
if(this.containsKey(key)&&(delete obj[key])){
length--;
}
};

/**
* 获得Map中的所有Value
*/
this.values=function(){
var _values= new Array();
for(var key in obj){
_values.push(obj[key]);
}
return _values;
};

/**
* 获得Map中的所有Key
*/
this.keySet=function(){
var _keys = new Array();
for(var key in obj){
_keys.push(key);
}
return _keys;
};

/**
* 获得Map的长度
*/
this.size = function(){
return length;
};

/**
* 清空Map
*/
this.clear = function(){
length = 0;
obj = new Object();
};
}
module.exports = HashMap;

收起阅读 »

uniapp开发px和rpx

开发中难免出现单位问题,就像获取系统信息,里面的屏幕宽度什么的都是px作为单位的,因此这里说明一下uniapp的转换使用rpx转pxuni.upx2px(rpx的值)px转rpxpx的值/(uni.upx2px(10)/10)使用的时候可以 let px = ...
继续阅读 »

开发中难免出现单位问题,就像获取系统信息,里面的屏幕宽度什么的都是px作为单位的,因此这里说明一下uniapp的转换使用
rpx转px

uni.upx2px(rpx的值)

px转rpx

px的值/(uni.upx2px(10)/10)

使用的时候可以 let px = uni.upx2px(rpx的值)什么的 返回值就是计算好了的

收起阅读 »

潜力APP新品推荐:Vibetoon——人人都可以是音乐视频创作人

1、Vibetoon——人人都可以是音乐视频创作人不同于在大多数人都在布局 3D Avatar 和虚拟数字人,Vibetoon 另辟蹊径选择用“2D Avatar+音乐+短视频”的模式为自己开路。并成功吸引了 Will Smith、Martin Lawrenc...
继续阅读 »

1、Vibetoon——人人都可以是音乐视频创作人

image.gif

不同于在大多数人都在布局 3D Avatar 和虚拟数字人,Vibetoon 另辟蹊径选择用“2D Avatar+音乐+短视频”的模式为自己开路。并成功吸引了 Will Smith、Martin Lawrence、Dj Jazzy Jeff、BLXST、Dave East、Macaulay Culkin 等多名音乐人、唱作人、明星们使用和宣传,同时也同华纳、Redbull等知名唱片公司达成合作。

而根据 Vibetoon 联合创始人 Veronica 表示,“Vibetoon 设立的初衷是希望每一个音乐人都可以不受资金和时间的限制,拥有自己专属的MV”。

而 Vibetoon 的使用方法也非常简单,用户只需要完成“Avatar 头像创作、场景选择和上传音乐”三步就可以创作一个专属自己的音乐视频了。

关于Avatar,用户可以自由选择选择 Avatar 的性别、体型、肤色、发色、眉毛、眼睛形状、虹膜颜色、眉毛、鼻子、嘴唇、睫毛、妆容、胡子、帽子、眼镜、耳饰、项链、手链、服装等内容。尽管每个类目提供的选项不是很丰富,但是也给出了一定的差异化空间,至少可以帮助创作者保留一定特点。

在场景上,Vibetoon 提供了沙发一角、跑车1(自然风光)、跑车2(城市风光)、录音房、泳池、阳台、地铁、沙滩等 8 个场景选择。

而除了场景本身,用户还可以加上如吸烟、弹吉他、敲电子键盘、打游戏、喝东西、写东西等不同的动画效果,选择不同的动画场景即可触发不同的动画效果。

从笔者个人的直观感受来看,Vibetoon 提供的金链子、编发、夸张耳饰、公路跑车等个性化选项,似乎更贴近“说唱”风格,这可能也是为何会多次在 Twitter 上被说唱歌手翻牌的原因。

Vibetoon 支持用户以不同比例的格式导出音乐视频并上传至 Instagram、YouTube 等不同社交媒体平台。

image.gif

目前 Vibetoon 正处于努力众筹阶段,会将筹措资金用于更多场景、动作、服装和新功能的研发和探索中。

全球越来越多的用户习惯用短视频的方法来进行表达,而 Vibetoon 似乎在短视频生态链路中找到了一条垂直,且被公司、创作者和普通用户同时需要的路线。

收起阅读 »

一句话总结工程师的辛酸

当产品出现问题时,锅已经甩到了工程师的头上,他们最喜欢说的一些话是: 1.明明在我的电脑上运行正常,为何就……2.不可能出现这种情况的啊,绝对是玄学!3.快了,已经完成了90%~4.这个很简单的,我三天就能搞定~然而……5.昨天程序运行明明是正常的,但不知道为...
继续阅读 »

当产品出现问题时,锅已经甩到了工程师的头上,他们最喜欢说的一些话是:

1.明明在我的电脑上运行正常,为何就……

2.不可能出现这种情况的啊,绝对是玄学!

3.快了,已经完成了90%~

4.这个很简单的,我三天就能搞定~然而……

5.昨天程序运行明明是正常的,但不知道为啥今天就不行了,奇了怪了~

6.只是改一行代码,不会对整个程序造成影响的,放心吧~

7.如果有问题,一定不会是我程序的原因,要不考虑一下硬件问题?

8.审查代码时:当时写这个程序的时候只有上帝和我知道我为啥这样写,现在只有上帝知道了。(我也不记得当时是什么原因了~~)

9.这个功能我会在下个版本修正……到下个版本的时候,再重复上面那句话。

10.已经做好了,但还有一些细节要调一下。

11.我会在代码更替的时候添加单元测试。

12.这只是暂时的解决方案,在正式版我会修改方案的,然后……

13.我觉得这文档写的很清楚啊,我就不明白为啥你说看不懂,这也太难了~

14.


15.


16.我正在调试这个bug,但程序是没问题的啊,是不是你硬件出错了?

17.这是字符编码的问题。

18.不用担心,这次肯定不会有问题了。上~~

19.这不可能,肯定是芯片坏了,或者是编译器出错了。

20.这个变量怎么可能被修改了,奇怪了~

21.我需要重构代码,因为上一个人写得太烂了。辣鸡代码~

22.我检查过一遍了,没问题的,版本可以发布上线了!

23.没办法,这是一个公认的bug,没有办法解决~

24.再给我两天,保证能做好。

25.之前一直都没有出现过这种情况啊~

26.我又不能测试所有的功能。

27.这不是bug,这肯定是配置问题,或者网络问题。

28.程序肯定是没问题了,你是不是改了什么,你重演一下我看看。

29.这些代码是上一个开发者写的,不是我写的。这锅我不背~

30.运行那么久,第一次出现这样的问题啊,我之前都没见过。还得瞧瞧~

看完是不是感觉自己躺着也中枪了?

声明:本文素材来源网络,版权归原作者所有。如涉及作品版权问题,请与我联系删除。

收起阅读 »

用80%的工时拿100%的薪水,英国正式开启“四天工作制”试验!

据外媒报道,英国从6日开始一项一周工作四天的试验,有来自70家企业的3000多名员工参与其中。这是迄今为止全球类似试验中规模最大的一次,也再次引发各界对“做四休三”工作模式的热议。全球最大型试验据报道,参与这项试验的有来自70家英国企业的3300多名员工。试验...
继续阅读 »

据外媒报道,英国从6日开始一项一周工作四天的试验,有来自70家企业的3000多名员工参与其中。这是迄今为止全球类似试验中规模最大的一次,也再次引发各界对“做四休三”工作模式的热议。

全球最大型试验

据报道,参与这项试验的有来自70家英国企业的3300多名员工。试验从本周一开始,为期6个月。参与试验的员工每周将额外获得1天带薪休假的机会,而收入不会减少。

参与企业表示,新冠疫情推动了企业管理人员对工作方式进行反思,希望通过这项试验测试是否可以在不造成生产力损失的情况下缩短工时,同时提升员工的心理健康和福利。

英国慈善银行(Charity Bank)首席执行官埃德·西格尔(Ed Siegel)说:“20世纪的五天工作制概念不再是21世纪企业的最佳选择。我们坚信,在工资和福利不变的情况下,四天工作制将创造更快乐的员工团队,并将对企业的生产率、客户体验和我们的社会使命产生同样积极的影响。

据悉,此次试验是全球同类试验中规模最大的一次,涉及金融、酒店、护理、餐饮、动画制作等多个行业的约70家英国公司,并将由牛津大学、剑桥大学和波士顿学院的研究人员对试验结果进行监测。

研究人员将记录员工对多休息一天的反应,包括压力、工作和生活满意度、健康、睡眠、能源使用和旅行等因素。

试验结果将在2023年公布,而后将由研究机构向英国政府提交正式审议,以建立每周工作32小时的制度。

我们创建了一个高质量的技术交流群,与优秀的人在一起,自己也会优秀起来,赶紧点击加群,享受一起成长的快乐。

“100-80-100”模式

消息一出,迅速占据各大英媒头条。英国广播公司(BBC)的标题十分吸引眼球:“员工们拿100%的薪水,工作时长只有80%”。

尽管上班时间减少了,但实际上,新模式鼓励的是企业更多关注工作效率,而不是工作时长。

据悉,这项试验的发起方是一家名为“4 Day Week Global”的非营利性组织。该组织自2021年开始倡议试行每周四天工作制,目前已在西班牙、爱尔兰、美国和英国的苏格兰地区进行试点。

该倡议采用的是“100-80-100”模式,即企业用100%的工资支付员工80%的工作时间,以换取100%的产出。具体操作上,不同公司可根据各自情况来决定哪一天休息,以及每周的工时。

“4 Day Week Global”首席执行官乔·奥康纳(Joe O’connor)表示,“越来越多的公司已经认识到,需要竞争的新领域是如何保证员工的生活质量,而缩短工时、聚焦产出将赋予它们竞争优势。”

该组织称,大量研究表明,每周四天工作制可以提高工作效率,降低公司开支,提升员工幸福感。78%的员工表示,每周工作四天会感到更快乐,压力更小。63%的企业发现,每周工作四天更容易吸引和留住人才。

此外,缩短工时也能产生一定的经济效益。有数据显示,引入四天工作制将使英国商业街销售额增加约580亿英镑,这是因为每周三天的假期会让消费者多出20%的购物时间,在培养兴趣爱好、园艺等方面的支出也可能增加。

如果你最近想跳槽的话,年前我花了2周时间收集了一波大厂面经,节后准备跳槽的可以点击这里领取

一种新趋势?

英国的做法让不少网友表示羡慕,希望自己有朝一日也能实现工作生活相平衡。更多人关注,多国尝试的“做四休三”会不会成为未来一种新趋势。

事实上,全球范围内已有不少国家和企业对此进行探索,并取得了一些成绩。

早在2015至2019年期间,冰岛就进行过一次缩短工时的试验,主要涉及幼儿园、医院等公共服务部门。当时,有2500名冰岛员工(相当于冰岛劳动力的1%)进行了每周工作四天的尝试。

试验结束后,参与者们普遍反映生活压力降低,幸福感提升,且工作效率也提高了25%-40%,员工士气亦有大幅提振。

如今,冰岛已有超过80%的员工每周工作35小时,并且正在酝酿迈向下一步——每周工作32小时。

而在加班文化盛行的日本,近年来也在探索这一新模式。2019年8月,微软日本办公室试行每周四天工作制且不减薪,约有2300名员工连续5个周五放假。

微软表示,新模式下生产率提高了40%,会议效率更高,员工也更快乐,还节省了用电量和用纸量。

尽管多数案例都获得成功,但缩短工时也可能带来意想不到的副作用。

批评人士指出,四天工作制可能给一些员工带来更大压力,因为他们必须在更短的时间内完成更多的工作。此外,缩短工时在以客户为导向,或者7天24小时运营的紧急服务类工作中是不现实的。

还有人指出,虽然规定的工作时间减少了,但由于工作量不变,因此真正的工作时间并未改变,由此导致的加班工资还会给雇主带来额外成本。

例如,法国进行的一项类似试验发现,员工们依然需要五天时间完成既定任务,这也导致公司的劳务成本上升。

相关人士表示,尽管“四天工作制”是对传统工作模式的颠覆,但要推进到政府层面并落地,还有很长的路要走。那么你觉得“四天工作制”怎么样呢?可能成为将来的主流吗?

来源:上观新闻

收起阅读 »

如何快速在团队内做一次技术分享?

前言相信很多小伙伴跟我一样,是一位奋斗在一线的业务开发,每天有做不完的任务,还有项目经理在你耳边催你,“这个功能今天能完成吗?”其实作为一名前端工程师,任务就是完成 Leader 的任务, 但公司实行 OKR 以来,你就不得不在完成任务的基础上加上几条,“提示...
继续阅读 »

前言

相信很多小伙伴跟我一样,是一位奋斗在一线的业务开发,每天有做不完的任务,还有项目经理在你耳边催你,“这个功能今天能完成吗?”其实作为一名前端工程师,任务就是完成 Leader 的任务, 但公司实行 OKR 以来,你就不得不在完成任务的基础上加上几条,“提示个人能力”是我任务之外一个长期目标。


为了能完成这个目标,团队内部分享就成了这个目标的关键结果,那么如何在短时间内完成这项任务呢?下面分享下我的技巧。

明确主题

首先我们要明确公司需要什么?我们不能随便搞一个知识点去分享,这样没有人愿意去听,比如公司接下来可能会上前端监控系统,那么我们可以在先做一个技术调研,出一个《前端监控体系搭建要点》,比如公司接下来需要做小程序,那么我们可以出一个《小程序跨端实现方案探索》等,如果没有什么新的功能要开发,那么我们也可以谈一谈《前端性能优化》、《Typescript 快速上手》,总之要明确一个切合实际的目标。

巧用搜索引擎

确定好主题后,我们可以在技术社区搜索相关的技术文章,比如掘金、知乎、思否、微信公众号等, 比如直接在掘金搜索“性能优化” 然后按热度排序,就可以找到不错的文章。


接下来我们需要根据这些文章中的内容制作 PPT

使用 markdown 来制作 PPT

程序员做 PPT 可能会浪费不少时间,所以我选择是 markdown 来制作 PPT,这里我分享 2 个工具

Marp for VS Code vscode 插件

只用关注内容,简单分隔一下,就可以制作 PPT,看下 marp 官方文档可以很快学会用法,看看 jeremyxu 写的效果,项目地址:kubernetes 分享 PPT 源文件


二: Slidev 也可以让我们用 Markdown 写 PPT 的工具库

官网地址:sli.dev, 基于 Node.js、Vue.js 开发,而且它可以支持各种好看的主题、代码高亮、公式、流程图、自定义的网页交互组件,还可以方便地导出成 PDF 或者直接部署成一个网页使用。

  • 演讲者头像

当然还有很多酷炫的功能,比如说,我们在讲 PPT 的时候,可能想同时自己也出镜,Slidev 也可以支持。


  • 演讲录制

Slidev 还支持演讲录制功能,因为它背后集成了 WebRTC 和 RecordRTC 的 API,


文章转 markdown


这里推荐下我写的油猴扩展

  • 第一步: 安装 chrome 油猴扩展

  • 第二步: 安装文章拷贝助手

可以直接将文章转为 markdown 格式,目前已经支持掘金、知乎、思否、简书、微信公众号文章。

接下来就根据 H2 分页组织PPT内容即可。

---
layout: cover
---

# 第 1 页

This is the cover page.

<!-- 这是一条备注 -->

较长的内容可以将内容改为幻灯片编写备注。它们将展示在演讲者模式中,供你在演示时参考。

小结

本文讲述了我在准备团队内容分享的小技巧,我认为最重要的就是结合公司实际来做分享修改,无论主题也好文章内容也罢,虽然文章是别人写的,但要经过自己的思考和消化,变成自己的知识,这样我们才可以快速成长!在此,祝各位小伙伴在能够获知识的同时得较高的 OKR 考核。

以上就是本文全部内容,希望这篇文章对大家有所帮助,也可以参考我往期的文章或者在评论区交流你的想法和心得,欢迎一起探索前端。

作者:狂奔滴小马
来源:juejin.cn/post/7106810693910790152

收起阅读 »

抖音 Android 包体积优化探索:基于 ReDex 的 DEX 优化落地实践

抖音是字节跳动规模最大、运行环境复杂度最高的应用之一。在 ReDex 落地初期,由于对复杂度估计不足,在独立灰度和全量灰度期间引起了一些问题,在解决问题的过程中,我们也逐步形成了一套迭代流程以保证优化的稳定性。前言应用安装包的体积会显著影响应用的下载速度和安装...
继续阅读 »

抖音是字节跳动规模最大、运行环境复杂度最高的应用之一。在 ReDex 落地初期,由于对复杂度估计不足,在独立灰度和全量灰度期间引起了一些问题,在解决问题的过程中,我们也逐步形成了一套迭代流程以保证优化的稳定性。

前言

应用安装包的体积会显著影响应用的下载速度和安装速度,按照 Google 的经验数据,包体积每增加 1M 会造成 0.17%的新增折损。抖音的一些实验也证明了包体积会显著影响下载激活的转化率。

Android 的安装包是 APK 格式的,在抖音的安装包中 DEX 的体积占比达到了 40%以上,所以针对 DEX 的体积优化是一种行之有效的包体积优化手段。

DEX 本质上是由 Java/Kotlin 代码编译而成的字节码,因此,针对字节码进行业务无感的通用优化成为我们的一个探索方向。

优化结果

终端基础技术团队和抖音基础技术团队在过去的一年里,利用 ReDex 在抖音包体积优化方面取得了一些明显的收益,这些优化也被同步到了其他各大 App 上。

在抖音、头条和其他应用上,我们的优化对 APK 体积的缩减普遍达到了 4%以上,对 DEX 体积的缩减则可以达到 8% ~ 10%

优化思路

在 android 应用的构建过程中,Java/Kotlin 代码会先被编译成 Class 字节码,在这个阶段 gradle 提供了 Transformer 可以进行字节码的自定义处理,很多插件都是在这个阶段处理字节码的。然后,Class 文件经过 dexBuilder/mergeDex 等任务的处理会生成 DEX 文件,并最终被打进安装包中。整个过程如下所示:


所以,针对字节码的优化是有 2 个时机可以进行的:

  • 在 transformer 阶段对 Class 字节码进行优化

  • 在 DEX 阶段对 DEX 文件进行优化

显然,对 DEX 进行优化是更理想的一种方式,因为在 DEX 文件中,除了字节码指令外,还存在跨 DEX 引用、字符串池这样的结构,针对这些 DEX 格式的优化是无法在 transformer 阶段进行的。

在确定了针对 DEX 文件进行优化的思路后,我们选择了 facebook 的开源框架 ReDex 作为优化工具,并对其进行了定制开发。

选择 ReDex 的原因是它提供了丰富的基础能力,ReDex 的基础能力包括:

  1. 读写及解析 DEX 的能力,同时可以在一定程度上读取并解析 xml 和 so 文件

  2. 解析简单的 proguard keep 规则并匹配类/方法/成员变量的能力

  3. 对字节码进行数据流分析的能力,提供了常用的数据流分析算法

  4. 对字节码进行合法性校验的能力,包括寄存器检查、类型检查等

  5. 一系列的字节码优化项,每项优化称为一个 pass,多个 pass 组成 pipeline 对 DEX 进行优化


我们基于这些能力进行了定制和扩展,并期望最终建立完善的优化体系。

优化项

在抖音落地的优化项,包括 facebook 开源的优化和我们自研的优化,从其出发点来看,可以大致分为下面几种:

  • 通用字节码优化:通常意义下的编译优化,如常量传播、内联等,一般也可在 Transformer 阶段实现

  • DEX 格式优化:DEX 中除了字节码指令外,还包括字符串池、类/方法引用、debug 信息等等,针对这些方面的优化归类为 DEX 格式优化

  • 针对编程语言的优化:Java/Kotlin 的一些语法糖会生成大量字节码,可以对这些字节码进行针对性的分析和优化

  • 提升压缩率的优化:将 DEX 打包成 APK 实质上是个压缩的过程,对 DEX 内容进行针对性的优化可以提升压缩率,从而产生体积更小的 APK

这几种优化没有明确的标准和界线,有时一个 Pass 会涉及到多种,下面详细介绍一下各项优化。

通用字节码优化

ConstantPropagationPass

该 Pass 实际上包含了常量折叠和常量传播。

常量折叠是在编译期简化常量的过程,比如

复制

y = 7 - 14 / 2
--->
y = 0

常量传播是在编译期替代指令中已知常量的过程,比如

int x = 14;
int y = 7 - x / 2;
return y * (28 / x + 2);
--->
int x = 14;
int y = 7 - 14 / 2;
return (7 - 14 / 2) * (28 / 14 + 2);

上面的例子经过 常量折叠 + 常量传播优化后就会简化为

int x = 14;
int y = 0;
return 0;

再经过死代码删除就可以最终变为return 0。

具体的优化过程是:

  1. 对方法进行数据流分析,主要针对 const/move 等指令,得出一个寄存器在某个位置可能的取值

  2. 根据分析的结果,进行指令替换或指令删除,包括:

  • 如果值肯定是非空的,可以将对应的判空去掉,比如 kotlin 生成的 null check 调用

  • 如果值肯定为空,可以将指令替换为抛空异常

  • 如果值肯定让某 if 分支走不到,可以删除对应的分支

  • 如果值是固定的,可以用 const 指令替换对应的赋值或计算指令

一个方法经过 ConstantPropagationPass 优化后,可能会产生一些死代码,比如例子中的int y = 0,这也为后续的死代码删除创造了条件。

AnnoKillPass

该 Pass 是用来移除无用注解的。注解主要分为三种类型:

  • SOURCE:java 源码编译为 class 字节码就不可见,此类注解一般不用过于关注

  • CLASS:字节码通过 dx 工具转成 DEX 就不可见,代码运行时不需要获取信息,所以一般来说也不需要关注,实测发现部分注解仍然存在于 DEX 中,这部分注解可以进行优化

  • RUNTIME:DEX 中仍然可见,代码运行中可以通过 getAnnotations 等接口获取注解信息,但是随着业务的迭代,可能获取注解信息的代码已经去掉,注解却没有下掉,这部分注解会被 ReDex 安全的移除

除此之外,实际上为了支持某些系统特性,编译器会自动生成系统注解,虽然注解本身是 RUNTIME 类型,但是可见性是VISIBILITY_SYSTEM

  • AnnotationDefault : 默认注解,不能删除

  • EnclosingClass : 当前内部类申明时所在的类

  • EnclosingMethod : 当前内部类申明时所在的方法

  • InnerClass : 当前内部类名称

  • MemberClasses : 当前类的所有内部类列表

  • MethodParameters : 方法参数

  • Signature : 泛型相关

  • Throws : 异常相关

举例说明


编译器生成 1MainApplication$1这个匿名内部类,带有 EnclosingMethod 和 InnerClass 注解


系统提供以下接口获取类相关的信息,就是通过分析相关的系统注解来实现的

  • Class.getEnclosingMethod

  • Class.getSimpleName

  • Class.isAnonymousClass

  • ....

如果代码中不存在使用这些接口获取类信息的逻辑,就可以安全的移除这部分注解,从而达到缩减包大小的目的。

RenameClassesPass

该 Pass 通过缩减类名的字符串长度来减小包体积

比如把类名从La/b/c/d/e;改为LX/a;,可以类名字符串的长度,从而达到包大小缩减的目的。实际上 Proguard 本身已经提供类似的功能: -repackageclasses 'X',效果如下:


但是-repackageclasses 'X'的处理会影响 ReDex 的 InterDexPass 的算法逻辑(InterDexPass 可以参考下文),导致收益缩减

  • 收益测试

  • Proguard-repackageclasses 'X' 收益: 600K+

  • RedexInterDexPass 收益: 400K+

  • 同时应用 Proguard-repackageclasses 'X' 和 RedexInterDexPass 收益: 40K+

本质原因在于 Proguard 重命名后,影响了 InterDexPass 函数引用权重分配,导致 InterDex 收益被回收

  • 解决方案

  • InterDexPass 深入分析原理,优化权重算法

  • 先执行 InterDexPass,后执行类似 Proguard 的-repackageclasses 'X'

权重算法优化相对来说比较复杂,同时存在众多不可确定性,比如潜在的跟其他优化的冲突,所以我们采取了第二种解决方案。

这里需要解决的一个关键点在于如何确定一个类名是否可以被安全的重命名,我们采取了一个比较取巧的方式,ReDex 会分析 Proguard 传递上来 mapping.txt 文件,只要我们保持跟 Proguard 类重命名优化一样的处理策略,就不会引发反射/native 调用/序列化等一系列问题。


但是执行起来还是碰到各种千奇百怪的问题,比如 Signature 系统注解失效问题。Signature 注解的内容是非标准的类名格式,所以类重命名后简单回写字符串或者更新 Type 类型会导致 Signature 注解失效,最后通过深入解析 Signature 格式规避了这个问题。

StringBuilderOutlinerPass

该 Pass 是针对 StringBuilder 的 CallSites 进行分析缩略的优化,与死代码删除搭配使用可以有不错的优化效果。

为何要优化 StringBuilder 呢?在 Java 的代码开发过程中,字符串操作几乎是我们最经常做的一件事情,无论是实际处理字符串拼接还是各种不同数据类型之间的拼接操作。而这些拼接操作都会被 Java 的 de-sugar 优化为 StringBuilder 操作。比如:var log = "A" + 1 + "B" + 1.0f + other_var; 会被优化为:

StringBuilder builder = new StringBuilder();
builder.append("A"); builder.append(1);
builder.append("B"); builder.append(1.0f);
builder.append(other_var);
builder.toString();

因此我们对 StringBuilder 的所有 Callsites 进行分析,在最好情况下多个方法调用可以被优化为一个调用,这个方法是一个 outline (外联)方法,具体的参数拼接和 toString 被隐藏在函数内部:

invoke-static {v1, v2, v3} Outline;.bind:([Ljava/lang/Object)Ljava/lang/String;

优化步骤可以被简单的分为如下几个步骤:

  1. 生成一个泛型的外联方法、以及数个特定参数的方法:我们可以认为生成的方法大概是这样的

@Keep
public static String bind(Object... args) {
  StringBuilder builder = new StringBuilder();
  for (int i = 0; i < args.length ; i++) {
      builder.append(args[i]);
  }
  return builder.toString();
}
  1. 收集StringBuilder 的 CallSites :通过抽象解释和不动点分析,分析所有的 StringBuilder 操作,对 append、new-instance、和 init 方法分类。判断每次 append 的参数是不是 immutable 操作,如果增加的 insn 少于减少的 insn 即会减少代码,就对这里进行处理。

  2. 生成外联方法调用:由于我们使用了泛型方法来接受参数,因此我们要对基础类型生成 ValueOf 的转换操作、并且删除append 方法前为了防止被错误优化我们还需要插入 move 指令来 copy 原有参数(这些 move 指令会被后续优化正确删除)、如果参数个数还在我们生成的特定 outline 方法范围内我们就可以使用特定方法来生成外联函数,其余的将使用泛化的外联来接受。

DEX 格式优化

InterDexPass

该 Pass 是针对跨 DEX 引用的优化。

跨 DEX 引用是指当一个 DEX 需要“使用”到另一个 DEX 中的类/方法/变量时,需要在本 DEX 中保存一份对应的类/方法/变量的 id,如果 2 个 DEX 用到了相同的字符串,那么这个字符串在 2 个 DEX 都需要进行定义。所以,改变类/方法/变量和字符串在 DEX 中的分布,可以减小引用的数量,从而减小 DEX 的体积。从原理中也可以看出,该优化对单 DEX 的应用是无效的。


从上图可以看到,进行类重排后,DEX0 的类引用和方法引用数量都减少了,DEX 的体积也会因此减小。

具体的优化过程是:

  1. 收集每个类涉及的所有引用,按照引用数量和类型计算出类的权重

  2. 根据权重计算出每个类的优先级

  3. 根据优先级选取一个类放入 DEX 中,然后调整剩余类的优先级,重复此步骤直到所有类都被处理

ReBindRefsPass

该 Pass 是针对方法引用的优化,其原理同 InterDexPass。

在字节码中,invoke-virtual/interface指令需要一个方法引用,在很多情况下,这个引用指向的是子类或者实现类的引用,把这个引用替换成父类和接口的方法引用不会影响运行时逻辑,同时会减少 DEX 中方法引用的数量。在生成 DEX 的时候,方法引用的 65536 限制通常是最先遇到的瓶颈,该优化也可以缓解这种情况。


如上图所示,优化前 caller 方法的 invoke 指令使用的是子类引用,其伪指令如下所示,需要用到 2 个引用

new-instance v0, Sub1
invoke-virtual v0, Sub1.a()
new-instance v1, Sub2
invoke-virtual v1, Sub2.a()

优化后,invoke 指令都指向其父类应用,2 个引用可以合并为 1 个,减少了 DEX 中的引用数量

new-instance v0, Sub1
invoke-virtual v0, Base.a()
new-instance v1, Sub2
invoke-virtual v1, Base.a()

针对编程语言的优化

KotlinDataClassPass

该 Pass 是对 Kotlin data class 的优化,基本思路是对 data class 的生成代码进行精简。

解构声明优化

Kotlin 中存在解构声明这种语法,可以更方便的创建多个变量,基本用法如下

data class Person(val name: String,val age: Int)
val (name,age) = person("John",20)

kotlinc 会为Person类生成 get 方法和 componentN 方法,如下是伪代码表示

Person {
    String name;
    Int age;

    getName(): String { return name; }
  getAge(): Int { return age; }
    component1(): String { return name; }
    component2(): Int { return age; }
}

// 解构声明编译为
val name = person.component12 1()
val age = person.component2()

可以看到,get 和 component 的逻辑是一样的,所以在编译期,可以进行全局的匹配,用 get 替换掉 component,然后再删除 component。

toString 等生成方法优化

kotlin compiler 为 data class 生成的 toString 具有相似的代码结构,因此可以生成一个辅助方法,然后在所有 data class 的 toString 方法中调用这个辅助方法,即外联,从而减少指令数量。

equals 和 hashCode 也可以进行类似优化,但是风险相对较高,因此单独为这些优化配置了开关,业务方可以视情况开启。

提升压缩率的优化

RegAllocPass

DEX 及其他文件经过压缩打成 APK,如果能通过改变 DEX 的内容来提升压缩率,那么也会减小最终的包体积。RegAllocPass 就是通过重新分配寄存器来提升压缩率的。

dx 生成 DEX 时使用的是线性寄存器分配算法,其基本步骤是进行存活变量分析,然后计算出每个变量的活跃区间,再根据活跃区间依次为变量分配寄存器,超出活跃区间的寄存器可以进行再分配,其优点是运行速度快,但结果往往不是最优的。

比如下面的代码,dx 分配了 6 个寄存器,v0 ~ v5

public static double calculateLuminance(@ColorInt int color) {
  final double[] result = getTempDouble3Array();
    colorToXYZ(color,result);
  return result[1] / 100;
}


相对的,ReDex 使用了图着色算法进行寄存器分配,基本步骤是进行存活变量分析,并构建冲突图,冲突图的每个节点是一个变量,如果 2 个变量可以同时存活,就在两个节点之间建立边,最后为冲突图着色,每个颜色代表一个寄存器,着色完成即寄存器分配完成。着色法相对更慢,结果一般更优。对上面同样的代码,着色法使用了 4 个寄存器,v0 ~ v3。


DEX 中的方法使用的寄存器越少,其内容重复率就越高,压缩率也会更大,从而减小了包体积。

抖音落地

抖音是字节跳动规模最大、运行环境复杂度最高的应用之一。在 ReDex 落地初期,由于对复杂度估计不足,在独立灰度和全量灰度期间引起了一些问题,在解决问题的过程中,我们也逐步形成了一套迭代流程以保证优化的稳定性。下面介绍一下我们遇到过的典型问题及当前的迭代流程。

遇到的问题

兼容性问题

一般来说,只要按照字节码规范进行优化,就不会有兼容性问题,因为 dalvik/art 也是按照规范去校验和运行字节码的,即使进行了错误的优化,引起的问题也应该是共性问题。但很多事都有例外,ReDex 就在某品牌手机的部分 Android 5.x 的机型上遇到了问题。

从 log 和一些 hook 来看,某品牌手机对 5.x 的 art 做了大量的魔改,可以推断其魔改存在一些问题,导致对正确的字节码的校验和运行也可能出现问题。一个可能的原因是:在 ReDex 进行优化时,会对一些方法体的指令顺序进行重排,这种重排是不影响方法的逻辑的,但是可能会改变一部分指令,魔改后的 art 在校验这样的方法时可能会报 verify error,引起 crash。

最终通过黑名单配置跳过了这些方法的优化规避了问题,在后续的优化过程中,没有再遇到类似的问题。

复杂场景优化问题

抖音业务复杂,代码写法多样,给静态分析和优化增加了一些难度,也更容易遇到问题。下面是 2 个典型问题:

1.空方法优化问题 代码中可能存在一些空方法,排除掉反射和 natvie 调用等场景后,剩下的空方法应该是可以删除的。但是在做优化时,却遇到了 crash,如以下代码

object XXXSDKHelper {
  init {
      initXXXSDK()
    }
    fun fakeInit() {
    }
}

// 初始化任务
public class XXInitTask implements Runnable {
    @Override
  public void run() {
        XXXSDKHelper.INSTANCE.fakeInit();
    }
}

在初始化代码中调用fakeInit,它是一个空方法,调用它的目的是触发XXSDKHelper类加载从而执行init语句块,如果删除了这个空方法,就会导致初始化未执行,在后续的流程中抛空指针。

2.复杂反射问题

对于 Class.forname(...)等简单的反射用法,静态分析是可以分析出来的,但是对一些经过字符串拼接或者嵌套之后的反射,静态分析很难分析到。因此,对可能会被反射的代码进行优化需要非常小心,通常来说,匿名内部类是不会通过反射调用的,基于此前提,我们进行了匿名内部类的重命名优化,但是在灰度后,发现某些第三方 SDK 会通过复杂的运行时逻辑对匿名内部类进行了反射调用,最终导致了 ClassNotFoundError。

复杂场景的优化问题有些是业务代码不规范造成的,但更多的是优化前提(空方法可以删除/匿名内部类不会被反射)不成立所导致,所以在进行优化时首先需要对假设进行谨慎的验证。

迭代流程

为了减少稳定性问题,我们总结了 ReDex Pass 的迭代流程。

在对一项 Pass 有了初步构思后,组内会进行可行性讨论,如果理论上可行就进入开发和验证阶段,之后同步进行至少 2 轮的独立灰度验证和业务方 Pass 评审,最后进行全量灰度验证。其中任意一个环节发现问题,都会重新进行整个流程。


通过这个流程,我们大大减少了稳定性问题遗留到灰度阶段的可能,在不断完善迭代流程的同时我们也在探索通过加强单元测试、自动化测试等方式来提升质量。

后续规划

ReDex 仍然在持续迭代中,未来我们会在以下几个方向继续进行深入探索:

  1. 更多包体积优化的探索和迭代,同时探索字节码优化在性能提升方面的可能性

  2. 提升字节码质量

  • 更加严格的合法性校验;ReDex 之前已经检测出若干自定义插件和 proguard 的问题,将问题拦截在了编译期,后续会继续提升该能力

  • 建立更加完善的质量验证体系;ReDex 作为编译期的全局字节码优化方案,如果保证优化后的字节码质量一直是个痛点,我们会继续在单元测试、自动化测试等方向探索质量提升的手段

  1. 增加编译期监控,更加快速便捷的解决编译期字节码问题,提升接入体验

  2. 其他应用方向探索;如方法插桩、某些条件下的死代码扫描等。

作者 | 冯瑞;廖斌斌;刘丰恺

来源:https://www.51cto.com/article/710484.html

收起阅读 »

瞄准Web3:互联网巨头捍卫流量“王座”之争

web
日前,谷歌云部门(Google Cloud)成立Web3团队的消息一出,也引起了一众Web3玩家们的关注。Web3 是什么?有人对它寄予厚望,认为这是真正可实现的下一代互联网;有人表示悲观,觉得这是一个“去中心化”的乌托邦,“就像是一场梦,醒来还是很感动”。对...
继续阅读 »
日前,谷歌云部门(Google Cloud)成立Web3团队的消息一出,也引起了一众Web3玩家们的关注。

Web3 是什么?

有人对它寄予厚望,认为这是真正可实现的下一代互联网;有人表示悲观,觉得这是一个“去中心化”的乌托邦,“就像是一场梦,醒来还是很感动”。对大多数人来说,Web3

的定义是什么并不重要。重要的是,在可见的未来,Web3 能给我们带来什么。

在普遍认知中,Web3 是一个基于区块链技术的去中心化互联网。其中,“去中心化”是Web3 的精神内核。围绕这一内核,Web3 的理想愿景是,将互联网及其生产内容的控制权从少数几家科技巨头手中返还到个人,从而让用户能对自己的身份和数据有更多控制权。

换句话说,Web3 就像是曾经的“占领华尔街运动”在当今互联网世界的复刻,针对的恰恰是 Web2 时代的既得利益者,即 Meta、亚马逊、谷歌,乃至BAT 这类巨头。面对这一可能的威胁,巨头们也陆续有了动作,纷纷落子,希冀在 Web3 的棋盘上继续巩固各自的生态帝国,继续成为互联网世界中隐形的规则制定者和秩序维护者。

日前,谷歌云部门(Google Cloud)成立Web3团队的消息一出,也引起了一众Web3玩家们的关注。

谷歌的布局

谷歌对于 Web3 的横空出世有其自身的判断。

在谷歌云的官方博客中,如此描述:“区块链和数字资产正在改变世界存储和传递信息以及价值的方式。如今的 Web3 热潮就如同10-15年前开源和互联网的兴起。正如开源开发是互联网早期不可或缺的一部分一样,区块链正在为用户和企业带来创新的推动力。”

在今年1月,谷歌云曾对外披露,他们正在研究怎么使用加密货币支付。当时,谷歌云金融业务副总裁 Yolande Piazza 表示,已经成立了一个谷歌云数字资产团队,来协助客户在基于区块链的平台上创建新产品。彼时已经有人猜测,谷歌云未来会接受数字货币作为支付方式。

而此次谷歌云组建的Web3团队目标指向则更为清晰。它旨在构建 Web3 世界的基础设施,主要为有兴趣编写Web3软件的开发人员提供后端服务。

在发给团队的电子邮件中,谷歌云副总裁 Amit Zavery 写道,虽然世界仍处于拥抱 Web3的早期阶段,但 Web3 是一个已经显示出巨大潜力的市场,许多客户要求谷歌增加对 Web3 和 Crypto 相关技术的支持。

在外媒的公开采访中,Zavery明确表示,对于谷歌来说,参与这一趋势的方式不是直接成为加密货币浪潮的一部分,而是为Web3开发者提供基础设施服务。谷歌不参与也不干涉具体业务,而是计划成为基础设施提供商,降低开发者基于区块链设计去中心化系统的门槛,推动企业在业务中使用和利用Web3的分布式特性。

尽管今年以来,资本对于比特币这一市场的投资热情大为减弱,但Zavery表示,区块链应用不断进入主流,并在金融服务和零售业等行业中有越来越高的参与度。未来,谷歌或许会设计相关系统,使人们更容易探索链上链下数据,同时简化构建和运行区块链节点进行验证和记录交易的过程。

而在团队构成上,Zavery透露,新组建的Web3团队主要是将内部参与过 Web3 项目的员工合并到一起,然后从外部招募一些区块链开发工程师和其他相关人才。2019年加入谷歌的前花旗集团高管 James Tromans 将领导产品和工程小组,并向Zavery汇报。

谷歌的动机

谷歌入场Web3 ,除了未雨绸缪布局 Web3 基础设施之外,是否有其他考量?

有人注意到,这支 Web3 团队的主导部门是谷歌云,因此猜测,云服务市场的博弈或许也是个中关键。

谷歌母公司 Alphabet 2022 年一季度财报显示,谷歌云营收同比增长 44% 至 58.2 亿美元。Alphabet 首席财务官 Ruth Porat 表示,谷歌云服务的增长速度已经超过了其核心的广告部门,且员工人数增长最快的就是云部门。

尽管谷歌云表现不俗,但目前来说,仍旧无法与微软 Azure 抗衡,更不用说在云计算市场一骑绝尘的 AWS 。更值得一提的是,早在2018年,就有大量以太坊、比特币还有其他区块链的节点部署在 AWS 上。而到了2021 年,AWS Marketplace 总监 Marta Whiteaker 曾透露:“目前以太坊全球 25% 的工作负载都运行在 AWS 上。”

可以说,亚马逊无论是在 Web2 还是 Web3 时代,在云服务领域都占得了先机。而在Web3赛道策略相对保守的谷歌之所以选择在这个时间入场,竞争对手带来的压力可能也是一大诱因。

在一定程度上可预见的赛道内,Web3 的发展极有可能会冲击到谷歌的云服务市场份额,甚至波及广告业务,进而在更大范围内降低谷歌对全球数字生态的影响力,为了捍卫其庞大的生态版图,适时入场至少不至于在真正交锋时完全陷入被动。

局中人

面对 Web3,科技巨头、开发者、用户表现出了泾渭分明的态度。

除了谷歌之外,其他互联网巨擘也在选择拥抱Web3。

亚马逊在2018 年便推出了自己的区块链支持服务 Amazon Managed Blockchain ,主要面向在 Hyperledger Fabric 或以太坊中搭建项目的开发人员提供托管和硬件服务,提高客户为 DeFi、供应链、金融服务等业务用例创建和利用可扩展区块链技术的能力。而不久前,亚马逊CEO Andy Jassy也公开表态,亚马逊在数字资产行业和 NFT领域看到了巨大的潜力。

微软从2015 起就开始为区块链开发商提供支持。今年3月,微软投资区块链初创公司ConsenSys也被视为微软在加密相关领域的一次罕见押注。因为ConsenSys被投资者认为是为 Web3 提供动力的公司之一。有分析人士认为,这一举动展示了微软对Web3日益增长的兴趣。

在科技巨头们纷纷下注之际,开发者们对 Web3 的反映要冷淡得多。

对于谷歌入场Web3 ,美国著名软件工程师 Grady Booch在推特上表达了他的失望,并直言这种投入是对资源的浪费。

调查机构Stack Overflow在今年4月出具的报告也揭示了类似的态度。在接受调查的595名开发人员中,37%的人不知道Web3 是什么;在知道的人群中,25%的人认为 Web3

是互联网的未来;15%的人认为这是一堆炒作;14%的人认为它对加密货币领域相关应用程序很重要;9%的人认为这是一个骗局。

对于 Web3 的潜在用户群体或者吃瓜群众来说,Web3更像是一个仍旧遥远的概念。虽然“去中心化”的愿景很美,但他们使用的产品和服务是否完全去中心化在现实角度看或许并不是关注焦点。

加密通讯应用 Signal 创始人 Moxie Marlinspike 指出,即使是很多极客,也不想运行自己的服务器。即使一家大的软件企业,运行自己的服务器也是很大的负担。基于此,云厂商才会取得成功。Web3 同样如此。“如果谷歌开发出更容易使用的服务,填补市场空白,那么即使该服务没有达到去中心化的程度,人们也会去那里。”

比如以太坊最大的节点服务提供商 Infura,其运行的节点分散在各地甚至是用户家中,但不断发生的 Infura 宕机事件向人们证明了,在“去中心化”的服务名目下,要真正实现大规模推广,还是要依赖中心化的基础设施。

用户期望的并不是一个单纯取代 Web2 的Web3 时代,而是一个边界不断拓宽、新场景不断涌现的数字世界,偶有惊喜又值得期待。

结语

在Web3 领域,关于“中心化”与“去中心化”之争一直存在。

矛盾的是,“去中心化”虽然是 Web3 信徒们奉为圭臬的理想,但真正主导 Web3 发展进度的其实是一群 Web2 时代“中心化”规则下的受益者。

根据网络监控公司Sandvine发布的2021年全球互联网现象报告显示,谷歌、Meta、Netflix、亚马逊、微软和苹果这六家企业产生了超过56%的全球网络流量,他们在2021年产生的流量占比超过了所有其他互联网公司的总和。

在如此可观的流量背后,这些科技巨头在事实上控制了用户的账号、交互、产出内容甚至是隐私,这也构成了其生态帝国的权力基石。由此来看,谷歌布局 Web3 这件事,依旧是 Web2 时代巨头博弈的续篇。

但 Web3 的可贵之处在于它仍是一片待开发的荒原,没有人能预判其爆发的时机。它提供了基于区块链的新价值模型,为市场带来了创新和颠覆的可能。不确定性让 Web3 危险,也让它浪漫,因为这块土地无限自由,不拒绝任何人的踏入。巨头的主动不见得是优势,小透明的崛起也不一定荒诞。前景不明,也意味着前景有无数可能。

参考资料:

  https://www.cnbc.com/2022/05/06/googles-cloud-group-forms-web3-product-and-engineering-team.html

  https://cointelegraph.com/news/amid-crypto-hype-google-s-cloud-unit-creates-web3-team

  https://www.itpro.co.uk/cloud/367612/google-cloud-is-reportedly-building-a-dedicated-team-to-support-web3-developers

  https://stackoverflow.blog/2022/04/20/new-data-developers-web3/

来源:www.51cto.com/article/710198.html

收起阅读 »

Flutter【移动端】如何进行多渠道打包发布

随着项目的运营推广,总少不了各种客户定制化的需求,当前大部分软件其实都离不开Saas的玩法;定制化需求虽然利润高(特别是海外客户),但对于开发人员来说却比较难搞,同一套代码需要支持不同的需求。一般我们处理这种需求的时候会引入渠道包的概念,每个客户拥有独立渠道,...
继续阅读 »

随着项目的运营推广,总少不了各种客户定制化的需求,当前大部分软件其实都离不开Saas的玩法;定制化需求虽然利润高(特别是海外客户),但对于开发人员来说却比较难搞,同一套代码需要支持不同的需求。
一般我们处理这种需求的时候会引入渠道包的概念,每个客户拥有独立渠道,通过渠道指定不同的资源、赋予不同的功能,从而编译出定制化的版本。
本篇文章将分享Flutter中如何进行移动端(iOS、Android)的渠道编译,替换应用图标、名称、appkey等。



Android端


1、配置build.grade


Android端的打包配置,主要是通过build.grade文件进行配置,在android目录下加入flavorDimensions,然后配置不同的风味维度;


android {
// ......
flavorDimensions 'channel'
productFlavors {
develop {
applicationId "${defaultConfig.applicationId}"
}
customer {
applicationId "${defaultConfig.applicationId}" // 可替换成客户的AppID
}
productFlavors.all {
// 遍历productFlavors多渠道,设置渠道名称,在flutter层也能取到
flavor -> flavor.manifestPlaceholders.put("CHANNEL", name)
}
}
}

之后我们为每个渠道设置资源的名称,每个渠道有不同的资源,避免不相关的资源打包进去,增加包大小。


productFlavors {
// 省略,见上
}
// 为不同渠道指定不同资源文件配置
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
// develop无指定就默认使用src/main/res
squatz.res.srcDirs 'src/main/res-customer'
}

2、配置mainfest


Mainfest在<application>下扩展一个元数据,字段名取build.grade中的风味秒速channel,字段值则是put出去的CHANNEL。其他的都不需要改变,因为mainfest所引用到的资源名称我们都没有改变。


<application>
<!-- 多渠道打包 -->
<meta-data
android:name="channel"
android:value="${CHANNEL}" />
</application>

3、新增对应资源


由于Mainfest的变量名没有变过,因此新增资源的名称就需要跟res中的保持一致


image.png


4、打包编译


flutter build apk --flavor Customer --obfuscate --split-per-abi

打包命令非常简单,指定flavor为build.grade中配置的渠道名称即可,注意首字母大写!


iOS端


笔者并无iOS的实际开发经验,对iOS并不熟悉;但网上对这块的记录真的是少之又少,所以还是决定记录下来,接下来的内容虽成功实践过,但未必是最佳方法,欢迎大家一起交流。


1、分发Target


Target其实是贯穿iOS整个开发过程的,无论是运行目标还是UI控制器,都离不开target;Target是工程编译的目标,其会继承Project的编译设置,并可重新设置自己的编译配置,比如Build SettingBuild Phases



  • 新建Target,直接在原target右键分发一个出来,默认会复制原target的所有配置。


image.png



  • 修改应用信息,注意图标、应用名称等资源另起一个文件夹去配置。


image.png
image.png



  • 打包


自此iOS就有了多个打包目标,非常简单。这也是iOS体系开发比较好的一点,没有太多花里胡哨的玩法,跟着文档配置就好了。

flutter打包命令:flutter build ipa --flavor Customer --release



  • 遇到问题


目前我们遇到如下问题,配置好后在flutter层执行flutter build ios --flavor Customer --release后,会导致xcode重新build项目,然后pod_Runner的动态依赖丢失,但是在xcode中执行又不会。


Flutter端区分渠道


在打包的时候我们可以使用参数-dart-define=CHANNEL=XXXX,其中CHANNEL是参数key,xxxx是name,然后在flutter中使用String.fromEnvironment('CHANNEL', defaultValue: 'develop');,即可获取到key为CHANNEL的值。


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

内存如何记录方法调用和返回过程

全文分为 视频版 和 文字版, 视频版: 通过语音和动画,能够更加直观的看到,内存记录方法调用和返回过程。 bilibili 地址: b23.tv/d5glsFn 文字版 我们在写代码的时候有没有思考过 方法如何调用 、 方法执行完之后如何返回 、 内存如何记...
继续阅读 »

全文分为 视频版文字版


视频版:


通过语音和动画,能够更加直观的看到,内存记录方法调用和返回过程。


bilibili 地址: b23.tv/d5glsFn


文字版


我们在写代码的时候有没有思考过 方法如何调用方法执行完之后如何返回内存如何记录方法调用过程 。而这也是今天这篇文章重点内容。


方法调用和返回过程涉及到了,虚拟机栈、程序计数器、局部变量表、操作数栈、方法返回地址、动态链接等等内容,涉及到知识点很多,同时这些内容也是高频面试题,所以我将拆分成多篇文章,针对每个知识点去做详细的分析。而今天这篇文章我们重点去看内存是如何记录方法调用和返回过程。


虚拟机栈


Java 方法以栈帧的形式,运行在虚拟机栈(Java 栈)中,栈是线程私有的,程序启动的时候,会创建一个 main 线程,操作系统会为每一个线程分配一段内存,线程创建的时候会创建一个虚拟机栈,虚拟机栈的生命周期和线程一样,线程结束了,虚拟机栈也销毁了。


每个 Java 方法,对应一个个栈帧,所以方法开始和结束,都是一个个栈帧入栈和出栈的过程,效果如下图所示。



栈帧


每个 Java 方法,都是一个个栈帧,每个栈帧包括了:局部变量表、操作数栈、方法返回地址、动态链接、附加信息。




  • 局部变量表: 保存方法参数列表和方法内的局部变量,按照声明的顺序存储,它以数组的形式展示,如果是实例方法,索引为 0 是 this 的引用,如下图所示。
























索引(Slot)名字(Name)
0this
1num
2res


  • 操作数栈: 保存方法执行过程中的临时结果

  • 返回地址: 保存调用该方法的 pc 寄存器的值(即 JVM 指令地址),用于方法结束时,返回调用处,让调用者方法继续执行下去

  • 动态链接: 指向运行时常量池中该栈帧所属方法的引用,即从常量池中找到目标方法的符号引用,然后转换为直接引用

  • 附加信息:比如程序 debug 时添加的一些附件信息(不重要,不需要关心,可忽略)


这里只需要知道它们的作用即可,它们的数据结构、字节码的含义、执行过程等等,后续的文章我将会针对每个知识点去做详细的分析。


方法调用过程


先写一段方法调用的代码,首先会调用 main() 方法之后调用 fun1() 然后调用 fun2() ,如下图所示。



现在我们来演示一下 Java 虚拟机执行这些 JVM 指令的过程,首先会调用 main () 方法。


main () 方法


main() 方法执行流程动画效果如下所示。




  1. 执行指令 0: aload_0,从局部变量表中,读取索引为 0 的值,压入操作数栈中,因为是实例方法,所以索引为 0 的值是 this 的引用




  1. 执行指令 1: iconst_5 ,将常量 5 压入操作数栈中




  1. 执行指令 2: invokevirtual #7,常量 5 和 this 从操作数栈中出栈,然后调用 this.fun1(5)



首先从常量池中找到方法 fun1() 的符号引用,然后通过动态链接将符号引用转换成直接引用,之后调用 this.fun1(5),将方法 fun1() 作为栈帧压入虚拟机栈中,跳转到 fun1(),继续往下执行。



如何从常量池中找到目标方法的符号引用,然后转换成直接引用的过程,将会在后面系列文章中详细分析



在调用 fun1() 之前,fun1() 的局部变量表和方法返回地址已经确定好了。



进入方法 fun1 (int num)


方法 fun1(int num) 执行流程动画效果如下所示。




  1. 执行指令 0: aload_0,从局部变量表中,读取索引为 0 的值,压入操作数栈中。因为是实例方法,所以索引为 0 的值是 this 的引用




  1. 执行指令 1: iload_1,从局部变量表中,读取索引为 1 变量 num 的值,并压入操作数栈




  1. 执行指令 2: invokevirtual #13,num 和 this 从操作数栈中出栈,然后调用 this.fun2(num)



首先从常量池中找到方法 fun2() 的符号引用,然后通过动态链接将符号引用转换成直接引用,之后调用 this.fun2(num),将方法 fun2() 作为栈帧压入虚拟机栈中,跳转到 fun2(),继续往下执行,调用 fun2() 之前,fun2() 的局部变量表和方法返回地址已经确定好了。



进入方法 fun2 (int num)


方法 fun2(int num) 执行流程动画效果如下所示。




  1. 执行指令 0: iload_1,从局部变量表中,读取索引为 1 变量 num 的值,并压入操作数栈中




  1. 执行指令 1: bipush ,将常量 10 压入操作数栈中




  1. 执行指令 3: iadd ,num 和常量 10 从操作数栈中出栈,然后进行相加,将结果压入操作数栈中




  1. 执行指令 4: istore_2,从操作数栈中取出结果,并赋值给局部变量表中索引为 2 的变量 res2




  1. 执行指令 5: iload_2,从局部变量表中,读取索引为 2 的变量 res2 的值,并压入操作数栈中



方法退出过程


每个方法即是一个栈帧,每个栈帧会保存方法返回地址,执行 return 系列指令时,会根据当前栈帧保存的返回地址,返回到调用的位置,继续往下执行。因此会分为两种情况。


异常退出


如果出现了异常且捕获了该异常,则会从异常表里查找 PC 寄存器的地址(JVM 指令地址),返回调用处继续执行。



正常退出时会做以下件事



  • 恢复上一个栈帧局部变量表、操作数栈

  • 如果有返回值,将返回值压入调用者栈帧的操作数栈,是否有返回值根据 return JVM 指令:

    • ireturn :返回值是 booleanbytecharshortint 类型

    • lreturn :返回值是 Long 类型

    • freturn :返回值是 Float 类型

    • dreturn :返回值是 Double 类型

    • areturn :返回值是引用类型

    • return :返回值类型为 void



  • 设置调用者栈帧的 JVM 指令地址

  • 当前栈帧从虚拟机栈中出栈


这篇文章我们只分析正常退出流程,异常退出和正常退出的流程大致都是一样的。正常退出流程动画效果如下所示。




  1. 方法 fun2 结束时,执行最后一条指令 ireturn ,当前栈帧从虚拟机栈中出栈,根据当前栈帧保存的方法返回地址,返回到 fun1 ,恢复 fun1 的局部变量表、操作数栈,将返回结果 res2 保存到调用者(fun1)操作数栈中




  1. 回到方法 fun1(int num) ,执行指令 5: istore_2,变量 res2 从操作数中出栈,赋值给局部变量表中索引为 2 的变量 res1




  1. 执行指令 6: iload_2 ,从局部变量表中,读取索引为 2 变量 res1 的值,并压入操作数栈中




  1. 执行方法 fun1 最后一条指令 7: ireturn,当前栈帧从虚拟机栈中出栈,根据当前栈帧保存的方法返回地址,返回到 main 方法,恢复 main 方法的局部变量表、操作数栈,将返回结果 res2 保存到调用者(main)操作数栈中




  1. 最后回到方法 main 中,执行最后一条指令 5: pop,操作数栈中的元素出栈




  1. 执行最后一条指令 6: return,至此所有的方法调用结束了,方法所占用的内存,也将返回给系统



每次的方法调用,即是栈帧入栈和出栈的过程,同时也需要占用部分内存,用于保存局部变量表、操作数栈、方法返回地址、动态链接、附加信息等等。


当方法执行完,即栈帧出栈,方法调用所占用的内存,也将返回给系统。



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

kotlin - 你真的了解 by lazy吗

背景 kotlin中的语法糖by lazy相信都有用过,但是这里面的秘密却很少有人深究下去,还有网上充斥着大量的文章,却很少能说到本质的点上,所以本文以字节码的视角,揭开by lazy的秘密。 一个例子 class LazyClassTest { v...
继续阅读 »

背景


kotlin中的语法糖by lazy相信都有用过,但是这里面的秘密却很少有人深究下去,还有网上充斥着大量的文章,却很少能说到本质的点上,所以本文以字节码的视角,揭开by lazy的秘密。


一个例子


class LazyClassTest {

val lazyTest :Test by lazy {
Log.i("hello","初始化") 1
Test()
}

fun test(){
Log.i("hello","$lazyTest")
Log.i("hello","$lazyTest")
}
}

如果执行test方法,请问代号为1的log会输出几次呢?答案是1次,明明我们在test方法中执行了两次lazyTest的获取,这其中有什么不为人知的事情吗!?其实这是kotlin在编译的时候给我们施加了魔法。


编译器背后的事情


为了看清楚编译器的事情,我们直接查看编译后的字节码,这里贴出来,后面解释


删除不必要的信息
// access flags 0x18
final static INNERCLASS com/example/newtestproject/LazyClassTest$lazyTest$2 null null

// access flags 0x12
private final Lkotlin/Lazy; lazyTest$delegate
@Lorg/jetbrains/annotations/NotNull;() // invisible

// access flags 0x1
public <init>()V
L0
LINENUMBER 5 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
L1
LINENUMBER 7 L1
ALOAD 0
GETSTATIC com/example/newtestproject/LazyClassTest$lazyTest$2.INSTANCE : Lcom/example/newtestproject/LazyClassTest$lazyTest$2;
CHECKCAST kotlin/jvm/functions/Function0
INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
PUTFIELD com/example/newtestproject/LazyClassTest.lazyTest$delegate : Lkotlin/Lazy;
L2
LINENUMBER 5 L2
RETURN
L3
LOCALVARIABLE this Lcom/example/newtestproject/LazyClassTest; L0 L3 0
MAXSTACK = 2
MAXLOCALS = 1

// access flags 0x11
public final getLazyTest()Lcom/example/newtestproject/Test;
@Lorg/jetbrains/annotations/NotNull;() // invisible
L0
LINENUMBER 7 L0
ALOAD 0
GETFIELD com/example/newtestproject/LazyClassTest.lazyTest$delegate : Lkotlin/Lazy;
ASTORE 1
ALOAD 1
INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object; (itf)
CHECKCAST com/example/newtestproject/Test
L1
LINENUMBER 7 L1
ARETURN
L2
LOCALVARIABLE this Lcom/example/newtestproject/LazyClassTest; L0 L2 0
MAXSTACK = 1
MAXLOCALS = 2

// access flags 0x11
public final test()V
L0
LINENUMBER 13 L0
LDC "hello"
ALOAD 0
INVOKEVIRTUAL com/example/newtestproject/LazyClassTest.getLazyTest ()Lcom/example/newtestproject/Test;
INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
POP
L1
LINENUMBER 14 L1
LDC "hello"
ALOAD 0
INVOKEVIRTUAL com/example/newtestproject/LazyClassTest.getLazyTest ()Lcom/example/newtestproject/Test;
INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
INVOKESTATIC android/util/Log.i (Ljava/lang/String;Ljava/lang/String;)I
POP
L2
LINENUMBER 15 L2
RETURN
L3
LOCALVARIABLE this Lcom/example/newtestproject/LazyClassTest; L0 L3 0
MAXSTACK = 2
MAXLOCALS = 1

我们惊讶的发现,原本的类中居然多出了一个内部类com/example/newtestproject/LazyClassTestlazyTestlazyTest2,命名这么长!没错,它就是编译的时候生成的“魔法的种子”,那么这里内部类有什么特别的地方吗?字节码层面是看不出来的,因为这个这只是编译时期的内容,我们在虚拟机运行的时候来看,它其实是一个实现了一个接口是Lazy的内部类


public interface Lazy<out T> {
public abstract val value: T

public abstract fun isInitialized(): kotlin.Boolean
}

lazy背后的延时加载


为什么用了lazy就有懒加载的效果呢?其实关键就是这个,我们在init阶段可以看到


    getstatic 'com/example/newtestproject/LazyClassTest$lazyTest$2.INSTANCE','Lcom/example/newtestproject/LazyClassTest$lazyTest$2;'
checkcast 'kotlin/jvm/functions/Function0'
INVOKESTATIC kotlin/LazyKt.lazy (Lkotlin/jvm/functions/Function0;)Lkotlin/Lazy;
putfield 'com/example/newtestproject/LazyClassTest.lazyTest$delegate','Lkotlin/Lazy;'

在初始化的时候,只是调用了kotlin/LazyKt.lazy类的一个静态方法,针对属性复制的putfield指令,也只是对LazyClassTest.lazyTest$delegate这个内部类的一个Lkotlin/Lazy对象进行赋值,看起来其实跟我们的lazyTest变量毫无关系。真相是lazyTest具体的赋值操作被隐藏了而已。从这里就可以看到,为什么lazy是如何实现延时加载的!本质就是在初始化的时候只是生成一个内部类,不进行任何对目标对象进行赋值操作罢了!


获取操作


我们再观察一下对于lazyTest变量的访问操作,从字节码看到,每次对变量的获取都调用了LazyClassTest的getLazyTest方法!这个也是编译器生成的方法,具体可以看到


  public final com.example.newtestproject.Test getLazyTest() {
aload 0
getfield 'com/example/newtestproject/LazyClassTest.lazyTest$delegate','Lkotlin/Lazy;'
astore 1
aload 1
INVOKEINTERFACE kotlin/Lazy.getValue ()Ljava/lang/Object; (itf)
checkcast 'com/example/newtestproject/Test'
areturn
}

天呐!我们越来越接近终点了,首先是通过getfield指令获取了一个Lkotlin/Lazy变量,这个不就是上面我们赋值的东西吗!然后调用了一个普通的方法getValue就结束了,也就是说,每次对lazyTest变量的访问,都间接转发到了一个编译时生成的内部类中的一个特殊属性所调用的方法!看到这个,读者可能会思考,既然每次访问都是调用同一个方法,为什么我们by lazy时声明的lambad会只执行一次呢?编译时的字节码已经不能给我们带来答案了,这个因为像java虚拟机这种,关于具体类的调用会在运行时确定这个特性所带来的(区别于c/cpp)。


运行时的魔法


那好,我们还有最后一个神奇,就是debug,我们最终会发现,在运行时by lazy的调用,其实最终都会转到如下代码的执行


private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this

override val value: T
get() {
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}

return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}

这个类位于LazyJVM中(kotlin1.5.10),我们就找到最终的秘密了,原来一开始的时候变量就是UNINITIALIZED_VALUE,经过一次赋值操作后,就会变成实际的T所指代的类型,下次再访问的时候,就直接满足if条件返回了!所以这就是一次赋值的秘密!还有我们可以看到,默认的by lazy操作第一次赋值时,是采用了synchronized进行了加锁操作!


总结


我们已经全方位揭秘了by lazy的魔法面纱,相信也对这个语法糖有了自己更深的理解,之所以写这篇文,是因为好多网上资料要么是含糊不清要么是无法解释本质,这里作为一个记录分享


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

Android自定义评分控件

无意中翻到几年前写过的一个RatingBar,可以拖拽,支持自定义星星图片,间距大小等参数。 自定义参数 为了方便扩展,支持更多的样式,这里将大部分参数设置成支持外部可配置的形式。 <declare-styleable name="RatingBarP...
继续阅读 »

无意中翻到几年前写过的一个RatingBar,可以拖拽,支持自定义星星图片,间距大小等参数。


GIF.gif


自定义参数


为了方便扩展,支持更多的样式,这里将大部分参数设置成支持外部可配置的形式。


<declare-styleable name="RatingBarPlus">
<attr name="hideImageResource" format="reference"/>
<attr name="showImageResource" format="reference"/>
<attr name="starSpace" format="dimension"/>
<attr name="maxStar" format="integer"/>
<attr name="stepSize" format="float"/>
<attr name="rating" format="float"/>
<attr name="starWidth" format="dimension"/>
<attr name="starHeight" format="dimension"/>
</declare-styleable>


  • hideImageResource 暗星星图片id

  • showImageResource 亮星星图片id

  • starSpace 星星间距

  • maxStar 星星最大个数

  • stepSize 评分步长,即能不能选中0.1个星

  • rating 默认评分

  • starWidth 星星宽度

  • starHeight 星星高度


解析参数


微信截图_20220526215020.png
创建星星位图的时候需要根据配置的大小和图片本身的宽高进行缩放。


绘制


微信截图_20220526215753.png
绘制完成之后我们就可以动态设置评分来回显之前的评分,但是经常我们需要与控件交互,动态地设置分数,所以我们还需要重写onTouchEvent方法完成事件处理。


事件处理


微信截图_20220526220208.png


评分需要随着手指的移动而动态变化,这里我们记录下当前手指所在的位置,如果在星星上面,就算出当前位置距离星星左边的长度占据整个星星宽度的百分比,然后根据设置的stepSize参数动态微调总评分。


评分监听


我们还需要将评分暴露给外部,处理主动调用getRating()方法获取之外,我们还可以提供一个监听接口,实时提供回调。


功能事件比较简单,只需要在事件处理的时候,微调总评分完成之后回调一下数据就可以了。


if (onRatingChangeListener != null) {
onRatingChangeListener.onRatingChange(rating);
}

外部使用


ratingBar.setOnRatingChangeListener{
ratingText.text = "当前评分:${it}"
}

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

Gradle 渠道包配置

Gradle 渠道包配置 安卓项目中默认使用gradle作为构建工具,gradle默认提供了很多Task,开发者也可以自己新建Task构建脚本,让打包、开发达到事半功倍的效果。这篇文章主要讲解安卓项目中常见的打包脚本。 基本任务 用gradle创建一个简单...
继续阅读 »

Gradle 渠道包配置



安卓项目中默认使用gradle作为构建工具,gradle默认提供了很多Task,开发者也可以自己新建Task构建脚本,让打包、开发达到事半功倍的效果。这篇文章主要讲解安卓项目中常见的打包脚本。



基本任务


用gradle创建一个简单的输出脚本。



  • 在安卓项目的build.gradle中的android{}中添加以下脚本


task myTask{
println 'this is my task'
}


  • 点击Sync Now之后,在Terminal中运行


./gradlew myTask

就可以打印出'this is my task',不仅仅是使用命令,也可以在开发工具AndroidStuido右侧的Gradle中找到Task -> Other -> myTask,点击运行也是一样的效果。


常见任务


渠道包配置


同一套代码可以打包出多个应用程序,它们的包名不同、图标不同、应用名称不同,这样就可以一个手机上共存多个应用程序。


如何操作:



  • 在app的build.gradle文件的android{}标签内


productFlavors {
// 产品版本1
product1 {
applicationId "com.android.application1"
manifestPlaceholders = [app_name:"产品1", app_ico: "@mipmap/ico1"]
}
// 产品版本2
product2 {
applicationId "com.android.application2"
manifestPlaceholders = [app_name:"产品2", app_ico: "@mipmap/ico2"]
}
// 产品版本3
product3 {
applicationId "com.android.application3"
manifestPlaceholders = [app_name:"产品3", app_ico: "@mipmap/ico3"]
}
}

product1、product2、product3是指不同的版本,applicationId对应的包名,manifestPlaceholders中的app_nameapp_ico代表的是应用名称和应用图标。



  • 相应的让应用名称和应用图标生效,还需要在AndroidManifest.xml中添加“变量”


<application
android:icon="${app_ico}"
android:label="${app_name}"
android:roundIcon="${app_ico}"
>


  • 在android标签内defaultConfig标签下添加


flavorDimensions "XXX"

flavorDimensions比较特殊,有多维度的理解,比如


A公司的A渠道产品,A公司的B渠道产品,B公司的A渠道产品,B公司的B渠道产品


详细了解可以看这篇文章flavorDimensions


为渠道添加动态变量


添加buildConfigField的内容


productFlavors {
// 产品版本1
product1 {
applicationId "com.android.application1"
manifestPlaceholders = [app_name:"产品1", app_ico: "@mipmap/ico1"]
buildConfigField "String","FLAVOR_NAME","\"product111\""
}
// 产品版本2
product2 {
applicationId "com.android.application2"
manifestPlaceholders = [app_name:"产品2", app_ico: "@mipmap/ico2"]
buildConfigField "String","FLAVOR_NAME","\"product222\""
}
// 产品版本3
product3 {
applicationId "com.android.application3"
manifestPlaceholders = [app_name:"产品3", app_ico: "@mipmap/ico3"]
buildConfigField "String","FLAVOR_NAME","\"product333\""
}
}

添加完成之后Rebuild Project,然后在Activity中就使用BuildConfig.FLAVOR_NAME可以进行判断使用了。


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

Java泛型详解

1 为什么需要泛型? 示例1: /** * @Description: 不使用泛型 * @CreateDate: 2022/3/18 3:08 下午 */ public class NoGeneric { private int addInt...
继续阅读 »

1 为什么需要泛型?


示例1:


/**
* @Description: 不使用泛型
* @CreateDate: 2022/3/18 3:08 下午
*/
public class NoGeneric {

private int addInt(int x, int y) {
return x + y;
}

private float addFloat(float x, float y) {
return x + y;
}

public static void main(String[] args) {
NoGeneric noGeneric = new NoGeneric();
System.out.println(noGeneric.addInt(1, 2));
System.out.println(noGeneric.addFloat(1f, 2f));
}

}

实际开发中,经常有数值类型求和的需求,例如实现int类型的加法, 有时候还需要实现float类型的求和, 如果还需要float类型的求和,需要重新在重载一个输入是float类型的add方法。每种类型的数据都需要重载一个方法,非常繁琐。如果使用泛型,就可以只定义一个方法。


示例2:


不使用泛型


private static void noGeneric() {
List list = new ArrayList<>();
list.add("x");
list.add("y");
list.add(1);//不使用泛型,List中可以添加任何类型的元素,但是获取数据的时候会报错

for (int i = 0; i < list.size(); i++) {
String str = (String) list.get(i);
System.out.println(str);
}
}

定义了一个List类型的集合,先向其中加入了两个字符串类型的值,随后加入一个Integer类型的值。这是完全允许的,因为此时List默认的类型为Object类型。在之后的循环中,由于忘记了之前在List中也加入了Integer类型的值或其他编码原因,很容易出现类强转错误。因为编译阶段正常,而运行时会出现“java.lang.ClassCastException”异常。因此,导致此类错误编码过程中不易发现。


运行后会报类型转换异常:



使用泛型,编译器就会提示类型不匹配。



在如上的编码过程中,我们发现主要存在两个问题


1.当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,改对象的编译类型变成了Object类型,但其运行时类型任然为其本身类型。


2.因此,取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现“java.lang.ClassCastException”异常。


所以泛型的好处就是:



  • 适用于多种数据类型执行相同的代码

  • 泛型中的类型在使用时指定,不需要强制类型转换,避免了可能出现的类型转换异常


2 泛型类和泛型接口


泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?


顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。


泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。


引入一个类型变量T(其他大写字母都可以,不过常用的就是T,E,K,V等等),并且用<>括起来,并放在类名的后面。泛型类是允许有多个类型变量的。


泛型类


/**
* @Description: 泛型类
* @CreateDate: 2022/3/18 3:51 下午
*/
public class NormalGenericClass<T> {
private T data;

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}

public static void main(String[] args) {
NormalGenericClass<String> normalGenericClass = new NormalGenericClass<>();
normalGenericClass.setData("A");
System.out.println(normalGenericClass.getData());

NormalGenericClass normalGenericClass2 = new NormalGenericClass();
normalGenericClass2.setData(1);
normalGenericClass2.setData("xyz");
}
}

/**
* @Description: 泛型类
* @CreateDate: 2022/3/18 3:58 下午
*/
public class NormalGenericClass2<T, R> {
private T data;
private R result;

public NormalGenericClass2() {
}

public NormalGenericClass2(T data) {
this();
this.data = data;
}

public NormalGenericClass2(T data, R result) {
this.data = data;
this.result = result;
}

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}

public R getResult() {
return result;
}

public void setResult(R result) {
this.result = result;
}

public static void main(String[] args) {
NormalGenericClass2<String, Integer> normalGenericClass = new NormalGenericClass2<>();
normalGenericClass.setData("A");
System.out.println(normalGenericClass.getData());

normalGenericClass.setResult(1);
System.out.println(normalGenericClass.getResult());
}
}

泛型接口


而实现泛型接口的类,有两种实现方法:


先写一个泛型接口:


public interface IGeneric<T> {
T next();
}

1、未传入泛型实参


public class GenericImpl<T> implements IGeneric<T> {

private T data;

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}

@Override
public T next() {
return data;
}

}

使用的时候需要指定具体类型:


GenericImpl<String> genericImpl = new GenericImpl<>();
genericImpl.setData("A");
System.out.println(genericImpl.getData());

2、传入泛型实参


public class GenericImpl2 implements IGeneric<String> {
@Override
public String next() {
return "Hello World";
}
}

使用的时候和普通类一样:


GenericImpl2 genericImpl2 = new GenericImpl2();
System.out.println(genericImpl2.next());

3 泛型方法


public class GenericMethod {

/**
* 方法名前边的T表示返回类型
*/
public <T> T genericMethod(T... t) {
return t[t.length / 2];
}

public static void main(String[] args) {
GenericMethod genericMethod = new GenericMethod();
System.out.println(genericMethod.genericMethod("A", "B", "C"));
System.out.println(genericMethod.genericMethod(11, 22, 33));
}
}

泛型方法,是在调用方法的时候指明泛型的具体类型 ,泛型方法可以在任何地方和任何场景中使用,包括普通类和泛型类。注意泛型类中定义的普通方法和泛型方法的区别。


/**
* @Description: 泛型类中普通方法和泛型方法
* @CreateDate: 2022/3/18 4:57 下午
*/
public class Generic<T> {
private T t;

public Generic(T t) {
this.t = t;
}

/**
* 普通方法
* 虽然此方法中使用了泛型,但这只是一个普通方法,只是它的返回值是泛型类中已经声明的泛型。
*
* @return T
*/
public T getData1() {
return t;
}

/**
* 泛型方法
* 在public和返回类型之间的<T>必不可少。表明了这是一个泛型方法。
*/
public <T> T getData(T t) {
return t;
}
}

4 限定类型变量


有时候,我们需要对类型变量加以约束,比如计算两个变量的最小,最大值。



这个方法需要确保传入的两个变量有compareTo方法,那么就需要将T限制为实现了Comparable的类,如下所示:


public static <T extends Comparable> T min(T a, T b) {
if (a.compareTo(b) > 0) {
return a;
} else {
return b;
}
}

T表示应该绑定类型的子类型,Comparable表示绑定类型,子类型和绑定类型可以是类也可以是接口。如果这个时候,我们试图传入一个没有实现接口Comparable的类的实例,将会发生编译错误。



同时extends左右都允许有多个,如 K,T extends Comparable & Serializable。注意限定类型中,只允许有一个类,而且如果有类,这个类必须是限定列表的第一个。这种类的限定既可以用在泛型方法上也可以用在泛型类上。


/**
* 限定一个接口
*/
public static <T extends Comparable> T min(T a, T b) {
if (a.compareTo(b) > 0) {
return a;
} else {
return b;
}
}

/**
* 限定多个接口
*/
public static <T extends Comparable & Serializable> T min2(T a, T b) {
if (a.compareTo(b) > 0) {
return a;
} else {
return b;
}
}

/**
* extends左侧也可以定义多个泛型
*/
public static <K, T extends Comparable & Serializable> T min3(T a, T b) {
if (a.compareTo(b) > 0) {
return a;
} else {
return b;
}
}

/**
* 如果限定类型中有类,类只能有一个,且必须放在第一个。比如该方法中的ArrayList类限定
*/
public static <K, T extends ArrayList & Comparable & Serializable> T min3(T a, T b) {
if (a.compareTo(b) > 0) {
return a;
} else {
return b;
}
}

5 泛型中的约束和局限性


首先定义泛型类:


public class GenericRestrict<T> {
//...
}

5.1 不能用基本类型实例化参数


//不能用基本数据类型实例化类型参数
//GenericRestrict<double> genericRestrict=new GenericRestrict<>();//不允许
GenericRestrict<Double> genericRestrict = new GenericRestrict<>();

5.2 运行时查询类型只适用于原始类型


//运行时查询类型只适用于原始类型
// if (genericRestrict instanceof GenericRestrict<Double>)//不允许
// if (genericRestrict instanceof GenericRestrict<T>) //不允许
GenericRestrict<String> stringGenericRestrict = new GenericRestrict<>();
System.out.println(genericRestrict.getClass() == stringGenericRestrict.getClass());
System.out.println(genericRestrict.getClass().getName());
System.out.println(stringGenericRestrict.getClass().getName());

运行结果:


true
site.exciter.learn.generic.GenericRestrict
site.exciter.learn.generic.GenericRestrict

5.3 泛型类的静态上下文中类型变量失效


//静态域或方法里不能引用类型变量
// private static T instance;//不允许
// private static T getInstance1(){//不允许
// return null;
// }
//静态方法 本身是泛型方法的话可以
private static <T> T getInstance2() {
return null;
}

不能在静态域或方法中引用类型变量。因为泛型是要在对象创建的时候才知道是什么类型的,而对象创建的代码执行先后顺序是static的部分,然后才是构造函数等等。所以在对象初始化之前static的部分已经执行了,如果你在静态部分引用的泛型,那么毫无疑问虚拟机根本不知道是什么东西,因为这个时候类还没有初始化。


5.4 不能创建参数化类型的数组


//不能创建参数化类型的数组
GenericRestrict<Float>[] arrayGenericRestrict;//允许
// GenericRestrict<Float>[] restricts=new GenericRestrict<Float>[10];//不允许

5.5 不能实例化类型变量


private T data;

//不能实例化类型变量
// public GenericRestrict() {
// this.data = new T();
// }

5.6 不能捕获泛型类的实例


public class GenericExceptionRestrict {

//泛型类型不能extends Exception/Throwable
// private class Problem<T> extends Exception{}

//不能捕获泛型类对象
// public <T extends Throwable> void doWork(T t){
// try {
//
// }catch (T e){
//
// }
// }

//但可以这样写
public <T extends Throwable> void doWork(T t) throws T {
try {

} catch (Throwable e) {
throw t;
}
}
}

6 泛型类的继承规则


定义一个游戏类和它的子类LOL:


public class Game {
private String name;
private String type;

public String getName() {
return name;
}

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

public String getType() {
return type;
}

public void setType(String type) {
this.type = type;
}
}

public class LOL extends Game{
}

定义一个泛型类:


public class Pair<T> {
private T one;
private T two;
//...
}

那么Pair<Game>和Pair<LOL> 有继承关系吗?


他们没有任何继承关系。


Game game = new LOL();
// Pair<Game> gamePair2=new Pair<LOL>();//不允许

但是泛型类可以继承或者扩展其他泛型类:


private static class ExtendPair<T> extends Pair<T> {

}

Pair<Game> gamePair3 = new ExtendPair<>();

完整代码:


public class Pair<T> {
private T one;
private T two;

public T getOne() {
return one;
}

public void setOne(T one) {
this.one = one;
}

public T getTwo() {
return two;
}

public void setTwo(T two) {
this.two = two;
}

private static <T> void set(Pair<Game> p) {
}

public static void main(String[] args) {

//Pair<Game>和Pair<LOL>没有任何继承关系
Pair<Game> gamePair = new Pair<>();
Pair<LOL> lolPair = new Pair<>();

Game game = new LOL();
// Pair<Game> gamePair2=new Pair<LOL>();//不允许
Pair<Game> gamePair3 = new ExtendPair<>();

set(gamePair);
// set(lolPair);//不允许
}

private static class ExtendPair<T> extends Pair<T> {

}
}

7 通配符的类型


定义一个泛型类:


public class GenericType<T> {
private T data;

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}
}

定义三个对象,继承关系如下:


/**
* @Description: 宠物
* @CreateDate: 2022/3/21 11:11 上午
*/
public class Pet {
private String color;

public String getColor() {
return color;
}

public void setColor(String color) {
this.color = color;
}
}

/**
* @Description: 猫
* @CreateDate: 2022/3/21 11:12 上午
*/
public class Cat extends Dog{
}

/**
* @Description: 狗
* @CreateDate: 2022/3/21 11:12 上午
*/
public class Dog extends Pet{
}

/**
* @Description: 哈士奇
* @CreateDate: 2022/3/21 11:14 上午
*/
public class Husky extends Dog{
}

这时候会出现以下情况:


public static void print(GenericType<Pet> g) {
System.out.println(g.getData().getColor());
}

public static void method() {
GenericType<Pet> g1 = new GenericType<>();
print(g1);
GenericType<Cat> g2 = new GenericType<>();
// print(g2);//不允许
GenericType<Dog> g3 = new GenericType<>();
// print(g3);//不允许
GenericType<Husky> g4 = new GenericType<>();
// print(g4);//不允许
}

为了解决这种问题,出现了通配符。


有两种使用方式:


?extends X表示类型的上界,类型参数是X的子类。


?super X表示类型的下界,类型参数是X的超类。


7.1 ?extends X


表示传递给方法的参数,必须是X的子类(包括X本身)。


public static void method2() {
GenericType<Pet> g1 = new GenericType<>();
print2(g1);
GenericType<Cat> g2 = new GenericType<>();
print2(g2);

GenericType<? extends Pet> g3 = new GenericType<>();
Pet pet = new Pet();
Cat cat = new Cat();
// g3.setData(pet);//不允许
// g3.setData(cat);//不允许
Pet p = g3.getData();
}

public static void print3(GenericType<? super Dog> g) {
System.out.println(g.getData());
}

但是对泛型类GenericType来说,如果其中提供了getset类型参数变量的方法的话,set方法是不允许被调用的,会出现编译错误;get方法则没问题,会返回一个Pet类型的值。


道理很简单,? extends X 表示类型的上界,类型参数是X的子类,那么可以肯定的说,get方法返回的一定是个X(不管是X或者X的子类)编译器是可以确定知道的。但是set方法只知道传入的是个X,至于具体是X的那个子类,不知道。


总结:主要用于安全地访问数据,可以访问X及其子类型,并且不能写入非null的数据。


7.2 ?super X


表示传递给方法的参数,必须是X的超类(包括X本身)。


public static void method3() {
GenericType<Pet> petGenericType = new GenericType<>();
GenericType<Dog> dogGenericType = new GenericType<>();
GenericType<Husky> huskyGenericType = new GenericType<>();
GenericType<Cat> catGenericType = new GenericType<>();
print3(petGenericType);
print3(dogGenericType);
// print3(huskyGenericType);//不允许
// print3(catGenericType);//不允许

GenericType<? super Dog> g = new GenericType<>();
// g.setData(new Pet());//不允许
g.setData(new Dog());
g.setData(new Husky());
Object o = g.getData();
}

但是对泛型类GenericType来说,如果其中提供了getset类型参数变量的方法的话,set方法可以被调用的,且能传入的参数只能是X或者X的子类get方法只会返回一个Object类型的值。


? super X表示类型的下界,类型参数是X的超类(包括X本身),那么可以肯定的说,get方法返回的一定是个X的超类,那么到底是哪个超类?不知道,但是可以肯定的说,Object一定是它的超类,所以get方法返回Object。编译器是可以确定知道的。对于set方法来说,编译器不知道它需要的确切类型,但是X和X的子类可以安全的转型为X。


总结:主要用于安全地写入数据,可以写入X及其子类型。


7.3 无限定通配符 ?


表示对类型没有什么限制,可以把?看成所有类型的父类。


//指定集合元素只能是T类型
List<T> list=new ArrayList<>();
//集合元素可以是任意类型,这种没有意义,一般是方法中,只是为了说明用法。
List<?> list2=new ArrayList<>();

8 虚拟机是如何实现泛型的?


泛型思想早在C++语言的模板(Template)中就开始生根发芽,在Java语言处于还没有出现泛型的版本时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。,由于Java语言里面所有的类型都继承于java.lang.Object,所以Object转型成任何对象都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会转嫁到程序运行期之中。


泛型技术在C#和Java之中的使用方式看似相同,但实现上却有着根本性的分歧,C#里面泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符),或是运行期的CLR中,都是切实存在的,List<int>List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。


Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList<int>ArrayList<String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。


将一段Java代码编译成Class文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都不见了,程序又变回了Java泛型出现之前的写法,泛型类型都变回了原生类型。



上面这段代码是不能被编译的,因为参数List<Integer>List<String>编译之后都被擦除了,变成了一样的原生类型List<E>,擦除动作导致这两种方法的特征签名变得一模一样。


由于Java泛型的引入,各种场景(虚拟机解析、反射等)下的方法调用都可能对原有的基础产生影响和新的需求,如在泛型类中如何获取传入的参数化类型等。因此,JCP组织对虚拟机规范做出了相应的修改,引入了诸如SignatureLocalVariableTypeTable等新的属性用于解决伴随泛型而来的参数类型的识别问题,Signature是其中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名[3],这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息。修改后的虚拟机规范要求所有能识别49.0以上版本的Class文件的虚拟机都要能正确地识别Signature参数。


另外,从Signature属性的出现我们还可以得出结论,擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。


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

抖音功耗优化实践

功耗优化是应用体验优化的一个重要课题,高功耗会引发用户的电量焦虑,也会导致糟糕的发热体验,从而降低了用户的使用意愿。而功耗又是涉及整机的长时间多场景的综合性复杂指标,影响因素很多。不论是功耗的量化拆解,还是异常问题的监控,以及主动的功耗优化对于开发人员来说都是...
继续阅读 »

功耗优化是应用体验优化的一个重要课题,高功耗会引发用户的电量焦虑,也会导致糟糕的发热体验,从而降低了用户的使用意愿。而功耗又是涉及整机的长时间多场景的综合性复杂指标,影响因素很多。不论是功耗的量化拆解,还是异常问题的监控,以及主动的功耗优化对于开发人员来说都是很有挑战性的。

本文结合抖音的功耗优化实践中产出了一些实验结论,优化思路,从功耗的基础知识,功耗组成,功耗分析,功耗优化等几个方面,对 Android 应用的功耗优化做一个总结沉淀。

功耗基础知识介绍

首先我们回顾一下功耗的概念,这里比较容易和能耗搞混。解释一下为什么手机上用mA(电流值)来表征功耗水平,用 mAh(物理意义上是电荷值)来表征能耗水平。我们先来看几个物理公式。

P = I × U, E = P × T

能耗(E):即能量损耗,指计算机系统一段时间内总的能量消耗,单位是焦耳(J)

功耗(P):即功率损耗,指单位时间内的能量消耗,反映消耗能量的速率,单位是瓦特(W)

电流(I):指手机电池放电的电流值,手机常用 mA 为单位

电压(U):指手机电池放电的电压值,标准放电电压 3.7V,充电截止电压 4.35V,放电截止电压 2.75V(以典型值举例,不同设备的电池电压数值有差异)

电池容量 :常用单位 mAh,从单位意义上看是电荷数,实际表征的是电池以典型电压放电的时长。
如下面的功耗测试图所示,手机通常以恒定的典型电压工作,为了计算方便,就把电压恒定为 3.7V,那么 P = I × 3.7, E = I × 3.7 × T,即用 mA 表征功耗,mAh 表征能耗。

总结:对同一机型,我们用电池容量(mAh)变化的来表征一段时间总能耗,用平均电流(mA)来表征功耗水平;如 4000mAh 电池的手机刷抖音 1 小时耗电 11%,耗电量(能耗)440mAh,平均电流 440mA

56b1d8fa1453546f4ae73c83cc8b43e3.png图 1. 功耗测试图

为什么要做功耗优化

从摘要里我们已经了解到高功耗会引发用户的电量焦虑,也会导致糟糕的发热体验,从而降低了用户的使用意愿。优化功耗除了可以我们带来更好的用户体验,提升用户使用时长外,降低应用耗电还具有很明显的社会价值,用一个当前比较火的词,就是可以为碳中和事业贡献一份力量。

如何来做功耗优化

不同于 Crash、ANR 等常见的 APM 指标,功耗是一个综合性的课题,分析起来很容易让人无从下手。用户反馈了耗电问题,可能是 CPU 出现高负载,又或者是后台频繁的网络访问,也可能是动画泄漏导致高功耗。或者我们自己的业务没什么变化,单纯就是环境因素影响,导致用户觉得耗电,比如低温导致的锂电池放电衰减。

我们的思路是从器件出发,应用的耗电最终都可以分解为手机器件的耗电,所以我们先对抖音做器件耗电的拆解,看主要耗电的是哪些器件,再看如何减少器件的使用,这样就做到有的放矢。

下面我们先从功耗组成,功耗分析,以及功耗优化等方面来讲述如何开展功耗优化。

功耗组成

74009e91e4e5746fc8ad1aa005259636.png

这里列举了手机硬件的基本形态,每个模块又是由复杂的器件构成。如我们常说的耗电大头 SoC 里就包含 CPU 的超大核,大核,小核,GPU,DDRC(内存接口),以及外设区的各种小 IP 核等。所以整机的功耗最终就可以拆解为各个器件的功耗,而应用的功耗就是计算其使用的器件产生的功耗。

以抖音的 Feed 流场景为例,亮度固定 120nit、7 格音量、WiFi 网络下,我们对抖音做了器件级的功耗拆解。可以看到抖音的 feed 功耗主要集中在 SOC(CPU,GPU,DDR),Display,Audio,WIFI 等四个模块。

3b4e51733501d0a2ed954100adecc27b.png

器件功耗计算

那这些器件功耗是如何被拆解出来的呢?原理是:先对器件进行耗电因子拆解,建立器件功耗模型,得到一个器件耗电的计算公式。通过运行时统计器件的使用数据,代入功耗模型,就可以计算出器件的功耗。应用的功耗则是从器件的总功耗里按应用使用的比较进行分配,这样就得到了应用的器件耗电。由于影响器件功耗的耗电因子众多,这里复杂的就是如何对耗电因子进行拆解以及建模。有了精准的建模,后面就是厂商适配校准参数的过程了。

谷歌提供了一套通用的器件耗电模型和配置方案,OEM 厂商可以按通用方案对自己的产品进行参数校准和配置。如下图里 AOSP 里的耗电配置里,以 Wifi 的耗电计算为例。https://source.android.com/devices/tech/power/values

8e4128e236778f64ff6f8b04c5841d32.png 856eea8e637b57bd63189a5c0fb54d6e.png

谷歌提供的建模方案是对 WIFI 分状态计算耗电,WIFI 不同状态下的耗电差异非常明显。这里分为了 wifi.on(对应 wifi 打开的基准电流), wifi.active(对应 wifi 传输数据时的基准电流), wifi.scan(对应 wifi 单次扫描的基准耗电), wifi 数据传输的耗电(controller.rx,controller.tx, controller.idle)。根据 wifi 收发数据的那计算 wifi 的耗电,通过统计这几个状态的时长或次数,乘以对应的电流,就得到 wifi 器件的耗电了。

由于谷歌是按照通用性来设计的器件耗电模型,通常只能大致计算出器件的耗电水平,具体到某个产品上可能误差很大。各 OEM 厂商通常有基于自身硬件的耗电统计方案,可以对耗电做更加精细准确的计算。这里还用 wifi 举例:如 OEM 厂商可以分别按照 2.4G,5GWIFI 单独建模,并引入天线信号的变化对应的基准电流变化,以及统计 wifi 芯片所工作的频点时长,按频点细化模型等等,OEM 厂商可以设计出更符合自己设备的精准功耗模型,计算出更精准的 wifi 耗电。这就要根据具体产品的硬件方案来确定了。

功耗分析

通过上面的功耗组成的介绍,我们可以看到功耗影响因素是多种多样。在做应用功耗分析时,我们既要有方法准确评估应用的耗电水平,又要有方法来分解出耗电的组成,以找到优化点。下面就分为功耗评估和功耗归因分析这两部分来介绍。

功耗评估

如前文功耗基础知识里所说,我们使用电流值来评估应用的功耗水平。在线下场景,我们通过控制测试条件(如固定测试机型版本,清理后台,固定亮度,音量,稳定的网络信号条件等)来测得可信的准确电流值来评估应用的前后台功耗。在线上场景,由于应用退后台时,用户使用场景的复杂性(指用户运行的前台应用不同),我们只采集前台整机电流来做线上版本监控,使用其他指标,如后台 CPU 使用率来监控后台功耗。下面我们介绍一些常用功耗评估的手段。

PowerMonitor

目前业界最通用的整机耗电评估方式是通过 PowerMonitor 外接电量计的方式,高频率高精度采集电流进行评估。常用需要精细化确认耗电情况,尤其是后台静置,灭屏等状态下的电流输出,厂商的准入测试等。常用的 Mosoon 公司的 PowerMonitorAAA10F,电流量程在 1uA ~ 6A 之间,电流精度 50uA,采样周期 200us (5KHZ)。

1f8375ab851431fd1c69e252748f8923.png

电池电量计

PowerMonitor 虽然测量结果最准确。但是需要拆机比较麻烦。我们还可以通过谷歌 BatteryManager 提供的接口直接读取电池电量计的统计结果来获得电流值。

电池电量计负责估计电池容量。其基本功能为监测电压,充电/放电电流和电池温度,并估计电池荷电状态(SOC)及电池的完全充电容量(FCC)。有两种典型的电量计:电压型电量计和电流型电量计,目前手机上使用的电量计主要是电流型电量计。

  • 电压型电量计:简单讲就是检测当前电压,然后查询电压-电池容量对应表,获得电量估算

  • 电流型电量计:也叫库仑计,原理是在电池的充电/放电路径上的连接一个检测电阻。ADC 量测在检测电阻上的电压,转换成电池正在充电或放电的电流值。实时计数器(RTC)则提供把该电流值对时间作积分,从而得知流过多少库伦。

a69d07da70d94ebb4e9a321d61d3addf.png

Android 提供了 BMS 的接口,通过属性提供了电池电量计的统计结果

  • BATTERY_PROPERTY_CHARGE_COUNTER 剩余电池容量,单位为微安时

  • BATTERY_PROPERTY_CURRENT_NOW 瞬时电池电流,单位为微安

  • BATTERY_PROPERTY_CURRENT_AVERAGE 平均电池电流,单位为微安

  • BATTERY_PROPERTY_CAPACITY 剩余电池容量,显示为整数百分比

  • BATTERY_PROPERTY_ENERGY_COUNTER 剩余能量,单位为纳瓦时

import android.os.BatteryManager;
import android.content.Context;
BatteryManager mBatteryManager = (BatteryManager)Context.getSystemService(Context.BATTERY_SERVICE);
Long energy = mBatteryManager.getLongProperty(BatteryManager.BATTERY_PROPERTY_ENERGY_COUNTER);
Slog.i(TAG, "Remaining energy = " + energy + "nWh");

以下面的 Nexus9 为例,该机型使用了 MAX17050 电流型电量计,解析度 156.25uA,更新周期 175.8ms。

069b692c3798cce7926c8d1f663c878c.png

从实践结果上看,由于不同的手机使用的电量计不同,导致直接读取出来的电流值单位也不同,需要做数据转化。为了简化电池数据的获取,我们开发了 Thor SDK,只保留电流、电压、电量等指标的采集过程,针对不同机型做了数据归一处理,用户可以不用关心内部实现,只需要提供需要采样的数据类型、采样周期就可以定时返回所需要的功耗相关的数据,我们用 Thor 对比 PowerMonitor 进行了数据一致性的校验,误差<5mA,满足线上监控需求。

此外我们做了 Thor 采集功能本身的功耗影响,可以看到 1s 采集 1 次的情况下,平均电流上涨了 0.59mA,所以说这种方案的功耗影响非常低,适合线上采集电流值。

ae60a2f641bd750eae5f2bcc67bbc61d.png

厂商自带耗电排行

耗电排行

厂商提供的耗电排行也可以用来查看一段时间内的应用耗电情况。如下面华为的耗电排行里,对硬件和软件耗电进行了分拆,并给出了应用的具体耗电量。其他厂商 OV 也是支持具体的耗电量,小米则是提供耗电占比,并不会提供具体耗电量。

入口:设置->电池->耗电排行

0ff8e33b865fb9b7f3200f033d5387d7.png

功耗归因

从功耗评估我们可以判断应用的整体耗电情况,但具体到某个 case 高耗电的原因是什么,就要具体问题选择不同的工具来进行分析了。目前可以直接归因到业务代码的主要是 CPU 相关的工具,这也是我们目前分析问题的主要方向,后续我们也会建设流量归因等能力,下面我列举了常用的分析工具。

Battery Historian

谷歌官方提供的分析工具,需要先进行功耗测试,再通过 adb 抓取 bugreport.zip,再通过网页工具打开,可提供粗粒度的功耗归因。

本质上是对 systemserver 里的各种服务统计信息+手机状态+内核统计信息(kernel 唤醒)的展示,应用耗电的估算依赖厂商配置的 power_profile.xml。比较适合对整机耗电问题做耗电归因,如归因到某应用耗电较高。

对于单个应用,由于对 wakelock,alarm,gps,job,syncservice,后台服务运行时长等统计的比较详细,比较适合做后台耗电的归因。对于网络异常,CPU 异常,只能看到消耗较多,无法归因到具体业务。https://developer.android.com/topic/performance/power/setup-battery-historian?hl=zh-cn

e772a23905e645b55f9cc135090adf41.png fa8dc50ef240559b5528ddaade6bfc11.png

AS Profiler

相比于 BatteryHistorian 需要先手动测试,再 adb 抓取的操作繁琐,AS 自带的 Profiler 提供了 Energy 的可视化展示。使用 debug 版本的应用,可以直观的看到功耗的消耗情况,方便了线下测试。需要注意的是这里展示的功耗值是通过 GPS+网络+CPU 计算的拟合值,并不是真实功耗值,只表征功耗水平。

cd98bf76f3b1f933e32c7aff372deea2.png

Profiler 同步展示了 CPU 使用率,网络耗电,内存信息。支持 CPU 和线程级别的跟踪。通过主动录制 Trace,可以分析各线程的 CPU 使用情况,以及耗时函数。对于容易复现的 CPU 高负载问题或者固定场景的耗时问题,这种方式可以很容易看到根因。但 trace 的展示方式并不适合偶现的 CPU 高负载,信息量特别多反而让人难以抓住重点。

网络耗电可以很方便抓取到上行下行的网络请求,可以展示网络请求的 api 细节,并且划分到线程上。对于频繁的网络访问,很容易找到问题点。但目前只支持通过 HttpURLConnection 和 OkHttp 的网络请求,使用其他的网络库,Profiler 追踪不到。

可以看到官方出品的工具,功能比较完善,但只支持 debug 版本的 app 分析,如果要分析 release 版本的 app,需要使用 root 手机。总体而言,Profiler 比较适合于线下固定某个业务场景的分析。https://developer.android.com/studio/profile/energy-profiler

线程池监控

使用上面的工具监控单个线程的 CPU 异常是可以的。但是对于线程池,Handler,AsyncTask 等异步任务不太容易归因具体的业务,尤其是网络库的线程池,由于执行的网络请求逻辑是一样的,只靠抓线程堆栈是不能归因到具体业务的。需要统计提交任务的源头代码才能抓到真正问题点。

我们可以通过多种机制,如改造线程池,java hook 等,对提交任务方进行了详细记录和聚合,可以帮忙我们分析线程池里的耗时任务。

线上 CPU 异常精准监控

除了线下的 CPU 分析,我们在进行线上 CPU 异常监控的建设时,我们考虑到单纯使用 CPU 使用率阈值不能精准的判断进程是否处于 CPU 异常。比如不同的 CPU 型号本身的性能不同,在某些低端 CPU 上的使用率就是比较高。又比如系统有不同的温控策略,省电策略,会对手机进行限频,对任务进行 CPU 核心迁移。在这种情况下,应用也会有更高的 CPU 使用率。

因此我们基于不同的变量因素(如 CPU 型号,进程/线程的 CPU 时长在不同核,不同频点的分布,充电,电量,内存,网络状态等),将 CPU 的使用阈值进行精细判定,针对不同场景、不同设备、不同业务制定精细化的 CPU 异常阈值,从而实现了高精度的 CPU 异常抓取。

此外还有业界的一些归因框架,在这里不展开介绍了。

  • Facebook BatteryMetrics:从 CPU/IO/Location 等多种归因点采集数据,和系统 BatteryStatsService 的统计行为类似,偏重于线下做 App 的耗电评估和器件分解。

  • WeChat BatteryCanary:提供了线程和线程池归因能力,相对于其他工具,增加前后台,亮灭屏,充放电,前台服务统计的统计。

功耗优化实践

上面介绍了功耗的组成,以及如何分析我们应用的耗电。这里我们对功耗优化做一个整体性介绍。我们把优化思路从器件角度展开,列举我们有哪些优化的思路和措施,可以减少器件的使用情况,进而降低功耗。此外对于一些用户可感知的有损业务的降级,我们通过低功耗模式来做,在低电量时通过更激进的降级手段,缓解用户的电量焦虑,带来用户的使用时长的提升。

下图列举了各器件上的优化思路,有一些优化思路会对多个器件都有收益,在这里没有特别详细的区分,就划分在主要影响的器件上,如减少刷新区域,对 GPU,CPU,DDR 都有收益,主要收益在 GPU 绘制上,在下图里就列举在 GPU 上了。

同时我们列举了厂商侧的一些优化方案,应用通常无需关注,比如降低屏幕刷新率,TP 扫描频率,整机低分辨率等,这种可以通过厂商合作的方式进行更细致的调优,如分场景动态调整屏幕刷新率,在搜索列表场景使用 90HZ 高刷,在短视频场景结合帧率对齐进行刷新率降低为 30HZ,以获得更平衡的功耗和性能体验。

1f476942cd6a859054b7cc944a4ad779.png

DISPLAY

显示功耗的优化主要围绕对屏幕,GPU,CPU,视频解码器,TP 等器件降级使用或者减少处理,尽量使用硬件处理等实现的。对于屏幕而言主要是降低亮度,刷新率,TP 扫描频率等。

屏幕亮度

屏幕亮度是屏幕功耗的最大来源,亮度和功耗几乎是正比的关系,参见下图:

4d4792fb9e17466daaee373eb5b4938c.png

可以看出无论是 IPS 屏幕还是 OLED 屏幕,随着屏幕亮度增加,功耗几乎是线性增加。针对 OLED 屏幕则是白色内容的功耗更高,深色内容则功耗相对更低。应用通用的降低亮度的方式有进入应用后主动降低亮度,或者使用深色的 UI 模式,来达到屏幕亮度降低的效果。厂商会通过 FOSS 或者 CABC 的方案,降低屏幕亮度。

深色模式

利用 AMOLED 屏幕本身的原理,黑色功耗最低,所以可以尽量采用较暗的主题颜色等,最终获取较低的功耗,可以保持用户使用时间更长。

为什么说 AMOLED 屏幕显示黑色界面会消耗更少的电量呢?这要从它与传统的 LCD 屏幕之间的发光原理区别上来说。

LCD 背光显示屏,主要是靠背光层,发光层由大量 LED 灯泡组成,显示白光,通过液晶层偏振控制,显示出 RGB 颜色。在这种情况下,黑色与其它颜色的像素并没有什么不同,虽然看起来并没有光亮,但是依然还是处于发光的状态。

AMOLED 屏幕根本就没有背光一说。相反,每个小的亚像素只是发出微弱的 RGB 光,如果屏幕需要显示黑色,只需要通过调整电压使得液晶分子排列旋转从而遮蔽住背光就可以实现黑色的效果,不会额外点亮任何颜色。

b3011c5e298566b77efc43f7556ce872.png

下面引用测试应用为 Reddit Sync 的不同场景下彩色和黑色模式功耗对比。(参考链接:https://m.zol.com.cn/article/4895723.html#p4

1a00637ae0aec9b084dfdd327cce5028.png

从上面的图表我们可以很清楚的看到,在黑色背景的情况下,AMOLED 屏幕在能耗上的确要比普通颜色背景少了很多,在 Reddit Sync 的测试中,平均耗电量要降低 40%左右。

应用可以设计自己的深色模式主题,同步手机系统深色模式开关的切换。目前抖音背景设置有两种模式如下图,可以看到经典模式就是深色模式,正好对应于深色主题,这个也可以和手机平台的深色模式也结合起来。

bfdfb7a3ebc128fcf1349ecbd609594f.png

FOSS

FOSS (Fidelity Optimized Signal Scaling,保真优化信号缩放)是芯片厂商提供的一种对 AMOLED 屏幕调节的低功耗方案。LCD 屏幕上对应的是 CABC (Content Adaptive Brightness Control,内容适应背光控制)。一方面降低屏幕亮度,一方面调节显示内容灰度值,从而使显示效果差异不大,由于降低了屏幕亮度,所以获取的功耗收益较大。一般大约是 0.2 小时左右,即平均可延长手机使用时间 0.2 小时左右。

已知的情况是厂商的 FOSS 方案在某些参数情况下会导致个别场景出现变色或闪烁问题。如果遇到未确认闪烁问题,在内部定位无法确认原因时,可以跟厂商咨询进行排除。

降低刷新率

目前市面上部分手机支持 60HZ,90HZ,120HZ,144HZ 等,高的刷新率带来了流畅度提高,用户的体验更好,但是功耗更高。通常来讲在系统应用界面比如桌面,设置,刷新率会跟当前系统设置保持一致,而在具体应用中,刷新率会根据不同场景做调整。比如抖音,即使在高刷屏幕上,平台系统一般选择让抖音运行在 60HZ 刷新率,从而相对功耗较低。

针对不同的刷新率,PhoneArena 就做了一个比较有参考性的数据来验证这个观点。他们选取了两个品牌四款产品,都是高刷新率的机型,在同一条件下进行 60Hz 刷新率和 120Hz 刷新率的测试,结果 120HZ 刷新率下手机续航相比 60HZ 下的确缩短了至少 10%,即便是支持 90Hz 的一加 8 也是比 60HZ 刷新率要差。

8536973b5bd8a8a6ae4a8a49df6802f7.png图片来源:https://www.sohu.com/a/394532665_115511

降低 TP 扫描频率

通常游戏中为了提高点击响应速度会提高 TP 扫描频率,其他场景都采用默认的扫描频率。抖音一般使用默认的 TP 扫描帧率。

0d9ed17e32161f4360cf401ea8310ab8.png

GPU

GPU 的优化思路主要在减少不必要的绘制或者降低绘制面积,这体现在更低的分辨率,更低的帧率,更少的绘制图层等方面。此外视频应用使用 SurfaceView 替换 TextureView 也有显著的功耗收益。对于复杂的运算,我们可以选择更高能效比的器件来进行,比如使用硬件绘制代替软件绘制,使用 NPU 代替 GPU 执行复杂算法,对整体功耗都有明显降低。

降低分辨率

应用低分辨率

通常该模式下游戏和特定应用一般以较低分辨率运行。缩小了 GPU 绘制区域和传输区域大小,降低了 GPU 和 CPU 以及传输 DDR 的功耗。功耗收益在游戏场景下比较大,线下测试特定平台下1080p->720p约20mA左右,1440p->720p约40mA左右。

其原理如下,应用图层在低分辨率下绘制,通过 HWC 通道放大到屏幕分辨率并跟其余图层合成后送显。

4c60c3000f5a9b6f89c0db048ca87fc4.png

该功能通常平台侧设置,非游戏应用无需关注,游戏应用可以自己选择设置低分辨率。

部分游戏比如腾讯系游戏(如 QQ 飞车、王者荣耀和和平精英等)内部也有不同分辨率的设置,默认以低分辨率运行,从而可以实现较低功耗。

整机低分辨率

所有应用都运行在低分辨率下。同样也缩小了 GPU 绘制区域和传输区域大小,降低了 GPU 和 CPU 以及传输 DDR 的功耗。功耗收益跟应用低分辨率相同,普通应用在该模式下也有功耗收益。用户从系统设置菜单中切换,应用本身通常无需关注。

其原理如下,所有图层都在低分辨率下绘制,并在低分辨率下进行合成。合成后经过 scaler 一次性放大到屏幕分辨率,然后进行送显。其中 scaler 是放缩硬件,由芯片平台提供。

63cd89e595bb18ebcdd450c3790d67f3.png

减少刷新区域

应用布局动画位置相近,布局出来一个较小的区域,绘制区域最小,刷新区域最小, 从而功耗最低。不同场景,收益不同。

如下图两种情况,可以看到左侧图,有 3 个动画区域(红色框住区域),最终形成的 Dirty 区域为大的红框区域,整个面积较大。而对比中间图,动画两个红色区域,经过运算后形成的 Dirty 大红框区域就较小,GPU 的绘制区域跟刷新的传输区域都较小,从而相对而言,功耗较低。从最右侧功耗数据图中可以看出收益较大。

可以在开发者选项中打开:设置 -> 开发者选项 -> 显示GPU视图更新,当刷新范围与动画范围明显不一致时便是动画布局不合理。这种情况需要具体到代码层面分析写法的问题并修改。

d06086f08a854c23e26f5915c80acca6.png

降低绘制频率

通常在游戏或应用动画中使用,可以降低 GPU 绘制频率和后面的刷新频率。通过降低动画绘制频率,可以降低 GPU,CPU 及 DDR 功耗。

不同帧率功耗情况对比如下,可以看到低帧率下相比高帧率,功耗明显低了很多。

1742f76650bf332f5f075004c989094e.png

在抖音应用中,低绘制帧率可以通过在抖音内部主动降低动画等帧率实现。在抖音推荐界面音乐转盘动画和音符动画中降低帧率,可以显著的降低功耗。此外也可以通过厂商侧提供 soft vsync 实现 30HZ 绘制,这部分抖音与厂商合作,SurfaceFlinger 控制 APP vsync,降帧时 SurfaceFlinger vsync 输出降为 30fps,在特定条件下主动降低帧率,以延长使用时长。

帧率对齐

在抖音推荐页面中,通过视频和降低频率后的动画达到同步,可以实现整个界面以30HZ 绘制和刷新。否则,如果视频30hz和动画30帧正好交错,最终形成的绘制/刷新频率还是60帧,没有达到最优。我们通过调节各种动画的绘制流程,将动画整体绘制对齐,整体帧率明显降低。

减少过度绘制

过度绘制(Overdraw)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构里面,如果不可见的 UI 也在做绘制的操作,会导致某些像素区域被绘制了多次,同时也会浪费大量的 CPU 以及 GPU 资源。

可以通过如下来调试过度绘制:打开手机,设置 -> 开发者选项 -> 调试 GPU 过度绘制 -> 显示 GPU 过度绘制。过度绘制的存在会导致界面显示时浪费不必要的资源去渲染看不见的背景,或者对某些像素区域多次绘制,就会导致界面加载或者滑动时的不流畅、掉帧,对于用户体验来说就是 App 特别的卡顿。为了提升用户体验,提升应用的流畅性,优化过度绘制的工作还是很有必要做的。

抖音的 feed 页的过度绘制非常的严重,抖音存在 5 层过度绘制。下图左侧是优化前的过渡绘制情况,右侧是优化后的过度绘制情况,可以看出优化后明显改善。

8454fbcbf4b0665e09920339bfa37691.png

使用 SurfaceView 视频播放

TextureView 和 SurfaceView 是两个最常用的播放视频控件。TextureView 控件位于主图层上,解码器将视频帧传递到 TextureView 对象还需要 GPU 做一次绘制才能在屏幕上显示,所以其功耗更高,消耗内存更大,CPU 占用率也更高。

控件位置差异如下,可以看出 SurfaceView 拥有独立的 Surface 位于单独的图层上,而 TextureView 位于主图层上。

a5fa7b5186daac73bbbc885b4a30dfed.png

BufferQueue 是 Android 图形架构的核心,其一侧是生产者,另一侧是消费者。从这方面看,SurfaceView 和 TextureView 的差异如下。容易看出,SurfaceView 流程更短,内存使用更少,也没有 GPU 绘制,功耗更省。

e67e3801ae58fd1e3213375878ab37bf.png

下面是一些 SurfaceView 替换 TextureView 后的收益数据:

  • CPU 数据上看,SurfaceView 要比 TextureView 优化 8%-13%

  • 功耗数据上看,SurfaceView 要比 TextureView 平均功耗低 20mA 左右。

硬件绘制和软件绘制

硬件绘制是指通过 GPU 绘制,Android 从 3.0 开始支持硬件加速绘制,它在 UI 显示和绘制效率方面远高于软件绘制,但是 GPU 功耗相对较高。目前是系统默认的绘制方式。

软件绘制是指通过 CPU 实现绘制,Android 上面使用 Skia 图形库来进行绘制。两者差异参见下图。

c183f4026386b77217d6730df6569dbd.png

目前默认是开硬件加速的,可以通过设置 Activity,Application,窗口,View 等方式来指定软件绘制。如果应用需要单独指定某些场景的软件绘制方式,需要对性能、功耗等做好评估。参考链接:https://developer.android.com/guide/topics/graphics/hardware-accel

复杂算法用 NPU 代替 GPU

现在的较新的 SoC 平台都带有专门进行 AI 运算的 NPU 芯片,使用 NPU 代替 GPU 运行一些复杂算法,可以有效的节省 GPU 功耗。如视频的超分算法,可以给用户带来很好的体验。但是超分开启对 GPU 的耗电影响很大,在某些平台测试整机功耗可以高出 100mA,选择用 NPU 替换 GPU 是一种优化方式。

CPU

CPU 的优化是功耗优化里最常见的,我们遇到的大部分的 CPU 异常都是出现了死循环。这里使用上面介绍过的功耗归因工具,都可以很容易的发现死循环问题。此外高频的耗时函数,效果和死循环类似,很容易让 CPU 大核跑到高频点,带来 CPU 功耗增加。另外一个典型的 CPU 问题,就是动画泄漏,泄漏动画大概能带来 20mA 的功耗增加。

由于 CPU 工作耗电很高,手机平台大多会增加各种低功耗的 DSP 来分担 CPU 的工作,减少耗电,如常见视频解码,使用硬解会有更好的功耗表现。

CPU 高负载优化

死循环治理

死循环是我们遇到的最明显的 CPU 异常,通常表现为某一个线程占满了一个大核。线程使用率达到了 100%,手机会很容易发热,卡顿。

这里举一个实际修复的死循环例子,在一段循环打包日志的代码逻辑里,所有 log打包完了,才会break跳出循环。当db query出现了异常,异常处理分支并没有做break,导致出现了死循环。

// 方法逻辑有裁剪,仅贴出主要逻辑
private JSONArray packMiscLog() {
  do {
      ......
      try {
          cursor = mDb.query(......);
          int n = cursor.getCount();
          ......
          if (start_id >= max_id) {
              break;
          }
      } catch (Exception e) {
      } finally {
          safeCloseCursor(cursor);
      }
  } while (true);
  return ret;
}

对于死循环治理,我们通过实际解决的问题,总结了几种常见的死循环套路。

// 边界条件未满足,无法break
while (true) {
  ...
  if (shouldExit()) {
      break
  }
}

// 异常处理不妥当,导致死循环
while (true) {
  try {
      do someting;
      break;
  } catch (e) {
  }
}

// 消息处理不当,导致Handler线程死循环
void handleMessage(Message msg) {
  //do something
  handler.sendEmptyMessage(MSG)
}

高频耗时函数治理

除了死循环问题,我们遇到的另外一种常见的就是高频的耗时函数。通过线上监控 CPU 异常,我们也找到很多可优化的点。如 md5 压缩算法的耗时,正则表达式的不合理使用,使用 cmd 执行系统命令的耗时等。这种就 case by case 的修复,就有很不错的收益。

后台资源规范使用

Alarm,Wakelock,JobScheduler 的规范使用

最常见的后台 CPU 耗电就是对后台资源的不合理使用。Alarm 的频繁唤醒,wakelock 的长时间不释放,JobScheduler 的频繁执行,都会使 CPU 保持唤醒状态,造成后台耗电。这种行为很容易让系统判断应用为后台异常耗电,通常会被系统清理,或者发出高耗电提醒。

我们可以通过 dumpsys alarm & dumpsys power & dumpsys jobscheduler 查看相关的统计信息,也可以通过 BH 的后台统计来分析自身的使用情况。

参考绿盟的功耗标准,灭屏 Alarm 触发小于过 12 次/h,即 5min 一次,5min 一次在数据业务下可以保证长链接存活,厂商的后台功耗优化也通常会强制对齐 Alarm 为 5min 触发一次。

后台的 Partial Wakelock 通常会被重点限制,非可感知的场景(音乐,导航,运动)等会被厂商强制释放 wakelock。按照绿盟的标准,灭屏下每小时累计持锁小于 5min,从实际经验上看,持 Partial 锁超过 1min 就会被标为 Long 的 wakelock,如果是应用在后台无可感知业务并且频繁持锁,导致系统无法休眠的,系统会触发 forcestop 清理。

d82f1693c177753e4bd577aa2d90a861.png

某些定时任务可以使用 JobScheduler 来替代 Alarm,Job 的好处是可以组合多种触发条件,选择一个最恰当的时刻让系统调度自己的后台任务。这里建议使用充电+网络可用状态下处理自己的后台任务,对功耗体验是最好的。如果是非充电场景下,设置条件频繁触发 job,同样会带来耗电问题。值得一提的是 Job 执行完要及时结束。因为 JobScheduler 在执行时会持有一个job/开头的 wakelock,最长执行时间 10min,如果一直在执行状态不结束,就会导致系统无法休眠。

视频硬解替换软解

硬解通常是用手机平台自带的硬件解码器来做解码从而实现视频播放,基于专用芯片的硬解码速度快、功耗低;软解码方面,通常使用 FFMPEG 内置的 H.264 和 H.265 的软件解码库来做解码。

下表是三星手机和苹果手机分别在软硬解情况下的功耗,可以看出硬解功耗比软解功耗显著降低,目前抖音默认使用硬解。

3bcc9402151f084b9be2a60de357ad3f.png图片来源:http://www.noobyard.com/article/p-eedllxrr-qz.html

NETWORK

网络耗电是应用耗电的一个重要部分,一个数据包的收发,会同步拉动 CPU 和 Modem/WIFI 两大系统。由于 LTE 的 CDRX 特性(即没有数据包接收,维持一定时间的激活态,再进入睡眠,依赖运营商配置,通常为 10s),所以批量进行网络访问,减少频繁的网络唤醒对网络功耗很有帮忙。此外优化压缩算法,减少数据传输量也从基础上减少了网络耗电。

此外弱信号条件下的网络请求会提高天线的功率,也会触发频繁的搜网,带来更高的网络功耗。根据网络质量进行网络请求调度,提前预缓存网络资源,可以减少网络耗电。

长链接心跳优化

对于应用的后台 PUSH 来说,使用厂商稳定的 push 链路替代自己的长链接可以减少功耗。如果不能替换,也可以优化长链接保活的心跳,根据不同的网络条件动态的调整心跳。根据经验,数据业务下通常是 5min,WIFI 网络下通常可以达到 20min 或更久。

抖音对于长链接进行了的心跳优化,进入后台的长链接心跳时间间隔 [4min, 28min],初始心跳 4min。采用动态心跳试探策略,每次步进 2min,确定最大心跳间隔。

Doze 模式适配

由于系统对后台应用有多种网络限制策略,最常见的是 Doze 模式,手机灭屏一段时间后会进入 doze,限制非白名单应用访问网络,并在窗口期解除限制,窗口期为每 10min 放开 30s。所以在后台进行网络访问前要特别注意进行网络可用的判断,选择窗口期进行网络访问,避免因为被限网而浪费了 CPU 资源。

这里举一个 Doze 未适配的后台耗电例子,用户反馈抖音自上次手机充满电(24h)后,没有在前台使用过,耗电占比 31%,分析日志发现 I 在 Doze 限制网络期间,会触发轮询判断网络是否及时恢复,此逻辑在后台未适配 Doze 的窗口期模式,导致了后台频繁尝试网络请求带来的 CPU 耗电。

c43587c8065f16cfa67ed4eb3219e3f1.png

AUDIO

降低音量

音频的耗电最终体现在 Codec 和 SmartPA(连接喇叭的功率放大器)两部分。减少 Audio 耗电最明显的就是减少音频的音量,这直接反应到喇叭的响度上。

用 0-15 级的音量进行测试,可以看到音量对功耗的影响巨大,尤其是超过 10 之后,整体增幅非常巨大。每一级几乎与功耗成百分比上涨。

acce588e8f9b0a18aed0ca2545ac2608.png

  • 10-15 :1:30ma

  • 5-10:1:1.62ma

  • 0-5:1:1.36ma

调整音频参数

由于用户对音量的感受很明显,直接全局降低音量会带来不好的体验。厂商通常会针对不同的场景,设计不同的音频参数,如电影场景,游戏场景,导航场景,动态调节音频的高低频配置参数,兼顾了效果和功耗。

从这个角度出发,可以选择和厂商合作,根据播放视频的内容,精细化调整音频参数,如电影剪辑类型视频就使用电影场景的参数,游戏视频就切换为游戏场景的配置参数,从而达到用户无感调节音量节省功耗的目的。

CAMERA

Camera 是功耗大户,尤其是高分辨率高帧率的录制会带来快速的功耗消耗和温升。经过线下测算,开播场景,Camera 功耗 200mA+,占整机的 25%以上。

优化Camera功耗的思路主要是从业务降级的角度上进行,如降低录制的分辨率,降低录制帧率等。之前抖音直播和生产端都是使用30帧,但最终只使用15帧,在开播端主动下调采集帧率,按需设置帧率为15帧,功耗显著降低了120ma。

SENSOR

sensor 的典型功耗值很低,如我们常用到的 accelerometer(加速度计)的典型功耗只有 180uA。但 sensor 的开启会导致 cpu 的唤醒与负载增加,尤其是在应用退到后台,sensor 的滥用会显著增加待机功耗。可以在低电量时关闭不必要的 sensor,减少耗电。

GPS

精确度,频率,间隔是影响 GPS 耗电的三个主要因素。其中精度影响定位的工作模式,频率和间隔是影响工作时长,我们可以通过优化这三者来减少 GPS 的耗电

降低精度

Android 原生定位提供 GPS 定位和网络定位两种模式。GPS 定位支持离线定位,依靠卫星,没有网络也能定位,精度高,但功耗大,因需要开启移动设备中的 GPS 定位模块,会消耗较多电量。

Network 定位(网络定位),定位速度快,只要具备网络或者基站要求,在任何地方都可实现瞬间定位,室内同样满足;功耗小,耗电量小;但定位精度差,容易受干扰,在基站或者 WiFi 数量少、信号弱的地方定位质量较差,或者无法定位;必须连接网络才能实现定位。

我们可以在满足定位要求的情况下,主动使用低精度的网络定位,减少定位耗电,抖音在进入低功耗模式时,进行了 GPS 降级为网络定位,并且扩大了定位间隔。

降低频率&提高间隔

这里除了业务上主动控制频率与间隔外,还推荐使用厂商的定位服务。为了优化定位耗电,海外 gms 以及国内厂商都提供了位置服务 SDK,本质上是通过系统服务统一管理位置请求,根据电量,信号,请求方的延迟精度要求,进行动态调整,达到功耗与定位需求的平衡。提供了诸如被动位置更新,获取最近一次定位的位置信息,批量后台位置请求等低功耗定位能力。

https://developer.android.com/guide/topics/location/battery https://developer.huawei.com/consumer/cn/doc/development/HMSCore-References/location-description-0000001088559417

低功耗模式

上述的优化措施,有些在常规模式下已经实施。但有一部分是有损用户体验的,我们选择在低电量场景下去做,降低功耗,减少用户的电量焦虑,获得用户在低电量下更多使用时长。

在低功耗模式预研中,我们列举了很多可做的措施,通过 AB 实验,我们去掉了业务负向的降级手段,比如亮度降低,音量降低等。此外在功能触发的策略上,我们通过对比了低电量弹窗提醒,设置里增加开关+Toast 提醒,以及低电量自动进入,最终选择了对用户体验最好的 30%电量无打扰自动进入的触发方式。

06827ecf92bc688522fdc01e1cb31cf5.png

经过实验发现,一些高发热机型,通过低功耗模式全程开启,也可以拿到业务收益。说明部分有损的降级,用户在易发热的情况下也是接受的,可以置换出业务收益,目前低功耗模式线下测试功耗收益稳定在 20mA 以上。

总结

功耗优化是一个复杂的综合课题,既包含了利用工具对功耗做拆解评估,对异常的监控治理,也包含了主动挖掘优化点进行优化。上面列举的优化思路,我们也只是做了部分,还有部分待开展,包括功耗归因的工具建设上,我们也还有很多可以优化的点。我们会持续发力,产出更多的方案,在满足使用需求的前提下,消耗更少的物理资源,给抖音用户带来更好的功耗体验。

作者: 字节跳动技术团队
来源:https://blog.csdn.net/ByteDanceTech/article/details/125109383

收起阅读 »

2022年前端四大框架谁值得更大的关注

web
2022 年 Angular、Vue、React 和 Svelte 四大前端框架从数据分析,谁更值得去学习呐?本文基于 Stack Overflow 和 State of JavaScript 调查以及 JavaScript 性能标准对四大框架进行客观的分析比...
继续阅读 »

2022AngularVueReactSvelte 四大前端框架从数据分析,谁更值得去学习呐?

本文基于 Stack OverflowState of JavaScript 调查以及 JavaScript 性能标准对四大框架进行客观的分析比较。

文章从使用率、满意度、性能效率以及薪资来分析不同框架,每个标准占比 10 分。

使用率

Stack Overflow 方数据显示,

  • 40% 的开发者使用过 React

  • 22% 的开发者使用过 Angular

  • 19% 的开发者使用过 Vue

  • Svelte 使用占比仅为 3%

State of JavaScript 调查显示: React 的 JS 开发者占比 80%,Angular 的开发者占比 54%,Vue 使用者占比 51%,Svelte 仅为 20%。

AngularVue 的使用率类似,React 独占鳌头,Svelte 明显落后,最终分数分配: React 5 分,VueAngular 2.5 分,Svelte 0 分。

开发者满意度

Stack Overflow 调查显示,Svelte 的满意度最高,达到 71%。ReactVue 满意度紧随其后,分别为 69% 和 64%。Angular 满意度为 55%,满意与不满意人数几乎相同。

State of JavaScript 调查的结果排名类似,但数值有所不同,Svelte 的满意度为 90%,React 为 84%,Vue 为 80%,Angular 仅为 45%。

当前标准分数分配: Svelte 4 分,ReactVue 各 3 分,Angular 0 分。

性能

使用 JavaScript Framework Benchmark工具来分析各个框架的执行时间、内存占用及启用时间。评测结果将与 ·vanilla JavaScript· 进行比较。输出表格中,每个单元格颜色都是从绿色到红色,越接近红色正证明越偏离基本 JavaScript 。

三个标准每个分配 10 分,取平均值得出总体相对性能得分。

执行速度


执行速度的方面,经过多次测试,Svelte 速度最快,Vue 紧随其后,ReactAngular 速度较慢,分数分配如下: Svelte 5 分,Vue 4 分,React 和 Angualr 各 0.5 分。

内存占用


内存占用方面,Svelte 仍然保持大幅度领先,Vue 略微优于并驾齐驱的 ReactAngular。分数分配如下: Svelte 6 分,Vue 3 分,ReactAngular 各 0.5 分。

启动时间


Svelte 的启动速度也非常出色,Vue 略逊一筹,ReactAngular 紧随其后。这次结果相对均匀,分数分配如下: Svelte 4 分,Vue 3 分,AngularReact 各 1 分。

性能整体表现分数

经过上面三项测试,四大框架在性能方面的得分,最终如下: Svelte 5 分,Vue 3.5 分,ReactAngular 0.5 分。

薪资

薪资评测数据来源于 Stack Overflow 的框架薪资中位数,各框架薪资如下:

  • Angular 49k 美元

  • Vue 50k 美元

  • React 58k 美元

  • Svelte 62k 美元

AngularVue 的薪资接近,ReactSvelte 的薪资遥遥领先,因此分数分配如下: AngularVue 各 1.5 分,React 3 分,Svelte 4 分。

最终成绩

经过上面几轮的评估,四大框架最终分数如下:

  • Angular: 4.5

  • Vue: 10.5

  • React: 12

  • Svelte: 13

得到评估结果后,我们再来客观的分析一下 JavaScript 调查报告。下面的视图结合了用户满意度(从左往右)和使用率(从下到上),同时涵盖了跨时间轨迹。


Svelte 90% 的满意度主要来源于乐于尝试新技术的开拓者。React 仍然占据使用的主导地位,但未能保持高满意度。

如果来分析使用率和满意度的四象限图,你会发现,Angular 使用频度一般,收获满意较少;Vue 满意度高但使用率并不高,而且随着时间的推移,满意度正在降低。React 则得到了广泛的使用和赞赏。近期 React 还推出了服务端的 Next.jsRemix,其越来越成为前端的标准。

如果想了解更多讯息,请参考: javascript.plainenglish.io/angular-vs-…


作者:战场小包
来源:https://juejin.cn/news/7102437237203140644

收起阅读 »

WebGPU 会取代 WebGL 吗?

前言 你知道WebGL并使用过吗?如果没有,那你也一定使用Three.js。在本文,我将向你介绍一下WebGL和其后起之秀 WebGPU。 什么是 WebGL ? WebGL 的起源 说起WebGL的起源,就不得不提起OpenGL。 在个人计算机的早期,使用最...
继续阅读 »

前言


你知道WebGL并使用过吗?如果没有,那你也一定使用Three.js。在本文,我将向你介绍一下WebGL和其后起之秀 WebGPU


什么是 WebGL ?


WebGL 的起源


说起WebGL的起源,就不得不提起OpenGL


在个人计算机的早期,使用最广泛的3D图形渲染技术是Direct3DOpenGLDirect3D是微软DirectX技术的一部分,并主要用于Windows平台。而OpenGL是一种开源的跨平台技术,并赢得了众多开发者的青睐。


然后,就是一个特殊的版本 - OpenGL ES。它专门为嵌入式计算机,智能手机,家用游戏机和其他设备而设计。他从OpenGL中移除了很多旧的和无用的特性,并为其添加了一些新的特性。例如,去除了矩形等多余的多边形,而只保留了点、线、三角形等基本图形。这使它在保持轻量级的同时仍然保留足够强大的能力来渲染漂亮的3D图形。


最后,WebGL就是从OpenGL ES衍生而来的。它专注于web3D图形渲染。


下图显示了它们之间的关系:


image.png


WebGL 的历史


image.png


从上图可以看出,WebGL已经很老了。不仅仅是因为它的存在时间长,还有它的标准是继承自OpenGL的。
OpenGL的设计理念可以追溯到1992年,这些古老的概念已经和如今GPU的工作原理不相符合了。


对于浏览器开发者来说,适配不同GPU的特性,给他们带来了诸多不便。


从上图中我们可以看到苹果在2014年发布了Metal。而Steve JobsOpenGL ES的忠实支持者,他认为这是行业的未来,所以当时Apple设备上的游戏依然依赖于OpenGL ES(例如愤怒的小鸟,水果忍者)。但在Steve Jobs去世后,苹果放弃了OpenGL ES,开发了新的图形框架Metal


微软也在2015年发布了自己的D3D12[Direct3D 12]图形框架。紧随其后的是Khronos Group


image.png


Khronos Group是图形行业的一个国际组织,类似于前端圈的W3CTC39。它的标准是WebGL。甚至他们也逐渐淡化了WebGL,转而支持现在的Vulkan


到此为止,MetalD3D12 [Direct3D 12] 和Vulkan并列为三大现代图形框架。这些框架充分释放了GPU 的可编程能力,让开发者可以最大程度地自由控制GPU


另外,今天的主流操作系统不再将OpenGL作为主要支持。这意味着我们今天编写的每一行WebGL代码90%的不会被OpenGL绘制。在Windows计算机上将使用DirectX绘制,而在Mac计算机上则使用Metal绘制。


从这些可以看出OpenGL已经很老了。但这并不意味着它会消失。它继续会在嵌入式和科学研究等特殊领域发挥作用。


WebGL也是如此,大量的适配工作使其难以向前推进。于是推出了WebGPU


什么是 WebGPU?


WebGPU的目标是提供现代3D图形和计算能力。它是由W3C组织(前端的老朋友)制定的标准。与WebGL不同,WebGPU不是OpenGL的包装。并且恰恰相反,它指的是当前的图形渲染技术,一种新的跨平台高性能图形界面。


它的设计更容易被三大图形框架实现,从而减轻了浏览器开发者的负担。它也是一个精确的图形API,完全开放了整个显卡的能力。而不再是像WebGL这样的上层API


更具体的优点有:



  • 减少了CPU开销

  • 对多线程的良好支持

  • 使用计算着色器将通用计算 (GPGPU) 的强大功能引入Web

  • 全新的着色器语言 - WebGPU Shading Language (WGSL)

  • 未来将支持 实时光线追踪 的技术


image.png


WebGPU 的发展现状


目前,WebGPUAPI仍在开发迭代中,但我们可以在Chrome Canary中试用


image.png


在目前的前端框架中,Three.js 已经开始实现WebGPU的后端渲染器,Babylon.js计划在5.x版本中支持 WebGPU


结论


我认为WebGPU取代WebGL是大势所趋。而且我相信它在元宇宙场景中有很大的潜力。


你如何看待WebGL?你看好WebGPU吗?


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

互联网公司端午礼盒大赏,谁家最诱人?

盼望着,盼望着,端午节就快到了。作为一个为数不多不用“调休”的节日,美美的吃上一个粽子就是对端午节最大的尊重。或许你还在考虑端午节是吃甜粽子还是咸粽子,但各大互联网公司却用实际行动告诉你,成年人不做选择题,我们都要!今天我们就来盘点看看,这些大厂们的端午礼盒都...
继续阅读 »

盼望着,盼望着,端午节就快到了。作为一个为数不多不用“调休”的节日,美美的吃上一个粽子就是对端午节最大的尊重。

或许你还在考虑端午节是吃甜粽子还是咸粽子,但各大互联网公司却用实际行动告诉你,成年人不做选择题,我们都要!今天我们就来盘点看看,这些大厂们的端午礼盒都如何虏获成年人的胃:

斗 鱼

今年端午节,斗鱼携手湖北省博物馆共同推出了名为“如此楚粽”的国潮风联名礼盒,这是斗鱼首次与湖北省博物馆进行合作。

礼盒的外观设计融入了“粽子”和“龙舟”两大端午代表性元素,深绿色的“外皮”与龙头炯炯有神的双目相映成谶,同时又不失几分次元萌,契合年轻国潮人的审美风格,封面是荆楚文化的代表藏品,气质拉满。礼盒内含三种不同口味的粽子、一盒绿豆糕、一颗传统工艺咸鸭蛋及一些小周边,品类丰富。

这好吃、好玩、又不失文化底蕴的礼盒,属实让人不得不爱~

快 手

没有一个冬天不会过去,没有一个春天不会到来。今年快手的端午礼盒以“按时花开”为主题,旨在为鼓励更多人关注残疾人就业、创业而打造,值得一提的是,这也是快手第一次由用户参与创作的礼盒。

礼盒中包含粽子、空调毯、果汁杯帆布袋、罐头种子和毛毡花摆件等多样内容。整体红绿配色鲜艳夺目,呼应「按时花开」的主题,蕴含着希望与未来可期,从视觉和礼盒内涵上都让人感觉眼前一亮。

此外,快手这款外包装的红绿撞色十分的吸引眼球,打开来后,里面的毛毡花可以拿出来摆饰。

携 程

与快手围绕春日展开的主题「按时花开」不同得是,携程集团围绕「不羁夏,端舞游」为主题展开礼盒设计,内包含骨瓷杯套装、百毒不侵锦囊、瑞鹤眼罩和三种口味粽子。民族舞会风情设计俏皮有趣,好看又实用。

据悉,礼盒以“舞会”为创作灵感,探索了高鲁山、狮子山、神仙坡、凤凰古城4个目的地和中华各民族的端午节日故事,并将不同民族的端午传统演绎为四场舞会。携程希望借此礼盒将各个文化中的吉祥元素和美好祝愿带给每一个人。

字节跳动

端午礼盒=种植礼包?

「吃粽子,播种子」这操作算是被字节跳动整明白了,今年端午字节跳动主打“绿色环保+创意涂鸦”,整体外包装是由环保材料甘蔗制成。

或许是受疫情启发,这次礼盒最大的亮点就是提供花盆、生菜、樱桃小萝卜等种子供大家种植,这波就算困在家里也不怕的操作属实是赢麻了。

知 乎

知乎的礼盒依旧围绕着“问题”和“知识”展开,主题叫「粽点知识」。

今年的端午礼盒不是常规的四四方方的纸质盒子,而是一个很实用的蓝色挎包,包里有一台筋膜枪、一些梅子红茶包和粽子。粽子配方用的是白砂糖不是代糖,应该不会像去年月饼那样成为喷射战士~

另外,包里的这件筋膜枪,个人感觉还是挺实用的,可以帮助用户锻炼健身,算是一个小小的惊喜。

顺 丰

顺丰礼盒「粽情丰驰」整体的造型还是很别致的,外包装是环保材料制成,拆开来有一辆顺丰无人车,顺丰无人车的盒子还能做抽纸盒,既体现顺丰速运环保的一环,也从侧面显现出顺丰的科技创新。

蔚来 NIO Life

蔚来今年端午礼盒的主题为“一篮出粽”,之所以叫这个名字许是因为这个礼盒篮是纯手工的编织的。据悉,这个篮子不用一滴胶水、不用化工染料,尽力做到环保与自然,这怎么看都觉得经费在燃烧啊!

礼盒中,咸粽选择用植物肉,植物肉粽相较于传统肉粽,植物肉粽脂肪、热量、胆固醇会更低,降低身体摄入负担,与现代人健康生活理念的追求相一致。

元气森林

元气森林的端午礼盒主题为「 逐光随影」,这看起来有点小清新的名字,却是以中国传统的“钟馗捉鬼”为设计元素,寓意平安吉祥。这种反差还是让人感觉颇为新颖的~

据悉,礼盒内含折纸香囊、御守平安符、走马灯、王星记扇子等具有中华传统习俗的内容物,特别是香囊的设计和寓意也十分有巧思,端午出行挂香囊,有愿所做皆所期,所求皆所愿,所行化坦途祈福平安,幸福相伴之意。

不知道从什么时候起,每一年的端午礼盒成为了各大企业的运营重点。看过了今年各品牌的礼盒设计之后,你们觉得谁家的最诱人呢端午?

来源:m.sohu.com/a/552980788_466940/

收起阅读 »

Compose 动画边学边做 - 夏日彩虹

引言 Compose 在动画方面下足了功夫,提供了种类丰富的 API。但也正由于 API 种类繁多,如果想一气儿学下来,可能会消化不良导致似懂非懂。结合例子学习是一个不错的方法,本文就带大家边学边做,通过高仿微博长按点赞的彩虹动画,学习和实践 Compose ...
继续阅读 »

引言


Compose 在动画方面下足了功夫,提供了种类丰富的 API。但也正由于 API 种类繁多,如果想一气儿学下来,可能会消化不良导致似懂非懂。结合例子学习是一个不错的方法,本文就带大家边学边做,通过高仿微博长按点赞的彩虹动画,学习和实践 Compose 动画的相关技巧。















原版:微博长按点赞本文:掘金夏日主题
ezgif.com-gif-maker (23).gifezgif.com-gif-maker (24).gif


代码地址: github.com/vitaviva/An…



1. Compose 动画 API 概览


Compose 动画 API 在使用场景的维度上大体分为两类:高级别 API 和低级别 API。就像编程语
言分为高级语言和低级语言一样,这列高级低级指 API 的易用性:



  • 高级别 API 主打开箱即用,适用于一些 UI 元素的展现/退出/切换等常见场景,例如常见的 AnimatedVisibility 以及 AnimatedContent 等,它们被设计成 Composable 组件,可以在声明式布局中与其他组件融为一体。


//Text通过动画淡入
var editable by remember { mutableStateOf(true) }
AnimatedVisibility(visible = editable) {
Text(text = "Edit")
}


  • 低级别 API 使用成本更高但是更加灵活,可以更精准地实现 UI 元素个别属性的动画,多个低级别动画还可以组合实现更复杂的动画效果。最常见的低级别 animateFloatAsState 系列了,它们也是 Composable 函数,可以参与 Composition 的组合过程。


//动画改变 Box 透明度
val alpha: Float by animateFloatAsState(if (enabled) 1f else 0.5f)
Box(
Modifier.fillMaxSize()
.graphicsLayer(alpha = alpha)
.background(Color.Red)
)


处于上层的 API 由底层 API 支撑实现,TargetBasedAnimation 是开发者可直接使用的最低级 API。Animatable 也是一个相对低级的 API,它是一个动画值的包装器,在协程中完成状态值的变化,向上提供对 animate*AsState 的支撑。它与其他 API 不同,是一个普通类而非一个 Composable 函数,所以可以在 Composable 之外使用,因此更具灵活性。本例子的动画主要也是依靠它完成的。


// Animtable 包装了一个颜色状态值
val color = remember { Animatable(Color.Gray) }
LaunchedEffect(ok) {
// animateTo 是个挂起函数,驱动状态之变化
color.animateTo(if (ok) Color.Green else Color.Gray)
}
Box(Modifier.fillMaxSize().background(color.value))

无论高级别 API 还是低级别 API ,它们都遵循状态驱动的动画方式,即目标对象通过观察状态变化实现自身的动画。


2. 长按点赞动画分解


长按点赞的动画乍看之下非常复杂,但是稍加分解后,不难发现它也是由一些常见的动画形式组合而成,因此我们可以对其拆解后逐个实现:



  • 彩虹动画:全屏范围内不断扩散的彩虹效果。可以通过半径不断扩大的圆形图案并依次叠加来实现

  • 表情动画:从按压位置不断抛出的表情。可以进一步拆解为三个动画:透明度动画,旋转动画以及抛物线轨迹动画。

  • 烟花动画:抛出的表情在消失时会有一个烟花炸裂的效果。其实就是围绕中心的八个圆点逐渐消失的过程,圆点的颜色提取自表情本身。


传统视图动画可以作用在 View 上,通过动画改变其属性;也可以在 onDraw 中通过不断重绘实现逐帧的动画效果。 Compose 也同样,我们可以在 Composable 中观察动画状态,通过重组实现动画效果(本质是改变 UI 组件的布局属性),也可以在 Canvas 中观察动画状态,只在重绘中实现动画(跳过组合)。这个例子的动画效果也需要通过 Canvas 的不断重绘来实现。
Compose 的 Canvas 也可以像 Composable 一样声明式的调用,基本写法如下:


Canvas {
...
drawRainbow(rainbowState) //绘制彩虹
...
drawEmoji(emojiState) //绘制表情
...
drawFlow(flowState) //绘制烟花
...
}

State 的变化会驱动 Canvas 会自动重绘,无需手动调用 invalidate 之类的方法。那么接下来针对彩虹、表情、烟花等各种动画的实现,我们的工作主要有两个:



  • 状态管理:定义相关 State,并在在动画中驱动其变化,如前所述这主要依靠 Animatable 实现。

  • 内容绘制:通过 Canvas API 基于当前状态绘制图案


3. 彩虹动画


3.1 状态管理


对于彩虹动画,唯一的动画状态就是圆的半径,其值从 0F 过渡到 screensize,圆形面积铺满至整个屏幕。我们使用 Animatable 包装这个状态值,调用 animateTo 方法可以驱动状态变化:


val raduis = Animatable(0f) //初始值 0f

radius.animateTo(
targetValue = screenSize, //目标值
animationSpec = tween(
durationMillis = duration, //动画时长
easing = FastOutSlowInEasing //动画衰减效果
)
)

animationSpec 用来指定动画规格,不同的动画规格决定了了状态值变化的节奏。Compose 中常用的创建动画规格的方法有以下几种,它们创建不同类型的动画规格,但都是 AnimationSpec 的子类:



  • tween:创建补间动画规格,补间动画是一个固定时长动画,比如上面例子中这样设置时长 duration,此外,tween 还能通过 easiing 指定动画衰减效果,后文详细介绍。

  • spring: 弹跳动画:spring 可以创建基于物理特性的弹簧动画,它通过设置阻尼比实现符合物理规律的动画衰减,因此不需要也不能指定动画时长

  • Keyframes:创建关键帧动画规格,关键帧动画可以逐帧设置当前动画的轨迹,后文会详细介绍。


AnimatedRainbow


ezgif.com-gif-maker (20).gif


要实现上面这样多个彩虹叠加的效果,我们还需有多个 Animtable 同时运行,在 Canvas 中依次对它们进行绘制。绘制彩虹除了依靠 Animtable 的状态值,还有 Color 等其他信息,因此我们定义一个 AnimatedRainbow 类保存包括 Animtable 在内的绘制所需的的状态


class AnimatedRainbow(
//屏幕尺寸(宽边长边大的一方)
private val screenSize: Float,
//RainbowColors是彩虹的候选颜色
private val color: Brush = RainbowColors.random(),
//动画时长
private val duration: Int = 3000
) {
private val radius = Animatable(0f)

suspend fun startAnim() = radius.animateTo(
targetValue = screenSize * 1.6f, // 关于 1.6f 后文说明
animationSpec = tween(
durationMillis = duration,
easing = FastOutSlowInEasing
)
)
}

animatedRainbows 列表


我们还需要一个集合来管理运行中的 AnimatedRainbow。这里我们使用 Compose 的 MutableStateList 作为集合容器,MutableStateList 中的元素发生增减时,可以被观察到,而当我们观察到新的 AnimatedRainbow 被添加时,为它启动动画。关键代码如下:


//MutableStateList 保存 AnimatedRainbow
val animatedRainbows = mutableStateListOf<AnimatedRainbow>()

//长按屏幕时,向列表加入 AnimtaedRainbow, 意味着增加一个新的彩虹
animatedRainbows.add(
AnimatedRainbow(
screenHeightPx.coerceAtLeast(screenWidthPx),
RainbowColors.random()
)
)

我们使用 LaunchedEffect + snapshotFlow 观察 animatedRainbows 的变化,代码如下:


LaunchedEffect(Unit) {
//监听到新添加的 AnimatedRainbow
snapshotFlow { animatedRainbows.lastOrNull() }
.filterNotNull()
.collect {
launch {
//启动 AnimatedRainbow 动画
val result = it.startAnim()
//动画结束后,从列表移除,避免泄露
if (result.endReason == AnimationEndReason.Finished) {
animatedRainbows.remove(it)
}
}
}
}

LaunchedEffectsnapshotFlow 都是 Compose 处理副作用的 API,由于不是本文重点就不做深入介绍了,这里只需要知道 LaunchedEffect 是一个提供了执行副作用的协程环境,而 snapshotFlow 可以将 animatedRainbows 中的变化转化为 Flow 发射给下游。当通过 Flow 收集到新加入的 AnimtaedRainbow 时,调用 startAnim 启动动画,这里充分发挥了挂起函数的优势,同步等待动画执行完毕,从 animatedRainbows 中移除 AnimtaedRainbow 即可。


值得一提的是,MutableStateList 的主要目的是在组合中观察列表的状态变化,本例子的动画不发生在组合中(只发生在重绘中),完全可以使用普通的集合类型替代,这里使用 MutableStateList 有两个好处:



  • 可以响应式地观察列表变化

  • 在 LaunchEffect 中响应变化并启动动画,协程可以随当前 Composable 的生命周期结束而终止,避免泄露。


3.2 内容绘制


我们在 Canvas 中遍历 animatedRainbows 所有的 AnimtaedRainbow 完成彩虹的绘制。彩虹的图形主要依靠 DrawScopedrawCircle 完成,比较简单。一点需要特别注意,彩虹动画结束时也要以一个圆形图案逐渐退出直至漏出底部内容,要实现这个效果,用到一个小技巧,我们的圆形绘制使用空心圆 (Stroke ) 而非 实心圆( Fill )


image.png



  • 出现彩虹:圆环逐渐铺满屏幕却不能漏出空心。这要求 StrokeWidth 宽度覆盖 ScreenSize,且始终保持 CircleRadius 的两倍

  • 结束彩虹:圆环空心部分逐渐覆盖屏幕。此时要求 CircleRadius 减去 StrokeWidth / 2 之后依然能覆盖 ScreenSize


基于以上原则,我们为 AnimatedRainbow 添加单个 AnnimatedRainbow 的绘制方法:


fun DrawScope.draw() {
drawCircle(
brush = color, //圆环颜色
center = center, //圆心:点赞位置
radius = radius.value,// Animtable 中变化的 radius 值,
style = Stroke((radius.value * 2).coerceAtMost(_screenSize)),
)
}

如上,StrokeWidth 覆盖 ScreenSize 之后无需继续增长,而 CircleRadius 的最终尺寸除去 ScreenSize 之外还要将 StrokeWidth 考虑进去,因此前面代码中将 Animtable 的 targetValue 设置为 ScreenSize 的 1.6 倍。


4. 表情动画


4.1 状态管理


表情动画又由三个子动画组成:旋转动画、透明度动画以及抛物线轨迹动画。像 AnimtaedRainbow 一样,我们定义 AnimatedEmoji 管理每个表情动画的状态,AnimatedEmoji 中通过多个 Animatable 分别管理前面提到的几个子动画


ezgif.com-gif-maker (21).gif


AnimatedEmoji


class AnimatedEmoji(
private val start: Offset, //表情抛点位置,即长按的屏幕位置
private val screenWidth: Float, //屏幕宽度
private val screenHeight: Float, //屏幕高度
private val duration: Int = 1500 //动画时长
) {

//抛出距离(x方向移动终点),在左右一个屏幕之间取随机数
private val throwDistance by lazy {
((start.x - screenWidth).toInt()..(start.x + screenWidth).toInt()).random()
}
//抛出高度(y方向移动终点),在屏幕顶端到抛点之间取随机数
private val throwHeight by lazy {
(0..start.y.toInt()).random()
}

private val x = Animatable(start.x)//x方向移动动画值
private val y = Animatable(start.y)//y方向移动动画值
private val rotate = Animatable(0f)//旋转动画值
private val alpha = Animatable(1f)//透明度动画值

suspend fun CoroutineScope.startAnim() {
async {
//执行旋转动画
rotate.animateTo(
360f, infiniteRepeatable(
animation = tween(_duration / 2, easing = LinearEasing),
repeatMode = RepeatMode.Restart
)
)
}
awaitAll(
async {
//执行x方向移动动画
x.animateTo(
throwDistance.toFloat(),
animationSpec = tween(durationMillis = duration, easing = LinearEasing)
)
},
async {
//执行y方向移动动画(上升)
y.animateTo(
throwHeight.toFloat(),
animationSpec = tween(
duration / 2,
easing = LinearOutSlowInEasing
)
)
//执行y方向移动动画(下降)
y.animateTo(
screenHeight,
animationSpec = tween(
duration / 2,
easing = FastOutLinearInEasing
)
)
},
async {
//执行透明度动画,最终状态是半透明
alpha.animateTo(
0.5f,
tween(duration, easing = CubicBezierEasing(1f, 0f, 1f, 0.8f))
)
}
)
}

infiniteRepeatable


上面代码中,旋转动画的 AnimationSpec 使用 infiniteRepeatable 创建了一个无限循环的动画,RepeatMode.Restart 表示它的从 0F 过渡到 360F 之后,再次重复这个过程。
除了旋转动画之外,其他动画都会在 duration 之后结束,它们分别在 async 中启动并行执行,awaitAll 等待它们全部结束。而由于旋转动画不会结束,因此不能放到 awaitAll 中,否则 startAnim 的调用方将永远无法恢复执行。


CubicBezierEasing


透明度动画中的 easing 指定了一个 CubicBezierEasing。easing 是动画衰减效果,即动画状态以何种速率逼近目标值。Compose 提供了几个默认的 Easing 类型可供使用,分别是:


//默认的 Easing 类型,以加速度起步,减速度收尾
val FastOutSlowInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 0.2f, 1.0f)
//匀速起步,减速度收尾
val LinearOutSlowInEasing: Easing = CubicBezierEasing(0.0f, 0.0f, 0.2f, 1.0f)
//加速度起步,匀速收尾
val FastOutLinearInEasing: Easing = CubicBezierEasing(0.4f, 0.0f, 1.0f, 1.0f)
//匀速接近目标值
val LinearEasing: Easing = Easing { fraction -> fraction }


上图横轴是时间,纵轴是逼近目标值的进度,可以看到除了 LinearEasing 之外,其它的的曲线变化都满足 CubicBezierEasing 三阶贝塞尔曲线,如果默认 Easing 不符合你的使用要求,可以使用 CubicBezierEasing,通过参数,自定义合适的曲线效果。比如例子中曲线如下:


image.png


这个曲线前半程状态值进度非常缓慢,临近时间结束才快速逼近最终状态。因为我们希望表情动画全程清晰可见,透明度的衰减尽量后置,默认 easiing 无法提供这种效果,因此我们自定义 CubicBezierEasing


抛物线动画


再来看一下抛物线动画的实现。通常我们可以借助抛物线公式,基于一些动画状态变量计算抛物线坐标来实现动画,但这个例子中我们借助 Easing 更加巧妙的实现了抛物线动画。
我们将抛物线动画拆解为 x 轴和 y 轴两个方向两个并行执行的位移动画,x 轴位移通过 LinearEasing 匀速完成,y 轴又拆分成两个过程



  • 上升到最高点,使用 LinearOutSlowInEasing 上升时速度加速衰减

  • 下落到屏幕底端,使用 FastOutLinearInEasing 下落时速度加速增加


上升和下降的 Easing 曲线互相对称,符合抛物线规律


animatedEmojis 列表


像彩虹动画一样,我们同样使用一个 MutableStateList 集合管理 AnimatedEmoji 对象,并在 LaunchedEffect 中监听新元素的插入,并执行动画。只是表情动画每次会批量增加多个


//MutableStateList 保存 animatedEmojis
val animatedEmojis = mutableStateListOf<AnimatedEmoji>()

//一次增加 EmojiCnt 个表情
animatedEmojis.addAll(buildList {
repeat(EmojiCnt) {
add(AnimatedEmoji(offset, screenWidthPx, screenHeightPx, res))
}
})

//监听 animatedEmojis 变化
LaunchedEffect(Unit) {
//监听到新加入的 EmojiCnt 个表情
snapshotFlow { animatedEmojis.takeLast(EmojiCnt) }
.flatMapMerge { it.asFlow() }
.collect {
launch {
with(it) {
startAnim()//启动表情动画,等待除了旋转动画外的所有动画结束
animatedEmojis.remove(it) //从列表移除
}
}
}
}

4.2 内容绘制


单个 AnimatedEmoji 绘制代码很简单,借助 DrawScopedrawImage 绘制表情素材即可


//当前 x,y 位移的位置
val offset get() = Offset(x.value, y.value)

//图片topLeft相对于offset的距离
val d by lazy { Offset(img.width / 2f, img.height / 2f) }


//绘制表情
fun DrawScope.draw() {
rotate(rotate.value, pivot = offset) {
drawImage(
image = img, //表情素材
topLeft = offset - dCenter,//当前位置
alpha = alpha.value, //透明度
)
}
}

注意旋转动画实际上是借助 DrawScoperotate 方法实现的,在 block 内部调用 drawImage 指定当前的 alphatopLeft 即可。


5. 烟花动画


5.1 状态管理


烟花动画紧跟在表情动画结束时发生,动画不涉及位置变化,主要是几个花瓣不断缩小的过程。花瓣用圆形绘制,动画状态值就是圆形半径,使用 Animatable 包装。


ezgif.com-gif-maker (22).gif


AnimatedFlower


烟花的绘制还要用到颜色等信息,我们定义 AnimatedFlower 保存包括 Animtable 在内的相关状态。


class AnimatedFlower(
private val intial: Float, //花瓣半径初始值,一般是表情的尺寸
private val duration: Int = 2500
) {
//花瓣半径
private val radius = Animatable(intial)

suspend fun startAnim() {
radius.animateTo(0f, keyframes {
durationMillis = duration
intial / 3 at 0 with FastOutLinearInEasing
intial / 5 at (duration * 0.95f).toInt()
})
}

keyframes


这里又出现了一种 AnimationSpec,即帧动画 keyframes,相对于 tween ,keyframes 可以更精确指定时间区间内的动画进度。比如代码中 radius / 3 at 0 表示 0 秒时状态值达到 intial / 3 ,相当于以初始值的 1/3 尺寸出现,这是一般的 tween 难以实现的。另外我们希望花瓣可以持久可见,所以使用 keyframe 确保时间进行到 95% 时,radius 的尺寸仍然清晰可见。



animatedFlower 列表


由于烟花动画设计是表情动画的延续,所以它紧跟表情动画执行,共享 CoroutienScope ,不需要借助 LaunchedEffect ,所以使用普通列表定义 animatedFlower 即可:


//animatedFlowers 使用普通列表创建
val animatedFlowers = mutableListOf<AnimatedFlower>()

launch {
with(it) {//表情动画执行
startAnim()
animatedEmojis.remove(it)
}
//创建 AnimatedFlower 动画
val anim = AnimatedFlower(
center = it.offset,
//使用 Palette 从表情图片提取烟花颜色
color = Palette.from(it.img.asAndroidBitmap()).generate().let {
arrayOf(
Color(it.getDominantColor(Color.Transparent.toArgb())),
Color(it.getVibrantColor(Color.Transparent.toArgb()))
)
},
initial = it.img.run { width.coerceAtLeast(height) / 2 }.toFloat()
)
animatedFlowers.add(anim) //添加进列表
anim.startAnim() //执行烟花动画
animatedFlowers.remove(anim) //移除动画
}

5.2 内容绘制


烟花的内容绘制,需要计算每个花瓣的位置,一共8个花瓣,各自位置计算如下:


//计算 sin45 的值
val sin by lazy { sin(Math.PI / 4).toFloat() }

val points
get() = run {
val d1 = initial - radius.value
val d2 = (initial - radius.value) * sin
arrayOf(
center.copy(y = center.y - d1), //0点方向
center.copy(center.x + d2, center.y - d2),
center.copy(x = center.x + d1),//3点方向
center.copy(center.x + d2, center.y + d2),
center.copy(y = center.y + d1),//6点方向
center.copy(center.x - d2, center.y + d2),
center.copy(x = center.x - d1),//9点方向
center.copy(center.x - d2, center.y - d2),
)
}

center 是烟花的中心位置,随着花瓣的变小,同时越来越远离中心位置,因此 d1d2 就是偏离 center 的距离,与 radius 大小成反比。
最后在 Canvas 中绘制这些 points 即可:


fun DrawScope.draw() {
points.forEachIndexed { index, point ->
drawCircle(color = color[index % 2], center = point, radius = radius.value)
}
}

6. 合体效果


最后我们定义一个 AnimatedLike 的 Composable ,整合上面代码


@Composable
fun AnimatedLike(modifier: Modifier = Modifier, state: LikeAnimState = rememberLikeAnimState()) {

LaunchedEffect(Unit) {
//监听新增表情
snapshotFlow { state.animatedEmojis.takeLast(EmojiCnt) }
.flatMapMerge { it.asFlow() }
.collect {
launch {
with(it) {
startAnim()
state.animatedEmojis.remove(it)
}
//添加烟花动画
val anim = AnimatedFlower(
center = it.offset,
color = Palette.from(it.img.asAndroidBitmap()).generate().let {
arrayOf(
Color(it.getDominantColor(Color.Transparent.toArgb())),
Color(it.getVibrantColor(Color.Transparent.toArgb()))
)
},
initial = it.img.run { width.coerceAtLeast(height) / 2 }.toFloat()
)
state.animatedFlowers.add(anim)
anim.startAnim()
state.animatedFlowers.remove(anim)
}
}
}


LaunchedEffect(Unit) {
//监听新增彩虹
snapshotFlow { state.animatedRainbows.lastOrNull() }
.filterNotNull()
.collect {
launch {
val result = it.startAnim()
if (result.endReason == AnimationEndReason.Finished) {
state.animatedRainbows.remove(it)
}
}
}
}

//绘制动画
Canvas(modifier.fillMaxSize()) {

//绘制彩虹
state.animatedRainbows.forEach { animatable ->
with(animatable) { draw() }
}

//绘制表情
state.animatedEmojis.forEach { animatable ->
with(animatable) { draw() }
}

//绘制烟花
state.animatedFlowers.forEach { animatable ->
with(animatable) { draw() }
}
}
}

我们使用 AnimatedLike 布局就可以为页面添加动画效果了,由于 Canvas 本身是基于 modifier.drawBehind 实现的,我们也可以将 AnimatedLike 改为 Modifier 修饰符使用,这里就不赘述了。
最后,复习一下本文例子中的内容:



  • Animatable :包装动画状态值,并且在协程中执行动画,同步返回动画结果

  • AnimationSpec:动画规格,可以配置动画时长、Easing 等,例子中用到了 tween,keyframes,infiniteRepeatable 等多个动画规格

  • Easing:动画状态值随时间变化的趋势,通常使用默认类型即可, 也可以基于 CubicBezierEasing 定制。


一个例子不可能覆盖到 Compose 所有的动画 API,但是我们只要掌握了上述几个关键知识点,再学习其他 API 就是水到渠成的事情了。


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

裁员新玩法,理想降薪表?最后哪个工资降得最多,就留下来哪个

近日,网上流传深圳某公司想给员工降薪,但不直接提出要求,而是让员工自己填写一份理想降薪表。公司也不说降薪数额、幅度、范围,员工根据自己的想法填写降薪幅度,让员工和员工之间竞标。最后,谁填写的工资降得最多,谁就留下。的确,今年以来,由于全球经济萎靡和疫情的持续影...
继续阅读 »

近日,网上流传深圳某公司想给员工降薪,但不直接提出要求,而是让员工自己填写一份理想降薪表。公司也不说降薪数额、幅度、范围,员工根据自己的想法填写降薪幅度,让员工和员工之间竞标。最后,谁填写的工资降得最多,谁就留下。


的确,今年以来,由于全球经济萎靡和疫情的持续影响,很多企业生产经营都受到不同程度的打击,同时今年又有超过一千万的大学毕业生进入社会,整个社会的就业环境确实很差,但是“竞标”降薪的这种搞法,无疑比直接裁员更让人觉得恶心。


按照正常逻辑,一般公司裁员,肯定是会优先裁掉能力较差的员工,最大限度的留住优质员工,而这种神操作带来的后果就是:劣币驱逐良币,能力差的员工自甘降薪的幅度肯定是大于能力强的员工,这无疑是要把优质员工直接送走。

运用这种变态方式裁员,只能说明在这家公司老板的心理,时常回荡着一句狠话:你们不干,有的是人干!而这也充分说明了这家公司毫无管理能力,基本上就是个小作坊模式,所谓的管理层,可能就老板一个光杆司令。


江山代有才人出,各领风骚数百年。现在的企业都在秀下限,大公司把裁员说成是“毕业”“为社会输送人才”,中等公司把降薪说成是“奋斗者计划”,小作坊更有趣直接是“竞标”降薪,员工都是无产者,是被拿捏的对象,没有自主权。企业这林林总总一系列让人眼花缭乱的操作,不是让员工之间竞争,而是老板们之间在进行一场“裁员表演秀”,目的就是看谁更没有下限,看谁能给劳动仲裁员枯燥乏味的工作带来更多的惊喜。


今年以来发生在劳动市场的各种奇葩事件,可以说是一种魔幻现实主义的真实写照,上演着让人瞠目结舌的职场淘汰潜规则明规则和各种职场“自愿”。


每个人都是社会的一员,每个人也都有活在世上最起码的尊严,如果企业确实经营出现困难,按照正常途径和相关规定,让员工体面的离开,何尝不是一种善良?而这种近乎侮辱的方式,只能让双方都不体面。

“我翻开历史一查,这历史没有年代,歪歪斜斜的每页上都写着‘仁义道德’四个字。我横竖睡不着,仔细看了半夜,才从字缝里看出字来,满本都写着两个字是‘吃人’!”——《狂人日记》

来源:http://www.163.com/dy/article/H8PD5INS0552IALL.html

收起阅读 »

Flutter——平台通信记录器 : channel_observer

前言 Flutter自身的定位,决定了基于其开发的项目在不断迭代的过程中,会有越来越多的平台通信。这些通信多来自各种平台端的sdk,而这些sdk一般是由不同人、团队甚至公司负责的,所以在sdk变动过程中,可能由于沟通不够及时、或者疏忽大意而未能及时通知到客户端...
继续阅读 »

前言


Flutter自身的定位,决定了基于其开发的项目在不断迭代的过程中,会有越来越多的平台通信。这些通信多来自各种平台端的sdk,而这些sdk一般是由不同人、团队甚至公司负责的,所以在sdk变动过程中,可能由于沟通不够及时、或者疏忽大意而未能及时通知到客户端。


例如,某个字段类型由int变为string,如果这个字段涉及到核心业务线那么可能会在测试中及时发现,而如果是在非核心业务线则不一定能及时发现。 这种错误,在抵达flutter侧时多为TypeCast Error。 初期, 我们的APM 会将此类错误进行上报,但是由于platform channel众多,很难确定是由哪个channel引起的,为此我们增加了channel observer用于记录最近n条的平台通信记录。 当APM再次上报类似错误后,会导出channel记录一同上报,藉此便可排查出bug点。


下面我简单的介绍一下具体原理与实现。


原理与实现


Flutter-平台通信简介


Flutter与平台端的通信连接层位于ServicesBinding中,其主要负责监听平台信息(系统/自定义)并将其转到defaultBinaryMessenger中处理,其内部初始化方法:


mixin ServicesBinding on BindingBase, SchedulerBinding {
@override
void initInstances() {
super.initInstances();
_instance = this;
_defaultBinaryMessenger = createBinaryMessenger();

//...无关代码
}

@protected
BinaryMessenger createBinaryMessenger() {
return const _DefaultBinaryMessenger._();
}
}

通过createBinaryMessenger 方法,创建了一个_DefaultBinaryMessenger对象,Flutter平台端通信都由此类来负责,其内部实现如下:


class _DefaultBinaryMessenger extends BinaryMessenger {
const _DefaultBinaryMessenger._();

///当我们在调用 xxxChannel.invokeMethod()方法时,最终会调用到send()方法,
@override
Future<ByteData?> send(String channel, ByteData? message) {
final Completer<ByteData?> completer = Completer<ByteData?>();

///channel : 通道名
///message : 你的参数
///通过engine中转到平台端
ui.PlatformDispatcher.instance.sendPlatformMessage(channel, message, (ByteData? reply){
try {
///reply : 平台端返回的结果
completer.complete(reply);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: ErrorDescription('during a platform message response callback'),
));
}
});
return completer.future;
}

///此方法与上面的 send 方法相对应,是服务于平台端调用flutter的方法。
///
///当我们通过方法 :
/// channel.setMethodCallHandler(xxHandler)
///在flutter侧对 channel绑定一个回调用于处理平台端的调用时,
///最终会转到此方法。
///
///通过channelBuffers,会记录下你的channel name以及对应的handler,
///当平台端调用flutter方法时,会查找对应channel的handler并执行。
@override
void setMessageHandler(String channel, MessageHandler? handler) {
if (handler == null) {
ui.channelBuffers.clearListener(channel);
} else {
ui.channelBuffers.setListener(channel, (ByteData? data, ui.PlatformMessageResponseCallback callback) async {
ByteData? response;
try {
response = await handler(data);
} catch (exception, stack) {
FlutterError.reportError(FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'services library',
context: ErrorDescription('during a platform message callback'),
));
} finally {
callback(response);
}
});
}
}
}

通过上面的了解我们便知道了入手点:只需增加一个_DefaultBinaryMessenger的代理类即可。


实现


首先,我们需要自定义WidgetsFlutterBinding以混入我们自定义的ServicesBinding :


class ChannelObserverBinding extends WidgetsFlutterBinding with ChannelObserverServicesBinding{
static WidgetsBinding ensureInitialized() {
if(WidgetsBinding.instance == null) {
ChannelObserverBinding();
}
return WidgetsBinding.instance!;
}
}

随后我们在自定义的ServicesBinding中,添加我们的代理类BinaryMessengerProxy


mixin ChannelObserverServicesBinding on BindingBase, ServicesBinding{

late BinaryMessengerProxy _proxy;

@override
BinaryMessenger createBinaryMessenger() {
_proxy = BinaryMessengerProxy(super.createBinaryMessenger());
return _proxy;
}
}

这样我们就可以在代理类中,对平台通信进行记录了:


class BinaryMessengerProxy extends BinaryMessenger{

BinaryMessengerProxy(this.origin);

///....省略代码

@override
Future<void> handlePlatformMessage(String channel, ByteData? data, PlatformMessageResponseCallback? callback) {
return origin.handlePlatformMessage(channel, data, callback);
}

///这里我们对flutter的调用做记录
@override
Future<ByteData?>? send(String channel, ByteData? message) async {
//记录channel通信
final ChannelModel model = _recordChannel(channel, message, true);
if(model.isAbnormal) {
return origin.send(channel, message);
}
final ByteData? result = await origin.send(channel, message);
_resolveResult(model, result);
return result;
}

///这里我们可以对平台端的调用做记录
/// * 对MessageHandler增加一个代理即可。
@override
void setMessageHandler(String channel, MessageHandler? handler) {
origin.setMessageHandler(channel, handler);
}

}

效果图


当我们捕捉到TypeCast error时,就可以将异常堆栈及channel的通信记录一同上传。开发同学便可借助堆栈信息和调用记录,定位到具体的异常channel


其他


项目地址


channel_observer_of_kit


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

视频直播技术干货:一文读懂主流视频直播系统的推拉流架构、传输协议等

本文由蘑菇街前端开发工程师“三体”分享,原题“蘑菇街云端直播探索——启航篇”,有修订。1、引言随着移动网络网速的提升与资费的降低,视频直播作为一个新的娱乐方式已经被越来越多的用户逐渐接受。特别是最近这几年,视频直播已经不仅仅被运用在传统的秀场、游戏类板块,更是...
继续阅读 »

本文由蘑菇街前端开发工程师“三体”分享,原题“蘑菇街云端直播探索——启航篇”,有修订。

1、引言

随着移动网络网速的提升与资费的降低,视频直播作为一个新的娱乐方式已经被越来越多的用户逐渐接受。特别是最近这几年,视频直播已经不仅仅被运用在传统的秀场、游戏类板块,更是作为电商的一种新模式得到迅速成长。

本文将通过介绍实时视频直播技术体系,包括常用的推拉流架构、传输协议等,让你对现今主流的视频直播技术有一个基本的认知。


2、蘑菇街的直播架构概览

目前蘑菇街直播推拉流主流程依赖于某云直播的服务。

云直播提供的推流方式有两种:

  • 1)一是通过集成SDK的方式进行推流(用于手机端开播);

  • 2)另一种是通过RTMP协议向远端服务器进行推流(用于PC开播端或专业控台设备开播)。

除去推拉流,该云平台也提供了云通信(IM即时通讯能力)和直播录制等云服务,组成了一套直播所需要的基础服务。

3、推拉流架构1:厂商SDK推拉流


如上题所示,这一种推拉流架构方式需要依赖腾讯这类厂商提供的手机互动直播SDK,通过在主播端APP和用户端APP都集成SDK,使得主播端和用户端都拥有推拉流的功能。

这种推拉流架构的逻辑原理是这样的:

  • 1)主播端和用户端分别与云直播的互动直播后台建立长连接;

  • 2)主播端通过UDT私有协议向互动直播后台推送音视频流;

  • 3)互动直播后台接收到音视频流后做转发,直接下发给与之建立连接的用户端。

这种推拉流方式有几点优势:

  • 1)只需要在客户端中集成SDK:通过手机就可以开播,对于主播开播的要求比较低,适合直播业务快速铺开;

  • 2)互动直播后台仅做转发:没有转码,上传CDN等额外操作,整体延迟比较低;

  • 3)主播端和用户端都可以作为音视频上传的发起方:适合连麦、视频会话等场景。

4、推拉流架构2:旁路推流

之前介绍了通过手机SDK推拉流的直播方式,看起来在手机客户端中观看直播的场景已经解决了。

那么问题来了:如果我想要在H5、小程序等其他场景下观看直播,没有办法接入SDK,需要怎么处理呢?

这个时候需要引入一个新的概念——旁路推流。

旁路推流指的是:通过协议转换将音视频流对接到标准的直播 CDN 系统上。

目前云直播开启旁路推流后,会通过互动直播后台将音视频流推送到云直播后台,云直播后台负责将收到音视频流转码成通用的协议格式并且推送到CDN,这样H5、小程序等端就可以通过CDN拉取到通用格式的音视频流进行播放了。


目前蘑菇街直播旁路开启的协议类型有HLS、FLV、RTMP三种,已经可以覆盖到所有的播放场景,在后续章节会对这几种协议做详细的介绍。

5、推拉流架构3:RTMP推流

随着直播业务发展,一些主播逐渐不满足于手机开播的效果,并且电商直播需要高保真地将商品展示在屏幕上,需要通过更加高清专业的设备进行直播,RTMP推流技术应运而生。

我们通过使用OBS等流媒体录影程序,对专业设备录制的多路流进行合并,并且将音视频流上传到指定的推流地址。由于OBS推流使用了RTMP协议,因此我们称这一种推流类型为RTMP推流。

我们首先在云直播后台申请到推流地址和秘钥,将推流地址和秘钥配置到OBS软件当中,调整推流各项参数,点击推流以后,OBS就会通过RTMP协议向对应的推流地址推送音视频流。

这一种推流方式和SDK推流的不同之处在于音视频流是直接被推送到了云直播后台进行转码和上传CDN的,没有直接将直播流转推到用户端的下行方式,因此相比SDK推流延迟会长一些。


总结下来RTMP推流的优势和劣势比较明显。

优势主要是:

  • 1)可以接入专业的直播摄像头、麦克风,直播的整体效果明显优于手机开播;

  • 2)OBS已经有比较多成熟的插件,比如目前蘑菇街主播常用YY助手做一些美颜的处理,并且OBS本身已经支持滤镜、绿幕、多路视频合成等功能,功能比手机端强大。

劣势主要是:

  • 1)OBS本身配置比较复杂,需要专业设备支持,对主播的要求明显更高,通常需要一个固定的场地进行直播;

  • 2)RTMP需要云端转码,并且本地上传时也会在OBS中配置GOP和缓冲,延时相对较长。

6、高可用架构方案:云互备

业务发展到一定阶段后,我们对于业务的稳定性也会有更高的要求,比如当云服务商服务出现问题时,我们没有备用方案就会出现业务一直等待服务商修复进度的问题。

因此云互备方案就出现了:云互备指的是直播业务同时对接多家云服务商,当一家云服务商出现问题时,快速切换到其他服务商的服务节点,保证业务不受影响。


直播业务中经常遇到服务商的CDN节点下行速度较慢,或者是CDN节点存储的直播流有问题,此类问题有地域性,很难排查,因此目前做的互备云方案,主要是备份CDN节点。

目前蘑菇街整体的推流流程已经依赖了原有云平台的服务,因此我们通过在云直播后台中转推一路流到备份云平台上,备份云在接收到了直播流后会对流转码并且上传到备份云自身的CDN系统当中。一旦主平台CDN节点出现问题,我们可以将下发的拉流地址替换成备份云拉流地址,这样就可以保证业务快速修复并且观众无感知。

7、视频直播数据流解封装原理

介绍流协议之前,先要介绍我们从云端拿到一份数据,要经过几个步骤才能解析出最终需要的音视频数据。


如上图所示,总体来说,从获取到数据到最终将音视频播放出来要经历四个步骤。

*第一步:*解协议。

协议封装的时候通常会携带一些头部描述信息或者信令数据,这一部分数据对我们音视频播放没有作用,因此我们需要从中提取出具体的音视频封装格式数据,我们在直播中常用的协议有HTTP和RTMP两种。

*第二步:*解封装。

获取到封装格式数据以后需要进行解封装操作,从中分别提取音频压缩流数据和视频压缩流数据,封装格式数据我们平时经常见到的如MP4、AVI,在直播中我们接触比较多的封装格式有TS、FLV。

*第三步:*解码音视频。

到这里我们已经获取了音视频的压缩编码数据。

我们日常经常听到的视频压缩编码数据有H.26X系列和MPEG系列等,音频编码格式有我们熟悉的MP3、ACC等。

之所以我们能见到如此多的编码格式,是因为各种组织都提出了自己的编码标准,并且会相继推出一些新的议案,但是由于推广和收费问题,目前主流的编码格式也并不多。

获取压缩数据以后接下来需要将音视频压缩数据解码,获取非压缩的颜色数据和非压缩的音频抽样数据。颜色数据有我们平时熟知的RGB,不过在视频的中常用的颜色数据格式是YUV,指的是通过明亮度、色调、饱和度确定一个像素点的色值。音频抽样数据通常使用的有PCM。

*第四步:*音视频同步播放。

最后我们需要比对音视频的时间轴,将音视频解码后的数据交给显卡声卡同步播放。

8、视频直播传输协议1:HLS

首先介绍一下HLS协议。HLS是HTTP Live Streaming的简写,是由苹果公司提出的流媒体网络传输协议。

从名字可以明显看出:这一套协议是基于HTTP协议传输的。

说到HLS协议:首先需要了解这一种协议是以视频切片的形式分段播放的,协议中使用的切片视频格式是TS,也就是我们前文提到的封装格式。

在我们获取TS文件之前:协议首先要求请求一个M3U8格式的文件,M3U8是一个描述索引文件,它以一定的格式描述了TS地址的指向,我们根据M3U8文件中描述的内容,就可以获取每一段TS文件的CDN地址,通过加载TS地址分段播放就可以组合出一整段完整的视频。


使用HLS协议播放视频时:首先会请求一个M3U8文件,如果是点播只需要在初始化时获取一次就可以拿到所有的TS切片指向,但如果是直播的话就需要不停地轮询M3U8文件,获取新的TS切片。

获取到M3U8后:我们可以看一下里面的内容。首先开头是一些通用描述信息,比如第一个分片序列号、片段最大时长和总时长等,接下来就是具体TS对应的地址列表。如果是直播,那么每次请求M3U8文件里面的TS列表都会随着最新的直播切片更新,从而达到直播流播放的效果。


HLS这种切片播放的格式在点播播放时是比较适用的,一些大的视频网站也都有用这一种协议作为播放方案。

首先:切片播放的特性特别适用于点播播放中视频清晰度、多语种的热切换。比如我们播放一个视频,起初选择的是标清视频播放,当我们看了一半觉得不够清晰,需要换成超清的,这时候只需要将标清的M3U8文件替换成超清的M3U8文件,当我们播放到下一个TS节点时,视频就会自动替换成超清的TS文件,不需要对视频做重新初始化。

其次:切片播放的形式也可以比较容易地在视频中插入广告等内容。


在直播场景下,HLS也是一个比较常用的协议,他最大的优势是苹果大佬的加持,对这一套协议推广的比较好,特别是移动端。将M3U8文件地址喂给video就可以直接播放,PC端用MSE解码后大部分浏览器也都能够支持。但是由于其分片加载的特性,直播的延迟相对较长。比如我们一个M3U8有5个TS文件,每个TS文件播放时长是2秒,那么一个M3U8文件的播放时长就是10秒,也就是说这个M3U8播放的直播进度至少是10秒之前的,这对于直播场景来说是一个比较大的弊端。


HLS中用到的TS封装格式,视频编码格式是通常是H.264或MPEG-4,音频编码格式为AAC或MP3。

一个ts由多个定长的packtet组成,通常是188个字节,每个packtet有head和payload组成,head中包含一些标识符、错误信息、包位置等基础信息。payload可以简单理解为音视频信息,但实际上下层还有还有两层封装,将封装解码后可以获取到音视频流的编码数据。


9、视频直播传输协议2:HTTP-FLV

HTTP-FLV协议,从名字上就可以明显看出是通过HTTP协议来传输FLV封装格式的一种协议。

FLV是Flash Video的简写,是一种文件体积小,适合在网络上传输的封包方式。FlV的视频编码格式通常是H.264,音频编码是ACC或MP3。


HTTP-FLV在直播中是通过走HTTP长连接的方式,通过分块传输向请求端传递FLV封包数据。


在直播中,我们通过HTTP-FLV协议的拉流地址可以拉取到一段chunked数据。

打开文件后可以读取到16进制的文件流,通过和FLV包结构对比,可以发现这些数据就是我们需要的FLV数据。

首先开头是头部信息:464C56转换ASCII码后是FLV三个字符,01指的是版本号,05转换为2进制后第6位和第8位分别代表是否存在音频和视频,09代表头部长度占了几个字节。

后续就是正式的音视频数据:是通过一个个的FLV TAG进行封装,每一个TAG也有头部信息,标注这个TAG是音频信息、视频信息还是脚本信息。我们通过解析TAG就可以分别提取音视频的压缩编码信息。

FLV这一种格式在video中并不是原生支持的,我们要播放这一种格式的封包格式需要通过MSE对影视片的压缩编码信息进行解码,因此需要浏览器能够支持MSE这一API。由于HTTP-FLV的传输是通过长连接传输文件流的形式,需要浏览器支持Stream IO或者fetch,对于浏览器的兼容性要求会比较高。

FLV在延迟问题上相比切片播放的HLS会好很多,目前看来FLV的延迟主要是受编码时设置的GOP长度的影响。

这边简单介绍一下GOP:在H.264视频编码的过程中,会生成三种帧类型:I帧、B帧和P帧。I帧就是我们通常说的关键帧,关键帧内包括了完整的帧内信息,可以直接作为其他帧的参考帧。B帧和P帧为了将数据压缩得更小,需要由其他帧推断出帧内的信息。因此两个I帧之间的时长也可以被视作最小的视频播放片段时长。从视频推送的稳定性考虑,我们也要求主播将关键帧间隔设置为定长,通常是1-3秒,因此除去其他因素,我们的直播在播放时也会产生1-3秒的延时。


10、视频直播传输协议3:RTMP

RTMP协议实际可以与HTTP-FLV协议归做同一种类型。

他们的封包格式都是FlV,但HTTP-FLV使用的传输协议是HTTP,RTMP拉流使用RTMP作为传输协议。

RTMP是Adobe公司基于TCP做的一套实时消息传输协议,经常与Flash播放器匹配使用。

RTMP协议的优缺点非常明显。

RTMP协议的优点主要是:

  • 1)首先和HTTP-FLV一样,延迟比较低;

  • 2)其次它的稳定性非常好,适合长时间播放(由于播放时借用了Flash player强大的功能,即使开多路流同时播放也能保证页面不出现卡顿,很适合监控等场景)。

但是Flash player目前在web端属于墙倒众人推的境地,主流浏览器渐渐都表示不再支持Flash player插件,在MAC上使用能够立刻将电脑变成烧烤用的铁板,资源消耗很大。在移动端H5基本属于完全不支持的状态,兼容性是它最大的问题。


11、视频直播传输协议4:MPEG-DASH

MPEG-DASH这一协议属于新兴势力,和HLS一样,都是通过切片视频的方式进行播放。

他产生的背景是早期各大公司都自己搞自己的一套协议。比如苹果搞了HLS、微软搞了 MSS、Adobe还搞了HDS,这样使用者需要在多套协议封装的兼容问题上痛苦不堪。

于是大佬们凑到一起,将之前各个公司的流媒体协议方案做了一个整合,搞了一个新的协议。

由于同为切片视频播放的协议,DASH优劣势和HLS类似,可以支持切片之间多视频码率、多音轨的切换,比较适合点播业务,在直播中还是会有延时较长的问题。


12、如何选择最优的视频直播传输协议

视频直播协议选择非常关键的两点,在前文都已经有提到了,即低延时和更优的兼容性。

首先从延时角度考虑:不考虑云端转码以及上下行的消耗,HLS和MPEG-DASH通过将切片时长减短,延时在10秒左右;RTMP和FLV理论上延时相当,在2-3秒。因此在延时方面HLS ≈ DASH > RTMP ≈ FLV。

从兼容性角度考虑:HLS > FLV > RTMP,DASH由于一些项目历史原因,并且定位和HLS重复了,暂时没有对其兼容性做一个详尽的测试,被推出了选择的考虑范围。

综上所述:我们可以通过动态判断环境的方式,选择当前环境下可用的最低延迟的协议。大致的策略就是优先使用HTTP-FLV,使用HLS作为兜底,在一些特殊需求场景下通过手动配置的方式切换为RTMP。

对于HLS和HTTP-FLV:我们可以直接使用 hls.jsflv.js 做做解码播放,这两个库内部都是通过MSE做的解码。首先根据视频封装格式提取出对应的音视频chunk数据,在MediaSource中分别对音频和视频创建SourceBuffer,将音视频的编码数据喂给SourceBuffer后SourceBuffer内部会处理完剩下的解码和音视频对齐工作,最后MediaSource将Video标签中的src替换成MediaSource 对象进行播放。


在判断播放环境时我们可以参照flv.js内部的判断方式,通过调用MSE判断方法和模拟请求的方式判断MSE和StreamIO是否可用:

// 判断MediaSource是否被浏览器支持,H.264视频编码和Acc音频编码是否能够被支持解码

window.MediaSource && window.MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"');

如果FLV播放不被支持的情况下:需要降级到HLS,这时候需要判断浏览器环境是否在移动端,移动端通常不需要 hls.js 通过MSE解码的方式进行播放,直接将M3U8的地址交给video的src即可。如果是PC端则判断MSE是否可用,如果可用就使用hls.js解码播放。

这些判读可以在自己的逻辑里提前判断后去拉取对应解码库的CDN,而不是等待三方库加载完成后使用三方库内部的方法判断,这样在选择解码库时就可以不把所有的库都拉下来,提高加载速度。

13、同层播放如何解决


电商直播需要观众操作和互动的部分比起传统的直播更加多,因此产品设计的时候很多的功能模块会悬浮在直播视频上方减少占用的空间。这个时候就会遇到一个移动端播放器的老大难问题——同层播放。

同层播放问题:是指在移动端H5页面中,一些浏览器内核为了提升用户体验,将video标签被劫持替换为native播放器,导致其他元素无法覆盖于播放器之上。

比如我们想要在直播间播放器上方增加聊天窗口,将聊天窗口通过绝对定位提升z-index置于播放器上方,在PC中测试完全正常。但在移动端的一些浏览器中,video被替换成了native播放器,native的元素层级高于我们的普通元素,导致聊天窗口实际显示的时候在播放器下方。

要解决这个问题,首先要分多个场景。

首先在iOS系统中:正常情况下video标签会自动被全屏播放,但iOS10以上已经原生提供了video的同层属性,我们在video标签上增加playsinline/webkit-playsinline可以解决iOS系统中大部分浏览器的同层问题,剩下的低系统版本的浏览器以及一些APP内的webview容器(譬如微博),用上面提的属性并不管用,调用三方库iphone-inline-video可以解决大部分剩余问题。

在Android端:大部分腾讯系的APP内置的webview容器用的都是X5内核,X5内核会将video替换成原生定制的播放器已便于增强一些功能。X5也提供了一套同层的方案(该方案官方文档链接已无法打开),给video标签写入X5同层属性也可以在X5内核中实现内联播放。不过X5的同层属性在各个X5版本中表现都不太一样(比如低版本X5中需要使用X5全屏播放模式才能保证MSE播放的视频同层生效),需要注意区分版本。

在蘑菇街App中,目前集成的X5内核版本比较老,在使用MSE的情况下会导致X5同层参数不生效。但如果集成新版本的X5内核,需要对大量的线上页面做回归测试,成本比较高,因此提供了一套折中的解决方案。通过在页面URL中增加一个开关参数,容器读取到参数以后会将X5内核降级为系统原生的浏览器内核,这样可以在解决浏览器视频同层问题的同时也将内核变动的影响范围控制在单个页面当中。

来源:http://www.blogjava.net/jb2011/archive/2022/05/31/450754.html

收起阅读 »

节日献礼:Flutter图片库重磅开源!

去年,闲鱼新一代图片库 PowerImage 在经过一系列灰度、问题修复、代码调优后,已全量稳定应用于闲鱼。相对于上一代 IFImage,PowerImage 经过进一步的演进,适应了更多的业务场景与最新的 flutter 特性,解决了一系列痛点:比如,因为完...
继续阅读 »

背景:

去年,闲鱼新一代图片库 PowerImage 在经过一系列灰度、问题修复、代码调优后,已全量稳定应用于闲鱼。相对于上一代 IFImage,PowerImage 经过进一步的演进,适应了更多的业务场景与最新的 flutter 特性,解决了一系列痛点:比如,因为完全抛弃了原生的 ImageCache,在与原生图片混用的场景下,会让一些低频的图片反而占用了缓存;比如,我们在模拟器上无法展示图片;比如我们在相册中,需要在图片库之外再搭建图片通道。

简介:

PowerImage 是一个充分利用 native 原生图片库能力、高扩展性的flutter图片库。我们巧妙地将外接纹理与 ffi 方案组合,以更贴近原生的设计,解决了一系列业务痛点。

能力特点:

  • 支持加载 ui.Image 能力。在基于外接纹理的方案中,使用方无法拿到真正的 ui.Image 去使用,这导致图片库在这种特殊的使用场景下无能为力。

  • 支持图片预加载能力。正如原生precacheImage一样。这在某些对图片展示速度要求较高的场景下非常有用。

  • 新增纹理缓存,与原生图片库缓存打通!统一图片缓存,避免原生图片混用带来的内存问题。

  • 支持模拟器。在 flutter-1.23.0-18.1.pre之前的版本,模拟器无法展示 Texture Widget。

  • 完善自定义图片类型通道。解决业务自定义图片获取诉求。

  • 完善的异常捕获与收集。

  • 支持动图。(来自淘特的PR)

Flutter 原生方案:

在介绍新方案开始之前,先简单回忆一下 flutter 原生图片方案。


原生 Image Widget 先通过 ImageProvider 得到 ImageStream,通过监听它的状态,进行各种状态的展示。比如frameBuilderloadingBuilder,最终在图片加载成功后,会 rebuildRawImageRawImage 会通过 RenderImage 来绘制,整个绘制的核心是 ImageInfo 中的 ui.Image

  • Image:负责图片加载的各个状态的展示,如加载中、失败、加载成功展示图片等。

  • ImageProvider:负责 ImageStream 的获取,比如系统内置的 NetworkImage、AssetImage 等。

  • ImageStream:图片资源加载的对象。

在梳理 flutter 原生图片方案之后,我们发现是不是有机会在某个环节将 flutter 图片和 native 以原生的方式打通?

新一代方案:

我们巧妙地将 FFi 方案与外接纹理方案组合,解决了一系列业务痛点。

FFI:

正如开头说的那些问题,Texture 方案有些做不到的事情,这需要其他方案来互补,这其中核心需要的就是 ui.Image。我们把 native 内存地址、长度等信息传递给 flutter 侧,用于生成 ui.Image

首先 native 侧先获取必要的参数(以 iOS 为例):

_rowBytes = CGImageGetBytesPerRow(cgImage);
CGDataProviderRef dataProvider = CGImageGetDataProvider(cgImage);
CFDataRef rawDataRef = CGDataProviderCopyData(dataProvider);
_handle = (long)CFDataGetBytePtr(rawDataRef);
NSData *data = CFBridgingRelease(rawDataRef);
self.data = data;
_length = data.length;

dart 侧拿到后

@override  FutureOr createImageInfo(Map map) {
Completer completer = Completer();
int handle = map['handle'];
int length = map['length'];
int width = map['width'];
int height = map['height'];
int rowBytes = map['rowBytes'];
ui.PixelFormat pixelFormat = ui.PixelFormat.values[map['flutterPixelFormat'] ?? 0];
pointer.asTypedList(length);
ui.decodeImageFromPixels(pixels, width, height, pixelFormat,(ui.Image image) {
ImageInfo imageInfo = ImageInfo(image: image);
completer.complete(imageInfo);     //释放 native 内存
PowerImageLoader.instance.releaseImageRequest(options);
}, rowBytes: rowBytes);
return completer.future;
}

我们可以通过 ffi 拿到 native 内存,从而生成 ui.Image。这里有个问题,虽然通过 ffi 能直接获取 native 内存,但是由于 decodeImageFromPixels 会有内存拷贝,在拷贝解码后的图片数据时,内存峰值会更加严重。

这里有两个优化方向:

  1. 解码前的图片数据给 flutter,由 flutter 提供的解码器解码,从而削减内存拷贝峰值。

  2. 与 flutter 官方讨论,尝试从内部减少这次内存拷贝。

FFI 这种方式适合轻度使用、特殊场景使用,支持这种方式可以解决无法获取 ui.Image 的问题,也可以在模拟器上展示图片(flutter <= 1.23.0-18.1.pre),并且图片缓存将完全交给 ImageCache 管理。

Texture:

Texture 方案与原生结合有一些难度,这里涉及到没有 ui.Image 只有 textureId。这里有几个问题需要解决:

问题一:Image Widget 需要 ui.Image 去 build RawImage 从而绘制,这在本文前面的Flutter 原生方案介绍中也提到了。 问题二:ImageCache 依赖 ImageInfo 中 ui.Image 的宽高进行 cache 大小计算以及缓存前的校验。 问题三:native 侧 texture 生命周期管理

都有解决方案:

问题一:通过自定义 Image 解决,透出 imageBuilder 来让外部自定义图片 widget 问题二:为 Texture 自定义 ui.image,如下:

import 'dart:typed_data';
import 'dart:
ui'
as ui show Image;
import 'dart:ui';
class TextureImage implements ui.Image {
    int _width;
    int _height;
    int textureId;
   TextureImage(this.textureId, int width, int height)     : _width = width,       _height = height;
    @override void dispose() {
    // TODO: implement dispose }
     @override int get height => _height;
     @override Future
toByteData(     {ImageByteFormat format = ImageByteFormat.rawRgba}) {  
         // TODO: implement toByteData  
             throw UnimplementedError();
     }
     @override int get width => _width;
}

这样的话,TextureImage 实际上就是个壳,仅仅用来计算 cache 大小。 实际上,ImageCache 计算大小,完全没必要直接接触到 ui.Image,可以直接找 ImageInfo 取,这样的话就没有这个问题了。这个问题可以具体看 @皓黯 的 ISSUE 与 PR。

问题三:关于 native 侧感知 flutter image 释放时机的问题

修改的 ImageCache 释放如下(部分代码):

typedef void HasRemovedCallback(dynamic key, dynamic value);

class RemoveAwareMap<K, V> implements Map<K, V> {
HasRemovedCallback hasRemovedCallback;
...
}
//------
final RemoveAwareMap<Object, _PendingImage> _pendingImages = RemoveAwareMap<Object, _PendingImage>();
//------
void hasImageRemovedCallback(dynamic key, dynamic value) {
if (key is ImageProviderExt) {
waitingToBeCheckedKeys.add(key);
}
if (isScheduledImageStatusCheck) return;
isScheduledImageStatusCheck = true;
//We should do check in MicroTask to avoid if image is remove and add right away
scheduleMicrotask(() {
waitingToBeCheckedKeys.forEach((key) {
if (!_pendingImages.containsKey(key) &&
!_cache.containsKey(key) &&
!_liveImages.containsKey(key)) {
if (key is ImageProviderExt) {
key.dispose();
}
}
});
waitingToBeCheckedKeys.clear();
isScheduledImageStatusCheck = false;
});
}

整体架构:

我们将两种解决方案非常优雅地结合在了一起:


我们抽象出了 PowerImageProvider ,对于 external(ffi)、texture,分别生产自己的 ImageInfo 即可。它将通过对 PowerImageLoader 的调用,提供统一的加载与释放能力。

蓝色实线的 ImageExt 即为自定义的 Image Widget,为 texture 方式透出了 imageBuilder。

蓝色虚线 ImageCacheExt 即为 ImageCache 的扩展,仅在 flutter < 2.2.0 版本才需要,它将提供 ImageCache 释放时机的回调。

这次,我们也设计了超强的扩展能力。除了支持网络图、本地图、flutter 资源、native 资源外,我们提供了自定义图片类型的通道,flutter 可以传递任何自定义的参数组合给 native,只要 native 注册对应类型 loader,比如「相册」这种场景,使用方可以自定义 imageType 为 album ,native 使用自己的逻辑进行加载图片。有了这个自定义通道,甚至图片滤镜都可以使用 PowerImage 进行展示刷新。

除了图片类型的扩展,渲染类型也可进行自定义。比如在上面 ffi 中说的,为了降低内存拷贝带来的峰值问题,使用方可以在 flutter 侧进行解码,当然这需要 native 图片库提供解码前的数据。

数据:

FFI vs Texture:


机型:iPhone 11 Pro;图片:300 张网络图;行为:在listView中手动滚动到底部再滚动到顶部;native Cache20 maxMemoryCount; flutter Cache30MBflutter version 2.5.3; release 模式下

这里有两个现象:

FFI:   186MB波动Texture: 194MB波动

在 2.5.3 版本中,Texture 方案与 FFI,在内存水位上差异不大,内存波动上面与 flutter 1.22 结论相反。

图中棋格图,为打开 checkerboardRasterCacheImages 后所展示,可以看出,ffi方案会缓存整个cell,而texture方案,只有cell中的文字被缓存,RasterCache 会使得 ffi 在流畅度方面会有一定优势。

滚动流畅性分析:


设备: Android OnePlus 8t,CPU和GPU进行了锁频。case: GridView每行4张图片,300张图片,从上往下,再从下往上,滑动幅度从500,1000,1500,2000,2500,5轮滑动。重复20次。方式: for i in {1..20}; do flutter drive --target=test_driver/app.dart --profile; done 跑数据,获取TimeLine数据并分析。
复制代码

结论:

  • UI thread 耗时 texture 方式最好,PowerImage 略好于 IFImage,FFI方式波动比较大。

  • Raster thread 耗时 PowerImage 好于 IFImage。Origin 原生方式好是因为对图片 resize了,其他方式加载的是原图。

更精简的代码:


dart 侧代码有较大幅度的减少,这归功于技术方案贴合 flutter 原生设计,我们与原生图片共用较多代码。

FFI 方案补全了外接纹理的不足,遵循原生 Image 的设计规范,不仅让我们享受到 ImageCache 带来的统一管理,也带来了更精简的代码。

单测:


为了保证核心代码的稳定性,我们有着较为完善的单测,行覆盖率接近95%。

关于开源:

我们期待通过社区的力量让 PowerImage 更加完善与强大,也希望 PowerImage 能为大家在工程研发中带来收益。

Issues:

关于 issue,我们希望大家在使用 PowerImage 遇到问题与诉求时,积极交流,提出 issue 时尽可能提供详细的信息,以减少沟通成本。在提出 issue 前,请确保已阅读 readme。


对于 bug 的 issue,我们自定义了模板(Bug report),可以方便地填一些必要的信息。其他类型则可以选择 Open a blank issue

我们每周会花部分时间统一处理 issues,也期待大家的讨论与 PR。

PR:

为了保持 PowerImage 核心功能的稳定性,我们有着完善的单测,行覆盖率达到了 95%(power_image库)。

在提交PR时,请确保所提交的代码被单测覆盖到,并且涉及到的单测代码请同时提交。


得益于 Github 的 Actions 能力,我们在主分支 push 代码、对主分支进行 PR 操作时,都会触发 flutter test任务,只有单测通过才可合入。

未来:

开源是 PowerImage 的开始,而不是结束,PowerImage 可做的事情还有很多,有趣而丰富。比如第一个 issue 中描述的 loadingBuilder 如何实现?比如 ffi 方案如何支持动图?再比如Kotlin和Swift···

PowerImage 未来将持续演进,在当前 texture 方案与 ffi 方案共存的情况下,伴随着 flutter 本身的迭代,我们将更倾向于向 ffi 发展,正如在上文的对比中, ffi 方案可以天然享用 raster cache 所带来的流畅度的优势。

PowerImage 也会持续追随 flutter 的脚步,以始终贴合原生的设计理念,不断进步,我们希望更多的同学加入进来,共同成长。

其他四个Flutter开源项目: 闲鱼技术**公众号-闲鱼开源

PowerImage相关链接:

GitHub:(✅star🌟)

github.com/alibaba/pow…

Flutter pub:(✅like👍)

pub.dev/packages/po…

作者:闲鱼技术——新宿

收起阅读 »

MapperStruct:一款CURD神器

前言 相信绝大多数的业务开发同学,日常的工作都离不开写getter、setter方法。要么是将下游的RPC结果通过getter、setter方法进行获取组装。要么就是将自己系统内部的处理结果通过getter、setter方法处理成前端所需要的VO对象。publ...
继续阅读 »

前言

相信绝大多数的业务开发同学,日常的工作都离不开写getter、setter方法。要么是将下游的RPC结果通过getter、setter方法进行获取组装。要么就是将自己系统内部的处理结果通过getter、setter方法处理成前端所需要的VO对象。

public UserInfoVO originalCopyItem(UserDTO userDTO){
   UserInfoVO userInfoVO = new UserInfoVO();
   userInfoVO.setUserName(userDTO.getName());
   userInfoVO.setAge(userDTO.getAge());
   userInfoVO.setBirthday(userDTO.getBirthday());
   userInfoVO.setIdCard(userDTO.getIdCard());
   userInfoVO.setGender(userDTO.getGender());
   userInfoVO.setIsMarried(userDTO.getIsMarried());
   userInfoVO.setPhoneNumber(userDTO.getPhoneNumber());
   userInfoVO.setAddress(userDTO.getAddress());
   return userInfoVO;
}

传统的方法一般是采用硬编码,将每个对象的值都逐一设值。当然为了偷懒也会有采用一些BeanUtil简约代码的方式:

public UserInfoVO utilCopyItem(UserDTO userDTO){
   UserInfoVO userInfoVO = new UserInfoVO();
   //采用反射、内省机制实现拷贝
   BeanUtils.copyProperties(userDTO, userInfoVO);
   return userInfoVO;
}

但是,像BeanUtils这类通过反射、内省等实现的框架,在速度上会带来比较严重的影响。尤其是对于一些大字段、大对象而言,这个速度的缺陷就会越明显。针对速度这块我还专门进行了测试,对普通的setter方法、BeanUtils的拷贝以及本次需要介绍的mapperStruct进行了一次对比。得到的耗时结果如下所示:(具体的运行代码请见附录)

运行次数setter方法耗时BeanUtils拷贝耗时MapperStruct拷贝耗时
12921528(1)3973292(1.36)2989942(1.023)
102362724(1)66402953(28.10)3348099(1.417)
1002500452(1)71741323(28.69)2120820(0.848)
10003187151(1)157925125(49.55)5456290(1.711)
100005722147(1)300814054(52.57)5229080(0.913)
10000019324227(1)244625923(12.65)12932441(0.669)

以上单位均为毫微秒。括号内的为当前组件同Setter比较的比值。可以看到BeanUtils的拷贝耗时基本为setter方法的十倍、二十倍以上。而MapperStruct方法拷贝的耗时,则与setter方法相近。由此可见,简单的BeanUtils确实会给服务的性能带来很大的压力。而MapperStruct拷贝则可以很好的解决这个问题。

使用教程

maven依赖

首先要导入mapStruct的maven依赖,这里我们选择最新的版本1.5.0.RC1。

...
<properties>
   <org.mapstruct.version>1.5.0.RC1</org.mapstruct.version>
</properties>
...

//mapStruct maven依赖
<dependencies>
   <dependency>
       <groupId>org.mapstruct</groupId>
       <artifactId>mapstruct</artifactId>
       <version>${org.mapstruct.version}</version>
   </dependency>
</dependencies>
...
   
//编译的组件需要配置
<build>
   <plugins>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.8.1</version>
           <configuration>
               <source>1.8</source> <!-- depending on your project -->
               <target>1.8</target> <!-- depending on your project -->
               <annotationProcessorPaths>
                   <path>
                       <groupId>org.mapstruct</groupId>
                       <artifactId>mapstruct-processor</artifactId>
                       <version>${org.mapstruct.version}</version>
                   </path>
                   <!-- other annotation processors -->
               </annotationProcessorPaths>
           </configuration>
       </plugin>
   </plugins>
</build>

在引入maven依赖后,我们首先来定义需要转换的DTO及VO信息,主要包含的信息是名字、年龄、生日、性别等信息。

@Data
public class UserDTO {
   private String name;

   private int age;

   private Date birthday;

   //1-男 0-女
   private int gender;

   private String idCard;

   private String phoneNumber;

   private String address;

   private Boolean isMarried;
}
@Data
public class UserInfoVO {
   private String userName;

   private int age;

   private Date birthday;

   //1-男 0-女
   private int gender;

   private String idCard;

   private String phoneNumber;

   private String address;

   private Boolean isMarried;
}

紧接着需要编写相应的mapper类,以便生成相应的编译类。

@Mapper
public interface InfoConverter {

   InfoConverter INSTANT = Mappers.getMapper(InfoConverter.class);

   @Mappings({
           @Mapping(source = "name", target = "userName")
  })
   UserInfoVO convert(UserDTO userDto);
}

需要注意的是,因为DTO中的name对应的其实是VO中的userName。因此需要在converter中显式声明。在编写完对应的文件之后,需要执行maven的complie命令使得IDE编译生成对应的Impl对象。(自动生成)

image-20220526161736140.png

到此,mapperStruct的接入就算是完成了~。我们就可以在我们的代码中使用这个拷贝类了。

public UserInfoVO newCopyItem(UserDTO userDTO, int times) {
   UserInfoVO userInfoVO = new UserInfoVO();
   userInfoVO = InfoConverter.INSTANT.convert(userDTO);
   return userInfoVO;
}

怎么样,接入是不是很简单~

FAQ

1、接入项目时,发现并没有生成对应的编译对象class,这个是什么原因?

答:可能的原因有如下几个:

  • 忘记编写对应的@Mapper注解,因而没有生成

  • 没有配置上述提及的插件maven-compiler-plugin

  • 没有执行maven的Compile,IDE没有进行相应编译

2、接入项目后发现,我项目内的Lombok、@Data注解不好使了,这怎么办呢?

由于Lombok本身是对AST进行修改实现的,但是mapStruct在执行的时候并不能检测到Lombok所做的修改,因此需要额外的引入maven依赖lombok-mapstruct-binding

......
   <org.mapstruct.version>1.5.0.RC1</org.mapstruct.version>
   <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
   <lombok.version>1.18.20</lombok.version>
......

......
<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct</artifactId>
   <version>${org.mapstruct.version}</version>
</dependency>
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok-mapstruct-binding</artifactId>
   <version>${lombok-mapstruct-binding.version}</version>
</dependency>
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <version>${lombok.version}</version>
</dependency>

更详细的,mapperStruct在官网中还提供了一个实现Lombok及mapStruct同时并存的案例

3、更多问题:

欢迎查看MapStruct官网文档,里面对各种问题都有更详细的解释及解答。

实现原理

在聊到mapstruct的实现原理之前,我们就需要先回忆一下JAVA代码运行的过程。大致的执行生成的流程如下所示:

image-20220529181541401.png 可以直观的看到,如果我们想不通过编码的方式对程序进行修改增强,可以考虑对抽象语法树进行相应的修改。而mapstruct也正是如此做的。具体的执行逻辑如下所示:

image-20220529181953035.png

为了实现该方法,mapstruct基于JSR 269实现了代码。JSR 269是JDK引进的一种规范。有了它,能够在编译期处理注解,并且读取、修改和添加抽象语法树中的内容。JSR 269使用Annotation Processor在编译期间处理注解,Annotation Processor相当于编译器的一种插件,因此又称为插入式注解处理。想要实现JSR 269,主要有以下几个步骤:

  1. 继承AbstractProcessor类,并且重写process方法,在process方法中实现自己的注解处理逻辑。

  2. 在META-INF/services目录下创建javax.annotation.processing.Processor文件注册自己实现的Annotation Processor。

通过实现AbstractProcessor,在程序进行compile的时候,会对相应的AST进行修改。从而达到目的。

public void compile(List<JavaFileObject> sourceFileObjects,
                   List<String> classnames,
                   Iterable<? extends Processor> processors)
{
   if (processors != null && processors.iterator().hasNext())
       explicitAnnotationProcessingRequested = true;
   // as a JavaCompiler can only be used once, throw an exception if
   // it has been used before.
   if (hasBeenUsed)
       throw new AssertionError("attempt to reuse JavaCompiler");
   hasBeenUsed = true;

   // forcibly set the equivalent of -Xlint:-options, so that no further
   // warnings about command line options are generated from this point on
   options.put(XLINT_CUSTOM.text + "-" + LintCategory.OPTIONS.option, "true");
   options.remove(XLINT_CUSTOM.text + LintCategory.OPTIONS.option);

   start_msec = now();

   try {
       initProcessAnnotations(processors);

       //此处会调用到mapStruct中的processor类的方法.
       delegateCompiler =
           processAnnotations(
               enterTrees(stopIfError(CompileState.PARSE, parseFiles(sourceFileObjects))),
               classnames);

       delegateCompiler.compile2();
       delegateCompiler.close();
       elapsed_msec = delegateCompiler.elapsed_msec;
  } catch (Abort ex) {
       if (devVerbose)
           ex.printStackTrace(System.err);
  } finally {
       if (procEnvImpl != null)
           procEnvImpl.close();
  }
}

关键代码,在mapstruct-processor包中,有个对应的类MappingProcessor继承了AbstractProcessor,并实现其process方法。通过对AST进行相应的代码增强,从而实现对最终编译的对象进行修改的方法。

@SupportedAnnotationTypes({"org.mapstruct.Mapper"})
@SupportedOptions({"mapstruct.suppressGeneratorTimestamp", "mapstruct.suppressGeneratorVersionInfoComment", "mapstruct.unmappedTargetPolicy", "mapstruct.unmappedSourcePolicy", "mapstruct.defaultComponentModel", "mapstruct.defaultInjectionStrategy", "mapstruct.disableBuilders", "mapstruct.verbose"})
public class MappingProcessor extends AbstractProcessor {
   public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
       if (!roundEnvironment.processingOver()) {
           RoundContext roundContext = new RoundContext(this.annotationProcessorContext);
           Set<TypeElement> deferredMappers = this.getAndResetDeferredMappers();
           this.processMapperElements(deferredMappers, roundContext);
           Set<TypeElement> mappers = this.getMappers(annotations, roundEnvironment);
           this.processMapperElements(mappers, roundContext);
      } else if (!this.deferredMappers.isEmpty()) {
           Iterator var8 = this.deferredMappers.iterator();

           while(var8.hasNext()) {
               MappingProcessor.DeferredMapper deferredMapper = (MappingProcessor.DeferredMapper)var8.next();
               TypeElement deferredMapperElement = deferredMapper.deferredMapperElement;
               Element erroneousElement = deferredMapper.erroneousElement;
               String erroneousElementName;
               if (erroneousElement instanceof QualifiedNameable) {
                   erroneousElementName = ((QualifiedNameable)erroneousElement).getQualifiedName().toString();
              } else {
                   erroneousElementName = erroneousElement != null ? erroneousElement.getSimpleName().toString() : null;
              }

               deferredMapperElement = this.annotationProcessorContext.getElementUtils().getTypeElement(deferredMapperElement.getQualifiedName());
               this.processingEnv.getMessager().printMessage(Kind.ERROR, "No implementation was created for " + deferredMapperElement.getSimpleName() + " due to having a problem in the erroneous element " + erroneousElementName + ". Hint: this often means that some other annotation processor was supposed to process the erroneous element. You can also enable MapStruct verbose mode by setting -Amapstruct.verbose=true as a compilation argument.", deferredMapperElement);
          }
      }

       return false;
  }
}

如何断点调试:

因为这个注解处理器是在解析->编译的过程完成,跟普通的jar包调试不太一样,maven框架为我们提供了调试入口,需要借助maven才能实现debug。所以需要在编译过程打开debug才可调试。

  • 在项目的pom文件所在目录执行mvnDebug compile

  • 接着用idea打开项目,添加一个remote,端口为8000

  • 打上断点,debug 运行remote即可调试。

image-20220529194616314.png

附录

测试代码如下,采用Spock框架 + JAVA代码实现。Spock框架作为当前最火热的测试框架,你值得学习一下。 Spock框架初体验:更优雅地写好你的单元测试

//    @Resource
   @Shared
   MapperStructService mapperStructService

   def setupSpec() {
       mapperStructService = new MapperStructService()
  }

   @Unroll
   def "test mapperStructTest times = #times"() {
       given: "初始化数据"
       UserDTO dto = new UserDTO(name: "笑傲菌", age: 20, idCard: "1234",
               phoneNumber: "18211932334", address: "北京天安门", gender: 1,
               birthday: new Date(), isMarried: false)

       when: "调用方法"
//       传统的getter、setter拷贝
       long startTime = System.nanoTime();
       UserInfoVO oldRes = mapperStructService.originalCopyItem(dto, times)
       Duration originalWasteTime = Duration.ofNanos(System.nanoTime() - startTime);

//       采用工具实现反射类的拷贝
       long startTime1 = System.nanoTime();
       UserInfoVO utilRes = mapperStructService.utilCopyItem(dto, times)
       Duration utilWasteTime = Duration.ofNanos(System.nanoTime() - startTime1);

       long startTime2 = System.nanoTime();
       UserInfoVO mapStructRes = mapperStructService.newCopyItem(dto, times)
       Duration mapStructWasteTime = Duration.ofNanos(System.nanoTime() - startTime2);

       then: "校验数据"
       println("times = "+ times)
       println("原始拷贝的消耗时间为: " + originalWasteTime.getNano())
       println("BeanUtils拷贝的消耗时间为: " + utilWasteTime.getNano())
       println("mapStruct拷贝的消耗时间为: " + mapStructWasteTime.getNano())
       println()

       where: "比较不同次数调用的耗时"
       times || ignore
       1     || null
       10    || null
       100   || null
       1000  || null
  }

测试的Service如下所示:

public class MapperStructService {

   public UserInfoVO newCopyItem(UserDTO userDTO, int times) {
       UserInfoVO userInfoVO = new UserInfoVO();
       for (int i = 0; i < times; i++) {
           userInfoVO = InfoConverter.INSTANT.convert(userDTO);
      }
       return userInfoVO;
  }

   public UserInfoVO originalCopyItem(UserDTO userDTO, int times) {
       UserInfoVO userInfoVO = new UserInfoVO();
       for (int i = 0; i < times; i++) {
           userInfoVO.setUserName(userDTO.getName());
           userInfoVO.setAge(userDTO.getAge());
           userInfoVO.setBirthday(userDTO.getBirthday());
           userInfoVO.setIdCard(userDTO.getIdCard());
           userInfoVO.setGender(userDTO.getGender());
           userInfoVO.setIsMarried(userDTO.getIsMarried());
           userInfoVO.setPhoneNumber(userDTO.getPhoneNumber());
           userInfoVO.setAddress(userDTO.getAddress());
      }
       return userInfoVO;
  }

   public UserInfoVO utilCopyItem(UserDTO userDTO, int times) {
       UserInfoVO userInfoVO = new UserInfoVO();
       for (int i = 0; i < times; i++) {
           BeanUtils.copyProperties(userDTO, userInfoVO);
      }
       return userInfoVO;
  }
}

参考文献

踩坑BeanUtils.copy**()导致的业务处理速度过慢

mapstruct原理解析

MapStruct官网

Mapstruct源码解析- 框架实现原理

作者:DrLauPen
来源:https://juejin.cn/post/7103135968256851976

收起阅读 »

跟我学企业级flutter项目:如何重新定制cached_network_image的缓存管理与Dio网络请求

前言 flutter中需要展示网络图片时候,不建议使用flutter原本Image.network(),建议最好还是采用cached_network_image这个三方库。那么我今天就按照它来展开说明,我再做企业级项目时如何重新定制cached_network...
继续阅读 »

前言


flutter中需要展示网络图片时候,不建议使用flutter原本Image.network(),建议最好还是采用cached_network_image这个三方库。那么我今天就按照它来展开说明,我再做企业级项目时如何重新定制cached_network_image。


由于我的项目网络请求采用Dio库,所以我希望我的图片库也采用Dio来网络请求,也是为了方便请求日志打印(在做APM监控时候可以看到网络请求状态,方便定位问题)。


前期准备


准备好mime_converter类,由于cached_network_image中的manager这个文件不是export的状态,那么我们需要准备好该类,以便我们自己实现网络请求修改。


实现mime_converter


创建mime_converter 类,代码如下:


import 'dart:io';

///将最常见的MIME类型转换为最期望的文件扩展名。
extension ContentTypeConverter on ContentType {
String get fileExtension => mimeTypes[mimeType] ?? '.$subType';
}

///MIME类型的来源:
/// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
///2020年3月20日时更新
const mimeTypes = {
'application/vnd.android.package-archive': '.apk',
'application/epub+zip': '.epub',
'application/gzip': '.gz',
'application/java-archive': '.jar',
'application/json': '.json',
'application/ld+json': '.jsonld',
'application/msword': '.doc',
'application/octet-stream': '.bin',
'application/ogg': '.ogx',
'application/pdf': '.pdf',
'application/php': '.php',
'application/rtf': '.rtf',
'application/vnd.amazon.ebook': '.azw',
'application/vnd.apple.installer+xml': '.mpkg',
'application/vnd.mozilla.xul+xml': '.xul',
'application/vnd.ms-excel': '.xls',
'application/vnd.ms-fontobject': '.eot',
'application/vnd.ms-powerpoint': '.ppt',
'application/vnd.oasis.opendocument.presentation': '.odp',
'application/vnd.oasis.opendocument.spreadsheet': '.ods',
'application/vnd.oasis.opendocument.text': '.odt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation':
'.pptx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
'.docx',
'application/vnd.rar': '.rar',
'application/vnd.visio': '.vsd',
'application/x-7z-compressed': '.7z',
'application/x-abiword': '.abw',
'application/x-bzip': '.bz',
'application/x-bzip2': '.bz2',
'application/x-csh': '.csh',
'application/x-freearc': '.arc',
'application/x-sh': '.sh',
'application/x-shockwave-flash': '.swf',
'application/x-tar': '.tar',
'application/xhtml+xml': '.xhtml',
'application/xml': '.xml',
'application/zip': '.zip',
'audio/3gpp': '.3gp',
'audio/3gpp2': '.3g2',
'audio/aac': '.aac',
'audio/x-aac': '.aac',
'audio/midi audio/x-midi': '.midi',
'audio/mpeg': '.mp3',
'audio/ogg': '.oga',
'audio/opus': '.opus',
'audio/wav': '.wav',
'audio/webm': '.weba',
'font/otf': '.otf',
'font/ttf': '.ttf',
'font/woff': '.woff',
'font/woff2': '.woff2',
'image/bmp': '.bmp',
'image/gif': '.gif',
'image/jpeg': '.jpg',
'image/png': '.png',
'image/svg+xml': '.svg',
'image/tiff': '.tiff',
'image/vnd.microsoft.icon': '.ico',
'image/webp': '.webp',
'text/calendar': '.ics',
'text/css': '.css',
'text/csv': '.csv',
'text/html': '.html',
'text/javascript': '.js',
'text/plain': '.txt',
'text/xml': '.xml',
'video/3gpp': '.3gp',
'video/3gpp2': '.3g2',
'video/mp2t': '.ts',
'video/mpeg': '.mpeg',
'video/ogg': '.ogv',
'video/webm': '.webm',
'video/x-msvideo': '.avi',
'video/quicktime': '.mov'
};

实现FileServiceResponse


FileServiceResponse是数据处理的关键,那么我们来实现该类



class DioGetResponse implements FileServiceResponse {
DioGetResponse(this._response);

final DateTime _receivedTime = clock.now();

final Response<ResponseBody> _response;

@override
int get statusCode => _response.statusCode!;


@override
Stream<List<int>> get content => _response.data!.stream;

@override
int? get contentLength => _getContentLength();

int _getContentLength() {
try {
return int.parse(
_header(HttpHeaders.contentLengthHeader) ?? '-1');
} catch (e) {
return -1;
}
}

String? _header(String name) {
return _response.headers[name]?.first;
}


@override
DateTime get validTill {
// Without a cache-control header we keep the file for a week

var ageDuration = const Duration(days: 7);
final controlHeader = _header(HttpHeaders.cacheControlHeader);
if (controlHeader != null) {
final controlSettings = controlHeader.split(',');
for (final setting in controlSettings) {
final sanitizedSetting = setting.trim().toLowerCase();
if (sanitizedSetting == 'no-cache') {
ageDuration = const Duration();
}
if (sanitizedSetting.startsWith('max-age=')) {
var validSeconds = int.tryParse(sanitizedSetting.split('=')[1]) ?? 0;
if (validSeconds > 0) {
ageDuration = Duration(seconds: validSeconds);
}
}
}
}

return _receivedTime.add(ageDuration);
}

@override
String? get eTag => _header(HttpHeaders.etagHeader);

@override
String get fileExtension {
var fileExtension = '';
final contentTypeHeader = _header(HttpHeaders.contentTypeHeader);
if (contentTypeHeader != null) {
final contentType = ContentType.parse(contentTypeHeader);
fileExtension = contentType.fileExtension;
}
return fileExtension;
}
}

实现FileService


实现FileService 参数为dio


class DioHttpFileService extends FileService {
final Dio _dio;

DioHttpFileService(this._dio);

@override
Future<FileServiceResponse> get(String url, {Map<String, String>? headers}) async {
Options options = Options(headers: headers ?? {}, responseType: ResponseType.stream);
Response<ResponseBody> httpResponse = await _dio.get<ResponseBody>(url, options: options);
return DioGetResponse(httpResponse);
}
}

制定框架缓存管理器


我在项目中,设定了缓存配置最多缓存 100 个文件,并且每个文件只应缓存 7天,如果需要使用日志拦截器的话,就在拦截器中增加日志拦截:


class LibCacheManager {
static const key = 'libCacheKey';

///缓存配置 {最多缓存 100 个文件,并且每个文件只应缓存 7天}
static CacheManager instance = CacheManager(
Config(
key,
stalePeriod: const Duration(days: 7),
maxNrOfCacheObjects: 100,
fileService : DioHttpFileService(Dio()))
),
);

}

项目中使用


使用如下


CachedNetworkImage(imageUrl: "https://t8.baidu.com/it/u=3845489932,4046458829&fm=74&app=80&size=f256,256&n=0&f=JPEG&fmt=auto?sec=1654102800&t=f6de842e1e7086ffc73536795d37fd2c",
cacheManager: LibCacheManager.instance,
width: 100,
height: 100,
placeholder: (context, url) => ImgPlaceHolder(),
errorWidget: (context, url, error) => ImgError(),
);

如上便是 如何重新定制cached_network_image的缓存管理与Dio网络请求

收起阅读 »

微前端乾坤使用过程中的坑

微前端乾坤使用过程中的坑乾坤在启动子应用的时候默认开启沙箱模式{sandbox: true},这样的情况下,乾坤节点下会生成一个 shadow dom,shadow dom 内的样式与外部样式是没有关联的,这样就会给子应用内的样式带来一系列问题。这其...
继续阅读 »

微前端乾坤使用过程中的坑

乾坤在启动子应用的时候默认开启沙箱模式{sandbox: true},这样的情况下,乾坤节点下会生成一个 shadow dom,shadow dom 内的样式与外部样式是没有关联的,这样就会给子应用内的样式带来一系列问题。这其中很多问题并不是乾坤造成的,而是 shadow dom 本身的特性导致的,乾坤还是不错的(不背锅)。随时补充

1.iconffont 字体在子应用无法加载

原因:shadow dom 是不支持@font-face 的,所以当引入 iconfont 的时候,尽管可以引入样式,但由于字体文件是不存在的,所以相对应的图标也无法展示。相关链接:@font-face doesn't work with Shadow DOM?Icon Fonts in Shadow DOM

方案:

  1. 把字体文件放在主应用加载
  2. 使用通用的字体文件,这样就不需要单独加载字体文件了(等于没说~

2.dom的查询方法找不到指定的元素

原因:shadow dom 内的元素是被隔离的元素,故 document下查询的方法例如,querySelector、getElementsById 等是获取不到 shadow dom 内元素的。

方案:代理 document 下各个查询元素的方法,使用子应用外面的 shadow dom 一层查询。如何获取子应用dom对象可以参考乾坤的这个方法 initGlobalState

3.组件库动态创建的元素无法使用自己的样式

原因:有些对话框或提示窗是通过document.body.appendChild添加的,所以 shadow dom 内引入的 CSS 是无法作用到外面元素的。方案:代理document.body.appendChild方法,即把新加的元素添加到 shadow dom容器下,而不是最外面的 body节点下。

补充:类似的问题都可以往这个方向靠,看是不是shadow dom节点或者dom方法的问题。

4.第三方引入的 JS 不生效

原因:有些 JS 文件本身是个立即执行函数,或者会动态的创建 scipt 标签,但是所有获取资源的请求是被乾坤劫持处理,所以都不会正常执行,也不会在 window 下面挂载相应的变量,自然在取值调用的时候也不存在这个变量。方案:参考乾坤的 issue,子应用向body添加script标签失败

5.webpack-dev-server 代理访问的接口 cookie 丢失

原因:在主应用的端口下请求子应用的端口,存在跨域,axios 默认情况下跨域是不携带 cookie 的,假如把 axios 的 withCredential设置为 true(表示跨域携带 cookie),那么子应用需要设置跨域访问头Access-Control-Allow-Origin(在 devServer 下配置 header)为指定的域名,但不能设置为*,这时候同时存在主应用和子应用端口发出的请求,而跨域访问头只能设置一个地址,就导致无法代理指定服务器接口。

方案:子应用接口请求的端口使用主应用接口请求的端口,使用主应用的配置代理请求

// 主应用

devServer:
{
...
port: 9600
proxy: {
// 代理配置
}
}

// 子应用
devServer: {
...
port: 9600, // 使用主应用的页面访问端口
}

原文:https://segmentfault.com/a/1190000037641251


收起阅读 »

Vue + qiankun 快速实现前端微服务

什么是微前端Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -...
继续阅读 »

什么是微前端

Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. -- Micro Frontends

微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。

qiankun

qiankun 是蚂蚁金服开源的一套完整的微前端解决方案。具体描述可查看 文档 和 Github

下面将通过一个微服务Demo 介绍 Vue 项目如何接入 qiankun,代码地址:micro-front-vue)

二、配置主应用

  1. 使用 vue cli 快速创建主应用;
  2. 安装 qiankun
$ yarn add qiankun # 或者 npm i qiankun -S
  1. 调整主应用 main.js 文件:具体如下:
import Vue from "vue"
import App from "./App.vue"
import router from "./router"

import { registerMicroApps, setDefaultMountApp, start } from "qiankun"
Vue.config.productionTip = false
let app = null;
/**
* 渲染函数
* appContent 子应用html内容
* loading 子应用加载效果,可选
*/
function render({ appContent, loading } = {}) {
if (!app) {
app = new Vue({
el: "#container",
router,
data() {
return {
content: appContent,
loading
};
},
render(h) {
return h(App, {
props: {
content: this.content,
loading: this.loading
}
});
}
});
} else {
app.content = appContent;
app.loading = loading;
}
}

/**
* 路由监听
* @param {*} routerPrefix 前缀
*/
function genActiveRule(routerPrefix) {
return location => location.pathname.startsWith(routerPrefix);
}

function initApp() {
render({ appContent: '', loading: true });
}

initApp();

// 传入子应用的数据
let msg = {
data: {
auth: false
},
fns: [
{
name: "_LOGIN",
_LOGIN(data) {
console.log(`父应用返回信息${data}`);
}
}
]
};
// 注册子应用
registerMicroApps(
[
{
name: "sub-app-1",
entry: "//localhost:8091",
render,
activeRule: genActiveRule("/app1"),
props: msg
},
{
name: "sub-app-2",
entry: "//localhost:8092",
render,
activeRule: genActiveRule("/app2"),
}
],
{
beforeLoad: [
app => {
console.log("before load", app);
}
], // 挂载前回调
beforeMount: [
app => {
console.log("before mount", app);
}
], // 挂载后回调
afterUnmount: [
app => {
console.log("after unload", app);
}
] // 卸载后回调
}
);

// 设置默认子应用,与 genActiveRule中的参数保持一致
setDefaultMountApp("/app1");

// 启动
start();
  1. 修改主应用 index.html 中绑定的 id ,需与 el  绑定 dom 为一致;
  2. 调整 App.vue 文件,增加渲染子应用的盒子:
<template>
<div id="main-root">
<!-- loading -->
<div v-if="loading">loading</div>
<!-- 子应用盒子 -->
<div id="root-view" class="app-view-box" v-html="content"></div>
</div>
</template>

<script>
export default {
name: "App",
props: {
loading: Boolean,
content: String
}
};
</script>
  1. 创建 vue.config.js 文件,设置 port :
module.exports = {
devServer: {
port: 8090
}
}

三、配置子应用

  1. 在主应用同一级目录下快速创建子应用,子应用无需安装 qiankun
  2. 配置子应用 main.js:
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';
import routes from './router';
import './public-path';

Vue.config.productionTip = false;

let router = null;
let instance = null;

function render() {
router = new VueRouter({
base: window.__POWERED_BY_QIANKUN__ ? '/app1' : '/',
mode: 'history',
routes,
});

instance = new Vue({
router,
render: h => h(App),
}).$mount('#app');
}

if (!window.__POWERED_BY_QIANKUN__) {
render();
}

export async function bootstrap() {
console.log('vue app bootstraped');
}

export async function mount(props) {
console.log('props from main app', props);
render();
}

export async function unmount() {
instance.$destroy();
instance = null;
router = null;
}
  1. 配置 vue.config.js
const path = require('path');
const { name } = require('./package');

function resolve(dir) {
return path.join(__dirname, dir);
}

const port = 8091; // dev port

module.exports = {
/**
* You will need to set publicPath if you plan to deploy your site under a sub path,
* for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
* then publicPath should be set to "/bar/".
* In most cases please use '/' !!!
* Detail: https://cli.vuejs.org/config/#publicpath
*/
outputDir: 'dist',
assetsDir: 'static',
filenameHashing: true,
// tweak internal webpack configuration.
// see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
devServer: {
// host: '0.0.0.0',
hot: true,
disableHostCheck: true,
port,
overlay: {
warnings: false,
errors: true,
},
headers: {
'Access-Control-Allow-Origin': '*',
},
},
// 自定义webpack配置
configureWebpack: {
resolve: {
alias: {
'@': resolve('src'),
},
},
output: {
// 把子应用打包成 umd 库格式
library: `${name}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${name}`,
},
},
};

其中有个需要注意的点:

  1. 子应用必须支持跨域:由于 qiankun 是通过 fetch 去获取子应用的引入的静态资源的,所以必须要求这些静态资源支持跨域;
  2. 使用 webpack 静态 publicPath 配置:可以通过两种方式设置,一种是直接在 mian.js 中引入 public-path.js 文件,一种是在开发环境直接修改 vue.config.js:
{
output: {
publicPath: `//localhost:${port}`;
}
}

public-path.js 内容如下:

if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line no-undef
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__
}
至此,Vue 项目的前端微服务已经简单完成了。

但是在实际的开发过程中,并非如此简单,同时还存在应用间跳转、应用间通信等问题。


原文:https://segmentfault.com/a/1190000021872481


收起阅读 »

可部署于windows和Linux的即时通讯系统

系统概况信贸通即时通讯系统,一款跨平台可定制的 P2P 即时通信系统,为电子商务网站及各行业门户网站和企事业单位提供“一站式”定制解决方案,打造一个稳定,安全,高效,可扩展的即时通信系统,支持在线聊天、视频/语音对话、点对点断点续传文件、自定义皮肤等。软件能真...
继续阅读 »

系统概况
信贸通即时通讯系统,一款跨平台可定制的 P2P 即时通信系统,为电子商务网站及各行业门户网站和企事业单位提供“一站式”定制解决方案,打造一个稳定,安全,高效,可扩展的即时通信系统,支持在线聊天、视频/语音对话、点对点断点续传文件、自定义皮肤等。软件能真正无缝与电子商务网站整合,有效提高工作效率,节约成本。同时可根据用户的需求进行二次开发,并提供与其他软件整合或嵌入方案

系统架构
自研协议独立开发,采用高并发go语言开发的即时通讯及历史消息云存储通信系统。系统安全性高可扩展能力强,系统兼容性好。可快速无缝集成到各种应用系统,有效提高开发效率,节约成本。能轻松在线定制客户端。支持多平台客户端实现多端与多设备同步。

私有部署
整个系统部署在您自己的服务器上,可以部署在公网也可以部署在内网中,支持Windows服务和Linux服务器,硬件要求低(主流服务器和云服务器均可运行)。系统独立运行,完全自主管理和监控,最大程度上保障数据安全,避免信息泄露,安全性更高,带来更多的便捷和保障。

定制开发
可根据客户的需求量身定制符合客户实际应用的即时通聊天软件,可控性强、易扩展,系统集成度高。可以快速进行二次开发,简单方便来进行定制管理。

客户端 / 功能
支持windows,安卓,ios,主流浏览器,功能单聊,群聊,消息互通,朋友圈等主流功能,安全可靠。


http://www.51dn.top/wp-content/uploads/2022/04/QQ截图20220407104051-624x315.jpg 624w, http://www.51dn.top/wp-content/uploads/2022/04/QQ截图20220407104051.jpg 671w" sizes="(max-width: 500px) 100vw, 500px" style="vertical-align: baseline; border-radius: 3px; box-shadow: rgba(0, 0, 0, 0.2) 0px 1px 4px;">


收起阅读 »

使用自定义url发图片的坑

发送URL图片消息 App端需要开发者自己实现下载,Web端需要在 WebIMConfig.js中 设置 useOwnUploadFun: true。实际上还得在WEBIM里面再配置一下WebIM.conn = new WebIM.connect...
继续阅读 »


发送URL图片消息





App端需要开发者自己实现下载,Web端需要在 WebIMConfig.js中 设置 useOwnUploadFun: true

实际上还得在WEBIM里面再配置一下

WebIM.conn = new WebIM.connection({
appKey: WebIM.config.appkey,
isMultiLoginSessions: WebIM.config.isMultiLoginSessions,
https: typeof WebIM.config.https === "boolean" ? WebIM.config.https : location.protocol === "https:",
url: WebIM.config.xmppURL,
apiUrl: WebIM.config.apiURL,
isAutoLogin: false,
heartBeatWait: WebIM.config.heartBeatWait,
autoReconnectNumMax: WebIM.config.autoReconnectNumMax,
autoReconnectInterval: WebIM.config.autoReconnectInterval,
useOwnUploadFun: WebIM.config.useOwnUploadFun,
isDebug: false,
isHttpDNS:false
});







单聊通过URL发送图片消息的代码示例如下:


// 单聊通过URL发送图片消息
var sendPrivateUrlImg = function () {
var id = conn.getUniqueId(); // 生成本地消息id
var msg = new WebIM.message('img', id); // 创建图片消息
var option = {
body: {
type: 'file',
url: url,
size: {
width: msg.width,
height: msg.height,
},
length: msg.length,
filename: msg.file.filename,
filetype: msg.filetype
},
to: 'username', // 接收消息对象
};
msg.set(option);
conn.send(msg.body);
}
收起阅读 »

探究EventBus粘性事件实现机制

粘性事件观察者 @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun registerEventBus(o: Any) { } 发送粘性事件 EventBus.getDefault...
继续阅读 »
  1. 粘性事件观察者


@Subscribe(threadMode = ThreadMode.MAIN, sticky = true)
fun registerEventBus(o: Any) {
}


  1. 发送粘性事件


EventBus.getDefault().postSticky(Any())


  1. 注册EventBus


EventBus.getDefault().register(this)

接下来我们就来探究下EventBus的粘性事件是如何实现的。


postSticky()内部机制


image.png



  1. 如果是发送的粘性事件,会添加到stickyEvents中,看下这个属性的实现:


image.png

可以看到这个属性是一个Map集合,其中key为事件类型的class对象,value为对应的事件类型。



  1. 继续看下post(Event)方法:


image.png




  1. 首先将这个粘性事件添加到PostingThreadState(线程私有)的eventQueue集合中




  2. 通过isMainThread方法判断当前是否为主线程,最终会调用到我们熟悉的Looper.getMainLooper() == Looper.myLooper()进行判断




  3. 循环遍历eventQueue队列,不断的取出集合元素进行分发,看下postSinleEvent()方法如何实现:




image.png




  1. 如果eventInheritance为true,会查找当前发送的粘性事件类型的父类型,并返回查找到的集合




  2. 接下来就会调用postSingleEventForEventType()方法来进行最终粘性事件的分发,即通知通过@Subscribe注解注册的粘性事件观察者,看下具体实现:




image.png



  1. 调用subscriptionsByEventType获取注册该事件类型的所有订阅方法,但是由于这个时候我们是先发送的粘性事件再注册EventBus,而subscriptionsByEventType中集合元素的填充实在注册EventBus发生的,所以通过subscriptionsByEventType获取到的subscriptions将是null的,所以接下来肯定不会走下面的if代码块中的逻辑了。


postSticky()小结


上面这么多代码逻辑,其实只干了一件事,就是将这个粘性事件添加到了stickyEvents这个集合中。之后的逻辑虽多,但和粘性事件没啥关系。


register内部机制


image.png



  1. findSubscriberMethods()这个方法里面的逻辑就不带大家进行分析了,总之就干了一件事情:



查找当前类通过@Subscribe注册的所有事件订阅方法,并返回一个List<SubscriberMethod>集合,其中SubscriberMethod就是对每个注册的订阅方法和当前注册类的封装




  1. subscribe这个方法是关键,深入探究下:


image.png


image.png




  • 第1、2、3、4步中其实就干了两件事情:



    • 填充subscriptionsByEventType集合,key为事件类型,value为通过@Subscribe订阅了该事件类型的方法集合

    • 填充typesBySubscriber集合,key为注册EventBus的类,value为该类中所有@Subscribe注解修饰的方法集合




  • 第5步就是实现粘性事件分发的关键地方



    • 首先判断当前@Subscribe修饰的订阅方法是否为粘性,即@Subscribe(sticky = true)sticky等于true

    • 是的话就从stickyEvents集合中判断是否存在和订阅方法中注册的事件类型相同的事件:



    这个stickyEvents是不是很熟悉,就是我们之前发送粘性事件时,将粘性事件添加到的方法集合




    • 如果存在,则就执行该粘性事件的分发,即调用执行该订阅方法,最终会调用到invokeSubscriber()方法:




image.png



从上面可以看到,最终是通过反射来实现的订阅了粘性事件方法的执行。



register小结


该方法最终会判断当前是否存在注册EventBus前发送的粘性事件,且当前注册类中存在订阅该事件类型的方法,然后立即执行。


总结


以上就是EventBus粘性事件的内部实现机制,总体来说不算复杂,大家看着文章跟着源码一步步分析应该就很容易理解这部分实现逻辑了。


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

并发编程-线程的启动、死锁、线程安全、ThreadLocal

1 线程的启动方式 线程的启动方式只有两种。 方式1:继承Thread,然后调用start()启动。 private static class PrimeThread extends Thread { @Override public void...
继续阅读 »

1 线程的启动方式


线程的启动方式只有两种。


方式1:继承Thread,然后调用start()启动。


private static class PrimeThread extends Thread {
@Override
public void run() {
System.out.println("thread extend Thread---name:" + Thread.currentThread().getName());
}
}

PrimeThread thread = new PrimeThread();
thread.start();

方式2:实现Runnable,然后交给Thread去启动。


private static class PrimeRunnable implements Runnable {
@Override
public void run() {
System.out.println("thread implements Runnable---name:" + Thread.currentThread().getName());
}
}

PrimeRunnable runnable = new PrimeRunnable();
new Thread(runnable).start();

其他的比如线程池、FutureTask等都属于这两种的包装或封装。


并且Thread源码的注释中也清楚的写了有两种方式创建线程:


* <p>
* There are two ways to create a new thread of execution. One is to
* declare a class to be a subclass of <code>Thread</code>. This
* subclass should override the <code>run</code> method of class
* <code>Thread</code>. An instance of the subclass can then be
* allocated and started. For example, a thread that computes primes
* larger than a stated value could be written as follows:
* <hr><blockquote><pre>
* class PrimeThread extends Thread {
* long minPrime;
* PrimeThread(long minPrime) {
* this.minPrime = minPrime;
* }
*
* public void run() {
* // compute primes larger than minPrime
* &nbsp;.&nbsp;.&nbsp;.
* }
* }
* </pre></blockquote><hr>
* <p>
* The following code would then create a thread and start it running:
* <blockquote><pre>
* PrimeThread p = new PrimeThread(143);
* p.start();
* </pre></blockquote>
* <p>
* The other way to create a thread is to declare a class that
* implements the <code>Runnable</code> interface. That class then
* implements the <code>run</code> method. An instance of the class can
* then be allocated, passed as an argument when creating
* <code>Thread</code>, and started. The same example in this other
* style looks like the following:
* <hr><blockquote><pre>
* class PrimeRun implements Runnable {
* long minPrime;
* PrimeRun(long minPrime) {
* this.minPrime = minPrime;
* }
*
* public void run() {
* // compute primes larger than minPrime
* &nbsp;.&nbsp;.&nbsp;.
* }
* }
* </pre></blockquote><hr>
* <p>

2 线程的状态


Java中线程的状态分为6种:


1、初始(NEW):新创建了一个线程,但是还没有调用start()方法。


2、运行(RUNNABLE):Java线程中将就绪(READY)和运行中(RUNNING)两种装填笼统的称为“运行”。


线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法,该状态的线程位于可运行线程池中,获取CPU的使用权,此时处于就绪状态(READY),就绪状态的线程在获得CPU时间片后变为运行中状态(RUNNING)。


3、阻塞(BLOCKED):表示线程阻塞于锁。


4、等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。


5、超时等待(TIMED_WAITING):该状态不同于WATING,它可以在指定的时间后自行返回。


6、终止(TERMINATED):表示该线程已经执行完毕。


线程生命周期如下:



3 死锁


3.1 概念


死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。


死锁是必然发生在多操作者(M>=2个)情况下,争夺多个资源(N>=2个,且N<=M)才会发生这种情况。很明显,单线程自然不会有死锁。


死锁还有几个要求:



  1. 争夺资源的顺序不对,如果争夺资源的顺序是一样的,也不会产生死锁。

  2. 争夺者拿到资源不放手。


3.1.1 学术定义


死锁的发生必须具备以下四个必要条件。



  1. 互斥条件: 指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

  2. 请求和保持条件: 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。

  3. 不剥夺条件: 指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。

  4. 环路等待条件: 指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。


理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。只要打破四个必要条件之一就能有效预防死锁的发生。



  • 打破互斥条件:改造独占性资源为虚拟资源,大部分资源已无法改造。

  • 打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。

  • 打破占有且申请条件:采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。

  • 打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。


避免死锁常见的算法有有序资源分配法、银行家算法。


示例代码:


/**
* @Description: 死锁的产生
* @CreateDate: 2022/3/15 2:31 下午
*/
public class NormalDeadLock {

/**
* 第1个锁
*/
private static final Object LOCK_1 = new Object();
/**
* 第2个锁
*/
private static final Object LOCK_2 = new Object();

/**
* 第1个拿锁的方法 先去拿锁1,再去拿锁2
*
* @throws InterruptedException 中断异常
*/
private static void method1() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (LOCK_1) {
System.out.println(threadName + " get LOCK_1");
Thread.sleep(100);
synchronized (LOCK_2) {
System.out.println(threadName + " get LOCK_2");
}
}
}

/**
* 第2个拿锁的方法 先去拿锁2,再去拿锁1,这就导致方法1和方法2各拿一个锁,然后互不相让,都不释放自己的锁,造成了互斥,就产生了死锁
*
* @throws InterruptedException 中断异常
*/
private static void method2() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (LOCK_2) {
System.out.println(threadName + " get LOCK_2");
Thread.sleep(100);
synchronized (LOCK_1) {
System.out.println(threadName + " get LOCK_1");
}
}
}

/**
* 子线程PrimeThread1
*/
private static class PrimeThread1 extends Thread {
private final String name;

public PrimeThread1(String name) {
this.name = name;
}

@Override
public void run() {
Thread.currentThread().setName(name);
try {
method1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

/**
* 子线程PrimeThread2
*/
private static class PrimeThread2 extends Thread {
private final String name;

public PrimeThread2(String name) {
this.name = name;
}

@Override
public void run() {
Thread.currentThread().setName(name);
try {
method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
PrimeThread1 thread1 = new PrimeThread1("PrimeThread1");
PrimeThread2 thread2 = new PrimeThread2("PrimeThread2");

thread1.start();
thread2.start();
}

}

执行后,可以看到控制台没有结束运行,看不到Process finished with exit code 0,但是又一直处于静止状态。


PrimeThread1 get LOCK_1
PrimeThread2 get LOCK_2

3.2 危害



  1. 线程不工作了,但是整个程序还是活着的。

  2. 没有任何的异常信息可以供我们检查。

  3. 一旦程序发生了发生了死锁,是没有任何的办法恢复的,只能重启程序,对正式已发布程序来说,这是个很严重的问题。


3.3 解决方案


关键是保证拿锁的顺序一致。


两种解决方式:


1、内部通过顺序比较,确定拿锁的顺序。


比如上述示例代码中,可以让方法1和方法2同时都先拿锁1,然后再去拿锁2,就能解决死锁问题。


private static void method1() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (LOCK_1) {
System.out.println(threadName + " get LOCK_1");
Thread.sleep(100);
synchronized (LOCK_2) {
System.out.println(threadName + " get LOCK_2");
}
}
}

private static void method2() throws InterruptedException {
String threadName = Thread.currentThread().getName();
synchronized (LOCK_1) {
System.out.println(threadName + " get LOCK_1");
Thread.sleep(100);
synchronized (LOCK_2) {
System.out.println(threadName + " get LOCK_2");
}
}
}

修改后后,可以看到程序能正常执行。


PrimeThread1 get LOCK_1
PrimeThread1 get LOCK_2
PrimeThread2 get LOCK_1
PrimeThread2 get LOCK_2

Process finished with exit code 0

2、采用尝试拿锁的机制。


示例代码:


/**
* @Description: 尝试拿锁,解决死锁问题
* @CreateDate: 2022/3/15 2:57 下午
*/
public class TryGetLock {
/**
* 第1个锁
*/
private static final Lock LOCK_1 = new ReentrantLock();
/**
* 第2个锁
*/
private static final Lock LOCK_2 = new ReentrantLock();

/**
* 方法1 先尝试拿锁1,再尝试拿锁2,拿不到锁2的话连同锁1一起释放
*
* @throws InterruptedException 中断异常
*/
private static void method1() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random r = new Random();
while (true) {
if (LOCK_1.tryLock()) {
System.out.println(threadName + " get LOCK_1");
try {
if (LOCK_2.tryLock()) {
try {
System.out.println(threadName + " get LOCK_2");
System.out.println("method1 do working...");
break;
} finally {
LOCK_2.unlock();
}
}
} finally {
LOCK_1.unlock();
}
}
//注意:这里需要给个很短的间隔时间去让其他线程拿锁,不然可能会造成活锁
Thread.sleep(r.nextInt(3));
}
}

/**
* 方法2 先尝试拿锁2,再尝试拿锁1,拿不到锁1的话连同锁2一起释放
*
* @throws InterruptedException 中断异常
*/
private static void method2() throws InterruptedException {
String threadName = Thread.currentThread().getName();
Random r = new Random();
while (true) {
if (LOCK_2.tryLock()) {
System.out.println(threadName + " get LOCK_2");
try {
if (LOCK_1.tryLock()) {
try {
System.out.println(threadName + " get LOCK_1");
System.out.println("method2 do working...");
break;
} finally {
LOCK_1.unlock();
}
}
} finally {
LOCK_2.unlock();
}
}
Thread.sleep(r.nextInt(3));
}
}

private static class PrimeThread1 extends Thread {
private final String name;

public PrimeThread1(String name) {
this.name = name;
}

@Override
public void run() {
Thread.currentThread().setName(name);
try {
method1();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

private static class PrimeThread2 extends Thread {
private final String name;

public PrimeThread2(String name) {
this.name = name;
}

@Override
public void run() {
Thread.currentThread().setName(name);
try {
method2();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
PrimeThread1 thread1 = new PrimeThread1("PrimeThread1");
PrimeThread2 thread2 = new PrimeThread2("PrimeThread2");

thread1.start();
thread2.start();
}
}

执行结果:


PrimeThread2 get LOCK_2
PrimeThread1 get LOCK_1
PrimeThread2 get LOCK_2
PrimeThread2 get LOCK_1
method2 do working...
PrimeThread1 get LOCK_1
PrimeThread1 get LOCK_2
method1 do working...

Process finished with exit code 0

4 其他线程安全问题


4.1 活锁


两个线程在尝试拿锁的机制中,发生多个线程之间互相谦让,不断发生同一个线程总是拿到同一把锁,在尝试拿另一把锁时因为拿不到,而将本来已经持有的锁释放的过程。


解决办法:每个线程休眠随机数,错开拿锁的时间。


如上边的尝试拿锁示例代码中,如果不加随机sleep,就会造成活锁。


4.2 线程饥饿


低优先级的线程,总是拿不到执行时间。


5 ThreadLocal


5.1 与Synchonized的比较


ThreadLocalsynchonized都用于解决多线程并发訪问。但是ThreadLocalsynchronized有本质的差别。synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程访问。而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。


5.2 ThreadLocal的使用


ThreadLocal类接口很简单,只有4个方法:



  • protected T initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

  • public void set(T value)设置当前线程的线程局部变量。

  • public T get()返回当前线程所对应的线程局部变量。

  • public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。


public final static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<String>();

THREAD_LOCAL代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。


示例代码:


/**
* @Description: 使用ThreadLocal
* @CreateDate: 2022/3/15 3:37 下午
*/
public class UseThreadLocal {

private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();

private void startThreadArray() {
Thread[] threads = new Thread[3];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new PrimeRunnable(i));
}
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
}

private static class PrimeRunnable implements Runnable {
private final int id;

public PrimeRunnable(int id) {
this.id = id;
}

@Override
public void run() {
String threadName = Thread.currentThread().getName();
THREAD_LOCAL.set("线程" + id);
System.out.println(threadName + ":" + THREAD_LOCAL.get());
}
}

public static void main(String[] args) {
UseThreadLocal useThreadLocal = new UseThreadLocal();
useThreadLocal.startThreadArray();
}
}

5.3 ThreadLocal的内部实现




    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;
}

ThreadLocal.ThreadLocalMap threadLocals = null;

上面先取到当前线程,然后调用getMap方法获取对应的ThreadLocalMapThreadLocalMapThreadLocal的静态内部类,然后Thread类中有一个这样类型成员,所以getMap是直接返回Thread的成员。


看下ThreadLocal的内部类ThreadLocalMap源码:


    static class ThreadLocalMap {

/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

//类似于map的key、value结构,key就是ThreadLocal,value就是要隔离访问的变量
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;

/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
* 用数组保存了Entry,因为可能有多个变量需要线程隔离访问
*/
private Entry[] table;

可以看到有个Entry内部静态类,它继承了WeakReference,总之它记录了两个信息,一个是ThreadLocal<?>类型,一个是Object类型的值。getEntry方法则是获取某个ThreadLocal对应的值,set方法就是更新或赋值相应的ThreadLocal对应的值。


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

        private void set(ThreadLocal<?> key, Object value) {

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

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();
}

回顾get方法,其实就是拿到每个线程独有的ThreadLocalMap,然后再用ThreadLocal的当前实例,拿到Map中的相应的Entry,然后就可以拿到相应的值返回出去。当然,如果Map为空,还会先进行Map的创建,初始化等工作。


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

Flutter入口中的runApp方法解析

前言 开发中,如果在runApp方法执行之前设置Android沉浸式样式报错,需要先设置WidgetsFlutterBinding.ensureInitialized();这一行代码才行,为什么,接下来看下这一行代码具体做了啥。 点进去发现这个方法在runAp...
继续阅读 »

前言


开发中,如果在runApp方法执行之前设置Android沉浸式样式报错,需要先设置WidgetsFlutterBinding.ensureInitialized();这一行代码才行,为什么,接下来看下这一行代码具体做了啥。


点进去发现这个方法在runApp中进行了实现,并且还调用了WidgetsFlutterBinding的另两个方法,

方法体:


void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
}

接下来我们对这三个方法进行一个一个进行分析。


1、WidgetsFlutterBinding.ensureInitialized()


代码:



/// widgets框架的具体绑定,将框架绑定到Flutter引擎的中间层。
/// A concrete binding for applications based on the Widgets framework.
/// This is the glue that binds the framework to the Flutter engine.
class WidgetsFlutterBinding extends BindingBase with GestureBinding,
SchedulerBinding, ServicesBinding, PaintingBinding,
SemanticsBinding, RendererBinding, WidgetsBinding {

/// 只有需要绑定时,才需要调用这个方法,在runApp之前调用。
/// You only need to call this method if you need the binding to be
/// initialized before calling [runApp].

static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
WidgetsFlutterBinding();
return WidgetsBinding.instance!;
}
}

可以看到,WidgetsFlutterBinding 继承了 BindingBase,并混入了一些其他Binding


先看下BindingBase类,


/// 初始化 获取唯一window
ui.SingletonFlutterWindow get window => ui.window;
/// 初始化PlatformDispatcher实例,平台消息和配置的中心入口点,负责分发事件给Flutter引擎。
ui.PlatformDispatcher get platformDispatcher => ui.PlatformDispatcher.instance;

主要是获取了window实例和PlatformDispatcher实例。


再看下其他Binding解释:




  • GestureBinding:处理手势相关。




  • SchedulerBinding: 处理系统调度。




  • ServicesBinding:处理与原生的交互。




  • PaintingBinding:处理绘制相关。




  • SemanticsBinding:处理语义化。




  • RendererBinding:处理渲染相关。




  • WidgetsBindingWidgets相关。




Flutter框架层的相关基础绑定。


接着我们看下改变状态栏的代码。


改变样式核心代码:


if (_pendingStyle != _latestStyle) {
// 通过和原生平台进行通信 来改变具体平台状态样式
SystemChannels.platform.invokeMethod<void>(
'SystemChrome.setSystemUIOverlayStyle',
_pendingStyle!._toMap(),
);
_latestStyle = _pendingStyle;
}

通过 ensureInitialized的注释和修改样式的代码即可解决我们开头的疑问,因为设置状态栏样式是通过原生window窗口进行修改的,所以这里如果需要修改状态栏,就需要进行和原生绑定才能拿到原生的window窗口来进行修改。


从注释来看:WidgetsFlutterBinding是widgets框架的具体绑定,将框架绑定到Flutter引擎的中间层。


通过 ensureInitialized方法返回 一个WidgetsBinding单例类。


至此,WidgetsFlutterBinding.ensureInitialized();的工作已经结束。

就是做了初始化引擎绑定,返回WidgetsBinding


..scheduleAttachRootWidget(app)


上一个方法我们知道返回了WidgetsBinding类,那这个方法就是在WidgetsBinding这个类里,接下来先看下这个类。


/// widgets和Flutter引擎之间的粘合剂,中间层
/// The glue between the widgets layer and the Flutter engine.
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
@override
void initInstances() {
super.initInstances();
_instance = this;
// Initialization of [_buildOwner] has to be done after
// [super.initInstances] is called, as it requires [ServicesBinding] to
// properly setup the [defaultBinaryMessenger] instance.
_buildOwner = BuildOwner();
buildOwner!.onBuildScheduled = _handleBuildScheduled;

/// 略

@protected
void scheduleAttachRootWidget(Widget rootWidget) {
Timer.run(() {
attachRootWidget(rootWidget);
});
}

void attachRootWidget(Widget rootWidget) {
final bool isBootstrapFrame = renderViewElement == null;
_readyToProduceFrames = true;
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
if (isBootstrapFrame) {
SchedulerBinding.instance!.ensureVisualUpdate();
}
}

可以看到这是一个mixin类,创建了BuildOwner的实例,这个类是用来管理Element的,在attachRootWidget方法中创建了RenderObjectToWidgetAdapter实例,并设置了我们的runApp中的参数根节点rootWigdt


/// 桥接 RenderObject 到 Element 
/// A bridge from a [RenderObject] to an [Element] tree.
RenderObjectToWidgetAdapter({
this.child,
required this.container,
this.debugShortDescription,
}) : super(key: GlobalObjectKey(container));

RenderObjectToWidgetAdapter 是桥接RenderObjectElement的,Element是持有Widget的具体实现,通过RenderObject进行渲染,也就是通过这个方法实现了 Widget、Element、RenderObject的初始及绑定关系。


..scheduleWarmUpFrame();


绑定之后,接下来就是将内容显示在屏幕上,从以下代码分别调用了handleBeginFramehandleDrawFrame方法,通过hadScheduledFrame判断是否调用handleBeginFrame触发scheduleFrame方法,调用 window.scheduleFrame(); 最终调用 platformDispatcher.scheduleFrame();通知引擎在合适的时机进行帧绘制。


void scheduleWarmUpFrame() {
if (_warmUpFrame || schedulerPhase != SchedulerPhase.idle)
return;
_warmUpFrame = true;
final TimelineTask timelineTask = TimelineTask()..start('Warm-up frame');
final bool hadScheduledFrame = _hasScheduledFrame;
// We use timers here to ensure that microtasks flush in between.
Timer.run(() {
assert(_warmUpFrame);
handleBeginFrame(null);
});
Timer.run(() {
assert(_warmUpFrame);
handleDrawFrame();

resetEpoch();
_warmUpFrame = false;
if (hadScheduledFrame)
scheduleFrame();
});



void scheduleFrame() {
window.scheduleFrame();
}

/// Requests that, at the next appropriate opportunity, the [onBeginFrame] and
/// [onDrawFrame] callbacks be invoked.
void scheduleFrame() => platformDispatcher.scheduleFrame();

小结


runApp中的三个方法执行的三步分别是:

1、初始化WidgetsFlutterBinding返回WidgetBinding实例。

2、初始化Widget、Elment、RenderObject三棵树并确定绑定关系。

3、通知引擎合适时机进行帧绘制。更快的将内容显示到屏幕中。


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

Kotlin - 改良装饰者模式

一、前言 装饰者模式 作用:在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。 本质:该模式通过创建一个包装对象,来包裹真实的对象。 核心操作: 创建一个装饰类,包含一个被装饰类的实例 装饰类重写所有被装饰类的方法 在装饰类中对需要增强的功...
继续阅读 »

一、前言



  • 装饰者模式

    • 作用:在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。

    • 本质:该模式通过创建一个包装对象,来包裹真实的对象。

    • 核心操作:

      • 创建一个装饰类,包含一个被装饰类的实例

      • 装饰类重写所有被装饰类的方法

      • 在装饰类中对需要增强的功能进行扩展






二、使用装饰者模式



  • 例子:枪支部件

  • 重点:装饰器类设计(实现被装饰类相同的接口,构造器接收被装饰类的接口实例对象)


像绝地求生这种大型射击游戏,里面的枪支系统是很复杂的,有很多种枪,而且几乎每种枪上都可以装配各种各样的部件,比如消声器、八倍镜之类的,部件的作用各不相同,有的可以增加火力,有的可以提高精确度,等等,现在我们来简单设计一下这个枪支系统,枪有很多种,所以需要定义一个接口来描述枪都有哪些能力,供后续扩展各种新枪:


/**
* 枪支接口
*
* @author GitLqr
*/
interface Gun {
/**
* 攻击力
*/
fun attack(): Float

/**
* 噪音
*/
fun noise(): Float

/**
* 生产日期
*/
fun prodDate(): String
}

/**
* Ump9
*
* @author GitLqr
*/
class Ump9Gun : Gun {
override fun attack() = 100f

override fun noise() = 20f

override fun prodDate() = "2020-02-18"
}

这里只实现了 Ump9 这个型号的枪,后续还可以根据需要扩展,现在来想想枪支部件怎么设计?在 Java 中,给一个类扩展行为有两种选择:



  • 设计一个继承它的子类

  • 使用装饰者模式对该类进行装饰


那么枪支部件合适用继承方式来设计吗?显然不合适,因为一个部件可以装配在不只一种枪上,所以继承这种方式排除。另一种方式,使用装饰者模式有一个很大的优势,在于符合“组合优于继承”的设计原则,我们知道,部件可以和任意枪组合,显示,使用装饰者模式来设计枪支部件是一个不错的选择:


/**
* 枪支部件
*
* @author GitLqr
*/
abstract class GunPart(protected val gun: Gun) : Gun

/**
* 消声器
*
* @author GitLqr
*/
class Muffler(gun: Gun) : GunPart(gun) {
override fun attack() = gun.attack() - 5

override fun noise() = 0f

override fun prodDate() = gun.prodDate()
}

/**
* 燃烧子弹
*
* @author GitLqr
*/
class FireBullet(gun: Gun) : GunPart(gun) {
override fun attack() = gun.attack() + 200

override fun noise() = gun.noise()

override fun prodDate() = gun.prodDate()
}

程序设计时,装饰器(部件)会引用被装饰实例(枪),并实现被装饰实例的所有接口,然后在需要增强的接口方法中加入增强逻辑。因为枪支部件 GunPart 接收 Gun 类型构造参数,而且本身也是 Gun 接口的实现类,所以,可以让多种枪支部件 GunPart 嵌套修饰枪实例:


// 使用
var ump9: Gun = Ump9Gun()
println("装配前:ump9 攻击力 ${ump9.attack()},噪音 ${ump9.noise()}")
ump9 = Muffler(FireBullet(ump9)) // 装配了 燃烧子弹、消声器 的ump9
println("装配后:ump9 攻击力 ${ump9.attack()},噪音 ${ump9.noise()}")

// 输出
装配前:ump9 攻击力 100.0,噪音 20.0
装配后:ump9 攻击力 295.0,噪音 0.0

三、改良装饰者模式



  • 例子:枪支部件

  • 重点:类委托(by 关键字)


在上面的例子中,装饰者模式可以很好的解决实例组合的情况,但是代码还是显得比较啰唆,因为需要重写所有的装饰对象方法,所以可能会存在大量样板代码。比如 FireBullet 只装饰增强 attack() 方法,而 noise()prodDate() 均不做修改,但还要是把这两个方法重写一遍。Kotlin 中有类委托特性,利用 by 关键字,将装饰类的所有方法委托给一个被装饰的类对象,然后只需覆写装饰的方法即可:


/**
* 枪支部件
*
* @author GitLqr
*/
abstract class GunPart(protected val gun: Gun) : Gun by gun

/**
* 消声器
*
* @author GitLqr
*/
class Muffler(gun: Gun) : GunPart(gun) {
override fun attack() = gun.attack() - 5
override fun noise() = 0f
}

/**
* 燃烧子弹
*
* @author GitLqr
*/
class FireBullet(gun: Gun) : GunPart(gun) {
override fun attack() = gun.attack() + 200
}

可以看到,使用类委托之后,装饰类 FireBullet 中的样板代码不用重写了,从而减少了代码量。


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

开源框架 Egg.js 文档未经授权被转载,原作者反成“恶人”在 v2ex 上被讨伐

5 月 26 日,Egg.js(阿里开源的企业级 Node.js 框架)核心开发者 @“天猪”在知乎发了一篇题为《关于我个人“恶意投诉”别人未授权转载事件的说明》的声明,对近期自己反成“恶人”在 v2ex 上被“讨伐”的事情表示困惑。开发者原文转载 MIT L...
继续阅读 »

5 月 26 日,Egg.js(阿里开源的企业级 Node.js 框架)核心开发者 @“天猪”在知乎发了一篇题为《关于我个人“恶意投诉”别人未授权转载事件的说明》的声明,对近期自己反成“恶人”在 v2ex 上被“讨伐”的事情表示困惑。


开发者原文转载 MIT License 协议文档

被知乎告侵权


原来在很多年前,@天猪 写了一篇关于 Egg.js 某个开源项目的某个特性的使用文档,并于 2018 年将该文档发布到了 2 个地方 —— Egg.js 知乎专栏(文档 A)和 Egg.js 的 GitHub repo 文档库(文档 B)。

其中,文档 A 的版权已授权给知乎,而发布在 GitHub 上的文档 B 则采用了 MIT License 协议。

文档地址:https://zhuanlan.zhihu.com/p/35334932

值得注意的是,发布到这两个地方的内容(文档 A 和文档 B )大部分是重合的。

2019 年,开发者 @an168bang521 在未告知原作者@“天猪”的前提下,从 GitHub 将 Egg.js(文档 B)原文转载到了其个人网站上。(现文章已删除)

地址:https://www.axihe.com/edu/egg/tutorials/typescript.html


但由于 Egg.js 文档(文档 B)使用的是 MIT License 协议,即“允许任何人在 MIT 协议下进行使用和操作”,因此开发者 @an168bang521 原封不动转载 该文档就引发了争议。

Eggjs 使用的 MIT LICENS 链接: https://github.com/eggjs/egg/blob/master/LICENSE

随后,开发者 @an168bang521 搬运自文档 B(采用了 MIT License)的个人网页收到来自知乎的 “侵权告知函” 。

因此,这位开发者 @an168bang521 才终于想起了 Egg.js 文档(文档 B)的原作者,并在知乎平台上发私信给@“天猪”。
 

未经授权被转载

Egg.js 文档原作者反成恶人

在 v2ex 上被“讨伐”


也就在发布这篇声明的前一天晚上,@“天猪” 刚刚收到了这位开发者 @an168bang521 的私信邮件。

在邮件中,该开发者称自己因在 2019 年摘抄了原作者 @“天猪” 的一篇“开源软件 Egg.js 在 GitHub 的技术文档”而被知乎告知侵权,且收到了知乎委托的公司发送的 “侵权告知函”。

开发者 @an168bang521 表示,因为文档 B 使用的是 MIT License 协议,因此自己“大段使用该仓库内的文档,是属于 MIT 里的使用、复制、修改、合并、发布、分发、再许可或出售”。

对此,Egg.js 核心维护者@“天猪”回应称,这因为他们在知乎的专栏(文档 A)已授权给平台的版权服务,(但由于文档 A 和 文档 B 的内容大部分重合)因此当知乎平台检测到对应的文章被未授权转载时,就会自动发送侵权通知。

让人意外的是,在收到该邮件的第二天,就在@“天猪”莫名其妙且感到困惑的时候,该开发者 @an168bang521 已经将该事件的帖子发布在了 v2ex 上,且遭到了来自评论区一堆回复者的“讨伐”。

事情发展到这里,作者@“天猪”才发现:自己的开源 Egg.js 技术文档未经授权被转载,现在自己反而被迫变成了“恶人和小丑”?

随后,@“天猪”开始重视该事件,且正式着手研究关于“基于MIT 协议的开源框架文档未授权被转载”的法律相关事宜。

目前,@“天猪”在该声明中已经附上了自己的“诉求”——“唯一的要求就是:事先跟我打招呼获取授权,注明原文出处,不要破坏文章结构以及加太多广告。”

@“天猪”表示,关于文档站的三方发布问题,自己的观点跟去年 Vue @尤雨溪的一样 —— 关于文档,协议和版权,主要期望的是:及时同步 + 注明非官方 + 出处 + 不要破坏文章结构以及加太多广告。

最后,@“天猪”也强调,正“因为我们都热爱开源,所以基本上都默认 MIT ,真的要用,我们也似乎没有太多的办法,如果三方有过度的行为,也只能倒逼我后续的开源项目都会重新考虑开源协议。”

关于该事件的后续发展,本站 Segmentfault 编辑部也将持续关注,如果您对该事件有相关看法,也欢迎在评论区留言互动。

参考链接:https://zhuanlan.zhihu.com/p/520119900
收起阅读 »

<版本>Android统一依赖管理

总结: 在多module的项目中,对版本的统一管理很重要,可以避免多个版本库的冲突问题,也方便日后的统一升级等等 Android的版本依赖的统一管理,有三种方式: 传统apply from的方式 buildsrc方式 composing builds方式 ...
继续阅读 »

总结:


在多module的项目中,对版本的统一管理很重要,可以避免多个版本库的冲突问题,也方便日后的统一升级等等


Android的版本依赖的统一管理,有三种方式:



  • 传统apply from的方式

  • buildsrc方式

  • composing builds方式


一、传统apply from的方式


在根目录新建一个config.gradle(或其他随意的xxx.gradle)文件
或在根目录的build.gradle定义一些变量
如:


ext {
android = [
compileSdkVersion: 30,
buildToolsVersion: "30",
minSdkVersion : 16,
targetSdkVersion : 28,
versionCode : 100,
versionName : "1.0.0"
]

versions = [
appcompatVersion : "1.1.0",
coreKtxVersion : "1.2.0",
supportLibraryVersion : "28.0.0",
glideVersion : "4.11.0",
okhttpVersion : "3.11.0",
retrofitVersion : "2.3.0",
constraintLayoutVersion: "1.1.3",
gsonVersion : "2.8",
//等等······
]

dependencies = [
//base
"constraintLayout" : "androidx.constraintlayout:constraintlayout:${version["constraintLayoutVersion"]}",
"appcompat" : "androidx.appcompat:appcompat:${version["appcompatVersion"]}",
"coreKtx" : "androidx.core:core-ktx:${version["coreKtxVersion"]}",
//等等······
]
}

在工程的根目录build.gradle添加:


apply from"config.gradle"

在需要依赖的modulebuild.gradle中,依赖的方式如下:


dependencies {
...
// 添加appcompatVersion依赖
api rootProject.ext.dependencies["appcompatVersion"]
...
}

【缺点】



  • 无法跟踪代码,需要手动搜索相关的依赖

  • 可读性很差


二、buildsrc方式


什么是buildsrc


当运行 gradle 时会检查项目中是否存在一个名为 buildsrc 的目录。然后 gradle 会自动编译并测试这段代码,并将其放入构建脚本的类路径中。


对于多项目构建,只能有一个 buildsrc 目录,该目录必须位于根项目目录中, buildsrc 是 gradle 项目根目录下的一个目录,它可以包含我们的构建逻辑。


与脚本插件相比,buildsrc 应该是首选,因为它更易于维护、重构和测试代码。


优缺点:


1】优点:



  • buildSrc是Android默认插件,共享 buildsrc 库工件的引用,全局只有这一个地方可以修改

  • 支持自动补全,支持跳转。


2】缺点:



  • 依赖更新将重新构建整个项目,项目越大,重新构建的时间就越长,造成不必要的时间浪费。


Gradle 文档



A change in buildSrc causes the whole project to become out-of-date. Thus, when making small incremental changes, the --no-rebuild command-line option is often helpful to get faster feedback. Remember to run a full build regularly or at least when you’re done, though.


buildSrc的更改会导致整个项目过时,因此,在进行小的增量更改时,-- --no-rebuild命令行选项通常有助于获得更快的反馈。不过,请记住要定期或至少在完成后运行完整版本。



使用方式:


参考:Kotlin + buildSrc for Better Gradle Dependency Management




  • 在项目根目录下新建一个名为 buildSrc 的文件夹(名字必须是 buildSrc,因为运行 Gradle 时会检查项目中是否存在一个名为 buildSrc 的目录)




  • 在 buildSrc 文件夹里创建名为 build.gradle.kts 的文件,添加以下内容




plugins {
`kotlin-dsl`
}
repositories{
jcenter()
}


  • 在 buildSrc/src/main/java/包名/ 目录下新建 Deps.kt 文件,添加以下内容


object Versions {
......

val appcompat = "1.1.0"

......
}

object Deps {
......

val appcompat = "androidx.appcompat:appcompat:${Versions.appcompat}"

......
}


  • 重启 Android Studio,项目里就会多出一个名为 buildSrc 的 module,实现效果


示意图


三、composing builds方式


摘自 Gradle 文档:复合构建只是包含其他构建的构建. 在许多方面,复合构建类似于 Gradle 多项目构建,不同之处在于,它包括完整的 builds ,而不是包含单个 projects



  • 组合通常独立开发的构建,例如,在应用程序使用的库中尝试错误修复时

  • 将大型的多项目构建分解为更小,更孤立的块,可以根据需要独立或一起工作


优缺点:


1】优点:



  • 支持单向跟踪

  • 自动补全

  • 依赖更新时,不会重新构建整个项目


2】缺点:



  • 需要在每一个module中都添加相应的插件引用


使用方式:


参考Gradle文档



  • 新建的 module 名称 VersionPlugin(名字随意)

  • 在 versionPlugin 文件夹下的 build.gradle 文件内,添加以下内容


buildscript {
repositories {
jcenter()
}
dependencies {
// 因为使用的 Kotlin 需要需要添加 Kotlin 插件
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
}
}

apply plugin: 'kotlin'
apply plugin: 'java-gradle-plugin'
repositories {
// 需要添加 jcenter 否则会提示找不到 gradlePlugin
jcenter()
}

gradlePlugin {
plugins {
version {
// 在 app 模块需要通过 id 引用这个插件
id = 'com.yu.plugin'
// 实现这个插件的类的路径
implementationClass = 'com.yu.versionplugin.VersionPlugin'
}
}
}


  • 在 VersionPlugin/src/main/java/包名/ 目录下新建 DependencyManager.kt 文件,添加相关的依赖配置,如:


package com.yu.versionplugin

/**
* 配置和 build相关的
*/
object BuildVersion {
const val compileSdkVersion = 29
const val buildToolsVersion = "29.0.2"
const val minSdkVersion = 17
const val targetSdkVersion = 26
const val versionCode = 102
const val versionName = "1.0.2"
}

/**
* 项目相关配置
*/
object BuildConfig {
//AndroidX
const val appcompat = "androidx.appcompat:appcompat:1.2.0"
const val constraintLayout = "androidx.constraintlayout:constraintlayout:2.0.4"
const val coreKtx = "androidx.core:core-ktx:1.3.2"
const val material = "com.google.android.material:material:1.2.1"
const val junittest = "androidx.test.ext:junit:1.1.2"
const val swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
const val recyclerview = "androidx.recyclerview:recyclerview:1.1.0"
const val cardview = "androidx.cardview:cardview:1.0.0"

//Depend
const val junit = "junit:junit:4.12"
const val espresso_core = "com.android.support.test.espresso:espresso-core:3.0.2"
const val guava = "com.google.guava:guava:24.1-jre"
const val commons = "org.apache.commons:commons-lang3:3.6"
const val zxing = "com.google.zxing:core:3.3.2"

//leakcanary
const val leakcanary = "com.squareup.leakcanary:leakcanary-android:2.4"

//jetPack
const val room_runtime = "androidx.room:room-runtime:2.2.5"
const val room_compiler = "androidx.room:room-compiler:2.2.5"
const val room_rxjava2 = "androidx.room:room-rxjava2:2.2.5"
const val lifecycle_extensions = "android.arch.lifecycle:extensions:1.1.1"
const val lifecycle_compiler = "android.arch.lifecycle:compiler:1.1.1"
const val rxlifecycle = "com.trello.rxlifecycle3:rxlifecycle:3.1.0"
const val rxlifecycle_components = "com.trello.rxlifecycle3:rxlifecycle-components:3.1.0"

//Kotlin
const val kotlinx_coroutines_core = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7"
//...
}


  • 在 VersionPlugin/src/main/java/包名/ 目录下新建 VersionPlugin.kt,实现Plugin接口,如下:


package com.yu.versionplugin

import org.gradle.api.Plugin
import org.gradle.api.Project

class VersionPlugin : Plugin<Project>{
override fun apply(p0: Project) {

}

companion object{

}
}

项目目录结构



  • settings.gradle 文件内添加如下代码,并重启 Android Studio


//注意是 includeBuild
includeBuild 'VersionPlugin'


  • app 模块 build.gradle 文件内 首行 添加以下内容


plugins{
// 这个 id 就是在 VersionPlugin 文件夹下 build.gradle 文件内定义的 id
id "com.yu.plugin"
}
// 定义的依赖地址
import com.yu.versionplugin.*


  • 使用如下:


import com.yu.versionplugin.*

plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.yu.plugin'
}

android {
compileSdk 32

defaultConfig {
applicationId "com.yu.versiontest"
minSdk BuildVersion.minSdkVersion
targetSdk BuildVersion.targetSdkVersion
versionCode BuildVersion.versionCode
versionName BuildVersion.versionName
}
//.....
}

dependencies {

implementation BuildConfig.coreKtx
implementation BuildConfig.appcompat
implementation BuildConfig.material
//......
}

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

如何解决Flutter的WebView白屏和视频自动播放

前言 众所周知,Flutter 的 WebView 不太友好,用起来不顺手。 我们 Flutter 开发常用的 WebView 库有2个,一个是 Flutter 官方自己出的 webview_flutter ,另一个是比较流行的 flutter_inappwe...
继续阅读 »

前言


众所周知,Flutter 的 WebView 不太友好,用起来不顺手。
我们 Flutter 开发常用的 WebView 库有2个,一个是 Flutter 官方自己出的 webview_flutter ,另一个是比较流行的 flutter_inappwebview 。这两个库其实差不多,flutter_inappwebview 功能比较丰富,封装了很多事件、方法等,但是很多问题这两个库都会遇到。本文以 webview_flutter 为基础库展开讲解相关问题以及解决方案。


问题


白屏、UI错乱




如上图所示



  • 测试的时候发现部分手机(如OPPO)会出现白屏现象(左图)

  • 原生与 Flutter 混编,打开页面会发现页面布局变了,顶部banner变小了(右图)


查阅网上的一些解决方案,千篇一律都是:


if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView();

但是,这样设置其实是不对的,还会出现以上问题,真正的解决方案是:


if (Platform.isAndroid) WebView.platform = AndroidWebView();

视频自动播放


由于需求需要,打开页面的时候,列表的第一个视频(YouTube/Facebook 视频)需要自动播放。
但是发现没法自动播放,如下图,会出现播放之后马上暂停的现象。



查阅资料得知,是谷歌浏览器的隐私政策导致的。



所以要想视频自动播放,有两种方案:



  • 静音播放。

    • 在 Web 端调用视频播放器的静音即可自动播放。



  • 模拟点击。

    • 给 WebView 设置一个 GlobalKey 。
      WebView(
      key: logic.state.videoGlobalKey,
      ......
      );
      }


    • 然后在 WebView 的 onPageFinished 方法里,通过 GlobalKey 获取 WebView 的位置,从而进行模拟点击,就可以自动播放视频了。
      var currentContext = state.videoGlobalKey.currentContext;
      var offset = (currentContext?.findRenderObject() as RenderBox)
      .localToGlobal(Offset.zero);
      //模拟点击
      var addPointer = PointerAddedEvent(
      pointer: 0,
      position: Offset(
      offset.dx + 92.w,
      offset.dy + 92.w),
      );
      var downPointer = PointerDownEvent(
      pointer: 0,
      position: Offset(
      offset.dx + 92.w,
      offset.dy + 92.w),
      );
      var upPointer = PointerUpEvent(
      pointer: 0,
      position: Offset(
      offset.dx + 92.w,
      offset.dy + 92.w),
      );
      GestureBinding.instance!.handlePointerEvent(addPointer);
      GestureBinding.instance!.handlePointerEvent(downPointer);
      GestureBinding.instance!.handlePointerEvent(upPointer);





这两种方案各有利弊,方案一无法播放声音(需要用户手动点击开启声音),方案二偶尔会有误触的操作。我们 APP 通过与产品商量最终选取的是方案一的解决方案。


另外 iOS 端自动播放会自动全屏,需要设置以下属性:


WebView(
key: logic.state.videoGlobalKey,
// 允许在线播放(解决iOS播放视频自动全屏)
allowsInlineMediaPlayback: true,
......
);
}

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

台积电多人离职:老婆受不了

作为全球晶圆代工的一哥,台积电这几年成了香饽饽,营收及盈利也大涨,员工待遇也是业内最高的,今年4月份还要全员加薪8%,然而高薪诱惑下还有大量员工离职,有员工爆料称这样的工作方式已经影响家庭和谐。有网友在论坛上发帖请教台积电内部员工,称自己的同学之前离开了台积电...
继续阅读 »

作为全球晶圆代工的一哥,台积电这几年成了香饽饽,营收及盈利也大涨,员工待遇也是业内最高的,今年4月份还要全员加薪8%,然而高薪诱惑下还有大量员工离职,有员工爆料称这样的工作方式已经影响家庭和谐。

有网友在论坛上发帖请教台积电内部员工,称自己的同学之前离开了台积电,现在看到加薪8%之后又有些后悔,想知道大家为什么在台积电有这么丰厚的收入还要离职呢?


很快这个帖子就变成了吐槽大会,台积电内部员工抱怨起台积电的工作制度了,直言钱已经不重要了。

11、12点下班隔天6点跟国外开会,我朋友姐夫是这样,老婆受不了。

生活品质很差,不然为何老外做不起来?

有钱没生活,当过半导体制造厂工程师就懂

新同事硕士毕业去2年肝指数上升很多

完全无法理解只有工作跟睡觉的生活

用命换的你是能待多久

里面的想出来,外面的想进去,离职时到七厂人资办理,真的是要排队

要不要去台积电就是钱/生活的选择而已

因为台积电实际上跟外商比起来给的不算高薪又血汗。

3月初有报道称,今年预计招募超过8000名新员工,其中,硕士毕业工程师平均年薪上看200万新台币,约合人民币45万元。


今年2月份,台积电董事会批准了2021年薪酬奖励,去年员工业绩奖金与酬劳(分红)合计712亿290万元,其中员工业绩奖金约新台币356亿145万元已于每季季后发放,而酬劳(分红)约新台币356亿145万元将于今年七月发放。另外,搜索公众号Linux就该这样学后台回复“git书籍”,获取一份惊喜礼包。

台积电去年的总奖励相比2020年增长了2.4%,按照5.7万员工总数来看,人均奖励约为124万新台币,约合28.2万元人民币。

网友表示:“用钱换命,可以啊!多少人命在消耗,钱没进口袋。”

你还有什么想要补充的吗?

版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!

收起阅读 »

恶意技术时代下的负责任技术

从数百万美元的勒索软件赔款,到数亿用户私人信息的数据泄露,围绕恶意技术的头条新闻一直很吸引眼球,但它们并不能说明全部问题。真实情况是,恶意技术远不止蓄意的、有针对性的破坏系统或窃取用户数据。尽管黑客攻击、勒索软件攻击、数据泄露和 DDoS 攻击在这类新闻叙述中...
继续阅读 »

从数百万美元的勒索软件赔款,到数亿用户私人信息的数据泄露,围绕恶意技术的头条新闻一直很吸引眼球,但它们并不能说明全部问题。

真实情况是,恶意技术远不止蓄意的、有针对性的破坏系统或窃取用户数据。尽管黑客攻击、勒索软件攻击、数据泄露和 DDoS 攻击在这类新闻叙述中占据着主导地位,也的确造成了严重损害,但实际上它们只是沧海一粟。

认识所有形式的恶意技术行为

首先要摆正心态,不要对 "恶意技术 "一词感到过于恐慌。这里谈论的不仅是技术攻击本身,我们需要更广泛地思考这一问题。

恶意技术并不单纯指非法行为,我们甚至不是在谈论那些必然是恶意的事情。例如,有些人完全乐于接受在线监控,因为这可以帮助他们获得更精准、更具个性化的推荐。与此同时也有另外一些人会不遗余力地躲避任何数字监控的追踪,因为这在他们看来是不道德的。

有些技术看似恶意,其出发点却并不如此。例如,图像识别软件的开发者不会刻意让软件在识别到黑人妇女面孔时提供不一致的结果,只是由于软件现阶段有一个糟糕的数据集,才会呈现出看似有偏见的结果,而这些并非开发者的本意。这些都不是恶意的项目,当然也不归属于恶意的品牌。在许多情况下,它们甚至不是设计或规划上的失败,而是团队没有充分考虑到一项技术决定会对所有潜在的利益相关者群体产生怎样不同的影响。

这是一个关键点。在设计之初,软件通常有一个特定的利益相关者群体,设计者试图直接服务或满足这一群体的需求,却常常忽视了该产品对其他利益相关者的影响。比如训练一个自然语言处理模型所产生的二氧化碳排放足迹,与纽约和北京之间往返125次航班相同——很少有人考虑到这种环境上的影响。除此之外还有很多人忽视的公平、公正问题。例如,疫情期间很多教育转入线上,一些家庭没有良好的网络情况,根本无法同时支撑几个孩子共同参与线上课程,更不用说父母也要同时在家办公。许多人看到的是数字教育的精彩革命,但他们没有看到那些因数字不平等而被落下的人。

为什么现在是拥抱道德技术的正确时机?

疫情所暴露的数字不平等和正在发生的气候危机只是原因之二。现如今,技术几乎深深扎根于我们生活的各个方面——医疗决定、信贷决定、缓刑或判刑决定,所有这些对个人生活有巨大影响的事情,都受到技术选择的巨大影响。这其中的利害关系是非常真实的,而技术决策者在做这些决定时却很少做到全面考虑。这就是为什么对各种组织来说,树立负责任的技术思维是如此重要。

负责任技术的定义

负责任技术——有时也被称为道德技术或公平技术——是一个总括性的术语,包含了多种概念,总之就是用技术做正确的事情——这可能意味着任何事情,从采取措施使更容易获得一个应用程序,到实施政策以帮助持续提供公平的技术体验。

从字面上看,这是一个相对简单的概念,但仍然被误解所笼罩。我最近读到一些信息,说“责任”在土木工程等领域很容易定义,你的责任是确保建筑物稳定,不倒塌,不对相关居民的生活产生负面影响。其隐藏的含义是,在软件或技术领域,这一点很难定义。

是的,我们并不像医疗行业那样受到任何形式的“希波克拉底誓言”的约束。但是,当涉及到用技术做出道德以及负责任的决定时,我们常常给自己留了过多的余地。

我们可以采取哪些措施来变得更加负责?

为了减少技术所附带的无意伤害并做出负责任的决定,今天的企业需要着重关注的是,识别可能受到特定技术决定影响的潜在利益相关者。这意味着需要关注:

  • 受测群体——他们是否真正代表了预期中将使用该产品的终端用户群体?这个过程是否涵盖了所有利益相关群体,他们是否有发声渠道?

  • 服务所需数据集的质量和准确性——是否没有偏见,是否能够提供真实、全面的反馈和包容性体验?

  • 设计中是否考虑到了平等和易用性——以及复杂的特征或功能是否以整体可用性和可及性为代价?

  • 这一决策是否会对人类社会产生更广义的负面影响?例如,它是否与我们的可持续发展目标相一致,是否有可能破坏环境?

我希望鼓励组织做的另一件事是,明确声明组织关心什么,以及希望所采用的技术能帮助实现什么。正如《数学毁灭武器》的作者Cathy O'Neil所说,有些时候,你必须在公平和利润之间进行交易。

这取决于你想在这个光谱上处于什么位置,但重要的是要明确目标和意图。我曾与一个组织合作,该组织制定了一个框架,表达了他们在使用客户数据方面的价值观和原则,清楚地阐述了他们打算如何操作这些数据,以及为什么会做出这个决定。这花了几个月的时间,但这使他们的意图和道德立场十分清晰,并且容易保持一致。

减轻技术的无意识伤害

技术所产生的无意识伤害有多种形式,可能潜伏在任何技术决策中。作为技术管理者,我们有责任提出正确的问题,并考虑这些技术将如何被每个人使用,以及可能对使用者的生活和经历产生怎样的影响。

转向这种负责任的方法和心态在理论上相对简单,但在看到有意义的结果之前,整个行业的组织和专业人士需要作出真正的奉献。正如我们有责任保护客户数据免受恶意威胁一样,我们也有道德责任尽我们所能减少技术所产生的负面影响,并为所有人建立一个平等、可访问的数字世界。

来源: Thoughtworks洞见

收起阅读 »

IE浏览器6月退役 微软力推Edge:渲染时间减少85%、CPU内存占用大降

前不见微软宣布IE浏览器6月退役,这个始于Win95时代的浏览器在27年后还是要被放弃了,微软现在的重点放在了Edge浏览器上,不仅技术更新,而且性能更好,CPU及内存占用大幅下降。在日前的微软Build 2022大会上,微软介绍了Edge浏览器的进展,很快就...
继续阅读 »

前不见微软宣布IE浏览器6月退役,这个始于Win95时代的浏览器在27年后还是要被放弃了,微软现在的重点放在了Edge浏览器上,不仅技术更新,而且性能更好,CPU及内存占用大幅下降。

在日前的微软Build 2022大会上,微软介绍了Edge浏览器的进展,很快就会升级WebView2,届时Edge浏览器性能会得到更好的提升。

IE浏览器6月退役 微软力推Edge:渲染时间减少85%、CPU内存占用大降

微软也公布了对比结果,与使用Internet Explorer运行他们的解决方案相比,使用 Edge WebView2,可以将渲染时间减少了85%,CPU占用率降低33%,内存占用率也会降低32%,改善明显。

4月初,根据分析机构StatCounter的数据,Edge成功实现了对Safari的反超。

截止2022年3月,Edge获得了9.65%的市场份额,超过Safari的9.56%成功夺得第二名,但与Chrome堪称恐怖的67.29%相比,依旧存在明显的差距。

IE浏览器6月退役 微软力推Edge:渲染时间减少85%、CPU内存占用大降

来源:tech.ifeng.com/c/8GKXHf1ZEWO

收起阅读 »

微前端框架 qiankun 技术分析

如何加载子应用single-spa 通过 js entry 的形式来加载子应用。而 qiankun 采用了 html entry 的形式。这两种方式的优缺点我们在理解微前端技术原理中已经做过分析,这里不再赘述,我们看看 qiankun 是如何实现 html e...
继续阅读 »

如何加载子应用

single-spa 通过 js entry 的形式来加载子应用。而 qiankun 采用了 html entry 的形式。这两种方式的优缺点我们在理解微前端技术原理中已经做过分析,这里不再赘述,我们看看 qiankun 是如何实现 html entry 的。

qiankun 提供了一个 API registerMicroApps 来注册子应用,其内部调用 single-spa 提供的 registerApplication 方法。在调用 registerApplication 之前,会调用内部的 loadApp 方法来加载子应用的资源,初始化子应用的配置。

通过阅读 loadApp 的代码,我们发现,qiankun 通过 import-html-entry 这个包来加载子应用。import-html-entry 的作用就是通过解析子应用的入口 html 文件,来获取子应用的 html 模板、css 样式和入口 JS 导出的生命周期函数。

import-html-entry

import-html-entry 是这样工作的,假设我们有如下 html entry 文件:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>test</title>
</head>
<body>

<!-- mark the entry script with entry attribute -->
<script src="https://unpkg.com/mobx@5.0.3/lib/mobx.umd.js" entry></script>
<script src="https://unpkg.com/react@16.4.2/umd/react.production.min.js"></script>
</body>
</html>

我们使用 import-html-entry 来解析这个 html 文件:

import importHTML from 'import-html-entry';

importHTML('./subApp/index.html')
.then(res => {
console.log(res.template);

res.execScripts().then(exports => {
const mobx = exports;
const { observable } = mobx;
observable({
name: 'kuitos'
})
})
});

importHTML 的返回值有如下几个属性:

  • template 处理后的 HTML 模板
  • assetPublicPath 静态资源的公共路径
  • getExternalScripts 获取所有外部脚本的函数,返回脚本路径
  • getExternalStyleSheets 获取所有外部样式的函数,返回样式文件的路径
  • execScripts 执行脚本的函数

在 importHTML 的返回值中,除了几个工具类的方法,最重要的就是 template 和 execScripts 了。

importHTML('./subApp/index.html') 的整个执行过程代码比较长,我们只讲一下大概的执行原理,感兴趣的同学可以自行查看importHTML 的源码

importHTML 首先会通过 fetch 函数请求具体的 html 内容,然后在 processTpl 函数 中通过一系列复杂的正则匹配,解析出 html 中的样式文件和 js 文件。

importHTML 函数返回值为 { template, scripts, entry, styles },分别是 html 模板,html 中的 js 文件(包含内嵌的代码和通过链接加载的代码),子应用的入口文件,html 中的样式文件(同样是包含内嵌的代码和通过链接加载的代码)。

之后通过 getEmbedHTML 函数 将所有使用外部链接加载的样式全部转化成内嵌到 html 中的样式。getEmbedHTML 返回的 html 就是 importHTML 函数最终返回的 template 内容。

现在,我们看看 execScripts 是怎么实现的。

execScripts 内部会调用 getExternalScripts 加载所有 js 代码的文本内容,然后通过 eval("code") 的形式执行加载的代码。

注意,execScripts 的函数签名是这样的 (sandbox?: object, strictGlobal?: boolean, execScriptsHooks?: ExecScriptsHooks): Promise<unknown>。允许我们传入一个沙箱对象,如果子应用按照微前端的规范打包,那么会在全局对象上设置 mountunmount 这几个生命周期函数属性。execScripts 在执行 eval("code") 的时候,会巧妙的把我们指定的沙箱最为全局对象包装到 "code" 中,子应用能够运行在沙盒环境中。

在执行完 eval("code") 以后,就可以从沙盒对象上获取子应用导出的生命周期函数了。

loadApp

现在我们把视线拉回 loadApp 中,loadApp 在获取到 templateexecScripts 这些信息以后,会基于 template 生成 render 函数用于渲染子应用的页面。之后会根据需要生成沙盒,并将沙盒对象传给 execScripts 来获取子应用导出的声明周期函数。

之后,在子应用生命周期函数的基础上,构建新的生命周期函数,再调用 single-spa 的 API 启动子应用。

在这些新的生命周期函数中,会在不同时机负责启动沙盒、渲染子应用、清理沙盒等事务。

隔离

在完成子应用的加载以后,作为一个微前端框架,要解决好子应用的隔离问题,主要要解决 JS 隔离和样式隔离这两方面的问题。

JS 隔离

qiankun 为根据浏览器的能力创建两种沙箱,在老旧浏览器中会创建快照模式 的浏览器中创建 VM 模式的沙箱 ProxySandbox

篇幅限制,我们只看 ProxySandbox 的实现,在其构造函数中,我们可以看到具体的逻辑:首先会根据用户指定的全局对象(默认是 window)创建一个 fakeWindow,之后在这个 fakeWindow 上创建一个 proxy 对象,在子应用中,这个 proxy 对象就是全局变量 window

constructor(name: string, globalContext = window) {
const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext);
const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {},
get: (target: FakeWindow, p: PropertyKey): any => {},
has(target: FakeWindow, p: string | number | symbol): boolean {},

getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {},

ownKeys(target: FakeWindow): ArrayLike<string | symbol> {},

defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {},

deleteProperty: (target: FakeWindow, p: string | number | symbol): boolean => {},

getPrototypeOf() {
return Reflect.getPrototypeOf(globalContext);
},
});
this.proxy = proxy;
}

其实 qiankun 中的沙箱分两个类型:

  • app 环境沙箱
    app 环境沙箱是指应用初始化过之后,应用会在什么样的上下文环境运行。每个应用的环境沙箱只会初始化一次,因为子应用只会触发一次 bootstrap 。子应用在切换时,实际上切换的是 app 环境沙箱。
  • render 沙箱
    子应用在 app mount 开始前生成好的的沙箱。每次子应用切换过后,render 沙箱都会重现初始化。

上面说的 ProxySandbox 其实是 render 沙箱。至于 app 环境沙箱,qiankun 目前只针对在应用 bootstrap 时动态创建样式链接、脚本链接等副作用打了补丁,保证子应用切换时这些副作用互不干扰。

之所以设计两层沙箱,是为了保证每个子应用切换回来之后,还能运行在应用 bootstrap 之后的环境下。

样式隔离

qiankun 提供了多种样式隔离方式,隔离效果最好的是 shadow dom,但是由于其存在诸多限制,qiankun 官方在将来的版本中将会弃用,转而推行 experimentalStyleIsolation 方案。

我们可以通过下面这段代码看到 experimentalStyleIsolation 方案的基本原理。

const styleNodes = appElement.querySelectorAll('style') || [];
forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
css.process(appElement!, stylesheetElement, appInstanceId);
});

css.process 的核心逻辑,就是给读取到的子应用的样式添加带有子应用信息的前缀。效果如下:

/* 假设应用名是 react16 */
.app-main {
font-size: 14px;
}

div[data-qiankun-react16] .app-main {
font-size: 14px;
}

通过上面的隔离方法,基本可以保证子应用间的样式互不影响。

小结

qiankun 在 single-spa 的基础上根据实际的生产实践开发了很多有用的功能,大大降低了微前端的使用成本。

本文仅仅针对如何加载子应用和如何做好子应用间的隔离这两个问题,介绍了 qiankun 的实现。其实,在隔离这个问题上,qiankun 也仅仅是根据实际中会遇到的情况做了必要的隔离措施,并没有像 iframe 那样实现完全的隔离。我们可以说 qiankun 实现的隔离有缺陷,也可以说是 qiankun 在实际的业务需求和完全隔离的实现成本之间做的取舍。

原文:https://segmentfault.com/a/1190000041151414

收起阅读 »

pc端微信授权登录两种实现方式的总结

在开发pc端项目中,使用微信授权登录是种很常用的功能,目前在功能实现上有两种不同的方式,现根据两种方式做如下总结。一、跳转微信授权登录页面进行扫码授权这种方法实现非常简单只用跳转链接就可以实现微信授权登录window.location = https://op...
继续阅读 »

在开发pc端项目中,使用微信授权登录是种很常用的功能,目前在功能实现上有两种不同的方式,现根据两种方式做如下总结。

一、跳转微信授权登录页面进行扫码授权

这种方法实现非常简单只用跳转链接就可以实现微信授权登录

window.location = https://open.weixin.qq.com/connect/qrconnect?appid=${appid}&redirect_uri=${回调域名}/login&response_type=code&scope=snsapi_login&state=${自定义配置}#wechat_redirect

跳转之后进行微信扫码,之后微信会带着code,回调回你设置的回调域名,这之后拿到code再和后台进行交互,即可实现微信登陆。
这种方法相对来说实现起来非常简单,但是因为需要先跳转微信授权登录页面,在体验上来说可能不是太好。

二、在当前页面生成微信授权登录二维码

这种方法是需要引入wxLogin.js,动态生成微信登陆二维码,具体实现方法如下:

const s = document.createElement('script')
s.type = 'text/javascript'
s.src = 'https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js'
const wxElement = document.body.appendChild(s)
wxElement.onload = function () {
var obj = new WxLogin({
id: 'wx_login_id', // 需要显示的容器id
appid: '', // 公众号appid
scope: 'snsapi_login', // 网页默认即可
redirect_uri:'', // 授权成功后回调的url
state: '', // 可设置为简单的随机数加session用来校验
style: 'black', // 提供"black"、"white"可选。二维码的样式
href: '' // 外部css(查看二维码的dom结构,根据类名进行样式覆盖)文件url,需要https
})
}

其中href参数项还可以通过node将css文件转换为data-url,实现方式如下:

var fs = require('fs');
function base64_encode(file) {
var bitmap = fs.readFileSync(file);
return 'data:text/css;base64,'+new Buffer(bitmap).toString('base64');
}
console.log(base64_encode('./qrcode.css'))

在终端对该js文件执行命令:

node qr.js

把打印出来的url粘贴到href即可。
这种实现方法避免了需要跳转新页面进行扫码,二维码的样式也可以进行更多的自定义设置,可能在体验上是更好的选择。

原文:https://segmentfault.com/a/1190000024492932


收起阅读 »

苹果:App自6月30日起支持删除账号,开发者相关问题都在这里了

今晨,苹果正式宣布自 2022 年 6 月 30 日起,提交至 App Store 且支持账号创建的应用,必须允许用户在应用内删除账号。6 月 30 日起,App 必须允许用户删除账号从 2022 年 6 月 30 日开始,App Store 内支持账号创建的...
继续阅读 »

今晨,苹果正式宣布自 2022 年 6 月 30 日起,提交至 App Store 且支持账号创建的应用,必须允许用户在应用内删除账号。

6 月 30 日起,App 必须允许用户删除账号

从 2022 年 6 月 30 日开始,App Store 内支持账号创建的应用,必须提供删除账号的功能。

1653473320(1).jpg

出海痛点很多?点击这里解决



开发者如需更新应用程序以完善删除账号功能,需要注意以下几点:

1)用户能在应用中快速找到删除账号的入口,一般可在账户设置中找到;

2)如果用户是通过 Apple ID 登录,需要在删除账号时使用 Sign in with Apple REST API 来撤销用户令牌;

发.png

3)用户删除账号不仅是暂时停用或禁用账号,苹果要求在应用内,所有与该账号相关的个人数据都可以被删除,以帮助用户更好地管理隐私数据;

4)受高度监管的应用可能需要提供额外的客户服务流程,以跟进账号删除过程;

5)遵守有关存储和保留用户账号信息以及处理账号删除的适用法律要求,包括遵守不同国家或地区的当地法律。

此外,如果用户需要访问网站以指引如何删除账号,开发者也需提供相关链接。

若删除账号需要额外的时间,或删除时应用购买问题需要另外解决,开发者也应告知用户。

App 删除账号功能相关问题

Q:开发者可以将用户引导到客户服务流程以完成账号删除吗?

A:受高度监管的应用,如中应用商店审查指南 5.1.1(ix)所述,可能会使用额外的客户服务流程来确认和促进账号删除过程。

不在高度监管的行业中运行的应用程序不应要求用户拨打电话、发送电子邮件或通过其他支持流程完成账号删除。

Q:开发者是否可以要求重新认证,或添加确认步骤以确保账号不会被意外删除或被账号持有人以外的人删除?

A:可以,确保删除动作是用户期望进行的。

开发者可以添加步骤来验证用户身份,并确认他们想要删除该账号(如通过输入已与该账号关联的电子邮件或电话号码)。

但是,给用户删除账号增加不必要的困难将不会通过审核。

Q:如应用使用 Sign in with Apple 为用户提供账号创建和身份验证,需要进行哪些更改?

A:支持 Sign in with Apple 的应用需要使用 Sign in with Apple REST API 来撤销用户令牌。更多信息,请查看苹果官方文档和设计建议。

Q:如果开发者的应用链接到默认网络浏览器以创建账号,是否仍需要在应用内提供账号删除功能?

A:是的。但请注意链接到默认 Web 浏览器进行登录或注册账号,会影响用户体验,具体可查看应用商店审查指南 4。

Q:应用会自动为用户创建一个账号,是否需要提供进行账号删除的选项?

A:是的。用户应该可以选择删除自动生成的账号(包括访客账号)以及与这些账号关联的数据。

同时,开发者需要确保应用中的任何账号创建都符合当地法律。

Q:账号删除是否必须立即自动完成?

A:不是,可以接受手动删除账号,并花费一些时间。

开发者需要通知用户删除账号需要多长时间,并在删除完成后提供确认,并确保删除账号所用的时间。

Q:删除账号后,用户产生的内容是否需要在共享的应用中删除?

A:是的。用户删除账号时,将删除与其账号关联的所有数据,包括与他人一起生成的内容,如照片、视频、文字帖子和评论等。

如果当地法律要求开发者维护某些数据,请另外告知用户。

Q:是否允许应用只在某些地方根据 CCPA、GDPR 或其他当地法律删除账号?

A:不可以。应该允许所有用户删除他们的账号,无论他们身在何处,开发者的账号删除流程也需要提供给所有用户。

Q:如何管理自动续订的用户,以免在用户删除账号后意外收费?

A:告知用户管理订阅,后续计费将通过 Apple 继续,并提醒用户在下一次收费前取消订阅。

开发者使用 App Store 自动续订的 Server Notifications,可以实时查看用户的订阅状态,或者使用订阅状态 API 进行识别。

同时,开发者可以提供 Apple 支持链接(https: //support.apple.com/en-us/HT204084),帮助用户提交退款请求。

此外,开发者还可以提供一个选项,即设置账号删除日期与订阅到期时间一致,但仍需提供可立即删除账号的选项。

应用更新过程中的更多常见问题,可访问以下网站了解:

https://developer.apple.com/support/offering-account-deletion-in-your-app

据悉,苹果去年就已宣布调整 App Store 的指导方针,要求应用允许用户删除自己的账户,但由于功能实现较复杂,苹果两度推迟实行。如今正式推行,预计未来一段时间内或将有大量应用进行更新。

收起阅读 »