Android View滑动处理大法
对于触控式操作来说,滑动是一个特别重要的手势操作,如何做到让应用程序的页面滑动起来如丝般顺滑,让用户感觉到手起刀落的流畅感,是开发人猿需要重点解决的问题,这对提升用户体验是最为重要的事情。本文就将探讨一下,Android中View的滑动相关知识,以及如何做到丝般顺滑。
如何让View滑动起来
View的滑动是GUI支持的一项基本特性,就像触摸事件一件,这是废话,平台如果不支持,你还搞个毛线。
View滑动的基本原理
我们先来看一下Android中实现View的滑动的基本原理。其实屏幕并没有动啊,一个View的可绘制区域,对于屏幕来说,对于view tree来说都是没有变化 的。父布局给某一个View的绘制区域是在layout之后就确定好了的,当View的真实高度或者宽度超过了这块可绘制区域,那么就需要滑动才可以把整个View做到用户可见。View内部通过两个关键成员变量mScrollX和mScrollY来记录滑动之后的坐标,View本身有mLeft和mTop来标识自己相对于父布局的坐标位置,那么当有滑动的时候,在此View当中具体要绘制的区域就变成了以mLeft+mScrollX和mTop+mScrollY为起点的区域了。由此View便滚动起来了。
如何实现View的滑动
对于开发人猿来说,实现View的滑动,需要关注三个重要的方法,也即是View#scrollBy,View#scrollTo以及View#onScrollChanged,这是实现滑动的三个最为核心的方法。
scrollBy提供的参数是需要滑动的距离,而scrollTo则是需要传入要滑动到的目标坐标值,这两个方法都是要修改mScrollX和mScrollY的值,本质上是一样的。而onScrollChanged则是一个回调,用以通知更新了的滑动位置。
Scroll手势
要想让View滑动起来,离不开事件手势的支持。最简单也是最直接的手势就是onScroll手势,这个在GestureDetecor中可以识别出此手势,或者自己去直接处理touch event也可以得出此手势。这个并不复杂,就是直接通过touch 事件来计算滑动多少距离就好了,按照View预设计的可以滑动的方向,比如横向就计算不同时间点MotionEvent的坐标值,得到一个水平距离deltaX,然后调用scrollBy即可。垂直方向依此类推。
Scroll手势简单是因为它是直接来源于事件,且速度较慢,并不需要额外处理,所以整体逻辑处理流程并不复杂。
在GestureDetector中的识别就是在ACTION_MOVE时,查看滑动过的距离,这个距离(由sqrt(dx x dx, dy x dy)如果大于touch slop,就会触发onScroll手势回调。
Fling手势
Fling也即是快速滑动,就是手指在屏幕上使劲的『挠』一下,手势的要点是手指在屏幕快速滑过一小段短距离,就像把一个小球弹出去的感觉一样。对于Fling手势来说,最重要的是速度,水平方向的速度和垂直方向的速度,可以理解为高中物理常讲到的平抛运动一样。
GestureDetector识别Fling的逻辑是,在ACTION_UP时,检查此次事件的速度,如果水平方向速度或者垂直方向速度超过了阈值,便会触发Fling手势回调。
注意:留意Scroll与Fling的区别,Scroll是慢的,不关心时间与速度,只关心滑动的距离,是在ACTION_MOVE时,手指并未有离开屏幕时就触发了,只要是ACTION_MOVE还在继续,就会继续触发onScroll,并且ACTION_UP时终止整个Scroll,而Fling只关心速度,不关心距离,是在ACTION_UP时,手指离开了屏幕了(此次事件流处理结了)才会触发。
VelocityTracker
Fling事件速度是决定性的,仔细看GestureDetector的处理过程会发现它使用了一个叫做VelocityTracker的对象,来帮忙处理一些关于速度的具体逻辑,那么有必要深入了解一下这个对象。
VelocityTracker使用起来并不复杂,获取它的一个对象后,只需要不断的把MotionEvent塞给它就可以了,然后在需要的时候让其计算两个方向上的速度,然后就没有然后了:
velocityTracker = VelocityTracker.obtain();
onTouchEvent(MotionEvent ev) {
velocityTracker.addMovement(ev);
if (want to know velocities) {
velocityTracker.computeCurrentVelocity(100);
vx = velocityTracker.getXVelocity();
vy = veolocityTracker.getYVelocity();
be happy with vx and vy.
}
}
这个类的实现,值得仔细看一下,它主要的实现都是用JNI去实现,可能是因为计算方式较复杂,所以computeCurrentVelocity方法也说明了,让你真用的时候再调,这个不用去管细节实现。重点看一下这个类,里面有一个对象池,用以缓存对象,并且创建对象的方式并不是直接new,而是用其obtain方法。这里用的是叫享元(Flyweight Pattern)的设计模式,也就是说VelocityTracker对象其实是共享的。
顺滑如丝
前面提到了,让View滑动,只需要调用scrollBy或者scrollTo即可,但这个吧,是直接修改了mScrollX,mScrollY,然后invalidate,View下次draw时就直接在把目标区域内容绘制出来了,换句话说这两个方法滑动是瞬间跳格式的。
一般来说,这也没有问题,就像onScroll手势,ACTION_MOVE时,不断的scrollBy刚刚滑过的距离,都还okay,没有什么问题。
但是对于Fling事件就不行了,Fling事件,也即快速滑动,要求短时间内进行大距离滑动,或者像有跳转的需求时,也是短时间内要滑动大距离。如果直接scrollBy或者scrollTo一步到位了,会显得 相当的突兀,体验相当不好,卡顿感特别强。如果能像做动画那样,在一定时间内,让其平滑的滑动,就会如丝般顺滑,体验好很多。Scroller就是专门用来解决此问题的。
Scroller
Scroller是对滑动的封装,并不是View的子类,其实它跟View一点关系也没有,也不能操作View,实际上它与属性动画类似,它仅是一个滚动位置的计算器,告诉它起始位置和要滚动的距离,然后它就会告诉你位置随时间变化的值。其实这是一个中学物理题,也即给定初始位置,给定要滚动的距离,以一定的方式来计算每个时间点的位置。具体的计算方式由mInterpolater成员来控制,默认是ViscousFluid,是按自然指数为减速度来计算的,具体的可以查看Scroller的源码。如果不喜欢默认的计算方式,可以自己实现个Interpolator,然后在构造时传进去。
Scroller的作用在于实现平稳滑动,不让View的滚动出现跳跃,比如滑动一下ListView,开始滑动时的位置是x0,y0(ActionDown的位置),要向下滑动比如500个像素,不平稳的意思是,从x0,一下跳到x0+500的位置。要平稳,就要不断的一点点的改变x的值然后invalidate,这也就是Scroller的典型使用场景:
Scroller scroller = new Scroller(getContext());
scroller.startScroll(x0, y0, 500, 0);
然后在computeScroll时:
if (scroller.computeScrollOffset()) {
int currX = scroller.getCurrX();
int currY = scroller.getCurrY();
invalidate(); // with currX and currY
}
computeScrollOffset在滚动没结束时返回true,也就是说你需要继续刷新view。返回false时表明滚动结束了,当然也就没有必要再刷新view(当然如果你乐意也可以继续刷,但是位置啥的都不变了,所以刷了也白刷)。
滑动冲突处理
关于View的滑动,最难搞的问题便是手势冲突处理,特别是当页面的结构变得复杂了以后。一般来讲,滑动手势,是让某一个View沿着某一个方向『平移』一段距离,如果某一个页面中只有一个View是可以滑动的,或者页面中不同的View的可滑动方向是垂直正交的,那么就不会有冲突的问题。
所谓滑动冲突,是指父View和子View都接受滑动手势,并且方向又是一样的,这时就产生了滑动冲突,常见就是ScrollView中套着ListView(这个通常是垂直Y方向上面有滑动冲突),或者ViewPager中套着ScrollView(这个是水平X方向上有滑动冲突)。
要想解决好滑动冲突问题,需要先确实好整体的设计方案,有了大的原则后,就容易用技术方案找到解法。最理想的方案,也是目前用的最多的方案就是在子View的边界设定一个margin区域,当ACTION_DOWN在margin区域以外,认定滑动手势归父View处理,否则交由子View处理。像一些全局手势也是要用如此的方案,当点击距离屏幕一定范围内(margin区域)认定此事件归当前页面处理,否则就认定为全局手势,就好比从屏幕左边向右滑动,很多应该将此识别为BACK到上一页,但如果离左边较远时滑动,就会是页面内部的滑动事件(假如它有可滑动的组件的话,事件手势会被其滑消耗掉)。
链接:https://juejin.cn/post/7254092954431995964
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
VUE3基础学习(四)事件处理
监听事件
我们可以使用 v-on
指令 (简写为 @
) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="handler"
或 @click="handler"
。
事件处理器 (handler) 的值可以是:
- 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与
onclick
类似)。 - 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。
内联事件处理器
内联事件处理器通常用于简单场景,例如:
data() {
return {
count: 0
}
}
<button @click="count++">Add 1</button>
<p>Count is: {{ count }}</p>
方法事件处理器
随着事件处理器的逻辑变得愈发复杂,内联代码方式变得不够灵活。因此 v-on
也可以接受一个方法名或对某个方法的调用。
data() {
return {
name: 'Vue.js'
}
},
methods: {
greet(event) {
// 方法中的 `this` 指向当前活跃的组件实例
alert(`Hello ${this.name}!`)
// `event` 是 DOM 原生事件
if (event) {
alert(event.target.tagName)
}
}
}
<!-- `greet` 是上面定义过的方法名 -->
<button @click="greet">Greet</button>
方法与内联事件判断
模板编译器会通过检查 v-on
的值是否是合法的 JavaScript 标识符或属性访问路径来断定是何种形式的事件处理器。举例来说,foo
、foo.bar
和 foo['bar']
会被视为方法事件处理器,而 foo()
和 count++
会被视为内联事件处理器。
在内联处理器中调用方法
除了直接绑定方法名,你还可以在内联事件处理器中调用方法。这允许我们向方法传入自定义参数以代替原生事件:
methods: {
say(message) {
alert(message)
}
}
<button @click="say('hello')">Say hello</button>
<button @click="say('bye')">Say bye</button>
在内联事件处理器中访问事件参数
有时我们需要在内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event
变量,或者使用内联箭头函数:
<!-- 使用特殊的 $event 变量 -->
<button @click="warn('Form cannot be submitted yet.', $event)">
Submit
</button>
<!-- 使用内联箭头函数 -->
<button @click="(event) => warn('Form cannot be submitted yet.', event)">
Submit
</button>
methods: {
warn(message, event) {
// 这里可以访问 DOM 原生事件
if (event) {
event.preventDefault()
}
alert(message)
}
}
事件修饰符
在处理事件时调用 event.preventDefault()
或 event.stopPropagation()
是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。
为解决这一问题,Vue 为 v-on
提供了事件修饰符。修饰符是用 .
表示的指令后缀,包含以下这些:
.stop
.prevent
.self
.capture
.once
.passive
<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>
<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>
<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>
<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>
<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>
例子说明:
stop
阻止了事件冒泡,相当于调用了event.stopPropagation方法
<div @click="shout(2)">
<button @click.stop="shout(1)">ok</button>
</div>
点击shout只输出1,不会触发shout(2)
prevent
单击“提交”按钮,阻止其提交表单
阻止了事件的默认行为,相当于调用了event.preventDefault方法
<form v-on:submit.prevent="onSubmit"></form>
self
只当在 event.target 是当前元素自身时触发处理函数
<div v-on:click.self="doThat">...</div>
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有的点击,而 v-on:click.self.prevent 只会阻止对元素自身的点击
once
绑定了事件以后只能触发一次,第二次就不会触发
<button @click.once="shout(1)">ok</button>
capture
使事件触发从包含这个元素的顶层开始往下触发
<div @click.capture="shout(1)">
obj1
<div @click.capture="shout(2)">
obj2
<div @click="shout(3)">
obj3
<div @click="shout(4)">
obj4
</div>
</div>
</div>
</div>
passive
在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符
<div v-on:scroll.passive="onScroll">...</div>
native
让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件
按键修饰符
在监听键盘事件时,我们经常需要检查特定的按键。Vue 允许在 v-on
或 @
监听按键事件时添加按键修饰符。
<!-- 仅在 `key` 为 `Enter` 时调用 `submit` -->
<input @keyup.enter="submit" />
你可以直接使用 [KeyboardEvent.key
]
<input @keyup.page-down="onPageDown" />
在上面的例子中,仅会在 $event.key
为 'PageDown'
时调用事件处理。
按键别名
Vue 为一些常用的按键提供了别名:
.enter
.tab
.delete
(捕获“Delete”和“Backspace”两个按键).esc
.space
.up
.down
.left
.right
系统按键修饰符
你可以使用以下系统按键修饰符来触发鼠标或键盘事件监听器,只有当按键被按下时才会触发。
.ctrl
.alt
.shift
.meta
例子:
<!-- Alt + Enter --> <input @keyup.alt.enter="clear" /> <!-- Ctrl + 点击 -->
<div @click.ctrl="doSomething">Do something</div>
.exact
修饰符
.exact
修饰符允许控制触发一个事件所需的确定组合的系统按键修饰符。
<!-- 当按下 Ctrl 时,即使同时按下 Alt 或 Shift 也会触发 -->
<button @click.ctrl="onClick">A</button>
<!-- 仅当按下 Ctrl 且未按任何其他键时才会触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 仅当没有按下任何系统按键时触发 -->
<button @click.exact="onClick">A</button>
鼠标按键修饰符
.left
.right
.middle
这些修饰符将处理程序限定为由特定鼠标按键触发的事件。
链接:https://juejin.cn/post/7254384497659707451
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
VUE3基础学习(三)Class,Style,条件渲染,列表渲染
class 绑定
数据
data() { return { isActive: true, hasError: false } }
模板:
<div :text-danger': hasError }" ></div>
渲染:
<div></div>
计算属性:
computed: {
classObject()
{
return
{
active: this.isActive && !this.error,
'text-danger': this.error && this.error.type === 'fatal'
}
}
}
绑定数组
<div :></div>
数组中嵌套对象
<div :></div>
在组件上使用
<!-- 子组件模板 -->
<p class="foo bar">Hi!</p>
在使用时添加一些 class:
<!-- 在使用组件时 -->
<MyComponent class="baz boo" />
渲染出的 HTML 为:
<p class="foo bar baz boo">Hi!</p>
如果你的组件有多个根元素,你将需要指定哪个根元素来接收这个 class。你可以通过组件的 $attrs
属性来实现指定:
<!-- MyComponent 模板使用 $attrs 时 -->
<p :class="$attrs.class">Hi!</p>
<span>This is a child component</span>
<MyComponent class="baz" />
这将被渲染为:
<p>Hi!</p>
<span>This is a child component</span>
绑定内联Style
data() {
return {
activeColor: 'red',
fontSize: 30
}
}
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
直接绑定一个样式对象
data() {
return {
styleObject: {
color: 'red',
fontSize: '13px'
}
}
}
<div :style="styleObject"></div>
绑定一个包含多个样式对象的数组
<div :style="[baseStyles, overridingStyles]"></div>
样式多值
你可以对一个样式属性提供多个 (不同前缀的) 值,举例来说:
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
数组仅会渲染浏览器支持的最后一个值。在这个示例中,在支持不需要特别前缀的浏览器中都会渲染为 display: flex
。
条件渲染
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。
<h1 v-if="awesome">Vue is awesome!</h1>
v-else
你也可以使用 v-else
为 v-if
添加一个“else 区块”。
<button @click="awesome = !awesome">Toggle</button>
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 😢</h1>
一个
v-else
元素必须跟在一个v-if
或者v-else-if
元素后面,否则它将不会被识别。
v-else-if
顾名思义,v-else-if
提供的是相应于 v-if
的“else if 区块”。它可以连续多次重复使用:
<div v-if="type === 'A'">
A
</div>
<div v-else-if="type === 'B'">
B
</div>
<div v-else-if="type === 'C'">
C
</div>
<div v-else>
Not A/B/C
</div>
和
v-else
类似,一个使用v-else-if
的元素必须紧跟在一个v-if
或一个v-else-if
元素后面。
<template>
上的 v-if
<template v-if="ok">
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</template>
v-else
和 v-else-if
也可以在 <template>
上使用。
v-show
另一个可以用来按条件显示一个元素的指令是 v-show
。其用法基本一样:
<h1 v-show="ok">Hello!</h1>
不同之处在于 v-show
会在 DOM 渲染中保留该元素;v-show
仅切换了该元素上名为 display
的 CSS 属性。
v-show
不支持在 <template>
元素上使用,也不能和 v-else
搭配使用。
v-if
对比 v-show
v-if
是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。
v-if
也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。
相比之下,v-show
简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display
属性会被切换。
总的来说,
v-if
有更高的切换开销,而v-show
有更高的初始渲染开销。因此,如果需要频繁切换,则使用v-show
较好;如果在运行时绑定条件很少改变,则v-if
会更合适
列表渲染
v-for
我们可以使用 v-for
指令基于一个数组来渲染一个列表。v-for
指令的值需要使用 item in items
形式的特殊语法,其中 items
是源数据的数组,而 item
是迭代项的别名:
data() {
return {
items: [{ message: 'Foo' }, { message: 'Bar' }]
}
}
<li v-for="item in items">
{{ item.message }}
</li>
在 v-for
块中可以完整地访问父作用域内的属性和变量。v-for
也支持使用可选的第二个参数表示当前项的位置索引。
data() {
return {
parentMessage: 'Parent',
items: [{ message: 'Foo' }, { message: 'Bar' }]
}
}
<li v-for="(item, index) in items">
{{ parentMessage }} - {{ index }} - {{ item.message }}
</li>
v-for
变量的作用域和下面的 JavaScript 代码很类似:
const parentMessage = 'Parent'
const items = [
/* ... */
]
items.forEach((item, index) => {
// 可以访问外层的 `parentMessage`
// 而 `item` 和 `index` 只在这个作用域可用
console.log(parentMessage, item.message, index)
})
注意 v-for
是如何对应 forEach
回调的函数签名的。实际上,你也可以在定义 v-for
的变量别名时使用解构,和解构函数参数类似:
<li v-for="{ message } in items">
{{ message }}
</li>
<!-- 有 index 索引时 -->
<li v-for="({ message }, index) in items">
{{ message }} {{ index }}
</li>
对于多层嵌套的 v-for
,作用域的工作方式和函数的作用域很类似。每个 v-for
作用域都可以访问到父级作用域:
<li v-for="item in items">
<span v-for="childItem in item.children">
{{ item.message }} {{ childItem }}
</span>
</li>
你也可以使用 of
作为分隔符来替代 in
,这更接近 JavaScript 的迭代器语法:
<div v-for="item of items"></div>
v-for
与对象
你也可以使用 v-for
来遍历一个对象的所有属性。遍历的顺序会基于对该对象调用 Object.keys()
的返回值来决定。
data() {
return {
myObject: {
title: 'How to do lists in Vue',
author: 'Jane Doe',
publishedAt: '2016-04-10'
}
}
}
<ul>
<li v-for="value in myObject">
{{ value }}
</li>
</ul>
可以通过提供第二个参数表示属性名 (例如 key):
<li v-for="(value, key) in myObject">
{{ key }}: {{ value }}
</li>
第三个参数表示位置索引:
<li v-for="(value, key, index) in myObject">
{{ index }}. {{ key }}: {{ value }}
</li>
在 v-for
里使用范围值
v-for
可以直接接受一个整数值。在这种用例中,会将该模板基于 1...n
的取值范围重复多次。
<span v-for="n in 10">{{ n }}</span>
注意此处 n
的初值是从 1
开始而非 0
。
<template>
上的 v-for
与模板上的 v-if
类似,你也可以在 <template>
标签上使用 v-for
来渲染一个包含多个元素的块。例如:
<ul>
<template v-for="item in items">
<li>{{ item.msg }}</li>
<li class="divider" role="presentation"></li>
</template>
</ul>
通过 key 管理状态
Vue 默认按照“就地更新”的策略来更新通过 v-for
渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。
<div v-for="item in items" :key="item.id"> <!-- 内容 --> </div>
必要性
vue在patch过程中判断两个节点是否为相同节点,key是一个必要条件,渲染一组列表时key往往是唯一标识,如果不定义key,vue会认为比较的两个节点是同一个(哪怕不是),这会导致频繁更新元素,使得整个patch过程比较低效,影响性能
实际使用方式
实际使用中在渲染一组列表时key必须设置,而且是唯一标识,应避免使用数组索引作为key,因为这可能会导致一些隐藏的bug;vue在相同标签元素过渡切换的时候也会使用可以,目的也是让vue能够区分它们,否则vue只会替换其内部属性,而不会触发过渡效果
数组变化侦测
变更方法
Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
在计算属性中使用
reverse()
和sort()
的时候务必小心!这两个方法将变更原始数组,计算函数中不应该这么做。请在调用这些方法之前创建一个原数组的副本:
- return numbers.reverse() + return [...numbers].reverse()
链接:https://juejin.cn/post/7254169497201049660
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
VUE3基础学习(二)模板语法,简单响应式,计算属性
说明:Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。
一、模板语法
{{}}使用
<span>Message: {{ msg }}</span>
{{}} 标识的数据将会从数据中获取,同时每次 msg
属性更改时它也会同步更新。
v-html
<p>Using v-html directive: <span v-html="rawHtml"></span></p>
若想插入 HTML,你需要使用 v-html 指令
Attribute 绑定
<div v-bind:id="dynamicId"></div>
简写 <div :id="dynamicId"></div>
布尔型 Attribute
<button :disabled="isButtonDisabled">Button</button>
当 isButtonDisabled
为[真值]或一个空字符串 (即 <button disabled="">
) 时,元素会包含这个 disabled
attribute。而当其为其他[假值]时 attribute 将被忽略。
动态绑定多个值
如果你有像这样的一个包含多个 attribute 的 JavaScript 对象:
data() {
return {
objectOfAttrs: {
id: 'container',
class: 'wrapper'
}
}
}
通过不带参数的 v-bind
,你可以将它们绑定到单个元素上:
template
<div v-bind="objectOfAttrs"></div>
使用 JavaScript 表达式
注意:仅仅支持简单的表达式
{{ number + 1 }}
{{ ok ? 'YES' : 'NO' }}
{{ message.split('').reverse().join('') }}
<div :id="`list-${id}`"></div>
二、 响应式
如下例子:
<script>
export default {
created() {
// 生命周期钩子中创建
},
unmounted() {
// 最好是在组件卸载时
},
data() {
return {
count: 1
}
},
methods :{
increment(){
this.count ++
console.log(this.count)
}
},
// `mounted` is a lifecycle hook which we will explain later
mounted() {
// `this` refers to the component instance.
console.log(this.count) // => 1
this.increment()
}
}
</script>
<template>
<div>
<span>Count is: {{ count }}</span>
<button @click="increment">{{ count }}</button>
</div>
</template>
1、 data
选项来声明组件的响应式状态 2、要为组件添加方法,我们需要用到 methods
选项。
计算属性
说明:模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护。这时候我们需要用计算属性了。 实例:
export default {
data() {
return {
author: {
name: 'John Doe',
books: [
'Vue 2 - Advanced Guide',
'Vue 3 - Basic Guide',
'Vue 4 - The Mystery'
]
}
}
},
computed: {
// 一个计算属性的 getter
publishedBooksMessage() {
// `this` 指向当前组件实例
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
}
template
<p>Has published books:</p>
<span>{{ publishedBooksMessage }}</span>
计算属性 与 方法 对比
如上同样用方法也可以实现,但其中有什么区别么?
<p>{{ calculateBooksMessage() }}</p>
js
// 组件中
methods: {
calculateBooksMessage() {
return this.author.books.length > 0 ? 'Yes' : 'No'
}
}
若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 books
不改变,无论多少次访问 publishedBooksMessage
都会立即返回先前的计算结果,而不用重复执行 getter 函数。
链接:https://juejin.cn/post/7254043722945675321
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
微信图片防撤回
了解需求
实际生活中,由于好奇朋友撤回的微信图片信息,但直接去要又怎会是我的性格呢。由此萌生出做一个微信防撤回程序(已向朋友说明)。
当前网络上其实存在一些微信防撤回程序,不过担心不正规软件存在漏洞,泄漏个人信息,这里也就不考虑此种方法。
解决方案
思路
由于当前微信不支持微信网页版登陆,因此使用itchat的方法不再适用。
后来了解到电脑端微信图片会先存储在本地,撤回后图片再从本地删除,因此只要在撤回前将微信本地图片转移到新文件夹即可。
在此使用Python的watchdog包来监视文件系统事件,例如文件被创建、修改、删除、移动,我们只需监听创建文件事件即可。
安装watchdog包: pip install watchdog
我的python环境为python3.9版本
实现
1.首先进行文件创建事件监听,在监听事件发生后的事件处理对象为复制微信图片到新文件夹。具体代码如下。
需要注意的是微信在2022.05前,图片存储在images目录下;在2022.05后,图片存储在MsgAttach目录下,并按微信对象分别进行存储。
# 第一步:加载路径,并实时读取JPG信息
import os
import shutil
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
def mycopyfile(srcfile,dst_dir):
if not os.path.isfile(srcfile):
print ("%s not exist!"%(srcfile))
else:
fpath,fname=os.path.split(srcfile) # 分离文件名和路径
if fname.endswith('.jpg') or fname.endswith('.png') or fname.endswith('.dat'):
dst_path = os.path.join(dst_dir, fname)
shutil.copy(srcfile, dst_path) # 复制文件
class MyEventHandler(FileSystemEventHandler):
# 文件移动
# def on_moved(self, event):
# print("文件移动触发")
# print(event)
def on_created(self, event):
# print("文件创建触发")
print(event)
mycopyfile(event.src_path, dst_dir)
# def on_deleted(self, event):
# print("文件删除触发")
# print(event)
#
# def on_modified(self, event):
# print("文件编辑触发")
# print(event)
if __name__ == '__main__':
dst_dir = r"E:\03微信防撤回\weixin" #TODO:修改为自己的保存文件目录
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)
observer = Observer() # 创建观察者对象
file_handler = MyEventHandler() # 创建事件处理对象
listen_dir = r"C:\Users\hc\Documents\WeChat" #TODO:修改为自己的监听目录
observer.schedule(file_handler, listen_dir, True) # 向观察者对象绑定事件和目录
observer.start() # 启动
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()
2.由于微信保存文件以.dat格式保存,因此需要对微信文件格式进行解码,具体解码代码如下。
# weixin_Image.dat 破解
# JPG 16进制 FF D8 FF
# PNG 16进制 89 50 4e 47
# GIF 16进制 47 49 46 38
# 微信.bat 16进制 a1 86----->jpg ab 8c----jpg dd 04 --->png
# 自动计算异或 值
import os
into_path = r'E:\03微信防撤回\weixin' # 微信image文件路径
out_path = r'E:\03微信防撤回\image'
def main(into_path, out_path):
dat_list = Dat_files(into_path) # 把路径文件夹下的dat文件以列表呈现
lens = len(dat_list)
if lens == 0:
print('没有dat文件')
exit()
num = 0
for dat_file in dat_list: # 逐步读取文件
num += 1
temp_path = into_path + '/' + dat_file # 拼接路径:微信图片路径+图片名
dat_file_name = dat_file[:-4] # 截取字符串 去掉.dat
imageDecode(temp_path, dat_file_name, out_path) # 转码函数
value = int((num / lens) * 100) # 显示进度
print('正在处理--->{}%'.format(value))
def Dat_files(file_dir):
"""
:param file_dir: 寻找文件夹下的dat文件
:return: 返回文件夹下dat文件的列表
"""
dat = []
for files in os.listdir(file_dir):
if os.path.splitext(files)[1] == '.dat':
dat.append(files)
return dat
def imageDecode(temp_path, dat_file_name, out_path):
dat_read = open(temp_path, "rb") # 读取.bat 文件
xo, j = Format(temp_path) # 判断图片格式 并计算返回异或值 函数
if j == 1:
mat = '.png'
elif j == 2:
mat = '.gif'
else:
mat = '.jpg'
out = out_path + '/' + dat_file_name + mat # 图片输出路径
png_write = open(out, "wb") # 图片写入
dat_read.seek(0) # 重置文件指针位置
for now in dat_read: # 循环字节
for nowByte in now:
newByte = nowByte ^ xo # 转码计算
png_write.write(bytes([newByte])) # 转码后重新写入
def Format(f):
"""
计算异或值
各图片头部信息
png:89 50 4e 47
gif: 47 49 46 38
jpeg:ff d8 ff
"""
dat_r = open(f, "rb")
try:
a = [(0x89, 0x50, 0x4e), (0x47, 0x49, 0x46), (0xff, 0xd8, 0xff)]
for now in dat_r:
j = 0
for xor in a:
j = j + 1 # 记录是第几个格式 1:png 2:gif 3:jpeg
i = 0
res = []
now2 = now[:3] # 取前三组判断
for nowByte in now2:
res.append(nowByte ^ xor[i])
i += 1
if res[0] == res[1] == res[2]:
return res[0], j
except:
pass
finally:
dat_r.close()
# 运行
if __name__ == '__main__':
main(into_path, out_path)
链接:https://juejin.cn/post/7221376169370583101
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
北美 2023 被裁员的感悟
不贩卖焦虑,就事论事,希望能帮助到有需要的朋友。
很多人觉得在裁员之前是没有任何迹象的,其实真的不是这样。
公司在裁员的过程中有很多要裁员的迹象,我会在另外一篇文章中对我遇到的一些裁员信号设置一些雷区和警告,当你遇到上面的这些信号的时候,直觉告诉你需要马上考虑寻找下一个替代方案了。
因为当这些信号的任何一个或者多个同时出现的时候就意味着裁员在进行中了,通常会在 3 到 6 个月左右发生。
在公司的职位
在被裁公司的职位是 Tech Lead。
虽然这个职位并不意味着你对其他同事而言能够获得更多的有效信息,但是通常可能会让自己与上级有更好的沟通管道。
但是,非常不幸的是这家公司的沟通渠道非常有问题。
因为负责相关开发部分的副总是从 PHP 转 Java 的,对 Java 的很多情况都不非常明确,所以他非常依赖一个架构师。
但,公司大部分人都认为这个架构师的要求是错误的,并且是非常愚蠢的。
比如说要求代码必须要放在一行上面,导致代码上面有不少行甚至超过了 1000 个字符。
所有开发都对这个要求非常不理解,并且多次提出这种要求是无理并且愚蠢的,我们组是对这个要求反应最激烈,并且抵触最大的(也有可能是因为我的原因,我不希望在明显错误的地方让步;我可以让步,但是需要给一个能说服的理由)。
然而,这个所谓的架构师就利用 PR 合并的权力,不停的让我们的组员进行修改。
裁员之前
正是因为在公司的职位和上面说到的和架构师直接的冲突。
在 6 个月之前,我就已经和组里的同事说要准备进行下一步了,你们该面试的就面试了,不要拖延。
在这个中间过程中,我的领导还找我谈过一次。领导的意思就是他非常同意我们的有关代码 PR 的要求,也觉得这些要求都是狗屁。
但,负责开发的副总,认为我们组现在是所有组里面最差的。
可能当时没有认真理解这句话的意思,我们组从所有组里面最好的,变成最差的只用了 2 周(一个 Sprint)的时间。
在这次谈话后,我更加坚信让我的组员找下一家的信息了,对他们中途要面试其他公司我都放开一面。
非常不幸的,我自己那该死的拖延症,我是把我自己的简历准备好了,但是还没有来得及投就等来了真正裁员的这一天。
深刻的教训和学到的经验:
如果公司的运营或者管理让感觉到不舒服,并且已经有开始寻找下家的想法的时候,一定要尽快准备,马上实施,不要拖延。
这就是我在上面标黑马上的原因。
裁员过程
裁员过程非常简单和迅速,并且在毫不知情的情况下进行。
在周四的时候,公司的高层提示所有的会议今天全部取消,并且把应该 11 点开的全公司会议提前到了 9 点。
因为很多人都没有准备,所以很多人也没有参加。
后来才知道,9 点就是宣布裁员的开始,事后知道裁员比率为 40%。
然后就是各个部门找自己的被裁的员工开会,这个会议通常首先是一个 Zoom 的 Group 会议,说了一堆屁话,就是什么这是不得已的决定呀,什么乱七八糟的东西。
当然,在这个时候你还需要或者期待公司给你什么合理的理由呢?
然后就是 P&C 人员说话,基本上就是每个人 15 分钟的时间单独 Zoom。
这个 15 分钟,主要就是读下文件了,至于 2 个会议上是不是开摄像头,随意。
你愿意开也行,不愿意开也行,反正上面的所有人都心不在焉。我是懒得开,因为和你谈话的人,你都根本不认识。
第二个会议就是 P&C,这个会议主要就是和你说说被裁员后可以有的一些福利和什么的,对我个人来说我更关注的是补偿。
至于 401K 和里面的资金都是可以自行转的,也没啥需要他们说的,了解到补偿就是 6 周工资,不算多也凑合能接受。
负责裁员的人说,还有什么需要问的,我简单的回答了下 All Set 然后 have a nice day 就好了。毕竟他们只是具体做事的人,问他们也问不出个所以然,这有啥的。
裁员之后
裁员之后,感觉所有认识的被裁的同事都是懵的。
开完 15 分钟的 P&C 会议后,基本上电脑和邮箱马上就不能用了。公司貌似说电脑可以自己留着,但是上面的数据会被远程清理掉。
留在公司里面的东西会有人收拾后寄到家里。
我在公司里的办公桌就属于离职型办公桌,简单的来说,上面只有一台不属于我的显示器,另外就是从其他地方拿过来的一盒消毒湿巾,公司里面压根没有我需要的东西。
很多人认为公司禁用账户有点太不讲人情,其实从技术层面来说根本没有什么,因为所有的管理都是 LDAP,直接在 LDAP 上禁用你账户就好了,没啥稀奇的。
中午的时候,被裁的同事都互相留下了手机号码,感觉大家因为我在裁员列表里面感觉有点扯。另外更扯的同事在这个公司工作了 7 年了,也在列表里面(所有 PHP 的基础架构都是他写的和建立的)。
虽然最开始和这个同事有过一些摩擦,但是这个印度的同事真的挺好的,我们都觉得他挺不错,也愿意和他一起共事。
很多人,包括我。都对这个同事感觉不值,也觉得这很扯。
奈何公司的选择就是一些阿谀奉承,天天扯淡的人,比如说那个奇葩的架构师。
没多久,被裁的同事建了一个群,然后把我给拉进去了,主要还是我们组里面的同事,大家希望能够分享一些面试经验和机会,偶尔吐槽下。
在晚上的时候,突然收到另外一个同事的 LinkedIn 好友邀请,他不在这次裁员内。
但是他也被降职了,他本来是 Sr 开发人员和小组长,后来被提拔成架构师了,现在连小组长都不是了。
他和我说,如果需要帮助的话,他会尽量帮忙,并且还给他的一些曾经的招聘专员账号推送给了我。
我也非常感谢他们,虽然经历过,但是也收获了一些朋友,虽然说在美国职场比较难收获朋友,但是也并不是完全这样的。
没有了利益的纠葛,更容易说点实话。
链接:https://juejin.cn/post/7240052076624035901
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
2023年35大龄程序员最后的挣扎
一、自身情况
我非科班出身,年龄到了35岁。然后剧本都差不多,2022年12月各种裁员,失业像龙卷风一样席卷社会各个角落。
- 其实30岁的时候已经开始焦虑了,并且努力想找出路。
- 提升技术,努力争增加自己的能力。
- 努力争取进入管理层,可是卷无处不在,没有人离开这个坑位,你努力的成效很低。
- 大环境我们普通人根本改变不了。
- 自己大龄性价比不高,中年危机就是客观情况。
- 无非就是在本赛道继续卷,还是换赛道卷的选择了。
啊Q精神
:我还不是最惨的那一批,最惨的是19年借钱买了恒大的烂尾楼
,并且在2021年就失业的那拨人。简直不敢想象,那真是绝望啊。心里不够坚强的,想不开轻生的念头都会有。我至少拿了点赔偿,手里还有些余粮,暂时饿不死。
二、大环境情况
大环境不好已经不是秘密了,整个经济走弱。大家不敢消费,对未来信心不足已经是板上钉钉的事了。
这剧本就是30年前日本的剧本,不敢说一摸一样。可以说大差不差了,互联网行业的薪资会慢慢的回归平均水平,或者技术要求在提升一个等级。
大部分普通人,还是做应用层拧螺丝,少部分框架师能造轮子也就是2:8理论。
能卷进这20%里,就能在上一层楼。也不是说这行就不行了,只不过变成了存量市场,而且坑位变少,人并没有变少还增加了。
不要怀疑自己的能力,这也不是你的问题了,是外部环境导致的市场萎缩。我们能做的就是,脱下孔乙己的长衫,先保证生活。努力干活,不违法乱纪做什么都是光荣了,不要带有色眼镜看待任何人。
三、未来出路
未来的出路在哪里?
这个我也很迷惑,因为大佬走的路,并不是我们这些普通的不能在普通的人能够走的通的。当然也有例外的情况,这些就是幸存者偏差了。
我先把chartGPT
给的答应贴出来:
可以看到chartGPT还是给出,相对可行有效的方案
。当然这些并不是每个人都适用。
我提几个普通人能做的建议(普通人还是围绕生存在做决策):
- 有存款的,并且了解一些行业的可以开店,比如餐饮店,花店,水果店等。
- 摆摊,国家也都改变政策了。
- 超市,配送员,外卖员。
- 开滴滴网约车。
- 有能力的,可以润出G。可以吸一吸GW“free的air”,反正都是要被ZBJ榨取的。
以上都是个人不成熟的观点,jym多多包涵。
每个行业都卷,没有很好的建议都是走一步算一步,保持学习,减少精神内耗
链接:https://juejin.cn/post/7230656455808335930
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
用Kotlin开发时,如何优化一下Lambda的开销
前言
在Kotlin中声明一个Lambda表达式,在编译字节码中会产生一个匿名类。此匿名类中有一个 invoke
方法,为Lambda的调用方法,每次调用会创建一个新匿名类对象。可想而知,Lambda语法虽简洁,但额外增加的开销也不少。还有,若Lambda捕捉某个变量,每次调用时都会创建一个新对象,会导致效率较低。
在Kotlin中采取优化Lambda额外开销的方式就是:内联函数。
回顾Java中采取的优化方式:invokedynamic
invokedynamic技术是Java7后提出,在运行期才产生相应翻译代码。
invokedynamic被首次调用时,会触发产生一个匿名类来替换中间码invokedynamic,后续调用会直接采用该匿名类代码。这种做的好处主要有:
- 具体的转换实现是在运行时产生,在字节码中只有一个固定的invokedynamic,所以需要静态生成的类的个数及字节码大小都显著减少。
- 与编译时写死在字节码中的策略不同,利用invokedynamic可把实际的翻译策略隐藏在JDK库的实现, 极大提高了灵活性,在确保向后兼容性的同时,后期可以继续对编译策略不断优化升级
- JVM天然支持针对该方式的Lambda表达式的翻译和优化,开发者在书写Lambda表达式时,可以完全不用关心这个问题,极大地提升了开发体验。
Kotlin中采取的优化方式:内联函数
Kotlin拥抱内联函数,在C++、C#等语言中也支持这种特性。可以用 inline 关键字来修饰函数,这些函数就称为内联函数。它的函数体在编译期被嵌入到每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。
内联函数的工作原理并不复杂,就是Kotlin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样就不存在运行时的开销。
若想在用Kotlin开发时获得尽可能良好的性能支持,以及控制匿名类的生成数量,就来学习下内联函数:
如以下示例:
fun main() {
foo {
println("dive into Kotlin...")
}
}
fun foo(block: () -> Unit) {
println("before block")
block()
println("end block")
}
以上声明一个高阶函数foo,接受一个Lambda 参数为 () -> Unit,最后在main函数中调用它。下面是通过字节码反编译的Java代码:
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
foo((Function0)null.INSTANCE);
}
public static final void foo(@NotNull Function0 block) {
Intrinsics.checkParameterIsNotNull(block, "block");
String var1 = "before block";
System.out.println(var1);
block.invoke();
var1 = "end block";
System.out.println(var1);
}
调用foo会产生一个Function()类型的block类,然后通过 invovke() 来执行,这样会增加额外生成类和调用开销。下面给foo函数加上inline修饰符:
inline fun foo(block: () -> Unit) {
println("before block")
block()
println("end block")
}
看看相应Java代码:
public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
String va1 = "before block";
System.out.println(var1);
// block函数体在这里开始粘贴
String var2 = "dive into Kotlin...";
System.out.println(var2);
// block函数体在这里结束粘贴
var1 = "end block";
System.out.println(var1);
}
public static final void foo(@NotNull Function0 block) {
Intrinsics.checkParameterIsNotNull(block, "block");
String var2 = "before block";
System.out.println(var2);
block.invoke();
var2 = "end block";
System.out.println(var2);
}
如上面所说,foo 函数体代码及被调用的Lambda代码都粘贴到了相应调用的位置。试想下,若是一个公共方法,或被嵌套在一个循环调用中,该方法势必会被调用很多次。通过inline函数,可以消除这种额外调用,从而节省开销。
内联函数一个典型应用场景就是Kotlin集合类。Kotlin 中集合函数式API,如map、filter都是被定义成内联函数:
inline fun <T, R> Array<out T>.map {
transform: (T) -> R
}: List<R>
inline fun <T> Array<out T>.filter {
predicate: (T) -> Boolean
}: List<T>
很容易理解,因这些方法都接收Lambda表达式参数,需要对集合元素进行遍历操作,因此把相应的实现进行内联无疑是适合的。
但内联函数不是万能的,以下情况应避免使用内联函数:
- JVM对普通函数已经能够根据实际情况智能判断是否进行内联优化,因此并不需要对其使用Kotlin的inline语法,否则只会让字节码变得更加复杂
- 尽量避免对具有大量函数体的函数进行内联,会导致过多的字节码数量
- 一个函数被定义为内联函数,就不能获取闭包类的私有成员,除非把它声明为internal
链接:https://juejin.cn/post/7233809825204568124
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Kotlin和Swift的前世一定是兄
Swift介绍
Swift这门编程语言主要用于iOS和MacOS的开发,可以说是非常流行的一门编程语言,我只想说,如果你会Kotlin,那么你学习Swift会非常容易,反之亦然。下载XCode,然后你就可以创建Playground练习Swift语法了。
playground这个名字起的好,翻译成中文就是操场,玩的地方,也就是说,你可以尽情的测试你的Swift代码。
声明变量和常量
Kotlin的写法:
var a: Int = 10
val b: Float = 20f
Swift的写法:
var a: Int = 10
let b: Float = 20.0
你会发现它俩声明变量的方式一模一样,而常量也只是关键字不一样,数据类型我们暂不考虑。
导包
Kotlin的写法:
import android.app.Activity
Swift的写法:
import SwiftUI
这里kotlin和swift的方式一模一样。
整形
Kotlin的写法:
val a: Byte = -10
val b: Short = 20
val c: Int = -30
val d: Long = 40
Swift的写法:
let a: Int8 = -10
let b: Int16 = 20
let c: Int32 = -30
let d: Int = -30
let e: Int64 = 40
let f: UInt8 = 10
let g: UInt16 = 20
let h: UInt32 = 30
let i: UInt = 30
let j: UInt64 = 40
Kotlin没有无符号整型,Swift中Int32等价于Int,UInt32等价于UInt。无符号类型代表是正数,所以没有符号。
基本运算符
Kotlin的写法:
val a: Int = 10
val b: Float = 20f
val c = a + b
Swift的写法:
let a: Int = 10
let b: Float = 20
let c = Float(a) + b
Swift中没有隐式转换,Float类型不用写f。这里Kotlin没那么严格。
逻辑分支
Kotlin的写法:
val a = 65
if (a > 60) {
}
val b = 1
when (b) {
1 -> print("b等于1")
2 -> print("b等于2")
else -> print("默认值")
}
Swift的写法:
let a = 65
if a > 60 {
}
let b = 1
switch b {
case 1:
print("b等于1")
case 2:
print("b等于2")
default:
print("默认值")
}
Swift可以省略if的括号,Kotlin不可以。switch的写法倒是有点像Java了。
循环语句
Kotlin的写法:
for (i in 0..9) {
}
Swift的写法:
for var i in 0...9 {
}
// 或
for var i in 0..<10 {
}
Kotlin还是不能省略括号。
字符串
Kotlin的写法:
val lang = "Kotlin"
val str = "Hello $lang"
Swift的写法:
let lang = "Swift"
let str = "Hello \(lang)"
字符串的声明方式一模一样,拼接方式略有不同。
数组
Kotlin的写法:
val arr = arrayOf("Hello", "JYM")
val arr2 = emptyArray<String>()
val arr3: Array<String>
Swift的写法:
let arr = ["Hello", "JYM"]
let arr2 = [String]()
let arr3: [String]
数组的写法稍微有点不同。
Map和Dictionary
Kotlin的写法:
val map = hashMapOf<String, Any>()
map["name"] = "张三"
map["age"] = 100
Swift的写法:
let dict: Dictionary<String, Any> = ["name": "张三", "age": 100]
Swift的字典声明时必须初始化。Map和Dictionary的本质都是哈希。
函数
Kotlin的写法:
fun print(param: String) : Unit {
}
Swift的写法:
func print(param: String) -> Void {
}
或
func print(param: String) -> () {
}
除了关键字和返回值分隔符不一样,其他几乎一模一样。
高阶函数和闭包
Kotlin的写法:
fun showDialog(build: BaseDialog.() -> Unit) {
}
Swift的写法:
func showDialog(build: (dialog: BaseDialog) -> ()) {
}
Kotlin的高阶函数和Swift的闭包是类似的概念,用于函数的参数也是一个函数的情况。
创建对象
Kotlin的写法:
val btn = Button(context)
Swift的写法:
let btn = UIButton()
这里kotlin和swift的方式一模一样。
类继承
Kotlin的写法:
class MainPresenter : BasePresenter {
}
Swift的写法:
class ViewController : UIViewController {
}
这里kotlin和swift的方式一模一样。
Swift有而Kotlin没有的语法
guard...else的语法,通常用于登录校验,条件不满足,就执行else的语句,条件满足,才执行guard外面的语句。
guard 条件表达式 else {
}
另外还有一个重要的语法就是元组。元祖在Kotlin中没有,但是在一些其他编程语言中是有的,比如Lua、Solidity。元组主要用于函数的返回值,可以返回一个元组合,这样就相当于函数可以返回多个返回值了。
Swift的元组:
let group = ("哆啦", 18, "全宇宙最强吹牛首席前台")
Lua的多返回值:
function group() return "a","b" end
Solidity的元组:
contract MyContract {
mapping(uint => string) public students;
function MyContract(){
students[0] = "默认姓名";
students[1] = "默认年龄";
students[2] = "默认介绍";
}
function printInfo() constant returns(string,uint,string){
return("哆啦", 18, "全宇宙最强吹牛首席前台");
}
}
总结
编程语言很多地方都是相通的,学会了面向对象编程,你学习其他编程语言就会非常容易。学习一门其他编程语言的语法是很快的,但是要熟练掌握,还需要对该编程语言的API有大量的实践。还是那句话,编程语言只是工具,你的编程思维的高度才是决定你水平的重要指标。所以我给新入行互联网的同学的建议是,你可以先学习面向对象的编程思想,不用局限于一门语言,可以多了解下其他的编程语言,选择你更喜欢的方向。选择好后,再深耕一门技术。每个人的道不一样,可能你就更适合某一个方向。
链接:https://juejin.cn/post/7248962809023316028
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Flutter这么火为什么不了解一下呢?(上)
Flutter这么火为什么不了解一下呢?(上)
Flutter是Google移动UI框架,用以创建高质量的native接口,真正跨平台,同时在iOS和Android上运行。Flutter是免费开源的,全球开发者及组织均可以使用。
Flutter有又几个特点:
1.快速开发 毫秒级的热加载快速地将修改应用到app。使用丰富的可完全自定义的组件在几分钟内就可以构建native界面。
2.极具表现力,灵活的UI 快速地将特性集中到native终端用户体验。利用分层结构可以完整地自定义UI,进而完成快速绘制及灵活的设计。
3.native性能 Flutter组件包含了所有平台的关键差异,例如滚动,导航,图标和字体。使得Flutter在iOS和Android上使用可以获得完全的native性能体验。
快速开发
Flutter热加载技术有助于你快速且简单地进行试验,构建UI,增加特性,并且快速修复bug。体验不到一秒的重新加载体验。
漂亮的UI
Flutter内置MD设计风格及iOS组件,更有丰富的手势API,流畅的滚动体验和平台认同感会让用户感到愉悦。
现代的响应式框架(Modern,reactive framework)
利用Flutter响应式框架和丰富的平台,布局和功能组件是的UI构建非常简单。使用灵活并且强大的API(2D,动画,手势,性能等)可以解决在UI上各种问题。
int counter = 0;
void increment() {
// Tells the Flutter framework that state has changed,
// so the framework can run build() and update the display.
setState(() {
counter++;
});
}
Widget build(BuildContext context) {
// This method is rerun every time setState is called.
// The Flutter framework has been optimized to make rerunning
// build methods fast, so that you can just rebuild anything that
// needs updating rather than having to individually change
// instances of widgets.
return new Row(
children: <Widget>[
new RaisedButton(
onPressed: increment,
child: new Text('Increment'),
),
new Text('Count: $counter'),
],
);
}
}
使用native特性和SDKs
我们使用平台APIs,第三方SDKs和native代码开发APP。Flutter可以让你在iOS和Android继续使用Java,Swift,Objective-C代码并且使用native特性。
访问平台特性很简单。下边的代码片段开始:
var batteryLevel = 'unknown';
try {
int result = await methodChannel.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level: $result%';
} on PlatformException {
batteryLevel = 'Failed to get battery level.';
}
setState(() {
_batteryLevel = batteryLevel;
});
}
学习如何使用包(packages),或者写platform channels,使用native代码,APIs和SDKs。
统一的开发标准
Flutter拥有工具及库帮助你简单快速地在iOS和Android上实现你的想法。若你还没有任何移动开发经验,那么Flutter将会是你构建漂亮的移动APP的一种简单快速的额方式。若你是有经验的iOS或者Android开发人员,那么你可以使用Flutter组件,并且继续使用已有的Java/Objective-C/Swift程序。
构建 漂亮的APP UI 丰富的2D GPU加速APIs 响应式框架 动画/动作 APIs 兼容Android Material组件及苹果组件样式
流程的编码体验 急速热加载技术 IntelliJ:重构,自动补足功能等 Dart语言及核心库 包管理
拥有App所有特性 与移动OS APIs&SDKs互操作性 Maven/Java Cocoapods/ObjC/Swift
优化 测试 Unit测试 继承测试 无设备测试
Debug IDE debug 基于网络debug 异步/唤醒感知 表达式求值程序
配置 时间线 CPU和内存 应用性能图标
部署 编译 Native ARM程序 消除无效代码
发布 App市场 Play Store
标题安装Flutter
在国内安装Flutter需要首先需要一个值得信任的国内镜像。在镜像上边保存着Flutter需要的依赖及相关库,包等。为了使用Flutter,需要使用一个备用存储位置,我们需要配置环境变量。 配置环境变量名: PUB_HOSTED_URL 和 FLUTTER_STORAGE_BASE_URL。
在windows系统中,需要在环境变量设置中添加:
PUB_HOSTED_URL : pub.flutter-io.cn FLUTTER_STORAGE_BASE_URL : storage.flutter-io.cn
然后运行Git命令(前提是安装了GitBash工具):
git clone -b dev https://github.com/flutter/flutter.git Flutter
Flutter文件夹需要注意:文件夹存放的路径上不要出现空格,否则在IDE中进行工程创建后会有警告,SDK环境路径上存在分隔符。
在clone完成之后,即Flutter Sdk下载完毕,还需要配置Flutter环境: xxxx/Flutter/bin目录下。
重新打开一个命令行,在其中输入命令
flutter doctor
进行环境及缺失的依赖检查,并下载需要的依赖。 运行效果如下图:
在环境及相关依赖检查完成之后,可以开始在Android Studio中进行创建工程行为。
注意:Android Studio 预览版中无法保证运行Flutter成功。因此需要使用稳定版AS,且需要3.0版本以上。
Android Studio中需要安装Flutter Plugin,Dart Plugin两个插件。
Dart SDK也需要手动安装,直接下载zip包免安装。
成功准备好IDE环境之后,就可以创建Flutter Project了,默认创建Flutter Application就可以了,按照IDE创建提示一直到最终完成。
需要注意:同样由于网络环境,直接运行Flutter Project是不可行的,UI会一直停留在Gradle正在初始化工程。这时需要修改build.gradle配置中的中央Maven库到一个可信赖的公共Maven库。 这里我修改成Ali的Maven库
buildscript {
ext.kotlin_version = '1.1.51'
repositories {
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
google()
}
// ......
}
// ......
allprojects {
repositories {
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
}
google()
}
// ......
然后再次sync工程,进行运行。
首个创建的Flutter Project工程结构如下:
链接:https://juejin.cn/post/7250288616533049381
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android 实现单指滑动、双指缩放照片
一、前景提示
最近接到一个查看大图的需求,现在图片展示还不够大,要求还要能缩小能放大还能保存照片。直接开始Google实现方式。
二、实现功能
根据查询到的结果分为两种,一个是使用手势监听来实现,第二种监听触摸事件来实现
- 手势监听-- ScaleGestureDetector Google提供的手势监听类
- 触摸事件--OnTouchListener 自己监听触摸事件自己实现放大缩小的逻辑
2.1 手势监听
先写布局文件
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/iv_example"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="Hello World!"
android:scaleType="fitCenter"
android:src="@drawable/muffin_7870491_1920"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
再去实现手势监听方法
class MainActivity : AppCompatActivity() {
private lateinit var mScaleGestureDetector: ScaleGestureDetector
private var mScaleFactor: Float = 1.0f
private lateinit var mImageView: AppCompatImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mImageView = findViewById(R.id.iv_example)
mScaleGestureDetector = ScaleGestureDetector(this, ScaleGestureListener())
mImageView.setOnTouchListener { _, event ->
mScaleGestureDetector.onTouchEvent(event)
true
}
}
private inner class ScaleGestureListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
mScaleFactor *= detector.scaleFactor
// 限制缩放因子在0.1到10.0
mScaleFactor = mScaleFactor.coerceIn(0.1f, 10.0f)
mImageView.scaleX = mScaleFactor
mImageView.scaleY = mScaleFactor
return true
}
}
}
代码很简单直接使用ScaleGestureDetector去监听触摸事件,手势本质也是Google内部监听事件判断再回调给我们使用。当然我们这里不去查看源码,只看实现过程。
在使用过程中发现这种缩放并不平滑,而且响应有点慢,有延迟。猜想内部是由很多其他的判断吧。那我们只想简单一点怎么搞呢,那就是自己去判断缩放,还有实现单指滑动用手势也不太好实现的样子。所以我们试试第二种方式实现也就是触摸事件。
2.2 触摸事件
首先我们实现一下缩放,我们还是沿用上次使用onTouchListener来处理我们的触摸事件,布局文件中需要把imageView的缩放属性改为矩阵 android:scaleType="matrix"
private var startMatrix = Matrix()
mImageView.setOnTouchListener { _, event ->
when(event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_POINTER_DOWN -> {
// 记录双指按下的位置和距离
startDistance = getDistance(event)
if (startDistance > 10f) {
startMatrix.set(mImageView.imageMatrix)
mode = 2
}
return@setOnTouchListener true
}
}
true
}
没有自己处理过触摸事件的小伙伴可能会好奇MotionEvent.ACTION_MASK是什么,其实这个是为了处理多点触摸事件加的一个flag和action做and操作,我们就能处理ACTION_POINTER_DOWN和ACTION_POINTER_UP这两个多点触摸事件。
看下代码逻辑,我们先计算两个手指的距离,如果距离大于10就证明是缩放操作,设置成我们自己定义的模式,再把imageView的矩阵保存,后续对照片移动,缩放都是通过变换矩阵来实现的。
至于计算两个手指之间的距离用的勾股定理,来个示意图,大家就明白了。
计算如下。
private fun getDistance(event: MotionEvent): Float {
val dx = event.getX(0) - event.getX(1)
val dy = event.getY(0) - event.getY(1)
return sqrt(dx * dx + dy * dy)
}
通过计算能得到直角边和邻边,对他们使用勾股定理就能得到斜边的值,也就是两个手指之间的距离。
有做过触摸事件监听的同学就应该知道,我们下一步要监听移动事件了也就是MotionEvent.ACTION_MOVE。
mImageView.setOnTouchListener { _, event ->
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_POINTER_DOWN -> {
// 记录双指按下的位置和距离
startDistance = getDistance(event)
if (startDistance > 10f) {
startMatrix.set(mImageView.imageMatrix)
mode = 2
}
return@setOnTouchListener true
}
MotionEvent.ACTION_MOVE -> {
if (mode == 2) {
// 双指缩放
val currentDistance = getDistance(event)
if (currentDistance > 10f) {
val scale = currentDistance / startDistance
mImageView.imageMatrix = startMatrix.apply {
postScale(scale, scale, getMidX(event), getMidY(event))
}
}
}
return@setOnTouchListener true
}
MotionEvent.ACTION_POINTER_UP -> {
mode = 0
return@setOnTouchListener true
}
else -> return@setOnTouchListener true
}
}
这里在move事件中我们也需要对手指之间的距离进行计算,如果距离超过10,就开始计算缩放倍数,通过postScale进行矩阵变换。
在MotionEvent.ACTION_POINTER_UP事件中对mode值进行复位操作,毕竟还有个单指拖动操作。
如果大家把上面的代码运行过就会发现怎么图片没有居中显示,这是因为我们的缩放属性被改为矩阵也就是android:scaleType="matrix",那么想要图片居中显示怎么操作呢,只需要在触摸时去改变缩放属性,其他的时候不变即可。
我们把imageView恢复成android:scaleType="fitCenter",在onTouchListener中加入(放在when前即可)
mImageView.scaleType = ImageView.ScaleType.MATRIX
这样一开始就可以保持图片在中央了。
这样缩放功能实现了,下面实现单指拖动功能,思路很简单记录第一次按下的位置,在移动过程中计算应该需要偏移的距离,再记录下当前的位置,以便于下次计算。
private var lastX = 0f
private var lastY = 0f
mImageView.setOnTouchListener { _, event ->
mImageView.scaleType = ImageView.ScaleType.MATRIX
when (event.action and MotionEvent.ACTION_MASK) {
MotionEvent.ACTION_DOWN -> {
// 记录单指按下的位置
lastX = event.x
lastY = event.y
mode = 1
startMatrix.set(mImageView.imageMatrix)
return@setOnTouchListener true
}
MotionEvent.ACTION_POINTER_DOWN -> {
// 记录双指按下的位置和距离
startDistance = getDistance(event)
if (startDistance > 10f) {
startMatrix.set(mImageView.imageMatrix)
mode = 2
}
return@setOnTouchListener true
}
MotionEvent.ACTION_MOVE -> {
if (mode == 1) {
// 单指拖动
val dx = event.x - lastX
val dy = event.y - lastY
mImageView.imageMatrix = startMatrix.apply {
postTranslate(dx, dy)
}
lastX = event.x
lastY = event.y
} else if (mode == 2) {
// 双指缩放
val currentDistance = getDistance(event)
if (currentDistance > 10f) {
val scale = currentDistance / startDistance
mImageView.imageMatrix = startMatrix.apply {
postScale(scale, scale, getMidX(event), getMidY(event))
}
}
}
return@setOnTouchListener true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
mode = 0
return@setOnTouchListener true
}
else -> return@setOnTouchListener true
}
}
代码实现和思路一样,我们还需要在MotionEvent.ACTION_UP中复位模式,调用postTranslate进行偏移。
这样基本上功能我们都简单实现了。下面我们就需要优化了代码,如果各位跟着实现了,就会发现缩放倍数太大了导致轻轻动一下就会放很大,还有别的都是需要我们优化的。
三、功能优化
3.1 优化缩放倍数太大问题
其实这个问题和我们处理move事件有关系,熟悉Android事件机制都知道一个完整的事件流程就是down->move.....move->up。知道了这个之后,再仔细看我们的代码
val currentDistance = getDistance(event)
if (currentDistance > 10f) {
val scale = currentDistance / startDistance
mImageView.imageMatrix = startMatrix.apply {
postScale(scale, scale, getMidX(event), getMidY(event))
}
}
在move事件中我们这样处理的,计算缩放倍数然后缩放,大体一看是没有什么问题的。但是,我们的move事件不止执行一次,这就导致我们的缩放不止执行一次,每次都是在原来的基础上放大或者缩小。所以轻轻移动倍数就会很多。
最简单的办法就是我们记录一下move过程中累计的倍数,如果到达最大值或者最小值就不让放大或者缩小了。代码如下。
if (scale > 1.0f) {
sumScale += scale
} else {
sumScale -= scale
}
if (sumScale >= maxScale || sumScale <= minScale) {
return@setOnTouchListener true
}
简单但是有效的方式。其中max和min,可以自己赋值。
3.2 保持原图不缩小
实现起来也很简单,需要先定义一个变量记录当前缩放之后的倍数。大家测试就会发现,如果是放大操作那么倍数就会大于1如果是缩小倍数就会比1 小。我们就可以利用这点来处理我们的逻辑。
private var lastScaleFactor = 1f
if (scale * lastScaleFactor > 1.0f) {
if (sumScale >= maxScale || sumScale <= minScale) {
return@setOnTouchListener true
}
sumScale += scale
mImageView.imageMatrix = startMatrix.apply {
postScale(scale, scale, getMidX(event), getMidY(event))
lastScaleFactor *= scale
}
} else {
sumScale -= scale
}
demo在这里点我点我
tips:demo好像不是放大不是很顺畅,但是在项目里用Gilde加载后很流畅,猜测是照片大小问题。但是思路是一样的问题不大。
链接:https://juejin.cn/post/7224426218929012797
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android动态权限申请从未如此简单
前言
注:只想看实现的朋友们可以直接跳到最后面的最终实现
大家是否还在为动态权限申请感到苦恼呢?传统的动态权限申请需要在Activity
中重写onRequestPermissionsResult
方法来接收用户权限授予的结果。试想一下,你需要在一个子模块中申请权限,那得从这个模块所在的Activity
的onRequestPermissionsResult
中将结果一层层再传回到这个模块中,相当的麻烦,代码也相当冗余和不干净,逼死强迫症。
使用
为了解决这个痛点,我封装出了两个方法,用于随时随地快速的动态申请权限,我们先来看看我们的封装方法是如何调用的:
activity.requestPermission(Manifest.permission.CAMERA, onPermit = {
//申请权限成功 Do something
}, onDeny = { shouldShowCustomRequest ->
//申请权限失败 Do something
if (shouldShowCustomRequest) {
//用户选择了拒绝并且不在询问,此时应该使用自定义弹窗提醒用户授权(可选)
}
})
这样是不是非常的简单便捷?申请和结果回调都在一个方法内处理,并且支持随用随调。
方案
那么,这么方便好用的方法是怎么实现的呢?不知道小伙伴们在平时开发中有没有注意到过,当你调用startActivityForResult
时,AS会提示你该方法已被弃用,点进去看会告诉你应该使用registerForActivityResult
方法替代。没错,这就是androidx
给我们提供的ActivityResult
功能,并且这个功能不仅支持ActivityResult
回调,还支持打开文档,拍摄照片,选择文件等各种各样的回调,同样也包括我们今天要说的权限申请
其实Android在官方文档 请求运行时权限 中就已经将其作为动态权限申请的推荐方法了,如下示例代码所示:
val requestPermissionLauncher =
registerForActivityResult(RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
// Permission is granted. Continue the action or workflow in your
// app.
} else {
// Explain to the user that the feature is unavailable because the
// feature requires a permission that the user has denied. At the
// same time, respect the user's decision. Don't link to system
// settings in an effort to convince the user to change their
// decision.
}
}
when {
ContextCompat.checkSelfPermission(
CONTEXT,
Manifest.permission.REQUESTED_PERMISSION
) == PackageManager.PERMISSION_GRANTED -> {
// You can use the API that requires the permission.
}
shouldShowRequestPermissionRationale(...) -> {
// In an educational UI, explain to the user why your app requires this
// permission for a specific feature to behave as expected, and what
// features are disabled if it's declined. In this UI, include a
// "cancel" or "no thanks" button that lets the user continue
// using your app without granting the permission.
showInContextUI(...)
}
else -> {
// You can directly ask for the permission.
// The registered ActivityResultCallback gets the result of this request.
requestPermissionLauncher.launch(
Manifest.permission.REQUESTED_PERMISSION)
}
}
说到这里,可能有小伙伴要质疑我了:“官方文档里都写明了的东西,你还特地写一遍,还起了这么个标题,是不是在水文章?!”
莫急,如果你遵照以上方法这么写的话,在实际调用的时候会直接发生崩溃:
java.lang.IllegalStateException:
LifecycleOwner Activity is attempting to register while current state is RESUMED.
LifecycleOwners must call register before they are STARTED.
这段报错很明显的告诉我们,我们的注册工作必须要在Activity
声明周期STARTED
之前进行(也就是onCreate
时和onStart
完成前),但这样我们就必须要事先注册好所有可能会用到的权限,没办法做到随时随地有需要时再申请权限了,有办法解决这个问题吗?答案是肯定的。
绕过生命周期检测
想解决这个问题,我们必须要知道问题的成因,让我们带着问题进到源码中一探究竟:
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull ActivityResultContract<I, O> contract,
@NonNull ActivityResultCallback<O> callback) {
return registerForActivityResult(contract, mActivityResultRegistry, callback);
}
public final <I, O> ActivityResultLauncher<I> registerForActivityResult(
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultRegistry registry,
@NonNull final ActivityResultCallback<O> callback) {
return registry.register(
"activity_rq#" + mNextLocalRequestCode.getAndIncrement(), this, contract, callback);
}
public final <I, O> ActivityResultLauncher<I> register(
@NonNull final String key,
@NonNull final LifecycleOwner lifecycleOwner,
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
Lifecycle lifecycle = lifecycleOwner.getLifecycle();
if (lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED)) {
throw new IllegalStateException("LifecycleOwner " + lifecycleOwner + " is "
+ "attempting to register while current state is "
+ lifecycle.getCurrentState() + ". LifecycleOwners must call register before "
+ "they are STARTED.");
}
registerKey(key);
LifecycleContainer lifecycleContainer = mKeyToLifecycleContainers.get(key);
if (lifecycleContainer == null) {
lifecycleContainer = new LifecycleContainer(lifecycle);
}
LifecycleEventObserver observer = new LifecycleEventObserver() { ... };
lifecycleContainer.addObserver(observer);
mKeyToLifecycleContainers.put(key, lifecycleContainer);
return new ActivityResultLauncher<I>() { ... };
}
我们可以发现,registerForActivityResult
实际上就是调用了ComponentActivity
内部成员变量的mActivityResultRegistry.register
方法,而在这个方法的一开头就检查了当前Activity
的生命周期,如果生命周期位于STARTED
后则直接抛出异常,那我们该如何绕过这个限制呢?
其实在register
方法的下面就有一个同名重载方法,这个方法并没有做生命周期的检测:
public final <I, O> ActivityResultLauncher<I> register(
@NonNull final String key,
@NonNull final ActivityResultContract<I, O> contract,
@NonNull final ActivityResultCallback<O> callback) {
registerKey(key);
mKeyToCallback.put(key, new CallbackAndContract<>(callback, contract));
if (mParsedPendingResults.containsKey(key)) {
@SuppressWarnings("unchecked")
final O parsedPendingResult = (O) mParsedPendingResults.get(key);
mParsedPendingResults.remove(key);
callback.onActivityResult(parsedPendingResult);
}
final ActivityResult pendingResult = mPendingResults.getParcelable(key);
if (pendingResult != null) {
mPendingResults.remove(key);
callback.onActivityResult(contract.parseResult(
pendingResult.getResultCode(),
pendingResult.getData()));
}
return new ActivityResultLauncher<I>() { ... };
}
找到这个方法就简单了,我们将registerForActivityResult
方法调用替换成activityResultRegistry.register
调用就可以了
当然,我们还需要注意一些小细节,检查生命周期的register
方法同时也会注册生命周期回调,当Activity
被销毁时会将我们注册的ActivityResult
回调移除,我们也需要给我们封装的方法加上这个逻辑,最终实现就如下所示。
最终实现
private val nextLocalRequestCode = AtomicInteger()
private val nextKey: String
get() = "activity_rq#${nextLocalRequestCode.getAndIncrement()}"
fun ComponentActivity.requestPermission(
permission: String,
onPermit: () -> Unit,
onDeny: (shouldShowCustomRequest: Boolean) -> Unit
) {
if (ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED) {
onPermit()
return
}
var launcher by Delegates.notNull<ActivityResultLauncher<String>>()
launcher = activityResultRegistry.register(
nextKey,
ActivityResultContracts.RequestPermission()
) { result ->
if (result) {
onPermit()
} else {
onDeny(!ActivityCompat.shouldShowRequestPermissionRationale(this, permission))
}
launcher.unregister()
}
lifecycle.addObserver(object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
launcher.unregister()
lifecycle.removeObserver(this)
}
}
})
launcher.launch(permission)
}
fun ComponentActivity.requestPermissions(
permissions: Array<String>,
onPermit: () -> Unit,
onDeny: (shouldShowCustomRequest: Boolean) -> Unit
) {
var hasPermissions = true
for (permission in permissions) {
if (ContextCompat.checkSelfPermission(
this,
permission
) != PackageManager.PERMISSION_GRANTED
) {
hasPermissions = false
break
}
}
if (hasPermissions) {
onPermit()
return
}
var launcher by Delegates.notNull<ActivityResultLauncher<Array<String>>>()
launcher = activityResultRegistry.register(
nextKey,
ActivityResultContracts.RequestMultiplePermissions()
) { result ->
var allAllow = true
for (allow in result.values) {
if (!allow) {
allAllow = false
break
}
}
if (allAllow) {
onPermit()
} else {
var shouldShowCustomRequest = false
for (permission in permissions) {
if (!ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
shouldShowCustomRequest = true
break
}
}
onDeny(shouldShowCustomRequest)
}
launcher.unregister()
}
lifecycle.addObserver(object : LifecycleEventObserver {
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (event == Lifecycle.Event.ON_DESTROY) {
launcher.unregister()
lifecycle.removeObserver(this)
}
}
})
launcher.launch(permissions)
}
总结
其实很多实用技巧本质上都是很简单的,但没有接触过就很难想到,我将我的开发经验分享给大家,希望能帮助到大家。
链接:https://juejin.cn/post/7225516176171188285
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
IDEA 常用配置指南
1、外观配置
1.1 基本配置
图 1.1-1 修改更改主题 + 背景图片 |
---|
如果IDEA版本是2023.1.2以后的版本可以开启 newUI 体验新版的UI界面,我个人是挺喜欢的🌝
1.2 快捷键配置
图1.2-1 修改快捷键 |
---|
2、配置开发环境
2.1 配置GIT
图2.1-1配置git |
---|
2.2 配置maven
2.3 配置JDK
图 2.3-1 配置项目的JDK |
---|
配置项目的语言版本指的是设置 JDK 版本,比如有些比较老的项目需要JDK-6启动,此时不需要安装JDK-6,可以把语言版本设置为6即可运行项目。
3、编辑器设置
3.1 基本配置
图 3.1-1 代码补全提示去掉匹配规则 |
---|
图 3.1-2 配置字体样式和大小 |
---|
字体建议使用等宽字体,如
Consolas
或者JetBrains Mono
,最近英特尔开源的 intel-one-mono 字体也挺好用的,喜欢的话可以安装一下。
图3.1-3 配置行号显示和方法分割 |
---|
3.3 编码风格配置
图 3.3-1 Java引入折叠 |
---|
图 3.3-2 Java代码超字符数换行 |
---|
3.4 配置代码模板
可以生成常见的代码模板,方便开发使用
- 配置文件模板,如图3.4-1所示;
- 配置文件的所有者信息,新建文件后会添加在类的头部,如图3.4-2所示;
- 代码生成模板,输入关键词,点击回车后即可触发生成代码,配置如图 3.4-3所示,使用效果如图3.4-4所示;
图 3.4-1 文件生成模板 |
---|
图 3.4-2 文件头设置 |
---|
图 3.4-3 配置代码生成模板 |
---|
代码生成模板建议建立新的分组后,在新的分组内编写代码生成模板。
图 3.4-4 代码模板使用效果 |
---|
3.5 配置编辑器编码格式
建议都设置为 UTF-8
避免出现文件乱码问题
3.5-1 配置编辑器编码格式 |
---|
3.6 配置忽略的文件和文件夹
建议在忽略文件和文件夹内配置IDEA编辑器生成的文件,防止git提交时提交IDEA配置文件被打🦉
图 3.6-1 配置忽略的文件和文件夹 |
---|
4、好玩的插件
插件 | 名称 |
---|---|
Alibaba Java Coding | 阿里巴巴代码规范检查插件,根据《Java开发手册》进行代码规范性检查 |
Easy Code | 代码生成器,配置好代码模板后,可以从数据库一键生成从dao层到service层的代码 |
GenerateSerialVersionUID | 一键生成SerivalVersionUID |
Jrebel and XRebel | 热部署插件,代码更改后点击build可以免重启热部署 |
JSON Parser | JSON 格式化,在IDEA内增加JSON格式化窗口 |
LeetCode Editor | 实现IDEA内刷LeetCod题目 |
MyBatisX | 方便找到mapper和XML的映射 |
Rainbow Brackets | 彩虹括号,方便查看代码 |
Star Wars Progress Bar | 星球大战主题进度条 |
Translation | 翻译插件,支持翻译代码、注释等等 |
camelCase | 快捷的从全大写、下划线、大驼峰、小驼峰命名之间切换 |
链接:https://juejin.cn/post/7253743476390740005
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这么好的Android开发辅助工具App不白嫖可惜了
过年期间闲来没事,手撸了一个辅助Android开发调试的工具App,适合Android开发者和测试同学使用。
Github地址下载,
Gitee地址下载(需要登录gitee)
功能概览
对我这样的懒人开发者来说,反复的做同样一件事简直太煎熬了,因此我把我平时开发中需要反复操作的命令和一些繁琐的操作整理成了一个工具。
废话不多说, 先上图了解下工具的大概功能有哪些(内容比截图丰富,欢迎下载体验)
CodeCrafts的核心是一个可拖动的侧边栏的悬浮窗,悬浮窗可以折叠或展开,悬浮窗中包含5大块功能分别对应一个TAB, 这5大块功能分别是应用控制、开发者选项、常用功能,常用系统设置和全局功能
请看视频预览:
高清原图 introduction-floating-bar.gif
功能明细
1. 应用控制
应用控制能力将一些日常开发过程中对应用的一些繁琐的操作或者命令行指令转变为可视化的操作,而且还有自动收集和整理Crash, ANR日志,并且可以自动关联Logcat日志
文字太繁琐, 请直接看视频
高清原图 introduction-application-controls.gif
2. 开发者选项
这里的开发者选项功能是将系统的开发者选项中一些最常用的开关放在悬浮窗中, 随时启用或关闭。
优势是不需要频繁去系统的开发者选项中去找对应开关,一键开闭。
我调研了其他有类似能力的工具App,都是引导用户去开发者选项中去开启或关闭功能。CodeCrafts一键开闭,无需跳转到系统开发者选项页面。
请看视频预览:
3. 最常用功能
没什么好介绍的,略。
4. 常用系统设置页面
这里承载了一些开发过程中经常需要打开的系统设置页面的快捷按钮,没什么好介绍的,略
5. 全局功能
这里的全局是相对于应用控制的,应用控制可以选择你正在开发的任意一款App, 然后应用控制中的所有能力都是对你的这个App的操作。 而全局控制中的功能不针对选中的App,所有App都适用
5.1 实时数据(Realtime data)
实时数据会随着当前页面变化或者系统事件实时变化
(以上图为例介绍, 实时数据的内容不仅仅只有这些)
内容 | 含义 | 用途 |
---|---|---|
org.chromium.chrome.browser.firstrun.FirstRunActivity | 当前Activity的类名 | 代码定位 |
launch time: 208ms | 当前Activity的冷启动耗时 | 启动优化 |
com.android.chrome | 当前Activity所在应用的包名 | 常用信息 |
Chrome(uid: 10163) | 当前Activity所在应用的名称和UID | 常用信息 |
pid: 23017 | 当前Activity的进程ID | 常用信息 |
192.168.2.56,... | 当前系统的IP地址,可能有多个 | adb connect等 |
system | 当前应用是系统应用 | |
allowBackUp | 当前应用有allowBackUp属性 | 告警 |
5.2 不锁定屏幕
不会进入锁屏状态,也不会灭屏,避免开发过程中老是自动锁屏。
和系统开发者选项中的功能类似,区别是无论是否插入USB线都有效,开发者选项中的拔掉USB线后就无效了。
都可以用,具体选择看你的使用场景。
5.3 Latest Crashes
显示缓存中最近发生的Crash的调用堆栈,可能为空也可能不止一个Crash堆栈, 需要自行查看是否是你关注的Crash。
使用说明
CodeCrafts的很多功能依赖Shell权限, 如果发现存在功能不可用的情况,一般都是shell权限获取失败了, 只需要通过在电脑终端输入adb命令"adb tcpip 5555"指令, CodeCrafts就可以自动获取shell权限了。
adb tcpip 5555
- 第一次使用,连接电脑终端发送"adb tcpip 5555" 或
- 手机断电重启,连接电脑终端发送"adb tcpip 5555" 或
- 莫名其妙功能不能用了,连接电脑终端发送"adb tcpip 5555"
新增功能
有不少人反馈对CodeCrafts的实现原理感兴趣,后面新增的功能尽量配上实现原理
- CodeCrafts之断点调试 (1.0.15新增)
建设中
- 文件沙盒, 快速浏览App的文件目录
- 自动化,自动化点击,输入(比如自动跳广告,自动输入账号密码?)
- 组件检查, 快速查看View的类型, id, 颜色等
- ...
后期规划
- 悬浮窗的tab和内容可动态配置
- 应用控制增加应用性能数据
- 提供外部SDK接口,外部应用可接入CodeCrafts进行定制化改造
CodeCrafts持续更新中...
Github地址下载,
Gitee地址下载(需要登录gitee)
链接:https://juejin.cn/post/7194736298521788472
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
聊聊自己进入大厂前,创业公司的经历,我学到了什么?
前言
自从毕业开始工作之后,我坚持写了 2 年多的日记,基本节奏是每半年会写一篇,目的是为了记录自己成长的痕迹。
未来某个时间自己再回首的时候,能回味到此时此刻自己在做什么。当时的选择是否正确,是否在此时此刻,看到了未来发展的趋势,跟上了正确的车。
现在是 23 年 6 月,我第一次羊了,上一轮虽然逃过去了,但是这一次没能幸免,现在还在发着低烧。
距离上次写日记又过去了半年多,自己也步入 25 岁,这半年发生了很多事情,体会到时间变得更快了。
工作第一年:误打误撞加入创业公司
在来到字节之前,经历了校招被毁约,来到了创业公司,在创业公司工作了一年,作息是大小周,早 9 晚 9 ,对我来说这是一段比较记忆深刻的经历,教会了我很多互联网行业的东西。
校招被毁约
19 年的时候签约了一家金融公司,也是临近 20 年毕业前几十天,才被公司以疫情经济情况不好的理由,告知单方面毁约。
那个时候的我还是很害怕的,一点是临近毕业,大多校招已停止,第二点是自己有一段时间没有复习,很多知识已经遗忘。
一个朋友得知了我的情况,内推我到一家创业公司,也是内推第二天一大早就去公司面试,还好我还有一些基础在脑子里,现场面了两轮,当场就通过了,成为了前端的实习生一枚,公司也是答应毕业之后就转正。
面试通过之后,第二天就去公司上班了。第一天入职就拿到了 Macbook,也是第一次用 Mac 办公,想象那时的自己,就跟刚进城的人一样。
公司的结构
一个公司,总共就 50 来个人,做着 DAU 百万的产品。由于公司不大,所以做各种不同事情的人(销售、CEO、内容运营、新媒体运营、算法、产品),都坐在一起。敲代码的时候还能听到销售打电话的声音,因为是教育产品,所以销售的合作方大多是猿辅导、好未来、作业帮、学霸君等教育头部公司。
虽然公司小,但是产品各个环节非常全面:
- ABTest
- 算法
- Hive
公司小的好处是,决策层级少,一个想法想要落地是很快的。在公司 1 年期间,快速上线了 3 款小程序,也快速从 0 到 1 上线了金融产品。PC、H5、小程序开发都有涉及。
整个团队的氛围是非常 Open 的,可以试验各种新技术,不像大厂内部会封装一些框架,有内部标准,不好去实践一些开源的框架。
与普通公司不同的地方
个人扮演的角色
我觉得最大不同的地方是角色,我是一名前端,但不仅扮演着前端的角色。要对产品体验负责,也要自己设计页面 UI。
公司没有测试,所以在日常开发中,每个研发和产品都是 QA,要对 C 端百万 DAU 产品对应的用户体验负责。
每个人都对需求负责,一个需求上线之后,可以自己写 SQL 去 Hive 里捞埋点数据,验证 AB 的可行性。
可以深入感受互联网不同角色发挥的作用
在创业公司,因为每周都有公司周会,大家会聚在一起聊每周各部门的进展,在会上,也可以全流程的了解到一个产品的完整生命周期。
内容运营视角:
- 做公众号,发文章,在存在粉丝基础的情况下,头版收入是很可观的,可以达到 6 位数+。
产品运营视角:
- 做竞品分析,看出对方哪方面做的好,我们要抄袭哪快的功能。
- 做电话回访,了解用户的痛点,尤其针对停留时间较长的重度 C 端用户。
销售视角:
- 通过和大型教育机构合作,由于家长是很愿意为孩子进行教育付费的,所以通过弹窗收集信息配合电销,可以达到很可观的收入。
算法视角:
- 通过 AB 试验调整算法策略,可以优化广告收入,另外也可以提前计算预测插入广告可能带来的收益。
- 调整算法策略,也可以优化用户停留时长,增强用户粘性。
数据分析:
- 通过 Hive 离线数据计算,可以生成一些报表数据,给提供用户信息聚合查看功能。
产品视角:
- 在产品基本功能打磨完毕后,要尽可能往付费方向引导。
运维视角:
- 将服务迁移到 K8S 集群,可以降低维护成本。
后端视角:
- 和算法、数分团队配合,另外还需要负责机器运维相关。
老板视角:
- 关注一些重要事项的进展,以及查看上线后的数据,是否符合产品预期。
- 最终产品需要自负盈亏,功能不能一步设计到位,也需要把一些功能做成付费使用的。
- 关注公司每个方向资金支出情况,控制公司收支,避免快速花光融资资金。保持自己的股份不被过度稀释。
我的直观感受是,自己虽然初入茅庐,但通过这一年的感知,深入理解了 C 端产品的全流程。这对我来说也是一笔很大的财富。
营收方面
App 内广告占大头,开屏广告>插屏广告>贴片广告,其次公众号文章等也是赚钱的利器,销售带来的收益远不如以上两个。总共这些一个月7位数还是有的。实际上最大的开销除了人力成本,还是服务器的成本,这个成本逼近7位数。
创业公司的生命周期
- 公司在快速发展期,有很多功能需要开发,这时是需要人的时候,会无限招人。
- 在产品 2-3 年之后,如没有新的大方向,进入一段停滞期,指的是 DAU 的不增长或下降。
- 产品稳定期,不再需要人,核心骨干退出团队,HC 缩减,产品转向以盈利为目标。
- 自负盈亏,break even。不再为公司资金发愁,不再需要融资。
- 保持公司运作,通过手段维持 DAU 和用户付费意愿,通过一些预消费手段留住用户,扩大收益。
快速验证
快速验证是 CEO 经常提的一个点,不过在王兴和张一鸣成功的经验来看,这也是正确的。
快速验证是说快速从 0 到 1 上线一个产品,冷启动或硬广,在短期查看一个产品的数据,如果产品数据不够理想,便放弃产品。试验下一个风口上的题材。
像美团,或者字节现在也在使用这种策略,快速上线 App 并试错,留下那些抓住用户的产品。
公司的瓦解
一个产品的瓶颈
当一个产品被打磨到 3 年之后,一般来说主流程就比较完善了,换句话说是用户需要的功能,产品都有了。这个时候也就过了 PM 发力 RD 开发的时期,在这之后即便这个公司只有运营,也可以保持产品正常运行。
创业公司的问题
CEO 的话语权会很大
一个人带来的决策不一定合理,当产品的发展不再合理时。大家会出现不满情绪,久而久之大家也不再团结协作,在快速上线几个小程序无果之后,3 个月内 50% 的研发团队成员纷纷离职了,不过大伙也很厉害,离职之后大多都去了大厂。
转变方向为营收优先
通过缩减一系列支出,想方设法让公司达到赚钱的状态。
手段有:砍 HC,团建,下线产品不需要的费钱的功能。
另外我也是一步步看着,公司从半层多楼的工区面积,变成 5分之二,4分之一的大小,最后工区被卖掉,撤离北京。
我的离开
我的离开也是必然,在后期被拉到老板的新产品线帮忙做产品从 0 到 1 建设。对当时工作还不到 1 年的我来说,还是很有压力,独自 own 一个私人银行项目。
在长时间宣传下,仍是没有用户使用,我能明显的感受到,新产品前景是渺茫的,只是老板的一厢情愿。另外新产品线的研发非常少,只是一番的催活,其实过程也决定了结果,产品是做不成的。在这种情况下,我提出了离职。
不过我也很感谢这段经历,能让我对从 0 到 1 创业有新的理解,另外也锻炼了我的抗压能力,增强了技术积累。
最近的工作
工作上
工作上在建设插件市场,提供了一种能快速开发页面组件的方式,能直接嵌入组件到前端中,类似动态执行模块联邦注入的组件。是一块很有意思的功能,类似于 Chrome 应用商城,其实开发工具建设一直是我比较喜欢也擅长的方向,未来也会继续在这方面努力,学习其它语言,做更快更高效的工具,为开发提效。
详细可以看这篇我今年写的文章 带你从 0 到 1 实现前端「插件市场架构」
能力提升方面
编程技能
学习并实战了以下技能:
- VSCode 插件开发
- 实践开发插件 VSCode 插件开发
- Rush.js
- 大型项目构建管理。
- Golang (MySQL / Redis / Kafka)
- 主要还是 API 层面的熟悉,目的还是为了能用非 NodeJS 语言写一写后端,以及了解更多的后端知识。
- Rust
稍微了解了一下语法,之前也写了一篇文章:以 JS 的视角带你入门 Rust 语言,快速上手
开源库
非编程相关
最近这 2 年,锻炼了画图、写 PRD、拉通对齐的能力,大厂更加专精一个方面,这让我能静下心来,不再像创业公司一样,受老板的影响,不再做快速迭代的事情,而是把产品打磨好,更加以用户角度出发思考用户需要什么,补齐什么功能。
愿望
毕业之后,由于疫情一直都是在国内旅游。还没出过国,希望疫情后每年自己都能出去走走,行万里路。把最好的景色都记录下来,拓宽眼界,放松心情。我很喜欢大海,尤其是四环环海的小岛,看着大海能让自己的心平静下来。接下来还有几个非常想去的地方、意大利、冰岛、新西兰、瑞士,夏威夷,希望能在 30 岁之前达成目标。
链接:https://juejin.cn/post/7243252896392314937
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
抛开场景,一味的吹捧路由懒加载就像在耍流氓🤣
异步路由长啥样?
原理不做过多介绍(大家都知道),直接上代码了
一般来说,只有SPA需要异步路由。当配置了异步路由,通过split chunk
,打包后都会生成单独的chunk,以webpack为例,建议添加魔法注释,并开启prefetch,以进一步提升体验
import loadable from '@loadable/component';
const XXXA = loadable(() => import('@/views/xx/XXXA'/* webpackChunkName: 'XXXA', webpackPrefetch: true */));
const XXXB = loadable(() => import('@/views/xx/XXXB'/* webpackChunkName: 'XXXB', webpackPrefetch: true */));
// ...
const allRoutes: IRoute[] = [
{
path: '/xxx-a',
component: XXXA,
},
{
path: '/xxx-b',
component: XXXB,
},
// ...
];
// ...
SPA应用,白屏现象能不能解决??
首先我们要搞明白的是,异步路由给我们解决的痛点是啥?
balabala....
Yes, 你打的很对👍🏾,就是为了缩减🐷包的大小,减少资源加载的时间。
但是,结合页面的加载过程,我先给您下个结论:
“SPA无论怎么优化,都无法避免白屏的现象产生,哪怕网速快,一闪而过(实际上你调成3g或带宽更低的网络,白屏一直伴随着你🤮)”
为啥🤬?
SPA
的入口是index.html
, 初始dom,只有div#app
,并且一般都没有任何样式- 页面加载需要先加载
必要js
,比如main.[hash].js
- 当
main.[hash].js
加载并解析成功,页面才会正常展示,so,空档期一定存在,这就是白屏现象的原因所在(注意:此时和是否有其他路由
没有任何关系)
为什么说异步路由不是100%保险??
问题的根本在于这句话:异步路由能提升首屏用户体验 👀
但是,很多人不知道,这里的首屏并不是单指index.html,举个例子:
对于移动端混合应用开发
,首屏可能变成SPA中的任何一个路由路径
。原因也很简单,APP首页中有很多菜单:每个菜单都可以配置路径,这些路径很可能来自同一个SPA。 我们暂且把这些路径叫做一级路由(不包括index.html)
对于以上场景,首屏
不再单一。
开始划重点
- 上述
一级路由(不包括index.html)
请不要配置成异步加载,使用普通的import
即可, 让它打进主包
中。 - 对于其他
非一级路由
,只是通过push、replace
进行跳转,那么配置为异步路由
就很合适
实际案例分析
还是先下个结论:
比如有个路由/xx-a,它作为一级路由
配置在了APP菜单中,那么它就变成了"所谓的首屏页面",如果我们还是使用异步路由
,就会延长白屏的时间
import loadable from '@loadable/component';
const XxxA = loadable(() => import('@/views/xx/XxxA'/* webpackChunkName: 'XxxA', webpackPrefetch: true */));
// ...
const allRoutes: IRoute[] = [
{
path: '/xxx-a',
component: XXXA,
},
// ...
];
// ...
此时,build目录中存在
main.[hash].js
(包含react路由逻辑
)XxxA.[hash].js
此时,打卡devtools,就会知道,network中资源加载顺序如下
- index.html
- main.js
- 如果index.html中引入了其他资源, 比如jquery, lodash...,也会优先download这些资源(哪怕你配置的defer或者async)
- 当main加载并执行后,才会触发路由逻辑,并开始加载
XxxA.[hash].js
- 加载
XxxA.[hash].js
成功,开始解析执行XxxA,XxxA页面才会被正常渲染
以上过程中,白屏的开始是main的加载和执行(包含了路由逻辑)的时间消耗产生的,而XxxA.[hash].js
的加载解析和执行,又无疑增加了空档期,这样白屏时间也就被延长了
若index.html中引入了若干其他script资源,并且处于http1.1的服务器环境中,这个现象就会变得特别明显
- 因为1.1多路复用对于资源的请求数量有限制,chrome下6个作为一组。
我们可以借助network和performance进行实际演示说明:
为了演示,我们将网络调慢些(实际上客户的网络环境也有这样的情况)
network
后续每组都需等待前一组加载完成,才开始加载
只有main加载成功并执行,才会触发路由逻辑,从而开始加载XxxA
脚本。如果网络慢,main.js加载过程就会更长,从而间接导致XxxA
脚本的加载和解析执行被推迟, 这无疑也就延长了白屏时间!!🦮
performance截图 (异步路由)
performance截图 (非异步路由)
此时代码逻辑被打进了main.js中,直接第一波解析执行即可,很明显缩短了白屏时间。
对比一下,就可以一目了然,哎,什么也不说了~
总结
异步路由并不能100%缩短白屏时间,最关键的是我们要知道“首屏”这个词的意义,它并不是单指index.html入口,它可以是SPA中的任何一个路由(结合混合移动开发场景就知道了)
So:
- 如果SPA中的“首屏”只有一个,不存在
移动混合开发场景
或者路径分享场景
(比如分享微信,支付宝等),那所有路由都可以进行异步加载 - 如果存在
移动混合开发场景
或者路径分享场景
,那对应的路由请不要异步加载,使用import即可。
好了,到此结束,如果对您有帮助,还望点个小💖💖
链接:https://juejin.cn/post/7216213764777328697
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
学前端必读的从输入url到页面渲染全过程
从输入 URL 到页面展示这中间到底发生了什么?
,这是一道非常经典的面试题,这一过程涉及到了计算机网络、操作系统、Web等一系列知识,如果对这一过程有非常好的了解,对以后的开发甚至是程序的优化都是非常有益的。现将过程梳理如下:
1. 解析URL
分析所需要使用的传输协议和请求的资源路径
。如果url中的协议或主机名不合法,将会把地址栏中输入的内容传递给搜索引擎,如果没有问题,浏览器会检查url中是否出现了非法字符,如果存在,对非法字符(空格、汉字等双字节字符)进行转义后在进行下一过程。
编码和解码:
encodeURI()/decodeURI()
:encodeURI()函数只会把参数中的空格编码为%20,汉字进行编码,其余特殊字符不会转换encodeURIComponent()/decodeURIComponent()
:由于这个方法对:/都进行了编码,所以不能用它来对网址进行编码,适合对URL中的参数进行编码escape()/unescape()
:将字符的unicode编码转化为16进制序列,不对ASCII字母和数字进行编码,也不会对' * @ - _ + . / '这些ASCII符号进行编码,用于服务器与服务器端传输多
let url = "http://www.baidu.com/test /中国"
console.log(encodeURI(url)); // http://www.baidu.com/test%20/%E4%B8%AD%E5%9B%BD
let url1 = `https://www.baidu.com/from=${encodeURIComponent('http://wwws.udiab.com')}`
console.log(url1); // https://www.baidu.com/from=http%3A%2F%2Fwwws.udiab.com
console.log(escape(url)); // http%3A//www.baidu.com/test%20/%u4E2D%u56FD
1.1 URL地址格式
传统格式:scheme://host:port/path?query#fragment
。例:http://www.urltest.cn/system/user…
- scheme(必写):
协议
。http
(超文本传输协议)、https
(安全超文本传输协议)、ftp
(文件传输协议,用于将文件下载或上传至网站)、file
(计算机上的文件) - host(必写):
域名或IP地址
- port(可省略):
端口号
,http默认80,https默认443 - path:
路径
,例如 /system/user - query:
参数
,例如 username=falcon&age=18 - fragment:
锚点(哈希hash)
,用于定位页面的某个位置
Restful格式:可以通过不同的请求方式(get、post、put、delete)来实现不同的效果
- (GET) http://www.webtest.com/users
- (POST) http://www.webtest.com/users
- (PUT) http:http://www.webtest.com/users/217
- (DELETE) http:http://www.webtest.com/users/217
2. 缓存判断
浏览器缓存就是浏览器将用户请求过的资源存储到本地电脑。当浏览器再次访问时就可以直接从本地加载,不需要去服务器请求,能有效减少不必要的数据传输,减轻服务器负担,提升网站性能,提高客户端网页打开速度。浏览器缓存一般分为强缓存和协商缓存
- 浏览器在请求某一资源时,会先获取该资源缓存的header信息,判断是否命中强缓存(cache-control、expires信息),命中则直接从缓存中获取资源信息,不会向服务器发起请求;
- 如果没有命中强缓存,浏览器会发送请求(携带该资源缓存的第一次请求返回的header字段信息,Last-Modified/If-Modified-Since、Etag/If-None-Match)到服务器,由服务器根据请求中携带的相关header字段进行对比来判断是否命中协商缓存,命中则返回新的header信息更新缓存中对应的header信息,不返回资源内容,浏览器直接从缓存中获取;否则返回最新的资源内容
2.1 强缓存
expires
,绝对时间字符串,发送请求在这个时间之前有效,之后无效
catch-control
,几个比较常用的字段如下:
max-age=number
,相对字段,利用资源第一次请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,然后再进行比较no-cache
,不使用本地缓存,需要使用协商缓存no-store
,禁止浏览器缓存数据,每次用户都需要向服务器发送请求获取完整资源public
,可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器private
,只能被终端用户浏览器缓存
【注】
- 强缓存如何重新加载缓存过的资源?使用强缓存,不用向服务器发送请求就可以获取到资源,如果在强缓存期间,资源发生了变化,浏览器就一直得不到最新的资源,如何操作:
通过更新页面中引用的资源路径,让浏览器主动放弃缓存,加载新的资源
- 如果二者同时存在,
cache-control的优先级高于expires
2.2 协商缓存
Last-Modified/If-Modified-Since
- 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,会在response的header上加上Last-Modified(表示资源在服务器上的最后修改时间)字段
- 浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since(值就是上一次请求返回的Last-Modified值)字段,来判断是否发生变化,没变化返回304,从缓存中加载,也不会重新在response的header上添加Last-Modified;发生变化则直接从服务器加载,并且更新Last-Modified值
Etag/If-None-Match
- 这两个值是由服务器生成的每个资源的唯一标识字符串,只要资源变化值就会发生改变
- 判断过程与上面一组逻辑类似
- 不同的是,当服务器返回304时,由于Etag重新生成过,response的header还是会把这个Etag返回
【注】
为什么需要Etag?
一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变修改时间);某些文件修改非常频繁(例如在秒以下的时间进行修改);某些服务器不能精确得到文件的最后修改时间等这些情况下,利用Etag能够更加精确的控制缓存- 两者可以一起使用,
服务器会优先验证Etag,在一致的情况下,才会继续比对Last-Modified
2.3 用户行为对缓存的影响
3. DNS解析
获取输入url中的域名对应的IP地址。
- 第一步,检查
浏览器缓存
中是否缓存过该域名对应的IP地址; - 第二步,检查
本地的hosts文件(系统缓存)
; - 第三步,
本地域名解析服务器
进行解析; - 第四步,
根域名解析服务器
进行解析; - 第五步,gTLD服务器进行解析
(顶级域名)
; - 第六步,
权威域名服务器
进行解析,最终获得域名IP地址;
3.1 域名层级结构图
- 根域:位于域名空间最顶层,一般用一个点“.”表示
- 顶级域:一般表示一种类型的组织机构或者国家地区。
.net(网络供应商) .com(工商企业) .org(团体组织) .edu(教育机构) .gov(政府部门) .cn(中国国家域名)
- 二级域:用来标明顶级域内一个特定的组织。.com.cn .net.cn .edu.cn
- 子域:二级域下所创建的各级域名,各个组织或用户可以自由申请注册
- 主机:位于域名空间最下层,一台具体的计算机。完整格式域:http://www.sina.com.cn
3.2 递归查询、迭代查询
用户向本地DNS服务器发起请求属于递归请求
;本地DNS服务器向各级域名服务器发起请求属于迭代请求
- 递归查询:以本地DNS服务器为中心,客户端发出请求报文后就一直处于等待状态,直到本地DNS服务器发来最终查询结果
- 迭代查询:DNS服务器如有客户端请求数据则返回正确地址;没有则返回一个指针;按指针继续查询
4. TCP三次握手建立连接
- 第一步,客户端发送SYN包(seq=x)到服务器,等待服务器确认
- 第二步,服务器收到SYN包,确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(seq=y),即SYN+ACK包
- 第三步,客户端收到SYN+ACK包,向服务器发送确认包ACK(ack=y+1)
三次握手完成,客户端和服务器正式开始传递数据
4.1 TCP、UDP
TCP
面向连接的协议,只有建立后才可以传递数据UDP
无连接的协议,可以直接传送数据,传输效率较高,但不能保证数据的完整性
5. 发起http、https请求
5.1 http1.0、http1.1、http2.0区别
空
5.2 http、https
- https需要
CA申请证书
,一般需要交费 - http运行在TCP之上,
明文传输
;https运行在SSL/TLS
之上,SSL/TLS运行在TCP之上,加密传输
- http默认端口
80
,https默认端口443
- https可以有效的防止运营商劫持
5.3 XSS、CSRF
XSS
XSS(Cross-site Scripting)。跨域脚本攻击
,指通过利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序等。
分类如下:
反射型
。发出请求时,xss代码出现在URL中,作为输入提交到服务器端,服务器端解析后响应,xss代码随响应内容一起传回浏览器,最后浏览器解析执行xss代码。例如:"http://www.a.com/xss/reflect…"
存储型
。和反射型的差别是提交的代码会存储在服务器端(数据库、内存、文件系统等),下次请求目标页面时不用再提交XSS代码。例如:留言板xss,用户提交留言到数据库,目标用户查看留言板时,留言的内容会从数据库查询出来并显示,浏览器解析执行,触发xss攻击DOM型
。不需要服务器的参与,触发xss靠的是浏览器端的DOM解析,完全是客户端触发
防御措施:
过滤
。对用户的输入(和URL参数)进行过滤。移除用户输入的和事件相关的属性,如onerror、onclick等;移除用户输入的Style节点、Script节点(一定要特别注意,它是支持跨域的)、Iframe节点编码
。对输出进行html编码。对动态输出到页面的内容进行html编码,使脚本无法再浏览器中执行服务端设置会话Cookie的HTTP Only属性
,这样客户端的JS脚本就不能获取cookie信息了
CSRF
CSRF(Cross-site request forgery)。跨站请求伪造
,攻击者通过伪造用户的浏览器请求,向用户曾经认证访问过的网站发送出去,使目标网站接收并误以为是用户的真实操作而去执行命令。常用于转账、盗号、发送虚假消息等
防御措施:
token验证
。服务器返回给客户端一个token信息,客户端带着token发送请求,如果token不合法,服务器拒绝这个请求隐藏令牌
。将token隐藏在http的header中referer验证
。页面请求来源验证,只接受本站的请求,其他进行拦截
5.4 get、post
GET
- 一般用于获取数据;
- 参数放在url中;
- 浏览器回退或刷新无影响;
- 请求可被缓存;
- 请求的参数放url上,有长度限制;
- 请求的参数只能是ASCII码;
- url对所有人可见,安全性差;
- get产生一个tcp数据包,hearder、data一起发送一次请求,服务器返回200
POST
- 一般用于向后台传递数据、创建数据;
- 参数放在body里;
- 浏览器回退或刷新数据会被需重提交;
- 请求不会被缓存;
- 请求的参数放body上,无长度限制;
- 请求的参数类型无限制,允许二进制数据;
- 请求参数不会被保存在浏览器历史或web服务器日志中,相对更安全;
- post产生两个tcp数据包,先发送header,返回100 continue,再发送data,服务器返回200,但不是绝对的,Firefox只发送一次
5.5 状态码
1XX - 通知
- 100 -- 客户端继续发送请求
- 101 -- 切换协议
2XX - 成功
- 200 -- 请求成功,一般应用与 get 或 post 请求
- 201 -- 请求成功并创建新的资源,
3XX - 重定向
- 301 -- 永久移动,请求的资源已被永久移动到新的 url,返回信息包括新的 url,浏览器会自动定向到新的 url,今后所有的请求都是新的 url
- 302 -- 临时移动,资源只是临时移动,客户端应继续使用旧的 url
- 304 -- 所请求的资源未修改,不会返回任何资源。浏览器请求的时候,会先访问强缓存,没有则访问协商缓存,协商缓存命中,资源未修改,返回 304
4XX - 客户端错误
- 400 -- 客户端请求的语法错误,服务器无法理解(z 字段类型,或对应的值类型不一致;或者没有进行 JSON.toStringfy 的转换)
- 401 -- 请求需要用户的认证
- 403 -- 服务器理解客户端请求,但是拒绝执行
- 404 -- 服务器无法根据客户端的请求找到资源
5XX - 服务器端错误
- 500 -- 服务器内部错误,无法完成资源的请求
- 501 -- 服务器不支持请求功能,无法完成资源请求
- 502 -- 网关或代理服务器向远程服务器发送请求返回无效
5.6 跨域
- 跨域:浏览器不能执行其他网站的脚本,这是由于
同源策略(同协议、同域名、同端口)限制
造成的 - 同源策略限制的行为:cookie,localstorage和IndexDB无法读取;DOM无法获取;Ajax请求不能发送
跨域的几种解决方式:
jsonp
,实现原理是<script>
标签的src可以发跨域请求,不受同源策略限制,缺点是只能实现get一种请求document.domain + iframe跨域
domain属性可返回下载当前文档的服务器域名,此方案仅限主域相同,子域不同的跨域场景跨域资源共享(CORS)
只服务端设置Access-Control-Allow-Origin即可,前端无需设置;若要带cookie请求,前后端都需要设置nginx反向代理跨域
,项目中常用的一种方案html5的postMessage(跨文档消息传输),WebSocket(全双工通信、实时通信)
6. 返回数据
当页面请求发送到服务器端后,服务器端会返回一个html文件作为响应
7. 页面渲染
7.1 加载过程
- HTML会被渲染成
DOM树
。HTML是最先通过网址请求过来的,请求过来之后,HTML本身会由一个字节流转化成一个字符流,浏览器端拿到字符流,之后通过词法分析,将相应的词法分析成相应的token,转化不同的token tag,然后通过token类型append到DOM树 - 遇到link token tag,去请求css,然后对css进行解析,生成
CSSOM树
- DOM树和CSSOM树结合形成
Render Tree
,再进行布局和渲染 - 遇到script tag,然后去请求JS相关的web资源,请求回来的js交给浏览器的v8引擎进行解析
7.2 加载特点
- html文档解析,对tag依次从上到下解析,
顺序执行
- html中可能会引入很多css,js的web资源,这些资源在浏览器中是
并发加载
的。 - DOM树和CSSOM树通常是并行构建的, 所以
CSS加载不会阻塞DOM的解析
;Render树依赖DOM树和CSSOM树进行,所以CSS加载会阻塞DOM的渲染
;css会阻塞js文件执行,但不会阻塞js文件下载
,因为GUI渲染线程与JavaScript线程互斥,JS有可能影响样式;js会阻塞DOM的解析(把js文件放在最下面),也就会阻塞DOM的渲染
,同时js顺序执行,也会阻塞后续js逻辑的执行
依赖关系
。页面渲染依赖于css的加载;js的执行顺序依赖关系;js逻辑对于dom节点的依赖关系,有些js需要去获取dom节点引入方式
。直接引入,不会阻塞页面渲染;defer不会阻塞页面渲染,顺序执行;async不会阻塞页面渲染,先到先执行,不保证顺序;异步动态js,需要的时候引入
【注】css样式置顶;js脚本置底;用link代替import;合理使用js异步加载
资源加载完成后,通过样式计算、布局设置、分层、绘制
等过程,将页面呈现出来
7.3 重绘回流
根据渲染树,浏览器可以计算出网页中有哪些节点,各节点的CSS以及从属关系,发生回流
;根据渲染树以及回流得到的节点信息,计算出每个节点在屏幕中的位置,发生重绘
- 元素的规模尺寸、布局、显隐性发生变化时,发生回流,每个页面至少产生一次回流,第一次加载
- 元素的外观、风格、颜色等发生变化而不影响布局,发生重绘
回流一定发生重绘,重绘不一定发生回流
【避免措施】
样式设置
- 避免使用层级较深的选择器
- 避免使用 css 表达式
- 元素适当的定义高度或最小高度
- 给图片设置尺寸
- 不要使用 table 布局
- 能 css 实现的,尽量不要使用 js 实现
渲染层
- 将需要多次重绘的元素独立为 render layer,如设置 absolute,可以减少重绘范围
- 对于一些动画元素,使用硬件渲染
DOM 优化
- 缓存 DOM
- 减少 DOM 深度及 DOM 数量
- 批量操作 DOM
- 批量操作 CSS 样式
- 在内存中操作 DOM
- DOM 元素离线更新
- DOM 读写分离
- 事件代理
- 防抖和节流
- 及时清理环境
TCP四次挥手断开连接
- 客户端发送一个FIN(seq=u)数据包到服务器,用来关闭客户端到服务器的数据连接
- 服务器接受FIN数据包,发送ACK(seq=u+1)数据包到客户端
- 服务器关闭与客户端的连接并发送一个FIN(seq=w)数据包到客户端,请求关闭连接
- 客户端发送ACK(seq=w+1)数据包到服务器,服务器在收到ACK数据包后进行CLOSE状态,客户端在一定时间没有收到服务器的回复证明其关闭后,也进入关闭状态
链接:https://juejin.cn/post/7252869090549268538
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
VUE3基础学习(一)环境搭建与简单上手
VUE是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型。
个人学习感受:构建模板,通过数据就可以生成展示的html,上手简单,快速。
引用:VUE官网
- 声明式渲染:Vue 基于标准 HTML 拓展了一套模板语法,使得我们可以声明式地描述最终输出的 HTML 和 JavaScript 状态之间的关系。
- 响应性:Vue 会自动跟踪 JavaScript 状态并在其发生变化时响应式地更新 DOM。
开始:
1.安装环境
1.node.js (已安装 16.0 或更高版本的 Node.js)
说明:Node.js是一种基于Chrome V8引擎的JavaScript运行环境,是一个可以在服务器端运行JavaScript的开源工具。
NodeJS已经集成了npm,所以npm也一并安装好了。
验证测试: node -v
npm -v
2.cnpm
说明 :由于npm的服务器在海外,所以访问速度比较慢,访问不稳定 ,cnpm的服务器是由淘宝团队提供 服务器在国内cnpm是npm镜像。但是一般cnpm只用于安装时候,所以在项目创建与卸载等相关操作时候我们还是使用npm。
全局安装cnpm
npm install -g cnpm --registry=https://registry.npm.taobao.org
验证测试: cnpm -v
1.IDE与简单上手
1.IDE:webstorm
说明:我个人一直在用jetbrains 旗下的各种 IDA ,我使用起来比较熟练。
配置IDE:
2.简单上手:
到需要创建项目的文件目录
npm init vue@latest
这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。你将会看到一些诸如 TypeScript 和测试支持之类的可选功能提示:
如果不确定是否要开启某个功能,你可以直接按下回车键选择 No
。在项目被创建后,通过以下步骤安装依赖并启动开发服务器:
之后执行:
npm install
执行完安装后执行启动:
npm run dev
执行成功: 打开网页 http://localhost:5173/
当你准备将应用发布到生产环境时,请运行:
npm run build
此命令会在 ./dist
文件夹中为你的应用创建一个生产环境的构建版本。
简单例子 与 说明:
<script setup>
defineProps({
msg: {
type: String,
required: true
}
})
</script>
<!--模板区域-->
<template>
<div class="greetings">
<h1 class="green">{{ msg }}</h1>
<h3> You’ve successfully created a project with <a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> + <a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
</h3>
</div>
</template>
<!--样式区域-->
<style scoped>
h1 {
font-weight: 500;
font-size: 2.6rem;
position: relative;
top: -10px;
}
</style>
链接:https://juejin.cn/post/7252537738276208699
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Vue 为什么要禁用 undefined?
Halo Word!大家好,我是大家的林语冰(挨踢版)~
今天我们来伪科普一下——Vue 等开源项目为什么要禁用/限用 undefined
?
敏感话题
我们会讨论几个敏感话题,包括但不限于——
- 测不准的
undefined
如何引发复合 BUG? - 薛定谔的
undefined
如何造成二义性? - 未定义的
undefined
为何语义不明?
懂得都懂,不懂关注,日后再说~
1. 测不准的 undefined
如何引发复合 BUG?
一般而言,开源项目对 undefined
的使用有两种保守方案:
- 禁欲系——能且仅能节制地使用
undefined
- 绝育系——禁用
undefined
举个粒子,Vue 源码就选择用魔法打败魔法——安排黑科技 void 0
重构 undefined
。
事实上,直接使用 undefined
也问题不大,毕竟 undefined
表面上还是比较有安全感的。
猫眼可见,undefined
是一个鲁棒只读的属性,表面上相当靠谱。
虽然 undefined
自己问题不大,但最大的问题在于使用不慎可能会出 BUG。undefined
到底可能整出什么幺蛾子呢?
你知道的,不同于 null
字面量,undefined
并不恒等于 undefined
原始值,比如说祂可以被“作用域链截胡”。
举个粒子,当 undefined
变身成为 bilibili
,同事的内心是崩溃的。
猫眼可见,写做 undefined
变量,读做 'bilbili'
字符串,这样的代码十分反人类。
这里稍微有点违和感。机智如你可能会灵魂拷问,我们前面不是已经证明了 undefined
是不可赋值的只读属性吗?怎么祂喵地一言不合说变就变,又可以赋值了呢?来骗,来偷袭,不讲码德!
这种灵异现象主要跟变量查找的作用域链机制有关。读写变量会遵循“就近原则”优先匹配,先找到谁就匹配谁,就跟同城约会一样,和樱花妹异地恋的优先级肯定不会太高,所以当前局部作用域的优先级高于全局作用域,于是乎 JS 会优先使用当前非全局同名变量 undefined
。
换而言之,局部的同名变量 undefined
屏蔽(shadow,AKA“遮蔽”)了全局变量 globalThis.undefined
。
关于作用域链这种“远亲不如近邻”的机制,吾愿赐名为“作用域链截胡”。倘若你不会搓麻将,你也可以命名为“作用域链抢断”。倘若你不会打篮球,那就叫“作用域链拦截”吧。
globalThis.undefined
确实是只读属性。虽然但是,你们重写非全局的 undefined
,跟我 globalThis.undefined
有什么关系?
我们总以为 undefined
短小精悍,但其实 globalThis.undefined
才能扬长避短。
当我们重新定义了 undefined
,undefined
就名不副实——名为 undefined
,值为任意值。这可能会在团队协作中引发复合 BUG。
所谓“复合 BUG”指的是,单独的代码可以正常工作,但是多人代码集成就出现问题。
举个粒子,常见的复合 BUG 包括但不限于:
- 命名冲突,比如说 Vue2 的 Mixin 就有这个瑕疵,所以 Vue3 就引入更加灵活的组合式 API
- 作用域污染,ESM 模块之前也有全局作用域污染的老毛病,所以社区有 CJS 等模块化的轮子,也有 IIFE 等最佳实践
- 团队协作,Git 等代码版本管理工具的开发冲突
举个粒子,undefined
也可能造成类似的问题。
猫眼可见,双方的代码都问题不大,但放在一起就像水遇见钠一般干柴烈火瞬间爆炸。
这里分享一个小众的冷知识,这样的代码被称为“Jenga Code”(积木代码)。
Jenga 是一种派对益智积木玩具,它的规则是,先把那些小木条堆成一个规则的塔,玩家轮流从下面抽出一块来放在最上面,谁放上之后木塔垮掉了,谁就 GG 了。
积木代码指的是一点点的代码带来了亿点点的 BUG,一行代码搞崩整个项目,码农一句,可怜焦土。
换而言之,这样的代码对于 JS 运行时是“程序正义”的,对于开发者却并非“结果正义”,违和感拉满,可读性和可为维护性十分“赶人”,同事读完欲哭无泪。
所谓“程序正义”指的是——JS 运行时没有“阳”,不会抛出异常,直接挂掉,浏览器承认你的代码 Bug free,问题不大。
祂敢报错吗?祂不敢。虽然但是,无症状感染也是感染。你敢这么写吗?你不敢。除非忍不住,或者想跑路。
举个粒子,“离离原上谱”的“饭圈倒牛奶”事件——
- 有人鞠躬尽瘁粮食安全
- 有人精神饥荒疯狂倒奶
这种行为未必违法,但是背德,每次看到只能无视,毕竟语冰有“傻叉恐惧症”。
“程序正义”不代表“结果正义”,代码能 run 不代表符合“甲方肝虚”,不讲码德可能造成业务上的技术负债,将来要重构优化来还债。所谓“前猫拉屎,后人铲屎”大抵也是如此。
综上所述,要警惕测不准的 undefined
在团队开发中造成复合 BUG。
2. 薛定谔的 undefined
如何造成二义性?
除了复合 BUG,undefined
还可能让代码产生二义性。
代码二义性指的是,同一行代码,可能有不同的语义。
举个粒子,JS 的一些代码解读就可能有歧义。
undefined
也可能造成代码二义性,除了上文的变量名不副实之外,还很可能产生精神分裂的割裂感。
举个粒子,代码中存在两个一龙一猪的 undefined
。
猫眼可见,undefined
的值并不相同,我只觉得祂们双标。
undefined
变量之所以是 'bilibili'
字符串,是因为作用域链就近屏蔽,cat
变量之所以是 undefined
原始值,是因为已声明未赋值的变量默认使用 undefined
原始值作为缺省值,所以没有使用局部的 undefined
变量。
倘若上述二义性强度还不够,那我们还可以写出可读性更加逆天的代码。
猫眼可见,undefined
有没有精神分裂我不知道,但我快精神分裂了。
代码二义性还可能与代码的执行环境有关,譬如说一猫一样的代码,在不同的运行时,可能有一龙一猪的结果。
猫眼可见,我写你猜,谁都不爱。
大家大约会理直气壮地反驳,我们必不可能写出这样不当人的代码,var
是不可能 var
的,这辈子都不可能 var
。
问题在于,墨菲定律告诉我们,只要可能有 BUG,就有可能有 BUG。说不定你的猪队友下一秒就给你来个神助攻,毕竟不是每个人都像你如此好学,既关注了我,还给我打 call。
语冰以前也不相信倒牛奶这么“离离原上谱”的事件,但是写做“impossible”,读做“I M possible”。
事实上,大多数教程一般不会刻意教你去写错误的代码,这其实恰恰剥夺了我们犯错的权利。不犯错我们就不会去探究为什么,而对知识点的掌握只停留在表面是什么,很多人知错就改,下次还敢就是因为缺少了试错的成就感和多巴胺,不知道 BUG 的 G 点在哪里,没有形成稳固的情绪记忆。
请相信我,永远写正确的代码本身就是一件不正确的事情,你会看到这期内容就是因为语冰被坑了气不过,才给祂载入日记。
语冰很喜欢的一部神作《七龙珠》里的赛亚人,每次从濒死体验中绝处逢生战斗力就会增量更新,这个设定其实蛮科学的,譬如说我们身边一些“量变到质变”的粒子,包括但不限于:
- 骨折之后骨头更加坚硬了
- 健身也是肌肉轻度撕裂后增生
- 记忆也是不断复习巩固
语冰并不是让大家在物理层面去骨折,而是鼓励大家从 BUG 中学习。私以为大神从来不是没有 BUG,而是 fix 了足够多的 BUG。正如爱迪生所说,我没有失败 999 次,而是成功了 999 次,我成功证明了那些方法完全达咩。
综上所述,undefined
的二义性在于可能产生局部的副作用,一猫一样的代码在不同运行时也可以有一龙一猪的结果,最终导致一千个麻瓜眼中有一千个哈利波特,读码人集体精神分裂。
3. 未定义的 undefined
为何语义不明?
除了可维护性感人的复合 BUG 和可读性感人的代码二义性,undefined
自身的语义也很难把握。
举个粒子,因为太麻烦就全写 undefined 了。
猫眼可见,原则上允许我们可以无脑地使用 undefined
初始化任何变量,万物皆可 undefined
。
虽然但是,绝对的光明等于绝对的黑暗,绝对的权力导致绝对的腐败。undefined
的无能恰恰在于祂无所不能,语冰有幸百度了一本书叫《选择的悖论》,这大约也是 undefined
的悖论。
代码是写给人看的,代码的信息越具体明确越好,偏偏 undefined
既模糊又抽象。你知道的,我们接触的大多数资料会告诉我们 undefined
的意义是“未定义/无值”。
虽然但是,准确而无用的观念,终究还是无用的。undefined
的正确打开方式就是无为,使用 undefined
的最佳方式是不使用祂。
免责声明
本文示例代码默认均为 ESM(ECMAScript Module)筑基测评,因为现代化前端开发相对推荐集成 ESM,其他开发环境下的示例会额外注释说明,edge cases 的解释权归大家所有。
今天的《ES6 混合理论》合集就讲到这里啦,我们将在本合集中深度学习若干奇奇怪怪的前端面试题/冷知识,感兴趣的前端爱好者可以关注订阅,也欢迎大家自由言论和留言许愿,共享 BUG,共同内卷。
吾乃前端的虔信徒,传播 BUG 的福音。
我是大家的林语冰,我们一期一会,不散不见,掰掰~
链接:https://juejin.cn/post/7240483867123220540
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
大喊一声Fuck!代码就能跑了是什么体验?
大喊一声Fuck!代码就能跑了是什么体验?
1 前言
大家好,我是心锁,23届准毕业生。
程序员的世界,最多的不是代码,而是💩山和bug。
近期我在学习过程中,在github找到了这么一个项目,能在我们输错命令之后,大喊一声Fuck即可自动更正命令,据说喊得越大声效果越好。
2 项目基本介绍
thefuck是一个基于Python编写的项目,它能够自动纠正你在命令行中输入的错误命令。如果你输错了一个命令,只需要在命令行中输入“fuck”,thefuck就会自动纠正你的错误。该项目支持众多的终端和操作系统,包括Linux、macOS和Windows。
2.1 环境要求
- python环境(3.4+)
2.2 安装方式
thefuck支持brew安装,非常方便,在macOS和Linux上都可以通过brew安装。
brew install thefuck
也支持通过pip安装,便携性可以说是一流了。
pip3 install thefuck
2.3 配置环境变量
建议将下边的代码配置在环境变量中(.bash_profile
、.bashrc
或.zshrc
),不要问为什么,问就是有经验。
eval $(thefuck --alias)
eval $(thefuck --alias FUCK)
eval $(thefuck --alias fuck?)
eval $(thefuck --alias fuck?)
接着运行source ~/.bashrc
(或其他配置文件,如.zshrc
)确认更改立即可用。
3 使用效果
4 thefuck的工作原理
thefuck的工作原理非常简单。当你输入一个错误的命令时,thefuck会根据你输入的命令和错误提示自动推测你想要输入的正确命令,并将其替换为正确的命令。thefuck能够自动推测正确的命令是因为它内置了大量的规则,这些规则能够帮助thefuck智能地纠正错误的命令。
所以,该项目开放了自定义规则。
4.1 创建自己的规则
如果thefuck内置的规则不能够满足你的需求,你也可以创建自己的规则。thefuck的规则是由普通的Python函数实现的。你可以在~/.config/thefuck/rules
目录下创建一个Python脚本,然后在其中定义你的规则函数。
以创建一个名为my_rule
的规则为例,具体步骤如下:
4.1.1 创建rule.py文件
在~/.config/thefuck/rules
目录下创建一个Python脚本,比如my_rules.py
4.1.2 遵循的规则
在自定义脚本中,必须实现以下两个函数,match显然是用来匹配命令是否吻合的函数,而get_new_command则会在match函数返回True时触发。
match(command: Command) -> bool
get_new_command(command: Command) -> str | list[str]
同时可以包含可选函数,side_effect的作用是开启一个副作用,即除了允许原本的命令外,你可以在side_effect做更多操作。
side_effect(old_command: Command, fixed_command: str) -> None
5 yarn_uninstall_to_remove
以创建一个名为yarn_uninstall_to_remove
的规则为例,该规则会在我们错误使用yarn uninstall …
命令时,自动帮助我们修正成yarn remove …
。具体步骤如下:
5.1 创建yarn_uninstall_to_move.py文件
在~/.config/thefuck/rules
目录下创建一个Python脚本,yarn_uninstall_to_remove.py
5.2 编写代码
from thefuck.utils import for_app
@for_app('yarn')
def match(command):
return 'uninstall' in command.script
def get_new_command(command):
return command.script.replace('uninstall', 'remove')
priority=1 # 优先级,数字越小优先级越高
5.3 效果
6 总结
世界之大,无奇不有。不得不说的是,伴随着AI的逐渐发展,类似这种项目未来一定是优先接入AI者才可以继续发展。
友情提示,喊fuck的时候先设置后双击control打开听写功能,喊完再点击一下control完成输入。
链接:https://juejin.cn/post/7213651072145244221
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
一次线上事故,我顿悟了异步的精髓
在高并发的场景下,异步是一个极其重要的优化方向。
前段时间,生产环境发生一次事故,笔者认为事故的场景非常具备典型性 。
写这篇文章,笔者想和大家深入探讨该场景的架构优化方案。希望大家读完之后,可以对异步有更深刻的理解。
1 业务场景
老师登录教研平台,会看到课程列表,点击课程后,课程会以视频的形式展现出来。
访问课程详情页面,包含两个核心动作:
读取课程视频信息 :
从缓存服务器 Redis 获取课程的视频信息 ,返回给前端,前端通过视频组件渲染。
写入课程观看行为记录 :
当教师观看视频的过程中,浏览器每隔3秒发起请求,教研服务将观看行为记录插入到数据库表中。而且随着用户在线人数越多,写操作的频率也会指数级增长。
上线初期,这种设计运行还算良好,但随着在线用户的增多,系统响应越来越慢,大量线程阻塞在写入视频观看进度表上的 Dao 方法。上。
首先我们会想到一个非常直观的方案,提升写入数据库的能力。
- 优化 SQL 语句;
- 提升 MySQL 数据库硬件配置 ;
- 分库分表。
这种方案其实也可以满足我们的需求,但是通过扩容硬件并不便宜,另外写操作可以允许适当延迟和丢失少量数据,那这种方案更显得性价比不足。
那么架构优化的方向应该是: “减少写动作的耗时,提升写动作的并发度” , 只有这样才能让系统更顺畅的运行。
于是,我们想到了第二种方案:写请求异步化。
- 线程池模式
- 本地内存 + 定时任务
- MQ 模式
- Agent 服务 + MQ 模式
2 线程池模式
2014年,笔者在艺龙旅行网负责红包系统相关工作。运营系统会调用红包系统给特定用户发送红包,当这些用户登录 app 后,app 端会调用红包系统的激活红包接口 。
激活红包接口是一个写操作,速度也比较快(20毫秒左右),接口的日请求量在2000万左右。
应用访问高峰期,红包系统会变得不稳定,激活接口经常超时,笔者为了快速解决问题,采取了一个非常粗糙的方案:
"控制器收到请求后,将写操作放入到独立的线程池中后,立即返回给前端,而线程池会异步执行激活红包方法"。
坦率的讲,这是一个非常有效的方案,优化后,红包系统非常稳定。
回到教研的场景,见下图,我们也可以设计类似线程池模型的方案:
使用线程池模式,需要注意如下几点:
- 线程数不宜过高,避免占用过多的数据库连接池 ;
- 需要考虑评估线程池队列的大小,以免出现内存溢出的问题。
3 本地内存 + 定时任务
开源中国统计浏览数的方案非常经典。
用户访问过一次文章、新闻、代码详情页面,访问次数字段加 1 , 在 oschina 上这个操作是异步的,访问的时候只是将数据在内存中保存,每隔固定时间将这些数据写入数据库。
示例代码如下:
我们可以借鉴开源中国的方案 :
- 控制器接收请求后,观看进度信息存储到本地内存 LinkedBlockingQueue 对象里;
- 异步线程每隔1分钟从队列里获取数据 ,组装成 List 对象,最后调用 Jdbc batchUpdate 方法批量写入数据库;
- 批量写入主要是为了提升系统的整体吞吐量,每次批量写入的 List 大小也不宜过大 。
这种方案优点是:不改动原有业务架构,简单易用,性能也高。该方案同样需要考虑内存溢出的风险。
4 MQ 模式
很多同学们会想到 MQ 模式 ,消息队列最核心的功能是异步和解耦,MQ 模式架构清晰,易于扩展。
核心流程如下:
- 控制器接收写请求,将观看视频行为记录转换成消息 ;
- 教研服务发送消息到 MQ ,将写操作成功信息返回给前端 ;
- 消费者服务从 MQ 中获取消息 ,批量操作数据库 。
这种方案优点是:
- MQ 本身支持高可用和异步,发送消息效率高 , 也支持批量消费;
- 消息在 MQ 服务端会持久化,可靠性要比保存在本地内存高;
不过 MQ 模式需要引入新的组件,增加额外的复杂度。
5 Agent 服务 + MQ 模式
互联网大厂还有一种常见的异步的方案:Agent 服务 + MQ 模式。
教研服务器上部署 Agent 服务(独立的进程) , 教研服务接收写请求后,将请求按照固定的格式(比如 JSON )写入到本次磁盘中,然后给前端返回成功信息。
Agent 服务会监听文件变动,将文件内容发送到消息队列 , 消费者服务获取观看行为记录,将其存储到 MySQL 数据库中。
还有一种演进,假设我们不想在应用中依赖消息队列,不生成本地文件,可以采用如下的方式:
这种方案最大的优点是:架构分层清晰,业务服务不需要引入 MQ 组件。
笔者原来接触过的性能监控平台,或者日志分析平台都使用这种模式。
6 总结
学习需要一层一层递进的思考。
第一层:什么场景下需要异步
- 大量写操作占用了过多的资源,影响了系统的正常运行;
- 写操作异步后,不影响主流程,允许适当延迟;
第二层:异步的外功心法
本文提到了四种异步方式:
- 线程池模式
- 本地内存 + 定时任务
- MQ 模式
- Agent 服务 + MQ 模式
它们的共同特点是:将写操作命令存储在一个池子后,立刻响应给前端,减少写动作的耗时。任务服务异步从池子里获取任务后执行。
第三层:异步的本质
在笔者看来,异步是更细粒度的使用系统资源的一种方式。
在教研课程详情场景里,数据库的资源是固定的,但写操作占据大量数据库资源,导致整个系统的阻塞,但写操作并不是最核心的业务流程,它不应该占用那么多的系统资源。
我们使用异步的解决方案时,无论是使用线程池,还是本地内存 + 定时任务 ,亦或是 MQ ,对数据库资源的使用都需要在合理的范围内,只有这样系统才能顺畅的运行。
链接:https://juejin.cn/post/7118580043835506725
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
学会用Compose来做loading动效,隔壁设计师小姐姐投来了羡慕的目光
最近一直在用Compose练习做动效果,但是动效做的再多,在实际做项目的时候,最常用到动效的就是一些loading框,上拉加载下拉刷新之类的场景,我们以前往往遇到这样的需求的时候,会直接问设计师要个切图,或者一个lottie的json文件,但是除了问设计师要资源,我们能不能自己来做一些动效呢,下面我们就来做几个
转圈的点点
之前在网上看见别人用CSS写了一个效果,几个圆点绕着圆心转圈,每一个圆点依次变换着大小以及透明值,那么既然CSS可以,我们用Compose能不能做出这样的效果呢,就来试试看吧,首先既然是绕着圆心转圈的,那么就需要把圆心的坐标以及半径确定好
代码中centerX
与centerY
就是圆心坐标,radius
就是圆的半径,mSize
获取画布的大小,实时来更新圆心坐标和半径,然后还需要一组角度,这些角度就是用来绘制圆周上的小圆点的
angleList
里面保存着所有圆点绘制的角度,那么有了角度我们就可以利用正玄余玄公式算出每一个圆点的中心坐标值,公式如下
pointX
与pointY
就是计算每一个圆点的中心坐标函数,那么我们就能利用这个函数先绘制出一圈圆点
圆点画好了,如何让它们转圈并且改变大小与透明值呢,其实这个过程我们可以看作是每一个圆点绘制的时候不断的在改变自身的半径与透明值,所以我们可以再创建两个数组,分别保存变化的半径与透明值
然后在绘制圆点的时候,我们可以通过循环动画让每一个圆点循环从radius
与alphaList
两个list里面取值,那么就能实现大小与透明值的变化了
还差一点,就是让每一个点变化的大小与透明值不一样,那么我们只需要增加一个逻辑,每一次获取到一个listIndex
值的时候,我们就让它加一,然后当大小要超过radiusList
与alphaList
的临界值的时候,就让下标值变成0然后在重新计算,就能实现让每一个点的大小与透明值不同了,代码如下
这样我们这个动效就完成了,最终效果图如下所示
七彩圆环
这个动效主要用到了Modifier.graphicsLayer
操作符,可以看下这个操作符里面都有哪些参数
可以看到这个函数里面提供了许多参数,基本都跟图形绘制有关系,比如大小,位移,透明,旋转等,我们这次先用到了旋转相关的三个参数rotationX
,rotationY
,rotationZ
,比如现在有一个Ring
的函数,这个函数的功能就是画一个圆环
然后我们在页面上创建三个圆环,并且分别进行x,y,z轴上的旋转,代码如下
旋转使用了角度0到360度变化的循环动画,那么得到的效果就像下面这样
会发现看起来好像只有前两个圆环动了,第三个没有动,其实第三个也在动,它是在绕着z轴转动,就像一个车轮子一样,现在我们尝试下将这三个圆环合并成一个,让一个圆环同时在x,y,z轴上旋转,会有什么效果呢
圆环的旋转马上就变得立体多了,但是只有一个圆环未免显得有点单调了,我们多弄几个圆环,而且为了让圆环旋转的时候互相之间不重叠,我们让每一个圆环旋转的角度不一样,如何做?我们现在只有一种旋转动画,可以再做两个动画,分别为60到420度的旋转和90到450度的旋转,代码如下
然后这里有三个动画,方向也有xyz三个轴,通过排列组合的思想,一共可以有六个不同方向旋转的圆环,于是我们创建六个圆环,在x,y,z轴上代入不同的旋转角度
现在我们再给圆环染个色,毕竟叫七彩圆环,怎么着也得有七种颜色,所以在Ring函数里面定义一个七种颜色的数组,然后创建个定时器,定时从颜色数组中拿出不同的色值给圆环设置上
有个index
的变量默认指向数组的第一个颜色,然后每经过500毫秒切换一个色值,并且当index
指向数组最后一个色值的时候,重新再设置成指向第一个,我们看下效果
我们看到圆环可以自动改变自身的颜色了,但是效果还是有些单调,我们再优化下,将每一个圆环初始的颜色设置成不同的颜色,那么就要给Ring
函数多加一个初始下标的参数,就叫startIndex
,然后原来创建index
的初始值就从0变成startIndex
,其他不变,代码如下
现在差不多每一个圆环都有自己的“想法”了,旋转角度不一样,初始颜色也不一样,最终效果图我们看下
七彩尾巴
一样都是七彩,上面做了个圆环,这里我们做个尾巴,怎么做呢?首先我们从画一个圆弧开始
圆弧就是一个不与圆心相连的扇形,所以我们用到了drawArc
函数,然后参数我们随意先设置了几个,就得到了一个初始角度为0,跨度为150度的圆弧了
然后我们现在让这个圆弧转起来,通过循环改变startAngle
就能达到圆弧旋转的效果,所以我们这里添加上一个循环动画
就得到这样一个旋转的圆弧了,现在部分app里面的loading动画估计就是用的这样的样式,我们现在就在这个样式的基础上,将它变成一个会变颜色的尾巴,首先如何做成一个尾巴呢,貌似没有这样的属性,所以我们只能靠自己画了,我们可以把尾巴看成是若干个圆弧叠放在一起的样子,每一个圆弧的其实角度逐渐变大,sweepAngle
逐渐变小,圆弧粗细也逐渐变大,当这些圆弧都画完之后,视觉上看起来就像是一根尾巴了,所以我们需要三个数组,分别存放需要的初始角度,sweepAngle
以及圆弧粗细
然后遍历strokeList
,将三个数组对应下标所对应的值取出来用来绘制圆弧,绘制的代码如下
再运行一遍代码,一根会转圈的红色尾巴就出来了
接下来就是如何改变尾巴的颜色,我们可以像之前画圆环那样的方式来画尾巴,但是那样子的话尾巴切换颜色的过程就会变的很生硬,我们这里用另一个方式,就是animateColorAsState
函数,同样也是有一个定时器定时获取一个色值List的颜色,然后将获取到的颜色设置到animateColorAsState
的targetValue
里去,最后将动画生成的State<Color>
设置到圆弧的color属性里去,代码如下
最终我们就获得了一个会变颜色的尾巴
风车
风车的绘制方式有很多种,最简单两个交叉的粗线条就能变成一个风车,或者四个扇形也可以变成一个风车,这里我使用贝塞尔曲线来绘制风车,使用到的函数是quadraticBezierTo
,这个一般是拿来绘制二阶曲线的,首先我们先来画第一个叶片
我们看到这里的控制点选择了画布中心,以及左上角和画布上沿的中点这三个位置,得到的这样我们就获得了风车的一个叶片了
我们再用同样的方式画出其余三个叶片,代码如下
一个风车就画出来了,是不是很快,现在就是如何让风车动起来了,这个我们可以使用之前说到的函数graphicsLayer
,并且使用rotationZ
来实现旋转,但是如果仅仅只是z轴上的旋转的话,还可以使用另一个函数rotate
,它里面默认就是调用的graphicsLayer
函数
现在可以在上层调用Windcar
函数,并让它转起来
稍作优化一下,给风车加个手持棍子,这个只需要将Windcar
函数与一个Spacer
组件套在一个Box里面就好了
这样我们的风车也完成了,最终效果如下
总结
有没有觉得用Compose就能简简单单做出以前必须找设计师要切图或者json文件才能实现的动效呢,我们不妨也去试试看,把自己项目中那些loading动效用Compose去实现一下。
链接:https://juejin.cn/post/7248608019860684837
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android适配:判断机型和系统
在Android开发中,我们总是会碰到各种各样的适配问题。如果要解决适配问题,我们必须就要解决,出现问题的是什么机型?出现问题的是什么系统?怎么判断当前机型是不是出问题的机型?这几个问题。这篇文章,就将介绍如何判断机型和系统,介绍目前应该如何解决这些问题。
判断指定的机型
在Android里面可以通过 android.os.Build这个类获取相关的机型信息,它的参数如下:(这里以一加的手机为例)
Build.BOARD = lahaina
Build.BOOTLOADER = unknown
Build.BRAND = OnePlus //品牌名
Build.CPU_ABI = arm64-v8a
Build.CPU_ABI2 =
Build.DEVICE = OP5154L1
Build.DISPLAY = MT2110_13.1.0.100(CN01) //设备版本号
Build.FINGERPRINT = OnePlus/MT2110_CH/OP5154L1:13/TP1A.220905.001/R.1038728_2_1:user/release-keys
Build.HARDWARE = qcom
Build.HOST = dg02-pool03-kvm97
Build.ID = TP1A.220905.001
Build.IS_DEBUGGABLE = false
Build.IS_EMULATOR = false
Build.MANUFACTURER = OnePlus //手机制造商
Build.MODEL = MT2110 //手机型号
Build.ODM_SKU = unknown
Build.PERMISSIONS_REVIEW_REQUIRED = true
Build.PRODUCT = MT2110_CH //产品名称
Build.RADIO = unknown
Build.SERIAL = unknown
Build.SKU = unknown
Build.SOC_MANUFACTURER = Qualcomm
Build.SOC_MODEL = SM8350
Build.SUPPORTED_32_BIT_ABIS = [Ljava.lang.String;@fea6460
Build.SUPPORTED_64_BIT_ABIS = [Ljava.lang.String;@3a22d19
Build.SUPPORTED_ABIS = [Ljava.lang.String;@2101de
Build.TAGS = release-keys
Build.TIME = 1683196675000
Build.TYPE = user
Build.UNKNOWN = unknown
Build.USER = root
其中重要的属性已经设置了注释,所有的属性可以看官方文档。在这些属性中,我们一般使用 Build.MANUFACTURER 来判断手机厂商,使用 Build.MODEL 来判断手机的型号。
tips: 如果你是使用kotlin开发,可以使用 android.os.Build::class.java.fields.map { "Build.${it.name} = ${it.get(it.name)}"}.joinToString("\n")
方便的获取所有的属性
上面的获取机型的代码在鸿蒙系统(HarmonyOS)上也同样适用,下面是在华为P50 Pro的机型上测试打印的日志信息:
Build.BOARD = JAD
Build.BOOTLOADER = unknown
Build.BRAND = HUAWEI
Build.CPU_ABI = arm64-v8a
Build.CPU_ABI2 =
Build.DEVICE = HWJAD
Build.DISPLAY = JAD-AL50 2.0.0.225(C00E220R3P4)
Build.FINGERPRINT = HUAWEI/JAD-AL50/HWJAD:10/HUAWEIJAD-AL50/102.0.0.225C00:user/release-keys
Build.FINGERPRINTEX = HUAWEI/JAD-AL50/HWJAD:10/HUAWEIJAD-AL50/102.0.0.225C00:user/release-keys
Build.HARDWARE = kirin9000
Build.HIDE_PRODUCT_INFO = false
Build.HOST = cn-east-hcd-4a-d3a4cb6341634865598924-6cc66dddcd-dcg9d
Build.HWFINGERPRINT = ///JAD-LGRP5-CHN 2.0.0.225/JAD-AL50-CUST 2.0.0.220(C00)/JAD-AL50-PRELOAD 2.0.0.4(C00R3)//
Build.ID = HUAWEIJAD-AL50
Build.IS_CONTAINER = false
Build.IS_DEBUGGABLE = false
Build.IS_EMULATOR = false
Build.IS_ENG = false
Build.IS_TREBLE_ENABLED = true
Build.IS_USER = true
Build.IS_USERDEBUG = false
Build.MANUFACTURER = HUAWEI
Build.MODEL = JAD-AL50
Build.NO_HOTA = false
Build.PERMISSIONS_REVIEW_REQUIRED = true
Build.PRODUCT = JAD-AL50
Build.RADIO = unknown
Build.SERIAL = unknown
Build.SUPPORTED_32_BIT_ABIS = [Ljava.lang.String;@a90e093
Build.SUPPORTED_64_BIT_ABIS = [Ljava.lang.String;@8ce98d0
Build.SUPPORTED_ABIS = [Ljava.lang.String;@366a0c9
Build.TAGS = release-keys
Build.TIME = 1634865882000
Build.TYPE = user
Build.UNKNOWN = unknown
判断手机厂商的代码如下:
//是否是荣耀设备
fun isHonorDevice() = Build.MANUFACTURER.equals("HONOR", ignoreCase = true)
//是否是小米设备
fun isXiaomiDevice() = Build.MANUFACTURER.equals("Xiaomi", ignoreCase = true)
//是否是oppo设备
//realme 是oppo的海外品牌后面脱离了;一加是oppo的独立运营品牌。因此判断
//它们是需要单独判断
fun isOppoDevice() = Build.MANUFACTURER.equals("OPPO", ignoreCase = true)
//是否是一加手机
fun isOnePlusDevice() = Build.MANUFACTURER.equals("OnePlus", ignoreCase = true)
//是否是realme手机
fun isRealmeDevice() = Build.MANUFACTURER.equals("realme", ignoreCase = true)
//是否是vivo设备
fun isVivoDevice() = Build.MANUFACTURER.equals("vivo", ignoreCase = true)
//是否是华为设备
fun isHuaweiDevice() = Build.MANUFACTURER.equals("HUAWEI", ignoreCase = true)
需要判断指定的型号的代码如下:
//判断是否是小米12s的机型
fun isXiaomi12S() = isXiaomiDevice() && Build.MODEL.contains("2206123SC") //xiaomi 12s
如果你不知道对应机型的型号,可以看基于谷歌维护的表格,支持超过27,000台设备。如下图所示:
判断手机的系统
除了机型外,适配过程中我们还需要考虑手机的系统。但是相比于手机机型,手机的系统的判断就没有统一的方式。下面介绍几个常用的os的判断
● 鸿蒙
private static final String HARMONY_OS = "harmony";
/**
* check the system is harmony os
*
* @return true if it is harmony os
*/
public static boolean isHarmonyOS() {
try {
Class clz = Class.forName("com.huawei.system.BuildEx");
Method method = clz.getMethod("getOsBrand");
return HARMONY_OS.equals(method.invoke(clz));
} catch (ClassNotFoundException e) {
Log.e(TAG, "occured ClassNotFoundException");
} catch (NoSuchMethodException e) {
Log.e(TAG, "occured NoSuchMethodException");
} catch (Exception e) {
Log.e(TAG, "occur other problem");
}
return false;
}
● Miui
fun checkIsMiui() = !TextUtils.isEmpty(getSystemProperty("ro.miui.ui.version.name"))
private fun getSystemProperty(propName: String): String? {
val line: String
var input: BufferedReader? = null
try {
val p = Runtime.getRuntime().exec("getprop $propName")
input = BufferedReader(InputStreamReader(p.inputStream), 1024)
line = input.readLine()
input.close()
} catch (ex: IOException) {
Log.i(TAG, "Unable to read sysprop $propName", ex)
return null
} finally {
if (input != null) {
try {
input.close()
} catch (e: IOException) {
Log.i(TAG, "Exception while closing InputStream", e)
}
}
}
return line
}
● Emui 或者 Magic UI
Emui是2018之前的荣耀机型的os,18年之后是 Magic UI。历史关系如下图所示:
判断的代码如下。需要注意是对于Android 12以下的机型,官方文档并没有给出对于的方案,下面代码的方式是从网上找的,目前测试了4台不同的机型,均可正在判断。
fun checkIsEmuiOrMagicUI(): Boolean {
return if (Build.VERSION.SDK_INT >= 31) {
//官方方案,但是只适用于api31以上(Android 12)
try {
val clazz = Class.forName("com.hihonor.android.os.Build")
Log.d(TAG, "clazz = " + clazz)
true
}catch (e: ClassNotFoundException) {
Log.d(TAG, "no find class")
e.printStackTrace()
false
}
} else {
//网上方案,测试了 荣耀畅玩8C
// 荣耀20s、荣耀x40 、荣耀v30 pro 四台机型,均可正常判断
!TextUtils.isEmpty(getSystemProperty("ro.build.version.emui"))
}
}
● Color Os
下面是网上判断是否是Oppo的ColorOs的代码。经测试,在 OPPO k10 、 oppo findx5 pro、 oneplus 9RT 手机上都是返回 false,只有在 realme Q3 pro 机型上才返回了 true。
//这段代码是错的
fun checkIsColorOs() = !TextUtils.isEmpty(getSystemProperty("ro.build.version.opporom"))
从测试结果可以看出上面这段代码是错的,但是在 ColorOs 的官网上,没有找到如何判断ColorOs的代码。这种情况下,有几种方案:
1. 判断手机制造商,即 Build.MANUFACTURER 如果为 oneplus、oppo、realme就认为它是ColorOs
2. 根据系统应用的包名判断,即判断是否带有 com.coloros.* 的系统应用,如果有,就认为它是ColorOs
这几种方案都有很多问题,暂时没有找到更好的解决方法。
● Origin Os
//网上代码,在 IQOQ Neo5、vivo Y50、 vivo x70三种机型上
//都可以正常判断
fun checkIsOriginOs() = !TextUtils.isEmpty(getSystemProperty("ro.vivo.os.version"))
总结
对于手机厂商和机型,我们可以通过Android原生的 android.os.Build 类来判断。使用 Build.MANUFACTURER 来判断手机厂商,使用 Build.MODEL 来判断手机的型号。如果是不知道的型号,还可以尝试在谷歌维护的手机机型表格中查询。
但是对于厂商的系统,就没有统一的判断方法了。部分厂商有官方提供的判断方式,如Miui、Magic UI;部分厂商暂时没有找到相关的内容。这种情况下,只能通过网上的方式判断,但是部分内容也不靠谱,如判断Oppo的ColorOs。如果你有靠谱的方式,欢迎补充。
链接:https://juejin.cn/post/7241056943388983356
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
如果启动一个未注册的Activity
简述
要启动未注册的Activity主要是要逃避AMS的检测,思路是,检测前要启动的Activity换成注册的,检测通过了,再在启动前换回来。这里主要是两个点。检测前,hookAMS
。检测后hookHandler
。hook点有很多尽量找静态变量、单例和public
hookAMS
1、android 11举例,启动acitivty是在ATMS中(11之前是AMS,这个自己可以去适配)
2、拿到ATMS的代理。
3、然后ATMS整个动态代理
在startActivity之前将Intent
偷梁换柱
4、换成已经注册的Activity之后记得原目标Acitivty存起来,在骗完AMS之后换回来
public static void hookAMS() {
// 10之前
try {
Class<?> clazz = Class.forName("android.app.ActivityTaskManager");
Field singletonField = clazz.getDeclaredField("IActivityTaskManagerSingleton");
singletonField.setAccessible(true);
Object singleton = singletonField.get(null);
Class<?> singletonClass = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClass.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
Method getMethod = singletonClass.getMethod("get");
Object mInstance = getMethod.invoke(singleton);
Class IActivityTaskManagerClass = Class.forName("android.app.IActivityTaskManager");
Object mInstanceProxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{IActivityTaskManagerClass}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {
int index = -1;
// 获取 Intent 参数在 args 数组中的index值
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
// 生成代理proxyIntent -- 孙悟空(代理)的Intent
Intent proxyIntent = new Intent();
// 这个包名是宿主的
proxyIntent.setClassName("com.leo.amsplugin",
ProxyActivity.class.getName());
// 原始Intent能丢掉吗?保存原始的Intent对象
Intent intent = (Intent) args[index];
proxyIntent.putExtra(TARGET_INTENT, intent);
// 使用proxyIntent替换数组中的Intent
args[index] = proxyIntent;
}
// 原来流程
return method.invoke(mInstance, args);
}
});
// 用代理的对象替换系统的对象
mInstanceField.set(singleton, mInstanceProxy);
} catch (Exception e) {
e.printStackTrace();
}
}
hookHandler
hookAMS完成,欺骗了AMS,接下来要把Intent中的原目标扶起回正位,
启动Activity要用handler,我们从这里hook吧
1、Activtiy thread 中的handler用来启动activity class H extends Handler
2、handlerMessage中的EXECUTE_TRANSACTION(159)来启动activity
3、
final ClientTransaction transaction = (ClientTransaction) msg.obj;--包含Intent
mTransactionExecutor.execute(transaction);--执行启动
launchActivityItem
中有Intent
,而ta继承于ClientTransactionItem
,而ClientTransaction
中包含List<ClientTransactionItem>
4、所以我只要拿到msg
就可以拿到Intent
。
msg.obj --> ClientTransaction --> List mActivityCallbacks(LaunchActivityItem)
--> private Intent mIntent 替换
5、handlerMessage(MSG)之前有个callback也可以拿到msg。则会callback是一个接口,如果重写这个接口可就可重新handlerMessage这个方法,然后操作msg。
6、ActivityThread当中,Handler的构建没有传参数。
...//去ActivityThread.java里看
@UnsupportedAppUsage
final H mH = new H();
...
class H extends Handler //也没写构造方法
...//去Handler.java里看
@Deprecated
public Handler() {
this(null, false);
}
7、实际上callback是看,那么我自己替换系统的call就可以啦
8、那我通过反射
拿Handler中的mCallback
,
public void hoodHandler() {
try {
Class<?> clazz = Class.forName("android.app.ActivityThread");
Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
activityThreadField.setAccessible(true);
Object activityThread = activityThreadField.get(null);
Field mHField = clazz.getDeclaredField("mH");
mHField.setAccessible(true);
final Handler mH = (Handler) mHField.get(activityThread);
Field mCallbackField = Handler.class.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(mH, new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case 159:
// msg.obj = ClientTransaction
try {
// 获取 List<ClientTransactionItem> mActivityCallbacks 对象
Field mActivityCallbacksField = msg.obj.getClass()
.getDeclaredField("mActivityCallbacks");
mActivityCallbacksField.setAccessible(true);
List mActivityCallbacks = (List) mActivityCallbacksField.get(msg.obj);
for (int i = 0; i < mActivityCallbacks.size(); i++) {
// 打印 mActivityCallbacks 的所有item:
//android.app.servertransaction.WindowVisibilityItem
//android.app.servertransaction.LaunchActivityItem
// 如果是 LaunchActivityItem,则获取该类中的 mIntent 值,即 proxyIntent
if (mActivityCallbacks.get(i).getClass().getName()
.equals("android.app.servertransaction.LaunchActivityItem")) {
Object launchActivityItem = mActivityCallbacks.get(i);
Field mIntentField = launchActivityItem.getClass()
.getDeclaredField("mIntent");
mIntentField.setAccessible(true);
Intent proxyIntent = (Intent) mIntentField.get(launchActivityItem);
// 获取启动插件的 Intent,并替换回来
Intent intent = proxyIntent.getParcelableExtra(TARGET_INTENT);
if (intent != null) {
mIntentField.set(launchActivityItem, intent);
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
break;
}
return false;
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
总结
一个分为两步
1、hookAMS主要就是逃避ams检测,让ams检测的是一个已经注册了的activity。
2、hookHandler在生成activity之前再把activity换回来。
所以一定要熟悉动态代理,反射和Activity的启动流程。
主要通过hook,核心在于hook点
插桩
1、尽量找 静态变量 单利
2、public
动态代理
AMS检测之前我改下
链接:https://juejin.cn/post/7243272599769055292
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android 冷启动优化的3个小案例
背景
为了提高App的冷启动耗时,除了在常规的业务侧进行耗时代码优化之外,为了进一步缩短启动耗时,需要在纯技术测做一些优化探索,本期我们从类预加载、Retrofit 、ARouter方面进行了进一步的优化。从测试数据上来看,这些优化手段的收益有限,可能在中端机上加起来也不超过50ms的收益,但为了冷启动场景的极致优化,给用户带来更好的体验,任何有收益的优化手段都是值得尝试的。
类预加载
一个类的完整加载流程至少包括 加载、链接、初始化,而类的加载在一个进程中只会触发一次,因此对于冷启动场景,我们可以异步加载原本在启动阶段会在主线程触发类加载过程的类,这样当原流程在主线程访问到该类时就不会触发类加载流程。
Hook ClassLoader 实现
在Android系统中,类的加载都是通过PathClassLoader 实现的,基于类加载的父类委托机制,我们可以通过Hook PathClassLoader 修改其默认的parent 来实现。
首先我们创建一个MonitorClassLoader 继承自PathClassLoader,并在其内部记录类加载耗时
class MonitorClassLoader(
dexPath: String,
parent: ClassLoader, private val onlyMainThread: Boolean = false,
) : PathClassLoader(dexPath, parent) {
val TAG = "MonitorClassLoader"
override fun loadClass(name: String?, resolve: Boolean): Class<*> {
val begin = SystemClock.elapsedRealtimeNanos()
if (onlyMainThread && Looper.getMainLooper().thread!=Thread.currentThread()){
return super.loadClass(name, resolve)
}
val clazz = super.loadClass(name, resolve)
val end = SystemClock.elapsedRealtimeNanos()
val cost = end - begin
if (cost > 1000_000){
Log.e(TAG, "加载 ${clazz} 耗时 ${(end - begin) / 1000} 微秒 ,线程ID ${Thread.currentThread().id}")
} else {
Log.d(TAG, "加载 ${clazz} 耗时 ${(end - begin) / 1000} 微秒 ,线程ID ${Thread.currentThread().id}")
}
return clazz;
}
}
之后,我们可以在Application attach阶段 反射替换 application实例的classLoader 对应的parent指向。
核心代码如下:
companion object {
@JvmStatic
fun hook(application: Application, onlyMainThread: Boolean = false) {
val pathClassLoader = application.classLoader
try {
val monitorClassLoader = MonitorClassLoader("", pathClassLoader.parent, onlyMainThread)
val pathListField = BaseDexClassLoader::class.java.getDeclaredField("pathList")
pathListField.isAccessible = true
val pathList = pathListField.get(pathClassLoader)
pathListField.set(monitorClassLoader, pathList)
val parentField = ClassLoader::class.java.getDeclaredField("parent")
parentField.isAccessible = true
parentField.set(pathClassLoader, monitorClassLoader)
} catch (throwable: Throwable) {
Log.e("hook", throwable.stackTraceToString())
}
}
}
主要逻辑为
- 反射获取原始 pathClassLoader 的 pathList
- 创建MonitorClassLoader,并反射设置 正确的 pathList
- 反射替换 原始pathClassLoader的 parent指向 MonitorClassLoader实例
这样,我们就获取启动阶段的加载类了
基于JVMTI 实现
除了通过 Hook ClassLoader的方案实现,我们也可以通过JVMTI 来实现类加载监控。关于JVMTI 可参考之前的文章 juejin.cn/post/694278…。
通过注册ClassPrepare Callback, 可以在每个类Prepare阶段触发回调。
当然这种方案,相比 Hook ClassLoader 还是要繁琐很多,不过基于JVMTI 还可以做很多其他更强大的事。
类预加载实现
目前应用通常都是多模块的,因此我们可以设计一个抽象接口,不同的业务模块可以继承该抽象接口,定义不同业务模块需要进行预加载的类。
/**
* 资源预加载接口
*/
public interface PreloadDemander {
/**
* 配置所有需要预加载的类
* @return
*/
Class[] getPreloadClasses();
}
之后在启动阶段收集所有的 Demander实例,并触发预加载
/**
* 类预加载执行器
*/
object ClassPreloadExecutor {
private val demanders = mutableListOf<PreloadDemander>()
fun addDemander(classPreloadDemander: PreloadDemander) {
demanders.add(classPreloadDemander)
}
/**
* this method shouldn't run on main thread
*/
@WorkerThread fun doPreload() {
for (demander in localDemanders) {
val classes = demander.preloadClasses
classes.forEach {
val classLoader = ClassPreloadExecutor::class.java.classLoader
Class.forName(it.name, true, classLoader)
}
}
}
}
收益
第一个版本配置了大概90个类,在终端机型测试数据显示 这些类的加载需要消耗30ms左右的cpu时间,不同类加载的消耗时间差异主要来自于类的复杂度 比如继承体系、字段属性数量等, 以及类初始化阶段的耗时,比如静态成员变量的立即初始化、静态代码块的执行等。
方案优化思考
我们目前的方案 配置的具体类列表来源于手动配置,这种方案的弊端在于,类的列表需要开发维护,在版本快速迭代变更的情况下 维护成本较大, 并且对于一些大型App,存在着非常多的AB实验条件,这也可能导致不同的用户在类加载上是会有区别的。
在前面的小节中,我们介绍了使用自定义的 ClassLoader可以手动收集 启动阶段主线程的类列表,那么 我们是否可以在端上 每次启动时 自动收集加载的类,如果发现这个类不在现有 的名单中 则加入到名单,在下次启动时进行预加载。 当然 具体的策略还需要做详细设计,比如 控制预加载名单的列表大小, 被加入预加载名单的类最低耗时阈值, 淘汰策略等等。
Retrofit ServiceMethod 预解析注入
背景
Retrofit 是目前最常用的网络库框架,其基于注解配置的网络请求方式及Adapter的设计模式大大简化了网络请求的调用方式。 不过其并没有采用类似APT的方式在编译时生成请求代码,而是采用运行时解析的方式。
当我们调用Retrofit.create(final Class service) 函数时,会生成一个该抽象接口的动态代理实例。
接口的所有函数调用都会被转发到该动态代理对象的invoke函数,最终调用loadServiceMethod(method).invoke 调用。
在loadServiceMethod函数中,需要解析原函数上的各种元信息,包括函数注解、参数注解、参数类型、返回值类型等信息,并最终生成ServiceMethod 实例,对原接口函数的调用其实最终触发的是 这个生成的ServiceMethod invoke函数的调用。
从源码实现上可以看出,对ServiceMethod的实例做了缓存处理,每个Method 对应一个ServiceMethod。
耗时测试
这里我模拟了一个简单的 Service Method, 并调用archiveStat 观察首次调用及其后续调用的耗时,注意这里的调用还未触发网络请求,其返回的是一个Call对象。
从测试结果上看,首次调用需要触发需要消耗1.7ms,而后续的调用 只需要消耗50微妙左右。
优化方案
由于首次调用接口函数需要触发ServiceMethod实例的生成,这个过程比较耗时,因此优化思路也比较简单,收集启动阶段会调用的 函数,提前生成ServiceMethod实例并写入到缓存中。
serviceMethodCache 的类型本身是ConcurrentHashMap,所以它是并发安全的。
但是源码中 进行ServiceMethod缓存判断的时候 还是以 serviceMethodCache为Lock Object 进行了加锁,这导致 多线程触发同时首次触发不同Method的调用时,存在锁等待问题
这里首先需要理解为什么这里需要加锁,其目的也是因为parseAnnotations 是一个好事操作,这里是为了实现类似 putIfAbsent的完全原子性操作。 但实际上这里加锁可以以 对应的Method类型为锁对象,因为本身不同Method 对应的ServiceMethod实例就是不同的。 我们可以修改其源码的实现来避免这种场景的锁竞争问题。
当然针对我们的优化场景,其实不修改源码也是可以实现的,因为 ServiceMethod.parseAnnotations 是无锁的,毕竟它是一个纯函数。 因此我们可以在异步线程调用parseAnnotations 生成ServiceMethod 实例,之后通过反射 写入 Retrofit实例的 serviceMethodCache 中。这样存在的问题是 不同线程可能同时触发了一个Method的解析注入,但 由于serviceMethodCache 本身就是线程安全的,所以 它只是多做了一次解析,对最终结果并无影响。
ServiceMethod.parseAnnotations是包级私有的,我们可以在当前工程创建一个一样的包,这样就可以直接调用该函数了。 核心实现代码如下
package retrofit2
import android.os.Build
import timber.log.Timber
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.lang.reflect.Modifier
object RetrofitPreloadUtil {
private var loadServiceMethod: Method? = null
var initSuccess: Boolean = false
// private var serviceMethodCacheField:Map<Method,ServiceMethod<Any>>?=null
private var serviceMethodCacheField: Field? = null
init {
try {
serviceMethodCacheField = Retrofit::class.java.getDeclaredField("serviceMethodCache")
serviceMethodCacheField?.isAccessible = true
if (serviceMethodCacheField == null) {
for (declaredField in Retrofit::class.java.declaredFields) {
if (Map::class.java.isAssignableFrom(declaredField.type)) {
declaredField.isAccessible =true
serviceMethodCacheField = declaredField
break
}
}
}
loadServiceMethod = Retrofit::class.java.getDeclaredMethod("loadServiceMethod", Method::class.java)
loadServiceMethod?.isAccessible = true
} catch (e: Exception) {
initSuccess = false
}
}
/**
* 预加载 目标service 的 相关函数,并注入到对应retrofit实例中
*/
fun preloadClassMethods(retrofit: Retrofit, service: Class<*>, methodNames: Array<String>) {
val field = serviceMethodCacheField ?: return
val map = field.get(retrofit) as MutableMap<Method,ServiceMethod<Any>>
for (declaredMethod in service.declaredMethods) {
if (!isDefaultMethod(declaredMethod) && !Modifier.isStatic(declaredMethod.modifiers)
&& methodNames.contains(declaredMethod.name)) {
try {
val parsedMethod = ServiceMethod.parseAnnotations<Any>(retrofit, declaredMethod) as ServiceMethod<Any>
map[declaredMethod] =parsedMethod
} catch (e: Exception) {
Timber.e(e, "load method $declaredMethod for class $service failed")
}
}
}
}
private fun isDefaultMethod(method: Method): Boolean {
return Build.VERSION.SDK_INT >= 24 && method.isDefault;
}
}
预加载名单收集
有了优化方案后,还需要收集原本在启动阶段会在主线程进行Retrofit ServiceMethod调用的列表, 这里采取的是字节码插桩的方式,使用的LancetX 框架进行修改。
目前名单的配置是预先收集好,在配置中心进行配置,运行时根据配置中写的配置 进行预加载。 这里还可以提供其他的配置方案,比如 提供一个注解用于标注该Retrofit函数需要进行预解析,
之后,在编译期间收集所有需要预加载的Service及函数,生成对应的名单,不过这个方案需要一定开发成本,并且需要去修改业务模块的代码,目前的阶段还处于验证收益阶段,所以暂未实施。
收益
App收集了启动阶段20个左右的Method 进行预加载,预计提升10~20ms。
ARouter
背景
ARouter框架提供了路由注册跳转 及 SPI 能力。为了优化冷启动速度,对于某些服务实例可以在启动阶段进行预加载生成对应的实例对象。
ARouter的注册信息是在预编译阶段(基于APT) 生成的,在编译阶段又通过ASM 生成对应映射关系的注入代码。
而在运行时以获取Service实例为例,当调用navigation函数获取实例最终会调用到 completion函数。
当首次调用时,其对应的RouteMeta 实例尚未生成,会继续调用 addRouteGroupDynamic函数进行注册。
addRouteGroupDynamic 会创建对应预编译阶段生成的服务注册类并调用loadInto函数进行注册。而某些业务模块如何服务注册信息比较多,这里的loadInto就会比较耗时。
整体来看,对于获取Service实例的流程, completion的整个流程 涉及到 loadInto信息注册、Service实例反射生成、及init函数的调用。 而completion函数是synchronized的,因此无法利用多线程进行注册来缩短启动耗时。
优化方案
这里的优化其实和Retroift Service 的注册机制类似,不同的Service注册时,其对应的元信息类(IRouteGroup)其实是不同的,因此只需要对对应的IRouteGroup加锁即可。 另外由于这部分代码现在可能多线程同时在进行,部分逻辑需要进行二次判断,
在completion的后半部分流程中,针对Provider实例生产的流程也需要进行单独加锁,避免多次调用init函数。
收益
根据线下收集的数据 配置了20+预加载的Service Method, 预期收益 10~20ms (中端机) 。
其他
后续将继续结合自身业务现状以及其他一线大厂分享的样例,在 x2c、class verify、禁用JIT、 disableDex2AOT等方面继续尝试优化。
链接:https://juejin.cn/post/7249228528573513789
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
实现联动滚动
序言
在垂直滑动的过程中可以横向滚动内容。
效果
代码
就一个工具类就行了。可以通过root view向上查找recycleView。自动添加滚动监听。在子view完全显示出来以后,才分发滚动事件。这样用户才能看清楚第一个。
需要一个id资源
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="key_rv_scroll_helper" type="id"/>
</resources>
RVScrollHelper
package com.trs.myrb.provider.scroll;
import android.view.View;
import android.view.ViewParent;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.trs.myrb.R;
import com.trs.myrb.douyin.action.TRSFunction;
/**
* <pre>
* Created by zhuguohui
* Date: 2023/5/12
* Time: 13:49
* Desc:用于监听recycleView的垂直滚动,然后将垂直滚动转发给其他类。
*
* //在provider中使用
* RVScrollHelper.create(binding.getRoot(), dy -> {
* binding.rvHotVideos.scrollBy(dy,0);
* return null;
* });
* </pre>
*/
public class RVScrollHelper implements View.OnAttachStateChangeListener {
TRSFunction<Integer, Void> scrollCallBack;
View child;
RecyclerView recyclerView;
private int recyclerViewHeight;
public static RVScrollHelper create(View child,TRSFunction<Integer, Void> scrollCallBack){
if(child==null){
return null;
}
Object tag = child.getTag(R.id.key_rv_scroll_helper);
if(!(tag instanceof RVScrollHelper)){
RVScrollHelper helper = new RVScrollHelper(child, scrollCallBack);
tag=helper;
child.setTag(R.id.key_rv_scroll_helper,helper);
}
return (RVScrollHelper) tag;
}
private RVScrollHelper(View child, TRSFunction<Integer, Void> scrollCallBack) {
this.scrollCallBack = scrollCallBack;
this.child = child;
this.child.addOnAttachStateChangeListener(this);
}
@Override
public void onViewAttachedToWindow(View v) {
if(child==null){
return;
}
if (recyclerView == null) {
recyclerView = getRecyclerView(v);
recyclerViewHeight = recyclerView.getHeight();
recyclerView.addOnScrollListener(onScrollListener);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
if(recyclerView!=null) {
recyclerView.removeOnScrollListener(onScrollListener);
recyclerView=null;
}
}
private RecyclerView.OnScrollListener onScrollListener=new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
//只有在view全部显示出来以后,才发送滚动事件
boolean isAllShow=isAllShow();
if(isAllShow){
if(scrollCallBack!=null){
scrollCallBack.call(dy);
}
}
}
};
private boolean isAllShow() {
int top = child.getTop();
int bottom = child.getBottom();
return bottom>=0&&bottom<=recyclerViewHeight;
}
private RecyclerView getRecyclerView(View v) {
ViewParent parent = v.getParent();
while (!(parent instanceof RecyclerView) && parent != null) {
parent = parent.getParent();
}
if(parent!=null){
return (RecyclerView) parent;
}
return null;
}
}
使用
在provider中使用。
RVScrollHelper.create(holder.itemView, dy -> {
recyclerView.scrollBy(dy, 0);
return null;
});
链接:https://juejin.cn/post/7249985405125886009
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
每日一题:Zygote 为什么不采用Binder机制进行IPC通信呢?
在android面试中,我们常会遇到Framework面试相关问题,而今天要分享的就是Zygote 为什么不采用Binder机制进行IPC通信呢?
其主要考察的是程序员对binder的理解和zygote fork的理解。
问题正解:
这个很重要的原因是如果zygote采用binder 会导致 fork出来的进程产生死锁。
在UNIX上有一个 程序设计的准则:多线程程序里不准使用fork。
这个规则的原因是:如果程序支持多线程,在程序进行fork的时候,就可能引起各种问题,最典型的问题就是,fork出来的子进程会复制父进程的所有内容,包括父进程的所有线程状态。那么父进程中如果有子线程正在处于等锁的状态的话,那么这个状态也会被复制到子进程中。父进程的中线程锁会有对应的线程来进行释放锁和解锁,但是子进程中的锁就等不到对应的线程来解锁了,所以为了避免这种子进程出现等锁的可能的风险,UNIX就有了不建议在多线程程序中使用fork的规则。
在Android系统中,Binder是支持多线程的,Binder线程池有可以有多个线程运行,那么binder 中就自然会有出现子线程处于等锁的状态。那么如果Zygote是使用的binder进程 IPC机制,那么Zygote中将有可能出现等锁的状态,此时,一旦通过zygote的fork去创建子进程,那么子进程将继承Zygote的等锁状态。这就会出现子进程一创建,天生的就在等待线程锁,而这个锁缺没有地方去帮它释放,子进程一直处于等待锁的状态。
链接:https://juejin.cn/post/7251857574526451770
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
开发这么久,gradle 和 gradlew 啥区别、怎么选?
使用 Gradle
的开发者最常问的问题之一便是: gradle
和 gradlew
的区别? 。
这两个都是应用在特定场景的 Gradle 命令。通过本篇文章你将了解到每个命令干了什么,以及如何在两个命令中做选择。
快速摘要
如果你正在开发的项目当中已经包含
gradlew
脚本,安啦,可以一直使用它。没有包含的话,请使用gradle
命令生成这个脚本。
想知道为什么吗,请继续阅读。
gradle 命令
如果你从 Gradle 官网(gradle.org/releases)下载和安装了 Gradle 的话,你便可以使用安装在 bin 路径下的 gradle 命令了。当然你记得将该 bin 路径添加到设备的 PATH 环境变量中。
此后,在终端上运行 gradle
的话,你会看到如下输出:
你会注意到输出里打印了 Gradle 的版本,它对应着你运行的 gradle 命令在设备中的 Gradle 安装包版本。这听起来有点废话,但在谈论 gradlew 的时候需要明确这点,这很重要。
通过这个本地安装的 Gradle,你可以使用 gradle 命令做很多事情,包括:
- 使用
gradle init
命令创建一个新的 Gradle 项目或者使用gradle wrapper
命令创建 gradle wrapper 目录及文件 - 在一个 Gradle 项目内使用
gradle build
命令进行 Gradle 编译 - 通过
gradle tasks
命令查看当前的 Gradle 项目中支持哪些 task
上述的命令均使用你本地安装的 Gradle 程序,无论你安装的是什么版本。
如果你使用的是 Windows 设备,那么 gradle 命令等同于 gradle.bat,gradlew 命令等同于 gradlew.bat,非常简单。
gradlew 命令
gradlew
命令,也被了解为 Gradle wrapper
,与 gradle 命令相比它是略有不同的。它是一个打包在项目内的脚本,并且它参与版本控制,所以当年复制了某项目将自动获得这个 gradlew 脚本。
“可那又如何?”
好吧,如果你这么想。让我告诉你,它有很多重要的优势。
1. 无需本地安装 gradle
gradlew 脚本不依赖本地的 Gradle 安装。在设备上第一次运行的时候会从网络获取 Gradle 的安装包并缓存下来。这使得任何人、在任何设备上,只要拷贝了这个项目就可以非常简单地开始编译。
2. 配置固定的 gradle 版本
这个 gradlew 脚本和指定的 Gradle 版本进行绑定。这非常有用,因为这意味着项目的管理者可以强制要求该项目编译时应当使用的 Gradle 版本。
Gradle 特性并不总是互相兼容各版本的,所以使用 Gradle wrapper 可以确保项目每次编译都能获得一致性的结果。
当然这需要编译项目的人使用 gradlew 命令,如下是在项目内运行 ./gradlew
的示例:
输出和运行 gradle
命令的结果比较相似。但仔细查看你会发现版本不一样,不是上面的 6.8.2
而是 6.6.1
。
这个差异说重要也重要,说不重要也不重要。
但当使用 gradlew 的话可以免于担心由于 Gradle 版本导致的不一致性,缘自它可以保证所有的团队成员以及 CI 服务端都会使用相同的 Gradle 版本来构建这个项目。
另外,几乎所有使用 gradle
命令可以做的事情,你也可以使用 gradlew
来完成。比如编译一个项目就是 ./gradlew build
。
如果你愿意的话,可以拷贝 示例项目 并来试一下gradlew
。
gradle 和 gradlew 对比
至此你应该能看到在项目内使用 gradlew
通常是最佳选择。确保 gradlew 脚本受到版本控制,这样的话你以及其他开发者都可以收获如上章节提到的好处。
但是,难道没有任何情况需要使用 gradle
命令了吗?当然有。如果你期望在一个空目录下搭建一个新的 Gradle 项目,你可以使用 gradle init
来完成。这个命令同样会生成 gradlew 脚本。
(如下的表格简单列出两者如何选)可以说,使用 gradlew 确实是 Gradle 项目的最佳实践。
你想做什么? | gradle 还是 gradlew? |
---|---|
编译项目 | gradlew |
测试项目 | gradlew |
项目内执行其他 Gradle task | gradlew |
初始化一个 Gradle 项目或者生成 Gradle wrapper | gradle |
链接:https://juejin.cn/post/7144558236643885092
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
什么是 HTTP 长轮询?
什么是 HTTP 长轮询?
Web 应用程序最初是围绕客户端/服务器模型开发的,其中 Web 客户端始终是事务的发起者,向服务器请求数据。因此,没有任何机制可以让服务器在没有客户端先发出请求的情况下独立地向客户端发送或推送数据。
为了克服这个缺陷,Web 应用程序开发人员可以实施一种称为 HTTP长轮询的技术,其中客户端轮询服务器以请求新信息。服务器保持请求打开,直到有新数据可用。一旦可用,服务器就会响应并发送新信息。客户端收到新信息后,立即发送另一个请求,重复上述操作。
什么是 HTTP 长轮询?
那么,什么是长轮询?HTTP 长轮询是标准轮询的一种变体,它模拟服务器有效地将消息推送到客户端(或浏览器)。
长轮询是最早开发的允许服务器将数据“推送”到客户端的技术之一,并且由于其寿命长,它在所有浏览器和 Web 技术中几乎无处不在。即使在一个专门为持久双向通信设计的协议(例如 WebSockets)的时代,长轮询的能力仍然作为一种无处不在的回退机制占有一席之地。
HTTP 长轮询如何工作?
要了解长轮询,首先要考虑使用 HTTP 的标准轮询。
“标准”HTTP 轮询
HTTP 轮询由客户端(例如 Web 浏览器)组成,不断向服务器请求更新。
一个用例是想要关注快速发展的新闻报道的用户。在用户的浏览器中,他们已经加载了网页,并希望该网页随着新闻报道的展开而更新。实现这一点的一种方法是浏览器反复询问新闻服务器“内容是否有任何更新”,然后服务器将以更新作为响应,或者如果没有更新则给出空响应。浏览器请求更新的速率决定了新闻页面更新的频率——更新之间的时间过长意味着重要的更新被延迟。更新之间的时间太短意味着会有很多“无更新”响应,从而导致资源浪费和效率低下。
上图:Web 浏览器和服务器之间的 HTTP 轮询。服务器向立即响应的服务器发出重复请求。
这种“标准”HTTP 轮询有缺点:
- 更新请求之间没有完美的时间间隔。请求总是要么太频繁(效率低下)要么太慢(更新时间比要求的要长)。
- 随着规模的扩大和客户端数量的增加,对服务器的请求数量也会增加。由于资源被无目的使用,这可能会变得低效和浪费。
HTTP 长轮询解决了使用 HTTP 进行轮询的缺点
- 请求从浏览器发送到服务器,就像以前一样
- 服务器不会关闭连接,而是保持连接打开,直到有数据供服务器发送
- 客户端等待服务器的响应。
- 当数据可用时,服务器将其发送给客户端
- 客户端立即向服务器发出另一个 HTTP 长轮询请求
上图:客户端和服务器之间的 HTTP 长轮询。请注意,请求和响应之间有很长的时间,因为服务器会等待直到有数据要发送。
这比常规轮询更有效率。
- 浏览器将始终在可用时接收最新更新
- 服务器不会被永远无法满足的请求所搞垮。
长轮询有多长时间?
在现实世界中,任何与服务器的客户端连接最终都会超时。服务器在响应之前保持连接打开的时间取决于几个因素:服务器协议实现、服务器体系结构、客户端标头和实现(特别是 HTTP Keep-Alive 标头)以及用于启动的任何库并保持连接。
当然,许多外部因素也会影响连接,例如,移动浏览器在 WiFi 和蜂窝连接之间切换时更有可能暂时断开连接。
通常,除非您可以控制整个架构堆栈,否则没有单一的轮询持续时间。
使用长轮询时的注意事项
在您的应用程序中使用 HTTP 长轮询构建实时交互时,需要考虑几件事情,无论是在开发方面还是在操作/扩展方面。
- 随着使用量的增长,您将如何编排实时后端?
- 当移动设备在WiFi和蜂窝网络之间快速切换或失去连接,IP地址发生变化时,长轮询会自动重新建立连接吗?
- 通过长轮询,您能否管理消息队列并如何处理丢失的消息?
- 长轮询是否提供跨多个服务器的负载平衡或故障转移支持?
在为服务器推送构建具有 HTTP 长轮询的实时应用程序时,您必须开发自己的通信管理系统。这意味着您将负责更新、维护和扩展您的后端基础设施。
服务器性能和扩展
使用您的解决方案的每个客户端将至少每 5 分钟启动一次与您的服务器的连接,并且您的服务器将需要分配资源来管理该连接,直到它准备好满足客户端的请求。一旦完成,客户端将立即重新启动连接,这意味着实际上,服务器将需要能够永久分配其资源的一部分来为该客户端提供服务。当您的解决方案超出单个服务器的能力并且引入负载平衡时,您需要考虑会话状态——如何在服务器之间共享客户端状态?您如何应对连接不同 IP 地址的移动客户端?您如何处理潜在的拒绝服务攻击?
这些扩展挑战都不是 HTTP 长轮询独有的,但协议的设计可能会加剧这些挑战——例如,您如何区分多个客户端发出多个真正的连续请求和拒绝服务攻击?
消息排序和排队
在服务器向客户端发送数据和客户端发起轮询请求之间总会有一小段时间,数据可能会丢失。
服务器在此期间要发送给客户端的任何数据都需要缓存起来,并在下一次请求时传递给客户端。
然后出现几个明显的问题:
- 服务器应该将数据缓存或排队多长时间?
- 应该如何处理失败的客户端连接?
- 服务器如何知道同一个客户端正在重新连接,而不是新客户端?
- 如果重新连接花费了很长时间,客户端如何请求落在缓存窗口之外的数据?
所有这些问题都需要 HTTP 长轮询解决方案来回答。
设备和网络支持
如前所述,由于 HTTP 长轮询已经存在了很长时间,它在浏览器、服务器和其他网络基础设施(交换机、路由器、代理、防火墙)中几乎得到了无处不在的支持。这种级别的支持意味着长轮询是一种很好的后备机制,即使对于依赖更现代协议(如 WebSockets )的解决方案也是如此。
众所周知,WebSocket 实现,尤其是早期实现,在双重 NAT 和某些 HTTP 长轮询运行良好的代理环境中挣扎。
链接:https://juejin.cn/post/7240111396869161020
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
女朋友要我讲解@Controller注解的原理,真是难为我了
背景
女朋友被公司裁员一个月了,和我一样作为后端工程师,最近一直在找工作,面试了很多家还是没有找到工作,面试官问@Controller的原理,她表示一脸懵,希望我能给她讲清楚。之前我也没有好好整理这块知识,这次借助这个机会把它彻底搞清楚。
我们知道Controller注解的类能够实现接收并处理Http请求,其实在我看Spring mvc模块的源码之前也和我女朋友目前的状态一样,很疑惑,Spring框架是底层是如何实现的,通过使用Controller注解就简单的完成了http请求的接收与处理。
有疑问就好啊,因为兴趣是最好的老师,如果有兴趣才有动力去弄懂这个技术点。
看过前面的文章的同学就会知道,学习Spring的所有组件,脑袋里要有一个思路,那就是解析组件和运用组件两个流程,这是Spring团队实现组件的统一套路,大家可以回忆一下是不是这么回事。
一、Spring解析Controller注解
首先我们看看Spring是如何解析Controller注解的,打开源码看看他长啥样??
@Target({ElementType.TYPE})
@Component
public @interface Controller {
String value() default "";
}
发现Controller注解打上了Component的注解,这样Spring做类扫描的时候,发现了@Controller标记的类也会当作Bean解析并注册到Spring容器。
我们可以看到Spring的类扫描器,第一个就注册了Component注解的扫描
//org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider
protected void registerDefaultFilters() {
this.includeFilters.add(new AnnotationTypeFilter(Component.class));
}
这样Spring容器启动完成之后,bean容器中就有了被Controller注解标记的bean实例了。
到这里只是单纯的把Controller标注的类实例化注册到Spring容器,和Http请求接收处理没半毛钱关系,那么他们是怎么关联起来的呢?
二、Spring解析Controller注解标注的类方法
这个时候Springmvc组件中的另外一个组件就闪亮登场了
RequestMappingHandlerMapping
RequestMappingHandlerMapping 看这个名就可以知道他的意思,请求映射处理映射器。
这里就是重点了,该类间接实现了InitializingBean方法,bean初始化后执行回调afterPropertiesSet方法,里面调用initHandlerMethods方法进行初始化handlermapping。
//类有没有加Controller的注解
protected boolean isHandler(Class<?> beanType) {
return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
}
protected void initHandlerMethods() {
//所有的bean
String[] beanNames= applicationContext().getBeanNamesForType(Object.class);
for (String beanName : beanNames) {
Class<?> beanType = obtainApplicationContext().getType(beanName);
//有Controller注解的bean
if (beanType != null && isHandler(beanType)) {
detectHandlerMethods(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}
这里把标注了Controller注解的实例全部找到了,然后调用detectHandlerMethods方法,检测handler方法,也就是解析Controller标注类的方法。
private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
protected void detectHandlerMethods(final Object handler) {
Class<?> handlerType = (handler instanceof String ?
obtainApplicationContext().getType((String) handler) : handler.getClass());
if (handlerType != null) {
final Class<?> userType = ClassUtils.getUserClass(handlerType);
//查找Controller的方法
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
(MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType));
methods.forEach((method, mapping) -> {
//注册
this.registry.put(mapping,new MappingRegistration<>(mapping,method));
});
}
到这里为止,Spring将Controller标注的类和类方法已经解析完成。现在再来看RequestMappingHandlerMapping这个类的作用,他就是用来注册所有Controller类的方法。
三、Spring调用Controller注解标注的方法
接着还有一个重要的组件RequestMappingHandlerAdapter
它就是用来将请求转换成HandlerMethod,并且完成请求处理的流程。
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
@Override
public boolean supports(Object handler) {
return handler instanceof HandlerMethod;
}
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
//请求check
checkRequest(request);
//调用handler方法
mav = invokeHandlerMethod(request, response, handlerMethod);
//返回
return mav;
}
看到这里,就知道http请求是如何被处理的了,我们找到DispatcherServlet的doDispatch方法看看,确实是如此!!
四、DispatcherServlet调度Controller方法完成http请求
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 从注册表查找handler
HandlerExecutionChain mappedHandler = getHandler(request);
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 调用
ModelAndView m = ha.handle(processedRequest, response, mappedHandler.getHandler());
//
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
DispatcherServlet是Spring mvc的总入口,看到doDispatch方法后,全部都联系起来了。。。
最后我们看看http请求在Spring mvc中的流转流程。
第一次总结SpringMvc模块,理解不到位的麻烦各位大佬指正。
链接:https://juejin.cn/post/7222186286564311095
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
关于WorkManager需要知道的一切
背景
WorkManager 是google为Android推出Jetpack开发套件的成员之一,是一个持续化工作的推荐解决方案。 我们在开发过程中通常会遇到一些需要持续化的操作需求,例如,上传文件到服务端,Token过期刷新,定期更新服务端下发的配置等。
主要的用途是:
立即开始执行一个任务
有的任务必须立即开始执行,但这里可能会有个疑问,为什么立即开始执行的任务不直接写代码开始执行,要利用WorkManager呢?这主要归功于WorkManager的一些特性,例如根据约束安排任务,链式化任务等,让你代码写得更简洁,更好的扩展性。
需要长时间定期运行的任务
任务需要长时间定期运行的的情况,例如我们我需要每一个小时上传一次用户日志.
延迟任务
有的任务需要延后一段时间执行.
再继续讲解它如何使用之前先来说说它的几个特性.
特性
约束
提供一些约束条件去执行任务,当约束条件满足后才开始执行,例如,当连接上Wifi, 当设备有足够的电量等条件
可靠性
安排的任务保证能够顺利执行,因为WorkManager内部以SqLite存储任务的执行的情况,为执行的和执行失败的都会重新尝试.
加急任务
可能你会给WorkManager安排很多Task, 某些Task也许优先级比较高,需要立即执行,WorkManager提供加急的特性,可以尽早执行这类Task.
链式任务
某些Task可能需要顺序执行,也可能需要并行执行,WorkManager同样提供这类API满足需求
val continuation = WorkManager.getInstance(context)
.beginUniqueWork(
Constants.IMAGE_MANIPULATION_WORK_NAME,
ExistingWorkPolicy.REPLACE,
OneTimeWorkRequest.from(CleanupWorker::class.java)
).then(OneTimeWorkRequest.from(WaterColorFilterWorker::class.java))
.then(OneTimeWorkRequest.from(GrayScaleFilterWorker::class.java))
.then(OneTimeWorkRequest.from(BlurEffectFilterWorker::class.java))
.then(
if (save) {
workRequest<SaveImageToGalleryWorker>(tag = Constants.TAG_OUTPUT)
} else /* upload */ {
workRequest<UploadWorker>(tag = Constants.TAG_OUTPUT)
}
)
线程的互操作性
无缝继承了Coroutines和RxJava的异步特性,在WorkManager也能使用这类异步的API
实战
说了这么多,下面我们来看看如何使用WorkManager
第一步,添加依赖
dependencies {
def work_version = "2.8.1"
// (Java only)
implementation "androidx.work:work-runtime:$work_version"
// Kotlin + coroutines
implementation "androidx.work:work-runtime-ktx:$work_version"
// optional - RxJava2 support
implementation "androidx.work:work-rxjava2:$work_version"
// optional - GCMNetworkManager support
implementation "androidx.work:work-gcm:$work_version"
// optional - Test helpers
androidTestImplementation "androidx.work:work-testing:$work_version"
// optional - Multiprocess support
implementation "androidx.work:work-multiprocess:$work_version"
}
第二步,自定义Worker
这里我们需要定义,这个Task需要做什么,创建一个自定义Worker类,这里我们创建一个UploadWorker用于做一些后台上传的操作
class UploadWorker(appContext: Context, workerParams: WorkerParameters):
Worker(appContext, workerParams) {
override fun doWork(): Result {
// 做一些上传操作
uploadImages()
// 代表任务执行成功
return Result.success()
}
}
在doWork中我们可以写上任务需要执行的代码,当任务结束后需要返回一个Result,这个Result有三个值
Result.success() 任务执行成功
Result.failure() 任务执行失败
Result.retry() 任务需要重试
第三步, 创建一个WorkRequest
当定义完需要做什么后我们需要创建一个WorkRequest去启动这个任务的执行。WorkManager提供了很多灵活的API用于定义任务的启动逻辑,例如是否执行一次还是周期性执行,它的约束条件是什么等。这里演示我们使用OneTimeWorkRequest.
val uploadWorkRequest: WorkRequest =
OneTimeWorkRequestBuilder<UploadWorker>()
.build()
第四步, 提交WorkRequest
当创建完成WorkRequest,我们需要把它交给WorkManager去执行
WorkManager
.getInstance(myContext)
.enqueue(uploadWorkRequest)
进阶
一次性任务
创建一个简单的一次性任务
val myWorkRequest = OneTimeWorkRequest.from(MyWork::class.java)
如果需要增加一些配置如约束等可以使用builder
val uploadWorkRequest: WorkRequest =
OneTimeWorkRequestBuilder<MyWork>()
// 添加额外配置
.build()
加急工作
WorkManager 执行重要的工作,同时让系统更好地控制对资源的访问。
加急工作具有以下特点:
重要性:加急工作适合对用户重要或由用户启动的任务。
速度:加急工作最适合立即开始并在几分钟内完成的短任务。
配额:限制前台执行时间的系统级配额决定加急作业是否可以启动。
电源管理:电源管理限制(例如省电模式和打瞌睡模式)不太可能影响加急工作。
启动加急工作的方式也非常简单,可以直接调用setExpedited()设置该WorkRequest为一个加急任务
val request = OneTimeWorkRequestBuilder()
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(context)
.enqueue(request)
这里setExpedited会有一个参数OutOfQuotaPolicy,代表系统配额不足时候,把该任务作为一个普通任务对待.
周期性任务
我们有一些需求例如,备份应用数据,上传日志,下载一些应用配置,需要周期性进行,我们可以定义PeriodicWorkRequest去创建周期性的任务.
val saveRequest =
PeriodicWorkRequestBuilder<SaveImageToFileWorker>(1, TimeUnit.HOURS)
.build()
上面这段代码每一小时执行一次任务.
但是这个时间约束也不是固定的,这里定义的时间实际上是最小间隔时间,系统会根据当前系统的情况进行适当调整。
我们还可以定义flexInterval让间隔提前一点
val myUploadWork = PeriodicWorkRequestBuilder<SaveImageToFileWorker>(
1, TimeUnit.HOURS, // repeatInterval
15, TimeUnit.MINUTES) // flexInterval
.build()
这样我们执行任务的时间是repeatInterval - flexInterval,上面代码的任务会在1小时-15分钟的时候执行.
周期性任务遇上约束条件
当周期性任务遇到一些约束条件不满足的时候将会延迟执行,直到约束条件满足.
关于约束
WorkManager提供了下列的一些约束条件.
NetworkType 网络条件约束,例如只能在连接WIFI的情况下执行.
BatteryNotLow 非电量低约束,在有充足的电量的时候执行.
RequiresCharging 需要充电的时候执行约束
DeviceIdle 在设备无状态时候运行,这样不会对设备的效率产生影响.
StorageNotLow 当设备有有足够的存储空间时候运行
创建一个约束使用Contraints.Builder()并赋值给WorkRequest.Builder().
下面代码展示创建一个约束该任务只会在wifi并且在充电的时候执行.
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build()
val myWorkRequest: WorkRequest =
OneTimeWorkRequestBuilder<MyWork>()
.setConstraints(constraints)
.build()
延迟任务
如果你指定的任务没有约束或者约束已经满足,那么它会立即开始执行,如果想让它有个最少的延迟,可以指定一个最小的延迟执行时间.
下面这个例子展示设置最小10分钟后开始加入队列.
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
.setInitialDelay(10, TimeUnit.MINUTES)
.build()
上面展示的针对OneTimeWorkRequestBuilder同样也适用于PeriodicWorkRequest.
退避策略
如果一个任务失败返回Result.retry(), 你的任务可以在稍等进行重试,这种退避策略可以自定义,这里连个自定义的属性
退避延迟:退避延迟执行下次尝试任务的最少时间,通常我们自定义最少不能低于[MIN_BACKOFF_MILLIS]
退避策略:退避策略可以指定两种一个是LINEAR(线性)和EXPONENTIAL(幂等)
实际上每一个任务都有一个默认的退避策略,缺省的退避策略是EXPONENTIAL和30s的延迟,但是你可以自定义,下面是一个自定义的例子。
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS)
.build()
Tag任务
每一个任务都可以附加上一个标签,稍后可以使用这个标签找到该任务,取消它或者查看的执行进度. 如果你有一组任务,可以添加同一个标签,可以统一的操作它们。例如使用WorkManager.cancelAllWorkByTag(String)取消所有的任务,使用WorkManager.getWorkInfosByTag(String)返回一个任务信息列表查看当前任务的状态.
下面是一个展示给任务赋值一个标签
val myWorkRequest = OneTimeWorkRequestBuilder<MyWork>()
.addTag("cleanup")
.build()
链式任务
WorkManager还提供了一种定义顺序执行任务或者并发执行任务的方式。
使用这种方式创建任务通过
WorkManager.beginWith(OneTimeWorkRequest)
WorkManager.beginWith(List<OneTimeWorkRequest>)
以上两个方式都会返回一个WorkContinuation.
WorkContinuation随后可以继续调用
then(OneTimeWorkRequest)
then(List<OneTimeWorkRequest>)
执行链式任务
最后可以调用enqueue()去执行你的工作链. 举个例子
WorkManager.getInstance(myContext)
.beginWith(listOf(plantName1, plantName2, plantName3))
.then(cache)
.then(upload)
.enqueue()
Input Mergers
一个父任务的执行结果可以传递给子任务,例如上面plantName1, plantName2, plantName3执行的结果可以传递
给cache任务,WorkManager使用InputMerger去管理这些多个父任务的输出结果到子任务.
这里有两种不同类型的的InputMerger
OverwritingInputMerger 从输入到输出增加所有的key,遇到冲突的情况,覆盖之前的key的值
ArrayCreatingInputMerger 从输入到输出增加所有的key,遇到冲突的情况进行创建数组
工作链的状态
当前面的任务阻塞住的时候后面的任务同样也是阻塞状态.
当前面的任务执行成功后,后面的任务才能继续开始执行
当一个任务失败进行重试的时候并不会影响并发的任务
一个任务失败,后面的任务也会是失败状态
对于取消也是这样
结语
总的来说WorkManager是一个非常好用的组件,它解决了一些曾经实现起来比较繁琐的功能,例如它的约束执行,我们可以等待有网络时候执行任务。我们利用周期性执行任务功能能够很方便的执行一些诸如刷新token, 定期日志上传等功能.
链接:https://juejin.cn/post/7247404852945944635
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
末日终极坐标安卓辅助工具
前言
本工具完全免费,无需联网
本文档只介绍工具的使用方法,有时间再写一篇介绍一下实现细节。
总的来讲本工具可以帮助你时刻知道自己的坐标并知道和宝箱的位置关系,减少资源浪费。
5分钟即可完成100汽油的使用,大大节省时间。
阅读本文档前提是大家是《末日血战》等同款游戏的玩家。
工具下载安装
链接: pan.baidu.com/s/14GE-713c… 提取码: 29c5
安装工具
工具安装后,桌面会有这个图标。
在打开工具前需进入应用设置页打开这个应用的显示悬浮窗权限
。
填入初始坐标
打开工具大致显示是这个样子,坐标初始都是0,那么填入相应的坐标保存
就会是这个样子。
可以看到左上角有个小图,这是一个直角坐标系的缩略图,左上角位置是(0,0)右下角位置(301,301),拖拽可以移动位置。
小图中会显示3个红点,一个绿点。绿点表示当前坐标,红点表示终极坐标。绿点的坐标数值是固定显示在左上角的,不随绿点移动
此时可以按返回键退出app,但是不要杀掉应用。
建立坐标系
初次使用,建议可以打开小程序截一张生存之路的全屏图,然后我们打开这张图并横屏显示图片开始操作。当已经熟悉工具如何使用后可以在游戏中进行操作了
建立坐标系
打开图片或者游戏,进入到这个界面,点击左上角悬浮窗上的开始
按钮,会看到这样一个界面(没有中间两条直线)
我们的目标就是为了建立中间两条直线。
严格按图片指示的顺序操作。尽可能点击在轴线的中心位置
。
以上操作只需要执行一次,后续就不需要操作了。
完成以上操作,就得到有两条线的图了。这个时候就完成了建立坐标系。
如果坐标系建立的不太好(比较斜,良好的坐标系有助于减少误差。),可以重新再来直到满意为止。
开始寻找终极坐标
注意观察小地图,找一个我们没有到达的离的近的终极坐标为目标。可以看到x和y的差距。
举个例子,我们当前坐标49,52
,刚刚已经在48,52
这里取得了一个宝箱,那么下一个目的地选237,29
。因为是x坐标相差较大,我们x太小,而y坐标我们的大一点。所以主要的方向是加x,少量的减y。
所以我们应该按x轴正方向和y轴负方向这里走,因为x相差较大,所以如果可以的话(有障碍物就走不了)就直接沿着x轴正方向走就好了。
我想游戏玩家应该还是知道要往哪走的,但是容易算错坐标或者根本懒得记,凭运气。那么我们指定往哪走之后,接下来怎么使用这个工具。
- 1、点击一个位置(我们要让小车开到的位置),这个时候小车不会走,因为我们工具盖住了游戏
- 2、app回退一下(不是杀掉应用),这时可以发现小车在抖动了,其实就是小车可以走了,再点一下刚才那个位置,小车就会走到那个位置。这样我们就完成了一次移动和坐标记录。小地图当前坐标就会变化。绿点也会移动。
- 3、小车走完之后,我们再点
开始
,然后重复1,2 步骤。
补充
- 1、本工具存在误差,一般每次执行在小车x,y <=|2| 基本100%准确。x,y <= |3| 100个汽油可能会有|2|以内坐标的误差(仅本人测试数据)
- 2、点击位置尽可能点在地图块的中间,这样可以减少误差。遇到坐标点在路径上,可以进入其中对当前坐标进行校准,当然一般是不需要的。
- 3、如果遇到了事件,我们就处理完事件后再点
开始
按钮 - 4、
回退
怎么用:右下角回退用途是当我们不想走这一步,可以点回退
按钮撤销这一步,然后重新再点一个点。如果还是后悔不想走这一步(这个回退是指回退我们的记录,游戏中的步骤我们肯定是做不到回退的)可以再次回退,但要注意只能回退一步。 - 5、在工具操作界面(即点击
开始
后显示坐标线和按钮的那种情况),GETXY
按钮下的白色坐标数字是表示当前这一步的行进地图坐标,例如3,2
表示向x轴正方向移动3格,y轴正方向移动2格。可以通过这个坐标判断工具计算的坐标是否准确。 - 6、本次汽油用完后,就可以杀掉辅助工具app了。下次有汽油可以继续直接使用,记住使用过程中的退出都是回退而不是杀掉app
- 7、如果已经在工具上操作坐标了,但是发现汽油不够了,这个时候最好是买几个汽油仍然走到刚才记录的位置。当然也可以使用回退功能,再重新操作工具点击到你汽油够的位置。
最后
希望大家先熟悉工具流程,可以截一张图去操作,参考上文补充5的说明,通过这个坐标数值可以知道工具记录是否准确。然后再在游戏中操作避免浪费资源。如果通过截图去熟悉工具使用,在正式使用前要核对一下当前坐标是否准确。坐标可以随时矫正。
希望大家游戏愉快,也希望本工具对大家有所帮助。
链接:https://juejin.cn/post/7243081126826491941
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
为什么谷歌搜索不支持无限分页
这是一个很有意思却很少有人注意的问题。
当我用Google搜索MySQL
这个关键词的时候,Google只提供了13
页的搜索结果,我通过修改url的分页参数试图搜索第14
页数据,结果出现了以下的错误提示:
百度搜索同样不提供无限分页,对于MySQL
关键词,百度搜索提供了76
页的搜索结果。
为什么不支持无限分页
强如Google搜索,为什么不支持无限分页?无非有两种可能:
- 做不到
- 没必要
「做不到」是不可能的,唯一的理由就是「没必要」。
首先,当第1页的搜索结果没有我们需要的内容的时候,我们通常会立即更换关键词,而不是翻第2页,更不用说翻到10页往后了。这是没必要的第一个理由——用户需求不强烈。
其次,无限分页的功能对于搜索引擎而言是非常消耗性能的。你可能感觉很奇怪,翻到第2页和翻到第1000页不都是搜索嘛,能有什么区别?
实际上,搜索引擎高可用和高伸缩性的设计带来的一个副作用就是无法高效实现无限分页功能,无法高效意味着能实现,但是代价比较大,这是所有搜索引擎都会面临的一个问题,专业上叫做「深度分页
」。这也是没必要的第二个理由——实现成本高。
我自然不知道Google的搜索具体是怎么做的,因此接下来我用ES
(Elasticsearch)为例来解释一下为什么深度分页对搜索引擎来说是一个头疼的问题。
为什么拿ES举例子
Elasticsearch
(下文简称ES
)实现的功能和Google以及百度搜索提供的功能是相同的,而且在实现高可用和高伸缩性的方法上也大同小异,深度分页的问题都是由这些大同小异的优化方法导致的。
什么是ES
ES
是一个全文搜索引擎。
全文搜索引擎又是个什么鬼?
试想一个场景,你偶然听到了一首旋律特别优美的歌曲,回家之后依然感觉余音绕梁,可是无奈你只记得一句歌词中的几个字:「伞的边缘」。这时候搜索引擎就发挥作用了。
使用搜索引擎你可以获取到带有「伞的边缘」关键词的所有结果,这些结果有一个术语,叫做文档。并且搜索结果是按照文档与关键词的相关性进行排序之后返回的。我们得到了全文搜索引擎的定义:
全文搜索引擎是根据文档内容查找相关文档,并按照相关性顺序返回搜索结果的一种工具
网上冲浪太久,我们会渐渐地把计算机的能力误以为是自己本身具备的能力,比如我们可能误以为我们大脑本身就很擅长这种搜索。恰恰相反,全文检索的功能是我们非常不擅长的。
举个例子,如果我对你说:静夜思。你可能脱口而出:床前明月光,疑是地上霜。举头望明月,低头思故乡。但是如果我让你说出带有「月」的古诗,想必你会费上一番功夫。
包括我们平时看的书也是一样,目录本身就是一种符合我们人脑检索特点的一种搜索结构,让我们可以通过文档ID或者文档标题这种总领性的标识来找到某一篇文档,这种结构叫做正排索引
。
而全文搜索引擎恰好相反,是通过文档中的内容来找寻文档,诗词大会中的飞花令就是人脑版的全文搜索引擎。
全文搜索引擎依赖的数据结构就是大名鼎鼎的倒排索引
(「倒排」这个词就说明这种数据结构和我们正常的思维方式恰好相反),它是单词和文档之间包含关系的一种具体实现形式。
打住!不能继续展开了话题了,赶紧一句话介绍完ES吧!
ES
是一款使用倒排索引数据结构、能够根据文档内容查找相关文档,并按照相关性顺序返回搜索结果的全文搜索引擎
高可用的秘密——副本(Replication)
高可用是企业级服务必须考虑的一个指标,高可用必然涉及到集群和分布式,好在ES天然支持集群模式,可以非常简单地搭建一个分布式系统。
ES
服务高可用要求其中一个节点如果挂掉了,不能影响正常的搜索服务。这就意味着挂掉的节点上存储的数据,必须在其他节点上留有完整的备份。这就是副本的概念。
如上图所示,Node1
作为主节点,Node2
和Node3
作为副本节点保存了和主节点完全相同的数据,这样任何一个节点挂掉都不会影响业务的搜索。满足服务的高可用要求。
但是有一个致命的问题,无法实现系统扩容!即使添加另外的节点,对整个系统的容量扩充也起不到任何帮助。因为每一个节点都完整保存了所有的文档数据。
因此,ES
引入了分片(Shard
)的概念。
PB级数量的基石——分片(Shard)
ES
将每个索引(ES
中一系列文档的集合,相当于MySQL
中的表)分成若干个分片,分片将尽可能平均地分配到不同的节点上。比如现在一个集群中有3台节点,索引被分成了5个分片,分配方式大致(因为具体如何平均分配取决于ES
)如下图所示。
这样一来,集群的横向扩容就非常简单了,现在我们向集群中再添加2个节点,则ES
会自动将分片均衡到各个节点之上:
高可用 + 弹性扩容
副本和分片功能通力协作造就了ES
如今高可用和支持PB级数据量的两大优势。
现在我们以3个节点为例,展示一下分片数量为5
,副本数量为1
的情况下,ES
在不同节点上的分片排布情况:
有一点需要注意,上图示例中主分片和对应的副本分片不会出现在同一个节点上,至于为什么,大家可以自己思考一下。
文档的分布式存储
ES
是怎么确定某个文档应该存储到哪一个分片上呢?
通过上面的映射算法,ES
将文档数据均匀地分散在各个分片中,其中routing
默认是文档id。
此外,副本分片的内容依赖主分片进行同步,副本分片存在意义就是负载均衡、顶上随时可能挂掉的主分片位置,成为新的主分片。
现在基础知识讲完了,终于可以进行搜索了。
ES的搜索机制
一图胜千言:
- 客户端进行关键词搜索时,
ES
会使用负载均衡策略选择一个节点作为协调节点(Coordinating Node
)接受请求,这里假设选择的是Node3
节点; Node3
节点会在10个主副分片中随机选择5个分片(所有分片必须能包含所有内容,且不能重复),发送search request;- 被选中的5个分片分别执行查询并进行排序之后返回结果给
Node3
节点; Node3
节点整合5个分片返回的结果,再次排序之后取到对应分页的结果集返回给客户端。
注:实际上
ES
的搜索分为Query阶段
和Fetch阶段
两个步骤,在Query阶段
各个分片返回文档Id和排序值,Fetch阶段
根据文档Id去对应分片获取文档详情,上面的图片和文字说明对此进行了简化,请悉知。
现在考虑客户端获取990~1000
的文档时,ES
在分片存储的情况下如何给出正确的搜索结果。
获取990~1000
的文档时,ES
在每个分片下都需要获取1000
个文档,然后由Coordinating Node
聚合所有分片的结果,然后进行相关性排序,最后选出相关性顺序在990~1000
的10
条文档。
页数越深,每个节点处理的文档也就越多,占用的内存也就越多,耗时也就越长,这也就是为什么搜索引擎厂商通常不提供深度分页的原因了,他们没必要在客户需求不强烈的功能上浪费性能。
链接:https://juejin.cn/post/7136009269903622151
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
【android activity重难点突破】这些知识还不会,面试八成被劝退
Activity
作为android
四大组件之一,地位就不用多说了吧,该组件看起来是比较简单的,但是也涉及到很多知识点,要想全部理解并在合适的业务场景下使用,也是需要一定的技术沉淀,本文主要是对activity
一些重要知识点进行总结整理,可能平时不一定用到,但是一定要有所了解。
当然这些知识点并没有设计过多源码部分,比如activity
的启动流程什么的,主要是零散的知识点,对于activity
的启动流程网上文章太多了,后面自己也准备重新梳理下,好记性不如烂笔头,在不断学习整理的过程中,一定会因为某个知识点而豁然开朗。
1.生命周期
①.两个页面跳转
从MainActivity
跳转到SecordActivity
的生命周期,重点关注Main
的onPause
和onStop
与Secord
几个关键生命周期的顺序,以及从Secord
返回时与Main
的生命周期的交叉:
可以发现Main
页面的onPause
生命周期之后直接执行Secord
的onCreate,onStart,onResume
,所以onPause
生命周期内不要执行耗时操作,以免影响新页面的展示,造成卡顿感。
②.弹出Dialog
- 单纯的弹出
Dialog
是不会影响Activity
的生命周期的; - 启动
dialog theme
的Activity
的时候,启动的activity
只会执行onPause
方法,onStop
不会执行,被启动的activity
会正常走生命周期,back
的时候,启动的Activity
会对应执行onResume
方法;
③.横竖屏切换
- AndroidManifest不配置
configChanges
时,横竖屏切换,会销毁重建Activity,生命周期会重新走一遍; - 当Activity
configChanges="orientation|screenSize"
时,横竖屏切换不会重新走Activity生命周期方法,只会执行onConfigurationChanged
方法,如需要可以在此方法中进行相应业务处理;
如横竖屏切换时需要对布局进行适配,可在res下新建
layout-port
、layout-land
目录,并提供相同的xml布局文件,横竖屏切换时即可自动加载相应布局。(前提是未配置configChanges
忽略横竖屏影响,否则不会重新加载布局)
④.启动模式对生命周期的影响
1.A(singleTask)启动(startActivity)B(standard),再从B启动A,生命周期如下:
A启动B:A_onPause、B_onCreate、B_onStart、B_onResume、A_onStop
第二步:B_onPause、A_onNewIntent、A_onRestart、A_onStart、A_onResume、B_onStop、B_onDestory
2.A(singleTask)启动A,或者A(singleTop)启动A
A_onPause、A_onNewIntent、A_Resume
3.singleInstance模式的activity
多次启动A(singleInstance
),只有第一次会创建一个单独的任务栈(全局唯一),再次启动会调用A_onPause、A_onNewIntent、A_Resume
。
2.启动模式
Activity的启动模式一直是standard
、singleTop
、singleTask
、singleInstance
四种,Android 12新增了singleInstancePerTask
启动模式,在这里不一一介绍,仅介绍重要知识点。
①.singleTask
1.Activity
是一个可以跨进程、跨应用的组件,当你在 A App
里打开 B App
的Activity
的时候,这个Activity
会直接被放进A的Task
里,而对于B的Task
,是没有任何影响的。
从A应用启动B应用,默认情况下启动的B应用的Activity
会进入A应用当前页面所在的任务栈中,此时按home建,再次启动B应用,会发现B应用并不会出现A启动的页面(前提是A应用启动的不是B应用主activity
,如果是必然一样),而是如第一次启动一般.
如果想要启动B应用的时候出现被A应用启动的页面,需要设置B应用被启动页的launchmode
为singleTask
,此时从A应用的ActivityA
页面启动B应用的页面ActivityB
(launchmode
为singleTask
),发现动画切换方式是应用间切换,此时ActivityB
和ActivityA
分别处于各自的任务栈中,并没有在一个task中,此时按Home键后,再次点击启动B应用,发现B应用停留在ActivityB
页面。
如果想要实现上述效果,除了设置launchmode之外,还可以通过设置allowTaskReparenting
属性达到同样的效果,Activity
默认情况下只会归属于一个 Task,不会在多个Task之间跳来跳去,但你可以通过设置来改变这个逻辑,如果你不设置singleTask
,而是设置allowTaskReparenting
为true
,此时从A应用的ActivityA
页面启动B应用的页面ActivityB
(设置了allowTaskReparenting
为true
),ActivityB
会进入ActivityA
的任务栈,此时按Home键,点击启动B应用,会进入ActivityB
页面,也就是说ActivityB
从ActivityA
的任务栈移动到了自己的任务栈中,此时点击返回,会依次退出ActivityB
所在任务栈的各个页面,直到B应用退出。
注意:allowTaskReparenting
在不同Android版本上表现有所不同,Android9以下是生效的,Android9,10又是失效的,但Android11又修复好了,在使用时一定要好好测试,避免一些因版本差异产生的问题。
②.singleInstance
singleInstance
具备singleTask
模式的所有特性外,与它的区别就是,这种模式下的Activity
会单独占用一个Task栈,具有全局唯一性,即整个系统中就这么一个实例,由于栈内复用的特性,后续的请求均不会创建新的Activity
实例,除非这个特殊的任务栈被销毁了。以singleInstance
模式启动的Activity
在整个系统中是单例的,如果在启动这样的Activity
时,已经存在了一个实例,那么会把它所在的任务调度到前台,重用这个实例。
③.singleInstancePerTask
释义:singleInstancePerTask
的作用和singleTask
几乎一模一样,不过singleInstancePerTask不需要为启动的Activity设置一个特殊的taskAffinity就可以创建新的task,换句话讲就是设置singleInstancePerTask
模式的activity
可以存在于多个task任务栈中,并且在每个任务栈中是单例的。
多次启动设置singleInstancePerTask模式的Activity并不会多次创建新的任务栈,而是如singleInstance模式一样,把当前Activity所在的任务栈置于前台展示,如果想每次以新的任务栈启动需要设置FLAG_ACTIVITY_MULTIPLE_TASK
和FLAG_ACTIVITY_NEW_DOCUMENT
,使用方式如下:
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
此时,每次启动Activity就会单独创建新的任务栈。
注意:测试需要在Android12的真机或者模拟器上,否则默认为Standard模式
3.taskAffinity
taskAffinity
可以指定任务栈的名字,默认任务栈是应用的包名,前提是要和singleTask
,singleInstance
模式配合使用,standard
,singleTop
模式无效,当app存在多个任务栈时,如果taskAffinity
相同,则在最近任务列表中只会出现处于前台任务栈的页面,后台任务栈会“隐藏”在某处,如果taskAffinity
不同,最近任务列表会出现多个任务页面,点击某个就会把该任务栈至于前台。
4.清空任务栈
activity
跳转后设置FLAG_ACTIVITY_CLEAR_TASK
即可清空任务栈,并不是新建一个任务栈,而是清空并把当前要启动的activity
置于栈底,使用场景比如:退出登录跳转到登录页面,可以以此情况activity任务栈。
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK|Intent.FLAG_ACTIVITY_NEW_TASK);
注意:FLAG_ACTIVITY_CLEAR_TASK必须与FLAG_ACTIVITY_NEW_TASK一起使用.
5.Activity.FLAG
FLAG_ACTIVITY_NEW_TASK
FLAG_ACTIVITY_NEW_TASK
并不像起名字一样,每次都会创建新的task任务栈,而是有一套复杂的规则来判断:
- 通过
activity
类型的context
启动,如果要启动的Activity
的taskAffinity
与当前Activity
不一致,则会创建新的任务栈,并将要启动的Activity
置于栈底,taskAffinity
一致的话,就会存放于当前activity
所在的任务栈(注意启动模式章节第三点taskAffinity
的知识点); taskAffinity
一致的情况下,如果要启动的activity
已经存在,并且是栈根activity
,那么将没有任何反应(启动不了要启动的activity
)或者把要启动的activity
所在的任务栈置于前台;否则如果要启动的activity
不存在,将会在当前任务栈创建要启动的activity
实例,并入栈;taskAffinity
一致的情况下,如果要启动的activity
已经存在,但不是栈根activity
,依然会重新创建activity
示例,并入栈(前提是:要启动的activity
的launchMode
为standard
,意思就是是否会创建新实例会受到launchMode
的影响);- 非
activity
的context
启动activity
时(比如在service
或者broadcast
中启动activity
),在android7.0
之前和9.0
之后必须添加FLAG_ACTIVITY_NEW_TASK
,否则会报错(基于android-32的源码,不同版本可能不同):
//以下代码基于android 12
public void startActivity(Intent intent, Bundle options) {
warnIfCallingFromSystemProcess();
final int targetSdkVersion = getApplicationInfo().targetSdkVersion;
//检测FLAG_ACTIVITY_NEW_TASK
if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
&& (targetSdkVersion < Build.VERSION_CODES.N
|| targetSdkVersion >= Build.VERSION_CODES.P)
&& (options == null
|| ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
//未设置FLAG_ACTIVITY_NEW_TASK,直接抛出异常
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
+ " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ " Is this really what you want?");
}
//正常启动activity
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
(Activity) null, intent, -1, options);
}
注意:FLAG_ACTIVITY_NEW_TASK的设置效果受到taskAffinity以及其他一些配置的影响,实际使用过程中一定要进行充分测试,并且不同的android版本也会表现不同,极端场景下要仔细分析测试,选择最优方案;
提示:通过
adb shell dumpsys activity activities
命令可以查看activity任务栈;
6.多进程
正常情况下,app运行在以包名为进程名的进程中,其实android四大组件支持多进程,通过manifest配置process
属性,可以指定与包名不同的进程名,即可运行在指定的进程中,从而开启多进程,那么,开启多进程有什么优缺点呢?
多进程下,可以分散内存占用,可以隔离进程,对于比较重的并且与其他模块关联不多的模块可以放在单独的进程中,从而分担主进程的压力,另外主进程和子进程不会相互影响,各自做各自的事,但开启了多进程后,也会带来一些麻烦事,比如会引起Application
的多次创建,静态成员失效,文件共享等问题。
所以是否选择使用多进程要看实际需要,我们都知道app进程分配的内存是有限的,超过系统上限就会导致内存溢出,如果想要分配到更多的内存,多进程不失为一种解决方案,但是要注意规避或处理一些多进程引起的问题;
设置多进程的方式:
android:process=":childProcess" //实际上完整的进程名为:包名:childProcess,这种方式声明的属于私有进程。
android:process="com.child.process" //完整的进程名即为声明的名字:com.child.process,这种方式声明的属于全局进程。
7.excludeFromRecents
excludeFromRecents
如果设置为true
,那么设置的Activity
将不会出现在最近任务列表中,如果这个Activity
是整个Task
的根Activity
,整个Task将不会出现在最近任务列表中.
8.startActivityForResult被弃用
使用Activity Result Api代替,使用方式如下:
private val launcherActivity = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) {
Log.e("code","resultCode = "+it.resultCode)
}
findViewById<Button>(R.id.btn_jump).setOnClickListener {
launcherActivity.launch(Intent(this@MainActivity,SecordActivity::class.java))
}
//要跳转的Activity设置回调数据:
val resultIntent = Intent()
resultIntent.putExtra("dataKey","data value")
setResult(1001,resultIntent)
finish()
关于registerForActivityResult更多请点击这里查看。
9.Deep link
简单理解,所谓Deep Link就是可以通过外部链接来启动app或者到达app指定页面的一想技术,比如可以通过点击短信或者网页中的链接来拉起app到指定页面,以达到提供日活或者其他目的,一般流程是可以通过在manifest
的activity
标签中配置固定的scheme
来实现这种效果,形如:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:scheme="jumptest"
android:host="work"
android:port="8801"
android:path="/main"
/>
</intent-filter>
然后在网页中就可以通过如下方式来启动当前activity:
<a href="jumptest://work:8801/main?data=123456">你好</a>
格式
<scheme>://<host>:<port>/<path>?<query>
被启动的app可以通过如下方式拿到传递的参数以及schmea配置项:
val host = schemeIntent.data?.host
val path = schemeIntent.data?.path
val scheme = schemeIntent.data?.scheme
val query = schemeIntent.data?.query
Log.e("scheme","host = $host, path = $path, scheme = $scheme, query = $query")
结果:
注意:
1.
intent-filter
与Main主Activity搭配使用时,要单独开启一个intent-filter,否则匹配不到。
2.从android12开始,设置了intent-filter
标签后,activity的exported必须设置成true,这个要注意(android12之前,其实添加了intent-filter,系统也会默认设置exported为true)。
①.app link
App link是一种特殊的Deep link,它的作用就是可以使通过网站地址打开app的时候,不需要用户选择使用哪个应用来打开,换种说法就是,我可以设置默认打开次地址的应用,这样一来,就可以直接引导到自己的app。
更多关于App link的可以参考这篇文章,或者看官网介绍。
10.setResult和finish的顺序关系
通过startActivityForResult
启动activity
,通常会在被启动的activity
的合适时机调用setResult
来回调数据给上一个页面,然后当前页面返回的时候就会回调onActivityResult
,这里要注意setResult
的调用时机,请一定要在activity的finish()方法之前调用,否则可能不会生效(不会回调onActivityResult)。
原因如下:
private void finish(int finishTask) {
if (mParent == null) {
int resultCode;
Intent resultData;
//会在finish的时候把回调数据赋值
synchronized (this) {
resultCode = mResultCode;
resultData = mResultData;
}
···
if (ActivityClient.getInstance().finishActivity(mToken, resultCode, resultData,
finishTask)) {
mFinished = true;
}
} else {
mParent.finishFromChild(this);
}
···
}
//setResult对mResultCode,mResultData赋值
public final void setResult(int resultCode) {
synchronized (this) {
mResultCode = resultCode;
mResultData = null;
}
}
由上述代码可以看出,setResult
必须在finish
之前赋值,才能够在finish
的时候拿到需要callback
的数据,以便在合适的时机回调onActivityResult
;
11.onSaveInstanceState()和onRestoreInstanceState()
activity
在非正常情况被销毁的时候(非正常情况:横竖屏切换,系统配置发生变化,内存不足后台activity
被回收等),当重新回到该activity
,系统会重新实例化该对象,如果没有对页面输入的内容进行保存,就会存在内容丢失的情况,此时可以通过onSaveInstanceState
来保存页面数据,在onCreate
或者onRestoreInstanceState
中对数据进行恢复,形如:
override fun onSaveInstanceState(outState: Bundle) {
outState.putString("SAVE_KEY","SAVE_DATA")
outState.putString("SAVE_KEY","SAVE_DATA2")
super.onSaveInstanceState(outState)
}
//需要判空,savedInstanceState不一定有值
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(null != savedInstanceState){
saveData = savedInstanceState.getString("SAVE_KEY") ?: ""
saveData2 = savedInstanceState.getString("SAVE_KEY2") ?: ""
}
setContentView(R.layout.activity_main)
}
//或者在onRestoreInstanceState恢复数据,无需判空,回调此方法一定有值
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
saveData = savedInstanceState.getString("SAVE_KEY") ?: ""
saveData2 = savedInstanceState.getString("SAVE_KEY2") ?: ""
super.onRestoreInstanceState(savedInstanceState)
}
注意:请使用
onSaveInstanceState(outState: Bundle)
一个参数的方法,两个参数的方法和Activity
的persistableMode
有关。
本文主要对Activity重难点知识进行整理和解释,希望对大家有所帮助,当然难免存在错误,如有发现,希望指正,如果感觉不错,麻烦点个赞,这将给我持续更文以更大的动力,后续如有其他知识点,也会持续更新。
链接:https://juejin.cn/post/7130176321690288142
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
浅谈中间人攻击
之前聊了一些前端安全性的问题,好多小伙伴反馈说聊的东西比较浅,很轻松就搞懂了,但实际上,真正生产环境下的网络安全问题多数是由一种或者多种简单的攻击方式混合导致的,所以掌握好基本的网络安全原理还是很有必要的,今天来聊聊中间人攻击。
中间人攻击
所谓的中间人攻击一般发生在双方通信的过程当中,使用技术手段对通信进行拦截,然后基于通信的中间进行信息的转发,让通信双方错以为双方是在一条私密的信道上进行通信,实际上,整个会话已经被中间人完全掌控,其中的信息自然也是一览无余。
发生中间人攻击,信息泄露是肯定的了,还可能发生信息的篡改,所以危害比较大。
中间人攻击分析
和其他的安全问题一样,中间人攻击吃准了通信的双方缺乏授信的手段,然后占据双方通信的信道。基于这样的思路,可以想到的中间人攻击策略:
1、wifi欺骗:这个实际上是实现起来难度最小的一种攻击方法,大概的方法是攻击者创建一个周围受信的wifi,比如周围商场或者饭店为名称的wifi,引诱受害者链接wifi访问网络,这个时候数据通过wifi进行访问,那么在wife端很容易可以监控到使用者的信息。
2、HTTPS欺骗:利用大家对https协议的信任,通过一些是是而非的网站,比如:apple.com和Apple.com,或者浏览器识别但是肉眼不识别的特殊字符,比如o、ο,一个是英文的o,一个是希腊字母的omicron,肉眼不可见,但是浏览器确实会区分。
3、SSL劫持:通过给通信一方下发假的(中间人的)证书来阶段通信双方通信,一般以伪造SSL证书来攻击。
4、DNS欺骗:好多小伙伴在进入特殊的内网环境,比如公司的办公网,可能会配置自己的dns问题,比如windows当中的hosts文件,DNS欺骗就是通过修改DNS服务器的解析信息,将要访问的域名解析到中间人的服务器上。
5、电子邮件劫持:这个是近年来听说最多的一种攻击方式(我们公司的邮箱也发生过),这种攻击更需要社会学的知识,比如,以公司财务的类似的邮箱地址发送退税等邮件,诱惑受害者点击攻击链接。
当然攻击方式还有很多,但是上面的5种是我们常见的攻击方式。
思考:
中间人攻击其实已经相当有危害性了,因为这个攻击的发起人在了解技术的同时,对受害人的一些信息也是很了解的,比如:社会关系,家庭住址,对中间人攻击的防御更多的是需要考虑到使用网络的谨慎:
1、不随便链接模式的wife。
2、不要忽略浏览器的安全警告,好多小伙伴完全不在意。
3、不访问一些不好的网站(嘿嘿嘿)
4、定期查看自己的网络访问情况。
5、不要把核心的个人隐私放到计算机的浏览器缓存当中,比如银行卡的支付密码。
今天聊的是纯粹的理论,还是欢迎各位大佬多多指点。
链接:https://juejin.cn/post/7229336438725689403
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
从底层理解CAS原语
什么是硬件同步原语?
为什么硬件同步原语可以替代锁呢?要理解这个问题,你要首先知道硬件同步原语是什么。
硬件同步原语(Atomic Hardware Primitives)是由计算机硬件提供的一组原子操作,我们比较常用的原语主要是CAS和FAA这两种。
CAS(Compare and Swap),它的字面意思是:先比较,再交换。我们看一下CAS实现的伪代码:
<< atomic >>
function cas(p : pointer to int, old : int, new : int) returns bool {
if *p ≠ old {
return false
}
*p ← new
return true
}
它的输入参数一共有三个,分别是:
- p: 要修改的变量的指针。
- old: 旧值。
- new: 新值。
返回的是一个布尔值,标识是否赋值成功。
通过这个伪代码,你就可以看出CAS原语的逻辑,非常简单,就是先比较一下变量p当前的值是不是等于old,如果等于,那就把变量p赋值为new,并返回true,否则就不改变变量p,并返回false。
这是CAS这个原语的语义,接下来我们看一下FAA原语(Fetch and Add):
<< atomic >>
function faa(p : pointer to int, inc : int) returns int {
int value <- *location
*p <- value + inc
return value
}
FAA原语的语义是,先获取变量p当前的值value,然后给变量p增加inc,最后返回变量p之前的值value。
讲到这儿估计你会问,这两个原语到底有什么特殊的呢?
上面的这两段伪代码,如果我们用编程语言来实现,肯定是无法保证原子性的。而原语的特殊之处就是,它们都是由计算机硬件,具体说就是CPU提供的实现,可以保证操作的原子性。
我们知道, 原子操作具有不可分割性,也就不存在并发的问题。所以在某些情况下,原语可以用来替代锁,实现一些即安全又高效的并发操作。
CAS和FAA在各种编程语言中,都有相应的实现,可以来直接使用,无论你是使用哪种编程语言,它们底层的实现是一样的,效果也是一样的。
接下来,还是拿我们熟悉的账户服务来举例说明一下,看看如何使用CAS原语来替代锁,实现同样的安全性。
CAS版本的账户服务
假设我们有一个共享变量balance,它保存的是当前账户余额,然后我们模拟多个线程并发转账的情况,看一下如何使用CAS原语来保证数据的安全性。
这次我们使用Go语言来实现这个转账服务。先看一下使用锁实现的版本:
package main
import (
"fmt"
"sync"
)
func main() {
// 账户初始值为0元
var balance int32
balance = int32(0)
done := make(chan bool)
// 执行10000次转账,每次转入1元
count := 10000
var lock sync.Mutex
for i := 0; i < count; i++ {
// 这里模拟异步并发转账
go transfer(&balance, 1, done, &lock)
}
// 等待所有转账都完成
for i := 0; i < count; i++ {
<-done
}
// 打印账户余额
fmt.Printf("balance = %d \n", balance)
}
// 转账服务
func transfer(balance *int32, amount int, done chan bool, lock *sync.Mutex) {
lock.Lock()
*balance = *balance + int32(amount)
lock.Unlock()
done <- true
}
这个例子中,我们让账户的初始值为0,然后启动多个协程来并发执行10000次转账,每次往账户中转入1元,全部转账执行完成后,账户中的余额应该正好是10000元。
如果你没接触过Go语言,不了解协程也没关系,你可以简单地把它理解为进程或者线程都可以,这里我们只是希望能异步并发执行转账,我们并不关心这几种“程”他们之间细微的差别。
这个使用锁的版本,反复多次执行,每次balance的结果都正好是10000,那这段代码的安全性是没问题的。接下来我们看一下,使用CAS原语的版本。
func transferCas(balance *int32, amount int, done chan bool) {
for {
old := atomic.LoadInt32(balance)
new := old + int32(amount)
if atomic.CompareAndSwapInt32(balance, old, new) {
break
}
}
done <- true
}
这个CAS版本的转账服务和上面使用锁的版本,程序的总体结构是一样的,主要的区别就在于,“异步给账户余额+1”这一小块儿代码的实现。
那在使用锁的版本中,需要先获取锁,然后变更账户的值,最后释放锁,完成一次转账。我们可以看一下使用CAS原语的实现:
首先,它用for来做了一个没有退出条件的循环。在这个循环的内部,反复地调用CAS原语,来尝试给账户的余额+1。先取得账户当前的余额,暂时存放在变量old中,再计算转账之后的余额,保存在变量new中,然后调用CAS原语来尝试给变量balance赋值。我们刚刚讲过,CAS原语它的赋值操作是有前置条件的,只有变量balance的值等于old时,才会将balance赋值为new。
我们在for循环中执行了3条语句,在并发的环境中执行,这里面会有两种可能情况:
一种情况是,执行到第3条CAS原语时,没有其他线程同时改变了账户余额,那我们是可以安全变更账户余额的,这个时候执行CAS的返回值一定是true,转账成功,就可以退出循环了。并且,CAS这一条语句,它是一个原子操作,赋值的安全性是可以保证的。
另外一种情况,那就是在这个过程中,有其他线程改变了账户余额,这个时候是无法保证数据安全的,不能再进行赋值。执行CAS原语时,由于无法通过比较的步骤,所以不会执行赋值操作。本次尝试转账失败,当前线程并没有对账户余额做任何变更。由于返回值为false,不会退出循环,所以会继续重试,直到转账成功退出循环。
这样,每一次转账操作,都可以通过若干次重试,在保证安全性的前提下,完成并发转账操作。
其实,对于这个例子,还有更简单、性能更好的方式:那就是,直接使用FAA原语。
func transferFaa(balance *int32, amount int, done chan bool) {
atomic.AddInt32(balance, int32(amount))
done <- true
}
FAA原语它的操作是,获取变量当前的值,然后把它做一个加法,并且保证这个操作的原子性,一行代码就可以搞定了。看到这儿,你可能会想,那CAS原语还有什么意义呢?
在这个例子里面,肯定是使用FAA原语更合适,但是我们上面介绍的,使用CAS原语的方法,它的适用范围更加广泛一些。类似于这样的逻辑:先读取数据,做计算,然后更新数据,无论这个计算是什么样的,都可以使用CAS原语来保护数据安全,但是FAA原语,这个计算的逻辑只能局限于简单的加减法。所以,我们上面讲的这种使用CAS原语的方法并不是没有意义的。
另外,你需要知道的是,这种使用CAS原语反复重试赋值的方法,它是比较耗费CPU资源的,因为在for循环中,如果赋值不成功,是会立即进入下一次循环没有等待的。如果线程之间的碰撞非常频繁,经常性的反复重试,这个重试的线程会占用大量的CPU时间,随之系统的整体性能就会下降。
缓解这个问题的一个方法是使用Yield(), 大部分编程语言都支持Yield()这个系统调用,Yield()的作用是,告诉操作系统,让出当前线程占用的CPU给其他线程使用。每次循环结束前调用一下Yield()方法,可以在一定程度上减少CPU的使用率,缓解这个问题。你也可以在每次循环结束之后,Sleep()一小段时间,但是这样做的代价是,性能会严重下降。
所以,这种方法它只适合于线程之间碰撞不太频繁,也就是说绝大部分情况下,执行CAS原语不需要重试这样的场景。
总结
本文讲述了CAS和FAA这两个原语。这些原语,是由CPU提供的原子操作,在并发环境中,单独使用这些原语不用担心数据安全问题。在特定的场景中,CAS原语可以替代锁,在保证安全性的同时,提供比锁更好的性能。
链接:https://juejin.cn/post/7250424380369731645
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
一文看懂什么是fork/join
什么是Fork/Join
Fork/Join 是JUC并发包下的一个并行处理框架,实现了ExecutorService接口的多线程处理器,它专为那些可以通过递归分解成更细小的任务而设计,最大化的利用多核处理器来提高应用程序的性能。
Fork/Join的运行流程大致如下所示:
需要注意的是,图里的次级子任务可以一直分下去,一直分到子任务足够小为止,这里体现了分而治之(divide and conquer) 的算法思想。
工作窃取算法
工作窃取算法指的是在多线程执行不同任务队列的过程中,某个线程执行完自己队列的任务后从其他线程的任务队列里窃取任务来执行。
工作窃取流程如下图所示:
值得注意的是,当一个线程窃取另一个线程的时候,为了减少两个任务线程之间的竞争,我们通常使用双端队列来存储任务。被窃取的任务线程都从双端队列的头部拿任务执行,而窃取其他任务的线程从双端队列的尾部执行任务。
另外,当一个线程在窃取任务时要是没有其他可用的任务了,这个线程会进入阻塞状态以等待再次“工作”。
Fork/Join 实践
前面说Fork/Join框架简单来讲就是对任务的分割与子任务的合并,所以要实现这个框架,先得有任务。在Fork/Join框架里提供了抽象类ForkJoinTask来实现任务。
ForkJoinTask
ForkJoinTask是一个类似普通线程的实体,但是比普通线程轻量得多。
fork()方法:使用线程池中的空闲线程异步提交任务
public final ForkJoinTask<V> fork() {
Thread t;
// ForkJoinWorkerThread是执行ForkJoinTask的专有线程,由ForkJoinPool管理
// 先判断当前线程是否是ForkJoin专有线程,如果是,则将任务push到当前线程所负责的队列里去
if ((t = Thread.currentThread()) instanceof ForkJoinWorkerThread)
((ForkJoinWorkerThread)t).workQueue.push(this);
else
// 如果不是则将线程加入队列
// 没有显式创建ForkJoinPool的时候走这里,提交任务到默认的common线程池中
ForkJoinPool.common.externalPush(this);
return this;
}
其实fork()只做了一件事,那就是把任务推入当前工作线程的工作队列里。
join()方法:等待处理任务的线程处理完毕,获得返回值。
我们看下join()的源码:
public final V join() {
int s;
// doJoin()方法来获取当前任务的执行状态
if ((s = doJoin() & DONE_MASK) != NORMAL)
// 任务异常,抛出异常
reportException(s);
// 任务正常完成,获取返回值
return getRawResult();
}
/**
* doJoin()方法用来返回当前任务的执行状态
**/
private int doJoin() {
int s; Thread t; ForkJoinWorkerThread wt; ForkJoinPool.WorkQueue w;
// 先判断任务是否执行完毕,执行完毕直接返回结果(执行状态)
return (s = status) < 0 ? s :
// 如果没有执行完毕,先判断是否是ForkJoinWorkThread线程
((t = Thread.currentThread()) instanceof ForkJoinWorkerThread) ?
// 如果是,先判断任务是否处于工作队列顶端(意味着下一个就执行它)
// tryUnpush()方法判断任务是否处于当前工作队列顶端,是返回true
// doExec()方法执行任务
(w = (wt = (ForkJoinWorkerThread)t).workQueue).
// 如果是处于顶端并且任务执行完毕,返回结果
tryUnpush(this) && (s = doExec()) < 0 ? s :
// 如果不在顶端或者在顶端却没未执行完毕,那就调用awitJoin()执行任务
// awaitJoin():使用自旋使任务执行完成,返回结果
wt.pool.awaitJoin(w, this, 0L) :
// 如果不是ForkJoinWorkThread线程,执行externalAwaitDone()返回任务结果
externalAwaitDone();
}
我们在之前介绍过说Thread.join()会使线程阻塞,而ForkJoinPool.join()会使线程免于阻塞,下面是ForkJoinPool.join()的流程图:
RecursiveAction和RecursiveTask
通常情况下,在创建任务的时候我们一般不直接继承ForkJoinTask,而是继承它的子类RecursiveAction和RecursiveTask。
两个都是ForkJoinTask的子类,RecursiveAction可以看做是无返回值的ForkJoinTask,RecursiveTask是有返回值的ForkJoinTask。
此外,两个子类都有执行主要计算的方法compute(),当然,RecursiveAction的compute()返回void,RecursiveTask的compute()有具体的返回值。
ForkJoinPool
ForkJoinPool是用于执行ForkJoinTask任务的执行(线程)池。
ForkJoinPool管理着执行池中的线程和任务队列,此外,执行池是否还接受任务,显示线程的运行状态也是在这里处理。
我们来大致看下ForkJoinPool的源码:
@sun.misc.Contended
public class ForkJoinPool extends AbstractExecutorService {
// 任务队列
volatile WorkQueue[] workQueues;
// 线程的运行状态
volatile int runState;
// 创建ForkJoinWorkerThread的默认工厂,可以通过构造函数重写
public static final ForkJoinWorkerThreadFactory defaultForkJoinWorkerThreadFactory;
// 公用的线程池,其运行状态不受shutdown()和shutdownNow()的影响
static final ForkJoinPool common;
// 私有构造方法,没有任何安全检查和参数校验,由makeCommonPool直接调用
// 其他构造方法都是源自于此方法
// parallelism: 并行度,
// 默认调用java.lang.Runtime.availableProcessors() 方法返回可用处理器的数量
private ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory, // 工作线程工厂
UncaughtExceptionHandler handler, // 拒绝任务的handler
int mode, // 同步模式
String workerNamePrefix) { // 线程名prefix
this.workerNamePrefix = workerNamePrefix;
this.factory = factory;
this.ueh = handler;
this.config = (parallelism & SMASK) | mode;
long np = (long)(-parallelism); // offset ctl counts
this.ctl = ((np << AC_SHIFT) & AC_MASK) | ((np << TC_SHIFT) & TC_MASK);
}
}
WorkQueue:
双端队列,ForkJoinTask存放在这里。
runState:
ForkJoinPool的运行状态。SHUTDOWN状态用负数表示,其他用2的幂次表示。
当工作线程在处理自己的工作队列时,会从队列首取任务来执行(FIFO);如果是窃取其他队列的任务时,窃取的任务位于所属任务队列的队尾(LIFO)。
ForkJoinPool与传统线程池最显著的区别就是它维护了一个工作队列数组(volatile WorkQueue[] workQueues,ForkJoinPool中的每个工作线程都维护着一个工作队列)。
Fork/Join的使用
上面我们说ForkJoinPool负责管理线程和任务,ForkJoinTask实现fork和join操作,所以要使用Fork/Join框架就离不开这两个类了,只是在实际开发中我们常用ForkJoinTask的子类RecursiveTask 和RecursiveAction来替代ForkJoinTask。
下面我们用一个计算斐波那契数列第n项的例子来看一下Fork/Join的使用:
斐波那契数列数列是一个线性递推数列,从第三项开始,每一项的值都等于前两项之和:
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89······
如果设f(n)为该数列的第n项(n∈N*),那么有:f(n) = f(n-1) + f(n-2)。
public class FibonacciTest {
class Fibonacci extends RecursiveTask<Integer> {
int n;
public Fibonacci(int n) {
this.n = n;
}
// 主要的实现逻辑都在compute()里
@Override
protected Integer compute() {
// 这里先假设 n >= 0
if (n <= 1) {
return n;
} else {
// f(n-1)
Fibonacci f1 = new Fibonacci(n - 1);
f1.fork();
// f(n-2)
Fibonacci f2 = new Fibonacci(n - 2);
f2.fork();
// f(n) = f(n-1) + f(n-2)
return f1.join() + f2.join();
}
}
}
@Test
public void testFib() throws ExecutionException, InterruptedException {
ForkJoinPool forkJoinPool = new ForkJoinPool();
System.out.println("CPU核数:" + Runtime.getRuntime().availableProcessors());
long start = System.currentTimeMillis();
Fibonacci fibonacci = new Fibonacci(40);
Future<Integer> future = forkJoinPool.submit(fibonacci);
System.out.println(future.get());
long end = System.currentTimeMillis();
System.out.println(String.format("耗时:%d millis", end - start));
}
}
上面例子的输出:
- CPU核数:4
- 计算结果:102334155
- 耗时:9490 ms
- 需要注意的是,上述计算时间复杂度为O(2^n),随着n的增长计算效率会越来越低,这也是上面的例子中n不敢取太大的原因。
总结
并不是所有的任务都适合Fork/Join框架,比如上面的例子任务划分过于细小反而体现不出效率。因为Fork/Join是使用多个线程协作来计算的,所以会有线程通信和线程切换的开销。
如果要计算的任务比较简单,直接使用单线程会更快一些。但如果要计算的东西比较复杂,计算机又是多核的情况下,就可以充分利用多核CPU来提高计算速度。
链接:https://juejin.cn/post/7250718023815741477
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Kotlin的一些细节与技巧
kotlin在使用过程中有些有着一些小细节需要注意,搞清楚这些能够更加高效的使用Kotlin,下面来介绍一下。
查看字节码
kotlin本质上在编译之后还是会跟java一样生成字节码,as的工具自带查看字节码工具,能让我们看到一些舒服的kotlin操作背后的秘密 点击生成文件的Decompile 能看到kotlin文件从字节码到java代码后的结果,不过可读性并不是很好
扩展方法的小坑
Kotlin提供了扩展方法和扩展属性,能够对一些我们无法修改源码的类,增加一些额外的方法和属性 一个很简单的例子,String是JDK提供的类,我们没有办法直接修改它的源码,但是我们又经常会做一些判空、判断长度的操作,在以往使用Java的时候,我们会使用TextUtils.isEmpty来判断,但是有了Kotlin之后,我们可以像下面这样,给String定义一个扩展方法,之后在方法体中,使用this就可以方法到当前的String对象,从而实现**「看起来」**为这个类新增了一个方法的效果,如下所示
fun main() {
"".isEmpty()
}
fun String.isEmpty(): Boolean {
return this.length > 0
}
这实际上是Kotlin编译器的魔法,最终它在调用时还是以一个方法的形式,「所以扩展方法并没有真正的为这个类增加新的方法」,而只是让你在写代码时可以像调用方法一样调用工具类,来增加代码的可读性,看下它的字节码 了解这一原理之后,我们就可以理解在一些特殊case下,Kotlin的扩展为什么表现的有点不符合预期,
- 扩展类中一样签名的方法,将无效
class People {
fun run() = println("people run")
}
fun People.run() = println("extend run")
fun main() {
val people = People()
people.run()
}
//people run
因为从底层来看,类People自己的方法和扩展方法,方法签名是一样的,Kotlin编译器发现本身有这个方法了,就不会再给你做扩展方法的调用
- 扩展方法跟随声明时候类型
open class Fruit {
}
class Apple : Fruit() {
}
fun Fruit.printSelf() = println("Fruit")
fun Apple.printSelf() = println("Apple")
fun main() {
val fruit = A()
fruit.printSelf()
//注意这里
val apple1: Fruit = Apple()
apple1.printSelf()
val apple2 = Apple()
apple2.printSelf()
}
// 输出结果是
// Fruit
// Fruit
// Apple
但是第二个的输出结果却是Fruit,把apple的类型声明成了Fruit,虽然它是一个Apple的实例,但Kotlin编译器又不知道你运行时到底是什么,你声明是Fruit,就给你调用Fruit的扩展方法。
inline来帮你性能优化
在高阶函数在调用时总会创建新的Function对象,当被频繁调用,那么就会有大量的对象被创建,除此之外还可能会有基础类型的装箱拆箱问题,不可避免的就会导致性能问题,为此,「Kotlin为我们提供了inline关键字」。 inline的作用**,内联**,通过inline,我们可以把**「函数调用替换到实际的调用处」**,从而避免Function对象的创建,进一步避免性能损耗,看下代码以及 main方法的调用不再直接调用foo函数,而是把foo函数的函数体直接拷贝了过来进行调用, 不过也不能滥用inline,因为inline是在编译时进行代码的替换,那么就意味着你inline的函数体里的代码,会被替换到每一个调用的地方,从而导致字节码的膨胀,如果对产物对产物大小有严格的要求,需要关注下这个副作用。
借助reified来实现真泛型
在java中我们都知道由于编译时的类型擦除,JVM的泛型其实都是假泛型,如下的代码在编译时往往会报错
fun <T> foo() {
println(T::class.java) // 会报错
}
但是Kotlin为我们提供了**「reified关键字」,通过这个关键字,我们就可以让上面的代码成功编译并且运行,不过还需要「搭配inline关键字」**
inline fun <reified T> fooReal() {
println(T::class.java)
}
由于inline会把函数体替换到调用处,调用处的泛型类型一定是确定的,那么就可以直接把泛型参数进行替换,从而达成了「真泛型」的效果,比如使用上面的fooReal
fooReal<String>()
//调用它的打印方法时 替换为String类型
println(String::class.java)
Lateinit 和 by lazy的使用场景
这两个经常会被使用到用来实现变量的延迟初始化,不过二者还是有些区别的
- lateinit
在声明变量时不知道它的初始值是多少,依赖后续的流程来赋值,可以节省变量判空带来的便利。不过需要确保后续是会对其赋值的,不然会有异常出现
- lazy
**「一个对象的创建需要消耗大量的资源,而我不知道它到底会不会被用到」**的场景,并且只有在第一次被调用的时候才会去赋值。
fun main() {
val lazyTest by lazy {
println("create lazyTest instance")
}
println("before create")
val value = lazyTest
println("after create")
}
// before create
// create lazyTest instance
// after create
Sequence来提高性能
Kotlin为我们提供了大量的集合操作函数来简化对集合的操作,比如filter、map等,但是这些操作符往往**「伴随着性能的损耗」**,比如如下代码
fun main() {
val list = (1..20).toList()
val result = list.filter {
print("$it ")
it % 2 == 0
}.also {
println()
}.filter {
print("$it ")
it % 5 == 0
}
println()
println(result.toString())
}
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// 2 4 6 8 10 12 14 16 18 20
// [10, 20]
可以看出,我们定义了一个1~20的集合,然后通过两次调用**「filter」**函数,来先筛选出集合中的偶数,再筛选出集合中的5的倍数,最后得到结果10和20,让我们看下这个舒服的fliter操作符的实现
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
return filterTo(ArrayList<T>(), predicate)
}
可以看到,每次filter操作都会创建一个新的集合对象,如果你的操作次数很多并且你的集合对象很大,那么就会有额外的性能开销 「如果你对集合的操作次数比较多的话,这时候就需要Sequence来优化性能」
fun main() {
val list = (1..20).toList()
val sequenceResult = list.asSequence()
.filter {
print("$it ")
it % 2 == 0
}.filter {
print("$it ")
it % 5 == 0
}
val iterator = sequenceResult.iterator()
iterator.forEach {
print("result : $it ")
}
}
// 1 2 2 3 4 4 5 6 6 7 8 8 9 10 10 result : 10 11 12 12 13 14 14 15 16 16 17 18 18 19 20 20 result : 20
对于Sequence,由于它的计算是惰性的,在调用filter的时候,并不会立即计算,只有在调用它的iterator的next方法的时候才会进行计算,并且它并不会像List的filter一样计算完一个函数的结果之后才会去计算下一个函数的结果,「而是对于一个元素,用它直接去走完所有的计算」。 在上面的例子中,对于1,它走到第一个filter里面,不满足条件,直接就结束了,而对于5,它走到第一个filter里面,符合条件,这个时候会继续拿它去走第二个filter,不符合条件,就返回了,对于10,它走到第一个filter里面,符合条件,这个时候会继续拿它去走第二个filter,依然符合条件,最终就被输出了出来
Unit与void的区别
在Kotlin中,如果一个方法没有声明返回类型,那么它的返回类型会被默认设置为**「Unit」,但是「Unit并不等同于Java中的void」**关键字,void代表没有返回值,而Unit是有返回值的,如下
fun main() {
val foo = foo()
println(foo.javaClass)
}
fun foo() {
}
// 输出结果:class kotlin.Unit
继续跟进下看看Unit的实现
public object Unit {
override fun toString() = "kotlin.Unit"
}
在Kotlin中是函数作为一等公民,而不是对象。这一个特性就决定了它可以使用函数进行传递和返回。因此,Kotlin中的高阶函数应用就很广。高阶函数至少就需要一个函数作为参数,或者返回一个函数。如果我们没有在明明函数声明中明确的指定返回类型,或者没有在Lambda函数中明确返回任何内容,它就会返回Unit。 比如 如下实现实际是相同的
fun funcionNoReturnAnything(){
}
fun funcionNoReturnAnything():Unit{
}
或者是在lambda函数体中最后一个值会作为返回值返回,如果没有明确返回,就会默认返回Unit
view.setOnclickListener{
}
view.setOnclickListener{
Unit
}
Kotlin的包装类型
kotlin是字节码层面跟java是一样的,但是java中在基础类型有着 **「原始类型和包装类型」**的区别,比如int和Integer,但是在kotlin中我们只有Int这一种类型,那么kotlin编译器是如何做到区分的呢?先看一段kotlin代码以及反编译java之后的代码 可以看出
- 对于不可空的基础类型,Kotlin编译器会自动为我们选择使用原始类型,而对于可空的基础类型,Kotlin编译器则会选择使用包装类型
- 对于集合这种只能传包装类的情况,不论你是传可空还是不可空,都会选择使用包装类型
老生常谈run、let、also、with
run、let、apply、also、with都是Kotlin官方为我们提供的高阶函数,通常对比着4个操作符,
- 差异
我们关注receiver、argument、return之间的差异,如图所示
- 场景
简而言之
- **「run」**适用于在顶层进行初始化时使用
- **「let」**在被可空对象调用时,适用于做null值的检查,let在被非空对象调用时,适用于做对象的映射计算,比如说从一个对象获取信息,之后对另一个对象进行初始化和设置最后返回新的对象
- **「apply」**适用于做对象初始化之后的配置
- **「also」**适用于与程序本身逻辑无关的副作用,比如说打印日志等
==和===
在Java中我们一般使用==来判断两个对象的引用是否相等,使用equals方法来判断两个**「对象值」**是否相等 但是在Kotlin中,==和equals是相等的用来判断值,使用===来判断两个对象的引用是否相等
高阶函数
kotlin中一等公民是函数,函数也可以作为另一个函数的入参或者返回值,这就是高阶函数。 不过JVM本身是没有函数类型的,那Kotlin是如何实现这种效果的呢?先看段kotlin代码以及反编译了java的代码,一切就一目了然 我们可以看到,最终foo方法传入的类型是一个Function0类型,然后调用了Function0的invoke方法,继续看下Function0类型
public interface Function0<out R> : Function<R> {
/** Invokes the function. */
public operator fun invoke(): R
}
看来魔法就在这里 也就是说如下的两种写法也是等价的
//kotlin
fun main() {
foo {
println("foo")
}
}
//java
public static void main(String[] args) {
foo(new Function0<Unit>() {
@Override
public Unit invoke() {
System.out.println("foo");
return Unit.INSTANCE;
}
});
}
到这里是不是对高阶函数有着更深刻的认识了呢 Kotlin的高阶函数本质上是通过对函数的抽象,然后在运行时通过创建Function对象来实现的。
链接:https://juejin.cn/post/7250426696346878008
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android单元测试
本文主要内容
- 1、单元测试介绍
- 2、java单元测试
- 3、android单元测试
- 4、常用方法介绍
1、单元测试介绍
单元测试,是指对软件中的最小可测试单元进行检查和验证。
在Java中,最小单元可以是类也可以是方法,比如刚刚开发完成一个下载的方法,此时可以用单元测试其是否ok。如果不用单元测试,用手写代码调用的方式,则工作量会较大。
使用Android studio进行单元测试,一共有两种类型,一种就是普通的java单元测试,另一种就是android单元测试,android单元测试包括对ui测试,activity的相关方法进行测试等等,需要context参数
进行单元测试需要引入对应的依赖。
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
前面3个依赖包,在创建工程的时候会默认加进来,最后一个貌似不会默认添加,需要手动添加。最后一个依赖包与activity相关的单元测试有关。
2、java单元测试
以一个最简单的例子,计算器为例:
public class Util {
public static int add(int a, int b){
return a + b;
}
public int addInt(int a, int b){
return a + b;
}
}
Util类中有一个静态方法,一个非静态方法,都是简单的相加逻辑。接下来,可以右键选中方法,然后点击goto选项,生成对应的单元测试文件。
最后一步中可以选择为当前类中的哪些方法添加单元测试,也可以勾选before和after两个选项,顾名思义,before和after方法分别在单元测试前后调用,我们可以在这两个方法中做一些事情,例如初始化、回收等等。
public class UtilTest {
Util util;
@Before
public void setUp() throws Exception {
util = new Util();
System.out.println("sutup");
}
@After
public void tearDown() throws Exception {
System.out.println("tearDown");
}
@Test
public void add() {
assertEquals(2,Util.add(1, 1));
}
@Test
public void addInt() {
assertEquals(2, util.addInt(1,1));
}
}
Util类中,写了一个静态方法和非静态方法,其实就是为了演示 setUp 方法的作用,如果在单元测试中需要初始化一些类,则可以在 setUp 中初始化,在测试方法中使用已经初始化过的实例即可。
Java单元测试运行依赖于 JVM,执行单元测试方法非常简单,右键单元测试文件执行即可,也可以选择某个方法,只执行这一个方法。
3、android单元测试
Android单元测试,它依赖于Android的执行环境,也就是需要在android机器上运行。与java单元测试相比,它有一点点的不同。
前一章中讲过java单元测试,提到了 before 和 after 这两个选项,有点类似于切面编程,可以在其中做一些初始化的动作。但android中最常用的是activity,如何在activity中也添加一些周期回调函数呢?
@Rule
public ActivityTestRule rule = new ActivityTestRule(MainActivity.class){
@Override
protected Intent getActivityIntent() {
Intent intent = new Intent();
intent.putExtra("data","world");
return intent;
}
@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
Log.i("okunu","before");
}
};
通过如上方式添加activity相关的单元测试周期回调函数。
getActivityIntent ,顾名思义,对启动activity的intent进行测试封装,上例中就添加了相关的参数。值得注意的是,为何 intent 中没有添加 action 呢?我猜想就是 ActivityTestRule 对象已经与MainActivity相关联了,它就是要去启动MainActivity的,加不加action都无所谓了。这里也隐含了另一层意思,要对某个activity相关的任何方法进行单元测试,都要添加与之相关联的ActivityTestRule 对象。
beforeActivityLaunched ,就是在activity启动之前执行的函数
本例中,有一个EditText,TextView和一个Button,点击Button,将EditText中的文字显示到TextView,同时也会接收Intent中的相关参数,显示在TextView中
public class MainActivity extends AppCompatActivity {
String mData;
TextView text;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mData = getIntent().getStringExtra("data");
text = (TextView)findViewById(R.id.text);
text.setText(mData != null ? mData : "");
}
public void sayHello(View view){
EditText edit = (EditText)findViewById(R.id.edit);
String str = "hello " + mData + " " + edit.getText().toString() + " !";
text.setText(str);
}
}
它的单元测试类依然可以和第2节一样生成,我们看看详细代码:
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {
@Rule
public ActivityTestRule rule = new ActivityTestRule(MainActivity.class){
@Override
protected Intent getActivityIntent() {
Intent intent = new Intent();
intent.putExtra("data","world");
return intent;
}
@Override
protected void beforeActivityLaunched() {
super.beforeActivityLaunched();
Log.i("okunu","before");
}
};
Context appContext;
@Before
public void setUp() throws Exception {
Log.i("okunu","setUp");
appContext = InstrumentationRegistry.getTargetContext();
}
@After
public void tearDown() throws Exception {
Log.i("okunu","tearDown");
}
@Test
public void sayHello() {
onView(withId(R.id.edit)).perform(typeText("jim"), closeSoftKeyboard()); //line 1
onView(withText("hello")).perform(click()); //line 2
String expectedText = "hello " + "world " + "jim" + " !";
onView(withId(R.id.text)).check(matches(withText(expectedText))); //line 3
}
}
注意,context是可以获取的。另外最重要的就是理解这几个生命周期回调函数的作用。可以在setUp函数中获取context,如果与activity启动相关的要改动,则在ActivityTestRule类中修改即可。
4、常用方法介绍
在android单元测试中需要获取到某个view,如何获取呢?
- withText:通过文本来获取对象,如:ViewInteraction save = onView(withText(“保存”)) ;
- withId:通过id来获取对象,如:ViewInteraction save = onView(withId(R.id.save)) ;
通过文本获取,如上例,如果某个view上的文本是“保存”,则返回此view。通过id获取就比较容易理解了,建议使用id方式。
那么对view操作的接口又有哪些呢?
使用方式是onView(…).perform() 。也可以执行多个操作在一个perform中如:perform(click(),clearText()) 。所有的操作都有一个前提 ———— 就是要执行的view必须在当前界面上显示出来(有且可见)。
方法名 | 含义 |
---|---|
click() | 点击view |
clearText() | 清除文本内容 |
swipeLeft() | 从右往左滑 |
swipeRight() | 从左往右滑 |
swipeDown() | 从上往下滑 |
swipeUp() | 从下往上滑 |
click() | 点击view |
closeSoftKeyboard() | 关闭软键盘 |
pressBack() | 按下物理返回键 |
doubleClick() | 双击 |
longClick() | 长按 |
scrollTo() | 滚动 |
replaceText() | 替换文本 |
openLinkWithText() | 打开指定超链 |
链接:https://juejin.cn/post/7225092351357616184
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
写了个程序员真香的简历制作工具
看效果
不废话直入主题,这里是编辑页面,你可以来尝试一下
用法
左边写Markdown
内容,右边显示渲染效果,纯前台数据,不涉及数据库
扩展语法
Markdown
本身并没有实现多列布局,以及针对简历布局的需要,所以我自己写了个插件来支持这些语法并实现Markdown
的一些常用语法(只实现了常用的),比如多列布局、图标、个人信息、主体内容布局语法等
多列布局
实现一个三列布局,原理就是flex
::: start
第1列
:::
第2列
:::
第3列
::: end
多列布局显示效果
图标语法
下面的语法将被解析为<i></i>
,可以通过下面即将介绍的自定义CSS
来对其进行样式设置,感兴趣可以自行尝试一下.
icon:github
图标显示效果
其他语法
更多可以查看这里的语法助手
工具栏
为了更自由的定制化需求,暴露了一些工具提供使用,这里以编写CSS
为例,写的CSS
可以直接作用在简历模板上
编写CSS
这里所有的样式都需要写在.jufe
容器下面,以防影响到其他节点的样式
设置后的效果
其他小功能比如自定义主题配色、自定义字体颜色、调节模板边距等可以自行尝试一下,就不过多赘述了,都是比较实用的功能
两种编辑模式
提供了两种编辑模式,markdown
模式和内容模式,提供内容模式主要就是给一些不熟悉 Markdown
的同学使用,那这里就介绍一下内容模式吧
点击切换编辑模式(左侧右侧工具栏都有提供切换按钮,这里使用左侧的)
这样就切换到了内容模式,这两种模式之间的数据是同步的,可以自由切换
内容模式本质就是一个富文本编辑器模式,就类似写
word
一样就可以了,比如想修改图标,直接点击想修改的图标就会弹出图标选择框,点击想替换的图标后直接就完成了替换,这个感兴趣的自行尝试吧
模板
模板目前有十几个,都随便用,没事的时候就会更新一下
其他
其实这个工具还有很多功能都没介绍,因为全部写下来的话篇幅会太大,感兴趣可以自己去尝试,都是些比较实用的功能
仓库
感兴趣可以点个Star
,感谢支持,如果你有不错的Idea
,欢迎给仓库贡献代码.
链接:https://juejin.cn/post/7245836790040657978
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
为什么强烈不建议使用继承
这两天有空的时候看了下 继承和复合如何选择这个知识点,其实之前开发的时候遇到类似的问题我是无脑继承的,也没有考虑这么多,因为这些新增的父子类,都是在包内使用,而且父子类基本都是我们同一个开发人员,所以一般不会有什么意外情况。
但是如果我们要开发新的类,这个类需要对外开放,有很多模块会来继承我们的类(或者我们会继承第三方提供的公共类),这个时候就需要很小心的设计了,如果小伙伴们觉得以后不会接到这样的需求,其实就不用继续看的,下面内容还是有点枯燥无聊的🤣。
学习的内容
1. 继承(Inheritance)是什么
继承其实不用过多的去解释,因为大家都是非常熟悉的,它和封装(encapsulation) 、抽象(abstraction) 、多态(polymorphism) 组成面向对象编程的(Object-Oriented Programming)主要特征。
- 代码示例
//父类
public class Animal {
//名称
protected String name;
//种类
String species;
protected String getName() {
return name;
}
protected void setName(String name) {
this.name = name;
}
protected String getSpecies() {
return species;
}
protected void setSpecies(String species) {
this.species = species;
}
}
//子类
public class Birds extends Animal {
//翅膀长度
protected String wingSize;
protected String getWingSize() {
return wingSize;
}
protected void setWingSize(String wingSize) {
this.wingSize = wingSize;
}
}
总结:
继承的优点:
- 子类可以复用父类的代码,继承父类的特性,可以减少重复的代码量
- 父子类之前结构层次更加清晰
继承的缺点:
- 父子类之间属于强耦合性,一旦父类改动(比如增加参数),很可能会影响到子类,这就导致代码变得脆弱
- 如果子类新增一个方法,但是后续父类升级之后,和子类的方法签名相同返回类型不同,这会导致子类编译失败
- 会破坏封装性
- 不能进行访问控制
缺点第三条的解释:
下面是新建了一个集成HashSet的类,主要目的是想统计这个实例一共添加过多少次元素
- addAll:批量增加数据
- add:单个数据增加
最终统计出来的结果是 4 ,只是因为 super.addAll(c) 最终会调用add方法,也就导致重复计数了。
出现这种情况是因为我们在编写子类逻辑时不清楚父类方法的实现细节,从而造成了错误,即使我们把add中的addCount++ 删除,也同样不能保证父类的逻辑会不会变动,这样就会导致子类非常脆弱且不可控,简单总结就是子类依赖了父类的实现细节,所以这就是为什么会说破坏了封装性。
封装性:将数据和行为结合,形成一个整体,使用者不用了解内部细节,只能通过对外提供的接口进行访问
@Slf4j
public class DestroyInheritance<E> extends HashSet<E> {
private int addCount = 0;
@Override
public boolean add(E o) {
addCount++;
return super.add(o);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
public int getAddCount() {
return addCount;
}
public static void main(String[] args) {
//测试类,使用addAll批量增加
DestroyInheritance<String> item = new DestroyInheritance<>();
String[] arr = new String[]{"s","a"};
item.addAll(Arrays.asList(arr));
log.info("count:{}",item.getAddCount());
}
}
2.复合是什么
复合从字面意思上也是可以理解的,就是将多个实例的行为和特征组合成一个新的类,简单理解就是新增的这个类,它拥有其他一个或者多个类的特征,比如家用汽车有轮子、底盘、发动机等等组件组成,那车子这个类就包含了轮子类和底盘类这些属性。
看一下下面这段代码,Car是一个使用了复合的类,他包含了引擎类Engine和轮胎类Tyre,那为什么要这样写呢,我的想法是有下面几点:
- 在doSomething方法中,我无需全部继承引擎类或者轮胎类,只需要根据实际情况调用某些方法即可,减少了之前对父类的严重依赖,造成的耦合性影响
- 引擎类只需要提供个别公共的方法给Car类使用,不需要完全暴露其内部细节,也不用担心会出现类似addAll最终调用add的问题
- 代码示例
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
/**
* 汽车类
**/
@Slf4j
public class Car {
//引擎类实例
private final Engine engine;
//轮胎类实例
private final Tyre tyre;
public Car(Engine engine,Tyre tyre) {
this.engine = engine;
this.tyre = tyre;
}
public void doSomething(){
//自定义逻辑
engine.setBrand("坏牌子轮胎");
}
public String getEngineBrand(){
//返回轮胎名称
return engine.getBrand();
}
public static void main(String[] args) {
Engine engine = new Engine();
engine.setBrand("好牌子引擎");
engine.setPower("250");
Tyre tyre = new Tyre();
tyre.setBrand("好牌子轮胎");
tyre.setSize("50cm");
Car car = new Car(engine, tyre);
car.doSomething();
log.info("轮胎名称:{}",car.getEngineBrand());
}
}
/**
* 引擎类
*/
@Data
class Engine{
private String brand;
private String power;
}
/**
* 轮胎类
*/
@Data
class Tyre{
private String brand;
private String size;
}
3.继承和复合如何选择
说了半天,那究竟是用复合还是用继承呢,我觉得最重要的一点我觉得是要搞清楚类之间的关系,对于继承而言,它是 "is-a" 的关系,是对事情的一种比如:人是动物、华为mate60是手机,只是对于动物、手机这种是更为抽象的事物,人和华为是对其类的衍生。
复合则是 "has-a" 的关系,比如:健康的人有两只眼睛、家用汽车有四个轮子,对于这种情况而言,我们就需要用到复合。
继承和复合并非是绝对的好与坏,而是我们要结合实际情况,如果是is-a关系,只有当子类和父类非常明确存在这种关系时,我们可以使用继承,并且在代码设计时,一定要考虑日后可能出现的继承问题及后续代码的升级迭代,不然很可能出现令人崩溃的后续问题;而如果某一个对象只是新增类中的一个属性时,我们就要使用复合来解决问题。
总结
写这篇文章也搜索了一些其他博主的文章,总体的感受就是国内主流博客上相关的文章并不多,大家好像并不关心这个点😑,搜到有几篇还都是直接抄书放上去(而且长得都一模一样),很是郁闷,最后还是去跳出去看了几篇别人的文章,感觉还是有很多点还是得仔细琢磨琢磨,后面再继续学习学习。
链接:https://juejin.cn/post/7250091744527269944
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
挂起函数的返回值
返回值类型
协程中挂起函数的
返回值类型是 Object,
无论代码中写的是什么。
我们写的协程代码编译成 Java 代码后,挂起函数的返回值类型就会被修改成 Object,如下:
// 定义一个挂起函数,其返回值类型是 Int
private suspend fun test2(): Int {...}
// javap -v 反编译对应的 class 文件
.method private final test2(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
通过反编译可以看到,挂起函数额外多了个 Continuation 类型的参数,返回值类型也变成了 Object。关于前者它是协程能恢复的关键,是协程底层原理的基础知识,此处忽略。对于后者是本文重点。
返回值类型被修改的原因
调用到挂起函数时会返回特殊对象 COROUTINE_SUSPENDED
,最终也会返回自己定义的返回值。
一个挂起函数会被调用多次,当它执行到另一个挂起函数时会返回 COROUTINE_SUSPENDED 给调用者。执行到函数最后时,它会返回该返回的值给调用者。因此,挂起函数会返回两种类型的数据,所以返回结果型只能是 Object 类型。
验证
为验证上面结论,以下面代码为例说明
private suspend fun test2(): Int {
// withContext 是挂起函数
val a = withContext(Dispatchers.IO) {
delay(100)
1
}
return 1 + a
}
首先通过 as 自带的 show kotlin bytecode 查看上述代码对应的 java 代码,如下
关于 if 判断是否成立,可以直接反编译生成的 apk,向 apk 中插入代码,可以发现它和 var5 是同一个对象,所以 if 判断成立,因此此时 test() 返回的是 COROUTINE_SUSPENDED。
现在确定下上图中的 $continuation 到底是什么类型,反编译 apk 查看 smali 代码,可以看到 $continuation 其实是 MainActivity$test2$1 类型。
// test2 定义在 MainActivity 类中,所以生成的内部类都是 MainActivity$ 开头
new-instance v0, Lcom/example/demo/MainActivity$test2$1;
invoke-direct {v0, p0, p1}, Lcom/example/demo/MainActivity$test2$1;-><init>(Lcom/example/demo/
MainActivity;Lkotlin/coroutines/Continuation;)V
:goto_0
move-object p1, v0
.local p1, "$continuation":Lkotlin/coroutines/Continuation;
MainActivity$test2$1 继承 ContinuationImpl,最核心代码是它的 invokeSuspend(),对应的 smali 代码如下,看懂它的代码有助于我们理解 test2() 第二次执行逻辑:
.method public final invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object;
.locals 2
// 将 p1 赋值给 p0 的 result 中
// p0 是当前对象。invokeSuspend() 非 static 函数,默认有一个参数 this,即 p0
// 这句代码就是:将参数赋值给当前对象的 result 字段
iput-object p1, p0, Lcom/example/demo/MainActivity$test2$1;->result:Ljava/lang/Object;
// v0 = p0.label。即将当前对象的 label 赋值给 v0
iget v0, p0, Lcom/example/demo/MainActivity$test2$1;->label:I
// v1 = Int.MIN_VALUE
const/high16 v1, -0x80000000
// v0 与 v1 或运算,并将结果存储至 v0
or-int/2addr v0, v1
// 将 v0 赋值给 this.label
iput v0, p0, Lcom/example/demo/MainActivity$test2$1;->label:I
// this$0 是 jvm 中内部类添加的一个字段,用于表示外问类的引用,此处即 MainActivity 对象
// 这句话就是将 MainActivity 赋值给 v0
iget-object v0, p0, Lcom/example/demo/MainActivity$test2$1;->this$0:Lcom/example/demo/MainActivity;
// 用 v1 指向当前对象,即 v1 = this
move-object v1, p0
// 判断 v1 是不是 instanceof Continuation,肯定成立
check-cast v1, Lkotlin/coroutines/Continuation;
// 调用 MainActivity 的静态方法 access$test2,同时传入参数 MainActivity 实例
// 以及当前类对象
invoke-static {v0, v1}, Lcom/example/demo/MainActivity;->access$test2(Lcom/example/demo/MainActivity;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
// 将上面 access$test2() 执行结果赋值给 v0
move-result-object v0
// 返回 v0,也就是返回 access$test2() 的执行结果
return-object v0
.end method
在这段代码的最开始会将参数赋值给对象的 result 属性,结合验证一节中的截图 $result 字段,看一下它的赋值,就可以明白为啥 $result 取到的是挂起函数的返回值了。
上面代码提到了 MainActivity 的静态方法 access$test2 方法,看一眼,代码更简单:
.method public static final synthetic access$test2(Lcom/example/demo/MainActivity;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
.locals 1
.param p0, "$this" # Lcom/example/demo/MainActivity;
.param p1, "$completion" # Lkotlin/coroutines/Continuation;
.line 16
// 直接执行 MainActivity 的 test2() 方法
invoke-direct {p0, p1}, Lcom/example/demo/MainActivity;->test2(Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
// 同时将 test2() 的返回值直接返回
move-result-object v0
return-object v0
.end method
目前可知 test2() 由 invokeSuspend() 调用的,那该方法是由谁调用的呢?根据协程的基础知识可知,协程的恢复都是由它的 resumeWith() 开始的,该方法定义在 BaseContinuationImpl 中,如下:
上图中会调用 invokeSuspend(),也就是调用本节分析的 invokeSuspend() 方法,最终会执行到 test2() 方法,拿到 test2() 的最终返回值。结合 while 死循环,最终会执行到 test3() 后面的步骤。
以上就是协程的挂起恢复流程,也说明了挂起函数的返回值为啥是 Object。
链接:https://juejin.cn/post/7249633061440880700
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android面试经:Broadcast需要注意哪些细节
前言
在android中,广播也是我们经常使用的组件,但是因为大部分使用场景简单,所以关注不多,今天就让我们来探索一下Broadcast。
注册
这个是常识了,两种注册方式:静态注册(menifast)和动态注册,不展开说了。
这里注意动态注册后,我们一般会手动进行注销,不过如果没有手动注销,当context对象被销毁时,Broadcast会自动注销,但是我们还是及时注销释放资源。
线程及ANR
默认Broadcast都是运行在主线程,而且android对它的运行(onReceive)有一个时间限制——10秒,即ANR时间,所以不要在onReceive执行耗时操作。
但是Broadcast其实可以运行在其他线程(这时候就没有时间限制了),但是必须是动态注册才可以,Context的registerReceiver其实是一系列函数,其中就有
public abstract Intent registerReceiver(BroadcastReceiver receiver,
IntentFilter filter, @Nullable String broadcastPermission,
@Nullable Handler scheduler)
这里可以传入一个Handler,这个Handler就可以是其他线程创建的,这样就可以在其他线程运行Broadcast。
官方说明如下:
This method is always called within the main thread of its process, unless you
explicitly asked for it to be scheduled on a different thread using
{@linkandroid.content.Context#registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler)
}. When it runs on the main
thread you should
never perform long-running operations in it (there is a timeout of
10 seconds that the system allows before considering the receiver to
be blocked and a candidate to be killed). You cannot launch a popup dialog
in your implementation of onReceive().
那么既然onReceive中不能执行耗时操作,我们是否可以在onReceive中开启一个新的线程来处理?
在onReceive中开启新的线程,因为与其生命周期有关,所以下面与生命周期一起来说。
生命周期
Broadcast生命周期很简单,只有onReceive,当它在执行onReceive时是活跃状态,当执行完成则处于失活状态。根据网上资料:
拥有一个活跃状态的广播接收器的进程被保护起来而不会被杀死,但仅拥有失活状态组件的进程则会在其它进程需要它所占有的内存的时候随时被杀掉。
而根据Broadcast的官方文档,当onReceive执行完这个Broadcast对象不再是alive状态,所以可以随时被回收销毁。所以不能在onReceive中进行异步操作,即开启新的线程,因为当onReceive执行完处于失活状态,它和这个新的线程可能随时被销毁,导致不可预计的程序问题。如果想在onReceive中执行一些异步操作,那么可以使用JobService,或者service。官方文档如下:
If this BroadcastReceiver was launched through a <receiver> tag,
then the object is no longer alive after returning from this
function. This means you should not perform any operations that
return a result to you asynchronously. If you need to perform any follow up
background work, schedule a {@link android.app.job.JobService} with
{@link android.app.job.JobScheduler}.
If you wish to interact with a service that is already running and previously
bound using {@link android.content.Context#bindService(Intent, ServiceConnection, int) bindService()},
you can use {@link #peekService}.
所以说当Broadcast执行完onReceive后就可以随时被销毁了,当然动态注册不一样,因为它是手动创建的,所以还需要关心它的引用可达性。
同时,Broadcast的创建也一样,动态注册的时候我们手动创建,所以是一个对象。
而静态注册的时候,应该与activity等组件类似,(binder机制中)先通过intent条件查找创建Broadcast对象,经过测试每次都是重新创建。比如我们在menifast中静态注册一个Broadcast,然后通过一个按钮发送这个广播,在Broadcast的onReceive中打印自己的对象的toString,发现每次点击都是一个新的对象来执行。所以给Broadcast设置类变量,防止重复接收不会起作用,因为每次都是一个新的对象。
如果在onReceive中执行耗时操作,如下:
public class MyBroadcast extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Log.e("ssss", this.toString() + ":start");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.e("ssss", this.toString() + ":end");
}
}
再反复点击按钮发送广播,就会发现这些广播会按顺序执行,当上一个执行完才开始执行下一个(因为是在一个线程中)。
链接:https://juejin.cn/post/7244808577209516093
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
四个有用的Android开发技巧,又来了
大家好,本篇文章会继续给大家分享一些Android常见的开发技巧,希望能对读者有所帮助。
一. 通过堆栈快速定位系统版本
这个地方主要分享大家两个个技巧,通过问题堆栈简快速定位当前系统版本:
1. 快速区分当前系统版本是Android10以下,还是Android10及以上;
首先Android10及以上引入了一个新的服务Service:ActivityTaskManagerService
,将原本ActivityMangerService
原本负责的一些职能拆分给了前者,所以当你的问题堆栈中出现了ActivityTaskManagerService
相关的字眼,那肯定是Android10及以上了。
大家在Android9及以下的源码中是找不到这个类的。
2. 快速区分当前系统版本是Android12以下,还是Android12及以上;
这个就得借助Looper
了,给大家看下Android12上Looper
的源码:
Looper分发消息的核心方法loop()
,现在会转发给loopOnce()
进行处理,这个可是Android12及以上特有的,而Looper又是Android处理消息必要的一环,是咱们问题堆栈的源头祖宗,类似于下面的:
所以这个技巧相信还是非常有必要的:当你从问题堆栈中一看有loopOnce()
这个方法,那必定是Android12无疑了。
二. 实现按钮间距的一种奇特方式
最近看了一个新的项目代码,发现该项目实现按钮之间、按钮与顶部底部之间间距实现了,用了一种我之前没了解过的方式,于是这里分享给大家瞧瞧。
这里就以TextView和屏幕顶部间设置间距为例,初始的效果如下:
接下来我们来进行一步步改造:
1. 首先TextView是有一个自定义的xml背景:
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:height="70dp"
android:gravity="center_vertical">
<shape>
<solid android:color="#ff0000" />
</shape>
</item>
</layer-list>
核心就是定义了android:height
和android:gravity
这两个属性,来确保我们自定义背景在组件中的高度及居中位置。
2. 其次将布局中TextView
的属性调整下:
- 首先
height
属性一定要调整为wrap_content
,保证最后TextView按钮的高度的测量最终取minHeight
设置的属性值和背景设置的高度这两者的最大值;
- 其次还要设置
minHeight
最小高度属性,注意一定要比背景设置的高度值大,保证能和屏幕顶部产生边距效果;
- 最后要设置字体的位置为垂直居中,保证字体位置和背景不发生错位;
经过上面处理,效果就出来了:
其实上下空白的部分都是属于TextView,设置点击事件也会被响应,这算是其中的缺点之一,当前也可能在业务场景中认为这是一种合理表现。
上面实现的逻辑和TextView的测量逻辑密不可分,感兴趣的同学可以看下这块代码,这里就不带大家进行一一分析了:
三. logcat快速查看当前跳转的Activity类信息
忘了是在哪里看到的了,只要日志过滤start u0
,就可以看到每次跳转的Activity信息,非常的有帮助,既不需要改动业务层,也不需要麻烦的安装一些插件啥的。
使用时记得将logcat右边的过滤条件置为,否则你就只能在左边切换到系统进程去看了:
这里我们演示下效果:
1. 跳转到Google浏览器
logcat界面会输出:
会打印一些跳转到包名类名等相关信息。
2. 跳转到系统设置界面
logcat输出:
可以说start u0
还是相当好用的。
四. 项目gradle配置最好指向同一本地路径
最近开发中经常存在需要一次性检索多个项目的场景,而这样项目的gradle版本都是相同的,没啥区别。但每打开一个项目就得重新走一遍gradle下载流程,下载速度又是蜗牛一样的慢。
所以强烈建议大家,本地提前准备好几个gradle版本,然后通过设置将项目的gradle指向本地已存在好的gradle:
这样项目第一次打开的速度将是非常快的,而且按道理来说相同gradle版本的项目指向同一本地路径,也可以实现缓存共享。猜的。
如果项目好好的编译运行着,突然没网了,可能会提示一些找不到依赖库资源啥的,其实你本地都已经缓存好依赖库资源了,只需要设置下off-mode,不走网络直接通过本地资源编译运行即可:
总结
本篇文章主要是介绍了Android开发一些技巧,感觉都是项目中挺常用到的,算是我最近一个月收获的吧,后续准备研究研究compose了,毕竟看到大家们都在搞这个,羡慕的口水都流了一地了哈哈。
链接:https://juejin.cn/post/7250080519069007933
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
既当产品又当研发,字节大哥手把手带我追求极致
在学校的时候,计算机相关专业的同学应该都或多或少都被“大作业”折磨过,没有为“大作业”熬过夜通过宵的大学生活可以说是不完整的。步入公司后才发现,校园里的“大作业”就像玩具一样,需求明确、解决方案明确、最终产品效果明确、甚至还有前人的作品可以参考,而在公司里要做的东西,上面说的特点至少有一个不具备,甚至通通不具备。
而我在字节实习的过程中,所经手的恰恰就是这么一个需求不明确、解决方案不明确、最终产品效果不明确的项目。整个过程中有过焦头烂额毫无进展的时刻也有过欲哭无泪的时刻,还好有我的mentor带着我一路披荆斩棘、过关斩将。
首先和大家讲一下项目背景,当时我在的组是视频会议移动端,经历了近三年大流感的洗礼,相信大家对于视频会议中可能遇到的各种问题如数家珍,包括但不限于没声了、没音了、没画面了、画面卡顿、画面不清晰、画面和语音不同步、同步屏幕时闪退等等等等。作为一个服务企业级的B端产品,出现以上问题时就可能会投诉,然后经过客户成功部门转手到运营再转手到研发这里,研发就需要判断分析一下究竟是我们产品的原因、还是客户本身设备的问题、或者是第三方环境的因素,当用户的量级上来后,这样的客诉就会很多,会严重占用oncall的研发人员的时间以及精力。
我的mentor,一个专注于解决问题、避免重复劳动的人,一个字节范我觉得有E+的人,一个虽然身处移动端但是前后端甚至网络也都会的人,觉得这样很不OK,应该有个工具,能够自动的分析出来客户究竟遇到了什么问题,分析不出来的再找研发进行排查。没有这个工具也不影响业务开发的进展,所以整个项目并不存在时间上的紧迫性,但是呢,有这个工具做出来后肯定会大大降低研发的开发时间,所以项目的必要性还是有的。于是,我作为刚入职的实习新人,这个项目就交给我来做了。
而我,一个还没有从校园中完全出来的新兵蛋子,说实话面对这样的场面是一脸懵逼的,对于要做啥、要怎么做可以说是一无所知,我的mentor在我入职后,让我先了解了解背景,第一周就带着我oncall了,让我知道都可能有样的客诉,手把手给我演示他们正常的排查问题的方式。先了解客户反馈的情况,然后捞出来客户对应时间的设备信息以及设备日志。
说实话,作为一个新人,或者说我本身对于项目有一种畏难心理,碰到一点难题就总是想着往后拖,或者摆烂先不管然后就搁置在一边不想再问津了,但是我的mentor是一个有着坚定信念的人,差不多就是见山开山,见水架桥这种,遇到问题会主动找到相关人员一起解决,可以说就是有那种主人翁,项目owner的意识。于是,我就跟在他的后面,和整个团队的不同角色沟通他们遇到问题时排查的思路,试图总结出来一种通用的流程。在过程中,难免有许多困难,我的第一反应是退缩,但是导师的第一反应是拉会拉上相关人员一起讨论,看看用什么方式可以解决。比如在如何确定设备日志和故障表现的映射关系时,先后调研了多种方式看看相关团队有没有类似的做法以及他们最后实现的效果,包括大数据机器学习、代码状态流转图、自定义规则引擎等多种方式,最后调研后决定采用自定义规则引擎的方式。在实现需求的时候,需要其他团队协作时,他总是直接向前提出自己的需求,而我向一个陌生人发消息之前总要做一些心理建设,总是在担心些什么、害怕些什么,而事实上大家都是打工人,谁也不比谁厉害多少,对方不配合那就拉+1进群一起看看,解决不了就向上暴露问题。
于是,导师披荆斩棘在前,我在后面跟着实现他的设想。我们很快就做出来了第一个版本。通过Python自动化下载设备日志,然后正则匹配筛选出含有特定标记的日志,并对他们的出现频率次数做出判断。因为Python是解释型的语言,所以可以把规则直接写成python语言,用eval或者exec函数进行执行。第一个版本做出来后,导师又积极的带着我去给其他人宣传我们的这个工具。然后根据他们的反馈继续进行相关改进,最后我离职前实现的效果就是@ 一个群里的机器人,告诉他出现问题的ID,他就能自动化的拉下来日志进行排查,然后告诉你他分析的结果,整个交互非常的方便。
一个成功的项目必须要有一个负责的owner,我的导师就向我展示了一个优秀的owner是如何一步步解决问题、排除项目中的难关,如今我也正式成为一名打工人,希望我也能早日如他一般自如的面对工作。
我是日暮与星辰之间,出道两年半的Java选手,相信时间的力量,一起成为更好的自己!
链接:https://juejin.cn/post/7211801284709138493
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
如何实现比 setTimeout 快 80 倍的定时器?
起因
很多人都知道,setTimeout
是有最小延迟时间的,根据 MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间 中所说:
在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度)。
在 HTML Standard 规范中也有提到更具体的:
Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.
简单来说,5 层以上的定时器嵌套会导致至少 4ms 的延迟。
用如下代码做个测试:
let a = performance.now();
setTimeout(() => {
let b = performance.now();
console.log(b - a);
setTimeout(() => {
let c = performance.now();
console.log(c - b);
setTimeout(() => {
let d = performance.now();
console.log(d - c);
setTimeout(() => {
let e = performance.now();
console.log(e - d);
setTimeout(() => {
let f = performance.now();
console.log(f - e);
setTimeout(() => {
let g = performance.now();
console.log(g - f);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
在浏览器中的打印结果大概是这样的,和规范一致,第五次执行的时候延迟来到了 4ms 以上。
更详细的原因,可以参考 为什么 setTimeout 有最小时延 4ms ?
探索
假设我们就需要一个「立刻执行」的定时器呢?有什么办法绕过这个 4ms 的延迟吗,上面那篇 MDN 文档的角落里有一些线索:
如果想在浏览器中实现 0ms 延时的定时器,你可以参考这里所说的
window.postMessage()
。
这篇文章里的作者给出了这样一段代码,用 postMessage
来实现真正 0 延迟的定时器:
(function () {
var timeouts = [];
var messageName = 'zero-timeout-message';
// 保持 setTimeout 的形态,只接受单个函数的参数,延迟始终为 0。
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, '*');
}
function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}
window.addEventListener('message', handleMessage, true);
// 把 API 添加到 window 对象上
window.setZeroTimeout = setZeroTimeout;
})();
由于 postMessage
的回调函数的执行时机和 setTimeout
类似,都属于宏任务,所以可以简单利用 postMessage
和 addEventListener('message')
的消息通知组合,来实现模拟定时器的功能。
这样,执行时机类似,但是延迟更小的定时器就完成了。
再利用上面的嵌套定时器的例子来跑一下测试:
全部在 0.1 ~ 0.3 毫秒级别,而且不会随着嵌套层数的增多而增加延迟。
测试
从理论上来说,由于 postMessage
的实现没有被浏览器引擎限制速度,一定是比 setTimeout 要快的。但空口无凭,咱们用数据说话。
作者设计了一个实验方法,就是分别用 postMessage
版定时器和传统定时器做一个递归执行计数函数的操作,看看同样计数到 100 分别需要花多少时间。读者也可以在这里自己跑一下测试。
实验代码:
function runtest() {
var output = document.getElementById('output');
var outputText = document.createTextNode('');
output.appendChild(outputText);
function printOutput(line) {
outputText.data += line + '\n';
}
var i = 0;
var startTime = Date.now();
// 通过递归 setZeroTimeout 达到 100 计数
// 达到 100 后切换成 setTimeout 来实验
function test1() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setZeroTimeout took ' +
(endTime - startTime) +
' milliseconds.'
);
i = 0;
startTime = Date.now();
setTimeout(test2, 0);
} else {
setZeroTimeout(test1);
}
}
setZeroTimeout(test1);
// 通过递归 setTimeout 达到 100 计数
function test2() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setTimeout(0) took ' +
(endTime - startTime) +
' milliseconds.'
);
} else {
setTimeout(test2, 0);
}
}
}
实验代码很简单,先通过 setZeroTimeout
也就是 postMessage
版本来递归计数到 100,然后切换成 setTimeout 计数到 100。
直接放结论,这个差距不固定,在我的 mac 上用无痕模式排除插件等因素的干扰后,以计数到 100 为例,大概有 80 ~ 100 倍的时间差距。在我硬件更好的台式机上,甚至能到 200 倍以上。
Performance 面板
只是看冷冰冰的数字还不够过瘾,我们打开 Performance 面板,看看更直观的可视化界面中,postMessage
版的定时器和 setTimeout
版的定时器是如何分布的。
这张分布图非常直观的体现出了我们上面所说的所有现象,左边的 postMessage
版本的定时器分布非常密集,大概在 5ms 以内就执行完了所有的计数任务。
而右边的 setTimeout
版本相比较下分布的就很稀疏了,而且通过上方的时间轴可以看出,前四次的执行间隔大概在 1ms 左右,到了第五次就拉开到 4ms 以上。
作用
也许有同学会问,有什么场景需要无延迟的定时器?其实在 React 的源码中,做时间切片的部分就用到了。
借用 React Scheduler 为什么使用 MessageChannel 实现 这篇文章中的一段伪代码:
const channel = new MessageChannel();
const port = channel.port2;
// 每次 port.postMessage() 调用就会添加一个宏任务
// 该宏任务为调用 scheduler.scheduleTask 方法
channel.port1.onmessage = scheduler.scheduleTask;
const scheduler = {
scheduleTask() {
// 挑选一个任务并执行
const task = pickTask();
const continuousTask = task();
// 如果当前任务未完成,则在下个宏任务继续执行
if (continuousTask) {
port.postMessage(null);
}
},
};
React 把任务切分成很多片段,这样就可以通过把任务交给 postMessage
的回调函数,来让浏览器主线程拿回控制权,进行一些更优先的渲染任务(比如用户输入)。
为什么不用执行时机更靠前的微任务呢?参考我的这篇对 EventLoop 规范的解读 深入解析 EventLoop 和浏览器渲染、帧动画、空闲回调的关系,关键的原因在于微任务会在渲染之前执行,这样就算浏览器有紧急的渲染任务,也得等微任务执行完才能渲染。
总结
通过本文,你大概可以了解如下几个知识点:
setTimeout
的 4ms 延迟历史原因,具体表现。- 如何通过
postMessage
实现一个真正 0 延迟的定时器。 postMessage
定时器在 React 时间切片中的运用。- 为什么时间切片需要用宏任务,而不是微任务。
链接:https://juejin.cn/post/7249633061440749628
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
搞懂Kotlin委托
1、委托是什么
委托是一种设计模式,它的基本理念是操作对象自己不会去处理某段逻辑,而是会把工作委托给另外一个辅助对象去处理。也就是说在委托模式中,会有2个对象参与同一个请求的处理,接受请求的对象将请求委托给另一个对象来处理。
委托模式中,有三个角色,约束、委托对象和被委托对象。
委托模式其实不难理解,生活中有很多类似的地方。假如你手里有一套房子想要租出去,想要把房子租出去,联系房客、带人看房是必不可少的,如果让你自己来进行前面所说的工作,可能会占用你自己的业余生活时间,所以这种时候就可以把这些事委托给中介处理,接下来你不需要自己联系房客,也不需要亲自带人看房子,这些工作都由中介完成了,这其实就是一种委托模式。在这里,约束就是联系房客、看房子等一系列操作逻辑,委托对象是你,也就是房主,被委托对象是中介。
在Kotlin中将委托功能分为两种:类委托和属性委托。
1.1、类委托
类委托的核心思想是将一个类的具体实现委托给另一个类去完成。
以上面租房子的例子,我们使用Kotlin的by关键字
亲自实现一下委托模式。
首先,我们来定义一下约束类,定义租房子需要的业务:联系房客、看房子。
// 约束类
interface IRentHouse {
// 联系房客
fun contact()
// 带人看房
fun showHouse()
}
接着,我们来定义被委托类,也就是中介。
// 被委托类,中介
class HouseAgent(private val name: String): IRentHouse {
override fun contact() {
println("$name中介 联系房客")
}
override fun showHouse() {
println("$name中介 带人看房")
}
}
这里我们定义了一个被委托对象,它实现了约束类的接口。
最后,定义委托类,也就是房主。
// 委托对象
class HouseOwner(private val agent: IRentHouse): IRentHouse by agent {
// 签合同
fun sign() {
println("房主签合同")
}
// 带人看房
override fun showHouse() {
println("房主带人看房")
}
}
这里定义了一个委托类HouseOwner,同时把被委托对象作为委托对象的属性,通过构造方法传入。
在Kotlin中,委托用关键字by
,by后面就是委托的对象,可以是一个表达式。
测试:
fun main() {
val agent = HouseAgent("张三") // 初始化一个名叫张三的中介
val owner = HouseOwner(agent) // 初始化房主,并把“中介”介绍给他
owner.contact()
owner.showHouse()
owner.sign()
}
运行结果如下
张三中介 联系房客
房东带人看房
房主签合同
可以看到,在整个租房过程中,房主的一些工作由中介帮着完成,例如联系房客。如果房东心血来潮,觉得自己更理解自己的房子,想把原来委托给中介的工作自己处理,也可以自己来进行,例如带人看房。最后签合同只有房主能处理,所以这是房主独有的操作。以上就是一个委托的简单应用。
而这也是委托模式的意义所在,就是让大部分的方法实现调用被委托对象中的方法,少部分的方法实现由自己来,甚至加入一些自己独有的方法,那么房东租房的整个逻辑就能顺利进行了。
有的人可能会说,这样的话不用by关键字我也可以实现委托。如下:
// 委托对象
class HouseOwner(private val agent: IRentHouse): IRentHouse {
// 签合同
fun sign() {
println("房主签合同")
}
// 联系房客
override fun contact() {
agent.contact()
}
// 带人看房
override fun showHouse() {
println("房主带人看房")
}
}
运行结果如
张三中介 联系房客
房东带人看房
房主签合同
可以看到,与前面的输出结果一样。
但是这种写法是有一定弊端的,如果约束接口中的待实现方法比较少还好,如果有几十甚至上百个方法的话就会出现问题。
前面也说过委托模式的最大意义在于,大部分的委托类方法实现可以调用被委托对象中的方法。而既然使用委托模式,就说明委托类中的大部分的方法实现是可以通过调用被委托对象中的方法实现的,这样的话每个都像“联系房客”那样去调用被委托对象中的相应方法实现,还不知道要写到猴年马月,而且会产生大量样板代码,很不优雅。
所以Kotlin提供了关键字by
,在接口声明的后面使用by关键字,再接上被委托的辅助对象,这样可以免去仅调用被委托对象方法的模版代码。
而如果要对某个方法进行重新实现,只需要单独重写那一个方法就可以了,其他的方法仍然可以享受类委托所带来的便利。
如果想加入独有的方法逻辑,直接写一个方法即可。
这几种情况在前面的“租房”场景中都有体现。
1.2、属性委托
属性委托的核心思想是将一个属性的具体实现委托给另一个类去完成。
我们来看一下委托属性的语法结构
class Test {
// 属性委托
var prop: Any by Delegate()
}
可以看到,这里使用by关键字
连接了左边的prop属性和右边的Delegate实例,这种写法就代表着将prop属性的具体实现委托给了Delegate类去完成。
1.2.1、什么是属性委托
前面也说了属性委托是将一个属性的具体实现委托给另一个类去完成。那么属性把什么委托了出去,被委托类又有哪些实现呢?
其实,属性委托出去的是其set/get
方法,委托给了被委托类的setValue/getValue
方法。
// 属性的get/set方法
var prop: Any
get() {}
set(value) {}
// 委托后
var prop: Any by Delegate()
注意这里prop声明的是var,即可变变量,如果委托给Delegate类的话,则必须实现getValue()和setValue()这两个方法,并且都要使用operator
关键字进行声明。
class Delegate {
private var propValue: String? = null
operator fun getValue(thisRef: Any, property: KProperty<*>): String? {
return propValue
}
operator fun setValue(thisRef: Any, property: KProperty<*>, value: String?) {
propValue = value
}
}
到这里,属性委托已经完成了,这时候,当你点开by关键字的时候会出现如下提示。
这就表明prop已经把具体实现委托给Delegate类完成。
当调用prop属性的时候会自动调用Delegate类的getValue()方法,当给prop属性赋值的时候会自动调用Delegate类的setValue()方法。
如果prop声明的是val,即不可变变量,则Delegate类只需要实现getValue()方法即可。
有些人第一次看到方法中的参数可能有点懵,但其实这是一种标准的代码实现样板,并且官方也提供了接口类帮助我们实现,具体的接口类下面会说到。
虽然是一套固定的样板,但我们也要理解其中参数的含义。
thisRef
:用于声明该Delegate类的委托功能可以在什么类中使用。必须与 属性所在类 类型相同或者是它的父类,如果是扩展函数,则指的是扩展的类型。property
:KProperty是Kotlin中的一个属性操作类,可用于获取各种属性相关的值。多数情况下都不需要修改。value
:具体要赋值给委托属性的值,必须与getValue的返回值相同。
那么为什么要使用属性委托呢?
假如想实现一个比较复杂的属性,它们处理起来比把值保存在支持字段field中更复杂,但是却不想在每个访问器都重复这样的逻辑,于是把获取这个属性实例的工作委托给一个辅助对象(类),这个辅助对象就是被委托类。说白了,就是避免样板代码,防止出现大量重复逻辑。
1.2.2、ReadOnlyProperty/ReadWriteProperty接口
前面说到,如果要实现属性委托,就必须要实现getValue/setValue方法,可以看到getValue/setValue方法结构比较复杂,很容易遗忘,为了解决这个问题,Kotlin 标准库中声明了2个含所需operator
方法的 ReadOnlyProperty/ReadWriteProperty
接口。
interface ReadOnlyProperty {
operator fun getValue(thisRef: R, property: KProperty<*>): T
}
interface ReadWriteProperty {
operator fun getValue(thisRef: R, property: KProperty<*>): T
operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}
val属性实现ReadOnlyProperty接口,var属性实现ReadWriteProperty接口。这样就可以避免自己写复杂的实现方法了。
// val 属性委托实现
class Delegate1: ReadOnlyProperty{
private var propValue: String = "zkl"
override fun getValue(thisRef: Any, property: KProperty<*>): String {
return propValue
}
}
// var 属性委托实现
class Delegate2: ReadWriteProperty{
private var propValue: String? = null
override fun getValue(thisRef: Any, property: KProperty<*>): String? {
return propValue
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: String?) {
propValue = value
}
}
2、Kotlin标准库的几种委托
2.1、延迟属性 lazy
2.1.1、使用by lazy进行延迟初始化
使用by lazy()
进行延迟初始化相信大家都不陌生,在日常使用中也能信手拈来。如下,是DataStoreManager对象延迟初始化的例子。
//这里使用by lazy惰性初始化一个实例
val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
DataStoreManager(store) }
by lazy()
代码块是Kotlin提供的一种懒加载技术,代码块中的代码一开始并不会执行,只有当变量(instance)首次被调用的时候才会执行,并且会将代码块中最后一行代码的返回值赋值给变量。 调用如下:
fun main() {
println(instance::class.java.simpleName)
println(instance::class.java.simpleName)
println(instance::class.java.simpleName)
}
打印结果如下:
第一次调用时执行
DataStoreManager
DataStoreManager
DataStoreManager
可以看到,只有第一次调用才会执行代码块中的逻辑,后续调用只会返回代码块的最终值。
那么什么时候适合使用by lazy进行延迟初始化呢?当初始化过程消耗大量资源并且在使用对象时并不总是需要数据时,就非常适合了。
当然,如果变量第一次初始化时抛出异常,那么lazy将尝试在下次访问时重新初始化该变量。
2.1.2、拆解by lazy
可能大家刚接触的时候会觉得by lazy
本是一体使用的,其实不是,实际上,by lazy
并不是连在一起的关键词,只有by才是Kotlin中的关键字,lazy只是一个标准库函数而已。
那么就把二者拆开看,先点开by关键字
@kotlin.internal.InlineOnly
public inline operator fun Lazy.getValue(thisRef: Any?, property: KProperty<*>): T = value
会发现它是Lazy 类的一个扩展函数,按照前面我们对by的理解,它就是把被委托的属性的get函数和getValue进行配对,所以可以想象在Lazy类中,这个value便是返回的值。
//惰性初始化类
public interface Lazy {
//懒加载的值,一旦被赋值,将不会被改变
public val value: T
//表示是否已经初始化
public fun isInitialized(): Boolean
}
接下来看一下lazy,这个就是一个高阶函数,用来创建lazy实例的。
public actual fun lazy(initializer: () -> T): Lazy = SynchronizedLazyImpl(initializer)
可以看到,该方法中会把initializer,也就是代码块中的内容,传递给SynchronizedLazyImpl
类进行初始化并返回。大部分情况我们使用的都是这个方法。
当然我们也可以设置mode
,这样会调用下面的lazy方法,该方法中会根据mode类型来判断初始化那个类。如下
public actual fun lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy =
when (mode) {
LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
}
// 使用如下
val instance: DataStoreManager by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) {
println("第一次调用时执行")
DataStoreManager(store)
}
三个mode解释如下:
LazyThreadSafetyMode.SYNCHRONIZED
:添加同步锁,使lazy延迟初始化线程安全LazyThreadSafetyMode. PUBLICATION
:初始化的lambda表达式可以在同一时间被多次调用,但是只有第一个返回的值作为初始化的值。LazyThreadSafetyMode. NONE
:没有同步锁,多线程访问时候,初始化的值是未知的,非线程安全,一般情况下,不推荐使用这种方式,除非你能保证初始化和属性始终在同一个线程
而第一个lazy不设置mode时默认的就是SYNCHRONIZED
,也是最常用的mode,这里我们直接看一下对应类的代码:
//线程安全模式下的单例
private class SynchronizedLazyImpl(initializer: () -> T, lock: Any? = null) : Lazy, Serializable {
private var initializer: (() -> T)? = initializer
//用来保存值,当已经被初始化时则不是默认值
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
//锁
private val lock = lock ?: this
override val value: T
//见分析1
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)
}
这个单例就是双重校验锁实现的。
2.2、可观察属性
Kotlin除了提供了lazy函数实现属性延迟加载外,还提供了Delegates.observable
和Delegates.vetoable
标准库函数来观察属性变化。先来看observable
2.2.1、observable函数
public inline fun observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
ReadWriteProperty =
object : ObservableProperty(initialValue) {
override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
}
可以看到,该标准库函数接收了两个参数initialValue
、onChange
initialValue
:初始值onChange
:属性值变化时的回调逻辑。回调有三个参数:property
,oldValue
,newValue
,分别表示:属性、旧值、新值。
使用如下:
var observableProp: String by Delegates.observable("初始值") { property, oldValue, newValue ->
println("属性:${property.name} 旧值:$oldValue 新值:$newValue")
}
// 测试
fun main() {
observableProp = "第一次修改值"
observableProp = "第二次修改值"
}
打印如下:
属性:observableProp 旧值:初始值 新值:第一次修改值
属性:observableProp 旧值:第一次修改值 新值:第二次修改值
可以看到,当把属性委托给Delegates.observable
后,每一次赋值,都能观察到属性的变化。
2.2.2、vetoable函数
vetoable
函数与observable一样,都可以观察属性值变化,不同的是,vetoable可以通过代码块逻辑决定属性值是否生效。
public inline fun vetoable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Boolean):
ReadWriteProperty =
object : ObservableProperty(initialValue) {
override fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = onChange(property, oldValue, newValue)
}
接收的两个参数与observable函数几乎相同,不同的是onChange回调有一个Boolean的返回值。
使用如下:
var vetoableProp: Int by Delegates.vetoable(0) { property, oldValue, newValue ->
println("属性:${property.name} 旧值:$oldValue 新值:$newValue")
newValue > 0
}
// 测试
fun main() {
println("vetoableProp:$vetoableProp")
vetoableProp = 2
println("vetoableProp:$vetoableProp")
vetoableProp = -1
println("vetoableProp:$vetoableProp")
vetoableProp = 3
println("vetoableProp:$vetoableProp")
}
打印如下:
vetoableProp:0
属性:vetoableProp 旧值:0 新值:2
vetoableProp:2
属性:vetoableProp 旧值:2 新值:-1
vetoableProp:2
属性:vetoableProp 旧值:2 新值:3
vetoableProp:3
可以看到-1的赋值并没有生效。
那么具体的逻辑是什么呢?
回看observable和vetoable的源码可以发现,二者继承了ObservableProperty
抽象类,不同的是observable重写了该类afterChange
方法,vetoable重写了该类beforeChange
方法,并且beforeChange会有一个Boolean的返回,返回的是我们自己写的回调逻辑的返回值。
那么接着看setValue逻辑
protected open fun beforeChange(property: KProperty<*>, oldValue: V, newValue: V): Boolean = true
protected open fun afterChange(property: KProperty<*>, oldValue: V, newValue: V): Unit {}
public override fun setValue(thisRef: Any?, property: KProperty<*>, value: V) {
val oldValue = this.value
if (!beforeChange(property, oldValue, value)) {
return
}
this.value = value
afterChange(property, oldValue, value)
}
可以看到会先执行beforeChange方法,如果beforeChange为false则直接返回,并且不会更新值,为true时才会更新值,接着执行afterChange方法。这里beforeChange方法默认返回true。
其实只要查看这些函数源码可以发现,其内部调用的都是代理类,所以说白了这些都是属性委托。
3、总结
委托在Kotlin中有着至关重要的作用,但是也不能滥用,中间毕竟多了一个中间类,不合理的使用不但不会有帮助,反而会占用内存。前面也说过类委托最大的意义在于,大部分的方法实现调用被委托对象中的方法,少部分的方法实现由自己来,甚至加入一些自己独有的方法。属性委托的意义在于,对于比较复杂的一些属性,它们处理起来比把值保存在支持字段field中更复杂,且它们的逻辑相同,为了防止出现大量模版代码,可以使用属性委托。所以在使用委托前,我们也可以按照上面的标准考虑一下,合理的使用委托可以减少大量样板代码,提高代码的可扩展性和可读性。
链接:https://juejin.cn/post/7249584280837767228
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Gson与Kotlin"摩擦"的那件小事
大家好,本篇文章分享一下之前使用gson和kotlin碰撞出的一些火花,脑瓜子被整的懵懵的那种。
准备知识
总所周知,当Gson没有无参构造函数时,会使用UnSafe以一种非安全的方式去创建类的对象,这样会产生两个问题:
- 属性的默认初始值会丢失,比如某个类中有这么一个属性
public int age = 100
,经过unsafe创建该类对象,会导致age
的默认值100丢失,变为0;
- 会绕过Kotlin的空安全检验,因为经过unsafe创建的对象不会在属性赋值时进行可null校验。
所以一般比较在使用Gson反序列化时,比较推荐的做法就是反序列化的类要有无参构造函数。
PS:其实提供了无参构造函数,还是有可能会绕过Kotlin空安全校验,毕竟在Gson中属性是通过反射赋值的,所以一些人会推荐使用Moshi,这个笔者还没怎么使用过,后续会了解下。
看一个脑瓜子懵的例子
先上代码:
class OutClass {
val age: Int = 555
override fun toString(): String {
return "OutClass[age = $age]"
}
inner class InnerClass {
val age1: Int = 897
override fun toString(): String {
return "InnerClass[age = ${this.age1}]"
}
}
}
以上两个类OutClass
和InnerClass
看起来都有无参构造函数,现在我们来对其进行一一反序列化。
1. 反序列化OutClass
fun main(args: Array<String>) {
val content = "{"content": 10}"
val out = OutClass::class.java
val obj = Gson().fromJson(content, out)
println(obj)
}
反序列化使用的字符串是一个OutClass
类不存在的属性content
,咱们看下输出结果:
看起来没毛病,由于存在无参构造函数,且反序列化所使用的字符串也不包括age
字段,age
的默认值555得以保留。
2. 反序列化InnerClass
先上测试代码:
fun main(args: Array<String>) {
val content = "{"content": 10, "location": null}"
val out = OutClass.InnerClass::class.java
val obj = Gson().fromJson(content, out)
println(obj)
}
运行结果如下:
不是InnerClass
也是有无参构造函数的吗,为啥age
字段的默认值897没有被保留,当时给整蒙了。
于是进行了下debug断点调试,发现最终是通过Unsafe创建了InnerClass
:
当时是百思不得其解,后续想了想,非静态内部类本身会持有外部类的引用,而这个外部类的引用是通过内部类的构造方法传入进来的,咱们看一眼字节码:
所以非静态内部类根本就没有无参构造方法,所以最终通过Gson反序列化时自然就是通过Unsafe创建InnerClass
对象了。
如果想要解决上面这个问题,将非静态内部类改成静态内部类就行了,或者尽量避免使用非静态内部类作为Gson反序列化的类。
另外大家如果感兴趣想要了解下Gson是如何判断的反射无参构造方法还是走Unsafe创建对象的,可以看下源码:
ReflectiveTypeAdapterFactory#create ——>ConstructorConstructor#get
介绍下typeOf()
方法
回忆下我们之前是怎么反序列化集合的,看下下面代码:
fun main(args: Array<String>) {
val content = "[{"content": 10, "location": "aa"}, {"content": 10, "location": "bb"}]"
val obj = Gson().fromJson<List<OutClass>>(content, object : TypeToken<List<OutClass>>(){}.type)
println(obj)
}
要创建一个很麻烦的TypeToken
对象,获取其type
然后再进行反序列化,输出如下正确结果:
为了避免麻烦的创建TypeToken
,我之前写了一篇文章来优化这点,大家感兴趣的可以看下这篇文章:Gson序列化的TypeToken写起来太麻烦?优化它
然后之前有个掘友评论了另一个官方提供的解决方法:
于是我赶紧试了下:
@OptIn(ExperimentalStdlibApi::class)
fun main(args: Array<String>) {
val content = "[{"content": 10, "location": "aa"}, {"content": 10, "location": "bb"}]"
val obj = Gson().fromJson<List<OutClass>>(content, typeOf<List<OutClass>>().javaType)
println(obj)
}
运行输出:
没毛病,这个写法要比创建一个TypeToken
简单多了,这个api是很早就有了,不过到了kotlin1.6.0插件版本才稳定的,请大家注意下这点:
十分推荐大家使用这种方式,官方支持,就突出一个字:稳。
总结
本篇文章主要是给大家介绍了Gson反序列化非静态内部类时的坑,以及介绍了一个官方支持的api:typeOf()
,帮助大家简化反序列化集合的操作,希望本篇文章能对比有所帮助。
链接:https://juejin.cn/post/7249352715364483109
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
Android-我对代理模式的理解
以下业务场景不大现实,我这里只是提供一种思路
想象一种场景:有一天,产品经理让你记录某些地方的行为日志并且存储到本地方便查阅,你可能会写下如下代码:
interface ILogger {
fun logInfo(action: String)
fun logError(action: String)
}
class Logger : ILogger{
override fun logInfo(action: String) {
//存储到本地
saveToLocalFile(action)
}
override fun logError(action: String) {
//存储到本地
saveToLocalFile(action)
}
private fun saveToLocalFile(action: String) {}
}
当需要调用的时候:
val logger: ILogger = Logger()
logger.logError("出现问题")
当然了,你更大概率是考虑用一个单例类直接调用,而不是每次都这样写。
假如某天换了个产品经理,要求你在这些存储日志之前,先将日志上传到服务器,存储日志后,做一个埋点记录
class Logger : ILogger{
override fun logInfo(action: String) {
//上传到服务器
upLoadToCloud(action)
//存储到本地
saveToLocalFile(action)
//埋点
eventTracking(action)
}
override fun logError(action: String) {
//上传到服务器
upLoadToCloud(action)
//存储到本地
saveToLocalFile(action)
//埋点
eventTracking()
}
private fun saveToLocalFile(action: String) {}
private fun upLoadToCloud(action: String) {}
private fun eventTracking() {}
}
设计模式讲究一个职责单一,那么以上代码最直观的就是不同的功能耦合在一起。
什么是代理模式
一句话解释就是:在不改变原有功能的基础上,通过代理类扩展新的功能,使得功能之间解耦,或者框架和业务之间解耦,有点装饰器模式的味道。
静态代理
interface ILogger {
fun logInfo(action: String)
fun logError(action: String)
}
class Logger : ILogger{
override fun logInfo(action: String) {
//存储到本地
saveToLocalFile(action)
}
override fun logError(action: String) {
//存储到本地
saveToLocalFile(action)
}
private fun saveToLocalFile(action: String) {}
}
class LoggerProxy(val logger: Logger) : ILogger {
override fun logInfo(action: String) {
//上传到服务器
upLoadToCloud(action)
//通过传进来的logger对象来调用原来的实现方法
logger.logInfo(action)
//埋点
eventTracking(action)
}
override fun logError(action: String) {
//上传到服务器
upLoadToCloud(action)
//通过委托logger对象来调用原来的实现方法
logger.logError(action)
//埋点
eventTracking(action)
}
private fun upLoadToCloud(action: String) {}
private fun eventTracking(action: String) {}
}
//使用方式
val logger: ILogger = LoggerProxy(Logger())
logger.logError("出错了")
在第25行,我们新添加了一个新的LoggerProxy代理类同样的实现了ILogger接口,在两个方法中,我们按顺序完成了功能的调用,将上传到服务器和埋点的逻辑和存储到本地的逻辑进行了分离,代理类LoggerProxy在业务的执行前后附加了其他的逻辑。
看到这你可能会觉得,有点脱裤子放屁了。确实,当前代码量特别小,对于当前代码体现的可能不太明显,如果你正在一个设计相对大型的框架,业务和框架代码的分离显得就相对重要了。
作为一种设计思想,他提供的是一种思路,让你写出来的不是面向过程的代码,有好有坏,当然在实际项目中不要为了设计模式而设计模式,不然就适得其反了,写出来的代码可读性差。
动态代理
对于静态代理,上面的代码中我们在代理类中的前后加了两个不同的功能,这两个相对职责不同的功能耦合在了一起,我由于偷懒没将其中的一个功能拆走,正常情况是应该再写一个代理类去做相同的一部分操作,如果功能更多的话就要写更多的代理类,繁琐度可想而知。
再一个,静态代理是在程序运行前就已经存在代理类的字节码文件,代理类和委托类的关系在运行前就确定了。而动态代理类的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以不存在代理类的字节码文件。代理类和委托类的关系是在程序运行时确定。
class Logger : ILogger{
override fun logInfo(action: String) {
println("存储到本地: $action")
saveToLocalFile(action)
}
override fun logError(action: String) {
println("存储到本地: $action")
saveToLocalFile(action)
}
private fun saveToLocalFile(action: String) {}
}
class LoggerProxy(private val target: ILogger): InvocationHandler {
fun createProxy() = Proxy.newProxyInstance(
ILogger::class.java.classLoader,
arrayOf<Class<*>>(ILogger::class.java),
LoggerProxy(target)
) as ILogger
override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {
val action = args!![0].toString()
if (method?.name == "logInfo") {
uploadToCloud(action)
target.logInfo(action)
eventTracking(action)
} else if (method?.name == "logError") {
uploadToCloud(action)
target.logError(action)
eventTracking(action)
}
return null
}
private fun uploadToCloud(action: String) {
println("上传数据到服务器")
}
private fun eventTracking(action: String) {
println("埋点")
}
}
interface ILogger {
fun logInfo(action: String)
fun logError(action: String)
}
调用方式
val proxy = LoggerProxy(Logger())
proxy.createProxy().logError("出错了")
打印顺序
1. 上传数据到服务器
2. 存储到本地: 出错了
3. 埋点
- 在动态代理中,当我们通过createProxy()创建代理对象后,调用logError或logInfo方法的时候
- 代理对象的invoke()方法会被调用
- 由于我们传入的只有action这个参数,在invoke方法中,可通过args[0]来获取传入的数据;通过method.name获取待执行的方法名,以此来判断逻辑的走向
代理的创建方式createProxy()方法中的代码大部分都是固定的。
总结
静态代理:静态代理在编译时期就已经确定代理类的代码,代理类和被代理类在编译时就已经确定;如果需要扩展的功能越来越多,静态代理的缺点很明显就是要写大量的代理类,管理和维护都不太方便。
动态代理:动态代理在运行时动态生成代理对象,关系灵活,由于是在运行时动态的生成代理类,动态代理解决了静态代理大量代理类的问题,但是有个新的问题就是反射相对耗时一点。
我们常用的Retrofit就有用到动态代理,感兴趣的同学可以去深入了解下,这边就不过多讲解,包括AOP(面向切面编程),动态权限申请等等。
链接:https://juejin.cn/post/7247405681636884538
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
来个面试题,看看你对 kotlin coroutine掌握得如何?
给出下面代码:
lifecycleScope.launch(Dispatchers.IO) {
val task1 = async {
throw RuntimeException("task1 failed")
}
val task2 = async {
throw RuntimeException("task2 failed")
}
try {
task1.await()
} catch (e: Throwable){
Log.i("test", "catch task1: $e")
}
Log.i("test", "is coroutine active: $isActive")
try {
task2.await()
} catch (e: Throwable){
Log.i("test", "catch task2: $e")
}
Log.i("test", "scope end.")
}
问:app 会发生什么?输出的日志是怎样子的?为什么?
......
......
......
......
......
......
......
......
......
答:app 会 crash,输出日志为
I/test: catch task1: java.lang.RuntimeException: task1 failed
I/test: is coroutine active: false
I/test: catch task2: kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job=DeferredCoroutine{Cancelling}
I/test: scope end.
魔幻吗?
那我们就来分析下为啥结果是这个样子的。
协程有一个很基础的设定:默认情况下,异常会往外层 scope 抛,用以立刻取消外层 scope 内的其它的子 job。
在上面的例子中,假设:lifecycleScope.launch 创建的子 scope 为 A。task1 用 async 创建 scope A 的子 scope 为 B。task2 用 async 创建 scope A 的子 scope 为 C。
当 scope B 发生异常,scope B 会将异常抛给 scope A,scope A 会 cancel 掉 task2 和自己,再把异常抛给 lifecycleScope,因为 lifecycleScope 没有 CoroutineExceptionHandler 并且 scope A 是通过 launch 启动的,所以 crash 就发生了。
那如何打断异常的这个传播链呢?
答案就是使用 SupervisorJob,或者用基于它的 supervisorScope。它不会把异常往上抛,也不会取消掉其它的子 job。但是,SupervisorJob 对 launch 和 async 启动的协程的态度是不一样的,它的源码注释里写明了的,简单的认为它会吃掉异常是会踩坑的。
翻译出来就是,如果是 launch 启动的子协程,是需要 CoroutineExceptionHandler 配合处理的,如果是 async 启动的协程,就是真的不抛,等到 Deferred.await 时再抛。
所以,在上面的代码中,虽然 lifecycleScope 有用到 SupervisorJob,但异常从 scopeA 往上传时,因为没有 CoroutineExceptionHandler,所以跪了。
那么为什么 async 要往上抛异常,导致 await 的 try catch 还需要 supervisorScope 的配合?感觉有点反人类?
想象一下下面的场合:
lifecycleScope.launch {
val task1 = async { "非常耗时的操作,但没有异常" }
val task2 = async { throw RuntimeException("") }
val result1 = task1.await()
val result2 = task2.await()
}
因为 task2 有异常,所以整个协程必定会失败。如果等 await 时才跑错误, 那么就需要等耗时的 task1 执行完成,轮到 task 的 await 调用时,异常才能跑出来,虽然也没啥问题,就是白白耗费了 task1 的执行。
而依据当前的设计,task2 抛出异常,那么外层 scope 就会把 task1 也给取消了,整个 scope 也就执行结束了。async 源码里提到的原因是为了 structured concurrency,也是期望使用者更多的关注 scope 以及 scope 内各个任务的关联关系吧。不过这坑确实有点让人有时摸不着头脑,可能以后就变了也说不定。
剩下一个问题是,task1 失败后就往上抛吗?为啥 catch task1 后还有日志打印出来?
其实上面已经提到了,异常抛给 scope A 后,它会 cancel 掉自己,再往上抛,而 cancel 掉自己并不是强制终止掉协程的执行,而是先变更状态为 cancelling,所以日志中 isActive 已经变成 false 了,第二个异常也不是 task2 的异常,而是 await 本身抛出的 CancellationException。这里告诉我们要注意两点:
- try catch 时如果是 CancellationException,要记得 rethrow。
- 一些循环、耗时的点,要记得用 isActive 或者 ensureActive 检查,不要写出不能正常 cancel 的协程。像 delay 等 api,官方已经做好了这方面的检查,极大地方便了开发者。这个线程 interrupted 相关知识点是同一个道理。
了解了各种坑点以及背后的原因,我们就可以把协程用得飞起了。最后,修复文章开头提到的问题,就是简单包个 supervisorScope 就行啦。
lifecycleScope.launch(Dispatchers.IO) {
supervisorScope {
val task1 = async {
throw RuntimeException("task1 failed")
}
val task2 = async {
throw RuntimeException("task2 failed")
}
try {
task1.await()
} catch (e: Throwable){
Log.i("test", "catch task1: $e")
}
Log.i("test", "is coroutine active: $isActive")
try {
task2.await()
} catch (e: Throwable){
Log.i("test", "catch task2: $e")
}
Log.i("test", "scope end.")
}
}
链接:https://juejin.cn/post/7133450291751419941
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。