注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Kotlin协程:协程上下文与上下文元素

一.EmptyCoroutineContext    EmptyCoroutineContext代表空上下文,由于自身为空,因此get方法的返回值是空的,fold方法直接返回传入的初始值,plus方法也是直接返回传入的c...
继续阅读 »

一.EmptyCoroutineContext

    EmptyCoroutineContext代表空上下文,由于自身为空,因此get方法的返回值是空的,fold方法直接返回传入的初始值,plus方法也是直接返回传入的context,minusKey方法返回自身,代码如下:

public object EmptyCoroutineContext : CoroutineContext, Serializable {
private const val serialVersionUID: Long = 0
private fun readResolve(): Any = EmptyCoroutineContext

public override fun <E : Element> get(key: Key<E>): E? = null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R = initial
public override fun plus(context: CoroutineContext): CoroutineContext = context
public override fun minusKey(key: Key<*>): CoroutineContext = this
public override fun hashCode(): Int = 0
public override fun toString(): String = "EmptyCoroutineContext"
}

二.CombinedContext

    CombinedContext是组合上下文,是存储Element的重要的数据结构。内部存储的组织结构如下图所示:
image.png

    可以看出CombinedContext是一种左偏(从左向右计算)的列表,这么设计的目的是为了让CoroutineContext中的plus方法工作起来更加自然。

    由于采用这种数据结构,CombinedContext类中的很多方法都是通过循环实现的,代码如下:

internal class CombinedContext(
// 数据结构左边可能为一个Element对象或者还是一个CombinedContext对象
private val left: CoroutineContext,
// 数据结构右边只能为一个Element对象
private val element: Element
) : CoroutineContext, Serializable {

override fun <E : Element> get(key: Key<E>): E? {
var cur = this
while (true) {
// 进行get操作,如果当前CombinedContext对象中存在,则返回
cur.element[key]?.let { return it }
// 获取左边的上下文对象
val next = cur.left
// 如果是CombinedContext对象
if (next is CombinedContext) {
// 赋值,继续循环
cur = next
} else { // 如果不是CombinedContext对象
// 进行get操作,返回
return next[key]
}
}
}
// 数据结构左右分开操作,从左到右进行fold运算
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(left.fold(initial, operation), element)

public override fun minusKey(key: Key<*>): CoroutineContext {
// 如果右边是指定的Element对象,则返回左边
element[key]?.let { return left }
// 调用左边的minusKey方法
val newLeft = left.minusKey(key)
return when {
// 这种情况,说明左边部分已经是去掉指定的Element对象的,右边也是如此,因此返回当前对象,不需要在进行包裹
newLeft === left -> this
// 这种情况,说明左边部分包含指定的Element对象,因此返回只右边
newLeft === EmptyCoroutineContext -> element
// 这种情况,返回的左边部分是新的,因此需要和右边部分一起包裹后,再返回
else -> CombinedContext(newLeft, element)
}
}

private fun size(): Int {
var cur = this
//左右各一个
var size = 2
while (true) {
cur = cur.left as? CombinedContext ?: return size
size++
}
}

// 通过get方法实现
private fun contains(element: Element): Boolean =
get(element.key) == element

private fun containsAll(context: CombinedContext): Boolean {
var cur = context
// 循环展开每一个CombinedContext对象,每个CombinedContext对象中的Element对象都要包含
while (true) {
if (!contains(cur.element)) return false
val next = cur.left
if (next is CombinedContext) {
cur = next
} else {
return contains(next as Element)
}
}
}
...
}

三.Key与Element

    Key接口与Element接口定义在CoroutineContext接口中,代码如下:

public interface Key<E : Element>

public interface Element : CoroutineContext {
// 一个Key对应着一个Element对象
public val key: Key<*>
// 相等则强制转换并返回,否则则返回空
public override operator fun <E : Element> get(key: Key<E>): E? =
@Suppress("UNCHECKED_CAST")
if (this.key == key) this as E else null
// 自身与初始值进行fold操作
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
// 如果要去除的是当前的Element对象,则返回空的上下文,否则返回自身
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}

四.CoroutineContext

    CoroutineContext接口定义了协程上下文的基本行为以及Key和Element接口。同时,重载了"+"操作,相关代码如下:

public interface CoroutineContext {

public operator fun <E : Element> get(key: Key<E>): E?

public fun <R> fold(initial: R, operation: (R, Element) -> R): R

public operator fun plus(context: CoroutineContext): CoroutineContext =
// 如果要与空上下文相加,则直接但会当前对象,
if (context === EmptyCoroutineContext) this else
// 当前Element作为初始值
context.fold(this) { acc, element ->
// acc:已经加完的CoroutineContext对象
// element:当前要加的CoroutineContext对象

// 获取从acc中去掉element后的上下文removed,这步是为了确保添加重复的Element时,移动到最右侧
val removed = acc.minusKey(element.key)
// 去除掉element后为空上下文(说明acc中只有一个Element对象),则返回element
if (removed === EmptyCoroutineContext) element else {
// ContinuationInterceptor代表拦截器,也是一个Element对象
// 下面的操作是为了把拦截器移动到上下文的最右端,为了方便快速获取
// 从removed中获取拦截器
val interceptor = removed[ContinuationInterceptor]
// 若上下文中没有拦截器,则进行累加(包裹成CombinedContext对象),返回
if (interceptor == null) CombinedContext(removed, element) else {
// 若上下文中有拦截器
// 获取上下文中移除到掉拦截器后的上下文left
val left = removed.minusKey(ContinuationInterceptor)
// 若移除到掉拦截器后的上下文为空上下文,说明上下文left中只有一个拦截器,
// 则进行累加(包裹成CombinedContext对象),返回
if (left === EmptyCoroutineContext) CombinedContext(element, interceptor) else
// 否则,现对当前要加的element和left进行累加,然后在和拦截器进行累加
CombinedContext(CombinedContext(left, element), interceptor)
}
}
}

public fun minusKey(key: Key<*>): CoroutineContext

... // (Key和Element接口)
}

1.plus方法图解

    假设我们有一个上下文顺序为A、B、C,现在要按顺序加上D、C、A。

1)初始值A、B、C
27ee3db5-ba83-4f8b-b155-de7974e76e4a.png
2)加上D
335ec6b6-b12f-4367-a274-5f65b4330517.png
3)加上C
6c36e62f-f050-47ca-b769-c29a91ef6f07.png
4)加上A
de380c56-5377-4fcc-a8c3-e6a579bf6609.png

2.为什么要将ContinuationInterceptor放到协程上下文的最右端?

    在协程中有大量的场景需要获取ContinuationInterceptor。根据之前分析的CombinedContext的minusKey方法,ContinuationInterceptor放在上下文的最右端,可以直接获取,不需要经过多次的循环。

五.AbstractCoroutineContextKey与AbstractCoroutineContextElement

    AbstractCoroutineContextElement实现了Element接口,将Key对象作为构造方法必要的参数。

public abstract class AbstractCoroutineContextElement(public override val key: Key<*>) : Element

    AbstractCoroutineContextKey用于实现Element的多态。什么是Element的多态呢?假设类A实现了Element接口,Key为A。类B继承自类A,Key为B。这时将类B的对象添加到上下文中,通过指定不同的Key(A或B),可以得到不同类型对象。具体代码如下:

// baseKey为衍生类的基类的Key
// safeCast用于对基类进行转换
// B为基类,E为衍生类
public abstract class AbstractCoroutineContextKey<B : Element, E : B>(
baseKey: Key<B>,
private val safeCast: (element: Element) -> E?
) : Key<E> {
// 顶置Key,如果baseKey是AbstractCoroutineContextKey,则获取baseKey的顶置Key
private val topmostKey: Key<*> = if (baseKey is AbstractCoroutineContextKey<*, *>) baseKey.topmostKey else baseKey

// 用于类型转换
internal fun tryCast(element: Element): E? = safeCast(element)
// 用于判断当前key是否是指定key的子key
// 逻辑为与当前key相同,或者与当前key的顶置key相同
internal fun isSubKey(key: Key<*>): Boolean = key === this || topmostKey === key
}

1.getPolymorphicElement方法与minusPolymorphicKey方法

    如果衍生类使用了AbstractCoroutineContextKey,那么基类在实现Element接口中的get方法时,就需要通过getPolymorphicElement方法,实现minusKey方法时,就需要通过minusPolymorphicKey方法,代码如下:

public fun <E : Element> Element.getPolymorphicElement(key: Key<E>): E? {
// 如果key是AbstractCoroutineContextKey
if (key is AbstractCoroutineContextKey<*, *>) {
// 如果key是当前key的子key,则基类强制转换成衍生类,并返回
@Suppress("UNCHECKED_CAST")
return if (key.isSubKey(this.key)) key.tryCast(this) as? E else null
}
// 如果key不是AbstractCoroutineContextKey
// 如果key相等,则强制转换,并返回
@Suppress("UNCHECKED_CAST")
return if (this.key === key) this as E else null
}
public fun Element.minusPolymorphicKey(key: Key<*>): CoroutineContext {
// 如果key是AbstractCoroutineContextKey
if (key is AbstractCoroutineContextKey<*, *>) {
// 如果key是当前key的子key,基类强制转换后不为空,说明当前Element需要去掉,因此返回空上下文,否则返回自身
return if (key.isSubKey(this.key) && key.tryCast(this) != null) EmptyCoroutineContext else this
}
// 如果key不是AbstractCoroutineContextKey
// 如果key相等,说明当前Element需要去掉,因此返回空上下文,否则返回自身
return if (this.key === key) EmptyCoroutineContext else this
}


作者:李萧蝶
链接:https://juejin.cn/post/7126161787392622623
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

有趣的 Kotlin 0x0D: IntArray vs Array<Int>

介绍 IntArray 整数数组。在 JVM 平台上,对应 int[]。 Array Array<T> 表示 T 类型数组。在 JVM 平台上,Array<Int> 对应 Integer[]。 验证 fun main() { &nbs...
继续阅读 »

介绍


IntArray


整数数组。在 JVM 平台上,对应 int[]


Array


Array<T> 表示 T 类型数组。在 JVM 平台上,Array<Int> 对应 Integer[]


验证


fun main() {
   val one = IntArray(10) { it }
   val two = Array<Int>(10) { it }
}

Decompile


Java Code


综上,JVM 平台上,IntArrayArray<Int> 的区别在于对应的类型不同,一个是基础类型 int 数组,另外一个是封装类型 Integer 数组,有装箱开销


开销差距



一般情况下,看不出差距,只能用放大镜看一下了。



@OptIn(ExperimentalTime::class)
fun main() {

   val duration1 = measureTime {
       case1()
  }
   println(duration1)

   val duration2 = measureTime {
       case2()
  }
   println(duration2)
}

private fun case1() {
   val t = IntArray(10_000_000)
}

private fun case2() {
   val t = Array<Int>(10_000_000) { it }
}

运行结果


使用场景



  • 默认使用 IntArray,基础类型因无装箱开销而性能好,且每个元素都有默认值 0

  • 如果数组需要使用 null 值,使用 Array<Int>


StackOverflow



高赞回答,一言以蔽之。



StackOverflow Issues


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

Flutter实现微信朋友圈高斯模糊效果

1. 背景 最近一个需求改版UI视觉觉得微信朋友圈的边缘高斯模糊挺好看,然后就苦逼吭哧的尝试在Flutter实现了,来看微信朋友圈点击展开的大图效果图: 微信朋友圈高斯模糊效果大概分4部分区域实现,如下图: 居中图片为原始图,然后背景模糊全图是原始图放大c...
继续阅读 »

1. 背景


最近一个需求改版UI视觉觉得微信朋友圈的边缘高斯模糊挺好看,然后就苦逼吭哧的尝试在Flutter实现了,来看微信朋友圈点击展开的大图效果图:


image.png|400


微信朋友圈高斯模糊效果大概分4部分区域实现,如下图:
image.png


居中图片为原始图,然后背景模糊全图是原始图放大cover模式的高斯模糊,在上下两个区域分别是两层单独处理边界的高斯模糊效果特殊处理,因此有时候可以看到微信朋友圈在上下两侧有明显分界线;


2. 实践


在Flutter侧实现高斯模糊比较简单,可以直接使用系统的BackdropFilter函数实现,需要传入一个filter方式,然后对child区域进行模糊过滤;


  const BackdropFilter({
Key? key,
required this.filter,
Widget? child,
this.blendMode = BlendMode.srcOver,
}) : assert(filter != null),
super(key: key, child: child);

Flutter提供了简化ImageFiltered实现高斯模糊,代码如下:


ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Image.network(url,fit: BoxFit.cover, height: expandedHeight, width: width),
),

通过此方式,可以非常简约实现全屏高斯模糊~,现在难点是上下边界区域的边界模糊处理,这里需要使用一个ShaderMask组件,在Flutter侧ShaderMask主要是实现渐变过渡能力的;


  const ShaderMask({
Key? key,
required this.shaderCallback,
this.blendMode = BlendMode.modulate,
Widget? child,
}) : assert(shaderCallback != null),
assert(blendMode != null),
super(key: key, child: child);

其需要shaderCallback回调渐变Shader,共提供3种渐变模式:



  • RadialGradient:放射状渐变

  • LinearGradient:线性渐变

  • SweepGradient:扇形渐变


这里我们需要使用线性渐变LinearGradient从上到下的渐变过渡,代码如下:


             ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.white,
Colors.transparent
],
).createShader(bounds);
},
child: Image.network(url,
fit: BoxFit.cover, height: closeHeight, width: width),
)


就这样实现了?当我运行时候出现如下效果,效果还挺好的:


image.png


但是当我把封面图url替换了一个浅色图片,却出现如下效果,中间区域变成了黑色的,看来是我想的简单了:


image.png


分析了下Flutter线性过度源码,其将颜色进行过渡,
Color transparent = Color(0x00000000) , 而
Color white = Color(0xFFFFFFFF),可以看到除了透明度之外,需要保证颜色不要发生大变化,其实我们诉求只是需要将透明度发生渐变即可,因此将Colors.white改为Colors.black,


             ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black,
Colors.transparent
],
).createShader(bounds);
},
child: Image.network(url,
fit: BoxFit.cover, height: closeHeight, width: width),
)


出现如下效果:


image.png


这里颜色貌似符合预期,但是混合模式出现了问题,学过Android开发的一定属性如下这张BlendMode混合模式图片:


image.png


ShaderMaster默认的混合模式是BlendMode.modulate,这个我也解释不清楚:这里有一篇相关文章juejin.cn/post/684490…


这里我们将混合模式替换为BlendMode.dstIn:只显示src和dst重合部分,且src的重合部分只有不透明度有用,经过这些操作后,整体效果最后如下所示:


image.png


最后奉上完整demo的相关代码:


  Widget buildCover(BuildContext context) {
double width = MediaQuery.of(context).size.width;
double expandedHeight = 600;
double closeHeight = 300;
const String url =
'https://img.alicdn.com/imgextra/i2/O1CN01YWcPh81fbUvpcjUXp_!!6000000004025-2-tps-842-350.png';
return Container(
height: expandedHeight,
alignment: Alignment.center,
child: Stack(
children: [
ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Image.network(url,
fit: BoxFit.cover, height: expandedHeight, width: width),
),
Container(
height: expandedHeight,
alignment: Alignment.center,
child: ShaderMask(
shaderCallback: (Rect bounds) {
return const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black,
Colors.transparent
],
).createShader(bounds);
},
blendMode: BlendMode.dstIn,
child: Image.network(url,
fit: BoxFit.cover, height: closeHeight, width: width),
),
)
],
),
);
}

3. 总结


通过实践,发现Flutter实现高斯模糊BackdropFilter/ImageFiltered组件,渐变实现方式ShaderMask,此外还需要掌握图形学的BlendMode混合模式,以后在碰到类似需求时候建议直接砍了UI视觉吧~~费劲~~~~


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

年薪达到多少才适合留在北京?

07年大学毕业,最初没敢来北京,在三线城市哈尔滨找了份工作,也和我的小学同学在2008年5月12日之前确定了恋爱关系,她当时还在读本科,09年9月女朋友考研来到了北京,我们面临第一次异地,10年5月我辞掉哈尔滨的工作来到了北京,12年4月在媳妇毕业前在北京把证...
继续阅读 »

07年大学毕业,最初没敢来北京,在三线城市哈尔滨找了份工作,也和我的小学同学在2008年5月12日之前确定了恋爱关系,她当时还在读本科,09年9月女朋友考研来到了北京,我们面临第一次异地,10年5月我辞掉哈尔滨的工作来到了北京,12年4月在媳妇毕业前在北京把证领了,12年7月她研究生毕业,顺利的解决了北京的户口,2012年7月21日我们从北京回东北办婚礼。

2012年底我们攒了20万,结婚父母给了小10万,都拼西凑借了十几万,凑够首付44万,在南城的价格洼地买了套86平两居现房,13年10月5日搬进新居,10月26日摇号中标,10月29日喜提二手宝来,10月31日大宝降生。

2014年4月换了份待遇不错还不出差的工作,就是加班太狠,抓紧还债。15年想着换把房换到媳妇单位附近,媳妇上班方便一些,15年12月敲定为海淀某学区房,媳妇单位对面,一举多得。16年元旦后房子签约,发现钱不够,然后把南城的房子卖掉付了首付,手头还有点结余。此时出现了问题老人还想继续住在郊区不想进城,媳妇说不想租房,只想住自己的房子,几天几夜没怎么睡觉后我决定在南城原小区又贷款买了一套而且更大一点的。2017年初换了辆30多万的车,同年点电标又排到了,买了辆300公里的电车。2020年9月疫情后首批孩子开学,我们也搬进了海淀,2022年2月22日二宝也来了。

收入嘛,10年刚来北京时1万多点,每年都在涨,14年年薪30万+,16年套了点期权,19年离职,现在薪资又回到每月1万多点。轻轻松松的活着,媳妇工资一直都是1万多。

我想说的是,一定要在年轻的时候拼一拼,学到自己吃饭的本领,不要拿着6千的工资干着6千的活,那真是在浪费生命,因为再出来找工作可能也就能到8千。不论拿着多少钱都要全力投入工作,老板不给涨你也有跳槽的资本。还有我想说的就是车子是消耗品,代步工具,别追太高,够用就行,最好不要贷款买车,压力会变大。

作者:神的小屋
来源:http://www.zhihu.com/question/430567574/answer/2479008231


说说我本人的情况:老家河南农村,2005年本科毕业,在三线城市工作五年,2010年到北京读研,在学校认识了来自山东农村的老婆。2013年毕业后留京工作,2014年老婆博士毕业也留京工作。2014年底领证,2015年初结婚。2015年底买了一套小两居(当时北京出台了公积金可以最高贷120万的政策,我和老婆工作两年左右攒了30多万,又借了30多万,在南三环这个房价洼地买了套55平的两居室)。2016年7月儿子出生。2017年初获得新能源车指标,买了辆占号车。2019年底换了一辆续航里程更长的。2021年,孩子转年要上小学了,两居室满五年了,就换了套学校稍好一点的三居室(五年多时间,买第一套房借的钱还完了,又攒了三十多万。又借了几十万,还是在南三环,买了套68平的三居室)。

我的月工资收入,最初是6000多,陆续涨到1万多点。老婆的工资,最初是1万多,现在加上公积金有3万左右。

因此,年薪多少不重要。找到另一半最重要,即使家庭经济条件很差,只要工作稳定,人品好就可以,两个人一起慢慢奋斗 ,也挺好的。不要被太多焦虑误导。买不起大房子,可以买小房子;买不起海淀朝阳的,可以买丰台。良好的心态最重要。夫妻两个携手并肩最重要。

作者:坐看北二环
来源:http://www.zhihu.com/question/430567574/answer/2377968907

收起阅读 »

巧用摩斯密码作为调试工具的入口|vConsole 在线上的2种使用方式

web
前言在做手机端项目的时候,我们经常在测试环境使用 vConsole 作为调试工具,它大概可以做这么多事情:查看 console 日志查看网络请求查看页面 element 结构查看 Cookies、localStorage 和 SessionStorage手动执...
继续阅读 »

前言

在做手机端项目的时候,我们经常在测试环境使用 vConsole 作为调试工具,它大概可以做这么多事情:

  • 查看 console 日志

  • 查看网络请求

  • 查看页面 element 结构

  • 查看 Cookies、localStorage 和 SessionStorage

  • 手动执行 JS 命令

  • 自定义插件

除了开发人员,vConsole 对于,测试人员也很有用,测试 bug 的时候,如果测试人员能拿到 console 信息和网络请求,无疑对于帮助开发快速定位问题是很有帮助的。

那问题来了,这么好用的工具,貌似大家都是在测试环境使用的,线上就没有引入,是不想让这个大大的调试按钮影响用户的使用体验么?这个理由显然站不住脚啊,谁能保证线上不出问题呢,如果线上可以用 vConsole,也许就能帮助我们快速定位问题,鉴于此,我给大家提供 2 种比较好的方式来解决这个问题。

速点触发

防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时

这种方法的原理是利用了 函数防抖的概念,我们设置每次 600 ms 的间隔,在此间隔内的重复点击将计数总和,当达到 10或者10的倍数时,启用 vconsole 显示状态的改变;

若某次点击间隔超过 600 ms,则计数归零,从新开始;

实现代码如下:

import VConsole from "vconsole";

function handleVconsole() {
 new VConsole()
 let count = 0
 let lastClickTime = 0
 const VconsoleDom = document.getElementById("__vconsole")
 VconsoleDom.style.display = "none"

 window.addEventListener("click", function () {
   console.log(`连续点击数:${count}`)
   const nowTime = new Date().getTime()
   nowTime - lastClickTime < 600 ? count++ : (count = 0);
   lastClickTime = nowTime

   if (count > 0 && count % 10 === 0) {
     if (!VconsoleDom) return false
     const currentStatus = VconsoleDom.style.display
     VconsoleDom.style.display = currentStatus === "block" ? "none" : "block";
     count = 0
  }
});
}

实际效果


使用摩斯密码

摩尔斯电码(英語:Morse code)是一种时通时断的信号代码,通过不同的排列顺序来表达不同的英文字母数字标点符号。是由美國發明家萨缪尔·摩尔斯及其助手艾爾菲德·維爾在1836年发明。--维基百科

第一种方法虽然好用,不过貌似太简单了,可能会误触,有没有一种可以通过 click 模拟实现的复杂指令呢?没错,我想到了摩斯密码; 简单来说,我们可以通过两种「符号」用来表示字符:点(·)和划(-),或叫「滴」(dit)和「嗒」(dah),下面是常见字符、数字、标点符号的摩斯密码公式标识:


假设,我们用 SOS 这个单词来表示 vconsole 启用的指令,那么通过查询其标识映射表,可以得出 SOS 的 摩斯密码表示为 ...---...,只要执行这个指令我么就改变 vconsole 按钮的显示状态就好了;那么问题又来了,怎么表示点(·)和划(-)呢,本来我想还是用点击间隔的长短来表示,比如 600ms 内属于短间隔,表示点(·),600ms - 2000ms 内属于长间隔,表示划(-);

但是实现后发现效果不太好,实际操作这个间隔不太好控制,容易输错; 后来我想到可以了双击 dblclick 事件,我们用 click 表示点(·),dblclick表示划(-),让我们实现下看看。

function handleVconsole() {
 new VConsole();
 let sos = [];
 let lastClickTime = 0;
 let timeId;
 const VconsoleDom = document.getElementById("__vconsole");
 VconsoleDom.style.display = "none";

 window.addEventListener("click", function () {
   clearTimeout(timeId);
   const nowTime = new Date().getTime();
   const interval = nowTime - lastClickTime;
   timeId = setTimeout(() => {
     console.log("click");
     
     if (interval < 3000) {
       sos.push(".");
    }

     if (interval > 3000) {
       sos = [];
       lastClickTime = 0;
    }

     console.log(sos);
     lastClickTime = nowTime;

     if (sos.join("") === "...---...") {
       if (!VconsoleDom) return;
       const currentStatus = VconsoleDom.style.display;
       VconsoleDom.style.display =
         currentStatus === "block" ? "none" : "block";
       sos = [];
    }
  }, 300);
});

 window.addEventListener("dblclick", function () {
   console.log("dbclick");
   clearTimeout(timeId);
   const nowTime = new Date().getTime();
   const interval = nowTime - lastClickTime;

   if (interval < 3000) {
     sos.push("-");
  }

   if (interval > 3000) {
     sos = [];
     lastClickTime = 0;
  }

   console.log(sos);
   lastClickTime = nowTime;

   if (sos.join("") === "...---...") {
     if (!VconsoleDom) return;
     const currentStatus = VconsoleDom.style.display;
     VconsoleDom.style.display = currentStatus === "block" ? "none" : "block";
     sos = [];
  }
});
}

实际效果如下所示,感觉还不错,除了 SOS, 还可以用其他的单词或者数字什么的,这就大大增加了误触的难度,实现了完全的定制化。


总结

本文针对移动端线上调试问题,提出了 2 种解决方案,特别是通过摩斯密码这种方式,据我所知,实为首创,如果各位觉得有帮助和启发,请不要吝啬给个一件三连哦,这次一定~~~。

作者:Ethan_Zhou
来源:juejin.cn/post/7126434333442703367

收起阅读 »

Android抓包从未如此简单

一、情景再现: 有一天你正在王者团战里杀的热火朝天,忽然公司测试人员打你电话问为什么某个功能数据展示不出来了,昨天还好好的纳,是不是你又偷偷写bug了。。。WTF!,你会说:把你手机给我,我连上电脑看看打印的请求日志是不是接口有问题。然后吭哧吭哧搞半天看到接...
继续阅读 »

一、情景再现:



有一天你正在王者团战里杀的热火朝天,忽然公司测试人员打你电话问为什么某个功能数据展示不出来了,昨天还好好的纳,是不是你又偷偷写bug了。。。WTF!,你会说:把你手机给我,我连上电脑看看打印的请求日志是不是接口有问题。然后吭哧吭哧搞半天看到接口数据返回的格式确实不对,然后去群里丢了几句服务端人员看一下这个接口,数据有问题。然后有回去打游戏,可惜游戏早已结束,以失败告终,自己还被无情的举报禁赛了。。。人生最痛苦的事莫过于此。假如你的项目已经集成了抓包助手,并且也给其他人员介绍过如何使用,那么像这类问题根本就不需要你再来处理了,遇到数据问题他们第一时间会自己看请求数据,而你就可以安心上王者了。



二、Android抓包现状


目前常见的抓包工具有Charles、Fiddler、Wireshark等,这些或多或少都需要一些配置,略显麻烦,只适合开发及测试人员玩,如果产品也想看数据怎么办纳,别急,本文的主角登场了,你可以在项目中集成AndroidMonitor,只需两步简单配置即可实现抓包数据可视化功能,随时随地,人人都可以方便快捷的查看请求数据了。


三、效果展示



俗话说无图无真相



111.jpg


222.jpg


333.jpg


抓包pc.png


四、如何使用



抓包工具有两个依赖需要添加:monito和monitor-plugin



Demo下载体验


源码地址


1、monitor接入


添加依赖


   debugImplementation 'io.github.lygttpod:monitor:0.0.4'
复制代码

-备注: 使用debugImplementation是为了只在测试环境中引入


2、monitor-plugin接入



  1. 根目录build.gradle下添加如下依赖


    buildscript {
dependencies {
......
//monitor-plugin需要
classpath 'io.github.lygttpod:monitor-plugin:0.0.1'
}
}

复制代码

2.添加插件


    在APP的build.gradle中添加:

//插件内部会自动判断debug模式下hook到okhttp
apply plugin: 'monitor-plugin'

复制代码


原则上完成以上两步你的APP就成功集成了抓包工具,很简单有没有,如需定制化服务请看下边的个性化配置



3、 个性化配置


1、修改桌面抓包工具入口名字:在主项目string.xml中添加 monitor_app_name即可,例如:
```
<string name="monitor_app_name">XXX-抓包</string>
```
2、定制抓包入口logo图标:
```
添加 monitor_logo.png 即可
```
3、单个项目使用的话,添加依赖后可直接使用,无需初始化,库里会通过ContentProvider方式自动初始化

默认端口8080(端口号要唯一)

4、多个项目都集成抓包工具,需要对不同项目设置不同的端口和数据库名字,用来做区分

在主项目assets目录下新建 monitor.properties 文件,文件内如如下:对需要变更的参数修改即可
```
# 抓包助手参数配置
# Default port = 8080
# Default dbName = monitor_db
# ContentTypes白名单,默认application/json,application/xml,text/html,text/plain,text/xml
# Default whiteContentTypes = application/json,application/xml,text/html,text/plain,text/xml
# Host白名单,默认全部是白名单
# Default whiteHosts =
# Host黑名单,默认没有黑名单
# Default blackHosts =
# 如何多个项目都集成抓包工具,可以设置不同的端口进行访问
monitor.port=8080
monitor.dbName=app_name_monitor_db
```
复制代码

4、 proguard(默认已经添加混淆,如遇到问题可以添加如下混淆代码)


```
# monitor
-keep class com.lygttpod.monitor.** { *; }
```
复制代码

5、 温馨提示


    虽然monitor-plugin只会在debug环境hook代码,
但是release版编译的时候还是会走一遍Transform操作(空操作),
为了保险起见建议生产包禁掉此插件。

在jenkins打包机器的《生产环境》的local.properties中添加monitor.enablePlugin=false,全面禁用monitor插件
复制代码

6、如何使用



  • 集成之后编译运行项目即可在手机上自动生成一个抓包入口的图标,点击即可打开可视化页面查看网络请求数据,这样就可以随时随地的查看我们的请求数据了。

  • 虽然可以很方便的查看请求数据了但是手机屏幕太小,看起来不方便怎么办呐,那就去寻找在PC上展示的方法,首先想到的是能不能直接在浏览器里边直接看呐,这样不用安装任何程序在浏览输入一个地址就可以直接查看数据

  • PC和手机在同一局域网的前提下:直接在任意浏览器输入 手机ip地址+抓包工具设置的端口号即可(地址可以在抓包app首页TitleBar上可以看到)


五、原理介绍


①、 拦截APP的OKHTTP请求(添加拦截器处理抓包请求,使用ASM字节码插装技术实现)



  • 写一个Interceptor拦截器,获取请求及响应的数据,转化为需要的数据结构


override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (!MonitorHelper.isOpenMonitor) {
return chain.proceed(request)
}
val monitorData = MonitorData()
monitorData.method = request.method
val url = request.url.toString()
monitorData.url = url
if (url.isNotBlank()) {
val uri = Uri.parse(url)
monitorData.host = uri.host
monitorData.path = uri.path + if (uri.query != null) "?" + uri.query else ""
monitorData.scheme = uri.scheme
}
......以上为部分代码展示
}
复制代码


  • 有了拦截器就可以通过字节码插桩技术在编译期自动为OKHTTP添加拦截器了,避免了使用者自己添加拦截器的操作


        mv?.let {
it.visitVarInsn(ALOAD, 0)
it.visitFieldInsn(GETFIELD, "okhttp3/OkHttpClient\$Builder", "interceptors", "Ljava/util/List;")
it.visitFieldInsn(GETSTATIC, "com/lygttpod/monitor/MonitorHelper", "INSTANCE", "Lcom/lygttpod/monitor/MonitorHelper;")
it.visitMethodInsn(INVOKEVIRTUAL, "com/lygttpod/monitor/MonitorHelper", "getHookInterceptors", "()Ljava/util/List;", false)
it.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "addAll", "(Ljava/util/Collection;)Z", true)
it.visitInsn(POP)
}
复制代码

②、 数据保存到本地数据库(room)



  • 数据库选择官方推荐Room进行数据操作


@Dao
interface MonitorDao {
@Query("SELECT * FROM monitor WHERE id > :lastId ORDER BY id DESC")
fun queryByLastIdForAndroid(lastId: Long): LiveData<MutableList<MonitorData>>

@Query("SELECT * FROM monitor ORDER BY id DESC LIMIT :limit OFFSET :offset")
fun queryByOffsetForAndroid(limit: Int, offset: Int): LiveData<MutableList<MonitorData>>

@Query("SELECT * FROM monitor")
fun queryAllForAndroid(): LiveData<MutableList<MonitorData>>

@Query("SELECT * FROM monitor WHERE id > :lastId ORDER BY id DESC")
fun queryByLastId(lastId: Long): MutableList<MonitorData>

@Query("SELECT * FROM monitor ORDER BY id DESC LIMIT :limit OFFSET :offset")
fun queryByOffset(limit: Int, offset: Int): MutableList<MonitorData>

@Query("SELECT * FROM monitor")
fun queryAll(): MutableList<MonitorData>

@Insert
fun insert(data: MonitorData)

@Update
fun update(data: MonitorData)

@Query("DELETE FROM monitor")
fun deleteAll()
}
复制代码

③、 APP本地开启一个socket服务AndroidLocalService



  • AndroidLocalService基于NanoHttpd实现的一个本地微服务库,底层是通过socket实现,同时使用注解加上javapoet框架自动生成模版代码,这样就可以很方便的创建服务了,下边是创建服务并启动服务示例代码


   //@Service标记这是一个服务,端口号是服务器的端口号,注意端口号唯一
@Service(port = 9527)
abstract class AndroidService {

//@Page标注页面类,打开指定h5页面
@Page("index")
fun getIndexFileName() = "test_page.html"

//@Get注解在方法上边
@Get("query")
fun query(aaa: Boolean, bbb: Double, ccc: Float, ddd: String, eee: Int,): List<String> {
return listOf("$aaa", "$bbb", "$ccc", "$ddd", "$eee")
}

@Get("saveData")
fun saveData(content: String) {
LiveDataHelper.saveDataLiveData.postValue(content + UUID.randomUUID());
}

@Get("queryAppInfo")
fun getAppInfo(): HashMap<String, Any> {
return hashMapOf(
"applicationId" to BuildConfig.APPLICATION_ID,
"versionName" to BuildConfig.VERSION_NAME,
"versionCode" to BuildConfig.VERSION_CODE,
"uuid" to UUID.randomUUID(),
)
}
}

//初始化
ALSHelper.init(this)
//启动服务
ALSHelper.startService(ServiceConfig(AndroidService::class.java))


然后就可以通过 ip地址 + 端口号 访问了,例如:http://172.18.41.157:9527/index

复制代码


使用AndroidLocalService之后创建和启动服务就是这么简单有没有,具体用法及细节请查看其说明文档



④、 与本地socket服务通信



  • 剩下的就是与服务器的通信了,无论使用前端使用aJax还是客户端使用okhttp都可以正常请求数据了


⑤、 UI展示数据(手机端和PC端)



  • 有了接口和数据具体展示就看可以随意定制了,如果你不喜欢默认的UI风格,那就拉源码自己定制UI哦



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

Python办公软件自动化,5分钟掌握openpyxl操作

今天给大家分享一篇用openpyxl操作Excel的文章。各种数据需要导入Excel?多个Excel要合并?目前,Python处理Excel文件有很多库,openpyxl算是其中功能和性能做的比较好的一个。接下来我将为大家介绍各种Excel操作。打开Excel...
继续阅读 »

今天给大家分享一篇用openpyxl操作Excel的文章。

各种数据需要导入Excel?多个Excel要合并?目前,Python处理Excel文件有很多库,openpyxl算是其中功能和性能做的比较好的一个。接下来我将为大家介绍各种Excel操作。

打开Excel文件

新建一个Excel文件


打开现有Excel文件


打开大文件时,根据需求使用只读或只写模式减少内存消耗。


获取、创建工作表

获取当前活动工作表:


创建新的工作表:


使用工作表名字获取工作表:


获取所有的工作表名称:


保存

保存到流中在网络中使用:



单元格
单元格位置作为工作表的键直接读取:


为单元格赋值:


多个单元格 可以使用切片访问单元格区域:


使用数值格式:


使用公式:


合并单元格时,除左上角单元格外,所有单元格都将从工作表中删除:


行、列
可以单独指定行、列、或者行列的范围:


可以使用Worksheet.iter_rows()方法遍历行:


同样的Worksheet.iter_cols()方法将遍历列:


遍历文件的所有行或列,可以使用Worksheet.rows属性:


Worksheet.columns属性:


使用Worksheet.append()或者迭代使用Worksheet.cell()新增一行数据:


插入操作比较麻烦。可以使用Worksheet.insert_rows()插入一行或几行:


Worksheet.insert_cols()操作类似。Worksheet.delete_rows()Worksheet.delete_cols()用来批量删除行和列。

只读取值
使用Worksheet.values属性遍历工作表中的所有行,但只返回单元格值:


Worksheet.iter_rows()Worksheet.iter_cols()可以设置values_only参数来仅返回单元格的值:



作者:Sinchard | 来源:python中文社区

收起阅读 »

Android ViewModelScope 如何自动取消协程

先看一下 ViewModel 中的 ViewModelScope 是何方神圣 val ViewModel.viewModelScope: CoroutineScope get() { val scope: Corouti...
继续阅读 »

先看一下 ViewModel 中的 ViewModelScope 是何方神圣


val ViewModel.viewModelScope: CoroutineScope
get() {
val scope: CoroutineScope? = this.getTag(JOB_KEY)
if (scope != null) {
return scope
}
return setTagIfAbsent(JOB_KEY,
CloseableCoroutineScope(SupervisorJob() + Dispatchers.Main.immediate))
}

可以看到这个是一个扩展方法,


再点击 setTagIfAbsent 方法进去


 <T> T setTagIfAbsent(String key, T newValue) {
T previous;
synchronized (mBagOfTags) {
previous = (T) mBagOfTags.get(key);//第一次肯定为null
if (previous == null) {
mBagOfTags.put(key, newValue);//null 存储
}
}
T result = previous == null ? newValue : previous;
if (mCleared) {//判断是否已经clear了
// It is possible that we'll call close() multiple times on the same object, but
// Closeable interface requires close method to be idempotent:
// "if the stream is already closed then invoking this method has no effect." (c)
closeWithRuntimeException(result);
}
return result;
}

可以看到 这边 会把 我们的 ViewModel 存储到 ViewModel 内的 mBagOfTags 中


这个 mBagOfTags 是


    private final Map<String, Object> mBagOfTags = new HashMap<>();

这个时候 我们 viewModel 就会持有 我们 viewModelScope 的协程 作用域了。


那..这也只是 表述了 我们 viewModelScope 存在哪里而已,


什么时候清除呢?


先看一下 ViewModel 的生命周期



可以看到 ViewModel 的生命周期 会在 Activity onDestory 之后会被调用。


那...具体哪里调的?


翻看源码可以追溯到 ComponentActivity 的默认构造器内


 public ComponentActivity() {
/*省略一些*/
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});
}

可以看到内部会通对 Lifecycle 添加一个观察者,观察当前 Activity 的生命周期变更事件,如果走到了 Destory ,并且 本次 Destory 并非由于配置变更引起的,才会真正调用 ViewModelStore 的 clear 方法。


跟进 clear 方法看看


public class ViewModelStore {

private final HashMap<String, ViewModel> mMap = new HashMap<>();

/**
* Clears internal storage and notifies ViewModels that they are no longer used.
*/
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}
}

可以看到这个 ViewModelStore 内部实现 用 HashMap 存储 ViewModel


于是在 clear 的时候,会逐个遍历调用 clear方法


再次跟进 ViewModel 的 clear 方法


 @MainThread
final void clear() {
mCleared = true;
// Since clear() is final, this method is still called on mock objects
// and in those cases, mBagOfTags is null. It'll always be empty though
// because setTagIfAbsent and getTag are not final so we can skip
// clearing it
if (mBagOfTags != null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
// see comment for the similar call in setTagIfAbsent
closeWithRuntimeException(value);
}
}
}
onCleared();
}

可以发现我们最初 存放 viewmodelScope 的 mBagOfTags


这里面的逻辑 就是对 mBagOfTags 存储的数据 挨个提取出来并且调用 closeWithRuntimeException


跟进 closeWithRuntimeException


 private static void closeWithRuntimeException(Object obj) {
if (obj instanceof Closeable) {
try {
((Closeable) obj).close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

该方法内会逐个判断 对象是否实现 Closeable 如果实现就会调用这个接口的 close 方法,


再回到最初 我们 viewModel 的扩展方法那边,看看我们 viewModelScope 的真正面目


internal class CloseableCoroutineScope(context: CoroutineContext) 
: Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context

override fun close() {
coroutineContext.cancel()
}
}

可以明确的看到 我们的 ViewModelScope 实现了 Closeable 并且充写了 close 方法,


close 方法内的实现 会对 协程上下文进行 cancel。


至此我们 可以大致整理一下



  1. viewModelScope 是 ViewModel 的扩展成员,该对象是 CloseableCoroutineScope,并且实现了 Closeable 接口

  2. ViewModelScope 存储在 ViewModel 的 名叫 mBagOfTags 的HashMap中 啊

  3. ViewModel 存储在 Activity 的 ViewModelStore 中,并且会监听 Activity 的 Lifecycle 的状态变更,在ON_DESTROY 且 非配置变更引起的事件中 对 viewModelStore 进行清空

  4. ViewModelStore 清空会对 ViewModelStore 内的所有 ViewModel 逐个调用 clear 方法。

  5. ViewModel的clear方法会对 ViewModel的 mBagOfTags 内存储的对象进行调用 close 方法(该对象需实现Closeable 接口)

  6. 最终会会调用 我们 ViewModelScope 的实现类 CloseableCoroutineScope 的 close 方法中。close 方法会对协程进行 cancel。

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

Android 12新功能:使用SplashScreen优化启动体验

前言由于很多应用在启动时需要进行一些初始化事务,导致在启动应用时有一定的空白延迟,在之前我们一般的做法是通过替换 android:windowBackground 的自定义主题,使应用启动时及时显示一张默认图片来改善启动体验。在Androi...
继续阅读 »

前言

由于很多应用在启动时需要进行一些初始化事务,导致在启动应用时有一定的空白延迟,在之前我们一般的做法是通过替换 android:windowBackground 的自定义主题,使应用启动时及时显示一张默认图片来改善启动体验。

在Android 12中,官方添加了SplashScreen API,它可为所有应用启用新的应用启动界面。新的启动界面是瞬时显示的,所以就不必再自定义android:windowBackground 了。新启动页面的样式默认是正中显示应用图标,但是允许我们自定义,以便应用能够保持其独特的品牌。下面我们来看看如何使用它。

启动画面实现

其实在Android 12上已经默认使用了SplashScreen,如果没有任何配置,会自动使用App图标。

当然也允许自定义启动画面,在value-v31中的style.xml中,可以在App的主Theme中通过如下属性来进行配置:

<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
<item name="android:windowSplashScreenBackground">@android:color/white</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/anim_ai_loading</item>
<item name="android:windowSplashScreenAnimationDuration">1000</item>
<item name="android:windowSplashScreenBrandingImage">@mipmap/brand</item>
</style>
  • windowSplashScreenBackground设置启动画面的背景色

  • windowSplashScreenAnimatedIcon启动图标。就是显示在启动界面中间的图片,也可以是动画

  • windowSplashScreenAnimationDuration设置动画的长度。注意这里最大只能1000ms,如果需要动画时间更长,则需要通过代码的手段让启动画面在屏幕上显示更长时间(下面会讲到)

  • windowSplashScreenIconBackground设置启动图标的背景色

  • windowSplashScreenBrandingImage设置要显示在启动画面底部的图片。官方设计准则建议不要使用品牌图片。

运行启动应用就可以看到新的启动画面了,如下: 屏幕录制2022-01-19 上午10.gif

动画的元素

在Android 12上,显示在启动界面中间的图片会有一个圆形遮罩,所以在设计图片或动画的时候一定要注意,比如上面我的例子,动画其实就没有显示完整。对此官方给了详细的设计指导,如下:

image.png

  • 应用图标 (1) 应该是矢量可绘制对象,它可以是静态或动画形式。虽然动画的时长可以不受限制,但我们建议让其不超过 1000 毫秒。默认情况下,使用启动器图标。
  • 图标背景 (2) 是可选的,在图标与窗口背景之间需要更高的对比度时很有用。如果您使用一个自适应图标,当该图标与窗口背景之间的对比度足够高时,就会显示其背景。
  • 与自适应图标一样,前景的 ⅓ 被遮盖 (3)。
  • 窗口背景 (4) 由不透明的单色组成。如果窗口背景已设置且为纯色,则未设置相应的属性时默认使用该背景。

启动时长

默认当应用绘制第一帧后,启动画面会立即关闭。但是在我们实际使用中,一般在启动时进行一些初始化操作,另外大部分应用会请求启动广告,这样其实需要一些耗时的。通常情况下,这些耗时操作我们会进行异步处理,那么是否可以让启动画面等待这些初始化完成后才关闭?

我们可以使用 ViewTreeObserver.OnPreDrawListener让应用暂停绘制第一帧,直到一切准备就绪才开始,这样就会让启动画面停留更长的时间,如下:

...
var isReady = false
...

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
...

val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.addOnPreDrawListener(
object : ViewTreeObserver.OnPreDrawListener {
override fun onPreDraw(): Boolean {
return if (isReady) {
content.viewTreeObserver.removeOnPreDrawListener(this)
true
} else {
false
}
}
}
)
}

这样当初始化等耗时操作完成后,将isReady置为true即可关闭启动画面进入应用。

上面我们提到配置启动动画的时长最多只能是1000ms,但是通过上面的代码可以让启动画面停留更长时间,所以动画的展示时间也就更长了。

关闭动画

启动画面关闭时默认直接消失,当然我们也可以对其进行自定义。

在Activity中可以通过getSplashScreen来获取(注意判断版本,低版本中没有这个函数,会crash),然后通过它的setOnExitAnimationListener来定义关闭动画,如下:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
splashScreen.setOnExitAnimationListener { splashScreenView ->
val slideUp = ObjectAnimator.ofFloat(
splashScreenView,
View.TRANSLATION_Y,
0f,
-splashScreenView.height.toFloat()
)
slideUp.interpolator = AnticipateInterpolator()
slideUp.duration = 200L
//这里doOnEnd需要Android KTX库,即androidx.core:core-ktx:1.7.0
slideUp.doOnEnd { splashScreenView.remove() }
slideUp.start()
}
}

加上如上代码后,本来直接消失的启动画面就变成了向上退出了。

这里可以通过splashScreenView可以获取到启动动画的时长和开始时间,如下:

val animationDuration = splashScreenView.iconAnimationDurationMillis
val animationStart = splashScreenView.getIconAnimationStartMillis

这样就可以计算出启动动画的剩余时长。

顺便吐槽一下官网这里代码错了,开始时间也用了iconAnimationDurationMillis来获取,实际上应该是getIconAnimationStartMillis

低版本使用SplashScreen

只能在Android 12上体验官方的启动动画,显然不能够啊!官方提供了Androidx SplashScreen compat库,能够向后兼容,并可在所有 Android 版本上显示外观和风格一致的启动画面(这点我保留意见)。

首先要升级compileSdkVersion,并依赖SplashScreen库,如下:

android {
compileSdkVersion 31
...
}
dependencies {
...
implementation 'androidx.core:core-splashscreen:1.0.0-alpha01'
}

然后在style.xml添加代码如下:

<style name="Theme.App.Starting" parent="Theme.SplashScreen">
// Set the splash screen background, animated icon, and animation duration.
<item name="windowSplashScreenBackground">@android:color/white</item>

// Use windowSplashScreenAnimatedIcon to add either a drawable or an
// animated drawable. One of these is required.
<item name="windowSplashScreenAnimatedIcon">@drawable/anim_ai_loading</item>
<item name="windowSplashScreenAnimationDuration">1000</item> # Required for
# animated icons

// Set the theme of the Activity that directly follows your splash screen.
<item name="postSplashScreenTheme">@style/AppTheme</item> # Required.
</style>

前三个我们上面都介绍过了,这里新增了一个postSplashScreenTheme,它应该设置为应用的原主题,这样会将这个主题设置给启动画面之后的Activity,这样就可以保持样式的不变。

注意上面提到的windowSplashScreenIconBackgroundwindowSplashScreenBrandingImage没有,这是与Android12的不同之一。

然后我们将这个style设置给Application或Activity即可:

<manifest>
<application android:theme="@style/Theme.App.Starting">
<!-- or -->
<activity android:theme="@style/Theme.App.Starting">
...

最后需要在启动activity中,先调用installSplashScreen,然后才能调用setContentView,如下

class MainActivity : ComponentActivity() {

   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       val splashScreen = installSplashScreen()
       setContentView(R.layout.activity_main)
...

然后在低版本系统上启动应用就可以看到启动画面了。

installSplashScreen这一步很重要,如果没有这一行代码,postSplashScreenTheme就无法生效,这样启动画面后Activity就无法使用之前的样式,严重的会造成崩溃。比如在Activity中存在AppCompat组件,这就需要使用AppCompat样式,否则就会Crash。

最后注意在Android 12上依然有圆形遮罩,所以需要遵循官方的设计准则;但是在低版本系统上则没发现有这个遮罩,而且在低版本上动画无效,只会显示第一帧的画面,所以我对官方说的风格一致保留意见。

现有启动画面迁移

目前市场上的App基本都自己实现了启动页面,如果直接添加SplashScreen,就会造成重复,所以我们需要对原有启动页面进行处理。具体处理还要根据每个App自己的启动页面的实现逻辑来定,这里官方给出了一些意见,大家可以参考一下:将现有的启动画面实现迁移到 Android 12 及更高版本

总结

官方的SplashScreen有点姗姗来迟,不过效果还是不错的,使用起来也非常简单,但是一定要注意版本。虽然Androidx SplashScreen compat库可以向后兼容,但是与Android 12上还是有一些不同。


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

收起阅读 »

WebView初体验【Android】

每天认真洗脸,多读书,按时睡,少食多餐。变得温柔,大度,继续善良,保持爱心。不在人前矫情,四处诉说以求宽慰,而是学会一个人静静面对,自己把道理想通。这样的你,单身也所谓啊,你在那么虔诚地做更好的自己,一定会遇到最好的,而那个人也一定值得你所有等待。 在We...
继续阅读 »

每天认真洗脸,多读书,按时睡,少食多餐。变得温柔,大度,继续善良,保持爱心。不在人前矫情,四处诉说以求宽慰,而是学会一个人静静面对,自己把道理想通。这样的你,单身也所谓啊,你在那么虔诚地做更好的自己,一定会遇到最好的,而那个人也一定值得你所有等待。



书客创作


在WebView没有出现之前,如果要访问一个网页只能通过打开手机内的浏览器,通过浏览器来加载网页,但是打开浏览器的同时,也脱离了当前的应用软件,这样就大大的降低了网页与应用软件的交互。随着Android SDK的不断升级,官方提供一个WebView控件,专门用于加载网页并实现交互。那么到底WebView是什么?又该如何使用呢?


什么是WebView?

简单来说WebView是移动端用于加载Web页面的控件。


怎么使用WebView?

1、移动端加载网页方式


A、通过打开浏览器访问网页


String weburl ="http://www.baidu.com/";
Uri uri = Uri.parse(weburl);// weburl网址
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
startActivity(intent);

B、通过WebView打开本地网页


WebView.loadUrl("file:///android_asset/baidu.html");

注意1:本地文件放在assets文件中,assets文件是main的子文件,与res文件同级。
注意2:设置WebView支持加载本地文件。


WebSettings webSettings = webView.getSettings();
// 允许加载Assets和resources文件
webSettings.setAllowFileAccess(true);

本地baidu.html代码


C、通过WebView加载网址


webView.loadUrl("http://www.baidu.com/");

加载网址,需要在清单文件中加上网络请求权限


<uses-permission android:name="android.permission.INTERNET"/>

当WebView加载失败时,可以使用webView.reload();来重新加载。
注意:当加载完网页之后,如果发现网页无法点击,这很可能是WebView没有获取焦点。


webView.requestFocus();// 使页面获取焦点,防止点击无响应

2、WebView基本属性设置


WebView提供很多属性,需要通过WebSettings来进行设置,下面是对一些常用属性进行设置。


// 设置WebView相关属性
WebSettings webSettings = webView.getSettings();
// 是否缓存表单数据
webSettings.setSaveFormData(false);
// 设置WebView 可以加载更多格式页面
webSettings.setLoadWithOverviewMode(true);
// 设置WebView使用广泛的视窗
webSettings.setUseWideViewPort(true);
// 支持2.2以上所有版本
webSettings.setPluginState(WebSettings.PluginState.ON);
// 允许加载Assets和resources文件
webSettings.setAllowFileAccess(true);
// 告诉webview启用应用程序缓存api
webSettings.setAppCacheEnabled(true);
// 排版适应屏幕
webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
// 支持插件
webSettings.setPluginState(WebSettings.PluginState.ON);
// 设置是否启用了DOM storage AP搜索I
webSettings.setDomStorageEnabled(true);
// 设置缓存,默认不使用缓存-有缓存,使用缓存
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
// 是否允许缩放
webSettings.setSupportZoom(false);
// 是否支持通过js打开新的窗口
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
// 允许加载JS
webSettings.setJavaScriptEnabled(true);

// 隐藏滚动条
webView.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY);

3、WebView默认是通过浏览器打开网页,如何使用WebView打开网页?


WebViewClient是WebView的一个重要属性,它不仅仅能够实现WebView打开网页,而且还能够实现URL重构等功能。


// WebView默认是通过浏览器打开url,使用url在WebView中打开
webView.setWebViewClient(new WebViewClient() {
// // 旧版本
// @Override
// public boolean shouldOverrideUrlLoading(WebView view, String url) {
// 使url在WebView中打开,在这里可以进行重构url
// webView.loadUrl(url);
// return true;
// }

// 新版本
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
// 返回false,意味着请求过程中,不管有多少次的跳转请求(即新的请求地址),均交给webView自己处理,这也是此方法的默认处理
// 返回true,说明你自己想根据url,做新的跳转,比如在判断url符合条件的情况下,我想让webView加载http://baidu.com/
// 加载Url,使网页在WebView中打开,在这里可以进行重构url
if(Build.VERSION.SDK_INT>= Build.VERSION_CODES.LOLLIPOP) {
webView.loadUrl(request.getUrl().toString());
}
return true;
}

// WebViewClient帮助WebView去处理页面控制和请求通知
@Override
public void onLoadResource(WebView view, String url) {
super.onLoadResource(view, url);
}

// 错误代码处理,一般是加载本地Html页面,或者使用TextView显示错误
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
// 当网页加载出错时,加载本地错误文件
// webView.loadUrl("file:///android_asset/error.html");
}

// 页面开始加载-例如在这里开启进度条
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
}

// 页面加载结束,一般用来加载或者执行javaScript脚本
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
}
});

4、设置WebView的WebChromeClient属性


WebChromeClient是WebView中一个非常重要的属性,使用它可以监听网页加载的进度,获取网页主题等信息。


// 监听网页加载进度
webView.setWebChromeClient(new WebChromeClient() {
// 网页Title信息
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
}

// 监听网页alert方法
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super(view, url, message, result);
}

// 显示网页加载进度
@Override
public void onProgressChanged(WebView view, int newProgress) {
// newProgress 1-100
}
});

5、WebView中使用JavaScript


WebView与网页的交互大多数是使用JavaScript来实现


//设置WebView支持JavaScript
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);

6、下载文件监听


// 下载文件
webView.setDownloadListener(new DownloadListener() {
@Override
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
// url下载文件地址
// 处理下载文件逻辑
}
});

7、后退与前进


// 返回键监听
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
if (webView.canGoBack())
// 判断WebView是否能够返回,能-返回
webView.canGoBack();
else
finish();
return true;
}
return super.onKeyDown(keyCode, event);
}

8、WebView优化-缓存


//设置缓存,默认不使用缓存
webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);//有缓存,使用缓存
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);//不使用缓存

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

普通的加载千篇一律,有趣的 loading 万里挑一

前言在网络速度较慢的场景,一个有趣的加载会提高用户的耐心和对 App 的好感,有些 loading 动效甚至会让用户有想弄清楚整个动效过程到底是怎么样的冲动。然而,大部分的 App的 loading 就是下面这种千篇一律...
继续阅读 »

前言

在网络速度较慢的场景,一个有趣的加载会提高用户的耐心和对 App 的好感,有些 loading 动效甚至会让用户有想弄清楚整个动效过程到底是怎么样的冲动。然而,大部分的 App的 loading 就是下面这种千篇一律的效果 —— 俗称“转圈”。

loading-ios.gif

loading-android.gif

本篇我们利用Flutter 的 PathMetric来玩几个有趣的 loading 效果。

效果1:圆环内滚动的球

加载圆形球动画.gif

如上图所示,一个红色的小球在蓝色的圆环内滚动,而且在往上滚动的时候速度慢,往下滚动的时候有个明显的加速过程。这个效果实现的思路如下:

  • 绘制一个蓝色的圆环,在蓝色的圆环内构建一个半径更小一号的圆环路径(Path)。
  • 让红色小球在动画控制下沿着内部的圆环定义的路径运动。
  • 选择一个中间减速(上坡)两边加速的动画曲线。

下面是实现代码:

// 动画控制设置
controller =
AnimationController(duration: const Duration(seconds: 3), vsync: this);
animation = Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
parent: controller,
curve: Curves.slowMiddle,
))
..addListener(() {
setState(() {});
});

// 绘制和动画控制方法
_drawLoadingCircle(Canvas canvas, Size size) {
var paint = Paint()..style = PaintingStyle.stroke
..color = Colors.blue[400]!
..strokeWidth = 2.0;
var path = Path();
final radius = 40.0;
var center = Offset(size.width / 2, size.height / 2);
path.addOval(Rect.fromCircle(center: center, radius: radius));
canvas.drawPath(path, paint);

var innerPath = Path();
final ballRadius = 4.0;
innerPath.addOval(Rect.fromCircle(center: center, radius: radius - ballRadius));
var metrics = innerPath.computeMetrics();
paint.color = Colors.red;
paint.style = PaintingStyle.fill;
for (var pathMetric in metrics) {
var tangent = pathMetric.getTangentForOffset(pathMetric.length * animationValue);
canvas.drawCircle(tangent!.position, ballRadius, paint);
}
}

效果2:双轨运动

双轨运动.gif

上面的实现效果其实比较简单,就是绘制了一个圆和一个椭圆,然后让两个实心圆沿着路径运动。因为有了这个组合效果,趣味性增加不少,外面的椭圆看起来就像是一条卫星轨道一样。实现的逻辑如下:

  • 绘制一个圆和一个椭圆,二者的中心点重合;
  • 在圆和椭圆的路径上分别绘制一个小的实心圆;
  • 通过动画控制实心圆沿着大圆和椭圆的路径上运动。

具体实现的代码如下所示。

controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOutSine,
))
..addListener(() {
setState(() {});
});

_drawTwinsCircle(Canvas canvas, Size size) {
var paint = Paint()
..style = PaintingStyle.stroke
..color = Colors.blue[400]!
..strokeWidth = 2.0;

final radius = 50.0;
final ballRadius = 6.0;
var center = Offset(size.width / 2, size.height / 2);
var circlePath = Path()
..addOval(Rect.fromCircle(center: center, radius: radius));
paint.style = PaintingStyle.stroke;
paint.color = Colors.blue[400]!;
canvas.drawPath(circlePath, paint);

var circleMetrics = circlePath.computeMetrics();
for (var pathMetric in circleMetrics) {
var tangent = pathMetric
.getTangentForOffset(pathMetric.length * animationValue);

paint.style = PaintingStyle.fill;
paint.color = Colors.blue;
canvas.drawCircle(tangent!.position, ballRadius, paint);
}

paint.style = PaintingStyle.stroke;
paint.color = Colors.green[600]!;
var ovalPath = Path()
..addOval(Rect.fromCenter(center: center, width: 3 * radius, height: 40));
canvas.drawPath(ovalPath, paint);
var ovalMetrics = ovalPath.computeMetrics();

for (var pathMetric in ovalMetrics) {
var tangent =
pathMetric.getTangentForOffset(pathMetric.length * animationValue);

paint.style = PaintingStyle.fill;
canvas.drawCircle(tangent!.position, ballRadius, paint);
}
}

效果3:钟摆运动

钟摆球动画.gif 钟摆运动的示意图如下所示,一条绳子系着一个球悬挂某处,把球拉起一定的角度释放后,球就会带动绳子沿着一条圆弧来回运动,这条圆弧的半径就是绳子的长度。 钟摆示意图.png 这个效果通过代码来实现的话,需要做下面的事情:

  • 绘制顶部的横线,代表悬挂的顶点;
  • 绘制运动的圆弧路径,以便让球沿着圆弧运动;
  • 绘制实心圆代表球,并通过动画控制沿着一条圆弧运动;
  • 用一条顶端固定,末端指向球心的直线代表绳子;
  • 当球运动到弧线的终点后,通过动画反转(reverse)控制球 返回;到起点后再正向(forward) 运动就可以实现来回运动的效果了。

具体实现的代码如下,这里在绘制球的时候给 Paint 对象增加了一个 maskFilter 属性,以便让球看起来发光,更加好看点。

controller =
AnimationController(duration: const Duration(seconds: 2), vsync: this);
animation = Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOutQuart,
))
..addListener(() {
setState(() {});
}
..addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});

_drawPendulum(Canvas canvas, Size size) {
var paint = Paint()
..style = PaintingStyle.stroke
..color = Colors.blue[400]!
..strokeWidth = 2.0;

final ceilWidth = 60.0;
final pendulumHeight = 200.0;
var ceilCenter =
Offset(size.width / 2, size.height / 2 - pendulumHeight / 2);
var ceilPath = Path()
..moveTo(ceilCenter.dx - ceilWidth / 2, ceilCenter.dy)
..lineTo(ceilCenter.dx + ceilWidth / 2, ceilCenter.dy);
canvas.drawPath(ceilPath, paint);

var pendulumArcPath = Path()
..addArc(Rect.fromCircle(center: ceilCenter, radius: pendulumHeight),
3 * pi / 4, -pi / 2);

paint.color = Colors.white70;
var metrics = pendulumArcPath.computeMetrics();

for (var pathMetric in metrics) {
var tangent =
pathMetric.getTangentForOffset(pathMetric.length * animationValue);

canvas.drawLine(ceilCenter, tangent!.position, paint);
paint.style = PaintingStyle.fill;
paint.color = Colors.blue;
paint.maskFilter = MaskFilter.blur(BlurStyle.solid, 4.0);
canvas.drawCircle(tangent.position, 16.0, paint);
}
}

总结

本篇介绍了三种 Loading 动效的绘制逻辑和实现代码,可以看到利用路径属性进行绘图以及动画控制可以实现很多有趣的动画效果。


作者:岛上码农
链接:https://juejin.cn/post/7125029565625270286
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

女学霸考 692 分想当“程序媛”,网友:快劝劝孩子

当记者问道这个专业男孩子比较多时,女孩女王式发言:也没见男生考得比我好,一时间引起大家热议,下面就来一起了解一下吧。据了解,今年四川理科一本线为 521 分,这名女学霸超出 171 分之多。首先我们从各大厂校招网申数量上看(盘了字节跳动、网易、快手、三七互娱、...
继续阅读 »

近日四川成都一女学霸高考分数 692 分,直言想当程序员。

当记者问道这个专业男孩子比较多时,女孩女王式发言:也没见男生考得比我好,一时间引起大家热议,下面就来一起了解一下吧。

女孩考 692 分想当程序员


6月26日,四川成都。女学霸高考考了 692 分,其中数学成绩为 149 分,最后一道大题一个小细节扣了一分。


坦言想报考复旦大学,学电子信息类工科专业,未来要做一名“程序猿”。





当记者开玩笑提到“掉头发”和“行业里男生较多”时,女孩更是霸气发言,“家里遗传的头发比较好。程序员行业女生没有什么问题,我们学校也没有男生考得比我好”。

据了解,今年四川理科一本线为 521 分,这名女学霸超出 171 分之多。

有网友评论羡慕的同时也想劝劝孩子。


引人注意的是,记者的提问也引起了不少网友反感。毕竟程序员一开始从业者都是女性,而且当今世界上最伟大程序员排名第一位的也是女性。


那么计算机技术哪家高校强呢?

首先我们从各大厂校招网申数量上看(盘了字节跳动、网易、快手、三七互娱、金山云、浪潮集团),以下学校均排在投递数量前列:

华中科技大学、北京邮电大学、西安电子科技大学、电子科技大学、哈尔滨工业大学、东北大学、武汉大学、上海交通大学、南京大学。


当程序员包括的专业类型可以有计算机专业、软件开发专业、电子信息专业、通信专业、软件工程等,程序员的范围很广,主要包括软件设计/开发和程序编码两大类。

来源:mp.weixin.qq.com/s/vxb3c_5C-Ap_9NGRMeGltA

收起阅读 »

uniapp项目优化方式及建议

1.复杂页面数据区域封装成组件例如项目里包含类似论坛页面:点击一个点赞图标,赞数要立即+1,会引发页面级所有的数据从js层向视图层的同步,造成整个页面的数据更新,造成点击延迟卡顿对于复杂页面,更新某个区域的数据时,需要把这个区域做成组件,这样更新数据时就只更新...
继续阅读 »

介绍:性能优化自古以来就是重中之重,关于uniapp项目优化方式最全整理,会根据开发情况进行补充

1.复杂页面数据区域封装成组件

场景

例如项目里包含类似论坛页面:点击一个点赞图标,赞数要立即+1,会引发页面级所有的数据从js层向视图层的同步,造成整个页面的数据更新,造成点击延迟卡顿

优化方案

对于复杂页面,更新某个区域的数据时,需要把这个区域做成组件,这样更新数据时就只更新这个组件

注:app-nvue和h5不存在此问题;造成差异的原因是小程序目前只提供了组件差量更新的机制,不能自动计算所有页面差量

2.避免使用大图

场景

页面中若大量使用大图资源,会造成页面切换的卡顿,导致系统内存升高,甚至白屏崩溃;对大体积的二进制文件进行 base64 ,也非常耗费资源

优化方案

图片请压缩后使用,避免大图,必要时可以考虑雪碧图或svg,简单代码能实现的就不要图片

3.小程序、APP分包处理pages过多

前往官网手册查看配置

4.图片懒加载

功能描述

此功能只对微信小程序、App、百度小程序、字节跳动小程序有效,默认开启

前往uView手册查看配置

5.禁止滥用本地存储

不要滥用本地存储,局部页面之间的传参用url,如果用本地存储传递数据要命名规范和按需销毁

6.可在外部定义变量

在 uni-app 中,定义在 data 里面的数据每次变化时都会通知视图层重新渲染页面;所以如果不是视图所需要的变量,可以不定义在 data 中,可在外部定义变量或直接挂载在 vue实例 上,以避免造成资源浪费

7.分批加载数据优化页面渲染

场景

页面初始化时,逻辑层一次性向视图层传递很大的数据,使视图层一次性渲染大量节点,可能造成通讯变慢、页面切换卡顿

优化方案

以局部更新页面的方式渲染页面;如:服务端返回 100条数据 ,可进行分批加载,一次加载 50条 , 500ms 后进行下一次加载

8.避免视图层和逻辑层频繁进行通讯

  1. 减少 scroll-view 组件的 scroll 事件监听,当监听 scroll-view 的滚动事件时,视图层会频繁的向逻辑层发送数据

  2. 监听 scroll-view 组件的滚动事件时,不要实时的改变 scroll-top / scroll-left 属性,因为监听滚动时,视图层向逻辑层通讯,改变 scroll-top / scroll-left 时,逻辑层又向视图层通讯,这样就可能造成通讯卡顿

  3. 注意 onPageScroll 的使用, onPageScroll 进行监听时,视图层会频繁的向逻辑层发送数据

  4. 多使用 css动画 ,而不是通过js的定时器操作界面做动画

  5. 如需在 canvas 里做跟手操作, app端 建议使用 renderjs ,小程序端建议使用 web-view 组件; web-view 里的页面没有逻辑层和视图层分离的概念,自然也不会有通信折损

9.CSS优化

要知道哪些属性是有继承效果的,像字体、字体颜色、文字大小都是继承的,禁止没有意义的重复代码

10.善用节流和防抖

防抖

等待n秒后执行某函数,若等待期间再次被触发,则等待时间重新初始化

节流

触发事件n秒内只执行一次,n秒未过,再次触发无效

11.优化页面切换动画

场景

页面初始化时存在大量图片或原生组件渲染和大量数据通讯,会发生新页面渲染和窗体进入动画抢资源,造成页面切换卡顿、掉帧

优化方案

  1. 建议延时 100ms~300ms 渲染图片或复杂原生组件,分批进行数据通讯,以减少一次性渲染的节点数量

  2. App 端动画效果可以自定义; popin/popout 的双窗体联动挤压动画效果对资源的消耗更大,如果动画期间页面里在执行耗时的js,可能会造成动画掉帧;此时可以使用消耗资源更小的动画效果,比如 slide-in-right / slide-out-right

  3. App-nvue 和 H5 ,还支持页面预载,uni.preloadPage,可以提供更好的使用体验

12.优化背景色闪白

场景

进入新页面时背景闪白,如果页面背景是深色,在vue页面中可能会发生新窗体刚开始动画时是灰白色背景,动画结束时才变为深色背景,造成闪屏

优化方案

  1. 将样式写在 App.vue 里,可以加速页面样式渲染速度; App.vue 里面的样式是全局样式,每次新开页面会优先加载 App.vue 里面的样式,然后加载普通 vue 页面的样式

  2. app端 还可以在 pages.json 的页面的 style 里单独配置页面原生背景色,比如在 globalStyle->style->app-plus->background 下配置全局背景色

"style": { "app-plus": { "background":"#000000" } }
  1. nvue页面不存在此问题,也可以更改为nvue页面

13.优化启动速度

  1. 工程代码越多,包括背景图和本地字体文件越大,对小程序启动速度有影响,应注意控制体积

  2. App端的 splash 关闭有白屏检测机制,如果首页一直白屏或首页本身就是一个空的中转页面,可能会造成 splash 10秒才关闭

  3. App端使用v3编译器,首页为 nvue页面 时,并设置为fast启动模式,此时App启动速度最快

  4. App设置为纯 nvue项目 (manifest里设置app-plus下的renderer:"native"),这种项目的启动速度更快,2秒即可完成启动;因为它整个应用都使用原生渲染,不加载基于webview的那套框架

14.优化包体积

  1. uni-app 发行到小程序时,如果使用了 es6 转 es5 、css 对齐的功能,可能会增大代码体积,可以配置这些编译功能是否开启

  2. uni-app 的 H5端,uni-app 提供了摇树优化机制,未摇树优化前的 uni-app 整体包体积约 500k,服务器部署 gzip 后162k。开启摇树优化需在manifest配置

  3. uni-app 的 App端,Android 基础引擎约 9M ,App 还提供了扩展模块,比如地图、蓝牙等,打包时如不需要这些模块,可以裁剪掉,以缩小发行包;体积在 manifest.json-App 模块权限里可以选择

  4. App端支持如果选择纯nvue项目 (manifest里设置app-plus下的renderer:"native"),包体积可以进一步减少2M左右

  5. App端在 HBuilderX 2.7 后,App 端下掉了 非v3 的编译模式,包体积下降了3M

15.禁止滥用外部js插件

描述

有官方API的就不要额外引用js插件增加项目体积

例如

url传参加密直接用 encodeURIComponent() 和 decodeURIComponent()

作者:Panda_HYC
来源:juejin.cn/post/6997224351346982942

收起阅读 »

回村三天,二舅治好了我的精神内耗

这是我的二舅,村子里曾经的天才少年。这是我的姥姥,一个每天都在跳 poping的老太太。他们在这个老屋生活。建它的时候还没美国。老师们三次登门相劝,二舅闭着眼睛横躺在床上,一言不发,像一位断了腿的卧龙先生。第一年,二舅拒绝下床,他不知道从哪找到了一本赤脚医生手...
继续阅读 »

这是我的二舅,村子里曾经的天才少年。这是我的姥姥,一个每天都在跳 poping的老太太。他们在这个老屋生活。建它的时候还没美国。

二舅上小学是全校第一,上了初中还是全校第一,全市统考。从农村一共收上去三份试卷,其中一份就是二舅的。有一天,二舅发高烧请假回家,隔壁村的医生一天在他屁股上打了四针,二舅就成了残疾。十几岁的二舅躺在床上,再也不想回到学校。

老师们三次登门相劝,二舅闭着眼睛横躺在床上,一言不发,像一位断了腿的卧龙先生。第一年,二舅拒绝下床,他不知道从哪找到了一本赤脚医生手册,疯狂地看了一年。但二舅的腿不是伤了,而是废了,所以久病并不能成医。于是第二年,二舅扔掉了手册,从床上爬了下来,呆坐在天井里望天,像一只大号的青蛙。第三年,二舅不看天了,看家里来的一个木匠干活。木匠干了三天走了,二舅跟姥爷说他看会了,求姥爷去铁匠铺给自己打做木工的工具。三年来,二舅第一次走出了院门,去生产队给人做板凳,一天做两个,一个一毛钱,可以养活自己了。

如此几年,有一天,二舅照常拄着拐来到生产队,队长告诉二舅以后不用来了,生产队没了。二舅问为什么?队长说改革开放了,于是二舅就开始改革开放,游走在镇上的各个村子给人做木工。

有天在路上遇到了当年的那个医生,他跟二舅说要是在今天我早被告倒了,得承包你一辈子。二舅笑着骂他一句,一瘸一拐的又给人干活去了。

后来不知道什么手续上的原因,二舅的残疾证怎么都办不下来,他很失望,居然拄着拐辗转去了北京。他想去天安门广场的纪念堂说要去看看他,他就说改革开放很好,他也好。为什么呢?二舅说他公平。

很快,二舅的兜里就没剩几个钱了。他的一个堂弟在北京当兵,二舅作为军人家属住进了部队,没想到居然混得风生水起。因为二舅不爱搭讪交际,只爱干活,他不知道从哪借到了木工工具。在那个部队条件还很艰苦的年代,给士兵们默默地做了很多的柜子和桌子。

哪个士兵会不喜欢这样的homie呢?

有一天,二舅的堂弟去澡堂,看见一个老头和二舅正坐在一块泡澡,二舅的堂弟吓得一句话都说不出来,因为那个老头是他只见过几次的一位首长,此刻正蹲在池子里给二舅搓背。

后来二舅回到村里,大家都问北京怎么样?二舅说北京人搓背搓得很好。

到了两个妹妹出嫁的年纪,二舅心里很不舍。二舅有自己的表达,大姨和我妈结婚时的所有家具,每一张图纸、每一块木板、每一块玻璃、每一根装饰条、每一个螺丝、每一遍漆,都是二舅一个人完成的。

你能想象在 80 年代在一个山村的女孩子结婚的时候,能拥有这样的一套家具,是多么梦幻的事情吗?

姥姥家这么穷,妹妹出嫁有这么一套家具,婆家也会高看一眼,也许就会更好地对待自己的妹妹。你可能说我在吹牛,因为这是上海牌的家具,但你忘了这是我的二舅。二舅总有办法。什么牌子他都能给你贴上,你还要什么牌子,他还有天津牌、北京牌、香港牌,超豪华OK。

再后来,年轻的二舅领养了刚出生的宁宁,二舅拼命地在周边做工赚钱,大部分时间都把宁宁寄养在了大姨家里,很少陪伴他。宁宁小时候经常被人在背后议论,不懂礼貌。

一个被抛弃了两次的小孩,对这个世界还能有什么礼貌呢?十年前,宁宁和男朋友结婚了,20万出头的县城房子啊就出了十几万,真不敢想象他是怎么攒下来的,他就掏光了半辈子积蓄给宁宁买了房子,却开心得要死。这就是中国式的家长,中国式的可敬又可怜的家长卑微地伟大着。

二舅在30岁出头的时候迎来了说媒的高峰期。但二舅跟我说,他一时觉得他这辈子只能顾得住自己,顾不住别人了,所以从来没有动过这方面的心思。

二舅说谎了,当时有一个隔壁村的女人,有老公还有两个孩子,不知道是什么样的契机,二人的关系突然变得非常的熟络,并很快变得过于熟络。她经常来二舅家串门,二舅也经常去找他。即便是她老公在的时候,两个孩子也很喜欢二舅。

再后来他开始作为二舅家的正式一员,出席家族的一切红白喜事,并对二舅体贴入微,把他乱糟糟的小屋收拾得井井有条。二舅做工回来能吃上一碗热饭,顺手把今天结的钱递给他。就这样好多年过去了,她却并没有离婚。

二舅的四个兄妹从一开始的全力支持,转而怀疑这个女人只是图二舅的那一点钱而强烈反对。而还在上小学的宁宁则喊那个女人老狐狸,喊自己班里的她的女儿小狐狸。老实的二舅进退失据,不知所措。再后来这个女人和她的丈夫死在了外地的一个工棚,煤气中毒,二舅也终生未婚。

这段感情的细节我理解不了,大姨也都记不清了,二舅则是不愿意讲,这到底算怎么一回事呢?

既不是今日实行的仙人跳,也不是那个年月的拉帮套。那时候爱情来过没有呢?

几十年过去了,故人故事无疾而终,到现在什么也没剩下,只剩了一笔烂账,烂在了二舅一个人的心里。流了血,又长了痂,不能撕,一撕就会带下皮肉。

就这样又过去了三十年,乏善可陈。是的,普通人的生活就是这样,普通到不快进 1 万倍都没法看。

转眼姥姥已经88岁了,现在农村的人工成本也越来越高。二舅正是挣钱的好时候,他很想为自己多挣一点养老钱,将来就不用拖累宁宁。但是姥姥现在的生活已经不能自理,也不是很想活了,有一次甚至已经把绳子挂到了门框上。

中国人老说生老病死,生死之间何苦还要再隔上个老病呢,这可不是上天的不仁,而是怜悯。不然我们每个人都在七八十岁却还康健力壮之年去世,对这个世界该有多么的留恋呢?那不是更加的痛苦吗?从这个意义上来讲,老病是生死之间的必要演习。所以在几年前二舅出门的时候就开始把姥姥放到车上。去别人家做木工活的时候,就把姥姥放到身边的小板凳上。

66岁老汉随身携带88岁老母,这个6688组合简直是酷得要死。这几年二舅木工活也不做了,全职照顾姥姥,早上给姥姥洗脸,晚上给姥姥洗脚,下午给姥姥锻炼。

每走二十步就是坐下歇10秒,二舅每走20步就会落后姥姥3米,赶上这3米正好需要10秒。接着走。

这么默契的走位配合,我上一次见到还是在乔丹和皮蓬身上。乔丹喜欢给皮蓬送超跑,二舅喜欢给姥姥蒸面条,再浇上点西红柿炒鸡蛋。嗯好吃的。

二舅从小对宁宁没有什么教育可言,今天的宁宁却成为了村里最孝顺的孩子。可见让小孩将来孝顺自己的最好方法就是默默地孝顺自己的父母,小孩是小不是瞎。

其实很难把二舅定义为一个木匠。我在家这三天的时间里,他给村里人修好了一个插线板、一个燃气灶、一盏床头灯、一辆玩具车、一个掘头、一个洗衣机、一个水龙头,回来的路上被另一个婶子拦住,修好了他家的门锁。还没进家门,又被另一个老头叫到家里,说电磁炉坏了。

二舅到他家发现是他插线板的电源忘了打开。

可怜的老头。

回到家,又修好了一个买来的老人机和收音机。

姥姥有胃病,他就给姥姥针灸,人家嫌门楼上光秃秃的,木头不好看,二舅自己设计好了给人画上去,山顶修了座庙,所有的龙都是二舅雕的。村里没有神婆,二舅就成了算命师。

当然了,签子是自己做的,竹筒是自己做的,本子是自己做的,挂是自己抄来的。

他甚至有一天突发奇想,要做一把二胡。木头做弧身,电话线铜芯做弦,竹子做弓杆、钓鱼线做弓卯。我们这没有蟒蛇,他就上山抓了几条双斑锦拼成一张琴皮。

你看二舅总有办法。

很想给你们看看那把有模有样的二胡。可惜十几年前,姥姥让我的傻子弟弟拿二胡当锄头娃给玩坏了。

这个村子里有的一切农具、家具、电器、车辆。二舅不会修的,只有三样,智能手机、汽车和电脑。因为这些东西二舅也没有。不过现在智能手机也有了,宁宁买的,等他拆上几次也就会修了。

夜深了,二舅家的灯还亮着,又给谁家修东西呢?听见锣声和鞭炮声了吗?不是村里有人结婚,而是年轻人都走了之后,野猪回来了。吓唬野猪呢。

村里就剩下几百个老头老太太了,如果有什么东西坏了,送维修店去修,先别说得花钱,如果到镇上是三十里山路,如果坐客车去县城下了车,他们是连北都招不到的。

二舅就总说他能顾得住自己就不错了。他其实顾住了整个村子。村里人开玩笑叫他歪子。但我们每个人都很清楚,我们爱这个歪子,我们离不开这个歪子。

一九七七年恢复高考的时候,二舅正是十八九岁。如果不是当年发烧后轮的 4 针,二舅可能已经考上了大学,成为了一名工程师。单位分的房子,国家发的退休金,悠游自适,颐养天年。隔壁村一个老头就是这样,当年学习还没二舅学习好呢。

如果是这样,那该有多好。二舅一定会成为汪曾祺笔下父亲汪居生那样充满闲情野趣的老顽童。

看着眼前的二舅,总让我想起电影棋王里的台词:他这种奇才啊只不过是生不逢时,他应该受国家的栽培,名扬天下才对,不应该弄得这么落魄。太遗憾了,真的是太遗憾了。

我问二舅有没有这么想过?

他说从来没有。

这样的心态让二舅成为了村里第二快乐的人。第一快乐的人是刚刚——我们村的树先生。

所以你看,这个世界上第一快乐的人是不需要对别人负责的人,第二快乐的人就是从不回头看的人。

遗憾谁没有呢?人往往都是快死的时候才发现,人生最大的遗憾就是一直在遗憾过去的遗憾。遗憾在电影里是主角崛起的前戏,在生活里是让人沉沦的毒药。

我北漂九年,也曾有幸相识过几位人中龙凤,反倒是从二舅这里让我看到了我们这个民族身上所有的平凡美好与强悍。

都说人生最重要的不是胡一把好牌,而是打好一把烂牌。二舅这把烂牌,打的是真好。

他在挣扎与困顿中表现出来的庄敬自强,令我心生敬意。

我四肢健全,上过大学,又生在一个充满机遇的时代,我理应度过一个比二舅更为饱满的人生。今天二舅还在走在自己的人生路,这条长长的路最终会通往何处呢?

二舅的床下有一个几十年前的笔记本。笔记本的第一页是他摘抄的一句话:

下定决心,不怕牺牲,排除万难,去争取胜利。

是的,这条人生路最后通向的一定是胜利。

作者:衣戈猜想 https://www.bilibili.com/video/BV1MN4y177PB

收起阅读 »

作为一名前端工程师,我浪费了时间学习了这些技术

作为一名前端工程师我浪费时间学习了这些技术 不要犯我曾经犯过的错误! 我2015年刚刚开始学习前端开发的时候,我在文档和在线教程上了解到了许多技术,我浪费大量时间去学习这些技术。 在一个技术、库和框架数量不断增长的行业中,高效地学习才是关键。不管你是新的Web...
继续阅读 »

作为一名前端工程师我浪费时间学习了这些技术


不要犯我曾经犯过的错误!


我2015年刚刚开始学习前端开发的时候,我在文档和在线教程上了解到了许多技术,我浪费大量时间去学习这些技术。


在一个技术、库和框架数量不断增长的行业中,高效地学习才是关键。不管你是新的Web开发人员,还是你已经入门前端并有了一些开发经验,都可以了解一下,以下列出的技术,要么是我花费时间学习但从未在我的职业生涯中实际使用过的,要么是2021年不再重要的事情(也就是说,你可以不知道)。



Ruby / Ruby-on-rails


Ruby-on-Rails在本世纪早期非常流行。我花了几个月的时间尝试用Ruby-on-Rails构建应用程序。虽然一些大型科技公司的代码库中仍然会有一些Rails代码,但近年来我很少遇到使用Rails代码的公司。事实上,在我六年的职业生涯中,我一次也没有使用过Rails。更重要的是,我不想这么做。


AngularJS


不要把AngularJS和Angular混淆。AngularJS从版本2开始就被Angular取代了。不要因为这个原因而浪费时间学习AngularJS,你会发现现在很少有公司在使用它。


jQuery


jQuery仍然是最流行的JavaScript库,但这是一个技术上的历史遗留问题,而非真的很流行(只是很多10-15年前的老网站仍然使用它)。近年来,许多大型科技公司的代码都不再使用jQuery,而是使用常规的JavaScript。jQuery过去提供的许多好处已经不像以前那么关键了(比如能编写在所有类型的浏览器上都能工作的代码,在浏览器有非常不同的规范的年代,这是一个大的问题)。


Ember


学习Ember的热火很久以前就熄灭了。如果你需要一个JavaScript库,那就去学习React(或者Vue.js)。


React class components


如果你在工作中使用React,你可能仍然会发现一些React类组件。因此,理解它们是如何工作的以及它们的生命周期方法可能仍然是很好的。但如果你正在编写新的React组件,你应该使用带有React hook的功能性组件。


PHP


坦诚的说,PHP并没有那么糟糕。在我的第一份网页开发工作中(和Laravel一起),我确实需要经常使用它。但是现在,web开发者应该着眼于更有效地学习 Node.js。如果你已经在学习JavaScript,为什么还要在服务器端添加PHP之类的服务器端语言呢?现在你可以在服务器端使用JavaScript了。


Deno


Deno是一家新公司,在未来几年可能会成为一家大公司。然而,不要轻信炒作。现在很少有公司在使用Deno。因此,如果你是Web开发新手,那就继续学习Node.js(又名服务器端JavaScript)。不过,Deno可能是你在未来几年选择学习的东西。


Conclusion


这就是我今天想说的技术。我相信还有很多东西可以添加到技术列表中——请在评论中留下你的想法。我相信对于这里列出的技术也会有一些争论——Ruby开发者更容易破防。你也可以在评论中进行讨论,这些都是宝贵的意见。


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

收起阅读 »

API 请求慢?这次锅真不在后端

问题 我们在开发过程中,发现后端 API 请求特别慢,于是跟后端抱怨。 “怎么 API 这么慢啊,请求一个接口要十几秒”。 而且这种情况是偶现的,前端开发同学表示有时候会出现,非必现。 但是后端同学通过一顿操作后发现,接口没有问题,他们是通过 postman ...
继续阅读 »

问题


我们在开发过程中,发现后端 API 请求特别慢,于是跟后端抱怨。


“怎么 API 这么慢啊,请求一个接口要十几秒”。


而且这种情况是偶现的,前端开发同学表示有时候会出现,非必现。


但是后端同学通过一顿操作后发现,接口没有问题,他们是通过 postman 工具以及 test 环境尝试,都发现接口请求速度是没有问题的。


“那感觉是前端问题”?


我们来梳理一下问题,如下:



  • 后端 API 请求特别慢,而且是偶现的。

  • 在 test 环境没有复现。

  • postman 工具请求没有复现。


问题解决过程


时间都去哪了?


第一个问题,API 耗费的时间都用来做什么了?


我们打开 Chrome 调试工具。在 network 中可以看到每个接口的耗时。



hover 到你的耗时接口的 Waterful,就可以看到该接口的具体耗时。



可以看到,其耗时主要是在 Stalled,代表浏览器得到要发出这个请求的指令到请求可以发出的等待时间,一般是代理协商、以及等待可复用的 TCP 连接释放的时间,不包括 DNS 查询、建立 TCP 连接等时间等。


所以 API 一直在等待浏览器给它发出去的指令,以上面截图的为例,整整等待了 23.84S,它请求和响应的时间很快(最多也就几百毫秒,也就是后端所说的接口并不慢)。


所以 API 到底在等待浏览器的什么处理?


什么阻塞了请求?


经过定位,我们发现,我们项目中使用 Server-Sent Events(以下简称 SSE)。它跟 WebSocket 一样,都是服务器向浏览器推送信息。但不同的是,它使用的是 HTTP 协议。


当不通过 HTTP / 2 使用时,SSE 会受到最大连接数的限制,限制为 6 次。此限制是针对每个浏览器 + 域的,因此这意味着您可以跨所有选项卡打开 6 个 SSE 连接到 http://www.example1.com,并打开 6 个 SSE 连接到 http://www.example2.com。这一点可以通过以下这个 demo 复现。


复制问题的步骤:



结果是,第 6 次之后,SSE 请求一直无法响应,打开新的标签到同一个地址的时候,浏览器也无法访问。


效果图如下:



该问题在 ChromeFirefox 中被标记为“无法解决”。


至于偶现,是因为前端开发者有时候用 Chrome 会打开了多个选项卡,每个选项卡都是同一个本地开发地址,就会导致达到 SSE 的最大连接数的限制,而它的执行时间会很长,也就会阻塞其他的请求,一致在等待 SSE 执行完。


所以解决的方法是什么?


解决方案


简单粗暴的两个方法



  • 不要打开太多个选项卡。这样就不会达到它的限制数。(因为我们一个选项卡只请求一个 SSE)。

  • 开发环境下,关闭该功能。


使用 HTTP / 2


使用 HTTP / 2 时,HTTP 同一时间内的最大连接数由服务器和客户端之间协商(默认为 100)


这解释了为什么我们 test 环境没有问题,因为 test 环境用的是 HTTP / 2。而在开发环境中,我们使用的是 HTTP 1.1 就会出现这个问题。


那如何在开发环境中使用 HTTP / 2 呢?


我们现在在开发环境,大部分还是使用 webpack-dev-server 起一个本地服务,快速开发应用程序。在文档中,我们找到 server 选项,允许设置服务器和配置项(默认为 'http')。


只需要加上这一行代码即可。


devServer: {
+ server: 'spdy',
port: PORT,
}

看看效果,是成功了的。



原理使用 spdy 使用自签名证书通过 HTTP/2 提供服务。需要注意的一点是:



该配置项在 Node 15.0.0 及以上的版本会被忽略,因为 spdy 在这些版本中不会正常工作。一旦 Express 支持 Node 内建 HTTP/2,dev server 会进行迁移。



总结归纳


原本这个问题认为跟前端无关,没想到最后吃瓜吃到自己头上。提升相关技能的知识储备以及思考问题的方式,可能会方便我们定位到此类问题。


充分利用好浏览器的调试工具,对一个问题可以从多个角度出发进行思考。比如一开始,没想到本地也可以开启 HTTP / 2。后来偶然间想搜下是否有此类方案,结果还真有!




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

收起阅读 »

搞不定移动端性能,全球爆火的 Notion 从 Hybrid 转向了 Native

7 月 20 日,Notion 笔记程序发布了版本更新,并表示更改了移动设备上的技术栈,将从 webview 逐步切换到本机应用程序,以获得更快更流畅的性能。该团队声称该应用程序现在在 iOS 上的启动速度提高了 2 倍,在 Android 上的启动速度提高了...
继续阅读 »

7 月 20 日,Notion 笔记程序发布了版本更新,并表示更改了移动设备上的技术栈,将从 webview 逐步切换到本机应用程序,以获得更快更流畅的性能。

该团队声称该应用程序现在在 iOS 上的启动速度提高了 2 倍,在 Android 上的启动速度提高了 3 倍。


Notion 发布的这条 Twitter 也得到了广泛的关注,几天之内就有了上千条转发。由于前几年 Notion 的技术栈一直没有公开,开发者对此充满了各种猜测,很多人认为 Notion 使用的是 React Native 或 Electron,因此这次 Notion 宣称切换为原生 iOS 和原生 Android,再一次引发了“框架之争”。

其中有不少人发表了“贬低”跨平台开发的看法,对 React Native 等框架产生了质疑,毕竟现在向跨平台过渡是不可避免的,这些框架是对原生工具包的一个“威胁”,而 Notion 恰恰又切换到了“原生”开发模式。

实际上,在 2020 年之前 Notion 使用的是 React Native,随后切换到了 Hybrid 混合开发模式:使用 Kotlin/Swift + 运行网络应用程序的 Web 视图。但移动端的性能一直是一个问题,2 年之后,Notion 再次切换到了原生开发模式。

有网友认为,像 Notion 这样重 UI 和交互的产品,如果不知道如何掌握 Web 技术,那么对他们的产出速度表示担忧。面对这种吵翻天的状况,Notion 的前端工程师也因此再度出面回应这次切换的原因和一些思考。

Notion 的发展和理念

Notion 是一款将笔记、知识库和任务管理无缝衔接整合的多人协作平台。Notion 打破了传统的笔记软件对于内容的组合方式,将文档的内容分成一个个 Block,并且能够以乐高式的方式拖动组合这些 Block,让它使用起来十分灵活。

Notion 由 Ivan Zhao、Simon Last 于 2013 年在旧金山创立。去年底,Notion 获得了 2.75 亿美元的 C 轮融资。截至 2021 年 10 月,Notion 估值 103 亿美元,在全球拥有超 2000 万用户。Notion 的创始人和 CEO Ivan Zhao 是一位 80 后华人。他出生于中国新疆,曾就读于清华附中,中学时随家人移居加拿大,现在被很多人认为将成为硅谷的下一个袁征(Zoom 的创始人)。Ivan 在大学时期主修认知科学,学习的是人的大脑怎么运作,外加对计算机也很感兴趣。



Ivan 也曾表示“我的很多朋友都是艺术家。我是他们中唯一会编码的人。我想开发一款软件,它不仅可以为人们提供文档或网页。” 因此,在 2012 年大学毕业后,在文档共享初创公司 Inkling 工作期间,他创办了 Notion。原本的目标是构建一个无代码应用构建工具,不过项目很快失败了。随后 Ivan 与 Simon 迁往了日本京都,待了一年左右,小而安静的地方能“让我们专注在写代码”,在相对无压力和与世隔绝的环境下,构思并设计出了现在的 Notion 原型。用 Reddit 论坛上的一条获得高赞的网友总结就是:一个 Notion = Google docs + Evernote + Trello + Confluence + Github + Wiki +……

“工具应该模仿人脑的工作方式。但由于每个人的思维和工作方式都不同,这意味着工具需要非常灵活。”Ivan 解释道。而 Notion 创建的目的,就是将用户从一堆各式各样的生产力工具之中解放出来,给予一个干净清爽、简便易行的 All in One 工作平台。企业用户也可以在 Notion 上基本实现公司的内部管理所需要涉及到的所有功能。包括公司知识库和资料库的创建与管理、项目进度管理、信息共享、工作日志、内部社交、协作办公等等。


有人甚至说,Notion 堪比办公软件届的苹果。在 2016 年发布 1.0 版本后,因其独特的设计、专注于将事情做得更好、对投资人的冷淡态度,外加疫情远程办公潮,多方面因素让 Notion 迅速火遍全球。作为一款 All in one 的概念型工具,Notion 一直被众多企业抄作业,但它目前几乎未逢敌手。

Notion 为什么要两次更换技术栈?

Notion 在 2017 年、2018 年分别发布了 iOS 客户端和 Android 客户端。在发布 2.0 版本之后,该公司于 2019 年以 8 亿美元的估值筹集了 1000 万美元的资金。但也许和创始人的发展理念相关,Notion 的员工数量一直不多。

2019 年 3 月的时候,工程团队总共才 4 个人,当时 Notion 用 React Native 来渲染 web 视图。Notion 在 Twitter 上解释说,这是为了更快地部署新功能和进行一些其他修复。

但如果这个系统适合开发者,那么它对用户来说远非最佳:许多人抱怨移动版本非常缓慢。“即使是新 iPhone 也非常慢 - 大约 6-7 秒后我才能开始输入笔记。到那时我都快忘记了我之前想写什么。它基本上是一个非常重的 web 应用程序视图。”“如果 Notion 不选择改变,那么它将迅速被其它同类产品取代。”......



2020 年,Notion 第一次因这个问题,更改了技术栈,放弃 React Native,切换到了 Hybrid 开发环境。

Notion 前端负责人 Jake Teton‑Landis 表示,“React Native 的优势在于允许 Web 开发人员构建手机应用程序。如果我们已经有了 webview,那么 React Native 不会增加价值。对我们来说,它让一切变得更加困难:性能、代码复杂性、招聘等等。用 React Native 快速完成任务的同时,也在跟复杂性战斗,这让我们感觉束手束脚。”

虽然这次移动端的性能有了一些提升,但也没有根本解决问题,更新之后,Android 端依然是一个相当大的痛点。


Notion 也曾在 2019 年的时候表示不会很快发布本机应用程序,但他们同时强调“原生开发也是一个选择”。

7 月 20 日,Notion 发布了版本更新,并表示将从主页选项卡开始,从 webview 逐步一个个地切换到本机应用程序。

此时 Notion 工程团队也大约只有 100 人, 总共包含 3 位 iOS 工程师、4 位 android 工程师,除主页使用 SwiftUI/Jetpack Compose 进行渲染,其他部分仍然是 webview 进行绘制。

“似乎这还是招聘不足产生的人员问题。”Jake 解释说,“我们的策略是随着团队的壮大逐步本地化我们应用程序的更多部分。我们这个程序必须使用本机性能,如果它是原生的,则更容易达到这个性能要求。

凭借我们拥有的经验,以及对问题的了解,我们因此选择了原生 iOS 和原生 Android 开发。虽然出于复杂性的权衡,在可预见的未来,编辑器可能仍然是一个 webview,毕竟 Google Docs、Quip、Dropbox Paper、Coda 都使用原生 shell、webview 编辑器。”

原生开发才是王道?!

虽然无论是原生开发还是 Hybrid 都可以完成工作,但原生应用程序是按照操作系统技术和用户体验准则开发的,因此具有更快的性能优势,并能轻松访问和利用用户设备的内置功能(例如,GPS、地址簿、相机等)。

Hybrid 开发方式,通常是在面对市场竞争需要尽快构建并发布应用程序时候的一种选择。如果期望的发布时间少于六个月,那么混合可能是一个更好的选择,因为可以构建一套源代码,跨平台发布,与原生开发相比,其开发时间和工作量要少得多,但这也意味着需要做出许多性能和功能上的妥协。

如果有足够时间,那么原生方法最有意义,可以让应用程序具有最佳性能、最高安全性和最佳用户体验。毕竟,用户体验是应用程序成功的关键。互联网正在放缓,人们使用手机的时间越来越长,缓慢的应用程序意味着糟糕的业务。在这种情况下,对 Notion 来说,拥有一个快速应用程序比以往任何时候都更加重要。

参考链接:

https://www.notion.so/releases/2022-07-20

https://twitter.com/jitl/status/1530326516013342723?s=20&t=xT0gfWhFvs0yNvc1GQ3sTQ

收起阅读 »

写出优雅的Kotlin代码:聊聊我认为的 "Kotlinic"

"Kotlinic" 一词属于捏造的,参考的是著名的"Pythonic",后者可以译为“很Python”,意思是写的代码一看就很有Python味。照这个意思,"Kotlinic"就是“很Kotlin”,很有Kotlin味。 Kotlin程序员们不少是从Java...
继续阅读 »

"Kotlinic" 一词属于捏造的,参考的是著名的"Pythonic",后者可以译为“很Python”,意思是写的代码一看就很有Python味。照这个意思,"Kotlinic"就是“很Kotlin”,很有Kotlin味。


Kotlin程序员们不少是从Java转过来的,包括我;大部分时候,大家也都把它当大号的Java语法糖在用。但Kotlin总归是一门新语言,而且,在我眼里还是门挺优雅的语言。所以,或许我们可以把Kotlin写得更Kotlin些。我想简单粗浅的聊聊。



本文希望:聊聊一些好用的、简洁的但又不失语义的Kotlin代码


本文不希望:鼓励无脑追求高超技巧,完全放弃了可读性、可维护性,全篇奇技淫巧的操作



受限于本人水平,可能有错误或不严谨之处。如有此类问题,欢迎指出。也欢迎在评论区探讨交流~


善用with、apply、also、let


with和apply


with和apply,除了能帮忙少打一些代码外,重要的是能让代码区分更明确。比如


val textView = TextView(context)
textView.text = "fish"
textView.setTextColor(Color.BLUE)
textView.setOnClickListener { }
val imageView = ImageView(context)
// ...
复制代码

这就是典型的Java写法,自然,没什么问题。但要是类似的代码多起来,总感觉不知道哪里是哪里。如果换用apply呢?


val textView = TextView(context).apply {
text = "fish"
setTextColor(Color.BLUE)
setOnClickListener { }
}
val imageView = ImageView(context).apply {

}
复制代码

apply的大括号轻松划清了边界:我这里的代码和TextView相关。看着更整齐。


如果后面不需要这个变量,赋值还能省了


 // 设置某个view下的各个控件
with(view) {
findViewById<TextView>(R.id.some_id).apply {
text = "fish"
setTextColor(Color.BLUE)
setOnClickListener { }
}

findViewById<ImageView>(R.id.some_id).apply {

}
}
复制代码

apply的另一个常见场景是用于那些返回自己的函数,比如常见的Builder类的方法


fun setName(name: String): Builder{
this.name = name
return this
}
复制代码

改成apply就简洁得多


fun setName(name: String) = apply{ this.name = name }
复制代码

also


also的常见场景有很多,它的语义就是干完上一件事后附带干点什么事。 举个例子,给个函数


fun someFunc() : Model{
// ...
return Model(name = "model", value = "value")
}
复制代码

如果我们突然想加个Log,打印一下返回值,按Java的写法,要这么干:


fun someFunc(): Model{
// ...
val tempModel = Model(name = "model", value = "value")
print(tempModel)
return tempModel
}
复制代码

改的不少。但是按Kotlin的写法呢?


fun someFunc() : Model{
return Model(name = "model", value = "value").also {
print(it)
}
}
复制代码

不需要额外整个变量出来。


类似的,比如上面apply的例子,在没有声明变量的情况下,也可以这样用这个值


findViewById<ImageView>(R.id.some_id).apply {
// ...
}.also{ println(it) }
复制代码

整在一起


这几个函数结合起来,在针对一些比较复杂的场景时,对提高代码的可读性还是挺有帮助的。如【唐子玄】在这篇文章里所举的例子:



假设需求如下:“缩放 textView 的同时平移 button ,然后拉长 imageView,动画结束后 toast 提示”。



“Java”式写法


PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat("scaleX", 1.0f, 1.3f);
PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat("scaleY", 1.0f, 1.3f);
ObjectAnimator tvAnimator = ObjectAnimator.ofPropertyValuesHolder(textView, scaleX, scaleY);
tvAnimator.setDuration(300);
tvAnimator.setInterpolator(new LinearInterpolator());

PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", 0f, 100f);
ObjectAnimator btnAnimator = ObjectAnimator.ofPropertyValuesHolder(button, translationX);
btnAnimator.setDuration(300);
btnAnimator.setInterpolator(new LinearInterpolator());

ValueAnimator rightAnimator = ValueAnimator.ofInt(ivRight, screenWidth);
rightAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int right = ((int) animation.getAnimatedValue());
imageView.setRight(right);
}
});
rightAnimator.setDuration(400);
rightAnimator.setInterpolator(new LinearInterpolator());

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(tvAnimator).with(btnAnimator);
animatorSet.play(tvAnimator).before(rightAnimator);
animatorSet.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {}
@Override
public void onAnimationEnd(Animator animation) {
Toast.makeText(activity,"animation end" ,Toast.LENGTH_SHORT).show();
}
@Override
public void onAnimationCancel(Animator animation) {}
@Override
public void onAnimationRepeat(Animator animation) {}
});
animatorSet.start();
复制代码

乱糟糟的。改成“Kotlin式”写法呢?


AnimatorSet().apply {
ObjectAnimator.ofPropertyValuesHolder(
textView,
PropertyValuesHolder.ofFloat("scaleX", 1.0f, 1.3f),
PropertyValuesHolder.ofFloat("scaleY", 1.0f, 1.3f)
).apply {
duration = 300L
interpolator = LinearInterpolator()
}.let {
play(it).with(
ObjectAnimator.ofPropertyValuesHolder(
button,
PropertyValuesHolder.ofFloat("translationX", 0f, 100f)
).apply {
duration = 300L
interpolator = LinearInterpolator()
}
)
play(it).before(
ValueAnimator.ofInt(ivRight,screenWidth).apply {
addUpdateListener { animation -> imageView.right= animation.animatedValue as Int }
duration = 400L
interpolator = LinearInterpolator()
}
)
}
addListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
Toast.makeText(activity,"animation end",Toast.LENGTH_SHORT).show()
}
override fun onAnimationCancel(animation: Animator?) {}
override fun onAnimationStart(animation: Animator?) {}
})
start()
}
复制代码

从上往下读,层次分明。读起来可以感觉到:


构建动画集,它包含{
动画1
将动画1和动画2一起播放
将动画3在动画1之后播放
。。。
}
复制代码

(上面的代码均来自所引文章)


用好拓展函数


继续上面动画的例子接着说,可以看到,最后的Listener实际上我们只用了onAnimationEnd这一部分,但却写出了一大堆。这时候,拓展函数就起作用了。


幸运的是,Google官方的androidx.core:core-ktx已经有了对应的拓展函数:


public inline fun Animator.doOnEnd(
crossinline action: (animator: Animator) -> Unit
): Animator.AnimatorListener =
addListener(onEnd = action)


public inline fun Animator.addListener(
crossinline onEnd: (animator: Animator) -> Unit = {} ,
crossinline onStart: (animator: Animator) -> Unit = {} ,
crossinline onCancel: (animator: Animator) -> Unit = {} ,
crossinline onRepeat: (animator: Animator) -> Unit = {}
): Animator.AnimatorListener {
val listener = object : Animator.AnimatorListener {
override fun onAnimationRepeat(animator: Animator) = onRepeat(animator)
override fun onAnimationEnd(animator: Animator) = onEnd(animator)
override fun onAnimationCancel(animator: Animator) = onCancel(animator)
override fun onAnimationStart(animator: Animator) = onStart(animator)
}
addListener(listener)
return listener
}
复制代码

所以上面的最后几行addListener可以改成


doOnEnd { Toast.makeText(activity,"animation end", Toast.LENGTH_SHORT).show() } 
复制代码

是不是简单得多?


当然,弹出Toast似乎也很常用,所以再搞个拓展函数


inline fun Activity.toast(text: String, duration: Int = Toast.LENGTH_SHORT) 
= Toast.makeText(this, text, duration).show()
复制代码

上面的代码又可以改成这样


 (animation.) doOnEnd  { activity.toast("animation end") } 
复制代码

再比较下原来的


 (animation.) addListener(object : Animator.AnimatorListener {
override fun onAnimationRepeat(animation: Animator?) {}
override fun onAnimationEnd(animation: Animator?) {
Toast.makeText(activity,"animation end",Toast.LENGTH_SHORT).show()
}
override fun onAnimationCancel(animation: Animator?) {}
override fun onAnimationStart(animation: Animator?) {}
})
复制代码

是不是简洁得多?


上面提到androidx.core:core-ktx,其实它包含了大量有用的拓展函数。如果花点时间了解了解,或许能优化不少地方。最近掘金上也有不少类似的文章,可以参考参考


juejin.cn/post/711504…


juejin.cn/post/711692…


juejin.cn/post/712171…


用好运算符重载


Kotlin的运算符重载其实很有用,举个栗子


给List添加值


我见过这种代码


val list = listOf(1)
val newList = listOf(1, 2, 3)

val mutableList = list.toMutableList() // 转成可变的
mutableList.addAll(newList) // 添加新的
return mutableList.toList() // 返回,改成不可变的
复制代码

但是换成运算符重载呢?


val list = listOf(1)
val newList = listOf(1, 2, 3)
return list + newList
复制代码

一个"+"号,简明扼要。


又比如,想判断


某个View是否在ViewGroup中


最简单的看看索引呗


val group = LinearLayout(this)
val isContain = group.indexOfChild(view) != -1
复制代码

不过,借助core-ktx提供的运算符,我们可以写出这样的代码


val group = LinearLayout(this)
val isContain = view in group
复制代码

语义上更直接


想添加(删除)一个View?除了addView(removeView),也可以直接"+="(-=)


val group = LinearLayout(activity)
group += view // 添加子View

group -= view // 移除子View
复制代码

想遍历?重载下iterator()运算符(core-ktx也写好了),就可以直接for了


val group = LinearLayout(this)
for (child in group) {
//执行操作
}
复制代码

(这几个View的例子基本也来自上面的文章)


此外,良好设计的拓展属性和拓展函数也能帮助写出更符合语意的代码,形如


// 设置view的大小
view.setSize(width = 50.dp, height = 100.dp)
// 设置文字大小
textView.setFontSize(18.sp)
复制代码

// 获取三天后的时间
val dueTime = today + 3.days
复制代码

// 获取文本的md5编码
val md5 = "FunnySaltyFish".md5
复制代码

上面的代码很容易能看出是要干嘛,而且也非常容易实现,此处就不再赘述了。


DSL


关于DSL,大家可能都知道有这么个东西,但可能用的都不多。但DSL若用得好,确实能达到化繁为简的功效。关于DSL的基本原理和实现,fundroid大佬在Kotlin DSL 实战:像 Compose 一样写代码 - 掘金中已经写得非常清晰了,本人就不再画蛇添足,接下来仅谈谈可能的使用吧。


构建UI


DSL的一个广泛应用应该就是构建UI了。


Anko(已过时)


较早的时候,一个比较广泛的应用可能就是之前的anko库了。JetBrains推出的这个库允许我们能够不用xml写布局。放一个来自博客Kotlin之小试Anko(Anko库的导入及使用) - SoClear - 博客园的例子


private fun showCustomerLayout() {
verticalLayout {
padding = dip(30)
editText {
hint = "Name"
textSize = 24f
}.textChangedListener {
onTextChanged { str, _, _, _ ->
println(str)
}
}
editText {
hint = "Password"
textSize = 24f
}.textChangedListener {
onTextChanged { str, _, _, _ ->
println(str)
}
}
button("跳转到其它界面") {
textSize = 26f
id = BTN_ID
onClick {
// 界面跳转并携带参数
startActivity<IntentActivity>("name" to "小明", "age" to 12)
}
}

button("显示对话框") {
onClick {
makeAndShowDialog()
}
}
button("列表selector") {
onClick {
makeAndShowListSelector()
}
}
}
}

private fun makeAndShowListSelector() {
val countries = listOf("Russia", "USA", "England", "Australia")
selector("Where are you from", countries) { ds, i ->
toast("So you're living in ${countries[i]},right?")
}
}

private fun makeAndShowDialog() {
alert("this is the msg") {
customTitle {
verticalLayout {
imageView(R.mipmap.ic_launcher)
editText {
hint = "hint_title"
}
}
}

okButton {
toast("button-ok")
// 会自行关闭不需要我们手动调用
}
cancelButton {
toast("button-cancel")
}
}.show()
}
复制代码

简洁优雅,而且由于是Kotlin代码生成的,还省去了解析xml的消耗。不过,由于“现在有更好的选择”,Anko官方已经停止维护此库;而被推荐的、用于取而代之的两个库分别是:Views DSLJetpack Compose


Views DSL


关于这个库,Anko官方在推荐时说,它是“An extensible View DSL which resembles Anko.”。二者也确实很相像,但Views DSL在Anko之上提供了更高的拓展性、对AppCompat的支持、对Material的支持,甚至提供了直接预览kt布局的能力!



基本的使用可以看看上图,额外的感兴趣的大家可以去官网查看,此处就不多赘述。


\


Jetpack Compose


作为一个用Compose超过一年的萌新,我自己是十分喜欢这个框架的。但同时,目前(2022-07-25)Compose的基建确实还尚不完善,所以对企业项目来说还,是应该充分评估后再考虑。但我仍然推荐你尝试一下,因为它简单、易用。即使是在现有的View项目中,也能无缝嵌入部分Compose代码;反之亦然。


Talk is cheap, show me your code. 比如要实现一个列表,View项目(使用RecyclerView)需要xml+Adapter+ViewHolder。而Compose就简洁得多:


LazyColumn(Modifier.fillMaxSize()) {
items(10) { i ->
Text(text = "Item $i", modifier = Modifier
.fillMaxWidth()
.clickable {
context.toast("点击事件")
}
.padding(8.dp), style = MaterialTheme.typography.h4)
}
}
复制代码

上面的代码创造了一个全屏的列表,并且添加了10个子项。每个item是一个文本,并且简单设置了其样式和点击事件。即使是完全不懂Compose,阅读代码也不难猜到各项的含义。运行起来,效果如下:



构建复杂的“字符串”


拼接字符串是一项常见的工作,不过,当它复杂起来但又有一定结构时,简单的"+"或者模板字符串看起来就有些杂乱了。这时,DSL就能很优雅的解决这个任务。


举几个常见的例子吧:


Html


使用DSL,能够写出类似这样的代码


val htmlText = buildHtml{
html{
body{
div("id" to "wrapper"){
p{ +"这是一个段落" }
repeat(3){ i ->
li{ +"Item ${i+1}" }
}
img("src" to "https://www.xxx.xxx/", "width" to "100px")
}
}
}
}
复制代码

上述代码会生成类似这样的html


<!DOCTYPE html>
<html lang="zh-CN">
<body>
<div id="wrapper">
<p>这是一个段落</p>
<ul>Item 1</ul>
<ul>Item 2</ul>
<ul>Item 3</ul>
<img src="https://www.xxx.xxx/" width="100px">
</div>
</body>
</html>
复制代码

简洁直接,而且不容易出错。


你可能比较疑惑上面的+"xxx"是个啥,其实这是用了运算符重载把String转成了纯文本Tag。代码可能类似于


open class Tag()
open class TextTag(val value: String) : Tag()
operator fun String.unaryPlus() = TextTag(this)
复制代码

Markdown


类似的,也可以用这种方式生成markdown。代码可能类似于


val markDownText = buildMarkdown {
text("我是")
link("FunnyFaltyFish", "https://github.com/FunnySaltyFish")
newline()
bold("很高兴见到你~")
}
复制代码

生成的文本类似于


我是 [FunnySaltyFish](https://github.com/FunnySaltyFish)  
** 很高兴见到你~ **
复制代码

SpannableString


对Android开发者来说,这个东西估计更常见。但传统的构造方式可以说够复杂的,所以DSL也能用。好的是,Google已经在core-ktx里写好了更简便的方法


使用例子如下:


val build = buildSpannedString {
backgroundColor(Color.YELLOW) {
append("我叫")
bold {
append("FunnySaltyFish")
}
append(",是一名学生")
}
}
复制代码

渲染出的效果如下


image.png


待续


本文应该还没有完,不过貌似写着写着也不短了,所以就先发了吧(主要是再晚些就赶不上征稿了 (笑))。后面我还想聊聊kotlin的代理、协程、Collection……争取下次见!


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

Android 平台 Native Crash 问题分析与定位

一 Native Crash 简介 Native Crash 是发生在 Android 系统中 C/C++ 层面的 Crash,具体可参考: # Android 平台 Native Crash 捕获原理详解 二 Native C/C++ Libraries 简...
继续阅读 »

一 Native Crash 简介


Native Crash 是发生在 Android 系统中 C/C++ 层面的 Crash,具体可参考: # Android 平台 Native Crash 捕获原理详解


二 Native C/C++ Libraries 简介


Android 开发中通常是将 Native 层代码打包为.so格式的动态库文件,然后供 Java 层调用,.so库文件通常有以下三种来源:



  • Android 系统自带的核心组件和服务,如多媒体库、OpenGL ES 图形库等

  • 引入的第三方库

  • 开发者自行编译生成的动态库


2.1 .so文件组成


一个完整的 .so 文件由 C/C++代码和一些 debug 信息组成,这些 debug 信息会记录 .so中所有方法的对照表,就是方法名和其偏移地址的对应表,也叫做符号表 symbolic 信息,这种 .so被称为未 strip 的,通常体积会比较大。



通常 release 的.so都是需要经过 strip 操作,strip 之后的.so中的 debug 信息会被剥离,整个 so 的体积也会缩小许多。


可以简单将这个 debug 信息理解为 Java 代码混淆中的 mapping 文件,只有拥有这个 mapping 文件才能进行堆栈分析。如果堆栈信息丢了,基本上堆栈无法还原,问题也无法解决。


所以,这些 debug 信息尤为重要,是我们分析 Native Crash 问题的关键信息,那么我们在编译 .so 时 候务必保留一份未被 strip 的.so或者剥离后的符号表信息,以供后面问题分析。


2.2 查看 so 状态


也可以通过命令行来查看.so的状态,Linux 下使用 file 命令即可,在命令返回值里面可以查看到.so的一 些基本信息。


如下代码所示,stripped 代表是没有 debug 信息的.so,with debug_info, not stripped 代表携带 debug 信息的.so


file libbreakpad-core-s.so
libbreakpad-core-s.so: *******, BuildID[sha1]=54ad86d708f4dc0926ad220b098d2a9e71da235a, stripped
file libbreakpad-core.so
libbreakpad-core.so: ******, BuildID[sha1]=54ad86d708f4dc0926ad220b098d2a9e71da235a, with debug_info, not stripped
复制代码

2.3 获取 strip 和未被 strip 的 so


目前 Android Studio 无论是使用 mk 或者 Cmake 编译的方式都会同时输出 strip 和未 strip 的 so,如下图是 Cmake 编译 so 产生的两个对应的 so。




strip 之前的 so 路径:{project}/app/build/intermediates/merged_native_libs


strip 之后的 so 路径:{project}/app/build/intermediates/stripped_native_libs


三 Native Crash 捕获与解析


3.1 通过 DropBox 日志解析


Android Dropbox 是 Android 在 Froyo(API level 8) 引入的用来持续化存储系统数据的机制。主要用于记录 Android 运行过程中, 内核, 系统进程, 用户进程等出现严重问题时的 log, 可以认为这是一个可持续存储的系统级别的 logcat。


相关文件记录存储目录:/data/system/dropbox


只需要将 DropBox 的日志获取到即可进行分析解决,下面贴上一份 Log 示例。


DropBox 中的 Tombstone 文件显示,Native Crash 发生在动态库 libnativedemo.so 中,具体的方法和行数可以用 Android/SDK/NDK 提供的工具 linux-android-addr2line 来进一步定位。


addr2line 工具通常在 ndk 目录下,例如:


${SDK Path}/ndk/21.4.7075529/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line
复制代码

然后使用命令行,既可将偏移地址转换为 crash 方法和行数


arm-linux-androideabi-addr2line [option(s)] [addr(s)]
复制代码

简单来说就是 arm-linux-androideabi-addr2line + 可选项 + 异常地址



























































[option(s)]介绍
@从文件中读取 options
-a在结果中显示地址 addr
-b设置二进制文件的格式
-e设置输入文件(常用:选项后面需要跟报错的共享库,用于 addr2line 程序分析)
-iunwind inline function
-jRead section-relative offsets instead of addresses
-p让输出更易读
-s在输出中,剥离文件夹名称
-f显示函数名称
-C(大写的) 将输出的函数名 demangle
-h输出帮助
-v输出版本信息

使用 addr2line 进行解析,结果可以看到,Native Crash 发生在文件 native-lib.cpp17 行的 Crash() 方法


结合代码分析,在 Crash() 中,对空指针 *a 进行了赋值操作,所以造成了 crash。


#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_elijah_nativedemo_MainActivity_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

/**
* 引起 crash
*/
void Crash() {
volatile int *a = (int *) (NULL);
*a = 1;
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_elijah_nativedemo_MainActivity_nativeCrash(JNIEnv *env, jobject thiz) {
Crash();
}
复制代码

通过读取 DropBox 获得 crash log -> addr2line 解析偏移地址的方法确实可以定位到 native crash 发生的现场,但是 DropBox 只有系统应用能访问,非系统应用拿不到日志。对于非系统应用,可以使用 google 提供的开源工具 BreakPad 进行监测分析。


3.2 通过 BreakPad 捕获解析


3.2.1 breakpad 简介


BreakPad 是 Google 开发的一个跨平台 C/C++ dump捕获开源库,崩溃文件使用微软的 minidump格式存储,也支持发送这个 dump 文件到你的服务器,breakpad 可以在程序崩溃时触发 dump 写入操作,也可以在没有触发 dump 时主动写 dump 文件。breakpad 支持 windows、linux、macos、android、ios 等。目前已有 Google Chrome, Firefox, Google Picasa, Camino, Google Earth 等项目使用。


3.2.2 实现原理


在不同平台下使用平台特有的函数以及方式实现异常捕获:


Windows:通过 SetUnhandledExceptionFilter()设置崩溃回掉函数


Max OS:监听 Mach Exception Port 获取崩溃事件


Linux:监听 SIGILL SIGSEGV 等异常信号 获取崩溃事件


工作原理示意图


图片右上角是一个完整的应用程序,它包含了三部分即程序代码、Breakpad Client(即 brekapad 提供出来的静态库),调式信息




  • Build System中 breakpad 的 symbol 生成工具借助应用层序中的 Debugging Information 这一部分生成一个 Google 自己的符号文件,最终在发布应用层序的时候使用 strip 将调式信息去除




  • User's System中运行的应用程序是通过 strip 去除了调式信息的,若应用程序发生 Crash,Breakpad client 就会写 minidump 文件到指定目录,也可以将产生的 minidump 文件发送到远端服务器即 Crash Colletcor。




  • Crash Collector就可以利用 Build System 中产生的 symol 文件和 User's System 中上报的 minidump 文件生成用户可读的 stack trace




3.2.3 使用示例


获取 breakpad 源码


github.com/google/brea…


执行安装 breakpad


1. cd breakpad 目录
2. 直接命令窗口输入:

./configure && make
复制代码

移植 Breakpad 到客户端程序


breakpad 源码导入应用程序 cpp 目录下



然后在 breakpad 中创建 CMakeLists.txt


cmake_minimum_required(VERSION 3.18.1)
 
#导入头文件
include_directories(src src/common/android/include)
#支持汇编文件的编译
enable_language(ASM)
#源文件编译为静态库
add_library(breakpad STATIC
        src/client/linux/crash_generation/crash_generation_client.cc
        src/client/linux/dump_writer_common/thread_info.cc
        src/client/linux/dump_writer_common/ucontext_reader.cc
        src/client/linux/handler/exception_handler.cc
        src/client/linux/handler/minidump_descriptor.cc
        src/client/linux/log/log.cc
        src/client/linux/microdump_writer/microdump_writer.cc
        src/client/linux/minidump_writer/linux_dumper.cc
        src/client/linux/minidump_writer/linux_ptrace_dumper.cc
        src/client/linux/minidump_writer/minidump_writer.cc
        src/client/linux/minidump_writer/pe_file.cc
        src/client/minidump_file_writer.cc
        src/common/convert_UTF.cc
        src/common/md5.cc
        src/common/string_conversion.cc
        src/common/linux/breakpad_getcontext.S
        src/common/linux/elfutils.cc
        src/common/linux/file_id.cc
        src/common/linux/guid_creator.cc
        src/common/linux/linux_libc_support.cc
        src/common/linux/memory_mapped_file.cc
        src/common/linux/safe_readlink.cc)
#导入相关的库
target_link_libraries(breakpad log)
复制代码

breakpad 中的 CMakeLists.txt 创建完成后,还需要在 cpp 目录下的 CMakeLists.txt 中进行配置,将刚刚创建的 CMakeLists.txt 引入进去


cmake_minimum_required(VERSION 3.18.1)
 
#引入头文件
include_directories(breakpad/src breakpad/src/common/android/include)
 
add_library(nativecrash SHARED nativecrashlib.cpp)
 
#添加子目录,会自动查找这个目录下的 CMakeList
add_subdirectory(breakpad)
 
target_link_libraries(nativecrash log breakpad)
复制代码

breakpad 初始化


然后在自己项目的 native 文件中对 breakpad 进行初始化,如下


#include <jni.h>
#include <string>
#include "breakpad/src/client/linux/handler/exception_handler.h"
#include "breakpad/src/client/linux/handler/minidump_descriptor.h"

/**
* 引起 crash
*/
void Crash() {
volatile int *a = (int *) (NULL);
*a = 1;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_elijah_nativedemo_MainActivity_nativeCrash(JNIEnv *env, jobject thiz) {
Crash();
}

//回调函数
bool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor,
void* context,
bool succeeded) {
printf("Dump path: %s\n", descriptor.path());
return false;
}

//breakpad 初始化
extern "C"
JNIEXPORT void JNICALL
Java_com_elijah_nativedemo_MainActivity_initNative(JNIEnv *env, jclass clazz, jstring path_) {
const char *path = env->GetStringUTFChars(path_, 0);
google_breakpad::MinidumpDescriptor descriptor(path);
static google_breakpad::ExceptionHandler eh(descriptor, NULL, DumpCallback,
NULL, true, -1);
env->ReleaseStringUTFChars(path_, path);
}
复制代码

Java 层代码


Java 层传入 Crash dump 文件的保存路径,用于崩溃时文件的生成


package com.elijah.nativedemo;

import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import java.io.File;

public class MainActivity extends AppCompatActivity {

static {
System.loadLibrary("nativedemo");
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init(this);
findViewById(R.id.crash)
.setOnClickListener(
new View.OnClickListener() {
@Override
public void onClick(View view) {
nativeCrash();
}
});
}

public static void init(Context context){
Context applicationContext = context.getApplicationContext();
File file = new File(applicationContext.getExternalCacheDir(),"native_crash");
if(!file.exists()){
file.mkdirs();
}
initNative(file.getAbsolutePath());
}

/**
* 模拟崩溃
*/
public static native void nativeCrash();

/**
* 初始化 breakpad
* @param path
*/
private static native void initNative(String path);
}
复制代码

捕获 Crash,解析 dump


Native Crash 产生后,breakpad 会捕获 crash 信息,生成后缀为.dmp的 dump 文件到指定目录下。


.dmp 格式的文件通常无法查看,需要解析工具对这个文件进行解析。解析工具在步骤“执行安装 breakpad”中就已经生成在 breakpad/src/processor目录下,名为 minidump_stackwalk


输入如下指令即可解析 dump 文件


./minidump_stackwalk my.dump > crash.txt
复制代码

生成的 crash.txt 如下图所示,关键代码是红框的部分,Thread 0 后面有一个 crashed 标识,说明这里是发生崩溃的线程,而下面就是崩溃的文件以及内存地址,使用 3.1 中介绍的 addr2line 工具进行解析即可得到问题方法与行号


参考文献


Android NativeCrash 捕获与解析


Android---Native层崩溃的监听工具BreakPad


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

关于标准 MVVM 设计模式在 Android 中应用的思考

本来这篇文章很早就应该写的,一直没(比)有(较)时(懒)间 今天决定把它写完咯 首先表明态度, I think: 网上流传的 ViewModel + LiveData + XXX 的号称 MVVM 的代码设计基本都是假的(fake news :P) MV...
继续阅读 »

本来这篇文章很早就应该写的,一直没(比)有(较)时(懒)间

今天决定把它写完咯



首先表明态度, I think:

网上流传的 ViewModel + LiveData + XXX 的号称 MVVM 的代码设计基本都是假的(fake news :P)




MVVM


首先说一下 MVVM 这种架构模式(或者说设计模式),我对它的认识源于维基百科


mvvm_pattern


我们来看看关于 MVVM 各组件的标准定义



  • model: 没啥好说的,跟 MVP MVC 之流大差不差,定义了业务(数据)逻辑和数据的模型

  • view: 还是没啥好说的,就是视图层,用户界面

  • view-model: 关键在这里,我们所谓的 view-model 其实是从 presenter 胶水演化出来的,暴露视图的公开属性和指令,本质上就是 view 视图对应的一个 model

    • 首先,它是一个 view model,对 model 层暴露来自 view 层的一些公开属性,以及来自 view 层的一些指令(presenter?)

    • And,binder:
      view-model 区别于 presentercontroller,它有一个称之 binder 的东西,作用是处理 view-model 中暴露的视图属性(状态)与视图 UI 的自动同步




所以 MVVM 模式下的流转路径应该是这样的:



  • view: user input event -> view-model: view property or command

  • view-model: handle input, biz model -> model: biz data/logic processing

  • model: state chagne events(data) -> view-model: handle biz state

  • view-model: ui state update -> binder: handle ui state synching


MVP


ok,我们再来看一下 MVP 是怎么流转的


mvp_mode



  • view: User input event -> presenter: function (command)

  • presenter: biz model -> model: biz data/logic processing

  • model: state change events(data) -> presenter: convert biz state

  • presenter: biz state/data -> view: refresh ui


仔细对比一下往上流传的基于 Jetpack ViewModel + LiveData 的伪 MVVM(MVP)的流转情况:



  1. Activity/Fragemnt -> ViewModel, 这是 view -> presenter

  2. ViewModel 调用 model 处理网络请求、数据逻辑、文件 IO 等业务逻辑,这是 presenter -> model

  3. 这里我们看一下在 ViewModel 中完成了业务逻辑通知 UI 刷新,通过 LiveDatasetValue/postValue 更新状态,

    view 层通过 viewModel.xxxLiveData.observe(lifecycleOwner) { data -> … }


在最后一个环节,我们对比一下经典的 MVP 模式的写法:presenter 通过持有的 view 接口通知视图变更,view 层在对应的接口实现中完成对 UI 组件的更新


看 ~ 发现了什么

即使是通过 LiveData 观察者模式在 view 层实现对数据的观察,省去了经典 MVP 写法的 view 接口定义和耦合,但是在事件(数据)流转的路径上,依然是走的 MVP 的模式


对比 MVVM 定义的工作流程,不难发现,其中最大的差异在于 binder 这个角色的存在

binder 作为实现数据和 UI 同步的重要组件,同时按照 MVVM 模式的定义,属于 view-model 的内部成员


因此可以得出结论:MVVM 的关键在于,用户事件的流转是单向,从 view 层开始,到 view-model 结束;而这其中的关键在于 binder


Jetpack data-binding


Databinding 就是 Google 爸爸为我们提供的一个官方 binder 实现方案


即:



  • MVVM 中的 binder 可以直接使用官方 data-binding 组件来实现

  • 不使用 data-binding 组件,自定义处理数据与 UI 的同步的 binder 并在 view-model 中维护,也是规范的 MVVM 写法


DataBinding 分为两个部分


ViewDataBinding:每个被 <layout> 标签包裹的布局都会对应生成一个 ViewDataBinding 的子类作为视图与数据绑定的管理者
Observable/BaseObservable: 实际的 binder 开发接口,需要绑定与 view 层建立绑定关系的数据通过实现此接口并注册成员,即可自动完成监听与同步(实际代码在生成的 XxxLayoutBindingImpl 类中)


关于 DataBinding 的使用及原理,此处不予赘述


MVVM 在实际项目中的落地


sample Activity


class SampleBindingActivity : AppCompatActivity(), ActivityBindingHolder<SampleActivityBinding> by ActivityBinding(R.layout.sample_activity) {

private val viewModel: SampleViewModel by viewModel()

override fun onCreate(savedInstanceState: Bundle?) {
// replace setContentView(), and hold binding instance
inflateBinding { binding ->
// init with binding
binding.initView()

viewModel.bind(binding)
}

}

private fun SampleActivityBinding.initView() {
val random = Random()
btnTest.onClick {
viewModel.random = random.nextInt(100)
}
}
}

sample layout


<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="binder"
type="package.SampleBinder" />
</data>

<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_nickname"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="1"
android:text="@{binder.nickname}"
android:textSize="12sp"
app:layout_constraintBottom_toTopOf="parent"
app:layout_constraintEnd_toEndOf="@id/channel_name"
app:layout_constraintStart_toStartOf="@id/program_vip" />

<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_test"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_nickname" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

sample view-model


class SampleViewModel : ViewModel() {

var random: Int by ObservableProperty(0) { binder.nickname = "Nickname_$it" }

private val model = SampleModel() // biz handler model, network/data/io etc.
private val binder = SampleBinder() // binder for sync data and view state

fun bind(binding: ViewDataBinding) {
binding.setVariable(BR.binder, binder)
}


}

sample binder


class SampleBinder : BaseObservable() {

@get:Bindable
var nickname: String by observableField(BR.nickname, "Nickname")


}

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

Flutter【手势&绘制】模拟纸质书籍翻页

前言 今天继续探索绘制与手势的组合实践,之前在看电子书切换页面时会有一个模拟纸质书籍翻页效果,这是典型的绘制和手势的结合实现的效果,那么今天我们就用Flutter也实现这样的一个效果吧。 原理 大家可以找本书翻页到一半看下效果,从右下角翻到一半时,我们可以将可...
继续阅读 »

前言


今天继续探索绘制与手势的组合实践,之前在看电子书切换页面时会有一个模拟纸质书籍翻页效果,这是典型的绘制和手势的结合实现的效果,那么今天我们就用Flutter也实现这样的一个效果吧。


原理


大家可以找本书翻页到一半看下效果,从右下角翻到一半时,我们可以将可视区域分为下图ABC三部分区域。


image.png

A:下一页可视区域。

B:当前页不可视区域,翻的页不可见的区域。

C:当前页可视区域,也就是需要翻的页的可视区域。


原理分解:


我们可以先将A区域和B区域合为一个区域计算,那么根据路径联合C区域自然就可以得到,至于A、B区域区分后面再讲,看下图:

image.png

a为手指触摸点,表示翻页右下角位置。【已知】

f为固定书籍右下角位置。【已知】


a点和f已知,连接af,我们令g点为af的中点,过g点连接eh垂直af,为af中垂线, 可得 g = Point((a.x + f.x) / 2, (a.y + f.y) / 2);


并且知道△egf△emg△mfg为三个直角三角形,由直角三角形相似原理可知这三个三角型两两相似,所以,△emg相似△mfg,可知:

em/gm = gm/mf;

em = gm*gm/mf;

因为:gm = f.y-a.y; mf=f.x-g.x;

可得 e = Point(g.x - (pow(f.y - g.y, 2) / (f.x - g.x)), f.y);


同理过g点做fh垂直线可得h点坐标。略...


从上方理论图可知,cdb是一条二阶贝塞尔曲线,控制点为e点, abak为直线线段,接下来我们令nag的中点,同理过n点垂直于af连接cj,可知ce等于ef的一半;(可以画辅助线过gf中点垂直af得出)。

所以可得 c = Point(e.x - (f.x - e.x) / 2, f.y);

j点坐标同理。略...


接下来我们看下b点,目前我们已知 aecj点坐标,现在b点就是aecj的相交点。


那么问题来了:

用我们九年义务教育学的数学知识解决以下两个问题。


1、在坐标系中,已知两点(x1,y1)、(x2,y2)坐标,求过这两点直线函数?


2、已知两条直线函数求两条直线的相交点?


我们知道直线函数表达式为:y=kx+b;,假设k为正常值,我们可求得kb的值,


/// 两点求直线方程
static double towPointKb(Point p1, Point p2,
{bool isK = true})
{
/// 求得两点斜率
double k = 0;
double b = 0;
// 防止除数 = 0 出现的计算错误 a e x轴重合
if (p1.x == p2.x) {
// k 为无穷大 函数表达式变为 x= 常量。
k = (p1.y - p2.y) / (p1.x - p2.x-1);
} else {
k = (p1.y - p2.y) / (p1.x - p2.x);
}
b = p1.y - k * p1.x;
if (isK)
return k;
else
return b;
}

通过两条直线表达式的k值和b值,我们就可以求出两条直线是否平行、相交、重合等情况,若相交则可求出。


k相同b不同:平行无交点。

k相同b相同:重合。

k不同无论b相不相同,相交必有一交点。


那么就可得出b点坐标:(假设k永不相等)


b = Point((b2 - b1) / (k1 - k2), (b2 - b1) / (k1 - k2) * k1 + b1);

k点坐标同理。略...


绘制


以上AB区域的关键点已经全部得到了,我们将辅助线去掉将这些点连接起来看下效果。


image.png


得到AB区域的同时,我们间接的就得到了C区域,


// mPath 为书籍矩形区域
Path mPathC = Path.combine(PathOperation.reverseDifference, mPathAB, mPath);

接下来将AB区域进行区分,再回到上方,坐标图黄色线条部分,我们可以看到d点和i点坐标。

通过原理解析我们可知d点为pe的中点,而p点为cb的中点,那么就可以得出:

p.x = (e.x -c.x)/2; ,d.x = (e.x-p.x)/2;

p.y = (e.y -b.y)/2; ,d.y = (e.y-p.y)/2;


所以可得 d = Point(((c.x + b.x) / 2 + e.x) / 2, ((c.y + b.y) / 2 + e.y) / 2);

i点坐标同理。略...
接下来我们连接dai三角形区域,得到以下图形,
image.png


同理通过路径联合我们就可以将AB区域进行分开,


Path mPath1 = Path();
mPath1.moveTo(p.value.d.x, p.value.d.y);
mPath1.lineTo(p.value.a.x, p.value.a.y);
mPath1.lineTo(p.value.i.x, p.value.i.y);
mPath1.close();
Path mPathB = Path.combine(PathOperation.intersect, mPathAB, mPath1);

得到以下图形,


image.png


到这里梳理一下,目前我们A、B、C三个path路径区域已经全部得到,剩下的就是填充书籍颜色,接下来我们将画笔设置为填充不同颜色,通过手势不断变化a点坐标看下效果。


Jul-26-2022 14-47-57.gif


是不是有点翻书的意思了,这里有一个问题,书籍的左下角也就是c点坐标在我们翻页的过程中会跑到页面之外,一般书籍都是左侧装订,这里我们希望达到一个真实的翻页效果就需要将c点的x轴最小值设置为书籍最左侧0


image.png

这里涉及到相似图形的数学知识,手指触摸点是在不断变化的,当cx轴达到临界值固定的时候,我们需要重新计算a点坐标,
见下图,

image.png

a是我们真实的手指触碰的坐标,a1则为我们需要计算出来的触碰坐标,从上图可知,△acb相似△a1b1c1,并且acfd区域相似a1c1d1f,那么通过相似原理我们可以得到fb1/fc1 = fb/fc;


从而得到,fb1= fb * fc1/fc;,


已知:

fb = f.x - a.x;

fc1 = size.width;

fc = f.x-c.x;


同理 fd1/fd = fb1/fb; 得到,fd1 = fb1 * fd/fb; 即可得到a1点坐标。


计算代码:


double fc = f.x - cx;
double fa = f.x - a.x;

double bb1 = size.width * fa / fc;

double fd1 = f.y - a.y;
double fd = bb1 * fd1 / fa;

a1 = Point(f.x - bb1, f.y - fd);

这时候我们再来看下效果,


Jul-26-2022 14-45-21.gif


c点坐标被我们设定最小值为书籍最左侧,所以左侧不会被翻出区域,看起来更像真实的翻页效果。


添加阴影


我们可以在灯光下找本书翻页看下阴影效果,差不多是这个样子,这里我将阴影分为三个部分,A区域两个和C区域一个。


image.png


我们先添加A左区域的阴影,A左区域的阴影可以认为是从ha方向由h向a进行色值渐变,所以这里我们需要得到A左阴影区域左上角坐标点,也就是ha直线向外延伸固定数值的坐标。


image.png

可以理解为数学题表达:


已知ha直线方程式和a点坐标, 以a为圆心,画半径为r(r>0)的圆,


image.png


求:此圆和ha直线的相交的坐标。


设交点为坐标xy,可得 x²+y² =r²; y = kx+b;(k、b 、r)已知,最终我们得到一个一元二次方程。会解出两个坐标点,这里我们只需要往外延伸的坐标点就行,具体可以跟a点坐标判断得出,之后我们令double m1 = a.x-p1.x;double n1 = a.y-p1.y;


image.png


那么阴影外部曲线就可以用下方代码表示。


pyy1.moveTo(p.value.c.x - m1, p.value.c.y);
pyy1.quadraticBezierTo(p.value.e.x - m1, p.value.e.y - n1,
p.value.b.x - m1, p.value.b.y - n1);
pyy1.lineTo(p.value.p.x, p.value.p.y);
pyy1.lineTo(p.value.k.x, p.value.k.y);
pyy1.lineTo(p.value.f.x, p.value.f.y);
pyy1.close();

绘制出来看下效果

image.png

同理路径联合下:


Path startYY =
Path.combine(PathOperation.reverseDifference, mPathA, pyy1);

得到:

image.png

接下来通过设置画笔属性由a点向p1点进行渐变。


..shader = ui.Gradient.linear(
Offset(p.value.a.x, p.value.a.y),
Offset(p.value.p.x, p.value.p.y),
[Colors.black26, Colors.transparent]

效果:

image.png


这里我设置了由 black26,向透明渐变。延伸长度为10的效果,这里可以根据半径和色值调整影深。


A右同理,略...


效果:

image.png


接下来我们绘制C区域的阴影,C区域可以看到他是跟eh是平行的,那么我们连接c、j、h、e点,


// 右下
Path pr = Path();
pr.moveTo(p.value.c.x, p.value.c.y);
pr.lineTo(p.value.j.x, p.value.j.y);
pr.lineTo(p.value.h.x, p.value.h.y);
pr.lineTo(p.value.e.x, p.value.e.y);
pr.close();

得到下面效果:

image.png


继续与AB区域进行路径联合,


Path p1 = Path.combine(PathOperation.intersect, pr, mPathAB);

得到下面效果:


image.png


继续与B区域再次联合,


Path p2 = Path.combine(PathOperation.difference, p1, mPathB);

最终得到我们想要的阴影区域。


image.png


接下来就是跟A区域操作一样了,设置线性渐变色和渐变方向,这里渐变方向的坐标点我们为u点和g点,g点已知,主要求u点坐标,u点坐标为afdi直线的相交点。


image.png


通过两条直线方程求相交点,得到u点以后,设置渐变色和渐变方向。


核心代码:


// 右下
Path pc = Path();
pc.moveTo(p.value.c.x, p.value.c.y);
pc.lineTo(p.value.j.x, p.value.j.y);
pc.lineTo(p.value.h.x, p.value.h.y);
pc.lineTo(p.value.e.x, p.value.e.y);
pc.close();

Path p1 = Path.combine(PathOperation.intersect, pc, mPathA);
Path p2 = Path.combine(PathOperation.difference, p1, mPathB);

Offset u = Offset(
PaperPoint.toTwoPoint(p.value.a, p.value.f, p.value.d, p.value.i)
.x,
PaperPoint.toTwoPoint(p.value.a, p.value.f, p.value.d, p.value.i)
.y);
canvas.drawPath(
p2,
paint
..style = PaintingStyle.fill
..shader = ui.Gradient.linear(
u, Offset(p.value.g.x,p.value.g.y), [Colors.black26, Colors
.transparent]));

最后得到我们最终的效果。


image.png


这里阴影部分可能有些瑕疵,尤其上方a点坐标的处理有点生硬,但是没找到好的方式。以后有时间再优化。


翻页动画、回弹动画


目的: 我们希望可以滑动过程中页码可以自动翻过去,并且误触的情况下不要翻页。


这里我简单的判断当翻过去书籍宽度的3/1就理解为用户想翻页,当手势松开时自动翻过去;

当翻过去书籍宽度小于1/3,理解为用户误触并不想翻页,当手势松开自动回弹回去。


这里判断还可以根据用户滑动的速度进行判断,比如按下和松开之间的时间很快并且有想左滑动的距离,我们就可以判定用户想要翻页,不过这里就需要不断的调试优化达到一个比较理想的交互。


初始化动画


回弹动画,我们希望松开手指时,a点坐标回到和f点重合,这里我们需要在点击或移动的过程中保存当前手指触摸的坐标a


var move = d.localPosition;
// 临界值书籍以外区域 取消更新
if (move.dx >= size.width ||
move.dx < 0 ||
move.dy >= size.height ||
move.dy < 0) {
return;
}
currentA = Point(move.dx, move.dy);
...
if ((size.width - move.dx) / size.width > 1 / 3) {
isNext = false;
} else {
isNext = true;
}

然后通过动画将a点坐标置位f点;


Point currentA = Point(0, 0);
late AnimationController _controller = AnimationController(
vsync: this, duration: Duration(milliseconds: 800))
..addListener(() {
if (isNext) {
/// 不翻页 回到原始位置
_p.value = PaperPoint(
Point(
currentA.x + (size.width - currentA.x) * _controller.value,
currentA.y + (size.height - currentA.y) * _controller.value,
),
size);
} else {
/// 翻页
_p.value = PaperPoint(
Point(currentA.x - (currentA.x + size.width) * _controller.value,
currentA.y + (size.height - currentA.y) * _controller.value),
size);
}
});

翻页,我们希望a点坐标和(-f.x,f.y)重合,也就是f.x为负值,相当也我们书籍彻底翻过去,


这里需要注意的是当a.x<0时,也就是书籍左侧外面区域,这里需要将我们之前设定c值的最小值放开,否则无法彻底翻过去。


只有a.x>0才限制cx坐标点
if (a.x > 0) {
if (cx <= 0) {
// // 临界点
double fc = f.x - cx;
double fa = f.x - a.x;
double bb1 = size.width * fa / fc;
double fd1 = f.y - a.y;
double fd = bb1 * fd1 / fa;
a = Point(f.x - bb1, f.y - fd);
g = Point((a.x + f.x) / 2, (a.y + f.y) / 2);
e = Point(g.x - (pow((f - g).y, 2) / (f - g).x), f.y);
cx = 0;
}
}

ok,有了这些数据以后,我们看下效果。


Jul-26-2022 14-57-29.gif


填充内容


最后一步,填充内容,模拟书籍嘛,当然不能是这些纯色翻页了,上面我们有了A B C三个路径的区域,接下来就需要对书籍内容Widget进行裁剪,这里我们需要路径裁剪类ClipPath类,


// 裁剪的路径区域 默认组件的矩形区域
final CustomClipper? clipper;

const ClipPath({
Key? key,
this.clipper,
this.clipBehavior = Clip.antiAlias,
Widget? child,
}) : assert(clipBehavior != null),
super(key: key, child: child);


可以看到构造里有三个参数,除了子组件,clipBehavior是裁剪方式,可以设置抗锯齿等,clipper则是我们的核心裁剪方法,需要实现CustomClipper类里的Path getClip(Size size);方法。

通过它返回一个Path路径,即可将child进行自定义裁剪。


ok, 有了方法,接下来我们开始实现,首先我们将之前A区域的Path路径拿出来,裁剪当前页,通过Stack帧布局加载当前页和下一页内容,下一页内容永远在第一页内容下面,当翻过去动画结束时将下方页置位当前页,刷新第二页数据。


翻页动画结束当前页index+1;


if (status == AnimationStatus.completed) {
if (!isNext) {
setState(() {
currentIndex++;
});
}
}

填充内容布局代码:


// 定义电子书数据
List dataList = [
"第一页数据",
"第二页数据",
"第三页数据",
];

GestureDetector(
child: Stack(
children: [
currentIndex == dataList.length - 1
? SizedBox()
// 下一页
: ClipPath(
child: Container(
alignment: Alignment.center,
color: Colors.blue,
width: size.width,
height: size.height,
child: Text(
dataList[currentIndex + 1],
style: TextStyle(fontSize: 20),
),
),
),
// // 当前页
ClipPath(
child: Container(
alignment: Alignment.center,
width: size.width,
height: size.height,
color: Colors.blue,
child: Text(
dataList[currentIndex],
style: TextStyle(fontSize: 20),
),
),
clipper: CurrentPaperClipPath(_p),
),

// 最上面只绘制B区域和阴影
CustomPaint(
size: size,
painter: _BookPainter(
_p,
),
),
],
),
onPanDown: (d) {
if (currentIndex == dataList.length - 1) {
ToastUtil.show("最后一页了");
return;
}
isNext = false;
var down = d.localPosition;
_p.value = PaperPoint(Point(down.dx, down.dy), size);
currentA = Point(down.dx, down.dy);
},
onPanUpdate: currentIndex == dataList.length - 1
? null
: (d) {
var move = d.localPosition;

// 临界值取消更新
if (move.dx >= size.width ||
move.dx < 0 ||
move.dy >= size.height ||
move.dy < 0) {
return;
}
currentA = Point(move.dx, move.dy);
_p.value = PaperPoint(Point(move.dx, move.dy), size);

if ((size.width - move.dx) / size.width > 1 / 3) {
isNext = false;
} else {
isNext = true;
}
},
onPanEnd: currentIndex == dataList.length - 1
? null
: (d) {
_controller.forward(
from: 0,
);
},
),



/// 当前页区域
class CurrentPaperClipPath extends CustomClipper {
ValueNotifier p;

CurrentPaperClipPath(
this.p,
) : super(reclip: p);

@override
Path getClip(Size size)
{
///书籍区域
Path mPath = Path();
mPath.addRect(Rect.fromCenter(
center: Offset(size.width / 2, size.height / 2),
width: size.width,
height: size.height));

Path mPathA = Path();
if (p.value.a != p.value.f && p.value.a.x > -size.width) {
print("当前页 ${p.value.a} ${p.value.f}");
mPathA.moveTo(p.value.c.x, p.value.c.y);
mPathA.quadraticBezierTo(
p.value.e.x, p.value.e.y, p.value.b.x, p.value.b.y);
mPathA.lineTo(p.value.a.x, p.value.a.y);
mPathA.lineTo(p.value.k.x, p.value.k.y);
mPathA.quadraticBezierTo(
p.value.h.x, p.value.h.y, p.value.j.x, p.value.j.y);
mPathA.lineTo(p.value.f.x, p.value.f.y);
mPathA.close();
Path mPathC =
Path.combine(PathOperation.reverseDifference, mPathA, mPath);
return mPathC;
}

return mPath;
}

@override
bool shouldReclip(covariant CurrentPaperClipPath oldClipper)
{
return p != oldClipper.p;
}
}

最终看下效果.


Jul-26-2022 15-05-31.gif


返回上一页


上面只有翻页,没有返回上一页,其实返回上一页也很简单,上面我们实现了回弹动画,这里只需要修改当前a点坐标为为书籍左侧外面,之后调用回弹动画,当前页面-1即可。非常简单。


ElevatedButton(
onPressed: () {
setState(() {
// 表示从页面左侧外面开始回弹
currentA = Point(-100, size.height - 100);
currentIndex--;
// 回弹动画
isNext = false;
});
// _p.value = PaperPoint(currentA, size);
_controller.forward(
from: 0,
);
},
child: Text("上一页"))

下面再看下最终效果:


Jul-26-2022 15-14-19.gif


这里示例只是简单的填充了一个Text文本,更多内容也是可以的,毕竟裁剪的是个Widget。


总结


翻页示例可以说是手势和绘制的典型结合,实现过程中也是踩了许多的坑,网上找了很多资料,并且实现原理上也用到了一些初中数学知识,总的来说,过程还是比较曲折的,本篇文章主要讲了我在实现的过程中的一个详细过程及思路,代码目前先不传了,毕竟现在还是有些小问题,后续有时间再优化吧,后续有时间也许会将他优化下,做成一个开源组件,ok,那本篇文章到这里就结束了,希望对你有所帮助~


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

Android | ViewModel源码分析

前言ViewMode 是我们日常开发中最常用的组件之一,也是实现 MVVM 模式不可缺少的一环,这篇文章将从使用到源码分析以及常见的一些知识点来分析一下 ViewModel了解 ViewModelViewModel 旨在注重生命周期的方式存储和管理界面的相关数...
继续阅读 »

前言

ViewMode 是我们日常开发中最常用的组件之一,也是实现 MVVM 模式不可缺少的一环,这篇文章将从使用到源码分析以及常见的一些知识点来分析一下 ViewModel

了解 ViewModel

ViewModel 旨在注重生命周期的方式存储和管理界面的相关数据,ViewModel 类可以再发生旋转等配置更改后继续留存。

一般 ViewModel 配合 LiveData / Flow 实现数据驱动,由于 Activity 存在因配置改变而重建的机制,就会造成页面的数据丢失,例如网络数据已经其他数据等,而 ViewModel 可以应对 Activity 应配置而改变的场景,再重建的过程中恢复数据,从而降低用户体验受损。

ViewModel 生命周期如下:

 ViewModel 随着 Activity 状态的改变而经历的生命周期。

上图说明了 Activity 经历屏幕旋转而后结束的各种生命周期状态,旁边显示的就是 ViewModel 的生命周期了。

ViewModel 的使用

class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers()
}
}

fun getUsers(): LiveData<List<User>> {
return users
}

private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
val model: MyViewModel by viewModels()
model.getUsers().observe(this, Observer<List<User>>{ users ->
// update UI
})

ViewModel 的创建方式

  • 方式1:通过 ViewModelProvider 创建

    ViewModelProvider(this).get(WorkViewModel::class.java)

    也可以使用带工厂的创建方式

    ViewModelProvider(this, WorkViewModelFactory()).get(WorkViewModel::class.java)

    class WorkViewModelFactory() : ViewModelProvider.Factory {

    private val repository = WorkRepository()

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
    return WorkViewModel(repository) as T
    }
    }
  • 方式2:使用 Kotlin by 委托属性,实际上也是使用了 ViewModelProvider

    private val viewModel by viewModels<UserViewModel>()
  • 方式3:使用 Hilt 进行注入

ViewModel 源码分析

Viewmodel 创建的方法最终都是通过 ViewModelProvider 来完成的,他可以理解为创建 ViewModel 的工具类,在创建的时候需要两个参数:

  • ViewModelStoreOwner

    对应着 Activity / Fragment 等持有 Viewmode 的宿主,他们内部通过 ViewModelStore 维持一个 ViewModel 的映射表,ViewModelStore 是实现 ViewModel 作用域和数据恢复的关键。

  • Factory

    对于于创建 ViewModel 的工厂,如果没有传采用默认的 NewInstanceFactory 工厂反射创建 VIewModel 的实例。

创建完 ViewModelProvider 工具类后,就可以调用 get 方法来创建 ViewModel 的实例。get 方法会先从映射表 ViewModelStore 中读取缓存,若没有命中,则通过 VIewModel 的工厂创建实例在缓存到映射表中。

ViewModelProvider

//使用默认的工厂创建 ViewModel
public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
this(owner.getViewModelStore(), ...NewInstanceFactory.getInstance());
}

//指定工厂
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
this(owner.getViewModelStore(), factory);
}
//记录宿主的 viewmodelStore 和 factory
public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
mFactory = factory;
mViewModelStore = store;
}
private static final String DEFAULT_KEY =
"androidx.lifecycle.ViewModelProvider.DefaultKey";

@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
String canonicalName = modelClass.getCanonicalName();
if (canonicalName == null) {
throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
}
//使用 Default_key + 类名作为缓存的 key
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}

/** 通常是 fragment 使用*/
@SuppressWarnings("unchecked")
@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
//先从 viewModelStore 中获取缓存
ViewModel viewModel = mViewModelStore.get(key);

if (modelClass.isInstance(viewModel)) {
if (mFactory instanceof OnRequeryFactory) {
((OnRequeryFactory) mFactory).onRequery(viewModel);
}
return (T) viewModel;
}
//使用 factory 创建 ViewModel
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
} else {
viewModel = mFactory.create(modelClass);
}
//存储到 viewModelStore 中
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}

NewInstanceFactory

public static class NewInstanceFactory implements Factory {
private static NewInstanceFactory sInstance;

@NonNull
static NewInstanceFactory getInstance() {
if (sInstance == null) {
sInstance = new NewInstanceFactory();
}
return sInstance;
}


@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//反射创建 ViewModel
try {
return modelClass.newInstance();
}....
}
}

by viewModels

ActivityViewModelLazy

@MainThread
public inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}

return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}

ViewModelLazy

public class ViewModelLazy<VM : ViewModel> (
private val viewModelClass: KClass<VM>,
private val storeProducer: () -> ViewModelStore,
private val factoryProducer: () -> ViewModelProvider.Factory
) : Lazy<VM> {
private var cached: VM? = null

override val value: VM
get() {
val viewModel = cached
//如果第一次调用 by viewModels,则先初始化再返回
return if (viewModel == null) {
val factory = factoryProducer()
val store = storeProducer()
//最终是通过 ViewModelProvider 来创建
ViewModelProvider(store, factory).get(viewModelClass.java).also {
cached = it
}
} else {
//否则直接返回
viewModel
}
}

override fun isInitialized(): Boolean = cached != null
}

ViewModelStoreOwner

ViewModel 的宿主是 ViewModelStoreOwner 接口的实现类,例如 ComponentActivity,Fragment 等

public interface ViewModelStoreOwner {
@NonNull
ViewModelStore getViewModelStore();
}

该接口的实现的责任就是在配置期间保留拥有的 ViewModelStore,并在销毁的时候

此接口实现的责任是在配置更改期间保留拥有的 ViewModelStore 并在此范围将被销毁的时候调用 ViewModelStore.clear()

ComponentActivity
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
ContextAware,
LifecycleOwner,
ViewModelStoreOwner.... {

static final class NonConfigurationInstances {
Object custom;
ViewModelStore viewModelStore;
}

//viewmodel 的存储容器
private ViewModelStore mViewModelStore;
//创建 viewmodel 的工厂
private ViewModelProvider.Factory mDefaultFactory;

@NonNull
@Override
public ViewModelStore getViewModelStore() {
//.....
ensureViewModelStore();
return mViewModelStore;
}

void ensureViewModelStore() {
if (mViewModelStore == null) {
//先从配置文件中获取,看能不能获取到
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
mViewModelStore = nc.viewModelStore;
}
//如果没有获取到则重新创建 ViewModelStore
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
}
//重建时保存 viewModelStore
// ViweModelStore 会被封装为 NonConfigurationInstances 类,然后保存在 NonConfigurationInstances 类的 Object activity 属性中。
//前一个 NonConfigurationInstances 是 ComponentActivity 中定义的,后一个是 Activity 类中定义的,不是同一个类,不要搞混了哟!
public final Object onRetainNonConfigurationInstance() {
Object custom = onRetainCustomNonConfigurationInstance();
ViewModelStore viewModelStore = mViewModelStore;
if (viewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
viewModelStore = nc.viewModelStore;
}
}
if (viewModelStore == null && custom == null) {
return null;
}

NonConfigurationInstances nci = new NonConfigurationInstances();
nci.custom = custom;
nci.viewModelStore = viewModelStore;
return nci;
}
}

上面代码中的 NonConfigurationInstances 是一个配置文件的实例,当 activity 重建时,最终会调用到 onRetainNonConfigurationInstance() 方法中对 viewModelStore 进行缓存。所以上面才是先尝试从配置文件中获取,最后再创建新的 ViewModelStore。

Fragment
@NonNull
@Override
public ViewModelStore getViewModelStore() {
return mFragmentManager.getViewModelStore(this);
}
//fragment 中 ViewModel 的映射
private final HashMap<String, ViewModelStore> mViewModelStores = new HashMap<>();

@NonNull
ViewModelStore getViewModelStore(@NonNull Fragment f) {
return mNonConfig.getViewModelStore(f);
}

@NonNull
ViewModelStore getViewModelStore(@NonNull Fragment f) {
ViewModelStore viewModelStore = mViewModelStores.get(f.mWho);
if (viewModelStore == null) {
viewModelStore = new ViewModelStore();
mViewModelStores.put(f.mWho, viewModelStore);
}
return viewModelStore;
}

关于 ViewModel 的一些问题

  1. ViewModel 如何实现不同的作用域

    在使用 ViewModelProvider 时,需要传入一个 ViewModelStoreOwner 接口,这个接口的 getViewModelStore 会返回对应的 ViewModelStore 实例。

    对于 Activity 来说,ViewModelStore 是直接保存在成员变量中的。

    对于 Fragment 来说, ViewModelstore 是间接的存储在 FragmentManagerViewModel 中的 map 中。

    这样就实现了不同的 activity 或者 fragment 分别对应不同的 ViewModelStore 实例,进而区分不同的作用域

  2. 为什么 Activity 可以再重建后恢复 viewMdoel

    当 Activity 因为配置而发生重建时,我们可以将页面上的数据分为两类:

    1. 配置数据,例如窗口大小,主题资源等,当配置发生改变后,需要重新读取这些配置,因此这些数据在配置改变后就失去了意义,也就没有存在的价值

    2. 非配置数据,这些数据就是一些用户自己的信息,以及页面上显示的数据,这些数据和配置没有关系,如果丢失掉就会造成比较大的用户体验。

    说以,Activity 再重建时支持恢复非配置的数据,整个过程如下:

    1. 重建时保存数据

      Activity 再重建时会调用 retainNonConfigurationInstances 方法,在里面会获取需要保存的数据,例如 fragment ,activity 等数据,最后打包为 NonConfigurationInstances 类,保存在 ActivityClientRecord 中。

      NonConfigurationInstances retainNonConfigurationInstances() {
      //activity 中的非配置数据,例如 viewmodelStore
      //该方法需要子类实现 ,例如 ComponentActivity
      Object activity = onRetainNonConfigurationInstance();
      //fragment 中的非配置数据
      FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();
      // .....
      //构建 NonConfigurationInstances
      NonConfigurationInstances nci = new NonConfigurationInstances();
      nci.activity = activity;
      nci.fragments = fragments;
      return nci;
      }
    2. 恢复数据

      Activity 重新启动的最后,会通过 ActivityThread 中的 performLaunchActivity() 来完成整个启动过程,再这个方法中会通过类加载器来创建 Activity 对象,并调用 attach 方法为其关联所需要的一些信息。

      我们需要关注的就是 attach 方法:

      final void attach(NonConfigurationInstances lastNonConfigurationInstances //.. ) {
      //.....
      mLastNonConfigurationInstances = lastNonConfigurationInstances;
      }

      最后,数据被保存在了 Activity . mLastNonConfigurationInstances 成员变量中。

    3. 获取数据

      这个我们之前已经分析过了,我们简单回顾一下

      void ensureViewModelStore() {
      if (mViewModelStore == null) {
      //获取之前保存的数据
      NonConfigurationInstances nc =
      (NonConfigurationInstances) getLastNonConfigurationInstance();
      if (nc != null) {
      // Restore the ViewModelStore from NonConfigurationInstances
      mViewModelStore = nc.viewModelStore;
      }
      if (mViewModelStore == null) {
      mViewModelStore = new ViewModelStore();
      }
      }
      }

      @Nullable
      public Object getLastNonConfigurationInstance() {
      //mLastNonConfigurationInstances 就是 attach 中保存的
      return mLastNonConfigurationInstances != null
      ? mLastNonConfigurationInstances.activity : null;
      }

    至此,就完成了 ViewModel 的数据恢复了。

  3. Activity 重建的过程

    再 Activity 重建时,系统会执行 Relaunch 重建过程。在这个过程中通过 ActivityClientRecord 来完成信息传递,并销毁 Activity,紧接着马上重建同一个 Activity。

    这些操作都是在 ActivityThread 中完成的:

    private void handleRelaunchActivityInner(ActivityClientRecord r //...) {
    final Intent customIntent = r.activity.mIntent;
    //处理 onPause
    performPauseActivity(r, false, reason, null /* pendingActions */);
    //处理 onStop
    callActivityOnStop(r, true /* saveState */, reason);
    //1
    handleDestroyActivity(r.token, false, configChanges, true, reason);
    //2
    handleLaunchActivity(r, pendingActions, customIntent);
    }
    //1
    public void handleDestroyActivity(IBinder token, boolean finishing,//...) {
    ActivityClientRecord r = performDestroyActivity(token, finishing,
    configChanges, getNonConfigInstance, reason);
    }
    ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
    int configChanges, boolean getNonConfigInstance, String reason) {
    ActivityClientRecord r = mActivities.get(token);
    if (r != null) {
    if (getNonConfigInstance) {
    //调用 activity 的 retainNonConfigurationInstances 方法
    r.lastNonConfigurationInstances
    = r.activity.retainNonConfigurationInstances();
    }
    }
    return r;
    }
    //2
    public Activity handleLaunchActivity(ActivityClientRecord r,
    PendingTransactionActions pendingActions, Intent customIntent) {
    final Activity a = performLaunchActivity(r, customIntent);
    return a;
    }
    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

    activity = mInstrumentation.newActivity(
    cl, component.getClassName(), r.intent);
    //...
    //传递缓存数据以及其他数据
    activity.attach(appContext, this, getInstrumentation(), r.token,
    r.ident, app, r.intent, r.activityInfo, title, r.parent,
    r.embeddedID, r.lastNonConfigurationInstances, config,
    r.referrer, r.voiceInteractor, window, r.configCallback,
    r.assistToken);
    //....
    return activity;
    }

    上面的代码主要可以分为两部分:

    第一处:再处理 onDestory 逻辑时,调用 retainNonConfigurationInstances() 方法获取非配置数据,并临时保存在 ActivityClientRecord 上。

    第二处:再 Launch 新 activity 的时候通过 attach 方法将数据传到新 activity 中即可

    至此旧的 Activity 数据已经被传递到新的 Activity 中了。

  4. ViewModel 的数据在什么时候才会清除

    ViewModel 的数据会在 Activity 非配置变化销毁时清除,具体分为三种情况

    1. 直接调用 finish 或者按返回键退出
    2. 异常退出 Activity,例如内存不足
    3. 强制退出应用

    前两种都属于非配置变更触发的,再 Activity中存在一个 Lifecycle 的监听,当 Activity 进入 Destory 状态时,如果 Activity 不处于配置重建阶段,将调用 viewModelStore.clear() 清除 viewmodel 数据。

    public ComponentActivity() {
    Lifecycle lifecycle = getLifecycle();
    getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
    @NonNull Lifecycle.Event event) {
    if (event == Lifecycle.Event.ON_DESTROY) {
    // Clear out the available context
    mContextAwareHelper.clearAvailableContext();
    //是否处于配置变更引起的重建
    if (!isChangingConfigurations()) {
    getViewModelStore().clear();
    }
    }
    }
    });
    }
  5. ViewModel 和 onSaveInstanceState 对比

    这两种都是对数据恢复的机制,但是他们针对的场景不同,导致他们的实现原理也不同,进而优缺点也不同

    viewModel:使用常见针对于配置变更中的非配置数据恢复,由于数据是直接存储在内存中的,所以他的读取速度非常快,并且支持存储大数据,但是会收到内存空间的限制

    onSaveInstanceState:针对于应用被系统回收后重建时的数据恢复,由于应用进程坑会在这个过程中消亡,所以不能存在内存中,只能进行持久化存储,并且这种方式的数据传递是通过 Bundle 传递的,会受到 Binder 事务缓冲区的大小限制,只能存储小规模数据。

    这里借用一张大佬的图,来看一下具体的优缺点:

    https://juejin.cn/post/7121998366103306254#heading-12

总结

到这里,ViewModel 就整个分析完了,如果有任何问题可直接留言评论,谢谢!


作者:345丶
链接:https://juejin.cn/post/7124586548288290824
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

PermissionX 1.5发布,支持申请Android特殊权限啦

前言 Hello 大家早上好,说起 PermissionX,其实我已经有段时间没有更新这个框架了。一是因为现在工作确实比较忙,没有过去那么多的闲暇时间来写开源项目,二是因为,PermissionX 的主体功能已经相当稳定,并不需要频繁对其进行变更。 不过之前一...
继续阅读 »

前言


Hello 大家早上好,说起 PermissionX,其实我已经有段时间没有更新这个框架了。一是因为现在工作确实比较忙,没有过去那么多的闲暇时间来写开源项目,二是因为,PermissionX 的主体功能已经相当稳定,并不需要频繁对其进行变更。


不过之前一直有朋友在反映,对于 Android 中的一些特殊权限申请,PermissionX 并不支持。是的,PermissionX 本质上只是对 Android 运行时权限 API 进行了一层封装,用于简化运行时权限申请的。而这些特殊权限并不属于 Android 运行时权限的一部分,所以 PermissionX 自然也是不支持的。


但是特殊权限却是我们这些开发者们可能经常要与之打交道的一部分,它们并不难写,但是每次去写都感觉很繁琐。因此经慎重考虑之后,我决定将几个比较常用的特殊权限纳入 PermissionX 的支持范围。那么本篇文章我们就来看一看,对于这几个常见的特殊权限,使用 PermissionX 和不使用 PermissionX 的写法有什么不同之处。


事实上,Android 的权限机制也是经历过长久的迭代的。在 6.0 系统之前,Google 将权限机制设计的比较简单,你的应用程序需要用到什么权限,只需要在 AndroidManifest.xml 文件中声明一下就可以了。


但是从 6.0 系统开始,Android 引入了运行时权限机制。Android 将常用的权限大致归成了几类,一类是普通权限,一类是危险权限,一类是特殊权限。


普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,这种权限和过去一样,只需要在 AndroidManifest.xml 文件中声明一下就可以了,不需要做任何特殊处理。


危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等。这部分权限需要通过代码进行申请,并要用户手动同意才可获得授权。PermissionX 库主要就是处理的这种权限的申请。


而特殊权限则更加少见,Google 认为这种权限比危险权限还要敏感,因此不能仅仅让用户手动同意就可以获得授权,而是需要让用户到专门的设置页面去手动对某一个应用程序授权,该程序才能使用这个权限。


不过相比于危险权限,特殊权限没有非常固定的申请方式,每个特殊权限可能都要使用不同的写法才行,这也导致申请特殊权限比申请危险权限还要繁琐。


从 1.5.0 版本开始,PermissionX 对最常用的几个特殊权限进行了支持。正如刚才所说,特殊权限没有固定的申请方式,因此 PermissionX 也是针对于这几个特殊权限一个一个去适配并支持的。如果你发现你需要申请的某个特殊权限还没有被 PermissionX 支持,也可以向我提出需求,我会考虑在接下来的版本中加入。


在过去,我们发布开源库通常都是发布到 jcenter 上的,但是相信大家现在都已经知道了,jcenter 即将停止服务,具体可以参考我的这篇文章 浅谈 JCenter 即将被停止服务的事件


目前的 jcenter 处在一个半废弃的边缘,虽然还可以正常从 jcenter 下载开源库,但是已经不能再向 jcenter 发布新的开源库了。而在明年 2 月 1 号之后,下载服务也会被关停。


所以,以后要想再发布开源库我们只能选择发布到其他仓库,比如现在 Google 推荐我们使用 Maven Central。


于是,从 1.5.0 版本开始,PermissionX 也会将库发布到 Maven Center 上,之前的老版本由于迁移价值并不大,所以我也不想再耗费经历做迁移了。1.5.0 之前的版本仍然保留在 jcenter 上,提供下载服务直到明年的 2 月 1 号。


而关于如何将库发布到 Maven Central,请参考 再见 JCenter,将你的开源库发布到 MavenCentral 上吧


Android的特殊权限


Android 里具体有哪些特殊权限呢?


说实话,这个我也不太清楚。我所了解的特殊权限基本都是因为需要用到了,然后发现这个权限即不属于普通权限,也不属于危险权限,要用一种更加特殊的方式去申请,才知道原来这是一个特殊权限。


因此,PermissionX 1.5.0 版本中对特殊权限的支持,也就仅限于我知道的,以及从网友反馈得来的几个最为常用的特殊权限。


一共是以下 3 个:



  1. 悬浮窗

  2. 修改设置

  3. 管理外部存储


接下来我就分别针对这 3 个特殊权限做一下更加详细的介绍。


悬浮窗


悬浮窗功能在不少应用程序中使用得非常频繁,因为你可能总有一些内容是要置顶于其他内容之上显示的,这个时候用悬浮窗来实现就会非常方便。


当然,如果你只是在自己的应用内部实现悬浮窗功能是不需要申请权限的,但如果你的悬浮窗希望也能置顶于其他应用程序的上方,这就必须得要申请权限了。


悬浮窗的权限名叫做 SYSTEM_ALERT_WINDOW,如果你去查一下这个权限的文档,会发现这个权限的申请方式比较特殊:



按照文档上的说法,从 Android 6.0 系统开始,我们在使用 SYSTEM_ALERT_WINDOW 权限前需要发出一个 action 为 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 的 Intent,引导用户手动授权。另外我们还可以通过 Settings.canDrawOverlays() 这个 API 来判断用户是否已经授权。


因此,想要申请悬浮窗权限,自然而然就可以写出以下代码:


if (Build.VERSION.SDK_INT >= 23) {
if (Settings.canDrawOverlays(context)) {
showFloatView()
} else {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
startActivity(intent)
}
} else {
showFloatView()
}

看上去也不复杂嘛。


确实,但是它麻烦的点主要在于,它的请求方式是脱离于一般运行时权限的请求方式的,因此得要为它额外编写独立的权限请求逻辑才行。


而 PermissionX 的目标就是要弱化这种独立的权限请求逻辑,减少差异化代码编写,争取使用同一套 API 来实现对特殊权限的请求。


如果你已经比较熟悉 PermissionX 的用法了,那么以下代码你一定不会陌生:


PermissionX.init(activity)
.permissions(Manifest.permission.SYSTEM_ALERT_WINDOW)
.onExplainRequestReason { scope, deniedList ->
val message = "PermissionX需要您同意以下权限才能正常使用"
scope.showRequestReasonDialog(deniedList, message, "Allow", "Deny")
}
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
Toast.makeText(activity, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(activity, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
}
}

可以看到,这就是最标准的 PermissionX 的正常用法,但是我们在这里却用来请求了悬浮窗权限。也就是说,即使是特殊权限,在 PermissionX 中也可以用普通的方式去处理。


另外不要忘记,所有申请的权限都必须在 AndroidManifest.xml 进行注册才行:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.permissionx.app">

<uses-permission android: />

</manifest>

那么运行效果是什么样的呢?我们来看看吧:



可以看到,PermissionX 还自带了一个权限提示框,友好地告知用户我们需要悬浮窗权限,引导用户去手动开启。


修改设置


了解了悬浮窗权限的请求方式之后,接下来我们就可以快速过一下修改设置权限的请求方式了,因为它们的用法是完全一样的。


修改设置的权限名叫 WRITE_SETTINGS,如果我们去查看一下它的文档,你会发现它和刚才悬浮窗权限的文档简直如出一辙:



同样是从 Android 6.0 系统开始,在使用 WRITE_SETTINGS 权限前需要先发出一个 action 为 Settings.ACTION_MANAGE_WRITE_SETTINGS 的 Intent,引导用户手动授权。然后我们还可以通过 Settings.System.canWrite() 这个 API 来判断用户是否已经授权。


所以,如果是自己手动申请这个权限,相信你已经知道要怎么写了。


那么用 PermissionX 申请的话应该要怎么写呢?这个当然就更简单了,只需要把要申请的权限替换一下即可,其他部分都不用作修改:


PermissionX.init(activity)
.permissions(Manifest.permission.WRITE_SETTINGS)
...

当然,不要忘记在 AndroidManifest.xml 中注册权限:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.permissionx.app">

<uses-permission android: />

</manifest>

运行一下,效果如下图所示:



管理外部存储


管理外部存储权限也是一种特殊权限,它可以允许你的 App 拥有对整个 SD 卡进行读写的权限。


有些朋友可能会问,SD 卡本来不就是可以全局读写的吗?为什么还要再申请这个权限?


那你一定是没有了解 Android 11 上的 Scoped Storage 功能。从 Android 11 开始,Android 系统强制启用了 Scoped Storage,所有 App 都不再拥有对 SD 卡进行全局读写的权限了。


关于 Scoped Storage 的更多内容,可以参考我的这篇文章 Android 11 新特性,Scoped Storage 又有了新花样


但是如果有的应用就是要对 SD 卡进行全局读写该怎么办呢(比如说文件浏览器)?


不用担心,Google 仍然还是给了我们一种解决方案,那就是请求管理外部存储权限。


这个权限是 Android 11 中新增的,为的就是应对这种特殊场景。


那么这个权限要怎么申请呢?我们还是先来看一看文档:



大致可以分为几步吧:


第一,在 AndroidManifest.xml 中声明 MANAGE_EXTERNAL_STORAGE 权限。


第二,发出一个 action 为 Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 的 Intent,引导用户手动授权。


第三,调用 Environment.isExternalStorageManager() 来判断用户是否已授权。


传统请求权限的写法我就不再演示了,使用 PermissionX 来请求的写法仍然也还是差不多的。只不过要注意,因为 MANAGE_EXTERNAL_STORAGE 权限是 Android 11 系统新加入的,所以我们也只应该在 Android 11 以上系统去请求这个权限,代码如下所示:


if (Build.VERSION.SDK_INT >= 30) {
PermissionX.init(this)
.permissions(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
...
}

AndroidManifest.xml 中的权限如下:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.permissionx.app">

<uses-permission android: />

</manifest>

运行一下程序,效果如下图所示:



这样我们就拥有全局读写 SD 卡的权限了。


另外 PermissionX 还有一个特别方便的地方,就是它可以一次性申请多个权限。假如我们想要同时申请悬浮窗权限和修改设置权限,只需要这样写就可以了:


PermissionX.init(activity)
.permissions(Manifest.permission.SYSTEM_ALERT_WINDOW, Manifest.permission.WRITE_SETTINGS)
...

运行效果如下图所示:



当然你也可以将特殊权限与普通运行时权限放在一起申请,PermissionX 对此也是支持的。只有当所有权限都请求结束时,PermissionX 才会将所有权限的请求结果一次性回调给开发者。


关于 PermissionX 新版本的内容变化就介绍到这里,升级的方式非常简单,修改一下 dependencies 当中的版本号即可:


repositories {
google()
mavenCentral()
}


dependencies {
implementation 'com.guolindev.permissionx:permissionx:1.5.0'
}

注意现在一定要使用 mavenCentral 仓库,而不能再使用 jcenter 了。


如果你对 PermissionX 的源码感兴趣,可以访问 PermissionX 的项目主页:


github.com/guolindev/P…


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

IDEA 版防沉迷插件,有点意思!

分享一个 IDEA 编码防沉迷插件,特别适合沉迷编码无法自拔的朋友使用。这个插件诞生已经有一年多了,目前在 IDEA 官方有 10.3k 的下载量,算是一个不错的成绩了。 前言当初年少懵懂,那年夏天填志愿选专业,父母听其他长辈说选择计算机专业好。从那以后,我的...
继续阅读 »

分享一个 IDEA 编码防沉迷插件,特别适合沉迷编码无法自拔的朋友使用。这个插件诞生已经有一年多了,目前在 IDEA 官方有 10.3k 的下载量,算是一个不错的成绩了。


前言

当初年少懵懂,那年夏天填志愿选专业,父母听其他长辈说选择计算机专业好。从那以后,我的身上就有了计院深深的烙印。从寝室到机房,从机房到图书馆,C、C++、Java、只要是想写点自己感兴趣的东西,一坐就是几个小时,但那时年轻,起身,收拾,一路小跑会女神,轻轻松松。现在工作了,毫无意外的做着开发的工作,长时间久坐。写代码一忙起来就忘了起来活动一下,也不怎么喝水。经常等到忙完了就感觉腰和腿不舒服。直到今年的体检报告一下来,才幡然醒悟:没有一个好身体,就不能好好打工,让老板过上他自己想要的生活了。


试过用手机提醒自己,但是没用。小米手环的久坐提醒功能也开着,有时候写代码正入神的,时间到了也就点一下就关了,还是没什么作用。所以我想究竟是我太赖了,还是用 IDEA 写代码容易沉迷,总之不可能是改需求有意思。所以元旦节打算为自己开发一款小小的 IDEA 防沉迷插件,我叫她【StopCoding】。她应该可以设置每隔多少分钟,就弹出一个提醒对话框,一旦对话框弹出来,IDEA 的代码编辑框就自动失去了焦点,什么都不能操作,到这还不算完,关键是这个对话框得关不了,并且还显示着休息倒计时,还有即使我修改了系统时间,这个倒计时也依然有效,除非我打开任务管理器,关闭 IDEA 的进程,然后再重新启动 IDEA。但是想一下想,IDEA 都都关了,还是休息一下吧。

下面就介绍一下她简单的使用教程和开发教程。

安装使用教程

安装

  1. 在 IDEA 中直接搜索安装 StopCoding 插件(官方已经审核通过)

2. 内网开发的小伙伴 可以下载之后进行本地安装 下载地址

  • 本地安装:

使用

  • Step1. 然后在菜单栏中 tools->StopCoding

  • Step2. 设置适合你的参数然后保存。


  • Step3. 然后快乐的 Coding 吧,再不用担心自己会沉迷了。工作时间结束,她会弹出下框进行提醒,当然,


这个框是关不掉的.只有你休息了足够的时间它才会自动关闭。


开发教程

这个插件非常的简约,界面操作也很简单。所使用的技术基本上都是 Java 的基础编程知识。所以小伙伴感兴趣的话,一起看看吧。

技术范围

  • 插件工程的基本结构

  • Swing 主要负责两个对话框的交互

  • Timer 作为最基本的定时器选择

插件工程结构


  • plugin.xml

这是插件工程的核心配置文件,里面每一项的解释,可以参考第一篇的介绍核心配置文件说明。

  • data

    • SettingData :配置信息对应 model

    • DataCenter :作为运行时的数据中心,都是些静态的全局变量

  • service

    • TimerService :这个定时计算的核心代码

  • task

    • RestTask :休息时的定时任务

    • WorkTask :工作时的定时任务

  • ui

    • SettingDialog :设置信息的对话框

    • TipsDialog : 休息时提醒的对话框

  • StopCodingSettingAction :启动入口的 action

Swing

其实在 IDEA 中开发 Swing 项目的界面非常简单。因为 IDEA 提供了一系列可视化的操作,以及控件布局的拖拽。接下来就简单的介绍一下对话框的创建过程和添加事件。

创建对话框

  • Step1


  • Step2


  • Step3


  • 注:这里并没有详细的展开 Swing 的讲解,因为界面的这个东西,需要大家多去自己实践。这里就不做手册式的赘述了。

添加事件

其实,刚才创建的这个对话框里的两个按钮都是默认已经创建好了点击事件的。

public class TestDialog extends JDialog {
  private JPanel contentPane;
  private JButton buttonOK;
  private JButton buttonCancel;

  public TestDialog() {
      setContentPane(contentPane);
      setModal(true);
      getRootPane().setDefaultButton(buttonOK);

      buttonOK.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
              onOK();
          }
      }); //这是给OK按钮绑定点击事件的监听器

      buttonCancel.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
              onCancel();
          }
      });//这是给取消按钮绑定点击事件的监听器
  //其他代码
  }

当然我们也可以其它任何控件去创建不同的事件监听器。这里可以通过界面操作创建很多种监听器,只要你需要,就可以使用。

  • step1


  • step2


Timer 定时器

在这个插件里面,需要用到定时的功能,同时去计算公国和休息的时间。所以使用 JDK 自带的 Timer,非常的方便。下面我 Timer 的常用的 api 放在这里,就清楚它的使用了。

  • 构造方法

img

  • 成员防范

img

  • 主要是 schedule 去添加一个定时任务,和使用 cancel 去取消任务停止定时器。

最后

相信有了这些基本介绍,感谢兴趣的小伙伴想去看看源码和尝试自己写一个小插件就没什么大问题了。不说了,我得休息了。希望这个插件能帮到作为程序员得你,和这篇文章对你有一点点启发。当然麻烦小伙伴点个赞,鼓励一下打工人。

源码地址:https://github.com/jogeen/StopCoding


来源:https://sourl.cn/z8UiUv

收起阅读 »

请不要再下载这些vscode插件了

vscode好多插件都已经内置了,但是还是有很多憨批不知道,还在傻傻的推荐这些插件来坑萌新。 Auto Rename Tag 这个插件是在写html标签的时候可以重命名标签名的,但是现在vscode已经内置了,就不需要再下载这个插件了。只不过默认是关闭的...
继续阅读 »

vscode好多插件都已经内置了,但是还是有很多憨批不知道,还在傻傻的推荐这些插件来坑萌新。



  1. Auto Rename Tag


image.png


这个插件是在写html标签的时候可以重命名标签名的,但是现在vscode已经内置了,就不需要再下载这个插件了。只不过默认是关闭的,需要开启。


点击设置,搜索link,把这个勾选上,就可以左右重命名标签了。


在html和vue中可以自动重命名,而jsx中不行,如果有react开发的,那还是继续装上把。


image.png



  1. Auto Close Tag


image.png
这个插件是用来自动闭合html标签的,但是目前vscode已经内置了这个自动闭合标签的功能了,就不需要再下载了,默认是开启的。



  1. Bracket Pair Colorizer


image.png


这个标签是用来显示多个彩色括号的,但是目前vscode也内置了,所以也不用再下载了,默认是开启的。


如果没有开启,点击设置,搜索Bracket Pair,并勾选上。


image.png



  1. Guides


image.png


这个插件是用来显示代码层级的,但是vscode也已经内置了,默认是关闭的,在上面的配置中,把是否启用括号对指南改成true即可。



  1. CSS Peek


image.png
这个插件只是用于查找html的外部css样式,对于vue、react等文件是不起作用的,并且目前处于失效中。


6.HTML Snippets


image.png
该插件目前已不再维护。


未提到的,欢迎大家补充。



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

收起阅读 »

SDK无侵入初始化并获取Application

1.SDK无侵入初始化并获取Application 无侵入初始化SDK并获取Application的意思是不需要业务方手动调用SDK的初始化函数。 这个就得利用Android四大基本组件之一ContentProvider了,其执行的时机是位于Applicati...
继续阅读 »

1.SDK无侵入初始化并获取Application


无侵入初始化SDK并获取Application的意思是不需要业务方手动调用SDK的初始化函数。


这个就得利用Android四大基本组件之一ContentProvider了,其执行的时机是位于ApplicationattchBaseContext()之后,ApplicationonCreate()之前,无需程序手动调用。


所以我们就可以自定义个ContentProvider完成SDK的自动初始化并获取应用的Application。


class CPDemo : ContentProvider() {
override fun attachInfo(context: Context?, info: ProviderInfo?) {
super.attachInfo(context, info)
//编写SDK初始化逻辑,并获取Application
val application = context?.applicationContext
}

override fun onCreate(): Boolean = true
}

直接重写ContentProvider并在attachInfo执行SDK的初始化逻辑即可。


比较出名的内存泄漏检测库 LeakCanary、Google官方的ProcessLifecycleOwner就使用这个原理。


不过如果每个第三方库都借用ContentProvider来完成无侵入式的初始化,势必造成自定义的ContentProvider过多,直接增加了启动耗时:


image.png


为了避免ContentProvider过多的问题,Google官方提供了App Startup库,这个库主要是给SDK提供方实现无侵入初始化使用的:


    implementation("androidx.startup:startup-runtime:1.1.1")

该官方库会将所有用于初始化的ContentProvider合并成一个,减少启动的耗时


基本使用如下:


class CPDemo2 : Initializer<Unit> {
override fun create(context: Context) {
//执行初始化逻辑
}

override fun dependencies(): MutableList<Class<out Initializer<*>>> = mutableListOf()
}

然后再AndroidManifest中注册:


<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.gitlinux.CPDemo2"
android:value="androidx.startup" />
</provider>

更多用法可以参考郭神的文章:Jetpack新成员,App Startup一篇就懂


2.kotlin函数省略返回值类型真的好吗?


经常使用kotlin的程序都知道,kotlin的函数再某些场景下是可以不用显示声明返回值类型,这是为了提高开发效率,比如:


fun test() = ""

对于简单的函数来说,虽然省略了方法的返回类型,但是我们还是能够直接看出这个方法的返回值类型为String,但是方法中调用了其他方法呢,比如:


fun test() =  request()

//随便一个函数,这个函数体中还会调用其他的函数
fun request() = otherFun()

fun otherFun() = "hahaha"

这种情况下,如果我们要知道test()方法的返回值类型必须先通过request()函数再跳转到otherFun才能知道test()方法的返回值类型,这对于程序而言反而降低了开发效率。


所以我认为使用kotlin函数省略返回值类型的场景应该有一个前提:该函数的返回值类型程序能够很容易推断出来(尽量不依赖其他函数)


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

能说一说 Kotlin 中 lateinit 和 lazy 的区别吗?

使用 Kotlin 进行开发,对于 latelinit 和 lazy 肯定不陌生。但其原理上的区别,可能鲜少了解过,借着本篇文章普及下这方面的知识。 lateinit 用法 非空类型可以使用 lateinit 关键字达到延迟初始化。  class I...
继续阅读 »

使用 Kotlin 进行开发,对于 latelinit 和 lazy 肯定不陌生。但其原理上的区别,可能鲜少了解过,借着本篇文章普及下这方面的知识。


lateinit


用法


非空类型可以使用 lateinit 关键字达到延迟初始化。


 class InitTest() {
     lateinit var name: String
 
     public fun checkName(): Boolean = name.isNotEmpty()
 }

如果在使用前没有初始化的话会发生如下 Exception。


 AndroidRuntime: FATAL EXCEPTION: main
      Caused by: kotlin.UninitializedPropertyAccessException: lateinit property name has not been initialized
        at com.example.tiramisu_demo.kotlin.InitTest.getName(InitTest.kt:4)
        at com.example.tiramisu_demo.kotlin.InitTest.checkName(InitTest.kt:10)
        at com.example.tiramisu_demo.MainActivity.testInit(MainActivity.kt:365)
        at com.example.tiramisu_demo.MainActivity.onButtonClick(MainActivity.kt:371)
        ...

为防止上述的 Exception,可以在使用前通过 ::xxx.isInitialized 进行判断。


 class InitTest() {
     lateinit var name: String
 
     fun checkName(): Boolean {
         return if (::name.isInitialized) {
             name.isNotEmpty()
        } else {
             false
        }
    }
 }

 Init: testInit():false

当 name 初始化过之后使用亦可正常。


 class InitTest() {
     lateinit var name: String
 
     fun injectName(name: String) {
         this.name = name
    }
 
     fun checkName(): Boolean {
         return if (::name.isInitialized) {
             name.isNotEmpty()
        } else {
             false
        }
    }
 }

 Init: testInit():true

原理


反编译之后可以看到该变量没有 @NotNull 注解,使用的时候要 check 是否为 null。


 public final class InitTest {
    public String name;
       
    @NotNull
    public final String getName() {
       String var10000 = this.name;
       if (var10000 == null) {
          Intrinsics.throwUninitializedPropertyAccessException("name");
      }
 
       return var10000;
    }
 
     public final boolean checkName() {
       String var10000 = this.name;
       if (var10000 == null) {
          Intrinsics.throwUninitializedPropertyAccessException("name");
      }
 
       CharSequence var1 = (CharSequence)var10000;
       return var1.length() > 0;
    }
 }

null 则抛出对应的 UninitializedPropertyAccessException。


 public class Intrinsics {
  public static void throwUninitializedPropertyAccessException(String propertyName) {
         throwUninitializedProperty("lateinit property " + propertyName + " has not been initialized");
    }
 
  public static void throwUninitializedProperty(String message) {
         throw sanitizeStackTrace(new UninitializedPropertyAccessException(message));
    }
 
  private static <T extends Throwable> T sanitizeStackTrace(T throwable) {
         return sanitizeStackTrace(throwable, Intrinsics.class.getName());
    }
 
     static <T extends Throwable> T sanitizeStackTrace(T throwable, String classNameToDrop) {
         StackTraceElement[] stackTrace = throwable.getStackTrace();
         int size = stackTrace.length;
 
         int lastIntrinsic = -1;
         for (int i = 0; i < size; i++) {
             if (classNameToDrop.equals(stackTrace[i].getClassName())) {
                 lastIntrinsic = i;
            }
        }
 
         StackTraceElement[] newStackTrace = Arrays.copyOfRange(stackTrace, lastIntrinsic + 1, size);
         throwable.setStackTrace(newStackTrace);
         return throwable;
    }
 }
 
 public actual class UninitializedPropertyAccessException : RuntimeException {
    ...
 }

如果是变量是不加 lateinit 的非空类型,定义的时候即需要初始化。


 class InitTest() {
     val name: String = "test"
 
     public fun checkName(): Boolean = name.isNotEmpty()
 }

在反编译之后发现变量多了 @NotNull 注解,可直接使用。


 public final class InitTest {
    @NotNull
    private String name = "test";
 
    @NotNull
    public final String getName() {
       return this.name;
    }
 
    public final boolean checkName() {
       CharSequence var1 = (CharSequence)this.name;
       return var1.length() > 0;
    }
 }

::xxx.isInitialized 的话进行反编译之后可以发现就是在使用前进行了 null 检查,为空直接执行预设逻辑,反之才进行变量的使用。


 public final class InitTest {
    public String name;
    ...
    public final boolean checkName() {
       boolean var2;
       if (((InitTest)this).name != null) {
          String var10000 = this.name;
          if (var10000 == null) {
             Intrinsics.throwUninitializedPropertyAccessException("name");
          }
 
          CharSequence var1 = (CharSequence)var10000;
          var2 = var1.length() > 0;
      } else {
          var2 = false;
      }
 
       return var2;
    }
 }

lazy


用法


lazy 的命名和 lateinit 类似,但使用场景不同。其是用于懒加载,即初始化方式已确定,只是在使用的时候执行。而且修饰的只是能是 val 常量。


 class InitTest {
     val name by lazy {
         "test"
    }
     
     public fun checkName(): Boolean = name.isNotEmpty()
 }

lazy 修饰的变量可以直接使用,不用担心 NPE。


 Init: testInit():true

原理


上述是 lazy 最常见的用法,反编译之后的代码如下:


 public final class InitTest {
    @NotNull
    private final Lazy name$delegate;
 
    @NotNull
    public final String getName() {
       Lazy var1 = this.name$delegate;
       return (String)var1.getValue();
    }
 
    public final boolean checkName() {
       CharSequence var1 = (CharSequence)this.getName();
       return var1.length() > 0;
    }
 
    public InitTest() {
       this.name$delegate = LazyKt.lazy((Function0)null.INSTANCE);
    }
 }

所属 class 创建实例的时候,实际分配给 lazy 变量的是 Lazy 接口类型,并非 T 类型,变量会在 Lazy 中以 value 暂存,当使用该变量的时候会获取 Lazy 的 value 属性。


Lazy 接口的默认 mode 是 LazyThreadSafetyMode.SYNCHRONIZED,其默认实现是 SynchronizedLazyImpl,该实现中 _value 属性为实际的值,用 volatile 修饰。


value 则通过 get() 从 _value 中读写,get() 将先检查 _value 是否尚未初始化




  • 已经初始化过的话,转换为 T 类型后返回




  • 反之,执行同步方法(默认情况下 lock 对象为 impl 实例),并再次检查是否已经初始化:



    • 已经初始化过的话,转换为 T 类型后返回

    • 反之,执行用于初始化的函数 initializer,其返回值存放在 _value 中,并返回




 public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
 
 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
                }
            }
        }
 
     override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE
 
     override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
 
     private fun writeReplace(): Any = InitializedLazyImpl(value)
 }

总之跟 Java 里双重检查懒汉模式获取单例的写法非常类似。


 public class Singleton {
     private static volatile Singleton singleton;
 
     private Singleton() {
    }
 
     public static Singleton getInstance() {
         if (singleton == null) {
             synchronized (Singleton.class) {
                 if (singleton == null) {
                     singleton = new Singleton();
                }
            }
        }
         return singleton;
    }
 }

lazy 在上述默认的 SYNCHRONIZED mode 下还可以指定内部同步的 lock 对象。


     val name by lazy(lock) {
         "test"
    }

lazy 还可以指定其他 mode,比如 PUBLICATION,内部采用不同于 synchronizedCAS 机制。


     val name by lazy(LazyThreadSafetyMode.PUBLICATION) {
         "test"
    }

lazy 还可以指定 NONE mode,线程不安全。


     val name by lazy(LazyThreadSafetyMode.NONE) {
         "test"
    }

the end


lateinit 和 lazy 都是用于初始化场景,用法和原理有些区别,做个简单总结:


lateinit 用作非空类型的初始化:



  • 在使用前需要初始化

  • 如果使用时没有初始化内部会抛出 UninitializedPropertyAccess Exception

  • 可配合 isInitialized 在使用前进行检查


lazy 用作变量的延迟初始化:



  • 定义的时候已经明确了 initializer 函数体

  • 使用的时候才进行初始化,内部默认通过同步锁和双重校验的方式返回持有的实例

  • 还支持设置 lock 对象和其他实现 mode

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

Flutter 桌面探索 | 自定义可拖拽导航栏

1. 前言 上一篇 《桌面导航 NavigationRail》 中介绍了官方的桌面导航,但整体灵活性并不是太好,风格我也不是很喜欢。看到飞书桌面端的导航栏可以支持拖拽排序,感觉挺有意思。而且排序之后,下次进入时会使用该顺序,而且在其他设备上也会同步该配置顺序。...
继续阅读 »
1. 前言

上一篇 《桌面导航 NavigationRail》 中介绍了官方的桌面导航,但整体灵活性并不是太好,风格我也不是很喜欢。看到飞书桌面端的导航栏可以支持拖拽排序,感觉挺有意思。而且排序之后,下次进入时会使用该顺序,而且在其他设备上也会同步该配置顺序。这说明用户登录时会从服务器获取配置信息,作为导航栏的状态数据决定显示。



本文我们将来探讨两个问题:



  • 第一:如何将导航栏的数据变得 可配置

  • 第二:如何实现 拖拽 更改导航栏位置。




2.整体静态界面布局:

首先,我们先来对整体结构进行一下静态布局,也就是先抛开交互逻辑,对整体结构进行一下划分。整体是一个 上下 结构,下方是 导航栏 + 内容 的左右结构:



下面是对静态界面结构的简单仿写,本文主要介绍导航栏的交互实现,其他内容暂时忽略。以后有机会可以慢慢展开来说。





代码如下,整体界面的呈现由 AppNavigation 负责。通过 Column 实现上下结构,上面是 TopBar ,下面是通过 Expanded 包裹,可以让内容填充剩余部分。下方通过 Row 实现左右结构,左侧是今天的主角 LeftNavigationBar 组件,右侧是一个暂时空白的内容。


class AppNavigation extends StatelessWidget {
const AppNavigation({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
const TopBar(),
Expanded(
child: Row(
children: const [
LeftNavigationBar(),
// TODO 主题内容构建
Expanded(child: SizedBox.shrink()),
],
))
],
),
);
}
}

所以整体结构还是很简单的,通过 Expanded 组件,可以让指定的区域具有 “延展性” 。比如下面,当窗口尺寸变化时,中间的区域会自动收缩,而头部栏和导航栏不会受到影响。





3. 导航栏布局实现

导航栏是自定义的 LeftNavigationBar 组件,是一个上下结构:Logo 在最底端,LeftNavigationMenu 菜单在上方。这里的 Spacer 相当于一个占位组件,其高度为 Column 的剩余部分,也就是会 “撑开” 区域,在窗口高度发生变化时,这块区域会自动延展,来保证 Logo 始终在下方。





界面上呈现的内容,都有其对应的数据载体。这里先简单定义一个 LeftNavigationBarItem 的实体类,用于记录图标和标题信息。另外 id 用于菜单的唯一标识,因为后面要涉及到菜单位置的交换,不能靠索引进行标识:


class LeftNavigationBarItem {
final int id;
final IconData icon;
final String label;

const LeftNavigationBarItem({
required this.icon,
required this.label,
required this.id,
});
}



LeftNavigationMenu 组件中接收 LeftNavigationBarItem 列表数据。通过 Column 组件进行竖直排布,另外把每个菜单的单体抽离为 LeftNavigationBarItemWidget 组件方便维护:


class LeftNavigationMenu extends StatelessWidget {
final List<LeftNavigationBarItem> items;

const LeftNavigationMenu({Key? key, required this.items}) : super(key: key);

@override
Widget build(BuildContext context) {
return Column(
children: items
.map((e) => LeftNavigationBarItemWidget( item: e ))
.toList(),
);
}
}



对于导航栏而言,鼠标悬浮一般会有一个临时的激活状态。外界并不需要用到这个状态,所以可以将 LeftNavigationBarItemWidget 组件定义为 StatefulWidget ,来维护悬浮时的内部状态变化。



如下,在单体的组件状态类中定义 _hovering 私有状态量,通过 InkWell 监听悬浮的变化。由于这里是单独抽离的 LeftNavigationBarItemWidget 组件,所以这里在 _onHover 中触发的 setState 只会对局部组件进行构建。在构建时,根据 active 状态创建不同样式的条目即可。





4. 菜单的点击激活状态管理

界面上呈现的内容,都有其对应的数据载体,菜单的点击激活也不例外。比如你在飞书中点击了一个菜单,变成激活态,就表示在内存中一定对某个菜单的激活数据信息进行了变动,并重新渲染。我们想实现点击更换激活菜单,也是一样。需要考虑的只有两件事:



  • 如何 记录维护 数据的变化。

  • 如何在数据变化后触发更新。


状态管理的工具多种多样,但都不会脱离这两件本质的工作,不同的只是用法的形式而已。不必为了一些表面的功夫争论不休,而忽略问题的本质,适合自己就是好的。其实 State 类本身也是一种状态管理的工具,也有维护数据变化和触发更新的特定性,只不过处理较深层级间的共享数据时比较麻烦。


关于这一点,在上次掘金直播中进行过介绍,感兴趣的可以去看一下 回放 。由于没有什么直播经验,所以那次显得很紧张,不过想分享的核心知识还是都介绍到的。




这里用我比较熟悉的 flutter_bloc 来对激活菜单数据进行管理。现在引入 Cubit 后,对于小的数据进行管理变得非常方便。比如下面的 NavSelectionCubic ,只用 4 行代码就能实现对 激活菜单 id 的管理:


class NavSelectionCubic extends Cubit<int> {
NavSelectionCubic({int id = 1}) : super(id);

void selectMenu(int id) {
emit(id);
}
}



上面完成了 记录维护 数据的变化,那接下来的重点就是:如何在数据变化后触发更新。通过 BlocBuilder 可以在变化到新状态时,触发 builder 回调,重新构建局部组件,实现局部刷新。





在点击菜单是,触发 NavSelectionCubicselectMenu 方法,更新状态数据即可。这样就可以实现如下效果:点击某个菜单,变为激活状态:



---->[_LeftNavigationBarItemWidgetState#_onTap]----
void _onTap() {
BlocProvider.of<NavSelectionCubic>(context).selectMenu(widget.item.id);
}



5. 菜单数据的状态管理

我们现在的菜单数据是写死的,对于可拖拽的功能,需要对这些数据进行修改和触发更新。所以菜单数据本身也就上升为了需要管理的状态。对菜单数据状态进行管理,还有个好处:可以动态的修改菜单,比如不同角色的显示不同的菜单,只要根据角色维护数据即可。





这里再定义一个 NavMenuCubic 用于管理菜单数据,状态量是 NavMenus ,其中维护着 LeftNavigationBarItem 的列表。这样在拖拽时,执行 switchMenu 方法,进行拖拽菜单数据交换,再产出新的状态,即可完成需求。


class NavMenuCubic extends Cubit<NavMenus> {
NavMenuCubic({required List<LeftNavigationBarItem> item}) : super(NavMenus(menus:item ));

void switchMenu(int dragId, int targetId) {
// TODO 处理拖拽菜单数据交换
}
}

class NavMenus{
final List<LeftNavigationBarItem> menus;

const NavMenus({required this.menus});

}



另外说一点,导航模块使用了两个 Bloc ,可以单独抽离一个组件进行包裹 BlocProvider,这样其子树的上下文中才可以访问到相关的 Bloc。比如下面的 _NavigationScope ,这里的菜单数据直接给出,其实也可以通过服务端记录这些配置数据,在登录时读取进行初始化:


class _NavigationScope extends StatelessWidget {
const _NavigationScope({Key? key}) : super(key: key);

final List<LeftNavigationBarItem> items = const [
LeftNavigationBarItem(id: 1, icon: Icons.message_outlined, label: "消息"),
LeftNavigationBarItem(id: 2, icon: Icons.video_camera_back_outlined, label: "视频会议"),
LeftNavigationBarItem(id: 3, icon: Icons.book_outlined, label: "通讯录"),
LeftNavigationBarItem(id: 4, icon: Icons.cloud_upload_outlined, label: "云文档"),
LeftNavigationBarItem(id: 5, icon: Icons.games_sharp, label: "工作台"),
LeftNavigationBarItem(id: 6, icon: Icons.calendar_month, label: "日历"),
];

@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<NavSelectionCubic>(
create: (BuildContext context) => NavSelectionCubic(id:items.first.id),
),
BlocProvider<NavMenuCubic>(
create: (BuildContext context) => NavMenuCubic(items:items),
),
],
child: const LeftNavigationBar(),
);
}
}



6. 如何拖动菜单

我们先来分析一下拖拽菜单的界面表现。如下所示,可将一个菜单拖拽出来,拖出的组件具有一定的透明度;另外当拖拽物达到目标时,目标底部会显示蓝线示意移至其下。这里使用的是 DraggableDragTarget 的组合,其中 Draggable 指的是可拖拽物体,DragTarget 指的是受体目标。

可以看出,其实这里导航菜单同时承担着这两种角色,既需要拖拽,又需要作为目标接收拖拽物,这就是可拖拽导航的一个小难点。另外还有一个小细节,在拖拽过程中要禁止 _hovering 的悬浮激活,结束后要开启悬浮激活。



下面是代码实现的核心,其中对应 _disableHover 标识来控制是否可以悬浮激活,在 DragTarget 的相关回调中维护 _disableHover 的值。 DraggableDragTarget 需要一个泛型,也就是拖拽交互中需要传递的数据,这里是 int 类型的菜单 id 。数据由 Draggable 提供,如下 tag1 处所示,交互过程中有两个组件,其一是随拖拽浮动的部分,由 buildDraggableChild 方法构建,其二是主体菜单组件,由 buildTargetChild 方法构建。


class _LeftNavigationBarItemWidgetState  extends State<LeftNavigationBarItemWidget> {
bool _hovering = false;
bool _disableHover = false;

void _onTap() {
BlocProvider.of<NavSelectionCubic>(context).selectMenu(widget.item.id);
}

void _onHover(bool value) {
if (_disableHover) return;
setState(() {
_hovering = value;
});
}

final Color color = const Color(0xffcfd1d7);
final Color activeColor = Colors.blue;

@override
Widget build(BuildContext context) {
return DragTarget<int>(
onAccept: _onAccept,
builder: _buildTarget,
onMove: _onMove,
onLeave: _onLeave,
onWillAccept: _onWillAccept,
);
}

Widget buildTargetChild(bool active, bool dragging, int? dragItemId) {
// 暂略...
}

Widget buildDraggableChild(bool active) {
// 暂略...
}

Widget _buildTarget(BuildContext context, List<int?> candidateData,
List<dynamic> rejectedData) {
bool active = widget.active || _hovering;
int? id;
if (candidateData.isNotEmpty) {
id = candidateData.first;
}
Widget child = buildTargetChild(active, _disableHover, id);

return Draggable<int>(
data: widget.item.id, // tag1
feedback: buildDraggableChild(widget.active), // tag2
child: child,
);
}

下面来单独看一下 DragTarget 的几个回调方法。_onWillAccept 可以通过返回值来控制,是否拖拽物是否符合目标的接收条件,只有符合条件才会在后续触发 _onAccept。比如这里当携带的 id 不是自身的 id 时,符合接收条件,这样就可以避免自己拖到自己身上的问题。

_onAccept 顾名思义,表示拖拽符合条件被接收,我们之后在此回调中对菜单栏进行重排序,再触发更新即可。_onMove 在拖拽物移入目标时触发,_onLeave在拖拽物离开目标时触发。另外 Draggable 中有一些拖拽事件相关的回调,在这里作用不大,大家可以只了解一下。


  bool _onWillAccept(int? data) {
print('=====_onWillAccept=======$data===${data != widget.item.id}===');
return data != widget.item.id;
}

void _onAccept(int data) {
print('=====_onAccept=======$data======');
_disableHover = false;
}

void _onMove(DragTargetDetails<int> details) {
_hovering = false;
_disableHover = true;
}

void _onLeave(int? data) {
print('=====_onLeave=============');
_disableHover = false;
}
}

最后看一下 buildTargetChild 中的一个小细节,也就是达到目标时,目标组件底部出现蓝色线条示意。 DragTarget 组件的构建组件的回调中,可以感知到携带的数据。如下,只要根据 id 数据进行校验,当 enable 时添加底部边线即可:





7. 拖拽更新菜单数据

上面把所有的准备工作都完成了,接下来想要拖拽更新菜单数据,也就能水到渠成。前面说过。菜单数据由 NavMenuCubic 维护,现在只要在 switchMenu 中完成业务逻辑,在 _onAccept 中触发即可。这样界面交互、数据变化、界面更新三个层次就会非常清晰。


class NavMenuCubic extends Cubit<NavMenus> {
NavMenuCubic({required List<LeftNavigationBarItem> items}) : super(NavMenus(menus:items ));

void switchMenu(int dragId, int targetId) {
// TODO 处理拖拽菜单数据交换
}
}



如下,是交换的处理逻辑,根据 dragIdtargetId 获取在列表中的索引,然后移除和添加而已。就是最基本的数据处理,在刚才的 _onAccept 方法中触发交换即可,效果如下:



---->[NavMenuCubic#switchMenu]---- 
void switchMenu(int dragId, int targetId) {
List<LeftNavigationBarItem> items = state.menus;
int dragIndex = 0;
int targetIndex = 0;
for(int i =0;i<items.length;i++){
LeftNavigationBarItem item = items[i];
if(item.id == dragId){
dragIndex = i;
}
}
LeftNavigationBarItem dragItem = items.removeAt(dragIndex);
for(int i =0;i<items.length;i++) {
LeftNavigationBarItem item = items[i];
if (item.id == targetId) {
targetIndex = i;
}
}
items.insert(targetIndex+1, dragItem);
print(items);
emit(NavMenus(menus: items));
}
}

---->[_LeftNavigationBarItemWidgetState]----
void _onAccept(int data) {
print('=====_onAccept=======$data======');
BlocProvider.of<NavMenuCubic>(context).switchMenu(data,widget.item.id);
_disableHover = false;
}



这里只是进行最基础的拖拽导航栏需求,还有一些可以拓展的地方。比如将菜单的数据存储在本地,这样就可以保证程序关闭之后,再打开不会重置。另外也可以提供相关的后端接口,让数据同步到服务端,这样多设备就可以实现同步。

本文简单介绍了一下状态管理的使用价值,完成了一个简单的自定义可拖拽导航栏,相信从中你可以学到一些东西。后续会基于这个导航继续拓展,比如界面切换,支持添加移除等。那本文就到这里,谢谢观看~


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

Android AIDL使用指南

AIDL 全称 Android Interface Definition Language ,安卓接口定义语言。AIDL 用来解决 Android 的跨进程通信问题,底层原理是 Binder ,实现思路是 C / S 架构思想。Server:接收请求,提供处理...
继续阅读 »

AIDL 全称 Android Interface Definition Language ,安卓接口定义语言。

AIDL 用来解决 Android 的跨进程通信问题,底层原理是 Binder ,实现思路是 C / S 架构思想。

  • Server:接收请求,提供处理逻辑,并发送响应数据。
  • Client:发起请求,接收响应数据。

C / S 之间通过 Binder 对象进行通信。

  • Server 需要实现一个 Service 作为服务器,Client 侧则需要调用发起请求的能力。
  • Client 需要调用 bindService 绑定到远程服务,然后通过 ServiceConnection 来接收远程服务的 Binder 对象。拿到 Binder 对象后就可以调用远程服务中定义的方法了。

因为是跨进程通信,所以需要实现序列化,AIDL 专门为 Android 设计,所以它的序列化不能使用 Java 提供的 Serializable ,而是 Android 提供的 Parcelable 接口。

AIDL 的用法

以一个跨进程相互发送消息的 Demo 为例,演示 AIDL 的用法。

场景:两个 App ,一个作为 Server ,用来启动一个服务,接收另一个作为 Client 的 App 发来的请求(Request),然后并进行响应(Response),另外,Server 也可以主动发消息给绑定到 Server 的 Client 。

Step 1 定义通信协议

AIDL 解决的是远程通信问题,是一种 C / S 架构思想,参考网络通信模型,客户端和服务端之间,需要约定好彼此支持哪些东西,提供了什么能力给对方(这里主要是服务端提供给客户端的),而定义能力在面向对象的编程中,自然就是通过接口来定义。所以 AIDL 是 Interface Definition Language。

定义服务端暴露给客户端的能力,首先要创建 AIDL 文件。AIDL 文件在同包名下,与 java / res 等目录同级,创建 aidl 文件夹,其内部结构和 java 保持一致:

src
|- java
|-- com
|--- chunyu
|---- aidl
|----- service
|------ ...// java file

|- aidl
|-- com
|--- chunyu
|---- aidl
|----- service
|------ ... // aidl file

|- ...
复制代码

在定义 AIDL 接口前,我们现实现一个数据类,这个数据类作为服务端和客户端之间通信的数据结构。如果你的通信不需要复杂的数据对象,而是 int 、 long 等基本数据类型和 String ,则不需要这一个步骤。

通过 Android Studio 右键创建 AIDL 文件时,默认会生成一个方法:

interface IServerManager {
/**
* 演示了一些可以在AIDL中用作参数和返回值的基本类型。
*/
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
}
复制代码

这里说明了除了要实现 Parcelable 序列化接口的对象,这些类型可以直接传递。

这里我们定义一个 Msg 类,作为通信传递的数据类型,在 java 目录下实现这个类:

class Msg(var msg: String, var time: Long? = null): Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readValue(Long::class.java.classLoader) as? Long
) {
}

override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(msg)
parcel.writeValue(time)
}

override fun describeContents(): Int {
return 0
}

companion object CREATOR : Creator<Msg> {
override fun createFromParcel(parcel: Parcel): Msg {
return Msg(parcel)
}

override fun newArray(size: Int): Array<Msg?> {
return arrayOfNulls(size)
}
}
}
复制代码

其实只需要定义参数即可,方法和成员通过快捷键会自动实现。实现完这个类后,需要在 aidl 同包名下创建一个 Msg.aidl 文件,将这个类型进行声明:

package com.chunyu.aidl.service;

parcelable Msg;
复制代码

数据对象定义好了,下一步就可以定义服务端暴露给客户端的接口了。

首先定义一个服务端主动回调给客户端的接口,所有注册了的 Client ,都可以接收到服务端的主动消息:

package com.chunyu.aidl.service;

import com.chunyu.aidl.service.Msg;

interface IReceiveMsgListener {
void onReceive(in Msg msg);
}
复制代码

需要注意的一点是,AIDL 文件中的 import 并不会自动导入,需要开发者自行添加。

然后定义 Server 暴露给 Client 的能力:

package com.chunyu.aidl.service;

import com.chunyu.aidl.service.Msg;
import com.chunyu.aidl.service.IReceiveMsgListener;

interface IMsgManager {
// 发消息
void sendMsg(in Msg msg);
// 客户端注册监听回调
void registerReceiveListener(IReceiveMsgListener listener);
// 客户端取消监听回调
void unregisterReceiveListener(IReceiveMsgListener listener);
}
复制代码

IMsgManager 提供了三个方法,用来解决两个场景:

  • Client 主动发送 Msg 给 Server (sendMsg 方法,也可以理解为网络通信中的客户端发起请求)。
  • Server 主动发消息给所有订阅者,通过回调 IReceiveMsgListener 中的 onReceive 方法,每个注册的 Client 都会收到回调(典型的观察者模式)。

所有关于 AIDL 的部分就到这里了,此时,开发者需要手动运行 build 重新构建项目,这样 AIDL 会在 build 后生成一些 class 文件,供项目代码中调用。

注意:每次 AIDL 的改动都需要手动 build 一下。

Step 2 定义服务端

服务端的定义需要创建一个 Service 来作为服务器。这里创建一个 MyService 类,然后在 AndroidManifest.xml 中配置一个 action ,这个 action 后续会用来进行跨进程启动:

        <service
android:name=".MyService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.chunyu.aidl.service.MyService"></action>
</intent-filter>
</service>
复制代码

配置完这个后,就可以在 MyService 中实现服务器的逻辑了。

Service 的启动方式有两种,startService 和 bindService ,后者是我们在实现 Client 连接 Server 的核心方法。通过 bindService 创建 C / S 之间的连接。而 Service 在通过 bindService 启动时,会回调 onBind 方法:

fun onBind(intent: Intent): IBinder 
复制代码

onBind 的返回类型时 IBinder ,这个就是跨进程通信间传递的 Binder 。在同一个进程中不需要进行跨进程通信,这里可以返回为 null 。而此时需要实现 IPC ,自然这个方法需要返回一个存在的 Binder 对象,所以,配置服务器的第二步,就是实现一个 Binder 并在 onBind 中返回。

我们在定义通信协议时,定义了一个用来表示 Server 提供给 Client 能力的接口 IMsgManager,经过 build 后,编译器会自动生成 IMsgManager.Stub ,这是一个自动生成的实现了 IMsgManager 接口的 Binder 抽象实现:

public static abstract class Stub extends android.os.Binder implements com.chunyu.aidl.service.IMsgManager
复制代码

在 MyService 中实现这个抽象类:

class MyService : Service() {

// ...

inner class MyBinder: IMsgManager.Stub() {
override fun sendMsg(msg: Msg?) {
// todo 收到 Client 发来的消息,此处实现 Server 的处理逻辑
}
override fun sendMsg(msg: Msg?) {
// todo 收到 Client 发来的消息,此处实现 Server 的处理逻辑
val n = receiveListeners.beginBroadcast()
for (i in 0 until n) {
val listener = receiveListeners.getBroadcastItem(i)
listener?.let {
try {
val serverMsg = Msg("服务器响应 ${Date(System.currentTimeMillis())}\n ${packageName}", System.currentTimeMillis())
listener.onReceive(serverMsg)
} catch (e: RemoteException) {
e.printStackTrace()
}
}
}
receiveListeners.finishBroadcast()
}

override fun registerReceiveListener(listener: IReceiveMsgListener?) {
// receiveListeners 记录观察者
receiveListeners.register(listener)
}

override fun unregisterReceiveListener(listener: IReceiveMsgListener?) {
val success = receiveListeners.unregister(listener)
if (success) {
Log.d(TAG, "解除注册成功")
} else {
Log.d(TAG, "解除注册失败")
}
}
}
}
复制代码

然后,在 onBind 方法中返回这个类的对象:

    override fun onBind(intent: Intent): IBinder {
return MyBinder()
}
复制代码

整体的一个服务端代码:

class MyService : Service() {

private val receiveListeners = RemoteCallbackList<IReceiveMsgListener>()

override fun onBind(intent: Intent): IBinder {
return MyBinder()
}

inner class MyBinder: IMsgManager.Stub() {
override fun sendMsg(msg: Msg?) {
// server process request at here
}

override fun registerReceiveListener(listener: IReceiveMsgListener?) {
receiveListeners.register(listener)
}

override fun unregisterReceiveListener(listener: IReceiveMsgListener?) {
val success = receiveListeners.unregister(listener)
if (success) {
Log.d(TAG, "解除注册成功")
} else {
Log.d(TAG, "解除注册失败")
}
}
}
}
复制代码

这样,服务端的逻辑就完成了。

Step 3 客户端实现

客户端的实现在另一个 App 中实现,创建一个新项目,包名 com.chunyu.aidl.client ,将这个项目中的 MainActivity 作为 Client 。

Client 中需要实现的核心逻辑包括:

  • 创建 Client 到 Server 的连接。
  • 实现发送请求的功能。
  • 实现接收 Server 消息的功能。
  • 在销毁的生命周期中主动关闭连接。

创建连接

创建连接主要通过 bindService 来实现,分为两种情况,在同一个进程中,不需要跨进程,直接通过显式的 Intent 启动 Service :

val intent = Intent(this, MyService::class.java)
bindService(intent, connection!!, BIND_AUTO_CREATE)
复制代码

这是因为在同一个进程中,能直接访问到服务端 Service 的类。而跨进程没有这个 class ,需要通过隐式 Intent 启动 :

val intent = Intent()
intent.action = "com.chunyu.aidl.service.MyService"
intent.setPackage("com.chunyu.aidl.service")
bindService(intent, connection!!, BIND_AUTO_CREATE)
复制代码

Action 在实现服务端时,在 AndroidManifest.xml 中进行配置,此刻就用到了。

而不管是哪个进程调用 bindService ,都会需要一个 connection 参数,这是一个 ServiceConnection 的对象。

ServiceConnection 是一个监控应用 Service 状态的接口。和很多系统的其他回调一样,这个接口的实现的方法在进程的主线程中调用。

public interface ServiceConnection {

void onServiceConnected(ComponentName name, IBinder service);

void onServiceDisconnected(ComponentName name);

default void onBindingDied(ComponentName name) {
}

default void onNullBinding(ComponentName name) {
}
}
复制代码

了解了 ServiceConnection ,所以需要在 Client 中实现一个 ServiceConnection 对象,这里用匿名对象的形式:

private var deathRecipient = object : IBinder.DeathRecipient {
override fun binderDied() {
iMsgManager?.let {
// 当 binder 连接断开时,解除注册
it.asBinder().unlinkToDeath(this, 0)
iMsgManager = null
}
}
}

val connection = object : ServiceConnection {
// 服务连接创建成功时
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
iMsgManager = IMsgManager.Stub.asInterface(binder)
try {
iMsgManager?.asBinder()?.linkToDeath(deathRecipient, 0)
iMsgManager?.registerReceiveListener(receiveMsgListener)
} catch (e: RemoteException) {
e.printStackTrace()
}
}
override fun onServiceDisconnected(name: ComponentName?) { }
}
复制代码

接收 Server 消息

这里注册的 listener是一个 IReceiveMsgListener.Stub 对象:

    private var receiveMsgListener = object : IReceiveMsgListener.Stub() {
override fun onReceive(msg: Msg?) {
displayTv.post {
displayTv.text = "客户端:${msg?.msg}, time: ${msg?.time}"
}
}
}
复制代码

写到这里的时候,我是有一个疑问的,为什么是 IReceiveMsgListener.Stub 类型,而不是一个 IReceiveMsgListener 的匿名对象。

首先 IReceiveMsgListener.Stub 的继承关系是:

public static abstract class Stub extends android.os.Binder implements com.chunyu.aidl.service.IReceiveMsgListener
复制代码

IReceiveMsgListener.Stub 本身也是 IReceiveMsgListener 的实现,而且它还继承自 Binder ,也就是具备了 Binder 的能力,能够在跨进程通信中作为 Binder 传输,所以这里是 IReceiveMsgListener.Stub 类型的对象。

而如果直接使用 IReceiveMsgListener , 匿名对象要求多实现一个 asBinder 方法:

    private var receiveMsgListener = object : IReceiveMsgListener {
override fun asBinder(): IBinder {
TODO("Not yet implemented")
}

override fun onReceive(msg: Msg?) {
displayTv.post {
displayTv.text = "客户端:${msg?.msg}, time: ${msg?.time}"
}
Log.d(TAG, "客户端:$msg")
}
}
复制代码

其实 IReceiveMsgListener.Stub 就是帮我们实现好了一些逻辑,减少了开发的复杂度。

而从另一个方面也验证了 AIDL 跨进程通信调用的对象能需要具备作为 Binder 的能力。

Client 发送消息

val connection = object : ServiceConnection {
// 服务连接创建成功时
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
iMsgManager = IMsgManager.Stub.asInterface(binder)
try {
iMsgManager?.asBinder()?.linkToDeath(deathRecipient, 0)
iMsgManager?.registerReceiveListener(receiveMsgListener)
} catch (e: RemoteException) {
e.printStackTrace()
}
}
override fun onServiceDisconnected(name: ComponentName?) { }
}
复制代码

onServiceConnected 代表着连接创建成功了,此时首先赋值了 iMsgManager ,iMsgManager 是 AIDL 中定义的 Server 暴露给 Client 的能力的接口对象:

private var iMsgManager: IMsgManager?  = null
复制代码

它的初始化:

iMsgManager = IMsgManager.Stub.asInterface(binder)
复制代码

这里的 asInerface 和 asBinder 方法,说明 AIDL 中定义的接口可以转换为 Binder ,Binder 也可以转换为 Interface ,因为这里的 Binder 来自于 Service 的 onBind 方法,在 onBind 中返回的就是 IMsgManager.Stub 的实现,自然可以转换为 IMsgManager 。

通过 IMsgManager 就可以调用 Server 中的方法了:

        sendMsgBtn.setOnClickListener {
iMsgManager?.sendMsg(Msg("from 客户端,当前第 ${count++} 次", System.currentTimeMillis()))
}
复制代码

随便给一个 button 的点击事件中调用 sendMsg 方法,发送消息给 MyService 。

生命周期管理

最后是在 Client 的生命周期中及时关闭连接,清除不需要的对象。

    // in MainActivity
override fun onDestroy() {
if (iMsgManager?.asBinder()?.isBinderAlive == true) {
try {
iMsgManager?.unregisterReceiveListener(receiveMsgListener)
} catch (e: RemoteException) {
e.printStackTrace()
}
}
connection?.let {
unbindService(it)
}
super.onDestroy()
}
复制代码

基本上这就是一个完整的 AIDL 流程了。

总结

  • AIDL 是一套快速实现 Android Binder 机制的框架。
  • Android 中的 Binder 机制,架构思想是 C / S 架构。
  • 所有跨进程传递的数据需要实现 Parcelable (除了一些基本的类型)。
  • 所有跨进程调用的对象,都必须是 Binder 的实现。
  • Binder 对象可以和 Interface 实例进行转换,这是因为 Service 中返回的 Binder 对象实现了 Interface。
  • 通过 aidl 文件中定义的接口,可以跨进程调用远程对象的方法。


作者:自动化BUG制造器
链接:https://juejin.cn/post/7123129439898042376
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

千万不要用JSON.stringify()去实现深拷贝!有巨坑!!

当对象中有时间类型的元素时候 -----时间类型会被变成字符串类型数据const obj = { date:new Date()}typeof obj.date === 'object' //trueconst objCopy = JSON.parse(...
继续阅读 »

当对象中有时间类型的元素时候 -----时间类型会被变成字符串类型数据

const obj = {
date:new Date()
}
typeof obj.date === 'object' //true
const objCopy = JSON.parse(JSON.stringify(obj));
typeof objCopy.date === string; //true

然后你就会惊讶的发现,getTime()调不了了,getYearFull()也调不了了。就所有时间类型的内置方法都调不动了。

但,string类型的内置方法全能调了。

当对象中有undefined类型或function类型的数据时 --- undefined和function会直接丢失

const obj = {
undef: undefined,
fun: () => { console.log('叽里呱啦,阿巴阿巴') }
}
console.log(obj,"obj");
const objCopy = JSON.parse(JSON.stringify(obj));
console.log(objCopy,"objCopy")

然后你就会发现,这两种类型的数据都没了。

当对象中有NaN、Infinity和-Infinity这三种值的时候 --- 会变成null

1.7976931348623157E+10308 是浮点数的最大上线 显示为Infinity

-1.7976931348623157E+10308 是浮点数的最小下线 显示为-Infinity

const obj = {
nan:NaN,
infinityMax:1.7976931348623157E+10308,
infinityMin:-1.7976931348623157E+10308,
}
console.log(obj, "obj");
const objCopy = JSON.parse(JSON.stringify(obj));
console.log(objCopy,"objCopy")

当对象循环引用的时候 --会报错

const obj = {
objChild:null
}
obj.objChild = obj;
const objCopy = JSON.parse(JSON.stringify(obj));
console.log(objCopy,"objCopy")

假如你有幸需要拷贝这么一个对象 ↓

const obj = {
nan:NaN,
infinityMax:1.7976931348623157E+10308,
infinityMin:-1.7976931348623157E+10308,
undef: undefined,
fun: () => { console.log('叽里呱啦,阿巴阿巴') },
date:new Date,
}

然后你就会发现,好家伙,没一个正常的。

image.png

你还在使用JSON.stringify()来实现深拷贝吗?

如果还在使用的话,小心了。作者推荐以后深拷贝使用递归的方式进行深拷贝。

原文:https://juejin.cn/post/7113829141392130078

收起阅读 »

转行的程序猿都去做什么了?这些个案羡煞我也

程序猿是口青春饭,30有点“老”,35非常“老”。当年龄越来越大,体力精力学习能力越来越竞争不过年轻人时,除了技术和管理,码农还有没有别的选择吗?以下这些切实又不切实的选择仅供参考。 1.转往临近岗位,比如你讨厌的产品经理 程序猿和产品经理可谓是最像夫妻的两个...
继续阅读 »

程序猿是口青春饭,30有点“老”,35非常“老”。当年龄越来越大,体力精力学习能力越来越竞争不过年轻人时,除了技术和管理,码农还有没有别的选择吗?以下这些切实又不切实的选择仅供参考。


1.转往临近岗位,比如你讨厌的产品经理


程序猿和产品经理可谓是最像夫妻的两个职位,相爱相杀,知根知底。


程序员转产品经理有很大优势,因为了解产品的实现过程,所以对项目的时间把握有相当的话语权,能保证了项目的进度,对产品将来的扩展和升级都有帮助,所以程序员转过来的产品经理是很抢手的。


只要多学习一些产品营销和运营方面的知识,多一点沟通能力,程序员出生的产品经理就自然而然在市场上占据很大优势。



国内目前最牛逼的产品经理非微信之父张小龙莫属,他就是程序员转产品经理的最佳案例。如果你拥有绝佳的洞察力,能够了解人性需求,相信自己可以创造出人人都愿意的产品,你也可以像张小龙一样,升职加薪、当上总经理、出任CEO、迎娶白富美、走上人生巅峰。


2.任何行业的教育行业都可以作为备胎


随着猿在劳动力市场的走红,IT讲师也成为了一个热门职位。大龄码农在工作中年限越长上升越慢,到大龄阶段,就不得不面对自己的停滞,IT讲师对于他们会是一个很好的选择。


开发一线需要越年轻越好,这样薪水低能加班,不过IT讲师可谓是越老越有“味道”,资历越丰厚,越能吸引学员。因此相当一部分“退役程序猿”都转去了IT教育行业。


3.个体户,谁自由谁知道


除了做IT讲师和产品经理,有的程序猿纯粹是写腻了代码,想完全摆脱代码,摆脱程序猿的工作。他们有的就利用自己多年攒下的钱,做起了个体户。


不知是因为新闻性还是真有那么大基数,据说程序猿转行做餐饮的不少,并且都还做得不错。


无论是卖肠粉的,卖凉皮的,卖热干面,卖火锅的都有!






程序猿卖烧饼,卖凉皮,听上去有些屈才。但如今的个体户收入并不低,即使只是卖个热干面,卖个烧饼,好好经营,收入不比一个高级码农差。


4.一些高薪产业,比如做明星


此条建议,是认真的,毕竟明星中有不少的程序猿转行成功的案例。


潘玮柏


潘玮柏,曾设计一款游戏——熊猫屁王,他是第一个设计 iPhone app 的艺人, 还创造过 App Store 上下载量第一。


这也就算了,别的明星送礼都是送花送大牌,潘玮柏给周杰伦的发片礼竟然是以周杰伦为游戏主角的手机游戏。


李健


男神李健,原来也是码农。他是清华高材生,大学里学习的就是电子工程专业。李健的第一份工作就是在国家广电总局当一名网络工程师。


马东


奇葩说的主持人马东也曾经是一名计算机专业的人才。在他还未满18岁的时候,马东就只身前往澳洲学习计算机专业,在澳洲有着10年的IT工程师经历。而这使得他对数据有着灵敏的认知和有序的编程思维,能够在传媒行业风格别具一致,脱颖而出。


除了这些明星是程序猿转行过来的,还有猿混成了总理。




李显龙


新加坡总理李显龙也曾是位真正入行的程序员。他在剑桥大学三一学院修习数学和计算机科学,曾把自己写过的解熟读程序放了出来,让网民们一起帮忙找BUG。李显龙在一次科技创业论坛上透露,自己曾“非常享受编程”。


5.追求自己的爱好,找寻真正的自我


有人说,最好的转行是为了兴趣爱好而转。毕竟换一个行业需要投入大量时间精力,还要面对失败。做一个程序猿,奋斗几年,攒够一定资金后,就该找寻自我,追求自己的爱好了。以下两个案例中的程序猿,都曾有稳定的程序猿工作,但都放弃了选择了追求自己的爱好,最终闯出了一片天。


闫鹤祥


著名德云社相声演员——闫鹤祥原是一名程序猿。闫鹤祥曾在社交软件上晒出他当年任中国移动数字大厦无线局域网管理工程师职位。从一个程序员到相声演员,闫鹤祥从幕后转到台前,由原先的默默无闻变成了现在为德云社少帮主郭麒麟捧哏的相声大腕儿。


王小波


大多数人都知道王小波是小说家,却很少人知道王小波算得上是中国早期的程序员,在90年代初国内应用软件缺乏的时候,王小波学会了汇编和C语言,编了中文编辑器和输入法,相比同期的雷军、求伯君,王小波的编程能力毫不逊色。


王小波曾在自己的小说里骄傲地写到,我写书的软件都是自己编写的。当时的他还通过卖软件还赚了不少钱呢,很多中关村的老板想要拉他入伙,这对于当时的屌丝王小波还是有致命吸引力的,王小波也认真地考虑过,只不过后来觉得写东西更有意思,便一一回绝了。


以上为不完全程序猿转行方向手册,无论如何,转行需谨慎。



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

收起阅读 »

Go语言负责人离职后,一门国产语言诞生了

Go
1 事件回顾 上周,谷歌Go语言项目负责人Steve Francia宣布辞去职务,而他给出理由是:Go项目的工作停滞不前,让他感到难受。有意思的是,部分国内的Gopher(Go语言爱好者的自称)对Go语言也产生了新想法。比如,国内第一批Go语言爱好者之一的柴树...
继续阅读 »

1 事件回顾

上周,谷歌Go语言项目负责人Steve Francia宣布辞去职务,而他给出理由是:Go项目的工作停滞不前,让他感到难受。

有意思的是,部分国内的Gopher(Go语言爱好者的自称)对Go语言也产生了新想法。比如,国内第一批Go语言爱好者之一的柴树杉、全球Go贡献者榜上长期排名TOP 50的史斌等Gopher,他们决定以Go语言为蓝本,发起新的编程语言:凹语言™(凹读音“Wa”)。

目前凹语言™的代码已经在Github开源,并且提供了简单可执行的示例。根据其仓库的介绍,凹语言™的设计目标有以下几个:

1、披着Go和Rust语法外衣的C++语言

2、凹语言™源码文件后缀为.wa

3、凹语言™编译器兼容WaGo语法,凹语法与WaGo语法在AST层面一致(二者可生成相同的AST并无损的互相转换)

4、凹语言™支持中文/英文双语关键字,即任一关键字均有中文版和英文版,二者在语法层面等价


凹语言™示意,图片来源@GitHub

据柴树杉、史斌等人的说法,Go语言“克制”的风格是他们对编程语言审美的最大公约数。因此,凹语言™项目启动时大量借鉴了Go的设计思想和具体实现。

当然,他们也表示,选择Go语言作为初始的蓝本,是在有限投入下不得不作出的折衷。他们希望随着项目的发展,积累更多原创的设计,为自主创新的大潮贡献一点力量。

虽说柴树杉、史斌等人是资深的Gopher,偏爱Go语言并不难理解,但我们还是忍不住好奇:究竟Go语言有多神奇,让他们对Go语言这么着迷?

2 为什么选中Go语言

从许多使用过Go语言的开发者对Go的评价上看,Go语言在设计上有以下四个特点。

1、简单易用

不同于那些通过相互借鉴而不断增加新特性的主流编程语言(如C++、Java等),Go的设计者们在语言设计之初就拒绝走语言特性融合的道路,而选择了“做减法”。

他们把复杂留给了语言自身的设计和实现,留给了Go核心开发组,而将简单、易用和清晰留给了广大使用Go语言的开发者。因此,Go语言呈现出:

  • 简洁、常规的语法(不需要解析符号表),仅有25个关键字;

  • 没有头文件;

  • 显式依赖(package);

  • 没有循环依赖(package);

  • 常量只是数字;

  • 首字母大小写决定可见性;

  • 任何类型都可以拥有方法(没有类);

  • 没有子类型继承(没有子类);

  • 没有算术转换;

  • 没有构造函数或析构函数;

  • 赋值不是表达式;

  • 在赋值和函数调用中定义的求值顺序(无“序列点”概念);

  • 没有指针算术;

  • 内存总是初始化为零值;

  • 没有类型注解语法(如C++中的const、static等)

  • ……

2、偏好组合

C++、Java等主流面向对象语言,通过庞大的自上而下的类型体系、继承、显式接口实现等机制,将程序的各个部分耦合起来,但在Go语言中我们找不到经典面向对象的语法元素、类型体系和继承机制。

那Go语言是如何将程序的各个部分耦合在一起呢?是组合。

在语言设计层面,Go使用了正交的语法元素,包括Go语言无类型体系,类型之间是独立的,没有子类型的概念;每个类型都可以有自己的方法集合,类型定义与方法实现是正交独立的。

各类型之间通过类型嵌入,将已经实现的功能嵌入新类型中,以快速满足新类型的功能需求。在通过新类型实例调用方法时,方法的匹配取决于方法名字,而不是类型。

另外,通过在接口的定义中嵌入接口类型来实现接口行为的聚合,组成大接口,这种方式在标准库中尤为常用,并且已经成为Go语言的一种惯用法。

这是Go语言的一个创新设计:接口只是方法集合,且与实现者之间的关系是隐式的,如此可让程序各个部分之间的耦合降至最低。

3、并发和轻量

Go语言的三位设计者Rob Pike、Robert Griesemer和Ken Thompson曾认为C++标准委员会在思路上是短视的,因为硬件很可能在未来十年内发生重大变化,将语言与当时的硬件紧密耦合起来是十分不明智的,是没法给开发人员在编写大规模并发程序时带去太多帮助的。

因而他们把将面向多核、原生内置并发支持作为新语言的设计原则之一。

Go语言原生支持并发的设计哲学体现在下面两点。

(1)Go语言采用轻量级协程并发模型,使得Go应用在面向多核硬件时更具可扩展性。

(2)Go语言为开发者提供的支持并发的语法元素和机制。

4、面向工程

Go语言的设计者在Go语言最初设计阶段,就将解决工程问题作为Go的设计原则之一,进而考虑Go语法、工具链与标准库的设计,这也是Go与那些偏学院派、偏研究性编程语言在设计思路上的一个重大差异。

这让Go语言的规范足够简单灵活,有其他语言基础的程序员都能迅速上手。更重要的是Go自带完善的工具链,大大提高了团队协作的一致性。比如Gofmt自动排版Go代码,很大程度上杜绝了不同人写的代码排版风格不一致的问题。把编辑器配置成在编辑存档的时候自动运行Gofmt,这样在编写代码的时候可以随意摆放位置,存档的时候自动变成正确排版的代码。此外还有Gofix,Govet等非常有用的工具。

总之,Go在语言层面的简单让Go收获了不逊于C++/Java等的表现力的同时,还获得了更好的可读性、更高的开发效率等在软件工程领域更为重要的元素。

3 凹语言™的未来

虽然今天,Go凭借其优越的性能,已经成为主流编程语言之一(超过75%的CNCF项目,包括Kubernetes和Istio,都是用Go编写的,另外,Go也是主要的云应用程序语言之一),Go语言在中国也相当受欢迎,但我们还是不禁担心脱胎于Go的凹语言™,会有美好的未来吗?


Go语言搜索热度,图片来源@Google Trend

预测未来从来都是困难的,不过,好在凹语言™的前面有一个先行者——Go+语言,我们不妨基于Go+的发展,来大致推测凹语言™的未来。

Go+是七牛云CEO许式伟发明的编程语言,于2020年7月正式发布,2021年10月推出1.0版本,目前最新发布版本是今年6月13日发布的1.1版本。也就是说,从正式发布到现在,经过近两年的时间,Go+还处于初始阶段,距离大规模应用还有一定距离,那么可以预见,凹语言™在未来相当长的时间里,不会进入广大开发者的视野中。

另外,据ECUG Con 2022大会上许式伟发表的看法,虽然大家都比较看重编程语言的性能,但单从性能来看的话,许式伟认为Python在脚本语言里面只能算二流,Python其实并不快。

在许式伟看来,对新生的语言来说,最重要它选择的目标人群。

Go+选择的目标人群是全民,许式伟称其为“连儿童也能掌握的语言”,因而Go+从工程与STEM教育的一体化开始奠定用户基础。

正是Go+的这几个特性,让一部分开发者看好Go+的未来。而对Go+的正向预期,会成为Go+进一步发展的助力。

对凹语言™来说,这个道理也是适用的:凹语言™的发展重点可能不在于性能,而在于其选择哪些人群作为目标受众,以及通过何种方式获得种子用户。

如果日后凹语言™的项目方会公布这些消息,那么凹语言™的未来还是可以期待的。

来源:武穆、信远(51CTO技术栈)

收起阅读 »

快速搭建一个网关服务,动态路由、鉴权的流程,看完秒会(含流程图)

最近发现网易号有盗掘金文章的,xdm有空可以关注一下这个问题,希望帮助到大家同时能够保障自己权益。前言本文记录一下我是如何使用Gateway搭建网关服务及实现动态路由的,帮助大家学习如何快速搭建一个网关服务,了解路由相关配置,鉴权的流程及业务处理,有兴趣的一定...
继续阅读 »


最近发现网易号有盗掘金文章的,xdm有空可以关注一下这个问题,希望帮助到大家同时能够保障自己权益。

前言

本文记录一下我是如何使用Gateway搭建网关服务及实现动态路由的,帮助大家学习如何快速搭建一个网关服务,了解路由相关配置,鉴权的流程及业务处理,有兴趣的一定看到最后,非常适合没接触过网关服务的同学当作入门教程。

搭建服务

框架

  • SpringBoot 2.1

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.1.0.RELEASE</version>
</parent>
  • Spring-cloud-gateway-core

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-gateway-core</artifactId>
</dependency>
  • common-lang3

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
</dependency>

路由配置

网关作为请求统一入口,路由就相当于是每个业务系统的入口,通过路由规则则可以匹配到对应微服务的入口,将请求命中到对应的业务系统中

server:
port: 8080

spring:
cloud:
  gateway:
    enabled: true
    routes:
    - id: demo-server
      uri: http://localhost:8081
      predicates:
      - Path=/demo-server/**
      filters:
        - StripPrefix= 1

routes

配置项描述
id路由唯一id,使用服务名称即可
uri路由服务的访问地址
predicates路由断言
filters过滤规则

解读配置

  • 现在有一个服务demo-server部署在本机,地址和端口为127.0.0.1:8081,所以路由配置uri为http://localhost:8081

  • 使用网关服务路由到此服务,predicates -Path=/demo-server/**,网关服务的端口为8080,启动网关服务,访问localhost:8080/demo-server,路由断言就会将请求路由到demo-server

  • 直接访问demo-server的接口localhost:8081/api/test,通过网关的访问地址则为localhost:8080/demo-server/api/test,predicates配置将请求断言到此路由,filters-StripPrefix=1代表将地址中/后的第一个截取,所以demo-server就截取掉了

使用gateway通过配置文件即可完成路由的配置,非常方便,我们只要充分的了解配置项的含义及规则就可以了;但是这些配置如果要修改则需要重启服务,重启网关服务会导致整个系统不可用,这一点是无法接受的,下面介绍如何通过Nacos实现动态路由

动态路由

使用nacos结合gateway-server实现动态路由,我们需要先部署一个nacos服务,可以使用docker部署或下载源码在本地启动,具体操作可以参考官方文档即可

Nacos配置


groupId: 使用网关服务名称即可

dataId: routes

配置格式: json

[{
     "id": "xxx-server",
     "order": 1, #优先级
     "predicates": [{ #路由断言
         "args": {
             "pattern": "/xxx-server/**"
        },
         "name": "Path"
    }],
     "filters":[{ #过滤规则
         "args": {
             "parts": 0 #k8s服务内部访问容器为http://xxx-server/xxx-server的话,配置0即可
        },
         "name": "StripPrefix" #截取的开始索引
    }],
     "uri": "http://localhost:8080/xxx-server" #目标地址
}]

json格式配置项与yaml中对应,需要了解配置在json中的写法

比对一下json配置与yaml配置

{
   "id":"demo-server",
   "predicates":[
      {
           "args":{
               "pattern":"/demo-server/**"
          },
           "name":"Path"
      }
  ],
   "filters":[
      {
           "args":{
               "parts":1
          },
           "name":"StripPrefix"
      }
  ],
   "uri":"http://localhost:8081"
}
spring:
 cloud:
   gateway:
     enabled: true
     routes:
     - id: demo-server
       uri: http://localhost:8081
       predicates:
       - Path=/demo-server/**
       filters:
         - StripPrefix= 1

代码实现

Nacos实现动态路由的方式核心就是通过Nacos配置监听,配置发生改变后执行网关相关api创建路由


@Component
public class NacosDynamicRouteService implements ApplicationEventPublisherAware {

   private static final Logger LOGGER = LoggerFactory.getLogger(NacosDynamicRouteService.class);

   @Autowired
   private RouteDefinitionWriter routeDefinitionWriter;

   private ApplicationEventPublisher applicationEventPublisher;

   /** 路由id */
   private static List<String> routeIds = Lists.newArrayList();

   /**
    * 监听nacos路由配置,动态改变路由
    * @param configInfo
    */
   @NacosConfigListener(dataId = "routes", groupId = "gateway-server")
   public void routeConfigListener(String configInfo) {
       clearRoute();
       try {
           List<RouteDefinition> gatewayRouteDefinitions = JSON.parseArray(configInfo, RouteDefinition.class);
           for (RouteDefinition routeDefinition : gatewayRouteDefinitions) {
               addRoute(routeDefinition);
          }
           publish();
           LOGGER.info("Dynamic Routing Publish Success");
      } catch (Exception e) {
           LOGGER.error(e.getMessage(), e);
      }
       
  }


   /**
    * 清空路由
    */
   private void clearRoute() {
       for (String id : routeIds) {
           routeDefinitionWriter.delete(Mono.just(id)).subscribe();
      }
       routeIds.clear();
  }

   @Override
   public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
       this.applicationEventPublisher = applicationEventPublisher;
  }

   /**
    * 添加路由
    *
    * @param definition
    */
   private void addRoute(RouteDefinition definition) {
       try {
           routeDefinitionWriter.save(Mono.just(definition)).subscribe();
           routeIds.add(definition.getId());
      } catch (Exception e) {
           LOGGER.error(e.getMessage(), e);
      }
  }

   /**
    * 发布路由、使路由生效
    */
   private void publish() {
       this.applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this.routeDefinitionWriter));
  }
}

过滤器

gateway提供GlobalFilter及Ordered两个接口用来定义过滤器,我们自定义过滤器只需要实现这个两个接口即可

  • GlobalFilter filter() 实现过滤器业务

  • Ordered getOrder() 定义过滤器执行顺序

通常一个网关服务的过滤主要包含 鉴权(是否登录、是否黑名单、是否免登录接口...) 限流(ip限流等等)功能,我们今天简单介绍鉴权过滤器的流程实现

鉴权过滤器

需要实现鉴权过滤器,我们先得了解登录及鉴权流程,如下图所示

由图可知,我们鉴权过滤核心就是验证token是否有效,所以我们网关服务需要与业务系统在同一个redis库,先给网关添加redis依赖及配置

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
spring:
redis:
  host: redis-server
  port: 6379
  password:
  database: 0

代码实现

  • 1.定义过滤器AuthFilter

  • 2.获取请求对象 从请求头或参数或cookie中获取token(支持多种方式传token对于客户端更加友好,比如部分web下载请求会新建一个页面,在请求头中传token处理起来比较麻烦)

  • 3.没有token,返回401

  • 4.有token,查询redis是否有效

  • 5.无效则返回401,有效则完成验证放行

  • 6.重置token过期时间、添加内部请求头信息方便业务系统权限处理

@Component
public class AuthFilter implements GlobalFilter, Ordered {

@Autowired
private RedisTemplate<String, String> redisTemplate;

private static final String TOKEN_HEADER_KEY = "auth_token";

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求对象
ServerHttpRequest request = exchange.getRequest();
// 2.获取token
String token = getToken(request);
ServerHttpResponse response = exchange.getResponse();
if (StringUtils.isBlank(token)) {
// 3.token为空 返回401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 4.验证token是否有效
String userId = getUserIdByToken(token);
if (StringUtils.isBlank(userId)) {
// 5.token无效 返回401
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// token有效,后续业务处理
// 从写请求头,方便业务系统从请求头获取用户id进行权限相关处理
ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
request = builder.header("user_id", userId).build();
// 延长缓存过期时间-token缓存用户如果一直在操作就会一直重置过期
// 这样避免用户操作过程中突然过期影响业务操作及体验,只有用户操作间隔时间大于缓存过期时间才会过期
resetTokenExpirationTime(token, userId);
// 完成验证
return chain.filter(exchange);
}


@Override
public int getOrder() {
// 优先级 越小越优先
return 0;
}

/**
* 从redis中获取用户id
* 在登录操作时候 登陆成功会生成一个token, redis得key为auth_token:token 值为用户id
*
* @param token
* @return
*/
private String getUserIdByToken(String token) {
String redisKey = String.join(":", "auth_token", token);
return redisTemplate.opsForValue().get(redisKey);
}

/**
* 重置token过期时间
*
* @param token
* @param userId
*/
private void resetTokenExpirationTime(String token, String userId) {
String redisKey = String.join(":", "auth_token", token);
redisTemplate.opsForValue().set(redisKey, userId, 2, TimeUnit.HOURS);
}


/**
* 获取token
*
* @param request
* @return
*/
private static String getToken(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
// 从请求头获取token
String token = headers.getFirst(TOKEN_HEADER_KEY);
if (StringUtils.isBlank(token)) {
// 请求头无token则从url获取token
token = request.getQueryParams().getFirst(TOKEN_HEADER_KEY);
}
if (StringUtils.isBlank(token)) {
// 请求头和url都没有token则从cookies获取
HttpCookie cookie = request.getCookies().getFirst(TOKEN_HEADER_KEY);
if (cookie != null) {
token = cookie.getValue();
}
}
return token;
}
}

总结

Gateway通过配置项可以实现路由功能,整合Nacos及配置监听可以实现动态路由,实现GlobalFilter, Ordered两个接口可以快速实现一个过滤器,文中也详细的介绍了登录后的请求鉴权流程,如果有不清楚地方可以评论区见咯。

来源:juejin.cn/post/7004756545741258765

收起阅读 »

如何利用 Flutter 实现炫酷的 3D 卡片和帅气的 360° 展示效果

本篇将带你在 Flutter 上快速实现两个炫酷的动画特效,希望最后的效果可以惊艳到你。 这次灵感的来源于更新 MIUI 13 时刚好看到的卡片效果,其中除了卡片会跟随手势出现倾斜之外,内容里的部分文本和绿色图标也有类似悬浮的视差效果,恰逢此时灵机一动,我们也...
继续阅读 »

本篇将带你在 Flutter 上快速实现两个炫酷的动画特效,希望最后的效果可以惊艳到你。


这次灵感的来源于更新 MIUI 13 时刚好看到的卡片效果,其中除了卡片会跟随手势出现倾斜之外,内容里的部分文本和绿色图标也有类似悬浮的视差效果,恰逢此时灵机一动,我们也来用 Flutter 快速实现炫酷的 3D 视差卡片,最后再拓展实现一个支持帅气的 360° 展示的卡片效果



❤️ 本文正在参加征文投稿活动,还请看官们走过路过来个点赞一键三连,感激不尽~




既然需要卡片跟随手势产生不规则形变,我们第一个想到的肯定是矩阵变换,在 Flutter 里我们可以使用 Matrix4 配合 Transform 来实现矩阵变换效果。


开始之前,首先我们创建用 Transform 嵌套一个 GestureDetector ,并绘制出一个 300x400 的圆角卡片,用于后续进行矩阵变换处理。


Transform(
transform: Matrix4.identity(),
child: GestureDetector(
child: Container(
width: 300,
height: 400,
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.blueGrey,
borderRadius: BorderRadius.circular(20),
),
),
),
);


接着,如下代码所示,因为我们需要卡片跟随手势进行矩阵变换,所以我们可以直接在 GestureDetectoronPanUpdate 里获取到手势信息,例如 localPosition 位置信息,然后把对应的 dxdy赋值到 Matrix4rotateXrotateY 上实现旋转。


child: Transform(
transform: Matrix4.identity()
..rotateX(touchY)
..rotateY(touchX),
alignment: FractionalOffset.center,
child: GestureDetector(
onPanUpdate: (details) {
setState(() {
touchX = details.localPosition.dx;
touchY = details.localPosition.dy;
});
},
child: Container(

这里有个需要注意的是:上面代码里 rotateX 使用的是 touchY ,而 rotateY 使用的是 touchX ,为什么要这样做呢?



⚠️举个例子,当我们手指左右移动时,是希望卡片可以围绕 Y 轴进行旋转,所以我们会把 touchX 传递给了 rotateY ,同样 touchY 传递给 rotateX 也是一个道理。




但是当我们实际运行上述代码之后,如下图所示,可以看到基本上我们只是稍微移动手指,卡片就会陷入疯狂旋转的情况,并且实际的旋转速度会比 GIF 里快很多。



问题的原因其实是因为 rotateXrotateY 需要的是一个 angle 参数,假设这里对 rotateXrotateY 设置 pi / 4 ,就可以看到卡片在 X 轴和 Y 轴上都产生了 45 度的旋转效果。


 Transform(
transform: Matrix4.identity()
..rotateX(pi / 4)
..rotateY(pi / 4),
alignment: FractionalOffset.center,


所以如果直接使用手势的 localPosition 作用于 Matrix4 肯定是不行的,我们首先需要对手势数据进行一个采样,因为代码里我们设置了 FractionalOffset.center ,所以我们可以用卡片的中心点来计算手指位置,再进行压缩处理


如下代码所示,我们通过以卡片中心点为原点进行计算,其中 / 2 就是得到卡片的中心点,/ 100 是对数据进行压缩采样,但是为什么 touchXtouchY 的计算方式是相反的呢


touchX = (cardWidth / 2 - details.localPosition.dx) / 100;
touchY = (details.localPosition.dy - cardHeight / 2 ) / 100;

如下图所示,因为在设置 rotateXrotateY 时,赋予 > 0 的数据时卡片就会以图片中的方向进行旋转,由于我们是需要手指往哪边滑动,卡片就往哪边倾斜,所以:



  • 当我们往左水平滑动时,需要卡片往左边倾斜,也就是图中绕 Y 轴转动的 >0 的方向,并且越靠近左边需要正向的 Angle 数值越大,由于此时 localPosition.dx 是越往左越小,所以需要利用 CardWidth / 2 - details.localPosition.dx 进行计算,得到越往左有越大的正向 Angle 数值

  • 同理,当我们往下滑动时,需要卡片往下边倾斜,也就是图中绕 X 轴转动的 >0 的方向,并且越靠近下边需要正向 Angle 数值越大,由于此时 localPosition.dy 越往下越大,所以使用 details.localPosition.dy - cardHeight / 2 去计算得到正确数据










如果觉得太抽象,可以结合上边右侧的动图,和大家买股票一样,图中显示红色时是正数,显示绿色时是负数,可以看到:



  • 手指往左移动时,第一行 TouchX 是红色正数,被设置给 rotateY , 然后卡片绕 Y 轴正方向旋转

  • 手指往下移动时,第二行 TouchY 是红色正数,被设置给 rotateX , 然后卡片绕 X 轴正方向旋转


到这里我们就初步实现了卡片跟随手机旋转的效果,但是这时候的立体旋转效果看起来其实“很别扭”,总感觉差了点什么,其实这是因为卡片在旋转时没有产生视觉上的深度感知


所以我们可以通过矩阵的透视变换调整视觉效果,而为了在 Z 方向实现深度感知,我们需要在矩阵中配置 .setEntry(3, 2, 0.001) ,这里的 3 表示第 3 列,2 表示第 2 行,因为是从 0 开始排列,所以也就是图片中 Z 的位置。



其实 .setEntry(3, 2, 0.001) 就是调整 Z 轴的视角,而在 Z 上的 0.001 就是需要的透视效果测量值,类似于相机上的对焦点进行放大和缩小的作用,这个数字越大就会让交点处看起来好像离你视觉更近,所以最终代码如下


Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateX(touchY)
..rotateY(touchX),
alignment: FractionalOffset.center,

运行之后,可以看到在增加了 Z 角度的视角调整之后,这时候看起来的立体效果就好了很多,并且也有了类似 3D 空间的感觉。



接着我们在卡片上放上一个添加一个 13Text 文本,运行之后可以看到此时文本是跟随卡片发生变化,而接下来我们需要做的,就是通过另外一个 Transform 来让 Text 文本和卡片之间产生视差,从而出现悬浮的效果










所以接下来需要给文本内容设置一个 translateMatrix4 ,让它向着倾斜角度的相反方向移动,然后对前面的 touchXtouchY 进行放大,然后再通过 - 10 操作来产生一个位差。


    Transform(
transform: Matrix4.identity()
..translate(touchX * 100 - 10,
touchY * 100 - 10, 0.0),


-10 这个是我随意写的,你也可以根据自己的需求调节。



例如,这时候当卡片往左倾斜时,文字就会向右移动,从而产生视觉差的效果,得到类似悬浮的感觉。










完成这一步之后,接下来可以我们对文本内容进行一下美化处理,例如增加渐变颜色,添加阴影,更换字体,目的是让字体看起来更加具备立体的效果,这里使用的 shader ,也可以让文字在移动过程中出现不同角度的渐变效果










最后,我们还需要对卡片旋转进行一个范围约束,这里主要是通过卡片大小比例:



  • onPanUpdate 时对 touchXtouchY 进行范围约束,从而约束的卡片的倾斜角度

  • 增加了 startTransform 标志位,用于在 onTapUp 或者 onPanEnd 之后,恢复卡片回到默认状态的作用。


Transform(
transform: Matrix4.identity()
..setEntry(3, 2, 0.001)
..rotateX(startTransform ? touchY : 0.0)
..rotateY(startTransform ? touchX : 0.0),
alignment: FractionalOffset.center,
child: GestureDetector(
onTapUp: (_) => setState(() {
startTransform = false;
}),
onPanCancel: () => setState(() => startTransform = false),
onPanEnd: (_) => setState(() {
startTransform = false;
}),
onPanUpdate: (details) {
setState(() => startTransform = true);
///y轴限制范围
if (details.localPosition.dx < cardWidth * 0.55 &&
details.localPosition.dx > cardWidth * 0.3) {
touchX = (cardWidth / 2 - details.localPosition.dx) / 100;
}

///x轴限制范围
if (details.localPosition.dy > cardHeight * 0.4 &&
details.localPosition.dy < cardHeight * 0.6) {
touchY = (details.localPosition.dy - cardHeight / 2) / 100;
}
},
child:

到这里,我们只需要在全局再进行一些美化处理,运行之后就会如下图所示,再配合阴影和渐变效果,整体的视觉立体感会更强烈,此时我们基本就实现了一开始想要的功能,




完整代码可见: card_perspective_demo_page.dart


Web 体验地址,PC 端记得开 Chrome 手机模式: 3D 视差卡片



那有人可能就想问了: 学会了这个我们还可以实现什么


举个例子,比如我们可以实现一个 “伪3D” 的 360° 卡片效果,利用堆叠实现立体的电子银行卡效果。


依旧是前面的手势旋转逻辑,只是这里我们可以把具有前后画面的银行卡图片,通过 IndexedStack 嵌套起来,嵌套之后主要是根据旋转角度来调整 IndexedStack 里需要展示的图片,然后利用透视旋转来实现类似 3D 物体的 360° 旋转展示



这里的关键是通过手势旋转角度,判断当前需要展示 IndexedStack 里的哪个卡片,因为 Flutter 使用的 Skia 是 2D 渲染引擎,如果没有这部分逻辑,你就只会看到单张图片画面的旋转效果。


if (touchX.abs() % (pi * 3 / 2) >= pi / 2 ||
touchY.abs() % (pi * 3 / 2) >= pi / 2) {
showIndex = 0;
} else {
showIndex = 1;
}

运行效果如下图所示,可以看到在视差和图片切换的作用下,我们用很低的成本在 Flutter 上实现了 “伪3D” 的卡片的 360° 展示,类似的实现其实还可以用于一些商品展示或者页面切换的场景,本质上就是利用视差的效果,在 2D 屏幕上模拟现实中的画面效果,从而达到类似 3D 的视觉作用










最后我们只需要用 Text 在卡片上添加“模拟”凹凸的文字,就实现了我们现实中类似银行卡的卡面效果




完整代码可见: card_3d_demo_page.dart


Web 体验地址,PC 端记得开 chrome 手机模式: 360° 可视化 3D 电子银行卡



好了,本篇动画特效就到此为止,如果你有什么想法,欢迎留言评论,感谢大家耐心看完,也还请看官们走过路过的来个点赞一键三连,感激不尽


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

Android Native 异常捕获库

Android Native 异常捕获库 基于google/breakpad的Android Native 异常捕获库,在native层发生异常时java层能得到相关异常信息。 项目主页 现状 发生native异常时,安卓系统会将native异常信息输...
继续阅读 »

Android Native 异常捕获库


image image image


基于google/breakpad的Android Native 异常捕获库,在native层发生异常时java层能得到相关异常信息。


项目主页


现状



  • 发生native异常时,安卓系统会将native异常信息输出到logcat中,但是java层无法感知到native异常的发生,进而无法获取这些异常信息并上报到业务的异常监控系统。

  • 业务部门可以快速实现java层的异常监控系统(java层全局异常捕获的实现很简单),又或者业务部门已经实现了java层的异常监控系统,但没有覆盖到native层的异常捕获。

  • 安卓还可以接入Breakpad,其导出的minidump文件不仅体积小信息还全,但有两个问题:

    • 1.和现状第1点的问题相同。

    • 2.:需要拉取minidump文件并经过比较繁琐的步骤才可以得出有用的信息:

      • 启动时检测Breakpad是否有导出过minidump文件,有则说明发生过native异常。

      • 到客户现场,或者远程拉取minidump文件。

      • 编译出自己电脑的操作系统的minidump_stackwalk工具。

      • 使用minidump_stackwalk工具翻译minidump文件内容,例如拿到崩溃时的程序计数器寄存器内的值(下文称为pc值)。

      • 找到对应崩溃so库ABI的add2line工具,并根据上一步拿到的pc值定位出发生异常的代码行数。






整个步骤十分复杂和繁琐,且没有java层的crash线程栈信息,不利于java开发者快速定位调用native的代码。


设计意图



  1. 让java层有知悉native异常的通道:

    • java开发者可以在java代码中得到native异常的情况,进而对native异常做出反应,而不是再次启动后去检测Breakpad是否有导出过minidump文件。



  2. 增加信息的可用性,进而提升问题分析的效率:


    • 回调中提供naive异常信息、naive和java调用栈信息和minidump文件文件路径,这些信息可以直接通过业务部门的异常监控系统上报。




    • 划分为两个阶段解决问题,我预想是大部分都在阶段一解决了问题,而不需要再对minidump文件进行分析,总体来讲是提升了分析效率的:



      • 阶段一:有了java的调用栈和native的调用栈信息,大部分异常原因都可以快速定位并分析出来。

      • 阶段二:回调中也会提供minidump文件的存储路径,业务部门可以按需拉取。(这一步需要业务部门本身有拉取日志的功能,且需要按上文”现状部分进行操作”,较费时费力)





  3. 最少改动:

    • 让接入方不因为引入新功能而大量改动现有代码。例如:在native崩溃回调处,使用现有的java层异常监控系统上报native异常信息。



  4. 单一职责:

    • 只做native的crash捕获,不做系统内存情况、cpu使用率、系统日志等信息的采集功能。




整体流程


image.png


功能介绍



  • 保留breakpad导出minidump文件功能 (可选择是否启用)

  • 发生native异常时将异常信息、native层调用栈、java层的调用栈通过回调提供给开发者,将这些信息输出到控制台的效果如下:


2022-02-14 11:33:08.598 30228-30253/com.babyte.banativecrash E/crash:  
/data/user/0/com.babyte.banativecrash/cache/f1474006-60ca-40f4-c9d8e89a-47e90c2e.dmp
2022-02-14 11:33:08.599 30228-30253/com.babyte.banativecrash E/crash:
Operating system: Android 28 Linux 4.4.146 #37 SMP PREEMPT Wed Jan 20 18:26:59 CST 2021
CPU: aarch64 (8 core)

Crash reason: signal 11(SIGSEGV) Invalid address
Crash address: 0000000000000000
Crash pc: 0000000000000650
Crash so: /data/app/com.babyte.banativecrash-ptLzOQ_6UYz-W3Vgyact8A==/lib/arm64/libnative-lib.so(arm64)
Crash method: _Z5Crashv
2022-02-14 11:33:08.602 30228-30253/com.babyte.banativecrash E/crash:
Thread[name:DefaultDispatch] (NOTE: linux thread name length limit is 15 characters)
#00 pc 0000000000000650 /data/app/com.babyte.banativecrash-ptLzOQ_6UYz-W3Vgyact8A==/lib/arm64/libnative-lib.so (Crash()+20)
#01 pc 0000000000000670 /data/app/com.babyte.banativecrash-ptLzOQ_6UYz-W3Vgyact8A==/lib/arm64/libnative-lib.so (Java_com_babyte_banativecrash_MainActivity_nativeCrash+20)
#02 pc 0000000000565de0 /system/lib64/libart.so (offset 0xc1000) (art_quick_generic_jni_trampoline+144)
#03 pc 000000000055cd88 /system/lib64/libart.so (offset 0xc1000) (art_quick_invoke_stub+584)
#04 pc 00000000000cf740 /system/lib64/libart.so (art::ArtMethod::Invoke(art::Thread*, unsigned int*, unsigned int, art::JValue*, char const*)+200)
#05 pc 00000000002823b8 /system/lib64/libart.so (offset 0xc1000)
...
2022-02-14 11:33:08.603 30228-30253/com.babyte.banativecrash E/crash:
Thread[DefaultDispatcher-worker-1,5,main]
at com.babyte.banativecrash.MainActivity.nativeCrash(Native Method)
at com.babyte.banativecrash.MainActivity$onCreate$2$1.invokeSuspend(MainActivity.kt:39)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
Thread[DefaultDispatcher-worker-2,5,main]
at java.lang.Object.wait(Native Method)
at java.lang.Thread.parkFor$(Thread.java:2137)
at sun.misc.Unsafe.park(Unsafe.java:358)
at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:353)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.park(CoroutineScheduler.kt:795)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.tryPark(CoroutineScheduler.kt:740)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:711)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
...

定位到so中具体代码行示例


可以使用ndk中的add2line工具根据pc值和带符号信息的so库,定位出具体代码行数。


例:从上文的异常信息中可以看到abi是aarch64,对应的so库abi是arm64,所以add2line的使用如下:


$ ./ndk/android-ndk-r16b/toolchains/aarch64-linux-android-4.9/prebuilt/darwin-x86_64/bin/aarch64-linux-android-addr2line -Cfe ~/arm64-v8a/libnative-lib.so 0000000000000650

输出结果如下:


Crash()
/Users/ba/AndroidStudioProjects/NativeCrash2Java/app/.cxx/cmake/debug/arm64-v8a/../../../../src/main/cpp/native-lib.cpp:6

接入方式


根项目的build.gradle中:


allprojects {
repositories {
mavenCentral()//添加这一行
}
}

模块的build.gradle中:


dependencies {   
//添加这一行,releaseVersionCode填最新的版本
implementation 'io.github.BAByte:native-crash:releaseVersionCode'
}

初始化


两种模式可选:


//发生native异常时:回调异常信息并导出minidump到指定目录,
BaByteBreakpad.initBreakpad(this.cacheDir.absolutePath) { info:CrashInfo ->
//格式化输出到控制台
BaByteBreakpad.formatPrint(TAG, info)
}

//发生native异常时:回调异常信息
BaByteBreakpad.initBreakpad { info:CrashInfo ->
//格式化输出到控制台
BaByteBreakpad.formatPrint(TAG, info)
}

示例项目


点击查看:示例项目


致谢



  • 感谢google breakpad库提供的源码

  • 感谢腾讯bugly团队提供在发生异常时,native回调java层的思路

  • 感谢爱奇艺xCrash库源码中的dlopen思路

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

不掌握这些坑,你敢用BigDecimal吗?

背景 一直从事金融相关项目,所以对BigDecimal再熟悉不过了,也曾看到很多同学因为不知道、不了解或使用不当导致资损事件发生。 所以,如果你从事金融相关项目,或者你的项目中涉及到金额的计算,那么你一定要花时间看看这篇文章,全面学习一下BigDecimal。...
继续阅读 »

背景


一直从事金融相关项目,所以对BigDecimal再熟悉不过了,也曾看到很多同学因为不知道、不了解或使用不当导致资损事件发生。


所以,如果你从事金融相关项目,或者你的项目中涉及到金额的计算,那么你一定要花时间看看这篇文章,全面学习一下BigDecimal。


BigDecimal概述


Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。


一般情况下,对于不需要准确计算精度的数字,可以直接使用Float和Double处理,但是Double.valueOf(String) 和Float.valueOf(String)会丢失精度。所以如果需要精确计算的结果,则必须使用BigDecimal类来操作。


BigDecimal对象提供了传统的+、-、*、/等算术运算符对应的方法,通过这些方法进行相应的操作。BigDecimal都是不可变的(immutable)的, 在进行每一次四则运算时,都会产生一个新的对象 ,所以在做加减乘除运算时要记得要保存操作后的值。


BigDecimal的4个坑


在使用BigDecimal时,有4种使用场景下的坑,你一定要了解一下,如果使用不当,必定很惨。掌握这些案例,当别人写出有坑的代码,你也能够一眼识别出来,大牛就是这么练成的。


第一:浮点类型的坑


在学习了解BigDecimal的坑之前,先来说一个老生常谈的问题:如果使用Float、Double等浮点类型进行计算时,有可能得到的是一个近似值,而不是精确的值。


比如下面的代码:


  @Test
public void test0(){
float a = 1;
float b = 0.9f;
System.out.println(a - b);
}

结果是多少?0.1吗?不是,执行上面代码执行的结果是0.100000024。之所以产生这样的结果,是因为0.1的二进制表示是无限循环的。由于计算机的资源是有限的,所以是没办法用二进制精确的表示 0.1,只能用「近似值」来表示,就是在有限的精度情况下,最大化接近 0.1 的二进制数,于是就会造成精度缺失的情况


关于上述的现象大家都知道,不再详细展开。同时,还会得出结论在科学计数法时可考虑使用浮点类型,但如果是涉及到金额计算要使用BigDecimal来计算。


那么,BigDecimal就一定能避免上述的浮点问题吗?来看下面的示例:


  @Test
public void test1(){
BigDecimal a = new BigDecimal(0.01);
BigDecimal b = BigDecimal.valueOf(0.01);
System.out.println("a = " + a);
System.out.println("b = " + b);
}

上述单元测试中的代码,a和b结果分别是什么?


a = 0.01000000000000000020816681711721685132943093776702880859375
b = 0.01

上面的实例说明,即便是使用BigDecimal,结果依旧会出现精度问题。这就涉及到创建BigDecimal对象时,如果有初始值,是采用new BigDecimal的形式,还是通过BigDecimal#valueOf方法了。


之所以会出现上述现象,是因为new BigDecimal时,传入的0.1已经是浮点类型了,鉴于上面说的这个值只是近似值,在使用new BigDecimal时就把这个近似值完整的保留下来了。


而BigDecimal#valueOf则不同,它的源码实现如下:


    public static BigDecimal valueOf(double val) {
      // Reminder: a zero double returns '0.0', so we cannot fastpath
      // to use the constant ZERO. This might be important enough to
      // justify a factory approach, a cache, or a few private
      // constants, later.
      return new BigDecimal(Double.toString(val));
  }

在valueOf内部,使用Double#toString方法,将浮点类型的值转换成了字符串,因此就不存在精度丢失问题了。


此时就得出一个基本的结论:第一,在使用BigDecimal构造函数时,尽量传递字符串而非浮点类型;第二,如果无法满足第一条,则可采用BigDecimal#valueOf方法来构造初始化值


这里延伸一下,BigDecimal常见的构造方法有如下几种:


BigDecimal(int)       创建一个具有参数所指定整数值的对象。
BigDecimal(double)   创建一个具有参数所指定双精度值的对象。
BigDecimal(long)     创建一个具有参数所指定长整数值的对象。
BigDecimal(String)   创建一个具有参数所指定以字符串表示的数值的对象。

其中涉及到参数类型为double的构造方法,会出现上述的问题,使用时需特别留意。


第二:浮点精度的坑


如果比较两个BigDecimal的值是否相等,你会如何比较?使用equals方法还是compareTo方法呢?


先来看一个示例:


  @Test
public void test2(){
BigDecimal a = new BigDecimal("0.01");
BigDecimal b = new BigDecimal("0.010");
System.out.println(a.equals(b));
System.out.println(a.compareTo(b));
}

乍一看感觉可能相等,但实际上它们的本质并不相同。


equals方法是基于BigDecimal实现的equals方法来进行比较的,直观印象就是比较两个对象是否相同,那么代码是如何实现的呢?


    @Override
  public boolean equals(Object x) {
      if (!(x instanceof BigDecimal))
          return false;
      BigDecimal xDec = (BigDecimal) x;
      if (x == this)
          return true;
      if (scale != xDec.scale)
          return false;
      long s = this.intCompact;
      long xs = xDec.intCompact;
      if (s != INFLATED) {
          if (xs == INFLATED)
              xs = compactValFor(xDec.intVal);
          return xs == s;
      } else if (xs != INFLATED)
          return xs == compactValFor(this.intVal);

      return this.inflated().equals(xDec.inflated());
  }

仔细阅读代码可以看出,equals方法不仅比较了值是否相等,还比较了精度是否相同。上述示例中,由于两者的精度不同,所以equals方法的结果当然是false了。而compareTo方法实现了Comparable接口,真正比较的是值的大小,返回的值为-1(小于),0(等于),1(大于)。


基本结论:通常情况,如果比较两个BigDecimal值的大小,采用其实现的compareTo方法;如果严格限制精度的比较,那么则可考虑使用equals方法


另外,这种场景在比较0值的时候比较常见,比如比较BigDecimal("0")、BigDecimal("0.0")、BigDecimal("0.00"),此时一定要使用compareTo方法进行比较。


第三:设置精度的坑


在项目中看到好多同学通过BigDecimal进行计算时不设置计算结果的精度和舍入模式,真是着急人,虽然大多数情况下不会出现什么问题。但下面的场景就不一定了:


  @Test
public void test3(){
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("3.0");
a.divide(b);
}

执行上述代码的结果是什么?ArithmeticException异常


java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

at java.math.BigDecimal.divide(BigDecimal.java:1690)
...

这个异常的发生在官方文档中也有说明:


If the quotient has a nonterminating decimal expansion and the operation is specified to return an exact result, an ArithmeticException is thrown. Otherwise, the exact result of the division is returned, as done for other operations.


总结一下就是,如果在除法(divide)运算过程中,如果商是一个无限小数(0.333…),而操作的结果预期是一个精确的数字,那么将会抛出ArithmeticException异常。


此时,只需在使用divide方法时指定结果的精度即可:


  @Test
public void test3(){
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("3.0");
BigDecimal c = a.divide(b, 2,RoundingMode.HALF_UP);
System.out.println(c);
}

执行上述代码,输入结果为0.33。


基本结论:在使用BigDecimal进行(所有)运算时,一定要明确指定精度和舍入模式


拓展一下,舍入模式定义在RoundingMode枚举类中,共有8种:



  • RoundingMode.UP:舍入远离零的舍入模式。在丢弃非零部分之前始终增加数字(始终对非零舍弃部分前面的数字加1)。注意,此舍入模式始终不会减少计算值的大小。

  • RoundingMode.DOWN:接近零的舍入模式。在丢弃某部分之前始终不增加数字(从不对舍弃部分前面的数字加1,即截短)。注意,此舍入模式始终不会增加计算值的大小。

  • RoundingMode.CEILING:接近正无穷大的舍入模式。如果 BigDecimal 为正,则舍入行为与 ROUNDUP 相同;如果为负,则舍入行为与 ROUNDDOWN 相同。注意,此舍入模式始终不会减少计算值。

  • RoundingMode.FLOOR:接近负无穷大的舍入模式。如果 BigDecimal 为正,则舍入行为与 ROUNDDOWN 相同;如果为负,则舍入行为与 ROUNDUP 相同。注意,此舍入模式始终不会增加计算值。

  • RoundingMode.HALF_UP:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为向上舍入的舍入模式。如果舍弃部分 >= 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同。注意,这是我们在小学时学过的舍入模式(四舍五入)。

  • RoundingMode.HALF_DOWN:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则为上舍入的舍入模式。如果舍弃部分 > 0.5,则舍入行为与 ROUND_UP 相同;否则舍入行为与 ROUND_DOWN 相同(五舍六入)。

  • RoundingMode.HALF_EVEN:向“最接近的”数字舍入,如果与两个相邻数字的距离相等,则向相邻的偶数舍入。如果舍弃部分左边的数字为奇数,则舍入行为与 ROUNDHALFUP 相同;如果为偶数,则舍入行为与 ROUNDHALF_DOWN 相同。注意,在重复进行一系列计算时,此舍入模式可以将累加错误减到最小。此舍入模式也称为“银行家舍入法”,主要在美国使用。四舍六入,五分两种情况。如果前一位为奇数,则入位,否则舍去。以下例子为保留小数点1位,那么这种舍入方式下的结果。1.15 ==> 1.2 ,1.25 ==> 1.2

  • RoundingMode.UNNECESSARY:断言请求的操作具有精确的结果,因此不需要舍入。如果对获得精确结果的操作指定此舍入模式,则抛出ArithmeticException。


通常我们使用的四舍五入即RoundingMode.HALF_UP。


第四:三种字符串输出的坑


当使用BigDecimal之后,需要转换成String类型,你是如何操作的?直接toString?


先来看看下面的代码:


@Test
public void test4(){
BigDecimal a = BigDecimal.valueOf(35634535255456719.22345634534124578902);
System.out.println(a.toString());
}

执行的结果是上述对应的值吗?并不是:


3.563453525545672E+16

也就是说,本来想打印字符串的,结果打印出来的是科学计数法的值。


这里我们需要了解BigDecimal转换字符串的三个方法



  • toPlainString():不使用任何科学计数法;

  • toString():在必要的时候使用科学计数法;

  • toEngineeringString() :在必要的时候使用工程计数法。类似于科学计数法,只不过指数的幂都是3的倍数,这样方便工程上的应用,因为在很多单位转换的时候都是10^3;


三种方法展示结果示例如下:


计算法


基本结论:根据数据结果展示格式不同,采用不同的字符串输出方法,通常使用比较多的方法为toPlainString()


另外,NumberFormat类的format()方法可以使用BigDecimal对象作为其参数,可以利用BigDecimal对超出16位有效数字的货币值,百分值,以及一般数值进行格式化控制。


使用示例如下:


NumberFormat currency = NumberFormat.getCurrencyInstance(); //建立货币格式化引用
NumberFormat percent = NumberFormat.getPercentInstance(); //建立百分比格式化引用
percent.setMaximumFractionDigits(3); //百分比小数点最多3位

BigDecimal loanAmount = new BigDecimal("15000.48"); //金额
BigDecimal interestRate = new BigDecimal("0.008"); //利率
BigDecimal interest = loanAmount.multiply(interestRate); //相乘

System.out.println("金额:\t" + currency.format(loanAmount));
System.out.println("利率:\t" + percent.format(interestRate));
System.out.println("利息:\t" + currency.format(interest));

输出结果如下:


金额: ¥15,000.48 
利率: 0.8%
利息: ¥120.00

小结


本篇文章介绍了BigDecimal使用中场景的坑,以及基于这些坑我们得出的“最佳实践”。虽然某些场景下推荐使用BigDecimal,它能够达到更好的精度,但性能相较于double和float,还是有一定的损失的,特别在处理庞大,复杂的运算时尤为明显。故一般精度的计算没必要使用BigDecimal。而必须使用时,一定要规避上述的坑。


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

Flutter 组件集录 | 桌面导航 NavigationRail

我们都知道 BottomNavigationBar 是一个移动端非常常用的底部导航栏组件,可以用于点击处理激活菜单,并通过回调来处理界面的切换。 -- 但是在桌面端,由于一般是宽大于高,所以 BottomNavigationBar ...
继续阅读 »

我们都知道 BottomNavigationBar 是一个移动端非常常用的底部导航栏组件,可以用于点击处理激活菜单,并通过回调来处理界面的切换。















--



但是在桌面端,由于一般是宽大于高,所以 BottomNavigationBar 并不适用。而是侧边的导航栏较为常见,比如下面飞书的客户端界面布局。



为了满足桌面端的导航栏适用需求,官方新增了 NavigationRail 组件,而非对 BottomNavigationBar 组件进行适配。之前我也说过,对于差异较大的结构,并没有必要让一个组件通过适配来完成两端需求。分离开来也不是坏事,让一件衣服同时适配 蚂蚁燕子 是很困难的,这时做两件衣服,各司其职显然是更好地方式。


BottomNavigationBarNavigationRail 两个导航就是如此,从语义上来看 Bottom 就是用于底部的导航, Rail扶手铁轨 的意思,作为侧栏导航的语义,还是很生动有趣的。两者分别处理特定的结构,这也很符合 单一职责 的原则。


该组件已录入 【FlutterUnit】 ,可以在 App 中体验。另外,本文中的代码可在对应文件夹中查看:


image.png




1. NavigationRail 组件的基本使用

下面是 NavigationRail 组件的构造方法,其中必须传入的有两个参数:



  • destinations : 表示导航栏的信息,是 NavigationRailDestination 列表。

  • selectedIndex: 表示激活索引,int 类型。





我们先来实现如下最简单的使用场景,左侧导航栏,在点击时切换右侧内容页:



如果导航栏的数据是固定的,可以提前定义如下的 destinations 常量。如下的 _buildLeftNavigation 方法负责构建左侧导航栏,NavigationRail 在构造中可以通过 onDestinationSelected 回调方法,来监听用户和导航栏的交互事件,传递用点击的索引位置。


final List<NavigationRailDestination> destinations = const [
NavigationRailDestination(icon: Icon(Icons.message_outlined),label: Text("消息")),
NavigationRailDestination(icon: Icon(Icons.video_camera_back_outlined),label: Text("视频会议")),
NavigationRailDestination(icon: Icon(Icons.book_outlined),label: Text("通讯录")),
NavigationRailDestination(icon: Icon(Icons.cloud_upload_outlined),label: Text("云文档")),
NavigationRailDestination(icon: Icon(Icons.games_sharp),label: Text("工作台")),
NavigationRailDestination(icon: Icon(Icons.calendar_month),label: Text("日历"))
];

Widget _buildLeftNavigation(int index){
return NavigationRail(
onDestinationSelected: _onDestinationSelected,
destinations: destinations,
selectedIndex: index,
);
}

void _onDestinationSelected(int value) {
//TODO 更新索引 + 切换界面
}



NavigationRail 的文档注释中说道:该组件一般在 Row 中,使用于 Scaffold.body 属性下。这也很容易理解,这是一个左右结构,在 Row 中可以通过 Expanded 可以自动延伸主体内容。如下,主体内容界面通过 PageView 进行构建,其中的 TestContent 组件在实际使用中换成你的需求界面。


@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
_buildLeftNavigation(index),
Expanded(child: PageView(
children:const [
TestContent(content: '消息',),
TestContent(content: '视频会议',),
TestContent(content: '通讯录',),
TestContent(content: '云文档',),
TestContent(content: '工作台',),
TestContent(content: '日历',),
],
))
],
),
);
}



最后是关键的一点:点击时,如何实现导航索引的切换和主体内容的切页。思路其实很简单,我们已经知道用户点击导航菜单的回调事件。对于 PageView 来说,可以通过 PageController 切换界面,NavigationRail 可以通过 selectedIndex 确定激活索引,所以只要用新索引重新构建 NavigationRail即可。
如下代码所示,在 _onDestinationSelected 在处理这两件重要的事。如下 tag1 处,通过 PageControllerjumpToPage 方法进行界面跳转。


这里通过 ValueListenableBuilder 来监听 _selectIndex 实现局部更新构建,如下 tag2 处,只要更新 _selectIndex 的值,就可以通知 ValueListenableBuilder 触发 builder 方法,使用新索引,构建 NavigationRail 。这样可以避免直接触发 _MyHomePageState 的更新方法,对 Scaffold 整体进行更新。


class _MyHomePageState extends State<MyHomePage> {

final PageController _controller = PageController();
final ValueNotifier<int> _selectIndex = ValueNotifier(0);

// 略同...
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
ValueListenableBuilder<int>(
valueListenable: _selectIndex,
builder: (_,index,__)=>_buildLeftNavigation(index),
),
Expanded(child: PageView(
controller: _controller,
// 略同...
}

void _onDestinationSelected(int value) {
_controller.jumpToPage(value); // tag1
_selectIndex.value = value; //tag2
}

@override
void dispose(){
_controller.dispose();
_selectIndex.dispose();
super.dispose();
}
}

这样就完成了 NavigationRail 最基本的使用,实现了左侧导航结构以及点击时的切换逻辑。NavigationRail 在构造方法中还有很多其他的配置参数用于样式调整,这些不是核心,但可以锦上添花,下面一起来看一下。




2.首尾组件与折叠

leadingtrailing 属性相当于两个插槽,如下所示,表示导航菜单外的首尾组件。



Widget _buildLeftNavigation(int index){
return NavigationRail(
leading: const Icon(Icons.menu_open,color: Colors.grey,),
trailing: FlutterLogo(),
onDestinationSelected: _onDestinationSelected,
destinations: destinations,
selectedIndex: index,
);
}



这里有个小细节,trailing 紧随最后一个菜单,如何让它像飞书的导航那样,在最尾部呢?偷瞄一些源码可以看出 trailing 是和导航菜单一起被放入 Column 中的。



所以我们可以通过 Expanded 来延伸剩余空间形成紧约束,通过 Align 使 FlutterLogo 排在下方:



Widget _buildLeftNavigation(int index){
return NavigationRail(
leading: const Icon(Icons.menu_open,color: Colors.grey,),
extended: false,
trailing: const Expanded(
child: Align(
alignment: Alignment.bottomCenter,
child: Padding(
padding: EdgeInsets.only(bottom: 20.0),
child: FlutterLogo(),
),
),
),
onDestinationSelected: _onDestinationSelected,
destinations: destinations,
selectedIndex: index,
);
}



另外,NavigationRail 中有个 extendedbool 参数,用于控制是否展开侧边栏,当该属性变化时,会进行动画展开和收起。如下所示,点击头部时,更新 NavigationRailextended 入参即可:





3.影深 与 标签类型

elevation 表示阴影的深度,这是非常常见的一个属性,如下红框所示,设置 elevation 之后右侧会有阴影,该值越大,阴影越明显。





labelType 参数表示标签类型,对应的属性是 NavigationRailLabelType 枚举。用于表示什么时候显示文字标签,默认是 none ,也就是只显示图标,没有文字。


enum NavigationRailLabelType {
none,
selected,
all,
}

设置为 all 时,效果如下:导航菜单会同时显示 图标文字标签





设置为 selected 时,效果如下:只有激活的导航菜单会同时显示 图标文字标签



另外,有一点需要注意: 当 extended 属性为 true 时, labelType 必须为 NavigationRailLabelType.none 不然会报错。



---->[NavigationRail构造断言]----
assert(!extended || (labelType == null || labelType == NavigationRailLabelType.none)),



4.背景、文字、图标样式


  • unselectedLabelTextStyle : 未选中签文字样式

  • selectedLabelTextStyle : 选中标签文字样式

  • unselectedIconTheme : 未选中图标样式

  • selectedIconTheme : 选中图标样式


这四个样式基本上是顾名思义,下面通过一个深色背景版本来使用一下:



@override
Widget build(BuildContext context) {
const Color textColor = Color(0xffcfd1d7);
const Color activeColor = Colors.blue;
const TextStyle labelStyle = TextStyle(color: textColor,fontSize: 11);

return NavigationRail(
backgroundColor: const Color(0xff324465),
unselectedIconTheme: const IconThemeData(color: textColor) ,
selectedIconTheme: const IconThemeData(color: activeColor) ,
unselectedLabelTextStyle: labelStyle,
selectedLabelTextStyle: labelStyle,
// 略同...
}



5.指示器与最小宽度


  • useIndicator : 是否添加指示器

  • indicatorColor : 指示器颜色


这两个属性用于控制图标后面的背景指示器,如下是在 NavigationRailLabelType.all 类型下指示器的样式,通过圆角矩形包裹图标:





NavigationRailLabelType.none 类型下,指示器通过圆形包裹图标:






  • minWidth : 默认 72 ,未展开时导航栏宽度




  • indicatorColor :默认 256 ,展开时导航栏宽度



NavigationRail 组件的属性介绍就到这里,总的来看,悬浮和点击时,导航栏还是一股 Material 的味。个人觉得这并不适合桌面端,导航栏的菜单可定制性也一般般,只能满足基本的需求。对于稍微特别点的样式,无法支持,比如飞书客户端的导航样式。另外像 拖动更换菜单位置 这样的交互,我们也只通过自定义组件来实现。





6.剖析 NavigationRail 组件,借鉴思路

就像世界上并没有什么包治百病的 ,我们也并不能苛求一个组件能满足所有的布局需求。对于一个原生组件满足不了的需求,发挥创造能力去解决问题,这应是我们的本职工作。借鉴官方对于组件实现的思路是非常重要的,它可以为你提供一个主方向。



我们可以发现 NavigationRailSwitchBottomNavigationBar 等组件一样,虽然自身是 StatefulWidget, 但对于激活状态的数据并不是在内部状态中维护,而是让 使用者主动提供,比如这里在构造 NavigationRail 时必须传入 selectedIndex 。 该组件只提供回调事件来通知使用者,这样的用意是让使用者更容易 控制 该状态,而不是完全封装在状态类内部。


另外,从 selectedIndex 属性在状态类中的使用中可以看出,每个菜单的条目组件通过 _RailDestination 进行构建。从这里可以看出,_RailDestination 会通过 selected 属性来区分是否激活,而且会通过 onTap 回调点击事件。在此触发 widget.onDestinationSelected ,将当前索引 i 传递给用户。



这里 _RailDestinationStatelessWidget, 只说明并不需要维护内部状态的变化,组需要根据构造中的配置信息构建需要的组件即可。这就尽可能地简化了 _RailDestination 的构建逻辑,让其相对独立,专注地去做一件事。这就是组件分离的好处之一:既可以简化构建结构,增加可读性,又可以将相对独立的构建逻辑内聚在一起。我们完全可以在日常开发中对这样的分离进行借鉴和发挥。




另外这里比较值得借鉴的还有动画的处理,我看了一下目前桌面的一些应用,比如 微信飞书有道词典百度网盘AndroidStudio有道云笔记 ,这些导航栏在切换时都是没有动画的。如下所示,NavigationRail 对应的状态类中维护了两种动画控制器,这也是 NavigationRail 为什么需要是 StatefulWidget 的原因。



其中 _destinationControllers 用于处理,菜单背景指示器在点击时激活/非激活的透明度渐变动画。可以追踪一下动画器的去向: 在 NavigationIndicator 中通过 FadeTransition使用动画器完成透明度渐变动画。


_RailDestination -->  _AddIndicator --> NavigationIndicator
复制代码




最后看一下 _extendedController 动画控制器,它对应的动画器也被传入 _RailDestination 中来完成动画功能。这个动画控制器在 extended 属性变化时,展开折叠导航栏的动画。如下源码所示,可以看出关于这个动画更多的细节。 动画过程中文字标签有个透明度渐变的动画,宽度约束通过对 ConstrainedBox 进行限制,并通过 AlignwidthFactor 控制文字标签区域的尺寸。



这里的 ClipRect 组件套的很迷,我试了一下去除后并不影响动画效果,一开始不知道为什么要加。之后将动画时长拉长,进行了一些测试发现端倪,如果不进行裁剪,就会出现如下的不和谐情况。默认动画 200 ms 看不出太大差异。从这里我又学到了一个小技巧:如何动画展开一个区域。



所以说源码是最好的老师,通过分析源码的实现去思考和学习,是成长的一条很好的途径。而不是什么东西都靠别人给你灌输,遇到不会的或犹豫不决时就到处问。Flutter 组件的源码相对独立,套路也比较简单,很适合去研究学习。《Flutter 组件集录》 专栏专门用于收录我对 Flutter 常用组件的使用介绍,其中一般也会有相关源码实现的一些分析。对一些能力稍弱的朋友,也可以根据这些介绍去尝试研究。那本文就到这里,谢谢观看 ~


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

公司产品太多了,怎么实现一次登录产品互通?

大家好,我是老王,最近开发新产品,然后老板说我们现在系统太多了,每次切换系统登录太麻烦了,能不能做个优化,同一账号互通掉。作为一个资深架构狮,老板的要求肯定要满足,安排! 一个公司产品矩阵比较丰富的时候,用户在不同系统之间来回切换,固然对产品用户体验上较差,...
继续阅读 »

大家好,我是老王,最近开发新产品,然后老板说我们现在系统太多了,每次切换系统登录太麻烦了,能不能做个优化,同一账号互通掉。作为一个资深架构狮,老板的要求肯定要满足,安排!


image.png


一个公司产品矩阵比较丰富的时候,用户在不同系统之间来回切换,固然对产品用户体验上较差,并且增加用户密码管理成本。也没有很好地利用内部流量进行用户打通,并且每个产品的独立体系会导致产品安全度下降。因此实现集团产品的单点登录对用户使用体验以及效率提升有很大的帮助。那么如何实现统一认证呢?我们先了解一下传统的身份验证方式。


1 传统Session机制及身份认证方案


1.1 Cookie与服务器的交互


image.png


众所周知,http是无状态的协议,因此客户每次通过浏览器访问web

页面,请求到服务端时,服务器都会新建线程,打开新的会话,而且服务器也不会自动维护客户的上下文信息。比如我们现在要实现一个电商内的购物车功能,要怎么才能知道哪些购物车请求对应的是来自同一个客户的请求呢?


image.png


因此出现了session这个概念,session 就是一种保存上下文信息的机制,他是面向用户的,每一个SessionID 对应着一个用户,并且保存在服务端中。session主要 以 cookie 或 URL 重写为基础的来实现的,默认使用 cookie 来实现,系统会创造一个名为JSESSIONID的变量输出到cookie中。


JSESSIONID 是存储于浏览器内存中的,并不是写到硬盘上的,如果我们把浏览器的cookie 禁止,则 web 服务器会采用 URL 重写的方式传递 Sessionid,我们就可以在地址栏看到 sessionid=KWJHUG6JJM65HS2K6 之类的字符串。


通常 JSESSIONID 是不能跨窗口使用的,当你新开了一个浏览器窗口进入相同页面时,系统会赋予你一个新的sessionid,这样我们信息共享的目的就达不到了。


1.2 服务器端的session的机制


当服务端收到客户端的请求时候,首先判断请求里是否包含了JSESSIONID的sessionId,如果存在说明已经创建过了,直接从内存中拿出来使用,如果查询不到,说明是无效的。


如果客户请求不包含sessionid,则为此客户创建一个session并且生成一个与此session相关联的sessionid,这个sessionid将在本次响应中返回给客户端保存。


对每次http请求,都经历以下步骤处理:


-服务端首先查找对应的cookie的值(sessionid)。

-根据sessionid,从服务器端session存储中获取对应id的session数据,进行返回。

-如果找不到sessionid,服务器端就创建session,生成sessionid对应的cookie,写入到响应头中。


session是由服务端生成的,并且以散列表的形式保存在内存中


1.3 基于 session 的身份认证流程


基于seesion的身份认证主要流程如下:


image.png


因为 http 请求是无状态请求,所以在 Web 领域,大部分都是通过这种方式解决。但是这么做有什么问题呢?我们接着看


2 集群环境下的 Session 困境及解决方案


image.png


随着技术的发展,用户流量增大,单个服务器已经不能满足系统的需要了,分布式架构开始流行。通常都会把系统部署在多台服务器上,通过负载均衡把请求分发到其中的一台服务器上,这样很可能同一个用户的请求被分发到不同的服务器上,因为 session 是保存在服务器上的,那么很有可能第一次请求访问的 A 服务器,创建了 session,但是第二次访问到了 B 服务器,这时就会出现取不到 session 的情况。


我们知道,Session 一般是用来存会话全局的用户信息(不仅仅是登陆方面的问题),用来简化/加速后续的业务请求。

传统的 session 由服务器端生成并存储,当应用进行分布式集群部署的时候,如何保证不同服务器上 session 信息能够共享呢?


2.1 Session共享方案


Session共享一般有两种思路



  • session复制

  • session集中存储


2.1.1 session复制


session复制即将不同服务器上 session 数据进行复制,用户登录,修改,注销时,将session信息同时也复制到其他机器上面去

image.png


这种实现的问题就是实现成本高,维护难度大,并且会存在延迟登问题。


2.1.2 session集中存储


image.png


集中存储就是将获取session单独放在一个服务中进行存储,所有获取session的统一来这个服务中去取。这样就避免了同步和维护多套session的问题。一般我们都是使用redis进行集中式存储session。


3 多服务下的登陆困境及SSO方案


3.1 SSO的产生背景


image.png


如果企业做大了之后,一般都有很多的业务支持系统为其提供相应的管理和 IT 服务,按照传统的验证方式访问多系统,每个单独的系统都会有自己的安全体系和身份认证系统。进入每个系统都需要进行登录,获取session,再通过session访问对应系统资源。这样的局面不仅给管理上带来了很大的困难,对客户来说也极不友好,那么如何让客户只需登陆一次,就可以进入多个系统,而不需要重新登录呢?


image.png


“单点登录”就是专为解决此类问题的。 其大致思想流程如下:通过一个 ticket 进行串接各系统间的用户信息


3.2 SSO的底层原理 CAS


3.2.1 CAS实现单点登录流程


我们知道对于完全不同域名的系统,cookie 是无法跨域名共享的,因此 sessionId 在页面端也无法共享,因此需要实现单店登录,就需要启用一个专门用来登录的域名如(ouath.com)来提供所有系统的sessionId。当业务系统被打开时,借助中心授权系统进行登录,整体流程如下:


1.当b.com打开时,发现自己未登陆,于是跳转到ouath.com去登陆

2. ouath.com登陆页面被打开,用户输入帐户/密码登陆成功

3. ouath.com登陆成功,种 cookie 到ouath.com域名下

4. 把 sessionid 放入后台redis,存放<ticket,sesssionid>数据结构,然后页面重定向到A系统

5.当b.com重新被打开,发现仍然是未登陆,但是有了一个 ticket值

6. 当b.com用ticket 值,到 redis 里查到 sessionid,并做 session 同步,然后种cookie给自己,页面原地重定向

7. 当b.com打开自己页面,此时有了 cookie,后台校验登陆状态,成功


整个交互流程图如下:


image.png


3.2.2 单点登录流程演示


3.2.2.1 CAS登录服务demo核心代码


1.用户实体类



public class UserForm implements Serializable{
private static final long serialVersionUID = 1L;

private String username;
private String password;
private String backurl;

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getBackurl() {
return backurl;
}

public void setBackurl(String backurl) {
this.backurl = backurl;
}

}

2.登录控制器


@Controller
public class IndexController {
@Autowired
private RedisTemplate redisTemplate;

@GetMapping("/toLogin")
public String toLogin(Model model,HttpServletRequest request) {
Object userInfo = request.getSession().getAttribute(LoginFilter.USER_INFO);
//不为空,则是已登陆状态
if (null != userInfo){
String ticket = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(ticket,userInfo,2, TimeUnit.SECONDS);
return "redirect:"+request.getParameter("url")+"?ticket="+ticket;
}
UserForm user = new UserForm();
user.setUsername("laowang");
user.setPassword("laowang");
user.setBackurl(request.getParameter("url"));
model.addAttribute("user", user);

return "login";
}

@PostMapping("/login")
public void login(@ModelAttribute UserForm user,HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException {
System.out.println("backurl:"+user.getBackurl());
request.getSession().setAttribute(LoginFilter.USER_INFO,user);

//登陆成功,创建用户信息票据
String ticket = UUID.randomUUID().toString();
redisTemplate.opsForValue().set(ticket,user,20, TimeUnit.SECONDS);
//重定向,回原url ---a.com
if (null == user.getBackurl() || user.getBackurl().length()==0){
response.sendRedirect("/index");
} else {
response.sendRedirect(user.getBackurl()+"?ticket="+ticket);
}
}

@GetMapping("/index")
public ModelAndView index(HttpServletRequest request) {
ModelAndView modelAndView = new ModelAndView();
Object user = request.getSession().getAttribute(LoginFilter.USER_INFO);
UserForm userInfo = (UserForm) user;
modelAndView.setViewName("index");
modelAndView.addObject("user", userInfo);
request.getSession().setAttribute("test","123");
return modelAndView;
}
}

3.登录过滤器


public class LoginFilter implements Filter {
public static final String USER_INFO = "user";
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;

Object userInfo = request.getSession().getAttribute(USER_INFO);;

//如果未登陆,则拒绝请求,转向登陆页面
String requestUrl = request.getServletPath();
if (!"/toLogin".equals(requestUrl)//不是登陆页面
&amp;&amp; !requestUrl.startsWith("/login")//不是去登陆
&amp;&amp; null == userInfo) {//不是登陆状态

request.getRequestDispatcher("/toLogin").forward(request,response);
return ;
}

filterChain.doFilter(request,servletResponse);
}

@Override
public void destroy() {

}
}

4.配置过滤器


@Configuration
public class LoginConfig {

//配置filter生效
@Bean
public FilterRegistrationBean sessionFilterRegistration() {

FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new LoginFilter());
registration.addUrlPatterns("/*");
registration.addInitParameter("paramName", "paramValue");
registration.setName("sessionFilter");
registration.setOrder(1);
return registration;
}
}

5.登录页面


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>enjoy login</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div text-align="center">
<h1>请登陆</h1>
<form action="#" th:action="@{/login}" th:object="${user}" method="post">
<p>用户名: <input type="text" th:field="*{username}" /></p>
<p>密 码: <input type="text" th:field="*{password}" /></p>
<p><input type="submit" value="Submit" /> <input type="reset" value="Reset" /></p>
<input type="text" th:field="*{backurl}" hidden="hidden" />
</form>
</div>


</body>
</html>

3.2.2.2 web系统demo核心代码


1.过滤器


public class SSOFilter implements Filter {
private RedisTemplate redisTemplate;

public static final String USER_INFO = "user";

public SSOFilter(RedisTemplate redisTemplate){
this.redisTemplate = redisTemplate;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {

}

@Override
public void doFilter(ServletRequest servletRequest,
ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse)servletResponse;

Object userInfo = request.getSession().getAttribute(USER_INFO);;

//如果未登陆,则拒绝请求,转向登陆页面
String requestUrl = request.getServletPath();
if (!"/toLogin".equals(requestUrl)//不是登陆页面
&amp;&amp; !requestUrl.startsWith("/login")//不是去登陆
&amp;&amp; null == userInfo) {//不是登陆状态

String ticket = request.getParameter("ticket");
//有票据,则使用票据去尝试拿取用户信息
if (null != ticket){
userInfo = redisTemplate.opsForValue().get(ticket);
}
//无法得到用户信息,则去登陆页面
if (null == userInfo){
response.sendRedirect("http://127.0.0.1:8080/toLogin?url="+request.getRequestURL().toString());
return ;
}

/**
* 将用户信息,加载进session中
*/
UserForm user = (UserForm) userInfo;
request.getSession().setAttribute(SSOFilter.USER_INFO,user);
redisTemplate.delete(ticket);
}

filterChain.doFilter(request,servletResponse);
}

@Override
public void destroy() {

}
}

2.控制器


@Controller
public class IndexController {
@Autowired
private RedisTemplate redisTemplate;

@GetMapping("/index")
public ModelAndView index(HttpServletRequest request) {
ModelAndView modelAndView = new ModelAndView();
Object userInfo = request.getSession().getAttribute(SSOFilter.USER_INFO);
UserForm user = (UserForm) userInfo;
modelAndView.setViewName("index");
modelAndView.addObject("user", user);

request.getSession().setAttribute("test","123");
return modelAndView;
}
}

3.首页


<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>enjoy index</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<div th:object="${user}">
<h1>cas-website:欢迎你"></h1>
</div>
</body>
</html>

3.2.3 CAS的单点登录和OAuth2的区别


OAuth2:三方授权协议,允许用户在不提供账号密码的情况下,通过信任的应用进行授权,使其客户端可以访问权限范围内的资源。


CAS :中央认证服务(Central Authentication Service),一个基于Kerberos票据方式实现SSO单点登录的框架,为Web 应用系统提供一种可靠的单点登录解决方法(属于 Web SSO )。



  1. CAS的单点登录时保障客户端的用户资源的安全 ;OAuth2则是保障服务端的用户资源的安全 。

  2. CAS客户端要获取的最终信息是,这个用户到底有没有权限访问我(CAS客户端)的资源;OAuth2获取的最终信息是,我(oauth2服务提供方)的用户的资源到底能不能让你(oauth2的客户端)访问。


因此,需要统一的账号密码进行身份认证,用CAS;需要授权第三方服务使用我方资源,使用OAuth2;


好了,不知道大家对SSO是否有了更深刻的理解,大家有问题可以私信我。我是王老狮,一个有想法有内涵的工程狮,关注我,学习更多技术知识。


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

有哪些话一听就知道一个程序员是个水货?

要判断一个程序员是水货是非常难的。反过来说,要面试出一个程序员的真实水平是比较困难的。现在很多小公司面试,动不动就是手写代码。从框架问到具体的技术实现细节,结果错过了好多真正有能力的程序员。反而,大公司的面试就正常多了,也对,大公司已经通过简历筛选,把不入流的...
继续阅读 »

要判断一个程序员是水货是非常难的。反过来说,要面试出一个程序员的真实水平是比较困难的。

现在很多小公司面试,动不动就是手写代码。从框架问到具体的技术实现细节,结果错过了好多真正有能力的程序员。反而,大公司的面试就正常多了,也对,大公司已经通过简历筛选,把不入流的大学筛出去了。

实际上,我认为程序员面试应该只关注两方面的能力。代码能力和工程能力。二者有其一即可。

代码能力。这个不用多说,程序员招进去就是写代码的,所以很多公司会有上机或者直接让手写代码这一招,题目不用多难,就是基础的链表、数组、树就能解决的算法,给支笔能写个90%出来,招进去,这名同学便不会太差。所以各大公司都很欢迎ACM选手,这部分人受过严格的训练,再差也不会差到哪儿去。

工程能力。只要问清楚做了什么?为什么要这样做?还有没有更好的办法?这几个问题基本上就能判断出这个人的项目经历的真假。如果项目经历为真,对于项目实现有自己的思考,招进去也不会太差。如果用到了自己熟悉的框架,可以抓住细节再聊一下。

上述说的这两个能力其实都不太容易造假。反而,如果招安卓app开发,抓住相关的知识点,一顿猛问,太容易被培训班之流靠背题糊弄过去。也就是说,面试的时候要抓住一些编程需要的通用能力,通用技能扎实的同学,给点时间,上手其他的也很快。

下面,歪个题。说下我的两次失败的面试经历,两次面的都是头部互联网大厂。

先说A大厂。下文直接进入面试环节。

面试官:你对C++比较熟悉,对吧?

我:是的。

面试官:那请问子类与父类之间如何实现多态。

我:用一张表来实现。

面试官很疑惑的样子:什么表!?

我:这张表来保存函数的入口地址,子类如果重写了某个函数,那么同名函数的入口地址就用子类的函数地址来替换,这样。

面试官打断了我:你听说个虚函数吗?

我:听说过。

面试官:好了,我没有问题了。你回去等通知吧。

这位大厂的面试官铁定认为我是一个水货,连C++的基本概念都掌握不清楚,就是来浑水摸鱼的。实际上,按照我的理解,大厂面试官不会问这种C++的基本概念,应该问的是虚函数如何实现等比较深的背景,那段时间我刚好看完了C++之父写的《C++语言的设计和演化
》,凭着理解说了一通,但是面试官认为我在瞎说,妥妥的水货。实际上,我当时可能说成虚函数表就没有问题了。当然,我也没有等到通知,从酒店下楼出来喝了杯奶茶以后,就发现“面试未通过”。

再说下B大厂,该大厂我笔试获得了非常高的分数。下文直接讲面试环节。

面试官:讲一讲你这个项目中类是怎么划分的。

我:我用的是MFC,这个框架用了MVC模型。所以我对于需要展示处理效果的代码放在了xxxview类里面,讲计算相关的代码放到了xxxmodel类里面了。

面试官一头雾水的样子:也就是说用别人的框架,你就加点处理代码就行了。

我:差不多是这样。

然后,我又回去等通知了。实际上,这个项目完完全全是由我自己独立完成,并且前后重写了两遍,中间的思考和重构是非常值得说的。如果我当时的回答变一下,先讲一下项目的背景,具体需要实现的功能,功能之间如何解耦
,使用了xx设计模式来提供扩展和需求变化。按照这个思路讲一下去,肯定会得到一个比较好的面试评价。

上述两个经历,就是我第一、第二次面试大厂出的岔子。本质上,还是自己没明白别人在问什么,第一个问题以为别人问得很深,实际上很浅。第二个问题,以为别人随便问问,实际上是在判断这个项目是否造假。所以,看一下别人的面经还是很有用的,也不会白白浪费机会。

再后来,我靠上机满分顺利去了H厂,连续多次拿了绩效A。工作中也遇到了好些个水货程序员,有那种简历上SCI一作,入职后写个for循环用了一天的。也有那种一千行代码30+bug的,还有那种自己代码出bug,看着屏幕只会干瞪眼的。但是这些人的工资都比我高。

来源:blog.csdn.net/qq_66238169/article/details/124886138

收起阅读 »

社区纠纷不断:程序员何苦为难程序员

今年年初,我们报道“因为被多人侮辱大吼,Swift 之父正式退出 Swift 核心团队”。诸如此类的“语言暴力”、“网络暴力”事件在开源社区乃至整个 IT 社区屡见不鲜。多个技术社区,都出现过创始人、重要维护者、贡献者因为感觉“社区氛围糟糕”、“受到伤害”而宣...
继续阅读 »

今年年初,我们报道“因为被多人侮辱大吼,Swift 之父正式退出 Swift 核心团队”。

诸如此类的“语言暴力”、“网络暴力”事件在开源社区乃至整个 IT 社区屡见不鲜。多个技术社区,都出现过创始人、重要维护者、贡献者因为感觉“社区氛围糟糕”、“受到伤害”而宣布退出的现象。更有甚者,还有科技公司领导被爆出叫嚣着让 80 后退出 IT 圈。后者可粗略视为是该领导过于偏激,且是少数案例,先不做过多讨论。但是在开源圈,怎么就这样了呢?

为了回答这个问题,本文梳理了以往的一些社区纠纷事件,发现许多矛盾发生在社区实际的管理者/层,与社区贡献者、参与者之间,双方的不满大致也可以归总成几类原因。


众口难调

从大教堂的开发模式转向集市开发模式之后,技术社区存在的目的便是为了聚众人之力,让项目更好。在集市模式下,开源社区给了个人极大的自由度,所有贡献者都可以畅所欲言,发表自己的想法,为项目作出贡献。随之而来的问题,便是如何协调所有人的意见。

社区的管理模也在一定程度上决定了社区日后的争吵风险有多大。当下的开源社区管理主要分成四种模式。一是由社区主导,采用自由贡献模式,“共识”是其前提,功能开发和版本发布等重要决策以社区共识为准。二是公司主导,由公司资助社区及软件的发展,相对来说,公司会拥有更多的实权。三是 BDFL 仁慈的独裁者模式,这是在社区模式的基础之上,社区中有一个“独裁者”的角色存在,他对一些重要决策有最终决定权,而非依赖社区共识。四是精英治理,在社区中表现突出、贡献最大的人被任命为管理团队,决策基于投票确定。

我们常见的纠纷事件,往往可以归总为“掌权者”和“贡献者”之争,这种争议更容易出现在“仁慈的独裁者”模式的开源社区中。其他三种模式中,许多纠纷的出现也是因为实际管理团队未扮演好自己的角色。

掌权者的不满

闻名世界的大型开源项目往往是某个“天才程序员”的作品,早期的开源社区一般都是“仁慈的独裁者”模式,独裁者饱受敬仰,比如 Linux 社区的 Linus、Ubuntu 社区、Python 社区。然而,天才似乎更乐于单兵作战。

  • 对贡献不满

暴躁大佬 Linus 在评价那些没达到他个人标准的代码方面非常毒舌。曾有网友用了此前 Linus 在邮件列表中公开的一段回复,直指 Linus 在人际沟通中态度恶劣:“这也算是一个 BUG?你已经成为内核维护者多长时间了?还没有学会内核维护的第一条规则?我再也不想收到这种明显的垃圾,像白痴一样的提交…… ”

对于一直看不爽的 Intel,Linus 对其提交的代码也是口吐芬芳。2018 年初,为了修补 Spectre 漏洞,Intel 工程师提供了一个间接分支限制推测(indirect branch restricted speculation, IBRS)功能的补丁。Linus 当时就在邮件列表中公开指出 IBRS 会造成系统性能大幅降低,直言该补丁“就是彻彻底底的垃圾”。


不仅仅是 Linus,在崇尚“精英主义”的 BSD 社区,也存在明显的“鄙视链”。BSD 的第一版撰写者 Bill Joy 不愿意相信庞大的志愿贡献者者们,他曾说:“大多数人都是糟糕的程序员,让很多人盯着代码不会真正发现错误。真正的错误是由几个非常聪明的人发现的。大多数人看代码不会看到任何东西......不能期望成千上万的人做出贡献并都达到高标准。”

这种看法一直存在于 BSD 之后的发展中,FreeBSD 深度参与者 Marshall Kirk McKusick 就曾表示,90% 的 committers 所贡献的代码都不能用,还剩下的一小部分也需要被打磨。

  • 遭遇信任危机

在 Python 社区,很多年内,Python 之父 Guido van Rossum 都是最有威信的那个人,他也被社区授予“终身仁慈的独裁者”的称谓。但在 2018 年,当 Python 社区探讨改进提案时,Guido 发现“现在有这么多人鄙视我的决定”。

因此,他不想再为 PEP(Python 改进提案)[ PEP 572 ] (https://www.python.org/dev/peps/pep-0572/)争取什么,并决定自己将逐步脱离决策层,不再领导 Python 的发展,休息一段时间后将作为普通的核心开发者参与社区。


有时,“仁慈的独裁者”也会因为“不作为”被指责。Ubuntu 创始人 Mark Shuttleworth 在社区中被戏称为“自封的仁慈独裁者”。事实上,Ubuntu 社区早期也确实是“仁慈的独裁者”管理模式。

然而,在 Ubuntu 社区蓬勃发展,各项业务步入正轨之际,Mark Shuttleworth 本人的工作逐渐与开发者拉开距离,Jono Bacon 等一些早期在 Ubuntu 社区中颇具名望的核心成员相继离开,Mark Shuttleworth 也很少在社区活跃。逐渐,有一些资深的 Ubuntu 开发者认为社区正面临“群龙无首”的尴尬局面,指责沙特尔沃思没有尽到“BDFL”的责任。一位前 Ubuntu 开发人员在 Ubuntu 社区中留言指责 Mark Shuttleworth 作为 BDFL “放弃了社区,对社区治理的崩溃保持沉默”,令人失望。

Mark Shuttleworth 面对职责也欣然接受,承认自己在这些方面确实做得不够好,并表达了“挫败感”。

  • 没有回馈

开源项目最容易让人不满的点还当属“白嫖”,许多开源项目都是靠作者用爱发电,社区的汲取大于回馈,从而导致软件作者身心俱疲。

前段时间轰轰烈烈的 Apache Log4j2 高危漏洞事件,攻击者可以利用其 JNDI 注入漏洞远程执行代码,影响了众多项目和公司。此事也让 Log4j2 的作者受到关注与批评,Log4j2 的维护者之一 @Volkan Yazıcı 在推特上吐槽:Log4j2 维护者只有几个人,他们无偿、自愿地工作,没有人发工资,也没人提交代码修复问题,出了问题还要被一堆人在仓库里留言痛骂。

此前,PhantomJS 的核心开发者之一 Vitaly Slobodin 宣布辞任 maintainer ,不再维护项目,原因也是一个人发电太累:“我看不到 PhantomJS 的未来,作为一个单独的开发者去开发 PhantomJS 2 和 2.5 ,简直就像是一个血腥的地狱。即便是最近发布的 2.5 Beta 版本拥有全新、亮眼的 QtWebKit ,但我依然无法做到真正的支持 3 个平台。我们没有得到其他力量的支持!”

贡献者的不满

当社区随着项目生命周期的发展逐渐发生变化时,流程和规定上的改变不可避免,贡献者间交流的氛围也在不断变化中。贡献者对社区的不满往往是从社区发生变化开始……

  • 对项目或社区不再看好

一位 Debian 的包维护者 Stapelberg 在 2019 年写了篇长文宣布自己要退出 Debian 的开发流程,逐渐减少参与 Debian 的维护和相关活动。主要原因便在于,他发现 Debian 整个开发评估流程非常迟钝。

举例来说,补丁的评估没有截止日期,有时候他会收到通知说几年前递交的补丁现在合并了。Debian 的一些维护者出于个人喜好拒绝合作,维护者给予的个人自由度太高对 Debian 构成了严重影响。

在他对 Debian 的开发流程沮丧程度超过阈值之后,他便宣布退出,写了文章,希望能激励 Debian 作出改变。

在 LLVM 社区,2018 年一位名叫 Rafael Avila de Espindola 的资深开发者(LLVM 编译器贡献排名第五)发邮件宣布决定与该项目分道扬镳。 原因在于近几年的感受发生了变化,LLVM 日益庞大且变化缓慢,他也不赞成 LLVM 最近引入的社区行为规范。真正促使他做决定的是 LLVM 与一个公开根据性别和血统进行歧视的组织 Outreachy 进行合作,这让他感到非常不满。

除此之外,一些由公司主导的开源项目也会因为改变发展战略招致不满。

Qt 公司从 2021 年 1 月开始,将 Qt 5.15 作为仅供商业化的 LTS,现有的 Qt 5.15 分支将公开可见,但不会看到任何新补丁,只有付费账户才可以使用长期支持版本的 Qt 5.15 。

此举引起了社区的强烈不满,基于 Qt 开发的 KDE 社区的担忧。有用户在 Qt 官方公告下留言讽刺道:“所以,基本上您是在告诉所有忠实的开源用户,现在他们将仅被视为商业客户的 beta 测试者,并且作为奖励,他们将只能下载非 LTS 版本。你们真是太亲切了!”一位长期的 Qt 贡献者,来自英特尔公司的开发者 Thiago Macieira 表示,至少对于他在 Qt 中处理过的代码,他不会再参与修复、评论和审查后端错误报告。

  • 反独裁

与每个社区都有一个人或者几个人的实际管理团队向对应的是,在管理不当时,贡献者会起身反抗“管理”。

几个月前,Rust 审核团队 (Moderation Team) 昨日公告称,他们已集体辞职且即刻生效。团队成员 Andrew Gallant 表示此举是为了抗议 Rust 核心团队 (Core Team) 不对除自己以外的任何人负责。

Rust 的管理者分为很多个团队,比如核心团队、审核团队、发行团队、库管理团队等等...其中,Rust 核心团队的权限最大,而且其他团队无法影响他们。Andrew Gallant 在公告中写道,由于核心团队在组织结构层面的不负责任,他们一直无法按照社区对审核团队的期望和他们自己坚持的标准来执行 Rust 行为准则。对于在这种情况下选择离开,他们表达了对大家的歉意。但从治理 Rust 的角度来看,他们别无选择。因此 Rust 审核团队认为除了辞职和发表这份声明之外,已经没有其他办法了。


如何让众人满意?

矛盾激化的后果只有一个:成员慢慢离开。如果社区和项目不做出改变,则很有可能导致项目走向衰落。既然长时间充斥着争吵与不满的社区难以持久,那么,该如何构建一个让更多人愉快参与的社区?

首先,无论何种治理模式下的社区,个人文明表达观点,遵守社区行为准则都是减少冲突的必要条件。但是,个人行为是非常不稳定的因素,就像 Linus 曾说要改改自己的暴脾气,却每每被各种言论刺激,重回暴躁大佬。

其次,便是通过治理框架有效地管理社区,多权分立、避免独裁,并且成员间相互约束,基于“共识”发展。具体来说便是有一份好的“手册(原则、准则等)”,以及一个让人信服、能公证管理的团队。

比如在 Apache 软件基金会中,便采用精英治理的模式。Apache Way 中对于决策、监督、执行等各方面都有明确规定:包括结构扁平,无论职位如何,各个角色间是平等的,投票权重相同,不允许有仁慈的独裁者出现;单个项目由自选的活跃志愿者团队监督,倾向在达成共识的前提下决定项目发展,不能完全建立共识则用投票等方式做出决策;整个基金会的治理模型基于信任和委托监督……

在开放原子开源软件基金会中,项目的毕业标准考核中也有类似的关于社区需符合“贤能治理”的规定:

OA-CO-40

【英】The community strives to be meritocratic and over time aims to give more rights and responsibilities to contributors who add value to the project.

【中】社区要符合贤能治理的精神,随着时间的推移,为项目增值的贡献者会被赋予更多的权利和责任

TOC 成员徐亮曾介绍,贤能治理这四个字是经过很多轮讨论确定下来的。在英文语境中,这个词是 meritocracy,并不完全是一个褒义词,“我们并不觉得用 meritocracy 形容社区是完全正确的事情,但是我们又觉得在开源社区中,所谓的能者上氛围是比较积极向上的,是社区能够健康运转的主要因素。”

在许多项目中,一方面大家是贡献者,另一方面,每个人提交的功能越重要,或者对项目本身做出了更有意义的贡献,就更有可能被现任维护人员选中去承担更重要的角色。通过这种方式达成的精英治理或是贤能治理,往往是希望能够让项目可以自己成长,更好地走下去。

Ubuntu 的创始人 Mark Shuttleworth 在遭到信任危机之后,便着手参与将 Ubuntu 转向经营治理的模式。目前,Ubuntu 社区也重新组建了由 3 名核心成员组成的管理团队,分别是 Ubuntu 社区代表 Monica Ayhens-Madon,Ubuntu 开发者关系负责人 Rhys Davies,以及临时社区经理 Ken VanDine。Ken 还将负责继续为 Ubuntu 社区寻找一位合适的社区总监。

最后,借用一个当下共识:开源比以往任何时候都更加重要。也因此,维护一个好的社区氛围,正尤为重要。

来源:OSC开源社区

收起阅读 »

面试题 | 等待多个并发结果有哪几种方法?

引子 App 开发中,等待多个异步结果的场景很多见, 比如并发地在后台执行若干个运算,待所有运算执行完毕后归总结果。 比如并发地请求若干个接口,待所有结果返回后刷新界面。 比如统计相册页并发加载 20 张图片的耗时。 其实把若干异步任务串行化是最简单的解决办法...
继续阅读 »

引子


App 开发中,等待多个异步结果的场景很多见,


比如并发地在后台执行若干个运算,待所有运算执行完毕后归总结果。


比如并发地请求若干个接口,待所有结果返回后刷新界面。


比如统计相册页并发加载 20 张图片的耗时。


其实把若干异步任务串行化是最简单的解决办法,即前一个异步任务执行完毕后再执行下一个。但这样就无法利用多核性能,执行时间被拉长,此时的执行总时长 = 所有任务执行时长的和。


若允许任务并发,则执行总时长 = 执行时间最长任务的耗时。时间性能得以优化,但随之而来的一个复杂度是:“如何等待多个异步结果”。


本文会介绍几种解决方案,并将它们运用到不同的业务场景,比对一下哪个方案适用于哪个场景。


等待并发网络请求


布尔值


假设有如下两个网络请求:


// 拉取新闻
fun fetchNews() {
newsApi.fetchNews().enqueue(object : Callback<List<News>> {
override fun onFailure(call: Call<List<News>>, t: Throwable) { ... }
override fun onResponse(call: Call<List<News>>, response: Response<List<News>>) { ... }
})
}
// 拉取广告
fun fetchAd() {
newsApi.fetchAd().enqueue(object : Callback<List<Ad>> {
override fun onFailure(call: Call<List<Ad>>, t: Throwable) { ... }
override fun onResponse(call: Call<List<Ad>>, response: Response<List<Ad>>) { ... }
})
}

广告需要按一定规则插入到新闻列表中。


最简单的做法是,先请求新闻,待其返回后再请求广告。显然这会增加用户等待时间。而且会写出这样的代码:


// 拉取新闻
fun fetchNews() {
newsApi.fetchNews().enqueue(object : Callback<News> {
override fun onFailure(call: Call<News>, t: Throwable) { ... }
override fun onResponse(call: Call<News>, response: Response<News>) {
// 拉取广告
newsApi.fetchAd().enqueue(object : Callback<Ad> {
override fun onFailure(call: Call<Ad>, t: Throwable) { ... }
override fun onResponse(call: Call<Ad>, response: Response<Ad>) { ... }
})
}
})
}

嵌套回调,若再加一个接口,回调层次就会再加一层,不能忍。
用户和程序员的体验都不好,得想办法解决。


第一个想到的方案是布尔值:


var isNewsDone = false
var isAdDone = false
var news = emptyList()
var ads = emptyList()
// 拉取新闻
fun fetchNews() {
newsApi.fetchNews().enqueue(object : Callback<List<News>> {
override fun onFailure(call: Call<List<News>>, t: Throwable) {
isNewsDone = true
tryRefresh(news, ad)
}
override fun onResponse(call: Call<List<News>>, response: Response<List<News>>) {
isNewsDone = true
news = response.body().result
tryRefresh(news, ad)
}
})
}
// 拉取广告
fun fetchAd() {
newsApi.fetchAd().enqueue(object : Callback<List<Ad>> {
override fun onFailure(call: Call<List<Ad>>, t: Throwable) {
isAdDone = true
tryRefresh(news, ad)
}
override fun onResponse(call: Call<List<Ad>>, response: Response<List<Ad>>) {
isAdDone = true
ads = response.body().result
tryRefresh(news, ad)
}
})
}
// 尝试刷新界面(只有当两个请求都返回时才刷新)
fun tryRefresh(news: List<News>, ads: List<Ad>) {
if(isNewsDone && isAdDone){ //刷新界面 }
}

设置两个布尔值分别对应两个请求是否返回,并且在每个请求返回时检测两个布尔值,若都为 true 则进行刷新界面。


网络库通常会将请求成功的回调抛到主线程执行,所以这里没有线程安全问题。但如果不是网络请求,而是后台任务,此时需要将布尔值声明为volatile保证其可见性,关于 volatile 更详细的解释可以点击面试题 | 徒手写一个非阻塞线程安全队列 ConcurrentLinkedQueue?


这个方案能解决问题,但只适用于并发请求数量很少的请求,因为每个请求都要声明一个布尔值。而且每增加一个请求都要修改其余请求的代码,可维护性差。


CountdownLatch


更好的方案是CountDownLatch,它是java.util.concurrent包下的一个类,用来等待多个异步结果,用法如下:


val countdownLatch = CountDownLatch(2)//初始化,等待2个异步结果
var news = emptyList()
var ads = emptyList()
// 拉取新闻
fun fetchNews() {
newsApi.fetchNews().enqueue(object : Callback<List<News>> {
override fun onFailure(call: Call<List<News>>, t: Throwable) {
countdownLatch.countDown()
}
override fun onResponse(call: Call<List<News>>, response: Response<List<News>>) {
news = response.body().result
countdownLatch.countDown()
}
})
}
// 拉取广告
fun fetchAd() {
newsApi.fetchAd().enqueue(object : Callback<List<Ad>> {
override fun onFailure(call: Call<List<Ad>>, t: Throwable) {
countdownLatch.countDown()
}
override fun onResponse(call: Call<List<Ad>>, response: Response<List<Ad>>) {
ads = response.body().result
countdownLatch.countDown()
}
})
}
// countdownLatch 在新线程中等待
thread {
countdownLatch.await() // 阻塞线程等待两个请求返回
liveData.postValue() // 抛数据到主线程刷刷新界面
}.start()

CountDownLatch 在构造时需传入一个数量,它的语义可以理解为一个计数器。countDown() 将计数器减一,而 await() 会阻塞当前线程直到计数器为 0 才被唤醒。


该计数器是一个 int 值,可能被多线程访问,为了保证线程安全,它被声明为 volatile,并且 countDown() 通过 CAS + 自旋的方式将其减一。


关于 CAS 的介绍可以点击面试题 | 徒手写一个非阻塞线程安全队列 ConcurrentLinkedQueue?


若新增一个接口,只需要将计数器的值加一,并在新接口返回时调用 countDown() 即可,可维护性陡增。


协程


Kotlin 是降低复杂度的大师,它对于这个问题的解决方案可以让代码看上去更简单。


在 Kotlin 的世界里异步操作应该被定义为suspend方法,retrofit 就支持这样的操作,比如:


interface NewsApi {
@GET("/xxx")
suspend fun fetchNews(): List<News>
@GET("/xxx")
suspend fun fetchAd(): List<Ad>
}

然后在协程中使用async启动异步任务:


scope.launch {
// 并发地请求网络
val newsDefered = async { fetchNews() }
val adDefered = async { fetchAd() }
// 等待两个网络请求返回
val news = newsDefered.await()
val ads = adDefered.await()
// 刷新界面
refreshUi(news, ads)
}

不管是写起来还是读起来,体验都非常好。因为协程把回调干掉了,逻辑不会跳来跳去。


其中的async()是 CoroutineScope 的扩展方法:


// 启动协程,并返回协程执行结果
public fun <T> CoroutineScope.async(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
): Deferred<T> {
...
}

async() 和 launch() 唯一的不同是它的返回值是Defered,用于描述协程体执行的结果:


public interface Deferred<out T> : Job {
// 挂起方法: 等待值的计算,但不会阻塞当前线程,计算完成后恢复当前协程执行
public suspend fun await(): T
}

调用async()启动子协程不会挂起外层协程,而是立即返回一个Deferred对象,直到调用Deferred.await()协程的执行才会被挂起。当协程在多个Deferred对象上被挂起时,只有当它们都恢复后,协程才继续执行。这样就实现了“等待多个并行的异步结果”。


但这样写会问题:当广告拉取抛出异常时,新闻拉取也会被取消。


这是协程的一个默认设定,叫结构化并发,即并发是有结构性的。


Java 中线程的并发是没有结构的,所以做如下事情很困难:



  1. 结束一个线程时,如何一并结束它所有的子线程?

  2. 当某个子线程抛出异常时,如何结束和它同一层级的兄弟线程?

  3. 父线程如何等待所有子线程结束之后才结束?


之所以会很困难,是因为 Java 中的线程是没有级联关系的。而 Kotlin 通过协程域 CoroutineScope 以及协程上下文 CoroutineContext 实现级联关系。


在协程中启动的子协程会继承父协程的协程上下文,除了其中的 Job,一个新的 Job 会被创建并归属于父协程的子 Job。通过这套机制,协程和子协程之间有了级联关系,就能实现结构化并发。(以后会就结构化并发写一个系列,敬请期待~)


关于 CoroutineContext 内部结构的详细剖析可以点击Kotlin 协程 | CoroutineContext 为什么要设计成 indexed set?


但有些业务场景不需要子任务之间相互关联,比如当前场景,广告加载失败不应该影响新闻的拉取,大不了不展示广告。为此 kotlin 提供了supervisorScope


scope.launch {
supervisorScope {
// 并发地请求网络
val newsDefered = async { fetchNews() }
val adDefered = async { fetchAd() }
// 等待两个网络请求返回
val news = newsDefered.await()
val ads = adDefered.await()
// 刷新界面
refreshUi(news, ads)
}
}

supervisorScope 新建一个协程域继承父亲的协程上下文,但会将其中的 Job 重写为SupervisorJob,它的特点就是孩子的失败不会影响父亲,也不会影响兄弟。


现在广告和新闻加载互不影响,各自抛异常都不会影响对方。但就目前的业务场景来说,理想情况是这样的:“广告加载失败不应该影响新闻的加载。但新闻加载失败应该取消广告的加载(因为此时广告也没有展示的机会)”


稍改动下代码:


scope.launch {
supervisorScope {
// 并发地请求网络
val adDefered = async { fetchAd() }
val newsDefered = async { fetchNews() }
// 当新闻请求抛异常时,取消广告请求
newsDefered.invokeOnCompletion { throwable ->
throwable?.let { adDefered.cancel() }
}
// 等待新闻
val news = try {
newsDefered.await()
} catch (e: Exception) {
emptyList()
}
// 等待广告
val ads = try {
adDefered.await()
} catch (e: Exception) {
emptyList()
}
// 刷新界面
refreshUi(news, ads)
}
}

invokeOnCompletion()相当于注册了一个回调,在异步任务结束时调用,不管是正常结束还是因异常而结束。在该回调中判断,若新闻因异常而结束则取消广告任务。


因为新闻和广告任务都可能抛出异常,且 async 启动的异步任务是在调用 await() 时才会抛出异常,所以它应该包裹在 try-catch 中。Kotlin 中的 try-catch 是一个表达式,即是有返回值的。这个特性让正常和异常情况的值聚合在一个表达式中。


若不使用 try-catch,程序也不会奔溃,因为 supervisorScope 中异常是不会向上传播的,即子协程的异常不会影响兄弟和父亲。但这样就少了异常情况的处理。


若现有代码都是 Callback 形式的,还能不能享受协程的简洁?


能!Kotlin 提供了suspendCoroutine(),专门用于将回调风格的代码转换成 suspend 方法,以拉取新闻为例:


// Callback 形式
fun fetchNews() {
newsApi.fetchNews().enqueue(object : Callback<List<News>> {
override fun onFailure(call: Call<List<News>>, t: Throwable) { ... }
override fun onResponse(call: Call<List<News>>, response: Response<List<News>>) { ... }
})
}

// suspend 形式
suspend fun fetchNews() = suspendCoroutine<List<News>> { continuation ->
newsApi.fetchNews().enqueue(object : Callback<List<News>> {
override fun onFailure(call: Call<List<News>>, t: Throwable) {
continuation.resumeWithException(t)
}
override fun onResponse(call: Call<List<News>>, response: Response<List<News>>) {
continuation.resume(response.body().result)
}
})
}

其中的Continuation剩余的计算,从形式上看,它就是一个回调:


public interface Continuation<in T> {
public val context: CoroutineContext
public fun resumeWith(result: Result<T>) // 开始剩余的计算
}

每个 suspend 方法被编译成 java 之后,都会在原有方法参数表最后添加一个 Continuation 参数,用于表达这个挂起点之后“剩余的计算”,举个例子:


scope.launch {
fun1() // 普通方法
suspendFun1() // 挂起方法
// --------------------------
fun2() // 普通方法
suspendFun2() // 挂起方法
// --------------------------
}

整个协程体中有四个方法,其中两个是挂起方法,每个挂起方法都是一道水平的分割线,分割线下方的代码就是当前执行点相对于整个协程体剩余的计算,这“剩余的计算”会被包装成 Continuation 并作为参数传入挂起方法。所以上述代码翻译成 java 就类似于:


scope.launch {
fun1()
suspendFun1(new Continuation() {
@override
public void resumeWith(Result<T> result) {
fun2()
suspendFun2(new Continuation() {
@override
public void resumeWith(Result<T> result) {

}
})
}
})
}

所以挂起方法无异于 java 中带回调的方法,它自然不会阻塞当前线程,它只是把协程体中剩下的代码当成回调,该回调会在将来某个时间点被执行。通过这种方式,挂起方法主动让出了 cpu 执行权。


题外话


从业务上讲,将 Callback 方法改造成挂起式可以降低业务复杂度。举个例子:用户可以通过若干动作触发拉取新闻,比如首次进入新闻页、下拉刷新新闻页、上拉加载更多新闻、切换分区。新闻页有一个埋点,当首次展示某分区时,上报此时的新闻。


若没有 suspend 方法,代码应该这样写:


// NewsViewModel.kt
fun fetchNews(isFirstLoad: Boolean, isChangeType: Boolean) {
newsApi.fetchNews().enqueue(object : Callback<List<News>> {
override fun onFailure(call: Call<List<News>>, t: Throwable) { ... }
override fun onResponse(call: Call<List<News>>, response: Response<List<News>>) {
// 将新闻抛给界面刷新
newsLiveData.value = response.body.result
// 只有当首次加载或切换分区时时才埋点
if(isFirstLoad || isChangeType) {
reportNews(response.body.result)
}
}
})
}
// NewsActivity.kt
// 分区切换监听
tab.setOnTabChangeListener { index ->
newsViewModel.fetchNews(false, true)
}
// 首次加载新闻
fun init() {
newsViewModel.fetchNews(true, false)
}
// 下拉刷新
refreshLayout.setOnRefreshListener {
newsViewModel.fetchNews(false, false)
}
// 上拉加载更多
refreshLayout.setOnLoadMoreListener {
newsViewModel.fetchNews(false, false)
}

因为埋点需要带上新闻列表,所以必须在请求返回之后上报。不同业务场景的拉取接口是同一个,所以只能在统一的 onResponse() 中分类讨论,分类讨论依赖于标记位,不得不为 fetchNews() 添加两个参数。


如果将拉取新闻的接口改成 suspend 方式就能化解这类复杂度:


// NewsViewModel.kt
suspend fun fetchNews() = suspendCoroutine<List<News>> { continuation ->
newsApi.fetchNews().enqueue(object : Callback<List<News>> {
override fun onFailure(call: Call<List<News>>, t: Throwable) {
continuation.resumeWithException(t)
}
override fun onResponse(call: Call<List<News>>, response: Response<List<News>>) {
val news = response.body.result
newsLiveData.value = news
continuation.resume(news)
}
})
}

// NewsActivity.kt
fun initNews() {
scope.launch {
val news = viewModel.fetchNews()
reportNews(news)
}
}

fun changeNewsType() {
scope.launch {
val news = viewModel.fetchNews()
reportNews(news)
}
}

fun loadMoreNews() {
scope.launch { viewModel.fetchNews() }
}

fun refreshNews() {
scope.launch { viewModel.fetchNews() }
}

newsViewModel.newsLiveData.observe {news ->
showNews(news)
}

所有界面的刷新还是走 LiveData,但拉取新闻的方法被改造成挂起之后,也会将新闻列表用类似同步的方式返回,所以可以在相关业务点进行单独埋点。


统计相册加载图片耗时


再通过一个更高并发数的场景比对下各个方案代码上的差异,场景如下:


1657970793022(1).gif


测试并发加载 20 张网络图片的总耗时。该场景下已经无法使用布尔值,因为并发数太多。


CountdownLatch


var start = SystemClock.elapsedRealtime()
var imageUrls = listOf(...)
val countdownLatch = CountDownLatch(imageUrls.size)
// 另起线程等待 CountDownLatch 并输出耗时
scope.launch(Dispatchers.IO) {
countdownLatch.await()
Log.d( "test", "time-consume=${SystemClock.elapsedRealtime() - start}" )
}

// 遍历 20 张图片 url
imageUrls.forEach { img ->
ImageView {// 动态构建 ImageView
layout_width = 100
layout_height = 100
Glide.with(this@GlideActivity)
.load(img)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
countdownLatch.countDown() // 加载完一张
return false
}

override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
countdownLatch.countDown() // 加载完一张
return false
}
})
.into(this)
}
}

协程


var imageUrls = listOf(...)
scope.launch {
val start = SystemClock.elapsedRealtime()
// 将每个 url 都变换为一个 Defered
val defers = imageUrls.map { img ->
val imageView = ImageView {
layout_width = 100
layout_height = 100
}
async { imageView.loadImage(img) }
}
defers.awaitAll()//等待所有的异步任务结束
Log.d( "test", "time-consume=${SystemClock.elapsedRealtime() - start}" )
}
// 将 Callback 方式的加载转换为挂起方式
private suspend fun ImageView.loadImage(img: String) = suspendCoroutine<String> { continuation ->
Glide.with(this@GlideActivity)
.load(img)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
continuation.resume("")
return false
}

override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
continuation.resume("")
return false
}
})
.into(this)
}

你更喜欢哪种方式?


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

Android 实现App应用退到后台显示通知

需求背景 刚开始接到这个需求时,第一时间想到的是做成跟银行类app一样用户退到主页之后,需要在通知栏显示“XXX在后台运行”,并且该通知不能被清除,只有用户重新进入app再消失。然后就想到了一个方案前台服务(foregroundService) 来实现,于是撸...
继续阅读 »
需求背景

刚开始接到这个需求时,第一时间想到的是做成跟银行类app一样用户退到主页之后,需要在通知栏显示“XXX在后台运行”,并且该通知不能被清除,只有用户重新进入app再消失。然后就想到了一个方案前台服务(foregroundService) 来实现,于是撸起袖子就是干。



  • 1、创建一个ForegroundService继承Service

  • 2、重写onCreate等一系列方法

  • 3、创建通知,根据不同版本来开启服务



根据不同版本开启服务



  • 4、监听Application的生命周期,在onActivityStopped中显示前台服务,在onActivityResumed中取消前台服务


显示前台服务


关闭前台服务


搞定,运行代码看看效果。。。


哦豁


完全不对,遇到的问题:



  • 1、并不是所有onActivityStopped执行都是应用被切换至后台---此处百度“如何监听应用被切换至后台”

  • 2、onActivityResumed的时候stopService如果操作快一下到后台一下到前台会收到一大堆的崩溃信息



崩溃信息


遇到问题那咱就解决问题呗,开干~~



  • 1、这个问题倒是很好解决,百度上一大把,添加一个refCount变量,在onActivityStarted方法中++,在onActivityStopped方法中--,然后在onActivityStopped中判断当refCount等于0时表示应用退到后台


变量++


变量--



  • 2、这个问题崩溃的信息意思就是调用了startForegroundService之后没有调用 Service.startForeground()方法,造成这个问题的原因就是短时间内重复进入退出应用,前台服务来不及start就已经被stop
    那怎么办呢?
    第一时间想到的是延迟几秒再stopService,写完运行结果还是一大堆崩溃0.0


于是:于是:发自内心的问自己,为什么要用前台服务?为什么要用前台服务?有没有其他方案呢?


答案肯定是有的,为什么一定要用前台服务呢?直接用通知不行么,好,就用通知


于是,就用一个通知管理类ForegroundPushManager来处理通知的显示和关闭


关闭通知


显示通知


这样就完成了应用退到后台显示通知的功能了。


最后效果


最后遇到的第二个问题如果有好的方案解决的话请大家踊跃指点,谢谢!!


Demo地址:github.com/ling9400/Fo…


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

七夕节马上要到了,前端工程师,后端工程师,算法工程师都怎么哄女朋友开心?

这篇文章的前提是,你得有个女朋友,没有就先收藏着吧!七夕节的来源是梁山伯与祝英台的美丽传说,化成了一对蝴蝶~ 美丽的神话!虽然现在一般是过214的情人节了,但是不得不说,古老的传统的文化遗产,还是要继承啊~在互联网公司中,主要的程序员品种包括:前端工程师,后端...
继续阅读 »

这篇文章的前提是,你得有个女朋友,没有就先收藏着吧!

七夕节的来源是梁山伯与祝英台的美丽传说,化成了一对蝴蝶~ 美丽的神话!虽然现在一般是过214的情人节了,但是不得不说,古老的传统的文化遗产,还是要继承啊~

在互联网公司中,主要的程序员品种包括:前端工程师,后端工程师,算法工程师。

对于具体的职业职能划分还不是很清楚的,我们简单的介绍一下不同程序员岗位的职责:

前端程序员:绘制UI界面,与设计和产品经理进行需求的对接,绘制特定的前端界面推向用户

后端程序员:接收前端json字符串,与数据库对接,将json推向前端进行显示

算法工程师:进行特定的规则映射,优化函数的算法模型,改进提高映射准确率。

七夕节到了,怎么结合自身的的专业技能,哄女朋友开心呢?

前端工程师:我先来,画个动态的晚霞页面!

1.定义样式风格:

.star {
 width: 2px;
 height: 2px;
 background: #f7f7b6;
 position: absolute;
 left: 0;
 top: 0;
 backface-visibility: hidden;
}

2.定义动画特性

@keyframes rotate {
 0% {
   transform: perspective(400px) rotateZ(20deg) rotateX(-40deg) rotateY(0);
}

 100% {
   transform: perspective(400px) rotateZ(20deg) rotateX(-40deg) rotateY(-360deg);
}
}

3.定义星空样式数据

export default {
 data() {
   return {
     starsCount: 800, //星星数量
     distance: 900, //间距
  }
}
}

4.定义星星运行速度与规则:

starNodes.forEach((item) => {
     let speed = 0.2 + Math.random() * 1;
     let thisDistance = this.distance + Math.random() * 300;
     item.style.transformOrigin = `0 0 ${thisDistance}px`;
     item.style.transform =
         `
       translate3d(0,0,-${thisDistance}px)
       rotateY(${Math.random() * 360}deg)
       rotateX(${Math.random() * -50}deg)
       scale(${speed},${speed})`;
  });

前端预览效果图:


后端工程师看后,先点了点头,然后表示不服,画页面太肤浅了,我开发一个接口,定时在女朋友生日的时候发送祝福邮件吧!

1.导入pom.xml 文件

        <!-- mail邮件服务启动器 -->
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-mail</artifactId>
       </dependency>

2.application-dev.properties内部增加配置链接

#QQ\u90AE\u7BB1\u90AE\u4EF6\u53D1\u9001\u670D\u52A1\u914D\u7F6E
spring.mail.host=smtp.qq.com
spring.mail.port=587

## qq邮箱
spring.mail.username=#yourname#@qq.com
## 这里填邮箱的授权码
spring.mail.password=#yourpassword#

3.配置邮件发送工具类 MailUtils.java

@Component
public class MailUtils {
   @Autowired
   private JavaMailSenderImpl mailSender;
   
   @Value("${spring.mail.username}")
   private String mailfrom;

   // 发送简单邮件
   public void sendSimpleEmail(String mailto, String title, String content) {
       // 定制邮件发送内容
       SimpleMailMessage message = new SimpleMailMessage();
       message.setFrom(mailfrom);
       message.setTo(mailto);
       message.setSubject(title);
       message.setText(content);
       // 发送邮件
       mailSender.send(message);
  }
}

4.测试使用定时注解进行注释

@Component
class DemoApplicationTests {

   @Autowired
   private MailUtils mailUtils;

   /**
    * 定时邮件发送任务,每月1日中午12点整发送邮件
    */
   @Scheduled(cron = "0 0 12 1 * ?")
   void sendmail(){
       // 定制邮件内容
       StringBuffer content = new StringBuffer();
       content.append("HelloWorld");
       //分别是接收者邮箱,标题,内容
       mailUtils.sendSimpleEmail("123456789@qq.com","自定义标题",content.toString());
  }
}

@scheduled注解 使用方法: cron:秒,分,时,天,月,年,* 号表示 所有的时间均匹配

5.工程进行打包,部署在服务器的容器中运行即可。

算法工程师,又开发接口,又画页面,我就训练一个自动写诗机器人把!

1.定义神经网络RNN结构

def neural_network(model = 'gru', rnn_size = 128, num_layers = 2):
   cell = tf.contrib.rnn.BasicRNNCell(rnn_size, state_is_tuple = True)
   cell = tf.contrib.rnn.MultiRNNCell([cell] * num_layers, state_is_tuple = True)
   initial_state = cell.zero_state(batch_size, tf.float32)
   with tf.variable_scope('rnnlm'):
       softmax_w = tf.get_variable("softmax_w", [rnn_size, len(words)])
       softmax_b = tf.get_variable("softmax_b", [len(words)])
       embedding = tf.get_variable("embedding", [len(words), rnn_size])
       inputs = tf.nn.embedding_lookup(embedding, input_data)
   outputs, last_state = tf.nn.dynamic_rnn(cell, inputs, initial_state = initial_state, scope = 'rnnlm')
   output = tf.reshape(outputs, [-1, rnn_size])
   logits = tf.matmul(output, softmax_w) + softmax_b
   probs = tf.nn.softmax(logits)
   return logits, last_state, probs, cell, initial_state

2.定义模型训练方法:

def train_neural_network():
   logits, last_state, _, _, _ = neural_network()
   targets = tf.reshape(output_targets, [-1])
   loss = tf.contrib.legacy_seq2seq.sequence_loss_by_example([logits], [targets], \
      [tf.ones_like(targets, dtype = tf.float32)], len(words))
   cost = tf.reduce_mean(loss)
   learning_rate = tf.Variable(0.0, trainable = False)
   tvars = tf.trainable_variables()
   grads, _ = tf.clip_by_global_norm(tf.gradients(cost, tvars), 5)
   #optimizer = tf.train.GradientDescentOptimizer(learning_rate)
   optimizer = tf.train.AdamOptimizer(learning_rate)
   train_op = optimizer.apply_gradients(zip(grads, tvars))

   Session_config = tf.ConfigProto(allow_soft_placement = True)
   Session_config.gpu_options.allow_growth = True

   trainds = DataSet(len(poetrys_vector))

   with tf.Session(config = Session_config) as sess:
       sess.run(tf.global_variables_initializer())

       saver = tf.train.Saver(tf.global_variables())
       last_epoch = load_model(sess, saver, 'model/')

       for epoch in range(last_epoch + 1, 100):
           sess.run(tf.assign(learning_rate, 0.002 * (0.97 ** epoch)))
           #sess.run(tf.assign(learning_rate, 0.01))

           all_loss = 0.0
           for batche in range(n_chunk):
               x,y = trainds.next_batch(batch_size)
               train_loss, _, _ = sess.run([cost, last_state, train_op], feed_dict={input_data: x, output_targets: y})

               all_loss = all_loss + train_loss

               if batche % 50 == 1:
                   print(epoch, batche, 0.002 * (0.97 ** epoch),train_loss)

           saver.save(sess, 'model/poetry.module', global_step = epoch)
           print (epoch,' Loss: ', all_loss * 1.0 / n_chunk)

3.数据集预处理

poetry_file ='data/poetry.txt'
# 诗集
poetrys = []
with open(poetry_file, "r", encoding = 'utf-8') as f:
   for line in f:
       try:
           #line = line.decode('UTF-8')
           line = line.strip(u'\n')
           title, content = line.strip(u' ').split(u':')
           content = content.replace(u' ',u'')
           if u'_' in content or u'(' in content or u'(' in content or u'《' in content or u'[' in content:
               continue
           if len(content) < 5 or len(content) > 79:
               continue
           content = u'[' + content + u']'
           poetrys.append(content)
       except Exception as e:
           pass

poetry.txt文件中存放这唐诗的数据集,用来训练模型

4.测试一下训练后的模型效果:

藏头诗创作:“七夕快乐”

模型运算的结果


哈哈哈,各种节日都是程序员的表(zhuang)演(bi) 时间,不过这些都是锦上添花,只有实实在在,真心,才会天长地久啊~

提前祝各位情侣七夕节快乐!

作者:千与编程
来源:juejin.cn/post/6995491512716918814

收起阅读 »

七夕到了,还不快给你女朋友做一个专属chrome插件

web
前言七夕节马上就要到了,作为拥有对象(没有的话,可以选择 new 一个出来)的程序员来说,肯定是需要有一点表示才行的。用钱能买到的东西不一定能表达咱们的心意,但是用心去写的代码,还能让对象每天看到那才是最正确的选择。除了手机之外,在电脑上使...
继续阅读 »

前言

七夕节马上就要到了,作为拥有对象(没有的话,可以选择 new 一个出来)的程序员来说,肯定是需要有一点表示才行的。用钱能买到的东西不一定能表达咱们的心意,但是用心去写的代码,还能让对象每天看到那才是最正确的选择。

除了手机之外,在电脑上使用浏览器搜索想要的东西是最常用的功能了,所以就需要一个打开即用的搜索框,而且还能表达心意的chrome标签页来让 TA 随时可用。

新建项目

由于我们是做chrome标签页,所以新建的项目不需要任何框架,只需要最简单的HTML、js、css即可。

在任意地方新建一个文件夹chrome

chrome目录下新建一个manifest.json文件

配置chrome插件

{
"name": "Every Day About You",
"description": "Every Day About You",
"version": "1.0",
"manifest_version": 2,
"browser_action": {
"default_icon": "ex_icon.png"
},
"permissions": [
"activeTab"
],
"content_scripts": [
{
"matches": [
""
],
"js": [
"demo.js",
"canvas.js"
],
"run_at": "document_start"
}
],
"chrome_url_overrides": {
"newtab": "demo.html"
},
"offline_enabled": true,
}
复制代码
  • name:扩展名称,加载扩展程序时显示的名称。
  • description:描述信息,用于描述当前扩展程序,限132个字符。
  • version:扩展程序版本号。
  • manifest_version:manifest文件版本号。chrome18开始必须为2。
  • browser_action:设置扩展程序的图标。
  • permissions:需要申请的权限,这里使用tab即可。
  • content_scripts:指定在页面中运行的js和css及插入时机。
  • chrome_url_overrides:新标签页打开的html文件。
  • offline_enabled:脱机运行。

还有很多配置项可以在chrome插件开发文档中查询到,这里因为不需要发布到chrome商店中,所以只需要配置一些固定的数据项。

image.png

新建HTML和JS

在配置项中的content_scriptschrome_url_overrides中分别定义了html文件和js文件,所以我们需要新建这两个文件,名称对应即可。

image.png

HTML背景

没有哪个小天使可以拒绝来自程序猿霸道的满屏小心心好吗? 接下来我来教大家做一个飘满屏的爱心。

html>
<html>
<head>
<meta charset="utf-8">
<title>Every Day About Youtitle>
<script src="http://libs.baidu.com/jquery/1.10.2/jquery.min.js">script>
<
script type="text/javascript" src="canvas.js" >script>
head>
<
body>
<
canvas id="c" style="position: absolute;z-index: -1;text-align: center;">canvas>
body>
html>
复制代码
  • 这里引入的 jquery 是 百度 的CDN(matches中配置了可以使用所有的URL,所以CDN是可以使用外部链接的。)
  • canvas.js中主要是针对爱心和背景色进行绘画。

canvas

$(document).ready(function () {
var canvas = document.getElementById("c");
var ctx = canvas.getContext("2d");
var c = $("#c");
var w, h;
var pi = Math.PI;
var all_attribute = {
num: 100, // 个数
start_probability: 0.1, // 如果数量小于num,有这些几率添加一个新的
size_min: 1, // 初始爱心大小的最小值
size_max: 2, // 初始爱心大小的最大值
size_add_min: 0.3, // 每次变大的最小值(就是速度)
size_add_max: 0.5, // 每次变大的最大值
opacity_min: 0.3, // 初始透明度最小值
opacity_max: 0.5, // 初始透明度最大值
opacity_prev_min: .003, // 透明度递减值最小值
opacity_prev_max: .005, // 透明度递减值最大值
light_min: 0, // 颜色亮度最小值
light_max: 90, // 颜色亮度最大值
};
var style_color = find_random(0, 360);
var all_element = [];
window_resize();

function start() {
window.requestAnimationFrame(start);
style_color += 0.1;
//更改背景色hsl(颜色值,饱和度,明度)
ctx.fillStyle = 'hsl(' + style_color + ',100%,97%)';
ctx.fillRect(0, 0, w, h);
if (all_element.length < all_attribute.num && Math.random() < all_attribute.start_probability) {
all_element.push(new ready_run);
}
all_element.map(function (line) {
line.to_step();
})
}

function ready_run() {
this.to_reset();
}

function arc_heart(x, y, z, m) {
//绘制爱心图案的方法,参数x,y是爱心的初始坐标,z是爱心的大小,m是爱心上升的速度
y -= m * 10;

ctx.moveTo(x, y);
z *= 0.05;
ctx.bezierCurveTo(x, y - 3 * z, x - 5 * z, y - 15 * z, x - 25 * z, y - 15 * z);
ctx.bezierCurveTo(x - 55 * z, y - 15 * z, x - 55 * z, y + 22.5 * z, x - 55 * z, y + 22.5 * z);
ctx.bezierCurveTo(x - 55 * z, y + 40 * z, x - 35 * z, y + 62 * z, x, y + 80 * z);
ctx.bezierCurveTo(x + 35 * z, y + 62 * z, x + 55 * z, y + 40 * z, x + 55 * z, y + 22.5 * z);
ctx.bezierCurveTo(x + 55 * z, y + 22.5 * z, x + 55 * z, y - 15 * z, x + 25 * z, y - 15 * z);
ctx.bezierCurveTo(x + 10 * z, y - 15 * z, x, y - 3 * z, x, y);
}
ready_run.prototype = {
to_reset: function () {
var t = this;
t.x = find_random(0, w);
t.y = find_random(0, h);
t.size = find_random(all_attribute.size_min, all_attribute.size_max);
t.size_change = find_random(all_attribute.size_add_min, all_attribute.size_add_max);
t.opacity = find_random(all_attribute.opacity_min, all_attribute.opacity_max);
t.opacity_change = find_random(all_attribute.opacity_prev_min, all_attribute.opacity_prev_max);
t.light = find_random(all_attribute.light_min, all_attribute.light_max);
t.color = 'hsl(' + style_color + ',100%,' + t.light + '%)';
},
to_step: function () {
var t = this;
t.opacity -= t.opacity_change;
t.size += t.size_change;
if (t.opacity <= 0) {
t.to_reset();
return false;
}
ctx.fillStyle = t.color;
ctx.globalAlpha = t.opacity;
ctx.beginPath();
arc_heart(t.x, t.y, t.size, t.size);
ctx.closePath();
ctx.fill();
ctx.globalAlpha = 1;
}
}

function window_resize() {
w = window.innerWidth;
h = window.innerHeight;
canvas.width = w;
canvas.height = h;
}
$(window).resize(function () {
window_resize();
});

//返回一个介于参数1和参数2之间的随机数
function find_random(num_one, num_two) {
return Math.random() * (num_two - num_one) + num_one;
}

start();
});
复制代码
  • 因为使用了jquery的CDN,所以我们在js中就可以直接使用 $(document).ready方法

chrome-capture-2022-6-20.gif

土豪金色的标题

为了时刻展示出对 TA 的爱,我们除了在背景中体现出来之外,还可以再文字中体现出来,所以需要取一个充满爱意的标题。

<body>
<canvas id="c" style="position: absolute;z-index: -1;text-align: center;">canvas>
<div class="middle">
<h1 class="label">Every Day About Youh1>
div>
body>
复制代码

复制代码
  • 这里引入了googleapis中的字体样式。
  • 给label一个背景,并使用了动画效果。

text_bg.png

  • 这个就是文字后面的静态图片,可以另存为然后使用的哦~

chrome-capture-2022-6-20 (1).gif

百度搜索框

对于你心爱的 TA 来说,不管干什么估计都得用百度直接搜出来,就算是看个优酷、微博都不会记住域名,都会直接去百度一下,所以我们需要在标签页中直接集成百度搜索。让 TA 可以无忧无虑的搜索想要的东西。

由于现在百度搜索框不能直接去站长工具中获取了,所以我们可以参考掘金标签页插件中的百度搜索框。

1.gif

根据掘金的标签页插件我们可以发现,输入结果之后,直接跳转到百度的网址,并在url后面携带了一个 wd 的参数,wd 也就是我们输入的内容了。

http://www.baidu.com/s?wd=这里是输入的…

<div class="search">
<input id="input" type="text">
<button>百度一下button>
div>
复制代码

复制代码
.search {
width: 750px;
height: 50px;
margin: auto;
display: flex;
justify-content: center;
align-content: center;
min-width: 750px;
position: relative;
}

input {
width: 550px;
height: 40px;
border-right: none;
border-bottom-left-radius: 10px;
border-top-left-radius: 10px;
border-color: #f5f5f5;
/* 去除搜索框激活状态的边框 */
outline: none;
}

input:hover {
/* 鼠标移入状态 */
box-shadow: 2px 2px 2px #ccc;
}

input:focus {
/* 选中状态,边框颜色变化 */
border-color: rgb(78, 110, 242);
}

.search span {
position: absolute;
font-size: 23px;
top: 10px;
right: 170px;
}

.search span:hover {
color: rgb(78, 110, 242);
}

button {
width: 100px;
height: 44px;
background-color: rgb(78, 110, 242);
border-bottom-right-radius: 10px;
border-top-right-radius: 10px;
border-color: rgb(78, 110, 242);
color: white;
font-size: 14px;
}
复制代码

chrome-capture-2022-6-20 (2).gif

关于 TA

这里可以放置你们之间的一些生日,纪念日等等,也可以放置你想放置的任何浪漫,仪式感满满~

如果你不记得两个人之间的纪念日,那就换其他的日子吧。比如你和 TA 闺蜜的纪念日也可以。

<body>
<canvas id="c" style="position: absolute;z-index: -1;text-align: center;">canvas>
<div class="middle">
<h1 class="label">Every Day About Youh1>
<div class="time">
<span>
<div id="d">
00
div>
Love day
span> <span>
<div id="h">
00
div>
First Met
span> <span>
<div id="m">
00
div>
birthday
span> <span>
<div id="s">
00
div>
age
span>
div>
div>
<script type="text/javascript" src="demo.js">script>
body>
复制代码
  • 这里我定义了四个日期,恋爱纪念日、相识纪念日、TA 的生日、TA 的年龄。
  • 在页面最后引用了一个js文件,主要是等待页面渲染完成之后调用js去计算日期的逻辑。
恋爱纪念日
var date1 = new Date('2019-10-07')
var date2 = new Date()

var s1 = date1.getTime(),
s2 = date2.getTime();

var total = (s2 - s1) / 1000;

var day = parseInt(total / (24 * 60 * 60)); //计算整数天数

const d = document.getElementById("d");

d.innerHTML = getTrueNumber(day);

复制代码
相识纪念日
var date1 = new Date('2019-09-20')
var date2 = new Date()

var s1 = date1.getTime(),
s2 = date2.getTime();

var total = (s2 - s1) / 1000;

var day = parseInt(total / (24 * 60 * 60)); //计算整数天数

h.innerHTML = getTrueNumber(day);
复制代码
公共方法(将计算出来的日子转为绝对值)
const getTrueNumber = x => (x < 0 ? Math.abs(x) : x);
复制代码

chrome-capture-2022-6-20 (3).gif

由于生日和年龄的计算代码有些多,所以放在码上掘金中展示了。

添加到chrome浏览器中

image.png

开发完成之后,所有的文件就是这样的了,里面的icon可以根据自己的喜好去设计或者网上下载。

使用chrome浏览器打开:chrome://extensions/ 即可跳转到添加扩展程序页面。

2.gif

  • 打开右上角的开发者模式
  • 点击加载已解压的扩展程序
  • 选择自己的chrome标签页项目目录即可

3.gif

总结一下

为了让心爱的 TA 开心,作为程序员的我们可谓是煞费苦心呀!!

在给对象安装插件的时候,发现了一个小问题,可能是chrome版本原因,导致jquery的cdn无法直接引用,所以可能需要手动把jquery保存到项目文件中,然后在manifest.json配置js的地方把jquery的js加上即可。

码上掘金中我已经把jquery的代码、canvas的代码、计算纪念日的代码都放进去了,可以直接复制到自己项目中哦!!!

七夕节快到了,祝愿天下有情人终成眷属!

来源:juejin.cn/post/7122332008252080142

收起阅读 »

老板连夜抠掉全公司电脑Alt键,只为限制员工摸鱼...哄堂大笑了兄弟们

有人说,优秀公司抓产品,一般公司抓业绩,奇葩公司抓“摸鱼”。 “为了防止自家员工摸鱼,亲自出手扣除键盘alt键”,自认为这样一来,员工在摸鱼时想要切换窗口界面就没有那么方便了,就能更方便自己“抓到”摸鱼的人。正所谓天下代有“才人”出,在2022年的第...
继续阅读 »



有人说,优秀公司抓产品,一般公司抓业绩,奇葩公司抓“摸鱼”。 
近日,一位私企老板就为码君生动演绎了什么叫“与人斗,其乐无穷”。 



“为了防止自家员工摸鱼,亲自出手扣除键盘alt键”,自认为这样一来,员工在摸鱼时想要切换窗口界面就没有那么方便了,就能更方便自己“抓到”摸鱼的人。
说实话,刚看到这个消息的时候,码君的脑回路一时都没有转过弯来,甚至还在帮这位老板思考这样是不是真的有什么“深远”的作用。


仔细看了几遍后,我才终于接受了世界上真的有这么“离谱”的人和事存在。 
正所谓天下代有“才人”出,在2022年的第一个月,这位老板就成功预定了“年度十大迷惑事件”之一的位置。 


而根据视频中员工的爆料,这老板在之前还有过好几次类似的操作,比如:

“在厕所内偷偷装上信号屏蔽器,防止员工‘带薪蹲坑’”;

“给员工们定中午盒饭,美名其曰修复‘中午找不到人的BUG’”



码君属实是蚌埠住了,这老板的行事风格真是槽点满满。
从动机上来说,你一位私企老板不去好好谈项目,拉合作,每天关注自己员工有没有摸鱼,这样对公司的发展真的好嘛?

从某种意义上,这种抓摸鱼的行为对你自己来说是不是也是一种摸鱼呢?这就是老板,员工的摸鱼二象性?



而从行动上来说,您跑出来扣人家键盘是什么神奇操作啊?
咱首先想想,对于某些员工自配的机械键盘来说,人家是靠轴体操作的,换个不常用键位插上基本就没差啊。
再退一步想,拔了alt键是不是也会降低员工们干正事时的效率呢?
再再退一步想,这个事件有个最关键的问题在于——快捷键这个东西,它是可以自行改的呀!



扣除个别按键就想禁用“快速关闭窗口”和“快速切换窗口”功能,未免有些太小瞧人家微软了吧。 
还是说,作为一位老板,您不会不知道这个操作吧?
而面对这样的“大无语事件”,网友们自然也是有话要说—— 



有网友将其与“周扒皮”类比,要我说,多少是有些抬举了,至少人家周扒皮学鸡叫,比“扣alt键”可专业多了。



有网友也像文章开头那样认为,屁事越多的公司,基本都混得越差。 
员工手里的活都干完了,做一些自己的闲事又有什么关系呢,咱们是打工,又不是卖身是不是?



一些设计行业的朋友更是直呼内行——你都已经把我的alt键扣了,我不摸鱼我能干嘛呢?
不如一鼓作气,把Ctrl、Shift、C、V等键位都扣了吧,这样才叫皆大欢喜。



说实话,职场上限制员工摸鱼的事情,这几年也是挺常见的,有一些可以接受,但有一些简直就是“反智”。 

就比如码君之前曾报道过的“国美监控员工手机流量使用情况”的事情。

因为员工上班摸鱼对其进行行政处罚、通报批评,就算过了两个多月,再看也依旧觉得离谱。

还有“一 iOS 开发员工因玩手机被开除”的事情,不在业务上、产品上推陈出新,每天就对着员工制度作妖。

这样的公司要是日后黄了,码君都要欢呼一句:“好似,开香槟咯!” 




说到底,打工人们奔波在这社会上,落脚在你的公司里,无非是为了一个“”字。
职场制度存在的初心也不是为了限制什么“摸鱼”行为,而是更多让员工将力气在集中在业务上。
当你公司的业务做得红火,员工收获得满满当当,哪会有那么多时间浪费在摸鱼上呢?还不上赶着做项目,挣大钱?

希望这类新闻中的老板们早日清醒,不要等到哪天真的遭到“反噬”了,才能明白这些真理啊!

你遇见过哪些防摸鱼损招?
*期待你的留言!

来源 | 抓码青年
收起阅读 »

大公司为什么禁止SpringBoot项目使用Tomcat?

前言 在SpringBoot框架中,我们使用最多的是Tomcat,这是SpringBoot默认的容器技术,而且是内嵌式的Tomcat。同时,SpringBoot也支持Undertow容器,我们可以很方便的用Undertow替换Tomcat,而Undertow的...
继续阅读 »

前言


在SpringBoot框架中,我们使用最多的是Tomcat,这是SpringBoot默认的容器技术,而且是内嵌式的Tomcat。同时,SpringBoot也支持Undertow容器,我们可以很方便的用Undertow替换Tomcat,而Undertow的性能和内存使用方面都优于Tomcat,那我们如何使用Undertow技术呢?本文将为大家细细讲解。


SpringBoot中的Tomcat容器


SpringBoot可以说是目前最火的Java Web框架了。它将开发者从繁重的xml解救了出来,让开发者在几分钟内就可以创建一个完整的Web服务,极大的提高了开发者的工作效率。Web容器技术是Web项目必不可少的组成部分,因为任Web项目都要借助容器技术来运行起来。在SpringBoot框架中,我们使用最多的是Tomcat,这是SpringBoot默认的容器技术,而且是内嵌式的Tomcat。推荐:几乎涵盖你需要的SpringBoot所有操作


SpringBoot设置Undertow


对于Tomcat技术,Java程序员应该都非常熟悉,它是Web应用最常用的容器技术。我们最早的开发的项目基本都是部署在Tomcat下运行,那除了Tomcat容器,SpringBoot中我们还可以使用什么容器技术呢?没错,就是题目中的Undertow容器技术。SrpingBoot已经完全继承了Undertow技术,我们只需要引入Undertow的依赖即可,如下图所示。


image.png


image.png


配置好以后,我们启动应用程序,发现容器已经替换为Undertow。那我们为什么需要替换Tomcat为Undertow技术呢?


Tomcat与Undertow的优劣对比


Tomcat是Apache基金下的一个轻量级的Servlet容器,支持Servlet和JSP。Tomcat具有Web服务器特有的功能,包括 Tomcat管理和控制平台、安全局管理和Tomcat阀等。Tomcat本身包含了HTTP服务器,因此也可以视作单独的Web服务器。但是,Tomcat和ApacheHTTP服务器不是一个东西,ApacheHTTP服务器是用C语言实现的HTTP Web服务器。Tomcat是完全免费的,深受开发者的喜爱。


图片


Undertow是Red Hat公司的开源产品, 它完全采用Java语言开发,是一款灵活的高性能Web服务器,支持阻塞IO和非阻塞IO。由于Undertow采用Java语言开发,可以直接嵌入到Java项目中使用。同时, Undertow完全支持Servlet和Web Socket,在高并发情况下表现非常出色。


图片


我们在相同机器配置下压测Tomcat和Undertow,得到的测试结果如下所示:QPS测试结果对比: Tomcat


图片


Undertow


图片


内存使用对比:


Tomcat


image.png


Undertow


image.png


通过测试发现,在高并发系统中,Tomcat相对来说比较弱。在相同的机器配置下,模拟相等的请求数,Undertow在性能和内存使用方面都是最优的。并且Undertow新版本默认使用持久连接,这将会进一步提高它的并发吞吐能力。所以,如果是高并发的业务系统,Undertow是最佳选择。


最后


SpingBoot中我们既可以使用Tomcat作为Http服务,也可以用Undertow来代替。Undertow在高并发业务场景中,性能优于Tomcat。所以,如果我们的系统是高并发请求,不妨使用一下Undertow,你会发现你的系统性能会得到很大的提升。


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

Flutter 小技巧之优化使用的 BuildContext

Flutter 里的 BuildContext 相信大家都不会陌生,虽然它叫 Context,但是它实际是 Element 的抽象对象,而在 Flutter 里,它主要来自于 ComponentElement 。 关于 ComponentElement...
继续阅读 »

Flutter 里的 BuildContext 相信大家都不会陌生,虽然它叫 Context,但是它实际是 Element 的抽象对象,而在 Flutter 里,它主要来自于 ComponentElement


关于 ComponentElement 可以简单介绍一下,在 Flutter 里根据 Element 可以简单地被归纳为两类:



  • RenderObjectElement :具备 RenderObject ,拥有布局和绘制能力的 Element

  • ComponentElement :没有 RenderObject ,我们常用的 StatelessWidgetStatefulWidget 里对应的 StatelessElementStatefulElement 就是它的子类。


所以一般情况下,我们在 build 方法或者 State 里获取到的 BuildContext 其实就是 ComponentElement


那使用 BuildContext 有什么需要注意的问题


首先如下代码所示,在该例子里当用户点击 FloatingActionButton 的时候,代码里做了一个 2秒的延迟,然后才调用 pop 退出当前页面。


class _ControllerDemoPageState extends State<ControllerDemoPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
onPressed: () async {
await Future.delayed(Duration(seconds: 2));
Navigator.of(context).pop();
},
),
);
}
}

正常情况下是不会有什么问题,但是当用户在点击了 FloatingActionButton 之后,又马上点击了 AppBar 返回退出应用,这时候就会出现以下的错误提示。



可以看到此时 log 说,Widget 对应的 Element 已经不在了,因为在 Navigator.of(context) 被调用时,context 对应的 Element 已经随着我们的退出销毁。


一般情况下处理这个问题也很简单,那就是增加 mounted 判断,通过 mounted 判断就可以避免上述的错误


class _ControllerDemoPageState extends State<ControllerDemoPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
floatingActionButton: FloatingActionButton(
onPressed: () async {
await Future.delayed(Duration(seconds: 2));
if (!mounted) return;
Navigator.of(context).pop();
},
),
);
}
}

上面代码里的 mounted 标识位来自于 State因为 State 是依附于 Element 创建,所以它可以感知 Element 的生命周期,例如 mounted 就是判断 _element != null;



那么到这里我们收获了一个小技巧:使用 BuildContext 时,在必须时我们需要通过 mounted 来保证它的有效性


那么单纯使用 mounted 就可以满足 context 优化的要求了吗


如下代码所示,在这个例子里:



  • 我们添加了一个列表,使用 builder 构建 Item

  • 每个列表都有一个点击事件

  • 点击列表时我们模拟网络请求,假设网络也不是很好,所以延迟个 5 秒

  • 之后我们滑动列表让点击的 Item 滑出屏幕不可见


class _ControllerDemoPageState extends State<ControllerDemoPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ListView.builder(
itemBuilder: (context, index) {
return ListItem();
},
itemCount: 30,
),
);
}
}
class ListItem extends StatefulWidget {
const ListItem({Key? key}) : super(key: key);
@override
State<ListItem> createState() => _ListItemState();
}

class _ListItemState extends State<ListItem> {
@override
Widget build(BuildContext context) {
return ListTile(
title: Container(
height: 160,
color: Colors.amber,
),
onTap: () async {
await Future.delayed(Duration(seconds: 5));
if(!mounted) return;
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("Tip")));
},
);
}
}

由于在 5 秒之内,Item 被划出了屏幕,所以对应的 Elment 其实是被释放了,从而由于 mounted 判断,SnackBar 不会被弹出。


那如果假设需要在开发时展示点击数据上报的结果,也就是 Item 被释放了还需要弹出,这时候需要如何处理


我们知道不管是 ScaffoldMessenger.of(context) 还是 Navigator.of(context) ,它本质还是通过 context 去往上查找对应的 InheritedWidget 泛型,所以其实我们可以提前获取。


所以,如下代码所示,在 Future.delayed 之前我们就通过 ScaffoldMessenger.of(context); 获取到 sm 对象,之后就算你直接退出当前的列表页面,5秒过后 SnackBar 也能正常弹出。


class _ListItemState extends State<ListItem> {
@override
Widget build(BuildContext context) {
return ListTile(
title: Container(
height: 160,
color: Colors.amber,
),
onTap: () async {
var sm = ScaffoldMessenger.of(context);
await Future.delayed(Duration(seconds: 5));
sm.showSnackBar(SnackBar(content: Text("Tip")));
},
);
}
}

为什么页面销毁了,但是 SnackBar 还能正常弹出


因为此时通过 of(context); 获取到的 ScaffoldMessenger 是存在 MaterialApp 里,所以就算页面销毁了也不影响 SnackBar 的执行。


但是如果我们修改例子,如下代码所示,在 Scaffold 上面多嵌套一个 ScaffoldMessenger ,这时候在 Item 里通过 ScaffoldMessenger.of(context) 获取到的就会是当前页面下的 ScaffoldMessenger


class _ControllerDemoPageState extends State<ControllerDemoPage> {
@override
Widget build(BuildContext context) {
return ScaffoldMessenger(
child: Scaffold(
appBar: AppBar(),
body: ListView.builder(
itemBuilder: (context, index) {
return ListItem();
},
itemCount: 30,
),
),
);
}
}

这种情况下我们只能保证Item 不可见的时候 SnackBar 还能正常弹出, 而如果这时候我们直接退出页面,还是会出现以下的错误提示,因为 ScaffoldMessenger 也被销毁了 。



所以到这里我们收获第二个小技巧:在异步操作里使用 of(context) ,可以提前获取,之后再做异步操作,这样可以尽量保证流程可以完整执行


既然我们说到通过 of(context) 去获取上层共享往下共享的 InheritedWidget ,那在哪里获取就比较好


还记得前面的 log 吗?在第一个例子出错时,log 里就提示了一个方法,也就是 State 的 didChangeDependencies 方法。



为什么是官方会建议在这个方法里去调用 of(context)


首先前面我们一直说,通过 of(context) 获取到的是 InheritedWidget ,而 当 InheritedWidget 发生改变时,就是通过触发绑定过的 Element 里 State 的didChangeDependencies 来触发更新,所以在 didChangeDependencies 里调用 of(context) 有较好的因果关系



对于这部分内容感兴趣的,可以看 Flutter 小技巧之 MediaQuery 和 build 优化你不知道的秘密全面理解State与Provider



那我能在 initState 里提前调用吗


当然不行,首先如果在 initState 直接调用如 ScaffoldMessenger.of(context).showSnackBar 方法,就会看到以下的错误提示。



这是因为 Element 里会判断此时的 _StateLifecycle 状态,如果此时是 _StateLifecycle.created 或者 _StateLifecycle.defunct ,也就是在 initStatedispose ,是不允许执行 of(context) 操作。




of(context) 操作指的是 context.dependOnInheritedWidgetOfExactTyp



当然,如果你硬是想在 initState 下调用也行,增加一个 Future 执行就可以成功执行


@override
void initState() {
super.initState();
Future((){
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text("initState")));
});
}


简单理解,因为 Dart 是单线程轮询执行,initState 里的 Future 相当于是下一次轮询,自然也就不在 _StateLifecycle.created 的状态下。



那我在 build 里直接调用不行吗


直接在 build 里调用肯定可以,虽然 build 会被比较频繁执行,但是 of(context) 操作其实就是在一个 map 里通过 key - value 获取泛型对象,所以对性能不会有太大的影响。


真正对性能有影响的是 of(context) 的绑定数量和获取到对象之后的自定义逻辑,例如你通过 MediaQuery.of(context).size 获取到屏幕大小之后,通过一系列复杂计算来定位你的控件。


  @override
Widget build(BuildContext context) {
var size = MediaQuery.of(context).size;
var padding = MediaQuery.of(context).padding;
var width = size.width / 2;
var height = size.width / size.height * (30 - padding.bottom);
return Container(
color: Colors.amber,
width: width,
height: height,
);
}

例如上面这段代码,可能会导致键盘在弹出的时候,虽然当前页面并没有完全展示,但是也会导致你的控件不断重新计算从而出现卡顿。



详细解释可以参考 Flutter 小技巧之 MediaQuery 和 build 优化你不知道的秘密



所以到这里我们又收获了一个小技巧: 对于 of(context) 的相关操作逻辑,可以尽量放到 didChangeDependencies 里去处理


最后,今天主要分享了在使用 BuildContext 时的一些注意事项和技巧,如果你对于这方面还有什么疑问,欢迎留言评论。


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

记录一个温度曲线的View

 最近做项目需求的看到需要自定义一个温度曲线的图。由于之前的同事理解需求的时候没有很好的理解产品的需求,将温度的折线图分成了两个View,温度高的在一个View,温度低的在一个View。这样的做法其实是没有很好的理解产品的需求的。为什么这么说,因为一...
继续阅读 »

image-20220713155216246.png 最近做项目需求的看到需要自定义一个温度曲线的图。由于之前的同事理解需求的时候没有很好的理解产品的需求,将温度的折线图分成了两个View,温度高的在一个View,温度低的在一个View。这样的做法其实是没有很好的理解产品的需求的。为什么这么说,因为一旦拆成两个View,那么哪些相交的点绘制就会有缺陷了。什么意思,看图。

image-20220713155901206.png

如果按照两个View去做,就会有这种局限性。相交的点就会被切。所以这里就重新修改了这个自定义View。

有了上面的需求,那么就开始我们的设计了。首先为了我们自定义View的能比较好的通用性,我们需要把一些可能会变的东西提取出来。这里只是提取一些很常用的属性,其余需要自定义的,可自己加上。直接看代码

<declare-styleable name="NewWeatherChartView">
   <!--开始的x坐标-->
   <attr name="new_start_point_x" format="dimension"/>
   <!--两点之间x坐标的间隔-->
   <attr name="new_point_x_margin" format="dimension"/>
   <!--显示温度的字体大小-->
   <attr name="temperature_text_size" format="dimension"/>
   <!--圆点的半径-->
   <attr name="point_radius" format="dimension"/>

   <!--选中天气项,温度字体的颜色-->
   <attr name="select_temperature_text_color" format="reference|color"/>
   <!--未选中天气项,温度字体的颜色-->
   <attr name="unselect_temperature_text_color" format="reference|color"/>
   <!--选中天气项,圆点的颜色-->
   <attr name="select_point_color" format="reference|color"/>
   <!--未选中天气项,圆点的颜色-->
   <attr name="unselect_point_color" format="reference|color"/>
<!--连接线的颜色-->
   <attr name="line_color" format="reference|color"/>
   <!--连接线的类型,可以是实线,也可以是虚线,默认是虚线。0虚线,1实线-->
   <attr name="line_type" format="integer"/>

</declare-styleable>
public class NewWeatherChartView extends View {
   private final static String TAG = "NewWeatherChartView";
   private List<WeatherInfo> items;//温度的数据源

   //都是可以在XML里面配置的属性,目前项目里面都是用的默认配置。
   private int mLineColor;
   private int mSelectTemperatureColor;
   private int mUnSelectTemperatureColor;
   private int mSelectPointColor;
   private int mUnselectPointColor;
   private int mLineType;
   private int mTemperatureTextSize;
   private int mPointStartX = 0;
   private int mPointXMargin = 0;
   private int mPointRadius;


   
   private Point[] mHighPoints; //高温的点的坐标
   private Point[] mLowPoints; //低温的点的坐标

   //这里是为了方便写代码,多创建了几个画笔,也可以用一个画笔,然后配置不同的属性
   private Paint mLinePaint; //用于画线画笔
   private Paint mTextPaint; // 用于画小圆点旁边的温度文字的画笔
   private Paint mCirclePaint;//用来画小圆点的画笔
 

   private Float mMaxTemperature = Float.MIN_VALUE;//最高温度
   private Float mMinTemperature = Float.MAX_VALUE;//最低温度
   private Path mPath;//连接线的路径
   
   private DecimalFormat mDecimalFormat;


   private int mTodayIndex = -1;//用于判断哪一个被选中

   private Context mContext;
...
}

以上就是一些初始化的东西了,那么现在就来思考一下,怎么去画这些东西,上面的初始化也说明了,我们主要是画线,画文字,然后画圆点。那么应该从哪开始呢?首先是从点坐标开始,因为无论是线,还是文字,他们的位置和点都有关系。那么找到点的坐标就是首要的工作。怎么找点的坐标,以及最开始的X坐标是多少。第一个点的X坐标是根据我们的配置来的,那么第二个点的x坐标呢?,第二个点的x坐标就是第一个点的x坐标加上他们之间的在X方向上距离,而在x方向上的距离也是根据属性配置的。所以我们可以很容易得到所有点的x坐标。那么圆点的y坐标呢?首先我们看一张图。

image-20220713172903532.png

我们的点,应该是均匀分布在剩余高度里面的。

剩余高度 = 控件高度-2*文字的高度。

点的y坐标为

*剩余高度-((当前温度-最低温度)/(最高温度-最低温度)剩余高度)+文字高度

看起来有点复杂,但是有公式的话,代码会比较简单。接下来就需要看初始化的代码了和计算点坐标的代码了

代码如下:

//首先从两个参数的构造函数里面获取各种配置的值
public NewWeatherChartView(Context context, AttributeSet attrs) {
   super(context, attrs);
   TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.NewWeatherChartView);
   mPointStartX = (int) typedArray.getDimension(R.styleable.NewWeatherChartView_new_start_point_x, 0);
   mPointXMargin = (int) typedArray.getDimension(R.styleable.NewWeatherChartView_new_point_x_margin, 0);
   mTemperatureTextSize = (int) typedArray.getDimension(R.styleable.NewWeatherChartView_temperature_text_size, 20);
   mPointRadius = (int) typedArray.getDimension(R.styleable.NewWeatherChartView_point_radius, 8);

   mSelectPointColor = typedArray.getColor(R.styleable.NewWeatherChartView_select_point_color, context.getResources().getColor(R.color.weather_select_point_color));
   mUnselectPointColor = typedArray.getColor(R.styleable.NewWeatherChartView_unselect_point_color, context.getResources().getColor(R.color.weather_unselect_point_color));
   mLineColor = typedArray.getColor(R.styleable.NewWeatherChartView_line_color, context.getResources().getColor(R.color.weather_line_color));
   mSelectTemperatureColor = typedArray.getColor(R.styleable.NewWeatherChartView_select_temperature_text_color, context.getResources().getColor(R.color.weather_select_temperature_color));
   mUnSelectTemperatureColor = typedArray.getColor(R.styleable.NewWeatherChartView_unselect_temperature_text_color, context.getResources().getColor(R.color.weather_unselect_temperature_color));

   mLineType = typedArray.getInt(R.styleable.NewWeatherChartView_line_type, 0);

   this.mContext = context;
   typedArray.recycle();
}

private void initData() {
   //初始化线的画笔
   mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   mLinePaint.setStyle(Paint.Style.STROKE);
   mLinePaint.setStrokeWidth(2);
   mLinePaint.setDither(true);
   //配置虚线
   if (mLineType == 0) {
       DashPathEffect pathEffect = new DashPathEffect(new float[]{10, 5}, 1);
       mLinePaint.setPathEffect(pathEffect);
  }
   mPath = new Path();

   //初始化文字的画笔
   mTextPaint = new Paint();
   mTextPaint.setAntiAlias(true);
   mTextPaint.setTextSize(sp2px(mTemperatureTextSize));
   mTextPaint.setTextAlign(Paint.Align.CENTER);

   // 初始化圆点的画笔
   mCirclePaint = new Paint();
   mCirclePaint.setStyle(Paint.Style.FILL);

   mDecimalFormat = new DecimalFormat("0");

   for (int i = 0; i < items.size(); i++) {
       float highY = items.get(i).getHigh();
       float lowY = items.get(i).getLow();
       if (highY > mMaxTemperature) {
           mMaxTemperature = highY;
      }
       if (lowY < mMinTemperature) {
           mMinTemperature = lowY;
      }
       if (DateUtil.fromTodayDate(items.get(i).getDate()) == 0) {
           mTodayIndex = i;
      }
  }
   float span = mMaxTemperature - mMinTemperature;
   //这种情况是为了防止所有温度都一样的情况
   if (span == 0) {
       span = 6.0f;
  }
   mMaxTemperature = mMaxTemperature + span / 6.0f;
   mMinTemperature = mMinTemperature - span / 6.0f;

   mHighPoints = new Point[items.size()];
   mLowPoints = new Point[items.size()];
}

public int sp2px(float spValue) {
   return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spValue, Resources.getSystem().getDisplayMetrics());
}

public int dip2px(float dpValue) {
   return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, Resources.getSystem().getDisplayMetrics());

}

这些准备工作昨晚之后,我们就可以去onDraw里面画图了。

protected void onDraw(Canvas canvas) {
   Logging.d(TAG, "onDraw: ");
   if (items == null) {
       return;
  }
   int pointX = mPointStartX; // 开始的X坐标
   int textHeight = sp2px(mTemperatureTextSize);//文字的高度
   int remainingHeight = getHeight() - textHeight * 2;//除去文字后,剩余的高度

   // 计算每一个点的X和Y坐标
   for (int i = 0; i < items.size(); i++) {
       int x = pointX + mPointXMargin * i;
       float highTemp = items.get(i).getHigh();
       float lowTemp = items.get(i).getLow();
       int highY = remainingHeight - (int) (remainingHeight * ((highTemp - mMinTemperature) / (mMaxTemperature - mMinTemperature))) + textHeight;
       int lowY = remainingHeight - (int) (remainingHeight * ((lowTemp - mMinTemperature) / (mMaxTemperature - mMinTemperature))) + textHeight;
       mHighPoints[i] = new Point(x, highY);
       mLowPoints[i] = new Point(x, lowY);
  }

   // 画线
   drawLine(mHighPoints, canvas);
   drawLine(mLowPoints, canvas);
   for (int i = 0; i < mHighPoints.length; i++) {
       // 画文本度数 例如:3°
       String yHighText = mDecimalFormat.format(items.get(i).getHigh());
       String yLowText = mDecimalFormat.format(items.get(i).getLow());
       int highDrawY = mHighPoints[i].y - dip2px(mPointRadius + 8);
       int lowDrawY = mLowPoints[i].y + dip2px(mPointRadius + 8 + sp2px(mTemperatureTextSize));

       if (i == mTodayIndex) {
           mTextPaint.setColor(mSelectTemperatureColor);
           mCirclePaint.setColor(mSelectPointColor);
      } else {
           mTextPaint.setColor(mUnSelectTemperatureColor);
           mCirclePaint.setColor(mUnselectPointColor);
      }
       canvas.drawText(yHighText + "°", mHighPoints[i].x, highDrawY, mTextPaint);
       canvas.drawText(yLowText + "°", mLowPoints[i].x, lowDrawY, mTextPaint);
       canvas.drawCircle(mHighPoints[i].x, mHighPoints[i].y, mPointRadius, mCirclePaint);
       canvas.drawCircle(mLowPoints[i].x, mLowPoints[i].y, mPointRadius, mCirclePaint);

  }
}


private void drawLine(Point[] ps, Canvas canvas) {
   Point startp;
   Point endp;
   mPath.reset();
   mLinePaint.setAntiAlias(true);
   for (int i = 0; i < ps.length - 1; i++) {
       startp = ps[i];
       endp = ps[i + 1];
       mLinePaint.setColor(mLineColor);
       canvas.drawLine(startp.x, startp.y, endp.x, endp.y, mLinePaint);
  }
}

以上就是所有关键代码了,当然,还有一个赋值的代码

public void setData(List<WeatherInfo> list) {
   this.items = list;
   initData();
}

来看一下最后的效果图吧。

image-20220713194524550.png 以上就是一个简单的温度图了,但是这个图有很多地方可以优化,也有很多地方可以提取出来当作属性。比如我举一个优化的点,文字的测量,上面的代码对文字的测量其实是非常粗糙的。仔细观察会发现上面一条线,文字距离点的距离和下面一条线文字距离点的距离是不一样的。这就是上面没有进行文字测量的结果,我这里进行了一轮文字测量的优化,如下图: image-20220713194423946.png 这里是不是好很多了呢?大家还可以进行很多地方的优化。以上就是这篇文章的全部内容了。


作者:爱海贼的小码农
链接:https://juejin.cn/post/7119826029463470088
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

收起阅读 »

Android全局的通知的弹窗

需求分析 如何创建一个全局通知的弹窗?如下图所示。 从手机顶部划入,短暂停留后,再从顶部划出。 首先需要明确的是: 1、这个弹窗的弹出逻辑不一定是当前界面编写的,比如用户上传文件,用户可能继续浏览其他页面的内容,但是监听文件是否上传完成还是在原来的Activ...
继续阅读 »

需求分析


如何创建一个全局通知的弹窗?如下图所示。


image.png


从手机顶部划入,短暂停留后,再从顶部划出。


首先需要明确的是:

1、这个弹窗的弹出逻辑不一定是当前界面编写的,比如用户上传文件,用户可能继续浏览其他页面的内容,但是监听文件是否上传完成还是在原来的Activity,但是Dialog的弹出是需要当前页面的上下文Context的。


2、Dialog弹窗必须支持手势,用户在Dialog上向上滑时,Dialog需要退出,点击时可能需要处理点击事件。


一、Dialog的编写


/**
* 通知的自定义Dialog
*/
class NotificationDialog(context: Context, var title: String, var content: String) :
Dialog(context, R.style.dialog_notifacation_top) {

private var mListener: OnNotificationClick? = null
private var mStartY: Float = 0F
private var mView: View? = null
private var mHeight: Int? = 0

init {
mView = LayoutInflater.from(context).inflate(R.layout.common_layout_notifacation, null)
}


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mView!!)
window?.setGravity(Gravity.TOP)
val layoutParams = window?.attributes
layoutParams?.width = ViewGroup.LayoutParams.MATCH_PARENT
layoutParams?.height = ViewGroup.LayoutParams.WRAP_CONTENT
layoutParams?.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
window?.attributes = layoutParams
window?.setWindowAnimations(R.style.dialog_animation)
//按空白处不能取消
setCanceledOnTouchOutside(false)
//初始化界面数据
initData()
}

private fun initData() {
val tvTitle = findViewById<TextView>(R.id.tv_title)
val tvContent = findViewById<TextView>(R.id.tv_content)
if (title.isNotEmpty()) {
tvTitle.text = title
}

if (content.isNotEmpty()) {
tvContent.text = content
}
}


override fun onTouchEvent(event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
if (isOutOfBounds(event)) {
mStartY = event.y
}
}

MotionEvent.ACTION_UP -> {
if (mStartY > 0 && isOutOfBounds(event)) {
val moveY = event.y
if (abs(mStartY - moveY) >= 20) { //滑动超过20认定为滑动事件
//Dialog消失
} else { //认定为点击事件
//Dialog的点击事件
mListener?.onClick()
}
dismiss()
}
}
}
return false
}

/**
* 点击是否在范围外
*/
private fun isOutOfBounds(event: MotionEvent): Boolean {
val yValue = event.y
if (yValue > 0 && yValue <= (mHeight ?: (0 + 40))) {
return true
}
return false
}


private fun setDialogSize() {
mView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
mHeight = v?.height
}
}

/**
* 显示Dialog但是不会自动退出
*/
fun showDialog() {
if (!isShowing) {
show()
setDialogSize()
}
}

/**
* 显示Dialog,3000毫秒后自动退出
*/
fun showDialogAutoDismiss() {
if (!isShowing) {
show()
setDialogSize()
//延迟3000毫秒后自动消失
Handler(Looper.getMainLooper()).postDelayed({
if (isShowing) {
dismiss()
}
}, 3000L)
}
}

//处理通知的点击事件
fun setOnNotificationClickListener(listener: OnNotificationClick) {
mListener = listener
}

interface OnNotificationClick {
fun onClick()
}
}

Dialog的主题


<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">

<style name="dialog_notifacation_top">
<item name="android:windowIsTranslucent">true</item>
<!--设置背景透明-->
<item name="android:windowBackground">@android:color/transparent</item>
<!--设置dialog浮与activity上面-->
<item name="android:windowIsFloating">true</item>
<!--去掉背景模糊效果-->
<item name="android:backgroundDimEnabled">false</item>
<item name="android:windowNoTitle">true</item>
<!--去掉边框-->
<item name="android:windowFrame">@null</item>
</style>


<style name="dialog_animation" parent="@android:style/Animation.Dialog">
<!-- 进入时的动画 -->
<item name="android:windowEnterAnimation">@anim/dialog_enter</item>
<!-- 退出时的动画 -->
<item name="android:windowExitAnimation">@anim/dialog_exit</item>
</style>

</resources>

Dialog的动画


<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="600"
android:fromYDelta="-100%p"
android:toYDelta="0%p" />
</set>

<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="300"
android:fromYDelta="0%p"
android:toYDelta="-100%p" />
</set>

Dialog的布局,通CardView包裹一下就有立体阴影的效果


<androidx.cardview.widget.CardView
android:id="@+id/cd"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/size_15dp"
app:cardCornerRadius="@dimen/size_15dp"
app:cardElevation="@dimen/size_15dp"
app:layout_constraintTop_toTopOf="parent">

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/et_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/size_15dp"
app:layout_constraintTop_toTopOf="parent">

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#000000"
android:textSize="@dimen/font_14sp" android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/size_15dp"
android:textColor="#333"
android:textSize="@dimen/font_12sp"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_title" />


</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.cardview.widget.CardView>

二、获取当前显示的Activity的弱引用


/**
* 前台Activity管理类
*/
class ForegroundActivityManager {

private var currentActivityWeakRef: WeakReference<Activity>? = null

companion object {
val TAG = "ForegroundActivityManager"
private val instance = ForegroundActivityManager()

@JvmStatic
fun getInstance(): ForegroundActivityManager {
return instance
}
}


fun getCurrentActivity(): Activity? {
var currentActivity: Activity? = null
if (currentActivityWeakRef != null) {
currentActivity = currentActivityWeakRef?.get()
}
return currentActivity
}


fun setCurrentActivity(activity: Activity) {
currentActivityWeakRef = WeakReference(activity)
}

}

监听所有Activity的生命周期


class AppLifecycleCallback:Application.ActivityLifecycleCallbacks {

companion object{
val TAG = "AppLifecycleCallback"
}

override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
//获取Activity弱引用
ForegroundActivityManager.getInstance().setCurrentActivity(activity)
}

override fun onActivityStarted(activity: Activity) {
}

override fun onActivityResumed(activity: Activity) {
//获取Activity弱引用
ForegroundActivityManager.getInstance().setCurrentActivity(activity)
}

override fun onActivityPaused(activity: Activity) {
}

override fun onActivityStopped(activity: Activity) {
}

override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
}

override fun onActivityDestroyed(activity: Activity) {
}
}

在Application中注册


//注册Activity生命周期
registerActivityLifecycleCallbacks(AppLifecycleCallback())

三、封装和使用


/**
* 通知的管理类
* example:
* //发系统通知
* NotificationControlManager.getInstance()?.notify("文件上传完成", "文件上传完成,请点击查看详情")
* //发应用内通知
* NotificationControlManager.getInstance()?.showNotificationDialog("文件上传完成","文件上传完成,请点击查看详情",
* object : NotificationControlManager.OnNotificationCallback {
* override fun onCallback() {
* Toast.makeText(this@MainActivity, "被点击了", Toast.LENGTH_SHORT).show()
* }
* })
*/

class NotificationControlManager {

private var autoIncreament = AtomicInteger(1001)
private var dialog: NotificationDialog? = null

companion object {
const val channelId = "aaaaa"
const val description = "描述信息"

@Volatile
private var sInstance: NotificationControlManager? = null


@JvmStatic
fun getInstance(): NotificationControlManager? {
if (sInstance == null) {
synchronized(NotificationControlManager::class.java) {
if (sInstance == null) {
sInstance = NotificationControlManager()
}
}
}
return sInstance
}
}


/**
* 是否打开通知
*/
fun isOpenNotification(): Boolean {
val notificationManager: NotificationManagerCompat =
NotificationManagerCompat.from(
ForegroundActivityManager.getInstance().getCurrentActivity()!!
)
return notificationManager.areNotificationsEnabled()
}


/**
* 跳转到系统设置页面去打开通知,注意在这之前应该有个Dialog提醒用户
*/
fun openNotificationInSys() {
val context = ForegroundActivityManager.getInstance().getCurrentActivity()!!
val intent: Intent = Intent()
try {
intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS

//8.0及以后版本使用这两个extra. >=API 26
intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
intent.putExtra(Settings.EXTRA_CHANNEL_ID, context.applicationInfo.uid)

//5.0-7.1 使用这两个extra. <= API 25, >=API 21
intent.putExtra("app_package", context.packageName)
intent.putExtra("app_uid", context.applicationInfo.uid)

context.startActivity(intent)
} catch (e: Exception) {
e.printStackTrace()

//其他低版本或者异常情况,走该节点。进入APP设置界面
intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
intent.putExtra("package", context.packageName)

//val uri = Uri.fromParts("package", packageName, null)
//intent.data = uri
context.startActivity(intent)
}
}

/**
* 发通知
* @param title 标题
* @param content 内容
* @param cls 通知点击后跳转的Activity,默认为null跳转到MainActivity
*/
fun notify(title: String, content: String, cls: Class<*>) {
val context = ForegroundActivityManager.getInstance().getCurrentActivity()!!
val notificationManager =
context.getSystemService(AppCompatActivity.NOTIFICATION_SERVICE) as NotificationManager
val builder: Notification.Builder
val intent = Intent(context, cls)
val pendingIntent: PendingIntent? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
} else {
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_ONE_SHOT)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationChannel =
NotificationChannel(channelId, description, NotificationManager.IMPORTANCE_HIGH)
notificationChannel.enableLights(true);
notificationChannel.lightColor = Color.RED;
notificationChannel.enableVibration(true);
notificationChannel.vibrationPattern =
longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)
notificationManager.createNotificationChannel(notificationChannel)
builder = Notification.Builder(context, channelId)
.setSmallIcon(R.drawable.jpush_notification_icon)
.setContentIntent(pendingIntent)
.setContentTitle(title)
.setContentText(content)
} else {
builder = Notification.Builder(context)
.setSmallIcon(R.drawable.jpush_notification_icon)
.setLargeIcon(
BitmapFactory.decodeResource(
context.resources,
R.drawable.jpush_notification_icon
)
)
.setContentIntent(pendingIntent)
.setContentTitle(title)
.setContentText(content)

}
notificationManager.notify(autoIncreament.incrementAndGet(), builder.build())
}


/**
* 显示应用内通知的Dialog,需要自己处理点击事件。listener默认为null,不处理也可以。dialog会在3000毫秒后自动消失
* @param title 标题
* @param content 内容
* @param listener 点击的回调
*/
fun showNotificationDialog(
title: String,
content: String,
listener: OnNotificationCallback? = null
) {
val activity = ForegroundActivityManager.getInstance().getCurrentActivity()!!
dialog = NotificationDialog(activity, title, content)
if (Thread.currentThread() != Looper.getMainLooper().thread) { //子线程
activity.runOnUiThread {
showDialog(dialog, listener)
}
} else {
showDialog(dialog, listener)
}
}

/**
* show dialog
*/
private fun showDialog(
dialog: NotificationDialog?,
listener: OnNotificationCallback?
) {
dialog?.showDialogAutoDismiss()
if (listener != null) {
dialog?.setOnNotificationClickListener(object :
NotificationDialog.OnNotificationClick {
override fun onClick() = listener.onCallback()
})
}
}

/**
* dismiss Dialog
*/
fun dismissDialog() {
if (dialog != null && dialog!!.isShowing) {
dialog!!.dismiss()
}
}


interface OnNotificationCallback {
fun onCallback()
}

}

另外需要注意的点是,因为dialog是延迟关闭的,可能用户立刻退出Activity,导致延迟时间到时dialog退出时报错,解决办法可以在BaseActivity的onDestroy方法中尝试关闭Dialog:


override fun onDestroy() {
super.onDestroy()
NotificationControlManager.getInstance()?.dismissDialog()
}

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

Android Studio Debug:编码五分钟,调试俩小时

前言 整理并积累Android开发过程中用到的一些调试技巧,通过技巧性的调试技能,辅助增强代码的健壮性、安全性、正确性 案例一:抛出明显异常 常见的:除数为0问题 class MainActivty : AppCompatActivity(){ o...
继续阅读 »

前言


整理并积累Android开发过程中用到的一些调试技巧,通过技巧性的调试技能,辅助增强代码的健壮性、安全性、正确性


案例一:抛出明显异常



  • 常见的:除数为0问题


class MainActivty : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

button.setOnClickListener {
val i = 1/0
}
}
}

image.png



会提示错误原因,并告知在哪一行




  • 一般错误


class MainActivty : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

button.setOnClickListener {
val s = "Candy" //假设此处是在一个方法内,我们无法看到
var i = 0
i = s.toInt()
}
}
}

image.png



会提示错误原因,并告知在哪一行


错误原因可能不认识,直接找错误关键字,检索百度



案例二:逻辑问题



  • println()方式调试


class MainActivty : AppCompatActivity(){
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

button.setOnClickListener {
var res = 0
for (i in 1 until 10){
res=+i //该处将逻辑故意写错 应为 +=
println("i=${i},res=${res}")
}
val s:String = StringTo(res)
Toast.makeText(this,s,Toat.LENGTH.SHORT).show()
}
}
private fun StringTo(res:String){
println("将Int转换成String")
resturn res.roString()
}
}

image.png



会掺杂其他方法日志




  • log方式调试


class MainActivty : AppCompatActivity(){
val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

button.setOnClickListener {
var res = 0
for (i in 1 until 10){
res=+i //该处将逻辑故意写错 应为 +=
Log.d(TAG,"i=${i},res=${res}")
}
val s:String = StringTo(res)
Toast.makeText(this,s,Toat.LENGTH.SHORT).show()
}
}
private fun StringTo(res:String){
println("将Int转换成String")
resturn res.roString()
}
}

image.png



筛选条件多:Debug、Info、Worn、Error以及自定义筛选等


可以直接根据key筛选


调试数据较多时,不方便查看,不够灵活




  • debug模式调试


image.png



  • resume progrem: 继续执行

  • step over: 跳入下一行

  • step into: 进入自定义方法,非方法则下一行

  • force step into:进入所有方法,非方法则下一行

  • step out: 跳出方法,且方法执行完成

  • run to cursor: 跳入逻辑的下一个标记点
    image.png



debug运行时,会出现提示框,无需操作



案例三:代码丢失||项目问题



  • history

    • 不小心删除代码/文件且已save并退出
      右击项目 -> Local History -> Show History -> 选择某一历史右键 -> Revert




image.png


image.png


image.png


image.png


image.png


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