注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

假如互联网人都很懂冒犯

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。 脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。 阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。 一步跨进电梯...
继续阅读 »

大家好,我是老三,最近沉迷于听脱口秀,并且疯狂安利同事。


脱口秀演员常常说的一句话是:“脱口秀是冒犯的艺术”。最近我发现,同事们好像有点不一样了。




阳光灿烂的早上,趿拉着我的宝马拖鞋,跨上包浆的小黄车,屁股感受着阳光积累的炙热,往公司飞驰而去。


一步跨进电梯间,我擦汗的动作凝固住了,挂上了矜持的微笑:“老板,早上好。”


老板:“早,你还在呢?又来带薪划水了?”


我:“嗨,我这再努力,最后不也就让你给我们多换几个嫂子嘛。”


老板:“没有哈哈,我开玩笑。”


我:“我也是,哈哈哈。”


今天的电梯似乎比往常慢了很多。


我:“老板最近在忙什么?”


老板:“昨天参加了一个峰会,马xx知道吧?他就坐我前边。”


我:“卧槽,真能装。没有,哈哈。”


老板:“哈哈哈”。


电梯到了,我俩都步履匆匆地进了公司。


小组内每天早上都有一个晨会,汇报工作进度和计划。


开了一会,转着椅子,划着朋友圈的我停了下来——到我了。


我:“昨天主要……今天计划……”


Leader:“你这不能说没有一点产出,也可以说一点产出都没有。其实,我对你是有一些失望的,原本今年绩效考评给你一个……”


我:“影响你合周报了是吗?不是哈哈。”


Leader、小组同事:“哈哈哈“。


Leader:“好了,我们这次顺便来对齐一下双月OKR,你们OKR都写的太保守了,一看就是能完成的,往大里吹啊。开玩笑哈哈。”。


我:”我以前就耕一亩田,现在把整个河北平原都给犁了。不是,哈哈。”


同事:“我要带公司打上月球,把你踢下来,我来当话事人。唉,哈哈”


Leader、同事、我:“哈哈哈“。


晨会开完,开始工作,产品经理拉我和和前端对需求。


产品经理:“你们程序员懂Java语言、Python语言、Go语言,就是不懂汉语言,真不想跟你们对需求。开个玩笑,哈哈。”


我:“没啥,你吹牛皮像狼,催进度像狗,做需求像羊,就这需求文档,还没擦屁股纸字多,没啥好对的。不是哈哈。”


产品经理、前端、我:“哈哈哈”。


产品经理:“那我们就对到这了,你们接着聊技术实现。”


前端:“没啥好聊的,后端大哥看着写吧,反正你们那破接口,套的比裹脚布还厚,没事还老出BUG。没有哈哈。”


我:“还不是为了兼容你们,一点动脑子的逻辑都不写,天天切图当然不出错。不是哈哈。”


前端、我:“哈哈哈”。


经过一番拉扯之后,我终于开始写代码了。


看到一段代码,我皱起了眉头,同事写的,我顺手写下了这样一段注释:

/**
* 写这段代码的人,建议在脑袋开个口,把水倒掉。不是哈哈,开个玩笑。
**/

代码写完了,准备上线,找同事给我Review,同事看了一会,给出了评论。



又在背着我们偷偷写烂代码了,建议改行。没有哈哈。



同事、我:“哈哈哈”。


终于下班了,路过门口,HR小姐姐还在加班。


我:“小姐姐怎么还没下班?别装了,老板都走了。开玩笑哈哈。”


HR小姐姐:“这不是看看怎么优化你们嘛,任务比较重。不是,哈哈。”


HR小姐姐、我:“哈哈哈”。


我感觉到一种不一样的氛围在公司慢慢弥散开来,我不知道怎么形容,但我想到了一句话——


“既分高下,也决生死”。




写这篇的时候,想到两年前,有个叫码农小说家的作者横空出世,写了一些生动活泼、灵气十足的段子,我也跟风写了两篇,这就是“荒腔走板”系列的来源。


后来,他结婚了。


看(抄)不到的我只能自己想,想破头也写不不来像样的段子,这个系列就不了了之,今天又偶尔来了灵感,写下一篇,也顺带缅怀一下光哥带来的快乐。


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

数组去重的多种方式

iOS
前言 从数组中删除重复项是一项常见的任务,在 Swift 中,标准库没有直接提供一个系统函数给我们,必须自己实现这样的方法。 实现数组去重的方法有很多,今天来介绍一些常用的方法。 1、使用 Set 去重 Set 也是一个集合,只是它不包含重复项,利用这个特点,...
继续阅读 »

前言


从数组中删除重复项是一项常见的任务,在 Swift 中,标准库没有直接提供一个系统函数给我们,必须自己实现这样的方法。


实现数组去重的方法有很多,今天来介绍一些常用的方法。


1、使用 Set 去重


Set 也是一个集合,只是它不包含重复项,利用这个特点,我们可以简单的给一个数组去重:

let array: [Int] = [1, 1, 3, 3, 2, 2]
let set: Set<Int> = Set(array)
print(set)


上边的代码会打印去重之后的 [1, 2, 3],但结果也可能是 [3, 1, 2],也可能是 [3, 2, 1],这就涉及到 Set 的原始设计了,它内部的元素是无序的,不能保证固定的顺序,如果你对顺序有要求,就不能用 Set 来实现了。


2、巧用字典去重


我们都知道字典中是无法存储相同 Key 的,也就可以利用 Key 的唯一性来去重:

let array: [Int] = [1, 1, 3, 3, 2, 2]
var dic: [Int: Int] = [:]
array.forEach { dic[$0] = 0 }
print(dic.keys)


先把 array 遍历一遍,所有元素作为字典的 key 存储起来,最后再取 dic.keys 获得去重之后的数组。


但是字典的 key 也一样是无序的,而且使用字典会带来额外的性能开销,因此不推荐这种方式。


上边提到这两种方案都无法保证顺序,如果需要保证去重后的顺序和原数组保持一致,请看下边的几个方案。


3、利用 NSOrderedSet


NSOrderedSet 是 OC 时代的产物,继承自 NSObject,它可以像 Set 一样实现去重,也可以保证顺序:

let array: [Int] = [1, 1, 3, 3, 2, 2]
let orderSet = NSOrderedSet(array: array)
print(orderSet.array)


最终打印 [1, 2, 3],顺序和原数组保持一致,但是需要注意这玩意性能比 Set 差很多。


4、遍历数组去重


这也是最符合直觉的方法,把数组遍历一遍,创建个新数组,如果这个元素没有加入新数组就加进去,如果加过了就抛掉:

let array: [Int] = [1, 1, 3, 3, 2, 2]
var newArray: [Int] = []
array.forEach { item in
    if !newArray.contains(item) {
        newArray.append(item)
    }
}
print(newArray)


最终打印 [1, 3, 2],也是保持顺序的。


为了更方便调用还可以将这个方法写个数组扩展:

extension Array where Element: Hashable {
    var unique: Self {
        var newArray: Self = []
        forEach { ele in
            if !newArray.contains(ele) {
                newArray.append(ele)
            }
        }
        return newArray
    }
}


这样调用的时候就方便了:

let array: [Int] = [1, 1, 3, 3, 2, 2]
print(array.unique) // [1, 3, 2]


5、filter 高阶函数 + Set


Set 在插入元素的时候调用 insert 函数,这个函数返回一个元组,第一个值是一个 Bool 类型代表是否插入成功,如果已经包含了这个元素则不能插入成功,另一个是被插入的这个元素,这在 Set 的函数声明中可以看得出来:

func insert(_ newMember: Element) -> (inserted: Bool, memberAfterInsert: Element)


我们可以利用这个特性,再加上 filter 这个高阶函数,来给集合 Array 写一个扩展:

extension Array where Element: Hashable {
    var unique: Self {
        var seen: Set<Element> = []
        return filter { seen.insert($0).inserted }
    }
}


调用方法和上边的方式一样。


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

日志打得好,代码差不了

1. 前言 众所周知,一个完善的日志体系对于开发的顺利进行至关重要。尽管构建这样一个系统可能需要与开发几个功能模块相当的时间,但为了在日常开发中高效地记录日志,我进行了以下尝试。希望这些经验能为后端入门的同学和对日志管理不够熟悉的朋友们提供帮助。 (本文面向后...
继续阅读 »

1. 前言


众所周知,一个完善的日志体系对于开发的顺利进行至关重要。尽管构建这样一个系统可能需要与开发几个功能模块相当的时间,但为了在日常开发中高效地记录日志,我进行了以下尝试。希望这些经验能为后端入门的同学和对日志管理不够熟悉的朋友们提供帮助。


(本文面向后端入门同学或是对日志管理缺乏深入了解的朋友)


2. 注解+手动记录


使用注解@Sl4j来自动创建日志类,再通过log.info()手动记录日志,是一种常见且相对灵活的方法。这样,我们可以在需要的任何地方记录所想要的日志内容。


然而,在实际开发中,这种方法可能会导致大量的日志与业务逻辑代码混杂,增加了代码与日志的耦合度。当需要修改业务逻辑时,往往还需要调整相关的日志,这可能导致重复记录,如请求参数和鉴权信息。


尽管如此,我并不是完全反对这种日志处理方法。其简单性和灵活性是其显著优点。但建议开发者结合其他方法,以减少日志与业务逻辑的耦合。


3. AOP统一处理


首先,我会介绍如何利用AOP自动记录日志以及哪些日志适合用AOP来处理。


3.1 请求详情日志

使用AOP统一处理请求详情日志能够有效地解耦控制层和业务层,这是一个非常高效的策略。以下是一个示例,展示如何使用AOP通知类记录详细的请求信息:


@Slf4j
@Aspect
@Component
public class LoggingAspect {

/* 日志输出被访问的接口url */
@Before("execution(* com.steadon.example.controller.*.*(..))")
public void beforeRequest(JoinPoint joinPoint) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();

String httpMethod = request.getMethod();
String remoteAddr = request.getRemoteAddr();
String requestURI = request.getRequestURI();
String queryString = request.getQueryString();
String params = "";

if ("POST".equalsIgnoreCase(httpMethod)) {
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
params = Arrays.toString(args);
}
}

log.info("Request Info: IP [{}] HTTP_METHOD [{}] URL [{}] QUERY_STRING [{}] PARAMS [{}]",
remoteAddr, httpMethod, requestURI, queryString, params);
}
}

通过这个AOP通知类,我们可以轻松地记录详细的请求信息。在遇到问题时,分析此日志通常可以帮助我们迅速找到原因,例如前端参数传递错误或后端参数接收问题。效果图如下:


image.png


3.2 其他

你可以利用AOP来拦截几乎任何切点,无论是消息队列、Mybatis的Mapper操作,还是特定的循环,都可以织入相应的通知。然而,使用AOP时,我们也必须权衡其成本。


AOP的优势并不仅仅在于其性能或易用性,而是它出色的解耦能力。选择是否使用AOP应基于你的日志和业务逻辑之间的耦合程度。只有当耦合度足够高,需要解耦时,AOP才真正显示其价值。


4. Lark机器人 + 全局异常处理


我相信许多读者都已经熟悉飞书机器人(Lark机器人)。通过全局异常捕获并调用飞书机器人进行告警,这是一种极为有效的业务告警通知方式。你可能会想:“哦,这个我知道。” 但是,具体如何实现呢?其实操作起来并不复杂,接下来我会详细介绍:


4.1 创建告警群聊

对于简单的报警,我们通常选择创建群聊机器人,而不是单独的机器人应用。这样做的好处是,我们可以动态管理需要接收告警的开发团队成员。


4.2 创建自定义机器人


  1. 在群聊中,依次选择:设置 -> 群机器人 -> 添加机器人。


image.png



  1. 在机器人详情页面,获取webhook地址。请确保此地址保密。


image.png



  1. 接下来,在代码中集成飞书机器人的告警功能。下面是如何在全局异常处理中集成飞书机器人的示例代码:


/* 飞书机器人告警 */
public String sendLarkNotification(String webhookUrl, String user, String title, String messageBody) throws Exception {
URL url = new URL(webhookUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

connection.setDoOutput(true);
connection.setRequestMethod("POST");
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");

ObjectMapper mapper = new ObjectMapper();
ObjectNode root = mapper.createObjectNode();
ObjectNode content = root.putObject("content").putObject("post").putObject("zh_cn");
content.put("title", title);

ArrayNode contentArray = content.putArray("content");
ArrayNode atUserArray = contentArray.addArray();
atUserArray.addObject().put("tag", "at").put("user_id", user);

ArrayNode messageArray = contentArray.addArray();
messageArray.addObject().put("tag", "text").put("text", messageBody);

root.put("msg_type", "post");

byte[] input = mapper.writeValueAsBytes(root);

try (OutputStream os = connection.getOutputStream()) {
os.write(input, 0, input.length);
}

try (BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
response.append(line);
}
return response.toString();
}
}

代码相对简单,主要逻辑是通过HTTP请求向webhook地址发送带有告警信息的POST请求。为了实现这个请求,我们需要在全局异常处理中调用上述方法。为了便于展示,我直接在全局异常处理类中编写了这个方法。但读者可以考虑创建一个专门的工具类来实现这一功能。接下来,我将展示我的全局异常处理类的逻辑:


@ControllerAdvice
public class GlobalExceptionHandler {

@Value("${notifications.larkBotEnabled}")
private boolean larkBotEnabled;

@ExceptionHandler(value = Exception.class)
public CommonResult<String> handleException(Exception e) {
if (!larkBotEnabled) return CommonResult.fail();
// Send notification to Lark
String title = "线上BUG通报";
String user = "all";
String webhookUrl = "https://open.feishu.cn/open-apis/bot/v2/hook/f4150b6c-xxxx-xxxx-xxxx-xxxx-xxxx";

try {
String s = sendLarkNotification(webhookUrl, user, title, e.getMessage());
return CommonResult.fail(s);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}

你可能会对larkBotEnabled感到好奇。实际上,全局异常捕获并不会自动区分线上环境与开发环境。为了能够在不同的环境中控制是否发送告警,我们使用了这个变量。通过在线上和本地的配置文件中设置不同的值,我们可以轻松地区分这两种环境。


application.yml


spring:
profiles:
active: dev #决定是否使用本地配置
application:
name: app-name

notifications:
larkBotEnabled: true #自定义字段控制报警行为

application-dev.yml


notifications:
larkBotEnabled: false

这样,我们就可以根据不同的环境来决定是否发送告警。以下是告警效果的示例:


image.png


这种告警方式既简洁又直观,帮助我们迅速定位问题。当然,针对不同的问题,我们还可以进一步对告警进行分级处理。


总结:在大型项目中,日志体系的构建需要大量的时间和资源。虽然在实际业务中,我们可能会使用更复杂的日志系统和告警机制,但本文提供的方法对于日常开发已经足够使用。如有任何建议或纠正,欢迎在评论区提出!


作者:雾山小落
来源:juejin.cn/post/7280864416554500131
收起阅读 »

某律师执业诚信信息公示平台字体加密解决思路

web
本文章只做技术探讨,请勿用于非法用途。 目标网站 为持续加深对律法的学习, 我们需要再来收集一些数据。 本文来解决 credit.acla.org.cn/ 这个网站的字体加密问题。 网站分析 网站反爬 这个网站的各种反爬措施还挺多的, 接口加密啊, 验证码啊...
继续阅读 »

本文章只做技术探讨,请勿用于非法用途。



目标网站


为持续加深对律法的学习, 我们需要再来收集一些数据。


本文来解决 credit.acla.org.cn/ 这个网站的字体加密问题。


网站分析


网站反爬


这个网站的各种反爬措施还挺多的, 接口加密啊, 验证码啊(每个页面都有), 无限 debugger 啊, 什么的, 还是挺烦的, 如果不要求效率的话可以考虑用 selenium 来过掉, 这里重点来解决一下字体加密的问题。


加密分析


首先来看下字体加密什么样子。


image.png


如图, 为律所详情页的截图, 可以看到啊, 这个 标签下的字体为加密字体, 这个网站他大多数数据信息都会像这样来做一个加密。


开整


首先来说下解决的方法。




  1. 找到字体文件。




  2. 确定文件字体与网站字体的映射关系。




  3. 替换网站字体。




字体文件获取


image.png


刷新页面, 勾选字体栏即可看到返回的页面, 直接下载下来即可。



有些网站可能会返回多个字体文件来迷惑你, 这时候可以全局搜索 ttf 等字体文件的关键词, 来读相关代码来找到前端页面解密时用的具体是字体文件。



字体文件解析


字体文件处理可以用 TTFont 工具, 我们先将文件解析成可读的 xml 格式来看下这到底是什么个东西。


image.png


下载字体文件保存为本地 font.ttf, 然后解析为 font.xml。


image.png


可以看到文件里是一些映射关系, 和一些字形的信息, 如果是简单的数字加密或是很少的字体加密的话, 这一步直接拿到映射关系就可以用了, 但是这个网站他每次的字体文件都不一样, 所以这种简单的映射关系不可用。


image.png


在字体编辑软件里也可以看到是对哪些字体进行了修改加密, windows 可以用 font creator , mac 上我用的是 FontForge 来解析的。


映射关系获取


上一步我们拿到了字形信息, 这里来生成提供一个通用的方法来做映射关系。



font.ttf 文件中通过 unicode 码来标记对应的字体的字形信息, 我们也可以用同样的方式, 获取加密字体对应的原字体的字形信息(固定不变的), 以此为 key 来设计映射关系。



image.png


这里定义了一个全局变量 font_map 来存储映射关系, 通过 PIL 的 ImageFont 对象来将字体的字形信息复现出来, 然后通过 ocr 技术得到字的原型, 完成解密。将解密过的字存入 font_map, 随着收录的字越来越多, 解析效率会越来越高。


加密字体替换


image.png


这里没什么难度, 做一个简单的替换就好。


结语


这个也是我首次接触这种麻烦些的字体加密, 就想写出来权当分享, 思路也是借鉴于之前看到的一个帖子(找不见了。。)。 文中有些东西需要自己去调试后可能会理解更深些, 因为写的过程中被其他事情打断了几次, 之前整理的思路乱掉了, 写的可能不太顺畅, 大家哪里不懂的话可以留言讨论吧, 或者有什么更好的思路也欢迎来交流。


作者:Glommer
来源:juejin.cn/post/7272399042091909131
收起阅读 »

关于我的人生(假如我工作13年就能得到3w个花西币)

关于我的人生(假如我工作13年就能得到3w个花西币) 前言 我一直在思考一个问题,我们真的有正视过未来吗? 其实大家可能会遇到一种可能就是平平淡淡的过完一生,什么20岁CEO、年纪轻轻福布斯前几?这种小说中主角的模板可能不一定会出现在我们身上。(这里并不是嘲笑...
继续阅读 »

关于我的人生(假如我工作13年就能得到3w个花西币)


前言


我一直在思考一个问题,我们真的有正视过未来吗?


其实大家可能会遇到一种可能就是平平淡淡的过完一生,什么20岁CEO、年纪轻轻福布斯前几?这种小说中主角的模板可能不一定会出现在我们身上。(这里并不是嘲笑,谁不想拥有爽文男主的人生,笔者还是希望大家能过得很好)


这段时间我正处于离职状态就一直在思考,我究竟是在做什么?


毕业了两年,在一家公司做了两年拿了一笔不算多不算少的薪水,然后每天不知道为何一直忙碌着,这也许就是大部分人的现状。


image.png


image.png



有很多人一生都不知道,自己要做什么?



这几天我重新看到这句话,我就感觉说得真的很对


那么就会有人说,我一生要做的事情就是搞钱,没错很多人都是如此。


image.png


关于我




  • 目前做前端开发,月薪9k,2年工作经验,工作中算不算特别出彩,但是也有一定能力,简单说普通




  • 学历: 本科(非211、985)




  • 性格: 偏内向,朋友较少,交际一般




  • 爱好: 游戏、跑步、健身




  • 不良爱好: 无




预景


目前,25岁,假设工作到38岁10年时间薪资预计如下



  • 3-5年工作经验 11-14k 继续工作3年 一年按照13.2w算 三年39.6万

  • 5-10年工作经验 14-18k 继续工作5年 一年16.w算 5年84万

  • 10-15年工作经验 20-22k 继续工作5年 一年24w 5年 120万


预计会获得243.4w ,13年时间,当前这是目前比较客观并且没有考虑环境通货膨胀等其他元素的情况下,并且涨薪也是比较优异的情况下,不吃不喝工作13年获得的理想薪资。



是不是看着挺多,哇200多万呢,换算成花西币能有3w枚



那么按照我目前工作的地点厦门,我不吃不喝13年,可能连房子都买不起


副业的可能


副业:这个东西不一定大家都适合,其实我感说许多人都想过,但是大家真的挣到钱了吗?


我这里可能可以直接给出答案没有,为什么?



我自己就是一个实例:



上面介绍了我是一个程序员,上班时间是9:00~18:00通常是朝9晚6,如果项目赶还需要加班,那么我的副业,肯定需要一点时间灵活,那么就限制了很多,固定时间工作我都做不了


image.png



那么这样说大家就会想到几个选项




  • 自媒体

  • 滴滴

  • 骑手

  • 接私活

  • 摆摊


这是我目前能想到几个,我先逐个分析



自媒体



目前我有尝试过,那么问题来了我播什么?才艺没有颜值,额虽然是吴彦祖级别,身材是彭于晏,大家v我50,我可以继续吹下去


image-20230917135313799


不过我认为:做自媒体依然是最佳的选择,我现在虽然没有什么可以播的东西,但是我依然在寻找一个自己适合的方向,我个人认为内容应该有具备以下特点



  • 自己一直在坚持做的东西,例如: 我喜欢吃美食、经常会探店,这样你才有持续的内容输出

  • 稀缺的东西,例如:网上很多探店自助、米其林、高端,说白了就是平常少接触的东西

  • 能力主播(搞笑、颜值、特长、跳舞等等),这不得不说颜值也是一种优势,这个大家都懂


暂时就想到这么多,目前虽然做自媒体的人很多,但是依然有很多机会。


我自己也在寻求机会和方向



滴滴、骑手



这个两个我就一起说了仁者见仁,智者见智,首先个人肯定不推荐,你上班一天做了8小时,晚上下班继续跑滴滴、送外卖,说实话特别苦,我之前就跑了一段时间,收入非常惨淡,同时收入不具备发展性


我之前下班6.30左右,吃完饭7点左右去送外卖10点左右回家,可能3小时才30左右,一单4块一般,而且单子少,人家5点多-7点是外卖最多的时候,你这时候也是刚才下班去吃完饭,7-8都是晚吃饭。



时间冲突,并且收入不可观,如果是娱乐逛逛风景可以跑着玩



下面附上我送外卖的收入,可以说一晚上2 3小时最多收入30-40左右一天


image-20230917140243436



摆摊



摆摊:如果是我可能不考虑,周末可以一试,但是周一到周五,可能自己没办法摆摊


原因:


摆摊一般都是卖吃的6-7点下班。


假设材料你都以前准备好了,回家出摊7点左右,这时候饭点都差不多过了,怎么可能卖得出去,就算不是卖吃的下班人都回家,客流量小了,肯定会印象收益



接私活



接私活这一项我自己感觉太难了,说实话,除非是公司不忙的人,可以考虑。


例如: 我上班一天8小时,都在面对电脑,下班了还得继续干,说实话,我很难坚持,也许是我比较弱鸡,我自认为自己毅力还是有的,即使这样我自己也不太会去考虑私活


我认识的有些大佬是真的强,不过也许有些也是上班不忙的时候写,具体还是看自己的情况。


结论


目前环境真的很不好,互联网现在已经开始饱和了,后面失业的人可能会越来越多,同时环境也会越来越差,想到在这环境下面生存真的很不容易。加油!!!都看到这里不妨点个赞!



作者:柒丶月
来源:juejin.cn/post/7280747833384484919
收起阅读 »

Kotlin Flow入门

Flow作为Android开发中的重要的作用。尤其在Jetpack Compose里左一个collect,右一个collect。不交接Flow而开发Android是寸步难行。作为一个入门文章,如果你还不是很了解Flow的话,本文可以带你更进一步的了解Flow。...
继续阅读 »

Flow作为Android开发中的重要的作用。尤其在Jetpack Compose里左一个collect,右一个collect。不交接Flow而开发Android是寸步难行。作为一个入门文章,如果你还不是很了解Flow的话,本文可以带你更进一步的了解Flow。


Flow是一个异步数据流,它会发出数据给收集者,最终带或者不带异常的完成任务。下面我们通过例子来学习。


假设我们正在下载一幅图片。在下载的时候,还要把下载的百分比作为值发出来,比如:1%,2%,3%,等。收集者(collector)会接收到这些值并在界面上以合适的方式显示出来。但是如果出现网络问题,任务也会因此终止。


现在我们来看一下Flow里的几个API:



  • 流构建器(Flow builder)

  • 操作符(Operator)

  • 收集器(Collector)


流构建器


简单来说,它会执行一个任务并把值发出来,有时也会只发出值而不会执行什么任务。比如简单的发出一些数字值。你可以把流构建器当做一个发言人。这个发言人会思考(做任务)和说(发出值).


操作符


操作符可以帮助转化数据。


我们可以把操作符当做是一个翻译。一个发言人说了法语,但是听众(收集器)只能听懂英语。这就需要一个翻译来帮忙了。它可以把法语都翻译成英语让听众理解。


当然,操作符可以做的远不止这些。以上的例子只是帮助理解。


收集器


Flow发出的值经过操作符的处理之后会被收集器收集。


收集器可以当做是收听者。实际上收集器也是一种操作符,它有时被称作终端操作符


第一个例子


flow { 
(0..10).forEach {
emit(it)
}
}.map {
it * it
}.collect {
Log.d(TAG, it.toString())
}

flow {}->流构建器
map {}->操作符
collect {}->收集器

我们来过一下上面的代码:



  • 首先,流构建器会发出从0到10的值

  • 之后,一个map操作符会把每个值计算(it * it)

  • 之后,收集器收集这些发出来的值并打印出来:0,1,4,9,16,25,36,49,64,81,100.


注意:collect方法把流构建器和收集器连到了一起,这个方法调用之后流就开始执行了。


流构建器的不同类型


流构建器有四种:



  1. flowOf():从一个给定的数据集合生成流

  2. asFlow(): 一个扩展方法,可以把某个类型转化成流

  3. flow{}: 我们例子中使用的方法

  4. channelFlow{}:使用构造器自带的send方法发送的元素构建流


例如:


flowOf()


flowOf(4, 2, 5, 1, 7) 
.collect {
Log.d(TAG, it.toString())
}

asFlow()


(1..5).asFlow()
.collect {
Log.d(TAG, it.toString())
}

flow{}


flow {
(0..10).forEach {
emit(it)
}
}
.collect {
Log.d(TAG, it.toString())
}

channelFlow{}


channelFlow {
(0..10).forEach {
send(it)
}
}
.collect {
Log.d(TAG, it.toString())
}

flowOn操作符


flowOn这个操作符可以控制flow任务执行的线程的类型。在Android里一般是在一个后台线程执行任务,之后在界面上更新结果。


下面的例子里加了一个500毫秒的延迟来模拟实际任务。


val flow = flow {
// Run on Background Thread (Dispatchers.Default)
(0..10).forEach {
// emit items with 500 milliseconds delay
delay(500)
emit(it)
}
}
.flowOn(Dispatchers.Default)

CoroutineScope(Dispatchers.Main).launch {
flow.collect {
// Run on Main Thread (Dispatchers.Main)
Log.d(TAG, it.toString())
}
}

本例,流的任务就会在Dispatchers.Default这个“线程”里执行。接下来就是要在UI线程里更新UI了。为了做到这一点就需要在UI线程里collect


flowOn操作符就是用来控制任务执行的线程的。它的作用和RxJava的subscribeOn类似。


Dispatchers主要有这些类型:IODefaultMain。flowOn和CoroutineScope都可以使用Dispatchers来执行任务执行的“线程”(暂且这么理解)。


使用流构造器


我们通过几个例子学习。


移动文件


这里我们用流构造器新建一个流,让流任务在后台线程执行。完成后在UI线程显示状态。


val moveFileflow = flow {
// move file on background thread
FileUtils.move(source, destination)
emit("Done")
}
.flowOn(Dispatchers.IO)

CoroutineScope(Dispatchers.Main).launch {
moveFileflow.collect {
// when it is done
}
}

下载图片


这个例子构造一个流在后台线程下载图片,并且不断的在UI线程更新下载的百分比。


val downloadImageflow = flow {
// start downloading
// send progress
emit(10)
// downloading...
// ......
// send progress
emit(75)
// downloading...
// ......
// send progress
emit(100)
}
.flowOn(Dispatchers.IO)

CoroutineScope(Dispatchers.Main).launch {
downloadImageflow.collect {
// we will get the progress here
}
}

现在你对kotlin的流也有初步的了解了,在项目中可以使用简单的流来处理异步任务。


什么是终端操作符


上文已经提到过collect()方法是一个终端操作符。所谓的终端操作符就是让流跑起来的挂起方法(suspend function)。在以上的例子中,流构造器构造出来的流是不动的,让这个流动起来的操作符就是终端操作符。比如collect


还有:



  • 转化为各种集合的,toList, toSet

  • 获取第一个first,与确保流发射单个值的操作符single

  • 使用reduce, fold这类的把流的值规约到单个值的操作符。


比如:


val sum = (1..5).asFlow()
.map { it * it } // 数字 1 至 5 的平方
.reduce { a, b -> a + b } // 求和(末端操作符)
println(sum)

冷热流


前面的例子里的流都是冷流。我们来对比一下流的不同:


冷流热流
收集器调用的时候开始发出值没有收集器也会发出值
不存储数据可以存储数据
不支持多个收集器可以支持多个收集器

冷流,如果带上了多个收集器,流会每次遇到一个收集器就从头把完整的数据发送一次。


热流遇到多个收集器的时候,流会一直发出数据,收集器开始收集数据的时候遇到的是什么数据就收集什么数据。热流的多个收集器共享一份数据。


冷流是推模式,热流是拉模式。


下面看几个例子:


冷流实例


fun getNumbersColdFlow(): ColdFlow<Int> {
return someColdflow {
(1..5).forEach {
delay(1000)
emit(it)
}
}
}

开始收集


val numbersColdFlow = getNumbersColdFlow()

numbersColdFlow
.collect {
println("1st Collector: $it")
}

delay(2500)

numbersColdFlow
.collect {
println("2nd Collector: $it")
}

输出:


1st Collector: 1
1st Collector: 2
1st Collector: 3
1st Collector: 4
1st Collector: 5

2nd Collector: 1
2nd Collector: 2
2nd Collector: 3
2nd Collector: 4
2nd Collector: 5

两个收集器都从头获取到流的数据,在每次收集的时候都相当于遇到了一个全新的流。


热流实例。本例会设置一个热流每隔一秒发出一个1到5的数值。


fun getNumbersHotFlow(): HotFlow<Int> {
return someHotflow {
(1..5).forEach {
delay(1000)
emit(it)
}
}
}

现在开始收集:


val numbersHotFlow = getNumbersHotFlow()

numbersHotFlow
.collect {
println("1st Collector: $it")
}

delay(2500)

numbersHotFlow
.collect {
println("2nd Collector: $it")
}

输出:


1st Collector: 1
1st Collector: 2
1st Collector: 3
1st Collector: 4
1st Collector: 5

2nd Collector: 3
2nd Collector: 4
2nd Collector: 5

StateFlow


在Android开发中,热流的一个很重要的应用就是StateFlow


StateFlow是一种特殊的热流,它可以允许多个订阅者。如果你使用了jetpack compose来开发app的话,StateFlow可以简单而高效的在app的不同地方享状态(state)。因为热流只发送当前的状态(而不像冷流那样从开始发送值)。


要新建一个StateFlow,可以使用MutableStateFlow,然后给它一个初始值:


val count = MutableStateFlow(0)

在这里新建了一个叫做count的StateFlow,初始值为0。要更新它的值可以使用update方法,或者value属性:


this.count.update { v -> v + 1 }
this.count.value = 10

这时,订阅了count状态的订阅者就可以收到更新之后的值了。要订阅可以这样:


count.collect {
//...
}

在冷热流之外还有两种流:回调流和通道流。这个后面会详细讲到。


SharedFlow


SharedFlow也是一种热流,主要用于事件流。它会对所有的活的收集器发送事件。不同的消费者可以在同一时间收到同一个事件。


可以使用MutableSharedFlow()方法来创建一个SharedFlow对象。可以通过replay参数指明多少个已经发送的事件可以再发送给新的收集器,默认的是0。也即是在默认情况下,收集器只会接收到开始收集之后发送过来的事件。


这个时候可以来一个例子了:


class TickHandler(
    private val externalScope: CoroutineScope,
    private val tickIntervalMs: Long = 5000
) {
    // Backing property to avoid flow emissions from other classes
    private val _tickFlow = MutableSharedFlow<Unit>(replay = 0) // 1
    val tickFlow: SharedFlow<Event<String>> = _tickFlow // 2

    init {
        externalScope.launch {
            while(true) {
                _tickFlow.emit(Unit) // 3
                delay(tickIntervalMs)
            }
        }
    }
}

class NewsRepository(
    ...,
    private val tickHandler: TickHandler, // 4
    private val externalScope: CoroutineScope
) {
    init {
        externalScope.launch {
            // Listen for tick updates
            tickHandler.tickFlow.collect { // 5
                refreshLatestNews()
            }
        }
    }

    suspend fun refreshLatestNews() { ... }
    ...
}

示例解析:



  1. MutableSharedFlow声明了一个变量_tickFlow

  2. 定义了属性tickFlow

  3. 在初始化的时候使用SharedFlow成员变量_tickFlow每隔一段时间发送一个空事件

  4. NewsRepository类里声明成员变量tickHandler

  5. NewsRepository初始化之后开始收集事件,并在收集到事件之后调用refreshLatestNews方法来更新新闻。


看完这个例子再结合上面的介绍就会更加深入的了解SharedFlow了。


注意



  • 这SharedFlow是用于事件流处理的,可不是用来维护状态(state)的。

  • SharedFlow的另外一个重要的参数是extraBufferCapacity,它决定了流要在缓存里保留多少个发送过的事件。缓存满了之后会把缓存里面的一个值清理掉,并放入新的值。

  • 要处理缓存溢出的问题可以给onBufferOverflow指定一个方法。比如当缓存满了之后,并遇到新的事件的时候清理掉最旧的值或者暂停发送新事件一直到缓存有空余。

  • 可以使用tryEmit方法来检测是否存在一个活的收集器。这样可以避免无效的事件发送。


热流的坑


如果在同一个协成里订阅了多个热流,只有第一个才会被收集。其他的永远不会得到数据。


所以,要在同一个协成里订阅多个热流可以使用combine或者zip操作符把这些热流都合成到同一个流里。或者分别在每个协程订阅一个热流。


例如:


coroutineScope.launch {
hotFlow1.collect { value ->
// 处理收到的数据
}
hotFlow2.collect { value ->
// 永远不会执行到
}
}

在本例中,第二个collect不会收到数据。因为第一个collect会运行一个无限循环。


背压 (Backpressure)


背压,顾名思义,当消费者消费的速度没有生产者生产的速度快了。在Flow遇到这个情况的时候,生产者就会挂起直到消费者可以消费更多的数据。


runBlocking {
getFastFlow().collect { value ->
delay(1000) // simulate a slow collector
process(value)
}
}

在这个例子中,getFastFlow()会生成数据的速度比process(value)的速度快。因为collect是一个挂起函数,在process(value)数据处理不过来的时候getFastFlow()就会自动挂起。这样就防止了没有处理的数据的堆积。


使用缓存处理背压


有的时候,即使消费者处理速度已经慢于生产者产生数据的速度的时候,你还是想让生产者继续生产数据。这时就可以引入缓存了。Flow可以使用buffer操作符。如:


runBlocking {
getFastFlow().buffer().collect { value -> process(value) }
}

这个例子里使用了buffer操作符,这样在process(value)还在处理旧数据的时候getFastFlow()可以接着生产新的数据。


今日份先更新到这了。to be continued...


作者:小红星闪啊闪
来源:juejin.cn/post/7271153372793946168
收起阅读 »

打工:本身就毫无意义

昨天的时候有一个朋友联系我,说:“他被裁员了,一个工作了 11 年的老 JAVA” 其实在这段时间里面,我收到了很多类似的朋友来信:被裁员,焦虑到睡不着觉。 甚至有些公司,明确的表示不会给你 n+1 。 这些事,让我深切的感受到什么叫做:取之尽锱铢,用之...
继续阅读 »

昨天的时候有一个朋友联系我,说:“他被裁员了,一个工作了 11 年的老 JAVA”



其实在这段时间里面,我收到了很多类似的朋友来信:被裁员,焦虑到睡不着觉。



甚至有些公司,明确的表示不会给你 n+1 。



这些事,让我深切的感受到什么叫做:取之尽锱铢,用之如泥沙,弃之是垃圾。


那么对于我们这些普通的打工人而言,如何应对这样的一个时代,如何才能让自己更加具备竞争力,不要陷入毫无意义的打工内卷呢?


这个也就是咱们这次,主要说明的问题。


一共分为三点:



  1. 要工作,不要打工

  2. 如何得分,为自己工作

  3. 什么时候应该选择打工


要工作,不要打工


工作和打工是不一样的。


想要让自己真正的成长,首先我们需要区分出工作和打工的区别。


打工


回忆一下你的打工生涯,你有没有为今天摸鱼一天,但是工资照发而感到高兴。有没有为今天加班到深夜,但是一点加班费都没有,而感到愤怒。


如果你有这样的情绪,那么就是没有搞明白打工的本质的原因。


打工本质上就是一个机器,它有两个口 一个口吸收你的时间,一个口产出对应的金钱。


产出的金钱多少,与你的工作量,并无本质关系。它需要消耗的是你的时间。


现在很多公司都在要求 996、大小周。很多朋友都说:“ 996 有什么用啊,工作效率那么低,我们只需要在规定的时间之内,把事情做完不就可以了吗?不知道哪个王八蛋想出来的这个。公司也是傻X!”


你以为:公 司 不 知 道 9 9 6 效 率 低 吗? 。你可以怀疑某些公司的人品,但不要怀疑他们的智商(注意:我说的是某些公司,我相信大部分公司是好的~~)


那么既然公司知道 996 效率低,为什么还要让你 996 呢?


说白了就是尽量压榨你的时间。让你没有时间再去思考工作真正的意义。


工作


接下来,我们来说工作。


之前在群里看到了一个朋友说的话,我觉得非常准确,在这里分享给大家:



如果把一个公司比喻成一个篮球队。那么在这个球队里面,将会有两个角色,一个叫做 投篮手
,一个叫做 其他


公司的老板担任的就是投篮手的角色,公司的员工担任的就是其他的角色。


其他的角色的主要任务就是帮助投篮手得分。所以,你运球再好,传球再好,都没有关系。


但是,得分必须要投篮手完成,其他是不允许得分的,也不允许练习得分,甚至连得分的想法都不应该有。



打工压榨你的时间,不允许你做私活,不允许你做任何可以自己创造利润的事情。说白了就是让你尽可能的思考如何运球、如何传球,但是不允许思考如何得分。


而工作区别于打工的地方,就是:工作你要思考的问题就是:如何得分!


很多开发者都是 唯技术至上论,我们永远在思考如何利用技术来换取更高的薪酬,但是我们没有思考过如何利用技术来自己得分。


所以说,当我们想要把打工思维,转化为工作思维的时候。首先需要做的就是:如何可以利用自己技术,来创造出直接属于自己的利润。


这个利润一定不是你通过打工机器换来的,而是通过自己得分得来的。


当有一天,你发现:原来我也是可以投篮的。到这个时候,你就算真正的知道:如何工作了。


如何得分,为自己工作


那么到现在咱们知道了工作和打工是不一样,工作更关注如何投篮。


那么接下来,咱们就来说一说,投篮这个事怎么做呢?


所谓投篮,就是:通过自己现有的储备,产生直接价值,利用对应的价值换取收益


其中,最简单的方式就是:利用现有平台,产生延伸价值


利用现有平台,产生延伸价值


打工机器最好的一点就是:他可以让你在对市场一无所知的时候,快速的进入到这个市场中。从而让你可以知道这个市场运行的规律。


只要你有打过工,那么你一定可以接触到公司内部的一些业务。那么好好的利用这个机会,想一下:“这些业务究竟是解决了市场上哪些现有的问题?目前还有哪些不足?我是否做一些事情,弥补这些不足。”


以我为例:


我之前在黑马工作,从而接触到了线下培训的业务。发现了线下培训的三个问题:



  1. 线下培训,费用极高,通常需要在两万元以上。额外学生还需要支付住宿费、生活费等其他费用。六个月下来,每个学员支出需要在 4-5万元 之间。

  2. 老师的水平参差不齐,运气好撞见了一个好老师(比如像我这样的),可以学的比较好。如果点背,碰上了一个没有那么负责的老师,那么学习情况真的堪忧。

  3. 统一授课需要保证统一进度,所以每天要讲的内容都是固定的。你进度慢跟不上这个进度,那么不好意思。这些都是你的问题,你要想办法解决。课程不会等你。


所以,针对这三个问题,我就在想,那么是不是可以做线上的培训?这样有三点好处:



  1. 费用低:因为没有线下教室的成本,所以可以把费用压到很低。

  2. 1V1私教:我全程提供 1V1 的私教,手把手教学、解决问题。可以保证每个同学都可以得到一个好老师。

  3. 定制化教学:不统一授课,每个同学学习进度都可以不一样。根据每个同学的学习情况进行定制化的教学。


以此来解决线下培训中存在的三个问题。


这就属于:“发现现有业务的不足,从而通过一些改变,来弥补这些不足”。从而开始尝试自己投篮。


什么时候应该选择打工


那么说到这里,打工就毫无意义了吗?我们就应该直接开始学习投篮吗?


当然不是!


所以最后,咱们就来说一下,什么时候应该选择打工。


当我们刚刚走出校园,或者虽然已经工作,但是还没有发现现有业务的一些不足时。


那么打工是一个最好的选择。


千万不要贸然的放弃现有的收入!


但是,我们需要知道的是:打工的目的绝不仅仅只是为了通过时间换取金钱。而一定是,利用现有的平台,发现它所存在的问题,然后想办法解决这个问题。


也就是:打工的终极形态,就是可以真正的工作!


而:工作的终极形态,就是发现了一个社会中现有的问题,然后想办法解决它!


作者:程序员Sunday
来源:juejin.cn/post/7280104452399743028
收起阅读 »

各位微信小程序的开发者们,你们还好吗?

web
前言 最近微信小程序隐私指引这波骚操作实在是太好玩了,剧情跌宕起伏,让人不由得直呼周星驰在《唐伯虎点秋香》里的那句名言“人生大起大落得太快,实在是太刺激了”。 我自己因为有几个小程序需要做适配,所以全程体验了整个剧情,到今天感觉这过程实在是有点离谱,所以决定...
继续阅读 »

前言


最近微信小程序隐私指引这波骚操作实在是太好玩了,剧情跌宕起伏,让人不由得直呼周星驰在《唐伯虎点秋香》里的那句名言“人生大起大落得太快,实在是太刺激了”。


3583c74475ac4036932cf1421e1abafd.jpeg


我自己因为有几个小程序需要做适配,所以全程体验了整个剧情,到今天感觉这过程实在是有点离谱,所以决定记录一下。以飨读者。


起因


让我们把时钟回拨到一个月前的8月14日,微信默默的发了一个足以影响所有小程序的公告 《关于小程序隐私保护指引设置的公告》。大概意思就是对于某些隐私接口,如相册,地理位置啥的,要给用户弹个窗,需要用户点同意以后才能调用。公告内容很多,总结起来有两个重点:



  1. 这个弹窗要你们自己做哟。

  2. 9月15号生效呦,在此之前要是没做弹窗,你的小程序就死定了呦。


然后小伙伴们不敢怠慢,纷纷开始研究怎么个改法,社区论坛里乱成了一锅粥。


有看文档看不懂的:


Screen Shot 2023-09-15 at 12.46.41 AM.png


有没看到公告早上上班突然发现接口都出错一脸懵的:


6a5f760e492e69d02606d9339c33ea8a.jpeg


Screen Shot 2023-09-15 at 12.56.09 AM.png


还有先知先觉的:


Screen Shot 2023-09-15 at 1.05.15 AM.png


骂街的我就不贴了,以防不能过审。


发展


由于推出的过于仓促,阻碍了开发调试,引起开发者不满,官方很快又默默回滚了此次更新。并且鉴于大家都看不懂文档,也不知道怎么改代码。在小伙伴们的强烈呼吁下


Screen Shot 2023-09-15 at 1.17.53 AM.png
官方答应尽快推出demo。


终于在8天之后的8月22日,距离9月15日大限还有24天的时候,大家翘首以盼的官方demo终于出现了。而且不是1个,是3个(后来又多了1个,一共4个)。这下一脸懵变成三(四)脸懵了。


3d769c68d1e39eb2cfc670d8f47af595.jpeg


你让我用哪个???


demo都有了,那就开干吧,还有二十几天,来得及。于是天真的小伙伴们开始高ma高ma兴lie兴lie的写bug。这次改动不仅仅涉及代码层面,还需要后台配置,前台配置,更新缓存,uni/taro框架,基础库版本,第三方开发等等各种问题,大家也是磕磕绊绊的慢慢的都摸清楚了该怎么做。


由于此次改动涉及到所有的小程序,影响面太大,随着9月15号大限的临近,更多的问题渐渐浮出水面。


首先就是那些维护着几十上百个小程序的小朋友,改动看似不多,但每一个都要走开发测试发版流程,而且必须在这短短的不到一个月的时间内完成,听起来就头大有木有?改不完,根本改不完,加班加点不睡觉也改不完。


然后就是那些在线上跑了好几年没更新的小程序,有的是源代码都找不到了,有的原来的开发者早就提桶跑路了,直接欲哭无泪。


还有后知后觉的,剩下几天就到大限的时候来社区里问这个隐私协议是个啥?这么重要的事情平台为什么没有早点通知我???


最后,就是审核变的奇慢无比,原来几个小时就有结果,而现在有小程序审核了好几天了还是没有反应。这其实也是可以预见的,这种影响所有小程序的改动必然会导致版本发布量大增,微信这是人为的自己给自己制造了个审核DDOS,但后果都是开发者承担。就问你急不急吧


Screen Shot 2023-09-15 at 2.20.05 AM.png
就剩几天的时候,有小伙伴被逼的没办法了,不得不使用了一年一次平时打死都不敢用的加急审核。


当时间来到今天,也就是9.15大限前最后一天的时候,上了车的暗自庆幸,没上车的放弃治疗,尘埃即将落定,大家各安天命的时候,意想不到的转折又来了。。。


高潮


9月14号晚上20点38分,也就是距离9月15号0点还有3个多小时的时候,微信官方又发布了一条公告:


Screen Shot 2023-09-15 at 2.49.07 AM.png
总结下来就两点:



  1. 原来说大限是还剩3个小时的9月15号,新的大限推迟到10月17号。

  2. 代码不改也没关系,平台自己会弹窗。


啊???????????????


啥???????????????


就剩3个小时了你给我来个180度大转弯???????????


不瞒你们说,我晚上看到这个公告的开始的表情是:


96282a4cc8aa3522a78448bd660be56a.jpeg
然后是


210235eea23d7fcc4867ae9bbccb3df9.jpeg


这下原本上了车的直接被180度逮虾户给甩出去了,合着我吭哧吭哧费劲巴拉的折腾了一个月眼看就剩3个小时了你告诉我都是白干???你平台为什么不一开始就把方案定成你们自己处理,非要折磨我们开发者???我加班都是白加了?我的用掉的加急审核次数能不能退给我??我怎么觉得有一种被人耍了的感觉?


离最后期限3个小时啊,不是3周,也不是3天,是3个小时啊。你们官方人员业余都是玩赛车的还是兼职开渣土车的啊,平时上下班开车是不是都是漂移过弯啊。


未完待续


这事就这么完了吗? no no no,我觉得还没完,接下来这几天社区里估计又要炸锅了,大家喜欢看热闹的没事可以去围观一下。这不又延期了一个月吗?再出什么幺蛾子我是一点都不会感到意外了。


最后,我觉得这件事印证了我之前听过的一句话,“这个世界就是个巨大的草台班子”。


最后的最后,拿社区里最经典的一张动图镇楼


0-2.gif


作者:ad6623
来源:juejin.cn/post/7278517841884266536
收起阅读 »

记录一下27岁的前端,从二本到澳洲🦘的故事

前言 转眼在悉尼已经206天了,也算是跟大家走了一条不太一样的道路,想还是写下一点东西。为自己作记录,也可以让大家在摸鱼之际看看不太一样的故事。 长文预警,或许有点碎碎念,可以根据目录酌情跳转。 大学的故事 我的大学在成都的一个二本院校读的计算机专业,算不上...
继续阅读 »

前言


转眼在悉尼已经206天了,也算是跟大家走了一条不太一样的道路,想还是写下一点东西。为自己作记录,也可以让大家在摸鱼之际看看不太一样的故事。


FF826FEE-B67F-4661-9728-8139C4132183_1_201_a.jpeg


长文预警,或许有点碎碎念,可以根据目录酌情跳转。


大学的故事


我的大学在成都的一个二本院校读的计算机专业,算不上好,但也没那么坏。




  • 通宵跟室友开过黑,后来上了钻石。




  • 学了网球,当过网球社社长并且一直打到了现在。


    8CEBB138-EC6E-4659-90E9-24CB94747566_1_105_c.jpeg




  • 大二暑假去了美国看过外面的世界,回来结果留了一级🙈。


    899BEF1F-CC21-433A-9872-EF8D0AE2C9CE_1_105_c.jpeg




  • 在学校拿过英语演讲比赛第一名,后来代表学校去参赛见到了好多好优秀的同龄人。


    A5ABE88B-DFB0-498F-820F-8726A3606069_1_102_a.jpeg




  • 去星巴克打过工发生了很多有趣的故事。


    773B0525-473A-4B07-8D43-A652CDBE8E02_1_201_a.jpeg




但要说最大的收获,还是认识了我老婆,陪伴着我一直走到了许多至暗时刻,直至现在:


9548EBF0-5F0A-4803-9046-CB00CEFE30CC_1_105_c.jpeg


毕业以后


创业 Team


毕业之后第一家上班的公司是一个小的创业团队,当时算上老板一共才五个人。


但是依然非常感谢那个机会,我还记得入职的第一天,后端小哥让我看一下接口报错信息,我甚至不知道在 devtools 中如何找到 Networks 😄


后来,我在那里的几个月学会了 JavaScript,学到了简单的 Vue,做了一大堆没有成功的小程序。虽然回过头去看技术不那么酷,但自己凭借自己的三脚猫功夫也算入了门。


5295BBF8-FB29-438B-96B3-73E507A62A08_1_105_c.jpeg


本地房产媒体公司


后来因为要买房子,想获取更多的信息,机缘巧合入职了一家本地的房产媒体公司 🏠。主要做小程序和后台的开发,虽然工作强度不大,但在那里略微拓宽了自己的技术。有机会把学到的 React 上生产环境,后台依然用 Vue 也没有落下,还积累了不少的运维经验,有的用到了现在。


485E4E11-0870-4B8E-A03C-0694E860CCCB_1_105_c.jpeg


下班看到的行色匆匆的人们


医疗信息系统


在工作到第二年的时候,发现自己陷入了重复的怪圈当中。因为业务得限制,技术也不需要很大,所以一直没啥进步,于是决定去一下更能提升自己的公司。


在水群的过程中认识了很好的朋友,在他的推荐下进了一家阿里出来的团队。


8893E40B-94EE-48ED-B7EE-FFF1CE3291E1_1_201_a.jpeg


我在那里度过了飞速成长了一年半时光,虽然有被大家诟病的 996 等,但团队氛围非常好,我也承担了更多的责任,反而没觉得那么累。


在那里的时间,我精进了自己的 React 技术,维护开发过内部的组件库,开发过内部的医疗系统的富文本编辑器,给大家培训过算法。可以说是痛并快乐着的日子。


C7B6D439-E4B6-4D24-82B8-CB1137209638_1_105_c.jpeg


萌生出国的念头


其实到了现在,出国的心情已经跟最开始完全不太一样了。


在一切的最开始,是21年中国互联网开始裁员的那一段时间,阿里腾讯等大手笔地裁员甚至一度让未来科技城的房价下跌。彼时的我还在吭哧吭哧地魔改 antd 的代码,水群的时候偶然看到一条被裁员房贷还不下去还因为高龄找不到工作的消息,突然陷入了对自我的反思。开始觉得如果这种事情发生在我的身上,我会不会有什么不一样。


想了很久,答案是似乎没有什么不同。新闻中的老哥工作的我曾经梦想的“大厂”,拿着令人羡慕的工资,拉满了杠杆买房但却无情地被经济周期抛弃。虽然讨论了这么多年的年龄焦虑,曾经我也是“只要技术硬,不怕年纪大”中的一员,但当站在高龄失业的路口的时候,摆在我面前的似乎只有“降薪”和“牺牲家庭更拼命地走上管理岗”这两条路径。但我也见过自己曾经的领导被卡在高层和一线之间左右为难的境地,楼道间抽完一根烟却也只能苦笑着对团队说“我们这个周末加一加班”。


似乎没有办法改变身边的环境,那么唯一的答案就是只能改变自己。


幸运的是我曾经见识过外面的世界,也明白走出国门并不是那么遥不可及,只要一步一步往外迈,总是能够做到的。



写到这里的时候再次感慨自己何其幸运遇到了一个好的对象,支持我做出的如此巨大的决定,并愿意辞去自己白领的工作跟我走出来。



一个有意思的事情,也在跟一些朋友聊过出国这件事情后发现出其地一致。
当一旦你的心中埋下“出国”这颗种子之后,你会发现你身边所有的信息在一点一点的改变,以前自己从来没有接触过的观点和信息会开始进入你的世界,一次一次击碎你的三观,让你不断的反思自己和周围。


出国的准备


当我们两个确定了自己要一起做这件事情以后,也只是迈出了漫漫长征的第一步。


我们需要选定目的地,需要说服各自的父母,需要做好中短期的规划。


选定目的地


在选目的地之前,我们首先列出了自己的条件,排除掉不可能的路径。例如:我们只有很少的积蓄,决定了我们无法一开始通过读书的方式出国;再者,我的技术水平、经验还有学历还没有强到可以 offshore 直接面试上岸让雇主担保的程度。那么在排除掉了“读书”、“雇主担保”两条路径之后,我们开始了解各个国家。


我在这里列出几个当时我们自己的选项,过多的不再展开:



  • 日本 —— 老婆对它没有什么特殊的情结,不值得放弃一切去日本。

  • 美国 —— 最好,但难度太大,花费太高。

  • 欧洲 —— 备选

  • 加拿大 —— 备选

  • 澳洲 —— 备选

  • 新西兰 —— 备选


在划定完范围以后,就开始做签证的攻略了。


咨询加拿大


最开始我们是有做加拿大的攻略的,了解到加拿大政策稳定,对华人也较友好,除了天寒地冻以外,也挑不上什么毛病。于是我们认真的了解了政策,每天中午午休的时候就省去了睡觉的时间了解每一个省的政策,中间还认真花费咨询了持牌律师,然后选定了曼省配偶工签再走省提名的道路。


方法方式敲定了以后,我们又陷入了纠结的境地。当时算下来老婆读书要20W的学费,加上租房生活,还有签证律师费等一系列的费用,要准备4-50W。算出来数字的那一瞬间,我们知道还是跟我们无缘了。


了解 WHV


老婆突然有一天想起 WHV 这个签证类别,跟我说2022年的名额开放了。虽然我们早就知道这个签证类型,但因为它本质上还是给背包客们一个旅行干劳力活儿的临时签证,当时没有想到跟我们任何关系。但我后来认真的研读了它的签证条款后,发现它并不局限于签证持有人的劳动行业,只是不能为同一个雇主工作超过6个月,于是一条似乎可行的路径在我心中萌发出来。



裸辞,拿着WHV入境,落地后在境内找工作,工作一段时间卷出一定的成果后让雇主给我做担保。



并且这个签证的成本非常之低,只需要出450AUD的签证费,考出雅思4.5分或者PTE Academic 30分即可。


2022年澳洲政府将这个签证由之前的先到先得改成提交EOI等待邀请的形式,甚至提交EOI表格的时候不需要任何材料辅助,完全可以受邀再去考英语。


那就没有什么好商量的,我们挑了一个风和日丽的上午,将两个人的EOI都递进了澳洲官网,从此命运的齿轮发生了转动。


与此同时我们同时还花钱准备下来了新西兰的 WHV 签证,但这一部分按下不表。


前后受邀


递交完EOI的日子里,我们的生活在有条不紊地进行。我换到了一家五百强的外企继续做前端,在成都完成了我们的婚礼,也挑了好的时候跟双方父母表明了这个事情。更加幸运的是,我们双方的父母都非常开明支持我们的决定。我们看到过WHV交流群中有的小伙伴父母听到“出国”两个字,甚至以死、断绝关系来要挟孩子不让离开。我们倍感幸运,同时也感慨世界之大。


先受到澳洲政府邀请的是我老婆,但我们没有特别大的波动,如果我最后没拿到的话,依旧是没有意义的事情。


好在,我大概在一个月之后就在邮箱中收到了邀请递签的信。


image.png


于是,身份有了,接下来就是准备出发的事情。


正式出发


我们在双双受到邀请后的一个月递交了自己的签证申请,在等待下签的同时我们也没闲着。


老婆去跟她实习到毕业到最后一天的公司说了再见,整整三年,即是解脱也充满了忧愁。为了让自己有一技之长,她选择辞职以后去报班学了美甲💅,也为我们的生活增添一分可能性。


我也去跟 Team Leader 表明了想法,离开了在很多人眼里在成都非常好的工作。


7F01C619-D970-4F80-BF07-90681031CDE8_1_105_c.jpeg


image.png


我们决定了在过完年后出发,于是开始处理自己在成都几年积累下来东西,把自己结婚买的新车卖掉了;和住了很久的房子说再见,和很多好朋友告别,在2023年元旦过后回到了老家,然后定了2月21号出发的机票。


0A77260A-42FE-492F-95BF-8DB6005FCE2D_1_105_c.jpeg


image.png


在家里无忧无虑的日子还是过的飞快,转眼就到了要出发的日子。那个时候出境还需要48小时核酸,我们因为害怕感染,愣是三天都没有出过家门。


4DE70FCA-3201-498A-BA8E-1F294193216C_1_105_c.jpeg


从重庆 -> 厦门 -> 悉尼


B4B0A5BA-55B0-4A97-A5A3-D3CD0FD661D4_1_105_c.jpeg


2A130C69-71EF-4314-95E3-7C457E9B4476_1_105_c.jpeg


找工作的那些日子


落地之后我们选择住了三天酒店,因为租的房子的起租期在三天之后,然后又因为悉尼的正好在举办 Sydney WorldPride 🌈 节,世界各地的人都涌过来参加活动,酒店超级贵。我们租了一个角落里的房间三天,那是我们俩此生住过最小的房间,2000RMB。


595431C2-A897-49B3-A13A-C819973C1B40_1_105_c.jpeg


A00FEDB6-5855-4201-B0A2-4114EE82DC49_1_105_c.jpeg


落地后发现自己的英文完全属于没法用的阶段,便利店买东西也听不懂,办银行卡也听不懂,但磕磕绊绊两个人也还是一起完成了下来。不敢懈怠,第二天就开始把自己的简历电话号码更新成了当地号码,LinkinIn 状态改成了 open。期待着能够开始有一些面试或者电话,因为每天花澳币几乎是五倍的生活开销,心理压力实在是太大。


3C01548D-63DB-4F6C-BB91-69EBD3347588_1_105_c.jpeg


还记得躺在酒店的第二天晚上,我刷了一晚上小红书,因为发现了送外卖能赚到一些钱而非常激动。第二天早上很兴奋地跟老婆说,“我要去送外卖啦!”


第三天将我们巨多的行李箱搬到了租的一室房子内,房租 1200AUD 每周:


7521FA19-5105-443E-BED8-CFF7E04F1A61_1_105_c.jpeg


折腾了一天之后,看着窗外的夕阳🌇,发了一条朋友圈:


image.png


我戏谑地跟老婆说,我们毕业的第一年住的一室的房子,后来逐渐换得像一个家。现在又回到了一室的房子中,这次不知道再要住多久了。


7E1E2567-7B36-49E5-BEB4-DBA04420D3B6_1_105_c.jpeg


因为澳洲租的房子大多完全不带家具,我们头一个星期完全处于淘家具的过程中,家中不断地受到网上买的便宜家具包裹,然后安装:


4F40CE21-6E89-4356-8B4C-3B764C54F5B3_1_105_c.jpeg


在睡了一个星期地板的,人生第一次自己安装了床架:


277B3E14-1661-4885-A390-18E7FC704A02_1_105_c.jpeg


第一次安装了柜子:


9B2E8FCF-34F0-4EDB-8E5B-995B4A871E94_1_105_c.jpeg


家徒四壁依旧坚持面试:


F75880C1-D99E-456B-A182-88BF70BEF690_1_105_c.jpeg


中途去看了周董疫情后的第一场演唱会:


A1FADDD8-210F-4633-BB85-64E8B7E2F3CA_1_105_c.jpeg


给老婆圆了她一直的梦,用我们卖车的钱买了 Mini Cooper:


F4C5C70D-3C0C-4E2C-A071-3EECF7B3F242_1_105_c.jpeg


买了车之后,就一直在送外卖:


0923ED08-ADAB-49A3-A3F2-A4CA00663E97_1_105_c.jpeg


还记得送外卖的第一天,我们中午从11点跑到了2点,回来休息了2个小时,从4点跑到了8点,赚了100多刀。我们兴奋地去超市里买了很多的水果:


DAB0A1CE-C43F-499D-A99F-714832D3CFEF_1_105_c.jpeg


因为这边东西太贵,并且家里没有冰箱。我们吃了整整一周的速食食品,直到送外卖开始赚生活费:


D2BAFBB5-7BC2-4E4A-AD61-9517C937C097_1_105_c.jpeg


AF07C3C3-B406-4425-8A07-EB1663CEFC5F_1_105_c.jpeg


中途送外卖一度开心到忘记了自己曾经是一个程序员,笑。


9624BF24-2C40-4961-9D7F-3D7B60138020_1_105_c.jpeg


后来很多简历投递到公司邮箱,除了拒信就是没有回复,我变得十分焦虑。跟家里人打电话的时候也在说,只能再给自己两个月的时间了,家里人支持一个月,自己的生活费能再撑一个月,5月还没有找到工作就要回家了 🙈


于是我开始反思自己的方式方法。痛定思痛之后决定主动出击,厚着脸皮开始在 LinkedIn 上私信猎头和工程师,寻找内推的机会。


换了新的方法之后,果然成功率要高了不少,尽管有一半以上的人不会回复我的消息,但剩下的一半也至少让我开始收获一些电话询问我的情况。


image.png


然后陆陆续续地开始接到一些面试,但刚开始大多都还是 recruiter 和猎头了解情况的面试。我深知自己没有任何本地的工作经验,口语也算不得好,因为珍惜每一次能跟别人打电话的机会,尽量表现出热情,结束后也会记录自己的表现:


image.png


在足够多的厚着脸皮尬聊之后,终于迎来了生活的曙光。猎头给我打电话有澳洲最大的保险公司之一在招聘一批 contractor,并迅速地帮我安排了第二天的面试。也许这次运气比较好吧,顺利的通过了面试,拿到了6个月的合同,生活可以稳定一段时间了。


还记得在受到 offer 的那天晚上,我回家的路上,想起了在再次收到拒信的那个晚上看《当幸福来敲门》,一瞬间理解了 Chris 收到了正式 offer从办公室走出来的心情:


image.png


工作的时光


入职的一点小震撼


入职的第一天是我和一个父母中东但在这边出生的小哥一起办理入职,我的 Manager 带我们领了电脑,中午一起吃了饭。但是英语闲聊也太太太难了,我现在都记得第一天完全不知道他们在说什么...


但没过多久澳洲职场就给我这个中国小伙子来了一个大大的震撼:当天下午四点,我还在工位上吭哧吭哧 yarn install。中东小伙过来拍了拍我的肩膀,“你要去火车站吗?要不要一起?”,我看了看表,又看了看他真诚的目光,点了点头。于是我们去跟 Manager 说我们走了, Manager 面不改色,问我们的 VPN 的调通了吗,确保可以在非办公室网络下连上了吗?我说弄好了。她点点头说,那就好,这样你明天就可以在家工作了。


我当时听到并没有当一回事,只当是新人入职的客套。于是平生在4点30分搭上了回家的列车。


image.png


第二天,我高兴地背上电脑和小书包来到 CBD 的办公室,结果空无一人。早会的时候,我说我在办公室,结果大家一脸震惊,说你昨天不是去了吗?为什么今天又去?于是我在第三天顺理成章地居家办公了,直到今天,我还大概保持着一个月去一天的通勤频率。


image.png


技术栈


其实技术方面反而没什么好聊的,跟国内比起来,这边对技术的要求反而没这么高,对候选人的资质也更加宽容。


我们用的是 React + NestJS + AWS 的一揽子全栈方案,没有什么学习成本也就上手了。


在🦘工作和国内的区别


Make sure you are happy and health


在写下这一篇小记的时候,正是澳大利亚的 R U OK Day,每一个 manager 都会买上一些小吃或者礼物,关心自己的员工是否高兴和健康。


例如我们 Manager 最常用来总结早会的一句话就是:“如果你有感觉到任何不舒服或者对项目不那么满意的地方,请不要犹豫地找我聊聊,我的存在就是为了让团队更好的运转。”


9F0DBC26-3B0B-4869-8A02-8316A7CA2C3F.jpeg


大家在早会开始之前,也会毫不忌讳地在群里发出“I'm feeling unwell today. Will taking a day off”,然后潇洒地消失一整天。


3AA7D1D8-68CD-4D32-BCE2-1A709A20B5AB.jpeg


工作与生活的界限


在这边,天大的事情,也几乎不会在你下班之后给你发任何消息。我在下午5点之后收到过几次其他同事或者 Manger 的消息,开头的第一句话必定是道歉。至于晚上和周末,则一次都没有。


235A7542-085B-4817-855D-1A1AD595E70C_1_201_a.jpeg


对职业的容忍度



  1. 我们团队有两个工程师甚至没有上过大学,高中毕业自学编程。他们很大方地在闲聊中承认,团队中的其他人也没有觉得有任何问题。

  2. 有39岁转码带娃的妈妈,她很努力,我们都很喜欢她。

  3. 我们团队的 Senior Engineer 以前是仓库管理员。


后记


澳洲的工作很理想,但这边的生活却和国内大相径庭。在略微稳定下来以后,我们俩又经常思念国内有家人朋友在身边的日子,思念出门就可以吃到美食的日子,思念我们熟悉的文化和乡土。


窗外的明月高挂,不知道下一次回家又是什么时候了。


以此为记。


作者:yuetong3yu
来源:juejin.cn/post/7278929122302132279
收起阅读 »

又一个全新编程语言,诞生了!

最近,编程领域又一个黑马忽然冲进了开发者们的视野并正式开放下载。 它的名字叫Mojo,相信有不少小伙伴最近也看到了。 Mojo是为AI开发者所准备的编程语言,语法有点像Python。 根据Mojo官网的描述,它结合了Python的易用性和C语言的高性能,解...
继续阅读 »

最近,编程领域又一个黑马忽然冲进了开发者们的视野并正式开放下载。


它的名字叫Mojo,相信有不少小伙伴最近也看到了。



Mojo是为AI开发者所准备的编程语言,语法有点像Python。



根据Mojo官网的描述,它结合了Python的易用性和C语言的高性能,解锁了AI硬件的可编程性和AI模型的可扩展性。


Mojo看起来好像挺能打,它到底是哪个公司所推出来的呢?


看了一下才发现Mojo是由人工智能公司Modular所推出的全新编程语言。


而Modular这个公司则是一个非常年轻的新生AI创业公司,于2022年由Chris Lattner和Tim Davis所创立。



提到这两个创始人,相信有些同学也有所了解,都是业内顶级专家。其中Chris Lattner还被称为“LLVM之父”和“Swift之父”,在苹果、谷歌、特斯拉等多家知名科技巨头里曾带领构建了AI和核心系统。


Modular公司的愿景非常宏伟,目标是自下而上重塑AI基础设施。


去年的时候,Modular AI曾获得过3000万美金的融资。而就在前些天,Modular又再次宣布成功融资 1 亿美金,这对于一个刚诞生不久的初创型公司而言可谓是成绩斐然。



另外在公司官网的投资者名单里能看到,不少AI领域的知名投资机构都有参与。



Mojo这个编程语言有几个比较明显的特点。


1、首先是性能方面。


Mojo充分利用硬件的特性和功能,包括多核、矢量单元和加速器单元,以及先进的编译器和异构运行时机制,在不增加复杂性的前提下实现了与C++和CUDA相当的性能。


在并行化这一块,Mojo利用MLIR,使Mojo开发者能够充分利用向量、线程和AI硬件单元。



2、其次是互操作性方面。


大家都知道,发展到今天,Python的生态极其繁荣,各种函数、库、框架、模型、工具等等数不胜数。


而Mojo则可以访问整个Python生态。比如使用Mojo,可以在代码中无缝地接入和混合像Numpy和Matplotlib等库。



3、再者就是可扩展性方面。


可扩展性这块也是Mojo的优势。Mojo可以升级用户模型中的已有操作,以便开发者可以使用预处理、后处理、自定义替换等操作来轻松地扩展用户的模型。


Mojo最初发布于今年的5月初,上线数月以来就已形成基本规模和生态。



前不久,Modular官网宣布Mojo正式开放下载,首先是从Linux系统开始,并在后续的迭代版本中将陆续添加对Mac和Windows的支持。


这也意味着开发者可以通过Mojo SDK进行尝试并编写自己的Mojo代码。



而就在Mojo官宣可以下载后不久,一位名叫Aydyn Tairov开源作者就利用Mojo来做了一个突破性的尝试。


这个作者之前曾将GitHub上火热的由纯C实现的llama2.c项目移植到了基于Python的llama2.py。


而这次Aydyn Tairov又将llama2.py移植到了llama2.mojo,结果非常出乎意料,移植后性能提升了近250倍。



即便如此,作者仍然认为里面还有一些改进的空间。


看到Mojo如此的表现,有不少网友说Python这次可谓是遭遇了一个强大的对手,Mojo甚至有可能在未来会取代Python?


对此,公司CEO Chris Lattner直接回应称:


Mojo并不会对Python造成威胁,相反,还会帮助Python开发者变得更强大。要担心的也不是Python,而是C++们。



文章的最后也附上相关的页面,感兴趣的小伙伴可以尝试一下。



至于这门编程语言在接下来的AI时代会发展如何,我们可以拭目以待。


作者:CodeSheep
来源:juejin.cn/post/7280057907055902760
收起阅读 »

环信web、uniapp、微信小程序sdk报错详解---登录篇

项目场景:记录对接环信sdk时遇到的一系列问题,总结一下避免大家再次踩坑。这里主要针对于web、uniapp、微信小程序在对接环信sdk时遇到的问题。主要针对报错400、404、401、40 (一)登录用户报400原因分析:从console控制台输出...
继续阅读 »

项目场景:
记录对接环信sdk时遇到的一系列问题,总结一下避免大家再次踩坑。这里主要针对于web、uniapp、微信小程序在对接环信sdk时遇到的问题。主要针对报错400、404、401、40

 (一)
登录用户报400



原因分析:
从console控制台输出及network请求返回入手分析
可以看到报错描述invalid password,密码无效,这个时候就需要去排查一下该用户密码填写是否正确


排查思路:
因为环信不保存用户的密码,可以在console后台或者调用修改密码的restapi来修改一下密码再重新登录(修改密码目前只有这两种方式)



(二)
登录用户报404




原因分析:
从console控制台输出及network请求返回入手分析
可以看到报错描述user not found,这个时候就需要去排查一下该用户是否存在于该项目使用的appkey下了

排查思路:
可以看一下console后台拥有这个用户的appkey和自己项目初始化时用的是否是同一个,若在console后台并没有查到该用户,就要注意这个用户是否真的没有注册




(三)
登录用户报40、401




原因分析:
报错40或者401一般都是token的问题,需要排查一下token是否还在有效期,token是否是当前用户的用户token
40的报错还有一种情况,用户名密码登录需要排查用户名及密码传参是否都是string类型


注:此处需要注意用户token和apptoken两种概念
用户token指的是该用户的token,一般只用于该用户在客户端使用环信 token 登录和鉴权
app token指的是管理员权限 token,发送 HTTP 请求时需要携带 app token
token较为私密,一般不要暴露出去

排查思路:
排查用户名及密码传参是否都是string类型,这个可以直接将option传参打印出来取一下数据类型看看是否是string
关于token排查,现在没有合适的办法直接查询token是否还在有效期或者是不是当前用户的token,只能通过api调用看是否报错401,可以在console后台直接获取新的用户token来测试一下


是不是当前用户的token也可以找环信的技术支持帮忙查,但在不在有效期他们也查不了


话外
有人遇到为什么已经open成功了但是还会报错

这里要注意open只能证明获取到了token,证明不了已经建立了websocket连接,只有触发onOpened或者onConnected回调 只有onOpened或者onConnected回调触发,才算真正与环信建立连接。所以也不能在open返回的success或者.then中做任何逻辑处理,此外还要注意监听回调一定要放在调用api之前,在调用任何一个api时都要保证监听挂载完毕,包括open


如何判断自己是否在登录状态

可以用以下三种方法中的一种判断当前用户是否在登录状态~
1、WebIM.conn方法下有一个logOut字段,该字段为true时表明未登录状态,该字段为false时表明登录;
2、WebIM.conn.isOpened () 方法有三个状态,undefined为未登录状态,true为已登录状态,false为未登录状态,可以根据这三个状态去判断是否登录;
3、通过onOpened 这个回调来判断,只要执行了就说明登录成功了,输出的话,输出的是undefined
三者选其一判断登录状态

收起阅读 »

需要具备哪些技能才算中高级前端?

之前有人问过我,“到底什么样才算中高级前端,需要具备哪些技能才算中高级?”他的本意是让我推荐一下前端的学习路线,然后再问了我这个问题,估计是想看看有哪些技术是晋升中高级前端的关键,提前学习吧。 这里不管是前端、终端还是后台,我觉得是可以统一来讨论的。 有什么标...
继续阅读 »

之前有人问过我,“到底什么样才算中高级前端,需要具备哪些技能才算中高级?”他的本意是让我推荐一下前端的学习路线,然后再问了我这个问题,估计是想看看有哪些技术是晋升中高级前端的关键,提前学习吧。


这里不管是前端、终端还是后台,我觉得是可以统一来讨论的。


有什么标志性的技能或者技术是可以作为中级工程师和高级工程师的分水岭的吗?只要学会了这些技术和技能,就一定可以晋升中高级工程师?我想是没有的。


我分享一下我对初中高级工程师的理解,仅供参考。


初级工程师就是应届毕业生,标志是能够熟练支撑中小型业务需求开发。他可能会支撑所有业务模块的开发,或者非核心业务模块的开发,同时也会支撑基础技术项目的开发。所以,如果使用是否参与基础技术项目来作为判断的话,是不对的。


中级工程师的标志是能够独立负责一个核心模块。成为一个模块负责人,这个模块的所有事情,领导都可以放心交给你的时候,你就是中级工程师了。这个负责模块,不是指能够支撑涉及这个模块相关的需求。而是指,你要:


  • 了解它的全部代码、它的设计原理
  • 了解它在整个系统中的位置、它跟其他模块的关联关系
  • 了解它的各种特性、现状、问题、未来的优化、发展方向
  • 维护好它的文档
  • 可以很好地给其他人、你的领导描述清楚,这个模块的所有内容
  • 负责它的一切

高级工程师的标志是能够负责一个系统成为系统负责人,带领项目成员一起,承担这个系统的所有事情。对比中级工程师,负责的内容更大更加复杂了,但本质没变,就是要综合能力。同时,中级工程师还只是单人作战,如果想要成为高级工程师,一定需要了解团队的力量,并学习如何通过合理的项目管理手段,做好一个复杂系统。


这里中级和高级都提到了“负责”这个词,那具体怎样才算负责,是领导指派给你,让你负责一个核心模块,就算负责了吗?不是的。这里的“负责”是指能够完全胜任,做出让领导满意的成果,让领导非常放心


当然,每家公司对不同职级的能力要求是不一样的,你也可以完全按照上面的能力描述来进行有针对性的学习和成长。


以上就是我对于中高级前端开发的理解,希望能够给你带来一些启发。



【讨论问题】


你是如何理解中高级工程师的呢?


欢迎在评论区分享你的想法,一起讨论。



----------------【END】----------------



【往期文章】


给你介绍一个工具,帮你找到未来的努力方向


《程序员职场工具库》高效工作的神器 —— checklist


2023 年上半年最值得看的一篇文章



欢迎加我v【longyiyiyu】,进行无负担沟通,我会


  • 长期职业发展规划指导
  • 近期工作重点交流
  • 职场解惑
  • 面试辅导

也欢迎关注公众号【潜龙在渊灬】,收获程序员职场相关经验、提升工作效率和职场效能、结交更多人脉。


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

我又听到有人说:主要原因是人不行

在工作中,我们经常把很多问题的原因都归结为三个字:人不行。 曾经有UI同事指着我的鼻子说,你们没有把设计稿百分百还原,是因为你们人不行。 昨天,我又听到一个研发经理朋友说,唉呀,项目干不好,主要是人不行。 哦,我听到这里,有种似曾相识的感觉,于是我详细问了一...
继续阅读 »

在工作中,我们经常把很多问题的原因都归结为三个字:人不行。



曾经有UI同事指着我的鼻子说,你们没有把设计稿百分百还原,是因为你们人不行


昨天,我又听到一个研发经理朋友说,唉呀,项目干不好,主要是人不行


哦,我听到这里,有种似曾相识的感觉,于是我详细问了一下,你的人哪个地方不行。



朋友说,项目上线那天晚上,他们居然不主动留下来值班,下班就走了,自觉意识太差。代码写的很乱,不自测就发到生产环境,一点行业规范都没有。他们……还……反正就是,能不干就不干,能偷懒就偷懒,人不行!



这个朋友,代码写的很好,人品也很好,刚刚当上管理岗,我也没有劝他,因为我知道,劝他没用,反而会激怒他。


当一个人,代码写得好,人品好,他就会以为别人也和他一样。他的管理方式就会是:大家一定要像我这样自觉,不自觉我就生闷气了!


反而,当一个人代码写得差,自觉性不那么强,如果凑巧还有点自知之明,那么因为他很清楚自己是如何糊弄的,因此他才会考虑如何通过管理的方法去促成目标。


我的这些认知,满是血泪史。因为我就经历过了“好人”变“差人”的过程。


因为代码写得好,几乎在每一个公司,干上一段时间,领导都会让我做管理,这在IT行业,叫:码而优则仕


做管理以后,我就发现,并不是所有人都像我一样,也并不是各个部门都各司其职,所谓课程上学的项目流程,只存在于理想状态下。当然,其中原因非常复杂,并不一定就是人不行,也可能是流程制度有问题。比如我上面的朋友,他就没有安排上线必须留人,留什么人,留到几点,什么时候开始,什么标准算是上线完成,完成之后有什么小奖励,这些他都没有强调和干预。


但是,我们无法活在理想中。不能说产品经理的原型逻辑性差,UI的设计稿歪七扭八,我们就建议老板把公司解散吧,你这个公司不适合做软件产品,那样我们就失业了。


你只能是就目前遇到的问题,结合目前手头的仅有的仨瓜俩枣,想办法去解决。可能有些方案不符合常规的思路,但都是解决实际问题特意设置的。


比如我在项目实践中,经常遇到的一点:



产品经理没有把原型梳理明白,就拿出来给开发人员看,导致浪费大家的时间,同时也打击大家的积极性:这样就开始了,这项目能好的了吗?我们也做不完就交给测试!



这种情况,一般我都会提前和产品经理沟通,我先预审,我这关过了,再交给开发看,起码保证不会离大谱。这里面有一个点,产品没有干好自己的活,人不行?他也只有3天时间设计原型。


还有一个问题也经常出现:



即便是产品原型还算可以,评审也过了。让开发人员看原型,他们没有看的。一直到开发了,自己的模块发现了问题,然后开始吐槽产品经理设计的太烂,流程走不通。



这是开发人不行?他们不仔细看,光想着糊弄。其实是他们没有看的重点,你让我看啥,我就是一个小前端,让我看整个平台吗?让我看整个技术架构?Java该用什么技术栈?看前端,你告诉我前端我做哪一模块的功能?此时,我一般都是先分配任务,然后再进行原型评审。如果先把任务分下去,他知道要做这一块,因为涉及自己的利益,会考虑自己好不好实现,就会认真审视原型,多发现问题。这样会避免做的过程中,再返过头来,说产品经理没设计好。已经进入开发了,再回头说产品问题,其实是开发人员不负责,更确切说是开发领导的责任。


一旦听到“人不行”的时候,我就会想到一位老领导。


他在我心中的是神一般的存在,在我看来,他有着化腐朽为神奇的力量。


有一次,我们给市场人员做了一个开通业务的APP:上面是表单输入,下面是俩按钮,左边是立即开通,右边是暂时保存。后来,市场同事经常找我们:能不能把我已开通的业务,改为暂时保存,我点错了。这点小事还闹到公司大会上讨论,众人把原因归为市场推广的同事人不行:没有上过学?不认识字?开不开通自己分不清吗?


此事持续了很久,闹得不愉快。甚至市场部和研发部出现了对立的局面,市场部说研发部不支持销售,研发部说市场部销售不利乱甩锅。


我老领导知道后,他就去了解,不可能啊,成年人了,按钮老按错,肯定有问题。原来,客户即便是有合作意向,也很少有立即开通的,他们都会调查一下这个公司的背景,然后再联系市场人员开通。两个按钮虽然是左右平分,但是距离很近。于是,他把软件改了,立即开通按钮挪到上边,填完信息后,顺势点击暂时保存,想开通得滑到上面才能点击。此后,出错的人就少了。




后来,行政部又有人抱怨员工人不行。发给员工的表格填的乱七八糟,根本不认真。有一项叫:请确认是否没有错误_____。明明没有错误,但是很多人都填了“否”。尽管反复强调,一天说三遍,依然有人填错,没有基本的职场素质。


老领导,他又去了解。他把表格改了,“是否没有错误”改为“全对”,空格改为打钩。后来,填错的现象明显少了。




很多事情,我们都想以说教来控制形势。比如反复强调,多次要求,我嗓子都喊哑了。因为不管是区分按钮,还是填写表格,你不是个傻子,你的能力是可以做到的,不应该出错,出了错你就是人不行。而老领导总是以人性来控制,知道你是懒散的,肯定不愿意认真付出,因此设置一个流水线,让你随着预设的轨迹被迫走一圈。下线后,居然发现自己合格了,甚至自己都变成人才了。用他的话说就是:流程弥补能力不足。



当归因为人不行时,其实分两种情况:别人不行自己不行


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

离职交接,心态要好

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

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




01



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


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


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


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


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


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


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


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



02



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


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


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


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


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


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


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



03



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


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


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


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


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


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


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


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



04



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


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


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


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


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


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


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



05



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


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


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


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


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


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



06



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


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


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


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


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


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


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


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


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



07



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


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


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


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


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


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


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


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


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


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



08



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


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


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


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


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


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


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


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


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


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



09



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


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


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


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



END


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

Swift - 闭包

iOS
定义 闭包是一个自包含的函数代码块,可以在代码中被传递和引用。闭包可以捕获和存储其所在上下文中任意常量和变量的引用**。 闭包的语法有三种形式:全局函数、嵌套函数和闭包表达式。 全局函数是一个有名字但不会捕获任何值的闭包潜逃函数是一个有名字并可以捕获其封闭函数...
继续阅读 »

定义


  • 闭包是一个自包含的函数代码块,可以在代码中被传递和引用
  • 闭包可以捕获和存储其所在上下文中任意常量和变量的引用**。

闭包的语法有三种形式:全局函数、嵌套函数和闭包表达式。


  • 全局函数是一个有名字但不会捕获任何值的闭包
  • 潜逃函数是一个有名字并可以捕获其封闭函数域内值的闭包
  • 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包

闭包表达式


闭包表达式的一般形式

{ (parameters) -> return type in

statements

}

以数组的sorted(by:)方法为例

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})


写成一行

names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2})

根据上下文推断类型


  • sorted(by:)方法被一个字符串数组调用,Swift 可以推断其参数和返回值的类型,因此其参数必须是 (String, String) -> Bool
  • 这意味着(String, String) 和 Bool 类型并不需要作为闭包表达式定义的一部分。因为所有的类型都可以被正确推断,返回箭头(->)和围绕在参数周围的括号也可以被省略:
names.sorted(by: { s1, s2 in return s1 > s2})

单表达式闭包的隐式返回


  • 单行表达式闭包可以通过省略 return 关键字来隐式返回单行表达式的结果
names.sorted(by: { s1, s2 in s1 > s2})

参数名称缩写


  • Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过 $0$1$2 来顺序调用闭包的参数,以此类推。
  • 闭包接受的参数的数量取决于所使用的缩写参数的最大编号。
  • in 关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:
names.sorted(by: {s1 > s2})

运算符方法


  • Swift 的 String 类型定义了关于大于号(>)的字符串实现,其作为一个函数接受两个 String 类型的参数并返回 Bool 类型的值。而这正好与 sorted(by:) 方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断找到系统自带的那个字符串函数的实现:
names.sorted(by: >)

尾随闭包


尾随闭包是一种特殊的闭包语法,它可以在函数调用的括号外部以简洁的方式提供闭包作为函数的最后一个参数。
使用尾随闭包的优势在于增加了代码的可读性和简洁性。当闭包作为函数的最后一个参数时,将闭包放在括号外部,可以使函数调用更加清晰,更接近于自然语言的阅读顺序。

func calculate(a: Int, b: Int, closure: (Int, Int) -> Int) {
let result = closure(a, b)
print(result)
}

// 调用函数时使用尾随闭包
calculate(a: 5, b: 3) { (x, y) -> Int in
return x + y
}

// 如果闭包只包含一个表达式,可以省略 return 关键字
calculate(a: 5, b: 3) { (x, y) in
x + y
}

// 省略参数的类型和括号
calculate(a: 5, b: 3) { x, y in
x + y
}

// 使用 $0, $1 等缩写形式代替参数名
calculate(a: 5, b: 3) {
$0 + $1
}


如果一个函数接受多个闭包,需要省略第一个尾随闭包的参数标签,并为其余尾随闭包添加标签。



值捕获


闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值。



可以捕获值的闭包最简单的形式是嵌套函数,也就是定义在其他函数的函数体内的函数。嵌套函数可以捕获其外部函数所有的参数以及定义的常量和变量。



注意:



如果将闭包赋值给一个类实例的属性,并且该闭包通过访问该实例或其成员捕获了该实例,将会造成一个循环引用。



捕获列表


默认情况下,闭包会捕获附近作用域中的常量和变量,并使用强引用指向它们。你可以通过一个捕获列表来显示指定它的捕获行为。


捕获列表在参数列表之前,由中括号括起来,里面是由逗号分隔的一系列表达式。一旦使用了捕获列表,就必须使用in关键字,即使省略了参数名、参数类型和返回类型。


捕获列表中的项会在闭包创建时被初始化。每一项都会用闭包附近作用域中的同名常量或者变量的值初始化。例如下面的代码实例中,捕获列表包含a而不包含b,这将导致这两个变量有不同的行为。

var a = 0
var b = 0
let closure = { [a] in
print(a, b)
}

a = 10
b = 10
closure()
// 打印“0 10”

如果捕获列表中的值是类类型,可以使用weakunowned来修饰它,闭包会分别用弱引用、无主引用来捕获该值:

myFunction { print(self.title) }                    // 隐式强引用捕获
myFunction { [self] in print(self.title) } // 显式强引用捕获
myFunction { [weak self] in print(self!.title) } // 弱引用捕获
myFunction { [unowned self] in print(self.title) } // 无主引用捕获

在捕获列表中,也可以将任意表达式的值绑定到一个常量上。该表达式会在闭包被创建时进行求值,闭包会按照制定的引用类型来捕获表达式的值:

// 以弱引用捕获 self.parent 并赋值给 parent
myFunction { [weak parent = self.parent] in print(parent!.title) }

解决闭包的循环强引用


在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,声明每个捕获的引用为弱引用或无助引用,而不是强引用。应当根据代码关系来决定使用弱引用还是无主引用。


使用规则

  • 在闭包和捕获的实例总是互相引用并且同时销毁时,将闭包内的捕获定义为无主引用

  • 相反,在被捕获的引用可能会变为nil,将闭包内的捕获定义为弱引用,弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为nil。这使我们可以在闭包体内检查它们是否存在


注意



如果被捕获的实例绝对不会变为nil,应该使用无主引用,而不是弱引用。



闭包是引用类型


无论你将函数和闭包赋值给一个常量还是变量,你实际上都是将常量或变量的值设置为对应函数或闭包的引用


逃逸闭包


当一个闭包作为参数传到一个函数中,但是这个闭包在函数之后才被执行,称该闭包从函数中逃逸


在参数名之前标注@escaping指明这个闭包是允许逃逸出这个函数。


一种能使闭包"逃逸"出函数的方法是,将这个闭包包存在一个函数外部定义的变量中。例子:很多异步操作的函数接受一个闭包参数作为completion handler。这类函数会在异步操作开始之后立刻返回,但是闭包直到异步操作结束后才会被调用。这种情况下,闭包需要"逃逸"出函数,因为闭包需要在函数返回之后被调用:

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}

注意



将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self



自动闭包


自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// 打印“Now serving Ewa!”

总结


Swift 的闭包有以下几个主要的知识点:


  1. 闭包表达式(Closure Expressions):闭包表达式是一种在简短的几行代码中完成自包含的功能代码块。比如数组的排序方法 sorted(by:)
  2. 尾随闭包(Trailing Closures):如果你需要将一个很长的闭包表达式作为一个函数的最后一个参数,使用尾随闭包是很有用的。尾随闭包是一个书写在函数或方法的括号之后的闭包表达式。
  3. 值捕获(Value Capturing):闭包可以在其定义的上下文中捕获和存储任何常量和变量的引用。这就是所谓的闭包的值捕获特性。
  4. 闭包是引用类型(Closures Are Reference Types):无论你将函数/方法或闭包赋值给一个常量还是变量,你实际上都是将引用赋值给了一个常量或变量。如果你对这个引用进行了修改,那么它将影响原始数据。
  5. 逃逸闭包(Escaping Closures):一个闭包可以“逃逸”出被定义的函数并在函数返回后被调用。逃逸闭包通常存储在定义了该闭包的函数的外部。
  6. 自动闭包(Autoclosures):自动闭包能让你延迟处理,因为代码段不会被执行直到你调用这个闭包。自动闭包很有用,用来包装那些需要被延迟执行的代码。

Swift 闭包和OC Block


相似点:


  1. 都是可以捕获和存储其所在上下文的变量和常量的引用的代码块。
  2. 都可以作为参数传递给函数或方法,或者作为函数或方法的返回值。
  3. 都可以在代码块中定义局部变量和常量。
  4. 都可以访问其被创建时所处的上下文环境。

区别:


  1. 语法:Swift 的闭包语法更简洁明了,使用大括号 {} 来定义闭包,而 Objective-C 的 Block 语法相对复杂,使用 ^ 符号和大括号 ^{} 来定义 Block。
  2. 内存管理:Objective-C 的 Block 对捕获的对象默认使用强引用,需要注意避免循环引用;而 Swift 的闭包对捕获的变量默认使用强引用,但通过使用捕获列表(capture list)可以实现对捕获变量的弱引用或无引用。
  3. 类型推断:Swift 的闭包对于参数和返回值的类型具有类型推断的能力,可以省略类型注解;而 Objective-C 的 Block 需要明确指定参数和返回值的类型。
  4. 逃逸闭包:Swift 可以将闭包标记为 @escaping,表示闭包可能会在函数返回之后才被调用;而 Objective-C 的 Block 默认是可以在函数返回后被调用的。

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

盘点那些国际知名黑客(上篇)

iOS
电影中的黑客仅靠一部电脑就可以窃取别人的信息,利用自己高超的技术让公司甚至国家都胆战心惊。“黑客”原指热心于计算机技术、水平高超的电脑高手,但逐渐区分为黑帽、白帽、灰帽。这些术语源自美国流行文化的老式西部电影,其中主角戴白色或浅色帽子,反派戴黑色帽子。黑帽黑客...
继续阅读 »

电影中的黑客仅靠一部电脑就可以窃取别人的信息,利用自己高超的技术让公司甚至国家都胆战心惊。“黑客”原指热心于计算机技术、水平高超的电脑高手,但逐渐区分为黑帽、白帽、灰帽。这些术语源自美国流行文化的老式西部电影,其中主角戴白色或浅色帽子,反派戴黑色帽子。

  • 黑帽黑客以“利欲”为目标,通过破解、入侵去获取不法利益或发泄负面情绪。
    • 灰帽黑客以“昭告”为目标,透过破解、入侵炫耀自己所拥有的高超技术。
    • 白帽黑客以“改善”为目标,破解某个程序作出修改,透过入侵去提醒设备的系统管理者其安全漏洞,有时甚至主动予以修补。


白帽黑客大多是电脑安全公司的雇员,抑或响应招测单位的悬赏,通常是在合法的情况下攻击某系统,而黑帽黑客同时也被称作“Cracker”(溃客),打着黑客的旗帜做不光彩的事情。接下来我们为大家介绍一下世界上非常厉害的顶级黑客。


“互联网之子”亚伦·斯沃茨 



2013 年 1 月 11 日,年仅26 岁的互联网奇才亚伦斯沃茨自杀身亡。他的一生都在为互联网的信息自由而努力。亚伦·斯沃茨 被称作计算机天才、互联网时代的普罗米修斯。但在这些光环的背后,是美国政府为他定下的 13 项重罪指控和最高 35 年的监禁。



1986年亚伦出生在一个程序员之家。3岁学会编程,12岁创建了一个知识共享网站,叫做 The info,功能和维基百科一样,但比维基百科早了 5 年。15岁参与制订了CC协议。18岁入学斯坦福,20岁辍学创业与Reddit项目的两位创始人合伙开公司,并创建了Reddit网站。 Reddit在当时的影响力不断扩大,成为最受欢迎的网站之一。后来,雅伦卖掉Reddit网站,赚了 100 万美元,在他 20 岁那年成为百万富翁。



亚伦参与构建了RSS,这是博客时代的工具,能让用户订阅自己感兴趣的博客,当订阅更新的时候,用户会收到邮件提醒。彼时的亚伦沉浸在互联网程序世界的理想主义美梦里,他希望自己能像他的偶像万维网的发明人蒂姆·博纳斯·李那样,让互联网回归自由、共享的初心。亚伦对赚钱并不感兴趣,他的梦想是追求一个更宏大的目标——互联网知识的自由和共享。



一次机会,亚纶了解到一个名为PACER的网站,它是一个存放法庭电子记录的系统,每看一页里面的内容,联邦政府需要收取 8 美分的管理费用。这项业务每年能带给政府超过 100亿美元的收入。亚纶认为,这些联邦法庭记录的材料本就属于公众,应当免费向公众开放。于是他编写了一个程序,抓取了超过 2000 万页的PACER资料,并将它们投放到公共资源网上,供大家免费阅读,这一举动相当于直接减少了美国司法系统200万美元的收入,PACER也在巨大的舆论压力下逐渐免费。



亚伦有一个“开放图书馆”的梦想,他认为实体的图书馆限制了知识的传播,而互联网是连接书籍、读者、作者、纸张与思想最好的载体。他在08年发表的《开放获取游击队宣言》中写道:信息就是力量,但就像所有力量一样,有些人只想将其占为己有。世界上大多数的期刊都被类似Elsevier、JSTOR这样的巨头垄断,每阅读一篇文献都需要支付一定数量的费用。亚伦想帮助更多的人平等地享受这些知识。于是他通过自己高超的黑客技术,利用麻省理工学院的校园网络免费端口从JSTOR下载了 480 万篇论文,相当于整个文献数据库的80% 。



亚伦毫无意外地被警察逮捕,但由于并未用论文牟利,JSTOR放弃了对他的指控。但马萨诸塞州检察长坚持起诉雅伦违反了1986年的计算机欺诈与滥用法。若罪名成立,亚伦将面临35年的监禁和100万美元的巨额罚款。亚伦拒绝认罪他选择与美国政府斗争。在这期间,他积极参与到各种推动知识共享的运动中,传播他关于知识共享的理念。



2012 年9月 12 日,联邦检察官提出了一份替换起诉书,增加了电子欺诈、非经授权访问计算机等罪名,从原来的 4 项重罪指控变成了 13 项。2013 年1月,雅伦在布鲁克林的公寓中上吊自杀,结束了自己的生命。这一年,他26岁。他死后,超过5万人在白宫网站上请愿,要是起诉亚伦的检察官辞职,维基百科以黑屏为他悼念。



亚伦认为知识共享能提高全人类的智慧,信息共享、言论自由才是真正的平等。在他死后,黑客入侵了麻省理工官网,抗议这个被视为黑客起源地的学府对于亚伦的无所作为。麻省理工的标题页被改为亚伦在2008 年写下的《开放获取游击队宣言》宣言中鼓励每一个网络用户行动起来,阻止商人与政客将网络私有化。



2013 年3月,亚伦被追授詹姆斯麦迪逊奖,用以表彰他捍卫公众的知情权所作出的贡献。


“世界头号黑客”凯文·米特尼克



凯文·米特尼克曾说:“巡游五角大楼,登录克里姆林宫,进出全球所有计算机系统,摧垮全球金融秩序和重建新的世界格局,谁也阻挡不了我们的进攻,我们才是世界的主宰。”



如果说谁的人生像小说一样精彩,那一定当属凯文·米特尼克。他出生于美国洛杉矶,是第一个被美国联邦调查局通缉的黑客,号称“世界头号黑客”。



20世纪80年代,他因多次入侵美国联邦调查局的中央电脑系统等而被逮捕三次。米特尼克的所作所为与人们所熟知的犯罪不同,他所做的一切似乎都不是为了钱,他曾破坏了40多家的安全系统,只是为了表明他“有能力做到”。



2000年,米特改邪归正,成为了一名白帽黑客,成功创办了米特尼克安全咨询公司,专门世界500强企业做网络咨询工作。2023年7月16日去世,享年59岁。


“C语言之父”丹尼斯·里奇



“丹尼斯·里奇一点也不家喻户晓,但是如果你有一台显微镜,能在电脑里看到他的作品,你会发现里面到处都是他的作品。”



丹尼斯·里奇(Dennis Ritchie)是美国计算机科学家,被称为“C语言之父”“Unix之父”。20世纪60年代,丹尼斯·里奇和肯·汤普逊参与了贝尔实验室Multics系统的开发。在开发期间,肯·汤普逊开发了游戏【空间旅行】,但当时的系统不给力,游戏运行速度很慢。



然而不久之后贝尔实验室撤出了Multics计划,里奇和汤普逊利用一台旧的迷你计算机Digital PDP-7,1969年的圣诞节Unix系统诞生了。最初的Unix内核使用B语言编写,为了更好开发Unix,1973年,里奇以B语言为基础发展出C语言,在它的主体设计完成后,他和汤普森又用它完全重写了Unix。



随着计算机的发展,编程语言层出不穷,但无论如何翻涌,都无法改变C语言在编程界德高望重的地位,C++、Java、C#都是在C语言的基础上衍生出来的。而如今诸多流行的操作系统也是在Unix的基础上开发的,如Linux、MacOS甚至最流行的手机系统Android。


丹尼斯·里奇发明的C语言联合Unix操作系统,构建了当代计算机世界的钢筋水泥。正是因为C语言和Unix系统这两项成就,里奇成为了许多编程爱好者膜拜的对象。


“Linux之父”林纳斯·托瓦兹



“Given enough eyeballs,all bugs are shallow.”【很多双眼睛盯着的代码,bug无处藏身】


1991年Linus开发了Linux操作系统,在最初几年里,Linux并没有得到太多关注。但随着互联网的普及,如今的linux已经成为全球最受欢迎的操作系统之一,被广泛应用于服务器、移动设备、家庭电脑和超级计算机等领域。



Linux的诞生充满了偶然,林纳斯经常用他的终端仿真器去访问大学主机上的新闻组和邮件,为了方便读写和下载文件,他自己编写了磁盘驱动程序和文件系统。这些在后来成为了Linux第一个内核的雏形,那时的他年仅21岁。



我们能够看到如今日渐壮大的Linux,但也不难发现,在成功的Linux背后,有着几十年如一日的持之以恒,有着对高质量代码的坚持,更是有着合作的。林纳斯没有建立组织,仅仅通过吸引全球数以万计的自由开发者免费贡献就完成了项目。Linux不仅仅是一个代码项目,也是一种互联网出现以后的新的协作方式——开源模式。


写在最后


现在国家很重视网络安全建设,网络安全已经成为了很多高校的一级学科,因此通过正常学习即可进入网络安全行业,大家一定要遵纪守法,效仿黑客们的行为做一些非法的黑客攻击行为,下期我们将继续为大家送上其他几位世界著名黑客的传奇故事,请大家保持关注哦。


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

🔥🔥🔥996已明确违法,从此拒绝精神内耗!

之前一个禅道用户说,他在国外工作时主动加过两次班,然而被上司慰问了。上司特别严肃地跟他说:“请你不要再加班了,这让我很困扰。我们不加班,而且我无法向我的上司解释你为什么要加班,工作做不完可以明天做,工作只是你一天的一部分,利用好这8小时就可以了。” 对内卷严重...
继续阅读 »

之前一个禅道用户说,他在国外工作时主动加过两次班,然而被上司慰问了。上司特别严肃地跟他说:“请你不要再加班了,这让我很困扰。我们不加班,而且我无法向我的上司解释你为什么要加班,工作做不完可以明天做,工作只是你一天的一部分,利用好这8小时就可以了。”


对内卷严重的公司来说:一天干8小时怎么够?全天all in的状态才是我想要的。于是996疯狂盛行。


冷知识:“996”已严重违反法律规定。


早在2021年8月,最高法、人社部就曾联合发布超时加班典型案例,明确“工作时间为早9时至晚9时,每周工作6天”的内容,严重违反法律关于延长工作时间上限的规定,应认定为无效。


最近两会期间,全国政协委员蒋胜男也在提案中表示,应加强劳动法对劳动者的休息权保护。


由此,新的一波讨论已然来袭。


一、“996”带来了什么?



产品没有核心价值,缺乏核心竞争力,害怕落后于竞争激烈的市场……越来越多的管理者选择用加班、拉长工作时间来弥补技术创新的匮乏。


这种高强度的996工作制,侵占了我们的“充电”时间,甚至让我们丧失对新事物的接收力和思考能力;高强度的工作压力+长期的加班、熬夜、不规律饮食,给身体带来了沉重的负担;在忙碌了一周之后,感受到的是前所未有的迷茫与疲倦,精神内耗愈发严重


而对于企业来说,当员工沦为“执行工具”,原本的创新型发展却变成闭门造车,所以只能不停地加班、拉长工作时间,以产出更多的成果。长此以往,就形成了一种恶性循环。


在普遍“苦996久矣”的环境下,“8小时工作制”的推崇便显得尤为可贵。


二、“8小时工作制”从何而来?


8小时工作制,不应成为一个冷知识。《中华人民共和国劳动法》第三十六条规定:国家实行劳动者每日工作时间不超过8小时,平均每周工作时间不超过44小时的工时制度


8小时工作制的提出,要感谢来自英国的Robert Owen。1817年,他提出了“8小时工作制”,也就是将一天分成3等分,8小时工作、8小时娱乐、8小时休息。在当时一周普遍工作时间超过80个小时的情况下,这种要求简直是天方夜谭。


而8小时工作制得到推行,应归功于福特汽车品牌的创始人亨利·福特。1914年1月,福特公司宣布将员工的最低薪资从每天的2.34美元涨到5美元,工作时间减少至每天8小时。这项计划将会使福特公司多支付1000万美元。



在增加了员工薪资后,最直观的是员工流动率的下降。员工的稳定以及对操作的愈发熟练,增加了生产效率,从而降低成本、提高产量。最后,福特公司只用了两年时间,就将利润增加了一倍。


1926年,福特公司又宣布将员工的工作时间改为每周5天、每天8小时。亨利·福特用实际行动证明了增加工作收入、减少工作时间,对公司来说是可以实现正向创收的。


随后,8小时工作制才开始逐渐普及。随着Z时代的到来,更多新型职场状态也已经诞生。


液态职场早已到来,你准备好了吗?


三、液态职场是什么?



1)“3+2”混合办公模式


早在2022年,全国人大代表黄细花提交了建议,呼吁可推广“3+2”混合办公模式,允许员工每周可选择1-2天在家远程办公。黄细花还表示,推广“3+2”混合办公制,提高员工工作效率的同时,减轻年轻群体的生活压力,减少城市通勤压力。对女性员工而言,弹性的办公时间能让她们更好地平衡工作和生活。混合办公制对企业、员工和社会都将产生深远影响。


于是,不少企业开始了行动。携程推出了“3+2”混合办公模式的新政策:从 2022年3月起,允许员工每周三、周五在家远程办公。


2)四天半工作制


乐视也紧随其后,推出“四天半工作制”,每周三弹性工作半天。


3)“上4休3”的工作制


微软日本公司,也早在2019年8月曾宣布,公司开始试运行每周“上4休3”的工作制度,即每周五、六、日休息3天,周五所有办公室全部关闭。


不管是8小时工作制还是上4休3”,其实本质上都一样:都是为了迎合当下的现状,打破固有传统的工作模式,寻找更加多元化的新型职场状态,让员工能够充分休息,提升效率和创造力,也能节省企业开支,最终双方获益。


这世界变化太快了,上一秒还在“996”中疯狂内卷,下一秒就已经有先行者去探索更适合的工作节奏。液态职场时代已经到来,你准备好了吗?


四、提高工作效率,大胆对996说不!


作为打工人,不管是996还是8小时工作制,虽然都不是我们能决定的,但我们可以用法律来维护自己的权利,学会说“不”。利用好这8小时,发挥出自己的价值,提高自身的创新能力和效率,是为了更有底气的说“不”!这样才能保证企业与员工之间形成一个正向循环。如何利用好8小时?给大家分享几个提高工作效率的小技巧:

  1. 保持桌面整洁,减少其他事物对工作专注度的干扰;

  2. 巧用看板,可视化工作任务,便于进行任务管理;

  3. 排列优先级,按照任务的重要紧急程度,尽量避免并行多个任务;

  4. 随时记录工作中的创意和灵感

  5. 将重复、机械的工作自动化,解放双手;

  6. 定期复盘:不断改进与优化;

  7. 培养闭环思维:凡事有交代,件件有着落,事事有回音。


工作本应是我们热爱的样子。当我们还沉浸在无休止的工作与忙碌中,被疲惫、彷徨等负面情绪包围,开始精神内耗时,是时候明确拒绝996了!


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

iOS实现宽度不同无限轮播图

iOS
背景 项目中需要实现一个不同宽度的图片的无限轮播图效果,而且每次滚动,只滚到下一个图片。由于业界实现的轮播图效果都是等宽图片,所以需要重新根据“以假乱真”的原理,设计一款不同宽度的轮播效果; 演示效果 底部是个collectionView,顶部盖了个透明的sc...
继续阅读 »

背景


项目中需要实现一个不同宽度的图片的无限轮播图效果,而且每次滚动,只滚到下一个图片。由于业界实现的轮播图效果都是等宽图片,所以需要重新根据“以假乱真”的原理,设计一款不同宽度的轮播效果;


演示效果


底部是个collectionView,顶部盖了个透明的scrollView,传入的数据源是:

NSArray *imageWidthArray = @[@(200), @(60), @(120)];



实现思路


  1. 传入一个存储图片宽度的数组,计算出屏幕可见的个数,比如下图,假如可见数为3个;

  2. 左、右两侧各有2个灰块,用于实现以假乱真的数据;(两侧各需生成的灰块数=屏幕可见数-1)

    • 比如当前看到123,左滑会滚到231,再左滑会滚到312,此时设置contentOffset,切到前面那个312;
    • 比如当前看到123,右滑会滚到312,再右滑会滚到231,此时设置contentOffset,切到后面那个231;
    1. 为了性能方面的考虑,使用的是collectionView;
    2. 关于每次滚动,只滚到下一个,实现方式则是在collectionView上面盖一个scrollView,设置其isPagingEnabled = YES; scrollView里面的页数和数据源保持一致(方便计算滚到哪个page);





完整的代码实现


Github Demo


ViewController:

#import "ViewController.h"
#import "MyCollectionViewCell.h"

#define padding 10.f
#define margin 16.f
#define scrollViewWidth (self.view.bounds.size.width - 2 * margin)
#define scrollViewHeight 200.f

@interface ViewController ()<UIScrollViewDelegate, UICollectionViewDelegate, UICollectionViewDataSource>

@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) UIScrollView *topScrollView;
@property (nonatomic, strong) UIPageControl *pageControl;
@property (nonatomic, strong) NSArray *imageWidthArray; // 用户传入,图片宽度数组
@property (nonatomic, assign) NSInteger canSeeViewCount; // 屏幕最多可见几个view
@property (nonatomic, strong) NSMutableArray *imageWidthMuArray;
@property (nonatomic, strong) NSMutableArray *imageContentOffsetXArray;
@property (nonatomic, strong) NSMutableArray *currentPageMuArray;

@end

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    [self setupViewWithImageWidthArray:@[@(200), @(60), @(120)]];
//    [self setupViewWithImageWidthArray:@[@(150), @(80),@(60), @(120)]];
}

-(void)setupViewWithImageWidthArray:(NSArray *)imageWidthArray {
    // 根据机型宽度,计算屏幕可见数量
    self.canSeeViewCount = imageWidthArray.count;
    CGFloat checkWidth = 0;
    for (NSInteger i = 0; i < imageWidthArray.count; i ++) {
        checkWidth += [imageWidthArray[i] floatValue];
        if (checkWidth >= scrollViewWidth) {
            self.canSeeViewCount = i + 1;
        }
    }

    self.imageWidthArray = imageWidthArray;
    self.imageContentOffsetXArray = [NSMutableArray arrayWithCapacity:self.imageWidthArray.count];

    // 插入头尾数据(前后插入可见数-1个)、生成currentPageMuArray
    self.imageWidthMuArray = [NSMutableArray array];
    self.currentPageMuArray = [NSMutableArray array];
    for (NSInteger i = self.imageWidthArray.count - (self.canSeeViewCount - 1); i < self.imageWidthArray.count; i ++) {
        [self.imageWidthMuArray addObject:self.imageWidthArray[i]];
        [self.currentPageMuArray addObject:@(i)];
    }
    [self.imageWidthMuArray addObjectsFromArray:self.imageWidthArray];

    for (NSInteger i = 0; i < self.imageWidthArray.count; i ++) {
        [self.currentPageMuArray addObject:@(i)];
    }

    for (NSInteger i = 0; i < (self.canSeeViewCount - 1); i ++) {
        [self.imageWidthMuArray addObject:self.imageWidthArray[i]];
        [self.currentPageMuArray addObject:@(i)];
    }

    CGFloat collectionViewContentSizeWidth = 0;
    for (NSInteger i = 0; i < self.imageWidthMuArray.count; i ++) {
        CGFloat imageWidth = [self.imageWidthMuArray[i] floatValue];
        if ( i > 0) {
            collectionViewContentSizeWidth += padding;
        }
        [self.imageContentOffsetXArray addObject:@(collectionViewContentSizeWidth)];
        collectionViewContentSizeWidth += imageWidth;
    }

    // collectionView
    UICollectionViewFlowLayout *flowLayout = [[UICollectionViewFlowLayout alloc] init];
    flowLayout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
    flowLayout.minimumInteritemSpacing = padding;
    flowLayout.minimumLineSpacing = padding;

    UICollectionView *collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(margin, 100, scrollViewWidth, scrollViewHeight) collectionViewLayout:flowLayout];
    [collectionView registerClass:[MyCollectionViewCell class] forCellWithReuseIdentifier:@"MyCollectionViewCell"];
    collectionView.dataSource = self;
    collectionView.delegate = self;
    collectionView.bounces = NO;
    collectionView.showsHorizontalScrollIndicator = NO;
    collectionView.backgroundColor = [UIColor brownColor];
    [self.view addSubview:collectionView];
    collectionView.contentSize = CGSizeMake(collectionViewContentSizeWidth, 0);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [collectionView setContentOffset:CGPointMake([self.imageContentOffsetXArray[self.canSeeViewCount - 1] floatValue], 0)];
    });
    self.collectionView = collectionView;

    // topScrollView
    UIScrollView *topScrollView = [[UIScrollView alloc] initWithFrame:collectionView.frame];
    topScrollView.showsHorizontalScrollIndicator = NO;
    [topScrollView setPagingEnabled:YES];
    topScrollView.backgroundColor = [UIColor clearColor];
    topScrollView.delegate = self;
    topScrollView.bounces = NO;
    [self.view addSubview:topScrollView];
    self.topScrollView = topScrollView;
    topScrollView.contentSize = CGSizeMake(self.imageWidthMuArray.count * scrollViewWidth, 0);
    [topScrollView setContentOffset:CGPointMake((self.canSeeViewCount - 1) * scrollViewWidth, 0)];

    // pageControl
    CGFloat pageControlHeight = 50.f;
    UIPageControl *pageControl = [[UIPageControl alloc] initWithFrame:CGRectMake(margin, 100 + scrollViewHeight-pageControlHeight, scrollViewWidth, pageControlHeight)];
    pageControl.numberOfPages = self.imageWidthArray.count;
    pageControl.currentPage = 0;
    [self.view addSubview:pageControl];
    self.pageControl = pageControl;
}

#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (scrollView == self.collectionView) {
        return;
    }

    // 页面整数部分
    NSInteger floorPageIndex = floor(scrollView.contentOffset.x / scrollView.frame.size.width);

    // 小数部分
    CGFloat pageRate = scrollView.contentOffset.x / scrollView.frame.size.width - floor(scrollView.contentOffset.x / scrollView.frame.size.width);
    CGFloat imageContentOffsetX = [self.imageContentOffsetXArray[floorPageIndex] floatValue];
    CGFloat imageWidth = [self.imageWidthMuArray[floorPageIndex] floatValue];
    self.collectionView.contentOffset = CGPointMake(imageContentOffsetX + (imageWidth + 10.f) * pageRate, 0);
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    NSInteger rightIndex = (self.canSeeViewCount - 1) + (self.imageWidthArray.count) - 1;
    NSInteger leftIndex = (self.canSeeViewCount - 1) - 1;

    // 右边卡到尾时
    if (self.collectionView.contentOffset.x == [self.imageContentOffsetXArray[rightIndex] floatValue]) {
        [self.collectionView setContentOffset:CGPointMake([self.imageContentOffsetXArray[leftIndex] floatValue], 0)];
    }

    // 左边卡到头时
    else if (self.collectionView.contentOffset.x == 0) {
        [self.collectionView setContentOffset:CGPointMake([self.imageContentOffsetXArray[self.imageWidthArray.count] floatValue], 0)];
    }

    // 右边卡到尾时
    if (self.topScrollView.contentOffset.x == scrollViewWidth * rightIndex) {
        [self.topScrollView setContentOffset:CGPointMake(scrollViewWidth * leftIndex, 0)];
    }

    // 左边卡到头时
    if (self.topScrollView.contentOffset.x == 0) {
        [self.topScrollView setContentOffset:CGPointMake(scrollViewWidth * self.imageWidthArray.count, 0)];
    }

    // 设置currentPage
    NSInteger floorPageIndex = floor(scrollView.contentOffset.x / scrollView.frame.size.width);
    self.pageControl.currentPage = [self.currentPageMuArray[floorPageIndex] intValue];
}

#pragma mark - UICollectionViewDelegate, UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
    return self.imageWidthMuArray.count;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    MyCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"MyCollectionViewCell" forIndexPath:indexPath];
    cell.labelText = [NSString stringWithFormat:@"%.0f", [self.imageWidthMuArray[indexPath.item] floatValue]];
    return cell;
}

- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
    return CGSizeMake([self.imageWidthMuArray[indexPath.item] floatValue], scrollViewHeight);
}

@end

MyCollectionViewCell:

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface MyCollectionViewCell : UICollectionViewCell

@property (nonatomic, copy) NSString *labelText;

@end

NS_ASSUME_NONNULL_END
#import "MyCollectionViewCell.h"
#import "Masonry.h"

@interface MyCollectionViewCell()

@property (nonatomic, strong) UILabel *label;

@end

@implementation MyCollectionViewCell

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        [self setupUI];
    }
    return self;
}

- (void)setupUI {
self.backgroundColor = [UIColor grayColor];
    UILabel *label = [[UILabel alloc] init];
    label.textAlignment = NSTextAlignmentCenter;
    label.font = [UIFont boldSystemFontOfSize:18];
    [self.contentView addSubview:label];
    [label mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self.contentView);
    }];
    self.label = label;
}

- (void)setLabelText:(NSString *)labelText {
    _labelText = labelText;
    self.label.text = labelText;
}

-(void)prepareForReuse {
    [super prepareForReuse];
    self.label.text = @"";
}
@end

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

环信FCM推送详细步骤

集成FCM推推送
准备的地址有 :https://firebase.google.com
1.firebase官网选择我们自己创建的项目

2.点到这个设置按键

3.我们打开到项目设置->常规 拉到最下面有一个“您的应用” 点击下载json文件,json文件的使用是客户端放在安卓项目的app目录下

4.首先环信需要的信息有 项目设置中-> 服务账号 生成新的私钥 生成的文件我们要上传到环信的管理后台证书部分(V1)

5.点击上传证书会选择你下载的文件,注意!! 名称是由你设置的项目名称的json文件 并不是 google-services.json
6.项目名称 是你的发送者ID 这个id 我们在firebase官网中的项目设置-〉常规 -〉您的项目->的项目编号就是您的SenderID 填写到环信官网即可 另外客户端的 google-services.json 这个文件 打开后 project number 也是SenderID

7.将我们下载好的 google-services.json 文件放到app的目录下 (文件获取可以反回步骤3 查看)

8.打开build的根目录添加 :
buildscript {
dependencies {
// classpath 'com.android.tools.build:gradle:7.2.2'
classpath 'com.google.gms:google-services:4.3.8'
}
}

9.build.gradle.app部分添加:
implementation platform('com.google.firebase:firebase-bom:28.4.1')
implementation 'com.google.firebase:firebase-messaging'

10.对应好appkey 以及我们的客户端初始化fcm的senderID

11.在登陆前 初始化以后 添加以下代码:
EMPushHelper.getInstance().setPushListener(new PushListener() {
@Override
public void onError(EMPushType pushType, long errorCode) {
EMLog.e("PushClient", "Push client occur a error: " + pushType + " - " + errorCode);
}

@Override
public boolean isSupportPush(EMPushType pushType, EMPushConfig pushConfig) {
if(pushType==EMPushType.FCM)
{
return GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(MainActivity.this)
== ConnectionResult.SUCCESS;
}
return super.isSupportPush(pushType, pushConfig);
}
});

12.登陆成功后的第一个页面添加 :
if(GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(NewAcitivty.this) != ConnectionResult.SUCCESS) {
return;
}
FirebaseMessaging.getInstance().getToken().addOnCompleteListener(new OnCompleteListener() {
@Override
public void onComplete(@NonNull Task task) {
if (!task.isSuccessful()) {
EMLog.d("PushClient", "Fetching FCM registration token failed:"+task.getException());
return;
}
// 获取新的 FCM 注册 token
String token = task.getResult();
EMClient.getInstance().sendFCMTokenToServer(token);
}
});

13.清单文件注册sevices 主要是为了继承FCM的服务 必要操作!

添加代码: 重写onMessageReceived
收到消息后 就在这个方法中 自己调用 本地通知 因为fCM的推送只有唤醒
public class FireBaseservice extends FirebaseMessagingService {
@Override
public void onMessageReceived(@NonNull RemoteMessage message) {
super.onMessageReceived(message);
if (message.getData().size() > 0) {
String alter = message.getData().get("alter");
Log.d("", "onMessageReceived: " + alter);
}

}
@Override
public void onNewToken(@NonNull String token) {
Log.i("MessagingService", "onNewToken: " + token);
// 若要对该应用实例发送消息或管理服务端的应用订阅,将 FCM 注册 token 发送至你的应用服务器。
if(EMClient.getInstance().isSdkInited()) {
EMClient.getInstance().sendFCMTokenToServer(token);
}
}
}
14.准备测试 这个时候我们就要验证我们的成果了 首先要看自己登录到环信后 是否有绑定证书 借用环信的即时推送功能查看是否有绑定证书
这个时候看到登录了证书还是没有绑定上 那肯定是客户端出现问题了

15.检查错误 看到提示了com.xxxx.play 安装 这个是因为 你的设备没有打开 VPN 或者VPN不稳定,所以你首先要确定VPN打开并且 稳定 然后我们在重新登录测试

16.这个时候我们在借用即时推送查看 看看有没有绑定到环信 看到该字样就证明你的证书已经绑定上了 直接杀掉进程离线 测试离线推送,(一定要在清单文件注册的谷歌服务中 重新的onMessageReceived 中写入本地通知展示 不然fcm的推送只有唤醒)

升级Xcode 15后,出现大量Duplicate symbols问题的解决方案

升级到Xcode 15后,原先Xcode14可以编译的项目出现大量Duplicate symbols,且引用报错指向同一个路径(一般为Framework)下的同一个文件。经过查找相关资料,查到可通过在Xcode -> Target -> Build...
继续阅读 »

升级到Xcode 15后,原先Xcode14可以编译的项目出现大量Duplicate symbols,且引用报错指向同一个路径(一般为Framework)下的同一个文件。经过查找相关资料,查到可通过

在Xcode -> Target -> Build Setting -> Other Linker Flags 添加一行"-ld64"

即可解决该问题

原因是Xcode15采用了新的链接器(Linker),被称作“ld_prime”。新的连接器有诸多好处,尤其是对合并库的支持方面,具体可以查看WWDC 2023 SESSION 10268 Meet mergeable libraries.。然而,链接器的升级可能会出现不兼容老库的情况出现。遇到这种情况,可以通过恢复旧的连接器来解决这个问题。从Other Linker Flags添加"-ld64"后,就会覆盖Xcode编译时选择的链接器,因此可以正常访问。

收起阅读 »

iOS 开发:分享一个可以提高开发效率的技巧

iOS
前言 在日常的开发中,要想提高开发效率,重要的是要集中精力,今天来讲一个我自己日常在用的方法,我认为提高了我的开发效率,大家也可以尝试一下。 我们做开发都很讨厌写代码的过程中被打断,可能你在找一个 bug,或者在做一个很难的需求,好不容易有了思路,结果一被打断...
继续阅读 »

前言


在日常的开发中,要想提高开发效率,重要的是要集中精力,今天来讲一个我自己日常在用的方法,我认为提高了我的开发效率,大家也可以尝试一下。


我们做开发都很讨厌写代码的过程中被打断,可能你在找一个 bug,或者在做一个很难的需求,好不容易有了思路,结果一被打断,思路全忘了。所以在进入开发前,我会尽可能的把可能打断我的的因素屏蔽掉。比如我会关掉社交软件(尤其是微信),关掉软件推送。然后每过两个小时左右上一次社交软件,集中去处理消息,处理完了退掉继续工作


使用 Xcode 的时候我会开启全屏模式,这可以帮助我集中注意力,而不会分散其他应用程序的注意力,接下来讲讲如何把 Xcode 和模拟器同时进入全屏模式。


Xcode 和模拟器并行的全屏模式


最终的全屏模式如图所示,整个屏幕只有左边是 Xcode,右边是模拟器(当然你也可以调整顺序)。



这是一个能让你完全专注的环境,不被顶部的菜单栏和底部的程序坞栏内容分散注意力。


设置全屏只需这样操作:

  1. 打开 Xcode 和模拟器

  2. 点击 Xcode 左上角第三个按钮,开启全屏,或者使用快捷键 control + command + F

  3. 点击快捷键 control + ⬆️上箭头 打开程序控制,或者使用触控板上的四个手指向上滑动。

  4. 然后将你的模拟器拖入到屏幕顶部 Xcode 所在的窗口中,当拖动到窗口左侧或者右侧时,会显示一个加号,放置在上面即可

  5. 最后点击 Xcode 和模拟器所在的窗口就完成了



最后


保持专注是写好代码和提高效率的一种途径,我见过一些程序员一边写代码,一边还在用手机刷剧,这种写出的代码质量不可能很高,一心二用的开发效率也是很低的。


保持专注本身就是一种技能,刚开始你可能会觉得不习惯(没有微信消息、没有热点资讯),但当你适应了之后,你就会发现你的代码质量和效率都有一定提升,而省下来的时间足以做更多的事情了。


而且我发现每两个小时集中处理一次消息的策略还可以让处理信息的质量变高,比如以前在写代码的时候来了一条微信消息,你点开之后发现不是很重要,可以稍后再回,就先去写代码了,但是当你写完代码时可能已经忘记了回微信消息的事情(因为这条消息已经是已读状态了)。而集中处理可以把未读消息集中处理掉,不容易遗漏。


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

月入五万的西二旗人教你如何活得像月薪五千

在网上看到一篇神奇的文章,确实挺发人深思了的,蜜蜂最为珍惜的是蜂蜜,殊不知自身才是养蜂人的宝贵财产 昨天,有个朋友转给我一篇文章。《月入五万的西二旗人教你如何活得像月薪五千》 这篇文章大概写的是西二旗程序员们吃饭穿衣都会选最便宜的,然后把所有剩余的钱都拿来买房...
继续阅读 »

在网上看到一篇神奇的文章,确实挺发人深思了的,蜜蜂最为珍惜的是蜂蜜,殊不知自身才是养蜂人的宝贵财产


昨天,有个朋友转给我一篇文章。《月入五万的西二旗人教你如何活得像月薪五千》 这篇文章大概写的是西二旗程序员们吃饭穿衣都会选最便宜的,然后把所有剩余的钱都拿来买房。 然后朋友也问我了,说在房价下跌的2023年,你怎么看。


这让我想起了我小时候,家里的阳台上,曾经养过的一箱蜜蜂。因为我家靠近公园,有足够的花可以作为蜜源。所以在我的记忆中,家里从来不缺蜂蜜吃,因为家里人会定期戴着防护面罩开箱,把蜜取走。


「不过我们每次取蜜,都会留下一些蜂蜜给蜜蜂过冬,如果收割得太狠,蜜蜂活不下去,就没有长久的蜂蜜吃了。不能涸泽而渔」。 小学的语文课本里,一直在歌颂蜜蜂的勤劳,可我一直有一个疑问,是不是正因为蜜蜂的勤劳,才使得它们变成了我们的绝佳收割对象。


也许,在 「蜜蜂的认知中,蜂箱蜂巢就是他们最大的资产」,但在更高一维度的养蜂人眼中,把蜂箱视为最大资产的 「蜜蜂,才是养蜂人最大的资产」


所以呢?


生活中极尽节俭,并把房子视为最大资产而背负房贷的西二旗人,会不会是更高维度的操盘手眼中的最大资产呢?


一、操盘手的鬼牌


一个年薪百万的西二旗程序员,交掉社保个税后,到手大概70万,然后公司还要额外缴纳十几万的社保。
也就是,一个西二旗程序员每创造110多万的财富,在第一次分配环节,大概还能剩70万。然后,因为他们吃饭穿衣都极为节省,所以这70万,可能又有五六十万投入了楼市。自己还剩下十几万用于生活。


投入楼市的这五六十万,可能有40多万作为土地出让金交给了操盘手,剩下付给了开发商和上下游供应商。


这么看起来,似乎程序员每赚100块钱,就有80多块以社保个税土地出让金等方式回流到了操盘手的手中。自己只剩下十几块用于生活。


但这里其实是有个问题的,西二旗人可以不买新房的。如果他向老北京人买二手房,他的这笔巨大支出,不就回流到了老北京人手中,而没有回到操盘手的手中吗?


我们来看看这个问题,是通过怎样的步骤,被操盘手解决的。


第一步,零成本选址


操盘手首先要选一块只有很少居民的土地,最好土地上没有老北京人的房子。一块荒地那就是最好了,这样就能实现零拆迁成本征用所有土地。 我们看到,北京西二旗,上海张江唐镇,杭州未科,成都天府新区,几十年前,可能都是比较荒的,也几乎都是这个套路。


第二步,引入科技公司入驻


有花才能引来蜜蜂,有工作机会才能引来年轻人。所以如果能有一些政策优惠啥的,把科技大企业引来入驻,也就等同于,引来了大批期待高薪工作的年轻人。 于是,北京西二旗后厂村路成了程序员宇宙中心,上海张江唐镇成为高科技园区。杭州未科变身未来科技城。。。


第三步,开始售卖科技公司周边的土地


当年轻人开始在科技公司上班,就会就近选择可购买的房子。但附近所有的土地,都在操盘手的手中。所以,操盘手拥有绝对的定价权。于是,年轻人以未来的收入为背书,借债购买房产,支付房产的土地出让金。并开始定期还贷。


所以,当西二旗人把西二旗的房子视为他们最大资产的时候,他们不知道的是,房子并不是最大资产,「背上债务的他们,才是别人眼中的最大资产」


通过债务的跨时间周期交易,他们把每年收入的80%以上,以社保个税土地出让金的形式交了出去。但我始终有个顾忌,80%,这样的上交比例是不是太高了。


如果把80%降为50%,也许他们就不需要996,也可以像欧洲人那样去海边晒太阳,时间多了,生育率也会更高。 目前京沪的总和生育率,已是0.7,不但是全国最低,更是全球最低。


如果涸泽而渔的话,会不会生育率提升不起来呢? 但我似乎又发现了一个隐藏的解法,对于蜜蜂我们不能涸泽而渔,「但对于西二旗和张江程序员,其实是可以涸泽而渔的」


二、谁是蜂王,谁是工蜂


在一个蜂群巢穴,是有着明确的分工的。 一个巢穴的蜜蜂分为三种,蜂王,雄蜂,和工蜂。 工蜂是雌蜂但无生育能力,只负责采蜜工作和照顾小蜜蜂。 蜂王不采蜜,只接受工蜂的养料,专职生小蜜蜂。 雄蜂也不采蜜,唯一的工作就是,和蜂王交配。


也就是说,让每个蜂种,都只从事自己最擅长的工作。 「这似乎给了我一些启示」。 虽然京沪的总和生育率已经降到了0.7,是全球最低。但这并不可怕,其实是有解法的。


我们来做个战棋推演。 一个家庭的分工,夫妻当中赚钱多的那个去赚钱,赚钱少的在家照顾孩子,会让这个家庭的效率最大化。 那么,提升全国的生育率,我们如果仅从效率最大化的角度去考虑,也会有两个方向。



  • 方向一,用最少的钱,激励出最多的生育。


从这个方向看,显然,钱应该花在三四线城市。给一线城市居民补贴50万,可能人家也不愿意生,毕竟房价生活成本高。但如果是四线城市,可能给20万,人家就愿意生了。毕竟养育成本低。 所以,基于花钱花在刀刃上的原则,「补贴三四线城市,其拉动生育效果会明显好于一线城市」。补贴一线城市一个孩子的钱,在四线城市可以补贴好几个孩子了。



  • 方向二,激励同样生育成果的前提下,花费最小的代价。


从这个角度,如果一个985高学历,年薪百万的女性,辞职生二胎照顾孩子,每年会损失百万财富的创造。但如果是一个大专学历,年薪5万的女性辞职生二胎照顾孩子,每年只损失5万财富的创造。 也就是说,达成同样生育数量的情况下,代价是完全不同的。


当然,从人文角度,985女当然和大专女享有同等生育权。从个人角度自主生育的话,那当然都没问题,盈亏反正也是自负。但如果说要操盘手额外花钱激励生育的话,从全国总盘子的效率角度考虑,激励大专女,会代价更小。 那如果操盘手只从效率最大化的角度考虑,很显然,应该让一线城市高学历中产尽可能努力工作,并通过 「税收或买房形成的支付转移」,转移到三四线城市去补贴育龄女性生育。等三四线孩子长大,通过高考选拔后,再进入一线城市,开始下一次循环。 从这个角度出发,很明显, 北京西二旗或上海张江的程序员,贡献蜂蜜,低生育率,是工蜂; 三四线城市多子女家庭,获取转移支付的蜂蜜,高生育率,是蜂王。


蜂王的子女长大后,再去一线城市进入新的一次循环。从而一线城市低生育率问题可解。 而现在的真实情况也确实是如此,比如贵州的总和生育率,就是上海的大约三倍。 有人可能会问,蜂王子女长大后去一线,能那么容易留下来么? 答案是,容易的! 因为今天不容易不代表未来不容易,万物皆周期! 按上海如今0.7的总和生育率,每过一代,就会损失2/3的人口。两代过后,90%的人口就没了。


这时候,是急切需要蜂王的后代,来上海补充年轻劳动力的。 我记得20年前,上海还有一些教上海话的电视节目,而今天几乎绝迹。既然两代之后,上海人口就损失90%,那自然上海也就会变成一个完全的普通话城市。


三、北京西二旗和上海张江男的终极宿命


最后一个问题,西二旗程序员,为啥心甘情愿在吃穿上拼命节省,而把大笔的钱投入楼市呢?


答案是,他们认为房子是核心资产


但问题在于,任何资产,或者说财富,其本质,都是对他人劳动的索取权。也就是说,世间的一切资产,不论是房子,股票,货币,黄金,它最终要能兑换成人的劳动,才有意义。


可问题就在于,2020年之后的生育率断崖式下跌了。未来所有的人,都会盯着这仅有的少数年轻人的劳动价值。 这其中,当然也包括操盘手。毕竟操盘手要负责老人养老金,公务员工资,义务教育等一系列花钱的地方。


现在西二旗人每年收入的80%,切切实实通过各种渠道给出去了,然后换来了一套西二旗的大房子。可30年后,如果西二旗人要用这套房子去换取未来年轻人同样的劳动时,操盘手能让他们得逞吗? 操盘手会不会和今天一样,同样划出一块荒地,然后引入30年后的风口科技公司(不知道会不会是超导,人工智能这些,还是更超前的公司),然后把年轻人引到新的地块呢?毕竟只有这样,才能最大化虹吸未来年轻人的劳动价值。


毕竟蜂巢不是资产,采蜜的蜜蜂,才是操盘手最大的资产。 而30年后,目前人口结构处于青壮年期的西二旗,张江,会不会自然衰老为一个以六七十岁年龄结构为主的老龄化社区呢?


如果一个社区,居民都变成了中老年,即便没有操盘手号召,企业出于自身招聘的考虑,也要搬走了。至于搬去哪里,那自然要看操盘手要把年轻人引向哪里。 如果一个社区,没有企业和工作机会,住的都是中老年,那么必然就不存在接盘力量。


这一点,似乎细思极恐。 原住民年轻时花大力气努力购买的房子,最后会变成一个笑话吗? 如果真是如此,那么该社区原住民的终极悲惨宿命,也就是必然的结局了


四、后记


本文无意得罪张江和西二旗的程序员,因为文中所说的逻辑,其实适用于所有在科技新区安家的一二线城市中产。


但因为我自己是一个前淘宝的程序员。想想还是自嘲下自己这个群体吧。


不过确实能反映当下一些问题引发一些思考,当然还是要保持积乐观的生活态度,想到了学生时代 学习的 普希金的一首诗《假如生活欺骗了你》


「假如生活欺骗了你,」


「不要悲伤,不要心急!」


「忧郁的日子里须要镇静:」


「相信吧,快乐的日子将会来临!」


「心儿永远向往着未来;」


「现在却常是忧郁。」


「一切都是瞬息,一切都将会过去;」


「而那过去了的,就会成为亲切的怀恋。」


作者:Android茶话会
来源:juejin.cn/post/7268975896370937893
收起阅读 »

为什么我们总是被赶着走

最近发生了一些事情,让shigen不禁的思考:为什么我们总是被各种事情赶着走。 一 第一件事情就是工作上的任务,接触的是一个老系统ERP,听说是2018年就在线上运行的,现在出现问题了,需要我去修改一下。在这里,我需要记录一下技术背景: ERP系统背景 后端...
继续阅读 »

最近发生了一些事情,让shigen不禁的思考:为什么我们总是被各种事情赶着走。



第一件事情就是工作上的任务,接触的是一个老系统ERP,听说是2018年就在线上运行的,现在出现问题了,需要我去修改一下。在这里,我需要记录一下技术背景:



ERP系统背景

后端采用的是jfinal框架,让我觉得很奇葩的地方有:



  • 接受前端的参数采用的HashMap封装,意味着前端字段传递的值可以为字符串、数字(float double)

  • 仅仅一个金额,可以有多种形式:1111.001,1,111.001

  • 格式化 1.00000100 小数点保存8位,这样的显示被骂了

  • 数据库采用的是oracle,jfinal的ORM工具可以采取任何的类型存入数据表的字段里,我就遇到了‘1.1111’字符串存入到定义为double的字段中

  • 原来的设计者存储金额、数量全部采用 flaot、double,凭空出现0.0000000000000001的小数,导致数量金额对不上

  • 小数位0.00000000001 会在前端显示成1-e10,直接在sql上格式化

  • sql动辄几百行,上千行,各种连表

  • sql还会连接字典表,显示某个值代表的含义

  • ……


前端不知道啥框架,接近于jquery+原生的js



  • 每改一段代码,都需要重启后端服务

  • 各种代码冗余

  • 后端打包一次40分钟+

  • ……


最关键的是:所有的需求口头说,我也是第一次接触,一次需求没理解,被运维的在办公室大声批评:你让用户怎么想?



后来,需求本来要半个月完成,拖了一个月才勉强结束。一次快下班的时候出现了问题,我没有加班,也因为遇到了问题没人帮忙。第二天问进度,没进展,领导叫去看会,说态度不好。后来换组了……



第二件事情就是我的公众号更新问题,我在八月份的时候个自己定了一个目标:公众号不停更。到最近一段时间发现:很难保持每天更新的需求了。因为我接触到的技巧很少,每篇文章的成本也很大。就拿我的某个需求为例,我需要先把代码写出来,测试完成之后再去写文章,这整个过程最低也需要两个小时的时间。成本很大,所以我有一次很难定顶住这个压力,推荐了往期的文章。


我也经常关注一些技术类的博客,看他们写的文章发现部分的博客都是互相抄袭的,很难保持高质量。更多的是在贩卖焦虑,打广告。


我希望我的每一篇文章都是有意义的,都是原创的、有价值的。所以,我也在陷入了矛盾中,成本这么大,我需要改变一下更新的节奏吗?



最后一件事情就是:我感冒了。


事情是这样的,一连几天没有去跑步了,家里的健腹轮也很少去练了,除了每天骑行了5公里外,我基本没有啥运动量。我以为我吃点维生素B、维生素C我的体质就会好一点,大错特错了。


周一发现嗓子有点干痒疼,晚上还加了班,睡觉的时候已经是凌晨一点了。周二就头很晕、带一点发热的症状,我赶紧下午去医院,在前台测了一下体温,直接烧到了28.4摄氏度。血常规检测发现是病毒性感染,买了两盒药回来了。下午一直在睡觉,睡到了十一点。


也在想:难道我的体质真的这么差吗?如果我坚持那几天戴口罩,坚持运动会不会好一些。我想到了我的拖延症。


我的dock栏永远是满的,各种软件经常打开着,Java、数据库,总是有很多的事情要去做,很忙的样子,最后发现没时间去运动了。一次健腹轮的运动不到十分钟,我都没有去行动。



这次的感冒,让我更加的重视起我的健康了,也让我觉得我丧失了主动性,总是被生活赶着走。


所以,提到了这么多,涉及到了任务的规划、任务中的可变因素……我觉得除了计划之外,更多的是需要保持热爱。不仅仅是热爱生活、热爱运动、热爱事业,更是热爱自己拥有的一切,因为:爱你所爱,即使所爱譬如朝露


作者:shigen01
来源:juejin.cn/post/7280740613891981331
收起阅读 »

智能门锁临时密码的简单实现~~

引子 话说新房子装修,安装了遥遥领先智能门锁PRO,最近到了家具进场的阶段。 某日,接到一通电话:“哥,你现在家里有人吗?你的书桌到了。” 原来是快递小哥,我回复他:“家里没人,但是有智能锁,嗯,因为临时密码有时间限制,等下到了再给我回下电话,我把临时密码给你...
继续阅读 »

引子


话说新房子装修,安装了遥遥领先智能门锁PRO,最近到了家具进场的阶段。


某日,接到一通电话:“哥,你现在家里有人吗?你的书桌到了。”


原来是快递小哥,我回复他:“家里没人,但是有智能锁,嗯,因为临时密码有时间限制,等下到了再给我回下电话,我把临时密码给你。”


“好嘞,那到时候联系”


挂断电话,我随手打开手机上的花粉生活APP,但是感觉有点不对劲,我去,设备咋都离线了(后来发现是网络欠费)?我顿时虎躯一震,脑海中浮现了快递小哥到了后发现自己白跑一趟,带着满头大汗、气喘吁吁并且嘴里一顿C语言输出的尴尬场景...


但是我惊喜的发现,门锁的卡片虽然离线但还可以正常进入,我抱着试一试的心态点进去,临时密码竟然可以正常生成,真牛!


于是我点击了生成临时密码...


电话又响起:“哥我到了,把密码给我吧”


我将临时密码给小哥开了门,一切顺利...




实现


这是前段时间亲身经历的一件事,原本以为智能门锁临时密码的功能需要网络支持,服务器生成临时密码给用户,同时下发到门锁里面。现在发现,并不需要门锁联网也可以执行密码验证的操作。
脑海中思考了下,临时密码离线验证这个功能可能是类似这样实现的:



  • 门锁端和服务器端采用相同的规则生成临时密码,并且密码生成规则里面包含了时间这个因素

  • 用户请求临时密码,服务端按照规则生成临时密码返回给用户

  • 用户输入临时密码解锁,门锁按照同样的规则进行校验
    以上实现是一个直觉性的思考,实际编码落地根据不同的需求会有更多的考虑,以我在使用的遥遥领先牌智能门锁Pro为例,下面来做一个简单的实现...


首先,让来看看这款门锁的临时密码有哪些限制条件:


limit12.png


lim22.png


限制条件有:



  • 单个密码有效期为30分钟

  • 有效期内只能使用一次

  • 一分钟内只能添加一个临时密码


根据这些限制条件和前面的思考,密码生成规则可以这样设置:



  • 拼接产品序列号+当前时间字符串,获取拼接后字符串的hashcode,然后对1000000(百万)取余,得到6位数字作为临时密码。并且时间字符串按照yyyy-MM-dd HH:mm 格式,精确到分钟

  • 加入产品序列号的原因是为了让不同门锁在相同时间产生不同的密码,如果只以时间为变量肯定是不安全的

  • 由于门锁生成的限制条件里面约定了一分钟只能添加一个临时密码,因此时间变量也精确到分钟,保证每分钟的临时密码不同,分钟内相同。


然后是实现思路:



  • 用户请求服务端,服务端根据密码生成规则返回一个临时密码

  • 快递小哥拿着临时密码在门锁现场输入

  • 门锁按照临时密码输入的时间点,计算时间点前30分内每一分钟对应的密码,30分钟对应30个临时密码。为什么是30分钟?因为密码30分钟内有效

  • 门锁将快递小哥输入的密码与生成的30个密码进行一一比对,如果有匹配的密码,说明临时密码有效

  • 将输入的临时密码缓存,每次输入密码时都要去缓存里面判断临时密码是否在30分钟内使用过,如果使用过就不能开锁。为什么要判断是否30分钟内使用过?因为有效期内只能使用一次




有了以上思路,下面代码的编写工作就比较简单了,开整...


首先创建三个类:OtherTerminal、SmartLock、PasswordUtils 分别,表示其他可获取密码的终端、门锁以及跟密码相关的工具类


首先是OtherTerminal类,相当于可获取密码的终端,例如我们的手机或者平板,主要功能是调用PasswordUtils工具类根据门锁的序列号和当前时间来获取有效临时密码。



public class OtherTerminal {
private final static String serialNumber = "XiaoHuaSmartLock001";
public static void main(String[] args) {
System.out.println("当前开锁密码:"+PasswordUtils.generate(serialNumber, PasswordUtils.localDateTimeToStr(LocalDateTime.now())));
}
}


接着是SmartLock类


SmartLock的main方法里面等待控制台的输入,并对输入的密码进行验证。验证调用了verify方法。


verify方法的执行逻辑:调用PasswordUtils工具类,获取过去30分钟内每分钟对应的临时密码,判断输入的密码是否在这些临时密码当中。如果存在说明临时密码有效,还需对当前密码在过去30分钟内是否使用进行判断,保证密码只能使用一次。这个判断是通过调用PasswordUtils工具类的getAndSet方法实现的。


如果认证成功,则开锁。否则开锁失败。


// 智能门锁
public class SmartLock {

private final static String serialNumber = "XiaoHuaSmartLock001";
private final static Integer expirationTime = 30;


public static void main(String[] args) {
// 步骤:首先生成过去30分钟内的所有数字

Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
int password = scanner.nextInt();
if (verify(password)) {
System.out.println("开锁成功,当前时间:" + LocalDateTime.now());
} else {
System.out.println("开锁失败,当前时间:" + LocalDateTime.now());
}
}
scanner.close();

}

private static boolean verify(Integer inputPassword) {
// 获取当前时间点以前30分钟内的所有密码
LocalDateTime now = LocalDateTime.now();
LocalDateTime validityPeriod = now.minusMinutes(expirationTime);
List<Integer> validityPeriodPasswords = new ArrayList<>();

while (validityPeriod.isBefore(now.plusMinutes(1L))) {
validityPeriodPasswords.add(PasswordUtils.generate(serialNumber, PasswordUtils.localDateTimeToStr(validityPeriod)));
validityPeriod = validityPeriod.plusMinutes(1L);
}
System.out.println(validityPeriodPasswords);
return validityPeriodPasswords.contains(inputPassword) && PasswordUtils.getAndSet(inputPassword);
}
}

再来看下PasswordUtils工具类,这个类内容较多,分步解释:
首先是生成6位临时密码的generate方法,比较简单。但是这样生成的密码不能以0开头,是缺点!


/**
* 生成一个密码
*
* @return 返回一个六位正整数
*/

public static Integer generate(String serialNumber, String time) {
String toHash = time + serialNumber;
return Math.abs(toHash.hashCode() % 1000000);
}

接着是一个格式化时间的方法,将时间格式化为:yyyy-MM-dd HH:mm。精确到分钟,generate方法的第二个参数time需要调用此方法来保证时间以分钟为单位,这样分钟内生成的密码都是相同的


public static String localDateTimeToStr(LocalDateTime localDateTime) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
return formatter.format(localDateTime);
}

最后是门锁对临时密码的管理:



  • 临时密码存储在一个map对象中:usedPasswordMap

  • 有一个标记对象clearTag用于标记是否应当对usedPasswordMap进行清理操作,用于清理已过期的临时密码

  • 临时密码存在时间大于30分钟,判断为已过期


下面是临时密码过期判断和过期清理的方法


/**
* @param current 当前时间
* @param compare 比较时间
* @return 是否过期
*/

private static boolean expired(long current, long compare) {
Instant endInstant = Instant.ofEpochMilli(current);
LocalDateTime end = LocalDateTime.ofInstant(endInstant, ZoneId.systemDefault());
Instant beginInstant = Instant.ofEpochMilli(compare);
LocalDateTime begin = LocalDateTime.ofInstant(beginInstant, ZoneId.systemDefault());

Duration duration = Duration.between(begin, end);
long actualInterval = switch (PasswordUtils.expirationUnit) {
case SECONDS -> duration.toSeconds();
case MINUTES -> duration.toMinutes();
case HOURS -> duration.toHours();
case DAYS -> duration.toDays();
default -> throw new IllegalArgumentException("输入时间类型不支持");
};
return actualInterval >= (long) PasswordUtils.expirationTime;
}

/**
* 清理过期的密码
*/

private static void clearExpired() {
Iterator<Map.Entry<Integer, Long>> iterator = usedPasswordMap.entrySet().iterator();
Long currentTimestamp = System.currentTimeMillis();
while (iterator.hasNext()) {
Map.Entry<Integer, Long> item = iterator.next();
if (expired(currentTimestamp, item.getValue())) {
iterator.remove();
}
}
}

getAndSet方法:



  • 首先判断是否达到了清理阈值,从而执行是否清理的操作,用于节省资源消耗

  • 从usedPasswordMap中获取当前输入密码是否存在,如果不存在说明密码未使用过,则将当前密码设置到map里面并返回true,否则还要进行进一步的判断,因为可能存在历史密码但是已过期和当前密码重复的情况

  • 若usedPasswordMap中存在当前密码,调用expired方法,如果历史密码过期了说明当前密码有效,并刷新时间戳,否则说明有效期内当前密码已经使用过一次


/**
*
* @param password
* @return false说明密码已经使用过,true则表示密码可以使用
*/

public static boolean getAndSet(Integer password) {
// usedPasswordMap存储的过期密码可能会越来越多,需要定期清理
if (clearTag > clearThreshold) {
if (!usedPasswordMap.isEmpty()) {
clearExpired();
}
clearTag = 0;
}
clearTag++;
Long usedPasswordTimestamp = usedPasswordMap.get(password);
Long currentTimestamp = System.currentTimeMillis();
if (ObjectUtils.isEmpty(usedPasswordTimestamp)) {
usedPasswordMap.put(password, currentTimestamp);
return true;
}
// 到了这里说明密码已经使用过(有效期内,或之前),若使用时间距今在有效期内,说明当期已经使用过,否则是以前使用的
if (expired(currentTimestamp, usedPasswordTimestamp)) {
usedPasswordMap.put(password, currentTimestamp);
System.out.println("密码虽然已使用,但为历史使用,因此当前密码有效");
return true;
}
System.out.println("密码有效期内已使用一次");
return false;
}



验证


我将门锁程序部署到我的服务器上面,并运行。随便输入一个数字,例如123456,返回开锁失败。


image.png


然后本地运行OtherTerminal类获取临时密码:974971


image.png
再去门锁上验证试试:开锁成功!


image.png


最后完整的PasswordUtil工具类的代码贴在这里:


// 密码工具类

public class PasswordUtils {
private static Map<Integer, Long> usedPasswordMap = new HashMap<>();
private final static Integer expirationTime = 30;
private final static TimeUnit expirationUnit = TimeUnit.MINUTES;
private final static Integer clearThreshold = 30;
private static Integer clearTag = 0;

/**
* 获取code状态,并设置到使用code里面
*
* @param password
* @return false说明密码已经使用过,true则表示密码可以使用
*/

public static boolean getAndSet(Integer password) {
// usedPasswordMap存储的过期密码可能会越来越多,需要定期清理
if (clearTag > clearThreshold) {
if (!usedPasswordMap.isEmpty()) {
clearExpired();
}
clearTag = 0;
}
clearTag++;
Long usedPasswordTimestamp = usedPasswordMap.get(password);
Long currentTimestamp = System.currentTimeMillis();
if (ObjectUtils.isEmpty(usedPasswordTimestamp)) {
usedPasswordMap.put(password, currentTimestamp);
return true;
}
// 到了这里说明密码已经使用过(有效期内,或之前),若使用时间距今在有效期内,说明当期已经使用过,否则是以前使用的
if (expired(currentTimestamp, usedPasswordTimestamp)) {
usedPasswordMap.put(password, currentTimestamp);
System.out.println("密码虽然已使用,但为历史使用,因此当前密码有效");
return true;
}
System.out.println("密码有效期内已使用一次");
return false;
}


/**
* @param current 当前时间
* @param compare 比较时间
* @return 是否过期
*/

private static boolean expired(long current, long compare) {
Instant endInstant = Instant.ofEpochMilli(current);
LocalDateTime end = LocalDateTime.ofInstant(endInstant, ZoneId.systemDefault());
Instant beginInstant = Instant.ofEpochMilli(compare);
LocalDateTime begin = LocalDateTime.ofInstant(beginInstant, ZoneId.systemDefault());

Duration duration = Duration.between(begin, end);
long actualInterval;
switch (PasswordUtils.expirationUnit) {
case SECONDS:
actualInterval = duration.toSeconds();
break;
case MINUTES:
actualInterval = duration.toMinutes();
break;
case HOURS:
actualInterval = duration.toHours();
break;
case DAYS:
actualInterval = duration.toDays();
break;
default:
throw new IllegalArgumentException("输入时间类型不支持");
}
return actualInterval >= (long) PasswordUtils.expirationTime;
}

/**
* 清理过期的密码
*/

private static void clearExpired() {
Iterator<Map.Entry<Integer, Long>> iterator = usedPasswordMap.entrySet().iterator();
Long currentTimestamp = System.currentTimeMillis();
while (iterator.hasNext()) {
Map.Entry<Integer, Long> item = iterator.next();
if (expired(currentTimestamp, item.getValue())) {
iterator.remove();
}
}
}

/**
* 生成一个密码
*
* @return 返回一个六位正整数
*/

public static Integer generate(String serialNumber, String time) {
String toHash = time + serialNumber;
return Math.abs(toHash.hashCode() % 1000000);
}

public static String localDateTimeToStr(LocalDateTime localDateTime) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
return formatter.format(localDateTime);
}

}

最后的最后,这种方法生成的密码有个bug,就是30分钟内生成的30个密码里面会有重复的可能性,不过想来发生概率很低,看后续如何优化了。


作者:持剑的青年
来源:juejin.cn/post/7280459667129188387
收起阅读 »

中秋节,我只想回家😭

web
前言 中秋节马上就要到啦,中秋节作为刻入我们DNA里面的团圆佳节,我想大家关心的肯定是团圆了吧 对于普通人回家跟家人团圆可能也就是一张车票而已 但是今年~我就想知道28 29号的票都被谁买去了,我现在连30号的都没买到!!! 这让我怎么团圆啊!!! 忆中秋 还...
继续阅读 »

前言


中秋节马上就要到啦,中秋节作为刻入我们DNA里面的团圆佳节,我想大家关心的肯定是团圆了吧


对于普通人回家跟家人团圆可能也就是一张车票而已


但是今年~我就想知道28 29号的票都被谁买去了,我现在连30号的都没买到!!!


这让我怎么团圆啊!!!


忆中秋


还记得小时候过中秋,就跟过年一样。记忆中的中秋,各家各户外出的人都会从天南地北赶回来,一家人团聚在一起;记忆中的月亮,又圆又亮,坐在门前的小院子里,一起吃着月饼赏着月,已经很开心了。


小时候
听着嫦娥的故事
心里却惦记着月饼

长大了
手里捧着月饼
心里却想着嫦娥

中秋节到了
愿你重拾童年的快乐
点缀幸福的生活

程序员怎么过中秋呢?


当然是以代码来庆祝一下中秋啦,正好还可以参加下中秋创意大赛!但是很难有一些新奇的创意了,那就做一个猜灯谜的小游戏供大家消遣吧哈哈哈。话不多说,开始今天的主题,制作灯谜小游戏,码上掘金会有源码哦,很基础的~


1、游戏简介



  • 玩家在提交答案后,游戏将根据玩家的回答情况给予相应的提示信息。如果答案正确,将显示回答正确的提示,并增加相应的得分;如果答案错误,将显示回答错误的提示,并扣除相应的分数。同时,游戏会记录玩家的最高分数,以便玩家挑战自己的最好成绩。

  • 玩家可以选择继续猜下一道题目,直到回答完所有题目或不再继续。游戏结束后,将显示玩家的得分和最高分数,并提供重新开始和退出游戏的选项。


2、游戏规则



游戏包括多道灯谜题目,每个题目都有一个对应的答案。


玩家需要在输入框中输入自己的答案,并点击提交按钮进行确认。


如果答案正确,将显示相应的提示信息,表示回答正确;如果答案错误,将显示错误提示信息并扣除相应分数。


游戏根据玩家的回答情况给予评分,并记录最高分数。


玩家可以选择重置游戏重新开始,或者退出游戏。



3、游戏设计



  • 定义题目和答案数组,每个元素包含一个题目和对应的答案。

  • 初始化游戏数据,包括当前题目索引、得分和最高分数。

  • 显示当前题目,将题目显示在页面上供用户查看。

  • 用户输入答案后,点击提交按钮。

  • 检查用户答案是否正确,如果正确则增加得分,显示回答正确的提示;如果错误则显示回答错误的提示。

  • 更新最高分数,如果当前得分超过最高分数,则更新最高分数。

  • 显示当前得分和最高分数。

  • 清空输入框,准备接受下一题答案。

  • 判断是否回答完所有题目,若回答完所有题目则显示游戏结束的提示信息,并禁用提交按钮;若未完成则显示下一题。

  • 提供重新开始游戏的功能,重置游戏数据并重新显示第一题。

  • 提供退出游戏的功能,显示退出游戏的提示信息。


4、功能实现


题目和答案的存储



题目和答案的存储可以使用数组来实现,每个元素表示一道题目和对应的答案。例如:



// 定义题目和答案
const questions = [

{ question: '中秋佳节结良缘 (打一城市名)', answer: '重庆' },
{ question: '中秋鼓励消费 (打一成语)', answer: '月下花前' },
{ question: '中秋遥知兄弟赏光处 (打一唐诗目)', answer: '望月怀远' },
{ question: '木兰迷恋中秋夜 (打一成语)', answer: '花好月圆' },
{ question: '中秋渡蜜月 (打一成语)', answer: '喜出望外' }
];

每个元素都是一个对象,包含两个属性:question表示题目,answer表示答案。可以根据实际需要修改题目和答案的内容和数量。


5、游戏展示


话不多说直接上效果 !


作者:优秀稳妥的Zn
来源:juejin.cn/post/7280747221510733878
收起阅读 »

外甥女问我什么是代码洁癖,我是这么回答的...

1. 引言 哈喽,大家好,我是小 ❤,一个在二进制世界起舞的探险家,幻想有一天可以将代码作诗的后台开发。 今天,我要和大家聊聊程序员的神秘技能——重构!别担心,我会用通俗易懂的语言和一些趣味对话来帮助你理解和掌握这个技能,我 8 岁的外甥女听了都说懂。 1.1...
继续阅读 »

1. 引言


哈喽,大家好,我是小 ❤,一个在二进制世界起舞的探险家,幻想有一天可以将代码作诗的后台开发。


今天,我要和大家聊聊程序员的神秘技能——重构!别担心,我会用通俗易懂的语言和一些趣味对话来帮助你理解和掌握这个技能,我 8 岁的外甥女听了都说懂。


1.1 背景


代码开发:



一个月后:



后面有时间了改一改吧(放心,不会有时间的,有时间了也不会改)。


六个月后:



如上,是任何一个开发者都会经历的场景:早期的代码根本不能回顾,不然一定会陷入深深的怀疑,这么烂的代码真是出自自己的手吗?


更何况,目前大部分系统都是协同开发,每个程序员的命名规范、编码习惯都不尽相同,就导致了一个系统代码,多个味道的情况。


重构是什么


妍妍:嘿,舅舅,听说你要分享重构,这又是什么新鲜事?



❤:嗨,妍妍!重构就是改进既有代码的设计,让它更好懂、更容易维护,而不改变它的功能。想象一下,它就像是给代码来了个变美的化妆术,但内在还是那个代码,不会变成"不认识的人"。


为什么要重构


露露:哇,听起来好厉害,那为什么我们要重构呢?



❤:哈哈,好问题,露露!因为代码是活的,一天天在变大,当代码变得难以理解、难以修改时,它就像是一头头重的大象,拖慢了我们前进的步伐。重构就像是给大象减肥,使它更轻盈、更灵活,开发速度也能提升不少!


这和你们有小洁癖,爱收拾房间一样,有代码洁癖的程序员也会经常重构 Ta 们的代码呢!


什么时候要重构


妍妍:听起来有道理,但什么时候才应该使用重构呢?



❤:好问题,妍妍!有以下几种情况:




  • 当你看到代码中有好几处长得一模一样的代码,这时候可以考虑把它们合并成一个,减少冗余。




  • 当你的函数或方法看上去比词典还厚重时,可以把它拆成一些小的部分,更好地理解。




  • 当你要修复一个 bug,但却发现原来的代码结构太复杂,修复变得像解迷一样难时,先重构再修复就是个好主意。




  • 当你要添加新功能,但代码不让你轻松扩展时,也可以先重构,然后再扩展。




重构的步骤


露露:明白了舅舅,那重构的具体步骤是什么呢?



❤:问得好,露露,看来你有认真在思考!接下来让我给你介绍一下重构的基本步骤吧!


2. 如何重构


重构之前,我们需要识别出代码里面的坏味道代码。


所谓坏味道,就是指代码的表面的混乱,和深层次的腐化现象。简单来说,就是感觉不太对劲的代码。


2.1 坏味道代码



在《重构-改善既有代码的设计》一书中,讲述了这二十多种坏味道情况,我们下面将挑选最常见的几种来介绍。


1)方法过长


方法过长是指在一个方法里面做了太多的工作,常常伴随着方法中的语句不在同一个抽象层级,比如 dto 和 service 层代码混合在一起,即逻辑分散。


除此之外,方法过长还容易带来一些额外的问题。


问题1:过多的注释


方法太长会导致逻辑难以理解,需要大量的注释,如果 10 行代码需要 20 行注释,代码很难阅读。特别是读代码的时候,常常需要记住大量的上下文。


问题2:面向过程


面向过程的问题在于当逻辑复杂以后,代码会很难维护。


相反地,我们在代码开发时常常用面向对象的设计思想,即把事物抽象成具有共同特征的对象。


解决思路


解决方法过长时,我们遵循这样一条原则:每当感觉要写注释来说明代码时,就把这部分代码写进一个独立的方法里,并根据这段代码的意图来命名。



方法命名原则:可以概括要做的事,而非怎么做。



2)过大的类


一个类做了太多的事情,比如一个类的实现既包含商品逻辑,又包含订单逻辑。在创建时就会出现太多的实例变量和方法,难以管理。


除此之外,过大的类还容易带来两个问题。


问题1:冗余重复


当一个类里面包含两个模块的逻辑时,两个模块容易产生依赖。这在代码编写的过程中,很容易发生 “你带着我,我看着你” 的问题。


即在两个模块中,都看到了和另一个模块相关的程序结构或相同意图的方法。


问题2:耦合结构不良


当类的命名不足以描述所做的事情时,大概率产生了耦合结构不良的问题,这和我们想要编写 “高内聚,低耦合” 的代码目标相悖而行了。


解决思路


将大类根据业务逻辑拆分成小类,如果两个类之间有依赖,则通过外键等方式关联。当出现重复代码时,尽量合并提出来,程序会变得更简洁可维护。


3)逻辑分散


逻辑分散是由于代码架构层次或者对象层次上有不合理的依赖,通常会导致两个问题:


发散式变化


某个类经常因为不同的原因,在不同的方向上修改。


散弹式修改


发生某种变化时,需要多个类中修改。


4)其它坏味道


数据泥团


数据泥团是指很多数据项混乱地融合在一起,不易复用和扩展。


当许多数据项总是一起出现,并且一起出现时更容易分类。我们就可以考虑将数据按业务封装成数据对象。反例如下:


func AddUser(age int, gender, firstName, lastName string) {}

重构之后:


type AddUserRequest struct {
   Age int
   Gender string
   FirstName string
   LastName string
}
func AddUser(req AddUserRequest) {}

基本类型偏执


在大多数高级编程语言里面,都有基本类型和结构类型。在 Go 语言里面,基本类型就是 int、string、bool 等。


基本类型偏执是指我们在定义对象的变量时,常常不考虑变量的实际业务含义,直接使用基本类型。


反例如下:


type QueryMessage struct {
Role        int         `json:"role"`
Content  string    `json:"content"`
}

重构之后:


// 定义对话角色类型
type MessageRole int

const (
HUMAN     MessageRole = 0
ASSISTANT MessageRole = 1
)

type QueryMessage struct {
Role        MessageRole   `json:"role"`
Content  string               `json:"content"`
}

这是 ChatGPT 问答时的请求字段,我们可以看到对话角色为 int 类型,且 0 表示人类,1 表示聊天助手。


当直接使用 int 来表示对话 Role 时,没办法直接从定义里知道更多信息。


但是用 type MessageRole int 定义后,我们就可以根据常量值很清晰地看出对话角色分为两种:HUMAN & ASSISTANT.


混乱的代码层次调用


我们一般的系统都会根据业务 service、中转控制 controller 和数据库访问 dao 等进行分层。一般 controller 调用 service,service 调用 dao。


如果我们在 controller 直接调用 dao,或者 dao 调用 controller,就会出现层次混乱的问题,就可以进行优化了。


5)坏味道带来的问题


妍妍:舅舅,这些坏味道都需要解决吗,你说的这些坏味道代码会带来什么样的影响呢?


❤:是的,代码里如果坏味道代码太多,会带来四个 “难以”



  • 难以理解:新来的开发同学压根看不懂看人的代码,一个模块看了两个周还不知道啥意思。或许不是开发者的水平不够,可能是代码写的太一言难尽。



  • 难以复用:要么是读都读不懂,或者勉强读懂了却不敢用,担心有什么暗坑。或者系统耦合性严重,难以分离可重用部分。



  • 难以变化:牵一发而动全身,即散弹式修改。动了一处代码,整个模块都快没了。




  • 难以测试:改了不好测,难以进行功能验证。命名杂乱,结构混乱,在测试时可能测出新的问题。




3. 重构技巧


露露:哦,原来是这样啊,那我们可以去除它们吗?


❤:当然可以了!就像你们爱收拾房间一样,每一个有责任心(代码洁癖)的程序员,都会考虑代码重构。


而对于重构问题,业界已经有比较好的思路:通过持续不断地重构将代码中的 "坏味道" 清除掉。


1)命名规范


一个好的命名规范应该符合:



  • 精准描述所做的事情

  • 格式符合通用惯例


约定俗成的惯例


我们拿华为公司内部的 Go 语言的开发规范来举例:


场景约束示例
项目名全部小写,多个单词时用中划线 '-' 分隔user-order
包名全部小写,多个单词时用中划线 '-' 分隔config-sit
结构体名首字母大写Student
接口采用 Restful API 的命名方式,路径最后一部分是资源名词如 [get] api/v1/student
常量名首字母大写,驼峰命名CacheExpiredTime
变量名首字母小写,驼峰命名userName,password

2)重构手法


妍妍:哇,这么多成熟的规范可以用啊!那除了规范,我们还需要注意什么吗?


❤:好问题妍妍!接下来我还会介绍一些常见的重构手法:




  • 提取函数:将一个长长的函数分成小块,更容易理解和复用。




  • 改名字:给变量、函数、类等改个名字,更有意义。




  • 消除冗余:找到相似的代码块,合并它们,减少重复。




  • 搬家:把函数或字段移到更合适的地方,让代码更井然有序。




  • 抽象通用类:把通用功能抽出来,变成一个类,增加代码的可重用性。




  • 引入参数对象:当变量过多时,传入对象,消除数据泥团。




  • 使用卫语句:减少 else 的使用,让代码结构更加清晰。




4. 小结


露露:舅舅,你讲得太有趣了,我感觉我也会重构了!


❤:露露真棒,我相信你!重构的思想无处不在,就像生活中都应该留白一样,你们的人生也会非常精彩的。在编程里,重构可以让代码更美观、更容易读懂,提高开发效率,是程序员都应该掌握的技能。


妍妍:我也会了,我也会了!以后我也要写代码,做代码重构,我还要给舅舅的文章点赞。



❤:哈哈哈,好哒,你们都很棒!就像你们喜欢打扫卫生,爱好画画读诗一样,如果以后你们想写代码,它们也会十分的干净整洁,充满诗情画意。



最后,如果你觉得有所收获,别忘了点赞和在看,让更多的人了解重构的神奇之处,一起进步,一起写出更好的代码!


希望这篇文章对你有所帮助,也希望你能在编程的路上越走越远。感谢大家的支持,我们下次再见!🚀✨


最后


妍妍说:看完的你还不赶紧分享、点赞、加入在看吗?



作者:xin猿意码
来源:juejin.cn/post/7277836718760771636
收起阅读 »

前端监控究竟有多重要?

web
为什么要有前端监控? 一个很现实的原因是bug是不可能被全部测试出来的,由于成本和上线档期的考虑,测试无法做到“面面俱到”,即使时间充裕也总会有这样或那样的bug埋藏在某个角落。 所以一个可靠的前端监控系统可以帮助我们化被动为主动,不再被动的等待客服来找,而是...
继续阅读 »

为什么要有前端监控?


一个很现实的原因是bug是不可能被全部测试出来的,由于成本和上线档期的考虑,测试无法做到“面面俱到”,即使时间充裕也总会有这样或那样的bug埋藏在某个角落。


所以一个可靠的前端监控系统可以帮助我们化被动为主动,不再被动的等待客服来找,而是在问题出现时开发人员可以第一时间知道并解决。并且我们还可以通过监控系统获取用户行为以及跟踪产品在用户端的使用情况,并以监控数据为基础,指明产品优化的方向。


常见的前端监控


前端监控系统大体可以分为四部分



  • 异常监控

  • 用户数据监控

  • 性能监控

  • 异常报警


用户数据监控



数据监控,就是监听用户的行为,可以帮助我们评估和改进用户在使用网站时的体验:




  • PV:PV(page view):即用户访问特定页面的次数,也可以说是页面的浏览量或点击量,

  • UV:访问网站的不同个体或设备数量,而不是页面访问次数

  • 新独立访客:当日的独立访客中,历史上首次访问网站的访客为新独立访客。

  • 跳出次数:跳出指仅浏览了1个页面就离开网站的访问(会话)行为。跳出次数越多则访客对网站兴趣越低或站内入口质量越差。

  • 来访次数:由该来源进入网站的访问(会话)次数。

  • 用户在每一个页面的停留时间

  • 用户通过什么入口来访问该网页

  • 用户在相应的页面中触发的行为

  • 网站的转化率

  • 导航路径分析


统计这些数据是有意义的,我们可以清晰展示前端性能的表现,并依据这些监控结果来进一步优化前端性能。例如,我们可以改善动画效果以在低版本浏览器上兼容,或者采取措施加快首屏加载时间等。这些优化措施不仅可以提高转化率,因为快速加载的网站通常具有更高的转化率,还可以确保我们的网站在多种设备和浏览器上都表现一致,以满足不同用户的需求。最终达到,改善用户体验,提供更快的页面加载时间和更高的性能,增强用户满意度,降低跳出率的目的。


性能监控



性能监控是一种用于追踪和评估网站和性能的方法。它专注于用户在浏览器中与网站互时的性能体验




  • 首次绘制(FP): 全称 First Paint,标记浏览器渲染任何在视觉上不同于导航前屏幕内容之内容的时间点

  • 首次内容绘制(FCP):全称 First Contentful Paint,标记的是浏览器渲染来自 DOM 第一位内容的时间点,该内容可能是文本、图像、SVG 甚至 <canvas> 元素。

  • 首次有效绘制(FMP):全称 First Meaningful Paint,标记的是页面主要内容绘制的时间点,例如视频应用的视频组件、天气应用的天气信息、新闻应用中的新闻条目。

  • 最大内容绘制(LCP):全称 Largest Contentful Paint,标记在可视区“内容”最大的可见元素开始绘制在屏幕上的时间点。

  • 白屏时间

  • http 等请求的响应时间

  • 静态资源整体下载时间

  • 页面渲染时间

  • 页面交互动画完成时间


异常监控



由于产品的前端代码在客户端的执行过程中也会发生异常,因此需要引入异常监控。及时的上报异常情况,这样可以避免线上故障的发生。虽然大部分异常可以通过 try catch 的方式捕获,但是比如内存泄漏以及其他偶现的异常难以捕获。



常见的需要监控的异常包括:



  • Javascript 的异常监控:捕获并报告JavaScript代码中的错误,如未定义的变量、空指针引用、语法错误等

  • 数据请求异常监控:监控Ajax请求和其他网络请求,以便识别网络问题、服务器错误和超时等。

  • 资源加载错误:捕获CSS、JavaScript、图像和其他资源加载失败的情况,以减少页面加载问题。

  • 跨域问题:识别跨域请求导致的问题,如CORS(跨源资源共享)错误。

  • 用户界面问题:监控用户界面交互时的错误,如用户界面组件的不正常行为或交互问题


通过捕获和报告异常,开发团队可以快速响应问题,提供更好的用户体验,减少客户端问题对业务的不利影响


异常报警



前端异常报警是指在网站中检测和捕获异常、错误以及问题,并通过各种通知方式通知开发人员或团队,以便他们能够快速诊断、分析和解决问题。



常见的异常报警方式




  • 邮件通知:通过邮件将异常信息发送给相关人员,通常用于低优先级的问题。




  • 短信或电话通知:通过短信或电话自动通知相关人员,通常用于紧急问题或需要立即处理的问题。




  • 即时消息:使用即时通讯工具如企业微信 飞书或钉钉发送异常通知,以便团队及时协作。




  • 日志和事件记录:将异常信息记录到中央日志,或者监控中台系统,以供后续分析和审计。




报警级别和策略:


异常报警通常有不同的级别和策略,根据问题的紧急性和重要性来确定通知的方式和频率。例如,可以定义以下报警级别:




  • 紧急报警:用于严重的问题,需要立即处理,通常通过短信或电话通知。




  • 警告报警:用于中等级别的问题,需要在短时间内处理,可以通过即时消息或邮件通知。




  • 信息报警:用于一般信息和低优先级问题,通过邮件或即时消息通知。




  • 静默报警:用于临时性问题或不需要立即处理的问题,可以记录到日志而不发送通知。




异常报警是确保系统稳定性和可用性的重要机制。它能够帮助组织及时发现和解决问题,减少停机时间,提高系统的可靠性和性能,从而支持业务运营。异常报警有助于快速识别和响应问题,减少停机时间,提高系统的可用性和性能


介绍完了前端监控的四大部分,现在就来聊聊前端监控常见的几种监控方式。


SDK设计(埋点方案)


前端埋点是一种用于收集和监控网站数据的常见方法


image.png


手动埋点:


手动埋点也称为代码埋点,是通过手动在代码中插入埋点代码(SDK 的函数)的方式来实现数据收集。像腾讯分析(Tencent Analytics)、百度统计(Baidu Tongji)、诸葛IO(ZhugeIO)等第三方数据统计服务商大都采用这种方案,这种方法的优点是:



  • 灵活:开发人员可以根据需要自定义属性和事件,以捕获特定的用户行为和数据。

  • 精确:可以精确控制埋点位置,以确保收集到关键数据。


然而,手动埋点的缺点包括:



  • 工作量大:需要在代码中多次插入埋点代码,工程量较大。

  • 沟通成本高:需要开发、产品和运营之间的频繁沟通,容易导致误差和延迟。

  • 更新迭代成本高:每次有埋点更新或漏埋点都需要重新发布应用程序,成本较高。


可视化埋点:


可视化埋点通过提供可视化界面,允许用户在不编写代码的情况下进行添加埋点。这种方法的优点是:



  • 简单方便:非技术人员也可以使用可视化工具添加埋点,减少了对技术团队的依赖。

  • 实时更新:可以实时更新埋点配置,无需重新上传网站。


然而,可视化埋点的缺点包括:



  • 可定制性受限:可视化工具通常只支持有限的埋点事件和属性,无法满足所有需求。

  • 对控件有限制:可视化埋点通常只适用于特定的UI控件和事件类型。


无埋点:


无埋点是一种自动收集所有用户行为和事件的方法,然后通过后端过滤和分析以提取有用的数据。这种方法的优点是:



  • 全自动:无需手动埋点,数据自动收集,降低了工程量,而且不会出现漏埋和误埋等现象。

  • 全面性:捕获了所有用户行为,提供了完整的数据集。


然而,无埋点的缺点包括:



  • 数据量大:数据量庞大,需要后端过滤和处理,可能增加服务器性能压力。

  • 数据处理复杂:需要处理大量原始数据,提取有用的信息可能需要复杂的算法和逻辑。


作者:zayyo
来源:juejin.cn/post/7280430881964638262
收起阅读 »

闲来无事,拜拜电子财神

web
最近在刷抖音的时候,经常能刷到类似下面这种手机桌面,通过手机小组件功能,搭了一个电子供台。。。    由于最近闲来无事儿,就在想可不可以制作一个类似的网页,功能点有以下这些: 1.类似手机小组件一样的布局 2.点击木鱼一次,可以显示功德加一并且带音效 3.随着...
继续阅读 »

最近在刷抖音的时候,经常能刷到类似下面这种手机桌面,通过手机小组件功能,搭了一个电子供台。。。


  


由于最近闲来无事儿,就在想可不可以制作一个类似的网页,功能点有以下这些:


1.类似手机小组件一样的布局


2.点击木鱼一次,可以显示功德加一并且带音效


3.随着功德点击,香炉上方会有烟雾飘散的效果


4.统计不同省份的功德数据


5.心愿墙功能,


于是说干就干,就开始了开发工作;


经过了 2 个下午的忙碌,完成了前三个功能,有了大概的雏形,就是下面这个样子



开发的过程中也遇到了一些问题


1.在手机上连续点击木鱼时,会导致网页放大


在网上找了一些解决办法,设置 meta 属性


无效,在 ios 的浏览器上没有效果


这个方法类似于写个节流函数,不过这样做就没有连续敲击木鱼的快感了,所以也不行。


最后让我找到了一个插件 fastClick.js,完美解决了问题。只要正常引入,然后加入以下代码即可。


if ("addEventListener" in document) {            document.addEventListener(                "DOMContentLoaded",                function () {                    FastClick.attach(document.body);                },                false            );        }

2.播放木鱼音效延迟问题


通过document.createElement('audio')方式创建 audio 组件,代码如下


var audio = document.createElement('audio') //生成一个audio元素
audio.controls = true //这样控件才能显示出来
audio.src = 'xxxxx' //音乐的路径
document.body.appendChild(audio) //把它添加到页面中
audio.play()

声音是能播放出来了,但是延迟很高,点一下木鱼,过几秒钟后才有音效,所以这个方式 pass 了。还有说可以通过AudioContext API 来播放音效,但是看了一下,感觉写起来有些复杂,也 pass 掉了,最后也是找到了一款合适的插件解决了这个问题。



使用方式也是异常简单


var sound = new Howl({
src: ['sound.mp3']
});

sound.play();

由于有个功能是敲击木鱼后,页面香炉的位置会生成烟雾,自己不太会写,于是又找到了可以一个模拟烟雾的插件,可以在页面任意位置生成烟雾动画


使用时先创建一个 canvas 标签


<canvas id="smoke"></canvas>

然后初始化


let canvas = document.getElementById("smoke");let ctx = canvas.getContext("2d");canvas.width = window.innerWidth;canvas.height = window.innerHeight;party = SmokeMachine(ctx, [230, 230, 230]); // 数组里是颜色 rgb 值

点击木鱼一次,创建一次播放动画


party.start();party.addSmoke(    window.innerWidth / 2,    //烟雾生成的位置,x    window.innerHeight * 0.4, //烟雾生成的位置,y    10 //烟雾大小);

至此烟雾效果就完美实现了。


体验url:财神爷.我爱你


没错,是纯中文域名,中国的神仙就要用中文域名。


未完待续......


作者:yibeicha
来源:juejin.cn/post/7280435142245285946
收起阅读 »

前端又出新框架了,你还学得动吗?

web
最近前端又出来一个新框架/库,名为nue.js。一周前的9.13号提交了第一个commit,到今天已超过2000个star。 翻译一下: Nue 是一个强大的 React、Vue、Next.js、Vite 和 Astro 替代品。它可能会改变您的web开发...
继续阅读 »

最近前端又出来一个新框架/库,名为nue.js。一周前的9.13号提交了第一个commit,到今天已超过2000个star。


官网首页截图


翻译一下:



Nue 是一个强大的 React、Vue、Next.js、Vite 和 Astro 替代品。它可能会改变您的web开发方式。



What is Nue JS?


Nue JS 是一个非常小的(压缩后 2.3kb)JavaScript 库,用于构建 Web 界面。 它是即将推出的 Nue 生态系统的核心。 它就像 Vue.js、React.js 或 Svelte,但没有hooks, effects, props, portals, watchers, provides, injects, suspension 这些抽象概念。了解 HTML、CSS 和 JavaScript 的基础知识,就可以开始了。


用更少的代码构建用户界面


它表示,Nue 最大的好处是你需要更少的代码来完成同样的事情:


同样一个listBox组件,react需要2537行,vue需要1913行,svelte需要1286行,Nue只需要208行,比react小10倍。





仅仅是HTML


Nue 使用基于 HTML 的模板语法:


<div @name="media-object" class="{ type }">
<img src="{ img }">
<aside>
<h3>{ title }</h3>
<p :if="desc">{ desc }</p>
<slot/>
</aside>
</div>

React 和 JSX 声称是“Just JavaScript”,但 Nue 可以被认为是“Just HTML”


按比例构建


Nue 具有出色扩展性的三个原因:



  1. 关注点分离,易于理解的代码比“意大利面条代码”更容易扩展

  2. 极简主义,一百行代码比一千行代码更容易扩展

  3. 人才分离,当 UX 开发人员专注于前端,而 JS/TS 开发人员专注于前端后端时,团队技能就会达到最佳平衡:



解耦样式


Nue不提倡使用 Scoped CSS、样式属性、Tailwind 或其他 CSS-in-JS 体操:



  1. 更多可重用代码:当样式未硬编码到组件时,同一组件可能会根据页面或上下文而看起来有所不同。

  2. 没有意大利面条式代码:纯 HTML 或纯 CSS 比混合意大利面条式代码更容易阅读

  3. 更快的页面加载:通过解耦样式,可以更轻松地从辅助 CSS 中提取主 CSS,并将 HTML 页面保持在关键的14kb 限制以下。


反应式和同构


Nue拥有丰富的组件模型,它允许您使用不同类型的组件创建各种应用程序:



  1. 服务器组件在服务器上呈现。它们可以帮助您构建以内容为中心的网站,无需 JavaScript 即可加载速度更快,并且可以被搜索引擎抓取。

  2. 反应式组件在客户端上呈现。它们帮助您构建动态岛或单页应用程序。

  3. 混合组件部分在服务器端呈现,部分在客户端呈现。这些组件可帮助您构建响应式、SEO 友好的组件,例如视频标签或图片库。

  4. 通用组件在服务器端和客户端上使用相同的方式。


UI库文件


Nue允许您在单个文件上定义多个组件。这是将相关组件组合在一起并简化依赖关系管理的好方法。


<!-- shared variables and methods -->
<script>
import { someMethod } from './util.js'
</script>

<!-- first component -->
<article @name="todo">
...
</article>

<!-- second component -->
<div @name="todo-item">
...
</div>

<!-- third component -->
<time @name="cute-date">
...
</time>

使用库文件,您的文件系统层次结构看起来更干净,并且您需要更少的样板代码将连接的部分连接在一起。他们帮助为其他人打包库。


更简单的工具


Nue JS带有一个简单的render服务器端渲染功能和一个compile为浏览器生成组件的功能。不需要 WebpackVite 等复杂的捆绑程序来控制您的开发环境。只需将 Nue 导入到项目中即可。


如果应用程序因大量依赖项而变得更加复杂,可以在业务模型上使用打包器。Bunesbuild是很棒的高性能选择。


用例


Nue JS是一款多功能工具,支持服务器端和客户端渲染,可帮助您构建以内容为中心的网站和反应式单页应用程序。



  1. UI 库开发:为反应式前端或服务器生成的内容创建可重用组件。

  2. 渐进式增强:Nue JS 是一个完美的微型库,可通过动态组件或“岛”增强以内容为中心的网站

  3. 静态网站生成器:只需将其导入您的项目即可准备渲染。不需要捆绑器。

  4. 单页应用程序:与即将推出的Nue MVC项目一起构建更简单、更具可扩展性的应用程序。

  5. Template Nue:是一个用于生成网站和 HTML 电子邮件的通用工具。


本文参考资料



作者:xintianyou
来源:juejin.cn/post/7280747833371705405
收起阅读 »

iOS小技能:Xcode13的使用技巧

iOS
引言 Xcode13新建项目不显示Products目录的解决方案Xcode13新建的工程恢复从前的Info.plist同步机制的方法自动管理签名证书时拉取更新设备描述文件的方法。 I 显示Products目录的解决方案 问题:Xcode13 新建的项目不显示P...
继续阅读 »

引言


  1. Xcode13新建项目不显示Products目录的解决方案
  2. Xcode13新建的工程恢复从前的Info.plist同步机制的方法
  3. 自动管理签名证书时拉取更新设备描述文件的方法。

I 显示Products目录的解决方案


问题:Xcode13 新建的项目不显示Products目录


解决方式: 修改project.pbxproj 文件的productRefGroup配置信息


效果:

应用场景:Products目录的app包用于快速打测试包。


1.1 从Xcodeeproj 打开project.pbxproj



1.2 修改productRefGroup 的值


将mainGroup 对应的值复制给productRefGroup 的值,按command+s保存project.pbxproj文件,Xcode将自动刷新,Products目录显示出来了。



1.3 应用场景


通过Products目录快速定位获取真机调试包路径,使用脚本快速打包。


打包脚本核心逻辑:在含有真机包路径下拷贝.app 到新建的Payload目录,zip压缩Payload目录并根据当前时间来命名为xxx.ipa。

#!/bin/bash
echo "==================(create ipa file...)=================="
# cd `dirname $0`;
rm -rf ./Target.ipa;
rm -rf ./Payload;
mkdir Payload;
APP=$(find . -type d | grep ".app$" | head -n 1)
cp -rf "$APP" ./Payload;
data="`date +%F-%T-%N`"
postName="$data"-".ipa"
zip -r -q "$postName" ./Payload;
rm -rf ./Payload;
open .
# 移动ipa包到特定目录
mkdir -p ~/Downloads/knPayload
cp -a "$postName" ~/Downloads/knPayload
open ~/Downloads/knPayload
echo "==================(done)=================="
exit;






II 关闭打包合并Info.plist功能


Xcode13之前Custom iOS Target Properties面板和Info.plist的配置信息会自动同步。


Xcode13新建的工程默认开启打包合并Info.plist功能,不再使用配置文件(Info.plist、entitlements),如果需要修改配置,直接在Xcode面板target - Info - Custom iOS Target Propertiesbuild settings中设置。




Projects created from several templates no longer require configuration files such as entitlements and Info.plist files. Configure common fields in the target’s Info tab, and build settings in the project editor.



2.1 设置Info.plist为主配置文件


由于GUI配置面板没有配置文件plist的灵活,不支持查看源代码。所以我们可以在BuildSetting Generate Info.plist File设置为NO,来关闭打包合并功能。



关闭打包合并功能,重启Xcode使配置生效,Custom iOS Target Properties面板的信息以info.plist的内容为准。



每次修改info.plist都要重启Xcode,info.plist的信息才会同步到Custom iOS Target Properties面板。 



2.2 注意事项


注意: 关闭打包合并Info.plist功能 之前记得先手动同步Custom iOS Target Properties面板的信息到Info.plist

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>iOS逆向</string>
<key>CFBundleIdentifier</key>
<string>blog.csdn.net.z929118967</string>
<key>CFBundleName</key>
<string>YourAppName</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchScreen</key>
<dict>
<key>UILaunchScreen</key>
<dict/>
</dict>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~iphone</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>


III 自动管理签名证书时如何拉取最新设备描述文件?


方法:根据描述文件的创建时间来删除旧的自动管理证书的描述文件



 



原理:在~/Library/MobileDevice/Provisioning\ Profiles 文件夹中删除之前的描述文件,然后系统检测到没有描述文件则会自动生成一个新的


see also


iOS第三方库管理规范:以Cocoapods为案例



kunnan.blog.csdn.net/article/det…



iOS接入腾讯优量汇开屏广告教程



kunnan.blog.csdn.net/article/det…


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

开发没切图怎么办?矢量图标(iconFont)上手指南

iOS
需求: 有时候我们自己想独立开发一些App,但苦恼没有设计给icon切图? 这可怎么办? 今天我们来介绍一种比较高效且高质量的替代方案:使用矢量图标 —— iconFont。 一、iconFont简介 iconFont:是阿里巴巴提供的一个矢量图标库。简单...
继续阅读 »

需求:

有时候我们自己想独立开发一些App,但苦恼没有设计给icon切图?

这可怎么办?

今天我们来介绍一种比较高效且高质量的替代方案:使用矢量图标 —— iconFont



一、iconFont简介



iconFont:是阿里巴巴提供的一个矢量图标库。简单来说,就是可以把icon转换成font,再通过文本展示出来。官网链接

支持:WebiOSAndroid平台使用。



二、iOS端简单使用指南


第一步:


登录iconFont,挑选你需要的icon,并把它们加入购物车,下载代码。

  • 挑选统一风格的icon

    • 全局搜索想要的icon

    • 将需要使用的icon加入到购物车

    • 下载代码




第二步:


解压下载的压缩包,注意demo_index.htmliconFont.ttf文件。打开工程将ttf导入到项目中,并在info.plist中配置。


  • 压缩文件,找到demo_index.htmliconFont.ttf



  • iconFont.ttf文件导入项目:



第三步:


打开demo_index.html预览iconFont所对应的Unicode编码。并在项目中应用。


  • 打开demo_index.html文件


  • swift使用方法如下,用格式\u{编码}使用Unicode编码
//...
label.font = UIFont.init(name: "iconFont", size: 26.0)
label.text = "\u{e658}"
//...

  • Objective-C使用方法如下,用格式\U0000编码使用Unicode编码
//...
label.font = [UIFont fontWithName:@"uxIconFont" size: 34];;
label.text = @"\U0000e658";
//...

这样,在没有设计提供切图的情况下,就可以用LabeliconFont字体代替切图达成ImageView的效果了。


三、iconFont原理


先把icon通过像素点描述成自定义字体(svg格式字体),然后打包成ttf格式的文件,再通过对应的unicode对应到相关的icon


四、可能遇到的一些问题


  • ttf文件导入冲突问题:

由于从iconFont上打包生成的ttf文件,字体名均为“iconFont”,因此从官网上下载的ttf文件,字体名均为“iconFont”。因此多ttf文件引入时,会有冲突。


解决方案:用一些工具修改字体名,再导入多个ttf文件。(记得在info.plist文件里配置)


  • Unicode变化问题:

尽量使用一个账号下载ttf资源,不同的环境下可能会导致生成的Unicode不同。从而给项目替换icon带来成本。


  • 版权问题:

iconFont目前应该不支持商用,除非有特别的许可。
自己独立写一些小项目的时候可以使用。


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

全方位对比 Postgres 和 MySQL (2023 版)

根据 2023 年 Stack Overflow 调研,Postgres 已经取代 MySQL 成为最受敬仰和渴望的数据库。 随着 Postgres 的发展势头愈发强劲,在 Postgres 和 MySQL 之间做选择变得更难了。 如果看安装数量,MySQL...
继续阅读 »

根据 2023 年 Stack Overflow 调研,Postgres 已经取代 MySQL 成为最受敬仰和渴望的数据库。




随着 Postgres 的发展势头愈发强劲,在 Postgres 和 MySQL 之间做选择变得更难了。


如果看安装数量,MySQL 可能仍是全球最大的开源数据库。




Postgres 则自诩为全球最先进的开源关系型数据库。




因为需要与各种数据库及其衍生产品集成,Bytebase 和各种数据库密切合作,而托管 MySQL 和 Postgres 最大的云服务之一 Google Cloud SQL 也是 Bytebase 创始人的杰作之一。


我们对 Postgres 和 MySQL 在以下几个维度进行了比较:


  • 许可证 License
  • 性能 Performance
  • 功能 Features
  • 可扩展性 Extensibility
  • 易用性 Usability
  • 连接模型 Connection Model
  • 生态 Ecosystem
  • 可运维性 Operability



除非另有说明,下文基于最新的主要版本 Postgres 15 和 MySQL 8.0 (使用 InnoDB)。在文章中,我们使用 Postgres 而不是 PostgreSQL,尽管 PostgreSQL 才是官方名称,但被认为是一个错误的决定




许可证 License


  • MySQL 社区版采用 GPL 许可证。
  • Postgres 发布在 PostgreSQL 许可下,是一种类似于 BSD 或 MIT 的自由开源许可。

即便 MySQL 采用了 GPL,仍有人担心 MySQL 归 Oracle 所有,这也是为什么 MariaDB 从 MySQL 分叉出来。


性能 Performance


对于大多数工作负载来说,Postgres 和 MySQL 的性能相当,最多只有 30% 的差异。无论选择哪个数据库,如果查询缺少索引,则可能导致 x10 ~ x1000 的降级。
话虽如此,在极端的写入密集型工作负载方面,MySQL 确实比 Postgres 更具优势。可以参考下文了解更多:



除非你的业务达到了 Uber 的规模,否则纯粹的数据库性能不是决定因素。像 Instagram, Notion 这样的公司也能够在超大规模下使用 Postgres。


功能 Features


对象层次结构


MySQL 采用了 4 级结构:


  1. 实例
  2. 数据库

Postgres 采用了 5 级结构:


  • 实例(也称为集群)
  • 数据库
  • 模式 Schema

ACID 事务


两个数据库都支持 ACID 事务,Postgres 提供更强大的事务支持。




安全性


Postgres 和 MySQL 都支持 RBAC。


Postgres 支持开箱即用的附加行级安全 (RLS),而 MySQL 需要创建额外的视图来模拟此行为。


查询优化器


Postgres 的查询优化器更优秀,详情参考此吐槽


复制


Postgres 的标准复制使用 WAL 进行物理复制。MySQL 的标准复制使用 binlog 进行逻辑复制。


Postgres 也支持通过其发布/订阅模式进行逻辑复制。


JSON


Postgres 和 MySQL 都支持 JSON。 Postgres 支持的功能更多:


  • 更多操作符来访问 JSON 功能。
  • 允许在 JSON 字段上创建索引。

CTE (Common Table Expression)


Postgres 对 CTE 的支持更全面:


  • 在 CTE 内进行 SELECT, UPDATE, INSERT, DELETE 操作
  • 在 CTE 之后进行 SELECT, UPDATE, INSERT, DELETE 操作

MySQL 支持:


  • 在 CTE 内进行 SELECT 操作
  • 在 CTE 之后进行 SELECT, UPDATE, DELETE 操作

窗口函数 (Window Functions)


窗口帧类型:MySQL 仅支持 Row Frame 类型,允许定义由固定数量行组成的帧;而 Postgres 同时支持 Row Frame 和范围帧类型。


范围单位:MySQL 仅支持 UNBOUNDED PRECEDING 和 CURRENT ROW 这两种范围单位;而 Postgres 支持更多范围单位,包括 UNBOUNDED FOLLOWING 和 BETWEEN 等。


性能:一般来说,Postgres 实现的 Window Functions 比 MySQL 实现更高效且性能更好。


高级函数:Postgres 还支持更多高级 Window Functions,例如 LAG(), LEAD(), FIRST_VALUE(), and LAST_VALUE()。


可扩展性 Extensibility


Postgres 支持多种扩展。最出色的是 PostGIS,它为 Postgres 带来了地理空间能力。此外,还有 Foreign Data Wrapper (FDW),支持查询其他数据系统,pg_stat_statements 用于跟踪规划和执行统计信息,pgvector 用于进行 AI 应用的向量搜索。


MySQL 具有可插拔的存储引擎架构,并诞生了 InnoDB。但如今,在 MySQL 中,InnoDB 已成为主导存储引擎,因此可插拔架构只作为 API 边界使用,而不是用于扩展目的。


在认证方面,Postgres 和 MySQL 都支持可插拔认证模块 (PAM)。


易用性 Usability


Postgres 更加严格,而 MySQL 更加宽容:


  • MySQL 允许在使用 GROUP BY 子句的 SELECT 语句中包含非聚合列;而 Postgres 则不允许。
  • MySQL 默认情况下是大小写不敏感的;而 Postgres 默认情况下是大小写敏感的。
  • MySQL 允许 JOIN 来自不同数据库的表;而 Postgres 只能连接单个数据库内部的表,除非使用 FDW 扩展。

连接模型 Connection Model


Postgres 采用在每个连接上生成一个新进程的方式工作。而 MySQL 则在每个连接上生成一个新线程。因此,Postgres 提供了更好的隔离性,例如,一个无效的内存访问错误只会导致单个进程崩溃,而不是整个数据库服务器。另一方面,进程模型消耗更多资源。因此,在部署 Postgres 时建议通过连接池(如 PgBouncer 或 pgcat)代理连接。


生态 Ecosystem


常见的 SQL 工具都能很好地支持 Postgres 和 MySQL。由于 Postgres 的可扩展架构,并且仍被社区拥有,近年来 Postgres 生态系统更加繁荣。对于提供托管数据库服务的应用平台,每个都选择了 Postgres。从早期的 Heroku 到更新的 Supabase, render 和 Fly.io。


可运维性 Operability


由于底层存储引擎设计问题,在高负载下,Postgres 存在臭名昭著的 XID wraparound 问题。


对于 MySQL,在 Google Cloud 运营大规模 MySQL 集群时,我们遇到过一些复制错误。


这些问题只会在极端负载下发生。对于正常工作负载而言,无论是 Postgres 还是 MySQL 都是成熟且可靠的。数据库托管平台也提供集成备份/恢复和监控功能。


Postgres 还是 MySQL


2023 年了,在 Postgres 和 MySQL 之间做选择仍然很困难,并且经常引起激烈讨论




总的来说,Postgres 有更多功能、更繁荣的社区和生态;而 MySQL 则更易学习并且拥有庞大的用户群体。
我们观察到与 Stack Overflow 结果相同的行业趋势,即 Postgres 在开发者中变得越来越受欢迎。但根据我们的实际体验,精密的 Postgres 牺牲了一些便利性。如果你对 Postgres 不太熟悉,最好从云服务提供商那里启动一个实例,并运行几个查询来上手。有时候,这些额外好处可能并不值得,选择 MySQL 会更容易一些。


同时,在一个组织内部共存 Postgres 和 MySQL 也是很常见的情况。如果需要同时管理 Postgres 和 MySQL 的开发生命周期,可以来了解一下 Bytebase。






💡 你可以访问官网,免费注册云账号,立即体验 Bytebase。


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

少一点功利主义,多一点傻逼似的坚持

感谢你观看本文,希望在未来的时光中,我们都能找到真正的自己,做真正的自己 坚持只需要一个理由,而放弃则有无数个接口,坚持很难,而放弃就是一刹那的时间,作为普通人的我们,其实只要能坚持做一件事,那么其实是很了不起的,可能它暂时不能给你带来经济价值,但是经过时间的...
继续阅读 »

感谢你观看本文,希望在未来的时光中,我们都能找到真正的自己,做真正的自己


坚持只需要一个理由,而放弃则有无数个接口,坚持很难,而放弃就是一刹那的时间,作为普通人的我们,其实只要能坚持做一件事,那么其实是很了不起的,可能它暂时不能给你带来经济价值,但是经过时间的酝酿,它会迸发处惊人的力量!


不过有一关是很难过的,这一关基本上可以刷掉百分之九十五的人,那就是否有长期主义,是否能够忍受“没有回报”,因为人的本性就是贪婪,而我们从小受到的教育就是“付出就有收获”,所以我们在做每一件事的时候,心里第一反应是我做这件事能给我带来多少收获。


比如读一本书,其实很多时候我们都是带有目的性的,比如觉得事业不顺,人生失意,或者想赚快钱,那么这时候就会去快速翻阅一些诸如《快速致富》的书籍,然后加满鸡血后,第二天依旧是十二点起,起来又卷入精神内耗中,反反复复,最终宝贵是时光!


又比如你看到别人赚到了钱,于是眼睛一红,就问他怎么赚的,别人稍微指点后,你就暗下决心要搞钱,前几天到几个月期间赚了几块钱,你就失落了,你在想,这条路子行不通,于是就放弃了,又去折腾其它的了。


上述的例子是百分之九十的人的真实写照,那么我觉得可以总结为两点:


1.只要没有得到应有的回报,就觉得是损失


2.极强的功利主义


首先对于这一点,我觉得是我们最容易犯的错,比如当一个人说你去坚持做这件事情,一个月会有一千的附加收入,你去做了,而实际上只拿到了50元的收入,这时候你就会极度的不平衡,感到愤怒,你会觉得花了这么多时间才得到50元,老子不干了,实际上你在这个过程中学到的东西远比1000块多,不过你不会觉得,这时候你宁愿去刷短视频,追剧,你也不会去做这件事了。


所以当你心中满是“付出多少就应该得到多少回报”的时候,你不可能做好事,也不会得到更好的回报,因为你心中总是在想“会不会0回报”,“这玩意究竟靠谱不靠谱”,克服这种心态是一件十分难的事情!


第二点,我觉得我们应该少一点功利主义,多一点傻逼似的坚持,这说得有点理想主义了,人本质就是贪婪的,如果赚不到钱,我就不做,对我没好处,我也不会做,我有写文章的习惯其实从大学就开始了,以前没发公众号,之前朋友经常说我,你写的有什么卵用?能赚钱吗?有人看吗?


一开始我还会在乎,在问自己,你干嘛写这些,因为写个人的感悟和生活这种文章确实会有一定的心里压力,朋友说:”你自己都是这个鸟样,有什么资格去给别人说教“,不过随着时间的推移,我不再去在乎这些了。


就单拿写文章这件事来说,虽然没赚到钱,不过在这个过程中,我逐渐不再浮躁,能静下心来写,也结实了朋友,这是一种对自己的总结,对技术的总结,也是一种锻炼,虽然现在文笔依然很差,不过我依然会像一个傻逼一样去坚持。


时间是最奇妙的东西,你的一些坚持一定会在相应的时间点迸发处惊人的力量!


回头想一下,你没写文章,没看书,没学习,没出去看世界,而是拿着个手机躺在床上刷短视频,像个清朝抽鸦片的人一样,那么你又收获了多少呢?


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

Vision pro,当一切“眼见为实”

iOS
关于 Vision pro,留存一点感想,或许十年后再来回顾。缺点肯定不少,但是这个产品带来了很有趣的新维度 WWDC直播时,最大的疑问是眼动追踪交互足够准确吗?能即时反馈吗?看过各位媒体的文字或口述体验之后,才知道苹果竟然将这种交互方式做得像来自未来一样,...
继续阅读 »

关于 Vision pro,留存一点感想,或许十年后再来回顾。缺点肯定不少,但是这个产品带来了很有趣的新维度




WWDC直播时,最大的疑问是眼动追踪交互足够准确吗?能即时反馈吗?看过各位媒体的文字或口述体验之后,才知道苹果竟然将这种交互方式做得像来自未来一样,通过你的目光,精准得可以定位到一个小小的字母,随时随地随心而动,简直不可思议。


直播中所展示的三维交互效果,让我想起人类对信息的记录和显示方式。文字、图画刻在石头上,刻在竹简上,纸墨印刷成书册;照片、视频,呈现在手机或者电脑的二维屏幕上。而三维信息,可能从未被如此精确真实地呈现在我们的面前。诚然 3D 电影和许多别的 VR、AR 生产厂商也做了诸多努力和探索,但是那些效果都还不足以 “以假乱真”。从现在开始,消费级的信息记录方式,或许又能上升一个维度。


而直播中最大的震撼其实是迪士尼宣传片的一连串 What if...What if all the things we thought impossible were suddenly possible🤯一连串电影和想象中的角色和景观展现在我的眼前,和现实仿若融为一体。这让我感受到 Vision pro 或许能用一种全新的交互体验,让观众真正地身临其境,沉浸其中,甚至忘记真实和虚幻的界限。


直播接近尾声时,BGM 反复唱着 Be a Dreamer,苹果是个实干的梦想家,他们有足够的底气和积淀去梦想,更用努力和科技,将不可能变成可能,将许许多多科幻片中的梦想带来到 2023 年,用 Vision pro 为所有人铺就了无限大的画布,哦,不是二维的画布,是无限大的梦想空间!


当然,这个空间,目前似乎还只有基础的系统应用,像个刚通水电的毛坯房。他究竟能有怎样的表现,还是得看这些内容生产者开发出怎样的内容。有人诟病苹果在六月份拿出来这样的宣传,却要在明年年初才能售卖。可我相信,过去 APP Store 的成功很可能会在 Vision OS 中再现。WWDC,是苹果开发者大会,即主要面向开发者等专业人士的会议。Apple 召集起这些媒体,摄影师,导演,应用和游戏开发者率先开始了解 Vision pro。这些内容生产者,有他们,就有了 dream maker,造梦人,为普通用户编织光怪陆离的绚烂梦境。


看完各位博主的真机体验,Vision pro 并不是一个取代现有的手机、电脑的产品,这是一个全新的,开创新的体验维度,开创人类新需求的产品。


作为多年的哈迷,感觉现在 Vision pro 的语音输入,手势识别和 3D 交互完全可以让我们拿着魔杖释放咒语,让我们和神奇动物面对面,让我们就像骑着飞天扫帚一样去追踪金色飞贼。因为有了如此先进的科技,魔法世界不再是幻想🥺🤩


更可以想象,无论是工业设计,照片、视频、电视电影还是游戏,都可能会因为这种全新的沉浸式的三维交互体验,而被改写。


你可以和同事一起在虚拟空间中建造模型,模拟生产制造流程。


你可以把与亲朋好友、猫猫狗狗共度的美好时光定格在一片似真似幻的空间。无论何时再回首,他们好像永远在你身边,永不褪色。


电影制作人未来可以使用专门的摄像机制作沉浸式三维电影,在家就能有 100 英尺,接近 30 米的巨幕享受。 篮球比赛你可以选择不同的机位跟踪你喜欢的球星和精彩瞬间。 演唱会你可以在任何地方躺下享受最佳视角和空间音效。


而游戏,新增的交互体验更是给了游戏制作人们无限的想象空间。 操控赛车从北极的冰川到热带的雨林;在枪林弹雨中和队友并肩对抗敌人;在球赛场上面对面激情碰撞。配合上 AI 和语言模型,喜欢的二次元角色仿佛搭着你的肩膀和你耳语;所有的一切,开始“眼见为实”。


Vision pro 的眼部追踪、手势交互和 3D 显示混合现实的完成度,带来了像当年 iPhone 实现多点触控的革命性质变。从技术的进步来说,我个人认为这次的质变可能更加惊艳,更加了不起。但是 3499 美元,加上税可能 3 万人民币。说实话,这不是一个大众消费者能够接受的价格。即使有 air 版本,我感觉可能也需要上万人民币。所以,个人估计,它受欢迎的程度应该会大于等于 mac 小于 iPhone 和 AirPods。


当年 Apple Macintosh 开创了精美的高完成度的计算机图形界面 GUI,让电脑走入消费者群体,可价格太贵,真正让个人电脑普及的是微软;现在,iOS 将手机变成了一个功能强大的多媒体设备,可价格不便宜,真正让千家万户享受到智能手机的是 Android。未来,相比 Vision pro,或许有其他品牌的廉价替代品,更开放的开源混合现实系统,Vision 系列或许都不能获得最大的市场份额。可是由 Vision 所真正掀开的新维度不会被关闭,这个被精心打造出来的梦想空间只会无限延伸,满载着人类的创意和梦想…最后的最后,正如 WWDC 中 Apple 所言:


Be a dreamer. This is just the START.


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

SwiftUI 入门教程 - 基础控件

iOS
SwiftUI 是 Apple 新推出的一款能快速搭建页面的 framework。它采用的是声明式语法,简洁明了。 而且它是所见即所得的,你写的代码都能通过 Preview 实时的看到效果,这可以很大的节省开发者开发时间。当你开发一个复杂的项目,需要等待几分钟...
继续阅读 »

SwiftUI 是 Apple 新推出的一款能快速搭建页面的 framework。它采用的是声明式语法,简洁明了。


而且它是所见即所得的,你写的代码都能通过 Preview 实时的看到效果,这可以很大的节省开发者开发时间。当你开发一个复杂的项目,需要等待几分钟的时间去编译运行代码,只为了看一个 UILabel 字体大小或者颜色是否改变时,你就能体会到所见即所得的快乐了。


基础控件


当我们新建一个项目,选择 Interface 选择 SwiftUI 时,建好的项目会自带一个 ContentView,这是下面的默认代码:

struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}
}

struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

ContentView 是需要我们根据需求修改代码的部分,下面的 ContentView_Previews 则是为了实时预览的。


Tips:如果注释ContentView_Previews,你会发现预览页面也会消失。


ContentView 代码说明


首先,可以看到 ContentView 有一个 body 的计算属性,该属性代表当前视图的内容。当你实现一个自定义 view 的时候,必须要实现该属性,否则代码会报错。


VStack 代表的是一个垂直布局。里面包含 Image 和 Text,两个控件垂直布局。padding 则代表当前视图外边距的间距。


Text 对应 UILabel


在 SwiftUI 中,用 Text 控件来展示静态文本。下面是它的代码示例:

Text("我是一个文本")
.font(.title)
.foregroundColor(.red)
.frame(width: 100, alignment: .center)
.lineLimit(1)
.background(.yellow)

常用的属性基本就这几个:


  • font:字体。如果想更加细致化的指定字体,可以用 system,.font(.system(size: 16, weight: .light))
  • foregroundColor:字体颜色。
  • frame:控制文本的大小和对齐位置。这个不写的话默认是自适应宽高。如果仅指定宽度就是高度自适应,仅指定高度就是宽度自适应。
  • lineLimit:指定行数,默认为 0,不限制行数。
  • background:用来设置背景。比如背景形状、背景颜色等等。

Tips:SwiftUI 的布局简化了自动布局和弱化了 frame 指定具体数值的布局方式。默认都是自适应的,这一点和 Flutter 类似,大大提高了开发效率。


Image 对应 UIImageView


在 SwiftUI 中,Image 用来展示图像资源。下面是它的示例代码:

Image(systemName: "globe")
.resizable()
.aspectRatio(contentMode: .fit)
.foregroundColor(.accentColor)
.background(.red)

常用属性:


  • resizable:可调整大小以适应当前布局。
  • aspectRatio:调整缩放比。
  • foregroundColor、background:参见 Text。

Button 对应 UIButton


在 SwiftUI 中,用 Button 来表示一个按钮。下面是它的示例代码:

Button {
print("点击了按钮")
} label: {
Text("按钮文本")
Image(systemName: "globe")
}
.cornerRadius(10)
.background(.red)
.font(.body)
.border(.black, width: 2)

常用属性:


  • font、foregroundColor、background 等属性与 Text 使用一致。
  • label:用来自定义按钮的文本和图标。
  • cornerRadius:设置圆角。
  • border:设置边框。

总结


本文主要讲解了 SwiftUI 的三个基本控件 Text:用来展示静态文本;Image:用来加载图像资源;Button:用来展示按钮。以及三个控件的基本使用。希望通过此文大家可以对 SwiftUI 的语法有个基本的了解。


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

中国未来楼市,程序员的小窝购买指南

中国楼市持续火爆,未来趋势如何?中国楼市在过去的几年里一直保持着火爆的态势,无论是房价还是成交量都不断创下新高。本文将从中国楼市的背景介绍、市场分析、原因分析和未来展望等方面进行分析。一、背景介绍中国楼市的发展可以追溯到上世纪80年代,当时城市土地开始实行私有...
继续阅读 »

中国楼市持续火爆,未来趋势如何?

中国楼市在过去的几年里一直保持着火爆的态势,无论是房价还是成交量都不断创下新高。本文将从中国楼市的背景介绍、市场分析、原因分析和未来展望等方面进行分析。

一、背景介绍

中国楼市的发展可以追溯到上世纪80年代,当时城市土地开始实行私有化改革,房地产市场逐渐形成。随着经济的快速发展和城市化进程的加速,中国楼市也迎来了飞速发展的时期。特别是2000年以来,房地产市场逐渐成为国民经济的重要支柱产业,政府也出台了一系列扶持政策,如住房制度改革、住房公积金制度等。

二、市场分析

  1. 投资者热衷于购房

随着人们生活水平的提高和购房政策的宽松,越来越多的投资者热衷于购房。他们将购房视为一种投资手段,认为房价会持续上涨,从而获得更多的收益。这种投资需求的增加也推高了中国楼市的房价。

  1. 房贷违约案例逐渐增多

随着楼市的火爆,越来越多的人选择贷款购房。然而,近年来房贷违约案例逐渐增多,给银行和房地产市场带来了不小的风险。部分购房者由于收入不稳定、贷款利率上升等原因,无法按时偿还房贷,导致违约。

三、原因分析

  1. 政策调控

中国政府对房地产市场的调控政策对市场的影响非常大。例如,政府出台的“国八条”、“限购令”等政策,对楼市进行了严格的调控,使得市场逐渐回归理性。

  1. 利率变化

利率的变化也是影响楼市的重要因素之一。在利率较低的时候,购房者可以获得更低的贷款利率,从而降低了购房成本,提高了购房需求。而在利率较高的时候,购房者的负担加重,购房需求相应减少。

  1. 人口因素

中国拥有庞大的人口基数,这也为房地产市场提供了广阔的需求空间。特别是在城市化进程加速的情况下,大量人口涌入城市,使得城市房屋需求不断增长。

四、未来展望

  1. 政策调整

未来中国政府可能会对楼市政策进行适当调整。一方面,政府将继续加强对房地产市场的监管,抑制房价过快上涨;另一方面,政府可能会出台更加优惠的购房政策,鼓励刚需和改善型购房者购房。

  1. 经济环境的变化

中国经济的发展也可能会对中国楼市产生影响。未来中国经济可能会逐渐转型,从传统的制造业向服务业和高科技产业转型。这种转型可能会导致人们对住房的需求发生变化,对楼市产生一定的影响。

综上所述,中国楼市在经历了一段飞速发展的时期后,目前仍处于较为火热的态势。然而,受到政策调控、利率变化、人口因素等多种因素的影响,楼市也面临一定的挑战。未来,中国楼市将如何在政策调整和经济环境的变化中寻找新的发展方向,值得我们进一步关注和研究。

收起阅读 »

iOS 电商倒计时

iOS
背景 最近项目中,需要做一个如图所示的倒计时控件,上网搜了一圈,发现大家的方法大同小异,都是把倒计时的秒,转换成时分秒然后拼接字符串,见下图 网上大部分采用的方法 juejin.cn/post/684490…  在我的项目中,期望这个倒计时控件的f...
继续阅读 »

背景


最近项目中,需要做一个如图所示的倒计时控件,上网搜了一圈,发现大家的方法大同小异,都是把倒计时的秒,转换成时分秒然后拼接字符串,见下图




网上大部分采用的方法
juejin.cn/post/684490… 



在我的项目中,期望这个倒计时控件的format是可以自定义的,所以计算时分秒这样的方式,对于我的需求是不太灵活的


既然format需要自定义,那么很容易想到一个时间格式处理的类:DateFormatter


思路


后端返回的字段

init_time // 需要倒计时的时长,单位ms
format // 展示的倒计时格式

我们的需求其实非常明确,就是完成一个可以自定义format的倒计时label


那我们拆解一下整个需求:

  • 自定formatlabel
    • Date自定义format显示
    • 指定Date自定义format显示
  • 可以进行倒计时功能
  • 那么我们怎么才能把要倒计时的时长,转换为时分秒呢?

    • 直接计算后端给的init_time,算出是多少小时,多少分钟,多少秒
    • 如果我从每天的零点开始计时,然后把init_time作为偏移量不就是我要倒计时的时间吗,而且这个可以完美解决需要自定义format的问题,Date可以直接通过 DateFormatter转化成字符串 



Date自定义format显示

let df = DateFormatter()

df.dateFormat = "hh:mm:ss"

print("🍀", df.string(from: Date()), "🍀\n\n")

输出:🍀 03:56:28 🍀

指定Date自定义format显示

let df = DateFormatter()

var calendar = Calendar(identifier: .gregorian)

let startOfDate = calendar.startOfDay(for: Date())

df.dateFormat = "hh:mm:ss"

print("🍀", df.string(from: startOfDate), "🍀\n\n")

输出:🍀 12:00:00 🍀

完整功能

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

        initCountdownTimer()

        return true

    }

private var timer: DispatchSourceTimer?

private var second = 0

// 单位ms
var delayTime = 0

// 单位ms
var interval = 1000
var initSecound = 10

var format = "hh:mm:ss"

private lazy var startDate: Date = {
var calendar = Calendar(identifier: .gregorian)
        let startOfDate = calendar.startOfDay(for: Date())
        return Date(timeInterval: TimeInterval(initSecound), since: startOfDate)
  }()

  private lazy var df: DateFormatter = {
let df = DateFormatter()
        df.dateFormat = format
        return df
  }()

  func initCountdownTimer() {
        timer = DispatchSource.makeTimerSource(queue: .main)
        timer?.schedule(deadline: .now() + .milliseconds(delayTime), repeating: .milliseconds(interval), leeway: .milliseconds(1))
        timer?.setEventHandler { [weak self] in
            self?.updateText()
            self?.second += 1
        }

        timer?.resume()
    }

    func deinitTimer() {
        timer?.cancel()
        timer = nil
    }

    func updateText() {
        if second == initSecound && second != 0 {
            deinitTimer()
        }
        if second == initSecound {
            return
        }
        let date = Date(timeInterval: -TimeInterval(second + 1), since: startDate)
        let text = df.string(from: date)

        print(text)
    }

输出:
12:00:09
12:00:08
12:00:07
12:00:06
12:00:05
12:00:04
12:00:03
12:00:02
12:00:01
12:00:00

以上整个功能基本完成,但是细心的同学肯定发现了,按道理小时部分应该是00,但是实际是12,这是为什么呢,为什么呢?


我在这里研究了好久,上网查了很多资料


最后去研究了foramt每个字母的意思才知道:

  • h 代表 12小时制

  • H 代表 24小时制,如果想要显示00,把"hh:mm:ss"改成"HH:mm:ss"即可


时间格式符号字段详见


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

我有一刀,可斩全栈

引言 夜谈性的文章,思考篇幅会比较啰嗦,篇幅基本会以概念、发展、思考、未来这几个内容主题进行,最近结合软环境地狱,再到看到社区的很多未来思考,做一些总结和预测,去年的一些总结,今年基本应验了一部分,希望能起到警示和思考吧。 概念 什么是全栈 全栈(Full-...
继续阅读 »

引言


夜谈性的文章,思考篇幅会比较啰嗦,篇幅基本会以概念、发展、思考、未来这几个内容主题进行,最近结合软环境地狱,再到看到社区的很多未来思考,做一些总结和预测,去年的一些总结,今年基本应验了一部分,希望能起到警示和思考吧。


概念


什么是全栈



全栈(Full-Stack)是指一种解决问题域全局性技术的能力模型。


很多现代项目开发,需要掌握多种技术,以减少沟通成本、解决人手不够资源紧张、问题闭环的问题。全栈对业务的价值很大,如对于整个业务的统筹、技术方案的判断选型、问题的定位解决等,全栈技术能力有重要影响。另外对于各种人才配套不是很齐全的创业公司,全栈能解决各种问题,独挡多面,节省成本,能在早期促进业务快速发展。


技术有两个发展方向,一种是纵向一种是横向的,横向的是瑞士军刀,纵向的是削铁如泥的干将莫邪。这两个方向都没有对与错,发展到一定程度都会相互融合,就好比中国佛家禅修的南顿北渐,其实到了最后,渐悟与顿悟是一样的,顿由渐中来。可以说全栈什么都会,但又什么都不会。



全栈定义


狭义


全栈 = 前端 / 终端 + 后端


广义(问题全域)


全栈 = 呈现端(硬件 + 操作系统(linux/windows/android/ios/..) + 浏览器/宿主环境+端差异【机型、定制】) +H5+小程序(多端统一框架)+ 前端开发/终端开发 + 网络 + 后端开发(架构/算法) + 数据(SQL/NoSQL/半结构/时序/图特性) + 测试 + 运维


+软实力=文档能力+UI能力+业务能力+设计能力+技术视角(前瞻性)选型+不同语言掌握能力+项目管理能力+架构设计能力+客户沟通能力+技术撕逼能力+运营能力


价值


全局性思维


一个交付项目的全周期,除了传统的软件过程,需求调研、规划、商务、合同签订、立项、软件过程、交付、实施运维等,麻雀虽小,五脏俱全,如果对并发、相应、扩展性、并行开发等有硬性要求,软件过程会变得异常复杂,因此后来又拆前端架构、后端架构定向的解决某个领域内的技术规划岗位,因为人力反倒是小问题,要的是快和结果稳定,项目可以迅速肢解投入,每个岗位注重领域和边界问题,以做沟通的核心基础,对于一个团队特别是互联网企业来说,有一个全局性思维的人非常非常重要,这个角色常常会被赋予(产品/项目)或其他Tile,什么事业线、军团之类的,本质上也是对人员的细节化和边界的扩充。
回到本质问题,当人成为问题的时候,以3个人为例,一般开发层的东西,3个合理偏重的 【狭义全栈】,做事的效率和执行沟通结果和3个1+2的分端是完全不同的,一个是以业务块沟通的,一个是以功能块沟通的,一个是对业务块结果负责,一个是对功能块结果负责。


其实刚入职那会儿,就有人和我说,服务是看不到的,端是直面的,这其中有个度的问题,不过度设计、不过度随意,保持需求和设计在合理区间内,有适度的前瞻性即可。
我之前接触的单端普遍会犯在业务不可能的场景下,纯粹讨论逻辑性的问题,导致的无休止的无意义讨论,最终的反思是 我想把这个东西做好, 举个不太恰当的例子叫 "有一种冷,叫妈妈觉得你冷",我把这种归结起来就是不对结果负责,只对自己负责,这也多半是因为岗位边界的问题导致的。


沟通成本


项目越大,沟通成本越高,做过项目管理的都知道,项目中的人力是1+1<2的,人越多效率越低。因为沟通是需要成本的,不同技术的人各说各话,前端和后端是一定会掐架的。每个人都会为自己的利益而战,毫不为己的人是不存在的。


而全栈工程师的沟通成本会主要集中在业务上,因为各种技术都懂,胸有成竹,自己就全做了。即使是在团队协作中,与不同技术人员的沟通也会容易得多,让一个后端和一个前端去沟通,那完全是鸡同鸭讲,更不用说设计师与后端了。但如果有一个人懂产品懂设计懂前端懂后端,那沟通的结果显然不一样,因为他们讲的,彼此都能听得懂,相信经历过(纯业务/纯管理/纯产品)蹂躏过的开发应该有体会。


性价比与结果控制


创业公司不可能像大公司一样,各方面的人才都有。所以需要一个多面手,各种活都能一肩挑,独挡多面的万金油。对于创业公司,不可能说DBA前端后端客户端各种人才全都备齐了,很多工作请人又不饱和,不请人又没法做,外包又不放心质量,所以全栈工程师是省钱的一妙招,大公司不用担心人力,小公司绕不过的就是人力,当人力被卡住,事情被挡住了,独当一面可不只是说说而已,此时的价值就会被凸显,技术解决问题的途径很多样。


这里说个题外话,性价比是对企业的,那对个人来说,意味着个人的能量和价值会放大,如果你细心观察开源的趋势,会发现整体性的项目趋势变多了,而且基本在微小的时候可能只是单人支撑的,这个趋势从百度技术领跑再到阿里转换时有过方向和风格的转换。


困境


说得不好听一点,全栈工程师就是什么都会,什么都不会,但有需求,结果、时间、风险都会被很好的评估,因为思路和理念是完全不同的,全栈天然的就必然会重视执行结果,单端只注重过程,事情做了,坏的结果跟我一点儿关系都没有,其中甘苦,经历了才知道,所以也注定面试是不占优势的,而且全栈根本没有啥标准的划分,也注定游离在小公司才能如鱼得水,当然,如果你的目标是星辰大海,工作自由,这个事就另当别论了。


发展


天下大事分久必合,合久必分,最开始的没有前端,到分出前端,没有安卓/IOS到分出岗位,再到手机端合到前端,pc到前端,”大前端“的概念,不管技术怎么进步或者变化,总归是要为行业趋势负责的,就好比你为300人的企业用户考虑高并发,完全不计较实施和人力成本,很多的事情都是先试水再铺开的,没那么技术死板。


感觉整个软件生态发展至今,提供便利的同时,也用框架把每个人往工具这个方向上在培养,这本就是符合企业利益的事,但减量环境下,螺丝钉的支撑意义被无限的减弱和消磨,很多的单端从业一段时间后,想做事儿,发现另外领域的空白,也开始往横向考虑,这本就是危机思考和方向驱动的结果,一个大周期的循环又开始了,特别是在java国内的一家独大,再到个体开始挣扎的时候,多态的语言开始反噬,反噬的驱动力也从服务器这个层级开始了挣扎,亦如当年的java跨平台先机一样。


前端的框架随着框架的便捷性和易用性越来越完善,其竞争力变得隐形了,回归了工程化问题的解决能力,去年也提过,变化中思考,稳定中死亡,到了思考自己的核心竞争力是什么的时候了,这何尝不是自由工作者的春天。


端扩散


软件的路程发展已经有了很长一段路,概念和业务层级的提升服务有限,自动化、半自动化、AI的概念渐渐的可以走向技术成熟,端的发展又有了去处,只不过这个过程很慎重,需要打通很多封闭的东西,再加上工业信息化的政策加持,单纯的信息录入或者业务系统已经掀不起多大风浪,而纯互联网的金融、物联网也被玩的渣都不剩,突围和再上一层的变革,短时间内,公司级的突破已经很难找到出路,从收缩阵地,裁剪人员可见一斑。


复杂度提升


如果说有确切的变化,那基本就是我机器上的编译器环境和用的工具越来越多样,解决问题的途径和手段越来越多,不再是原来的一个整合ide解决所有问题,这就好比,我原先手上只有木棍,武器用它、做房子用它、生火也用它,挖掘的它所有的应用途径,那有一天,我有了刀、有了席梦思的床、有了大别墅,却因为害怕放着不用。当然,我之前听别人说过一个理论:”只要能解决好结果,哪怕你徒手,我也无所谓“,他站在老板的角度上,至于你是累死也好,花10倍的工作量也好,都无所谓。作为个体来说,既然只要结果,那就别怪我偷工作量了,个体的掌握技能的多样性,背后可是有语言生态支持的,因此复杂度的提升,也带来了生态支持,并非一边倒的情况。


人心异化


我依然怀念头几年的环境,都是集中在解决问题,目标一致,各自解决各自的问题,拼到一起,就是整体结果,各自的同事关系轻松和谐,上线前的交付大家一起搞的1点多,下班宵夜美滋滋,现在端分离和职责明确,天然存在利益冲突,摸鱼划水,撕逼的情况,虽说可能是部分老鼠屎引起的,但谁说这不是热情消退的结果呢,生活归生活,工作归工作,但生活真的归了生活,工作真的只归了工作吗?


思考


全栈的title就跟我参与了xxx开源项目一样,貌似也成为提升竞争力,标签化的一种,架构师、小组长、技术经理、总监,这些title,在离职那一刻其实都毫无意义,有意义的也只是待遇和自身的能力,如果你怀着高title在另外一家公司风生水起的想法,那很多3个月离职的经历,再一家还是3个月,难道不是面试能力和自身的能力出现不对等了嘛,可能是所有的公司都坑,那有没有可能是我们韧性太低,选择不慎呢。


好像刚工作那会儿,经常会被问到职业规划,之后很少被问到,却不停的在想,我能干嘛,今后想干嘛,之后就是无休止的躁动和不停的学习,不停的接项目,不停的用新技术,10年多的坚持,平均12点,找的工作基本也都是相对轻松的,那我能干啥,好像貌似什么也做不了,想法创意不停的被对比否认,找到合适的却不停的为盈利性的项目让路,貌似什么都会,貌似什么都没做成,原本以为是觉得自己修炼不够,没法实现自己的项目,后来发现,其实自己的第二职业,只需要一条路,一往无前的坚持,最终会有结果,尽管这个结果可能不好,但事情实践了,回想起刚工作那会儿”先理顺环节,再开发,还是先出东西再说“的争论,这会儿我完全认同了 ”先结果,再谈未来“


因此,别管什么 ”前端已死“”java已死“,大环境不好,行业低迷,去行动吧,亲手埋葬也许,焕发新生也好,回到内心,做好与行业诀别的决心,背水一战。即便是为了生活被迫转行,也可毫不顾忌的说,努力过,没戏,直面内心,回想起18年看到的新闻,”程序猿直播7天0观众“,我想我能够做的也只能是武装与坚持,至于大环境怎样,行业怎样,到那一天再说吧,套用领导的话”别想那些有的没的,做好自己的事“,至少,我人为,当软件公司不易时,恰恰是个体的机会,当个体的力量开始有竞争力,那全栈的优势会有很好的发挥,这个场景在我有意识的5人实践和2人优势互补中已经得到了长效的验证。


未来


也许从当前的公司离职那天,就是我职业生涯结束那天,我已经做好了心里预期,但我希望可以作为一个自由工作者,这是我后半段反复思考的结果,至于结果怎样,我只能说,预期的努力我已经做了,时机和后续有待生活的刀斩我不屈之心。


PS


认清内心、从容面对,不要有什么鸵鸟心态,事实不逃避,行动不耽误,这是斩龙之刀,破除未知的迷雾,我所能提的也只是从心和认知,没啥发展途径和规划,因为技术的发展,总是未知和充满惊喜的,这也正是它的魅力所在。


最后


我深怕自己本非美玉,故而不敢加以刻苦琢磨,却又半信自己是块美玉,故又不肯庸庸碌碌,与瓦砾为伍。于是我渐渐地脱离凡尘,疏远世人,结果便是一任愤懑与羞恨日益助长内心那恬弱的自尊心。


作者:沈二到不行
来源:juejin.cn/post/7248118049583628344
收起阅读 »

如果写劣质代码是犯罪,那我该判无期

导读 程序员痛恨遇到质量低劣的代码,但在高压环境下,我们常为了最快解决当下需求而忽略代码规范,在无意识中堆积大量债务。我们还观察到许多开发者被迫加班的罪魁祸首便是写低效代码、不重视代码优化。编程路上,欲速则不达。 接下来,我将为各位列举9种我个人工作中高频遇到...
继续阅读 »

导读


程序员痛恨遇到质量低劣的代码,但在高压环境下,我们常为了最快解决当下需求而忽略代码规范,在无意识中堆积大量债务。我们还观察到许多开发者被迫加班的罪魁祸首便是写低效代码、不重视代码优化。编程路上,欲速则不达。 接下来,我将为各位列举9种我个人工作中高频遇到的不整洁代码行为,并提出针对性优化建议。继续阅读~


目录


1 代码风格和可读性


2 注释


3 错误处理和异常处理


4 代码复用和模块化


5 硬编码


6 测试和调试


7 性能优化


8 代码安全性


9 版本控制和协作


10 总结


01、代码风格和可读性



  • 错误习惯


不一致的命名规则:使用多种命名规则,如 camelCase、snake_case 和 PascalCase 等。过长的函数和方法:编写过长的函数和方法,导致代码难以阅读和理解。 过长的行:编写超过50字符的代码行,导致代码难以阅读。

1.1 变量命名不规范


在编程中,变量命名是非常重要的,良好的变量命名能够提高代码的可读性和可维护性。不规范的命名会增加理解难度,以下是一个不规范命名的例子:


int a, b, c; // 不具有描述性的变量名
float f; // 不清楚变量表示的含义

这样的变量命名不仅会降低代码的可读性,还可能会导致变量混淆,增加代码维护的难度。正确的做法应该使用有意义的名称来命名变量。例如:


int num1, num2, result; // 具有描述性的变量名
float price; // 清晰明了的变量名

1.2 长函数和复杂逻辑


长函数和复杂逻辑是另一个常见的错误和坏习惯。长函数难以理解和维护,而复杂逻辑可能导致错误和难以调试。以下是一个长函数和复杂逻辑的案例:


def count_grade(score):
if score >= 90:
grade = 'A'
elif score >= 80:
grade = 'B'
elif score >= 70:
grade = 'C'
elif score >= 60:
grade = 'D'
else:
grade = 'F'

if grade == 'A' or grade == 'B':
result = 'Pass'
else:
result = 'Fail'
return result

在这个例子中,函数 count_grade 包含了较长的逻辑和多个嵌套的条件语句,使得代码难以理解和维护。正确的做法是将逻辑拆分为多个小函数,每个函数只负责一个简单的任务,例如:


def count_grade(score):
grade = get_grade(score)
result = pass_or_fail(grade)
return result
def get_grade(score):
if score >= 90:
return 'A'
elif score >= 80:
return 'B'
elif score >= 70:
return 'C'
elif score >= 60:
return 'D'
else:
return 'F'
def pass_or_fail(grade):
if grade == 'A' or grade == 'B':
return 'Pass'
else:
return 'Fail'

通过拆分函数,我们使得代码更加可读和可维护。


1.3 过长的行


代码行过长,会导致代码难以阅读和理解,增加了维护和调试的难度。例如:


def f(x):
if x>0:return 'positive' elif x<0:return 'negative'else:return 'zero'

这段代码的问题在于,它没有正确地使用空格和换行,使得代码看起来混乱,难以阅读。正确的方法是,我们应该遵循一定的代码规范和风格,使得代码清晰、易读。下面是按照 PEP 8规范改写的代码:


def check_number(x):
if x > 0:
return 'positive'
elif x < 0:
return 'negative'
else:
return 'zero'

这段代码使用了正确的空格和换行,使得代码清晰、易读。


02、注释



  • 错误习惯


缺少注释:没有为代码编写注释,导致其他人难以理解代码的功能和逻辑。 过时的注释:未及时更新注释,使注释与实际代码不一致。 错误注释:注释上并不规范,常常使用一些不合理的注释。



  • 错误的注释




注释是非常重要的,良好的注释可以提高代码的可读性和可维护性。以下是一个不规范的例子:


int num1, num2; // 定义两个变量

上述代码中,注释并没有提供有用的信息,反而增加了代码的复杂度。


03、错误处理和异常处理



  • 错误的习惯


忽略错误:未对可能出现的错误进行处理。 过度使用异常处理:滥用 try...except 结构,导致代码逻辑混乱。 捕获过于宽泛的异常:捕获过于宽泛的异常,如 except Exception,导致难以定位问题。

3.1 忽略错误


我们往往会遇到各种错误和异常。如果我们忽视了错误处理,那么当错误发生时,程序可能会崩溃,或者出现不可预知的行为。例如:


def divide(x, y):
return x / y

这段代码的问题在于,当 y 为0时,它会抛出 ZeroDivisionError 异常,但是这段代码没有处理这个异常。下面是改进的代码:


def divide(x, y):
try:
return x / y
except ZeroDivisionError:
return 'Cannot divide by zero!'

3.2 过度使用异常处理


我们可能会使用异常处理来替代条件判断,这是不合适的。异常处理应该用于处理异常情况,而不是正常的控制流程。例如:


def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
result = float('inf')
return result

在这个示例中,我们使用异常处理来处理除以零的情况。正确做法:


def divide(a, b):
if b == 0:
result = float('inf')
else:
result = a / b
return result

在这个示例中,我们使用条件判断来处理除以零的情况,而不是使用异常处理。


3.3 捕获过于宽泛的异常


捕获过于宽泛的异常可能导致程序崩溃或隐藏潜在的问题。以下是一个案例:


try {
// 执行一些可能抛出异常的代码
} catch (Exception e) {
// 捕获所有异常,并忽略错误}

在这个例子中,异常被捕获后,没有进行任何处理或记录,导致程序无法正确处理异常情况。正确的做法是根据具体情况,选择合适的异常处理方式,例如:


try {
// 执行一些可能抛出异常的代码
} catch (FileNotFoundException e) {
// 处理文件未找到异常
logger.error("File not found", e);
} catch (IOException e) {
// 处理IO异常
logger.error("IO error", e);
} catch (Exception e) {
// 处理其他异常
logger.error("Unexpected error", e);}

通过合理的异常处理,我们可以更好地处理异常情况,增加程序的稳定性和可靠性。


04、错误处理和异常处理



  • 错误的习惯


缺乏复用性:代码冗余,维护困难,增加 bug 出现的可能性。 缺乏模块化:代码耦合度高,难以重构和测试。

4.1 缺乏复用性


代码重复是一种非常常见的错误。当我们需要实现某个功能时,可能会复制粘贴之前的代码来实现,这样可能会导致代码重复,增加代码维护的难度。例如:


   def calculate_area_of_rectangle(length, width):
return length * width

def calculate_volume_of_cuboid(length, width, height):
return length * width * height

def calculate_area_of_triangle(base, height):
return 0.5 * base * height

def calculate_volume_of_cone(radius, height):
return (1/3) * 3.14 * radius * radius * height

上述代码中,计算逻辑存在重复,这样的代码重复会影响代码的可维护性。为了避免代码重复,我们可以将相同的代码复用,封装成一个函数或者方法。例如:


   def calculate_area_of_rectangle(length, width):
return length * width

def calculate_volume(length, width, height):
return calculate_area_of_rectangle(length, width) * height

def calculate_area_of_triangle(base, height):
return 0.5 * base * height

def calculate_volume_of_cone(radius, height):
return (1/3) * 3.14 * radius * radius * height

这样,我们就可以避免代码重复,提高代码的可维护性。


4.2 缺乏模块化


缺乏模块化是一种常见的错误,这样容易造成冗余,降低代码的可维护性,例如:


   class User:
def __init__(self, name):
self.name = name

def save(self):
# 保存用户到数据库的逻辑

def send_email(self, content):
# 发送邮件的逻辑

class Order:
def __init__(self, user, product):
self.user = user
self.product = product

def save(self):
# 保存订单到数据库的逻辑

def send_email(self, content):
# 发送邮件的逻辑
```

此例中,User 和 Order 类都包含了保存和发送邮件的逻辑,导致代码重复,耦合度高。我们可以通过将发送邮件的逻辑提取为一个独立的类,例如:


   class User:
def __init__(self, name):
self.name = name

def save(self):
# 保存用户到数据库的逻辑

class Order:
def __init__(self, user, product):
self.user = user
self.product = product

def save(self):
# 保存订单到数据库的逻辑

class EmailSender:
def send_email(self, content):
# 发送邮件的逻辑

通过把发送邮件单独提取出来,实现了模块化。现在 User 和 Order 类只负责自己的核心功能,而发送邮件的逻辑由 EmailSender 类负责。这样一来,代码更加清晰,耦合度降低,易于重构和测试。


05、硬编码



  • 错误的习惯


常量:设置固定常量,导致维护困难。 全局变量:过度使用全局变量,导致程序的状态难以跟踪。

5.1 常量


在编程中,我们经常需要使用一些常量,如数字、字符串等。然而,直接在代码中硬编码这些常量是一个不好的习惯,因为它们可能会在未来发生变化,导致维护困难。例如:


def calculate_score(score):
if (score > 60) {
// do something}

这里的60就是一个硬编码的常量,导致后续维护困难,正确的做法应该使用常量或者枚举来表示。例如:


PASS_SCORE = 60;
def calculate_score(score):
if (score > PASS_SCORE) {
// do something }

这样,我们就可以避免硬编码,提高代码的可维护性。


5.2 全局变量


过度使用全局变量在全局范围内都可以访问和修改。因此,过度使用全局变量可能会导致程序的状态难以跟踪,增加了程序出错的可能性。例如:


counter = 0
def increment():
global counter
counter +
= 1

这段代码的问题在于,它使用了全局变量 counter,使得程序的状态难以跟踪。我们应该尽量减少全局变量的使用,而是使用函数参数和返回值来传递数据。例如:


def increment(counter):
return counter + 1

这段代码没有使用全局变量,而是使用函数参数和返回值来传递数据,使得程序的状态更易于跟踪。


06、测试和调试



  • 错误的习惯


单元测试:不进行单元测试会导致无法及时发现和修复代码中的错误,增加代码的不稳定性和可维护性。 边界测试:不进行边界测试可能导致代码在边界情况下出现错误或异常。 代码的可测试性:有些情况依赖于当前条件,使测试变得很难。

6.1 单元测试


单元测试是验证代码中最小可测试单元的方法,下面是不添加单元测试的案例:


def add_number(a, b):
return a + b

在这个示例中,我们没有进行单元测试来验证函数 add_number 的正确性。正确示例:


import unittest

def add_number(a, b):
return a + b

class TestAdd(unittest.TestCase):
def add_number(self):
self.assertEqual(add(2, 3), 5)

if __name__ == '__main__': unittest.main()

在这个示例中,我们使用了 unittest 模块进行单元测试,确保函数 add 的正确性。


6.2 边界测试


边界测试是针对输入的边界条件进行测试,以验证代码在边界情况下的行为下面是错误示例:


def is_even(n):
return n % 2 == 0

在这个示例中,我们没有进行边界测试来验证函数 is_even 在边界情况下的行为。正确示例:


import unittest

def is_even(n):
return n % 2 == 0

class TestIsEven(unittest.TestCase):
def test_even(self):
self.assertTrue(is_even(2))
self.assertFalse(is_even(3))

if __name__ == '__main__': unittest.main()

在这个示例中,我们使用了 unittest 模块进行边界测试,验证函数 is_even 在边界情况下的行为。


6.3 可测试性


代码的可测试性我们需要编写测试来验证代码的正确性。如果我们忽视了代码的可测试性,那么编写测试将会变得困难,甚至无法编写测试。例如:


def get_current_time():
return datetime.datetime.now()

这段代码的问题在于,它依赖于当前的时间,这使得我们无法编写确定性的测试。我们应该尽量减少代码的依赖,使得代码更易于测试。例如:


def get_time(now):
return now

这段代码不再依赖于当前的时间,而是通过参数传入时间,这使得我们可以编写确定性的测试。


07、性能优化



  • 错误的习惯


过度优化:过度优化可能会导致代码难以理解和维护,甚至可能会引入新的错误。 合适的数据结构:选择合适的数据结构可以提高代码的性能。

7.1 过度优化


我们往往会试图优化代码,使其运行得更快。然而,过度优化可能会导致代码难以理解和维护,甚至可能会引入新的错误。例如:


def sum(numbers):
return functools.reduce(operator.add, numbers)

这段代码的问题在于,它使用了 functools.reduce 和 operator.add 来计算列表的和,虽然这样做可以提高一点点性能,但是这使得代码难以理解。我们应该在保持代码清晰和易读的前提下,进行适度的优化。例如:


def sum(numbers):
return sum(numbers)

这段代码使用了内置的 sum 函数来计算列表的和,虽然它可能比上面的代码慢一点,但是它更清晰、易读。


7.2 没有使用合适的数据结构


选择合适的数据结构可以提高代码的性能。使用不合适的数据结构可能导致代码执行缓慢或占用过多的内存。例如:


def find_duplicate(numbers):
duplicates = []
for i in range(len(numbers)):
if numbers[i] in numbers[i+1:]:
duplicates.append(numbers[i])
return duplicates

在这个示例中,我们使用了列表来查找重复元素,但这种方法的时间复杂度较高。我们可以使用集合来查找元素。例如:


def find_duplicate(numbers):
duplicates = set()
seen = set()
for num in numbers:
if num in seen:
duplicates.add(num)
else:
seen.add(num)
return list(duplicates)

我们使用了集合来查找重复元素,这种方法的时间复杂度较低。


08、代码安全性



  • 错误的习惯


输入验证:不正确的输入验证可能导致安全漏洞,如 SQL 注入、跨站脚本攻击等。 密码存储:不正确的密码存储可能导致用户密码泄露。 权限控制:不正确的权限控制可能导致未经授权的用户访问敏感信息或执行特权操作。

8.1 输入验证


没有对用户输入进行充分验证和过滤可能导致恶意用户执行恶意代码或获取敏感信息。例如:


import sqlite3
def get_user(username):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
query = f"SELECT * FROM users WHERE username = '{username}'"
cursor.execute(query)
user = cursor.fetchone()
conn.close()
return user

在这个示例中,我们没有对用户输入的 username 参数进行验证和过滤,可能导致 SQL 注入攻击。正确示例:


import sqlite3

def get_user(username):
conn = sqlite3.connect('database.db')
cursor = conn.cursor()
query = "SELECT * FROM users WHERE username = ?"
cursor.execute(query, (username,))
user = cursor.fetchone()
conn.close()
return user

在这个示例中,我们使用参数化查询来过滤用户输入,避免了 SQL 注入攻击。


8.2 不正确的密码存储


将明文密码存储在数据库或文件中,或使用不安全的哈希算法存储密码都是不安全的做法。错误示例:


import hashlib

def store_password(password):
hashed_password = hashlib.md5(password.encode()).hexdigest()
# 存储 hashed_password 到数据库或文件中

在这个示例中,我们使用了不安全的哈希算法 MD5 来存储密码。正确示例:


import hashlib
import bcrypt

def store_password(password):
hashed_password = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
# 存储 hashed_password 到数据库或文件中

在这个示例中,我们使用了更安全的哈希算法 bcrypt 来存储密码。


8.3 不正确的权限控制


没有正确验证用户的身份和权限可能导致安全漏洞。错误示例:


def delete_user(user_id):
if current_user.is_admin:
# 执行删除用户的操作
else:
raise PermissionError("You don't have permission to delete users.")

在这个示例中,我们只检查了当前用户是否为管理员,但没有进行足够的身份验证和权限验证。正确示例:


def delete_user(user_id):
if current_user.is_authenticated and current_user.is_admin:
# 执行删除用户的操作
else:
raise PermissionError("You don't have permission to delete users.")

在这个示例中,我们不仅检查了当前用户是否为管理员,还检查了当前用户是否已经通过身份验证。


09、版本控制和协作



  • 错误的习惯


版本提交信息:不合理的版本提交信息会造成开发人员难以理解和追踪代码的变化。 忽略版本控制和备份:没有备份代码和版本控制的文件可能导致丢失代码、难以追溯错误来源和无法回滚等问题。

9.1 版本提交信息


不合理的版本提交信息可能导致代码丢失、开发人员难以理解等问题。错误示例:


git commit -m "Fixed a bug"

在这个例子中,提交信息没有提供足够的上下文和详细信息,导致其他开发人员难以理解和追踪代码的变化。正确的做法是提供有意义的提交信息,例如:


$ git commit -m "Fixed a bug in calculate function, which caused grade calculation for scores below 60"

通过提供有意义的提交信息,我们可以更好地追踪代码的变化,帮助其他开发人员理解和维护代码。


9.2 忽略版本控制和备份


忽略使用版本控制工具进行代码管理和备份是一个常见的错误。错误示例:


$ mv important_code.py important_code_backup.py
$ rm important_code.py

在这个示例中,开发者没有使用版本控制工具,只是简单地对文件进行重命名和删除,没有进行适当的备份和记录。正确示例:


$ git clone project.git
$ cp important_code.py important_code_backup.py
$ git add .
$ git commit -m "Created backup of important code"
$ git push origin master
$ rm important_code.py

在这个示例中,开发者使用了版本控制工具进行代码管理,并在删除之前创建了备份,确保了代码的安全性和可追溯性。


10、总结


好的代码应该如同一首好文,让人爱不释手。优雅的代码,不仅是功能完善,更要做好每一个细节。


最后,引用韩磊老师在《代码整洁之道》写到的一句话送给大家:



细节之中自有天地,整洁成就卓越代码。


以上是本文全部内容,欢迎分享。


-End-


原创作者|孔垂航


技术责编|刘银松


作者:腾讯云开发者
来源:juejin.cn/post/7257894053902565433
收起阅读 »

听说你会架构设计?来,解释一下为什么错不在李佳琦

1. 引言 大家好,我是小❤,一个漂泊江湖多年的 985 非科班程序员,曾混迹于国企、互联网大厂和创业公司的后台开发攻城狮。 1.1 带货风波 近几天,“带货一哥” 李佳琦直播事件闹得沸沸扬扬,稳占各大新闻榜单前 10 名。 图来源:微博热点,侵删 虽然小❤...
继续阅读 »

1. 引言


大家好,我是小❤,一个漂泊江湖多年的 985 非科班程序员,曾混迹于国企、互联网大厂和创业公司的后台开发攻城狮。


1.1 带货风波


近几天,“带货一哥” 李佳琦直播事件闹得沸沸扬扬,稳占各大新闻榜单前 10 名。



图来源:微博热点,侵删


虽然小❤平时很少看直播,尤其是带货直播。


但奈何不住吃瓜的好奇心重啊!于是就趁着休息的功夫了解了一下,原来这场风波事件起源于前几天的一场直播。


当时,李佳琦在直播间介绍合作产品 “花西子” 眉笔的价格为 79 元时,有网友在评论区吐槽越来越贵了。他直言:哪里贵了?这么多年都是这个价格,不要睁着眼睛乱说,国货品牌很难的,哪里贵了?



图来源:网络,侵删


之后,李佳琦接着表示:有的时候找找自己原因,这么多年了工资涨没涨,有没有认真工作?



图来源:互联网,侵删


小❤觉得,这件事评论区网友说的没错,吐槽一下商品的价格有什么问题呢?我自己平时买菜还挑挑拣拣的,能省一毛是一毛。


毕竟,这个商品的价格也摆在那是不?



图来源:微博热点,侵删


1.2 身份决定立场,立场决定言论


但是,有一说一,从主播的角度呢,我也能理解。毕竟,不同的消费能力,说着自己立场里认可的大实话,也没啥问题。


那问题出在哪呢?


咳咳,两边都没问题,那肯定是评论系统有问题!


一边是年收入十多亿的带货主播,一边是普普通通的老百姓,你评论区为啥不甄别出用户画像,再隔离一下评论?


俗话说:“屁股决定脑袋”,立场不同,言论自然不一样。所以,这个锅,评论系统背定了!


2. 评论系统的特点


正巧,前几天在看关于评论系统的设计方案,且这类架构设计在互联网大厂的面试里出现的频率还是挺高的。所以我们今天就来探讨一下这个热门话题——《海量评论系统的架构设计》。


2.1 需求分析


首先,让我们来了解一下评论系统的特点和主要功能需求。评论系统是网站和应用中不可或缺的一部分,主要分为两种:



  • 一种是列表平铺式,只能发起评论,不能回复;

  • 一种是盖楼式评论,支持无限盖楼回复,可以回复用户的评论。


为了迎合目前大部分网站和应用 App 的需求,我们设计的评论系统采用盖楼式评论


需要满足以下几个功能需求:



评论系统中的观众和主播相当于用户和管理员的角色,其中观众用户可以:



  • 评论发布和回复:用户可以轻松发布评论,回复他人的评论。

  • 点赞和踩:用户可以给评论点赞或踩,以表达自己的喜好。

  • 评论拉取:评论需要按照时间或热度排序,并且支持分页显示。


主播可以:




  • 管理评论:主播可以根据直播情况以及当前一段时间内的总评论数,来判断是否打开 “喜好开关”。




  • 禁言用户:当用户发布了不当言论,或者恶意引流时,主播可以禁言用户一段时间。




  • 举报/删除:系统需要支持主播举报不当评论,并允许主播删除用户的评论。




2.2 非功能需求


除了功能需求,评论系统还需要满足一系列非功能需求,例如应对高并发场景,在海量数据中如何保证系统的稳定运行是一个巨大的挑战。




  • 海量数据:拿抖音直播举例,10 亿级别的用户量,日活约 2 亿,假设平均每 10 个人/天发一条评论,总评论数约 2 千万/天;




  • 高并发量:每秒十万级的 QPS 访问,每秒万级的评论发布量;




  • 用户分布不均匀:某个直播间的用户或者评论区数量,超出普通用户几个数量级;




  • 时间分布不均匀:某个主播可能突然在某个时间点成为热点用户,其评论数量也可能陡增几个数量级。




3. 系统设计


评论系统也具有一个典型社交类系统的特征,可归结为三点:海量数据,高访问量,非均匀性,接下来我们将对评论系统的关键特点和需求做功能设计。


3.1 功能设计


在直播平台或评论系统里,观众可以接收开通提醒,并且评论被回复之后也可以通过手机 App 收到回复消息,所以需要和系统建立 TCP 长连接。


同样地,主播由于要实时上传视频直播流,所以也需要 TCP 连接。架构图如下:



用户或主播上线时,如果是第一次登录,需要从用户长连接管理系统申请一个 TCP 服务器地址信息,然后进行 TCP 连接



不了解 TCP 连接的同学可以看我之前写的这篇文章:听说你会架构设计?来,弄一个打车系统



当观众或主播(统称用户)第一次登录,或者和服务器断开连接(比如服务器宕机、用户切换网络、后台关闭手机 App 等),需要重连时,用户可以通过用户长连接管理系统重新申请一个 TCP 服务器地址(可用地址存储在 Zookeeper 中),拿到 TCP 地址后再发起请求连接到集群的某一台服务器上。


用户系统


用户系统的用户表记录了主播和观众的个人信息,包括用户名、头像和地理位置等信息。


除此之外,用户还需要记录关注信息,比如某个用户关注了哪些直播间。


用户表(user)设计如下:




  • user_id:用户唯一标识




  • name:用户名




  • portrait:头像压缩存储




  • addr:地理位置




  • role:用户角色,观众或主播




直播系统


每次开播后,直播系统通过拉取直播流,和主播设备建立 TCP 长连接。这时,直播系统会记录直播表(live)信息,包括:




  • live_id:一场直播的唯一标识




  • live_room_id:直播间的唯一标识




  • user_id:主播用户ID




  • title:直播主题




参考微博的关注系统,我们可以引入用户关注表(attention),以便用户可以关注直播间信息,并接收其动态和评论通知:



  • user_id:关注者的用户ID。

  • live_room_id:被关注者的直播间ID。


这个表可以用于构建用户和主播之间的社交网络,并实现评论的动态通知。


用户关系表的设计可以支持关注、取消关注和获取关注列表等功能。


在数据库中,使用索引可以提高关系查询的性能。同时,可以定期清理不活跃的关系,以减少存储和维护成本。


评论系统


参考微博的评论系统,我们可以支持多级嵌套评论,让用户能够回复特定评论。


对于嵌套评论的存储,我们可以使用递归结构或层次结构的数据库设计,也可以使用关系型数据库表结构。评论表(comment)字段如下:



  • comment_id:评论唯一标识符,主键。

  • user_id:评论者的用户ID。

  • content:评论内容,可以是文本或富文本。

  • timestamp:评论时间戳。

  • parent_comment_id:如果是回复评论,记录被回复评论的comment_id。

  • live_id:评论所属的直播ID。

  • level:评论级别,用于标识评论的嵌套层级。


除此之外,我们可以根据业务需求添加一些额外字段:如点赞数、踩数、举报数等,以支持更多功能。


推送系统


为了提供及时的评论通知,我们可以设计消息推送系统,当用户收到关注直播间开播,或者有新评论或回复时,系统可以向其发送通知。


通知系统需要支持消息的推送和处理,当直播间关注人数很多或者用户发出了热点评论时,为了保证系统稳定,可以使用消息队列来处理异步任务


此外,在推送时需要考虑消息的去重、过期处理和用户偏好设置等方面的问题。


3.2 性能和安全


除了最基本的功能设计以外,我们还需要结合评论系统的数据量和并发量,考虑如何解决高并发、高性能以及数据安全的问题。


1)高并发处理


评论系统面临着巨大的并发压力,数以万计的用户可能同时发布和查看评论。为了应对这个挑战,我们可以采取以下策略。


分布式架构



采用分布式集群架构,将流量分散到多个服务器上,降低单点故障风险,提升用户的性能体验。


消息队列


引入消息队列,如 Kafka,来处理异步任务。



当直播间开播时,首先获取到关注该直播间的用户,然后将直播间名称、直播主题等信息,放入消息队列。


消息推送系统实时监听消息队列,当获取到开播提醒的 Topic 时,首先从 Redis 获取和用户连接的 TCP 服务器信息,然后将开播消息推送到用户手机上


同样地,当用户评论被回复时,将评论用户名和评论信息通过消息推送系统,也推送到用户手机上。


使用消息队列一方面可以减轻服务器的流量负担,另一方面可以根据用户离线情况,消息推送系统可以将历史消息传入延时队列,当用户重新上线时去拉取这些历史消息,以此提升用户体验。


数据缓存


引入缓存层,如 Redis,用于缓存最新的评论数据,以此减轻数据库负载并提升响应速度。例如,可以根据 LRU 策略缓存直播间最热的评论、用户地理位置等信息,并定时更新。


2)安全和防护


评论系统需要应对敏感词汇、恶意攻击等安全威胁。我们可以采取以下防护措施:


文字过滤


使用文字过滤技术,过滤垃圾评论和敏感词汇。实现时,可以用 Redis 缓存或者布隆过滤器。对比性能,我们这里采用布隆过滤器来实现。


布隆过滤器(Bloom Filter)是一个巧妙设计的数据结构,它的原理是将一个值多次哈希,映射到不同的 bit 位上并记录下来。


当新的值使用时,通过同样的哈希函数,比对各个 bit 位上是否有值:如果这些 bit 位上都没有值,说明这个数不存在;否则,就大概率是存在的。



以上图为例,具体操作流程为:



  1. 假设敏感词汇有 3 个元素{菜狗,尼玛,撒币},哈希函数的个数也设置为 3。我们首先将位数组初始化,将每个位都置为 0。



  1. 然后将集合里的敏感词语通过 3 个哈希函数进行映射,每次映射都会产生一个哈希值,即位数组里的 1.



  1. 当查询词语是否为敏感文字时,用相同的哈希函数进行映射,如果映射的位置有一个不为 1,说明该文字一定不存在于集合元素中。反之,如果 3 个点都为 1,则判定元素存在于集合中。


当然,这可能会产生误判,布隆过滤器一定可以发现重复的值,但也可能将不重复的值判断为重复值。如上图中的 “天气”,虽然都命中了 1,但是它并没有存在于敏感词集合里。


布隆过滤器在处理大量数据时非常有用,比如网页缓存、拼写检查、黑名单过滤等。虽然它有一定的误判率(约为 0.05%),但是其判重的速度和节省空间的优点足以瑕不掩瑜。


用户限制


除了从评论信息上加以限制,我们也可以从用户侧来限制:



  • 用户认证:要求用户登录后才能发布评论,降低匿名评论的风险。

  • 评论限制:根据用户 ID 和直播 ID 进行限流,比如让用户在一分钟之内最多只能发送 10 条的评论。



不知道如何限流的,可以看小❤之前的这篇文章:若我问到高可用,阁下又该如何应对呢?



4. 李佳琦该如何应对?


4.1 文本分析和情感分析


除了可以用布隆过滤器检测出恶意攻击和敏感内容,我们还可以引入文本分析和情感分析技术,使用自然语言处理(NLP)算法来检测不当评论。


并且,通过分析用户的评论内容,可以进行情感分析,以了解用户的情感倾向。



除了算法模块,我们还需要新增一个评论采集系统,定期(比如每天)从数据库里拉取用户的评论数据,传入对象存储服务。


算法模块监听对象存储服务,每天实时拉取训练数据,并获取可用的情感分析和语义理解模型。


当有新的评论出现时,会先调用算法模型,然后根据情感的分析结果来存储评论信息。我们可以在评论表(comment)里面新增一个表示情感正负倾向的字段 emotion,当主播打开喜好开关后,只拉取 emotion 为 TRUE 的评论信息,将“嫌贵的用户”或者 “评价为负面” 的评论设置为不可见。


这样,每次直播时,主播看到的都是情感正向且说话好听的评论,不仅能提升直播激情,还能增加与 “真爱粉” 的互动效果,可谓一箭三雕 🐶


但是,评论调用算法模型势必会牺牲一定的实时性与互动效果,主播也可以在开启直播时可以自己决定是否要打开评论喜好设置,并告知打开后评论会延时一段时间。


4.2 机器学习和推荐算法


除了从主播的角度,评论系统还可以引入机器学习算法来分析用户行为,根据用户的历史评论和喜好。


从观众来说,这可以提高观众的参与度和留存率,增强用户粘性。


从主播来说,可以筛选出真爱粉,脑残粉,甚至死亡芭比粉 🐶。这样,每次主播在直播时,只筛选一部分用户可以发表评论,其余的统统禁言,或者设置为不看用户评论。



除了直播领域,社交领域也经常使用推荐算法来获取评论内容。比如之前有 B 站 UP 主爆出:小红书在同一个帖子下,对女性用户和男性用户展示的评论区是不一样的,甚至评论区是截然相反的观点。


这个小❤没有试验过,大家不妨去看一下😃


5. 小结


目前,评论系统随着移动互联网的直播和社交平台规模不断扩大,许多网站和应用已经实现了社交媒体集成,允许用户使用他们的社交媒体帐户进行评论,增加了互动性和用户参与度。


一些平台也开始使用机器学习和人工智能技术来提供个性化评论推荐,以改善用户体验。


总的来说,评论系统是在线社交和内容互动的重要组成部分,希望看过这篇文章之后,大家以后知道如何应对类似的公关危机,到时候记得回来给我点赞。


什么?你想现在就分享、点赞,加入在看啊!


那你一定是社交领域的优质用户,如果直播间都是你这样的观众,评论系统设计成什么样已经不重要了!Love And Peace ❤



当然,前提是老板们都得时刻反思找找自己的原因,这么多年了有没有认真工作,有没有给打工人涨涨工资 🐶



作者:xin猿意码
来源:juejin.cn/post/7278592935468924963
收起阅读 »

一个烂分页,踩了三个坑!

你好呀,我是歪歪。 前段时间踩到一个比较无语的生产 BUG,严格来说其实也不能算是 BUG,只能说开发同事对于业务同事的需求理解没有到位。 这个 BUG 其实和分页没有任何关系,但是当我去排查问题的时候,我看了一眼 SQL ,大概是这样的: select *...
继续阅读 »

你好呀,我是歪歪。


前段时间踩到一个比较无语的生产 BUG,严格来说其实也不能算是 BUG,只能说开发同事对于业务同事的需求理解没有到位。


这个 BUG 其实和分页没有任何关系,但是当我去排查问题的时候,我看了一眼 SQL ,大概是这样的:



select * from table order by priority limit 1;



priority,就是优先级的意思。


按照优先级 order by 然后 limit 取优先级最高(数字越小,优先级越高)的第一条 ,结合业务背景和数据库里面的数据,我立马就意识到了问题所在。


想起了我当年在写分页逻辑的时候,虽然场景和这个完全不一样,但是踩过到底层原理一模一样的坑,这玩意印象深刻,所以立马就识别出来了。


借着这个问题,也盘点一下我遇到过的三个关于分页查询有意思的坑。


职业生涯的第一个生产 BUG


歪师傅职业生涯的第一个生产 BUG 就是一个小小的分页查询。


当时还在做支付系统,接手的一个需求也很简单就是做一个定时任务,定时把数据库里面状态为初始化的订单查询出来,调用另一个服务提供的接口查询订单的状态并更新。


由于流程上有数据强校验,不用考虑数据不存在的情况。所以该接口可能返回的状态只有三种:成功,失败,处理中。


很简单,很常规的一个需求对吧,我分分钟就能写出伪代码:


//获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
//select * from order where order_status=0;
ArrayList initOrderInfoList = queryInitOrderInfoList();
//循环处理这批数据
for(OrderInfo orderInfo : initOrderInfoList){
    //捕获异常以免一条数据错误导致循环结束
    try{
        //发起rpc调用
        String orderStatus = queryOrderStatus(orderInfo.getOrderId);
        //更新订单状态
        updateOrderInfo(orderInfo.getOrderId,orderStatus);    
    } catch (Exception e){
        //打印异常
    }
}

来,你说上面这个程序有什么问题?



其实在绝大部分情况下都没啥大问题,数据量不多的情况下程序跑起来没有任何毛病。


但是,如果数据量多起来了,一次性把所有初始化状态的订单都拿出来,是不是有点不合理了,万一把内存给你撑爆了怎么办?


所以,在我已知数据量会很大的情况下,我采取了分批次获取数据的模式,假设一次性取 100 条数据出来玩。


那么 SQL 就是这样的:



select * from order where order_status=0 order by create_time limit 100;



所以上面的伪代码会变成这样:


while(true){
    //获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
    //select * from order where order_status=0 order by create_time limit 100;
    ArrayList initOrderInfoList = queryInitOrderInfoList();
    //循环处理这批数据
    for(OrderInfo orderInfo : initOrderInfoList){
        //捕获异常以免一条数据错误导致循环结束
        try{
            //发起rpc调用
            String orderStatus = queryOrderStatus(orderInfo.getOrderId);
            //更新订单状态
            updateOrderInfo(orderInfo.getOrderId,orderStatus);    
        } catch (Exception e){
            //打印异常
        }
    }
}

来,你又来告诉我上面这一段逻辑有什么问题?



作为程序员,我们看到 while(true) 这样的写法立马就要警报拉满,看看有没有死循环的风险。


那你说上面这段代码在什么时候退不出来?


当有任何一条数据的状态没有从初始化变成成功、失败或者处理中的时候,就会导致一直循环。


而虽然发起 RPC 调用的地方,服务提供方能确保返回的状态一定是成功、失败、处理中这三者之中的一个,但是这个有一个前提是接口调用正常的情况下。


如果接口调用一旦异常,那么按照上面的写法,在抛出异常后,状态并未发生变化,还会是停留在“初始化”,从而导致死循环。


当年,测试同学在测试阶段直接就测出了这个问题,然后我对其进行了修改。


我改变了思路,把每次分批次查询 100 条数据,修改为了分页查询,引入了 PageHelper 插件:


//是否是最后一页
while(pageInfo.isLastPage){
    pageNum=pageNum+1;
    //获取订单状态为初始化的数据(0:初始化 1:处理中 2:成功 3:失败)
    //select * from order where order_status=0 order by create_time limit pageNum*100,100;
    PageHelper.startPage(pageNum,100);
    ArrayList initOrderInfoList = queryInitOrderInfoList();
    pageInfo = new PageInfo(initOrderInfoList);
    //循环处理这批数据
    for(OrderInfo orderInfo : initOrderInfoList){
        //捕获异常以免一条数据错误导致循环结束
        try{
            //发起rpc调用
            String orderStatus = queryOrderStatus(orderInfo.getOrderId);
            //更新订单状态
            updateOrderInfo(orderInfo.getOrderId,orderStatus);    
        } catch (Exception e){
            //打印异常
        }
    }
}

跳出循环的条件为判断当前页是否是最后一页。


由于每循环一次,当前页就加一,那么理论上讲一定会是翻到最后一页的,没有任何毛病,对不对?


我们可以分析一下上面的代码逻辑。


假设,我们有 120 条 order_status=0 的数据。


那么第一页,取出了 100 条数据:



SELECT * from order_info WHERE order_status=0 LIMIT 0,100;



这 100 条处理完成之后,第二页还有数据吗?


第二页对应的 sql 为:



SELECT * from order_info WHERE order_status=0 LIMIT 100,100;



但是这个时候,状态为 0 的数据,只有 20 条了,而分页要从第 100 条开始,是不是获取不到数据,导致遗漏数据了?


确实一定会翻到最后一页,解决了死循环的问题,但又有大量的数据遗漏怎么办呢?



当时我苦思冥想,想到一个办法:导致数据遗漏的原因是因为我在翻页的时候,数据状态在变化,导致总体数据在变化。


那么如果我每次都从后往前取数据,每次都固定取最后一页,能取到数据就代表还有数据要处理,循环结束条件修改为“当前页即是第一页,也是最后一页时”就结束,这样不就不会遗漏数据了?


我再给你分析一下。


假设,我们有 120 条 order_status=0 的数据,从后往前取了 100 天出来进行出来,有 90 条处理成功,10 条的状态还是停留在“处理中”。


第二次再取的时候,会把剩下的 20 条和这次“处理中”的 10 条,共计 30 条再次取出来进行处理。


确保没有数据遗漏。


后来测试环节验收通过了,这个方案上线之后,也确实没有遗漏过数据了。


直到后来又一天,提供 queryOrderStatus 接口的服务异常了,我发过去的请求超时了。


导致我取出来的数据,每一条都会抛出异常,都不会更新状态。从而导致我每次从后往前取数据,都取到的是同一批数据。


从程序上的表现上看,日志疯狂的打印,但是其实一直在处理同一批,就是死循环了。


好在我当时还在新手保护期,领导帮我扛下来了。


最后随着业务的发展,这块逻辑也完全发生了变化,逻辑由我们主动去调用 RPC 接口查询状态变成了,下游状态变化后进行 MQ 主动通知,所以我这一坨骚代码也就随之光荣下岗。


我现在想了一下,其实这个场景,用分页的思想去取数据真的不好做。


还不如用最开始的分批次的思想,只不过在会变化的“状态”之外,再加上另外一个不会改变的限定条件,比如常见的创建时间:



select * from order where order_status=0 and create_time>xxx order by create_time limit 100;



最好不要基于状态去做分页,如果一定要基于状态去做分页,那么要确保状态在分页逻辑里面会扭转下去。


这就是我职业生涯的第一个生产 BUG,一个低级的分页逻辑错误。


还是分页,又踩到坑


这也是在工作的前两年遇到的一个关于分页的坑。


最开始在学校的时候,大家肯定都手撸过分页逻辑,自己去算总页数,当前页,页面大小啥的。


当时功力尚浅,觉得这部分逻辑写起来是真复杂,但是扣扣脑袋也还是可以写出来。


后来参加工作了之后,在项目里面看到了 PageHelper 这个玩意,了解之后发了“斯国一”的惊叹:有了这玩意,谁还手写分页啊。



但是我在使用 PageHelper 的时候,也踩到过一个经典的“坑”。


最开始的时候,代码是这样的:


PageHelper.startPage(pageNum,100);
List<OrderInfo> list = orderInfoMapper.select(param1);

后来为了避免不带 where 条件的全表查询,我把代码修改成了这样:


PageHelper.startPage(pageNum,100);
if(param != null){
    List<OrderInfo> list = orderInfoMapper.select(param);
}

然后,随着程序的迭代,就出 BUG 了。因为有的业务场景下,param 参数一路传递进来之后就变成了 null。


但是这个时候 PageHelper 已经在当前线程的 ThreadLocal 里面设置了分页参数了,但是没有被消费,这个参数就会一直保留在这个线程上,也就是放在线程的 ThreadLocal 里面。


当这个线程继续往后跑,或者被复用的时候,遇到一条 SQL 语句时,就可能导致不该分页的方法去消费这个分页参数,产生了莫名其妙的分页。


所以,上面这个代码,应该写成下面这个样子:


if(param != null){
    PageHelper.startPage(pageNum,100);
    List<OrderInfo> list = orderInfoMapper.select(param);
}

也是这次踩坑之后,我翻阅了 PageHelper 的源码,了解了底层原理,并总结了一句话:需要保证在 PageHelper 方法调用后紧跟 MyBatis 查询方法,否则会污染线程。


在正确使用 PageHelper 的情况下,其插件内部,会在 finally 代码段中自动清除了在 ThreadLocal 中存储的对象。


这样就不会留坑。


这次翻页源码的过程影响也是比较深刻的,虽然那个时候经验不多,但是得益于 MyBatis 的源码和 PageHelper 的源码写的都非常的符合正常人的思维,阅读起来门槛不高,再加上我有具体的疑问,所以那是一次古早时期,尚在新手村时,为数不多的,阅读源码之后,感觉收获满满的经历。


分页丢数据


关于这个 BUG 可以说是印象深刻了。


当年遇到这个坑的时候排查了很长时间没啥头绪,最后还是组里的大佬指了条路。


业务需求很简单,就是在管理页面上可以查询订单列表,查询结果按照订单的创建时间倒序排序。


对应的分页 SQL 很简单,很常规,没有任何问题:



select * from table order by create_time desc limit 0,10;



但是当年在页面上的表现大概是这样的:



订单编号为 5 的这条数据,会同时出现在了第一页和第二页。


甚至有的数据在第二页出现了之后,在第五页又出现一次。


后来定位到产生这个问题的原因是因为有一批数量不小的订单数据是通过线下执行 SQL 的方式导入的。


而导入的这一批数据,写 SQL 的同学为了方便,就把 create_time 都设置为了同一个值,比如都设置为了 2023-09-10 12:34:56 这个时间。


由于 create_time 又是我作为 order by 的字段,当这个字段的值大量都是同一个值的时候,就会导致上面的一条数据在不同的页面上多次出现的情况。


针对这个现象,当时组里的大佬分析明白之后,扔给我一个链接:



dev.mysql.com/doc/refman/…



这是 MySQL 官方文档,这一章节叫做“对 Limit 查询的优化”。


开篇的时候人家就是这样说的:



如果将 LIMIT row_count 和 ORDER BY 组合在一起,那么 MySQL 在找到排序结果的第一行 count 行时就停止排序,而不是对整个结果进行排序。


然后给了这一段补充说明:



如果多条记录的 ORDER BY 列中有相同的值,服务器可以自由地按任何顺序返回这些记录,并可能根据整体执行计划的不同而采取不同的方式。


换句话说,相对于未排序列,这些记录的排序顺序是 nondeterministic 的:



然后官方给了一个示例。


首先,不带 limit 的时候查询结果是这样的:



基于这个结果,如果我要取前五条数据,对应的 id 应该是 1,5,3,4,6。


但是当我们带着 limit 的时候查询结果可能是这样的:



对应的 id 实际是 1,5,4,3,6。


这就是前面说的:如果多条记录的 ORDER BY 列中有相同的值,服务器可以自由地按任何顺序返回这些记录,并可能根据整体执行计划的不同而采取不同的方式。


从程序上的表现上来看,结果就是 nondeterministic。


所以看到这里,我们大概可以知道我前面遇到的分页问题的原因是因为那一批手动插入的数据对应的 create_time 字段都是一样的,而 MySQL 这边又对 Limit 参数做了优化,运行结果出现了不确定性,从而页面上出现了重复的数据。


而回到文章最开始的这个 SQL,也就是我一眼看出问题的这个 SQL:



select * from table order by priority limit 1;



因为在我们的界面上,只是约定了数字越小优先级越高,数字必须大于 0。


所以当大家在输入优先级的时候,大部分情况下都默认自己编辑的数据对应的优先级最高,也就是设置为 1,从而导致数据库里面有大量的优先级为 1 的数据。


而程序每次处理,又只会按照优先级排序只会,取一条数据出来进行处理。


经过前面的分析我们可以知道,这样取出来的数据,不一定每次都一样。


所以由于有这段代码的存在,导致业务上的表现就很奇怪,明明是一模一样的请求参数,但是最终返回的结果可能不相同。


好,现在,我问你,你说在前面,我给出的这样的分页查询的 SQL 语句有没有毛病?



select * from table order by create_time desc limit 0,10;



没有任何毛病嘛,执行结果也没有任何毛病?


有没有给你按照 create_time 排序?


摸着良心说,是有的。


有没有给你取出排序后的 10 条数据?


也是有的。


所以,针对这种现象,官方的态度是:我没错!在我的概念里面,没有“分页”这样的玩意,你通过组合我提供的功能,搞出了“分页”这种业务场景,现在业务场景出问题了,你反过来说我底层有问题?


这不是欺负老实人吗?我没错!



所以,官方把这两种案例都拿出来,并且强调:



在每种情况下,查询结果都是按 ORDER BY 的列进行排序的,这样的结果是符合 SQL 标准的。




虽然我没错,但是我还是可以给你指个路。


如果你非常在意执行结果的顺序,那么在 ORDER BY 子句中包含一个额外的列,以确保顺序具有确定性。


例如,如果 id 值是唯一的,你可以通过这样的排序使给定类别值的行按 id 顺序出现。


你这样去写,排序的时候加个 id 字段,就稳了:



好了,如果觉得本文对你有帮助的话,求个免费的点赞,不过分吧?


作者:why技术
来源:juejin.cn/post/7277187894870671360
收起阅读 »

懂点心理学 - 奶头乐效应

Ivy:今天事情真多,有点小沮丧。 Jimmy:要不一起玩局游戏 Ivy:赞同 然后,游戏一局接着一局玩 🐶 囧 奶头乐是什么 奶头乐泛指那一类让人着迷、低成本又使人满足的低俗娱乐内容。奶头乐理论是用来描述一个设想:由于生产力的不断上升,世界上的一大部...
继续阅读 »

  • Ivy:今天事情真多,有点小沮丧。

  • Jimmy:要不一起玩局游戏

  • Ivy:赞同



然后,游戏一局接着一局玩 🐶



pexels-cottonbro-studio-3945683.jpg



奶头乐是什么


奶头乐泛指那一类让人着迷、低成本又使人满足的低俗娱乐内容。奶头乐理论是用来描述一个设想:由于生产力的不断上升,世界上的一大部分人口将会不用也无法积极参与产品和服务的生产,为了安慰这些人,他们的生活应该被大量的娱乐活动(比如网络、电视和游戏)填满。



奶头乐 - 英文 tittytainmenttitty(奶头)与 entertainment(娱乐)的组合。



奶头乐的应用


奶头乐在我们的生活中扮演着重要的角色,有消极的作用,也有积极的作用。问题在于,我们应该怎么趋利避害?


最近很火的某音秀才和一笑倾城事件,关注的中老年的都开始幻想着如意郎君和贤惠姨婆,这可害惨了 TA 们。本来就是在闲暇时候看的小段子打发打发时间,不料,都变成精神寄托了,深陷泥潭的不在少数...


1080x2267_64f6cf1f8faaf.jpeg


但是,我们也可以把奶头乐的一些属性(比如让人着迷)玩成有利于我们的发展,比如玩具模型组装:工作了一周时间,存够了薪水,为自己买了一份乐高 - 法国巴黎铁塔。在娱乐的同时,又很好地锻炼了我们的动手能力和记忆力。


法国巴黎积木.png


奶头乐效应,可爱但又可恨。衡量它的好坏,就看站在哪个角度来看。然而,趋利避害才是我们在深陷奶头乐效应的时候,需要清醒认识(但是很难,往往是奶头乐之后,才会清醒认识)。


参考



作者:Jimmy
来源:juejin.cn/post/7276694924136087586
收起阅读 »

说说今年的秋招的情况与感受

目前秋招已经过了一段时间,说下我看到的情况和整体感受,仅限于计算机相关专业。 另外,我感受秋招的视角比较特别,今年做了半年的Java面试辅导副业,我是以一种“秋招面试陪跑”的”准老师“身份切入的。 时隔五年,又跟着十几个同学重新经历了一次秋招,有点儿“爷青回”...
继续阅读 »

目前秋招已经过了一段时间,说下我看到的情况和整体感受,仅限于计算机相关专业。


另外,我感受秋招的视角比较特别,今年做了半年的Java面试辅导副业,我是以一种“秋招面试陪跑”的”准老师“身份切入的。


时隔五年,又跟着十几个同学重新经历了一次秋招,有点儿“爷青回”的感觉。


对比去年,依然普天同庆(ai hong bian ye)


很多同学觉得今年疫情阴霾散去,经济复苏,形势一片转好,他们甚至动了冲一冲大厂的念头。但真的到了秋招的时候才发现,24秋招 = 没有迪子的23秋招,转而发下重誓,不进体制誓不为人。


不得不说,那些年,我们一起甩锅的疫情,这次终于自证清白了。


其实,经历了过往十年移动互联网的高速发展,目前流量红利已经见顶,进入到了增长受限的存量时代,这些互联网大厂对于人才的需求远不如以前那么强烈了,甚至从去年就开始一波又一波的“去肥增瘦”。



说完了需求侧,我们再用一张图来说说供给侧,2024年高校毕业生人数达到了1187万人,大概是1000万多点儿的国内毕业生和100多万海归毕业生的总盘子。


理性地思考一下,需求侧的日益饱和 + 供给侧的井喷之势 + 选专业时的追涨杀跌 + 转码时的后知后觉,今年还能形势转好?


我信你个鬼!


再说几个方面的细节:




  • 今年无论你是多牛逼的硕,只要本科不是211 985,有的大厂连笔试机会都不给。




  • 今年的大厂面试官,会因为你没有大厂实习经历而挂掉你,不再培养优秀人才了,希望开箱即用。




  • 今年的技术面真难,如果你回答不好生产环境的压测方案,以及流量激增100倍的解决方案,会直接挂掉你。




  • 今年力扣的算法原题越来越少了,今年的八股文考查源码的越来越多了。




  • 今年貌似没有985保底公司。




  • 3个985本硕目前投了七八十家公司,约面的只有十家,其他的均显示“简历评估中”,感觉企业根本不着急。




你大爷还是你大爷


那种学历牛逼,有大厂实习经历,有参赛获奖经历,技术功底扎实行业内的牛逼人才,依然是大厂offer收割机。



对于这类同学,只要在面试准备期别走偏,只要能正常发挥应有水平,只要别中二地犯了面试官的忌讳,他们能完全摆脱大环境萧条的左右。


接下来他们要做的事情就是选择取舍了,有句话说得还是很有道理的,“选择大于努力,命运大于选择”。


强烈建议,其中的那些能力出众、足够努力,但不善于选择的小镇做题家们,这个时候一定要多问问人,做到谋定后动,行且坚毅。


颈部同学受影响最大


除非整个行业团灭,否则头部同学永远都是稳如泰山的,而离头部同学差一个档位的颈部同学,则受影响最大。


颈部同学的人物画像大概是:



  • 技术储备出色,也有实习经历,但学历并没那么出色的同学。

  • 985本硕,技术储备一般,无实习经历,项目经历出自黑马或尚硅谷的同学。

  • 211本硕,技术储备尚可,有些中小厂实习经历的同学。

  • 名校海归硕,技术储备与中国式校招不match,边吃凉面边扳正认知的同学。

  • 985本硕,技术储备出色,也有实习经历,但沟通能力存在硬伤的同学。


这类同学是过往互联网黄金十年、企业人才扩招的最大受益者,也是现在行业萧条的最大受害者,跟几年前的学长学姐进行比较,则成为了他们最大的精神内耗。


他们会被面试官花式吊打屡屡凉面,他们所泡的池子是汪洋大海,他们阅尽千帆归来却依然0 offer,他们拿到offer的档位和数量会直线下降,他们拿到offer的薪资也没能实现倒挂上届。



有人会说,如果颈部都被影响了,那中部和尾部的同学不是影响更大了吗?


这个未必,中尾部的同学没有那么强的比较心理,在性格上更加随遇而安和知足常乐,甚至早早做好了“大不了转行,干啥不是干”的准备。


这就验证了,忧天的往往不是杞人。


逆商和复盘能力的最好考查


高考虽然可以复读,但浪费一年大好时光的成本过于庞大,因此其“一战定天下”的属性更加强烈。


而秋招面试,你甚至可以在前面挂99次,但只要有一次面试发挥出色,拿到了心仪公司的offer,你就是100%成功的。


因此,在不断的“凉面”和“挂面”中保持心态平和,不抛弃不放弃,认真做好复盘总结,不断完善自己的知识体系,不断提升自己的认知层次,你下次的面试成功率是会叠加的。


记住,乾坤未定,你我皆是黑马,秋招是对逆商和复盘能力的最好考查。



一场与面试官的心理博弈


高考是拿到考卷后的解题模式,秋招虽然也有笔试,但其只是敲门砖,绝不是终极态。


终极态是在两三个小时的面试过程中,迅速得到几个陌生面试官的肯定与认可,这是有很多前期工作需要准备的,往往会涉及到候选人与面试官的心理猜析和博弈,面试话题和节奏控制与反控制。



如果你设计合理,那么面试官会在不知不觉中陷入到你提前安排好的布局中。


这里举一个简历当中项目选型的例子:


有些同学为了充分体现其做的项目有技术含量,硬往简历上放手写RPC框架、消息队列、分布式缓存、仿滴滴打车,仿MySQL RDBMS之类的。


这有些乍一看挺唬人,但其实给自己埋下了不小的坑。因为这种颇具技术含量的项目,最大的问题就是它的深度和广度不收敛,你很难hold住。


所以,除非你确实深谙此道,否则就等着被各家公司的面试官,以各种姿势花式吊打吧。


而那些聪明的,善于与面试官博弈的同学,早就准备好了几个有些难度、但技术深度和广度可控的项目。在面试的时候,他就可以顺理成章地将面试官带入到自己所熟悉的八股文技术点中,最终成为了offer收割机。


写给那些心态崩了的同学




  • 有人说,秋招让他明白,读书的目的只是为了换文凭;




  • 有人说,秋招让他明白,人真的要学会接受自己的普通,要学会取悦自己;




  • 有人说,秋招让他明白,倘若是因为读书而耽误了正事,那么读书就是玩物丧志;




  • 有人说,秋招是应届生的头等大事,一旦错过了秋招,我的人生完蛋了;




对于那些心态崩了的同学,我要说的是:


人生中最辉煌的时刻,绝对不是你功成名就的那天,而是你坠入绝望之谷后,重新燃起挑战人生的欲望,再次义无反顾地踏上征程的那天。


作者:库森学长
来源:juejin.cn/post/7279313746450530315
收起阅读 »

为什么5.225.toFixed(2)!=5.23,令人摸不着头脑的银行家舍入法

web
前言 很多时候,我们在程序中计算数字,得到的结果也许并不和我们想象得一样,在我们大多数人的认知里几乎都是四舍五入法,但是程序中所呈现的好像并不是我们想要的结果。 今天就谈谈程序中的那匪夷所思得银行家舍入法(也会涉及到数字精度问题) 什么是银行家舍入法 银行家舍...
继续阅读 »

前言


很多时候,我们在程序中计算数字,得到的结果也许并不和我们想象得一样,在我们大多数人的认知里几乎都是四舍五入法,但是程序中所呈现的好像并不是我们想要的结果。

今天就谈谈程序中的那匪夷所思得银行家舍入法(也会涉及到数字精度问题)


什么是银行家舍入法


银行家舍入法,也称为四舍六入五留双或四舍六入五成双,是一种在计算机科学和金融领域广泛使用的舍入方法。


具体操作步骤如下:



  1. 如果被修约的数字小于5,则直接舍去;

  2. 如果被修约的数字大于5,则进行进位;

  3. 如果被修约的数字等于5,则需要查看5前面的数字。如果5前面的数字是奇数,则进位;如果5前面的数字是偶数,则舍去5,即修约后末尾数字都成为偶数。特别需要注意的是,如果5的后面还有不为0的任何数,则无论5的前面是奇数还是偶数,均应进位。


以上可以看出银行家舍入法得规则,当为5时,并不是所有得都会向前进一位,所以就可以知道5.225.toFixed(2)为什么不等于5.23了


举例


在浏览器的控制台中,我们可以试着打印一下


image.png
这个时候我们可以看到,哎,好像是符合我们的所认知得四舍五入法了,但是紧接着


image.png
这里看出,怎么又变成这样的了,这还是银行家舍入法呀,为了更严谨再试一下5前面为奇数时得结果


image.png
这里结果又变了,反而是整数大于等于4得正常了,但是小于4得又有些失常了,反而整数为1得总是按照咱们预想的结果在进行,这种结果让我大脑一片混乱,所以这到底是什么原因,导致结果不像是银行家舍入法,也不像是四舍五入法


在我掉了一花西币的头发后,终于想通了,是程序中的精度问题,我们所写的数字并不是表面那么纯粹,再次打印一下看看


image.png
现在可以清楚看出,我们所写的简单的数字后面并不见简单,之所以1.235和1.225使用toFiexd的时候都准确的四舍五入了,都是因为他的后面是多出来了0.0000000000几的数字,然而2.235就没有那么幸运了,所以2.235的0.005就被舍弃了!


解决方法


先说一种可行但不完全可行的解决方法,就是使用Math.round()
首先这个方法确实是js中提供的真正含义上的四舍五入的方法。


image.png
哎,这么一看,确实可行,既然简单的可以,那我们就试着进行复杂运算一下,再保留一下两位小数试试看


image.png
呕吼,错了,按我们正常来算应该是9.77,但却得到了9.76。

要知道程序中存在着精度问题,再我们算来这个式子的结果应该是9.765,但是在程序看来


image.png
可以说是无限趋近于9.765但还没有达到,然后就在Math.round这个方法中给舍弃掉了,这个方法似乎不完全可行


那么另外一招就是可行但有隐式风险的方式,就是在我们所算出来的结果后面添加0.0000000001,这样再让我们看一下结果


image.png
这样可以看出,无论使用哪种方法,都能达到我们所需的结果了,即使使用toFixed有了银行家舍入法的规则,依旧可以按我们所想的一样进行四舍五入,因为当我们加了0.000000001后,即使最后一位等于5了,5后面还有数字,它就会向前进一位,那如果说加了这0.000000001正好等于5然后又触发了银行家舍入法的规则,那只能说算你倒霉,这就是我说为什么会有隐式风险,有风险但很小。


当然还有一个方法就是自己写一个方法来解决这个问题


//有的时候也许传的参数就是计算过后的,无线趋近于5的数,可以根据需求来判断是否传入第二个参数
Number.prototype.myToFixed = function (n, d) {
//进来之后转为字符串 字符串不存在精度问题
const str = this.toString();
const dotIndex = str.indexOf(".");
//如果没有小数点传进来的就是整数,直接使用toFixed传出去
if (dotIndex === -1) {
return this.toFixed(n);
}
//当为小数的时候
const intStr = str.substring(0, dotIndex);
const decStr = str.substring(dotIndex + 1, str.length).split("");
//当大于5时,就进一
if (decStr[n] >= 5) {
decStr[n - 1] = Number(decStr[n - 1]) + 1;
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
} else {
//否则小于五时 先判断是否有第二个参数
if (d) {
//如果有就截取到第二个参数的位置
const newDec = decStr.splice(n, n + d);
let nineSum = 0;
//遍历循环有多少个9
for (let index = 0; index < newDec.length; index++) {
if (index != 0 && newDec[index] == 9) {
nineSum++;
}
}
//判断四舍五入后面的位置 是否为四 并且是否除了4之后全是9 或者 9的位数大于第二个传的参数
if (newDec[0] == 4 && (nineSum >= newDec.length - 2 || nineSum >= d)) {
//条件成立 就按5进一
decStr[n - 1] = Number(decStr[n - 1]) + 1;
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
} else {
//不成立则舍一
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
}
} else {
//没有第二个参数,小于五直接舍一
const dec = decStr.slice(0, n).join("");
return `${intStr}.${dec}`;
}
}
};

我们再进行测试一下


image.png


image.png
这样就是我们想要的结果了


总结


在程序中,银行家舍入法和数字的精度问题很多时候都会遇见,不论前端还是后端,然而处理这些数据也是比较头疼的事,我所讲的这些也许不能满足所有情况,但大多数情况都是可以处理的。


如果是相对于银行里这种对数字比较敏感的环境,这些参数的处理还需要更加谨慎的处理


写的如有问题,欢迎提出建议


作者:iceCode
来源:juejin.cn/post/7280430881952759862
收起阅读 »

你知道抖音的IP归属地是怎么实现的吗

1.背景 最近刷抖音发现上线了 IP 属地的功能,小伙伴在发表动态、发表评论以及聊天的时候,都会显示自己的 IP 属地信息,其核心意义是让用户更具有真实性,减少虚假欺骗事件。正好最近本人开发获取客户端ip,做一些接口限流,黑白名单等需求功能,顺路就研究了一下怎...
继续阅读 »

1.背景


最近刷抖音发现上线了 IP 属地的功能,小伙伴在发表动态、发表评论以及聊天的时候,都会显示自己的 IP 属地信息,其核心意义是让用户更具有真实性,减少虚假欺骗事件。正好最近本人开发获取客户端ip,做一些接口限流,黑白名单等需求功能,顺路就研究了一下怎么解析IP获取归属地问题。


接下来,就着重讲解一下Java后端怎么实现IP归属地的功能,其实只需要以下两大步骤:


2.获取客户端ip接口


做过web开发都知道,无论移动端还是pc端的请求接口都会被封装成为一个HttpServletRequest对象,该对象包含了客户端请求信息包括请求的地址,请求的参数,提交的数据等等。


如果服务器直接把IP暴漏出去,那么request.getRemoteAddr()就能拿到客户端ip。


但目前流行的架构中,基本上服务器都不会直接把自己的ip暴漏出去,一般前面还有一层或多层反向代理,常见的nginx居多。 加了代理后,相当于服务器和客户端中间还有一层,这时·request.getRemoteAddr()拿到的就是代理服务器的ip了,并不是客户端的ip。所以这种情况下,一般会在转发头上加X-Forwarded-For等信息,用来跟踪原始客户端的ip。


X-Forwarded-For: 这是一个 Squid 开发的字段,只有在通过了HTTP代理或者负载均衡服务器时才会添加该项。 格式为X-Forwarded-For:client1,proxy1,proxy2,一般情况下,第一个ip为客户端真实ip,后面的为经过的代理服务器ip。 上面的代码注释也说的很清楚,直接截取拿到第一个ip。 Proxy-Client-IP/WL- Proxy-Client-IP: 这个一般是经过apache http服务器的请求才会有,用apache http做代理时一般会加上Proxy-Client-IP请求头,而WL-Proxy-Client-IP是他的weblogic插件加上的头。这种情况也是直接能拿到。 HTTP_CLIENT_IP: 有些代理服务器也会加上此请求头。 X-Real-IP: nginx一般用这个。


但是在日常开发中,并没有规范规定用以上哪一个头信息去跟踪客户端,所以都有可能,只能一一尝试,直到获取到为止。代码如下:


@Slf4j
public class IpUtils {

   private static final String UNKNOWN_VALUE = "unknown";
   private static final String LOCALHOST_V4 = "127.0.0.1";
   private static final String LOCALHOST_V6 = "0:0:0:0:0:0:0:1";

   private static final String X_FORWARDED_FOR = "X-Forwarded-For";
   private static final String X_REAL_IP = "X-Real-IP";
   private static final String PROXY_CLIENT_IP = "Proxy-Client-IP";
   private static final String WL_PROXY_CLIENT_IP = "WL-Proxy-Client-IP";
   private static final String HTTP_CLIENT_IP = "HTTP_CLIENT_IP";

   private static final String IP_DATA_PATH = "/Users/shepherdmy/Desktop/ip2region.xdb";
   private static  byte[] contentBuff;
 
  /**
    * 获取客户端ip地址
    * @param request
    * @return
    */
   public static String getRemoteHost(HttpServletRequest request) {
       String ip = request.getHeader(X_FORWARDED_FOR);
       if (StringUtils.isNotEmpty(ip) && !UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           // 多次反向代理后会有多个ip值,第一个ip才是真实ip
           int index = ip.indexOf(",");
           if (index != -1) {
               return ip.substring(0, index);
          } else {
               return ip;
          }
      }
       ip = request.getHeader(X_REAL_IP);
       if (StringUtils.isNotEmpty(ip) && !UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           return ip;
      }
       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getHeader(PROXY_CLIENT_IP);
      }
       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getHeader(WL_PROXY_CLIENT_IP);
      }
       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getRemoteAddr();
      }

       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getHeader(HTTP_CLIENT_IP);
      }

       if (StringUtils.isBlank(ip) || UNKNOWN_VALUE.equalsIgnoreCase(ip)) {
           ip = request.getRemoteAddr();
      }
       return ip.equals(LOCALHOST_V6) ? LOCALHOST_V4 : ip;
  }

}


项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用


Github地址github.com/plasticene/…


Gitee地址gitee.com/plasticene3…


微信公众号Shepherd进阶笔记


交流探讨群:Shepherd_126



3.获取ip归属地


通过上面我们就能获取到客户端用户的ip地址,接下来就可以通过ip解析获取归属地了。


如果我们在网上搜索资料教程,大部分都是说基于各大平台(eg:淘宝,新浪)提供的ip库进行查询,不过不难发现这些平台已经不怎么维护这个功能,现在处于“半死不活”的状态,根本不靠谱,当然有些平台提供可靠的获取ip属地接口,但是收费、收费、收费


本着作为一个程序员的严谨:“能白嫖的就白嫖,避免出现要买的是你,不会用也是你的尴尬遭遇”。扯远了言归正传,为了寻求可靠有效的解决方案,只能去看看github有没有什么项目能满足需求,果然功夫不负有心人,发现一个宝藏级项目:ip2region,一个准确率 99.9% 的离线 IP 地址定位库,0.0x 毫秒级查询,ip2region.db 数据库只有数 MB的项目,提供了众多主流编程语言的 xdb 数据生成和查询客户端实现,这里只能说:开源真香,开源万岁。


3.1 Ip2region 特性


标准化的数据格式


每个 ip 数据段的 region 信息都固定了格式:国家|区域|省份|城市|ISP,只有中国的数据绝大部分精确到了城市,其他国家部分数据只能定位到国家,其余选项全部是0。


数据去重和压缩


xdb 格式生成程序会自动去重和压缩部分数据,默认的全部 IP 数据,生成的 ip2region.xdb 数据库是 11MiB,随着数据的详细度增加数据库的大小也慢慢增大。


极速查询响应


即使是完全基于 xdb 文件的查询,单次查询响应时间在十微秒级别,可通过如下两种方式开启内存加速查询:



  1. vIndex 索引缓存 :使用固定的 512KiB 的内存空间缓存 vector index 数据,减少一次 IO 磁盘操作,保持平均查询效率稳定在10-20微秒之间。

  2. xdb 整个文件缓存:将整个 xdb 文件全部加载到内存,内存占用等同于 xdb 文件大小,无磁盘 IO 操作,保持微秒级别的查询效率。


IP 数据管理框架


v2.0 格式的 xdb 支持亿级别的 IP 数据段行数,region 信息也可以完全自定义,例如:你可以在 region 中追加特定业务需求的数据,例如:GPS信息/国际统一地域信息编码/邮编等。也就是你完全可以使用 ip2region 来管理你自己的 IP 定位数据。


99.9% 准确率


数据聚合了一些知名 ip 到地名查询提供商的数据,这些是他们官方的的准确率,经测试着实比经典的纯真 IP 定位准确一些。


ip2region 的数据聚合自以下服务商的开放 API 或者数据(升级程序每秒请求次数 2 到 4 次):



备注:如果上述开放 API 或者数据都不给开放数据时 ip2region 将停止数据的更新服务。


3.2 整合Ip2region客户端进行查询


提供了众多主流编程语言的 xdb 数据生成和查询客户端实现,已经集成的客户端有:java、C#、php、c、python、nodejs、php扩展(php5 和 php7)、golang、rust、lua、lua_c,nginx。这里讲一下java的客户端。


首先我们需要引入依赖:


<dependency>
 <groupId>org.lionsoul</groupId>
 <artifactId>ip2region</artifactId>
 <version>2.6.5</version>
</dependency>

接下来我们需要先去下载数据文件ip2region.xdb到本地,然后基于数据文件进行查询,下面查询方法文件路径改为你本地路径即可,ip2region提供三种查询方式:


完全基于文件的查询


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
   public static void main(String[] args) {
       // 1、创建 searcher 对象
       String dbPath = "ip2region.xdb file path";
       Searcher searcher = null;
       try {
           searcher = Searcher.newWithFileOnly(dbPath);
      } catch (IOException e) {
           System.out.printf("failed to create searcher with `%s`: %s\n", dbPath, e);
           return;
      }

       // 2、查询
       try {
           String ip = "1.2.3.4";
           long sTime = System.nanoTime();
           String region = searcher.search(ip);
           long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
           System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
      } catch (Exception e) {
           System.out.printf("failed to search(%s): %s\n", ip, e);
      }

       // 3、关闭资源
       searcher.close();
       
       // 备注:并发使用,每个线程需要创建一个独立的 searcher 对象单独使用。
  }
}

缓存 VectorIndex 索引


我们可以提前从 xdb 文件中加载出来 VectorIndex 数据,然后全局缓存,每次创建 Searcher 对象的时候使用全局的 VectorIndex 缓存可以减少一次固定的 IO 操作,从而加速查询,减少 IO 压力。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
   public static void main(String[] args) {
       String dbPath = "ip2region.xdb file path";

       // 1、从 dbPath 中预先加载 VectorIndex 缓存,并且把这个得到的数据作为全局变量,后续反复使用。
       byte[] vIndex;
       try {
           vIndex = Searcher.loadVectorIndexFromFile(dbPath);
      } catch (Exception e) {
           System.out.printf("failed to load vector index from `%s`: %s\n", dbPath, e);
           return;
      }

       // 2、使用全局的 vIndex 创建带 VectorIndex 缓存的查询对象。
       Searcher searcher;
       try {
           searcher = Searcher.newWithVectorIndex(dbPath, vIndex);
      } catch (Exception e) {
           System.out.printf("failed to create vectorIndex cached searcher with `%s`: %s\n", dbPath, e);
           return;
      }

       // 3、查询
       try {
           String ip = "1.2.3.4";
           long sTime = System.nanoTime();
           String region = searcher.search(ip);
           long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
           System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
      } catch (Exception e) {
           System.out.printf("failed to search(%s): %s\n", ip, e);
      }
       
       // 4、关闭资源
       searcher.close();

       // 备注:每个线程需要单独创建一个独立的 Searcher 对象,但是都共享全局的制度 vIndex 缓存。
  }
}

缓存整个 xdb 数据


我们也可以预先加载整个 ip2region.xdb 的数据到内存,然后基于这个数据创建查询对象来实现完全基于文件的查询,类似之前的 memory search。


import org.lionsoul.ip2region.xdb.Searcher;
import java.io.*;
import java.util.concurrent.TimeUnit;

public class SearcherTest {
   public static void main(String[] args) {
       String dbPath = "ip2region.xdb file path";

       // 1、从 dbPath 加载整个 xdb 到内存。
       byte[] cBuff;
       try {
           cBuff = Searcher.loadContentFromFile(dbPath);
      } catch (Exception e) {
           System.out.printf("failed to load content from `%s`: %s\n", dbPath, e);
           return;
      }

       // 2、使用上述的 cBuff 创建一个完全基于内存的查询对象。
       Searcher searcher;
       try {
           searcher = Searcher.newWithBuffer(cBuff);
      } catch (Exception e) {
           System.out.printf("failed to create content cached searcher: %s\n", e);
           return;
      }

       // 3、查询
       try {
           String ip = "1.2.3.4";
           long sTime = System.nanoTime();
           String region = searcher.search(ip);
           long cost = TimeUnit.NANOSECONDS.toMicros((long) (System.nanoTime() - sTime));
           System.out.printf("{region: %s, ioCount: %d, took: %d μs}\n", region, searcher.getIOCount(), cost);
      } catch (Exception e) {
           System.out.printf("failed to search(%s): %s\n", ip, e);
      }
       
       // 4、关闭资源 - 该 searcher 对象可以安全用于并发,等整个服务关闭的时候再关闭 searcher
       // searcher.close();

       // 备注:并发使用,用整个 xdb 数据缓存创建的查询对象可以安全的用于并发,也就是你可以把这个 searcher 对象做成全局对象去跨线程访问。
  }
}

3.3 springboot整合示例


首先我们也需要像上面一样引入maven依赖。然后就可以基于上面的查询方式进行封装成工具类了,我这里选择了上面的第三种方式:缓存整个 xdb 数据


@Slf4j
public class IpUtils {
   private static final String IP_DATA_PATH = "/Users/shepherdmy/Desktop/ip2region.xdb";
   private static  byte[] contentBuff;

   static {
       try {
           // 从 dbPath 加载整个 xdb 到内存。
           contentBuff = Searcher.loadContentFromFile(IP_DATA_PATH);
      } catch (IOException e) {
           e.printStackTrace();
      }
  }
 
     /**
    * 根据ip查询归属地,固定格式:中国|0|浙江省|杭州市|电信
    * @param ip
    * @return
    */
   public static IpRegion getIpRegion(String ip) {
       Searcher searcher = null;
       IpRegion ipRegion = new IpRegion();
       try {
           searcher = Searcher.newWithBuffer(contentBuff);
           String region = searcher.search(ip);
           String[] info = StringUtils.split(region, "|");
           ipRegion.setCountry(info[0]);
           ipRegion.setArea(info[1]);
           ipRegion.setProvince(info[2]);
           ipRegion.setCity(info[3]);
           ipRegion.setIsp(info[4]);
      } catch (Exception e) {
           log.error("get ip region error: ", e);
      } finally {
           if (searcher != null) {
               try {
                   searcher.close();
              } catch (IOException e) {
                   log.error("close searcher error:", e);
              }
          }
      }
       return ipRegion;
  }

}

作者:shepherd111
来源:juejin.cn/post/7280118836685668367
收起阅读 »

Metal每日分享,不同色彩空间转换滤镜效果

iOS
本案例的目的是理解如何用Metal实现色彩空间转换效果滤镜,转换在不同色彩空间生成的图像; Demo HarbethDemo地址iDay每日分享文档地址 实操代码// 色彩空间转换滤镜 let filter = C7ColorSpace.init(with:...
继续阅读 »

本案例的目的是理解如何用Metal实现色彩空间转换效果滤镜,转换在不同色彩空间生成的图像;




Demo



实操代码

// 色彩空间转换滤镜
let filter = C7ColorSpace.init(with: .rgb_to_yuv)

// 方案1:
ImageView.image = try? BoxxIO(element: originImage, filters: [filter, filter2, filter3]).output()

// 方案2:
ImageView.image = originImage.filtering(filter, filter2, filter3)

// 方案3:
ImageView.image = originImage ->> filter ->> filter2 ->> filter3

效果对比图


  • 不同参数下效果

    实现原理


  • 过滤器


这款滤镜采用并行计算编码器设计.compute(kernel: type.rawValue)

/// 色彩空间转换
public struct C7ColorSpace: C7FilterProtocol {

public enum SwapType: String, CaseIterable {
case rgb_to_yiq = "C7ColorSpaceRGB2YIQ"
case yiq_to_rgb = "C7ColorSpaceYIQ2RGB"
case rgb_to_yuv = "C7ColorSpaceRGB2YUV"
case yuv_to_rgb = "C7ColorSpaceYUV2RGB"
}

private let type: SwapType

public var modifier: Modifier {
return .compute(kernel: type.rawValue)
}

public init(with type: SwapType) {
self.type = type
}
}

  • 着色器

每条通道乘以各自偏移求和得到Y,用Y作为新的像素rgb;

kernel void C7ColorSpaceRGB2Y(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half Y = half((0.299 * inColor.r) + (0.587 * inColor.g) + (0.114 * inColor.b));
const half4 outColor = half4(Y, Y, Y, inColor.a);

outputTexture.write(outColor, grid);
}

// See: https://en.wikipedia.org/wiki/YIQ
kernel void C7ColorSpaceRGB2YIQ(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half3x3 RGBtoYIQ = half3x3({0.299, 0.587, 0.114}, {0.596, -0.274, -0.322}, {0.212, -0.523, 0.311});
const half3 yiq = RGBtoYIQ * inColor.rgb;
const half4 outColor = half4(yiq, inColor.a);

outputTexture.write(outColor, grid);
}

kernel void C7ColorSpaceYIQ2RGB(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half3x3 YIQtoRGB = half3x3({1.0, 0.956, 0.621}, {1.0, -0.272, -0.647}, {1.0, -1.105, 1.702});
const half3 rgb = YIQtoRGB * inColor.rgb;
const half4 outColor = half4(rgb, inColor.a);

outputTexture.write(outColor, grid);
}

// See: https://en.wikipedia.org/wiki/YUV
kernel void C7ColorSpaceRGB2YUV(texture2d<half, access::write> outputTexture [[texture(0)]],
texture2d<half, access::read> inputTexture [[texture(1)]],
uint2 grid [[thread_position_in_grid]]) {
const half4 inColor = inputTexture.read(grid);

const half3x3 RGBtoYUV = half3x3({0.299, 0.587, 0.114}, {-0.299, -0.587, 0.886}, {0.701, -0.587, -0.114});
const half3 yuv = RGBtoYUV * inColor.rgb;
const half4 outColor = half4(yuv, inColor.a);

outputTexture.write(outColor, grid);
}

kernel void C7ColorSpaceYUV2RGB(texture2d<half, access::write> outputTexture [[texture(0)]],
                                texture2d<half, access::read> inputTexture [[texture(1)]],
                                uint2 grid [[thread_position_in_grid]]) {
    const half4 inColor = inputTexture.read(grid);

    const half3x3 YUVtoRGB = half3x3({1.0, 0.0, 1.28033}, {1.0, -0.21482, -0.38059}, {1.0, 2.21798, 0.0});
    const half3 rgb = YUVtoRGB * inColor.rgb;
    const half4 outColor = half4(rgb, inColor.a);

    outputTexture.write(outColor, grid);
}

色彩空间


  • YIQ

在YIQ系统中,是NTSC(National Television Standards Committee)电视系统标准;


  • Y是提供黑白电视及彩色电视的亮度信号Luminance,即亮度Brightness;
  • I代表In-phase,色彩从橙色到青色;
  • Q代表Quadrature-phase,色彩从紫色到黄绿色;



转换公式如下:




  • YUV

YUV是在工程师想要在黑白基础设施中使用彩色电视时发明的。他们需要一种信号传输方法,既能与黑白 (B&W) 电视兼容,又能添加颜色。亮度分量已经作为黑白信号存在;他们将紫外线信号作为解决方案添加到其中。

由于 U 和 V 是色差信号,因此在直接 R 和 B 信号上选择色度的 UV 表示。换句话说,U 和 V 信号告诉电视在不改变亮度的情况下改变某个点的颜色。
或者 U 和 V 信号告诉显示器以牺牲另一种颜色为代价使一种颜色更亮,以及它应该移动多少。
U 和 V 值越高(或负值越低),斑点的饱和度(色彩)就越高。
U 值和 V 值越接近零,颜色偏移越小,这意味着红、绿和蓝光的亮度会更均匀,从而产生更灰的点。
这是使用色差信号的好处,即不是告诉颜色有多少红色,而是告诉红色比绿色或蓝色多多少。
反过来,这意味着当 U 和 V 信号为零或不存在时,它只会显示灰度图像。
如果使用 R 和 B,即使在黑白场景中,它们也将具有非零值,需要所有三个数据承载信号。
这在早期的彩色电视中很重要,因为旧的黑白电视信号没有 U 和 V 信号,这意味着彩色电视开箱后只会显示为黑白电视。
此外,黑白接收器可以接收 Y' 信号并忽略 U 和 V 颜色信号,使 YUV 向后兼容所有现有的黑白设备、输入和输出。
如果彩色电视标准不使用色差信号,这可能意味着彩色电视会从 B& 中产生有趣的颜色 W 广播,否则需要额外的电路将黑白信号转换为彩色。
有必要为色度通道分配较窄的带宽,因为没有可用的额外带宽。
如果某些亮度信息是通过色度通道到达的(如果使用 RB 信号而不是差分 UV 信号,就会出现这种情况),黑白分辨率就会受到影响。

YUV 模型定义了一个亮度分量 (Y),表示物理线性空间亮度,以及两个色度分量,分别称为 U(蓝色投影)和 V(红色投影)。它可用于在 RGB 模型之间进行转换,并具有不同的颜色空间




转换公式如下:




最后


  • 慢慢再补充其他相关滤镜,喜欢就给我点个星🌟吧。

✌️.


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

烟雨蒙蒙的三月

金三银四好像失效了 从去年下半年开始,互联网寒冬就总是萦绕在耳边。大量的公司倒闭、裁员。本以为等疫情过,等春天,等金三银四,一切又会变得好起来。但是站在这个本应该金光闪闪的时刻,却是无人问津。那个我们希望的春天似乎没有到来。 你是否也会像我一样焦虑 从业...
继续阅读 »
金三银四好像失效了


从去年下半年开始,互联网寒冬就总是萦绕在耳边。大量的公司倒闭、裁员。本以为等疫情过,等春天,等金三银四,一切又会变得好起来。但是站在这个本应该金光闪闪的时刻,却是无人问津。那个我们希望的春天似乎没有到来。



你是否也会像我一样焦虑


从业六年多,但是最近将近两年都在中间件行业,技术、履历都算不上优秀。年纪每一年都在增长,而我有随着年纪一起快速增长吗?我想是没有的。年后公司部门会议说到部门发展,领导说我们的产品越发稳定,对于一个中间件来说,客户需要的就是稳定,太多功能对于他们来说是无用的。这就意味着我们的产品到头了。但是这个产品到头我们再做什么呢?没人给我们答案。



要跳出当前的圈子吗


如果在这里看不见曙光,那么在别的地方是不是能有希望呢?春天,万物复苏,楼下如枯骨林立的一排排树干纷纷长出绿芽,迸发生机。我是否可以像沉寂了一整个冬天的枯树一样迎来自己的春天呢?目前看来也是没有的。投了很多家的简历,犹如石沉大海了无音讯。不知道对于别人来说是怎样,但是对我而言,这个三月并不是春天。



会有春天吗


去年我第一次为开源社区贡献了自己的代码,我觉得我变得更好了。疫情也在年底宣布画上句号,春天似乎真的要来了。物理上的春天是到来了,可是那个我们期盼的春天它真的会到来吗?总是在期盼等一等一切就会好转,因为除了等,我们似乎也并没有太多的选择。时代的轮盘一直运转,无数人的命运随之沉浮。我们更多的只能逆来顺受,接受它的变化,并随之拥抱它。可是未知的未来总是让人充满惶恐,看看自己再过两年就三十了,未婚、未育。在本就三十五岁魔咒的行业,总是惴惴不安。我总是思考如果被这个行业抛弃,不做开发我又能做什么呢?如果是你,这个答案会是什么呢?



这个文章应该有个结尾


文章总是需要结尾的,生活不是。生活还需要继续,每个人的答案都需要自己去寻找。在茫然无措的时刻,只能自己去寻找一些解药,在心绪不宁的时候,学习什么也是学不进去的。最近在看《我的解放日记》,能够缓解一些我的焦虑情绪。如果你也需要一些治愈系的剧,也可以去看看它。时代的浪潮推着我们往前,我们惶恐不安,手足无措,这都不是我们的错,我们只能尽力做好能做的。但是那些决定命运的瞬间几乎都是我们不能选择的。能活着就已经很不错了。


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

学习能力必然是职场的核心能力

最近新工作的编程语言换为了Golang,同时也在面试招聘相关岗位的人才。通过简历面试(别人的经历),以及自己的亲身学习经历,真切的感受到学习能力将是未来的一大竞争力。 从面试方面来看,大多数人工作稳定之后便失去了学习能力,以为现在的工作可以长久的干下去。结果,...
继续阅读 »

最近新工作的编程语言换为了Golang,同时也在面试招聘相关岗位的人才。通过简历面试(别人的经历),以及自己的亲身学习经历,真切的感受到学习能力将是未来的一大竞争力。


从面试方面来看,大多数人工作稳定之后便失去了学习能力,以为现在的工作可以长久的干下去。结果,互联网的风停下来之后,市场的需求变了,从单一的编程语言、单一业务的能力变成更加综合的能力,需要的人逐渐变为T型人才甚至π型人才。此时,学习能力就变得更加重要。否则,面临的只能是市场的淘汰。


下面分享一下自己最近三周学习Golang的一些经验和方法,大家可以拿来借鉴的其他学习方面上:


第一、实践。任何的学习都离不开实践。不能够运用到实践中的学习大概率是无效学习,而实践也是学习最有效的手段。在刚开学学习Golang时,找了一份基础语法的文档,花一两个小时看了一遍,知道常见的语法结构怎么用的,便开始搭建项目,写业务功能。其实这样的效果最快,以具体的功能实践来驱动学习,同时把对这方面的手感和思路锻炼出来。


第二、系统学习。单纯动手实践的过程中会掺杂着业务逻辑的实现,学习效率和范围上会有一些局限,属于用到什么学什么,缺点是不够系统。这时还需要一两本书,通读全书,帮助系统的了解这门语言(或某个行业)是怎么运作的,整个生态是什么样的,底层逻辑是怎样的,以便查漏补缺。在系统学习这块,建议以书籍为主,书籍的优势就是方便、快捷、系统、准确。


第三、交流。之前找一个懂的大佬请教和交流不是那么容易。但随着AI的发展,交流形式不仅仅限于大佬了,也可以是GPT。GPT最强大的能力是无所不知,知无不言。当然,对于它提供的结果也需要辩证的去看,某些地方可能会有错误,但大方向基本上是没错的,再辅以佐证,基本上能够解决80%的问题。


如果有机会参与面试,无论是作为面试官或者被面试者,都是一个交流的过程。在相互沟通的过程中了解市场需要什么,市场流行什么。


最后,针对某些问题,还是得去跟大佬交流才行,交流的过程中会碰撞出很多火花来。比如,不断的迭代某个算法,学到更好的实现方式,了解到你不知道的知识点等。曾经,一个字符串截取的功能,与大佬交流了三次,升级了三版,也学到了不同的API的使用方法和特性。


第四,输出。检验是否学会的一个标准就是你能否清晰的给别人描述出来,让别人听得懂。这一条是否很耳熟?对,它就是费曼学法,世界公认的最快的学习法。如果没办法很好的表达,说明这块掌握的还不是很清楚。当然,这个过程中也属于交流,也会拿到别人的反馈,根据别人的反馈来认识到自己的掌握程度和薄弱点。


第五,利用别人的时间。个人的时间总是有限的,不可能什么事情都自己做,也不可能都亲手验证。而作为管理者,最大的技能之一就是靠别人、靠团队来实现目标。那么,一个技术方案是否可行,是否有问题,也可以交给别人来调研、实践、验证。这样,可以让学习的效率并行起来。


另外,我们可能都听说过“一万小时定律”,这个概念是极具迷惑性的,会让你觉得学习任何东西都需要花费大量的时间的。其实不然,一万小时定律指的是学习一个复杂的领域并且成为这个领域的专家。


而我们在生活和实践的过程中,往往不需要什么方面都成为专家,只需要知道、掌握或会用某一领域的知识即可。对于入门一个新领域,一般来说,可能只需要20小时、100小时不等,没有想象中那么难。对于一个懂编程语言的人来说,从零学习另外一门语言,一般也就一两周时间就可以上手了。因此,我们不要对此产生畏惧心理。


上面讲的是学习方法,但最根本的是学习的意愿。你是选择花一年时间学习一门技术,然后重复十年,还是愿意每年都不断的学习迭代自己?两者的结果差距超乎你的想象。


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

如何治愈拖延症

如何治愈拖延症 背景 最近发现我的拖延症很严重了😭😭,看了一下我的抖音主页,我已经很久没有去跑步了。最近的一次跑步的记录停留在了8月23日,周三。我的这篇文章写在周天的上午,掐指一算,已经有三天晚上没有跑步了。我不大喜欢给自己找借口,没有行动就是没有行动。 ...
继续阅读 »

如何治愈拖延症


背景


最近发现我的拖延症很严重了😭😭,看了一下我的抖音主页,我已经很久没有去跑步了。最近的一次跑步的记录停留在了8月23日,周三。我的这篇文章写在周天的上午,掐指一算,已经有三天晚上没有跑步了。我不大喜欢给自己找借口,没有行动就是没有行动。




就拿我昨天晚上来说吧,吃完饭已经是8点了,这个点没啥问题。和家里通了半小时的电话之后,发现手机没电了,于是又在充电。等到九点的时候,电池的电量还在30%左右,我知道我的手机电池不大行,不足以支撑一个小时,于是就放弃了😅。


但是当早上我坐在电脑前的时候,发现昨天的好多事情都没有完成,今天的事情又得往后推了。越堆积越是多,都喘不过气来了🤥。



哈哈🤭🤭,也不好意思让大家看到下周的推文内容啦,算是提前剧透了😎





我就不断的在思考,为什么我的执行力不行了。我觉得我的代言词就是:一个有思想有行动力的程序员。现在看来,我是一个懒惰、带有严重的拖延症的程序员了。不行,这个问题得治,不然我会更加的焦虑,堆积更多的任务导致更低的效率。


分析


结合这个低效率的周末,我反思了我为什么效率这么低。


🕢推迟开始


我发现我总喜欢做todo list,但是很少去看,也很少去核对一下我当前的进度。总觉得一天的时间很长,我可以先去做别的事情,比如碎片化的短视频、吃吃吃、发呆。于是一件件的本在计划中的事情被不断的推迟了。


⏲时间管理困难


从我8:00起来到晚上的凌晨入睡,减去我个人清洁、做饭、午睡,我剩下的时间大约是10个小时。但是,我一对比下来,我的时间利用率仅仅是40%,相当于我只有4个小时是在满满当当的学习的。我之前的ipad在的时候,我会用潮汐这个软件把我的时间分割成一个小时一个小时的。现在没了,我发现我的时间规划真的出了大问题。


🤖自我控制力下降


我觉得最近一年的时间,我真的太放松自我了。我的技术成长、学习上长进也是微乎其微。我总结下来就是因为我的自控力太差了,或者说没有承受着外界的干扰。因为一个短视频就可以刷上一个小时的短视频,因为一个好物就会不断的逛购物软件......碎片化的时间消耗,最终导致了效率低下。


解决方案


针对以上我总结的问题,我决定对症下药。


🧾明确的计划


我觉得我明确的计划真的很必要。就像我公众号shigen里面给自己定的一个目标一样:



2023年的8月开始,我先给自己定一个小目标:公众号文章不停更





“不停更”的意思是我每天都要更新文章。我的推文里还带了“新闻早知道”栏目,我哪天没更新或者说更新晚了,我就觉得目标没有实现了,新闻也没什么意义了。我觉得日常的计划和这个目标的设定和实现有着相似的地方,我要把我的计划和目标更明确一点。🤔🤔比方说我今天要干嘛,我完成了怎么样了。


优先级


事情分清楚轻重缓急,我记得我在实习的时候,就有一次因为项目要上线和我一点不大紧要的事情次序搞混了,导致晚上加班上线。现在的我也是,很多重要的事情也是放到了最后做甚至只延期了。所以,我的行动之前,得先做最要紧的事情。但是也会混杂一些个人的情绪在里边,比方说明明一件事情很重要,但是自己就是不想做或者说觉得事情很简单,我先做最有意思的事情。很多时候都是这样的,兴趣和意义占据了主导因素,优先级反而不是那么重要了。


抗拒干扰


手机就在我的边上,这很难不因为一个消息或者一个发愣就去拿起手机,一旦拿起来就放不下了。所以,我觉得最好就是把它放在我的抽屉里,然后眼不见就不去想它了。


奖励惩罚机制


最后,我觉得奖罚分明也挺重要的。在这里,我也想起了我在一线的时候,我周末总会有一天去我住的地方隔壁去逛超市,每次的消费金额大约在100-150左右。但是我出去的前提是我的学习目标完成了或者代码写完了。我现在却相反,目标缺少了一个验收和奖惩的过程。我觉得和我更喜欢宅有一点关系了,所以,我也得奖励我自己一下:目标完成了可以去逛超市消费🛒,也可以去骑行🚲;但是没完成,健腹轮😭😭安排上!


好了,以上就是我对于最近的拖延症的分析和解决方式的思考了。也欢迎伙伴们在评论区交流一下自己对于拖延症的看法。


shigen一起,每天不一样!


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

Swift - LeetCode - 二叉树的所有路径

iOS
题目 给你一个二叉树的根节点 root,按 任意顺序,返回所有从根节点到叶子节点的路径。 叶子节点 是指没有子节点的节点。 示例 1: 输入:root = [1,2,3,null,5]输出:["1->2->5","1->3"] 示例 2:...
继续阅读 »

题目


给你一个二叉树的根节点 root,按 任意顺序,返回所有从根节点到叶子节点的路径。


叶子节点 是指没有子节点的节点。


示例 1:



  • 输入:root = [1,2,3,null,5]
  • 输出:["1->2->5","1->3"]


示例 2:



  • 输入:root = [1]
  • 输出:["1"]


提示:


  • 树中节点的数目在范围 [1, 100] 内
  • -100 <= Node.val <= 100

方法一:深度优先搜索


思路及解法


最直观的方法是使用深度优先搜索。在深度优先搜索遍历二叉树时,我们需要考虑当前的节点以及它的孩子节点。


  • 如果当前节点不是叶子节点,则在当前的路径末尾添加该节点,并继续递归遍历该节点的每一个孩子节点。
  • 如果当前节点是叶子节点,则在当前路径末尾添加该节点后我们就得到了一条从根节点到叶子节点的路径,将该路径加入到答案即可。

如此,当遍历完整棵二叉树以后我们就得到了所有从根节点到叶子节点的路径。当然,深度优先搜索也可以使用非递归的方式实现,这里不再赘述。


代码

class Solution {
func binaryTreePaths(_ root: TreeNode?) -> [String] {
var paths: [String] = []
constructPaths(root, "", &paths)
return paths
}

func constructPaths(_ root: TreeNode?, _ path: String, _ paths: inout [String]) {
if nil != root {
var path = path
path += String(root!.val)
if nil == root?.left && nil == root?.right {
paths.append(path)
} else {
path += "->"
constructPaths(root?.left, path, &paths)
constructPaths(root?.right, path, &paths)
}
}
}
}

复杂度分析

  • 时间复杂度:(2),其中  表示节点数目。在深度优先搜索中每个节点会被访问一次且只会被访问一次,每一次会对  变量进行拷贝构造,时间代价为 (),故时间复杂度为 (2)

  • 空间复杂度:(2),其中  表示节点数目。除答案数组外我们需要考虑递归调用的栈空间。在最坏情况下,当二叉树中每个节点只有一个孩子节点时,即整棵二叉树呈一个链状,此时递归的层数为 ,此时每一层的  变量的空间代价的总和为 (=1)=(2) 空间复杂度为 (2)。最好情况下,当二叉树为平衡二叉树时,它的高度为 log,此时空间复杂度为 ((log)2)


方法二:广度优先搜索


思路及解法


我们也可以用广度优先搜索来实现。我们维护一个队列,存储节点以及根到该节点的路径。一开始这个队列里只有根节点。在每一步迭代中,我们取出队列中的首节点,如果它是叶子节点,则将它对应的路径加入到答案中。如果它不是叶子节点,则将它的所有孩子节点加入到队列的末尾。当队列为空时广度优先搜索结束,我们即能得到答案。


代码

class Solution {
func binaryTreePaths(_ root: TreeNode?) -> [String] {
var paths: [String] = []
if nil == root {
return paths
}
var node_queue: [TreeNode] = []
var path_queue: [String] = []

node_queue.append(root!)
path_queue.append(String(root!.val))

while !node_queue.isEmpty {
let node: TreeNode? = node_queue.removeFirst()
let path: String = path_queue.removeFirst()

if nil == node?.left && nil == node?.right {
paths.append(path)
} else {
if nil != node?.left {
node_queue.append(node!.left!)
path_queue.append(path + "->" + String(node!.left!.val))
}

if nil != node?.right {
node_queue.append(node!.right!)
path_queue.append(path + "->" + String(node!.right!.val))
}
}
}
return paths
}
}

复杂度分析


  • 时间复杂度:(2),其中  表示节点数目。分析同方法一。

  • 空间复杂度:(2),其中  表示节点数目。在最坏情况下,队列中会存在  个节点,保存字符串的队列中每个节点的最大长度为 ,故空间复杂度为 (2)


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