注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Flutter App开发实现循环语句的方式

如果您有小程序、APP、公众号、网站相关的需求,您可以通过私信来联系我 Flutter 中循环语句的使用方式与其他编程语言比较类似,常见的包括 for 循环和 while 循环。 1 for 循环 Flutter 中的 for 循环语法如下: for (va...
继续阅读 »

如果您有小程序、APP、公众号、网站相关的需求,您可以通过私信来联系我



Flutter 中循环语句的使用方式与其他编程语言比较类似,常见的包括 for 循环和 while 循环。


1 for 循环


Flutter 中的 for 循环语法如下:


for (var i = 0; i < count; i++) {
// 循环体
}

其中的 count 为循环次数, i 初始值默认为 0,每次循环自增 1。在循环体内部可以编写需要重复执行的代码。 例如,以下代码循环输出 1 到 10 的数字:


for (var i = 1; i <= 10; i++) {
print(i);
}

下面是一个使用 for 循环实现的案例,用于遍历一个列表并输出其中的元素。假设有一个列表 fruits ,其中包含了一些水果,现在需要遍历列表并输出其中的每个元素:


List<String> fruits = ['apple', 'banana', 'orange', 'grape'];
for (String fruit in fruits) {
print(fruit);
}

上述代码中,使用 for 循环遍历了列表 fruits 中的每个元素,变量 fruit 用于存储当前循环到的元素,并输出了该元素。在每次循环中,变量 fruit 都会被更新为列表中的下一个元素,直到遍历完整个列表为止。


2 for in


在 Flutter 中, for...in 主要是用于遍历集合类型的数据,例如 List、Set 和 Map。


下面是一个使用 for...in 遍历 List 的案例:


List<int> numbers = [1, 2, 3, 4, 5];
for (int number in numbers) {
print(number);
}

上述代码中, numbers 是一个包含整数的 List, for...in 循环遍历该 List 中的每个元素,将每个元素赋值给变量 number ,并输出 number 的值。在每次遍历中, number 都会被更新为 List 中的下一个元素,直到遍历完整个 List 为止。


下面是一个使用 for...in 遍历 Map 的案例:


Map<String, String> fruits = {
'apple': 'red',
'banana': 'yellow',
'orange': 'orange',
'grape': 'purple'
};
for (String key in fruits.keys) {
print('$key is ${fruits[key]}');
}

上述代码中, fruits 是一个包含水果名称和颜色的 Map, for...in 循环遍历该 Map 中的每个键,将每个键赋值给变量 key ,并输出该键及其对应的值。在每次遍历中, key 都会被更新为 Map 中的下一个键,直到遍历完整个 Map 为止。


在遍历集合类型的数据时,使用 for...in 语句可以简化代码,避免了使用下标、索引等方式进行访问和处理,使代码更加易读、优雅。


3 while 循环


Flutter 中的 while 循环语法如下:


while (expression) {
// 循环体
}

其中, expression 是布尔表达式,循环体内部的代码会一直循环执行,直到 expression 不再为真时跳出循环。 例如,以下代码使用 while 循环实现输出 1 到 10 的数字:


var i = 1;
while (i <= 5) {
print(i);
i++;
}

上述代码中,我们定义了一个变量 i ,并使用 while 循环判断 i 是否小于 5,如果为真,则输出变量 i 的值并将 i 的值加 1,然后继续循环;如果为假,则跳出 while 循环。


在每次循环中,变量 i 都会被更新为上一次的值加 1,直到变量 i 的值达到 5 时, while 循环结束。


while 循环还可以和条件表达式一起使用,例如,下面是一个使用 while 循环判断列表是否为空的示例:


List<int> numbers = [1, 2, 3, 4, 5];
while (numbers.isNotEmpty) {
print(numbers.removeLast());
}

上述代码中,我们定义了一个包含整数的列表 numbers ,并使用 while 循环判断 numbers 是否为空,如果不为空,则输出列表中的最后一个元素并将其从列表中删除,然后继续循环;如果为空,则跳出 while 循环。
在每次循环中, numbers 列表都会被更新,直到列表为空时 while 循环结束。 使用 while 循环可以在满足一定条件的情况下,重复执行一组语句,从而实现某些特定的功能需求。


在使用 while 循环时,需要注意控制循环条件,避免出现死循环的情况。


以上就是 Flutter 中实现循环语句的方式。




如果你有兴趣,可以关注一下我的综合公

作者:早起的年轻人
来源:juejin.cn/post/7229388804611932217
众号:biglead

收起阅读 »

前端访问系统文件夹

随着前端技术和浏览器的升级,越来越多的功能可以在前端实现而不用依赖于后端。其中,访问系统文件夹是一个非常有用的功能,例如上传文件时,用户可以直接从自己的电脑中选择文件。 使用方法 在早期的浏览器中,要访问系统中的文件夹需要使用 ActiveX 控件或 Java...
继续阅读 »

随着前端技术和浏览器的升级,越来越多的功能可以在前端实现而不用依赖于后端。其中,访问系统文件夹是一个非常有用的功能,例如上传文件时,用户可以直接从自己的电脑中选择文件。


使用方法


在早期的浏览器中,要访问系统中的文件夹需要使用 ActiveX 控件或 Java Applet,这些方法已经逐渐淘汰。现在,访问系统文件夹需要使用HTML5的 API。


最常使用的 API 是FileAPI,配合 input[type="file"] 通过用户的交互行为来获取文件。但是,这种方法需要用户选择具体的文件而不是像在系统中打开文件夹来进行选择。


HTML5 还提供了更加高级的 API,如 showDirectoryPicker。它支持在浏览器中打开一个目录选择器,从而简化了选择文件夹的流程。这个 API 的使用也很简单,只需要调用 showDirectoryPicker() 方法即可。


async function pickDirectory() {
const directoryHandle = await window.showDirectoryPicker();
console.log(directoryHandle);
}

但是需要注意的是该api的兼容性较低,目前所支持的浏览器如下图所示:


image.png


点击一个按钮后,调用 pickDirectory() 方法即可打开选择文件夹的对话框,选择完文件夹后,该方法会返回一个 FileSystemFileHandle 对象,开发者可以使用这个对象来访问所选择的目录的内容。


应用场景


访问系统文件夹可以用于很多场景,下面列举几个常用的场景。


上传文件


在前端上传文件时,用户需要选择所需要上传的文件。这时,打开一个文件夹选择器,在用户选择了一个文件夹后,就可以读取文件夹中的文件并进行上传操作。


本地文件管理


将文件夹中的文件读取到前端后,可以在前端进行一些操作,如修改文件名、查看文件信息等。这个在 纯前端文件管理器 中就被广泛使用。


编辑器功能


访问系统文件夹可以将前端编辑器与本地的文件夹绑定,使得用户直接在本地进行编写代码,而不是将代码保存到云端,这对于某些敏感数据的处理尤为重要。


结尾的话


通过HTML5的 API,前端可以访问到系统中的文件夹,这项功能可以应用于上传文件、本地文件管理和编辑器功能等场景,为用户带来了极大的便利。


作者:白椰子
来源:juejin.cn/post/7222636308740014135
收起阅读 »

可视化大屏:vue-autofit 一行搞定自适应

web
可视化大屏适配/自适应现状 可视化大屏的适配是一个老生常谈的话题了,现在其实不乏一些大佬开源的自适应插件、工具但是我为什么还要重复造轮子呢?因为目前市面上适配工具每一个都无法做到完美的效果,做出来的东西都差不多,最终实现效果都逃不出白边的手掌心,可以解决白边问...
继续阅读 »

可视化大屏适配/自适应现状


可视化大屏的适配是一个老生常谈的话题了,现在其实不乏一些大佬开源的自适应插件、工具但是我为什么还要重复造轮子呢?因为目前市面上适配工具每一个都无法做到完美的效果,做出来的东西都差不多,最终实现效果都逃不出白边的手掌心,可以解决白边问题的,要么太过于复杂,要么会影响dom结构。


三大常用方式




  1. vw/vh方案



    1. 概述:按照设计稿的尺寸,将px按比例计算转为vwvh

    2. 优点:可以动态计算图表的宽高,字体等,灵活性较高,当屏幕比例跟 ui 稿不一致时,不会出现两边留白情况

    3. 缺点:每个图表都需要单独做字体、间距、位移的适配,比较麻烦




  2. scale方案



    1. 概述:也是目前效果最好的一个方案

    2. 优点:代码量少,适配简单 、一次处理后不需要在各个图表中再去单独适配.

    3. 缺点:留白,据说有事件热区偏移,但是我目前没有发现有这个问题,即使是地图也没有




  3. rem + vw vh方案



    1. 概述:这名字一听就麻烦,具体方法为获得 rem 的基准值 ,动态的计算html根元素的font-size ,图表中通过 vw vh 动态计算字体、间距、位移等

    2. 优点:布局的自适应代码量少,适配简单

    3. 缺点:留白,有时图表需要单独适配字体




基于此背景,我决定要造一个简单又好用的轮子。


解决留白问题


留白问题是在使用scale时才会出现,而其他方式实现起来又复杂,效果也不算太理想,总会破坏掉原有的结构,可能使元素挤在一起,所以我们还是选择使用scale方案,不过这次要做出一点小小的改变。


常用分辨率


首先来看一下我的拯救者的分辨率:


image-20230420141240837 它可以代表从1920往下的分辨率


我们可以发现,比例分别是:1.77、1.6、1.77、1.6、1.33... 总之,没有特别夸张的宽高比。


计算补齐白边所需的px


只要没有特别夸张的宽高比,就不会出现特别宽或者特别高的白边,那么我们能不能直接将元素宽高补过去?也就是说,当屏幕右侧有白边时,我们就让宽度多出一个白边的px,当屏幕下方有白边时,我们就让高度多出一个白边的px。


很喜欢CSGO玩家的一句话:"啊?"


先想一下,如果此时按宽度比例缩放,会在下方留下白边,所以设置一下它的高度,设置多少呢?比如 scale==0.8 ,也就是说整个#app缩小了0.8倍,我们需要将高扩大多少倍才可以回到原来的大小呢?


QQ录屏20230420144111


emmm.....


算数我最不在行了,启动高材生


image-20230420143742913


原来是八分之十,我vue烧了。


当浏览器窗口比设计稿大或者小的时候,就应该触发缩放,但是比例不一定,如果按照scale等比缩放时,宽度从1920缩小0.8倍也就是1536,而高度缩小0.8也就是743,如果此时浏览器高度过高,那么就会出现下方的白边,根据高材生所说的,缩小0.8后只需要放大八分之十就可以变回原大小,所以以现在的高度743*1.25=928,使宽度=928px就可以完全充满白边!


真的是这样吗?感觉哪里不对劲...


是浏览器高度!我忽略了浏览器高度,我可以直接使用浏览器高度乘以1.25然后再缩放达0.8!就是 1 !


也就是说 clientHeight / scale 就等于我们需要的高度!


我们用代码试一试


function keepFit(designWidth, designHeight, renderDom) {
 let clientHeight = document.documentElement.clientHeight;
 let clientWidth = document.documentElement.clientWidth;
 let scale = 1;
 if (clientWidth / clientHeight < designWidth / designHeight) {
   scale = (clientWidth / designWidth)
   document.querySelector(renderDom).style.height = `${clientHeight / scale}px`;
} else {
   scale = (clientHeight / designHeight)
   document.querySelector(renderDom).style.width = `${clientWidth / scale}px`;
}
 document.querySelector(renderDom).style.transform = `scale(${scale})`;
}

上面的代码可能看起来乱糟糟的,我来解释一下:


参数分别是:设计稿的宽高和你要适配的元素,在vue中可以直接传#app。


下面的if判断的是宽度固定还是高度固定,当屏幕宽高比小于设计宽高比时,


我们把高度写成 clientHeight / scale ,宽度也是同理。


最终效果


将这段代码放到App.vue的mounted运行一下


autofit


如上图所示:我们成功了,我们仅用了1 2 3 4....这么几行代码,就做到了足以媲美复杂写法的自适应!


我把这些东西封装了一个npm包:vue-autofit ,开箱即用,欢迎下载!


亲手打造集成工具:vue-autofit


这是一款可以使你的项目一键自适应的工具 github源码👉go



  • 从npm下载


npm i vue-autofit


  • 引入


import autofit from 'vue-autofit'


  • 快速开始


autofit.init()


默认参数为1920*929(即去掉浏览器头的1080), 直接在大屏启动时调用即可




  • 使用


export default {  
 mounted() {
 autofit.init({
       designHeight: 1080,
       designWidth: 1920,
       renderDom:"#app",
       resize: true
  })
},
}


以上使用的是默认参数,可根据实际情况调整,参数分别为


   * - renderDom(可选):渲染的dom,默认是 "#app",必须使用id选择器 
  * - designWidth(可选):设计稿的宽度,默认是 1920
  * - designHeight(可选):设计稿的高度,默认是 929 ,如果项目以全屏展示,则可以设置为1080
  * - resize(可选):是否监听resize事件,默认是 true


结语


诺克萨斯即将崛起


作者:德莱厄斯
来源:juejin.cn/post/7224015103481118757
收起阅读 »

上手 Vue 新的状态管理 Pinia,一篇文章就够了

web
Vuex 作为一个老牌 Vue 状态管理库,大家都很熟悉了 Pinia 是 Vue.js 团队成员专门为 Vue 开发的一个全新的状态管理库,并且已经被纳入官方 github 为什么有 Vuex 了还要再开发一个 Pinia ? 先来一张图,看下当时对于 Vu...
继续阅读 »

Vuex 作为一个老牌 Vue 状态管理库,大家都很熟悉了


Pinia 是 Vue.js 团队成员专门为 Vue 开发的一个全新的状态管理库,并且已经被纳入官方 github


为什么有 Vuex 了还要再开发一个 Pinia ?


先来一张图,看下当时对于 Vuex5 的提案,就是下一代 Vuex5 应该是什么样子的


微信图片_20220314212501.png


Pinia 就是完整的符合了他当时 Vuex5 提案所提到的功能点,所以可以说 Pinia 就是 Vuex5 也不为过,因为它的作者就是官方的开发人员,并且已经被官方接管了,只是目前 Vuex 和 Pinia 还是两个独立的仓库,以后可能会合并,也可能独立发展,只是官方肯定推荐的是 Pinia


因为在 Vue3 中使用 Vuex 的话需要使用 Vuex4,还只能作为一个过渡的选择,存在很大缺陷,所以在 Componsition API 诞生之后,也就设计了全新的状态管理 Pinia


Pinia 和 Vuex


VuexStateGettesMutations(同步)、Actions(异步)


PiniaStateGettesActions(同步异步都支持)


Vuex 当前最新版是 4.x



  • Vuex4 用于 Vue3

  • Vuex3 用于 Vue2


Pinia 当前最新版是 2.x



  • 即支持 Vue2 也支持 Vue3


就目前而言 Pinia 比 Vuex 好太多了,解决了 Vuex 的很多问题,所以笔者也非常建议直接使用 Pinia,尤其是 TypeScript 的项目


Pinia 核心特性



  • Pinia 没有 Mutations

  • Actions 支持同步和异步

  • 没有模块的嵌套结构

    • Pinia 通过设计提供扁平结构,就是说每个 store 都是互相独立的,谁也不属于谁,也就是扁平化了,更好的代码分割且没有命名空间。当然你也可以通过在一个模块中导入另一个模块来隐式嵌套 store,甚至可以拥有 store 的循环依赖关系



  • 更好的 TypeScript 支持

    • 不需要再创建自定义的复杂包装器来支持 TypeScript 所有内容都类型化,并且 API 的设计方式也尽可能的使用 TS 类型推断



  • 不需要注入、导入函数、调用它们,享受自动补全,让我们开发更加方便

  • 无需手动添加 store,它的模块默认情况下创建就自动注册的

  • Vue2 和 Vue3 都支持

    • 除了初始化安装和SSR配置之外,两者使用上的API都是相同的



  • 支持 Vue DevTools

    • 跟踪 actions, mutations 的时间线

    • 在使用了模块的组件中就可以观察到模块本身

    • 支持 time-travel 更容易调试

    • 在 Vue2 中 Pinia 会使用 Vuex 的所有接口,所以它俩不能一起使用

    • 但是针对 Vue3 的调试工具支持还不够完美,比如还没有 time-travel 功能



  • 模块热更新

    • 无需重新加载页面就可以修改模块

    • 热更新的时候会保持任何现有状态



  • 支持使用插件扩展 Pinia 功能

  • 支持服务端渲染


Pinia 使用


Vue3 + TypeScript 为例


安装


npm install pinia

main.ts 初始化配置


import { createPinia } from 'pinia'
createApp(App).use(createPinia()).mount('#app')

在 store 目录下创建一个 user.ts 为例,我们先定义并导出一个名为 user 的模块


import { defineStore } from 'pinia'
export const userStore = defineStore('user', {
state: () => {
return {
count: 1,
arr: []
}
},
getters: { ... },
actions: { ... }
})

defineStore 接收两个参数


第一个参数就是模块的名称,必须是唯一的,多个模块不能重名,Pinia 会把所有的模块都挂载到根容器上

第二个参数是一个对象,里面的选项和 Vuex 差不多



  • 其中 state 用来存储全局状态,它必须是箭头函数,为了在服务端渲染的时候避免交叉请求导致的数据状态污染所以只能是函数,而必须用箭头函数则为了更好的 TS 类型推导

  • getters 就是用来封装计算属性,它有缓存的功能

  • actions 就是用来封装业务逻辑,修改 state


访问 state


比如我们要在页面中访问 state 里的属性 count


由于 defineStore 会返回一个函数,所以要先调用拿到数据对象,然后就可以在模板中直接使用了


如下这样通过 store.xxx 使用,是具备响应式的


<template>
<div>{{ store.count }}</div>
</template>
<script lang="ts" setup>
import { userStore } from '../store'
const store = userStore()
// 解构
// const { count } = userStore()
</script>


比如像注释中的解构出来使用,也可以用,只是这样拿到的数据不是响应式的,如果要解构还保持响应式就要用到一个方法 storeToRefs(),示例如下


<template>
<div>{{ count }}</div>
</template>
<script lang="ts" setup>
import { storeToRefs } from 'pinia'
import { userStore } from '../store'
const { count } = storeToRefs(userStore())
</script>


原因就是 Pinia 其实是把 state 数据都做了 reactive 处理,和 Vue3 的 reactive 同理,解构出来的也不是响应式,所以需要再做 ref 响应式代理


getters


这个和 Vuex 的 getters 一样,也有缓存功能。如下在页面中多次使用,第一次会调用 getters,数据没有改变的情况下之后会读取缓存


<template>
<div>{{ myCount }}</div>
<div>{{ myCount }}</div>
<div>{{ myCount }}</div>
</template>

注意两种方法的区别,写在注释里了


getters: {
// 方法一,接收一个可选参数 state
myCount(state){
console.log('调用了') // 页面中使用了三次,这里只会执行一次,然后缓存起来了
return state.count + 1
},
// 方法二,不传参数,使用 this
// 但是必须指定函数返回值的类型,否则类型推导不出来
myCount(): number{
return this.count + 1
}
}

更新和 actions


更新 state 里的数据有四种方法,我们先看三种简单的更新,说明都写在注释里了


<template>
<div>{{ user_store.count }}</div>
<button @click="handleClick">按钮</button>
</template>
<script lang="ts" setup>
import { userStore } from '../store'
const user_store = userStore()
const handleClick = () => {
// 方法一
user_store.count++

// 方法二,需要修改多个数据,建议用 $patch 批量更新,传入一个对象
user_store.$patch({
count: user_store.count1++,
// arr: user_store.arr.push(1) // 错误
arr: [ ...user_store.arr, 1 ] // 可以,但是还得把整个数组都拿出来解构,就没必要
})

// 使用 $patch 性能更优,因为多个数据更新只会更新一次视图

// 方法三,还是$patch,传入函数,第一个参数就是 state
user_store.$patch( state => {
state.count++
state.arr.push(1)
})
}
</script>


第四种方法就是当逻辑比较多或者请求的时候,我们就可以封装到示例中 store/user.ts 里的 actions 里


可以传参数,也可以通过 this.xx 可以直接获取到 state 里的数据,需要注意的是不能用箭头函数定义 actions,不然就会绑定外部的 this 了


actions: {
changeState(num: number){ // 不能用箭头函数
this.count += num
}
}

调用


const handleClick = () => {
user_store.changeState(1)
}

支持 VueDevtools


打开开发者工具的 Vue Devtools 就会发现 Pinia,而且可以手动修改数据调试,非常方便


image.png


模拟调用接口


示例:


我们先定义示例接口 api/user.ts


// 接口数据类型
export interface userListType{
id: number
name: string
age: number
}
// 模拟请求接口返回的数据
const userList = [
{ id: 1, name: '张三', age: 18 },
{ id: 2, name: '李四', age: 19 },
]
// 封装模拟异步效果的定时器
async function wait(delay: number){
return new Promise((resolve) => setTimeout(resolve, delay))
}
// 接口
export const getUserList = async () => {
await wait(100) // 延迟100毫秒返回
return userList
}

然后在 store/user.ts 里的 actions 封装调用接口


import { defineStore } from 'pinia'
import { getUserList, userListType } from '../api/user'
export const userStore = defineStore('user', {
state: () => {
return {
// 用户列表
list: [] as userListType // 类型转换成 userListType
}
},
actions: {
async loadUserList(){
const list = await getUserList()
this.list = list
}
}
})

页面中调用 actions 发起请求


<template>
<ul>
<li v-for="item in user_store.list"> ... </li>
</ul>

</template>
<script lang="ts" setup>
import { userStore } from '../store'
const user_store = userStore()
user_store.loadUserList() // 加载所有数据
</script>


跨模块修改数据


在一个模块的 actions 里需要修改另一个模块的 state 数据


示例:比如在 chat 模块里修改 user 模块里某个用户的名称


// chat.ts
import { defineStore } from 'pinia'
import { userStore } from './user'
export const chatStore = defineStore('chat', {
actions: {
someMethod(userItem){
userItem.name = '新的名字'
const user_store = userStore()
user_store.updateUserName(userItem)
}
}
})

user 模块里


// user.ts
import { defineStore } from 'pinia'
export const userStore = defineStore('user', {
state: () => {
return {
list: []
}
},
actions: {
updateUserName(userItem){
const user = this.list.find(item => item.id === userItem.id)
if(user){
user.name = userItem.name
}
}
}
})

结语


如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【赞】都是我创作的最大动力,感谢支持 ^_^


更多前端文章,或者加入前端交流群,欢迎关注公众号【沐华说技术】,大家一起共同交流和进步呀





往期精彩


【保姆级】Vue3 开发文档


Vue3的8种和Vue2的12种组件通信,值得收藏


作者:沐华
来源:juejin.cn/post/7075491793642455077
收起阅读 »

🔥面试官想听的离职原因清单

大家好,我是沐华。今天聊一个面试的问题 由于面试官还要摸鱼刷沸点,不想花那么多时间一个个面,所以采用群面的方式,就出现了上图这样的场景 交锋 面试官:方便说下离职原因吗? 掘友1:不方便 掘友2:在前公司长期工作量有些太大了,我自己身体上也出现了一些信号,有段...
继续阅读 »

大家好,我是沐华。今天聊一个面试的问题


由于面试官还要摸鱼刷沸点,不想花那么多时间一个个面,所以采用群面的方式,就出现了上图这样的场景


交锋


面试官:方便说下离职原因吗?


掘友1:不方便


掘友2:在前公司长期工作量有些太大了,我自己身体上也出现了一些信号,有段时间都完全睡不着觉,所以需要切换一个相对来讲工作量符合我个人要求的,比如说周末可以双休这样一个情况,这个对我现在的选择来讲还蛮重要的


掘友3:本来已经定好的前端负责人(组长),被关系户顶掉了,我需要一个相对公平的竞争环境,所以打算换个公司


掘友4:实不相瞒,一年前我投过咱们公司(或者面试过但没过),一年了,你知道我这一年是怎么过的吗,因为当时几轮面试都很顺利的,结果却回复我说有更合适的人选了,我难受了很久,因为真的很想进入咱们公司,于是我奋发图强,每天熬夜学习到凌晨两点半,如今学有所成,所以又来了


掘友5:团队差不多解散了吧,领导层变动,没多久时间原团队基本都走了,相当于解散了吧,现在剩几个关系户,干的不开心


掘友6:公司要开发一些灰产(买马/赌球/时时彩之类的),老员工都不愿意搞,就都要我来做,我堂堂掘友6可是与赌毒不共戴天的人,怎么会干这种事(就是害怕坐牢),就辞职了(这是位入职时间不长的掘友)


掘友7:公司业务调整,然后突然让我去外地分公司驻场/让我去搞 flutter(原本是前后端),虽然是个好机会可还是很难受,而且与我的职业发展规划不符,所以不想浪费时间,就第一时间辞职了


掘友8:前东家对我挺好的,工作也得心应手(进入舒适圈了),只是我不想一直呆在舒适圈,我不是那种混日子的人,所以希望跳出来,找一份更有挑战性,更有成就感的工作,贵公司的岗位很符合我的预期


掘友9:公司最近经营不理想:1.不给交社保/公积金了,2.拖欠几个月工资了,好不容易攒的奶粉钱都花完啦(虽然还单身,可也是有想法的),为了生活,这不办法呀,3.公司倒闭了,现在十几个同事都在找工作,咱们这还需要前后端、产品、设计、测试吗,我可以内推


掘友10:您可能也知道现在各行各业行情都不太好,很多公司都裁撤了部分业务,前公司前几年疫情时就已是踏雪而行了,现在在新业务的选择上就决定裁撤掉原来的业务线,我也是其中一员,虽然很遗憾但也能接受吧,在前公司这两年也是学到了很多


掘友11:我其实挺感谢上家公司的,各方面都挺好的,也给了我很好的成长空间,但是也三年多时间了,我的薪资也没涨过,相信你也知道,其实我现在的薪资能够值得更好的,嗯被认可


掘友12:克扣工资,领导说以后生产环境上出现一次 bug 就要扣所有参与的人工资,说真的,每天加班加点的干,我们都没问题,可结果就被这样对待,被连带扣了几次之后心里真的很难受


掘友13:回老家发展咯/对象在这边咯,因为准备结婚了,之后一直在这边发展定居了(这种换城市的回答要给出准备结婚或定居发展这样的原因,不然谈个对象就换城市会显得不靠谱);如果是小城市换大城市,可以直接说是为了高薪来的,因为家里买房了生孩子了啥的经济压力大,顾家其实是能体现稳定的,也给砍价打个预防针


掘友14:(有断层,面试时间和上次离职时间相隔时间有点长,有两三个月左右的,如果真实情况是家里或者生病啥的直说就好,如果只是找了几个月工作没找到,就要组织下语言了),由于长时间加班的原因,身体受到了影响每天睡不好觉,那段时间一直不在状态,没法好好投入工作,就想休息一段时间,为避免给公司造成不好的影响,所以辞职了。当时领导坚持批我几天假,我自己也不知道具体多久能恢复过来,毕竟那种种状态也不是一天两天了,还是坚持让领导批我的辞职了,然后这段时间我去了哪哪哪,身体已经调整过来了,可以全身心投入工作了,不过现在找工作希望是周末可以双休这样一个情况,这个对我现在的选择来讲还蛮重要的


(如果断层有一年的左右的,我有一段经历可以给大家参考下)我当时没工作了,家里投了点钱让我和一个亲戚合伙搞了点生意,结果赚了点钱,但那个亲戚喜欢赌钱,被他拿去赌了,输光了,于是我撤出来了


沐华:就是觉得翅膀硬了,想出去看看(其实这是我入职现公司面试时说的离职原因,当时面试官听着就笑了)


第一轮回答结束!





心法


离职原因真实情况绝大多数情况无非就几种:钱少了,不开心了,被裁了。


大家都差不多的,面试官心里也知道,可这能直说吗?


直说也不是不行,但是要注意表达方式,回答时有些场面话/润色一下还是需要的,去掉负面的字句,目的是让人家听的人舒服一点而已,毕竟谁也不喜欢一个陌生人来向自己诉苦抱怨,发牢骚吧,谁都希望认识正能量/积极向上的人吧


所以回答的关键在于:



  1. 不能是自己的能力、懒惰、不稳定等原因,或可能影响团队的性格缺陷

  2. 不要和现任说前任的不好,除非客观原因没办法,但也要知道即便是前公司真实存在的问题,hr 并不了解真实情况,还是会对是前公司有问题,还是你有问题持怀疑态度的


就像分手原因,对别人说出来时不能显得自己很绝情,又不能让自己很跌份,而且很忌讳疯狂抹黑前任


公司想降低用人风险,看我们离职的原因在这里会不会再发生,所以我们回答中应该体现:稳定性、有想法、积极向上、找工作看重什么、想得到什么、有规划不是盲目找的....


忌讳:升职机会渺茫、个人发展遇到瓶颈、人际关系复杂受人排挤、勾心斗角氛围差...这样的回答会让人质疑是前公司的问题,还是你的能力/情商有问题?


那么,你觉得最好的答案是什么呢,如果你是面试官,会选谁进入下一轮?


同时期待掘友们在评论区补充哦


往期精彩


【保姆级】Vue3 开发文档


TS 泛型进阶


深入浅出虚拟 DOM 和 Diff 算法源码,及 Vue2 与 Vue3 中的区别


上手 Vue 新的状态管理 Pinia,一篇文章就够了


作者:沐华
来源:juejin.cn/post/7225432788044267575
收起阅读 »

你到底值多少钱?2023打工人薪酬指南

刚毕业时,我为了赢得面试官的好感,说了很多违心话,如:“工资不要紧,主要是想学习”,又或者是“我对贵司的这块技术非常感兴趣”。 现在想想,呸!恶心,哪怕是花钱嫖培训呢,也不要再傻乎乎的说出“为块术”这种违心的话了。 那时候年轻,不知道起薪高的好处,现在被各种...
继续阅读 »

刚毕业时,我为了赢得面试官的好感,说了很多违心话,如:“工资不要紧,主要是想学习”,又或者是“我对贵司的这块技术非常感兴趣”。


现在想想,呸!恶心,哪怕是花钱培训呢,也不要再傻乎乎的说出“为块术”这种违心的话了。


图1:为块术.png


那时候年轻,不知道起薪高的好处,现在被各种压涨幅,各种倒挂,干最累的活,拿最少的钱,吃最硬的大饼。


2023年,后疫情时代的“元年”,我想明白了,我背上行囊,背井离乡来北漂,就为了3件事:挣钱,挣钱,还是TM的挣钱


既然要挣钱,首先要明确自己的价值。想必大家也对自己值多少钱感兴趣吧?可苦于薪酬保密协议,很难和身边人对比,难以了解自己的价值。没关系,我最近读了几份有趣的报告:



  • 《看看你该赚多少?2023薪资指南(亚太版)》连智领域

  • 《2023年市场展望与薪酬报告》任仕达

  • 《2023⾏业薪酬⽩⽪书》嘉驰国际


今天我就通过这几份报告和大家聊聊,在职场中“我”到底价值几何,“我”拿到怎样的薪资才没有辜负我的才华。


Tips



  • 本文重点分享信息技术岗位,互联网行业,金融行业,软件行业的数据,其余数据可自行阅报告,文末附下载方式;

  • 个人价值不单单由工作年限决定,更多的是与工作年限所匹配的能力。


应届生薪资指南


如果你经常逛各种论坛,可能会看到“今年春/秋招的白菜价是25K”,“XXXX给我开了20K的侮辱性Offer”这类言论。那么20K真的是侮辱性Offer吗?低于白菜价的Offer到底要不要接?


来看嘉驰国际统计到的信息技术行业应届生薪资数据:


图2:2023信息技术行业应届生平均薪资.png


数据似乎与看到的言论相反,一线城市中,本科毕业生薪资中位数是8.8K,只有25%的毕业生拿到了超过10K的薪资。热门城市(北京,上海,广州,深圳和杭州)中也只有北京,上海和深圳的应届生薪资中位数超过了8K


那么网上流传的“白菜价”是怎么回事?其实不难理解,“白菜价”是少数顶尖院校(115所211院校,含39所985院校)的学生拿到顶尖互联网大厂的平均薪资水平,而大部分应届毕业生是很难拿到这个薪资的。


我国拥有2759所普通高等院校,本科1270所,高职(专科)1489所,顶尖院校(115所211院校,含39所985院校)仅占本科院校的9%,普通高等院校的4.1%。


所以对于大部分的普通人院校的毕业生来说,没有所谓的“白菜价”。根据自身的硬性条件合理决定自身的价值范围,不要被HR忽悠,也不要有太过离谱的期望


插句题外话,我16年毕业于某双非院校,第一份工作8.5K,但我们年级的“神”,第一份工作18K。讲这个事情有两层意思:



  • 某些大佬真的可以挣脱本科院校的枷锁

  • 身边的个例并不能反应真实的平均情况


Tips



互联网的天花板


了解完应届生的薪资后,你一定会很想了解未来自己的天花板在哪。注意,标题是互联网的天花板,并非某个职业,也并非每个人都能达到天花板。


先来看互联网行业的年固定收入成长曲线:


图3:互联网年固定收入天花板.png


接着是互联网行业年总收入的成长曲线:


图4:互联网年总收入天花板.png


以我个人观察到的情况,互联网行业中,主管/高级通常对应阿里巴巴的技术职级序列的P6和P6+,经理/资深则对应的是P7,而总监/专家则是P8及以上的职级。


一个很惨淡的事实,对于大部分人来说,P7是通过勤奋可以达到的天花板


如果不是太差,当你达到P7时你的年固定收入会来到50W上下,总收入(奖金和少量股票)会在60W到70W徘徊;而其中的佼佼者,年固定收入会来到70W,总收入触摸到7位数的边界;佼佼者中的一部分会跨过P7这道坎来到P8,普通的P8年薪会在60W上下,总收入(奖金和股票)接近100W,而顶尖的P8薪资会超过100W,总收入更是超过150W。


如果说P7是普通人勤奋的天花板,那普通人想要晋升为P8就需要额外的借助机遇和人脉才有可能达成


Tips:正文部分只展示互联网行业的年固定收入成长曲线和年总收入成长曲线,附录部分提供其他行业的收入曲线。


上海地区研发岗位薪酬


任仕达在《2023年市场展望与薪酬报告》中,给出了信息技术行业中各个技术岗位的薪酬数据,但只有上海地区的数据较为全面,我们重点关注几个“奋战”在一线的技术岗位的薪酬数据:


图5:上海职位薪酬.png
可以看到,对于研发工程师来说,薪资的天花板都非常接近,最突出的是移动开发工程师,稍微落后的是Python开发工程师


当然,技术岗位的天花板远不是职业的终点,技术岗位之后是偏向管理的岗位,例如项目管理和技术管理等。


在大部分互联网公司中,产品经理也是一线岗位,但无论平均薪酬还是薪酬上限,都高于研发岗位(AI类除外)。


结语


了解市场上的薪酬行情,有助于你在求职市场上擦亮自己的双眼,一来可以防止HR恶意压薪资,二来可以清楚自身的定位。


因文章篇幅限制,仅展示部分数据,点击王有志,回复【薪酬报告】即可下载报告。




好了,今天就到这里了,Bye~~


附录


各行业应届生薪资数据


附1:2023各行业应届生平均薪资.png


电子商务年收入成长曲线


年固定收入成长曲线:


附2:电子商务年固定收入天花板.png


年总收入成长曲线:


附3:电子商务年总收入天花板.png


企业软件年收入成长曲线


年固定收入成长曲线:


图4:互联网年总收入天花板.png


年总收入成长曲线:


附5:企业软件年总收入天花板.png


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

微信发送语音自定义view中的事件分发

这里通过一个自定义view的例子来看事件分发在自定义view中的使用,其实大部分的Android框架下的事件分发的也都差不多的样子,抛砖引玉,我自己做一个记录,如果能帮到有需要的人那就更上一层楼。 先来看一个微信发送语音的效果图: 关于事件分发我们其实耳熟能...
继续阅读 »

这里通过一个自定义view的例子来看事件分发在自定义view中的使用,其实大部分的Android框架下的事件分发的也都差不多的样子,抛砖引玉,我自己做一个记录,如果能帮到有需要的人那就更上一层楼。

先来看一个微信发送语音的效果图:


关于事件分发我们其实耳熟能详,可以通过一段非常有名的伪代码来大致了解:


  @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else {
consume = child.dispatchTouchEvent(ev);
}
return consume;

}

事件都是从一个DOWN开始,中间经过一堆MOVE,到一个UP结束(先抛开CANCEL的情况)。

事件流向画了半天,感觉也没有人家画的好,可以参考 这里 ,很清晰,忘记了的或者细节不清楚模糊了的可以移步去复习一下。


我们的需求是点击发送按钮后显示浮层view,相当于事件先由发送按钮处理,等浮层view显示后再交由浮层view处理,这个事件的流向很清晰,那应该怎么做呢。


那最简单的view的层级结构就是发送按钮浮层view处在同一层级,那一个问题,事件能否在parent什么都不做的情况下实现事件在同级别view之间的转移呢?

肯定是不可以或者说没有必要的,最好的方式还是通过parent来做分发,由parent的决定此时到底是需要把事件交给发送按钮还是浮层view


所以层级结构上:


<?xml version="1.0" encoding="utf-8"?>
<!-- parent, 来控制事件的分发 -->
<com.yocn.af.view.widget.WeChatParentViewGroup
android:id="@+id/wechat_root"
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:id="@+id/ll_option"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_alignParentBottom="true">

<!-- 发送按钮view -->
<com.yocn.af.view.widget.WeChatVoiceTextView
android:id="@+id/tv_voice"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="center"
android:text="按住 说话"/>
</LinearLayout>
<!-- 点击后需要显示的浮层view -->
<com.yocn.af.view.widget.WeChatVoiceView
android:id="@+id/voice_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/half"
android:visibility="gone" />

</com.yocn.af.view.widget.WeChatParentViewGroup>

我们梳理一下思路,需要做的就是:



  1. 什么都没做时ParentViewGrouponInterceptTouchEvent要返回false,使得事件能顺利的从ParentViewGroup传递到VoiceTextView(发送按钮)

  2. 点击到VoiceTextView(发送按钮)时,发送按钮的dispatchTouchEvent返回true,处理DOWN事件并告诉parent需要显示WeChatVoiceView(浮层view)

  3. parent接收到需要显示浮层view的命令,显示浮层view并且onInterceptTouchEvent返回true,表示事件我parent来处理,这时VoiceTextView(发送按钮)会收到一个CANCEL事件并且不会继续接受MOVE事件。

  4. parent来分发事件,在WeChatVoiceView(浮层view)显示出来之后直接将后续的MOVE事件交给WeChatVoiceView(浮层view)处理,当然浮层view的onInterceptTouchEvent需要返回true,会回调到浮层view的onTouchEvent,直接做对应的动画或者手势操作。

  5. 当然不要忘记在parent收到ACTION_UP的时候将浮层view置为不可见,因为事件是由parent分发给浮层view的,当然parent可以一直拿到事件。


至此,整个事件分发的流程就结束了。
附上代码地址WeChatSendVoice


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

Android项目cicd流程总结(使用jenkins)

没有cicd之前我们都是怎么做的 相信做安卓开发的都做过这些事 手动运行单元测试,根据报错改代码 检查代码风格,根据报错改代码 构建apk包,发给测试,有时候还得打很多个 接收测试的反馈,改bug, 重复之前的步骤 把apk放到ftp或者其他地方去发布 是...
继续阅读 »

没有cicd之前我们都是怎么做的


相信做安卓开发的都做过这些事



  1. 手动运行单元测试,根据报错改代码

  2. 检查代码风格,根据报错改代码

  3. 构建apk包,发给测试,有时候还得打很多个

  4. 接收测试的反馈,改bug,

  5. 重复之前的步骤

  6. 把apk放到ftp或者其他地方去发布


是不是想到这一套流程,头都大了,虽然每一步都不难,但是连起来都手工操作就很繁琐


像这些手动流程固定的事,我们完全就可以交给机器来做,让我们有更多时间做点别的事,没错,这就是今天要说的cicd


什么是cicd,以及在cicd过程中包含了哪些步骤


一般来说,安卓开发的CI/CD流程包括以下几个阶段:代码提交、代码检查、编译构建、单元测试集成测试、部署发布、用户反馈。



  1. 在代码提交阶段,开发者将自己的代码推送到远程仓库,例如Git或SVN,并触发CI/CD工具或平台例如Jenkins或Travis CI等

  2. 在代码检查阶段,CI/CD工具或平台会对代码进行静态分析和风格检查,例如使用SonarQube或Checkstyle等。

  3. 在编译构建阶段,CI/CD工具或平台会使用Gradle或Maven等工具对代码进行编译和打包,生成APK文件

  4. 在单元测试阶段,CI/CD工具或平台会使用JUnit或Espresso等框架对代码进行单元测试,并生成测试报告。

  5. 在集成测试阶段,CI/CD工具或平台会使用Appium或Selenium等框架对应用进行集成测试,并生成测试报告

  6. 在部署发布阶段,CI/CD工具或平台会将APK文件上传到内部服务器或外部平台,例如蒲公英或Google Play等,并通知相关人员。

  7. 在用户反馈阶段,开发者可以通过Bugly或Firebase等工具收集用户的反馈和错误信息,并根据需要进行修复和更新


通过以上几个步骤,我们可以把以前的app构建流程从手动变为自动,而且可以通过不断以非常低的成本的重复这个过程,提高我们的项目质量,这就是cicd带给我们的自信


今天我们来通过jenkins来实现上面的几个步骤


安装配置jenkins


本文讨论的主要是在windows环境下安装jenkins



  1. 从jenkins官网下载对应的安装包即可

  2. 安装过程很简单但是需要提供一个账号,就像下图显示的界面,这个账号需要有权限
    图片.png
    打开开始菜单,搜索本地安全策略,选择本地策略用户权限分配,在右侧的策略中找到作为服务登录,双击打开。点击添加用户或组,在输入框中填入你的账户的名字,单击检查名称,如果加上了下划线,则说明没有问题,如果输入的用户不存在,则会跳出来一个找不到名称的对话框。
    图片.png


这里需要注意一点,windows家庭版默认是没有本地安全策略的,需要用一些技巧把它开启,如下:


1.  在桌面上单击右键,选择“新建”->“文本文档”。

2. 将文本文档重命名为“OpenLocalSecurityPolicy.bat”。

3. 右键单击“OpenLocalSecurityPolicy.bat”,选择“编辑”。

4. 将以下命令复制并粘贴到文本编辑器中:


@echo off
pushd "%SystemRoot%\system32"
findstr /c:"[SR] Cannot repair member file" %windir%\logs\cbs\cbs.log >%userprofile%\Desktop\sfcdetails.txt
start ms-settings:windowsdefender
start ms-settings:windowsupdate
start ms-settings:windowsupdate-history
start ms-settings:windowsupdate-options
start ms-settings:storagesense
start ms-settings:storagesense-diagnostics
start ms-settings:storagesense-configurecleanup
start ms-settings:storagesense-changehowwesave
start ms-settings:storagesense-runstoragecleanupnow
start ms-settings:storagesense-storageusage
start ms-settings:storagesense-changestoragesavelocations
start ms-settings:backup
start ms-settings:backup-advancedsettings
start ms-settings:backup-addalocaldriveornetworklocation
start ms-settings:backup-managebackups
start ms-settings:backup-moreoptions
start ms-settings:dateandtime
start ms-settings:regionlanguage
start ms-settings:regionlanguage-languagepacks
start ms-settings:regionlanguage-speech
start ms-settings:regionlanguage-keyboards
start ms-settings:regionlanguage-morespeechservicesonline
start ms-settings:speech
start ms-settings:speech-microphoneprivacysettings


5. 保存并关闭文本编辑器。
6. 双击“OpenLocalSecurityPolicy.bat”文件,以打开本地安全策略。
复制代码

3. 修改默认根地址到其他盘符


默认情况下,jenkins的主目录都是在c盘,如果这样,我们使用中产生的数据都是在c盘,用过windows的都知道,数据放在c盘是很危险也是很让人不爽的一件事,我们可以通过修改jenkins的主目录方法来把数据放到其他盘


在成功安装了jenkins并解锁之后,我们可以配置环境变量JENKINS_HOME,地址就是我们想改的目录,


图片.png
然后修改jenkins.xml


    <env name="JENKINS_HOME" value="%LocalAppData%\Jenkins.jenkins"/>
复制代码

改为


    <env name="JENKINS_HOME" value="E:\jenkins"/>
复制代码


  1. 配置常用的插件
    在第一次启动jenkins的时候,会让你选择安装哪些插件,这时候直接选择推荐的插件就好,包含了一些常用插件,比如git等等,如下图


图片.png


配置针对于android的环境



  1. android sdk 见下图
    图片.png

  2. gradle -- Dashboard -> Manage Jenkins -> Global Tool Configuration中的Gradle配置gradle路径即可

  3. jdk -- Dashboard -> Manage Jenkins -> Global Tool Configuration中的JDK配置jdk路径即可

  4. git -- Dashboard -> Manage Jenkins -> Global Tool Configuration中的Git installations配置git路径即可


配置Android的具体job信息


新建一个freestyle的item,在里面做以下几步:




  1. 配置git仓库地址以及构建分支




  2. 设置构建触发器(定时构建) -- 找到构建触发器,勾选build periodically,在编辑框里按照规则设置构建时间,在某天的某个时段自动构建,比如45 9-15/2 * * 1-5,虽然可以提交一次就构建一次,但是不建议这么做。构建表达式的规则见下图,可以根据自己的需要写表达式。
    图片.png




  3. 添加构建步骤,打包出apk,如下图在build step中触发
    图片.png




  4. 配置构建后步骤,自动把apk包上传到ftp或者其他地方


    在Jenkins的项目中,选择“构建”->“增加构建步骤”->“执行shell”或“执行Windows批处理命令”
    一个上传ftp的例子




ftp -n <<EOF
open http://ftp.example.com
user username password
cd /remote/directory
put /local/file
bye
EOF
复制代码

配置邮件通知


在构建完成之后,特别是失败的时候,我们希望收到一封邮件告诉我们构建失败了,快去处理,我们可以通过以下步骤来实现



  1. 在Jenkins中安装Email Extension Plugin插件,可以在插件管理中搜索并安装。

  2. 在Jenkins的系统管理中,配置邮件服务器的地址,用户名,密码和端口。如果使用的是QQ邮箱或者163邮箱,还需要获取邮箱授权码并使用授权码作为密码。

  3. 在Jenkins的项目中,选择“构建后操作”->“增加构建后操作”->“Editable Email Notification”。

  4. 在邮件通知的配置中,填写收件人,抄送人,邮件主题,邮件内容等信息。可以使用一些变量来自定义邮件内容,例如BUILDSTATUS表示构建状态,BUILDSTATUS表示构建状态, {BUILD_URL}表示构建链接等。


这里特别要注意的是,上面的配置地址和授权码需要在job的设置里面进行,在全局配置有可能发不出邮件


配置单元测试和代码检查


我们还需要在运行前执行代码lint检查和单元测试,也需要配插件,插件名字是JUnit和Warnings Next Generation



  1. 参考上面 配置Android的具体job信息 中的配置,添加lint和单元测试的任务

  2. 配置单元测试插件和lint插件,主要指定报告文件的位置,见下图


图片.png


图片.png
3. 把单元测试的结果放到邮件的附件中去,配置见下图,也可以放些别的东西


图片.png


一劳永逸,使用docker把上面的配置做到随时随地使用


上面的步骤完成之后,我们就能自动构建,上传apk什么的了,但是每次换台机器我们都得再配一次,想下就很累,这时候我们就可以用docker,创建一个容器,把上面这些操作放在容器里面,在新环境里面拉个镜像,创建容器跑起来,就ok啦,关于怎么用docker,就需要大家自己去搜索学习了


最后放张图吧,jenkins真好用啊


封面.png


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

模拟点击与群控——autojs使用

写在前面 autojs是利用accessibility功能实现的一套免root自动化模拟点击框架,让开发者可以直接通过使用js脚本实现一系列的自动化操作,包括:触摸屏幕、滑动、输入文字等。autojs具有很高的灵活性和可扩展性,可以被用于各种场景,例如自动化游...
继续阅读 »

写在前面


autojs是利用accessibility功能实现的一套免root自动化模拟点击框架,让开发者可以直接通过使用js脚本实现一系列的自动化操作,包括:触摸屏幕、滑动、输入文字等。autojs具有很高的灵活性和可扩展性,可以被用于各种场景,例如自动化游戏操作、自动登录、自动化测试等。


autojs免费版本已经停止维护,官网只有Autojs Pro付费版本。然而最近(20230214)又因为一些合规问题,被强制下架已经不允许用户注册。市面上可使用代替产品autox,该项目基于原autojs4.1版本基础上进行维护开发。本文主要围绕该项目展开。


开发环境搭建


使用vscode,添加插件Auto.js-Autox.js-VSCodeExt,如下图,注意不要重复安装多个类似插件,会存在冲突问题。




按住cmd+shift+p,输入> Auto.js,选择"开启服务(Start Server)",此时右下角会提示服务正在运行提示,并显示ip地址信息,如下图,ip为:192.168.1.102




手机与电脑连接同个wifi,并打开autox app,打开"无障碍服务",并打开"连接电脑"按钮,输入ip地址,如下图,点击确认,即可与电脑同步。



连接成功后出现如下提示,此时开发环境搭建完成。



测试hello world程序,创建Autox.js文件,并输入内容toast("hello world!"),选择js文件,右键-重新运行,即可将脚本同步到手机运行,此时手机会出现hello world!的一个toast提示。



js脚本开发指导


关于autojs的API可参考官方文档,这里主要是讲解一下使用的思路。我们在开发自动化工具时,最常见的问题就是如何找到我们所需要点击的控件节点,每一个节点包含的信息包括:



  • className 类名。类名表示一个控件的类型,例如文本控件为"android.widget.TextView",图片控件为"android.widget.ImageView"等。

  • id控件节点的唯一id。

  • text节点名字,不一定有,可能为空。

  • desc节点的描述信息,不一定有,可能为空。

  • packageName 包名。包名表示控件所在的应用包名,例如 QQ 界面的控件的包名为"com.tencent.mobileqq"。

  • bounds 控件在屏幕上的范围。

  • drawingOrder 控件在父控件的绘制顺序。

  • indexInParent 控件在父控件的位置。

  • clickable 控件是否可点击。

  • longClickable 控件是否可长按。

  • checkable 控件是否可勾选。

  • checked 控件是否可以勾选。

  • scrollable 控件是否可滑动。

  • selected 控件是否已选择。

  • editable 控件是否可编辑。

  • visibleToUser 控件是否可见。

  • enabled 控件是否已启用。

  • depth 控件的布局深度。


控件id是最为常用的一个唯一性标记,我们写自动化认为时,经常使用id来对特定控件做点击操作。但是我们如何得知具体控件id信息呢?我们可以利用以下js脚本,将整个界面的控件信息进行打印输出。


toastLog("start.");

function printNode(node){
if(node){
var text = node.text();
var desc = node.desc();
let bounds = node.bounds();
let left = bounds.left;
let top = bounds.top;
let right = bounds.right;
let bottom = bounds.bottom;
var click = node.clickable();
var id = node.id();
log(id, text, desc, click, left, right, top, bottom);
}
}

function traverse(node) {
printNode(node);
var cnt = node.childCount();
for (var i = 0; i < cnt; i++) {
traverse(node.child(i));
}
}

let windowRoot = auto.rootInActiveWindow;
if(windowRoot){
log("tracerse node.");
traverse(windowRoot);
}else{
log("window root is null.");
}

我们可以结合node.bounds()中控件的大小以及所在位置,来猜测我们所要点击的目标控件。在获得某个具体控件id后,即可使用如下js脚本进行点击操作。


target_id=""
id(target_id).findOne().click()

查看viewid脚本开发


这一节我们将利用canvas绘图将每个控件绘制出来,让我们方便地看出来我们所要操作的控件viewid。首先我们需要利用递归方式遍历当前页面上的所有控件,并存放在list变量中,如下。


function traverse(node) {
if(node != null){
viewNodes.push(node);
}
var cnt = node.childCount();
for (var i = 0; i < cnt; i++) {
traverse(node.child(i));
}
}

//x:946, y:80
let windowRoot = auto.rootInActiveWindow;

if(windowRoot){
log("tracerse node.");
traverse(windowRoot); // 开始遍历控件树并打印控件的text属性
}else{
log("window root is null.");
}

function printNode(i, node){
if(node){
var text = node.text();
var desc = node.desc();
let bounds = node.bounds();
let left = bounds.left;
let top = bounds.top;
let right = bounds.right;
let bottom = bounds.bottom;
var click = node.clickable();
var id = node.id();
log(i, id, text, desc, click, left, right, top, bottom);
}
}
var len = viewNodes.length;
for (var i = 0; i < len; i++) {
let childViewNode = viewNodes[i];
printNode(i, childViewNode);
}

使用浮窗功能,在顶层绘制一张透明的画布,如下:


//ui布局为一块画布
var w = floaty.rawWindow(
<frame gravity="center" bg="#ffffff">
<canvas id="canvas" layout_weight="1"/>
</frame>
);

w.setSize(device.width, device.height); // 设置窗口大小
w.setTouchable(false); // 设置触摸透传

使用canvas绘图库,用绿色边框将各个控件圈出,并在每个控件上显示在list中对应的序号。


let paint = new Paint();
paint.setColor(colors.parseColor("#00ff00"));
paint.setStrokeWidth(5);
paint.setStyle(Paint.Style.STROKE);

let paintText = new Paint();
paintText.setColor(colors.parseColor("#FF0000"));
paintText.setTextSize(80);
paintText.setStrokeWidth(20);

var isDraw = 1;
w.canvas.on("draw", function (canvas) {
if(isDraw < 20){
isDraw = isDraw + 1;
var len = viewNodes.length;
for (var i = 0; i < len; i++) {
let childViewNode = viewNodes[i];
let bounds = childViewNode.bounds();
let left = bounds.left;
let top = bounds.top;
let right = bounds.right;
let bottom = bounds.bottom;
canvas.drawRect(left, top, right, bottom, paint);
// log(left, bottom, right, top)
canvas.drawText("" + i, left, bottom, paintText);
}
}
});

为了不让脚本退出,我们需要使用设置等待时间,让脚本持续运行,如下,若没有等待执行,脚本执行后立马退出,我们将无法看到绘图内容。


setTimeout(()=>{
w.close();
}, 50000);

效果图如下:



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

Android通过BLE传输文件

1、遇到的问题 公司要通过Android设备给外围设备的固件进行OTA升级,最开始想到的有两种方案。 1、将当前Android设备所连接 Wifi名称,WiFi密码通过BLE发送给外围设备。 外围设备拿到当前环境的WiFi名称和密码连接热点, 然后自己去服务器...
继续阅读 »

1、遇到的问题


公司要通过Android设备给外围设备的固件进行OTA升级,最开始想到的有两种方案。


1、将当前Android设备所连接 Wifi名称,WiFi密码通过BLE发送给外围设备。 外围设备拿到当前环境的WiFi名称和密码连接热点, 然后自己去服务器下载OTA文件,进行升级

2、当前Android设备和外围设备通过经典蓝牙进行传输OTA文件, 外围设备拿到OTA文件进行升级

但是很遗憾,外围设备既没有WiFi芯片, 也没有经典蓝牙芯片, 只有一颗BLE(低功耗蓝牙)芯片。 这意味着上面的两种方案都行不通。 那我们能不能通过BLE芯片来做文章, 来传输OTA文件?


BLE设计之初就是为了传输简单的指令的, 传输一些小数据的, 每次发送的数据大小不能超过20个字节。到底靠不靠谱啊?


2、 能不能通过BLE传输文件


让我们来问问 GPT 吧


p9uZOaR.png


GPT 的回答, 是可以通过BLE传输文件的, 由于BLE 每次传输的内容最大为20个字节, 传输大文件时就需要分包传输,
同时需要确保分包传输的可靠性和稳定性。


3、 如何传输文件


让 GPT 给我们一些示例代码


p9uekdA.png


可以看出, 发送端分包批量发送数据,接收端


4、如何保证可靠性和稳定性


p9K6UHO.png


1、超时重传


蓝牙在传输过程中, 可能会存在丢包的情况。分两种情况,
1、Android设备发送的数据,外设设备没有收到。
2、Android设备发送的数据,外设设备收到了,并且发送了回复确认。 回复确认包Android设备却没有收到。


出现了这两种情况的任意一种, 则认为发生了丢包的情况。 Android 对这个包进行重发。


2、序列号


针对超时重传的第二种情况, 外设设备会收到两个相同的包。 但是外设设备不清楚是不是重装包。 这时就要给每个数据包添加序列号。 等外设设备收到两个相同序列号的数据包时, 丢弃这个数据包, 回复Android设备收到此包, 开始发送下一个数据包。


3、数据校验


BLE在传输的过程中, 如果周围环境有强蓝牙干扰,或者其他传输通道, 可能会导致数据包变更, 所以需要在数据包添加一个校验位, 这个校验位根据特定的算法,由数据包的数据计算得来。 外设设备收到数据后, 重新计算校验位, 判断数据传输过程是否出现差错, 如果计算的校验位和包传输的校验位不一致, 则要求Android设备重新发送这个包。


5、 传输速度提升 RequestMtu


为了保证传输过程中的可靠性和稳定性,我们需要在传输包中,添加序列号,数据校验等信息。 Android默认每个BLE数据包不超过20个字节,当我们加了一些其他信息时, 每次传输的有效数据可能只有15个字节左右。 导致在传输的过程中分包更多, 传输时间更长。


为了提升传输的速度, 我们来提升BLE每个数据包的传输大小限制, 使每个分包可以传输更多的数据。 系统为我们提供了 RequestMtu这个接口。 需要在gatt连接成功时调用


    private val bluetoothGattCallback = object : BluetoothGattCallback() {

override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
super.onConnectionStateChange(gatt, status, newState)

if (newState == BluetoothGatt.STATE_CONNECTED) {
Log.d(TAG, "gatt 连接成功")
gatt?.requestMtu(40)
} else {
Log.d(TAG, "gatt 连接失败 status $status newstate $newState")
}

}


override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
super.onMtuChanged(gatt, mtu, status)

if (BluetoothGatt.GATT_SUCCESS == status) {
Log.d(TAG, "onMtuChanged suc : $mtu")
gatt?.discoverServices()
} else {
Log.d(TAG, "onMtuChanged fail : $status")
}
}
}

MTU改变成功后, 再去gatt.discoverServices()发现服务


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

【Android】Kotlin 中的 apply、let、also、run 到底有啥区别?

前言 Kotlin 作为一门现代的编程语言,为了提高代码的可读性和简洁性,引入了许多扩展函数。本文将详细介绍 Kotlin 中的四个常用扩展函数:apply、let、also、run,并比较它们的联系与区别。 apply apply 函数接收一个对象并返回该对...
继续阅读 »

前言


Kotlin 作为一门现代的编程语言,为了提高代码的可读性和简洁性,引入了许多扩展函数。本文将详细介绍 Kotlin 中的四个常用扩展函数:applyletalsorun,并比较它们的联系与区别。


apply


apply 函数接收一个对象并返回该对象本身。它允许您在对象上执行一些操作,同时仍然返回原始对象。


这个函数的语法为:


fun <T> T.apply(block: T.() -> Unit): T

其中,T 是对象的类型,block 是一个 lambda 表达式,可以在该对象上执行一些操作。在这个 lambda 中,您可以使用 this 关键字来引用当前对象。


例如:


val person = Person().apply {
name = "John"
age = 30
address = "New York"
}

在这个例子中,我们创建了一个 Person 对象并在 apply 函数中设置了该对象的属性。最终返回的是这个 Person 对象本身。


let


let 同样是一个扩展函数,它也接收一个 Lambda 表达式作为参数。不同于 apply,在 Lambda 表达式中,let 函数将接收者对象作为 Lambda 的参数,通常用 it 作为隐式名称。let 函数的返回值是 Lambda 表达式的结果。


这个函数的语法为:


kotlinCopy code
fun <T, R> T.let(block: (T) -> R): R

其中,T 是对象的类型,R 是返回值的类型,block 是一个 lambda 表达式,它对该对象进行一些操作并返回一个结果。


val person = Person("Alice", 25)
val ageAfterFiveYears = person.let {
it.age + 5
}

在这个示例中,let 函数用于计算 Person 对象五年后的年龄。


also


also 是一个扩展函数,与 apply 类似,但有一个关键区别:also 函数的返回值是接收者对象本身,而 Lambda 表达式的参数是接收者对象,通常用 it 作为隐式名称。
这个函数的语法为:


fun <T> T.also(block: (T) -> Unit): T

其中,T 是对象的类型,block 是一个 lambda 表达式,可以在该对象上执行一些操作。在这个 lambda 中,您可以使用 it 关键字来引用当前对象。


val person = Person("Alice", 25).also {
it.name = "Bob"
it.age = 30
}

在上述示例中,also 函数用于修改 Person 类的属性,最后返回修改后的对象。


run


run 是一个扩展函数,它结合了 applylet 的特点。run 函数在 Lambda 表达式中直接访问接收者对象的属性和方法,同时返回 Lambda 表达式的结果。


这个函数的语法为:


fun <T, R> T.run(block: T.() -> R): R

其中,T 是对象的类型,R 是返回值的类型,block 是一个 lambda 表达式,它对该对象进行一些操作并返回一个结果。在这个 lambda 中,您可以使用 this 关键字来引用当前对象。


val person = Person("Alice", 25)
val greeting = person.run {
"Hello, $name! You are $age years old."
}

在这个示例中,run 函数用于生成一个包含 Person 对象信息的字符串。


总结


四个函数的相同点是,它们都可以操作对象,并可以在 lambda 中引用当前对象。但是,它们的返回值和返回时机有所不同。


apply 和 also 函数的返回值是该对象本身,而 let 和 run 函数的返回值是 lambda 表达式的结果。


apply 函数在对象上执行一些操作,并返回该对象本身。它通常用于在对象创建后立即对其进行初始化。


also 函数类似于 apply 函数,但它返回原始对象的引用。它通常用于对对象进行一些副作用,例如打印日志或修改对象状态。


let 函数在 lambda 中对对象进行一些操作,并返回 lambda 表达式的结果。它通常用于在某些条件下对对象进行转换或计算。


run 函数类似于 let 函数,但它返回 lambda 表达式的结果。它通常用于对对象进行计算,并返回计算结果。


总之,这四个函数都是非常有用的函数式编程工具,可以帮助您以简洁、可读性强的方式操作对象和代码块。对于每个情况,您应该选择最合适的函数,以便以最有效的方式编写代码。


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

我的30岁,难且正确的事情是什么?

3月意料之中的最后裁员到来了,在充分了解个人意愿后留下两个不想看新工作的小伙伴,IOS Android各一个。我把自己也写进了名单,跟其他能力相对强一点的一起出来了。回顾过去2年我们做的事情,我对大家还是蛮有信心的。同时基于对《钱》这本书的学习,我从前两年开始...
继续阅读 »

3月意料之中的最后裁员到来了,在充分了解个人意愿后留下两个不想看新工作的小伙伴,IOS Android各一个。我把自己也写进了名单,跟其他能力相对强一点的一起出来了。回顾过去2年我们做的事情,我对大家还是蛮有信心的。同时基于对《钱》这本书的学习,我从前两年开始就一直留有1年以上的备用金,所以暂时也没太大经济压力,不至于因为囊中羞涩着急找一份谋生的工作。


刚离开公司的前两周,先花了1000多找了两个职业咨询师,了解目前的招聘环境,招聘平台,招聘数据,以及针对性的帮助我修改简历。都准备好以后,开始选公司试投简历,认真看完大部分JD后大概清楚自己的能力所匹配的公司,薪资范围。机会确实不多,移动端管理岗位,架构岗位就更少,尤其是像我这样工作不到10年,架构跟管理经验都还未满5年的人,选择更是寥寥无几。


先后参加了两个2面试,一个是小团队的移动 TL,在了解后双边意向都不大。另一个是 Android 架构方向。虽然拿了offer,薪资包平移,但我最终没去。一是发生了一点小误会,发offer前电话没告诉我职级,我以为架构岗过了其实没有,差一点点到P7。回看面试记录,提升并不困难,有能力冲一冲的,这一次并不会影响我的信心。


另一个则是我真的冷静下来了,也就有了这篇文章。


在这两周里,陆续写了一些文章,做了一些开源项目。完全是出于助人为乐回馈社区,没想到也因此结识了几个做阅读业务的同学,纷纷向我抛来橄榄枝。其中包含一个已经在行业内做到Top3的产品。这让我有些受宠若惊,毕竟我觉得我给的技术方案并非有很大门槛,只是运气好站在巨人的肩膀上想到了不同的方案而已。


正是这些非常正面的反馈,帮助我消化了很大一部分所谓的焦虑(难说我本身一点不受环境影响)。在Zhuang的帮助下,我大概做了两次自我梳理,最后在前几天我从地铁回家大概3km的步行中想明白了很多事情。


每次出去旅游时,比如我躺在草原上,看着日落,说实话我欣赏不了10分钟。因为我的思绪停不下来,我就会去想一些产品或者是管理方面的问题。我很爱工作,或者说喜欢工作时可以反复获取创造性的快乐,比如做出一个新的技术方案或者优化工作流程解决一个团队问题,都让人很兴奋。但现在,我想强迫自己来思考一些更长期的事情。


我的30岁,难而且正确的事情是什么?


是找一份工作吗?这显然不难,作为技术人,找一份薪资合理的工作不难,这恰恰是最容易的


是找一份自己喜欢的工作吗?这有一点难,更多的是需要运气。职业生涯就十几年,有几次选择的机会呢?这更多的是在合理化自己对稳定性,舒适性的追求,掩盖自己对风险的逃避。


是找一个自己喜欢的事情,并以此谋生吗?这很难,比如先找到自己长期喜欢长期坚持投入的事情就很难,再以此谋生就需要更多的运气与常年积累去等待这个运气出现,比如一些up主。这可以是顺其自然的理想,但不适合作为目标。


上面似乎都是一个个的问题,或者说看到这些问题的时候我就看到了自己的天花板了。因为我可以预见我在这些方向上的学习能力,积累速度,成长空间,资源储备。


这半年涌出了太多的新事物,像极了02年前后的互联网,14前后的移动互联网。我从去年12月5日开始使用GPT,帮助我提高工作,学习效率,帮助我做UI设计,帮助我改善代码,甚至帮助我学习开网店时做选品,做策略,可以说他已经完全融入我的工作学习了。


开发自己的GPT应用要仔细阅读OPEN AI 的API,我再次因为英语的理解速度过慢严重影响学习效率,即使是有众多实时翻译软件帮助下丝毫不会有所改善。


翻译必然会对原文做二次加工,翻译的质量也许很高,甚至超过原文,但这样意味着阅读者离原文越远。


比如我在Tandem上教老外“冰雪聪明”这个词的意思,我很难解释给她,更多的是告诉她这个词在什么场景用比较恰当,比“聪明”更高级。但是如果用翻译软件,这个词会变着花样被翻译成“很聪明”,美感全无。


在Tandem跟人瞎聊时以涉及复杂事件就词穷,直到认识了一个 西班牙的 PHD 与另一个 印尼的大学生,她们帮我找到了关键点,基础语法知识不扎实,英语的思维不足。有些时候他们会说我表达的很棒,口语也行,有些时候他们会说我瞎搞。其实很好理解,就像他们学中文一样,入门也不难,难的是随意调动有限的词汇自由组织句子进行表达,而不是脑子里先想一个母语再试着翻译成外语,难的是在陌生场景下做出正确的表达,能用已经学的知识学习新知识,也就是进入用英语学习英语的阶段。


另外一个例子就是做日常技术学习的时候,尤其是阅读源码的时候,往往是不翻译看懂一部分注释,翻译后看懂一部分,两个一结合就半懂不懂,基于这个半懂不懂的理解写大量测试去验证自己的理解,反推注释是否理解正确,这个过程非常慢,效率极低。


这就是为什么很多东西需要依赖大佬写个介绍文档,或是翻译+延伸解释之后才能高效率学习,为什么自己找不到深入学习的路径,总是觉得前方有些混沌。


记得在刚入行的前几年写过一篇学习笔记,把自定义view 在view层测量相关的代码中的注释,变量名称整个都翻译了,备注2进制标记位变化结果,再去理解逻辑就非常简单了。跟读小说没啥区别(读Java代码就像读小说,我一直这么觉得),很快就理解了。但这个过程要花太多时间了,多到比别人慢10倍不止。


所以这第一个难而正确的事情是学习英语


达到能顺畅阅读技术资料与代码的地步,才能提高我在学习效率上的天花板。


第二个是有关生活的事情,增加不同的收入手段,主业以外至少赚到1块钱


裁员给我最大的感触就是,我很脆弱,我的职业生涯很脆弱,我的生存能力很脆弱,不具备一点反脆弱性。如果没有工作我就没有任何收入,只要稍微发生一点意外,就会面临巨大的经济压力,对于我和家庭都会陷入严重的经济困难中。


市场寒冬与我有关但却不受我影响,我无法改变。同时平庸的职业经历在行业内的影响微乎其微,大佬们是不管寒风往哪吹的,他们只管找自己想做的方向,或者别人找到他们。


我就认识这样的大佬,去年让我去新公司负责组新团队,连续一两周持续对我进行电话轰炸,因为当时正负责的团队处于关键期,我有很深的“良知”情节,我婉拒了,这是优点也是缺点。


而我只有不断提高自己的能力,让人觉得有价值才能持续在这个行业跟关系网里谋生。


但是我知道,大风之下我依然是树叶,我不是树枝,成为树枝需要天时地利人和。就像在公司背小锅的永远都是一线,因为如果是管理层背锅那公司就出了决策性的大问题了,对公司而言已然就是灾难。


这几周陆续跟很多人聊了各种事情,了解他们在做什么。有双双辞职1年多就在家做私活忙得不亦乐乎,有开网店有做跨境电商的,也了解了很多用Chat GPT,Midjourney 等AI工具做实物产品在网上卖的。包括去年了解的生财有术知识星球等等,真的花心思去了解,打开知识茧房确实了解到非常多不同的方向,有一种刘姥姥进大观园的感觉。


自己做了一些实际尝试,跑了下基本流程,确实有一些门槛但各不相同。同时在这个过程中,又因为英语阅读效率低而受阻,文档我也是硬看,不懂的词翻译一下,理解不透再整句翻译,再倒回来看原文。


比如网上midjourney的教程一大把,其实大多数都不如看midjourney官方文档来的快,我从看到用到商品上架,不过几个小时,这中间还包括开通支付跟调整模型。


至于赚到1块钱,有多难呢,当我试了我才有所体会。


种一棵树最好的时间是在10年前,其次是现在。


继续保持在社区的输出,保持技术学习。休假我都不会完全休息,Gap 中当然也不会。


后记


去年公司陆续开始裁撤业务线,有的部门直接清零,公司规模从几千人下降到千人以内不过是几个月的事情,有被裁的,也有为了降低自身风险而主动走裁员名单,这也是双赢的解决方案,公司能精简人员个人可以拿到赔偿。管理层的主要工作是尽力留下核心成员,温和的送走要离开的成员,最大程度降低团队的负面情绪,做人才盘点,申请HC做些人力补充,减少团队震动保障项目支撑。没错,一边裁员一边还会招人。


彼时我个人也才刚刚在管理岗上站稳脚跟不久,团队里没有人申请主动离职算是对我挺大一个宽慰。有的团队人员流失率接近70%,相比之下我压力小得多,但我依然要交出一个名字给到部门负责人。我当然很不舍同时也为他们担忧,过去一年多大家一起相互成长,很多人也才刚刚步入新职级。


我努力寻找第三选择,功夫不负有心人,之前做过的一个项目独立出去了,成立了独立的子公司运营,新团还没搭建完。当时跟那个项目团队的产品,后端负责人配合得相当不错,我便以个人的背书将一个曾重点负责过这个项目的成员推荐过去,加上直属上级的帮助,最终在所有HC都要HRD审批的环境下平滑的将裁员变成了团队调配。现在即使我离开了母公司,他们小团队依然还不错,没有受到后续裁员影响。这位小伙伴人特别实在,他是我见过执行里最强的人,他值得这样的好运气。


作为管理者,我有些单纯的善意,不满足于工作层面的帮助。因为我觉得个人能量实在是太小了,而未来无人知晓。


作为核心部门虽然裁员的影响波及较为滞后,但明显的感觉还是研发压力骤减,加上公司为了早点达到账面盈亏平衡,对部分薪资采取缓发,在这样的背景下整个部门的氛围变了,需求评审得过且过,项目质量得过且过,此类情况比比皆是,工作的宽容度也一再升高。


作为个人来讲这真是躺平型工作,工作任务骤减但薪资还照样发,绩效照发,每天到公司跟去上学一样。我心里就出现了一个声音「你喜欢在这里继续混吗?这里如此安逸」。


今年3月意料之中的新一轮裁员到来,我几乎没有犹豫就答复了部门负责人。团里谁想留下谁不想留我很清楚,过去我们一直也保持比较健康的氛围,始终鼓励能力强的人出去看看,也明确告知留下来与出去将面临的不同风险。大家都有心理准备,但大家都没有懈怠自己的学习,技术目标按部就班,丝毫没有陷入负面漩涡,偶尔还会因为讨论技术问题忘记下班时间。


这一次,我把自己放在了名单上,当然这并不突然。我与部门负责人一直保持着较高的工作沟通频率,就向上管理这一点上,我自认做得非常不错。


离职后大家都积极找工作,我对他们非常有信心,抛开头部大厂,中厂依然是他们的主阵地,他们在各自专精的领域里技术都很扎实,尤其是去年大家一起补足了深层次的网络层知识。不出意料部分人都很快拿了offer,有的更是觉得不想面试了匆匆就入职了,这我就不行使自己好为人师的毛病了。


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

Android 自定义开源库 EasyView

  这是一个简单方便的Android自定义View库,我一直有一个想法弄一个开源库,现在这个想法付诸实现了,如果有什么需要自定义的View可以提出来,不一定都会采纳,合理的会采纳,时间周期不保证,咱要量力而行呀,踏实一点。 配置EasyView 1. 工程b...
继续阅读 »

  这是一个简单方便的Android自定义View库,我一直有一个想法弄一个开源库,现在这个想法付诸实现了,如果有什么需要自定义的View可以提出来,不一定都会采纳,合理的会采纳,时间周期不保证,咱要量力而行呀,踏实一点。


1682474222191_095705.gif.gif


配置EasyView


1. 工程build.gradle 或 settings.gradle配置


   代码已经推送到MavenCentral(),在Android Studio 4.2以后的版本中默认在创建工程的时候使用MavenCentral(),而不是jcenter()


   如果是之前的版本则需要在repositories{}闭包中添加mavenCentral(),不同的是,老版本的Android Studio是在工程的build.gradle中添加,而新版本是工程的settings.gradle中添加,如果已经添加,则不要重复添加。


repositories {
...
mavenCentral()
}

2. 使用模块的build.gradle配置


   例如在app模块中使用,则打开app模块下的build.gradle,在dependencies{}闭包下添加即可,之后记得要Sync Now


dependencies {
implementation 'io.github.lilongweidev:easyview:1.0.2'
}

使用EasyView


   这是一个自定义View的库,会慢慢丰富里面的自定义View,我先画个饼再说。


一、MacAddressEditText


   MacAddressEditText是一个蓝牙Mac地址输入控件,点击之后出现一个定制的Hex键盘,用于输入值。


1. xml中使用


   首先是在xml中添加如下代码,具体参考app模块中的activity_main.xml。


    <com.easy.view.MacAddressEditText
android:id="@+id/mac_et"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:boxBackgroundColor="@color/white"
app:boxStrokeColor="@color/black"
app:boxStrokeWidth="2dp"
app:boxWidth="48dp"
app:separator=":"
app:textColor="@color/black"
app:textSize="14sp" />


2. 属性介绍


   这里使用了MacAddressEditText的所有属性,可以自行进行设置,使用说明参考下表。







































属性说明
app:boxBackgroundColor设置输入框的背景颜色
app:boxStrokeColor设置输入框的边框颜色
app:boxStrokeWidth设置输入框的边框大小
app:boxWidth设置输入框大小
app:separatorMac地址的分隔符,例如分号:
app:textColor设置输入框文字颜色
app:textSize设置输入框文字大小

3. 代码中使用


    MacAddressEditText macEt = findViewById(R.id.mac_et);
String macAddress = macEt.getMacAddress();

   macAddress可能会是空字符串,使用之前请判断一下,参考app模块中的MainActivity中的使用方式。


二、CircularProgressBar


   CircularProgressBar是圆环进度条控件。


1. xml中使用


   首先是在xml中添加如下代码,具体参考app模块中的activity_main.xml。


    <com.easy.view.CircularProgressBar
android:id="@+id/cpb_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
app:maxProgress="100"
app:progress="10"
app:progressbarBackgroundColor="@color/purple_500"
app:progressbarColor="@color/purple_200"
app:radius="80dp"
app:strokeWidth="16dp"
app:text="10%"
app:textColor="@color/teal_200"
app:textSize="28sp" />

2. 属性介绍


   这里使用了MacAddressEditText的所有属性,可以自行进行设置,使用说明参考下表。















































属性说明
app:maxProgress最大进度
app:progress当前进度
app:progressbarBackgroundColor进度条背景颜色
app:progressbarColor进度颜色
app:radius半径,用于设置圆环的大小
app:strokeWidth进度条大小
app:text进度条中心文字
app:textColor进度条中心文字颜色
app:textSize进度条中心文字大小

3. 代码中使用


    CircularProgressBar cpbTest = findViewById(R.id.cpb_test);
int progress = 10;
cpbTest.setText(progress + "%");
cpbTest.setProgress(progress);

   参考app模块中的MainActivity中的使用方式。


三、TimingTextView


   TimingTextView是计时文字控件


1. xml中使用


   首先是在xml中添加如下代码,具体参考app模块中的activity_main.xml。


    <com.easy.view.TimingTextView
android:id="@+id/tv_timing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="计时文字"
android:textColor="@color/black"
android:textSize="32sp"
app:countdown="false"
app:max="60"
app:unit="s" />

2. 属性介绍


   这里使用了TimingTextView的自定义属性不多,只有3个,TextView的属性就不列举说明,使用说明参考下表。























属性说明
app:countdown是否倒计时
app:max最大时间长度
app:unit时间单位:s(秒)、m(分)、h(时)

3. 代码中使用


    TimingTextView tvTiming = findViewById(R.id.tv_timing);
tvTiming.setMax(6);//最大时间
tvTiming.setCountDown(false);//是否倒计时
tvTiming.setUnit(3);//单位 秒
tvTiming.setListener(new TimingListener() {
@Override
public void onEnd() {
//定时结束
}
});
//开始计时
tvTiming.start();
//停止计时
//tvTiming.end();

   参考app模块中的MainActivity中的使用方式。


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

前端实习近半年的工作总结

前言:     来北京工作实习已经一年,总结一下自己这最近半年的实习经历的感受与收获吧。     首先感谢一下组内的同事,以及我的mentor对我入职实习期间的帮助,以及对我日常问题的解答,特别是我的mentor,从他身...
继续阅读 »

前言:


    来北京工作实习已经一年,总结一下自己这最近半年的实习经历的感受与收获吧。


    首先感谢一下组内的同事,以及我的mentor对我入职实习期间的帮助,以及对我日常问题的解答,特别是我的mentor,从他身上学到了很多优秀的开发习惯和技巧。


    其实在写这篇总结之前,我也去阅读了很多作者写的工作总结,看完大家工作总结之后,我发现自己这半年的工作还是太安逸了,没有逃离自己的舒适圈,这半年自己并没有主动去学习新的一些技术,更多的只是在完成业务需求,但是在工作中想有所提升仅仅做业务开发是远远不够的,要想成长就必须学习新的技术,做一些技术产出才可以,所以这次的总结更多的是对自己的反思以及对之后正式工作的计划与展望。


工作内容:


     部门的业务主要是toC方向做小程序的开发,刚来到公司前期主要跟我的mentor一起负责砍价业务,我用了一周时间来了解砍价相关业务以及代码,并且做了一个宣讲(由于自己当时对于部门业务还不是非常了解,当时的宣讲更多的仅仅是针对前端的业务以及代码逻辑,并没有去了解每一个接口内部的实现,所以感觉第一次宣讲并没有表现很好),后来由于内部调整我就开始主要负责列表的相关需求。我所在的部门属于全栈开发,前端主要采用是微信小程序以及Vue,并且我们有自己的node中间层来封装接口,所以平时会经常写node.js+TS,这在我上一家公司是没有体会过的,(我觉得这样非常合理,再也不用像在上家公司一样,因为一些接口返回字段类型不统一去排查和后端交流好久)这也让我页接触学习到了更广泛的技术栈。


    平时除了开发任务还包括一些日常的巡检等等,虽然现在我还没有加入到日常的巡检当中,但是在最近几个迭代,我也开始自己对每天的指标进行观察巡检,总结巡检报告。


成长与收获:


工具的使用:


1、charles



一款常见的抓包工具,通过代理连接,把客户端的请求数据,以及服务端返回的数据完完整整的抓取下来,方便开发调试,一般搭配switchhost来使用,switchhos是用来改变本地host的工具,实现原理就是通过修改我们本地dns域名解析与ip之间的映射关系。


2、apiPost


在此之前我只知道用postman来请求接口、mock数据,但是经过我mentor的推荐,我发现apiPost是真的好用,用起来很方便推荐给大家!


(我mentor电脑里有非常多奇奇怪怪但是又实用的工具,每一样拿出来都让我直呼🐂🍺,包括他vscode中的各种插件👍,附上一个巨好用的截图插件Snipaste,大家自行感受)


3、Echarts



echarts.js是百度团队推出的一款用于图表可视化的插件,用于以图表的形式展现数据,功能强大,上手简单,在我认知里他和element-UI都属于辅助开发的工具,当时用的多了自然就会精通熟练,不用刻意的去学习,所以把它归到了工具使用这一类。这个是刚进入公司和我mentor一起做一个砍价报表的需求里面大量的图表就用到了Echarts,里面包含折线图,柱状图等等,非常丰富。


技术的拓展:


1、node.js


在来到这个公司之前,我自己也使用node的express框架写过一些小的demo,多少对node有一些了解,express是基于content中间件框架,框架自身封装了大量的功能比如路由router和视图处理等等;我现在开发使用的是基于koa搭建的一个自己的框架,koa相较于express的使用更加灵活,并且我们框架的层次划分非常清晰,把业务代码按照controller、busoness、agent三层层来细分处理,减少了代码的冗余并且更加整洁,方便理解。


2、TypeScript


TS又称为javascript的超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。自我感觉Ts在帮我们写出更加强壮的代码,在开发过程中就能将我们的一些错误暴漏出来,编写代码的提示也极大的提升了我们的开发效率,不过TypeScript的学习并不是那么简单的,需要经过大量的联系和阅读,对我来说泛型对我来说掌握的还不够,也是后边学习的重点。


3、vuex


Vuex属于vue的一种状态管理模式,将页面组件中可以共享的变量方法进行统一的管理,告别了一层一层的变量传递,在之前只是说去了解学习过Vuex的一些用法知识,最近做公司的H5项目时多数页面都是用vuex来管理数据的,也是在实践中系统的使用学习了一下。


4、socket


socket通信是新接触的知识,Socket是应用层与TCP/IP协议族通信的中间软件抽象层,是一组接口(也是刚刚查的),部门前段时间刚刚做过分享,建立微信与h5之间的socket通信,还是比较感兴趣的,计划在下半年进行了解和学习。


5、webpack/vite


最近在做公司h5项目的时候感到很烦恼,由于项目很庞大,加上我的电脑真是非常的卡,所以每次启动h5项目的时候都要超级长的时间,n分钟,公司的项目是由webpack构建的,所以就在考虑学习一种更加快速的大包构建工具去尝试优化一下这个项目,vite就是学习的对象,从底层原理上来说,Vite是基于esbuild预构建依赖。而esbuild是采用go语言编写,因为go语言的操作是纳秒级别,而js是以毫秒计数,所以vite比用js编写的打包器快10-100倍。所以接下来的目标就是去学习这个webpack以及vite,学习优化的过程也是对自己的考验。


习惯的养成:


      其实这半年里我觉得最好的习惯就是跟我mentor学习的记笔记,刚来公司就发现我mentor每次做完需求都会记笔记复盘,包括学习一些技术等等都会记笔记,渐渐的我也养成了这个习惯,但是现在记得一些东西都还不是很成熟,都是一些做需求过程中当作思路分析来写的,之后在笔记的书写上应该对自己增加些要求不能太过随意(因为前几天看之前写的已经看不懂当时写的是啥了!)。


      还有一个就是写文章!这个总结是我在掘金写的第一篇文章,之后每周或者每学一一个内容都会在掘金记录一下总结和收获,看了这么多大佬的文章,真的觉得自己非常渺小,作为一个小白,要从头开始把所有知识都从新学习一下。


下半年学习计划:


1、vue3.x:自己之前一直都在使用学习vue2,并没有扩展到vue3,在不学习就要被淘汰了,所以vue3是下半年学习的重点!


2、TS:ts自己现在也只是了解皮毛,可以进行开发,但是掌握的还是太少,要继续学习。


3、webpack/vite:还有就是刚才说的webpack/vite,自己要学习并且优化公司的h5项目!


4、socket:这个作为下半年的拓展,了解的同时也可以复习一下网络的所有知识。


5、markdown语法:第一次写文章就没那么讲究了,写的很丑,下次学一下markdown语法,把文章写的漂亮一点。


阅读书籍:


下面这些是被我列在清单里的书籍,也是同事推荐给我的







结束语


总之这半年过的还是浑浑噩噩太过安逸,嘴上说着是因为弄毕业设计但是自己知道并没有去努力,下半年逃出自己的舒适圈!完成自己的计划,向着目标前进!


作者:fc550
来源:juejin.cn/post/7121378029678362638
收起阅读 »

一个前端实习生在蔚来的成长小结

此文章2332字,预计花费时间7-11分钟。 一、聊聊工作氛围 & 个人成长 1. 这是我的期许 “所谓前途,不过是以后生活的归途。” 这是我人生中的第一段技术实习,之前并不是一心做技术的,所以为了探索自己喜欢的事情在某小公司做过翻译实习,并且在交行...
继续阅读 »

此文章2332字,预计花费时间7-11分钟。


image.png


一、聊聊工作氛围 & 个人成长


1. 这是我的期许


“所谓前途,不过是以后生活的归途。”


这是我人生中的第一段技术实习,之前并不是一心做技术的,所以为了探索自己喜欢的事情在某小公司做过翻译实习,并且在交行做过金融实习(其实就是纯打杂)。


image.png


我很喜欢这样一段话: “我曾以为我的23岁会手提皮包西装革履,但我还是穿着休闲裤,带着十几岁的意气行事,幼稚又成熟;我曾以为我的23岁会性格外向,做事圆滑,但我连最简单的亲情都处理不好;我曾以为我的23岁会和喜欢的人看山河大海落日余晖,但没想道周围的人谈婚论嫁都近在眼前,我还在路上找自己。”


我一直在探索着自己的边界,在能闯能疯的年纪反复横跳,寻找着自己的热爱与期许。在真正从事这个行业之后,我发现了我对于这个岗位的喜爱,当你看着一个个实际的视图出现于自己的手中,你会有一种莫名其妙的成就感,这种感觉难以描述的程度就好像你要向一个完全不看vtuber的人描述你对嘉然的喜爱。


2. 工作氛围:这里是一个乌托邦(适合摸鱼学习的好地方!)


说实话,我最开始预期是每天九点来上班,九点下班的(因为看学长们实习都好辛苦的样子)。


来了之后发现完全不是,每天十点上班,六点下班(我当然是准点跑路)



实习两个月左右的时候接的一个需求,第一天是另一个前端实习生来搞,后来他要跑路,leader就把活给我了。


周四,后端六点把接口给另一个前端实习生。


另一个前端实习生:“明天再说”


周五我来接这个活,我边画页面边让他加字段。


然后提完了,六点他给我改好的接口,让我看看有没问题


我:“下周再说”。


后端:“前端是不是,都很快乐啊[流泪]”



image.png


最开始因为我对 react 不是特别熟悉,leader 让我看着组内文档学了半个月,才开始了第一个需求。


leader 没有给我指定 mentor,所以当我有问题的时候,我看组内谁没开会(或者有时间)就会去问,都能得到很耐心的解答,这点来说还是很有安全感的。


然后每天都会跟着老板和大老板一起去吃饭,有时听他们说说自己的事情,有时听听他们对某个语言的看法,也算有不少收获。


值得一提的是刚入职三天部门就开始团建了,从周五下午五点玩到了第二天凌晨两点,炫了一只烤全羊,然后就开始电玩篮球各种 happy,后面玩狼人杀我次次狼人,大老板也总觉得我是狼人,我次次和他对着刚(乐)



马上就要第二次团建了,可惜参加不了呜呜呜



在团建上 leader 说我是从五个面试感觉都 ok 的人里面选出来的(当时我超惊喜的)


还有几件有趣的事情值得一提



第一件事情是中午和 leader 散步,他说:“你干了两个月这里的情况也看到,很难接触到同龄的小姐姐的,找对象的优先级应该要提高了。”


我:“说的对说的对。”


当时我心里就暗暗想着,这是我不想找吗?这tm是我找不到啊(悲)


第二件事情是我有事开了自己的热点,热点的名字叫:“要失业了咋办呐。


被同事发到了前端大群里。


同事:“这是谁的啊?”


我:“是实习生的(悲)”



3. 个人成长:“不卑不亢,低调务实”


最开始入职当然会担心一些七的八的,诸如这样说会不会不太客气,这样搞会不会让老板不爽,后来和老板还有大老板一起吃饭之后发现他们人都挺随和的,没什么架子,他们更多的关心的是这件事情做的怎么样。


大老板曾经在周会上说:“这个事情可以做的慢一些,这是能力上的问题,这个可以商量,但是如果到了约定的日期没有交付,这就有问题了。 ”这个是说的务实。


然后就是为人处事方面了,自己有时候挺跳脱的,没有什么边界感,在实习和他们一起吃饭的时候我就回默默的听着,有些问题大家都不会问,算是看着看着就成长了。


回校远程的时候我写了这样一段话:



去打工吧,去打上海冬夜准时下班,踩雪走回家的工。


去打工吧,去打一边聊天一边发现,这个产品也是清华✌️的工。


去打工吧,去打测试前一天,人都走光了,mentor陪我赶工到半夜的工。


去打工吧,去打部门团建,大leader带我们玩狼人杀到凌晨两点,超级尽兴的工。


冴羽曾在一次读书会上分享:“开眼界就像开荤一样,只有见过了才会产生饥饿感。”


打工虽然让我变成了稍不注意就会摆烂的成年人,但大平台汇聚了很多丰富有趣的同事,让我看到了截然不同的经历与一波三折的人生。


不知道是不是部门的原因,我这边总是十六五准点上下班。


我现在依然处于打工真香的阶段,不用早起,不用日复一日的和同龄人卷同一件事,身边的人年岁不同,人生阶段也不相同,卷不到一起去。


我还在路上~



image.png


4. 代码方面 learning


说实话看到组内项目的时候体会到了不少的震撼,看着组内的项目之后真的就感觉自己写的东西和玩具一样,每次写完项目,都会兴冲冲的找组内的哥哥姐姐帮忙 CR,然后 CR 出一堆问题,自己在一个一个的修改,把这些规范点记周报上,总之就是学到了很多很多。


timeLine 大概是这样的



  • 前两周熟悉 react 写小 demo

  • 然后以两周一个需求的速度给咱活干~


记得第二次写完一个稍微有点复杂的需求,带着我做这个需求的 mentor 还夸了我一波(骄傲)


5. 对于技术和业务的想法


大leader组织组内 vau 对齐的时候我仔细的听了听,我们的很多东西都需要落地,相比来说技术只是一个实现的手段,并不是做这个的目的。


但怎么说呢,我个人还是对技术本身抱有很大的期许的,希望自己能够变得很厉害,参与到很多的开源项目中,我坚信代码可以改变世界。


二、展望未来



实习不去字节,就像读四大名著不看红楼梦,基督徒不看圣经,学相对论不知道爱因斯坦,看vtuber不看嘉然今天吃什么,这个人的素养与精神追求不足,成了无源之水,无本之木。他的格局就卡在这里了,只能度过一个相对失败的人生!




  • 话是这么说啦,但最后还是没有成功去到字节,但是我是字节不折不扣的舔狗,后面再看吧。

  • 字节给我发面试一定是喜欢我(普信)


下面这段是之前写的



离开的契机也很简单,我在小红书实习的同学跑路了,然后要找继任,顺手把我的简历投过去了,然后我顺手面了一下小红书,小红书顺手给我发了个Offer(bushi,然后就去小红书了。



image.png


小红书确实Offer了,但是老板和我约谈了很久,我决定继续远程实习,在这篇文章发布的当天,我已经实习了 一百四十天,我相信,我的旅途还在继续。


image.png


三、写在最后


不知不觉就实习快半年了啊


我真的非常感谢遇到的leader和同事,感恩遇到的每一位愿意拉我一把的人。


在这段时间里学到了好多一个人学习学不到的东西啊。


那么这就是我在蔚来的实习小结啦!


感谢阅读~


作者:阳树阳树
来源:juejin.cn/post/7228245665334198333
收起阅读 »

Android 自定义View 之 计时文字

前言   在Android开发中,常常会有计时的一些操作,例如收验证码的时候倒计时,秒表的计时等等,于是我就有了一个写自定义View的想法,本文效果图。 正文   那么现在我们将想法换成现实,这个自定义View比较简单,我们来看怎么写的,首先我们还是在Eas...
继续阅读 »

前言


  在Android开发中,常常会有计时的一些操作,例如收验证码的时候倒计时,秒表的计时等等,于是我就有了一个写自定义View的想法,本文效果图。


在这里插入图片描述


正文


  那么现在我们将想法换成现实,这个自定义View比较简单,我们来看怎么写的,首先我们还是在EasyView中进行添加。


一、XML样式


  根据上面的效果图,我们首先来确定XML中的属性样式,在attrs.xml中增加如下代码:


    <!--计时文字-->
<declare-styleable name="TimingTextView">
<!--倒计时-->
<attr name="countdown" format="boolean" />
<!--时间最大值-->
<attr name="max" format="integer" />
<!--时间单位,时:h,分:m,秒:s-->
<attr name="unit">
<enum name="h" value="1" />
<enum name="m" value="2" />
<enum name="s" value="3" />
</attr>
</declare-styleable>

  这里的计时文字目前有3个属性,第一个boolean用来确定是计时还是倒计时,第二个是最大时间,第三个是时间单位:时分秒。


二、构造方法


  之前我说自定义View有三种方式,一种是继承View,一种是继承现有的View,还有一种是继承ViewGroup,那么今天的这个计时文字,我们就可以继承现有的View,这样做的目的就是可以让我们减少一定的工作量,专注于功能上,下面我们在com.llw.easyview包下新建一个TimingTextView类,里面的代码如下所示:


public class TimingTextView extends MaterialTextView {

/**
* 时间单位
*/

private int mUnit;
/**
* 计时最大值
*/

private int mMax;
/**
* 是否倒计时
*/

private boolean mCountDown;
private int mTotal;
/**
* 是否计时中
*/

private boolean mTiming;

public TimingTextView(Context context) {
this(context, null);
}

public TimingTextView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}

public TimingTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
@SuppressLint("CustomViewStyleable")
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.TimingTextView);
mCountDown = typedArray.getBoolean(R.styleable.TimingTextView_countdown, false);
mMax = typedArray.getInteger(R.styleable.TimingTextView_max, 60);
mUnit = typedArray.getInt(R.styleable.TimingTextView_unit, 3);
typedArray.recycle();
}
}

  因为有计时的缘故,所以我们需要一个计时监听,主要用于结束的时候进行调用,可以在com.llw.easyview下新建一个TimingListener接口,代码如下:


public interface TimingListener {
void onEnd();
}

三、API方法


下面在TimingTextView中新增一些API方法和变量,首先增加变量:


    private TimingListener listener;
private CountDownTimer countDownTimer;

然后增加API方法:


    /**
* 设置时间单位
*
* @param unit 1,2,3
*/

public void setUnit(int unit) {
if (unit <= 0 || unit > 3) {
throw new IllegalArgumentException("unit value can only be between 1 and 3");
}
mUnit = unit;
}

/**
* 设置最大时间值
*
* @param max 最大值
*/

public void setMax(int max) {
mMax = max;
}

/**
* 设置是否为倒计时
*
* @param isCountDown true or false
*/

public void setCountDown(boolean isCountDown) {
mCountDown = isCountDown;
}

public void setListener(TimingListener listener) {
this.listener = listener;
}

public boolean isTiming() {
return mTiming;
}

/**
* 开始
*/

public void start() {
switch (mUnit) {
case 1:
mTotal = mMax * 60 * 60 * 1000;
break;
case 2:
mTotal = mMax * 60 * 1000;
break;
case 3:
mTotal = mMax * 1000;
break;
}
if (countDownTimer == null) {
countDownTimer = new CountDownTimer(mTotal, 1000) {
@Override
public void onTick(long millisUntilFinished) {
int time = 0;
if (mCountDown) {
time = (int) (millisUntilFinished / 1000);
setText(String.valueOf(time));
} else {
time = (int) (mTotal / 1000 - millisUntilFinished / 1000);
}
setText(String.valueOf(time));
}

@Override
public void onFinish() {
//倒计时结束
end();
}
};
mTiming = true;
countDownTimer.start();
}

}

/**
* 计时结束
*/

public void end() {
mTotal = 0;
mTiming = false;
countDownTimer.cancel();
countDownTimer = null;
if (listener != null) {
listener.onEnd();
}
}

代码还是很简单的,你敢信,这个自定义View就写完了,不过可能存在一些问题,我将自定义View的代码都放到了一个library下面里,然后将这个library进行构建成aar,然后上传到mavenCentral()中。


四、使用


  然后我们修改一下activity_main.xml,代码如下所示:


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp"
tools:context=".MainActivity">


<com.easy.view.MacAddressEditText
android:id="@+id/mac_et"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:boxBackgroundColor="@color/white"
app:boxStrokeColor="@color/black"
app:boxStrokeWidth="2dp"
app:boxWidth="48dp"
app:separator=":"
app:textColor="@color/black"
app:textSize="16sp" />


<Button
android:id="@+id/btn_mac"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="获取地址" />


<com.easy.view.CircularProgressBar
android:id="@+id/cpb_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
app:maxProgress="100"
app:progress="10"
app:progressbarBackgroundColor="@color/purple_500"
app:progressbarColor="@color/purple_200"
app:radius="80dp"
app:strokeWidth="16dp"
app:text="10%"
app:textColor="@color/teal_200"
app:textSize="28sp" />


<Button
android:id="@+id/btn_set_progress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="随机设置进度" />


<com.easy.view.TimingTextView
android:id="@+id/tv_timing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="计时文字"
android:textColor="@color/black"
android:textSize="32sp"
app:countdown="false"
app:max="60"
app:unit="s" />


<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center"
android:orientation="vertical">


<CheckBox
android:id="@+id/cb_flag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="计时" />


<Button
android:id="@+id/btn_start"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="开始" />

</LinearLayout>
</LinearLayout>

预览效果如下图所示:


在这里插入图片描述


下面我们回到MainActivity中,在onCreate()方法中添加如下代码:


        //计时文本操作
TimingTextView tvTiming = findViewById(R.id.tv_timing);
CheckBox cbFlag = findViewById(R.id.cb_flag);
Button btnStart = findViewById(R.id.btn_start);
tvTiming.setListener(new TimingListener() {
@Override
public void onEnd() {
tvTiming.setText("计时文字");
btnStart.setText("开始");
}
});
cbFlag.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
cbFlag.setText(isChecked ? "倒计时" : "计时");
}
});
//计时按钮点击
btnStart.setOnClickListener(v -> {
if (tvTiming.isTiming()) {
//停止计时
tvTiming.end();
btnStart.setText("开始");
} else {
tvTiming.setMax(6);
tvTiming.setCountDown(cbFlag.isChecked());
tvTiming.setUnit(3);//单位 秒
//开始计时
tvTiming.start();
btnStart.setText("停止");
}
});

下面运行一下看看:


在这里插入图片描述


五、源码


如果对你有所帮助的话,不妨 Star 或 Fork,山高水长,后会有期~


源码地址:EasyView


作者:初学者_Study
来源:juejin.cn/post/7225045596029075511
收起阅读 »

无聊的分享:点击EditText以外区域隐藏软键盘

1.前言 当我们在使用一个应用的搜索功能或者任何带有输入功能的控件时,如果想取消输入往往会点击外部空间,这个时候系统的软键盘就会自动收起,并且输入框也会清楚焦点,这样看上去很自然,其实使用EditText控件是没有这种效果的,本文章就如何实现上述效果提供一点小...
继续阅读 »

1.前言


当我们在使用一个应用的搜索功能或者任何带有输入功能的控件时,如果想取消输入往往会点击外部空间,这个时候系统的软键盘就会自动收起,并且输入框也会清楚焦点,这样看上去很自然,其实使用EditText控件是没有这种效果的,本文章就如何实现上述效果提供一点小小的思路。


2.如何实现


当我们在Activity单纯的添加一个EditText时,点击吊起软键盘,这个时候再点击EditText外部区域会是这个样子的:



会发现,无论我们怎么点击外部区域软键盘都不会收起。所以要达到点击外部区域收起键盘效果需要我们自己添加方法去隐藏键盘:


重写dispatchTouchEvent


override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
   ev?.let {
       if (it.action == MotionEvent.ACTION_DOWN) {
           //如果现在取得焦点的View为EditText则进入判断
           currentFocus?.let { view ->
               if (view is EditText) {
                   if (!isInSide(view, ev) && isSoftInPutDisplayed()) {
                       hideSoftInPut(view)
                  }
              }
          }
      }
  }
   return super.dispatchTouchEvent(ev)
}

在Activity 中重写dispatchTouchEvent,对ACTION_DOWN事件做处理,使用getCurrentFocus()方法拿到当前获取焦点的View,判断其是否为EditText,若为EditText,则看当前软键盘是否展示(isSoftInPutDisplayed)并且点击坐标是否在EditText的外部区域(isInSide),满足条件则隐藏软键盘(hideSoftInPut)。


判断点击坐标是否在EditText内部


//判断点击坐标是否在EditText内部
private fun isInSide(currentFocus: View, ev: MotionEvent): Boolean {
   val location = intArrayOf(0, 0)
//获取当前EditText坐标
   currentFocus.getLocationInWindow(location)
//上下左右
   val left = location[0]
   val top = location[1]
   val right = left + currentFocus.width
   val bottom = top + currentFocus.height
//点击坐标是否在其内部
   return (ev.x >= left && ev.x <= right && ev.y > top && ev.y < bottom)
}

定义一个数组location存储当前EditText坐标,计算出其边界,再用点击坐标(ev.x,ev.y)和边界做比较最终得出点击坐标是否在其内部。


来判断软键盘是否展示


private fun isSoftInPutDisplayed(): Boolean {
   return ViewCompat.getRootWindowInsets(window.decorView)
       ?.isVisible(WindowInsetsCompat.Type.ime()) ?: false
}

使用
WindowInsetsCompat类来判断当前状态下软键盘是否展示,WindowInsetsCompat是AndroidX库中的一个类,用于处理窗口插入(WindowInsets)的辅助类,可用于帮助开发者处理设备的系统UI变化,如状态栏、导航栏、软键盘等,给ViewCompat.getRootWindowInsets传入decorView拿到其实例,利用isVisible方法判断软键盘(WindowInsetsCompat.Type.ime())是否显示。


隐藏软键盘


private fun hideSoftInPut(currentFocus: View) {
   currentFocus.let {
    //清除焦点
       it.clearFocus()
    //关闭软键盘
       val imm = getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
       imm.hideSoftInputFromWindow(it.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
  }
}

首先要清除当前EditText的焦点,防止出现键盘收起但是焦点还在的情况:



最后是获取系统Service隐藏当前的键盘。


来看看最终的效果吧:



3.结尾


以上就是关于点击EditText外部区域隐藏软键盘并且清除焦点的实现方法,当然这只是其中的一种方式,如有不足请在评论区或私信指出,如果你们有更多的实现方法也欢迎在论区或私信留言捏❤️❤️


作者:Otaku_尻男
来源:juejin.cn/post/7226248402798936119
收起阅读 »

Android大图预览

前言 加载高清大图时,往往会有不能缩放和分段加载的需求出现。本文将就BitmapRegionDecoder和subsampling-scale-image-view的使用总结一下Bitmap的分区域解码。 定义 假设现在有一张这样的图片,尺寸为3040 × ...
继续阅读 »

前言


加载高清大图时,往往会有不能缩放分段加载的需求出现。本文将就BitmapRegionDecodersubsampling-scale-image-view的使用总结一下Bitmap的分区域解码


定义


image.png


假设现在有一张这样的图片,尺寸为3040 × 1280。如果按需要完全显示在ImageView上的话就必须进行压缩处理。当需求要求是不能缩放的时候,就需要进行分段查看了。由于像这种尺寸大小的图片在加载到内存后容易造成OOM,所以需要进行区域解码


图中红框的部分就是需要区域解码的部分,即每次只有进行红框区域大小的解码,在需要看其余部分时可以通过如拖动等手势来移动红框区域,达到查看全图的目的。


BitmapRegionDecoder


Android原生提供了BitmapRegionDecoder用于实现Bitmap的区域解码,简单使用的api如下:


// 根据图片文件的InputStream创建BitmapRegionDecoder
val decoder = BitmapRegionDecoder.newInstance(inputStream, false)

val option: BitmapFactory.Options = BitmapFactory.Options()
val rect: Rect = Rect(0, 0, 100, 100)

// rect制定的区域即为需要区域解码的区域
decoder.decodeRegion(rect, option)


  • 通过BitmapRegionDecoder.newInstance可以根据图片文件的InputStream对象创建一个BitmapRegionDecoder对象。

  • decodeRegion方法传入一个Rect和一个BitmapFactory.Options,前者用于规定解码区域,后者用于配置Bitmap,如inSampleSize、inPreferredConfig等。ps:解码区域必须在图片宽高范围内,否则会出现崩溃。


区域解码与全图解码


通过区域解码得到的Bitmap,宽高和占用内存只是指定区域的图像所需要的大小


譬如按1080 × 1037区域大小加载,可以查看Bitmap的allocationByteCount为4479840,即1080 * 1037 * 4


image.png


若直接按全图解码,allocationByteCount为15564800,即3040 * 1280 * 4
image.png


可以看到,区域解码的好处是图像不会完整的被加载到内存中,而是按需加载了。


自定义一个图片查看的View


由于BitmapRegionDecoder只是实现区域解码,如果改变这个区域还是需要开发者通过具体交互实现。这里用触摸事件简单实现了一个自定义View。由于只是简单依赖触摸事件,滑动的灵敏度还是偏高,实际开发可以实现一些自定义的拖拽工具来进行辅助。代码比较简单,可参考注释。


class RegionImageView @JvmOverloads constructor(
context: Context,
attr: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attr, defStyleAttr) {

private var decoder: BitmapRegionDecoder? = null
private val option: BitmapFactory.Options = BitmapFactory.Options()
private val rect: Rect = Rect()

private var lastX: Float = -1f
private var lastY: Float = -1f

fun setImage(fileName: String) {
val inputStream = context.assets.open(fileName)
try {
this.decoder = BitmapRegionDecoder.newInstance(inputStream, false)

// 触发onMeasure,用于更新Rect的初始值
requestLayout()
} catch (e: Exception) {
e.printStackTrace()
} finally {
inputStream.close()
}
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
this.decoder ?: return false
this.lastX = event.x
this.lastY = event.y
return true
}
MotionEvent.ACTION_MOVE -> {
val decoder = this.decoder ?: return false
val dx = event.x - this.lastX
val dy = event.y - this.lastY

// 每次MOVE事件根据前后差值对Rect进行更新,需要注意不能超过图片的实际宽高
if (decoder.width > width) {
this.rect.offset(-dx.toInt(), 0)
if (this.rect.right > decoder.width) {
this.rect.right = decoder.width
this.rect.left = decoder.width - width
} else if (this.rect.left < 0) {
this.rect.right = width
this.rect.left = 0
}
invalidate()
}
if (decoder.height > height) {
this.rect.offset(0, -dy.toInt())
if (this.rect.bottom > decoder.height) {
this.rect.bottom = decoder.height
this.rect.top = decoder.height - height
} else if (this.rect.top < 0) {
this.rect.bottom = height
this.rect.top = 0
}
invalidate()
}
}
MotionEvent.ACTION_UP -> {
this.lastX = -1f
this.lastY = -1f
}
else -> {

}
}

return super.onTouchEvent(event)
}

// 测量后默认第一次加载的区域是从0开始到控件的宽高大小
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)

val w = MeasureSpec.getSize(widthMeasureSpec)
val h = MeasureSpec.getSize(heightMeasureSpec)

this.rect.left = 0
this.rect.top = 0
this.rect.right = w
this.rect.bottom = h
}

// 每次绘制时,通过BitmapRegionDecoder解码出当前区域的Bitmap
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.let {
val bitmap = this.decoder?.decodeRegion(rect, option) ?: return
it.drawBitmap(bitmap, 0f, 0f, null)
}
}
}

SubsamplingScaleImageView


davemorrissey/subsampling-scale-image-view可以用于加载超大尺寸的图片,避免大内存导致的OOM,内部依赖的也是BitmapRegionDecoder。好处是SubsamplingScaleImageView已经帮我们实现了相关的手势如拖动、缩放,内部还实现了二次采样和区块显示的逻辑。


如果需要加载assets目录下的图片,可以这样调用


subsamplingScaleImageView.setImage(ImageSource.asset("sample1.jpeg"))

public final class ImageSource {

static final String FILE_SCHEME = "file:///";
static final String ASSET_SCHEME = "file:///android_asset/";

private final Uri uri;
private final Bitmap bitmap;
private final Integer resource;
private boolean tile;
private int sWidth;
private int sHeight;
private Rect sRegion;
private boolean cached;

ImageSource是对图片资源信息的抽象



  • uri、bitmap、resource分别指代图像来源是文件、解析好的Bitmap对象还是resourceId。

  • tile:是否需要分片加载,一般以uri、resource形式加载的都会为true。

  • sWidth、sHeight、sRegion:加载图片的宽高和区域,一般可以指定加载图片的特定区域,而不是全图加载

  • cached:控制重置时,是否需要recycle掉Bitmap


public final void setImage(@NonNull ImageSource imageSource, ImageSource previewSource, ImageViewState state) {
...

if (imageSource.getBitmap() != null && imageSource.getSRegion() != null) {
...
} else if (imageSource.getBitmap() != null) {
...
} else {
sRegion = imageSource.getSRegion();
uri = imageSource.getUri();
if (uri == null && imageSource.getResource() != null) {
uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + getContext().getPackageName() + "/" + imageSource.getResource());
}
if (imageSource.getTile() || sRegion != null) {
// Load the bitmap using tile decoding.
TilesInitTask task = new TilesInitTask(this, getContext(), regionDecoderFactory, uri);
execute(task);
} else {
// Load the bitmap as a single image.
BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);
execute(task);
}
}
}

由于在我们的调用下,tile为true,setImage方法最后会走到一个TilesInitTask当中。是一个AsyncTask。ps:该库中的多线程异步操作都是通过AsyncTask封装的。


// TilesInitTask
@Override
protected int[] doInBackground(Void... params) {
try {
String sourceUri = source.toString();
Context context = contextRef.get();
DecoderFactory<? extends ImageRegionDecoder> decoderFactory = decoderFactoryRef.get();
SubsamplingScaleImageView view = viewRef.get();
if (context != null && decoderFactory != null && view != null) {
view.debug("TilesInitTask.doInBackground");
decoder = decoderFactory.make();
Point dimensions = decoder.init(context, source);
int sWidth = dimensions.x;
int sHeight = dimensions.y;
int exifOrientation = view.getExifOrientation(context, sourceUri);
if (view.sRegion != null) {
view.sRegion.left = Math.max(0, view.sRegion.left);
view.sRegion.top = Math.max(0, view.sRegion.top);
view.sRegion.right = Math.min(sWidth, view.sRegion.right);
view.sRegion.bottom = Math.min(sHeight, view.sRegion.bottom);
sWidth = view.sRegion.width();
sHeight = view.sRegion.height();
}
return new int[] { sWidth, sHeight, exifOrientation };
}
} catch (Exception e) {
Log.e(TAG, "Failed to initialise bitmap decoder", e);
this.exception = e;
}
return null;
}

@Override
protected void onPostExecute(int[] xyo) {
final SubsamplingScaleImageView view = viewRef.get();
if (view != null) {
if (decoder != null && xyo != null && xyo.length == 3) {
view.onTilesInited(decoder, xyo[0], xyo[1], xyo[2]);
} else if (exception != null && view.onImageEventListener != null) {
view.onImageEventListener.onImageLoadError(exception);
}
}
}

TilesInitTask主要的操作是创建一个SkiaImageRegionDecoder,它主要的作用是封装BitmapRegionDecoder。通过BitmapRegionDecoder获取图片的具体宽高和在Exif中获取图片的方向,便于显示调整。


后续会在onDraw时调用initialiseBaseLayer方法进行图片的加载操作,这里会根据比例计算出采样率来决定是否需要区域解码还是全图解码。值得一提的是,当采样率为1,图片宽高小于Canvas的getMaximumBitmapWidth()getMaximumBitmapHeight()时,也是会直接进行全图解码的。这里调用的TileLoadTask就是使用BitmapRegionDecoder进行解码的操作。


ps:Tile对象为区域的抽象类型,内部会包含指定区域的Bitmap,在onDraw时会根据区域通过Matrix绘制到Canvas上。


private synchronized void initialiseBaseLayer(@NonNull Point maxTileDimensions) {
debug("initialiseBaseLayer maxTileDimensions=%dx%d", maxTileDimensions.x, maxTileDimensions.y);

satTemp = new ScaleAndTranslate(0f, new PointF(0, 0));
fitToBounds(true, satTemp);

// Load double resolution - next level will be split into four tiles and at the center all four are required,
// so don't bother with tiling until the next level 16 tiles are needed.
fullImageSampleSize = calculateInSampleSize(satTemp.scale);
if (fullImageSampleSize > 1) {
fullImageSampleSize /= 2;
}

if (fullImageSampleSize == 1 && sRegion == null && sWidth() < maxTileDimensions.x && sHeight() < maxTileDimensions.y) {
// Whole image is required at native resolution, and is smaller than the canvas max bitmap size.
// Use BitmapDecoder for better image support.
decoder.recycle();
decoder = null;
BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);
execute(task);
} else {
initialiseTileMap(maxTileDimensions);

List<Tile> baseGrid = tileMap.get(fullImageSampleSize);
for (Tile baseTile : baseGrid) {
TileLoadTask task = new TileLoadTask(this, decoder, baseTile);
execute(task);
}
refreshRequiredTiles(true);

}

}

加载网络图片


BitmapRegionDecoder只能加载本地图片,而如果需要加载网络图片,可以结合Glide使用,以SubsamplingScaleImageView为例


Glide.with(this)
.asFile()
.load("")
.into(object : CustomTarget<File?>() {
override fun onResourceReady(resource: File, transition: Transition<in File?>?) {
subsamplingScaleImageView.setImage(ImageSource.uri(Uri.fromFile(resource)))
}

override fun onLoadCleared(placeholder: Drawable?) {}
})

可以通过CustomTarget获取到图片的File文件,然后再调用SubsamplingScaleImageView#setImage


最后


本文主要总结Bitmap的分区域解码,利用原生的BitmapRegionDecoder可实现区域解码,通过SubsamplingScaleImageView可以对BitmapRegionDecoder进行进一步的交互扩展和优化。如果需要是TV端开发可以参考这篇文章,里面有结合具体的TV端操作适配:Android实现TV端大图浏览


参考文章:



Android实现TV端大图浏览


tddrv.cn/a/233555


Android 超大图长图浏览库 SubsamplingScaleImageView 源码解析



作者:Cy13er
来源:juejin.cn/post/7224311569778229304
收起阅读 »

Android动态权限申请从未如此简单

前言 注:只想看实现的朋友们可以直接跳到最后面的最终实现 大家是否还在为动态权限申请感到苦恼呢?传统的动态权限申请需要在Activity中重写onRequestPermissionsResult方法来接收用户权限授予的结果。试想一下,你需要在一个子模块中申请权...
继续阅读 »

前言


注:只想看实现的朋友们可以直接跳到最后面的最终实现


大家是否还在为动态权限申请感到苦恼呢?传统的动态权限申请需要在Activity中重写onRequestPermissionsResult方法来接收用户权限授予的结果。试想一下,你需要在一个子模块中申请权限,那得从这个模块所在的ActivityonRequestPermissionsResult中将结果一层层再传回到这个模块中,相当的麻烦,代码也相当冗余和不干净,逼死强迫症。


使用


为了解决这个痛点,我封装出了两个方法,用于随时随地快速的动态申请权限,我们先来看看我们的封装方法是如何调用的:


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)
}

总结


其实很多实用技巧本质上都是很简单的,但没有接触过就很难想到,我将我的开发经验分享给大家

作者:dreamgyf
来源:juejin.cn/post/7225516176171188285
,希望能帮助到大家。

收起阅读 »

怎么实现微信扫码登录

web
最近在给企业健康管理系统做一个微信扫码登录的功能,借此机会总结下微信登录这个技术点。 网站应用微信登录是基于 OAuth2.0 协议标准构建的。OAuth 协议规范了五种授权模式,Authorization Code、PKCE、Client CreDentia...
继续阅读 »

最近在给企业健康管理系统做一个微信扫码登录的功能,借此机会总结下微信登录这个技术点。


网站应用微信登录是基于 OAuth2.0 协议标准构建的。OAuth 协议规范了五种授权模式,Authorization Code、PKCE、Client CreDentials、Device Code 和 Refresh Token。微信目前只支持 authorization_code 模式。


微信网站应用接入基础知识


第一步需要先到微信开放平台注册一个开发者账号,并创建一个微信登录网站应用,然后获得AppIDAppSecret


微信的authorization_code模式:



  1. 发起微信授权登录请求



// 请求格式
https://open.weixin.qq.com/connect/qrconnect?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=SCOPE&state=STATE#wechat_redirect



  1. 用户扫码授权之后,微信会重定向到回调地址并且给予一个临时票据code;

  2. 后端拿codeAppIDAppSecret通过 API 换取access_token;

  3. 通过access_token进行接口调用,换取用户基本信息

  4. 根据用户信息中的 openId 查询是否已经和系统用户绑定


流程图:


微信官方


关于 state 参数:


state参数: state 参数会在用户授权成功后和code一起携带给 redirect URL。主要用来做 CSRF 防范。



redirect_url?code=CODE&state=STATE

关于 code:
Code 的超时时间为 10 分钟,一个 code 只能成功换取一个 access_token 即失效。


关于展示形式


微信登录有两种展示形式,一种是弹窗打开登录二维码,另一种是将二维码嵌套在自己网页内


我们的设计


交互流程


login_flow.png


前端工作流程:


1、二维码展示,请求/wechat/qrcode 地址获取二维码地址,返回的参数有 state 字段(重要)。


Note:后端生成一个 uuid state,并存储在 Redis 中用来检测用户的扫码状态


2、在当前二维码页面轮询/wechat/qrcode/status/{state} 接口,判断是否已授权、未绑定、已绑定三种状态。


1)未授权,继续轮询

2)已授权已绑定,根据/wechat/qrcode/status/{state} 接口返回的租户、登录凭证,调用登录接口/login/wechat。

3)已授权未绑定,进去用户绑定微信流程

> 用户绑定微信流程
请求/wechat/bind/sms/send 接口,发送用户绑定微信的验证码
请求/wechat/bind 接口,绑定用户。
根据/wechat/bind 接口返回的租户、凭证信息,调用登录接口/login/wechat。

参考文档


网站应用微信登录开发指南


博客原文


iloveu.xyz/2023/04/25/…


作者:贾克森
来源:juejin.cn/post/7225867003720974373
收起阅读 »

微信小程序背景音频开发

web
最近又新开发了一款听书类的小程序,现在一阶段已基本完工。代码已开源,链接在文章结尾。欢迎star。 本期给大家讲解一下关于背景音频开发的一些基本业务场景和踩坑。 1.需求拆解 先来看一张图: 从图中可以看到,基本的业务包含以下几个部分 播放 暂停 切换上一...
继续阅读 »

最近又新开发了一款听书类的小程序,现在一阶段已基本完工。代码已开源,链接在文章结尾。欢迎star。


本期给大家讲解一下关于背景音频开发的一些基本业务场景和踩坑。


1.需求拆解


先来看一张图:


image.png
从图中可以看到,基本的业务包含以下几个部分



  1. 播放

  2. 暂停

  3. 切换上一个音频

  4. 切换下一个音频

  5. 拖动进度条改变音频进度

  6. 音频进度时间

  7. 音频总时间

  8. 在音频列表切换任意的音频


还有一个需求就是在小程序退出以后,还会播放:


image.png
在上图里看到,当背景音频播放的时候,会出现上方这个小图标:


image.png
同样的,在手机的通知栏里面,长这样子:
image.png


接下来我们动手实现一下整个功能。


2. 技术分析


要想实现背景音频,我们需要使用微信小程序提供的一个API:getBackgroundAudioManager(),因为我这里使用的UNI开发的,所以直接贴的是UNI的文档了。具体方法参数可以查看文档。


这里注意以下几个点:



  • ios App平台,背景播放需在manifest.json -> app-plus -> distribute -> ios 节点添加 "UIBackgroundModes":["audio"] 才能保证音乐可以后台播放(打包成ipa生效)

  • 小程序平台,需在manifest.json 对应的小程序节点下,填写"requiredBackgroundModes": ["audio"]。发布小程序时平台会审核。

  • 在page.json中添加"UIBackgroundModes":["audio"]

  • 配置完以后,重新编译一下项目。


3. 功能实现


3.1 播放slider


1.获取音频数据


在进入播放音频页面的时候,默认获取一下第一个需要播放的音频。我这里是是根据音频的id去获取音频的详情信息:


/**
* @description: 获取专辑声音详情信息
* @returns {*}
*/

const getTrackInfo = async (trackId: number) => {
try {
const res = await albumsService.getTrackInfo(trackId)
trackInfo.value = res.data
audios.trackId = res.data?.id as number;
createBgAudioManager()
} catch (error) {
console.log(error)
}
}
onLoad((options) => {
const { trackId } = options
audios.trackId = trackId
getTrackInfo(trackId)
})

getTrackInfo返回的的数据里面长这样的:


image.png
播放音频需要设置红色框标识的字段。


2.创建音频


请求成功,拿到音频详情数据,就需要创建背景音频。


// 初始化背景音频控件
const bgAudioManager = uni.getBackgroundAudioManager();

/**
* @description: 修改音频地址
* @returns {*}
*/

const createBgAudioManager = () => {
// 音频测试地址
// innerAudioContext.src = 'https://bjetxgzv.cdn.bspapp.com/VKCEYUGU-hello-uniapp/2cc220e0-c27a-11ea-9dfb-6da8e309e0d8.mp3';
if (bgAudioManager) {

// 若原先的音频未暂停,则先暂停
if (!bgAudioManager.paused) {
// stop和pause是不一样的,stop直接停止播放,然后从头开始
bgAudioManager.stop();
}
bgAudioManager.title = trackInfo.value?.trackTitle;
bgAudioManager.coverImgUrl = trackInfo.value?.coverUrl
// 设置了src以后会自动播放
bgAudioManager.src = trackInfo.value?.mediaUrl;
// bgAudioManager.autoplay = true;
initAudio(bgAudioManager)
}
}


这里注意一下,title是必须要设置的。不然音频不播放。当设置了src以后,音频会自动进行播放,无需设置autoPlay。



3.获取播放进度和音频总长度


在上面的函数里面,有个initAudio函数。


/**
* @description: 初始化音频相关的方法
* @param {*} ctx
* @returns {*}
*/

const initAudio = (ctx: any) => {
ctx.onTimeUpdate((e) => {
// 当拖动进度条的时候不需要更新进度,使用seek方法
if(!sliders.isDraging) {
// 获取当前进度
const currentTime:number = ctx.currentTime
// 跟新音频进度和slider进度
if (currentTime) {
sliders.progressTime = ctx.currentTime
audios.currentTime = formatTime(currentTime);
}
}
})
ctx.onCanplay(() => {
setTimeout(() => {
console.log('音频长度', bgAudioManager.duration);
// 音频长度,时分秒格式
const duration = bgAudioManager.duration
audios.duration = formatTime(duration);
// 进度条长度=音频长度
sliders.max = duration
}, 300)
})
ctx.onPlay(() => {
audios.playStatus = true
})
ctx.onPause(() => {
audios.playStatus = false
})
ctx.onEnded((e) => {
// 播放结束自动切换到下一首歌
nextAudio()
})
}

onCanplay中,我们可以获取音频的总长度,注意这里的setTimeout必须加,不然获取不到duration。这里需要把秒格式和时分秒的格式都保存下来。


onTimeUpdate中,我们可以获取到当前音频播放的进度。然后实时更新进度条。


接下来就是进度条的实现了。


这里我用的是uni内置组件中的slider组件。


<slider
step="1"
activeColor="#f86442"
block-color="#fff"
block-size="10"
:min="0"
:max="sliders.max"
:value="sliders.progressTime"
@change="sliderChange"
@touchstart="handleSliderMoveStart "
@touchend="handleSliderMoveEnd"
/>


max必须设置,也就是音频的总长度。
value值是当前音频播放的进度。


4.拖动进度条


当拖动进度条的时候,触发sliderChange事件,改变音频的进度。



/**
* @description: 进度条改变事件
* @returns {*}
*/

const sliderChange = (e) => {
console.log(e);
// 拖动slider的值
const position = e.detail.value
seekAudio(position)
}
/**
* 音频跳转
*/

const seekAudio = (position: number) => {
bgAudioManager.seek(position)
// 修改当前进度
audios.currentTime = formatTime(position)
sliders.progressTime = position
}

这里通过seek方法来设置进度的跳转。


当拖动进度条的时候,在onTimeUpdate中也在修改进度。两个之间会打架。所以这里我们在onTimeUpdate中使用isDraging字段来控制。当鼠标按下和抬起的时候来控制isDraging的值,不让onTimeUpdate修改进度。


/**
* @description: 开始拖动进度条事件
* @returns {*}
*/

const handleSliderMoveStart = () => {
sliders.isDraging = true
}
/**
* @description: 结束拖动进度条时间
* @returns {*}
*/

const handleSliderMoveEnd = () => {
sliders.isDraging = false
}

// 此逻辑在前面的代码有了
ctx.onTimeUpdate((e) => {
// 当拖动进度条的时候不需要更新进度,使用seek方法
if(!sliders.isDraging) {
...
}
})

3.2 播放暂停


播放和暂停就非常简单了。


通过playStatus字段来控制播放和暂停按钮的样式切换即可。


其次是事件:


/**
* @description: 暂停音频
* @returns {*}
*/

const pauseAudio = () => {
bgAudioManager.pause() // 停止
}

/**
* @description: 播放音频
* @returns {*}
*/

const playAudio = () => {
bgAudioManager.play() // 播放
}

// 在钩子函数监听

ctx.onPlay(() => {
audios.playStatus = true
})
ctx.onPause(() => {
audios.playStatus = false
})

3.3 音频列表渲染和切换


image.png


这个列表怎么渲染的我就不讲了。这里还有个下拉刷新和上拉加载更多的功能。


当点击某个音频,获取对应的id,然后请求接口获取对应的音频详情。接口和流程跟之前的一样。唯一注意的是,当我们点击的是正在播放的一个音频的话,啥也不要做。还有一个注意的点,当切换音频的时候需要先暂停,然后再设置src和别的属性。



const createBgAudioManager = () => {

if (bgAudioManager) {

// 若原先的音频未暂停,则先暂停
if (!bgAudioManager.paused) {
// stop和pause是不一样的,stop直接停止播放,然后从头开始
bgAudioManager.stop();
}
bgAudioManager.title = trackInfo.value?.trackTitle;
bgAudioManager.coverImgUrl = trackInfo.value?.coverUrl
// 设置了src以后会自动播放
bgAudioManager.src = trackInfo.value?.mediaUrl;
// bgAudioManager.autoplay = true;
initAudio(bgAudioManager)
}
}

注意这里的暂停不是pause,是stop。


3.4 上一个下一个切换


当切换上一个和下一个音频的时候,逻辑也是需要票拿到对应的id,然后请求音频详细数据。


/**
* @description: 切换上一首音频
* @returns {*}
*/

const prevAudio = () => {
// 判断是不是第一首,是则提示
const firstId = audioList.value[0]?.trackId
if (firstId === audios.trackId) {
uni.showToast({
title : "当前已经是第一首了",
icon : "none"
})
return;
}
// 获取上一首的id
// 从播放列表寻找
let id = 0;
audioList.value.forEach((item, index) => {
if (item.trackId === audios.trackId) {
id = audioList.value[index - 1]?.trackId
}
})

getTrackInfo(id)
}
/**
* @description: 切换下一首音频
* @returns {*}
*/

const nextAudio = () => {
// 判断是不是最后一首。是则提示
const len = audioList.value.length
const lastId = audioList.value[len - 1]?.trackId
if (lastId === audios.trackId) {
uni.showToast({
title : "当前播放列表已是最新的了,请加载更多",
icon : "none"
})
return;
}
// 获取下一首的id
// 从播放列表寻找
let id = 0;
audioList.value.forEach((item, index) => {
if (item.trackId === audios.trackId) {
id = audioList.value[index + 1]?.trackId
}
})
getTrackInfo(id)
}

这里只需要注意的是,如果是第一个和最后一个音频,需要做特殊处理。


3.5 播放结束


最后,当某个音频播放结束的时候,直接请求nextAudio函数即可。


ctx.onEnded((e) => {
// 播放结束自动切换到下一首歌
nextAudio()
})

到此为止,我想要的功能基本全部实现了。


4. 更多功能



  • 实时上报播放进度

  • 音频地址防盗

  • 付费,免费体验功能


5. 代码地址


完整代码地址参考:gitee.com/xiumubai/li…


作者:白哥学前端
来源:juejin.cn/post/7226228585371041848
收起阅读 »

不就new个对象的事,为什么要把简单的问题复杂化?为什么要使用Hilt依赖注入?看完就知道!

为什么要使用Hilt依赖注入 之前有写过一些文章 Hilt与Koin的对比,和 Android开发中Hilt的常用场景。看起来是非常简单与实用了,但是会引起一些同学的疑问? 为什么要使用依赖注入?直接new对象不香吗?为什么要把简单的问题复杂化? 你是不是在炫...
继续阅读 »

为什么要使用Hilt依赖注入


之前有写过一些文章 Hilt与Koin的对比,和 Android开发中Hilt的常用场景。看起来是非常简单与实用了,但是会引起一些同学的疑问?


为什么要使用依赖注入?直接new对象不香吗?为什么要把简单的问题复杂化?


你是不是在炫技,是不是像装13?


这还真不是,如果说我使用的Dagger2,还真是炫技,NB啊。Dagger的坑也是真的多,能在大项目中把Dagger用好,那还真是牛,但是我们现在都用Hilt了有什么可装的,使用是真的简单,都是一些场景化的东西,一些固定用法。没必要没必要。


回归正题,为什么要使用依赖注入?哪种情况下推荐使用依赖注入?就算要用依赖注入,为什么依赖注入推荐使用Hilt?


一、自动管理(灵活与解耦)


首先不是说大家写项目就一定要使用依赖注入,如果大家的项目不是大项目,总共就5、6个,10多个页面,你没必要上依赖注入框架,如果是大项目,分模块,分组件,多人协同开发的,并且可能依赖的对象很复杂,或者说套娃似的对象依赖,那么使用Hilt就非常方便。不同模块/组件的开发人员直接在他自己的组件/模块下定义好对象的提供方式,另一边直接用即可,无需关系依赖的复杂度,和实现的逻辑。


我们先看看一些复杂的嵌套依赖,比如我们来一个三层套娃的依赖:


@Singleton
class UserServer @Inject constructor(private val userDao: UserDao) {

fun testUser() {
YYLogUtils.w(userDao.printUser())
toast(userDao.printUser())
}

fun getDaoContent():String{
return userDao.printUser()
}

}

@Singleton
class UserDao @Inject constructor(private val user: UserBean) {

fun printUser(): String {
return user.toString()
}

}

data class UserBean(
val name: String,
val age: Int,
val gender: Int,
val languages: List<String>
)

其他三个类都是必须了,其实也就多了一个这个类,提供UserBean对象


@Module
@InstallIn(SingletonComponent::class)
class Demo10DIModule {

@Singleton
@Provides
fun provideUser(): UserBean {
return UserBean("newki", 18, 1, listOf("中文", "英文"))
}

}

使用:


@AndroidEntryPoint
class Demo10DIActivity : BaseVMActivity() {

@Inject
lateinit var userServer: UserServer

override fun getLayoutIdRes(): Int = R.layout.activity_demo10_di

override fun init() {
findViewById<Button>(R.id.btn_01).click {
YYLogUtils.w(userServer.toString())
userServer.testUser()
}
}

如果不使用Hilt,自己new对象也能实现


class Demo10DIActivity : BaseVMActivity() {

lateinit var userServer: UserServer

override fun getLayoutIdRes(): Int = R.layout.activity_demo10_di

override fun init() {
//自己new三个对象
userServer = UserServer(UserDao(UserBean("newki", 18, 1, listOf("中文", "英文"))))

findViewById<Button>(R.id.btn_01).click {
YYLogUtils.w(userServer.toString())
userServer.testUser()
}
}

这样new出来的对象,且不说生命周期是跟随页面的,无法保持单例,我们就说如果需求变了,UseDao中需要UserBean和UserProfile2个对象了,如果你是new对象,那么就要到处修改,如果是Hilt的方式,我们就只需要修改UserDao对象的构造即可


@Singleton
class UserDao @Inject constructor(private val user: UserBean,private val profile:UserProfile) {

fun printUser(): String {
return user.toString()
}

}

以上只是举个简单例子,构建一个对象,还要构建一堆其他的对象,并且其他对象的构建同样复杂,并且必须按顺序构建,而且需要的对象的生命周期都不一样,有些生命周期可能和Activity一样,有些可能是单例,所以在构建的时候还要考虑对象声明周期,考虑对象的来源。


特别是在大型项目中很痛苦,因为项目不是一个人写的,大家协同合作开发,看别人的代码也和看天书一样,并不知道同事的对象是如何创建的,如果一个对象的构建方式发生改变,会影响整个的构建过程以及所关联的代码,牵一发而动全身。


这个时候依赖注入框架就派上用场了,我们只用专注于怎么实现功能,对象的依赖关系和生命周期,都让它来帮我们管理,一个Inject,它会按照依赖关系帮我们注入我们需要的对象,并且它会管理好每个对象的生命周期,在生命周期还没结束的情况下是不会重复new的。


所以Hilt依赖注入非常适合大项目,小项目开发者因为项目复杂度低,没遇到这些问题,所以不会理解为什么要用Hilt依赖注入,发出疑问,为什么要让简单的new对象搞这么复杂。


二、生命周期控制


这里说对象的生命周期,其实就是在一定作用域的生命周期,如果只是说单例有点太浅薄,可以说是是在一定范围内的单例。


我们直接new对象是无法控制生命周期的,除非我们使用全局单例的对象,而通过Hilt依赖注入我们可以很方便的实现对象的生命周期的控制。


比如我们普通对象的快速注入方式,直接注解Singleton就标注的是全局范围单例


@Singleton
class UserServer @Inject constructor(private val userDao: UserDao) {

fun testUser() {
YYLogUtils.w(userDao.printUser())
toast(userDao.printUser())
}

fun getDaoContent():String{
return userDao.printUser()
}

}

另一种用法是我们使用Module的方式定义依赖注入,那么使用SingletonComponent + Singleton 同样是全局范围单例的意思


@Module
@InstallIn(SingletonComponent::class)
class Demo10DIModule {

@Singleton
@Provides
fun provideUser(): UserBean {
return UserBean("newki", 18, 1, listOf("中文", "英文"))
}

}

如果我们想Activity内的单例,我们使用ActivityComponent + ActivityScoped 就是Activity范围的单例。


@Module
@InstallIn(ActivityComponent::class)
class Demo10DIModule {

@ActivityScoped
@Provides
fun provideUser(): UserBean {
return UserBean("newki", 18, 1, listOf("中文", "英文"))
}

}

以上两种都是比较常用的作用域,其他的我们还能保证Fragment内单例,View内部单例等等,用法都是同样的用法。


所以对依赖对象的生命周期控制也是Hilt很方便的一个特点,使用new对象是无法做到这一点的。


三、对比其他依赖注入


目前Android主流的也就是三种依赖注入 Dagger2 Hilt Koin。


之前比较过Koin,只能在Kotlin语言环境中使用。并且性能并不会好过Hilt。错误提示也不友好。


Dagger2不是不能用,17年18年的时候特别火,但是学习成本很高,每次创建UI个依赖注入类还得mackproject,并且错误的提示也不友好,



其实我17年就已经在使用Dagger2了,然后自己做了Demo与框架封装,后来做项目中并没有使用,一个是坑太多,一个是同事不会用,学习成本太高。也就放弃使用Dagger2了。


而Hilt其实就是Daggert2的Android场景化的实现,内部对Dagger2进行了封装,在Android开发中使用Hilt更加的简便,学习成本很低,错误提示友好。并且还对ViewModel都可以注入了哦。所以说它是专为Android开发而生。


关于注入的对象内存占用是否有优化的这一点,其实并没有文章或者官方的文档指出有内存优化这一点,仅我自己的测试来说,整个页面如果有多个注入对象和直接new对象相比,感觉注入的对象占用内存稍微少一点,不知道是不是测试的波动,我不理解,如有懂的大佬还望指点一下。


总结


总结为什么要使用Hilt。



  1. 偷懒;自动管理,多对象的自动注入,万一有修改不需要到尸山中到处趴。

  2. 单例;让对象拥有生命周期,无需我们自己手动单例创建,然后去手动注销。

  3. 解耦;不需要到处引入我一些不需要的对象,特别是组件化的项目,另一个组件只管注入,在我的组件中我只管引用。


我觉得这是我使用Hilt最吸引我的三个点,


所以说目前2022年了,依赖注入我推荐Hilt。关键使用简单,在Android的常用场景下我还做了一些 Demo, 总共就那么多固定的一些用法,之前我写过Demo覆盖Android开发大部分的使用场景, 有需要直接拿走即可,可以查看我之前的文章。


顺便说一句,这是国外程序员的必备面试技能,感觉相比国内的开发者老外的Android开发者特别喜欢使用Dagger2和Hilt,不少老项目都是用Dagger2的。


好了,本期内容如讲的不到位或错漏的地方,希望同学们可以指出交流。


如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。


Ok,这一期就此完结。



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

理解Kotlin中的reified关键字

标题:理解Kotlin中的reified关键字 摘要:本文介绍了Kotlin中的reified关键字的用途,特点以及如何在实际项目中应用。我们将通过实例详细了解reified的功能以及如何在内联函数中使用它。 正文: 什么是reified关键字? 在Kotli...
继续阅读 »

标题:理解Kotlin中的reified关键字


摘要:本文介绍了Kotlin中的reified关键字的用途,特点以及如何在实际项目中应用。我们将通过实例详细了解reified的功能以及如何在内联函数中使用它。


正文:


什么是reified关键字?


在Kotlin中,reified是一个特殊的关键字,用于修饰内联函数中的类型参数。这使得在函数内部可以访问类型参数的具体类型。通常情况下,由于类型擦除(type erasure),在运行时是无法直接获取泛型类型参数的具体类型的。reified关键字解决了这个问题。


使用reified关键字的条件


要使用reified关键字,需要遵循以下几点:



  1. 函数必须是内联的(使用inline关键字修饰)。

  2. 类型参数前需要加上reified关键字。


示例:reified关键字的用法


下面是一个使用reified关键字的简单示例:


inline fun <reified T> checkType(value: Any) {
if (value is T) {
println("Value is of type T.")
} else {
println("Value is NOT of type T.")
}
}

fun main() {
val stringValue = "Hello, Kotlin!"
val intValue = 42

checkType<String>(stringValue) // 输出 "Value is of type T."
checkType<String>(intValue) // 输出 "Value is NOT of type T."
}

在这个示例中,我们定义了一个内联函数checkType,它接受一个reified类型参数T。然后,我们使用is关键字检查传入的value变量是否为类型T。在main函数中,我们用不同的类型参数调用checkType函数来验证它的功能。


获取类型参数的Java类


当你使用reified关键字修饰一个内联函数的类型参数时,你可以通过T::class.java获取类型参数对应的Java类。这在需要访问泛型类型参数的具体类型时非常有用,比如在反射操作中。


下面是一个简单的例子:


import kotlin.reflect.KClass

inline fun <reified T : Any> getClass(): KClass<T> {
return T::class
}

inline fun <reified T : Any> getJavaClass(): Class<T> {
return T::class.java
}

fun main() {
val stringKClass = getClass<String>()
println("KClass for String: $stringKClass") // 输出 "KClass for String: class kotlin.String"

val stringJavaClass = getJavaClass<String>()
println("Java class for String: $stringJavaClass") // 输出 "Java class for String: class java.lang.String"
}

在这个示例中,我们定义了两个内联函数,getClassgetJavaClass,它们都接受一个reified类型参数TgetClass函数返回类型参数对应的KClass对象,而getJavaClass函数返回类型参数对应的Java类。在main函数中,我们用String类型参数调用这两个函数,并输出结果。


注意事项


需要注意的是,reified关键字不能用于非内联函数,因为它们的类型参数在运行时会被擦除。此外,reified类型参数不能用于普通类和接口,只能用于内联函数。


总结


Kotlin中的reified关键字允许我们在内联函数中访问类型参数的具体类型。它在需要访问泛型类型参数的场景中非常有用,例如在反射操作中。本文通过实例介绍了如何使用reified关键字,并讨论了相关注意事项。希望这些示例能够帮助您更好地理解和应用reified关键字。


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

关于时间管理的一点建议

在成为 Tech Lead 之后我发现时间变得极度不够用,甚至会成为了我焦虑和殚精竭虑的源泉。因为我无法主动的去做我应该(定方向、做定期回顾)做和想做的事情,而总是被动的被他人牵着鼻子走:无穷无尽的决策请求、寻求帮助、会议邀约。 有两个可以解决这个问题的办法可...
继续阅读 »

在成为 Tech Lead 之后我发现时间变得极度不够用,甚至会成为了我焦虑和殚精竭虑的源泉。因为我无法主动的去做我应该(定方向、做定期回顾)做和想做的事情,而总是被动的被他人牵着鼻子走:无穷无尽的决策请求、寻求帮助、会议邀约。


有两个可以解决这个问题的办法可以简单提一嘴:1)首先你应该尽可能的帮助你的团队成长,让他们尽可能的独当一面,你也就自然不会成为单点依赖;2)根据问题的重要程度和紧急程度划分优先级依然奏效,在精力有限的情况下,有些问题可以暂且搁置不用去解决。


今天我想聊的是第三个办法来源于哈佛商业评论的一篇文章:《Management Time: Who's Got the Monkey》


猴子在谁那呢


问题


文章从一个再正常不过的场景开始说起:经理与下属 A 在走廊上相遇了,下属 A 向经理发起了求助:“早上好啊,顺便说一句我现在遇到了一个问题,情况是这样的……”;经理也很自然的回答道:“很高兴你告诉了我,我现在有点忙,先让我想想再答复你”。


同样的事件在一天之内发生了三次,B 和 C 也同时向经理发起了求助。经理忙的焦头烂额,直至下班前 A、B、C 都没有得到经理的答复。他们在经理门外等待答复的过程中难免开始质疑起经理的能力,因为他现在成为了多数人工作的瓶颈。


第二天刚好是周六,经理为了避免下周继续阻塞其他人的工作决定周末去公司加班,在开车经过公司附近的一片高尔夫球场时,你猜他看到了哪三位熟悉的身影?


“到底谁在为谁工作呢?”经理不由得思考起来。


办法


如果我们把问题比喻成猴子的话,从 A 向经理求助的那一刻起,猴子就从 A 的背上跳到了经理的背上,也就是下属的问题成了经理的问题。经理采取了三个手段来解决这个困境。



  • 摆脱猴子


在新一周开始的第一天,经理将 A、B、C 依次喊入办公室进行一对一沟通。经理把“猴子”摆在他们的中间,商讨下一步下属能针对问题能够采取些哪些独立行动。在达成一致的同时约定好下次沟通进展的时间。这样下属便又可以继续工作在问题上,猴子得到归还。



  • 明确主动性


我们必须要回答的另一个问题是,下属究竟是真的遇到了困难,还是他根本就是想甩锅而已。主动性从低到高可以分为5类:



  1. 等着被告诉要做什么(主动性最低)

  2. 主动询问我要做什么

  3. 建议应该做些什么,并且采取相应行动

  4. 直接行动起来,并且给出自己的建议

  5. 独自行动起来,定期进行汇报(主动性最高)


前两类主动性不应该被鼓励,或者直接点说是不被允许的。时间管理的关键在于掌控自己的工作内容和工作时机。(“等着被告诉要做什么”即无法让自己掌控工作内容也无法控制自己的工作时机)。



  • 对待猴子(问题)的原则



  1. 猴子要么被投喂(持续解决)要么被击毙(立即关闭)。等待它自然死亡的过程等无异于无限投入时间却又看不到任何结果,等同于浪费

  2. 猴子数量应该保持在经理的投喂带宽之下。

  3. 猴子只应该在约定的时间被投喂(追着猴子投喂,或者允许被随时邀请投喂意味着经理被问题牵着鼻子走)

  4. 猴子应该通过面对面的方式进行投喂——通过邮件的方式相当于问题又异步的被甩给了经理

  5. 每只猴子的下一次投喂时间应该被明确下来


作为 Tech Lead 当面对求助时,会惯性的将问题包揽下来。无论这种“我来搞定”的态度无论是出自下意识还是顾虑都不是一个好的实践。不妨把问题当作机会,推他们一把,甚至允许一定范围内的犯错,是让团队成长的一个途径


三类时间


文章把经理时间划分为三类:



  • 老板强加给你的时间(Boss-imposed Time)——你必须完成老板交给你的那些任务,否则会得到迅速且直接的惩罚

  • 系统强加给你的时间(System-imposed Time)——你必须响应同级别同事的工作诉求

  • 自我时间(Self-imposed Time)——做那些你的经理和团队同意你去做的事情。这类时间又可以继续划分为两类

    • 下属时间(Subordinate-imposed Time)——用于支援下属的时间

    • 自由支配时间(Discretionary Time)——真正属于经理自己的时间




很明显,对于前两者你能够拒绝的空间十分有限。适当的降低下属占用你的时间,也许是更切实有效为自己争取一点自由的方法。


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

两个故事:送给面试失败的你

故事1:从世界五百强高管手中拿下项目 那年我大二。 在北五环外的宏福校区上满一年课后,我们正式搬回了位于市中心的海淀区本部,终于算是进城了。 进城的第一个好处,就是做项目的机会变多了! 当年为了尽早的实现经济独立,课没怎么好好上,兼职项目和实习倒是干了不少。...
继续阅读 »

故事1:从世界五百强高管手中拿下项目


图片


那年我大二。


在北五环外的宏福校区上满一年课后,我们正式搬回了位于市中心的海淀区本部,终于算是进城了。


进城的第一个好处,就是做项目的机会变多了!


当年为了尽早的实现经济独立,课没怎么好好上,兼职项目和实习倒是干了不少。


北邮有一个著名的BBS,上面啥都有,极其热闹。其中最热门的板块有俩,一个是“缘来如此”,听名字就知道是用来谈情说爱的。另一个则是就业机会交流板块,各种招聘内推资讯满天飞。


我当时天天刷帖子找实习和兼职机会,自然也收到不少面试邀约,而我要聊的这一次,格外的与众不同。


面试约在了晚上8点,北师大东门外的一个安静的咖啡店里,二楼。


我面试从来不迟到,一般都会掐着点到场,可那次我到的时候,面试官已经落座了。是一个四十出头的中年男人,高高瘦瘦,戴银边方框眼镜。桌上摆了一壶明亮的金丝皇菊花茶,两个小巧精致的玻璃茶杯。


我入座后,他客气的给我倒了一杯茶,面试便开始了。说是面试,其实更像是一场闲聊,整个谈话氛围非常轻松自在,咖啡店是文艺范儿的,灯光幽暗,空气里飘荡着舒缓的爵士乐和淡淡的松香,木质的楼板被过往的客人踩出咯咯吱吱的响声。


我们大约是聊了半个多小时的样子,从他的表情可以看得出他挺满意。结束谈话的时候他说,我稳重的样子不太像90后。我羞涩的笑了笑,带着他给我布置的第一个项目走出咖啡店。


没错,这个项目就这么轻而易举的被我拿到手了,也许是出于对我母校的信任,又或许是当晚的聊天氛围实在舒服,总之我获得了他的信任。


可好戏才刚刚开始。


我收到的第一个任务,要求我在一周内根据需求文档写出一份静态网站Demo出来。


这个任务现在看来很easy,但是当时的我,连最基本的网页怎么写都不会,我只用过Dreamweaver拖拖拽拽搞过一个蹩脚且丑陋的页面。而这一次,我面对的是一个商业任务,并且限定了交付日期,一周!这可把我愁坏了。甚至都不是时间紧不紧的问题,是我根本不知道从哪里下手啊。


事情的突破口出现在我大学舍友身上。


这小子是个富二代,家境优渥,从小就接触计算机,自然是见多识广,啥都会一点。我撺掇着让他给我演示“如何使用现成框架搭建网页”,当时用的是Bootstrap 1.0。我俩在通信原理的课堂上,坐在教室的后排,偷偷摸摸的教学着。我目不转睛的盯着他一点点用现成的类名,逐渐拼出了一个漂亮的页面,那种感觉真是兴奋极了!


其实现在回头看,我们当时的编码方法非常拙劣,完全上不了台面的那种,还真是一个敢教,一个敢学哈哈哈。


一节课偷偷摸摸下来,我基本上算是知道了一个静态页面如何从0到1的写出来了,剩下的事情就只是时间问题了。一下课我就飞快地跑回宿舍,埋头学习Bootstrap的API文档,一点点一点点,小心翼翼的拼凑着页面。


一周后,我如期把Demo交给了老板,保住了这次兼职机会。这份工作给我带来了每个月2000块的收入,整整持续了一年(当时我一个月生活费也就不到800)。


我是在后来的一年的交往中才知道,这个老板是某世界五百强的集团高管,空闲时间和朋友一起做了个创业公司,出于竞业关系,不方便公开招募团队,于是私下找学生兼职做点项目。


而我,就成了那个幸运的学生。


故事2:屡败屡战拿下百度Offer


图片


转眼到了大三。


杂七杂八的项目做了不少,钱也倒是有赚一些,但基本上都是小打小闹,尤其是技术层面,苦于一直没有接触过企业级项目,我并不知道一个成熟的商业项目的代码长什么样。


抱着这样一个期望,我开始争取大公司的实习机会。


还是从北邮人论坛,我拿到了一个百度的面试机会,具体哪个部门我已经记不太清了。


面试官安排我在茶水间坐下,丢给我一份试题和一支笔。


那份题目我做得稀烂,面试官看了看卷子,当场跟我说,你可能不太行。不过没等他赶我走,我迅速拿出电脑,打开作品集,指着我画的一些设计图对他说:那么……你们招UI设计师么?这是我的作品。


面试官先是一愣(心想这小子不是码农么,怎么还会设计),然后让我等一下,便拿着我的作品,跑去找他们团队的UI设计师帮忙过目。


是不是觉得我的行为很奇怪?没错,我自己也没想明白其实。当时我还没想好毕业后从事什么工作,由于从小就喜欢画点东西,平时也会看一些用户体验设计相关的书,大学期间在学生社团里做了不少有意思的图,所以我天真的以为自己毕业后也是可以从事UI设计工作的。


面试官很快就回来了,跟我摇摇头,结束了这场面试。


这次经历给我打击是比较大的,我原本自豪的以为我既能写代码,又能做设计,一定是个抢手货,但事实上,我的两种技能在学校里勉强能秀一下,到了职场专业人士眼里,就是些玩具级别的东西,根本不够看的。


回去后我继续寻找实习机会,中间拿到了亚信联创的Offer,在亚联实习了三个月,学了些CSS基础知识。但我一直不甘心,还是一心想去大公司。


不久后,我等来了百度的另一个面试机会。这次是核心部门:网页搜索部,百度的立足之本。


经过亚联的短暂实习,我对自己的前端技能颇有信心,感觉这次应该是稳了。


然而,是我天真了。


网页搜索部的前端是重逻辑的,需要写大量的JS代码。而彼时的我,才刚把CSS入门,JS的部分只会使用jQuery写一些简单的页面交互效果。


面试官跟我反馈说没过的时候,我赶紧叫住他,说,能否指点一下我,如果想要在百度做前端的话,我应该学习些什么知识?


他也愣了一下(心想这小子不按套路出牌啊),然后拿过笔,在我笔试卷子的背面空白处,一边说一边写下一些零零散散的知识点。


拿过这张画得乱七八糟的A4纸,我如获至宝,连声感谢之后便离开了百度。


参考着这份凌乱的前端知识图谱,我窝在宿舍学习了一个月。一个月后,我给这个面试官发了条短信,请求说能不能再给我一次面试机会,我感觉我准备好了。


他同意了。于是我第3次到百度面试。


面试很快就结束了。面试官跟我说,看得出来这一个月你确实学到不少东西,进步很大,但是距离录用标准还是有点距离的,抱歉。


我又又又一次叫住面试官,和上次一样,我又跟他要了一份更加进阶的前端知识图谱。我说我回去再学习下,还会再来的。


他尴尬而不失礼貌的跟我笑了笑,便起身离开了。


这次的白纸更加密密麻麻,而且看得出来知识点比上一次又更加专业和精深了许多,学起来自然也吃力了不少。


两个月后,我再次联系到他,请求再一次面试。


他又同意了。于是我第4次到百度面试。


这次面试更快的结束了,可能我们彼此已经相对比较熟悉了吧,前几次考察过的知识点这次也就直接跳过了。


看得出来他有些犹豫,可能是他已经不太好意思当面拒绝我了,于是让我等等,找来了他的同事,给我加了一次交叉技术面。


这位新面试官说话也很直肠子,聊了二十分钟后,他直接跟我说:“技术的话勉勉强强吧,但是你这小子身上有一股抑制不住的热情和活力,学习能力也不错,感觉可以培养一下试试,我这里就给你过了吧,我再跟同事商量下,你稍等”。


过了几分钟,先前那个被我屡次骚扰的面试官来了,通知我,面试过了!


这可把我高兴坏了,经过半年多,4次5轮面试,我终于凭自己本事(厚脸皮)拿到了大公司的实习机会!(当年的百度在技术领域可是BAT之首)


这份实习工作给我带来了4000块每月的收入,让我实现了彻底的经济独立。不过这都不是最重要的,最最最重要的是,我终于知道大公司的前端开发是什么样的了!


后来我入职百度后,当我面对着屏幕上漂亮的,模块化的面向对象风格的JS代码的时候,我终于知道了业余和专业的差距。


Anyway,幸运再一次光顾了我这个小子。




在我十多年的工作经历中,诸如此类的幸运不胜枚举。旁人看到的,是我的一路顺风顺水,但只有我自己明白,生活没有奇迹,所有的幸运,都是一次次暗中努力换来的福报


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

终于可以彻底告别手写正则表达式了

大家好,我是风筝 这篇文章的目的是让你能得到完美的正则表达式,而且还不用自己拼。 说到正则表达式,一直是令我头疼的问题,这家伙一般时候用不到,等用到的时候发现它的规则是一点儿也记不住,\d表示一个数字,\s表示包括下划线在内的任意单词字符,也就是 [A-Za-...
继续阅读 »

大家好,我是风筝


这篇文章的目的是让你能得到完美的正则表达式,而且还不用自己拼。


说到正则表达式,一直是令我头疼的问题,这家伙一般时候用不到,等用到的时候发现它的规则是一点儿也记不住,\d表示一个数字,\s表示包括下划线在内的任意单词字符,也就是 [A-Za-z0-9_],还有[\s\S]*可以匹配包括换行在内的任意字符串。



这你都能记住吗,如果能的话,那真的佩服,反正我是记不住,之前每次手写的时候都得跟查字典似的一个个的查,简单的还好,复杂的就很痛苦了。



过程往往是这个样子的:


1、 先打开 Google,搜索一篇正则表达式,找到一份像上图那样的字典教程,先看个几分钟,回忆回忆,还有可能回忆不起来。


2、然后就开始根据需求写一个正则表达式。


3、放到程序中执行一下。


4、诶,怎么不好用,匹配不上啊,接着修改正则。


5、继续从 3 - 4 的循环,直到运气来了,正常出结果了。


这是最早的时候,真的是全靠那点仅有的实力和运气了。


记得刚毕业不久的时候,有一次领导给安排一个任务,要在一堆 PDF 文件里把我们需要的数据摘出来。PDF 这玩意儿吧,你把它的内容读出来,它就是一大段文本,要在这一堆内容不一致的文件中准确的拿到数据,第一反应就是用正则。


当时的做法就是上面的 1-5这几步来的,加上当时候刚毕业比较菜,跌跌撞撞才把程序写好,中间有几次调试的时候,程序一跑起来,VS(Visual Studio)就特别卡。对的,就是宇宙第一强大的 IDE ,当时我还在写 C#,纵然是宇宙第一强大,也被我弄的特别卡。


当时只道是正则写的有问题,然后就一直改。


后来才知道,那是因为正则写的不合理,发生了回溯现象,越不合理,回溯越严重,加上当时的 PDF 内容很多,所以导致开发工具都卡了,这要是整到线上,那怕是混不下去了。


关于回溯的问题,可以参考下面这篇文章《失控的正则表达式:灾难性的回溯》


http://www.regular-expressions.info/catastrophi…



后来就不至于那么菜了,知道了一些关于正则表达式的在线网站,上面有一些常用的正则表达式,不用自己捣鼓了,能偷懒当然要偷懒了。可以在 Goolge 上搜索关键词「正则表达式 在线」,然后就会出来一大堆,直接在上面用那些常用的正则,例如手机号、邮箱、网址啊,基本上能解决90%的需求场景。


另外的10%呢,以前可能只能自己琢磨了,现在都2023年了,基本上99%的概率都不用亲自动手了,当然了,如果是大佬呢,就想自己写,那完全没问题。


ChatGPT 完美解决


ChatGPT 是LLM(大语言模型)的产品,最最擅长的事情就是分析语言,而正则表达式的应用场景是什么呢,其实就是在一大堆文本语言中按照我们的规则,找到我们需要的内容,总的来说,也是对于文本语言的处理,所以用 ChatGPT 解决正则表达式的问题简直太合适不过了。


比如最简单的,匹配中国的手机号,直接让 ChatGPT 把正则写出来,而且连代码都给你写好了。



至于网址、邮箱等等也不在话下了。


不仅ChatGPT 可以,连百度文心一言也可以。百度文心一言虽然这样可以,但是如果你反过来问它,它就蒙圈了。


比如我问 aaa@126.com 是不是一个合法的邮箱,ChatGPT 会告诉你这个邮箱是合法的,但是百度文心一言就不行了。


下面这个是 ChatGPT 的回答:


ChatGPT 的回答


下面这个是百度文心一言的回答:


文心一言的回答


不仅邮箱不行,你问它一个手机号是否合法,百度文心一言也不行,还会告诉你这个号码的归属地,但是这个归属地也是错误的。


这样就看出来什么是智能,什么是大数据了,明显 ChatGPT 更智能一点,希望国产的大模型能在这两年追上吧。


再举一个例子


匹配一段 HTML 中的某个部分也是正则的常用场景,做过爬虫的或多或少都用过正则吧。


比如我在一大段 HTML 中有这么一部分


<div class="time">这是一个,this is some</div>

现在要拿到这个 div 中的内容部分,当然有很多其他的方式了,比如 Java 版的 jsoup,使用 xpath、css selector 等都可以,但是如果就要用正则呢,是不是自己写的话,一般菜鸟就感觉很麻烦了。


这时候我们问问 ChatGTP ,看看它怎么搞的。


直接就这么问了:



<div> <div> <div>这是一个,this is some</div> <div>button</div> </div> </div>, 用 Java 正则表达式匹配这段 HTML 中 的这个标签的 Text 部分



image-20230418224312067


直接拿过代码跑一下,没有任何问题。


有同学说了,这么明显的标签,还用的着 ChatGPT ,直接拿过来就写了。


这里只是举个例子,如果哪位有比较复杂的匹配逻辑,也可以用ChatGPT 来试试,基本上99%都能直接解决。


还有一个网站很厉害


如果你没有办法或者不想用 ChatGPT ,也不想用百度文心一言这些,我还发现一个网站,这个网站我严重怀疑它已经接入了 ChatGPT ,它也支持通过自然语言描述,就能给出相应的正则表达式。


网站地址:wangwl.net/static/proj…


比如我跟他说:提取一段字符串中的中国手机号码部分,而且还有正则可视化。



上面的那个匹配 HTML 的例子,我也在这个网站上试过,结果也是可以的。


纯粹的好东西分享,我跟这个网站没有任何关系。


一个帮你分析正则的网站


接下来这个网站呢,如果你想对正则有比较深入的理解,或者想看看自己写好的正则或ChatGPT 帮你生成的正则表达式效果怎么样,性能好不好,都可以在这个网站进行。


网站地址:regex101.com/



网站左侧可以选择你的目标语言,也就是你的代码实现是哪种语言 Java 还是 JavaScript 等。


中间上方是正则表达式,中间下方是待匹配的内容。


右侧上方是你写的正则对待匹配内容完整的匹配分析过程,非常详细,可以通过这里清楚的看出这个正则匹配的时候经过了哪些路径。


右侧下方是最终的匹配结果。


如果你写的正则在工作的时候发生了明显的回溯,这里也会给出提示,告诉你问题,让你去优化。


总结


君子善假于物也,虽然我很菜,但是工具好用啊,我+好用的工具,等于我也很厉害了。


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

超有用的Android开发技巧:拦截界面View创建

本篇文章主要是分析如何拦截Activity中View的创建流程,实现无感知的使用自定义View替换指定的系统View,这对于换肤、埋点设计等等将是非常有帮助的一种方式。 LayoutInflater.Factory2是个啥? Activity内界面的创建是由...
继续阅读 »

本篇文章主要是分析如何拦截ActivityView的创建流程,实现无感知的使用自定义View替换指定的系统View,这对于换肤、埋点设计等等将是非常有帮助的一种方式。



LayoutInflater.Factory2是个啥?


Activity内界面的创建是由LayoutInflater负责,LayoutInflater最终会交给内部的一个类型为LayoutInflater.Factory2factory2成员变量进行创建。


这个属性值可以外部自定义传入,默认的实现类为AppCompatDelegateImpl


image.png


然后在AppCompatActivity的初始化构造方法中向LayoutInflater注入AppCompatDelegateImpl:


image.png


image.png


image.png


常见的ImageViewTextView被替换成AppcompatImageViewAppCompatTextView等就是借助AppCompatDelegateImpl进行实现的。


这里有个实现的小细节,在initDelegate()方法中,调用了addOnContextAvailableListener()方法传入一个监听事件实现的factory2注入,这个addOnContextAvailableListener()方法有什么魅力呢?


addOnContextAvailableListener()是干啥用的?


咱们先看下这个方法是干啥用的:


image.png


image.png


最终是将这个监听对象加入到了ContextAwareHelper类的内部mListeners集合中,咱们接下里看下这个监听对象集合最终是在哪里被调用的。


image.png


image.png


可以看到,这个集合最终在ComponetActivityonCreate()方法中调用,请注意,这个调用时机还是在父类的super.onCreate()方法前进行调用的。


所以我们可以得出结论,addOnContextAvailableListener()添加的监听器将在父类onCreate()方法前进行调用。


这个用处的场景还是比较多的,比如我们设置Activity的主题就必须在父类的onCreate()方法前调用,借助这个监听,可以轻松实现。


代码实战



请注意,这个factory2的设置必须在ActivityonCreate()方法前调用,所以我们可以直接借助addOnContextAvailableListener()进行实现,也可以重写onCreate()方法在指定位置实现。当然了,前者更加的灵活,这里我们还是以后者进行举例。



override fun onCreate(savedInstanceState: Bundle?) {
LayoutInflaterCompat.setFactory2(layoutInflater, object : LayoutInflater.Factory2 {
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? {

return if (name == "要替换的系统View名称") {
CustumeView()
} else delegate.createView(parent, name, context, attrs)
}
})
}

请注意,这里也有一个实现的小细节,如果当某个系统View不属于我们要替换的View,请继续委托给AppCompatDelegateImpl进行处理,这样就保证了实现系统组件特有功能的前提下,又能完成我们的View替换工作。


统一所有界面View的替换工作


如果要替换View的界面非常多,一个Activity一个Activity替换过去太麻烦 ,这个时候就可以使用我们经常使用到的ApplicationregisterActivityLifecycleCallbacks()监听所有Activity的创建流程,其中我们用到的方法就是onActivityPreCreated():


registerActivityLifecycleCallbacks(object : Application.ActivityLifecycleCallbacks {
override fun onActivityPreCreated(activity: Activity, savedInstanceState: Bundle?) {
LayoutInflaterCompat.setFactory2(activity.layoutInflater, object : LayoutInflater.Factory2 {
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? {

return if (name == "要替换的系统View名称") {
CustumeView()
} else (activity as? AppCompatActivity)?.delegate?.createView(parent, name, context, attrs) ?: null
}

override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
TODO("Not yet implemented")
}
})

}
}

不过这个Application.ActivityLifecycleCallbacks接口要重写好多无用的方法,太麻烦了,之前写过一篇关于接口优化相关的文章吗,详情可以参考:接口使用额外重写的无关方法太多?优化它


总结


之前看过很多换肤、埋点统计上报等相关文章,多多少少都介绍了向AppCompatActivity中注入factory2拦截系统View创建的思想,我们设置还可以借助此实现界面黑白化的效果,非常的好用,每个开发者都应该去了解掌握的知识点。


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

Transform API 废弃了,路由插件怎么办?

前言 在 AGP 7.2 中,谷歌废弃了Android开发过程非常常用的Transform API,具体信息可以查看Android Gradle 插件 API 更新。 可以看到Transform API在 AGP 7.2 标记了废弃,且在 AGP 8.0 将...
继续阅读 »

前言


在 AGP 7.2 中,谷歌废弃了Android开发过程非常常用的Transform API,具体信息可以查看Android Gradle 插件 API 更新


image.png


可以看到Transform API在 AGP 7.2 标记了废弃,且在 AGP 8.0 将面临移除的命运。如果你将Android工程的AGP升级到7.2.+,尝试运行使用了Transform API的插件的项目时,将得到以下警告。


API 'android.registerTransform' is obsolete.
It will be removed in version 8.0 of the Android Gradle plugin.
The Transform API is removed to improve build performance. Projects that use the
Transform API force the Android Gradle plugin to use a less optimized flow for the
build that can result in large regressions in build times. It’s also difficult to
use the Transform API and combine it with other Gradle features; the replacement
APIs aim to make it easier to extend the build without introducing performance or
correctness issues.

There is no single replacement for the Transform API—there are new, targeted
APIs for each use case. All the replacement APIs are in the
`androidComponents {}` block.
For more information, see https://developer.android.com/studio/releases/gradle-plugin-api-updates#transform-api.
REASON: Called from: /Users/l3gacy/AndroidStudioProjects/Router/app/build.gradle:15
WARNING: Debugging obsolete API calls can take time during configuration. It's recommended to not keep it on at all times.

看到这种情况,相信很多人第一反应都是how old are you?。Gradle API的频繁变动相信写过插件的人都深受其害,又不是一天两天了。业界一些解决方案大都采用封装隔离来最小化Gradle API的变动。常见的如



此次 Transform API 将在 AGP 8.0 移除,这一改动对于目前一些常用的类库、插件都将面临一个适配的问题,常见的如路由、服务注册、字符串加密等插件都广泛使用了Transform API。那么究竟该怎么解决此类适配问题找到平替方案呢?本篇将探讨目前主流的一些观点是否能够满足需求以及如何真正的做到适配。


主流观点



当你尝试解决此问题时,一通检索基本会得到两种不同的见解,目前也有一些同学针对这两个API进行了探索。




那么上述提到的两种API是否真的就能解决我们的问题呢?其实行也不行!


AsmClassVisitorFactory



首先来看看AsmClassVisitorFactory



AsmClassVisitorFactory是没有办法做到像Transform一样,先扫描所有class收集结果,再执行ASM修改字节码。原因是AsmClassVisitorFactoryisInstrumentable方法中确定需要对哪些class进行ASM操作,当返回true之后,就执行了createClassVisitor方法进行字节码操作去了,这就导致可能你路由表都还没收集完成就去修改了目标class


机灵的小伙伴可能会想,那我再注册一个收集路由表的AsmClassVisitorFactory,然后在注册一个真正执行ASM操作的AsmClassVisitorFactory不就好了,那么这种做法可以吗,其实在你的插件想适配Transform Action? 可能还早了点 - 掘金这边文章里已经给出了答案。


TransformAction



既然 AsmClassVisitorFactory 不能打,那 TransformAction 能打不,我们来看下AGP中的实现。




可以看到是有相关ASM实现的。TransformAction 的应用目前较少,主要常见的有 JetifyTransformAarTransform等,主要做产物转换。但 TransformAction 操作起来比较麻烦,详细可以看Transforming dependency artifacts on resolution


平替方案



既然两种观点,一个不能打,一个嫌麻烦,那有没有简单既易用,又可少量修改即可完成适配的方案呢,答案当然是有了。不然水本篇就没有意义了。那么本篇就带大家来简单探索下 Transform API的废弃,针对路由类库的插件适配的一种平替方案。



首先我们要知道Transform在Gradle中其实也对应一个Task,只是有点特殊。我们来看下定义:


public abstract class Transform {

··· omit code ···

public void transform(
@NonNull Context context,
@NonNull Collection<TransformInput> inputs,
@NonNull Collection<TransformInput> referencedInputs,
@Nullable TransformOutputProvider outputProvider,
boolean isIncremental) throws IOException, TransformException, InterruptedException {
}

public void transform(@NonNull TransformInvocation transformInvocation)
throws TransformException, InterruptedException, IOException {
// Just delegate to old method, for code that uses the old API.
//noinspection deprecation
transform(transformInvocation.getContext(), transformInvocation.getInputs(),
transformInvocation.getReferencedInputs(),
transformInvocation.getOutputProvider(),
transformInvocation.isIncremental());
}

··· omit code ···
}

看到这里,有些同学就要疑问了。你这不扯淡吗,Transform根本没有继承 DefaultTaskAbstractTask或者实现 Task 接口。你怎么断定Transform本质上也是一个GradleTask呢?这部分完全可以由Gradle的源码里找到答案,这里不赘述了。


Plugin



回到正题。究竟该怎么去使用Task去适配呢?我们先用伪代码来简要说明下。



class RouterPlugin : Plugin<Project> {

override fun apply(project: Project) {

··· omit code ···

with(project) {

··· omit code ···

plugins.withType(AppPlugin::class.java) {
val androidComponents =
extensions.findByType(AndroidComponentsExtension::class.java)
androidComponents?.onVariants { variant ->
val name = "gather${variant.name.capitalize(Locale.ROOT)}RouteTables"
val taskProvider = tasks.register<RouterClassesTask>(name) {
group = "route"
description = "Generate route tables for ${variant.name}"
bootClasspath.set(androidComponents.sdkComponents.bootClasspath)
classpath = variant.compileClasspath
}
variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)
.use(taskProvider)
.toTransform(
ScopedArtifact.CLASSES,
RouterClassesTask::jars,
RouterClassesTask::dirs,
RouterClassesTask::output,
)
}
}
}

··· omit code ···
}
}

我们使用了onVariants API 注册了一个名为gather[Debug|Release]RouteTablesTask,返回一个TaskProvider对象,然后使用variant.artifacts.forScope(ScopedArtifacts.Scope.ALL)来使用这个Task进行toTransform操作,可以发现我们无需手动执行该Task。来看下这个toTransform的定义。


注意ScopedArtifacts需要AGP 7.4.+以上才支持


/**
* Defines all possible operations on a [ScopedArtifact] artifact type.
*
* Depending on the scope, inputs may contain a mix of [org.gradle.api.file.FileCollection],
* [RegularFile] or [Directory] so all [Task] consuming the current value of the artifact must
* provide two input fields that will contain the list of [RegularFile] and [Directory].
*
*/
interface ScopedArtifactsOperation<T: Task> {

/**
* Append a new [FileSystemLocation] (basically, either a [Directory] or a [RegularFile]) to
* the artifact type referenced by [to]
*
* @param to the [ScopedArtifact] to add the [with] to.
* @param with lambda that returns the [Property] used by the [Task] to save the appended
* element. The [Property] value will be automatically set by the Android Gradle Plugin and its
* location should not be considered part of the API and can change in the future.
*/
fun toAppend(
to: ScopedArtifact,
with: (T) -> Property<out FileSystemLocation>,
)

/**
* Set the final version of the [type] artifact to the input fields of the [Task] [T].
* Those input fields should be annotated with [org.gradle.api.tasks.InputFiles] for Gradle to
* property set the task dependency.
*
* @param type the [ScopedArtifact] to obtain the final value of.
* @param inputJars lambda that returns a [ListProperty] or [RegularFile] that will be used to
* set all incoming files for this artifact type.
* @param inputDirectories lambda that returns a [ListProperty] or [Directory] that will be used
* to set all incoming directories for this artifact type.
*/
fun toGet(
type: ScopedArtifact,
inputJars: (T) -> ListProperty<RegularFile>,
inputDirectories: (T) -> ListProperty<Directory>)

/**
* Transform the current version of the [type] artifact into a new version. The order in which
* the transforms are applied is directly set by the order of this method call. First come,
* first served, last one provides the final version of the artifacts.
*
* @param type the [ScopedArtifact] to transform.
* @param inputJars lambda that returns a [ListProperty] or [RegularFile] that will be used to
* set all incoming files for this artifact type.
* @param inputDirectories lambda that returns a [ListProperty] or [Directory] that will be used
* to set all incoming directories for this artifact type.
* @param into lambda that returns the [Property] used by the [Task] to save the transformed
* element. The [Property] value will be automatically set by the Android Gradle Plugin and its
* location should not be considered part of the API and can change in the future.
*/
fun toTransform(
type: ScopedArtifact,
inputJars: (T) -> ListProperty<RegularFile>,
inputDirectories: (T) -> ListProperty<Directory>,
into: (T) -> RegularFileProperty)

/**
* Transform the current version of the [type] artifact into a new version. The order in which
* the replace [Task]s are applied is directly set by the order of this method call. Last one
* wins and none of the previously set append/transform/replace registered [Task]s will be
* invoked since this [Task] [T] replace the final version.
*
* @param type the [ScopedArtifact] to replace.
* @param into lambda that returns the [Property] used by the [Task] to save the replaced
* element. The [Property] value will be automatically set by the Android Gradle Plugin and its
* location should not be considered part of the API and can change in the future.
*/
fun toReplace(
type: ScopedArtifact,
into: (T) -> RegularFileProperty
)
}

可以看到不光有toTransform,还有toAppendtoGettoReplace等操作,这部分具体用法和案例感兴趣的同学可以自行尝试。接下来来看看Task中的简要代码


Task


abstract class RouterClassesTask : DefaultTask() {

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val jars: ListProperty<RegularFile>

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val dirs: ListProperty<Directory>

@get:OutputFile
abstract val output: RegularFileProperty

@get:Classpath
abstract val bootClasspath: ListProperty<RegularFile>

@get:CompileClasspath
abstract var classpath: FileCollection

@TaskAction
fun taskAction() {
// 输入的 jar、aar、源码
val inputs = (jars.get() + dirs.get()).map { it.asFile.toPath() }
// 系统依赖
val classpaths = bootClasspath.get().map { it.asFile.toPath() }
.toSet() + classpath.files.map { it.toPath() }

··· omit code ···

JarOutputStream(BufferedOutputStream(FileOutputStream(output.get().asFile))).use { jarOutput ->

jars.get().forEach { file ->
Log.d("handling jars:" + file.asFile.absolutePath)
val jarFile = JarFile(file.asFile)
jarFile.entries().iterator().forEach { jarEntry ->
if (jarEntry.isDirectory.not() &&
// 针对需要字节码修改的class进行匹配
jarEntry.name.contains("com/xxx/xxx/xxx", true)
) {
// ASM 自己的操作
} else {
// 不处理,直接拷贝自身到输出
}
jarOutput.closeEntry()
}
jarFile.close()
}

··· omit code ···
}
}

··· omit code ···

}

看完了伪代码,相信很多同学已经知道该怎么做了。那么我们再来个简单🌰来看下我们如何适配现有的路由插件。


在开始之前,我们要知道主流的路由插件使用Transform主要是干了啥,简单概括下其实就是两大步骤:



  • 扫描依赖,收集路由表使用容器存储结果

  • 根据收集到的路由表修改字节码进行路由注册


前面的伪代码其实也是按照这两大步来做的。


示例


chenenyu/Router 为例我们来具体实现以下,至于其他类似库如:alibaba/ARouter 操作方法也类似,这部分工作就留给其他说话又好听的同学去做了。


期望结果


chenenyu/Router需要进行ASM字节码操作的类是com.chenenyu.router.AptHub,这里仅以chenenyu/RouterSample进行演示。我们先看一下使用Transform进行字节码修改后的代码是什么,可以看到通过Gradle动态的新增了一个静态代码块,里面注册了各个module的路由表、拦截器表、路由拦截器映射表等。


static {
HashMap hashMap = new HashMap();
routeTable = hashMap;
HashMap hashMap2 = new HashMap();
interceptorTable = hashMap2;
LinkedHashMap linkedHashMap = new LinkedHashMap();
targetInterceptorsTable = linkedHashMap;
new Module1RouteTable().handle(hashMap);
new Module2RouteTable().handle(hashMap);
new AppRouteTable().handle(hashMap);
new AppInterceptorTable().handle(hashMap2);
new AppTargetInterceptorsTable().handle(linkedHashMap);
}

Plugin



  1. RouterPlugin代码与伪代码基本一致


Task


RouterClassesTask大部分实现与伪代码也相同。这里我们主要以说明用法为主,相应的接口设计以及优化不做处理。通俗点说,就是代码将就看~


abstract class RouterClassesTask : DefaultTask() {

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val jars: ListProperty<RegularFile>

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val dirs: ListProperty<Directory>

@get:OutputFile
abstract val output: RegularFileProperty

@get:Classpath
abstract val bootClasspath: ListProperty<RegularFile>

@get:CompileClasspath
abstract var classpath: FileCollection

@TaskAction
fun taskAction() {
val inputs = (jars.get() + dirs.get()).map { it.asFile.toPath() }
val classpaths = bootClasspath.get().map { it.asFile.toPath() }
.toSet() + classpath.files.map { it.toPath() }
val grip: Grip = GripFactory.newInstance(Opcodes.ASM9).create(classpaths + inputs)
val query = grip select classes from inputs where interfaces { _, interfaces ->
descriptors.map(::getType).any(interfaces::contains)
}
val classes = query.execute().classes

val map = classes.groupBy({ it.interfaces.first().className.separator() },
{ it.name.separator() })

Log.v(map)

JarOutputStream(BufferedOutputStream(FileOutputStream(output.get().asFile))).use { jarOutput ->

jars.get().forEach { file ->
println("handling jars:" + file.asFile.absolutePath)
val jarFile = JarFile(file.asFile)
jarFile.entries().iterator().forEach { jarEntry ->
if (jarEntry.isDirectory.not() &&
jarEntry.name.contains("com/chenenyu/router/AptHub", true)
) {
println("Adding from jar ${jarEntry.name}")
jarOutput.putNextEntry(JarEntry(jarEntry.name))
jarFile.getInputStream(jarEntry).use {
val reader = ClassReader(it)
val writer = ClassWriter(reader, 0)
val visitor =
RouterClassVisitor(writer, map.mapValues { v -> v.value.toSet() })
reader.accept(visitor, 0)
jarOutput.write(writer.toByteArray())
}
} else {
kotlin.runCatching {
jarOutput.putNextEntry(JarEntry(jarEntry.name))
jarFile.getInputStream(jarEntry).use {
it.copyTo(jarOutput)
}
}
}
jarOutput.closeEntry()
}
jarFile.close()
}

dirs.get().forEach { directory ->
println("handling " + directory.asFile.absolutePath)
directory.asFile.walk().forEach { file ->
if (file.isFile) {
val relativePath = directory.asFile.toURI().relativize(file.toURI()).path
println("Adding from directory ${relativePath.replace(File.separatorChar, '/')}")
jarOutput.putNextEntry(JarEntry(relativePath.replace(File.separatorChar, '/')))
file.inputStream().use { inputStream ->
inputStream.copyTo(jarOutput)
}
jarOutput.closeEntry()
}
}
}
}
}

companion object {
@Suppress("SpellCheckingInspection")
val descriptors = listOf(
"Lcom/chenenyu/router/template/RouteTable;",
"Lcom/chenenyu/router/template/InterceptorTable;",
"Lcom/chenenyu/router/template/TargetInterceptorsTable;"
)
}

}

需要额外说明一下的是,一般我们进行路由表收集的工作都是扫描所有classesjarsaars,找到匹配条件的class即可,这里我们引入了一个com.joom.grip:grip:0.9.1依赖,能够像写SQL语句一样帮助我们快速查询字节码。感兴趣的可以详细了解下grip的用法。



  1. 这里我们把所有依赖产物作为Input输入,然后创建grip对象。


val inputs = (jars.get() + dirs.get()).map { it.asFile.toPath() }
val classpaths = bootClasspath.get().map { it.asFile.toPath() }
.toSet() + classpath.files.map { it.toPath() }

val grip: Grip = GripFactory.newInstance(Opcodes.ASM9).create(classpaths + inputs)


  1. 查询所有满足特定描述符的类。


val query = grip select classes from inputs where interfaces { _, interfaces ->
descriptors.map(::getType).any(interfaces::contains)
}
val classes = query.execute().classes


  1. 对查询的结果集进行分类,组装成ASM需要处理的数据源。标注的separator扩展函数是由于字节码描述符中使用/,在ASM操作中需要处理为.


val map = classes.groupBy({ it.interfaces.first().className.separator() }, { it.name.separator() })

通过打印日志,可以看到路由表已经收集完成。




  1. 至此几行简单的代码即实现了字节码的收集工作,然后把上面的map集合直接交给ASM去处理。ASM的操作可以沿用之前的ClassVisitorMethodVisitor,甚至代码都无需改动。至于ASM的操作代码该如何编写,这个不在本篇的讨论范围。由于我们需要修改字节码的类肯定位于某个jar中,所以我们直接针对输入的jars进行编译,然后根据特定条件过滤出目标字节码进行操作。


JarOutputStream(BufferedOutputStream(FileOutputStream(output.get().asFile))).use { jarOutput ->

jars.get().forEach { file ->
val jarFile = JarFile(file.asFile)
jarFile.entries().iterator().forEach { jarEntry ->
if (jarEntry.isDirectory.not() &&
jarEntry.name.contains("com/chenenyu/router/AptHub", true)
) {
println("Adding from jar ${jarEntry.name}")
jarOutput.putNextEntry(JarEntry(jarEntry.name))
jarFile.getInputStream(jarEntry).use {
val reader = ClassReader(it)
val writer = ClassWriter(reader, 0)
val visitor =
RouterClassVisitor(writer, map.mapValues { v -> v.value.toSet() })
reader.accept(visitor, 0)
jarOutput.write(writer.toByteArray())
}
} else {
kotlin.runCatching {
jarOutput.putNextEntry(JarEntry(jarEntry.name))
jarFile.getInputStream(jarEntry).use {
it.copyTo(jarOutput)
}
}
}
jarOutput.closeEntry()
}
jarFile.close()
}

dirs.get().forEach { directory ->
println("handling " + directory.asFile.absolutePath)
directory.asFile.walk().forEach { file ->
if (file.isFile) {
val relativePath = directory.asFile.toURI().relativize(file.toURI()).path
println("Adding from directory ${relativePath.replace(File.separatorChar, '/')}")
jarOutput.putNextEntry(JarEntry(relativePath.replace(File.separatorChar, '/')))
file.inputStream().use { inputStream ->
inputStream.copyTo(jarOutput)
}
jarOutput.closeEntry()
}
}
}
}

所有需要修改的代码已经写完了,是不是很简单。最后我们来验证下是否正常。执行编译后发现字节码修改成功,且与Transform执行结果一致。至此,基本完成了功能适配工作。



总结



  • 本篇通过一些伪代码对适配 AGP 7.4.+ 的 Transform API 进行了简单说明,并通过一个示例进行了实践。

  • 实践证明,对于 Transform API 的废弃,此方案简单可用,但此方案存在一定限制,需AGP 7.4.+。

  • 相比较于TransformAction,迁移成本小且支持增量编译、缓存


示例代码已上传至Router,有需要的请查看v1.7.6分支代码。主要代码在RouterPluginRouterClassesTask


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

中小型项目统一处理请求重复提交

请求重复提交的危害 数据重复:例如用户重复提交表单,造成数据重复。 资源浪费:多次重复请求提交将会浪费服务器的处理资源。但这个相比数据重复的危害性较小。 不一致性:假设我们触发请求增加用户的积分500,如果多次触发这个请求,积分是累加的。这个危害性比重复的数...
继续阅读 »

请求重复提交的危害



  • 数据重复:例如用户重复提交表单,造成数据重复。

  • 资源浪费:多次重复请求提交将会浪费服务器的处理资源。但这个相比数据重复的危害性较小。

  • 不一致性:假设我们触发请求增加用户的积分500,如果多次触发这个请求,积分是累加的。这个危害性比重复的数据更大。

  • 安全性:例如我们在登录页面触发手机验证码的发送请求。频繁触发这个请求将会耗费我们的验证码成本。


防请求重复提交的方案


前端



  • 在用户第一次点击按钮后,即禁用提交按钮。

  • 限制用户提交请求间隔,在一定的时间间隔内只允许用户发起某个请求一次。

  • 在表单提交前,检查前一次请求是否提交成功,已成功的话则提示用户无需再重复提交。


后端



  • 严谨的做法

    • Token机制,在每一个请求中都添加一个Token。Token由服务端生成并发放给前端。服务端接收到请求时,根据Token进行校验。看这个Token是否已被使用。(一般基于缓存)

    • 唯一标志,比如在创建订单的时候,即生成一个唯一的订单号,并将其作为订单的唯一标识。在后续的请求中携带该订单号。当收到订单创建请求时,检查订单号是否已经存在。(一般基于数据库)



  • 非严谨的做法

    • 后端拦截请求,检查请求的用户和参数是否和上次请求相同,相同的话即为重复请求。




这种防请求重复提交的实现有基于Filter的实现,也有基于HandlerInterceptor的实现。最后考量下笔者认为利用RequestBodyAdviceAdapter类来实现代码实现更加简洁,配置更加简单。


在此笔者提供一个注解+RequestBodyAdviceAdapter配合使用的防重复提交的实现。
但是这个方案有个小弊端。仅生效于有RequestBody注解的参数,因为使用RequestBodyAdvice来实现。但是大部分我们需要做请求防重复提交的接口一般都是POST请求,且有requestBody。


完整实现在开源项目中:github.com/valarchie/A…


实现


声明注解


/**
* 自定义注解防止表单重复提交
* 仅生效于有RequestBody注解的参数 因为使用RequestBodyAdvice来实现
* @author valarchie
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Resubmit {

/**
* 间隔时间(s),小于此时间视为重复提交
*/
int interval() default 5;

}

继承RequestBodyAdviceAdapter实现ResubmitInterceptor


大致的实现是。



  • 覆写了supports方法,指明我们仅处理拥有Resubmit注解的方法。

  • 生成每一个请求的签名作为Key。key的生成由generateResubmitRedisKey方法实现。格式如下:resubmit:{}:{}:{}。比如用户是userA。我们请求的类是UserService。方法名是addUser。则这个key为resubmit:userA:UserService:addUser

  • 将Key和请求的参数作为值存到redis当中去

  • 每一次请求过来时,我们检查缓存中这个请求的签名对应的参数是否相同,相同的话即为重复请求。


/**
* 重复提交拦截器 如果涉及前后端加解密的话 也可以通过继承RequestBodyAdvice来实现
*
* @author valarchie
*/
@ControllerAdvice(basePackages = "com.agileboot")
@Slf4j
@RequiredArgsConstructor
public class ResubmitInterceptor extends RequestBodyAdviceAdapter {

public static final String NO_LOGIN = "Anonymous";
public static final String RESUBMIT_REDIS_KEY = "resubmit:{}:{}:{}";

@NonNull
private RedisUtil redisUtil;

@Override
public boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
return methodParameter.hasMethodAnnotation(Resubmit.class);
}

/**
* @param body 仅获取有RequestBody注解的参数
*/
@NotNull
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
// 仅获取有RequestBody注解的参数
String currentRequest = JSONUtil.toJsonStr(body);

Resubmit resubmitAnno = parameter.getMethodAnnotation(Resubmit.class);
if (resubmitAnno != null) {
String redisKey = generateResubmitRedisKey(parameter.getMethod());

log.info("请求重复提交拦截,当前key:{}, 当前参数:{}", redisKey, currentRequest);

String preRequest = redisUtil.getCacheObject(redisKey);
if (preRequest != null) {
boolean isSameRequest = Objects.equals(currentRequest, preRequest);

if (isSameRequest) {
throw new ApiException(ErrorCode.Client.COMMON_REQUEST_RESUBMIT);
}
}
redisUtil.setCacheObject(redisKey, currentRequest, resubmitAnno.interval(), TimeUnit.SECONDS);
}

return body;
}

public String generateResubmitRedisKey(Method method) {
String username;

try {
LoginUser loginUser = AuthenticationUtils.getLoginUser();
username = loginUser.getUsername();
} catch (Exception e) {
username = NO_LOGIN;
}

return StrUtil.format(RESUBMIT_REDIS_KEY,
method.getDeclaringClass().getName(),
method.getName(),
username);
}
}

使用


通过在Controller上打上Resubmit注解即可,interval即多久的间隔内相同参数视为重复请求。


/**
* 新增通知公告
*/
@Resubmit(interval = 60)
@PostMapping
public ResponseDTO<Void> add(@RequestBody NoticeAddCommand addCommand) {
noticeApplicationService.addNotice(addCommand);
return ResponseDTO.ok();
}

这是笔者关于中小型项目防请求重复提交的实现,如有不足欢迎大家评论指正。


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

Android 13 平行视界 ActivityEmbedding详解

背景 Android 13推出了一个大屏幕设备显示方案:Activity嵌入(Activity Embedding)。该功能不同于分屏模式(将多个应用同时显示在屏幕上),而是类似华为平行视界将同一个应用的多个不同Activity同时显示到屏幕上。 本文结合An...
继续阅读 »

背景


Android 13推出了一个大屏幕设备显示方案:Activity嵌入(Activity Embedding)。该功能不同于分屏模式(将多个应用同时显示在屏幕上),而是类似华为平行视界将同一个应用的多个不同Activity同时显示到屏幕上。


本文结合Android 13窗口架构,介绍Activity Embedding的创建流程。


image.png


image.png


窗口模型与Activity Embedding平行视界


Actiivty Embedding的实现流程与Android 13的窗口模型紧密相关。最简单最抽象的情况下,窗口对象之间的关系如下:


image.png


简单来说,Activity所在的Task,是以DisplayArea的形式在整个层级中组织管理的。代表Activity节点的ActivityRecord作为Task的child来管理,Task则由TaskDisplayArea来管理。


用层级结构来看非常直观。下图为常规情况(未进入Activity Embedding平行视界的情况)的层级描述:


image.png


简单解释一下这个层级,该Task位于一个DisplayArea,并以Activity栈的形式管理多个Activity,Activity管理WindowState。其中“V“图标含义是”可见“。


而进入平行视界的分屏模式时,两个Activity将同时显示,而Task本身不提供这个能力,而是由TaskFragment来实现。TaskFragment插入到Task和Activity之间,分割了Task在屏幕上的显示区域,提供给平行视界的两个Activity:


image.png


进入平行视界的层级,可见Task不再管理Actiivty栈,而被TaskFragment取代。


因此,TaskFragment是实现Activity Embedding平行视界的关键。进入平行视界实际上也就是创建TaskFragment并排布位置。


平行视界的一大特征是,Configuration有专属的WindowingMode、MaxBounds不等于Bounds。


image.png


Activity Embedding的两个Activity是独立绘制的:


image.png


Activity Embedding平行视界的流程


Activity Embedding的应用层经过封装,使用相对简单,并且可以根据配置的分屏规则自动完成分屏。


其中关键的两个技术点,自动完成分屏、执行分配操作分别依靠Instrumentation.ActivityMonitor和android.window.ITaskFragmentOrganizerController这个Binder来实现。当新的Activity启动时,ActivityMonitor.onActivityStart()被回调,判断需要启动平行视界时,自动调用ITaskFragmentOrganizerController,实现自动进入分屏。


ITaskFragmentOrganizerController


进入Activity Embedding分屏的关键接口为ITaskFragmentOrganizerController.applyTransaction(),该接口在WindowManagerService中由WindowOrganizerController实现


该方法主要处理Transaction,分为两大类类:Change、HierachyOps。对于平行视界的进入流程,为HierachyOps,简称为HOPs。


通过Transaction为左右两个Activity创建平行视界


创建平行视界的流程封装到了Transaction内,作为HOPs进行处理,保存到Transaction.hops(ArrayLists)内。


创建一个平行视界,实际上分为4步走:1. 创建左边Activity的TaskFragment,设置Configuration内的Bounds属性为左边的平行视界面积大小,并添加到Task内;2. 将左边Activity添加到TaskFragment内;3. 重复第一步,只不过创建的是右边Activity的Fragment;4. 重复第2步,只是目标是右边的Activity。


对应到源码,分别为HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT、HIERARCHY_OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT、HIERARCHY_OP_TYPE_CREATE_TASK_FRAGMENT、HIERARCHY_OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS。


image.png


image.png


image.png


image.png


创建的各个参数,关键点为initialBounds属性记录的Rect,它确定了TaskFragment的大小和位置,最终确定了平行视界左右两个Activity的位置和大小。


添加完成后,会按照常规方式触发窗口的relayout和绘制,将平行视界的内容显示到屏幕上。


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

程序员的6个真面目,没有一个被冤枉!

纵使工作中有许多酸甜苦辣 你在岗位上的每一点付出 每一行代码的敲下 每一次需求的完成 每一个bug的修复 都让我们的生活变得更加高效便捷 让科技绽放出向善而动人的力量 致敬在岗位上创造不凡的你! 作者:腾讯云开发者来源:juejin.cn/post/72265...
继续阅读 »

图片图片


图片


图片


图片


图片


图片


图片


图片


图片


图片


图片


图片


图片


图片


图片


图片


图片


图片


图片


图片


图片


图片


图片


图片图片图片


图片


图片


图片


图片


图片


图片


纵使工作中有许多酸甜苦辣


你在岗位上的每一点付出


每一行代码的敲下


每一次需求的完成


每一个bug的修复


都让我们的生活变得更加高效便捷


让科技绽放出向善而动人的力量


致敬在岗位上创造不凡的你!



作者:腾讯云开发者
来源:juejin.cn/post/7226526110171889720
收起阅读 »

从解决一个页面请求太多的问题开始的

web
一、写在前面   上周测试同事给我提了个bug。他说在公司运营系统某个编辑页面中,一个post请求调用太多次了,想让我看看怎么回事。我刚听他讲这个事情时心里有点不屑一顾,觉得能有多少次啊,大惊小怪的。然而当我在测试环境中打开那个页面一看,直呼好家伙!这个页面...
继续阅读 »

一、写在前面




  上周测试同事给我提了个bug。他说在公司运营系统某个编辑页面中,一个post请求调用太多次了,想让我看看怎么回事。我刚听他讲这个事情时心里有点不屑一顾,觉得能有多少次啊,大惊小怪的。然而当我在测试环境中打开那个页面一看,直呼好家伙!这个页面调用了30次相同的请求,属实有点离谱的!


image-20230409202804026.png
  既然情况属实,那么肯定是需要优化一下的。我打开项目代码全局搜索这个请求,发现是在全局公用的一个 Upload 组件的created方法里面调用的。这个请求发送的目的是获取图片上传 oss 系统的签名。因为这个页面一共有30个 Upload 组件,所以整个页面渲染完成后会调用30次接口!!我接着查看接口请求返回的数据,发现签名的有效期是1小时。每次请求的发送又会重新刷新了这个签名和有效时间。但是为什么最先调用接口的 Upload 组件还能上传图片成功,这我还不知道。


  我灵机一动,如果把这个获取签名的方法单纯抽取出来。第一次调用方法后将返回数据缓存下来,后面请求时岂不美哉!但实际操作时发现事情没我想象的那么简单。。。


二、我的解决方案1.0




  一开始我的方案是使用 Vuex 缓存接口返回的签名数据,Upload 组件每次都先从 Vuex 中 state 中查找签名数据 cosConfig,如果没找到再去请求接口。大致的流程如下图:


image-20230410231612623.png


  在捋清楚后逻辑之后,我开始新写 Vuex 的 state 和对应的 mutation了。当我写完代码后一运行,发现这个也能还是依旧调用了30次请求。这让我我很是纳闷啊!!!无奈只好debugger语句开始一行行代码进行调试。
经过一小段时间的调试,问题被我发现了。那就是:签名数据的异步获取。这个签名数据是通过调用后端接口异步返回给前端的。当这个页面存在30个 Upload 组件时,每个组件都会在自己的 created 生命周期函数里先查找了 Vuex 中有没有缓存的签名数据。当页面第一次渲染时,vuex 中肯定是没有签名数据的。所以每个 Upload 组件都会找不到签名数据,然后每个组件都会继续调用接口获取签名数据。等获取到了签名之后,签名配置数据再缓存在 Vuex 中,也就没有意义了。所以方案一失败!!


三、我的解决方案2.0




  我需要承认的是平时困于重复性业务的开发中,很少去处理稍微复杂一点的问题,脑子容易混沌。我在发现方案1.0失败了之后,开始想其他的解决方案。通过 google 的无私帮助下,我找到了这篇文章([vue中多个相同组件重复请求的问题?]),完全就是和我一样的问题嘛。我进去看了第一个赞最多的回答,清晰透彻!主要的解决方案就是运用设计模式中的单例模式,把 Upload 组件中的获取签名的方案单独抽出来。这样子页面上不管有多少个 Upload 组件,调用的获取签名的方法都是同一个。这样子就可以在这个方法里面做文章了。


  那么要做什么文章呢?我们假设这个获取上传图片签名的方法名叫做 getCosConfig,无论多少个 Upload 组件,都是调用同一个 getCosConfig 方法。那么在这个方法外部添加一个缓存对象 cacheConfig,组件每次先从这个缓存对象查找存不存在配置数据。如果存在直接获取缓存对象,如果不存在就调用接口获取。


  但光是这样效果还是和方案1.0结果一样的,同样会调用30次接口。所以我们还需要加一个计数器变量 count。count 的初始值是0,Upload 组件每次发送请求时都会给 count 加1。这样子当我们发现是第一次请求时就去调用接口,不是第一次的话就等待,直到第一次请求结束获得数据。逻辑流程图如下:


image-20230415123202746.png


四、我的解决方案2.1




  到此,本以为这个问题完美解决,但是我突然发现这个接口有入参的!这个页面调用的30个接口中,其中两个剩余的28个参数是不同的。我赶忙去查询了接口文档,发现这个接口是用于获取图片上传的签名,并且不同的业务模块的存储位置是不同的。那么自然返回的上传签名也是不同的,这也意味着原来的 cosConfig 的数据结构是不对的。因为原来的一级对象结构会导致不同业务模块的签名数据混乱了,搞不好弄成了p0级的线上bug。想到这里我心里一凉,感慨还好我细心多瞅了一眼。


  既然问题已经定位到了,那么解决方案2.1自然而然也出来了,只要改造一下 co sConfig 和 count 的结构即可,增加一个key,变成二级的对象。最后我的代码成品如下:


image.png


image.png


五、总结




  最后总结一下,数据结构和设计原则的学习看似虚无缥缈,实际上能够帮助我们解决复杂度很高的问题。通过结合我们日常的开发工作,我们才能感受到这些知识的魅力,也会让我们更加有动力去提高我们的水平。


六、评论区其他方案推荐




 之前写文章都是自娱自乐,没啥人看。这篇文章不知道怎么看的人挺多,评论的朋友也不少。评论区也提出了不少其他方案和业界通用的解决方案,让我见识到了自己知识面的狭窄。我也总结一下供有需要的人使用:


1.【业务维度】在上传图片时再去获取服务端的token,不需要提前去获取。


2.【技术维度】一些请求库自带了去重的功能,例如vue-query。


3.【技术维度】缓存池的概念和处理,这个老哥写的很好【你不知道的promise】设计一个支持并发的前端接口缓存


4.【技术维度】使用异步单例模式,将请求的Promise缓存下来,再次调用函数的时候返回这个Promise。这篇文章讲的不错,给大家推荐一下高级异步模式 - Promise 单例


image.png


作者:徐徐徐叨叨
来源:juejin.cn/post/7222096611635003451
收起阅读 »

程序员入行感触二三事

引言 好久没有发感触了,之前一直在做讲师授课,接触了好多入门的程序员,有很多感触,但是在做讲师的时候有时候不方便说,在做开发又开始忙,所以就沉淀下来了,忽然今天收到了之前一个学习的小伙伴的消息,心里有些触动,本人也不是一个特别喜欢发朋友圈的人,但是总感觉想说点...
继续阅读 »

引言


好久没有发感触了,之前一直在做讲师授课,接触了好多入门的程序员,有很多感触,但是在做讲师的时候有时候不方便说,在做开发又开始忙,所以就沉淀下来了,忽然今天收到了之前一个学习的小伙伴的消息,心里有些触动,本人也不是一个特别喜欢发朋友圈的人,但是总感觉想说点啥(矫情了,哈哈),所以写写做一个回顾吧。


编程行业从开始到现在被贴上了很多标签: 幸苦,掉头发,工资高,不愁工作等等,这些有好有坏,但是总结起来大多数人对编程行业的认知是:


1、需要一定的学历,尤其对数学和英语要求很高。


2、工作比较累,加班是便饭。


3、收入很可观,10k轻轻松松。


4、岗位比较多,是一个搞高级技术(嘿嘿嘿,之前一个家长和我聊的)的行业。


当然还有很多,但是就是上面这些认知让好多毕业迷茫、家境一般、工作遇到问题的人,把编程行业作为了一个全新开始的选择。于是,就有了市场,有了市场很快就有了资本,有了资本很快就有了营造焦虑氛围的营销策略,然后就有各种各样在掩盖在光鲜下的问题,又得真的很无奈,那么今天就聊聊吧。


问题


1、社会是你做程序员的第一绊脚石


啥意思,啥叫做社会,这里的社会不是一个群居的结构,而是人情世故(嘿嘿嘿),好多小伙伴是转行过来的,老话说的好,人往高处走,水往低处流,大部分转行的小伙伴不是来自于大家认知当中更好的行业(比如:公务员,医生,律师..嘿嘿嘿,扯远了),甚至编程本行业的也很少(程序员自学的能力还是很不错的),所以大家在学习之前就已经在社会上摸爬滚打了很久,久历人情,好处是好沟通,不好的地方就是真的把人情世故看的比技术更重要了,这一点可能拉低这些小伙伴70%的学习效果,你要明白,程序员这个行业确实也有人情世故,但前提是大家可以在一个水平上,这个水平可以是技术,也可以是职级,但是如果开头就这么琢磨的话,没有一个扎实的编程基础,真的很难立足在这个行业。没有必要的谦让,习惯性的差不多损耗了太多的学习效果了,既然选择编程,首先请把技术学好,哪怕是基础(当然那个行业也会有浑水摸鱼的,但是对于转行的小伙伴来说,概率太低了)


2、学历重要,学力也很重要


编程行业是一个需要终生学习的行业,不论是前端,后端,测试,运维还是其他岗位,如果在做技术就一定需要学习,好多人会说学历不够所以干不了编程,但是在我个人的眼里,学历确实重要,但是并没有完全限制你进入编程行业,因为:


(1)任何行业都是有完整的岗位结构的,需要的高精尖人才是需要的,但是普通的岗位也少不了,编程行业也是如此,有些岗位的学历要求不是很高。


(2)在编程行业除了那些竞争激烈的大厂,自考学历是有一定的市场和认可程度的


但是,在学历背后的学力就不是这样一个概念了,这里想表述的是学习能力,包括:


(1)专注能力,好多小伙伴如果之前有一定的社会经历或者在大学过的比较懒散,在没有聊到学历之前,先决条件就是能静下心来学习,但是很多小伙伴专注力根本不达标,听课走神,练习坐不住...(其实个人感觉任何一个行业,能静下心来做,并且活下来的都不会很差)


(2)学习习惯,这里不贬低学历低的小伙伴,但是不能否认的是,参加高考后获得一个高学历的小伙伴能力不谈,但是99%都有一个很好的学习习惯。比如不会在学习的时候把手机放到旁边,科学的记笔记,有效的复习和预习等等,所以在担心学历之前,请先培养好自己的学习习惯(个人建议,如果真的没有一个好的学习习惯,那么学习的时候就不要在眼前出现多余的东西分散注意力,比如: 课桌上除了听课的电脑,不要有其他的,之前见过的容易分散注意力的:手机,水杯,指尖陀螺,魔方....)


3、不要在没有选择能力的时候做出选择


这里想聊的是一些学习恐慌的小伙伴的惯性,好多小伙伴在选择了一种学习方式(买书,看视频,加入培训班)之后,还会进行类比学习,比如:买了Python的一本基础书,然后再大数据或者小伙伴的推荐下又买了另外一本,或者参加了培训班,又去看其他的教学视频,这些对小白同学的学习伤害会很大,因为,本身对技术没有全面的理解,不同的书,不同的教程传递的教学方法是不一样的,混着来有点像老家喝酒掺着喝,白酒不醉,啤酒不醉,白加啤那么就不一定了(很大概率会醉),所以小白同学最总要的不是再学习的过程当中进行对比,而是可以最快最稳的完成基础感念的学习,在自己脑子当中有了基础概念再做选择。


当然了,还有很多,一次也聊不完,之后有时间再聊吧,今天就先写这么多

作者:老边
来源:juejin.cn/post/7174259081484763173
,欢迎大家讨论交流。

收起阅读 »

简述 js 的代码整洁之道

web
前言 为什么代码要整洁? 代码质量与整洁度成正比。有的团队在赶工期的时候,不注重代码的整洁,代码写的越来越糟糕,项目越来越混乱,生产力也跟着下降,那就必须找更多人来提高生产力,开发成本越来越高。 整洁的代码是怎样的? 清晰表达意图、消除重复、简单抽象、能通过测...
继续阅读 »

前言


为什么代码要整洁?


代码质量与整洁度成正比。有的团队在赶工期的时候,不注重代码的整洁,代码写的越来越糟糕,项目越来越混乱,生产力也跟着下降,那就必须找更多人来提高生产力,开发成本越来越高。


整洁的代码是怎样的?


清晰表达意图、消除重复、简单抽象、能通过测试。
换句话说:具有可读性、可重用性和可重构性。


命名




  1. 名副其实:不使用缩写、不使用让人误解的名称,不要让人推测。


    // bad: 啥?
    const yyyymmdstr = moment().format("YYYY/MM/DD");
    // bad: 缩写
    const cD = moment().format("YYYY/MM/DD");

    // good:
    const currentDate = moment().format("YYYY/MM/DD");

    const locations = ["Austin", "New York", "San Francisco"];

    // bad:推测l是locations的项
    locations.forEach(l => doSomeThing(l));

    // good
    locations.forEach(location => doSomeThing(location));



  2. 使用方便搜索的名称:避免硬编码,对数据用常量const记录。


    // bad: 86400000指的是?
    setTimeout(goToWork, 86400000);

    // good: 86400000是一天的毫秒数
    const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000;
    setTimeout(goToWork, MILLISECONDS_PER_DAY);



  3. 类名应该是名词,方法名应该是动词。


    // bad
    function visble() {}

    // good
    function getVisble() {}



  4. 多个变量属于同一类型的属性,那就他们整合成一个对象。同时省略多余的上下文。


    // bad:可以整合
    const carMake = "Honda",
    const carModel = "Accord",
    const carColor = "Blue",

    // bad: 多余上下文
    const Car = {
    carMake: "Honda",
    carModel: "Accord",
    carColor: "Blue",
    };

    // good
    const Car = {
    make: "Honda",
    model: "Accord",
    color: "Blue",
    };



其他:




  • 不要写多余的废话,比如theMessagethe可以删除。




  • 统一术语。比如通知一词,不要一会在叫notice,一会叫announce




  • 用读得通顺的词语。比如getElementById就比 useIdToGetElement好读。




函数(方法)




  • 删除重复的代码,don't repeat yourself。很多地方可以注意dry,比如偷懒复制了某段代码、try...catch或条件语句写了重复的逻辑。


     // bad
    try {
    doSomeThing();
    clearStack();
    } catch (e) {
    handleError(e);
    clearStack();
    }
    // good
    try {
    doSomeThing();
    } catch (e) {
    handleError(e);
    } finally {
    clearStack();
    }



  • 形参不超过三个,对测试函数也方便。多了就使用对象参数。




    • 同时建议使用对象解构语法,有几个好处:



      1. 能清楚看到函数签名有哪些熟悉,

      2. 可以直接重新命名,

      3. 解构自带克隆,防止副作用,

      4. Linter检查到函数未使用的属性。




     // bad
    function createMenu(title, body, buttonText, cancellable) {}

    // good
    function createMenu({ title, body, buttonText, cancellable }) {}



  • 函数只做一件事,代码读起来更清晰,函数就能更好地组合、测试、重构。


     // bad: 处理了输入框的change事件,并创建文件的切片,并保存相关信息到localStorage
    function handleInputChange(e) {
    const file = e.target.files[0];
    // --- 切片 ---
    const chunkList = [];
    let cur = 0;
    while (cur < file.size) {
    chunkList.push({
    chunk: file.slice(cur, cur + size)
    });
    cur += size;
    }
    // --- 保存信息到localstorage ---
    localStorage.setItem("file", file.name);
    localStorage.setItem("chunkListLength", chunkList.length);
    }

    // good: 将三件事分开写,同时自顶而下读,很舒适
    function handleInputChange(e) {
    const file = e.target.files[0];
    const chunkList = createChunk(file);
    saveFileInfoInLocalStorage(file, chunkList);
    }
    function createChunk(file, size = SLICE_SIZE) {
    const chunkList = [];
    let cur = 0;
    while (cur < file.size) {
    chunkList.push({
    chunk: file.slice(cur, cur + size)
    });
    cur += size;
    }
    return chunkList
    }
    function saveFileInfoInLocalStorage(file, chunkList) {
    localStorage.setItem("file", file.name);
    localStorage.setItem("chunkListLength", chunkList.length);
    }



  • 自顶向下地书写函数,人们都是习惯自顶向下读代码,如,为了执行A,需要执行B,为了执行B,需要执行C。如果把A、B、C混在一个函数就很难读了。(看前一个的例子)。




  • 不使用布尔值来作为参数,遇到这种情况时,一定可以拆分函数。


     // bad
    function createFile(name, temp) {
    if (temp) {
    fs.create(`./temp/${name}`);
    } else {
    fs.create(name);
    }
    }

    // good
    function createFile(name) {
    fs.create(name);
    }

    function createTempFile(name) {
    createFile(`./temp/${name}`);
    }



  • 避免副作用。




    • 副作用的缺点:出现不可预期的异常,比如用户对购物车下单后,网络差而不断重试请求,这时如果添加新商品到购物车,就会导致新增的商品也会到下单的请求中。




    • 集中副作用:遇到不可避免的副作用时候,比如读写文件、上报日志,那就在一个地方集中处理副作用,不要在多个函数和类处理副作用。




    • 其它注意的地方:



      • 常见就是陷阱就是对象之间共享了状态,使用了可变的数据类型,比如对象和数组。对于可变的数据类型,使用immutable等库来高效克隆。

      • 避免用可变的全局变量。




    // bad:注意到cart是引用类型!
    const addItemToCart = (cart, item) => {
    cart.push({ item, date: Date.now() });
    };

    // good
    const addItemToCart = (cart, item) => {
    return [...cart, { item, date: Date.now() }];
    };



  • 封装复杂的判断条件,提高可读性。


     // bad
    if (!(obj => obj != null && typeof obj[Symbol.iterator] === 'function')) {
    throw new Error('params is not iterable')
    }

    // good
    const isIterable = obj => obj != null && typeof obj[Symbol.iterator] === 'function';
    if (!isIterable(promises)) {
    throw new Error('params is not iterable')
    }



  • 在方法中有多条件判断时候,为了提高函数的可扩展性,考虑下是不是可以使用能否使用多态性来解决。


     // 地图接口可能来自百度,也可能来自谷歌
    const googleMap = {
    show: function (size) {
    console.log('开始渲染谷歌地图', size));
    }
    };
    const baiduMap = {
    render: function (size) {
    console.log('开始渲染百度地图', size));
    }
    };

    // bad: 出现多个条件分支。如果要加一个腾讯地图,就又要改动renderMap函数。
    function renderMap(type) {
    const size = getSize();
    if (type === 'google') {
    googleMap.show(size);
    } else if (type === 'baidu') {
    baiduMap.render(size);
    }
    };
    renderMap('google')

    // good:实现多态处理。如果要加一个腾讯地图,不需要改动renderMap函数。
    // 细节:函数作为一等对象的语言中,作为参数传递也会返回不同的执行结果,也是“多态性”的体现。
    function renderMap (renderMapFromApi) {
    const size = getSize();
    renderMapFromApi(size);
    }
    renderMap((size) => googleMap.show(size));



其他




  • 如果用了TS,没必要做多余类型判断。




注释




  1. 一般代码要能清晰的表达意图,只有遇到复杂的逻辑时才注释。


     // good:由于函数名已经解释不清楚函数的用途了,所以注释里说明。
    // 在nums数组中找出 和为目标值 target 的两个整数,并返回它们的数组下标。
    const twoSum = function(nums, target) {
    let map = new Map()
    for (let i = 0; i < nums.length; i++) {
    const item = nums[i];
    const index = map.get(target - item)
    if (index !== undefined){
    return [index, i]
    }
    map.set(item, i)
    }
    return []
    };

    // bad:加了一堆废话
    const twoSum = function(nums, target) {
    // 声明map变量
    let map = new Map()
    // 遍历
    for (let i = 0; i < nums.length; i++) {
    const item = nums[i];
    const index = map.get(target - item)
    // 如果下标为空
    if (index !== undefined){
    return [index, i]
    }
    map.set(item, i)
    }
    return []
    };



  2. 警示作用,解释此处不能修改的原因。


    // hack: 由于XXX历史原因,只能调度一下。
    setTimeout(doSomething, 0)



  3. TODO注释,记录下应该做但还没做的工作。另一个好处,提前写好命名,可以帮助后来者统一命名风格。


    class Comment {
    // todo: 删除功能后期实现
    delete() {}
    }



  4. 没用的代码直接删除,不要注释,反正git提交历史记录可以找回。


    // bad: 如下,重写了一遍两数之和的实现方式

    // const twoSum = function(nums, target) {
    // for(let i = 0;i<nums.length;i++){
    // for(let j = i+1;j<nums.length;j++){
    // if (nums[i] + nums[j] === target) {
    // return [i,j]
    // }
    // }
    // }
    // };
    const twoSum = function(nums, target) {
    let map = new Map()
    for (let i = 0; i < nums.length; i++) {
    const item = nums[i];
    const index = map.get(target - item)
    if (index !== undefined){
    return [index, i]
    }
    map.set(item, i)
    }
    return []
    };



  5. 避免循规式注释,不要求每个函数都要求jsdoc,jsdoc一般是用在公共代码上。


    // bad or good?
    /**
    * @param {number[]} nums
    * @param {number} target
    * @return {number[]}
    */

    const twoSum = function(nums, target) {}



对象




  • 多使用getter和setter(getXXX和setXXX)。好处:



    • 在set时方便验证。

    • 可以添加埋点,和错误处理。

    • 可以延时加载对象的属性。


    // good
    function makeBankAccount() {
    let balance = 0;

    function getBalance() {
    return balance;
    }

    function setBalance(amount) {
    balance = amount;
    }

    return {
    getBalance,
    setBalance
    };
    }

    const account = makeBankAccount();
    account.setBalance(100);



  • 使用私有成员。对外隐藏不必要的内容。


    // bad
    const Employee = function(name) {
    this.name = name;
    };

    Employee.prototype.getName = function getName() {
    return this.name;
    };
    const employee = new Employee("John Doe");
    delete employee.name;
    console.log(employee.getName()); // undefined


    // good
    function makeEmployee(name) {
    return {
    getName() {
    return name;
    }
    };
    }




solid




  • 单一职责原则 (SRP) - 保证“每次改动只有一个修改理由”。因为如果一个类中有太多功能并且您修改了其中的一部分,则很难预期改动对其他功能的影响。


    // bad:设置操作和验证权限放在一起了
    class UserSettings {
    constructor(user) {
    this.user = user;
    }

    changeSettings(settings) {
    if (this.verifyCredentials()) {
    // ...
    }
    }

    verifyCredentials() {
    // ...
    }
    }
    // good: 拆出验证权限的类
    class UserAuth {
    constructor(user) {
    this.user = user;
    }

    verifyCredentials() {
    // ...
    }
    }

    class UserSettings {
    constructor(user) {
    this.user = user;
    this.auth = new UserAuth(user);
    }

    changeSettings(settings) {
    if (this.auth.verifyCredentials()) {
    // ...
    }
    }
    }



  • 开闭原则 (OCP) - 对扩展放开,但是对修改关闭。在不更改现有代码的情况下添加新功能。比如一个方法因为有switch的语句,每次出现新增条件时就要修改原来的方法。这时候不如换成多态的特性。


    // bad: 注意到fetch用条件语句了,不利于扩展
    class AjaxAdapter extends Adapter {
    constructor() {
    super();
    this.name = "ajaxAdapter";
    }
    }

    class NodeAdapter extends Adapter {
    constructor() {
    super();
    this.name = "nodeAdapter";
    }
    }

    class HttpRequester {
    constructor(adapter) {
    this.adapter = adapter;
    }

    fetch(url) {
    if (this.adapter.name === "ajaxAdapter") {
    return makeAjaxCall(url).then(response => {
    // transform response and return
    });
    } else if (this.adapter.name === "nodeAdapter") {
    return makeHttpCall(url).then(response => {
    // transform response and return
    });
    }
    }
    }

    function makeAjaxCall(url) {
    // request and return promise
    }

    function makeHttpCall(url) {
    // request and return promise
    }

    // good
    class AjaxAdapter extends Adapter {
    constructor() {
    super();
    this.name = "ajaxAdapter";
    }

    request(url) {
    // request and return promise
    }
    }

    class NodeAdapter extends Adapter {
    constructor() {
    super();
    this.name = "nodeAdapter";
    }

    request(url) {
    // request and return promise
    }
    }

    class HttpRequester {
    constructor(adapter) {
    this.adapter = adapter;
    }

    fetch(url) {
    return this.adapter.request(url).then(response => {
    // transform response and return
    });
    }
    }



  • 里氏替换原则 (LSP)




    • 两个定义



      • 如果S是T的子类,则T的对象可以替换为S的对象,而不会破坏程序。

      • 所有引用其父类对象方法的地方,都可以透明的替换为其子类对象。

      •     也就是,保证任何父类对象出现的地方,用其子类的对象来替换,不会出错。下面的例子是经典的正方形、长方形例子。




    // bad: 用正方形继承了长方形
    class Rectangle {
    constructor() {
    this.width = 0;
    this.height = 0;
    }

    setColor(color) {
    // ...
    }

    render(area) {
    // ...
    }

    setWidth(width) {
    this.width = width;
    }

    setHeight(height) {
    this.height = height;
    }

    getArea() {
    return this.width * this.height;
    }
    }

    class Square extends Rectangle {
    setWidth(width) {
    this.width = width;
    this.height = width;
    }

    setHeight(height) {
    this.width = height;
    this.height = height;
    }
    }

    function renderLargeRectangles(rectangles) {
    rectangles.forEach(rectangle => {
    rectangle.setWidth(4);
    rectangle.setHeight(5);
    const area = rectangle.getArea(); // BAD: 返回了25,其实应该是20
    rectangle.render(area);
    });
    }

    const rectangles = [new Rectangle(), new Rectangle(), new Square()];// 这里替换了
    renderLargeRectangles(rectangles);

    // good: 取消正方形和长方形继承关系,都继承Shape
    class Shape {
    setColor(color) {
    // ...
    }

    render(area) {
    // ...
    }
    }

    class Rectangle extends Shape {
    constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
    }

    getArea() {
    return this.width * this.height;
    }
    }

    class Square extends Shape {
    constructor(length) {
    super();
    this.length = length;
    }

    getArea() {
    return this.length * this.length;
    }
    }

    function renderLargeShapes(shapes) {
    shapes.forEach(shape => {
    const area = shape.getArea();
    shape.render(area);
    });
    }

    const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
    renderLargeShapes(shapes);



  • 接口隔离原则 (ISP) - 定义是"客户不应被迫使用对其而言无用的方法或功能"。常见的就是让一些参数变成可选的。


     // bad
    class Dog {
    constructor(options) {
    this.options = options;
    }

    run() {
    this.options.run(); // 必须传入 run 方法,不然报错
    }
    }

    const dog = new Dog({}); // Uncaught TypeError: this.options.run is not a function

    dog.run()

    // good
    class Dog {
    constructor(options) {
    this.options = options;
    }

    run() {
    if (this.options.run) {
    this.options.run();
    return;
    }
    console.log('跑步');
    }
    }



  • 依赖倒置原则(DIP) - 程序要依赖于抽象接口(可以理解为入参),不要依赖于具体实现。这样可以减少耦合度。


     // bad
    class OldReporter {
    report(info) {
    // ...
    }
    }

    class Message {
    constructor(options) {
    // ...
    // BAD: 这里依赖了一个实例,那你以后要换一个,就麻烦了
    this.reporter = new OldReporter();
    }

    share() {
    this.reporter.report('start share');
    // ...
    }
    }

    // good
    class Message {
    constructor(options) {
    // reporter 作为选项,可以随意换了
    this.reporter = this.options.reporter;
    }

    share() {
    this.reporter.report('start share');
    // ...
    }
    }
    class NewReporter {
    report(info) {
    // ...
    }
    }
    new Message({ reporter: new NewReporter });



其他




  • 优先使用 ES2015/ES6 类而不是 ES5 普通函数。




  • 多使用方法链。




  • 多使用组合而不是继承。




错误处理




  • 不要忽略捕获的错误。而要充分对错误做出反应,比如console.error()到控制台,提交错误日志,提醒用户等操作。




  • 不要漏了catch promise中的reject。




格式


可以使用eslint工具,这里就不展开说了。


最后


接受第一次愚弄


让程序一开始就做到整洁,并不是一件很容易的事情。不要强迫症一样地反复更改代码,因为工期有限,没那么多时间。等到下次需求更迭,你发现到代码存在的问题时,再改也不迟。


入乡随俗


每个公司、项目的代码风格是不一样的,会有与本文建议不同的地方。如果你接手了一个成熟的项目,建议按照此项目的风格继续写代码(不重构的话)。因为形成统一的代码风格也是一种代码整洁。



参考:



  1. 《代码整洁之道》

  2. github.com/ryanmcdermo…
    (里面有很多例子。有汉化但没更新)


作者:xuwentao
来源:juejin.cn/post/7224382896626778172

收起阅读 »

ES6 Class类,就是构造函数语法糖?

web
一、Class 类可以看作是构造函数的语法糖 ES6引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。constructor()方法,这就是构造方法,而this关键字则代表实例对象。类的所有方法都定义在类的prototype...
继续阅读 »

一、Class 类可以看作是构造函数的语法糖



ES6引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。constructor()方法,这就是构造方法,而this关键字则代表实例对象。类的所有方法都定义在类的prototype属性上面,方法前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。使用的时候,类必须使用new调用跟构造函数的用法完全一致。



  • 类不存在变量提升



    class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}

toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var p = new Point(1, 2);

通过代码证明:


    class Point {
// ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

类的数据类型就是函数,类本身就指向构造函数。



constructor: 方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。



class Point {
}

// 等同于
class Point {
constructor() {}
}

取值函数(getter)和存值函数(setter)


        class Person {
constructor(name, age) {
this.name = name
this.age = age
}

get nl() {
return this.age
}

set nl(value) {
this.age = value
}
}
let p = new Person('fzw', 25)
console.log(p.nl);
p.nl = 44
console.log(p.nl);

class表达式


        let person = new class {
constructor(name) {
this.name = name;
}

sayName() {
console.log(this.name);
}
}('张三');

person.sayName(); // "张三"


上面代码中,person是一个立即执行的类的实例。


二、静态方法、静态属性



类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。



         class Foo {
static classMethod() {
this.baz(); // 'hello'
return '我被调用了';
}
static baz() {
console.log('hello');
}
baz() {
console.log('world');
}
}

console.log(Foo.classMethod()); // 我被调用了

var foo = new Foo();
foo.classMethod() // TypeError: foo.classMethod is not a function

注意 如果静态方法包含this关键字,这个this指的是类,而不是实例。静态方法可以与非静态方法重名。


        class Foo {
static classMethod() {
return 'hello';
}
}

class Bar extends Foo {
}

Bar.classMethod() // 'hello'

父类的静态方法,可以被子类继承。父类Foo有一个静态方法,子类Bar可以调用这个方法。
静态方法也是可以从super对象上调用的。


        class Foo {
static classMethod() {
return 'hello';
}
}

class Bar extends Foo {
static classMethod() {
// super在静态方法之中指向父类
return super.classMethod() + ', too';
}
}

console.log(Bar.classMethod());

注意 super 在静态方法之中指向父类。


静态属性



static 关键词修饰,可继承使用



         class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // 42
}
}
class Bar extends MyClass {
}
new MyClass()
console.log(Bar.myStaticProp);

三、私有方法和私有属性



#修饰属性或方法,私有属性和方法只能在类的内部使用。
私有属性也可以设置 getter 和 setter 方法
私有属性和私有方法前面,也可以加上static关键字,表示这是一个静态的私有属性或私有方法。



    class Counter {
#xValue = 0;

constructor() {
console.log(this.#x);
}

get #x() { return this.#xValue; }
set #x(value) {
this.#xValue = value;
}
}

四、class 继承




  • Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。

  • ES6 规定,子类必须在constructor()方法中调用super(),如果不调用super()方法,子类就得不到自己的this对象。调用super()方法会执行一次父类构造函数。

  • 在子类的构造函数中,只有调用super()之后,才可以使用this关键字,



        class Foo {
constructor() {
console.log(1);
}
}

class Bar extends Foo {
constructor(color) {
// this.color = color; // ReferenceError
super();
this.color = color; // 正确
}
}

const bar = new Bar('blue');
console.log(bar); // Bar {color: 'blue'}

super 关键字



super这个关键字,既可以当作函数使用,也可以当作对象使用。



  • super作为函数调用时,代表父类的构造函数。只能用在子类的构造函数之中,用在其他地方就会报错。

  • super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。



作为对象,普通方法中super指向父类的原型对象


    class A {
constructor() {
this.x = 1;
}
print() {
console.log(this.x);
}
}

class B extends A {
constructor() {
super();
this.x = 2;
}
m() {
super.print();
}
}

let b = new B();
b.m() // 2

注意:
在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。


作为对象,静态方法之中,这时super将指向父类


        class Parent {
static myMethod(msg) {
console.log('static', msg);
}

myMethod(msg) {
console.log('instance', msg);
}
}

class Child extends Parent {
static myMethod(msg) {
// super 代表父类
super.myMethod(msg);
}

myMethod(msg) {
// super 代表父类原型对象
super.myMethod(msg);
}
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

extends 关键字


168fb9a3828f9cb4_tplv-t2oaga2asx-zoom-in-crop-mark_4536_0_0_0.awebp
    // 1、构造器原型链
Child.__proto__ === Parent; // true
Parent.__proto__ === Function.prototype; // true
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
// 2、实例原型链
child.__proto__ === Child.prototype; // true
Child.prototype.__proto__ === Parent.prototype; // true
Parent.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true


extends 继承,主要就是:



  1. 把子类构造函数(Child)的原型(__proto__)指向了父类构造函数(Parent),

  2. 把子类实例child的原型对象(Child.prototype) 的原型(__proto__)指向了父类parent的原型对象(Parent.prototype)。


这两点也就是图中用不同颜色标记的两条线。



子类构造函数Child继承了父类构造函数Parent的里的属性,使用super调用的。




作者:f_人生如戏
来源:juejin.cn/post/7225511164125855781
收起阅读 »

深拷贝的终极实现

web
引子 通过本文可以学习到深拷贝的三种写法的实现思路与性能差异 首先,我们要理解什么是深拷贝,以及为什么要实现深拷贝 深拷贝是什么 通俗来讲,深拷贝就是深层的拷贝一个变量值; 为什么要实现深拷贝 因为在拷贝引用值时,由于复制一个变量只是将其指向要复制变量的引...
继续阅读 »

引子



通过本文可以学习到深拷贝的三种写法的实现思路与性能差异



首先,我们要理解什么是深拷贝,以及为什么要实现深拷贝


深拷贝是什么


通俗来讲,深拷贝就是深层的拷贝一个变量值;


为什么要实现深拷贝


因为在拷贝引用值时,由于复制一个变量只是将其指向要复制变量的引用内存地址,他们并没有完全的断开,而使用就可以实现深拷贝将其完全拷贝为两个单独的存在,指向不同的内存地址;


如何实现深拷贝


一行实现


let deepClone = JSON.parse(JSON.stringify(obj))

这种是最简单的实现方法,虽然这个方法适用于常规,但缺点是无法拷贝 Date()或是RegExp()
 


简单实现


function deepClone(obj) {
// 判断是否是对象
if (typeof obj !== 'object') return obj
// 判断是否是数组 如果是数组就返回一个新数组 否则返回一个新对象
var newObj = obj instanceof Array ? [] : {};
// 遍历obj
for (var key in obj) {
// 将key值拷贝,再层层递进拷贝对象的值
newObj[key] = deepClone(obj[key]);
}
// 返回最终拷贝完的值
return newObj;
}

对于普通的值(如数值、字符串、布尔值)和常见的引用类型(如对象和数组),这个写法完全够用。


但是这个写法有个缺陷,就是无法正确拷贝 Date()  和  RegExp()  等实例对象,因为少了对这些引用类型的特殊处理


普通版


function deepClone(origin, target) {
let tar = target || {};
for (var key in origin) {
if (origin.hasOwnProperty(key)) {
if (typeof origin[key] === 'object' && origin[key] !== null) {
tar[key] = Array.isArray(origin[key]) ? [] : {};
deepClone(origin[key], tar[key]);
} else {
tar[key] = origin[key];
}
}
}
return tar;
}

这个深拷贝方法通过判断属性的值类型,实现了对 对象数组 以及 DateRegExp 等引用类型对象的递归拷贝,同时也考虑了拷贝基本类型值的情况,能够满足大多数场景的要求。


最终版


为什么还有最终版?

上面的案例,可以应对一般场景。


但是对于有两个对象相互拷贝的场景,会导致循环的无限递归,造成死循环!



Uncaught RangeError: Maximum call stack size exceeded



场景:


image.png


如何解决无限递归的问题?

首先我们要了解 WeakMap()
WeakMap的键名所指向的对象,不计入垃圾回收机制;


而通过 WeakMap 记录已经拷贝过的对象,能防止循环引用导致的无限递归;



WeakMap 的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用



代码

利用 WeakMap() 在属性遍历完绑定,并在每次循环时获取当前键名,如果存在则返回数据,不存在则拷贝


function deepClone(origin, hashMap = new WeakMap()) {
// 判断是否是对象
if (origin == undefined || typeof origin !== 'object') return origin;
// 判断是否是Date类型
if (origin instanceof Date) return new Date(origin);
if (origin instanceof RegExp) return new RegExp(origin);

// 判断是否是数组
const hashKey = hashMap.get(origin);
// 如果是数组
if (hashKey) return hashKey;

// 从原型上复制一个值
// *:利用原型构造器获取新的对象 如: [], {}
const target = new origin.constructor();
// 将对象存入map
hashMap.set(origin, target);
// 循环遍历当前层数据
for (let k in origin) {
// 判断当前属性是否为引用类型
if (origin.hasOwnProperty(k)) {
target[k] = deepClone(origin[k], hashMap);
}
}
return target;
}

我们再来看一下使用最新版后的两个对象互相拷贝:


image.png


可以看到,通过使用 WeakMap 记录已经拷贝的对象,有效防止循环引用导致的栈溢出错误,是功能最完备的深拷贝实现。


总结


深拷贝可以完全拷贝一个对象,生成两个独立的且相互不影响的对象。


明白各种深拷贝实现的思路和性能差异,可以在不同场景选用最优的方案。


作者:Shrimpsss
来源:juejin.cn/post/7226181917997547576
收起阅读 »

前端想要做一个定时任务?来试试这个吧

前言 在工作以及学习中,我们有时候会遇到一些定时任务的需求,比如每天定时发送邮件,定时给群或者频道发消息,定时爬取数据等等。这些其实都是比较常见的需求,但是我们又不想去部署一个专门的定时任务服务,这个时候我们就可以使用 Google App Script 实现...
继续阅读 »

前言


在工作以及学习中,我们有时候会遇到一些定时任务的需求,比如每天定时发送邮件,定时给群或者频道发消息,定时爬取数据等等。这些其实都是比较常见的需求,但是我们又不想去部署一个专门的定时任务服务,这个时候我们就可以使用 Google App Script 实现这些需求。


Google App Script 是什么?


Google App Script 是谷歌提供的一种脚本语言,可以在谷歌的各种服务中使用,比如 Google docsGoogle sheetsGoogle forms 等等。它的语法和 js 基本差不多,但是又有一些不同,比如没有 windowdocument 等对象,但是有SpreadsheetAppDocumentApp 等对象,这些对象可以用来操作 Google docsGoogle sheets 等服务。


写代码前的准备


既然 Google App Script 可以直接操作 Google sheets,那我们就可以直接使用 Google Sheets 来充当我们的数据库。我们在读取表格中的数据以后,再根据数据的内容来执行不同的操作。


首先我们先新建一张表格,然后在表格中填入一些数据,比如下面这样:


image.png


channel 字段表示消息要发送到的频道,hourOfDayminutesOfDay 表示消息要发送的时间,message 表示消息的内容,isWorkDay 则使用了 Google Sheets 中的 NETWORKDAYS 函数来判断今天是否是工作日。而 webhook 就是我们要发送消息的 URL


slack 为例,如果想用机器人给频道发消息,我们可以在 slack 中创建一个 App,然后在 App 中创建 一个新的Incoming Webhooks,然后我们就可以获取到一个 Webhook URL,我们只需要发送 POST 请求 URLbody 中带上消息内容,就可以实现给频道发消息的功能了。


image.png


然后我们在 Google Sheets中点击 扩展程序 -> App 脚本,就会跳转到 Google App Script 的编辑器中,这个时候我们就可以开始写 js 代码了。


用法


首先,我们要先获取到 Google Sheets 中的数据。并将其转换成我们想要的格式。


const getDailyMessage = () => {
const workbook = SpreadsheetApp.getActiveSpreadsheet();
const sheet = workbook.getSheetByName(sheets.dailyMessage.sheetName);
return convertValues(sheet.getDataRange().getValues());
}

const convertValues = (values) => {
const [header, ...rows] = values;
return rows.map((row, rowIndex) => {
const result = {};
row.forEach((column, columnIndex) => {
result[header[columnIndex].trim()] = {
value: column,
position: {
row: rowIndex + 2, // skip the header row
column: columnIndex + 1,
},
};
});
return result;
});
};

有了表格中的数据以后,我们就要开始发请求了


const shouldRun = item => {
const hourOfDay = item[sheets.dailyMessage.columnNames.hourOfDay].value;
const minutesOfDay = item[sheets.dailyMessage.columnNames.minutesOfDay].value;
const currentHour = new Date().getHours();
const currentMinutes = new Date().getMinutes()
const isWorkDay = item[sheets.dailyMessage.columnNames.isWorkDay].value
if (isWorkDay && hourOfDay === currentHour && currentMinutes === minutesOfDay) {
return true
}
return false
}
// daily message
function runDailyMessage() {
const dailyMessage = getDailyMessage();
Logger.log(`${dailyMessage.length} daily message channel(s) are ready to notify...`)
const tasks = dailyMessage.filter(shouldRun).map(item => new Promise((resolve) => {
try {
const channelName =
item[sheets.dailyMessage.columnNames.channel].value;
const message = item[sheets.dailyMessage.columnNames.message].value;
const url = item[sheets.dailyMessage.columnNames.webhook].value
const options = {
method: "post",
payload: JSON.stringify({
text: message,
}),
}
const fetchResult = UrlFetchApp.fetch(url, options).getContentText();
if (fetchResult === "ok") {
afterNotify(cfg, nextMemberIndex);
}
Logger.log(`${channelName} daily message notify successfully`);
} catch (err) {
Logger.log(`${channelName} daily message throws error: `);
Logger.log(err);
} finally {
resolve()
}
}))
Promise.all(tasks)
.then(() => {
Logger.log("daily message have been executed");
})
.catch((err) => Logger.log(err));
}

写完以后,我们就可以在 Google Sheets 中点击 运行 -> runDailyMessage 来执行我们的代码了。


image.png


这时候,就可以在 slack 中看到我们的消息了。


image.png


触发器


上面是手动执行的代码,但是我们希望代码能够自动执行,这时候就需要用到 Google App Script 的触发器了。


Tips


在设置触发器以前,有两个值得注意的地方:



  1. 上面的函数中,我们使用的是 SpreadsheetApp.getActiveSpreadsheet() 获取到 Google Sheets 中的数据。但是在触发器中,使用这个 api 是无法获取到数据的,这会导致你的触发器失败。所以我们需要在触发器中使用 SpreadsheetApp.openById 来获取到 Google Sheets 中的数据。


//  use openById replace getActiveSpreadsheet when use app script trigger
const workbook = SpreadsheetApp.openById("在这里填入你 Google Sheets 的 id");


  1. 我们最好是部署我们当前的应用, Google App Script 编辑器右上角就有部署按钮


image.png


选择脚本库,添加新版本描述,然后点击部署。就部署完成了。


触发器设置


image.png


App Script 中点击小时钟图标,就可以看到触发器的设置了。


image.png


选择我们要运行的函数,选择刚才部署的版本,然后设置触发器的时间,这里我们设置为每分钟执行一次,就完成了一个简单的定时任务啦。


总结


Google App Script语法跟 JavaScript 很像,所以对于前端学习起来也很容易,提供了部署以及触发器的功能,可以帮助我们快速实现一些简单的功能。希望这篇文章能够帮助到你。


作者:xinglee
来源:juejin.cn/post/7226229808199581756
收起阅读 »

关于前端实现上传文件这个功能,我只能说so easy!

web
前言 在web前端开发中,文件上传属于很常见的功能,不论是图片、还是文档等等资源,或多或少会有上传的需求。一般都是从添加文件开始,然后读取文件信息,再通过一定的方式将文件上传到服务器上,以供后续展示或下载使用。 下面简单介绍几种上传的方法 简单文件上传 文件上...
继续阅读 »

前言


在web前端开发中,文件上传属于很常见的功能,不论是图片、还是文档等等资源,或多或少会有上传的需求。一般都是从添加文件开始,然后读取文件信息,再通过一定的方式将文件上传到服务器上,以供后续展示或下载使用。


下面简单介绍几种上传的方法


简单文件上传


文件上传的传统形式,是使用表单元素 file


<input type="file" id="file-uploader">

你可以添加 change 事件监听器读取 event.target.files 文件对象。


const fileUploader = document.getElementById('file-uploader')
fileUploader.addEventListener('change', (e) => {
const files = e.target.files
console.log('files', files)
})

多个文件上传


使用 multiple 属性


<input type="file" id="file-uploader" multiple />

文件元数据


在成功上传文件内容后,您可能需要显示该文件内容。对于图片,如果我们在上传后不立即将上传的图片显示给用户,则会感到困惑。


每当上传文件时,File 对象都会包含元数据信息,如文件名称、大小、上次更新时间、类型等。此信息可用于进一步验证和决策。


const fileUploader = document.getElementById('file-uploader')

// 侦听更改事件并读取元数据
fileUploader.addEventListener('change', (e) => {
// 获取文件列表数组
const files = e.target.files

// 循环浏览文件并获取元数据
for (const file of files) {
const name = file.name
const type = file.type ? file.type: 'NA'
const size = file.size
const lastModified = file.lastModified
console.log({ file, name, type, size, lastModified })
}
})

上传前预览图像


我们准备一个上传文件控件,并为预览所选文件准备 img 元素,结构如下:


<input type="file" id="fileInput" />

<img id="preview" />

getElementById() 方法可以获取这两个元素:


const fileEle = document.getElementById('fileInput')
const previewEle = document.getElementById('preview')

使用 URL.createObjectURL() 方法


URL.createObjectURL() 方法包含一个表示参数中给出的对象的 URL。这个新的 URL 对象表示指定的 File对象或 Blob 对象。


fileEle.addEventListener('change', function (e) {
// 获取所选文件
const file = e.target.files[0]

// 创建引用该文件的新 URL
const url = URL.createObjectURL(file)

// 设置预览元素的源
previewEle.src = url
})

使用 FileReader 的 readAsDataURL() 方法



  • 使用 FileReader 对象将文件转换为二进制字符串。然后添加 load 事件侦听器,以获得成功文件上传的二进制字符串。

  • FileReader.readAsDataURL() 方法用于读取指定的 BlobFile对象。


// 获取 FileReader 的实例
const reader = new FileReader()

fileUploader.addEventListener('change', (e) => {
const files = e.target.files
const file = files[0]

// 上传后获取文件对象,以 URL 二进制字符串的形式读取数据
reader.readAsDataURL(file)

// 加载后,对字符串进行处理
reader.addEventListener('load', (e) => {
// 设置预览元素的源
previewEle.src = reader.result
})
})

accept 属性


使用 accept 属性来限制要上传的文件类型。


<input type="file" id="file-uploader" accept=".jpg, .png" multiple>

上面示例中,浏览器将只允许具有 .jpg 和 .png 的文件类型。


验证文件大小


我们读取了文件的大小元数据,可以使用它进行文件大小验证。您可以允许用户上传高达 1MB 的图像文件。


// 文件上载更改事件的侦听器
fileUploader.addEventListener('change', (event) => {
// 读取文件大小
const file = event.target.files[0]
const size = file.size

let msg = ''

// 检查文件大小是否大于 1MB,提示对应消息。
if (size > 1024 * 1024) {
msg = `<span style="color: red;">允许的文件大小为 1MB。您尝试上载的文件属于${returnFileSize(size)}</span>`
} else {
msg = `<span style="color: green;"> ${returnFileSize(size)} 文件已成功上载。 </span>`
}

// 向用户显示消息
feedback.innerHTML = msg
})

显示文件上传进度


更好的可用性是让用户了解文件上传进度。XMLHttpRequest 第二版还定义了一个 progress 事件,可以用来制作进度条。


先在页面中放置一个 progress 标签


<label id="progress-label" for="progress"></label>
<progress id="progress" value="0" max="100" value="0">0</progress>

定义 progress 事件的回调函数


const reader = new FileReader()

reader.addEventListener('progress', (e) => {
if (e.loaded && e.total) {
// 计算完成百分比
const percent = (e.loaded / e.total) * 100
// 将值设置为进度组件
progress.value = percent
}
})

上传目录



有一个非标准属性 webkitdirectory,使我们能够上传整个目录。
虽然最初仅针对基于 WebKit 的浏览器实施,但 WebkitDirectory 在微软 Edge 以及 Firefox 50 及以后也可用。然而,即使它有相对广泛的支持,它仍然不是标准的,不应该使用,除非你别无选择。



<input type="file" id="file-uploader" webkitdirectory />

拖放上传


主要的 JS 如下:


const dropZone = document.getElementById('drop-zone')
const content = document.getElementById('content')

dropZone.addEventListener('dragover', event => {
event.stopPropagation()
event.preventDefault()
event.dataTransfer.dropEffect = 'copy'
})
dropZone.addEventListener('drop', event => {
// 获取文件
const files = event.dataTransfer.files
// ..
})

用对象处理文件


使用 URL.createObjectURL() 方法从文件创建一个唯一的 URL。使用 URL.revokeObjectURL() 方法释放它。



DOM 和 URL.createObjectURL()URL.revokeObjectURL() 方法允许您创建简单的 URL 字符串,可用于引用任何可以使用 DOM 文件对象引用的数据,包括用户计算机上的本地文件。



示例:


<div>
<h1>使用 Object URL</h1>
<input type="file" id="file-uploader" accept=".jpg, .jpeg, .png" >
<div id="image-grid"></div>
</div>

const fileUploader = document.getElementById('file-uploader')
const reader = new FileReader()
const imageGrid = document.getElementById('image-grid')

fileUploader.addEventListener('change', (event) => {
const files = event.target.files
const file = files[0]

const img = document.createElement('img')
imageGrid.appendChild(img)
img.src = URL.createObjectURL(file)
img.alt = file.name
})

总结



  1. 表单元素 file

  2. 文件元数据

  3. 上传前预览图像

  4. URL.createObjectURL() 方法

  5. 使用 FileReader 的 readAsDataURL() 方法

  6. accept
    作者:整天想死的鱼
    来源:juejin.cn/post/7224402365452238906
    属性

收起阅读 »

5分钟速通Kotlin委托

1、什么是委托? 委托,又叫委托模式是一种常用的设计模式,它可以让一个对象在不改变自己原有的行为的前提下,将某些特定的行为委托给另一个对象来实现。它通过将对象之间的关系分离,可以降低系统的耦合度,提高代码的复用性和可维护性。 其中有三个角色,约束、委托对象和被...
继续阅读 »

1、什么是委托?


委托,又叫委托模式是一种常用的设计模式,它可以让一个对象在不改变自己原有的行为的前提下,将某些特定的行为委托给另一个对象来实现。它通过将对象之间的关系分离,可以降低系统的耦合度,提高代码的复用性和可维护性。


其中有三个角色,约束、委托对象和被委托对象。



  • 约束: 一般为接口也可以是抽象类,定义了某个行为。

  • 被委托对象: 负责执行具体的行为。

  • 委托对象: 负责将约束中定义的行为交给被委托对象。


2、Java中的委托


先来说一说委托在Java中的应用用一个简单的例子来说明:


老板在创业初期时因为只有一个人而需要负责产品的客户端UI服务器
这个时候老板负责的这些工作就可以被抽象出来形成一个约束接口:


public interface Work {
void app();
void ui();
void service();
}


public class Boss implements Work {

@Override
public void app() {
System.out.println("Boss doing app");
}

@Override
public void ui() {
System.out.println("Boss doing ui");
}

@Override
public void service() {
System.out.println("Boss doing service");
}
}

现在老板每天都在做这几件事:


public class Main {  
public static void main(String[] args) {
Boss boss = new Boss();
boss.app();
boss.ui();
boss.service();
}
}

输出:


Boss doing app
Boss doing ui
Boss doing service

运气不错,产品赚了不少钱,老板花钱雇了一个员工,将这些工作委托给他处理,自己直接脱产,只需要知道结果就可以了,于是就有了:


public class Employee implements Work{  
@Override
public void app() {
System.out.println("Employee doing app");
}

@Override
public void ui() {
System.out.println("Employee doing ui");
}

@Override
public void service() {
System.out.println("Employee doing service");
}
}


public class Boss implements Work{  
private Employee employee;

public Boss(Employee employee) {
this.employee = employee;
}

@Override
public void app() {
employee.app();
}

@Override
public void ui() {
employee.ui();
}

@Override
public void service() {
employee.service();
}
}


public class Main {  
public static void main(String[] args) {
Boss boss = new Boss(new Employee());
boss.app();
boss.ui();
boss.service();
}
}


Employee doing app
Employee doing ui
Employee doing service

这就是一个委托模式,老板委托对象)将 工作约束)委托给 员工被委托者)处理,老板并不关心每项工作具体是如何实现的,员工在完成工作后也会和老板汇报,就算这几项工作内容发生变化也只是员工需要处理。


3、Kotlin中的委托


那么针对上述的委托所描述例子在Kotlin中是如何实现的呢?


答案是使用关键字by,Kotlin专门推出了by来实现委托:
上述例子中的工作员工都不变:


interface Work {  
fun app()
fun ui()
fun service()
}


class Employee : Work {  
override fun app() {
println("Employee doing app")
}

override fun ui() {
println("Employee doing ui")
}

override fun service() {
println("Employee doing service")
}
}

老板这个类中,我们要将工作使用关键字by委托给员工


class Boss(private val employee: Employee) : Work by employee

就这么一行,实现了Java代码中老板类的效果。


fun main(args: Array<String>) {  
val boss = Boss(Employee())
boss.app()
boss.ui()
boss.service()
}

结果肯定是一样的。
那么by是如何实现Java中委托的效果的呢?通过反编译Kotlin字节码后我们看到:


public final class Boss implements Work {  
private final Employee employee;

public Boss(@NotNull Employee employee) {
Intrinsics.checkNotNullParameter(employee, "employee");
super();
this.employee = employee;
}

public void app() {
this.employee.app();
}

public void service() {
this.employee.service();
}

public void ui() {
this.employee.ui();
}
}

其实就是Java中实现委托的代码,Kotlin将它包成一个关键字by,效率大幅提升。


4、属性委托


上述说明的委托都属于类委托,而在Kotlin当中by不仅可以实现类委托,还可以实现属性委托,属性委托为Kotlin的一大特性,将对属性的访问委托给另一个对象。使用属性委托可以让我们编写更简洁、更模块化的代码,并且能够提高代码的可重用性。


4.1 如何实现属性委托?


Kotlin官方文档中给出了定义:


使用方式:val/var <属性名>: <类型> by <表达式>


在 by 后面的表达式是该 委托, 属性对应的 get() 和set()会被委托给它的 getValue() 与 setValue() 方法。 如果该属性是只读的(val)其委托只需要提供一个 getValue() 函数如果该属性是var则还需要提供 setValue()函数。例如:


   class Example {  
var str: String by Delegate()
}


    class Delegate {  
operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
return "$thisRef, thank you for delegating '${property.name}' to me!"
}

operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
println("$value has been assigned to '${property.name}' in $thisRef.")
}
}


fun main(args: Array<String>) {  
val p = Example()
p.str = "Hello"
println(p.str)
}

因为属性str是可变的所以在Delegate类中实现了getValue和setValue两个函数,其中一共出现了三个参数分别是



  • thisRef :读出 str 的对象

  • property :保存了对 str 自身的描述 (例如你可以取它的名字)

  • value :保存将要被赋予的值


运行结果如下:


Hello has been assigned to 'str' in Example@1ddc4ec2.
Example@1ddc4ec2, thank you for delegating 'str' to me!

我们再将Example类中的代码转为Kotlin字节码反编译得到以下代码:


 public final class Example {  
// $FF: synthetic field
static final KProperty[] $$delegatedProperties = new KProperty[]{Reflection.mutableProperty1(new MutablePropertyReference1Impl(Example.class, "str", "getStr()Ljava/lang/String;", 0))};
@NotNull
private final Delegate str$delegate = new Delegate();

@NotNull
public final String getStr() {
return this.str$delegate.getValue(this, $$delegatedProperties[0]);
}

public final void setStr(@NotNull String var1) {
Intrinsics.checkNotNullParameter(var1, "<set-?>");
this.str$delegate.setValue(this, $$delegatedProperties[0], var1);
}
}

就是创建了一个Delegate对象,再通过调用setVaule和getValue一对方法来获取和设置值的。


4.2 标准委托


在Kotlin标准库为委托提供了几种方法


4.2.1 延迟属性 Lazy


public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)

首次访问属性时才进行初始化操作,lazy() 是接受一个 lambda 并返回一个 Lazy <T> 实例的函数,返回的实例可以作为实现延迟属性的委托, 该lambda表达式将在第一次访问该属性时被调用,初始化属性并返回属性值,之后的访问将直接返回初始化后的值。


简单的例子:


fun main(args: Array<String>) {  
val str : String by lazy {
println("Hello str")
"lazy"
}
println(str)
println(str)
}

输出:


Hello str//只在第一次访问时执行
//后续访问只返回值
lazy
lazy

当我们使用 by lazy 委托实现延迟初始化时,Kotlin 编译器会生成一个私有的内部类,用于实现委托属性的懒加载逻辑,其内部包含一个名为 value 的属性,用于存储真正的属性值。同时,还会生成一个名为 isInitialized 的私有 Boolean 属性,用于标识属性是否已经初始化。


当我们首次访问被 lazy 修饰的属性时,如果它还未被初始化,就会调用 lazy 所接收的 lambda 表达式进行初始化,并将结果保存在 value 属性中。之后,每次访问该属性时,都会返回 value 中存储的属性值。


4.2.2 可观察属性 Observable


Delegates.observable() 接受两个参数:初始值与修改时处理程序(handler)。 每当我们给属性赋值时会调用该处理程序(在赋值执行)。它有三个参数:被赋值的属性、旧值与新值:


class User {  
var name : String by Delegates.observable("no value") {
property, oldValue, newValue ->
println("property :${property.name}, old value $oldValue -> new value $newValue")
}
}


fun main() {
val user = User()
user.name = "Alex"
user.name = "Bob"
}


property :name, old value no value -> new value Alex
property :name, old value Alex -> new value Bob

如果你想截获赋值并“否决”它们,那么使用 vetoable() 取代 observable()。 在属性被赋新值生效之前会调用传递给 vetoable 的处理程序,简单来说就是利用你设定的条件来决定设定的值是否生效,还是以上述代码为例,在User中增加一个年龄属性:


var age : Int by Delegates.vetoable(0) {  
_, oldValue, newValue ->
println("old value : $oldValue, new value : $newValue")
newValue > oldValue
}

在这里我们设定了输入的年龄大于现在的年龄才生效,运行一下看看输出什么:
0
old value : 0, new value : 20
20
old value : 20, new value : 19
20
old value : 20, new value : 25
25


0
old value : 0, new value : 20
20
old value : 20, new value : 19
20
old value : 20, new value : 25
25

4.2.3 将属性储存在映射中


映射(map)里存储属性的值。 这经常出现在像解析 JSON 或者做其他“动态”事情的应用中。 在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。


class User(map: MutableMap<String, Any?>) {  
val name: String by map
val age: Int by map
}


fun main(args: Array<String>) {  
val user = User(
mutableMapOf(
"name" to "Alex",
"age" to 18
)
)
println("name : ${user.name}, age : ${user.age}")
}

输出:


name : Alex, age : 18

5、总结


委托是一种常见的软件设计模式,旨在提高代码的复用性和可维护性,在 Java 中,委托通过定义接口和实现类来实现。实现类持有接口的实例,并将接口的方法委托给实例来实现。这种方式可以实现代码的复用和解耦,但是需要手动实现接口中的方法,比较繁琐,而在 Kotlin 中,委托通过by关键字实现委托其中还包括了属性委托一大特性,Kotlin 提供了很多内置的属性委托,比如延迟属性、映射属性等。此外,Kotlin 还支持自定义属性委托。自定义属性委托需要实现 getValuesetValue 方法,用于获取和设置属性的值,与 Java 的委托相比,Kotlin 的属性委托更加方便和简洁,减少样板代码。


6、感谢



  1. 校稿:ChatGpt

  2. 文笔优化:ChatGpt


参考:Kotlin官方文档:委托 Kotlin官方文档:属性委托


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

哎,今天在公司的最后一天了

“啊!” 我今天居然被通知裁员了!!! 虽然之前一直盛传我们这边要裁员,但是我想着,应该一时半会轮不到我这边,我感觉我们项目还是相对挣点钱的。但是呢,心里也是挺忐忑的。 今天,我正在那全心全意的敲代码,敲的正起劲呢。我突然看到我们项目经理被叫走了。啊?我一想啥...
继续阅读 »

“啊!” 我今天居然被通知裁员了!!!


虽然之前一直盛传我们这边要裁员,但是我想着,应该一时半会轮不到我这边,我感觉我们项目还是相对挣点钱的。但是呢,心里也是挺忐忑的。


今天,我正在那全心全意的敲代码,敲的正起劲呢。我突然看到我们项目经理被叫走了。啊?我一想啥情况?要开会的话怎么只叫我们项目经理,怎么不叫我呀。


难道是要裁员?!难道真的是要裁员?!然后我就看着我们项目经理和我们的上级领导他们一起坐在小屋里聊了半天,啊,我的小心脏呀,我心里就祈祷呀:“千万不要裁员啊!千万不要裁员呀!千万不要裁员呀!!!”


等我们项目经理出来之后,他走到了我这边,然后 “啪” 拍了一下我的肩膀,然后 “哎” 叹了口气。他说:“我们这个项目要被裁掉了。”


我说心里特别失落,但还故作镇定的说:“为啥?我们的项目不是还挣钱呢吗?”


项目经理说:“哎,挣钱也不行。我们现在不需要这么多人了。我们现在的项目,没有一个大的发展了啊!你先等一会吧,等一会他们还得找你谈。”。他走的时候,又拍了拍我肩膀。


哎,当时我就感觉我心里呀那种失落感呀,没法说的那种感觉。果然,没一会,我们经理就来了。他过来之后跟我说:“走,请你到小屋里喝点水。”


我苦笑着跟他说:“经理,我能不去吗?我现在不渴。”


然后我们经理说:“哎,不行呀,我都已经给你倒好了,走吧走吧,歇会去。”


然后我就默默的跟他去了。进去之后呢,我们俩都坐下了。经理跟我笑着说:“恭喜你呀,脱离苦海了。”


哎,我当时心情比较低落,我说:“是呀,脱离苦海了,但又上了刀山了呀。哈哈哈。。。”


然后他说:“哎,确实是,没办法,现在,哎,公司也不容易。现在有一些项目确实得收缩。”


我说:“哎,这也没啥,这都很正常。咱公司还算不错的,最起码还让过了个节。很多公司什么都不管,就这样让走了呀。哎!”


后面我们就谈了一些所谓的那种离职补偿啊,等等一些列的东西**。**


反正当时感觉着吧,就是,嗯,聊完之后呢就准备出去嘛。然后走路的时候呀,就感觉这个腿上啊就跟绑了铅块一样。


当时我感觉,哎,裁员这玩意怎么说呢,都没法回去和亲人说呀,弄的一下午这个心里慌慌的。怎么跟家人交代呢?人至中年居然混成这样,哎!!!


郑重声明,本文不是为了制造焦虑,发文的原因有两个:



  1. 我今年 33 了,一方面给大家展现下一个普通程序员 35 岁后能咋样?是送外卖还是跑滴滴?难道真的就找不到工作了吗?

  2. 感觉我并没有走好自己的人生路,把自己的经历写出来发到网上,让年轻人以我为鉴,能更好的走好自己的人生路。

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

写在入职九周年这天,讲讲这些年的心路历程

往前翻翻才意识到已经很长很长时间没有写文章,大概具体有多长?感觉上有一光年那么长。 今天,刚好是入职九周年,竟然遇到周末,省了奶茶钱。是的,没错,我在一个窝里面趴了九年。 这些年,彷徨过,迷茫过,孤独过,也充满热血的奋斗过,激情的追求过,有犹豫,有脆弱,也有失...
继续阅读 »

往前翻翻才意识到已经很长很长时间没有写文章,大概具体有多长?感觉上有一光年那么长。


今天,刚好是入职九周年,竟然遇到周末,省了奶茶钱。是的,没错,我在一个窝里面趴了九年。


这些年,彷徨过,迷茫过,孤独过,也充满热血的奋斗过,激情的追求过,有犹豫,有脆弱,也有失落。在悠悠岁月中,能及时不断做出调整,让自己学会享受工作带来的乐趣,学会慢慢成长。在当下浮躁的时代,写些闲言碎语,给诸君放松下心情,缓解压力。


过去的那些年


入职那天,阳光明媚,清风柔和,大厦旁边的道路开满迎春花,连空气都是甜的,让人不由自主地深呼吸,可以闻到花香草香,还有阳光的味道。


那一天也是为数不多来上班较早的一天,哦吼有大草坪,哦哟还有篮球场,这楼还波浪线,牛批,B座这个大厅有点大,有点豪华,牛批牛批,头顶上这看着怎么像熊掌,12345,设计真不孬啊。慢慢的大厅上聚集了很多人,有点吵,咋也没人组织一下呢,大家都很随意的站着等待,陆陆续续有员工来上班。


“XXX”,听到有人喊我名字,吓我一跳,还以为偷看小姐姐被发现了。


“站到靠近电梯入口的最右一列,第一个位置上”,“XX,去站在他后面”,“大家按我叫名字的顺序排好队,咱们准备上楼了”。


那会儿,AI 还不会人脸识别过闸机。


呦呵,这公司真牛批,还有扶手电梯。跟着带路的同学来到三楼五福降中天会议室,一个挺老大的屋子,还有各种数不过来的高大上仪器电子设备,一周后,也是在这里,我和厂长面对面聊聊人生。坐稳扶好后,HR 同学开始入职培训,我摸摸新电脑,摸摸工卡牌,心里美滋滋,想到未来几年,将在这样美妙的环境中度过,喜不胜收,甚至我都闻到了楼下食堂啵啵鱼的香味。


培训刚结束。


“我叫到名字的同学,跟我来。XXX,XX……”,纳尼???中午还管饭??这福利也太好了吧,真不用吧,我自己能找到食堂,再说,你知道我喜欢吃什么吗?“跟着我走,咱们去北门做班车,去另一个办公楼,你们的工位不在这儿。”不在这?还坐班车?what?被外包了?她刚刚喊我了吗???差不多六七个同学,跟着楼长鱼贯而出,下楼,走小路,几分钟后,上了班车。司机大哥,一脚油门,就带着我远离啵啵鱼。


大约10分钟,也有可能是5分钟,或者15分钟,按照现在萝卜快跑无人车的速度,是7分钟。来到一栋圆了咕咚的,长的像长南瓜的楼,它有一个很科技感的名字,“首创空间”,就是这个空间,不仅给我带来技术的成长,还有十几斤的未来多年甩也甩不掉的肥肉。没有啵啵鱼的日子,相见便成为世上最奢侈的愿望。


大约是两年后的初春 ,准确的说,不到两年,记得是二、三月份,北京的PM2.5比较严重的时候,鼻子还不会过敏,也没学会发炎,眼睛也不知道怎么迎风流泪,总之,我们要搬家了。


“科技园”\color{#333333}“科技园”,听听,听听,多好的名字,长得像无穷大与莫比乌斯环,楼顶带跑道,位置也牛批,毗邻猪厂、鹅厂、渣浪,北邻联想,西靠壹号院,远眺百望山,低头写代码,啧啧,美滋滋的日子又来了。当时还没有共享单车,晚饭时蹭着班车和一群小伙伴过去看新工位,喏,不错不错,挺大,位置离厕所不远,不错不错,会议室安静舒适好多个,不错不错。重点来了,食堂大的离谱,还有很多美食,连吃几个月,基本不重样。吃过几次啵啵鱼,与大厦简直天壤之别,怀念。


机会说来就来,几个月后的一天,发生了一件大事。我回到了梦开始的地方,那让人朝思暮想的啵啵鱼,那让人魂牵梦绕的味道,那让人无法忘怀的美妙口感。


清醒一下.gif


命运说变就变,国庆休假回来,食堂换了运营商,我他么……¥#%@%#!@#@!&%


一直没变的:不忘初心,砥砺前行。


曾经觉得自己无所不能,可以改变世界,总幻想像蝴蝶一样扇扇翅膀,亚马逊的雨林就会刮起大风。食堂吃的多了,越来越认识到自己的影响力微乎其微,我们能做到,是把交代的工作做好,做到极致,应该就是对公司最大的回馈了,也对得起日渐增多的白发。


早些年,搞视频直播,课程学习,每天研究各种编解码技术,与视频流打交道,看过不少底层技术原理书籍,探索低延迟的 P2P 技术,枯燥,乏味,也跟不上时代变化,觉得自己会的那些早晚被淘汰,技术乏陈革新的速度超乎想象,而你所负责的,恰恰不是那些与时代贴合度较高的业务,边缘化。


怎么破?


从来没有人限制你,不允许你去学习。\color{red}从来没有人限制你,不允许你去学习。


因为恰巧在做课程的直播、录播,需要特别关注课程内容,主要担心出现线上问题,刚好利用这个契机,了解到很多跨专业,跨部门的业务,当时给自己的宗旨是,“只要有时间,就去听课”,“凡是所学,皆有收获”。前后积累近千小时的学习时长,现在想想,觉得都有些不可能,怎么做到的,是人吗?这是人干的事?


日常工作,专心不摸鱼,积极努力提高工作效率,解决研发任务,配合 peer 做好产品协同。晚饭后,专心研究 HTML大法,通勤路上手机看文档,学 api 用法,学习各种牛批的框架,技巧,逛各大论坛,写博客做积累,与各种人物斯比,每天晚上十点,跑步半小时,上床睡觉,生活很规律。


机缘巧合下,我终于从一个小坑,成功跳到一个大坑,并至今依然在坑中。那天,我想起了啵啵鱼。


16797732_0_final.png


一直在变的


团队在变,用两只手数了数,前前后后换了七次 leader,管理风格比五味杂陈还多一味,有的事无巨细,有的不闻不问,有的给你空间让你发挥,有的完全帮不上忙。怎么破?尊重,学习,并努力适应,不断调整心态,适应环境的变化。


业务在变,这么多年数过来,参与过的产品没有一百也有八十了,真正能够长期坚守下来的产品不多,机会可遇不可求,能得一二,实属幸运。把一款产品从零做到一,很容易;再做到十,很难但能够完成;再从十到百,几乎不可能。互联网公司怎么会有这样的产品存在,少之又少。


技能在变,经历过前端技术栈井喷的同学都深有体会,学不动的感受。


时代在变,社会在变,人心也在改变。


曾经多次想过换个环境,换一个坑趴着,毕竟很多机会还是很诱人的。印象最深的一次,是在某年夏天,对手头的工作实在是感到无聊。由于前一年小伙伴们的共同努力,产品架构设计相当完美,今年的工作接近于智力劳动转变为纯人力的重复的机械的体力劳动,对产品建设渐失激情,每天如同行尸走肉般的敲键盘,突然意识到,自己到了职业发展瓶颈期。如何抉择,走或留,临门一脚的事,至于这一脚踢向何方,还未知。


忧思几天后,去找 leader 沟通,好家伙,他让我呆在这里别动,帮他稳住团队,他要撤,一两个月的事。好家伙,你不殿后掩护我们,自己先撂了,还说可以试试带团队,我说大哥,也没几个人呀。他说你还能招兵买马,试试新的角色,体会下不同的视角,很好的机会。坑,绝对的大坑,我他么竟然义不容辞的答应了。


好在,不枉大家这么多年的认可,团队战斗力很强大。


你觉得什么是幸福



  • 有独处的时间

  • 有生活的追求

  • 工作能给你带来乐趣


颐和园.jpg


前些日子,给娃拿药请了半天假,工作日人不多,十点多就完事了,看看时间地铁回去差不多到公司刚好中午饭。医院出来看到很多小黄车,美团那种新式的自行车,看着很不错,还没体验过,特别想兜几圈。查地图,距离公司有22公里,按照骑行的速度推算,70分钟也差不多到了。打定主意后,书包里翻出俩水煮蛋(鬼知道我为什么早上去公司拿了俩鸡蛋)和一瓶水(鬼使神差的早上往书包放的),算是吃过早饭了。于是一个人,一条狗,开局一把刀,沿着滨河路,经过木樨地,二里河,中关村南大街,北大街,信息路,上地西路回来了。您还别说,就是一个地道。竟然还路过玉渊潭,还遇到了封路限行,静悄悄的圆明园东路,过国图,还有数不清的大学,附中,有那么一瞬间好想回母校去看看,总之,重点是顺路吃到心心念的煎饼果子。


路上给媳妇打电话,这小妞竟然说我疯了,疯了?你懂个屁,这叫幸福。


人生的乐趣


人生的乐趣何在?你的答案和我的答案可能不一样,作为打工人,我知道,肯定不是工作。但似乎又不能没有工作,不工作我们怎么活着?怎么在这个社会上,换取资源,立足于当下,着眼于未来。说回工作,最后悔的事,曾经有那么一小段,人际关系没有处理好,可能造成误会,当时来自于我对某些事情的不表态,默许的态度,十周年前修复它。最快乐的时光,是和大家一起沉浸在技术点的探讨,Bug的跟进定位,发现问题解决问题的成就感;参与产品的规划,出谋划策,影响他人;挑灯夜战,专注于产品的 DDL,为上线争分夺秒的努力前行,感受团队的力量。


这个春天,爬过许多京郊的小山头,站在山顶,凝视着壮丽的景色,总以为自己是秦始皇。不惑之前,去征服贡嘎雪山。


总之,故事太多讲也讲不完,作为一个九年的老东西,我是不会爆金币的。


到结尾了,给点建议吧


建议?给不了给不了,我自己还没活明白。


历史的滚滚车轮中,每个生命都很渺小,时代一直在变,抓住机遇,让自己成长,多读书,沉下心,慢慢来。


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

该写好代码吗?我也迷茫了

我在抖音上看到当当网创始人李国庆发了一条视频,感觉他说的挺有意思。 他说,作为企业中层管理者,该不该有自己的山头,用于自我保护,这让人很迷茫。 其实我也迷茫过。我猜测,我们迷茫的可能是同一件事。 程序员内部,曾经流传着这样几句圣经: 代码写的好,写得快,会像...
继续阅读 »

我在抖音上看到当当网创始人李国庆发了一条视频,感觉他说的挺有意思。


他说,作为企业中层管理者,该不该有自己的山头,用于自我保护,这让人很迷茫。


其实我也迷茫过。我猜测,我们迷茫的可能是同一件事。


程序员内部,曾经流传着这样几句圣经:



代码写的好,写得快,会像个闲人。代码有注释,逻辑清晰,任何人都能轻松取代你。


代码写的烂,只有自己能看懂,一次次救火,你反而会成为团队不可缺少的人才。



那么,问题来了:到底该把事情干好呢,还是不要干好呢?


这是一个问题吗?当然是往多快好省了做呀!


我以前的想法就是这样。


我做底层员工时,代码写的清晰简洁,高效严谨。有时候我会因为计算循环次数而费心设计。如果循环层数太多,我会先把关键数据放到Map里,后续可以直接取用。我也会关注代码的可读性,尽量少套几层循环,命名兼顾字符长度和表意指向,如果代码太多就抽离成一个方法函数,并且要在代码里注释清楚。而这些操作,在形成习惯之后,是不会影响开发效率的。反而在某些情况下,还会提高效率。因为不管逻辑多复杂,不管过去多久,一看就能懂,很容易排查问题和他人接手。


我做中层管理时,除了培养团队内每个员工都能做到上述标准外,让我投入很大精力的事情就是“去我化”。也就是通过手段、流程、文化做到团队自治。我在团队时,大家能很高效地完成工作。当我短时间内离开时,大家依然能依靠惯性维持高效的状态。


我的想法很单纯:不管我是一线开发,还是中层管理,我修炼的都是自己。当你具备一定的职场能力时,你就是值钱的。作为员工你能把手头的活干得又快又好,作为管理你能把团队管理得积极健康。这就是亮点。不要在意别人的看法,你只需要修炼自己。当你具备了这个能力,这里不适合你,好多地方都会求此类贤人若渴。


其实,后面慢慢发现,这种想法可能还值得商榷。


因为创业和打工的区别还是挺大的。


创业是给自己干,想干好是肯定的,谁都不愿意面对一团糟。


我看明朝的历史,建文帝朱允炆刚登基时,就想削弱其他藩王的势力,加强自己的权力。当建文帝打算办燕王朱棣时,朱棣就起兵造反,自己做了皇帝。我感觉朱棣其实是自卫。后来,感情朱棣当上皇帝的第一件事,也是继续削弱藩王的势力。其实,大家都一样。


打工就不一样了,干得好不好,不是你说了算,是你的上级领导说了算,周围同事说了算,规章制度说了算。


因此,扁鹊三兄弟的现象就出现了。


扁鹊大哥,医术最高,能预防病人生病。扁鹊二哥,医术很高,能消灭病症在萌芽阶段。到扁鹊这里,只能到人快死了,开刀扎针,救人于生死之间。但是,世人都称扁鹊为神医。


如果你的领导是一个技术型的,同时他还能对你的工作质量做一些审查,那么他对你的评价,可能还具有些客观性。


但是,如果你碰到的是一个行政型的领导,他不是很懂医术,那他就只能像看医生治病一样,觉得救治好了快要死的人,才是高人。而对于预防重症这类防患于未然的事情,他会觉得你在愚弄他。


事实上,不少中小企业的领导,多是行政型领导。他们常常以忠心于老板而被提拔。


因此,为了自己获得一些利益。有些人常常是先把大病整出来,然后再治好,以此来体现自己的价值。


老维修工建设管道,水管里流的是燃气,燃气管的作用是排废水。出了问题,来一批一批的新师傅,都解决不了,越弄越乱。结果老维修工一出手,就把问题解决了。老板觉得,哎呀,看,还得是我的老师傅管用。


相反,如果你把事情打理的井井有条,没有一丝风浪,就像扁鹊大哥一样,我都不得病,还养你干啥,随便换谁都可以。这样的话,往往你的结局就多是被领导忽视。


我有好几个大领导都在大会上说过:你请假一周,你的部门连给你打一个电话的都没有,这说明你平时疏于管理,对于团队没有一丝作用!


从业这么多年,见过各种现实,很讽刺,就像是笑话。有一个同事,给程序加了一个30秒后延时执行。后来,领导让他优化速度,他分4次,将30秒调到5秒。最后领导大喜,速度提高6倍,他被授予“超级工匠”的荣誉称号。


一个是领导的评价。还有一个是同事的评价。


我有一次,在自己的项目组里搞了个考核。考核的核心就是,干好了可以奖,干差了便会罚。我觉得这样挺好,避免伤了好人心,杜绝隧了闲人的意。结果因为其他项目组没有搞,所以我成了众矢之的。他为什么要搞?人家项目组都没有,就他多事,咱们不在他这里干了!


我想,国内的管理可能不是一种客观的结果制。而是另一种客观的”平衡制“。


就像是古代的科举,按照才学,按照成绩来说,状元每届多是江南的。但是,皇帝需要平衡,山西好久没有出个状元了,点一个吧。河南今年学子闹事,罢考,为了稳一稳人心,给一个吧。江南都这么多了,少一个没什么关系的。


他更多是要让各方都满意。一个”平衡“贯穿了整个古今现代的价值观。


有人遵循自己的内心做事,也有人遵循别人的内心做事。不管遵循哪一方,坚持就好,不要去轻易比较,各自有各自的付出和收获。


当路上都在逆行时,你会发现,其实是你在逆行。


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

为什么面试聊得很好,转头却挂了?

了解校招、分享校招知识的学长来了! 四月中旬了,大家面试了几场? 大家有没有这样的感受:面试的时候和面试官聊得火热朝天,气味相投。 面完觉得自己稳了,已经在畅想入职事宜了,结果一封感谢信让人瞬间清醒。 不少同学应该有这样的经历。 学长也曾经有过:面试两小时,...
继续阅读 »

了解校招、分享校招知识的学长来了!


四月中旬了,大家面试了几场?


大家有没有这样的感受:面试的时候和面试官聊得火热朝天,气味相投。


面完觉得自己稳了,已经在畅想入职事宜了,结果一封感谢信让人瞬间清醒。


image.png


不少同学应该有这样的经历。


学长也曾经有过:面试两小时,自觉面试问题回答得不错,但是面试官只说:你回去等消息吧。


经历过面试的同学应该懂”回去等消息“这句话的杀伤力有多大。


在此也想先和那些面试多次但还是不通过的朋友说:千万别气馁!


找工作看能力,有时候也看运气,面试没有通过,这并不说明你不优秀。


所有,面试未通过,这其中的问题到底出在哪呢?


01 缺乏相关经验或技能


如果应聘者没有足够的经验或技能来完成职位要求,或者面试的时候没有展现自己的优势,那么失败很常见。


而面试官看重也许就是那些未展现的经验或技能,考察的是与岗位的匹配程度。


02 没有准备充分


每年学长遇到一些同学因为时间安排不当,没有任何了解就开投简历。


而被春招和毕业论文一起砸晕的同学更是昏头转向。


如果没有花足够的时间和精力来了解公司和职位,并准备回答常见的面试问题,那么可能表现不佳。


03 与招聘人员沟通不畅


在面试过程中,面试官真的非常看重沟通效果!


如果应聘者无法清晰地表达自己的想法,或者不能理解面试官的问题,那么可能会被认为不适合该职位。


04 缺乏信心或过度紧张


学长也很理解应届生的局促感,以及面对面试官的紧张。


image.png


但是如果面试场上感到非常紧张或缺乏自信,那么可能表现得不自然或不真诚。


好像,面试的时候需要表现得自信、大方,才能入面试官的眼。


05 不符合公司文化或价值观


企业文化,也成为考察面试者的一个利器。


如果应聘者的个人品格、行为或态度与公司文化或价值观不符,那么可能无法通过面试。


比如一个一心躺平的候选人,面对高压氛围,只会 Say goodbye。


image.png


06 其他候选人更加匹配


如果公司有其他候选人比应聘者更加匹配该职位,那么应聘者可能无法通过面试。


一个岗位,你会面对强劲的对手。


同样学历背景,但有工作经验比你丰富的;


工作经验都 OK,但有其他学历背景比你合适,或稳定性比你高的面试者。


经常有同学发帖吐槽面试经历:一场群面,只有 Ta 是普通本科生,其余人均 Top 学校研究生学历。


面试不容易,祝大家都能斩获心仪的 Offer!


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

程序员如何给变量起名字

程序员如何给变量起名字 在编写代码时,为变量命名是非常重要的。良好的命名习惯可以提高代码的可读性和可维护性,使得其他开发者能够更容易地理解你的代码。在这篇文章中,我们将讨论程序员如何为变量选择合适的名称。 规范 首先,需要了解所用编程语言和项目的命名规范。不同...
继续阅读 »

程序员如何给变量起名字


在编写代码时,为变量命名是非常重要的。良好的命名习惯可以提高代码的可读性和可维护性,使得其他开发者能够更容易地理解你的代码。在这篇文章中,我们将讨论程序员如何为变量选择合适的名称。


规范


首先,需要了解所用编程语言和项目的命名规范。不同的编程语言和团队可能有不同的命名约定。例如,Python 中通常使用下划线分隔单词(snake_case),而 Java 和 JavaScript 则倾向于驼峰式大小写(camelCase)。遵循一致的命名规则会使得整个代码库更具统一性,降低学习成本。


见名知意


一个好的变量名应该尽可能描述它代表的实际含义。换句话说,当其他开发者看到变量名时,他们应该能够猜测出它表示什么以及如何使用。


好例子



  • user_name 代表用户名;

  • password_hash 表示经过哈希处理的密码;

  • email_list 是一个邮件列表。


不好的例子



  • x, y, z 这样的简单字母命名无法反映变量的实际含义(除非在特定场景下,如表示坐标或数学公式中);

  • tempdata 等过于泛化,无法直接理解其用途;

  • string1array2 只提供了数据类型信息,但未说明其用途。


避免冗长


虽然应该让变量名具有描述性,但同时需要避免使用冗长的名称。太长的名称可能会导致代码难以阅读和维护。通常情况下,选择简洁明确的单词组合更为可取。


好例子



  • index

  • user_count


不好的例子



  • the_index_of_the_current_element_in_the_list

  • the_total_number_of_users_in_the_database


使用专业术语


如果你正在编写涉及某个领域知识的代码,可以使用该领域的专业术语作为变量名。这将使得对该领域能较好理解的开发者更容易理解你的代码意图。


好例子



  • 在计算几何领域,变量名 centroid 表示多边形的质心;

  • 在密码学领域,变量名 salt 代表加密时混入的额外值。


处理复数


当变量包含一系列对象时,最好使用复数名称。这样可以让读者知道它是一个集合类型(如列表、数组、集等),而不仅仅包含一个对象。


好例子



  • users

  • files


避免重名和相似命名


为了提高代码的可读性,应尽量避免在同一作用域内使用相似或容易混淆的变量名。


不好的例子



  • user_listusers_list

  • convert_to_stringtransform_to_string


结论


良好的命名习惯对于编写高质量的代码至关重要。请确保你所选择的变量名既简洁明了,又具有描述性,并且遵循项目规范。这将使得其他开发者能够更容易地理解和维护你的代码。


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

Android补间动画

帧动画是通过连续播放图片来模拟动画效果,而补间动画开发者只需指定动画开始,以及动画结束"关键帧",而动画变化的"中间帧"则由系统计算并补齐! 1.补间动画的分类和Interpolator Andoird所支持的补间动画效果有如下这五种,或者说四种吧,第五种是...
继续阅读 »

帧动画是通过连续播放图片来模拟动画效果,而补间动画开发者只需指定动画开始,以及动画结束"关键帧",而动画变化的"中间帧"则由系统计算并补齐!



1.补间动画的分类和Interpolator


Andoird所支持的补间动画效果有如下这五种,或者说四种吧,第五种是前面几种的组合而已。




  • AlphaAnimation: 透明度渐变效果,创建时许指定开始以及结束透明度,还有动画的持续时间,透明度的变化范围(0,1),0是完全透明,1是完全不透明;对应<alpha/>标签!

  • ScaleAnimation:缩放渐变效果,创建时需指定开始以及结束的缩放比,以及缩放参考点,还有动画的持续时间;对应<scale/>标签!

  • TranslateAnimation:位移渐变效果,创建时指定起始以及结束位置,并指定动画的持续时间即可;对应<translate/>标签!

  • RotateAnimation:旋转渐变效果,创建时指定动画起始以及结束的旋转角度,以及动画持续时间和旋转的轴心;对应<rotate/>标签

  • AnimationSet:组合渐变,就是前面多种渐变的组合,对应<set/>标签



在开始讲解各种动画的用法之前,我们先要来讲解一个东西:Interpolator


用来控制动画的变化速度,可以理解成动画渲染器,当然我们也可以自己实现Interpolator接口,自行来控制动画的变化速度,而Android中已经为我们提供了五个可供选择的实现类:



  • LinearInterpolator:动画以均匀的速度改变

  • AccelerateInterpolator:在动画开始的地方改变速度较慢,然后开始加速

  • AccelerateDecelerateInterpolator:在动画开始、结束的地方改变速度较慢,中间时加速

  • CycleInterpolator:动画循环播放特定次数,变化速度按正弦曲线改变:Math.sin(2 * mCycles * Math.PI * input)

  • DecelerateInterpolator:在动画开始的地方改变速度较快,然后开始减速

  • AnticipateInterpolator:反向,先向相反方向改变一段再加速播放

  • AnticipateOvershootInterpolator:开始的时候向后然后向前甩一定值后返回最后的值

  • BounceInterpolator: 跳跃,快到目的值时值会跳跃,如目的值100,后面的值可能依次为85,77,70,80,90,100

  • OvershottInterpolator:回弹,最后超出目的值然后缓慢改变到目的值


2.各种动画的详细讲解


这里的android:duration都是动画的持续时间,单位是毫秒


1)AlphaAnimation(透明度渐变)


anim_alpha.xml


<alpha xmlns:android="http://schemas.android.com/apk/res/android"  
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:fromAlpha="1.0"
android:toAlpha="0.1"
android:duration="2000"/>

属性解释:


fromAlpha :起始透明度toAlpha:结束透明度透明度的范围为:0-1,完全透明-完全不透明


2)ScaleAnimation(缩放渐变)


anim_scale.xml


<scale xmlns:android="http://schemas.android.com/apk/res/android"  
android:interpolator="@android:anim/accelerate_interpolator"
android:fromXScale="0.2"
android:toXScale="1.5"
android:fromYScale="0.2"
android:toYScale="1.5"
android:pivotX="50%"
android:pivotY="50%"
android:duration="2000"/>

属性解释:




  • fromXScale/fromYScale:沿着X轴/Y轴缩放的起始比例

  • toXScale/toYScale:沿着X轴/Y轴缩放的结束比例

  • pivotX/pivotY:缩放的中轴点X/Y坐标,即距离自身左边缘的位置,比如50%就是以图像的中心为中轴点



3)TranslateAnimation(位移渐变)


anim_translate.xml


<translate xmlns:android="http://schemas.android.com/apk/res/android"  
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:fromXDelta="0"
android:toXDelta="320"
android:fromYDelta="0"
android:toYDelta="0"
android:duration="2000"/>

属性解释:




  • fromXDelta/fromYDelta:动画起始位置的X/Y坐标

  • toXDelta/toYDelta:动画结束位置的X/Y坐标



4)RotateAnimation(旋转渐变)


anim_rotate.xml


<rotate xmlns:android="http://schemas.android.com/apk/res/android"  
android:interpolator="@android:anim/accelerate_decelerate_interpolator"
android:fromDegrees="0"
android:toDegrees="360"
android:duration="1000"
android:repeatCount="1"
android:repeatMode="reverse"/>

属性解释:




  • fromDegrees/toDegrees:旋转的起始/结束角度

  • repeatCount:旋转的次数,默认值为0,代表一次,假如是其他值,比如3,则旋转4次另外,值为-1或者infinite时,表示动画永不停止

  • repeatMode:设置重复模式,默认restart,但只有当repeatCount大于0或者infinite或-1时才有效。还可以设置成reverse,表示偶数次显示动画时会做方向相反的运动!



5)AnimationSet(组合渐变)


非常简单,就是前面几个动画组合到一起而已


anim_set.xml


<set xmlns:android="http://schemas.android.com/apk/res/android"  
android:interpolator="@android:anim/decelerate_interpolator"
android:shareInterpolator="true" >

<scale
android:duration="2000"
android:fromXScale="0.2"
android:fromYScale="0.2"
android:pivotX="50%"
android:pivotY="50%"
android:toXScale="1.5"
android:toYScale="1.5" />

<rotate
android:duration="1000"
android:fromDegrees="0"
android:repeatCount="1"
android:repeatMode="reverse"
android:toDegrees="360" />

<translate
android:duration="2000"
android:fromXDelta="0"
android:fromYDelta="0"
android:toXDelta="320"
android:toYDelta="0" />

<alpha
android:duration="2000"
android:fromAlpha="1.0"
android:toAlpha="0.1" />

</set>

3.写个例子来体验下


好的,下面我们就用上面写的动画来写一个例子,让我们体会体会何为补间动画:首先来个简单的布局:activity_main.xml


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<Button
android:id="@+id/btn_alpha"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="透明度渐变" />

<Button
android:id="@+id/btn_scale"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="缩放渐变" />

<Button
android:id="@+id/btn_tran"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="位移渐变" />

<Button
android:id="@+id/btn_rotate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="旋转渐变" />

<Button
android:id="@+id/btn_set"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="组合渐变" />

<ImageView
android:id="@+id/img_show"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="48dp"
android:src="@mipmap/img_face" />

</LinearLayout>

好哒,接着到我们的MainActivity.java,同样非常简单,只需调用AnimationUtils.loadAnimation()加载动画,然后我们的View控件调用startAnimation开启动画即可。


public class MainActivity extends AppCompatActivity implements View.OnClickListener{

private Button btn_alpha;
private Button btn_scale;
private Button btn_tran;
private Button btn_rotate;
private Button btn_set;
private ImageView img_show;
private Animation animation = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bindViews();
}

private void bindViews() {
btn_alpha = (Button) findViewById(R.id.btn_alpha);
btn_scale = (Button) findViewById(R.id.btn_scale);
btn_tran = (Button) findViewById(R.id.btn_tran);
btn_rotate = (Button) findViewById(R.id.btn_rotate);
btn_set = (Button) findViewById(R.id.btn_set);
img_show = (ImageView) findViewById(R.id.img_show);

btn_alpha.setOnClickListener(this);
btn_scale.setOnClickListener(this);
btn_tran.setOnClickListener(this);
btn_rotate.setOnClickListener(this);
btn_set.setOnClickListener(this);

}

@Override
public void onClick(View v) {
switch (v.getId()){
case R.id.btn_alpha:
animation = AnimationUtils.loadAnimation(this,
R.anim.anim_alpha);
img_show.startAnimation(animation);
break;
case R.id.btn_scale:
animation = AnimationUtils.loadAnimation(this,
R.anim.anim_scale);
img_show.startAnimation(animation);
break;
case R.id.btn_tran:
animation = AnimationUtils.loadAnimation(this,
R.anim.anim_translate);
img_show.startAnimation(animation);
break;
case R.id.btn_rotate:
animation = AnimationUtils.loadAnimation(this,
R.anim.anim_rotate);
img_show.startAnimation(animation);
break;
case R.id.btn_set:
animation = AnimationUtils.loadAnimation(this,
R.anim.anim_set);
img_show.startAnimation(animation);
break;
}
}
}

运行效果图



有点意思是吧,还不动手试试,改点东西,或者自由组合动画,做出酷炫的效果吧。


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

Swift快速集成环信IM iOS SDK并实现单聊

本文介绍如何使用swift快速集成环信即时通讯 IM iOS SDK 实现单聊。前提条件• Xcode (推荐最新版本)。• 安装 iOS 10.0 或更高版本的 iOS 模拟器或 Apple 设备。• CocoaPods 1.10.1 或更高版本。• 有效的...
继续阅读 »

本文介绍如何使用swift快速集成环信即时通讯 IM iOS SDK 实现单聊。

前提条件
• Xcode (推荐最新版本)。
• 安装 iOS 10.0 或更高版本的 iOS 模拟器或 Apple 设备。
• CocoaPods 1.10.1 或更高版本。
• 有效的环信即时通讯 IM 开发者账号(注册环信账号:https://console.easemob.com/user/register)和 App Key,见 环信即时通讯云管理后台(https://console.easemob.com/user/login)。

• 如果你的网络环境部署了防火墙,请联系环信技术支持设置白名单。

集成方式
使用CocoaPods来添加环信SDK,具体步骤如下:

platform :ios, ‘10.0’
use_frameworks!

target ‘YourTarget’ do
pod ‘HyphenateChat’, ‘~> 4.0.2’
end

然后在终端中运行pod install,即可将环信SDK添加到项目中。

因为环信sdk是OC的代码,所以需要创建桥接文件(Bridging Header)来让Swift可以调用Objective-C的代码和库。下面是创建桥接文件的步骤:

1. 创建桥接文件
在Xcode项目中,选择File -> New -> File…,在弹出的对话框中选择iOS -> Source -> Header File,然后给该文件起一个名字,例如YourProjectName-Bridging-Header.h

2.配置桥接文件选项
在桥接文件的属性中,设置Objective-C Bridging Header选项。具体操作如下:
• 选中项目,在Xcode菜单中选择Build Settings
• 在搜索框中输入bridging header,找到Objective-C bridges Header选项
• 双击该选项,然后在弹出的对话框中输入桥接文件的路径,例如$(SRCROOT)/YourProjectName/YourProjectName-Bridging-Header.h

3.导入Objective-C头文件

// YourProjectName-Bridging-Header.h
#import


4.初始化环信SDK
在AppDelegate.swift文件中的application(_:didFinishLaunchingWithOptions:)方法中初始化环信SDK。以下是初始化代码示例:

let options = EMOptions(appkey: "yourappkey#demo")
let error = EMClient.shared().initializeSDK(with: options)
if error == nil {
//初始化成功
} else {
//初始化失败
}


5.登录环信服务器

注册服务端账号:http://docs-im-beta.easemob.com/document/server-side/account_system.html

EMClient.shared().login(withUsername: "yourUsername", password: "yourPassword") { (aUserName, aError) in
if aError != nil {
//登录失败处理
print("\(aUserName) login fail")
}else {
//登录成功处理
print("\(aUserName) login success")
}
}


6.发送消息

初始化聊天页面文档链接:http://docs-im-beta.easemob.com/document/ios/quickstart.html#_4-

let chatText = "Hello, World!"
let message = EMChatMessage(conversationID: "yourConversationID", from: "yourFrom", to: "yourTo", body: EMTextMessageBody(text: chatText), ext: ["yourKey": "yourValue"])
message.chatType = EMChatTypeChat // 设置为单聊消息
EMClient.shared().chatManager?.send(message, progress: nil) { (aMessage, aError) in
if let error = aError {
// 发送失败处理
} else {
// 发送成功处理
}
}

至此,即时通讯的基本功能已经集成完,如果您在集成中遇到问题可以随时联系环信技术支持或IMGeek社区提问。


SDK地址:https://www.easemob.com/download/im
IMGeek社区:https://www.imgeek.net/

收起阅读 »

launchAnyWhere: Activity组件权限绕过漏洞解析

前言 今年3月份,知名反病毒软件公司卡巴斯基实验室发布了一份关于中国电商平台拼多多的调查报告,称该平台的安装程序中含有恶意代码。这一消息引起了广泛的关注和讨论,也引发了人们对于拼多多平台安全性的担忧 作为技术开发人员,我看到了PDD对安卓OEM源码中的漏洞的...
继续阅读 »

前言


Screenshot_20230414163441298_com.ss.android.article.newsedit.jpg


今年3月份,知名反病毒软件公司卡巴斯基实验室发布了一份关于中国电商平台拼多多的调查报告,称该平台的安装程序中含有恶意代码。这一消息引起了广泛的关注和讨论,也引发了人们对于拼多多平台安全性的担忧


作为技术开发人员,我看到了PDD对安卓OEM源码中的漏洞的深入研究。



了解和学习Android漏洞原理有以下几个用处:





  • 提高应用安全性:通过了解漏洞原理,开发者可以更好地了解漏洞的产生机理,进而在应用开发过程中采取相应的安全措施,避免漏洞的产生,提高应用的安全性。




  • 提升应用质量:学习漏洞原理可以帮助开发者更好地理解 Android平台的工作原理,深入了解操作系统的内部机制,有助于开发高质量的应用程序。




  • 改善代码风格:学习漏洞原理可以帮助开发者更好地理解代码的运行方式和效果,从而提高代码的可读性和可维护性。




  • 了解安全防护技术:学习漏洞原理可以帮助开发者了解目前主流的安全防护技术,掌握安全防护的最佳实践,从而更好地保障应用程序的安全性。




总之,了解和学习Android漏洞原理可以帮助开发者更好地理解操作系统的内部机制,提高应用程序的安全性、质量和可维护性。


LaunchAnyWhere漏洞


这是一个AccountManagerService的漏洞,利用这个漏洞,我们可以任意调起任意未导出的Activity,突破进程间组件访问隔离的限制。这个漏洞影响2.3 ~ 4.3的安卓系统。



有些同学看到这里或许有些疑问,这个漏洞不是在Android4.3以后被解决了么?我想要说的是要了解startAnyWhere就需要了解它的历史,而LaunchAnyWhere漏洞可以说是它的一部分历史。



普通应用(记为AppA)去请求添加某类账户时,会调用AccountManager.addAccount,然后AccountManager会去查找提供账号的应用(记为AppB)的Authenticator类,调用Authenticator. addAccount方法;AppA再根据AppB返回的Intent去调起AppB的账户登录界面。


关于AccountManagerService


AccountManagerService同样也是系统服务之一,暴露给开发者的的接口是AccountManager。该服务用于管理用户各种网络账号。这使得一些应用可以获取用户网络账号的token,并且使用token调用一些网络服务。很多应用都提供了账号授权功能,比如微信、支付宝、邮件Google服务等等。


关于AccountManager的使用,可以参考Launchanywhere的Demo:github.com/stven0king/…


由于各家账户的登陆方法和token获取机制肯定存在差异,所以AccountManager的身份验证也被设计成可插件化的形式:由提供账号相关的应用去实现账号认证。提供账号的应用可以自己实现一套登陆UI,接收用户名和密码;请求自己的认证服务器返回一个token;将token缓存给AccountManager


可以从“设置-> 添加账户”中看到系统内可提供网络账户的应用:


添加账户页面.png


如果应用想要出现在这个页面里,应用需要声明一个账户认证服务AuthenticationService


<service android:name=".AuthenticationService"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="android.accounts.AccountAuthenticator" />
</intent-filter>
<meta-data
android:name="android.accounts.AccountAuthenticator"
android:resource="@xml/authenticator" />
</service>

并在服务中提供一个Binder:


public class AuthenticationService extends Service {
private AuthenticationService.AccountAuthenticator mAuthenticator;
private AuthenticationService.AccountAuthenticator getAuthenticator() {
if (mAuthenticator == null)
mAuthenticator = new AuthenticationService.AccountAuthenticator(this);
return mAuthenticator;
}
@Override
public void onCreate() {
mAuthenticator = new AuthenticationService.AccountAuthenticator(this);
}
@Override
public IBinder onBind(Intent intent) {
Log.d("tanzhenxing33", "onBind");
return getAuthenticator().getIBinder();
}
static class AccountAuthenticator extends AbstractAccountAuthenticator {
/****部分代码省略****/
@Override
public Bundle addAccount(AccountAuthenticatorResponse response, String accountType, String authTokenType, String[] requiredFeatures, Bundle options) throws NetworkErrorException {
Log.d("tanzhenxing33", "addAccount: ");
return testBundle();
}
}
}

声明账号信息:authenticator.xml


<?xml version="1.0" encoding="utf-8"?>
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="com.tzx.launchanywhere"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
</account-authenticator>

漏洞原理


普通应用(记为AppA)去请求添加某类账户时,会调用AccountManager.addAccount,然后AccountManager会去查找提供账号的应用(记为AppB)的Authenticator类,调用Authenticator.addAccount方法;AppA再根据AppB返回的Intent去调起AppB的账户登录界面。


这个过程如图所示:


launchanywhere.png


我们可以将这个流程转化为一个比较简单的事实:



  • AppA请求添加一个特定类型的网络账号

  • 系统查询到AppB可以提供一个该类型的网络账号服务,系统向AppB发起请求

  • AppB返回了一个intent给系统,系统把intent转发给appA

  • AccountManagerResponse在AppA的进程空间内调用 startActivity(intent)调起一个Activity;

  • AccountManagerResponse是FrameWork中的代码, AppA对这一调用毫不知情。


这种设计的本意是,AccountManagerService帮助AppA查找到AppB账号登陆页面,并呼起这个登陆页面。而问题在于,AppB可以任意指定这个intent所指向的组件,AppA将在不知情的情况下由AccountManagerResponse调用起了一个Activity. 如果AppA是一个system权限应用,比如Settings,那么AppA能够调用起任意AppB指定的未导出Activity


如何利用


上文已经提到过,如果假设AppA是Settings,AppB是攻击程序。那么只要能让Settings触发addAcount的操作,就能够让AppB launchAnyWhere。而问题是,怎么才能让Settings触发添加账户呢?如果从“设置->添加账户”的页面去触发,则需要用户手工点击才能触发,这样攻击的成功率将大大降低,因为一般用户是很少从这里添加账户的,用户往往习惯直接从应用本身登陆。
不过现在就放弃还太早,其实Settings早已经给我们留下触发接口。只要我们调用com.android.settings.accounts.AddAccountSettings,并给Intent带上特定的参数,即可让``Settings触发launchAnyWhere


Intent intent1 = new Intent();
intent1.setComponent(new ComponentName("com.android.settings", "com.android.settings.accounts.AddAccountSettings"));
intent1.setAction(Intent.ACTION_RUN);
intent1.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
String authTypes[] = {"自己的账号类型"};
intent1.putExtra("account_types", authTypes);
AuthenticatorActivity.this.startActivity(intent1);

这个过程如图Step 0所示:


launchanywhere2.png


应用场景


主要的攻击对象还是应用中未导出的Activity,特别是包含了一些intenExtraActivity。下面只是举一些简单例子。这个漏洞的危害取决于你想攻击哪个Activity,还是有一定利用空间的。比如攻击很多app未导出的webview,结合FakeID或者JavascriptInterface这类的浏览器漏洞就能造成代码注入执行。


重置pin码



  • 绕过pin码认证界面,直接重置手机系统pin码。


intent.setComponent(new ComponentName("com.android.settings","com.android.settings.ChooseLockPassword"));
intent.setAction(Intent.ACTION_RUN);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra("confirm_credentials",false);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;

重置锁屏


绕过原有的锁屏校验,直接重置手机的锁屏密码。


Intent intent = new Intent();
intent.setComponent(new ComponentName("com.android.settings", "com.android.settings.ChooseLockPattern"));
intent.setAction(Intent.ACTION_RUN);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final Bundle bundle = new Bundle();
bundle.putParcelable(AccountManager.KEY_INTENT, intent);
return bundle;

漏洞修复


安卓4.4已经修复了这个漏洞,检查了Step3中返回的intent所指向的Activity和AppB是否是有相同签名的。避免了luanchAnyWhere的可能。
Android4.3源代码:androidxref.com/4.3_r2.1/xr…
Android4.4源代码:androidxref.com/4.4_r1/xref…
官网漏洞修复的Diff:android.googlesource.com/platform/fr…


diffcode.png


文章到这里就全部讲述完啦,若有其他需要交流的可以留言哦~!


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

Android - 统一依赖管理(config.gradle)

前言 本文属于 《一款基于MVP架构的快速应用开发框架,kotlin版本》(注:此文章还在更新中,可先看看,敬请期待!) 的扩展文章,详细介绍在使用 LeoFastDevMvpKotlin 快速开发框架的时候,进行项目依赖管理的方法。 介绍 Android 依...
继续阅读 »

前言


本文属于 《一款基于MVP架构的快速应用开发框架,kotlin版本》(注:此文章还在更新中,可先看看,敬请期待!) 的扩展文章,详细介绍在使用 LeoFastDevMvpKotlin 快速开发框架的时候,进行项目依赖管理的方法。


介绍


Android 依赖统一管理距目前为止,博主一共知道有三种方法,分别是:




  1. 传统apply from的方式(也是本文想讲的一种方式):新建一个 「config.gradle」 文件,然后将项目中所有依赖写在里面,更新只需修改 「config.gradle」 文件内容,作用于所有module。

  2. buildSrc 方式:当运行 Gradle 时会检查项目中是否存在一个名为 buildSrc 的目录。然后 Gradle 会自动编译并测试这段代码,并将其放入构建脚本的类路径中, 对于多项目构建,只能有一个 buildSrc 目录,该目录必须位于根项目目录中, buildSrc 是 Gradle 项目根目录下的一个目录。

  3. Composing builds 方式:复合构建只是包含其他构建的构建. 在许多方面,复合构建类似于 Gradle 多项目构建,不同之处在于,它包括完整的 builds ,而不是包含单个 projects,总的来说,他有 buildSrc 方式的优点,同时更新不需要重新构建整个项目。



三种方式各有各的好,目前最完美的应该是第三种实现。但是这种方式不利于框架使用,因为它属于的是新建一个module,如果项目远程依赖了框架,默认也包含了这个 module。所以博主选择了第一种方式。以下文章也是围绕第一种方式进行讲解。


实现方式


实现这个统一依赖管理,拢共分三步,分别是:




  • 第一步:创建「config.gradle」 文件

  • 第二步:项目当中引入「config.gradle」

  • 第三步:在所有module的「build.gradle」当中添加依赖





  • 第一步:创建 「config.gradle」 文件


    首先将 Aandroid Studio 目录的Android格式修改为Project,然后再创建一个「config.gradle」的文件


    1681962514751.jpg


    然后我们编辑文章里面的内容,这里直接给出框架的代码出来(篇幅太长,省略部分代码):


    ext {
    /**
    * 基础配置 对应 build.gradle 当中 android 括号里面的值
    */
    android = [
    compileSdk : 32,
    minSdk : 21,
    targetSdk : 32,
    versionCode : 1,
    versionName : "1.0.0",
    testInstrumentationRunner: "androidx.test.runner.AndroidJUnitRunner",
    consumerProguardFiles : "consumer-rules.pro"

    ......
    ]

    /**
    * 版本号 包含每一个依赖的版本号,仅仅作用于下面的 dependencies
    */
    version = [
    coreKtx : "1.7.0",
    appcompat : "1.6.1",
    material : "1.8.0",
    constraintLayout : "2.1.3",
    navigationFragmentKtx: "2.3.5",
    navigationUiKtx : "2.3.5",
    junit : "4.13.2",
    testJunit : "1.1.5",
    espresso : "3.4.0",

    ......
    ]

    /**
    * 项目依赖 可根据项目增加删除,但是可不删除本文件里的,在 build.gradle 不写依赖即可
    * 因为MVP框架默认依赖的也在次文件中,建议只添加,不要删除
    */
    dependencies = [

    coreKtx : "androidx.core:core-ktx:$version.coreKtx",
    appcompat : "androidx.appcompat:appcompat:$version.appcompat",
    material : "com.google.android.material:material:$version.material",
    constraintLayout : "androidx.constraintlayout:constraintlayout:$version.constraintLayout",
    navigationFragmentKtx : "androidx.navigation:navigation-fragment-ktx:$version.navigationFragmentKtx",
    navigationUiKtx : "androidx.navigation:navigation-ui-ktx:$version.navigationUiKtx",
    junit : "junit:junit:$version.junit",
    testJunit : "androidx.test.ext:junit:$version.testJunit",
    espresso : "androidx.test.espresso:espresso-core:$version.espresso",

    ......
    ]
    }

    简单理解就是将所有的依赖,分成版本号以及依赖名两个数组的方式保存,所有都在这个文件统一管管理。用 ext 包裹三个数组:第一个是「build.gradle」Android 里面的,第二个是版本号,第三个是依赖的名字。依赖名字数组里面的依赖版本号通过 $ 关键字指代 version 数组里面的版本号




  • 第二步:项目当中引入 「config.gradle」


    将「config.gradle」文件引入项目当中,在项目的根目录的「build.gradle」文件(也就是刚刚新建的 「config.gradle」同目录下的),添加如下代码:


    apply from:"config.gradle"

    需要注意的的是,如果你是 AndroidStudio 4.0+ 那么你将看到这样的「build.gradle」文件


    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    plugins {
    id 'com.android.application' version '7.2.2' apply false
    id 'com.android.library' version '7.2.2' apply false
    id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
    }

    apply from:"config.gradle"

    相反,如果你是 AndroidStudio 4.0- 那么你将会看到这样的「build.gradle」文件



    apply from: "config.gradle"

    buildscript {
    ext.kotlin_version="1.7.10"
    repositories {
    maven { url "https://jitpack.io" }
    mavenCentral()
    google()
    }
    dependencies {
    classpath "com.android.tools.build:gradle:4.2.1"
    classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
    }
    }

    allprojects {
    repositories {
    maven { url "https://jitpack.io" }
    mavenCentral()
    google()
    }
    }

    task clean(type: Delete) {
    delete rootProject.buildDir
    }

    不过仅仅是两个文件里面的内容不一致,这个文件的位置是一样的,而且我们添加的引入代码也是一样的。可以说,这只是顺带提一嘴,实际上不影响我们实现统一依赖管理这个方式。




  • 第三步:在所有module的「build.gradle」当中添加依赖


    这一步是最重要的,我们完成了上面两步之后,只是做好了准备,现在我们需要将我们每一个module里面「build.gradle」文件里面的依赖指向「config.gradle」文件。也就是下图圈起来的 那两个「build.gradle」文件。


    Snipaste_2023-04-20_14-15-58.png


    因为我们第二步的时候已经在根目录引入了「config.gradle」,所以我们在「build.gradle」就可以指向「config.gradle」例如:



    implementation rootProject.ext.dependencies.coreKtx



    这一行,就指代了我们「config.gradle」文件里面的 dependencies 数组里面的 coreKtx 的内容。完整示例如下:


    plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    }
    android {
    namespace 'leo.dev.mvp.kt'
    // compileSdk 32
    compileSdk rootProject.ext.android.compileSdk

    defaultConfig {
    applicationId "leo.dev.mvp.kt"
    // minSdk 21
    // targetSdk 32
    // versionCode 1
    // versionName "1.0"
    //
    // testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

    minSdk rootProject.ext.android.minSdk
    targetSdk rootProject.ext.android.targetSdk
    versionCode rootProject.ext.android.versionCode
    versionName rootProject.ext.android.versionName

    testInstrumentationRunner rootProject.ext.android.testInstrumentationRunner

    }

    ......
    }

    dependencies {

    implementation fileTree(include: ['*.jar'], dir: 'libs')

    // implementation 'androidx.core:core-ktx:1.7.0'
    // implementation 'androidx.appcompat:appcompat:1.6.1'
    // implementation
    //
    // testImplementation 'junit:junit:4.13.2'
    // androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    // androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

    implementation rootProject.ext.dependencies.coreKtx
    implementation rootProject.ext.dependencies.appcompat
    implementation rootProject.ext.dependencies.material

    testImplementation rootProject.ext.dependencies.junit
    androidTestImplementation rootProject.ext.dependencies.testJunit
    androidTestImplementation rootProject.ext.dependencies.espresso

    }

    需要注意的是,我们在编写代码的时候,是没有代码自动补全的。所以得小心翼翼,必须要和「config.gradle」文件里面的名字向一致。




注意事项



  • 首先就是这种方式在coding的时候,是没有代码补全的(只有输入过的,才会有提示),我们需要确保我们的名字一致

  • 我们在增加依赖的时候,在「config.gradle」里面添加完之后,记得在对应的module里面的「build.gradle」里面添加对应的指向代码。


总结


以上就是本篇文章的全部内容,总结起来其实步骤不多,也就三步。但是需要注意的是细节。需要保持写入的依赖与「config.gradle」文件一致,并且未写过的词,是不会有代码自动补全的。


另外本篇文章是本文属于 《一款基于MVP架构的快速应用开发框架,kotlin版本》 的扩展文章,所以会一步一步说得比较详细一点。大家可以挑重点跳过阅读。


抬头图片


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

Android大图预览

前言 加载高清大图时,往往会有不能缩放和分段加载的需求出现。本文将就BitmapRegionDecoder和subsampling-scale-image-view的使用总结一下Bitmap的分区域解码。 定义 假设现在有一张这样的图片,尺寸为3040 × ...
继续阅读 »

前言


加载高清大图时,往往会有不能缩放分段加载的需求出现。本文将就BitmapRegionDecodersubsampling-scale-image-view的使用总结一下Bitmap的分区域解码


定义


image.png


假设现在有一张这样的图片,尺寸为3040 × 1280。如果按需要完全显示在ImageView上的话就必须进行压缩处理。当需求要求是不能缩放的时候,就需要进行分段查看了。由于像这种尺寸大小的图片在加载到内存后容易造成OOM,所以需要进行区域解码


图中红框的部分就是需要区域解码的部分,即每次只有进行红框区域大小的解码,在需要看其余部分时可以通过如拖动等手势来移动红框区域,达到查看全图的目的。


BitmapRegionDecoder


Android原生提供了BitmapRegionDecoder用于实现Bitmap的区域解码,简单使用的api如下:


// 根据图片文件的InputStream创建BitmapRegionDecoder
val decoder = BitmapRegionDecoder.newInstance(inputStream, false)

val option: BitmapFactory.Options = BitmapFactory.Options()
val rect: Rect = Rect(0, 0, 100, 100)

// rect制定的区域即为需要区域解码的区域
decoder.decodeRegion(rect, option)


  • 通过BitmapRegionDecoder.newInstance可以根据图片文件的InputStream对象创建一个BitmapRegionDecoder对象。

  • decodeRegion方法传入一个Rect和一个BitmapFactory.Options,前者用于规定解码区域,后者用于配置Bitmap,如inSampleSize、inPreferredConfig等。ps:解码区域必须在图片宽高范围内,否则会出现崩溃。


区域解码与全图解码


通过区域解码得到的Bitmap,宽高和占用内存只是指定区域的图像所需要的大小


譬如按1080 × 1037区域大小加载,可以查看Bitmap的allocationByteCount为4479840,即1080 * 1037 * 4


image.png


若直接按全图解码,allocationByteCount为15564800,即3040 * 1280 * 4
image.png


可以看到,区域解码的好处是图像不会完整的被加载到内存中,而是按需加载了。


自定义一个图片查看的View


由于BitmapRegionDecoder只是实现区域解码,如果改变这个区域还是需要开发者通过具体交互实现。这里用触摸事件简单实现了一个自定义View。由于只是简单依赖触摸事件,滑动的灵敏度还是偏高,实际开发可以实现一些自定义的拖拽工具来进行辅助。代码比较简单,可参考注释。


class RegionImageView @JvmOverloads constructor(
context: Context,
attr: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attr, defStyleAttr) {

private var decoder: BitmapRegionDecoder? = null
private val option: BitmapFactory.Options = BitmapFactory.Options()
private val rect: Rect = Rect()

private var lastX: Float = -1f
private var lastY: Float = -1f

fun setImage(fileName: String) {
val inputStream = context.assets.open(fileName)
try {
this.decoder = BitmapRegionDecoder.newInstance(inputStream, false)

// 触发onMeasure,用于更新Rect的初始值
requestLayout()
} catch (e: Exception) {
e.printStackTrace()
} finally {
inputStream.close()
}
}

override fun onTouchEvent(event: MotionEvent?): Boolean {
when (event?.action) {
MotionEvent.ACTION_DOWN -> {
this.decoder ?: return false
this.lastX = event.x
this.lastY = event.y
return true
}
MotionEvent.ACTION_MOVE -> {
val decoder = this.decoder ?: return false
val dx = event.x - this.lastX
val dy = event.y - this.lastY

// 每次MOVE事件根据前后差值对Rect进行更新,需要注意不能超过图片的实际宽高
if (decoder.width > width) {
this.rect.offset(-dx.toInt(), 0)
if (this.rect.right > decoder.width) {
this.rect.right = decoder.width
this.rect.left = decoder.width - width
} else if (this.rect.left < 0) {
this.rect.right = width
this.rect.left = 0
}
invalidate()
}
if (decoder.height > height) {
this.rect.offset(0, -dy.toInt())
if (this.rect.bottom > decoder.height) {
this.rect.bottom = decoder.height
this.rect.top = decoder.height - height
} else if (this.rect.top < 0) {
this.rect.bottom = height
this.rect.top = 0
}
invalidate()
}
}
MotionEvent.ACTION_UP -> {
this.lastX = -1f
this.lastY = -1f
}
else -> {

}
}

return super.onTouchEvent(event)
}

// 测量后默认第一次加载的区域是从0开始到控件的宽高大小
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)

val w = MeasureSpec.getSize(widthMeasureSpec)
val h = MeasureSpec.getSize(heightMeasureSpec)

this.rect.left = 0
this.rect.top = 0
this.rect.right = w
this.rect.bottom = h
}

// 每次绘制时,通过BitmapRegionDecoder解码出当前区域的Bitmap
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.let {
val bitmap = this.decoder?.decodeRegion(rect, option) ?: return
it.drawBitmap(bitmap, 0f, 0f, null)
}
}
}

SubsamplingScaleImageView


davemorrissey/subsampling-scale-image-view可以用于加载超大尺寸的图片,避免大内存导致的OOM,内部依赖的也是BitmapRegionDecoder。好处是SubsamplingScaleImageView已经帮我们实现了相关的手势如拖动、缩放,内部还实现了二次采样和区块显示的逻辑。


如果需要加载assets目录下的图片,可以这样调用


subsamplingScaleImageView.setImage(ImageSource.asset("sample1.jpeg"))

public final class ImageSource {

static final String FILE_SCHEME = "file:///";
static final String ASSET_SCHEME = "file:///android_asset/";

private final Uri uri;
private final Bitmap bitmap;
private final Integer resource;
private boolean tile;
private int sWidth;
private int sHeight;
private Rect sRegion;
private boolean cached;

ImageSource是对图片资源信息的抽象



  • uri、bitmap、resource分别指代图像来源是文件、解析好的Bitmap对象还是resourceId。

  • tile:是否需要分片加载,一般以uri、resource形式加载的都会为true。

  • sWidth、sHeight、sRegion:加载图片的宽高和区域,一般可以指定加载图片的特定区域,而不是全图加载

  • cached:控制重置时,是否需要recycle掉Bitmap


public final void setImage(@NonNull ImageSource imageSource, ImageSource previewSource, ImageViewState state) {
...

if (imageSource.getBitmap() != null && imageSource.getSRegion() != null) {
...
} else if (imageSource.getBitmap() != null) {
...
} else {
sRegion = imageSource.getSRegion();
uri = imageSource.getUri();
if (uri == null && imageSource.getResource() != null) {
uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + getContext().getPackageName() + "/" + imageSource.getResource());
}
if (imageSource.getTile() || sRegion != null) {
// Load the bitmap using tile decoding.
TilesInitTask task = new TilesInitTask(this, getContext(), regionDecoderFactory, uri);
execute(task);
} else {
// Load the bitmap as a single image.
BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);
execute(task);
}
}
}

由于在我们的调用下,tile为true,setImage方法最后会走到一个TilesInitTask当中。是一个AsyncTask。ps:该库中的多线程异步操作都是通过AsyncTask封装的。


// TilesInitTask
@Override
protected int[] doInBackground(Void... params) {
try {
String sourceUri = source.toString();
Context context = contextRef.get();
DecoderFactory<? extends ImageRegionDecoder> decoderFactory = decoderFactoryRef.get();
SubsamplingScaleImageView view = viewRef.get();
if (context != null && decoderFactory != null && view != null) {
view.debug("TilesInitTask.doInBackground");
decoder = decoderFactory.make();
Point dimensions = decoder.init(context, source);
int sWidth = dimensions.x;
int sHeight = dimensions.y;
int exifOrientation = view.getExifOrientation(context, sourceUri);
if (view.sRegion != null) {
view.sRegion.left = Math.max(0, view.sRegion.left);
view.sRegion.top = Math.max(0, view.sRegion.top);
view.sRegion.right = Math.min(sWidth, view.sRegion.right);
view.sRegion.bottom = Math.min(sHeight, view.sRegion.bottom);
sWidth = view.sRegion.width();
sHeight = view.sRegion.height();
}
return new int[] { sWidth, sHeight, exifOrientation };
}
} catch (Exception e) {
Log.e(TAG, "Failed to initialise bitmap decoder", e);
this.exception = e;
}
return null;
}

@Override
protected void onPostExecute(int[] xyo) {
final SubsamplingScaleImageView view = viewRef.get();
if (view != null) {
if (decoder != null && xyo != null && xyo.length == 3) {
view.onTilesInited(decoder, xyo[0], xyo[1], xyo[2]);
} else if (exception != null && view.onImageEventListener != null) {
view.onImageEventListener.onImageLoadError(exception);
}
}
}

TilesInitTask主要的操作是创建一个SkiaImageRegionDecoder,它主要的作用是封装BitmapRegionDecoder。通过BitmapRegionDecoder获取图片的具体宽高和在Exif中获取图片的方向,便于显示调整。


后续会在onDraw时调用initialiseBaseLayer方法进行图片的加载操作,这里会根据比例计算出采样率来决定是否需要区域解码还是全图解码。值得一提的是,当采样率为1,图片宽高小于Canvas的getMaximumBitmapWidth()getMaximumBitmapHeight()时,也是会直接进行全图解码的。这里调用的TileLoadTask就是使用BitmapRegionDecoder进行解码的操作。


ps:Tile对象为区域的抽象类型,内部会包含指定区域的Bitmap,在onDraw时会根据区域通过Matrix绘制到Canvas上。


private synchronized void initialiseBaseLayer(@NonNull Point maxTileDimensions) {
debug("initialiseBaseLayer maxTileDimensions=%dx%d", maxTileDimensions.x, maxTileDimensions.y);

satTemp = new ScaleAndTranslate(0f, new PointF(0, 0));
fitToBounds(true, satTemp);

// Load double resolution - next level will be split into four tiles and at the center all four are required,
// so don't bother with tiling until the next level 16 tiles are needed.
fullImageSampleSize = calculateInSampleSize(satTemp.scale);
if (fullImageSampleSize > 1) {
fullImageSampleSize /= 2;
}

if (fullImageSampleSize == 1 && sRegion == null && sWidth() < maxTileDimensions.x && sHeight() < maxTileDimensions.y) {
// Whole image is required at native resolution, and is smaller than the canvas max bitmap size.
// Use BitmapDecoder for better image support.
decoder.recycle();
decoder = null;
BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false);
execute(task);
} else {
initialiseTileMap(maxTileDimensions);

List<Tile> baseGrid = tileMap.get(fullImageSampleSize);
for (Tile baseTile : baseGrid) {
TileLoadTask task = new TileLoadTask(this, decoder, baseTile);
execute(task);
}
refreshRequiredTiles(true);

}

}

加载网络图片


BitmapRegionDecoder只能加载本地图片,而如果需要加载网络图片,可以结合Glide使用,以SubsamplingScaleImageView为例


Glide.with(this)
.asFile()
.load("")
.into(object : CustomTarget<File?>() {
override fun onResourceReady(resource: File, transition: Transition<in File?>?) {
subsamplingScaleImageView.setImage(ImageSource.uri(Uri.fromFile(resource)))
}

override fun onLoadCleared(placeholder: Drawable?) {}
})

可以通过CustomTarget获取到图片的File文件,然后再调用SubsamplingScaleImageView#setImage


最后


本文主要总结Bitmap的分区域解码,利用原生的BitmapRegionDecoder可实现区域解码,通过SubsamplingScaleImageView可以对BitmapRegionDecoder进行进一步的交互扩展和优化。如果需要是TV端开发可以参考这篇文章,里面有结合具体的TV端操作适配:Android实现TV端大图浏览


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

你可能需要了解下的Android开发技巧(一)

callbackFlow {}+debounce()降频 假如当前要做一个实时搜索的功能,监听输入框动态输入的内容向服务器发起搜索请求,这不仅会增大服务器的压力,而且也会产生很多的无用请求。 比如其实你想搜索一个“android”,但随着你在输入框中动态编辑,...
继续阅读 »

callbackFlow {}+debounce()降频


假如当前要做一个实时搜索的功能,监听输入框动态输入的内容向服务器发起搜索请求,这不仅会增大服务器的压力,而且也会产生很多的无用请求。


比如其实你想搜索一个“android”,但随着你在输入框中动态编辑,最多可能会向服务器发送7次请求,很明显前面6次请求都是属于无用请求(暂时不考虑模糊匹配的场景)。


这个时候我们就可以借助于callbackFlow{}将输入框的动态输入转换成流,再借助debounce()对流进行降频即可。关于对debounce()的讲解,可以参考之前的文章:debounce()限流


fun test4(editText: EditText) {
lifecycleScope.launchWhenResumed {
callbackFlow {
val watcher = editText.doAfterTextChanged {
trySend(it?.toString() ?: "")
}

invokeOnClose {
editText.removeTextChangedListener(watcher)
}
}.debounce(200).collect {
//对于输入框中的内容向服务器发起实时搜索请求

}
}
}

判断当前是否为主进程


常见的业务场景中,可能我们会把Service单独放一个进程处理,比如为了单独存放WebView再或者专门开一个服务进程与服务器进行通信,这样当UI进程死掉,也能缓存最新的数据到内容和本地 。


但有时,Service单独放一个进程处理,也会走Application的初始化逻辑,比如初始化第三方SDK、获取某些资源等等,但这些可能是只有UI进程才需要,所以Service进程初始化应该跳过这些逻辑。


所以我们需要判断当前的线程是否属于UI线程,可以利用UI进程的包名和进程名相同的特性实现,代码如下:


fun isMainProcess(): Boolean =
getSystemService<ActivityManager>()?.let {
it.runningAppProcesses.find { info ->
info.pid == Process.myPid()
}?.let { res ->
res.processName == packageName
}
} ?: true

当我写完上面的代码之后,发现Application竟然直接提供了一个获取当前进程名称的方法:


image.png


不过这个只有SDK28以上才能使用,可以判断一下,SDK28以下用上面的代码判断,SDK28及以上用下面的代码判断:


fun isMainProcess2(): Boolean = packageName == getProcessName()

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