注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

iOS小技能: 抽奖轮盘跑马灯边框的实现

iOS
携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情 前言 跑马灯的应用场景:iOS 抽奖轮盘边框动画 原理: 用NSTimer无限替换背景图片1和背景图片2,达到跑马灯的效果 - (void)touchesBega...
继续阅读 »

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情


前言


跑马灯的应用场景:

  1. iOS 抽奖轮盘边框动画


原理: 用NSTimer无限替换背景图片1和背景图片2,达到跑马灯的效果


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

[self rotate:_rotaryTable];

}

/**

iOS翻牌效果

*/
- (void)rotate:(id)sender {

[UIView beginAnimations:@"View Filp" context:nil];
[UIView setAnimationDelay:0.25];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
[UIView setAnimationTransition:UIViewAnimationTransitionFlipFromLeft forView:sender
cache:NO];
[UIView commitAnimations];

}


2. 在待办界面或者工作台界面,往往需要应用到跑马灯的地方


原理:利用QMUIMarqueeLabel 进行cell封装简易的跑马灯 label 控件


文章:kunnan.blog.csdn.net/article/det…





如用户登陆未绑定手机号,进行提示。



简易的跑马灯 label 控件,在文字超过 label 可视区域时会自动开启跑马灯效果展示文字,文字滚动时是首尾连接的效果 



I iOS 抽奖轮盘边框动画


1.1 原理


用NSTimer无限替换UIImageView的Image为互为错位的bg_horse_race_lamp_1或者bg_horse_race_lamp_2,达到跑马灯的效果



应用场景: iOS 抽奖轮盘边框动画



审核注意事项:



  1. 在抽奖页面添加一句文案“本活动与苹果公司无关”
    2, 在提交审核时修改分级至17+



1.2 实现代码

//
// ViewController.m
// horse_race_lamp
//
// Created by mac on 2021/4/7.
#import <Masonry/Masonry.h>


#import "ViewController.h"
NSString *const bg_horse_race_lamp_1=@"bg_horse_race_lamp_1";
NSString *const bg_horse_race_lamp_2=@"bg_horse_race_lamp_2";

@interface ViewController ()
/**

用NSTimer无限替换bg_horse_race_lamp_1和bg_horse_race_lamp_2,达到跑马灯的效果

应用场景: iOS 抽奖轮盘边框动画
*/
@property (nonatomic,strong) UIImageView *rotaryTable;
@property (nonatomic,strong) NSTimer *itemBordeTImer;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.


//通过以下两张图片bg_lamp_1 bg_lamp_2,用NSTimer无限替换,达到跑马灯的效果
_rotaryTable = [UIImageView new];
_rotaryTable.tag = 100;

[_rotaryTable setImage:[UIImage imageNamed:bg_horse_race_lamp_1]];

[self.view addSubview:_rotaryTable];

[_rotaryTable mas_makeConstraints:^(MASConstraintMaker *make) {

make.center.offset(0);

}];



_itemBordeTImer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(itemBordeTImerEvent) userInfo:nil repeats:YES];


[[NSRunLoop currentRunLoop] addTimer:_itemBordeTImer forMode:NSRunLoopCommonModes];







}
// 边框动画
- (void)itemBordeTImerEvent
{
if (_rotaryTable.tag == 100) {
_rotaryTable.tag = 101;
[_rotaryTable setImage:[UIImage imageNamed:bg_horse_race_lamp_2]];
}else if (_rotaryTable.tag == 101){
_rotaryTable.tag = 100;
[_rotaryTable setImage:[UIImage imageNamed:bg_horse_race_lamp_1]];
}
}




@end


1.3 下载Demo


从CSDN下载Demo:https://download.csdn.net/download/u011018979/16543761



private :https://github.com/zhangkn/horse_race_lamp


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

搞明白什么是零拷贝,就是这么简单

我们总会在各种地方看到零拷贝,那零拷贝到底是个什么东西。 接下来,让我们来理一理啊。 拷贝说的是计算机里的 I/O 操作,也就是数据的读写操作。计算机可是一个复杂的家伙,包括软件和硬件两大部分,软件主要指操作系统、驱动程序和应用程序。硬件那就多了,CPU、内存...
继续阅读 »

我们总会在各种地方看到零拷贝,那零拷贝到底是个什么东西。


接下来,让我们来理一理啊。


拷贝说的是计算机里的 I/O 操作,也就是数据的读写操作。计算机可是一个复杂的家伙,包括软件和硬件两大部分,软件主要指操作系统、驱动程序和应用程序。硬件那就多了,CPU、内存、硬盘等等一大堆东西。


这么复杂的设备要进行读写操作,其中繁琐和复杂程度可想而知。


传统I/O的读写过程


如果要了解零拷贝,那就必须要知道一般情况下,计算机是如何读写数据的,我把这种情况称为传统 I/O。


数据读写的发起者是计算机中的应用程序,比如我们常用的浏览器、办公软件、音视频软件等。


而数据的来源呢,一般是硬盘、外部存储设备或者是网络套接字(也就是网络上的数据通过网口+网卡的处理)。


过程本来是很复杂的,所以大学课程里要通过《操作系统》、《计算机组成原理》来专门讲计算机的软硬件。


简化版读操作流程


那么细的没办法讲来,所以,我们把这个读写过程简化一下,忽略大多数细节,只讲流程。



上图是应用程序进行一次读操作的过程。

  1. 应用程序先发起读操作,准备读取数据了;
  2. 内核将数据从硬盘或外部存储读取到内核缓冲区;
  3. 内核将数据从内核缓冲区拷贝到用户缓冲区;
  4. 应用程序读取用户缓冲区的数据进行处理加工;

详细的读写操作流程


下面是一个更详细的 I/O 读写过程。这个图可好用极了,我会借助这个图来厘清 I/O 操作的一些基础但非常重要的概念。



先看一下这个图,上面红粉色部分是读操作,下面蓝色部分是写操作。


如果一下子看着有点儿迷糊的话,没关系,看看下面几个概念就清楚了。


应用程序


就是安装在操作系统上的各种应用。


系统内核


系统内核是一些列计算机的核心资源的集合,不仅包括CPU、总线这些硬件设备,也包括进程管理、文件管理、内存管理、设备驱动、系统调用等一些列功能。


外部存储


外部存储就是指硬盘、U盘等外部存储介质。


内核态

  • 内核态是操作系统内核运行的模式,当操作系统内核执行特权指令时,处于内核态。
  • 在内核态下,操作系统内核拥有最高权限,可以访问计算机的所有硬件资源和敏感数据,执行特权指令,控制系统的整体运行。
  • 内核态提供了操作系统管理和控制计算机硬件的能力,它负责处理系统调用、中断、硬件异常等核心任务。

用户态


这里的用户可以理解为应用程序,这个用户是对于计算机的内核而言的,对于内核来说,系统上的各种应用程序会发出指令来调用内核的资源,这时候,应用程序就是内核的用户。

  • 用户态是应用程序运行的模式,当应用程序执行普通的指令时,处于用户态。
  • 在用户态下,应用程序只能访问自己的内存空间和受限的硬件资源,无法直接访问操作系统的敏感数据或控制计算机的硬件设备。
  • 用户态提供了一种安全的运行环境,确保应用程序之间相互隔离,防止恶意程序对系统造成影响。

模式切换


计算机为了安全性考虑,区分了内核态和用户态,应用程序不能直接调用内核资源,必须要切换到内核态之后,让内核来调用,内核调用完资源,再返回给应用程序,这个时候,系统在切换会用户态,应用程序在用户态下才能处理数据。


上述过程其实一次读和一次写都分别发生了两次模式切换。



内核缓冲区


内核缓冲区指内存中专门用来给内核直接使用的内存空间。可以把它理解为应用程序和外部存储进行数据交互的一个中间介质。


应用程序想要读外部数据,要从这里读。应用程序想要写入外部存储,要通过内核缓冲区。


用户缓冲区


用户缓冲区可以理解为应用程序可以直接读写的内存空间。因为应用程序没法直接到内核读写数据, 所以应用程序想要处理数据,必须先通过用户缓冲区。


磁盘缓冲区


磁盘缓冲区是计算机内存中用于暂存从磁盘读取的数据或将数据写入磁盘之前的临时存储区域。它是一种优化磁盘 I/O 操作的机制,通过利用内存的快速访问速度,减少对慢速磁盘的频繁访问,提高数据读取和写入的性能和效率。


PageCache

  • PageCache 是 Linux 内核对文件系统进行缓存的一种机制。它使用空闲内存来缓存从文件系统读取的数据块,加速文件的读取和写入操作。
  • 当应用程序或进程读取文件时,数据会首先从文件系统读取到 PageCache 中。如果之后再次读取相同的数据,就可以直接从 PageCache 中获取,避免了再次访问文件系统。
  • 同样,当应用程序或进程将数据写入文件时,数据会先暂存到 PageCache 中,然后由 Linux 内核异步地将数据写入磁盘,从而提高写入操作的效率。

再说数据读写操作流程


上面弄明白了这几个概念后,再回过头看一下那个流程图,是不是就清楚多了。


读操作
  1. 首先应用程序向内核发起读请求,这时候进行一次模式切换了,从用户态切换到内核态;
  2. 内核向外部存储或网络套接字发起读操作;
  3. 将数据写入磁盘缓冲区;
  4. 系统内核将数据从磁盘缓冲区拷贝到内核缓冲区,顺便再将一份(或者一部分)拷贝到 PageCache;
  5. 内核将数据拷贝到用户缓冲区,供应用程序处理。此时又进行一次模态切换,从内核态切换回用户态;

写操作
  1. 应用程序向内核发起写请求,这时候进行一次模式切换了,从用户态切换到内核态;
  2. 内核将要写入的数据从用户缓冲区拷贝到 PageCache,同时将数据拷贝到内核缓冲区;
  3. 然后内核将数据写入到磁盘缓冲区,从而写入磁盘,或者直接写入网络套接字。

瓶颈在哪里


但是传统I/O有它的瓶颈,这才是零拷贝技术出现的缘由。瓶颈是啥呢,当然是性能问题,太慢了。尤其是在高并发场景下,I/O性能经常会卡脖子。


那是什么地方耗时了呢?


数据拷贝


在传统 I/O 中,数据的传输通常涉及多次数据拷贝。数据需要从应用程序的用户缓冲区复制到内核缓冲区,然后再从内核缓冲区复制到设备或网络缓冲区。这些数据拷贝过程导致了多次内存访问和数据复制,消耗了大量的 CPU 时间和内存带宽。


用户态和内核态的切换


由于数据要经过内核缓冲区,导致数据在用户态和内核态之间来回切换,切换过程中会有上下文的切换,如此一来,大大增加了处理数据的复杂性和时间开销。


每一次操作耗费的时间虽然很小,但是当并发量高了以后,积少成多,也是不小的开销。所以要提高性能、减少开销就要从以上两个问题下手了。


这时候,零拷贝技术就出来解决问题了。


什么是零拷贝


问题出来数据拷贝和模态切换上。


但既然是 I/O 操作,不可能没有数据拷贝的,只能减少拷贝的次数,还有就是尽量将数据存储在离应用程序(用户缓冲区)更近的地方。


而区分用户态和内核态有其他更重要的原因,不可能单纯为了 I/O 效率就改变这种设计吧。那也只能尽量减少切换的次数。


零拷贝的理想状态就是操作数据不用拷贝,但是显示情况下并不一定真的就是一次复制操作都没有,而是尽量减少拷贝操作的次数。


要实现零拷贝,应该从下面这三个方面入手:

  1. 尽量减少数据在各个存储区域的复制操作,例如从磁盘缓冲区到内核缓冲区等;
  2. 尽量减少用户态和内核态的切换次数及上下文切换;
  3. 使用一些优化手段,例如对需要操作的数据先缓存起来,内核中的 PageCache 就是这个作用;

实现零拷贝方案


直接内存访问(DMA)


DMA 是一种硬件特性,允许外设(如网络适配器、磁盘控制器等)直接访问系统内存,而无需通过 CPU 的介入。在数据传输时,DMA 可以直接将数据从内存传输到外设,或者从外设传输数据到内存,避免了数据在用户态和内核态之间的多次拷贝。




如上图所示,内核将数据读取的大部分数据读取操作都交个了 DMA 控制器,而空出来的资源就可以去处理其他的任务了。


sendfile


一些操作系统(例如 Linux)提供了特殊的系统调用,如 sendfile,在网络传输文件时实现零拷贝。通过 sendfile,应用程序可以直接将文件数据从文件系统传输到网络套接字或者目标文件,而无需经过用户缓冲区和内核缓冲区。


如果不用sendfile,如果将A文件写入B文件。



  1. 需要先将A文件的数据拷贝到内核缓冲区,再从内核缓冲区拷贝到用户缓冲区;

  2. 然后内核再将用户缓冲区的数据拷贝到内核缓冲区,之后才能写入到B文件;


而用了sendfile,用户缓冲区和内核缓冲区的拷贝都不用了,节省了一大部分的开销。


共享内存


使用共享内存技术,应用程序和内核可以共享同一块内存区域,避免在用户态和内核态之间进行数据拷贝。应用程序可以直接将数据写入共享内存,然后内核可以直接从共享内存中读取数据进行传输,或者反之。



通过共享一块儿内存区域,实现数据的共享。就像程序中的引用对象一样,实际上就是一个指针、一个地址。


内存映射文件(Memory-mapped Files)


内存映射文件直接将磁盘文件映射到应用程序的地址空间,使得应用程序可以直接在内存中读取和写入文件数据,这样一来,对映射内容的修改就是直接的反应到实际的文件中。


当文件数据需要传输时,内核可以直接从内存映射区域读取数据进行传输,避免了数据在用户态和内核态之间的额外拷贝。


虽然看上去感觉和共享内存没什么差别,但是两者的实现方式完全不同,一个是共享地址,一个是映射文件内容。


Java 实现零拷贝的方式


Java 标准的 IO 库是没有零拷贝方式的实现的,标准IO就相当于上面所说的传统模式。只是在 Java 推出的 NIO 中,才包含了一套新的 I/O 类,如 ByteBufferChannel,它们可以在一定程度上实现零拷贝。


ByteBuffer:可以直接操作字节数据,避免了数据在用户态和内核态之间的复制。


Channel:支持直接将数据从文件通道或网络通道传输到另一个通道,实现文件和网络的零拷贝传输。


借助这两种对象,结合 NIO 中的API,我们就能在 Java 中实现零拷贝了。


首先我们先用传统 IO 写一个方法,用来和后面的 NIO 作对比,这个程序的目的很简单,就是将一个100M左右的PDF文件从一个目录拷贝到另一个目录。

public static void ioCopy() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileInputStream fis = new FileInputStream(sourceFile);
FileOutputStream fos = new FileOutputStream(targetFile)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
System.out.println("传输 " + formatFileSize(sourceFile.length()) + " 字节到目标文件");
} catch (IOException e) {
e.printStackTrace();
}
}

下面是这个拷贝程序的执行结果,109.92M,耗时1.29秒。



传输 109.92 M 字节到目标文件
耗时: 1.290 秒



FileChannel.transferTo() 和 transferFrom()


FileChannel 是一个用于文件读写、映射和操作的通道,同时它在并发环境下是线程安全的,基于 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 getChannel() 方法可以创建并打开一个文件通道。FileChannel 定义了 transferFrom() 和 transferTo() 两个抽象方法,它通过在通道和通道之间建立连接实现数据传输的。


这两个方法首选用 sendfile 方式,只要当前操作系统支持,就用 sendfile,例如Linux或MacOS。如果系统不支持,例如windows,则采用内存映射文件的方式实现。


transferTo()


下面是一个 transferTo 的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

public static void nioTransferTo() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);
try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long transferredBytes = sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);

System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

只耗时0.536秒,快了一倍。



传输 109.92 M 字节到目标文件
耗时: 0.536 秒



transferFrom()


下面是一个 transferFrom 的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

public static void nioTransferFrom() {
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);

try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long transferredBytes = targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
System.out.println("传输 " + formatFileSize(transferredBytes) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

执行时间:



传输 109.92 M 字节到目标文件
耗时: 0.603 秒



Memory-Mapped Files


Java 的 NIO 也支持内存映射文件(Memory-mapped Files),通过 FileChannel.map() 实现。


下面是一个 FileChannel.map()的例子,仍然是拷贝那个100M左右的 PDF,我的系统是 MacOS。

    public static void nioMap(){
try {
File sourceFile = new File(SOURCE_FILE_PATH);
File targetFile = new File(TARGET_FILE_PATH);

try (FileChannel sourceChannel = new RandomAccessFile(sourceFile, "r").getChannel();
FileChannel targetChannel = new RandomAccessFile(targetFile, "rw").getChannel()) {
long fileSize = sourceChannel.size();
MappedByteBuffer buffer = sourceChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileSize);
targetChannel.write(buffer);
System.out.println("传输 " + formatFileSize(fileSize) + " 字节到目标文件");
}
} catch (IOException e) {
e.printStackTrace();
}
}

执行时间:



传输 109.92 M 字节到目标文件
耗时: 0.663 秒



推荐阅读


我的第一个 Chrome 插件上线了,欢迎试用!


前端同事最讨厌的后端行为,看看你中了没有


RPC框架的核心到底是什么


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

27岁程序媛未来的出路到底在哪里?

不太聪明的脑子的思考原因 最近回老家面试了一个工作,发现老家的思想的底层逻辑是:到了这个年纪女性就应该相夫教子,不愿意给女性与男性同等的工资标准或对女性进行培养, 看到他们这种嘴脸真的不想回去,但是目前互联网环境也不好,对未来开始变得迷茫不安, 不过作为i型...
继续阅读 »

不太聪明的脑子的思考原因


最近回老家面试了一个工作,发现老家的思想的底层逻辑是:到了这个年纪女性就应该相夫教子,不愿意给女性与男性同等的工资标准或对女性进行培养,

看到他们这种嘴脸真的不想回去,但是目前互联网环境也不好,对未来开始变得迷茫不安,

不过作为i型人格真的很喜欢这种沉浸式工作,暂时没有换行业的打算,所以还是先从目前做程序出发,去提升自己的能力,争取能再多干个几年,然后回东北老家花几万块买个小房子,开始我的摆烂养老人生
(人生终极目标)


在此总结一下今年上半年的成果和下半年的目标吧~


上半年成果


1.刷力扣拿到排名


摆烂人生是在去年感知到危机的时候结束的,于是开始疯狂刷LeetCode,学习算法,最终的结果是对待代码问题脑子变得灵光了但生活中越发糊涂了,但是目前困难的题还是基本摸不到头绪的状态,好多数学公式也不知道,位运算符也不咋会用,就目前感觉自己还是很差,提升的空间还是非常非常高的

(今年四月拿到排名时截的图)



2.开始准备软考


年初的时候开始考虑考一个专业资格证,于是开始做一些功课,上半年从bilibili上看了一些公开的课先做了初步了解,六月份买了一套课开始进行系统的学习,备战11月的考试



3.涨薪


很幸运自己能在目前经济环境下行的情况下没有失业,并且领导对我还算认可,给我们在竞争中留下来的人涨了工资,但说是涨薪,其实最终结果我们未必拿到的多了,因为目前公司效益不景气,如果公司效益持续低迷,年底的14薪必定要打水漂,但是还能稳定的存活下来也算是比较满意了,真心希望公司越来越好,因为我们的老板人真的非常不错(虽然我不接受pua但是发自内心感谢公司)


4.买了自行车开始骑行健身


其实早就想买个自行车,可以骑行上班,周末也可以当运动,不过身边的好多人都不赞同,因为像夏天太热、冬天太冷、刮风下雨都骑不出去,但是最终我还是买了,嘎嘎开心,不过确实影响因素很多最终也没骑过几次哈哈(主要是本人太懒总是找借口不骑车出门)



下半年目标


1.软考通过!


最近还是按照规划的持续学习,每个月给自己定一个总体的目标,然后分到每一天里去,现在距离考试还有两个多月,还是要加油的!


2.争取换一个更高的平台


感觉目前的公司体量还是太小了,做了很多微信小程序,工作对自己的提升已经到达了极限,但是就目前的情况来说,还是对年底的14薪抱有一丝丝幻想,所以这个目标可能在今年年底或者明年去达成


3.持续精进算法


还是在有条不紊的刷LeetCode,给自己的最低要求是每周至少一道中级,保持一个持续学习的状态


4.做一个开源项目


这个规划应该会在11月份开始实施,或者如果突然来了灵感可以立马启动,也是给以后面试提供一个优势条件吧


最后希望还在圈子中的同行们也能越来越好,不管这些努力会不会给自己带来实质性的收益,本质上都是在提升自己,目的其实很简单,就是不被这日新月异的时代所淘汰,
就好像一句金句里描述的那样:我们所做的一切,不是为了改变世界,而是不让世界改变我们!



最后,大家有什么能提升自己的点子也可以给我留言,让我们一起努力吧,加油!




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

[译] 2021 年的 SwiftUI: 优势、劣势和缺陷

iOS
2021 年的 SwiftUI: 优势、劣势和缺陷 在生产环境使用 SwiftUI?仍然不可行。 过去的 8 个月,我一直在用 SwiftUI 开发复杂的应用程序,其中就包括最近在 App Store 上架的 Fave。期间遇到了很多限制,也找到了大多数...
继续阅读 »

2021 年的 SwiftUI: 优势、劣势和缺陷



在生产环境使用 SwiftUI?仍然不可行。



由 Maxwell Nelson 在 Unsplash 发布


过去的 8 个月,我一直在用 SwiftUI 开发复杂的应用程序,其中就包括最近在 App Store 上架的 Fave。期间遇到了很多限制,也找到了大多数问题的解决方法。


简而言之,SwiftUI 是一个很棒的框架,并且极具前景。我认为它就是未来。但是要达到和 UIKit 同等的可靠性和健壮性,可能还需要 3-5 年。但是这并不意味着现在不应该使用 SwiftUI。我的目的是帮助你理解它的利弊,这样你可以就 SwiftUI 是否适合下一个项目做出更明智的决定。


SwfitUI 的优势


1. 编写 SwiftUI 是一件乐事,而且你可以快速构建用户界面


使用 addSubviewsizeForItemAtIndexPath,小心翼翼地计算控件的大小与位置,应对烦人的约束问题,手动构建视图层次结构,这样的日子已经一去不复返了。SwiftUI 的声明式和响应式设计模式使得创建响应式布局和 React 一样简单,同时它还背靠 Apple 强大的 UIKit。用它构建、启动并运行视图快到不可思议。


2. SwiftUI 简化了跨平台开发


我最兴奋的事情就是只需要编写一次 SwiftUI 代码,就可以在 iOS (iPhone 和 iPad),WatchOS 和 macOS 上使用。同时开发和维护 Android 和 Windows 各自的代码库已经很困难了,所以在减少不同代码库的数量这方面,每一个小的改变都很有帮助。当然还是有一些缺点,我将会在 “劣势” 章节分享。


3. 你可以免费获取漂亮的转场效果,动画和组件


你可以把 SwiftUI 当作一个 UI 工具箱,这个工具箱提供了开发专业应用程序所需的所有构建块。另外,如果你熟悉 CSS 的 Transition 属性,你会发现 SwiftUI 也有一套类似的方法,可以轻松创建优雅的交互过程。声明式语法的魅力在于你只需要描述你需要什么样的效果,效果就实现了,这看上去像魔法一样,但是也有不好的一面,我之后将会介绍。


4. UI 是完全由状态驱动并且是响应式的


如果你熟悉 React 的话,SwiftUI 在这一点上完全类似。当你监听整个 UI 的”反应“,动画和所有一切的时候,你只需要修改 @State@Binding 以及 @Published 属性,而不是使用多达几十层的嵌套回调函数。使用 SwiftUI,你可以体会到 CombineObservableObject 以及 @StateObject 的强大。这方面是 SwiftUI 和 UIKit 最酷的区别之一,强大到不可思议。


5. 社区正在拥抱 SwiftUI


几乎每个人都在因为 SwiftUI 而兴奋。SwiftUI 有许多学习资源可供获取,从 WWDC 到书,再到博客 —— 资料就在那里,你只需要去搜索它。如果不想搜索的话,我这里也汇总了一份最佳社区资源列表。


拥有一个活跃且支持度高的社区可以加速学习,开发,并且大量的新库会使得 SwiftUI 用途更加广泛。


劣势


1. 不是所有组件都可以从 SwiftUI 中获取到


在 SwiftUI 中有许多缺失、不完整或者过于简单的组件,我将在下面详细介绍其中一部分。


使用 UIViewRepresentableUIViewControllerRepresentableUIHostingController 协议可以解决这一问题。前两个让你可以在 SwiftUI 视图层中嵌入 UIKit 视图和控制器。最后一个可以让你在 UIKit 中嵌入 SwiftUI 视图。在 Mac 开发中也存在类似的三种协议 (NSViewRepresentable 等)。


这些协议是弥补 SwiftUI 功能缺失的权宜之计,但并不是一直天衣无缝。而且,尽管 SwiftUI 的跨平台承诺很好,但是如果某些功能不可用的话,你仍然需要为 iOS 和 Mac 分别实现协议代码。


2. NavigationView 还没有真正实现


如果你想在隐藏导航栏的同时仍然支持滑动手势,这是不可能的。我最终参考一些找到的代码创建了一个 UINavigationController wrapper。尽管可以起作用,但这不是一个长远的解决方案。


如果你想要在 iPad 上拥有一个 SplitView,但目前你还不能以纵向模式同时展示主视图和详情视图。他们选择用一个简陋的按钮展示默认关闭的抽屉。显然,你可以通过添加 padding 来解决这个问题,它可以突出显示你在使用 SwiftUI 时必须做的事情。


当你想使用编程式导航的时候,NavigationLink 是一种流行的解决方案。这里有一个有趣的讨论


3. 文本输入十分受限


TextFieldTextEditor 现在都太简单了,最终你还是会退回到 UIKit。所以我不得不为 UITextFieldUITextView 构建自己的 UIViewRepresentable 协议(以实现文本行数的自动增加)。


4. 编译器困境


当视图开始变得笨重,并且你已经竭尽所能去提取分解,编译器仍然会冲着你咆哮:



The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions.



这个问题已经多次拖慢进度。由于这个问题,我已经很擅长注释代码定位到引起问题的那一行,但是 2021 年了还在用这种方法调试代码感觉非常落后。


5. matchedGeometryEffect


我第一次发现这个的时候,感觉很神奇。它目的是通过匹配一隐一现的几何形状,帮助你更加流畅地转换两个不同标识的视图。我觉得这有助于从视图 A 优雅地转场到 B 视图。


我一直想让它起作用。但最终还是放弃了,因为它并不完美。此外,在包含大量列表项的 ListScrollView 中使用它会导致项目瘫痪。我只推荐在同一视图中使用这个做简单的转换过渡。当你在多个不同的视图中共享一个命名空间的时候(包括转场期间的视图剪裁在内),事情就会开始变得奇怪。


6. 对手势的支持有限


SwiftUI 提供了一系列新的手势(即 DragGestureLongPressGesture)。这些手势可以通过 gesture 修饰符(如 tapGesturelongPressGesture)添加到视图中。它们都能正常工作,除非你想要做更复杂的交互。


比如,DragGestureScrollView 交互就不是很好。即使有了 simultaneousGesture 修饰符,在 ScrollView 中放一个 DragGesture 还是会阻止滚动。在其他情况下,拖动手势可以在没有任何通知的情况下被取消,使得手势处于不完整状态。


为了解决这个问题,我构建了自己的 GestureView,它可以在 SwiftUI 中使用 UIKit 手势。我会在下一篇关于最佳 SwiftUI 库和解决方案的文章中分享这部分内容。


7. 分享扩展中的 SwiftUI


我可能是错的,但是分享扩展还是使用 UIKit 吧。我通过 UIHostingController 用 SwiftUI 构建了一个分享扩展,当分享扩展加载完毕后,有一个非常明显的延迟,用户体验较差。你可以尝试通过在视图中添加动画去掩盖它,但是仍然有 500 毫秒左右的延迟。


值得一提的点

  • 无法访问状态栏 (不能修改颜色或拦截点击)
  • 由于缺少 App,我们仍然需要 @UIApplicationDelegateAdaptor
  • 不能向后兼容
  • UIVisualEffectsView 会导致滚动延迟(来源于推特:@AlanPegoli

缺陷


1. ScrollView


这是迄今为止最大的缺点之一。任何一个构建过定制化 iOS 应用的人都知道我们有多依赖 ScrollView 去支持交互。

  • 主要的障碍:视图中的 LazyVStack 导致卡顿、抖动和一些意外的行为LazyVStack 对于需要滚动的混合内容(如新闻提要)的长列表至关重要。仅凭这一点,SwiftUI 就还没准备好投入生产环境: Apple 已经证实,这是 SwiftUI 自身的漏洞。尚未清楚他们什么时候会修复,但是一旦修复了,这将是一个巨大的胜利。
  • 滚动状态:原生不支持解析滚动的状态(滚动视图是否正在被拖拽?滚动?偏移多少?)。尽管有一些解决方案,但是还是很繁琐且不稳定。
  • 分页:原生不支持分页滚动视图。所以打消实现类似于可滑动的媒体库的念头吧(但是如果你想要关闭一些东西的时候,可以使用 SwiftUIPager)。在技术上你可以使用 TabView 加 PageTabViewStyle,但是我认为它更适合少部分的元素,而不是大的数据集。
  • 性能:使用 List 是性能最好的,并且避免了 LazyVStack 的卡顿问题,但由于工作方式的转换,它仍然不适合显示可变大小的内容。例如,在构建聊天视图时,其过渡很奇怪,会裁剪子视图,并且无法控制插入的动画样式。

结论


毫无疑问我觉得应该学习 SwiftUI ,自己去理解它,并享受乐趣。但是先别急着全盘采用。


SwiftUI 已经为简单的应用程序做好了准备,但是在写这篇文章的时候(iOS 15,beta 4 版本),我不认为它已经适合复杂应用程序的生产环境,主要是由于 ScrollView 的问题和对 UIViewRepresentable 的严重依赖。我很遗憾,尤其是像即时通信产品,新闻摘要,以及严重依赖复杂视图或者想要创建手势驱动的定制体验产品,目前还不适合使用 SwiftUI。


如果你想要精细的控制和无限的可能性,我建议在可预见的未来坚持使用 UIKit。你可以在一些视图(如设置页)里通过使用 UIHostingController 包装 SwiftUI 视图以获得 SwiftUI 的好处。


未来会发生什么?


当开始着手我们项目的下一次大迭代的时候。我知道这个新项目的交互范围不在 SwiftUI 目前支持的范围之内。即使当我知道 SwiftUI 在某些关键方面存在不足的时候,我的心都碎了,但是我还是不打算退回到 UIKit,因为我知道当 SwiftUI 运行起来时,构建它是一件多么快乐的事情。它的速度如此之快。


SwiftUI 会兼容 UIKit 么?如果这样的话,我们可能需要等待 SwiftUI 使用 3-5 年的时间来移植所有必要的 UIKit API。如果 SwiftUI 不准备兼容 UIkit,那你也能通过 SwiftUI 封装的方式使用 UIKit。


我好奇的是 Apple 会在 SwiftUI 上投入多少。他们是否有让所有的开发者采用 SwiftUI 的长期计划,或者说 SwiftUI 只是另一个界面构建器而已?我希望不是,也希望他们能全心投入 SwiftUI,因为它的前景是非常诱人的。


更多看法


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

iOS crash 报告分析系列 - 看懂 crash 报告的内容

iOS
在日常工作中,开发者最怕的应该就是线上的崩溃了。线上的崩溃不像我们开发中遇到的崩溃,可以在 Xcode 的 log 中直观的看到崩溃信息。 不过,线上的崩溃也并不是线索全无,让我们卖虾的不拿秤 -- 抓瞎。 每当 App 发生崩溃时,系统会自动生成一个后缀 i...
继续阅读 »

在日常工作中,开发者最怕的应该就是线上的崩溃了。线上的崩溃不像我们开发中遇到的崩溃,可以在 Xcode 的 log 中直观的看到崩溃信息。


不过,线上的崩溃也并不是线索全无,让我们卖虾的不拿秤 -- 抓瞎。


每当 App 发生崩溃时,系统会自动生成一个后缀 ips 的崩溃报告。我们可以通过崩溃报告来进行问题定位。但崩溃报告的内容繁多,新手看很容易一脸懵。所以本文先讲解一下报告中各字段的含义,后面再说报告符号化。


废话不多说,让我们开始吧!


前期准备


首先,报告解读我们需要先生成一个 crash 报告。


1、新建一个项目,在 ViewController 中写下面的代码:

NSString *value;
NSDictionary *dict = @{@"key": value}; // 字典的 value 不可为 nil,所以会崩溃

2、在真机上运行项目,然后去设置 - 隐私与安全性 - 分析与改进 - 分析数据,拿去生成的 crash 报告(报告的名字与项目名字一致,比如我的项目名为:CrashDemo,崩溃报告的名则为:CrashDemo-2023-05-30-093930.ips)。


注意:连着 Xcode 运行时不会产生崩溃报告,需要真机拔掉数据线再次运行 app 才会生成崩溃报告。


拿到报告,接下来就是解读了。


报告内容解读


官网的示例图:




Header


首先来看 Header:

Incident Identifier: 9928A955-FE71-464F-A2AF-A4593A42A26B
CrashReporter Key: 7f163d1c67c5ed3a6be5c879936a44f10b50f0a0
Hardware Model: iPhone14,5
Process: CrashDemo [45100]
Path: /private/var/containers/Bundle/Application/6C9D4CF7-4C16-4B50-A4A5-389BED62C699/CrashDemo.app/CrashDemo
Identifier: cn.com.fengzhihao.CrashDemo
Version: 1.0 (1)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: cn.com.fengzhihao.CrashDemo [3547]

Date/Time: 2023-05-30 09:39:29.6418 +0800
Launch Time: 2023-05-30 09:39:28.5579 +0800
OS Version: iPhone OS 16.3.1 (20D67)
Release Type: User
Baseband Version: 2.40.01
Report Version: 104

Header 主要描述了目标设备的软硬件环境。比如上图可以看出:是 iphone 14 的设备,系统版本是16.3,发生崩溃的事件是 2023-05-30 09:39:29 等等。


需要注意的是 Incident Identifier 相当于当前报告的 id,报告和 Incident Identifier 是一一对应的关系,绝对不会存在两份不同的报告 Incident Identifier 相同的情况。


Exception information

Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000

这一部分主要是告诉我们 app 是因为什么错误而导致的崩溃,但不会包含完整的信息。


可以看到当前的 Type 为:EXC_CRASH (SIGABRT),这代表当前进程因收到了 SIGABRT 信号而导致崩溃,这是一个很常见的类型,字典 value 为nil或者属于越界等都会是此类型。更多的 Exception Type 解释请参见此处


Diagnostic messages

Application Specific Information:
abort() called

操作系统有时包括额外的诊断信息。此信息使用多种格式,具体取决于崩溃的原因,并且不会出现在每个崩溃报告中。


本次的崩溃原因是因为调用了 abort() 函数。


接下来,就是报告的重点了。


Backtraces


这部分记录了当前进程的线程的函数调用栈,我们可以通过调用栈来定位出问题的代码。


崩溃进程的每一条线程都会被捕获成回溯。回溯会展示当前线程被中断时的线程的函数调用栈。如果崩溃是由于语言异常造成的,会额外有一个Last Exception Backtrace,位于第一个线程之前。关于 Last Exception Backtrace 的详细介绍请看这里


比如我们示例中的崩溃就是由于语言异常造成的,所以崩溃报告中会有 Last Exception Backtrace。

Last Exception Backtrace:
0 CoreFoundation 0x191560e38 __exceptionPreprocess + 164
1 libobjc.A.dylib 0x18a6f78d8 objc_exception_throw + 60
2 CoreFoundation 0x191706078 -[__NSCFString characterAtIndex:].cold.1 + 0
3 CoreFoundation 0x1917113ac -[__NSPlaceholderDictionary initWithCapacity:].cold.1 + 0
4 CoreFoundation 0x19157c2b8 -[__NSPlaceholderDictionary initWithObjects:forKeys:count:] + 320
5 CoreFoundation 0x19157c158 +[NSDictionary dictionaryWithObjects:forKeys:count:] + 52
6 CrashDemo 0x104a69e0c -[ViewController touchesBegan:withEvent:] + 152
.... 中间内容省略
25 CrashDemo 0x104a6a0c4 main + 120
26 dyld 0x1afed0960 start + 2528

以下是上述每一列元素的含义:

  • 第一列:栈帧号。堆栈帧按调用顺序排列,其中帧 0 是在执行暂停时正在执行的函数。第 1 帧是调用第 0 帧函数的函数,依此类推
  • 第二列:包含正在执行函数的二进制包名
  • 第三列:正在执行的机器指令的地址
  • 第四列:在完全符号化的崩溃报告中,正在执行的函数的名称。出于隐私原因,函数名称有时限制为前 100 个字符
  • 第五列(+ 号后面的数字):函数入口点到函数中当前指令的字节偏移量

通过第 6 行我们可以推断出问题是由 NSDictionary 引起的。


但大部分时候我们得到的报告都是未符号化的,我们需要对报告进行符号化来获得更多的信息。关于符号化的相关内容可以看这里


Thread state

Thread 0 crashed with ARM Thread State (64-bit):
x0: 0x0000000000000000 x1: 0x0000000000000000 x2: 0x0000000000000000 x3: 0x0000000000000000
...中间内容省略
far: 0x00000001e4d30560 esr: 0x56000080 Address size fault

崩溃报告的线程状态部分列出了应用程序终止时崩溃线程的 CPU 寄存器及其值。


Binary images

0x1cf074000 -        0x1cf0abfeb libsystem_kernel.dylib arm64e  <c76e6bed463530c68f19fb829bbe1ae1> /usr/lib/system/libsystem_kernel.dylib
...中间内容省略
0x18b8ca000 - 0x18c213fff Foundation arm64e <e5f615c7cc5e3656860041c767812a35> /System/Library/Frameworks/Foundation.framework/Foundation

以下是上述每一列元素的含义:

  • 第一列:二进制镜像在进程中的地址范围
  • 第二列:二进制镜像的名称
  • 第三列:操作系统加载到进程中的二进制映像中的 CPU 架构
  • 第四列:唯一标识二进制映像的构建 UUID。符号化崩溃报告时使用此值定位相应的 dSYM 文件
  • 第五列:二进制文件在磁盘上的路径

至此,报告上的所有 section 都已经解读完。希望大家看完这篇文章后,再分析崩溃日志的时候能更加得心应手。


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

开发工具 2.0 的时代已经来临

AI 正在变革软件工程:开发工具 2.0 时代 生成式 AI 的爆发已经开始改变了很多行业的工作方式,但对于软件工程来说,转型才刚刚开始。 从 Copilot 说起 Github Copilot 的成功引发了一场 AI 编程工具的浪潮,《Research: q...
继续阅读 »

AI 正在变革软件工程:开发工具 2.0 时代


生成式 AI 的爆发已经开始改变了很多行业的工作方式,但对于软件工程来说,转型才刚刚开始。


从 Copilot 说起


Github Copilot 的成功引发了一场 AI 编程工具的浪潮,《Research: quantifying GitHub Copilot’s impact on developer productivity and happiness》这份报告研究了 Copilot 对开发者效率和幸福感的提升,如下

  • 使用 GitHub Copilot 的开发人员比不使用 GitHub Copilot 的开发人员完成任务的速度快 55%
  • 使用 GitHub Copilot 的小组完成任务的比例为 78%,而没有使用 Copilot 的小组为 70%
  • 88% 的使用者认为自己生产力提高了
  • 96% 的使用者认为自己处理重复性的工作更快了
  • 88% 的使用者认为自己可以更加专注于更喜欢的工作上了


原文地址:github.blog/2022-09-07-…





从数据上来看,Copilot 已经是非常成功了,我们会认为这已经是一个大的变革,但是当我们把眼光放到整个软件工程行业的时候,才发现 Copilot 可能只是 AI 改变软件工程师工作方式的开端。



我曾经写了一篇 Copilot 的体验文章,有兴趣可以看看 # 与 AI 结对编程,好搭档 Copilot



开发工具 2.0 与现状


红衫资本在《Developer Tools 2.0》中定义了”开发工具 2.0“ :通过 AI 改变软件创造方式的工具。


还整理了一张图用以展示现有的开发工具在不同的软件研发阶段的应用。




这图本质上是一个表格,每一行从左到右代表了软件在当前市场的占用水平,分为

  • Incumbents:当前主流使用的标准工具
  • Challengers:挑战者,一些加入了 AI 特性的创新型工具
  • Dev Tools 2.0:通过 AI 改变软件创造方式的工具

列的话从上到下代表了软件开发的各个阶段,或者说生命周期,分别为

  • Deployment:部署阶段,包括 CI/CD、云、监控等
  • Implementation:实现阶段,包括 code review 工具、文档工具、代码编写维护工具等
  • Setup:配置阶段,包括 IDE、终端、ISSUE 记录工具等

接下来我们从上往下来分析。


Deployment 所属区域中,软件还是集中在 Incumbents(主流) 和 Challengers(挑战者) 中,这里可以看到很多熟悉的产品,比如 Datadog、Grafana、Aws、Jenkins 等。


但 Deployment 目前还没有 Dev Tools 2.0 的工具




Implementation 中,目前已有很多 Dev Tools 2.0 了,比如 AI code review 工具 Codeball、DEBUG 和对话工具 ChatGPT、AI 文档工具 Mintlify、以及 AI 代码补全工具 Copilot 和 Tabnine。


注意看细分的 write docs(文档编写) 和 write & maintain code (代码编写维护)中,在主流中这些都是人力维护,这说明当前的软件工程已经处于一个分水岭了:从人工到 AI。


对比 Deployment 的话,Implementation 的 2.0 工具可谓是百花齐放。




最后就是 Setup 了,目前只有 Cursor (一款集成了 ChatGPT 4 的代码编辑器)被完全定义为 Dev Tools 2.0




这里比较意外的是 warp 和 fig 居然没有被定义为 2.0 工具,因为我前段时间刚试用了 warp 终端,有兴趣的可以看看我发的视频


其实回顾一下红衫资本对 Dev Tools 2.0 的定义就能理解了:通过 AI 改变软件创造方式的工具。


warp 和 fig 只是带了 AI 的特性,还没有改变软件的创造规则,所以就被列入了 challenger 里。


从目前世面上的工具来看,AI 已经有了巨大的机会改变软件工程,并且这是一个关于“谁”,而不是“是与否”的问题。


开发工具 2.0 的共同点


再再再次啰嗦一下红衫资本对 Dev Tools 2.0 的定义:通过 AI 改变软件创造方式的工具。


我考察了 5 个图中被归类为 2.0 的软件,看看它们是如何改变软件的创作方式的



首先是 Cursor,我们可以用自然语言来写新的代码、维护既有代码,从这点来看它是超越了 Copilot (这不是指下一代 Copilot X )。




然后是 Codeball,它主要是用 AI 来自动执行 code review,它可以为每一个 PR 进行评分(检查代码规范、Bug 等)并自动合并,大量节省功能特性因 PR 被 Block 的时间,而且用机器代替人做检查也能避免 Review 成为形式主义的流程。




ChatGPT 此处就不做演示了,直接看一下 Grit 吧。虽然下面展示的动图只是将代码片段的优化,但 Grit 给自己的定位是通过 AI 自动化完成整个项目的代码迁移和升级,比如从 JavaScript 到 TypeScript、自动处理技术债等




最后就是 Adrenaline 了,它是一个 AI Debuger(调试器?),我输入了一段会导致 NullPointerException 的代码,但是因为服务器请求的数量太多无法运行。所以我直接在对话框里问了一句:Is there anything wrong with this code?(这段代码有问题吗?)。Adrenaline 不仅回答了会出问题,还详细分析了这段代码的功能




再来对比一下这几个场景下传统的处理方式



基于以上工具的特点,我们也可以畅想一下 Deployment 2.0 工具的特点

  1. 首先肯定是通过自然语言进行交互,比如:帮我在阿里云上部署一下 xxx 项目;也可以说帮我创建一个项目,这项目叫熔岩巨兽,需要使用到 mysql、redis,需要一个公网域名等…
  2. 然后是能够自动分析并配置项目的依赖,比如:部署 xxx 项目需要 mysql 数据库、redis 缓存
  3. 如果能够为我使用最优(成本、性能等多方面)的解决方案更好

其实随着云平台的成熟、容器化的普及,我相信这样的 Deployment 2.0 工具肯定不会太遥远。


事实上在写这篇文章的时候我就发现了 Github 上的一个项目叫 Aquarium,它已经初步基于 AI 的能力实现了部署,它给 AI 输入了以下的前提提示:



你现在控制着一个Ubuntu Linux服务器。你的目标是运行一个Minecraft服务器。不要回答任何批判、问题或解释。你会发出命令,我会回应当前的终端输出。 回答一个要给服务器的Linux命令。



然后向 AI 输入要执行的部署,比如:”Your goal is to run a minecraft server“。


接着 AI 就会不断的输出命令,Aquarium 负责在程序执行命令并将执行结果返回给 AI,,不断重复这个过程直到部署结束。


对开发者的影响


作为一名软件开发者,我们经常会自嘲为 CV 工程师,CV 代表了 ctrl + cctral + v ,即复制粘贴工程师。


这是因为大多数的代码都是通过搜索引擎查询获得,开发者可以直接复制、粘贴、运行,如果运行失败就把错误信息放进搜索引擎再次搜索,接着又复制、粘贴、运行……


但基于开发工具 2.0,这个流程就产生了变化:搜索、寻找答案、检查答案的过程变成了询问、检查答案,直接省去了最费时间的寻找答案的过程。




还有就是开发模式的改变,以前是理解上游的需求并手写代码,而现在是理解上游的需求并用自然语言描述需求,由 AI 写代码。


也就是说在代码上的关注会降低,需要将更多的注意力集中在需求上




也许你发现了,其实可以直接从产品到 AI,因为程序员极有可能是在重复的描述产品需求。


这个问题其实可以更大胆一点假设:如果 AI 可以根据输入直接获得期望的输出,那么老板可以直接对接 AI 了,80% 的业务人员都不需要。


既然已经谈到了对”人“的影响,那不如就接着说两点吧

  • 这些工具会让高级开发者的技能经验价值打折扣,高级和初级的编码能力会趋于拟合,因为每个人都拥有一个收集了全人类知识集的 AI 助手
  • 会编程的人多了,但是适合以编程为工作的人少了

很多开发者对此产生焦虑,其实也不必,因为这是时代的趋势,淹没的也不止你一个,浪潮之下顺势而为指不定也是一个机遇。


如果光看软件工具 2.0,它给软件工程带来的是一次转型,是一次人效的变革,目前来看还没有达到对软件工程的颠覆,那什么时候会被颠覆呢?



有一天有一个这样的游戏出现了,每个人在里面都是独一无二的,系统会为每个人的每个行为动态生成接下来的剧情走向,也就是说这个游戏的代码是在动态生成,并且是为每一个人动态生成。这个游戏的内存、存储空间等硬件条件也是动态在增加。 这就是地球 Online



短期来看,AI 还不会代替程序员,但会替代不会用 AI 的程序员。


AI 正在吞噬软件


最后就用两位大佬的话来结束本文吧。


原 Netscape(网景公司)创始人 Marc Andreessen 说过一句经典的话:软件正在吞噬世界。


人工智能领域知名科学家 Andrej Karpathy 在 2017 年为上面的话做了补充:软件(1.0)正在吞噬世界,现在人工智能(软件2.0)正在吞噬软件



Software (1.0) is eating the world, and now AI (Software 2.0) is eating software.



所以,你准备好了吗?


参考

  1. http://www.sequoiacap.com/article/ai-…
  2. karpathy.medium.com/software-2-…
  3. github.blog/2022-09-07-…
  4. github.com/fafrd/aquar…

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

CSS命名太头疼?这个Vite插件自动生成,让你解放双手!

web
CSS样式一直以来都是一个让前端开发者头疼的问题,随着前端工程化的发展,使用原子CSS进行样式开发正变得越来越流行。相比传统的CSS样式书写方式,原子CSS可以让我们以更模块化和可复用的方式进行样式的编码。但是手动编写大量原子类样式也比较烦琐。有没有办法自动生...
继续阅读 »

CSS样式一直以来都是一个让前端开发者头疼的问题,随着前端工程化的发展,使用原子CSS进行样式开发正变得越来越流行。相比传统的CSS样式书写方式,原子CSS可以让我们以更模块化和可复用的方式进行样式的编码。但是手动编写大量原子类样式也比较烦琐。有没有办法自动生成原子CSS类呢?今天我要介绍的Vite插件atom-css-generator就可完美实现这一功能。


原子CSS简介


原子CSS(Atomic CSS)将传统的CSS类拆分成一个个独立的、原子级的类,每个类仅包含一个CSS属性,例如:



.p-10 {
padding: 10px;
}

.bg-red {
background: red;
}

相比于传统的CSS类,原子类具有以下特点:



  • 原子:每个类只包含一个CSS属性,拆分到最小粒度

  • 独立:类名语义明确,可以任意组合使用而不会产生冲突

  • 可复用:一个原子类可以重复使用在不同的组件中


使用原子CSS的优势在于:



  • 更模块化:样式属性高内聚、解耦

  • 更可维护:不同类名称、不同文件,避免影响

  • 更灵活:组件样式由原子类组合,更容易扩展和维护


但是编写大量原子类也比较麻烦,多达几千个类定义都可能出现。有没有自动生成的方式呢?


atom-css-generator插件介绍


atom-css-generator是一个Vite插件,它可以通过解析Vue组件中的class,自动生成对应的原子CSS定义


安装和配置


使用npm或yarn安装:


Copy code

npm install atom-css-generator

在vite.config.js中引入插件:


js

Copy code

import atomCssGenerator from 'atom-css-generator';

export default {
plugins: [
atomCssGenerator({
outputPath: 'assets/styles'
})
]
}

主要的配置项有:



  • outputPath:指定生成的CSS文件输出目录,默认为public


使用方式



  1. 在Vue组件的template中,使用特定格式的class,例如:


html

Copy code

<template>
<div class="bg-red fc-white p-20">
<!-- ... -->
</div>
</template>


  1. 构建项目时,插件会自动生成对应的原子CSS类定义:


css

Copy code

.bg-red {
background-color: red;
}

.fc-white {
color: white;
}

.p-20 {
padding: 20px;
}


  1. style.css会被自动生成到指定的outputPath中,并注入到HTML文件头部。


支持的类名格式


插件支持多种格式的类名规则生成,包括:



  • 颜色类名:bg-red、fc-333

  • 间距类名:p-20、ml-10

  • 尺寸类名:w-100、h-200

  • Flexbox类名:jc-center、ai-stretch

  • 边框类名:bc-333、br-1-f00-solid

  • 布局类名:p-relative、p-fixed

  • 文字类名:fs-14、fw-bold


等等,非常全面。


而且也内置了一些预设的实用样式类,比如文字截断类te-ellipsis。


原理简析


插件主要通过以下处理流程实现自动生成原子CSS:



  1. 使用@vue/compiler-sfc解析Vue文件,获取模板内容

  2. 通过正则表达式提取模板中的class名称

  3. 根据特定类名规则,生成对应的CSS定义

  4. 将CSS写入style.css文件中,并注入到HTML中


同时,插件还会在热更新时自动检查新添加的类名,从而动态更新style.css。


总结


通过atom-css-generator这个插件,我们可以非常轻松地在Vue项目中使用原子CSS样式,而不需要手动编写。它省去了我们大量重复的工作,使得样式的维护和扩展更加简单。


如果你也想尝试在自己的项目中引入原子CSS,不妨试试这个插件。相信它能给你带来意想不到的便利!
GitHub地址

收起阅读 »

扒一扒uniapp是如何做ios app应用安装的

iOS
为何要扒 因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来...
继续阅读 »

为何要扒


因为最近有移动端业务的需求,用uniapp做了ios、Android双端的app应用,由于没有资质上架AppStore和test flight,所以只能使用苹果的超签(需要ios用户提供uuid才能加入测试使用,并且只支持100人安装使用)。打包出来生成的是一个ipa包,并不能直接安装,要通过爱思助手这类的应用装一下ipa包。但交付到客户手上就有问题了,还需要电脑连接助手才能安装,那岂不是每次安装新版什么的,都要打开电脑搞一下。因此,才有了这次的扒一扒,目标就是为了解决只提供一个下载链接用户即可下载,不用再通过助手类应用安装ipa包。




开干


官方模板




先打开uniapp云打包一下项目看看


image-20230824112232275.png




复制地址到移动端浏览器打开看看


image-20230824112410817.png


这就对味了,都知道ios是不能直接打开ipa文件进行安装的,接下来就研究下这个页面的执行逻辑。




开扒




F12打开choromdevtools,ctrl+s保存网页html。


image.png


保存成功,接下来看看html代码(样式代码删除了)


    <!DOCTYPE html>
<!-- saved from url=(0077)https://ide.dcloud.net.cn/build/download/2425a4b0-4229-11ee-bd1b-67afccf2f6a7 -->
<html>
   <head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
  <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no, width=device-width">
  <meta name="apple-mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
 </head>

<body>
<br><br>
   <center>
       <a class="button" href="itms-services://?action=download-manifest&amp;url=https://ide.dcloud.net.cn/build/ipa-xxxxxxxxxxx.plist">点击安装</a>
   </center>
   <br><br>
   <center>注意:只提供包名为io.dcloud.appid的包的直接下载安装,如果包名不一致请自行搭建下载服务器</center>
</body>
</html>



解析




从上面代码可以看出,关键代码就一行也就是a标签的href地址("itms-services://?action=download-manifest&url=ide.dcloud.net.cn/build/ipa-x…")


先看看itms-services是什么意思,下面是代码开发助手给的解释


image-20230824113418246.png


大概意思就是itms-services是苹果提供给开发者一个的更新或安装应用的协议,用来做应用分发的,需要指向一个可下载的plist文件地址。




什么又是plist呢,这里再请我们的代码开发助手解释一下


image-20230824113748570.png


对于没接触过ios相关开发的,连plist文件怎么写都不知道,既然如此,那接下来就来扒一下dcloud的pilst文件,看看官方是怎么写的吧。




打开浏览器,copy一下刚刚扒下来的html文件下a标签指向的地址,复制url后面plist文件的下载地址粘贴到浏览器保存到桌面。


image-20230824114108792.png


访问后会出现


image-20230824115354028.png




别担心,这时候直接按ctrl+s可以直接保存一个plist.xml文件,也可以打开devtools查看网络请求,找到ipa开头的请求


image-20230824115609551.png


直接新建一个plist文件,cv一下就好,我这里就选择保存它的plist.xml文件,接下来康康文件里到底是什么


<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>https://bdpkg-aliyun.dcloud.net.cn/20230824/xxxxx/Pandora.ipa?xxxxxxxx</string>
</dict>
      <dict>
<key>kind</key>
<string>display-image</string>
<key>needs-shine</key>
<false/>
<key>url</key>
<string>https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni.png</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>kind</key>
<string>software</string>
<key>bundle-identifier</key>
<string>xxxxx</string>
<key>title</key>
<string>HBuilder手机应用</string>
</dict>
</dict>
</array>
</dict>
</plist>

直接抓重点,这里存你存放ipa包的地址


image-20230824120013828.png


这里改你应用的昵称


image-20230824120453368.png


这里改图标


image-20230824120509797.png


因篇幅限制,想了解plist的自行问代码助手或者搜索引擎。




为我所用


分析完了,如何为我所用呢,首先按照分析上扒下来的plist文件修改下自身应用的信息,并且需要服务器存放ipa文件,这里我选择了unicloud,开发者可以申请一个免费的空间(想了解更多的自己去dcloud官网看看,说多了有打广告嫌疑),替换好大概如下:


image-20230824155040313.png


将plist文件放到服务器上后,拿到plist的下载地址,打开扒下来的html,将a标签上的url切换成plist文件的下载地址,如图:


image-20230824155306228.png


可以把页面上没用的信息都删掉,保存,再把html放到服务器上,用户访问这个地址,就可以直接下载描述文件安装ipa包应用了(记得需要添加用户的uuid到开发者账号上),其实至此需求已经算是落幕了,但转念想想还是有点麻烦,于是又优化了一下,将a标签中的href信息,直接加载到二维码上供用户扫描便可直接下载,相对来说更方便一点,于是我直接打开草料,生成了一个二维码,至

作者:廿一c
来源:juejin.cn/post/7270799565963149324
此,本次扒拉过程结束,需求落幕!

收起阅读 »

iOS - 上手AR

iOS
前言 随着 Apple Vision Pro 的发布,势必掀起新一波的Ar潮,简单了解一下来个小Demo 开始 要在iOS中创建一个的AR物体,你可以使用 ARKit 和 SceneKit 来实现 首先,确保你的项目已经导入了 ARKit 和 SceneKit...
继续阅读 »

前言


随着 Apple Vision Pro 的发布,势必掀起新一波的Ar潮,简单了解一下来个小Demo


开始


要在iOS中创建一个的AR物体,你可以使用 ARKitSceneKit 来实现


首先,确保你的项目已经导入了 ARKit 和 SceneKit 框架。你可以在 Xcode 中的项目设置中添加 ARKit.framework 和 SceneKit.framework 到 "Frameworks, Libraries, and Embedded Content" 部分




然后,在你的程序文件中,导入 ARKit 和 SceneKit

import UIKit
import ARKit

接下来,创建一个 ARSCNView,并将其添加到你的视图层次结构中:

// 创建 ARSCNView 实例
sceneView = ARSCNView(frame: view.bounds)
view.addSubview(sceneView)
sceneView.delegate = self

// 创建 SCNScene 实例,并设置为 sceneView 的场景
let scene = SCNScene()
sceneView.scene = scene

然后,在视图控制器的生命周期方法中,配置 ARSession 并启动 AR 会话:

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)

// 配置 AR 会话并启动
let configuration = ARWorldTrackingConfiguration()
sceneView.session.run(configuration)
}

override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)

// 暂停 AR 会话
sceneView.session.pause()
}

现在,已经设置好 ARKit 和 AR 会话已经开始运行。接下来,我们将创建3D 模型:

func createBallNode() {
// 创建球体几何体
let ballGeometry = SCNSphere(radius: 0.1)
let ballMaterial = SCNMaterial()
ballMaterial.diffuse.contents = UIImage(named: "cxkj.webp") // 使用纹理图片
ballGeometry.materials = [ballMaterial]

// 在屏幕范围内生成随机位置坐标
let randomX = Float.random(in: -1.0...1.0) // 在屏幕宽度范围内生成随机 X 坐标
let randomY = Float.random(in: -1.0...1.0) // 在屏幕高度范围内生成随机 Y 坐标
let randomZ = Float.random(in: -3.0...0.0) // 在屏幕深度范围内生成随机 Z 坐标
ballNode = SCNNode(geometry: ballGeometry)
ballNode.position = SCNVector3(randomX, randomY, randomZ)

// 将球体节点添加到场景的根节点上
sceneView.scene.rootNode.addChildNode(ballNode)
}

最后我们通过点击事件将3D模型添加到场中

@objc func handleTap(_ gesture: UITapGestureRecognizer) {
if gesture.state == .ended {
createBallNode() // 创建球体节点
}
}

效果




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

iOS 快速复习GCD

iOS
多线程-串行、并行队列,同步、异步任务 1、创建串行队列和并行队列 //并行队列 dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_C...
继续阅读 »

多线程-串行、并行队列,同步、异步任务


1、创建串行队列和并行队列

    //并行队列
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_CONCURRENT);
//串行队列
dispatch_queue_t queue = dispatch_queue_create("com.lg.cooci.cn", DISPATCH_QUEUE_SERIAL);

  • 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
  • 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务),并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。

2、同步异步任务

//同步
dispatch_sync(queue, ^{
        NSLog(@"1");
    });
//异步
dispatch_async(queue, ^{
        NSLog(@"1");
    });

同步执行:

  • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
  • 只能在当前线程中执行任务,不具备开启新线程的能力。

异步执行:

  • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
  • 可以在新的线程中执行任务,具备开启新线程的能力。

异步执行(async) 虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。

默认全局并发队列:dispatch_get_global_queue

第一个参数表示队列优先级,一般用 DISPATCH_QUEUE_PRIORITY_DEFAULT

第二个参数暂时没用,用 0 即可。


信号量 dispatch_semaphore_t



GCD中的信号量dispatch_semaphore_t中主要有三个函数:

  • dispatch_semaphore_create:创建信号
  • dispatch_semaphore_wait:等待信号
  • dispatch_semaphore_signal:释放信号

1、dispatch_semaphore_create
参数为int,表示信号量初始值,需大于等于0,否则创建失败,返回一个dispatch_semaphore_t


2、dispatch_semaphore_wait
参数1:

需传递一个 dispatch_semaphore_t 类型对象,对信号进行减1,然后判断信号量大小

参数2:

传递一个超时时间:dispatch_time_t 对象

  • 减1后信号量小于0,则阻塞当前线程,直到超时时间到达或者信号量大于等于0后继续执行后面代码
  • 减1后信号量大于等于0,对dispatch_semaphore_t 进行赋值,并返回dispatch_semaphore_t对象,继续执行后面代码

3、dispatch_semaphore_signal
参数:dispatch_semaphore_t

进行信号量加1操作,如果加1后结果大于等于0,则继续执行,否则继续等待。


用法:

- (void)startAsync{
//创建信号量 值为0
    self.sem = dispatch_semaphore_create(0);
//开启异步并发线程执行
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"dispatch_semaphore 2\n");
        sleep(5);
//发送信号,信号量值+1
        dispatch_semaphore_signal(self.sem);
        NSLog(@"dispatch_semaphore 3\n");
    });
    NSLog(@"dispatch_semaphore 0\n");
//信号量 值-1 小于0 等待信号。。。
    dispatch_semaphore_wait(self.sem, DISPATCH_TIME_FOREVER);
    NSLog(@"dispatch_semaphore 1\n");

}
执行顺序0 2 1 3 1和3不确定顺序
如果初始化创建是信号量值为1
执行顺序0 1 2 3

常用总结:

1、异步并发线程顺序执行

2、异步并发线程控制最大并发数,比如下载功能控制最大下载数


调度组 dispatch_group_t


主要API:

  • dispatch_group_create:创建组

  • dispatch_group_async:进组任务

  • dispatch_group_notify:组任务执行完毕的通知

  • dispatch_group_enter:进组

  • dispatch_group_leave:出组

  • dispatch_group_wait:等待组任务时间


组合用法1:

- (void)dispatchGroupAsync{
//创建调度组
    dispatch_group_t group = dispatch_group_create();
//获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//开启异步线程
    dispatch_group_async(group, queue, ^{
        sleep(2);
        NSLog(@"11");
    });
    dispatch_group_async(group, queue, ^{
        sleep(1);
        NSLog(@"12");
    });
    dispatch_group_async(group, queue, ^{
        sleep(3);
        NSLog(@"13");
    });
    NSLog(@"14");
    dispatch_group_notify(group, queue, ^{
//收到执行完成的通知后执行
        NSLog(@"15");
    });
//等待调度组执行完成
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
调度组执行完成后执行
    NSLog(@"16");
}

用法2:

- (void)dispatchSyncEnterGroup{
//创建调度组
    dispatch_group_t group = dispatch_group_create();
//获取全局并发队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//进入调度组
    dispatch_group_enter(group);
//执行异步任务
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"21");
//执行完成后立刻调度组
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        sleep(1);
        NSLog(@"22");
        dispatch_group_leave(group);
    });
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        sleep(3);
        NSLog(@"23");
        dispatch_group_leave(group);
    });
    NSLog(@"24");
    dispatch_group_notify(group, queue, ^{
//执行完后回调
        NSLog(@"25");
    });
    NSLog(@"26");
//等待调度组执行完成
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    NSLog(@"27");
}

总结:

1、dispatch_group_async 是对dispatch_group_enter和dispatch_group_leave的封装

2、dispatch_group_enter和dispatch_group_leave的须成双成对的出现


事件源 dispatch_source_t


主要API:

  • dispatch_source_create :创建源

  • dispatch_source_set_event_handler: 设置源的回调

  • dispatch_source_merge_data: 源事件设置数据

  • dispatch_source_get_data: 获取源事件的数据

  • dispatch_resume:恢复继续

  • dispatch_suspend:挂起

  • uintptr_t dispatch_source_get_handle(dispatch_source_t source) //得到dispatch源创建,即调用dispatch_source_create的第二个参数

  • unsignedlong dispatch_source_get_mask(dispatch_source_t source); //得到dispatch源创建,即调用dispatch_source_create的第三个参数


源的类型dispatch_source_type_t

1. DISPATCH_SOURCE_TYPE_DATA_ADD:用于ADD合并数据
2. DISPATCH_SOURCE_TYPE_DATA_OR:用于按位或合并数据
3.DISPATCH_SOURCE_TYPE_DATA_REPLACE:跟踪通过调用dispatch_source_merge_data获得的数据的分派源,新获得的数据值将替换 尚未交付给源处理程序 的现有数据值
4. DISPATCH_SOURCE_TYPE_MACH_SEND:用于监视Mach端口的无效名称通知的调度源,只能发送没有接收权限
5. DISPATCH_SOURCE_TYPE_MACH_RECV:用于监视Mach端口的挂起消息
6. DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:用于监控系统内存压力变化
7.DISPATCH_SOURCE_TYPE_PROC:用于监视外部进程的事件
8. DISPATCH_SOURCE_TYPE_READ:监视文件描述符以获取可读取的挂起字节的分派源
9. DISPATCH_SOURCE_TYPE_SIGNAL:监控当前进程以获取信号的调度源
10. DISPATCH_SOURCE_TYPE_TIMER:基于计时器提交事件处理程序块的分派源
11. DISPATCH_SOURCE_TYPE_VNODE:用于监视文件描述符中定义的事件的分派源
12. DISPATCH_SOURCE_TYPE_WRITE:监视文件描述符以获取可写入字节的可用缓冲区空间的分派源。

1、dispatch_source_create 参数:

  • dispatch_source_type_t 要创建的源类型
  • uintptr_t 句柄 用于和其他事件并定,很少用,通常为0
  • uintptr_t mask 很少用,通常为0
  • dispatch_queue_t 事件处理的调度队列

用法:

self.sourceAdd = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));

2、dispatch_source_set_event_handler 设置回调函数,当触发源事件时执行

//需要注意循环引用
dispatch_source_set_event_handler(self.sourceAdd, ^{
需要执行的代码
});
//启动
dispatch_resume(self.sourceAdd);
//挂起,即暂停
dispatch_suspend(self.sourceAdd);
这两个API需要成对使用,不可多次挂起或者多次恢复

3、dispatch_source_cancel 取消事件源,取消后不可再恢复或挂起,需要再次创建
4、dispatch_source_set_timer 当事件源类型为定时器类型(DISPATCH_SOURCE_TYPE_TIMER)时,设置开始时间、重复时间、允许时间误差


定时器实现比较简单容易,网上教程也多,这里主要介绍一下:DISPATCH_SOURCE_TYPE_DATA_ADD、DISPATCH_SOURCE_TYPE_DATA_OR、DISPATCH_SOURCE_TYPE_DATA_REPLACE。


先说下结果:

  • DISPATCH_SOURCE_TYPE_DATA_ADD 会把事件源累加 可以记录总共发送多少次事件进行合并
  • DISPATCH_SOURCE_TYPE_DATA_OR 会把事件源合并,最终得到的数据源数为1
  • DISPATCH_SOURCE_TYPE_DATA_REPLACE 会用最新事件源替换旧有未处理事件,最终得到的数据源数为1
  • 循环10000次实际跑处理回调事件次数 add315 or275 replace 284

从结果上来看,当需要把快速频繁的重复事件进行合并,最好的选择是DISPATCH_SOURCE_TYPE_DATA_OR,使用场景,监听消息时,多消息频繁下发需要刷新UI,如果不进行合并处理,会导致UI太过频繁的刷新,影响最终效果,且对性能开销过大。


当然,类似的场景也可使用其他方式处理,比如建立消息池,接收消息后标记消息池状态及变化,然后定时从消息池中取消息。诸如此类的方法较多,如果只是简单的处理,上面的DISPATCH_SOURCE_TYPE_DATA_OR模式应该满足使用。


代码:

//创建源
self.sourceAdd = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));
//弱引用
__weak typeof(self) weakifySelf = self;
//设置回调事件
dispatch_source_set_event_handler(self.sourceAdd, ^{
//强引用
__strong typeof(self) strongSelf = weakifySelf;
//获取接收到的源数据
strongSelf.handleData = dispatch_source_get_data(strongSelf.sourceAdd);
NSLog(@"dispatch_source1 %ld\n",strongSelf.handleData);
//需要执行的代码
[strongSelf sourceHandle];

        });
//开启源
dispatch_resume(self.sourceAdd);
for (int i = 0; i<10000; i ++) {

[self dispatchSource];
}
- (void)dispatchSource{

    NSLog(@"dispatch_source2 %ld\n",self.handleData);
//发送源信号
    dispatch_source_merge_data(self.sourceAdd, 1);
}

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

自建”IT兵器库”,你值得一看!

web
现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!! 常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小...
继续阅读 »

现在市面的组件库,我们用的越发熟练,越发爆嗨,只需cv就能完成需求,为何不爆嗨!!!


常用库有 element、antd、iView、antd pro,这些组件库都是在以往的需求开发当中进行总结提炼,一次次符合我们的需求,只要有文档,只要有示例,页面开发都是小问题,对吧,各位优秀开发前端工程师。


接下来,根据开发需求,一步步完成一个组件的开发,当然可能自己的思路,并不是最好的,欢迎大家留言讨论,一起进步


需求:


动态列表格,就是一个表格,存在默认列,但是支持我们操控,实现动态效果


实现效果


默认表格配置


image.png


默认列配置


image.png


动态列组件支持查询


image.png


动态列组件支持勾选


image.png


动态列组件支持清空


image.png


动态列组件支持一键全选


image.png


动态列组件支持一键清空


image.png


功能点划分



  • 表格默认列和动态列组件默认选中项 实现双向绑定

  • 动态列组件 增删改与表格 实现双向绑定

  • 动态列组件 实现搜索

  • 动态列组件 实现单点控制 添加与删除

  • 动态列组件 实现一键控制功能 全选与清空

  • 动态列组件 实现恢复初始态


使用到组件(Antd 组件库哈)



  • Table

  • Pagination

  • Modal

  • Input

  • Button

  • Checkbox


动态列组件区域划分



  • 头部标题

  • 头部提示语

  • 核心内容区



    • 核心区域头部功能按钮





    • 搜索区域





    • 左边所有内容项





    • 待选内容项




动态列组件最终可支持配置项


  open?: boolean // Modal状态
setOpen?: React.Dispatch> // 控制Modal状态
modalTitle?: string | React.ReactNode
modalWidth?: number
modalHeadContent?: React.ReactNode
leftHeadContent?: React.ReactNode | string
rightHeadContent?: React.ReactNode | string
modalBodyStyle?: any
searchPlaceholder?: string
modalOk?: (val, isUseDefaultData?: boolean) => void // 第二个参数 内部数据处理支持
enableSelectAll?: boolean // 是否开启全选功能
selectData: SelectItem[] // 下拉框数据
isOutEmitData?: boolean
defaultSelectKeyList?: string[] // 默认选中的key(当前表格列)自定义数据(外部做逻辑处理)
initSelectKey?: string[] // 初始表格选中的key 自定义数据(外部做逻辑处理)
curColumns?: any[] // 当前表格列 内部做逻辑处理
originColumns?: any[] // 原始表格列 内部做逻辑处理
isDelHeadCol?: boolean // 删除头部columnKey 例如序号 只有内部处理数据时生效
isDelTailCol?: boolean // 删除尾部columnKey 例如操作 只有内部处理数据时生效
isDelHeadAndTail?: boolean // 删除头尾部columnKey 只有内部处理数据时生效

动态列组件布局



    

// 头部内容区
{modalHeadContent}

// 以下维核心区


// 核心区-左边

// 核心区-功能按钮 - 一键全选
{enableSelectAll && (

onCheckBoxChange([], e)} checked={checkAll} indeterminate={indeterminate}>
全选


)}
{leftHeadContent || ''}



// 核心区-左搜索
{childSearchRender({ curData: leftSelectData })}

// 核心区-左列表区域
{selectItemRender(leftSelectData)}



// 核心区-右边


{rightHeadContent || ''}

// 核心区-功能按钮 - 一键清空
handleRightClearSelectData()}>
清空



// 核心区-右搜索
{childSearchRender({ curData: rightSelectData }, true)}

// 核心区-右列表区域
{selectItemRender(rightSelectData, true)}






动态列组件-列表渲染


const selectItemRender = (listArr = [], isRight = false) => {
return (


// 数据遍历形式
{listArr?.map(({ label, value, disabled = false }) => (

{!isRight && (

{label}

)}
// 判断是否是 右边列表区域 添加删除按钮
{isRight && {label}}
{isRight && (



)}

))}


)
}

动态列组件-搜索渲染


const childSearchRender = (childSearchProps: any, isRight = false) => {
// eslint-disable-next-line react/prop-types
const { curData } = childSearchProps
return (
{
onSearch(e, curData, isRight)
}}
allowClear
/>
)
}

动态列组件样式


.content-box {
width: 100%;
height: 550px;
border: 1px solid #d9d9d9;
}
.content-left-box {
border-right: 1px solid #d9d9d9;
}
.content-left-head {
padding: 16px 20px;
background: #f5f8fb;
height: 48px;
box-sizing: border-box;
}
.content-right-head {
padding: 16px 20px;
background: #f5f8fb;
height: 48px;
box-sizing: border-box;

&-clear {
color: #f38d29;
cursor: pointer;
}
}
.content-right-box {
}
.content-left-main {
padding: 10px 20px 0 20px;
height: calc(100% - 50px);
box-sizing: border-box;
}
.content-right-main {
padding: 10px 20px 0 20px;
height: calc(100% - 50px);
box-sizing: border-box;
}
.right-head-content {
font-weight: 700;
color: #151e29;
font-size: 14px;
}
.modal-head-box {
color: #151e29;
font-size: 14px;
height: 30px;
}
.icon-box {
color: #f4513b;
}
.ant-checkbox-group {
flex-wrap: nowrap;
}
.left-select-box {
height: 440px;
padding-bottom: 10px;
}
.right-select-box {
height: 440px;
padding-bottom: 10px;
}
.ant-checkbox-wrapper {
align-items: center;
}
.display-box {
height: 22px;
}

功能点逐一拆解实现


点1:表格默认列和动态列组件默认选中项 实现双向绑定



  • 首先,先写一个表格啦,确定列,这个就不用代码展示了吧,CV大法

  • 其次,把表格原始列注入动态列组件当中,再者注入当前表格列当前能选择的所有项

  • 当前能选择所有项内容参数示例


[
{ label: '项目编码', value: 'projectCode' },
{ label: '项目名称', value: 'projectName' },
{ label: '项目公司', value: 'company' },
{ label: '标段', value: 'lot' },
]




  • 动态组件内部默认选中当前表格列

  • 这里需要把表格列数据 进行过滤 映射成 string[]


   内部是通过checkbox.Grop 实现选中 我们只需要 通过一个状态去控制即可 `selectKey`
<Checkbox.Group onChange={onCheckChange} className="flex-col h-full flex" value={selectKey}>
...
Checkbox.Group>

动态列组件 单点控制 增删改



  • 增,删,改就是实现 左右边列表的双向绑定

  • 监听 左边勾选事件 + 右边删除事件 + 一键清空事件

  • 通过左右两边的状态 控制数据即可

  • 状态


  const [originSelectData, setOriginSelectData] = useState([]) // 下拉原始数据 搜索需要
const [originRightSelectData, setOriginRightSelectData] = useState([])
const [rightSelectData, setRightSelectData] = useState([])
const [selectKey, setSelectKey] = useState([])
const [transferObj, setTransferObj] = useState({})
const [indeterminate, setIndeterminate] = useState(false)
const [checkAll, setCheckAll] = useState(false)
const [leftSelectData, setLeftSelectData] = useState([])
const [defaultSelectKey, setDefaultSelectKey] = useState([])
const [originSelectKey, setOriginSelectKey] = useState([])

const onCheckChange = checkedValues => {
// 往右边追加数据
const selectResArr = checkedValues?.map(val => transferObj[val])
setSelectKey(checkedValues) // 我们选中的key (选项)
setRightSelectData(selectResArr) // 右边列表数据
setOriginRightSelectData(selectResArr) // 右边原数据(搜索时候需要)
}

const deleteRightData = key => {
const preRightData = rightSelectData
const filterResKeyArr = preRightData?.filter(it => it.value !== key).map(it => it.value) // 数据处理 只要你不等于删除的key 保留
const filterResItemArr = preRightData?.filter(it => it.value !== key)
setRightSelectData(filterResItemArr) // 更新右边数据 即可刷新右边列表
setOriginRightSelectData(filterResItemArr) // 更新右边数据
setSelectKey(filterResKeyArr) // 更新选中的key 即可刷新左边选中项
}

  const handleRightClearSelectData = () => {
// 这就暴力了塞
setSelectKey([])
setRightSelectData([])
setOriginRightSelectData([])
}

动态列组件 实现搜索



  • 搜索,就是改变一下数据 视觉上看起来有过滤的效果塞

  • 刚才我们不是多存了一份数据源嘛

  • 出来见见啦~


const onSearch = (val, curData, isRight = false) => {
const searchKey = val
// 这个是同时支持 左右两边
// 做个判断
if (!isRight) {
// 在判断一下是否有搜索内容 因为也需要清空的啦
if (searchKey) {
// 有,我就过滤呗
const searchResArr = curData?.filter(item => item.label.includes(searchKey))
setLeftSelectData(searchResArr)
}
if (!searchKey) {
// 没有 我就把原本数据还给你呗
setLeftSelectData(originSelectData)
}
}
// 右边 一样
if (isRight) {
if (searchKey) {
const searchResArr = curData?.filter(item => item.label.includes(searchKey))
setRightSelectData(searchResArr)
}
if (!searchKey) {
setRightSelectData(originRightSelectData)
}
}
}

动态列组件 增删改与表格 实现数据绑定



  • 里面的数据 处理好了 直接再关闭的时候 丢给外面嘛

  • 把右边的内容(也就是选中的key)返回给表格

  • 表格再自己构造


  const handleOk = (colVal, isUseDefaultCol) => {
`colVal` : 选中的列key
`isUseDefaultCol`:是否使用默认列
// table column字段组装
const normalColConstructor = (
title,
dataIndex,
isSort = true,
colWidth = 150,
isEllipsis = false,
render = null
) => {
const renderObj = render ? { render } : {}
return {
title,
dataIndex,
sorter: isSort,
width: colWidth,
ellipsis: isEllipsis,
key: dataIndex,
...renderObj,
}
}
const statusRender = text => approvalStatusRender(text)
const dateRender = (text, record) => {dayjs(text).format('YYYY-MM-DD')}
const newColArr = []
// 定制化处理 (其实还有2.0)
colVal?.forEach(({ label, value }, index) => {
let isSort = false
let renderFn = null
const isSubmissionAmount = value === 'submissionAmount'
const isApprovalAmount = value === 'approvalAmount'
const isReductionRate = value === 'reductionRate'
const isInitiationTime = value === 'initiationTime'

// 特定的业务场景 特殊状态渲染
const isStatus = value === 'status'
// 特定的业务场景 时间类型 加上排序
if (isApprovalAmount || isInitiationTime || isReductionRate || isSubmissionAmount) {
isSort = true
}
if (isStatus) {
renderFn = statusRender
}
// 普通列 已就绪
// 普通列 标题 拿label就ok
newColArr.push(normalColConstructor(label, value, isSort, 100, true, renderFn))
})

// 最后在头部追加一个序号
newColArr.unshift({
title: '序号',
dataIndex: 'orderCode',
width: 45,
render: (_: any, record: any, index) => tablePageSize * (tablePage - 1) + index + 1,
})
// 最后在尾部部追加一个操作
newColArr.push({
title: '操作',
dataIndex: 'action',
fixed: 'right',
width: 50,
render: (text, row: DataType) => (



),
})

if (colVal?.length) {
if (isUseDefaultCol) {
setColumns([...originColumns])
} else {
setColumns([...newColArr])
}
} else {
setColumns([...originColumns])
}
}

// 解决表格存在子列 -- 先搞定数据结构 -- 在解决表格列内容填充 -- 无需捆绑
// 遍历拿到新增列数组 匹配上代表 需要子数组 进行转换
// eslint-disable-next-line consistent-return
colVal?.forEach(({ label, value }, index) => {
// DesignHomeDynamicCol[value] 返回的值能匹配到 说明存在 嵌套列
const validVal = DesignHomeDynamicClassify[DesignHomeDynamicCol[value]]
const isHasChild = newColChildObj[validVal]
const titleText = DesignHomeDynamicLabel[value]
if (validVal) {
// 如果已经有孩子 追加子列
if (isHasChild) {
newColChildObj[validVal] = [...isHasChild, normalColConstructor(titleText, value)]
} else {
// 则 新增
newColChildObj[validVal] = [normalColConstructor(titleText, value)]
}
} else {
// 普通列 已就绪
// 普通列 标题 拿label就ok
newColArr.push(normalColConstructor(label, value, false, 100, true))
}
})

动态列组件 实现恢复初始态 实现双向绑定



  • 这个就更简单啦 再点击确定的时候 传一个 isUseDefaultData:true

  • 只是这个isUseDefaultData 的逻辑判断问题

  • 当动态列组件 点击恢复默认列 我们只需把 当初传进来的 原始列数据 更新到 selectKey 状态即可


const handleDefaultCol = () => {
// 这里是考虑到组件灵活性 数据可由自己处理好在传入
if (isOutEmitData) {
setSelectKey(initSelectKey)
} else {
// 这里是使用 内部数据处理逻辑
setSelectKey(originSelectKey)
}
}

const handleOk = () => { 
// 数据比对 是否使用默认校验
// originColumnMapSelectKey 源数据与传出去的数据 进行比对
const originRightMapKey = originRightSelectData?.map(it => it.value)
// 采用 lodash isEqual 方法
const isSame = isEqual(originSelectKey, originRightMapKey)
// 判断外部是否有传 确定事件 handleOk
if (modalOk) {
modalOk(originRightSelectData, isSame)
}
setOpen(false)
}

const handleOk = (colVal, isUseDefaultCol) => {
... 一堆代码
// 当用户清空以后 还是恢复表格默认状态
if (colVal?.length) {
// 恢复默认列
if (isUseDefaultCol) {
setColumns([...originColumns])
} else {
// 否则就拿新数据更新
setColumns([...newColArr])
}
} else {
setColumns([...originColumns])
}
}

动态列组件 实现一键控制功能 全选与清空



  • 这就是Vip版本的噻

  • 但是也简单 无非就是操作多选框 无非多选框就三种态

  • 未选 半选 全选

  • 既然我们下面的逻辑已处理好 这个其实也很快的锅

  • 首先,就是下面数据变化的时候 我们上面需要去感应

  • 其次就是 上面操作的时候 下面也需要感应

  • 最后 双向数据绑定 就能搞定 没有那么神秘

  • 一步一步来 先分别把 上下事件处理好


const onCheckBoxChange = (dataArr = [], e = null) => {
// 判断所有数据长度
const allLen = originSelectData?.length
// 根据当前选中数据长度 判断是 多选框三种状态当中的哪一种
const checkLen = e ? selectKey?.length : dataArr?.length // 全选
const isAllSelect = allLen === checkLen // 半选
const isHalfSelect = allLen > checkLen
// 然后再判断一下是点击一键全选事件 触发还是 点击下面选项的时候触发
// 点击一键全选 能拿到事件的 e.target 从而来判断
// 这里是操作下面按钮的时候 触发
if (!e) {
// 如果没有选中
if (checkLen === 0) {
// 恢复未选状态
setCheckAll(false)
setIndeterminate(false)
return ''
}
if (isAllSelect) {
// 如果是全选 改为全选态
setCheckAll(true)
setIndeterminate(false)
}
if (isHalfSelect) {
// 半选态
setIndeterminate(true) // 这个控制 多选框的半选态
setCheckAll(false)
}
}
// 这个就是用户操作 一键全选按钮触发
if (e) {
// 如果当前长度为0 那么应该更新为全选
if (checkLen === 0) {
setCheckAll(true)
setIndeterminate(false)
setSelectKey(originSelectData?.map(it => it.value))
}
// 如果已经全选 就取消全选
if (isAllSelect) {
setCheckAll(false)
setIndeterminate(false)
setSelectKey([])
}
// 如果是半选态 就全选
if (isHalfSelect) {
setCheckAll(true)
setIndeterminate(false)
setSelectKey(originSelectData?.map(it => it.value))
}
}
}

const onCheckChange = checkedValues => {
// 往右边追加数据
const selectResArr = checkedValues?.map(val => transferObj[val]) setSelectKey(checkedValues)
setRightSelectData(selectResArr)
setOriginRightSelectData(selectResArr)
}


  • 我们两个事件都处理好 那么开始进行联动

  • 意思就是 我们拿什么去控制这两个机关 核心是不是就是 选中的选项啊

  • 有两种解法,第二种可能有点绕



    • 一、就是在下面每个选项改变的时候 都去调一下 上面一键全选事件;然后上面变化的时候 也去调一下 下面选项的事件 (常规)缺点:容易漏 一变多改





    • 二、监听选项key的变化 去触发开关 避免我们第一种缺点 (优解)同时解决了确保外面传进来的 表格列 使得下拉框选中这些项 生效




// 这里通过 useEffect() 实现监听 确保外面传进来的 表格列 使得下拉框选中这些项 生效 
useEffect(() => {
onCheckBoxChange(selectKey)
onCheckChange(selectKey)
// eslint-disable-next-line react-hooks/exhaustive-deps },
[selectKey]
)

结束


都看到这里了,不留点痕迹,是怕我发现么?

作者:造更多的轮子
来源:juejin.cn/post/7266463919139684367

收起阅读 »

论如何在Android中还原设计稿中的阴影

每当设计稿上注明需要添加阴影时,Android上总是显得比较棘手,因为Android的阴影实现方式与Web和iOS有所区别。 一般来说阴影通常格式是有: X: 在X轴的偏移度 Y: 在Y轴偏移度 Blur: 阴影的模糊半径 Color: 阴影的颜色 何为阴影 ...
继续阅读 »

每当设计稿上注明需要添加阴影时,Android上总是显得比较棘手,因为Android的阴影实现方式与Web和iOS有所区别。


一般来说阴影通常格式是有:


X: 在X轴的偏移度


Y: 在Y轴偏移度


Blur: 阴影的模糊半径


Color: 阴影的颜色


何为阴影


但是在Android中却比较单一,只有一个度量单位:Elevation,作为在Android5.0的material2引入的概念,用一个图来形象的描绘一下,其实本质上就是虚拟的Z轴坐标。


image.png


那好,高度差有了,还差个光源,这样才能形成阴影,在material2中,光源不是单一的位于屏幕正上方的,而且有两组光源,分为主光源(Key light)和环境光源(Ambient light)如下图所示:
image.png


最终形成的效果是一种复合光源下更自然的阴影。


image.png


其中环境光源,在屏幕空间中没有实际的位置,但是主光源是有实际的位置的,具体的参数见:


frameworks/base/core/res/res/values/dimens.xml - Android Code Search
image.png


好,既然知道了阴影本身的机制,那下一步现在则是如何自定义控制阴影,这也是本文的目的。


从SDK 21开始,提供了Elevation可以实现类似于阴影的模糊半径的效果,但是毕竟尺度过于单一,往往有时候无法满足所需的效果,所以,还需要控制阴影的颜色。


在SDK 28之后,可以通过outlineSpotShadowColoroutlineAmbientShadowColor来分别设置Key light和Ambient light投射的阴影颜色,但是说实话,这两个属性基本用不到或者说比较鸡肋。


不过这里引入了一个概念:Outline。


四种常见方案


Elevation + Outline


Outline其实是View的边框(轮廓),通过OutlineProvider可以自定义一个View的Outline从而影响View本身在elevation下的投影,比如定义以实现一个圆角ImageView为例:


<ImageView
android:id="@+id/image"
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@color/material_dynamic_primary90" />


image.clipToOutline = true
image.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View?, outline: Outline?) {
view ?: return
outline?.setRoundRect(0, 0, view.width, view.height, 32f)
}

}

效果基本没啥问题:
image.png


同样的,既然View的轮廓变化了,阴影自然也会跟着随之变化,所以outline也可以改变阴影:


image.elevation = 32f
image.outlineAmbientShadowColor = Color.RED
image.outlineSpotShadowColor = Color.BLUE
image.clipToOutline = true
image.outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View?, outline: Outline?) {
view ?: return
outline?.setRoundRect(0, 0, view.width, view.height, 32f)
}

}

效果如下:(不过outlineAmbientShadowColoroutlineSpotShadowColor仅支持SDK 28及以上)


image.png


通常,到这一步通过调整elevation的数值和outline以及高版本可用的shadowColor大体上可以满足设计师的阴影需求。
而且通常来说shadowColor都是Color.Black以及alpha的区别,所以你也可以这样:


outlineProvider = object : ViewOutlineProvider() {
override fun getOutline(view: View?, outline: Outline?) {
view ?: return
outline?.alpha = 0.5f
outline?.setRoundRect(0, 0, view.width, view.height, 32f)
}
}

但是,还记着前面提到的两个光源吗?其中有一个光源是位于屏幕斜上方的,这就带来了另外一个问题,同一个View设置相同的Elevation在不同的Y轴坐标它的阴影效果是不一样的,如下图所示:



总之,阴影的Blur和Color参数勉强是可以得到满足的。


优点:原生的阴影效果


缺点:设置阴影的颜色需要SDK>=28,需要配合使用outline来实现对阴影的轮廓控制


下面我们先来引申一下Android中了解过的阴影实现方式。


LayerDrawable


我相信大家肯定见过这种实现方式,通过绘制一层层渐变色来模拟阴影,其实官方也有通过该方式实现的阴影:MaterialShapeDrawable,示例如下:


val drawable = MaterialShapeDrawable(
ShapeAppearanceModel.builder()
.setAllCornerSizes(16.dp)
.build()
)
drawable.fillColor = ColorStateList.valueOf(getColor(com.google.android.material.R.color.material_dynamic_primary90))
drawable.setShadowColor(Color.RED)
drawable.shadowVerticalOffset = 8.dp.toInt()
drawable.elevation = 32f
drawable.shadowCompatibilityMode = MaterialShapeDrawable.SHADOW_COMPAT_MODE_ALWAYS
image.background = drawable

效果如图:
image.png


只能说很一般,毕竟是模拟的阴影模糊效果,而且目前只支持Y轴的offset。


优点:几乎是开箱即用的Drawable且自带圆角


缺点:模拟的阴影效果,展示效果不够精细且效率不高


NinePatchDrawable


说实话想在Android上实现一个简单的阴影太折腾了,什么奇怪的技巧都来了,比如.9图,至于什么是.9图这里便不再过多介绍。
通过这个网站:Android Shadow Generator (inloop.github.io)


image.png
你可以直接生成一个CSS Style的阴影效果,几乎可以完美还原Figma的阴影效果,效果如下:
image.png


其实还是很还原的,但是它有一个致命的缺点,就是圆角,因为是一张图片,所以圆角的单位本质上是px而非Android上的dp,如果你需要一个带圆角弧度的阴影是达不到预期的。


优点:参数完全可控的阴影,可以做到1:1还原设计稿


缺点:因为是图片,所以阴影的圆角无法跟随像素密度缩放(非常致命的缺点)


Paint.setShadowLayer/BlurMaskFilter


这两个我之所以放在一起本质上是因为实现起来都是类似的,
如:


paint.setShadowLayer(radius, offsetX, offsetY, shadowColor)
// 或者使用maskFilter然后通过paint.color以及绘制的区域进行offset来变相控制阴影的Color及offset
paint.maskFilter = BlurMaskFilter(field, BlurMaskFilter.Blur.NORMAL)

相比之下更推荐使用setShadowLayer,最终效果如下,基本上没啥问题:
image.png


但是值得注意的是,其绘制的阴影本质上等价于BlurMaskFilter,是占位的,而且是需要留出空间来展示的,所以必要时需要对父布局设置android:clipChildren="false"或者预留出足够的空间。


优点:


1. 参数完全可控的阴影,可以做到1:1还原设计稿


2. 参数的自定义程度及可控性强


缺点:


1. 阴影占位,需要通过clipChildren=false来或者预留空间规避


2. 需要自定义View或者Drawable,写起来较为麻烦。


总的来说,上面介绍了4种可能常见的阴影实现方式,其中按我的经验来说,较为推荐采用Outline或者setShadowLayer的方式来实现,如果可以的话原生Elevation配合Outline基本可以满足大部分需求场景。


当然还有部分实现方式比如用RenderScriptBlur等等,我没提是因为是前几种方式较为复杂,性价比不高。


Paint.setShadowLayer 扩展内容


下面则重点讲一下Paint.setShadowLayer/BlurMaskFilter这种方式,为什么说这两种方式实现的阴影都是一致的呢?这个就需要深入到C++层。
首先直接跳到paint.setShadowLayer的native实现类:
frameworks/base/libs/hwui/jni/Paint.cpp


Paint.cpp - Android Code Search


    static void setShadowLayer(CRITICAL_JNI_PARAMS_COMMA jlong paintHandle, jfloat radius,
jfloat dx, jfloat dy, jlong colorSpaceHandle,
jlong colorLong)
{
SkColor4f color = GraphicsJNI::convertColorLong(colorLong);
sk_sp<SkColorSpace> cs = GraphicsJNI::getNativeColorSpace(colorSpaceHandle);

Paint* paint = reinterpret_cast<Paint*>(paintHandle);
if (radius <= 0) {
paint->setLooper(nullptr);
}
else {
SkScalar sigma = android::uirenderer::Blur::convertRadiusToSigma(radius);
paint->setLooper(BlurDrawLooper::Make(color, cs.get(), sigma, {dx, dy}));
}
}


里面将我们传入的阴影radius参数转为Sigma并创建了BlurDrawLooper,我们来看看其实现


#include "BlurDrawLooper.h"
#include <SkMaskFilter.h>

namespace android {

BlurDrawLooper::BlurDrawLooper(SkColor4f color, float blurSigma, SkPoint offset)
: mColor(color), mBlurSigma(blurSigma), mOffset(offset) {}

BlurDrawLooper::~BlurDrawLooper() = default;

SkPoint BlurDrawLooper::apply(Paint* paint) const {
paint->setColor(mColor);
if (mBlurSigma > 0) {
paint->setMaskFilter(SkMaskFilter::MakeBlur(kNormal_SkBlurStyle, mBlurSigma, true));
}
return mOffset;
}

sk_sp<BlurDrawLooper> BlurDrawLooper::Make(SkColor4f color, SkColorSpace* cs, float blurSigma,
SkPoint offset)
{
if (cs) {
SkPaint tmp;
tmp.setColor(color, cs); // converts color to sRGB
color = tmp.getColor4f();
}
return sk_sp<BlurDrawLooper>(new BlurDrawLooper(color, blurSigma, offset));
}

} // namespace android

内容不多,可以看到本质上还是利用了setMaskFilter来实现的。


然后还剩下一个点就是通过SkMaskFilter::MakeBlur生成的模糊是占位的,如果能知道模糊具体需要多大的空间,就可以方便的进行预留以免实际展示时阴影被裁剪。
MakeBlur最终返回的是一个SkBlurMaskFilterImpl对象,我们可以先看一下其父类SkMaskFilterBase的虚函数:重点关注computeFastBounds函数


SkMaskFilterBase.h - Android Code Search


    /**
* The fast bounds function is used to enable the paint to be culled early
* in the drawing pipeline. This function accepts the current bounds of the
* paint as its src param and the filter adjust those bounds using its
* current mask and returns the result using the dest param. Callers are
* allowed to provide the same struct for both src and dest so each
* implementation must accommodate that behavior.
*
* The default impl calls filterMask with the src mask having no image,
* but subclasses may override this if they can compute the rect faster.
*/

virtual void computeFastBounds(const SkRect& src, SkRect* dest) const;

可以看到该函数的作用便是计算MaskFiter的bounds,看一下子类的SkBlurMaskFilterImpl的实现


void SkBlurMaskFilterImpl::computeFastBounds(const SkRect& src,
SkRect* dst)
const
{
// TODO: if we're doing kInner blur, should we return a different outset?
// i.e. pad == 0 ?

SkScalar pad = 3.0f * fSigma;

dst->setLTRB(src.fLeft - pad, src.fTop - pad,
src.fRight + pad, src.fBottom + pad);
}

其中fSigme便是最开始通过convertRadiusToSigma(radius)获取到的返回值,其计算方式如下:
SkBlurMask.cpp - Android Code Search


// This constant approximates the scaling done in the software path's
// "high quality" mode, in SkBlurMask::Blur() (1 / sqrt(3)).
// IMHO, it actually should be 1: we blur "less" than we should do
// according to the CSS and canvas specs, simply because Safari does the same.
// Firefox used to do the same too, until 4.0 where they fixed it. So at some
// point we should probably get rid of these scaling constants and rebaseline
// all the blur tests.
static const SkScalar kBLUR_SIGMA_SCALE = 0.57735f;

SkScalar SkBlurMask::ConvertRadiusToSigma(SkScalar radius) {
return radius > 0 ? kBLUR_SIGMA_SCALE * radius + 0.5f : 0.0f;
}

这样,我们可以得到一个模糊的近似Bound,虽然不是一个准确的值但是至少可以保证绘制的阴影不会被裁剪。
当然,如果无法预留Padding也可以通过clipChildren=false来实现。


总结


最后我也是针对setShadowLayer提供了一个自定义View的实现方式:


Lowae/Shadows: A simple and customizable library on Android to implement CSS style shadows (github.com)


感兴趣的可以尝试使用,有任何兼容性问题欢迎提issue~



(我十分清楚会有很多兼容性问题,没办法,这种Api就是这样,不,准确来说,Android就是这样)



所以,想在Android上1:1还原设计稿上的阴影是比较困难的,但是如果不去追求参数的还原只是寻求视觉的略显一致,那还是可以做到的,简单点的通过第一种方式(Elevation + Outline),如果设置到阴影颜色或者offset这种便可以尝试最后一种方式(

作者:Lowae
来源:juejin.cn/post/7270503053358874664
setShadowLayer)。

收起阅读 »

卸下if-else 侠的皮衣!- 适配器模式

web
🤭当我是if-else侠的时候 😶怕出错 给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅 😑难调试 我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且...
继续阅读 »

🤭当我是if-else侠的时候


😶怕出错


给我一个功能,我总是要写很多if-else,虽然能跑,但是维护起来确实很难受,每次都要在一个方法里面增加逻辑,生怕搞错,要是涉及到支付功能,分分钟炸锅


😑难调试


我总是不知道之前写的逻辑在哪里,一个方法几百行逻辑,而且是不同功能点冗余在一起!这可能让我牺牲大量时间在这查找调试中


🤨交接容易挨打


当你交接给新同事的时候,这个要做好新同事的白眼和嘲讽,这代码简直是坨翔!这代码简直是个易碎的玻璃,一碰就碎!这代码简直是个世界十大奇迹!


🤔脱下if-else侠的皮衣


先学习下开发的设计原则


单一职责原则(SRP)



就一个类而言,应该仅有一个引起他变化的原因



开放封闭原则(ASD)



类、模块、函数等等应该是可以扩展的,但是不可以修改的



里氏替换原则(LSP)



所有引用基类的地方必须透明地使用其子类的对象



依赖倒置原则(DIP)



高层模块不应该依赖底层模块



迪米特原则(LOD)



一个软件实体应当尽可能的少与其他实体发生相互作用



接口隔离原则(ISP)



一个类对另一个类的依赖应该建立在最小的接口上



在学习下设计模式


大致可以分三大类:创建型结构型行为型

创建型:工厂模式 ,单例模式,原型模式

结构型:装饰器模式,适配器模式,代理模式

行为型:策略模式,状态模式,观察者模式


上篇文章学习了 策略模式,有兴趣可以过去看看,下面我们来学习适配器模式


场景:将一个接口返回的数据,转化成列表格式,单选框数据格式,多选框数据格式


用if-else来写,如下


let list = []
let selectArr = []
let checkedArr = []

http().then(res =>{
//处理成列表格式
this.list = this.handel(res,0)
//处理成下拉框模式
this.selectArr = this.handel(res,1)
//处理成多选框模式
this.checkedArr = this.handel(res,2)
})
handel(data,num){
if(num == 0){
....
}else if(num ==1){
....
}else if(num ==2){
....
}
}

分下下问题,假如我们多了几种格式,我们又要在这个方法体里面扩展,违反了开放封闭原则(ASD),所以我们现在来采用适配器模式来写下:


//定义一个Adapter
class Adpater {
data = [];
constructor(){
this.data = data
}
toList(col){
//col代表要的字段,毕竟整个data的字段不是我们都想要的
//转列表
return this.data.map(item =>{
const obj = {}
for(let e of col){
const f = e.f
obj[f] = item[f];
}
return obj
})
}
//用了map的省略写法,来处理转化单选的格式
toSelect(opt){
const {label,value} = opt
return this.data.map(item => ({
label,
value
}))
}
//同上处理转化多选的格式
toChecked(opt){
const {field} = opt
return this.data.map(item => ({
checked:false,
value:item[field]
}))
}
}

//下面是调用这个适配类
let list = []
let selectArr = []
let checkedArr = []
http.then(data =>{
const adapter = new Adatpter(data)
//处理列表
list = adapter.toList(['id','name','age'])
//处理下拉
selectArr = adapter.toSelect({
label:'name'
value:'id'
})
//处理多选
checkedArr = adapter.toChecked({
field:'id'
})
})

这个扩展性就能大大提高,看着也会好看很多,可以通过继承等等方式来扩展,继承方式下次有机会再来写代码,文章先到这里!


结尾


遵守设计规则,脱掉if-else的皮衣,善用设计模式,加

作者:向乾看
来源:juejin.cn/post/7265694012962537513
油,骚年们!给我点点赞,关注下!

收起阅读 »

当文字成为雨滴:HTML、CSS、JS创作炫酷的"文字雨"动画!

web
简介 大家好,今天要给大家带来一个Super Cool的玩意儿😎! 在本篇技术文章中,将介绍如何使用HTML、CSS和JavaScript创建一个独特而引人注目的"文字(字母&数字)"雨🌨️动画效果。通过该动画,展现出的是一系列随机字符将从云朵中下落...
继续阅读 »

简介


大家好,今天要给大家带来一个Super Cool的玩意儿😎!


rain-preview.gif


在本篇技术文章中,将介绍如何使用HTML、CSS和JavaScript创建一个独特而引人注目的"文字(字母&数字)"雨🌨️动画效果。通过该动画,展现出的是一系列随机字符将从云朵中下落像是将文字变成雨滴从天而降,营造出与众不同的视觉效果;


HTML


创建一个基本的HTML结构,这段HTML代码定义了一个容器,其中包含了"云朵"和"雨滴"(即文字元素)。基本结构如下:



  • 首先是类名为container的容器,表示整个动画的容器;

  • 其次是类名为cloud的容器,表示云朵的容器;

  • 接着是cloud容器中的文字元素,表示雨滴(即文字元素);
    然后引入外部创建的css和js文件,可以先定义几个text容器,用于调整样式;


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Text Rain Animation</title>

<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<div class="container">
<div class="cloud">
<!-- <div class="text">a</div> -->
<!-- <div class="text">b</div> -->
<!-- <div class="text">c</div> -->
<!-- 雨滴将会在这里出现 -->
</div>
</div>

<script src="./js/main.js"></script>
</body>
</html>

CSS


CSS是为文字雨效果增色添彩的关键,使动画效果更加丰富,关于一些 CSS 样式:



  • 使用了自定义的颜色变量来为背景色和文本颜色提供值,有助于使代码易于维护和修改;

  • 利用CSS的阴影效果和动画功能,创造逼真的"云朵"和流畅的"雨滴"动画;


* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

:root {
--body-color: #181c1f;
--primary-color: #ffffff;
}

body {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background-color: var(--body-color);
}

.container {
width: 100%;
height: 400px;
display: flex;
justify-content: center;
border-bottom: 1px solid rgba(255, 255, 255, .1);
/* 添加一个从下往上线性渐变的镜像效果,增加视觉层次感 */
-webkit-box-reflect: below 1px linear-gradient(transparent, transparent, transparent, transparent, #0005);
}

.cloud {
position: relative;
top: 50px;
z-index: 100;

/* 横向云朵 */
width: 320px;
height: 100px;
background-color: var(--primary-color);
border-radius: 100px;

/* drop-shadow函数将阴影效果应用于投影图像 */
filter: drop-shadow(0 0 30px var(--primary-color));
}
.cloud::before {
content: "";
/* 左侧小云朵 */
width: 110px;
height: 110px;
background-color: var(--primary-color);
border-radius: 50%;
position: absolute;
top: -50px;
left: 40px;

/* 右侧大云朵 */
box-shadow: 90px 0 0 30px var(--primary-color);
}

.cloud .text {
position: absolute;
top: 40px;
height: 20px;
line-height: 20px;

text-transform: uppercase;
color: var(--primary-color);
/* 为文字添加阴影,看上去发光,增加视觉效果 */
text-shadow: 0 0 5px var(--primary-color), 0 0 15px var(--primary-color), 0 0 30px var(--primary-color);
transform-origin: bottom;
animation: animate 2s linear forwards;
}

@keyframes animate {
0% {
transform: translateX(0);
}

70% {
transform: translateY(290px);
}

100% {
transform: translateY(290px);
}
}

通过关键帧动画 @keyframes animate 定义文字运动的过程,在这里是垂直移动290px,也就是向下移动,模拟下雨的状态。当然为了让文字雨效果更加好看,还可以引入一下字体库;



Warning


-webkit-box-reflect:可将元素内容在特定方向上进行轴对称反射;


但是该特性是非标准的,请尽量不要在生产环境中使用它!


目前只有webkit内核的浏览器支持,如:谷歌浏览器、Safari浏览器。在火狐浏览器中是不支持的;



JavaScript


最后,使用JavaScript来实现文字雨的效果。通过动态生成并随机选择字符,可以实现让这些字符(雨滴)从.cloud(云朵)中降落的效果。JavaScript 脚本逻辑:



  • 首先,定义函数 generateText() 并创建字符集,定义函数 randomText() 通过从给定的字符集中随机选择一个字符返回;

  • 接下来,编写 rain() 函数,在函数内部,首先选取 .cloud 元素同时创建一个新的 <div>元素作为字符节点,设置元素文本内容为函数返回的字符,并添加类名;

  • 然后,利用 Math.random() 方法生成一些随机值,将这些随机值应用到创建的 <div> 元素上,包括:

    • 字符距离左侧位置,在 .cloud 容器的宽度区间;

    • 字体大小,最大不超过32px;

    • 动画周期所需的时间(动画持续时间),2s内;



  • 最后,将其<div>添加到 .cloud 元素中,使用 setTimeout() 函数在2秒后将文字节点从 .cloud 元素中移除,模拟雨滴落地消失效果;


定时器: 为了让字符(雨滴)持续下落,使用 setInterval 函数和一个时间间隔值来调用 rain() 函数。这样就是每20毫秒就会生成一个新的字符(雨滴)节点并添加到云朵中。


// 生成字母和数字数组
function generateText() {
const letters = [];
const numbers = [];

const a = "a".charCodeAt(0);

for (let i = 0; i < 26; i++) {
letters.push(String.fromCharCode(a + i));

if (i < 9) {
numbers.push(i + 1);
}
};

return [...letters, ...numbers];
};

// 从生成的数组中随机取出一个字符
function randomText() {
const texts = generateText();
const text = texts[Math.floor(Math.random() * texts.length)];

return text;
};

function rainEffect() {
const cloudEle = document.querySelector(".cloud");
const textEle = document.createElement("div");

textEle.innerText = randomText();
textEle.classList.add("text");

const left = Math.floor(Math.random() * 310);
const size = Math.random() * 1.5;
const duration = Math.random();
const styleSheets = {
left: `${left}px`,
fontSize: `${0.5 + size}em`,
animationDuration: `${1 + duration}s`,
};
Object.assign(textEle.style, styleSheets);

cloudEle.appendChild(textEle);
setTimeout(() => {
cloudEle.removeChild(textEle);
}, 2000);
};

// 每隔20ms创建一个雨滴元素
setInterval(() => rainEffect(), 20);

结论


通过HTML、CSS和JS的紧密合作,成功创建了一个炫酷的"文字雨"动画效果,这个动画可以增加网页的吸引力! 不要犹豫🖐️,动手尝试一下,或者甚至你也可以根据自己的需求对文字、样式和动画参数进行调整,进一步改善和扩展这个效果;


希望这篇文章对你在开发类似动画效果时有所帮助!另外如果你对这个案例还有任何问题,欢迎在评论区留

作者:掘一
来源:juejin.cn/post/7270648629378367528
言或联系(私信)我。谢谢阅读!🎉

收起阅读 »

大厂产品为何集体下架

快手一个 30 多人的产品团队每月开销上百万,其中技术、产品和运营是开支大头。如果碰上产品商业化能力不行,那这就是铁亏。 昨天看了Tech星球发表的一篇文章,说的是各互联网大厂集中下架 60 多款 App 的事儿,有些感想。 不可否认,互联网已经过了最激进的时...
继续阅读 »

快手一个 30 多人的产品团队每月开销上百万,其中技术、产品和运营是开支大头。如果碰上产品商业化能力不行,那这就是铁亏。


昨天看了Tech星球发表的一篇文章,说的是各互联网大厂集中下架 60 多款 App 的事儿,有些感想。


不可否认,互联网已经过了最激进的时期,尤其是过去十年以移动互联网为代表的这一轮技术周期。


接下来,几乎所有的互联网企业都会关注成本和效益。


说白了,要赚稳当的钱并好好活下去。


而那些烧钱的产品、业务、部门,则大概率都会关停并转。


光是今年,腾讯就已经下架了包括游戏在内的 40 多款 App,其中不乏 QQ 堂这样的元老级产品。


除此之外,其他大厂也都在进行同样的操作,产品下架意味着团队解散,对应就会出现人员缩减。


我知道,很多人看到这样的信息一定会产生焦虑,认为互联网是不是不行了?找工作是不是更难了?会不会面临裁员失业了?


这里,我想说几个内在的逻辑关系。


先说很多人关心的裁员和失业问题,以及应对策略。


对于企业来说,他们会优先从自身立场出发进行决策,在面对发展危机时,开源节流、壮士断腕、集中火力就会成为优先考虑。


这种影响首先会辐射到边缘业务和产品以及探索型项目上,直接关停就是解决方案,以此释放出来的资源可以投入到主营业务上,而多余的资源则对外释放。


这里说的对外释放,其实也就是裁员。



被裁员的对象通常符合这么几个特征,一线螺丝钉、高成本中层、多余的高层。


因此,要想避免自己陷入这样的困境,至少有这么几件事可以做。


第一,选择公司和业务时尽量避开边缘部门和探索创新性业务,比如去主营电商业务的公司做社交产品。


可能有人会说了,没办法啊,有时候选择权不在自己,能找到工作就不错了,哪还能挑三拣四。


不过我想说,这种破罐子破摔的思想一定会害人不浅。


没选择的前提一定是没本事,而没本事的原因就是学习和实践能力不行。


不管是公司还是社会,都是典型金字塔结构,越往上,能容纳的空间就越小。


当别人奋力往上爬的时候,你还在原地踏步,更好的工作机会自然就不会属于你,这是现实。


所以,别再抱怨工作不好找,这是从外界找原因。先看看自己有什么,从内在找问题。


浑浑噩噩过下去是一种选择,精明筹划过下去也是一种选择。选择不同,结果不同。


第二,尽早建立自己的职业标签,尽快找到自己的关键位置,尽力打造自己的不可替代性。


职业标签、关键位置、不可替代性,这三点是一个成熟专业且有独特价值职场人的标配。


职业标签是专业能力的体现,SaaS 产品专家、电商供应链产品专家、数据产品专家、搜索和策略产品专家等,这都是职业标签。


什么都会,什么都不精,是不可能形成职业标签的,也就无法获得第二个优势,关键位置。


关键位置是你所在部门和公司的岗位,不一定是领导,但一定是一个不可或缺的角色。


比如,负责搭建公司后台业务系统的产品经理,对整体结构和细节是最了解的那个人。


获得了关键位置的前提是职业标签,而获得关键位置后就能进而获得第三个优势,不可替代性。


遇上公司裁员,可能会裁掉一线大头兵,但不会裁掉处于关键位置的人。铁打的营盘,流水的兵,只要核心还在,就可以继续生长。


职业标签、关键位置、不可替代性,评估一下自己有没有,缺不缺。


问题找到了,才有形成解决方案的可能性。


第三,不断为自己打造优质的信息和人脉渠道,你可以不喜欢圈子,但你不得不混圈子。


任何一个领域都会有对应的圈子,那里聚集了人、信息、机会,越是专业的圈子,这三方面的质量就越高。


应届生找工作靠校招,工作几年后找工作靠社招,工作多年后找工作靠朋友,理解了这一点,你就知道信息和人脉渠道有多重要了。


不知道你们有没有一种感觉,这几年世界变化很快,这种变化带来了太多的不确定性,让原本的稳定得以打破。


有的人认为这是危机,还有的人认为这是机会。要我看,越是不确定性增强的危机时刻,可探索的机会其实就更多。


原因很简单,稳定格局的建立意味着机会窗口的关闭,而稳定被打破,说明机会窗口的重新打开。


我对未来还是持有乐观态度的,即便寒气逼人,我们依旧可以拥有一颗火热向前的内心。



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

如果你的退路有1亿人在卷了,阁下又该如何应对?

你所认为的退路,其实已经有1亿人在卷了,"那么阁下将如何应对?" 引子 最近进了一些技术交流群,群友们也是来自五湖四海,有些碰巧还在找工作,自嘲 「“大不了去送外卖”、“大不了去开滴滴”」 这句话在工作中有时候大家也经常会有这样的调侃。 其实这几年互联网头部公...
继续阅读 »

你所认为的退路,其实已经有1亿人在卷了,"那么阁下将如何应对?"


引子


最近进了一些技术交流群,群友们也是来自五湖四海,有些碰巧还在找工作,自嘲 「“大不了去送外卖”、“大不了去开滴滴”」 这句话在工作中有时候大家也经常会有这样的调侃。


其实这几年互联网头部公司都比较难做,在21年 2记重拳打向这个行业:打击打击资本无序扩张与规范平台经济发展、反垄断。阿里和美团前年也都收到了182亿和32亿的天价罚单,这不最近蚂蚁不是又被罚了71亿,降本增效、开源节流成为各大公司的主旋律,毕竟_"地主家也没有余粮啦"_,再加上坊间传闻35岁的潜规则,大家的危机感还是挺强的。


送快递、送外卖、跑滴滴,这些职业因为没有什么门槛,也没有什么年龄限制,只要你愿意吃苦,就能稳定赚到钱,可是事实上,这些真的是不错的退路吗?真相可能跟你想象的完全不一样。




卷起来


在今年3月份时候,中华总工会发布了一个统计:目前全国职工总数4.02亿人,而从 「事外卖、快递、网约车等新就业形态的群体,已达8400万人」





「也就是说:你以为的退路,实际上已经有一亿人在卷了」



网约车


记得在五月份的时候流传过一个 段子“报名滴滴网约车由于饱和被劝退到美团外卖 ”,网约车行业被视为一个门槛较低的行业,只要会开车,身体健康没有犯罪记录,就可以轻松成为网约车司机,无论专职还是兼职,都是一个不错的选择。所以,最近几年,网约车成为我国一个相当重要的灵活就业市场,也在很大程度上减轻了我国的就业压力。「司机们纷纷涌入网约车行业,但是网约车的订单数量却在持续萎缩。」



数据显示,2022年,我国网约车用户规模为4.37亿人,同比减少了3%,网约车市场规模约为3100多亿元,同比下降1.4%,



今年5月,中国多个城市,发布了 「网约车饱和预警,劝大家谨慎入行」「长沙、三亚等城市,已暂停受理网约车运输证新增业务」。 考虑到同期司机数量的激增,那么势必会造成 网约车司机能够接到的订单迅速减少。很多城市的网约车司机日均接单量不到10单,和前几年相比大概下降了一半。「过去很多网约车司机可以月收入轻松过万元,而现在想要这个目标难度加了不止一点点。」


外卖



 今年3月份广州出现有史以来第一次外卖骑手“爆满”!想当外卖骑手还得托关系。外卖已经成为知识密集型行业,广州外卖骑手本科率接近30%,毕竟全国人口的本科率也只有4.4%左右。情况变化之快令人咂舌,前几年还有篇文章 “被困在系统里面出不去的美团骑手”,今年却出现了 “想要到系统里面却进不去”。 美团财报显示,2021年美团骑手的数量为527万,2022年,美团骑手的数量变成了624万,2022年,美团骑手的数量变成了624万,其中81.6%是来自县域乡村地区的农村转移劳动力,平台一年新增骑手的人数达到了97万 


 而行业进入饱和期之后,不可避免的就是降薪了。这对于本不富裕的外卖小哥来说无疑是雪上加霜 


做好当下


其实呢 也不需要气馁或者过于焦虑,了解一些事实,有助于我们更好的把握好当下。看清楚事实,坦诚的面对自己,“知人者智,自知者明”。面对当前互联网的大环境,可以主动思考一下自己的“护城河”是什么,高端的学历?大厂的履历?扎实的技术?殷实的家境?甚至是人脉广情商高。如果暂时想不起的来话,还是得行动起来去有意识的构建属于自己的“护城河”。每个人都是独一无二的,至少我们可以从认真做好当下的本职工作做起,最后跟大家共享一句我比较喜欢的一句话



yesterday is history,tomorrow is a mystery,but today is a gift



做好当下,尽人事听天命~

作者:Android茶话会

来源:juejin.cn/post/7270871863160700984

收起阅读 »

人情世故职场社会生存实战篇(二)

这个专栏文章都是群友问的实际问题,我在这里分享出来,希望可以帮助到大家,本篇是本专栏的第二篇,欢迎大家踊跃发言 人情人情世故职场社会生存实战篇(一)人情人情世故职场社会生存实战篇(二)11、问:如何改善和领导的关系? 答:1、工作上有能力。现在社会变化快,三、...
继续阅读 »

这个专栏文章都是群友问的实际问题,我在这里分享出来,希望可以帮助到大家,本篇是本专栏的第二篇,欢迎大家踊跃发言


人情人情世故职场社会生存实战篇(一)

人情人情世故职场社会生存实战篇(二)

11、问:如何改善和领导的关系?


答:1、工作上有能力。现在社会变化快,三、五年组织环境,内部的领导可能都会有大的变化,自己不能无所作为的等着,要努力提升自己的工作能力,业余时间学习工作的各个环节的东西,毕竟职场还是要有人干活的,领导再差劲也不能只用小人,还需要具体的做事的人,有真本事不怕。


2、权力上能维护。有工作能力的人很多,但是越是有工作能力的人,越是要处处维护领导的权力,早请示晚汇报,让他觉得一切都在掌控之中,有功归上。这样的工作才能得到领导的认可。如果没有权力意识,工作能力越强,就让领导越是感到威胁,很多有能力的人,对领导毫不尊重,自然备受打击。


3、生活上凑交集。现在人分的很清楚,工作是工作,生活是生活,下班了也就没有什么交集了,工作关系如果未能产生私交,两个人的关系不会稳定,私交是关系的粘合剂。所以一定要有私下的交集,这个交集要偶然,也就是要凑机会,多了领导烦。把握好度,比如孩子在一个培训班,妻子在一个瑜伽馆。


12、问:我是刚毕业两年的研究生,在国企工作。我得知一个消息,我有一个老乡任命为我们单位的财务副总,下个月就会到岗,我想请他提携我一下。我该怎么做呢?


答:首先打铁必须自身硬,他下个月到岗,我建议你不要一来就仗着自己是他老乡就张口请人帮你,没有这样的道理。你可以仔细的写一下你了解的单位的现状、存在的问题,以及你预想的以后未来发展的方向,形成一份书面材料。等他到岗后,你借向他汇报工作的时候交给他,让他看到你不是废铁一块,这样子人家自然更乐意帮你。其次,职场中关系也非常重要的,有关系就有一切,你呀,先组个局,请老乡吃个饭,祝贺一下,把原来不温不火的关系回温一下,关系到位了,你不用求,别人自然会帮。


13、问:老师我实习快结束了,需要转正,想给领导送礼,请问怎么送比较好?


答:很多人给领导送礼,送了一条烟,马上说:我转正的事儿,麻烦您了。你转正的事儿,又不是他一个人说了算,所以你的东西,他收了,反而睡不着,甚至会梦到自己进去了。你一直送,领导说,转正的事儿,你争取下。你说:争取个啥啊,在哪儿不是做贡献啊。领导说,你先走个程序嘛,你勉为其难说:哎,那就走个吧。失败了,咱说:我说我不行吧,你非要让我上,我啥水平我不知道吗,谢谢您高看我。领导说:别急,下次再试试,你说不试了,我对我目前的待遇非常满意。


你没野心,他反而帮你了,也就是你不能让领导有压力。有人说了,马勒戈壁,我都送礼了,为什么还忍着,我只能说对不起兄弟,你别忍了,你去爆炸吧。领导让你试试,你成功了,你要说:哎呦,又是你帮我使劲了。一句话,去送就好了,习惯性送有价值的东西 ,如果领导拒绝你,你就说,这个是自己的一点小心意,在这实习期期间,你在公司学习到了很多,也跟领导学习了很多,一直感恩就好了。


14、问:因为我工作失误,害的我部门领导被大领导骂,还要帮我各方协调挽回损失。我想买点礼物感谢一下我部门领导,但是从来没有过给领导送东西的经验,请问我什么时候送合适啊 ?是现在事情刚过就送 ?还是过年过节有合适机会了送啊。


答:你傻呀!肯定是现在马上去办了,此时过去,你名正言顺的感谢领导帮你挽回了工作上的失误,并表示,如果没有他,你可能就丢了这份工作,或者造成更大的损失,然后感激之情,无以言表,送上一点小心意(烟酒+红包),请领导一定要收下,不然自己寝食难安。人呐,都喜欢懂得感恩的人。


15、问:老师,您好!单位中层调整,之前托领导前辈已给老大打过招呼,礼数走过了,现事已到跟前,私下和老总见面,老大说竞争很激烈,走程序,请老师指点下一步怎么走?


答:老大的话,其实意思也比较明显了,“竞争很激烈”=打招呼的人很多,表心意的人很多,“走程序”=你送的不够,咱就走程序,你送的够,咱就插队,走后门,老大的意思,就是现在 你这边没啥竞争力,你看看要不要加码。


如何破局?你找前辈领导在出一次面,你把心意给前辈领导,让他帮你给老大。很多事儿,可以通过送梨搞定。愿送,永远都有机会。不愿送,只能走程序……在咱们这儿,领导说,该走的程序,咱必须走。这就意味着有戏。要是领导说,走正常程序吧,意味着这个事儿啊,100%搞不定。


16、问:老师请问找老大办事,给老大送礼,老大说送礼破坏感情,说你的心意我领了,我会找机会帮你办的,我该怎么办?


答:心意我领了,我会找机会帮你办的,这个就是典型套话,你要是当真了,你的事就会一直杳无音讯,没有下文。这个时候,你要说,领导,你帮我太多了,然后列举123件事,一直感恩就好了。你说,领导,没有你的关照,就没有我的今天,可能这些帮助,都是您随手为之的,但是,对我的帮助是巨大的,这个恩我一直记得,这点心意您要是不收下,我真的问心有愧,以后都不敢找你指点工作,指点人生了。领导这心意我就放着了,我还有事我先走了,我那个事不打紧,如果麻烦的话,领导您千万别为难。


17、问:老师我身边有个同事,嘴有点J,喜欢乱开玩笑,我不知道怎么怼他,每次虽然我没说啥,但心里是很不高兴的。比如我昨天穿了一件新衣服,他说你新衣服真的是不得了,像个新郎官,怎么着又想做一次新郎官,那样可不行。那是犯了生活作风问题,不过你可以偷偷摸摸的夜夜做新郎。


答:有时候你让他一小步,他就会前进一大步。由刚开始不怀好意的开玩笑,慢慢变成欺负你。对于这种毫无顾忌的玩笑,必须给予反击。你可以这样说:我是既没那贼心也没那贼胆,老婆管得严,不像哥哥你在哪里都没人敢管,你能夜夜做新郎,我只能做一回新郎。


18、问:单位风气不正,很多老员工总是不干活,同事也是甩活给我,我该怎么办?我是一个合同工,单位的关系错综复杂,一杆子打下去,他们都是关系户,很多老员工仗着资历老,都不干活,同事也是照猫画虎,喜欢把活甩给我,请问,我是要学会拒绝,还是继续接受?


答:一般来说,单位里面就几种人老是被针对。


1、新人,太嫩,总是希望通过表达自己的热情和善意,以期希望得到对方的帮助,其实没必要!你够强,你有价值,别人就会尊重你,你一来就暴露底牌,别人就知道怎么拿捏你了。


2、软人,性格太软,别人听你说话就知道你是一个可以欺负的人,你要学会提高自己的气场。


3、孤立的人,这种人不会抱团,狮子落单了,鬣狗都要去掏肛,更别提小绵羊了,狮子、野狗,狼群都会群殴它。


4、情商低,不会跟领导搭关系,没有领导撑腰你是合同工,你也可以很硬气,谁知道单位的大领导不会是你舅舅?你硬气一点,别人还会觉得你有关系,反倒是不敢欺负你,这里,你会拒绝就行了,会推,会找借口,很好办的。人性本贱,这是我在群内一直讲的,你好说话,别人就喜欢欺负你你不好说话,别人反而尊重你。


19、问:我有个领导超级傻叉,工作上很多事情的流程和解决办法,她都不清楚,遇到问题她也搞不明白,就比如这两天,其他部门的员工都知道的消息,就只有我们部门的员工不知道,问题是她也不知道,搞的我们乱成一团麻,然后还瞎喷瞎指挥。后面我自己去找其他部门问情况,才得知最新消息和工作流程!


我是非常不明白,为什么像这种蠢H一样的货色,都能当领导?能力那么差就不要出来嚯嚯别人了好吗!请问如何对待这种领导?是不是能力不行的领导都喜欢通过折腾别人,在一些细枝末节而没有意义的事情是精益求精,才能彰显自己的存在感?


答:没有能力,但是却做了你的领导,那么她必然有她优势的一面,比如关系!不然,她又如何到了这个位置?一将无能,累死三军,主将无能,其实就是一个很大的机会,篡位的宰相,为什么都喜欢皇帝昏庸无能?因为他只要控制了这个蠢货,他就控制了整个朝堂!一人之下,万人之上,甚至他就是无冕之王。


所以,这里你的答案也很简单,你只要会向上管理,想办法帮她消化所有的问题,让自己成为她离不开的人。每次遇到问题了,你就带着通盘的方案去跟她讨论该怎么办,流程如何,细节如何,无形之中把你的方案植入他的脑子里面,然后以后遇到事情,她跟任何人都搭不来,但是跟你特别有缘,你说是不是很有趣。这还只是初步的,你要知道她的背后一定有更强的关系,想办法融入进去,在职场中你一步一高升,不是更简单?问题就是机遇,就看你拥有多宽广的视野了!


20、问:老师您好,我认识一个退休的老领导(不在本地),他跟现在我们市局一把手关系好。最近我们要有一波提拔,我看中一个位置。我想通过这个老领导给我们一把手送礼,我该怎么办?我该怎么说让这个老领导引荐一下比较合适?如果他愿意帮忙的话,怎么给我们一把手送礼合适?


答:退休了,人走茶凉。他有没有能量,就一个判断标准∶他的儿女立起来了吗?要是他的儿女都没立起来。你想立,他也爱莫能助。人,只要退休了,说话跟放屁一样,无人接招。他跟你一把手关系好,这个关系好必须建立在有共同利益的基础上,没有这个基础,这个关系就是假的。再说,动用他的资源帮你,他的回报率难道就是烟酒茶?他缺这点儿东西?一句话, 你送礼没戏..想想你能为他提供什么其他价值......


欢迎同学们关注本专栏,会持续更新社会上的人情世故,有问题的同学也欢迎留言~


作者:公z号_纵横潜规则
来源:juejin.cn/post/7268050036474232851

收起阅读 »

继copilot之后,又一款免费帮你写代码的插件

写在前面 在之前的文章中推荐过一款你写注释,它就能帮你写代码的插件copilot。 copilot写代码的能力没得说,但是呢copilot试用没几天之后就收费了。 按理说这么好用,又可以提高效率的工具,收点费也理所当然 但是秉承白嫖一时爽,一直白嫖一直爽的原则...
继续阅读 »

写在前面


在之前的文章中推荐过一款你写注释,它就能帮你写代码的插件copilot。


copilot写代码的能力没得说,但是呢copilot试用没几天之后就收费了。


按理说这么好用,又可以提高效率的工具,收点费也理所当然


但是秉承白嫖一时爽,一直白嫖一直爽的原则(主要是我穷穷穷),又发现了一款可以平替的插件CodeGeex


一、CodeGeex简介


① 来自官方的介绍



CodeGeeX is a powerful intelligent programming assistant based on LLMs. It provides functions such as code generation/completion, comment generation, code translation, and AI-based chat, helping developers significantly improve their work efficiency. CodeGeeX supports multiple programming languages.



翻译过来大概是



CodeGeeX是一个功能强大的基于llm的智能编程助手。它提供了代码生成/完成、注释生成、代码翻译和基于ai的聊天等功能,帮助开发人员显著提高工作效率。CodeGeeX支持多种编程语言。



GitHub地址


github.com/THUDM/CodeG…


目前在GitHub上 2.6k star 最近更新是2周前




③ 下载量

  • vscode 目前已有129k下载量
  • idea 目前已有58.7k 下载量

二、插件安装


① vscode




②idea


注: idea低版本的搜不到这个插件,小编用的是2023.01 这个版本的




安装完成后,注册一个账号即可使用


三、帮你写代码

① 我们只需要输入注释回车,它就可以根据注释帮你写代码

② tab接受一行代码 ctrl+space 接受一个单词








四、帮你添加注释



有时候,我们拿到同事没有写注释的代码,或者翻看一周前自己写的代码时。


这写得啥,完全看不懂啊,这时候就可以依靠它来帮我们的代码添加注释了



操作方法:

① 选中需要添加注释的代码

② 鼠标右键选择Add Comment

③ 选择中文或者英文






这是没加注释的代码

public class test02 {
   public static void main(String[] args) {
       int count=0;
       for(int i=101;i<200;i+=2) {
           boolean flag=true;
           for(int j=2;j<=Math.sqrt(i);j++) {
               if(i%j==0) {
                   flag=false;
                   break;
              }
          }
           if(flag==true) {
               count++;
               System.out.println(i);
          }
      }
       System.out.println(count);
  }
}

这是CodeGeex帮加上的注释

public class test02 {
   //主方法,用于执行循环
   public static void main(String[] args) {
       //定义一个变量count,初始值为0
       int count=0;
       //循环,每次循环,计算101到200之间的值,并判断是否是因子
       for(int i=101;i<200;i+=2) {
           //定义一个变量flag,初始值为true
           boolean flag=true;
           //循环,每次循环,计算i的值,并判断是否是因子
           for(int j=2;j<=Math.sqrt(i);j++) {
               //如果i的值不是因子,则flag设置为false,并跳出循环
               if(i%j==0) {
                   flag=false;
                   break;
              }
          }
           //如果flag为true,则count加1,并打印出i的值
           if(flag==true) {
               count++;
               System.out.println(i);
          }
      }
       //打印出count的值
       System.out.println(count);
  }
}

基本上每一行都加上了注释,这还怕看不懂别人写的代码


五、帮你翻译成其他语言



除了上面功能外,CodeGeeX 还可以将一种语言的代码转换成其他语言的代码



操作方法:

① 选中需要转换的代码

② 鼠标右键选择Translation mode

③ 在弹出的侧边栏中选择需要转换成的语言,例如C++、 C#、Javascript 、java、Go、Python、C 等等

④ 选择转换按钮进行转换






六 小结


试用了一下,CodeGeeX 还是可以基本可以满足需求的,日常开发中提高效率是没得说了


作为我这样的穷逼,完全可以用来平替copilot,能白嫖一天是一天~


也不用当心哪天不能用了,等用不了了再找其他的呗




本期内容到此就结束了


希望对你有所帮助,我们下期再见~ (●'◡'●)


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

小米备案 mios.cn 网站域名

8月22日消息,近期有关小米科技备案新域名和自研操作系统的消息引起了广泛关注。根据最新披露的信息,小米科技有限责任公司于去年11月11日备案了mios.cn网站域名。然而,当前访问该域名时遇到了403错误,意味着资源暂时不可用,同时网站也尚未设置HTTPS安全...
继续阅读 »

8月22日消息,近期有关小米科技备案新域名和自研操作系统的消息引起了广泛关注。根据最新披露的信息,小米科技有限责任公司于去年11月11日备案了mios.cn网站域名。然而,当前访问该域名时遇到了403错误,意味着资源暂时不可用,同时网站也尚未设置HTTPS安全证书。



关于自研操作系统的消息更是引发了热议。有博主在今年8月15日爆料称,小米正在积极打磨一款自研操作系统,并已取得了阶段性进展。然而,这一爆料后来却被删除,导致外界对其真实性产生了疑问。值得注意的是,同一博主在今日上午发布了新消息,表示小米的自研系统将实现全端覆盖,并且兼容了AOSP(Android开放源代码项目)。尽管有人曾称MIUI 15将更名为mios,但博主在最新消息中否认了这一说法。

此外,一些人或许会对小米为何没有采用更通用的mios.com域名产生疑惑。事实上,据ITBEAR科技资讯了解,该域名已经被他人注册,目前被用于一个国外的物联网设备自动化网站。

尽管目前关于小米备案域名和自研操作系统的信息仍存一定的不确定性,然而这些信息的传播已经引发了业界和用户的广泛关注。未来,我们可以期待小米在科技领域带来更多令人期待的创新。

作者:ITBEAR科技资讯

来源:www.itbear.com.cn/html/2023-08/469514.html

收起阅读 »

App本地配置持久化方案

iOS
概述 在App开发过程中,会遇到很多简单配置项的持久化需求。比如App最近一次启动的时间,App最后一次登陆的用户ID,用户首次使用功能的判断条件。并且随着业务的扩展,零碎的配置还会不断增加。 UserDefaults Apple提供了UserDefault框...
继续阅读 »

概述


在App开发过程中,会遇到很多简单配置项的持久化需求。比如App最近一次启动的时间,App最后一次登陆的用户ID,用户首次使用功能的判断条件。并且随着业务的扩展,零碎的配置还会不断增加。


UserDefaults


Apple提供了UserDefault框架来帮助我们存储离散的配置,UserDefaults将以plist文件的形式存储在沙盒环境中。在不引入NoSql数据库的情况下,这是首推的方案。


注意事项


为了提升读取速度,App在启动时会将UserDefaults Standard对应的plist加载到内存中,如果文件过大就会增加App在启动时的加载时间,同时提高一定的内存消耗。


所以在Standard中,我们应该存放需要在App启动阶段立即获取的信息,比如用户最近登录的ID,App远程配置缓存的版本。


我们可以通过分表来缩减Standard的数据量。使用UserDefaults的suiteName模式创建不同的配置表,这样配置项将存储到各自的plist文件中,这些独立的plist不会在启动时被自动加载。


配置管理的常见问题

  1. 使用硬编码的String Key将配置存储到UserDefaults中,通过复制粘贴Key的字符串来存取数据。

  2. 零散的使用UserDefaults,缺少中心化管理方案。比如需要存储“开启通知功能”的配置,Key通常会直接被放在业务相关代码中维护。


方案 1.0


管理UserDefaults


创建一个UserDefault的管理类,主要用途是对UserDefault框架使用的收口,统一使用策略。

public class UserDefaultsManager {
public static let shared = UserDefaultsManager()
private init() {}
public var suiteName:String? {
didSet {
/**
根据传入的 suiteName的不同会产生四种情况:
传入 nil:跟使用UserDefaults.standard效果相同;
传入 bundle id:无效,返回 nil;
传入 App Groups 配置中 Group ID:会操作 APP 的共享目录中创建的以Group ID命名的 plist 文件,方便宿主应用与扩展应用之间共享数据;
传入其他值:操作的是沙箱中 Library/Preferences 目录下以 suiteName 命名的 plist 文件。
*/
userDefault = UserDefaults(suiteName: suiteName) ?? UserDefaults.standard
}
}
public var userDefault = UserDefaults.standard
}


创建常量表

  1. 对配置项的Key进行中心化的注册与维护
struct UserDefaultsKey {
static let appLanguageCode = "appLanguageCode"
static let lastLaunchSaleDate = "resetLastLaunchSaleDate"
static let lastSaleDate = "lastSaleDate"
static let lastSaveRateDate = "lastSaveRateDate"
static let lastVibrateTime = "lastVibrateTime"
static let exportedImageSaveCount = "exportedImageSaveCount"

static let onceFirstLaunchDate = "onceFirstLaunchDate"
static let onceServerUserIdStr = "onceServerUserIdStr"
static let onceDidClickCanvasButton = "onceDidClickCanvasButton"
static let onceDidClickCanvasTips = "onceDidClickCanvasTips"
static let onceDidClickEditBarGuide = "onceDidClickEditBarGuide"
static let onceDidClickEditFreestyleGuide = "onceDidClickEditFreestyleGuide"
static let onceDidClickManualCutoutGuide = "onceDidClickManualCutoutGuide"
static let onceDidClickBackgroundBlurGuide = "onceDidClickBackgroundBlurGuide"
static let onceDidTapCustomStickerBubble = "onceDidTapCustomStickerBubble"
static let onceDidRequestHomeTemplatesFromAPI = "onceDidRequestHomeTemplatesFromAPI"

static let firSaveExportTemplateKey = "firSaveExportTemplateKey"
static let firSaveTemplateDateKey = "firSaveTemplateDateKey"
static let firShareExportTemplateKey = "firShareExportTemplateKey"
static let firShareTemplateDateKey = "firShareTemplateDateKey"
}

2. 提供CURD API
private let appConfigUserDefaults = UserDefaultsManager(suiteName: "com.pe.config").userDefaults

var exportedImageSaveCount: Int {
return appConfigUserDefaults.integer(forKey: key)
}

func increaseExportedImageSaveCount() {
let key = UserDefaultsKey.exportedImageSaveCount
var count = appConfigUserDefaults.integer(forKey: key)
count += 1
appConfigUserDefaults.setValue(count, forKey: key)
}

我们对UserDefaults数据源进行了封装,String Key的注册也统一到常量文件中。当我们要查找或修改时,可以从配置表方便的查到String Key。


随着业务的膨胀,配置项会越来越多,我们会需要根据业务功能的分类,重新整理出多个分表。


随后我们会发现一些问题:

  1. String Key的注册虽然不麻烦,但Key中无法体现出Key归属与哪个UserDefaults。

  2. CURD API的数量会膨胀的更快,需要更多的维护成本。那么能不能将配置的管理更加面向对象,实现类似ORM的方式来管理呢?


方案2.0


根据上述的问题,来演化下方案2.0,我们来创建一个协议,用来规范UserDefaults的使用类。


它将包含CURD API的默认实现,初始化关联UserDefaults,自动生成String Key。

/// UserDefaults存储协议,建议用String类型的枚举去实现该协议
public protocol UserDefaultPreference {

var userDefaults: UserDefaults { get }
var key: String { get }

var bool: Bool { get }
var int: Int { get }
var float: Float { get }
var double: Double { get }

var string: String? { get }
var stringValue: String { get }

var dictionary: [String: Any]? { get }
var dictionaryValue: [String: Any] { get }

var array: [Any]? { get }
var arrayValue: [Any] { get }

var stringArray: [String]? { get }
var stringArrayValue: [String] { get }

var data: Data? { get }
var dataValue: Data { get }

var object: Any? { get }
var url: URL? { get }

func codableObject<T: Decodable>(_ as:T.Type) -> T?

func save<T: Encodable>(codableObject: T) -> Bool

func save(string: String)
func save(object: Any?)
func save(int: Int)
func save(float: Float)
func save(double: Double)
func save(bool: Bool)
func save(url: URL?)
func remove()
}

定义完协议后,我们再添加一些默认实现,降低使用成本。

// 生成默认的String Key
public extension UserDefaultPreference where Self: RawRepresentable, Self.RawValue == String {
var key: String { return "\(type(of: self)).\(rawValue)" }
}

public extension UserDefaultPreference {
// 默认使用 standard UserDefaults,可以在实现类中配置
var userDefaults: UserDefaults { return UserDefaultsManager.shared.userDefaults }

func codableObject<T: Decodable>(_ as:T.Type) -> T? {
return UserDefaultsManager.codableObject(`as`, key: key, userDefaults: userDefaults)
}

@discardableResult
func save<T: Encodable>(codableObject: T) -> Bool {
return UserDefaultsManager.save(codableObject: codableObject, key: key, userDefaults: userDefaults)
}

var object: Any? { return userDefaults.object(forKey: key) }

func hasKey() -> Bool { return userDefaults.object(forKey: key) != nil }

var url: URL? { return userDefaults.url(forKey: key) }

var string: String? { return userDefaults.string(forKey: key) }
var stringValue: String { return string ?? "" }

var dictionary: [String: Any]? { return userDefaults.dictionary(forKey: key) }
var dictionaryValue: [String: Any] { return dictionary ?? [String: Any]() }

var array: [Any]? { return userDefaults.array(forKey: key) }
var arrayValue: [Any] { return array ?? [Any]() }

var stringArray: [String]? { return userDefaults.stringArray(forKey: key) }
var stringArrayValue: [String] { return stringArray ?? [String]() }

var data: Data? { return userDefaults.data(forKey: key) }
var dataValue: Data { return userDefaults.data(forKey: key) ?? Data() }

var bool: Bool { return userDefaults.bool(forKey: key) }
var boolValue: Bool? {
guard hasKey() else { return nil }
return bool
}

var int: Int { return userDefaults.integer(forKey: key) }
var intValue: Int? {
guard hasKey() else { return nil }
return int
}

var float: Float { return userDefaults.float(forKey: key) }
var floatValue: Float? {
guard hasKey() else { return nil }
return float
}

var double: Double { return userDefaults.double(forKey: key) }
var doubleValue: Double? {
guard hasKey() else { return nil }
return double
}

func save(object: Any?) { userDefaults.set(object, forKey: key) }
func save(string: String) { userDefaults.set(string, forKey: key) }
func save(int: Int) { userDefaults.set(int, forKey: key) }
func save(float: Float) { userDefaults.set(float, forKey: key) }
func save(double: Double) { userDefaults.set(double, forKey: key) }
func save(bool: Bool) { userDefaults.set(bool, forKey: key) }
func save(url: URL?) { userDefaults.set(url, forKey: key) }

func remove() { userDefaults.removeObject(forKey: key) }
}

OK,我们来看下使用的案例

// MARK: - Launch
enum LaunchEventKey: String {
case didShowLaunchGuideOnThisLaunch
case launchGuideIsAlreadyShow
}
extension LaunchEventKey: UserDefaultPreference { }

func checkIfNeedLaunchGuide() -> Bool {
return !LaunchEventKey.launchGuideIsAlreadyShow.bool
}
func launchContentView() {
LaunchEventKey.launchGuideIsAlreadyShow.save(bool: true)
}

// MARK: - Language
enum LanguageEventKey: String {
case appLanguageCode
}
extension LanguageEventKey: UserDefaultPreference { }

static var appLanguageCode: String {
get {
let code = LanguageEventKey.appLanguageCode.string ?? ""
return code
}
set {
LanguageEventKey.appLanguageCode.save(codableObject: newValue)
}
}

// MARK: - Purchase
enum PurchaseStatusKey: String {
case iapSubscribeExpireDate
}
extension PurchaseStatusKey: UserDefaultPreference { }

func handle() {
let expirationDate: Date = Entitlement.expirationDate
PurchaseStatusKey.iapSubscribeExpireDate.save(object: expirationDate)
}

func getValues() {
let subscribeExpireDate = PurchaseStatusKey.iapSubscribeExpireDate.object as? Date
}

// MARK: - GlobalConfig
enum AppConfig: String {
case globalConfig
}

private let appConfigUserDefaults = UserDefaultsManager(suiteName: "com.pe.AppConfig").userDefaults

extension AppConfig: UserDefaultPreference {
var userDefaults: UserDefaults { return appConfigUserDefaults }
}

// 自定义类型
public class GlobalConfig: Codable {
/// 配置版本号
let configVersion: Int
/// 用户初始试用次数
let userInitialTrialCount: Int
/// 生成时间 如:2022-09-19T02:58:31Z
let createDate: String

enum CodingKeys: String, CodingKey {
case configVersion = "version"
case userInitialTrialCount = "user_initial_trial_count"
case createDate = "create_date"
}
...
}

lazy var globalConfig: GlobalConfig = {
guard let config = AppConfig.globalConfig.codableObject(GlobalConfig.self) else {
return GlobalConfig()
}
return config
}() {
didSet { AppConfig.globalConfig.save(codableObject: globalConfig) }
}

从上述案例可以看出,在配置项的注册和维护成本相对方案1.0有了大幅度的降低,对UserDefaults的使用进行了规范性的约束,提供了更方便的CURD API,使用方式也更加符合面向对象的习惯。


同时为了满足复杂结构体的存储需求,我们可以扩展实现Codable对象的存取逻辑。


总结


本方案的目的是解决乱象丛生的UserDefaults的使用情况,分析后向两个方向进行了优化:

  1. 提供中心化的配置方式,关联UserDefaults、维护String Key。
  2. 提供类ORM的管理方式,减少业务的接入成本。


针对更复杂的、类缓存集合的,或者有查询需求的配置项管理,请尽快用NoSQL替换,避免数据量上升带来的效率下降。


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

七夕节那天,我被裁员了

前言 先祝jym昨天七夕节快乐吧,有情人终成眷属 一直想在掘金上发文但迟迟未行动,上学时在b乎写了一段时间一直切换不来平台。看着b乎仅存的几百号关注者到这边只能从0开始很是不情愿( ̄Д ̄)ノ,也行吧就从零开始了,我的工作似乎也得从零开始了。该死的仪式感... ...
继续阅读 »

前言


先祝jym昨天七夕节快乐吧,有情人终成眷属


一直想在掘金上发文但迟迟未行动,上学时在b乎写了一段时间一直切换不来平台。看着b乎仅存的几百号关注者到这边只能从0开始很是不情愿( ̄Д ̄)ノ,也行吧就从零开始了,我的工作似乎也得从零开始了。该死的仪式感...




难忘的七夕


如标题所见,七夕节被通知裁员了。单身狗今天受到双重痛击。说的是公司迫于压力需要节流,部门会裁1/4。而我恰好就是那个“幸运儿”。其实从前些时间就早有风向,先是ui,到测试,终于到开发人员了,而到了开发最先动刀的果然还就是鼠鼠前端。


23届真的太惨了。在这里从实习到转正也快干了1年了,但才刚拿了一个月转正工资就遇到调整,实属难绷。


应届生身份也没了,工作年限也达不到,双重叠buff了属于是,老实说刚听到这消息的时候自己还是懵的,也就和往常一样上班怎么今天就这么突然开这样的会议呢?


而后是无奈,但是又有点兴奋,正好可以逼自己再出去外面探探,看看市场如何。

回家路上,打开手机刷了刷boss,要么招24的要么一到两年工作经验的,一下子又把我难住了。。。我怎么这么背啊5555


整个晚上我都在思考人生,我在想假如自己不是读计算机会如何?我是不是能没负担的尝试各种工作,自己真的要在程序员这条赛道一条路走到黑吗?


程序员的工作又何尝不是围墙,技术类的工作是建立起了行业壁垒,在外行人看来能将代码变成程序是一件很酷的事,而正是这道壁垒,让外行的人想进来,里面的人想走却又鼓不起勇气。




是啊 我除了写代码还能干什么呢?难道真的放弃学了这么久在外行人看来很厉害的技术去做其他工作吗?(脱不下的长衫又穿上了)


总结


我一直认为无论发生什么事,一切都是上天最好的安排,世上无所谓输赢祸福,关键是从中有所收获,写几点感悟吧:

  • 拥抱不确定性是踏入社会后最需要学会的。读书时每个人都是按部就班,除了学习剩下的挑战就是安排到日程上的考试,而步入社会每天遇到的事情都不一样,so be water my friend 随机应变 随遇而安
  • 负能量可以有,适当摆烂一下,但睡一觉起来就得调整好心态了,享受生活不要被蛋疼的事影响
  • 不要畏手畏脚,敞开自己而不是压抑自己,一辈子很短,很多事情不去尝试就来不及了
  • 灵活就业或许才是这个时代下的答案?不知道
  • 行动起来最重要,如前面说的想在掘金发文却拖延了很久,而正是这回吐槽却让我奋笔疾书了起来,命运的齿轮或许就此转动,后面我会督促自己继续更开发过程中的杂记、生活记录
  • 接下来的安排:先编辑个简历再出发,然后整理一下这一年来开发的东西,记录一下

结语


我也不知道后面会如何,走一步是一步吧,通知还没正式下来。如果读到这里的你觉得我我是个有趣的人,那么点个关注吧,这也是我更文的动力


失败并不存在,关键是故事仍在继续......


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

iOS 拖拽式控件:QiDragView

iOS
首先,我们先看一下QiDragView的效果图:  一、QiDragView整体架构设计 话不多说,上架构图~ QiDragView(QiDragSortView)是一种可选择可拖拽的自定义控件,可以满足一些拖拽排序的业务需求场景。 二、如何自定义...
继续阅读 »

首先,我们先看一下QiDragView的效果图: 


一、QiDragView整体架构设计


话不多说,上架构图~



QiDragView(QiDragSortView)是一种可选择可拖拽的自定义控件,可以满足一些拖拽排序的业务需求场景。


二、如何自定义使用QiDragView?


在上Demo之前,先介绍几个可以自定义的UI配置属性:



以及一些逻辑配置属性:



使用起来也很方便:

  • 直接设置titles即可创建出对应title的Buttons。
  • 通过dragSortEnded的block方法回调,处理拖拽后的业务逻辑:按钮的排序、按钮是否选择等属性

默认配置用法:

QiDragSortView *dragSortView = [[QiDragSortView alloc] initWithFrame:CGRectMake(.0, 100.0, self.view.bounds.size.width, .0)];
dragSortView.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:.5];

dragSortView.titles = @[@"首页推荐", @"奇舞周刊", @"众成翻译", @"QiShare", @"HULK一线杂谈", @"Qtest之道"];//!< 初始的Buttons(必填)
[self.view addSubview:dragSortView];

//! 拖拽方法回调:能拿到Button数组的排序和选择状态
dragSortView.dragSortEnded = ^(NSArray<UIButton *> * _Nonnull buttons) {
for (UIButton *button in buttons) {
NSLog(@"title: %@, selected: %i", button.currentTitle, button.isSelected);
}
};

自定义配置用法:

QiDragSortView *dragSortView = [[QiDragSortView alloc] initWithFrame:CGRectMake(.0, 100.0, self.view.bounds.size.width, .0)];
dragSortView.backgroundColor = [[UIColor lightGrayColor] colorWithAlphaComponent:.5];
dragSortView.rowHeight = 50.0;
dragSortView.rowMargin = 30.0;
dragSortView.rowPadding = 20.0;
dragSortView.columnCount = 3;
dragSortView.columnMargin = 30.0;
dragSortView.columnPadding = 20.0;
dragSortView.normalColor = [UIColor redColor];
dragSortView.selectedColor = [UIColor purpleColor];
dragSortView.enabledTitles = @[@"奇舞周刊", @"众成翻译", @"QiShare", @"HULK一线杂谈", @"Qtest之道"];//!< 可以点击选择的Buttons(选填,默认全选)
dragSortView.selectedTitles = @[@"首页推荐", @"HULK一线杂谈", @"Qtest之道"];//!< 初始选择的Buttons(选填,默认全选)
dragSortView.titles = @[@"首页推荐", @"奇舞周刊", @"众成翻译", @"QiShare", @"HULK一线杂谈", @"Qtest之道"];//!< 初始的Buttons(必填)
[self.view addSubview:dragSortView];

//! 拖拽方法回调:能拿到Button数组的排序和选择状态
dragSortView.dragSortEnded = ^(NSArray<UIButton *> * _Nonnull buttons) {
for (UIButton *button in buttons) {
NSLog(@"title: %@, selected: %i", button.currentTitle, button.isSelected);
}
};

三、QiDragView的技术点


3.1 长按手势:


长按手势分别对应三种状态:UIGestureRecognizerStateBeganUIGestureRecognizerStateChangedUIGestureRecognizerStateEnded

//! 长按手势
- (void)longPress:(UILongPressGestureRecognizer *)gesture {

UIButton *currentButton = (UIButton *)gesture.view;

if (gesture.state == UIGestureRecognizerStateBegan) {

[self bringSubviewToFront:currentButton];

[UIView animateWithDuration:.25 animations:^{
self.originButtonCenter = currentButton.center;
self.originGesturePoint = [gesture locationInView:currentButton];
currentButton.transform = CGAffineTransformScale(currentButton.transform, 1.2, 1.2);
}];
}
else if (gesture.state == UIGestureRecognizerStateEnded) {

[UIView animateWithDuration:.25 animations:^{
currentButton.center = self.originButtonCenter;
currentButton.transform = CGAffineTransformIdentity;
} completion:^(BOOL finished) {
if (self.dragSortEnded) {
self.dragSortEnded(self.buttons);
}
}];
}
else if (gesture.state == UIGestureRecognizerStateChanged) {

CGPoint gesturePoint = [gesture locationInView:currentButton];
CGFloat deltaX = gesturePoint.x - _originGesturePoint.x;
CGFloat deltaY = gesturePoint.y - _originGesturePoint.y;
currentButton.center = CGPointMake(currentButton.center.x + deltaX, currentButton.center.y + deltaY);

NSInteger fromIndex = currentButton.tag;
NSInteger toIndex = [self toIndexWithCurrentButton:currentButton];

if (toIndex >= 0) {
currentButton.tag = toIndex;

if (toIndex > fromIndex) {
for (NSInteger i = fromIndex; i < toIndex; i++) {
UIButton *nextButton = _buttons[i + 1];
CGPoint tempPoint = nextButton.center;
[UIView animateWithDuration:.5 animations:^{
nextButton.center = self.originButtonCenter;
}];
_originButtonCenter = tempPoint;
nextButton.tag = i;
}
}
else if (toIndex < fromIndex) {
for (NSInteger i = fromIndex; i > toIndex; i--) {
UIButton *previousButton = self.buttons[i - 1];
CGPoint tempPoint = previousButton.center;
[UIView animateWithDuration:.5 animations:^{
previousButton.center = self.originButtonCenter;
}];
_originButtonCenter = tempPoint;
previousButton.tag = i;
}
}
[_buttons sortUsingComparator:^NSComparisonResult(UIButton *obj1, UIButton *obj2) {
return obj1.tag > obj2.tag;
}];
}
}
}

3.2 配置按钮:


设计思路:在属性titles的setter方法中,初始化并配置好各个Buttons。

- (void)setTitles:(NSArray<NSString *> *)titles {

_titles = titles;

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.01 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSInteger differCount = titles.count - self.buttons.count;

if (differCount > 0) {
for (NSInteger i = self.buttons.count; i < titles.count; i++) {
[self.buttons addObject:[self buttonWithTag:i]];
}
}
else if (differCount < 0) {
NSArray *extraButtons = [self.buttons subarrayWithRange:(NSRange){titles.count, self.buttons.count - titles.count}];
[self.buttons removeObjectsInArray:extraButtons];
for (UIButton *button in extraButtons) {
[button removeFromSuperview];
}
}

self.enabledTitles = self.enabledTitles ?: titles;//!< 如果有,就传入,否则传入titles
self.selectedTitles = self.selectedTitles ?: titles;

for (NSInteger i = 0; i < self.buttons.count; i++) {
[self.buttons[i] setTitle:titles[i] forState:UIControlStateNormal];
[self.buttons[i] addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)]];//!< 长按手势
[self selectButton:self.buttons[i] forStatus:[self.selectedTitles containsObject:titles[i]]];
if ([self.enabledTitles containsObject:titles[i]]) {
[self.buttons[i] addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside];
}
}

for (NSInteger i = 0; i < self.buttons.count; i++) {
NSInteger rowIndex = i / self.columnCount;
NSInteger columnIndex = i % self.columnCount;
CGFloat buttonWidth = (self.bounds.size.width - self.columnMargin * 2 - self.columnPadding * (self.columnCount - 1)) / self.columnCount;
CGFloat buttonX = self.columnMargin + columnIndex * (buttonWidth + self.columnPadding);
CGFloat buttonY = self.rowMargin + rowIndex * (self.rowHeight + self.rowPadding);
self.buttons[i].frame = CGRectMake(buttonX, buttonY, buttonWidth, self.rowHeight);
}

CGRect frame = self.frame;
NSInteger rowCount = ceilf((CGFloat)self.buttons.count / (CGFloat)self.columnCount);
frame.size.height = self.rowMargin * 2 + self.rowHeight * rowCount + self.rowPadding * (rowCount - 1);
self.frame = frame;
});
}

源码地址:QiDragView


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

第三方库并不是必须的

iOS
前言 我在Lyft的八年间,很多产品经理以及工程师经常想往我们 app 里添加第三方库。有时候集成一个特定的库(比如 PayPal)是必须的,有时候是避免去开发一些非常复杂的功能,有时候仅仅只是避免c重复造轮子。 虽然这些都是合理的考量,但使用第三方库的风险和...
继续阅读 »

前言


我在Lyft的八年间,很多产品经理以及工程师经常想往我们 app 里添加第三方库。有时候集成一个特定的库(比如 PayPal)是必须的,有时候是避免去开发一些非常复杂的功能,有时候仅仅只是避免c重复造轮子。


虽然这些都是合理的考量,但使用第三方库的风险和相关成本往往被忽视或误解。在某些情况下,风险是值得的,但是在决定冒险之前,首先要能够明确的定义风险。为了使风险评估更加的透明和一致,我们制定了一个流程来衡量我们将其集成到app有多大的风险。


风险


大多数大型组织,包括我们,都有某种形式的代码审查,作为开发实践的一部分。对这些团队来说,添加一个第三方库就相当于添加了一堆由不属于团队成员开发,未经审查的代码。这破坏了团队一直坚持的代码审查原则,交付了质量未知的代码。这给app的运行方式以及长期开发带来了风险,对于大型团队而言,更是对整体业务带来了风险。


运行时风险


库代码通常来说,对于系统资源,和app拥有相同级别的访问权限,但它们不一定应用团队为管理这些资源而制定的最佳实践。这意味着它们可以在没有限制的情况下访问磁盘,网络,内存,CPU等等,因此,它们可以(过度)将文件写入磁盘,使用未优化的代码占用内存或CPU,导致死锁或主线程延迟,下载(和上传!)大量数据等等。更糟糕的是他们会导致崩溃,甚至崩溃循环


其中许多情况直到 app 已经上架才被发现,在这种情况下,修复它需要创建一个新版本,并通过审核,这通常需要大量时间和成本。这种风险可以通过一个变量控制是否调用来进行一定程度的控制,但是这种方法也并非万无一失(看下文)。


开发风险


引用一个同事的话:“每一行代码都是一种负担”,对不是你自己写的代码而言,这句话更甚。库在适配新技术或API时可能很慢,这阻碍了代码开发,或者太快,导致开发的版本过高。


库在采用新技术或API时可能很慢,阻碍了代码库,或者太快,导致部署目标太高。每当 Apple 和 Google 每年发布一个新 OS 版本时,他们通常要求开发人员根据SDK的变化更新代码,库开发人员也必须这样做。这需要协调一致的努力、优先事项的一致性以及及时完成工作的能力。


随着移动平台的不断变化,以及团队(成员)也不是一成不变,这将会成为一个持续不断的风险。当被集成的库不存在了,而库又需要更新时,会花很多时间来决定谁来做。事实证明一旦一个库存在,就很少也很难被移除,因此我们将其视为长期维护成本。


商业风险


如同我上面所说,现代的操作系统并没有对 app 代码和库代码进行区分,因此除了系统资源之外,它们还可以访问用户信息。作为 app 的开发者,我们负责恰当的使用这部分信息,也需要为任何第三方库负责。


如果用户给了 Lyft app 地理位置授权,任何第三方库也将自动得获得授权。他们可以将那些(地理位置)数据上传到自己服务器,竞对服务器,或者谁知道还有什么地方。当一个库需要我们没有的权限时,那问题就更大了。


同样,一个系统的安全取决于其最薄弱的环节,但如果其中包含未经审核的代码,那么你就不知道它到底有多安全。你精心设计的安全编码实践可能会被一个行为不当的库所破坏。苹果和谷歌实施的任何政策都是如此,例如“你不得对用户追踪”。


减少风险


当对一个库(是否)进行使用评估时,我们首先要问几个问题,以了解对库的需求。


我们内部能做么?


有时候我们只需要简单的粘贴复制真正需要的部分。在更复杂的场景中,库与自定义后端通信,我们对该API进行了逆向,并自己构建了一个迷你SDK(同样,只构建了我们需要的部分)。在90%的情况下,这是首选,但在与非常特定的供应商或需求集成时并不总是可行。


有多少用户从该库中受益?


在一种情况下,我们正在考虑添加一个风险很大的库(根据下面的标准),旨在为一小部分用户提供服务,同时将我们的所有用户都暴露在该库中。 对于我们认为会从中受益的一小部分客户,我们冒了为我们所有用户带来问题的风险。


这个库有什么传递依赖?


我们还需要评估库的所有依赖项的以下标准。


退出标准是什么?


如果集成成功,是否有办法将其转移到内部? 如果不成功,是否有办法删除?


评价标准


如果此时团队仍然希望集成库,我们要求他们根据一组标准对库进行“评分”。下面的列表并不全面,但应该能很好地说明我们希望看到的。


阻断标准


这些标准将阻止我们从技术上或者公司政策上集成此库,在进行下一步之前,我们必须解决:


过高的 deployment target/target SDKs。 我们支持过去4年主流的操作系统(版本),所以第三方库至少也需要支持一样多。


许可证不正确/缺失。 我们将许可文件与应用捆绑在一起,以确保我们可以合法使用代码并将其归属于许可持有人。


没有冲突的传递依赖关系。 一个库不能有一个我们已经包含但版本不同的传递依赖项。


不显示它自己的 UI 。 我们非常小心地使我们的产品看起来尽可能统一,定制用户界面对此不利。


它不使用私有 API 。 我们不愿意冒 app 因使用私有 API 而被拒绝的风险。


主要关注点


闭源。 访问源代码意味着我们可以选择我们想要包含的库的哪些部分,以及如何将该源代码与应用程序的其余部分捆绑在一起。 对于我们来说,一个封闭源代码的二进制发行版更难集成。


编译时有警告。 我们启用了“警告视为错误”,具有编译警告的库是库整体质量(下降)的良好指示。


糟糕的文档。 我们希望有高质量的内联文档,外部”如何使用“文档,以及有意义的更新日志。


二进制体积。 这个库有多大?一些库提供了很多功能,而我们只需要其中的一小部分。尤其是在没有访问源码权限的情况下,这通常是一个全有或全无的情况。


外部的网络流量。 与我们无法控制的上游服务器/端点通信的库可能会在服务器关闭、错误数据被发回等时关闭整个应用程序。这也与我上面提到的隐私问题相同。


技术支持。 当事情不能正常工作时,我们需要能够报告/上报问题,并在合理的时间内解决问题。开源项目通常由志愿者维护,也很难有一个时间线,但至少我们可以自己进行修改。这在闭源项目是不可能的。


无法禁用。 虽然大多数库特别要求我们初始化它,但有些库在实例化时更“主动”,并且在我们不调用它的情况下可以自己执行工作。这意味着当库导致问题时,我们无法通过功能变量或其他机制将其关闭。


我们为所有这些(和其他一些)标准分配了点数,并要求工程师为他们想要集成的库汇总这些点数。虽然默认情况下,低分数并不难被拒绝,但我们通常会要求更多的理由来继续前进。


最后


虽然这个过程看起来非常严格,在许多情况下,潜在风险是假设的,但我们有我在这篇博文中描述的每个场景的实际例子。将评估记录下来并公开,也有助于将相对风险传达给不熟悉移动平台工作方式的人,并证明我们没有随意评估风险。


此外,我不想声称每一个第三方库本质上都是坏的。事实上,我们在Lyft使用了很多:RxSwiftRxJavaBugsnagSDKGoogle MapsTensorflow,以及一些较小的用于非常特定的用例。但所有这些要么都经过了充分审查,要么我们已经决定风险值得收益,同时对这些风险和收益的真正含义有了清晰的认识。


最后,作为一个专业开发人员提示:始终在库的API之上创建自己的抽象,不要直接调用它们的API。这使得将来替换(或删除)底层库更加容易,再次减轻了与长期开发相关的一些风险。


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

所有开发者注意,苹果审核策略有变

iOS
这里每天分享一个 iOS 的新知识,快来关注我吧 访问敏感数据的 App 新规 苹果最近在 Apple Developer 上发了篇新闻公告,对需要访问用户敏感数据的 App 增加了审核要求。 这件事的缘由是苹果发现有一小部分 API 可能会被开发者滥用,通过...
继续阅读 »


这里每天分享一个 iOS 的新知识,快来关注我吧


访问敏感数据的 App 新规


苹果最近在 Apple Developer 上发了篇新闻公告,对需要访问用户敏感数据的 App 增加了审核要求。


这件事的缘由是苹果发现有一小部分 API 可能会被开发者滥用,通过信息指纹收集有关用户设备的信息。


早在今年 6 月的 WWDC23 上苹果就宣布,开发人员需要在其应用程序的隐私清单中声明使用某些 API 的原因,目前正式放出了这份需要声明的 API 列表。


新规详情


从今年(2023年)秋天开始,大概是 9 月中旬左右,如果你将你的 App 上传到 App Store Connect,你的应用程序使用到了需要声明原因的 API(也包括你引入的第三方 SDK),但是你没有在隐私清单文件中添加原因,那么 Apple 会给你发送一封警告性的邮件。


从 2024 年春季开始,大概是 3 月左右,没有在隐私清单文件中说明使用原因的 App 将会被拒审核。


需要声明原因的 API 有哪些?


1、NSUserdefaults 相关 API


这个 API 是被讨论最多争议最大的,因为几乎每个 App 都会用到,而且因为有沙盒保护,每个 app 的存储空间是隔离的,这都要申报理由,的确十分荒谬。


2、获取文件时间戳相关的 API

  • creationDate

  • modificationDate

  • fileModificationDate

  • contentModificationDateKey

  • creationDateKey

  • getattrlist(::::_:)

  • getattrlistbulk(::::_:)

  • fgetattrlist(::::_:)

  • stat

  • fstat(::)

  • fstatat(::::)

  • lstat(::)

  • getattrlistat(::::::)


3、获取系统启动时间的 API


大多数衡量 App 启动时间的 APM 库会用到这个 API。

  • systemUptime

  • mach_absolute_time()


4、磁盘空间 API

  • volumeAvailableCapacityKey

  • volumeAvailableCapacityForImportantUsageKey

  • volumeAvailableCapacityForOpportunisticUsageKey

  • volumeTotalCapacityKey

  • systemFreeSize

  • systemSize

  • statfs(::)

  • statvfs(::)

  • fstatfs(::)

  • fstatvfs(::)

  • getattrlist(::::_:)

  • fgetattrlist(::::_:)

  • getattrlistat(::::::)


5、活动键盘 API


这个 API 可以来确定当前用户文本输入的主要语言,有些 App 可能会用来标记用户。

  • activeInputModes

如何在 Xcode 中配置


由于目前 Xcode 15 正式版还没有发布,下边的操作是在 Beta 版本进行的。


在 Xcode 15 中隐私部分全部归类到了一个后缀为 .xcprivacy 的文件中,创建项目时默认没有生成这个文件,我们先来创建一下。


打开项目后,按快捷键 Command + N 新建文件,在面板中搜索 privacy,选择 App Pirvacy 点击下一步创建这个文件。



这个文件是个 plist 格式的面板,默认情况下长这样:



然后点击加号,创建一个 Privacy Accessed API TypesKey,这是一个数组,用来包含所有你 App 使用到需要申明原因的 API。



在这个数组下继续点击加号,创建一个 Item,会看到两个选项:

  • Privacy Accessed API Type:用到的 API 类型

  • Privacy Accessed API Reasons:使用这个 API 的原因(也是个数组,因为可能包含多个原因)



这两个 Key 都创建出来,然后在 Privacy Accessed API Type 一栏点击右侧的菜单,菜单中会列出上边提到的所有 API,选择你需要申报的 API,我这里就拿 UserDefault 来举例:



然后在 Privacy Accessed API Reasons 一览中点击加号,在右侧的选项框中选择对应的原因,每个 API 对应的原因都会列出来,可以到苹果的官方文档上查看这个 API 的原因对应的是哪个,比如 UserDefault 对应的是 CA92.1,我这里就选择这个:



到此,申报原因就完成了,原因不需要自己填写,直接使用苹果给出的选项就可以了,还是蛮简单的。


参考资料


[1]


公告原文: developer.apple.com/news/?id=z6…


[2]


需要在 App 内声明的 API 列表: developer.apple.com/documentati…


[3]


API 列表对应的原因: developer.apple.com/documentati…


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

Java仿抽奖系统

Java仿抽奖系统 前言 今天也是刚看完最近挺火的电影《孤注一掷》,也是亲眼的看到了,一个完整的家庭,是如何因为赌,而导致分崩离析,最后导致走向破碎的。 一旦涉及到电子的东西,很多东西都是变得可以控制的。这个作为程序员的我们是最清楚的,同时现在的反诈宣传,做的...
继续阅读 »

Java仿抽奖系统


前言


今天也是刚看完最近挺火的电影《孤注一掷》,也是亲眼的看到了,一个完整的家庭,是如何因为赌,而导致分崩离析,最后导致走向破碎的。


一旦涉及到电子的东西,很多东西都是变得可以控制的。这个作为程序员的我们是最清楚的,同时现在的反诈宣传,做的也是非常的到位,当时剧中哪位女警说的话,影响也非常的深刻。人都有贪心和不甘心,这也就是赌能真正抓住人的东西


好了不说那么多了,下面看一个简易的程序的代码实现


代码实现


首先我们定义一些常量


private static final int PRIZE_LEVELS = 4; // 奖品级别数量
private static final int[] PRIZE_AMOUNTS = {1, 10, 100, 1000}; // 奖品金额
private static final double[] WINNING_RATES = {10, 0, 0, 0}; // 中奖率

public static void main(String[] args) {
       // 设定中奖率
       double winningRate = 0.1;

       // 抽奖
       int prize = drawLottery(winningRate);

       // 发放奖品
       if (prize > 0) {
           System.out.println("恭喜你中奖了!奖金:" + prize + "元");
      } else {
           System.out.println("很遗憾,未中奖");
      }
  }

   // 抽奖方法
   private static int drawLottery(double winningRate) {
       Random random = new Random();
       int prize = 0;

       // 根据奖品级别逐级判断中奖
       for (int i = 0; i < PRIZE_LEVELS; i++) {
           // 生成0到1之间的随机数,判断是否中奖
           if (random.nextDouble() < winningRate * WINNING_RATES[i]) {
               prize = PRIZE_AMOUNTS[i];
               break;
          }
      }

       return prize;
  }
}

一个简单的抽奖程序。我们根据这个进行一些修改,更加的客观真实,我们加上已经有的金额和权重,让他更像是真正的赌。


我们加入权重以及自己的现金


private static double[] WEIGHTS;
// 自己的现金余额
static int cashBalance = 1000;

之后我们进行这样设计


   public static void main(String[] args) {
       // 计算权重
       calculateWeights();

       // 自己的现金余额
       int cashBalance = 1000;

       // 抽奖一次
       drawLottery(cashBalance);
  }

public static void calculateWeights() {
   WEIGHTS = new double[WINNING_RATES.length];
   double totalWeight = 0;

   // 计算总权重
   for (double rate : WINNING_RATES) {
       totalWeight += rate;
  }

   // 计算每个奖品级别的权重
   for (int i = 0; i < WEIGHTS.length; i++) {
       WEIGHTS[i] = WINNING_RATES[i] / totalWeight;
  }
}

public static void drawLottery() {
   Random random = new Random();
   double randomValue = random.nextDouble();

   int prizeIndex = 0;
   double cumulativeWeight = 0;

   // 根据随机值选择对应的奖品级别
   for (int i = 0; i < WEIGHTS.length; i++) {
       cumulativeWeight += WEIGHTS[i];
       if (randomValue <= cumulativeWeight) {
           prizeIndex = i;
           break;
      }
  }

   // 判断是否中奖
   if (random.nextDouble() <= WINNING_RATES[prizeIndex]) {
       int prizeAmount = PRIZE_AMOUNTS[prizeIndex];
       System.out.println("恭喜您中奖了!获得奖金:" + prizeAmount + "元");
       cashBalance += prizeAmount;
  } else {
       System.out.println("很遗憾,未中奖。");
  }

   // 更新现金余额
   cashBalance -= COST_PER_DRAW;
   System.out.println("抽奖后的现金余额:" + cashBalance + "元");
}

可以看出,我们这里规定的是20元抽奖一次,最高能达到1000元。


image-20230821164510692


运行一次后发现从原来的升值到了5000,


可是当你一旦陷入进去的话,只要我们稍微修改一下中奖率


image-20230821164625143


就会不断的去输。



赌博有害健康,需要我们每个人去

作者:小u
来源:juejin.cn/post/7270173541457723452
制止


收起阅读 »

别再用unload了:拥抱浏览器生命周期API

web
以前的前端页面很少有涉及页面的生命周期相关的话题,我们最熟悉的还是这几个load, beforeunload, unload 事件,用来监听页面加载完、离开页面之前、页面卸载事件并做一些相应的处理,比如保存需要持久化的数据,或者上报一些埋点数据。首先我们先理下...
继续阅读 »

以前的前端页面很少有涉及页面的生命周期相关的话题,我们最熟悉的还是这几个load, beforeunload, unload 事件,用来监听页面加载完、离开页面之前、页面卸载事件并做一些相应的处理,比如保存需要持久化的数据,或者上报一些埋点数据。首先我们先理下现有问题:


unload事件的问题


上面的这些事件(特别是unload)会有以下一些问题:



  • 用户主动触发的事件,无系统自动触发的一些状态监控

  • 无法稳定触发,特别是在手机端

  • 前进/后退时无法进入浏览器缓存(back/forward cache),导致用户来回切换时加载慢

  • 用户离开时埋点数据可能无法稳定上报

  • 无法追踪用户在页面上的完整生命周期


虽然现在计算机的算力硬件越来越强,但是现在我们同时需要处理的事情越来越多,开的Tab也就越来越多,一直被人诟病的比如chrome的疯狂吃内存的问题也很是头疼,接近顶配的电脑可能很快就被chrome给耗尽😂,所以现在的chrome版本会根据系统资源消耗程度来对不活动的Tab冻结或直接销毁来释放内存及减少电量的消耗。


针对这些问题现代浏览器提供了多个生命周期相关的事件来让开发者可以监听,并在触发时做相应处理。这里主要介绍几个最常用的而且对埋点相对比较重要的节点的生命周期事件。


页面是否可见: visibilitychange


visibilitychange事件涉及的场景会比较多,主要有用户切换tab导航到其他页面关闭Tab最小化窗口手机端切换app等,可在document上添加该事件,回调里通过document.visibilityState可以知道当前Tab是否隐藏了:


document.addEventListener(
'visibilitychange',
(e) => {
const isTabVisible = document.visibilityState === 'visible'
console.log('tab is visible: ', isTabVisible)
},
{
capture: true,
}
)

以上事件需要在捕获阶段进行监听(包括下面讲到的其他相关事件),避免被业务代码阻止冒泡,而且有些是window层面的,没有冒泡的阶段,所以需要在capture阶段执行。


页面的加载与离开: pageshow/pagehide


首先,这事件名是真的很容易让人理解错😭(pageshow/pagehide感觉才是上面visibilitychange所表示的意义)。


pageshow主要在页面新加载或被浏览器冻结后重新解冻时触发, 可通过e.persisted来确定是如何加载的:


window.addEventListener(
'pageshow',
(e) => {
// true 为之前冻结现在解冻,false相当于重新加载或新加载
console.log('e.persisted: ', e.persisted)
},
{
capture: true,
}
)

pagehide本质上是对unload事件的真正替换且具有更稳定的触发时机,我们可以在这里对一些埋点事件或者其他一些小批量的数据进行上报(sendBeacon或fetch, 下面讲到),这里还有个需要注意的是visibilitychange触发范围更广,也就是页面进来和离开时也会触发,和pageshow/pagehide同时触发(都触发时在这2个之前),所以如果业务需要区分页面离开还是用户仅仅是切换tab时,需要在不同的事件回调里做不同的处理。


页面离开时的数据上报


当需要在用户离开页面时(pagehide触发时)稳定的上报一些数据,我们一般会使用navigator.sendBeacon()方法:


// queued = true 说明浏览器接收请求,马上会上报出去,false的话可能业务要做其他处理
const queued = navigator.sendBeacon('/api', data)

当然sendBeacon有大小限制,一般在64KB以下,超出很可能会失败,所以我们在这里上报时要控制大小,如果数据量较大,建议提前上报一部分,比如在visibilitychange时(用户切换tab时)先上报一部分,确保只留64KB以下的数据放到最后。
除了sendBeacon之外我们还可以用fetch上报,通过设置keepalive: true来达到sendBeacon一样的效果,当然也有一样的大小限制,这里可以做兼容处理:


function send(body) {
if (navigator.sendBeacon) {
navigator.sendBeacon('/api', body)
return
}
fetch('/api', {
body,
method: 'POST',
keepalive: true
})
}

其他生命周期事件


除了上面常用的生命周期之外,浏览器的生命周期API还提供了页面的其他状态如:freeze, resume,freeze一般在用户导航到其他页面并且满足进入back/forward cache的条件时,或者用户在其他tab,浏览器根据系统资源自动使其进入冻结状态,当用户通过浏览器的返回按钮或重新切回到该Tab时,会触发resume事件说明页面继续,紧接着会触发pageshow事件,说明页面又进来了。关于更多的说明可以参考Page Lifecycle API


电脑息屏、休眠时


还有一些场景是电脑休眠或者只是关闭屏幕时,页面的生命周期以及页面里的一些定时器会有哪些变化?


休眠比较简单,定时器、网络连接这些都会被暂停,如果不想丢失数据,需要在pagehide做处理;息屏时可在document.visibilityState !== 'visible'时做处理,相当于页面不可见了,而且页面里的定时器不会被停掉但是可能会被浏览器延时处理,比如正常代码里是5s执行一次,此时可能会变成30s或者1min执行一次来节省资源。


总结


以上主要介绍了如何使用现代浏览器提供的生命周期API来在页面的不同阶段做相应的处理,pagehide事件主要用来替换unload, 关于beforeunload事件在有些应用中还是需要的,但是我们应该有选择性的添加该事件及在合适的时间移除监听该事件,比如未保存的数据已经保存完毕时可以移除,当又有新改动时再监听。


还有很多其他的生命周期事件可以让开发者能对用户在页面里/外的整个生命周期有更好的理解,以此来分析并提升网站的整体体验。笔者后面会写一篇如何跟踪用户在浏览器里的同一tab/不同tab之间的来回操作、记录并上报,并能够在后台进行session回放的文章,通过这些扩展能力相较于纯粹的埋点能更好地理

作者:jasonboy7
来源:juejin.cn/post/7269983642155663419
解用户行为以此来优化产品体验🍻。

收起阅读 »

一个写了3年半flutter的小伙,突然写了2个月uniapp的感悟!

前言 因为某些原因,在过去的三年半时间,我除了flutter之外,很少接触其他的框架,期间除了学习了Android(主要是Kotlin、jetpack)、GoLang Gin之外基本上很少接触其他的框架。而在最近的两个月,突然来了一个要求用uniapp实现的项...
继续阅读 »

前言


因为某些原因,在过去的三年半时间,我除了flutter之外,很少接触其他的框架,期间除了学习了Android(主要是Kotlin、jetpack)、GoLang Gin之外基本上很少接触其他的框架。而在最近的两个月,突然来了一个要求用uniapp实现的项目,在接下这个前,我是有些抵触的。第一点是觉得自己短期内去学一个新的框架,学到的东西不足以完成整个项目,第二点是不想脱离舒适圈。当然,最后我还是选择了直面困难,不然您也看不到这篇文章了🤣。


本文更多的是帮助您解决是否要学习uni-app或flutter框架的这一问题,以及两个框架的一些代码对比。如果您想要判断的是一个新项目该使用哪个框架,那么本文就不是很合适了~


跨平台层面的对比感悟


在Flutter刚出来的这几年,经常会在各种跨平台框架对比的文章下,看到将其与uni-app进行比较。当时我也没有在意太多,以为uni-app也是个差不多的“正经”跨平台框架,但当我打开uni-app官网的时候,我震惊了,因为我看到了这样一句话:一套代码编到15个平台,这不是梦想。我瞬间就傻眼了,这么nb?Flutter不也才横跨六大平台 ?在仔细一想,不对啊,这哪来的15个平台?再仔细一看,然后我的心中只剩下一万个省略号了,横跨一堆小程序平台是吧...



学习成本的对比感悟


1. 开发语言的不同

Flutter,要求开发者学习dart,了解dart和flutter的API,最好还会写点原生。而uni-app只需要学Vue.js,没有附加专有技术。所以从学习一个框架来看,很明显uni-app的学习成本很低。而从我个人的角度去分析,当年我只是一个刚入编程世界的菜鸡中的菜鸡,只学了半年的html+css+js和半年的java。抛开学了1个月的SpringBoot,Flutter可以算是我学习的第一个框架,当时我是直接上手学的Flutter,没有去单独学习dart,因为和java很相似。个人觉得学习成本也还好,如果你喜欢这个框架的话~而最近两个月学习uni-app,我也确实是感受到了学习成本很低,基本上看了看文档,就直接上手了,很多组件的名字也是和flutter大差不差。就是写css有点难受🤣,好在flex布局和flutter的rowcolumn用法一样,基本上半小时就能把基本的、简单的页面布局写好了。


2. 第三方插件&社区氛围

截至目前2023.7,flutter在github上有155K的star,uni-app有着38.4K的star。从star的数量也可以看出一个框架的热度,很明显,flutter是远高于uni-app的(毕竟uni-app的主要使用场景还是在国内小程序中)。对于第三方插件呢Flutter有着pub.dev,uni-app有插件市场,但相比Flutter呢可能略显不足。


3. 开发工具的使用

Flutter可以选择vscode或者android studio等来进行开发,uni-app可以选择HBuilderX,当然也可以使用vscode,用什么开发工具其实大差不差,如果你一直使用vscode,那么你对工具的使用会更加的熟悉,而如果你和我一样,用的是android studio,再去使用HBuilderX,说实话,有点点难受...例如我最常用的Alt+回车(提示),crtl+alt+l(代码格式化)。当然,反过来也是一样的(●'◡'●)


编码实现对比


1. 布局区别


  • 代码整体结构:Flutter使用Widget层级嵌套来构建用户界面,也是被很多人所不喜欢的嵌套地狱(这一点因人而异,根据自己的习惯和代码风格)。 uni-app 使用 Vue.js 的组件化布局方式,templatestylescripttemplate 定义了组件的 HTML 结构,style 定义了组件的样式,script 定义了组件的行为。

  • 布局原理区别:Flutter 中的布局是基于约束的,可以使用Constraints来控制小部件的最大和最小尺寸,并根据父级小部件的约束来确定自身的尺寸。uni-app则是,可以使用类似于 CSS 中 Flex 弹性布局的方式来控制组件的排列和布局。通过设置组件的样式属性,如 display: flexflexjustify-content 等,可以实现垂直和水平方向上的灵活布局。当然flutter也有和flex差不多的rowcolumn

  • 自定义布局:Flutter支持自定义布局,可以通过继承 SingleChildLayoutDelegateMultiChildLayoutDelegate 来实现自定义布局,而uni-app目前并没有直接提供类似的专门用于自定义布局的机制,不过uni-app常见的做法是创建一个自定义组件,并在该组件的 template 中使用各种布局方式、样式和组件组合来实现特定的布局效果。


2. 状态管理的区别

Flutter 提供了内置的状态管理机制,最常见的就是通过setState来管理小部件的状态,uni-app是利用Vue.js的响应式数据绑定和状态管理,通过 data 属性来定义和管理组件的状态。


3. 开发语言的区别与联系

区别:众所周知,JavaScript 是一门弱类型的语言,而 Dart 是强类型的语言(dart也支持一些弱类型,Dart中弱类型有var, Object 以及dynamic)。Dart有类和接口的概念,并支持面向对象编程,如果你喜欢 OOP 概念,那么你会喜欢使用 Dart 进行开发,此外,它还支持接口、Mixin、抽象类和静态类型等,这一点对写过java的朋友很友好,而JavaScript则支持基于原型的面向对象编程。Dart和JavaScript还有一个重要的区别就是:Dart是类型安全的,使用AOT和JIT编译器编译。


联系:从一个学习这个两个语言的角度去看, 两者都支持异步编程模型,如 Dart 的 async/await和 JavaScript 的 Promiseasync/await,这就非常友好了。


4. 一个简单的计数器例子,更好的理解他们直接的区别以及相关的地方:


Flutter代码:


import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
)
;
}
}

class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;

@override
State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
int _counter = 0;

void _incrementCounter() {
setState(() {
_counter++;
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'
You have pushed the button this many times:',
)
,
Text(
'$_counter',
style:
Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton:
FloatingActionButton(
onPressed: _incrementCounter,
tooltip: '
Increment',
child: const
Icon(Icons.add),
),
)
;
}
}

uniapp代码:


<template>
<view class="container">
<text class="count">{{ count }}text>
<view class="buttons">
<button class="btn" @tap="incrementCounter">+button>
view>
view>
template>

<script>
export default {
data() {
return {
count: 0,
};
},
methods: {
incrementCounter() {
this.count++;
},
},
};
script>

<style>
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
height: 100vh;
background-color: #f0f0f0;
}

.count {
display: flex;
justify-content: center;
align-items: center;
font-size: 48px;
font-weight: bold;
height: 100%;
}

.buttons {
display: flex;
width: 100vw;
flex-direction: row;
justify-content: flex-end;
}

.btn {
width: 108rpx;
height: 108rpx;
font-size: 24px;
display: flex;
justify-content: center;
align-items: center;
margin: 8px;
background-color: #2196F3;
color: #fff;
border-radius: 50%;
}
style>

总结


从App开发的角度来看,uni-app的最大价值在于让国内庞大的Vue开发群体也能够轻松地开发“高性能”的App,不用去承担flutter或react native的学习成本,短时间内开发一款简单的偏展示类的app的话,uni-app肯定是首选,小公司应该挺受益的。再加上uni-app可以同时开发多端小程序,就足以保证在国内有足够的市场。但是稍微有点动效或者说有video、map之类的app,那么要慎重考虑,个人觉得挺限制的。不过很多时候技术并不是一个项目选型第一标准,适合才是,uni-app很适合国内,毕竟试错成本低...


注:本文仅为一个写了几年flutter小伙,突然写了2个月uniapp的感悟,存在一定个人主观,有错误欢迎指出😘

作者:编程的平行世界
来源:juejin.cn/post/7261162911615926331

收起阅读 »

优化重复冗余代码的8种方式

前言 大家好,我是田螺。好久不见啦~ 日常开发中,我们经常会遇到一些重复代码。大家都知道重复代码不好,它主要有这些缺点:可维护性差、可读性差、增加错误风险等等。最近呢,我优化了一些系统中的重复代码,用了好几种的方式。感觉挺有用的,所以本文给大家讲讲优化重复代码...
继续阅读 »

前言


大家好,我是田螺。好久不见啦~


日常开发中,我们经常会遇到一些重复代码。大家都知道重复代码不好,它主要有这些缺点:可维护性差、可读性差、增加错误风险等等。最近呢,我优化了一些系统中的重复代码,用了好几种的方式。感觉挺有用的,所以本文给大家讲讲优化重复代码的几种方式。

  • 抽取公用方法
  • 抽个工具类
  • 反射
  • 泛型
  • 继承和多态
  • 设计模式
  • 函数式
  • AOP

1. 抽取公用方法


抽取公用方法,是最常用的代码去重方法~


比如这个例子,分别遍历names列表,然后各自转化为大写和小写打印出来:


public class TianLuoExample {

public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "TianLuo");

System.out.println("Uppercase Names:");
for (String name : names) {
String uppercaseName = name.toUpperCase();
System.out.println(uppercaseName);
}

System.out.println("Lowercase Names:");
for (String name : names) {
String lowercaseName = name.toLowerCase();
System.out.println(lowercaseName);
}
}
}

显然,都是遍历names过程,代码是重复的,只不过转化大小写不一样。我们可以抽个公用方法processNames,优化成这样:


public class TianLuoExample {

public static void processNames(List<String> names, Function<String, String> nameProcessor, String processType) {
System.out.println(processType + " Names:");
for (String name : names) {
String processedName = nameProcessor.apply(name);
System.out.println(processedName);
}
}

public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "TianLuo");

processNames(names, String::toUpperCase, "Uppercase");
processNames(names, String::toLowerCase, "Lowercase");
}
}

2. 抽工具类


我们优化重复代码,抽一个公用方法后,如果发现这个方法有更多共性,就可以把公用方法升级为一个工具类。比如这样的业务场景:我们注册的时候,修改邮箱,重置密码等,都需要校验邮箱


实现注册功能时,用户会填邮箱,需要验证邮箱格式


public class RegisterServiceImpl implements RegisterService{
private static final String EMAIL_REGEX =
"^[A-Za-z0-9+_.-]+@(.+)$";

public boolean registerUser(UserInfoReq userInfo) {
String email = userInfo.getEmail();
Pattern pattern = Pattern.compile(EMAIL_REGEX);
Matcher emailMatcher = pattern.matcher(email);
if (!emailMatcher.matches()) {
System.out.println("Invalid email address.");
return false;
}

// 进行其他用户注册逻辑,比如保存用户信息到数据库等
// 返回注册结果
return true;
}
}

密码重置流程中,通常会向用户提供一个链接或验证码,并且需要发送到用户的电子邮件地址。在这种情况下,也需要验证邮箱格式合法性


public class PasswordServiceImpl implements PasswordService{

private static final String EMAIL_REGEX =
"^[A-Za-z0-9+_.-]+@(.+)$";

public void resetPassword(PasswordInfo passwordInfo) {
Pattern pattern = Pattern.compile(EMAIL_REGEX);
Matcher emailMatcher = pattern.matcher(passwordInfo.getEmail());
if (!emailMatcher.matches()) {
System.out.println("Invalid email address.");
return false;
}
//发送通知修改密码
sendReSetPasswordNotify();
}
}

我们可以抽取个校验邮箱的方法出来,又因为校验邮箱的功能在不同的类中,因此,我们可以抽个校验邮箱的工具类


public class EmailValidatorUtil {
private static final String EMAIL_REGEX =
"^[A-Za-z0-9+_.-]+@(.+)$";

private static final Pattern pattern = Pattern.compile(EMAIL_REGEX);

public static boolean isValid(String email) {
Matcher matcher = pattern.matcher(email);
return matcher.matches();
}
}

//注册的代码可以简化为这样啦
public class RegisterServiceImpl implements RegisterService{

public boolean registerUser(UserInfoReq userInfo) {
if (!EmailValidatorUtil.isValid(userInfo.getEmail())) {
System.out.println("Invalid email address.");
return false;
}

// 进行其他用户注册逻辑,比如保存用户信息到数据库等
// 返回注册结果
return true;
}
}

3. 反射


我们日常开发中,经常需要进行PO、DTO和VO的转化。所以大家经常看到类似的代码:


    //DTO 转VO
public UserInfoVO convert(UserInfoDTO userInfoDTO) {
UserInfoVO userInfoVO = new UserInfoVO();
userInfoVO.setUserName(userInfoDTO.getUserName());
userInfoVO.setAge(userInfoDTO.getAge());
return userInfoVO;
}
//PO 转DTO
public UserInfoDTO convert(UserInfoPO userInfoPO) {
UserInfoDTO userInfoDTO = new UserInfoDTO();
userInfoDTO.setUserName(userInfoPO.getUserName());
userInfoDTO.setAge(userInfoPO.getAge());
return userInfoDTO;
}

我们可以使用BeanUtils.copyProperties() 去除重复代码BeanUtils.copyProperties()底层就是使用了反射


    public UserInfoVO convert(UserInfoDTO userInfoDTO) {
UserInfoVO userInfoVO = new UserInfoVO();
BeanUtils.copyProperties(userInfoDTO, userInfoVO);
return userInfoVO;
}

public UserInfoDTO convert(UserInfoPO userInfoPO) {
UserInfoDTO userInfoDTO = new UserInfoDTO();
BeanUtils.copyProperties(userInfoPO,userInfoDTO);
return userInfoDTO;
}

4.泛型


泛型是如何去除重复代码的呢?给大家看个例子,我有个转账明细和转账余额对比的业务需求,有两个类似这样的方法:


private void getAndUpdateBalanceResultMap(String key, Map<String, List> compareResultListMap,
List balanceDTOs
) {
List<TransferBalanceDTO> tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(balanceDTOs);
compareResultListMap.put(key, tempList);
}

private void getAndUpdateDetailResultMap(String key, Map<String, List> compareResultListMap,
List detailDTOS
) {
List<TransferDetailDTO> tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(detailDTOS);
compareResultListMap.put(key, tempList);
}

这两块代码,流程功能看着很像,但是就是不能直接合并抽取一个公用方法,因为类型不一致。单纯类型不一样的话,我们可以结合泛型处理,因为泛型的本质就是参数化类型.优化为这样:


private  void getAndUpdateResultMap(String key, Map<String, List> compareResultListMap, List accountingDTOS) {
List tempList = compareResultListMap.getOrDefault(key, new ArrayList<>());
tempList.addAll(accountingDTOS);
compareResultListMap.put(key, tempList);
}

5. 继承与多态


假设你正在开发一个电子商务平台,需要处理不同类型的订单,例如普通订单和折扣订单。每种订单都有一些共同的属性(如订单号、购买商品列表)和方法(如计算总价、生成订单报告),但折扣订单还有特定的属性和方法


在没有使用继承和多态的话,会写出类似这样的代码:


//普通订单
public class Order {
private String orderNumber;
private List products;

public Order(String orderNumber, List products) {
this.orderNumber = orderNumber;
this.products = products;
}

public double calculateTotalPrice() {
double total = 0;
for (Product product : products) {
total += product.getPrice();
}
return total;
}

public String generateOrderReport() {
return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice();
}
}

//折扣订单
public class DiscountOrder {
private String orderNumber;
private List products;
private double discountPercentage;

public DiscountOrder(String orderNumber, List products, double discountPercentage) {
this.orderNumber = orderNumber;
this.products = products;
this.discountPercentage = discountPercentage;
}

public double calculateTotalPrice() {
double total = 0;
for (Product product : products) {
total += product.getPrice();
}
return total - (total * discountPercentage / 100);
}
public String generateOrderReport() {
return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice();
}
}

显然,看到在OrderDiscountOrder类中,generateOrderReport() 方法的代码是完全相同的。calculateTotalPrice()则是有一点点区别,但也大相径庭。


我们可以使用继承和多态去除重复代码,让DiscountOrder去继承Order,代码如下:


public class Order {
private String orderNumber;
private List products;

public Order(String orderNumber, List products) {
this.orderNumber = orderNumber;
this.products = products;
}

public double calculateTotalPrice() {
double total = 0;
for (Product product : products) {
total += product.getPrice();
}
return total;
}

public String generateOrderReport() {
return "Order Report for " + orderNumber + ": Total Price = $" + calculateTotalPrice();
}
}

public class DiscountOrder extends Order {
private double discountPercentage;

public DiscountOrder(String orderNumber, List products, double discountPercentage) {
super(orderNumber, products);
this.discountPercentage = discountPercentage;
}

@Override
public double calculateTotalPrice()
{
double total = super.calculateTotalPrice();
return total - (total * discountPercentage / 100);
}
}

6.使用设计模式


很多设计模式可以减少重复代码、提高代码的可读性、可扩展性.比如:



  • 工厂模式: 通过工厂模式,你可以将对象的创建和使用分开,从而减少重复的创建代码

  • 策略模式: 策略模式定义了一族算法,将它们封装成独立的类,并使它们可以互相替换。通过使用策略模式,你可以减少在代码中重复使用相同的逻辑

  • 模板方法模式:模板方法模式定义了一个算法的骨架,将一些步骤延迟到子类中实现。这有助于避免在不同类中重复编写相似的代码


我给大家举个例子,模板方法是如何去除重复代码的吧,业务场景:



假设你正在开发一个咖啡和茶的制作流程,制作过程中的热水和添加物质的步骤是相同的,但是具体的饮品制作步骤是不同的



如果没有使用模板方法模式,实现是酱紫的:


public class Coffee {
public void prepareCoffee() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addCondiments();
}

private void boilWater() {
System.out.println("Boiling water");
}

private void brewCoffeeGrinds() {
System.out.println("Brewing coffee grinds");
}

private void pourInCup() {
System.out.println("Pouring into cup");
}

private void addCondiments() {
System.out.println("Adding sugar and milk");
}
}

public class Tea {
public void prepareTea() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}

private void boilWater() {
System.out.println("Boiling water");
}

private void steepTeaBag() {
System.out.println("Steeping the tea bag");
}

private void pourInCup() {
System.out.println("Pouring into cup");
}

private void addLemon() {
System.out.println("Adding lemon");
}
}

这个代码例子,我们可以发现,烧水和倒入杯子的步骤代码,在CoffeeTea类中是重复的。


使用模板方法模式,代码可以优化成这样:


abstract class Beverage {
public final void prepareBeverage() {
boilWater();
brew();
pourInCup();
addCondiments();
}

private void boilWater() {
System.out.println("Boiling water");
}

abstract void brew();

private void pourInCup() {
System.out.println("Pouring into cup");
}

abstract void addCondiments();
}

class Coffee extends Beverage {
@Override
void brew() {
System.out.println("Brewing coffee grinds");
}

@Override
void addCondiments() {
System.out.println("Adding sugar and milk");
}
}

class Tea extends Beverage {
@Override
void brew() {
System.out.println("Steeping the tea bag");
}

@Override
void addCondiments() {
System.out.println("Adding lemon");
}
}

在这个例子中,我们创建了一个抽象类Beverage,其中定义了制作饮品的模板方法 prepareBeverage()。这个方法包含了烧水、倒入杯子等共同的步骤,而将制作过程中的特定步骤 brew() 和 addCondiments() 延迟到子类中实现。这样,我们避免了在每个具体的饮品类中重复编写相同的烧水和倒入杯子的代码,提高了代码的可维护性和重用性。


7.自定义注解(或者说AOP面向切面)


使用 AOP框架可以在不同地方插入通用的逻辑,从而减少代码重复。


业务场景:


假设你正在开发一个Web应用程序,需要对不同的Controller方法进行权限检查。每个Controller方法都需要进行类似的权限验证,但是重复的代码会导致代码的冗余和维护困难


public class MyController {
public void viewData() {
if (!User.hasPermission("read")) {
throw new SecurityException("Insufficient permission to access this resource.");
}
// Method implementation
}

public void modifyData() {
if (!User.hasPermission("write")) {
throw new SecurityException("Insufficient permission to access this resource.");
}
// Method implementation
}
}

你可以看到在每个需要权限校验的方法中都需要重复编写相同的权限校验逻辑,即出现了重复代码.我们使用自定义注解的方式能够将权限校验逻辑集中管理,通过切面来处理,消除重复代码.如下:


@Aspect
@Component
public class PermissionAspect {

@Before("@annotation(requiresPermission)")
public void checkPermission(RequiresPermission requiresPermission) {
String permission = requiresPermission.value();

if (!User.hasPermission(permission)) {
throw new SecurityException("Insufficient permission to access this resource.");
}
}
}

public class MyController {
@RequiresPermission("read")
public void viewData() {
// Method implementation
}

@RequiresPermission("write")
public void modifyData() {
// Method implementation
}
}

就这样,不管多少个Controller方法需要进行权限检查,你只需在方法上添加相应的注解即可。权限检查的逻辑在切面中集中管理,避免了在每个Controller方法中重复编写相同的权限验证代码。这大大提高了代码的可读性、可维护性,并避免了代码冗余。


8.函数式接口和Lambda表达式


业务场景:



假设你正在开发一个应用程序,需要根据不同的条件来过滤一组数据。每次过滤的逻辑都可能会有些微的不同,但基本的流程是相似的。



没有使用函数式接口和Lambda表达式的情况:


public class DataFilter {
public List<Integer> filterPositiveNumbers(List numbers) {
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (number > 0) {
result.add(number);
}
}
return result;
}

public List<Integer> filterEvenNumbers(List numbers) {
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (number % 2 == 0) {
result.add(number);
}
}
return result;
}
}

在这个例子中,我们有两个不同的方法来过滤一组数据,但是基本的循环和条件判断逻辑是重复的,我们可以使用使用函数式接口和Lambda表达式,去除重复代码,如下:


public class DataFilter {
public List<Integer> filterNumbers(List numbers, Predicate predicate) {
List<Integer> result = new ArrayList<>();
for (Integer number : numbers) {
if (predicate.test(number)) {
result.add(number);
}
}
return result;
}
}


我们将过滤的核心逻辑抽象出来。该方法接受一个 Predicate函数式接口作为参数,以便根据不同的条件来过滤数据。然后,我们可以使用Lambda表达式来传递具体的条件,这样最终也达到去除重复代码的效果啦.


最后


我是捡田螺的小男孩,大家如果觉得看了本文有帮助的话,麻烦给个三连(点赞、分享、转发)支持一下哈。最近我在工作中,用了其中的几种方式,去优化重复代码。下一篇文章,我打算出一篇后端思维系列的文章,基于业务代码,手把手教大家去除重复代码哈。一起加油~~

作者:捡田螺的小男孩
来源:juejin.cn/post/7270026656663322685

收起阅读 »

消息太大,kafka受不了

前言 上周在进行自测的时候,kafka抛出一个RecordTooLargeException异常,从名字我们可以直接看出是消息太大了,导致发不出去而抛出异常,那么怎么应该怎么解决这个问题呢,其实很简单,要么将消息拆分得小一点,要么调节kafka层面的参数,依然...
继续阅读 »

前言


上周在进行自测的时候,kafka抛出一个RecordTooLargeException异常,从名字我们可以直接看出是消息太大了,导致发不出去而抛出异常,那么怎么应该怎么解决这个问题呢,其实很简单,要么将消息拆分得小一点,要么调节kafka层面的参数,依然它抛出这个异常,那么就证明超过了某个参数的阈值,由此我们可以有两种方式来处理这个问题,但是一切还要从我们的业务背景和数据结构去看这个问题。


业务背景


我们这边会将数据写入文件,通过FTP的方式,没产生数据,就往FTP里面追加,而这些数据都是需要保证不丢失的,由于业务的发展,我这边需要专门去处理这些文件,然后通过kafka投递给下游系统,所以自然需要解析文件,还得一条一条的解析后发送。


问题出现


一开始我看到文件都比较小,所以处理方式是只有这个文件的数据全部解析完成并成功投递kafka,那么我这边才记录这个文件处理成功,但是处理了很多个大文件过后,发现数据条数对不上,看日志是RecordTooLargeException异常,因为上面的处理方式是文件处理完成并全部投递到kafka才记录文件解析完成,所以这是有问题的,一个大文件可能有即使上百万条数据,难免会遇到很大的数据,所以只要一条没解析成功,那么后面的数据就不去解析了,这个文件就不算解析成功,所以应该要设计容错,并对数据进行监控和补偿。


处理问题


在得知是某些数据过大的问题,我就DEBUG去看源码,在kafka生产端的KafkaProducer类中,发现问题出在下面这方法中。



ensureValidRecordSize方法就是对消息的大小进行判断的,参数size就是我们所发送的消息的字节数,maxRequestSize就是允许消息的最大字节,因为没有进行设置,所以这个值使用的是默认值,默认为1M,所以就应该将maxRequestSize这个参数进行重新设置。


因为我们使用的是SpringBoot开发,于是通过yml方式配置,但是发现spring-kafka没提示这个属性,于是只有写一个Kafka的配置类,然后再读取yml文件内容进行配置


配置类


yml文件



通过上面的配置后,我们看到我将max.request.size参数的值设置为10M,这需要根据实际情况来,因为我在处理的过程中发现像比较大的数据行也只有个别。


如果在实际使用过程中数据比较大,那么可能需要拆分数据,不过如果数据不能拆分,那么我们应该考虑消息压缩方式,将数据压缩后再发送,然后在消费者进行解压,不过这种压缩是我们自己实现的,并不是kafka层面的压缩,kafka本身也提供了压缩功能,有兴趣可以了解一下。


扩展


上面设置了max.request.size参数,我们在上面的截图代码中看到第二个判断中有一个参数totalMemorySize,这个值是缓冲区大小,我们发送的消息并不会马上发送kafka服务端,而是会先放在内存缓冲区,然后kafka通过一个线程去取,然后发送,可通过buffer.memory设置,这个值的默认值为32M,所以我们在设置max.request.size的时候也要考虑一下这个值。


总结


有必要对kafka进行比较深一点的学习,这样在出现问题的时候能够快速定位,并且合理解决,当然,在业务处理的时候要充分考虑可能出现的问题,做好容错和相应的补偿方案。


今天的分享就到这里,感谢你的观

作者:刘牌
来源:juejin.cn/post/7269745800178286627
看,我们下期见

收起阅读 »

从读《微信背后的产品观》到思考前端工程师的“35岁”

我是一名前端开发者,同时兼任pm职责,近半年在公司负责升级一直在开发与运营的一个B端的Saas商城系统; 在对需求剖析、需求抽象、每个字段含义的推敲的时候,我越发对微信这样简洁、自然的产品产生兴趣与共鸣。 也会情不自禁的赞叹微信清晰明了、谨慎内敛的结构化产品思...
继续阅读 »


我是一名前端开发者,同时兼任pm职责,近半年在公司负责升级一直在开发与运营的一个B端的Saas商城系统;


在对需求剖析、需求抽象、每个字段含义的推敲的时候,我越发对微信这样简洁、自然的产品产生兴趣与共鸣。


也会情不自禁的赞叹微信清晰明了、谨慎内敛的结构化产品思路。


虽然微信这么多年上线了如此多功能与特性,但是产品简洁而克制的灵魂从来没有改变,哪怕对于60岁的人来说,也几乎不存在用不好微信的情况。


而我做为微信的使用者,广义技术上的开发者,无论是产品还是技术维度,都让我越加佩服与引起共鸣(恶心的小程序开发除外)。


昨天终于行动了起来,把这本知名的 《微信背后的产品观》 找出来并读了起来,与其叫一本书,不如说是一个演讲的记录,很短,大约2小时就读完了,主要内容是2012年微信4.0发布时候,张小龙长达8个多小时的公开演讲的内容;


不同于其他产品经理的书籍,他们会告诉你各种方法论、科学分析方法,张小龙截然相反的采用了一种极其浪漫的方式去看待产品,去理解所谓的用户需求,我认为这种产品思路的领先是微信这么多年在社交领域立于不败之地的根本。


虽然在最后,张小龙看似补刀似的说:我所说的,都是错的;但是这恰恰就是他的产品理念,所谓产品没有任何科学方法,完全来自于对人,对人类群体的理解,对自己的拷问与质疑。


我也斗胆推荐大家看看这本书,很快就能看完,人类之所以存在信息差,就是因为不知道,或许看完这本书,你会打开新的思路,某些问题也能豁然开朗。


借此机会,我也想与大家分享一下,这几年我作为一名前端开发者的迷茫与努力


业务前端开发者的困境



我只是一名普通学历,普通的业务前端开发者,所以以下仅是我的个人感觉,不代表所有前端开发者。



目前在一个小型互联网公司的saas电商部门下,主要职责是前端开发组长,我们公司的主营业务不是saas电商,所以这几年算不上受到到很强的市场冲击,平时会管理几人的小团队,我18年毕业至今,一直在这家公司。


而我大学毕业之后一直从事前端开发方向,在我工作1-3年的阶段,我都保持着对技术的热情,主要是因为尝到了学习技术的甜头,那时候我坚定的认为下一个阶段是全栈,在工作之余我花费了大量的精力学习技术;但是后面我就发现了一个很现实的问题,公司需要大家更好分工协作,所以高级别的项目后端是绝对轮不到一个前端去开发开发;并不是说能力不行,而是人的精力有限,前后端都干,还要管团队,是忙不过来的。


在这样的环境下,关于全栈技术的学习,我也越发疑虑,逐渐走到了大多数业务前端开发者的临界点。



  • 业务前端的35岁危机在普通人身上是存在的,我们的年纪、精力、外部压力都不允许你永远征战一线,并始终保持高竞争力。

  • 业务前端的上限很低,大多数努力的前端开发者可以在3 - 5年内触摸到业务前端的上限,职位也就是前端组长。

  • 技术纵深是很好的选择,但是受限于综合实力(英语、计算机基础、天分),普通人可以达到的纵深远比想象的要浅。

  • 技术学会了,但是用不上,也会慢慢被遗忘;demo级别的应用无法让开发者对某一项技术有深刻理解。

  • 在业务开发场景下的前端,永远是没有灵魂的大头兵,上限低就意味着待遇相对较低、可替代性相对较强。

  • 代码写的越多,与人交流的机会越少,对于几十年的人生而言,这是一件很没安全的事情。


总结一下,就是因为兴趣而走的前端技术路径开始越来越窄,前路开始越来越看不清,时间推着我向前,这不禁让我低头沉思,下一步究竟要怎么走?


说回产品



到目前为止我也依旧不确定前路怎么走,接下来的一些结论,只是我的一些探索。



在大学后期,我隐约感觉技术路线并非我所擅长的时候,我有目的的学习了微观经济学、企业管理、竞争战略相关的知识,从而间接接触到了互联网产品,也就是pm。


我很快就感受到了pm的魅力:创造,我恰好是一个喜欢新鲜事物的人,在技术上总喜欢优化、迭代、升级,亲手构建出优美并且有价值的产品极具满足感,而前端开发者与用户的距离比任何一个岗位都要近,甚至可以说:前端开发者决定了用户体验。


如果一个人同时具备前端开发 + 产品的能力是不是还不错?



  • 消除技术与产品的认知壁垒,后续我们做到了,在我们公司,产品和技术从来不吵架

  • 如果有能力决定产品走向,开发者就可以是一名有灵魂的大头兵,甚至晋升军衔。


也就是说,我逐渐不再下探技术,而是走向用户


走向用户,并不意味着开发者要放弃对技术的学习,技术很重要,技术能力依旧是核心竞争力,而是随着我对技术的理解逐渐深入,开始越发清晰的了解到,究竟什么样的能力是前端开发者最需要的,什么样的能力边际收益是最高的。



在我从事前端开发第2年至今,我一直都有在产品这方面作出努力与尝试。


关于这几年产品的结果,大概可以用这句话来形容:


100个想法中,80个想法死在在调研与分析阶段,15个想法死在在demo阶段,最后落地5个想法,其中4个反响平平,只有1个还算成功。


虽然绝大部分都是失败,但是站在此刻回头来看,这几年的产品的学习将我的思维高度提升了很多,综合能力也提升了很多,因为很多想法初期是没有团队介入的,凡事都需要亲力亲为,需要思考需求、写最小demo、UI设计、沟通,而上线后,有需要又要为产品负责,就需要进行数据分析,线上数据的观察,等等....,这其实比写代码累多了。


这些进步不像程序的学习有一个可量化的指标,这样的软实力很多的是一种感觉,虽然依旧是时而迷茫,但是也偶尔会有一些收获。


虽然我做的产品决策越来越多,公司与同事给予的信任也越来越多,所以在产品上的舞台也是越来越大,而看了《微信背后的产品观》,里面的想法非常符合我对产品的理解,当然我的理解是相对浅显与张小龙没法比的,不过张小龙对产品的理念,以及他对需求的理解,这样一套浪漫的方法论,真的非常有魅力,这也是为什么,我看完最后决定写这篇文章。


跨越“技术”思维


这几年,我的老板经常会找我聊天,因为我和他提过对产品很有兴趣,在前两年,他反复和我提一句话技术为业务服务,我当时觉得我理解这句话了。


我想,这不是废话,写代码最终都是为了公司的项目,为了更好更快的完成公司需求,我要狠狠的学习技术。


之后来随着我写的代码越来越多,我对这句话逐渐有了新的理解。


之前过于执着于技术,总是站在写代码的角度去理解,而这句话的侧重点是业务,或者我们换个词交付


并不是技术推动交付,而是在推动交付的因素中,技术是其中之一,我们可以衍生出很多类似的话;设计为业务服务、产品为业务服务....


所以技术的目的并非技术,而是交付价值。


人们总是不自知的放大自己在团队中的价值,这样只会蒙蔽大家的视野,走到更高处,对很多事物将会有不一样的理解。



低纬度的技术思维,走向高纬度的业务(交付)思维。


最后


其实我本来只是觉得读了还不错的一本书,不复盘、不留下点什么反思会达不到学习的目的,写着写着就想到了自己的职业,想到了这几年的经历;


总结性的话不说太多,希望可以帮助到屏

作者:狗阿木
来源:juejin.cn/post/7270331527506100264
幕前你,我们共同成长,共同进步。

收起阅读 »

人情世故职场社会生存实战篇(一)

这个专栏文章都是群友问的实际问题,我在这里分享出来,希望可以帮助到大家,欢迎大家踊跃发言 1、问:老师,我们领导今天请我吃饭,我送了两瓶梦之蓝,一条中华,临走他问了我一下这酒多少钱,有点奇怪,他问酒多少钱是什么意思?另外走的时候非要给我一箱枣,我感觉去了人家家...
继续阅读 »

这个专栏文章都是群友问的实际问题,我在这里分享出来,希望可以帮助到大家,欢迎大家踊跃发言


1、问:老师,我们领导今天请我吃饭,我送了两瓶梦之蓝,一条中华,临走他问了我一下这酒多少钱,有点奇怪,他问酒多少钱是什么意思?另外走的时候非要给我一箱枣,我感觉去了人家家里提东西很正常,拿人家的枣是不礼貌的,何况不值两个钱,有点奇怪。


答:领导问你这两瓶酒多少钱?他想听到的答案是:这两瓶酒是朋友送的,我也不喜欢喝酒,麻烦你帮我消化一下吧。领导想听这句话,醉翁之意不在酒嘛。临走的时候他把这个大枣送给你,这个枣肯定是别人送给他的,这个大枣他不想要,他想让你帮他消化一 下,那这个枣你是要接住的,你说太好了,我老婆最喜欢吃枣了,这样的话,你就满足了领导的精神需求,他就感觉到你特别懂事儿。也就是领导想跟你做一个良性的循环和互动。


2、问:老大经常分给我费时又费力的垃圾项目,别的同事都是简单高效的赚钱项目,我认为是随机分的,我该怎么拒绝领导的安排啊?


答:你手里有2个项目,一个可以轻轻松松赚10万,一个累死才能赚8000。现在有人找你要项目?有人送了你阿弥陀佛,有人送了你烟酒,有人送了你美女,有人送了你5W真金白银,有人送了你烟酒茶美女+五W。最后你把项目给谁啊?你觉得是随机的,你急什么啊?下次能不能轮到你啊?找人算一卦试试,比如有些考试啊,面试和笔试都是私人订制,然后拉一群沙雕陪着走个形式,这样显得更公平。公平是相对而言,不是绝对的。


3、问:下周即将和我们公司大老板开会,参会的有同级别的其他中心经理,还有我们的直属总监,要求我们经理都说一下各自中心今年的工作计划。请教您,在会议上说工作计划,有没有什么技巧?有需要注意的地方吗?


答:1、领导安排的工作,我全落实了。


2、领导的目标也是我的目标,领导安排什么,我做什么,我力求每一步工作都做扎实 ,都做到位。


3、我会跟领导搞好关系,我会跟同事搞好关系,在领导的帮助下,还有同事的帮助下,去为公司创 造更多的利润。


就是用这三句话,在里面转圈圈就行了。


4、问:请教一个事,我是一个老师,我平时很注重提升自己,会不断学习各种课程,所以经常会在办公室学到很晚,然后我感觉有同事在背后不断嘲讽我,她们经常问我,你不是晚自习没有课程吗,怎么还在这里呀,就是类似这种,我感觉很反感。


答:同事为什么会在背后嘲讽你呀,因为你不懂人性。你要进步没有错,但你不能给身边的人压力。你要进步,在家里你学到凌晨2点都无所谓,但在公共场合,不要展现你的野心,如此会给别人造成心理上的压迫感。其次你要学会回话,会示弱。人家问你:晚上不是没课程吗?怎么还在这里呀?你说,我笨鸟先飞,下周的课我要提前准备,XX老师你的教案可以借我参考吗,听其他老师说,你的教案做的很好。你这样子给她回话,以后,她就不会在背后说你坏话了。


5、问:在单位,直接大领导一直很挺我,因为各种原因并没有走得很近,单位副职一直不太认可我。这次选上了大领导的秘书(有竞争对手的情况下),我是否应该送礼给大领导?


答:在这种情况下,你不要送礼,这时候送礼对你来说根本不是加分,他反而减分。万一被你的竞争对手看到呢,把你给举报了,肯定对你是不利的,那你这个时候应该怎么样做呢,非常简单。以前怎么样,现在你还怎么样,你现在不要乱了这个方寸。你要是送礼表态的话,你早都应该送了,你非要等到最后再送礼,那人家觉得你临时抱佛脚,你这个人怎么那么势利啊?这个时候,你要做的,你想一想,你能够帮他们做点什么,为他们创造一些什么价值,这些价值有可能是物质方面的,有可能是精神层面的,就是你要一直旺他们,替他们排忧解难,你就顺着这个点往前跑,你100%的能立起来。


6、问:如果领导和你聊骚,你又不想牺牲自己,你该怎么办?


答:只要你在一个位阶和势差比你大的人手下工作,他们总能创造各种有利的机会。男人最懂男人,狗改不了吃屎,你只要被得手了,对于他来说,你的结局大致已经有一个雏形了。如果他和你聊骚,你就把他对你的聊骚内容截图,他发一次,你截图一次,找个隐秘的空间保存起来。如果他不微信,打电话怎样?录音啊,傻呀!以后有需要动动或者求到他的地方,没有什么比这个更有分量了。或者,你直接点他一下,你说,领导,给我发的信息,我好多都截图(录音)保存了,以后我不想再存了,到此为止吧。领导我相信您是一个睿智的人,希望能跟您多聊点正面的,积极的东西。


7、问:如何搞关系?


关系是跑出来的,跑关系三个字,点出了人际交往的秘密。关系不是等出来的,是天天跑,多多跑,关系就有了。好处有多到位,机会就有多到位。很多傻子被君子之交淡如水严重洗脑,脑子坏掉了,不知道君子只出现在书里面,忘记了人情往来中的尊重和礼仪。


处处感恩,处处逢源,处处遇贵人。时时送出好处,处处遇见活雷锋。不要空手套白狼、四两拨千斤、以小博大,谁这么教你谁就是把你当成傻X呢。有求于人的的时候,就要舍得投入,舍得砸钱,舍得亲近。这个时候不舍不得,等事情过去了,再怎么投入,也意义不大了。


求人办事,最忌讳临时抱佛脚。如果遇到这种情况,最好找个中间人帮忙。备好双份礼物,一份给引荐人,一份给办事人。但是一定要明白,如果事情事能够办下来,并不是因为礼物,而是中间人的关系网与情面的作用。所以啊,对咱有用的人,必须要细水长流地去维护。人生在世,需要有靠山。


天生的靠山可遇不可求,可求的靠山都是自己主动经营来的。你打牌、抽烟、喝酒,不是因为喜欢,而是因为别人喜欢。只做自己喜欢的事,只能和自己玩,只有做了别人喜欢的事,才能和别人玩到一块。


8、问:老师,为什么你说能不送礼就尽量不要送礼?


答:能不送礼就不送礼。尤其是职场。送了,就要持续送。送着送着,你不送了,收礼的人觉得你看不起他了,你不重视他了,你过桥拆河。以后他会弄你。你是怎么死的,都不知道。咱不想送礼了,咱遇到对方了,咱彬彬有礼就行了。不要做啥深度链接。你用不上他,为什么给他送礼呀,难道你贱啊?职场中,很多人,一开始觉得好玩,也想锻炼一下自己,就去给领导送礼了,送了一年,发现自己的福利没啥变化,就不想送了,第二年,他发现他过得比以前更不好了,他也不知道为什么!关于送礼,第一,我的看法就是,能不送,就不送,送了,就不能断,你送着送着不送了,领导觉得你是不是看不起他了,还是觉得他没用?啥,既然觉得我没用了,那我就要拿出点手段让你重新认识我!人心难测!


9、问:我们公司现在在搞改革,很多岗位会涉及到变动,甚至被安排分配到下一级公司干活。现在新来的上司是我四年前的老领导,他之前被调去别的单位了。我跟他以前关系蛮好,期间就过节短信问候,现在我想找他,让他帮我继续留在现在这个层级干活,你觉得他会帮我嘛?要怎么开口?


答:这个帮不帮暂时还不知道,你需要带上心意,烧香拜神 投石问路。你现在也犯了多数人都会犯的错误,以至于到了关键时刻非常被动。


第一个就是临时抱佛脚,看的不够远,总以为太阳可以一直照耀着你,不懂的提前去布局,铺设你日后能用得到的关系站点,导致你现在关键性的节点就缺乏了很重要的助力。


第二个,那就是不懂的主动去搭关系,你的老领导调回来的那天,你为什么不去拜访、祝贺?为什么不提这猪头去拜庙门?现在才想到人家。不过,如果他之前帮助过你的,一般还会帮助你第二次,第三次,所以,你带上心意,去拜访一下他,事到临头,见面了把礼物放下,你先回望过去,把彼此的关系拉进,然后谈谈自己的想法,你说 老领导,我的事情劳烦您费心了,这事成不成,没有关系,很久没有见过领导了,这点小心意,请领导不要嫌弃。


10、问:我找一个领导帮忙和单位打招呼,因为这一次有大调整,请求帮忙调整一下,之前与帮忙这位领导都是过节礼仪性的人情,这次找这个领导帮忙,领导答应帮忙,简历也给了,但是一直没给反馈,我第二次找,他正好周末和我们单位的领导们一起吃饭,他说会帮我说,现在都没给反馈,是不是我要去意思意思,我又担心送了,万一这领导也没帮上忙咋办?


答:我送你一句话吧:送礼是从承担风险开始的,而不是从刻意追求回报率开始的!其实,在我看来,你不适合送礼,因为你的的觉悟还没有到。觉悟不到的东西,你是拿不住也做不好的。


送礼,原本就是风险投资。咱不是说送礼了,100%有回报率。但送礼有回报率的概率至少在50%以上。上211/985的概率有多少呀?听说985全国只有2%,211占5%(是我粘贴复制过来的,数据真假不知道啊)。都说读书改变命运,但通过读过改变命运的概率有多大?3%。通过送礼改变命运的概率有多大?50%。


生活中,开窍的人非常少。开窍的人,都是直接给我送红砖,说领导辛苦了,事成不成,遇到您认识您,就是我最大的福气。这样的人,都是开窍的人。习惯性承担风险,习惯性承担责任。可惜这样的人实在太少了。


送礼,是从习惯性承担风险开始的。而不是说我送了,对方必须100%给我一个说法。建议你带上心意,去拜访一下他,他收了,会告诉你一两句有用的话,自然,你就知道下一步怎么样了。


欢迎同学们关注本专栏,会持续更新社会上的人情世故,有问题的同学也欢迎留言~


作者:公z号_纵横潜规则
来源:juejin.cn/post/7267576629295022080
>如果对你有帮助的话给个关注吧~

收起阅读 »

七夕节那天,我被裁员了

前言 先祝jym昨天七夕节快乐吧,有情人终成眷属 一直想在掘金上发文但迟迟未行动,上学时在b乎写了一段时间一直切换不来平台。看着b乎仅存的几百号关注者到这边只能从0开始很是不情愿( ̄Д ̄)ノ,也行吧就从零开始了,我的工作似乎也得从零开始了。该死的仪式感... ...
继续阅读 »

前言


先祝jym昨天七夕节快乐吧,有情人终成眷属


一直想在掘金上发文但迟迟未行动,上学时在b乎写了一段时间一直切换不来平台。看着b乎仅存的几百号关注者到这边只能从0开始很是不情愿( ̄Д ̄)ノ,也行吧就从零开始了,我的工作似乎也得从零开始了。该死的仪式感...


image.png


难忘的七夕


如标题所见,七夕节被通知裁员了。单身狗今天受到双重痛击。说的是公司迫于压力需要节流,部门会裁1/4。而我恰好就是那个“幸运儿”。其实从前些时间就早有风向,先是ui,到测试,终于到开发人员了,而到了开发最先动刀的果然还就是鼠鼠前端。


23届真的太惨了。在这里从实习到转正也快干了1年了,但才刚拿了一个月转正工资就遇到调整,实属难绷。


应届生身份也没了,工作年限也达不到,双重叠buff了属于是,老实说刚听到这消息的时候自己还是懵的,也就和往常一样上班怎么今天就这么突然开这样的会议呢?


而后是无奈,但是又有点兴奋,正好可以逼自己再出去外面探探,看看市场如何。

回家路上,打开手机刷了刷boss,要么招24的要么一到两年工作经验的,一下子又把我难住了。。。我怎么这么背啊5555


整个晚上我都在思考人生,我在想假如自己不是读计算机会如何?我是不是能没负担的尝试各种工作,自己真的要在程序员这条赛道一条路走到黑吗?


程序员的工作又何尝不是围墙,技术类的工作是建立起了行业壁垒,在外行人看来能将代码变成程序是一件很酷的事,而正是这道壁垒,让外行的人想进来,里面的人想走却又鼓不起勇气。


image.png


是啊 我除了写代码还能干什么呢?难道真的放弃学了这么久在外行人看来很厉害的技术去做其他工作吗?(脱不下的长衫又穿上了)


总结


我一直认为无论发生什么事,一切都是上天最好的安排,世上无所谓输赢祸福,关键是从中有所收获,写几点感悟吧:



  • 拥抱不确定性是踏入社会后最需要学会的。读书时每个人都是按部就班,除了学习剩下的挑战就是安排到日程上的考试,而步入社会每天遇到的事情都不一样,so be water my friend 随机应变 随遇而安

  • 负能量可以有,适当摆烂一下,但睡一觉起来就得调整好心态了,享受生活不要被蛋疼的事影响

  • 不要畏手畏脚,敞开自己而不是压抑自己,一辈子很短,很多事情不去尝试就来不及了

  • 灵活就业或许才是这个时代下的答案?不知道

  • 行动起来最重要,如前面说的想在掘金发文却拖延了很久,而正是这回吐槽却让我奋笔疾书了起来,命运的齿轮或许就此转动,后面我会督促自己继续更开发过程中的杂记、生活记录

  • 接下来的安排:先编辑个简历再出发,然后整理一下这一年来开发的东西,记录一下


结语


我也不知道后面会如何,走一步是一步吧,通知还没正式下来。如果读到这里的你觉得我我是个有趣的人,那么点个关注吧,这也是我更文的动力


失败并不存在

作者:前端咖啡豆
来源:juejin.cn/post/7270152997166252071
,关键是故事仍在继续......

收起阅读 »

iOS 陀螺仪技术的应用探究

iOS
本文源自本人的学习记录整理与理解,其中参考阅读了部分优秀的博客和书籍,尽量以通俗简单的语句转述。引用到的地方如有遗漏或未能一一列举原文出处还望见谅与指出,另文章内容如有不妥之处还望指教,万分感谢。 前言 陀螺仪是一种硬件传感器,能够感知设备的旋转和方向变化。...
继续阅读 »

本文源自本人的学习记录整理与理解,其中参考阅读了部分优秀的博客和书籍,尽量以通俗简单的语句转述。引用到的地方如有遗漏或未能一一列举原文出处还望见谅与指出,另文章内容如有不妥之处还望指教,万分感谢。



前言


陀螺仪是一种硬件传感器,能够感知设备的旋转和方向变化。它通常通过MEMS(微机电系统)技术来实现,内部包含了微小但高精度的陀螺仪器件、加速度计和磁力计等传感器,可以实时地感知设备在空间中的旋转角度和方向。


在iOS系统中,可以通过CoreMotion框架来访问陀螺仪的数据。在开发iOS应用程序时,可以使用CoreMotion框架提供了一个CMMotionManager类,该类可以用来获取设备的运动数据,包括陀螺仪数据、加速度计数据等。


iOS陀螺仪的精度和灵敏度通常比较高,可以实现比加速度计更加准确的姿态估计和方向识别,也可以帮助开发者实现更加真实的虚拟现实和增强现实应用。同时,iOS陀螺仪的实时响应和低功耗特性,也使得它在移动应用程序开发中得到了广泛的应用和认可。


基础知识


在开发前,有几个基础的知识点,我们需要事先了解,这对我们后期开发会有更好的帮助


三轴方向


在 iOS 中,陀螺仪传感器的三轴方向通常遵循右手系的规则。具体来说:

  • x 轴:表示设备绕着横轴旋转。当设备的屏幕朝上时,x 轴指向设备的右侧;当设备的屏幕朝下时,x 轴指向设备的左侧
  • y 轴:表示设备绕着纵轴旋转。当设备的屏幕朝上时,y 轴指向设备的顶部;当设备的屏幕朝下时,y 轴指向设备的底部
  • z 轴:表示设备绕着竖轴旋转。当设备的屏幕朝上时,z 轴指向设备的正面;当设备的屏幕朝下时,z 轴指向设备的背面



姿态信息


陀螺仪用于侦测设备沿三个轴为中线所旋转时的角速度,故有了三个姿态信息,分别为 pitch (纵倾), roll (横倾) 和 yaw (横摆)

  • pitch(俯仰角):表示设备绕着 x 轴旋转的角度,也称为纵倾角。当设备正面朝上时,俯仰角为 0°;当设备向上仰起时,俯仰角为正值;当设备向下倾斜时,俯仰角为负值
  • roll(横滚角):表示设备绕着 y 轴旋转的角度,也称为横倾角。当设备正面朝上时,横滚角为 0°;当设备向右侧倾斜时,横滚角为正值;当设备向左侧倾斜时,横滚角为负值
  • yaw(偏航角):表示设备绕着 z 轴旋转的角度,也称为横摆角。当设备正面朝北时,偏航角为 0°;当设备逆时针旋转时,偏航角为正值;当设备顺时针旋转时,偏航角为负值
  • CMRotationMatrix 结构体表示设备绕X、Y、Z轴的旋转矩阵,可用于描述设备在三维空间中的方向和旋转状态, 这里再细讲该结构体中9个元素所代表的含义


陀螺仪的使用

import CoreMotion

let motionManager = CMMotionManager()
if motionManager.isGyroAvailable {
motionManager.gyroUpdateInterval = 0.1
motionManager.startGyroUpdates(to: OperationQueue.main) { (data, error) in
if let gyroData = data {
let rotationRateX = gyroData.rotationRate.x
let rotationRateY = gyroData.rotationRate.y
let rotationRateZ = gyroData.rotationRate.z

print("Rotation Rate X: \(rotationRateX)")
print("Rotation Rate Y: \(rotationRateY)")
print("Rotation Rate Z: \(rotationRateZ)")
}
}
} else {
print("Gyroscope is not available.")
}

关键类解析


CMDeviceMotion


CMDeviceMotion 是一个 Core Motion 框架中的类,用于表示设备的运动和姿态信息。通过 CMDeviceMotion 类,可以获取到设备在三维空间中的加速度、旋转速度、重力加速度、旋转矩阵以及设备的姿态信息等,以便进一步进行处理和计算。


下面是 CMDeviceMotion 类中常用的属性和方法:

  • attitude 属性:表示设备的姿态信息,包括俯仰角(pitch)、横滚角(roll)和偏航角(yaw)等信息。
  • userAcceleration 属性:表示设备在三维空间中的加速度,即不包括重力加速度的加速度
  • rotationRate 属性:表示设备在三维空间中的旋转速度
  • gravity 属性:表示设备在三维空间中的重力加速度,即不包括设备加速度的重力加速度

需要注意的是,在使用 CMDeviceMotion 类时,需要首先创建一个 CMMotionManager 对象,并设置其属性和回调函数,以便获取设备的运动和姿态信息。此外,由于设备运动和姿态信息的获取涉及到多个传感器的协同工作,因此在使用时需要考虑传感器的准确性和稳定性,以避免误差和不良体验。


CMAttitude


CMAttitude 表示设备在三维空间中的姿态信息,包括设备的旋转、倾斜、方向等信息。在 iOS 开发中,可以通过 CMMotionManager 获取设备的姿态信息,然后将其保存为 CMAttitude 对象,并使用其中的各个属性来进行相应的处理和计算。


CMAttitude 类中的主要属性如下:

  • pitch:设备绕 x 轴的旋转角度,单位为弧度
  • roll:设备绕 y 轴的旋转角度,单位为弧度
  • yaw:设备绕 z 轴的旋转角度,单位为弧度
  • quaternion:设备的四元数表示,用于表示设备的旋转状态,包括旋转角度和旋转轴等信息
  • rotationMatrix:设备的旋转矩阵表示,用于表示设备在三维空间中的旋转状态

其中,pitchroll 和 yaw 属性是最基本的属性,用于表示设备绕 x、y、z 轴的旋转角度。一般来说,可以通过这三个属性来进行设备的姿态检测和相应的处理。其余的属性包括四元数、旋转矩阵,都可以用于更加精确和复杂的姿态检测和处理。


应用场景

  • 姿态估计和方向识别:通过陀螺仪获取设备旋转的角度和方向,可以实现设备的姿态估计和方向识别,广泛应用于游戏、导航、运动感知等领域
  • 图像校正和稳定:通过将陀螺仪中的旋转信息应用于图像处理,可以实现图像校正和稳定,提高图像质量和用户体验
  • 虚拟现实和增强现实:通过与其他传感器的结合和数据处理,可以实现更加真实的虚拟现实和增强现实应用,如3D游戏、AR导航、AR应用等
  • 运动检测和姿态跟踪:通过结合加速度计和地磁计等传感器的信息,可以实现设备的运动检测和姿态跟踪,如步数统计、运动轨迹记录、体感游戏等
  • 安全防护和权限控制:通过使用陀螺仪的数据,可以实现设备的安全防护和权限控制,如设备锁定、用户身份验证、数据加密等

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

iOS:NSNotification.Name从OC到Swift的写法演进

iOS
前言 在闲来无事的时候,我会抽时间看看Foundation、UIKit等相关库的Swift代码说明与注释。说实话,有的时候看起来真的很乏味,也不容易理解。 不过有的时候也会觉得Apple这么设计API真是书写的简单漂亮,一脸佩服,今天给大家分享的就是从NSNo...
继续阅读 »

前言


在闲来无事的时候,我会抽时间看看Foundation、UIKit等相关库的Swift代码说明与注释。说实话,有的时候看起来真的很乏味,也不容易理解。


不过有的时候也会觉得Apple这么设计API真是书写的简单漂亮,一脸佩服,今天给大家分享的就是从NSNotification.Name学习一种代码编写方式,并且已经在我自己的项目中进行类似这种写法的落地实战分享。


OC时代的通知名写法


NSNotification想必大家都使用过,在iOS中处理跨非相邻页面、一对多的数据处理的时候,我们通常就是通过发通知传参,告之相关页面处理逻辑。


一般情况下,如果可以避免NSNotification的时候,我都会尽量避免使用,当然,既然系统给你了这个方法,那么在合适的场景使用也会妙手生花。


当然,对于NSNotification的通知名的管理,其实是一个看似简单,实际上可以做得非常优雅的事情。


特别是从OC过渡到Swift的过程,这段简单的代码,其实进行了很大的演变,我们不妨来看看。


下面这个是我早期写OC代码的时候,发送一个通知:

[[NSNotificationCenter defaultCenter] postNotificationName:@"CancelActivateSuccessNotification" object:nil];

大家注意看,通知名,我就是非常简单的使用硬编码字符串@"CancelActivateSuccessNotification" 来表示,硬编码的缺点就不用我多说了,编译器是不会给提示的,写错了,甚至连通知事件都没法收到,总之,这种写法是不好的。


于是,看看系统代码以及AFNetworking,我们会看见这样一种写法:


系统通知名:

UIKIT_EXTERN NSNotificationName const UIApplicationDidFinishLaunchingNotification;

AFNetworking的通知名,也是学习系统通知名的写法进行的扩展:


.h文件




.m文件 



看起来并不是太高明?也许确实如此,只不过通过.h与.m的分隔,将一个硬编码字符串变成了一个全局可以引用、IDE可以快速键入的方式,但是它至少让调用变得简单与安全,这样就足够了。


于是乎,OC时代通知名的写法,我们基本上都会用以上这种方式进行编写:


.h

UIKIT_EXTERN NSString *const CancelActivateSuccessNotification;

.m

NSString *const CancelActivateSuccessNotification = @"CancelActivateSuccessNotification";

Swift时代还是这么写吗?


Swift时代的通知名写法


其实Swift的早期,基本上还是沿用着OC的这一套写法来写通知名,不过在Swift4.2之后就迎来比较大的改变,让我们来看看调用的API与源码:

open func post(name aName: NSNotification.Name, object anObject: Any?)

open func post(name aName: NSNotification.Name, object anObject: Any?, userInfo aUserInfo: [AnyHashable : Any]? = nil)

发通知的时候,通知名被一个NSNotification.Name类型代替了,我们进去追着NSNotification.Name看:




大家一定要记住这种编码的书写方式,先送上结论:

  • 可以在一个类型里面再定义一个类型,大家可以自己尝试。
  • 什么时候嵌套?为何要这么写?当嵌套的类型与外层定义的类型有着较强关联的时候可以这么写。

说完了这些,我们可以看到在Swift中,发通知,通知名不再是一个字符串了,而是一个NSNotification.Name类型了。


那么在开发过程中,我们如何使用呢?我们不妨还是从系统提供的API开始找:




因为Swift可以随处编写一个类的分类,于是在一个类的分类中定义好该类的通知名这种书写方式随处可见,这样的好处就是通知名与类紧紧联系在一起,一来便于查找,二来便于绑定业务类型。

NotificationCenter.default.post(name: UIApplication.didFinishLaunchingNotification, object: nil)

上面这个通知一发出,通过通知名我就知道是涉及UIApplication的操作行为。


说完了系统提供的API,我们再来看看一些知名第三方库是怎么定义吧,这里以Alamofire为例:




Alamofire保持了和系统API一样的风格来定义通知名。


我们再来看看Kingfisher




Kingfisher是在NSNotification.Name分类中扩展了通知名。


顺带说一下,我自己管理与编写通知名是这样的:

extension Notification.Name {
    enum LoginService {
        /// 退出
        static let logoutNotification = NSNotification.Name("logoutNotification")
    }
}
NotificationCenter.default.post(name: .LoginService.logoutNotification, object: nil)

通过在NSNotification.Name分类中进行二级业务扩展,细化通知名。


至于大家更喜欢哪一种写法,那就是仁者见仁智者见智的事情了。


总结


本篇文章从NotificationCenter发通知的通知名开始,对OC到Swift的写法演进进行梳理与说明,举了系统API和著名第三方库的例子,给大家讲解如何写好并管理好NSNotification.Name


吐槽


掘金的这个编辑器,我直接从Xcode里面复制粘贴代码的体验真的很不友好,导致我比较长的代码都是截图,只有较少的代码使用的代码块。


自己写的项目,欢迎大家star⭐️


RxStudy:RxSwift/RxCocoa框架,MVVM模式编写wanandroid客户端。


GetXStudy:使用GetX,重构了Flutter wanandroid客户端。


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

Swift 中async/await 简单使用

iOS
在 Swift 5.5 中,终于加入了语言级别的异步处理 async/await,这应该会让用回调闭包写异步调用方法时代彻底结束了! 这篇文章就简单总结一下这个功能使用吧。 异步函数 所谓异步,是相对于同步而言,这是一种执行任务的方式,同步的执行任务,任务需...
继续阅读 »

在 Swift 5.5 中,终于加入了语言级别的异步处理 async/await,这应该会让用回调闭包写异步调用方法时代彻底结束了!



这篇文章就简单总结一下这个功能使用吧。


异步函数


所谓异步,是相对于同步而言,这是一种执行任务的方式,同步的执行任务,任务需要一个一个的顺序执行,前边的好了,后边的才能运行。而异步就不是这样,它不需要等待当前任务执行完成,其他任务就可以执行。


在 Swift 5.5 中,添加了 async 关键字,标记这个函数是一个异步函数。

func getSomeInfo() async -> String { ... }
/// 可以抛出错误的异步函数
func getSomeInfoWithError() async throws -> String { ... }


这里需要注意的是,如果我们想调用异步函数,就必须在其他异步函数或者闭包里面使用 await关键字。

func runAsyncFunc() async {
let info = await getSomeInfo()
...
}

func runAsyncErrorFunc() async throws {
let info = try await getSomeInfoWithError()
...
}

实际使用异步函数的时候,我们是无法在同步函数里使用的,这时Swift会报错。要使用的话,就需要我们要提供了一个异步执行的环境 Task

func someFunc() {
Task {
runAsyncFunc()
}
}


异步序列


如果一个序列中的每个信息都是通过异步获取的,那么就可以使用异步序列的方式遍历获取。前提是序列是遵守AsyncSequence协议,只要在for in 中添加 await关键字。

let asyncItems = [asyncItem1, asyncItem2, asyncItem3]
for await item in asyncItems { ... }

多个异步同时运行


这个可以使用叫做异步绑定的方式,就是在每个存储异步返回信息的变量前边添加async

async let a = getSomeInfo()
async let b = getSomeInfo()
async let c = getSomeInfo()
let d = await [a, b, c]
...

这时运行的情况就是 a b c 是同时执行的,也就是所说的并行执行异步任务,即并发。


结构化并发


上边在提到在同步函数中使用异步函数,我们需要添加一个Task,来提供异步运行的环境。 每个 Task 都是一个单独任务,里面执行一些操作,这操作可以是同步也可以是异步。多个任务执行时,可以把它们添加到一个任务组TaskGroup中,那么这些任务就有了相同的父级任务,而这些任务Task又可以添加子任务,这样下来,任务之间就有了明确的层级关系,这也就是所谓的结构化并发


任务和任务组


任务组可以更为细节的处理结构化并发,使用方式如下,就在任务组中添加单个任务即可。

func someTasksFunc() {
Task {
await withTaskGroup(of: String.self) { group in
group.addTask {
let a = await getSomeInfo()
...
}
group.addTask {
let b = await getSomeInfo()
...
}
}
}
}

从运行的方式来说,这种使用任务组的情况和异步绑定的效果一样,简单的异步任务,完全可以使用异步绑定的方式。而任务和任务组是为更为复杂的并发情况提供支持,比如任务的优先级,执行和取消等。


如果异步函数是可抛出错误的,使用withThrowingTaskGroup就行。


解决数据竞争的Actor


在并发过程中,对于同一属性数据的读取或者写入,有时会有奇怪的结果,这些由于在不同的线程,同时进行了操作。消除这种问题的方式,就是使用 Swift 提供的 Actor类型。 一个和类差不多的类型,但是对自身可变内容进行了隔离。

actor SomeInfo {
var info: String
}

外部在访问其info属性时,actor 做了隔离,每次只能一个线程访问,直接访问就会报错。而且外部不能进行修改,只有内部才能修改。


外部访问方式就是需要异步执行,在异步环境中,添await

let content = SomeInfo(info: "abc")
let info = await content.info)
...

总结


以上就是Swift 5.5 async/await的简单使用了。了解了这些,就可以日常开发中替代闭包回调,告别回调地狱和处理不完的 completionHandler了。😎
目前官方已经在已有闭包回调处理异步的地方,都增加async异步版本,自行查看文档了解吧。。


另外附上一些很有帮助的文章地址,这些地方都有更为详尽的说明,参考学习起来!


Swift 5.5 有哪些新功能?


http://www.hackingwithswift.com/articles/23…


Swift 并发初步


onevcat.com/2021/07/swi…


并发


swiftgg.gitbook.io/swift/swift…


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

人走茶凉?勾心斗角?职场无友谊?

你和同事之间存在竞争关系 要不要把工作关系维护成伙伴关系 明枪暗箭防不胜防 背后捅刀子往往最不设防 大家是否在职场上交友是有也遇到过以上困扰呢? 不要在职场上交“朋友”,而是要寻找“盟友”。 这两者的区别在于应对策略: 我们会愿意为“朋友”牺牲自己的利益,像是...
继续阅读 »

你和同事之间存在竞争关系


要不要把工作关系维护成伙伴关系


明枪暗箭防不胜防


背后捅刀子往往最不设防


大家是否在职场上交友是有也遇到过以上困扰呢?


不要在职场上交“朋友”,而是要寻找“盟友”。


这两者的区别在于应对策略:


我们会愿意为“朋友”牺牲自己的利益,像是一张年卡。


而结交“盟友”就是为了一起争取更多利益,《孔乙己》说得好:“这次是现钱,酒要好。”


所以,在职场上的“受欢迎”和社交场、朋友圈上的“受欢迎”之间有着本质的区别:


你和你的同事未必真心喜欢彼此,但在日常相处当中能够客气、友善地交往。


大家需要寻找盟友时会第一个想到你,在争斗冲突时会尽量绕开你,这就是一种非常理想的“受欢迎”状态。 不要在职场上寻求友谊和爱,这件事是不对的。


在这里给大家列出一个在职场上受欢迎的清单。


1.实力在及格线以上


这是一切的前提。职场新人要“先活下来,再做兄弟”,稳住了工作能力这个基本面,才有资格和同事谈交情。


实力不够的人会拖累整个团队、增加所有人的工作量,大家恨都来不及,绝对不会和他称兄道弟。


实力强可以表现为实力本身,在初级职位上,也可以表现为潜力。


极少数特别强大的人可能从一开始就能很好地完成工作,但是大部分人在新加入一个团队时都需要经过一段时间的磨合,在这个过程中有欠缺和不足都是正常的,你所表现出来的敬业精神、学习能力和进步的速度才是大家对你进行评价的关键。


刚入职的新人,对于要做的事情完全没有概念,但是为人极勤奋又上进,给他布置的任务会完成得特别扎实,每一天都在飞快地进步。这样的人在职场上永远都能收获一大把来自他人的橄榄枝。


2.比较高的自尊水平


高自尊的人对自己评价高,要求也高,又能够带着欣赏的眼光去看周围的人,他们不光是很好的父母、伴侣和朋友,同时也是职场上最好的结盟对象。


高自尊的人往往拥有很多优秀的品质,同时他们也能够理解“大局”,和他们合作不用在鸡毛蒜皮的细节上纠缠推诿,可以把精力全部用来开疆拓土,极大地降低团队的内耗。


如果你是一个高自尊的人,在日常生活中表现出了自律和很好的品行,就会收获高自尊同类的赞赏。有些低自尊的人可能会认为你的言行是在“装X”,别犹豫,把他们从你的结交名单当中划掉,高自尊会帮你筛掉一批最糟糕的潜在合作者。


如果你是一个部门的领导者,记得要维护高自尊的下属,他们都是潜在的优秀带队者,给他们一个位子就可以坐上来自己动,给他们一点精神鼓励和支持,他们就会变得无所不能。


即使高自尊的手下可能某些地方让你感到嫉妒或者冒犯(这是常见的,嫉妒是每个人都一定会有的情感),也绝对不要默许或者纵容低自尊的妄人跑去伤害他们,否则会伤了大家的心,事业就难以成功了。


“朕可以敲打丞相,但你算什么东西”就是对这种低自尊妄人最好的态度。


3.嘴严,可靠


在任何一个群体当中,多嘴多舌的人都不会受到尊重,而在职场上,嘴不严尤其危险。


如果你是一个爱说是非的人,围绕在你周围的只会是一帮同样没正事、低级趣味的家伙。你会被打上“不可靠”的标记,愿意和你交流的人越来越少,大家等着看你什么时候因为多嘴闯祸,而强者根本不会和你为伍。


有些同学曾经给我留言说,自己很内向,不知道如何跟同事拉近关系。内向的人最适合强调自己的“嘴严”和“可靠”,在职场上,这两项品质远比“能说会道”更让人喜欢。


4.随和,有分寸


体面的人不传闲话,也不会轻易对旁人发表议论。


“思想可以特立独行,生活方式最好随大流”,这是对自己的要求,而他人的生活方式是不是合理,不是我们能评价的。


哪怕是最亲近的人,都未必能知晓对方的全部经历和心里藏着的每一件小事。在职场上大家保持着客气有礼的距离,就更不可能了解每个人做事的出发点和逻辑,“看不懂”是正常的,但是完全没有必要“看不惯”。如果还要大发议论,把自己的“看不惯”到处传播,你的伙伴就只会越来越少。


有人说在北上广深这样的大城市,人和人之间距离遥远,缺人情味,太冷漠。


这不是冷漠,而是对“和自己不一样”的宽容,这份宽容就是我们在向文明社会靠拢的标志。


5.懂得如何打扮


还记得斯大林的故事吗?在他离开校园之后,从头到脚都经过精心设计,不是为了精神好看,而是要让自己看起来就像一位投身革命事业的进步青年。


有句老话叫做“先敬罗衣后敬人”,本意是讽刺那些根据衣饰打扮来评价一个人的现象。我们自己在做判断的时候要尽量避免受到这类偏见的影响,但是对他人可能存在的偏见一定要心中有数。人是视觉动物,穿着打扮是“人设(人物设定)”的一部分,在我们开口说话之前,衣饰鞋袜就已经传达了无数信息。


想要成为职场当中受欢迎的人,穿着打扮的风格就要和公司的调性保持一致,最安全的做法是向你的同事靠拢。


在一个风格统一的群体当中,“与众不同”这件事自带攻击性。如果在事业单位之类的上年纪同事比较多的地方上班,马卡龙色的衣服和颜色夸张的口红,最好等到下班时间再上身。


这不是压抑天性,而是自我保护和职业精神。


6.和优秀的人站在一起


在职场上,优秀的人品质都是相似的:勤奋,自律,不断精进。如果发现了这样的同事,就要尽量和他们保持良好关系。


但是,单纯的日常沟通并不足以让你们成为盟友,正式结盟往往是通过利益交换和分享:当你遇到棘手的工作任务,就可以主动邀请对方共同跟进,同时将一部分利益让出去。愉快的合作是关系飞跃的最好契机。


优秀的人能认可的,通常也都是自己的同类。如果你能获得他们的称许和背书,在同事当中的地位自然会有所提升。


7.知道如何求助


前两天有一位关系户同学留言说,自己即将去实习,因为家人的关系可以得到一些行业资深专家的指点,问自己应该如何表现,是不是不懂就要问,像“好奇宝宝”一样,对方就会觉得自己好学上进。


我告诉她说,不要上去就问,有任何疑惑都先用搜索引擎找一下答案,如果找不出来,再带着你搜到的细节去询问那些资深前辈。


互联网时代有个很大的变化,就是人们获取信息的成本大大降低。善用搜索引擎寻找答案,就能更快、更精准、更全面地找到自己想要的东西,这种方式比跑到对方工位边用嘴问效率高得多。


凡事都问,只会让人觉得你的文字阅读能力有限,同时既不把自己的时间当回事,也不尊重别人的时间。尤其对方还是行业中的专家,他们的时间一定比实习生的宝贵多了。如果网上找不到答案,再带着细节去仔细咨询,这样的请教才是高效的,才能证明你是一个“好学上进”的人。


职场不是校园,不会再有一群老师专门负责手把手地教你,不轻易占用其他同事的时间会让你成为一个自立、有分寸、受尊重的人。毕业之后,你取得进步的速度、最终的上升空间,都和使用搜索引擎寻找答案的能力呈正相关。


8.技巧地送出小恩小惠


小恩小惠带两个“小”字,并不意味着这是一种微末小技。事实上,即使是最普通的零食,只要讲究得法,都可以送到人心里。


你的同事当中有没有因为宗教信仰而忌口的情况?


甲和乙爱吃辣,丙和丁爱吃甜,是不是两种口味都来上一点?


要留心同事的自我暴露,最好是用一个小本本记下来,关键时刻可能派上大用场。大家都是成年人,不会像孩子一样轻易被小恩小惠打动,打动我们的往往是“你把我放在心上”的温暖。


9.良好的情绪管理能力


很多时候这是个隐藏特征,但是自带“一票否决”属性:平时表现得沉着稳重,周围同事们不会有特别明显的感觉,然而歇斯底里和失控只要有一次,之前苦心经营的人设就会全面崩塌。情绪不稳定的人一般没人敢惹,但是也没人会在意了:你会被视为一个“病人”,很难再有大的发展。


已经发泄出去的情绪不能收回来,这个时候不要反复陷入纠结和悔恨,待在情绪里不出来,钱花出去了就不要去想,不要去比价。


如果情绪失控了,应该立刻做到的是原谅自己,然后考虑如何不再有下一次失控。要知道大多数人一辈子都至少会换三四次工作,了不起是换个地方,重新再来。


有的人特别幸运,天生长得好看,容易被人喜欢。


如果不是让人眼前一亮的高颜值人士,就不要太心急了。


成为一个自律、行为可以预期的人,也能慢慢地被别人喜欢。


人生很长,

作者:程序员小高
来源:juejin.cn/post/7255589558996992059
被人喜欢这件事,我们不用赶时间。

收起阅读 »

开发者不需要成为 K8s 专家!!!

之前有一篇文章 “扯淡的DevOps,我们开发者根本不想做运维!” 得到了许多开发者的共鸣,每一个开发人员,都希望能够抛却运维工作,更专注于自己开发的代码,将创意转化为令人惊叹的应用。然而事不尽如人意,到了云原生时代,开发者的运维工作似乎并没有减少,而是变成了...
继续阅读 »

之前有一篇文章 “扯淡的DevOps,我们开发者根本不想做运维!” 得到了许多开发者的共鸣,每一个开发人员,都希望能够抛却运维工作,更专注于自己开发的代码,将创意转化为令人惊叹的应用。然而事不尽如人意,到了云原生时代,开发者的运维工作似乎并没有减少,而是变成了在 K8s 上的应用部署和管理。


对运维人员来说,只需要维护好底层的 K8s,便可以在弹性、便捷性上得到巨大提升。然而 K8s 对于我们开发者而言还是太复杂了,我们还需要学习如何打包镜像以及 K8s 相关知识。许多时间都浪费在了应用部署上,我们真的需要成为 K8s 专家吗?我只是想部署一个应用至于那么复杂吗?你是否曾想过,能否有平台或方法,让我们不必成为 K8s 专家,甚至都不需要懂 K8s 就能部署好你的应用,轻松管理应用?


实际面临的问题


对于我们开发者而言,总会遇到以下不同的场景,也许是公司层面的问题、又或是业务层面的问题,也许现在用传统部署方式很简单,但随着业务增长,又不得不迁移。而面对这些问题,我们也要发出自己的声音。




  • 身处小公司,没有专门的运维。需要程序员自己负责写 Dockerfile + YAML + Kustomize 然后部署到 k8s 上面。除了工作量以外,还面临 K8s 自身的复杂性,对于多套业务,Dockerfie、Yaml、CI、CD 脚本占据了绝大部分的工作量。不写这些行不行?




  • 公司内的微服务越来越复杂,在写代码的基础上还得考虑各个服务之间的通信、依赖和部署问题,毕竟除了我们开发者以外,运维人员也不会比你更熟悉微服务之间的复杂依赖。也许已经开始尝试 Helm ,但是编写一个完整的 Chart 包依然是如此复杂,还可能面临格式问题、配置解耦不完全导致的换个环境无法部署问题。时间全写 Yaml 了。不额外编写 Helm Chart,直接复制应用行不行?




  • 在大型企业内部,正处于在传统应用迁移到云环境的十字路口。面对多种集群的需求、现有应用的平稳迁移、甚至一些公共的模块的复用如何做都将成为我们需要解决的问题。不要每次都重新开发,把现有的应用或模块积累下来行不行?




在这些场景下,我们大量的时间都消耗在额外的 Dockerfile、Yaml、Helm Chart 这些编写上了。K8s 很好,但似乎没解决我们开发者的问题,我们开发者用 K8s 反而变得更加复杂。不说需要额外编写的这些文件或脚本,单单是掌握 K8s 的知识就需要耗费大量时间和精力。
这些问题真的绕不过去吗?我觉得不是。来了解下 Rainbond 这个不需要懂 K8s 的云原生应用管理平台吧。谁说只有成为 K8s 专家后,才能管理好你的应用?


为什么是 Rainbond?


Rainbond 是一个不需要懂 K8s 的应用管理平台。不用在服务器上进行繁琐操作,也不用深入了解 K8s 的相关知识。Rainbond 遵循“以应用为中心”的设计理念。在这里只有你的业务模块和应用。每一个业务模块都可以从你的代码仓库直接部署并运行,你不是 K8s 专家也可以管理应用的全生命周期。同时利用 Rainbond 的模块化拼装能力,你的业务可以灵活沉淀为独立的应用模块,这些模块可以随意组合、无限拼装,最终构建出多样化的应用系统。


1. 不懂 K8s 部署 K8s 行不行?


行! 对于很多初学者或者开发人员来说,如果公司内部已经搭建好了可用的 K8s 平台,那么这一步不会是需要担心的问题。但是对于一些独立开发者而言,却很难有这样的环境,而 Rainbond 就提供了这样的解决方案,在 Linux 服务器上,只需要先运行一个 Docker 容器,访问到 Rainbond 控制台后,再输入服务器的 IP 地址,即可快速部署一套完整的 K8s 集群。


add_cluster


如果这还是太复杂,那么可以尝试使用 Rainbond 的快速安装,只需要一个容器和 5 分钟时间,就能为你启动一个带 K8s 集群的平台,而你在平台上部署的业务也都会部署到这个集群中。


2. 不想或不会写 Dockerfile、Yaml 等文件能不能部署应用?


能! Rainbond 支持自动识别各类开发语言,不论你是使用哪种开发语言,如Java、Python、Golang、NodeJS、Dockerfile、Php、.NetCore等,通过简单的向导式流程,无需配置或少量配置,Rainbond 都能够将它们识别并自动打包为容器镜像,并将你的业务快速部署到 K8s 集群中进行高效管理。你不再需要编写任何与代码无关的文件。只需要提供你的代码仓库地址即可。


source_code_build


3. 各类业务系统如何拼装?


在 Rainbond 中,不同的业务程序可以通过简单的连线方式进行快速编排。如果你需要前端项目依赖后端,只需打开编排模式,将它们连接起来,便能迅速建立依赖关系,实现模块化的拼装。这为你的应用架构带来了极大的灵活性,无需复杂的配置和操作,即可快速构建复杂的应用系统。


同时如果你已经实现了完整的业务程序,它可能包含多个微服务模块,你还可以将其发布到本地的组件库实现模块化的积累。下次部署时可以直接即点即用,且部署后可以与你其他应用程序再次进行拼装。实现无限拼装组合的能力。


component_assembly


4. 不会 K8s 能不能管理部署好的应用?


没问题! Rainbond 提供了面向应用的全生命周期管理运维,不需要学习 Kubectl 命令,也不需要知道 K8s 内复杂的概念,即可在页面上一键管理应用内各个业务模块的批量启动、关闭、构建、更新、回滚等关键操作,同时还支持应用故障时自动恢复,以及应用自动伸缩等功能。同时还支持应用 http 和 tcp 策略的配置,以及相应的证书管理。


app_manage


如何使用?


在 Linux 终端执行以下命令, 5 分钟之后,打开浏览器,输入 http://<你的IP>:7070 ,即可访问 Rainbond 的 UI 了。


curl -o install.sh https://get.rainbond.com && bash ./install.sh

作者:Rainbond开源
来源:juejin.cn/post/7268539925086519353

收起阅读 »

Amazon SageMaker 让机器学习轻松“云上见”!

最近,“上云探索实验室”活动 正在如火如荼进行,亚马逊云、“大厂”背景加持,来看看它们有什么新鲜技术/产品? 本篇带来对 shouAmazon SageMaker 的认识~ 闻名不如一见 首先,开宗明义,shouAmazon SageMaker 是什么? 从...
继续阅读 »

最近,“上云探索实验室”活动 正在如火如荼进行,亚马逊云、“大厂”背景加持,来看看它们有什么新鲜技术/产品?


本篇带来对 shouAmazon SageMaker 的认识~


闻名不如一见


首先,开宗明义,shouAmazon SageMaker 是什么?




官网我们可以了解到:Amazon SageMaker 是一项帮助开发人员快速构建、训练和部署机器学习 (ML) 模型的托管服务。


其实,类比于:就像咱们开发人员经常把代码部署 Github Page 上,自动构建、运行我们的代码服务。


当下,我们可以把 AIGC 领域火爆的模型共享平台 —— Hugging Face 中的模型放到 Amazon SageMaker 中去部署运行、进行微调等~


如果你还不了解 HuggingFace?那抓紧快上车!




就这张小黄脸,目前已经估值 20 亿美元,它拥有 30K+ 的模型,5K+ 的数据集,以及各式各样的 Demo,用于构建、训练最先进的 NLP (自然语言处理)模型。


是的,如果你:


1、不想关心硬件、软件和基础架构等方面的问题


2、想简化操作机器学习模型的开发流程


3、想灵活选择使用自己的算法和框架以满足不同业务需求


就可以 把目光投向 Amazon SageMaker,用它的云服务来部署你想要用的 HuggingFace 模型等~




百思不如一试


Amazon SageMaker 可以全流程的帮助我们构建机器学习模型,这样真的会省下很多心力(少掉几根头发)~


具体实践中,我们知道在数据预处理过程中,在训练模型之前,需要做一系列操作,比如:缺失值处理、数据归一化和特征选择等,Amazon SageMaker 提供了很好的预处理和转换数据的工具,助力快速完成这些工作。


在模型选择环节,Amazon SageMaker 提供多种内置的机器学习算法和框架,你可以根据数据集和任务类型选择合适的模型。


还有,提高模型性能是我们需要特别关注的,Amazon SageMaker 让你可以指定调优的目标和约束条件,系统会自动搜索最优的参数组合,这就很智能、很舒服。


模型训练完后,Amazon SageMaker 也自带易用的模型部署和监控功能;


一整套下来,训练模型感觉就像呼吸一样自然~


官网教程写的很清晰,还有很多视频讲解:# Amazon SageMaker - 入门手册


这里不赘述,仅看实战中代码表示,感受一二:


如何在Amazon SageMaker 上部署Hugging Face 模型?




1、首先,在 Amazon SageMaker 中创建一个 Notebook 实例。可以使用以下代码在 Amazon SageMaker 中创建 Notebook 实例:

import sagemaker from sagemaker
import get_execution_role

role = get_execution_role()

sess = sagemaker.Session()

# 创建 Notebook 实例
notebook_instance_name = 'YOUR_NOTEBOOK_INSTANCE_NAME'
instance_type = 'ml.t2.medium'
sagemaker_session = sagemaker.Session()
sagemaker_session.create_notebook_instance(notebook_instance_name=notebook_instance_name,instance_type=instance_type,role=role)


2、其次,下载 Hugging Face 模型,你可以使用以下代码下载 Hugging Face 模型:

!pip install transformers

from transformers import AutoTokenizer, AutoModelForSequenceClassification

# 下载 Hugging Face 模型
model_name = "distilbert-base-uncased-finetuned-sst-2-english"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSequenceClassification.from_pretrained(model_name)

3、创建一个推理规范,以指定部署的模型。可以参照以下代码创建推理规范:

from sagemaker.predictor import Predictor
from sagemaker.serializers import JSONSerializer
from sagemaker.deserializers import JSONDeserializer
from sagemaker.tensorflow.serving import Model

# 创建推理规范
class HuggingFacePredictor(Predictor):
def __init__(self, endpoint_name, sagemaker_session):
super().__init__(endpoint_name, sagemaker_session, serializer=JSONSerializer(),
deserializer=JSONDeserializer())

model_name = "huggingface"
model_data = "s3://YOUR_S3_BUCKET/YOUR_MODEL.tar.gz"
entry_point = "inference.py"
instance_type = "ml.m5.xlarge"
role = sagemaker.get_execution_role()

model = Model(model_data=model_data,
image_uri="763104351884.dkr.ecr.us-east-1.amazonaws.com/huggingface-pytorch-inference:1.6.0-transformers4.0.0",
role=role,
predictor_cls=HuggingFacePredictor,
entry_point=entry_point)

predictor = model.deploy(initial_instance_count=1, instance_type=instance_type)


4、测试部署模型

data = {"text": "I love using Amazon SageMaker!"}
response = predictor.predict(data)

print(response)

过程就是,创建 NoteBook => 下模型 => 指定模型、设定推理脚本 => 测试部署,足够简洁~


人人都能上云,人人都能训练机器模型~


不知道大家发现没有,其实现在的编程开发都逐渐在“云”化,不仅是机器学习,普通开发也是;类似低代码平台,开发不再是一点点复制代码、拼凑代码、修改代码,更多是通过自动化平台“点点点”就能构建自己想要的服务了!拥抱“云”平台,省心又省力,也在拥抱未来~


防守不如亮剑


目前市面上同类型的产品也有一些,比如:


1、Google Cloud AI Platform:Google提供的全托管的机器学习平台,可以帮助用户构建、训练和部署机器学习模型。


2、Microsoft Azure Machine Learning:微软提供的一款全托管的机器学习平台,可以帮助用户构建、训练和部署机器学习模型。


3、IBM Watson Studio:IBM提供的一款机器学习工具和服务平台,可以帮助用户构建、训练和部署机器学习模型。此外它还提供了数据可视化、模型解释和协作等功能。


它们与Amazon SageMaker 类似,具有全托管和自动化的特点,同时提供了多种算法和框架供用户选择。但尺有所长、寸有所短,咱们不妨用表格来一眼对比它们各自的优缺点:



可以看到,Amazon SageMaker 的配置更简单;


至于谈到:“需掌握 AWS 服务”,其实它也好上手,类比于阿里云,AWS 是亚马逊云服务,全球顶流、百万客户、加速敏捷,即使不用来开发机器模型,也建议体验、上手其它云服务产品~具体参见aws.amazon.com




活动福利环节


刚好,最近正在进行“上云探索实验室”活动;6月27-28日亚马逊云科技中国峰会上海现场,展区5楼【开发者会客厅】有互动活动:


现场为开发者提供了部分 Amazon codewhisperer 账号,开发者可以到现场直接进行代码体验,参与体验问卷回复可获得社区定制周边礼品一份。


同时开发者也可于现场进行专业版账号注册,完成注册即可获得定制周边礼品一份或免费 Serverlesspresso 咖啡券一张。


欢迎大家注册~~


话不多少,先冲为敬~




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

什么?同一张图在Flutter和iOS原生渲染出来效果不一样?

iOS
最近遇到了一个很奇怪的现象,同样的一张图在iOS原生能正常显示,但是在Flutter就显得很暗 具体往下看 现象 可以很清晰的看到iOS对于原图的还原度要比Flutter好太多 思考 看到这种场景顿时懵逼,明明是一张图,不至于Flutter这么拉胯吧,但是这...
继续阅读 »

最近遇到了一个很奇怪的现象,同样的一张图在iOS原生能正常显示,但是在Flutter就显得很暗
具体往下看


现象


截屏2023-08-18 16.49.37.png


可以很清晰的看到iOS对于原图的还原度要比Flutter好太多


思考


看到这种场景顿时懵逼,明明是一张图,不至于Flutter这么拉胯吧,但是这个现象是做了这么久的Flutter第一次遇见(或者是之前没太仔细看小的icon,这次大图渲染更加直观。。),所以觉得这个事情或许没那么复杂。于是开始怀疑是Impeller的问题,但是我尝试了@恋猫de小郭的文章里的
在iOS工程中修改info.plist


<key>FLTEnableImpeller</key>
<true />

尝试关闭Impeller,但是并没有什么效果,所以初步怀疑Impeller的思路或许不太对,所以就开始看之前的在Flutter显示正常的图,最后发现了一个微妙之处,在图片的简介里有一个我不懂的地方不一样,就是颜色描述文件


WechatIMG394.jpg


这里询问chatgpt颜色描述文件:sRGB IEC61966-2.1Display P3有什么区别,得到的回答是



sRGB IEC61966-2.1和Display P3是两种不同的颜色描述文件,它们主要用于不同的显示设备和应用场景。它们的区别主要在于颜色范围和应用领域。



  1. sRGB IEC61966-2.1:sRGB是一种标准的RGB颜色空间,广泛用于互联网、数字摄影和普通的显示设备上。sRGB IEC61966-2.1的颜色范围相对较窄,适合一般图像显示,它的色域较小,能够在大多数常见显示设备上提供相对一致的颜色显示。sRGB通常是图像文件的默认颜色描述文件。

  2. Display P3:Display P3是一种更宽广的颜色空间,偏向于更鲜明和逼真的色彩展示。它主要用于高级的显示设备,如苹果公司的Retina显示器。相对于sRGB,Display P3能够呈现更宽广的颜色范围,对于鲜艳和饱和度较高的颜色效果更为明显。 总结来说,sRGB IEC61966-2.1适合一般的互联网应用和常见显示设备,而Display P3则适用于高级显示设备,如高分辨率显示器和专业图形处理工作。在选择使用哪一种颜色描述文件时,需要考虑图像的应用场景和目标设备的能力来做出合适的选择。



呃。。好像看不出来咋回事,后来看到一篇文章说到Flutter对于Display P3的支持问题,具体意思就是原因就是 Flutter 直接把 Display P3 色域当做 sRGB 色域的图像处理了,而没有做色域转换,这一下就真相大白了~。


解决办法


文章中提到可以让原生来处理图片


CGImageSourceRef src = CGImageSourceCreateWithData((__bridge CFDataRef) imageData, NULL);
NSUInteger frameCount = CGImageSourceGetCount(src);
if (frameCount > 0) {
NSDictionary *options = @{(__bridge NSString *)kCGImageSourceShouldCache : @YES,
(__bridge NSString *)kCGImageSourceShouldCacheImmediately : @NO
};
NSDictionary *props = (NSDictionary *) CFBridgingRelease(CGImageSourceCopyPropertiesAtIndex(src, (size_t) 0, (__bridge CFDictionaryRef)options));
NSString *profileName = [props objectForKey:(NSString *) kCGImagePropertyProfileName];
if ([profileName isEqualToString:@"Display P3"]) {

NSMutableData *data = [NSMutableData data];
CGImageDestinationRef destRef = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)data, kUTTypePNG, 1, NULL);

NSMutableDictionary *properties = [NSMutableDictionary dictionary];
properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(1);
properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(0);

properties[(__bridge NSString *)kCGImagePropertyNamedColorSpace] = (__bridge id _Nullable)(kCGColorSpaceSRGB);
properties[(__bridge NSString *)kCGImageDestinationOptimizeColorForSharing] = @(YES);

CGImageDestinationAddImageFromSource(destRef, src, 0, (__bridge CFDictionaryRef)properties);

CGImageDestinationFinalize(destRef);
CFRelease(destRef);
return data;

}
}

return imageData;

这里偷懒了,因为找UI小姐姐让她切图的时候调整一下就可以了~,最后的解决方案是UI根据设计稿导出sRGB IEC61966-2.1类型的图片,同时这个图片的色值是向Display P3

作者:Jerry815
来源:juejin.cn/post/7268539503907307520
code>靠拢的,至此问题解决。

收起阅读 »

新一代前端工具链Rome:革新前端开发

web
在前端开发领域,每时每刻都在涌现着各种新的工具和框架。而 Rome,作为一款新一代前端工具链,引起了广泛的关注和热议。它不仅提供了卓越的性能,还整合了各种强大的功能,使前端开发变得更加高效。本文将深入介绍 Rome,并为你提供一些代码示例,帮助你更好地了解和使...
继续阅读 »

在前端开发领域,每时每刻都在涌现着各种新的工具和框架。而 Rome,作为一款新一代前端工具链,引起了广泛的关注和热议。它不仅提供了卓越的性能,还整合了各种强大的功能,使前端开发变得更加高效。本文将深入介绍 Rome,并为你提供一些代码示例,帮助你更好地了解和使用这个令人激动的工具。


image.png
image.png


Rome 是什么?


Rome 是一个全新的前端工具链,旨在重新定义前端开发体验。它是一个一站式解决方案,涵盖了许多前端开发中常见的问题和任务。Rome 的主要目标是提供一致性和高性能,以加速前端开发流程。它的核心特点包括:


1. 依赖管理


Rome 提供了强大的依赖管理系统,可用于管理你的项目中的依赖关系。它支持 JavaScript、TypeScript 和 Flow,并能够准确地分析和处理依赖项,以确保你的项目始终保持一致。


// 安装依赖
rome deps add react

// 查看依赖树
rome deps list

这个特性非常有用,因为它将所有的依赖关系都纳入统一管理,无需依赖其他工具。


2. 代码格式化


Rome 自带了一个先进的代码格式化工具,可帮助你统一项目中的代码风格。无需争论缩进或分号,Rome 将自动处理这些问题。


# 格式化整个项目
rome format

代码格式化对于团队协作和维护项目非常重要,它可以消除代码风格的争议,使代码更易读。


3. 静态类型检查


Rome 集成了强大的静态类型检查器,可以在编码过程中捕获潜在的类型错误,提高代码质量和可维护性。它支持多种类型系统,包括 TypeScript、Flow 等。


// 检查类型
rome check

这个特性有助于减少运行时错误,提前发现潜在的问题。


4. 构建工具


Rome 提供了一套强大的构建工具,可用于将你的代码编译成浏览器可执行的代码。这有助于优化性能并减小最终部署包的大小。


# 构建项目
rome build

Rome 的构建工具支持多种目标,你可以轻松地将项目构建成不同的输出格式。


5. 包管理


Rome 不仅支持 JavaScript 包管理,还可以处理 CSS、图片、字体等多种资源。这意味着你可以在一个地方管理所有资源,而无需额外的工具。


// 导入 CSS 文件
import './styles.css';

这个特性使得资源管理变得更加一致和方便。


Rome 的安装和配置


现在,让我们来看看如何安装 Rome 并进行基本配置。


步骤 1:安装 Rome


你可以使用 npm 或 yarn 安装 Rome。这里以 npm 为例:


npm install -g rome

安装完成后,你可以在终端中运行 rome -v 来确认 Rome 是否成功安装。


步骤 2:初始化项目


在你的项目目录中,运行以下命令来初始化一个 Rome 项目:


rome init

这将创建一个 .romerc.js 文件,其中包含了


项目的配置信息。


步骤 3:配置选项


你可以编辑 .romerc.js 文件来配置 Rome 的选项,以满足你的项目需求。例如,你可以指定项目的目标环境、依赖管理方式、构建选项等。


// .romerc.js
module.exports = {
target: 'browser',
module: {
type: 'commonjs',
},
build: {
minify: true,
},
};

使用 Rome


一旦你的项目配置好了,就可以开始使用 Rome 提供的工具来进行开发。以下是一些常用的命令和示例:


运行 linter 来检查代码风格和潜在问题。


rome check

运行格式化程序来自动格式化你的代码。


rome format

运行类型检查以确保类型安全。


rome typecheck

构建你的项目。


rome build

运行你的应用程序。


rome run

自定义和插件


Rome 还支持自定义插件和配置。你可以编写自己的插件,或者将现有的插件集成到你的项目中,以满足特定需求。


// .romerc.js
module.exports = {
custom: {
myPlugin: {
enabled: true,
options: {
// 自定义选项
},
},
},
};

在这个示例中,我们启用了一个名为 "myPlugin" 的自定义插件,并提供了一些自定义选项。


Rome发展前景




  1. 性能优化: Rome 的核心目标之一是提供高性能。未来,它将不断进行性能优化,以确保更快的构建和更快的开发体验。随着前端项目变得越来越复杂,性能优化将成为前端开发的重要问题,Rome 在这方面有望发挥重要作用。




  2. 更好的类型检查: Rome 集成了静态类型检查器,帮助开发者在编码阶段捕获潜在的类型错误。未来,这个类型系统可能会进一步增强,提供更多的类型推断和错误检查功能。




  3. 更多的插件和扩展: Rome 的自定义插件和配置功能使得开发者可以根据自己的需求扩展 Rome。随着社区的不断壮大,预计会有更多的插件和扩展出现,为开发者提供更多的选择和解决方案。




  4. 更广泛的应用领域: Rome 目前主要用于前端开发,但未来可能会扩展到其他领域,如后端开发或跨平台开发。这将使 Rome 成为一个更加通用的工具链,适用于各种不同类型的项目。




  5. 更丰富的文档和教程: 随着 Rome 的发展,预计会有更多的文档、教程和学习资源出现,帮助开发者更好地掌握和使用 Rome。这将有助于扩大 Rome 的用户群体。




  6. 更强大的生态系统: Rome 的生态系统将继续扩大,包括各种开发工具、编辑器插件和整合解决方案。这将使 Rome 成为一个完整的开发生态系统,为开发者提供一站式的解决方案。




总之,Rome作为一个新一代前端工具链,充满了潜力,未来有望在前端开发领域取得更多的成功和影响力。开发者可以继续关注 Rome 的发展,利用它来提高前端开发效率和质量。


结语


Rome 是一个前端工具链的新星,它为前端开发者提供了许多强大的功能,以提高开发效率和代码质量。虽然 Rome 还在不断发展和改进中,但它已经展现出了巨大的潜力。无论你是新手还是经验丰富的前端开发者,都值得一试 Rome,看看它如何改变你的前端开发流程。在使用 Rome 时,记得查阅官方文档以获取更多详细信息和示例代码。希望这篇文章能帮助你入门 Ro

作者:侠名风
来源:juejin.cn/post/7269745800178925603
me 并开始在你的项目中使用它。

收起阅读 »

JS 不写分号踩了坑,但也可以不踩坑

web
踩的坑 写一个方法将秒数转为“xx天xx时xx分xx秒”的形式 const ONEDAYSECOND = 24 * 60 * 60 const ONEHOURSECOND = 60 * 60 const ONEMINUTESECOND = 60 functi...
继续阅读 »

踩的坑


写一个方法将秒数转为“xx天xx时xx分xx秒”的形式


const ONEDAYSECOND = 24 * 60 * 60
const ONEHOURSECOND = 60 * 60
const ONEMINUTESECOND = 60

function getQuotientandRemainder(dividend,divisor){
const remainder = dividend % divisor
const quotient = (dividend - remainder) / divisor
return [quotient,remainder]
}

function formatSeconds(time){
let restTime,day,hour,minute
restTime = time
[day,restTime] = getQuotientandRemainder(restTime,ONEDAYSECOND)
[hour,restTime] = getQuotientandRemainder(restTime,ONEHOURSECOND)
[minute,restTime] = getQuotientandRemainder(restTime,ONEMINUTESECOND)
return day + '天' + hour + '时' + minute + '分' + restTime + '秒'
}
console.log(formatSeconds(time)) // undefined天undefined时undefined分NaN,NaN秒

按照这段代码执行完后,day、hour、minute这些变量得到的都是 undefined,而 restTime 则好像得到一个数组。

问题就在于 13、14、15、16 行之间没有添加分号,导致解析时,没有将这三行解析成三条语句,而是解析成一条语句。最终的表达式就是这样的:


restTime = time[day,restTime] = getQuotientandRemainder(restTime,ONEDAYSECOND)[hour,restTime] = getQuotientandRemainder(restTime,ONEHOURSECOND)[minute,restTime] = getQuotientandRemainder(restTime,ONEMINUTESECOND)

那执行的过程相当于给 restTime 进行赋值,表达式从左往右执行,最终表达式的值为右值。最右边的值就是 getQuotientandRemainder(restTime,ONEMINUTESECOND),由于在计算过程中 restTime 还没有被赋值,一直是 undefined,所以经过 getQuotientandRemainder 计算后得到的数组对象每个成员都是 NaN,最终赋值给 restTime 就是这样一个数组。


分号什么时候会“自动”出现


有时候好像不写分号也不会出问题,比如这种情况:


let a,b,c
a = 1
b = 2
c = 3
console.log(a,b,c) // 1 2 3

这是因为,JS 进行代码解析的时候,能够识别出语句的结束位置并“自动添加分号“,从而能够解析出“正确”的抽象语法树,最终执行的结果也就是我们所期待的。

JS 有一个语法特性叫做 ASI (Automatic Semicolon Insertion),就是上面说到的”自动添加分号”的东西,它有一定的插入规则,在满足时会为代码自动添加分号进行断句,在我们不写分号的时候,需要了解这个规则,才能不踩坑。(当然这里说的加分号并不是真正的加分号,只是一种解析规则,用分号来代表语句间的界限)


ASI 规则


JS 只有在出现换行符的时候才会考虑是否添加分号,并且会尽量“少”添加分号,也就是尽量将多行语句合成一行,仅在必要时添加分号。


1. 行与行之间合并不符合语法时,插入分号


比如上面那个自动添加分号的例子,就是合并多行时会出现语法错误。

a = 1b = 2 这里 1b 是不合法的,因此会加入分号使其合法,变为 a = 1; b = 2


2. 在规定[no LineTerminator here]处,插入分号


这种情况很有针对性,针对一些特定的关键字,如 return continue break throw async yield,规定在这些关键字后不能有换行符,如果在这些关键字后有了换行符,JS 会自动在这些关键字后加上分号。
看下面这个例子🌰:


function a(){
return
123
}
console.log(a()) // undefined

function b(){
return 123
}
console.log(b()) // 123

在函数a中,return 后直接换行了,那么 return 和 123 就会被分成两条语句,所以其实 123 根本不会被执行到,而 return 也是啥也没返回。


3. ++、--这类运算符,若在一行开头,则在行首插入分号


++ 和 -- 既可以在变量前,也可以在变量后,如果它们在行首,当多行进行合并时,会产生歧义,到底是上一行变量的运算,还是下一行变量的运算,因此需要加入分号,处理为下一行变量的运算。


a
++
b
// 添加分号后
a
++b

如果你的预期是:


a++ 
b

那么就会踩坑了。


4. 在文件末尾发现语法无法构成合法语句时,会插入分号


这条和 1 有些类似


不写分号时需要注意⚠️


上面的 ASI 规则中,JS 都是为了正确运行代码,必须按照这些规则来分析代码。而它不会做多余的事,并且在遵循“尽量合并多行语句”的原则下,它会将没有语法问题的多行语句都合并起来。这可能违背了你的逻辑,你想让每行独立执行,而不是合成一句。开头贴出的例子,就是这样踩坑的,我并不想一次次连续的对数组进行取值🌚。

因此我们要写出明确的语句,可以被合并的语句,明确是多条语句时需要加上分号。


(如果你的项目中使用了某些规范,它不想让你用分号,别担心,它只是不想让你在行尾用分号,格式化时它会帮你把分号移到行首)像这样:


// before lint
restTime = time;
[day, restTime] = getQuotientandRemainder(restTime, ONEDAYSECOND);
[hour, restTime] = getQuotientandRemainder(restTime, ONEHOURSECOND);
[minute, restTime] = getQuotientandRemainder(restTime, ONEMINUTESECOND);

// after lint
restTime = time
;[day, restTime] = getQuotientandRemainder(restTime, ONEDAYSECOND)
;[hour, restTime] = getQuotientandRemainder(restTime, ONEHOURSECOND)
;[minute, restTime] = getQuotientandRemainder(restTime, ONEMINUTESECOND)

参考


收起阅读 »

生病的35岁程序员

这两天又生病了,应该来说我这差不多一个月生了三回病。一回病是感冒,然后第二次呢是新冠的二阳,然后第三次就是昨天发烧,后面被检测是肺炎。 俗话说,病来如山倒,病去如抽丝。我觉得一旦当生病的时候,就会开始怀疑人生,去思考人生,当病好的时候,又把自己生病时候的病痛忘...
继续阅读 »


这两天又生病了,应该来说我这差不多一个月生了三回病。一回病是感冒,然后第二次呢是新冠的二阳,然后第三次就是昨天发烧,后面被检测是肺炎。


俗话说,病来如山倒,病去如抽丝。我觉得一旦当生病的时候,就会开始怀疑人生,去思考人生,当病好的时候,又把自己生病时候的病痛忘得一干二净,好像疼痛就不曾存在过。


可能当你十几岁或者二十几岁的时候,你对生病两个字没有特别的深刻的概念,也就是你会把生病认为是一个很平常的事情,甚至很多人几年都不会有一种影响生活的大病。


可是你到了30多岁的时候,你生病的时候,你就开始思考人生了。因为到了30岁,整体的抵抗力可能已经开始下降了,不像二十几岁的青年,对一切病毒免疫,所以你对于病痛的体感会更加的明显。同时上有老下有小,你会思考,如果万一自己就这么挂掉的话,小孩,媳妇,老人怎么办?这个世界如果没有了我又会是怎么样的?是整个世界陷入一片混沌,还是我的意识就此消亡?我不存在的日子里面,地球和人类又会怎么发展?我是不是又会错过很多令人惊奇的事情?


对于大部分的职业来说,比如医生、律师、公务员,30岁左右应该是黄金时间,30岁左右是对于整个行业最应该有发言的时候。刚毕业的时候,一路荆棘和摸索,因为前面几年沉淀了很多经验,踩了很多坑,而到了30多岁,就是应该对人生和对自己的工作有一定洞见的时候。这个时候的男人女人,既有经历又有能力,应该能够更好的去输出给社会自己的价值。


而程序员确实不一样,很多人会羡慕程序员的高薪,高工资,高福利等等。但实际上很多程序员确实是在透支自己的生命和体力。比如长期996就是一种极度不正常的状态。当然这还算好的,在早点之前可能有极大的概率是要求007的。我的运气比较好,我基本上没有做过996的工作,但是我基本上是995。有极少的一段时间里面我是005。也就是我周末基本上没有加过班,这个也是我最值得庆幸的,当然有很多程序员可能就没有这么好的待遇了,很多中小型公司长期在996里不能自拔,一方面被老板的饼所吸引,另外一方面人在江湖,身不由己,谁不想快速改变自己的命运,而概念命运的稻草,就是程序员“狭隘”的眼光里看到的那点金元宝。


当然我觉得我的工作本身相对来说没有想象中的那么忙,但同样我觉得这个行业里面会给我们灌输或者浸润一种不太健康的状态。比如说你正常情况下来说你9:00下班,稍微折腾一下10点,11点你可能就能够睡觉了。但实际上却非常的难,因为一个人在一天的工作以后会进入一种疲劳状态,你就不自觉地想去放松一下,也就是叫报复性的刷手机。所以在9点~11点甚至12点这个时间段里面,你会去想办法来弥补出来自己在工作自己的那种休闲和放松。


所以9:00下班了情况下,你可能需要12点才能够真正地睡着。而正常的作息时间,朝九晚五才是一种最最最健康的工作时间。互联网给我们程序员带来了一种疲劳感,而且特别多的熬夜,而熬夜本身对身体就是有一种最大的伤害。这种伤害是持久的,并且在一定的时间里面,它甚至是不可逆的。


就如同慢性病一样,就因为它的慢,所以才导致了被很多人忽视,但是一旦这种慢性病叠加到了一定的程度以后,它就会变成了一种不治之症。比如空气污染,比如噪音,比如光污染,比如甲醛污染,这些所谓的社会上面的一些问题给我们带来的都是慢性的伤害,我们短期内没办法看到它的效果,但是时间一长一定会有极大的伤害。熬夜也是一样,我们这短期的一两天的熬夜根本不会影响什么,甚至像很多大学生通宵熬夜,第二天照样精力满满。但是量变引起质变,我们长时间的熬夜终究会带来反噬的效果,这一天可能会早,可能会晚,但它一定是有极度的伤害的。


所以我现在看来,我觉得一切都是有成本的。比如你选择了传统的行业,你可能赚到的钱不会多,但是大概率是可以持续的,到了互联网行业你可能赚得比较多,但大概率可能是不可持续的。也就是说不管怎么样,你付出的是你的时间和身体的成本。


我一个朋友他在前两年说了一句话,他说他完全不羡慕用时间去换取高工资的。我刚开始的时候还不理解,我认为如果你能赚到越来越多的钱,那你不就牛逼吗?现在我回想起来确实是的,这种不值得去鼓励,因为你付出的代价也非常的大。


所以如果以现在的视角看来的话,我觉得在这个行业里面你很难做到被这个行业不会被感染。你可能很多生活习惯和思维能力全部会被这个行业所渗透。就包括了很多不好的习惯,就比如说缺少运动,比如说熬夜,当然还有所谓的职场PUA。


所以我这里面也想跟大家分享一点经验,第一个就是可能还是要多运动,运动是能够缓解很多疲劳感以及增强免疫力的。我记得我之前在坚持运动和锻炼的时候,基本上没有生过特别大的病,只是偶尔极少数的感冒,并且也不会有那么严重。


第二个真的还是要养成一个良好的生活习惯,比如早睡早起。虽然我觉得现在已经有点难了,但是要改正起来确实也是有办法的,否则等待你的可能就是各种猝死,各种大病。这里不是危言耸听,虽然概率很小,但是很多程序员过了35岁,你就会发现他根本就不能够接受这种高强度,高压力,高时间的工作。而且并不是他主观上不能接受,因为对于30多岁的程序员来说,上有老下有小,其实经济压力也非常的大,但是身体各方面的信号已经告诉了他,不大适合这种高强度的加班,否则等待他的只是更恶的恶果。


第三就是要尽量减少被行业的影响。在加班文化,在熬夜文化盛行的今天,我们确实要真正问一下自己,这种是不是好的,我们能不能有办法去杜绝?在完全没有办法的情况下,我们能不能通过这样极少数的空隙,我们好好休息,好好锻炼?


最后,希望大家身体健康。

作者:ali老蒋
来源:juejin.cn/post/7269794410572922934

收起阅读 »

关于离职那些事的有感而发

一、面对离职的心态 每当有同事离职,在群里写下感谢的话语,最后来一句“后会有期”。 下面一定是一条又一条的“苟富贵、勿相忘”的祝福语,洋洋洒洒排起了长龙,婉若一首悲壮的赞歌。 而里面一定没有我。 其中的心态非常微妙,在平时,同事的输出我会积极点赞,分享知识会...
继续阅读 »

一、面对离职的心态


每当有同事离职,在群里写下感谢的话语,最后来一句“后会有期”。


下面一定是一条又一条的“苟富贵、勿相忘”的祝福语,洋洋洒洒排起了长龙,婉若一首悲壮的赞歌。



而里面一定没有我。


其中的心态非常微妙,在平时,同事的输出我会积极点赞,分享知识会打赏鼓励,可一旦离职,实在没办法说些积极的话,为什么呢?


可能是因为不再年轻了吧,时间改变了很多东西,毕竟见得多了,所以看得开了,也放得下了。


我10年正式参加工作,第一家是个小公司,公司虽小,五脏俱全,该有的工种都有。


其中,有个设计师很对我的胃口,很多设计理念不谋而合,我认同他的设计,他认同我的实现,可谓合作上的好搭档。


可是某一天,没有任何征兆的,其突然离职了,就很突然的那种,那种突然就好像,吃着火锅唱着歌,突然就被麻匪给劫了。


明明说好的一起做个伟大的产品的,你却潇洒出走不回头。



现在想想,当时那种遭遇背叛,遭受欺骗的感受是有些“幼稚”的,混合了过多的主观情感,是不理性的表现。


托尔斯泰曾经说过,友谊好比一壶开水,一旦离开了炉子,就会逐渐凉了下来,朋友尚且如此,更何况只是工作关系的同事?


当然,这种对于新人入职和同事离职变得足够理性的心态并不是一蹴而就的。


你要知道,当付出了很多心血、寄予诸多希望的同事突然离职,但是其另谋高就的地方并没有上升一个台阶,那种心情……就像辛苦种的白菜被猪拱了一样难受,久而久之,心态自然就麻了。


铁打的营盘流水的兵,走了就走了,无需留念。


人在职场,最重要的还是自我的提升,淡薄的人情牵系可有可无,至少对于我而言是这样的。


共谋事,自然互帮互助,共同成长,可你要是另谋高就,那就祝你一路顺风,心中祝福,行动上就别指望了。


OK,个人部分唠叨完了,下面从旁观者的角度,讲讲关于如何避免离职,以及催促离职的一些看法,纯属个人看法,不一定正确,仅供参考。


二、如何降低离职率


如果希望团队成员不要离职那么频繁,我觉得可以从下面几个方面入手(不能控制的部分,如公司福利,薪资什么的,不在讨论之列)。


1. 招聘把控


说两点。


其一,尽量不要选择工作一年一换,连换三四年的人,论迹不论心。


其二,如实介绍公司和团队的现状,不要捧太高,否则入职后发现和预期差别较大,肯定留不住的。


2. 关注成长


根据我的大胆预估,90%的技术人员都是项目驱动成长型人才。


如果不做项目,或者做些没什么技术含量的项目,其技术成长就是0。


所以,作为管理者,需要关注下面每个人的成长情况,很多人会比较内向,有想法也不会主动说出来,此时,就需要管理者时不时一对一沟通,或者通过其他方式收集信息,或者对成员的状态有敏锐的观察。


这就比较考验管理者的水平了。


当然,也是有策略可以帮助下面的人成长的。


一是项目轮换制,但要注意度,且不可频繁,或者为了轮换而轮换,跨度也不要太大,要和对方兴趣契合。


比方说王二是Web的,张三十做Node的,王二对Node并不感兴趣,但是你觉得全栈对个人发展有好处,就强行安排王二去做Node相关的工作,这就不合适。


二是孵化内部项目,团队总是需要一些基础建设的东西,或者说提效的工具等,可以立为项目,让下面的人都参与。


通常这类内部项目无需考虑兼容性和代码美不美丽,因此,可以满足开发人员使用新技术的爽感,一举多得。


三是建立好专业培训流程,基础内容可以固化,在内部系统中沉淀,业务技术和前沿发展可以使用分享会的形式输出。


虽然实际效果可能就那样,但是,重要的是让下面的人知道团队有这份心,或者说,让下面的人觉得自己成长了。


不过,我有必要提醒下,千万不要为了让团队成员干得开心,必有成长而过度保护。


什么意思呢?


身在商业公司,在一个大的团体中,必然是有所权衡,在技术使用上是需要有所克制的。


比方说对外项目使用过于新颖的技术,或者还不成熟的技术,或者生僻的技术,虽然干活的人开心了,但是产品呢,以后维护的人呢?


又比方说项目组希望你可以多做几个运营活动帮忙拉新,你不能说运营活动没有技术含量,对技术成长没帮助,就推脱或拒绝。


3. 不要过度倾向某个人


团队中有个人你非常看好,工作积极,产出高,质量好,你要作为骨干培养,你特别害怕他离职。


于是,各种资源都向他倾斜,每次绩效都是前列。


这种做法是危险的,过度倾向某个人,对于团队的稳定性是非常大的考验。


因为会让人觉得不公平,除非你能保证信息足够的透明,此人的工作与产出有足够了信服力。


说到信息透明,不得不单独讲一下。


4. 信息顺畅,上传下达


​如果管理者,可以在团队内部建立一种机制,也就是信息可以触及到每一个人,无论是上下传达,还是左右互通的信息,那你的团队一定会非常的健康,效率绝对不会差。


古代的昏君之所以是昏君,就是奸臣拦截了下面的信息,只让皇帝知道希望皇帝知道的事情,本质上就是信息不顺畅导致。


大到国家,小到团队,道理都是类似的。


这里举一个我知道的例子。


有一个很勤勉的同行,接到了很多需求,然后他连续数周都干活到凌晨,实在累得受不了,然后提了离职。


领导知道后都傻了,怎么突然离职了,干得好好的。


细问才知道,领导根本不知道需求方绕过他给同事安排了很多“紧急”需求,这名同事也没有向上反馈活太多,需要帮助,以为是领导默许的。


这就是信息不顺畅导致的,我估计,这名同行的周报也没有好好写。


5. 管理者自身的提高


兵熊熊一个,将熊熊一窝。


管理者需要不断提高自身的管理水平,要不断提高自己的影响力和话语权。


一方面可以通过业绩支撑来提高,另一方面可以通过日常表现去强化,即表现出足够的管理素养。


公平公正,信息传达,人文关怀等。


具体不展开,大家可以去找找一些资深前端管理人员写的心得,会有不少的帮助。


6. 文化与氛围建设


即要通过一些制度或手段,让团队成员平时产生更多的交集。


常见且效果不错的方法就是团建,而最常见的团建方式就是一起吃饭。


或者邀请其他部门,或者外部的人进行分享。


总之,能够让大家聚在一起方法,都是可以尝试的。


但是需要注意不要用力过猛,比方说安排周末时间团建,或者吃饭都是自掏腰包,那就适得其反了。


记住,我们的目的是让下面的人都感觉到是团队中的一员,精神有所寄托自然可以有效降低离职的意愿。


三、如何让人主动离职


林子大了,什么样的鸟都有。


自然,团队中可能会有不合适的员工,例如,总是传递负面情绪,上班总是玩游戏刷股票,代码质量总是很糟糕。


这样的员工显然是不适合待在团队的。


如果是我,直接找HR,谈N+1开除,无需拖泥带水,我的风格就是雷厉风行。


但是,咳咳,N+1毕竟成本高,所以,就有些公司或者个人有些小心思,有没有办法让某个不合适的员工主动离职呢?


对于这个问题,我也不知道怎么办,我又没做过这样的事情。


随后我网上搜寻了一些方法,简单整理了下。


首先,先礼后兵。


指出问题,希望达到什么样的结果。


如果结果好,那就说明员工进步了,自然也就没有离职一说。


如果还是和之前一样不行,那就低绩效,可以连续低绩效。


然后工作这块,可以安排边缘工作,重复工作。


通常,对自己还有些要求的人此时就会选择更合适的地方。


但有些人就是觉得好死不如赖活着,就算每月拿死工资,不干正事也可以,那可以找HR,他们经验丰富,知道该怎么办。


至于其他一些方法……咳咳,我什么都不知道。


OK,以上就是我想说的内容,也是一时之间的有感而发,欢迎点赞,欢迎

作者:张鑫旭
来源:juejin.cn/post/7269792419544170548
评论交流。


(完)

收起阅读 »

情人节这天,跟女友 研究点餐小程序的目录 是怎么搞的?

web
今天情人节(七夕特辑) 🏩 背景(餐厅) 现在去餐厅订餐,桌角那里差不多都会有一个二维码,叫你扫码自己手机上点菜。扫码进去,要么是公众号的,要么就是小程序。 那我们来看看一个以下案例: 除了点餐系统外,还有一些文档目录用到这种方式的目录导航锚点。比如说...
继续阅读 »

今天情人节(七夕特辑)



🏩 背景(餐厅)



现在去餐厅订餐,桌角那里差不多都会有一个二维码,叫你扫码自己手机上点菜。扫码进去,要么是公众号的,要么就是小程序。



那我们来看看一个以下案例:


Kapture 2023-08-21 at 16.16.17.gif


除了点餐系统外,还有一些文档目录用到这种方式的目录导航锚点。比如说文档,或者掘金的右侧的目录也用到了同样的效果。


Kapture 2023-08-22 at 09.31.45.gif


🔪 剖析原理


监听scroll事件,获取分类最开始的offsetTop,拿当前页面的scrollTop跟这些offsetTop比较,到了就把左边菜单栏的那个分类变颜色。至于点击,点到哪个分类,就找到对应的右侧分类的标题的offsetTop,通过window.scrollTo(0, 要点击分类的右侧内容的offsetTop)就可以使得内容对应滚动到该位置。


🥬 上菜实操


数据结构:【nestjs返回的数据】如若只是测试,可以写死某些数据即可。


image.png


image.png


http://127.0.0.1:5173/


界面布局:【左(分类)右(内容)】分为两个区域,两边均可滚动。


image.png


<template>
<div class="order">
<div class="category">
<div
v-for="(item, key) in goods"
:class="{
'category-name': true,
active: currentKey === key,
}"

:key="key"
@click="changeCategory(key)"
>
{{ item.categoryName }}
</div>
</div>
<div ref="content" class="content" @scroll="handleScroll">
<div
v-for="(item, key) in goods"
:key="key"
class="content-item"
ref="categoryRefs"
>
<div class="title">{{ item.categoryName }}</div>
<div
class="each-item"
v-for="good in item.goodsList"
:key="good.goodsCode"
>
<div class="image-url">
<img :src="good.imagePathSmall" />
</div>
<div class="desc">
<div class="goods-name">{{ good.goodsName }}</div>
<div class="goods-slogan">{{ good.goodsSlogan }}</div>

<div class="bottom">
<div>¥{{ good.goodsStandardList[0].acturalPrice }}</div>
<div>+</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>

image.png


整体效果如下:


image.png


变量以及后端返回的商品列表:


image.png


逻辑:(一个是左侧点分类区域)、(一个是滚动右边内容,左侧分类要根据哪个分类变样式)


左侧点击分类区域:


image.png


content.value.scrollTo({
top: categoryRefs.value[key].offsetTop,
behavior: "smooth",
})

这里值得提的是,scrollTo加behavior方法进行滚动时,可以通过添加behavior参数来指定滚动的动画行为。(smooth 滚动行为具有平滑的动画效果,窗口会平滑地滚动到指定位置)


滚动右边内容:


image.png


当内容整体滚动的高度等于或者超过了某个分类的高度,那边就把左侧分类变样式就可以了,变样式通过key的方式来判断active: currentKey === key


注意点:


content.value.scrollTo({
top: categoryRefs.value[key].offsetTop,
behavior: "smooth",
});

因为我们设置的scrollTo是带有平滑滑动的属性的behavior: "smooth",所以导致点击左侧分类,自然而然也会触发到右侧的滚动事件,导致左侧跨多个点击的时候,中间所有的类都会改样式,问题如下:


Kapture 2023-08-22 at 10.46.59.gif


所以要解决这个问题,思路是,用一个变量,判断左侧点击彻底结束,右侧滚动事件才生效,解决办法如下:


image.png


最终效果


Kapture 2023-08-22 at 10.51.01.gif


🚶‍♀️ 总结消化


做点餐平台,重点(地图和定位服务支付平台和第三方支付配送跟踪);当然最后用户的体验设计也是很重要的,易用性、响应速度、交互设计的,都会影响到顾客当天来店里吃饭点餐的心情和体验。


以上是关于公众号或者小程序一般的点餐系统的大多数点餐页面滚动效果的研究。



爱在朝夕 不止七夕




☎️ 希望对大家有所帮助,如有错误,望不吝赐教,欢迎评论区留言互相学

作者:盏灯
来源:juejin.cn/post/7269786623813074996
习。


收起阅读 »

Stack Overflow 2023 开发者调查报告

iOS
众所周知,Stack Overflow 是全球最大的程序员问答社区,本篇带来它的 2023 开发者调查报告解析! 闲话少说,冲冲冲~ 2023 一共收集了 9 万份开发者的报告,他们反馈了自己正在使用的编程工具以及编程语言。完整的报告在:survey.stac...
继续阅读 »

众所周知,Stack Overflow 是全球最大的程序员问答社区,本篇带来它的 2023 开发者调查报告解析!


闲话少说,冲冲冲~


2023 一共收集了 9 万份开发者的报告,他们反馈了自己正在使用的编程工具以及编程语言。完整的报告在:survey.stackoverflow.co/2023




另外,今年与以往不一样的是对人工智能领域做了更加深入的调查,调查目的是想知道如今以 ChatGPT 为代表的 AIGC工具到底是否改变了开发人员的工作方式、还是只是一场炒作??详细报告在:hype-or-not-developers-have-something-to-say-about-ai 以及给出了一些见解 《developer-sentiment-ai-ml》(挖坑有空翻译~)


悄然变化


一些老程序员都习惯在 Stack Overflow 进行问答,从今年统计看,各个国家的回答率占比有所变化:美国仍然排第一、德国(增长30%)超越印度(下降50%)位列第二。


本次调查中,来自印度的开发人员平均年龄更加年轻,89% 低于 34 岁;而整体样本中,低于 34 岁的占比是 62%。


从整体角度来看,开发者年龄分布略有增长,有 37% 的程序员年龄大于 35 岁,而去年只有 31%;


今年的十大编程语言中,有三门语言的地位提高了,它们分别是:Python、Bash/Shell、C


《comparing-tag-trends-with-our-most-loved-programming-languages/》 了解到:过去三年,大家对 Python 的关注又在不断提升;




尤其是对于非职业的编程人员来说,Python 是一门相当不错的入手编程语言:




另外,C 语言重回台面,这个就很有意思了:尽管 C 是一门很古老的编程语言(始于1970),但在之前它从未进入开发者调查报告受欢迎语言的前十名,


C 语言作为一门基础语言,是嵌入式编程语言所需,从这个角度看,是不是意味着:设备的嵌入式编程开发近年也在急速发展?物联网正在发力。学习 C :Codecademy




薪资情况,调查显示 2023 年程序员整体收入将比去年增长约 10%;


其中最受欢迎的三种编程语言:JavaScript、HTML/CSS 和 Python,薪资中位数却出现了下降;


而一些小众语言,比如 APL 和 Crystal ,薪资增幅较大。由此推断,在 2023 年一些小众语言的程序员薪资上涨会更多!


期待使用


今年,在调查报告中还新增了一个概念区分,即“期望使用的编程语言”,在之前,我们只关注“受欢迎的语言”,这次还综合统计了大家的预期。




如图所示,Rust 是所有语言中最受欢迎且被期望继续使用的语言!有 80% 的 Rust 使用者选择将来会继续使用~(Rust 你用上了吗??)


而比如 JavaScript 使用者大约只有 60% 选择会继续使用它~~


薪资水平


另外,技术受欢迎,但是也肯定同样要考虑工资水平。其中,Rust、Elixir 和 Zig 语言的开发者薪资中位数比其它语言普遍高出 20%,年薪约 50+ 万人民币~ 薪资完整情况




(可惜没统计咱们的。。。)


欲知更多报告,观点在:《dev-survey-results-2023》


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

年中总结、再品苏轼、怀古望今、胡思漫谈

-- 迟到年终总结/虽迟但到 -- 现在是公元2023年7月22日,不知觉,2023年已过一半有余。 在整 1000 年前,1023 年,北宋宋仁宗开启“天圣”元年,仁宗在位四十二年,搜揽天下豪杰,不可胜数,其中就包括文坛 T1 阵容 —— 苏轼。 仁宗可以说...
继续阅读 »

-- 迟到年终总结/虽迟但到 --


现在是公元2023年7月22日,不知觉,2023年已过一半有余。


在整 1000 年前,1023 年,北宋宋仁宗开启“天圣”元年,仁宗在位四十二年,搜揽天下豪杰,不可胜数,其中就包括文坛 T1 阵容 —— 苏轼。


仁宗可以说是苏轼最大的伯乐,认苏轼有宰相之才,为他的诗词/才华直连拍手叫好。


而仁宗之后,苏轼便逐渐踏上了他的一生被贬之路。


怀古


step1


苏轼先是和王安石政见不一,自请至杭州做通判,苏堤春晓、三潭印月,欲把西湖比西子,淡妆浓抹总相宜。


现实中与人气场不合、难以互存,而又不得不共事的时候,确实是一件不幸之事。


王安石改革激进、求快,苏轼体恤民情,当宋神宗选择王安石的时候,苏轼就知道了:道不同不相谋。


从杭州再到湖州,远离庙堂、过足山水之瘾,有道是:“待君诗百首,来写浙西春”。


step2


苏杭山水之后,再是乌台诗案,被文字狱、坐牢 100 多天,被贬黄州;


在黄州的四年多时间,从“寂寞沙洲冷”转变为“一蓑烟雨任平生”,他天生是乐观的,能够快速的整理、修复情绪;


最后,黄州成就了苏轼,写出了千古流传的《赤壁赋》,“天地之间,物各有主”,不是我等能占有的,假若想要有无限的时光、享用无尽的自然美景,只得是把自己交给自然,“耳得之而为声,目遇之而成色,取之无禁,用之不竭”。


我认为,这种豁达,可以解忧,现代人的忙碌、焦虑、急切心态等。


事物兴衰、时间流转,不是我等能掌控的,占有欲再强,最后也是赤条条来去无牵挂。


不如就是“适之”,你我“共适”当下,便就是永久的享受了。


step3


虽然,不多久,苏轼又被高太后启用,但随着这位迷妹去世,哲宗启用新派、打击旧派,苏轼又被贬到惠州;“日啖荔枝三百颗,不辞长作岭南人”,即使离皇帝/权利/显贵越来越远,但并不妨碍他这样快活的心态;


step4


一贬再贬,晚年苏轼被贬到海南儋州,可谓是:天涯海角。有一首《西江月》:



世事一场大梦,人生几度秋凉?夜来风叶已鸣廊。看取眉头鬓上。


酒贱常愁客少,月明多被云妨。中秋谁与共孤光。把盏凄然北望。



有人分析说这是在儋州所作,有人分析说在黄州所作;


个人感觉,前者说法可能性更大,人生之短促,壮志之难酬,确实悲凉,难有青壮年的狂放、通达。


望今


p1


现在年轻人也是很艰难的,虽绝大部分人都没有苏轼这样有才华的诗词表达,但对于这种人生变迁的体味肯定是闷在心头,千滋百味、无法言说的。


时间转眼就没,就像李白所说:朝如青丝、暮成雪;


没有什么是永恒的,可能在一家公司日复一日、勤勤恳恳工作两、三年,转眼间,因为某一天来了一个擅长 PUA 的小领导,愤懑之下就裸辞;或者某一天,突然就不想起早八了,不想麻木/盲目了,毅然离开,像是重启、代谢更新,又像是一种惋惜,恨不得志;


当然没时间啦。


早八到晚八,或者早十到晚十,都一样,最普通的,基本工作时间8小时,还要提前准备、通勤;晚归后还要休息调整思路;就算是一点不加班,工作也要消耗掉一天二分之一的时间。正常的,还要有睡眠时间,7个、8个小时左右,剩下的,也就3、4小时;再除去内务整理、社交交友、家人长谈,还有几个属于自己的时间,可以用来思考:文学、艺术、哲学、宗教、政治、科学等等?


只要是随着这个时间流去走,不去挣扎的话,真的时间一晃而过。就像有人所说的,普通人能忙于物质生活都已经足够累了,还有几个能在此之外,建设/丰富精神世界?


这是给自己找理由吗?时间就像海绵里的水,挤一挤还是有的?怎么挤?不是人人都能凿壁借光、或闻鸡起舞,不然这故事也没什么好值得人敬佩的了。


有一说是:中国人把吃做到了极致,而欧洲人把闲做到了极致。


我辈当负大任,这也是时代的洪流所决定,不是个人的决定;长达三年的疫情的洪流,其实也就在今年才结束,相信还有许多人被影响、没走出来,但没谁会在乎,时间就是如水、如刀,不谈感情,继续往前,即使青丝变成白发。


2023 年,不一样的是什么,听到了许多,比如常谈的公众号已死、前端已死、B站收益已死、xxx已死,whatever,几乎没人会收集自己表皮更新而产生的死皮吧,落在满地,灰尘皆是,该怎样,还是怎样。


还是想到 2018 我说的一句话,每当年中,每当盛夏:“常言道:韶光易逝、寸暑难留、然其虚无、何以擒之”。夏天很好,夏夜更好,但没人能无穷尽的享用。或许,也只有想到,苏轼所说:共适此时,才得宝藏。


故:怎样都好。


p2


具体一点,2023 年上半年有什么不一样,自己敲定了人生大事之一,当然无非几件其一:出生、上学、考学升学、大学、就业、结婚、生子、循环往复,外乎还有买房、买车等等,就像大你十岁的人,聊天时,一定会问类似这些大事上的问题。


其次,工作转型,之前是前端,现在是项目经理;写文方向转型,之前写具体的前端技术文章,现在写 AIGC 的专栏;偶尔,用 AIGC 发一发想要翻译的潮流前线技术文章,但囿于自己时间、囿于自己执行力、囿于其它事情的权重取舍,所以现状就是这么个现状。


另外:发了很多知乎、但有起伏,马上7级;做了AIGC抖音号,获赞1k+;还有在同花顺论股,发声就是在生产观点、观点即内容,有冲突,也有价值,也会带来认知变化,以及有可能生产产生财富。


还有,改书一事,来回修订好几次,有种有心无力的感觉,给到的压力不小,但是怀疑自己究竟能否COVER;工作上事情很多,KPI 的压力也是直接到项目,薪酬结构调整、增量激励等等,没有人会在之前问你:你准备好了吗?你对这件事怎么看?要不等等你?


能留一个下午,去回看这些事,都是一种“偷窃”之举,得之所幸。


p3


再到,重点看看“起伏”这个事。红楼梦的经典在于此,“训有方、保不定日后作强梁;择膏梁,谁承望流落烟花巷”;


苏轼的几次被召、几次被贬也在于此,哀吾生之须臾、羡长江之无穷;


长安三万里高适、李白也如此,十年寒窗、十年还乡、十年扬州、十年边塞、十年朝堂、十年流亡;人生又几个十年,少小十年、老弱十年、睡梦十年又十年、乱哄哄,你方唱罢我登场。


说回股市,上半年关注很多,我是在想:财富的本质在于生产力,现在觉得也在于流动性;打工人是无产者,唯一能有产的就是,把微薄的薪水几成放到股市,同市场共振,让钱成为一种资产,成为一种员工,为自己所用。所以投资是开公司,我们打工,被当做资产来估价、人是一种资产、钱更应该是一种资产,怎样选对方向,是一直需要去专研的,虽然看不太明白,股市是最复杂的混沌系统,不能预测,但也只能尽全力在无序中找有序。


找规律、模仿、是我们一直在做的,将信息整合、同步各方,也是我们一直在做的,殊途同归、并无新鲜。


p4


再看以后,无复多言。做好工作,做好精神建设,做好“负熵”。


1、坚持输出、生产内容,输出倒逼输入,生产带来财富、流动带来财富;


2、做好身体锻炼,25 岁以后,身体代谢下降,真的是一个客观现实;如果每天手表的三个圆环都难合拢,身体只会走下坡路吧;


3、正能量/乐观,待人待事,在随波逐流和坚守原则之间权衡、适之。


最后用苏轼最经典之一的词总结:“回首向来萧瑟处,归去,也无风雨”。


所以,2023 年中,I`m fine,Thanks all,And you?


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

🐻优化GIF的内存加载

iOS
一、内存OOM问题 使用 UIImage.animatedImage(with:duration:) 方法:UIImage 类提供了一个便利的方法来加载并处理 GIF 图像,该方法可以将 GIF 图像转换为 UIImage 的动画表示。这种方法可以有效地管理内...
继续阅读 »

一、内存OOM问题


使用 UIImage.animatedImage(with:duration:) 方法:UIImage 类提供了一个便利的方法来加载并处理 GIF 图像,该方法可以将 GIF 图像转换为 UIImage 的动画表示。这种方法可以有效地管理内存,并且不需要手动处理每一帧的图像。但会存在内存问题,UIImage(contentsOfFile:) 虽然不会立即放入内存中,但显示时还是会加载到内存中。

if let gifURL = Bundle.main.url(forResource: "example", withExtension: "gif") {
let gifData = try? Data(contentsOf: gifURL)
let gifImage = UIImage.animatedImage(with: gifData)
imageView.image = gifImage
}

大量的GIF会导致OOM问题,一旦使用超过系统的阈值,就会崩溃。


二、使用FLAnimatedImageView可以有效的解决GIF内存暴涨的问题

  • FLAnimatedImageView 使用渐进式解码:FLAnimatedImageView 使用渐进式解码来加载 GIF 图片。渐进式解码允许在图片尚未完全加载时就开始显示并逐步增加清晰度。这意味着 FLAnimatedImageView 可以在加载 GIF 图片的同时,逐帧渲染和显示动画,而不需要等待整个 GIF 图片加载完成。这对于大型 GIF 图片特别有利,因为可以显著降低首次加载的延迟,并提高用户体验。
  • 内存优化:FLAnimatedImageView 在加载和显示大型 GIF 图片时进行了内存优化。它只会将当前帧所需的数据加载到内存中,并在显示下一帧时释放之前的帧数据,从而避免占用过多的内存。这有助于在加载大型 GIF 图片时降低内存使用,减少内存压力和 OOM 问题。

三、让FLAnimatedImageView支持网络GIF


FLAnimatedImageView 是用于显示 GIF 动画的 FLAnimatedImage 库中的特殊控件,它并不直接用于加载网络图片,但我们可以扩展方法为其增加加载网络图片的功能。

import FLAnimatedImage
import Kingfisher

extension FLAnimatedImageView {
func setGifImage(withURL url: URL) {
// 使用 Kingfisher 加载网络图片
self.kf.setImage(with: url, completionHandler: { result in
switch result {
case .success(let value):
// 成功加载图片,value.image 是 UIImage 类型
// 将加载的图片转换为 FLAnimatedImage 类型
let animatedImage = FLAnimatedImage(animatedGIFData: value.image.kf.gifRepresentation())
// 在 FLAnimatedImageView 中显示 GIF 动画
self.animatedImage = animatedImage
case .failure(let error):
// 加载图片失败,处理错误
print("Error loading image: (error)")
}
})
}
}

上述方法利用Kingfisher不仅添加了缓存,还能后直接显示来自网络的GIF图片。


四、测试效果


总体内存可以降低70%,CPU在迅速滑动时波动较大,大概为原来的1-2倍,但是停止滑动时降低为原来的50%左右。由于目前iPhone手机的CPU普遍较好,而内存较低;所以这种用CPU缓解内存压力的方法是可行的。


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

总是跳转到国内版(cn.bing.com)?New Bing使用全攻略

你是否想要使用强大的(被削后大嘘)New Bing? 你是否已经获得了New Bing的使用资格? 你是否在访问www.bing.com/new时提示页面不存在? 你是否在访问www.bing.com时总是重定向到cn.bing.com而使用不了New Bin...
继续阅读 »

你是否想要使用强大的(被削后大嘘)New Bing?


你是否已经获得了New Bing的使用资格?


你是否在访问www.bing.com/new时提示页面不存在?


你是否在访问www.bing.com时总是重定向到cn.bing.com而使用不了New Bing?


New Bing的使用本来就不需要依赖科学上网,看完下面的教程,不论你卡在哪一步,你都可以成功使用New Bing。


3.13更新


根据大量评论反馈,微软似乎已经让本文中的方法:“修改请求头X-Forwarded-For来伪装请求地址”的方法失效了。现在微软已经可以检测到请求的源IP了,使用科学上网(代理)的方法依然可用,使用前需清空cookie,否则还是国内特供版Bing。没有办法科学上网的朋友,目前暂时帮不上忙了,如果未来有新方法,我仍然会更新在文档中。


一、加入New Bing的候选名单


现在的情况是:访问 http://www.bing.com/new,会自动跳转到cn.bing.com
如果你是Chrome或者Edge浏览器(下面以Edge举例,Chrome也同理)可以通过以下扩展的方式,修改请求Header来防止被重定向。 


打开浏览器的扩展,找到 管理扩展 按钮,在打开的页面左侧找到 获取 Microsoft Edge 扩展 并打开。
搜索 ModHeader ,安装下面这个扩展。




接着在已有扩展中,找到这个扩展并点击打开。 


打开扩展后就会弹出下面这个弹窗,点击 FILTER,选择 Request URL filter (在Chrome中是 URL filter

  


填写下面三个内容(分别是:X-Forwarded-For8.8.8.8.*://www.bing.com/.*)并确保都勾选。 



设置好后,再次访问 http://www.bing.com/new ,登录自己的微软账号,点击加入候选名单即可。

  


静静等待Microsoft Bing发来的 “你已中奖”的邮件,或者你微软的邮箱不是你常用的邮箱,时不时重新访问一下 http://www.bing.com/new 也可以看到你是否已经 “中奖”。


2023.3.6更新


在这里,可能会有一些朋友遇到重定向次数过多,请清除Cookie的问题,地址栏会有很多相同的zh-CN“后缀”,可以尝试以下方法:

  1. 点击Request URL filters一行的右方加号,并添加一个 .*://cn.bing.com/.* 。这时候Request URL filters中,同时存在两个筛选规则,包括www与cn两个
  2. 清除bing相关网站的cookie。在设置->cookie与网站权限->管理和删除cookie->查看所有网站cookie->右上方搜索bing,然后删除所有相关的条目
  3. 如果还有登录账号时遇到类似的问题。可以先按上一步Cookie,然后关闭扩展,在cn.bing.com中登录账号,再开启扩展访问正常版本试试。

二、下载Edge Dev 目前普通版Edge也可以了


获得资格后,首先需要解决的问题应该是下载Edge的DEV版本,在除了dev版本的Edge以外的任何浏览器中,均不能使用带有 Chat 功能的 New Bing。


通过下面这个链接,下载dev版本的Edge
http://www.microsoft.com/en-us/edge


不要直接点页面中大大的下载按钮(那个是普通版),找到下面这个部分并打开我框住的链接。 


在打开的页面也不要直接下载,往下找到下面这个图片的部分:



确保Edge图标上有 DEV 字样,点击右侧的下载适合自己电脑的版本(macOS、Windows、Linux)


三、访问New Bing


在你走完上面的流程后,访问 http://www.bing.com 即可看到上面的导航栏有Chat字样。

点击后,开始你的New Bing使用之旅吧。 



四、手机访问New Bing


现在(更新时间2023.2.28)微软已经将New Bing带上了手机。


现在有了更方面的访问途径,使用手机的Bing App使用New Bing的Chat功能。
但国内的应用商城应该是名叫“微软必应”的阉割版,我这里找到了微软官方的下载地址:Microsoft Bing


不过我并不是通过这种方式下载的APP,我在谷歌的应用商城下载的Bing APP,如果上述官网的地址下载的APP在账号已拥有测试资格的情况下仍没有Chat功能,请自行尝试谷歌商店下载。


五、结语


有问题可以评论中询问我,请确保你清楚地描述出你遇到的问题和尝试过的方法。New Bing、ChatGPT还有本文的作者我,都需要你具备基本的提问题的能力。我有看到部分评论(CSDN)的朋友仅简单问了一句话,我根本无从得知你的问题的现状,自然也无法解决你的问题。请你清楚地思考完下面几个问题:

  1. 你进行到哪一步卡住了?
  2. 你尝试了哪些方法?
  3. 你是否完整阅读了本文?

如果你的问题描述不清楚,恕我拒不回答。


另外如果有帮到你成功访问到New Bing,还请不要吝啬你的点赞和收藏 ^_^


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

博客园宣布上线会员:弹尽粮绝,命悬一线

相比CSDN,博客园在商业化上还是比较克制的,没想到也到了乞求用户开会员得以存活的地步。博客园称,自从今年4月绝境求商之后,博客园苦苦支撑了4个月,能够支撑到现在,主要源于大家的捐助,一位园友的小额投资,以及天使投资方顺顺智慧的出手相救。博客园表示,在当前极其...
继续阅读 »
相比CSDN,博客园在商业化上还是比较克制的,没想到也到了乞求用户开会员得以存活的地步。

博客园称,自从今年4月绝境求商之后,博客园苦苦支撑了4个月,能够支撑到现在,主要源于大家的捐助,一位园友的小额投资,以及天使投资方顺顺智慧的出手相救。

博客园表示,在当前极其困难的情况下,其没有足够的力量在其他商业模式上搏一把,接下来博客园走出困境的唯一希望,取决于有多少园友选择成为VIP会员,目前已有140位会员。接下来,博客园将全力以赴开发更多会员权益。

自从今年4月绝境求商之后,园子苦苦支撑了4个月,能够支撑到现在,主要源于大家的捐助,一位园友的小额投资,以及天使投资方顺顺智慧的出手相救。感谢这段时间所有帮助过园子的朋友!

在当前极其困难的情况下,我们没有足够的力量在其他商业模式上搏一把,接下来园子走出困境的唯一希望,园子的命运就取决于——到年底有多少园友选择成为VIP会员。

园子生于偶然,长于天然,执于本然,困于漠然(商业化),难于突然(危机),接下来将交于自然——用户的自然选择。

不知道会有多少用户会选择帮助园子走出困境,但我们知道园子的存在与发展至今就是因为用户的选择。

不管结局如何,我们都无怨无悔,因为这是我们的选择,而且选择已经成为我们的使命。最无憾的人生之一就是找到自己的使命,并为之努力。

当2021年面临关站还是缴纳对园子来说巨额罚款的选择时,我们选择了坚持下去。

当2022年面临百度全面降权与收入暴降时,我们选择了坚持下去。

当2023年弹尽粮绝时,我们依然想选择坚持下去,但这一次如果没有足够多用户的支持,我们就没有足够的力量坚持下去。

会员上线,这是我们正式发布园子会员,之前放了一些链接,到目前已有140位会员,感谢这些园友的支持!

命悬一线,这根从用户到会员的感情线,聚少成多将成为园子的命运线。

接下来,我们会保持心态:谋事在园,成事在用户。

接下来,我们会满怀期待:有园千里来相会,会员万维来救园。

接下来,我们会火力全开:全力以赴开发更多会员权益,让园子的会员成为非常超值的会员。


来源:三言科技

收起阅读 »

Git stash 存储本地修改

前言 我们在开发的过程中经常会遇到以下的一些场景:当我们在 dev 分支开发时,可能需要临时切换到 master 拉分支修复线上 bug 或者修改其他分支代码,此时对于 dev 分支的修改,最常见的处理方式是将代码提交到远程后再切到对应的分支进行开发,但是如果...
继续阅读 »

前言


我们在开发的过程中经常会遇到以下的一些场景:

  • 当我们在 dev 分支开发时,可能需要临时切换到 master 拉分支修复线上 bug 或者修改其他分支代码,此时对于 dev 分支的修改,最常见的处理方式是将代码提交到远程后再切到对应的分支进行开发,但是如果 dev 分支的修改不足以进行一次 commit(功能开发不完整、还有 bug 未解决等各种原因),或者觉得提交代码的步骤过多,此时 dev 的修改就不好处理

  • 在开发阶段可能某个分支需要修改特定的配置文件才能运行,而其他分支不需要,那么当我们在这个分支和其他分支来回切换的时候,就需要反复的修改、回滚对应的配置文件,这种操作也是比较低效且麻烦的


而我们通过 Git 提供的 stash 存储操作,可以将当前分支的修改或者开发常用的配置文件存储至暂存区并生成一条存储记录,在需要使用时通过存储记录的索引直接取出,无需额外提交或单独保存,可以有效的解决以上的问题


Git stash 使用流程


1. 存储修改:我们可以使用 git stash 对 dev 的修改进行存储,存储修改后会生成一条存储记录




2. 查看存储记录:通过 git stash list 查看存储记录列表,存储记录的格式为:

stash@{索引值}:WIP on [分支名]: [最近的一次 commitId + 提交信息]



3. 多次存储记录相同:如果多次存储修改的过程中没有进行过 commit 提交,存储记录除了 索引值 之外将会完全相同,此时我们就无法快速辨识存储记录对应的修改内容




4. 存储记录标识


为了解决存储记录无法辨识问题,存储修改时可以用 git stash -m '标识内容' 对存储记录进行标识




此时我们再查看存储记录列表,就可以看到存储记录的标识,此时存储记录的格式为:

stash@{索引值}:on [分支名]: [标识内容]



5. 恢复存储:当我们在其他分支完成开发再回到 dev 分支时,就可以通过 git stash apply index 将指定的存储记录恢复至工作区,index 是存储记录的索引,未指定则恢复第一条存储记录




6. 删除存储


对于不再需要的存储记录,可以通过 git stash drop index 删除指定的存储记录,此时我们执行 git stash drop 删除第一条记录后再使用 git stash list 查看存储记录就已经少了一条了




如果所有的存储记录都不需要,可以使用 git stash clear 清除所有存储记录




Git Stash 命令


查看存储记录


查看存储记录列表

git stash list

查看 最近一次 存储记录的具体修改内容,即修改了哪些文件

git stash show

查看 指定索引 存储记录的具体修改内容

git stash show index
git stash show stash@{index}

存储修改


直接存储修改

git stash

存储修改,并添加备注

git stash -m '备注内容'

恢复存储记录



恢复存储记录的修改内容



恢复 最近一次 的存储记录

git stash apply

恢复 指定索引 的存储记录

git stash apply index
git stash apply stash@{index}

删除存储记录



对不需要的存储记录进行删除,可以删除部分或全部

  • 删除 最近一次 的存储记录
git stash drop
  • 删除 指定索引 的存储记录
git stash drop index
git stash drop stash@{index}
  • 删除所有的暂存修改
git stash clear

恢复并删除存储记录



恢复存储记录的同时删除对应的存储记录

  • 恢复并删除 最近一次 的存储记录
git stash pop
  • 恢复并删除 指定索引 的存储记录
git stash pop index
git stash pop stash@{index}

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