注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

环信十周年趴——《我的程序人生》

在我的程序生涯中,我深刻体会到了编程是一场修行。这场修行中有喜悦、有痛苦、有枯燥,但最终会有收获。 我曾经是一个对编程一无所知的菜鸟,每天只是机械地敲着键盘,重复着Ctrl+C和Ctrl+V的操作。但是随着时间的推移,我开始对编程产生了兴趣,并开始了自己的修...
继续阅读 »

在我的程序生涯中,我深刻体会到了编程是一场修行。这场修行中有喜悦、有痛苦、有枯燥,但最终会有收获。


我曾经是一个对编程一无所知的菜鸟,每天只是机械地敲着键盘,重复着Ctrl+C和Ctrl+V的操作。但是随着时间的推移,我开始对编程产生了兴趣,并开始了自己的修行之路。
我通过阅读大量的编程书籍、观看视频和参加线上课程来不断提高自己的技能。我还加入了一些编程社区,和其他程序员分享经验和技巧,并不断地学习和探索新的编程技术和工具。


在这个过程中,我遇到了很多挑战和困难。有时候我会遇到难以解决的问题,或者是代码出现了错误,但是我从来没有放弃过。我不断地调试、修改和重构代码,直到找到解决问题的方法。这个过程让我深刻理解到了编程的本质,即通过不断地试错和调试来完善代码。
随着我的技能不断提高,我也开始在工作中发挥更大的作用。我可以更快速地开发出高质量的代码,并能够帮助团队解决一些复杂的问题。我也开始在各种编程竞赛中获奖,这让我更加有信心和动力去追求更高的目标。


现在,我已经成为了一名经验丰富的程序员,并且深深地热爱着编程这个行业。我相信,只要不断地学习和探索,坚持自己的修行之路,就一定能够在这条路上不断前进,并取得更大的成就。


在我的程序生涯中,我也深刻地体会到了团队合作的重要性。在开发一个项目时,需要与其他成员进行紧密的合作,共同解决遇到的问题。这需要我们有良好的沟通能力和协作精神,能够理解并尊重其他人的观点和意见。正是这种团队合作的精神,让我们能够更高效地完成任务,并取得更好的成果。
总之,编程是一项充
满挑战和机遇的工作,让我们不断地成长和进步。通过不断地学习和探索,秉持着精益求精的态度,我们可以在这条修行之路上不断前进,并取得更大的成就。

在我的程序生涯中,我经历了很多挑战,但也有很多收获和成就。我深深地热爱着编程这个行业,并会继续坚持我的修行之路,不断地提高自己的技能和素质,成为一名更加优秀和出色的程序员。


最后,作为一名环信的老用户,我希望能够在未来的日子里,继续见证环信的成长和壮大。环信是一款优秀的产品,它让人们能够更加方便和快捷地进行交流和协作,这是一项非常有意义和有价值的事情。我会继续支持和使用环信,为它的发展贡献自己的力量。

总之,编程是一项非常有趣和富有挑战性的工作。在这个过程中,我们不仅能够不断地学习和进步,还能够结识到许多志同道合的朋友。如果你也对编程感兴趣,并愿意付出努力和时间,那么欢迎加入到我们的编程修行之路中来。
在环信十周年之际,我衷心地祝愿环信能够继续壮大和发展,为更多的人提供优秀的产品和服务。同时,我也希望自己能够继续在这条修行之路上前进,不断地取得更大的成就和收获。
收起阅读 »

环信十周年趴——我的程序人生

程序人生是一段充满挑战、成长与收获的旅程。它可以让你在无限创意的空间中实现自己的天马行空的想象,也可以带给你沉淀思考、坚毅追求的人生智慧。以下是我过去几年在程序开发和编程学习中所获得的心得和体悟,希望能对正在学习编程或准备进入编程领域的朋友们提供一些借鉴和启示...
继续阅读 »

程序人生是一段充满挑战、成长与收获的旅程。它可以让你在无限创意的空间中实现自己的天马行空的想象,也可以带给你沉淀思考、坚毅追求的人生智慧。以下是我过去几年在程序开发和编程学习中所获得的心得和体悟,希望能对正在学习编程或准备进入编程领域的朋友们提供一些借鉴和启示。

首先,我的编程之路始于兴趣。在大学期间,由于对计算机技术的好奇和热爱,我开始接触编程,学习了C语言和Java等编程语言。那时候,我并没有特别强烈的目标和压力,只是因为喜欢而去学习。但是很快,我就发现编程的魅力所在:在计算机的世界中,我可以实现自己的创意和想象,探索更多未知的领域,并且不断挑战自己的能力极限。

当我逐渐深入学习编程时,我开始体会到编程所带来的快感和成就感。每当我发现一个Bug,并最终成功解决时,就有一种说不出的成就感和满足感。这种满足感不仅来自于程序运行正常,也来自于我自己的成功,对自己能力的证明。这也让我更加爱上了编程,开始不断地探索更多新技术,学习更多新知识。

但是,学习编程并不一定就是一帆风顺的。在这条通往成功的路上,不可避免地会遇到各种困难和挑战。在我的编程之路中,我也遇到过很多错综复杂的问题。有时候会卡在一个很小的细节上,一遍遍地调试改错,却迟迟无法找到问题所在;有时候会觉得编程技能难以提高,感觉每个任务都前途渺茫;有时候会疲于奔命地赶着截止日期,在压力和时间的双重压力下,一点点地完善项目。但这些困难并没有阻挡我的步伐,反而让我更加坚定了自己的信念。

在这个过程中,最重要的就是要保持坚持和耐心。无论遇到怎样的困难,都要尝试解决,尽可能的学习和掌握更多的技能和知识。有时候,我会在论坛或社区中与其他程序员交流,分享自己的问题和经验。还有时候,我会利用一些资源,如各种教程、网站和视频等,来不断提升自己的编程技能。与此同时,我还不断地进行探索和实践,开发个人项目和实验,以此来不断深入学习。这种“付出比收获多”的过程有时候会让人感到压抑,但有时间积累,功夫不负有心人,最终一定会有收获。

随着自己的不断努力和学习,我逐渐摸索出了自己所喜欢编程的领域和方向。我喜欢从事物的本质和内在逻辑入手,研究算法和数据结构的奥秘、探索计算机科学的理论界限。同时,我也很热衷于与其他领域的知识结合,如人工智能、机器学习、大数据分析等,以此来开发和创新更加复杂的系统和应用。这种对于理论和实践的结合,也是我平衡好奇心和实用性的秘诀。通过不断挑战自我而达到不断进步,从而实现个人成长和价值的提升。


在实践中,我深刻理解编程旨在解决问题的本质,更加关注代码的质量和可维护性,以此来提高项目的效率和可靠性。我认识到了一个好的程序员并不仅仅是一个语法熟练、技能娴熟的写手,他还要拥有扎实的编程基础、算法功底、代码组织和风格良好的习惯等。同时,编程对于沟通和协作能力的要求也在不断加强。在编程开发中,只有细致发掘需求,灵活妥善的应对变化,与他人协作合作,才能提供出在用户满意的产品和服务。通过以上不同方面的提升和努力,我的程序人生也不断拓展、深入,从基础语言开始,逐渐实现提炼程序本质、解决实际问题的能力。

此外,编程与人生的有趣之处还在于,无处不在并随时随地可见。尽管不是所有人都需要成为程序员,但是对于了解和掌握最基本编程思维和方法,对于从事现代社会的工作、学习和生活都是有益的。无论是自动驾驶、智能家居、科技创新,还是文化艺术、社交娱乐等等,都需要程序员来提供必要的软硬件支撑。随着新技术的崛起和应用领域的不断扩大,未来的程序人生也将更加丰富,更加精彩。在这样一个变化万千的时代,学习编程不仅仅是为了获得一份职业,它也是一种与时代同步的生活方式,一种新世界的探索和发现,更是充实自己生活的一种方式。

总之,我的程序人生虽然经历了困难和挑战,但只要坚信不懈,不断学习、改进,就能够获得切实的进步和收获。编程不仅仅是一种技术,也是一种态度,一种不断探索和改进的生活方式。在此,我希望有更多的人能够加入到编程的行列中,体验到程序人生带来的乐趣和挑战,用技术的力量来改变未来。


本文参与环信十周年活动

收起阅读 »

Android深思如何防止快速点击

前言 其实快速点击是个很好解决的问题,但是如何优雅的去解决确是一个难题,本文主要是记录一些本人通过解决快速点击的过程中脑海里浮现的一些对这个问题的深思。 1. AOP 可以通过AOP来解决这个问题,而且AOP解决的方法也很优雅,在开源上也应该是能找到对应的成熟...
继续阅读 »

前言


其实快速点击是个很好解决的问题,但是如何优雅的去解决确是一个难题,本文主要是记录一些本人通过解决快速点击的过程中脑海里浮现的一些对这个问题的深思。


1. AOP


可以通过AOP来解决这个问题,而且AOP解决的方法也很优雅,在开源上也应该是能找到对应的成熟框架。


AOP来解决这类问题其实是近些年一个比较好的思路,包括比如像数据打点,通过AOP去处理,也能得到一个比较优雅的效果。牛逼的人甚至可以不用别人写的框架,自己去封装就行,我因为对这个技术栈不熟,这里就不献丑了。

总之,如果你想快速又简单的处理这种问题,AOP是一个很好的方案


2. kotlin


使用kotlin的朋友有福了,kotlin中有个概念是扩展函数,使用扩展函数去封装放快速点击的操作逻辑,也能很快的实现这个效果。它的好处就是突出两个字“方便”


那是不是我用java,不用kotlin就实现不了kotlin这个扩展函数的效果?当然不是了。这让我想到一件事,我也有去看这类问题的文章,看看有没有哪个大神有比较好的思路,然后我注意到有人就说用扩展函数就行,不用这么麻烦。


OK,那扩展函数是什么?它的原理是什么?不就是静态类去套一层吗?那用java当然能实现,为什么别人用java去封装这套逻辑就是麻烦呢?代码不都是一样,只不过kotlin帮你做了而已。所以我觉得kotlin的扩展函数效果是方便,但从整体的解决思路上看,缺少点优雅。


3. 流


简单来说也有很多人用了Rxjava或者kotlin的flow去实现,像这种实现也就是能方便而已,在底层上并没有什么实质性的突破,所以就不多说了,说白了就是和上面一样。


4. 通过拦截


因为上面已经说了kt的情况,所以接下来的相关代码都会用java来实现。

通过拦截来达到防止快速点击的效果,而拦截我想到有2种方式,第一种是拦截事件,就是基于事件分发机制去实现,第二种是拦截方法。

相对而言,其实我觉得拦截方法会更加安全,举个场景,假如你有个页面,然后页面正在到计算,到计算完之后会显示一个按钮,点击后弹出一个对话框。然后过了许久,改需求了,改成到计算完之后自动弹出对话框。但是你之前的点击按钮弹出对话框的操作还需要保留。那就会有可能因为某些操作导致到计算完的一瞬间先显示按钮,这时你以迅雷不及掩耳的速度点它,那就弹出两次对话框。


(1)拦截事件


其实就是给事件加个判断,判断两次点击的时间如果在某个范围就不触发,这可能是大部分人会用的方式。


正常情况下我们是无法去入侵事件分发机制的,只能使用它提供的方法去操作,比如我们没办法在外部影响dispatchTouchEvent这些方法。当然不正常的情况下也许可以,你可以尝试往hook的方向去思考能不能实现,我这边就不思考这种情况了。

public class FastClickHelper {

private static long beforeTime = 0;
private static Map<View, View.OnClickListener> map = new HashMap<>();

public static void setOnClickListener(View view, View.OnClickListener onClickListener) {
map.put(view, onClickListener);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
long clickTime = SystemClock.elapsedRealtime();
if (beforeTime != 0 && clickTime - beforeTime < 1000) {
return;
}
beforeTime = clickTime;

View.OnClickListener relListener = map.get(v);
if (relListener != null) {
relListener.onClick(v);
}
}
});
}

}

简单来写就是这样,其实这个就和上面说的kt的扩展函数差不多。调用的时候就

FastClickHelper.setOnClickListener(view, this);

但是能看出这个只是针对单个view去配置,如果我们想其实页面所有view都要放快速点击,只不过某个view需要快速点击,比如抢东西类型的,那肯定不能防。所以给每个view单独去配置就很麻烦,没关系,我们可以优化一下

public class FastClickHelper {

private Map<View, Integer> map;
private HandlerThread mThread;

public void init(ViewGroup viewGroup) {
map = new ConcurrentHashMap<>();
initThread();
loopAddView(viewGroup);

for (View v : map.keySet()) {
v.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
int state = map.get(v);
if (state == 1) {
return true;
} else {
map.put(v, 1);
block(v);
}
}
return false;
}
});
}
}

private void initThread() {
mThread = new HandlerThread("LAZY_CLOCK");
mThread.start();
}

private void block(View v) {
// 切条线程处理
Handler handler = new Handler(mThread.getLooper());
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (map != null) {
map.put(v, 0);
}
}
}, 1000);
}

private void exclude(View... views) {
for (View view : views) {
map.remove(view);
}
}

private void loopAddView(ViewGroup viewGroup) {
for (int i = 0; i < viewGroup.getChildCount(); i++) {
if (viewGroup.getChildAt(i) instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) viewGroup.getChildAt(i);
map.put(vg, 0);
loopAddView(vg);
} else {
map.put(viewGroup.getChildAt(i), 0);
}
}
}

public void onDestroy() {
try {
map.clear();
map = null;
mThread.interrupt();
} catch (Exception e) {
e.printStackTrace();
}
}

}

我把viewgroup当成入参,然后给它的所有子view都设置,因为onclicklistener比较常用,所以改成了设置setOnTouchListener,当然外部如果给view设置了setOnTouchListener去覆盖我这的set,那就只能自己做特殊处理了。


在外部直接调用

FastClickHelper fastClickHelper = new FastClickHelper();
fastClickHelper.init((ViewGroup) getWindow().getDecorView());

如果要想让某个view不要限制快速点击的话,就调用exclude方法。这里要注意使用完之后释放资源,要调用onDestroy方法释放资源。


关于这个部分的思考,其实上面的大家都会,也基本是这样去限制,但是就是即便我用第二种代码,也要每个页面都调用一次,而且看起来,多少差点优雅。


首先我想的办法是在事件分发下发的过程去做处理,就是在viewgroup的dispatchTouchEvent或者onInterceptTouchEvent这类方法里面,但是我简单看了源码是没有提供方法出来的,也没有比较好去hook的地方,所以只能暂时放弃思考在这个下发流程去做手脚。


补充一下,如果你是自定义view,那肯定不会烦恼这个问题,但是你总不能所有的view都做成自定义的吧。


其次我想怎么能通过不写逻辑代码能实现这个效果,但总觉得这个方向不就是AOP吗,或者不是通过开发层面,在开发结束后想办法去注入字节码等操作,我觉得要往这个方向思考的话,最终的实现肯定不是代码层面去实现的。


(2)拦截方法


上面也说了,相对于拦截事件,假设如果都能实现的情况下,我更倾向于去拦截方法。


因为从这层面上来说,如果实现拦截方法,或者说能实现中断方法,那就不只是能做到防快速点击,而是能给方法去定制相对应的规则,比如某个方法在1秒的间隔内只能调用一次,这个就是防快速点击的效果嘛,比如某个方法我限制只能调一次,如果能实现,我就不用再额外写判断这个方法调用一次过后我设置一个布尔类型,然后下次调用再判断这个布尔类型来决定是否调用,


那现在是没办法实现拦截方法吗?当然有办法,只不过会十分的不优雅,比如一个方法是这样的。

public void fun(){
// todo 第1步
// todo 第2步
// todo ......
// todo 第n步
}

那我可以封装一个类,里面去封装一些策略,然后根据策略再去决定方法要不要执行这些步骤,那可能就会写成

public void fun(){
new FunctionStrategy(FunctionStrategy.ONLY_ONE, new CallBack{
@Override
public void onAction() {
// todo 第1步
// todo 第2步
// todo ......
// todo 第n步
}
})
}

这样就实现了,比如只调用一次,具体的只调用一次的逻辑就写在FunctionStrategy里面,然后第2次,第n次就不会回调。当然我这是随便乱下来表达这个思路,现实肯定不能这样写。首先这样写就很不优雅,其次也会存在很多问题,扩展性也很差。


那在代码层面还有其它办法拦截或者中断方法吗,在代码层还真有办法中断方法,没错,那就是抛异常,但是话说回来,你也不可能在每个地方都try-catch吧,不切实际。


目前对拦截方法或者中断方法,我是没想到什么好的思路了,但是我觉得如果能实现,对防止快速点击来说,肯定会是一个很好的方案。


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

Android-Deeplink跳转失败问题修复

Android Deeplink实现 在Android中,Deeplnk通过声明Activity的intent-filter来实现对自定义url访问事件的捕捉。在有道背单词的项目中,我们需要通过前端分享词单的方式,将词单分享给别人,并通过点击前端页面收藏按钮,...
继续阅读 »

Android Deeplink实现


在Android中,Deeplnk通过声明Activity的intent-filter来实现对自定义url访问事件的捕捉。在有道背单词的项目中,我们需要通过前端分享词单的方式,将词单分享给别人,并通过点击前端页面收藏按钮,实现调起客户端收藏词单的功能。

从前端通过自定义url的方式调起客户端这个功能原来一直都没有什么问题,直到最近有部分用户反馈在某些浏览器下无法调起。下面我们来看一下分析查找问题的方法以及如何解决。
转载请注明来源「Bug总柴」


检查客户端deeplink配置


在AndroidManifest.xml文件中,对路由Activity配置如下:

<activity
android:name=".deeplink.RouterActivity"
android:configChanges="keyboardHidden|orientation|screenSize"
android:launchMode="singleTask"
android:theme="@style/Theme.Translucent">
<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:host="youdao.com"
android:scheme="recite"
android:pathPattern=".*"/>
</intent-filter>

<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".home.ui.MainActivity" />
</activity>

里面比较重要的部分是intent-filter中的data配置,检查后发现配置正常,可以正常拦截到 recite://youdao.com/.*的所有请求。

转到RouterActivity通过断点调试,发现并没有到达。从而可以确认是浏览器调起的时候发生了异常。


tips: adb 命令同样可以启动deeplink进行测试
adb_test.png


分析浏览器对deeplink处理


通过用户反馈,主要集中是在UC和华为自带的浏览器点击前端页面的【收藏词单】无法调起有道背单词

同时我们在chrome上面发现通过deeplink只有第一次会跳转到应用,往后几次都是没有任何相应,确实有点百思不得其解。

经过查找资料,发现了chrome的一个对Android Intent处理的介绍

Android Intents with Chrome

里面提到



One scenario is launching an app when the user lands on a page, which you can achieve by embedding an iframe in the page with a custom URI-scheme set as the src, as follows: . This works in the Chrome for Android browser, version 18 and earlier. It also works in the Android browser, of course.




The functionality has changed slightly in Chrome for Android, versions 25 and later. It is no longer possible to launch an Android app by setting an iframe's src attribute. For example, navigating an iframe to a URI with a custom scheme such as paulsawesomeapp:// will not work even if the user has the appropriate app installed. Instead, you should implement a user gesture to launch the app via a custom scheme, or use the “intent:” syntax described in this article.



翻译一下,大概的意思就是之前通过没有用户主动操作就打开app的行为在chrome25版本及之后会被禁止。开发者必须通过用户操作来触发跳转应用的行为。目前chrome的版本都已经68了,证明这个规则已经由来已久。抱着试试看的姿态,开始查找是否是前端的代码有问题。
通过chrome inspect,捕捉到前端代码果然有一处疑似iframe的使用
ebc8daf14130474bbd69103bf4e6ff5d_ac466235c97c6e9fb45c8820addbce1d.jpg


018ff33c02e047c59196534a3a28ef8b_e15f0d6e314113b2260119e917400191.jpg


随后经过对前端代码debug,果然有走了这段逻辑
ca286076d8594b55b7db5847e6d031b6_c0169d9d3acbb13b8835c5a0a8aa028f.jpg


证据确凿,可以找前端大神反馈了。经过了解,确实是之前有改动过这部分的代码,使用了iframe来处理deeplink的打开。处理的办法也相对简单,将iframe换成href来做跳转处理就可以了。


测试


最后我们对国内的浏览器试了一下deeplink是否生效


UC浏览器


会弹出一个应用打开提醒,如果用户本次没有【允许】操作,则浏览器下次会拦截打开应用行为,没有任何提醒,不知道这是一个bug还是故意为之。点击【允许】后可以跳转应用
Screenshot_20180818-112921.jpg


QQ浏览器


同样会弹出应用打开题型,如果用户本次没有【打开】,下次用户操作还是会继续提醒。点击【打开】后可以跳转应用
Screenshot_20180818-113231.jpg


360浏览器


行为与QQ浏览器类似,每次都会提醒
Screenshot_20180818-113459.jpg


猎豹浏览器


行为与QQ浏览器类似,每次都会提醒
Screenshot_20180818-113718.jpg


一加系统默认浏览器


行为与QQ浏览器类似,每次都会提醒
Screenshot_20180818-113921.jpg


搜狗浏览器


没有提醒,直接跳转到app


chrome


行为与搜狗浏览器类似,没有提醒,直接跳转app


测试结果除了UC浏览器第一次不点击跳转之后会跳转不了之外,
其他浏览器跳转app问题得到解决。


结语


通过这次查deeplink跳转的问题,收获了两点知识。



  • 一个是前端使用iframe来处理deeplink跳转会有问题

  • 二个是除了采用
"scheme://host/path"

这种deeplink方式之外,还可以采用

"intent://about/#Intent;action=[string];scheme=[string];package=[string];S.browser_fallback_url=[encoded_full_url];end"

的方式来触发应用intent的请求访问。


同时,在处理deeplink的规则里面,体会到了一条原则:



  • 最短路径处理原则


意思就是刚开始的时候,deeplink处理的逻辑要从根目录开始进行。比如有一个收藏词单的需求,没有使用最短路径原则可能会设计成这样



recite://youdao.com/bookId?&action=collect



对应的处理是如果action为collect就收藏词单。这个时候需求如果改成默认进来不需要收藏就非常尴尬了。因为对于旧版本而已,只认有action=collect才会处理,那就意味这如果想对默认的recite://youdao.com/bookId只是查看不收藏的需求,对于旧版本就没办法实现,会出现兼容性问题。

而最短路径处理原则,意思就是在开始的时候,尽量对最短的路径行为进行处理,具体到上面的例子,对于收藏某个词单的需求,我们可以设计deeplink为



recite://youdao.com/bookId?&action=collect



然后我们对 recite://youdao.com/bookId以及recite://youdao.com/bookId?&action=collect 都处理成收藏词单。上线之后,如果想修改默认参数行为,就可以直接改对 recite://youdao.com/bookId 的处理,这样对于旧版本仍然是可以执行的收藏行为,对于新版本就可以对应新的逻辑


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

一点点编译优化

正文 经过一段时间的工作(摸鱼划水),从几个很小的地方给大家介绍下我是如何提升编译构建速度的,但是本次分享内容还是主要针对当前阿逼的工程架构,不一定对你们有帮助哦。 FileWalker 剪枝 + 多线程优化 我们工程内会在编译和同步阶段首先获取到整个工程的模...
继续阅读 »

正文


经过一段时间的工作(摸鱼划水),从几个很小的地方给大家介绍下我是如何提升编译构建速度的,但是本次分享内容还是主要针对当前阿逼的工程架构,不一定对你们有帮助哦。


FileWalker 剪枝 + 多线程优化


我们工程内会在编译和同步阶段首先获取到整个工程的模型,之后计算出每个模块的version版本。之前我们通过java.nio.file.FileVisitor来进行工程文件遍历的操作。同样文件展开的api也是可以进行剪枝的,但是由于是用groovy写的,我还是不太喜欢。


本次优化我们采用了kotlin的file相关的walkTopDown(可以快速的从上到下的遍历一个文件树)语法糖。然后通过其中的onEnterdsl进行剪枝,然后我们可以通过filter进行第二波过滤,筛选出我们实际要访问的目录。最后再进行一次foreach。

fun File.walkFileTree(action: (File) -> Unit) {
walkTopDown().onEnter {
# 对于不需要的目录进行剪枝
val value = if (!(it.isDirectory && (it.name == "build" || it.name == "src"))) {
!it.isHidden
} else {
false
}
value
}.filter {
val value = if (this == it) {
false
} else {
if (it.isDirectory) {
val build = File(it, "build.gradle")
build.exists()
} else {
false
}
}
value
}.forEachIndexed { _, file ->
action.invoke(file)
}
}


优化效果如下,原本没有剪枝的版本,我们本机进行一次FileWalker需要1分钟左右的时间。使用剪枝的版本之后,我们可以把时间优化到2s左右的时间。我们跳过了些什么?































格式是否跳过
隐藏文件夹
build
src
文件
其他文件夹

通过上述的剪枝,我们可以减少非常非常多的文件遍历操作,在完成同样的能力的情况下,可以大大的加快文件的遍历操作。


另外,我们会获取对应文件下的git commit sha值,然后作为该模块的version版本,而这个操作也是有几百毫秒的耗时,而我们工程大概有800+这样的模块,所以如果按照同步的方式去执行,就会变得耗时了。


而优化方案就比较简单了,我们通过线程池提供的invokeAll方法,并发执行完之后再继续向下执行就可以完成该优化了。

        Executors.newCachedThreadPool().invokeAll(callableList)

整体优化下来,原来在CI上一次FileWalker需要1mins,因为CI的机器足够牛逼,所以优化完仅仅只需要3.5s就可以完成整个工程的遍历以及获取对应git commit sha值的操作。


修改前:


企业微信截图_9431b7d7-1599-4f3f-a916-276cde61f71d.png


修改后:


企业微信截图_9127d691-a792-41e2-bc38-62f08697ea64.png


skip 一些非必须task


在AGP的打包流程中,会插入很多预检查的任务,比如类似kotlin版本检查, compileSdk版本检查等等任务。而这些任务即时不执行,也并不会影响本次打包任务,还是可以打出对应的apk产物的。当然前提是编译没有啥问题的情况下。


我们仔细观察了下apk打包下的所有的task,并观察了下task任务耗时情况,找到了几个看起来并没有什么实际用处的任务。


企业微信截图_912eebd8-2104-4a10-bb5a-6d60637e922f.png


接下来就是如何去关闭这个任务了,其实方式还是比较简单的。我们主要用字符串的形式去获取到这个Task,然后把enable设置成false就可以了。但是前提是这个Task的输出并不会影响到后续的Task就行了。另外这几个应该是高apg新增的task,7.x才出现的低版本的是没有的。

afterEvaluate {
def metaDebugTask = tasks.findByName("checkApinkDebugAarMetadata")
def debugCheck = System.getenv().containsKey("ENABLE_APINK_DEBUG_CHECK")
if( metaDebugTask != null && !debugCheck) {
metaDebugTask.setEnabled(false)
}
def preCheck = tasks.findByName("checkApinkDebugDuplicateClasses")
if( preCheck != null && !debugCheck) {
preCheck.setEnabled(false)
}
}

企业微信截图_99e7c774-4e22-40be-bbac-73080faa80c0.png


这样我们就可以在一个构建中优化掉大概1min30s的时间了。这些手段相对来说都比较简单,另外我们还可以考虑把一些不互相依赖的任务从线性执行变成并行执行。可以参考gradle的Worker相关api。



worker_api



二进制产物发布


原始的baseversion是基于文件内容的md5 生成的一个全局统一版本号,然后再结合仓库内的gitsha生成二进制缓存。但是由于大仓内的代码量越来越大,所以一旦变更baseversion,需要消耗大概80min左右的时间重新生成所有的二进制缓存。


虽然但是,其实并没有这个必要全部模块都进行一次发布。方法签名出现问题,我们只需要让对应的模块重编即可。所以这种全局性的baseversion 就需要进行迭代了。需要让baseversion被拆解成多个,然后进行混合生成一个新值,底层改动可以影响到上层,而最上层模块也可以具备独立更新的能力。

# 后续该文件改动不会导致整个baseVersion变更
# 基于文件路径匹配的规则,可以给每个文件路径设置一个version,但是由于工程之间存在依赖,所以可以合并多个baseVersion到一起
# 测试结果如下,framework变更80min comm 变更 50min app上变更10min
# 后续会配合a8检查,直接通知各位那些模块的方法签名检查不通过,之后直接修改局部version版本就好了


# 全局默认混入所有 如果非必要情况下 别改这个版本号 !!!!!!!
- name: global
path:
version: 1
# app目录缓存

- name: app
path: app
version: 1
mixin: [ framework,common ]
# framework基础,该层目录变更之后所有向上的全部需要变更 非必要也最好不要改
- name: framework
path: framework
version: 2
mixin:
# comm 模块,该层目录变更之后所有向上的全部需要变更 非必要也最好不要改
- name: common
path: common
version: 2
mixin: [ framework ]

通过定义出一个新的yaml文件,我们可以定义出namepath代表相对路径,version表示版本号,mixin代表混合入别的name,而相对底层改动情况下会影响到上层模块重编。


这样,我们就可以在app下进行独立的版本号增加,让app目录下的模块进行一次重编,从而解决一部分方法签名问题导致的上层库缓存刷新。


测试结果大概如下:























目录耗时情况
global80min
common50min
app10min

结尾


以下仅代表个人看法哦,我觉得编译优化还是要从本工程实际情况出发,你的工具箱内的工具要足够多,要先知道哪些东西是慢的你才会有思考的去进行一些优化,而不是很盲目的进行尝试。


另外优化不应该破坏整个工程的情况,我们不应该魔改整个编译流程,最好就是通过最小的手段去进行一些微量的优化,小步慢跑的进行一些对应的优化。


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

我就问Zygote进程,你到底都干了啥

ZYGOTE 前言 OK,这是Android系统启动的第二篇文章。第二篇我们讲解一个我们一直都在用,但是却很少提起的进程---Zygote。 提到Zygote可能了解一些的小伙伴会说,它是分裂进程用的。没错它最大的作用的确是分裂进程,但是它除了分裂进程外还做了...
继续阅读 »

ZYGOTE


前言


OK,这是Android系统启动的第二篇文章。第二篇我们讲解一个我们一直都在用,但是却很少提起的进程---Zygote
提到Zygote可能了解一些的小伙伴会说,它是分裂进程用的。没错它最大的作用的确是分裂进程,但是它除了分裂进程外还做了什么呢。
还是老规矩,让我们抱着几个问题来看文章。最后在结尾,再对问题进行思考回复。



  1. 你能大概描述一遍Zygote的启动流程吗

  2. 我们为什么可以执行java代码,和zygote有什么关系

  3. Zygote到底都做了哪些事情


另外,我最近也看了一些写底层的文章。要么就是言简意赅到只知道Zygote的作用,但是完全不知道如何实现的,就像是背作文一样。要么是全篇都是代码,又臭又长,让人完全没有看下去的动力。


不过作为一个读者,太长的我可能完全不想看,太短的又真的完全就是被课文一点都不明白原理。


因此,本篇文章,仍旧会引入一些代码,方便大家通过代码方便记忆。但是又会将代码进行精简,以免给大家造成过度疲劳。我们的目的都是希望用最少的时间,能掌握更多的知识。


读源码时:不要过于纠结细节,不要过于纠结细节,不要过于纠结细节!重要的事情说三遍!!!


OK,让我们进入正题瞅瞅Zygote到底是个什么东西。




1.C++还是Java


选这个当标题当然是有原因的。我们知道的是Android系统启动的时候,运行的是Linux内核,执行的是C++代码。这是一个很有趣的事情。


因为我们在写App的时候,AndroidStudio默认给我们创建的都是Activity.java,而选择的语言,要么是Java要么就是Kotlin


启动时候运行的是C++代码,应用层却可以使用Java代码,这到底是为什么呢?其实这就是Zygote的功劳之一


2.Native层


Init进程创建Zygote时,会调用app_main.cpp的main() 方法。此时它依旧运行的是C++代码。我们通过下面的代码,来看下它到底做了什么。


这里它做的最关键的一件事就是启动AndroidRuntime(Android运行时)。另外这里需要注意的是 start方法 中的 "com.android.internal.os.ZygoteInit" 这个类似于全类名。他到底是做什么的?别着急,大概2分钟后,你可能就会得到答案。


Zygote的新手村:app_main.cpp

int main(int argc, char* const argv[])
{
// 创建Android运行时对象
AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
// 代码省略...

// 调用AppRuntime.start方法,
// 而AppRuntime是AndroidRuntime的子类,并且没有重写start方法
// 因此调用的是AndroidRuntime的start方法
runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
}

精简后的代码告诉我们,这里一共就做了两件事,第一件创建AppRuntime,第二件调用start方法


不过,AppRuntimeAndroidRuntime的子类,他没有重写start方法,因此这里调用的是 AndroidRuntime的start() 方法。


奇迹的诞生地:AndroidRuntime:


这里我依旧只保留关键代码,源码关键注释也进行了保留。由下方代码看出这里做了三件改变命运的事情。



  1. startVM -- 启动Java虚拟机

  2. startReg -- 注册JNI

  3. 通过JNI调用Java方法,执行com.android.internal.os.ZygoteInit 的 main 方法
/*
* Start the Android runtime.
*/
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
/* start the virtual machine */
JNIEnv* env;
if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {
return;
}

/*
* Register android functions.
*/
if (startReg(env) < 0) {
ALOGE("Unable to register all android natives\n");
return;
}
/*
* Start VM. This thread becomes the main thread of the VM, and will
* not return until the VM exits.
*/
jmethodID startMeth = env->GetStaticMethodID(startClass, "main","([Ljava/lang/String;)V");
env->CallStaticVoidMethod(startClass, startMeth, strArray);
}

有了JVM,注册了JNI,我们就可以执行Java代码了。


这里我个人建议不要过于纠结细节,比如JVM是如何创建的,JNI是如何注册的。如果感兴趣的童鞋,可以下载源码,到app_main.cpp里进行查看。这里就不进行赘述了。否则代码量太大,适得其反。




3.Java层


命运的十字路口:ZygoteInit.java:


之所以说是命运的十字路口,因为Zygote会在这里创建SystemServer,但是二者却走向了截然不同的道路。


从这里就开始执行Java代码了,当然这些Java代码是运行在JVM中的。


让我们通过代码来看一下,到底都做了些什么。

class ZygoteInit{

/**
* This is the entry point for a Zygote process. It creates the Zygote server, loads resources,
* and handles other tasks related to preparing the process for forking into applications.
* This process is started with a nice value of -20 (highest priority).
*/
// 上面是源码中的注释,小伙伴们可以自行翻译一下。
// 创建了ZygoteServer,加载资源,并且介绍了这个进程的优先级是最高的-20.
public static void main(String argv[]) {
ZygoteServer zygoteServer = null;

// 1. 预加载资源,常用的:resource,class,library等在此处进行加载
preload(bootTimingsTraceLog);

// 2. 创建ZygoteServer,实际是一个Socket用来进行跨进程间通信用的。
zygoteServer = new ZygoteServer(isPrimaryZygote);

// 3. fork出SystemServer进程,这个进程会创建AMS,ATMS,WMS,电池服务等一切只有你想不到没有它做不到的服务
forkSystemServer(abiList, zygoteSocketName, zygoteServer);

// 4.里面是一个while(true)循环,等待接收AMS创建进程的消息,类似于handler中的Looper.loop()
zygoteServer.runSelectLoop(abiList);
}
}

上面代码里的注释基本说明了ZygoteInit都干了啥。下面会再稍微总结下:



  1. 创建了ZygoteServer:这是一个Socket相关的服务,目的是进行跨进程通信。

  2. 预加载preload:预加载相关的资源。

  3. 创建SystemServer进程:通过forkSystemServer分裂出了两个进程,一个Zygote进程,一个SystemServer进程。而且由于是分裂的,所以新分裂出来的进程也拥有虚拟机,也能调用JNI,也拥有预加载的资源,也会执行后续的代码。

  4. 执行runSelectLoop():内部是一个while(true)循环,等待AMS创建新的进程的消息。(想想Looper.loop())




4. 戛然而止


没错就是这么突然,Zygote的故事到这就结束了。至于它是如何创建SystemServer,如何去创建App那就是后面的故事了。所以你还记得我的问题嘛?我替你总结一下Zygote到底做了什么:



  1. 创建虚拟机

  2. 注册JNI

  3. 回调Java方法ZygoteInit.java 的main方法,从这儿开始运行Java代码

  4. 创建ZygoteServer,内部包含Socket用于跨进程通信

  5. 预加载class,resource,library等相关资源

  6. fork 出了 SystemServer进程。他俩除了返回的pid不同,剩下一模一样。通过返回值不同来决定剩下的代码如何运行。这个留待后续进行讲解。

  7. 进入while(true)循环等待,等待AMS创建进程的消息(类似于Looper.loop()




5. 多说一句


这个系列才刚刚进行到第二章,我尽量让文章不是特别长的情况下,既有代码整理思路,又有总结方便记忆。代码是源码中摘抄的省略了很大一部分,只能保证执行顺序。有兴趣的小伙伴可以下载源码进行查看。

其实最近我看了很多文章和视频,最后却选择写文章来对知识进行总结。归根结底就是因为一句话,好记性不如烂笔头。要是真的想快速记住,真的需要亲自查看一下源码。翻源码的同时,也是在不知不觉中锻炼你阅读源码的能力。 万一以后让你学习一个新的库,你也知道大概该怎么看。


都已经到这儿了,就稍微厚个脸皮,希望各位看官,能够点个赞加个关注。毕竟一篇文章可能就要耗费几个小时的时间,上一篇文章就几个赞,感谢这几个小伙伴,让我有继续写下去的动力。 真心感谢你们。


另外,文章中有哪里出错,请及时指出。毕竟各位才是真正的大佬。希望各位Androider,可以一起取暖,度过这个互联网寒冬!加油各位!!!




6. Zygote功能图


zygote.png


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

一个28岁程序员入行自述和感受

我是一个容易焦虑的人,工作时候想着跳槽,辞职休息时候想着工作,休息久了又觉得自己每天在虚度光阴毫无意义,似乎陷入了一个自我怀疑自我焦虑的死循环了。我想我该做的点什么去跳出这个循环。。。 自我叙述 我相信,每个人都有一个自命不凡的梦,总觉得自己应该和别人不一样,...
继续阅读 »

我是一个容易焦虑的人,工作时候想着跳槽,辞职休息时候想着工作,休息久了又觉得自己每天在虚度光阴毫无意义,似乎陷入了一个自我怀疑自我焦虑的死循环了。我想我该做的点什么去跳出这个循环。。。


自我叙述


我相信,每个人都有一个自命不凡的梦,总觉得自己应该和别人不一样,我不可能如此普通,自己的一生不应该泯然众生,平凡平庸的度过。尤其是干我们it这一行业的,都有一个自己的程序员梦,梦想着,真的能够用 “代码改变世界”


入行回顾



你们还记得自己是什么时候,入行it行业的吗



我今年已经28岁了,想起来入行,还挺久远的,应该是2016入行的,我也算是半路出家的,中间有过武术梦 歌唱梦 但是电脑什么上学那会就喜欢玩,当然是指游戏,




武术梦




来讲讲我得第一个·梦,武术梦,可能是从小受到武打演员动作电视剧的影响,尤其那个时候,成龙大哥的电影,一直再放,我觉得学武术是很酷的一件事情,尤其那会上小学,还是初中我的体育还是非常好的,


然后我们家那个时候电视还是黑白的,电视机。哈哈哈😀电视台就那么几个,放来放去,有一个台一直重复放成龙电影,还有广告, 都是 学武术就到 xxxx学校, 我被洗脑了吧


于是真的让我爸,打电话质询了一下,可是好像他们这种武术学校都是托管式的,封闭式学习,听说很苦,,,,当然这不是重点,重点每年学费非常的贵,en~,于是乎我的这个梦想终止了,。。




歌唱梦




为啥会有唱歌想法,你猜对了,是被那个时候的好声音给影响了,那个时候好声音是真的很火,看的时候我一度以为我也可以上好声音,去当歌手然后出道,当明星,什么的。


不过不经历打击,怎么会知道自己的下线在哪里呢


我小学换了两到三个学校,到初中,再到高中,你们还记得自己读高中那会吗,高中是有专业选择的,入学军训完以后。


我们代班主任,和我们说有三个专业方向可以选择,艺术类,分美术,和唱歌,然后是文化类,然后艺术类就业考大学分数会低很多,然后一系列原因,哈哈哈,我就选择了歌唱班。


我最好伙伴他选择了,美术类就是素描。这里我挺后悔没有选择 美术类。


到了歌唱班,第一课就是到专业课有钢琴的教室,老是要测试每个同学的,音色和音高,音域
然后各自上台表演自己的拿手的一首歌,。我当时测试时候就是跟着老师的弹的钢琴键瞎唱,


表演的歌曲是张雨生《大海》 也就唱了高潮那么几句。。 😀现在想起来还很羞耻,那是我第一次在那么多人面前唱歌,


后面开始上课老师说我当时分班时候音色什么还不错,但学到后面,我是音准不太行,我发现。再加上我自己的从小感觉好像有点自卑敏感人格,到现在把,我唱歌,就越来越差,


当然我们也有乐理。和钢琴课,我就想主助攻乐理和钢琴,


但是我很天真


乐理很难学习,都是文科知识需要背诵,但是他也要有视唱,也就是唱谱子,duo,re,mi,fa,suo,la,xi,duo。。等,我发现我也学不进去


后面我又开始去学钢琴,但是钢琴好像需要一定童子功,不然可能很难学出来,于是我每天早上6点钟起来,晚上吃完饭就去钢琴教师抢占位置, 还得把门堵着怕人笑话,打扰我,


结果你们也猜到了,音乐方面天赋很重要,然后就是性格上面表演上面,要放得开,可是我第一年勉强撑过去了,后面第二年,专业课越来越多了,我感觉我越来越自卑~,然后成绩就越来越差,老师也就没太重视,嗯~好不容撑到了第二年下半年,放暑假,


但是老师布置任务暑假要自己去外面练钢琴,来了之后要考试,我还花钱去外面上了声乐课钢琴课,哎,我感觉就是浪费钱,,,,,因为没什么效果,性格缺陷加上天赋不行,基本没效果,那段时间我也很痛苦的,因为越来越感觉根本容入不进去班级体,尤其是后面高二,了专业课很多大部分是前面老师带着发生开嗓,后面自由练习,我也不好意思,不想练习,所以
到后面,高二下学习我就转学了,,,,


当然我们班转学的,不止我一个,还有一个转学的 和我一个寝室的,他是因为音高上不去,转到了文科班, 还有一个是挺有天赋,我挺羡慕的,但是人家挺喜欢学习,不喜欢唱歌什么,就申请转到了,文科班。 不过她转到文科班,没多久也不太好,后面好像退学了,,我一直想打听他的消息,都在也没打听到了




玩电脑




我对电脑的组装非常感兴趣,喜欢研究电脑系统怎么装,笔记本拆装,台式机拆装,我会拿我自己的的笔记本来做实验,自己给自己配台式机,自己给自己笔记本增加配置,哈哈哈哈。对这些都爱不释手。



这还是我很早时候,自己一点一点比价,然后去那种太平洋电脑城,电脑一条街,那种地去找人配置的。想想那时候配置这个电脑还挺激动,这是人生的第一台自己全部从零开始组装配的电脑,


本来打算,后面去电脑城上班,开一个笔记本维修,电脑装配的门面的,(因为自己研究了很多笔记本系统,电脑组装),可是好像听电脑城的人说,电脑组装什么的已经不赚钱了,没什么价格利润,都是透明的而且更新迭代非常的快,电脑城这种店铺也越来越少了,都不干了,没有新人再去干这个了,于是乎我的第一份工作失业 半道崩殂了,哈哈哈哈还没有开始就结束了。




学it




后面我又报名自学了,it编程,《xxx鸟》 但是学it我学起来,好像挺快的,挺有感觉的,入学前一个星期,要等班人数到齐才能开班,我们先来的就自己学习打字了,我每天都和寝室人,一起去打字,我感觉那段时间我过得挺开心和充实的,


后面我们觉得自带寝室不好,环境差,于是就几个人一起,搬出去住了,一起学习时候有一个年级26了,我和他关系还蛮好的,不过现在也没什么联系了,,,


学习时候,每次做项目时候我都是组长,那个时候原来是有成就感的,嗯,学习it好像改变了,我学唱歌那个时候,一些自卑性格,可能是遇到了一个好的老师吧


当然后面就顺利毕业,然后找到了工作了,,,


直到现在我还在it行业里


嗯~还想往下面写一点什么,,,下一篇分享一下我入门感受和经历吧


关注公众号,程序员三时 希望给你带来一点启发和帮助


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

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

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

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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


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


后记


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


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


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


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


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


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


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


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


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


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

🔥🔥🔥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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

副业奇谈

楔子 在一家名为叹佬鸡煲餐馆的小桌子上,坐着我和他,榴莲鸡煲溢出的香味,让人垂涎欲滴,鸡肉和榴莲嫩滑的口感,仿佛让人重回到那个十七八岁的青春时光。他叫小润,高中时期经常带着我为非作歹,中午午休跑去打篮球,晚自习溜去操场趟草地上吹牛逼,最刻骨铭心的,还是晚自习偷...
继续阅读 »

楔子


在一家名为叹佬鸡煲餐馆的小桌子上,坐着我和他,榴莲鸡煲溢出的香味,让人垂涎欲滴,鸡肉和榴莲嫩滑的口感,仿佛让人重回到那个十七八岁的青春时光。他叫小润,高中时期经常带着我为非作歹,中午午休跑去打篮球,晚自习溜去操场趟草地上吹牛逼,最刻骨铭心的,还是晚自习偷偷溜去学校附近大厦最高层天台,再爬上去水塔仰望星空,俯视我们的高中,看着每个教室里面一个个奋发图强的同学,我丝毫没有半点做错事的羞愧,眼里只有天上的繁星,地下的灯光,还有旁边的那个他。


小聚


“小饿,我们95后的都已经老了,现在社会都是年轻人00后的天下,比学历能力,985、211一抓一大把,比耐力,我们身体大不如前,精力强壮的年轻人比比皆是...”


“难道你不行了?”


“你别打岔,你这一行不是也有一个35岁的梗吗,你这个前端开发岗位我了解过,是需要不断学习的,技术迭代如此之快,你跟的上吗?”


我默默的摇了摇头,诚然,我是跟不上的,vue2我都没学完vue3就已经出来了,不过我相信我还是极少数。因为我安于现状太久了,拿着不上不下的薪资,没有房贷车贷育儿的压力,不像以前住在城中村每天晚上睡觉听着管道排泄物的声音,没有压力,就没有动力,我就是这么一个充满惰性的人。


小润跟我是高中同学,那时我们的关系不错,但是毕业后各自去往自己的大学,有自己的生活,便没怎么联系了,这次出来也是近三年第一次小聚。他在一个比较老牌的做文具,做设备的大厂工作,主要内容是去一些大型物业竞标,为了竞争得到那个“标”,付出的也不少,陪酒送礼一样不落,但就算得到那个“标”,公司的绩效奖励分配制度却让小润很不满,所以他不禁感慨,“我们每个月累死累活得到的薪资,除去日常花销,本来就已经所剩不多,而且社会上还存在一种叫通货膨胀的东西,想想我们年龄越来越大,面临的职场危机,手上的筹码到底能不能支撑我们维持当前消费水平,过上自己想要的生活,这是一个比较大的问题。”我听得津津有味,虽然心里认为他的说法有点过度焦虑,但有这么一个意识,总是好的,小润看到我向他投向肯定的目光,便继续说道,“这几年我都在看书,其中看到一个企业家有一句创业名言————空手套白狼”。


空手套白狼


小润看着我一脸的疑惑,嘴角微微一笑,一脸正经的告诉我,“空手套白狼虽然百度翻译是个贬义词,但是在创业翻译过来就是用最低的成本,创造最大的价值。我想要做一些0成本,价值高,只需要付出时间的生意”。


“那么请问哪里有那么大的馅饼?”据我所知,现在谈起普通人做副业,想要0成本,要不就是什么做信息差买卖,或者视频搬运,网上一搜一大把,现在根本不是能真正获利的渠道了。当然,也可能有很多人的确做着0成本生意,闷声发大财


微信图片_20230307134118.jpg


小润从煲里夹了一块榴莲肉,放入嘴中品尝了几番后吞入腹中,真诚的向我道来,“之前你有跟我聊过你做的副业,上面的功能我看了,感觉你比较厉害,对小程序开发这一块也是比较熟悉。你有没有看过小区的停车场,白天的时候很多车位都是空闲的,极大部分都是车主开车上班,那么车子不就空闲起来了?我们可以做一个平台,让车主在平台上面登记,只要车位空闲,可以告诉平台某一个时间段空闲,让平台的其他需要在附近停车的用户看到,用户微信支付停留相对应的时间,这样不仅解决了车位紧张的问题,车位车主也能利用闲置的车位赚到一笔钱,平台也能进行抽成。”


我一听,陷入了沉思,感觉好像很有道理的样子,但又觉得哪里不对,“这种做法当然是不可能的,物业停车场大都是一个车牌对应一个停车位,不可能给别人钻这种空子。”


“那你说个der啊”


微信图片_20230307134254.jpg


“刚刚只是我在生活中发现的一些奇思妙想,就是利用闲置这个属性,接下来才是我要说的重点。你平时看街边上停着的电车多吗?”我点了点头,电车在广州这所大城市,那肯定是多的。突然,小润用筷子翻了翻鸡煲中的食物,一脸愤然的对着我说“我擦,那些肥牛都被你吃完了?”我又用筷子探寻了一下,的确,肥牛还真被我吃完了,软嫩的肥牛搭配着由榴莲和鸡煲化学反应产生的汤底,让我感觉到味蕾在跳动,入口即化,难以言喻,自然而然就多吃了几片,我尴尬又不失礼貌的问他,“要不多点一份?”


他笑了笑,摆了摆手,继续说道,“我的想法是将空闲的电车利用起来,做一个平台,平台的载体是小程序,像膜拜小程序一样,用户能找到附近的单车,而我们则是电车,但是我们不需要成本,因为在平台中,电车的信息是由车主自己主动上传上来的,所以就有两个群体,一个是车主,一个是需要用电车的用户。车主能在电车空闲的时间将电车上传到我们的平台,通过出租自己的电车进行赚钱,当出租的次数多了,不仅能回本,到时候或许还能赚点小钱。而普通用户想用电车的时候,根据小程序提供的定位,找到离他最近的那台电车,进行微信支付就能骑走,按照骑行时间进行收费,收费标准由电车车主自己提供。而我们平台的收入,则是对每笔订单进行抽成”。


我一听,又陷入了沉思,又感觉好像很有道理的样子,但又觉得哪里不对,咦,我为什么要说又?


QA



用户场景有哪些,用户需求多吗?



多,平时使用电车都是上班族居多,那上班族使用完电车后电车就闲置了,可以进行出租赚点奶茶钱,何乐而不为?况且平时下班我想去别的地方玩一下,也可以租一台电车去逛一逛,就再也不需要每个人都要买一台电车了。确实,之前去湛江游玩,也有电车提供出租,骑着电车到处逛逛吃吃,真的十分快乐,不过电车是由公司统一提供。



普通用户怎么开启这些电车呢,电车五花八门,难道要让车主统一购买我们提供的电锁进行控制?



目标电车当前只试行小牛和九号电车,用户需要开启电车的时候,在小程序可以找到电车车主联系方式,通过电话联系让他用电车钥匙开启电车,同时在小程序按下开启按钮告诉平台和用户已经开启,开始计费。用户骑行完电车后,用户致电车主进行结算并关闭电车。



客户借车后,将车的某些零件换改,偷窃,损坏,如何处理?例如将电瓶车电池换成低端电池,也能用,,但车主不知道?



这的确是个问题,我也在思考是否有必要弄押金,但是电车的押金弄小了没啥用,弄大了也不合适,没人想进行支付,所以如何平衡这个问题,是我们这个项目后续所要思考的。



用户把电车开到离起始点十万八千里,这样车主怎么找回自己的电车?



好问题,我也有想过,车主在上传电车到平台的时候,可以设置自己的使用类型,可以规定使用用户骑行归还到原位置,也可以不规定,全由车主自由设定



听起来好像真的可以落地,但是用户附近可用的电车如果多起来,在地图上展示密密麻麻,这个需要点技术,我得研究研究



我们初期可能不需要那么复杂,只需要展示一个列表,可以让用户进行筛选,用户能看到每台电车的外观,点击电车详情,就能知道用户与电车的相对位置,不需要在同一个页面展示那么多的标记(如此甚好)

// 小程序在地图上显示用户与标记方法

// js
const markers = [
{
id: 1,
// 标记的大小
width: '40px',
height: '40px',
// 标记的经纬度
longitude,
latitude,
// 标记的icon图标
iconPath
}
]
this.setData({ markers })

// wxml
// center.longitude center.latitude 为中心经纬度
<map class='map' id='map' longitude='{{center.longitude}}' latitude='{{center.latitude}}' markers="{{markers}}" scale='16'></map>


政治问题...



******<-内容加密了


我们聊了很多细节,包括首页如何设计,一键控制电车上线下线,越聊越兴奋,感觉真的可以落地,说到尽情之处,还说日后被大厂收购,实现财富自由指日可待,因为我们都知道,一个产品成熟了,稍微露出苗头,就会被人借鉴。当天晚上我回到家,就把整个大纲梳理了出来,并发给小润看。


dianche.png


但同时我们也发现问题,如果用户在骑行的途中,被车主通过车钥匙远程停车会发生什么事情,之前我们一致认为电车平台会有相对应的API提供,不仅可以获取电车信息(车辆电池,型号,外观元素等),也能有启动车辆和关停车辆的接口,但浏览了两个电车平台的官网,发现平台并没有这种东西,我们的思路一下子遇到卡壳,而且押金问题也是一个重点,热情一下子就冷却了下来,这场看似热血沸腾的副业计划就此搁置了下来。


对于做副业,我个人是非常感兴趣的,低成本的副业能赚钱是根本条件,更主要能拓展人的视野,之前我第一个副业,进行的比较顺利,但前提是市场已经有先驱,可以有模板进行复刻,而这一次纯属天马行空,没有前车之鉴,需要考虑到很多细节,如果有一些致命因素导致项目行不通,那可能这个项目就真的凉了。其实也很合理,世界上人才千千万,一个脑暴出来能赚钱的项目,为什么市场没有落地,或许不是因为没有人能想出来,更大因素是有人想出来了,但是此路不通。


省流


不亏,那顿鸡煲很香,而且是小润掏的钱


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

微信图片防撤回

了解需求 实际生活中,由于好奇朋友撤回的微信图片信息,但直接去要又怎会是我的性格呢。由此萌生出做一个微信防撤回程序(已向朋友说明)。 当前网络上其实存在一些微信防撤回程序,不过担心不正规软件存在漏洞,泄漏个人信息,这里也就不考虑此种方法。 解决方案 思路 由于...
继续阅读 »

了解需求


实际生活中,由于好奇朋友撤回的微信图片信息,但直接去要又怎会是我的性格呢。由此萌生出做一个微信防撤回程序(已向朋友说明)。


当前网络上其实存在一些微信防撤回程序,不过担心不正规软件存在漏洞,泄漏个人信息,这里也就不考虑此种方法。


解决方案


思路


由于当前微信不支持微信网页版登陆,因此使用itchat的方法不再适用。


后来了解到电脑端微信图片会先存储在本地,撤回后图片再从本地删除,因此只要在撤回前将微信本地图片转移到新文件夹即可。


在此使用Python的watchdog包来监视文件系统事件,例如文件被创建、修改、删除、移动,我们只需监听创建文件事件即可。


安装watchdog包:    pip install watchdog
我的python环境为python3.9版本

实现


1.首先进行文件创建事件监听,在监听事件发生后的事件处理对象为复制微信图片到新文件夹。具体代码如下。


需要注意的是微信在2022.05前,图片存储在images目录下;在2022.05后,图片存储在MsgAttach目录下,并按微信对象分别进行存储。


# 第一步:加载路径,并实时读取JPG信息
import os
import shutil
import time
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler

def mycopyfile(srcfile,dst_dir):
if not os.path.isfile(srcfile):
print ("%s not exist!"%(srcfile))
else:
fpath,fname=os.path.split(srcfile) # 分离文件名和路径
if fname.endswith('.jpg') or fname.endswith('.png') or fname.endswith('.dat'):
dst_path = os.path.join(dst_dir, fname)
shutil.copy(srcfile, dst_path) # 复制文件

class MyEventHandler(FileSystemEventHandler):
# 文件移动
# def on_moved(self, event):
# print("文件移动触发")
# print(event)


def on_created(self, event):
# print("文件创建触发")
print(event)
mycopyfile(event.src_path, dst_dir)


# def on_deleted(self, event):
# print("文件删除触发")
# print(event)
#
# def on_modified(self, event):
# print("文件编辑触发")
# print(event)

if __name__ == '__main__':

dst_dir = r"E:\03微信防撤回\weixin" #TODO:修改为自己的保存文件目录
if not os.path.exists(dst_dir):
os.makedirs(dst_dir)

observer = Observer() # 创建观察者对象
file_handler = MyEventHandler() # 创建事件处理对象
listen_dir = r"C:\Users\hc\Documents\WeChat" #TODO:修改为自己的监听目录
observer.schedule(file_handler, listen_dir, True) # 向观察者对象绑定事件和目录
observer.start() # 启动
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
observer.stop()
observer.join()

2.由于微信保存文件以.dat格式保存,因此需要对微信文件格式进行解码,具体解码代码如下。


# weixin_Image.dat 破解
# JPG 16进制 FF D8 FF
# PNG 16进制 89 50 4e 47
# GIF 16进制 47 49 46 38
# 微信.bat 16进制 a1 86----->jpg ab 8c----jpg dd 04 --->png
# 自动计算异或 值
import os

into_path = r'E:\03微信防撤回\weixin' # 微信image文件路径
out_path = r'E:\03微信防撤回\image'

def main(into_path, out_path):

dat_list = Dat_files(into_path) # 把路径文件夹下的dat文件以列表呈现
lens = len(dat_list)
if lens == 0:
print('没有dat文件')
exit()

num = 0
for dat_file in dat_list: # 逐步读取文件
num += 1
temp_path = into_path + '/' + dat_file # 拼接路径:微信图片路径+图片名
dat_file_name = dat_file[:-4] # 截取字符串 去掉.dat
imageDecode(temp_path, dat_file_name, out_path) # 转码函数
value = int((num / lens) * 100) # 显示进度
print('正在处理--->{}%'.format(value))


def Dat_files(file_dir):
"""
:param file_dir: 寻找文件夹下的dat文件
:return: 返回文件夹下dat文件的列表
"""

dat = []
for files in os.listdir(file_dir):
if os.path.splitext(files)[1] == '.dat':
dat.append(files)
return dat

def imageDecode(temp_path, dat_file_name, out_path):
dat_read = open(temp_path, "rb") # 读取.bat 文件
xo, j = Format(temp_path) # 判断图片格式 并计算返回异或值 函数

if j == 1:
mat = '.png'
elif j == 2:
mat = '.gif'
else:
mat = '.jpg'

out = out_path + '/' + dat_file_name + mat # 图片输出路径
png_write = open(out, "wb") # 图片写入
dat_read.seek(0) # 重置文件指针位置

for now in dat_read: # 循环字节
for nowByte in now:
newByte = nowByte ^ xo # 转码计算
png_write.write(bytes([newByte])) # 转码后重新写入


def Format(f):
"""
计算异或值
各图片头部信息
png:89 50 4e 47
gif: 47 49 46 38
jpeg:ff d8 ff
"""

dat_r = open(f, "rb")

try:
a = [(0x89, 0x50, 0x4e), (0x47, 0x49, 0x46), (0xff, 0xd8, 0xff)]
for now in dat_r:
j = 0
for xor in a:
j = j + 1 # 记录是第几个格式 1:png 2:gif 3:jpeg
i = 0
res = []
now2 = now[:3] # 取前三组判断
for nowByte in now2:
res.append(nowByte ^ xor[i])
i += 1
if res[0] == res[1] == res[2]:
return res[0], j
except:
pass
finally:
dat_r.close()


# 运行
if __name__ == '__main__':
main(into_path, out_path)
复制代码
作者:空气猫
来源:juejin.cn/post/7221376169370583101
>
收起阅读 »

前端路由访问权限控制方案

web
本篇所讲的路由控制方案是由前端实现的,根据具体的业务做的设计,可能不具备一般性,仅供参考! 项目背景及路由初步设计 目前在做的一个项目,目标是为了解决互联网行业里面关于资金清分业务的一些痛点。虽然目前只成功对接并上线了一个第三方企业,随着产品功能的不断完善,...
继续阅读 »

本篇所讲的路由控制方案是由前端实现的,根据具体的业务做的设计,可能不具备一般性,仅供参考!



项目背景及路由初步设计


目前在做的一个项目,目标是为了解决互联网行业里面关于资金清分业务的一些痛点。虽然目前只成功对接并上线了一个第三方企业,随着产品功能的不断完善,相信后续还会有更多的第三方企业对接,以及更多的业务场景。


这就需要提前对系统的菜单权限进行规划,由于后期开发的不确定性以及人力资源有限,路由权限控制没有采用跟后端强耦合的方式实现,由前端自行配置并处理。


一开始由于意向企业业务上的强相关性,并没有规划太多的模块(主业务全都写在了src/views/main目录下),也没有对用户行为进行规划分类,路由控制方面也是根据平台标识手动配置的路由表


const xxx = [
'/main/nopage',
'/main/checkFace',
// ...
]

缺点


最近又做了一个B端的项目,发现上面的实现方法并不是很好,原因有以下几点:




  1. 对接平台多的话,就会出现一堆路由配置,不优雅、不美观




  2. 业务上的不一致性带来扩展的不灵活




  3. 暂时没有想起来 : )




经过一番考量,我决定这样做(其实也是常见的方法)


改进方案


首先业务实现上需要我新建一个文件目录(src/views/xxx,不能再往main目录下放了),在里面写路由组件。


期间我想过以对接的平台标识创建路由组件目录,进行业务上的隔离,没有做出实际尝试就被我舍弃了,原因是:以平台标识作为业务的根目录,跟原先的做法本质上是一致的,只是改进,相当于是补丁,而我要做的是寻找另一种解决办法。


根据Linux系统一切皆文件的思想,类似的,我还是采用了老套的办法,给每一个路由菜单赋予一个访问权限的配置。


这样做,后面维护起来也简单(有了平台标识和用户行为的划分)


{
path: "/test",
name: "Test",
meta: {
belong: ["xxx", "xxx"] // 所属平台信息,操作行为信息...
},
component: () => import("@/views/test")
},

后端同事配合规划用户平台和行为,在用户访问的时候,后端返回用户信息的同时,返回平台标识和行为标识。


同样的,在全局路由钩子里验证访问权限。



router.beforeEach((to, from, next) => {
try {
const { belong = [] } = to.meta
const authInfo = ["platform", "action"]
if (accessTokenStr) {
// 已登录, 做点什么
// belong <--> authInfo
// arrayInclude(belong, authInfo)
} else {
next()
}
} catch (err) {
console.log(err);
}
})

/**
* 判断数组元素的包含关系,不要求顺序一致
* 数组中不存在重复元素
* 用于验证路由权限
* arrA包含arrB,返回true, 否则返回false
*/

export const arrayInclude = (arrA, arrB) => {
let flag = true
for (let i = 0; i < arrB.length; i++) {
if (!arrA.includes(arrB[i])) {
flag = false
break
}
}
return flag
}


👉👉以上方案写于2021-11-06





维护总结


2023-04-03


近期业务扩展,发现上面的菜单权限控制有点不合理


这种配置不直观,有点混乱!!!


还是采用json的方式分配路由, 比如:


const menus = {
[platformId]: [
"/a"
"/b"
],
[platformId]: [
"/a"
"/b"
],
}

这样可以更加直观的显示出来某个业务包含哪些菜单,而不是像之前那样把菜单的权限配置在路由上!


总结: 路由设计要中电考虑可读性、易维护性




我是 甜点cc,个人网站(国外站点): blog.i-xiao.space/


公众号

作者:甜点cc
来源:juejin.cn/post/7239173692228255802
:【看见另一种可能】

收起阅读 »

一文搞清楚Node.js的本质

web
学习Node.js已有很长的时间了,但一直学的懵懵懂懂,不得要领,现决定跟网上的大佬从头开始理一下其中的底层逻辑,为早日成为全栈工程师打下基础。 Node.js 是什么? Node.js 是一个基于 V8 引擎 的 JS 运行时,它由 Ryan Dahl 在 ...
继续阅读 »

学习Node.js已有很长的时间了,但一直学的懵懵懂懂,不得要领,现决定跟网上的大佬从头开始理一下其中的底层逻辑,为早日成为全栈工程师打下基础。


Node.js 是什么?


Node.js 是一个基于 V8 引擎JS 运行时,它由 Ryan Dahl 在 2009 年创建。


这里有两个关键词,一是 JS 引擎,二是 JS 的运行时


那什么叫 JS 引擎呢?


image.png


JS 引擎就是把一些 JS 的代码进行解析执行,最后得到一个结果。


比如,上图的左边是用 JS 的语法定义一个变量 a 等于 1,b 等于 1,然后把 a 加 b 的值赋值给新的变量 c,接着把这个字符串传入 JS 引擎里,JS 引擎就会进行解析执行,执行完之后就可以得到对应的结果。


那么 JS 运行时又是什么呢?它和 JS 本身有什么区别 ?


要搞清楚上面的问题,可以看下面这张图:


image.png


从下往上看:




  1. 最下面一层是脚本语言规范ECMAScript,也就是常说的ES5、ES6语法。




  2. 往上一层就是对于该规范的实现了,如JavaScriptJScript等都属于对 ECMAScript语言规范的实现。




  3. 再往上一层就是执行引擎JavaScript 常见的引擎有 V8QuickJS等,用来解释执行代码。




  4. 最上面就是运行时环境了,比如基于 V8 封装的运行时环境有 ChromiumNode.jsDeno 等等。




可以看到,JavaScript 在第二层,Node.js 则在第四层,两个根本不是一个东西。


所以,Node.js 并不是语言,而是一个 JavaScript 运行时环境,它的语言是 JavaScript。这就跟 PHP、Python、Ruby 这类不一样,它们既代表语言,也可代表执行它们的运行时环境(或解释器)。


JS 作为一门语言,有独立的语法规范,提供一些内置对象和 API(如数组、对象、函数等)。但和其他语言(C、C++等)不一样的是,JS 不提供网络、文件、进程等功能,这些额外的功能是由运行时环境实现和提供的,比如浏览器或 Node.js


所以,JS 运行时可以理解为 JS 本身 + 一些拓展的能力所组成的一个运行环境,如下面这张图:


image.png


可以看到,这些运行时都不同程度地拓展了 JS 本身的功能。JS 运行时封装底层复杂的逻辑,对上层暴露 JS API,开发者只需要了解 JS 的语法,就可以使用这些 JS 运行时做很多 JS 本身无法做到的事情。


Node.js 的组成


搞清楚了什么是Node.js后,再来看看 Node.js 的组成。


Node.js 主要是由 V8Libuv 和一些第三方库组成的。


V8引擎


V8 是一个 JS 引擎,它不仅实现了 JS 解析和执行,还支持自定义拓展。


这有什么用处呢?


比如说,在下面这张图中我们直接使用了 A 函数,但 JS 本身并没有提供 A 这个函数。这个时候,我们给 V8 引擎提供的 API 里注入一个全局变量 A ,就可以直接在 JS 中使用这个 A 函数了。正是因为 V8 支持这个自定义的拓展,才有了 Node.js 等 JS 运行时


image.png


Libuv


Libuv 是一个跨平台的异步 IO 库,它主要是封装各个操作系统的一些 API,提供网络还有文件进程这些功能


我们知道在 JS 里面是没有网络文件这些功能的,前端是由浏览器提供,而 Node.js 里则是由 Libuv 提供


image.png




  1. 左侧部分是 JS 本身的功能,也就是 V8 实现的功能。




  2. 中间部分是一些C++ 胶水代码。




  3. 右侧部分是 Libuv 的代码。




V8Libuv 通过第二部分的胶水代码粘合在一起,最后就形成了整一个 Node.js


因此,在 Node.js 里面不仅可以使用 JS 本身给我们提供的一些变量,如数组、函数,还能使用 JS 本身没有提供的 TCP、文件操作和定时器功能


这些扩展出来的能力都是扩展到V8上,然后提供给开发者使用,不过,Node.js 并不是通过全局变量的方式实现扩展的,它是通过模块加载来实现的。


第三方库工具库


有了 V8 引擎和拓展 JS 能力的 Libuv,理论上就可以写一个 JS 运行时了,但是随着 JS 运行时功能的不断增加,Libuv 已经不能满足需求,比如实现加密解密、压缩解压缩。


这时候就需要使用一些经过业界验证的第三方库,比如异步 DNS 解析 c-ares 库、HTTP 解析器 llhttp、HTTP2 解析器 nghttp2、解压压缩库 zlib、加密解密库 openssl 等等。


Node.js 代码组成


了解了 Node.js 的核心组成后,再来简单看一下 Node.js 代码的组成。


image.png


Node.js 代码主要是分为三个部分,分别是 CC++JavaScript


JS


JS 代码就是我们平时使用的那些 JS 模块,像 http 和 fs 这些模块


Node.js 之所以流行,有很大一部分原因在于选择了 JS 语言。


Node.js 内核通过 CC++ 实现了核心的功能,然后通过 JS API 暴露给用户使用,这样用户只需要了解 JS 语法就可以进行开发。相比其他的语言,这个门槛降低了很多。


C++


C++代码主要分为三个部分:




  1. 第一部分主要是封装 Libuv 和第三方库的 C++ 代码,比如 netfs 这些模块都会对应一个 C++ 模块,它主要是对底层 Libuv 的一些封装。




  2. 第二部分是不依赖 Libuv 和第三方库的 C++ 代码,比方像 Buffer 模块的实现,主要依赖于 V8




  3. 第三部分 C++ 代码是 V8 本身的代码。




C++ 代码中最重要的是了解如何通过 V8 API 把 C、C++ 的能力暴露给 JS 层使用,正如前面讲到的通过拓展一个全局变量 A,然后就可以在 JS层使用 A。


C 语言层


C 语言代码主要是包括 Libuv 和第三方库的代码,它们大多数是纯 C 语言实现的代码。


Libuv 等库提供了一系列 C API,然后在 Node.jsC++ 层对其进行封装使用,最终暴露 JS APIJS 层使用。


总结


文章第一部分介绍了Node.js的本质,它实际上是一个JS运行时,提供了网络、文件、进程等功能,类似于浏览器,提供了JS的运行环境。


第二部分介绍了Node.js的组成,它由V8引擎、Libuv及第三方库构成,Node.js核心功能大都是Libuv提供的,它封装底层各个操作系统的一些 API,因此,Node.js是跨平台的。


第三部分从代码的角度描述了Node.js的组成,包括JavaScriptC++C三部分,中间的C++部分通过对C部分的封装提供给JavaScript部分使用。


作者:小p
来源:juejin.cn/post/7238814783598297144
收起阅读 »

Android 14 新增权限

原文: medium.com/proandroidd… 译者:程序员 DHL 本文已收录于仓库 Technical-Article-Translation 这篇文章,主要分享在 Android 14 以上新增的权限 READ_MEDIA_VISUAL_US...
继续阅读 »



这篇文章,主要分享在 Android 14 以上新增的权限 READ_MEDIA_VISUAL_USER_SELECTED,该权限允许用户仅授予对选定媒体的访问权限(Photos / Videos)),而不是访问整个媒体库。


新的权限弹窗


当你的 App 运行在 Andrid 14 以上的设备时,如果请求访问照片,会出现以下对话框,你将看到新的选项。



受影响的行为


当我们在项目中声明新的权限 READ_MEDIA_VISUAL_USER_SELECTED ,并且用户选择 Select photos and videos(Select photos or Select videos)




  • READ_MEDIA_IMAGESREAD_MEDIA_VIDEO 权限都会被拒绝




  • READ_MEDIA_VISUAL_USER_SELECTED 权限被授予时,将会被允许临时访问用户的照片和视频




  • 如果我们需要访问其他照片和视频,我们需要同时申请 READ_MEDIA_IMAGES 或者 READ_MEDIA_VIDEO 权限




如何在项目中使用新的权限



  • AndroidManifest.xml 文件中添加下面的权限


<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

// new permisison
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />


  • 使用 ActivityResultContract 请求新的权限


val permissionLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { mapResults ->
mapResults.forEach {
Log.d(TAG, "Permission: ${it.key} Status: ${it.value}")
}
// check if any of the requested permissions is granted or not
if (mapResults.values.any { it }) {
// query the content resolver
queryContentResolver(context) { listOfImages ->
imageDataModelList = listOfImages
}
}
}

为什么要使用 RequestMultiplePermissions,因为我们需要同时请求 READ_MEDIA_IMAGES , READ_MEDIA_VIDEO 权限



  • 启动权限申请流程


OutlinedButton(onClick = {
permissionLauncher.launch(arrayOf(READ_MEDIA_IMAGES, READ_MEDIA_VISUAL_USER_SELECTED))
}) {
Text("Allow to read all or select images")
}

关于 Android 12 、 Android 13 、Android 14 功能和权限的变更,点击下方链接前往查看:



最后我们看一下运行效果





全文到这里就结束了,感谢你的阅读,坚持原创不易,欢迎在看、点赞、分享给身边的小伙伴,我会持续分享原创干货!!!




我开了一个云同步编译工具(SyncKit),主要用于本地写代码,同步到远程设备,在远程设备上进行编译,最后将编译的结果同步到本地,代码已经上传到 Github,欢迎前往仓库 hi-dhl/SyncKit 查看。





Hi 大家好,我是 DHL,就职于美团、快手、小米。公众号:ByteCode ,分享有用、有趣的硬核原创内容,Kotlin、Jetpack、性能优化、系统源码、算法及数据结构、动画、大厂面经,真诚推荐你关注我。





最新文章



开源新项目




  • 云同步编译工具(SyncKit),本地写代码,远程编译,欢迎前去查看 SyncKit




  • KtKit 小巧而实用,用 Kotlin 语言编写的工具库,欢迎前去查看 KtKit




  • 最全、最新的 AndroidX Jetpack 相关组件的实战项目以及相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看 AndroidX-Jetpack-Practice




  • LeetCode / 剑指 offer,包含多种解题思路、时间复杂度、空间复杂度分析,在线阅读




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

如何使用localStorage判断设置值是否过期

web
简介:本文介绍了使用 localStorage 判断设置值是否过期的方法。通过设置过期时间,我们可以使用 setItemWithExpiration 函数将数据存储到 localStorage 中,并使用 getItemWithExpiration 函数获取数...
继续阅读 »

简介:本文介绍了使用 localStorage 判断设置值是否过期的方法。通过设置过期时间,我们可以使用 setItemWithExpiration 函数将数据存储到 localStorage 中,并使用 getItemWithExpiration 函数获取数据并检查是否过期。localStorage 提供简单的 API 方法来存储、检索和删除数据,并具有持久性和域隔离的特点。通过本文的方法,你可以方便地管理数据,并灵活设置过期时间,实现更多的数据存储和管理功能。



目标:在网站中实现定期弹窗功能,以提示用户。


选择实现方式:为了实现持久化存储和检测功能,我们选择使用localStorage作为首选方案。


解决方案:



  1. 存储设置值:使用localStorage将设置值存储在浏览器中,以便在用户访问网站时保持数据的持久性。

  2. 设置过期时间:根据需求,为设置值设定一个过期时间,表示多长时间后需要再次弹窗提示用户。

  3. 检测过期状态:每次用户访问网站时,检测存储的设置值是否已过期。若过期,则触发弹窗功能,提醒用户。

  4. 更新设置值:在弹窗提示后,可以根据用户的操作进行相应的更新操作,例如延长过期时间或更新设置内容。


优势:



  1. 持久化存储:使用localStorage可以将设置值保存在浏览器中,即使用户关闭了浏览器或重新启动设备,设置值仍然有效。

  2. 简单易用:localStorage提供了简单的API,方便存储和读取数据,实现起来相对简单。

  3. 跨浏览器支持:localStorage是HTML5的标准特性,几乎所有现代浏览器都支持它,因此具有良好的跨浏览器兼容性。


注意事项:



  1. 需要根据业务需求合理设置过期时间,避免频繁弹窗对用户体验造成困扰。

  2. 在使用localStorage时,要考虑浏览器的隐私设置,以确保能够正常存储和读取数据。


说一下locaStorage


localStorage 是 Web 浏览器提供的一种存储数据的机制,它允许在浏览器中存储和检索键值对数据。


以下是关于 localStorage 的一些重要特点和使用方法:




  1. 持久性:与会话存储(session storage)相比,localStorage 是一种持久性的存储机制。存储在 localStorage 中的数据在用户关闭浏览器后仍然保留,下次打开网页时仍然可用。




  2. 容量限制:每个域名下的 localStorage 存储空间通常为 5MB。这个限制是针对整个域名的,不是针对单个页面或单个 localStorage 对象的。




  3. 键值对数据存储:localStorage 使用键值对的方式存储数据。每个键和对应的值都是字符串类型。如果要存储其他数据类型(如对象或数组),需要进行序列化和反序列化操作。




  4. API 方法:


    属性方法
    localStorage.setItem(key, value)将键值对存储到 localStorage 中。如果键已存在,则更新对应的值。
    localStorage.getItem(key)根据键获取存储在 localStorage 中的值
    localStorage.removeItem(key)根据键从 localStorage 中删除对应的键值对
    localStorage.clear()清除所有存储在 localStorage 中的键值对。
    localStorage.key(index)根据索引获取对应位置的键名。



  5. 域限制:localStorage 存储的数据与特定的域名相关。不同的域名之间的 localStorage 是相互隔离的,一个网站无法访问另一个网站的 localStorage 数据。




  6. 安全性:localStorage 中的数据存储在用户的本地浏览器中,因此可以被用户修改或删除。敏感数据应该避免存储在 localStorage 中,并使用其他更安全的机制来处理。




以下是一个示例,展示如何使用 localStorage 存储和检索数据:


// 存储数据到 localStorage
localStorage.setItem('name', 'localStorage');
localStorage.setItem('size', '5mb');

// 从 localStorage 中获取数据
const name = localStorage.getItem('name');
const age = localStorage.getItem('size');

console.log(name); // 输出: localStorage
console.log(size); // 输出: 5mb

// 从 localStorage 中删除数据
localStorage.removeItem('size');

// 清除所有的 localStorage 数据
localStorage.clear();

总结来说,localStorage 是一种用于在 Web 浏览器中持久存储键值对数据的机制。它提供简单的 API 方法来存储、检索和删除数据,并具有一定的容量限制和域隔离。


判断本地存储时间的实现


存储时间与值


使用以下代码将值与过期时间存储到localStorage中:


const expiration = 24 * 60 * 60 * 1000 * 7; // 设置过期时间为七天
// const expiration = 2 * 60 * 1000; // 设置过期时间为2分钟
setItemWithExpiration("read_rule", true, expiration);

下面是相应的函数实现:


// 存储数据到LocalStorage,并设置过期时间(单位:毫秒)
function setItemWithExpiration(key, value, expiration) {
   const item = {
       value: value,
       expiration: Date.now() + expiration
  };
   localStorage.setItem(key, JSON.stringify(item));
}

获取值并判断是否过期


使用以下代码从localStorage中获取值,并判断其是否过期:


const retrievedData = getItemWithExpiration("read_rule");

下面是相应的函数实现:


// 从LocalStorage中获取数据,并检查是否过期
function getItemWithExpiration(key) {
   const item = JSON.parse(localStorage.getItem(key));
   if (item && Date.now() < item.expiration) {
       return item.value;
  }
   // 如果数据已过期或不存在,则返回 null 或其他默认值
   return null;
}

通过以上实现,可以方便地存储带有过期时间的值,并根据需要获取和判断其有效性。如果存储的数据已过期或不存在,函数getItemWithExpiration将返回null

作者:猫头_
来源:juejin.cn/post/7238794430966677564
其他您设定的默认值。

收起阅读 »

程序员的坏习惯

前言 每位开发人员在自己的职业生涯、学习经历中,都会出一些坏习惯,本文将列举开发人员常犯的坏习惯。希望大家能够意识和改变这些坏习惯。 不遵循项目规范 每个公司都会定义一套代码规范、代码格式规范、提交规范等,但是有些开发人员就是不遵循相关的 规范,命名不规范、...
继续阅读 »

前言


每位开发人员在自己的职业生涯、学习经历中,都会出一些坏习惯,本文将列举开发人员常犯的坏习惯。希望大家能够意识和改变这些坏习惯。


图片.png


不遵循项目规范


每个公司都会定义一套代码规范、代码格式规范、提交规范等,但是有些开发人员就是不遵循相关的 规范,命名不规范、魔鬼数字、提交代码覆盖他人代码等问题经常发生,如果大家能够遵循相关规范,这些问题都可以避免。


用复杂SQL语句来解决问题


程序员在开发功能时,总想着是否能用一条SQL语句来完成这个功能,于是实现的SQL语句写的非常复杂,包含各种子查询嵌套,函数转换等。这样的SQL语句一旦出现了性能问题,很难进行相关优化。


缺少全局把控思维,只关注某一块业务


新增新功能只关注某一小块业务,不考虑系统整体的扩展性,其他模块已经有相关的实现了,却又重复实现,导致重复代码严重。修改功能不考虑对其他模块的影响。


函数复杂冗长,逻辑混乱


一个函数几百行,复杂函数不做拆分,导致代码变得越来月臃肿,最后谁也不敢动。函数还是要遵循设计模式的单一职责,一个函数只做一件事情。如果函数逻辑确实复杂,需要进行拆分,保证逻辑清晰。


缺乏主动思考,拿来主义


实现相关功能,先网上百度一下,拷贝相关的代码,能够运行成功认为万事大吉。到了生产却出现了各种各样的问题,因为网上的demo程序和实际项目的在场景使用上有区别,尤其是相关的参数配置,一定要弄清楚具体的含义,不同场景下,设置参数的值不同。


核心业务逻辑,缺少相关日志和注释


很多核心的业务逻辑实现,整个方法几乎没看到相关注释和日志打印,除了自己能看懂代码逻辑,其他人根本看不懂。一旦生产出了问题,找不到有效的日志输出,问题根本无法定位。


修改代码,缺少必要测试


很多人都会存在侥幸心里,认为只是改了一个变量或者只修改一行代码,不用自测了应该没有问题,殊不知就是因为改一行代码导致了严重的bug。所以修改代码一定要进行自测。


需求没理清,直接写代码


很多程序员在接到需求后,不怎么思考就开始写代码,写着写着发现自己的理解与实际的需求有偏差,造成无意义返工。所以需要多花些时间梳理需求,整理相关思路,能规避很多不合理的问题。


讨论问题,表达没有逻辑、没有重点


讨论问题不交代背景,上来就说自己的方案,别人听得云里雾里,让你从头描述你又讲不明。需要学会沟通和表达,才能进行有效的沟通和合作。


不能从错误中吸取教训


作为一位开发人员,你会犯很多错误,这不可避免也没什么大不了的。但如果你总是犯同样的错误,不能从中吸取教训,那态度就出现问题了。


总结


关于这些坏习惯,你是否中招了,大家应该尽早规避这些坏习惯,成为一名优秀的程序员。


作者:剑圣无痕
来源:juejin.cn/post/7136455796979662862
收起阅读 »

CSS样式穿透?你准备好了吗!

web
你是否遇到过这样的情况:想要修改子元素的样式却发现使用父元素选择器无法生效。这时,你就需要了解一下CSS样式穿透的概念。 简单介绍 一般来说,我们可以通过父级选择器来选中它下面的子元素。例如: .parent .child { color: red; } ...
继续阅读 »

cover.png


你是否遇到过这样的情况:想要修改子元素的样式却发现使用父元素选择器无法生效。这时,你就需要了解一下CSS样式穿透的概念。


简单介绍


一般来说,我们可以通过父级选择器来选中它下面的子元素。例如:


.parent .child {
color: red;
}

但是,有些时候我们需要给子元素中特定的元素修改样式,而不是所有的子元素都修改。这时,我们就需要了解CSS样式穿透这个概念。


CSS样式穿透


在CSS中,我们可以使用“/deep/”、“::v-deep”、“::shadow”等方式实现CSS样式的穿透。


使用/deep/


通过使用/deep/关键字,可以达到子组件穿透自身样式的目的。例如:


.parent /deep/ .child {
color: red;
}

这种方式相比于上述普通方法,能够选中更深层次的子元素(即使用多个空格连接的子元素)。但是,由于浏览器对“/deep/”选择器支持并不友好,因此尽量避免使用。


使用::v-deep


在Vue框架中,如果需要穿透组件样式,可以使用::v-deep或者>>>选择器。例如:


.parent ::v-deep .child {
color: red;
}

这种方式只对Vue组件可用,且与/deep/的作用类似。


使用::shadow


在Web Components规范中,定义了Shadow DOM的概念,它能够使得元素的样式隔离开来,不会影响到其它元素。如果我们需要在Shadow DOM中修改样式,可以使用::shadow伪类。


parent::shadow .child {
color: red;
}

这种方式相比较于上述两种方法,更加安全和规范,但需要先了解Shadow DOM的概念。


补充说明


尽管CSS样式穿透能够方便地修改子元素样式,但是在实际开发中还是应当尽可能地避免使用它们。


CSS一直致力于封装样式,降低代码耦合度,而使用CSS样式穿透会将样式的层级深度加深,增加样式的维护成本。


此外,在跨浏览器、跨框架的情况下,CSS样式穿透的表现都不尽相同,因此建议在项目中谨慎使用。


结语


CSS样式穿透虽然能够带来方便,却也需要谨慎使用,遵循代码封装的原则,保持样式的简洁、规范和易维护。


作者:𝑺𝒉𝒊𝒉𝑯𝒔𝒊𝒏𝒈
来源:juejin.cn/post/7238999952553771066
收起阅读 »

为什么面试官这么爱问性能优化?

web
笔者是一个六年前端,没有大厂经历,也没有什么出彩的项目,所以今年以来,前端现在这种行情下并没有收到多少面试,但是为数不多的面试中,百分之九十都问到了性能优化的问题,而且问题都出奇的一致: 平时的工作中你有做过什么性能优化? 对于这个问题其实我的内心os是(...
继续阅读 »

笔者是一个六年前端,没有大厂经历,也没有什么出彩的项目,所以今年以来,前端现在这种行情下并没有收到多少面试,但是为数不多的面试中,百分之九十都问到了性能优化的问题,而且问题都出奇的一致:



平时的工作中你有做过什么性能优化?



对于这个问题其实我的内心os是(各位轻喷~):



你们怎么都这么爱问性能优化的问题?我的简历中也没有写到这个啊。


你们的业务都这么复杂吗?怎么动不动就要性能优化?


你们的代码写的这么拉吗?不优化都不能使用吗?


性能优化是一个高级前端的必要技能吗?



首先客观现实是笔者平时工作中的业务并不复杂,需要性能优化的地方确实不多,一些存在性能瓶颈的大多是使用了其他团队开发的东西,比如播放直播视频的SDK、3D地图引擎等,也找过他们进行优化,但是没用,他们也优化不动。


所以每次被问到这个问题我就很尴尬,说工作中没有遇到过性能问题,估计面试官也不信,直接说没有做过性能优化,那又显得我这个六年经验的前端太水了,连这个都不做,所以每次我只能硬说。


没吃过猪肉,还没见过猪跑吗?其实性能优化的文章我也看过很多,各种名词我还是知道一点的,比如:



  • 性能问题排查:



1.数据埋点上报


2.使用控制台的NetWork、Performance等工具


3.webpack-bundle-analyzer插件分析打包产物




  • http相关:



1.gzip压缩


2.强缓存、协商缓存




  • 图片相关:



1.图片压缩


2.图片懒加载


3.雪碧图、使用字体图标、svg




  • webpack相关:



1.优化文件搜索


2.多进程打包


3.分包


4.代码压缩


5.使用CDN




  • 框架相关:



1.vue性能优化、react性能优化


2.异步组件


3.tree shaking


4.服务端渲染




  • 代码实现



1.按需加载,逻辑后移,优先保证首屏内容渲染


2.复杂计算使用web worker


3.接口缓存、计算结果缓存


4.预加载


5.骨架屏


6.虚拟滚动



等等。


但这些绝大部分我并没有实践过,所以我都说不出口,说我没有机会实践也行,说我没有好奇心不好学不爱思考不主动发现问题也行,总之结果就是没有经验。


所以通常我硬着头皮只能说出以下这些:


1.开发前会花点时间梳理业务,全局视角过一遍交互和视觉,思考组件划分,找出项目中相似的部分,提取为公共组件和通用逻辑。


2.代码开发中尽量保证写出的代码清晰、可维护,比如:清晰的目录和文件结构、添加必要的注释、提取公共函数公共组件、组件单向数据流、组件功能尽量单一等。


3.时刻关注可能会存在性能问题的部分,比如:



路由组件异步加载


动态加载一些初始不需要用到的资源


频繁切换的组件使用KeepAlive进行缓存


缓存复杂或常用的计算结果


对实时性不高的接口进行缓存


同一个接口多次请求时取消上一次没有完成的请求


页面中存在很多接口时进行优先级排序,优先请求页面重要信息的接口,并关注同一时刻请求的接口数量,如果过多进行分批请求


对于一些确实比较慢的接口使用loading或骨架屏


懒加载列表,懒加载图片,对移出可视区的图片和dom进行销毁


关注页面中使用到的图片大小,推动后端进行图片压缩


地图撒点时使用聚合减少地图引擎渲染压力


对于一些频繁的操作使用防抖或节流


使用三方库或组件库尽量采用按需加载,减少打包体积


组件卸载时取消事件的监听、取消组件中的定时器、销毁一些三方库的实例



我工作中的实践也就以上这些,其实就是写代码的基本要求,另外我觉得如果业务复杂,以上这些也并不能阻止性能问题的出现,更多的还是当出现了问题,去思考如何解决。


比如我开源的一个思维导图项目mind-map,当节点数量多了会非常卡,调试分析思考后发现原因是做法不合理,每次画布上有操作后都是清空画布上的所有元素,然后重新创建所有元素,数据驱动视图,原理非常简单,但是因为是通过svg实现,所以就是DOM节点,这玩意我们都知道,当节点数量非常多以后,删除节点和创建节点都是非常耗时的,所以数据驱动视图的框架,比如Vue会通过虚拟DOM的diff算法对比来找出最小的更新部分,但是我没有做。。。所以。。。那么我就自然的做了一些优化,比如:



思维导图场景,大部分情况下操作的其实就是其中一个或部分节点,所以不需要重新删除创建所有元素,那么就可以通过节点复用的方式来优化,将真实节点缓存起来,渲染时通过数据唯一的id来检查是否存在可复用节点,如果没有,那么代表是新增节点,那么创建新节点即可;如果有,那么就判断节点数据是否发生改变,没有改变直接复用,如果发生了改变那么判断是否可以进行更新,如果更新成本高那么直接重新创建;另外也需要和上一次的缓存进行对比,找出本次渲染不需要的节点进行删除;当然,为了避免缓存节点数量无限膨胀,也通过LRU缓存算法来管理


对于不影响其他节点的操作只更新被操作的节点


通过setTimeout异步渲染节点,留一些中间时间来响应页面其他操作


将触发渲染的任务放到队列中,在下一帧进行处理,合并掉一些中间状态


对于鼠标移动和滚动的场景,通过节流来优化


进行一些取舍,早期节点激活时可以修改节点的所有样式,导致激活操作需要重新计算节点大小,更新节点样式,在多选和全选操作下非常耗时,所以后期改为只允许修改不改变节点大小的样式属性


其他一些细节优化:对于数据没有改变的操作不触发赋值或函数调用,一些不起眼的操作可能也是需要耗费时间的;改变了不涉及节点大小的属性不触发节点大小重新计算等



经过以上这些修改后,性能确实有了很大程度的提升,不过有些项目可以通过不断的优化来提升性能,但是有些可能就是设计缺陷,比如我开源的另一个白板项目,更好的方式其实是重做它。


写到这里其实并没有解决本文标题提出的问题:



为什么面试官这么爱问性能优化?



因为我没有怎么做过面试官,甚至面试经验其实都不太多,写这篇文章目的主要有两个:


1.想听听有面试官经验的各位的想法或建议


2.想看看和我有类似情况的面试者面对这个问题,或者说类似的问题是如何回答的


最后再牢骚几句:



有时会感慨时间过的真快,一转眼,作为一个前端已经工作了六年,即将三十而立却立不起来,这么多年的工作,更多的只是收获了六年的经历,但是并没有六年的能力,回过头看,当初的有些选择确实是错误的,也许这就是人生把。


作为一个普通的前端,在如今的行情下面试确实很艰难,尤其是我这种不擅长面试的人,不过话说回来,改变哪有不痛苦的,除了面对也没有其他办法。



作者:街角小林
来源:juejin.cn/post/7239267216805838903
收起阅读 »

项目很大,得忍一下

web
背景 常和我们的客户端厮混,也经常陪他们发版,每次发版编译打包都可以在那边玩一局游戏了。一边幸灾乐祸,一边庆幸h5编译还好挺快的,直到我们的项目也发展成了*山,巨石项目。由于线上要给用户查看历史的推广活动,所以很多老的业务项目都还是留在项目中,导致我们的rou...
继续阅读 »

背景


常和我们的客户端厮混,也经常陪他们发版,每次发版编译打包都可以在那边玩一局游戏了。一边幸灾乐祸,一边庆幸h5编译还好挺快的,直到我们的项目也发展成了*山,巨石项目。由于线上要给用户查看历史的推广活动,所以很多老的业务项目都还是留在项目中,导致我们的router层爆炸,打包速度直线下降,开发过程中,开了hmr稍微有点改动也要等个几秒钟,恨不得立刻重启一个新项目。但是现实告诉你,忍住,别吐,后面还有更多的业务活动加进来。那么怎么解决这个问题呢,这个时候mp的思路是个不错的选择。


关键点


打包慢,本质原因是依赖庞大,组件过多。开发过程中,我们开新的业务组件时,往往和其他业务组件是隔离的,那么我们打包的时候是不是可以把那些不相干的业务组件隔离出去,当然可以。打包工具,从入口开始进行扫描,单页面的模块引入基本都是借助router,所以,关键的是如果我们能够控制router的数量,其实就能够控制编译和打包规模了。


问题


router在vue项目中我们常用的是全家桶的库vue-router,vue-router最多提供了懒加载,动态引入功能并不支持。有小伙伴说router的引入路径可不可以动态传入,我只能说小伙子你很机智,但是vue-router并不支持动态的引入路径。因此我们换个思路,就是在入口的位置控制router的规模,通过不同规模的router实例来实现router的动态引入。当然这需要我们对router库进行一定改造,使其变的灵活易用


一般的router


通常的router如下:



// router.js

/*global require*/

const Vue = require('vue')

const Router = require('vue-router')

Vue.use(Router)

const routes = [

{

path: '/routermap',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'routermap',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

const router = new Router({

mode: 'history',

routes

})

router.afterEach((to, from) => {

///

})

export default router

// 引入 entry.js

import router from './router.js'

router.beforeEach((to, from, next) => {

///

next()

})

router.afterEach(function(to, from) {

///

})

new Vue({

el: '#app',

template: '<App/>',

router,

})


我们可以不断的往routes数组中添加新的router item来添加新的业务组件,这也是我们的项目不断变大的根本,这样既不好维护,也会导致后面的编译效率


易于维护和管理的router


其实好的管理和维护本质就是分门别类,把类似功能的放在一起,而不是一锅粥都放在一起,这样基本就能解决追踪维护的功能,对应router管理其实也不是很复杂,多建几个文件夹就行如下:


router.png


对应routes/index.js代码如下:



import testRouter from './test.js'

const routes = [

{

path: '/map',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'map',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

...testRouter,

// 可以扩展其他router

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

// test.js

/**

* 测试相关页面路由映射

*/


/*global require*/

export default [

{

path: '/test/tools',

name: 'testTools',

component: resolve => require(['@test/tools/index.vue'], resolve),

desc: '测试工具'

}

]


我们通过把router分为几个类别的js,然后在通过router item的数组展开合并,就做到了分门别类,虽然看似简单,但是可以把管理和维护效果提升几个数量级。


支持mp的router


虽然上面支持了易于管理和维护,但是实际上我们如果只是单一入口的话,导出的还是一个巨大的router。那么如何支持多入口呢,其实也不用想的过于复杂,我们让类似test.js的router文件既支持router item的数组导出,也支持类似routes/index.js一样的router实例导出即可。所谓既能分也能合才是最灵活的,这里我们可以利用工厂模式做一个factory.js,如下:



/**

* app 内的页面路由映射

*/


/*global require*/

const Vue = require('vue')

const Router = require('vue-router')

Vue.use(Router)

const RouterFactory = (routes) => {

return new Router({

mode: 'history',

routes: [

{

path: '/map',

component: (resolve) => require(['../containers/map.vue'], resolve),

name: 'map',

desc: '路由列表'

},

{

path: '/',

component: (resolve) => require(['../containers/index.vue'], resolve)

},

...routes,

{

path: '*',

component: (resolve) => require(['../containers/nofound.vue'], resolve),

name: 'defaultPage',

desc: '默认页'

}

]

})

}

export default RouterFactory


这个factory.js产出的router实例和routes/index.js一模一样所以我们只需组装一下test.js即可,如下:



/*global require*/

import RouterFactory from './factory'

export const testRouter = [

{

path: '/test/tools',

name: 'testTools',

component: resolve => require(['@test/tools/index.vue'], resolve),

desc: '测试工具'

}

]

export default RouterFactory(developRouter)

// routes/index.js的引入变化一下即可

import testRouter from './test.js'

// 修改为=》

import { testRouter } from './test.js'


那么我们的入口该如何修改呢?也很简单:



// testEntry.js

import router from './routes/test.js'

router.beforeEach((to, from, next) => {

///

next()

})

router.afterEach(function(to, from) {

///

})

new Vue({

el: '#app',

template: '<App/>',

router,

})


我们建立了一个新的入口文件 testEntry.js 这个入口只引入了test相关的模块组件


如何灵活的和编译命令做配合呢


根据上面,我们进行mp改造的基础已经做好,关于如何多入口编译webpack或者其他打包里面都是基础知识,这里就不多赘述。这里主要聊一下如何灵活的配合命令做编译和部署。


既然router我们都可以分为不同的文件,编译文件我们同样可以拆分为不同的文件,这也使得我们的命令可以灵活多变,这里我们以webpack做为示例:


config.png


config1.png


config2.png


config3.png


根据上图示例 我们的webpack的配置文件仅仅改动了entry,我们稍微改造一下build.js,使其能够接受不同的编译命令:



// build.js

let page = 'all'

if (process.argv[2]) {

page = process.argv[2]

}

let configMap = {

'all': require('./webpack.prod.conf'),

'app': require('./webpack.app.conf')

}

let webpackConfig = configMap[page]

// dev-server.js

let page = 'all'

if (process.argv[2]) {

page = process.argv[2]

}

let configMap = {

'all': require('./webpack.dev.conf'),

'app': require('./webpack.app.dev.conf')

}

let webpackConfig = configMap[page]


对应的脚本配置:



// package.json

"scripts": {

"dev": "node build/dev-server.js",

"build": "node build/build.js",

"build:app": "node build/build.js app",

"dev:app": "node build/dev-server.js app"

},


以上app对应test。最后,我们只需要在命令行执行相应命令,即可实现我们可控的router规模的开发,基本随便来新的需求,咱都可以夜夜做新郎,怎么搞都是飞速。当然部署的话我们也可以单独执行一部分页面的部署命令到单独的域名,换个思路也可以作为一种预发测试的部署方法。



#
整体项目的开发编译

npm run dev

#
单独的app,也即test项目的开发编译

npm run dev:app

#
整体项目的部署

npm run build

#
单独的app,也即test项目的部署

npm run build:app


结语


以上,即如何利用mp思路,提高我们的编译开发效率。时常有人会在提高网页性能的时候说到mp,但mp本质上并不能提高页面的性能,比如白屏优化。而路由中使用懒加载其实才是提高部分网页性能的出力者,关于白屏优化,本篇文章不作展开讨论。


作者:CodePlayer
来源:juejin.cn/post/7218866717739696183
收起阅读 »

移植五周年

这几天在医院对身体各方面进行了一次比较全面的检查,结果比较令人满意。一转眼,接受肾移植已经 5 周年了,写一篇博文,对这些年的身体以及治疗情况进行了一个汇总。 发病 在 30 岁左右进行体检时,已经发现肾功能指标不太理想,因此进行了有针对性的调整。但随着时间的...
继续阅读 »

这几天在医院对身体各方面进行了一次比较全面的检查,结果比较令人满意。一转眼,接受肾移植已经 5 周年了,写一篇博文,对这些年的身体以及治疗情况进行了一个汇总。


发病


在 30 岁左右进行体检时,已经发现肾功能指标不太理想,因此进行了有针对性的调整。但随着时间的推移和工作的繁忙,逐渐也放松了对健康的重视。尽管发病前几个月进行体检时,各方面的指标也还说得过去。但病来如山倒,对于肾脏疾病来说,后期恶化的速度是十分迅猛的。


当时手里有几个项目正在进行,资金的压力也比较大。尽管身体已经发出了明显的信号,但总想着再坚持一下,即使家人一再催促,我也始终没有进行必要的检查。直到几乎完全无法进食(吃任何东西都马上会吐)才不得已去了医院,确诊十分迅速,因为已经没有了讨论的必要。此时,肌酐在 2100+,血红蛋白 46,按照医生的话来说,能活着走到医院已经属于大惊喜了。


由于贫血严重,输了几次血,但血红蛋白并没有得到太大的改善。为了尽快进行较彻底的透析,在与医生商量后,尽管血红蛋白不足 60,还是强行进行了植入透析管的手术,开启了我的透析生涯。


透析


透析有两种形式:一种是血液透析(大多数人知道的透析方式),另一种是腹膜透析。血液透析通常在指定的血液透析中心(或医院)进行,每周三次,每次四个小时。腹膜透析则可以在家中进行,每天透析次数根据患者的身体情况而有所变化,通常为 2 到 5 次,每次透析液置换时间约为 20 分钟。


我选择了腹膜透析。相较于血液透析,腹膜透析的场地和时间更自由。另外,腹膜透析通常对残肾功能保护比较好,有利于日后的移植。腹膜透析也有两种方式,一种是手动,就是每天手工进行几次透析液的置换操作。另一种则是自动透析(APD),通常是每天临睡前将腹部的透析管连接到设备上。在患者睡眠的过程中,设备会进行多次的液体交换。早上醒来的时候便将腹透管与设备断开,白天与正常人一样可以自由行动。有这么多的优势,我自然会选择自动透析的方式。


理想很丰满,现实很残酷。透析后发现我的腹膜滲透能力不是太好。仅依赖夜间的透析机进行透析完全无法满足排出毒素和水分的需要。因此,在此基础上逐渐增加了白天的手动透析操作。到移植前,我白天需要进行 5 次手工操作,夜间进行 12 个小时的机器自动透析。已然创下了我所在地区的透析记录。


经过几个月的透析治疗,随着身体状况的逐步改善,家人便联系了移植医院,催促我进行移植手术。但是,处于某种考虑,当时我并没有太基于进行移植。在外人看来,透析是一种无趣、繁琐、束缚人的治疗方式,但对我来说,它是一种身心调理。在这几年的透析治疗中,我的心态发生了巨大的转变,变得更加平和从容。此外,透析还让我的生活变得十分有规律,为我日后的健康生活打下了基础。


移植


最终,在进行了四年的透析后,我选择进行肾移植。为了能够更好地应对这次手术,我在准备移植前已经进行了一年有针对性的锻炼。再加上之前有意无意地“错过”了几次移植的机会,在接到医院的电话时,我十分平静。内心很笃定手术会成功。


不过事情似乎没有想的那么顺利。手术当天还是出现了不理想的状况,本来只需要 4 到 6 个小时的手术进行了接近 10 个小时。而且在手术后的第五天,从很多指标上看,手术似乎出现了明显失败的迹象。最重要的是,医生怀疑移植肾出现了破碎的可能,于是又进行了一次手术。在第二次手术前,尽管从各方面的指标上来看问题不小,但我个人感觉异常良好,因此我是抱着十分轻松的心态接受第二次手术。事后从妻子的口中得知,当时医生已经让家人做最坏打算了。第二次手术进行的时间不比第一次少,而且同第一次一样,出现了十分严重的术后反应。经过 ICU 的洗礼,人没有太大的事情。好消息是,第二次手术确认了第一次手术没有太大的问题,移植肾也无大碍。


在突破了该医院移植的最长住院记录后,经过 35 天,我终于回到了家,进入了术后康复阶段。


康复


回到家后马上遇到了几个十分棘手的问题。


第一个是体能太差。这是因为在进行第二次手术后,创口部分又出现了一次比较严重的出血情况。为了加快速度,两个医生在加护病房中对创口进行了紧急处置。经过这次治疗,我被要求只能用一个姿势躺着。经过了 20 多天的卧床,虽然伤口问题不大了,但下肺萎缩严重。刚回到家一个月里,即使在静息的状态下,心率也在 100 以上。为了改善体能,我从手术后 2 个月便开始恢复体能锻炼,并将这个习惯一直保持至今,现在每天也会进行一到两个小时的健身。


第二个症状是神经震颤,这是药物他克莫司所具备的副作用,强度因人而异,而我显然是反应比较大的那个。当时手已经很难拿住筷子了,情况与帕金森症很类似。为了改善这种情况,我尝试每天写毛笔字,不是为了练字,主要是提高自己的控制力。随着药量的减少、身体对药物的逐步适应,在手术一年后,这个症状得到了明显的改善。目前除了做一些很精细的操作外,基本上看不出有什么异常。


第三点是脑子出现了问题。大量的麻醉以及短期内高剂量的激素治疗让我的思维出现了明显的窒塞。在手术后的三到四个月中,我几乎无法通过短信发出一段没有错误的文字,基本上是脑子知道想要什么,但表述总是有问题。好在自己对这种情况有清晰的认识,努力尝试通过阅读、思考、交流、学习来改善这种状况。学习 Swift、SwiftUI、Core Data 也是在这个背景下自己主动采用的一种治疗措施,想了解这段时间的情况,可以阅读 老人新兵 —— 一款 iOS APP 的开发手记一文。


应该说,几年的透析生活给了我相当大的帮助。在移植手术后的这几年,我保持了相当健康的生活方式和乐观的心态。经过数年的调养,身体的指标也越来越好。


这是我每个月经常性检查的一些指标,通过这些指标可以看出,我的肾功指标在移植后逐年好转直至正常。大多数移植患者的指标在移植后很短的时间(几天到几周)都会恢复正常,然后随着时间的推移逐渐再出现问题。而我至少到目前来看,指标还处于上升通道中。希望能够继续保持下去。


image-20230530142243400



只有经过长时间的积累,才能看出数据的价值。使用“健康笔记”App,我不仅对自己的身体指标有了清晰的认识,而且这些数据给我和医生提供了重要的参考指标,让我能有针对性地调整身体。如果你或你的家人、朋友需要长期跟踪健康数据,可以尝试使用该应用程序。请注意,该应用程序最初是为我自己编写的,对新手不够友好,但功能非常实用。



未来


尽管这些年,身体出现了或多或少的问题,不过我还是非常幸运的。遇到了善良的器官捐赠者,有最爱我的家人,总能转危为安的运气、一直碰到不错的医护,以及获得了很多朋友的支持。


明年我就到了知天命的年龄,希望能以平和的心态继续积极、健康地生活下去,做一些自己想做并且对社会有意义的事情。


祝大家身体健康。


作者:东坡肘子
来源:juejin.cn/post/7238999195825881144
收起阅读 »

你还在凭感觉来优化性能?

web
前言 大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题。 众所周知,感觉是最不靠谱的东西,这完全取决于主观意识。我们做性能优化一定要取决于一些指标,而Performance API向我们提供了访问和测量浏览器性能相关信息的方式。通...
继续阅读 »

前言


大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题


众所周知,感觉是最不靠谱的东西,这完全取决于主观意识。我们做性能优化一定要取决于一些指标,而Performance API向我们提供了访问和测量浏览器性能相关信息的方式。通过它,我们可以获取有关页面加载时间、资源加载性能、用户交互延迟等详细信息,用于性能分析和优化,而不是完全靠自己的感官意识,感觉更快了或者更慢了。


指标收集


1. Navigation Timing API - 页面加载时间


// 获取页面加载时间相关的性能指标
const navigationTiming = performance.timing;
console.log('页面开始加载时间: ', navigationTiming.navigationStart);
console.log('DOMContentLoaded 事件发生时间: ', navigationTiming.domContentLoadedEventEnd);
console.log('页面加载完成时间: ', navigationTiming.loadEventEnd);
console.log('页面从加载到结束所需时间',navigationTiming.loadEventEnd - navigationTiming.navigationStart)

2. Resource Timing API - 资源加载性能


// 获取资源加载性能数据
const resources = performance.getEntriesByType('resource');
resources.forEach(resource => {
console.log('资源 URL: ', resource.name);
console.log('资源开始加载时间: ', resource.startTime);
console.log('资源加载结束时间: ', resource.responseEnd);
console.log('资源加载持续时间: ', resource.duration);
});

3. User Timing API - 自定义时间点


// 标记自定义时间点
performance.mark('startOperation');
// 执行需要测量的操作

for(let i = 0;i < 10000;i++) {}

performance.mark('endOperation');
// 测量时间差
performance.measure('operationDuration', 'startOperation', 'endOperation');
const measurement = performance.getEntriesByName('operationDuration')[0];
console.log('操作执行时间: ', measurement.duration);
// 和console.time,console.timeEnd比较相似

4. Long Tasks API - 长任务性能


// 获取长任务性能数据
const longTasks = performance.getEntriesByType('longtask');
longTasks.forEach(task => {
console.log('任务开始时间: ', task.startTime);
console.log('任务持续时间: ', task.duration);
});

5. Navigation Observer API - 导航事件监测


// 创建 PerformanceObserver 对象并监听导航事件
const observer = new PerformanceObserver(list => {
const entries = list.getEntries();
entries.forEach(entry => {
console.log('导航类型: ', entry.type);
// navigate 表示页面的初始导航,即浏览器打开新的网页或重新加载当前网页。
// reload 表示页面的重新加载,即浏览器刷新当前网页。
// back_forward 表示通过浏览器的前进或后退按钮导航到页面。
console.log('导航开始时间: ', entry.startTime);
console.log('导航持续时间: ', entry.duration);
});
});
// 监听 navigation 类型的事件
observer.observe({ type: 'navigation', buffered: true });

6. LCP的采集


LCP(Largest Contentful Paint)表示最大内容绘制,指的是页面上最大的可见内容元素(例如图片、视频等)绘制完成的时间点。LCP反映了用户感知到的页面加载速度,因为它代表了用户最关注的内容何时变得可见。LCP 应在页面首次开始加载后的2.5 秒内发生。


new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('Largest Contentful Paint:', entry.startTime);
  }
}).observe({type'largest-contentful-paint'bufferedtrue});


浏览器会多次报告 LCP ,而真正的 LCP 是用户交互前最近一次报告的 LCP。



7. FID的收集


FID(First Input Delay)表示首次输入延迟,衡量了用户首次与页面进行交互(例如点击按钮、链接等)时,响应所需的时间。较低的FID值表示页面对用户输入更敏感,用户可以更快地与页面进行交互,页面的 FID 应为100 毫秒或更短。


new PerformanceObserver(function(list, obs) {  
  const firstInput = list.getEntries()[0];
  const firstInputDelay = firstInput.processingStart - firstInput.startTime;
  const firstInputDuration = firstInput.duration;
  console.log('First Input Delay', firstInputDuration);
  obs.disconnect();
}).observe({type'first-input'bufferedtrue});

8. CLS的收集


CLS(Cumulative Layout Shift)表示累积布局偏移,衡量了页面在加载过程中发生的意外布局变化程度。当页面上的元素在加载过程中发生位置偏移,导致用户正在交互时意外点击链接或按钮,就会产生布局偏移。页面的 CLS 应保持在 0.1  或更少,这里的0.1表示10%。请注意,CLS 的计算可能涉及复杂的算法和权重计算,下列代码示例仅演示了基本的计算过程。


const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
let clsScore = 0;
entries.forEach(entry => {
// 计算每个布局变化的分数
clsScore += entry.value;
});
console.log('CLS 值: ', clsScore);
});

// 监听 Layout Shift 类型的条目
observer.observe({ type: 'layout-shift', buffered: true });

小结



  • 测量页面加载时间:性能 API 允许我们测量和分析网页加载所需的时间。通过使用性能计时指标,如 navigationStart、domContentLoadedEventEnd 和 loadEventEnd,我们可以准确测量页面加载过程中各个阶段的持续时间。

  • 分析资源加载性能:利用性能 API,我们可以检查网页上正在加载的各个资源(如图像、脚本、样式表)的性能。这包括跟踪资源加载时间、大小和状态码,有助于识别影响整体性能的瓶颈或问题。

  • 监测用户交互延迟:性能 API 使我们能够测量用户交互和浏览器响应之间的延迟。通过跟踪类似于 firstInputDelay(FID)和 firstInputTime 的指标,我们可以评估网页对用户操作(如点击或触摸)的响应速度,并确定改进的方向。

  • 基准测试和比较分析:性能 API 允许我们对网页的不同版本或不同网页的性能进行基准测试和比较分析。通过收集性能数据和指标,我们可以评估代码更改、优化或第三方资源对页面性能的影响,并做出明智的决策。

  • 性能优化和报告:利用性能 API 获得的洞察力,我们可以确定性能瓶颈和改进的方向。然后,可以使用这些信息来实施优化,例如减小文件大小、降低服务器响应时间、优化缓存策略和提高渲染
    作者:simple_lau
    来源:juejin.cn/post/7238779568478552122
    效率。

收起阅读 »

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

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

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


图片


图片


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


图片


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


以下是正文。


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




1. 直接操作DOM


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

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

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

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

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


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


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

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

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

2. useEffect不指定依赖


依赖参数缺失。


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

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


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

3. 硬编码


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


图片


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


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


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


图片


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


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


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


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


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


图片


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


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


图片


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


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

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


5.变量无初始值


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


const [variable, setVariable] = useState();

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

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

return variable.show ?  : null;

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


6. 三元选择符嵌套使用


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


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

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


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


替代方案: 



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

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


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

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


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

    ) : (
        
            {condition6 ? children1 : children2}
        

    )
)

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


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


7. 逻辑不拆分


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


图片


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


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

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


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

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


8. 随意读取window对象的值


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


window.tmeXXX.a.func();

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


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


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

关于“凌晨服务器告警!我被动把性能优化了2000%”这件事~

web
前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。 大早上被叫醒! 前几天周末,大早上的时候,太阳才刚出来,我突然被老大电话叫醒了,并通知我速速进入飞书会议,说是服务器发生了警报,出现了严重事故。 进到会议才...
继续阅读 »

前言


大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心。



大早上被叫醒!


前几天周末,大早上的时候,太阳才刚出来,我突然被老大电话叫醒了,并通知我速速进入飞书会议,说是服务器发生了警报,出现了严重事故。



进到会议才发现是我们的后端有一个接口,让监控直接红色了。由于这一块代码我比较熟,所以老大让我紧急定位并处理一下这个严重的问题~


定位问题


所以本全干工程师就开始了后端接口的问题定位~



初步定位


首先说说这个接口的背景,这个接口是提供给用户用作导入用的,用户在做导入的时候,有可能导入的数据超级大,所以会不会是因为导入的数据量太大了,所以导致此接口直接崩掉呢?


但是老大查了日志后跟我说,这个用户在以前也导入过这么大的数据量,但是那时候都没啥问题的啊~


所以我觉得这应该不是用户行为造成的,而是什么新功能造成的这个BUG~于是我看了涉及到此接口的最近的提交,发现确实是有一个新功能在最近上线了,是涉及到 json-schema 的


简单认识 json-schema


什么是 json-schema 呢?给大家举个小例子,让大家简单认识一下吧,比如下方的三个属性



  • name

  • age

  • cars


他们都有对应的数据类型,他们需要通过 json-schema 来寻找到自己对应的类型


// jsonschema
const schema = {
name: {
$ref: 1
},
age: {
$ref: 2
},
cars: {
$ref: 3
}
}

// jsonschema合集
const schemaDefination = {
1: {
type: 'string'
},
2: {
type: 'number'
},
3: {
$ref: 4
},
4: {
type: 'array'
}
}

// 得出结果
const result = build(schema, schemaDefination)
console.log(result)
// {
// name: 'string',
// age: 'number',
// cars: 'array'
// }

继续定位问题


回到这个 BUG 上,我继续定位。其实刚刚上面的例子是很简单的例子,但是其实在这个功能里,数据量和负责度远远没这么简单,我们刚刚看到的 schemaDefination 其实就是所有 jsonschema 的引用的结合


// 实际上可能有几百个
const schema1 = {
$ref: 1
}
const schema2 = {
$ref: 2
}
const schema3 = {
$ref: 3
}

const schemaDefination = gen(schema1, schema2, schema3)
console.log(schemaDefination)
// 实际上可能有几百个
// {
// 1: {
// type: 'string'
// },
// 2: {
// type: 'number'
// },
// 3: {
// type: 'array'
// }
// }

也就是一开始会先根据所有 schema 去生成一个引用的集合 schemaDefination,而这个集合可能有几百个,数据量挺大


最终定位


然后到最终的时候 schema 结合 schemaDefination 去生成结果,我感觉就是在这一步导致了 BUG


// 得出结果
// 可能要 build 几百次
const result1 = build(schema1, schemaDefination)
const result2 = build(schema2, schemaDefination)
const result3 = build(schema3, schemaDefination)

为什么我觉得是这一步出问题呢?我们刚刚说了 schemaDefination 是所有 schema 的引用集合,数据量很大,你每次 build 的时候 schema 传的是一个 schema,但是你 schemaDefination 传的是集合!!!


正常来说应该是传 schema 时只需要传对应的 schemaDefination 即可,比如


// 合理的
const result1 = build({
$ref: 1
}, {
1: {
type: 'string'
}
})

// 不合理的
const result1 = build({
$ref: 1
}, {
1: {
type: 'string'
},
2: {
type: 'number'
},
3: {
type: 'array'
}
})

而我们现在就是处于不合理的情况,于是我特地看了 build 这个函数的内部实现,发现有 对象序列化处理 的代码,想一下下面的模拟代码


const obj = { 几百个数据 }
while(i < 300) {
JSON.stringfy(obj)
i++
}

这样的代码会给服务器造成非常大的压力,甚至把接口给搞崩!!!


解决问题,性能提升几百倍!


上面其实我已经分析出问题所在了:传 schema 的时候不要传整个 Defination集合!,所以我们只需要传入所需的 defination, 那么性能是不是可以优化几百倍!!!


解决手段


所以我们只需要写一个函数,过滤出所需要的 defination 即可,例如


// 找出所有被 ref 的数据模型
const filter = (
schema,
schemaDefinitions,
) => {
// 进行过滤操作
}

// jsonschema
const schema = {
name: {
$ref: 1
}
}

// jsonschema合集
const schemaDefination = {
1: {
type: 'string'
},
2: {
type: 'number'
},
3: {
$ref: 4
},
4: {
type: 'array'
}
}

// 过滤
const defination = filter(schema, schemaDefination)
console.log(defination)
//{
// 1: {
// type: 'string'
// },
//}

所以只需要在 build 的时候传入过滤后的 defination 即可!


const result1 = build(schema1, filter(schema1, schemaDefination))
const result2 = build(schema2, filter(schema2, schemaDefination))
const result3 = build(schema3, filter(schema3, schemaDefination))

测试无误,继续睡觉!


然后拿到一份用户的数据,在测试环境测了一下,没有发生之前那个 BUG 了!合并代码!打包上线!继续睡觉!



结语 & 加学习群 & 摸鱼群


我是林三心



  • 一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手;

  • 一个偏前端的全干工程师;

  • 一个不正经的掘金作者;

  • 一个逗比的B站up主;

  • 一个不帅的小红书博主;

  • 一个喜欢打铁的篮球菜鸟;

  • 一个喜欢历史的乏味少年;

  • 一个喜欢rap的五音不全弱鸡


如果你想一起学习前端,一起摸鱼,一起研究简历优化,一起研究面试进步,一起交流历史音乐篮球rap,可以来俺的摸鱼学习群哈哈,点这个,有7000多名前端小伙伴在等着一起学习哦 --> 摸鱼沸点


image.png


作者:Sunshine_Lin
来源:juejin.cn/post/7238973821801594935
收起阅读 »

对接了个三方支付,给俺气的呀

故事是这样的: 我们的商城需要在日本上线去赚小日子过得不错的日本人的钱,所以支付是首要的。就找了一家做日本本地支付的公司做对接,公司名字当然不能说,打我也不说。 第一天,很愉快,签了协议,给了开发文档。俺就准备开始撸代码了。 API文档 这开发文档,打开两秒钟...
继续阅读 »

故事是这样的:


我们的商城需要在日本上线去赚小日子过得不错的日本人的钱,所以支付是首要的。就找了一家做日本本地支付的公司做对接,公司名字当然不能说,打我也不说。


第一天,很愉快,签了协议,给了开发文档。俺就准备开始撸代码了。


API文档


这开发文档,打开两秒钟就自动挂掉了,我只能一次又一次的点击Reload


image.png


后来实在受不了了,我趁着那两三秒钟显示的时间,截图对照着看。结果就是所有的字段不能复制粘贴了,只能一个一个敲。


还是这个API文档,必输字段非必输字段乱的一塌糊涂,哪些是必输纯靠试,有的必输字段调试的时候有问题,对面直接甩过来一句,要么不传了吧。听得我懵懵的。


加密,验签


然后到了加密,验签。竟然能只给了node的demo,咱对接的大部分是后端程序员吧,没事,咱自己写。比较坑的是MD5加密完是大写的字母,这三方公司转小写了,也没有提示,折腾了一会,测了一会也就通了,还好还好。


场景支持


在之后就是支付的一个场景了,日本是没有微信支付宝这样的便捷支付的,要么信用卡,要么便利店支付



稍微说下便利店支付,就是说,客户下完单之后,会给到一个回执,是一串数字,我们且称之为支付码,他们便利店有一个类似于ATM机的柜员机,去这个机子上凭借这串支付码打印出来一个凭条,然后拿着这个凭条去找便利店的店员,现金支付



就是这个场景,就是这个数字,三方它就给客户显示一次,就一次,点击支付的时候显示一次。要是客户不截图,那么不好意思,您就重新下单吧。而且这个支付码我们拿不到,这个跳转了他们的页面,也不发个邮件告知。这明显没法用啊,我们的订单过期时间三天,客户这三天啥时候想去便利店支付都行,可是这只显示一次太扯了。

同样的请求再发一次显示的是支付进行中。这怎么玩,好说歹说他们排期开发了两周,把这个订单号重入解决了,就是说同一笔订单再次进入是可以看到那串支付码的。


测试环境不能测


最后,写完了,调通了,测一下支付,结果他们测试环境不支持日本的这个便利店支付测试,what? 测试环境不能测试?那我这么久在干什么,让我们上线上测,我的代码在测试环境还没搞完,让我上生产,最后上了,没测通,对方的问题,当天下午就把代码给回滚了。等着对方调试完继续测。


业务不完整


还有,不支持退款,作为一个支付公司,不支持退款,我们客户退款只能线下转账,闹呢。


以前对接三方的时候,遇到问题地想到的是我们是不是哪里做错了,再看看文档。对接这个公司,就跟他们公司的测试一样,找bug呢。


建议


这里是本文的重点,咱就是给点建议,作为一家提供服务的公司,不管是啥服务。



  1. 对外API文档应当可以正常显示,必输非必输定义正确,字段类型标注准确。

  2. 若是有验签的步骤,介绍步骤详细并配上各个语言的demo,并强调格式以及大小写。

  3. 牵扯到业务的,需要站在客户的角度思考,这个是否合情合理。

  4. 业务的完整性,有可能是尚未开发完全,但是得有备选的方案。


作者:奔跑的毛球
来源:juejin.cn/post/7127691522010529799
收起阅读 »

让整个网站界面无滚动条

web
界面无滚动条 滚动条的优化也有很多种,比如随便再网上搜索美化浏览器滚动条样式,就会出现些用css去美化滚动条的方案。 那种更好呢? 没有更好只有更合适 像默认的滚动条的话,他能让你方便摁着往下滑动(他比较宽)特别省劲,不用担心美化过后变细摁不到问题。 美化...
继续阅读 »

界面无滚动条


滚动条的优化也有很多种,比如随便再网上搜索美化浏览器滚动条样式,就会出现些用css去美化滚动条的方案。


那种更好呢?


没有更好只有更合适



  • 像默认的滚动条的话,他能让你方便摁着往下滑动(他比较宽)特别省劲,不用担心美化过后变细摁不到问题。

  • 美化后的滚动条样式啊更贴合网站主题,让用户体验更好。

  • 无滚动条(鼠标放上去后出现)这种更适合像一个页面好多个块,每个块的话还很多内容(都有滚动条)。如果像这种都默认都出现滚动条的话,也太不美观了。



那咱们就从无滚动条展开说说!!!



无滚动条设计



比如像element ui组件内像input的自定义模块数据过多的时候出现的下拉框内的滚动条,如下图:



element-ui里面它其实是有内部组件el-scrollbar在的。那么它是怎么实现无滚动条呢?


如下图咱们先把:hover勾选上让滚动条一直处于显示得状态。然后咱们再分析他的实现。


当我把样式稍微修改下,咱们再观察下图:


01.jpg


这么看是不是就很明白了 他其实用margin值把整个容器扩大了点然后溢出隐藏,其实滚动条还在就是给界面上看不到了而已。


然后它自己用dom画了个滚动条,如下图:


02.jpg



经过上面分析,咱们已经很清楚得了解到一个无滚动条是从那个方面实现得了。



  1. 使用margin值把滚动条给溢出隐藏掉。

  2. 使用div自己画了一个滚动条。方便咱们隐藏、显示、更改样式等。



无滚动条实现


那咱们再从细节上拆分下具体实现要考虑那些点:



  1. 需要计算滚动条得宽度用来margin扩大得距离(每个界面上得滚动条得宽度是不一样得)。

  2. 需要计算画的div滚动条得高度(这个内容多少会影响滚动条的高度)。

  3. 需要根据滚动条去transform: translateY(19.3916%);移动咱们自己画的div滚动条的。

  4. 需要根据摁着画的div滚动条,去实际更改需要滚动的高度。

  5. 需要点击滚动轴的柱子支持跳到目标的滚动位置;


一 计算界面原本滚动条的宽度



计算下界面上原本滚动条的宽度如下:



let scrollBarWidth;

export default function() {
if (scrollBarWidth !== undefined) return scrollBarWidth;

const outer = document.createElement('div');
outer.className = 'el-scrollbar__wrap';
outer.style.visibility = 'hidden';
outer.style.width = '100px';
outer.style.position = 'absolute';
outer.style.top = '-9999px';
document.body.appendChild(outer);

const widthNoScroll = outer.offsetWidth;
outer.style.overflow = 'scroll';

const inner = document.createElement('div');
inner.style.width = '100%';
outer.appendChild(inner);

const widthWithScroll = inner.offsetWidth;
outer.parentNode.removeChild(outer);
scrollBarWidth = widthNoScroll - widthWithScroll;

return scrollBarWidth;
};


先创建了一个div, 设置成scroll, 然后再在里面嵌套一个没有滚动条的div设置宽度100%, 获取到两者的offsetWidth, 相减获取到scrollBarWidth赋值给scrollBarWidth 是惰性加载的优化,只需要计算一次就可以了。 具体展现如下图:



03.jpg


二 计算画的滚动条的高度height



计算下画的div滚动条的高度height。是用当前容器的内部高度 / 容器整个滚动条的高度 * 100计算出百分比;



比如:


const warp = this.$refs.wrap; // 或者使用documnet获取容器
const heightPercentage = (wrap.clientHeight * 100 / wrap.scrollHeight); // height
const widthPercentage = (wrap.clientWidth * 100 / wrap.scrollWidth); // width


解析: 如当前容器高30px,内容撑起来总共高100px,那么滚动条的高度就是当前容器的30%;



三 计算滚动条需要移动的值



计算画的div需要滚动条的高度moveY是, 获取当前容器滚动的scrollTop / 当前容器内部高度 * 100



算法一:



解析 使用transform: translateY(0%);是移动的是自己本身的百分比那么(容器滚动的scrollTop / 当前容器内部高度 * 100)算法如下:



const warp = this.$refs.wrap; // 或者使用documnet获取容器
this.moveY = ((wrap.scrollTop * 100) / wrap.clientHeight);
this.moveX = ((wrap.scrollLeft * 100) / wrap.clientWidth);

算法二:



解析:使用定位top值,这个比较好理解滚动条的滚动 / 容器的滚动总高度 * 100得到百分比,如下:



const warp = this.$refs.wrap; // 或者使用documnet获取容器
this.moveY = ((wrap.scrollTop * 100) / wrap.scrollHeight);
this.moveX = ((wrap.scrollLeft * 100) / wrap.scrollWidth);


把计算出来的moveYmoveX的方法 绑定给scroll 滚动事件就可以了。



四 摁着画的div滚动条 经行拖动



滚动条都是支持拖着上下左右移动的,那咱们也要支持下:




  • 获取当前滚动条的高度或者宽度可以使用getBoundingClientRect()如下图:

  • 获取拖着移动的距离 就是再鼠标摁下先计一个当前的x1、y1监听movex2、y2相减就是拖动的距离了。

  • 获取到拖动的距离后转成transform || top值。


一个简单的拖动组件如下:


<template>
<div
ref="draggableRef"
class="draggable"
:style="style"
>
<slot />
</div>
</template>

<script>
export default {
name: 'DraggableComponent',

props: {
initialValue: {
type: Object,
required: false,
default: () => ({ x: 0, y: 0 }),
},
},

data() {
return {
currentValue: { x: 0, y: 0 },
isDragging: false,
startX: 0,
startY: 0,
diffX: 0,
diffY: 0,
};
},

computed: {
style() {
return `left: ${this.currentValue.x + this.diffX}px; top: ${this.currentValue.y + this.diffY}px`;
},
},

watch: {
initialValue: {
handler(val) {
this.currentValue = val;
},
immediate: true,
},
},

mounted() {
this.$nextTick(() => {
const { draggableRef } = this.$refs;
if (draggableRef) {
draggableRef.addEventListener('mousedown', this.startDrag);
document.addEventListener('mousemove', this.moveDrag);
document.addEventListener('mouseup', this.endDrag);
}
});
},

beforeDestroy() {
const { draggableRef } = this.$refs;
draggableRef.removeEventListener('mousedown', this.startDrag);
document.removeEventListener('mousemove', this.moveDrag);
document.removeEventListener('mouseup', this.endDrag);
},

methods: {
startDrag({ clientX: x1, clientY: y1 }) {
this.isDragging = true;
document.onselectstart = () => false;
this.startX = x1;
this.startY = y1;
},

moveDrag({ clientX: x2, clientY: y2 }) {
if (this.isDragging) {
this.diffX = x2 - this.startX;
this.diffY = y2 - this.startY;
}
},

endDrag() {
this.isDragging = false;
document.onselectstart = () => true;
this.currentValue.x += this.diffX;
this.currentValue.y += this.diffY;
this.diffX = 0;
this.diffY = 0;
},
},
};
</script>

<style>
.draggable {
position: fixed;
z-index: 9;
}
</style>


咱们需要获取到画的滚动条的高度,然后根据拖动的距离算出来transform: translateY(0%);或者top值;

如上面拖动组件 拖动部分代码就不在重复了 咱们直接用diffX、diffY、lastX、lastY来用了。



  • diffX、diffY 是拖动差的值

  • lastX、lastY 是上一次也就是未拖动前的值translateY || top



算法一(transform)


const thumb = document.querySelector('el-scrollbar__thumb'); // element ui  el-scrollbar 的滚动条
const { height: thumbHeight } = thumb?.getBoundingClientRect() || {};


const diffY = 10;
const lastY = '300'; // transform: translateY(300%);`
const moveY = (diffY / thumbHeight) + lastY;

算法二(top)


const thumb = document.querySelector('el-scrollbar__thumb'); // element ui  el-scrollbar 的滚动条
const { height: thumbHeight } = thumb?.getBoundingClientRect() || {};


const diffY = 10;
const lastY = 30; // top: 30%`
const moveY = (diffY / wrap.scrollWidth * 100) + lastY;

五 点击滚动轴使滚动条跳转到该位置



  • getBoundingClientRect 的 top 是获取到距离浏览器顶部的距离。
    写一个点击事件如下


function clickTrackHandler(event) {
const wrap = this.$refs.wrap;
// 1. 减去clientX 正好能获取到需要滚动到的位置
const offset = Math.abs(e.target.getBoundingClientRect().top - e.clientX);

// 2. 利用offset 的到画的滚动条的实际位置 两种算法transform || top
const thumb = document.querySelector('el-scrollbar__thumb'); // element ui el-scrollbar 的滚动条
const { height: thumbHeight } = thumb?.getBoundingClientRect() || {};

const translateY = offset / height * 100; // transform
const top = offset / wrap.scrollHeight * 100; // top

// 3、计算实际需要滚动的高度 使界面滚动到该位置。两种算法transform(scrollTop2) || top(scrollTop1)
const scrollTop1 = top * wrap.scrollHeight; // top
const scrollTop2 = translateY * wrap.clientHeight; // transform
}

总结



针对无滚动条如果是vue使用的话,再了解具体实现后可以直接用elementel-scrollbar组件就好,如果在其他框架中, 结合上面的逻辑也会很快封装一个组件。



作者:三原
来源:juejin.cn/post/7227033124856135738
收起阅读 »

不想写代码的程序员可以尝试的几种职位

标题不够严谨,应该是不想写业务代码的程序员可以做什么。 这里主要覆盖大家可能平时没关注,或者是国内少见的工作;所以像 technical product manager, project manager 这种就不再赘述了。 这里也主要分享 IT 行业内的岗位,...
继续阅读 »

标题不够严谨,应该是不想写业务代码的程序员可以做什么。


这里主要覆盖大家可能平时没关注,或者是国内少见的工作;所以像 technical product manager, project manager 这种就不再赘述了。


这里也主要分享 IT 行业内的岗位,要是除开行业限制,范围就太大了。


Developer Relation/Advocate


国外有很多面向开发者的技术创新公司,比如 Vercel ,PlanetScale ,Prisma ,Algolia 等。


这类公司的用户就是开发者,所以他们的市场活动也都是围绕着开发者;他们需要让更多的开发者可以更容易地把他们的技术用到他们的技术栈里,所以就有了这种岗位。用中文来表达,可能有点类似是布道师的意思?


国内更多是将技术应用起来,而不是创造一些新的技术,所以这种岗位在国内就非常少见了。当然近几年也还是有一些技术驱动型公司的,像 PingCAP ,Agora 等。


希望国内有更多像这样的公司出来。


Technical Recruiter


这个工作从 title 上就大概知道是做什么的了。


这个岗位有深有浅,深的可能是比较完整的招聘职能,浅的可能就是 HR 部门里面试和筛选技术候选人的。


Technical Writer


这个听着像是产品经理的工作,确实会和产品的职责有小部分重叠。


这是个面向内部的岗位,不喜欢对外对用户 /客户的朋友会非常喜欢。通常是一些比较大型的企业要做软件迁移或者什么系统、流程升级之类的时候,因为会牵扯到非常多的 moving parts ,所以通常都需要一个独立岗位来负责 documentation 的工作。


工作内容包括采访以及记录各部门的现有流程和业务需求,然后是新流程 /系统 /软件的手册、图表等等。


这里的“technical”不是我们研发中的技术,更多是“业务”层面的意思。同样这个岗位对技术要求不高,但是有研发背景是非常加分的。


Technical Support


通常这个岗位归属客服部门,高于普通 customer service rep 。普通的 customer support 是客户遇到问题时的第一层支持 - 基本会讲话、了解产品就能干的工作;如果第一层解决不了客户的问题,就会升级到后面 technical support 了。


这个岗位范围会更广一点,几乎任何 IT 公司都会有这种支持岗;对技术要求根据不同公司而不同,比如 Vercel 对这个岗位的技术要求肯定比 HelpScout (一个客服软件)要高。


但整体来说都不如研发要求高,但对应的薪酬待遇也没有研发那么好。


结语


其实说了这么多总结下来就是国外技术生态、开源氛围好很多,并且对技术足够的重视,促使很多技术公司的出现,然后催生了这些工作。


如果觉得本帖有启发,欢迎留言支持鼓励后续的创作。


其他相关文章


《找海外工作时可以做的一

些提前准备》

收起阅读 »

代码优雅之道——如何干掉过多的if else

1、前言 注意标题是过多的,所以三四个就没必要干掉了。实际开发中我们经常遇到判断条件很多的情况,比如下图有20多种情况,不用想肯定是要优化代码的,需要思考的是如何去优化? 网上很多说用switch case啊,首先不比较if else与switch case...
继续阅读 »

1、前言


注意标题是过多的,所以三四个就没必要干掉了。实际开发中我们经常遇到判断条件很多的情况,比如下图有20多种情况,不用想肯定是要优化代码的,需要思考的是如何去优化?



网上很多说用switch case啊,首先不比较if else与switch case效率问题的,只从代码整洁度来看二者没啥区别啊!我们这里更重要的是代码整洁度问题,为什么呢?来看下文的比较。


2、If else与switch case效率真的差距很大么?


网上有两种见解:


第一种是说switch…case会生成一个跳转表来指示实际的case分支的地址,而这个跳转表的索引号与switch变量的值是相等的。从而,switch…case不用像if…else那样遍历条件分支直到命中条件,而只需访问对应索引号的表项从而到达定位分支的目的。简单来说就是以空间换时间


第二种是说二者效率上差距并不大



于是我们自己去体验一下,不存在复杂业务逻辑,仅仅比较两种方式的效率:

    @Test
void contextLoads() {
testIf(100000);
System.gc();
testSwitch(100000);
}

private void testIf(Integer param) {
long start = System.currentTimeMillis();
for (int i = 0; i < param; i++) {
if (i == param-1){
System.out.println("if判断100000次");
}
}
long end = System.currentTimeMillis();
long total = end - start;
System.out.println("Test消耗时间:" + total);
}

private void testSwitch(Integer param){
long start = System.currentTimeMillis();
for (int i = 0; i < param; i++) {
switch (i){
case 99999:
System.out.println("switch判断100000次");
break;
}
}
long end = System.currentTimeMillis();
long total = end - start;
System.out.println("Test消耗时间:" + total);
}


可见差距并不大。而情况太多的时候谁还会去用if else和switch case呢?下面还是对两种方式的使用场景做简单的分析:


if else能够把复杂的逻辑关系表达得清晰、易懂,包容了程序执行的各种情况。


switch不适合业务系统的实际复杂需求,业务不断的变更迭代,一更改需求,条件的复杂度高了,switch无力处理。switch经常忘记写break,估计很多人一不小心就忘记写了。switch…case只能处理case为常量的情况。当情况不大于5种并且单一变量的值(如枚举),此时我们就可以使用switch,它的可读性比if条件更清晰。


除了上述说到枚举的这种场景,建议使用switch,其他个人愚见:只要情况不大于5种就直接使用if else


3、策略+工厂模式


上述说到情况较少时并且业务逻辑不复杂的使用if else可以让代码清晰明了。当每种情况对应的业务逻辑复杂时,建议使用策略+工厂模式。这里我们举个栗子:厂家每个季度要举行不同的活动,我们使用策略工厂模式来实现


策略接口

public interface Strategy {

/**
* 处理各种活动
* @return
*/
String dealActivity();
}

然后春夏秋冬四季活动类实现该接口


@Service
public class SpringActivity implements Strategy{
@Override
public String dealActivity() {
return "春季活动逻辑";
}
}

策略类工厂

public class StrategyFactory {
public static Strategy execute(Integer levelCode){
Strategy strategy = null;
switch (levelCode){
case 1:
strategy = new SpringActivity();
break;
case 2:
strategy = new SummerActivity();
break;
case 3:
strategy = new AutumnActivity();
break;
case 4:
strategy = new WinterActivity();
break;
default:
throw new IllegalArgumentException("活动编号错误");
}
return strategy;
}
}

然后在service层中传入对应的编码即可 ,我这里省略了service

@RestController
public class TestController {

@PostMapping("/dealActivity")
public String dealActivity(Integer code){
Strategy strategy = StrategyFactory.execute(1);
return strategy.dealActivity();
}
}


上述已经干掉了if else ,后续季度活动调整去修改对应活动策略类中逻辑即可。缺点:如果情况比这多,那么策略类会越来越多,也就是所谓的策略类膨胀,并且没有****没有一个地方可以俯视整个业务逻辑。


4、Map+函数式接口


将上述策略类全部作为方法

@Service
public class ActivityStrategyService {

public String dealSpringActivity(){
return "春季活动逻辑";
}

public String dealSummerActivity() {
return "夏季活动逻辑";
}

public String dealAutumnActivity() {
return "秋季活动逻辑";
}

public String dealWinterActivity() {
return "冬季活动逻辑";
}
}

再写个活动Service

@Service
public class ActivityService {

@Autowired
private ActivityStrategyService activityStrategyService;

@FunctionalInterface
interface ActivityFunction<A>{
//这里可以传参啊,我这里举例用不上参数
//String dealActivity(A a);
String dealActivity();
}

private final Map<Integer, ActivityFunction> strategyMap = new HashMap<>();

/**
* 初始化策略
*/
@PostConstruct
public void initDispatcher(){
strategyMap.put(1,()->activityStrategyService.dealSpringActivity());
strategyMap.put(2, ()-> activityStrategyService.dealSummerActivity());
strategyMap.put(3, ()-> activityStrategyService.dealAutumnActivity());
strategyMap.put(4, ()-> activityStrategyService.dealWinterActivity());
}

public String dealActivity(Integer code){
ActivityFunction<Integer> function = strategyMap.get(code);
//这里防止活动编号没匹配上,可以使用断言来判断从而抛出统一异常
return function.dealActivity();
}

}

改变Controller

@RestController
public class TestController {

@Autowired
private ActivityService activityService;

@PostMapping("/dealActivity")
public String dealActivity(Integer code){
// Strategy strategy = StrategyFactory.execute(1);
// return strategy.dealActivity();
return activityService.dealActivity(code);
}
}

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

Android渠道包自动更新

一、背景 转转集团旗下有多款APP产品,随着业务发展,各APP发版频率变高。在持续交付的背景下,渠道包更新存在以下几个效率问题: (1)Android渠道包提交应用市场审核,工作重复&人工成本高   (2)公司目前存在多个APP、需...
继续阅读 »

一、背景


转转集团旗下有多款APP产品,随着业务发展,各APP发版频率变高。在持续交付的背景下,渠道包更新存在以下几个效率问题:


(1)Android渠道包提交应用市场审核,工作重复&人工成本高  


(2)公司目前存在多个APP、需更多人支持,有培训成本


(3)每次发版需要人工通知项目成员渠道包审核进度 


  针对以上问题,我们设计开发了渠道包自动更新后台,用来解决渠道更新的效率问题。


二、方案调研


1、基于业务现状,做了技术调研和逻辑抽象


  不同APP支持的渠道不同,不同渠道更包api不同,如下图:


图片


针对以上调研结果,我们将通用的逻辑统一封装开发,将差异点进行配置,做到灵活配置可扩展。


2、整体的实现方案演变


初期方案,每个应用市场单独提审(需要先选择物料,选好物料后上传包文件,文件上传成功后再点击提交审核),多个应用市场需要重复该操作。


图片


上线运行了一段时间后,发现存在一些问题:单个市场提交步骤繁琐、多个应用市场需要分开多次提交。这些步骤是重复且可简化的,因此我们又对提审的过程做了封装,提供批量上传的入口,简化交互过程,做到一键提审。以下是当前运行的第二版方案:


图片


第二版方案上线后,提审同学只需要在入口处选择要更新的应用市场,然后一键上传全部物料,再点击提审按钮即可提审成功。代码内部会处理具体的逻辑,比如:根据配置规则将物料匹配到对应市场、自动匹配包文件进行提审。


三、方案设计


自动上传包含以下核心模块:



  • APP管理:支持配置多个APP信息,包括转转、找靓机、采货侠等

  • 包管理:支持下载不同渠道,不同版本的包

  • 物料管理:包括历史物料的选择,和新增物料的存储(icon、市场截图)

  • 提交审核:包括包下载、物料下载,支持按照APP配置账号密码提交审核

  • 消息提醒:对提交的结果和审核的结果进行消息通知


图片


实现效果:


提审前信息确认,选择APP,可选择单个或者多个渠道,系统自动选择包地址,用户选择物料后可一键提审多应用市场。操作简单便捷,使用成本低


图片


提审后发送消息通知,便于各方了解渠道的审核结果,对审核异常信息进行及时干预。同时自动存储不同版本的审核记录,方便后续分析。


图片


四、总结


渠道包自动更新功能,节省了大量的提交审核人力成本,打通了Android整体的持续交付过程,降低了人工学习成本。之后我们也会针对各种体验问题进行不断的改进和更新~


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

理解Kotlin中的reified关键字

标题:理解Kotlin中的reified关键字 摘要:本文介绍了Kotlin中的reified关键字的用途,特点以及如何在实际项目中应用。我们将通过实例详细了解reified的功能以及如何在内联函数中使用它。 正文: 什么是reified关键字? 在Kotli...
继续阅读 »

标题:理解Kotlin中的reified关键字


摘要:本文介绍了Kotlin中的reified关键字的用途,特点以及如何在实际项目中应用。我们将通过实例详细了解reified的功能以及如何在内联函数中使用它。


正文:


什么是reified关键字?


在Kotlin中,reified是一个特殊的关键字,用于修饰内联函数中的类型参数。这使得在函数内部可以访问类型参数的具体类型。通常情况下,由于类型擦除(type erasure),在运行时是无法直接获取泛型类型参数的具体类型的。reified关键字解决了这个问题。


使用reified关键字的条件


要使用reified关键字,需要遵循以下几点:



  1. 函数必须是内联的(使用inline关键字修饰)。

  2. 类型参数前需要加上reified关键字。


示例:reified关键字的用法


下面是一个使用reified关键字的简单示例:

inline fun <reified T> checkType(value: Any) {
if (value is T) {
println("Value is of type T.")
} else {
println("Value is NOT of type T.")
}
}

fun main() {
val stringValue = "Hello, Kotlin!"
val intValue = 42

checkType<String>(stringValue) // 输出 "Value is of type T."
checkType<String>(intValue) // 输出 "Value is NOT of type T."
}

在这个示例中,我们定义了一个内联函数checkType,它接受一个reified类型参数T。然后,我们使用is关键字检查传入的value变量是否为类型T。在main函数中,我们用不同的类型参数调用checkType函数来验证它的功能。


获取类型参数的Java类


当你使用reified关键字修饰一个内联函数的类型参数时,你可以通过T::class.java获取类型参数对应的Java类。这在需要访问泛型类型参数的具体类型时非常有用,比如在反射操作中。


下面是一个简单的例子:

import kotlin.reflect.KClass

inline fun <reified T : Any> getClass(): KClass<T> {
return T::class
}

inline fun <reified T : Any> getJavaClass(): Class<T> {
return T::class.java
}

fun main() {
val stringKClass = getClass<String>()
println("KClass for String: $stringKClass") // 输出 "KClass for String: class kotlin.String"

val stringJavaClass = getJavaClass<String>()
println("Java class for String: $stringJavaClass") // 输出 "Java class for String: class java.lang.String"
}

在这个示例中,我们定义了两个内联函数,getClassgetJavaClass,它们都接受一个reified类型参数TgetClass函数返回类型参数对应的KClass对象,而getJavaClass函数返回类型参数对应的Java类。在main函数中,我们用String类型参数调用这两个函数,并输出结果。


注意事项


需要注意的是,reified关键字不能用于非内联函数,因为它们的类型参数在运行时会被擦除。此外,reified类型参数不能用于普通类和接口,只能用于内联函数。


总结


Kotlin中的reified关键字允许我们在内联函数中访问类型参数的具体类型。它在需要访问泛型类型参数的场景中非常有用,例如在反射操作中。本文通过实例介绍了如何使用reified关键字,并讨论了相关注意事项。希望这些示例能够帮助您更好地理解和应用reified关键字。


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

既然有Map了,为什么还要有Redis?

一、同样是缓存,用map不行吗? Redis可以存储几十个G的数据,Map行吗? Redis的缓存可以进行本地持久化,Map行吗? Redis可以作为分布式缓存,Map只能在同一个JVM中进行缓存; Redis支持每秒百万级的并发,Map行吗? Redis有...
继续阅读 »

一、同样是缓存,用map不行吗?



  1. Redis可以存储几十个G的数据,Map行吗?

  2. Redis的缓存可以进行本地持久化,Map行吗?

  3. Redis可以作为分布式缓存,Map只能在同一个JVM中进行缓存;

  4. Redis支持每秒百万级的并发,Map行吗?

  5. Redis有过期机制,Map有吗?

  6. Redis有丰富的API,支持非常多的应用场景,Map行吗?



二、Redis为什么是单线程的?



  1. 代码更清晰,处理逻辑更简单;

  2. 不用考虑各种锁的问题,不存在加锁和释放锁的操作,没有因为可能出现死锁而导致的性能问题;

  3. 不存在多线程切换而消耗CPU;

  4. 无法发挥多核CPU的优势,但可以采用多开几个Redis实例来完善;


三、Redis真的是单线程的吗?



  1. Redis6.0之前是单线程的,Redis6.0之后开始支持多线程;

  2. Redis内部使用了基于epoll的多路服用,也可以多部署几个Redis服务器解决单线程的问题;

  3. Redis主要的性能瓶颈是内存和网络;

  4. 内存好说,加内存条就行了,而网络才是大麻烦,所以Redis6内存好说,加内存条就行了;

  5. 而网络才是大麻烦,所以Redis6.0引入了多线程的概念,

  6. Redis6.0在网络IO处理方面引入了多线程,如网络数据的读写和协议解析等,需要注意的是,执行命令的核心模块还是单线程的。


四、Redis优缺点


1、优点



  1. Redis是KV数据库,MySQL是关系型数据库,Redis速度更快;

  2. Redis数据操作主要在内存中,MySQL主要将数据存储在硬盘,Redis速度更快;

  3. Redis同样支持持久化(RDB+AOF),Redis支持将数据异步将内存的数据持久化到硬盘上,避免Redis宕机出现数据丢失的问题;

  4. Redis性能极高,读的速度是110000次/秒,写的速度是81000次/秒;

  5. Redis数据类型丰富,不仅支持KV键值对,还支持list、set、zset、hash等数据结构的存储;

  6. Redis支持数据的备份,即master-slave模式的数据备份;

  7. Redis支持简单的事务,操作满足原子性;

  8. Redis支持读写分离,分担读的压力;

  9. Redis支持哨兵模式,实现故障的自动转移;

  10. 单线程操作,避免了频繁的上下文切换;

  11. 采用了非阻塞I/O多路复用机制,性能卓越;


2、缺点



  1. 数据存储在内存,容易造成数据丢失;

  2. 存储容量受内存的限制,只能存储少量的常用数据;

  3. 缓存和数据库双写一致性问题;

  4. 用于缓存时,容易出现内存穿透、缓存击穿、缓存雪崩的问题;

  5. 修改配置文件后,需要进行重启,将硬盘中的数据同步到内存中,消耗的时间较长,而且数据同步的时间里Redis不能提供服务;


五、Redis常见业务场景



  1. Redis是基于内存的nosql数据库,可以通过新建线程的形式进行持久化,不影响Redis单线程的读写操作

  2. 通过list取最新的N条数据

  3. 模拟类似于token这种需要设置过期时间的场景

  4. 发布订阅消息系统

  5. 定时器、计数器

  6. 缓存加速、分布式会话、排行榜、分布式计数器、分布式锁;

  7. Redis支持事务、持久化、LUA脚本、发布/订阅、缓存淘汰、流技术等特性;


六、Redis常见数据类型



1、String


(1)String简介


String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M。


(2)应用场景


① 作为缓存数据库


在Java管理系统体系中,大多数都是用MySQL存储数据,redis作为缓存,因为Redis具有支撑高并发的特性,通常能起到加速读写和降低数据库服务器压力的作用,大多数请求都会先请求Redis,如果Redis中没有数据,再请求MySQL数据库,然后再缓存到Redis中,以备下次使用。



② 计数器


Redis字符串中有一个命令INCR key,incr命令会对值进行自增操作,比如CSDN的文章阅读,视频的播放量,都可以通过Redis来计数,每阅读一次就+1,同时将这些数据异步存储到MySQL数据库中,降低MySQL服务器的写入压力。


③ 共享session


在分布式系统中,用户每次请求一般会访问不同的服务器 ,这就会导致session不同步的问题,这时,一般会使用Redis来解决这个问题,将session存入Redis,使用的时候从Redis中取出就可以了。


④ 分布式锁



  1. setnx key value,加锁

  2. del key,释放锁


(3)key操作命令



(4)set key value


SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]



  1. EX seconds,设置过期时间,单位秒

  2. PX milliseconds,设置过期时间,单位毫秒

  3. EXAT timestamp-seconds,设置过期时间,以秒为单位的UNIX时间戳

  4. PXAT timestamp-milliseconds,设置过期时间,以毫秒为单位的UNIX时间戳

  5. NX,键不存在的时候设置键值

  6. XX,键存在的时候设置键值

  7. KEEPTTL,保留设置前指定键的生存时间

  8. GET,返回指定键原本的值,若键不存在返回nil


备注:


命令不区分大小写,而key是区分大小写的。


help @类型:查看当前类型相关的操作命令。


Since the SET command options can replace SETNX, SETEX, PSETEX, GETSET, it is possible that in future versions of Redis these commands will be deprecated and finally removed。


(5)同时设置多个键值


(6)获取指定区间范围内的值


getrange、setrange。


(7)数值增减



  1. INCR key,递增数字

  2. INCRBY key increment,增加指定的数值递增

  3. DECR key,递减数值

  4. DECRBY key decrement,指定指定的数值递减


(8)获取字符串的长度,内容追加



  1. STRLEN key,获取值的长度

  2. APPEND key value,内容追加


2、List


(1)List 列表简介


List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。


列表的最大长度为 2^32 - 1,也即每个列表支持超过 40 亿个元素。


主要功能有push/pop,一般用在栈、队列、消息队列等场景。



  1. left、right都可以插入添加;

  2. 如果键不存在,创建新的链表;

  3. 如果键存在,新增内容;

  4. 如果值全部移除,对应的键也会消失;


它的底层是双向链表,对两端的操作性能很高,通过索引下标操作中间的节点,性能会较差。


(2)应用场景


① 消息队列


使用 lpush + rpop或者 rpush + lpop实现消息队列,Redis还支持阻塞操作,在弹出元素的时候使用阻塞命令来实现阻塞队列。



② 作为栈使用


使用 lpush+lpop或者 rpush+rpop实现栈。



③ 文章列表


(3)常用命令



3、Hash


(1)hash简介


Hash 是一个键值对(key - value)集合,value也是一个hash,相当于 Map<String,Map<Object,Object>>


(2)常用场景


由于特殊的数据结构,hash一般作为存储bean使用,String+JSON的数据结构存储特定的应用场景。



(3)常用命令




4、Set


(1)Set类型简介


Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。


一个集合最多可以存储 2^32-1 个元素。概念和数学中个的集合基本类似,可以交集,并集,差集等等,所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。


(2)应用场景


① 相同好友可见


在朋友圈场景中,对于点赞、评论的功能,通过交集实现相同还有可见的功能。


② 共同关注、共同喜好


③ 抽奖功能


(3)常用命令



5、Zset


(1)Zset 类型简介


Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。


有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。


zset k1 score1 v1 score2 v2


(2)应用场景


① 排行榜


通过score来记录点赞数,然后根据score进行排序,实现排行榜的功能。


② 延迟消息队列


订单系统,下单后需要在15分钟内进行支付操作,否则自动取消订单。


将下单后15分钟后的时间作为score,订单作为value存入Redis,消费者轮询去消费,如果消费的大于等于score,则取消该订单。


(3)Zset常用命令



6、BitMap


(1)Bitmap简介


Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1的设置,表示某个元素的值或者状态,时间复杂度为O(1)。


(2)应用场景


由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。


① 签到统计


② 判断用户是否登录


③ 统计连续学习打卡的人


(3)BitMap常用命令



7、BitField


通过bitfield命令可以一次性操作多个比特位,它会执行一系列操作并返回一个响应数组,这个数组中的元素对参数列表中的相应操作的执行结果。


8、HyperLogLog


(1)HyperLogLog简介


Redis HyperLogLog 是 Redis 2.8.9 版本新增的数据类型,是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。但要注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。


所以,简单来说 HyperLogLog 提供不精确的去重计数。


HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的内存空间总是固定的、并且是很小的。


在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。


(2)应用场景


百万级网页 UV 计数


(3)常用命令



  1. pfadd key element,添加元素

  2. pfcount key,返回指定HyperLogLog的基数的估算值;

  3. pfmerge destkey sourcekey,将多个HyperLogLog合并成一个HyperLogLog;


9、GEO


(1)GEO简介


Redis GEO 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。


在日常生活中,我们越来越依赖搜索“附近的餐馆”、在打车软件上叫车,这些都离不开基于位置信息服务(Location-Based Service,LBS)的应用。LBS 应用访问的数据是和人或物关联的一组经纬度信息,而且要能查询相邻的经纬度范围,GEO 就非常适合应用在 LBS 服务的场景中。


(2)应用场景


高德地图、滴滴打车等定位软件。


(3)常用命令



10、Stream


(1)Stream简介


Redis Stream 是 Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。



在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:



  • 发布订阅模式,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;

  • List 实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一 ID。


基于以上问题,Redis 5.0 便推出了 Stream 类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。


(2)应用场景


消息队列


(3)常用命令



七、总结


Redis是一个key-value存储系统,支持10种数据类型,总结了为何要用Redis替代map作为程序缓存、Redis为什么是单线程的、Redis的优缺点、Redis的常用场景,做了一次Redis的快速入门。


最后说一句(求关注,别白嫖我)


如果这篇文章对您有所帮助,或者有所启发的话,您的关注和点赞是我坚持写作最大的动力。


关注公众号:【哪吒编程】,在公众号中回复【掘金】,获取Java学习资料、电子书;回复【星球】加入Java学习星球,陪伴学习,共同优秀。


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

大哥,这是并发不是并行,Are You Ok?

多线程概述 基础概念 进程和线程 进程是程序运行资源分配的最小单位 进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘IO等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。进程是具有一定独立功能...
继续阅读 »

多线程概述


file


基础概念


进程和线程



进程是程序运行资源分配的最小单位



进程是操作系统进行资源分配的最小单位,其中资源包括:CPU、内存空间、磁盘IO等,同一进程中的多条线程共享该进程中的全部系统资源,而进程和进程之间是相互独立的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。


进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的、静态的,进程是活的、动态的。进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身,用户进程就是所有由你启动的进程。



线程是CPU调度的最小单位,必须依赖于进程而存在



线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的、能独立运行的基本单位。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。



线程无处不在



任何一个程序都必须要创建线程,特别是Java不管任何程序都必须启动一个main函数的主线程; Java Web开发里面的定时任务、定时器、JSP和 Servlet、异步消息处理机制,远程访问接口RM等,任何一个监听事件, onclick的触发事件等都离不开线程和并发的知识。


CPU核心数和线程数的关系


多核心:也指单芯片多处理器( Chip Multiprocessors,简称CMP),CMP是由美国斯坦福大学提出的,其思想是将大规模并行处理器中的SMP(对称多处理器)集成到同一芯片内,各个处理器并行执行不同的进程。这种依靠多个CPU同时并行地运行程序是实现超高速计算的一个重要方向,称为并行处理


多线程: Simultaneous Multithreading.简称SMT.让同一个处理器上的多个线程同步执行并共享处理器的执行资源。


核心数、线程数:目前主流CPU都是多核的。增加核心数目就是为了增加线程数,因为操作系统是通过线程来执行任务的,一般情况下它们是1:1对应关系,也就是说四核CPU一般拥有四个线程。但 Intel引入超线程技术后,使核心数与线程数形成1:2的关系


file


CPU时间片轮转机制


file


为什么感受不到CPU线程数的限制


我们平时在开发的时候,感觉并没有受cpu核心数的限制,想启动线程就启动线程,哪怕是在单核CPU上,为什么?这是因为操作系统提供了一种CPU时间片轮转机制。


时间片轮转调度是一种最古老、最简单、最公平且使用最广的算法,又称RR调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。


什么是CPU轮转机制


百度百科对CPU时间片轮转机制原理解释如下:


如果在时间片结束时进程还在运行,则CPU将被剥夺并分配给另一个进程。如果进程在时间片结束前阻塞或结来,则CPU当即进行切换。调度程序所要做的就是维护一张就绪进程列表,当进程用完它的时间片后,它被移到队列的末尾


时间片长度


时间片轮转调度中唯一有趣的一点是时间片的长度。从一个进程切换到另一个进程是需要定时间的,包括保存和装入寄存器值及内存映像,更新各种表格和队列等。假如进程切( processwitch),有时称为上下文切换( context switch),需要5ms,再假设时间片设为20ms,则在做完20ms有用的工作之后,CPU将花费5ms来进行进程切换。CPU时间的20%被浪费在了管理开销上了。


为了提高CPU效率,我们可以将时间片设为5000ms。这时浪费的时间只有0.1%。但考虑到在一个分时系统中,如果有10个交互用户几乎同时按下回车键,将发生什么情况?假设所有其他进程都用足它们的时间片的话,最后一个不幸的进程不得不等待5s才获得运行机会。多数用户无法忍受一条简短命令要5才能做出响应,同样的问题在一台支持多道程序的个人计算机上也会发


结论可以归结如下:时间片设得太短会导致过多的进程切换,降低了CPU效率:而设得太长又可能引起对短的交互请求的响应变差。将时间片设为100ms通常是一个比较合理的折衷。


在CPU死机的情况下,其实大家不难发现当运行一个程序的时候把CPU给弄到了100%再不重启电脑的情况下,其实我们还是有机会把它KILL掉的,我想也正是因为这种机制的缘故。


澄清并行和并发


我们举个例子,如果有条高速公路A上面并排有8条车道,那么最大的并行车辆就是8辆此条高速公路A同时并排行走的车辆小于等于8辆的时候,车辆就可以并行运行。CPU也是这个原理,一个CPU相当于一个高速公路A,核心数或者线程数就相当于并排可以通行的车道;而多个CPU就相当于并排有多条高速公路,而每个高速公路并排有多个车道。


当谈论并发的时候一定要加个单位时间,也就是说单位时间内并发量是多少?离开了单位时间其实是没有意义的。


俗话说,一心不能二用,这对计算机也一样,原则上一个CPU只能分配给一个进程,以便运行这个进程。我们通常使用的计算机中只有一个CPU,也就是说只有一颗心,要让它一心多用同时运行多个进程,就必须使用并发技术。实现并发技术相当复杂,最容易理解的是“时间片轮转进程调度算法”。


综合来说:


并发:指应用能够交替执行不同的任务,比如单CPU核心下执行多线程并非是同时执行多个任务,如果你开两个线程执行,就是在你几乎不可能察觉到的速度不断去切换这两个任务,已达到"同时执行效果",其实并不是的,只是计算机的速度太快,我们无法察觉到而已.


并行:指应用能够同时执行不同的任务,例:吃饭的时候可以边吃饭边打电话,这两件事情可以同时执行


两者区别:一个是交替执行,一个是同时执行.


file
感觉上是同时发生的,但是微观上还是有区别的,并行是同意时刻发生的,并发是同一时刻交替执行


file


高并发的意义



由于多核多线程的CPU的诞生,多线程、高并发的编程越来越受重视和关注。多线程可以给程序带来如下好处。



1. 充分利用CPU的资源


从上面的CPU的介绍,可以看的出来,现在市面上没有CPU的内核不使用多线程并发机制的,特别是服务器还不止一个CPU,如果还是使用单线程的技术做思路,明显就out了。因为程序的基本调度单元是线程,并且一个线程也只能在一个CPU的一个核的一个线程跑,如果你是个i3的CPU的话,最差也是双核心4线程的运算能力:如果是一个线程的程序的话,那是要浪费3/4的CPU性能:如果设计一个多线程的程序的话,那它就可以同时在多个CPU的多个核的多个线程上跑,可以充分地利用CPU,减少CPU的空闲时间,发挥它的运算能力,提高并发量。


就像我们平时坐地铁一样,很多人坐长线地铁的时候都在认真看书,而不是为了坐地铁而坐地铁,到家了再去看书,这样你的时间就相当于有了两倍。这就是为什么有些人时间很充裕,而有些人老是说没时间的一个原因,工作也是这样,有的时候可以并发地去做几件事情,充分利用我们的时间,CPU也是一样,也要充分利用。


2. 加快响应用户的时间


比如我们经常用的迅雷下载,都喜欢多开几个线程去下载,谁都不愿意用一个线程去下载,为什么呢?答案很简单,就是多个线程下载快啊。


我们在做程序开发的时候更应该如此,特别是我们做互联网项目,网页的响应时间若提升1s,如果流量大的话,就能增加不少转换量。做过高性能web前端调优的都知道,要将静态资源地址用两三个子域名去加载,为什么?因为每多一个子域名,浏览器在加载你的页面的时候就会多开几个线程去加载你的页面资源,提升网站的响应速度。多线程,高并发真的是无处不在。


3. 可以使你的代码模块化,异步化,简单化


例如我们实现电商系统,下订单和给用户发送短信、邮件就可以进行拆分,将给用户发送短信、邮件这两个步骤独立为单独的模块,并交给其他线程去执行。这样既增加了异步的操作,提升了系统性能,又使程序模块化,清晰化和简单化。


多线程应用开发的好处还有很多,大家在日后的代码编写过程中可以慢慢体会它的魅力。


多线程程序需要注意事项


1. 线程之间的安全性


从前面的章节中我们都知道,在同一个进程里面的多线程是资源共享的,也就是都可以访问同一个内存地址当中的一个变量。例如:若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的:若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。


2. 线程之间的死锁


为了解决线程之间的安全性引入了Java的锁机制,而一不小心就会产生Java线程死锁的多线程问题,因为不同的线程都在等待那些根本不可能被释放的锁,从而导致所有的工作都无法完成。假设有两个线程,分别代表两个饥饿的人,他们必须共享刀叉并轮流吃饭。他们都需要获得两个锁:共享刀和共享叉的锁。


假如线程A获得了刀,而线程B获得了叉。线程A就会进入阻塞状态来等待获得叉,而线程B则阻塞来等待线程A所拥有的刀。这只是人为设计的例子,但尽管在运行时很难探测到,这类情况却时常发生


3. 线程太多了会将服务器资源耗尽形成死机当机


线程数太多有可能造成系统创建大量线程而导致消耗完系统内存以及CPU的“过渡切换”,造成系统的死机,那么我们该如何解决这类问题呢?


某些系统资源是有限的,如文件描述符。多线程程序可能耗尽资源,因为每个线程都可能希望有一个这样的资源。如果线程数相当大,或者某个资源的侯选线程数远远超过了可用的资源数则最好使用资源池。一个最好的示例是数据库连接池。只要线程需要使用一个数据库连接,它就从池中取出一个,使用以后再将它返回池中。资源池也称为资源库。


多线程应用开发的注意事项很多,希望大家在日后的工作中可以慢慢体会它的危险所在。


作者:博学谷_狂野架构师
链接:https://juejin.cn/post/7197622529599324215
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

金三银四好像消失了,IT行业何时复苏!

疫情时候不敢离职,以为熬过来疫情了,行情会好一些,可是疫情结束了,反而行情更差了, 这是要哪样 我心中不由一万个 草泥🐴 路过 我心中不惊有了很多疑惑和感叹 接着上一篇 一个28岁程序员入行自述和感受 自我10连问 我的心情 自去年下半年以来,互联网行业一片...
继续阅读 »

疫情时候不敢离职,以为熬过来疫情了,行情会好一些,可是疫情结束了,反而行情更差了,
这是要哪样 我心中不由一万个 草泥🐴 路过



我心中不惊有了很多疑惑和感叹 接着上一篇


一个28岁程序员入行自述和感受


自我10连问


我的心情


自去年下半年以来,互联网行业一片寒冬传言,众多企业倒闭,裁员。本以为随着疫情、春季和金融楼市的回暖,一切都会变好。然而,站在这个应该是光明的时刻,举世瞩目的景象却显得毫无生气。令人失望的是,我们盼望已久的春天似乎仍未到来。


我的工作生涯


我已经从业近十年,然而最近两年一直在小公司中工作,



我的技术和经历并不出色。随着年龄的增长,是否我的技能也在快速提高呢?我们该如何前进呢 ,转产品,产品到达极限,转管理,可是不会人情事故,



我们该如何继续前进呢?目前还没有人给出答案。


第一家公司


我记得那是很早的时候了,那个时候简历投递出去,就马上会收到很多回复,不像现在 ,
失联招聘前程堪忧boss直坑


你辛苦的写完简历,满怀期待投递了各大招聘平台,可等来的 却是已读未回,等的心也凉透了。


好怀念之前的高光时刻 神仙打架日子


前面我面试 几乎一周都安排满了,都面试不过来,我记得那会最多时候一天可以跑三家面试哈哈哈,也是很拼命的,有面试机会谁不想多试试呢


我第一家进入的是一个外包公司,叫xxx东软集团, 那个时候也不不懂,什么是外包给公司,只看工资给的所有offer中最高的,然后就去了哈哈哈哈。


入职第一天,我背着我自己的电脑满怀着激动就去了,然后被眼前一幕吸引了,办公的人真多啊,办公室都是拿透明玻璃隔开那种,人挺多,我一想公司还挺大的,
随后我就被带到也是一个玻璃格子办公室,里面就三个人,加我一个4个。


我害怕极了,这个时候一个稍微有一些秃顶的 大叔过来了 哈哈哈(内心台词,早就知道这一行干就了,会秃头这不会就是下一个我把


他把我安排在了靠近玻璃门的也就是大门位置,这是知道我准备随时跑路节奏吗。然后就去忙他自己的了。整个上午我就像是一个被遗忘在角落里的人一样。根本没人管我,就这样第一天结束了,我尴尬了做了一整天。


这工作和我想象的有点不太一样啊!


后面第三天还是如此,办公室里依旧是那么几个人,直到第四天,大叔来了,问我直到多线程吗,让我用多线程写一个抽奖的活动项目。(内心我想终于有事情干了,可是也高兴不起来谁知道怎么写)


不过好在,他也没有说什么时候交,只是说写完了给他看一下,经过我几天的,复制粘贴工程师 一顿谷歌,百度,终于是勉强写出来了。。。。。


后面,就又陆陆续续来了几个小伙伴,才开始新项目和开会,第一份工作大致就是这样开始了我的职业生涯。怎么说呢和我想象的有所不一样,但又有一些失望。


后面干了1年多,我就离职了原因是太累了没时间休息,一个项目接着一个项目的


第二家公司


在离开第一家公司时候,我休息了好长一段时间,调整了我自己的状态


也了解了什么是外包公司,什么是工作外派,也是我这一次就在投递简历,和面试时候刻意去避免进那种外包,和外派公司。


面试什么也还算顺利,不到半个月就拿到了offer。 但是工资总体来说比上一家是要少一点,但是我也接受了,是一家做本地生鲜电商公司,,本来生活科技有公司, 我觉得公司氛围,和公司都挺不错的,就入职了。


入职了我们那个项目经理还算很热情的,让同事帮我开git账号,开了邮箱,我自己拉取了公司项目,然后同事帮我运行调试环境第一天,项目什么都跑了起来,


你知道的,每次去一家新公司,开始新项目难的是项目复杂配置文件,和各种mave包依赖,接口,环境冲突,所以跑起来项目自己一个人摸索还是需要一些时间的。


在这家公司前期也还不错,公司维护自己项目,工作时间也比较自由和灵活,


大体流程是,每次有新的pm时候 产品经理就会组织各个部门开会


h5端-移动端-接口端开会讨论需求细节和实现,如果有问题头就会pass掉


然后产品经理就会把需求指派到每一个头,头把需求指派给组员,然后组员按照
redmine 上截止时间开发需求,


开发过程中自己去找对应接口负责方,其他业务负责方 去对接数据,没有问题了就可以提交给指定测试组测试了。


然后测试组头会把,测试分配给他们组员,进行测试。


有问题了就会在指派回来给对应负责各个开发同学去解决bug,直到测试完成。测试会让你提交堡垒环境 然后等待通知发布上线,


我们一般是晚上8点时候发布,发布时候,一般开发人员都要留守,直到发布上线没有问题了,才可以回家。如果弄得很晚的话,第二天可以晚点上班。


这一点是我觉得比较好的地方,工作时间弹性比较自由。


记得有一次生产事故。


我影响很深刻,东西上线了,然后产品经理说这个和他设计的预期的不符合要求,需要重新写,那一晚我们整组弄得很晚,就是为了等我弄好去吃饭。


你知道人在心急如焚情况下,是写不好代码的最后还是同事帮我一起完成了产品经理变态需求修改。。。。。。(也就在那时候我才知道产品经理和开发为什么不和了


因为五行相克


因为经常这样发版,然后一起吃饭公司报销。我们组员和领导关系还算不错氛围还挺好。在这一家公司我待了挺久的。


离职原因
后期因为说公司项目战略升级。空降了一位携程cto,还带来了他的手下人,我们组头,职权被削弱了,我不在由原来头管理了。再加上后面一些其他原因。老同事一个一个走了。


最后这个组只剩下我,和一个进来不久新同事。 不久我也离职了。


第三家公司


这次离职后,我调整休息了差不多有一年,中间离开上海去了江苏,因为家里,女朋友等各种事情。后面我才又从新去了上海,开始找工作。


找工作期间投奔的同事,合同事住一起。


这次面试我明显感觉,有一些慌张了,可能是太久没上班原因,有一些底气不足。好在也是找到了工作虽然不太理想。


这个过程太曲折了,后面公司终究没有扛过疫情,可以说在疫情边缘倒闭了,钱赔偿也没拿到,。。。这里就不赘述了。


IT行业如何破局 大家有什么想法和故事吗。可以关注 程序员三时公众号 进行技术交流讨论


嗯~还想往下面写一点什么,,,下一篇分享一下我现在工作和未来思考


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

话说工作的“边界感”

一句话的合作例子 今天有一个业务的运营同学匆匆忙忙找到我,说要跟我这边有一个问题,业务要合作,然后已经提前和我老板打过招呼了。事情是这样的,我这边负责的是工作台门户,然后作为一个平台业务,有大量的客户需要找到对应的服务商来给自己定制门户。然后这位同学负责的是定...
继续阅读 »

一句话的合作例子


今天有一个业务的运营同学匆匆忙忙找到我,说要跟我这边有一个问题,业务要合作,然后已经提前和我老板打过招呼了。事情是这样的,我这边负责的是工作台门户,然后作为一个平台业务,有大量的客户需要找到对应的服务商来给自己定制门户。然后这位同学负责的是定制业务,所以要统一把所有的定制业务全部收口,但是这位定制同学的业务没有对应的技术研发同学,所以他就找到我的老板同步了这个情况。


分工协作的本质


其实问题的合作方式是比较简单的,但是当她跟我说最终客户定制界面也由我来开发的时候,比如定制的费用是多少、定制的时间要求等等,我就觉得问题有些奇怪了。因为按照常理来说,我负责的是工作台,但是由于有定制业务相关的逻辑,所以我要处理一定的业务逻辑,但是让我去承担这个定制页面的开发,我觉得是有问题的。


举一个简单的例子,假如我现在是一个博物馆,原来用户是直接可以免费没有任何阻挡地进入博物馆的,但是突然有一天市政府说所有公共设施要收费了,那么对于博物馆的工作人员来说肯定是支持的,但是突然你又告诉我,我这个博物馆还要去维护全市统一的收费系统,这个就是不合理的。哪怕他找我的主管沟通结果也是一样,因为我和我的主管是属于博物馆体系的工作人员,他也没有义务和责任去维护整个所有的公共设施的收费系统。但是作为公共设施系统的一部分,如果有统一的收费规则,那么对于博物馆来说也是要遵守的。


所以这面就引出了我对于业务边界上面的一个思考。我经常看到同学给我转发一段话,说跟你老板打沟通了业务的合作情况,你的老板觉得非常不错,于是这位同学就匆匆忙忙的找到我来开始谈业务,谈实施细节并且需要我快速落地。而实际上这种所谓的业务协同的情况大部分也只会停留在沟通的层面,在最终落地的时候,往往和业务同学的预期不相符。在业务同学眼里看来,就是你们阴奉阳违,恨不得马上就开始投诉。


这里面非常核心的一个误区就是业务同学往往没有划清业务界限和系统界限的边界。对于业务同学来说,边界可能不会那么明显,但对于一个系统开发的同学来说,业务和边界是非常明显的,因为系统是物理存在的,有着天然的“隔离”。所以对于业务同学,如果想要顺畅的推动业务,必须要事先清晰的划分参与方的角色和业务边界,并且可以进一步了解到系统边界在哪里。


这个由谁来做就涉及到了一个很大权责问题。简单来说就是我做了有什么好处,换句话来说做这件事和我的职务目标有什么关系?如果没有关系,我为什么要做?就算同一个公司,也有很多需要完成的事,比如公司保洁不到位,我作为公司的员工,是否也立即从事保洁?


如果是我的职务目标,我的责任有多少?我承担了既定的责任,那我是否能够承担起对应的权利?在我上次借用的博物馆的例子可以看到,如果我承担了全市的公共系统的收费设施的维护,那么我的权利在哪里?如果我的权利只是在博物馆这一个地方的收费上面,那么这就变成了权责不对等。


但是如果我做成了全市公共收费系统,并且能掌管全市所有公共设施的收费业务,那么对于这个收费系统的开发权则是相等的,但是对于我本身职务的权责又是不等的,因为公司请我来管理博物馆的,而非管理整个全市的收费系统。


所以在思考业务推进的时候,首先就要思考系统的边界和权责对等关系,如果这一层面没有理清楚的话,合作大概率是不能完成的。而很多的业务同学就以“我和你老板谈好的东西,为什么你不去做”这么简单的方式来拷问协同关系,我觉得是非常的幼稚的。


所以我希望其实我们在去和别人沟通业务的时候,往往要带着权责,带着边界的思考,去和对方去讨论,去协商,去沟通。简单来说,我在跟你聊之前,我要知道你的系统,你的业务边界在哪里?我跟你聊的时候,我要清晰地告诉你,这个事情做了对你有什么好处,对我有什么好处,哪部分应该你做,哪部分应该我来做。只有在这样的一种沟通方式下面才是真正合理的,真正是可以落地的沟通和协作方式。


而在这些问题没有达成一致之前,由谁来做都没有定下来的时候,应该先去往上升,在顶层设计里面去规划去重新思考如何从组织设计的方式去让业务协作自然的发生。


总结


这里再总结一下,这里是一个小的心得。这个案例也告诉我们,我们去沟通协同的时候要有边界感,包括业务的边界和系统的边界。只有把边界理顺了,合作才有可能。

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

代码中被植入了恶意删除操作,太狠了!

背景在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。对方拿到镜...
继续阅读 »

背景

在交接的代码中做手脚进行删库等操作,之前只是网上听说的段子,没想到上周还真遇到了,并且亲自参与帮忙解决。

事情是这样的,一老板接手了一套系统,可能因为双方在交接时出现了什么不愉快的事情,对方不提供源代码,只是把生产环境的服务器打了一个镜像给到对方。

对方拿到镜像恢复之后,系统起来怎么也无法正常处理业务,于是就找到我帮忙看是什么原因。经过排查,原来交接的人在镜像中做了多处手脚,多处删除核心数据及jar包操作。下面来给大家细细分析排查过程。

排查过程

由于只提供了镜像文件,导致到底启动哪些服务都是问题。好在是Linux操作系统,镜像恢复之后,通过history命令可以查看曾经执行了哪些命令,能够找到都需要启动哪些服务。但服务启动之后,业务无法正常处理,很多业务都处于中间态。

原本系统是可以正常跑业务的,打个镜像之后再恢复就不可以了?这就奇怪了。于是对项目(jar包或war)文件进行排查,查看它们的修改时间。

在文件的修改时间上还真找到了一些问题,发现在打镜像的两个小时前,项目中一个多个项目底层依赖的jar包被修改过,另外还有两个class文件被修改过。

于是,就对它们进行了重点排查。首先反编译了那两个被修改过的class文件,在代码中找到了可疑的地方。

可疑代码

在两个被修改的类中都有上述代码。最开始没太留意这段代码,但直觉告诉我不太对,一个查询业务里面怎么可能出现删除操作呢?这太不符合常理了。

于是仔细阅读上述代码,发现上述红框中的代码无论何时执行最终的结果都是id=1。你是否看出来了?问题就出在三目表达式上,无论id是否为null,id被赋的值都是1。看到这里,也感慨对方是用心了。为了隐藏这个目的,前面写了那么多无用的代码。

但只有这个还不是什么问题,毕竟如果只是删除id为1的值,也只是删除了一条记录,影响范围应该有限。

紧接着反编译了被修改的jar包,依次去找上述删除方法的底层实现,看到如下代码:

删除操作

原来前面传递的id=1是为了配合where条件语句啊,当id=1被传递进来之后,就形成了where 1=1的条件语句。这个大家在mybatis中拼接多条件语句时经常用到。结果就是一旦执行了上述业务逻辑,就会触发删除T_QUART_DATA全表数据的操作。

T_QUART_DATA表中是用于存储触发定时任务的表达式,到这里也就明白了,为啥前面的业务跑不起来,全部是中间态了。因为一旦在业务逻辑中触发开关,把定时任务的cron表达式全部删除,十多个定时任务全部歇菜,业务也就跑步起来了。

找到了问题的根源,解决起来就不是啥事了,由于没有源代码,稍微费劲的是只能把原项目整个反编译出来,然后将改修改地方进行了修改。

又起波折

本以为到此问题已经解决完毕了,没想到第二天又出现问题了,项目又跑不起来了。经过多方排查和定位,感觉还有定时任务再进行暗箱操作。

于是通过Linux的crontab命令查看是否有定时任务在执行,执行crontab -ecrontab -l,还真看到有三个定时任务在执行。跟踪到定时任务执行的脚本中,而且明目张胆的起名deleteXXX:

删除脚本

而在具体的脚本中,有如下执行操作:

删除核心依赖包

这下找到为什么项目中第二天为啥跑不起来了,原来Linux的定时任务将核心依赖包删除了,并且还会去重启服务。

为了搞破坏,真是煞费苦心啊。还好的是这个jar包在前一天已经反编译出来了,也算有了备份。

小结

原本以为程序员在代码中进行删库操作或做一些其他小手脚只是网络上的段子,大多数人出于职业操守或个人品质是不会做的。没想到这还真遇到了,而且对方为了隐藏删除操作,还做了一些小伪装,真的是煞费苦心啊。如果有这样的能力和心思,用在写出更优秀的代码或系统上或许更好。

当然,不知道他们在交接的过程中到底发生了什么,竟然用这样的方式对待昔日合作的伙伴。之所以写这篇文章,是想让大家学习如何排查代码问题的过程,毕竟用到了不少知识点和技能,但这并不是教大家如何去做手脚。无论怎样,最起码的职业操守还是要有的,这点不接受反驳。


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

电视剧里的代码真能运行吗?

大家好,欢迎来到 Crossin的编程教室 ! 前几天,后台老有小伙伴留言“爱心代码”。这不是Crossin很早之前发过的内容嘛,怎么最近突然又被人翻出来了?后来才知道 ,原来是一部有关程序员的青春偶像剧《点燃我,温暖你》在热播,而剧中有一段关于期中考试要用程...
继续阅读 »

大家好,欢迎来到 Crossin的编程教室 !


前几天,后台老有小伙伴留言“爱心代码”。这不是Crossin很早之前发过的内容嘛,怎么最近突然又被人翻出来了?后来才知道


,原来是一部有关程序员的青春偶像剧《点燃我,温暖你》在热播,而剧中有一段关于期中考试要用程序画一个爱心的桥段。


于是出于好奇,Crossin就去看了这一集(第5集,不用谢)。这一看不要紧,差点把刚吃的鸡腿给喷出来--槽点实在太多了!


忍不住做了个欢乐吐槽向的代码解读视频,在某平台上被顶到了20个w的浏览,也算蹭了一波人家电视剧的热度吧……


下面是图文版,给大家分析下剧中出现的“爱心”代码,并且来复刻一下最后男主完成的酷炫跳动爱心。


剧中代码赏析


1. 首先是路人同学的代码:



虽然剧中说是“C语言期中考试”,但这位同学的代码名叫 draw2.py,一个典型的 Python 文件,再结合截图中的 pen.forward、pen.setpos 等方法来看,应该是用 turtle 海龟作图库来画爱心。那效果通常是这样的:

import turtle as t
t.color('red')
t.setheading(50)
t.begin_fill()
t.circle(-100, 170)
t.circle(-300, 40)
t.right(38)
t.circle(-300, 40)
t.circle(-100, 170)
t.end_fill()
t.done()



而不是剧中那个命令行下用1组成的不规则的图形。


2. 然后是课代表向路人同学展示的优秀代码:



及所谓的效果:



这确实是C语言代码了,但文件依然是以 .py 为后缀,并且 include 前面没有加上 #,这显然是没法运行的。


里面的内容是可以画出爱心的,用是这个爱心曲线公式:



然后遍历一个15*17的方阵,计算每个坐标是在曲线内还是曲线外,在内部就输出#或*,外部就是-


用python改写一下是这样的:

for y in range(9, -6, -1):
for x in range(-8, 9):
print('*##*'[(x+10)%4] if (x*x+y*y-25)**3 < 25*x*x*y*y*y else '-', end=' ')
print()

效果:



稍微改一下输出,还能做出前面那个全是1的效果:

for y in range(9, -6, -1):
for x in range(-8, 9):
print('1' if (x*x+y*y-25)**3 < 25*x*x*y*y*y else ' ', end=' ')
print()


但跟剧中所谓的效果相去甚远。


3. 最后是主角狂拽酷炫D炸天的跳动爱心:



代码有两个片段:




但这两个片段也不C语言,而是C++,且两段并不是同一个程序,用的方法也完全不一样。


第一段代码跟前面一种思路差不多,只不过没有直接用一条曲线,而是上半部用两个圆形,下半部用两条直线,围出一个爱心。



改写成 Python 代码:

size = 10
for x in range(size):
for y in range(4*size+1):
dist1 = ((x-size)**2 + (y-size)**2) ** 0.5
dist2 = ((x-size)**2 + (y-3*size)**2) ** 0.5
if dist1 < size + 0.5 or dist2 < size + 0.5:
print('V', end=' ')
else:
print(' ', end=' ')
print()

for x in range(1, 2*size):
for y in range(x):
print(' ', end=' ')
for y in range(4*size+1-2*x):
print('V', end=' ')
print()

运行效果:



第二段代码用的是基于极坐标的爱心曲线,是遍历角度来计算点的位置。公式是:



计算出不同角度对应的点坐标,然后把它们连起来,就是一个爱心。

from math import pi, sin, cos
import matplotlib.pyplot as plt
no_pieces = 100
dt = 2*pi/no_pieces
t = 0
vx = []
vy = []
while t <= 2*pi:
vx.append(16*sin(t)**3)
vy.append(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t))
t += dt
plt.plot(vx, vy)
plt.show()

效果:



代码中循环时用到的2π是为了保证曲线长度足够绕一个圈,但其实长一点也无所谓,即使 π=100 也不影响显示效果,只是相当于同一条曲线画了很多遍。所以剧中代码里写下35位小数的π,还被女主用纸笔一字不落地抄写下来,实在是让程序员无法理解的迷惑行为。



但不管写再多位的π,上述两段代码都和最终那个跳动的效果差了五百只羊了个羊。


跳动爱心实现


作为一个总是在写一些没什么乱用的代码的编程博主,Crossin当然也不会放过这个机会,下面就来挑战一下用 Python 实现最终的那个效果。


1. 想要绘制动态的效果,必定要借助一些库的帮助,不然代码量肯定会让你感动得想哭。这里我们将使用之前 羊了个羊游戏 里用过的 pgzero 库。然后结合最后那个极坐标爱心曲线代码,先绘制出曲线上离散的点。

import pgzrun
from math import pi, sin, cos

no_p = 100
dt = 2*3/no_p
t = 0
x = []
y = []
while t <= 2*3:
x.append(16*sin(t)**3)
y.append(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t))
t += dt

def draw():
screen.clear()
for i in range(len(x)):
screen.draw.filled_rect(Rect((x[i]*10+400, -y[i]*10+300), (4, 4)), 'pink')

pgzrun.go()


2. 把点的数量增加,同时沿着原点到每个点的径向加一个随机数,并且这个随机数是按照正态分布来的(半个正态分布),大概率分布在曲线上,向曲线内部递减。这样,就得到这样一个随机分布的爱心效果。

...
no_p = 20000
...
while t <= 2*pi:
l = 10 - abs(random.gauss(10, 2) - 10)
x.append(l*16*sin(t)**3)
y.append(l*(13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)))
t += dt
...


3. 下面就是让点动起来,这步是关键,也有一点点复杂。为了方便对于每个点进行控制,这里将每个点自定义成了一个Particle类的实例。


从原理上来说,就是给每个点加一个缩放系数,这个系数是根据时间变化的正弦函数,看起来就会像呼吸的节律一样。

class Particle():
def __init__(self, pos, size, f):
self.pos = pos
self.pos0 = pos
self.size = size
self.f = f

def draw(self):
screen.draw.filled_rect(Rect((10*self.f*self.pos[0] + 400, -10*self.f*self.pos[1] + 300), self.size), 'hot pink')

def update(self, t):
df = 1 + (2 - 1.5) * sin(t * 3) / 8
self.pos = self.pos0[0] * df, self.pos0[1] * df

...

t = 0
def draw():
screen.clear()
for p in particles:
p.draw()

def update(dt):
global t
t += dt
for p in particles:
p.update(t)


4. 剧中爱心跳动时,靠中间的点波动的幅度更大,有一种扩张的效果。所以再根据每个点距离原点的远近,再加上一个系数,离得越近,系数越大。

class Particle():
...
def update(self, t):
df = 1 + (2 - 1.5 * self.f) * sin(t * 3) / 8
self.pos = self.pos0[0] * df, self.pos0[1] * df


5. 最后再用同样的方法画一个更大一点的爱心,这个爱心不需要跳动,只要每一帧随机绘制就可以了。

def draw():
...
t = 0
while t < 2*pi:
f = random.gauss(1.1, 0.1)
x = 16*sin(t)**3
y = 13*cos(t)-5*cos(2*t)-2*cos(3*t)-cos(4*t)
size = (random.uniform(0.5,2.5), random.uniform(0.5,2.5))
screen.draw.filled_rect(Rect((10*f*x + 400, -10*f*y + 300), size), 'hot pink')
t += dt * 3


合在一起,搞定!



总结一下,就是在原本的基础爱心曲线上加上一个正态分布的随机量、一个随时间变化的正弦函数和一个跟距离成反比的系数,外面再套一层更大的随机爱心,就得到类似剧中的跳动爱心效果。


但话说回来,真有人会在考场上这么干吗?


除非真的是超级大学霸,不然就是食堂伙食太好--


吃太饱撑的……


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

开发的功能不都是经过上线测试,为什么上线后还会那么多 Bug ?

你是否也经过这样的灵魂拷问:「开发的功能不都是经过上线测试的吗?为什么上线后还会那么多 Bug ?」。 大家明明都很努力,为什么「输出」的结果没有更进一步?今天我们就水一水这个「狗血」话题,究竟是谁个锅? 本篇只是毫无意义的「故事」,内容纯属「虚构」,如有...
继续阅读 »

你是否也经过这样的灵魂拷问:「开发的功能不都是经过上线测试的吗?为什么上线后还会那么多 Bug ?」。


大家明明都很努力,为什么「输出」的结果没有更进一步?今天我们就水一水这个「狗血」话题,究竟是谁个锅?




本篇只是毫无意义的「故事」,内容纯属「虚构」,如有雷同,自己「反思」。



对于这个话题,我想用一个「虚构」的故事来介绍下,这个故事里我有一个「朋友」,他在某电商项目组,当时恰逢经历了公司内部的「双十一需求立项」:


立项时


老板:“这次的需求很简单,主要就是参考去年 TB 的预热活动来走,核心就是提高客单量和活跃,具体细节你们和产品沟通就行”


产品:“TB 去年的活动预热大家应该都了解吧,我们这次主要就是复刻一个类似的活动,时间一个月,具体有***,总的来说,双十一活动一定要准时上线,这次运营那边投入很多经费,需求方面我会把控好,核心围绕老板的思路,细节大家可以看文档,基本就是这些内容,一个月应该够的,大家没问题吧?”


开发:“没问题,保证完成任务”


3 天后:


老板:“我刚看了 JD 好像推出了一个不错的小游戏,我觉得这个可以导入到这边活动里,这样可以提高用户的活跃,用户活跃了,消费自然就起来了”


开会


产品:“鉴于老板的意见,我觉得 JD 的这个游戏活动效果可以提升我们的复购,所以我计划在原本需求基础上增加一个支线活动。


产品:“大家不要紧张,支线会和当前主线同步开发,支线活动比较灵活,不对账号级别做限制,另外「设计」那边看下入口放哪里比较合适。”


开发:“上线时间还是没变吗?”


产品:“双十一日期会变吗?老板说了大家抓紧点,功能不多,时间还是够的”


10 天后:


老板:“我刚和x总沟通了下,他觉得我们的活动少了互动,不利于留存,你看下怎么处理”


开会


产品:“经过这几天和老板的探讨,我们发现这次活动少了一些互动性,必须增加一些交互游戏,这样才能提升日活和用户体验。


产品:“这样吧,这部分功能也是比较迫切,我知道任务比较重,但是为了这次能取到较好成果,大家加把劲,接下来周末幸苦下大家,先不休假,后面调休补回来,具体需求大家可以看文档,有一些调整,不多,时间还是够的”


开发:“。。。。”


14 天后:


老板:“我看大家工作的热情还是可以的,刚运营提了可以增加一些视频支持,我觉得这是一个很好的意见”


开会


产品:“目前看起来我们的开发进度还比较乐观,运营的同学说了他们录制了一些活动视频,我看了还不错,就在主会场增加一些视频播放的功能吧,细节你们直接和设计讨论下”


产品:“这个应该不难吧,把分享和下载加上,视频播放这么基础的东西,应该不耽误事,我看网上开源服务就挺多的。”


开发:“。。。。”


20 天后:


老板:“我刚去开发那里看了下效果,感觉分会场的效果挺好的,做支线太可惜了,给他加回主流程里”


开会


产品:“老板那边觉得分会场的效果更好,让我们把分会场的效果做到主会场里的核心交互里,分会场部分的逻辑就不要了,入口也取消。


产品:“大家不要激动,都是现成的东西,改一改很快的,不过项目进度目前看来确实起有点拉垮,接下来大家晚上多幸苦点,我们晚上11点后再下班,我申请给大家报销打车的费用”


开发:“。。。。”


23 天后:


老板:“整体效果我觉得可以,就是好像有一些糙,你们再过一过,另外大家开个会统一下目标,看看能不能有新的补充”


产品:“可以的,过去我们也一直在开会对齐,基本每两天都会对齐一次”


开会


产品:“我和设计对了下,发现有一些细节可以优化,比如一些模块的颜色可以细调一下,还有这些按键的动画效果也加上,我知道工期紧张,所以我从「隔壁」项目组借了几个开发资源,未来一周他们可以帮助大家缓解下压力”


开发:“。。。。”


28 天后:


开会


产品:“好了,项目可以提测了,相信之前测试也陆续介入跟进了需求,应该问题不大,目前看起来「燃尽图」还可以,测试完了尽快上线,老板那边也在催了”


测试:“不行啊,今天走了用例,一堆问题,而且提测版本怎么和用例还有需求文档不一致,这个提测的版本开发是不是自己没做过测试啊,一大堆问题,根本测不下去,这样我们很难做”


产品:“这个我来沟通,开发接下来这几天大家就不要回家了,马上活动上线,一起攻克难关”


产品:“我也理解大家很努力了,但是这次提测的质量确实不行,你们还是要自己多把把关,不能什么问题都等到 QA 阶段才暴露,这样不利于项目进度,需求一直都很明确,大家把 Bug 尽可能修一修,有什么问题我们及时沟通,尽快解决,敏捷开发”


开发:“。。。。”


上线前一天晚上 10 点:


开会


测试:“不行,还有 20 几个问题没有确认,这种情况我不能签字,上线了有问题谁负责。”


产品:“一些问题其实并不影响正常使用,目前主流程应该没问题,让开发把 P0 的两个问题修了之后先上线,剩下的在运营过程中逐步更新就好了,有问题让运营先收集和安抚”


开发:“上线了脏数据不好弄,会有一些账号同步的问题,而且用户等级可能还有坑”


产品:“没什么不好弄的,到时候有问题的用户让运营做个标志,接下来小步快跑修复就好了,时间不等人,明天就是上线时间, 活动上不去对老板和运营都没办法交代”


项目上线:


老板:“运营和我反馈了好多问题,你们版本上线是怎么测试的,要反思一下xxxx”


开会


产品:“我说过用户要集齐碎片和好友砍价之后才能给优惠券,等级不够的不让他们砍价成功,为什么只完成砍价的新人拿到大额优惠券?”


产品:“什么?因为账号数据绑定有 Bug ,不同渠道用户合并账号就可以满足?为什么会有这个 Bug ,测试那边没覆盖到吗?”


测试:“我不知道啊,需求文档没有说还能账号合并,而且这个功能之前没说过要限制用户等级吧?”


产品:“我出的需求肯定有,文档里肯定有,另外开发你既然知道问题,为什么不提前沟通,现在用户都消费了,这个事故你和测试 55 责,后面复盘的时候要避免这样的问题再次发生”


开发:“。。。。。”



最后


所以大家觉得是谁应该背锅?是开发的能力不够,还是测试用例的覆盖缺失?说起来,其实在此之前,我在掘金也遇到了一个 “Bug” ,比如:



文章 Markdown 里图片链接的 content-type 字段如果不符合 image/*** 规格,那么发布出来的时候链接就不会被掘金转码,所以不会有图片水印,同时直接指向了我自己的图床地址。



那么你觉得这个是 Bug 吗?明明是用户不遵循规范。但是这样不就留下漏洞了吗?



如果在文章审核通过之后,我修改图床上的图片内容,这样不就可以绕过审核在掘金展示一些「违规」内容了吗?



所以有时候一些功能的初衷是好的,但是引发的问题却又很隐蔽,那么这能怪「测试用例覆盖不到位吗」?


那么,你觉得「经过测试,为什么上线后还会那么多 Bug 」更多可能是谁的问题?


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

28岁小公司程序员,无车无房不敢结婚,要不要转行?

大家好,这里是程序员晚枫,又来分享程序员的职场故事了~ 今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成为了一名Java开发。 **最近他遇到了新的职业上的困惑,又...
继续阅读 »

大家好,这里是程序员晚枫,又来分享程序员的职场故事了~


今天分享的这位朋友叫小青,我认识他2年多了。以前从事的是土木行业,2年前找我咨询转行程序员的学习路线和职业规划后,通过自学加入了一家创业公司,成为了一名Java开发。


**最近他遇到了新的职业上的困惑,又找我聊了一下,我也没想到好的解决方法,**大家可以一起看一下~下面是沟通的核心内容。


1、他的问题


小青是中原省份省会城市的大专毕业,毕业季就去了帝都实习和工作。后来发现同学中有转行程序员的,薪资很诱惑,所以就找到我咨询如何学习和转行,现在一家帝都创业公司负责后端开发。工资1w出头。


今年已经28岁了,有一个女朋友,最近女方家里催他结婚,他自己也有结婚的意愿。但是考虑到自己人在大城市,无车无房,创业公司的工作也不稳定,以后吃住花销,结婚后养孩子的花销,再看看自己1w多的工资,女朋友做财务,一个月到手不到1w。


双方家里也都是普通家庭,给不了什么实质的资助,靠自己目前的收入根本不敢想象成家后压力有多大。


所以目前非常迷茫, 不知道自己在28岁这个年龄应该怎么办,应不应该成家?应该怎样提高收入?


虽然自己很喜欢程序员这份工作,但是感觉自己学历不好,天花板有限,程序员还能继续干下去吗?


2、几个建议


平时收到后台读者的技术问题或者转行的困惑,我都会尽力给一些详尽的回复。


但是这次听到小青的问题,说实话,我也不知道该说什么。


在28岁这种黄金年龄,想去大城市奋斗一番也是人之常情,但因为现实的生活压力,却不得不面临着选择离开大城市或者转行到自己不喜欢但是更务实的职业里去。


如果想继续留在帝都,我能想到的有以下几个办法:



  • 首先,如果想继续从事程序员工作,努力提高收入。最快的办法就是跳槽,已经工作2年多了,背一背八股文,总结一下项目经验,应该是可以跳槽到一家更好的公司了。

  • 其次,探索另一个副业收入,例如自媒体。因为我自己就是通过在各个平台开通了自媒体账号:程序员晚枫,分享自己的程序员学习经验获得粉丝,进而得到自媒体收入的。小青也可以实事求是的分享一下自己大专毕业从建筑工作转行到程序员的经验,应该也能帮助到别人。

  • 最后,努力提高学历,想继续在程序员这行卷出高收入,趁着年轻,获得一个本科或者本科以上的学历还是很有帮助的。


受限于自己的经验,我只能给出以上几个建议了。


大家还有什么更有效的建议,欢迎在评论区交流~


3、写在最后


说句题外话,很多人都觉得程序员工资高想来转行,但其实程序员和其它行业一样,高收入的只是一小部分,而且那部分人既聪明又努力。


最重要的是,高收入的那部分人里,大部分都不是转行的,而是在一个专业深耕了多年,最终获得了应有的报酬。


无意冒犯,但听完小青的经历,我依然要给大专以下,想转行程序员拿高薪的朋友提个醒:如果不是十分热爱,请务必三思~


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

内向性格的开发同学,没有合适的工作方法是不行的

一、背景 做软件开发同学的从性格上来说有两类人:外向的、内向的。 外向的人在工作中擅长交流,内向的人在工作中善于总结,两种的人都是开发团队需要的。 外向的人在工作中善于活跃团队内的气氛,逐渐走向技术管理路线,带领团队走的更远,控制开发目标与路线;内向的人更擅长...
继续阅读 »

一、背景


做软件开发同学的从性格上来说有两类人:外向的、内向的。


外向的人在工作中擅长交流,内向的人在工作中善于总结,两种的人都是开发团队需要的。


外向的人在工作中善于活跃团队内的气氛,逐渐走向技术管理路线,带领团队走的更远,控制开发目标与路线;内向的人更擅长观察,容易成为团队的定心骨,逐渐走向技术专家路线,肯研究肯花时间提高自己。



那么,在这个过程中,内向人前期的成长尤为重要,合适的工作方法和习惯也会提高在团队中的地位,而不是单纯的低头干活,本文分享下自己的经验,不一定对希望对大家有参考。


不同的性格的人,具有不同的工作方式和方法,和生活习惯,对于软件开发这个职场环境来说,内向性格不是劣势,很多人外表看着外向,其实潜意识也有很多内向性格的特征。


内向也是人的宝贵的一面,有时也是能力优势的一部分(如善于深度思考等),如果让自己掌握外向同学的行动方式,逐渐的做出改变,会更好。



二、现状


 刚毕业不久进入到职场中工作的毕业生,如果性格是外向的,那么他其实问题并不大,很多的时候,可以快速调整自己,并被其他人看到自己的工作成果,而内向性格的毕业生,如果在职场中没有主动去做某些工作和承担哪些职责,或对自己目前的工作状况没有及时调整和改变,就会造成成长缓慢,有的人会出现明明自己每天努力学习,却还是工作中那个让同时感觉能力最差的,导致经常没有分配到核心的开发工作,长此以往,消极的各种状态就出现了。


比如内向性格的毕业生在初入职场中经常会出现如下症状:


1、明知项目组的工作环境和方式存在一些不健康的因素,自己不太愿意去参与或评论


2、对开发整体流程和环节不清楚,及需求的判断有问题,需求频繁改动,代码写了被删除,自己却不敢说,或说了一次被骂以后沉默了


3、项目组缺失技术经理等全流程人员,需求自己理解和功能设计,自己却没有及时吧自己的想法与他人沟通 ,外包团队更明显


4、身边缺乏可以聊天的mentor、同事,自己感觉开发能力无法提升,却一直憋在心里,产生怀疑


5、不知道工作中如何问同事问题,才愿意帮忙解答,持续很长时间未获得同事的信任


6、有时过于逞强,不想让别人觉得自己不行,不会拒绝,实际工作量与评估有差别,导致自己延误工期。



以上的这些问题,可能不止内向性格的人会有,很多外向的人可能也会有,只是在内向性格的人身上更明显而已,如果内向性格的毕业生,明知道自己有这种情况,却不思考解决办法和改变,长时间后自我开始产生怀疑。 职场中,沟通、反馈、改变是很重要的,但是沟通不一定就是说话,反馈不一定是面对面,而改变是一直要持续去做的。 


之前看过一点得到的沟通训练营的视频教程,感觉里面有些技巧是值得大家去学习的,不仅仅是开发类型的同学。


三、经验分享


 下面我分享下,我的一些经验,可能不太对,但是希望可以帮助到看到这篇文章,深有同感的你。 


问题1:内向性格的毕业生,说的话,或者请求别人的东西,别人听不懂怎么办?


 这里先记住一件事情,在职场中,开发者要学会给不懂技术的人员,讲明白事情,要逐渐学会用生活中的事情去类比。


这个真的很重要,当你给不懂技术人讲的多以后,很多人可能都会来请教你关于某件事的理解,这个通常我们和系统的售前、需求人员、产品人员用的比较多,得学会用生活中的例子或故事去告诉他,XX能做,XX不能做的原因是什么。要坚持去练习。 


 对于请教一些人技术问题时,不管是同事也好还是网友也好,要明确自己给他的这个消息,别人是否会听懂,马上给出解决办法,还是别人看到这个问题以后,还要和我交流1小时才能知道是啥意思,这个也是很多有经验的人,不愿因帮助低级程序员的原因,这里分享下请教问题的描述模板: 


我遇到了一个问题或场景:【问题描述】,我想要实现【X功能】,但是出现了【Y现象】,我经过以下尝试:【思路细节】,但是不能解决,报错如下:【报错信息或截图】,或者我使用【关键词】百度,但是找不到答案,请问我该怎么解决或分析。


 而很多时候有经验的人,也会发现你百度的搜索词不对,这个时候,他根据你的阐述可能会告诉你怎么输入比较靠谱的搜索词来解决办法。 


问题2:评估工作计划有时过于逞强,不想让别人觉得自己不行,不会拒绝


这个真的想说,工作前期真的别逞强,没做过就是没做过,不行就是不行,别找啥接口,但是别直接和负责人说这个东西我不会(这个是很不好的,不能说不会,这是明显不相干的意思),比较合适的说法是:这个东西或概念我暂时不太清楚,没接触过过,需要一会儿或下来后我需要去研究下,然后咱们在沟通或者确定一下。 


 而很多内向性格的毕业生,缺少了这种意识,同时安排某项工作任务时,缺少对任务的分解能力和排期能力和工作后排期后的To do List梳理能力,以至于自己5天完成的任务,口头说2天就搞定了。 


 其实这种,前期mentor该给你做个示范分解的操作,或者自己主动问下,如何分解项目的需求和任务。


 而真正开发的时候,每天可能都感觉这里需要加上XXX功能,那里需要加上YYY功能,但是不知道是否需要做,这里我的建议是,把他加入到自己To do List中,然后找个时间和同事去沟通下这个想法,长此以往,同事的心里,你就是一个有想法的人,虽然不善言辞。


 主要就是这里,我们要体现自己的一个工作的对待方式,而不是一直被动接受,不拒绝,不反馈。 


问题3:明显知道产品经理、项目经理等等人员对需求的认识不足,自己闷着不反馈和说话


 很多时候,任务的返工和需求的变更,有一部分是这个原因的,在经验尚少的情况下,自己未能说出自己对这个需求的认识和怀疑,就去搞了,最后大家都不是特别的好,尤其是在产品需求设计初期,包括需求提出者也是理解不够的,这里可能有很多内容其实是你可以提供的服务,也有一些是产品在犹豫使用哪种方式实现的功能,在与你讨论后,觉得你说的又道理,而决定复用你已经有的系统。 


 很多出入职场的同学,觉得没成长也有这方面的一点原因,自己开发的功能,缺少自己设计思想和认知的影子,如果能在当前系统中体现出了自己的想法,时间久了多少成就感会有点提升的。 


要学会做自己负责的模块/功能的主人,把他们当做自己的孩子一样,主键养成主人翁的意识


问题4:项目组,当前啥都没有,文档、测试,自己也和别人一样不做改变


 这个也是目前很多公司的现状,但是不代表别人不干,你就不干,这个时候,谁主动,谁就能表现一把,同时,这也是被动让同事主动问你或咨询你的机会。


 比如没有协同的东西,那你能不能自己先装个Confluence Wiki或飞书云文档工具,自己先用起来,然后某个时机在同事眼前展示下,自己基于这个软件形成的技术思考、技术经验、技术记录等等等。


比如没有自动发布或代码质量的东西,那你能不能自己先搞个jenkins、sonarqube、checkstyle、findbug,让自己每次写完的代码,自己先搞下,然后某个时机告诉同事这个东西必须这么写怎怎么样。


 是不是有人又说了,工作没时间搞这些东西,你是不是又在扯皮呢,我只能说起码比你空闲时间自己偷偷学习公司短期内用不上的技术或长时间用不上的东西好吧,至少我能非常快速的获得1个同事的信任、2个同事的信任,从而获得团队的信任与核心工作的委派。


大部分人的想用的技术都是和公司的技术栈不搭边的,至少先把脚下的路走出来。


四、总结


 其实最近几年,发现好像很多人被卷字冲昏了头脑,每天都在想着高大尚的技术点和八股文,导致短期的这个工作没干好,还说没成长,以至于某些情况下还被认为是工作和团队中那个能力最差的,即使做了很多的努力。我想说的是,某段时间点或时期内,至少要把当前工作做好在谈论吧,这个在一些内向性格的人身上会表现的明显一些。


IT行业,很多优秀的人也是内向性格的,掌握了合适方法,会让他们成为内向性格顶端的那批优秀的人群。 


说道性格吧,即使是内向型的,可能针对十二星座还是衍生出不同的人生和结果,每个星座的也是有区别的。而在这里面最突出的我觉得是天蝎座的人群。


身为天蝎座的我,经常会想到那些和我一个星座的大佬们:


搜狐创始人张朝阳、腾讯创始人马化腾、百度创始人李彦宏、雅虎创始人杨致远、微软创始人比尔.盖茨、联想集团CEO杨元庆、推特CEO杰克.多尔西、新浪董事长曹国伟。


他们的成长也一直在激励着我。


这些经验对正在阅读文章的你有用吗,欢迎一起交流,让我们一起交流您遇到的问题。 


这篇文章是去年写的,今天增加了点内容,掘金上同步更新了一下,希望可以被更多的人看到。


如果这篇文章说道你心里了,可以点赞、分享、评论、收藏、转发哦。


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

Android如何设计一个全局可调用的ViewModel对象?

很多时候我们需要维护一个全局可用的ViewModel,因为这样可以维护全局同一份数据源,且方便使用协程绑定App的生命周期。那如何设计全局可用的ViewModel对象? 一、思路 viewModel对象是存储在ViewModelStore中的,那么如果我们创建...
继续阅读 »

很多时候我们需要维护一个全局可用的ViewModel,因为这样可以维护全局同一份数据源,且方便使用协程绑定App的生命周期。那如何设计全局可用的ViewModel对象?


一、思路


viewModel对象是存储在ViewModelStore中的,那么如果我们创建一个全局使用的ViewModelStore并且在获取viewModel对象的时候从它里面获取就可以了。


viewModel是通过ViewModelProviderget方法获取的,一般是ViewModelProvider(owner: ViewModelStoreOwner, factory: Factory).get(ViewModel::class.java)


如何将ViewModelProviderViewModelStore关联起来? 纽带就是ViewModelStoreOwner, ViewModelStoreOwner是一个接口,需要实现getViewModelStore()方法,而该方法返回的就是ViewModelStore:

public interface ViewModelStoreOwner {
/**
* Returns owned {@link ViewModelStore}
*
* @return a {@code ViewModelStore}
*/
@NonNull
ViewModelStore getViewModelStore(); //返回一个ViewModelStore
}

让某个类实现这个接口,重写方法返回我们定义的ViewModelStore就可以了。


至于上面ViewModelProvider构造方法的第二个参数Factory是什么呢?


源码中提供了二种Factory,一种是NewInstanceFactory,一种是AndroidViewModelFactory,它们的主要区别是:




  • NewInstanceFactory创建ViewModel时,会为每个Activity或Fragment创建一个新的ViewModel实例,这会导致ViewModel无法在应用程序的不同部分共享数据。(ComponentActivity源码getDefaultViewModelProviderFactory方法)




  • AndroidViewModelFactory可以访问应用程序的全局状态,并且ViewModel实例可以在整个应用程序中是共享的。




根据我们的需求,需要用的是AndroidViewModelFactory。


二、具体实现


1、方式一:可以全局添加和获取任意ViewModel


定义Application,Ktx.kt文件

import android.app.Application

lateinit var appContext: Application

fun setApplicationContext(context: Application) {
appContext = context
}

定义全局可用的ViewModelOwner实现类

object ApplicationScopeViewModelProvider : ViewModelStoreOwner {

private val eventViewModelStore: ViewModelStore = ViewModelStore()

override fun getViewModelStore(): ViewModelStore {
return eventViewModelStore
}

private val mApplicationProvider: ViewModelProvider by lazy {
ViewModelProvider(
ApplicationScopeViewModelProvider,
ViewModelProvider.AndroidViewModelFactory.getInstance(appContext)
)
}

fun <T : ViewModel> getApplicationScopeViewModel(modelClass: Class<T>): T {
return mApplicationProvider.get(modelClass)
}
}

定义一个ViewModel通过StateFlow定义发送和订阅事件的方法

class EventViewModel : ViewModel() {

private val mutableStateFlow = MutableStateFlow(0)

fun postEvent(state: Int) {
mutableStateFlow.value = state
}

fun observeEvent(scope: CoroutineScope? = null, method: (Int) -> Unit = { _ -> }) {
val eventScope = scope ?: viewModelScope
eventScope.launch {
mutableStateFlow.collect {
method.invoke(it)
}
}
}
}

定义一个调用的类



object FlowEvent {

//发送事件
fun postEvent(state: Int) {
ApplicationScopeViewModelProvider.getApplicationScopeViewModel(EventViewModel::class.java)
.postEvent(state)
}

//订阅事件
fun observeEvent(scope: CoroutineScope? = null, method: (Int) -> Unit = { _ -> }) {
ApplicationScopeViewModelProvider.getApplicationScopeViewModel(EventViewModel::class.java)
.observeEvent(scope, method)
}
}

测试代码如下:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//打印协程名称
System.setProperty("kotlinx.coroutines.debug", "on")

FlowEvent.observeEvent {
printMsg("MainActivity observeEvent before :$it")
}
//修改值
FlowEvent.postEvent(1)


FlowEvent.observeEvent {
printMsg("MainActivity observeEvent after :$it")
}

}

}

//日志
内容:MainActivity observeEvent before :0 线程:main @coroutine#1
内容:MainActivity observeEvent before :1 线程:main @coroutine#1
内容:MainActivity observeEvent after :1 线程:main @coroutine#2

2、方式二:更方便在Activity和Fragment中调用


定义Application,让BaseApplication实现ViewModelStoreOwner

//BaseApplication实现ViewModelStoreOwner接口
class BaseApplication : Application(), ViewModelStoreOwner {

private lateinit var mAppViewModelStore: ViewModelStore
private var mFactory: ViewModelProvider.Factory? = null

override fun onCreate() {
super.onCreate()
//设置全局的上下文
setApplicationContext(this)
//创建ViewModelStore
mAppViewModelStore = ViewModelStore()

}

override fun getViewModelStore(): ViewModelStore = mAppViewModelStore

/**
* 获取一个全局的ViewModel
*/
fun getAppViewModelProvider(): ViewModelProvider {
return ViewModelProvider(this, this.getAppFactory())
}

private fun getAppFactory(): ViewModelProvider.Factory {
if (mFactory == null) {
mFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(this)
}
return mFactory as ViewModelProvider.Factory
}
}

Ktx.kt文件也有变化,如下

lateinit var appContext: Application

fun setApplicationContext(context: Application) {
appContext = context
}

//定义扩展方法
inline fun <reified VM : ViewModel> Fragment.getAppViewModel(): VM {
(this.requireActivity().application as? BaseApplication).let {
if (it == null) {
throw NullPointerException("Application does not inherit from BaseApplication")
} else {
return it.getAppViewModelProvider().get(VM::class.java)
}
}
}

//定义扩展方法
inline fun <reified VM : ViewModel> AppCompatActivity.getAppViewModel(): VM {
(this.application as? BaseApplication).let {
if (it == null) {
throw NullPointerException("Application does not inherit from BaseApplication")
} else {
return it.getAppViewModelProvider().get(VM::class.java)
}
}
}

BaseActivityBaseFragment中调用上述扩展方法

abstract class BaseActivity: AppCompatActivity() {

//创建ViewModel对象
val eventViewModel: EventViewModel by lazy { getAppViewModel() }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

}
}
abstract class BaseFragment: Fragment() {

//创建ViewModel对象
val eventViewModel: EventViewModel by lazy { getAppViewModel() }

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

}
}

测试代码

class MainActivity : BaseActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//打印协程名称
System.setProperty("kotlinx.coroutines.debug", "on")

eventViewModel.observeEvent {
printMsg("MainActivity observeEvent :$it")
}

findViewById<AppCompatButton>(R.id.bt).setOnClickListener {
//点击按钮修改值
eventViewModel.postEvent(1)
//跳转到其他Activity
Intent(this, TwoActivity::class.java).also { startActivity(it) }
}
}

}
class TwoActivity : BaseActivity() {

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

eventViewModel.observeEvent {
printMsg("TwoActivity observeEvent :$it")
}
}
}

日志

内容:MainActivity observeEvent :0 线程:main @coroutine#1
内容:MainActivity observeEvent :1 线程:main @coroutine#1
内容:TwoActivity observeEvent :1 线程:main @coroutine#2

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

2023年的现代安卓开发

免责声明这是一篇来自我的观点和专业经验的文章, 考虑到了安卓开发者社区的不同意见, 也不断回顾了谷歌为安卓提供的指南.我必须明确指出, 有一些非常有趣的工具, 模式和架构, 我可能没有提到, 但这并不意味着它们不能成为开发Android应用程序的其他有趣的选择...
继续阅读 »


免责声明

这是一篇来自我的观点和专业经验的文章, 考虑到了安卓开发者社区的不同意见, 也不断回顾了谷歌为安卓提供的指南.

我必须明确指出, 有一些非常有趣的工具, 模式和架构, 我可能没有提到, 但这并不意味着它们不能成为开发Android应用程序的其他有趣的选择.

什么是Android?

Android是一个基于Linux内核的开源操作系统, 由谷歌开发.它被广泛用于各种设备, 包括智能手机, 平板电脑, 电视和智能手表.

目前, 安卓是世界上移动设备使用最多的操作系统;根据statcounter的报告, 以过去12个月为样本, 安卓的市场份额为71.96%.

接下来, 我将提到一个工具, 库, 架构, 指南和其他实用工具的清单, 我认为这些工具对在Android上构建现代应用程序非常重要.

Kotlin ❤️

0_piQN_I004o_ugTCN.webp

Kotlin是由JetBrains开发的一种编程语言.由谷歌推荐, 谷歌在2017年5月正式宣布了它(见这里的出版物).它是一种现代编程语言, 具有与Java的兼容性, 可以在JVM上运行, 这使得它在Android应用开发中的采用速度非常快.

无论你是否是安卓新手, 你都应该考虑将Kotlin作为你的首选, 不要逆水行舟🏊🏻 😎, 谷歌在2019年谷歌I/O大会上宣布了这一做法.使用Kotlin, 你将能够使用现代语言的所有功能, 包括Coroutines的强大实力和使用为Android生态系统开发的现代库.

官方kotlin文档在这里

Jetpack Compose 😍

0_kG-9BQIyUm8MblpZ.webp

Jetpack Compose是Android推荐的用于构建本地UI的现代工具包.它简化并加速了Android上的UI开发.

Jetpack Compose是Android Jetpack库的一部分, 使用Kotlin编程语言来轻松创建本地用户界面.同时, 它还与其他Android Jetpack库(如LiveData和ViewModel)集成, 使其更容易建立反应性和可维护的Android应用程序.

Jetpack Compose的一些主要特点包括:

  1. 声明式UI.
  2. 可定制的小工具.
  3. 易于与现有代码集成.
  4. 实时预览.
  5. 改进性能.

资源:

Jetpack Compose文档

Android Jetpack

0_3LHozcwxQYiKVhPG.webp

Jetpack是一套库, 帮助开发人员遵循最佳实践, 减少模板代码, 并编写在不同的Android版本和设备上一致运行的代码, 以便开发人员可以专注于他们关心的代码.

它的一些最常用的工具是:

Material Design

1_D3MK4AocfnktSnVGe4rg0g.webp

Material Design是一个由指导方针, 组件和工具组成的适应性系统, 支持用户界面设计的最佳实践.在开源代码的支持下, Material Design简化了设计师和开发人员之间的合作, 并帮助团队快速建立漂亮的产品.

Material Design网站

Material Design得到了来自谷歌的设计师和开发人员的支持, 它将使我们有一个指南来为我们的Android, Flutter和Web的UI/UX工作.

目前, Material Design的最后一个版本是3, 你可以看到更多这里.

Clean Architecture

0_KgFh38gn_lDEuoB9.webp

Clean Architecture的概念是由Robert C. Martin提出的.它的基础是通过将软件划分为不同的层来分离责任.

特点:

  1. 独立于框架.
  2. 可测试.
  3. 独立于用户界面.
  4. 独立于数据库.
  5. 独立于任何外部代理.

依赖性规则

博文Clean Architecture对依赖性规则做了很好的描述.

使得这个架构发挥作用的首要规则是依赖性规则.这个规则说, 源代码的依赖关系只能指向内部.内圈的任何东西都不能知道外圈的任何东西.特别是, 外圈中声明的东西的名字不能被内圈中的代码所提及.这包括, 函数, 类, 变量或任何其他命名的软件实体.

博文Clean Architecture

安卓系统中的Clean Architecture

  • 表示层: Activities, Fragments, ViewModels, 其他视图组件.
  • 领域层: 用例, 实体, 仓库, 其他的域组件.
  • 数据层: 存储库的实现, 映射器, DTO等.

Presentation层的架构模式

架构模式是一种更高层次的策略, 旨在帮助设计一个软件架构, 其特点是在一个可重复使用的框架内为常见的架构问题提供解决方案.架构模式类似于设计模式, 但它们的规模更大, 解决的是更多的全局性问题, 如系统的整体结构, 组件之间的关系以及数据的管理方式.

在Presentation层中, 我们有一些架构模式, 其中我想强调以下几点:

  • MVVM
  • MVI

我不想逐一解释, 因为在互联网上你可以找到太多的相关信息.

此外, 你还可以看看应用架构指南

0_QJ56TjhdXPcQweAk.webp

依赖注入

依赖注入是一种软件设计模式, 它允许客户端从外部来源获得其依赖, 而不是自己创建.它是一种在对象和其依赖关系之间实现反转控制(IoC)的技术.

模块化

模块化是一种软件设计技术, 它允许你将一个应用程序划分为独立的模块, 每个模块都有自己的功能和责任.

0_NNUw83lZ228t5yLD.webp

模块化的好处

可重复使用: 通过拥有独立的模块, 它们可以在应用程序的不同部分甚至在其他应用程序中重复使用.

严格的可见性控制: 模块使你能够轻松地控制你向你的代码库的其他部分暴露的内容.

可定制的交付Google Play的特性交付使用应用程序捆绑的高级功能, 允许你有条件地或按需交付你的应用程序的某些功能.

可扩展性: 通过独立的模块, 功能可以被添加或删除而不影响应用程序的其他部分.

易于维护: 通过将应用程序分为独立的模块, 每个模块都有自己的功能和责任, 更容易理解和维护代码.

易于测试: 通过拥有独立的模块, 它们可以被隔离测试, 这使得检测和修复错误变得容易.

架构的改进: 模块化有助于改善应用程序的架构, 使代码有更好的组织和结构.

改进协作: 通过独立的模块, 开发人员可以同时工作在应用程序的不同部分, 不受干扰.

构建时间: 一些Gradle功能, 如增量构建, 构建缓存或并行构建, 可以利用模块化来提高构建性能.

更多内容请见官方文档.

网络

序列化

在本节中, 我想提及我认为的两个重要工具: MoshiRetrofit一起广泛使用, 以及Kotlin Serialization, 这是Jetbrain的Kotlin团队的赌注.

MoshiKotlin Serialization是Kotlin和Java的两个序列化/反序列化库, 允许你将对象转换成JSON或其他序列化格式, 反之亦然.两者都提供了一个用户友好的界面, 为在移动和桌面应用程序中使用而优化.Moshi主要专注于JSON序列化, 而Kotlin Serialization则支持各种序列化格式, 包括JSON.

图像加载

要从互联网上加载图片, 有几个第三方库可以帮助你处理这个过程.图片加载库为你做了很多繁重的工作;它们既能处理缓存(这样你就不会多次下载图片), 也能处理网络逻辑以下载图片并在屏幕上显示.

Reactivity / Thread Management反应性/线程管理

1_jm3wnFbTBvURFtLlcQAYRg.webp

当我们谈论反应式编程和异步进程时, 我们的第一选择是Kotlin Coroutines;由于suspend函数Flow, 我们可以满足所有这些需求.然而, 我认为在这一节中值得强调的是RxJava的重要性, 即使在Android应用程序的开发中.对于我们这些已经在Android上工作了几年的人来说, 我们知道RxJava是一个非常强大的工具, 它有非常多的功能来处理数据流.今天我仍然认为RxJava是一个值得考虑的替代方案.

本地存储

在构建移动应用程序时, 很重要的一点是要有在本地持久化数据的能力, 比如一些会话数据或缓存数据等等.根据你的应用程序的需要, 选择合适的存储方式是很重要的.我们可以存储非结构化的数据, 如键值或结构化的数据, 如数据库.请记住, 这一点并没有提到我们可用的所有本地存储类型(如文件存储), 只是提到了允许我们保存数据的工具.

1_rILOhf6I_dtR-ircBkKvtQ.webp

建议:

测试

R8优化

R8是默认的编译器, 它将你项目的Java字节码转换为在Android平台上运行的DEX格式.它是一个帮助我们混淆和减少应用程序代码的工具, 通过缩短类和其属性的名称, 消除项目内未使用的代码和资源.想了解更多, 请查看Android文档中关于缩减, 混淆和优化你的应用程序.

1_KzoahZDnZ25lv5ydi39JSw.webp

  • 代码缩减
  • 资源缩减
  • 混淆
  • 优化

Play特性交付

Google Play的应用服务模式, 称为动态交付, 使用Android App Bundles为每个用户的设备配置生成和提供优化的APK, 因此用户只下载运行你的应用所需的代码和资源.

Android文档

0_FitxQQeB7XC7MVUq.webp

自适应布局

0_MHJwbEuvl8cXDjeq.webp

随着具有不同外形尺寸的移动设备使用的增长, 我们需要有一些工具, 使我们的Android应用程序能够适应不同类型的屏幕.这就是为什么Android为我们提供了Window Size类, 简单地说, 它是三个大的屏幕格式组, 为我们开发设计标记了关键点.这样我们就避免了考虑许多屏幕设计的复杂性, 将我们的可能性减少到三组, 即: CompatMedium 和 Expanded..

Windows Size类

1_5Tm17OKlC5n0oy6L641A5g.webp

1_Qv1nt0JJzQPzFfr2G78ulg.webp

支持不同的屏幕尺寸

我们拥有的另一个重要资源是经典布局, 这是预定义的屏幕设计, 可以用于我们的安卓应用中的大多数场景, 还向我们展示了如何将其适应大屏幕的指南.

1_XASUz4kVTK4I0dH8F5slYQ.gif

其他相关资源

Form-Factor培训

Google I/O 2022上的Form Factors

性能

0_QcvMmljmmcvCuqfN.webp

当我们为Android开发应用程序时, 我们必须确保用户体验更好, 不仅是在应用程序的开始, 而且在整个执行过程中.出于这个原因, 重要的是要有一些工具, 使我们能够对可能影响应用程序性能的情况进行预防性分析和持续监测, 因此, 这里有一个工具清单, 可以帮助你达到这个目的:

应用内更新

当你的用户在他们的设备上保持你的应用程序的更新时, 他们可以尝试新的功能, 以及从性能改进和错误修复中获益.虽然有些用户在他们的设备连接到无计量的连接时启用后台更新, 但其他用户可能需要被提醒安装更新.应用内更新是Google Play核心库的一项功能, 提示活跃用户更新你的应用.

应用内更新功能在运行Android 5.0(API级别21)或更高的设备上得到支持.此外, 应用内更新仅支持Android移动设备, Android平板电脑和Chrome OS设备.

0_m8wEQzEW1M1fwwKC.webp

应用内评论

Google Play应用内评论API让你可以提示用户提交Play Store的评分和评论, 而不需要离开你的应用或游戏, 这很方便.

一般来说, 应用内评论流程可以在你的应用的整个用户旅程中的任何时候被触发.在流程中, 用户可以使用1至5星系统对你的应用程序进行评分, 并添加一个可选的评论.一旦提交, 评论将被发送到Play Store并最终显示出来.

为了保护用户隐私和避免API被滥用, 您的应用程序应遵循关于何时请求应用内评论评论提示的设计的严格准则.

应用内评论文档

0_--T1rkTL7DEGJT9B.webp

辅助功能

0_fO3BnqLh8b-H_zLo.webp

辅助功能是软件设计和建造的一个重要特征, 除了改善他们的用户体验外, 还为有可访问性需求的人提供了使用应用程序的能力.这个概念旨在改善的一些残疾是:有视力问题的人, 色盲, 听力问题, 灵巧问题和认知障碍等等.

考虑的因素:

  • 增加文本的可见性(颜色对比, 可调整文本).
  • 使用大型, 简单的控件
  • 描述每个用户界面元素

查看辅助功能--Android文档

安全性

0_Fk42FqLrujNE0O1Z.png

安全性是我们在开发保护设备的完整性, 数据的安全性和用户的信任的应用程序时必须考虑的一个方面, 甚至是最重要的方面, 这就是为什么我在下面列出了一系列的提示, 将帮助你实现这一目的.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.myapp">
<permission android:name="my_custom_permission_name"
android:protectionLevel="signature" />
  • 不要将应用程序配置所需的密钥, 令牌或敏感数据直接放在项目库内的文件或类中.使用local.properties代替.

版本目录

Gradle提供了一种集中管理项目依赖关系的标准方式, 称为版本目录;它在7.0版本中试验性地引入, 并在7.4版本中正式发布.

优点是:

  • 对于每个目录, Gradle都会生成类型安全的访问器, 这样你就可以在IDE中用自动完成的方式轻松添加依赖关系.
  • 每个目录对一个构建的所有项目都是可见的.它是一个集中的地方, 可以声明一个依赖的版本, 并确保对该版本的改变适用于每个子项目.
  • 目录可以声明依赖包, 这是通常一起使用的"依赖包组".
  • 目录可以将依赖的组和名称与它的实际版本分开, 并使用版本参考来代替, 这样就可以在多个依赖之间共享一个版本声明.

更多请查看

Logger

Logger是一种软件工具, 用于登记有关程序执行的信息;重要事件, 错误调试信息和其他可能对诊断问题或了解程序如何工作有用的信息.记录器可以被配置为将信息写入不同的位置, 如日志文件, 控制台, 数据库, 或通过将信息发送到日志服务器.

Linter

0_T3lk9cUYryUAo6G1.webp

Linter是一种编程工具, 用于分析程序源代码, 以发现代码中的潜在问题或漏洞.这些问题可能是语法问题, 不恰当的代码风格, 缺乏文档, 安全问题等等, 它们会对代码的质量和可维护性产生影响.


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

深入理解Android-Runtime

上图是Android整体的架构,Android Runtime之于Android而言相当于心脏之于人体,是Android程序加载和运行的环境。这篇文章主要针对Android Runtime部分进行展开,探讨Android Runtime的发展以及目前现状,并介...
继续阅读 »
image.png

上图是Android整体的架构,Android Runtime之于Android而言相当于心脏之于人体,是Android程序加载和运行的环境。这篇文章主要针对Android Runtime部分进行展开,探讨Android Runtime的发展以及目前现状,并介绍应用Profile-Guided Optimization(PGO)技术对应用启动速度进行优化的可行性。转载请注明来源「申国骏」

App运行时演进

JVM

Android原生代码使用Java或者Kotlin编写,这些代码会通过javac或者kotlinc编译成.class文件,在Android之前,这些.class文件会被输入到JVM中执行。JVM可以简单分为三个子系统,分别是Class Loader、Runtime Data Area以及Execution Engine。其中Class Loader主要负责加载类、校验字节码、符号引用链接及对静态变量和静态方法分配内存并初始化。Runtime Data负责存储数据,分为方法区、堆区、栈区、程序计数器以及本地方法栈。Execution Engine负责二进制代码的执行以及垃圾回收。

image.png

Execution Engine中,会采用Interpreter或者JIT执行。其中Interpreter表示在运行的过程中对二进制代码进行解释,每次执行相同的二进制代码都进行解释比较浪费资源,因此对于热区的二进制代码会进行JIT即时编译,对二进制代码编译成机器码,这样相同的二进制代码执行时,就不用再次进行解释。

image.png

DVM(Android 2.1/2.2)

JVM是stack-based的运行环境,在移动设备中对性能和存储空间要求较高,因此Android使用了register-based的Dalvik VM。从JVM转换到DVM我们需要将.class文件转换为.dex文件,从.class转换到.dex的过程需要经过 desugar -> proguard -> dex compiler三个过程,这三个过程后来逐步变成 proguard -> D8(Desugar) 直到演变到今天只需要一步R8(D8(Desugar))。

image.png

我们主要关注Android中Runtime Engine与JVM的区别。在Android早期的版本里面,只存在Interpreter解释器,到了Android2.2版本将JIT引入,这个版本Dalvik与JVM的Runtime Engine区别不大。

image.png

ART-AOT(Android 4.4/5.0)

为了加快应用的启动速度和体验,到了Android4.4,Google提供了一个新的运行时环境ART(Android Runtime),到了Android5.0,ART替换Dalvik成为唯一的运行时环境。

image.png

ART运行时环境中,采用了AOT(Ahead-of-time)编译方式,即在应用安装的时候就将.dex提前编译成机器码,经过AOT编译之后.dex文件会生成.oat文件。这样在应用启动执行的时候,因为不需要进行解释编译,大大加快了启动速度。

image.png

然而AOT带来了以下两个问题:

  1. 应用安装时间大幅增加,由于在安装的过程中同时需要编译成机器码,应用安装时间会比较长,特别在系统升级的时候,需要对所有应用进行重新编译,出现了经典的升级等待噩梦。

image.png

  1. 应用占用过多的存储空间,由于所有应用都被编译成.oat机器码,应用所占的存储空间大大增加,使得本来并不充裕的存储空间变得雪上加霜。

进一步思考对应用全量进行编译可能是没有必要的,因为用户可能只会用到一个应用的部分常用功能,并且全量编译之后更大的机器码加载会占用IO资源。

ART-PGO(Android 7.0)

从Android7.0开始,Google重新引入了JIT的编译方式,不再对应用进行全量编译,结合AOT、JIT、Interpreter三者的优势提出了PGO(Profile-guided optimization)的编译方式。

在应用执行的过程中,先使用Interpreter直接解释,当某些二进制代码被调用次数较多时,会生成一个Profile文件记录这些方法存储起来,当二进制代码被频繁调用时,则直接进行JIT即时编译并缓存起来。

当应用处于空闲(屏幕关闭且充电)的状态时,编译守护进程会根据Profile文件进行AOT编译。

当应用重新打开时,进行过JIT和AOT编译的代码可以直接执行。

这样就可以在应用安装速度以及应用打开速度之间取得平衡。

image.png

image.png

JIT 工作流程:

image.png

ART-Cloud Profile(Android 9.0)

不过这里还是有一个问题,就是当用户第一次安装应用的时候并没有进行任何的AOT优化,通常会经过用户多次的使用才能使得启动速度得到优化。

image.png

考虑到一个应用通常会有一些用户经常使用执行的代码(例如启动部分以及用户常用功能)并且大多数时候会有先行版本用于收集Profile数据,因此Google考虑将用户生成的Profile文件上传到Google Play中,并在应用安装时同时带上这个Profile文件,在安装的过程中,会根据这个Profile对应用进行部分的AOT编译。这样当用户安装完第一次打开的时候,就能达到较快的启动速度。

image.png

image.png

Profile in cloude 需要系统应用市场支持,在国内市场使用Google Play的占比非常低,因此cloud profile的优化在国内几乎是没有作用的,不过Profile的机制提供了一个可以做启动优化的思路。早在2019年,支付宝就在秒开技术的回应的里面提到过profile-based compile的技术,参考:如何看待今日头条自媒体发布谣言称「支付宝几乎秒开是因为采用华为方舟编译器」?,这也是我们一直研究Profile技术的原因。困扰着我们的一直有两个问题,第一个问题是如何生成Profile文件,第二个问题是怎么使用生成的Profile文件。对于第一个问题的解决相对还是有思路的,因为app运行就会生成profile文件,因此我们手动运行几次app就能在文件系统中收集到这个文件,不过如何以一种较为自动化的手段收集仍然是个问题。第二个问题我们知道Profile文件最终生成的位置,因此我们可以把生成的文件放到相应的系统目录,不过大多数手机和应用都没有权限直接放置这个文件。因此Profile优化技术一直都没有落地,直到Baseline Proflie让我们看到了希望。

Baseline Profile

Baseline Profile是一套生成和使用Profile文件的工具,在2022年一月份开始进入视野,随后在Google I/O 2022随着Jetpack新变化得到广泛关注。其背景是Google Map加快了发版速度,Cloud Profle还没完全收集好就上新版,导致Cloud Proflie失效。还有一个背景是Jetpack Compose 不是系统代码,因此没有完全编译成机器码,而且Jetpack Compose库比较大,因此在Profile生成之前使用了Jetpack Compose的应用启动会产生性能问题。最后Google为了解决这些问题,创造了收集Profile的BaselineProfileRule Macrobenchmark以及使用Profile的ProfileInstaller。

使用Baseline Profile的机制可以在Android7及以上的手机上得到应用的启动加速,因为从上述知道Android7就已经开始有PGO(Profile-guided optimization)的编译方式。生成的Profile文件会打包到apk里面,并且会结合Google Play的Cloud Profile来引导AOT编译。虽然在国内基本上用不了Cloud Profile,不过Baseline Profile是可以独立于Google Play单独使用的。

image.png

在使用了Baseline Proflie之后,有道词典的启动速度从线上统计上看,冷启动时间有15%的提升。

这篇文章主要介绍了Android Runtime的演进以及对于应用启动的影响,下一篇文章我会详细介绍关于Profile&dex文件优化、Baseline Profile工具库原理,以及在实际操作上如何使用的问题,敬请大家期待一下!

收起阅读 »

什么情况下Activity会被杀掉呢?

首先一个报错来作为开篇:Caused by androidx.fragment.app.Fragment$InstantiationException Unable to instantiate fragment xxx: could not find Fra...
继续阅读 »

首先一个报错来作为开篇:

Caused by androidx.fragment.app.Fragment$InstantiationException
Unable to instantiate fragment xxx: could not find Fragment constructor

这个报错原因就是Fragment如果重载了有参的构造方法,没有实现默认无参构造方法。Activity被回收又回来尝试重新恢复Fragment的时候报错的。


那如何模拟Activity被回收呢?

可能有人知道,一个方便快捷的方法就是:打开 开发者选项 - 不保留活动,这样每次Activity回到后台都会被回收,也就可以很方便的测试这种case。


但抛开这种方式我怎么来复现这种情况呢?

这里我提出一种方式:我是不是可以打开我的App,按Home回到后台,然后疯狂的打开手机里其他的大型应用或者游戏这类的能占用大量手机内存的App,等手机内存占用大的时候是不是可以复现这种情况呢?


结论是不可以,不要混淆两个概念,系统内存不足App内存不足,两者能引起的后果也是不同的



  • 系统内存不足 -> 杀掉应用进程

  • App内存不足 -> 杀掉后台Activity


首先明确一点,Android框架对进程创建与管理进行了封装,对于APP开发者只需知道Android四大组件的使用。当Activity, Service, ContentProvider, BroadcastReceiver任一组件启动时,当其所承载的进程存在则直接使用,不存在则由框架代码自动调用startProcessLocked创建进程。所以说对APP来说进程几乎是透明的,但了解进程对于深刻理解Android系统是至关关键的。


1. 系统内存不够 -> 杀掉应用进程


1.1. LKM简介

Android底层还是基于Linux,在Linux中低内存是会有oom killer去杀掉一些进程去释放内存,而Android中的lowmemorykiller就是在此基础上做了一些调整来的。因为手机上的内存毕竟比较有限,而Android中APP在不使用之后并不是马上被杀掉,虽然上层ActivityManagerService中也有很多关于进程的调度以及杀进程的手段,但是毕竟还需要考虑手机剩余内存的实际情况,lowmemorykiller的作用就是当内存比较紧张的时候去及时杀掉一些ActivityManagerService还没来得及杀掉但是对用户来说不那么重要的进程,回收一些内存,保证手机的正常运行。


lowmemkiller中会涉及到几个重要的概念:

/sys/module/lowmemorykiller/parameters/minfree:里面是以”,”分割的一组数,每个数字代表一个内存级别

/sys/module/lowmemorykiller/parameters/adj: 对应上面的一组数,每个数组代表一个进程优先级级别


比如:

/sys/module/lowmemorykiller/parameters/minfree:18432, 23040, 27648, 32256, 55296, 80640

/sys/module/lowmemorykiller/parameters/adj: 0, 100, 200, 300, 900, 906


代表的意思是两组数一一对应:



  • 当手机内存低于80640时,就去杀掉优先级906以及以上级别的进程

  • 当内存低于55296时,就去杀掉优先级900以及以上的进程


可能每个手机的配置是不一样的,可以查看一下手头的手机,需要root。


1.2. 如何查看ADJ

如何查看进程的ADJ呢?比如我们想看QQ的adj

-> adb shell ps | grep "qq" 
UID PID PPID C STIME TTY TIME CMD
u0_a140 9456 959 2 10:03:07 ? 00:00:22 com.tencent.mobileqq
u0_a140 9987 959 1 10:03:13 ? 00:00:07 com.tencent.mobileqq:mini3
u0_a140 16347 959 0 01:32:48 ? 00:01:12 com.tencent.mobileqq:MSF
u0_a140 21475 959 0 19:47:33 ? 00:01:25 com.tencent.mobileqq:qzone

# 看到QQ的PID为 9456,这个时候打开QQ,让QQ来到前台
-> adb shell cat /proc/9456/oom_score_adj
0

# 随便打开一个其他的App
-> adb shell cat /proc/9456/oom_score_adj
700

# 再随便打开另外一个其他的App
-> adb shell cat /proc/9456/oom_score_adj
900

我们可以看到adj是在根据用户的行为不断变化的,前台的时候是0,到后台是700,回到后台后再打开其他App后是900

常见ADJ级别如下:























































































ADJ级别取值含义
NATIVE_ADJ-1000native进程
SYSTEM_ADJ-900仅指system_server进程
PERSISTENT_PROC_ADJ-800系统persistent进程
PERSISTENT_SERVICE_ADJ-700关联着系统或persistent进程
FOREGROUND_APP_ADJ0前台进程
VISIBLE_APP_ADJ100可见进程
PERCEPTIBLE_APP_ADJ200可感知进程,比如后台音乐播放
BACKUP_APP_ADJ300备份进程
HEAVY_WEIGHT_APP_ADJ400重量级进程
SERVICE_ADJ500服务进程
HOME_APP_ADJ600Home进程
PREVIOUS_APP_ADJ700上一个进程
SERVICE_B_ADJ800B List中的Service
CACHED_APP_MIN_ADJ900不可见进程的adj最小值
CACHED_APP_MAX_ADJ906不可见进程的adj最大值

So,当系统内存不足的时候会kill掉整个进程,皮之不存毛将焉附,Activity也就不在了,当然也不是开头说的那个case。


2. App内存不足 -> 杀掉后台Activity


上面分析了是直接kill掉进程的情况,一旦出现进程被kill掉,说明内存情况已经到了万劫不复的情况了,抛开内存泄漏的情况下,framework也需要一些策略来避免无内存可用的情况。下面我们来找一找fw里面回收Activity的逻辑(代码Base Android-30)。



Android Studio查看源码无法查看com.android.internal包名下的代码,双击Shift,勾选右上角Include non-prject Items.



入口定位到ActivityThreadattach方法,ActivityThread是App的入口程序,main方法中创建并调用atttach

// ActivityThread.java
private void attach(boolean system, long startSeq) {
...
// Watch for getting close to heap limit.
BinderInternal.addGcWatcher(new Runnable() {
@Override public void run() {
// mSomeActivitiesChanged在生命周期变化的时候会修改为true
if (!mSomeActivitiesChanged) {
return;
}
Runtime runtime = Runtime.getRuntime();
long dalvikMax = runtime.maxMemory();
long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
if (dalvikUsed > ((3*dalvikMax)/4)) {
mSomeActivitiesChanged = false;
try {
ActivityTaskManager.getService().releaseSomeActivities(mAppThread);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
});
...
}

这里关注BinderInternal.addGcWatcher, 下面有几个点需要理清:



  1. addGcWatcher是干嘛的,这个Runnable什么时候会被执行。

  2. 这里的maxMemory() / totalMemory() / freeMemory()都怎么理解,值有什么意义

  3. releaseSomeActivities()做了什么事情,回收Activity的逻辑是什么。


还有一个小的点是这里还用了mSomeActivitiesChanged这个标记位来标记让检测工作不会过于频繁的执行,检测到需要releaseSomeActivities后会有一个mSomeActivitiesChanged = false;赋值。而所有的mSomeActivitiesChanged = true操作都在handleStartActivity/handleResumeActivity...等等这些操作Activity声明周期的地方。控制了只有Activity声明周期变化了之后才会继续去检测是否需要回收。


2.1. GcWatcher

BinderInternal.addGcWatcher是个静态方法,相关代码如下:

public class BinderInternal {
private static final String TAG = "BinderInternal";
static WeakReference<GcWatcher> sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
static ArrayList<Runnable> sGcWatchers = new ArrayList<>();
static Runnable[] sTmpWatchers = new Runnable[1];

static final class GcWatcher {
@Override
protected void finalize() throws Throwable {
handleGc();
sLastGcTime = SystemClock.uptimeMillis();
synchronized (sGcWatchers) {
sTmpWatchers = sGcWatchers.toArray(sTmpWatchers);
}
for (int i=0; i<sTmpWatchers.length; i++) {
if (sTmpWatchers[i] != null) {
sTmpWatchers[i].run();
}
}
sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
}
}

public static void addGcWatcher(Runnable watcher) {
synchronized (sGcWatchers) {
sGcWatchers.add(watcher);
}
}
...
}

两个重要的角色:sGcWatcherssGcWatcher



  • sGcWatchers保存了调用BinderInternal.addGcWatcher后需要执行的Runnable(也就是检测是否需要kill Activity的Runnable)。

  • sGcWatcher是个装了new GcWatcher()的弱引用。


弱引用的规则是如果一个对象只有一个弱引用来引用它,那GC的时候就会回收这个对象。那很明显new出来的这个GcWatcher()只会有sGcWatcher这一个弱引用来引用它,所以每次GC都会回收这个GcWatcher对象,而回收的时候会调用这个对象的finalize()方法,finalize()方法中会将之前注册的Runnable来执行掉。
注意哈,这里并没有移除sGcWatcher中的Runnable,也就是一开始通过addGcWatcher(Runnable watcher)进来的runnable一直都在,不管执行多少次run的都是它。


为什么整个系统中addGcWatcher只有一个调用的地方,但是sGcWatchers确实一个List呢?我在自己写了这么一段代码并且想着怎么能反射搞到系统当前的BinderInternal一探究竟的时候明白了一点点,我觉着他们就是怕有人主动调用了addGcWatcher给弄了好多个GcWatcher导致系统的失效了才搞了个List吧。。


2.2. App可用的内存

上面的Runnable是如何检测当前的系统内存不足的呢?通过以下的代码

        Runtime runtime = Runtime.getRuntime();
long dalvikMax = runtime.maxMemory();
long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
if (dalvikUsed > ((3*dalvikMax)/4)) { ... }

看变量名字就知道,在使用的内存到达总内存的3/4的时候去做一些事情,这几个方法的注释如下:

    /**
* Returns the amount of free memory in the Java Virtual Machine.
* Calling the gc method may result in increasing the value returned by freeMemory.
* @return an approximation to the total amount of memory currently available for future allocated objects, measured in bytes.
*/
public native long freeMemory();

/**
* Returns the total amount of memory in the Java virtual machine.
* The value returned by this method may vary over time, depending on the host environment.
* @return the total amount of memory currently available for current and future objects, measured in bytes.
*/
public native long totalMemory();

/**
* Returns the maximum amount of memory that the Java virtual machine will attempt to use.
* If there is no inherent limit then the value java.lang.Long#MAX_VALUE will be returned.
* @return the maximum amount of memory that the virtual machine will attempt to use, measured in bytes
*/
public native long maxMemory();

首先确认每个App到底有多少内存可以用,这些Runtime的值都是谁来控制的呢?


可以使用adb shell getprop | grep "dalvik.vm.heap"命令来查看手机给每个虚拟机进程所分配的堆配置信息:

yocn@yocn ~ % adb shell getprop | grep "dalvik.vm.heap"
[dalvik.vm.heapgrowthlimit]: [256m]
[dalvik.vm.heapmaxfree]: [8m]
[dalvik.vm.heapminfree]: [512k]
[dalvik.vm.heapsize]: [512m]
[dalvik.vm.heapstartsize]: [8m]
[dalvik.vm.heaptargetutilization]: [0.75]

这些值分别是什么意思呢?



  • [dalvik.vm.heapgrowthlimit]和[dalvik.vm.heapsize]都是当前应用进程可分配内存的最大限制,一般heapgrowthlimit < heapsize,如果在Manifest中的application标签中声明android:largeHeap=“true”,APP直到heapsize才OOM,否则达到heapgrowthlimit就OOM

  • [dalvik.vm.heapstartsize] Java堆的起始大小,指定了Davlik虚拟机在启动的时候向系统申请的物理内存的大小,后面再根据需要逐渐向系统申请更多的物理内存,直到达到MAX

  • [dalvik.vm.heapminfree] 堆最小空闲值,GC后

  • [dalvik.vm.heapmaxfree] 堆最大空闲值

  • [dalvik.vm.heaptargetutilization] 堆目标利用率


比较难理解的就是heapminfree、heapmaxfree和heaptargetutilization了,按照上面的方法来说:
在满足 heapminfree < freeMemory() < heapmaxfree的情况下使得(totalMemory() - freeMemory()) / totalMemory()接近heaptargetutilization


所以一开始的代码就是当前使用的内存到达分配的内存的3/4的时候会调用releaseSomeActivities去kill掉某些Activity.


2.3. releaseSomeActivities

releaseSomeActivities在API 29前后差别很大,我们来分别看一下。


2.3.1. 基于API 28的版本的releaseSomeActivities实现如下:
// step①:ActivityManagerService.java
@Override
public void releaseSomeActivities(IApplicationThread appInt) {
synchronized(this) {
final long origId = Binder.clearCallingIdentity();
try {
ProcessRecord app = getRecordForAppLocked(appInt);
mStackSupervisor.releaseSomeActivitiesLocked(app, "low-mem");
} finally {
Binder.restoreCallingIdentity(origId);
}
}
}

// step②:ActivityStackSupervisor.java
void releaseSomeActivitiesLocked(ProcessRecord app, String reason) {
TaskRecord firstTask = null;
ArraySet<TaskRecord> tasks = null;
for (int i = 0; i < app.activities.size(); i++) {
ActivityRecord r = app.activities.get(i);
// 如果当前有正在销毁状态的Activity,Do Nothing
if (r.finishing || r.state == DESTROYING || r.state == DESTROYED) {
return;
}
// 只有Activity在可以销毁状态的时候才继续往下走
if (r.visible || !r.stopped || !r.haveState || r.state == RESUMED || r.state == PAUSING
|| r.state == PAUSED || r.state == STOPPING) {
continue;
}
if (r.task != null) {
if (firstTask == null) {
firstTask = r.task;
} else if (firstTask != r.task) {
// 2.1 只有存在两个以上的Task的时候才会到这里
if (tasks == null) {
tasks = new ArraySet<>();
tasks.add(firstTask);
}
tasks.add(r.task);
}
}
}
// 2.2 只有存在两个以上的Task的时候才不为空
if (tasks == null) {
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Didn't find two or more tasks to release");
return;
}
// If we have activities in multiple tasks that are in a position to be destroyed,
// let's iterate through the tasks and release the oldest one.
// 2.3 遍历找到ActivityStack释放最旧的那个
final int numDisplays = mActivityDisplays.size();
for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
final ArrayList<ActivityStack> stacks = mActivityDisplays.valueAt(displayNdx).mStacks;
// Step through all stacks starting from behind, to hit the oldest things first.
// 从后面开始遍历,从最旧的开始匹配
for (int stackNdx = 0; stackNdx < stacks.size(); stackNdx++) {
final ActivityStack stack = stacks.get(stackNdx);
// Try to release activities in this stack; if we manage to, we are done.
// 尝试在这个stack里面销毁这些Activities,如果成功就返回。
if (stack.releaseSomeActivitiesLocked(app, tasks, reason) > 0) {
return;
}
}
}
}

上面代码都加了注释,我们来理一理重点需要关注的点。整个流程可以观察tasks的走向



  • 2.1 & 2.2: 第一次循环会给firstTask赋值,当firstTask != r.task的时候才会给tasks赋值,后续会继续对tasks操作。所以单栈的应用不会回收,如果tasks为null,就直接return了,什么都不做

  • 2.3: 这一大段的双重for循环其实都没有第一步遍历出来的tasks参与,真正释放Activity的操作在ActivityStack中,所以尝试找到这些tasks对应的ActivityStack,让ActivityStack去销毁tasks,直到成功销毁。


继续查看releaseSomeActivitiesLocked:

// step③ ActivityStack.java
final int releaseSomeActivitiesLocked(ProcessRecord app, ArraySet<TaskRecord> tasks, String reason) {
// Iterate over tasks starting at the back (oldest) first.
int maxTasks = tasks.size() / 4;
if (maxTasks < 1) {
maxTasks = 1;
}
// 3.1 maxTasks至少为1,至少清理一个
int numReleased = 0;
for (int taskNdx = 0; taskNdx < mTaskHistory.size() && maxTasks > 0; taskNdx++) {
final TaskRecord task = mTaskHistory.get(taskNdx);
if (!tasks.contains(task)) {
continue;
}
int curNum = 0;
final ArrayList<ActivityRecord> activities = task.mActivities;
for (int actNdx = 0; actNdx < activities.size(); actNdx++) {
final ActivityRecord activity = activities.get(actNdx);
if (activity.app == app && activity.isDestroyable()) {
destroyActivityLocked(activity, true, reason);
if (activities.get(actNdx) != activity) {
// Was removed from list, back up so we don't miss the next one.
// 3.2 destroyActivityLocked后续会调用TaskRecord.removeActivity(),所以这里需要将index--
actNdx--;
}
curNum++;
}
}
if (curNum > 0) {
numReleased += curNum;
// 移除一个,继续循环需要判断 maxTasks > 0
maxTasks--;
if (mTaskHistory.get(taskNdx) != task) {
// The entire task got removed, back up so we don't miss the next one.
// 3.3 如果整个task都被移除了,这里同样需要将获取Task的index--。移除操作在上面3.1的destroyActivityLocked,移除Activity过程中,如果task为空了,会将task移除
taskNdx--;
}
}
}
return numReleased;
}



  • 3.1: ActivityStack利用maxTasks 保证,最多清理tasks.size() / 4,最少清理1个TaskRecord,同时,至少要保证保留一个前台可见TaskRecord,比如如果有两个TaskRecord,则清理先前的一个,保留前台显示的这个,如果三个,则还要看看最老的是否被有效清理,也就是是否有Activity被清理,如果有则只清理一个,保留两个,如果没有,则继续清理次老的,保留一个前台展示的,如果有四个,类似,如果有5个,则至少两个清理。一般APP中,很少有超过两个TaskRecord的。




  • 3.2: 这里清理的逻辑很清楚,for循环,如果定位到了期望的activity就清理掉,但这里这个actNdx--是为什么呢?注释说activity从list中移除了,为了能继续往下走,需要index--,但在这个方法中并没有将activity从lsit中移除的操作,那肯定是在destroyActivityLocked方法中。继续追进去可以一直追到TaskRecord.java#removeActivity(),从当前的TaskRecord的mActivities中移除了,所以需要index--。




  • 3.3: 我们弄懂了上面的actNdx--之后也就知道这里为什么要index--了,在ActivityStack.java#removeActivityFromHistoryLocked()中有

	if (lastActivity) {
removeTask(task, reason, REMOVE_TASK_MODE_DESTROYING);
}

如果task中没有activity了,需要将这个task移除掉。


以上就是基于API 28的releaseSomeActivities分析。


2.3.2. 基于29+的版本的releaseSomeActivities实现如下:
// ActivityTaskManagerService.java
@Override
public void releaseSomeActivities(IApplicationThread appInt) {
synchronized (mGlobalLock) {
final long origId = Binder.clearCallingIdentity();
try {
final WindowProcessController app = getProcessController(appInt);
app.releaseSomeActivities("low-mem");
} finally {
Binder.restoreCallingIdentity(origId);
}
}
}

// WindowProcessController.java
void releaseSomeActivities(String reason) {
// Examine all activities currently running in the process. Candidate activities that can be destroyed.
// 检查进程里所有的activity,看哪些可以被关掉
ArrayList<ActivityRecord> candidates = null;
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Trying to release some activities in " + this);
for (int i = 0; i < mActivities.size(); i++) {
final ActivityRecord r = mActivities.get(i);
// First, if we find an activity that is in the process of being destroyed,
// then we just aren't going to do anything for now; we want things to settle
// down before we try to prune more activities.
// 首先,如果我们发现一个activity正在执行关闭中,在关掉这个activity之前什么都不做
if (r.finishing || r.isState(DESTROYING, DESTROYED)) {
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Abort release; already destroying: " + r);
return;
}
// Don't consider any activities that are currently not in a state where they can be destroyed.
// 如果当前activity不在可关闭的state的时候,不做处理
if (r.mVisibleRequested || !r.stopped || !r.hasSavedState() || !r.isDestroyable()
|| r.isState(STARTED, RESUMED, PAUSING, PAUSED, STOPPING)) {
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Not releasing in-use activity: " + r);
continue;
}

if (r.getParent() != null) {
if (candidates == null) {
candidates = new ArrayList<>();
}
candidates.add(r);
}
}

if (candidates != null) {
// Sort based on z-order in hierarchy.
candidates.sort(WindowContainer::compareTo);
// Release some older activities
int maxRelease = Math.max(candidates.size(), 1);
do {
final ActivityRecord r = candidates.remove(0);
r.destroyImmediately(true /*removeFromApp*/, reason);
--maxRelease;
} while (maxRelease > 0);
}
}

新版本的releaseSomeActivities放到了ActivityTaskManagerService.java这个类中,这个类是API 29新添加的,承载部分AMS的工作。
相比API 28基于Task栈的回收Activity策略,新版本策略简单清晰, 也激进了很多。


遍历所有Activity,刨掉那些不在可销毁状态的Activity,按照Activity堆叠的顺序,也就是Z轴的顺序,从老到新销毁activity。


有兴趣的读者可以自行编写测试代码,分别在API 28和API 28+的手机上测试看一下回收策略是否跟上面分析的一致。

也可以参考我写的TestKillActivity,单栈和多栈的情况下在高于API 28和低于API 28的手机上的表现。


总结:



  1. 系统内存不足时LMK会根据内存配置项来kill掉进程释放内存

  2. kill时会按照进程的ADJ规则来kill

  3. App内存不足时由GcWatcher来决定回收Activity的时机

  4. 可以使用getprop命令来查看当前手机的JVM内存分配和OOM配置

  5. releaseSomeActivities在API 28和API 28+的差别很大,低版本会根据Task数量来决定清理哪个task的。高版本简单粗暴,遍历activity,按照z order排序,优先release掉更老的activity。

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

从前端变化看终身就业

接触前端已10年有余,从菜鸟到现在的老鸟;肚皮围度增长了,发量却减少了;头开始往下低了,发际线却往上走了!感叹时光易逝之际,不免有些伤感;风云突变的年代,总是少不了焦虑和无奈; 而立之年开始背负了各种债务,操很多心,最担心的莫过于父母一年年老去,小孩成长路上各...
继续阅读 »

接触前端已10年有余,从菜鸟到现在的老鸟;肚皮围度增长了,发量却减少了;头开始往下低了,发际线却往上走了!感叹时光易逝之际,不免有些伤感;风云突变的年代,总是少不了焦虑和无奈; 而立之年开始背负了各种债务,操很多心,最担心的莫过于父母一年年老去,小孩成长路上各种担心、顾虑;当然还有工作,一份安稳的、相对不错的收入是一家人生活的保障,也让希望增加了不少,今天跟大家一起聊聊工作吧!



终身就业还是终身职业



作为社会人、搬砖人,已经错过了最好的机会;也许是从来就不曾有过机会,已经失去了终生职业的可能(公务员、事业单位....);在国内很少有人能在一家企业供职到领社保,听说国外(比如德国、日本)有,我想说的是: 而今我们(在能力不是超强、又没有Backgroud的情况下)应该努力追求终生就业; 



企业自然想要你终生职业,但实际呢? 不管在任何一家单位,企业总是希望员工极度忠诚,可以胜任公司各种任务,把公司当自己家,有奉献精神;错吗? ---当然是对的; 如果我是老板我也会这么想;


很不幸,现实和理想总是惊为天人,面对糟糕的经济形式,活下去成为企业第一要务,而精简人员总是成为企业断臂求生的一种惯用方式,现实就是残酷如此;于个人而言,在不乐观的环境中能有一份相对稳定的工作,有个可观的收入就显得极为重要,这就需要追求终生就业的能力(不论什么行业);


互联网的发展与前端



说说自己这10多年的心路历程,可能是反面教材,如果能为你带来一些参考或借鉴或一些帮助我是很高兴的;



进入编程世界,与PHP的初恋

进入编程世界,源于羡慕!(2010年)看到同学用HTML写了一个表单,当时觉得觉得很高级,很厉害; 当时他学的是PHP(号称最好的语言),所以也就不自然的被影响、认可了PHP;


转行

从高中到大学,想从事的一直是健身相关的行业和工作,但是真正接触了发现似乎跟自己设想的那么好;当然这不是真正导致转行的原因;真正原因是朋友找我(大学专业:软件工程,但是从来没有学过)做一个网站,我却屁都不会,跟亲戚、朋友说自己还是软件相关专业毕业的;所以,为了装X,也为了对得起软件工程专业那个本本(成人自考),放弃了自己研究多年的健身,毅然报了培训班学起了PHP; 诸位有没有跟我一样的呢?


痛苦的学习

大学几年浑浑噩噩的过了,去报PHP培训时候,老师说:“学过C很容易学的,PHP先从HTML开始,很容易上手的”;交钱的时候自以为学过(上课虽然在睡觉)也或多或少听了一些,没吃过猪肉还没见过猪跑啊(实际上啥都不会),应该问题不大;想起同学说(10年刚毕业)在深圳(拿6-8K),就开始幻想上了;


最难的Table + CSS布局

学习PHP的路上,最让我难堪的竟然是HTML和CSS;保守起见,老师选择了(相比DIV + CSS)更为简单的Table(用Dreamweaver拖拽) + CSS;然而,半个月过去了,竟然连写个百度首页都写不出来;呜呼哀哉,布局难难于上青天!恰逢十一国庆节,老师留的任务就是写个百度首页,如果连百度首页都写不出来,那说明不适合走这一行;结果呢? 一周过后还是写不出来,唉,每天上课时候会想到一万个放弃,回家后每一分钟都会想到N多个放弃;后来想着,钱也交了,就多坚持一下吧,就稀里糊涂的把课程学完了(HTML CSS Javascript PHP)


找到适合自己的方向

课程结束的时候,老师给个建议: PHP感觉有难度,就好好把div + css + jQuery学好,做前端、做前端、做前端!然而,入门的我还是选择了做PHP,一年多的时间学会了从切图、写页面、写PHP、写SQL语句、搭建服务器,天呐,完全飘了(实际上还是个小白),直到偶然机会(2013年)做了前端,突然找到了码页面的灵感,这种所见即所得的搬砖工作很有感觉,哈哈哈;


其实,这里想说的是:1是坚持;2是老师的层次比我高很多,他在很早就给我指明了道路,而执着于自己的愚见(当然也不全是错,也有收获),最后还是走上了老师指导的方向!


诸位,如果你们有个好的老师、高人指导,那是极为幸福的事情,一定要珍惜!


PS: (2011年)《编写高质量代码--web前端修炼之道》这本书对我前端方面的能力提升帮助非常大; 同时也感谢作者: 阿当,在我成长道路上的一些指导和帮助;


PS: 现在互联网平台很发达,在学习视频课程、阅读技术类书籍、技术资料的时候,建议可以尝试联系一下作者(或译者);很多技术大牛还是很乐意给一些建议和指导的<致敬>;


学会听取建议、做出自己的判断

3年后,厚着脸皮请教老师接下来该学点啥能让薪资再增长一些,对未来有帮助; 老师给了一个方向: “Web GIS”,这一次照做了,掌握了一些Gis相关的基础,了解了Arcgis for javascript的常用方法等,结合近期的招聘,我觉得这算是很好的扩展了自己的选择;


PS: 建议菜鸟多向行业内的大牛请教,向身边段位高的朋友、同事多请教;


拥抱变化



互联网变化之快,技术更新之快,已经让很多人发出"学不动"的呼喊,但是我想说的是,只要你还想吃这碗饭,学不动还是要学;



yu6.png


学习&&提升

记得入行时前端面试:

- 会不会处理IE6、IE8兼容,有没有写过hack

- DIV + CSS 怎么实现div的垂直居中和水平居中,有几种实现方式

- 块级标签和行内标签的区别

- jQuery的prop方法和attr方法的区别

- Ajax有没有用过

- 会不会PS切图?gif和png的区别

- 什么是闭包?举个栗子


再后来学习了: 


- Bootstrap (不用了)

- AngularJs \ BackboneJS (不用了)

- requireJs \ seajs (不用了)

- grunt \ gulp \ bower(不用了)

- 响应式布局 (几乎不用了)


现在用的Vue \ React 也写了有好多年了,我想很快也会被新的所替代吧;

18年花了接近一万大洋购买了珠峰架构的课程,系统的学习了几个月,算是第二次技术比较大的提升吧,当然收入也相应的提高了一些;


PS: 想分享的是,很多技能可能生命周期很短,但是,身处当下我们还是要去积极学习,哪怕后来不用了,但是里面的一些思想会给我们未来某个时候带来很多帮助(懂得Bootstrap的设计思想就容易理解less\sass的使用,看到ElementUI、AntD等就一看就懂);


PS: 决定工作岗位、薪资的技术只是一部分,切勿过于迷恋于某个技术,跟随时代、拥抱变化,市场才是决定二者的最重要的因素!


运动&&养生

说点轻松又严肃的,各位看官,身体才是革命的本钱! 10年的老菜鸟目前除了颈椎不舒服(怪手机不怪写代码)外,其他的还好,论加班还能跟年轻人一战,哈哈哈! 这当然得益于过去多年的习惯:


- 经常跑步、爬山、健身;

- 很少胡吃海喝,水果吃的多,烧烤啤酒几乎不碰;

- 每天吃饭不吃饱,原则上是不饿就行;


PS: 建议大家适当的增加运动; 如果歇了很久,要启动你的小马达,要慢慢来,勿操之过急; 最重要的是坚持;


"舍"&&"得"


舍得之间品味人生,舍得之间自有方寸;然而,舍 && 得又何其的难;



- 菜鸟期间的我是舍不得花钱买课程学习的,心疼钱啊; 后来受朋友影响开始花钱去买课程,花钱找老师学习(有的技能人家凭什么告诉你呢?),发现自己的进步突然就快了很多、收获也很大(为什么工作后就不舍得花钱学习了?);


- 知识就是金钱,如今我们知道听歌、追剧都要买VIP,为什么找工作的时候不知道购买VIP呢(我好多朋友、同事上BOSS刷招聘说每天都是那几个,殊不知买了VIP后消息就多了很多,你都没购买服务,招聘APP凭什么给你最新的资讯呢?)


- 工作、学习之余一定要花点时间去陪陪家人、运动、多走一走(哪怕是带小孩玩、哪怕去公园晒晒太阳、去商场逛逛看看美女),工作、技术很重要,人生的全部还有很多;工作是个弹力球,掉下去还有机会弹起来,而身体、家庭是玻璃球,要是碎了那就。。。


踏平坎坷成大道,路就在脚下

- 说了那么多,此刻会想什么呢? 代码要一行一行的写,日子还得一天一天的过,我曾因为负债累累(每个月却只发一次工资)而着急,然急又能如何,倒不如平静以对,正如《论语》中有云: "吾尝终日不食 终夜不寝,以思,无益,不如学也"!


- 环境不友好,是不是就没有机会了? ----当然有机会,当然有路可走! 

- 路在哪? ---- 路在脚下


前端的路该怎么走


各位,我们看到招聘APP上前端岗的需求量比往年同期少很多,这个是事实;与此同时企业还是有各种各样的需求的; 2023年了,还是以过去的思维去看(劳资会Vue 、 React),无异于缘木求鱼,那一定会让你感动悲观;何不换个思路、换个角度呢?



- 大前端方向还有很大空间: Vue\React + Flutter(或类似) + 小程序,正所谓:“山重水复疑无路 柳暗花明又一村”


- 前端 + GIS(或3D),观察BOSS上关于Webgis的招聘就知道了,如果能先于大多数人掌握了GIS、3D方面的知识,那选择是不是广阔了很多,正所谓: "有心栽花花不开 无心插柳柳成荫",何必要拘泥于某一种形态呢


- 前端架构师也是一些技术深度追求者的方向


(个人在二线城市,结合自己的经历和对Boss上岗位、薪资变化的观察,提出的拙见,欢迎批评、指导)


结语


- 强哥说了:“风浪越大鱼越贵”,挑战与机遇共存,我们应当在大变化的浪潮中调整自己的帆,拥抱惊涛骇浪和变化,磨砺出终身就业的能力!



  • 不要给自己贴标签(强哥:“我就是个卖鱼的”),现在的处境不代表未来没有机会、希望(到强盛集团);


- 编码之路上是: 路漫漫其修远兮 吾将上下而求索


- 人生道路上需要另一种气度《定风波·莫听穿林打叶声》---苏轼 : "莫听穿林打叶声 何妨吟啸且徐行; 竹杖芒鞋轻胜马,谁怕? 一蓑烟雨任平生; 料峭春风吹酒醒,微冷,山头斜照却相迎; 回首向来萧瑟处,归去, 也无风雨也无晴" 。


作者:风雪中的兔子
来源:juejin.cn/post/7220800667589197885
收起阅读 »

跟我一起探索 HTTP-HTTP缓存

web
概览 HTTP 缓存会存储与请求关联的响应,并将存储的响应复用于后续请求。 可复用性有几个优点。首先,由于不需要将请求传递到源服务器,因此客户端和缓存越近,响应速度就越快。最典型的例子是浏览器本身为浏览器请求存储缓存。 此外,当响应可复用时,源服务器不需要处理...
继续阅读 »

概览


HTTP 缓存会存储与请求关联的响应,并将存储的响应复用于后续请求。


可复用性有几个优点。首先,由于不需要将请求传递到源服务器,因此客户端和缓存越近,响应速度就越快。最典型的例子是浏览器本身为浏览器请求存储缓存。


此外,当响应可复用时,源服务器不需要处理请求——因为它不需要解析和路由请求、根据 cookie 恢复会话、查询数据库以获取结果或渲染模板引擎。这减少了服务器上的负载。


缓存的正确操作对系统的稳定运行至关重要。


不同种类的缓存


HTTP Caching 标准中,有两种不同类型的缓存:私有缓存共享缓存


私有缓存


私有缓存是绑定到特定客户端的缓存——通常是浏览器缓存。由于存储的响应不与其他客户端共享,因此私有缓存可以存储该用户的个性化响应。


另一方面,如果个性化内容存储在私有缓存以外的缓存中,那么其他用户可能能够检索到这些内容——这可能会导致无意的信息泄露。


如果响应包含个性化内容并且你只想将响应存储在私有缓存中,则必须指定 private 指令。


Cache-Control: private

个性化内容通常由 cookie 控制,但 cookie 的存在并不能表明它是私有的,因此单独的 cookie 不会使响应成为私有的。


请注意,如果响应具有 Authorization 标头,则不能将其存储在私有缓存(或共享缓存,除非 Cache-Control 指定的是 public)中。


共享缓存


共享缓存位于客户端和服务器之间,可以存储能在用户之间共享的响应。共享缓存可以进一步细分为代理缓存托管缓存


代理缓存


除了访问控制的功能外,一些代理还实现了缓存以减少网络流量。这通常不由服务开发人员管理,因此必须由恰当的 HTTP 标头等控制。然而,在过去,过时的代理缓存实现——例如没有正确理解 HTTP 缓存标准的实现——经常给开发人员带来问题。


Kitchen-sink 标头如下所示,用于尝试解决不理解当前 HTTP 缓存规范指令(如 no-store)的“旧且未更新的代理缓存”的实现。


Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

然而,近年来,随着 HTTPS 变得越来越普遍,客户端/服务器通信变得加密,在许多情况下,路径中的代理缓存只能传输响应而不能充当缓存。因此,在这种情况下,无需担心甚至无法看到响应的过时代理缓存的实现。


另一方面,如果 TLS 桥接代理通过在 PC 上安装来自组织管理的 CA 证书,以中间人方式解密所有通信,并执行访问控制等,则可以查看响应的内容并将其缓存。但是,由于证书透明度(certificate transparency)在最近几年变得很普遍,并且一些浏览器只允许使用证书签署时间戳(signed certificate timestamp)颁发的证书,因此这种方法需要应用于企业策略。在这样的受控环境中,无需担心代理缓存“已过时且未更新”。


托管缓存


托管缓存由服务开发人员明确部署,以降低源服务器负载并有效地交付内容。示例包括反向代理、CDN 和 service worker 与缓存 API 的组合。


托管缓存的特性因部署的产品而异。在大多数情况下,你可以通过 Cache-Control 标头和你自己的配置文件或仪表板来控制缓存的行为。


例如,HTTP 缓存规范本质上没有定义显式删除缓存的方法——但是使用托管缓存,可以通过仪表板操作、API 调用、重新启动等实时删除已经存储的响应。这允许更主动的缓存策略。


也可以忽略标准 HTTP 缓存规范协议以支持显式操作。例如,可以指定以下内容以选择退出私有缓存或代理缓存,同时使用你自己的策略仅在托管缓存中进行缓存。


Cache-Control: no-store

例如,Varnish Cache 使用 VCL(Varnish Configuration Language,一种 DSL逻辑来处理缓存存储,而 service worker 结合缓存 API 允许你在 JavaScript 中创建该逻辑。


这意味着如果托管缓存故意忽略 no-store 指令,则无需将其视为“不符合”标准。你应该做的是,避免使用 kitchen-sink 标头,但请仔细阅读你正在使用的任何托管缓存机制的文档,并确保你选择的方式可以正确的控制缓存。


请注意,某些 CDN 提供自己的标头,这些标头仅对该 CDN 有效(例如,Surrogate-Control)。目前,正在努力定义一个 CDN-Cache-Control 标头来标准化这些标头。


缓存的类型


启发式缓存


HTTP 旨在尽可能多地缓存,因此即使没有给出 Cache-Control,如果满足某些条件,响应也会被存储和重用。这称为启发式缓存


例如,采取以下响应。此回复最后一次更新是在 1 年前。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2021 22:22:22 GMT

<!doctype html>


试探性地知道,整整一年没有更新的内容在那之后的一段时间内不会更新。因此,客户端存储此响应(尽管缺少 max-age)并重用它一段时间。复用多长时间取决于实现,但规范建议存储后大约 10%(在本例中为 0.1 年)的时间。


启发式缓存是在 Cache-Control 被广泛采用之前出现的一种解决方法,基本上所有响应都应明确指定 Cache-Control 标头。


基于 age 的缓存策略


存储的 HTTP 响应有两种状态:freshstalefresh 状态通常表示响应仍然有效,可以重复使用,而 stale 状态表示缓存的响应已经过期。


确定响应何时是 fresh 的和何时是 stale 的标准是 age。在 HTTP 中,age 是自响应生成以来经过的时间。这类似于其他缓存机制中的 TTL


以下面的示例响应为例(604800 秒是一周):


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800

<!doctype html>


存储示例响应的缓存会计算响应生成后经过的时间,并将结果用作响应的 age


对于该示例的响应,max-age 的含义如下:



  • 如果响应的 age 小于一周,则响应为 fresh

  • 如果响应的 age 超过一周,则响应为 stale


只要存储的响应保持新鲜(fresh),它将用于兑现客户端请求。


当响应存储在共享缓存中时,有必要通知客户端响应的 age。继续看示例,如果共享缓存将响应存储了一天,则共享缓存将向后续客户端请求发送以下响应。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Cache-Control: max-age=604800
Age: 86400

<!doctype html>


收到该响应的客户端会发现它在剩余的 518400 秒内是新鲜(fresh)的,这是响应的 max-ageAge 之间的差异。


Expires 或 max-age


在 HTTP/1.0 中,新鲜度过去由 Expires 标头指定。


Expires 标头使用明确的时间而不是通过指定经过的时间来指定缓存的生命周期。


Expires: Tue, 28 Feb 2022 22:22:22 GMT

但是时间格式难以解析,也发现了很多实现的错误,有可能通过故意偏移系统时钟来诱发问题;因此,在 HTTP/1.1 中,Cache-Control 采用了 max-age——用于指定经过的时间。


如果 ExpiresCache-Control: max-age 都可用,则将 max-age 定义为首选。因此,由于 HTTP/1.1 已被广泛使用,无需特地提供 Expires


Vary 响应


区分响应的方式本质上是基于它们的 URL:


使用 url 作为键


但是响应的内容并不总是相同的,即使它们具有相同的 URL。特别是在执行内容协商时,来自服务器的响应可能取决于 AcceptAccept-LanguageAccept-Encoding 请求标头的值。


例如,对于带有 Accept-Language: en 标头并已缓存的英语内容,不希望再对具有 Accept-Language: ja 请求标头的请求重用该缓存响应。在这种情况下,你可以通过在 Vary 标头的值中添加“Accept-Language”,根据语言单独缓存响应。


Vary: Accept-Language

这会导致缓存基于响应 URLAccept-Language请求标头的组合进行键控——而不是仅仅基于响应 URL。


使用 url 和语言作为键


此外,如果你基于用户代理提供内容优化(例如,响应式设计),你可能会想在 Vary 标头的值中包含“User-Agent”。但是,User-Agent 请求标头通常具有非常多的变体,这大大降低了缓存被重用的机会。因此,如果可能,请考虑一种基于特征检测而不是基于 User-Agent 请求标头来改变行为的方法。


对于使用 cookie 来防止其他人重复使用缓存的个性化内容的应用程序,你应该指定 Cache-Control: private 而不是为 Vary 指定 cookie。


验证响应


过时的响应不会立即被丢弃。HTTP 有一种机制,可以通过询问源服务器将陈旧的响应转换为新的响应。这称为验证,有时也称为重新验证


验证是通过使用包含 If-Modified-SinceIf--Match 请求标头的条件请求完成的。


If-Modified-Since


以下响应在 22:22:22 生成,max-age 为 1 小时,因此你知道它在 23:22:22 之前是新鲜的。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

<!doctype html>


到 23:22:22 时,响应会过时并且不能重用缓存。因此,下面的请求显示客户端发送带有 If-Modified-Since 请求标头的请求,以询问服务器自指定时间以来是否有任何的改变。


GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If-Modified-Since: Tue, 22 Feb 2022 22:00:00 GMT

如果内容自指定时间以来没有更改,服务器将响应 304 Not Modified


由于此响应仅表示“没有变化”,因此没有响应主体——只有一个状态码——因此传输大小非常小。


HTTP/1.1 304 Not Modified
Content-Type: text/html
Date: Tue, 22 Feb 2022 23:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
Cache-Control: max-age=3600

收到该响应后,客户端将存储的陈旧响应恢复为新鲜的,并可以在剩余的 1 小时内重复使用它。


服务器可以从操作系统的文件系统中获取修改时间,这对于提供静态文件的情况来说是比较容易做到的。但是,也存在一些问题;例如,时间格式复杂且难以解析,分布式服务器难以同步文件更新时间。


为了解决这些问题,ETag 响应标头被标准化作为替代方案。


ETag/If--Match


ETag 响应标头的值是服务器生成的任意值。服务器对于生成值没有任何限制,因此服务器可以根据他们选择的任何方式自由设置值——例如主体内容的哈希或版本号。


举个例子,如果 ETag 标头使用了 hash 值,index.html 资源的 hash 值是 deadbeef,响应如下:


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
ETag: "deadbeef"
Cache-Control: max-age=3600

<!doctype html>


如果该响应是陈旧的,则客户端获取缓存响应的 ETag 响应标头的值,并将其放入 If--Match 请求标头中,以询问服务器资源是否已被修改:


GET /index.html HTTP/1.1
Host: example.com
Accept: text/html
If--Match: "deadbeef"

如果服务器为请求的资源确定的 ETag 标头的值与请求中的 If--Match 值相同,则服务器将返回 304 Not Modified


但是,如果服务器确定请求的资源现在应该具有不同的 ETag 值,则服务器将其改为 200 OK 和资源的最新版本进行响应。



备注: 在评估如何使用 ETagLast-Modified 时,请考虑以下几点:在缓存重新验证期间,如果 ETagLast-Modified 都存在,则 ETag 优先。因此,如果你只考虑缓存,你可能会认为 Last-Modified 是不必要的。然而,Last-Modified 不仅仅对缓存有用;相反,它是一个标准的 HTTP 标头,内容管理 (CMS) 系统也使用它来显示上次修改时间,由爬虫调整爬取频率,以及用于其他各种目的。所以考虑到整个 HTTP 生态系统,最好同时提供 ETagLast-Modified



强制重新验证


如果你不希望重复使用响应,而是希望始终从服务器获取最新内容,则可以使用 no-cache 指令强制验证。


通过在响应中添加 Cache-Control: no-cache 以及 Last-ModifiedETag——如下所示——如果请求的资源已更新,客户端将收到 200 OK 响应,否则,如果请求的资源尚未更新,则会收到 304 Not Modified 响应。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Date: Tue, 22 Feb 2022 22:22:22 GMT
Last-Modified: Tue, 22 Feb 2022 22:00:00 GMT
ETag: deadbeef
Cache-Control: no-cache

<!doctype html>


max-age=0must-revalidate 的组合与 no-cache 具有相同的含义。


Cache-Control: max-age=0, must-revalidate

max-age=0 意味着响应立即过时,而 must-revalidate 意味着一旦过时就不得在没有重新验证的情况下重用它——因此,结合起来,语义似乎与 no-cache 相同。


然而,max-age=0 的使用是解决 HTTP/1.1 之前的许多实现无法处理 no-cache 这一指令——因此为了解决这个限制,max-age=0 被用作解决方法。


但是现在符合 HTTP/1.1 的服务器已经广泛部署,没有理由使用 max-age=0must-revalidate 组合——你应该只使用 no-cache


不使用缓存


no-cache 指令不会阻止响应的存储,而是阻止在没有重新验证的情况下重用响应。


如果你不希望将响应存储在任何缓存中,请使用 no-store


Cache-Control: no-store

但是,一般来说,实践中“不缓存”的原因满足以下情况:



  • 出于隐私原因,不希望特定客户以外的任何人存储响应。

  • 希望始终提供最新信息。

  • 不知道在过时的实现中会发生什么。


在这种情况下,no-store 并不总是最合适的指令。


以下部分更详细地介绍了这些情况。


不与其他用户共享


如果具有个性化内容的响应意外地对缓存的其他用户可见,那将是有问题的。


在这种情况下,使用 private 指令将导致个性化响应仅与特定客户端一起存储,而不会泄露给缓存的任何其他用户。


Cache-Control: private

在这种情况下,即使设置了 no-store,也必须设置 private


每次都提供最新的内容


no-store 指令阻止存储响应,但不会删除相同 URL 的任何已存储响应。


换句话说,如果已经为特定 URL 存储了旧响应,则返回 no-store 不会阻止旧响应被重用。


但是,no-cache 指令将强制客户端在重用任何存储的响应之前发送验证请求。


Cache-Control: no-cache

如果服务端不支持条件请求,你可以强制客户端每次都访问服务端,总是得到最新的 200 OK 响应。


兼容过时的实现


作为忽略 no-store 的过时实现的解决方法,你可能会看到使用了诸如以下内容的 kitchen-sink 标头:


Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate

推荐使用 no-cache 作为处理这种过时的实现的替代方案,如果从一开始就设置 no-cache 就没问题,因为服务器总是会收到请求。


如果你关心的是共享缓存,你可以通过添加 private 来防止意外缓存:


Cache-Control: no-cache, private

no-store 丢失了什么


你可能认为添加 no-store 是选择退出缓存的正确方法。


但是,不建议随意授予 no-store,因为你失去了 HTTP 和浏览器所拥有的许多优势,包括浏览器的后退/前进缓存。


因此,要获得 Web 平台的全部功能集的优势,最好将 no-cacheprivate 结合使用。


重新加载和强制重新加载


可以对请求和响应执行验证。


重新加载强制重新加载操作是从浏览器端执行验证的常见示例。


重新加载


为了从页面错误中恢复或更新到最新版本的资源,浏览器为用户提供了重新加载功能。


在浏览器重新加载期间发送的 HTTP 请求的简化视图如下所示:


GET / HTTP/1.1
Host: example.com
Cache-Control: max-age=0
If--Match: "deadbeef"
If-Modified-Since: Tue, 22 Feb 2022 20:20:20 GMT

请求中的 max-age=0 指令指定“重用 age 为 0 或更少的响应”——因此,中间存储的响应不会被重用。


请求通过 If--MatchIf-Modified-Since 进行验证。


该行为也在 Fetch 标准中定义,并且可以通过在缓存模式设置为 no-cache 的情况下,在 JavaScript 中调用 fetch() 来重现(注意 reload 不是这种情况下的正确模式):


// 注意:“reload”不是正常重新加载的正确模式;“no-cache”才是
fetch("/", { cache: "no-cache" });

强制重新加载


出于向后兼容的原因,浏览器在重新加载期间使用 max-age=0——因为在 HTTP/1.1 之前的许多过时的实现中不理解 no-cache。但是在这个用例中,no-cache 已被支持,并且强制重新加载是绕过缓存响应的另一种方法。


浏览器强制重新加载期间的 HTTP 请求如下所示:


GET / HTTP/1.1
Host: example.com
Pragma: no-cache
Cache-Control: no-cache

由于这不是带有 no-cache 的条件请求,因此你可以确定你会从源服务器获得 200 OK


该行为也在 Fetch 标准中定义,并且可以通过在缓存模式设置为 reload 的情况下,在 JavaScript 中调用 fetch() 来重现(注意它不是 force-reload):


// 注意:“reload”——而不是“no-cache”——是“强制重新加载”的正确模式
fetch("/", { cache: "reload" });

避免重新验证


永远不会改变的内容应该被赋予一个较长的 max-age,方法是使用缓存破坏——也就是说,在请求 URL 中包含版本号、哈希值等。


但是,当用户重新加载时,即使服务器知道内容是不可变的,也会发送重新验证请求。


为了防止这种情况,immutable 指令可用于明确指示不需要重新验证,因为内容永远不会改变。


Cache-Control: max-age=31536000, immutable

这可以防止在重新加载期间进行不必要的重新验证。


删除存储的响应


基本上没有办法删除用很长的 max-age 存储的响应。


想象一下,来自 https://example.com/ 的以下响应已被存储。


HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 1024
Cache-Control: max-age=31536000

<!doctype html>


一旦响应在服务器上过期,你可能希望覆盖该响应,但是一旦存储响应,服务器就无法执行任何操作——因为由于缓存,不再有请求到达服务器。


规范中提到的方法之一是使用不安全的方法(例如 POST)发送对同一 URL 的请求,但对于许多客户端而言,通常很难故意这样做。


还有一个 Clear-Site-Data: cache 标头和值的规范,但并非所有浏览器都支持它——即使使用它,它也只会影响浏览器缓存,而不会影响中间缓存。


因此,除非用户手动执行重新加载、强制重新加载或清除历史操作,否则应该假设任何存储的响应都将保留其 max-age 期间。


缓存减少了对服务器的访问,这意味着服务器失去了对该 URL 的控制。如果服务器不想失去对 URL 的控制——例如,在资源被频繁更新的情况下——你应该添加 no-cache,以便服务器始终接收请求并发送预期的响应。


请求折叠


共享缓存主要位于源服务器之前,旨在减少到源服务器的流量。


因此,如果多个相同的请求同时到达共享缓存,中间缓存将代表自己将单个请求转发到源,然后源可以将结果重用于所有客户端。这称为请求折叠


当请求同时到达时会发生请求折叠,因此即使响应中给出了 max-age=0no-cache,它也会被重用。


如果响应是针对特定用户个性化的,并且你不希望它在折叠中共享,则应添加 private 指令:


请求折叠


常见的缓存模式


Cache-Control 规范中有很多指令,可能很难全部理解。但是大多数网站都可以通过几种模式的组合来覆盖。


本节介绍设计缓存的常见模式。


默认设置


如上所述,缓存的默认行为(即对于没有 Cache-Control 的响应)不是简单的“不缓存”,而是根据所谓的“启发式缓存”进行隐式缓存。


为了避免这种启发式缓存,最好显式地为所有响应提供一个默认的 Cache-Control 标头。


为确保默认情况下始终传输最新版本的资源,通常的做法是让默认的 Cache-Control 值包含 no-cache


Cache-Control: no-cache

另外,如果服务实现了 cookie 或其他登录方式,并且内容是为每个用户个性化的,那么也必须提供 private,以防止与其他用户共享:


Cache-Control: no-cache, private

缓存破坏


最适合缓存的资源是静态不可变文件,其内容永远不会改变。而对于会变化的资源,通常的最佳实践是每次内容变化时都改变 URL,这样 URL 单元可以被缓存更长的时间。


例如,考虑以下 HTML:


<script src="bundle.js"></script>
<link rel="stylesheet" href="build.css" />
<body>
hello
</body>

在现代 Web 开发中,JavaScript 和 CSS 资源会随着开发的进展而频繁更新。此外,如果客户端使用的 JavaScript 和 CSS 资源的版本不同步,则显示将中断。


所以上面的 HTML 用 max-age 缓存 bundle.jsbuild.css 变得很困难。


因此,你可以使用包含基于版本号或哈希值的更改部分的 URL 来提供 JavaScript 和 CSS。一些方法如下所示。


# version in filename
bundle.v123.js

# version in query
bundle.js?v=123

# hash in filename
bundle.YsAIAAAA-QG4G6kCMAMBAAAAAAAoK.js

# hash in query
bundle.js?v=YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

由于缓存根据它们的 URL 来区分资源,因此如果在更新资源时 URL 发生变化,缓存将不会再次被重用。


<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
hello
</body>

通过这种设计,JavaScript 和 CSS 资源都可以被缓存很长时间。那么 max-age 应该设置多长时间呢?QPACK 规范提供了该问题的答案。


QPACK 是一种用于压缩 HTTP 标头字段的标准,其中定义了常用字段值表。


一些常用的缓存头值如下所示。


36 cache-control max-age=0
37 cache-control max-age=604800
38 cache-control max-age=2592000
39 cache-control no-cache
40 cache-control no-store
41 cache-control public, max-age=31536000

如果你选择其中一个编号选项,则可以在通过 HTTP3 传输时将值压缩为 1 个字节。


数字“37”、“38”和“41”分别代表一周、一个月和一年。


因为缓存会在保存新条目时删除旧条目,所以一周后存储的响应仍然存在的可能性并不高——即使 max-age 设置为 1 周。因此,在实践中,你选择哪一种并没有太大的区别。


请注意,数字“41”具有最长的 max-age(1 年),但具有 public


public 值具有使响应可存储的效果,即使存在 Authorization 标头。



备注: 只有在设置了 Authorization 标头时需要存储响应时才应使用 public 指令。否则不需要,因为只要给出了 max-age,响应就会存储在共享缓存中。



因此,如果响应是使用基本身份验证进行个性化的,public 的存在可能会导致问题。如果你对此感到担忧,你可以选择第二长的值 38(1 个月)。


# response for bundle.v123.js

# If you never personalize responses via Authorization
Cache-Control: public, max-age=31536000

# If you can't be certain
Cache-Control: max-age=2592000

验证响应


不要忘记设置 Last-ModifiedETag 标头,以便在重新加载时不必重新传输资源。对于预构建的静态文件生成这些标头很容易。


这里的 ETag 值可能是文件的哈希值。


# response for bundle.v123.js
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

此外,可以添加 immutable 以防止重新加载时验证。


组合结果如下所示。


# bundle.v123.js
200 OK HTTP/1.1
Content-Type: application/javascript
Content-Length: 1024
Cache-Control: public, max-age=31536000, immutable
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: YsAIAAAA-QG4G6kCMAMBAAAAAAAoK

缓存破坏是一种通过在内容更改时更改 URL 来使响应在很长一段时间内可缓存的技术。该技术可以应用于所有子资源,例如图像。


备注: 在评估 immutable 和 QPACK 的使用时:如果你担心 immutable 会更改 QPACK 提供的预定义值,请考虑在这种情况下,immutable 部分可以通过将 Cache-Control 值分成两行来单独编码——尽管这取决于特定 QPACK 实现使用的编码算法。


Cache-Control: public, max-age=31536000
Cache-Control: immutable

主要资源


与子资源不同,主资源不能使用缓存破坏,因为它们的 URL 不能像子资源 URL 一样被修饰。


如果存储以下 HTML 本身,即使在服务器端更新内容,也无法显示最新版本。


<script src="bundle.v123.js"></script>
<link rel="stylesheet" href="build.v123.css" />
<body>
hello
</body>

对于这种情况,no-cache 将是合适的——而不是 no-store——因为我们不想存储 HTML,而只是希望它始终是最新的。


此外,添加 Last-ModifiedETag 将允许客户端发送条件请求,如果 HTML 没有更新,则可以返回 304 Not Modified


200 OK HTTP/1.1
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE

该设置适用于非个性化 HTML,但对于使用 cookie 进行个性化的响应(例如,在登录后),不要忘记同时指定 private


200 OK HTTP/1.1
Content-Type: text/html
Content-Length: 1024
Cache-Control: no-cache, private
Last-Modified: Tue, 22 Feb 2022 20:20:20 GMT
ETag: AAPuIbAOdvAGEETbgAAAAAAABAAE
Set-Cookie: __Host-SID=AHNtAyt3fvJrUL5g5tnGwER; Secure; Path=/; HttpOnly

favicon.icomanifest.json.well-known 和无法使用缓存破坏更改 URL 的 API 端点也是如此。


大多数 Web 内容都可以通过上述两种模式的组合来覆盖。


有关托管缓存的更多信息


使用前面章节描述的方法,子资源可以通过缓存破坏来缓存很长时间,但主资源(通常是 HTML 文档)不能。


缓存主要资源很困难,因为仅使用 HTTP 缓存规范中的标准指令,在服务器上更新内容时无法主动删除缓存内容。


但是,可以通过部署托管缓存(例如 CDN 或 service worker)来实现。


例如,允许通过 API 或仪表板操作清除缓存的 CDN 将通过存储主要资源并仅在服务器上发生更新时显式清除相关缓存来实现更积极的缓存策略。


如果 service worker 可以在服务器上发生更新时删除缓存 API 中的内容,它也可以这样做。


作者:demo007x
来源:juejin.cn/post/7237022394790281271
收起阅读 »

时光里能否开出花

时光里能否开出花 昨天偶然翻出了一个老电影《何时是读书天》,有人会觉得压抑而难以理解,有人会关注里面的狗血情节。每个人的故事虽然都不同,但是归宿终究还是回到一条道上--如何让自己幸福。 似乎情感文章不应该发到技术圈,作为一名技术人,每天最多的事大概就是用充沛的...
继续阅读 »

时光里能否开出花


昨天偶然翻出了一个老电影《何时是读书天》,有人会觉得压抑而难以理解,有人会关注里面的狗血情节。每个人的故事虽然都不同,但是归宿终究还是回到一条道上--如何让自己幸福。


似乎情感文章不应该发到技术圈,作为一名技术人,每天最多的事大概就是用充沛的精力设计项目,规划项目,反思项目。但自然和世界不是人造事物,作为一个人,在生活和人生上始终要屈从于人类最原始的感情。


回顾


从毕业到现在,感觉就是一眨眼的事,但好像经历了很多。一开始每天想的是coding,怎么处理扩展性最好,一般在工作时间奴鲁推进,最喜欢下班在路上在思想中徘徊,这个节点和那个环节的对接。这样过了一段时间(说不好,感觉我的感受时间被冻结了),仿佛触碰到了瓶颈,感觉成了业务和上游数据生产者的工具,对于被拆解后的一个个任务也感觉到无趣。


还记得我在上学时,那时候会遇到各样的人,很容易遇到志同道合的人。和同学探讨小说《时间移民》,他抛出问题‘如果你不断穿越,到未来500年,一万年,这时候人类实现了意识上传,可以完成任何幻想,你会怎么办。’只是幻想在思想命题‘上帝能否造出一块它举不起的石头’这样的寻找假设漏洞的简单辩论的我,只能空洞地制造一些空泛的回答。那时候我还不明白‘人生的定义’,‘幸福的定义’,好像历史,自然,那些在长河中熠熠生辉的事物,在我的脑中只是一个个概念的字符。


那时候只是简单翻看《乔布斯传》,就以为伟大的产品,创意这些就是一个个文字意思,现在想来,真是无知的幸福。这些堵上多少人的创意才可能推进历史,流传成故事,里面浓缩了怎样的心血、放弃、执着。埋头在选择题,问答题的我,思考意识不到,这样的故事就是你以后的主线,每一次抉择、痛苦和兴奋,甚至连变成故事藏于大山的资格都没有。


当我带着只是‘识别纸质文字大脑’的我,进入工作,人生变成了一副绘声绘色,崇山峻岭的彩色图,不断打破自信而又建立信心,越发感受到我应该认识到‘人生应该是个什么样’。


触碰电影的感受


这部电影故事线就不赘述了,主暗线都很明显。虽然电影名以读书为主旨,但电影里读书的情节只在几个片段,没有主人公一脸淡然地拿着书在晨光或者夕阳下沉思,结合上下文,能看到主角在一个孤独且消沉的环境中抑制着情感捧着书,真如‘一个瘦弱的身子里藏着一个深邃的灵魂’。电影里,送牛奶,收营员这些普通的元素,现在看来有些狗血的故事线,我仍然忍不住和主角共情。不断有人问主角‘你孤独吗’,质疑和打击一直追随主角,甚至结尾你以为获得了幸福,又一瞬间落入谷底。面对‘未来怎么过’的疑问,主人公还是说着‘继续读书’。我压抑着心中的冲动,仿佛感受不同时光相同归途的念头。


我觉得最温馨的就是主人公握着书入睡那一段,此刻‘我‘很平凡(只是一个收银员和一个兼职送牛奶),又不平凡(面对满屋的书,谁能否认我的灵魂?)


我认为最精彩的就是结尾幸福的坠落,有人觉得艺术应该是给以人希望满足,但圆满结局的艺术映照在现实总是个例。尽全力抓住也不到才是常态,怎么走下去?这就是真实的艺术给我这类历经了社会法则洗礼的人的共鸣。


前后又看了两遍,仍然感觉意犹未尽,沉溺在这个我赋予自己感受的梦呓中,始终无法回转。


结语


克制,传统,礼节,在这个文化内敛的国度上,组成了我‘做一个学生’的前半段旅程,接着坐在这个不断重组的人生列车上,经历起伏旋转,在书中我迸发了情绪。


面对纷至沓来地时代变幻,无论是框架迭代还是AI变革,努力是普通人的必需品。但另一方面,心中压抑着的情感,可能需要感受另一段情感发泄。


愿你我都能像电影里的主人公,坚韧的人生里开出一朵花。


小记



看起来普通的文字,可能很多人就像看一个故事。只有讲述者才晓得说出来有多么不易。因为信息的快速传播,人们失去了对文字的敬畏,以为那不过是一个个文字,正是这样才组成了鲜活的人生。想起欧阳修每读朋友的信,总要焚香净手。是时代塑造了人,还是人推进了时代?




在感觉无光的那一段旅程,我想拾起信心,但又重新被自己打破。我碰见了《被讨厌的勇气》,真的是欲罢不能。它并不能一瞬间改变人,在面对列车跌宕,仍然会俯下身去,但我认为我心中仍然如花。推荐时刻处于自我怀疑的人读一读。不能说技术人的圈子很小,但是一般的技术人执拗于自己的逻辑,期盼着向外界伸出触角,又因为担心被拒绝,最终被局限在自我的心灵空间里。建议这类处境的人可以通过这本书了解一下阿德勒心理学,我们不是负担着‘原罪’的人。


作者:用户6970670035699
来源:juejin.cn/post/7238443713873559607

收起阅读 »

Vue+Element-UI 中 el-table 动态合并单元格 :span-method 方法

web
合并单元格 记录一下工作时遇到的 el-table 合并单元格的需求,超详细😊 el-table官方提供了合并单元格的方法与返回格式 如下: 根据叙述有了如下思路: 因为后端返回的数据非统一, 可能不是按照类别排好的😨, 所以官网的例子满足不了所有的需求...
继续阅读 »
合并单元格


记录一下工作时遇到的 el-table 合并单元格的需求,超详细😊



el-table官方提供了合并单元格的方法与返回格式 如下:

在这里插入图片描述

根据叙述有了如下思路:

因为后端返回的数据非统一, 可能不是按照类别排好的😨, 所以官网的例子满足不了所有的需求所以我们通过遍历table的数据比较前后两个元素是否相等, 来构造一个spanArr用来存放rowspan, 最后通过rowspan的值来判断colspan的值😊.


案例如下, 这是我需要处理的一个表格:

需要根据数据动态的合并

在这里插入图片描述

对应的配置数组为

在这里插入图片描述


处理数据


因为获取的数据的非统一性, 我们首先要将数据根据我们想要合并的字段进行排序分组, 这里我实现了一个简单的方法来处理数据:


// data 为 表格数据 , params 为需要合并的字段
groupBy (data, params) {
const groups = {};
data.forEach(v => {
// 获取data中的传入的params属性对应的属性值
const group = JSON.stringify(v[params]);
// 把group作为groups的key,初始化value,循环时找到相同的v[params]时不变
groups[group] = groups[group] || [];
// 将对应找到的值作为value放入数组中
groups[group].push(v);
})
// 返回处理好的二维数组
return Object.values(groups);
},

此时打印一下我们的数据console.log(this.groupBy(this.tableListData.items, 'FirstIndex'))

在这里插入图片描述

如图, 我们已经将数据分好组并合并在一个数组中啦, FirstIndex相同的在一个数组


构造控制合并的数组spanArr


这里实现了一个方法, 用来构造一个spanArr数组赋予rowspan,即控制行合并



  • 接收重构数组 let arr = []

  • 设置索引 let pos = 0

  • 控制合并的数组 this.spanArr = []


先将groupby()处理好的数据再次用arr进行处理:连接所有数组成员为一个新数组

this.groupBy(this.tableListData.items, 'FirstIndex').map(v => (arr = arr.concat(v)))


现在处理好了数据,需要赋予原数据了:this.tableListData.items = arr


但是因为我是写在getSpanArr(data, params)方法中的,已经通过形参data将 this.tableListData.items传入了这里,如果想方便封装调用的话,不用每次使用都需要再次写入 this.tableListData.items = arr

于是想到一个办法,js数组的shift()和push()是直接修改数组所占内存的方法。

所以有:


arr.map(res => {
// 每次遍历都删除data && this.tableListData.items的第一个元素
data.shift()
// 每次遍历都将arr数组元素对应push进 data && this.tableListData.items
data.push(res)
})

还需要定义一个redata存放arr要合并字段的value

const redata = arr.map(v => v[params])


reduce处理spanArr数组 ⭐⭐


使用reduce方法比较redata前后两个元素是否相等,相等的话spanArr中对应索引的元素的值+1,并且在其后增加一个0占位(防止合并过后表格数据错位),否则的话增加一个1占位,并记录当前索引,往复循环,构造一个给 rowspan 取值判断合并的数组:


  const redata = arr.map(v => v[params])
redata.reduce((old, cur, i) => {
// old 上一个元素 cur 当前元素 i 索引
if (i === 0) {
// 第一次判断先增加一个 1 占位 ,索引为0
this.spanArr.push(1)
pos = 0
} else {
if (cur === old) {
this.spanArr[pos] += 1
this.spanArr.push(0)
} else {
this.spanArr.push(1)
pos = i
}
}
return cur
}, {})

看一下现在的数据spanArr, 这里传的参数为SecondIndex, 即表格的第二列

在这里插入图片描述

数组中大于0的数字就是我们数据中要合并的这组数据的数量, 同时也是这组数据需要合并的列数,而0就是代表这列不合并, 依次遍历,实现合并所选字段这一列的最终目的 如图理解:

在这里插入图片描述


返回最终结果


最后一步啦😊根据官方给的方法把我们处理好的spanArr传给rowspan即可


spanMethod({ row, column, rowIndex, columnIndex }) {
// 第一列
if (columnIndex === 0) {
const _row = this.spanArr[rowIndex];
const _col = _row > 0 ? 1 : 0;
return {
rowspan: _row,
colspan: _col
}
}
}

效果如图!

在这里插入图片描述


完整代码


就很nice, !!最后把完整代码贴上:


// ......
mounted() {
this.getSpanArr(this.tableListData.items, 'FirstIndex');
},
methods: {
groupBy (data, params) {
const groups = {}
data.forEach(v => {
const group = JSON.stringify(v[params])
groups[group] = groups[group] || []
groups[group].push(v)
})
return Object.values(groups)
},
getSpanArr (data, params) {
let arr = []
let pos = 0
this.spanArr = []
this.groupBy(data, params).map(v => (arr = arr.concat(v)))
arr.map(res => {
data.shift()
data.push(res)
})
const redata = arr.map(v => v[params])
redata.reduce((old, cur, i) => {
if (i === 0) {
this.spanArr.push(1)
pos = 0
} else {
if (cur === old) {
this.spanArr[pos] += 1
this.spanArr.push(0)
} else {
this.spanArr.push(1)
pos = i
}
}
return cur
}, {})
},
spanMethod({ row, column, rowIndex, columnIndex }) {
if (columnIndex === 0) {
const _row = this.spanArr[rowIndex];
const _col = _row > 0 ? 1 : 0;
return {
rowspan: _row,
colspan: _col
}
}
}
}

完美! 撒花!!!🎉🎉🎉


作者:小星星__
来源:juejin.cn/post/7238478149049483301
收起阅读 »

改写el-table表格排序, 支持多列排序远程排序!!!

web
改写el-table的默认排序 提示:在el-table封装的表格基础上改写排序方法 前言 我们在做表格的时候经常会遇到表头有一个排序的icon 用来对数据进行, el-table有自己的排序方法, 如下: 在列中设置sortable属性即可实现以该列为基...
继续阅读 »

改写el-table的默认排序


提示:在el-table封装的表格基础上改写排序方法




前言


我们在做表格的时候经常会遇到表头有一个排序的icon 用来对数据进行, el-table有自己的排序方法, 如下:



在列中设置sortable属性即可实现以该列为基准的排序,接受一个Boolean,默认为false。





一、el-table支持调接口排序吗?


el-table默认的排序支持从接口获取排序的数据



sortable: 对应列是否可以排序,如果设置为 custom,则代表用户希望远程排序,需要监听 Table 的 sort-change 事件



二、el-table支持多列排序吗?


默认的排序很简单, 加一个参数就可以了, 而且会自动根据数据进行排序, 但是我们会发现, 默认的排序只支持一列进行排序, 当我们排过一列之后在点击另一列的排序图标, 之前的排序就会消失😨.


三、如何实现多列远程排序?



  1. 自己写一个组件插入到表头的位置实现排序

  2. 根据el-table已有的属性以及抛出的方法实现多列排序


如果手动封装一个组件肯定能实现, 但是比较麻烦, 所以就研究了el-table相关了一些属性和方法, 思路如下:



header-cell-class-name: 表头单元格的 className 的回调方法,也可以使用字符串为所有表头单元格设置一个固定的className



在点击表头的时候排序的列以及是升降序保存到一个数组对象ordersList里, 然后通过header-cell-class-name属性设置选中的样式.


四、核心代码


	data: {
return {
ordersList: [],
}
}
// 点击表头
handleHeaderCLick(column){
if (column.sortable !== 'custom') {
return
}
if (!column.multiOrder) {
column.multiOrder = 'descending'
} else if (column.multiOrder === 'descending') {
column.multiOrder = 'ascending'
} else {
column.multiOrder = ''
}
this.handleOrderChange(column.property, column.multiOrder)

},
handleOrderChange (orderColumn, orderState) {
let result = this.ordersList.find(e => e.orderColumn === orderColumn)
if (result) {
result.orderState = orderState
} else {
this.ordersList.push({
orderColumn: orderColumn,
orderState: orderState,
})
}
// 调接口查询,在传参的时候把ordersList进行处理成后端想要的格式(这里是把数据抛出, 外部调用组件的地方处理)
this.sendInfo(this.ordersList, 'sort-change')
},
// 上面缺点是只能通过点击表头切换排序状态,点击小三角排序不会触发,处理sort-change事件和点击表头一样
sortChange({column}) {
// 有些列不需要排序,提前返回
if (column.sortable !== 'custom') {
return
}
if (!column.multiOrder) {
column.multiOrder = 'descending'
} else if (column.multiOrder === 'descending') {
column.multiOrder = 'ascending'
} else {
column.multiOrder = ''
}
this.handleOrderChange(column.property, column.multiOrder)
},
// 设置列的排序为我们自定义的排序
handleHeaderClass({ column }) {
column.order = column.multiOrder
}

这样外部拿到的就是一个所有排序的数组, 包括prop以及当前列的排序规则(ascending/descending/null), 将其处理成正确的入参格式即可.




在这里插入图片描述

在这里插入图片描述


如此, 就实现了多列远程排序, 欢迎大家一起讨论学习😊~


作者:小星星__
来源:juejin.cn/post/7238479015723089980
收起阅读 »

如何应对核心员工提离职?

最近一年互联网行情不好,很多大厂都在裁员,但裁员并不是不要人做事了。原来你这个岗位10个人做,企业有钱赚养得起,现在企业不怎么赚钱了,只能养4个人了。那么会有六个被裁掉。这时候对企业价值最大的4个人会被留下。也许因为人更少了,反而工资还会有所提升。 越是大公司...
继续阅读 »

最近一年互联网行情不好,很多大厂都在裁员,但裁员并不是不要人做事了。原来你这个岗位10个人做,企业有钱赚养得起,现在企业不怎么赚钱了,只能养4个人了。那么会有六个被裁掉。这时候对企业价值最大的4个人会被留下。也许因为人更少了,反而工资还会有所提升。


越是大公司,人员越冗余。开掉一批人对项目进度影响其实不大。但如果掌握核心技术的员工离职,可能项目真的就黄了。


我朋友老张最近就跟我抱怨他公司技术能力最强的哥们要离职。根据他的描述,这离职的哥们属于1个打10个那种。公司里有些问题只有他能解决,公司一直想要培养个接班人,但大多数都只学到了点皮毛。现在就问我该怎么办?所以,今天就和大家聊聊这个话题。


企业最怕的就是最优秀的那批员工离职。而且这部分人只要提了离职基本上就很难挽回了。


为什么会离职?


为什么环境这么差,还有人会主动离职?因为环境再差,总有一些企业还在招人,越是对能力要求高的岗位,越难招。所以,那些真正优秀的人才是不用担心工作问题的。


马云曾说员工提离职,就两个原因,钱给少了,或者心受委屈了。其实还有一类,是工作不能给自己带来成长了。 很多人对工作追求的是成长,是获得尊重、获得一些更高级的意义。你想要挽回对方,首先得弄清楚对方离职的原因。不过这种时候,大概率已经找我下家了。


PS:绝不建议大家裸辞,除非你是准备离职后休息两月。但就算要休息两月,也记得找人把社保交了,别断社保哈。


能不能留下来?


不管对方是否找好下家,作为公司管理者还是要去做努力争取对方留下,万一对方还没有跟下家确定好,只是有意向呢?所以在对方提出离职后,不要去做正式离职沟通。先找理由拒绝,然后约个时间私下里做一次沟通。可以找个地方,边吃饭边聊天。


在这个私下沟通的场景下要表示不希望对方离开,要是遇到了什么难处可以如实说。如果是薪资这块问题,差别太大你可能拿不定主意。但如果是因为什么工作太忙,家里事情很多这类问题。完全可以拍板让对方调整工作时间。


这里我讲一个案例,以前有个朋友跟我说,公司太卷了,最近感觉身体不行了。所以准备离职换个轻松点的工作环境?我说:“啥叫轻松点的工作环境”


他说:"每天能正常下班,不用经常加班熬夜。这样我就能有更多时间睡觉,还能抽出一部分时间出来健身啥的。"


我问:“那为什么不在现在公司里就调整下工作时间呢?”


他说:"公司这么忙,我要这么做,老板估计也会开了我的"。


我反问说:“你都要离职了,还怕他辞退啊”


就这样过了一年,对方也没离职,工作也没耽误。工作时间越长并不表示工作效率越高。我真的建议很忙的人能抽出一部分时间来冥想,每天10多分钟就行,让自己脑袋空一下。你会获得很多不一样的收获。


需要我做什么?


如果对方已经下定决定要走了,那么还可以问对方,现在自己能够做点什么。如果对方希望早点走完流程,那就帮忙让流程走快点。当然,流程走快了,后面接手人肯定会有问题还会请教你,这点可以直接说。


如果对方对未来也有迷茫,有犹豫。那么作为管理者,你肯定也有着丰富的见识,在自己能力范围内的话,帮助对方去分析利弊,提供建议参考。


员工离职,特别是核心员工离职,管理者可能会有点生气。毕竟会影响到自己的项目。但把格局放大,未来就没有再合作的机会?现在很少一个人会在公司呆一辈子。人来人走是平常。现在离开,未必不会再回来。虽然现在留不住人,但我可以留心。你以真诚待人,别人也会真诚待你。


我记得在《联盟》这本书里说过,很多大公司都有前员工联盟,公司里有专门人进行管理。好处很多。


首先前员工可以为企业带来声誉和良好的社会效应;


其次前员工可以给企业引进人才;


再次前员工还能给企业带来更多新的行业信息。


甚至公司一些新的产品都可以给到前员工试用,你找其他人还需要培训,前员工就不需要。


对于我们自己来说,我们是一起战斗过的战友。不管企业有没有正式组织,都应该常联系。


有什么办法能避免核心员工提离职?


1. 上工治未病,最好的方法就是不给对方提离职的机会。


离职过的人都知道,从想离职到提出离职,中间是有很长的时间的。而且在这个过程中,总会露出一些异常的行为。比如,开始抱怨公司的某件事情;在一些以前经常发表建议的场景下,变得不爱沟通,该怎样就怎样吧;工作没精神,不再主动推进某些工作等等。反正总会有点异常。作为管理者,如果你不能提前发生这些异常,那是失职了。你可以说自己很忙,但再怎么忙,都要抽出时间来关注这些核心员工。不仅仅是工作状态,还有家庭状态。你要是真关心员工,什么问题都好解决。


2. 把核心员工跟项目收益做强绑定,增加离职成本。


管理者不仅要会画饼,还要会分饼。如何分饼决定了饼的大小。既然都说对方是核心员工了,那么就应该让对方享受到同样的待遇。公司现在没这么多钱没关系,拿出部分期权、股权总可以吧。做成了,大家一起赚钱,失败了,是我们没做好,咱也认。


3. 核心员工要离职创业?行。我投资


核心员工如果愿意舍弃这么好的收益去创业,那么作为公司为什么就不能参与进去呢?既然挡不住,那我就不挡了。我大大方方的把投资方案公布出来。你想离职创业?可以,有好项目,我们公司愿意做你的天使投资人。如果项目真的好,那么公司赚了。如果项目不好,帮对方分析弊端,也许对方就

作者:石云升
来源:juejin.cn/post/7141021800624095246
不离职了。都是好事。

收起阅读 »

项目开发过程中,成员提离职,怎么办?

之前写过一篇《如何应对核心员工提离职》反响特别好,今天做个延展篇,在项目过程中,员工突然提离职,我们有什么办法让项目按时按质的上线。 项目做多了,总会碰到这种情况。这里给大家介绍一个解决项目问题的分析方法:从问题本身、环境、问题的主体三个方面去思考解决方案。 ...
继续阅读 »

之前写过一篇《如何应对核心员工提离职》反响特别好,今天做个延展篇,在项目过程中,员工突然提离职,我们有什么办法让项目按时按质的上线。


项目做多了,总会碰到这种情况。这里给大家介绍一个解决项目问题的分析方法:从问题本身、环境、问题的主体三个方面去思考解决方案。


通常情况下,一个员工向上级提出离职,那意味着他已经下决心走了,你留得住人,留不住心。而且这段时间,最好别派太多活,他只想早点交接完早点离开。


我们试着从环境、问题本身、问题主体三个方面来思考解决方案。


  • 环境从问题发生的环境看,如果我们有一个好的氛围,好的企业文化。员工会不会突然突出离职?或者哪怕提出离职,会不会给我们更多一点时间,在离职期间仍然把事情做好?如果答案是肯定的,那么管理者可以尝试从问题发生的上游解决问题。
  • 提前安排更多的资源来做项目,预防资源不足的情况发生。比如整体预留了20%的开发时间做缓冲,或者整体安排的工作量比规划的多20%。

问题本身


从问题本身思考,员工离职导致的问题是资源不够用。

  • 新增资源,能不能快速找到替代离职员工的人?或者我们能不能使用外包方式完成需求?跟团队商量增加一些工作时间或提高工作效率?
  • 减少需求,少做一些不是很重要的需求,把离职员工的需求分给其他人。


这2个解决方案其实都有一个前提,那就是离职人员的代码是遵循编码规范的,这样接手的人才看得懂。否则,需要增加的资源会比原来规划的多很多。这种问题不能靠员工自觉,而应该要有一套制度来规范编码。


问题的主体


我们不一定能解决问题,但可以解决让问题发生的人。这样问题就不存在了。比如,既然问题出现在张三面前,那就想办法搞定张三,让他愿意按计划把项目完成。如果公司里没人能搞定这个事,这里还有另一个思路,就是想想谁能解决这个问题,找那个能解决问题的人。


从环境、问题本身、问题的主体三个维度来分析,我们得到了好几个解决方案。我们接着分析哪种方案更靠谱。


解决方案分析


方案一,从环境角度分析,让问题不发生。这种成本是最小的。但如果问题已经发生,那这个方案就没用了。


方案二,在项目规划的时候,提前安排更多资源。这招好是好,但前提是你公司有那么多资源。大部分公司都是资源不足。


方案三,新增资源,这个招人不会那么快,就算招进来了,一时半会还发挥不出多大的价值。请外包的话,其实跟招人一样,一时半会还发挥不出多大的价值,成本还更高,也不适合。至于跟团队成员商量提高工作效率或者大家加个班赶上进度,这也是一个解决方案。不过前提是团队还有精力承担这些工作。


方案四,减少需求。这个成本最小,对大部分公司其实也适用。关键是需求管理要做好,对需求的优先级有共识。


方案五,解决让问题发生的人。这个如果不是有大的积怨,也是一个比较好的方案。对整个项目来说,成本也不会很大,项目时间和质量都有保证。


项目管理里有一个生命周期概念,越是在早期发生问题,成本越小。越到后期成本越大。所以,如果让我选,我会选择方案一。但如果已经发生,那只能在四和五里选一个。


实战经验


离职是一场危机管理


让问题不发生,那么解决之道就是不让员工离职。尤其是不让核心骨干员工提离职。离职就是一场危机管理。


这里的本质的是人才是资产,我们在市场上看到很多案例,很多企业的倒闭并不是因为经营问题,而是管理层的大批量流失,资本市场也不看好管理层流失的企业。了解这点,你就能理解为什么人才是资产了。所以对企业来说,核心员工离职不亚于一场危机。


下面分享一个危机管理矩阵,这样有助于我们对危机进行分类。


横轴是一件事情发生之后,危害性有多大,我们分为大、中、小。纵轴就是这件事发生的概率,也可以分为大、中、小。然后就形成了九种不同的类型。



我自己的理解是,有精力的话,上图红色区域是需要重点关注的。如果精力有限,就关注最右边那三种离职后,危害性特别大的员工(不管概率发生的大小)。要知道给企业造成大影响的往往是那些发生概率小的,因为概率大的,你肯定有预防动作,而那些你认为不会离职的员工,突然一天找到你提离职,你连什么准备都没,这种伤害是最大的。


理论上所有岗位都应该准备好”接班人“计划,但实际上很多公司没办法做到。在一些小公司是一个萝卜一个坑,这个岗位人员离职,还得现招。这不合理,但这就是现状。


公司如何管理危机?


好,回到公司身上,公司如何管理危机?


第一,稳住关键性员工,让员工利益和公司利益进行深入绑定。


那些创造利润最大的前10~20%的员工,就应该获得50%甚至更高的收益。当然除了金钱上的激励外,还要有精神上的激励,给他目标,让他有成就感等等。


第二,有意识地培养关键岗位的接班人或者助理。


比如通过激励鼓励他们带新人、轮岗等等


第三,人员的危机管理是动态变化的,要时不时地明确团队各成员的位置。


比如大公司每年都会做人才盘点。


第四,当危机真的出现后,要有应对方案。


也就是把危机控制在可承受的范围内。比如,项目管理中的planB方案,真遇到资源不够,时间不够的情况下,我们能不能放弃一些不重要的需求?亦或者能不能先用相对简单但可用的方案?


离职管理的核心是:降低离职发生的概率和降低离职造成危害的大小。


离职沟通


如果事情已经发生了,管理者应该先通过离职沟通,释放自己的善意。我会按照如下情况跟离职员工沟通


第一,先做离职沟通,了解对方为什么离职?还有没有留下来的可能,作为管理者有什么能帮他做的?


第二,确定走的话,确认下对方期望的离职时间,然后根据公司情况,协商一个双方都能接受的离职时间点。不要因为没有交接人,就不给明确时间。


第三,征求对方意见,是否需要公布离职。然后一起商量这段时间的工作安排。比如,你会坦诚告知会减少工作量,但哪些工作是需要他继续支持的。希望他能一如既往地高效完成工作。


第四,如果还没有交接人到岗,最好在一周内安排人员到岗,可以考虑内部换岗,内招、猎聘等手段尽快让人员到岗。


第五,如果已经到离职时间,但还没有交接人,作为公司管理者,你就是最好的交接人。在正式交接工作之前,要理清楚需要哪些相关的资料,做好文档分类。如果实在对离职员工的工作不了解,可以让离职人员写一封日常工作的总结。


如果做完这些,离职员工还是消极怠工。作为管理者能做得就比较有限,可以尝试以下几个方法


1、再进行一次沟通。表明现在公司的情况,希望他给予支持。


2、看看自己能给予对方哪些帮助,先把这些落实好。比如写推荐信。另外有些公司入职的时候会做背景调查,这也是你能够帮助到他的。


3、如果你有权利,可以跟离职员工商量是否可以以兼职的方式来完成后续工作。这种方式对大家都好,他可以早点离职,你也不用担心因为时间仓促招错人。


如果做完以上这些还不行,那么就考虑减少一些需求,用更简单的方案先用着,后期做迭代。至于说让团队加班加点赶进度,这个要根据项目实际情况来定。


总结:今天给大家分享了一个简单分析问题的方法。然后重点聊了一下项目成员突然要离职,项目负责人有哪些应对方案。如果你看完有收获,欢迎留言讨论。


作者:石云升
来源:juejin.cn/post/7147319129542770702
收起阅读 »

我裸辞了

前言 时间过得真快,不知不觉已经23岁了。今年是我工作的第4年,入职现在的公司也2年了。完成了2年前定下的目标,成功弥补了自己项目上的短板以及技术深度的不足。 经过一番深思熟虑后,我决定裸辞,向下一个目标出发。 为什么离职 有位企业家曾说过,员工离职就两个原因...
继续阅读 »

前言


时间过得真快,不知不觉已经23岁了。今年是我工作的第4年,入职现在的公司也2年了。完成了2年前定下的目标,成功弥补了自己项目上的短板以及技术深度的不足。


经过一番深思熟虑后,我决定裸辞,向下一个目标出发。


为什么离职


有位企业家曾说过,员工离职就两个原因:



  • 工资没给够

  • 心受委屈了


其实,去年7月份的时候,我就萌生离职想法了,那个时候公司做了制度改革,将入职时谈的薪资进行了拆分,拆了20%出来做绩效。



  • 升职加薪按照每月的绩效考核,取出平均分,超过85分才有机会

  • 请假回来后,需要通过加班把你请假所耗费的工时补回来,否则扣绩效分

  • 上班忘记打卡直接扣全勤奖300块(全勤奖是算在入职时所谈的薪资里)

  • 下班忘记关显示器扣绩效分

  • 工位上吃东西扣绩效分


绩效制度推出后,一改再改,条件越来越苛刻。


想拿到B绩效你必须卷起来,超时超额完成任务(加班加点完成手头工作),请假(事假、病假)回来后,需要自己通过加班来把工时补回来。



制度刚推出来的时候,我就打开了boss直聘开始看机会,刷了2周,联系了很多家。最终就1家收了我简历,最后约了面试,其他的要么是送达,要么是已读未回。



到了面试当天,是前端主管面的我,问的更多的是项目相关的问题,问了一些js相关的问题以及基础的数据结构和算法,我都回答的不错。前前后后聊了1个多小时,面完后hr就过来跟我聊了下,他问了我三个问题:



  • 刚才的面试感觉怎么样?

  • 你觉得我们的产品怎么样?

  • 你还有什么想了解的吗?


这些问题回答后,他就说:那行,今天的面试就到这,后面合适的话微信通知你进行复试。他把我送出公司后,正好看到了那个面我的前端主管在等电梯,我跟他打了招呼,在电梯上简单聊了下,他问我现在还在不在职、现在的公司在哪里、住在哪里。电梯下到1楼后,他去买咖啡了,我跟他道了别。


回去后,过了两天也没有复试的消息,我就主动发微信问了hr,他给我的结果是:能力尚未达到高级🤡



还是怂了


那场面试得到结果后,我就在自己的群里跟群友聊了下这件事,他们说,这或许只是一个委婉拒绝你的理由,市场上人太多了,可能有人要价比你低。很多群友都说很难,互联网寒冬,他们也在boss直聘上联系了很多,也都是已读未回和送达,面试机会寥寥无几。


看到这么多群友说难,我心里也打起了退堂鼓,要不就再等等吧,我本本分分做事,做好自己的工作,只要能拿到自己正常的薪资就行,先不跳了,等市场好些了再跳吧。



冰冻三尺,非一日之寒


时间来到今年3月份,领导给我发放当月考核表的时候,本来能拿到B绩效(组里来了新人,是我在带,给我加了绩效分),但是,他又从其他地方扣掉了这些分。


这波操作触及到我的底线了,不能再忍让下去了。我还是走吧,想到公司制度规定了每年4月份会有一次薪资涨幅,如果我现在走的话,有点不划算,那就等5月15日拿到涨幅后的薪资再提离职吧。临走前把薪资base提升一点也挺好的。



很多群友也在劝我三思,今年的市场行情比去年还差,还是建议骑驴找马,找到了再辞职。




不过,我觉得行情差就差吧,我的条件本来就很差了(对我过往不了解的读者可以阅读我的另一篇文章:一枚19岁程序员的自学之路),再差又能差到哪里去呢,我始终坚信自己的努力总有一天会得到回报的,天无绝人之路。



提出离职


时间来到2023年5月15号,薪资没有得到增长。我的期望没有如期而至,不过无所谓了,裸辞吧。



跟领导在大会议室聊了5分钟左右吧,开场白说完后,他说:我猜到了,其实去年制度改革后,我就知道公司留不住你了。说说理由吧。


我:主要有两点吧,制度和薪资,绩效薪资这个制度本身没有问题,但是我觉得它应该是在我入职时所谈的薪资之外。


领导:行,了解了,你更希望薪资是固定的对吧。那你期望薪资能涨到多少?


我:说了我的期望


领导:我们公司的制度你也知道,薪资的涨幅很小的。那你打算什么时候走?


我:按照制度来吧,一个月后走。


领导:行,好聚好散嘛。


再然后就是说了一些我手头上还有哪些工作,虽然提了离职,但是工作上可不能懈怠摆烂之类的话,讲完后,就结束了这场谈话。


寻求内推


感谢各位读者阅读本文,如果你们公司有前端开发岗位在招聘的话,可以内推我下我🤗。


我的联系方式:



写在最后


至此,文章就分享完毕了。


我是神奇的程序员,一位前端开发工程师。


如果你对我感兴趣,请移步我的个人网站,进一步了解。



  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊

  • 本文首发于神奇的程序员公众号,未经许可禁止转载💌


作者:神奇的程序员
来源:juejin.cn/post/7233407035772616763
收起阅读 »