注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android 13这些权限废弃,你的应用受影响了吗?

无论是更改个人头像、分享照片、还是在电子邮件中添加附件,选择和分享媒体文件是用户最常见的操作之一。在听取了 Android 用户反馈之后,我们对应用程序访问媒体文件的方式做了一些改变。 Android 13 已被废弃的权限 许多用户告诉我们,文件和媒体权限让他...
继续阅读 »

无论是更改个人头像、分享照片、还是在电子邮件中添加附件,选择和分享媒体文件是用户最常见的操作之一。在听取了 Android 用户反馈之后,我们对应用程序访问媒体文件的方式做了一些改变。


Android 13 已被废弃的权限


许多用户告诉我们,文件和媒体权限让他们很困扰,因为他们不知道应用程序想要访问哪些文件。


在 Android 13 上废弃了 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 权限,用更好的文件访问方式代替这些废弃的 API。


从 Android 10 开始向共享存储中添加文件不需要任何权限。因此,如果你的 App 只在共享存储中添加文件,你可以停止在 Android 10+ 上申请任何权限。


在之前的系统版本中 App 需要申请 READ_EXTERNAL_STORAGE 权限访问设备的文件和媒体,然后选择自己的媒体选择器,这为开发者增加了开发和维护成本,另外 App 依赖于通过 ACTION_GET_CONTENT 或者 ACTION_OPEN_CONTENT 的系统文件选择器,但是我们从开发者那里了解到,它感觉没有很好地集成到他们的 App 中。


System File Picker using ACTION_OPEN_CONTENT


图片选择器


在 Android 13 中,我们引入了一个新的媒体工具 Android 照片选择器。该工具为用户提供了一种选择媒体文件的方法,而不需要授予对其整个媒体库的访问权限。


它提供了一个简洁界面,展示照片和视频,按照日期排序。另外在 "Albums" 页面,用户可以按照屏幕截图或下载等等分类浏览,通过指定一些用户是否仅看到照片或视频,也可以设置选择最大文件数量,也可以根据自己的需求定制照片选择器。简而言之,这个照片选择器是为私人设计的,具有干净和简洁的 UI 易于实现。



我们还通过谷歌 Play 系统更新 (2022 年 5 月 1 日发布),将照片选择器反向移植到 Android 11 和 12 上,以将其带给更多的 Android 用户。


开发一个照片选择器是一个复杂的项目,新的照片选择器不需要团队进行任何维护。我们已经在 ActivityX 1.6.0 版本中为它创建了一个 ActivityResultContract。如果照片选择器在你的系统上可用,将会优先使用照片选择器。


// Registering Photo Picker activity launcher with a max limit of 5 items
val pickMultipleVisualMedia = registerForActivityResult(PickMultipleVisualMedia(5)) { uris ->
// TODO: process URIs
}
// Launching the photo picker (photos & video included)
pickMultipleVisualMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageAndVideo))

如果希望添加类型进行筛选,可以采用这种方式。


// Launching the photo picker (photos only)
pickMultipleVisualMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))
// Launching the photo picker (video only)
pickMultipleVisualMedia.launch(PickVisualMediaRequest(PickVisualMedia.VideoOnly))
// Launching the photo picker (GIF only)
pickMultipleVisualMedia.launch(PickVisualMediaRequest(PickVisualMedia.SingleMimeType("image/gif")))

可以调用 isPhotoPickerAvailable 方法来验证在当前设备上照片选择器是否可用。


ACTION_GET_CONTENT 将会发生改变


正如你所见,使用新的照片选择器只需要几行代码。虽然我们希望所有的 Apps 都使用它,但在 App 中迁移可能需要一些时间。


这就是为什么我们使用 ACTION_GET_CONTENT 将系统文件选择器转换为照片选择器,而不需要进行任何代码更改,从而将新的照片选择器引入到现有的 App 中。



针对特定场景的新权限


虽然我们强烈建议您使用新的照片选择器,而不是访问所有媒体文件,但是您的 App 可能有一个场景,需要访问所有媒体文件(例如图库照片备份)。对于这些特定的场景,我们将引入新的权限,以提供对特定类型的媒体文件的访问,包括图像、视频或音频。您可以在文档中阅读更多关于它们的内容。


如果用户之前授予你的应用程序 READ_EXTERNAL_STORAGE 权限,系统会自动授予你的 App 访问权限。否则,当你的 App 请求任何新的权限时,系统会显示一个面向用户的对话框。


所以您必须始终检查是否仍然授予了权限,而不是存储它们的授予状态。



下面的决策树可以帮助您更好的浏览这些更改。



我们承诺在保护用户隐私的同时,继续改进照片选择器和整体存储开发者体验,以创建一个安全透明的 Android 生态系统。


新的照片选择器被反向移植到所有 Android 11 和 12 设备,不包括 Android Go 和非 gms 设备。



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

一些有用的技巧帮助你开发 flutter

前言 你好今天给你带来了些有用的建议,让我们开始吧。 正文 1. ElevatedButton.styleFrom 快速样式 你是否厌倦了 container 里那些乏味的 decorations ,想要轻松实现这个美丽的按钮别担心,我给你准备了一些魔法密码...
继续阅读 »

前言


你好今天给你带来了些有用的建议,让我们开始吧。



正文


1. ElevatedButton.styleFrom 快速样式


你是否厌倦了 container 里那些乏味的 decorations ,想要轻松实现这个美丽的按钮别担心,我给你准备了一些魔法密码。



示例代码


SizedBox(
height: 45,
width: 200,
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
elevation: 10, shape: const StadiumBorder()),
child: const Center(child: Text('Elevated Button')),
),
),


示例代码


SizedBox(
height: 45,
width: 60,
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
elevation: 10, shape: const CircleBorder()),
child: const Center(child: Icon(Icons.add)),
),
),

2. TextInputAction.next 焦点切换


你们都知道“焦点节点”。这基本上是用来识别 Flutter 的“焦点树”中特定的 TextField 这允许您在接下来的步骤中将焦点放在 TextField 上。但你知道 Flutter 提供了一个神奇的一行代码同样..。


示例代码


Padding(
padding: const EdgeInsets.all(30.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
textInputAction: TextInputAction.next,
),
const SizedBox(
height: 50,
),
TextFormField(
textInputAction: TextInputAction.next,
),
const SizedBox(
height: 50,
),
TextFormField(
textInputAction: TextInputAction.done,
),
const SizedBox(
height: 50,
),
],
),
);

3. 设置 status Bar 状态栏颜色


你的状态栏颜色破坏了你的页面外观吗? 让我们改变它..。



示例代码


void main() {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent, // transparent status bar
));
runApp(const MyApp());
}

4.设置 TextStyle.height 段落间距


如果您在页面上显示了一个段落(例如: 产品描述、关于我们的内容等) ,并且它看起来不如 xd 设计那么好!使用这个神奇的代码,使它有吸引力和顺利。



示例代码


 Text(
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.',
style: TextStyle(
fontSize: 17.0,
height: 1.8,
),
)

5. 设置文字 3D


想让你的“标题”文字更有吸引力吗? 给它一个有阴影的 3D 效果..。



示例代码


Center(
child: Text(
'Hello, world!',
style: TextStyle(
fontSize: 50,
color: Colors.pink,
fontWeight: FontWeight.w900,
shadows: <Shadow>[
const Shadow(
offset: Offset(4.0, 4.0),
blurRadius: 3.0,
color: Color.fromARGB(99, 64, 64, 64),
),
Shadow(
offset: const Offset(1.0, 1.0),
blurRadius: 8.0,
color: Colors.grey.shade100),
],
),
),
)

6. vscode 插件 Pubspec Assist


你知道 Flutter extensions 吗?你当然是! !我正在分享我最喜欢的 Flutter extensions..。



Pubspec Assist 是一个 VisualStudio 代码扩展,它允许您轻松地向 Dart 和 Flutter 项目的 Pubspec 添加依赖项。Yaml 不需要你编辑。你必须试试。


7. 应用 app 尺寸控制


Application 大小很重要!应用程序大小的 Flutter 应用程序是非常重要的。当它是一个更大的应用程序时,尺寸变得更加重要,因为你需要在设备上有更多的空间。更大的应用程序下载时间也更长。它扩大了 Flutter 应用程序,可以是两个、三个或更多的安装尺寸。因此,在 Android 平台上减小 Flutter 应用程序的大小是非常重要的。


这里有一些减小 Flutter 应用程序大小的技巧 ~ ~ ~



  1. 减小应用程序元素的大小

  2. 压缩所有 JPEG 和 PNG 文件

  3. 使用谷歌字体

  4. 在 Android Studio 中使用 Analyzer

  5. 使用命令行中的分析 Analyzer

  6. 减少资源数量和规模

  7. 使用特定的 Libraries


谢谢你的阅读...


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

哈啰 Quark Design 正式开源,下一代跨技术栈前端组件库

官网:quark-design.hellobike.comQuark(夸克) Design 是由哈啰平台 UED 和增长&电商前端团队联合打造的一套面向移动端的跨框架 UI 组件库。与业界第三方组件库不一样,Quark Design 底层基于 Web ...
继续阅读 »

Quark Design 是什么?

官网:quark-design.hellobike.com

github:github.com/hellof2e/qu…

Quark(夸克) Design 是由哈啰平台 UED 和增长&电商前端团队联合打造的一套面向移动端的跨框架 UI 组件库。与业界第三方组件库不一样,Quark Design 底层基于 Web Components 实现,它能做到一套代码,同时运行在各类前端框架中。

Quark Design 历经一年多的开发时间,已在集团内部大量业务中得到验证,本着“共创、共建、共享”的开源精神,我们于即日起将 Quark 正式对外开源!Github地址:github.com/hellof2e/qu… (求star、求关注~😁)


注:文档表现/样式参考了HeadlessUI/nutui/vant等。

Quark Design 与现有主流组件库的区别是什么?

Quark(夸克)有别于业界主流的移动端组件库,Quark 能同时运行在业界所有前端框架/无框架工程中,做到真正的技术栈无关 !我们不一样,:)

  • 不依赖技术栈(eg. Vue、React、Angular等)

  • 不依赖技术栈版本(eg. Vue2.x、Vue3.x)

  • 全新的Api设计(eg. 弹窗的打开属性由传统的 Visible 调整为符合浏览器原生弹窗的 open等)

  • 公司前端技术生态项目技术栈多时,保持视觉/交互统一

  • 完全覆盖您所需要的各类通用组件

  • 支持按需引用

  • 详尽的文档和示例

  • 支持定制主题

性能优势-优先逻辑无阻塞

我们以对 React 组件的 Web Components 化为例,一个普通的 React 组件在初次执行时需要一次性走完所有必须的节点逻辑,而这些逻辑的执行都同步占用在 js 的主线程上,那么当你的页面足够复杂时,一些非核心逻辑就将会阻塞后面的核心逻辑的执行。

比如首次加载时,你的页面中有一个复杂的交互组件,交互组件中又包含 N多逻辑和按钮等小组件,此时页面的首次加载不应该优先去执行这些细节逻辑,而首要任务应当是优先渲染出整体框架或核心要素,而后再次去完善那些不必要第一时间完成的细节功能。 例如一些图像处理非常复杂,但你完全没必要在第一时间就去加载它们。

当我们使用 Web Components 来优化 React的时候,这个执行过程将会变得简洁的多,比如我们注册了一个复杂的逻辑组件,在 React 执行时只是执行了一个 createElement 语句,创建它只需要 1-2 微秒即可完成,而真正的逻辑并不在同时执行,而是等到“核心任务”执行完再去执行,甚至你可以允许它在合适的时机再去执行。

我们也可以简单的理解为,部分逻辑在之后进行执行然后被 render 到指定 id 的 Div 中的,那么为什么传统的组件为什么不能这么做呢?而非得 Web Components 呢?那就不得不提到它所包含的另一个技术特性:Shadow DOM


组件隔离(Shadow Dom)

Shadow DOM 为自定义的组件提供了包括 CSS、事件的有效隔离,不再担心不同的组件之间的样式、事件污染了。 这相当于为自定义组件提供了一个天然有效的保护伞。

Shadow DOM 实际上是一个独立的子 DOM Tree,通过有限的接口和外部发生作用。 我们都知道页面中的 DOM 节点数越多,运行时性能将会越差,这是因为 DOM 节点的相互作用会时常在触发重绘(Repaint)和重排(reflow)时会关联计算大量 Frame 关系。


而对 CSS 的隔离也将加快选择器的匹配速度,即便可能是微秒级的提升,但是在极端的性能情况下,依然是有效的手段。

Quark 能为你带来什么?

提效降本几乎是所有企业的主旋律,Quark 本身除了提供了通用组件之外,我们还为大家提供了开箱即用的 CLI,可以让大家在直接在日常开发中开发横跨多个技术栈/框架的业务组件。比如一个相同样式的营销弹窗,可以做到:

  • 同时运行在不同技术栈(Angular、Vue、React等)的前端工程中

  • 同时运行在不同版本的技术栈中,比如能同时运行在 Vue2.x、Vue3.x 中

CLI 内部 Beta 版本目前初版已完成,github 地址:github.com/hellof2e/qu…

适合场景:前端团队想发布一个独立的组件或npm包,让其他各类技术栈的工程使用,从而达到提效降本的目的。

npm i -g @quarkd/quark-cli
npx create-quark


相关链接

作者:Allan91
来源:juejin.cn/post/7160483409691672606

收起阅读 »

一个瞬间让你的代码量暴增的脚本

web
在某些特殊情况下,需要凑齐一定的代码量,或者一定的提交次数,为了应急不得不采用一些非常规的手段来保证达标。本文分享的是一段自动提交代码的脚本,用于凑齐code review流程数量,将单次code review代码修改行数拉下来(备注:如果git开启自动生成c...
继续阅读 »

1 功能概述

在某些特殊情况下,需要凑齐一定的代码量,或者一定的提交次数,为了应急不得不采用一些非常规的手段来保证达标。本文分享的是一段自动提交代码的脚本,用于凑齐code review流程数量,将单次code review代码修改行数拉下来(备注:如果git开启自动生成code review流程,则每次push操作就会自动生成一次code review流程)。

2 友情提示

本脚本仅用于特殊应急场景下,平时开发中还是老老实实敲代码。

重要的事情说三遍:

千万不要在工作中使用、千万不要在工作中使用、千万不要在工作中使用

3 实现思路

3.1 准备示例代码

可以多准备一些样例代码,然后随机取用, 效果会更好。例如:

需要确保示例代码是有效的代码,有些项目可能有eslint检查,如果格式不对可能导致无法自动提交

function huisu(value, index, len, arr, current) {
 if (index >= len) {
     if (value === 8) {
         console.log('suu', current)
    }
     console.log('suu', current)
     return
}
 for (let i = index; i < len; i++) {
     current.push(arr[i])
     console.log('suu', current)
     if (value + arr[i] === 8) {
         console.log('结果', current)
         return
    }
     huisu(value + arr[i], i + 1, len, arr, [...current])
     console.log('suu', value)
     current.pop()
     onsole.log('suu', current)
}
}

3.2、准备一堆文件名

准备一堆文件名,用于生成新的问题,如果想偷懒,直接随机生成问题也不大。例如:

// 实现准备好的文件名称,随机也可以
const JS_NAMES = ['index.js', 'main.js', 'code.js', 'app.js', 'visitor.js', 'detail.js', 'warning.js', 'product.js', 'comment.js', 'awenk.js', 'test.js'];

3.3 生成待提交的文件

这一步策略也很简单,就是根据指定代码输出文件夹内已有的文件数量,来决定是要执行新增文件还是删除文件

if (codeFiles.length > MIN_COUNT) {
 rmFile(codeFiles);
} else {
 createFile(codeDir);
}

【新增文件】

根据前面两步准备的示例代码和文件命名,随机获取文件名和代码段,然后创建新文件

// 创建新的代码文件
function createFile(codeDir) {
 const ran = Math.floor(Math.random() * JS_NAMES.length);
 const name = JS_NAMES[ran];
 const filePath = `${codeDir}/${name}`;
 const content = getCode();
 writeFile(filePath, content);
}

【删除文件】

这一步比较简单,直接随机删除一个就行了

// 随机删除一个文件
function rmFile(codeFiles) {
 const ran = Math.floor(Math.random() * codeFiles.length);
 const filePath = codeFiles[ran];
 try {
   if (fs.existsSync(filePath)) {
     fs.unlinkSync(filePath);
  }
} catch (e) {
   console.error('removeFile', e);
}
}

3.4 准备commit信息

这一步怎么简单怎么来,直接准备一堆,然后随机取一个就可以了

const msgs = ['feat:消息处理', 'feat:详情修改', 'fix: 交互优化', 'feat:新增渠道', 'config修改'];
const ran = Math.floor(Math.random() * msgs.length);
console.log(`${msgs[ran]}--测试提交,请直接通过`);

3.5 扩大增幅

上述步骤执行一次可能不太够,咱们可以循环多来几次。随机生成一个数字,用来控制循环的次数

const ran = Math.max(3, parseInt(Math.random() * 10, 10));
console.log(ran);

3.6 组合脚本

组合上述步骤,利用shell脚本执行git提交,详细代码如下:

#! /bin/bash

git pull

cd $(dirname $0)

# 执行次数
count=$(node ./commit/ran.js)
echo $count

# 循环执行
for((i=0;i<$count;i++))
do
node ./commit/code.js
git add .

msg=$(node ./commit/msg.js)
git commit -m "$msg"

git push
done

总结

总的来就就是利用shell脚本执行git命令,随机生成代码或者删除代码之后执行commit提交,最后push推送到远程服务器。

源码

欢迎有需要的朋友取用,《源码传送门》

作者:先秦剑仙
来源:juejin.cn/post/7160649931928109092

收起阅读 »

程序员转行做运营,降薪降得心甘情愿

自2019年末新冠疫情爆发以来,近三年的就业形势一直不太乐观,大厂裁员的消息接踵而至。身边的朋友都在感慨:现阶段能保住工作就不错了,新工作就算了。但,就是在这样严峻的大环境下,我的前同事不三不仅跳槽还转岗,1年的转行之路,经受了各种磨难。通过小摹的热情邀请,和...
继续阅读 »

自2019年末新冠疫情爆发以来,近三年的就业形势一直不太乐观,大厂裁员的消息接踵而至。身边的朋友都在感慨:现阶段能保住工作就不错了,新工作就算了。

但,就是在这样严峻的大环境下,我的前同事不三不仅跳槽还转岗,1年的转行之路,经受了各种磨难。通过小摹的热情邀请,和不三聊了聊程序员转运营过程中的经验与心得。

小摹把这份干货分享出来,希望能为每一位即将转行的伙伴提供动力支撑,也能给其他岗位的朋友新增一些不同视角的思考。

试用期差点被劝退

小摹:从事前端四年,是什么让你下定决心转行?

不三:后续有创业的打算,所以希望自己在了解产品研发的基础上,也多了解一下市场,为自己创业做准备吧。

小摹:你做的是哪方面的运营呢?这一年的感触如何?

不三:运营岗位细分很多:新媒体运营、产品运营、用户运营、活动运营、市场推广等,我所从事的是内容运营和用户运营。

公司是SaaS通信云服务提供商,对于之前从未接触过这方面工作的我而言,门槛比较高。为了能尽快熟悉产品业务,也能让我更了解用户,为后续用户运营和内容运营打基础,领导安排我前期先接触和客户相关的工作。

我试用期大部分的工作都涉及到和用户打交道,他们总会反馈给我们各种产品的需求和bug,我基本都冲在第一线安抚用户。Bug提交给开发后或许还能尽快修复,而需求反馈过去后,只能等到那句再熟悉不过的话“等排期吧”。


刚做运营的前三个月,提给开发的需求大多都被驳回了,要么做出来的东西无法达到预期。那段时间,每天上班心态濒临崩溃,颇有打道回府之意。

转正之前,领导找我谈了一次话,让我醍醐灌顶:

运营身为提需求大户,你连需求都没规划好,想一出是一出,产品开发为啥会帮你做?

你之前是前端,设身处地的想,是不是非常反感产品或运营给你提莫名其妙的需求?不注重用户体验、忽略了产品的长远发展,即便当下你的KPI完成了,你有获得真正的成长,产品有迭代得更好吗?

在和领导沟通的过程中慢慢意识到,我把自己的位置摆错了,即使运营是结果驱动,但我直面用户,所以我必须要学会洞察用户的心理,重视产品的长远发展,这样才能让我有所进度。

跟领导聊完之后,我便开始调整了工作状态和节奏,明白了自己的不足,接下来就是有目标、有计划的解决问题。

回到岗位后,我梳理了公司的业务方向,写好MRD(市场需求报告),重新制定了我的运营策略,提交给了领导。

三天后,人事找到我:我通过了试用期,成功转正了。


我很感谢我的领导,尽管试用期我做得很烂,但他仍然愿意给我机会,让我转正,继续工作。现在回过头看这一年,试用期阶段很痛苦,找不到工作的方向,但后来越来越熟悉了解后,也能更快上手了。

小摹:你认为一名优秀的运营要具备什么样的特质?

不三:现在的我只能说刚刚入门,我发现身边的运营大佬身上有以下特点,我希望自己能尽快向他靠拢。

  • 用户体感:所有的产品研发出来后,面向对象一定是用户,那么产品的使用体验、页面设计、活动机制、规则设定是否都能满足用户的胃口。

如果只是冲着所谓的KPI目标,而忽略了用户体验,或许你会收获万人骂的情况。

例如,随时随地朋友圈砍一刀的拼夕夕。

  • 把控热点能力:无论做什么方向的运营,都逃不了蹭热点,你可以说蹭热点low,但不可否认它会给自己和产品带来新机遇。

例如,写一篇文章蹭了热点之后,爆的几率更大;疫情刚出现时,异地办公、社区团购也随之应运而生。

  • 产品思维:互联网运营和产品经理的联系是非常紧密的,所以在推广的过程中,需要和产品部门多多碰撞。这样不仅能收获创意灵感,还能学到不少的产品思维。

在需求迭代时,应该站在更高的层次思考问题,一味给产品做加法,根本行不通。

  • 数据思维:运营以结果为导向,从数据中发现不足,从数据中发现增长点,弥补缺陷,让增长幅度更大。程序员比较有优势,可以写SQL导数据,但拿到数据只是第一步,还要懂得分析才行。

  • 抵御公关风险:例如我们在做活动时,我们要提前考虑活动的风险有哪些,如何积极应对,当有别有用心的人利用规则薅羊毛时,也应该有相应的解决方案。


这段简单且干货的采访随着烧烤啤酒的上桌步入了尾声。最后不三给我说到:

一年前我调整了自己的职业方向,从前端步入运营,苦涩欢笑并存,有时看着达到目标很是激动,有时苦于KPI的折磨。一年间,我经历了人生的成长,思想也更加成熟。但我还没有达到最终目的地,现在的一切只是为了以后的创业蓄力。我不想一辈子为别人打工,也想为自己活一次。


===

后记

小摹见过太多转行失败的案例,所以很为不三感到高兴,不仅仅是为他的转行成功,更多的是他坚定人生的方向,并为之做出了各种努力而高兴。

给大家分享这段采访经历,是希望大家能尽早对自己的职业生涯有所规划,有了目标后,再细分到某一阶段,这样工作起来积极性也会更高。停止摆烂,对自己负责!

人生之难,一山又一山,愿你我共赴远山。

作者:摹客
来源:juejin.cn/post/7158734145575714853

收起阅读 »

入坑两个月自研创业公司

一、拿 offer 其实入职前,我就感觉到有点不对劲,居然要自带电脑。而且人事是周六打电话发的 offer!自己多年的工作经验,讲道理不应该入这种坑,还是因为手里没粮心中慌,工作时间长的社会人,还是不要脱产考研、考公,疫情期间更是如此,本来预定 2 月公务员面...
继续阅读 »

一、拿 offer


其实入职前,我就感觉到有点不对劲,居然要自带电脑。而且人事是周六打电话发的 offer!自己多年的工作经验,讲道理不应该入这种坑,还是因为手里没粮心中慌,工作时间长的社会人,还是不要脱产考研、考公,疫情期间更是如此,本来预定 2 月公务员面试,结果一直拖到 7 月。


二、入职工作


刚入职工作时,一是有些抗拒,二呢是有些欣喜。抗拒是因为长时间呆家的惯性,以及人的惰性,我这只是呆家五个月,那些呆家一年两年的,再进入社会,真的很难,首先心理上他们就要克服自己的惰性和惯性,平时生活习惯也要发生改变


三、人言可畏


刚入职工作时,有工作几个月的老员工和我说,前公司的种种恶心人的操作,后面呢我也确实见识到了:无故扣绩效,让员工重新签署劳动协议,但是,也有很多不符实的,比如公司在搞幺蛾子的时候,居然传出来我被劝退了……


四、为什么离开


最主要的原因肯定还是因为发不出工资,打工是为了赚钱,你想白嫖我?现在公司规模也不算小了,想要缓过来,很难。即便缓过来,以后就不会出现这样的状况了?公司之前也出现过类似的状况,挺过来的老员工们我也没看到有什么优待,所以这家公司不值得我去熬。技术方面我也基本掌握了微信和支付宝小程序开发,后面不过是需求迭代。个人成长方面,虽然我现在是前端部门经理,但前端组跑的最快,可以预料后面我将面临无人可用的局面,我离职的第二天,又一名前端离职了,约等于光杆司令,没意义。


五、收获


1. 不要脱产,不要脱产
2. 使用 uniapp 进行微信和支付宝小程序开发
3. 工作离家近真的很爽
4. 作为技术人员,只要你的上司技术还行,你的工期他是能正常估算,有什么难点说出来,只要不是借口,他也能理解,同时,是借口他也能一下识别出来,比如,一个前端和我说:“后端需求不停调整,所以没做好。” 问他具体哪些调整要两个星期?他又说不出来。这个借口就不要用了,但是我也要走了,我也没必要去得罪他。
5. 进公司前,搞清楚公司目前是盈利还是靠融资活,靠融资活的创业公司有风险…


六、未来规划


关于下一份工作:
南京真是外包之城,找了两周只有外包能满足我目前 18k 的薪资,还有一家还降价了 500…
目前 offer 有
vivo 外包,20k
美的外包,17.5k
自研中小企业,18.5k


虽然美的外包薪资最低,但我可能还是偏向于美的外包。原因有以下几点:
1. 全球手机出货量下降,南京的华为外包被裁了不少,很难说以后 vivo 会不会也裁。
2. 美的目前是中国家电行业的龙头老大,遥遥领先第二名,目前在大力发展 b2c 业务,我进去做的也是和商场相关。
3. 美的的办公地点离我家更近些
4. 自研中小企业有上网限制,有过类似经验的开发人,懂得都懂,很难受。


关于考公:
每年 10 月到 12 月准备下,能进就进,不能再在考公上花费太多时间了。


作者:哇哦谢谢你
链接:https://juejin.cn/post/7160138475688165389

收起阅读 »

Android性能优化 -- 内存优化

内存,是Android应用的生命线,一旦在内存上出现问题,轻者内存泄漏,重者直接crash,因此一个应用保持健壮,内存这块的工作是持久战,而且从写代码这块就需要注意合理性,所以想要了解内存优化如何去做,要先从基础知识开始。 1 JVM内存原理 这一部分确实很枯...
继续阅读 »

内存,是Android应用的生命线,一旦在内存上出现问题,轻者内存泄漏,重者直接crash,因此一个应用保持健壮,内存这块的工作是持久战,而且从写代码这块就需要注意合理性,所以想要了解内存优化如何去做,要先从基础知识开始。


1 JVM内存原理


这一部分确实很枯燥,但是对于我们理解内存模型非常重要,这一块也是面试的常客


image.png


从上图中,我将JVM的内存模块分成了左右两大部分,左边属于共享区域(方法区、堆区),所有的线程都能够访问,但也会带来同步问题,这里就不细说了;右边属于私有区域,每个线程都有自己独立的区域。


1.1 方法执行流程


class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
execute()
}

private fun execute(){

val a = 2.5f
val b = 2.5f
val c = a + b

val method = Method()

val d = getD()
}

private fun getD(): Int {
return 0
}

}

class Method{
private var a:Int = 0
}

我们看到在MainActivity的onCreate方法中,执行了execute方法,因为当前是UI线程,每个线程都有一个Java虚拟机栈,从上图中可以看到,那么每执行一个方法,在Java虚拟机栈中都对应一个栈帧。


image.png


每次调用一个方法,都代表一个栈帧入栈,当onCreate方法执行完成之后,会执行execute方法,那么我们看下execute方法。


execute方法在Java虚拟机栈中代表一个栈帧,栈帧是由四部分组成:


(1)局部变量表:局部变量是声明在方法体内的,例如a,b,c,在方法执行完成之后,也会被回收;
(2)操作数栈:在任意方法中,涉及到变量之间运算等操作都是在操作数栈中进行;例如execute方法中:


val a = 2.5f

当执行这句代码时,首先会将 2.5f压入操作数栈,然后给a赋值,依次类推

(3)返回地址:例如在execute调用了getD方法,那么这个方法在执行到到return的时候就结束了,当一个方法结束之后,就要返回到该方法的被调用处,那么该方法就携带一个返回地址,告诉JVM给谁赋值,然后通过操作数栈给d赋值

(4)动态链接:在execute方法中,实例化了Method类,在这里,首先会给Method中的一些静态变量或者方法进行内存分配,这个过程可以理解为动态链接。


1.2 从单例模式了解对象生命周期


单例模式,可能是众多设计模式中,我们使用最频繁的一个,但是单例真是就这么简单吗,使用不慎就会造成内存泄漏!


interface IObserver {

fun send(msg:String)

}

class Observable : IObserver {

private val observers: MutableList<IObserver> by lazy {
mutableListOf()
}

fun register(observer: IObserver) {
observers.add(observer)
}

fun unregister(observer: IObserver) {
observers.remove(observer)
}

override fun send(msg: String) {
observers.forEach {
it.send(msg)
}
}

companion object {
val instance: Observable by lazy {
Observable()
}
}
}

这里是写了一个观察者,这个被观察者是一个单例,instance是存放在方法区中,而创建的Observable对象则是存在堆区,看下图


image.png


因为方法区属于常驻内存,那么其中的instance引用会一直跟堆区的Observable连接,导致这个单例对象会存在很长的时间


btnRegister.setOnClickListener {
Observable.instance.register(this)
}
btnSend.setOnClickListener {
Observable.instance.send("发送消息")
}

在MainActivity中,点击注册按钮,注意这里传入的值,是当前Activity,那么这个时候退出,会发生什么?我们先从profile工具里看一下,退出之后,有2个内存泄漏的地方,如果使用的leakcannary(后面会介绍)就应该会明白


image.png


那么在MainActivity中,哪个地方发生的了内存泄漏呢?我们紧跟一下看看GcRoot的引用,发现有这样一条引用链,MainActivity在一个list数组中,而且这个数组是Observable中的observers,而且是被instance持有,前面我们说到,instance的生命周期很长,所以当Activity准备被销毁时,发现被instance持有导致回收失败,发生了内存泄漏。


image.png


那么这种情况,我们该怎么处理呢?一般来说,有注册就有解注册,所以我们在封装的时候一定要注意单例中传入的参数


override fun onDestroy() {
super.onDestroy()
Observable.instance.unregister(this)
}

再次运行我们发现,已经不存在内存泄漏了


image.png


1.3 GcRoot


前面我们提到了,因为instance是Gcroot,导致其引用了observers,observers引用了MainActivity,MainActivity退出的时候没有被回收,那么什么样的对象能被看做是GcRoot呢?


(1)静态变量、常量:例如instance,其内存是在方法区的,在方法区一般存储的都是静态的常量或者变量,其生命周期非常长;

(2)局部变量表:在Java虚拟机栈的栈帧中,存在局部变量表,为什么局部变量表能作为gcroot,原因很简单,我们看下面这个方法


private fun execute() {

val a = 2.5f
val method = Method()
val d = getD()
}

a变量就是一个局部变量表中的成员,我们想一下,如果a不是gcroot,那么垃圾回收时就有可能被回收,那么这个方法还有什么意义呢?所以当这个方法执行完成之后,gcroot被回收,其引用也会被回收。


2 OOM


在之前我们简单介绍了内存泄漏的场景,那么内存泄漏一旦发生,就会导致OOM吗?其实并不是,内存泄漏一开始并不会导致OOM,而是逐渐累计的,当内存空间不足时,会造成卡顿、耗电等不良体验,最终就会导致OOM,app崩溃


那么什么情况下会导致OOM呢?

(1)Java堆内存不足

(2)没有连续的内存空间

(3)线程数超出限制


其实以上3种状况,前两种都有可能是内存泄漏导致的,所以如何避免内存泄漏,是我们内存优化的重点


2.1 leakcanary使用


首先在module中引入leakcanary的依赖,关于leakcanary的原理,之后会单独写一篇博客介绍,这里我们的主要工作是分析内存泄漏


debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'

配置依赖之后,重新运行项目,会看到一个leaks app,这个app就是用来监控内存泄漏的工具


image.png
那我们执行之前的应用,打开leaks看一下gcroot的引用,是不是跟我们在as的profiler中看到的是一样的


image.png


如果使用过leakcanary的伙伴们应该知道,leakcanary会生成一个hprof文件,那么通过MAT工具,可以分析这个hprof文件,查找内存泄漏的位置,下面的链接能够下载MAT工具
http://www.eclipse.org/mat/downloa…


2.2 内存泄漏的场景


1. 资源性的对象没有关闭


例如,我们在做一个相机模块,通过camera拿到了一帧图片,通常我们会将其转换为bitmap,在使用完成之后,如果没有将其回收,那么就会造成内存泄漏,具体使用完该怎么办呢?


if(bitmap != null){
bitmap?.recycle()
bitmap = null
}

调用bitmap的recycle方法,然后将bitmap置为null


2. 注册的对象没有注销


这种场景其实我们已经很常见了,在之前也提到过,就是注册跟反注册要成对出现,例如我们在注册广播接收器的时候,一定要记得,在Activity销毁的时候去解注册,具体使用方式就不做过多的赘述。


3. 类的静态变量持有大数据量对象


因为我们知道,类的静态变量是存储在方法区的,方法区空间有限而且生命周期长,如果持有大数据量对象,那么很难被gc回收,如果再次向方法区分配内存,会导致没有足够的空间分配,从而导致OOM


4. 单例造成的内存泄漏


这个我们在前面已经有一个详细的介绍,因为我们在使用单例的时候,经常会传入context或者activity对象,因为有上下文的存在,导致单例持有不能被销毁;


因此在传入context的时候,可以传入Application的context,那么单例就不会持有activity的上下文可以正常被回收;


如果不能传入Application的context,那么可以通过弱引用包装context,使用的时候从弱引用中取出,但这样会存在风险,因为弱引用可能随时被系统回收,如果在某个时刻必须要使用context,可能会带来额外的问题,因此根据不同的场景谨慎使用。


object ToastUtils {

private var context:Context? = null

fun setText(context: Context) {
this.context = context
Toast.makeText(context, "1111", Toast.LENGTH_SHORT).show()
}

}

我们看下上面的代码,ToastUtils是一个单例,我们在外边写了一个context:Context? 的引用,这种写法是非常危险的,因为ToastUtils会持有context的引用导致内存泄漏


object ToastUtils {

fun setText(context: Context) {
Toast.makeText(context, "1111", Toast.LENGTH_SHORT).show()
}

}

5. 非静态内部类的静态实例


我们先了解下什么是静态内部类和非静态内部类,首先只有内部类才能设置为静态类,例如


class MainActivity : AppCompatActivity() {

private var a = 10

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

inner class InnerClass {
fun setA(code: Int) {
a = code
}
}
}

InnerClass是一个非静态内部类,那么在MainActivity声明了一个变量a,其实InnerClass是能够拿到这个变量,也就是说,非静态内部类其实是对外部类有一个隐式持有,那么它的静态实例对象是存储在方法区,而且该对象持有MainActivity的引用,导致退出时无法被释放。


解决方式就是:将InnerClass设置为静态类


class InnerClass {

fun setA(code: Int) {
a = code //这里就无法使用外部类的对象或者方法
}
}

大家如果对于kotlin不熟悉的话,就简单介绍一下,inner class在java中就是非静态的内部类;而直接用class修饰,那么就相当于Java中的 public static 静态内部类。


6. Handler


这个可就是老生常谈了,如果使用过Handler的话都知道,它非常容易产生内存泄漏,具体的原理就不说了,感觉现在用Handler真的越来越少了


其实说了这么多,真正在写代码的时候,不能真正的避免,接下来我就使用leakcanary来检测某个项目中存在的内存泄漏问题,并解决


3 从实际项目出发,根除内存泄漏


1. 单例引发的内存泄漏


image.png


我们从gcroot中可以看到,在TeachAidsCaptureImpl中传入了LifeCycleOwner,LifeCycleOwner大家应该熟悉,能够监听Activity或者Fragment的生命周期,然后CaptureModeManager是一个单例,传入的mode就是TeachAidsCaptureImpl,这样就会导致一个问题,单例的生命周期很长,Fragment被销毁的时候因为TeachAidsCaptureImpl持有了Fragment的引用,导致无法销毁


fun clear() {
if (mode != null) {
mode = null
}
}

所以,在Activity或者Fragment销毁前,将model置为空,那么内存泄漏就会解决了,直到看到这个界面,那么我们的应用就是安全的了


image.png


2.使用Toast引发的内存泄漏


image.png


在我们使用Toast的时候,需要传入一个上下文,我们通常会传入Activity,那么这个上下文给谁用的呢,在Toast中也有View,如果我们自定过Toast应该知道,那么如果Toast中的View持有了Activity的引用,那么就会导致内存泄漏


Toast.makeText(this,"Toast内存泄漏",Toast.LENGTH_SHORT).show()

那么怎样避免呢?传入Application的上下文,就不会导致Activity不被回收。


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

线上kafka消息堆积,consumer掉线,怎么办?

线上kafka消息堆积,所有consumer全部掉线,到底怎么回事? 最近处理了一次线上故障,具体故障表现就是kafka某个topic消息堆积,这个topic的相关consumer全部掉线。 整体排查过程和事后的复盘都很有意思,并且结合本次故障,对kafka使...
继续阅读 »

线上kafka消息堆积,所有consumer全部掉线,到底怎么回事?


最近处理了一次线上故障,具体故障表现就是kafka某个topic消息堆积,这个topic的相关consumer全部掉线。


整体排查过程和事后的复盘都很有意思,并且结合本次故障,对kafka使用的最佳实践有了更深刻的理解。


好了,一起来回顾下这次线上故障吧,最佳实践总结放在最后,千万不要错过。


1、现象



  • 线上kafka消息突然开始堆积

  • 消费者应用反馈没有收到消息(没有处理消息的日志)

  • kafka的consumer group上看没有消费者注册

  • 消费者应用和kafka集群最近一周内没有代码、配置相关变更


2、排查过程


服务端、客户端都没有特别的异常日志,kafka其他topic的生产和消费都是正常,所以基本可以判断是客户端消费存在问题。


所以我们重点放在客户端排查上。


1)arthas在线修改日志等级,输出debug


由于客户端并没有明显异常日志,因此只能通过arthas修改应用日志等级,来寻找线索。


果然有比较重要的发现:


2022-10-25 17:36:17,774 DEBUG [org.apache.kafka.clients.consumer.internals.AbstractCoordinator] - [Consumer clientId=consumer-1, groupId=xxxx] Disabling heartbeat thread

2022-10-25 17:36:17,773 DEBUG [org.apache.kafka.clients.consumer.internals.AbstractCoordinator] - [Consumer clientId=consumer-1, groupId=xxxx] Sending LeaveGroup request to coordinator xxxxxx (id: 2147483644 rack: null)

看起来是kafka-client自己主动发送消息给kafka集群,进行自我驱逐了。因此consumer都掉线了。


2)arthas查看相关线程状态变量

用arthas vmtool命令进一步看下kafka-client相关线程的状态。



可以看到 HeartbeatThread线程状态是WAITING,Cordinator状态是UNJOINED。


此时,结合源码看,大概推断是由于消费时间过长,导致客户端自我驱逐了。


于是立刻尝试修改max.poll.records,减少一批拉取的消息数量,同时增大max.poll.interval.ms参数,避免由于拉取间隔时间过长导致自我驱逐。


参数修改上线后,发现consumer确实不掉线了,但是消费一段时间后,还是就停止消费了。


3、最终原因


相关同学去查看了消费逻辑,发现了业务代码中的死循环,确认了最终原因。



消息内容中的一个字段有新的值,触发了消费者消费逻辑的死循环,导致后续消息无法消费。

消费阻塞导致消费者自我驱逐,partition重新reblance,所有消费者逐个自我驱逐。



这里核心涉及到kafka的消费者和kafka之间的保活机制,可以简单了解一下。



kafka-client会有一个独立线程HeartbeatThread跟kafka集群进行定时心跳,这个线程跟lisenter无关,完全独立。


根据debug日志显示的“Sending LeaveGroup request”信息,我们可以很容易定位到自我驱逐的逻辑。



HeartbeatThread线程在发送心跳前,会比较一下当前时间跟上次poll时间,一旦大于max.poll.interval.ms 参数,就会发起自我驱逐了。


4、进一步思考


虽然最后原因找到了,但是回顾下整个排查过程,其实并不顺利,主要有两点:



  • kafka-client对某个消息消费超时能否有明确异常?而不是只看到自我驱逐和rebalance

  • 有没有办法通过什么手段发现 消费死循环?


4.1 kafka-client对某个消息消费超时能否有明确异常?


4.1.1 kafka似乎没有类似机制


我们对消费逻辑进行断点,可以很容易看到整个调用链路。



对消费者来说,主要采用一个线程池来处理每个kafkaListener,一个listener就是一个独立线程。


这个线程会同步处理 poll消息,然后动态代理回调用户自定义的消息消费逻辑,也就是我们在@KafkaListener中写的业务。



所以,从这里可以知道两件事情。


第一点,如果业务消费逻辑很慢或者卡住了,会影响poll。


第二点,这里没有看到直接设置消费超时的参数,其实也不太好做。


因为这里做了超时中断,那么poll也会被中断,是在同一个线程中。所以要么poll和消费逻辑在两个工作线程,要么中断掉当前线程后,重新起一个线程poll。


所以从业务使用角度来说,可能的实现,还是自己设置业务超时。比较通用的实现,可以是在消费逻辑中,用线程池处理消费逻辑,同时用Future get阻塞超时中断。


google了一下,发现kafka 0.8 曾经有consumer.timeout.ms这个参数,但是现在的版本没有这个参数了,不知道是不是类似的作用。


4.1.2 RocketMQ有点相关机制


然后去看了下RocketMQ是否有相关实现,果然有发现。


在RocketMQ中,可以对consumer设置consumeTimeout,这个超时就跟我们的设想有一点像了。


consumer会启动一个异步线程池对正在消费的消息做定时做 cleanExpiredMsg() 处理。



注意,如果消息类型是顺序消费(orderly),这个机制就不生效。


如果是并发消费,那么就会进行超时判断,如果超时了,就会将这条消息的信息通过sendMessageBack() 方法发回给broker进行重试。



如果消息重试超过一定次数,就会进入RocketMQ的死信队列。



spring-kafka其实也有做类似的封装,可以自定义一个死信topic,做异常处理



4.2 有没有办法通过什么手段快速发现死循环?


一般来说,死循环的线程会导致CPU飙高、OOM等现象,在本次故障中,并没有相关异常表现,所以并没有联系到死循环的问题。


那通过这次故障后,对kafka相关机制有了更深刻了解,poll间隔超时很有可能就是消费阻塞甚至死循环导致。


所以,如果下次出现类似问题,消费者停止消费,但是kafkaListener线程还在,可以直接通过arthas的 thread id 命令查看对应线程的调用栈,看看是否有异常方法死循环调用。


5、最佳实践


通过此次故障,我们也可以总结几点kafka使用的最佳实践:




  • 使用消息队列进行消费时,一定需要多考虑异常情况,包括幂等、耗时处理(甚至死循环)的情况。




  • 尽量提高客户端的消费速度,消费逻辑另起线程进行处理,并最好做超时控制。




  • 减少Group订阅Topic的数量,一个Group订阅的Topic最好不要超过5个,建议一个Group只订阅一个Topic。




  • 参考以下说明调整参数值:max.poll.records:降低该参数值,建议远远小于<单个线程每秒消费的条数> * <消费线程的个数> * <max.poll.interval.ms>的积。max.poll.interval.ms: 该值要大于<max.poll.records> / (<单个线程每秒消费的条数> * <消费线程的个数>)的值。


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

深入学习 Kotlin 特色之 Sealed Class 和 Interface

前言sealed class 以及 1.5 里新增的 sealed interface 可谓是 Kotlin 语言的一大特色,其在类型判断、扩展和实现的限制场景里非常好用。本文将从特点、场景和原理等角度综合分析 sealed 语法。Sealed ClassSe...
继续阅读 »

前言

sealed class 以及 1.5 里新增的 sealed interface 可谓是 Kotlin 语言的一大特色,其在类型判断、扩展和实现的限制场景里非常好用。

本文将从特点、场景和原理等角度综合分析 sealed 语法。

  • Sealed Class
  • Sealed Interface
  • Sealed Class & Interface VS Enum
  • Sealed Class VS Interface

🏁 Sealed Class

sealed class,密封类。具备最重要的一个特点:

  • 其子类可以出现在定义 sealed class 的不同文件中,但不允许出现在与不同的 module 中,且需要保证 package 一致

这样既可以避免 sealed class 文件过于庞大,又可以确保第三方库无法扩展你定义的 sealed class,达到限制类的扩展目的。事实上在早期版本中,只允许在 sealed class 内部或定义的同文件内扩展子类,这些限制在 Kotlin 1.5 中被逐步放开。

如果在不同 module 或 package 中扩展子类的话,IDE 会显示如下的提示和编译错误:

Inheritor of sealed class or interface declared in package xxx but it must be in package xxx where base class is declared

sealed class 还具有如下特点或限制:

  1. sealed class 是抽象类,可以拥有抽象方法,无法直接实例化。否则,编译器将提示如下:

    Sealed types cannot be instantiated

  2. sealed class 的构造函数只能拥有两种可见性:默认情况下是 protected,还可以指定成 private,public 是不被允许的。

    Constructor must be private or protected in sealed class

  3. sealed class 子类可扩展局部以及匿名类以外的任意类型子类,包括普通 class、data classobject、sealed class 等,子类信息在编译期可知。

    假使匿名类扩展自 sealed class 的话,会弹出错误提示:

    This type is sealed, so it can be inherited by only its own nested classes or objects

  4. sealed class 的实例,可配合 when 表达式进行判断,当所有类型覆盖后可以省略 else 分支

    如果没有覆盖所有类型,也没有 else 统筹则会发生编译警告或错误

    1.7 以前:

    Non-exhaustive 'when' statements on sealed class/interface will be prohibited in 1.7.

    1.7 及以后:

    'when' expression must be exhaustive, add ...

当 sealed class 没有指定构造方法或定义任意属性的时候,建议子类定义成单例,因为即便实例化成多个实例,互相之间没有状态的区别:

'sealed' subclass has no state and no overridden 'equals()'

下面结合代码看下 sealed class 的使用和原理:

示例代码:

 // TestSealed.kt
 sealed class GameAction(times: Int) {
     // Inner of Sealed Class
     object Start : GameAction(1)
     data class AutoTick(val time: Int) : GameAction(2)
     class Exit : GameAction(3)
 }

除了在 sealed class 内嵌套子类外,还可以在外部扩展子类:

 // TestSealed.kt
 sealed class GameAction(times: Int) {
    ...
 }
 
 // Outer of Sealed Class
 object Restart : GameAction(4)

除了可以在同文件下 sealed class 外扩展子类外,还可以在同包名不同文件下扩展。

 // TestExtendedSealedClass.kt
 // Outer of Sealed Class file
 class TestExtendedSealedClass: GameAction(5)

对于不同类型的扩展子类,when 表达式的判断亦不同:

  • 判断 sealed class 内部子类类型自然需要指定父类前缀
  • object class 的话可以直接进行实例判断,也可以用 is 关键字判断类型匹配
  • 普通 class 类型的话则必须加上 is 关键字
  • 判断 sealed class 外部子类类型自然无需指定前缀
 class TestSealed {
     fun test(gameAction: GameAction) {
         when (gameAction) {
             GameAction.Start -> {}
             // is GameAction.Start -> {}
             is GameAction.AutoTick -> {}
             is GameAction.Exit -> {}
 
             Restart -> {}
             is TestExtendedSealedClass -> {}
        }
    }
 }

如下反编译的 Kotlin 代码可以看到 sealed class 本身被编译为 abstract class。

扩展自其的内部子类按类型有所不同:

  • object class 在 class 内部集成了静态的 INSTANCE 实例
  • 普通 class 仍是普通 class
  • data Class 则是在 class 内部集成了属性的 gettoString 以及 hashCode 函数
 public abstract class GameAction {
    private GameAction(int times) { }
 
    public GameAction(int times, DefaultConstructorMarker $constructor_marker) {
       this(times);
    }
     
    // subclass:object
    public static final class Start extends GameAction {
       @NotNull
       public static final GameAction.Start INSTANCE;
 
       private Start() {
          super(1, (DefaultConstructorMarker)null);
      }
 
       static {
          GameAction.Start var0 = new GameAction.Start();
          INSTANCE = var0;
      }
    }
 
    // subclass:class
    public static final class Exit extends GameAction {
       public Exit() {
          super(3, (DefaultConstructorMarker)null);
      }
    }
 
    // subclass:data class
    public static final class AutoTick extends GameAction {
       private final int time;
 
       public final int getTime() {
          return this.time;
      }
 
       public AutoTick(int time) {
          super(2, (DefaultConstructorMarker)null);
          this.time = time;
      }
      ...
       @NotNull
       public String toString() {
          return "AutoTick(time=" + this.time + ")";
      }
 
       public int hashCode() { ... }
 
       public boolean equals(@Nullable Object var1) { ... }
    }
 }

而外部子类则自然是定义在 GameAction 抽象类外部。

 public abstract class GameAction {
    ...
 }
 
 public final class Restart extends GameAction {
    @NotNull
    public static final Restart INSTANCE;
 
    private Restart() {
       super(4, (DefaultConstructorMarker)null);
    }
 
    static {
       Restart var0 = new Restart();
       INSTANCE = var0;
    }
 }

文件外扩展子类可想而知。

 public final class TestExtendedSealedClass extends GameAction {
    public TestExtendedSealedClass() {
       super(5, (DefaultConstructorMarker)null);
    }
 }

🏴 Sealed Interface

sealed interface 即密封接口,和 sealed class 有几乎一样的特点。比如:

  • 限制接口的实现:一旦含有包含 sealed interface 的 module 经过了编译,就无法再有扩展的实现类了,即对其他 module 隐藏了接口

还有些额外的优势:

  • 帮助密封类、枚举类等类实现多继承和扩展性,比如搭配枚举,以处理更复杂的分类逻辑

    Additionally, sealed interfaces enable more flexible restricted class hierarchies because a class can directly inherit more than one sealed interface.

    比如 Flappy Bird 游戏的过程中会产生很多 Action 来触发数据的计算以推动 UI 刷新以及游戏的进程,Action 可以用 enum class 来管理。

    其中有些 Action 是关联的,有些则没有关联、不是同一层级。但是 enum class 默认扩展自 Enum 类,无法再嵌套 enum。

    Enum class cannot inherit from classes

    这将导致层级混乱、阅读性不佳,甚至有的时候功能相近的时候还得特意取个不同的名称。

     enum class Action {
         Tick,
         // GameAction
         Start, Exit, Restart,
         // BirdAction
         Up, Down, HitGround, HitPipe, CrossedPipe,
         // PipeAction
         Move, Reset,
         // RoadAction
         // 防止和 Pipe 的 Action 重名导致编译出错,
         // 将功能差不多的 Road 移动和重置 Action 定义加上了前缀
         RoadMove, RoadReset
     }
     
     fun dispatch(action: Action) {
         when (action) {
             Action.Tick -> TODO()
     
             Action.Start -> TODO()
             Action.Exit -> TODO()
             Action.Restart -> TODO()
     
             Action.Up -> TODO()
             Action.Down -> TODO()
             Action.HitGround -> TODO()
             Action.HitPipe -> TODO()
             Action.CrossedPipe -> TODO()
     
             Action.Move -> TODO()
             Action.Reset -> TODO()
     
             Action.RoadMove -> TODO()
             Action.RoadReset -> TODO()
        }
     }

    借助 sealed interface 我们可以给抽出 interface,并将 enum 进行层级拆分。更加清晰、亦不用担心重名。

     sealed interface Action
     
     enum class GameAction : Action {
         Start, Exit, Restart
     }
     
     enum class BirdAction : Action {
         Up, Down, HitGround, HitPipe, CrossedPipe
     }
     
     enum class PipeAction : Action {
         Move, Reset
     }
     
     enum class RoadAction : Action {
         Move, Reset
     }
     
     object Tick: Action

    使用的时候就可以对抽成的 Action 进行嵌套判断:

     fun dispatch(action: Action) {
         when (action) {
             Tick -> TODO()
             
             is GameAction -> {
                 when (action) {
                     GameAction.Start -> TODO()
                     GameAction.Exit -> TODO()
                     GameAction.Restart -> TODO()
                }
            }
             is BirdAction -> {
                 when (action) {
                     BirdAction.Up -> TODO()
                     BirdAction.Down -> TODO()
                     else -> TODO()
                }
            }
             is PipeAction -> {
                 when (action) {
                     PipeAction.Move -> TODO()
                     PipeAction.Reset -> TODO()
                }
            }
             is RoadAction -> {
                 when (action) {
                     RoadAction.Move -> TODO()
                     RoadAction.Reset -> TODO()
                }
            }
        }
     }

🤔 总结

1. Sealed Class & Interface VS Enum

总体来说 sealed class 和 interface 和 enum 有相近的地方,也有明显区别,需要留意:

  • 每个 enum 常量只能以单例的形式存在
  • sealed class 子类可以拥有多个实例,不受限制,每个均可以拥有自己的状态
  • enum class 不能扩展自 sealed class 以及其他任何 Class,但他们可以实现 sealed 等 interface

2. Sealed Class VS Interface

Sealed classes and interfaces represent restricted class hierarchies that provide more control over inheritance.

sealed class 和 interface 都意味着受限的类层级结构,便于在继承和实现上进行更多控制。具备如下的共同特性:

  • 其 sub class 需要定义在同一 Module 以及同一 package,不局限于 sealed 内部或同文件内

看下对比:

Sealed适用/优势原理
Class限制类的扩展abstract class
Interface限制接口的实现 帮助类实现多继承和复杂的扩展性interface


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

带你深入理解Flutter及Dart单线程模型

前言 大家好,我是未央歌,一个默默无闻的移动开发搬砖者~ 众所周知,Java 是一种多线程语言,适量并合适地使用多线程,会极大提高资源利用率和运行效率,但缺点也明显,比如开启过多的线程会导致资源和性能的消耗过大以及多线程共享内存容易死锁。 而 Dart 则是一...
继续阅读 »

前言


大家好,我是未央歌,一个默默无闻的移动开发搬砖者~


众所周知,Java 是一种多线程语言,适量并合适地使用多线程,会极大提高资源利用率和运行效率,但缺点也明显,比如开启过多的线程会导致资源和性能的消耗过大以及多线程共享内存容易死锁


而 Dart 则是一种单线程语言,单线程语言就意味着代码执行顺序是有序的,下面结合一个demo带大家深入了解单线程模型。


demo 示例


点击 APP 右下角的刷新按钮,会调用如下方法,读取一个约 2M 大小的 json 文件。


void loadAssetsJson() async {
var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
}

如下图所示,点击刷新按钮之后,中间的 loading 会卡一下。很多同学一看这个代码就知道,肯定会卡,解析一个 2M 的文件,而且是同步解析,主页面肯定是会卡的。

那如果我换成异步解析呢?还卡不卡?大家可以脑海中思考下这个问题。


异步解析


void loadAssetsJson() async {
var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

// 异步解析
Future(() {
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
}).then((value) {});
}


大家可以看到,我已经放在异步里解析了,为什么还是会卡呢?大家可以先思考下这个问题。


前面已经提到了 Dart 是一种单线程语言,单线程语言就意味着代码执行顺序是有序的。当然 Dart 也是支持异步的。这两点其实并不冲突。


Dart 线程解析



我们来看看 Dart 的线程,当我们 main() 方法启动之后,Dart已经开启了一个线程,这个线程的名字就叫 Isolate。每一个 Isolate 线程都包含了图示的两个队列,一个 Microtask queue,一个 Event queue


如图,Isolate 线程会优先执行 Microtask queue 里的事件,当 Microtask queue 里的事件变成空了,才会去执行 Event queue 里的事件。如果正在执行 Microtask queue 里的事件,那么 Event queue 里的事件就会被阻塞,就会导致渲染、手势响应等都得不到响应(绘制图形,处理鼠标点击,处理文件IO等都是在 Event Queue 里完成)。


所以为了保证功能正常使用不卡顿,尽量少在 Microtask queue 做事情,可以放在 Event queue 做


为什么单线程可以做一个异步操作呢?



  • 因为 APP 只有在你滑动或者点击操作的时候才会响应事件。没有操作的时候进入等待时间,两个队列里都是空的。这个时间正是可以进行异步操作的,所以基于这个特点,单线程模型可以在等待过程中做一些异步操作,因为等待的过程并不是阻塞的,所以给我们的感觉就像同时在做多件事情,但自始至终只有一个线程在处理事情。


Future


当方法加上 async 关键字,就代表这个方法开启了一个异步操作,如果这个方法有返回值,就必须要返回一个 Future。


void loadAssetsJson() async {
var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

// 异步解析
Future(() {
...
}).then((value) {});
}

一个 Future 异步任务的执行,相对简单。在我们声明一个 Future 之后,Dart 会将异步里的代码函数体放在 Event queue 里执行然后返回。这里注意下,Future 和 then 是放在同一个 Event queue 里的。


假设,我执行 Future 代码之后没有立即执行 then 方法,而是等 Future 执行之后5秒,才调用 then 方法,这时候还是放在同一个 Event queue 里吗?显然是不可能的,我们看一下源码是怎么实现的。


Future<R> then<R>(FutureOr<R> f(T value), {Function? onError}) {
...
_addListener(new _FutureListener<T, R>.then(result, f, onError));
return result;
}

bool get _mayAddListener => _state <= (_statePendingComplete | _stateIgnoreError);

void _addListener(_FutureListener listener) {
assert(listener._nextListener == null);
if (_mayAddListener) {
// 待完成
listener._nextListener = _resultOrListeners;
_resultOrListeners = listener;
} else {
// 已完成
...
_zone.scheduleMicrotask(() {
_propagateToListeners(this, listener);
});
}
}

可以看到 then 方法里有一个监听,Future 执行之后5秒才调用,很明显是已完成状态,走 else 那里的 scheduleMicrotask() 方法,就是说把 then 里面的方法放到 Microtask queue 里。


Future 为何卡顿


再来说一下刚刚的问题,我已经放在异步里解析了,为什么还是会卡呢?


其实很简单,Future 里的代码可能需要执行10s,也就是 Event queue 需要10s才能执行完。那这个10s内其他代码肯定就无法执行了。所以 Future 里的代码执行时间过长,还是会卡 UI 的。


以 Android 为例,Android的刷新频率是60帧/秒,Android系统中每隔16.6ms会发送一次 VSYNC(同步)信号,触发UI的渲染。所以我们就要考虑下,一旦代码执行时间超过16.6ms,到底应不应该放在 Future 里执行?


这时候是不是有同学有疑问,我网络请求也是用 Future 写的,为什么就不卡呢?


这个大家就需要注意一下,网络请求不是放在 Dart 层面执行的,它是由操作系统提供的异步线程去执行的,当这个异步执行完系统又返回给 Dart。所以即使 http 请求需要耗时十几秒,也不会感到卡顿。


compute


既然 Future 执行也会卡顿,那要怎么去优化呢?这时候我们可以开一个线程操作,Flutter 为我们封装好了一个 compute()方法,这个方法可以为我们开一个线程。我们用这个方法来优化一下代码,然后再看下执行效果。


void loadAssetsJson() async {
var jsonStr = await DefaultAssetBundle.of(context).loadString("assets/list.json");

var result = compute(parse,jsonStr);
}

static VideoListModel parse(String jsonStr){
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
VideoListModel.fromJson(json.decode(jsonStr));
return VideoListModel.fromJson(json.decode(jsonStr));
}


可以看到此时点击刷新按钮,已经不再卡顿了。遇到一些耗时的操作,这确实是一种比较好的解决方式。


我们再看看 DefaultAssetBundle.of(context).loadString("assets/list.json") 方法里面是怎么执行的。


Future<String> loadString(String key, { bool cache = true }) async {
final ByteData data = await load(key);
if (data == null)
throw FlutterError('Unable to load asset: $key');
// 50 KB of data should take 2-3 ms to parse on a Moto G4, and about 400 μs
// on a Pixel 4.
if (data.lengthInBytes < 50 * 1024) {
return utf8.decode(data.buffer.asUint8List());
}
// For strings larger than 50 KB, run the computation in an isolate to
// avoid causing main thread jank.
return compute(_utf8decode, data, debugLabel: 'UTF8 decode for "$key"');
}

从官方源码可以看到,当文件的大小超过 50kb 时,也是采用 compute() 方法开一个线程去操作的。


多线程机制


Dart 作为一个单线程语言,虽然提供了多线程的机制,但是在多线程的资源是隔离的,两个线程之间资源是不互通的


Dart 的多线程数据交互需要从 A 线程传给 B 线程,再由 B 线程返回给 A 线程。而像 Android 在主线程开一个子线程,子线程可以直接拿主线程的数据,而不用让主线程传给子线程。


总结



  • Future 适合耗时小于 16ms 的操作

  • 可以通过 compute() 进行耗时操作

  • Dart 是单线程原因,但也支持多线程,但是线程间数据不互通

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

Kotlin Sequence 是时候派上用场了

前言 在进入Flow世界之前,先来分析Sequence,进而自然延伸到Flow。 通过本篇文章,你将了解到: Java与Kotlin 对集合的处理 Java Stream 的简单使用 Sequence 的简单使用 Sequence 的原理 Sequence...
继续阅读 »

前言


在进入Flow世界之前,先来分析Sequence,进而自然延伸到Flow。

通过本篇文章,你将了解到:




  1. Java与Kotlin 对集合的处理

  2. Java Stream 的简单使用

  3. Sequence 的简单使用

  4. Sequence 的原理

  5. Sequence 的优劣势



1. Java与Kotlin 对集合的处理


场景分析


客户有个场景想考验一下Java和Kotlin:

从一堆数据里(0--10000000)找到大于1000的偶数的个数。


Java和Kotlin 均表示so easy,跃跃欲试。

秉着尊老爱幼的优良传统,老大哥Java先出场。


Java 出场


    public List<Integer> dealCollection() {
List<Integer> evenList = new ArrayList<>();
for (Integer integer : list) {
//筛选出偶数
if (integer % 2 == 0) {
evenList.add(integer);
}
}

List<Integer> bigList = new ArrayList<>();
for (Integer integer : evenList) {
//从偶数中筛选出大于1000的数
if (integer > 1000) {
bigList.add(integer);
}
}
//返回筛选结果列表
return bigList;
}

Java解释说:“先将偶数的结果保存到列表里,再从偶数列表里筛选出大于1000的数。”


Kotlin 出场


Kotlin 看到Java的解决方案,表示写法有点冗余,不够灵活,于是拿出自己的方案:


    fun testCollection() {
var time = measureTimeMillis {
var list = (0..10000000).filter {
it % 2 == 0
}.filter {
it > 1000
}
println("kotlin collection list size:${list.size}")
}
println("kotlin collection use time:$time")
}

Kotlin 说:“老大哥,看看我这个写法,只需要几行代码,简洁如斯。”

Java 淡定到:“确实够简洁,但是表面的简洁掩盖了背后的许多冗余,能一层一层剥开你的心吗?”

Kotlin道:“你我赤诚相对,士为知己者死,刀来!”

Java赶紧递上自己随身携带的水果刀...


Kotlin 反编译


遇事不决反编译:


    public final void testCollection() {
//构造迭代器
Iterable $this$filter$iv = (Iterable)(new IntRange(var8, 10000000));
//构造链表用来存储偶数
Collection destination$iv$iv = (Collection)(new ArrayList());
//取出迭代器
Iterator var13 = $this$filter$iv.iterator();

//遍历取出偶数
while(var13.hasNext()) {
element$iv$iv = var13.next();
it = ((Number)element$iv$iv).intValue();
var16 = false;
if (it % 2 == 0) {
destination$iv$iv.add(element$iv$iv);
}
}

$this$filter$iv = (Iterable)((List)destination$iv$iv);
$i$f$filter = false;
//构造链表用来存储>1000的偶数
destination$iv$iv = (Collection)(new ArrayList());
$i$f$filterTo = false;
//取出迭代器
var13 = $this$filter$iv.iterator();
//遍历链表
while(var13.hasNext()) {
element$iv$iv = var13.next();
it = ((Number)element$iv$iv).intValue();
var16 = false;
if (it > 1000) {
destination$iv$iv.add(element$iv$iv);
}
}

//最终的结果
List list = (List)destination$iv$iv;
}

看到这,Java恍然大悟到:“原来如此,你也是分步存储结果,我俩想到一起了,真机智啊。”

Kotlin:“彼此彼此。”


客户说:“你俩就不要商业互吹了,我就想要一个结果而已,你们就给我弄了两个循环,若是此时我再加一两个条件,你们是不是得要再加几个循环遍历?那不是白白增加耗时吗?”

Java 神秘的笑道:“非也非也,我好歹也是沉浸代码界几十年的存在,早有预案。”

客户说:“那就开始你的表演吧...”


2. Java Stream 的简单使用


什么是流


Java说:“我从Java8开始就支持Stream(流) API了,可以满足你的需求。”

客户不解道:“什么是流?”

Java:“流就是一个过程,比如说你之前的需求就可以当做一个流,可以在中途对流做一系列的处理,而后在流的末尾取出处理后的结果,这个结果就是最终的结果。”

Kotlin补充道:“老大哥,你说的比较抽象,我举个例子吧。”



image.png


在一个管道的入口处放入了各种鱼,如草鱼、鲤鱼、鲢鱼、金鱼等,管道允许接入不同的小管道用以筛选不同组合的鱼类。

比如有个客户只想要金鱼,于是它分别接了4个小管道,第一个管道用来将草鱼分流,第二个管道用来分流鲤鱼,第三个管道用来分流鲢鱼,最后剩下的就是金鱼。

当然,他也可以只分流草鱼,剩下的鲤鱼、鲢鱼、金鱼他都需要,这就增加了操作的灵活性。

客户说:“talk is cheap, show me the code。”


Java Stream


Java 撸起袖子,几个呼吸之间就写好了如下代码:


    public long dealCollectionWithStream() {
Stream<Integer> stream = list.stream();
return stream.filter(value -> value % 2 == 0)
.filter(value -> value > 1000)
.count();
}

客户不解地问:“这确实很简洁了,但是和Kotlin写法一样的嘛?”

Java道:“No No No,别被简洁的外表迷惑了,我们直接来看看处理的耗时即可。”


    public static void main(String args[]) {
Java8Stream java8Stream = new Java8Stream();
//普通集合耗时
long startTime = System.currentTimeMillis();
List<Integer> list = java8Stream.dealCollection();
System.out.println("java7 list size:" + list.size() + " use time:" + (System.currentTimeMillis() - startTime) + "ms");

//Stream API 的耗时
long startTime2 = System.currentTimeMillis();
long count = java8Stream.dealCollectionWithStream();
System.out.println("java8 stream list size:" + count + " use time:" + (System.currentTimeMillis() - startTime2) + "ms");
}

打印结果如下:



image.png


Java 继续解释:“既然只关心最后的结果,那么对于流来说,可以在各个位置指定条件对流的内容进行筛选,对于同一个内容来说只有上一个条件满足了,才会继续处理下一个条件,否则将会处理流里的其它内容。如此一来,再也不用反复存取中间结果了,对于大批量的数据来说,大大减少了耗时。”

客户赞赏:“不错,能解决我的痛点。”

Java 说:“不仅如此,我还可以并行操作流,最后将结果汇总,又可以减少一些耗时了。”

客户:“优秀,那我就选...”

Kotlin 急道:“住口...不,等等,我有话说。”

客户:“你快说,说不出子丑寅卯,我就...”


3. Sequence 的简单使用


Sequence 引入


Kotlin:“和Java老大哥一样,我也可以对流进行操作,主要是用sequence实现”


    fun testSequence() {
var time = measureTimeMillis {
val count = (0..10000000)
.asSequence()//转换为sequence
.filter {
it % 2 == 0//过滤偶数
}.filter {
it > 1000//过滤>1000
}.count() //统计个数
println("kotlin sequence list size:${count}")
}
println("kotlin sequence use time:$time")
}

和未使用sequence 对比耗时:


    public static void main(String args[]) {
SequenceDemo sequenceDemo = new SequenceDemo();
//使用集合操作
sequenceDemo.testCollection();
//使用sequence操作
sequenceDemo.testSequence();
}


image.png


可以看出,使用了sequence后,可以大大减少耗时。


Kotlin 反编译Sequence



image.png


由此可见,并没有对中间结果进行存储遍历,而是通过嵌套调用进而操作流的。


4. Sequence 的原理


集合转Sequence


(0..10000000)

这表示的是0到10000000的集合,它的实现类是:



image.png


IntRange 里定义了集合的开始值和结束值,重点在其父类:IntProgression。

IntProgression 实现了Iterable接口,并实现了该接口里的唯一方法:iterator()

具体实现类为:


internal class IntProgressionIterator(first: Int, last: Int, val step: Int) : IntIterator() {
private val finalElement: Int = last
private var hasNext: Boolean = if (step > 0) first <= last else first >= last
private var next: Int = if (hasNext) first else finalElement

override fun hasNext(): Boolean = hasNext

override fun nextInt(): Int {
val value = next
if (value == finalElement) {
if (!hasNext) throw kotlin.NoSuchElementException()
hasNext = false
}
else {
next += step
}
return value
}
}

通常来说,迭代器有三个重要元素:




  1. 起始值

  2. 步长

  3. 结束值



对应的两个核心方法:




  1. 检测是否还有下个元素

  2. 取出下个元素



对于当前的Int迭代器来说:它的起始值为0,步长是1,结束值是10000000,当我们调用迭代器时就可以取出里面的每个数。


迭代器有了,接下来看看如何构造为一个Sequence。


public fun <T> Iterable<T>.asSequence(): Sequence<T> {
//取当前的迭代器,也就是IntProgressionIterator
return Sequence { this.iterator() }
}
//构造一个Sequence
public inline fun <T> Sequence(crossinline iterator: () -> Iterator<T>): Sequence<T> = object : Sequence<T> {
override fun iterator(): Iterator<T> = iterator()
}

Sequence 是个接口,它的唯一接口是:


public interface Sequence<out T> {
public operator fun iterator(): Iterator<T>
}

结合两者分析可知:



asSequence() 构造了Sequence匿名内部类对象,而其实现的方法就是iterator(),该方法最终返回IntProgressionIterator 对象
也就是说Sequence初始迭代器即为Collection的迭代器



Sequence中间操作符


以filter为例:


public fun <T> Sequence<T>.filter(predicate: (T) -> Boolean): Sequence<T> {
//构造Sequence 子类,该子类用来过滤流
return FilteringSequence(this, true, predicate)
}

override fun iterator(): Iterator<T> = object : Iterator<T> {
//上一个Sequence的迭代器
val iterator = sequence.iterator()
var nextState: Int = -1 // -1 for unknown, 0 for done, 1 for continue
var nextItem: T? = null

private fun calcNext() {
//先判断上一个Sequence的迭代器
while (iterator.hasNext()) {
val item = iterator.next()
//拿到值后判断本Sequence的逻辑
//是否符合过滤条件,符合就取出值,交个下一个条件,不符合则找下一个元素
if (predicate(item) == sendWhen) {
nextItem = item
nextState = 1
return
}
}
nextState = 0
}

//重写next()与hasNext(),里边调用了calcNext
override fun next(): T {
if (nextState == -1)
calcNext()
if (nextState == 0)
throw NoSuchElementException()
val result = nextItem
nextItem = null
nextState = -1
@Suppress("UNCHECKED_CAST")
return result as T
}

override fun hasNext(): Boolean {
if (nextState == -1)
calcNext()
return nextState == 1
}
}

我们调用了两次filter操作符,最终形成的结构如下:



image.png


此处用到了设计模式里的装饰模式:




  1. Sequence 只有普通的迭代功能,现在需要为它增强过滤偶数的功能,因此新建了FilteringSequence 对象A,并持有Sequence对象,当需要调用过滤偶数的功能时,先借助Sequence获取基本数据,再使用FilteringSequenceA过滤偶数

  2. 同样的,还需要在1的基础上继续增强FilteringSequence的过滤功能,再新建FilteringSequence B持有FilteringSequence对象A
    A,当需要调用过滤>1000的数时,先借助FilteringSequence 对象A获取偶数,再使用FilteringSequenceB过滤>1000的数



如此一来,通过嵌套调用就实现了众多操作过程。


Sequence终端操作符


你可能已经发现了:中间操作符仅仅只是建立了装饰(引用)关系,并没有触发迭代啊,那什么时候触发迭代呢?

这个时候就需要用到终端操作符(也叫末端操作符)。

比如count方法:


    public fun <T> Sequence<T>.count(): Int {
var count = 0
//触发遍历,统计个数
for (element in this) checkCountOverflow(++count)
return count
}

当调用了count()方法后,将会触发遍历,最终调用栈如下:



image.png


只有调用了终端操作符,流才会动起来,这也就是为啥说Sequence、Java Stream 中间操作符是惰性操作符的原因。


Sequence与普通集合链式调用区别


还是之前的Demo

普通集合链式调用



image.png


每次操作(如filter)都需要遍历集合找到符合条件的条目加入到新的集合,然后再在新的集合基础上再次进行操作。

如上图,先执行紫色区块,再执行蓝色区块。


Sequence 调用



image.png


每次先对某个条目进行所有的操作(比如filter),先判断每一步该条目是否符合,不符合则再找下一个条目进行所有的操作。

如上图:从左到右按顺序执行紫色区块。


5. Sequence 的优劣势


与普通集合链式调用对比,Sequence也有链式调用。

前者链式调用每次都需要完整遍历集合并将中间结果缓存,下一次调用依赖上一次调用缓存的结果。

而后者链式调用先是将每个操作关联起来,然后当触发终端操作符时针对每一个条目(元素)先执行所有的操作(这些操作在上一步已经关联)。

由此可见,如果集合里元素很多,Sequence可以大大节约时间(没有多次遍历,没有暂存结果)


除此之外,Sequence 只做最少的操作,尽可能地节约时间。

怎么理解呢?还是上面的例子,我们只想取前10个偶数,代码如下:


    fun testSequence1() {
var time = measureTimeMillis {
val count = (0..10000000)
.asSequence()//转换为sequence
.filter {
it % 2 == 0//过滤偶数
}.take(10).count()
println("kotlin sequence1 list size:${count}")
}
println("kotlin sequence1 use time:$time")
}

该序列只会执行到集合里的条目=18就终止了,因为到了0~18已经有10个偶数了,而剩下的一堆条目都无需执行,大大节省了时间。


因此,Sequence 优势:




  1. 不暂存数据,不进行多次遍历

  2. 只做最少的操作

  3. 可以产生无限的序列



以上以filter/take/count 操作符阐述了Sequence的原理及其优势,当然还有更多的具体的使用场景/方式待挖掘。


此时,Kotlin 迫不及待跳出来说:“怎么样,我这个Sequence 6吧?”

客户说:“看你字多的份上,我选择信你,那我就选...”

Java急忙道:“我有问题,我的Stream支持并行,你支持吗?”

Kotlin:“...”

想了一会儿,Kotlin继续道:“Sequence 虽然不支持切换线程,但是它的兄弟支持,它就是Flow。”

Java补充说:“你有Flow,我有LiveData,那我俩继续PK?”

没等Kotlin回话,客户急忙道:“哎哎,行了,时间不够了,下次再继续吧,散会...”

Java:“...”

Kotlin:“...”


第100篇博客,不忘初心,砥砺前行,继续输出高质量、成体系的博客。

下次将会进入Flow的世界。


本文基于Kotlin 1.5.3,文中完整Demo请点击


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

uni-app跨端开发之疑难杂症

今年,公司决定解决各个团队移动端开发的混战局面,由架构部出一套移动端框架,规范化开发标准。经过一段时间的调研,考虑到跨端以及公司主要技术栈为vue,最终选择了uni-app作为移动端框架,在大家都“很忙”的情况下,我成为了移动端框架的主要开发。以前就总听同事说...
继续阅读 »

前言

今年,公司决定解决各个团队移动端开发的混战局面,由架构部出一套移动端框架,规范化开发标准。经过一段时间的调研,考虑到跨端以及公司主要技术栈为vue,最终选择了uni-app作为移动端框架,在大家都“很忙”的情况下,我成为了移动端框架的主要开发。以前就总听同事说,uni-app有很多坑,我对其也只是有些许了解,这回的全身心投入,才知道一入深坑愁似海

这段时间也做了一些成效,头大如斗的路由拦截、必不可少的http请求封装、提高成效的组件库、仿照微信的oAuth 2.0登录、复杂逻辑的离线存储、用户需要的增量更新包

有成效也踩了一些坑,百思不得解的console.log、烦到吐血的网络调试、爬坑许久的APP与h5通讯、性能极差的微信小程序端uni.canvasToTempFilePath

今天就要聊聊一些疑难杂症,有些忘记了,有些还没碰到,后续持续更新吧!

百思不得解的console.log

移动端框架是采用npm包的方式提供给业务部门使用,其中包含oAuth2.0登录方式,这其中涉及到了h5通过scheme协议唤醒app并且带回code等参数,相应的参数会存放在plus.runtime.arguments,其他情况下,plus.runtime.arguments的值为空。在给同事排查问题时我就简单操作,在node_modules对应的npm包里面写了不是很严谨的如下代码:

const args = plus.runtime.arguments;
// 这个是业务部门出错时,我添加的调试代码
console.log('>>>>>>'args)
if (args) {
 const isLogout = args.includes('logout');
 if (isLogout) {
   await this.handleSession();
else {
   await this.handleAuthorization(args);
}
}

我测试是正常的,args是空值,所以是不会执行if内的逻辑的,但是他这边会执行if内的逻辑的,初步判断args由于某个原因导致存在值了,为了简单明了的查看输出内容,然后我就写了毁一生的console.log('>>>>>>', args),这行调试代码的输出内容如下,我一直以为args是空值,但是判断依旧为true,有点颠覆了我的人生观,后来灵机一动,删掉了第一个修饰参数,发现args原来是有值的,经过排查,是因为添加了微信小程序打开指定页面,导致记录当前页面数据。


烦到吐血的网络调试

网络调试对于我们的日常开发是很重要的,有助于快速判断资源请求问题,但uni-app在这方面有很大的缺陷,在讨论这个问题时,先来看一下uni-app的真机调试方式。

终端调试工具

当项目运行时,点击终端上的调试按钮,会弹出一个调试界面。


从调试面板中,可以看到仅有ConsoleElementsSources三个选项,期待许久的Network并没有出现,这种调试方式没办法实现网络请求调试。


webview调试控制台

点击工具栏的 运行 -> 运行到手机或模拟器 -> 显示webview调试控制台 会出现一个跟谷歌浏览器一样的调试界面,虽然这里有Network,但是很可惜,这个功能存在问题,没办法监听到网络请求。


Fiddler 抓取网络请求

在走投无路之下,只能另辟蹊径,借助工具,抓取真机的网络请求,接下来阐述一下怎么使用Fiddler抓取真机的网络请求,配置完需要重启才生效。

下载Fiddler

这是一个免费工具,自行在网络上下载即可。

Fiddler 基础配置

点击工具栏的tools,选择options就会弹出一个配置界面



HTTPS 配置

选择HTTPS选项,勾选选矿中的Capture HTTPS CONNECTsDecrypt HTTPs trfficIgnore server certificate errors


Connections 配置

这边配置的端口号后面配置代理的时候需要使用到。


手机配置代理

注意需要和电脑连接同一网络,点击进入手机WIFI详情界面,有个代理,选择手动模式,输入电脑的IP地址和Fiddler的监听端口,即可拦截到真机的所有网络请求,包含我们app对应的网络请求。


过滤

这边可以选择过滤对应的ip或域名,多个的话通过分号隔开即可。


爬坑许久的APP与h5通讯

谈论这个问题时,先描述一下uni-app实现的app怎么和h5通讯

app端

对于app端的通讯,.vue.nvue有两点区别,1. 获取webView实例不一致,2. 监听方法不一致。app向h5传递数据时,需要借助webview.evalJS执行h5的全局方法,而h5向app传递参数时,类似于h5发送postMessage,可以在webview的message/onPostMessage监听函数获取数据。

vue

获取webView示例

webView实例的获取,对于vue文件不是特别友好,需要借助于this.$scope.$getAppWebview(),如果是在组件中需要使用this.$parent.$scope.$getAppWebview(),添加延时的原因是,h5页面可能未加载完成,无法获取到对应的全局函数,会提示xxx函数undefined;

<template>
   <web-view src="http://www.juejin.com"></web-view>
</template>
<script>
   export default {
       onReady() {
           const currentWebview = this.$scope.$getAppWebview();
           const account = '清欢bx'
           setTimeout(() => {
               const webView = currentWebview.children()[0];
               webView.evalJS(`setAccountInfo(${account})`);
          }, 1000);
      }
  }
</script>

监听方法

vue文件采用@message触发监听函数

<template>
   <web-view @message="handleMessage" src="http://www.juejin.com"></web-view>
</template>
<script>
   export default {
       methods: {
           handleMessage(data) {
               console.log(data)
          }
      }
  }
</script>

nvue

获取webView示例

在nvue获取webView实例就很流畅了,直接通过this.$refs.webview就能获取到。

<template>
   <web-view ref="webview" src="http://www.juejin.com"></web-view>
</template>
<script>
   export default {
       onReady() {
           const account = '清欢bx'
           this.$refs.webview.evalJs(`setAccountInfo(${account})`);
      }
  }
</script>

监听方法

nvue文件采用@onPostMessage触发监听函数

<template>
   <web-view @onPostMessage="handleMessage" src="http://www.juejin.com"></web-view>
</template>
<script>
   export default {
       methods: {
           handleMessage(data) {
               console.log(data)
          }
      }
  }
</script>

h5 端

发送数据

需要引入一个uni-app的sdk,uni.webview.1.5.4.js,最低版本需要1.5.4,可以在index.html引入,也可以在main.js引入,注意点是传递的参数必须写在data里面,也就是维持这样的数据结构。

uni.postMessage({
   data: {
     xxxxxx,
     xxxxxx
  }
});

如果是页面加载完成时就需要发送数据,需要等待UniAppJSBridgeReady钩子结束后触发postMessage;

<script>
   export default {
       mounted() {
           document.addEventListener('UniAppJSBridgeReady'function() {
               uni.webView.getEnv(function(res) {
                   console.log('当前环境:' + JSON.stringify(res));
              });
               uni.postMessage({
                   data: {
                     action'message'
                  }
              });
          });
      }
  }
</script>

如果是通过事件点击发送数据,因为这时候页面已经加载完成,不需要再去监听UniAppJSBridgeReady钩子,直接触发uni.postMessage即可。

<template>
   <view>
       <button @click="handlePostMessage">发送数据</button>
   </view>
</template>
<script>
   export default {
       methods: {
           handlePostMessage() {
               uni.postMessage({
                   data: {
                     action'message'
                  }
              });
          }
      }
  }
</script>

获取数据

获取数据的函数,需要挂载到window上,可以直接写在main.js里面,数据需要共享到具体页面内,可以使用本地村存储localStorage、事件总线eventBusvuex,根据自己的需求选择。

window.setAccountInfo = function(data) {
   console.log(data)
}

踩坑点

uni is not defined

app需要涉及到离线或者内网,索引uni.webview.js下载到本地进行引入,因为uni.webview.js已经被编译成了umd格式,在vue项目中在进行一次打包后,导致this指向不是window,所以没有把uni挂在到全局上,将this指向改为window即可。

未改造之前的代码


改造后


或者


app向h5传递参数时,无法传递对象,并且传递的参数需要字符串序列化

在传递参数时,对象传递过去没办法识别,同时传递的参数需要执行JSON.stringify(),多个参数时,可以多个参数传递,也可以把多个参数进行字符串拼接,然后再h5端进行拆分处理。

const { accountpassword } = accountInfo;
const _account = JSON.stringify(account);
const _password = JSON.stringify(password);
setTimeout(() => {
   const webView = currentWebview.children()[0];
   webView.evalJS(`setAccountInfo(${_account}, ${_password})`);
}, 1000);

四、性能极差的canvas转图片

自定义组件库里包含手写签名组件,需要用到uni.canvasToTempFilePathcanvas转成图片,这个方法的生成基础图片大小是根据当前屏幕分辨率,在模拟器上运行感觉性能还可以,但是在真机上的性能不高,如果笔画多的话,有时需要十几秒时间,这是没办法接受的,不过也有解决方式,可以通过设置destWidthdestHeight来自定义图片生成的大小,牺牲一些图片清晰度,来提高性能。

uni.canvasToTempFilePath(
  {
     canvasIdthis.canvaId,
     destWidththis.imgWidth,
     destHeightthis.imgHeight,
     success: (res) => {
       console.log('success')
    },
     fail(e) {
       console.error(e);
    },
  },
   this,
);

小结

我目前主要负责公司uni-app移动端框架的开发,包含组件库相应的生态工具多端适配离线存储hybrid,如果你也正在做相同的事,或者在使用uni-app开发,或者在学习uni-app都可以相互探讨,在这踩坑的过程中,我会持续完善此系类文章,帮助大家和自己更好的使用uni-app开发项目,fighting~

作者:清欢bx
来源:juejin.cn/post/7156017191169556511

收起阅读 »

三个多月,被现实雪藏了的锐气

距离 7.15 已经过去了三个多月。体内的热情不能说熄灭,但不足以点燃残留的激情。三个多月不算长,没能把梦想耗尽,遗憾变少;也不算短,没能让想法付出实践,自由变得可贵。回看自己的脚印,有延续,也有分叉,但只有少数几步能够留下青草的芳香,大部分脚印风干后让人难以...
继续阅读 »

距离 7.15 已经过去了三个多月。体内的热情不能说熄灭,但不足以点燃残留的激情。三个多月不算长,没能把梦想耗尽,遗憾变少;也不算短,没能让想法付出实践,自由变得可贵。回看自己的脚印,有延续,也有分叉,但只有少数几步能够留下青草的芳香,大部分脚印风干后让人难以辨认,甚至不相信是从前的自己。像溅到了油渍的白衬衫,但不同的是,白衬衫还有机会回到出厂时的样子。

二十多岁也不是一个少不经事的年纪了,随着自我意识变强,属于自己的世界观正在构建。慢慢的对生活、工作、理想有了自己的看法。当发现课本上所教授的和社会需要你所掌握的相差甚远,自己也就有了想逃避的情绪。不愿随波逐流,像片枯黄的叶子飘落在水中,没有方向。路人不会因为一片叶子而驻足,或许有少部分人会感叹,在寒冬里积极汲取的养分敌不过春去秋来的自然规律和经典力学中的万有引力。


手的一个作用就是捂住耳朵,隔绝噪音

三个多月的时间,锻炼了写作,结识了新朋友,捡起了读书时最爱的篮球,经历疫情,重温友情,放空自己。

惊喜

写作是带来惊喜最大的尝试,分享技术的同时加深了对于某项知识点的理解,并且还能提升自己的文笔,让自己在面试中多一个加分项。在这个过程中,自己还加了一位远在厦门独立开发者的微信,大家分享想法,交流行业的运作和进程,交换资源,挺好。虽然写作没能带来实际的收益,但也让我在点滴生活中找到一点满足感。在这也想问下,大家最近是都成了 优秀毕业生吗 ?两个多月前写的一篇 一位 98 年程序员离职后 的阅读量和点赞量最近又多了起来,是不是当看到 离职 这个字眼,都想点进去看两眼。当然我也希望各位能从中找到了一些方向和归属感,让自己疲惫的心能得到片刻的缓解。毕竟大家在两微一抖上看到了很多的精英人士,觉得全世界都在挣着自己的钱。

友情

联系了初中同学、大学同学。先说说初中同学,距今认识已超过 10 年。初中时,在同一屋檐下共同生活了三年,也有幸 “同床异梦” 过。到现在也还记得初中时那啼笑皆非的日子,一起罚站过,像展品一样立在操场被路过的人打量;一起挑灯夜聊,讲着班里的女生和男生,调侃授课老师,一遍遍模仿着他们略带喜感的动作和回荡在耳边的经典语录;期末前在宿舍厕所嚼着白加黑,背着让人痛不欲生的课文;上午的最后一声下课铃响后,等在对方的桌前或课室外,一起走向校园中承载了不少话题量的饭堂。

毕业后的初中同学也还是会有联系。前些日子和他们吃了几顿饭。我想想啊,有东南亚菜、江浙菜、新疆菜、顺德菜、粤菜。对这几个菜系排序,粤菜 = 顺德菜 > 东南亚菜 > 新疆菜 > 江浙菜。


西湖龙井虾,姿色不错,口感上个人认为没什么很特别的


顺德醉鹅,肉多,对得起这价


新疆烤包子,牛肉馅的,皮薄馅多,不错


罗布泊烤鱼,唯一一道让我们产生分歧的菜,只因我只吃了我面前的那面,他吃另一面


咕噜肉,酸甜的口感,不腻

一个进入了广告业,刚找到了工作,结束了家里蹲的日子;另一个成了编导,不加班,目前积累作品中;而我正试图通过写作和运动摆脱焦虑和迷茫带来的副作用。

大学同学当了两年兵,九月份退伍回来。外表变化不大,但性格变了挺多。大学做事时会想的比较多,属于那种给了机会都要思前想后的,现在呢?没有机会,创造机会都要上,胆子大了不少。在兵营里不仅身体得到了锻炼,心态更是被蹂躏到需要推倒重建。跟他交流时,能感觉到和社会脱离的有点久,有些事情想的过于简单。但对朋友,他还像从前那样。

篮球

女篮世界杯夺得了第二名的好成绩,国庆在家看了世界杯的几场比赛。比男篮强的不是一星半点,不管是队内的配合、队员的基本功、防守和进攻的态度都让我感觉女篮未来可期。这次世界杯也让我回忆起以前那块让我无比留恋的场地,不管是水泥地还是塑胶地,篮架是崭新还是磨损,边界是清晰还是模糊,这些元素加起来都足以让那时的我顶着烈日,不厌其烦的追逐着那颗用青春编织起的篮球,一上一下也像极了年轻人那有力的心跳。


我追过的人不多,但追过的球,不少

上班后打球的时间呈指数级下降。原以为对篮球的热爱就到这了,但把手从键盘放到篮球上时,体内有关它的一切都被唤醒了。


灯光一点都不耀眼

疫情后踏上球场,竟然有种疫情从未发生过的错觉。不用戴口罩,每个人分享着球权,对抗时肌肉之间的碰撞让人忘记了在这两年里不停被提起的一米安全线。如果有读者也在广州,也爱打球,可以私信我约场球,让自己痛快一场,酣畅淋漓。

放空自己

脑子空着的时候,大部分想的是创业的东西。创业这个想法从大学时期就有了,但不具备所需的条件,于是一门心思的想横向扩展。结果出来后又开始纠正大学时的想法,横向扩展行不通,个人认为一万小时定律忽略了实际环境对结果的影响,于是决定踹开这扇门。鉴于本人对于创业还是萌新一个,就不花篇幅了。有兴趣的,私下交流。

放空自己的时候,除了想创业,也想过其他东西。比如,自己的优势是啥,如何能把优势更好的发挥出来,如何说服自己目前是个平凡人的事实,为啥 boss 直聘上一堆已读未回等等。

作者:对方正在输入
来源:juejin.cn/post/7158817364467777550

收起阅读 »

离职交接,心态要好

话说今年经历了几次项目交接?主动和被动的都算!01实在是没想到,都到年底快收尾的时候,还要突然接手离职人员的项目;不断拉扯和管理内心情绪,避免原地裂开;年度中再次经历突发的交接事宜,并且团队要在极短的时间内完成所有事项的交接流程;毫无征兆的变动必然会引起一系列...
继续阅读 »

话说今年经历了几次项目交接?主动和被动的都算!

01

实在是没想到,都到年底快收尾的时候,还要突然接手离职人员的项目;

不断拉扯和管理内心情绪,避免原地裂开;

年度中再次经历突发的交接事宜,并且团队要在极短的时间内完成所有事项的交接流程;

毫无征兆的变动必然会引起一系列问题,最直接的就是影响团队现有节奏进度,需要重新调整和规划;

人员的小规模变动,对部门甚至公司产生的影响是显而易见的,道理都懂;

但是从理性上思考,这个问题并非是无解的,是可以在各个团队中,进行内部消化的;

而人力减少带来的成本降低,以及确保公司的可持续,这是极具确定性的,也是核心目的;

所以感性上说,这个梦幻的职场,可能真的是"爱了";

02

如果是常规情况下的离职流程,交接并不是一件复杂的事情,因为有时间有心情来处理这事,好聚好散;

然而最骚的是,奇袭一般的裁员手段,几分钟谈话结束直接走人;

丝毫不顾及由此带来的影响,认定留下的人应该兜底相应的责任,实现无缝接坑;

当然并不是什么公司都有底气这么做的,大部分还是在裁员通知后,留有一定的时间处理交接事项;

对于交的过程是否有质量,完全看接的一方是否聪明;

从感性上分析,都已经被裁了自然要牢牢把握摸鱼的机会,根本不会在意交出的事项谁来维护,不反越防线就不错了;

而压力会直接传送后闪现到接的人正上方;

03

面对被动离职的交接,确实很难妥善处理,情绪化容易导致事情变质,能真正理性对待的并不多;

交接涉及到三方的核心利益:公司、交出人、接手人,不同角度对待这件事件,态度完全不同;

公司,并不关心交接的质量,只要项目有人兜底即可;

交出方,感性上说直接敷衍交接单上的流程即可,并不在意后续的影响;

接手方,项目交接完成后的第一责任人,可能会关心项目的质量状况;

至于说接手的人能否有时间,有能力,有心情接下这种天降大任,可能除了自己以外,不到出问题的时候关注的很少;

因为项目交接过程没有处理好,从而导致后续的事故与甩锅,情绪化的现象并不少见;

如果是在内部矛盾突出的团队中,由此引发的离职效应也并不少见;

04

人的情绪真的是很奇怪,能让复杂的事情变的简单,也能让简单的事情变的离谱;

情绪上头的时候,事情本身是否真的复杂就已经不太重要了;

接手方最大的问题在于吃力不讨好,如果接了一个质量奇差的项目,意味之后很长一段时间内,工作状态都会陷入混乱的节奏中;

对于大部分研发团队来说,都是存在排期规划的,如果被交接的项目横插一脚,重新调规划影响面又偏大;

向上反馈,多半是回答一句:自行消化;

何谓自行消化,就是占用空闲时间处理,比如下班后,比如周末,比如摸鱼,这些都是对工作情绪的持续伤害;

最终兜底的个人或者团队,可能需要带着夜宵去公司搬砖;

05

吐槽归吐槽,裂开归裂开,成熟的搬砖人不该表现出明显的情绪化;

先捋一捋在面对离职交接时的注意事项,虽然说离职后有一个过渡期,但是真正涉及交接的时间通常一周左右;

作为接手一方,自然期待的是各种文档齐全,对于坑坑洼洼的描述足够清楚;

然而对于被离职的交出方,会带着若隐若现的情绪化状态,很难用心处理交接事项,能不挖坑就已经是良心队友了;

接手方作为后续的兜底人员,兜不住就是一地鸡毛;

如果兜住了呢?那是职责所在、理所应当、不要多想、安心搬砖;

06

面对项目交接,这种隔三差五个月就会突发的事,完全可以用一套固定的模式和节奏去执行;

强烈建议:不排斥、不积极、不情绪化;

但是在处理的过程中要理性且严谨,这样可以规避掉许多可能出现的麻烦,毕竟签了交接单,从此该项目问题根本甩不开;

职场几年,在多次"交"与"接"的角色转换过程中,总结以下几点是研发需要注意的;

P1:文档,信息的核心载体;

不管项目涉及多少文档,照单全收;

如果文档严重缺失甚至没有,直接在交接单上写明情况,并且得加粗划重点展示;

文档和项目的维护极有可能是线性不相关,但是手有文档心里不慌,因为方便后续再把项目交接给其他人;

所以,敷衍一时爽,出事火葬场;

07

P2:代码工程,坑与不坑全看此间;

接到手里的项目,是否会导致情绪崩塌,全看项目代码工程的质量,遇上一堆烂摊子,心情会持续的跌跌跌,然后裂开;

直接把人打包送走的情况也并不少见;

如果代码工程质量极高,架构设计稳定,组件集成比较常规,分包井然有序,悬着的情绪可以适当下落;

P3:库表设计,就怕没注释;

对于数据库层面的设计,与代码工程和业务文档三者相辅相成,把握其中的主线逻辑即可;

但前提是表的设计得有清晰的注释,如果是纯中式英文混搭拼音,且缺乏注释,必然会成为解决问题的最佳卡点;

P4:核心接口,应当关注细节;

从项目的核心业务中选出2-3个复杂的接口读一读;需要将注意点放在细节逻辑上,给内心积蓄一丢丢解决问题的底气;

熟悉接口的基本思路:请求从客户端发出,业务服务的处理逻辑,对数据层面的影响,最终响应的主体;

08

P5:遗留问题,考验职场关系的时候到了;

公司一片祥和的时候,员工之间还可以做做样子;

但是已经走到了一别两宽的地步,从感性上来说只要不藏着掖着就行,还想窥探别人安稳摸鱼的秘密,确实想的不错;

老练的开发常干的事,为了解决某个问题临时上线一段代码,处理好后关闭触发的入口,但是会保留代码主体;

这还算常规操作,最骚的是在本地写一段脚本工具解决线上的问题;

这些隐藏的接口和脚本只有开发的人自己清楚,如果不给个说明文档,这不单是挖坑,还顺手倒了一定比例的水进行混合;

P6:结尾事项,寒暄几句还是要的;

安全意识好的公司,会对员工的账号权限做好备份,以便离职时快速处理,不会留下风险隐患;

在所有权限关闭之后,接手人就可以在交接单上完成签字仪式;

交接完成后还是得适当的寒暄几句,万一接了个坑,转头就得再联系也不稀奇,所以职场留一线方便语音再连线;

09

年度收到的离职交接,已经累计好几份,对这种事情彻底麻了;

事来了先兜着,等兜不住的时候自然会有解决办法;

抗拒与烦躁都不会影响流程的持续推进,这种心态需要自己用清醒的意识不断的说服自己;

最后想探讨一个话题,跟项目前负责人联系,用什么话术请教问题,才能显得不卑不亢?

作者:知了一笑
来源:juejin.cn/post/7157651258046677029

收起阅读 »

Flutter之事件节流、防抖封装

在应用开发过程中经常会遇到因用户短时间内连续多次重复触发某个事件,导致对应事件的业务逻辑重复执行而出现业务异常,此时就需要对事件进行节流或者防抖处理避免出现业务异常。本文将介绍在 Flutter 开发中如何实现节流和防抖的统一封装。 前言 首先我们来了解一下节...
继续阅读 »

在应用开发过程中经常会遇到因用户短时间内连续多次重复触发某个事件,导致对应事件的业务逻辑重复执行而出现业务异常,此时就需要对事件进行节流或者防抖处理避免出现业务异常。本文将介绍在 Flutter 开发中如何实现节流和防抖的统一封装。


前言


首先我们来了解一下节流和防抖的定义,以及在什么场景下需要用到节流和防抖。


节流


节流是在事件触发时,立即执行事件的目标操作逻辑,在当前事件未执行完成时,该事件再次触发时会被忽略,直到当前事件执行完成后下一次事件触发才会被执行。


throttle.png


按指定时间节流


按指定时间节流是在事件触发时,立即执行事件的目标操作逻辑,但在指定时间内再次触发事件会被忽略,直到指定时间后再次触发事件才会被执行。


throttle-timeout.png


防抖


防抖是在事件触发时,不立即执行事件的目标操作逻辑,而是延迟指定时间再执行,如果该时间内事件再次触发,则取消上一次事件的执行并重新计算延迟时间,直到指定时间内事件没有再次触发时才执行事件的目标操作。


debounce.png


使用场景


节流多用于按钮点击事件的限制,如数据提交等,可有效防止数据的重复提交。防抖则多用于事件频繁触发的场景,如滚动监听、输入框输入监听等,可实现滚动停止间隔多久后触发事件的操作或输入框输入变化停止多久后触发事件的操作。


效果


先看一下最终封装完成后的使用示例及效果,实现计数器功能,对点击分别进行节流、指定时间节流、防抖限制。


/// 事件目标操作
void increase() {
count += 1;
}

/// 节流
() async{
await Future.delayed(Duration(seconds: 1));
increase();
}.throttle()

/// 指定时间节流
increase.throttleWithTimeout(2000)

///防抖
increase.debounce(timeout: 1000)


increase 是事件目标操作,即这里的数字加一,分别进行节流、指定时间节流、防抖限制,调用封装的 throttlethrottleWithTimeoutdebounce 扩展方法实现。其中节流为了模拟事件耗时操作增加了一秒延迟。


实现效果:


flutter-throttle-demo.gif


实现


接下来将通过从单事件的节流/防抖限制到封装抽取一步一步实现对节流和防抖的通用封装。


简单节流实现


首先来看一下节流的简单实现,前面讲了节流的原理,就是在事件未执行完成时忽略事件的再次触发,根据这个原理添加一个变量标识事件是否可执行,默认为 true 可执行,当事件执行时设置为 false,执行完成后重新设置为 true,当标识为 false 时忽略事件,这样就实现了对事件的节流,代码实现如下:


Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}

/// 事件是否可执行
bool enable = true;

void throttleIncrease() async{
if(enable){
enable = false;
await increase();
enable = true;
}
}

添加一个 enable 变量标识事件是否可执行。这里为了模拟事件的耗时操作在 increase 方法里添加了一秒的延时。这样就简单实现了事件的节流,运行看一下效果:


flutter-throttle-simple.gif


节流封装


通过上面的简单代码实现了对事件的节流,但是只对某一个确定的事件有效,如果还有其他事件也需要实现节流效果那就得重新写一遍上面的代码,这样很明显是不科学的。那么我们就需要对上面的代码进行封装,使其能应用到多个事件上。


上面的代码事件调用是直接写在节流的实现里的,那么将事件进行抽象,把事件的具体执行方法抽取为一个参数,这样就能满足多个事件的节流控制了,实现如下:


bool enable = true;
void throttle(Function func) async{
if(enable){
enable = false;
await func();
enable = true;
}
}

/// 使用
throttle(increase);

throttle(decrease);

进一步封装


经过前面的封装后,确实可以对多个事件进行节流限制,但在实际开发过程中发现有两个问题:


**问题一:**所有事件的节流控制使用的是一个 enable 变量控制,这样就会导致在事件 1 执行过程中事件 2 会被忽略,这显然不是我们想要的效果。


举一个典型的场景,在 Flutter 中跳转新页面并获取页面的返回值,此时实现如下:


Future toNewPage() async{
var result = await Navigator.pushNamed(context, "/newPage");
/// do something
}

此时如果对 toNewPage 进行节流控制,并且跳转的页面里的按钮事件也做了同样的节流控制,就会导致新界面的按钮事件无法执行,因为我们节流用的是同一个变量进行控制,而 toNewPage 需要接收页面返回值,事件未执行完一直在等待页面返回值导致 enable 变量一直为 false 所以新界面的点击事件就会被忽略。


**问题二:**当事件的执行报错,会导致后续所有使用该方式节流的事件都不会被触发。原理跟上面的一样,当事件执行报错时不会继续向下执行,此时 enable 无法赋值为 true,一直为 false 从而导致后续事件都不会被执行。


怎么解决上面两个问题呢?首先解决简单的问题二,问题二很好解决,加一个 try-catch-finally 即可:


void throttle(Function func) async{
if(enable){
enable = false;
try {
await func();
} catch (e) {
rethrow;
} finally {
enable = true;
}
}
}

在方法调用上增加 try-catch-finally ,在 finally 中将 enable 设置为 true,在 catch 中不对异常做任何处理,使用 rethrow 将异常重新抛出去即可,这样就解决了问题二。


再来看问题一,既然使用同一个 enable 会有问题,那就使用多个变量来控制,每个事件用一个 enable 变量来控制,实现如下:


Map _funcThrottle = {};

void throttle(Function func) async{
String key = func.hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if(enable){
_funcThrottle[key] = false;
try {
await func();
} catch (e) {
rethrow;
} finally {
_funcThrottle.remove(key);
}
}
}

使用一个 Map 来存放事件的 enable 变量,使用事件方法的 hashCode 作为事件的 key,这样就解决了问题一。


但实际开发过程中发现还是有问题,封装后的 throttle 方法在使用时有下面两种方式:


/// 1
Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}

throttle(increase);

/// 2
throttle(() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
});

使用第一种方式时是没有问题,但是第二种发现就有问题,节流不起作用了,为什么呢?是因为第二种使用的是匿名函数或者叫 lambda 函数,这种方式每次触发事件相当于都会重新创建一个函数参数传入 throttle 就会导致 func.hashCode.toString() 获取的值每次都不一样,所以导致节流无效。


那这种情况又该怎么解决呢?首先想到的是给 throttle 增加一个参数 key ,不同的事件传入不同的 key 值。这样确实能解决问题,但是增加了使用成本,每个事件都得传入一个 key,对于已有代码改造也相对来说不方便。于是想到了另外一种解决办法,也是本方案最终实现的方法,用一个对象来代理执行事件,具体实现如下:


class FunctionProxy {
static final Map _funcThrottle = {};
final Function? target;

FunctionProxy(this.target);

void throttle() async {
String key = hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if (enable) {
_funcThrottle[key] = false;
try {
await target?.call();
} catch (e) {
rethrow;
} finally {
_funcThrottle.remove(key);
}
}
}
}

创建一个方法的代理类,在该类里实现 throttle ,此时使用的 key 为该代理类的 hashCode , 使用如下:


onPressed: FunctionProxy(increase).throttle
/// or
onPressed: FunctionProxy(() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}).throttle

这样最终返回给 onPressed 的是 FunctionProxythrottle 函数,而 throttle 是一个确定的函数,这就最终解决了上述问题。


但是使用时每次都创建 FunctionProxy 类,看着不太友好,给 Function 增加一个 throttle 方法,让使用更加简单:


extension FunctionExt on Function{
VoidCallback throttle(){
return FunctionProxy(this).throttle;
}
}

/// 使用
Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}

onPressed: increase.throttle()

onPressed: () async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}.throttle()

指定时间节流


节流是事件执行完后才允许下次事件执行,指定时间节流是事件开始执行指定时间后允许下次事件执行,使用延迟指定时间后将 enable 设置为 true 来实现,代码如下:


class FunctionProxy {
static final Map _funcThrottle = {};
final Function? target;

final int timeout;

FunctionProxy(this.target, {int? timeout}) : timeout = timeout ?? 500;

void throttleWithTimeout() {
String key = hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if (enable) {
_funcThrottle[key] = false;
Timer(Duration(milliseconds: timeout), () {
_funcThrottle.remove(key);
});
target?.call();
}
}
}

增加了 timeout 参数,即指定的节流时间,使用 Timer 延迟指定时间后将 key 从 _funcThrottle 中移除,这里没有加 try-catch ,因为事件异常并不会影响 Timer 的执行,同样的为 Function 增加一个 throttleWithTimeout 扩展:


extension FunctionExt on Function{
VoidCallback throttleWithTimeout({int? timeout}){
return FunctionProxy(this, timeout: timeout).throttleWithTimeout;
}
}

使用:


Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}
onPressed: increase.throttleWithTimeout(timeout: 1000)

onPressed: () async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}.throttleWithTimeout(timeout: 1000)

防抖


防抖是在事件触发指定时间内该事件未再次触发时再执行,同样可以使用 Timer 来实现:


class FunctionProxy {
static final Map _funcDebounce = {};
final Function? target;

final int timeout;

FunctionProxy(this.target, {int? timeout}) : timeout = timeout ?? 500;

void debounce() {
String key = hashCode.toString();
Timer? timer = _funcDebounce[key];
timer?.cancel();
timer = Timer(Duration(milliseconds: timeout), () {
Timer? t = _funcDebounce.remove(key);
t?.cancel();
target?.call();
});
_funcDebounce[key] = timer;
}
}

同样增加 timeout 参数用于指定防抖的时间间隔,与节流不同的是防抖的 Map 的 value 不是 bool 类型而是 Timer 类型,当事件触发时创建一个 Timer 设置延迟 timeout 后执行,并将 Timer 添加到 Map 中,如果在 timeout 时间内事件再次触发则将 Map 中的 Timer 取消再重新创建新的 Timer,从而实现防抖效果。


同样为 Function 添加 debounce 防抖扩展方法:


extension FunctionExt on Function{
VoidCallback debounce({int? timeout}){
return FunctionProxy(this, timeout: timeout).debounce;
}
}

使用:


Future increase() async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}
onPressed: increase.debounce(timeout: 1000)

onPressed: () async{
count += 1;
await Future.delayed(const Duration(seconds: 1));
}.debounce(timeout: 1000)

完整代码


至此节流、防抖的封装就完成了,完整代码如下:


extension FunctionExt on Function{
VoidCallback throttle(){
return FunctionProxy(this).throttle;
}

VoidCallback throttleWithTimeout({int? timeout}){
return FunctionProxy(this, timeout: timeout).throttleWithTimeout;
}

VoidCallback debounce({int? timeout}){
return FunctionProxy(this, timeout: timeout).debounce;
}
}

class FunctionProxy {
static final Map _funcThrottle = {};
static final Map _funcDebounce = {};
final Function? target;

final int timeout;

FunctionProxy(this.target, {int? timeout}) : timeout = timeout ?? 500;

void throttle() async {
String key = hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if (enable) {
_funcThrottle[key] = false;
try {
await target?.call();
} catch (e) {
rethrow;
} finally {
_funcThrottle.remove(key);
}
}
}

void throttleWithTimeout() {
String key = hashCode.toString();
bool enable = _funcThrottle[key] ?? true;
if (enable) {
_funcThrottle[key] = false;
Timer(Duration(milliseconds: timeout), () {
_funcThrottle.remove(key);
});
target?.call();
}
}

void debounce() {
String key = hashCode.toString();
Timer? timer = _funcDebounce[key];
timer?.cancel();
timer = Timer(Duration(milliseconds: timeout), () {
Timer? t = _funcDebounce.remove(key);
t?.cancel();
target?.call();
});
_funcDebounce[key] = timer;
}
}

点击组件封装


完成对节流、防抖的封装后,我们还可以对点击组件进行封装,这样不管是对现有 Flutter 代码还是新代码增加节流、防抖功能都会更加的简单。比如对 GestureDetector 组件可做如下封装:


enum ClickType {
none,
throttle,
throttleWithTimeout,
debounce
}

class ClickWidget extends StatelessWidget {
final Widget child;
final Function? onTap;
final ClickType type;
final int? timeout;

const ClickWidget(
{Key? key,
required this.child,
this.onTap,
this.type = ClickType.throttle,
this.timeout})
: super(key: key);

@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
child: child,
onTap: _getOnTap(),
);
}

VoidCallback? _getOnTap() {
if (type == ClickType.throttle) {
return onTap?.throttle();
} else if (type == ClickType.throttleWithTimeout) {
return onTap?.throttleWithTimeout(timeout: timeout);
}else if (type == ClickType.debounce) {
return onTap?.debounce(timeout: timeout);
}
return () => onTap?.call();
}
}

增加 type,用于指定节流、指定时间节流、防抖或者不限制,分别调用对应的方法。默认为节流,可根据项目实际需求设置默认方式或对项目中使用到的其他点击组件进行封装,经过封装后,修改已有代码增加默认限制功能就可以直接替换组件名字而无需改动其他代码实现事件限制的功能。


使用:


/// before
GestureDetector(
child: Text("xxx"),
onTap: increase,
)
/// after
ClickWidget(
child: Text("xxx"),
onTap: increase,
)

ClickWidget(
child: Text("指定时间节流"),
type: ClickType.throttleWithTimeout,
timeout: 1000,
onTap: increase,
)

ClickWidget(
child: Text("防抖"),
type: ClickType.debounce,
timeout: 1000,
onTap: increase,
)

总结


开发过程中,大部分的事件处理都需要进行节流或者防抖限制,防止事件的重复处理导致业务的异常,经过封装后不管是对老项目的优化改造还是新项目的开发,节流和防抖的处理都将变得更简单快捷。


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

常见的Android编译优化问题

编译常见问题 在开发过程中,有碰到过一些由于编译优化导致的代码修改并不符合我们预期的情况。这也就是之前为什么我经常说编译产物其实是不太可以被信任的。 方法签名变更,底层仓库的方法变更但是上层模块并没有跟随一起重新编译导致的这个问题。 常量优化,将一些常量的调...
继续阅读 »

编译常见问题


在开发过程中,有碰到过一些由于编译优化导致的代码修改并不符合我们预期的情况。这也就是之前为什么我经常说编译产物其实是不太可以被信任的。



  1. 方法签名变更,底层仓库的方法变更但是上层模块并没有跟随一起重新编译导致的这个问题。

  2. 常量优化,将一些常量的调用点直接替换成常量的值。

  3. 删除空导包, 没有用的一些导包就会做一次剔除。


最近倒霉了


我们最近碰到一个pipeline相关而且很妖怪的问题。我们一个pipeline会检查apk产物中是否存在异常的方法调用,就是之前介绍的在R8的基础上开发出来的A8。但是最近有一个类被删除了之后呢,但是代码中还有一处调用点。但是这个检测竟然被通过了,然后这部分代码就被合入了master。


image.png


这个引用的文件就如上图所示,是一个debug buildType中的,所以并不是所有的apk中都会存在这部分代码。


然后呢,这个MergeRequest就被合入了master分支,因为当天是我们出下一个版本包的时间,然后交付给测试的就是全量编译的debugrelease包。别的开发同学rebase完master之后就发现piepline都跑不过了,就导致了他们当天的代码无法被合入。


这个就是事情大概的起因和经过,但是各位有没有想过为什么会发生这个问题吗。这个是不是我们的pipeline出现了bug,导致了这种问题无法被识别出来了呢。


以前有说过,如果简单的说我们的快编系统就是把模块替换成对应的aar,从而达到编译提速。所以因为我们使用的是这个模块对应的aar产物,所以大概率就是因为这个模块的编译产物和源代码有差异导致了这个问题。


其实这个问题一出现我就已经知道大概率是由空导包优化导致的这个问题,因为在pipeline检查的时候,检测的apk产物中确实不存在这个导包。因为我们使用的是一个历史版本的aar,其中无效导包的部分已经被编译器做了删除空导包的优化了。接下来我们看下我写的一个demo中的无效导包。


image.png


image.png


图一呢是源代码java文件,图二呢则是jar包中的代码。可以简单的看出来行号呢是可以对应的上的,但是这个AppCompatActivity的无效导包在产物中已经被优化掉了。这里也就回答了在编译过程中会保留行号,但是也会优化掉一部分不需要的代码,让我们编译出来的产物更小。


所以也就导致了我们的产物和我们的源代码之间的差异,另外一个角度就是说从apk中我们确实是不存在这个类的导包。但是呢在我们把这部分代码重新编译成aar的时候,就会出现source缺失,导致的语法树无法生成,之后导致的编译失败问题。


这也就是所以我一直和大家说编译产物是不可以被信任的呢。


以前倒霉过


这个是之前的一个故事了,我们之前呢在模块中定义了一些静态常量吧,然后用来标识当前SDK的版本,然后这个值在别的模块中被引用到了。


有一次因为需求变更,我们更改了这个静态变量的值,然后呢我就把这个需求提测了。之后测试反馈给我为什么这边的这个值没有变化啊。


image.png


我的天,当时我就是这样,发生了什么情况。然后呢我全量打了个包好了,我当时也就以为只是编译时的一个bug而已。然后后来呢,我查了下资料发现这个就是一个java编译时的常量优化问题。过了一阵子吧,我面试了下字节跳动,然后我和面试官也聊了下这个话题,然后呢在这个方法签名变更的问题上,当时我略输一筹,哈哈哈哈。接下来我们就看下一个demo。


image.png


image.png


图1呢也是java代码,图2呢则是aar中的编译产物。其中我们可以看到,这个静态常量在编译成产物之后就会被编译成这样。


所以这个就解释了我一开始碰到的这个问题,他就是由于我们的编译器已经把aar中的这部分静态常量编译成了直接的值,然后呢我们的源变化之后如果没有重新编译对应的模块,就会导致这个值一直无法被更新到最新的值。


结论


如果大家对安卓编译相关有兴趣的话,这些问题很可能都会在面试的时候被问到。希望这不仅仅只是一篇我对于这些问题的思考,也能对各位有所帮助吧。


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

KeyValueX:消除样板代码,让 Android 项目不再 KV 爆炸

背景源于深夜一段独白:Key Value 定义几十上百个是常见事,目前有更简便方式么,此为项目中为数不多不受控制之地,指数膨胀,且易埋下一致性隐患,每新增一 value,需兼顾 key、get、put、init,5 处 …public class Config...
继续阅读 »

背景

源于深夜一段独白:

Key Value 定义几十上百个是常见事,目前有更简便方式么,

此为项目中为数不多不受控制之地,指数膨胀,且易埋下一致性隐患,

每新增一 value,需兼顾 key、get、put、init,5 处 …

public class Configs {
 
...
   
 private static int TEST_ID;
 
 public final static String KEY_TEST_ID = "KEY_TEST_ID";
 
 public static void setTestId(int id) {
   TEST_ID = id;
   SPUtils.getInstance().put(KEY_TEST_ID, id);
}
 
 public static int getTestId() {
   return TEST_ID;
}
 
 public static void initConfigs() {
   TEST_ID = SPUtils.getInstance().getInt(KEY_TEST_ID, 0);
}
}

随后陆续收到改善建议,有小伙伴提到 “属性代理”,并推荐了群友 DylanCai 开源库 MMKV-KTX

与此同时,受 “属性代理” 启发,本人萌生 Java 下 key、value、get、put、init 缩减为一设计。

Github:KeyValueX

V1.0

V1.0 使用 3 步曲

1.如读写 POJO,需实现 Serializable 接口

public class User implements Serializable {
 public String title;
 public String content;
}

2.像往常一样,创建项目配置管理类,如 Configs

//Configs 中不再定义一堆 KEY、VALUE 常量和 get、put、init 静态方法,
//只需一条条 KeyValue 静态变量:

public class Configs {
 public final static KeyValueString accountId = new KeyValueString("accountId");
 public final static KeyValueSerializable<User> user = new KeyValueSerializable<>("user");
}

3.在页面等处通过 get( ) set( ) 方法读写 KeyValue

public class MainActivity extends AppCompatActivity {
...
         
 //测试持久化写入
 Configs.user.set(u);

 //测试读取
 Log.d("---title", Configs.user.get().title);
 Log.d("---content", Configs.user.get().content);
}

V1.0 复盘

KeyValueX v1.0 一出,很快在群里引发热议,群友 DylanCai 提到多模块或存在 KeyName 重复问题,群友彭旭锐提议通过 “注解” 消除重复等问题。

与此同时我也发现,KeyValueX 虽然已消除 key、value、get、put、init 样板代码,但还是有两处一致性问题:

final 修饰符和 KeyName 一致性,

—— final 在 Java 下必写,以免开发者误给 KeyValue 直接赋值:

public class MainActivity extends AppCompatActivity {
...
     
 //正常使用
 Configs.user.set(u);
 
 //误用
 Configs.user = u;
}

那么有开发者可能说,我每次新增 KeyValue,通过 ctrl + D 复制行不就行了

public class Configs {
 public final static KeyValueString accountId = new KeyValueString("accountId");
 public final static KeyValueBoolean isAdult = new KeyValueBoolean("accountId");
}

确实这样可解决 final 一致性,但同时也会滋生 KeyName 一致性问题,也即记得改 KeyValue 变量名,忘改 KeyName,且这种疏忽编译器无法发现,唯有线上出事故专门排查时才可发现。

故综合多方面因素考虑,v2.0 采取注解设计:

V2.0

V2.0 使用 3 步曲

1.创建 KeyValueGroup 接口

@KeyValueGroup
public interface KeyValues {
 @KeyValue KeyValueInteger days();
 @KeyValue KeyValueString accountId();
 @KeyValue KeyValueSerializable<User> user();
}

2.像往常一样,创建项目配置管理类,如 Configs

//Configs 中不再定义一堆 KEY、VALUE 常量和 get、put、init 静态方法,
//只需一条 KeyValueGroup 静态变量:

public class Configs {
 public final static KeyValues keyValues = KeyValueCreator.create(KeyValues.class);
}

3.在页面等处通过 get( ) set( ) 方法读写 KeyValue

public class MainActivity extends AppCompatActivity {
...
         
 //测试持久化写入
 Configs.keyValues.user().set(u);

 //测试读取
 Log.d("---title", Configs.keyValues.user().get().title);
 Log.d("---content", Configs.keyValues.user().get().content);
}

V2.0 复盘

V2.0 通过接口 + 注解设计,一举消除 final 和 KeyName 一致性问题,

且通过无参反射实现 KeyValues 的实例化,使写代码过程中无需特意 build 生成 Impl 类,对 build 一次便需数分钟的巨型项目较友好。

根据上图可见,无参反射加载类,耗时仅次于 new,故 V2.0 设计本人还算满意,已在 Java 项目中全面使用,欢迎测试反馈。

Github:KeyValueX

KeyValue-Dispatcher

期间群友 DylanCai 提出可改用动态代理实现,也即效仿 Retrofit,根据接口定义运行时动态生成方法,

如此无需声明注解,使接口定义更简洁,看起来就像:

public interface KeyValues {
 KeyValueInteger days();
 KeyValueString accountId();
 KeyValueSerializable<User> user();
}

与此同时,可根据适配器模式实现个转换器,例如转换为 UnPeek-LiveData,如此即可顺带完成高频操作 —— 更新配置后通知部分页面刷新 UI。

public interface KeyValues {
Result<Integer> days();
Result<String> accountId();
Result<User> user();
}

Configs.keyValues.days().observer(this, result -> {
...
});

不过动态代理有个硬伤,即类名方法名不可混淆,不然运行时难调到对应方法,

故动态代理方式最终未考虑,不过转换器设计我甚是喜欢,加之 Java 后端 Properties 启发,故萌生 Dispatcher 设计 ——

基于 MVI-Dispatcher 实现 KeyValue-Dispatcher。具体思路即通过 HashMap 内聚 KeyValue,如此只需声明 Key,而无需考虑 value、getter、setter、init:

KV-D 使用 3 步曲

1.定义 Key 列表

public class Key {
 public final static String TEST_STRING = "test_string";
 public final static String TEST_BOOLEAN = "test_boolean";
}

2.读写

//读
boolean b = GlobalConfigs.getBoolean(Key.TEST_BOOLEAN);
//写
GlobalConfigs.put(Key.TEST_STRING, value);

3.顺带可通知 UI 刷新

GlobalConfigs.output(this, keyValueEvent -> {
 switch (keyValueEvent.currentKey) {
   case Key.TEST_STRING: ... break;
   case Key.TEST_BOOLEAN: ... break;
}
});

依托 MVI-Dispatcher 消息聚合设计,任何由配置变更引发的 UI 刷新,皆从这唯一出口响应。

目前已更新至 MVI-Dispatcher 项目,感兴趣可自行查阅。

KV-D 复盘

KV-D 旨在消除学习成本,让开发者像 SPUtils 一样使用,与此同时自动达成内存快读、消除样板代码、规避不可预期错误。

不过 KV-D 只适合 Java 项目用。如欲于 Kotlin 实现属性代理,还需基于 KeyValueX 那类设计。

于是 KeyValueX 再做升级:

1.简化注解:只需接口处声明此为 KeyValueX 接口,

2.自动分组:以 KeyValueX 接口为单位生成路径 MD5,KeyName 根据 MD5 自动分组,

3.全局内存快读:如 ViewModelProvider 使用,并提供全局内存快读。

V3.0

V3.0 使用 2 步曲

1.创建 KeyValueGroup 接口,例如

@KeyValueX
public interface Configs {
 KeyValueInteger days();
 KeyValueString accountId();
 KeyValueSerializable<User> user();
}

2.在页面等处通过 get( ) set( ) 方法读写 KeyValue

public class MainActivity extends AppCompatActivity {
 private final Configs configs = KeyValueProvider.get(Configs.class);
 
...

 //写
 configs.user().set(user);

 //读
 configs.user().get().title;
 configs.user().get().content;
}

已更新至 KeyValueX 项目,感兴趣可自行查阅。


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

Flutter Clip 用来实现文本标签的效果

Clip 是Material Design的一个 Widget,用来实现标签效果,Clip中通过属性可配置一个文本、完整的 Widget、一个动作(比如按钮点击)。 1 基本使用效果如下 class ClipHomeState extends State {...
继续阅读 »

Clip 是Material Design的一个 Widget,用来实现标签效果,Clip中通过属性可配置一个文本、完整的 Widget、一个动作(比如按钮点击)。


1 基本使用效果如下


在这里插入图片描述


class ClipHomeState extends State {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Clip")),
body: const Center(
///-----------------------------------------------------
///核心代码
child: Chip(
//左侧的小组件
avatar: CircleAvatar(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Text('早'),
),
//右侧的文本
label: Text('Flutter 基础 '),
),

///-----------------------------------------------------
),
);
}
}

Chip 的 avatar 属性配置的是一个Widget,所这里可以组合足够复杂的 Widget合集,在本实例中只是使用了 CircleAvatar,CircleAvatar常用来展示圆形的小头像。


2 结合 Wrap 组件实现多标签效果


class ClipWrapHomeState extends State {
final List<String> _tagList = [
"早起", "晚睡", "测试", "努力",
"不想睡觉", "清晨的太阳", "这是什么", "哈哈"
];

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Clip")),
body: Padding(
///-----------------------------------------------------
///核心代码
padding: EdgeInsets.all(12),
child: Wrap(
spacing: 10,
children: _tagList.map((e) => Chip(label: Text(e))).toList(),
),
///-----------------------------------------------------
),
);
}
}

在这里插入图片描述


3 Clip 的属性概述


3.1 label 文本相关配置

label 是用来配置文本的,labelStyle是用来设置这个文本的样式,如颜色、大小 、粗细等等,labelPadding是用来设置文本四周的边距的。


  Chip buildChip() {
return const Chip(
//左侧的小组件
avatar: CircleAvatar(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Text('早'),
),

///右侧的文本
label: Text('Flutter 基础 '),
labelStyle: TextStyle(color: Colors.red),
labelPadding: EdgeInsets.only(left: 12, right: 12),
);
}
}

在这里插入图片描述


3.2 右侧的删除按钮配置 deleteIcon

avatar 是用来配置左侧的显示的小 Widget,deleteIcon是用来配置右侧的删除按钮的。


  Chip buildChip() {
return Chip(
//左侧的小组件
avatar: const CircleAvatar(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Text('早'),
),

///右侧的文本
label: Text('Flutter 基础 '),
labelStyle: TextStyle(color: Colors.red),
labelPadding: EdgeInsets.only(left: 12, right: 12),

///
deleteIcon: Icon(Icons.close),
//删除按钮颜色
deleteIconColor: Colors.red,
//长按的提示
deleteButtonTooltipMessage: "删除",
onDeleted: () {
print("--点击了删除");
},
);
}

在这里插入图片描述


3.3 阴影设置


属性 elevation 用来设置阴影高度,shadowColor属性用来设置阴影颜色


Chip buildChip() {
return Chip(
//左侧的小组件
avatar: const CircleAvatar(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
child: Text('早'),
),

///右侧的文本
label: Text('Flutter 基础 '),
labelStyle: TextStyle(color: Colors.red),
labelPadding: EdgeInsets.only(left: 12, right: 12),

///
deleteIcon: Icon(Icons.close),
//删除按钮颜色
deleteIconColor: Colors.red,
//长按的提示
deleteButtonTooltipMessage: "删除",
onDeleted: () {
print("--点击了删除");
},

///
elevation: 10,//阴影 高度
shadowColor: Colors.blue,//阴影颜色
backgroundColor: Colors.grey,//背景色

);
}



完毕


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

Flutter状态管理-Bloc的使用

前言 目前Flutter三大主流状态管理框架分别是provider、flutter_bloc、getx,三大状态管理框架各有优劣,本篇文章将介绍其中的flutter_bloc框架的使用,他是bloc设计思想模式在flutter上的实现,bloc全程全称 bus...
继续阅读 »

前言


目前Flutter三大主流状态管理框架分别是providerflutter_blocgetx,三大状态管理框架各有优劣,本篇文章将介绍其中的flutter_bloc框架的使用,他是bloc设计思想模式在flutter上的实现,bloc全程全称 business logic ,业务逻辑的意思,核心思想就是最大程度的将页面ui与数据逻辑的解耦,使我们的项目可读性、可维护性、健壮性增强。


两种使用模式


首先第一步引入插件:


flutter_bloc: ^8.1.1

引入之后,flutter_bloc使用支持以下两种模式管理。


Bloc模式,分别有ui层(view)、数据层(state)、事件层(event)、逻辑处理层(bloc),适合大型复杂页面使用。这四层结构bloc在源码处就进行了封装处理,所以我们使用的时候是必须要分离出来的,比如eventstate是要强制分开去写的。这也导致了简单页面使用此模式复杂化的问题,所以这种模式对于简单页面是非常没有必要的,但是如果是复杂页面的话,非常建议使用此模式,相信你在处理页面数据逻辑的时候会非常的清晰。

下面我们以计数器为例写个demo,记住bloc模式有四层,且必须分开,我们建四个文件分别代表这四层,

image.png

数据层: 用来存放数据,这个很简单。


/// 数据层
class DemoState {
// 自增数字
late int num;

DemoState init() {
// 初始化为0
return DemoState()..num = 0;
}

DemoState clone() {
// 获取最新对象
return DemoState()..num = num;
}
}

事件层: 用来存放页面所有事件的地方,比如计数器页面只有初始化事件和自增事件。


/// 页面中所有的交互事件
abstract class DemoEvent {}
/// 初始化事件
class InitEvent extends DemoEvent {}
/// 自增事件
class IncrementEvent extends DemoEvent {}

Bloc逻辑处理层: 处理上方数据和事件逻辑的地方,通过源码就能发现作者的意图,泛型里必须分开传入事件和数据,也足以说明这个模式的特点,就是为复杂页面准备的。所以如果写计数器的话,你就会感觉非常没有必要,因为计数器页面很简单,但是当你的state层里的数据非常多且复杂的时候,你就能体会出分开的好处了。
image.png
代码:


/// 逻辑处理层 继承Bloc
class DemoBloc extends Bloc<DemoEvent, DemoState> {
///构造方法
DemoBloc() : super(DemoState().init()) {
/// on 注册所有事件 on固定写法
on<InitEvent>(_init);
on<IncrementEvent>(_add);
}

/// 私有化逻辑方法 暴露Event事件即可
void _init(InitEvent event, Emitter<DemoState> emit) {
// emit方法,通知更新状态 类似于 ChangeNotifier的notifyListeners方法。
emit(state.clone());
}

_add(IncrementEvent event, Emitter<DemoState> emit) {
state.num++;
// 调用emit方法更新状态
emit(state.clone());
}
}

UI层: UI层只负责页面的编写,而无需关心数据的生成,根节点返回BlocProvider,并实现create方法,返回我们的bloc实体类。child实现我们的 UI页面。


/// UI层
class BlocNumPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 以下固定写法
return BlocProvider(
create: (BuildContext context) => DemoBloc()..add(InitEvent()),
child: Builder(builder: (context) => _buildPage(context)),
);
}

Widget _buildPage(BuildContext context) {
// 获取bloc实例
final bloc = BlocProvider.of<DemoBloc>(context);
return Stack(
children: [
Center(
// 对于需要更新状态的组件 外层包裹一个BlocBuilder,传入bloc、state
child: BlocBuilder<DemoBloc, DemoState>(
builder: (context, state) {
// 返回具体ui组件
return Text("点击了${bloc.state.num}次");
},
),
),
Positioned(
bottom: 20,
right: 20,
child: FloatingActionButton(
onPressed: () {
// 调用add方法触发自增事件,
bloc.add(IncrementEvent());
},
child: Icon(Icons.add),
),
)
],
);
}
}

效果:

Oct-30-2022 18-33-01.gif


Cubit模式


Cubit模式,分别有ui层(view)、数据层(state)、逻辑处理层(cubit),相较于bloc模式去掉了event层,适合比较简单的页面。跟bloc模式只是逻辑处理哪里发生了改变,数据层、页面ui层代码一模一样。


可以看到Cubit模式的逻辑就少了很多代码,而且是直接处理数据即可。通过源码,作者意图也很明显,只是传递了数据层。
image.png


/// 写逻辑
class CubitCubit extends Cubit<CubitState> {
CubitCubit() : super(CubitState().init());

///自增
void increment() => emit(state.clone()..num = state.num + 1);
}

其他写法跟Bloc是一样的,就不粘贴了,那看到这有的小伙伴可能就要问了,一个页面要创建3、4个文件,这也太麻烦了吧,高端的程序员往往不会去写一些重复性较高的代码,其实上面的四个文件都是可以通过插件自动生成的,这里下面两个插件,一个是官方的,官方的不会自动生成ui层的文件,一个是小呆呆写的,可以自动生成ui层重复性的代码文件,两者区别不大,推荐小呆呆的,因为可以多生成一个文件。
image.png


最后


Bloc本质上是一种数据逻辑和UI解耦思想,上面的演示只是非常非常简单的用法,就可以看出作者在源码层给我们强制性的设定了非常明确的各个模型,每个模型只专心负责一个事情,这样看起来一个页面会非常的清晰,可以说Flutter_Bloc是一个非常适合大型项目使用的状态管理框架。


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

公司没钱了,工资发不出来,作为员工怎么办?

现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发 80%、50% 工资这样的操作。 员工遇到这种情况,无非以下几种选择。1认同公司的决策,愿意跟公司共同进退。2不认同...
继续阅读 »

现在大环境不好,很多公司都会遇到一些困难。如果公司真的有现金流还好,如果没有,还等着客户回款。那么就有点难办了。很多公司会采取延期发工资,先发 80%、50% 工资这样的操作。


员工遇到这种情况,无非以下几种选择。

1认同公司的决策,愿意跟公司共同进退。

2不认同公司的决策,我要离职。

3不认同公司的决策,但感觉自己反对也没用。所以嘴上先答应,事后会准备去找新的工作机会。

4不认同公司的决策,我也不主动离职。准备跟公司 battle,” 你们这么做是不合法滴 “


你可以代入这个场景看看自己是哪一类。首先由于每个人遇到的真实情况不一样,所以所有的选择只有适合自己的,并没有对错之分。


我自己的应对思路是,抛开存量,看增量。存量就是我在公司多少多少年了,公司开除我要给我 N+1 的补偿。公司之前对我特别特别好,老板对我有知遇之恩等等。你就当做自己已经不在公司了,现在公司给你发了 offer,现在的你是否愿意接受公司开给你的条件?如果愿意,那么你可以选择方案一。如果不愿意,那么你就可以选择方案三。现在这环境,骑驴找马,否则离职后还要自己交社保。还不如先苟着。


为什么不选择方案四?因为不值得,如果公司属于违法操作,你先苟着,后面离职后还是可以找劳动局仲裁的。这样既不耽误你换工作,也不耽误你要赔偿。如果公司是正规操作,那么闹腾也没用,白浪费自己的时间。


离职赔偿还是比较清晰明确的,如果是散伙那可能会牵扯到更多利益。我自己的经验是,不能什么都想着要。当最优解挺难获得的时候,拿个次优解也可以。当然,不管你选择的哪个,我都有一个建议。那就是当一天和尚,敲一天钟。在职期间,还是要把事情干好的,用心并不全是为了公司,更多是为了自己。人生最大的投资是投资自己的工作和事业,浪费时间就是浪费生命。哪怕公司没有事情安排给你做,也要学会自己找事情做。


如果公司后面没钱了,欠的工资还拿得到吗?



我们作为员工是很难知道公司财务状况的,所以出了这样的事就直接去仲裁,最好是跟同事凑齐十个人去,据说会优先处理。公司如果还要做生意,一般会在仲裁前选择和解,大概是分几个月归还欠款。如果公司不管,那么仲裁后,会冻结公司公账。但有没有钱就看情况了。


如果公司账上没钱且股东已经实缴了股本金,那么公司是可以直接破产清算的。公司破产得话,基本上欠员工的钱就没有了。如果没有实缴,那么股东还需要按照股份比例偿还债务。


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

收起阅读 »

阿里面试官:请设计一个不能操作DOM和调接口的环境

web
前言四面的时候被问到了这个问题,当时第一时间没有反应过来,觉得这个需求好奇特面试官给了一些提示,我才明白这道题目的意思,最后回答的也是磕磕绊绊后来花了一些时间整理了下思路,那么如何设计这样的环境呢?最终实现实现思路:1)利用 iframe 创建沙箱,取出其中的...
继续阅读 »

前言

四面的时候被问到了这个问题,当时第一时间没有反应过来,觉得这个需求好奇特

面试官给了一些提示,我才明白这道题目的意思,最后回答的也是磕磕绊绊

后来花了一些时间整理了下思路,那么如何设计这样的环境呢?

最终实现

实现思路:

1)利用 iframe 创建沙箱,取出其中的原生浏览器全局对象作为沙箱的全局对象

2)设置一个黑名单,若访问黑名单中的变量,则直接报错,实现阻止\隔离的效果

3)在黑名单中添加 document 字段,来实现禁止开发者操作 DOM

4)在黑名单中添加 XMLHttpRequest、fetch、WebSocket 字段,实现禁用原生的方式调用接口

5)若访问当前全局对象中不存在的变量,则直接报错,实现禁用三方库调接口

6)最后还要拦截对 window 对象的访问,防止通过 window.document 来操作 DOM,避免沙箱逃逸

下面聊一聊,为何这样设计,以及中间会遇到什么问题

如何禁止开发者操作 DOM ?

在页面中,可以通过 document 对象来获取 HTML 元素,进行增删改查的 DOM 操作

如何禁止开发者操作 DOM,转化为如何阻止开发者获取 document 对象

1)传统思路

简单粗暴点,直接修改 window.document 的值,让开发者无法获取 document

// 将document设置为null
window.document = null;

// 设置无效,打印结果还是document
console.log(window.document);

// 删除document
delete window.document

// 删除无效,打印结果还是document
console.log(window.document);

好吧,document 修改不了也删除不了🤔

使用 Object.getOwnPropertyDescriptor 查看,会发现 window.document 的 configurable 属性为 false(不可配置的)

Object.getOwnPropertyDescriptor(window, 'document');
// {get: ƒ, set: undefined, enumerable: true, configurable: false}

configurable 决定了是否可以修改属性描述对象,也就是说,configurable为false时,value、writable、enumerable和configurable 都不能被修改,以及无法被删除

此路不通,推倒重来

2)有点高大上的思路

既然 document 对象修改不了,那如果环境中原本就没有 document 对象,是不是就可以实现该需求?

说到环境中没有 document 对象,Web Worker 直呼内行,我曾在《一文彻底了解Web Worker,十万、百万条数据都是弟弟🔥》中聊过如何使用 Web Worker,和对应的特性

并且 Web Worker 更狠,不但没有 document 对象,连 window 对象也没有😂

在worker线程中打印window

onmessage = function (e) {
console.log(window);
postMessage();
};

浏览器直接报错


在 Web Worker 线程的运行环境中无法访问 document 对象,这一条符合当前的需求,但是该环境中能获取 XMLHttpRequest 对象,可以发送 ajax 请求,不符合不能调接口的要求

此路还是不通……😓

如何禁止开发者调接口 ?

常规调接口方式有:

1)原生方式:XMLHttpRequest、fetch、WebSocket、jsonp、form表单

2)三方实现:axios、jquery、request等众多开源库

禁用原生方式调接口的思路:

1)XMLHttpRequest、fetch、WebSocket 这几种情况,可以禁止用户访问这些对象

2)jsonp、form 这两种方式,需要创建script或form标签,依然可以通过禁止开发者操作DOM的方式解决,不需要单独处理

如何禁用三方库调接口呢?

三方库很多,没办法全部列出来,来进行逐一排除

禁止调接口的路好像也被封死了……😰

最终方案:沙箱(Sandbox)

通过上面的分析,传统的思路确实解决不了当前的需求

阻止开发者操作DOM和调接口,沙箱说:这个我熟啊,拦截隔离这类的活,我最拿手了😀

沙箱(Sandbox) 是一种安全机制,为运行中的程序提供隔离环境,通常用于执行未经测试或不受信任的程序或代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的运行

前端沙箱的使用场景:

1)Chrome 浏览器打开的每个页面就是一个沙箱,保证彼此独立互不影响

2)执行 jsonp 请求回来的字符串时或引入不知名第三方 JS 库时,可能需要创造一个沙箱来执行这些代码

3)Vue 模板表达式的计算是运行在一个沙箱中,模板字符串中的表达式只能获取部分全局对象,详情见源码

4)微前端框架 qiankun ,为了实现js隔离,在多种场景下均使用了沙箱

沙箱的多种实现方式

先聊下 with 这个关键字:作用在于改变作用域,可以将某个对象添加到作用域链的顶部

with对于沙箱的意义:可以实现所有变量均来自可靠或自主实现的上下文环境,而不会从全局的执行环境中取值,相当于做了一层拦截,实现隔离的效果

简陋的沙箱

题目要求: 实现这样一个沙箱,要求程序中访问的所有变量,均来自可靠或自主实现的上下文环境,而不会从全局的执行环境中取值

举个🌰: ctx作为执行上下文对象,待执行程序code可以访问到的变量,必须都来自ctx对象

// ctx 执行上下文对象
const ctx = {
func: variable => {
  console.log(variable);
},
foo: "f1"
};

// 待执行程序
const code = `func(foo)`;

沙箱示例:

// 定义全局变量foo
var foo = "foo1";

// 执行上下文对象
const ctx = {
 func: variable => {
   console.log(variable);
},
 foo: "f1"
};

// 非常简陋的沙箱
function veryPoorSandbox(code, ctx) {
 // 使用with,将eval函数执行时的执行上下文指定为ctx
 with (ctx) {
   // eval可以将字符串按js代码执行,如eval('1+2')
   eval(code);
}
}

// 待执行程序
const code = `func(foo)`;

veryPoorSandbox(code, ctx);
// 打印结果:"f1",不是最外层的全局变量"foo1"

这个沙箱有一个明显的问题,若提供的ctx上下文对象中,没有找到某个变量时,代码仍会沿着作用域链一层层向上查找

假如上文示例中的 ctx 对象没有设置 foo属性,打印的结果还是外层作用域的foo1

With + Proxy 实现沙箱

题目要求: 希望沙箱中的代码只在手动提供的上下文对象中查找变量,如果上下文对象中不存在该变量,则提示对应的错误

举个🌰: ctx作为执行上下文对象,待执行程序code可以访问到的变量,必须都来自ctx对象,如果ctx对象中不存在该变量,直接报错,不再通过作用域链向上查找

实现步骤:

1)使用 Proxy.has() 来拦截 with 代码块中的任意变量的访问

2)设置一个白名单,在白名单内的变量可以正常走作用域链的访问方式,不在白名单内的变量,会继续判断是否存 ctx 对象中,存在则正常访问,不存在则直接报错

3)使用new Function替代eval,使用 new Function() 运行代码比eval更为好一些,函数的参数提供了清晰的接口来运行代码

new Function与eval的区别

沙箱示例:

var foo = "foo1";

// 执行上下文对象
const ctx = {
 func: variable => {
   console.log(variable);
}
};

// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
function withedYourCode(code) {
 code = "with(shadow) {" + code + "}";
 return new Function("shadow", code);
}

// 可访问全局作用域的白名单列表
const access_white_list = ["func"];

// 待执行程序
const code = `func(foo)`;

// 执行上下文对象的代理对象
const ctxProxy = new Proxy(ctx, {
 has: (target, prop) => {
   // has 可以拦截 with 代码块中任意属性的访问
   if (access_white_list.includes(prop)) {
     // 在可访问的白名单内,可继续向上查找
     return target.hasOwnProperty(prop);
  }
   if (!target.hasOwnProperty(prop)) {
     throw new Error(`Not found - ${prop}!`);
  }
   return true;
}
});

// 没那么简陋的沙箱
function littlePoorSandbox(code, ctx) {
 // 将 this 指向手动构造的全局代理对象
 withedYourCode(code).call(ctx, ctx);
}
littlePoorSandbox(code, ctxProxy);

// 执行func(foo),报错: Uncaught Error: Not found - foo!

执行结果:


天然的优质沙箱(iframe)

iframe 标签可以创造一个独立的浏览器原生级别的运行环境,这个环境由浏览器实现了与主环境的隔离

利用 iframe 来实现一个沙箱是目前最方便、简单、安全的方法,可以把 iframe.contentWindow 作为沙箱执行的全局 window 对象

沙箱示例:

// 沙箱全局代理对象类
class SandboxGlobalProxy {
 constructor(sharedState) {
   // 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
   const iframe = document.createElement("iframe", { url: "about:blank" });
   iframe.style.display = "none";
   document.body.appendChild(iframe);
   
   // sandboxGlobal作为沙箱运行时的全局对象
   const sandboxGlobal = iframe.contentWindow;

   return new Proxy(sandboxGlobal, {
     has: (target, prop) => {
       // has 可以拦截 with 代码块中任意属性的访问
       if (sharedState.includes(prop)) {
         // 如果属性存在于共享的全局状态中,则让其沿着原型链在外层查找
         return false;
      }
       
       // 如果没有该属性,直接报错
       if (!target.hasOwnProperty(prop)) {
         throw new Error(`Not find: ${prop}!`);
      }
       
       // 属性存在,返回sandboxGlobal中的值
       return true;
    }
  });
}
}

// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
function withedYourCode(code) {
 code = "with(sandbox) {" + code + "}";
 return new Function("sandbox", code);
}
function maybeAvailableSandbox(code, ctx) {
 withedYourCode(code).call(ctx, ctx);
}

// 要执行的代码
const code = `
 console.log(history == window.history) // false
 window.abc = 'sandbox'
 Object.prototype.toString = () => {
     console.log('Traped!')
 }
 console.log(window.abc) // sandbox
`;

// sharedGlobal作为与外部执行环境共享的全局对象
// code中获取的history为最外层作用域的history
const sharedGlobal = ["history"];

const globalProxy = new SandboxGlobalProxy(sharedGlobal);

maybeAvailableSandbox(code, globalProxy);

// 对外层的window对象没有影响
console.log(window.abc); // undefined
Object.prototype.toString(); // 并没有打印 Traped

可以看到,沙箱中对window的所有操作,都没有影响到外层的window,实现了隔离的效果😘

需求实现

继续使用上述的 iframe 标签来创建沙箱,代码主要修改点

1)设置 blacklist 黑名单,添加 document、XMLHttpRequest、fetch、WebSocket 来禁止开发者操作DOM和调接口

2)判断要访问的变量,是否在当前环境的 window 对象中,不在的直接报错,实现禁止通过三方库调接口

// 设置黑名单
const blacklist = ['document', 'XMLHttpRequest', 'fetch', 'WebSocket'];

// 黑名单中的变量禁止访问
if (blacklist.includes(prop)) {
 throw new Error(`Can't use: ${prop}!`);
}

但有个很严重的漏洞,如果开发者通过 window.document 来获取 document 对象,依然是可以操作 DOM 的😱

需要在黑名单中加入 window 字段,来解决这个沙箱逃逸的漏洞,虽然把 window 加入了黑名单,但 window 上的方法,如 open、close 等,依然是可以正常获取使用的

最终代码:

// 沙箱全局代理对象类
class SandboxGlobalProxy {
constructor(blacklist) {
// 创建一个 iframe 标签,取出其中的原生浏览器全局对象作为沙箱的全局对象
const iframe = document.createElement("iframe", { url: "about:blank" });
iframe.style.display = "none";
document.body.appendChild(iframe);

// 获取当前HTMLIFrameElement的Window对象
const sandboxGlobal = iframe.contentWindow;

return new Proxy(sandboxGlobal, {
// has 可以拦截 with 代码块中任意属性的访问
has: (target, prop) => {

// 黑名单中的变量禁止访问
if (blacklist.includes(prop)) {
throw new Error(`Can't use: ${prop}!`);
}
// sandboxGlobal对象上不存在的属性,直接报错,实现禁用三方库调接口
if (!target.hasOwnProperty(prop)) {
throw new Error(`Not find: ${prop}!`);
}

// 返回true,获取当前提供上下文对象中的变量;如果返回false,会继续向上层作用域链中查找
return true;
}
});
}
}

// 使用with关键字,来改变作用域
function withedYourCode(code) {
code = "with(sandbox) {" + code + "}";
return new Function("sandbox", code);
}

// 将指定的上下文对象,添加到待执行代码作用域的顶部
function makeSandbox(code, ctx) {
withedYourCode(code).call(ctx, ctx);
}

// 待执行的代码code,获取document对象
const code = `console.log(document)`;

// 设置黑名单
// 经过小伙伴的指导,新添加Image字段,禁止使用new Image来调接口
const blacklist = ['window', 'document', 'XMLHttpRequest', 'fetch', 'WebSocket', 'Image'];

// 将globalProxy对象,添加到新环境作用域链的顶部
const globalProxy = new SandboxGlobalProxy(blacklist);

makeSandbox(code, globalProxy);

打印结果:


持续优化

经过与评论区小伙伴的交流,可以通过 new Image() 调接口,确实是个漏洞

// 不需要创建DOM 发送图片请求
let img = new Image();
img.src= "http://www.test.com/img.gif";

黑名单中添加'Image'字段,堵上这个漏洞。如果还有其他漏洞,欢迎交流讨论💕

总结

通过解决面试官提出的问题,介绍了沙箱的基本概念、应用场景,以及如何去实现符合要求的沙箱,发现防止沙箱逃逸是一件挺有趣的事情,就像双方在下棋一样,你来我往,有攻有守😄

关于这个问题,小伙伴们如果有其他可行的方案,或者有要补充、指正的,欢迎交流讨论


参考资料:
浅析 JavaScript 沙箱机制

作者:海阔_天空
来源:juejin.cn/post/7157570429928865828

收起阅读 »

数据结构:7种哈希散列算法,你知道几个?

一、前言 哈希表的历史 哈希散列的想法在不同的地方独立出现。1953 年 1 月,汉斯·彼得·卢恩 ( Hans Peter Luhn ) 编写了一份IBM内部备忘录,其中使用了散列和链接。开放寻址后来由 AD Linh 在 Luhn 的论文上提出。大约在同一...
继续阅读 »

一、前言


哈希表的历史


哈希散列的想法在不同的地方独立出现。1953 年 1 月,汉斯·彼得·卢恩 ( Hans Peter Luhn ) 编写了一份IBM内部备忘录,其中使用了散列和链接。开放寻址后来由 AD Linh 在 Luhn 的论文上提出。大约在同一时间,IBM Research的Gene Amdahl、Elaine M. McGraw、Nathaniel Rochester和Arthur Samuel为IBM 701汇编器实现了散列。 线性探测的开放寻址归功于 Amdahl,尽管Ershov独立地有相同的想法。“开放寻址”一词是由W. Wesley Peterson在他的文章中创造的,该文章讨论了大文件中的搜索问题。


二、哈希数据结构


哈希表的存在是为了解决能通过O(1)时间复杂度直接索引到指定元素。


这是什么意思呢?通过我们使用数组存放元素,都是按照顺序存放的,当需要获取某个元素的时候,则需要对数组进行遍历,获取到指定的值。如图所示;





而这样通过循环遍历比对获取指定元素的操作,时间复杂度是O(n),也就是说如果你的业务逻辑实现中存在这样的代码是非常拉胯的。那怎么办呢?这就引入了哈希散列表的设计。




在计算机科学中,一个哈希表(hash table、hash map)是一种实现关联数组的抽象数据结构,该结构将键通过哈希计算映射到值。


也就是说我们通过对一个 Key 值计算它的哈希并与长度为2的n次幂的数组减一做与运算,计算出槽位对应的索引,将数据存放到索引下。那么这样就解决了当获取指定数据时,只需要根据存放时计算索引ID的方式再计算一次,就可以把槽位上对应的数据获取处理,以此达到时间复杂度为O(1)的情况。如图所示;





哈希散列虽然解决了获取元素的时间复杂度问题,但大多数时候这只是理想情况。因为随着元素的增多,很可能发生哈希冲突,或者哈希值波动不大导致索引计算相同,也就是一个索引位置出现多个元素情况。如图所示;





李二狗拎瓢冲都有槽位的下标索引03的 叮裆猫 发生冲突时,情况就变得糟糕了,因为这样就不能满足O(1)时间复杂度获取元素的诉求了。


那么此时就出现了一系列解决方案,包括;HashMap 中的拉链寻址 + 红黑树、扰动函数、负载因子ThreadLocal 的开放寻址、合并散列、杜鹃散列、跳房子哈希、罗宾汉哈希等各类数据结构设计。让元素在发生哈希冲突时,也可以存放到新的槽位,并尽可能保证索引的时间复杂度小于O(n)


三、实现哈希散列


哈希散列是一个非常常见的数据结构,无论是我们使用的 HashMap、ThreaLocal 还是你在刷题中位了提升索引效率,都会用到哈希散列。


只要哈希桶的长度由负载因子控制的合理,每次查找元素的平均时间复杂度与桶中存储的元素数量无关。另外许多哈希表设计还允许对键值对的任意插入和删除,每次操作的摊销固定平均成本。


好,那么介绍了这么多,小傅哥带着大家做几个关于哈希散列的数据结构,通过实践来了解会更加容易搞懂。



1. 哈希碰撞


说明:通过模拟简单 HashMap 实现,去掉拉链寻址等设计,验证元素哈新索引位置碰撞。


public class HashMap01<K, V> implements Map<K, V> {

private final Object[] tab = new Object[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
tab[idx] = value;
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
return (V) tab[idx];
}

}





  • HashMap01 的实现只是通过哈希计算出的下标,散列存放到固定的数组内。那么这样当发生元素下标碰撞时,原有的元素就会被新的元素替换掉。


测试


@Test
public void test_hashMap01() {
Map<String, String> map = new HashMap01<>();
map.put("01", "花花");
map.put("02", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));

// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
}




06:58:41.691 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
06:58:41.696 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:苗苗

Process finished with exit code 0


  • 通过测试结果可以看到,碰撞前 map.get("01") 的值是花花,两次下标索引碰撞后存放的值则是苗苗

  • 这也就是使用哈希散列必须解决的一个问题,无论是在已知元素数量的情况下,通过扩容数组长度解决,还是把碰撞的元素通过链表存放,都是可以的。


2. 拉链寻址


说明:既然我们没法控制元素不碰撞,但我们可以对碰撞后的元素进行管理。比如像 HashMap 中拉链法一样,把碰撞的元素存放到链表上。这里我们就来简化实现一下。


public class HashMap02BySeparateChaining<K, V> implements Map<K, V> {

private final LinkedList<Node<K, V>>[] tab = new LinkedList[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
if (tab[idx] == null) {
tab[idx] = new LinkedList<>();
tab[idx].add(new Node<>(key, value));
} else {
tab[idx].add(new Node<>(key, value));
}
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
for (Node<K, V> kvNode : tab[idx]) {
if (key.equals(kvNode.getKey())) {
return kvNode.value;
}
}
return null;
}

static class Node<K, V> {
final K key;
V value;

public Node(K key, V value) {
this.key = key;
this.value = value;
}

public K getKey() {
return key;
}

public V getValue() {
return value;
}

}

}



  • 因为元素在存放到哈希桶上时,可能发生下标索引膨胀,所以这里我们把每一个元素都设定成一个 Node 节点,这些节点通过 LinkedList 链表关联,当然你也可以通过 Node 节点构建出链表 next 元素即可。

  • 那么这时候在发生元素碰撞,相同位置的元素就都被存放到链表上了,获取的时候需要对存放多个元素的链表进行遍历获取。


测试


@Test
public void test_hashMap02() {
Map<String, String> map = new HashMap02BySeparateChaining<>();
map.put("01", "花花");
map.put("05", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));

// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
}


07:21:16.654 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:22:44.651 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花

Process finished with exit code 0


  • 此时第一次和第二次获取01位置的元素就都是花花了,元素没有被替代。因为此时的元素是被存放到链表上了。


3. 开放寻址


说明:除了对哈希桶上碰撞的索引元素进行拉链存放,还有不引入新的额外的数据结构,只是在哈希桶上存放碰撞元素的方式。它叫开放寻址,也就是 ThreaLocal 中运用斐波那契散列+开放寻址的处理方式。


public class HashMap03ByOpenAddressing<K, V> implements Map<K, V> {

private final Node<K, V>[] tab = new Node[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
if (tab[idx] == null) {
tab[idx] = new Node<>(key, value);
} else {
for (int i = idx; i < tab.length; i++) {
if (tab[i] == null) {
tab[i] = new Node<>(key, value);
break;
}
}
}
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
for (int i = idx; i < tab.length; i ++){
if (tab[idx] != null && tab[idx].key == key) {
return tab[idx].value;
}
}
return null;
}

static class Node<K, V> {
final K key;
V value;

public Node(K key, V value) {
this.key = key;
this.value = value;
}

}

}



  • 开放寻址的设计会对碰撞的元素,寻找哈希桶上新的位置,这个位置从当前碰撞位置开始向后寻找,直到找到空的位置存放。

  • 在 ThreadLocal 的实现中会使用斐波那契散列、索引计算累加、启发式清理、探测式清理等操作,以保证尽可能少的碰撞。


测试


@Test
public void test_hashMap03() {
Map<String, String> map = new HashMap03ByOpenAddressing<>();
map.put("01", "花花");
map.put("05", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
}


07:20:22.382 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:20:22.387 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:20:22.387 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:HashMap{tab=[null,{"key":"01","value":"花花"},{"key":"09","value":"蛋蛋"},{"key":"12","value":"苗苗"},null,{"key":"05","value":"豆豆"},null,null]}

Process finished with exit code 0


  • 通过测试结果可以看到,开放寻址对碰撞元素的寻址存放,也是可用解决哈希索引冲突问题的。


4. 合并散列


说明:合并散列是开放寻址和单独链接的混合,碰撞的节点在哈希表中链接。此算法适合固定分配内存的哈希桶,通过存放元素时识别哈希桶上的最大空槽位来解决合并哈希中的冲突。


public class HashMap04ByCoalescedHashing<K, V> implements Map<K, V> {

private final Node<K, V>[] tab = new Node[8];

@Override
public void put(K key, V value) {
int idx = key.hashCode() & (tab.length - 1);
if (tab[idx] == null) {
tab[idx] = new Node<>(key, value);
return;
}

int cursor = tab.length - 1;
while (tab[cursor] != null && tab[cursor].key != key) {
--cursor;
}
tab[cursor] = new Node<>(key, value);

// 将碰撞节点指向这个新节点
while (tab[idx].idxOfNext != 0){
idx = tab[idx].idxOfNext;
}

tab[idx].idxOfNext = cursor;
}

@Override
public V get(K key) {
int idx = key.hashCode() & (tab.length - 1);
while (tab[idx].key != key) {
idx = tab[idx].idxOfNext;
}
return tab[idx].value;
}

static class Node<K, V> {
final K key;
V value;
int idxOfNext;

public Node(K key, V value) {
this.key = key;
this.value = value;
}

}

}



  • 合并散列的最大目的在于将碰撞元素链接起来,避免因为需要寻找碰撞元素所发生的循环遍历。也就是A、B元素存放时发生碰撞,那么在找到A元素的时候可以很快的索引到B元素所在的位置。


测试


07:18:43.613 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:18:43.618 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:苗苗
07:18:43.619 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:HashMap{tab=[null,{"idxOfNext":7,"key":"01","value":"花花"},null,null,null,{"idxOfNext":0,"key":"05","value":"豆豆"},{"idxOfNext":0,"key":"12","value":"苗苗"},{"idxOfNext":6,"key":"09","value":"蛋蛋"}]}

Process finished with exit code 0


  • 相对于直接使用开放寻址,这样的挂在链路指向的方式,可以提升索引的性能。因为在实际的数据存储上,元素的下一个位置不一定空元素,可能已经被其他元素占据,这样就增加了索引的次数。所以使用直接指向地址的方式,会更好的提高索引性能。


5. 杜鹃散列


说明:这个名字起的比较有意思,也代表着它的数据结构。杜鹃鸟在孵化🐣的时候,雏鸟会将其他蛋或幼崽推出巢穴;类似的这个数据结构会使用2组key哈希表,将冲突元素推到另外一个key哈希表中。


private V put(K key, V value, boolean isRehash) {
Object k = maskNull(key);
if (containsKey(k)) {
return null;
}
if (insertEntry(new Entry<K, V>((K) k, value))) {
if (!isRehash) {
size++;
}
return null;
}
rehash(2 * table.length);
return put((K) k, value);
}

private boolean insertEntry(Entry<K, V> e) {
int count = 0;
Entry<K, V> current = e;
int index = hash(hash1, current.key);
while (current != e || count < table.length) {
Entry<K, V> temp = table[index];
if (temp == null) {
table[index] = current;
return true;
}
table[index] = current;
current = temp;
if (index == hash(hash1, current.key)) {
index = hash(hash2, current.key);
} else {
index = hash(hash1, current.key);
}
++count;
}
return false;
}



  • 当多个键映射到同一个单元格时会发生这种情况。杜鹃散列的基本思想是通过使用两个散列函数而不是仅一个散列函数来解决冲突。

  • 这为每个键在哈希表中提供了两个可能的位置。在该算法的一种常用变体中,哈希表被分成两个大小相等的较小的表,每个哈希函数都为这两个表之一提供索引。两个散列函数也可以为单个表提供索引。

  • 在实践中,杜鹃哈希比线性探测慢约 20-30%,线性探测是常用方法中最快的。然而,由于它对搜索时间的最坏情况保证,当需要实时响应率时,杜鹃散列仍然很有价值。杜鹃散列的一个优点是它的无链接列表属性,非常适合 GPU 处理。


测试



07:52:04.010 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
07:52:04.016 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:苗苗
07:52:04.016 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:{01=花花, 12=苗苗, 05=豆豆, 09=蛋蛋}

Process finished with exit code 0


  • 从测试结果可以看到,杜鹃散列可以通过两个散列函数解决索引冲突问题。不过这个探测的过程比较耗时。


6. 跳房子散列


说明:跳房子散列是一种基于开放寻址的算法,它结合了杜鹃散列、线性探测和链接的元素,通过桶邻域的概念——任何给定占用桶周围的后续桶,也称为“虚拟”桶。 该算法旨在在哈希表的负载因子增长超过 90% 时提供更好的性能;它还在并发设置中提供了高吞吐量,因此非常适合实现可调整大小的并发哈希表。


public boolean insert(AnyType x) {
if (!isEmpty()) {
return false;
}
int currentPos = findPos(x);
if (currentPos == -1) {
return false;
}
if (array[currentPos] != null) {
x = array[currentPos].element;
array[currentPos].isActive = true;
}
String hope;
if (array[currentPos] != null) {
hope = array[currentPos].hope;
x = array[currentPos].element;
} else {
hope = "10000000";
}
array[currentPos] = new HashEntry<>(x, hope, true);
theSize++;
return true;
}



  • 该算法使用一个包含n 个桶的数组。对于每个桶,它的邻域是H个连续桶的小集合(即索引接近原始散列桶的那些)。邻域的期望属性是在邻域的桶中找到一个项目的成本接近于在桶本身中找到它的成本(例如,通过使邻域中的桶落在同一缓存行中)。在最坏的情况下,邻域的大小必须足以容纳对数个项目(即它必须容纳 log( n ) 个项目),但平均只能是一个常数。如果某个桶的邻域被填满,则调整表的大小。


测试


@Test
public void test_hashMap06() {
HashMap06ByHopscotchHashing<Integer> map = new HashMap06ByHopscotchHashing<>();
map.insert(1);
map.insert(5);
map.insert(9);
map.insert(12);
logger.info("数据结构:{}", map);
}


17:10:10.363 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:HashMap{tab=[null,{"element":1,"hope":"11000000","isActive":true},{"element":9,"hope":"00000000","isActive":true},null,{"element":12,"hope":"10000000","isActive":true},{"element":5,"hope":"10000000","isActive":true},null,null]}

Process finished with exit code 0


  • 通过测试可以看到,跳房子散列会在其原始散列数组条目中找到,或者在接下来的H-1个相邻条目之一找到对应的冲突元素。


7. 罗宾汉哈希


说明:罗宾汉哈希是一种基于开放寻址的冲突解决算法;冲突是通过偏向从其“原始位置”(即项目被散列到的存储桶)最远或最长探测序列长度(PSL)的元素的位移来解决的。


public void put(K key, V value) {
Entry entry = new Entry(key, value);
int idx = hash(key);
// 元素碰撞检测
while (table[idx] != null) {
if (entry.offset > table[idx].offset) {
// 当前偏移量不止一个,则查看条目交换位置,entry 是正在查看的条目,增加现在搜索的事物的偏移量和 idx
Entry garbage = table[idx];
table[idx] = entry;
entry = garbage;
idx = increment(idx);
entry.offset++;
} else if (entry.offset == table[idx].offset) {
// 当前偏移量与正在查看的检查键是否相同,如果是则它们交换值,如果不是,则增加 idx 和偏移量并继续
if (table[idx].key.equals(key)) {
// 发现相同值
V oldVal = table[idx].value;
table[idx].value = value;
} else {
idx = increment(idx);
entry.offset++;
}
} else {
// 当前偏移量小于我们正在查看的我们增加 idx 和偏移量并继续
idx = increment(idx);
entry.offset++;
}
}
// 已经到达了 null 所在的 idx,将新/移动的放在这里
table[idx] = entry;
size++;
// 超过负载因子扩容
if (size >= loadFactor * table.length) {
rehash(table.length * 2);
}
}



  • 09、12 和 01 发生哈希索引碰撞,进行偏移量计算调整。通过最长位置探测碰撞元素位移来处理。


测试


public void test_hashMap07() {
Map<String, String> map = new HashMap07ByRobinHoodHashing<>();
map.put("01", "花花");
map.put("05", "豆豆");
logger.info("碰撞前 key:{} value:{}", "01", map.get("01"));
// 下标碰撞
map.put("09", "蛋蛋");
map.put("12", "苗苗");
logger.info("碰撞前 key:{} value:{}", "01", map.get("12"));
logger.info("数据结构:{}", map);
}


07:34:32.593 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:花花
09 1
12 1
01 1
09 9
12 1
05 5
07:35:07.419 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 碰撞前 key:01 value:苗苗
07:35:07.420 [main] INFO cn.bugstack.algorithms.test.AlgorithmsTest - 数据结构:HashMap{tab=[null,{"key":"01","offset":0,"value":"花花"},{"key":"12","offset":1,"value":"苗苗"},null,null,{"key":"05","offset":0,"value":"豆豆"},null,null,null,{"key":"09","offset":0,"value":"蛋蛋"},null,null,null,null,null,null]}

Process finished with exit code 0


  • 通过测试结果和调试的时候可以看到,哈希索引冲突是通过偏向从其“原始位置”(即项目被散列到的存储桶)最远或最长探测序列长度(PSL)的元素的位移来解决。这块可以添加断点调试验证。


四、常见面试问题



  • 介绍一下散列表

  • 为什么使用散列表

  • 拉链寻址和开放寻址的区别

  • 还有其他什么方式可以解决散列哈希索引冲突

  • 对应的Java源码中,对于哈希索引冲突提供了什么样的解决方案

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

高并发技巧-redis和本地缓存使用技巧

在这篇文章中,我主要介绍的是分布式缓存和本地缓存的使用技巧,包括缓存种类介绍,各种的使用场景,以及如何使用,最后再给出实战案例。 众所周知,缓存最主要的目的就是加速访问,缓解数据库压力。最常用的缓存就是分布式缓存,比如redis,在面对大部分并发场景或者一些...
继续阅读 »

在这篇文章中,我主要介绍的是分布式缓存和本地缓存的使用技巧,包括缓存种类介绍,各种的使用场景,以及如何使用,最后再给出实战案例。



众所周知,缓存最主要的目的就是加速访问,缓解数据库压力。最常用的缓存就是分布式缓存,比如redis,在面对大部分并发场景或者一些中小型公司流量没有那么高的情况,使用redis基本都能解决了。但是在流量较高的情况下可能得使用到本地缓存了,比如guava的LoadingCache和快手开源的ReloadableCache。


三种缓存的使用场景


这部分会介绍redis,比如guava的LoadingCache和快手开源的ReloadableCache的使用场景和局限,通过这一部分的介绍就能知道在怎样的业务场景下应该使用哪种缓存,以及为什么。


Redis的使用场景和局限性


如果宽泛的说redis何时使用,那么自然就是用户访问量过高的地方使用,从而加速访问,并且缓解数据库压力。如果细分的话,还得分为单节点问题和非单节点问题。


如果一个页面用户访问量比较高,但是访问的不是同一个资源。比如用户详情页,访问量比较高,但是每个用户的数据都是不一样的,这种情况显然只能用分布式缓存了,如果使用redis,key为用户唯一键,value则是用户信息。


redis导致的缓存击穿


但是需要注意一点,一定要设置过期时间,而且不能设置到同一时间点过期。举个例子,比如用户又个活动页,活动页能看到用户活动期间获奖数据,粗心的人可能会设置用户数据的过期时间点为活动结束,这样会


单(热)点问题


单节点问题说的是redis的单个节点的并发问题,因为对于相同的key会落到redis集群的同一个节点上,那么如果对这个key的访问量过高,那么这个redis节点就存在并发隐患,这个key就称为热key。


如果所有用户访问的都是同一个资源,比如小爱同学app首页对所有用户展示的内容都一样(初期),服务端给h5返回的是同一个大json,显然得使用到缓存。首先我们考虑下用redis是否可行,由于redis存在单点问题,如果流量过大的话,那么所有用户的请求到达redis的同一个节点,需要评估该节点能否抗住这么大流量。我们的规则是,如果单节点qps达到了千级别就要解决单点问题了(即使redis号称能抗住十万级别的qps),最常见的做法就是使用本地缓存。显然小爱app首页流量不过百,使用redis是没问题的。


LoadingCache的使用场景和局限性


对于这上面说的热key问题,我们最直接的做法就是使用本地缓存,比如你最熟悉的guava的LoadingCache,但是使用本地缓存要求能够接受一定的脏数据,因为如果你更新了首页,本地缓存是不会更新的,它只会根据一定的过期策略来重新加载缓存,不过在我们这个场景是完全没问题的,因为一旦在后台推送了首页后就不会再去改变了。即使改变了也没问题,可以设置写过期为半小时,超过半小时重新加载缓存,这种短时间内的脏数据我们是可以接受的。


LoadingCache导致的缓存击穿


虽然说本地缓存和机器上强相关的,虽然代码层面写的是半小时过期,但由于每台机器的启动时间不同,导致缓存的加载时间不同,过期时间也就不同,也就不会所有机器上的请求在同一时间缓存失效后都去请求数据库。但是对于单一一台机器也是会导致缓存穿透的,假如有10台机器,每台1000的qps,只要有一台缓存过期就可能导致这1000个请求同时打到了数据库。这种问题其实比较好解决,但是容易被忽略,也就是在设置LoadingCache的时候使用LoadingCache的load-miss方法,而不是直接判断cache.getIfPresent()== null然后去请求db;前者会加虚拟机层面的锁,保证只有一个请求打到数据库去,从而完美的解决了这个问题。


但是,如果对于实时性要求较高的情况,比如有段时间要经常做活动,我要保证活动页面能近实时更新,也就是运营在后台配置好了活动信息后,需要在C端近实时展示这次配置的活动信息,此时使用LoadingCache肯定就不能满足了。


ReloadableCache的使用场景和局限性


对于上面说的LoadingCache不能解决的实时问题,可以考虑使用ReloadableCache,这是快手开源的一个本地缓存框架,最大的特点是支持多机器同时更新缓存,假设我们修改了首页信息,然后请求打到的是A机器,这个时候重新加载ReloadableCache,然后它会发出通知,监听了同一zk节点的其他机器收到通知后重新更新缓存。使用这个缓存一般的要求是将全量数据加载到本地缓存,所以如果数据量过大肯定会对gc造成压力,这种情况就不能使用了。由于小爱同学首页这个首页是带有状态的,一般online状态的就那么两个,所以完全可以使用ReloadableCache来只装载online状态的首页。


小结


到这里三种缓存基本都介绍完了,做个小结:



  1. 对于非热点的数据访问,比如用户维度的数据,直接使用redis即可;

  2. 对于热点数据的访问,如果流量不是很高,无脑使用redis即可;

  3. 对于热点数据,如果允许一定时间内的脏数据,使用LoadingCache即可;

  4. 对于热点数据,如果一致性要求较高,同时数据量不大的情况,使用ReloadableCache即可;


小技巧


不管哪种本地缓存虽然都带有虚拟机层面的加锁来解决击穿问题,但是意外总有可能以你意想不到的方式发生,保险起见你可以使用两级缓存的方式即本地缓存+redis+db。


缓存使用的简单介绍


这里redis的使用就不再多说了,相信很多人对api的使用比我还熟悉


LoadingCache的使用


这个是guava提供的网上一抓一大把,但是给两点注意事项



  1. 要使用load-miss的话, 要么使用V get(K key, Callable<? extends V> loader);要么使用build的时候使用的是build(CacheLoader<? super K1, V1> loader)这个时候可以直接使用get()了。此外建议使用load-miss,而不是getIfPresent==null的时候再去查数据库,这可能导致缓存击穿;

  2. 使用load-miss是因为这是线程安全的,如果缓存失效的话,多个线程调用get的时候只会有一个线程去db查询,其他线程需要等待,也就是说这是线程安全的。


LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000L)
.expireAfterAccess(Duration.ofHours(1L)) // 多久不访问就过期
.expireAfterWrite(Duration.ofHours(1L)) // 多久这个key没修改就过期
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
// 数据装载方式,一般就是loadDB
return key + " world";
}
});
String value = cache.get("hello"); // 返回hello world

reloadableCache的使用


导入三方依赖


<dependency>
<groupId>com.github.phantomthief</groupId>
<artifactId>zknotify-cache</artifactId>
<version>0.1.22</version>
</dependency>

需要看文档,不然无法使用,有兴趣自己写一个也行的。


public interface ReloadableCache<T> extends Supplier<T> {

/**
* 获取缓存数据
*/
@Override
T get();

/**
* 通知全局缓存更新
* 注意:如果本地缓存没有初始化,本方法并不会初始化本地缓存并重新加载
*
* 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()}
*/
void reload();

/**
* 更新本地缓存的本地副本
* 注意:如果本地缓存没有初始化,本方法并不会初始化并刷新本地的缓存
*
* 如果需要初始化本地缓存,请先调用 {@link ReloadableCache#get()}
*/
void reloadLocal();
}

老生常谈的缓存击穿/穿透/雪崩问题


这三个真的是亘古不变的问题,如果流量大确实需要考虑。


缓存击穿


简单说就是缓存失效,导致大量请求同一时间打到了数据库。对于缓存击穿问题上面已经给出了很多解决方案了。



  1. 比如使用本地缓存

  2. 本地缓存使用load-miss方法

  3. 使用第三方服务来加载缓存


1.2和都说过,主要来看3。假如业务愿意只能使用redis而无法使用本地缓存,比如数据量过大,实时性要求比较高。那么当缓存失效的时候就得想办法保证只有少量的请求打到数据库。很自然的就想到了使用分布式锁,理论上说是可行的,但实际上存在隐患。我们的分布式锁相信很多人都是使用redis+lua的方式实现的,并且在while中进行了轮训,这样请求量大,数据多的话会导致无形中让redis成了隐患,并且占了太多业务线程,其实仅仅是引入了分布式锁就加大了复杂度,我们的原则就是能不用就不用。


那么我们是不是可以设计一个类似分布式锁,但是更可靠的rpc服务呢?当调用get方法的时候这个rpc服务保证相同的key打到同一个节点,并且使用synchronized来进行加锁,之后完成数据的加载。在快手提供了一个叫cacheSetter的框架。下面提供一个简易版,自己写也很容易实现。


import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;

/**
* @Description 分布式加载缓存的rpc服务,如果部署了多台机器那么调用端最好使用id做一致性hash保证相同id的请求打到同一台机器。
**/
public abstract class AbstractCacheSetterService implements CacheSetterService {

private final ConcurrentMap<String, CountDownLatch> loadCache = new ConcurrentHashMap<>();

private final Object lock = new Object();

@Override
public void load(Collection<String> needLoadIds) {
if (CollectionUtils.isEmpty(needLoadIds)) {
return;
}
CountDownLatch latch;
Collection<CountDownLatch> loadingLatchList;
synchronized (lock) {
loadingLatchList = excludeLoadingIds(needLoadIds);

needLoadIds = Collections.unmodifiableCollection(needLoadIds);

latch = saveLatch(needLoadIds);
}
System.out.println("needLoadIds:" + needLoadIds);
try {
if (CollectionUtils.isNotEmpty(needLoadIds)) {
loadCache(needLoadIds);
}
} finally {
release(needLoadIds, latch);
block(loadingLatchList);
}

}

/**
* 加锁
* @param loadingLatchList 需要加锁的id对应的CountDownLatch
*/
protected void block(Collection<CountDownLatch> loadingLatchList) {
if (CollectionUtils.isEmpty(loadingLatchList)) {
return;
}
System.out.println("block:" + loadingLatchList);
loadingLatchList.forEach(l -> {
try {
l.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}

/**
* 释放锁
* @param needLoadIds 需要释放锁的id集合
* @param latch 通过该CountDownLatch来释放锁
*/
private void release(Collection<String> needLoadIds, CountDownLatch latch) {
if (CollectionUtils.isEmpty(needLoadIds)) {
return;
}
synchronized (lock) {
needLoadIds.forEach(id -> loadCache.remove(id));
}
if (latch != null) {
latch.countDown();
}
}

/**
* 加载缓存,比如根据id从db查询数据,然后设置到redis中
* @param needLoadIds 加载缓存的id集合
*/
protected abstract void loadCache(Collection<String> needLoadIds);

/**
* 对需要加载缓存的id绑定CountDownLatch,后续相同的id请求来了从map中找到CountDownLatch,并且await,直到该线程加载完了缓存
* @param needLoadIds 能够正在去加载缓存的id集合
* @return 公用的CountDownLatch
*/
protected CountDownLatch saveLatch(Collection<String> needLoadIds) {
if (CollectionUtils.isEmpty(needLoadIds)) {
return null;
}
CountDownLatch latch = new CountDownLatch(1);
needLoadIds.forEach(loadId -> loadCache.put(loadId, latch));
System.out.println("loadCache:" + loadCache);
return latch;
}

/**
* 哪些id正在加载数据,此时持有相同id的线程需要等待
* @param ids 需要加载缓存的id集合
* @return 正在加载的id所对应的CountDownLatch集合
*/
private Collection<CountDownLatch> excludeLoadingIds(Collection<String> ids) {
List<CountDownLatch> loadingLatchList = Lists.newArrayList();
Iterator<String> iterator = ids.iterator();
while (iterator.hasNext()) {
String id = iterator.next();
CountDownLatch latch = loadCache.get(id);
if (latch != null) {
loadingLatchList.add(latch);
iterator.remove();
}
}
System.out.println("loadingLatchList:" + loadingLatchList);
return loadingLatchList;
}
}

业务实现


import java.util.Collection;
public class BizCacheSetterRpcService extends AbstractCacheSetterService {
@Override
protected void loadCache(Collection<String> needLoadIds) {
// 读取db进行处理
// 设置缓存
}
}

缓存穿透


简单来说就是请求的数据在数据库不存在,导致无效请求打穿数据库。


解法也很简单,从db获取数据的方法(getByKey(K key))一定要给个默认值。


比如我有个奖池,金额上限是1W,用户完成任务的时候给他发笔钱,并且使用redis记录下来,并且落表,用户在任务页面能实时看到奖池剩余金额,在任务开始的时候显然奖池金额是不变的,redis和db里面都没有发放金额的记录,这就导致每次必然都去查db,对于这种情况,从db没查出来数据应该缓存个值0到缓存。


缓存雪崩


就是大量缓存集中失效打到了db,当然肯定都是一类的业务缓存,归根到底是代码写的有问题。可以将缓存失效的过期时间打散,别让其集中失效就可以了。


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

摆脱USB线,使用无线连接去开发安卓

前言 工作了大半年,之前一直都需要USB线连接手机才能用Android Studio去调试和安装安卓APP,然后上个礼拜,我突然发现前辈没连无线就可以调试,这让我好奇心一下上来,但又不好意思问,于是搜索了一下关于无线调试的内容,就看到谷歌早就给安卓用上了无线调...
继续阅读 »

前言


工作了大半年,之前一直都需要USB线连接手机才能用Android Studio去调试和安装安卓APP,然后上个礼拜,我突然发现前辈没连无线就可以调试,这让我好奇心一下上来,但又不好意思问,于是搜索了一下关于无线调试的内容,就看到谷歌早就给安卓用上了无线调试,只不过我一直不知道。


image.png
经过我探究了一番,踩了许多坑,最终于今天总算是知道如何稳定的进行无线连接了。


正篇


先感叹一下,不得不说,无线调试真的好用,总算不需要担心线绕来绕去。


一波三折


第一波风雨


为什么我对无线调试这么喜悦,原因就在于我手上的这个手机实在太坑,我几乎半年都被它所折磨,其实我一报型号你们就会明白了,它叫XiaoMi11,将它与电脑连接,结果说是在充电但电量一直在减少,而且那条线也在我一直连接高强度使用后露出了内部结构,所以又换了一根。


第二波浪潮


无线调试功能知晓后,我也是仔细阅读官方文档后,先把adb找到,配置到环境


image.png
我的路径在C:\Users\86152\AppData\Local\Android\Sdk\platform-tools,其实目的就是将这个包含adb.exe的platform-tools路径添加到系统环境变量Path:


image.png
这步完成后,直接win+r,再输入cmd回车之后输入adb devices即可看到自己连接的设备,这时也代表adb已经配好了环境。(本人是在Windows环境下的操作)

接着看看自己手机无线网是不是和电脑在一个局域下,直接ping自己手机ip,如果正常收发包即代表可以无线调试,作者是电脑连着经过路由器的线,而无线网也是该路由器发出的,不一定要电脑也连无线网:


image.png


adb devices

ping 你的手机IP 可以在手机无线调试的地方找到,文中下面的图可以看到

再查看自己手机已经是Android12,是大于官方给出的Android11,于是选择第一种连接方式:


image.png
就是如下图,直接在Android Studio中选择这个功能进行无线连接
image.png
如下图,它提供了两种方式,扫二维码,或者在手机开发者选项的无线调试使用验证码:


image.png
image.png

我又按照文档操作,打开了手机的无线调试:


image.png
结果无论是扫描二维码还是使用验证码都如石沉大海,了无音讯,一直就没成功过,就搁这一直转圈。


60b1098f1ac18e3273cb7dce45ee990.jpg
bb2dbd11f03c0b899bd38a4dc4a0f61.jpg

第三波呼啸


迫不得已,我又看了一些博客的老方法与官网对Android11以下的处理终于连上了,不过我每次都得打开手机无线调试看到IP地址和端口全部输进去才能连,而我就这么傻乎乎的连了一个星期:


6c7581671af66ac12318d52be2c7ed8.jpg

解决方法


所以作者在此不介绍这个不大好用的方法了,直接聊聊最好用的吧:
配置完adb环境后可以重启Android Studio,然后我们打开下图的AS的命令编辑器,同样可以输入adb命令了,当然同样可以ping命令去看看是否可以接通。


image.png
首先我们先输入命令:


image.png
会发现第一次给出failed,再输入一次报already connect即可连接成功


adb connect 手机ip地址(即无线调试中IP地址和端口中":xxxx"前面部分)

下次只要你手机仍连接此WIFI,只需要AS每次打开时输入过上面命令即可,不过在第一次连接时要同意手机上的弹窗哦,不然后面可能无线调试在连WIFI时不会自动开启,需要手动去点。


总结


快快掌握这个方法,让我们开启无线调试的安卓开发生活吧!


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

以感知生命周期的方式观察 Flow 数据

问题 Android 是有生命周期的,在 UI 展示的时候可以接受一些数据更新 UI,在 App 进入后台的时候应该停止接受数据以便释放资源,并且避免一些意想不到的异常; 协程和 Flow 是和 Android 平台无关的 API,正常情况下无法感知 ...
继续阅读 »

问题



  • Android 是有生命周期的,在 UI 展示的时候可以接受一些数据更新 UI,在 App 进入后台的时候应该停止接受数据以便释放资源,并且避免一些意想不到的异常;




  • 协程和 Flow 是和 Android 平台无关的 API,正常情况下无法感知 Android 生命周期方法;


解决方案


我们现在当然是有一些方案来解决上述问题,可能会有以下几种方式。


Flow Lifecycle-aware.gif


方式一:手动取消 Job


Activity 方式:



Fragment 方式:



以上方式显然是比较麻烦的,有一些模板代码。


方式二:使用 repeatOnLifecycle 解决模板代码


Activity 方式:



Fragment 方式:




注:需要添加依赖 androidx.lifecycle:lifecycle-runtime-ktx



这种方式虽然解决了一些模板代码的问题,但是仍然有多层嵌套的问题。


方式三:使用 flowWithLifecycle 解决多层嵌套


Activity 方式:




注:此 API 是在 2.6.0-alpha01 版本中添加。



Fragment 方式类似,使用 flowWithLifecycle 即可。flowWithLifecycle 是新版中新增的一个扩展函数,实现方式如下:



看上去是解决了嵌套的问题,但是并彻底,观察数据的操作还是需要在协程体中进行。


方式四:使用 collectWithLifecycle 彻底解决多层嵌套



自定义扩展函数 collectWithLifecycle 实现大致如下:



这个是我自己的一个扩展,官方库中并没有添加,不过我已经反馈给官方了。这种方式仅仅是解决了嵌套的问题,但是却隐藏了 Flow ,从而导致一系列的操作符将无法使用,不过这个在 UI Element/Compose 中并不是什么大问题。。


扩展阅读


处理 Android UI 的方式之外,Compose 本身也有同样的问题,官方也通过自定义扩展函数的形式添加了支持,大致如下:



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

Flutter3.3对Material3设计风格的支持

在Flutter3.3版本以上,支持Material3,使用Material3样式首先是要配置启用Material3。 Material3 主要体现在 圆角风格、颜色、文本样式等方面。 1 配置启用 Material3 查看当前 Flutter的版本 在程序...
继续阅读 »

在Flutter3.3版本以上,支持Material3,使用Material3样式首先是要配置启用Material3。


Material3 主要体现在 圆角风格、颜色、文本样式等方面。


1 配置启用 Material3


查看当前 Flutter的版本


image.png
在程序的入口 MaterialApp中设置主题ThemeData中的useMaterial3属性为true.


///flutter应用程序中的入口函数
void main() => runApp(TextApp());

///应用的根布局
class TextApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
///构建Materia Desin 风格的应用程序
return MaterialApp(
title: "Material3",
///在主题中启用
theme: ThemeData(
brightness: Brightness.light,
colorSchemeSeed: Colors.blue,
//启用
useMaterial3: true,
),
///默认的首页面
home: Material3Home(),
);
}
}

2 按钮样式的改变


按钮默认的圆角大小改变


2.1 ElevatedButton

ElevatedButton(
onPressed: (){},
child: const Text("Elevated"),
),


image.png


2.2 ElevatedButton 自定义样式

  ElevatedButton(
style: ElevatedButton.styleFrom(
// 前景色
// ignore: deprecated_member_use
onPrimary: Theme.of(context).colorScheme.onPrimary,
// 背景色
// ignore: deprecated_member_use
primary: Theme.of(context).colorScheme.primary,
).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
onPressed: (){},
child: const Text('Filled'),
),

image.png


2.3 OutlinedButton

OutlinedButton(
onPressed: (){},
child: const Text("Outlined"),
),

image.png


2.4 FloatingActionButton.small

 FloatingActionButton.small(
onPressed: () {},
child: const Icon(Icons.add),
),

image.png


2.5 FloatingActionButton

FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),

image.png


2.6 FloatingActionButton.extended

   FloatingActionButton.extended(
onPressed: () {},
icon: const Icon(Icons.add),
label: const Text("Create"),
),

image.png


2.7 FloatingActionButton.large

  FloatingActionButton.large(
onPressed: () {},
child: const Icon(Icons.add),
),

image.png


3 AlertDialog 的边框圆角改变


  void openDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text("Basic Dialog Title"),
content: const Text(
"A dialog is a type of modal window that appears in front of app content to provide critical information, or prompt for a decision to be made."),
actions: <Widget>[
TextButton(
child: const Text('Dismiss'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text('Action'),
onPressed: () => Navigator.of(context).pop(),
),
],
),
);
}

image.png


4 主题文本样式的默认大小改变


final textTheme = Theme.of(context)
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);


TextStyle large =textTheme.displayLarge;
TextStyle displayMedium =textTheme.displayMedium;
TextStyle displaySmall =textTheme.displaySmall;


image.png


5 ColorScheme 的变更


Widget buildMaterial() {
///当前颜色主题
ColorScheme colorScheme = Theme.of(context).colorScheme;
//背景色
final Color color = colorScheme.surface;
//阴影色
Color shadowColor = colorScheme.shadow;
Color surfaceTint = colorScheme.primary;
return Material(
borderRadius: BorderRadius.all(Radius.circular(4.0)),
elevation: 4,//阴影
color: color,//背景色
shadowColor: shadowColor,//阴影色
surfaceTintColor: surfaceTint,//
type: MaterialType.card,
child: Padding(
padding: const EdgeInsets.all(38.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'测试1',
style: Theme.of(context).textTheme.labelMedium,
),
Text(
'测试2',
style: Theme.of(context).textTheme.labelMedium,
),
],
),
),
);
}

image.png


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

环信文档提升计划|提建议找bug,领京东卡,环信文档“捉虫“活动进行中!

文档是帮助开发者更好地使用产品的重要一环。为更好的提升集成环信产品的便捷性和易用性,环信文档进行了重大升级,此次改版主要提升内容: 1.导航点击转跳丝滑顺畅 ;2.使用体验更好的搜索插件;3.开发者可以直接对文档进行反馈,提交github pul...
继续阅读 »

文档是帮助开发者更好地使用产品的重要一环。为更好的提升集成环信产品的便捷性和易用性,环信文档进行了重大升级,此次改版主要提升内容:

1.导航点击转跳丝滑顺畅 ;2.使用体验更好的搜索插件;3.开发者可以直接对文档进行反馈,提交github pull request


此次版本的重大更新离不开环友们的日常反馈和建议需求,目前环信正式发起文档提升计划,邀您一起打造更加丝滑的文档体验。无论你是第一次接触环信的新朋友还是深度用户,都欢迎参与此次计划。我们将有专人跟进您的反馈和建议。在参与过程中,你可以感受到文档一点点被打磨被完善的过程,感受到所提的建议被采纳的乐趣,还能累计积分兑换环信周边礼品。


活动时间:

即日至2024年12月31日


参与方式:

此次活动文档包含环信即时通讯云文档、超级社区文档。

反馈在文档中发现或遇到的问题,或针对文档提出的建议意见,发送邮件至:market@easemob.com

3个工作日内平台进行反馈。

经评估确认后,可获得一定积分奖励,累计的积分可兑换环信周边或京东卡。


积分规则:

错误类型

具体描述

积分

不规范或低错类

错别字

1分

拼写错误

表述不通顺,病句等

文档格式错乱

内容结构不合理、语言表达不清晰

图形、表格、文字等晦涩难懂

2分

描述存在歧义或有误

缺少必要的条件,说明,注意事项等

不合理的文档结构

内容错误或缺失

界面/功能和文档描述不一致

3分

代码有误,无法指导操作

链接错误

关键步骤错误或缺失

响应结果与文档描述不符

文档接口与SDK不符

内容未更新

内容过时,文档接口过时等,无法指导集成步骤

3分


活动规则:

1、请将详细的文档问题描述发送邮件至 market@easemob.com,邮件内容经官方确认属实,邮件反馈您提交的问题及获得的积分;

2、除以上表格罗列的内容以外,您发现的任何问题都可以邮件反馈给我们,我们都会酌情归类计分。

3、本次活动积分有效期至2024年6月30日,您可在活动期间联系官方兑换礼品。


积分兑换

达到以下积分,即可扣除相应积分进行礼品兑换。

积分

奖励

3分

优秀环友定制徽章

5分

环信定制钥匙扣/小风扇2选1

10分

50元京东卡/环信定制保温杯 2选1

15分

100元京东卡


加入官方活动群


扫码备注“捉虫”,添加冬冬好友拉你进群。


相关地址及开发文档获取

即时通讯云文档:https://docs-im-beta.easemob.com/document/android/quickstart.html

环信超级社区文档:https://docs-im.easemob.com/ccim/circle/overview


*本活动解释权归环信所有

收起阅读 »

Sourcery 的 Swift Package 命令行插件

什么是Sourcery?Sourcery 是当下最流行的 Swift 代码生成工具之一。其背后使用了 SwiftSyntax[1],旨在通过自动生成样板代码来节省开发人员的时间。Sourcery 通过扫描一组输入文件,然后借助模板的帮助,自动生成模板中定义的 ...
继续阅读 »

什么是Sourcery?

Sourcery 是当下最流行的 Swift 代码生成工具之一。其背后使用了 SwiftSyntax[1],旨在通过自动生成样板代码来节省开发人员的时间。Sourcery 通过扫描一组输入文件,然后借助模板的帮助,自动生成模板中定义的 Swift 代码。

示例

考虑一个为摄像机会话服务提供公共 API 的协议:

protocol Camera {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}

当使用此新的 Camera service 进行单元测试时,我们希望确保 AVCaptureSession 没有被真的创建。我们仅仅希望确认 camera service 被测试系统(SUT)正确的调用了,而不是去测试 camera service 本身。
因此,创建一个协议的 mock 实现,使用空方法和一组变量来帮助我们进行单元测试,并断言(asset)进行了正确的调用是有意义的。这是软件开发中非常常见的一个场景,如果你曾维护过一个包含大量单元测试的大型代码库,这么做也可能有点乏味。
好吧~不用担心!Sourcery 会帮助你!⭐️ 它有一个叫做 AutoMockable[2] 的模板,此模板会为任意输入文件中遵守 AutoMockable 协议的协议生成 mock 实现。

注意:在本文中,我扩展地使用了术语 Mock,因为它与 Sourcery 模板使用的术语一致。Mock 是一个相当重载的术语,但通常,如果我要创建一个 双重测试[3],我会根据它的用途进一步指定类型的名称(可能是 Spy 、 Fake 、 Stub 等)。如果您有兴趣了解更多关于双重测试的信息,马丁·福勒(Martin Fowler)有一篇非常好的文章,可以解释这些差异。

现在,我们让 Camera 遵守 AutoMockable。该接口的唯一目的是充当 Sourcery 的目标,从中查找并生成代码。

import UIKit

// Protocol to be matched
protocol AutoMockable {}

public protocol Camera: AutoMockable {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}

此时,可以在上面的输入文件上运行 Sourcery 命令,指定 AutoMockable 模板的路径:

sourcery --sources Camera.swift --templates AutoMockable.stencil --output .

💡 本文通过提供一个 .sourcery.yml 文件来配置 Sourcery 插件。如果提供了配置文件或 Sourcery 可以找到配置文件,则将忽略与其值冲突的所有命令行参数。如果您想了解有关配置文件的更多信息,Sourcery的 repo 中有一节[4]介绍了该主题。
命令执行完毕后,在输出目录下会生成一个 模板名 加 .generated.swift 为后缀的文件。在此例是 ./AutoMockable.generated.swift:

// Generated using Sourcery 1.8.2 — https://github.com/krzysztofzablocki/Sourcery
// DO NOT EDIT
// swiftlint:disable line_length
// swiftlint:disable variable_name

import Foundation
#if os(iOS) || os(tvOS) || os(watchOS)
import UIKit
#elseif os(OSX)
import AppKit
#endif

class CameraMock: Camera {

//MARK: - start

var startCallsCount = 0
var startCalled: Bool {
return startCallsCount > 0
}
var startClosure: (() -> Void)?

func start() {
startCallsCount += 1
startClosure?()
}

//MARK: - stop

var stopCallsCount = 0
var stopCalled: Bool {
return stopCallsCount > 0
}
var stopClosure: (() -> Void)?

func stop() {
stopCallsCount += 1
stopClosure?()
}

//MARK: - capture

var captureCallsCount = 0
var captureCalled: Bool {
return captureCallsCount > 0
}
var captureReceivedCompletion: ((UIImage?) -> Void)?
var captureReceivedInvocations: [((UIImage?) -> Void)] = []
var captureClosure: ((@escaping (UIImage?) -> Void) -> Void)?

func capture(_ completion: @escaping (UIImage?) -> Void) {
captureCallsCount += 1
captureReceivedCompletion = completion
captureReceivedInvocations.append(completion)
captureClosure?(completion)
}

//MARK: - rotate

var rotateCallsCount = 0
var rotateCalled: Bool {
return rotateCallsCount > 0
}
var rotateClosure: (() -> Void)?

func rotate() {
rotateCallsCount += 1
rotateClosure?()
}

}

上面的文件(AutoMockable.generated.swift)包含了你对mock的期望:使用空方法实现与目标协议的一致性,以及检查是否调用了这些协议方法的一组变量。最棒的是… Sourcery 为您编写了这一切!🎉

怎么运行 Sourcery?

怎么使用 Swift package 运行 Sourcery?
至此你可能在想如何以及怎样在 Swift package 中运行 Sourcery。你可以手动执行,然后讲文件拖到包中,或者从包目录中的命令运行脚本。但是对于 Swift Package 有两种内置方式运行可执行文件:
通过命令行插件,可根据用户输入任意运行
通过构建工具插件,该插件作为构建过程的一部分运行。
在本文中,我将介绍 Sourcery 命令行插件,但我已经在编写第二部分,其中我将创建构建工具插件,这带来了许多有趣的挑战。

创建插件包

让我们首先创建一个空包,并去掉测试和其他我们现在不需要的文件夹。然后我们可以创建一个新的插件 Target 并添加 Sourcery 的二进制文件作为其依赖项。
为了让消费者使用这个插件,它还需要被定义为一个产品:

// swift-tools-version: 5.6
import PackageDescription

let package = Package(
name: "SourceryPlugins",
products: [
.plugin(name: "SourceryCommand", targets: ["SourceryCommand"])
],
targets: [
// 1
.plugin(
name: "SourceryCommand",
// 2
capability: .command(
intent: .custom(verb: "sourcery-code-generation", description: "Generates Swift files from a given set of inputs"),
// 3
permissions: [.writeToPackageDirectory(reason: "Need access to the package directory to generate files")]
),
dependencies: ["Sourcery"]
),
// 4
.binaryTarget(
name: "Sourcery",
path: "Sourcery.artifactbundle"
)
]
)

让我们一步一步地仔细查看上面的代码:

1.定义插件目标。
2.以 custom 为意图,定义了 .command 功能,因为没有任何默认功能( documentationGeneration 和 sourceCodeFormatting)与该命令的用例匹配。给动词一个合理的名称很重要,因为这是从命令行调用插件的方式。
3.插件需要向用户请求写入包目录的权限,因为生成的文件将被转储到该目录。
4.为插件定义了一个二进制目标文件。这将允许插件通过其上下文访问可执行文件。
💡 我知道我并没有详细介绍上面的一些概念,但如果您想了解更多关于命令插件的信息,这里有一篇由 Tibor Bödecs 写的超级棒的文章⭐。如果你还想了解更多关于 Swift Packages 中二级制的目标(文件),我同样有一篇现今 Swift 包中的二进制目标。

编写插件

现在已经创建了包,是时候编写一些代码了!我们首先在 Plugins/SourceryCommand 下创建一个名为 SourceryCommand.swift 的文件,然后添加一个 CommandPlugin 协议的结构体,这将作为该插件的入口:

import PackagePlugin
import Foundation

@main
struct SourceryCommand: CommandPlugin {
func performCommand(context: PluginContext, arguments: [String]) async throws {

}
}

然后我们为命令编写实现:

func performCommand(context: PluginContext, arguments: [String]) async throws {
// 1
let configFilePath = context.package.directory.appending(subpath: ".sourcery.yml").string
guard FileManager.default.fileExists(atPath: configFilePath) else {
Diagnostics.error("Could not find config at: \(configFilePath)")
return
}

//2
let sourceryExecutable = try context.tool(named: "sourcery")
let sourceryURL = URL(fileURLWithPath: sourceryExecutable.path.string)

// 3
let process = Process()
process.executableURL = sourceryURL

// 4
process.arguments = [
"--disableCache"
]

// 5
try process.run()
process.waitUntilExit()

// 6
let gracefulExit = process.terminationReason == .exit && process.terminationStatus == 0
if !gracefulExit {
Diagnostics.error("🛑 The plugin execution failed")
}
}

让我们仔细看看上面的代码:

1.首先 .sourcery.yml 文件必须在包的根目录,否则将报错。这将使 Sourcery 神奇的工作,并使包可配置。
2.可执行文件路径的 URL 是从命令的上下文中检索的。
3.创建一个进程,并将 Sourcery 的可执行文件的 URL 设置为其可执行文件路径。
4.这一步有点麻烦。Sourcery 使用缓存来减少后续运行的代码生成时间,但问题是这些缓存是在包文件夹之外读取和写入的文件。插件的沙箱规则不允许这样做,因此 --disableCache 标志用于禁用此行为并允许命令运行。
5.进程同步运行并等待。
6.最后,检查进程终止状态和代码,以确保进程已正常退出。在任何其他情况下,通过 Diagnostics API 向用户告知错误。
就这样!现在让我们使用它

使用(插件)包

考虑一个用户正在使用插件,该插件将依赖项引入了他们的 Package.swift 文件:

// swift-tools-version: 5.6
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "SourceryPluginSample",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "SourceryPluginSample",
targets: ["SourceryPluginSample"]),
],
dependencies: [
.package(url: "https://github.com/pol-piella/sourcery-plugins.git", branch: "main")
],
targets: [
.target(
name: "SourceryPluginSample",
dependencies: [],
exclude: ["SourceryTemplates"]
),
]
)

💡 注意,与构建工具插件不同,命令插件不需要应用于任何目标,因为它们需要手动运行。
用户只使用了上面的 AutoMockable 模板(可以在 Sources/SourceryPluginSample/SourceryTemplates 下找到),与本文前面显示的示例相匹配:

protocol AutoMockable {}

protocol Camera: AutoMockable {
func start()
func stop()
func capture(_ completion: @escaping (UIImage?) -> Void)
func rotate()
}

根据插件的要求,用户还提供了一个位于 SourceryPluginSample 目录下的 .sourcery.yml 配置文件:

sources:
- Sources/SourceryPluginSample
templates:
- Sources/SourceryPluginSample/SourceryTemplates
output: Sources/SourceryPluginSample

运行命令

用户已经设置好了,但是他们现在如何运行包?🤔 有两种方法:

命令行

运行插件的一种方法是用命令行。可以通过从包目录中运行 swift package plugin --list 来检索特定包的可用插件列表。然后可以从列表中选择一个包,并通过运行 swift package <command's verb> 来执行,在这个特殊的例子中,运行: swift package sourcery-code-generation。
注意,由于此包需要特殊权限,因此 --allow-writing-to-package-directory 必须与命令一起使用。
此时,你可能会想,为什么我要费心编写一个插件,仍然必须从命令行运行,而我可以用一个简单的脚本在几行 bash 中完成相同的工作?好吧,让我们来看看 Xcode 14 中会出现什么,你会明白为什么我会提倡编写插件📦。

Xcode

这是运行命令插件最令人兴奋的方式,但不幸的是,它仅在 Xcode 14 中可用。因此,如果您需要运行命令,但尚未使用 Xcode 14,请参阅命令行部分。
如果你正好在使用 Xcode 14,你可以通过在文件资源管理器中右键单击包,从列表中找到要执行的插件,然后单击它来执行包的任何命令。

下一步

这是插件的初始实现。我将研究如何改进它,使它更加健壮。和往常一样,我非常致力于公开构建,并使我的文章中的所有内容都开源,这样任何人都可以提交问题或创建任何具有改进或修复的 PRs。这没有什么不同😀, 这是 公共仓库的链接。
此外,如果您喜欢这篇文章,请关注即将到来的第二部分,其中我将制作一个 Sourcery 构建工具插件。我知道这听起来不多,但这不是一项容易的任务!

收起阅读 »

后端一次给你10万条数据,如何优雅展示,到底考察我什么

web
前言大家好,我是林三心,用最通俗的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天跟大家来唠唠嗑,如果后端真的返回给前端10万条数据,咱们前端要怎么优雅地展示出来呢?(哈哈假设后端真的能传10万条数据到前端)前置工作先把前置工作给做好,后面才能进...
继续阅读 »

前言

大家好,我是林三心,用最通俗的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心,今天跟大家来唠唠嗑,如果后端真的返回给前端10万条数据,咱们前端要怎么优雅地展示出来呢?(哈哈假设后端真的能传10万条数据到前端)


前置工作

先把前置工作给做好,后面才能进行测试

后端搭建

新建一个server.js文件,简单起个服务,并返回给前端10w条数据,并通过nodemon server.js开启服务

没有安装nodemon的同学可以先全局安装npm i nodemon -g

// server.js

const http = require('http')
const port = 8000;

http.createServer(function (req, res) {
 // 开启Cors
 res.writeHead(200, {
   //设置允许跨域的域名,也可设置*允许所有域名
   'Access-Control-Allow-Origin': '*',
   //跨域允许的请求方法,也可设置*允许所有方法
   "Access-Control-Allow-Methods": "DELETE,PUT,POST,GET,OPTIONS",
   //允许的header类型
   'Access-Control-Allow-Headers': 'Content-Type'
})
 let list = []
 let num = 0

 // 生成10万条数据的list
 for (let i = 0; i < 100000; i++) {
   num++
   list.push({
     src: 'https://p3-passport.byteacctimg.com/img/user-avatar/d71c38d1682c543b33f8d716b3b734ca~300x300.image',
     text: `我是${num}号嘉宾林三心`,
     tid: num
  })
}
 res.end(JSON.stringify(list));
}).listen(port, function () {
 console.log('server is listening on port ' + port);
})

前端页面

先新建一个index.html

// index.html

// 样式
<style>
   * {
     padding: 0;
     margin: 0;
  }
   #container {
     height: 100vh;
     overflow: auto;
  }
  .sunshine {
     display: flex;
     padding: 10px;
  }
   img {
     width: 150px;
     height: 150px;
  }
 </style>

// html部分
<body>
 <div id="container">
 </div>
 <script src="./index.js"></script>
</body>

然后新建一个index.js文件,封装一个AJAX函数,用来请求这10w条数据

// index.js

// 请求函数
const getList = () => {
   return new Promise((resolve, reject) => {
       //步骤一:创建异步对象
       var ajax = new XMLHttpRequest();
       //步骤二:设置请求的url参数,参数一是请求的类型,参数二是请求的url,可以带参数
       ajax.open('get', 'http://127.0.0.1:8000');
       //步骤三:发送请求
       ajax.send();
       //步骤四:注册事件 onreadystatechange 状态改变就会调用
       ajax.onreadystatechange = function () {
           if (ajax.readyState == 4 && ajax.status == 200) {
               //步骤五 如果能够进到这个判断 说明 数据 完美的回来了,并且请求的页面是存在的
               resolve(JSON.parse(ajax.responseText))
          }
      }
  })
}

// 获取container对象
const container = document.getElementById('container')

直接渲染

最直接的方式就是直接渲染出来,但是这样的做法肯定是不可取的,因为一次性渲染出10w个节点,是非常耗时间的,咱们可以来看一下耗时,差不多要消耗12秒,非常消耗时间


const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   list.forEach(item => {
       const div = document.createElement('div')
       div.className = 'sunshine'
       div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
       container.appendChild(div)
  })
   console.timeEnd('列表时间')
}
renderList()

setTimeout分页渲染

这个方法就是,把10w按照每页数量limit分成总共Math.ceil(total / limit)页,然后利用setTimeout,每次渲染1页数据,这样的话,渲染出首页数据的时间大大缩减了


const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   console.log(list)
   const total = list.length
   const page = 0
   const limit = 200
   const totalPage = Math.ceil(total / limit)

   const render = (page) => {
       if (page >= totalPage) return
       setTimeout(() => {
           for (let i = page * limit; i < page * limit + limit; i++) {
               const item = list[i]
               const div = document.createElement('div')
               div.className = 'sunshine'
               div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
               container.appendChild(div)
          }
           render(page + 1)
      }, 0)
  }
   render(page)
   console.timeEnd('列表时间')
}

requestAnimationFrame

使用requestAnimationFrame代替setTimeout,减少了重排的次数,极大提高了性能,建议大家在渲染方面多使用requestAnimationFrame

const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   console.log(list)
   const total = list.length
   const page = 0
   const limit = 200
   const totalPage = Math.ceil(total / limit)

   const render = (page) => {
       if (page >= totalPage) return
       // 使用requestAnimationFrame代替setTimeout
       requestAnimationFrame(() => {
           for (let i = page * limit; i < page * limit + limit; i++) {
               const item = list[i]
               const div = document.createElement('div')
               div.className = 'sunshine'
               div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
               container.appendChild(div)
          }
           render(page + 1)
      })
  }
   render(page)
   console.timeEnd('列表时间')
}

文档碎片 + requestAnimationFrame

文档碎片的好处

  • 1、之前都是每次创建一个div标签就appendChild一次,但是有了文档碎片可以先把1页的div标签先放进文档碎片中,然后一次性appendChildcontainer中,这样减少了appendChild的次数,极大提高了性能

  • 2、页面只会渲染文档碎片包裹着的元素,而不会渲染文档碎片

const renderList = async () => {
   console.time('列表时间')
   const list = await getList()
   console.log(list)
   const total = list.length
   const page = 0
   const limit = 200
   const totalPage = Math.ceil(total / limit)

   const render = (page) => {
       if (page >= totalPage) return
       requestAnimationFrame(() => {
           // 创建一个文档碎片
           const fragment = document.createDocumentFragment()
           for (let i = page * limit; i < page * limit + limit; i++) {
               const item = list[i]
               const div = document.createElement('div')
               div.className = 'sunshine'
               div.innerHTML = `<img src="${item.src}" /><span>${item.text}</span>`
               // 先塞进文档碎片
               fragment.appendChild(div)
          }
           // 一次性appendChild
           container.appendChild(fragment)
           render(page + 1)
      })
  }
   render(page)
   console.timeEnd('列表时间')
}

懒加载

为了比较通俗的讲解,咱们启动一个vue前端项目,后端服务还是开着

其实实现原理很简单,咱们通过一张图来展示,就是在列表尾部放一个空节点blank,然后先渲染第1页数据,向上滚动,等到blank出现在视图中,就说明到底了,这时候再加载第二页,往后以此类推。

至于怎么判断blank出现在视图上,可以使用getBoundingClientRect方法获取top属性

IntersectionObserver 性能更好,但是我这里就拿getBoundingClientRect来举例


<script setup lang="ts">
import { onMounted, ref, computed } from 'vue'
const getList = () => {
 // 跟上面一样的代码
}

const container = ref<HTMLElement>() // container节点
const blank = ref<HTMLElement>() // blank节点
const list = ref<any>([]) // 列表
const page = ref(1) // 当前页数
const limit = 200 // 一页展示
// 最大页数
const maxPage = computed(() => Math.ceil(list.value.length / limit))
// 真实展示的列表
const showList = computed(() => list.value.slice(0, page.value * limit))
const handleScroll = () => {
 // 当前页数与最大页数的比较
 if (page.value > maxPage.value) return
 const clientHeight = container.value?.clientHeight
 const blankTop = blank.value?.getBoundingClientRect().top
 if (clientHeight === blankTop) {
   // blank出现在视图,则当前页数加1
   page.value++
}
}

onMounted(async () => {
 const res = await getList()
 list.value = res
})
</script>

<template>
 <div id="container" @scroll="handleScroll" ref="container">
   <div class="sunshine" v-for="(item) in showList" :key="item.tid">
     <img :src="item.src" />
     <span>{{ item.text }}</span>
   </div>
   <div ref="blank"></div>
 </div>
</template>

结语

如果你觉得此文对你有一丁点帮助,点个赞,鼓励一下林三心哈哈。

作者:Sunshine_Lin
来源:juejin.cn/post/7031923575044964389

收起阅读 »

超好用的官方core-ktx库,了解一下(终)~

ktx
Handler.postDelayed()简化lambda传入 不知道大家在使用Handler下的postDelayed()方法是不是感觉很不简洁,我们看下这个函数源码: public final boolean postDelayed(@NonNull Ru...
继续阅读 »

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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

【Android爬坑周记】用SplashScreen做一个会动的开屏!

Android 12以上加入了SplashScreen,并且支持开屏动画了!因此我在【小鹅事务所】项目中加入了一个开屏动画,如下(为方便动图展示,我故意延长了几秒钟): SplashScreen 简单介绍一下SplashScreen,仅在冷启动或者温启动的时...
继续阅读 »

Android 12以上加入了SplashScreen,并且支持开屏动画了!因此我在【小鹅事务所】项目中加入了一个开屏动画,如下(为方便动图展示,我故意延长了几秒钟):


开屏.gif


SplashScreen


简单介绍一下SplashScreen,仅在冷启动或者温启动的时候会展示SplashScreen,支持VAD动画、帧动画。我就先使用帧动画实现这个开屏动画,后面会考虑换成VAD动画。关于SplashScreen具体就不细讲啦,我讲这些讲不明白,没有官方文档讲得好,直接进入实战!!


注意裁切


image_bbWXUd5R2b.png


ICON在设计的时候只能够占用三分之二大小的圆,超出这部分的会被裁切掉,所以这点需要注意!


设计


首先打开UI设计软件,我此处用Figma,新建一个方形的框框,方形的框框里面整一个三分二大小的圆圈,像这样。


image_zN3wn3Qj0_.png


然后呢,就把设计好的Icon放进去


image_nPXCuEjgOv.png


这个时候一张静态图就做好啦,但是帧动画需要让图片动起来的话,就需要多张静态图。怎么设计它动起来呢?我的思路是让它扭头!像这样。


image_bKPGvPkiso.png


然后再把框框的颜色隐藏掉,我们只需要透明背景的Icon


image_9vcZawLYyF.png


注意,为了展示外边需要留空间,我给它们的框框加上描边,实际不需要!这个时候就可以导出图片啦,我这边选择导出矢量图,也就是SVG格式。


image_tg-XgUBqeW.png


导入动画


打开Android Studio,右键点击res → new → Vector Asset,再导入图片,将静态图都导进去就可以做动画啦。


image_7DgCf5ExTe.png


新建anim_little_goose.xml,根标签是animation-list,并在里面放4个item。


<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false">
<item
android:drawable="@drawable/ic_little_goose"
android:duration="50" />
<item
android:drawable="@drawable/ic_little_goose_back"
android:duration="150" />
<item
android:drawable="@drawable/ic_little_goose"
android:duration="50" />
<item
android:drawable="@drawable/ic_little_goose_fore"
android:duration="150" />
</animation-list>

根据命名可以看出




  • 第一帧为正常的小鹅,展示50毫秒




  • 第二帧为向后扭头的小鹅,展示150毫秒




  • 第三帧为正常的小鹅,展示50毫秒




  • 第四帧为向前扭头的小鹅,展示150毫秒




一次循环就是400毫秒,点开Split界面,就能在右边预览动画了,这个时候,动画就简简单单做好了。


As.gif


SplashScreen


引入依赖


由于SplashScreen是Android12以上才有的,而Android12以下需要适配,但是!Jetpack提供了同名适配库,去gradle引用就好了。


//SplashScreen
implementation 'androidx.core:core-splashscreen:1.0.0'

设置开屏主题


然后在res/values/themes中新建一个style标签,并将其父标签设为Theme.SplashScreen,需要注意的是,如果适配了黑夜模式的话,也可以在values-night/themes文件下单独配置。


<style name="Theme.AppSplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenBackground">@color/primary_color</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/anim_little_goose</item>
<item name="windowSplashScreenAnimationDuration">3000</item>
<item name="postSplashScreenTheme">@style/Theme.Account</item>
</style>

我这边配置一个无ICON背景的动画,因此不用windowSplashScreenIconBackgroundColor标签设置ICON背景。


简单介绍一下我设置的4个标签




  • windowSplashScreenBackground设置整个开屏动画的背景颜色。




  • windowSplashScreenAnimatedIcon设置的是开屏动画播放的动画文件,也就是上面写的动画文件。




  • windowSplashScreenAnimationDuration设置的是动画的播放时长,也就是说小鹅抖三秒钟头就会停止播放。




  • postSplashScreenTheme这个设置的是开屏动画播放完需要回到的主题,此处设置了我的主题。


    <style name="Theme.Account" parent="Theme.MaterialComponents.DayNight.NoActionBar">
    ...
    </style>



在Manifest注册


    <application
android:label="@string/app_name"
...
android:theme="@style/Theme.Account">

...
<activity
android:name=".ui.MainActivity"
android:theme="@style/Theme.AppSplashScreen"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>

可以看到在打开应用打开的第一个Activity,即MainActivity中设置了开屏主题,而在Application中设置了自己的主题。在Application设置主题的话,这个Application中的除了特殊设置Theme的Activity,其它都默认使用Application主题。


去MainActivity吧!


class MainActivity : BaseActivity() {

private val binding by viewBinding(ActivityMainBinding::inflate)

override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !isAppInit }
super.onCreate(savedInstanceState)
initView()
}
...

}

重写onCreate函数,并在调用super.onCreate之前加载SplashScreen,即调用installSplashScreen,获得一个splashScreen实例,理论上来说调用installSplashScreen函数已经可以实现开屏动画了,可是我想等到一部分数据加载完再进入APP怎么办?


可以看到我调用了setKeepOnScreenCondition 函数,传入一个接口,这个接口返回一个Boolean值,如果返回true则继续展示开屏,如果返回false则进入APP。而此函数在每次绘制之前都会调用,是主线程调用的,因此不能在这里处理太多东西阻塞主线程!


我这边就设置了一个顶层变量,每次都去看看这个顶层变量的值,不会阻塞主线程。


class AccountApplication : Application() {

val supervisorScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

//初始化数据 防止第一次打开还要加载
private fun initData() {
supervisorScope.launch {
val initIconDataDeferred = async { TransactionIconHelper.initIconData() }
val initTransactionDeferred = async { TransactionHelper.initTransaction() }
val initScheduleDeferred = async { ScheduleHelper.initSchedule() }
val initNoteDeferred = async { NoteHelper.initNote() }
val initMemorialsDeferred = async { MemorialHelper.initMemorials() }
val initTopMemorialDeferred = async { MemorialHelper.initTopMemorial() }
val initDataStoreDeferred = async { DataStoreHelper.INSTANCE.initDataStore() }
initIconDataDeferred.await()
initTransactionDeferred.await()
initScheduleDeferred.await()
initNoteDeferred.await()
initMemorialsDeferred.await()
initTopMemorialDeferred.await()
initDataStoreDeferred.await()
isAppInit = true
}
}
}

var isAppInit = false

我在Application中对所有需要初始化的东西先初始化一遍,初始化完之后再将isAppInit设置为true,此时在闪屏那边获取的为false,也就是说就会进入APP了。


到这里就结束了,去运行一下吧!


开屏.gif


总结


说实话,在我看来,SplashScreen其实用处不大,因为我们的闪屏一般是用来放advertisement的,而不是放有趣的动画的!


参考


SplashScreen: developer.android.google.cn/develop/ui/…


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

Android APT实战学习技巧

apt
简介 APT(Annotation Processing Tool)即注解处理器,在编译的时候可以处理注解然后搞一些事情,也可以在编译时生成一些文件之类的。ButterKnife和EventBus都使用了APT技术,如果不会APT技术就很难看懂这两个框架的源码...
继续阅读 »

简介


APT(Annotation Processing Tool)即注解处理器,在编译的时候可以处理注解然后搞一些事情,也可以在编译时生成一些文件之类的。ButterKnife和EventBus都使用了APT技术,如果不会APT技术就很难看懂这两个框架的源码。


作用


使用APT可以在编译时来处理编译时注解,生成额外的Java文件,有如下效果:



  • 可以达到减少重复代码手工编写的效果。



如ButterKnife,我们可以直接使用注解来减少findviewbyid这些代码,只需要通过注解表示是哪个id就够了。




  • 功能封装。将主要的功能逻辑封装起来,只保留注解调用。

  • 相对于使用Java反射来处理运行时注解,使用APT有着更加良好的性能。


Android基本编译流程


Android中的代码编译时需要经过:Java——>class ——> dex 流程,代码最终生成dex文件打入到APK包里面。


编译流程如图所示:




  • APT是在编译开始时就介入的,用来处理编译时注解。

  • AOP(Aspect Oridnted Programming)是在编译完成后生成dex文件之前,通过直接修改.class文件的方式,来对代码进行修改或添加逻辑。常用在在代码监控,代码修改,代码分析这些场景。


基本使用


基本使用流程主要包括如下几个步骤:



  1. 创建自定义注解

  2. 创建注解处理器,处理Java文件生成逻辑

  3. 封装一个供外部调用的API

  4. 项目中调用


整理思路



  1. 首先我们需要创建两个JavaLibrary

  2. 一个用来定义注解,一个用来扫描注解

  3. 获取到添加注解的成员变量名

  4. 动态生成类和方法用IO生成文件


实战


创建一个空项目



创建两个JavaLibrary



  • 注解的Lib: apt-annotation

  • 扫描注解的Lib: apt-processor




创建完之后



app模块依赖两个Library


implementation project(path: ':apt-annotation')
annotationProcessor project(path: ':apt-processor')


注解Lib中创建一个注解类


如果还不会自定义注解的同学,可以先去看我之前写的一篇Java自定义注解入门到实战


@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Print {

}


扫描注解的Lib添加依赖


dependencies {
//自动注册,动态生成 META-INF/...文件
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
//依赖apt-annotation
implementation project(path: ':apt-annotation')
}


创建扫描注解的类



重写init方法,输出Hello,APT


注意: 这里是JavaLib,所以不能使用Log打印,这里可以使用Java的println()或注解处理器给我们提供的方法,建议使用注解处理器给我们提供的



现在我们已经完成了APT的基本配置,现在我们可以build一下项目了,成败在此一举



如果你已经成功输出了文本,说明APT已经配置好,可以继续下一步了


继续完成功能


现在我们可以继续完成上面要实现的功能了,我们需要先来实现几个方法


/**
* 要扫描扫描的注解,可以添加多个
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> hashSet = new HashSet<>();
hashSet.add(Print.class.getCanonicalName());
return hashSet;
}

/**
* 编译版本,固定写法就可以
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return processingEnv.getSourceVersion();
}


定义注解


我们先在MianActivity中添加两个成员变量并使用我们定义的注解



定义注解


真正解析注解的地方是在process方法,我们先试试能不能拿到被注解的变量名


/**
* 扫描注解回调
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//拿到所有添加Print注解的成员变量
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Print.class);
for (Element element : elements) {
//拿到成员变量名
Name simpleName = element.getSimpleName();
//输出成员变量名
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,simpleName);
}
return false;
}


编译试一下



生成类


既然能拿到被注解的变量名,后面就简单了,我们只需要用字符串拼出来一个工具类,然后用IO流写到本地就ok了



查看效果


现在点击一下编译,然后我们可以看到app模块下的build文件已经有我们生成的类了



调用方法


现在我们回到MainActivity,就可以直接调用这个动态生成的类了



总结


优点:


它可以做任何你不想做的繁杂的工作,它可以帮你写任何你不想重复代码,将重复代码抽取出来,用AOP思想去编写。 它可以生成任何java代码供你在任何地方使用。


难点:


在于设计模式和解耦思想的灵活应用。在于代理类代码生成的繁琐:你可以手动进行字符串拼接,也可以用squareup公司的javapoet库来构建出任何你想要的java代码。


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

安卓之如何优雅的处理Activity回收突发事件

情景与原因 前面的文章说过,我的一个业务要从页面A进入页面B,也就意味着我的应用出现了在ActivityA的基础上启动了ActivityB的情景,这个时候ActivityA就进入了停止状态,但这个时候如果出现系统内存不足的情况,就会把ActivityA回收掉,...
继续阅读 »

情景与原因


前面的文章说过,我的一个业务要从页面A进入页面B,也就意味着我的应用出现了在ActivityA的基础上启动了ActivityB的情景,这个时候ActivityA就进入了停止状态,但这个时候如果出现系统内存不足的情况,就会把ActivityA回收掉,此时用户按下Back键返回A,仍然会正常显示A,但此时的A是执行onCreate()方法加载的,而不是执行onRestart()方法,也就是说我们的ActivityA页面是被重新创建出来的。


那有什么区别呢,这就是我们平时网上填表之类的最讨厌的情景会浮现了,比如你好不容易把信息填好,点击下一步,然后发现想修改上个页面信息,哎,这时候你会发现由于系统内存不足回收掉了A,于是你辛辛苦苦填好的信息都没了,这太痛了。这一情景的出现就说明回收了A再重新创建会导致我们在A当中存的一些临时数据与状态都可能被丢失。


这就是我们今天要解决的问题。


解决方法


虽然出现这个问题是是否影响用户体验的,但解决方法是非常简单的,因为我们的Activity中还提供了一个onSaveInstanceState()回调方法,它就可以保证在Activity被回收之前一定被调用,而我们就可以利用这一特性去解决上述问题。


方法介绍


onSaveInstanceState()方法中会携带一个Bundle类型的参数,而Bundle提供了一系列方法用于保存我们想要的数据,其中如putString()就是用于保存字符串的,而putInt()方法显而易见也是保存整型数据的,而每个保存方法都需要传入两个参数,其中第一个参数我们称之为键,是用于在Bundle中取值的特定标记,第二个参数是我们要保存的内容,属于键值对类型,学过数据库的一般都知道。


写法


如下,我们可以这么去写:


override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
val tempData = "your temp data"
outState.putString("data_key", tempData)
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState != null) {
val tempData = savedInstanceState.getString("data_key")
}
...
}

在onSaveInstanceState()中保存你想要的数据,在onCreate()中给取出来,这样就不用害怕再创建的时候数据丢失了,不过如果是横竖屏切换造成数据丢失还是依照前文ViewModel的写法更好。


结语


其实基础往往是最容易忽视的,我们之前光在那说高楼大厦如何建成,但地基并不牢靠,所以还是需要落实到基础上最好。


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

聊聊ART虚拟机_对象的分配问题

ART
前置知识 有Android开发基础 了解 Java 语法和 JVM 前言 ART 虚拟机(下图 Runtime 层),相信各位搞 Android 开发的同学都有知道,总体的印象呢就是:ART 与 JVM 不同,其不符合 JVM 规范不属于 JVM ,且为 ...
继续阅读 »

前置知识



  • 有Android开发基础

  • 了解 Java 语法和 JVM


前言


ART 虚拟机(下图 Runtime 层),相信各位搞 Android 开发的同学都有知道,总体的印象呢就是:ART 与 JVM 不同,其不符合 JVM 规范不属于 JVM ,且为 Dalvik 的进阶版。


但是,我们有必要对 ART 进行更加深入的了解,其有助于我们对 Android 的更深层次的理解。所以,本文将和聊一聊 ART 虚拟机,以及 ART 中一个对象是如何分配的。



何为ART虚拟机


在开始阶段,我们还是需要来聊一下什么是 ART 虚拟机,其不同在何处。



探析Android中的四类性能优化一文中,我们有提到 ART 虚拟机是 Google 在 Android4.4 的时候引入的,其用于替代 Dalvik 虚拟机。而在替代 Dalvik 虚拟机的同时,他也是兼容之前的 dex 格式的。ART 与 Dalvik 的不同点如下所示。



ART特性

1. 预编译

Dalvik 中的应用每次运行时,字节码都需要通过即时编译器 JIT 转换为机器码,这会使得应用的运行效率降低。在 ART 中,系统在安装应用时会进行一次预编译(AOT,Ahead-Of-Time),将字节码预先编译成机器码并存储在本地,这样应用就不用在每次运行时执行编译了,运行效率也大大提高。


2. 垃圾回收算法

在 Dalvik 采用的垃圾回收算法是标记-清除算法,启动垃圾回收机制会造成两次暂停(一次在遍历阶段,另一次在标记阶段)。而在 ART 下,GC 速度比 Dalvik 要快,这是因为应用本身做了垃圾回收的一些工作,启动 GC 后,不再是两次暂停,而是一次暂停,而且 ART 使用了一种新技术(packard pre-cleaning),在暂停前做了许多事情,减轻了暂停时的工作量。


3. 64 位

Dalvik 是为 32 位 CPU 设计的,而 ART 支持 64 位并兼容 32 位 CPU,这也是 Dalvik 被淘汰的主要原因。



由此可知,ART 让 Android 的性能有了很大的提升,从 2015 直到现在,我们使用的都还是 ART 虚拟机。


下图为 ART 的整体架构,我们可以看出,上层是执行层,负责直接对书写的代码进行处理,而下层则为运行时刻对 Java 语法的支持。


ART架构


对象的分配


对于对象的分配问题,实际上是 ART 对于类的管理问题。而类中则是描述了一个对象的内存布局(类成员的大小、类型和排布)和其函数信息。


例如 Object 类,包含以下的信息:


一个保存的是类型定义,一个保存的是锁的信息。


Object 类


类加载


一个类分配的对象的大小,是由继承链所决定的。当 Java 中的类首次使用的时候,就会进行类加载。例如首次使用到一个子类的时候,会自动将继承链上面的所有父类都进行加载,而整个继承链上面的类的总和就是该子类的大小。


例如下文中的子类的大小就是 AWork + BaseWork 两者合起来的大小。


puvlic class AWork extends BaseWork{

public AWork(WorkBean workBean){
super(work);
}

@Override
public process(Processbean processbean){
workBean.getA().actionA(processbean.getProcessA);
}
}

内存布局


如下图所示,当有 A->B->Object 这个继承关系的时候,其内存布局是父类在上,子类在下的方式进行排布的。而在每一个类里面,则是将引用类型置于最上方,而其他的类型则按字母顺序进行排序。


内存布局


双亲继承(双亲委派)


何为双亲继承呢?


双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父类加载器去完成,每一层都是如此。一直递归到顶层,当父加载器无法完成这个请求时,子类才会尝试去加载。


这么做的好处有一点,那就是不会出现假的委派父类,我们在委派的时候按照既定的逻辑寻找、只有在继承链上面的才是正确的,使得不会有虚假的父类出现。


这类底层的逻辑,反映出合理的继承链是有利于设计和执行的。其实由此我们也可以看到,其实很多设计原则的道理和这些底层逻辑设计也是相同的,例如迪米特原则和接口隔离原则,都是反映出继承链要合理,不要贪多的思维。


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

我爸53岁了,居然还能找到年薪25万的管理岗位,突然很羡慕传统行业!

在35岁焦虑席卷许多打工人的时候,一位53岁的老父亲竟然找到了年薪25万的管理岗位,他的儿子不禁感叹“突然有点羡慕传统行业”!有人问楼主父亲是什么行业?楼主回答:造船。楼主说,父亲之前在央企做项目经理,年薪也有四十几万,后来得罪人被降职,辞职后失业两年,尝试过...
继续阅读 »

在35岁焦虑席卷许多打工人的时候,一位53岁的老父亲竟然找到了年薪25万的管理岗位,他的儿子不禁感叹“突然有点羡慕传统行业”!


有人问楼主父亲是什么行业?

楼主回答:造船。


楼主说,父亲之前在央企做项目经理,年薪也有四十几万,后来得罪人被降职,辞职后失业两年,尝试过很多职业,经历了这么大落差,心态还这么好,真的很佩服他。


许多网友纷纷出来爆料自己的家人也有类似经历,大多都是五六十岁还能找到不错的工作,或者挣的钱比自己还多。

网友感叹:治好了自己的精神内耗。


有人说,这才是正常的,在一个行业耕耘多年,有经验的人不该失业,在传统行业里,三四十岁正是挑大梁的时候,年龄越大挣得越多。

有人说,深耕一个领域的人不缺offer ,因为有不可替代性。打铁还需自身硬,只要是人才,走到哪里都是人才。

有人建议应届生选一个能在一个赛道做久的行业,不要因为一点涨幅就频繁换行业。


但也有人说,就算年薪25万,应届生依然不愿意去一些行业,因为传统行业真的很苦。


不是每一个行业都是吃青春饭,也不是每一个行业都有35岁红线,相反,许多行业是越老越值钱,比如医生、教师、律师、会计、制造等。在这些行业里,年龄大意味着更丰富的经验和阅历,可以担当更重要的责任,承担更重要的工作,自然也能拿到更高的薪资。

可能是互联网行业的声音更容易被听到,时间久了,人们觉得高薪和大龄被裁是所有行业的现状。其实在我们不注意的地方,在许多低调的传统行业里,那些大龄打工人也生活得很好,甚至比互联网行业从业者还好。

所以,在选择行业和赛道时,别总盯着眼前的一亩三分地,多去了解了解那些不起眼的行业,说不定会有意外之喜。

作者:行者

来源:devabc

收起阅读 »

安卓之如何优雅的处理Activity回收突发事件

情景与原因前面的文章说过,我的一个业务要从页面A进入页面B,也就意味着我的应用出现了在ActivityA的基础上启动了ActivityB的情景,这个时候ActivityA就进入了停止状态,但这个时候如果出现系统内存不足的情况,就会把ActivityA回收掉,此...
继续阅读 »

情景与原因

前面的文章说过,我的一个业务要从页面A进入页面B,也就意味着我的应用出现了在ActivityA的基础上启动了ActivityB的情景,这个时候ActivityA就进入了停止状态,但这个时候如果出现系统内存不足的情况,就会把ActivityA回收掉,此时用户按下Back键返回A,仍然会正常显示A,但此时的A是执行onCreate()方法加载的,而不是执行onRestart()方法,也就是说我们的ActivityA页面是被重新创建出来的。

那有什么区别呢,这就是我们平时网上填表之类的最讨厌的情景会浮现了,比如你好不容易把信息填好,点击下一步,然后发现想修改上个页面信息,哎,这时候你会发现由于系统内存不足回收掉了A,于是你辛辛苦苦填好的信息都没了,这太痛了。这一情景的出现就说明回收了A再重新创建会导致我们在A当中存的一些临时数据与状态都可能被丢失。

这就是我们今天要解决的问题。

解决方法

虽然出现这个问题是是否影响用户体验的,但解决方法是非常简单的,因为我们的Activity中还提供了一个onSaveInstanceState()回调方法,它就可以保证在Activity被回收之前一定被调用,而我们就可以利用这一特性去解决上述问题。

方法介绍

onSaveInstanceState()方法中会携带一个Bundle类型的参数,而Bundle提供了一系列方法用于保存我们想要的数据,其中如putString()就是用于保存字符串的,而putInt()方法显而易见也是保存整型数据的,而每个保存方法都需要传入两个参数,其中第一个参数我们称之为键,是用于在Bundle中取值的特定标记,第二个参数是我们要保存的内容,属于键值对类型,学过数据库的一般都知道。

写法

如下,我们可以这么去写:

override fun onSaveInstanceState(outState: Bundle) {
   super.onSaveInstanceState(outState)
   val tempData = "your temp data"
   outState.putString("data_key", tempData)
}
override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  setContentView(R.layout.activity_main)
  if (savedInstanceState != null) {
      val tempData = savedInstanceState.getString("data_key")
  }
  ...
}

在onSaveInstanceState()中保存你想要的数据,在onCreate()中给取出来,这样就不用害怕再创建的时候数据丢失了,不过如果是横竖屏切换造成数据丢失还是依照前文ViewModel的写法更好。

结语

其实基础往往是最容易忽视的,我们之前光在那说高楼大厦如何建成,但地基并不牢靠,所以还是需要落实到基础上最好。

作者:ObliviateOnline
来源:juejin.cn/post/7158096746583687205

收起阅读 »

整体学历较高,硕士占比达 40%,周星驰也开始招募Web3人才!

编辑:Datawhale近期,周星驰发布人才招募令。人才要求:熟悉Web3,有项目管理经验,有头脑还又宅心仁厚;工作范围:助我建造创意未来;还提醒对此职位感兴趣的候选人投简历时请贴出个人简介影片或Web3作品并tag,他本人会亲自拣人。随着数字经济的发展,We...
继续阅读 »

编辑:Datawhale

近期,周星驰发布人才招募令。

人才要求:熟悉Web3,有项目管理经验,有头脑还又宅心仁厚;工作范围:助我建造创意未来;还提醒对此职位感兴趣的候选人投简历时请贴出个人简介影片或Web3作品并tag,他本人会亲自拣人。


随着数字经济的发展,Web3 成为了新的风口,各大厂商和投资人纷纷将目光聚集其上。在部分人眼中,Web3 能够重塑数字金融交易体系,改变全球竞争格局,在未来互联网上实现弯道超车。其中,不乏顶级投资机构红杉和 A16Z,前者甚至一度将简介更改为 DAO。


不过,时不时出现的裁员新闻又让行业之外的人对 Web3 望而生畏。如 Coinbase 此前宣布裁员 18%,规模高达上千人。当然,Web3 也存在正不断开放招聘岗位的企业。

传说中可以跨国分布式从业的 Web3 是精英遍布还是草根丛生,什么样的人才是目前 Web3 企业所需要的成为了大部分用户想了解的信息呢?

Web3企业需要的是什么人才?

中国人才增速较低,但需求强劲

从宏观上来看,截至 2022 年 6 月,区块链人才总量同比增加了 76%,其中,印度、新加坡和美国增速最高,分别为 122%、92%、62%,中国相对较低,仅仅只有 12%。

在人才总量增加的同时,人才需求量呈现出远超供应的增长。根据领英人才大数据洞察获得的数据来看,2021 年相较 2020 年人才需求呈倍数级增长,其中,加拿大增速最高,达到了 560%。印度、新加坡、美国、中国的增速分别为 145%、180%、82%、78%。


虽然供需缺口,但事实上,除了科技和金融公司以外,大部分区块链的人才主要又以内部流动为主。领英人才大数据洞察显示,2021 年至今,人才主要在 Coinbase、Crypto.com、Gemeni、Rippl e 等区块链企业间交叉流动。而外部流入的人才主要来自华尔街和硅谷等地知名巨头如高盛、JPMorgan、HSBS、谷歌、微软、Facebook 等。

从人才的需求端和供应端来看,区块链的人才受到地区限制少,在全球各地都呈现需求量增长的趋势。但是从绝对数量上来看,美国、法国、英国等发达国家依旧占据着优势。对于金融业和 IT 业较为繁荣的国家而言,切入区块链和 Web3 存在着不小的产业优势。

具体到中国的区块链产业上,根据 IDC 研究预测,中国 2020-2025 年区块链市场规模年复合增长率将达 54.6%,增速位居全球第一。而全球区块链市场规模年复合增长率将达 48%。

换言之,中国目前的区块链产业对人才的需求量大,但进入该行业的人才少,同时,中国区块链产业后续增长强劲。从报告来看,换方向从事 Web3 行业的工作对于个人的发展而言或许是一个不错的选择。

核心人才需求主要以金融和研发为主

相较于 Web1 和 Web2 而言,Web3 的定义更宽泛。目前业内对于 Web3 并没有严格的定义,不过其有几个较为明显的特征,比如数据的确权与授权、隐私保护、去中心化运行等等。而这些明显的特征决定了行业主要人才的构成。

从全球区块链领域人才构成上分析,金融、研发、业务开发、信息技术、销售人才为全球区块链前五大人才类型。


全球区块链领域前五大人才类型中,最热门细分职业分别为加密货币交易员、软件工程师、分析师、支持分析师及客户经理。


从人才增速来看,测试工程师、密码逻辑技术专家、合规分析师、设计师和支持分析师分别位列前五,其中,测试工程师增速高达 713%。

从人才构成和需求来看,可以发现,区块链行业发展依旧处于早期阶段,大量的基础设施正在搭建。区块链人才的构成成分最主要还是取决于行业的发展。在行业发展的初期阶段往往需要大量基础性的工作职位,如研发、开发、产品构建。如想要等区块链行业发展更加成熟之后再参与这个行业,或许可以锻炼自己运营、营销、市场等方面的能力。

同时,需要注意的是,不同的国家和地区对于人才的需求也有较为明显的差异,人才容易在地域上产生集聚效应,如大量工程师聚集于硅谷。在考虑城市和职业方向的同时,或许还得思考城市和职业的契合度。如在区块链领域,新加坡侧重于产品经理、软件工程师的招聘,而中国香港更侧重于产品设计师、用户体验作者等。

人才竞争初始,硕士从业人数占比 40%

由于市场对区块链人才的需求远远超过供应,区块链从业者的平均薪资已经超过了大部分行业。

据 Glassdoor 报告,美国区块链开发人员的平均年基本工资为 9.1 万美元。而 2020 年美国社会安全署数据显示,美国民众平均年薪 5.3 万美元,中位数为 3.4 万美元。同样,北京人社局于 2021 年 11 月发布的《2021 年北京市人力资源市场薪酬大数据报告》,在 30 个新职业薪酬排行榜中,区块链工程技术人员最高,年度薪酬中位值达 48.7 万人民币。北、上、广、深等重点城市区块链产业人才平均年薪水平大幅领先城市整体产业人才平均年薪水平。

高薪促使着优秀的人才向区块链行业聚拢,目前全球区块链领域中学士群体占 59%,硕士占比达 40%,整体学历较高。同时,数据显示,全球区块链人才排名前十的学校均为世界知名院校,其中包括加州大学伯克利分校、斯坦福大学、哈佛大学等顶级大学。

与此同时,中国包括中央财经大学、同济大学、浙江大学等在内的多所双一流大学也开设了区块链课程。

综合来看,目前区块链行业依旧处于起始阶段,各国的政策扶持力度正在不断加大,人才流动频繁,需求量巨大。作为从业者,除了学历等硬性指标外,还需要持续拓展延伸自己的能力,从而持续构建核心竞争力。

来源:Datawhale


收起阅读 »

我的灿烂前端人生

本人是 95 前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回公司太子 北京时间 18 点 50 分,离下班时间还有十分钟,本该是令人愉悦的时刻...
继续阅读 »

本人是 95 前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回

公司太子


北京时间 18 点 50 分,离下班时间还有十分钟,本该是令人愉悦的时刻,我心里的雾霾又浓郁了一分。因为我在公司当太子当了大半年了。



能力出众



遥想今年年初,领着上家公司大礼包四处求职碰壁,踏破铁鞋寻寻觅觅,靠着投机取巧的八股文背诵,终于求得广州一家高大上小企业公司的岗位。入职不到一周立刻加入新的项目团队,做一个抽奖小程序,技术栈是 typescript+taro,我之前没有深入开发过,十分的开心,又可以边工作边学习了。花费三个多月,与团队之间不断擦枪走火,这个项目也是勉强完成,开发完成之余,我有空也加入了测试大军,生怕自己第一个项目上线后因为自己的 bug 造成毁灭的影响毕竟以前经常发生。万万没想到,这个项目最终没有落地,老板总结就是我们做的打不过别人竞品,没啥创新,让团队去搞商城小程序去了。我万分失落惊喜,心里想着这样岂不是等于我做了三个月的项目稳定在线上运行,没有bug,不会被用户投诉,也不会被影响绩效,安稳白嫖三个月薪资?美滋滋!。


度过三个月的试用期,因为项目线上无 bug,能力出众,我也如愿以偿拿下转正。



虚空需求



完成了上一任务,接下来 leader 给我分配了一个大 project,重构以前管理后台的权限。这波重构任务,是 leader 直接文字需求下达指令了,我有点头皮发麻,好几年没遇到这种需求了,真的是梦回 S1 赛季,本来和我合作的小伙伴说他要做个原型出来,结果因为分配任务我负责管理后台前端,他负责管理后台 nodejs 的代码,他也就没有做出来,让原型图随风而去,跟我说了句一把梭。我也想一把梭,但我发现 leader 的需求十分灵性,加之我对之前的业务也不熟悉,想着还是花点时间加班把原型图做一下吧。


我战战兢兢的把原型图发到群里,leader 已读并回了没啥问题了,可以开工。我悬着的心放了下来,撸起袖子大胆干。说实话,我心里其实很慌的,首先对 React+Typescript 不熟悉,且这套管理后台十分深奥,用的是自研的核心框架,各种 typescript abstract 抽象类,复杂的类型泛型,对我这个半吊子前端还是比较吃力的。但好在我是拷贝忍者,写业务代码先找下之前代码是怎么写的,CCCV,改个英文单词,就是我的杰作



TX leader 真的很严格



我的 leader 是腾讯大厂出来的,我也是打心底里对他有一丝敬畏,毕竟大厂大佬恐怖如斯,技术水平肯定不是我这种切图仔比拟的。


任务花费了三周多一点,包含联调自测,自测完后就提个 MQ 上去了,信心十足。万万没想到,leaderCode Review 对着我的杰作一顿输出,大概有二十几个修改建议,我都有仔细去看,发现很多都是代码规范,代码优化,leader 都给了一定的建议。说实话,一开始我的心里多多少少有些芥蒂,但是谁让别人是领导呢?开个玩笑。但是 leader 指出来的问题的确是不容忽视的,程序员就是要有更好的追求,其实有人把问题指出来,才是对我最大的帮助,我也是花了不少时间去更改这些问题。下面就放一些 bad code 出来献丑。




之前一直想不明白,传进来的组件是在 children 里面,我如何去改变组件的点击函数,想来想去想不懂,脑门一热直接在组件上加一层蒙层,通过蒙层阻碍组件点击,当时设计完出来我还挺高兴,leader 也直呼天才,送了我两个字 ———— 重做


因为我技术能力确实平庸,只能请教我的良师百度,不断去寻找 children 是否有什么方法或钩子处理事件,功夫不负有心人,果真被我找到了。下面就是修改后的方法

// after
return permission ? children : React.Children.map(children, child => React.cloneElement(child as React.ReactElement, { onClick: () => { message.error('无权限'); } }));

ps:leader 也勉为其难的接受这个方法,可能他不知道有什么更好的方法。如果观众大佬们知道,可以提下意见,不胜感激。

设计组织架构图

07rebuild.png

先让大伙看看原来的功能图吧,之后我们开了一个会议,这里要重做。


我心想我发原型图出来的时候,大佬您可是没有半个不字,怎么 codereview 直接改了一个方向了啊?

不过,毕竟他是我的 leader,我的生死全由他掌控,我也不敢多言,上网找了一个 npm 库 react-organizational-chart。react 的社区就是强~下面是更改后的视图

不得不说,的确是更饱满更清晰直观了一些,leader 还是很有远见的怕他也上掘金,吹了再说


这个项目陆陆续续做了三个月了,因为 leader 平时也很忙,两个城市飞,导致这个项目的进度也进展缓慢,而我就在空闲时间上上掘金学习技术,刷刷 leetcode。


来了大半年,我深刻明白我对公司的建设为 0,所做项目为公司带来 0 收入,就是我的价值完全没有体现,公司把我当太子养了大半年,我非常感谢公司。然后每天都会浏览 boss 直聘,深怕下午就被拉进小黑屋,在这个大环境下,我也时刻准备着,毕竟也有前车之鉴,我明白我只是个平庸的程序员,只能尽力做好自己的本分,随时做好最坏的打算,当真正的打击来临之时,我也不会手忙脚乱。

灿烂?摆烂!


最近 IT 的 HRBP 要我一个新入职的去做一场技术分享,我在这里呆了大半年,没有等来其他前端大佬的分享,竟然是要我亲自上阵,小丑竟是我自己



空虚寂寞冷



回想了一下这六个月,其实自己的水平真的没有半点进步,我想不到有什么可以拿来分享的。而且从入职以来,我在这个公司说的话可能没有超过 100句,其实有时我也纳闷,我印象中自己不是一个这么闷的一个人,在上家公司我吹 * 技术游走于天地之间,能很好的融入团队,并能展开身心为其奋斗前期战神,后期老油条。但是来了新公司之后,我只会干完手头上的活,也没有跟其他同事聊聊天,不过我附近的同事也极少聊天,感觉稍微有点死气沉沉。


以前年轻的时候,看到一些新入职的同事,闷葫芦一个,找他搭话或者说骚话,他都没啥兴趣,现在的我,好像成为了自己以前眼中的怪人。我苦思久已,只能得出几个结论,第一点可能是我以前投入太多,经历过分离,不想再投入更多的感情,投入的越深,离开时就越痛 1000-7=? 痛,太痛了。第二点是因为现在的大环境,让我精神焦虑,我深怕我和某位同事今天刚去饭堂吃个饭,明天人就没了。想看我之前为啥被裁,可以看我往期文章


不过,我觉得出来工作,重点是挣钱,以这个为核心,其他一切都是空谈。而且,解决我的聊天需求还有一大神器,不是陌陌,而是网易狼人杀APP。自从入职新公司以来,每天下班回到家根本不想学习,不想运动,只想躺着,然后冲进大师场厮杀,里面个个都是人才,说话又好听,我喜欢这个游戏,因为它能锻炼提高我的骗人能力当然是表达能力啦!而且它还夹杂着些许人性的味道,人性的魅力也让我欲罢不能。网易打钱。所以要我分享,我真不知道分享什么,难道分享如何悍跳吃警徽,狼查杀狼打板子做高狼同伴身份?



保持平常心



最终 leader 让我去分享一下这个重构项目,我想了一下也可以,其实它不是一次分享,可以把它当做一次项目复盘,把自己的问题抛出来给到大家欣赏,虽然有点丢人,但是赚钱嘛,不寒碜。而且自己的技术也拉胯,可以让自己加深这些问题的印象,对自己成长的路也是有极大帮助的。


不止是大环境,最近社会也出现了许多光怪陆离的事情,心态也有些许变化,我不再绞尽脑汁去想着如何跳槽获得高薪,我只想取悦自己,做自己认为让自己开心而正确的事情,心累了就去外面走走,馋了就去吃点美食,觉得知识匮乏了就化身小厂做题家刷刷 leetcode,看看别人的源码见解虽然多数都看不懂。偶尔什么都想学,什么都学不进去的时候,也会焦虑,解决焦虑的办法,我常常是...... 奖励自己


当下所面临的的困难、焦虑,都会被时间而抚平,我作为一个平庸程序员,面对每天新开始的人生,我只能对自己说一句,啊,又是新的一天

链接:https://juejin.cn/post/7122401595966357518
来源:稀土掘金
收起阅读 »

组员大眼瞪小眼,forEach 处理异步任务遇到的坑

一位组员遇到一个问题,几个同事都没能帮忙解决,我在这边就开门见山直接描述当时他遇到的问题。他在 forEach 处理了异步,但是始终不能顺序执行,至此想要的数据怎么都拿不到,组员绞尽脑汁,不知道问题发生在哪里。此篇文章我们就来探究下 forEach 循环下处理...
继续阅读 »

一位组员遇到一个问题,几个同事都没能帮忙解决,我在这边就开门见山直接描述当时他遇到的问题。他在 forEach 处理了异步,但是始终不能顺序执行,至此想要的数据怎么都拿不到,组员绞尽脑汁,不知道问题发生在哪里。此篇文章我们就来探究下 forEach 循环下处理异步会发生什么样的情况。

探索

我们先看一段简单的 forEach 处理异步的代码

//forEach 处理
let promiseTasek = (num) => {
return new Promise((resolve, rejece) => {
setTimeout(() => {
console.log(num)
resolve(true)
}, 4000)
})
}
function toTaskByForEach() {
const arr = [1, 2, 3, 4, 5, 6]
arr.forEach(async (item) => {
await promiseTasek(item)
})
}

toTaskByForEach()

执行结果 注意执行输出的变化,他会直接打印出 1,2,3,4,5,6 本来想录制一个 gif 的,确实没找到一个好的工具录制浏览器的控制台


我们尝试换一种循环 for of 看一下效果对比一下

let promiseTasek = (num) => {
return new Promise((resolve, rejece) => {
setTimeout(() => {
console.log(num)
resolve(true)
}, 4000)
})
}

async function toTaskByForOf(){
const arr = [1,2,3,4,5,6]
for (let i of arr) {
await promiseTasek(i)
}
}
toTaskByForOf()

来看下执行结果 他会按顺序执行依次打印出 1,2,3,4,5,6

所以这是为啥呢

后来我们研究了一下 map

//forEach 处理
let promiseTasek = (num) => {
return new Promise((resolve, rejece) => {
setTimeout(() => {
console.log(num)
resolve(true)
}, 4000)
})
}
function toTaskByMap() {
const arr = [1, 2, 3, 4, 5, 6]
arr.map(async (item) => {
await promiseTasek(item)
})
}

toTaskByMap()

输出结果和 forEach 一样
后来我们发现 Array.prototype.forEach 不是一个 async 函数,即使 Array.prototype.forEach 的参数 callback 是 async 函数,也暂停不了 Array.prototype.forEach 函数,map 也是同理


Array.forEach 会并发启动所有方法但是丢弃结果,如果 forEach 需要 await 结果的时候可以用这个方法 await Promise.all(arr.map(async (item) => { /** ... */ }))
链接:https://juejin.cn/post/7154650750038048781


收起阅读 »

安卓关于Bitmap.isRecycled()空指针报错的解决方案

前言 起因是我在开发功能需要使用Bitmap的方法: BitmapFactory.decodeResource(my.main.getResources(),R.drawable.vector_my_need); 结果就倒大霉,运行时直接报错: java.la...
继续阅读 »

前言


起因是我在开发功能需要使用Bitmap的方法:


BitmapFactory.decodeResource(my.main.getResources(),R.drawable.vector_my_need);

结果就倒大霉,运行时直接报错:


java.lang.NullPointerException: Attempt to invoke virtual method 'boolean android.graphics.Bitmap.isRecycled()' on a null object reference

从日志分析,我们知道是出现了空指针,当时我先是想自己找原因,结果定位到view的源代码里,是在draw方法中,但没找到,我还没放弃,于是又定位了updateDisplayListIfDirty() 方法以及其他报错对应点,于是终于发现了是我bitmap的使用出了问题:


image.png
找到问题固然是好事,可是如何解决呢?这就要靠搜索了,接下来让我们看看解决方法。


正篇


正确的搜索方法


其实我在搜索上吃了许多亏,一开始在国内搜索上一直给我推C站的结果虽然有点相似但其实都相差甚远,最后我在StackOverflow上找到了答案,这也是曾经让别人困惑的一个问题:


image.png
可以看到有31K人浏览过此问题,所以该问题早有认可的答案:


image.png
意思说,我们用的vector矢量可绘制对象需要创建位图,而不是对其进行解码,且方法在下一个帖子中。


完美的解决方案


其实说到这,我已经明白,是我用SVG图资源放到安卓项目中转成Vector的xml文件,这种文件解码无法获得正确的bitmap,于是我恍然大悟的点开了下个帖子:


image.png
我为这个标准答案标记了中文解释,给出的是将我们的vector资源实例成Drawable对象,然后通过Bitmap的创建方法去创建成一个新的bitmap,代码如下:


Drawable d = getResources().getDrawable(R.drawable.your_drawable, your_app_theme);

这一步就是变成Drawable对象,接下来:


public static Bitmap drawableToBitmap (Drawable drawable) {

if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable)drawable).getBitmap();
}

Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);

return bitmap;
}

这段代码是将Drawable对象转换成Bitmap封装成了工具方法,便于直接应用于主代码。

而方法内部,也分成两层:

首先第一层,我们用instanceof测试它左边的对象是否是它右边的类的实例,如果是真命题则直接返回强制转成BitmapDrawable,并直接调用它的getBitmap()方法即可。


image.png
而如果第一层没有成功,则由第二层处理,我们先实例化Bitmap对象,利用Bitmap的createBitmap()方法输入drawable对象的固有宽高和BItmap通道配置获取bitmap


image.png


image.png
然后调用canvas绘制bitmap,最后先用drawable的setBounds()方法为Drawable对象指定一个边界矩形,这是为了调用 draw() 方法前可以确定绘制对象将绘制的位置,接着用draw()方法完成绘制,返回最终的bitmap即完成。


image.png


结语


这还是我第一次这么容易就获取到了明确的正确解决方案,所以特地记录下来,当然,如果你出现了这个空指针就不需要去看英文结果了,让我们更方便的解决这个问题吧。


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

Android动态更换应用图标

一、背景 近日,微博官方发布了一项新功能,即可以在App设置中动态更换微博的显示图标样式。根据微博官方的说法,除了最原始的图标外,微博还推出了另外10种不同的样式,既有3D微博、炫彩微博等保留了眼睛造型的新样式,也有奶酪甜馨、巧克力等以食物命名的“新口味”,还...
继续阅读 »

一、背景


近日,微博官方发布了一项新功能,即可以在App设置中动态更换微博的显示图标样式。根据微博官方的说法,除了最原始的图标外,微博还推出了另外10种不同的样式,既有3D微博、炫彩微博等保留了眼睛造型的新样式,也有奶酪甜馨、巧克力等以食物命名的“新口味”,还有梦幻紫、幻想星空等抽象派新造型,给了微博用户多种选择的自由。


不过需要注意的是,这一功能并不是面对所有人开放的,只有微博年费会员才能享受。此外,iOS 10.3及以上和Android 10及以上系统版本支持该功能,但是iPad与一加8Pro手机无法使用该功能。因部分手机存在系统差异,会导致该功能不可用,微博方面后续还会对该功能进行进一步优化。


image.png


二、技术实现


其实,说到底,上述功能用到的是动态更换桌面图标的技术。如果说多年以前,实现图标的切换还是一种时髦的技术,那么,我们可以直接使用PackageManager就可以实现动态更换桌面图标。


实现的细节是,在Manifest文件中使用标签准备多个Activity入口,没个activity都指向入口Activity,并且为每个拥有标签的activity设置单独的icon和应用名,最后调用SystemService 服务kill掉launcher,并执行launcher的重启操作。


首先,我们在AndroidManifest.xml文件中添加如下代码:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.xzh.demo">

<!-- 权限-->
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES"/>

<application
android:allowBackup="true"
android:icon="@mipmap/wb_default_logo"
android:label="@string/app_name"
android:roundIcon="@mipmap/wb_default_logo"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidDemo">

...//省略其他代码

<!-- 默认微博-->
<activity-alias
android:name="com.xzh.demo.default"
android:targetActivity=".MainActivity"
android:label="@string/app_name"
android:enabled="false"
android:icon="@mipmap/wb_default_logo"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

<!-- 3D微博-->
<activity-alias
android:name=".threedweibo"
android:targetActivity=".MainActivity"
android:label="@string/wb_3d"
android:enabled="false"
android:icon="@mipmap/wb_3dweibo"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity-alias>

... //省略其他

</application>
</manifest>

上面配置中涉及到的属性如下:



  • android:name:注册的组件名字,启动组件的名称。

  • android:enabled:是否启用这个组件,也就是是否显示这个入口。

  • android:icon:图标

  • android:label:名称

  • android:targetActivity:默认的activity没有这个属性,指定目标activity,与默认的activity中的name属性是一样的,需要有相应的java类文件。


接着,我们在MainActivity触发Logo图标更换逻辑,代码如下:


class MainActivity : AppCompatActivity() {
var list: List<LogoBean> = ArrayList()
var recyclerView: RecyclerView? = null
var adapter: LogoAdapter? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
initView()
initData()
initRecycle()
}
private fun initView() {
recyclerView = findViewById(R.id.recycle_view)
}
private fun initData() {
list = Arrays.asList(
LogoBean(R.mipmap.wb_default_logo, "默认图标", true),
LogoBean(R.mipmap.wb_3dweibo, "3D微博", false),
LogoBean(R.mipmap.wb_cheese_sweetheart, "奶酪甜心", false),
LogoBean(R.mipmap.wb_chocolate_sweetheart, "巧克力", false),
LogoBean(R.mipmap.wb_clear_colorful, "清透七彩", false),
LogoBean(R.mipmap.wb_colorful_sunset, "多彩日落", false),
LogoBean(R.mipmap.wb_colorful_weibo, "炫彩微博", false),
LogoBean(R.mipmap.wb_cool_pool, "清凉泳池", false),
LogoBean(R.mipmap.wb_fantasy_purple, "梦幻紫", false),
LogoBean(R.mipmap.wb_fantasy_starry_sky, "幻想星空", false),
LogoBean(R.mipmap.wb_hot_weibo, "热感微博", false),
)
}
private fun initRecycle() {
adapter =LogoAdapter(this,list);
val layoutManager = GridLayoutManager(this, 3)
recyclerView?.layoutManager = layoutManager
recyclerView?.adapter = adapter
adapter?.setOnItemClickListener(object : OnItemClickListener {
override fun onItemClick(view: View?, position: Int) {
if(position==1){
changeLogo("com.xzh.demo.threedweibo")
}else if (position==2){
changeLogo("com.xzh.demo.cheese")
}else if (position==3){
changeLogo("com.xzh.demo.chocolate")
}else {
changeLogo("com.xzh.demo.default")
}
}
})
}

fun changeLogo(name: String) {
val pm = packageManager
pm.setComponentEnabledSetting(
componentName,
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP
)
pm.setComponentEnabledSetting(
ComponentName(this, name),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP
)
reStartApp(pm)
}
fun reStartApp(pm: PackageManager) {
val am = getSystemService(ACTIVITY_SERVICE) as ActivityManager
val intent = Intent(Intent.ACTION_MAIN)
intent.addCategory(Intent.CATEGORY_HOME)
intent.addCategory(Intent.CATEGORY_DEFAULT)
val resolveInfos = pm.queryIntentActivities(intent, 0)
for (resolveInfo in resolveInfos) {
if (resolveInfo.activityInfo != null) {
am.killBackgroundProcesses(resolveInfo.activityInfo.packageName)
}
}
}
}

注意上面的changeLogo()方法中的字符串需要和AndroidManifest.xml文件中的<activity-alias>的name相对应。运行上面的代码,然后点击应用中的某个图标,就可以更换应用的桌面图标,如下图所示。


image.png


不过,测试的时候也遇到一些适配问题:



  • 小米9:版本升级时,新版本在AndroidManifest中删除A3,老版本切换图标到A3,为卸载直接覆盖安装新版本,手机桌面图标消失。

  • magic 4:版本升级时,新版本在AndroidManifest中删除A3,老版本切换图标到A3,为卸载直接覆盖安装新版本,手机桌面图标切换到默认图标,但点击之后未能打开APP。

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

Android gradle迁移至kts

背景 在kotlin语言已经渗透至各领域的环境下,比如服务端,android,跨平台Kmm,native for kotlin,几乎所有的领域都可以用kotlin去编写了,当然还有不成熟的地方,但是JB的目标是很一致的!我们最常用的gradle构建工具,也支持...
继续阅读 »

背景


在kotlin语言已经渗透至各领域的环境下,比如服务端,android,跨平台Kmm,native for kotlin,几乎所有的领域都可以用kotlin去编写了,当然还有不成熟的地方,但是JB的目标是很一致的!我们最常用的gradle构建工具,也支持kotlin好久了,但是由于编译速度或者转换成本的原因,真正实现kts转换的项目很少。在笔者的mac m1 中使用最新版的AS去编译build.gradle.kts,速度已经是和用groovy写的gradle脚本不相上下了,所以就准备写了这篇文章,希望做一个记录与分享。

groovykotlin
好处:构建速度较快,运用广泛,动态灵活好处:编译时完成所有,语法简洁,android项目中可用一套语言开发构建脚本与app编写
坏处:语法糖的作用下,很难理解gradle运行的全貌,作用单一,维护成本较高坏处:编译略慢于groovy,学习资料较少

虽然主流的gradle脚本编写依旧是groovy,但是android开发者官网也在推荐迁移到kotlin


编译前准备


这里推荐看看这篇文章,里面也涵盖了很多干货,


全局替换‘’为“”


在kotlin中,表示一个字符串用“”,不同于groovy的‘ ’,所以我们需要全局替换。可以通过快捷方式command/control+shift+R 全局替换,选中匹配正则表达式并设定file mask 为 *.gradle:


正则表达式
'(.*?[^\\])'
作用范围为
"$1"

image.png


全局替换方法调用


在groovy中,方法是可以隐藏(),举个例子


apply plugin: "com.android.application"

这里实际上是调用apply方法,然后命名参数是plugin,内容围为"com.android.application",然而在kotlin语法中,我们需要以()或者invoke的方式才能调用一个方法,所以我们要给所有的groovy函数调用添加()


正则表达式
(\w+) (([^=\{\s]+)(.*))
作用范围为
$1($2)

image.png
很遗憾的是,这个对于多行来说还是存在不足的,所以我们全局替换后还需要手动去修正部分内容即可,这里我们只要记得一个原则即可,想要调用一个kotlin函数,把参数包裹在()内即可,比如调用一个task函数,那么参数即为


task(sourcesJar(type: Jar) {
from(android.sourceSets.main.java.srcDirs)
classifier = "sources"
})

gradle kt化


接下来我们只需要把build.gradle 更改为文件名称为build.gradle.kts 即可,由于我们修改文件为了build.gradle.kts,所以当前就以kts脚本进行编译,所以很多的参数都是处于找不到状态的,即使sync也会报错,所以我们需要把报错的地方先注释掉,然后再进行sync操作,如果成功的话,AS就会帮我们进行一次编译,此时就可以有代码提示了。


开始前准备


以kotlin的方式编译,此时函数就处于可点击查看状态,区别于groovy,因为groovy是动态类型语言,所以很多做了很多语法糖,但是也给我们在debug阶段带来了很多困难,比如没有提示等等,因为groovy只需要保证在运行时找到函数即可,而kotlin却不一样,所以很多动态生成的函数就无法在编译时直接使用了,比如


image.png
对于这种动态函数,kotlin for gradle 其实也给我们内置了很多参数来对应着groovy的动态函数,下面我们来从以下方面去实践吧,tip:以下是gradle脚本编写常用


ext


我们在groovy脚本中,可以定义额外的变量在ext{}中,那么这个在kotlin中可以使用吗?嘿嘿,能用我就不会提到对吧!对的,不可以,因为ext也是一个动态函数,我们kotlin可没法用呀!那怎么办!别怕,kts中给我们定义了一个类似的变量,即extra,我们可以通过by extra去定义,然后就可以自由用我们的myNewProperty变量啦!


val myNewProperty by extra("initial value")

但是,如果我们在其他的gradle.kts脚本中用myNewProperty这个变量,那么也会找不到,因为myNewProperty这个的作用域其实只在当前文件中,确切来说是我们的build.gradle 最后会被编译生成一个Build_Init的类,这个类里面的东西能用的前提是,被先编译过!如果当前编译中的module引用了未被编译的module的变量,这当然不可行啦!当然,还是有对策的,我们可以在BuildScr这个module中定义自定义的函数,因为BuildScr这个module被定义在第一个先执行的module,所以我们后面的module就可以引用到这个“第一个module”的变量的方式去引用自定义的变量!


task



  • 新建task


groovy版本

task clean(type: Delete) {
delete rootProject.buildDir
}

比如clean就是一个我们自定义的task,转换为kotlin后其实也很简单,task是一个函数名,Delete是task的类型,clean是自定义名称


task("clean",{
delete(rootProject.buildDir)
})

当然,我们的task类型可能在编写的由于泛型推断,隐藏了具体的类型,这个时候我们可以通过


 ./gradlew help --task task名

去查看相应的类型



  • 已有task修改
    对于有些是已经在gradle编译时存在的函数任务,比如


groovy版本

wrapper{
gradleVersion = "7.1.1"
distributionType = Wrapper.DistributionType.BIN
}

这个我们kotlin版本的build.gradle能不能识别呢?其实是不可以的,因为编译器也不知道从哪里去找wrapper的定义,因为这个函数在groovy中隐藏了作用域,其实它存在于TaskContainerScope这个作用域中,所以对于所有的的task,其实都是执行在这里面的,我们可以通过tasks去找到


tasks {
named("wrapper") {
gradleVersion = "7.1.1"
distributionType = Wrapper.DistributionType.BIN

}
}

这种方式,去找到一个我们想要的task,并配置其内容



  • 生命周期函数
    我们可以通过函数调用的方式去配置相应的生命周期函数,比如doLast


tasks.create("greeting") {
doLast { println("Hello, World!") }
}

再比如dependOn


task("javadocJar", {
dependsOn(tasks.findByName("javadoc"))
})

动态函数


sourceSets就是一个典型的动态函数,为什么这么说,因为很多plugin都有自己的设置,比如Groovy的sourceSets,再比如Android的SourceSets,它其实是一个接口,正在实现其实是在plugin中。如果我们需要自定义配置一些东西,比如配置jniLibs的libs目录,直接迁移到kts就会出现main找不到的情况,这里是因为main不是一个内置的函数,但是存在相应的成员,这个时候我们可以通过by getting方式去获取,只要我们的变量在作用域内是存在的(编译阶段会添加),就可以获取到。如果我们想要生成其他成员,也可以通过by creating{}方式去生成一个没有的成员


sourceSets{
val main by getting{
jniLibs.srcDirs("src/main/libs")
jni.srcDirs()
}

}

也可以通过getByName方式去获取


sourceSets.getByName("main")

plugins


在比较旧的版本中,我们AS默认创建引入一个plugin的方式是


apply plugin: 'com.android.application'

其实这也是依赖了groovy的动态编译机制,这里针对的是,比如android{}作用域,如果我们转换成了build.gradle.kts,我们会惊讶的发现,android{}这个作用域居然爆红找不到了!这个时候我们需要改写成


plugins {
id("com.android.application")
}

就能够找到了,那么这背后的原理是什么呢?我们有必要去探究一下gradle的内部实现。


说了这么多的应用层写法,了解我的小伙伴肯定知道,原理解析肯定是放在最后啦!但是gradle是一个庞大的工程,单单靠着干唠是写不完的,所以我选出了最重要的一个例子,即plugins的解析,希望能够抛砖引玉,一起学习下去吧!


Plugins解析


我们可以通过在gradle文件中设置断点,然后debug运行gradle调试来学习gradle,最终在编译时,我们会走到DefaultScriptPluginFactory中进行相应的任务生成,我们来看看


DefaultScriptPluginFactory


            final ScriptTarget initialPassScriptTarget = initialPassTarget(target);

ScriptCompiler compiler = scriptCompilerFactory.createCompiler(scriptSource);

// 第一个阶段Pass 1, extract plugin requests and plugin repositories and execute buildscript {}, ignoring (i.e. not even compiling) anything else
CompileOperation initialOperation = compileOperationFactory.getPluginsBlockCompileOperation(initialPassScriptTarget);
Class scriptType = initialPassScriptTarget.getScriptClass();
ScriptRunner initialRunner = compiler.compile(scriptType, initialOperation, baseScope, Actions.doNothing());
initialRunner.run(target, services);

PluginRequests initialPluginRequests = getInitialPluginRequests(initialRunner);
PluginRequests mergedPluginRequests = autoAppliedPluginHandler.mergeWithAutoAppliedPlugins(initialPluginRequests, target);

PluginManagerInternal pluginManager = topLevelScript ? initialPassScriptTarget.getPluginManager() : null;
pluginRequestApplicator.applyPlugins(mergedPluginRequests, scriptHandler, pluginManager, targetScope);

// 第二个阶段Pass 2, compile everything except buildscript {}, pluginManagement{}, and plugin requests, then run
final ScriptTarget scriptTarget = secondPassTarget(target);
scriptType = scriptTarget.getScriptClass();

CompileOperation operation = compileOperationFactory.getScriptCompileOperation(scriptSource, scriptTarget);

final ScriptRunner runner = compiler.compile(scriptType, operation, targetScope, ClosureCreationInterceptingVerifier.INSTANCE);
if (scriptTarget.getSupportsMethodInheritance() && runner.getHasMethods()) {
scriptTarget.attachScript(runner.getScript());
}
if (!runner.getRunDoesSomething()) {
return;
}

Runnable buildScriptRunner = () -> runner.run(target, services);

boolean hasImperativeStatements = runner.getData().getHasImperativeStatements();
scriptTarget.addConfiguration(buildScriptRunner, !hasImperativeStatements);
}



可以看到,源码中特别注释了,编译时的两个阶段,我们可以看到,所有的script(指函数调用),都是分别经过了阶段1和阶段2之后才真正生效的。


image.png


那么为什么android作用域在apply plugin的方式不行,plugins方式却可以呢?其实就是两个运行阶段不一致的问题。groovy可以在运行时动态找到android 这个函数,即使两者都在阶段2运行,因为groovy语法本身的特性,即使android这个函数没有定义我们也可以引用,也是在运行时阶段报错。而kotlin不一样,kotlin需要在编译的时候需要找到我们要引用的函数,即android,所以同一个阶段即plugin都没有生效(需要执行完阶段才生效),我们当然也找不到android函数,那为什么plugins又可以呢?其实很容易想到,因为plugins是在第一阶段中执行并生效的,而android引用在第二个阶段,我们接着看源码


重点关注一下compileOperationFactory.getPluginsBlockCompileOperation方法,这个方法的实现类是DefaultCompileOperationFactory,在这里我们可以看到里面定义了两个阶段


public class DefaultCompileOperationFactory implements CompileOperationFactory {
private static final StringInterner INTERNER = new StringInterner();
private static final String CLASSPATH_COMPILE_STAGE = "CLASSPATH";
private static final String BODY_COMPILE_STAGE = "BODY";

private final BuildScriptDataSerializer buildScriptDataSerializer = new BuildScriptDataSerializer();
private final DocumentationRegistry documentationRegistry;

public DefaultCompileOperationFactory(DocumentationRegistry documentationRegistry) {
this.documentationRegistry = documentationRegistry;
}

public CompileOperation getPluginsBlockCompileOperation(ScriptTarget initialPassScriptTarget) {
InitialPassStatementTransformer initialPassStatementTransformer = new InitialPassStatementTransformer(initialPassScriptTarget, documentationRegistry);
SubsetScriptTransformer initialTransformer = new SubsetScriptTransformer(initialPassStatementTransformer);
String id = INTERNER.intern("cp_" + initialPassScriptTarget.getId());
return new NoDataCompileOperation(id, CLASSPATH_COMPILE_STAGE, initialTransformer);
}

public CompileOperation getScriptCompileOperation(ScriptSource scriptSource, ScriptTarget scriptTarget) {
BuildScriptTransformer buildScriptTransformer = new BuildScriptTransformer(scriptSource, scriptTarget);
String operationId = scriptTarget.getId();
return new FactoryBackedCompileOperation<>(operationId, BODY_COMPILE_STAGE, buildScriptTransformer, buildScriptTransformer, buildScriptDataSerializer);
}
}

getPluginsBlockCompileOperation中创建了一个InitialPassStatementTransformer类对象,我们关注transform方法的内容,即如果找到了plugins,我们就进行接下来的transform操作transformPluginsBlock,这就验证了,plugins的确在第一个阶段即classpath阶段运行



@Override
public Statement transform(SourceUnit sourceUnit, Statement statement) {
...

if (scriptBlock.getName().equals(PLUGINS)) {
return transformPluginsBlock(scriptBlock, sourceUnit, statement);
}
...


总结


文章列出来了几个关键的迁移了,相信大部分的问题都可以解决了,的确在迁移到kotlin之后,还是存在一定的迁移成本的,大部分就只能生啃官网介绍,希望看完都有收获吧!


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

使用 Flutter 轻松搞定短视频上滑翻页效果

前言 我们在短视频应用中经常会看到不停上滑浏览下一条视频的沉浸式交互效果,这种交互能够让用户不停地翻页,直到找到喜欢的视频内容,从而营造一种不断“搜寻目标”的感觉,让用户欲罢不能。这种交互形式在 Flutter 中可以轻松使用 PageView 组件实现。 ...
继续阅读 »

前言


我们在短视频应用中经常会看到不停上滑浏览下一条视频的沉浸式交互效果,这种交互能够让用户不停地翻页,直到找到喜欢的视频内容,从而营造一种不断“搜寻目标”的感觉,让用户欲罢不能。这种交互形式在 Flutter 中可以轻松使用 PageView 组件实现。
上滑交互.gif


PageView 组件介绍


PageView 组件专门设计用来实现翻页效果,类定义如下:


PageView({
Key? key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
PageController? controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,
List<Widget> children = const <Widget>[],
this.dragStartBehavior = DragStartBehavior.start,
this.allowImplicitScrolling = false,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
this.scrollBehavior,
this.padEnds = true,
})

其中常用的属性说明如下:



  • scrollDirection:滑动方向,可以支持纵向翻页或横向翻页,默认是横向翻页。

  • controller:翻页控制器,可以通过控制器来制定初始页,以及跳转到具体的页面。

  • onPageChanged:翻页后的回调函数,会告知翻页后的页码。

  • reverse:是否反向翻页,默认是 false。如果横向滑动翻页的话,如果开启反向翻页,则是从右到左翻页。如果是纵向翻页的话,就是从顶部到底部翻页。

  • children:在翻页中的组件列表,每一页都以自定义组件内容,因此这个组件也可以用于做引导页,或是类似滑动查看详情的效果。


使用示例


PageView 使用起来非常简单,我们先定义一个PageView 翻页的内容组件,简单地将接收的图片文件满屏显示。代码如下,实际应用的时候可以根据需要换成其他自定义组件。


 class ImagePageView extends StatelessWidget {
final String imageName;
const ImagePageView({Key? key, required this.imageName}) : super(key: key);

@override
Widget build(BuildContext context) {
return Scaffold(
body: Image.asset(
imageName,
fit: BoxFit.fitHeight,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
),
);
}
}

之后是定义一个 PageViewDemo 来应用 PageView 翻页应用示例,代码如下:


class PageViewDemo extends StatefulWidget {
const PageViewDemo({Key? key}) : super(key: key);

@override
State<PageViewDemo> createState() => _PageViewDemoState();
}

class _PageViewDemoState extends State<PageViewDemo> {
late PageController _pageController;
int _pageIndex = 1;

@override
void initState() {
_pageController = PageController(
initialPage: _pageIndex,
viewportFraction: 1.0,
);
super.initState();
}

@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: PageView(
scrollDirection: Axis.vertical,
onPageChanged: (index) {
_pageIndex = index;
},
controller: _pageController,
allowImplicitScrolling: false,
padEnds: true,
reverse: false,
children: const [
ImagePageView(imageName: 'images/earth.jpeg'),
ImagePageView(imageName: 'images/island-coder.png'),
ImagePageView(imageName: 'images/mb.jpeg'),
],
),
);
}
}

这个示例里,我们的 pageController 只是演示了设置初始页码。我们看到的 viewportFraction 可以理解为一页内容占据屏幕的比例,比如我们可以设置该数值为1/3,支持一个屏幕分段显示3个页面内容。


分段显示.gif


PageController 应用


PageController 可以控制滑动到指定位置,比如我们可以调用 animateToPage方法实现一个快速滑动到顶部的悬浮按钮。


floatingActionButton: FloatingActionButton(
onPressed: () {
_pageController.animateToPage(
0,
duration: const Duration(
milliseconds: 1000,
),
curve: Curves.easeOut,
);
},
backgroundColor: Colors.black.withAlpha(180),
child: const Icon(
Icons.arrow_upward,
color: Colors.white,
),
),

实现效果如下。


滑动到顶部.gif
PageController 还有如下控制翻页的方法:



  • jumpToPage:跳转到指定页面,但是没有动画。注意这里不会校验页码是否会超出范围。

  • nextPage:滑动到下一页,实际上调用的是 animateToPage 方法。

  • previousPage:滑动到上一页,实际上调用的是 animateToPage 方法。


总结


本篇介绍了 Flutter 的翻页组件 PageView 的使用,通过 PageView 可以轻松实现类似短视频的纵向上滑翻页的效果,也可以实现横向翻页效果(如阅读类软件)。在接下来的系列文章中,本专栏将会介绍更多 Flutter 实用的组件。


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

❤️Android 快别用Toast了,来试试Snackbar❤️

🔥 应用场景 Toast提示默认显示在界面底部,使用Toast.setGravity()将提示显示在中间,如下: Toast toast = Toast.makeText(this, str, Toast.LENGTH_SHORT); ...
继续阅读 »

🔥 应用场景


Toast提示默认显示在界面底部,使用Toast.setGravity()将提示显示在中间,如下:


        Toast toast = Toast.makeText(this, str, Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();

运行在在Android 12上无法显示,查看Logcat提示如下:


Toast: setGravity() shouldn't be called on text toasts, the values won't be used

意思就是:你不能使用toast调用setGravity,调用无效。哎呀,看给牛气的,咱看看源码找找原因


🔥 源码


💥 Toast.setGravity()


    /**
* 设置Toast出现在屏幕上的位置。
*
* 警告:从 Android R 开始,对于面向 API 级别 R 或更高级别的应用程序,此方法在文本 toast 上调用时无效。
*/
public void setGravity(int gravity, int xOffset, int yOffset) {
if (isSystemRenderedTextToast()) {
Log.e(TAG, "setGravity() shouldn't be called on text toasts, the values won't be used");
}
mTN.mGravity = gravity;
mTN.mX = xOffset;
mTN.mY = yOffset;
}

妥了,人家就告诉你了 版本>=Android R(30),调用该方法无效。无效就无效呗,还不给显示了,过分。


Logcat的提示居然是在这里提示的,来都来了,咱们看看isSystemRenderedTextToast()方法。


💥 Toast.isSystemRenderedTextToast()


    /**
*Text Toast 将由 SystemUI 呈现,而不是在应用程序内呈现,因此应用程序无法绕过后台自定义 Toast 限制。
*/
@ChangeId
@EnabledAfter(targetSdkVersion = Build.VERSION_CODES.Q)
private static final long CHANGE_TEXT_TOASTS_IN_THE_SYSTEM = 147798919L;

private boolean isSystemRenderedTextToast() {
return Compatibility.isChangeEnabled(CHANGE_TEXT_TOASTS_IN_THE_SYSTEM) && mNextView == null;
}

重点了。Text Toast 将由 SystemUI 呈现,而不是在应用程序内呈现。


清晰明了,可以这样玩,但是你级别不够,不给你玩。


事情整明白了,再想想解决解决方案。他说了Text Toast 将由 SystemUI 呈现,那我不用 Text 不就行了。


🔥 Toast 提供的方法


先看看Tast提供的方法:



有这几个方法。咱们实践一下。保险起见看看源码


💥 Toast.setView() 源码


    /**
* 设置显示的View
* @deprecated 自定义 Toast 视图已弃用。 应用程序可以使用 makeText 方法创建标准文本 toast,
* 或使用 Snackbar
*/
@Deprecated
public void setView(View view) {
mNextView = view;
}

这个更狠,直接弃用。




  • 要么老老实实的用默认的Toast。




  • 要么使用 Snackbar。




🔥 Snackbar


Snackbar 就是一个类似Toast的快速弹出消息提示的控件(我是刚知道,哈哈)。


与Toast相比:




  • 一次只能显示一个




  • 与用户交互



    • 在右侧设置按钮来添加事件,根据 Material Design 的设计原则,只显示 1 个按钮 (添加多个,以最后的为准)




  • 提供Snackbar显示和关闭的监听事件



    • BaseTransientBottomBar.addCallback(BaseCallback)




💥 代码实现


    showMessage(findViewById(android.R.id.content), str, Snackbar.LENGTH_INDEFINITE);

public static void showMessage(View view, String str, int length) {
Snackbar snackbar = Snackbar.make(view, str, length);

View snackbarView = snackbar.getView();
//设置布局居中
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(snackbarView.getLayoutParams().width, snackbarView.getLayoutParams().height);
params.gravity = Gravity.CENTER;
snackbarView.setLayoutParams(params);
//文字居中
TextView message = (TextView) snackbarView.findViewById(R.id.snackbar_text);
//View.setTextAlignment需要SDK>=17
message.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY);
message.setGravity(Gravity.CENTER);
message.setMaxLines(1);
snackbar.addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {
@Override
public void onDismissed(Snackbar transientBottomBar, int event) {
super.onDismissed(transientBottomBar, event);
//Snackbar关闭
}

@Override
public void onShown(Snackbar transientBottomBar) {
super.onShown(transientBottomBar);
//Snackbar显示
}
});
snackbar.setAction("取消", new View.OnClickListener() {
@Override
public void onClick(View v) {
//显示一个默认的Snackbar。
Snackbar.make(view, "我先走", BaseTransientBottomBar.LENGTH_LONG).show();
}
});
snackbar.show();
}

Snackbar.make的三个参数:



  • View:从View中找出当前窗口最外层视图,然后在其底部显示。

  • 第二个参数(text)

    • CharSequence

    • StringRes



  • duration(显示时长)

    • Snackbar.LENGTH_INDEFINITE 从 show()开始显示,直到它被关闭或显示另一个 Snackbar

    • Snackbar.LENGTH_SHORT 短时间

    • Snackbar.LENGTH_LONG 长时间

    • 自定义持续时间 以毫秒为单位




💥 效果


Android 12



Android 5.1



💥 工具类


如果觉得设置麻烦可以看看下面这边文章,然后整合一套适合自己的。


一行代码搞定Snackbar


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

Vue.js 3 开源组件推荐:代码差异查看器插件

web
一个Vue.js差异查看器插件,可以用来比较两个代码片断之间的差异。Github地址:github.com/hoiheart/vu…支持语言:cssxml: xml, html, xhtml, rss, atom, xjb, xsd, xsl, plist, ...
继续阅读 »

一个Vue.js差异查看器插件,可以用来比较两个代码片断之间的差异。


Github地址:github.com/hoiheart/vu…

支持语言:

  • css

  • xml: xml, html, xhtml, rss, atom, xjb, xsd, xsl, plist, svg

  • markdown: markdown, md, mkdown, mkd

  • javascript: javascript, js, jsx

  • json

  • plaintext: plaintext, txt, text

  • typescript: typescript, ts

如何使用:

  1. 导入并注册diff查看器。

import VueDiff from 'vue-diff'
import 'vue-diff/dist/index.css'
app.use(VueDiff);

2.向模板中添加组件。

<Diff />

3.可用的组件props。

mode: {
 type: String as PropType<Mode>,
 default: 'split' // or unified
},
theme: {
 type: String as PropType<Theme>,
 default: 'dark' // or light
},
language: {
 type: String,
 default: 'plaintext'
},
prev: {
 type: String,
 default: ''
},
current: {
 type: String,
 default: ''
},
inputDelay: {
 type: Number,
 default: 0
},
virtualScroll: {
 type: [Boolean, Object] as PropType<boolean|VirtualScroll>,
 default: false
}

4.使用 highlight.js 扩展插件。

// 注册一门新语言
import yaml from 'highlight.js/lib/languages/yaml'
VueDiff.hljs.registerLanguage('yaml', yaml)

作者:杭州程序员张张
来源:juejin.cn/post/7156839676677423112

收起阅读 »

随机裁员?Meta用算法随机裁掉60名“劳务派遣”员工

Facebook 母公司 Meta 最近使用算法“随机”解雇了 60 名来自埃森哲的劳务派遣人员。此前 Meta 与埃森哲签订了近 5 亿美元的合同,由隶属于后者的劳务派遣人员到 Meta 位于奥斯汀的办公室工作,主要开展内容审核和商业诚信等业务。Meta 通...
继续阅读 »

Facebook 母公司 Meta 最近使用算法“随机”解雇了 60 名来自埃森哲的劳务派遣人员。

此前 Meta 与埃森哲签订了近 5 亿美元的合同,由隶属于后者的劳务派遣人员到 Meta 位于奥斯汀的办公室工作,主要开展内容审核和商业诚信等业务。


Meta 通过视频电话会议告知被裁的 60 名员工,裁员将于 9 月 2 日正式生效,10 月 3 日结束工资发放。除了明确是“随机”选择之外,Meta 没有给出裁员的具体原因。

埃森哲没有立即向这些劳务派遣人员提供其他工作机会,但这些员工被告知可以在未来两周内重新申请新职位。

在今年 6 月 30 日公司举行的一次全体员工大会上,Meta 首席执行官马克・扎克伯格(Mark Zuckerberg)警告员工,最近的市场低迷“可能是我们近年来看到的最严峻的挑战之一”,因此需要通过“积极的业绩评估”来淘汰表现不佳的员工。从扎克伯格的话来看,这次裁员也许并不令人意外。

扎克伯格说:“实际上,公司里可能有很多人不该留在这里。”

扎克伯格补充道:“通过提高期望值,制定更有进取心的目标,并稍微加大压力,我想这可能会让你们中的一些人觉得这个地方不适合自己。我觉得这种自我选择没问题。”

在举行这次全员大会之时,Meta 已经采取了冻结招聘和其他削减成本措施,主要是因为公司股票今年以来已经下跌过半。

就在 Meta 裁减劳务派遣人员几天前,苹果解雇了 100 名负责招聘新员工的人事专员。苹果此前确实警告称,公司将控制支出并放缓招聘。

去年 8 月份,游戏行业支付处理公司 Xsolla 也使用算法裁掉了 150 名员工,所以让机器人解雇员工可能是未来的一种趋势。

来源:IT之家

收起阅读 »

1024程序员节,别人家的公司真香!羡慕ing~

今天是传说中属于程序猿的节日,各大互联网公司已经开整,小编已经在朋友圈里感受到了不同氛围的节日氛围,为大家整合了以下几类:一、掏心窝子型有哪个程序员能对漂亮小姐姐说不?!天天在办公室撸代码的码农而言,在黑白的代码间,小姐姐就是天使一样的存在~没看错,是真人女仆...
继续阅读 »

今天是传说中属于程序猿的节日,各大互联网公司已经开整,小编已经在朋友圈里感受到了不同氛围的节日氛围,为大家整合了以下几类:


一、掏心窝子型


有哪个程序员能对漂亮小姐姐说不?!天天在办公室撸代码的码农而言,在黑白的代码间,小姐姐就是天使一样的存在~


没看错,是真人女仆出现了。


我见过好事成双, 却没想过能站在女团中央~



还有献舞的小姐姐,一起蹦虾咔啦咔


同时还有男人的终极梦想,你相信光吗?


二、驱魔保命型


程序员的梦想是什么 No Code No Bug,此符居家旅行,建议常备。


虽说是防bug,可这猫仔何意?防BUG灵兽?


三、紧张兮兮型


不是所有的符都有用,比如这块氛围感糖饼的出现,让舒缓神经再次紧绷起来,瞬间觉得手里的符咒不香了。


如果有比这个还让人紧张的,那就是抠破了~


这个拔河游戏,看得D哥虎躯一震,往前一步是孤独,退后一步是幸福


四、扎得不行型


开开心心过节不行吗?这波操作,扎疼了码农的心。


比如:这个平平无奇的小黑盒竟读懂了我的内心,不过这个应该送给老板吧


泪崩,你以为我不想有个对象吗?


谁能拒绝一个奔三的秃头小宝贝?爱护头顶,从防脱开始,所以接下来是防脱专场:


单瓶装:防脱就防脱,旁边的青春永驻,是何意?


礼盒装:防脱产品都是成双成对,你呢?


套装:我宣布,今年这篇头顶被我承包了


嗯嗯,终于明白,霸王才是真真的程序员之友。


五、“特殊”服务型


肩颈不适是程序员们的通病,一顿贴心的按摩服务,也能让程序员朋友短暂放松,看这架势,专业~


不过,有些公司的定制化服务,简直服务到工位,反手就是一个赞~


其实,舒不舒服不重要,就是想体验差别化服务。


六、斗智斗勇型


不少公司开启游园会项目,打卡所有项目,就能兑换礼品,游戏项目包括但不限于:


穿越火线(这游戏搁夜市必火)


赌场风云(赌啥,KPI吗?)


数字coding(呵呵,怕这个就枉为程序猿)


也可窥见,很多人事绞尽脑汁,只为大家欢愉一刻,这个必须加鸡腿儿。


七、吃饱喝足型


不少公司准备了精致下午茶,慰藉代码兄弟们,昨天已经被朋友圈投喂饱了,独乐乐不如众乐乐,上图(菜):


精致可口的甜品,琳琅满目的零食,啧啧啧···



零食就算了,大闸蟹就过分了!



八、彰显身份型


一些公司虽然准备的是日常用品,但是····我们一定要透过现象看本质,体味公司的一番深意,比如:


公司送衬衣,称(衬)心如意(衣)。好兆头,这么正式的衣服,恨不能现在就穿上,感受节日氛围。


公司送双肩包,寓意:双减(双肩)别想了,但保(包)你有饭吃。


公司送键盘,沉吟片刻,我悟到了:见(键)一个,盘一个,淦!



礼物或大或小,心意或深或浅,1024,希望大家都能1G棒~


欢迎评论区留言,说出你的程序员礼物~


注:文章素材来源于网络。如侵,请联系删除


收起阅读 »

过几年你不看,就不用胡椒盐

法规及法规vbnmbnm,bn鼓风机发个人fghjghffg不会难看美女吧

法规及法规vbnmbnm,bn鼓风机发个人fghjghffg不会难看美女吧

fghjgf8ytuj复工后的非官方的个

和对方过后就VB你吧VNfghjghffg好看吗帮你们

和对方过后就VB你吧VNfghjghffg好看吗帮你们

非过户结果符合复工后很过分

法国的红酒地方各个很舒服fghjghffg搞好看皇冠

法国的红酒地方各个很舒服fghjghffg搞好看皇冠