注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

偏爱console.log的你,肯定会觉得这个插件泰裤辣!

web
前言 毋庸置疑,要说前端调试代码用的最多的,肯定是console.log,虽然我现在 debugger 用的比较多,但对于生产环境、小程序真机调试,还是需要用到 log 来查看变量值,比如我下午遇到个场景:选择完客户后返回页面,根据条件判断是否弹窗: if (...
继续阅读 »

前言


毋庸置疑,要说前端调试代码用的最多的,肯定是console.log,虽然我现在 debugger 用的比较多,但对于生产环境、小程序真机调试,还是需要用到 log 来查看变量值,比如我下午遇到个场景:选择完客户后返回页面,根据条件判断是否弹窗:


if (global.isXXX || !this.customerId || !this.skuList.length) return

// 到了这里才会执行弹窗的逻辑

这个时候只能真机调试,看控制台打印的值是怎样的,但对于上面的条件,如果你这样 log 的话,那控制台只会显示:


console.log(global.isXXX, !this.customerId, !this.skuList.length)
false false false

且如果参数比较多,你可能就没法立即将 log 出的值对应到相应的变量,还得回去代码里面仔细比对。


还有一个,我之前遇到过一个项目里一堆 log,同事为了方便看到 log 是在哪一行,就在 log 的地方加上代码所在行数,但因为 log 那一刻已经硬编码了,而代码经常会添加或者删除,这个时候行数就不对了:




比如你上面添加了一行,这里的所有行数就都不对了



所以,我希望 console.log 的时候:



  1. 控制台主动打印源码所在行数

  2. 变量名要显示出来,比如上面例子的 log 应该是 global.isXXX = false !this.customerId = false !this.skuList.length = false

  3. 可以的话,每个参数都有分隔符,不然多个参数看起来就有点不好分辨


即源码不做任何修改:



而控制台显示所在行,且有变量名的时候添加变量名前缀,然后你可以指定分隔符,如换行符\n



因为之前有过 babel 插件的经验,所以想着这次继续通过写一个 babel plugin 实现以上功能,所以也就有了babel-plugin-enhance-log,那究竟怎么用?很简单,下面 👇🏻 我给大家说说。


babel-plugin-enhance-log


老规矩,先安装插件:


pnpm add babel-plugin-enhance-log -D
# or
yarn add babel-plugin-enhance-log -D
# or
npm i babel-plugin-enhance-log -D

然后在你的 babel.config.js 里面添加插件:


module.exports = (api) => {
return {
plugins: [
'enhance-log',
...
],
}
}

看到了没,就是这么简单,之后再重新启动,去你的控制台看看,小火箭咻咻咻为你刷起~



options


上面了解了基本用法后,这里再给大家说下几个参数,可以看下注释,应该说是比较清楚的:


interface Options {
/**
* 打印的前缀提示,这样方便快速找到log 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀
* @example
* console.log('line of 1 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀', ...)
*/

preTip?: string
/** 每个参数分隔符,默认空字符串,你也可以使用换行符\n,分号;逗号,甚至猪猪🐖都行~ */
splitBy?: boolean
/**
* 是否需要endLine
* @example
* console.log('line of 1 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀', ..., 'line of 10 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀')
* */

endLine?: boolean
}

然后在插件第二个参数配置即可(这里偷偷跟大家说下,通过/** @type {import('babel-plugin-enhance-log').Options} */可以给配置添加类型提示哦):



return {
plugins: [
['enhance-log', enhanceLogOption],
],
...
}

比如说,你不喜欢小 🚀,你喜欢猪猪 🐖,那可以配置 preTip 为 🐖🐖🐖🐖🐖🐖🐖🐖🐖🐖:



比如说,在参数较多的情况下,你希望 log 每个参数都换行,那可以配置 splitBy 为 \n



或者分隔符是;:



当然,你也可以随意指定,比如用个狗头🐶来分隔:



又比如说,有个 log 跨了多行,你希望 log 开始和结束的行数,中间是 log 实体,那可以将 endLine 设置为 true:





我们可以看到开始的行数是13,结束的行数是44,跟源码一致



实现思路


上面通过多个例子跟大家介绍了各种玩法,不过,我相信还是有些小伙伴想知道怎么实现的,那我这里就大致说下实现思路:


老规格,还是通过babel-ast-explorer来查看


1.判断到 console.log 的 ast,即 path 是 CallExpression 的,且 callee 是 console.log,那么进入下一步



2.拿到 console.log 的 arguments,也就是 log 的参数



3.遍历 path.node.arguments 每个参数



  • 字面量的,则无须添加变量名

  • 变量的,添加变量名前缀,如 a =

  • 如果需要分隔符,则根据传入的分隔符插入到原始参数的后面


4.拿到 console.log 的开始行数,创建一个包含行数的 StringLiteral,同时加上 preTip,比如上面的 🚀🚀🚀🚀🚀🚀🚀,或者 🐖🐖🐖🐖🐖🐖🐖🐖🐖🐖,然后 unshift,放在第一个参数的位置


5.拿到 console.log 的结束行数,过程跟第 4 点类似,通过 push 放到最后一个参数的位置



6.在这过程中需要判断到处理过的,下次进来就要跳过,防止重复添加


以下是源码的实现过程,有兴趣的可以看看:


import { declare } from '@babel/helper-plugin-utils'
import generater from '@babel/generator'
import type { StringLiteral } from '@babel/types'
import { stringLiteral } from '@babel/types'


const DEFAULT_PRE_TIP = '🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀'
const SKIP_KEY = '@@babel-plugin-enhance-logSkip'

function generateStrNode(str: string): StringLiteral & { skip: boolean } {
const node = stringLiteral(str)
// @ts-ignore
node.skip = true
// @ts-ignore
return node
}

export default declare<Options>((babel, { preTip = DEFAULT_PRE_TIP, splitBy = '', endLine = false }) => {
const { types: t } = babel
const splitNode = generateStrNode(splitBy)
return {
name: 'enhance-log',
visitor: {
CallExpression(path) {
const calleeCode = generater(path.node.callee).code
if (calleeCode === 'console.log') {
// add comment to skip if enter next time
const { trailingComments } = path.node
const shouldSkip = (trailingComments || []).some((item) => {
return item.type === 'CommentBlock' && item.value === SKIP_KEY
})
if (shouldSkip)
return

t.addComment(path.node, 'trailing', SKIP_KEY)

const nodeArguments = path.node.arguments
for (let i = 0; i < nodeArguments.length; i++) {
const argument = nodeArguments[i]
// @ts-ignore
if (argument.skip)
continue
if (!t.isLiteral(argument)) {
if (t.isIdentifier(argument) && argument.name === 'undefined') {
nodeArguments.splice(i + 1, 0, splitNode)
continue
}
// @ts-ignore
argument.skip = true
const node = generateStrNode(`${generater(argument).code} =`)

nodeArguments.splice(i, 0, node)
nodeArguments.splice(i + 2, 0, splitNode)
}
else {
nodeArguments.splice(i + 1, 0, splitNode)
}
}
// the last needn't split
if (nodeArguments[nodeArguments.length - 1] === splitNode)
nodeArguments.pop()
const { loc } = path.node
if (loc) {
const startLine = loc.start.line
const startLineTipNode = t.stringLiteral(`line of ${startLine} ${preTip}:\n`)
nodeArguments.unshift(startLineTipNode)
if (endLine) {
const endLine = loc.end.line
const endLineTipNode = t.stringLiteral(`\nline of ${endLine} ${preTip}:\n`)
nodeArguments.push(endLineTipNode)
}
}
}
},
},
}
})

对了,这里有个问题是,我通过标记 path.node.skip = true 来跳过,但是还是会多次进入:


if (path.node.skip) return
path.node.skip = true

所以最终只能通过尾部添加注释的方式来避免多次进入:



有知道怎么解决的大佬还请提示一下,万分感谢~


总结


国际惯例,我们来总结一下,对于生产环境或真机调试,或者对于一些偏爱 console.log 的小伙伴,我们为了更快在控制台找到 log 的变量,通常会添加 log 函数,参数变量名,但前者一旦代码位置更改,打印的位置就跟源码不一致,后者又得重复写每个参数变量名的字符串,显得相当的麻烦。


为了更方便地使用 log,我们实现了个 babel 插件,功能包括:



  1. 自动打印行数

  2. 可以根据个人喜好加上 preTip,比如刷火箭 🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀🚀,或者可爱的小猪猪 🐖🐖🐖🐖🐖🐖🐖🐖🐖🐖

  3. 同时,对于有变量名的情况,可以加上变量名前缀,比如 const a = 1, console.log(a) => console.log('a = ', a)

  4. 还有,我们可以通过配置 splitBy、endLine 来自主选择任意分隔符、是否打印结束行等功能


最后


不知道大家有没有在追不良人,我是从高三追到现在。今天是周四,不良人第六季也接近尾声了,那就谨以此文来纪念不良人第六季的完结吧~



好了,再说一句,如果你是个偏爱 console.log 的前端 er,那请你喊出:泰裤辣(逃~)


作者:暴走老七
来源:juejin.cn/post/7231577806189133884
收起阅读 »

python简单实现校园网自动认证,赶紧部署到你的路由器上吧

2023-05-20:使用python库来实现定时任务,不再依赖系统的定时任务,部署起来容易100倍 原文链接 python实现校园网自动认证 - 歌梦罗 - 努力生活,努力热爱 (gmero.com) 说在前面 大部分校园网都需要认证才能够上网,而且就...
继续阅读 »

2023-05-20:使用python库来实现定时任务,不再依赖系统的定时任务,部署起来容易100倍




原文链接 python实现校园网自动认证 - 歌梦罗 - 努力生活,努力热爱 (gmero.com)



说在前面


大部分校园网都需要认证才能够上网,而且就算你保持在线隔一段时间也会将你强行踢下线,导致游戏中断,自己挂在校园网的web程序宕机等头疼的问题。


本文主要是使用python与主机的计划任务来实现校园网的自动认证,需要注意的是,我们学校的校园网认证系统与认证方式可能与你们学校的不一样,所以主要是分享我的实现思路,以供大家参考参考。


准备工作:



  • 一台电脑

  • 一台用于挂载自动认证脚本的服务器(也可以是你的电脑,或者不那么硬的路由器之类的)


了解校园网认证的过程



这是最繁琐的一步,在网上见过的这么多认证的教程,感觉我们学校是最复杂的



其实总结来说这一步就是反复的在浏览器f12里观察你在认证的时候都经过了哪些程序,我们学校的认证流程大概是这样的



我们需要传入以下参数进行认证,其中后三项传空值,因为是本地认证后面脚本就懒得加密密码了,把passwordEncrypt传false,password直接明文就可以了,可能不安全,但校园网无所谓。



看起来感觉实现很容易对不对?python随便几行request就能实现了的。但有以下几个问题:



  • 重复的认证是无效的,不会重新计算在线时间(到点还是踢你)

  • 高频的认证可能导致莫名奇妙的bug,比如明明已经认证成功了而认证服务器那边确觉得你没有认证,导致认证界面直接卡死(因为123.123.123.123在你已经接入互联网的情况下是无法访问的)

  • 多次认证还可能导致出现验证码,目前脚本还无法处理验证码


于是我们在程序中需要判断是否已经认证了,在已经认证的情况下运行程序需要先登出来重置在线时间从而推迟强制下线的时间(于是,我们又需要找到两个请求:一个是判断认证情况的,一个是退出登录的)


经过疯狂的F12和api调试之后,找到了172.208.2.102/eportal/InterFace.do?method=logout172.208.2.102/eportal/InterFace.do?method=getUserInfo两个api来进行登出和判断操作


python实现


认证流程用python的requests库很容易就能实现,定时任务用schedule来创建也很容易。


import re
import time
import urllib.parse
from urllib.parse import urlparse

import requests
import schedule

apiUrl = "http://172.208.2.102/eportal/InterFace.do"
authUrl = apiUrl + "?method=login"
logoutUrl = apiUrl + "?method=logout"
getQueryUrl = "http://123.123.123.123/"
getUserInfoUrl = apiUrl + "?method=getOnlineUserInfo"
webService = "中国电信"


# 判断是否已经认证了
def is_authed():
   try:
       resp = requests.get(getUserInfoUrl, timeout=3)
       if resp.status_code != 200:
           # 网络问题直接退出
           print("判断认证状态失败")
           return False
       else:
           json_data = resp.json()
           result = json_data["result"]
           return result != "fail"
   except requests.RequestException:
       print("判断认证状态失败: 请求失败")
       return False


# 获取query认证信息
def get_query_str():
   try:
       resp = requests.get(getQueryUrl, timeout=8)
       if resp.status_code != 200:
           print("获取query信息失败")
           return False
       pattern = "href='(.*)'"
       match = re.search(pattern, resp.text)

       if match:
           url = urlparse(match.group(1))
           return url.query
       else:
           return
   except requests.RequestException:
       print("获取query信息失败: 请求失败")
       return False


# 认证
def do_auth():
   query_str = get_query_str()
   if not query_str:
       return False
   # 表单数据
   data = {
       "userId": "871390441",
       "password": "yourpassword",
       "service": urllib.parse.quote(webService),
       "queryString": query_str,
       "passwordEncrypt": "false",
       "operatorPwd": ,
       "operatorUserId": ,
       "validcode":
  }

   try:
       resp = requests.post(authUrl, data)
       if resp.status_code == 200 and resp.json()["result"] == "success":
           print("认证成功")
           return True
       else:
           print("认证失败")
           return False
   except requests.RequestException:
       print("认证失败: 请求失败")
       return False


# 退出登录
def do_logout():
   resp = requests.get(logoutUrl)

   if resp.status_code == 200:
       if resp.json()["result"] == "success":
           print("退出登录成功")
           return True
       else:
           print("退出登录失败: " + resp.json()["message"])
           return False
   else:
       print("退出登录失败: 网络错误")
       return False


# 一次认证流程
def auth_job():
   print("\n====校园网自动认证开始====")
   if is_authed():
       if do_logout():
           do_auth()
   else:
       do_auth()
   print("====校园网自动认证结束====\n")


if __name__ == '__main__':
   auth_job()
   # 定时任务
   schedule.every().day.at("12:00").do(auth_job)
   schedule.every().day.at("00:00").do(auth_job)

   while True:
       schedule.run_pending()
       time.sleep(1)


代码部分有了思路之后其实就很简单了,接下来就是最重要的部署环节了。


部署到服务器


我这里以部署到我的破烂x86linux服务器上为例(windows就更简单了,直接运行python程序即可),采取docker部署的方式


首先写Dockerfile, 这里我就不解释了,不懂的话找一找资料吧


FROM python:3.11-slim-bullseye

WORKDIR /app

ADD . /app

RUN pip3 config set global.index-url http://mirrors.aliyun.com/pypi/simple && \
  pip3 config set install.trusted-host mirrors.aliyun.com && \
  pip install --upgrade pip &&\
  pip3 install requests &&\
  pip3 install schedule


CMD python3 -u main.py

然后在当前文件夹输入命令来建立镜像和运行容器,很简单对不对


docker build -t autoauth:v1 . 
docker run --restart always --name myautoauth autoauth:v1

写在最后


以上操作在我这是完美运行的,需要一些折腾,但你能看完我这篇博客说明你肯定也是个喜欢折腾的人吧。上面的一些命名方法路径之类的不一定是绝对的。


作者:歌梦罗
来源:juejin.cn/post/7234864940799213625
收起阅读 »

环信 uni-app-demo 升级改造计划——整体代码重构优化(二)

概述本次关于 uni-app 代码整体重构工作,基于上一期针对 uni-app 官网 demo 从 vue2 迁移 vue3 框架衍生而来,在迁移过程中有明显感知,目前的项目存在的问题为,项目部分代码风格较为不统一,命名不够规范,注释不够清晰、可读性...
继续阅读 »

概述

本次关于 uni-app 代码整体重构工作,基于上一期针对 uni-app 官网 demo 从 vue2 迁移 vue3 框架衍生而来,在迁移过程中有明显感知,目前的项目存在的问题为,项目部分代码风格较为不统一,命名不够规范,注释不够清晰、可读性差、以造成如果复用困难重重,本地重构期望能够充分展示 api 在实际项目中的调用方式,尽可能达到示例代码可移植,或能辅助进行即时通讯功能二次开发的能力。

目的

  • 使代码更加可读。

  • 简化或去除冗余代码。

  • 部分组件以及逻辑重命名、重拆分、重合并。

  • 增加全局状态管理。

  • 修改 SDK 引入方式为通过 npm 形式引入

  • 收束 SDK 大部分 API 到统一文件、方便管理调用。

  • 升级 SDK api 至最新的调用方式(监听、发送消息)

  • 增加会话列表接口、消息漫游接口。

  • SDK 指环信 IM uni-app SDK

重构计划

一、修改原 WebIM 的导出导入使用方式。

目的

  1. 现有 uniSDK 已支持 npm 形式导入。
  2. 原有实例化代码与 config 配置较为混乱不够清晰。
  3. 分离初始化以及配置形成独立文件方便管理。

实现

  1. 项目目录中创建 EaseIM 文件夹并创建 index.js,在 index.js 中完成导入 SDK 并实现实例化并导出。
  2. EaseIM -> config 文件夹并将 SDK 中相关配置在此文件中书写并导出供实例化使用。

影响(无影响)

二、引入 pinia 进行状态管理

pinia 还能通过$reset()方法即可完成对某个 store 的初始化,利用该方法可以非常方便的在切换账号时针对缓存在 stores 中的数据初始化,防止切换后的账号与上一个账号的数据造成冲突。

目的

  1. 存放 SDK 部分数据以及状态(登录状态、会话列表数据、消息数据)
  2. 方便各组件状态或数据取用避免数据层层传递。
  3. 用以平替原有本地持久化数据存储。
  4. 可以替代原有 disp 发布订阅管理工具,因为 store 中状态改变,各组件可以进行重新计算或监听,无需通过发布订阅通知改变状态。

实现

  1. 在 mian.js 中引入 pinia,并挂载
//Pinia
import * as Pinia from 'pinia';
export function createApp() {
const app = createSSRApp(App);
app.use(Pinia.createPinia());
return {
app,
Pinia,
};
}
  1. 项目目录中新建 stores 并创建各个所需 store,类似目录如下:

影响(无影响)

三、重新梳理 App.vue 根组件中代码

目的

  1. 简化项目中 App.vue 根组件中的冗长代码。
  2. 迁移根组件中的监听代码。
  3. globalData,method 中代码转为 stores 中或剔除。
  4. disp 代码剔除。

实现

  1. App.vue 中的监听嵌入至 EaseIM 文件夹下的 listener 集中管理,并在 App.vue 中重新进行挂载
  2. import ‘@/EaseIM’;从而实现实例化 SDK。
  3. 将需要 IM 连接成功后调用的数据,合为一个方法中,并在 onConnected 触发后调用。
  4. 部分关于 SDK 调用的代码迁入至 EaseIM 文件夹下的 imApis 文件夹中
  5. 部分有关 SDK 的工具方法代码迁入至 EaseIM 文件夹下的 utils 文件夹中

影响

App.vue 改动相对较大,主要为监听的迁移,一部分方法迁移至 stores 中,并且需要重新进行监听的挂载。具体代码可在后续迁移前后比对中看到,或者文尾的看到 github 中看到代码地址。

四、优化 login 页面代码

目的

原有 login 组件登录部分代码比较冗长并且可读性较差因此进行优化。

实现

  1. 删除原有操作 input 的代码,改为通过 v-model 双向绑定。
  2. 拆分登录代码为通过 username+password,以及手机号+验证码两个登录方法。
  3. 增加登录存储 token 方法,方便后续重连时通过用户名+token 形式进行重新登录。
  4. 登录成功之后将登录的 id 以及手机号信息 set 进入到 stores 中。

影响(无影响)

五、增加 home 页面

目的

  1. 作为 Conversation、Contacts、Me 三个核心页面容器组件,方便页面切换管理。
  2. 作为 Tabbar 的容器组件

实现

  1. 去除原有会话、联系人、我的(原 setting 页面)pages.json 的路由配置,增加 home 页面路由相关配置。
  2. pages 中增加 home 组件,并以组件化的形式引入三个核心页面组件。
  3. 项目根目录中新建 layout 文件夹并增加 tabbar 组件,将三个页面中的 tabbar 功能抽离至 tabbar 组件中,并增加相应切换逻辑。

影响

此改动主要影响为要涉及到将原 setting 组件改为 Me 组件,并将三个原有页面 pages.json 删除并在 home 中引入,并无其他副作用。

六、重构 Conversation、Contacts、Me 等基础组件

目的

  1. 将原有数据(会话列表数据,联系人数据,昵称头像数据)来源切换为从 SDK 接口+stores 中获取。
  2. 去除组件内的 disp 发布订阅相关代码,以及 WebIM 的使用。
  3. 调整原组件代码中的不合理的命名,去除不再使用的方法简化该组件代码。

实现

以 Conversation 组件举例

  1. 以 SDK 接口 getConversationlist 获取会话列表数据,缓存至 stores 中并做排序处理,在组件中使用计算属性获取作为新的会话列表数据来源。
  2. 由于会话列表的更新是动态的,因此不再需要 disp 订阅一系列的事件进行处理,因此相关代码可以开始进行删除。
  3. 原有的通过会话列表跳转至联系人页面或者其他群组页面命名改为单词从 into 改为 entry 并改为驼峰命名,经过改造该组件用不到的方法则完全删除。

影响

主要影响则是这些组件内的逻辑代码会有从结构以及数据源会有较大变化,需要边改造边验证,并且会与 stores、EaseIM 等组件有较大的关系,需要耐心进行调整。

七、增加 emChatContainer 组件

目的

  1. 新增此组件命名更为语义化,能够通过组件名看出其实际功能为 emChat 聊天页组件容器。
  2. 合并原有 singleChatEntry 组件以及 groupChatEntry 组件,两个相似功能组件至统一的一个 emChatContainer 内。

实现

  1. 在 pages 下新建一个名为 emChatContainer 的组件,并先将 components 下的 chat 组件参考 singleChatEntry 组件引入,并在 pages 中配置对应路由路径映射。
  2. 观察发现该组件作为 chat 组件容器,主要向下传递两个核心参数,1)目标 ID(也就是聊天的目标环信 ID)。2)chatType(也就是目标聊天的类型,常规为单聊、群聊。),且这两个核心参数经常被 chat 组件中的各个子组件用到,一层层向下传递较为繁琐,因此使用到 Vue 组件传参方法之一的,provide、inject 方式将参数注册并向下传递下去。
  3. 完成合并之后将 singleChatEntry、groupChatEntry 删去,并且将原有用到向该组件跳转的方法路径全部指向 emChatContainer,且在 pages.json 中删除对应的页面路径。

影响

从会话进入到聊天页、从联系人、群组页面进入到聊天页的路由跳转路径全部改为 emChatContainer,并且将会改变 chat 组件使用 targetId(聊天目标 ID)以及 chatType 的方式,因为需要改为通过 inject 接收。

八、emChat 组件重构

目的

  1. 改写该组件下不合理的文件命名。
  2. 删除非必要的 js 文件或组件。
  3. 该组件内各个功能子组件进行局部代码重构。

实现

  1. 配合 emChatContainer 将 chat 组件改名为 emChat。
  2. 删除其组件内的 msgpackager.js、msgstorage.js、msgtype.js、pushStorage.js,这几个 js 文件。
  3. messagelist inputbar 改为驼峰命名。
  4. messageList 组件内的消息列表来源改为从 stores 中获取,增加下拉通过 getHistroyMessage 获取历史消息。
  5. 子组件内接收目标 id 以及消息类型改为通过 inject 接收。
  6. msgType 从 EaseIM/constant 中获取。
  7. 发送消息 API 全部改为 SDK4.xPromise 写法,在 EaseIM/imApis/emMessages 统一并导出,在需要的发送的组件中导入调用,剔除原有发送消息的方式。

影响

该组件调整难度最大,因为牵扯的组件,以及需要新增的调整的代码较多,需要逐个组件修改并验证,具体代码将在下方局部展示。详情请参看源码地址。

九、新增重连中提示监听回调

目的

能够在 IM websocket 断开的时候有相应的回调出来,并给到用户相应的提示。

实现

在 addEventHandler 监听中增加 onReconnecting 监听回调,并且在实际触发的时候增加 Toast 提示监听 IM 正在重连中。

PS:onReconnecting 属于实验性回调。

影响(无影响)


由于篇幅限制,本文的下半部分请参考:https://blog.csdn.net/huan132456765/article/details/130763984

友情链接

最后多说一句,如果觉得有帮助请点赞支持一下!本 demo 还有三期计划(增加音视频功能),敬请期待!

友情链接

最后多说一句,如果觉得有帮助请点赞支持一下!本 demo 还有三期计划(增加音视频功能),敬请期待!

收起阅读 »

【思考】时间不等人,学会聚焦重要事情,享受现在的快乐!

时间对每个人都是有限的 时间是一种奢侈品,在许多情况下,我们不能在拥有更多的财富和资源,但我们可以通过聪明的时间管理来确保我们能够在有限的时间内做更多的事情。所以我们必须学会如何利用时间,找出哪些对我们最重要的事情,并把它们放在更加优先的位置。 找出那些对你最...
继续阅读 »

时间对每个人都是有限的


时间是一种奢侈品,在许多情况下,我们不能在拥有更多的财富和资源,但我们可以通过聪明的时间管理来确保我们能够在有限的时间内做更多的事情。所以我们必须学会如何利用时间,找出哪些对我们最重要的事情,并把它们放在更加优先的位置。


找出那些对你最重要的事情


我们不是时间的奴隶,而是它的朋友。活到老学到老,要知道区分好事情的先后优先级。你需要挖掘出对你而言真正重要的事情,并且把它们放在更优先的位置。只有专注于最重要的事情,你才能够彻底放下手中的琐事,让时间为你而舞。


时间管理不应该是目标,它应该是一种手段来实现目标。你需要找出那些对你最重要的事情,并将它们放在更优先的位置。只有这样,你才能在有限的时间里取得最好的成果。


不要把快乐推迟到未来


当你把所有的重要事情放到更优先的位置上后,你需要学会享受现在。不要把快乐推迟到未来,学会珍惜每个瞬间。因为时间是快乐的载体,珍惜当下不仅可以让你快乐,还能让你更加享受未来的生活。


面对优先事情时,学会说“不”


你会遇到很多无关紧要的请求,但是你需要学会拒绝那些与你最重要的事情无关的请求。说“不”并不是一种自私的行为,它可以让你从无关紧要的事情中解放出来,让你有更多的时间去关注那些真正重要的事情。


对“无关紧要”的事情说不!有时候,那些琐事会在你不经意间夺去你的时间,让你无暇顾及真正重要的事情。对于那些无关紧要的请求,大声说不!真正重要的事情,才是我们的核心,更值得我们花费精力。


图片遵循CC0 1.0协议


时间的价值取决于你如何利用它


时间的价值在于你如何使用它。尽管每个人的时间价值不同,但对于每个人来说,时间都是宝贵的。在一天结束时,如果你有实现一些重要的事情并感到充实和满足,那么这一天就是美好的。因此,你需要学会关注你的目标,并且学会更好地安排时间。


时间管理要始于自我管理


自我管理是实现聪明时间管理的核心。自我管理意味着在思想上建立目标和优先事项,并且始终监控、计划和执行任务。如果你能够始于自我管理,你将能够更加有效地管理时间,并且做出更加明智的决策。


学习重点管理和时间分配


重点管理和时间分配是实现聪明时间管理的关键。你需要始终关注那些重要的事情,并且制定适当的计划,并在时间上严格控制。因为时间是快速流逝的,你需要掌握时间并且利用它,不让时间流失于无关紧要的事情中。


把握时间,抓住机会。时间的价值在于你如何使用它。每天结束时,回首自己所做的一切是什么感觉?充实满足?还是焦虑疲惫?抓住机会,把握时间,完成更多的事情,才是让我们拥有更充实快乐的生活。


图片遵循CC0 1.0协议


享受时间的本质


最后,记住时间是有限的资源,珍惜现在的每一分每一秒,学会享受时间的本质。当我们的思维放松,我们的身心健康时,我们的工作表现会更加出色。在享受生活的同时,我们也应该学习新的技能和经验。所以让我们将时间花在有意义的事情上,并且享受成功的成果。


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

Android jetpack Compose之约束布局

概述 我们都知道ConstraintLayout在构建嵌套层级复杂的视图界面时可以有效降低视图树的高度,使视图树扁平化,约束布局在测量布局耗时上比传统的相对布局具有更好的性能,并且约束布局可以根据百分比自适应各种尺寸的终端设备。因为约束布局确实很好用,所以,官...
继续阅读 »

概述


我们都知道ConstraintLayout在构建嵌套层级复杂的视图界面时可以有效降低视图树的高度,使视图树扁平化,约束布局在测量布局耗时上比传统的相对布局具有更好的性能,并且约束布局可以根据百分比自适应各种尺寸的终端设备。因为约束布局确实很好用,所以,官方也为我们将约束布局迁移到了Compose平台。本文就是介绍约束布局在Compose中的使用。


实例解析


在使用约束布局之前,我们需要先在项目中的app.gradle脚本中添加compose版本的ConstraintLayout依赖

implementation('androidx.constraintlayout:constraintlayout-compose:1.0.1')

引入依赖以后,我们就可以看下如何在Compose中使用约束布局了


1.创建引用


在传统View系统中,我们在布局XML文件中可以给View设置资源的ID,并将资源ID作为索引来声明对应组件的摆放位置。而在Compose的约束布局中,可以主动创建引用并绑定到某个具体组件上,从而实现与资源ID相似的功能,每个组件都可以利用其他组件的引用获取到其他组件的摆放位置信息,从而确定自己摆放位置。



Compose 创建约束布局的方式有两种,分别时createRef()和createRefs(),根据字面意思我们就可以很清楚的知道,createRef(),每次只会创建一个引用,而createRefs()每次可以创建多个引用(最多可以创建16个),创建引用的方式如下:

// 创建单个引用
val text = createRef()
// 创建多个引用
val (button1,button2,text) = createRefs()

2.绑定引用


当我们创建完引用后就可以使用Modifier.constrainAs()修饰符将我们创建的引用绑定到某个具体组件上,可以在contrainAs尾部Lambda内指定组件的约束信息。我们需要注意的是,我们只能在ConstraintLayout尾部的Lambda中使用createRefer,createRefs函数创建引用,并使用Modifier.constrainAs函数来绑定引用,因为ConstrainScope尾部的Lambda的Reciever是一个ConstraintLayoutScope作用域对象。我们可以先看下面一段代码了解下约束布局的引用绑定:

@Composable
fun ConstrainLayoutDemo()
{
ConstraintLayout(modifier = Modifier
.width(300.dp)
.height(100.dp)
.padding(5.dp)
.border(5.dp, color = Color.Red)) {
val portraitImageRef = remember {
createRef()
}

Image(painter = painterResource(id = R.drawable.portrait)
, contentDescription = null,
modifier = Modifier.constrainAs(portraitImageRef){
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
})

}
}

运行结果:
在这里插入图片描述



上面的代码是实现一个用户卡片的部分代码,从代码中看到我们使用约束的时候需要用到Modifier.constrainsAs(){……}的方式。Modifier.constrainsAs的尾部Lambda是一个ConstrainScope作用域对象,可以在其中获取当前组件的parent,top,bottom,start,end等信息。并使用linkTo指定组件约束。在上面的界面中,我们希望用户的头像可以居左对齐,所以将top拉伸至父组件的顶部,bottom拉伸至父组件的底部,start拉伸至父组件的左边。我们再为卡片添加上用户的昵称和描述,全部代码如下所示:

@Composable
fun ConstrainLayoutDemo()
{
ConstraintLayout(modifier = Modifier
.width(300.dp)
.height(100.dp)
.padding(5.dp)
.border(5.dp, color = Color.Red)) {
val (portraitImageRef,usernameTextRef,descriptionTextRef) = remember {
createRefs()
}

Image(painter = painterResource(id = R.drawable.portrait)
, contentDescription = null,
modifier = Modifier.constrainAs(portraitImageRef){
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
start.linkTo(parent.start)
})

Text(text = "旅游小美女", fontSize = 16.sp, maxLines = 1,
textAlign = TextAlign.Left,
modifier = Modifier.constrainAs(usernameTextRef){
top.linkTo(portraitImageRef.top)
start.linkTo(portraitImageRef.end,10.dp)
})

Text(text = "个人描述。。。。。。。。", fontSize = 14.sp,
color = Color.Gray,
fontWeight = FontWeight.Light,
modifier = Modifier.constrainAs(descriptionTextRef){
top.linkTo(usernameTextRef.bottom,5.dp)
start.linkTo(portraitImageRef.end,10.dp)
}
)
}
}

运行结果:
在这里插入图片描述
在上面的代码中我们也可以在ConstrainScope中指定组件的宽高信息,在ConstrainScope中直接设置width与height的可选值如下所示:



在ConstrainScope中指定组件的宽高信息时,通过在Modifier.constrainAs(xxxRef){width = Dimension.可选值}来设置,可选值如下:
Dimension.wrapContent: 实际尺寸为根据内容自适应
Dimension.matchParent: 实际尺寸为铺满父组件的尺寸
Dimension,wrapContent: 实际尺寸为根据约束信息拉伸后的尺寸
Dimension.preferredWrapContent: 如果剩余空间大于更具内容自适应的尺寸时,实际尺寸为自适应的尺寸,如果剩余空间小于内容自适应尺寸时,实际尺寸为剩余空间尺寸
Dimension.ratio(String): 根据字符串计算实际尺寸所占比率:如1 :2
Dimension.percent(Float): 根据浮点数计算实际尺寸所占比率
Dimension.value(Dp): 将尺寸设置为固定值
Dimension.perferredValue(Dp): 如果剩余空间大于固定值时,实际尺寸为固定值,如果剩余空间小于固定值时,实际尺寸则为剩余空间尺寸



我们想象下,假如用户的昵称特别长,那么按照我们上面的代码展示则会出现展示不全的问题,所以我们可以通过设置end来指定组件所允许的最大宽度,并将width设置为preferredWrapContent,意思是当用户名很长时,实际的宽度会做自适应调整。我们将上面展示用户名的地方改一下,代码如下:

// 上面的代码只用改这个部分
Text(
text = "旅游小美女美美美美美名字很长长长长长长长长长",
fontSize = 16.sp,
textAlign = TextAlign.Left,
modifier = Modifier.constrainAs(usernameTextRef){
top.linkTo(portraitImageRef.top)
start.linkTo(portraitImageRef.end,10.dp)
end.linkTo(parent.end,10.dp)
width = Dimension.preferredWrapContent
})

运行结果:
在这里插入图片描述


辅助布局工具


在传统View的约束布局中有Barrier,GuideLine等辅助布局的工具,在Compose中也继承了这些特性,方便我们完成各种复杂场景的布局需求。


1.Barrier分界线


Barrier顾名思义就是一个屏障,使用它可以隔离UI布局上面的一些相互挤压的影响,举一个例子,比如我们希望两个输入框左对齐摆放,并且距离文本组件中的最长者仍保持着10dp的间隔,当用户名和密码等发生变化时,输入框的位置能够自适应调整。这里使用Barrier特性可以简单的实现这一需求:

@Composable
fun InputFieldLayout(){
ConstraintLayout(
modifier = Modifier
.width(400.dp)
.padding(10.dp)
) {
val (usernameTextRef, passwordTextRef, usernameInputRef, passWordInputRef) = remember { createRefs() }
val barrier = createEndBarrier(usernameTextRef, passwordTextRef)
Text(
text = "用户名",
fontSize = 14.sp,
textAlign = TextAlign.Left,
modifier = Modifier
.constrainAs(usernameTextRef) {
top.linkTo(parent.top)
start.linkTo(parent.start)
}
)

Text(
text = "密码",
fontSize = 14.sp,
modifier = Modifier
.constrainAs(passwordTextRef) {
top.linkTo(usernameTextRef.bottom, 20.dp)
start.linkTo(parent.start)
}
)
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier.constrainAs(usernameInputRef) {
start.linkTo(barrier, 10.dp)
top.linkTo(usernameTextRef.top)
bottom.linkTo(usernameTextRef.bottom)
height = Dimension.fillToConstraints
}
)
OutlinedTextField(
value = "",
onValueChange = {},
modifier = Modifier.constrainAs(passWordInputRef) {
start.linkTo(barrier, 10.dp)
top.linkTo(passwordTextRef.top)
bottom.linkTo(passwordTextRef.bottom)
height = Dimension.fillToConstraints
}
)
}
}

运行结果:
在这里插入图片描述


2.Guideline引导线


Barrier分界线需要依赖其他引用,从而确定自身的位置,而使用Guideline不依赖任何引用,例如,我们希望将用户头像摆放在距离屏幕顶部2:8的高度位置,头像以上是用户背景,以下是用户信息,这样的需求就可以使用Guideline实现,代码如下:

@Composable
fun GuidelineDemo(){
ConstraintLayout(modifier = Modifier
.height(300.dp)
.background(color = Color.Gray)) {
val guideline = createGuidelineFromTop(0.2f)
val (userPortraitBackgroundRef,userPortraitImgRef,welcomeRef) = remember {
createRefs()
}

Box(modifier = Modifier
.constrainAs(userPortraitBackgroundRef) {
top.linkTo(parent.top)
bottom.linkTo(guideline)
height = Dimension.fillToConstraints
width = Dimension.matchParent
}
.background(Color(0xFF673AB7)))

Image(painter = painterResource(id = R.drawable.portrait),
contentDescription = null,
modifier = Modifier
.constrainAs(userPortraitImgRef) {
top.linkTo(guideline)
bottom.linkTo(guideline)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
.size(100.dp)
.clip(CircleShape)
.border(width = 2.dp, color = Color(0xFF96659E), shape = CircleShape))

Text(text = "不喝奶茶的小白兔",
color = Color.White,
fontSize = 26.sp,
modifier = Modifier.constrainAs(welcomeRef){
top.linkTo(userPortraitImgRef.bottom,10.dp)
start.linkTo(parent.start)
end.linkTo(parent.end)
})

}
}

运行结果:
在这里插入图片描述



在上面的代码中,我们使用createGuidelineFromTop()方法创建了一条从顶部出发的引导线,然后用户背景就可以依赖这条引导线确定宽高了,然后对于头像,我们只需要将top和bottom连接至引导线即可



3.Chain链接约束


ContraintLayout的另一个好用的特性就是Chain链接约束,通过链接约束可以允许多个组件平均分配布局空间,类似于weight修饰符。例如我们要展示一首古诗,用Chain链接约束实现如下:

@Composable
fun showQuotesDemo() {
ConstraintLayout(
modifier = Modifier
.size(400.dp)
.background(Color.Black)
) {
val (quotesFirstLineRef, quotesSecondLineRef, quotesThirdLineRef, quotesForthLineRef) = remember {
createRefs()
}

createVerticalChain(
quotesFirstLineRef, quotesSecondLineRef, quotesThirdLineRef, quotesForthLineRef,
chainStyle = ChainStyle.Spread
)

Text(text = "窗前明月光,",
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.constrainAs(quotesFirstLineRef) {
start.linkTo(parent.start)
end.linkTo(parent.end)
})

Text(text = "疑是地上霜。",
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.constrainAs(quotesSecondLineRef) {
start.linkTo(parent.start)
end.linkTo(parent.end)
})

Text(text = "举头望明月,",
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.constrainAs(quotesThirdLineRef) {
start.linkTo(parent.start)
end.linkTo(parent.end)
})

Text(text = "低头思故乡。",
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.constrainAs(quotesForthLineRef) {
start.linkTo(parent.start)
end.linkTo(parent.end)
})
}
}

运行结果:
在这里插入图片描述
如上面代码所示,我们要展示四句诗就需要创建四个引用对应四句诗,然后我们就可以创建一条垂直的链接约束将四句诗词连接起来,创建链接约束时末尾参数可以传一个ChainStyle,用来表示我们期望的布局样式,它的取值有三个,效果和意义如下所示:



(1)Spread:链条中的每个元素平分整个父空间

 createVerticalChain(
quotesFirstLineRef, quotesSecondLineRef, quotesThirdLineRef, quotesForthLineRef,
chainStyle = ChainStyle.Spread)

运行效果:


在这里插入图片描述



(2)SpreadInside:链条中的首尾元素紧贴边界,剩下的每个元素平分整个父空间

 createVerticalChain(
quotesFirstLineRef, quotesSecondLineRef, quotesThirdLineRef, quotesForthLineRef,
chainStyle = ChainStyle.SpreadInside)

运行效果:


在这里插入图片描述



(3)Packed:链条中的所有元素都聚集到中间,效果如下

 createVerticalChain(
quotesFirstLineRef, quotesSecondLineRef, quotesThirdLineRef, quotesForthLineRef,
chainStyle = ChainStyle.Packed)

运行效果:


在这里插入图片描述


总结


关于Compose约束布局的内容就是这些了,本文主要是简单的介绍了Compose中约束布局的基本使用,要熟练掌握Compose约束布局,还需要读者多去联系,多使用约束布局写界面,这样就会熟能生巧,在此我只做一个抛砖引玉的活。有任何疑问,欢迎在评论区交流。


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

安卓组件学习——NavigationView导航视图

前言 日新计划可真头疼,每天更文养成习惯是好,但有时候没思路就很烦,回到正题,本篇回到很久之前的组件学习(安卓UI设计开发——Material Design(BottomSheetDialogFragment篇) - 掘金 (juejin.cn)),这次我们来...
继续阅读 »

前言


日新计划可真头疼,每天更文养成习惯是好,但有时候没思路就很烦,回到正题,本篇回到很久之前的组件学习(安卓UI设计开发——Material Design(BottomSheetDialogFragment篇) - 掘金 (juejin.cn)),这次我们来看看许多APP首页常用的NavigationView——滑动菜单如何使用。


2/22 更正:NavigationView为导航视图,DrawerLayout为抽屉布局,一起组合成滑动菜单(实现侧滑交互体验)


733c1e864882815b26a38f1520a843a.jpg


正篇


首先,使用NavigationView前我们先创建一个新项目,我这里为了学习这些组件,命名为MaterialDemo,作为我们学习Materia组件的项目,然后因为要使用Material库的NavigationView,所以我们项目的app目录下的build.gradle文件中的dependencies闭包中添加下面依赖:

implementation 'com.google.android.material:material:1.8.0'
implementation 'de.hdodenhof:circleimageview:3.1.0'

其中第二个依赖是我们导入的开源项目CircleImageView,这可以让我们更容易实现图片圆形化,也就是这个滑动菜单栏上圆形头像的形成。


当然,新建的是Kotlin安卓空Activity项目,我们采用了ViewBinding,所以同时也要在该文件下启用ViewBinding,位置在android闭包中:

buildFeatures {
viewBinding = true
}

sync Gradle(同步 Gradle)完成后,我们再把res/values/theme.xml文件AppThemeparent主题换为Theme.MaterialComponents.Light.NoActionBar,用来适配我们的Material库组件:


image.png


我们还得事先准备几张图片备用(按钮,头像等),这里我放在了drawable-xxhdpi目录下,当然如果有需要可以到文末我的Github项目中找:


image.png


接着,我们在res目录下创建新的名为menu文件夹(和之前文章安卓开发基础——Menu菜单的使用 - 掘金 (juejin.cn)一样):


image.png


image.png


再在这个文件夹上右击->New->Menu resource file ,创建一个nav_menu.xml文件,添加下面的代码:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:checkableBehavior="single">
<item
android:id="@+id/navCall"
android:icon="@drawable/nav_call"
android:title="Call" />
<item
android:id="@+id/navFriends"
android:icon="@drawable/nav_friends"
android:title="Friends" />
<item
android:id="@+id/navLocation"
android:icon="@drawable/nav_location"
android:title="Location" />
<item
android:id="@+id/navMail"
android:icon="@drawable/nav_mail"
android:title="Mail" />
<item
android:id="@+id/navTask"
android:icon="@drawable/nav_task"
android:title="Tasks" />
</group>
</menu>

上面代码中我们加了一个group标签,并把其中的checkableBehavior属性设置为single,这样就能让菜单项变为只可以单选。
然后我们就能预览到这个菜单样式,这就是我们即将用的具体的菜单项:


image.png


但NavigationView样式不是这个预览到的,因为有菜单项这样还是不够的,我们还需要准备一个herderLayout用于显示NavigationView的头部布局,这个布局可以按照需求来定制,这里我们就构建了头像、用户名和邮箱地址这三项,这个布局我们直接在layout目录正常创建布局文件就行,我们这里创建一个名为nav_header的XML布局文件:


image.png
该文件代码如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="180dp"
android:padding="10dp"
android:background="?attr/colorPrimary">

<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/iconImage"
android:layout_width="70dp"
android:layout_height="70dp"
android:src="@drawable/nav_icon"
android:layout_centerInParent="true" />

<TextView
android:id="@+id/mailText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:text="tonygreendev@gmail.com"
android:textColor="#FFF"
android:textSize="14sp" />

<TextView
android:id="@+id/userText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/mailText"
android:text="Tony Green"
android:textColor="#FFF"
android:textSize="14sp" />

</RelativeLayout>

我们使用了相对布局(RelativeLayout)作为最外层布局,其中CircleImageView就是之前加依赖提到的开源控件,将我们的图片圆形化,和ImageView用法一样,这里我们用于构建圆形的头像图片,且设为居中,还有两个TextView分别就是我们的用户名和邮箱地址,用相对布局属性定位即可。


这些做完后,我们的准备阶段就完成了,接下来我们开始使用NavigationView控件:


我们先把布局添加NavigationView:

<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawerLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">

<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
</FrameLayout>


<com.google.android.material.navigation.NavigationView
android:id="@+id/navView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
app:menu="@menu/nav_menu"
app:headerLayout="@layout/nav_header"/>

</androidx.drawerlayout.widget.DrawerLayout>

Activity中:

package com.example.materialdemo

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.materialdemo.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

setSupportActionBar(binding.toolbar)

supportActionBar?.let {
it.setDisplayHomeAsUpEnabled(true)
it.setHomeAsUpIndicator(R.drawable.ic_menu)
}
binding.navView.setCheckedItem(R.id.navCall)
binding.navView.setNavigationItemSelectedListener {
binding.drawerLayout.closeDrawers()
true
}
}
}

最终效果:


36ef295afdeec85f9a5cb6a449700d38.gif


这里一开始忘记加我的项目地址了,现在补上:GitHub - ObliviateOnline/MaterialDemo: Material库组件学习


总结


今天学了NavigationView,结果忘记写前面的滑动菜单DrawerLayout和标题栏Toolbar控件了,下一篇就把前面的构建过程给理一遍。


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

Android性能启动优化——IO优化进阶

IO优化 1、启动过程不建议出现网络IO。 2、为了只解析启动过程中用到的数据,应选择合适的数据结构,如将ArrayMap改造成支持随机读 写、延时解析的数据存储结构以替代SharePreference。 这里需要注意的是,需要考虑重度用户的使用场景。 补充加...
继续阅读 »

IO优化


1、启动过程不建议出现网络IO。


2、为了只解析启动过程中用到的数据,应选择合适的数据结构,如将ArrayMap改造成支持随机读


写、延时解析的数据存储结构以替代SharePreference。


这里需要注意的是,需要考虑重度用户的使用场景。


补充加油站:Linux IO知识


1、磁盘高速缓存技术


利用内存中的存储空间来暂存从磁盘中读出的一系列盘块中的信息。因此,磁盘高速缓存在逻辑上属于


磁盘,物理上则是驻留在内存中的盘块。


其内存中分为两种形式:


在内存中开辟一个单独的存储空间作为磁速缓存,大小固定。


把未利用的内存空间作为一个缓沖池,供请求分页系统和磁盘I/O时共享。


2、分页


存储器管理的一种技术。


可以使电脑的主存使用存储在辅助存储器中的数据。


操作系统会将辅助存储器(通常是磁盘)中的数据分区成固定大小的区块,称为“页”(pages)。


当不需要时,将分页由主存(通常是内存)移到辅助存储器;当需要时,再将数据取回,加载主存


中。


相对于分段,分页允许存储器存储于不连续的区块以维持文件系统的整齐。


分页是磁盘和内存间传输数据块的最小单位。


3、高速缓存/缓冲器


都是介于高速设备和低速设备之间。


高速缓存存放的是低速设备中某些数据的复制数据,而缓冲器则可同时存储高低速设备之间的数


据。


高速缓存存放的是高速设备经常要访问的数据。


4、linux同步IO:sync、fsync、msync、fdatasync


为什么要使用同步IO?


当数据写入文件时,内核通常先将该数据复制到缓冲区高速缓存或页面缓存中,如果该缓冲区尚未写


满,则不会将其排入输入队列,而是等待其写满或内核需要重用该缓冲区以便存放其他磁盘块数据时,


再将该缓冲排入输出队列,最后等待其到达队首时,才进行实际的IO操作—延迟写。


延迟写减少了磁盘读写次数,但是却降低了文件内容的更新速度,可能会造成文件更新内容的丢失。为


了保证数据一致性,则需使用同步IO。


sync


sync函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际磁盘写操作结束


再返回。


通常称为update的系统守护进程会周期性地(一般每隔30秒)调用sync函数。这就保证了定期冲


洗内核的块缓冲区。


fsync


fsync函数只对文件描述符filedes指定的单一文件起作用,并且等待磁盘IO写结束后再返回。通常


应用于需要确保将修改内容立即写到磁盘的应用如数据库。


文件的数据和metadata通常存放在硬盘的不同地方,因此fsync至少需要两次IO操作。


msync


如果当前硬盘的平均寻道时间是3-15ms,7200RPM硬盘的平均旋转延迟大约为4ms,因此一次IO操作


的耗时大约为10ms。


如果使用内存映射文件的方式进行文件IO(mmap),将文件的page cache直接映射到进程的地址空


间,这时需要使用msync系统调用确保修改的内容完全同步到硬盘之上。


fdatasync


fdatasync函数类似于fsync,但它只影响文件的数据部分。而fsync还会同步更新文件的属性。


仅仅只在必要(如文件尺寸需要立即同步)的情况下才会同步metadata,因此可以减少一次IO操


作。


日志文件都是追加性的,文件尺寸一致在增大,如何利用好fdatasync减少日志文件的同步开销?


创建每个log文件时先写文件的最后一个page,将log文件扩展为10MB大小,这样便可以使用


fdatasync,每写10MB只有一次同步metadata的开销。


2.10 磁盘IO与网络IO


磁盘IO(缓存IO)


标准IO,大多数文件系统默认的IO操作。


数据先从磁盘复制到内核空间的缓冲区,然后再从内核空间中的缓冲区复制到应用程序的缓冲区。


读操作:操作系统检查内核的缓冲区有没有需要的数据,如果已经有缓存了,那么直接从缓存中返


回;否则,从磁盘中返回,再缓存在操作系统的磁盘中。


写操作:将数据从用户空间复制到内核空间中的缓冲区中,这时对用户来说写操作就已经完成,至


于什么时候写到磁盘中,由操作系统决定,除非显示地调用了sync同步命令。


QQ截图20220517202252.png



以上讲解部分Android性能优化中的IO优化部分,除了这些还有启动优化、卡顿优化、UI优化等等技术。了解更多可以前往




传送直达↓↓↓ :link.juejin.cn/?target=htt…



文末


优点


在一定程度上分离了内核空间和用户空间,保护系统本身安全。


可以减少磁盘IO的读写次数,从而提高性能。


缺点


DMA方式可以将数据直接从磁盘读到页缓存中,或者将数据从页缓存中写回到磁盘,而不能在应用程序


地址空间和磁盘之间进行数据传输,这样,数据在传输过程中需要在应用程序地址空间(用户空间)和


缓存(内核空间)中进行多次数据拷贝操作,这带来的CPU以及内存开销是非常大的。


磁盘IO主要的延时(15000RPM硬盘为例)


机械转动延时(平均2ms)+ 寻址延时(2~3ms)+ 块传输延时(0.1ms左右)=> 平均5ms


网络IO主要延时


服务器响应延时 + 带宽限制 + 网络延时 + 跳转路由延时 + 本地接收延时(一般为几十毫秒到几千毫秒, 受环境影响极大)


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

如何告诉后端出身的领导:这个前端需求很难实现

本文源于一条评论。 有个读者评论说:“发现很多前端的大领导多是后端。他们虽然懂一点前端,但总觉得前端简单。有时候,很难向其证明前端不好实现。” 这位朋友让我写一写,那我就写一写。 反正,不管啥事儿,我高低都能整两句,不说白不说,说了也白说。本文暂且算是一条评...
继续阅读 »

本文源于一条评论。


test.png


有个读者评论说:“发现很多前端的大领导多是后端。他们虽然懂一点前端,但总觉得前端简单。有时候,很难向其证明前端不好实现。


这位朋友让我写一写,那我就写一写。


反正,不管啥事儿,我高低都能整两句,不说白不说,说了也白说。本文暂且算是一条评论回复吧。愿意看扯淡的同学可以留一下。


现象分析


首先,前半句让我很有同感。因为,我就是前端出身,并且在研发管理岗上又待过。这个身份,让我既了解前端的工作流程,又经常和后端平级一起交流经验。这有点卧底的意思。


有一次,我们几个朋友聚餐闲聊。他们也都是各个公司的研发负责人。大家就各自吐槽自己的项目。


有一个人就说:“我们公司,有一个干前端的带项目了,让他搞得一团糟”


另一个人听到后,就叹了一口气:“唉!这不是外行领导内行吗?!


我当时听了感觉有点别扭。我扫视了一圈儿,好像就我一个前端。因此,我也点了点头(默念:我是卧底)。


是啊,一般的项目、研发管理岗,多是后端开发。因为后端是业务的设计者,他掌握着基本的数据结构以及业务流转。而前端在他们看来,就是调调API,然后把结果展示到页面上。


另外,一般项目遇到大事小情,也都是找后端处理。比如手动修改数据、批量生成数据等等。从这里可以看出,项目的“根基”在后端手里。


互联网编程界流传着一条鄙视链。它是这样说的,搞汇编的鄙视写C语言的,写C的鄙视做Java的,写Java的鄙视敲Python的,搞Python的鄙视写js的,搞js的鄙视作图的。然后,作图的设计师周末带着妹子看电影,前面的那些大哥继续加班写代码。


当然,我们提倡一个友好的环境,不提倡三六九等。这里只是调侃一下,采用夸张的手法来阐述一种现象。


这里所谓的“鄙视”,其本质是源于谁更接近原理。


比如写汇编的人,他认为自己的代码能直接调用“CPU”、“内存”,可以直接操纵硬件。有些前端会认为美工设计的东西,得依靠自己来实现,并且他们不懂技术就乱设计,还有脸要求100%还原。


所以啊,有些只做过后端的人,可能会认为前端开发没啥东西。


好了,上面是现象分析。关于谁更有优越感,这不能细究啊,因为这是没有结论的。如果搞一个辩论赛,就比方说”Python一句话,汇编写三年“之类论据,各有千秋。各种语言既然能出现,必定有它的妙用。


我就是胡扯啊。不管您是哪个工种,请不要对号入座。如果您觉得被冒犯了,可以评论“啥都不懂”,然后离开。千万不要砸自己的电脑。


下面再聊聊第二部分,面对这种情况,有什么好的应对措施?


应对方法


我感觉,后端出身的负责人,就算是出于人际关系,也是会尊重前端开发的


“小张啊,对于前端我不是很懂。所以请帮我评估下前端的工期。最好列得细致一些,这样有利于我了解人员的工作量。弄完了,发给我,我们再讨论一下!”


一般都是这么做。


这种情况,我们就老老实实给他上报就行了,顶多加上10%~30%处理未知风险的时间。


但是,他如果这样说:“什么?这个功能你要做5天?给你1天都多!


这时,他是你的领导,对你又有考核,你怎么办?


你心里一酸:“我离职吧!小爷我受不了这委屈!”


这……当然也可以。


如果你有更好的去处,可以这样。就算是没有这回事,有好去处也赶紧去。


但是,如果这个公司整体还行呢?只是这个直接领导有问题。那么你可以考虑,这个领导是流水的,你俩处不了多长时间。哪个公司每年不搞个组织架构调整啊?你找个看上的领导,吃一顿饭,求个投靠呗。


或者,这个领导只是因为不懂才这样。谁又能样样精通呢?给他说明白,他可能就想通了。人是需要说服的。


如果你奔着和平友好的心态去,那么可以试试以下几点:


第一,列出复杂原因


既然你认为难以实现,那肯定是难以实现。具体哪里不好实现,你得说说


记得刚工作时,我去找后端PK,我问他:“你这个接口怎么就难改了?你不改这个字段,我们得多调好几个接口,而且还对应不起来!”


后端回复我:“首先,ES……;其次,mango……;最后,redis……”


我就像是被反复地往水缸中按,听得”呜噜呜噜“的,一片茫然。


虽然,我当时不懂后端,但我觉得他从气势上,就显得有道理


到后来,还是同样的场景。我变成了项目经理,有一个年轻的后端也是这么回复我的。


我说:“首先,你不用……;其次,数据就没在……;最后,只需要操作……”。他听完,挠了挠头说,好像确实可以这么做。


所以,你要列出难以实现的地方。比如,没有成熟的UI组件,需要自己重新写一个。又或者,某种特效,业内都没有,很难做到。再或者,某个接口定得太复杂,循环调组数据,会产生什么样的问题。


如果他说“我看到某某软件就是这样”。


你可以说,自己只是一个初级前端,完成那些,得好多高级前端一起研究才行。


如果你懂后端,可以做一下类比,比如哪些功能等同于多库多表查询、多线程并发问题等等,这可以辅助他理解。不过,如果能到这一步,他的位子好像得换你来坐。


第二,给出替代方案


这个方案,适用于”我虽然做不了,但我能解决你的问题“。


就比如那个经典的打洞问题。需求是用电钻在墙上钻一个孔,我搞不定电钻,但是我用锤子和钉子,一样能搞出一个孔来。


如果,你遇到难以实现的需求。比如,让你画一个很有特色的扇形玫瑰图。你觉得不好实现,不用去抱怨UI,这只会激化问题。咱可以给他提供几个业界成熟的组件。然后,告诉他,哪里能改,都能改什么(颜色、间距、字体……)。


我们有些年轻的程序员很直,遇到不顺心的事情就怼。像我们大龄程序员就不这样。因为有房贷,上有老下有小。年龄大的程序员就想,到底是哪里不合适,我得怎样通过我的经验来促成这件事——这并不光彩,年轻人就得有年轻人的样子。


第二招是给出替代方案。那样难以实现,你看这样行不行


第三,车轮战,搞铺垫


你可能遇到了一个硬茬。他就是不变通,他不想听,也不想懂,坚持“怎么实现我不管,明天之前要做完”。


那他可能有自己的压力,比如老板就是这么要求他的。他转手就来要求你。


你俩的前提可能是不一样。记住我一句话,没有共同前提,是不能做对比的。你是员工,他是领导,他不能要求你和他有一样的压力,这就像你不能要求,你和他有一样的待遇。多数PUA,就是拿不同的前提,去要求同样的结果。


那你就得开始为以后扯皮找铺垫了。


如果你们组有多个前端,可以发动大家去进谏。


”张工啊,这个需求,确实不简单,不光是小刘,我看了,也觉得不好实现“


你一个人说了他不信,人多了可能就信了。


如果还是不信。那没关系,已经将风险提前抛出了


“这个需求,确实不好实现。非要这么实现,可能有风险。工期、测试、上线后的稳定性、用户体验等,都可能会出现一些问题”


你要表现出为了项目担忧,而不是不想做的样子。如果,以后真发生了问题,你可以拿出“之前早就说过,多次反馈,无人理睬”这类的说辞。


”你居然不想着如何解决问题,反倒先想如何逃避责任?!“


因此说,这是下下策。不建议程序员玩带有心机的东西。


以上都是浅层次的解读。因为具体还需要结合每个公司,每个领导,每种局面。


总之,想要解决问题,就得想办法


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

Android 14 快速适配要点

随着 Google I/O 2023 发布的 Android beta2 ,预计 Android 14 将在2023年第三季度发布,目前看整体需要适配的内容已经趋向稳定,那就根据官方文档简单做个适配要点总结吧。 如何做到最优雅的版本适配?那就是尽可能提高 m...
继续阅读 »

随着 Google I/O 2023 发布的 Android beta2 ,预计 Android 14 将在2023年第三季度发布,目前看整体需要适配的内容已经趋向稳定,那就根据官方文档简单做个适配要点总结吧。



如何做到最优雅的版本适配?那就是尽可能提高 minitSdkVersion ,说服老板相信低版本用户无价值论,低版本用户更多是羊毛党~



针对 Android 14 或更高版本的应用


这部分主要是影响 targetSdkVersion 34 的情况 ,目前 Google Play 已经开始要求 33 了,相信未来 34 也不远了。



前台服务类型


targetSdkVersion 34 的情况下,必须为应用内的每个前台服务(foreground-services) 指定至少一种前台服务类型。


前台服务类型是在 Android 10 引入的,通过 android:foregroundServiceType 可以指定 <service> 的服务类型,可供选择的前台服务类型有:



例如:

<manifest ...>
 <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
 <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
   <application ...>
     <service
         android:name=".MyMediaPlaybackService"
         android:foregroundServiceType="mediaPlayback"
         android:exported="false">
     </service>
   </application>
</manifest>

如果你 App 中的用例与这些类型中的任何一种都不相关,那么建议还是将服务迁移成 WorkManager jobs



更多详细可见:developer.android.com/about/versi…



安全


对 pending/implicit intent 的限制


对于面向 Android 14 的应用,Android 通过以下方式限制应用向内部应用组件发送隐式 intent:



  • 隐式 intent 仅传递给导出的组件,应用必须使用明确的 intent 来交付给未导出的组件,或者将组件标记为已导出(exported) 。

  • 如果应用创建一个 mutable pending intent ,但 intent 未指定组件或包,系统现在会抛出异常。


这些更改可防止恶意应用拦截只供给用内部组件使用的隐式 intent,例如:

<activity
   android:name=".AppActivity"
   android:exported="false">
   <intent-filter>
       <action android:name="com.example.action.APP_ACTION" />
       <category android:name="android.intent.category.DEFAULT" />
   </intent-filter>
</activity>

如果应用尝试使用隐式 intent 启动该 activity,则会抛出异常:

// Throws an exception when targeting Android 14.
context.startActivity(Intent("com.example.action.APP_ACTION"))

要启动未导出的 Activity,应用应改用显式 Intent:

// This makes the intent explicit.
val explicitIntent =
       Intent("com.example.action.APP_ACTION")
explicitIntent.apply {
   package = context.packageName
}
context.startActivity(explicitIntent)

运行时注册的广播接收器必须指定导出行为


以 Android 14 为目标,并使用 context-registered receiversContextCompat.registerReceiver)应用和服务的需要指定一个标志,以指示接收器是否应导出到设备上的所有其他应用:分别为 RECEIVER_EXPORTEDRECEIVER_NOT_EXPORTED

val filter = IntentFilter(APP_SPECIFIC_BROADCAST)
val listenToBroadcastsFromOtherApps = false
val receiverFlags = if (listenToBroadcastsFromOtherApps) {
   ContextCompat.RECEIVER_EXPORTED
} else {
   ContextCompat.RECEIVER_NOT_EXPORTED
}
ContextCompat.registerReceiver(context, br, filter, receiverFlags)

仅接收系统广播的接收器例外


如果应用仅通过 Context#registerReceiver 方法为系统广播注册接收器时,那么它可以不在注册接收器时指定标志, 例如 android.intent.action.AIRPLANE_MODE


更安全的动态代码加载


如果应用以 Android 14 为目标平台并使用动态代码加载 (DCL),则所有动态加载的文件都必须标记为只读,否则,系统会抛出异常。


我们建议应用尽可能避免动态加载代码,因为这样做会大大增加应用因代码注入或代码篡改而受到危害的风险。


如果必须动态加载代码,请使用以下方法将动态加载的文件(例如 DEX、JAR 或 APK 文件)在文件打开后和写入任何内容之前立即设置为只读:

val jar = File("DYNAMICALLY_LOADED_FILE.jar")
val os = FileOutputStream(jar)
os.use {
   // Set the file to read-only first to prevent race conditions
   jar.setReadOnly()
   // Then write the actual file content
}
val cl = PathClassLoader(jar, parentClassLoader)

处理已存在的动态加载文件


为防止现有动态加载文件抛出异常,我们建议可以尝试在应用中再次动态加载文件之前,删除并重新创建这些文件。


重新创建文件时,请按照前面的指导在写入时将文件标记为只读,或者将现有文件重新标记为只读,但在这种情况下,强烈建议首先验证文件的完整性(例如,通过根据可信值检查文件的签名),以帮助保护应用免受恶意操作。


Zip path traversal


对于针对 Android 14 的应用,Android 通过以下方式防止 Zip 路径遍历漏洞:如果 zip 文件条目名称包含 “..” 或以 “/” 开头,则 ZipFile(String)ZipInputStream.getNextEntry() 会抛出一个 ZipException


应用可以通过调用 dalvik.system.ZipPathValidator.clearCallback() 选择退出验证。


从后台启动活动的附加限制


针对 Android 14 的应用,系统进一步限制了应用在后台启动 Activity 的时间:



这些更改扩展了现有的一组限制 ,通过防止恶意应用滥用 API 从后台启动破坏性活动来保护用户。



具体可见:developer.android.com/guide/compo…



OpenJDK 17


Android 14 会要求的 OpenJDK 17 的支持,这对一些语法上可以会有一定影响,例如:



  • 对正则表达式的更改:现在不允许无效的组引用

  • UUID 处理java.util.UUID.fromString() 方法现在在验证输入参数时会进行更严格的检查

  • ProGuard 问题:在某些情况下,如果使用 ProGuard 缩小、混淆和优化应用,添加 java.lang.ClassValue 会导致出现问题,问题源于 Kotlin 库,库会根据是否 Class.forName("java.lang.ClassValue") 返回类来更改运行时行为。



反正适配 OpenJDK 17 就对了,新版 Android Studio Flamingo 也默认内置 OpenJDK 17 了。



针对所有版本的 Android 14 变更


以下行为主要针对在 Android 14 上运行的所有应用,可以看到从 Android 10 开始, Androd Team 针对这些变动越来越强势。


核心功能


默认情况下拒绝计划精确提醒


精确提醒(Exact alarms)用于用户有目的的通知,或用于需要在精确时间发生的操作情况,从 Android 14 开始,该 SCHEDULE_EXACT_ALARM 权限不再预先授予大多数新安装的针对 Android 13 及更高版本的应用,默认情况下该权限会被拒绝。



如果用户通过备份还原操作将应用数据传输到运行Android 14 的设备上,权限仍然会被拒绝。如果现有的应用已经拥有该权限,它将在设备升级到 Android 14 时预先授予。



而现在需要权限 SCHEDULE_EXACT_ALARM 才能通过以下 API 启动确切的提醒,否则将抛出 SecurityException




注意: 如果确切的 alarms 是使用 OnAlarmListener 对象设置的,例如在 setExact API 中,SCHEDULE_EXACT_ALARM 则不需要权限。



现有的权限最佳实践 SCHEDULE_EXACT_ALARM 仍然适用,包括以下内容:




详细可见:developer.android.com/about/versi…



Context-registered 广播在缓存应用时排队


在 Android 14 上,当应用处于缓存状态时,系统可能会将上下文注册的广播放入队列中。


这类似于 Android 12(API 级别 31)为异步活页夹事务引入的排队行为,清单声明的广播不会排队,并且应用会从缓存状态中删除以进行广播传输。


当应用离开缓存状态时,例如返回前台,系统会传送任何排队中的广播,某些广播的多个实例可以合并为一个广播。


根据其他因素(例如系统运行状况),应用可能会从缓存状态中删除,并且先前排队的所有广播都会被传送。


应用只能杀死自己的后台进程


从 Android 14 开始,应用调用 killBackgroundProcesses() 时,该 API 只能杀死自己应用的后台进程。


如果传入其他应用的包名,该方法对该应用的后台进程没有影响,Logcat中会出现如下信息:

Invalid packageName: com.example.anotherapp

应用不应该使用 killBackgroundProcesses() API ,或以其他方式尝试影响其他应用的进程生命周期,即使是在较旧的操作系统版本上。



Android 旨在将缓存的应用保留在后台,并在系统需要内存时自动终止它们,如果应用出现不必要地杀死其他应用,它会降低系统性能并增加电池消耗,因为稍后需要完全重启这些应用,比恢复现有的缓存应用占用的资源要多得多。



安全


最低可安装目标 API 级别


从 Android 14 开始,无法安装 targetSdkVersion 低于 23 的应用 ,要求应用必须满足这些最低目标 API 级别,这样可以提高用户的安全性和隐私性。



恶意软件通常以较旧的 API 级别为 target,以绕过较新 Android 版本中引入的安全和隐私保护,例如一些恶意软件应用使用 targetSdkVersion 22 的来避免受到 Android 6.0 的运行时权限模型的约束。



Android 14 的这一变化使恶意软件更难避开安全和隐私管理,尝试安装针对较低 API 级别的应用将导致安装失败,并在 Logcat 中显示以下消息:

INSTALL_FAILED_DEPRECATED_SDK_VERSION: App package must target at least SDK version 23, but found 7

在升级到 Android 14 的设备上,任何低于targetSdkVersion23 的应用都将保持安装状态,如果你需要针对较旧 API 级别的应用进行测试,请使用以下 ADB 命令:

adb install --bypass-low-target-sdk-block FILENAME.apk

用户体验


授予对照片和视频的部分访问权限


注意: 如果你的应用已经使用了系统照片选择器(photopicker),那么无需进行任何改动。


在 Android 14 上,当应用请求 Android 13(API 级别 33)中引入的任何 READ_MEDIA_IMAGESREAD_MEDIA_VIDEO 媒体权限时,用户可以授予对其照片和视频的部分访问权限。



新对话框显示以下权限选项:



  • 选择照片和视频: Android 14 中的新功能,用户选择他们想要提供给应用的特定照片和视频。

  • 全部允许:用户授予对设备上所有照片和视频的完整库访问权限。

  • 不允许:用户拒绝所有访问。


要在应用中出现改逻辑,可以声明新的 READ_MEDIA_VISUAL_USER_SELECTED 权限。



详细可见:developer.android.com/about/versi…



安全的全屏 Intent 通知


在 Android 11(API 级别 30)中,任何应用都可以通过 Notification.Builder.setFullScreenIntent 在手机锁定时发送全屏 intent。


一般可以通过在 AndroidManifest 中声明 USE_FULL_SCREEN_INTENT 权限来在应用安装时自动授予该权限 ,全屏 intent 通知专为需要用户立即关注的极高优先级通知而设计,例如来电或用户配置的闹钟设置。



从 Android 14 开始,允许使用该权限的应用仅限于提供通话和闹钟的应用,Google Play 商店会撤销任何不符合该配置文件的应用的默认权限。



在用户更新到 Android 14 之前,该权限对安装在手机上的应用保持启用状态,用户可以打开和关闭该权限。


开发者可以使用新的API NotificationManager.canUseFullScreenIntent 来检查应用是否具有权限;如果没有,应用可以使用新的 ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT 来启动跳转到可以授予权限的设置页面。


改变用户体验不可关闭通知的方式


如果你的应用向用户显示不可关闭的前台通知,Android 14 已更改行为以允许用户关闭此类通知。


此次更改适用于通过 Notification.Builder#setOngoing(true) 或者 NotificationCompat.Builder#setOngoing(true) 来设置 Notification.FLAG_ONGOING_EVENT 从而阻止用户关闭前台通知的应用 的行为。


现在 FLAG_ONGOING_EVENT 的行为已经改变,用户实际上可以关闭此类通知。


在以下情况下,该类通知仍然不可关闭:



  • 当手机被锁定时

  • 如果用户选择清除所有通知操作(这有助于防止意外)


此外,新行为不适用于以下用例中的不可关闭通知:



  • 使用 CallStyle与真实通话相关的通知

  • 使用 MediaStyle 创建的通知

  • 设备策略控制器 (DPC) 和企业支持包


辅助功能


非线性字体缩放至 200%


从 Android 14 开始,系统支持高达 200% 的字体缩放,为弱视用户提供符合 Web 内容无障碍指南 (WCAG) 的额外无障碍选项。


如果已经使用缩放像素 (sp) 单位来定义文本大小,那么此次更改可能不会对应用产生重大影响。


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

DialogFragment 与 BottomSheetDialogFragment

DialogFragment Android 中 Dialog 并没办法感知生命周期,但 Frg 可以感知,所以将 Diglog 与 Frg 结合后生成 DialogFragment,它提供了可以感知生命周期的 Dialog。另外 DialogFragment...
继续阅读 »

DialogFragment


Android 中 Dialog 并没办法感知生命周期,但 Frg 可以感知,所以将 Diglog 与 Frg 结合后生成 DialogFragment,它提供了可以感知生命周期的 Dialog。另外 DialogFragment 也继承 Fragment,所以也可以当作普通的 Fragment 使用。


由于 DialogFragment 可以感知生命周期,而且还处理了 Dialog 状态的保存与恢复,所以 DialogFragment 应多用


原理


DialogFragment 中的 Dialog 是在 onGetLayoutInflater 中创建的,该方法会先于 onCreateView() 调用,但后于 onCreate()。


第一个问题:DialogFragment 当作 Dialog 使用时为什么不会显示到 Activity 布局中,而是以 Dialog 形式出现。将 DialogFragment 当作 Dialog 用时需要调用其 show 方法,其内部也是使用了 FragmentTransaction,只不过依赖的 containerViewId 是 0,因此无法添加到某个 View 上,也就不会在 Activity 布局中显示。

// show() 方法节选
FragmentManager manager = ....
FragmentTransaction ft = manager.beginTransaction();
ft.setReorderingAllowed(true);
ft.add(this, tag);
ft.commit();

// FragmentTransaction::add() 方法
public FragmentTransaction add(@NonNull Fragment fragment, @Nullable String tag) {
// 主要第一个参数是 0,这就导致调用 show 方法时不会显示到具体界面
doAddOp(0, fragment, tag, OP_ADD);
return this;
}

// 将 Frg 当作普通 Frg 使用时会调用该方法
public FragmentTransaction add(@IdRes int containerViewId, @NonNull Fragment fragment) {
// 此时 containerViewId 就不是 0,所以 Fragment 中的 view 会显示到界面上
doAddOp(containerViewId, fragment, null, OP_ADD);
return this;
}

通过 show() 方法将当前 Fragment 绑定到了某一个 Activity,当 Frg 感知到生命周期发生变化时就会将 Dialog 显示或隐藏,而且在 onSaveInstanceState 等方法也处理了状态的保存与显示。

// onResume() 回调中会显示 dialog
// onStop() 回调中隐藏 dialog
// onDestroyView() 中会销毁 Dialog
public void onStop() {
super.onStop();
if (mDialog != null) {
mDialog.hide();
}
}

第二个问题:onCreateView() 返回的 View 有什么用onCreateView() 返回的 View 就是 Dialog 要显示的内容。在 DialogFragment::onAttach() 中会添加一个监听

// mObserver 中通过 requireView() 拿到 onCreateView() 的返回值
// 同时调用 Dialog::setContentView() 将返回值设置成 dialog 显示的内容
// mObserver 节选
if (lifecycleOwner != null && mShowsDialog) {
// 拿到 onCreateView 返回值
View view = requireView();
if (view.getParent() != null) {
throw new IllegalStateException(
"DialogFragment can not be attached to a container view");
}
if (mDialog != null) {
mDialog.setContentView(view);
}
}
// onAttach() 节选
getViewLifecycleOwnerLiveData().observeForever(mObserver);

第三个问题:上面 mObserver 看出如果不重写 onCreateView(),requireView() 应该会报错的,为啥使用时不会出问题。这个主要在 Fragment::performCreateView() 中

mViewLifecycleOwner = new FragmentViewLifecycleOwner(this, getViewModelStore());
// 调用 onCreateView() 默认返回 null
mView = onCreateView(inflater, container, savedInstanceState);
if (mView != null) {
// 只有 mView 不为 null 时才触发 liveData 更新,上面的 mObserver 才会收到通知
// Then inform any Observers of the new LifecycleOwner
mViewLifecycleOwnerLiveData.setValue(mViewLifecycleOwner);
} else {
mViewLifecycleOwner = null;
}

BottomSheetDialogFragment


继承于 DialogFragment,只不过它返回的 Dialog 是 BottomSheetDialog。从上面可以看出 onCreateView() 的返回值最终会传递给 Dialog::setContentView() 中。BottomSheetDialog::setContentView() 会调用 wrapInBottomSheet(),最核心的两句就是:

// 它会初始化一个 View,该 View 就是 Dialog 要显示的 View
ensureContainerAndBehavior();
// view 就是我们传入 setContentView 中的 view
// bottomSheet 就是上面 View 的一个子 View
bottomSheet.addView(view);

// 生成 Dialog 要显示的 View
private FrameLayout ensureContainerAndBehavior() {
if (container == null) {
container =
(FrameLayout) View.inflate(getContext(), R.layout.design_bottom_sheet_dialog, null);
coordinator = (CoordinatorLayout) container.findViewById(R.id.coordinator);

// 查看上面的布局,可以发现 bottomSheet 是 CoordinatorLayout 的子类
// 其 behavior 是 com.google.android.material.bottomsheet.BottomSheetBehavior
bottomSheet = (FrameLayout) container.findViewById(R.id.design_bottom_sheet);
behavior = BottomSheetBehavior.from(bottomSheet);
behavior.addBottomSheetCallback(bottomSheetCallback);
behavior.setHideable(cancelable);
}
return container;
}

总之,BottomSheetDialogFragment 会将 onCreateView() 的返回结果包裹在 CoordinatorLayout 中,从而实现 bottom_sheet 效果


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

动手实现Kotlin协程同步切换线程,以及Kotlin协程是如何实现线程切换的

前言 突发奇想想搞一个同步切换线程的Kotlin协程,而不用各种withContext(){},可以减少嵌套且逻辑更清晰,想实现的结果如下图: 分析 实现我们想要的结果,首先需要知道协程为什么可以控制线程的切换以及在挂起函数恢复的时候回到原来设定的线程中 p...
继续阅读 »

前言


突发奇想想搞一个同步切换线程的Kotlin协程,而不用各种withContext(){},可以减少嵌套且逻辑更清晰,想实现的结果如下图:



分析


实现我们想要的结果,首先需要知道协程为什么可以控制线程的切换以及在挂起函数恢复的时候回到原来设定的线程中


ps:挂起函数比普通函数多出了两个操作:挂起和恢复,具体参考:Kotlin协程在项目中的实际应用_lt的博客-CSDN博客_kotlin协程使用


其实控制线程切换是协程库内内置的一个拦截器类:ContinuationInterceptor


拦截器是一个协程上下文元素(ContinuationInterceptor实现了Element接口 ,Element实现了CoroutineContext接口)


拦截器的作用是将协程体包装一层后,拦截其恢复功能(resumeWith),这样就可以在协程恢复的时候将包装在其内部的协程体在相应的线程中恢复执行(执行resumeWith方法)


比如协程自带的Dispatchers.Main,Dispatchers.IO等都是协程拦截器,下面简单分析下Dispatchers.IO拦截器



我们点进去IO的定义,兜兜转转的找到其实现类LimitingDispatcher,其继承了ExecutorCoroutineDispatcher ,ExecutorCoroutineDispatcher继承了CoroutineDispatcher ,CoroutineDispatcher实现了ContinuationInterceptor接口,也就是其最终实现了协程拦截器的接口


CoroutineDispatcher重写了拦截器的interceptContinuation方法,该方法就是用来包装并拦截的



然后我们在看看DispatchedContinuation的resumeWith方法(也就是如何拦截并将包装的协程体运行在子线程的)


ps:其实走的是resumeCancellableWith方法,因为协程内部做了一个判断,IO的拦截器是继承了DispatchedContinuation的



第一个红框IO那是写死的true,所以只会走第一个流程,而第二个红框你可以简单的理解为将后续任务(下面的代码逻辑)放在这个dispatcher的线程池中运行(就相当于将协程中的代码的线程放到了IO子线程中运行了)


通过上面的分析,其实IO的拦截器可以简单理解为如下代码:



实现


那我们是不是可以将拦截器从协程上下文中移除呢?我试了下并不行,发现是在launch的时候会自动判断,如果没有拦截器则默认附加Dispatchers.Default拦截器用于将操作置于子线程中


ps:可以通过遍历来查看当前协程上下文中都有哪些协程元素(下面是反射的实现,可以使用系统提供的fold来遍历):

/**
* 通过反射遍历协程上下文中的元素
*/
fun CoroutineContext.forEach(action: (CoroutineContext.Element) -> Unit) {
val modeClass = Class.forName("kotlin.coroutines.CombinedContext")
val elementField = modeClass.getDeclaredField("element")
elementField.isAccessible = true
val leftField = modeClass.getDeclaredField("left")
leftField.isAccessible = true
var context: CoroutineContext? = this
while (context?.javaClass == modeClass) {
(elementField.get(context) as? CoroutineContext.Element)?.let(action)
context = leftField.get(context) as? CoroutineContext
}
(context as? CoroutineContext.Element)?.let(action)
}

coroutineContext.forEach {
it.toString().e()
}


pps:协程上下文其实是以链表形式来存储的,CombinedContext就相当于链表的Node节点,其element相当于数据,其left相当于下一个节点,而存储的最后一个节点是未经过CombinedContext包装的协程上下文元素(可能是节省空间);而不管协程上下文,或者CombinedContext的element和left,都是val的,这样上下文都是只读的,避免了并发修改的危险.


那现在看来其实我们只要自己写一个拦截器,然后在运行协程的时候附加上去,线程切换就可以由我们来控制了,那实现起来其实也很简单,代码如下:

/**
* 同步切换线程的协程元素,需要注意所有挂起函数都有可能影响到后面的线程,所以需要注意:最好内部使用不会切换线程的挂起函数(或者你清楚使用的后果)
*/
fun CoroutineScope.launchSyncSwitchThread(block: suspend SyncSwitchThreadCoroutineScope.() -> Unit): Job =
launch(SyncSwitchThreadContinuationInterceptor) { SyncSwitchThreadCoroutineScope(this@launch).block() }

//拦截器
object SyncSwitchThreadContinuationInterceptor : ContinuationInterceptor {
override val key: CoroutineContext.Key<*> = ContinuationInterceptor

override fun <T> interceptContinuation(continuation: Continuation<T>): Continuation<T> = SyncSwitchThreadContinuation(continuation)

//协程体包装类
class SyncSwitchThreadContinuation<T>(private val continuation: Continuation<T>) : Continuation<T> {
override val context: CoroutineContext = continuation.context

//这里我们不进行拦截恢复方法,直接使用恢复者(谁调用了resume)的线程
override fun resumeWith(result: Result<T>): Unit = continuation.resumeWith(result)
}

//协程作用域包装类,用于控制toMain等方法不会被别的地方使用
class SyncSwitchThreadCoroutineScope(coroutineScope: CoroutineScope) : CoroutineScope by coroutineScope {
private suspend inline fun suspendToThread(crossinline threadFunction: (()->Unit) -> Unit) =
suspendCoroutine<Unit> {
threadFunction {
if (it.context.isActive)
it.resume(Unit)
}
}

//切换到主线程,[force]是否强制post到主线程(false:如果当前是主线程则不会post)
suspend fun toMain(force: Boolean = false) {
if (!force && isMainThread) return
suspendToThread(HandlerPool::postEmptyListener)//Handler#post方法
}

//切换到单例子线程
suspend fun toSingle(): Unit = suspendToThread(ThreadPool::submitToSingleThreadPool)//提交到单线程线程池

suspend fun toIO(): Unit = suspendToThread(ThreadPool::submitToCacheThreadPool)//提交到子线程池

suspend fun toCPU(): Unit = suspendToThread(ThreadPool::submitToCPUThreadPool)//提交到CPU密集型子线程池

//或者为了理解起来简单减少封装写成如下方式
suspend fun toCPU(): Unit = suspendCoroutine {
ThreadPool.submitToCacheThreadPool {
if(it.context.isActive)
it.resume(Unit)
}
}
}
}


ps:第一次运行的线程是启动它的线程


pps:这里我们包装了一下协程作用域CoroutineScope,可以防止toMain这些方法用在别的地方造成歧义且无用


然后我们就可以像开头那样使用了


或者通过协程上下文的plus(+)方法来替换掉默认的拦截器:



最后在封装一下:

fun CoroutineScope.launchSyncSwitchThread(block: suspend SyncSwitchThreadCoroutineScope.() -> Unit): Job =
launch(SyncSwitchThreadContinuationInterceptor) { SyncSwitchThreadCoroutineScope(this@launch).block() }


使用方式就和最开始的图一样了


结语


这样就ok了,其实任何事情只要了解原理,就可以很快的想到方案,如果不清楚原理,就会对一些事情一头雾水


ps:其实使用Dispatchers.Unconfined也可以实现相同的效果2333,但是了解原理还是更重要


pps:如果想限制launchSyncSwitchThread的lambda范围内只能使用自己定义的几个挂起函数,只需要给SyncSwitchThreadCoroutineScope类加上@RestrictsSuspension注解即可,这样在SyncSwitchThreadCoroutineScope的作用域内,只能使用他内部定义的挂起函数,可以有效减少别的挂起函数意外切换掉线程的情况


end


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

思考 | 差别之心

奶奶去世后,我们一家人打算去庙里超度,于是带了两袋米、两桶油还有些许蔬果。我把米油拎到了大殿佛像的右侧,转头瞥见左侧也放了些物资,应该是别人超度时带来的。走过去一数,整整六袋米六桶油。于是我的内心便起了妄念,“别人带了六袋,为什么我们只带了两袋?是不是我们带少...
继续阅读 »

奶奶去世后,我们一家人打算去庙里超度,于是带了两袋米、两桶油还有些许蔬果。我把米油拎到了大殿佛像的右侧,转头瞥见左侧也放了些物资,应该是别人超度时带来的。走过去一数,整整六袋米六桶油。于是我的内心便起了妄念,“别人带了六袋,为什么我们只带了两袋?是不是我们带少了?我要不要再去买几袋跟别人一样?”放在以前,我不会对这个想法有过多的审视,因为它仿佛呼吸吃饭一样自然。但是那一天,我忽然对这个念头产生了怀疑。


我想了很久,发现这背后的根源在于“差别之心”。万事万物皆有不同,这是事物的外在形式。然而如何看待这些不同,是我们内心的事情。因有差别之心,所以攀比、嫉妒渐生;因有差别之心,所以掩饰、虚伪渐生;因有差别之心,所以空虚、焦虑渐生。前段时间我打算买车,走在路上会仔细打量路过的各种车辆,但我关注最多的居然是牌子(价格)。譬如保时捷的车,即便从审美角度来说我并不欣赏那两个凸出的“眼泡”,但内心仍然会高看它一眼。又或者我读了一所还不错的大学,但看到清华时心里仍然会羡慕,看到普通的学校又会难掩鄙夷。这是一种非常真实,且又十分糟糕的心态,捧高踩低,欺软怕硬。世人都崇尚慕强,然而慕强好么?曾经我也会不假思索的赞同,然而现在我会认为:慕强就一定会凌弱。这种凌弱并非行为上,而是心理上的鄙夷和轻视。因为慕强和凌弱同源,皆来自于“差别之心”。


这种“差别之心”在东亚的文化圈中十分泛滥,使得每个人都活在别人的目光里,活在一场虚拟的排位赛中。考试讲究名次,出了社会谈论工资,买个房也要分高低贵贱、内中外环。人生永远在比,别人怎么样了,我怎么样了。就像许嵩歌词里唱的,“飞上枝头的都风趣,占了巢的都在窃喜”。然而人生到处是枝头,占了这处,那下处呢?


“差别之心”的泛滥,其中一个原因就在于居住环境的拥塞。因为拥塞,所以需要抢夺有限的资源;因为拥塞,所以需要承受别人更多的目光。我想这也是为什么东亚这块人口密度极高的土地上,竞争更加激烈的原因。最近这几年,每次回老家走在空旷的河堤时,内心总会升腾起一种熟悉又陌生的平静。之所以熟悉,是因为它总让我回想起儿时的各种场景:夕阳西下,各家的烟囱里飘出袅袅炊烟,人们收拾完农具走在通往家的路上;秋日的午后,睁开睡眼的刹那看见一片飘零的枫叶在空中打转,声音很轻,空气很凉;大雪纷飞的除夕夜,独自一人爬上楼顶,在硝烟弥漫的空气中感受着灯火忽明忽暗的闪烁。这些平静而美好的瞬间,构成我对人生最深层的留恋。然而之所以陌生,是因为我在钢筋水泥的城市里生活得越来越久,久到变得有些麻木。我想,这种趋势未来一定会引发反思,事实上日本在这方面已经先行一步。观察中日韩三国的文艺作品后,我有一个不成熟的观点:日本的影视作品中出现了不少平凡视角却安静美好的内容,像是一杯初上的新茶,清新,淡雅;韩国的影视作品中有不少社会阴暗面的讽刺和揭露,像是一杯呛人的烈酒,辛辣,刺激;而国内的影视作品则像一杯甜甜的奶茶,好喝,但不健康。


与“差别之心”相对的便是“平常心”。平常,意味着对每个来到你面前的个体都保有同等的尊重和自然,不因为他是领导或权贵而卑躬屈膝,也不因他是无家可归、流落街头的浪人而轻视怠慢。佛家讲普渡众生,“无缘大慈,同体大悲”。这背后的根源是你我皆有佛性,佛是开悟的人,人是未开悟的佛。当我们认识到人人皆有佛性,或人这个主体有着无穷的价值时,那么他的穿着和谈吐将显得暗淡无光,由这些附着品产生的差别也将毫无意义。


如今,“平凡”仿佛成了“失败”的近义词,我们羞于将这个词冠名在自己身上。因此我们要折腾,要拼搏,要努力地甩掉与生俱来的“平凡”。多么可笑又执拗的观念,然而它却广泛地存在着。要我说,“平凡”从来都不是失败,它是人生大船的压舱石,是进可攻退可守的城池,是根基,是源头。商品社会邪恶的地方在于,它努力地要拔除我们内心所依凭的这个港湾,让我们成为无家可归的游子,然后在商品产生的种种符号中去短暂停留。


这些絮絮叨叨的话只是我个人的反思,抑或是我追寻初心的一种方式,实际上我离真正做到它们还差得很远。在我刚上小学的时候,一位同学的父亲因病早逝,于是学校举行全校募捐。回家后母亲给了我二十块钱,我却缠着她要一百块钱,那个我认知里最大数额的金钱。我哭着对母亲说,“二十块太少了,救不了他们一家”。如今这纷繁复杂的社会里,我时常分不清对错。但我总在心里提醒自己,“二十块太少了

作者:芦半山
来源:juejin.cn/post/7235806561724891192
,救不下他们一家”。

收起阅读 »

详解越权漏洞

1.1. 漏洞原理 越权漏洞是指应用程序未对当前用户操作的身份权限进行严格校验,导致用户可以操作超出自己管理权限范围的功能,从而操作一些非该用户可以操作的行为。简单来说,就是攻击者可以做一些本来不该他们做的事情(增删改查)。 1.2. 漏洞分类 主要分为 水...
继续阅读 »

1.1. 漏洞原理


越权漏洞是指应用程序未对当前用户操作的身份权限进行严格校验,导致用户可以操作超出自己管理权限范围的功能,从而操作一些非该用户可以操作的行为。简单来说,就是攻击者可以做一些本来不该他们做的事情(增删改查)


IDOR


1.2. 漏洞分类


主要分为 水平越权垂直越权 两大类


1.2.1. 水平越权


发生在具有相同权限级别的用户之间。攻击者通过利用这些漏洞,访问其他用户拥有的资源或执行与其权限级别不符的操作。


1.2.2. 垂直越权


发生在具有多个权限级别的系统中。攻击者通过利用这些漏洞,从一个低权限级别跳转到一个更高的权限级别。例如,攻击者从普通用户身份成功跃迁为管理员。


1.3. 漏洞举例


1.3.1. 水平越权


假设一个在线论坛应用程序,每个用户都有一个唯一的用户ID,并且用户可以通过URL访问他们自己的帖子。应用程序的某个页面的URL结构如下:


https://example.com/forum/posts?userId=<用户ID>

应用程序使用userId参数来标识要显示的用户的帖子。假设Alice的用户ID为1,Bob的用户ID为2。


Alice可以通过以下URL访问她自己的帖子:


https://example.com/forum/posts?userId=1

现在,如果Bob意识到URL参数是可变的,他可能尝试修改URL参数来访问Alice的帖子。他将尝试将URL参数修改为Alice的用户ID(1):


https://example.com/forum/posts?userId=1

如果应用程序没有正确实施访问控制机制,没有验证用户的身份和权限,那么Bob将成功地通过URL参数访问到Alice的帖子。


1.3.2. 垂直越权


假设一个电子商务网站,有两种用户角色:普通用户和管理员。普通用户有限的权限,只能查看和购买商品,而管理员则拥有更高的权限,可以添加、编辑和删除商品。


在正常情况下,只有管理员可以访问和执行与商品管理相关的操作。然而,如果应用程序没有正确实施访问控制和权限验证,那么普通用户可能尝试利用垂直越权漏洞提升为管理员角色,并执行未经授权的操作。


例如,普通用户Alice可能意识到应用程序的URL结构如下:


https://example.com/admin/manage-products

她可能尝试手动修改URL,将自己的用户角色从普通用户更改为管理员,如下所示:


https://example.com/admin/manage-products?role=admin

如果应用程序没有进行足够的验证和授权检查,就会错误地将Alice的角色更改为管理员,从而使她能够访问和执行与商品管理相关的操作。


1.4. 漏洞危害


具体以实际越权的功能为主,大多危害如下:



  1. 数据泄露:攻击者可以通过越权访问敏感数据,如个人信息、财务数据或其他敏感业务数据。这可能导致违反隐私法规、信用卡信息泄露或个人身份盗用等问题。

  2. 权限提升:攻击者可能利用越权漏洞提升其权限级别,获得系统管理员或其他高权限用户的特权。这可能导致对整个系统的完全控制,并进行更广泛的恶意活动。


1.5. 修复建议



  1. 实施严格的访问控制:确保在应用程序的各个层面上实施适当的访问控制机制,包括身份验证、会话管理和授权策略。对用户进行适当的身份验证和授权,仅允许其执行其所需的操作。

  2. 验证用户输入:应该对所有用户输入进行严格的验证和过滤,以防止攻击者通过构造恶意输入来利用越权漏洞。特别是对于涉及访问控制的操作,必须仔细验证用户请求的合法性。

  3. 最小权限原则:在分配用户权限时,采用最小权限原则,即给予用户所需的最低权限级别,以限制潜在的越权行为。用户只应具备完成其任务所需的最小权限。

  4. 安全审计和监控:建立安全审计和监控机制,对系统中的访问活动进行监视和记录。这可以帮助检测和响应越权行为,并提供对事件的审计跟踪。


作者:初始安全
来源:juejin.cn/post/7235801811525664825
收起阅读 »

我是这样实现并发任务控制的

web
尽管js是一门单线程的脚本语言,其同步代码我们是自上而下读取执行的,我们无法干涉其执行顺序,但是我们可以借助异步代码中的微任务队列来实现任务的并发任务控制。那我们用一个例子来带入一下。 如何使下面的代码按照我所想的效果来输出 function timeout(...
继续阅读 »

尽管js是一门单线程的脚本语言,其同步代码我们是自上而下读取执行的,我们无法干涉其执行顺序,但是我们可以借助异步代码中的微任务队列来实现任务的并发任务控制。那我们用一个例子来带入一下。


如何使下面的代码按照我所想的效果来输出


function timeout(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, time)
})
}

function addTask(time, name) {
superTask
.add(() => timeout(time))
.then(() => {
console.log(`任务${name}完成`);
})
}

addTask(10000, 1) // 10000 3
addTask(5000, 2) // 5000 1
addTask(3000, 3) // 8000 2
addTask(4000, 4) // 12000 4
addTask(5000, 5) // 15000 5

就是使得两个任务并发执行,当一个任务执行完了,下一个任务就接在空出来的任务队列中,以此类推。
在代码中我们可以看到在add方法调用后接了.then那么说明我们必须在add函数执行完返回出一个Promise对象


我们在实现并发任务控制之前需要明确并发几个任务执行我们应该可以人工控制


class SuperTask {
constructor(executeCount = 2) {
this.executeCount = executeCount; // 并发执行的任务数
this.runningCount = 0; // 正在执行的任务数
this.tasks = []; // 任务队列
}

// 添加任务
add(task) {
return new Promise((resolve,reject) => {
this.tasks.push({
task,
resolve,
reject
}); // add方法将任务添加到任务队列中,后面我会解释为什么需要这么做

/*接下来就是判断正在执行的任务,是不是达到了并发任务数量*/
this._run(); // 为了代码的可读性,将这个另外写一个方法
})
}

// 执行任务队列中的任务
_run() {
// 如果正在执行的任务数小于并发执行的任务数,那么我们就将任务队列队头的元素取出来执行
if(this.tasks.length && this.runningCount < this.executeCount) {
//任务队列中有任务, 并发任务队列有空余,可以执行任务
this.runningCount++;
const { task,resolve,reject } = this.tasks.shift();
task().then(resolve,reject).finally(res => {
this.runningCount--;
this._run();
})
}
}
}

利用Promise.then微任务的特性,我们可以控制Promise的状态改变时,再去取任务队列中队头元素再执行,这个地方有一些细节,就是add函数执行时,返回出的是一个Promise对象,但是我们需要将他resolve以及reject保存下来,使得所有的任务并不是共用同一个Promsie对象,而是每一个任务执行完都是独立的Promise,所以我们需要将其的resolve、reject保存下来,当任务并发执行的队列调用时再使用,再每个task执行完之后的.then之后就说明这个任务已经执行完了,此时this.runningCount--;再递归调用_run函数,考虑代码的完整性,我们使用Promise.finally来执行后续任务,Promise.finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。


代码测试


const superTask = new SuperTask(2)  // 并发执行两个任务

function addTask(time, name) {
superTask
.add(() => timeout(time))
.then(() => {
console.log(`任务${name}完成`);
})
}

addTask(10000, 1) // 10000 3
addTask(5000, 2) // 5000 1
addTask(3000, 3) // 8000 2
addTask(4000, 4) // 12000 4
addTask(5000, 5) // 15000 5

image.png

个人见解


在js中实现并发任务控制,其核心在于借助我们可控制的Promise的状态何时改变,在其状态改变时,说明之前在执行的任务已经执行完毕或者报错了,那么就应该执行后一个任务,但是如果只单纯使用Promsie.then去调用的话,如果其中某一个函数报错,那后续的就不会执行了,所以我们使用Promsie.finally来递归调用,为了保证每一个任务不会相互受影响,我们将其resolve和reject一并保存,在其执行时使用保存的resolve和reject,在后面接.finally这样就实现了。以上就是我个人对于并发任务的想法与见解,当然方式有各种各样,欢迎大家留言讨论。


作者:寒月十九
来源:juejin.cn/post/7209863093672394811
收起阅读 »

内存的清道夫——函数的尾调用

web
函数的尾调用 尾调用是什么,它能解决什么问题,他的存在意味着什么,为什么我叫他内存的清道夫,下面我将带读者通过概念,作用,尾巴递归三个方面来学习使用函数的尾调用。 尾调用概念 尾调用指的是在函数的最后一步通过return调用另一个函数 function fn(...
继续阅读 »



函数的尾调用


尾调用是什么,它能解决什么问题,他的存在意味着什么,为什么我叫他内存的清道夫,下面我将带读者通过概念作用尾巴递归三个方面来学习使用函数的尾调用。


尾调用概念


尾调用指的是在函数的最后一步通过return调用另一个函数


function fn() {
   return _fn()
}

如上就是一个标准的函数尾调用


尾调用的作用


尾调用的作用非常重要,它可以为我们节省在函数调用时的内存,这也是我为什么叫它内存的清道夫,我们先来看一下函数执行的底层原来,再来了解尾调用是如何节省内存的


函数执行中的帧


伴随函数的执行,函数会在内存中生成一个调用帧,我们先设定A,B,C三个函数,通过下面的形式调用(注意下面是普通的函数调用)


fn_A() {
   fn_B()
}
fn_B() {
   fn_C()
}

如上:A函数执行,执行B函数,B函数内执行C函数,函数执行的过程是这样的:



  • A执行生成一个A调用帧,在内部执行B函数

  • B函数生成一个调用帧在A调用帧上方

  • B函数内执行C函数,C函数执行生成一个调用帧在B调用帧上方,其本质就是一个调用帧栈

  • C函数执行完毕,C调用帧出栈

  • B函数执行完毕,B调用帧出栈

  • A函数执行完毕,A调用帧出栈


通过以上过程就能了解函数执行中会生成调用帧占用内存,而不断地嵌套函数会占据越来越多的内存空间,下面我们来看看尾调用是如何改变这一过程达到优化的效果。


尾调用优化的过程


那么如果我们使用尾调用来执行函数内部的函数。它的过程是怎么样的?


fn_A() {
  return fn_B()
}
fn_B() {
  return fn_C()
}


  • A执行生成一个A调用帧入栈

  • 由于B函数在尾部执行,无需A的资源,所有A调用帧出栈,生成B调用帧入栈

  • B函数执行,尾部调用C函数,无需B的资源,B调用帧出栈出栈,生成C调用帧入栈

  • C执行结束,C调用帧出栈


尾调用:在执行A函数中的B函数的前就可以对A调用帧进行出栈处理,也就是说在这连续嵌套一过程中,栈中只有一个调用帧,大大节省了内存空间,成为一名合格的内存清道夫!


注意:真正的尾调用是需要考虑到资源的占用,即B函数执行不需要A函数内的资源,才能算是真正的尾调用


一种特殊的尾调用


当尾调用的函数是自身的时候就诞生了一种特殊的尾调用形式即尾递归


function fn() {
   return fn()
}

正常的递归使用如果过多的话会产生栈溢出的现象,所以可以使用尾递归来解决这个问题,我们来看下面的例子


function fn(n) {
   if(n === 1) return 1
   return n * fn(n-1)
}
console.log(fn(6))
; // 720

如上是一个普通的递归函数求阶乘,那么我们可以使用尾递归来优化这个过程


function fn(n, tol) {
 if (n === 1) return tol;
 return fn(n - 1, n * tol);
}
console.log(fn(6, 1)); // 720

尾递归的实现


需要注意的是我们只有在严格模式下,才能开启尾调用模式,所以在其他场景我们需要使用其他的解决方案来替代尾调用,尾递归也同理,因为尾递归的过程其实是循环调用,所以利用循环调用可以变相实现尾递归,这里涉及到了一个名词:蹦床函数


function trampoline(f) {
 while (f && f instanceof Function) {
   f = f();
}
 return f;
}

如上就是一个蹦床函数的封装,传入的参数是要进行递归的函数,其作用是代替递归,进行循环调用传入参数,下面我们来看看具体应用


function num (x,y) {
   if(y > 0) {
       return num(x+1,y-1)
  }else {
       return x
  }
}
num(1,10000) // Maximum call stack size exceeded

Maximum call stack size exceeded就是栈溢出的报错,递归直接使用如果次数过多就会造成这样的现象,那么我们下面搭配蹦床函数使用。


function trampoline(f) {
 while (f && f instanceof Function) {
   f = f();
}
 return f;
}

function num(x, y) {
 if (y > 0) {
   return num.bind(null, x + 1, y - 1);
} else {
   return x;
}
}
console.log(trampoline(num(1, 1000))); // 1001

通过蹦床函数将递归函数纳入,以循环的形式调用,最后得到结果,不会发生栈溢出现象,总结来看,尾调用是切断函数与尾调用函数之间的联系,用完即释放,藕断丝不连,不占用内存的效果。


最后


函数的尾调用就到这里啦!如有错误,请多指教,欢迎关注猪痞

作者:猪痞恶霸
来源:juejin.cn/post/7125958517600550919
恶霸的JS进阶专栏。

收起阅读 »

深度介绍瀑布流布局

web
瀑布流布局 瀑布流又称瀑布流式布局,是比较流行的一种网站页面 布局方式。多行等宽元素排列,后面的元素依次添加到后面,接下来,要开始介绍这种布局如何实现 Html代码以及效果展示: 代码: 先使用一个container容器作为父容器,里面分别装了十个子容器b...
继续阅读 »

瀑布流布局



瀑布流又称瀑布流式布局,是比较流行的一种网站页面 布局方式。多行等宽元素排列,后面的元素依次添加到后面,接下来,要开始介绍这种布局如何实现
Html代码以及效果展示:


代码:


1.png
先使用一个container容器作为父容器,里面分别装了十个子容器box,这十个子容器下面分别装载了各自的样品图片。


效果展示:


2.png


Div是块级元素,所以每一张图片分别占据了一行。


 



接下来介绍瀑布流css的代码以及效果实现



Css的代码展示如下图:


3.png


在对整个页面自带的边框进行清除之后,对父容器container使用了相对定位,让其不需要脱离文档流,给子容器加上该有的样式后将其下面装载的图片进行父容器宽的100%继承,一般在这里设置只需要考虑宽或者高的一种继承设置即可。


 


效果展示:


4.png


因为每一张图片的高度不一致,所以图片排列并没有按照顺序排列。


 



JS部分的代码以及效果展示:



总体思路:


首先我们要获取所有我们要摆放的照片,根据用户视窗的宽度以及每一张图片的宽度计算出每一行会占据多少张图片,再计算第一列图片的高度,将下一列的图片分别对应最低高度的图片在其后面进行存放布局展示,然后更新新的高度,重复上述的操作。


 


首先,我们要调用imgLocation函数,将父容器和子容器放进这个函数进行调用。


5.png


Winodw.onload()函数的意思是必须等待网页全部加载完毕,包括在内的图片,然后再执行包裹代码。



接下来详细介绍imgLocation函数内容



6.png


在imgLocation函数里面设置parent和content两个形参,这两个形参将会对应调用函数的时候传递过来的两个实参,用cparent变量和ccontent变量分别对应parent(就是父容器container)以及content(就是父容器下对应的第一层子容器box)


getChildElement()函数是自己写的,再来介绍一下这个函数的内容:这个函数的作用就是取得父容器中的某一层子容器


代码展示:


7.png


说明:


首先我们设置一个变量数组contentArr存放最终取到的所有子容器,再设置一个变量存放所取得整个父容器的标签,直接通过标签来取得,显示就是直接用的是数组的形式。然后写一个for循环函数,在所有的标签下寻找对应的类名,便将其存放在contentArr数组中,最后返回一个数组形式,因此ccontent是一个数组形式。


 


接下来再回到imgLocation函数中,在获取所有要摆放的图片之后,我们要进行计算从哪一张图片是需要被操作,被摆放位置,计算第一行能存放多少图片。


代码展示:


8.png


说明:
winWidth装载的是用户的视窗宽度,imgWidth装载的是一张图片的宽度,因为每一张图片的宽度都是一致的,因此可以直接固定随意写一张图片的宽度,num存放的就是第一行能存放多少图片的张数。


接下来要开始操作第num+1章图片,先拿到第一列所有的图片的高度,我们使用一个数组进行高度的存放。


代码展示:


9.png


说明:


循环遍历取得的每一个子容器,前num个子容器只需要取得他们的高度即可,就是第一行的图片的高度。接下来这部分是我们要进行操作的box,math.min()这个里面装的是数字不是数组,调用apply将这个找最小值的方法借给数组用取得最小高度,然后minIndex拿到最低高度的图片所在的位置,就是下标。将需要操作的子容器图片的样式变成绝对定位,距离顶部的距离就是最低图片的高度,距离左边就是图片的小标乘以图片的宽度,因为每张图片的宽度都是一致的。最后再更新最矮的那一列高度,重复循环再找到新的最矮的高度进行排列。



所有代码展示:



10.png


11.png



效果展示:



12.png


作者:用户7299721929423
来源:juejin.cn/post/7233597845918679099
收起阅读 »

Vuex状态更新流程你都会了,面试官敢不给你发offer?

web
什么是Vuex?Vuex官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。 说到Vuex其实很多小伙伴的项目中基本都引入了,对Vuex...
继续阅读 »

什么是Vuex?Vuex官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。



说到Vuex其实很多小伙伴的项目中基本都引入了,对Vuex的使用其实并不陌生!但面试的时候,面试官突然问起:你对Vuex是怎么理解的?或者怎么看待Vuex?


此刻我估计有些小伙伴回答得还是磕磕盼盼,那么这篇文章就从O到1,再彻底讲一下我对Vuex的理解


首先咱从面试的角度出发?


1.面试官第一个问题:Vuex有哪几种属性?


共有5个核心属性:stategettersmutationsactionsmodules 。小伙伴们这里一定要背哦!不仅要背出来,还要英文发音标准哦。这是必须要装的逼!不然第一题都挂了,就没有然后了!


2.面试官第二个问题:请你说说这几属性的作用?


此时小伙伴们就要注意了,你不能这么回答:state是定义数据的,等同与vue里面的data啥啥啥的... getters又等同与vue里面的计算属性啥啥啥的...


这样回答虽说问题不大,但总又感觉好像差点意思!显得不专业,半吊子(半桶水),培训机构刚那个啥一样(纯属玩笑,没有任何贬低的意思)


咱换个思路,直接从Vuex的使用层面跟面试官去讲!咱先看官网流程图


1845321-20200916095435719-171834298.png


解析这张图之前咱先想一下,我们怎么获取Vuex里面的数据? 如果配置过Vuex的小伙伴此时就会说:新建store文件->index.js,进行如下配置



//store/index.js
import Vue from 'vue' //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //在vue里面注入

export default new Vuex.Store({
state: { //存放状态
flag:'我是一个全局变量'
},
})


//全局入口文件main.js
import store from './store/index.js'
new Vue({
store,
...//省
})

这样一个简单的vuex就配置好了。接下来怎么去页面引用呢? 非常简单!



//组件
<template>
<div>{{$store.state.flag}}</div>
</template>

<script>
//省...
</script>




怎么样? 小伙伴们,复杂吗?


那此时小伙伴就问了,那为什么上面那个流程图那么复杂呀?


这里咱就要解开你的疑惑了,上面流程图表述得更多的是教你修改状态。而不是读取状态!


咱现在要修改Vuex的里面state的flag,小伙伴们怎么改呀?


<template>
<div>{{$store.state.flag}}</div>
<button @click='edit()' >修改</button>
</template>

<script>
export default {
methods:{
edit(){
this.$store.state.flag = '修改全局变量'
},
}
}
</script>


上面这样做能改掉吗? 很明确的告诉你们,能改,但是不建议也不允许这样改!


为什么呢? 没有为什么(可参考这篇博客)!肯定是按照上面流程图的方式走呀。不然要这个流程图干嘛呀?对吧!那按照流程图的意思是,你要改状态,必须在mutations上面定义方法去改! 该怎么改呢?这里咱就要修改一下store文件->index.js


import Vue from 'vue' //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //在vue里面注入

export default new Vuex.Store({
state: { //存放状态
flag:'我是一个全局变量'
},
mutations:{ //新增mutations配置项,里面写修改状态的方法
Edit(state){
state.flag = '修改全局变量'
}
}
})

完成store文件->index.js文件修改后,咱组件里面使用如下:


<template>
<div>{{$store.state.flag}}</div>
<button @click='edit()' >修改</button>
</template>

<script>
export default {
methods:{
edit(){
this.$store.commit('Edit') //直接调用vuex下面mutations的方法名
},
}
}
</script>


怎么样?伙伴们?简单吗? 咱再回顾一下上面的流程图


微信截图_20230521213746.png


咱上面的案例是不是就完全按照流程图的方式修改的状态? 细心的小伙伴很快就发现了,很显然不是呀!
没经过黄色的Actions 这个玩意啊?


这里要补充说明一下,我之前也被误导了很久。这也是这张图没表明的地方!跟伙伴们这么说吧,黄色的可以省略!什么意思呢? 就是你可以直接从绿色(组件)->红色(mutations)->蓝色(state状态)


就像我们上面的案例:直接在Vue Components组件视图里面,通过 $store.commit 调用 Vuex里面的方法,vuex里面的方法再修改Vuex的数据,总之一句话,Vuex的数据修改,只能Vuex自己来,外人别参与!


那下面咱为了巩固一下这个思想,再次演示一下



//vuex 文件
import Vue from 'vue' //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //在vue里面注入

export default new Vuex.Store({
state: { //存放状态
flag:'我是一个全局变量'
},
mutations:{ //新增mutations配置项,里面写修改状态的方法
Edit(state,data){ //data是组件传过来的参数
state.flag = data
}
}
})

//vue组件
<template>
<div>{{$store.state.flag}}</div>
<button @click='edit("red")' >红色</button>
<button @click='edit("yellow")' >黄色</button>
<button @click='edit("green")' >绿色</button>
</template>
<script>
export default {
methods:{
edit(color){
this.$store.commit('Edit',color) //commit 有2个参数,第一个是方法名称,第二个参数
},
}
}
</script>



再次通过案例,说明了想要修改Vuex状态。就要遵守Vuex修改流程,把你要修改的值动态传入,就像普通的函数传参一样!只不过,你是通过 $store.commit 调用的函数方法。


好了,讲到这里。其实vuex核心就已经讲完了,因为你们已经知道了,Vuex怎么获取数据,也知道了Vuex怎么修改数据!


伙伴们又要说了,你开始讲了有5个属性!现在才讲2个,一个state,一个mutations? 就这么糊弄?


别急呀,伙伴们。咱也得喝口水嘛! 其实接下来剩下的3个就属于辅助性作用的存在了


什么叫辅助性呢? 因为刚才我们说了,核心已经讲完,你们知道怎么获取,也知道怎么修改。一个状态知道怎么获取,怎么修改。那就是已经完了呀! 剩下的只是说,让你怎么更好的去获取状态,更好的去管理状态,以及特殊情况下怎么修改状态了


接下来咱先讲特殊情况下怎么修改状态。什么叫特殊情况?异步就叫特殊情况。


咱们刚才的一系列操作,是不是都属于同步操作呀? 那么咱想象一个场景,咱要修改的这个状态一开始是不知道的,什么情况下知道呢?必须调用后端接口,后端返回给你,你才知道! 此时聪明的小伙伴就说了:这还不简单吗? 我直接在函数里面发请求呗!如下



//vuex 文件
import request form '../utils/request.js'
import Vue from 'vue' //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //在vue里面注入

export default new Vuex.Store({
state: { //存放状态
flag:'我是一个全局变量'
},
mutations:{ //新增mutations配置项,里面写修改状态的方法
Edit(state){
request('/xxx').then(res=>{ //异步请求
state.flag = res.data
})
}
}
})


这样做虽然可以改。但是小伙伴们咱又回到了最初的话题,这样改合规吗?符合流程图流程么?


是不是显然不符合呀,咱刚才讲同步代码修改状态的时候,是不是也特意把Actions提了一嘴?


所以此时Actions作用就在此发挥出来了,废话少说,Actions就是用来定义异步函数的,每当我们要发起请求获取状态,就必须写在这个函数里面,如下:



//vuex 文件
import request form '../utils/request.js'
import Vue from 'vue' //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //在vue里面注入

export default new Vuex.Store({
state: { //存放状态
flag:'我是一个全局变量'
},
mutations:{ //新增mutations配置项,里面写修改状态的方法
Edit(state){
state.flag = state
}
},
actions:{ //新增actions配置项
GetData({commit}){ //定义获取异步数据的方法
request('/xxx').then(res=>{ //异步请求
commit('Edit',res.data)
})
}
}
})


//vue组件
<template>
<div>{{$store.state.flag}}</div>
<button @click='setData()' >调用异步</button>
</template>
<script>
export default {
methods:{
setData(color){
this.$store.dispatch('GetData') //通过dispatch调用vuex里面的actions下面定义的方法名
},
}
}
</script>



这样一来,是不是就跟流程图彻底对应上了?


微信截图_20230521213746.png


Vue Components组件视图里面,通过 $store.dispatch 调用 Vuex里面的actions下定义的异步方法,然后通过vuex里面的异步 $store.commit 再调用vuex里面的同步方法,最终由同步方法修改状态。


最后总结出:只要涉及到修改状态,必须调用同步里面方法。不管是在组件使用 $store.commit, 还是在 actions 里面使用commit ,都必须调用mutations里面定义的方法 !


通过以上的实例,我相信大家已经明白了整个Vuex修改状态的过程。也对官网的流程图有了更清晰的认知了。


接下来就只剩下2个最简单的属性了,getters与modules


getters类似于vue组件中的computed,进行缓存,对于Store中的数据进行加工处理形成新的数据!直接看实例:



//vuex 文件
import request form '../utils/request.js'
import Vue from 'vue' //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //在vue里面注入

export default new Vuex.Store({
state: { //存放状态
flag:'我是一个全局变量!',
name:'伙伴们'
},
getters:{ //新增mutations配置项,里面写修改状态的方法
flagName(state){
return state.name + state.flag
}
},

})


//vue组件
<template>
<div>{{$store.state.flagName}}</div>
</template>
<script>
export default {

}
</script>



最后咱们来讲modulesmodules针对比较大型的项目时才能发挥优势,不然你项目太小,整个维护的状态都不超过10,那就没必要用modules了!


modules其实就是把Vuex里面的所有功能进行一个更细化的拆分:



//vuex 文件
import request form '../utils/request.js'
import Vue from 'vue' //引入vue
import Vuex from 'vuex' //引入vuex

Vue.use(Vuex) //在vue里面注入

const moduleA = { //组件A需要单独维护的状态
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}

const moduleB = { //组件B需要单独维护的状态
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}

const store = new Vuex.Store({
state:{}, //全局状态
modules: { //整体合并
moduleA: moduleA,
moduleB: moduleB
}
})



//vue组件A
<template>
<div>{{$store.state[moduleA].flag}}</div> //调用Vuex模块里面的状态时,要加moduleA模块名
</template>
<script>
export default {

}
</script>


//vue组件B
<template>
<div>{{$store.state[moduleB].flag}}</div> //调用Vuex模块里面的状态时,要加moduleB模块名
</template>

<script>
export default {

}
</script>



怎么样?小伙伴们。。是不是也挺简单的。


文章到了这里也就把Vuex基本知识都讲完了。小伙伴们需要自己多加练习与消化! 把文章中所讲的Vuex更新状态的流程在脑海里面多回想几遍。有任何疑问,评论区留言吧!


作者:阳火锅
来源:juejin.cn/post/7235603140262084665
收起阅读 »

能把队友气死的8种屎山代码(React版)

web
前几天在前端技术群里聊起Code Review的事,大伙儿似乎都憋了一肚子气: 我觉得这份难言之隐应该要让更多人看到,就跟Henry约了个稿: 于是Henry赶在周末,一边带娃,一边给我抹眼泪整理(脱敏)出了这篇小小的屎山合集,供大家品鉴。 以下是正文。...
继续阅读 »

前几天在前端技术群里聊起Code Review的事,大伙儿似乎都憋了一肚子气:


图片


图片


我觉得这份难言之隐应该要让更多人看到,就跟Henry约了个稿:


图片


于是Henry赶在周末,一边带娃,一边给我抹眼泪整理(脱敏)出了这篇小小的屎山合集,供大家品鉴。


以下是正文。


(文字大部分是Henry所写,沐洒进行了一些精简和调整)




1. 直接操作DOM


const a = document.querySelector('.a');

const scrollListener = throttle(() => {
  const currentY = window.scrollY;

  if (currentY > 100) {
    a.classList.add('show');
  } else {
    a.classList.remove('show');
  }
}, 300);

window.addEventListener('scroll', scrollListener);
return () => {
  window.removeEventListener('scroll', scrollListener);
};

上面的代码在监听scroll方法的回调函数中,直接上手修改DOM的类名。众所周知,React属于响应式编程,大部份情况都不需要直接操作DOM,具体原因参考官方文档(react.dev/learn/manip…


优化方法也很简单,充分发挥响应式编程的优点,用变量代替即可:


const [refreshStatus, setRefreshStatus] = useState('');

const scrollListener = throttle(() => {
  if (tab.getBoundingClientRect().top < topH) {
    setRefreshStatus('show');
  } else {
    setRefreshStatus('');
  }
}, 300);

return <div className={['page_refresh', refreshStatus].join(' ')}/>;

2. useEffect不指定依赖


依赖参数缺失。


useEffect(() => {
    console.log('no deps=====')
    // code...
});

这样的话,每次页面有重渲染,该useEffect都会执行,带来严重的性能问题。例如我们项目中,这个useEffect内部执行的是第一点中的内容,即每次都会绑定一个scroll事件的回调,而且页面中有实时轮询接口每隔5s刷新一次列表,用户在该页面稍加停留,就会有卡顿问题出现。解决方案很简单,根据useEffect的回调函数内容可知,如果需要在第一次渲染之后挂载一个scroll的回调函数,那么就给useEffect第二个参数传入空数组即可,参考官方文档(react.dev/reference/r…


useEffect(() => {
    // code...
}, []);

3. 硬编码


硬编码,即一些数据信息或配置信息直接写死在逻辑代码中,例如


图片


这两行代码本意是从url上拿到指定的参数的值,如果没有,会用一个固定的配置做兜底。


乍一看代码逻辑很清晰,但再想深一层,兜底值具体的含义是什么?为什么要用这两个值来兜底?写这行代码的同学可能很快可以解答,但是一段时间之后,写代码的人和提需求的人都找不到了呢?


这个示例代码还比较简单,拿对应的值去后台可以找到对应的含义,如果是写死的是枚举值,而且还没有类型定义,那代码就很难维护了。


图片


解决此类问题,要么将这些内容配置化,即写到一个config文件中,使用清晰的语义化命名变量;要么,至少在硬编码的地方写上注释,交代清楚这里需要硬编码的前因后果。



沐洒


关于硬编码问题,我在之前的一篇关于“配置管理”的文章里有详细阐述和应对方案,感兴趣的朋友可以看看《小白也能做出满分前端工程:01 配置管理



4. 放任文件长度,只着眼于当下的需求


很多同学做需求、写代码都比较少从全局考虑,只关注到当前需求如何完成。从“战术”上来说没有问题,快速完成产品的需求、快速迭代产品也是大家希望看到的。


可一旦只关注“战术实现”而忽略“战略设计”,除非做的产品是月抛型的,否则一定会遇到旧逻辑难以修改的情况。


如果再加上一个文件被多达10余人修改过的情况,那么每改一行代码都会是一场灾难,例如最近接手的一个页面:


图片


单文件高达1600多行!哪怕去除300多行的注释,和300多行的模板,剩下的逻辑代码也有1000行左右,这种代码可读性就极其糟糕,必须进行拆分。


而很常见的是,由于每一任经手人都疏于考虑全局,导致大量代码毫无模块化可言,甚至出现多个useEffect的依赖是完全相同的:


图片


这里明显还有另一个问题:滥用hooks。


从行号可以看出来确实是相同的依赖写了多个useEffect,很明显是多个同学各写各的的需求引入的这些hooks。

这代码跑肯定是能跑的,但是很可能会出现多个hooks中修改同一个变量,导致其他地方在使用的时候需要搞一些很tricky的操作来修Bug。


5.变量无初始值


在typescript的加持下,对变量的类型定义可以说是日益严格了。可是在一些变量的类型定义比较复杂的情况下,可能一个变量的字段很多、层级很复杂,此时有些同学就可能想偷个懒了,例如:


const [variable, setVariable] = useState();

// some code...
const queryData = function({
    // some logic
    setVariable({ showtrue });
};

useEffect(() => {
    queryData();
}, []);

return variable.show ?  : null;

这里的问题很明显,如果queryData耗时比较长,在第一次渲染的时候,最后一行的variable.show就会报错了,因为variable的初始值是undefined。所以声明变量时,一定要根据变量的类型设置好有效默认值。


6. 三元选择符嵌套使用


网上很多人会推荐说用三元选择符代替简单的if-else,但几乎没有见过有人提到嵌套使用三元选择符的事情,如果看到如下代码,不知道各位读者会作何感想?


{condition1 === 1
    ? "数据加载中"
    : condition2
    ? "没有更多了"
    : condition3
    ? "当前没有可用房间"
    : "数据加载中"}

真的很难理解,明明只是一个简单的提示语句的判断,却需要拿出分析性能的精力去理解,多少有点得不偿失了。


这还只是一种比较简单的三元选择符的嵌套,因为当各个条件分支都为true时,就直接返回了,没有做更多的判断,如果再多做一层,都会直接把人的cpu的干爆炸了。 


替代方案: 



  1. 直接用if-else,可读性更高,以后如果要加逻辑也很方便。

  2. Early Return,也叫卫语句,这种写法能有效简化逻辑,增加可读性。


if (condition1 === 1return "数据加载中";
if (condition2) return "没有更多了";
if (condition3) return "当前没有可用房间";
return "数据加载中";

虽然不嵌套的三元选择符很简单,但是在例如jsx的模版中,仍然不建议大量使用三元选择符,因为可能会出现如下代码:


return (
    condition1 ? (
        <div className={condition2 ? cls1 : cls2}>
            {condition3 ? "111" : "222"}
            {condition4 ? (
                a : b} />
            ) : null
        

    ) : (
        
            {condition6 ? children1 : children2}
        

    )
)

类似的代码在我们的项目中频繁出现,模版中大量的三元选择符导致文件内容拉得很长,很容易看着看着就不记得自己在哪个逻辑分支上了。


像这种简单的三元选择符,做成一个简单的memo变量,哪怕是在组件内直接写变量定义(例如:const clsName = condition2 ? cls1 : cls2),最终到模板的可读性也会比上述代码高。


7. 逻辑不拆分


React hooks可以很方便地帮助开发者聚合逻辑抽离成自定义hooks,千万不要把一个页面所有的useState、useEffect等全都放在一个文件中:


图片


其实从功能上可以对页面进行拆分,拆分之后这些变量的定义也就可以拆出去了。其中有一个很简单的原则就是,如果一个逻辑同时涉及到了useState和useEffect,那么就可以一并抽离出去成为一个自定义hooks。例如接口请求大家一般都是直接在业务逻辑中做:


const Comp = () => {
    const [data, setData] = useState({});
    const [loading, setLoading] = useState(false);
    
    useEffect(() => {
        setLoading(true);
        queryData()
            .then((response) => {
                setData(response);
            })
            .catch((error) => {
                console.error(error);
            })
            .finally(() => {
                setLoading(false);
            });
    });
    
    if (loading) return "loading...";
    
    return <div>{data.text}div>;
}

根据上面的原则,和数据拉取相关的内容涉及到了useState和useEffect,这整块逻辑就可以拆出去,那么最终就只剩下:


const Comp = () => {
    const { data, loading } = useQueryData();
    
    if (loading) return "loading...";
    
    return 
{data.text}
;
};

这样下来,Comp组件就变得身份清爽了。大家可以参考阿里的ahooks库,里面收集了很多前端常用的hooks,可以极大提升开发效率和减少重复代码。


8. 随意读取window对象的值


作为大型项目,很容易需要依赖别的模板挂载到window对象的内容,读取的时候需要考虑到是否有可能拿不到window对象上的内容,从而导致js报错?例如:


window.tmeXXX.a.func();

如果这个tmeXXX所在的js加载失败了,或者是某个版本中没有a这个属性或者func这个函数,那么页面就会白屏。


好啦,最近CR常出现的8种屎山代码都讲完了,你写过哪几种?你们团队的代码中又有哪些让你一口老血喷出来的不良代码呢?欢迎评论区告诉我。


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

原来JS可以这么实现继承

web
当我们在编写代码的时候,有一些对象内部会有一些方法(函数),如果将这些函数在构造函数内部声明会导致内存的浪费,因为实例化构造函数得到不同的实例对象,其内部都有同一个方法,但是占据了不同的内存,就存在内存浪费问题。于是乎我们就需要用到继承。 什么是继承? 通过某...
继续阅读 »

当我们在编写代码的时候,有一些对象内部会有一些方法(函数),如果将这些函数在构造函数内部声明会导致内存的浪费,因为实例化构造函数得到不同的实例对象,其内部都有同一个方法,但是占据了不同的内存,就存在内存浪费问题。于是乎我们就需要用到继承。


什么是继承?


通过某种方式让一个对象可以访问到另一个对象中属性和方法,我们将这种方法称之为继承(inheritance)


如果一个类B继承自另一个类A,就把B称之为A的子类,A称之为B的父类或者超类


如何实现继承?


1、原型链继承


// 原型链的继承
SuperType.prototype.getSuperValue = function () {
return this.property;
}
function SuperType() {
this.property = true
}

Type.prototype = new SuperType();

function Type() {
this.typeproperty = false
}

console.log(Type.prototype);

var instance = new Type()

console.log(instance.getSuperValue()); // true

让SuperType的实例对象赋给Type的原型,Type就能继承到SuperType的属性和方法


image.png


优点:原型链继承容易上手,书写简单,父类可以复用,被多个子类继承。


缺点:会在子类实例对象上共享父类所有的引用类型实例属性,(子类改不动父类的原始类型),更改一个子类的引用属性,其他子类均受影响;子类不能改父类传参。


2、经典继承(伪造对象)


// 经典继承
SuperType.prototype.name = '寒月十九'

function SuperType(age) {
this.color = ['red', 'green', 'blue'],
this.age = age
}

function Type(age) {
SuperType.call(this,age)
}

var instance = new Type(18)
console.log(instance);
console.log(instance.color);

经典继承就是借助this的显示绑定,将SuperType的指向绑定到Type身上,使得我们可以直接访问到SuperType身上的属性。


image.png


优点:解决了原型链继承子类不能向父类传参的问题和原型共享的问题。


缺点:方法不能复用;子类继承不到父类原型上的属性,只能继承到父类实例对象上的属性。


3、组合继承(原型链继承 + 经典继承)


// 组合继承 (伪经典继承)
SuperType.prototype.sayName =function() {
console.log(this.name);
}

function SuperType(name) {
this.name = name,
this.color = ['red', 'green', 'blue']
}
function Type(age,name) {
this.age = age
SuperType.call(this,name)
}

Type.prototype = new SuperType()
Type.prototype.constructor = Type

//Type.prototype被替换了,所以要补充一个constructor属性,指向自身,这样new Type得到的实例对象就有constructor属性

Type.prototype.sayAge = function() {
console.log(this.age)
}

var instance = new Type(20,'寒月十九');
instance.sayAge();

组合继承就是将上面两种继承方式结合起来,先将SuperType的this指向显示绑定到Type,然后再替换Type的原型,再添加上构造器属性指向自身,使得new Type()得到的实例对象就具备构造属性,并且可以继承到SuperType的属性。


image.png


优点:解决了原型链继承和经典继承的缺点造成的影响。


缺点:每一次都会调用两次父类的构造函数,一次是在创建子类原型上,另一次是在子类构造函数内部。


4、原型式继承


(1)借助构造函数实现对象的继承


// 原型式继承
function object(obj) {
function newFn() {}
newFn.prototype = obj;
return new newFn();
};
var person = {
name: '寒月十九',
age: 20,
like: {
sport: 'coding'
}
};
let newObj = object(person);

image.png


(2)Object.create()


var person = {
name: '寒月十九',
age: 20,
like: {
sport: 'coding'
}
};
let newPerson = Object.create(person,{sex: 'boy'});

image.png


优点:不需要单独构建构造函数。


缺点:属性中的引用地址会在相关对象中共享。


5、寄生式继承


function createPerson(original) {
var clone = Object.create(original)
clone.say = function() { // 增强这个对像
console.log('hello');
}
return clone;
};
var person = {
name: '寒月十九',
age: 20,
like: {
sport: 'coding'
}
};
let newPerson =createPerson(person);


寄生式继承是原型式继承的加强版,它结合原型式继承和工厂模式,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后返回对象。


image.png


优点:上手简单,不用单独创建构造函数。


缺点:寄生式继承给对象添加函数会导致函数难以重用,因此不能做到函数复用而效率降低;引用类型的属性始终都会被继承所共享。


寄生组合式继承


// 寄生组合式继承
SuperType.prototype.sayName = function() {
console.log(this.name);
};
SuperType.prototype.like = {
a: 1,
b: 2
};
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green']
};

function Type(name, age) {
this.age = age;
SuperType.call(this, name);
}

var anotherPrototype = Object.assign(Type.prototype, SuperType.prototype);
// anotherPrototype.constructor = Type

Type.prototype = anotherPrototype; // new SuperType()

寄生组合继承是为降低父类构造函数的开销。通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。


image.png


优点:高效,只调用一个次父构造函数,不会在原型上添加多余的属性,原型链还能保持不变;开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。


缺点:代码较复杂。


class类继承


class Parent{
constructor(name) {
this.name = name;
this.hobbies = ["running", "basketball", "writting"];
}
getHobbies() {
return this.hobbies;
}
static getCurrent() { // 静态方法,只能类本身调用
console.log(this);
}
}

class Child extends Parent {
constructor(name) {
super(name);
}
}

var c1 = new Child('寒月十九');
var c2 = new Child('十九');

image.png


作者:寒月十九
来源:juejin.cn/post/7173708454975471646
收起阅读 »

早起、冥想、阅读、写作、运动

周岭在《认知觉醒》一书中提出了快速改变人生的五件事,即:「早起」、「冥想」、「阅读」、「写作」、「运动」。低调务实优秀中国好青年交流群也正是从这 5 件事入手,帮你养成好习惯。我也试着实践了有将近一年的时间,今谈谈收获与心得。 早起 一日之计在于晨,一年之计在...
继续阅读 »

周岭在《认知觉醒》一书中提出了快速改变人生的五件事,即:「早起」、「冥想」、「阅读」、「写作」、「运动」。低调务实优秀中国好青年交流群也正是从这 5 件事入手,帮你养成好习惯。我也试着实践了有将近一年的时间,今谈谈收获与心得。


早起


一日之计在于晨,一年之计在于春;早起是个老生常谈的话题了,鲁迅先生小时候为了上课不迟到,还把「早」刻在桌上告诫自己。我小时候每天晚上吃完饭,没什么事早早地就睡了,甚至觉得十点睡觉都是一件很可怕的事。如今呢,自从步入互联网时代,十点?不好意思,十点夜生活才刚刚开始。


秉承着先僵化、后优化、再固化的原则,我决定尝试一段时间。起初几天是真的很难受,白天浑浑噩噩的完全提不起精神。不过慢慢的,晚上倒是越来越早的睡了。差不多半个月时间几乎都习惯了 10 点左右睡觉,6 点前起床。正常早上六点起床后,稍微锻炼一会回来坐那下下汗,冲个凉水澡,然后吃个早饭就去工作了。


持续了有半年时间,直观感受就是身体越来越好,精神头越来越棒;但我并不认为这是早起带来的,潜移默化改变了我的是生活规律。毕竟美国人时差和咱们完全反着来,也没见几个英年嗝屁的。现在为止,我想早起也许就真的只是早点起来罢了。


但有一天,我翻看着旧日的朋友圈:星光不问赶路人,豁然开朗。也深刻地认识到了自己的肤浅,早起其实并不只意味着早点起来罢了。想象一下,如果明天要和女神约会?或者新工作的第一天?不用考虑肯定早早的就起来收拾了,因为你开心,快乐,幸福;甚至要迎来人生新阶段了。所以早起真谛可能不仅仅是早点起来,更重要的是进一步寻找人生的意义,创造生命的价值,为我所热爱奋斗终生!


冥想


关于冥想,老实说太高端了,高端到有点不接地气,反正 100 个人有 100 个见解。刚开始还看了各种视频、翻了有关的书、试了各种动作体验冥想、有没有效果我不清楚,不过睡得倒很快。


感受呼吸、扫描身体、提升专注力,但越努力就越凌乱……由于不能形成持续的正反馈,所以我有点消极。去你的冥想,浪费生命。后续冥想也是断断续续的持续了好久,那天想起来就尝试一下,想不起来就算了。


直到有阵子,忘记具体在做什么,总之就是在写代码。从上班来坐那,要不是同事喊我,还真没感觉一个上午都过去了……也是瞬间明白了《十分钟冥想》中:心流。


我把冥想定义为心无杂念、极致专注。但是早期的努力只是停留在表面上而没有透彻地理解。我认为冥想最重要的一点:感知力、尝试学会深入感受身体各个部位,体会情绪在大脑波动,品尝茶水在身体流淌,体会世间万物。

一个小和尚问得道的师父:“您得道之前做什么?”

老和尚说:“砍柴、挑水、做饭。”

“那得道之后呢?”小和尚继续问道。

老和尚回答:“还是砍柴、挑水、做饭。”

小和尚一脸疑惑:“师父,这不是一样吗?”

老和尚说:“当然不一样。得道前,我砍柴时惦记着挑水,挑水时惦记着做饭;得道后,砍柴即砍柴,挑水即挑水,做饭即做饭。”

阅读


生命是有限的,但书籍却是无限的,怎么用有限的生命阅读无限的书籍呢?根据不科学统计,人的一生最多只能阅读 15000 本书籍,那估计是没有一个人可以活着读完。所以我们应该要追求精读细阅和高质量的阅读。


首先要会读书,读好书。《如何阅读一本书》就非常详细的讨论了怎么样阅读一本书,尽管有关读书的方法论确实很好,但我觉得阐述得太过重复啰嗦。其实读书喜欢什么就读什么,不要拘泥于阅读世界名著,人文哲理。但我建议少读都市言情,穿越爽文,其可吸收的营养价值较少。具体想怎么读就怎么读,咬文嚼字、一目十行都无所谓,但是这一种读法仅限于是一本好书的情况下。可是究竟什么是好书呢?追随那些走的快的人,阅读其推荐的书单。


假如面临的是一本新书,那么你可以尝试:



  1. 深入了解书的作者、写作的背景。

  2. 详细阅读书的自序、引言、大纲、目录等重要信息。

  3. 快速翻阅书中的部分章节。如果感觉这本书很有价值,那就接着继续。

  4. 带着疑问追随作者的步伐,选择最适合的方式阅读。

    1. 这本书讲了什么?

    2. 作者细说了什么?

    3. 作者的观点是否正确?

    4. 作者讲的和我有什么关系?



  5. 收获体会,记录笔记。


再分享一种进一步的阅读方法:主题阅读。在某个类目中挑选同方向的若干本书,然后确认自己研究的主题和方向。



  1. 依次阅读每本书。

  2. 理清问题、界定主题。

  3. 与不同作者达成共识。

  4. 分析讨论。


写作



我学生时期其实最厌恶写作了……为什么会是你给我段话,让我来研究一下它怎么想的,然后再为你阐述一下自己的观点。我 TM 怎么知道他想什么,爱想什么想什么。



写作实际上可以和阅读相结合,从而构成完美的闭环。


不知道是不是只有自己写作效率低,感觉自己就像间歇泉,总是时不时的迸发灵感。但有时候喷多了,我还写不下来。所以我一般阅读书籍的时候总是会主动掺杂一些技术类目书籍,这样既有助于提高专业技能,又能留足思考时间。


写作我倒没啥可分享心得的,随心所欲,不必整的很累。但必须重视以下三点:



  1. 务必不要出现错字。

  2. 一定要正确地运用标点符号和合理地分段。

  3. 确保文章整体阅读流畅性。


运动


生命在于运动,如只老乌龟一样冲击活 100 年!


运动锻炼不局限于任何形式,爬楼梯也可以,最重要的是生活态度。千万不要眼高手低,今天运动明天就想超越博尔特,持续保持正反馈,日拱一卒,冲吧骚年!


如果不知道如何下手,可以参考我的 wiki 手册:健身手册




其实吧,哪怕你尝试了「早起」、「冥想」、「阅读」、「写作」、「运动」,也不可能立刻获得收获。过去既然无法改变,未来更不知道何去。


那么请尝试着慢一点,慢一点,再慢一点,也许当你回头那刻,轻舟已过万重山。


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

提高你工作效率的10条建议

最近看到一个关于工作效率的问题,这里系统整理下自己总结的一些经验。 有一个跟工作效率有点像的词汇:生产效率。 生产效率指的是单位时间内的有效产出,想要生产效率高,要么做事的“质”和“量”更高,要么缩短所花费的时间。 工作效率和生产效率比较类似,很多都可以借鉴。...
继续阅读 »

最近看到一个关于工作效率的问题,这里系统整理下自己总结的一些经验。


有一个跟工作效率有点像的词汇:生产效率。


生产效率指的是单位时间内的有效产出,想要生产效率高,要么做事的“质”和“量”更高,要么缩短所花费的时间。


工作效率和生产效率比较类似,很多都可以借鉴。


有些工作效率高的,三年经验可以顶别人五年工作经验。


找到自己精力最旺盛的时间段


有人喜欢早自习时候睡觉,有人喜欢晨读。每个人的作息规律不同,可以在自己正常运行一段时间后,找到最佳节奏。在这个精力最旺盛的时间段,更容易进入心流,可以集中精力处理优先级比较高的事情。另外,大段时间尽量不要被打断。


掌握通用技能


掌握基础的电脑办公软件技能、沟通能力、时间管理能力、快速学习一项技能的能力等等。能够使用软件等解决日常工作中遇到的问题,提高工作效率。


如何提高解决问题的能力?


掌握工作必备的基础技能


基础知识扎实的话,就可以避免在一些低级错误上花费很多时间。如果基础不好,而工作任务又比较重,就类似于每天都在考试,但是却没有时间学习新知识,这样学习成绩也无法提升。


单位时间内不断给自己施压


一小时干别人两三个小时干的活,同样的任务,第二次、第三次做的时候就有意识地提高效率。



像训练肌肉一样的训练自己的大脑。同等时间内,从明天起,让自己思考学习双倍的工作量。注意我加粗加重的关键词,不许增加时间,一个小时还是那一个小时,时间不变,内容翻番。


一开始一定有些疲劳感,但只要不生病,那说明你的大脑就能够适应。坚持半个月,习惯它。再加倍。再坚持半个月,习惯它。


一直加倍加倍坚持到你感觉要生病了为止,把速度降下来,降低20%。把这个效率维持终身。当训练成为习惯的时候,你会越来越轻松,越来越惬意。全力以赴的思考也是一样的。


任何一个人,只要你肯,你都能这么去训练自己的工作效率。而当你的效率提升到别人的4倍,8倍。你会发现生活很惬意,不是因为压力变小了,而是因为你习惯了。


——记忆承载《韦小宝绝境》



定期复盘


每周大致回顾下自己本周做了什么,有什么需求改进的。可以自己给自己写周报。


充分利用碎片化时间


可以在上班大致想下今天要做的内容,在下班路上回顾下今天都做了些什么,哪些做得好,哪些还有待改进。


多出妙招不如减少失误


尽量少出岔子,可以避免因为失误而带来的对已有时间的占用。


做最重要的事情


领会领导意图,抓住重点。细枝末节可以在大的事情基本上确认无误的时候再做。


不会就搜


总有些问题是自己措手不及的,不会就搜,不行就换一个搜索引擎,或者换一个关键词。


搜商系列文章汇总


适当摸鱼


该休息休息会,劳逸结合。休息时间可以整理下文档,换换思路也行,有时候现在百思不得其解的问题,出去溜达一圈回来就豁然开朗了。


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

写好代码,我的三个 Code

国内很多大学的计算机专业,比较偏重基础和理论的“灌输”(就我当年上学的体验,现在可能会好一些),对于代码能力,虽然也有一些课程实验,但往往不太够用。于是,在进入正式工作前,很多同学就会对自己代码水平不太自信。下面我就根据我自身的写代码经历提供一些建议。 一些经...
继续阅读 »

国内很多大学的计算机专业,比较偏重基础和理论的“灌输”(就我当年上学的体验,现在可能会好一些),对于代码能力,虽然也有一些课程实验,但往往不太够用。于是,在进入正式工作前,很多同学就会对自己代码水平不太自信。下面我就根据我自身的写代码经历提供一些建议。


一些经历


我是 2010 年上的北邮,当时也是很迷糊的就进了计算机专业。自然的,在大学一开始也谈不上什么学习规划。只能是沿用着高中的学习方法,懵懂地跟着老师走——上课就听课,课余就自习做作业。结果便是,学习效率很低,上课听不太懂、题目做不通透。但总归,上完计算机导论后,编程作业都是自己啃出来的,跌跌撞撞的完成之后,慢慢地竟感受到了编程的乐趣。


我们当时大作业最多的几门课,C++ 程序设计、算法和数据结构、操作系统、计算机网络、微机原理等,现在想来,大部分都都跟玩具一样。后来做了国外一些知名大学公开课的实验才知道,要打造好一个实验项目,是非常难的事情:




  1. 首先,得适配学生的水平,准备详尽的实验材料。




  2. 其次,得搭好代码框架,在合适的地方“留白”,给学生“填空”。




  3. 最后,还得构建足够好的自动化测试平台,进行打分。




如果从头开发,这里面涉及到的复杂度、需要花的心思,并不比发一篇顶会论文简单。那作为教授来说,有这些时间,我为什么不去发一篇论文呢?毕竟国内高校都是科研第一、教学老末。


因此,我在本科课内,代码水平也并没有打下太好的基础。在后面在读研和工作中,不断摸索,代码水平才一点点提高。回头来看,对我代码能力提升有比较大影响的可以总结为 “Code”:LeetCodeWriting/Review Code LoopClean Code


LeetCode


在说 LeetCode 前,想先说说工作后,见到的一类神奇的人——打过算法比赛(通称 ACM,其实是 ICPC 和 CCPC)的同学的印象。这类同学的一大突出特点,用最简单直接的语言来形容,就是:出活快。几年的竞赛经历,让他们只要在脑袋中对需求(题目)理解之后,就能在最短的时间内转化为代码。


由于太过懵懂,我自然是没有打过竞赛,等反应过来竞赛的诸般好处时,已经大三下了。当时,校队也不会招这么“大龄”的队员了,就算招,门槛也非常高,也是大学诸多憾事中的一件了。


后来读了研,在找工作前一年时,LeetCode 已经相当流行了,便也和同学组队,互相激励着刷了起来。当时题目还不是特别多,到研二暑假找实习时,大概把前两百多道刷了两遍。一开始,会不断思考题目是什么意思,该用什么算法解,有时半天想不出来,便去看高票答案。很多高票解真的是精妙而简练,这大概也是当时 LeetCode 最吸引人的地方之一。慢慢的对各种类型题目有些感觉之后,就开始练速度和通过率。也就是上文说的,在理解题目后,能够迅速转变为 bug free 的代码。


因此,虽然没有打过比赛,但是通过 LeetCode 的训练,确实也有了类似竞赛的收获。但自然,在深度、广度和速度上都远不及那些“身经百赛”的同学。不过于我已经是受益匪浅:




  1. 对常见数据结构和算法掌握纯熟。比如现在说起六种排序,特点、使用场景、背后原理,可以做到如数家珍;比如说起树的各种递归非递归遍历,脑动模拟递归执行过程,也是信手拈来;再比如链表、队列、图等特点,也能在脑中边模拟,边换成代码。




  2. 学到了很多精巧的代码片段“构件”。比如如何二分、如何迭代、如何处理链表头尾节点、如何设计基本数据结构的接口等等。这些偏“原子”的构件,是我后来工作中写代码的血肉来源。




但只有这些,是远远不够的,一到大项目里,写出的代码就很容易——“有佳句无佳章”。


Writing/Review Code Loop


遇到上述窘境,往往是因为缺少中大型项目的磨练。表现在空间上,不知道如何组织上万行的代码,如何划分功能模块、构建层次体系;体现在时间上,没有经过项目“起高楼、宴宾客、楼塌了”的构建-腐烂-重构循环。


工程中在理解代码和组织代码时有个矛盾:




  1. 可理解性。作为维护人员,我们学习代码时,多喜欢顺着数据流控制流来理解,所谓根据某个头,一路追查到底,是为纵向




  2. 可维护性。但作为架构人员,我们组织代码时,为了容易维护,多是按照围绕模块来组织代码——把关联紧密的代码聚合到一块,是为横向




所以我们在拿到一个大工程时,如果立即地毯式的看代码,肯定会昏昏欲睡、事倍功半。不幸的是,由于多年读书养成的强大习惯,这个毛病,跟了我很多年。正确的打开方式是,要像对待团在一起的多条线一样,找到“线头”,然后慢慢往外揪。在项目中,这些线头是:service 的 main 函数、各种单测入口。


但我们在构建一个大工程时,又得反着来:先搭建一个揉在一起的主流程,然后逐渐迭代。就像盘古开天辟地一样,随着时间而演化,让天慢慢地升高、地慢慢下降,让整体化为地上四极、山川河流、太阳月亮。如是迭代,将一个混沌的流程,慢慢地模块化。比如常用的工具模块(utils)、业务相关基础模块(common)、控制模块(controller、manager)、RPC HTTP 等回调处理模块(processor)等等。


但当然,如果你已经有了构建某种类型系统的经验,则并不需要在构建初期经历这个漫长过程,可以直接按经验分出一些模块。更进一步,你已经形成了自己的一个代码库,比如时钟、网络、多线程、流控等等,可以直接拿来就用。


那剩下的问题就是细节的微调,我们在进行分层时,边界处的功能,是往上升,还是往下沉;某个较完整的结构,是拍平到使用类里,还是单独拎出来;这些形形色色的决策,都没有一个定则,更多的还是根据场景的需求、工期的长短等诸多实际情况,便宜行事。而这种背后的决策,则是在长时间对中大型项目的学习、对别人修改的 Review、自己上手搭架子和修修补补中,一点点形成的直觉。


就像股票市场有周期一样,工程代码也是有其周期。不经历一个股市牛熊周期,我们不敢轻言空多;不经历过一个工程构建-成熟-腐烂的周期,我们也不敢轻言取舍。即,没办法在工程构建初期,预见到其最常用的打开方式,进而面向主要场景设计,牺牲次要场景的便利性。


单元测试的重要性,怎么强调都不为过。一方面,能不能写出的单元测试,意味着你代码的模块边界是否清楚;另一方面,通过设计好的输入和输出,测试能够保证某种“不变性”,之后无论你怎么微调、重构,只要能跑过之前的测试,那就可以放心一半。另一半,就要靠自己和别人不断 Review 、测试集群线上集群不断地迭代了。


所以,这个过程是一个无休止的 loop,不断的磨,尔后不断地提升。


Clean Code


最后说说对代码的品味。小节标题是:Clean Code,是因为我对代码的品味,最初是从 Clean Code: A Handbook of Agile Software Craftsmanship[1] 这本书建立起来的。其第二章对命名——这个工程中“最难”的事情——的阐述,给我印象很深。


举几个例子:




  1. 单一职责。如果你不能清晰的对你的类或者函数命名,说明你的类或者函数干的事情太多了。




  2. 命名代替注释。比如不要直接使用字面值常量,最好给其一个名字;比如最好不要使用匿名函数,也要给其一个能看出含义的名字。




工作中,我们常说,某某对代码有“洁癖”。我也多少有一些,但我并不以为这是洁癖,而是一种对美的欣赏和追求。代码的美体现在哪里呢?我这里稍微抛个砖(当然,我之前也写文章就代码命名问题啰嗦过,感兴趣的可以点这里[2]可以去看看):




  1. 一致性。比如具有相同含义的实体,使用相同的命名;而需要区分的实体,则要通过命名阈、前缀来进行甄别。从而给读者造成最小的心智负担。




  2. 体系性。是指我们在做一组相关接口时,要考虑其体体系性。比如增删改查,比如生产消费,比如预处理、处理、处理后,比如读取写入等等。体系性又包括对称性和逻辑性,让人在拿到一组接口时,就能最小成本地理解其是如何相互联系、又是如何具有区别的。




  3. 没有赘肉。写代码,不要啰嗦,不要啰嗦,不要啰嗦。如果不小心啰嗦了,说明你可能没有想清楚所解决问题的本质。复杂的表象,在不断地剥离杂质后,往往有很简单的关窍。抓住这些关窍,再往其上附着骨肉,同时理清楚一对一、一对多、多堆多等依赖关系,往往能化简为繁。




不同概念(对应代码中的类)间的关系,在理解代码组织的时候至关重要,最好在名字上有所体现,比如一对一,一对多还是多对多。每个概念的内涵,以及多个概念之间的包含、连接关系,是在做模块设计的时候最需要考虑的事情之一。


在审美之外,还要说说建模(在某种程度上和隐喻是相通的)。毕竟,我们在说构建时,本身就是借助的建筑学中的隐喻。软件工程中,类似的隐喻随处可见。


我们大脑在认知新事物时,多建立在基于旧的模型推演上。因此,如果在处理模块时,如果能从经典的模型库中,找到一个相对合适的抽象,往往能够极大降低用户理解门槛。比如经典的生产者消费者模型、树形组织模型、路由器模型、线程调度模型、内存模型等等。此外,也可以使用某种常见意象、隐喻来命名项目,往往也能在易理解性上收获奇效。比如监控系统,可以叫“鹰眼”;比如各种流水线管控,可以叫“富士康”(手动斜眼);再比如更常见一些的数据采集,我们都知道他叫——“爬虫”。


The last Thing


世间的事情往往是多方印证、互相补足的——如果你想写好代码,就不能只是低头写代码,你得去读读历史、学学美术、写写文字、见见河山,建立一套你自己的审美偏好,然后将其理念平移到写代码里来,才能写出符合直觉、具有美感的好代码。


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

用Canvas绘制一个数字键盘

Hello啊老铁们,这篇文章还是阐述自定义View相关的内容,用Canvas轻轻松松搞一个数字键盘,本身没什么难度,这种效果实现的方式也是多种多样,这篇只是其中的一种,要说本篇有什么特别之处,可能就是纯绘制,没有用到其它的任何资源,一个类就搞定了,文中不足之处...
继续阅读 »

Hello啊老铁们,这篇文章还是阐述自定义View相关的内容,用Canvas轻轻松松搞一个数字键盘,本身没什么难度,这种效果实现的方式也是多种多样,这篇只是其中的一种,要说本篇有什么特别之处,可能就是纯绘制,没有用到其它的任何资源,一个类就搞定了,文中不足之处,各位老铁多包含,多指正。


今天的内容大概如下:


1、效果展示


2、快速使用及属性介绍


3、具体代码实现


4、源文件地址及总结


一、效果展示


很常见的数字键盘,背景,颜色,文字大小,点击的事件等等,均已配置好,大家可以看第2项中相关介绍。


静态效果展示:



动态效果展示,录了一个gif,大家可以看下具体的触摸效果。



二、快速使用及属性介绍


鉴于本身就一个类,不值当去打一个远程的Maven,大家用的话可以直接下载,把文件复制到项目里即可,复制到项目中,就可以按照下面的步骤去使用。


引用


1、xml中引用,可以根据需要,设置宽高及相关属性

<KeyboardView
android:id="@+id/key_board_view"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

2、代码直接创建,然后追加到相关视图里即可

val keyboardView=KeyboardView(this)

方法及属性介绍


单个点击的触发监听:

keyboardView.setOnSingleClickListener {
//键盘点击的数字

}

获取最终的字符串点击监听,其实就是把你点击的数字拼接起来,一起输出,特别在密码使用的时候,省的你再自己拼接了,配合number_size属性和setNumberSize方法一起使用,默认是6个长度,可以根据需求,动态设置。

keyboardView.setOnNumClickListener {
//获取最终的点击数字字符串,如:123456,通过number_size属性或setNumberSize方法,设置最长字符
}

其它方法




















































方法参数概述
hintLetter无参隐藏字母
setBackGroundColorint类型的颜色值设置整体背景色
setRectBackGroundColorint类型的颜色值设置数字格子背景色
setTextColorint类型的颜色值设置文字颜色
setTextSizeFloat设置数字大小
setNumberSizeint类型设置按下的数字总长度
setRectHeightFloat设置数字键盘每格高度
setSpacingFloat设置数字键盘每格间隔距离

属性介绍






























































属性类型概述
background_colorcolor背景颜色
rect_background_colorcolor数字格子背景色
down_background_colorcolor手指按下的背景颜色
text_colorcolor文字颜色
text_sizedimension文字大小
letter_text_sizedimension字母的文字大小
rect_heightdimension数字格子高度
rect_spacingdimension格子间距
is_rect_letterboolean是否显示字母
number_sizeinteger按下的数字总长度字符

三、具体代码实现


代码实现上其实也没有什么难的,主要就是用到了自定义View中的onDraw方法,简单的初始化,设置画笔,默认属性就不一一介绍了,直接讲述主要的绘制部分,我的实现思路如下,第一步,绘制方格,按照UI效果图,应该是12个方格,简图如下,需要注意的是,第10个是空的,也就是再绘制的时候,需要进行跳过,最后一个是一个删除的按钮,绘制的时候也需要跳过,直接绘制删除按钮即可。



1、关于方格的绘制


方格的宽度计算很简单,(手机的宽度-方格间距*4)/3即可,绘制方格,直接调用canvas的drawRoundRect方法,单纯的和数字一起绘制,直接遍历12个数即可,记住9的位置跳过,11的位置,绘制删除按钮。

mRectWidth = (width - mSpacing * 4) / 3
mPaint!!.strokeWidth = 10f
for (i in 0..11) {
//设置方格
val rect = RectF()
val iTemp = i / 3
val rectTop = mHeight * iTemp + mSpacing * (iTemp + 1f)
rect.top = rectTop
rect.bottom = rect.top + mHeight
var leftSpacing = (mSpacing * (i % 3f))
leftSpacing += mSpacing
rect.left = mRectWidth!! * (i % 3f) + leftSpacing
rect.right = rect.left + mRectWidth!!
//9的位置是空的,跳过不绘制
if (i == 9) {
continue
}
//11的位置,是删除按钮,直接绘制删除按钮
if (i == 11) {
drawDelete(canvas, rect.right, rect.top)
continue
}
mPaint!!.textSize = mTextSize
mPaint!!.style = Paint.Style.FILL
//按下的索引 和 方格的 索引一致,改变背景颜色
if (mDownPosition == (i + 1)) {
mPaint!!.color = mDownBackGroundColor
} else {
mPaint!!.color = mRectBackGroundColor
}
//绘制方格
canvas!!.drawRoundRect(rect, 10f, 10f, mPaint!!)
}

2、关于数字的绘制


没有字母显示的情况下,数字要绘制到中间的位置,有字母的情况下,数字应该往上偏移,让整体进行居中,通过方格的宽高和自身文字内容的宽高来计算显示的位置。

//绘制数字
mPaint!!.color = mTextColor
var keyWord = "${i + 1}"
//索引等于 10 从新赋值为 0
if (i == 10) {
keyWord = "0"
}
val rectWord = Rect()
mPaint!!.getTextBounds(keyWord, 0, keyWord.length, rectWord)
val wWord = rectWord.width()
val htWord = rectWord.height()
var yWord = rect.bottom - mHeight / 2 + (htWord / 2)
//上移
if (i != 0 && i != 10 && mIsShowLetter) {
yWord -= htWord / 3
}
canvas.drawText(
keyWord,
rect.right - mRectWidth!! / 2 - (wWord / 2),
yWord,
mPaint!!
)

3、关于字母的绘制


因为字母是和数字一起绘制的,所以需要对应的字母则向下偏移,否则不会达到整体居中的效果,具体的绘制如下,和数字的绘制类似,拿到方格的宽高,以及字母的宽高,进行计算横向和纵向位置。

    	//绘制字母
if ((i in 1..8) && mIsShowLetter) {
mPaint!!.textSize = mLetterTextSize
val s = mWordArray[i - 1]
val rectW = Rect()
mPaint!!.getTextBounds(s, 0, s.length, rectW)
val w = rectW.width()
val h = rectW.height()
canvas.drawText(
s,
rect.right - mRectWidth!! / 2 - (w / 2),
rect.bottom - mHeight / 2 + h * 2,
mPaint!!
)
}

4、关于删除按钮的绘制


删除按钮是纯线条的绘制,没有使用图片资源,不过大家可以使用图片资源,因为图片资源还是比较的靠谱。

 /**
* AUTHOR:AbnerMing
* INTRODUCE:绘制删除按键,直接canvas自绘,不使用图片
*/
private fun drawDelete(canvas: Canvas?, right: Float, top: Float) {
val rWidth = 15
val lineWidth = 35
val x = right - mRectWidth!! / 2 - (rWidth + lineWidth) / 4
val y = top + mHeight / 2
val path = Path()
path.moveTo(x - rWidth, y)
path.lineTo(x, y - rWidth)
path.lineTo(x + lineWidth, y - rWidth)
path.lineTo(x + lineWidth, y + rWidth)
path.lineTo(x, y + rWidth)
path.lineTo(x - rWidth, y)
path.close()
mPaint!!.strokeWidth = 2f
mPaint!!.style = Paint.Style.STROKE
mPaint!!.color = mTextColor
canvas!!.drawPath(path, mPaint!!)

//绘制小×号
mPaint!!.style = Paint.Style.FILL
mPaint!!.textSize = 30f
val content = "×"
val rectWord = Rect()
mPaint!!.getTextBounds(content, 0, content.length, rectWord)
val wWord = rectWord.width()
val htWord = rectWord.height()
canvas.drawText(
content,
right - mRectWidth!! / 2 - wWord / 2 + 3,
y + htWord / 3 * 2 + 2,
mPaint!!
)

}

5、按下效果的处理


按下的效果处理,重写onTouchEvent方法,然后在down事件里通过,手指触摸的XY坐标,判断当前触摸的是那个方格,记录下索引,并使用invalidate进行刷新View,在onDraw里进行改变画笔的颜色即可。


根据XY坐标,返回触摸的位置

 /**
* AUTHOR:AbnerMing
* INTRODUCE:返回触摸的位置
*/
private fun getTouch(upX: Float, upY: Float): Int {
var position = -2
for (i in 0..11) {
val iTemp = i / 3
val rectTop = mHeight * iTemp + mSpacing * (iTemp + 1f)
val top = rectTop
val bottom = top + mHeight
var leftSpacing = (mSpacing * (i % 3f))
leftSpacing += 10f
val left = mRectWidth!! * (i % 3f) + leftSpacing
val right = left + mRectWidth!!
if (upX > left && upX < right && upY > top && upY < bottom) {
position = i + 1
//位置11默认为 数字 0
if (position == 11) {
position = 0
}
//位置12 数字为 -1 意为删除
if (position == 12) {
position = -1
}
}
}
return position
}

在onDraw里进行改变画笔的颜色。

 //按下的索引 和 方格的 索引一致,改变背景颜色
if (mDownPosition == (i + 1)) {
mPaint!!.color = mDownBackGroundColor
} else {
mPaint!!.color = mRectBackGroundColor
}

6、wrap_content处理


在使用当前控件的时候,需要处理wrap_content的属性,否则效果就会和match_parent一样了,具体的处理如下,重写onMeasure方法,获取高度的模式后进行单独的设置。

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
if (heightSpecMode == MeasureSpec.AT_MOST) {
//当高度为 wrap_content 时 设置一个合适的高度
setMeasuredDimension(widthSpecSize, (mHeight * 4 + mSpacing * 5 + 10).toInt())
}
}

四、源文件地址及总结


源文件地址:


github.com/AbnerMing88…


源文件不是一个项目,是一个单纯的文件,大家直接复制到项目中使用即可,对于26个英文字母键盘绘制,基本上思路是一致的,大家可以在此基础上进行拓展,本文就先到这里吧,整体略有瑕疵,忘包含。


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

Android 系统启动到App 界面完全展示终于明白(图文版)

之前文章有分析过Activity创建到View的显示过程,属于单应用层面的知识范畴,本篇将结合Android 系统启动部分知识将两者串联分析,以期达到融会贯通的目标。 通过本篇文章,你将了解到: Android 系统启动流程概览 ServiceManage...
继续阅读 »

之前文章有分析过Activity创建到View的显示过程,属于单应用层面的知识范畴,本篇将结合Android 系统启动部分知识将两者串联分析,以期达到融会贯通的目标。

通过本篇文章,你将了解到:




  1. Android 系统启动流程概览

  2. ServiceManager 进程作用

  3. Zygote 进程创建与fork子进程

  4. system_server 进程作用

  5. App 与 system_server 交互

  6. Activity 与 View的展示

  7. 全流程图



1. Android 系统启动流程概览



image.png




  • init 是用户空间的第一个进程,它的父进程是idle进程

  • init 进程通过解析init.rc 文件并fork出相应的进程

  • zygote是第一个Java 虚拟机进程,通过它孵化出system_server 进程

  • system_server 进程启动桌面(Launcher)App



以上为Android 系统上电到桌面启动的简略过程,我们重点关注其中几个进程:



init、servicemanger、zygote、system_server



idle 与 init 关系如下:



image.png


查看依赖关系:



image.png


init.rc 启动servicemanager、zygote 配置如下:



image.png



image.png


2. ServiceManager 进程作用


Android 进程间通信运用最广泛的是Binder机制,而ServiceManager进程与Binder息息相关。
DNS 存储着域名和ip的映射关系,类似的ServiceManager存储着Binder客户端和服务端的映射。



image.png


App1作为Binder Client端,App2 作为Binder Server端,App2 开放一个接口给App1使用(通常称为服务),此时步骤如下:




  1. App2 向ServiceManager注册服务,过程为:App2 获取ServiceManager的Binder引用,通过该Binder引用将App2 的Binder对象(实现了接口)添加到Binder驱动,Binder驱动记录对象与生成handle并返回给ServiceManager,ServiceManager记录关键信息(如服务名,handle)。

  2. App1 向ServcieManager查询服务,过程为: App1 获取ServiceManager的Binder引用,通过该Binder引用发送查询命令给Binder驱动,Binder驱动委托ServiceManager进行查询,ServiceManager根据服务名从自己的缓存链表里查出对应服务,并将该服务的handle写入驱动,进而转为App1的Binder代理。

  3. App1 拿到App2 的Binder代理后,App1 就可以通过Binder与App2进行IPC通信了,此时ServiceManager已经默默退居幕后,深藏功与名。



由上可知,ServiceManager进程扮演着中介的角色。


3. Zygote 进程创建与fork子进程


Zygote 进程的创建


Zygote 进程大名鼎鼎,Android 上所有的Java 进程都由Zygote孵化,Zygote名字本身也即是受精卵,当然文雅点一般称为孵化器。



image.png


Zygote 进程是由init进程fork出来的,进程启动后从入口文件(app_main.cpp)入口函数开始执行:




  1. 构造AppRuntime对象,并创建Java虚拟机、注册一系列的jni函数(Java和Native层关联起来)

  2. 从Native层切换到Java层,执行ZygoteInit.java main()函数

  3. fork system_server进程,预加载进程公共资源(后续fork的子进程可以复用,加快进程执行速度)

  4. 最后开启LocalSocket,并循环监听来自system_server创建子进程的Socket请求。



通过以上步骤,Zygote 启动完成,并等待创建进程的请求。



image.png


初始状态步骤:




  1. Zygote fork system_server 进程并等待Socket请求

  2. system_server 进程启动后会请求打开Launcher(桌面),此时通过Socket发送创建请求给Zygote,Zygote 收到请求后负责fork 出Launcher进程并执行它的入口函数

  3. Launcher 启动后用户就可以看到初始的界面了



用户操作:

桌面显示出来后,此时用户想打开微信,于是点击了桌面上的微信图标,背后的故事如下:




  1. Launcher App 收到点击请求,会执行startActivity,这个命令会通过Binder传递给system_server进程里的AMS(ActivityManagerService)模块

  2. AMS 发现对应的微信进程并没有启动,于是通过Socket发送创建微信进程的请求给Zygote

  3. Zygote 收到Socket请求后,fork 微信进程并执行对应的入口函数,之后就会显示出微信的界面了



用图表示如下:



image.png


由上可知,App进程和system_server 进程之间通信方式为Binder,而system_server和Zygote 通信方式为Socket,App进程并不直接请求Zygote做事情,而是通过system_server进行处理,system_server 记录着当前所有App 进程的状态,由它来统一管理各个App的生命周期。


Zygote 进程fork 子进程



image.png


Zygote 进程在Java层监听Socket请求,收到请求后层层调用最后切换到Native执行系统调用fork()函数,最后根据fork()返回值区分父子进程,并在子进程里执行入口函数。


4. system_server 进程作用


system_server 为所有App提供服务,可以说是系统的核心进程之一,它主要的功能如下:



image.png


可以看出,它创建并启动了许多服务,常见的AMS、PMS、WMS,我们常说系统某某服务返回了啥,往细的说这里的"系统"可以认为是system_server进程。

需要注意的是,这里所说的服务并不是Android四大组件的Service,而是某一类功能。


四大组件的交互也要依靠system_server:



image.png


实际调用流程如下:



image.png


由上图可知,不管是同一进程内的通信亦或是不同进程间的通信,都需要system_server介入。


App 和 system_server 是属于不同的进程,App进程如何找到system_server呢?

还是要借助ServiceManager进程:



image.png


system_server 在启动时候不仅开启了各种服务,同时还将需要暴露的服务注册到ServiceManager里,其它进程想要使用system_server的功能时只需要从SystemManager里查询即可。


5. App 与 system_server 交互


App 想要获取系统的功能,在大部分情况下是绕不过system_server的,接着来看看App如何与system_server进行交互。


前面分析过,App想要获取system_server 服务只需要从ServiceManager里获取即可,调用形式如下:

getSystemService(Context.WINDOW_SERVICE)

那反过来呢?system_server如何主动调用App的服务呢?

既然获取服务的本质是拿到对端的Binder引用,那么也可以反过来,将App的Binder传递给system_server,等到system_server想要调用App时候拿出来用即可,类似回调的功能,如下图:



image.png


再细化一下流程:



image.png




  1. App 进程在启动后执行ActivityThread.java里的main()方法,在该方法里调用system_server的接口,并将自己的Binder引用(mAppThread)传递给system_server

  2. system_server 掌管着Application和四大组件的生命周期,system_server会告诉App进程当前是需要创建Application实例还是调用到Activity某个生命周期阶段(如onCreate/onResume等),此时就是依靠mAppThread回调回来

  3. 此时的App进程作为Binder Server端,它是在子线程收到system_server进程的消息,因此需要通过post到主线程执行

  4. 最终Application/Activity 的生命周期函数将会在主线程执行,这也就是为什么四大组件不能执行耗时任务的原因,因为都会切换到主线程执行四大组件的各种重写方法



6. Activity 与 View的展示


通过上面的分析可知现在的流程已经走到App进程本身,Application、Activity 都已经创建完毕了,什么时候会显示View呢?

先看Activity.onCreate()的调用流程:



image.png


此流程结束,整个ViewTree都构建好了。


接着需要将ViewTree添加到Window里流程如下:



image.png


最后监听屏幕刷新信号,当信号到来之后遍历ViewTree进行Measure、Layout、Draw操作,最终渲染到屏幕上,此时我们的App界面就显示出来了。



image.png


7. 全流程图



image.png


附源码路径:

init.rc配置文件

ServiceManager入口

Zygote native入口

Zygote java入口

system_server入口

App入口


更多Android 源码查看方式请移步:Android-系统源码查看的几种方式


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

Kotlin 协程如何与 Java 进行混编?

问题 在 Java 与 Kotlin 混编项目中大概率是会遇到 Kotlin 线程的使用问题。协程的混编相对于其他特性的使用上会相对麻烦而且比较容易踩坑。我们以获取 token 来举例,比如有一个获取 token 的 suspend 函数:// 常规的 sus...
继续阅读 »

问题


在 Java 与 Kotlin 混编项目中大概率是会遇到 Kotlin 线程的使用问题。协程的混编相对于其他特性的使用上会相对麻烦而且比较容易踩坑。我们以获取 token 来举例,比如有一个获取 token 的 suspend 函数:

// 常规的 suspend 函数,可以供 Kotlin 使用,Java 无法直接使用
suspend fun getTokenSuspend(): String {
// do something too long
return "Token"
}

想要在 Java 中直接调用则会产出如下错误:


image.png


了解 Kotlin 协程机制的同学应该知道 suspend 修饰符在 Kotlin 编译器期会被处理成对应的 Continuation 类,这里不展开讨论。


这个问题也可以使用简单的方式进行解决,那就是使用 runBlocking 进行简单包装一下即可。


使用 runBlocking 解决


一般情况下我们可能会使用以下代码解决上述问题。定义的 Kotlin 协程代码如下:

// 提供给 Java 使用的封装函数,Java 代码可以直接使用
fun getTokenBlocking(): String =runBlocking{// invoke suspend fun
getTokenSuspend()
}

在 Java 层代码的使用方式大致如下:

public void funInJava() {
String token = TokenKt.getTokenBlocking();
}

看上去方案比较简单,但是直接使用 runBlocking 也会存在一些隐患。 runBlocking 会阻塞当前调用者的线程,如果是在主线程进行调用的话,会导致 App 卡顿,严重的会导致 ANR 问题。那有没有比 runBlocking 更合理的解决方案呐?


回答这个问题之前,先梳理下 Java 与 Kotlin 两种语言在处理耗时函数的一般做法。


Java & Kotlin 耗时函数的一般定义


Java



  • 靠语义约束。比如定义的函数名中 sync 修饰,表明他可能是一个耗时的函数,更好的还会添加 @WorkerThread 注解,让 lint 帮助使用者去做一些检查,确保不会在主线程中去调用一些耗时函数导致页面卡顿。

  • 靠语法约束,定义 Callback。将耗时的函数执行放到一个单独的线程中执行,然后将回调的结果通过 Callback 的形式返回。这种方式无论调用者是什么水平,代码质量都不会有问题;


Kotlin



  • 靠语义约束,同 Java

  • 添加 suspend 修饰,靠语法约束。内部耗时函数切到子线程中执行。外部调用者使用同步的方式调用耗时函数却不会阻塞主线程(这也是 Kotlin 协程主要宣传的点)。


在 Java 与 Kotlin 混编的项目中,上述情况的复杂度将会上升。


使用 CompletableFuture 解决


在审视一下 runBlocking 的使用问题,这种做法是将 Kotlin 中的语法约束退化到语义约束层面了,有的可能连语义层面的约束都没有,这种情况只能祈求调用者的使用是正确的 -- 在子线程调用,而不是在主线程调用。那应该如何怎么处理,就是采用回调的方式,让语法能够规避的问题就不要采用语义来处理。

suspend fun getToken(): String {
// do something too long
return "Token"
}

fun getTokenFuture(): CompletableFuture<String> {
returnCoroutineScope(Dispatchers.IO).future{getToken()}
}

注意:future 是 org.jetbrains.kotlinx:kotlinx-coroutines-jdk8 包中提供的工具类,基于 CoroutineScope 定义的扩展函数,使用时需要导入依赖包。


Java 中的使用方式如下:

public void funInJava() {
try {
// 通过 Future get() 显示调用 getTokenFuture 函数
TestKt.getTokenFuture().get();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

可能会问这里看上去和直接在函数内部使用 runBlocking 没有太大的区别,反而使用上会更麻烦些。的确是这样的,这样的目的是把选择权交给调用者,或者说让调用者显示的知道这不是一个简单的函数,从而提高其在使用 API 时的警惕度,也就是之前提到的从语法层面对 API 进行约束。


退一步说,上述的内容是针对的仅仅是“不得不”这么做的场景,但是对于大部分场景都是可以通过合理的设计来避免出现上述情况:



  • 底层定义的 suspend 函数可以在上层的 ViewModel 中的 viewModelScope 中调用解决;

  • 统一对外暴露的 API 是 Java 类的话,新增的 API 提供可以使用 suspend 类型的扩展函数,使用 suspend 类型对外暴露;

  • 如果明确知道调用者是 Java 代码,那么请提供 Callback 的 API 定义;


总结


尽量使用合理的设计来尽量规避 Kotlin 协程与 Java 混用的情况,在 API 的定义上语法约束优先与语义约束,语义约束优于没有任何约束。当然在特殊的情况下也可以使用 CompletableFuture API 来封装协程相关 API。


下面对几种常见场景推荐的一些写法:



  1. 在单元测试中可以直接使用 runBlocking

  2. 耗时函数可以直接定义为 suspend 函数或者使用 Callback 形式返回;

  3. 对于 Java 类中调用协程函数的场景应使用显示的声明告知调用者,严格一点的可以判断线程,对于在主线程调用的可以抛出异常或者记录下来统一处理;

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

通俗易懂 Android 架构组件发展史

前言 谈到 Android 架构,相信谁都能说上两句。从 MVC,MVP,MVVM,再到时下兴起 MVI,架构设计层出不穷。如何为项目选择合适架构,也成常备课题。 由于架构并非空穴来风,每一种设计都有其存在依据。故今天我们一起探寻 “架构演化” 来龙去脉,相信...
继续阅读 »

前言


谈到 Android 架构,相信谁都能说上两句。从 MVC,MVP,MVVM,再到时下兴起 MVI,架构设计层出不穷。如何为项目选择合适架构,也成常备课题。


由于架构并非空穴来风,每一种设计都有其存在依据。故今天我们一起探寻 “架构演化” 来龙去脉,相信阅读后你会豁然开朗。


文章目录一览



  • 前言

  • 原生架构

    • 原始图形化架构

      • 高频痛点 1:Null 安全一致性问题



    • 原始工程架构 MVC

      • 高频痛点 2:成员变量爆炸

      • 高频痛点 3:状态管理一致性问题

      • 高频痛点 4:消息分发一致性问题





  • 它山之石

    • 矫枉过正 MVP

      • 反客为主 Presenter

      • 简明易用 三方库



    • 拨乱反正 MVVM

      • 曲高和寡 DataBinding

      • 未卜先知 mBinding





  • 力挽狂澜

    • 官方牵头 Jetpack

      • 一举多得 ViewModel



    • 半路杀出 Kotlin

      • 喜闻乐见 ViewBinding





  • 百花齐放

    • 最佳实践 Jetpack MVVM

      • 屏蔽回推 UnPeekLiveData

      • 消息分发 Dispatcher

      • 严格模式 DataBinding



    • 另起炉灶 Compose



  • 综上


原生架构


原始图形化架构


完整软件服务,通常包含客户端和服务端。


Linux 服务端,开发者通过命令行操作;Android 客户端,面向普通用户,须提供图形化操作。为此,Android 将图形系统设计为,通过客户端 Canvas 绘制图形,并交由 Surface Flinger 渲染。


但正如《过目难忘 Android GUI 关系梳理》所述,复杂图形绘制离不开排版过程,而开发者良莠不齐,如直接暴露 Canvas,易导致开发者误用和产生不可预期错误,


为此 Android 索性基于 “模板方法模式” 设计 View、Drawable 等排版模板,让 UI 开发者可继承标准化模板,配置出诸如 TextView、ImageView、ShapeDrawable 等自定义模板,供业务开发者用。



这样误用 Canvas 问题看似解决,却引入 “高频痛点 1”:View 实例 Null 安全一致性问题。这是 Java 语言项目硬伤,客户端背景下尤明显。



高频痛点 1:Null 安全一致性问题


例如某页面有横竖两布局,竖布局有 TextViewA,横布局无,那么横屏时,findViewbyId 拿到则是 Null 实例,后续 mTextViewA.setText( ) 如未判空处理,即造成 Null 安全问题,



对此不能一味强调 “手动判空”,毕竟一个页面中,控件成员多达十数个,每个控件实例亦遍布数十方法中。疏忽难避免。



那怎办?此时 2008 年,回顾历史,可总结为:“同志们,7 年暗夜已开始,7 年后会有个框架,驾着七彩祥云来救你”。



原始工程架构 MVC


时间来到 2013,以该年问世 Android Studio 为例,


工程结构主要包含 Java 代码和 res 资源。考虑到布局编写预览需求,Android 开发默认基于 XML 声明 Layout,MVC 形态油然而生,



其中 XML 作 View 角色,供 View-Controller 获取实例和控制,


Activity 作 View-Controller 角色,结合 View 和 Model 控制逻辑,


开发者另外封装 DataManager,POJO 等,作 Model 角色,用于数据请求响应,



显而易见,该架构实际仅两层:控制层和数据层,


Activity 越界承担 “领域层” 业务逻辑职责,也因此滋生如下 3 个高频痛点:


高频痛点 2:成员变量爆炸


成员声明,动辄数十行,令人眼花缭乱。接手老项目开发者,最有体会。



高频痛点 3:状态管理一致性问题


View 状态保存和恢复,使用原生 onInstanceStateSave & Restore 机制,开发者容易因 “记得 restore、遗漏 save” 而产生不可预期错误。



高频痛点 4:消息分发一致性问题


由于 Activity 额外承担 “领域层” 职责,乃至消息收发工作也直接在 Activity 内进行,这使消息来源无法保证时效性、一致性,易 “被迫收到” 不可预期推送,滋生千奇百怪问题。


EventBus 等 “缺乏鉴权结构” 框架,皆为该背景下 “消息分发不一致” 帮凶。



“同志们,5 年水深火热已过去,再过 2 年,曙光降临”


好家伙,这是提前拿到剧本。既然如此,这 2 年时间,不如放开手脚,引入它山之石试试(就逝世)。



它山之石


矫枉过正 MVP


这一版对 “现实状况” 判断有偏差。


MVP 规定 Activity 应充当 View,而 Presenter 独吞 “表现层” 逻辑,通过 “契约接口” 与 View、Model 通信,


这使 Activity 职能被严重剥夺,只剩末端通知 View 状态改变,无法全权自治 “表现逻辑”。



反客为主 Presenter


从 Presenter 角度看,似乎遵循 “依赖倒置原则” 和 “最小知道原则”,但从关系界限层面看,Presenter 属 “空降” 角色,一切都其自作主张、暗箱操作,不仅 “未能实质解决” 原 Activity 面临上述 4 大痛点,反因贪婪夺权引入更多烂事。


这也是为何,开发过 MVP 项目,都知有多别扭。


简明易用 三方库


基于其本质 “依赖倒置原则” 和 “最小知道原则”,更建议将其用于 “局部功能设计”,如 “三方库” 设计,使开发者 无需知道内部逻辑,简单配置即可使用



Github:Linkage-RecyclerView


我们维护的 “饿了么二级联动列表” 库,即是基于该模式设计,感兴趣可自行查阅。



拨乱反正 MVVM


经历漫长黑夜,Android 开发引来曙光。


2015 年 Google I/O 大会,DataBinding 框架面世。


该框架可用于解决 “高频痛点1:View 实例 Null 安全一致性问题”,并跟随 MVVM 模式步入开发者视野。


曲高和寡 DataBinding



MVVM 是种约定,双向绑定是 MVVM 特征,但非 DataBinding 本质,故长久以来,开发者对 DataBinding 存在误解,认为使用 DataBinding 即须双向绑定、且在 XML 中调试。



事实并非如此。


DataBinding 是通过 “可观察数据 ObservableField” 在编译时与 XML 中对应 View 实例绑定,这使上文所述 “竖布局有 TextViewA 而横布局无” 情况下,有 TextViewA 即被绑定,无即无绑定,于是无论何种情况,都不至于 findViewById 拿到 Null 实例从而诱发 Null 安全问题。



也即,DataBinding 仅负责通知末端 View 状态改变,仅用于规避 Null 安全问题,不参与视图逻辑。而反向绑定是 “迁就” 这一结构的派生设计,非核心本质。



碍于篇幅限制,如这么说无体会,可参见《从被误解到 “真香” Jeptack DataBinding》解析,本文不再累述。



未卜先知 mBinding


除了本质难理解,DataBinding 也有硬伤,由于隔着一层 BindingAdapter,难获取 View 体系坐标等 getter 属性,乃至 “属性动画” 等框架难兼容。



有说 MotionLayout 可破此局,于多数场景轻松完成动画。


但它也非省油灯,不同时支持 Drag & Click,难实现我们 示例项目 “展开面板” 场景。



于是,DataBinding 做出 “违背祖宗” 决定 —— 允许开发者在 Java 代码中拿到 mBinding 乃至 View 实例 … 如此上一节提到的 “改用 ObservableField 的绑定来消除 Null 安全问题” 的努力前功尽弃。


—— 鉴于 App 页面并非总是 “横竖布局皆有”,于是开发者索性通过 “强制竖屏” 扼杀 View 实例 Null 安全隐患,而调用 mBinding 实例仅用于规避 findViewById 样板代码。



至于为何说 mBinding 使用即 “未卜先知”,因为群众智慧多年后即被应验。



力挽狂澜


官方牵头 Jetpack


时间回到 2017,这年 Google I/O 引入一系列 AAC(Android Architecture Components)


一举多得 ViewModel


其中 Jetpack ViewModel,通过支持 View 实例状态 “托管” 和 “保存恢复”,


一举解决 “高频痛点2:成员变量爆炸” 和 “高频痛点 3:状态管理一致性问题”,


Activity 成员变量表,一下简洁许多。Save & Restore 样板代码亦烟消云散。



半路杀出 Kotlin


并且这时期,Kotlin 被扶持为官方语言,背景发生剧变。


Kotlin 直接从语言层面支持 Null 安全,于是 DataBinding 在 Kotlin 项目式微。


喜闻乐见 ViewBinding


千呼万唤,ViewBinding 问世 2019。


如布局中 View 实例隐含 Null 安全隐患,则编译时 ViewBinding 中间代码为其生成 @Nullable 注解,使 Kotlin 开发过程中,Android Studio 自动提醒 “强制使用 Null 安全符”,由此确保 Null 安全一致。



ViewBinding 于 Kotlin 项目可平替 DataBinding,开发者喜闻乐见 mBinding 使用。


百花齐放


最佳实践 Jetpack MVVM


自 2017 年 AAC 问世,部分原生 Jetpack 架构组件至今仍存在设计隐患,


基于 “架构组件本质即解决一致性问题” 理解,我们于 2019 陆续将 “隐患组件” 改造和开源。


屏蔽回推 UnPeekLiveData


LiveData 是效仿响应式编程 BehaviorSubject 的设计,由于


1.Jetpack 架构示例通常只包含 “表现层” 和 “数据层” 两层,缺乏在 “领域层” 分发数据的工具,


2.LiveData Observer 的设计缺乏边界感,


容易让开发者误当做 “一次性事件分发组件” 来使用,造成订阅时 "自动回推脏数据";


容易让开发者误将同一控件实例放在多个 Observer 回调中 造成恢复状态时 “数据不一致” 等问题(具体可参见《MVI 的存在意义》 关于 “响应式编程漏洞” 的描述)


3.DataBinding ObservableField 组件的 Observer 能限定为 "与控件一对一绑定",更适合承担表现层 BehaviorSubject 工作,


4.LiveData 具备生命周期安全等优势,


因此决定将 LiveData 往领域层 PublishSubject 方向改造,去除其 “自动推送最后一次状态” 的能力,使其专职生命周期安全的数据分发。



具体可参见 Github:UnPeek-LiveData 使用。



消息分发 Dispatcher


由于 LiveData 存在的初衷并非是专业的 “一次性事件分发组件”,改造过的 UnPeekLiveData 也只适用于 “低频次数据分发(例如每秒推送 1 次)” 场景,


因而若想满足 “高频次事件分发” 需求(例如每秒推送 5 次以上),请改用或参考专职 “领域层” 数据分发的 Github:MVI-Dispatcher 组件,该组件内部通过消息队列设计,确保不漏掉每一次推送。




Dispatcher 的存在解决了 “高频痛点 4:消息分发一致性问题”,


也即通过在领域层设立 “专职业务处理和结果回推” 的 Dispatcher,来将业务处理过程中产生的 Event 或 State,以串流的方式统一从 output 出口回传,


由此表现层页面可根据消息的性质,采取 “一致性执行” 或 “交由 BehaviorSubject 托管状态”。


对此具体可参见《解决 MVI 架构实战痛点》 解析。



严格模式 DataBinding


此外我们明确约定 Java 下 DataBinding 使用原则,确保 100% Null 安全。如违背原则,便 Debug 模式下警告,方便开发者留意。



具体可参见 Github:KunMinX-MVVM 使用。



另起炉灶 Compose


回到文章开头 Canvas,为实现 View 实例 Null 安全,先是 DataBinding 框架,但它作为一框架,并不体系自洽,与 “属性动画” 等框架难兼容。


于是出现声明式 UI,通过函数式编程 “纯函数原子性” 解决 Null 安全一致。且体系自洽,动画无兼容问题,学习成本也低于 View 体系。


后续如性能全面跟上、120Hz 无压力,建议直接上手 Compose 开发。



注:关于声明式 UI 函数式编程本质,及纯函数原子性为何能实现 Null 安全一致,详见《一通百通 “声明式 UI” 扫盲干货》,本文不作累述。



综上


高频痛点1:Null 安全一致性问题


客户端,图形化,需 Canvas,


为避免接触 Canvas 导致不可预期错误,原生架构提供 View、Drawable 排版模板。


为解决 Java 下 View 实例 Null 安全一致性问题,引入 DataBinding。


但 DataBinding 仅是一框架,难体系自洽,


于是兵分两路,Kotlin + ViewBinding 或 Kotlin + Compose 取代 DataBinding。


高频痛点2:成员变量爆炸


高频痛点3:状态管理一致性问题


引入 Jetpack ViewModel,实现状态托管和保存恢复。


高频痛点4:消息分发一致性问题


引入 Dispatcher 承担 PublishSubject,实现统一的消息推送。


最后,天下无完美架构,唯有高频痛点熟稔于心,不断死磕精进,集思广益,迭代特定场景最优解。


相关资料


Canvas,View,Drawable,排版模板:《过目难忘 Android GUI 关系梳理》


DataBinding,Null 安全一致,ViewBinding:《从被误解到 “真香” Jetpack DataBinding》


Dispatcher,消息分发,State,Event:《解决 MVI 架构实战痛点》


架构组件解决一致性问题:《耳目一新 Jetpack MVVM 精讲》


Compose,纯函数原子性,Null 安全一致:《一通百通 “声明式 UI” 扫盲干货》


版权声明



Copyright © 2019-present KunMinX 原创版权所有。



如需 转载本文,或引用、借鉴 本文 “引言、思路、结论、配图” 进行二次创作发行,须注明链接出处,否则我们保留追责权利。


本文封面 Android 机器人是在 Google 原创及共享成果基础上再创作而成,遵照知识共享署名 3.0 许可所述条款付诸应用。


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

Kotlin 协程探索

Kotlin 协程是什么? 本文只是自己经过研究后,对 Kotlin 协程的理解概括,如有偏差,还请斧正。 简要概括: 协程是 Kotlin 提供的一套线程 API 框架,可以很方便的做线程切换。 而且在不用关心线程调度的情况下,能轻松的做并发编程。也可以说...
继续阅读 »

Kotlin 协程是什么?


本文只是自己经过研究后,对 Kotlin 协程的理解概括,如有偏差,还请斧正。


简要概括:



协程是 Kotlin 提供的一套线程 API 框架,可以很方便的做线程切换。 而且在不用关心线程调度的情况下,能轻松的做并发编程。也可以说协程就是一种并发设计模式。



下面是使用传统线程和协程执行任务:

       Thread{
//执行耗时任务
}.start()

val executors = Executors.newCachedThreadPool()
executors.execute {
//执行耗时任务
}

GlobalScope.launch(Dispatchers.IO) {
//执行耗时任务
}

在实际应用开发中,通常是在主线中去启动子线程执行耗时任务,等耗时任务执行完成,再将结果给主线程,然后刷新UI:

       Thread{
//执行耗时任务
runOnMainThread {
//获取耗时任务结果,刷新UI
}
}.start()

val executors = Executors.newCachedThreadPool()
executors.execute {
//执行耗时任务
runOnMainThread {
//获取耗时任务结果,刷新UI
}
}

Observable.unsafeCreate<Unit> {
//执行耗时任务
}.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe {
//获取耗时任务结果,刷新UI
}

GlobalScope.launch(Dispatchers.Main) {
val result = withContext(Dispatchers.IO){
//执行耗时任务
}
//直接拿到耗时任务结果,刷新UI
refreshUI(result)
}

从上面可以看到,使用Java 的 ThreadExecutors 都需要手动去处理线程切换,这样的代码不仅不优雅,而且有一个重要问题,那就是要去处理与生命周期相关的上下文判断,这导致逻辑变复杂,而且容易出错。


RxJava 是一套优雅的异步处理框架,代码逻辑简化,可读性和可维护性都很高,很好的帮我们处理线程切换操作。这在 Java 语言环境开发下,是如虎添翼,但是在 Kotlin 语言环境中开发,如今的协程就比 RxJava 更方便,或者说更有优势。


下面看一个 Kotlin 中使用协程的例子:

        GlobalScope.launch(Dispatchers.Main) {
Log.d("TestCoroutine", "launch start: ${Thread.currentThread()}")
val numbersTo50Sum = withContext(Dispatchers.IO) {
//在子线程中执行 1-50 的自然数和
Log.d("TestCoroutine", "launch:numbersTo50Sum: ${Thread.currentThread()}")
delay(1000)
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo50 = naturalNumbers.takeWhile { it <= 50 }
numbersTo50.sum()
}

val numbers50To100Sum = withContext(Dispatchers.IO) {
//在子线程中执行 51-100 的自然数和
Log.d("TestCoroutine", "launch:numbers50To100Sum: ${Thread.currentThread()}")
delay(1000)
val naturalNumbers = generateSequence(51) { it + 1 }
val numbers50To100 = naturalNumbers.takeWhile { it in 51..100 }
numbers50To100.sum()
}

val result = numbersTo50Sum + numbers50To100Sum
Log.d("TestCoroutine", "launch end:result=$result ${Thread.currentThread()}")
}
Log.d("TestCoroutine", "Hello World!,${Thread.currentThread()}")
控制台输出结果:
2023-01-02 16:05:45.846 10153-10153/com.wangjiang.example D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-02 16:05:48.058 10153-10153/com.wangjiang.example D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-02 16:05:48.059 10153-10322/com.wangjiang.example D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-02 16:05:49.114 10153-10322/com.wangjiang.example D/TestCoroutine: launch:numbers50To100Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-02 16:05:50.376 10153-10153/com.wangjiang.example D/TestCoroutine: launch end:result=5050 Thread[main,5,main]

在上面的代码中:



  • launch 是一个函数,用于创建协程并将其函数主体的执行分派给相应的调度程序。

  • Dispatchers.MAIN 指示此协程应在为 UI 操作预留的主线程上执行。

  • Dispatchers.IO 指示此协程应在为 I/O 操作预留的线程上执行。

  • withContext(Dispatchers.IO) 将协程的执行操作移至一个 I/O 线程。


从控制台输出结果中,可以看出在计算 1-50 和 51-100 的自然数和的时候,线程是从主线程(Thread[main,5,main])切换到了协程的线程(DefaultDispatcher-worker-1,5,main),这里计算 1-50 和 51-100 都是同一个子线程。


在这里有一个重要的现象,代码从逻辑上看起来是同步的,并且启动协程执行任务的时候,没有阻塞主线程继续执行相关操作,而且在协程中的异步任务执行完成之后,又自动切回了主线程。这就是 Kotlin 协程给开发做并发编程带来的好处。这也是有个概念的来源: Kotlin 协程同步非阻塞


同步非阻塞”是真的“同步非阻塞” 吗?下面探究一下其中的猫腻,通过 Android Studio ,查看 .class 文件中的上面一段代码:

      BuildersKt.launch$default((CoroutineScope)GlobalScope.INSTANCE, (CoroutineContext)Dispatchers.getMain(), (CoroutineStart)null, (Function2)(new Function2((Continuation)null) {
int I$0;
int label;

@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var10000;
int numbersTo50Sum;
label17: {
Object var5 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
Function2 var10001;
CoroutineContext var6;
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
Log.d("TestCoroutine", "launch start: " + Thread.currentThread());
var6 = (CoroutineContext)Dispatchers.getIO();
var10001 = (Function2)(new Function2((Continuation)null) {
int label;

@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
Log.d("TestCoroutine", "launch:numbersTo50Sum: " + Thread.currentThread());
this.label = 1;
if (DelayKt.delay(1000L, this) == var4) {
return var4;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}

Sequence naturalNumbers = SequencesKt.generateSequence(Boxing.boxInt(0), (Function1)null.INSTANCE);
Sequence numbersTo50 = SequencesKt.takeWhile(naturalNumbers, (Function1)null.INSTANCE);
return Boxing.boxInt(SequencesKt.sumOfInt(numbersTo50));
}

@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}

public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
});
this.label = 1;
var10000 = BuildersKt.withContext(var6, var10001, this);
if (var10000 == var5) {
return var5;
}
break;
case 1:
ResultKt.throwOnFailure($result);
var10000 = $result;
break;
case 2:
numbersTo50Sum = this.I$0;
ResultKt.throwOnFailure($result);
var10000 = $result;
break label17;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}

numbersTo50Sum = ((Number)var10000).intValue();
var6 = (CoroutineContext)Dispatchers.getIO();
var10001 = (Function2)(new Function2((Continuation)null) {
int label;

@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
Log.d("TestCoroutine", "launch:numbers50To100Sum: " + Thread.currentThread());
this.label = 1;
if (DelayKt.delay(1000L, this) == var4) {
return var4;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}

Sequence naturalNumbers = SequencesKt.generateSequence(Boxing.boxInt(51), (Function1)null.INSTANCE);
Sequence numbers50To100 = SequencesKt.takeWhile(naturalNumbers, (Function1)null.INSTANCE);
return Boxing.boxInt(SequencesKt.sumOfInt(numbers50To100));
}

@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}

public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
});
this.I$0 = numbersTo50Sum;
this.label = 2;
var10000 = BuildersKt.withContext(var6, var10001, this);
if (var10000 == var5) {
return var5;
}
}

int numbers50To100Sum = ((Number)var10000).intValue();
int result = numbersTo50Sum + numbers50To100Sum;
Log.d("TestCoroutine", "launch end:result=" + result + ' ' + Thread.currentThread());
return Unit.INSTANCE;
}

@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}

public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), 2, (Object)null);
Log.d("TestCoroutine", "Hello World!," + Thread.currentThread());

虽然上面 .class 文件中的代码比较复杂,但是从大体逻辑可以看出,Kotlin 协程也是通过回调接口来实现异步操作的,这也解释了 Kotlin 协程只是让代码逻辑是同步非阻塞,但是实际上并没有,只是 Kotlin 编译器为代码做了很多事情,这也是说 Kotlin 协程其实就是一套线程 API 框架的原因。


再看一个上面例子的变种:

        GlobalScope.launch(Dispatchers.Main) {
Log.d("TestCoroutine", "launch start: ${Thread.currentThread()}")
val numbersTo50Sum = async {
withContext(Dispatchers.IO) {
Log.d("TestCoroutine", "launch:numbersTo50Sum: ${Thread.currentThread()}")
delay(2000)
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo50 = naturalNumbers.takeWhile { it <= 50 }
numbersTo50.sum()
}
}

val numbers50To100Sum = async {
withContext(Dispatchers.IO) {
Log.d("TestCoroutine", "launch:numbers50To100Sum: ${Thread.currentThread()}")
delay(500)
val naturalNumbers = generateSequence(51) { it + 1 }
val numbers50To100 = naturalNumbers.takeWhile { it in 51..100 }
numbers50To100.sum()
}
}
// 计算 1-50 和 51-100 的自然数和是两个并发操作
val result = numbersTo50Sum.await() + numbers50To100Sum.await()
Log.d("TestCoroutine", "launch end:result=$result ${Thread.currentThread()}")
}
Log.d("TestCoroutine", "Hello World!,${Thread.currentThread()}")

控制台输出结果:
2023-01-02 16:32:12.637 13303-13303/com.wangjiang.example D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-02 16:32:13.120 13303-13303/com.wangjiang.example D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-02 16:32:14.852 13303-13444/com.wangjiang.example D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-2,5,main]
2023-01-02 16:32:14.853 13303-13443/com.wangjiang.example D/TestCoroutine: launch:numbers50To100Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-02 16:32:17.462 13303-13303/com.wangjiang.example D/TestCoroutine: launch end:result=5050 Thread[main,5,main]

async 创建了一个协程,它让计算 1-50 和 51-100 的自然数和是两个并发操作。上面控制台输出结果可以看到计算 1-50 的自然数和是在线程 Thread[DefaultDispatcher-worker-2,5,main] 中,而计算 51-100 的自然数和是在另一个线程Thread[DefaultDispatcher-worker-1,5,main]中。


从上面的例子,协程在异步操作,也就是线程切换上:主线程启动子线程执行耗时操作,耗时操作执行完成将结果更新到主线程的过程中,代码逻辑简化,可读性高。


suspend 是什么?


suspend 直译就是:挂起


suspend 是 Kotlin 语言中一个 关键字,用于修饰方法,当修饰方法时,表示这个方法只能被 suspend 修饰的方法调用或者在协程中被调用。


下面看一下将上面代码案例拆分成几个 suspend 方法:

    fun getNumbersTo100Sum() {
GlobalScope.launch(Dispatchers.Main) {
Log.d("TestCoroutine", "launch start: ${Thread.currentThread()}")
val result = calcNumbers1To100Sum()
Log.d("TestCoroutine", "launch end:result=$result ${Thread.currentThread()}")
}
Log.d("TestCoroutine", "Hello World!,${Thread.currentThread()}")
}

private suspend fun calcNumbers1To100Sum(): Int {
return calcNumbersTo50Sum() + calcNumbers50To100Sum()
}

private suspend fun calcNumbersTo50Sum(): Int {
return withContext(Dispatchers.IO) {
Log.d("TestCoroutine", "launch:numbersTo50Sum: ${Thread.currentThread()}")
delay(1000)
val naturalNumbers = generateSequence(0) { it + 1 }
val numbersTo50 = naturalNumbers.takeWhile { it <= 50 }
numbersTo50.sum()
}
}

private suspend fun calcNumbers50To100Sum(): Int {
return withContext(Dispatchers.IO) {
Log.d("TestCoroutine", "launch:numbers50To100Sum: ${Thread.currentThread()}")
delay(1000)
val naturalNumbers = generateSequence(51) { it + 1 }
val numbers50To100 = naturalNumbers.takeWhile { it in 51..100 }
numbers50To100.sum()
}
}
控制台输出结果:
2023-01-03 14:47:57.047 11349-11349/com.wangjiang.example D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-03 14:47:59.311 11349-11349/com.wangjiang.example D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-03 14:47:59.312 11349-11537/com.wangjiang.example D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-3,5,main]
2023-01-03 14:48:00.336 11349-11535/com.wangjiang.example D/TestCoroutine: launch:numbers50To100Sum: Thread[DefaultDispatcher-worker-1,5,main]
2023-01-03 14:48:01.339 11349-11349/com.wangjiang.example D/TestCoroutine: launch end:result=5050 Thread[main,5,main]

suspend 关键字标记方法时,其实是告诉 Kotlin 从协程内调用方法。所以这个“挂起”,并不是说方法或函数被挂起,也不是说线程被挂起


假设一个非 suspend 修饰的方法调用 suspend 修饰的方法会怎么样呢?

  private fun calcNumbersTo100Sum(): Int {
return calcNumbersTo50Sum() + calcNumbers50To100Sum()
}

此时,编译器会提示:

Suspend function 'calcNumbersTo50Sum' should be called only from a coroutine or another suspend function
Suspend function 'calcNumbers50To100' should be called only from a coroutine or another suspend function

下面查看 .class 文件中的上面方法 calcNumbers50To100Sum 代码:

   private final Object calcNumbers50To100Sum(Continuation $completion) {
return BuildersKt.withContext((CoroutineContext)Dispatchers.getIO(), (Function2)(new Function2((Continuation)null) {
int label;

@Nullable
public final Object invokeSuspend(@NotNull Object $result) {
Object var4 = IntrinsicsKt.getCOROUTINE_SUSPENDED();
switch(this.label) {
case 0:
ResultKt.throwOnFailure($result);
Log.d("TestCoroutine", "launch:numbers50To100Sum: " + Thread.currentThread());
this.label = 1;
if (DelayKt.delay(1000L, this) == var4) {
return var4;
}
break;
case 1:
ResultKt.throwOnFailure($result);
break;
default:
throw new IllegalStateException("call to 'resume' before 'invoke' with coroutine");
}

Sequence naturalNumbers = SequencesKt.generateSequence(Boxing.boxInt(51), (Function1)null.INSTANCE);
Sequence numbers50To100 = SequencesKt.takeWhile(naturalNumbers, (Function1)null.INSTANCE);
return Boxing.boxInt(SequencesKt.sumOfInt(numbers50To100));
}

@NotNull
public final Continuation create(@Nullable Object value, @NotNull Continuation completion) {
Intrinsics.checkNotNullParameter(completion, "completion");
Function2 var3 = new <anonymous constructor>(completion);
return var3;
}

public final Object invoke(Object var1, Object var2) {
return ((<undefinedtype>)this.create(var1, (Continuation)var2)).invokeSuspend(Unit.INSTANCE);
}
}), $completion);
}

可以看到 private suspend fun calcNumbers50To100Sum() 经过 Kotlin 编译器编译后变成了private final Object calcNumbers50To100Sum(Continuation $completion)suspend 消失了,方法多了一个参数 Continuation $completion,所以 suspend修饰 Kotlin 的方法或函数,编译器会对此方法做特殊处理。


另外,suspend 修饰的方法,也预示着这个方法是耗时方法,告诉方法调用者要使用协程。当执行 suspend 方法,也预示着要切换线程,此时主线程依然可以继续执行,而协程里面的代码可能被挂起了。


下面再稍为修改 calcNumbers50To100Sum 方法:

   private suspend fun calcNumbers50To100Sum(): Int {
Log.d("TestCoroutine", "launch:numbers50To100Sum:start: ${Thread.currentThread()}")
val sum= withContext(Dispatchers.Main) {
Log.d("TestCoroutine", "launch:numbers50To100Sum: ${Thread.currentThread()}")
delay(1000)
val naturalNumbers = generateSequence(51) { it + 1 }
val numbers50To100 = naturalNumbers.takeWhile { it in 51..100 }
numbers50To100.sum()
}
Log.d("TestCoroutine", "launch:numbers50To100Sum:end: ${Thread.currentThread()}")
return sum
}
控制台输出结果:
2023-01-03 15:28:04.349 15131-15131/com.bilibili.studio D/TestCoroutine: Hello World!,Thread[main,5,main]
2023-01-03 15:28:04.803 15131-15131/com.bilibili.studio D/TestCoroutine: launch start: Thread[main,5,main]
2023-01-03 15:28:04.804 15131-15266/com.bilibili.studio D/TestCoroutine: launch:numbersTo50Sum: Thread[DefaultDispatcher-worker-3,5,main]
2023-01-03 15:28:06.695 15131-15131/com.bilibili.studio D/TestCoroutine: launch:numbers50To100Sum:start: Thread[main,5,main]
2023-01-03 15:28:06.696 15131-15131/com.bilibili.studio D/TestCoroutine: launch:numbers50To100Sum: Thread[main,5,main]
2023-01-03 15:28:07.700 15131-15131/com.bilibili.studio D/TestCoroutine: launch:numbers50To100Sum:end: Thread[main,5,main]
2023-01-03 15:28:07.700 15131-15131/com.bilibili.studio D/TestCoroutine: launch end:result=5050 Thread[main,5,main]

主线程不受协程线程的影响。


总结


Kotlin 协程是一套线程 API 框架,在 Kotlin 语言环境下使用它做并发编程比传统 Thread, Executors 和 RxJava 更有优势,代码逻辑上“同步非阻塞“,而且简洁,易阅读和维护。


suspend 是 Kotlin 语言中一个关键字,用于修饰方法,当修饰方法时,该方法只能被 suspend 修饰的方法和协程调用。此时,也预示着该方法是一个耗时方法,告诉调用者需要在协程中使用。


参考文档:



  1. Android 上的 Kotlin 协程

  2. Coroutines guide


下一篇,将研究 Kotlin Flow。


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

如何不依着惯性做事

文章从一个小和尚的故事开始。 在一个古老的寺庙里,住着一位小和尚,每天的任务就是从山下的小溪里挑水到山上的寺庙。小和尚每天日出而作,日落而息,用他的小木桶,一次又一次地从山下挑水到山上,生活在这种重复中过去了好多年。 有一天,小和尚遇到了一个问题,随着寺庙里的...
继续阅读 »

文章从一个小和尚的故事开始。


在一个古老的寺庙里,住着一位小和尚,每天的任务就是从山下的小溪里挑水到山上的寺庙。小和尚每天日出而作,日落而息,用他的小木桶,一次又一次地从山下挑水到山上,生活在这种重复中过去了好多年。


有一天,小和尚遇到了一个问题,随着寺庙里的弟子越来越多,他一个人挑水已经无法满足大家的需求了。他感到非常焦虑,但加快了挑水的速度,每天几乎都精疲力尽。然而,无论他多么努力,总是无法满足大家的需求。


这一天老和尚问小和尚:「你为什么不想想看,有没有更好的方法来解决这个问题呢?」


小和尚一愣,他才意识到,他一直都是在埋头做事,从未想过有其他的方法。


于是,小和尚开始思考,他观察发现,从山上到山下有一条小溪,而这条小溪的水源就是他每天去挑水的地方。他萌生了一个念头,为何不直接将山下的水引到山上来呢?


于是,小和尚开始行动,他用竹子和石头制作了一套简单的引水系统,经过数天的努力,他成功地将水从山下引到了山上的寺庙。从此,他不再需要每天辛苦地挑水,寺庙里的水源也变得更加充足。


这个 AI 写的小故事虽然有点浅,但也告诉我们,在生活和工作中,往往会习惯性地使用过去的方法和思维模式,而忽视了其他可能的解决方案。只有当我们打破惯性,从新的角度去思考问题,才能找到更好的解决方法,实现真正的创新和突破。


这也是我们今天要聊的「如何不依着惯性做事」。


1 定义


咱们先从定义开始。




  • 惯性:惯性原本是物理学中的一个概念,指的是物体在没有受到外力作用时,静止的物体保持静止,运动的物体保持匀速直线运动的状态。在这里,我们将惯性的概念引申到思维和行为上,表示一种习惯性的思考和行动方式,倾向于维持现状,抵制改变。




  • 思维惯性:思维惯性是指个体在思考问题和决策过程中,容易受到过去经验和认知的影响,导致思维和判断受限,难以接受新观念和变革。这种现象使得人们在面对新旧问题和挑战时,容易陷入固定思维模式,缺乏创新和灵活性。




  • 依着惯性做事:依着惯性做事是指在工作和生活中,人们习惯于沿用过去的方式和方法,对新的观念和变革持保守态度,不愿意主动寻求改进和创新。这种行为方式往往会导致效率低下、缺乏竞争力,甚至无法适应不断变化的环境。




2 惯性的好与坏


辩证的看,依着惯性做事有好有坏。在某些情况下,惯性思维和行为可能有利于保持稳定和效率。然而,在其他情况下,过于依赖惯性可能会限制创新和发展。我们需要客观地评价惯性思维对于特定情境的影响,以便在恰当的时机采取适当的行动。


优点如下:




  • 保持稳定:惯性思维和行为有助于维持现状,确保日常工作的稳定进行。在某些情况下,这可能有助于降低风险和不确定性。




  • 提高效率:对于一些已经经过优化的任务和流程,遵循惯例可能会提高工作效率。在这些情况下,尝试新的方法可能会浪费时间和资源。




  • 简化决策:依赖惯性可以简化决策过程,减少思考和计划的时间。这在面临紧迫的截止日期或资源有限的情况下可能有一定优势。




缺点如下:




  • 限制创新:过度依赖惯性会阻碍创新和改进,导致陈旧的观念和方法得以延续。这可能会让我们错过新的机遇和发展潜力。




  • 降低适应能力:在不断变化的市场和技术环境中,过于依赖惯性可能导致我们在应对新挑战时缺乏灵活性和适应能力。




  • 忽视潜在问题:依赖惯性做事可能让我们忽视潜在的问题和风险。在这些情况下,持续改进和调整可能更加重要。




我们需要在不同的情境下权衡惯性思维和行为的利弊。在某些情况下,遵循惯例可能是合理的选择;而在其他情况下,我们需要挑战惯性,寻求创新和改进。关键在于识别何时应该保持现状,何时应该追求变革。


保持惯性大多数人可以做到,今天我们要聊的是如何打破惯性,不依着惯性做事,因为能够用打破惯性,突破常规的人毕竟是少数。


3 如何不依着惯性做事


在个人的认知中,有一个原理和一个架构能在一定程度上打破惯性。他们是「第一性原理」和「四项行动架构」。


3.1 第一性原理


3.1.1 第一性原理简介


第一性原理是指将问题拆解到最基本的事实或原则,然后从这些基本事实出发来重新构建问题的解决方案。在解决问题和制定决策时,从第一性原理出发,有助于深入挖掘问题的本质,避免受到惯性思维的限制。


3.1.2 如何运用第一性原理


运用第一性原理规避惯性思维可以采用以下的 4 个步骤:




  1. 拆解问题:将问题拆解到最基本的事实或原则,剥离掉惯性思维带来的先入为主的观念和偏见。




  2. 重新构建解决方案:基于拆解后的基本事实,从零开始思考问题的解决方案,避免受到过往经验和传统观念的束缚。




  3. 鼓励创新:以第一性原理为指导,积极探索新的解决方案,提高创新能力和应对变革的能力。




  4. 实事求是:第一性原理要求我们在思考问题时,始终以事实为依据,避免陷入惯性思维的主观判断。




3.1.3 应用实例


以输出质量报告为例,如何应用第一性原理呢?


当我们谈论编写质量报告时,通常会按照固定的模板或流程进行,这是惯性思维的体现。然而,当我们想要打破惯性,提升报告的质量和有效性时,我们可以尝试以下策略:




  1. 反思报告的目标:通常,我们按照惯性写报告,可能因为这是例行公事,或者是因为上级要求。但是,如果我们从第一性原理思考,报告的真正目标是什么?是传达信息,是指导行动,还是促进决策?明确目标后,我们可能需要对报告的结构、内容甚至呈现方式进行改变,以更好地达成目标。




  2. 重新审视数据和信息:在收集和呈现数据时,我们往往依赖于固定的方式和工具,如表格、图表等。但是,这样真的能有效地传达信息吗?如果我们打破惯性,尝试新的数据分析和可视化工具,可能会发现更深入、更直观的洞察,从而更好地支持报告的目标。




  3. 采用迭代的方式编写报告:依照惯性,我们可能会一次性完成报告的编写,然后提交。但是,如果我们采取迭代的方式,先编写一个初稿,然后进行反馈、修订,再反馈、修订,这样可能会花费更多的时间,但最终的报告可能会更准确、更有洞见。




  4. 引入跨领域的视角:我们通常会从自己的专业角度编写报告,但是如果我们引入其他领域的视角,比如用户体验、商业模式等,可能会发现一些意想不到的洞见,这也是打破惯性的一种方式。




3.2 四项行动架构


金教授和莫博涅教授提出的「四项行动架构」是一个用于挑战现有商业模式和行业战略逻辑的工具。其最开始出处是蓝海战略,来自《蓝海战略》一书,它通过改变现有的商业模式来区分与竞争对手的模式,从而创造出新的行业。


3.2.1 四项行动架构简介


四项行动架构主要包含以下四个关键问题,通过这四个问题挑战一个行业的战略逻辑和现行的商业模式:




  1. 删除:在我们的产品、服务或流程中,哪些被视为理所当然的元素其实可以删除?




  2. 减少:哪些元素我们可以大幅度削减,使其低于行业标准?




  3. 提升:哪些元素我们可以大幅度提升,使其高于行业标准?




  4. 创新:哪些从未在我们的产品、服务或流程中出现过的元素值得创新引入?




3.2.2 四项行动架构应用实例


在带技术团队过程中,我们也可以应用「四项行动架构」来打破惯性,以挑战技术团队的做事逻辑和现行的工作模式:



  1. 删除:技术团队中哪些看起来理所当然的要素应该被删除?




  • 可以考虑减少过多的会议和报告,将精力集中在实际的技术开发和创新上。




  • 去除过时的技术和方法,避免拖慢团队的发展速度。




  • 削减在某些环节的过度管理,让团队成员有更多自主权和创新空间。




如取消每周的固定例会,改为根据项目进度和团队需求灵活安排讨论和分享会。



  1. 消减:哪些要素应该被大幅削减到行业标准之下?




  • 减少冗余的代码审查和质量控制流程,以提高团队的工作效率。




  • 精简项目管理流程,减少不必要的文档和审批环节。




如将代码审查流程简化为一次轮流审查,而不是多次审查。



  1. 提升:哪些要素应该大幅提升到行业标准之上?




  • 加大对新技术和方法的投入,以求在行业中领先地位。




  • 提高团队成员的技能培训和成长机会,以便团队更好地适应市场变化。




如为团队成员提供更多的技术培训和参加行业大会的机会,以便他们能跟上技术的最新发展。



  1. 创造:哪些行业中从未提供的要素是应该被创造出来的?




  • 开发独特的技术解决方案,为客户创造更大的价值。




  • 创造新的合作模式,跨部门和跨行业合作,以实现技术的广泛应用。




如开发一款能够实时分析用户行为的工具,以便为客户提供更精准的个性化推荐服务。


通过运用这个四项行动架构,技术团队可以挑战现有的做事逻辑和工作模式,实现价值创新。这将有助于提高团队的竞争力,为公司创造更大的价值。


4 应用在技术团队管理


4.1 应用方法:「解脱」


打破惯性的常用方法我称之为「解脱」,从一个惯性中解脱出来。


解脱看到背后的真实,透过真实,把全部的惯性打破,也就自然而然地走出来了。


一般我们会基于意识到问题、解决问题和透过真实来不依着惯性做事,我们可以遵循以下步骤:




  1. 意识到问题:在日常工作中,关注自己的行为和思维模式。观察是否存在惯性思维,例如对新观念的排斥、抵制变革或过于依赖过去的经验。当我们意识到这些问题时,就迈出了第一步。




  2. 深入了解问题:分析惯性思维的来源,可能来自个人经验、团队文化或行业惯例。了解问题背后的原因有助于我们制定更有效的解决方案。




  3. 寻求解决方案:针对发现的问题,积极寻求创新性和实用性的解决方案。这可能包括改变思维模式、学习新技能、尝试新方法或调整工作流程。




  4. 解脱:在实践新方案的过程中,逐步摆脱惯性思维的束缚。这可能需要时间和努力,但随着不断的尝试和改进,我们会越来越不依赖惯性。




  5. 看到背后的真实:透过表面现象,关注背后的实质和深层次需求。这有助于我们更好地理解问题,找到更有效的解决方案。




  6. 持续改进:在摆脱惯性思维的过程中,保持对问题的关注和反思。不断学习和成长,培养开放、创新和挑战的心态。




通过以上六个步骤,我们可以逐渐摆脱惯性思维,实现真正的自我解脱。在这个过程中,我们需要保持耐心和毅力,不断努力提升自己的认知水平和能力,以达到更好的工作成果。


4.2 应用实践


我们以常见的问题解决方式和团队沟通为例看一下如何应用「解脱」


4.2.1 问题解决方式




  1. 意识到问题:观察团队成员在解决问题时是否习惯于采用已知的方法和经验,而忽视了其他可能的解决方案。




  2. 深入了解问题:可能是因为团队文化倾向于避免冒险,或者团队成员没有足够的技能或知识去尝试新的方法。




  3. 寻求解决方案:可以通过培训和学习,提升团队成员的技能和知识,鼓励他们在解决问题时尝试多种可能的解决方案。




  4. 解脱:在实践中,尝试新的方法和技术,逐步摆脱对过去经验的依赖。




  5. 看到背后的真实:意识到问题解决的本质是创新和改进,而不仅仅是应用已知的方法。




  6. 持续改进:在实践中,不断反思和改进,培养开放和创新的心态。




4.2.2 团队沟通




  1. 意识到问题:注意到团队成员是否在沟通中经常遇到障碍,比如信息传递不畅、沟通效率低下等。




  2. 深入了解问题:可能是因为沟通方式过于传统,比如过度依赖会议,而忽视了其他可能的沟通方式。




  3. 寻求解决方案:可以尝试新的沟通方式,比如异步沟通、立即反馈、跨部门沟通等。




  4. 解脱:在实践中,尝试新的沟通方式,逐步摆脱传统的沟通模式。




  5. 看到背后的真实:理解到沟通的本质是传递和理解信息,而不是遵循某种固定的方式。




  6. 持续改进:在实践中,不断反思和改进沟通方式,提高沟通的效率和效果。




这样的思考和实践,可以应用到技术团队管理的所有方面,包括任务分配、技术选型、项目管理等。关键是要有意识地发现和打破惯性,以创新和改进的心态去面对问题和挑战。


5 小结


小结一下,在上面的文章中我们探讨了惯性思维的利弊,尤其强调了依赖惯性思维可能对创新和发展产生限制。提出两种打破惯性思维的方法:「第一性原理」和「四项行动架构」。第一性原理鼓励我们将问题拆解到最基本的事实或原则,然后从这些基本事实出发重新构建解决方案;而四项行动架构则挑战现有商业模式和行业战略逻辑,包括"删除"、"减少"、"提升"和"创新"四个关键问题。最后详细阐述了如何在技术团队管理中应用这些方法,通过一个被称为「解脱」的过程来

作者:潘锦
来源:juejin.cn/post/7234887157000650810
摆脱惯性思维的束缚。

收起阅读 »

电视剧里的代码真能运行吗?

​大家好,欢迎来到 Crossin的编程教室 ! 前几天,后台老有小伙伴留言“爱心代码”。这不是Crossin很早之前发过的内容嘛,怎么最近突然又被人翻出来了?后来才知道 ,原来是一部有关程序员的青春偶像剧《点燃我,温暖你》在热播,而剧中有一段关于期中考试要用...
继续阅读 »

​大家好,欢迎来到 Crossin的编程教室 !


前几天,后台老有小伙伴留言“爱心代码”。这不是Crossin很早之前发过的内容嘛,怎么最近突然又被人翻出来了?后来才知道


,原来是一部有关程序员的青春偶像剧《点燃我,温暖你》在热播,而剧中有一段关于期中考试要用程序画一个爱心的桥段。


于是出于好奇,Crossin就去看了这一集(第5集,不用谢)。这一看不要紧,差点把刚吃的鸡腿给喷出来--槽点实在太多了!


忍不住做了个欢乐吐槽向的代码解读视频,在某平台上被顶到了20个w的浏览,也算蹭了一波人家电视剧的热度吧……


下面是图文版,给大家分析下剧中出现的“爱心”代码,并且来复刻一下最后男主完成的酷炫跳动爱心。


剧中代码赏析


1. 首先是路人同学的代码:



虽然剧中说是“C语言期中考试”,但这位同学的代码名叫 draw2.py,一个典型的 Python 文件,再结合截图中的 pen.forward、pen.setpos 等方法来看,应该是用 turtle 海龟作图库来画爱心。那效果通常是这样的:


import turtle as t
t.color('red')
t.setheading(50)
t.begin_fill()
t.circle(-100, 170)
t.circle(-300, 40)
t.right(38)
t.circle(-300, 40)
t.circle(-100, 170)
t.end_fill()
t.done()



而不是剧中那个命令行下用1组成的不规则的图形。


2. 然后是课代表向路人同学展示的优秀代码:



及所谓的效果:



这确实是C语言代码了,但文件依然是以 .py 为后缀,并且 include 前面没有加上 #,这显然是没法运行的。


里面的内容是可以画出爱心的,用是这个爱心曲线公式:



然后遍历一个15*17的方阵,计算每个坐标是在曲线内还是曲线外,在内部就输出#或*,外部就是-


用python改写一下是这样的:


for y in range(9, -6, -1):
for x in range(-8, 9):
print('*##*'[(x+10)%4] if (x*x+y*y-25)**3 < 25*x*x*y*y*y else '-', end=' ')
print()

​​​​​​效果:



稍微改一下输出,还能做出前面那个全是1的效果:


for y in range(9, -6, -1):
for x in range(-8, 9):
print('1' if (x*x+y*y-25)**3 < 25*x*x*y*y*y else ' ', end=' ')
print()


但跟剧中所谓的效果相去甚远。


3. 最后是主角狂拽酷炫D炸天的跳动爱心:



代码有两个片段:




但这两个片段也不C语言,而是C++,且两段并不是同一个程序,用的方法也完全不一样。


第一段代码跟前面一种思路差不多,只不过没有直接用一条曲线,而是上半部用两个圆形,下半部用两条直线,围出一个爱心。



改写成 Python 代码:


size = 10
for x in range(size):
for y in range(4*size+1):
dist1 = ((x-size)**2 + (y-size)**2) ** 0.5
dist2 = ((x-size)**2 + (y-3*size)**2) ** 0.5
if dist1 < size + 0.5 or dist2 < size + 0.5:
print('V', end=' ')
else:
print(' ', end=' ')
print()

for x in range(1, 2*size):
for y in range(x):
print(' ', end=' ')
for y in range(4*size+1-2*x):
print('V', end=' ')
print()

运行效果:



第二段代码用的是基于极坐标的爱心曲线,是遍历角度来计算点的位置。公式是:



计算出不同角度对应的点坐标,然后把它们连起来,就是一个爱心。


from math import pi, sin, cos
import matplotlib.pyplot as plt
no_pieces = 100
dt = 2*pi/no_pieces
t = 0
vx = []
vy = []
while t <= 2*pi:
vx.append(16*sin(t)**3)
vy.append(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t))
t += dt
plt.plot(vx, vy)
plt.show()

效果:



代码中循环时用到的2π是为了保证曲线长度足够绕一个圈,但其实长一点也无所谓,即使 π=100 也不影响显示效果,只是相当于同一条曲线画了很多遍。所以剧中代码里写下35位小数的π,还被女主用纸笔一字不落地抄写下来,实在是让程序员无法理解的迷惑行为。



但不管写再多位的π,上述两段代码都和最终那个跳动的效果差了五百只羊了个羊。


跳动爱心实现


作为一个总是在写一些没什么乱用的代码的编程博主,Crossin当然也不会放过这个机会,下面就来挑战一下用 Python 实现最终的那个效果。


1. 想要绘制动态的效果,必定要借助一些库的帮助,不然代码量肯定会让你感动得想哭。这里我们将使用之前 羊了个羊游戏 里用过的 pgzero 库。然后结合最后那个极坐标爱心曲线代码,先绘制出曲线上离散的点。


import pgzrun
from math import pi, sin, cos

no_p = 100
dt = 2*3/no_p
t = 0
x = []
y = []
while t <= 2*3:
x.append(16*sin(t)**3)
y.append(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t))
t += dt

def draw():
screen.clear()
for i in range(len(x)):
screen.draw.filled_rect(Rect((x[i]*10+400, -y[i]*10+300), (4, 4)), 'pink')

pgzrun.go()


2. 把点的数量增加,同时沿着原点到每个点的径向加一个随机数,并且这个随机数是按照正态分布来的(半个正态分布),大概率分布在曲线上,向曲线内部递减。这样,就得到这样一个随机分布的爱心效果。


...
no_p = 20000
...
while t <= 2*pi:
l = 10 - abs(random.gauss(10, 2) - 10)
x.append(l*16*sin(t)**3)
y.append(l*(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)))
t += dt
...


3. 下面就是让点动起来,这步是关键,也有一点点复杂。为了方便对于每个点进行控制,这里将每个点自定义成了一个Particle类的实例。


从原理上来说,就是给每个点加一个缩放系数,这个系数是根据时间变化的正弦函数,看起来就会像呼吸的节律一样。


class Particle():
def __init__(self, pos, size, f):
self.pos = pos
self.pos0 = pos
self.size = size
self.f = f

def draw(self):
screen.draw.filled_rect(Rect((10*self.f*self.pos[0] + 400, -10*self.f*self.pos[1] + 300), self.size), 'hot pink')

def update(self, t):
df = 1 + (2 - 1.5) * sin(t * 3) / 8
self.pos = self.pos0[0] * df, self.pos0[1] * df

...

t = 0
def draw():
screen.clear()
for p in particles:
p.draw()

def update(dt):
global t
t += dt
for p in particles:
p.update(t)


4. 剧中爱心跳动时,靠中间的点波动的幅度更大,有一种扩张的效果。所以再根据每个点距离原点的远近,再加上一个系数,离得越近,系数越大。


class Particle():
...
def update(self, t):
df = 1 + (2 - 1.5 * self.f) * sin(t * 3) / 8
self.pos = self.pos0[0] * df, self.pos0[1] * df


5. 最后再用同样的方法画一个更大一点的爱心,这个爱心不需要跳动,只要每一帧随机绘制就可以了。


def draw():
...
t =
0
while t < 2*pi:
f = random.gauss(1.1, 0.1)
x = 16*sin(t)**3
y = 13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)
size = (random.uniform(0.5,2.5), random.uniform(0.5,2.5))
screen.draw.filled_rect(Rect((10*f*x + 400, -10*f*y + 300), size), 'hot pink')
t += dt * 3


合在一起,搞定!



总结一下,就是在原本的基础爱心曲线上加上一个正态分布的随机量、一个随时间变化的正弦函数和一个跟距离成反比的系数,外面再套一层更大的随机爱心,就得到类似剧中的跳动爱心效果。


但话说回来,真有人会在考场上这么干吗?


除非真的是超级大学霸,不然就是食堂伙食太好--


吃太饱撑的……



代码已开源:python666.cn/c/9


如二创发布请注明代码来源:Crossin的编程教室



作者:Crossin先生
来源:juejin.cn/post/7168388057631031332
收起阅读 »

基于人脸识别算法的考勤系统

​ 作为一个基于人脸识别算法的考勤系统的设计与实现教程,以下内容将提供详细的步骤和代码示例。本教程将使用 Python 语言和 OpenCV 库进行实现。 一、环境配置 安装 Python 请确保您已经安装了 Python 3.x。可以在Python 官网...
继续阅读 »


作为一个基于人脸识别算法的考勤系统的设计与实现教程,以下内容将提供详细的步骤和代码示例。本教程将使用 Python 语言和 OpenCV 库进行实现。


一、环境配置



  1. 安装 Python


请确保您已经安装了 Python 3.x。可以在Python 官网下载并安装。



  1. 安装所需库


在命令提示符或终端中运行以下命令来安装所需的库:


pip install opencv-python
pip install opencv-contrib-python
pip install numpy
pip install face-recognition


二、创建数据集



  1. 创建文件夹结构


在项目目录下创建如下文件夹结构:


attendance-system/
├── dataset/
│ ├── person1/
│ ├── person2/
│ └── ...
└── src/


将每个人的照片放入对应的文件夹中,例如:


attendance-system/
├── dataset/
│ ├── person1/
│ │ ├── 01.jpg
│ │ ├── 02.jpg
│ │ └── ...
│ ├── person2/
│ │ ├── 01.jpg
│ │ ├── 02.jpg
│ │ └── ...
│ └── ...
└── src/


三、实现人脸识别算法


src 文件夹下创建一个名为 face_recognition.py 的文件,并添加以下代码:


import os
import cv2
import face_recognition
import numpy as np

def load_images_from_folder(folder):
images = []
for filename in os.listdir(folder):
img = cv2.imread(os.path.join(folder, filename))
if img is not :
images.append(img)
return images

def create_known_face_encodings(root_folder):
known_face_encodings = []
known_face_names = []
for person_name in os.listdir(root_folder):
person_folder = os.path.join(root_folder, person_name)
images = load_images_from_folder(person_folder)
for image in images:
face_encoding = face_recognition.face_encodings(image)[0]
known_face_encodings.append(face_encoding)
known_face_names.append(person_name)
return known_face_encodings, known_face_names

def recognize_faces_in_video(known_face_encodings, known_face_names):
video_capture = cv2.VideoCapture(0)
face_locations = []
face_encodings = []
face_names = []
process_this_frame = True

while True:
ret, frame = video_capture.read()
small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25)
rgb_small_frame = small_frame[:, :, ::-1]

if process_this_frame:
face_locations = face_recognition.face_locations(rgb_small_frame)
face_encodings = face_recognition.face_encodings(rgb_small_frame, face_locations)

face_names = []
for face_encoding in face_encodings:
matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
name = "Unknown"

face_distances = face_recognition.face_distance(known_face_encodings, face_encoding)
best_match_index = np.argmin(face_distances)
if matches[best_match_index]:
name = known_face_names[best_match_index]

face_names.append(name)

process_this_frame = not process_this_frame

for (top, right, bottom, left), name in zip(face_locations, face_names):
top *= 4
right *= 4
bottom *= 4
left *= 4

cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), 2)
cv2.rectangle(frame, (left, bottom - 35), (right, bottom), (0, 0, 255), cv2.FILLED)
font = cv2.FONT_HERSHEY_DUPLEX
cv2.putText(frame, name, (left + 6, bottom - 6), font, 0.8, (255, 255, 255), 1)

cv2.imshow('Video', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break

video_capture.release()
cv2.destroyAllWindows()

if __name__ == "__main__":
dataset_folder = "../dataset/"
known_face_encodings, known_face_names = create_known_face_encodings(dataset_folder)
recognize_faces_in_video(known_face_encodings, known_face_names)


四、实现考勤系统


src 文件夹下创建一个名为 attendance.py 的文件,并添加以下代码:


import os
import datetime
import csv
from face_recognition import create_known_face_encodings, recognize_faces_in_video

def save_attendance(name):
attendance_file = "../attendance/attendance.csv"
now = datetime.datetime.now()
date_string = now.strftime("%Y-%m-%d")
time_string = now.strftime("%H:%M:%S")
if not os.path.exists(attendance_file):
with open(attendance_file, "w", newline="") as csvfile:
csv_writer = csv.writer(csvfile)
csv_writer.writerow(["Name", "Date", "Time"])
with open(attendance_file, "r+", newline="") as csvfile:
csv_reader = csv.reader(csvfile)
rows = [row for row in csv_reader]
for row in rows:
if row[0] == name and row[1] == date_string:
return
csv_writer = csv.writer(csvfile)
csv_writer.writerow([name, date_string, time_string])

def custom_recognize_faces_in_video(known_face_encodings, known_face_names):
video_capture = cv2.VideoCapture(0)
face_locations = []
face_encodings = []
face_names = []
process_this_frame = True
while True:
ret, frame = video_capture.read()
small_frame = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25)
rgb_small_frame = small_frame[:, :, ::-1]

if process_this_frame:
face_locations = face_recognition.face_locations(rgb_small_frame)
face_encodings = face_recognition.face_encodings(rgb_small_frame, face_locations)
face_names = []
for face_encoding in face_encodings:
matches = face_recognition.compare_faces(known_face_encodings, face_encoding)
name = "Unknown"
face_distances = face_recognition.face_distance(known_face_encodings, face_encoding)
best_match_index = np.argmin(face_distances)
if matches[best_match_index]:
name = known_face_names[best_match_index]
save_attendance(name)
face_names.append(name)
process_this_frame = not process_this_frame
for (top, right, bottom, left), name in zip(face_locations, face_names):
top *= 4
right *= 4
bottom *= 4
left *= 4
cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), 2)
cv2.rectangle(frame, (left, bottom - 35), (right, bottom), (0, 0, 255), cv2.FILLED)
font = cv2.FONT_HERSHEY_DUPLEX
cv2.putText(frame, name, (left + 6, bottom - 6), font, 0.8, (255, 255, 255), 1)

cv2.imshow('Video', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break

video_capture.release()
cv2.destroyAllWindows()

if __name__ == "__main__":
dataset_folder = "../dataset/"
known_face_encodings, known_face_names = create_known_face_encodings(dataset_folder)
custom_recognize_faces_in_video(known_face_encodings, known_face_names)


五、运行考勤系统


运行 attendance.py 文件,系统将开始识别并记录考勤信息。考勤记录将保存在 attendance.csv 文件中。


python src/attendance.py


现在,您的基于人脸识别的考勤系统已经实现。请注意,这是一个基本示例,您可能需要根据实际需求对其进行优化和扩展。例如,您可以考虑添加更多的人脸识别算法、考勤规则等

作者:A等天晴
来源:juejin.cn/post/7235458133505867837


收起阅读 »

Android自定义一个省份简称键盘

hello啊各位老铁,这篇文章我们重新回到Android当中的自定义View,其实最近一直在搞Flutter,初步想法是,把Flutter当中的基础组件先封装一遍,然后接着各个工具类,列表,网络,统统由浅入深的搞一遍,弄完Flutter之后,再逐步的更新And...
继续阅读 »

hello啊各位老铁,这篇文章我们重新回到Android当中的自定义View,其实最近一直在搞Flutter,初步想法是,把Flutter当中的基础组件先封装一遍,然后接着各个工具类,列表,网络,统统由浅入深的搞一遍,弄完Flutter之后,再逐步的更新Android当中的技术点,回头一想,还是穿插着来吧,再系统的规划,难免也有变化,想到啥就写啥吧,能够坚持输出就行。


今天的这个知识点,是一个自定义View,一个省份的简称键盘,主要用到的地方,比如车牌输入等地方,相对来说还是比较的简单,我们先看下最终的实现效果:



实现方式呢有很多种,我相信大家也有自己的一套实现机制,这里,我采用的是组合View,用的是LinearLayout的方式。


今天的内容大致如下:


1、分析UI,如何布局


2、设置属性和方法,制定可扩展效果


3、部分源码剖析


4、开源地址及实用总结


一、分析UI,如何布局


拿到UI效果图后,其实也没什么好分析的,无非就是两块,顶部的完成按钮和底部的省份简称格子,一开始,打算用RecyclerView网格布局来实现,但是最后的删除按钮如何摆放就成了问题,直接悬浮在网格上边,动态计算位置,显然不太合适,也没有这样去搞的,索性直接抛弃这个方案,多布局的想法也实验过,但最终还是选择了最简单的LinearLayout组合View形式。


所谓简单,就是在省份简称数组的遍历中,不断的给LinearLayout进行追加子View,需要注意的是,本身的View,也就是我们自定义View,继承LinearLayout后,默认的是垂直方向的,往本身View追加的是横向属性的LinearLayout,这也是换行的效果,也就是,一行一个横向的LinearLayout,记住,横向属性的LinearLayout,才是最终添加View的直接父类。



换行的条件就是基于UI效果,当模于设置length等于0时,我们就重新创建一个水平的LinearLayout,这就可以了,是不是非常的简单。


至于最后的删除按钮,使其靠右,占据两个格子的权重设置即可。


二、设置属性和方法,制定可扩展效果


当我们绘制完这个身份简称键盘后,肯定是要给他人用的,基于灵活多变的需求,那么相对应的我们也需要动态的进行配置,比如背景颜色,文字的颜色,大小,还有边距,以及点击效果等等,这些都是需要外露,让使用者选择性使用的,目前所有的属性如下,大家在使用的时候,也可以对照设置。


设置属性


属性类型概述
lp_backgroundcolor整体的背景颜色
lp_rect_spacingdimension格子的边距
lp_rect_heightdimension格子的高度
lp_rect_margin_topdimension格子的距离上边
lp_margin_left_rightdimension左右距离
lp_margin_topdimension上边距离
lp_margin_bottomdimension下边距离
lp_rect_backgroundreference格子的背景
lp_rect_select_backgroundreference格子选择后的背景
lp_rect_text_sizedimension格子的文字大小
lp_rect_text_colorcolor格子的文字颜色
lp_rect_select_text_colorcolor格子的文字选中颜色
lp_is_show_completeboolean是否显示完成按钮
lp_complete_text_sizedimension完成按钮文字大小
lp_complete_text_colorcolor完成按钮文字颜色
lp_complete_textstring完成按钮文字内容
lp_complete_margin_topdimension完成按钮距离上边
lp_complete_margin_bottomdimension完成按钮距离下边
lp_complete_margin_rightdimension完成按钮距离右边
lp_text_click_effectboolean是否触发点击效果,true点击后背景消失,false不消失

定义方法


方法参数概述
keyboardContent回调函数获取点击的省份简称简称信息
keyboardDelete函数删除省份简称简称信息
keyboardComplete回调函数键盘点击完成
openProhibit函数打开禁止(使领学港澳),使其可以点击

三、关键源码剖析


这里只贴出部分的关键性代码,整体的代码,大家滑到底部查看源码地址即可。


定义身份简称数组


    //省份简称数据
private val mLicensePlateList = arrayListOf(
"京", "津", "渝", "沪", "冀", "晋", "辽", "吉", "黑", "苏",
"浙", "皖", "闽", "赣", "鲁", "豫", "鄂", "湘", "粤", "琼",
"川", "贵", "云", "陕", "甘", "青", "蒙", "桂", "宁", "新",
"藏", "使", "领", "学", "港", "澳",
)

遍历省份简称


mLength为一行展示多少个,当取模为0时,就需要换行,也就是再次创建一个水平的LinearLayout,添加至外层的垂直LinearLayout中,每个水平的LinearLayout中,则是一个一个的TextView。


  //每行对应的省份简称
var layout: LinearLayout? = null
//遍历车牌号
mLicensePlateList.forEachIndexed { index, s ->
if (index % mLength == 0) {
//重新创建,并添加View
layout = createLinearLayout()
layout?.weightSum = 1f
addView(layout)
val params = layout?.layoutParams as LayoutParams
params.apply {
topMargin = mRectMarginTop.toInt()
height = mRectHeight.toInt()
leftMargin = mMarginLeftRight.toInt()
rightMargin = mMarginLeftRight.toInt() - mSpacing.toInt()
layout?.layoutParams = this
}
}

//创建文字视图
val textView = TextView(context).apply {
text = s
//设置文字的属性
textSize = px2sp(mRectTextSize)
//最后五个是否禁止
if (mNumProhibit && index > (mLicensePlateList.size - 6)) {
setTextColor(mNumProhibitColor)
mTempTextViewList.add(this)
} else {
setTextColor(mRectTextColor)
}

setBackgroundResource(mRectBackGround)
gravity = Gravity.CENTER
setOnClickListener {
if (mNumProhibit && index > (mLicensePlateList.size - 6)) {
return@setOnClickListener
}
//每个格子的点击事件
changeTextViewState(this)
}
}

addRectView(textView, layout, 0.1f)
}

追加最后一个View


由于最后一个视图是一个图片,占据了两个格子的大小,所以需要特殊处理,需要做的就是,单独设置权重weight和单独设置宽度width,如下所示:


  /**
* AUTHOR:AbnerMing
* INTRODUCE:追加最后一个View
*/

private fun addEndView(layout: LinearLayout?) {
val endViewLayout = LinearLayout(context)
endViewLayout.gravity = Gravity.RIGHT
//删除按钮
val endView = RelativeLayout(context)
//添加删除按钮
val deleteImage = ImageView(context)
deleteImage.setImageResource(R.drawable.view_ic_key_delete)
endView.addView(deleteImage)

val imageParams = deleteImage.layoutParams as RelativeLayout.LayoutParams
imageParams.addRule(RelativeLayout.CENTER_IN_PARENT)
deleteImage.layoutParams = imageParams
endView.setOnClickListener {
//删除
mKeyboardDelete?.invoke()
invalidate()
}
endView.setBackgroundResource(mRectBackGround)
endViewLayout.addView(endView)
val params = endView.layoutParams as LayoutParams
params.width = (getScreenWidth() / mLength) * 2 - mMarginLeftRight.toInt()
params.height = LayoutParams.MATCH_PARENT

endView.layoutParams = params

layout?.addView(endViewLayout)
val endParams = endViewLayout.layoutParams as LayoutParams
endParams.apply {
width = (mSpacing * 3).toInt()
height = LayoutParams.MATCH_PARENT
weight = 0.4f
rightMargin = mSpacing.toInt()
endViewLayout.layoutParams = this
}


}

四、开源地址及使用总结


开源地址:github.com/AbnerMing88…


关于使用,其实就是一个类,大家可以下载源码,直接复制即可使用,还可以进行修改里面的代码,非常的方便,如果懒得下载源码,没关系,我也上传到了远程Maven,大家可以按照下面的方式进行使用。


Maven具体调用


1、在你的根项目下的build.gradle文件下,引入maven。


 allprojects {
repositories {
maven { url "https://gitee.com/AbnerAndroid/almighty/raw/master" }
}
}

2、在你需要使用的Module中build.gradle文件下,引入依赖。


 dependencies {
implementation 'com.vip:plate:1.0.0'
}

代码使用


   <com.vip.plate.LicensePlateView
android:id="@+id/lp_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:lp_complete_text_size="14sp"
app:lp_margin_left_right="10dp"
app:lp_rect_spacing="6dp"
app:lp_rect_text_size="19sp"
app:lp_text_click_effect="false" />


总结


大家在使用的时候,一定对照属性表进行选择性使用;关于这个省份简称自定义View,实现方式有很多种,我目前的这种也不是最优的实现方式,只是自己的一个实现方案,给大家一个作为参考的依据,好了,铁子们,本篇文章就先到这里,希望可以帮助到大家。


作者:二流小码农
来源:juejin.cn/post/7235484890019659834
收起阅读 »

Android 逆向从入门到入“狱”

免责声明 本次技术分享仅用于逆向技术的交流与学习,请勿用于其他非法用途;技术是把双刃剑,请善用它。 逆向是什么、可以做什么、怎么做 简单讲,就是将别人打包好的 apk 进行反编译,得到源码并分析代码逻辑,最终达成自己的目的。 可以做的事: 修改 sm...
继续阅读 »

免责声明


本次技术分享仅用于逆向技术的交流与学习,请勿用于其他非法用途;技术是把双刃剑,请善用它。


逆向是什么、可以做什么、怎么做




  • 简单讲,就是将别人打包好的 apk 进行反编译,得到源码并分析代码逻辑,最终达成自己的目的。




  • 可以做的事:



    • 修改 smali 文件,使程序达到自己想要的效果,重新编译签名安装,如去广告、自动化操作、电商薅羊毛、单机游戏修改数值、破解付费内容、汉化、抓包等

    • 阅读源码,借鉴别人写好的技术实践

    • 破解:小组件盒子:http://www.coolapk.com/apk/io.ifte…




  • 怎么做:



    • 这是门庞杂的技术活,需要知识的广度、经验、深度

    • 需要具体问题,具体分析,有针对性的学习与探索

    • 了解打包原理、ARM、Smali汇编语言

    • 加固、脱壳

    • Xposed、Substrate、Fridad等框架

    • 加解密

    • 使用好工具## 今日分享涉及工具




  • apktool:反编译工具



    • 反编译:apktool d <apkPath> o <outputPath>

    • 重新打包:apktool b <fileDirPath> -o <apkPath>

    • 安装:brew install apktool




  • jadx:支持命令行和图形界面,支持apk、dex、jar、aar等格式的文件查看





  • apksigner:签名工具





  • Charles:抓包工具



    • http://www.charlesproxy.com/

    • Android 7 以上抓包 HTTPS ,需要手机 Root 后将证书安装到系统中

    • Android 7 以下 HTTPS 直接抓




正题





  • 正向编译



    • java -> class -> dex -> apk




  • 反向编译



    • apk -> dex -> smali -> java




  • Smali 是 Android 的 Dalvik 虚拟机所使用的一种 dex 格式的中间语言




  • 官方文档source.android.com/devices/tec…




  • code.flyleft.cn/posts/ac692…




  • 正题开始,以反编译某瓣App为例:




    • jadx 查看 Java 源码,找到想修改的代码




    • 反编译得到 smali 源码:apktool d douban.apk -o doubancode --only-main-classes




    • 修改:找到 debug 界面入口并打开




    • 将修改后的 smali 源码正向编译成 apk:apktool b doubancode -o douban_mock1.apk




    • 重签名:jarsigner -verbose -keystore keys.jks test.apk key0




    • 此时的包不能正常访问接口,因为豆瓣 API 做了签名校验,而我们的新 apk 是用了新的签名,看接口抓包




    • 怎么办呢?




    • 继续分析代码,修改网络请求中的 apikey




    • 来看看新的 apk






  • 也可以做爬虫等




启发与防范



  • 混淆

  • 加固

  • 加密

  • 运行环境监测

  • 不写敏感信息或操作到客户端

  • App 运行签名验证

  • Api 接口签名验证


One More Thing



作者:Sinyu101220157
来源:juejin.cn/post/7202573260659163195
收起阅读 »

Java常用JVM参数实战

在Java应用程序的部署和调优过程中,合理配置JVM参数是提升性能和稳定性的关键之一。本文将介绍一些常用的JVM参数,并给出具体的使用例子和作用的分析。 内存管理相关参数 -Xmx和-Xms -Xmx参数用于设置JVM的最大堆内存大小,而-Xms参数用于设置J...
继续阅读 »

在Java应用程序的部署和调优过程中,合理配置JVM参数是提升性能和稳定性的关键之一。本文将介绍一些常用的JVM参数,并给出具体的使用例子和作用的分析。


内存管理相关参数


-Xmx和-Xms


-Xmx参数用于设置JVM的最大堆内存大小,而-Xms参数用于设置JVM的初始堆内存大小。这两个参数可以在启动时通过命令行进行配置,例如:


java -Xmx2g -Xms512m MyApp

上述示例将JVM的最大堆内存设置为2GB,初始堆内存设置为512MB。


作用分析:



  • 较大的最大堆内存可以增加应用程序的可用内存空间,提高性能。但也需要考虑服务器硬件资源的限制。

  • 合理设置初始堆内存大小可以减少JVM的自动扩容和收缩开销。


-XX:NewRatio和-XX:SurvivorRatio


-XX:NewRatio参数用于设置新生代与老年代的比例,默认值为2。而-XX:SurvivorRatio参数用于设置Eden区与Survivor区的比例,默认值为8。


例如,我们可以使用以下参数配置:


java -XX:NewRatio=3 -XX:SurvivorRatio=4 MyApp

作用分析:



  • 调整新生代与老年代的比例可以根据应用程序的特点来优化内存分配。

  • 调整Eden区与Survivor区的比例可以控制对象在新生代中的存活时间。


-XX:MaxMetaspaceSize


在Java 8及之后的版本中,-XX:MaxMetaspaceSize参数用于设置元空间(Metaspace)的最大大小。例如:


java -XX:MaxMetaspaceSize=512m MyApp

作用分析:



  • 元空间用于存储类的元数据信息,包括类的结构、方法、字段等。

  • 调整元空间的最大大小可以避免元空间溢出的问题,提高应用程序的稳定性。


-Xmn


-Xmn参数用于设置新生代的大小。以下是一个例子:


java -Xmn256m MyApp


  • -Xmn256m将新生代的大小设置为256MB。


作用分析:



  • 新生代主要存放新创建的对象,设置合适的大小可以提高垃圾回收的效率。


垃圾回收相关参数


-XX:+UseG1GC


-XX:+UseG1GC参数用于启用G1垃圾回收器。例如:


java -XX:+UseG1GC MyApp

作用分析:



  • G1垃圾回收器是Java 9及之后版本的默认垃圾回收器,具有更好的垃圾回收性能和可预测的暂停时间。

  • 使用G1垃圾回收器可以减少垃圾回收的停顿时间,提高应用程序的吞吐量。


-XX:ParallelGCThreads和-XX:ConcGCThreads


-XX:ParallelGCThreads参数用于设置并行垃圾回收器的线程数量,而-XX:ConcGCThreads参数用于设置并发垃圾回收器的线程数量。例如:


java -XX:ParallelGCThreads=4 -XX:ConcGCThreads=2 MyApp

作用分析:



  • 并行垃圾回收器通过使用多个线程来并行执行垃圾回收操作,提高回收效率。

  • 并发垃圾回收器在应用程序运行的同时执行垃圾回收操作,减少停顿时间。


-XX:+ExplicitGCInvokesConcurrent


-XX:+ExplicitGCInvokesConcurrent参数用于允许主动触发并发垃圾回收。例如:


java -XX:+ExplicitGCInvokesConcurrent MyApp

作用分析:



  • 默认情况下,当调用System.gc()方法时,JVM会使用串行垃圾回收器执行垃圾回收操作。使用该参数可以改为使用并发垃圾回收器执行垃圾回收操作,减少停顿时间。


性能监控和调优参数


-XX:+PrintGCDetails和-XX:+PrintGCDateStamps


-XX:+PrintGCDetails参数用于打印详细的垃圾回收信息,-XX:+PrintGCDateStamps参数用于打印垃圾回收发生的时间戳。例如:


java -XX:+PrintGCDetails -XX:+PrintGCDateStamps MyApp

作用分析:



  • 打印垃圾回收的详细信息可以帮助我们了解垃圾回收器的工作情况,检测潜在的性能问题。

  • 打印垃圾回收发生的时间戳可以帮助我们分析应用程序的垃圾回收模式和频率。


-XX:+HeapDumpOnOutOfMemoryError和-XX:HeapDumpPath


-XX:+HeapDumpOnOutOfMemoryError参数用于在发生内存溢出错误时生成堆转储文件,-XX:HeapDumpPath参数用于指定堆转储文件的路径。例如:


java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump/file MyApp

作用分析:



  • 在发生内存溢出错误时生成堆转储文件可以帮助我们分析应用程序的内存使用情况,定位内存泄漏和性能瓶颈。


-XX:ThreadStackSize


-XX:ThreadStackSize参数用于设置线程栈的大小。以下是一个例子:


java -XX:ThreadStackSize=256k MyApp

作用分析:



  • 线程栈用于存储线程执行时的方法调用和局部变量等信息。

  • 通过调整线程栈的大小,可以控制应用程序中线程的数量和资源消耗。


-XX:MaxDirectMemorySize


-XX:MaxDirectMemorySize参数用于设置直接内存的最大大小。以下是一个例子:


java -XX:MaxDirectMemorySize=1g MyApp

作用分析:



  • 直接内存是Java堆外的内存,由ByteBuffer等类使用。

  • 合理设置直接内存的最大大小可以避免直接内存溢出的问题,提高应用程序的稳定性。


其他参数


除了上述介绍的常用JVM参数,还有一些其他参数可以根据具体需求进行配置,如:



  • -XX:+DisableExplicitGC:禁止主动调用System.gc()方法。

  • -XX:+UseCompressedOops:启用指针压缩以减小对象引用的内存占用。

  • -XX:OnOutOfMemoryError:在发生OutOfMemoryError时执行特定的命令或脚本。


这些参数可以根据应用程序的特点和需求进行调优和配置,以提升应用程序的性能和稳定性。


总结


本文介绍了一些常用的JVM参数,并给出了具体的使用例子和作用分析。合理配置这些参数可以优化内存管理、垃圾回收、性能监控等方面,提升Java应用程序的性能和稳定性。


在实际应用中,建议根据应用程序的需求和性能特点,综合考虑不同参数的使用。同时,使用工具进行性能监控和分析,以找出潜在的问题和瓶颈,并根据实际情况进行调优。



我是蚂蚁背大象,文章对你有帮助给项目点个❤关注我GitHub:mxsm,文章有不正确的地方请您斧正,创建ISSUE提交PR~谢谢!


作者:蚂蚁背大象
来源:juejin.cn/post/7235435351049781304

收起阅读 »

从JS执行过程彻底讲清楚闭包、作用域链、变量提升等

web
前言 今天和大家一起 来 弄清楚一段 JavaScript 代码,它是如何执行的呢? 进而彻底讲明白闭包和作用于链的含义。 JavaScript 是一门高级语言,需要转化成机器指令,才能在电脑的 CPU 中运行。使 JavaScript 代码转换成机器指令,是...
继续阅读 »

前言


今天和大家一起 来 弄清楚一段 JavaScript 代码,它是如何执行的呢? 进而彻底讲明白闭包和作用于链的含义。

JavaScript 是一门高级语言,需要转化成机器指令,才能在电脑的 CPU 中运行。使 JavaScript 代码转换成机器指令,是通过 JavaScript 引擎来完成的。

JavaScript 引擎在把 JavaScript 代码转换成机器指令过程中,先对 JavaScript 代码进行解析(词法分析,语法分析),生成 AST 树,然后在通过一些列操作转换成机器指令,从而在 CPU 中运行。今天带大家详细讲解一下相关概念,并通过一个具体的案例加深大家对相关概念的理解。


JavaScript 执行过程


JavaSc 是一门高级语言,JavaScript 引擎会先把 JavaScript 代码转换成机器指令,先对 JavaScript 代码进行解析(词法分析,语法分析),生成 AST 树,然后转换成机器指令,进而会才能 CPU 中运行。

如下图所示:


JS执行过程.png


JS 执行过程,我们会遇到一些名词,这里在前面先做个解释


名词解释
ECS (Execution Context Stack) 执行上下文栈/调用栈以栈的形式调用创建的执行上下文。JavaScript 引擎内部实现了一个执行上文栈,目的就是为了执行代码。只要有代码执行,一定是在执行上下文栈中执行的。
GECGEC(Global Execution Context)全局执行上下文在执行全局代码前创建。 代码想要执行一定经过调用栈(上个关键词),也就意味着代码是以函数的形式被调用。但是全局代码(比如:定义变量、定义函数等)并不是函数形式,我们并不能主动调用代码,而被动的需要浏览器去调用代码。起到该作用的就是全局执行上下文,先解析全局代码然后执行。
FEC(Functional Execution Context)函数执行上下文在执行函数前创建。如果遇到函数的主动调用,就会生成一个函数执行上下文,入栈到函数调用栈中;当函数调动完成之后,就会执行出栈操作
VO(Variable Object)变量对象早期 ECMA 规范中的变量环境,对应 Object。该对象保存了当前执行上下文中的变量和函数地址(也就是当前作用域)。
VE(Variable Environment 变量环境最新 ECMA 规范中的变量环境,对应环境记录。 在最新 ECMA 的版本规范中:每一个执行上下文会关联到一个变量环境(Variable Environment,简称 VE),在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。对于函数来说,参数也会被作为环境记录添加到变量环境中。 简单来讲:1. 也就是相比于早期的版本规范,对于变量环境,已经去除了 VO 这个概念,提出了一个新的概念 VE;2. 没有规定 VE 必须为 Object,不同的 JS 引擎可以使用不同的类型,作为一条环境记录添加进去即可;3. 虽然新版本规范将变量环境改成了 VE,但是 JavaScript 的执行过程还是不变的,只是关联的变量环境不同,将 VE 看成 VO 即可;
GO(Global Object)全局对象全局对象,解析全局代码时创建,GEC 中关联的 VO 就是 GO
AO (Activation Object)函数对象函数对象,解析函数体代码时创建,FEC 中关联的 VO 就是 AO

名词太多不容易理解,这里不用去记,下面用到的时候重新从这里查找即可。


⚠️⚠️❗️❗️ 下面的小章节是按照特定顺序讲解的,讲解了代码生成执行过程。


解析阶段(编译器伪代码)



  1. 创建一个全局对象 GO/window(全局作用域)

  2. 词法分析。词法分析就是将我们写的代码块分解成词法单元。

  3. 检查语法是否有错误。语法分析是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。 并检查你的代码有没有什么低级的语法错误,如果有,引擎会停止执行并抛出异常。

  4. 给全局对象 GO 赋值(GO/VO 中不止包括变量自身,还包含其他的上下文等)


如果遇到了函数,编译阶段是不会去解析他,仅仅是在堆内存中创建了 FO 对象(会记录他的 parent scope 和 当前代码块),在 GO 中定义的函数变量会指向此变量。


生成全局对象的伪代码是什么? (变量提升考点)



  1. 从上到下查找,遇到 var 声明,先去全局作用域查找是否有同名变量,如有忽略当前声明,没有则添加声明变量为 GO 对象的属性,值为 undefined,并为变量分配内存。

  2. 遇到 function,如有同名变量,则将值替换为 function 函数,没有则添加到 GO,并分配内存并赋值。

  3. ES6 中的 class 声明也存在提升,不过它和 let、const 一样,被约束和限制了,其规定,如果再声明位置之前引用,则是不合法的,会抛出一个异常。


创建全局对象有什么用?



  • 所有的作用域(scope)都可以访问该全局对象;

  • 对象里面会包含一些全局的方法和类,像 Math、Date、String、Array、setTimeout 等等;

  • 其中有一个 window 属性是指向该全局对象自身的;

  • 该对象中会收集我们上面全局定义的变量,并设置成 undefined;

  • 全局对象是非常重要的,我们平时之所以能够使用这些全局方法和类,都是在这个全局对象中获取的;


什么是变量提升?


面试经常问是因为工作中经常因为他出现 BUG。
什么是变量提升:通常 JS 引擎会在正式执行之前先进行一次预编译,在这个过程中,首先将变量声明及函数声明提升至当前作用域的顶端,然后进行接下来的处理。



  • 函数提升只针对具名函数,而对于赋值的匿名函数(表达式函数),并不会存在函数提升。

  • 【提升优先级问题】函数提升优先级高于变量提升,且不会被同名变量声明覆盖,但是会被变量赋值后覆盖。而且存在同名函数与同名变量时,优先执行函数。


console.log(a);      //f a()
console.log(a()); //1
var a=1;
function a(){
console.log(1);
}
console.log(a); //1
a=3
console.log(a()) //a not a function

🤔 思考:(一道腾讯面试题)


var a=2;
function a() {
console.log(3);
}
console.log(typeof a);

为什么会进行变量提升?



  • 【比较信服的一种说法】正是由于第一批 JavaScript 虚拟机编译器上代码的设计失误,导致变量在声明之前就被赋予了 undefined 的初始值,而又由于这个失误产生的影响(无论好坏)过于广泛,因此在现在的 JavaScript 编译器中仍保留了变量提升的“特性”。

  • 【提升性能,这是预编译的好处,与变量提升没有没有关系】解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间

  • 【但也带了很多弊端】声明提升还可以提高 JS 代码的容错性,使一些不规范的代码也可以正常执行


现在讲完了变量赋值过程,接下来我们了解一下,全局执行上下文和函数执行上下文。


什么是 全局执行上下文 和 函数执行上下文?


全局执行上下文和函数执行上下文,大致也分为两个阶段:编译阶段和执行阶段。

解析过程中,获得了三个重要的信息(上下文包含的重要信息)【上下文对象中包含的信息有哪些?】:



  1. VO(Variable Object)对象:该对象保存了当前执行上下文中的变量和函数地址(也就是当前作用域)。

  2. 作用域链:VO(当前作用域) + ParentScope(父级作用域) 【在函数部分重要讲解】

  3. this 的指向: 视情况而定。


什么是作用域?


JavaScript 中的作用域说的是变量的可访问性和可见性。也就是说整个程序中哪些部分可以访问这个变量,或者说这个变量都在哪些地方可见。

作用域两个重要作用是 安全 和 命名压力(可以在不同的作用域下面定义相同的变量名)。

Javascript 中有三种作用域:



  1. 全局作用域

  2. 函数作用域

  3. 块级作用域
    作用域链:当在 Javascript 中使用一个变量的时候,首先 Javascript 引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。


记住两句话:



  1. 父级作用域在编译阶段就已经确定了。

  2. 查找变量就是按照作用域链查找(找到近停止)[]

    也可以这么理解:作用域链是 AO 对象上的一个变量[scopeChain] 里面的变量是 当前的 VO+parentVO,当某个变量不存在时会顺着 parentVO 向上查找,直到找到为止。


什么是词法作用域?


词法作用域(也叫静态作用域)从字面意义上看是说作用域在词法化阶段(通常是编译阶段)确定而非执行阶段确定的。 JavaScript 的作用域是词法作用域。

例如:


let number = 42;
function printNumber() {
console.log(number);
}
function log() {
let number = 54;
printNumber();
}
// Prints 42
log();

上面代码可以看出无论printNumber()在哪里调用console.log(number)都会打印42

动态作用域不同,console.log(number)这行代码打印什么取决于函数printNumber()在哪里调用。

如果是动态作用域,上面console.log(number)这行代码就会打印54

使用词法作用域,我们可以仅仅看源代码就可以确定一个变量的作用范围,但如果是动态作用域,代码执行之前我们没法确定变量的作用范围。


什么是执行上下文?


简单的来说,执行上下文是一种对 Javascript 代码执行环境的一种抽象概念,也就是说只要有 Javascript 代码运行,那么它就一定是运行在执行上下文中。


Javascript 一共有三种执行上下文:



  • 全局执行上下文。

    这是一个默认的或者说基础的执行上下文,所有不在函数中的代码都会在全局执行上下文中执行。它会做两件事:创建一个全局的 window 对象(浏览器环境下),并将 this 的值设置为该全局对象,另外一个程序中只能有一个全局上下文。

  • 函数执行上下文。

    每次调用函数时,都会为该函数创建一个执行上下文,每一个函数都有自己的一个执行上下文,但注意是该执行上下文是在函数被调用的时候才会被创建。函数执行上下文会有很多个,每当一个执行上下文被创建的时候,都会按照他们定义的顺序去执行相关代码(这会在后面会说到)。

  • Eval 函数执行上下文。

    eval 函数中执行的代码也会有自己的执行上下文,但由于 eval 函数不会被经常用到,这里就不做讨论了。(译者注:eval 函数容易导致恶意攻击,并且运行代码的速度比相应的替代方法慢,因为不推荐使用)。


执行上下文栈(调用栈)?


了解了什么是全局对象后,下面就来聊聊代码具体执行的地方。JS 引擎为了执行代码,引擎内部会有一个执行上下文栈(Execution Context Stack,简称 ECS),它是用来执行代码的调用栈。


ECS 如何执行?先执行谁呢?



  • 无疑是先执行我们的全局代码块;

  • 在执行前全局代码会构建一个全局执行上下文(Global Execution Context,简称 GEC);

  • 一开始 GEC 就会被放入到 ECS 中执行;
    GEC 主要包含三个内容(和 FEC 基本一样): VO,作用域链,this 的指向。


调用栈(ECS)、全局执行上下文、函数执行上下文(FEC)三者大致的关系如下:


调用栈与全局执行上下文与函数执行上下文三者关系.png


函数执行上下文



在执行全局代码遇到函数如何执行呢?




  • 在执行的过程中遇到函数,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称 FEC),并且加入到执行上下文栈(ECS)中。

  • 函数执行上下文(FEC)包含三部分内容:

    • AO:在解析函数时,会创建一个 Activation Objec(AO);

    • 作用域链:由函数 VO 和父级 VO 组成,查找是一层层往外层查找;

    • this 指向:this 绑定的值,在函数执行时确定;



  • 其实全局执行上下文(GEC)也有自己的作用域链和 this 指向,只是它对应的作用域链就是自己本身,而 this 指向为 window。


变量环境和记录(VO 和 VE)


上文中提到了很多次 VO,那么 VO 到底是什么呢?下面从 ECMA 新旧版本规范中来谈谈 VO。

在早期 ECMA 的版本规范中:每一个执行上下文会被关联到一个变量环境(Variable Object,简称 VO),在源代码中的变量和函数声明会被作为属性添加到 VO 中。对应函数来说,参数也会被添加到 VO 中。



  • 也就是上面所创建的 GO 或者 AO 都会被关联到变量环境(VO)上,可以通过 VO 查找到需要的属性;

  • 规定了 VO 为 Object 类型,上文所提到的 GO 和 AO 都是 Object 类型;
    在最新 ECMA 的版本规范中:每一个执行上下文会关联到一个变量环境(Variable Environment,简称 VE),在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中。对于函数来说,参数也会被作为环境记录添加到变量环境中。

  • 也就是相比于早期的版本规范,对于变量环境,已经去除了 VO 这个概念,提出了一个新的概念 VE;

  • 没有规定 VE 必须为 Object,不同的 JS 引擎可以使用不同的类型,作为一条环境记录添加进去即可;

  • 虽然新版本规范将变量环境改成了 VE,但是 JavaScript 的执行过程还是不变的,只是关联的变量环境不同,将 VE 看成 VO 即可;


什么是闭包?


MDN 上解释:闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。
也可以简单:函数 + 函数定义时的词法环境。


具体实例来理解整个执行过程


var name = 'curry'

console.log(message)

var message = 'I am new-coder.cn'

function foo() {
var name = 'foo'
console.log(name)
}

var num1 = 1
var num2 = 2

var result = num1 + num2

foo()

如下图:图中描述了上面这段代码在执行过程中所生成的变量。


JS代码执行过程


图中三个步骤的详细描述:



  1. 初始化全局对象。



  • 这里需要注意的是函数存放的是地址,会指向函数对象,与普通变量有所不同;

  • 从上往下解析 JS 代码,当解析到 foo 函数时,因为 foo 不是普通变量,并不会赋为 undefined,JS 引擎会在堆内存中开辟一块空间存放 foo 函数,在全局对象中引用其地址;

  • 这个开辟的函数存储空间最主要存放了该函数的父级作用域和函数的执行体代码块;




  1. 构建一个全局执行上下文(GEC),代码执行前将 VO 的内存地址指向 GlobalObject(GO)。




  2. 将全局执行上下文(GEC)放入执行上下文栈(ECS)中。




  3. 从上往下开始执行全局代码,依次对 GO 对象中的全局变量进行赋值。





  • 当执行 var name = 'curry'时,就从 VO(对应的就是 GO)中找到 name 属性赋值为 curry;

  • 接下来执行 console.log(message),就从 VO 中找到 message,注意此时的 message 还为 undefined,因为 message 真正赋值在下一行代码,所以就直接打印 undefined(也就是我们经常说的变量作用域提升);

  • 后面就依次进行赋值,执行到 var result = num1 + num2,也是从 VO 中找到 num1 和 num2 两个属性的值进行相加,然后赋值给 result,result 最终就为 50;

  • 最后执行到 foo(),也就是需要去执行 foo 函数了,这里的操作是比较特殊的,涉及到函数执行上下文,下面来详细了解;



  1. 遇到函数是怎么执行的
    继续来看上面的代码执行,当执行到 foo()时:



  • 先找到 foo 函数的存储地址,然后解析 foo 函数,生成函数的 AO;

  • 根据 AO 生成函数执行上下文(FEC),并将其放入执行上下文栈(ECS)中;

  • 开始执行 foo 函数内代码,依次找到 AO 中的属性并赋值,当执行 console.log(name)时,就会去 foo 的 VO(对应的就是 foo 函数的 AO)中找到 name 属性值并打印;



  1. 如此下去函数执行完成后会进行出栈,直到栈为空。代码出栈,如果出栈中发现当前上下文中的一些变量仍然被引用(形成了闭包),那就会将此出栈的上下文移动到堆中。


参考链接



作者:大熊全栈分享
来源:juejin.cn/post/7235463575300702263
收起阅读 »

前端开发:关于diff算法详解

web
前言 前端开发中,关于JS原生的内容和前端算法相关的内容一直都是前端工作中的核心,不管是在实际的前端业务开发还是前端求职面试,都是非常重要且必备的内容。那么本篇博文来分享一个关于前端开发中必备内容:diff算法,diff算法在前端实战中和前端求职面试中都是必...
继续阅读 »

前言



前端开发中,关于JS原生的内容和前端算法相关的内容一直都是前端工作中的核心,不管是在实际的前端业务开发还是前端求职面试,都是非常重要且必备的内容。那么本篇博文来分享一个关于前端开发中必备内容:diff算法,diff算法在前端实战中和前端求职面试中都是必备知识,整理总结一下,方便查阅使用。



diff算法是什么?


diff算法其实就是用于比较新旧虚拟DOM节点的差异性的算法。众所周知,每一个虚拟DOM节点都会有一个唯一标识,即Key,diff算法把树形结构按照层级分解,只比较同级元素,不同层级的节点只有创建和删除操作,通过对比新旧节点的Key来判断当前节点是否改变,把两个节点不同的地方存储在patch对象中,然后利用patch记录的消息进行局部更新DOM操作。


注意:若输入的新节点不是虚拟DOM , 那么需要将DOM节点转换为虚拟DOM才行,也就是说diff算法是针对虚拟DOM的。


patch()函数


patch函数其实就是用于节点上树,更新DOM的函数,也就是将新旧节点进行比较的函数。


diff算法的诞生


想必大家都知道,前端领域中在之前传统的DOM操作非常昂贵,数据的改变往往需要更新 DOM 树上的多个节点,可谓是牵一发而动全身,所以虚拟DOM和Diff算法的诞生就是为了解决上述问题。


前端的Web界面由 DOM 树来构成,当某一部分发生变化的时候,其实就是对应的某个 DOM 节点发生了变化。在 Vue中,构建 UI 界面的思路是由当前状态决定界面,前后两个状态就对应两套界面,然后由 Vue来比较两个界面的区别,本质是比较 DOM 节点差异当两个节点不同时应该如何处理,分为两种情况:一、节点类型不同;二、节点类型相同,但是属性不同。了解它们就需要对 DOM 树进行 Diff 算法分析。


diff算法的优势


diff算法的性能优势在于对比新旧两个 DOM节点的不同的时候,只对比同一级别的 DOM 节点,一旦发现有不同的地方,后续的DOM子节点将被删掉而不再作对比操作。使用diff算法提高了更新DOM的性能,不用再把整个页面全部删除后重新渲染;使用diff算法让虚拟DOM只包括必须的属性,不再把真实DOM的全部属性都拿出来。


diff算法的示例


这里先来以Vue来介绍一下diff算法的示例,这里直接在vue文件的模板中进行一个简单的标签实现,需要被vue处理成虚拟DOM,然后渲染到真实DOM中,具体代码如下所示:


//标签设置

//相对应的虚拟DOM结构

const dom = {

type: 'div',

attributes: [{id: 'content'}],

children: {

type: 'p',

attributes: [{class: 'sonP'}],

text: 'Hello'

}

}

通过上面的代码演示可以看到,新建标签之后,系统内存中会生成对应的虚拟DOM结构,由于真实DOM属性有很多,无法快速定位是哪个属性发生改变,然后通过diff算法能够快速找到发生变化的地方,然后只更新发生变化的部分渲染到页面上,也叫打补丁。


虚拟DOM


虚拟DOM是保存在程序内存中的,它只记录DOM的关键信息,然后结合diff算法来提高DOM更新的性能操作,在程序内存中比较差异,最后给真实DOM打补丁更新操作。


diff算法的比较规则


diff算法在进行比较操作的规则是这样的:



  1. 新节点前和旧节点前;

  2. 新节点后和旧节点后;

  3. 新节点后和旧节点前;

  4. 新节点前和旧节点后。


只要符合一种情况就不会再进行判断,若没有符合的,就需要循环来寻找,移动到旧前之前操作。结束查找的前提是:旧节点前<旧节点后 或者 新节点后>新节点前。


image.png


diff算法的三种比较方式


diff算法的比较方式有三种,分别如下所示:


方式一:根元素发生改变,直接删除重建


也就是同级比较,根元素发生改变,整个DOM树会被删除重建。如下示例:


//旧的虚拟DOM
<ul id="content">
<li class="sonP">hello</li>
</ul>
//新的虚拟DOM
<div id="content">
<p class="sonP">hello</p>
</div>

方式二:根元素不变,属性改变,元素复用,更新属性


这种方式就是在同级比较的时候,根元素不变,但是属性改变之后更新属性,示例如下所示:


//旧的虚拟DOM
<div id="content">
<p class="sonP">hello</p>
</div>
//新的虚拟DOM
<div id="content" title="hello">
<p class="sonP">hello</p>
</div>

方式三:根元素不变,子元素不变,元素内容发生变化


也就是根元素和子元素都不变,只是内容发生改变,这里涉及到三种小的情况:无Key直接更新、有Key但以索引为值、有Key但以id为值。


1、无Key直接更新


无Key直接就地更新,由于v-for不会移动DOM,所以只是尝试复用,然后就地更新;若需要v-for来移动DOM,则需要用特殊 attribute key 来提供一个排序提示。示例如下所示:


<ul id="content">
<li v-for="item in array">
{{ item }}
<input type="text">
</li>
</ul>

<button @click="addClick">在下标为1的位置新增一行</button>
export default {
data(){
return {
array: ["11", "44", "22", "33"]
}
},
methods: {
addClick(){
this.array.splice(1, 0, '44')
}
}
};

2、有Key但以索引为值


这里也是直接就地更新,通过新旧虚拟DOM对比,key存在就直接复用该标签更新的内容,若key不存在就直接新建一个。示例如下所示:


-

{{ item }}

在下标为1的位置新增一行

export default {

data(){

return {

array: ["11", "44", "22", "33"]

}

},

methods: {

addClick(){

this.array.splice(1, 0, '44')

}

}

};

通过上面代码可以看到,通过v-for循环产生新的DOM结构, 其中key是连续的, 与数据对应一致,然后比较新旧DOM结构, 通过diff算法找到差异区别, 接着打补丁到页面上,最后新增补一个li,然后从第二元素以后都要更新内容。


3、有Key但以id为值


由于Key的值只能是唯一不重复的,所以只能以字符串或数值来作为key。由于v-for不会移动DOM,所以只是尝试复用,然后就地更新;若需要v-for来移动DOM,则需要用特殊 attribute key 来提供一个排序提示。


若新DOM数据的key存在, 然后去旧的虚拟DOM里找到对应的key标记的标签, 最后复用标签;若新DOM数据的key存在, 然后去旧的虚拟DOM里没有找到对应的key标签的标签,最后直接新建标签;若旧DOM结构的key, 在新的DOM结构里不存在了, 则直接移除对应的key所在的标签。


<ul id="content">
<li v-for="object in array" :key="object.id">
{{ object.name }}
<input type="text">
</li>
</ul>

<button @click="addClick">在下标为1的位置新增一行</button>
export default {
data(){
return {
array: [{id:11,name:"11"}, {id:22,name:"22"}, {id:33,name:"33"}]
}
},
methods: {
addClick(){
this.array.splice(1, 0,{id:44,name: '44'})
}
}
};

最后


通过本文关于前端开发中关于diff算法的详细介绍,diff算法不管是在实际的前端开发工作中还是在前端求职面试中都是非常关键的知识点,所以作为前端开发者来说必须要掌握它相关的内容,尤其是从事前端开发不久的开发者来说尤为重要,是一篇值得阅读的文章,重要性就不在赘述。欢迎关注,一起交流,共同进步。


作者:三掌柜
来源:juejin.cn/post/7235534634775347261
收起阅读 »

另类年终总结:在煤老板开的软件公司实习是怎样一种体验?

某个编剧曾经说过:“怀念煤老板,他们从不干预我们创作,除了要求找女演员外,没有别的要求。”,现在的我毕业后正式工作快半年了,手上的活越来越多,负责的事项越来越多越来越杂,偶尔夜深人静走在回家的路上,也怀念当时在煤老板旗下的软件公司实习时无忧无虑的快乐生活,谨以...
继续阅读 »

某个编剧曾经说过:“怀念煤老板,他们从不干预我们创作,除了要求找女演员外,没有别的要求。”,现在的我毕业后正式工作快半年了,手上的活越来越多,负责的事项越来越多越来越杂,偶尔夜深人静走在回家的路上,也怀念当时在煤老板旗下的软件公司实习时无忧无虑的快乐生活,谨以此文纪念一下当时的时光。


煤老板还会开软件公司?


是的,煤老板家大业大,除了名下有几座矿之外,还有好多处农场、餐厅、物流等产业,可以说涉足了多个产业。当然最赚钱的主业还是矿业,听坊间传闻说,只要矿一开,钱就是哗哗的流进来。那么这个软件公司主要是做什么的呢,一小部分是给矿业服务的,负责矿山的相关人员使用记录展示每天矿上的相关数据,比如每天运输车辆的流转、每日矿上人力的核算。大部分的主力主要用于实现老板的雄伟理想,通过一个超级APP,搞定衣食住行,具体的业务如下,可以说是相当红火的。



煤老板的软件公司是怎么招聘的


这么有特色的一家公司,我是如何了解到并加入的呢。这还要从老板如何创立这家公司说起,老板在大学进修MBA的时候,认识了大学里计算机学院的几名优秀学子,然后对他们侃侃而谈自己的理念和对未来的设想,随后老板大笔一挥,我开家公司,咱们一起创业吧,钱我出,你们负责出技术。然后这几个计算机学院的同学,就携带着技术入股成为了这家软件公司的一员。随着老板的设想越来越丰富,最初进去的技术骨干也在不停的招兵买马,当时还是流行在QQ空间转发招聘信息。正是在茫茫动态中,多看了招聘信息一眼,使得该公司深深留在我的印象当中。后来我投递的时候,也是大学同学正在里面实习,于是简历直达主管。


面试都问了些啥


由于公司还处于初创阶段,所以没有那么复杂的一面二面三面HR面,一上来就是技术主管们来一个3对1面,开头聊聊大家都是校友,甚至可能还是同一个导师下的师兄弟,所以面试相对来说就没有那么难,问一问大学里写过的大作业项目,聊一聊之前实习做的东西,问一问熟悉的八股文,比如数据库事务,Spring等等,最后再关切的问一下实习时间,然后就送客等HR通知了。


工作都需要干啥


正如第一张图所示,公司的产品分成了几个模块,麻雀虽小,五脏俱全,公司里后端、前端、移动端、测试一应具全。我参与的正是公司智慧餐饮行业线的后端开发,俗称Java CRUD boy。由于公司里一众高薪招揽过来的开发,整体采用的开发理念还是很先进的。会使用sprint开发流程,每周一个迭代,就是发版上线还是不够devops,需要每周五技术leader自己启动各个脚本进行发版,将最新的代码启动到阿里云服务机器上。 虽然用户的体量不是很大,但是仍然包含Spring Cloud分布式框架、分库分表、Redis分布式锁、Elastic Search搜索框架、DTS消息传输复制框架等“高新科技”。每周伊始,会先进行需求评审,评估一下开发需要的工作量,随后就根据事先制定的节奏进行有条不紊的开发、测试、验收、上线。虽然工作难度不高,但是我在这家公司第一次亲身参与了产品迭代的全流程,为以后的实习、找工作都添加了一些工作经验。


因为是实习嘛,所以基本上都是踩点上班、准时下班。不过偶尔也存在老板一拍脑袋,说我们要两周造一个电子商城的情况,那个时候可真是加班加点,披星戴月带月的把项目的简易版本给完成、上线了。但是比较遗憾的是,后面也没有能大范围投入使用。


比如下面的自助借伞机,就是前司的一项业务,多少也是帮助了一些同学免于淋雨。



画重点,福利究竟有多好


首先公司的办公地点位于南京市中心,与新街口德基隔基相望。



每天发价值88元的内部币,用于在楼下老板开的餐厅里点餐,工作套餐有荤有素有汤有水果,可以说是非常的上流了。



如果不想吃工作套餐,还可以一起聚众点餐,一流的淮扬菜式,可以说非常爽了。 听说在点餐系统刚上线还没有内部币时,点餐是通过白名单的方式,不用付钱随便点。可惜我来晚了,没有体验到这么个好时候。



工作也标配imac一整套,虽然不好带走移动办公,但是用起来依然逼格满满。



熟悉的健身房福利当然少不了,而且还有波光粼粼的大泳池,后悔没有利用当时的机会多去几次学会游泳了。



除了这些基础福利之外,老板给的薪资比肩BAT大厂,甚至可能比他们还高一丢丢,在南京可以生活的相当滋润了。


既然说的这么好,那么为啥没有留下来呢。


唯一的问题当然是因为公司本身尚未盈利,所有这一切都依赖老板一个人的激情投入,假如老板这边出了啥问题,那整个公司也就将皮之不存,毛将焉附了。用软件领域的话来说,就是整个系统存在单点故障。所以尽管当时的各种福利很好,也选择离开找个更大的厂子先进去锻炼锻炼。


最后希望前老板矿上的生意越来越好,哪天我在外面卷不动了,还能收留我一下。


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

一个特别简单的队列功能

背景 身为一名ui仔,不光要会画ui,也有可能接触一些其他的需求,就比如我做直播的时候,就需要做礼物的队列播放,用户送礼,然后客户收到消息,然后一次播放礼物动画,这个需求很简单,自定义一个view并且里面有一个队列就可以搞定,但是如果要播放不同类型的内容,如果...
继续阅读 »

背景


身为一名ui仔,不光要会画ui,也有可能接触一些其他的需求,就比如我做直播的时候,就需要做礼物的队列播放,用户送礼,然后客户收到消息,然后一次播放礼物动画,这个需求很简单,自定义一个view并且里面有一个队列就可以搞定,但是如果要播放不同类型的内容,如果再去改这个ui,耦合度就会越来越大,那么这个view的定义就变了,那就太不酷啦,所以要将队列和ui拆开,所以我们要实现一个队列功能,然后可以接受不同类型的参数,然后依次执行。


如何实现的


一、咱们有两个队列,一个更新ui,一个缓存消息


二、咱们还要定时器,要轮询的检查任务


三、我们还要有队列进入的入口


四、我们也需要有处理队列的地方


五、我们还要有最后处理结果的方案


六、还得需要一个清除的功能,要不怎么回收呢


举一个栗子🌰


假设我们有个需求,收到消息后,弹出一个横幅通知,弹出横幅通知后几秒后消失,但是在这几秒中,会收到多条消息,你需要将这多条消息合并展示,听起来是不是很耳熟,就是说咱们聊天消息,一条消息展示一条内容,多条消息做合并。
现在看实际代码:如下所示

/**
* 堆栈消息帮助类
* */
object QueuePushHelper {
/**
* 通过type修改监听状态
* */
private var type = false


private var queuePushInterface: QueuePushInterface? = null

fun setQueuePushInterface(queuePushInterface: QueuePushInterface) {
this.queuePushInterface = queuePushInterface
}

/**
* 缓存所有消息
*/
private var cacheGiftList: LinkedList<QueuePushBean> =
LinkedList<QueuePushBean>()

/**
* 用于更新界面消息的队列
*/
private var uiMsgList: LinkedList<QueuePushBean> =
LinkedList<QueuePushBean>()

/**
* 定时器
*/
private var msgTimer = Executors.newScheduledThreadPool(2)

private lateinit var futures: ScheduledFuture<*>

/**
* 消息加入队列
*/
@JvmStatic
@Synchronized
fun onMsgReceived(customMsg: QueuePushBean) {
cacheGiftList.offer(customMsg)
}

/**
* 清空队列
* */
fun clearQueue() {
if (cacheGiftList.size > 0) {
cacheGiftList.clear()
}
if (uiMsgList.size > 0) {
uiMsgList.clear()
}
updateStatus(true)
}

/**
* 修改队列状态
* */
fun updateStatus(status: Boolean) {
type = status
}

/**
* 开启定时任务,数据清空
* */
@JvmStatic
fun startReceiveMsg() {
if (cacheGiftList.size > 0) {
cacheGiftList.clear()
}
if (uiMsgList.size > 0) {
uiMsgList.clear()
}
updateStatus(true)
futures = msgTimer.scheduleAtFixedRate(
TimerTask(),
0,
500,
TimeUnit.MILLISECONDS
)
}

/**
* 结束定时任务,数据清空
* ### 退出登录,需要清楚
* */
fun stopReceiveMsg() {
updateStatus(false)
if (cacheGiftList.size > 0) {
cacheGiftList.clear()
}
if (uiMsgList.size > 0) {
uiMsgList.clear()
}
if (::futures.isInitialized) {
futures.cancel(true)
}
msgTimer.shutdown()
}

/**
* 定时任务
* */
class TimerTask : Runnable {
override fun run() {
try {
synchronized(cacheGiftList) {
if (type) {
if (cacheGiftList.isNullOrEmpty()) {
return
}
uiMsgList.clear()
uiMsgList.offer(cacheGiftList.pollLast())
uiMsgList.poll()?.let {
if (cacheGiftList.size > 1) {
it.type = true
}
//poll一个用户信息,且从剩余集合中过滤出第一个不同名字的用户
queuePushInterface?.handleMessage(
it,
cacheGiftList.firstOrNull { its->
it.msg.fromNick !=its.msg.fromNick
}?.msg?.fromNick,
cacheGiftList.size + 1 // 因为poll了一个 所以数量加1
)
}
cacheGiftList.clear()
}
}
} catch (e: Exception) {
Log.d("QueuePushHelper", "run: e $e")
}
}
}

interface QueuePushInterface {

fun handleMessage(item: QueuePushBean, name: String?, msgCount: Int)
}
}

我代码都贴出来了,大家一看就知道 这个也太简单了。就不多解释了,如果还需要解释就留言吧,祝大家事事平安


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

浅谈 Android 线上帧率统计方案演进

帧率是我们衡量应用流畅度的一个重要基准指标。本文将简单介绍 Android 线上帧率计算方案的演进和业界基于帧率来衡量卡顿的相关指标设计。 帧率计算方案的演进 Choreographer.postFrameCallback 自 Android 4.1 引入 C...
继续阅读 »

帧率是我们衡量应用流畅度的一个重要基准指标。本文将简单介绍 Android 线上帧率计算方案的演进和业界基于帧率来衡量卡顿的相关指标设计。


帧率计算方案的演进


Choreographer.postFrameCallback


自 Android 4.1 引入 Choreographer 来调度 UI 线程的绘制相关任务之后,我们便有了一个简单衡量 UI 线程绘制效率的方案:通过持续调用 Choreographer 的 postFrameCallback 方法来得到基于 VSync 周期的回调,基于回调的间隔或者方法参数中的当前帧起始时间 frameTimeNanos 来计算帧率( 注意到基于回调间隔计算帧率的情况,由于 postFrameCallback 注册的回调类型是 Animation,早于 Traversal 但晚于 Input,实际回调的起点并不是当前帧的起点 )。


这一方案简单可靠,但关键问题在于 UI 线程通常都不会不间断地执行绘制任务,在不需要执行绘制任务( scheduleTraversals )时,UI 线程原本是不需要请求 VSync 信号的,而持续调用 postFrameCallback 方法的情况,则会连续请求 VSync 信号,使得 UI 线程始终处于比较活跃的状态,同时计算得到的帧率数据实际也会包含不需要绘制时的情况。准确来说,这一方案实际衡量的是 UI 线程的繁忙程度。


Matrix 早期方案


怎么能得到真正的帧率数据呢?腾讯 Matrix 的早期实现上,结合 Looper Printer 和 Choreographer 实现了一个比较巧妙的方案,做到了统计真正的 UI 线程绘制任务的细分耗时。


具体来说,基于 Looper Printer 做到对 UI 线程每个消息执行的监控,同时反射给 Choreographer 的 Input 回调队列头部插入一个任务,用来监听下一帧的起点。队头的 Input 任务被调用时,说明当前所在的消息是处理绘制任务的,则消息执行的终点也就是当前帧的终点。同时,在队头的 Input 任务被调用时,给 Animation 和 Traversal 的回调队列头部也插入任务,如此总共得到了四个时间点,可以将当前帧的耗时细分为 Input,Animation 和 Traversal 三个阶段。在当前处理绘制任务的消息执行完后,重新注册一个 Input 回调队列头部的任务,便可以继续监听下一帧的耗时情况。


这一方案没有使用 postFrameCallback( 不主动请求 VSync 信号 ),避免了前一个方案的问题,但整体方案上偏 hack,可能存在兼容性问题( 实际 Android 高版本上对 Choreographer 的内部回调队列确实有所调整 )。此外,当前方案也会受到上一方案的干扰,如果存在其他业务在持续调用 postFrameCallback,也会使得这里统计到的数据包含非绘制的情况。


JankStats 方案


实际在 Android 7.0 之后,官方已经引入了 FrameMetrics API 来提供帧耗时的详细数据。androidx 的 JankStats 库主要就是基于 FrameMetrics API 来实现的帧耗时数据统计。


在 Android 4.1 - 7.0 之间,JankStats 仍然是基于 Choreographer 来做帧率计算的,但方案和前两者均不同。具体来说,JankStats 通过监听 OnPreDrawListener 来感知绘制任务的发生,此时,通过反射 Choreographer 的 mLastFrameTimeNanos 来获取当前帧的起始时间,再通过往 UI 线程的消息队列头部抛任务的方式来获取当前帧的 UI 线程绘制任务结束时间( 在支持异步消息情况下,将该消息设置为异步消息,尽量保证获取结束时间的任务紧跟在当前任务之后 )。


这一方案简单可靠,而且得到的确实是真正的帧率数据。


在 Android 7.0 及以上版本,JankStats 则直接通过 Window 的新方法 addOnFrameMetricsAvailableListener,注册回调得到每一帧的详细数据 FrameMetrics。
FrameMetrics 的数据统计具体是怎么实现的?简单来说,Android 官方在整个绘制渲染流程上都做了打点来支持 FrameMetrics 的数据统计逻辑,具体包括了



  • 基于 Choreographer 记录了 VSync,Input,Animation,Traversal 的起始时间点

  • 基于 ViewRootImpl 记录了 Draw 的起始时间点( 结合 Traversal 区分开了 MeasureLayout 和 Draw 两段耗时 )

  • 基于 CanvasContext( hwui 中 RenderThread 的渲染流程 )记录了 SyncDisplayList,IssueDrawCommand,SwapBuffer 等时间点,Android 12 上更是进一步支持了 GPU 上的耗时统计


可以看到,FrameMetrics 提供了以往方案难以给到的详细分阶段耗时( 特别注意 FrameMetrics 提供的数据存在系统版本间的差异,具体的数据处理可以参考 JankStats 的内部实现 ),而且在内部实现上,相关数据在绘制渲染流程上默认便会被统计( 即使我们不注册监听 ),基于 FrameMetrics 来做帧率计算在数据采集阶段带来的额外性能开销微乎其微。


帧率相关指标设计


简单的 FPS( 平均帧率 )数据并不能很好的衡量卡顿。在能够准确采集到帧数据之后,怎么基于采集到的数据做进一步处理得到更有实际价值的指标才是更为关键的。


Android Vitals 方案


Android Vitals 本身只定义了两个指标,基于单帧耗时简单区分了卡顿问题的严重程度。将耗时大于 16ms 的帧定义为慢帧,将耗时大于 700ms 的帧定义为冻帧。


JankStats 方案


JankStats 的实现上,则默认将单帧耗时在期望耗时 2 倍以上的情况视为卡顿( 即满帧 60FPS 的情况,将单帧耗时超过 33.3ms 的情况定义为卡顿 )。


Matrix 方案


Matrix 的指标设计则在 Android Vitals 的基础上做了进一步细分,以掉帧数为量化指标( 即满帧 60FPS 的情况,将单帧耗时在 16.6ms 到 33.3ms 间的情况定义为掉一帧 ),将帧耗时细化为 Best / Normal / Middle / High / Frozen 多类情况,其中 Frozen 实际对应的就是 Android Vitals 中的冻帧。


可以看到以上三者都是基于单帧耗时本身而非平均帧率来衡量卡顿。


手淘方案


手淘的方案则给出了更多的指标。


基于帧率数据本身的,细分场景定义了滑动帧率和卡顿帧率。
滑动帧率比较好理解;
卡顿帧率指的是,在出现卡顿帧( 定义为单帧耗时 33.3ms 以上 )之后,持续统计一段时间的帧耗时( 直到达到 99.6ms ( 6 帧 )并且下一帧不是卡顿帧 )来计算帧率,通过单独统计以卡顿帧为起点的细粒度帧率来避免卡顿被平均帧率掩盖的问题。和前面几个方案相比,卡顿帧率的特点在于一定程度上保留了帧数据的时间属性,一定程度上可以区分出离散的卡顿帧和连续的卡顿。


基于单帧耗时的,将冻帧占比( 即大于 700 ms 的帧占总帧数的比例 )作为独立指标;此外还有参考 iOS 定义的 scrollHitchRate,即滑动场景下,预期外耗时( 即每一帧超过期望耗时部分的累加 )的占比。


此外,得益于 FrameMetrics 的详细数据,手淘的方案还实现了简单的自动化归因,即基于 FrameMetrics 的分阶段耗时数据简单判断是哪个阶段导致了当前这一帧的卡顿。


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

超好用的官方core-ktx库,了解一下(终)~

ktx
Handler.postDelayed()简化lambda传入 不知道大家在使用Handler下的postDelayed()方法是不是感觉很不简洁,我们看下这个函数源码:public final boolean postDelayed(@NonNull Run...
继续阅读 »

Handler.postDelayed()简化lambda传入


不知道大家在使用Handler下的postDelayed()方法是不是感觉很不简洁,我们看下这个函数源码:

public final boolean postDelayed(@NonNull Runnable r, long delayMillis) {
return sendMessageDelayed(getPostMessage(r), delayMillis);
}

可以看到Runnable类型的参数r放在第一位,在Kotlin中我们就无法利用其提供的简洁的语法糖,只能这样使用:

private fun test11(handler: Handler) {
handler.postDelayed({
//编写代码逻辑
}, 100)
}

有没有感觉很别扭,估计官方也发现了这个问题,就提供了这样一个扩展方法:

public inline fun Handler.postDelayed(
delayInMillis: Long,
token: Any? = null,
crossinline action: () -> Unit
): Runnable {
val runnable = Runnable { action() }
if (token == null) {
postDelayed(runnable, delayInMillis)
} else {
HandlerCompat.postDelayed(this, runnable, token, delayInMillis)
}
return runnable
}

可以看到将函数类型(相当于上面的Runnable中的代码执行逻辑)放到了方法参数的最后一位,这样利用kotlin的语法糖就可以这样使用:

private fun test11(handler: Handler) {
handler.postDelayed(200) {

}
}

可以看到这个函数类型使用了crossinline修饰,这个是用来加强内联的,因为其另一个Runnable的函数类型中进行了调用,这样我们就无法在这个函数类型action中使用return关键字了(return@标签除外),避免使用return关键字带来返回上的歧义不稳定性


除此之外,官方core-ktx还提供了类似的扩展方法postAtTime()方法,使用和上面一样!!


Context.getSystemService()泛型实化获取系统服务


看下以往我们怎么获取ClipboardManager:

private fun test11() {
val cm: ClipboardManager = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
}

看下官方提供的方法:

public inline fun <reified T : Any> Context.getSystemService(): T? =
ContextCompat.getSystemService(this, T::class.java)

借助于内联泛型实化简化了获取系统服务的代码逻辑:

private fun test11() {
val cm: ClipboardManager? = getSystemService()
}

泛型实化的用处有很多应用场景,大家感兴趣可以参考我另一篇文章:Gson序列化的TypeToken写起来太麻烦?优化它


Context.withStyledAttributes简化操作自定义属性


这个扩展一般只有在自定义View中较常使用,比如读取xml中设置的属性值,先看下我们平常是如何使用的:

private fun test11(
@NonNull context: Context,
@Nullable attrs: AttributeSet,
defStyleAttr: Int
) {
val ta = context.obtainStyledAttributes(
attrs, androidx.cardview.R.styleable.CardView, defStyleAttr,
androidx.cardview.R.style.CardView
)
//获取属性执行对应的操作逻辑
val tmp = ta.getColorStateList(androidx.cardview.R.styleable.CardView_cardBackgroundColor)

ta.recycle()
}

在获取完属性值后,还需要调用recycle()方法回收TypeArray,这个一旦忘记写就不好了,能让程序保证的写法那就尽量避免人为处理,所以官方提供了下面的扩展方法:

public inline fun Context.withStyledAttributes(
@StyleRes resourceId: Int,
attrs: IntArray,
block: TypedArray.() -> Unit
) {
obtainStyledAttributes(resourceId, attrs).apply(block).recycle()
}

使用如下:

private fun test11(
@NonNull context: Context,
@Nullable attrs: AttributeSet,
defStyleAttr: Int
) {
context.withStyledAttributes(
attrs, androidx.cardview.R.styleable.CardView, defStyleAttr,
androidx.cardview.R.style.CardView
) {
val tmp = getColorStateList(androidx.cardview.R.styleable.CardView_cardBackgroundColor)
}
}

上面的写法就保证了recycle()不会漏写,并且带接收者的函数类型block: TypedArray.() -> Unit也能让我们省略this直接调用TypeArray中的公共方法。


SQLiteDatabase.transaction()自动开启事务读写数据库


平常对SQLite进行写操作时为了效率及安全保证需要开启事务,一般我们都会手动进行开启和关闭,还是那句老话,能程序自动保证的事情就尽量避免手动实现,所以一般我们都会封装一个事务开启和关闭的方法,如下:

private fun writeSQL(sql: String) {
SQLiteDatabase.beginTransaction()
//执行sql写入语句
SQLiteDatabase.endTransaction()
}

官方core-ktx也提供了相似的扩展方法:

public inline fun <T> SQLiteDatabase.transaction(
exclusive: Boolean = true,
body: SQLiteDatabase.() -> T
): T {
if (exclusive) {
beginTransaction()
} else {
beginTransactionNonExclusive()
}
try {
val result = body()
setTransactionSuccessful()
return result
} finally {
endTransaction()
}
}

大家可以自行选择使用!


<K : Any, V : Any> lruCache()简化创建LruCache


LruCache一般用作数据缓存,里面使用了LRU算法来优先淘汰那些近期最少使用的数据。在Android开发中,我们可以使用其设计一个Bitmap缓存池,感兴趣的可以参考Glide内存缓存这块的源码,就利用了LruCache实现。


相比较于原有创建LruCache的方式,官方库提供了下面的扩展方法简化其创建流程:

inline fun <K : Any, V : Any> lruCache(
maxSize: Int,
crossinline sizeOf: (key: K, value: V) -> Int = { _, _ -> 1 },
@Suppress("USELESS_CAST")
crossinline create: (key: K) -> V? = { null as V? },
crossinline onEntryRemoved: (evicted: Boolean, key: K, oldValue: V, newValue: V?) -> Unit =
{ _, _, _, _ -> }
): LruCache<K, V> {
return object : LruCache<K, V>(maxSize) {
override fun sizeOf(key: K, value: V) = sizeOf(key, value)
override fun create(key: K) = create(key)
override fun entryRemoved(evicted: Boolean, key: K, oldValue: V, newValue: V?) {
onEntryRemoved(evicted, key, oldValue, newValue)
}
}
}

看下使用:

private fun createLRU() {
lruCache<String, Bitmap>(3072, sizeOf = { _, value ->
value.byteCount
}, onEntryRemoved = { evicted: Boolean, key: String, oldValue: Bitmap, newValue: Bitmap? ->
//缓存对象被移除的回调方法
})
}

可以看到,比之手动创建LruCache要稍微简单些,能稍微节省下使用成本。


bundleOf()快捷写入并创建Bundle对象


image.png


bundleOf()方法的参数被vararg声明,代表一个可变的参数类型,参数具体的类型为Pair,这个对象我们之前的文章有讲过,可以借助中缀表达式函数to完成Pair的创建:

private fun test12() {
val bundle = bundleOf("a" to "a", "b" to 10)
}

这种通过传入可变参数实现的Bundle如果大家不太喜欢,还可以考虑自行封装通用扩展函数,在函数类型即lambda中实现更加灵活的Bundle创建及写入:


1.自定义运算符重载方法set实现Bundle写入:

operator fun Bundle.set(key: String, value: Any?) {
when (value) {
null -> putString(key, null)

is Boolean -> putBoolean(key, value)
is Byte -> putByte(key, value)
is Char -> putChar(key, value)
is Double -> putDouble(key, value)
is Float -> putFloat(key, value)
is Int -> putInt(key, value)
is Long -> putLong(key, value)
is Short -> putShort(key, value)

is Serializable -> putSerializable(key, value)
//其实数据类型自定参考bundleOf源码实现
}
}

2.自定义BundleBuild支持向Bundle写入多个值

class BundleBuild(private val bundle: Bundle) {

infix fun String.to(that: Any?) {
bundle[this] = that
}
}

其中to()方法使用了中缀表达式的写法


3.暴漏扩展方法实现在lambda中完成Bundle的写入和创建

private fun bundleOf(block: BundleBuild.() -> Unit): Bundle {
return Bundle().apply {
BundleBuild(this).apply(block)
}
}

然后就可以这样使用:

private fun test12() {
val bundle = bundleOf {
"a" to "haha"
//经过一些逻辑操作获取结果后在写入Bundle
val t1 = 10 * 5
val t2 = ""
t2 to t1
}
}

相比较于官方库提供的bundleOf()提供的创建方式,通过函数类型也就是lambda创建并写入Bundle的方式更加灵活,并内部支持执行操作逻辑获取结果后再进行写入。


总结


关于官方core-ktx的研究基本上已经七七八八了,总共输出了五篇相关文章,对该库了解能够节省我们编写模板代码的时间,提高开发效率,大家如果感觉写的不错,可以点个赞支持下哈,感谢!!


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

咱不吃亏,也不能过度自卫

我之前写了一篇《吃亏不是福》,主要奉劝大家不要吃亏。这属于保护弱者的一面。 这次我谈谈不吃亏的一种人,他们不吃亏近乎强硬。这类人一点亏都不吃,以至于过度自我保护。 我们公司人事小刘负责考勤统计。发完考勤表之后,有个员工找到他,说出勤少统计了一天。 小刘一听,感...
继续阅读 »

我之前写了一篇《吃亏不是福》,主要奉劝大家不要吃亏。这属于保护弱者的一面。


这次我谈谈不吃亏的一种人,他们不吃亏近乎强硬。这类人一点亏都不吃,以至于过度自我保护。


我们公司人事小刘负责考勤统计。发完考勤表之后,有个员工找到他,说出勤少统计了一天。


小刘一听,感觉自己有被指控的风险。


他立刻严厉起来:“每天都来公司,不一定就算全勤。没打卡我是不统计的”。


最后小刘一查,发现是自己统计错了。


小刘反而更加强势了:“这种事情,你应该早点跟我反馈,而且多催着我确认。你自己的事情都不上心,扣个钱啥的只能自己兜着”


这就是明显的不愿意吃亏,即使自己错了,也不愿意让自己置于弱势。


你的反应,决定别人怎么对你。这种连言语的亏都不吃的人,并不会让别人敬畏,反而会让人厌恶,进而影响沟通


我还有一个同事老王。他是一个职场老人,性格嘻嘻哈哈,业务能力也很强。


以前同事小赵和老王合作的时候,小赵宁愿经两层人传话给老王,也不愿意和他直接沟通。


我当时感觉小赵不善于沟通。


后来,当我和老王合作的时候,才体会到小赵的痛苦。


因为,老王是一个什么亏都不吃的人,谁来找他理论,他就怼谁。


你告诉他有疏漏,他会极力掩盖问题,并且怒怼你愚昧无知。


就算你告诉他,说他家着火了。他首先说没有。你一指那不是烧着的吗?他回复,你懂个屁,你知道我几套房吗?我说的是我另一个家没着火。


有不少人,从不吃亏,无论什么情况,都不会让自己处于弱势。


这类人喜欢大呼小叫,你不小心踩他脚了,他会大喊:践踏我的尊严,和你拼了!


心理学讲,愤怒源于恐惧,因为他想逃避当前不利的局面


人总会遇到各种不公的待遇,或误会,或委屈。


遇到争议时,最好需要确认一下,排除自己的问题。


如果自己没错,那么比较好的做法就是:“我认为你说得不合理,首先……其次……最后……”。


不盲目服软,也不得理不饶人,全程平心静气,有理有据。这种人绝对人格魅力爆棚,让人敬佩。


最后,有时候过度强硬也是一种策略,可以很好地过滤和震慑一些不重要的事物。


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

抽象类与抽象方法

类到对象是实例化。对象到类是抽象。 抽象类: 1.什么是抽象类? 类和类之间具有共同特征,将这些共同特征提取出来,形成的就是抽象类。 类本身是不存在的,所以抽象类无法创建对象(无法实例化) 2.抽象类属于什么类型?抽象类也属于引用数据类型。 3.抽象类怎么定义...
继续阅读 »

类到对象是实例化。对象到类是抽象。


抽象类:
1.什么是抽象类?
类和类之间具有共同特征,将这些共同特征提取出来,形成的就是抽象类。
类本身是不存在的,所以抽象类无法创建对象(无法实例化)


2.抽象类属于什么类型?抽象类也属于引用数据类型。


3.抽象类怎么定义?@#^%&^^&^%
语法:
(修饰符列表)abstract class类名{类体}


4.抽象类是无法实例化的,无法创建对象的,所以抽象类是用来被子类继承的。


5.final和abstract不能联合使用,这两个关键字是对立的。


6.抽象类的子类可以是抽象类。


7.抽象类虽然无法实例化,但是抽象类有构造方法,这个构造方法提供子类使用的。


8.抽象类关联到一个概念,抽象方法,什么是抽象方法呢?
抽象方法标识没有实现的方法,没有方法体的方法。例如:
public abstract void dosome();


9.抽象方法的类必须是抽象类,抽象类的方法不一定要是抽象方法。
抽象方法的特点是:特点1:没有方法体,以分号结尾。特点2:前面修饰符列表中有abstract关键字


10.抽象类中不一定有抽象方法,但抽象方法必须出现在抽象类中


11.非抽象方法集成抽象类,必须重写抽象类里的方法,如果是抽象类继承抽象类,那么就不一定要重写父类的方法


1663425406511.jpg

public abstract class AbstractTest extends AbstractChildTest{
/**
* 类到对象是实例化。对象到类是抽象
*
*/
public static void main(String[] args) {

}

@Override
void Bird() {

}
}
abstract class AbstractChildTest{
//子类继承抽象类
abstract void Bird();
}

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

检测zip文件完整(进阶:APK文件渠道号)

zip
朋友聊天讨论到一个问题,怎么检测zip的完整性。zip是我们常用的压缩格式,不管是Win/Mac/Linux下都很常用,我们做文件的下载也会经常用到,网络充满不确定性,对于多个小文件(比如配置文件)的下载,我们希望只发起一次连接,因为建立连接是很耗费资源的,即...
继续阅读 »

朋友聊天讨论到一个问题,怎么检测zip的完整性。zip是我们常用的压缩格式,不管是Win/Mac/Linux下都很常用,我们做文件的下载也会经常用到,网络充满不确定性,对于多个小文件(比如配置文件)的下载,我们希望只发起一次连接,因为建立连接是很耗费资源的,即使现在http2.0可以对一条TCP连接进行复用,我们还是希望网络请求的次数越少越好,不管是对于稳定性还是成功失败的逻辑判断,都会有益处。


这个时候我们常用的其实就是把他们压缩成一个zip文件,下载下来之后解压就好了。


但很多时候zip会解压失败,如果我们的zip已经下载下来了,其实不存在没有访问权限的问题了,那原因除了空间不够之外,就是zip格式有问题了,zip文件为空或者只下载了一半。

这个时候就需要检测一下我们下载下来的zip是不是合法有效的zip了。

有这么几种思路:



  1. 直接解压,抛异常表明zip有问题

  2. 下载前得到zip文件的length,下载后检测文件大小

  3. 使用md5或sha1等摘要算法,下载下来后做md5,然后比对合法性

  4. 检测zip文件结尾的特殊编码格式,检测是否zip合法


这几种做法有利有弊,这里我们只看第4种。

我们讨论之前,可以大致了解一下zip的格式ZIP文件格式分析,我们关注的是End of central directory record,核心目录结束标记,每个zip只会出现一次。



































































OffsetBytesDescription
04End of central directory signature = 0x06054b50核心目录结束标记(0x06054b50)
42Number of this disk当前磁盘编号
62number of the disk with the start of the central directory核心目录开始位置的磁盘编号
82total number of entries in the central directory on this disk该磁盘上所记录的核心目录数量
102total number of entries in the central directory核心目录结构总数
124Size of central directory (bytes)核心目录的大小
164offset of start of central directory with respect to the starting disk number核心目录开始位置相对于archive开始的位移
202.ZIP file comment length(n)注释长度
22n.ZIP Comment注释内容

我们可以看到,0x06054b50所在的位置其实是在zip.length减去22个字节,所以我们只需要seek到需要的位置,然后读4个字节看是否是0x06054b50,就可以确定zip是否完整。

下面是一个判断的代码

        //没有zip文件注释时候的目录结束符的偏移量
private static final int RawEndOffset = 22;
//0x06054b50占4个字节
private static final int endOfDirLength = 4;
//目录结束标识0x06054b50 的小端读取方式。
private static final byte[] endOfDir = new byte[]{0x50, 0x4B, 0x05, 0x06};

private boolean isZipFile(File file) throws IOException {
if (file.exists() && file.isFile()) {
if (file.length() <= RawEndOffset + endOfDirLength) {
return false;
}
long fileLength = file.length();
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
//seek到结束标记所在的位置
randomAccessFile.seek(fileLength - RawEndOffset);
byte[] end = new byte[endOfDirLength];
//读取4个字节
randomAccessFile.read(end);
//关掉文件
randomAccessFile.close();
return isEndOfDir(end);
} else {
return false;
}
}

/**
* 是否符合文件夹结束标记
*/
private boolean isEndOfDir(byte[] src) {
if (src.length != endOfDirLength) {
return false;
}
for (int i = 0; i < src.length; i++) {
if (src[i] != endOfDir[i]) {
return false;
}
}
return true;
}

有人可能注意到了,你上面写的结束标识明明是0x06054b50,为什么检测的时候是反着写的。这里就涉及到一个大端小端的问题,录音的时候也能会遇到大小端顺序的问题,反过来读就好了。


涉及到二进制的查看和编辑,我们可以使用010editor这个软件来查看文件的十六进制或者二进制,并且可以手动修改某个位置的二进制。





他的界面大致长这样子,小端显示的,我们可以看到我们要得到的06 05 4b 50


我们看上面的表格里面最后一个表格里的 .ZIP file comment length(n).ZIP Comment ,意思是描述长度是两个字节,描述长度是n,表示这个长度是可变的。这个有啥作用呢?

其实就是给了一个可以写额外的描述数据的地方(.ZIP Comment),他的长度由前面的.ZIP file comment length(n)来控制。也就是zip允许你在它的文件结尾后面额外的追加内容,而不会影响前面的数据。描述文件的长度是两个字节,也就是一个short的长度,所以理论上可以寻址2^16^个位置。

举个例子:

修改之前:

修改之前


修改之后

修改之后

看上面两个文件,修改之前长度为0,我们把它改成2(注意大小端),我们改成2,然后随便在后面追加两个byte,保存,打开修改之后的zip,发现是可以正常运行的,甚至我们可以在长度是2的基础上追加多个byte,其实还是可以打开的。

所以回到标题内容,其实apk就是zip,我们同样可以在apk的Comment后面追加内容,比如可以当做渠道来源,或者完成这样的需求:h5网页A上下载的需要打开某个ActivityA,h5网页B上下载的需要打开某个ActivityB。


原理还是上面的原理,写入渠道或者配置,读取apk渠道或者配置,做相应统计或者操作。

        //magic -> yocn
private static final byte[] MAGIC = new byte[]{0x79, 0x6F, 0x63, 0x6E};
//没有zip文件注释时候的目录结束符的偏移量
private static final int RawEndOffset = 22;
//0x06054b50占4个字节
private static final int endOfDirLength = 4;
//目录结束标识0x06054b50 的小端读取方式。
private static final byte[] endOfDir = new byte[]{0x50, 0x4B, 0x05, 0x06};
//注释长度占两个字节,所以理论上可以支持 2^16 个字节。
private static final int commentLengthBytes = 2;
//注释长度
private static final int commentLength = 8;

private boolean isZipFile(File file) throws IOException {
if (file.exists() && file.isFile()) {
if (file.length() <= RawEndOffset + endOfDirLength) {
return false;
}
long fileLength = file.length();
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
//seek到结束标记所在的位置
randomAccessFile.seek(fileLength - RawEndOffset);
byte[] end = new byte[endOfDirLength];
//读取4个字节
randomAccessFile.read(end);
//关掉文件
randomAccessFile.close();
return isEndOfDir(end);
} else {
return false;
}
}

/**
* 是否符合文件夹结束标记
*/
private boolean isEndOfDir(byte[] src) {
if (src.length != endOfDirLength) {
return false;
}
for (int i = 0; i < src.length; i++) {
if (src[i] != endOfDir[i]) {
return false;
}
}
return true;
}

/**
* zip(apk)尾追加渠道信息
*/
private void write2Zip(File file, String channelInfo) throws IOException {
if (isZipFile(file)) {
long fileLength = file.length();
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw");
//seek到结束标记所在的位置
randomAccessFile.seek(fileLength - commentLengthBytes);
byte[] lengthBytes = new byte[2];
lengthBytes[0] = commentLength;
lengthBytes[1] = 0;
randomAccessFile.write(lengthBytes);
randomAccessFile.write(getChannel(channelInfo));
randomAccessFile.close();
}
}

/**
* 获取zip(apk)文件结尾
*
* @param file 目标哦文件
*/
private String getZipTail(File file) throws IOException {
long fileLength = file.length();
RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
//seek到magic的位置
randomAccessFile.seek(fileLength - MAGIC.length);
byte[] magicBytes = new byte[MAGIC.length];
//读取magic
randomAccessFile.read(magicBytes);
//如果不是magic结尾,返回空
if (!isMagicEnd(magicBytes)) return "";
//seek到读到信息的offest
randomAccessFile.seek(fileLength - commentLength);
byte[] lengthBytes = new byte[commentLength];
//读取渠道
randomAccessFile.read(lengthBytes);
randomAccessFile.close();
char[] lengthChars = new char[commentLength];
for (int i = 0; i < commentLength; i++) {
lengthChars[i] = (char) lengthBytes[i];
}
return String.valueOf(lengthChars);
}

/**
* 是否以魔数结尾
*
* @param end 检测的byte数组
* @return 是否结尾
*/
private boolean isMagicEnd(byte[] end) {
for (int i = 0; i < end.length; i++) {
if (MAGIC[i] != end[i]) {
return false;
}
}
return true;
}

/**
* 生成渠道byte数组
*/
private byte[] getChannel(String s) {
byte[] src = s.getBytes();
byte[] channelBytes = new byte[commentLength];
System.arraycopy(src, 0, channelBytes, 0, commentLength);
return channelBytes;
}

//读取源apk的路径
public static String getSourceApkPath(Context context, String packageName) {
if (TextUtils.isEmpty(packageName))
return null;
try {
ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(packageName, 0);
return appInfo.sourceDir;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return null;
}

这里使用了一个魔数的概念,表明是否是写入了我们特定的渠道,只有写了我们特定渠道的基础上才会去读取,防止读到了没有写过的文件。

读取渠道的时候首先获取安装包的绝对路径。Android系统在用户安装app时,会把用户安装的apk拷贝一份到/data/apk/路径下,通过getSourceApkPath 可以获取该apk的绝对路径。如果使用rw可能会有权限问题,所以读取的时候只使用r就可以了。


参考:
ZIP文件格式分析
全民K歌增量升级方案


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

由浅入深,详解 Lifecycle 生命周期组件的那些事

Hi , 你好 :) 引言 在2022的今天,AndroidX 普遍的情况下,JetPack Lifecycle 也早已经成为了开发中的基础设施,小到 View(扩展库) ,大到 Activity,都隐藏着它的身影,而了解 Lifecycle 也正是理解 Je...
继续阅读 »

Hi , 你好 :)


引言


在2022的今天,AndroidX 普遍的情况下,JetPack Lifecycle 也早已经成为了开发中的基础设施,小到 View(扩展库) ,大到 Activity,都隐藏着它的身影,而了解 Lifecycle 也正是理解 JetPack 组件系列库生命感知设计的基础。



本篇定位中级,将从背景到源码实现,从而理解其背后的设计思想。



导航


看完本篇,你将会搞清以下问题:



  • Lifecycle 的前世今生;

  • Lifecycle 可以干什么;

  • Lifecycle 源码解析;


背景


在开始前,我们先聊聊关于 Lifecycle 的前世今生,从而便于我们更好的理解 Lifecycle 存在的意义。


洪荒之时


Lifecycle 之前(不排除现在😂),如果我们要在某个生命周期去执行一些操作时,经常会在Act或者Fragment写很多模版代码,如下两个示例:



  1. 比如,有一个定时器,我们需要在 Activity 关闭时将其关闭,从而避免因此导致的内存问题,所以我们自然会在 onDestory() 中去stop一下。这些事看起来似乎不麻烦,但如果是一个重复多处使用的代码,细心的开发者会将其单独抽离成为一个 case ,从而通过组合的方式降低我们主类中的逻辑,但不可避免我们依然还要存在好多模版代码,因为每次都需要 onStop() 清理或者其他操作(除非写到base,但不可接受☹️)。


📌 如果能不需要开发者自己手动,该有多好?



  1. 在老版本的友盟中,我们经常甚至需要在基类的 Activity 中复写 onResume()onPause() 等方法,这些事情说麻烦也不麻烦,但总是感觉很不舒服。不过,有经验的开发者肯定会想喷,为什么一个三方库,你就自己不会通过application.registerActivityLifecycleCallbacks 监听吗🤌🏼。


📌 注意,Application有监听全局Act生命周期的方法,Act也有这个方法。🤖


盘古开天


JetPack 之前,Android 一直秉承着传统的 MVC 架构,即 xml 作为 View, Activity 作为 ControlModel 层由数据模型或者仓库而定。虽然说官方曾经有一个MVP的示例,但真正用的人并不多。再加上官方一直也没推荐过 Android 的架构指南,这就导致传统的Android开发方式和系统的碎片化一样☹️,五花八门。随着时间的推移,眼看前端的MVVM已愈加成熟,后有Flutter,再加上开发者的需求等背景下,Google于2017年发布了新的开发架构: AAC,全名 Architecture,并且伴随着一系列相关组件,从而帮助开发者提高开发体验,降低错误概率,减少模版代码。


而本篇的主题 Lifecycle 正是其中作为基础设施的存在,在 sdk26 之后,更是被写入了基础库中。


那Lifecycle到底是干什么的呢?



Lifecycle 做的事情很简单,其就是用于检测组件(FragmentAct) 的生命周期,从而不必强依赖于 ActivityFragment ,帮助开发者降低模版代码。



常见用法


在官网中,对于 Lifecycle 的生命周期感知的整个流程如下所示:


image.png




Api介绍


相关字段




  • Event


    生命周期事件,对应具体的生命周期:


    ON_CREATE, ON_START, ON_RESUME, ON_PAUSE, ON_STOP, ON_DESTROY, ON_ANY




  • State


    生命周期状态节点,与 Event 紧密关联,Event 是这些结点的具体边界,换句话说,State 代表了这一时刻的具体状态,其相当于一个范围集,在这个范围里,都是这个状态。而Event代表这一状态集里面触发的具体事件是什么。


    INITIALIZED

    构造函数初始化时,且未收到 `onCreate()` 事件时的状态;

    CREATED

    在 `onCreate()` 调用之后,`onStop()` 调用之前的状态;

    STARTED

    在 `onStart()` 调用之后,`onPause()` 调用之前的状态;



RESUMED

    在 `onResume()` 调用时的状态;

`DESTROYED`

`onDestory()` 调用时的状态;

相关接口




  • LifecycleObserver


    基础 Lifecycler 实现接口,一般调用不到,可用于自定义的组件,从而避免在 Act 或者 Fragment 中的模版代码,例如ViewTreeLifecyleOwner




  • LifecycleEventObserver


    可以接受所有生命周期变化的接口;




  • FullLifecycleObserver


    用于监听生命周期变化的接口,提供了 onCreate()onStart()onPause()onStop()onDestory()onAny();




  • DefaultLifecycleObserver


    FullLifecycleObserver 的默认实现版本,相比前者,增加了默认 null 实现;






举个栗子


如下所示,通过实现 DefaultLifecycleObserver 接口,我们可以在中重写我们想要监听的生命周期方法,从而将业务代码从主类中拆离出来,且更便于复用。最后在相关类中直接使用 lifecycle.addObserver() 方法添加实例即可,这也是google推荐的用法。


image.png



上述示例中,我们使用了 viewLifecycle ,而不是 lifecycle ,这里顺便提一下。


见名之意,前者是视图(view)生命周期,后者则是非视图的生命周期,具体区别如下:


viewLifecycle 只会在 onCreateView-onDestroyView 之间有效。


lifecycle 则是代表 Fragment 的生命周期,在视图未创建时,onCreate(),onDestory() 时也会被调用到。





或者你有某个自定义View,想感知Fragment或者Act的生命周期,从而做一些事情,比如Banner组件等,与上面示例类似:


image.png



当然你也可以选择依赖:androidx.lifecycle:lifecycle-runtime 扩展库。


从而使用 view.findViewTreeLifecycleOwner() 的扩展函数获得一个 LifecycleOwner,从而在View内部自行监听生命周期,免除在Activity手动添加观察者的模版代码。


lifecycle.addObserver(view)



源码解析


Lifecycle



在官方的解释里,Lifecycle 是一个类,用于存储有关组件(Act或Fragment)声明周期状态新的类,并允许其他对象观察此类。



直接去看 Lifecycle 的源码,其实现方式如下:


image.png
总体设计如上所示,比较简单,就是观察者模式的接口模版:



使用者实现 LifecycleObserver 接口(),然后调用 addObserver() 添加到观察者列表,取消观察者时调用 rmeoveObserver() 移除掉即可。在相应的生命周期变动时,遍历观察者列表,然后通知实现了 LifecycleObserver 的实例,从而调用相应的方法。



因为其是一个抽象类,所以我们调用的一般都是它的具体实现类,也就是 LifecycleRegistry ,目前也是其的唯一实现类。




LifecycleRegistry


Lifecycle 的具体实现者,正如其名所示,主要用于管理当前订阅的 观察者对象 ,所以也承担了 Lifecycle 具体的实现逻辑。因为源码较长,所以我们做了一些删减,只需关注主流程即可,伪代码如下:

public class LifecycleRegistry extends Lifecycle {

// 生命周期观察者map,LifecycleObserver是观察者接口,ObserverWithState具体的状态分发的包装类
   private FastSafeIterableMap<LifecycleObserver, ObserverWithState> mObserverMap;
// 当前生命周期状态
   private State mState = INITIALIZED;
// 持有生命周期的提供商,Activity或者Fragment的弱引用
   private final WeakReference<LifecycleOwner> mLifecycleOwner;
// 当前正在添加的观察者数量,默认为0,超过0则认为多线程调用
   private int mAddingObserverCounter = 0;
// 是否正在分发事件
   private boolean mHandlingEvent = false;
// 是否有新的事件产生
   private boolean mNewEventOccurred = false;
// 存储主类的事件state
   private ArrayList<State> mParentStates = new ArrayList<>();

   @Override
   public void addObserver(@NonNull LifecycleObserver observer) {
       // 初始化状态,destory or init
       State initialState = mState == DESTROYED ? DESTROYED : INITIALIZED;
    // 📌 初始化实际分发状态的包装类
       ObserverWithState statefulObserver = new ObserverWithState(observer, initialState);
    // 将观察者添加到具体的map中,如果已经存在了则返回之前的,否则创建新的添加到map中
       ObserverWithState previous = mObserverMap.putIfAbsent(observer, statefulObserver);
// 如果上一步添加成功了,putIfAbsent会返回null
       if (previous != null) {
           return;
      }
    // 如果act或者ff被回收了,直接return
       LifecycleOwner lifecycleOwner = mLifecycleOwner.get();
       if (lifecycleOwner == null) {
           return;
      }
// 当前添加的观察者数量!=0||正在处理事件
       boolean isReentrance = mAddingObserverCounter != 0 || mHandlingEvent;
    // 📌 取得观察者当前的状态
       State targetState = calculateTargetState(observer);
       mAddingObserverCounter++;
    // 📌 如果当前观察者状态小于当前生命周期所在状态&&这个观察者已经被存到了观察者列表中
       while ((statefulObserver.mState.compareTo(targetState) < 0
               && mObserverMap.contains(observer))) {
        // 保存当前的生命周期状态
           pushParentState(statefulObserver.mState);
        // 返回当前生命周期状态对应的接下来的事件序列
           final Event event = Event.upFrom(statefulObserver.mState);
          ...
           // 分发事件
           statefulObserver.dispatchEvent(lifecycleOwner, event);
        // 移除当前的生命周期状态
           popParentState();
           // 再次获得当前的状态,以便继续执行
           targetState = calculateTargetState(observer);
      }

    // 处理一遍事件,保证事件同步
       if (!isReentrance) {
           sync();
      }
    // 回归默认值
       mAddingObserverCounter--;
  }
...
   static class ObserverWithState {
       State mState;
       LifecycleEventObserver mLifecycleObserver;

       ObserverWithState(LifecycleObserver observer, State initialState) {
        // 初始化事件观察者
           mLifecycleObserver = Lifecycling.lifecycleEventObserver(observer);
           mState = initialState;
      }

       void dispatchEvent(LifecycleOwner owner, Event event) {
           State newState = event.getTargetState();
           mState = min(mState, newState);
           mLifecycleObserver.onStateChanged(owner, event);
           mState = newState;
      }
  }
...
}

我们重点关注的是 addObserver() 即订阅生命周期变更时的逻辑,具体如下:



  1. 先初始化当前观察者的状态,默认两种,即 DESTROYED(销毁) 或者 INITIALIZED(无效),分别对应 onDestory()onCreate() 之前;

  2. 初始化 状态观察者(ObserverWithState) ,内部使用 Lifecycling.lifecycleEventObserver() 将我们传递进来的 生命周期观察者(LifecycleObser) 包装为一个 生命周期[事件]观察者LifecycleEventObserver,从而在状态变更时触发事件通知;

  3. 将第二步生成的状态观察者添加到缓存map中,如果之前已经存在,则停止接下来的操作,否则继续初始化;

  4. 调用 calculateTargetState() 获得当前真正的状态。

  5. 开始事件轮训,如果 当前观察者的状态小于此时真正的状态 && 观察者已经被添加到了缓存列表 中,则获得当前观察者下一个状态,并触发相应的事件通知 dispatchEvent(),然后继续轮训。直到不满足判断条件;



需要注意的是, 关于状态的判断,这里使用了compareTo() ,从而判断当前状态枚举是否小于指定状态。





Activity中的实现


Tips:


写过权限检测库的小伙伴,应该很熟悉,为了避免在 Activity 中手动实现 onActivityRequest() ,从而实现以回调的方式获得权限结果,我们往往会使用一个透明的 Fragment ,从而将模版方法拆离到单独类中,而这种实现方式正是组合的思想。


LifecycleActivity 中的实现正是上述的方式。




如下所示,当我们在 Activity 中调用 lifecycle 对象时,内部实际上是调用了 ComponentActivity.mLifecycleRegistry,具体逻辑如下:


image.png
不难发现,在我们的 Activity 初始化时,相应的 LifecycleRegistry 已经被初始化。


在上面我们说过,为了避免对基类的入侵,我们一般会用组合的方式,所以这里的 ReportFragment 正是 LifecycleActivity 中具体的逻辑承载方,具体逻辑如下:


ReportFragment.injectIfNeededIn


image.png


内部会对sdk进行判断,对应着两套流程,对于 sdk>=29 的,通过注册 Activity.registerActivityLifecycleCallbacks() 事件实现监听,对于 sdk<29 的,重写 Fragment 相应的生命周期方法完成。


ReportFragment 具体逻辑如下:


image.png




Fragment中的实现


直接去 Fragment.lifecycle 中看一下即可,伪代码如下:


image.png


总结如下:lifecycle 实例会在 Fragment 构造函数 中进行初始化,而 mViewLifecycleOwner 会在 performCreateView() 执行时初始化,然后在相应的 performXxx 生命周期执行时,调用相应的 lifecycle.handleLifecycleEvent() 从而完成事件的通知。


总结


Lifecycle 作为 JetPack 的基石,而理解其是我们贯穿相应生命周期的关键所在。


关于生命周期的通知,Lifecycle 并没有采用直接通知的方式,而是采用了 Event(事件) + State(状态) 的设计方式。




  • 对于外部生命周期订阅者而言,只需要关注事件 Event 的调用;

  • 而对于Lifecycle而言,其内部只关注 State ,并将生命周期划分为了多个阶段,每个状态又代表了一个事件集,从而对于外部调用者而言,无需拘泥于当前具体的状态是什么。



在具体的实现底层上面:




  • Activity 中,采用组合的方式而非继承,在 Activity.onCreate() 触发时,初始化了一个透明Fragment,从而将逻辑存于其中。对于sdk>=29的版本,因为 Activity 本身有监听生命周期的方法,从而直接注册监听器,并在相应的会调用触发生命周期事件更新;对于sdk<29的版本,因为需要兼容旧版本,所以重写了 Fragment 相应的生命周期方法,从而实现手动触发更新我们的生命周期观察者。

  • Fragment 中,会在 Fragment 构造函数中初始化相应的 Lifecycle ,并重写相应的生命周期方法,从而触发事件通知,实现生命周期观察者的更新。



每当我们调用 addObserver() 添加新的观察者时:



内部都会对我们的观察者进行包装,并将其包装为一个具体的事件观察者 LifecycleEventObserver,以及生成当前的观察者对应的状态实例(内部持有LifecycleEventObserver),并将其保存到 缓存map 中。接着会去对比当前的 观察者的状态lifecycle此时实际状态 ,如果 当前观察者状态<lifecycle对应的状态 ,则触发相应 Event 的通知,并 更新此观察者对应的状态 ,不断轮训,直到当前观察者状态 >= lifecycle 对应状态。



参阅



关于我


我是 Petterp ,一个 Android工程师 ,如果本文对你有所帮助,欢迎点赞支持,你的支持是我持续创作的最大鼓励!


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

雨下不下,花都结果;风吹不吹,我都是我。

最近不怎么忙了,想来写写文章,但突然也不知道从哪里开始聊,感情?工作?还是这稀里糊涂的生活。 2023年大年初六,我结束了和女朋友长达四年零七个月的爱情长跑,但很遗憾不是进入婚姻殿堂而是分手。 从18岁到23岁几乎已经占了我青春为数不多最美好的几年。但好在我熬...
继续阅读 »

最近不怎么忙了,想来写写文章,但突然也不知道从哪里开始聊,感情?工作?还是这稀里糊涂的生活。


2023年大年初六,我结束了和女朋友长达四年零七个月的爱情长跑,但很遗憾不是进入婚姻殿堂而是分手。
从18岁到23岁几乎已经占了我青春为数不多最美好的几年。但好在我熬过来了,最痛苦的那段已经过去了,我独自一人趴在地板上哭的泪水和口水混在一滩也没人看见的那段日子已经过去了,接下来是漫长而偶尔刺痛的恢复不知道要几年。


微信图片_20230330154857.jpg

“男人不经历分手永远不会成长”这句话忘了是谁说的了。以前觉得纯扯蛋的成不成长跟分手有毛关系,现在我认了,心里性格的变化自己都肉眼可见。越来越多的人生道理从几年前的不屑一顾到现在发生在自己身上才认同。可能成长就是这个样子?毕竟谁都是第一次做人,也不知道成长了该因为变成熟了而高兴还是因为离懵懂幼稚的青春越来越远而难过。


年后回来项目就开始忙,压力骤增。入职到现在八个月,平均一个月加班一次(周末)但过完年回来开始忙基本天天晚上九点十点走,其实作为程序员来说这种程度也还好,但跟年前相比确实忙了很多。年前基本是到点下班就走,但也好,忙一点,让自己不那么闲就不会胡思乱想那些乱七八糟的事情。不会那么伤感也不会那么焦虑。而且拿这个工资心里也没那么愧疚,要不天天摸鱼发工资我都想给老板退回去一点。
不过分手后也真的能攒下钱来了,就很神奇真的很神奇,两个月攒了一万二了。还借了朋友2000。以前每个月都剩不下钱来。今年回家终于不用听爸妈再说那句“攒攒钱吧”了


微信图片_20230330152333.jpg
微信图片_20230330150759.jpg

好在还有街舞、吉他、健身、养猫。这些事让我觉得不那么无聊,劳累的工作之余,周末还是要找点事情做。
不能让自己闲下来,闲下来就忍不住开始emo。


其实来南京快一年了,很去那些景点多看看,鸡鸣寺、玄武湖、夫子庙、还有逼哥的1701live house(偷偷告诉你我是因为听了很多年逼哥才选择来南京生活) 拍拍照什么的。但碍于一个人,没有小伙伴一起。觉得怪无聊的,就也放弃了,但我会改变自己的,接下来的日子我也会一个人吃火锅,一个人看电影,一个人去旅游,一个人逛小吃街,一个人拍照打卡。


其实也不是没有朋友,南京也有几个小伙伴小兄弟,认识好些年,关系也都很好,但我们的热衷不在一个频道,他们除了敲代码就是吃鸡、CSGO、联盟、元神。而我除了联盟偶尔还和他们打一打以外,基本也不怎么玩游戏了,可能是岁数大了,游戏瘾没有前几年那么狠了。


虽然现在也有很多爱好但好像也都只是单纯的打发时间,并没有哪件事不做就难受的不行的感觉,换句话说现在好像多少有点无欲无求?


微信图片_20230330152914.jpg

上周去报了江宁一个摩托车驾校,准备考个D证买个摩托车玩一玩(目前计划先买个二手GSX或者春风骑个一两年,没把自己撞死的话再换个najia或者450),但工作的地方在玄武区好像不让骑。那就只能出去玩骑啦,但也祈祷我能顺利考过,可能是我太笨了去年在北京练摩托车,油离配合老弄不好,动不动就熄火,让人笑话,去练了两次就不练了,希望这次能认真一点。


微信图片_20230330154408.jpg

其实也不是只顾着玩了,工作这方面的也在时不时的计划以及改变计划,因为毕竟现在一个岗位放出去一万个简历等着的行情确实不友好。所以现在的行情基本就是已经不再缺少初级、初中级前端。中级少了很多。那么想要在这行继续混下去就只有一条路那就是使劲的打怪刷经验,进化为高级前端。


由于前三年都是vuer,最近半年才转的reacter。所以react写起来还是没有vue那么得心应手,但是我总感觉react才是前端的未来(狗头保命)如果接下来的计划中准备好好恶补一下技术的话,应该就是以react为主vue为辅了。


去年立的flag依旧没变:在南京待1-2年狠补技术去上海冲刺,目标还是带10+人数的前端leader,天天熬夜加班的那种。


作者:我看你像个promise
来源:juejin.cn/post/7216223889487478840
收起阅读 »

从前后端的角度分析options预检请求

本文分享自华为云社区《从前后端的角度分析options预检请求——打破前后端联调的理解障碍》,作者: 砖业洋__ 。 options预检请求是干嘛的?options请求一定会在post请求之前发送吗?前端或者后端开发需要手动干预这个预检请求吗?不用文档定义堆砌...
继续阅读 »

本文分享自华为云社区《从前后端的角度分析options预检请求——打破前后端联调的理解障碍》,作者: 砖业洋__ 。


options预检请求是干嘛的?options请求一定会在post请求之前发送吗?前端或者后端开发需要手动干预这个预检请求吗?不用文档定义堆砌名词,从前后端角度单独分析,大白话带你了解!


从前端的角度看options——post请求之前一定会有options请求?信口雌黄!


你是否经常看到这种跨域请求错误?


image.png


这是因为服务器不允许跨域请求,这里会深入讲一讲OPTIONS请求。


只有在满足一定条件的跨域请求中,浏览器才会发送OPTIONS请求(预检请求)。这些请求被称为“非简单请求”。反之,如果一个跨域请求被认为是“简单请求”,那么浏览器将不会发送OPTIONS请求。


简单请求需要满足以下条件:



  1. 只使用以下HTTP方法之一:GETHEADPOST

  2. 只使用以下HTTP头部:AcceptAccept-LanguageContent-LanguageContent-Type

  3. Content-Type的值仅限于:application/x-www-form-urlencodedmultipart/form-datatext/plain


如果一个跨域请求不满足以上所有条件,那么它被认为是非简单请求。对于非简单请求,浏览器会在实际请求(例如PUTDELETEPATCH或具有自定义头部和其他Content-TypePOST请求)之前发送OPTIONS请求(预检请求)。


举个例子吧,口嗨半天是看不懂的,让我们看看 POST请求在什么情况下不发送OPTIONS请求


提示:当一个跨域POST请求满足简单请求条件时,浏览器不会发送OPTIONS请求(预检请求)。以下是一个满足简单请求条件的POST请求示例:


// 使用Fetch API发送跨域POST请求
fetch("https://example.com/api/data", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "key1=value1&key2=value2"
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("Error:", error));

在这个示例中,我们使用Fetch API发送了一个跨域POST请求。请求满足以下简单请求条件:



  1. 使用POST方法。

  2. 使用的HTTP头部仅包括Content-Type

  3. Content-Type的值为"application/x-www-form-urlencoded",属于允许的三种类型之一(application/x-www-form-urlencoded、multipart/form-data或text/plain)。


因为这个请求满足了简单请求条件,所以浏览器不会发送OPTIONS请求(预检请求)。


我们再看看什么情况下POST请求之前会发送OPTIONS请求,同样用代码说明,进行对比


提示:在跨域请求中,如果POST请求不满足简单请求条件,浏览器会在实际POST请求之前发送OPTIONS请求(预检请求)。


// 使用Fetch API发送跨域POST请求
fetch("https://example.com/api/data", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Custom-Header": "custom-value"
},
body: JSON.stringify({
key1: "value1",
key2: "value2"
})
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error("Error:", error));

在这个示例中,我们使用Fetch API发送了一个跨域POST请求。请求不满足简单请求条件,因为:



  1. 使用了非允许范围内的Content-Type值("application/json" 不属于 application/x-www-form-urlencodedmultipart/form-datatext/plain)。

  2. 使用了一个自定义HTTP头部 “X-Custom-Header”,这不在允许的头部列表中。


因为这个请求不满足简单请求条件,所以在实际POST请求之前,浏览器会发送OPTIONS请求(预检请求)。


你可以按F12直接在Console输入查看Network,尽管这个网址不存在,但是不影响观察OPTIONS请求,对比一下我这两个例子。


总结:当进行非简单跨域POST请求时,浏览器会在实际POST请求之前发送OPTIONS预检请求,询问服务器是否允许跨域POST请求。如果服务器不允许跨域请求,浏览器控制台会显示跨域错误提示。如果服务器允许跨域请求,那么浏览器会继续发送实际的POST请求。而对于满足简单请求条件的跨域POST请求,浏览器不会发送OPTIONS预检请求。


后端可以通过设置Access-Control-Max-Age来控制OPTIONS请求的发送频率。OPTIONS请求没有响应数据(response data),这是因为OPTIONS请求的目的是为了获取服务器对于跨域请求的配置信息(如允许的请求方法、允许的请求头部等),而不是为了获取实际的业务数据,OPTIONS请求不会命中后端某个接口。因此,当服务器返回OPTIONS响应时,响应中主要包含跨域配置信息,而不会包含实际的业务数据


本地调试一下,前端发送POST请求,后端在POST方法里面打断点调试时,也不会阻碍OPTIONS请求的返回


image.png


2.从后端的角度看options——post请求之前一定会有options请求?胡说八道!


在配置跨域时,服务器需要处理OPTIONS请求,以便在响应头中返回跨域配置信息。这个过程通常是由服务器的跨域中间件(Node.jsExpress框架的cors中间件、PythonFlask框架的flask_cors扩展)或过滤器(JavaSpringBoot框架的跨域过滤器)自动完成的,而无需开发人员手动处理。


以下是使用Spring Boot的一个跨域过滤器,供参考


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {

public CorsConfig() {
}

@Bean
public CorsFilter corsFilter()
{
// 1. 添加cors配置信息
CorsConfiguration config = new CorsConfiguration();
// Response Headers里面的Access-Control-Allow-Origin: http://localhost:8080
config.addAllowedOrigin("http://localhost:8080");
// 其实不建议使用*,允许所有跨域
config.addAllowedOrigin("*");

// 设置是否发送cookie信息,在前端也可以设置axios.defaults.withCredentials = true;表示发送Cookie,
// 跨域请求要想带上cookie,必须要请求属性withCredentials=true,这是浏览器的同源策略导致的问题:不允许JS访问跨域的Cookie
/**
* withCredentials前后端都要设置,后端是setAllowCredentials来设置
* 如果后端设置为false而前端设置为true,前端带cookie就会报错
* 如果后端为true,前端为false,那么后端拿不到前端的cookie,cookie数组为null
* 前后端都设置withCredentials为true,表示允许前端传递cookie到后端。
* 前后端都为false,前端不会传递cookie到服务端,后端也不接受cookie
*/

// Response Headers里面的Access-Control-Allow-Credentials: true
config.setAllowCredentials(true);

// 设置允许请求的方式,比如get、post、put、delete,*表示全部
// Response Headers里面的Access-Control-Allow-Methods属性
config.addAllowedMethod("*");

// 设置允许的header
// Response Headers里面的Access-Control-Allow-Headers属性,这里是Access-Control-Allow-Headers: content-type, headeruserid, headerusertoken
config.addAllowedHeader("*");
// Response Headers里面的Access-Control-Max-Age:3600
// 表示下回同一个接口post请求,在3600s之内不会发送options请求,不管post请求成功还是失败,3600s之内不会再发送options请求
// 如果不设置这个,那么每次post请求之前必定有options请求
config.setMaxAge(3600L);
// 2. 为url添加映射路径
UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
// /**表示该config适用于所有路由
corsSource.registerCorsConfiguration("/**", config);

// 3. 返回重新定义好的corsSource
return new CorsFilter(corsSource);
}
}


这里setMaxAge方法来设置预检请求(OPTIONS请求)的有效期,当浏览器第一次发送非简单的跨域POST请求时,它会先发送一个OPTIONS请求。如果服务器允许跨域,并且设置了Access-Control-Max-Age头(设置了setMaxAge方法),那么浏览器会缓存这个预检请求的结果。在Access-Control-Max-Age头指定的时间范围内,浏览器不会再次发送OPTIONS请求,而是直接发送实际的POST请求,不管POST请求成功还是失败,在设置的时间范围内,同一个接口请求是绝对不会再次发送OPTIONS请求的。


后端需要注意的是,我这里设置允许请求的方法是config.addAllowedMethod("*")*表示允许所有HTTP请求方法。如果未设置,则默认只允许“GET”和“HEAD”。你可以设置的HTTPMethodGET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE


经过我的测试,OPTIONS无需手动设置,因为单纯只设置OPTIONS也无效。如果你设置了允许POST,代码为config.addAllowedMethod(HttpMethod.POST); 那么其实已经默认允许了OPTIONS,如果你只允许了GET,尝试发送POST请求就会报错。


举个例子,这里只允许了GET请求,当我们尝试发送一个POST非简单请求,预检请求返回403,服务器拒绝了OPTIONS类型的请求,因为你只允许了GET,未配置允许OPTIONS请求,那么浏览器将收到一个403 Forbidden响应,表示服务器拒绝了该OPTIONS请求,POST请求的状态显示CORS error



Spring Boot中,配置允许某个请求方法(如POSTPUTDELETE)时,OPTIONS请求通常会被自动允许。这意味着在大多数情况下,后端开发人员不需要特意考虑OPTIONS请求。这种自动允许OPTIONS请求的行为取决于使用的跨域处理库或配置,最好还是显式地允许OPTIONS请求。


点击关注,第一时间了解华为云新鲜技术~


作者:华为云开发者联盟
来源:juejin.cn/post/7233587643724234811
收起阅读 »

快速生成定制化的Word文档:Python实践指南

1.1. 前言 众所周知,安服工程师又叫做Word工程师,在打工或者批量SRC的时候,如果产出很多,又需要一个一个的写报告的情况下会非常的折磨人,因此查了一些相关的资料,发现使用python的docxtpl库批量写报告效果很不错,记录一下。 1.2. 介绍 d...
继续阅读 »

1.1. 前言


众所周知,安服工程师又叫做Word工程师,在打工或者批量SRC的时候,如果产出很多,又需要一个一个的写报告的情况下会非常的折磨人,因此查了一些相关的资料,发现使用python的docxtpl库批量写报告效果很不错,记录一下。


1.2. 介绍


docxtpl 是一个用于生成 Microsoft Word 文档的模板引擎库,它结合了 docx 模块和 Jinja2 模板引擎,使用户能够使用 Microsoft Word 模板文件并在其中填充动态数据。它提供了一种方便的方式来生成个性化的 Word 文档,并支持条件语句、循环语句和变量等控制结构,以满足不同的文档生成需求。


官方GitHub地址:github.com/elapouya/py…


官方文档地址:docxtpl.readthedocs.io/en/latest/



简单来说:就是创建一个类似Jinja2语法的模板文档,然后往里面动态填充内容就可以了



安装:


pip3 install docxtpl

1.3. 基础使用


from docxtpl import DocxTemplate

doc = DocxTemplate("test.docx")
context = {'whoami': "d4m1ts"}
doc.render(context)
doc.save("generated_doc.docx")

其中,test.docx内容如下:


test.docx


生成后的结果如下:


gen


1.4. 案例介绍


1.4.1. 需求假设


写一份不考虑美观的漏扫报告,需要有统计结果图和漏洞详情,每个漏洞包括漏洞名、漏洞地址、漏洞等级、漏洞危害、复现过程、修复建议六个部分。


1.4.2. 模板文档准备


编写的模板文档如下,使用到了常见的iffor赋值等,保存为template.docx,后续只需要向里面填充数据即可。


template


1.4.3. 数据结构分析


传入数据需要一串json字符串,因此我们根据模板文档梳理好json结构,然后传入即可。


梳理好的数据结构如下:


{
"饼图": "111",
"柱状图": "222",
"漏洞简报": [
{
"漏洞名": "测试漏洞名1",
"漏洞等级": "高危"
}
],
"漏洞详情": [
{
"漏洞名": "测试漏洞名1",
"漏洞地址": "http://blog.gm7.org/",
"漏洞等级": "高危",
"漏洞危害": "危害XXX",
"复现过程": "先xxx,再xxx,最后xxx",
"修复建议": "更新到最新版本即可"
}
]
}

编写代码测试一下可行性:


from docxtpl import DocxTemplate

doc = DocxTemplate("template.docx")
context = {
"饼图": "111",
"柱状图": "222",
"漏洞简报": [
{
"漏洞名": "测试漏洞名1",
"漏洞等级": "高危"
},
{
"漏洞名": "测试漏洞名2",
"漏洞等级": "严重"
},
{
"漏洞名": "测试漏洞名2",
"漏洞等级": "中危"
}
],
"漏洞详情": [
{
"漏洞名": "测试漏洞名1",
"漏洞地址": "http://blog.gm7.org/",
"漏洞等级": "高危",
"漏洞危害": "危害XXX",
"复现过程": "先xxx,再xxx,最后xxx",
"修复建议": "更新到最新版本即可"
},
{
"漏洞名": "测试漏洞名2",
"漏洞地址": "http://bblog.gm7.org/",
"漏洞等级": "严重",
"漏洞危害": "危害XXX",
"复现过程": "先xxx,再xxx,最后xxx",
"修复建议": "更新到最新版本即可"
},
{
"漏洞名": "测试漏洞名3",
"漏洞地址": "http://cblog.gm7.org/",
"漏洞等级": "中危",
"漏洞危害": "危害XXX",
"复现过程": "先xxx,再xxx,最后xxx",
"修复建议": "更新到最新版本即可"
}
]
}

doc.render(context)
doc.save("generated_doc.docx")

很好,达到了预期的效果。


res


1.4.4. 加入图表


在上面的过程中,内容几乎是没问题了,但是图表还是没有展示出来。生成图表我们使用plotly这个库,并将生成内容写入ByteIO


相关代码如下:


import plotly.graph_objects as go
from io import BytesIO

def generatePieChart(title: str, labels: list, values: list, colors: list):
"""
生成饼图
https://juejin.cn/post/6911701157647745031#heading-3
https://juejin.cn/post/6950460207860449317#heading-5

:param title: 饼图标题
:param labels: 饼图标签
:param values: 饼图数据
:param colors: 饼图每块的颜色
:return:
"""

# 基础饼图
fig = go.Figure(data=[go.Pie(
labels=labels,
values=values,
hole=.4, # 中心环大小
insidetextorientation="horizontal"
)])
# 更新颜色
fig.update_traces(
textposition='inside', # 文本显示位置
hoverinfo='label+percent', # 悬停信息
textinfo='label+percent', # 饼图中显示的信息
textfont_size=15,
marker=dict(colors=colors)
)
# 更新标题
fig.update_layout(
title={ # 设置整个标题的名称和位置
"text": title,
"y": 0.96, # y轴数值
"x": 0.5, # x轴数值
"xanchor": "center", # x、y轴相对位置
"yanchor": "top"
}
)
image_io = BytesIO()
fig.write_image(image_io, format="png")
return image_io


def generateBarChart(title: str, x: list, y: list):
"""
生成柱状图
https://cloud.tencent.com/developer/article/1817208
https://blog.csdn.net/qq_25443541/article/details/115999537
https://blog.csdn.net/weixin_45826022/article/details/122912484

:param title: 标题
:param x: 柱状图标签
:param y: 柱状图数据
:return:
"""

# x轴长度最为18
b = x
x = []
for i in b:
if len(i) >= 18:
x.append(f"{i[:15]}...")
else:
x.append(i)

# 基础柱状图
fig = go.Figure(data=[go.Bar(
x=x,
y=y,
text=y,
textposition="outside",
marker=dict(color=["#3498DB"] * len(y)),
width=0.3
)])
# 更新标题
fig.update_layout(
title={ # 设置整个标题的名称和位置
"text": title,
"y": 0.96, # y轴数值
"x": 0.5, # x轴数值
"xanchor": "center", # x、y轴相对位置
"yanchor": "top"
},
xaxis_tickangle=-45, # 倾斜45度
plot_bgcolor='rgba(0,0,0,0)' # 背景透明
)
fig.update_xaxes(
showgrid=False
)
fig.update_yaxes(
zeroline=True,
zerolinecolor="#17202A",
zerolinewidth=1,
showgrid=True,
gridcolor="#17202A",
showline=True
)

image_io = BytesIO()
fig.write_image(image_io, format="png")
return image_io

1.4.5. 最终结果


要插入图片内容,代码语法如下:


myimage = InlineImage(tpl, image_descriptor='test_files/python_logo.png', width=Mm(20), height=Mm(10))

完整代码如下:


from docxtpl import DocxTemplate, InlineImage
from docx.shared import Mm
import plotly.graph_objects as go
from io import BytesIO


def generatePieChart(title: str, labels: list, values: list, colors: list):
"""
生成饼图
https://juejin.cn/post/6911701157647745031#heading-3
https://juejin.cn/post/6950460207860449317#heading-5

:param title: 饼图标题
:param labels: 饼图标签
:param values: 饼图数据
:param colors: 饼图每块的颜色
:return:
"""

# 基础饼图
fig = go.Figure(data=[go.Pie(
labels=labels,
values=values,
hole=.4, # 中心环大小
insidetextorientation="horizontal"
)])
# 更新颜色
fig.update_traces(
textposition='inside', # 文本显示位置
hoverinfo='label+percent', # 悬停信息
textinfo='label+percent', # 饼图中显示的信息
textfont_size=15,
marker=dict(colors=colors)
)
# 更新标题
fig.update_layout(
title={ # 设置整个标题的名称和位置
"text": title,
"y": 0.96, # y轴数值
"x": 0.5, # x轴数值
"xanchor": "center", # x、y轴相对位置
"yanchor": "top"
}
)
image_io = BytesIO()
fig.write_image(image_io, format="png")
return image_io


def generateBarChart(title: str, x: list, y: list):
"""
生成柱状图
https://cloud.tencent.com/developer/article/1817208
https://blog.csdn.net/qq_25443541/article/details/115999537
https://blog.csdn.net/weixin_45826022/article/details/122912484

:param title: 标题
:param x: 柱状图标签
:param y: 柱状图数据
:return:
"""

# x轴长度最为18
b = x
x = []
for i in b:
if len(i) >= 18:
x.append(f"{i[:15]}...")
else:
x.append(i)

# 基础柱状图
fig = go.Figure(data=[go.Bar(
x=x,
y=y,
text=y,
textposition="outside",
marker=dict(color=["#3498DB"] * len(y)),
width=0.3
)])
# 更新标题
fig.update_layout(
title={ # 设置整个标题的名称和位置
"text": title,
"y": 0.96, # y轴数值
"x": 0.5, # x轴数值
"xanchor": "center", # x、y轴相对位置
"yanchor": "top"
},
xaxis_tickangle=-45, # 倾斜45度
plot_bgcolor='rgba(0,0,0,0)' # 背景透明
)
fig.update_xaxes(
showgrid=False
)
fig.update_yaxes(
zeroline=True,
zerolinecolor="#17202A",
zerolinewidth=1,
showgrid=True,
gridcolor="#17202A",
showline=True
)

image_io = BytesIO()
fig.write_image(image_io, format="png")
return image_io


doc = DocxTemplate("template.docx")
context = {
"饼图": InlineImage(doc, image_descriptor=generatePieChart(
title="漏洞数量",
labels=["严重", "高危", "中危", "低危"],
values=[1, 1, 1, 0],
colors=["#8B0000", "red", "orange", "aqua"]
), width=Mm(130)),
"柱状图": InlineImage(doc, image_descriptor=generateBarChart(
title="漏洞类型",
x=["测试漏洞名1", "测试漏洞名2", "测试漏洞名3"],
y=[1, 1, 1]
), width=Mm(130)),
"漏洞简报": [
{
"漏洞名": "测试漏洞名1",
"漏洞等级": "高危"
},
{
"漏洞名": "测试漏洞名2",
"漏洞等级": "严重"
},
{
"漏洞名": "测试漏洞名2",
"漏洞等级": "中危"
}
],
"漏洞详情": [
{
"漏洞名": "测试漏洞名1",
"漏洞地址": "http://blog.gm7.org/",
"漏洞等级": "高危",
"漏洞危害": "危害XXX",
"复现过程": "先xxx,再xxx,最后xxx",
"修复建议": "更新到最新版本即可"
},
{
"漏洞名": "测试漏洞名2",
"漏洞地址": "http://bblog.gm7.org/",
"漏洞等级": "严重",
"漏洞危害": "危害XXX",
"复现过程": "先xxx,再xxx,最后xxx",
"修复建议": "更新到最新版本即可"
},
{
"漏洞名": "测试漏洞名3",
"漏洞地址": "http://cblog.gm7.org/",
"漏洞等级": "中危",
"漏洞危害": "危害XXX",
"复现过程": "先xxx,再xxx,最后xxx",
"修复建议": "更新到最新版本即可"
}
]
}

doc.render(context)
doc.save("generated_doc.docx")

结果如下:


result


作者:初始安全
来源:juejin.cn/post/7233597845919662139
收起阅读 »

看了十几篇MVX架构的文章后,我悟了...

当经过学一遍又一遍,改一遍又一遍,觉得学不懂想哭的时候,我选择了放弃(洗澡),然后我悟了。 适当开摆有益身心健康 当你纠结于文件该放在哪个包下,纠结于什么功能的代码应该放在哪个类中,于是在学习过程中不断修改,不断重构...开始了这样一个循环。 这有很大一部分原...
继续阅读 »

当经过学一遍又一遍,改一遍又一遍,觉得学不懂想哭的时候,我选择了放弃(洗澡),然后我悟了。


适当开摆有益身心健康


当你纠结于文件该放在哪个包下,纠结于什么功能的代码应该放在哪个类中,于是在学习过程中不断修改,不断重构...开始了这样一个循环。


这有很大一部分原因是因为不统一,架构是一种设计思想,而且大部分是由国外公司、大牛提出,首先在语言理解上就会有一定的差异和误解,如果我们能正确理解设计原则,就可以事半功倍。


就比如并发和并行,看过很多对于这两个的解释就是:并发是多个任务交替使用CPU,同一时刻只有一个任务在跑;并行是多个任务同时跑。


其实就是一种误解,【并发】和【并行】描述的是两个频道的事情。并发是一种处理方法,通过拆分代码,各部分代码互不影响,这样可以充分利用多核心。所以如果想让一个事情变得容易【并行】,先得让制定一个【并发】的方法。倘若一个事情压根就没有【并发】的方法,那么无论有多少个可以干活的人,也不能【并行】。


回到MVX架构,对于怎么样分包,怎么样拆分代码,我觉得应该从思想原理入手,因为文章的写法是各个作者理解,他们的理解不一定就是正确的,包括我。


就以谷歌推荐的架构原则来说,它有以下4点:分离关注点、通过数据模型驱动界面、单一数据源、单向数据流;推荐的架构图如下:


image.png


按照上面这张图,我们在Activity中写界面和界面的展示的数据,现在回看架构原则第一点”关注分离点”,于是我们把界面和界面的数据拆分开,这个过程是自然而然的,所以我更倾向于发挥自己的想象力去把架构实现好,而不是去进行拙略的模仿,现在回想起来20年时我在写项目的时候会自己思考如何去改进,于是自然而然添加了事件和状态(单向数据流),而在之前我并没有去看关于这方面的文章。


当你学习累了,那就大喊一句“开摆”,什么屁架构文章一边去,不学了。(优秀的文章还是值得我们学习的,这里只是我的情绪宣泄)


也许回过头你就学会了,这并不是什么魔法,而是把你从一个深坑中拉了出来,让你的大脑能换个方向思考问题。


我们需要重点学习的是设计原则,剩下的就是发挥我们的想象力。


相关资料


如何理解:程序、进程、线程、并发、并行、高并发? - 大宽宽 知乎 (zhihu.com)


应用架构指南  |  Android 开发者  |  Android Developers (google.cn)


作者:Fanketly
来源:juejin.cn/post/7234057845620408375
收起阅读 »

我给我的博客加了个在线运行代码功能

web
获取更多信息,可以康康我的博客,所有文章会在博客上先发布随记 - 记录指间流逝的美好 (xiaoyustudent.github.io) 前言 新的一年还没过去,我又开始搞事情了,偶尔一次用到了在线编辑网页代码的网站,顿时想到,能不能自己实现一个呢?(PS:反...
继续阅读 »

获取更多信息,可以康康我的博客,所有文章会在博客上先发布随记 - 记录指间流逝的美好 (xiaoyustudent.github.io)


前言


新的一年还没过去,我又开始搞事情了,偶尔一次用到了在线编辑网页代码的网站,顿时想到,能不能自己实现一个呢?(PS:反正也没事干),然后又想着,能不能用在博客上呢,这样有些代码可以直接展现出来,多好,说干就干,让我们康康怎么去实现一个在线编辑代码的功能吧。(PS:有瑕疵,还在学习!勿喷!orz)


大致介绍


大概的想法就是通过iframe标签,让我们自己输入的内容能够在iframe中显示出来,知识点如下,如果有其他问题,欢迎在下方评论区进行补充!



  1. 获取输入的内容

  2. 插入到iframe中

  3. 怎么在博客中显示



当然也有未解决的问题:目前输入的js代码不能操作输入的html代码,查了许多文档,我会继续研究的,各位大佬如果有想法欢迎讨论



页面搭建


页面搭建很简单,就是三个textarea块,加4个按钮,就直接上代码了


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>在线编辑器</title>
<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>
<script>
$(document).ready(function () {
$('.HTMLBtn').click(function () {
$("#cssTextarea").fadeOut(function () {
$("#htmlTextarea").fadeIn();
});
})

$('.CSSBtn').click(function () {
$("#htmlTextarea").fadeOut(function () {
$("#cssTextarea").fadeIn();
});
})
});
</script>
<style>
* {
padding: 0;
margin: 0;
}

body,
html {
width: 100%;
height: 100%;
}

.main {
display: flex;
flex-direction: row;
justify-content: space-between;
width: 100%;
height: 100%;
}

.textarea-box {
display: flex;
flex-direction: column;
width: calc(50% - 20px);
padding: 10px;
background: rgba(34, 85, 85, 0.067);
}

.textarea-function-box {
display: flex;
flex-direction: row;
justify-content: space-between;
}

.textarea-function-left,.textarea-function-right {
display: flex;
flex-direction: row;
}

.textarea-function-left div,
.textarea-function-right div {
padding: 5px 10px;
border: 1px solid rgb(9, 54, 99);
border-radius: 3px;
cursor: pointer;
}

.textarea-function-left div:not(:first-child) {
margin-left: 10px;
}

#htmlTextarea,
#cssTextarea {
height: calc(100% - 30px);
width: calc(100% - 20px);
margin-top: 10px;
padding: 10px;
overflow-y: scroll;
background: #fff;
}

.html-div {
background-color: cadetblue;
margin-top: 10px;
flex: 1;
}

.iframe-box {
width: 50%;
flex: 1;
overflow: hidden;
}
</style>
</head>

<body>
<div class="main">
<div class="textarea-box">
<div class="textarea-function-box">
<div class="textarea-function-left">
<div class="HTMLBtn">HTML</div>
<div class="CSSBtn">CSS</div>
</div>
<div class="textarea-function-right">
<input type="text" id="input_name">
<div class="run">运行</div>
<div class="download">保存</div>
</div>
</div>
<textarea id="htmlTextarea" placeholder="请输入html代码"></textarea>
<textarea id="cssTextarea" placeholder="请输入css代码" style="display: none;"></textarea>
</div>
<div class="iframe-box">
<iframe style="height: 100%;width: 100%;" src="" frameborder="0"></iframe>
</div>
</div>
</body>
</html>

忽略我的样式,能用就行!!


运行代码


这里是核心功能,应该怎么把代码运行出来呢,我这里用的是iframe,通过获取iframe元素,然后把对应的代码插入进去


$('.run').click(function () {
var htmlTextarea = document.querySelector('#htmlTextarea').value;
var cssTextarea = document.querySelector('#cssTextarea').value;
htmlTextarea += '<style>' + cssTextarea + '</style>'
// 获取html和css代码
let frameWin, frameDoc, frameBody;
frameWin = document.querySelector('iframe').contentWindow;
frameDoc = frameWin.document;
frameBody = frameDoc.body;
// 获取iframe元素

$(frameBody).html(htmlTextarea);
// 使用jqury的html方法把代码插入进去,这样能够直接执行
})

这样一个基本的在线代码编辑网页就完成了,接下来,我们看下怎么把这玩意给用在博客当中!


hexo设置


首先我们需要创建一个文件夹,用来放置我们写好的在线的html文件。在source文件夹下新建文件online,并且设置禁止渲染此文件夹,打开_config.yml文件,并设置以下


skip_render: online/*

页面设置


我目前想到的办法就是保存文件,然后在hexo里使用,添加以下代码


<div class="textarea-function-right">
<input type="text" id="input_name">
<div class="download">保存</div>
<!-- .... -->
</div>

<script>
function fake_click(obj) {
var ev = document.createEvent("MouseEvents");
ev.initMouseEvent(
"click", true, false, window, 0, 0, 0, 0, 0
, false, false, false, false, 0, null
);
obj.dispatchEvent(ev);
}

function export_raw(name, data) {
var urlObject = window.URL || window.webkitURL || window;
var export_blob = new Blob([data]);
var save_link = document.createElementNS("http://www.w3.org/1999/xhtml", "a")
save_link.href = urlObject.createObjectURL(export_blob);
save_link.download = name;
fake_click(save_link);
}

$(document).ready(function () {
$(".download").click(function () {
let scriptStr = $('html').first().context.getElementsByTagName("script")[2].innerHTML
var htmlTextarea = document.querySelector('#htmlTextarea').value != "" ? document.querySelector('#htmlTextarea').value : '""';
var cssTextarea = document.querySelector('#cssTextarea').value != "" ? document.querySelector('#cssTextarea').value : '""';
let htmlStr = $('html').first().context.getElementsByTagName("html")[0].innerHTML.replace(scriptStr, "").replace('<div class="download">保存</div>', "").replace('<input type="text" id="input_name">',"").replace("<script><\/script>", "<script>$(document).ready(function(){document.querySelector('#htmlTextarea').value = `" + htmlTextarea + "`;document.querySelector('#cssTextarea').value = `" + cssTextarea + "`;})<\/script>")
let n = $('#input_name').val()!=""?$('#input_name').val():"text";
export_raw(n+'.html', htmlStr);
})
})
</script>

可能很多同学会好奇为啥我这里用的script标签框起来,我们看下这个图片和这个代码


script.png


et scriptStr = $('html').first().context.getElementsByTagName("script")[2].innerHTML

很简单,我们保存后的代码,是没有这一段js代码的,所以需要替换掉,而这里一共有3个script块,最后一个,也就是下标为2的script块会被替换掉。同理,后面替换掉保存按钮,input输入框(输入框是输入文件名称的,默认名称是text)。


同时这里把我们输入的数据,通过js代码的方式加入进保存后的文件里,实现打开文件就能看到我们写的代码。之后我们把保存后的文件放在刚才我们创建的online文件夹下


text.png


hexo里面使用


使用就很简单了,我们通过iframe里面的src属性即可


<iframe src="/online/text.html" style="display:block;height:400px;width:100%;border:0.5px solid rgba(128,128,128,0.4);border-radius:8px;box-sizing:border-box;"></iframe>

展示图


show.png


作者:新人打工仔
来源:juejin.cn/post/7191520909709017144
收起阅读 »

判断数组成员的几种方法

web
在开发中经常需要我们在数组中查找元素又或者是判断元素是否存在,所以我列举了几种常用的方法供掘友参考学习。 indexOf() 首先想到的就是indexOf()方法,查找元素,并返回第一个找到的位置索引 [1,2,3,2].indexOf(2)  // 1 ...
继续阅读 »



在开发中经常需要我们在数组中查找元素又或者是判断元素是否存在,所以我列举了几种常用的方法供掘友参考学习。


indexOf()


首先想到的就是indexOf()方法,查找元素,并返回第一个找到的位置索引


 [1,2,3,2].indexOf(2)  // 1

他还支持第二个可选参数,指定开始查找的位置


 [1,2,3,2].indexOf(2,2)  // 3

但是indexOf()有个问题,他的实现是由===作为判断的,所以这容易造成一些问题,比如他对于NaN会造成误判


[NaN].indexOf(NaN) // -1
console.log(NaN === NaN) // false

如上,由于误判,没有找到匹配元素,所以返回-1,而在ES6对数组的原型上新增了incudes()方法,他可以代替indexOf(),下面来看看这个方法。


includes()


在ES6之前只有字符串的原型上含有include()方法来判断是否包含字串,而数组在ES6中也新增了include()方法来判断是否包含某个元素,下面来看看如何使用。


[1,2,3].includes(2) // true

数组实例直接调用,参数为要查找的目标元素,返回值为布尔值。而且他能很好地解决indexOf()的问题:


[NaN].includes(NaN) // true

如上includes()可以正确地判断NaN的查找问题,而includes()是用来判断是否包含,查找条件也比较单一,那么如果想要自定义查找条件,比如查找的范围,可以使用这么一对方法:find()与findIndex()接下来看一看他们是如何使用的。


find()与findIndex()


find()findIndex()可以匹配数组符合条件的元素


find()


find()支持三个参数,分别为valueindexarr,分别为当前值,当前位置,与原数组,,返回值为符号条件的值


let arr = [1,2,10,6,19,20]
arr.find((value,index,arr) => {
   return value > 10
}) // 19

如上,我以元素大于10为范围条件,返回了第一个符合范围条件的值:19。而find()可以返回符合条件的第一个元素,那么我们要是想拿到符合条件的第一个元素索引就可以使用findIndex()


findIndex()


findIndex()find相似也支持三个参数,但是返回值不同,其返回的是符合条件的索引


let arr = [1,2,10,6,19]
arr.findIndex((value,index,arr) => {
   return value > 10
}) // 4

例子与find()相同,返回的是19对应的索引


对于NaN值


find()findIndex()NaN值也不会误判,可以使用Object.is()来作为范围条件来判断NaN值,如下


[NaN].find((value)=> {
   return Object.is(NaN,value)
}) // NaN

如上例子,findIndex()也同理


最后


判断元素在某数组中是否存在的四种方法就说到这里,对掘友有所帮助的话就点个小心心吧,也欢迎关

作者:猪痞恶霸
来源:juejin.cn/post/7125632393821552677
注我的JS进阶专栏。

收起阅读 »

JS实现继承的几种方式

web
继承作为面向对象语言的三大特性之一,可以在不影响父类对象实现的情况下,使得子类对象具有父类对象的特性;同时还能再不影响父类对象行为的情况下扩展子类对象独有的特性,为编码带来了极大的便利。 下面我们就来看看 JavaScript 中都有哪些实现继承的方法。 原...
继续阅读 »

继承作为面向对象语言的三大特性之一,可以在不影响父类对象实现的情况下,使得子类对象具有父类对象的特性;同时还能再不影响父类对象行为的情况下扩展子类对象独有的特性,为编码带来了极大的便利。



下面我们就来看看 JavaScript 中都有哪些实现继承的方法。


原型链继承


原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。



原型链继承的主要思想是:重写子类的prototype属性,将其指向父类的实例



下面我们结合代码来了解一下。



function Animal (name) {

  // 属性

  this.name = name

  this.type = 'Animal'

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉');

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`);

}




// 子类

function Cat (name) {

  this.name = name

}

// 原型继承

Cat.prototype = new Animal()

// 将Cat的构造函数指向自身

Cat.prototype.constructor = Cat




let cat = new Cat('Tom')

console.log(cat.name) // Tom

console.log(cat.type) // Animal

cat.sleep() // Tom正在睡觉

cat.eat('猫罐头') // Tom正在吃猫罐头


在子类Cat中,我们没有增加type属性,因此会直接继承父类Animaltype属性。


在子类Cat中,我们增加了name属性,在生成子类实例时,name属性会覆盖父类Animal属性值。


同样因为Catprototype属性指向了Animal类型的实例,因此在生成实例Cat时,会继承实例函数和原型函数。



需要注意:
Cat.prototype.constructor = Cat


如果不将Cat原型对象的constructor属性指向自身的构造函数,那将指向父类Animal的构造函数。



原型链继承的优点


简单,易于实现


只需要设置子类的prototype属性指向父类的实例即可。


可通过子类直接访问父类原型链属性和函数


原型链继承的缺点


子类的所有实例将共享父类的属性


子类的所有实例将共享父类的属性会带来一个很严重的问题,父类包含引用值时,子类的实例改变该引用值会在所有实例中共享。



function Animal () {

  this.skill = ['eat', 'jump', 'sleep']

}

function Cat () {}

Cat.prototype = new Animal()

Cat.prototype.constructor = Cat




let cat1 = new Cat()

let cat2 = new Cat()

cat1.skill.push('walk')

console.log(cat1.skill) // ["eat", "jump", "sleep", "walk"]

console.log(cat2.skill) // ["eat", "jump", "sleep", "walk"]


在子类实例化时,无法向父类的构造函数传参


在通过new操作符创建子类的实例时,会调用子类的构造函数,而在子类的构造函数中并没有设置与父类关联,从而导致无法向父类的构造函数传递参数。


无法实现多继承


子类的prototype只能设置一个值,设置多个值时,后面的值会覆盖前面的值。


构造函数继承(借助 call)



构造函数继承的主要思想:在子类的构造函数中通过call()函数改变thi的指向,调用父类的构造函数,从而将父类的实例的属性和函数绑定到子类的this上。




  // 父类

function Animal (age) {

  // 属性

  this.name = 'Animal'

  this.age = age

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉');

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`);

}

function Cat (name) {

  // 核心,通过call()函数实现Animal的实例的属性和函数的继承

  Animal.call(this)

  this.name = name

}




let cat = new Cat('Tom')

cat.sleep() // Tom正在睡觉

cat.eat() // Uncaught TypeError: cat.eat is not a function


通过代码可以发现,子类可以正常调用父类的实例函数,而无法调用父类原型上的函数,这是因为子类并没有通过某种方式来调用父类原型对象上的函数


构造继承的优点


解决了子类实例共享父类属性的问题


call()函数实际时改变父类Animal构造函数中this的指向,然后调用this指向了子类Cat,相当于将父类的属性和函数直接绑定到了子类的this中,成了子类实例的熟属性和函数,因此生成的子类实例中是各自拥有自己的属性和函数,不会相互影响。


创建子类的实例时,可以向父类传参



   // 父类

function Animal (age) {

  this.name = 'Animal'

  this.age = age

}

function Cat (name, parentAge) {

  // 在子类生成实例时,传递参数给call()函数,间接地传递给父类,然后被子类继承

  Animal.call(this, parentAge)

  this.name = name

}




let cat = new Cat('Tom', 10)

console.log(cat.age)


可以实现多继承


在子类的构造函数中,可以多次调用call()函数来继承多个父对象。


构造函数的缺点


实例只是子类的实例,并不是父类的实例


因为我们并未通过原型对象将子类与父类进行串联,所以生成的实例与父类并没有关系。


只能继承父类实例的属性和函数,并不能继承原型对象上的属性和函数


与上面原因相同。


无法复用父类的构造函数


因为父类的实例函数将通过call()函数绑定到子类的this中,因此子类生成的每个实例都会拥有父类实例的引用,这会造成不必要的内存消耗,影响性能。


组合继承



组合继承的主要思想:结合构造继承和原型继承的两种方式,一方面在子类的构造函数中通过call()函数调用父类的构造函数,将父类的实例的属性和函数绑定到子类的this中;另一方面,通过改变子类的prototype属性,继承父类的原型对象上的属性和函数。




// 父类

function Animal (age) {

  // 实例属性

  this.name = 'Animal'

  this.age = age

  this.skill = ['eat', 'jump', 'sleep']

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉')

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`)

}




// 子类

function Cat (name) {

  // 通过构造函数继承实例的属性和函数

  Animal.call(this)

  this.name = name

}

// 通过原型继承原型对象上的属性和函数

Cat.prototype = new Animal()

Cat.prototype.constructor = Cat




let cat = new Cat('Tom')

console.log(cat.name) // Tom

cat.sleep() // Tom正在睡觉

cat.eat('猫罐头') // Tom正在吃猫罐头


组合继承的优点


既能继承父类实例的属性和函数,又能继承原型对象上的属性和函数


既是子类的实例,又是父类的实例


不存在引用属性共享的问题


构造函数作用域优先级比原型链优先级高,所以不会出现引用属性共享的问题。


可以向父类的构造函数中传参


组合继承的缺点


父类的实例属性会被绑定两次


在子类的构造函数中,通过call()函数调用了一次父类的构造函数;在改写子类的prototype属性,生成的实例时又调用了一次父类的构造函数。


寄生组合继承


组合继承方案已经足够好,但是针对其存在的缺点,我们仍然可以进行优化。


在进行子类的prototype属性的设置时,可以去掉父类实例的属性的函数



  //父类

function Animal (age) {

  // 实例属性

  this.name = 'Animal'

  this.age = age

  this.skill = ['eat', 'jump', 'sleep']

  // 实例函数

  this.sleep = function () {

    console.log(this.name + '正在睡觉')

  }

}

// 原型函数

Animal.prototype.eat = function (food) {

  console.log(`${this.name}正在吃${food}`)

}

// 子类

function Cat (name) {

  // 继承父类的实例和属性

  Animal.call(this)

  this.name = name

}

// 继承父类原型上的实例和属性

Cat.prototype = Object.create(Animal.prototype)

Cat.prototype.constructor = Cat

let cat = new Cat('Tom')




console.log(cat.name) // Tom

cat.sleep() // Tom正在睡觉

cat.eat('猫罐头') // Tom正在吃猫罐头



其中最关键的语句:






Cat.prototype = Object.create(Animal.prototype)






只取父类Animal的prototype属性,过滤掉Animal的实例属性,从而避免了父类的实例属性绑定两次。



这种寄生组合式继承方式,基本可以解决前几种继承方式的缺点,较好地实现了继承想要的结果,同时也减少了构造次数,减少了性能的开销。


整体看下来,这六种继承方式中,寄生组合式继承是这里面最优的继承方式。


总结


image.png


作者:蜡笔小群
来源:juejin.cn/post/7168856064581091364
收起阅读 »