注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

类的布局——方法缓存hash表

回顾一下class的结构:struct objc_class : objc_object { // Class ISA; // 继承自 struct objc_object Class superclass; cache_t cache;...
继续阅读 »

回顾一下class的结构:struct objc_class : objc_object {

    // Class ISA;			// 继承自 struct objc_object
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
class_rw_t *data() const {
return bits.data();
}
};

不难发现,在objc_class结构中,有一个cache_t类型的成员变量cache

其结构如下:struct bucket_t {

private:
IMP _imp;
SEL _sel;
};
struct cache_t {
private:
uintptr_t _bucketsAndMaybeMask;
union {
struct {
uint32_t _unused;
uint16_t _occupied;
uint16_t _flags;
};
preopt_cache_t * _originalPreoptCache;
};
};
// objc-cache.m
static constexpr uintptr_t maskShift = 48;
// Additional bits after the mask which must be zero. msgSend
// takes advantage of these additional bits to construct the value
// `mask << 4` from `_maskAndBuckets` in a single instruction.
static constexpr uintptr_t maskZeroBits = 4;
// The largest mask value we can store.
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
// The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;

struct bucket_t *cache_t::buckets() const {
uintptr_t addr = _bucketsAndMaybeMask;
return (bucket_t *)(addr & bucketsMask);
}
// 哈希表已用
mask_t cache_t::occupied() const {
return _occupied;
}
// 哈希表容积
unsigned cache_t::capacity() const {
return mask() ? mask()+1 : 0;
}
mask_t cache_t::mask() const {
uintptr_t maskAndBuckets = _bucketsAndMaybeMask;
return maskAndBuckets >> maskShift;
}

通过查看cache_tbucket_t的结构以及其实现,可以很清晰的看到类的整个缓存表的内容。

接下来用lldb简单验证下:

//  CacheClass.h
@interface CacheClass : NSObject
- (void)cacheMethodA;
- (void)cacheMethodB:(NSString *)str;
- (void)cacheMethodC:(NSUInteger)integer andString:(NSString *)str;
@end

// CacheClass.m
#import "CacheClass.h"
@implementation CacheClass
- (void)cacheMethodA {
NSLog(@"%s", __func__);
}
- (void)cacheMethodB:(NSString *)str {
NSLog(@"%@===>%s", str, __func__);
}
- (void)cacheMethodC:(NSUInteger)integer andString:(NSString *)str {
NSLog(@"%lu====>%@===>%s", integer, str, __func__);
}
@end

// 调用
CacheClass *cacheObj = [[CacheClass alloc] init];
[cacheObj cacheMethodA];
NSLog(@"===================="); // 此行断点

  • 获取CacheObj的isa
(lldb) x/1gx cacheObj
0x281da0bc0: 0x0100000102c2d101
(lldb) p/x 0x0100000102c2d101 & 0x0000000ffffffff8ULL
(unsigned long long) $4 = 0x0000000102c2d100
  • 读取cache_t结构体:
(lldb) x/2gx 0x0000000102c2d100+0x10		// 此处+0x10是因为cache是在`objc_class`结构体的第16字节处开始。
0x102c2d110: 0x0001000281f850c0 0x8010000200000000 // 0x0001000281f850c0 为 _bucketsAndMaybeMask的值 0x8010000200000000 为 联合体的值
  • 获取bucket_t数组的首地址:
(lldb) p/x ((uintptr_t)1 << (48-4))-1		// 计算 bucketsMask 的值
(unsigned long) $4 = 0x00000fffffffffff
(lldb) p/x 0x0001000281f850c0 & 0x00000fffffffffff // _bucketsAndMaybeMask & bucketsMask
(long) $5 = 0x0000000281f850c0 // bucket_t数组的首地址
  • 获取bucket_t数组的count:
(lldb) p/x (0x0001000281f850c0 >> 48) + 1		// 相当于调用cache_t::capacity()函数
(long) $9 = 0x0000000000000002
  • 输出bucket_t数组的内容:
(lldb) x/4gx 0x0000000281f850c0
0x281f850c0: 0x58226481ad686ff0 0x00000001b0870410
0x281f850d0: 0x3c04798102c25d08 0x0000000102c26f97
  • 以{IMP,SEL}的结构验证:
(lldb) p (char *)0x00000001b0870410
(char *) $10 = 0x00000001b0870410 "init"
(lldb) dis -a 0x58226481ad686ff0
libobjc.A.dylib`-[NSObject init]:
0x1ad686ff0 <+0>: ret
0x1ad686ff4 <+4>: udf #0x0
0x1ad686ff8 <+8>: udf #0x0
(lldb) p (char *)0x0000000102c26f97
(char *) $11 = 0x0000000102c26f97 "cacheMethodA"
(lldb) dis -a 0x3c04798102c25d08
MethodCacheDemo`-[CacheClass cacheMethodA]:
0x102c25d08 <+0>: sub sp, sp, #0x30
0x102c25d0c <+4>: stp x29, x30, [sp, #0x20]
0x102c25d10 <+8>: add x29, sp, #0x20
0x102c25d14 <+12>: stur x0, [x29, #-0x8]
0x102c25d18 <+16>: str x1, [sp, #0x10]
0x102c25d1c <+20>: mov x9, sp
0x102c25d20 <+24>: adrp x8, 2
0x102c25d24 <+28>: add x8, x8, #0x363 ; "-[CacheClass cacheMethodA]"
0x102c25d28 <+32>: str x8, [x9]
0x102c25d2c <+36>: adrp x0, 3
0x102c25d30 <+40>: add x0, x0, #0x90 ; @"%s"
0x102c25d34 <+44>: bl 0x102c26344 ; symbol stub for: NSLog
0x102c25d38 <+48>: ldp x29, x30, [sp, #0x20]
0x102c25d3c <+52>: add sp, sp, #0x30
0x102c25d40 <+56>: ret

从输出结果可以看出,缓存数组位置是正确的,类CacheClass缓存了两个方法,分别为:-[NSObject init]-[CacheClass cacheMethodA]

用代码获取:

#if __arm64__
#if TARGET_OS_EXCLAVEKIT
#define ISA_MASK 0xfffffffffffffff8ULL
#elif __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#define ISA_MASK 0x007ffffffffffff8ULL
#else
#define ISA_MASK 0x0000000ffffffff8ULL
#endif
#endif

uintptr_t _isaForObject(NSObject *obj) {
if (obj == nil) return 0;
struct _object {
BytePtr isa;
};
struct _object *obj_ptr = (struct _object *)(__bridge void *)obj;
return (uintptr_t)((uintptr_t)obj_ptr->isa & ISA_MASK);
}

typedef uint32_t mask_t;
//cache_t源码模仿
const uintptr_t maskShift = 48;
const uintptr_t maskZeroBits = 4;
const uintptr_t maxMask = (((uintptr_t)1 << (64 - maskShift))-1);
const uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1; //0x100000000000-1 = 0xfffffffffff
struct bucket_t {
IMP _imp;
SEL _sel;
};
struct cache_t {
uintptr_t _bucketsAndMaybeMask; // 8
union {
struct {
uint32_t _unused;
uint16_t _occupied;
uint16_t _flags;
};
uintptr_t _originalPreoptCache; // 8
};
};
struct bucket_t *buckets(struct cache_t *cache) {
return (struct bucket_t *)(cache->_bucketsAndMaybeMask & bucketsMask);
}
uint32_t mask(struct cache_t *cache) {
return (uint32_t)(cache->_bucketsAndMaybeMask >> maskShift);
}
uint32_t capacity(struct cache_t *cache) {
return mask(cache) ? mask(cache)+1 : 0;
}
mask_t occupied(struct cache_t *cache) {
return cache->_occupied;
}

void _printMethodCaches(id obj) {
printf("============================\n");
uintptr_t isa = _isaForObject(obj);

// 读取cache结构体
struct cache_t *cache = (struct cache_t *)(isa + 0x10);

// 读取bucket_t
struct bucket_t *bucket_array = buckets(cache);

// 获取count
uint32_t count = capacity(cache);

// 获取已缓存数
uint32_t occupied_count = occupied(cache);

printf("哈希表容积:%u\t\t\t已缓存方法数:%u\n",count, occupied_count);

// 输出缓存内容
for (int c = 0; c < count; c++) {
struct bucket_t *bucket = (bucket_array + c);
printf("imp->sel:0x%lx->%s\n", (intptr_t)bucket->_imp, sel_getName(bucket->_sel));
}
printf("============================\n");
}

// 调用
CacheClass *cacheObj = [[CacheClass alloc] init];
[cacheObj cacheMethodA];
_printMethodCaches(cacheObj);

// 输出
============================
哈希表容积:2 已缓存方法数:2
imp->sel:0x4f5fd201ad686ff0->init
imp->sel:0xe9344e810494dc50->cacheMethodA
============================

需要注意的是,缓存哈希表有一个扩容的过程,当缓存方法超过了哈希表容积时,就会触发扩容,此时,之前的缓存并不会被复制到新的hash表中,而是重新还是缓存!

例如上面的调用修改为如下:

    CacheClass *cacheObj = [[CacheClass alloc] init];
[cacheObj cacheMethodA];
[cacheObj cacheMethodB:@"B"];

_printMethodCaches(cacheObj);

则输出为:

============================
哈希表容积:4 已缓存方法数:1
imp->sel:0x0-><null selector>
imp->sel:0x0-><null selector>
imp->sel:0xf85bf08100269c6c->cacheMethodB:
imp->sel:0x0-><null selector>
============================

从上面的输出可以看出,调用方法-[CacheClass cacheMethodB:]时,触发了缓存表扩容;扩容过程中,它舍弃了原缓存表中的方法,仅缓存了当前方法(-[CacheClass cacheMethodB:])。


收起阅读 »

一个前端实习生在美团的成长小结

一些前言 这是第二篇有关实习的小结,上一篇是在蔚来离职的时候写的。 也就是这一篇:一个前端实习生在蔚来的成长小结 离职的时候我在朋友圈是这样写的: Delay Version..... 半年的实习生活结束了 主题词可能是:“成长,反思,感恩” 这半年里有太多...
继续阅读 »

一些前言


这是第二篇有关实习的小结,上一篇是在蔚来离职的时候写的。


也就是这一篇:一个前端实习生在蔚来的成长小结


离职的时候我在朋友圈是这样写的:



Delay Version.....


半年的实习生活结束了


主题词可能是:“成长,反思,感恩”


这半年里有太多的第一次


第一次出省实习,第一次根据 prd 写代码,第一次上线,第一次领工资,第一次一个人在外生活那么长时间~


这半年里有太多的感动


感谢直线经理对我的照顾,从租房买药到个人成长,感谢组内同事耐心地解答我一个又一个愚蠢的问题(doge


这半年里有不少的思考


会感觉自己有些偏航,与自己所想的成长状态有些出入,许愿自己能在接下来的半年里探索出满意的生活状态。


以上,有缘再见啦~



我个人认为在 2023 的后半年里,我真的探索到了自己喜欢的生活方式。


通过亲人,朋友,过往的经历,所看的事物,慢慢构建起了自己的一套价值体系,能够圆融自洽,消化外界的冲击,同时还能通过和朋友定期的见面得到支持。


整理好自己之后,我们来聊聊在美团的故事。


蔚来与美团的区别


image.png


从蔚来来到美团之后,给我的感受就是:“这也太不一样了吧。”


考勤与强度


在蔚来那边,如果不是赶 C 端的业务的话,我平时走的都蛮早的,一般 10-6 这个时间段就 ok 了。在美团这边就是正常 10-8,因为要等到晚上八点钟拿餐补再下班(悲


基础建设


第二个特别明显的点就是美团的基础建设方面是真的做到了跨部门跨团队,很多东西可以直接复用,不需要重复造轮子,包括 CI/CD,组件库,还有一些基础的解决方案;


然后美团的文档真的超级超级多而全,我从这里面学到了不少东西。


第二段实习-美团基础研发平台~


9461697267534_.pic.jpg


接下来就来到了这篇文章的正题,聊聊在美团的一些收获与成长。


一些感谢


我觉得自己很幸运,又遇到了一个很好的组,组里面的人完全不 push,而且会很耐心的和你去讲你并不熟悉的点,不会因为你不会就说你怎么怎么样。


感谢 mentor 和 leader,还有一起实习一起约饭的小伙伴们,这四个月过的真的很开心。


个人成长


技术方面



  1. 因为我进去的那个组已经存在了很久了,需求很多都是在慢慢推进,技术驱动可能更加明显,就有好多好多优化的场景,在理解业务的基础之上去做一些相关的优化。



我接触的主要有两种优化方式:



  • 第一种是直接更改技术的实现方案,通过比对几种实现的优劣,再结合业务场景进行选择。

  • 第二种则是在代码层面的优化,诸如 v-if/v-show 这种,如果做得好了,优化的效果其实是相当可观的。





  1. 除了技术本身,我可能对“数据”这种业务场景更加了解了,这些了解包括了对于一些业务术语的理解,还包括了对于 echarts以及虚拟列表这些常见工具与优化的使用。




  2. 当然啦,还成功的在组内进行了一次技术分享。主要是围绕着我们组内经常使用的“装饰器”,我的分享囊括了基础的用法,再到三种装饰器的原理,还有在VueReact这些框架中的应用。




软技能方面




  1. 来了美团之后养成了一个习惯,基本我每开一个需求,我就会开一个文档,记录排期、技术难点、开发过程中不知道的点。这种习惯让我在整个开发的过程中效率提升了好多,我可以随时去查阅自己已经做过的部分,了解当时的想法。




  2. 第二点呢可能是在写代码的管理方面,学了一些对于端口的判断(lsof -i 8080)之类的,除此之外还有



    • 写代码的时候,没写完的部分可以列一个 TODOcommit 之前检查一下。

    • yarn start:test 之类的环境配置




  3. 还组织了一次 60-70 人的大团建,虽然是帮着 leader 去直接和团建机构做的对接,但是这个过程中也感受到了乙方的耐心和卑微,还有“做生意就是很难”的无奈。




生活


虽然美团这边实习 💰 给的确实不多,但是中秋的月饼和实习生的活动我算是都赶上了,会觉得人文活动其实还是蛮丰富的。


又因为美团在杨浦这边,离五角场那边并不远,我就总是去五角场那边酒吧喝酒qwq


还有还有,美团这边有健身房,所以我会比较固定的一周去三次,基本就是跑步加上一些基本的拉伸(在努力成为优秀的sigma(笑


9401697267529_.pic.jpg


ENDING...


后面因为个人身体的原因,这段实习只持续了大概四个月的时间,和最开始约定的时间有些出入。仔细想想,说遗憾其实也没有那么多遗憾,人生就是这样来去匆匆,经历一场离别,然后搭上列车,赶往下一场离别。


有缘再见啦,美团~


image.png


作者:阳树阳树
来源:juejin.cn/post/7289666055829192719
收起阅读 »

一个前端实习生在蔚来的成长小结

此文章2332字,预计花费时间7-11分钟。 一、聊聊工作氛围 & 个人成长 1. 这是我的期许 “所谓前途,不过是以后生活的归途。” 这是我人生中的第一段技术实习,之前并不是一心做技术的,所以为了探索自己喜欢的事情在某小公司做过翻译实习,并且在交行...
继续阅读 »

此文章2332字,预计花费时间7-11分钟。


image.png


一、聊聊工作氛围 & 个人成长


1. 这是我的期许


“所谓前途,不过是以后生活的归途。”


这是我人生中的第一段技术实习,之前并不是一心做技术的,所以为了探索自己喜欢的事情在某小公司做过翻译实习,并且在交行做过金融实习(其实就是纯打杂)。


image.png


我很喜欢这样一段话: “我曾以为我的23岁会手提皮包西装革履,但我还是穿着休闲裤,带着十几岁的意气行事,幼稚又成熟;我曾以为我的23岁会性格外向,做事圆滑,但我连最简单的亲情都处理不好;我曾以为我的23岁会和喜欢的人看山河大海落日余晖,但没想道周围的人谈婚论嫁都近在眼前,我还在路上找自己。”


我一直在探索着自己的边界,在能闯能疯的年纪反复横跳,寻找着自己的热爱与期许。在真正从事这个行业之后,我发现了我对于这个岗位的喜爱,当你看着一个个实际的视图出现于自己的手中,你会有一种莫名其妙的成就感,这种感觉难以描述的程度就好像你要向一个完全不看vtuber的人描述你对嘉然的喜爱。


2. 工作氛围:这里是一个乌托邦(适合摸鱼学习的好地方!)


说实话,我最开始预期是每天九点来上班,九点下班的(因为看学长们实习都好辛苦的样子)。


来了之后发现完全不是,每天十点上班,六点下班(我当然是准点跑路)



实习两个月左右的时候接的一个需求,第一天是另一个前端实习生来搞,后来他要跑路,leader就把活给我了。


周四,后端六点把接口给另一个前端实习生。


另一个前端实习生:“明天再说”


周五我来接这个活,我边画页面边让他加字段。


然后提完了,六点他给我改好的接口,让我看看有没问题


我:“下周再说”。


后端:“前端是不是,都很快乐啊[流泪]”



image.png


最开始因为我对 react 不是特别熟悉,leader 让我看着组内文档学了半个月,才开始了第一个需求。


leader 没有给我指定 mentor,所以当我有问题的时候,我看组内谁没开会(或者有时间)就会去问,都能得到很耐心的解答,这点来说还是很有安全感的。


然后每天都会跟着老板和大老板一起去吃饭,有时听他们说说自己的事情,有时听听他们对某个语言的看法,也算有不少收获。


值得一提的是刚入职三天部门就开始团建了,从周五下午五点玩到了第二天凌晨两点,炫了一只烤全羊,然后就开始电玩篮球各种 happy,后面玩狼人杀我次次狼人,大老板也总觉得我是狼人,我次次和他对着刚(乐)



马上就要第二次团建了,可惜参加不了呜呜呜



在团建上 leader 说我是从五个面试感觉都 ok 的人里面选出来的(当时我超惊喜的)


还有几件有趣的事情值得一提



第一件事情是中午和 leader 散步,他说:“你干了两个月这里的情况也看到,很难接触到同龄的小姐姐的,找对象的优先级应该要提高了。”


我:“说的对说的对。”


当时我心里就暗暗想着,这是我不想找吗?这tm是我找不到啊(悲)


第二件事情是我有事开了自己的热点,热点的名字叫:“要失业了咋办呐。


被同事发到了前端大群里。


同事:“这是谁的啊?”


我:“是实习生的(悲)”



3. 个人成长:“不卑不亢,低调务实”


最开始入职当然会担心一些七的八的,诸如这样说会不会不太客气,这样搞会不会让老板不爽,后来和老板还有大老板一起吃饭之后发现他们人都挺随和的,没什么架子,他们更多的关心的是这件事情做的怎么样。


大老板曾经在周会上说:“这个事情可以做的慢一些,这是能力上的问题,这个可以商量,但是如果到了约定的日期没有交付,这就有问题了。 ”这个是说的务实。


然后就是为人处事方面了,自己有时候挺跳脱的,没有什么边界感,在实习和他们一起吃饭的时候我就回默默的听着,有些问题大家都不会问,算是看着看着就成长了。


回校远程的时候我写了这样一段话:



去打工吧,去打上海冬夜准时下班,踩雪走回家的工。


去打工吧,去打一边聊天一边发现,这个产品也是清华✌️的工。


去打工吧,去打测试前一天,人都走光了,mentor陪我赶工到半夜的工。


去打工吧,去打部门团建,大leader带我们玩狼人杀到凌晨两点,超级尽兴的工。


冴羽曾在一次读书会上分享:“开眼界就像开荤一样,只有见过了才会产生饥饿感。”


打工虽然让我变成了稍不注意就会摆烂的成年人,但大平台汇聚了很多丰富有趣的同事,让我看到了截然不同的经历与一波三折的人生。


不知道是不是部门的原因,我这边总是十六五准点上下班。


我现在依然处于打工真香的阶段,不用早起,不用日复一日的和同龄人卷同一件事,身边的人年岁不同,人生阶段也不相同,卷不到一起去。


我还在路上~



image.png


4. 代码方面 learning


说实话看到组内项目的时候体会到了不少的震撼,看着组内的项目之后真的就感觉自己写的东西和玩具一样,每次写完项目,都会兴冲冲的找组内的哥哥姐姐帮忙 CR,然后 CR 出一堆问题,自己在一个一个的修改,把这些规范点记周报上,总之就是学到了很多很多。


timeLine 大概是这样的



  • 前两周熟悉 react 写小 demo

  • 然后以两周一个需求的速度给咱活干~


记得第二次写完一个稍微有点复杂的需求,带着我做这个需求的 mentor 还夸了我一波(骄傲)


5. 对于技术和业务的想法


大leader组织组内 vau 对齐的时候我仔细的听了听,我们的很多东西都需要落地,相比来说技术只是一个实现的手段,并不是做这个的目的。


但怎么说呢,我个人还是对技术本身抱有很大的期许的,希望自己能够变得很厉害,参与到很多的开源项目中,我坚信代码可以改变世界。


二、展望未来



实习不去字节,就像读四大名著不看红楼梦,基督徒不看圣经,学相对论不知道爱因斯坦,看vtuber不看嘉然今天吃什么,这个人的素养与精神追求不足,成了无源之水,无本之木。他的格局就卡在这里了,只能度过一个相对失败的人生!




  • 话是这么说啦,但最后还是没有成功去到字节,但是我是字节不折不扣的舔狗,后面再看吧。

  • 字节给我发面试一定是喜欢我(普信)


下面这段是之前写的



离开的契机也很简单,我在小红书实习的同学跑路了,然后要找继任,顺手把我的简历投过去了,然后我顺手面了一下小红书,小红书顺手给我发了个Offer(bushi,然后就去小红书了。



image.png


小红书确实Offer了,但是老板和我约谈了很久,我决定继续远程实习,在这篇文章发布的当天,我已经实习了 一百四十天,我相信,我的旅途还在继续。


image.png


三、写在最后


不知不觉就实习快半年了啊


我真的非常感谢遇到的leader和同事,感恩遇到的每一位愿意拉我一把的人。


在这段时间里学到了好多一个人学习学不到的东西啊。


那么这就是我在蔚来的实习小结啦!


感谢阅读~


作者:阳树阳树
来源:juejin.cn/post/7228245665334198333
收起阅读 »

在这个大环境下我是如何找工作的

蛮久没更新了,本次我想聊聊找工作的事情,相信大家都能感受到从去年开始到现在市场是一天比一天差,特别是在我们互联网 IT 行业。 已经过了 18 年之前的高速发展的红利期,能做的互联网应用几乎已经被各大公司做了个遍,现在已经进入稳定的存量市场,所以在这样的大背景...
继续阅读 »

蛮久没更新了,本次我想聊聊找工作的事情,相信大家都能感受到从去年开始到现在市场是一天比一天差,特别是在我们互联网 IT 行业。
已经过了 18 年之前的高速发展的红利期,能做的互联网应用几乎已经被各大公司做了个遍,现在已经进入稳定的存量市场,所以在这样的大背景下再加上全世界范围内的经济不景气我想每个人都能感受到寒意。


我还记得大约在 20 年的时候看到网上经常说的一句话:今年将是未来十年最好的一年。


由于当时我所在的公司业务发展还比较顺利,丝毫没有危机意识,对这种言论总是嗤之以鼻,直到去年国庆节附近。


虽然我们做的是海外业务,但是当时受到各方面的原因公司的业务也极速收缩(被收购,资本不看好),所以公司不得不进行裁员;
其实到我这里的时候前面已经大概有 2~3 波的优化,我们是最后一波,几乎等于是全军覆没,只留下少数的人维护现有系统。


这家公司也是我工作这么多年来少数能感受到人情味的公司,虽有不舍,但现实的残酷并不是由我们个人所决定的。


之后便开始漫长的找工作之旅,到现在也已经入职半年多了;最近看到身边朋友以及网上的一些信息,往往是坏消息多于好消息。


市场经历半年多的时间,裁员的公司反而增多,岗位也越来越少,所以到现在不管是在职还是离职的朋友或多或少都有所焦虑,我也觉得有必要分享一下我的经历。


我的预期目标


下面重点聊聊找工作的事情;其实刚开始得知要找工作的时候我并不是特别慌,因为当时手上有部分积蓄加上公司有 N+1 的赔偿,同时去年 10 月份的时候岗位相对于现在还是要多一些。


所以我当时的目标是花一个月的时间找一个我觉得靠谱的工作,至少能长期稳定的工作 3 年以上。


工作性质可以是纯研发或者是偏管理岗都可以,结合我个人的兴趣纯研发岗的话我希望是可以做纯技术性质的工作,相信大部分做业务研发的朋友都希望能做一些看似“高大上”的内容。
这一点我也不例外,所以中间件就和云相关的内容就是我的目标。


不过这点在重庆这个大洼地中很难找到对口工作,所以我的第二目标是技术 leader,或者说是核心主程之类的,毕竟考虑到 3 年后我也 30+ 了,如果能再积累几年的管理经验后续的路会更好走一些。


当然还有第三个选项就是远程,不过远程的岗位更少,大部分都是和 web3,区块链相关的工作;我对这块一直比较谨慎所以也没深入了解。


找工作流水账


因为我从入职这家公司到现在其实还没出来面试过,也不太知道市场行情,所以我的想法是先找几家自己不是非去不可的公司练练手。



有一个我个人的偏好忘记讲到,因为最近的一段时间写 Go 会多一些,所以我优先看的是 Go 相关的岗位。



第一家


首先第一家是一个 ToB 教育行业的公司,大概的背景是在重庆新成立的研发中心,技术栈也是 Go;


我现在还记得最后一轮我问研发负责人当初为啥选 Go,他的回答是:



Java 那种臃肿的语言我们首先就不考虑,PHP 也日落西山,未来一定会是 Go 的天下。



由于是新成立的团队,对方发现我之前有管理相关的经验,加上面试印象,所以是期望我过去能做重庆研发 Leader。


为此还特地帮我申请了薪资调整,因为我之前干过 ToB 业务,所以我大概清楚其中的流程,这种确实得领导特批,所以最后虽然没成但依然很感谢当时的 HR 帮我去沟通。


第二家


第二家主要是偏年轻人的 C 端产品,技术栈也是 Go;给我印象比较深的是,去到公司怎么按电梯都不知道🤣



他们办公室在我们这里的 CBD,我长期在政府赞助的产业园里工作确实受到了小小的震撼,办公环境比较好。



当然面试过程给我留下的印象依然非常深刻,我现在依然记得我坐下后面试官也就是 CTO 给我说的第一句话:



我看过你的简历后就决定今天咱们不聊技术话题了,直接聊聊公司层面和业务上是否感兴趣,以及解答我的疑虑,因为我已经看过你写的很多博客和 GitHub,技术能力方面比较放心。



之后就是常规流程,聊聊公司情况个人意愿等。


最后我也问了为什么选 Go,这位 CTO 给我的回答和上一家差不多😂


虽然最终也没能去成,但也非常感谢这位 CTO,他是我碰到为数不多会在面试前认真看你的简历,博客和 GitHub 都会真的点进去仔细阅读👍🏼。



其实这两家我都没怎么讲技术细节,因为确实没怎么聊这部分内容;这时就突出维护自己的技术博客和 GitHub 的优势了,技术博客我从 16 年到现在写了大约 170 篇,GitHub 上开源过一些高 star 项目,也参与过一些开源项目,这些都是没有大厂经历的背书,对招聘者来说也是节约他的时间。





当然有好处自然也有“坏处”,这个后续会讲到。


第三家


第三家是找朋友推荐的,在业界算是知名的云原生服务提供商,主要做 ToB 业务;因为主要是围绕着 k8s 社区生态做研发,所以就是纯技术的工作,面试的时候也会问一些技术细节。



我还记得有一轮 leader 面,他说你入职后工作内容和之前完全不同,甚至数据库都不需要安装了。



整体大概 5、6 轮,后面两轮都是 BOSS 面,几乎没有问技术问题,主要是聊聊我的个人项目。


我大概记得一些技术问题:



  • k8s 相关的一些组件、Operator

  • Go 相关的放射、接口、如何动态修改类实现等等。

  • Java 相关就是一些常规的,主要是一些常用特性和 Go 做比较,看看对这两门语言的理解。


其实这家公司是比较吸引我的,几乎就是围绕着开源社区做研发,工作中大部分时间也是在做开源项目,所以可以说是把我之前的业余爱好和工作结合起来了。


在贡献开源社区的同时还能收到公司的现金奖励,不可谓是双赢。


对我不太友好的是工作地在成都,入职后得成渝两地跑;而且在最终发 offer 的前两小时,公司突然停止 HC 了,这点确实没想到,所以阴差阳错的我也没有去成。


第四家


第四家也就是我现在入职的公司,当时是我在招聘网站上看到的唯一一家做中间件的岗位,抱着试一试的态度我就投了。
面试过程也比较顺利,一轮同事面,一轮 Leader 面。


技术上也没有聊太多,后来我自己猜测大概率也和我的博客和 Github 有关。




当然整个过程也有不太友好的经历,比如有一家成都的“知名”旅游公司;面试的时候那个面试官给我的感觉是压根没有看我的简历,所有的问题都是在读他的稿子,根本没有上下文联系。


还有一家更离谱,直接在招聘软件上发了一个加密相关的算法,让我解释下;因为当时我在外边逛街,所以没有注意到消息;后来加上微信后说我为什么没有回复,然后整个面试就在微信上打字进行。


其中问了一个很具体的问题,我记得好像是 MD5 的具体实现,说实话我不知道,从字里行间我感觉对方的态度并不友好,也就没有必要再聊下去;最后给我说之所以问这些,是因为看了我的博客后觉得我技术实力不错,所以对我期待较高;我只能是地铁老人看手机。


最终看来八股文确实是绕不开的,我也花了几天时间整理了 Java 和 Go 的相关资料;不过我觉得也有应对的方法。


首先得看你面试的岗位,如果是常见的业务研发,从招聘的 JD 描述其实是可以看出来的,比如有提到什么 Java 并发、锁、Spring等等,大概率是要问八股的;这个没办法,别人都在背你不背就落后一截了。


之后我建议自己平时在博客里多记录八股相关的内容,并且在简历上着重标明博客的地址,尽量让面试官先看到;这样先发制人,你想问的我已经总结好了😂。


但这个的前提是要自己长期记录,不能等到面试的时候才想起去更新,长期维护也能加深自己的印象,按照 “艾宾浩斯遗忘曲线” 进行复习。


选择



这是我当时记录的面试情况,最终根据喜好程度选择了现在这家公司。


不过也有一点我现在觉得但是考虑漏了,那就是行业前景。


现在的 C 端业务真的不好做,相对好做的是一些 B 端,回款周期长,同时不太吃现金流;这样的业务相对来说活的会久一些,我现在所在的公司就是纯做 C 端,在我看来也没有形成自己的护城河,只要有人愿意砸钱随时可以把你干下去。


加上现在的资本也不敢随意投钱,公司哪天不挣钱的话首先就是考虑缩减产研的成本,所以裁员指不定就会在哪一天到来。


现在庆幸的是入职现在这家公司也没有选错,至少短期内看来不会再裁员,同时我做的事情也是比较感兴趣的;和第三家有些许类似,只是做得是内部的基础架构,也需要经常和开源社区交流。


面对裁员能做的事情


说到裁员,这也是我第一次碰上,只能分享为数不多的经验。


避免裁员


当然第一条是尽量避免进入裁员名单,这个我最近在播客 作为曾经的老板,我们眼中的裁员和那些建议 讲到在当下的市场情况下哪些人更容易进入裁员名单:



  • 年纪大的,这类收入不低,同时收益也没年轻人高,确实更容易进入名单。

  • 未婚女性,这点确实有点政治不正确,但确实就是现在的事实,这个需要整个社会,政府来一起解决。

  • 做事本本分分,没有贡献也没出啥事故。

  • 边缘业务,也容易被优化缩减成本。


那如何避免裁员呢,当然首先尽量别和以上特征重合,一些客观情况避免不了,但我们可以在第三点上主动“卷”一下,当然这个的前提是你还想在这家公司干。


还有一个方法是提前向公司告知降薪,这点可能很多人不理解,因为我们大部分人的收入都是随着跳槽越来越高的;但这些好处是否是受到前些年互联网过于热门的影响呢?


当然个人待遇是由市场决定的,现在互联网不可否认的降温了,如果你觉得各方面呆在这家公司都比出去再找一个更好,那这也不失为一个方法;除非你有信心能找到一个更好的,那就另说了。


未来计划


我觉得只要一家公司只要有裁员的风声传出来后,即便是没被裁,你也会处于焦虑之中;要想避免这种焦虑确实也很简单,只要有稳定的被动收入那就无所谓了。


这个确实也是说起来轻松做起来难,我最近也一直在思考能不能在工作之余做一些小的 side project,这话题就大了,只是我觉得我们程序员先天就有自己做一个产品的机会和能力,与其把生杀大权给别人,不如握在自己手里。


当然这里得提醒下,在国内的企业,大部分老板都认为签了合同你的 24 小时都是他的,所以这些业务项目最好是保持低调,同时不能影响到本职工作。



欢迎关注作者公众号于我交流🤗。



作者:crossoverJie
来源:juejin.cn/post/7246570594991718455
收起阅读 »

5分钟回顾webpack的前世今生

web
引言 模块化编程是软件设计的一个重要思想。在JavaScript中,处理模块一直是个问题,由于浏览器只能执行JavaScrip、CSS、HTML 代码,所以模块化的前端代码必须进行转换后才能运行。例如 CommonJS 或 AMD,甚至是ECMA 提出的 Ja...
继续阅读 »

引言


模块化编程是软件设计的一个重要思想。在JavaScript中,处理模块一直是个问题,由于浏览器只能执行JavaScrip、CSS、HTML 代码,所以模块化的前端代码必须进行转换后才能运行。例如 CommonJS 或 AMD,甚至是ECMA 提出的 JavaScript 模块化规范——ES6 模块,这些模块系统要么是在浏览器无法运行,要么是无法被浏览器识别和加载,所以针对不同的模块系统,就需要使用专门的工具将源代码转换成浏览器能执行的代码。


整个转化过程被称为构建,构建过程就是“模块捆绑器”或“模块加载器”发挥作用的地方。


Webpack是JavaScript模块捆绑器。在Webpack之前,已经有针对各类型的代码进行编译和构建的流程,例如使用Browserify对CommonJS模块进行编译和打包,然后将打包的资源通过HTML去加载;或者通过gulp进行任务组排来完成整个前端自动化构建。


但是这些方式的缺点是构建环节脱离,编译、打包以及各类资源的任务都分离开。


Webpack模块系统的出现,能将应用程序的所有资源(例如JavaScript、CSS、HTML、图像等)作为模块进行管理,并将它们打包成一个或多个文件并进行优化。Webpack的强大和灵活性使得其能够处理复杂的依赖关系和资源管理,已经成为了构建工具中的首选。


本文主要来扒一扒Webpack的发展进阶史,一起来看看Webpack是如何逐渐从一个简单的模块打包工具,发展成一个全面的前端构建工具和生态系统。


webpack发展历程


webpack从2012年9月发布第一个大版本至2020年10月一共诞生了5个大的版本,我们从下面一张图可以清晰具体地看到每一个版本的主要变化

Webpack发展史.png

Webpack 版本变化方向



  1. Webpack 1:在此之前多是用gulp对各个类型的编译任务进行编排,最后在Html文件中将各种资源引用进来,而Webpack的初始版本横空出世,凭借如下其功能、理念、内核等优点成为众多前端构建工具的最新选择。



  • 理念:一切皆资源,在代码中就能能对Html、Js、Css、图片、文本、JSON等各类资源进行模块化处理。

  • 内核:实现了独有的模块加载机制,引入了模块化打包和代码分割的概念。

  • 功能:集合了编译、打包、代码优化、性能改进等以前各类单一工具的功能,成为前端构建工具标准选择。

  • 特点:通过配置即可完成前端构建任务,同时支持开发者自定义LoaderPlugin对Webpack的生态进行更多的扩展。



  1. Webpack 2: Webpack 2的在第一个版本后足足过了4年,其重点在于满足更多的打包需求以及少量对打包产物的优化



  • 引入对ES6模块的本地支持。

  • 引入import语法,支持按需加载模块。

  • 支持Tree Shaking(无用代码消除)。



  1. Webpack 3:Webpack 3提供了一些优化打包速度的配置,同时对打包体积的优化再次精益求精



  • 引入Scope Hoisting(作用域提升),用于减小打包文件体积。

  • 引入module.noParse选项,用于跳过不需要解析的模块。



  1. Webpack 4:Webpack 4带来了显著的性能提升,同时侧重于用户体验,倡导开箱即用



  • 引入了mode选项,用于配置开发模式或生成模式,减少用户的配置成本,开箱即用

  • 内置Web Workers支持,以提高性能



  1. Webpack 5:Webpack 5继续在构建性能和构建输出上进行了改进,且带来跨应用运行时模块共享的方案



  • 支持WebAssembly模块,使前端能够更高效地执行计算密集型任务。

  • 引入了文件系统持久缓存,提高构建速度

  • 引入Module Federation(模块联邦),允许多个Webpack应用共享模块


webpack打包后的代码分析


为了更方便理解后续章节,我们先看一下Webpack打包后的代码长什么样(为了方便理解,这里以低版本Webpack为例,且不做过多描述)


jsx
复制代码
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};

/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ /* 省略 */
/******/ }

/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;

/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;

/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";

/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {/*省略*/})
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {/*省略*/})
/* 2 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {/*省略*/})
/******/ ]);

可以看到其实入口文件就是一个IIFE(立即执行函数),在这个IIFE里核心包括两块:



  1. 模块系统:Webpack 在IIFE里实现了模块系统所需要的Module、Require、export等方法组织代码。每个模块都被包装在一个函数内,这个函数形成了一个闭包,模块的作用域在这个闭包内。

  2. 模块闭包IIFE的入参即是Modules,它是一个数组,数组的每一项则是一个模块,每个模块都有自己的作用域。模块和模块之间通过Webpack的模块系统可以进行引用。


webpack的发展长河中,笑到最后和沦为历史

笑到最后:OccurrenceOrderPlugin



有趣的是该插件在Webpack 1叫做OccurenceOrderPluginWebpack 2才更名为OccurrenceOrderPluginWebpack 3则不需要手动配置该插件了。


插件作用:用于优化模块的顺序,以减小输出文件的体积。其原理基于模块的使用频率,将最常用的模块排在前面,以便更好地利用浏览器的缓存机制。


有了前面对于Webpack打包后的代码分析,OcurrenceOrderPlugin的优化效果也就很好理解了。它的原理主要基于两个概念:模块的使用频率模块的ID



  1. 模块的使用频率:OccurrenceOrderPlugin 插件会分析在编译过程中每个模块的出现次数。这个出现次数是指模块在其他模块中被引用的次数。插件会统计模块的出现次数,通常情况下,被引用次数更多的模块将被认为更重要,因此会更早地被加载和执行。

  2. 模块的 ID:Webpack 使用数字作为模块的 ID,OccurrenceOrderPlugin 插件会根据模块的出现次数,为每个模块分配一个优化的 ID。这些 ID 的分配是按照出现次数从高到低的顺序进行的,以便出现次数较多的模块获得较短的 ID,这可以减小生成的 JavaScript 文件的大小。假设一共有100个模块,最高的频率为被引用100次,则减小文件体积200B。(确实好像作用很小,但是作为最贴近用户体验的前端er,不应该是追求精益求精嘛)


这个插件的主要目标是减小 JavaScript 文件的体积,并提高加载性能,因为浏览器通常更倾向于缓存较小的文件。通过将频繁使用的模块分配到较短的 ID,可以减小输出文件的体积,并提高缓存的效率。


笑到最后:Scope Hoisting


过去 Webpack 打包时的一个取舍是将 bundle 中各个模块单独打包成闭包。这些打包函数使你的 JavaScript 在浏览器中处理的更慢。相比之下,一些工具像 Closure Compiler 和 RollupJS 可以提升(hoist)或者预编译所有模块到一个闭包中,提升你的代码在浏览器中的执行速度。


而Scope Hoisting 就是实现以上的预编译功能,通过静态分析代码,确定哪些模块之间的依赖关系,然后将这些模块合并到一个函数作用域中。这样,多个模块之间的函数调用关系被转化为更紧凑的代码,减少了函数调用的开销。这样不仅减小了代码体积,同时也提升了运行时性能。


Scope Hoisting 的原理是在 Webpack 的编译过程中自动进行的,开发人员无需手动干预。要启用 Scope Hoisting,你可以使用 Webpack 4 版本中引入的 moduleConcatenation 插件。在 Webpack 5 及更高版本中,Scope Hoisting 是默认启用的,不需要额外的配置。


CommonsChunkPlugin的作用和不足,为何会被optimization.splitChunks所取代


CommonsChunkPlugin 插件,是一个可选的用于建立一个独立chunk的功能,这个文件包括多个入口 chunk 的公共模块。主要配置项包含


json
复制代码
{
name: string, // or
names: string[],
filename: string,
minChunks: number|Infinity|function(module, count) => boolean,
chunks: string[],
// 通过 chunk name 去选择 chunks 的来源。chunk 必须是 公共chunk 的子模块。
// 如果被忽略,所有的,所有的 入口chunk (entry chunk) 都会被选择。

children: boolean,
deepChildren: boolean,
}

通过上面的配置项可以看到虽然CommonsChunkPlugin将一些重复的模块传入到一个公共的chunk,以减少重复加载的情况,尤其是将第三方库提取到一个单独的文件中,但是其首要依赖是通过Entry Chunk进行的。在Webpack4以及更高的版本当中被optimization.splitChunks所替代,其提供了配置让webpack根据策略来自动进行拆分,被替代的原因主要有以下几点:



  1. 灵活度不足:在配置上相对固定,只能将指定 Entry Chunk的共享模块提取到一个单独的chunk中,可能无法满足复杂的代码拆分需求。

  2. 配置复杂:需要手动指定要提取的模块和插件的顺序,配置起来相对复杂,开发者需要约定好哪些chunk可以被传入,有较高的心智负担。而optimization.splitChunks只需要配置好策略就能够帮你自动拆分。


因此在Webpack 4这个配置和开箱即用的版本里,它自然也就“香消玉损”。只能遗憾地看到一句:


the CommonsChunkPlugin 已经从 Webpack v4 legato 中移除。想要了解在最新版本中如何处理 chunk,请查看 SplitChunksPlugin


被移除的DedupePlugin


这是 Webpack 1.x 版本中的插件,用于在打包过程中去除重复的模块(deduplication),其原理不知道是通过内容hash,还是依赖调用关系图。但是在Webpack 2中引入了Tree Shaking功能,则不再需要了。原因有以下几点:



  • Tree Shaking控制更精确:能通过静态分析来判断哪些代码是不需要的,实现了更细力度的优化。

  • Scope Hositing减少了重复模块:Webpack 3引入了Scope Hositing,将模块包裹在函数闭包中,进一步减少了重复模块的依赖


因此我们在Webpack的文档中看到:



DedupePlugin has been removed


不再需要 Webpack.optimize.DedupePlugin。请从配置中移除。



总结


或许有些插件你已经看不到它的身影,有些特性早已被webpack内置其中。webpack从第一个版本诞生后一直致力于以下几个方面的提升:



  1. 性能优化:通过去除重复代码、作用域提升、压缩等方式减少代码体积和提高运行时性能。

  2. 构建提效:通过增量编译、缓存机制、并行处理等提升打包速度。

  3. 配置简化:通过内置必要的特性和插件以及简化配置提升易用性。


作者:古茗前端团队
来源:juejin.cn/post/7289718324858355769
收起阅读 »

”调试小技巧,让未来更美好“

web
① 自动打断点(抛异常时自动断点) 偶然一次可能不小心打开某个设置选项,可能设置了英文又不知道是打开了什么,只知道当每次打开F11打开控制台调试看数据的时候,就是不会自动停在某个位置,又不知道怎么停掉,怀疑会不会是安装了什么谷歌插件或者是油猴哪个脚本代码写错写...
继续阅读 »

自动打断点(抛异常时自动断点)


偶然一次可能不小心打开某个设置选项,可能设置了英文又不知道是打开了什么,只知道当每次打开F11打开控制台调试看数据的时候,就是不会自动停在某个位置,又不知道怎么停掉,怀疑会不会是安装了什么谷歌插件或者是油猴哪个脚本代码写错写了什么。


不小心打钩了断点调试的遇到未捕获的异常时暂停,或者在遇到异常时暂停这两个选项其中一个。就有可能导致了谷歌的调试器暂停,取决于这个网站有没有一些异常触发到这一点,勾选上每次异常浏览器会帮我们打断点。


image.png


所以解决办法就是把谷歌浏览器中的这两个勾去掉,如果不是你本意打开想要调试网站中一些异常的报错。


image.png


一键重发请求(不用每次重新请求就刷新页面)


排查接口的时候,需要重新请求该接口,不必要每次重新刷新页面去请求试接口里面传参对不对返回来的数据对不对。重发请求很简单,右击该接口重发xhr即可。


image.png


image.png


③ 断点调试+debugger+console+try...catch


(1) console.log


找bug解决bug是很重要滴。console.log-输出某个变量值是非常非常常用的,只要做码农一定得会各种语言的输出消息和变量的语句,方便我们查看和调试。


(2) debugger(不用每次都console)


在代码某个语句后面或者前面输debugger


在我入行到在学校生涯那段时间都不知道debugger;这玩意,有一次项目有一个比较棘手不知道怎么解决的问题,甲方公司项目负责人开会重点讲了那个问题,就见他这里输一下dubugger,那里输一个debugger,当时就觉得那玩意很神(反正意识上只要我们不懂的东西刚开始接触都是这样,这里神那里神的,接触久了就觉的也就那样不过如此,很平常),最后也没看出什么来。


debugger就是在某个状态下,用这个debugger;语句在那里断一下点,然后当下,上下文的状态和值都可以在查看,哪个分支导致变量状态错误。


使用debugger可以查看:



  • 作用域变量

  • 函数参数

  • 函数调用堆栈

  • 代码整个执行过程(从哪一句到哪一句的)

  • 如果是异步promise async...await 等这种的话就需要在then和catch里面debugger去调试


(3) try...catch 捕获异常


try {
// 可能会抛出异常的代码
} catch {
// 处理所有异常的代码
}

try...catch捕获异常,包括运行时错误和自定义以及语法错误。


try...catch中还可以在某些情况下用throw在代码中主动抛出异常。


try {
// 可能会抛出异常的代码

if (某某情况下) throw '某某错误提示信息'

} catch {
// 处理所有异常的代码
} finally {
// 结束处理用于清理操作
}

image.png


④ 联调查看接口数据


image.png


如上图这个接口,如果想要复制接口preview里面的数据,


除了去Responese里面去找我们需要的某个值去选择复制之外(这个有个缺点就是要找值不直观),还可以右击某个值,然后通过点击store object as global variable(存储为全局变量) 获取。


image.png


当我们点击了之后,控制台就会出现tempXX这个变量。


image.png


我们就只需要在控制台输入copy(temp3)copy(要复制的json名),在粘贴板上就有这个json数据了。



💡
全局方法copy()在console里copy任何你能拿到的数据资源。



image.png


⑤ 后端接口数据返回json


这个有时候有的同学有可能碰到类似这种的JSON数据{\"name\":\"John\",\"address\":\"123 Main St, City\"}


解决方法


直接打开控制台console,输入 JSON.parse("{"name":"John","address":"123 Main St, City"}"),这样


image.png


如果你想复制下来用,直接跟上面我们用copy这好碰上,赋值加上一个copy就可以了。


image.png


这样这个值就在粘贴板上了。


总结


报错和bug,多多少少会贯穿我们的职业生涯中,如何定位问题、解决问题,加以调试,是我们必须也是不能不必备的技能。


当你捕获bug的时候.gif



☎️ 希望对大家有所帮助,如有错误,望不吝赐教,欢迎评论区留言互相学习。



作者:盏灯
来源:juejin.cn/post/7288963208396603450
收起阅读 »

我是如何走上程序员的道路的

封面图 学生时代的照片,想来已是十几年前的事情了 学生时代 从2009年9月开始读大学,当然也不是什么好大学,位于中原腹地的一所三流大学,所学专业是经济学中的《审计》。 上学的时候其实也不知道审计这个专业到底是做什么的,当时只是根据大学老师的讲述,大抵是可以...
继续阅读 »

封面图


image.png


学生时代的照片,想来已是十几年前的事情了


学生时代


从2009年9月开始读大学,当然也不是什么好大学,位于中原腹地的一所三流大学,所学专业是经济学中的《审计》。


上学的时候其实也不知道审计这个专业到底是做什么的,当时只是根据大学老师的讲述,大抵是可以到企业中做个会计师什么的,当然我们的专业课除了《审计》之外,《会计》《成本会计》《经济法》《税法》《财务管理》之类的都有。


学生时代对于我这种普通家庭的学生,大部分时候都是比较迷茫的,虽然每天都去上课,也很努力的去学习,识记老师讲课的内容,然后参加各种专业考试,会计证,助理会计师之类的考试,但是其实对于自己将来到底想要成为什么样的人,将来做什么样的工作,过怎样的生活,都没有一个完整的概念,只是随着日子一天一天的逝去,随波逐流而已,对于这种状态,我现在通常用一个词来概括:局限性


局限性我给它的定义是:在个人所处的时代,环境以及个人认知水平有限的情况下,个人所能做事情的极限。简而言之,就是所有的事情都是命里注定的一个圈,超出了这个圈的范围,别的事情就做不了,这就是局限。


在这种局限下浑浑噩噩的度过了三年的时光,除了和班里同学一同度过这三年时光能够留下一些快乐的回忆之外。还有一件事情我觉得值得聊一下,就是我对英语比较感兴趣,这种兴趣表现在我喜欢在没课的时候去蹭英语系的外教课,喜欢听那个胖胖的外交讲课,有时候也到讲台上和他们一起做一些互动,讲一些英文,练习一下自己的口语。


等他们下课了,clay,那个外教的名字叫clay,是一个胖胖的60多岁的老头,我喜欢在他等班车的的时候跟他聊会儿天,内容我也记不得是什么了,大抵是些怎么学好英语之类的话,他也非常和蔼,只是说:dont warry,you need more pritice之类的,也有些是他在中国收养了很多孩子,住在大学路上什么的。


外语系的姑娘都很漂亮,当然,这可能也是我去蹭英语课的一个原因。


日子就这么一天一天的过去,上课,逃课,上课,逃课,逃课去蹭课。逃课出去玩,忽然有一天发现,自己马上就要毕业了,然后发现大家都开始准备找工作了,于是自己也开始准备简历,去人才市场,去各种招聘会~


那里能有什么好工作呢~


工作


好在是2012年的8月份吧,被一家单位录取了,名义上是做财务,其实是被派到外地做出纳去了,地址在东莞分理处。


彼时的想法是,好好工作两年,然后混个分理处的主管什么的,其实自己非常清楚自己本身也不适合做主管,好在分理处的同时都是年轻人,比较好相处。


于是在这里待了有一年多,分理处的其他人主要是做销售工作,基于工作性质的不稳定性,人员流失率很大,后来我就也离职了。


进京


从那里离职之后,去了四川的一家建筑单位,驻扎在项目部,职务是会计,主要工作内容是帮忙做些单位的内账,负责企业的报税及项目部的部分出纳工作。


彼时已经是2014年了,淘宝等线上购物平台已经非常流行了,听说当时的美工等工作也非常吃香,于是打算自学photoshop,将来转成美工,做些淘宝店铺的装修等工作。


可惜美工没有做成,恰逢建筑项目上需要进行融资,跟银行进行贷款,所以当时有一项工作是用photoshop伪造发票,将金额10万的发票,改成100万,甚至更大的金额。修改以后用打印机打印出来,公司用于向银行贷款等。


说是项目部,其实就是旁边就是工地,工地的生活非常艰苦,管理人员还好一些,一线工人每天干的都是体力活,确实挺辛苦的,于是到了2014年底,就提出了离职。


离职之后,辗转就到了北京。


到了北京之后,找了一个主要做供应链的单位,对接的是外企,职位名称忘了,主要负责回复一些英文文件,偶尔也需要通过电话和老外做些沟通,问他们要一些文件什么的。


幸运的是认识了一个做前端的同事。这时候是2015年,这个同事高中没毕业,做前端,薪资当时是12000,让我很是羡慕,有时候开玩笑跟我说你也转前端吧。


2015年,当时正是前端市场非常火的时候,移动互联网正在兴头上,于是下定决心开始学习前端,从最简单的html标签,css样式,Javascript开始,一点一点的去背,去学习相关的知识,也从简单的仿站开始练习,各种布局,样式属性之类的,写完了就拿给他看,虽然写的不怎么样~


就这么持续了大概有一年吧~


转前端


好在功夫不负有心人,2016年开始试着找一些前端相关的工作,最终还是找到了一份工作,那时候还没有现在的各种框架。


记得第一份前端工作的项目是一个javaweb的项目,然后还有一个用appcan做的混合开发的移动端项目。


就这样从看别人的代码,到自己写一些代码,从纯粹的前端三剑客html+css+js,慢慢的写到了前端三大框架vue+react+ng 。


转眼间从事前端工作也这么些年了,越写越觉得自己对前端的兴趣越浓,因为它有很多新的东西,虽然我不一定每个知识点都能弄的明白,但是当你思考时,那种沉浸其中的感觉是妙不可言的。


我也感谢这个行业,虽然它不能让我大富大贵,但是它起码做到了这些年让我衣食无忧。


最后


单纯的从技术方向出发,前端的方向很多,web,小程序,客户端,移动端,每个领域都有很多值得探索的东西。


虽然我不是科班出身,但是在这些年的学习和探索中,它教会了我很多东西。


最后更多的想说的可能只有两个字:感谢


感谢那些帮助过我的人;


感谢那些年遇到的苦难;


也感谢自己的坚持~


希望每个人在自己的人生中都能得偿所愿~


谢谢~


作者:前端那些年
来源:juejin.cn/post/7162205132292096037
收起阅读 »

谁还没个靠bug才能正常运行的程序😌

web
最近遇到一个问题,计算滚动距离,滚动比例达到某界定值时,显示mask,很常见吧^ _ ^ 这里讲的不是这个需求的实现,是其中遇到了一个比较有意思的bug,靠这个bug才达到了正确效果,以及这个bug是如何暴露的(很重要)。 下面是演示代码和动图 <!DO...
继续阅读 »

最近遇到一个问题,计算滚动距离,滚动比例达到某界定值时,显示mask,很常见吧^ _ ^


这里讲的不是这个需求的实现,是其中遇到了一个比较有意思的bug,靠这个bug才达到了正确效果,以及这个bug是如何暴露的(很重要)。


下面是演示代码和动图


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.container {
width: 300px;
max-height: 300px;
background-color: black;
position: absolute;
top: 60px;
left: 50%;
transform: translateX(-50%);
overflow-y: auto;
}
.child {
width: 260px;
height: 600px;
margin: 0px 20px;
background-color: pink;
position: relative;
}
.flag {
position: absolute;
width: 100%;
height: 25px;
background-color: blueviolet;
color: aliceblue;
text-align: center;
line-height: 25px;
font-size: 14px;
left: 0;
right: 0;
}
.top {
top: 0;
}
.bottom {
bottom: 0px;
}
</style>
</head>

<body>
<div class="container">
<div class="child">
<div class="flag top">top</div>
<div class="flag bottom">bottom</div>
</div>
</div>
</body>
</html>


20230927105849_rec_.gif




开始计算啦,公式:滚动比例 = 滚动距离 / 可滚动距离


滚动距离: $0.scrollTop


可滚动距离: $0.scrollHeight - $0.offsetHeight


即:scrollRatio = scrollTop / (scrollHeight - offsetHeight)


滚动到底部,计算结果是 300 / (600 - 300) = 1


image.png


我们需要拿scrollRatio某界定值(比如0.1)作大小的比较,计算是true还是false(用isShow = scrollRatio < 某界定值来保存)。


这里一切正常。




不正常的情况出现了


就是没有出现滚动条的情况,即.child的高度没有超过.container的高度时,把.child的高度设成.containermax-height,就没有滚动条了(下面讲的情景也都是没有滚动条的情况)。


image.png


这个时候再去计算,得到了NaN,以至于 NaN < 0.1 = false


image.png


因为isShow的预期就是false,所以一直都没有发现这个bug。




那么它是如何暴露的呢?


后来新的需求给.container加了border。演示一下加border,然后再去计算:


image.png


发现没,这时候$0.offsetHeight的高度把border的高度也算进去了,结果就成了true,这不是想要的结果 ❌。




然后就是一番查验


offsetHeight是一个元素的总高度,包括可见内容的高度、内边距(padding)、滚动条的高度(如果存在)以及边框(border)的高度。


而我们这里只需要可见的高度,就可以用到另一个属性了clientHeight


clientHeight是指元素的可见内容区域的高度,不包括滚动条的高度和边框的高度。它仅包括元素的内部空间,即内容加上内边距。


image.png


当然这也只是继续使除数为0,然后得到结果为NaN,不过bug已经暴露出来了,后面就是一些其他的优化啦~




总结 + 复习(盒模型 box-sizing)


发现没有,offsetHeightclientHeight的区别,就像盒模型中的标准盒模型怪异盒模型的区别:


box-sizing: content-box(默认,标准盒模型):宽度和高度的计算值都 不包含 内容的边框(border)和内边距(padding)。添加padding和border时, 使整个div的宽高变大。


box-sizing: border-box(怪异盒模型):宽度和高度的计算值都 包含 内容的边框(border)和内边距(padding)。添加padding和border时, 不会 使整个div的宽高变大。


这样讲是不是加深一下对这两种属性的印象


^ - ^


作者:aomyh
来源:juejin.cn/post/7283087306603823116
收起阅读 »

一年空窗期后我是如何准备面试的?

web
在此之前我是自由职业者,满打满算一年空窗期,在被动收入不稳定,心想还是需要一份收入来维持日常生活开销,再去考虑打造自己的额外收入。 前前后后从准备到上岸历时一个半月,今天从三个方面分享这个过程我做了什么? 心态 做事情之前,心态很重要,我遇事很少否定自己,在...
继续阅读 »

在此之前我是自由职业者,满打满算一年空窗期,在被动收入不稳定,心想还是需要一份收入来维持日常生活开销,再去考虑打造自己的额外收入。



前前后后从准备到上岸历时一个半月,今天从三个方面分享这个过程我做了什么?


心态


做事情之前,心态很重要,我遇事很少否定自己,在我看来,别人可以做到的,自己也可以,虽然一年空窗,基本上不接触技术,写代码量远不如以前,但又不是要上天或者造原子弹,取决于谁执行力强,谁付出的时间多,仅此而已。


换作以前,相信大部分的同学去找半个月都可以入职自己期望的岗位,看了一下网上的情绪,行情在这个环境下的确蛮消极的,很多人找了几个月都没有上岸的,当然我自己也有感受到,简历丢出去之后没有声音,并且在各大招聘网站上坑位也减少了,相比两三年前如日中天的行情,难免会有这类情绪。


但我没有那么焦虑,为什么呢?其一是我心态比较好,其二是跟我的定位有关。


定位


第一个是我要找的岗位定位为中高级开发,而这类人在市场上来看一直都是稀缺资源,其他行业也如此。


第二个是薪酬范围定位在20k-25k范围,给不到我会觉得工作没劲,累点没关系,主要还是相信自己可以胜任。


第三个是前期投几个低于期望值的试试水,了解一下目前行情顺便找找感觉。


所以,接下来我只需要把目标定位在寻找中高级开发岗位即可,完善自己达到这个能力要求,下面是行动计划,细看下来你会发现这又是个PDCA


计划


我把计划分为这几个模块:


1. 简历优化


我一开始是不会写简历的,因为中间没有跳过槽,也没定时更新,所以就随便拿了以前的模板改了改时间和项目就开始投了,简历回复少不说,即使有机会面试了也没有把简历提到的讲清楚,结果可想而知。


后面想想不行,虽然没写过,但是我会看简历啊,之前带团队有时候一天要看上百份简历,大概知道面试官青睐哪些人才,优化之后断断续续才有面试。


其次是我在面试过程结束时问面试官哪些地方还需要提升的,不少也会反馈简历问题,诸如:



  • 管理工作内容太笼统了,看不出具体做了什么

  • 没有说清楚空窗期做了什么

  • 没有体现出你在项目中做了什么

  • ......


知道自己问题之后,前后迭代了大概十几个版本,越到后面的质量越高,直至我入职之后,还有一个目标企业发来邀请。


2. 技能要求


前端领域涉及到这么多技能,需要有方向进行准备,分享一下我是如何分类:


基础:



  • 前端三大件:HTML、CSS、JS

  • 主流框架:Vue2/Vue3、React

  • 状态管理:Vuex、redux、mobox

  • 构建工具:webpack、Vue-cli、Vite、gulp

  • 前端安全:xss、csrf、cors 常见问题和防御措施


进阶:



  • JS运行机制、事件轮询机制、浏览器运行原理

  • 前端性能监控、前端异常监控如何做?

  • 前端工程化体系包含哪些

  • 前端性能优化手段:页面性能、构建性能、代码性能

  • Vue、React核心原理

  • 基础算法数据结构

  • Http协议


面对上面的技术基础类,主要是刷官方文档+常见面试题,这些更多是概念性的东西,在这里就不多说了,相信大家手上多少都有八股文资料,如果没有可以私信我。


而面对进阶类,首先总结自己项目中用到了哪些,吃透它。其次,面对不太熟悉的板块如HTTP网络,我会通过专栏学习或者一些讲得好的课程来弥补。


除了上面的方法,还有一种我常用的技巧来覆盖知识盲区,就是下面要说的模拟面试,几乎适用于任何技能面试。


3. 模拟面试


这里要说的模拟面试并不是找一些大佬一对一模拟训练,而是换位思考(当然能够模拟面试效果更好啦~)。


即把自己想象成面试官,在考察某一个知识点的时候,你会问自己什么问题呢?


举2个栗子🌰


对于用Vue的同学,我会问:



  • vue diffreact diff有什么区别?

  • 为什么v-for中建议带:key,有什么情况下是可以不带的?

  • 写过组件或者插件吗,有什么注意点?

  • vue-router原理是什么


结合一些热门的话题,我会问:



  • vue2vue3对比,你觉得主要区别是什么?

  • vue2vue3在性能优化做了什么?两者的构建产物有什么区别?

  • 如果你去学vue3,你会从哪里开始,怎么学?


除了以上我给自己虚构的问题之外,还有诸如vue生命周期啊、组件通信啊等等基础肯定是要会的,我会刷文档或虚构题目,这些比较简单,搞懂就行了。


对于设计模式,我也问了自己几个问题:



  • 你知道的设计模式有哪些,知道他们的应用场景吗(解决了什么问题)?

  • 在工作中用到的设计模式有哪些?说说它们的优劣势

  • Vue中用了哪些设计模式?

  • 观察者和发布订阅有什么不同?


基本上这个薪酬范围的设计模式,搞懂了以上问题大差不差。


再来说说这种方式有什么优势?


首先,问题是通过我们自身思考提出并主动寻求解决的,这本身已经存在闭环了,有利于我们理解一个知识点。其次,我们思考提出某个问题,意味着大脑🧠的神经元网络中有存在某些游离神经节点,它没有被连接到一起,随着提出并解决的问题越多,连接起来的网络就越大,这就形成了所谓的知识网络,相比没有目的刷题,它的持久性更强,更能抗遗忘。


总结


结束之前,再分享面试过程中的一个小插曲,当时面了一家小企业,终面的时候面试官问我期望薪酬,就报了18k,但是面试官说给不到,17k考不考虑?我当时没有回绝,就说回去考虑一下。


回去考虑一番之后,我根据当时岗位给到的薪资范围,加上当时家里事情比较多,想先稳定下来再考虑其他的,打算接了这个offer准备上班,突然闹了个乌龙,HR说老板那边重新定了价,只能给到16k,我说还能这么操作?这不明摆着欺负老实人嘛?


想了想如果接了这个offer,岂不是比之前离职时更低,更别说对比以前的同事了。心里忍不下这口气,以至于那两周,每天都撸到一点钟,功夫不负有心人,最后顺利上岸了!


分享几点个人觉得比较关键的:



  • 永远相信自己,心态很重要,不仅仅面试,它贯穿人的一生

  • 简历真实,不玩心思,例如空窗期这种,如实说明

  • 吃透简历内容,不留疑点

  • 面试过程中不着急回答问题,可以先澄清问题动机,不要为了回答而拼凑答案

  • 前面几次不通过没关系,但一次要比一次好


以至于如何备战高级开发,等我升级了再来分享~


最后,祝愿所有航海者都能够顺利靠岸!!!


注:由于最近比较多朋友私信我咨询简历优化建议或者八股文,可以加我的微信followJavaScript,丢简历过来即可,备注“掘金”。


作者:寻找奶酪的mouse
来源:juejin.cn/post/7285915718666944547
收起阅读 »

从钱塘江边到余杭塘河

把两个月前 钱江边的聚会小酌 成功移到余杭塘河。三巡五味后,一行三人在偌大的校园走着聊着。在滨江某知名支付公司的测试经理,感叹管理层间人浮于事站队排位;在阿里的前端程序员,困顿于今年未能升P竞争压力山大。而我,定位为服务端却在走全栈路线,也在纠结更换技术栈,看...
继续阅读 »

把两个月前 钱江边的聚会小酌 成功移到余杭塘河。三巡五味后,一行三人在偌大的校园走着聊着。在滨江某知名支付公司的测试经理,感叹管理层间人浮于事站队排位;在阿里的前端程序员,困顿于今年未能升P竞争压力山大。而我,定位为服务端却在走全栈路线,也在纠结更换技术栈,看不清寻不到一个更满意充满想象力的未来。


似乎吐槽和患得患失,多过幸福和豪气干云。而回想这一切刚开始时的忐忑不安踌躇满志,绝没想到,会在一个雨过方晴的深夜,有这样一番对话。


4年前的今天,我离开家乡,从济南坐上一节南下的橙皮车。当慢慢悠悠走过十余个小时,从微山湖夕阳落下到寒山寺晨钟响彻,我来到了曾一见倾心风云际会的魔都,一只怯怯什么也不会的菜鸟,正式开启了必将色彩斑斓的职业生涯。


此前几天,第一次用58找房,还不知有那么多套路。我发了100多条短信,大多如泥牛入海不见回声。偶有的几条,也是中介答复:“便宜的前几天租出去了,还有套价格贵一些的”。我终在豆瓣小组,在车来到济宁时收到了这条回复。满是欣喜打开,充满失望关闭。


此前几个月,我放弃割舍许多,不曾随我的相当多数同学,去那几家中字头央企,也不曾青眼相向,可以一辈子安稳无虞的公务员事业编,而选择了一家未曾听闻的“互联网”公司。我知道,当我听到这三个字,郁结于胸口怏怏不乐的大石顿被粉碎,我知道,我的第一家公司,不会再有其他。然而回宿舍后我做的第一件事,是查证这家公司是否为诈骗组织皮包公司。我也在暗暗忧心,空有一腔热爱,就真能做得好吗?兴趣真的可以打败科班无视专业?


我在仙霞路工程技术大学的国家级创业园,度过了终生难忘的两年,也完美回答了上面的疑惑。又在两年前魔都落户安家周折太巨,选择如候鸟,随潮离沪来杭。


四年里,我以平均一年一家的频率,换过四家公司,薪资较最初翻转500%。四年前,创新创业之风荡涤全国,o2o风云乍起,p2p方兴未艾。而今,内外部趋势交织,猪从风口跌落,独角沦为毒角。我也面临职业和薪资门卡:我是否还能心平静气在一家单位多呆几年,沉淀而不颓废?我如何还能保持在基数较大的情况下,使薪资以较高速率增长?我有哪些要学哪些要放,我要走哪条职业路线?…


实在有太多要学,我给自己的目标是:除去工作所用,还要试图从数学和物理角度,理解背后机理;要紧跟潮流,对新出现的技术和事物保有热切好奇;要外语足够优秀,能无障碍阅读英语文档。…


从没想过,有一天“学习”也会成为甜蜜负担。但我清楚,并不是所有的职业,所有的从业者,都有我们这样可以不断学习不断提高,如果愿意可以练就一身本事的机会。我会在无边学海中偶尔迷失,但却清楚,一样技能可以一陈不变从生到死,那会让我因无成就感和提高的满足而生不如死。


我的感触是,从毕业到30岁,真的太短太短了。只有夜以继日只有目不见睫。几年里,太多的熬夜让我皮肤不复当年紧致滑腻,太多的久坐让我发福增重,或许还有双眼疲劳颈椎酸痛。…我会注意保重,但当解决一个问题,当当搞懂一样东西,那种感觉,像一个木匠做出一件工艺品,抖落浑身木屑站起时的满足。所有的疲乏,所有的周折劳累,都将烟消云散。


回瞰这几年,我总会在他们看不见的地方,感谢B哥L哥H哥,但我还想感谢并告诫自己:我很勇敢,也因而幸运,在一个几百年不遇的信息时代,如果我还因循守旧患得患失,畏葸不前混吃等死,那既是对自己得辜负,也是对时代得辜负。


作者:fliter
来源:juejin.cn/post/7281651969247166527
收起阅读 »

一个艰难就业的23年应届生的2022年

自我介绍 我的家乡是浙江-宁波-余姚,是一名就读于一所位于宁波-慈溪(学校:笑死,这就我一所大学,你直接报我名字得了)的双非独立学院的软件工程专业的23年应届生,7到10月有在南京实习,现在是孤身一人在杭州实习的社恐前端实习生,前端练习时长一年半,擅长唱、跳、...
继续阅读 »

自我介绍


我的家乡是浙江-宁波-余姚,是一名就读于一所位于宁波-慈溪(学校:笑死,这就我一所大学,你直接报我名字得了)的双非独立学院的软件工程专业的23年应届生,7到10月有在南京实习,现在是孤身一人在杭州实习的社恐前端实习生,前端练习时长一年半,擅长唱、跳、rap... 还只擅长Vue的渣渣前端程序猿,有兴趣可以关注我的公众号程序猿青空,23年开始我会时不时分享各种优秀文章、学习资源、学习课程,探索初期,还请多多关照。这篇文章会是我公众号的第一篇文章,主要对我这一年来的经历做一个简单的流水账总结,涉及到恋爱、租房、学习、工作等各方面内容,希望这份经验对你也能有所帮助。


学习


大二下半年的时候分流,自主报名到了我们学校的产业学院——企业和学校联合创办的培养应用型人才的学院。我文科相当薄弱,埋头考研会相当痛苦,也很清楚自己做不来官僚主义那一套,公职也不是适合我的职业(没错我对公职有偏见),很坚定就业这条路。因为还没有毕业,我的身份归根结底就是一个双非下流本科的一名大学生,为了避免自己毕业即失业,看当时产业学院的宣传也不错就去了。


事实上因为产业学院刚创办不久,而且并不是所有人来到这里都是为了就业的,也有可能是为了学分、助学金等其他方面的原因,课程设计、师资力量、同学质量等各方面都良莠不齐、鱼龙混杂。每门课程的期末大作业基本都是一个小项目,大三一年里两个期末都有为了大作业通宵的几天,再加上1500💰凑活过的生活费,死贵的电费和食堂伙食费,在这里学习和生活有时候还蛮辛苦的。好在我很清楚自己应该做什么,天赋不够,努力来凑,本来起跑线就低,更应该比别人卷一点。当然我也不是那种能够没日没夜卷的人(👀),关注了鱼皮,加入了他的知识星球,在星球天天学习健身(没错我还健身💪)打卡的flag没两个礼拜就立不住了,知识付费的事咱也没少干,就是说能一直坚持下来的着实不多,咱也明白咱就是个普通人,逆袭这种事确实还是很难做到的,我这人还是比较佛系的。


大三这一年我用一年的时间从零学前端,自认为还算是没有辜负自己,这一年时间的学习也还算有成果,虽然没法和卷王们争第一,也能跟在他们后面做个万年老二(😭呜呜呜)。下半年开始实习后更别说了,新的技术栈的学习基本就停滞了。实习前我还天真的以为能有更多的时间学习,正相反,比在学校学的更少,因为下班到家七八点,生活琐事会比在学校里多得多,而且我下班后还要花一个多钟头健身,再加上忙碌一天后更无心学习,只想躺平。


下半年做过的最卷的事也就参与了字节青训营,课题选择了前端监控平台,可惜的就是没能在青训营期间完成(😭呜呜呜,队友都摆烂了),当然也就没有结营证书。但我也不甘心就这样算罢,这个项目我就自己拉出来,作为我的毕业设计去完成它。解决实习期间学习效率低的最好办法就是在公司学习一些对公司业务有关或者优化公司项目的知识,名正言顺地摸鱼。我是Vue入门的,这一年里也一直守着Vue,来年第一季度目标就是学习React和Nest,开发一个自己的数据聚合的网站,能变现就最好了(😎欸嘿)。


生活&实习


大三下,也就是今年上半年,为了冲刺暑期实习,也就没去做兼职了,感叹本就艰难的生活的同时,殊不知这是为数不多还能自己自由掌控的日子了(😥我哭死)。其实我开始准备实习还是挺晚了,再加上期末没有太多时间,准备并不是太充分,没有太多自信心,投了几家大厂,不是没回应,就是笔试挂,就有点望而却步。


在我一个大佬同学的介绍下,面试了一家南京的小厂,过程很顺利,实习薪资给的也很可观,当时就没考虑那么多,就选择接受offer了(后来在杭州实习认识了几个小伙伴,才学了没几个月,暑假就面试进了独角兽企业,我那个时候确实应该再多投一投的)。刚开始的想法是第一次出门实习,有份经验就可以,在什么城市没关系,然而事实是工作上确实没什么关系,生活上关系可大了。7月13日第一次一个人拎上行李,义无反顾地去了南京,以为自己终于能够大展拳脚,再不济也能够在公司有所贡献,然而现实总是没那么理想。


上路


因为一个人前往外地工作,第一件事情便是租房,为了省点钱就托南京实习公司的一个同事看房子,因为他的房租到期也要找房子就顺便可以租在一起,有个照应。然而实际上因为是第一次出远门工作和生活,一切和自己的理想差距显然大了许多:因为不是自己实地看的房,而且也是第一次租房,虽然房租只有850💰,但是也可能因为是夏季大家都开空调,差不多50多💰一个礼拜的电费和其他乱七八糟的费用,一个月光租房子就差不多得1200💰,并不算贵,但是性价比极低;我的房间没地方晒衣服,只能晒在那个同事的房间的阳台,作为一个社恐患者,每次去都要做很多心理斗争(他会不会睡了,他会不会在忙....🙃);桌上只能堪堪放下我的显示器和笔记本,鼠标活动范围极小;床应该是睡过好几个租客了,明显的不舒服;吃的方面因为有点水土不服不能随便乱吃,同时也是为了省钱所以选择自己做饭,因此还得购置很多厨具调味品等等,一次性的开销💰不小;回学校的频率比我想象的高,因此来回车费也成为一大负担;当时租房合同是同事代签的,他签了一年,我那时候也不懂也没问,再加上当时换工作离开的比较急,没时间找转租,违约金直接血亏1700💰。


日常挤地铁


生活的种种问题都还能接受或者解决,然而工作方面,因为进入公司的时间段比较特殊再加上疫情影响,在南京实习的三个月里,我始终没有能够在技术上得到足够的提升,再加上与公司和领导的气场不合,使得我在公司整天如坐针毡,甚至有点无所事事(总之就是过的很不开心),虽然有不低的实习薪资,但是我始终没法在那里躺平。因此在中秋决定参与秋招,开始寻找第二份实习工作。


然而今年找工作并不简单,因为频繁发作的疫情,再加上互联网行业这些年的发展,行业的形势非常的严峻,各大公司都削减了HC(head count,人头数,就是最终录用的人数,肯定有小伙伴不懂这个词,我一开始就不懂🤏),作为一个民本23年应届生,在今年的秋招着实很难找到一份理想的工作。那段时间的想法就是尽快找到下一份工作(急急急急急急,我是急急国王),找到一份离家近、工资高、平台大至少满足两个的工作。从9月10日中秋就开始投出第一份简历,到10月19日确定来到杭州的一家四五百人的SaaS公司,这期间投出过几百份简历,得到的回应却寥寥无几,这是一段非常难忘的经历。


这一个月里每一天都在为找工作烦恼,一开始专注于线上面试,却始终的得不到理想工作的认可,持续的碰壁使得开始怀疑自己这些年的学习,自己的选择是不是错了,是不是自己能力确实没法满足他们的要求(被ktv了),后来也决定不放过线下面试的机会,顶着疫情在南京、杭州、家、学校几地频繁奔波,在杭州线下面试的那一天还是顶着自己身体上的各种不适(持续拉肚子,全身酸痛,萎靡不振),仍然要拿出饱满的精神去面对面试,好在当时就获得了面试官也是现在的leader的认可,简直就是久旱逢甘霖,虽然并不是直接发的offer,但是也是十分有信心。杭州比起南京的工作,实习薪资低了很多,但是因为线下面试,对于当时感受到的公司的氛围十分的心动,也就放弃了其他小公司更高薪资的offer,决定了自己的第二份实习工作。


又上路啦


换工作又是换城市,所以又需要租房搬家,购置各种必需品,又是一大笔开销,在还没进公司前始终在担忧自己先择了薪资更低的工作,到时候会不会付出了这么多,结果又远不如预期让自己更痛苦。不过在经过了一个月左右实习后,我在杭州的公司工作的感受让我相信自己的选择没有错。


10月23日我再一次拖着一大堆行李开始了迁徙,本来打算先简单看房子,先回家住几天再自驾,拖着行李回来看房子签合同,所以我把被子等一些大件的行李都寄回家了,但是这次进入杭州后就黄🐎了(之前几地来回跑黄都没黄一下),只能多看几套房子然后就签下来,好在当天就看到一个自己满意的,10几平,押一付一,一个月算上水电差不多也就1300💰,不至于睡大街,但是我没有被子,当时杭州刚开始降温,温度也就个位数,但是买被子太亏了,之后用不上,就买了床毛毯,多盖几件衣服,凑活过了两天(真的凑活,冷的雅痞)。


杭州租的房


11月1日正式入职,正式开启了在杭州的工作生活,有条不紊的入职手续,时长1周的实习生培训,认识了许多和我一起实习的小伙伴,刚进来还赶上公司的双十一活动,让我对未来的工作生活充满希望。


双十一零食自助


第一月开始接触了一些简单的业务,重新开始了健身,第二个月就参与开发了一个简单的项目,还封装了公共组件、开发了简单的提高开发效率的脚手架工具,我终于能够继续有条不紊运转了。


在南京实习的期间除了参加了字节青训营和准备面试而巩固基础外,专业上可以说是没有丝毫提升,不过生活经验确实收获满满,坚定了自己的目标,职业生涯规划更加清晰,为了达到目标去学会自律。这几个月的开销给自己和父母都增添了不小得负担,好在现在稳定下来勉强能够在杭州自给自足,生活重新步入正轨,比起在南京,杭州的生活更加得心应手。但是并不是说南京不好,南京是一个非常优雅的城市,这里有他躺在超市里超乖的猫猫,超治愈


超乖的猫猫


离开南京前我也花时间去好好游玩了两天(去了一些免费的博物馆,景点)。


忘记叫啥地了


比起杭州,我认为南京更适合生活,我只是去到了一个不适合我的公司和因为经验不足吃了不少亏才离开了这个城市。我很珍惜在杭州的这份工作,也非常享受现在忙碌充实的生活,我也希望自己的能力能够不断得到认可,继续探索自己的人生价值。


感情


呜呜呜,鼠鼠该死啊,鼠鼠长了个恋爱脑,但是好在现在穷的雅痞,我还社恐,可以心无旁骛地工作学习(搞💰)。出来实习没几个礼拜就跟在一起一年的女孩子分手了,其实在上半年因为我们对未来规划的分歧就吵过架,她想留在慈溪,而我更向往大城市(当然不止这一点原因啦),那个时候我就很清楚这段感情肯定没法坚持很久,下半年又异地,在各自的城市实习,天天吵架,自然而然就吵分了,累觉不爱。我深知自己不是啥好男人(男人没一个好东西),还没有资本,毕业前绝对要水泥封心(做杭州第一深情)。


其实我家离学校很近,但是从念大学开始还是很少回家了,在学校里没有什么感觉,直到独自出门在外工作才知道在家真好,爸爸妈妈真好(我是妈宝男,呜呜呜😭),看这篇文章的小伙伴不要再随便跟爸爸妈妈撒气了哦。家里的老人只剩下奶奶独自在乡下了,以后一定要多打电话。


展望


在未来的一年中,希望自己能够吸收已经犯过的错误的经验,保质保量地完成未来的各项工作,作为一名程序员最重要的最重要的就是自我驱动,持续学习,通过不断学习才能够在未来的工作中创造更多的价值,以下是我23年的一些计划


学习



  • 这个月先抓紧时间把自己的毕设解决,写复盘的分享博客,之后顺利毕业

  • 上半年学习React,Nest,开发一个数据聚合分享平台,同样做分享

  • 运营自己的博客和各平台账号,不说多少粉丝,能坚持不凉就行,争取每周一个博客

  • 每季至少阅读一本书,学习一个技术栈

  • 坚持自己的每日计划和每月复盘总结(包含年中和年终总结)


工作



  • 因为现在常态化了,不知道今年的就业形势会是什么样的,着实不想再像去年那样被支配了,所以还是希望得到自己满意的薪资的前提下在这里转正,但愿不要出什么幺蛾子吧

  • 继续卷进部门更深层业务,目标负责6个项目

  • 学习更多优化开发效率和质量的技术栈,明年就简单定个两个的目标吧,要求不高


生活



  • 我真的超级想买机车的,但是杭州主城区禁摩,所以先23年下半年花时间考个D照,看情况决定买个机车还是电驴

  • 3月份房租到期了,看房肯定又要放进日程了,看看到时候有没有合租的小伙伴吧,如果有人有兴趣到时候可以分享一下杭州租房经验

  • 健身肯定是要继续的,有一说一我肉体确实没啥天赋(也可能是吃得不够多),健身更多的是一种生活态度吧

  • 我是一个很不喜欢打电话的人,尤其是和长辈,感觉没话聊,但是老人家接到自己孩子的电话,知道孩子过得不错,真的会很开心。明年定个小目标,一个月给奶奶打一通电话。


2022年好像所有人都过的很艰难,或许所有人都想离开浪浪山,但是也不要忘记看看浪浪山的风景,让我们一起加油吧。最后再打个广告,关注公众号程序猿青空,免费领取191本计算机领域黑皮书电子书,更有集赞活动免费挑选精品课程(各个领域的都有),不定期分享各种优秀文章、学习资源、学习课程,能在未来(因为现在还没啥东西)享受更多福利。


作者:CyanSky
来源:juejin.cn/post/7189562801159929915
收起阅读 »

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

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

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


自我叙述


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


入行回顾



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



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




武术梦




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


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


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




歌唱梦




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


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


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


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


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


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


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


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


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


但是我很天真


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


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


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


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


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




玩电脑




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



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


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




学it




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


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


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


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


直到现在我还在it行业里


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


作者:程序员三时
来源:juejin.cn/post/7230351646798643255
收起阅读 »

我竟然突然焦虑,并且迷茫了

【随想录】我尽然突然焦虑,并且迷茫了 「随想录」 这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发或以我为鉴,不去做无谓思想内耗! 最近是怎么了 最近几个朋友,突然询问我,现在应该...
继续阅读 »

【随想录】我尽然突然焦虑,并且迷茫了



「随想录」


这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发以我为鉴,不去做无谓思想内耗



最近是怎么了


最近几个朋友,突然询问我,现在应该怎么学习,将来才会更好的找工作,怕毕业以后没有饭吃,我说我其实也不太清楚,我目前三段实习我都没有找到一份真正意义的好工作,就是那种我喜欢这门领域,并且喜欢公司的氛围,并且到老了还能保持竞争力(莫有35岁危机)。



所以说我真的没有一个准确的答案回复。但是我以为目前的眼光来看一份好工作必备的条件就是,我在这个领域学的越多,我的工资和个人发展瓶颈越高,这份工作是一个持续学习的过程,并且回报和提高是肉眼可见的!



回忆那个时候


其实说实话,这个疑惑我上大一就开始有,但是那个时候是从高考的失落中寻找升学的路径,开始无脑的刷那种考研短视频



(看过可能都知道真的一下子励志的心就有了,但是回到现实生活中,看到身边人的状态~~~没错人就是一个从众的种群,你可能会问你会不会因为大一没有那么努力学习而后悔,但是其实我不会,因为那一年我的经历也是我最开心大学生活,虽然也干了很多被室友做成梗的糗事,但是想一想那不就是青春嘛,要是从小就会很有尺度的为人处世,想一想活着也很累嘛,害,浅浅致敬一下充满快乐和遗憾的青春呀!)


个人看法


哈哈,跑题了。给大家点力量把!前面满满的焦虑。其实我感觉我们都应该感谢我们来到计算机类的专业,从事这方面的学习和研究。


因为计算机的扩展性,不得不说各行各业都开始越来越喜欢我们计算机毕业的大学生(就业方向更加广),我也因为自己会计算机,成功进入一个一本高校以上的教育类公司实习(同时也是这个时候知道了更多优秀学校的毕业年轻人,真正认识到学校的层次带给人的很多东西真正的有差距);



虽然我是二本的学生,但是在亲戚朋友眼里,虽然学校比不上他们的孩子,但是计算机专业也能获得浅浅的也是唯一一点可以骄傲的东西(活在别人嘴这种思考方式肯定不是对的,但是现实就是在父母那里,我们考上什么大学和进入了哪里工作真的是他们在外人的脸面,这种比较情况在大家族或者说农村尤为严重);



技术论打败学校论,计算机专业是在“广义”上为数不多能打破学校出身论的学科,在公司上只要你能干活,公司就愿意要你,这个时候肯定有人diss我,现在培训班出来的很多都找不到工作呀,我的回答只能是:的确,因为这个行业的红利期展示达到了瓶颈期,加上大环境的不理想,会受到一些影响,但是我还是相信会好的,一切都会好的。



做技术既然这样了


关于最近论坛上说“前段已死”“后端当牛做马”“公司磨刀霍霍向测试”......



这个东西怎么说,我想大部分人看到这个都会被这个方向劝退,我从两个角度分析一下,上面说了,真滴卷,简历真滴多,存在过饱和;第二点,希望这个领域新人就不要来了,就是直接劝退,被让人来卷,狭义上少卷一些......



现在就是导致我也不敢给朋友做建议了,因为当他看到这些的时候,和进入工作环境真的不好,我真的怕被喷死


包括现在我的实习,大家看我的朋友圈看出工作环境不错很好,但是和工作的另一面,是不能发的呀,有时候我都笑称自己是“产业工人”(这个词是一个朋友调侃我的)


不行了,在传播焦虑思想,我该被喷死了,现在我给建议都变得很含蓄,因为时代红利期真的看不透,我也不敢说能维持多少年,而且我工作也一般,我不敢耽误大家(哈哈哈,突然想起一句话,一生清贫怎敢入繁华,二袖清风怎敢误佳人,又是emo小文案,都给我开E)


个人总结


本文就是调侃一下现在的环境啊,下面才是重点,只有干活和真话放在后面(印证一个道理:看到最后的才是真朋友才敢给真建议,我也不怕被骂)



心态方面:我们这个年纪就是迷茫的年纪,迷茫是一种正常的状态,因为作为一名成年人你真正在思考你的个人发展的状态,所以请把心放大,放轻松,你迷茫了已经比身边的人强太多了,如果真正焦虑的不能去学习了,去找个朋友聊一聊,实在不行,drink个两三瓶,好好睡一觉,第二天继续干,这摸想,这些都算个啥,没事你还有我,实在不行微我聊一聊,我永远都在,我的朋友!



工作方面:俗话说:女怕入错行,男怕娶错人!(突然发现引用没什么用,哈哈)我们可以多去实践,没错就是去实习,比如你想做前端的工作,你就可以直接去所在的城市(推荐省会去找实习)但是朋友其实实习很难,作为过来人,我能理解你,一个人在陌生的城市而且薪资很可怜,面对大城市的租房和吃饭有很多大坑,你要一一面对,但是在外面我们真要学会保护自己,而且实习生活中经济方面肯定要父母支持,所以一定要和父母好好沟通,其实你会发现我们越长大,和父母相处的时光越短。(我今年小年和十五都没在家过,害,那种心理苦的滋味很不好受)



升学方面:不是每一个都适合考研,不要盲从考研。但是这句话又是矛盾的,在我的实习生涯中,学历问题是一个很重要的问题,我们的工作类型真的不同,还是那句话,学历只是一个门槛,只要你迈入以后看的是你的个人能力。说一句悄悄话,我每天工作,最想的事情就是上学,心想老子考上研,不在干这活了,比你们都强。所以你要想考研,请此刻拿出你的笔,在纸上写下你要考研主要三个理由,你会更好的认识自己,更好选择。



好吧,今天的随想录就这摸多,只是对最近看文章有了灵感写下自己的看法,仅供参考哦!


回答问题


回应个问题:很多朋友问我为什么给这摸无私的建议,这是你经历了很多才得到的,要是分享出去,不是很亏?


(你要这摸问,的确你有卷到我的可能性,快给我爬。哈哈哈)可能是博客圈给的思想把,其实我说不上开源的思想,但是我遇到的人对我都是无私分享自己的经验和自己走过的坑,就是你懂吗,他们对我帮助都很大,他们在我眼里就是伟大的人,所以我也想要跟随他们,做追光的人!(上价值了哦,哈哈)



写在最后


最后一句话,迷茫这个东西,走着走着就清晰了,迷茫的时候,搞一点学习总是没错的。


作者:武师叔
来源:juejin.cn/post/7201752978259378232
收起阅读 »

一线城市or回老家

前言 哈喽~还在纠结是继续一线城市干着,还是回老家发展吗?先带大家回顾一下我工作的经历 19年还在大四的时候,我就去了上海,干起了前端,敲起了代码,刚开始干啥啥不行,整个流程一脸懵逼,过需求、开发、对接口、提测…… 过需求嘛,不就是听一听pm怎么讲; 开发嘛...
继续阅读 »

前言


哈喽~还在纠结是继续一线城市干着,还是回老家发展吗?先带大家回顾一下我工作的经历


19年还在大四的时候,我就去了上海,干起了前端,敲起了代码,刚开始干啥啥不行,整个流程一脸懵逼,过需求、开发、对接口、提测……



  • 过需求嘛,不就是听一听pm怎么讲;

  • 开发嘛,就自己慢慢开发;

  • 对接口嘛,等着后端给呗,慢慢对;

  • 过测试用例嘛,就听听测试怎么讲呗;

  • 提测嘛,主要测试干,有bug我改改呗~


no!现在回想起当时的这些心里所想,简直是大错特错啦!经历过很多事情后,我来给你们整理个干货:



  • 过需求,很重要,不只是听pm说,自己还要审视需求,从技术的角度,让技术实现起来简单,又能满足产品需求,否则最后坑的还是自己,吭哧吭哧的去实现pm提出来的奇奇怪怪的需求~

  • 开发,不单单要开发,还要提前预估好时间,安排好自己的计划,有什么问题,要 delay了,都要提前跟pm说,否则 最后难办的还是你自己,熬大夜的还是自己。。提前跟他们说,提前要时间,提前规划好,我就是不加班的那个崽!

  • 对接口,也不仅仅是对接口,要提前跟后端要接口文档,否则你都不会想到你的后端怎么能给你跟你开发差别这么大的数据格式,尤其是陌生的后端,别问我咋知道的,说多了都是泪o(╥﹏╥)o

  • 过测试用例也要给我好好听,谁知道测试那个货看完需求文档后,理解的跟你开发的有啥区别,没准完全不一样,这时候你要给予反驳,问pm到底是啥,否则你会收到很多奇奇怪怪,每个字都认识,但是结合起来无法理解的bug。。一样也别问我咋知道的0.0

  • 提测,一定一定要自测,确保主体流程通顺,否则被打下来的话,是piapia的被测试打脸


从摆烂到涨知识


经历过初期的摧残之后,我进入到了摆烂期,因为什么都熟悉了,给东西也能做出来了,就日常摆烂了,直到跳槽去了另一家比较新型的互联网公司,接触了好多之前没接触的,干一天学到的东西是上家公司干一年也可能学不到的。


之前每次发包,是自己吭哧吭哧远程服务器,贴个包,现在是Jenkins自动化部署,一点就好;


之前没开发过微信小程序、没用react写过项目,现在天天是uniapp开发的微小和react+hooks的后管……


总之,就感觉学了好多东西,每天都在学习。


2022 放飞


直到2022年三月,上海疫情的到来,开启了在家办公,身边同事也被辞了好多。


2022年6月复工,又开始了正常去公司上班,但是任务很少,几乎没再学到东西了,每天上班就是再跟同事扯皮子。


2022年9月我也被辞了,公司因自己发展原因,辞退了我,然后就计划回老家了,给外面也干了三年了,决定在走之前玩一把,就去了杭州、去了好多之前想去的的地方



回老家安安稳稳


因为自己还养了两只猫,我自己还晕车,总之就很艰难的在2022年九月中旬回了老家,又休息了半个多月,开始找工作,老家的工作真的很不好找,boss、智联都被翻烂了,全聊过了,而且薪资也很低,简直是比之前的一半都低,面试也根本不像一线城市一样那么难,好多还不是双休,就这样艰难的挑挑拣拣,在十月中旬,我入职了,过起了躺平的日子,从来不加班,九点到,六点跑,双休,技术上有很多之前没接触过的,但不怕,慢慢整呗,而且我身兼数职,虽然是前端,但可以帮忙做icon图标,还可以当当测试。


跟之前比,难免有技术上和管理规范上的落差感,但是回老家后的生活相当充实和真实,每天都能吃的爸爸妈妈做的饭,走从小走过的路,虽然钱不多,但是真的幸福感+真实感上升了好多。


2023年我就希望可以安安稳稳,平平安安过着简单的小日子,只要自己觉得快乐就好。


还在犹豫在一线城市打拼还是回老家的友友们,你们也可以看看我的经历,来判断哟,我个人觉得如果不打算在一线城市买房安家的,早点回老家挺好的,安安稳稳,愿大家也可以过自己觉得舒服的日子哟~


作者:wenLi
来源:juejin.cn/post/7187315339846713399
收起阅读 »

终究还是翻车了,人肉运维100 次后

翻车现场 5年前的一个晚上,我接到数据组同事的消息,要求将A用户的磁盘快照共享给B用户。我对这个线上运维工作早已轻车熟路,登录线上服务器仅用了2分钟就完成了。 我继续忙着其他事情,3分钟后,我正要打开新的控制台页面,猛然发现控制台上的“ public = tr...
继续阅读 »

翻车现场


5年前的一个晚上,我接到数据组同事的消息,要求将A用户的磁盘快照共享给B用户。我对这个线上运维工作早已轻车熟路,登录线上服务器仅用了2分钟就完成了。


我继续忙着其他事情,3分钟后,我正要打开新的控制台页面,猛然发现控制台上的“ public = true”。我惊慌地查看磁盘快照状态,发现磁盘快照已经共享给了所有用户。任何用户都可以在自己的快照列表中看到这个快照,并用快照创建新的磁盘,这意味着这些快照数据已经泄露了。这可是公司重要客户的磁盘数据啊!!!!


我心里明白,对于云计算行业,数据安全问题比线上bug还要严重!


我立刻就慌了,心脏砰砰的跳,手也开始颤抖。我心里很忐忑,一开始试图偷偷回滚,纠结之后,最终选择告诉了组长。


我倒吸一口气,一边进行回滚,一边试图平静的说,“我把刚才的快照共享给了所有租户”。瞬间,组长瞪大眼睛了看着我,“回滚了吗,赶紧回滚……”。 我颤抖地编辑SQL,手都麻木了,心脏还在怦怦跳个不停,开始担心这件事的后果。


领导边看我回滚,边小声对我说,“赶紧回滚,下次小心点”,看的出来,组长不想声张,他想先看看影响。


”嗯,好“,我努力嗯了一声,组长没大声骂我,我很感动。本以为回滚了,就没事了。



(后来这家小公司黄了,这是被我干黄的第二家公司,你们干黄了几家?)



然而,这远远没有结束。


原本宁静的办公室突然变得热闹起来,周围的同事们纷纷接到了报警通知。他们“兴高采烈”地讨论着报警的原因,我的注意力也被吸引了过去,听起来似乎与我有关,但我却没有心情去理会他们。


最终,快照被共享 5 分钟后,回滚完成,我长舒一口气,心想幸好我多看了一眼控制台,否则不知道被泄露多久。


与此同时,邻居组的成员钱哥找到了我,问道:“刚才快照计费数据暴涨了,你们这边有做过什么操作吗?”


随后,邻居组的组长王哥也过来了,询问情况如何。


我的组长苦笑着告诉他们:“刚才一个磁盘快照错误地被共享给了所有租户,不过现在已经回滚了。”


邻居组的王哥听后惊愕地说道:“卧槽,谁干的?”他的脸上露出了一丝微笑,似乎是看热闹的微笑。


我实在不知道该怎么说了,苦着脸问他们:“计费数据能回滚吗?”


邻居组的王哥没有回答我的问题,看了我一眼,说:“我叫上老板,先找个会议室讨论一下吧。”


万幸的是这 5分钟里没有用户使用此快照创建磁盘,这意味快照数据没有发生实质性泄露。


至暗时刻


接下来的两天里,我只做了两件事,参加复盘会议和去会议室的路上。这两天是我人生中最难忘的时刻,我尴尬得连脚丫子都能拧成麻花。


我真希望能立刻辞职离开这个地方。”别再鞭尸了,老子不干了,行不行。md,不就是共享个快照嘛!“ 我的心理状态从忐忑变得暴躁~



(每次造成线上故障,我都有类似的想法,我不想干了,不就是个bug吗,不干了,还不行吗?你们有类似想法吗?)



后来我开始后悔 ,为什么不早点下班,九点多还帮同事进行高危的线上操作,我图个啥


对,我图个啥。我脑子被驴踢了,才提出这个人肉运维方案,一周运维十几次,自己坑自己……


背景


2个月前,组长接到一个大客户需求,要求在两个租户之间共享云磁盘数据,当时提出很多个方案,其中包括分布式存储系统提供工具共享两个云磁盘数据等非常复杂的方案。 我当时听到这个需求,就立马想到, 我们的云管理系统可以实现两个租户的资源共享啊,通过给云磁盘打快照、共享快照等,就实现两个云磁盘的数据共享。


当时我非常得意,虽然我对存储并不是很了解,但是我相信我的方案比存储团队的底层方案更加简单且可行性更高。经过与客户的沟通,确定了这个方案能够满足他们的诉求,于是我们定下了这个方案。


由于大客户要的比较急,我改了代码就急匆匆上线,这个需求甚至没有产品参与,当客户需要共享数据时,需要我构造请求参数,在线上服务器上命令行执行共享操作。第一版方案在线上验证非常顺利,客户对这样快速的交付速度非常满意


因为我们使用了开源的框架,资源共享能力是现成的,所以改动起来很快。只不过有一个核弹级feature,我忽略了它的风险。


public = true时,资源将共享给全部用户。“只要不设置这个参数就不会有什么问题。” 这是我的想法,我没有考虑误操作的可能,更没有想到自己会犯下这个错误。


本以为只是低频的一次性操作,没想到后来客户经常性使用。我不得不一次次在线上执行高危操作,刚开始我非常小心谨慎,仔细的检查每个参数,反复确认后才执行命令。


然而,后来我感到这个工作太过枯燥乏味,于是开始集中处理,一次性执行一批操作。随着时间的推移,我越来越熟悉这件事。这种运维操作我两分钟就能完成……之所以这么快,是因为我不再仔细检查参数,只是机械地构造参数,随手执行。正是我松懈的态度导致闯下了大祸,在那个日常性加班的晚上。


后来我开始反思,从需求提出到故障发生前,我有哪些做的不对的地方。我认为有如下问题。




  1. 技术方案不能仅限于提供基本的资源共享能力,还要提供可视页面,提供产品化能力。




  2. 高危接口,一定要严格隔离成 单独的接口,不能和其他接口混合在一起,即使功能类似




  3. 线上重要操作要提供审核能力!或者有double check 的机制!




深刻的反思


任何工作都是有风险的,尤其是程序员无时无刻都在担心发生线上问题,如果不学会保护自己,那么多干一件事就多增加很多风险,增加背锅的风险。


拿我来说,本来这个需求不需要我参与,我提出了一个更简单的方案,高效的响应了大客户需求,是给自己长脸的事情。然而,我犯了一个巨大的错误,之前所做的努力都付之一炬。大领导根本不知道我提出的方案更简洁高效,他只认为我办事不可靠。在复盘会议上,我给大领导留下了非常糟糕的印象。


话说回来,在这个事情上如何保护自己呢?




  1. 技术方案一定要避免人肉运维,对于高危运维操作要求产品提供可视化页面运维。一定要尽全力争取,虽然很多时候,因为排期不足,前端资源不足等原因无法做到。




  2. 如果没有运维页面,等基础能力上线后,继续寻求组长帮助,协调产品提供操作页面,避免一直依赖自己人肉运维去执行高危操作。




  3. 在还没有产品化之前,要求客户或上游同事将所有的需求整理到文档上,使用文档进行沟通交流,记录自己的工作量,留存一份自己的”苦劳“。




  4. 在低频操作,变为高频操作时,不应该压迫自己更加“高效运维”,而是将压力和风险再次传达给产品和组长,让他们意识到我的人肉运维存在极大危险,需要要尽快提供产品化能力。让他们明白:“如果不尽快排期,他们也会承担风险!”




  5. 任何时候,对于线上高危操作,一定要小心谨慎。万万不可麻痹大意!




总之,千万不要独自承担所有的压力和风险。在工作中,我们可以付出辛勤努力,承受一定的风险,但是必须得到相应的回报。



风浪越大,鱼越贵。但是如果大风大浪,鱼还是很便宜,就不要出海了!风险收益要对等



就这个事情来说,每天我都要执行高风险的运维操作,是一种辛苦而不太受重视的工作。尽管如此,我却必须承担着巨大的风险,并自愿地让自己不断追求更高效的人工运维方式。然而结果却让人啼笑皆非,我终究翻车了。实在是可笑。



挣着卖白菜的钱,操着卖白粉的心,这是我的真实写照。



吾日三省吾身、这事能不能不干、这事能不能明天干、这事能不能推给别人干。


程序员不善于沟通,往往通过加班、忍一忍等方式默默地承担了很多苦活、脏活、累活。但是我们要明白,苦活可以,脏活等高风险的活 千万不要自己扛。


你干好十件事不一定传到大领导耳朵里,但是你出了一次线上大事故,他肯定第一时间知道。


好事不出门,坏事传千里。


我们一定要对 高危的人工运维,勇敢说不!


作者:他是程序员
来源:juejin.cn/post/7285673629526753316
收起阅读 »

希望你多出去看看,别活在短视频和文字里!

感谢你阅读本文! 这段时间在校友群里看到一些“混得比较好的”同学发言,类似于“5w的月薪很高吗?”,“我身边年薪六七十w的人不少”之类的话,加上偶尔看到一些“年薪百万很简单”的标题党文章或者视频,其实对于我来说,我根本懒得去考证这些是真是假! 但是我觉得有必要...
继续阅读 »

感谢你阅读本文!


这段时间在校友群里看到一些“混得比较好的”同学发言,类似于“5w的月薪很高吗?”,“我身边年薪六七十w的人不少”之类的话,加上偶尔看到一些“年薪百万很简单”的标题党文章或者视频,其实对于我来说,我根本懒得去考证这些是真是假!


但是我觉得有必要去聊一聊!


一、知识的贫乏


首先在说这个问题之前,我想引用罗翔老师的一句话。


一个知识越贫乏的人,就越有莫名的优越感!


一年多以前,我回老家,和以前的高中女同学出来聊天,彼此聊了聊自己现在的工作,然后他问我,“你现在一个月能赚三四万吧!”,我当时惊呆了,我回她:“瞧你说的,捡黄树叶也要赶上秋天呢”,我反问她你现在多少呢,她说两千八,我继续问,“你觉得工资多少才算高?”,她说最起码5万以上吧!我苦笑答:“我的妈呀,怎么都这么厉害呀!”。


事实是怎样的呢?


我们先不把事情说得太远,“脉脉上人均年薪百万”,“抖音上人手一台劳斯莱斯”这些不在叙述范围内,感情咱也不会那么不要脸去吹!


二、大众才是真相


像我们这种普通二本学校的学生现状应该最能接近真相了,往上不谈双一流,往下不谈专科,据我所知,我校2021毕业的学生,如果继续做软件工程的话,现在一个月能拿两万以上的人没几个,还得是一线城市,在一线城市的大多都是一万多,所以一万多就是一个中位数。


不过要注意,软件工程专业毕业后从事本专业的人是很少的,就拿我们班来说,班上50人,但是从事软件的不超过20个,20个还是比较理想的。


那么就有一部分从事其它职业,一部分待业,一部分考公考编。


软件行业在整个市场来说工资高一点,就业相对来说简单一点,虽然近几年来行情越来越差,但是相比于其他行业来说,还是稍微好一点!


从事其他行业的人来说,如果家里有点关系的人,条件好一点的人,可能去到一个单位里面暂时上班,条件不好的,那就出来随便找一个班上,对于销售型的,在广州深圳,大多都是六七千,小城市的话,五千基本上已经很高了。对于待业的,那基本上没收入,考公考编的一般都回到了小县城,随便找个单位临时上班,一个月也就两千来块!


我们就不去分析双一流,专科,中职这些了,所以整体算下来,我们现在的年轻人的收入是很低的。


三、时代特征


努力在这个社会貌似已经不是一个正能量的词了,仿佛已经是一个调侃的词了,就像现在大多女孩子,他现在不会选择一个很努力的男孩子作为伴侣,因为努力后得到回报是一个概率事件,大多会选择有“存货”的人!当然,并不是人人都这样!


社会的发展就是这样,就像森林里面的狼越来越多,那么捕获到猎物的概率就越来越小,这和努力没多大的关系,这是时代特征!


八九十年代别说考上大学,考上中专谋个职位都不难,而现在一砖头下去都能打中几个研究生已经不是什么稀奇事了。


还有现在的经济形势如此严峻,企业和单位的寒冬一直在降临,无数的人蜂拥而至,导致形势更加紧张,本来在夹缝中已经难以生存了,现在变成了针眼!


所以前段时间网红带货主播李佳琦在网上说:“找找自己的原因,工资涨了没涨,有没有认真工作”,是因为的认知出现了谬误,所以才说出了这种言论,而他的成功完全靠努力吗?你怎么看!


四、这和你有鸡毛关系!


浮躁来自于你的认知水平,在这个信息爆炸的时代,如果不能分辨真假是非,那么就很容易陷入浮躁的状态!


网络上和现实中总是充斥着一股“赚钱很容易”的妖风,他们去编造一些故事,制造一些假象来迷惑人的双眼,如果你的甄别能力不够,那么你就会觉得为啥别人那么厉害,自己为啥混成这样,从而陷入浮躁和迷茫之中,当你进入这个状态后,等待你的要么是镰刀,要么是内耗!


做人过程中的一大蠢事就是自己啥也不是的时候,总是去炫耀自己拥有的那些八竿子打不着的人脉和资源,被那些不知真假的事物去影响,去自我否定,当一个人不能独立去思考问题,不站在现实角度去看待问题的时候,那么是永远不可能获得成长的。


五、最后


现实中,很多人都是很窘迫的,赚到钱的人永远在少数,这是时代特征和个人运气所决定的,努力只占了很小一部分,所以别被互联网上的一些妖风所影响!


这个时代我们虽然能决定的东西很少,事物都充满不确定性,但是依然要如罗曼罗兰说的那样“世界上只有一种英雄主义,看清生活的真相依然热爱生活”,正因为充满不确定性,所以才有“赌”的意义!


作者:刘牌
来源:juejin.cn/post/7289692200161329210
收起阅读 »

入职腾讯两周年的收获与感受

早晨收到了HR助手的提醒,今天是白牙入职腾讯的两周年,两年前选择offer的纠结与入职腾讯时的兴奋还历历在目北漂7年程序猿回看工作选择,但也许因为历经新冠的原因,这两年的时间黏稠而缓慢,仿佛流体,乏味得可怕 今天带小白牙去国际鲜花港玩了一天,小白牙很开心,收...
继续阅读 »

图片


早晨收到了HR助手的提醒,今天是白牙入职腾讯的两周年,两年前选择offer的纠结与入职腾讯时的兴奋还历历在目北漂7年程序猿回看工作选择,但也许因为历经新冠的原因,这两年的时间黏稠而缓慢,仿佛流体,乏味得可怕


图片


今天带小白牙去国际鲜花港玩了一天,小白牙很开心,收拾完,想记录些什么,给时间留下一点记忆,就写一些在腾讯两年收获与感受吧




  • 收获了自信,「出身不好」的白牙也能凭努力跻身头部大厂,原来自己并不是很差,但同样也很清醒,公司确实起了背书的作用,但不能错把平台的能力当自身的能力,时刻保持清醒




  • 经历了一个系统从0到1的建设,见证了系统的不断迭代,虽说是内部系统,但还是有一些用户量的。这种机会挺难得,很多时候都是在现有系统上做一些修修补补




  • 扩展了自己的见识,身边的同事背景都很优秀,我们组13人,2个清华的,2个北邮的,1个中科院的,1个是复旦还是浙大的记不清了,还有一个兰大,其他的没问,但应该都不错。平时工作交集中,可以从他们身上学到很多




  • 更加明白了「主动原则」的重要性,越主动的人收获越多,包括但不限于技术、业务理解、被认可以及自我认可等。以前比较被动,不太想主动去做一些边界模糊或没人要求但做了收益很大的事儿,但总觉得缺少点什么,所以在工作中主动找一些需求外的事做,比如一些提效的事情。在日常的繁忙工作中,我们需要偶尔抬头,想想哪些问题困扰着我们,也许这就是一个工具的出发点




  • 一次性晋升成功,赶上了公司晋升制度调整,由之前的一年两次到一年一次,且考核重点由之前的「武功」到现在的「战功」,也就是「提效创收」,是否解决了业务的痛点。这可能和大家理解的晋升有点不一样,晋升不应该看技术多牛逼吗?之前确实是这样,但当想不明白时,换个角度,站在老板的角度就懂了



    关于晋升,还是有一些感触的,后面再单独分享一下,关注白牙,后续更新不迷路





  • 在做业务开发外,接触了一些简单的大数据开发,比如用 Scala 写 SparkSql,扩展了技术边界




  • 及时找领导反馈与沟通,这算向上管理吧,让领导知道事情的进展以及是否有问题需要他帮忙,别一个憋着。如果和领导有误解或矛盾,一定要及时私下找他沟通,不要一个人"意淫"




  • 逐渐理解领导了,当他的言行或企微上的话让你产生不爽的情绪时,尝试站在他的角度想想,可能就理解了,领导压力很大的,压力都是一层一层往下传递的。比如领导突然把你拉到了一个群,艾特你跟进,别慌,可能他比你进群早不了几分钟,他也很懵。这个时候应该庆幸,领导还能想到你




  • 绩效是比较出来的,可能你也挺优秀的,但在团队不是最优秀的那个,所以5星不是你,也就没必要怀疑自己了




除了一些正面的,也有一些"负面"的



  • 大厂的基建不一定都好用,加入之前,白牙以为大厂的基建肯定很牛逼,用着非常丝滑,确实有这样的系统,但也有用着很烂的系统。整体好,不一定都好。谈恋爱一样适用

  • 部门壁垒很高,白牙所在大团队是腾讯广告,下面拆分很多部门、中心、组,很多事都需要部门合作,广告部门又分在深圳、北京、上海,即使在同一个城市,座位也是分散的,大家大多数的沟通都是线上,有时沟通推动一件事儿真的很难,已读不回、艾特不回,这些都是有的,但其他公司应该也有吧?!记住,已读不回,必有苦衷,如果真的是急事,打电话吧或者工位近直接当面聊吧(在我们这不太现实,不同城市的沟通太多了)


今天小白牙玩了一天,白天没睡觉,这不趁着小白牙睡觉的时间赶紧写的,比较仓促,也比较混乱,想起来就写啥了,如果白牙写的都忘记了也没啥,但白牙希望记住两个字「主动」越主动收获越多,明白这两个字并在工作中践行时,真的挺有魔力的,不信,你试试?


作者:每天晒白牙
来源:juejin.cn/post/7220797267715555383
收起阅读 »

类的布局——方法列表(1)

和成员变量的使用方式一样,都是先初始化对象,再使用。这就容易让人产生一个误区:实例方法和成员变量一样,每个对象独一份,在对象初始化时存储在堆区。类对象的定义:struct class_rw_ext_t { DECLARE_AUTHED_PTR_TEMPL...
继续阅读 »

对于OC的实例方法,用法是先实例化对象,再用实例对象调用方法:

// ObjectA.h
@interface ObjectA : NSObject
@property(nonatomic,assign,readonly)BOOL b;
- (void)funcA;
- (void)funcB:(NSString *)str;
@end

// ObjectA.m
#import "ObjectA.h"
@implementation ObjectA
- (void)funcA {
NSLog(@"方法A");
}
- (void)funcB:(NSString *)str {
NSLog(@"方法B:%@", str);
}
@end

// 调用实例方法
ObjectA *aObj = [[ObjectA alloc] init];
[aObj funcA];

和成员变量的使用方式一样,都是先初始化对象,再使用。这就容易让人产生一个误区:实例方法和成员变量一样,每个对象独一份,在对象初始化时存储在堆区。

事实上,实例方法和成员变量有着本质的区别,成员变量是在对象实例化之后,存储在内存的堆区(即对象所在的地址);而实例方法作为可执行部分,编译之后存储在代码段,实例方法的地址(IMP)、方法名等信息则被存储在类对象中。

类对象的定义:

struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
const char *demangledName;
uint32_t version;
};

struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
explicit_atomic ro_or_rw_ext;

private:
using ro_or_rw_ext_t = objc::PointerUnion;
};
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
};

struct objc_class : objc_object {
// Class ISA; // 继承自 struct objc_object
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
class_rw_t *data() const {
return bits.data();
}
};

用一个图大概描述一下其内存布局:


用lldb大致验证一下:

  • 初始化对象:
  ObjectA *aObj = [[ObjectA alloc] init];
NSLog(@"=====分割线====="); // 此处下断点
  • 获取对象地址:
(lldb) po aObj
  • 获取isa指针:
(lldb) x/1gx 0x282dccdd0
0x282dccdd0: 0x01000001001f14a9 // 0x01000001001f14a9 为优化后的isa指针,要获取真实的isa指针,需进一步处理
(lldb) p/x 0x01000001001f14a9 & 0x0000000ffffffff8ULL
(unsigned long long) $1 = 0x00000001001f14a8 // 0x00000001001f14a8 为真实的isa指针
  • 获取bits:
(lldb) x/5gx 0x00000001001f14a8
0x1001f14a8: 0x00000001001f1480 0x000000020afa07d8
0x1001f14b8: 0x0003000283a88440 0x8018000100000000
0x1001f14c8: 0x8000000282fea4e4 // 0x8000000282fea4e4 即为bits的值
  • 从bits值中获取class_rw_t结构体:
(lldb) p/x 0x8000000282fea4e4 & 0x0f00007ffffffff8UL
(unsigned long) $2 = 0x0000000282fea4e0 // struct class_rw_t指针
  • 获取class_ro_t结构体:
(lldb) x/2gx 0x0000000282fea4e0
0x282fea4e0: 0x0000000080080006 0x00000001001f02c8 // 0x00000001001f02c8 为ro_or_rw_ext联合指针
(lldb) p/x 0x00000001001f02c8 & 0x1
(long) $3 = 0x0000000000000000 // ro_or_rw_ext & 0x1 == 0 ,则 ro_or_rw_ex 的值即为class_ro_t指针
  • 获取baseMethods
(lldb) x/5gx 0x00000001001f02c8
0x1001f02c8: 0x0000000800000080 0x0000000000000009
0x1001f02d8: 0x0000000000000000 0x00000001001ef3d3
0x1001f02e8: 0x00000001001f0238 // 该指针即为baseMethods
  • 查看baseMethods的内容:
// baseMethods为struct method_list_t指针。
// struct method_list_t 结构如下:

template
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
uint32_t entsize() const {
return entsizeAndFlags & ~FlagMask;
}
uint32_t flags() const {
return entsizeAndFlags & FlagMask;
}

ALWAYS_INLINE
Element& getOrEnd(uint32_t i) const {
ASSERT(i <= count);
uint32_t iBytes;
if (os_mul_overflow(i, entsize(), &iBytes))
_objc_fatal("entsize_list_tt overflow: index %" PRIu32 " in list %p with entsize %" PRIu32,
i, this, entsize());
return *PointerModifier::modify(*(List *)this, (Element *)((uint8_t *)this + sizeof(*this) + iBytes));
}

Element& get(uint32_t i) const {
ASSERT(i < count);
return getOrEnd(i);
}
...
};

struct method_list_t : entsize_list_tt {
...
};

乍一看,似乎这里并没有存储任何和方法列表相关的信息,但是细看结构体模版entsize_list_ttgetOrEnd实现就会发现,该模版在结构体内部维护了一个Element类型的数组,其实际结构可以理解为:

struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
Element elements[0];
};

从上面可以看出,该模版作为method_list_t的实现,Element的类型为:method_t

struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
};

到这里,baseMethods的内容便呼之欲出了:

(lldb) x/15gx 0x00000001001f0238			// 以指针的形式读取15条baseMethods的内容
0x1001f0238: 0x000000030000001b 0x00000001001ef035 //0x000000030000001b 为 entsizeAndFlags 和 count 的值
0x1001f0248: 0x00000001001ef444 0x00000001001edd24
0x1001f0258: 0x00000001001ef03b 0x00000001001ef44c
0x1001f0268: 0x00000001001edd50 0x00000001b04f22e9
0x1001f0278: 0x00000001001ef457 0x00000001001eddb4
0x1001f0288: 0x0000000100000020 0x00000001001f1420
0x1001f0298: 0x00000001001ee619 0x00000001001ef45f
0x1001f02a8: 0x0000000100000000
(lldb) p (SEL)0x00000001001ef035 // 读取第1个method_t的name
(SEL) $11 = "funcA"
(lldb) p (char *)0x00000001001ef444 // 读取第1个method_t的types
(char *) $12 = 0x00000001001ef444 "v16@0:8"
(lldb) dis -a 0x00000001001edd24 // 通过反编译指令验证第1个method_t的imp
ObjectDemo`-[ObjectA funcA]:
0x1001edd24 <+0>: sub sp, sp, #0x20
0x1001edd28 <+4>: stp x29, x30, [sp, #0x10]
0x1001edd2c <+8>: add x29, sp, #0x10
0x1001edd30 <+12>: str x0, [sp, #0x8]
0x1001edd34 <+16>: str x1, [sp]
0x1001edd38 <+20>: adrp x0, 3
0x1001edd3c <+24>: add x0, x0, #0x90 ; @
0x1001edd40 <+28>: bl 0x1001ee3a8 ; symbol stub for: NSLog
0x1001edd44 <+32>: ldp x29, x30, [sp, #0x10]
0x1001edd48 <+36>: add sp, sp, #0x20
0x1001edd4c <+40>: ret
(lldb) p (SEL)0x00000001001ef03b // 读取第2个method_t的name
(SEL) $13 = "funcB:"
(lldb) p (char *)0x00000001001ef44c // 读取第2个method_t的types
(char *) $14 = 0x00000001001ef44c "v24@0:8@16"
(lldb) dis -a 0x00000001001edd50 // 通过反编译指令验证第2个method_t的imp
ObjectDemo`-[ObjectA funcB:]:
0x1001edd50 <+0>: sub sp, sp, #0x40
0x1001edd54 <+4>: stp x29, x30, [sp, #0x30]
0x1001edd58 <+8>: add x29, sp, #0x30
0x1001edd5c <+12>: mov x8, x1
0x1001edd60 <+16>: mov x1, x2
0x1001edd64 <+20>: stur x0, [x29, #-0x8]
0x1001edd68 <+24>: stur x8, [x29, #-0x10]
0x1001edd6c <+28>: add x0, sp, #0x18
0x1001edd70 <+32>: str x0, [sp, #0x8]
0x1001edd74 <+36>: mov x8, #0x0
0x1001edd78 <+40>: str x8, [sp, #0x10]
0x1001edd7c <+44>: str xzr, [sp, #0x18]
0x1001edd80 <+48>: bl 0x1001ee42c ; symbol stub for: objc_storeStrong
0x1001edd84 <+52>: ldr x8, [sp, #0x18]
0x1001edd88 <+56>: mov x9, sp
0x1001edd8c <+60>: str x8, [x9]
0x1001edd90 <+64>: adrp x0, 3
0x1001edd94 <+68>: add x0, x0, #0xb0 ; @
0x1001edd98 <+72>: bl 0x1001ee3a8 ; symbol stub for: NSLog
0x1001edd9c <+76>: ldr x0, [sp, #0x8]
0x1001edda0 <+80>: ldr x1, [sp, #0x10]
0x1001edda4 <+84>: bl 0x1001ee42c ; symbol stub for: objc_storeStrong
0x1001edda8 <+88>: ldp x29, x30, [sp, #0x30]
0x1001eddac <+92>: add sp, sp, #0x40
0x1001eddb0 <+96>: ret

用lldb将方法列表的存储位置过一遍后,整个过程清晰了很多,接下来再用代码走一遍:

// 定义 ISA_MASK 用于获取isa指针
#if __arm64__
#if TARGET_OS_EXCLAVEKIT
#define ISA_MASK 0xfffffffffffffff8ULL
#elif __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#define ISA_MASK 0x007ffffffffffff8ULL
#else
#define ISA_MASK 0x0000000ffffffff8ULL
#endif
#endif

// 定义 FAST_DATA_MASK 用于获取class_rw_t
#if TARGET_OS_EXCLAVEKIT
#define FAST_DATA_MASK 0x0000001ffffffff8UL
#elif TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
#define FAST_DATA_MASK 0x0f00007ffffffff8UL
#else
#define FAST_DATA_MASK 0x0f007ffffffffff8UL
#endif

// 定义方法结构体
struct method_t {
SEL sel;
char *types;
IMP imp;
};

// 定义方法列表结构体
struct method_list_t {
uint32_t entsizeAndFlags;
uint32_t count;
struct method_t elements[0];
};

// 从实例对象中获取isa指针
BytePtr _isaForObject(NSObject *obj) {
if (obj == nil) return NULL;
struct _object {
BytePtr isa;
};
struct _object *obj_ptr = (struct _object *)(__bridge void *)obj;
return (BytePtr)((int64_t)obj_ptr->isa & ISA_MASK);
}

// 从isa指针中获取方法列表和方法数
void _methodsWithIsa(BytePtr isa, struct method_t **methods, uint32_t *count) {
// 获取isa中的bits
uintptr_t bits = *((uintptr_t *)(isa + 8/*isa*/ + 8/*superclass*/ + 16/*cache*/));

// 获取 class_rw_t
uintptr_t class_rw_t = bits & FAST_DATA_MASK;

// 获取 ro_or_rw_ext
uintptr_t ro_or_rw_ext = *((uintptr_t *)(class_rw_t + 0x8));

// 获取class_ro_t
// 判断是class_rw_ext_t 还是 class_ro_t
uintptr_t class_ro_t = ro_or_rw_ext;
if (ro_or_rw_ext & 0x1) {
// class_rw_ext_t
class_ro_t = *((uintptr_t *)ro_or_rw_ext);
}

// 获取class_ro_t 中的 baseMethods
uintptr_t baseMethods = *(uintptr_t *)(class_ro_t + 4/*flags*/ + 4/*instanceStart*/ + 4/*instanceSize*/ + 4/*reserved*/ + 8/*ivarLayout*/ + 8/*name*/);

struct method_list_t *_method_list_t = (struct method_list_t *)baseMethods;
*count = _method_list_t->count;
*methods = _method_list_t->elements;
};

// 输出方法信息
void _printMethods(struct method_t *methods, uint32_t count) {
struct method_t *node = methods;
for (int i=0; i printf("=============================\n");
printf("SEL:%s\n", sel_getName(node->sel));
printf("types:%s\n",node->types);
printf("imp:0x%lx\n",(uintptr_t)node->imp);
node++;
}
printf("=============================\n");
}

// 调用
ObjectA *aObj = [[ObjectA alloc] init];

BytePtr a_isa = _isaForObject(aObj);
struct method_t *method = NULL;
uint32_t count;
_methodsWithIsa(a_isa, &method, &count);
_printMethods(method, count);

// 输出如下:
=============================
SEL:funcA
types:v16@0:8
imp:0x1021ddce8
=============================
SEL:funcB:
types:v24@0:8@16
imp:0x1021ddd14
=============================
SEL:b
types:B16@0:8
imp:0x1021ddd78
=============================


收起阅读 »

新手工程师需要知道的 7 件事

今年已经是我从事软件开发行业的第七个年头,我觉得现在是一个好的时机向一些新入行的软件工程师分享一些我认为越早知道越好的知识。希望能对您有所启发。 1. 需求总是会变 也许您已经和团队成员评估了需求的合理性,并且达成了共识。您胸有成竹的认为产品需求不会再发生改变...
继续阅读 »

今年已经是我从事软件开发行业的第七个年头,我觉得现在是一个好的时机向一些新入行的软件工程师分享一些我认为越早知道越好的知识。希望能对您有所启发。


1. 需求总是会变


也许您已经和团队成员评估了需求的合理性,并且达成了共识。您胸有成竹的认为产品需求不会再发生改变。但真相是残酷的,需求总是会因为各种各样的原因发生变化,例如:



  • 您或您的上司有了一个新的想法;

  • 产品的目标群体发生了变化;

  • 攻克某项技术的成本超出了预期;


您需要意识到,正是因为这残酷的真相,我们才会有一个对于软件工程师重要的专业性评判标准:您是否能够编写有更好组织性的代码


这就引出了我们下一个主题。


2. 始终以编写更好组织性代码为目标


新手工程师有时候会执着于快速的实现功能,从而编写出「面条式」的代码,面条式代码是指代码一段段连接在一起,然后当代码规模到达一定程度后,会突然发现无法移动任何代码。任何对代码的改动,都将是一场噩梦。


因此新手工程师要比有经验的工程师更加厌恶需求变动,因为他们需要花更多时间理解和修改代码,有时甚至是需要重写整个模块。


所以,为了节省宝贵的时间,所有的工程师都应该站在更高的层次上,在编写代码前思考代码组织问题,我知道一些优秀的框架已经在语法层面上解决这个问题,但您应该在此基础上做的更好。


更好的代码组织意味着:



  • 更稳定的应用;

  • 更少的 Bug;

  • 更清晰的代码结构;

  • 代码能够更加灵活的变通与扩展;


您可以学习一些 MVC 思想,并将其应用在您的软件开发中。要知道「谋定而后动」在软件开发领域也是适用的。


3. 代码有保质期


相信我,您的代码总会过时,随着您技术水平的增长,功能的不断迭代,总有一天,您会发现有更好的方式可以组织您现有的代码。对于需要长期迭代的产品,放任代码过期,就是我们常说的「技术债务」。


优秀的工程师不会给未来的自己或接手的下一任工作者留下太多债务,这事关开发者的责任心和声誉,需要引起重视。


而且清除技术债务并非只是对产品有好处,在此过程中,您的技术水平也会不断提高。当您意识到代码临近过期,需要改动时,说明您的技术水平相较于以前已经大大提高了。


处理过期代码有两种方式:



  1. 重构:即保障功能接口输入/输出稳定,但内部结构优化的更加易读,易维护;

  2. 完全删了,重头写:您很少有机会这么做,不过当一段代码已经没有人能够理解,维护成本过于高昂时,这么做是必要的;


大多数工程师不重视代码重构工作,他们不了解或不愿意为代码添加单元测试并定期重构代码,这使得他们错失了宝贵的进步机会。作为新手工程师,您应该尽早意识到代码重构的重要性,并尽力劝说您的上级和合作伙伴,在开发计划中预留代码重构的时间。


4. 不要重复造轮子


新手工程师由于在工作初期认领不到有挑战性的工作,于是倾向于在实现功能模块时,即使有第三方库或框架可以使用,也会选择自己去实现。


这一行为的出发点值得肯定,作为工程师,我们都想证明自己的实力,但是优秀工程师的核心能力之一是在于充分调动现有的成熟资源。


很多时候,自己 DIY 的功能模块在健壮性和可靠性上都无法媲美社区成熟的功能模块,贸然使用自己 DIY 的功能模块会为产品添加风险,成熟的开发者应该极力避免。


我建议新手工程师不妨换一种思路,我们都是站在巨人的肩膀上才能看的更远,与其在工作中花大把时间重复造轮子,不如利用业余时间研究成熟轮子的优势,并在工作中发现新的挑战。


5. 文档是非常重要的


这包含了两层意思:



  1. 查阅文档非常重要

  2. 撰写文档非常重要


新手工程师有时会出于种种原因不愿意查阅官方文档,但事实是,查阅官方文档是工程师经常要做的事情。优秀工程师的核心职责是构思如何组织代码实现目标功能,并将其付诸实践,而查阅文档的过程,使我们准确挑选合适「建材」的必备途径。通过经常查阅文档,某个 API 最终会成为您的长期记忆,您的工作效率也会大大提升。


与查阅文档同样重要的是撰写文档,很可惜大多数工程师要么是重视不足,要么是无法撰写易读的文档。而这两者对于一份好文档而言是十分重要的。


文档的阅读者将会是所有对您当下工作感兴趣的人,包括未来的自己。对于一个没有文档或文档没有及时更新的大型项目而言,下场只有死亡。


请积极撰写产品或技术文档吧!


6. 如何面对陌生代码库


无论是刚入职一家公司,还是换了一个项目组,我们总是无法避免接手一个陌生的代码库,新手程序员往往在此时手足无措,不知道如何开始。


对此我的建议是:



  1. 运行该应用,尝试修改一些代码,看看程序是如何运作的;

  2. 分析目录结构,理解代码的组织方式;

  3. 查看项目引入的第三方库,了解它们的作用是什么;

  4. 对于要修改的核心逻辑,试着画一个流程图,搞清楚整个程序的逻辑是什么;


请务必确保您已经基本掌握了代码逻辑,再着手进行改动,否则无疑将增加代码的坏味道,您很有可能会在将来某个意料之外的时刻不得不偿还债务。


当您打算进入一个代码库,着手添加代码时,请务必确保您与原代码库的编码风格保持一致,即使您认为您的代码风格更加合理和优雅。


这么做的好处在于:



  1. 您的代码风格可能并非是最佳的,您在未来可能会对代码风格有新的想法;

  2. 当每个项目的参与者都把自己个性化的代码风格带入代码中时,代码就会变得越发难以阅读,也容易让下一个接手的开发者感到迷惑;


具备一定规模的开发团队,都会考虑统一代码风格,请您务必确保与团队要求的代码风格保持一致。如果您的团队没有统一的代码风格,那么就由您来主导建设吧,这正是您表现的好机会!


除此之外,新手开发者应该坚定「我一定能做到」的信念,我可以坦率的告诉您,在 Web 开发世界里,没有什么神秘的魔法,所有的问题最终都可以被解决,无论是软件开发的新手还是老手,区别只在于解决问题的思路和时间。


这正是软件开发行业的魅力所在,通过自己的努力解决一个棘手的问题,不仅会为所服务的企业解决问题,还能提升自己的专业水平,让心灵得到满足。所以请享受您当下的工作,并勇于解决任何问题。


7. 始终学习,永不止步


软件开发行业的特点之一便是,您需要通过不断学习提升自己在市场的竞争力,而编程世界也总是会有学不完的思想,技术和产品。


您需要清楚地认识到一个残酷的事实,在大多数工作环境中,您获得进步的动力只能来源于自己。很多团队都没有完善的培训机制,也不会为您制定职业发展规划,团队成员疲于奔命完成自己的业绩或是不感兴趣与您讨论前沿的技术话题,所以您更需要保持警惕,寻求不断进步。


请确保您的技术水平总是高于,至少是配的上您的工作年限。否则工作的时间越久,您的市场竞争力会越低。请勇敢的踏出当前的舒适圈。


8. 总结


在本篇文章中,我向您分享了我认为值得刚步入软件开发行业的新手们需要注意的 7 件事。这些事一些是我希望我在刚入行时有人告诉我的,一些是我从事本行业以来的一些经验之谈,感谢您的阅读,也期待您在评论区补充您的经验,让更多人受益。


作者:libinfs
来源:juejin.cn/post/7188488134923911229
收起阅读 »

高考这件“小事”

我是09年参加的安徽高考,14年光阴白驹过隙似在昨天,从安徽农村一路求学经历了多个城市,也有幸去国外见到了世界的繁华,最后定居杭州,总之是通过“知识改变命运”。最近恰好又是一年一度的高考,随着年龄和阅历的增长,对高考又多了一些理解,遂行文以记之,希望也能给大家...
继续阅读 »

我是09年参加的安徽高考,14年光阴白驹过隙似在昨天,从安徽农村一路求学经历了多个城市,也有幸去国外见到了世界的繁华,最后定居杭州,总之是通过“知识改变命运”。最近恰好又是一年一度的高考,随着年龄和阅历的增长,对高考又多了一些理解,遂行文以记之,希望也能给大家在读书方面带来一些启发


image.png


一次公平的机会



这里我们需要认清一个事实,世界本身就不是公平的,虽说“条条大路通罗马”,但是有人就出生在罗马。也没有什么可抱怨的,客观的认清现实,努力就好。



高考也就2天,在人生长河里面真的是小事一桩,但是它的影响却又是那么的深远。


高考,搁在古代也叫科举,“学而优则仕”,其实本质上是上层人为设计出来的让基层通往中层的一种相对公平的通道。 上层的份额占比比较有限,中间隔着中层,上层对基层是比较友好的,“水能载舟亦能覆舟”,上层是知道基层的实际能量的可怕,因此上层需要给到基层切实能跃迁到中层的通道,并且每年切实有一定比例的基层能够跻身中层的行列;让基层感受到有个机会是公平的,是让人能够看到希望的,比如通过教育改变下一代的命运,将来去大城市做个白领;自己能过上电视里的幸福生活等。这种希望简而言之就是 让基层感受到“公平”,能够让所有人站在同一个起跑线,然后通过这个机会给到我们一个相对高一点的起点,然后去竞争下一个希望,这样整个社会才能减少戾气,保持和谐。 在改革开放的同时搭配了一个关键动作恢复高考。“自愿报考,严格考试,择优录取”让最广大的基层群众有机会流动起来,给了大众希望与公平以及机会,最早那批大学生在短时间内相当一部分一路冲到了上层,这属于时代的红利,后来又是教育和高考的内容逐渐规范化,国家开始打造出一个相对公平的学习考核体系。 国家要你学习那些知识是为什么呢?第一、通过学习这些知识的过程,培养你独自思考和解决问题的思维能力。第二、筛选,把孩子们分层筛选出来。除了选拔人才,还能减少社会矛盾,比如孩子们走入社会后,会发现很多岗位明明是个人就能干,但凭什么你去干?人家是博士生,你是高中生,你有啥话可说的?这样最公平,社会矛盾更小。 对待高考,国家应试教育阶段最本质的核心是公平,在国家能够提供的有限高的教育资源下,通过分数,将369等所有家庭当中不差的孩子筛选出来,给予一个更高的平台。国家是需要给基层一个途径,能够最大程度上的接近中层门槛儿。如果说人生分十层,考上985、211,意味着从一层坐电梯到四层,考上普通的一本,意味着做到了三层,这是国家给予的。别的你啥都不用考虑,生而平等的一次努力得来的。总有鼓吹上学无用论的,看明白这层逻辑推导,就明白高考的红利有多大。一个孩子去985,哪怕玩了的四年,将来的朋友圈儿和这四年的见识,都不是一个特别在家自主创业的孩子能比的。总会有那些天赋异禀的孩子不上学也能冒出来,这在今天这个幸存者偏差越来越强烈的社交媒体时代,确实容易被炒作出来。但你知道而且这种人要是上了大学大概率会混的比现在更好。 image.png


有限博弈和无限博弈


在高考前的教育属于有限博弈,局限在国家设定好的一个公平的筛选机制里,本质上是为了维护社会的长久运转,防止社会固化分层,全都是拿分数说话,这套规则是专门儿设定出来的,给予基层一次机会,尽可能缩短进入中层的门槛儿。因为种种原因没有考上大学的孩子或者孩子家长,如果你看到今天这个,我建议不要放弃,再辛苦一点,因为这种阶层跃迁的轻松机会未来不会再有了。但是当我们跨过高考这个门槛儿后,就要端正一个态度,所有的规则全变了,因为国家给你这个机会,是帮你达到门槛儿的,后面你要靠自己了。就像游戏一样,前面的大部分积分都被清零了,所有的孩子都需要端正好自己的态度去迎接后面的人生。 进入大学乃至工作之后,开始到了无限博弈阶段,这个阶段主要考察你的三种能力,不再是解题了



  1. 终身持续学习的兴趣和能力


没有一个牛人是不擅长学习的,没有一个牛人是停止终身学习的。大多数的失败和停滞不前,本质上都是放弃了终身学习和提高。很多草根出身,后面做成大事都是离不开这个阶段的持续学习。



  1. 妥善处理各种各样的人际关系的能力


你将来会面对大量的竞争者、协作者、执法者、帮助者,需要慢慢整理明白他们的社会身份,然后妥善应对,从而趋利避害的让自己利益最大化,借助大家的力量合作共赢



  1. 能抗压,面对失败能勇敢站起来的能力


人生不如意之事十之有八九,利用自身的努力,调集多方的资源去将这个成功的概率提高,然后尽人力,听天命。


比较麻烦的是有限博弈和无限博弈 这两个阶段在教育上其实是背到相驰的。这需要孩子们慢慢转变心态。高考因为有它的具体指正意义,这条赛道对设计的是有限博弈的算法,每道题都有解,有限博弈是在有限的赛道上给你考出无懈可击的分数,有限博弈会让你对每一次的得与失相当看重,对分数产生执念,一考定终身的可怕结果会让孩子们在年少时就会产生输不起的感觉。考不上就觉得人生完蛋了,其实还有下半场。然后在切换到无限博弈赛道里包括考上和考不上的,从此再没有规定的解题动作和规定答案了,这就会导致有很多在应试教育中如鱼得水的孩子不适应。不清楚社会运行的本质,其实把一个事业做成功的复杂度可比解题难度大多了,这玩意儿没有标准答案,没有指定路径,更可怕的是,除此之外,别人家家族可以拉伸手甚至火箭助推的,人家无论是工作支持还是小目标的支持,本质上都是可以祖孙三代齐上阵的,八仙过海各显神通的,这也没啥抱怨的,毕竟人家有着好几代人的积累,哪能被你这一代轻易超越,抱怨是最没用的,得努力改变现状,心态要稳住


image.png


考后人生


高考后的人生通常有两种模式



  1. 一种是跟着已经成熟的、规则明确的赛道往前走.


这个环节即便是同时起跑,那也开始八仙过海,各显神通。有的孩子一年顶别人十年,有的孩子起点就是别人一辈子终点,这都很正常,因为赛道已经成熟了,规则已经定好了,在规定赛道上按部就班好好走,通常也会收获一个还算不错的人生。不过这种规则已经设定了相当明确的赛道更多是为优势者设定的,是为了秩序和稳定,越是这种赛道,越意味着稳定,越意味着没啥运气可言。更重要的是,这种规则和赛道明确的行业,通常不会对人造成太大的负担,更多的是把人变成平台或者组织的零件儿,时间越长,你越离不开他,到时候就会越被这个平台的控制。



  1. 另一种模式是在那些规则不成熟、不明确的赛道上切换.


只有规则和秩序还没有明确形成的时候,或者说机会还都没有被看懂的时候,布局还没形成的时候,才是后来者的机会所在。这条规则适用于创业等等大多领域。比如前十年的刚刚兴起的互联网领域,产生了大量高收入群体,本质上还是因为写代码这事儿,别的家族和既得利群体帮不上忙,所谓的人与人的距离迅速拉开,其实也就是因为变了赛道,要么他机缘巧合的得到了上层相助,跑到了上层赛道上,要么就是找到了时代的风口,提前搭了进去,然后成为了第一波儿吃肉的人,更多的都是能力、眼光、运气三位一体的时势造英雄。 这也是我们基层群众为什么要去北京、上海、深圳这样的城市去呢?只有最大的城市,最多的人口,才会产生出最多的碰撞,从而创造出最多的不对等,然后才有一些新的规则不成熟、不明确的赛道出现。越是小的城镇,越是固化,越是熟人,里面全都是按部就班的人,你如果是个既得利益的群体还好,不是的话,那就真的一眼望到头了。越大的城市,就算他活的有千般辛苦,即便会变成干电池,但最起码不确定性和碰运气的成分更大,能给你提供最大的新赛道,可能见个世面也比小城镇要大的多。


image.png


写在最后


我们考虑正常教育和军备竞赛的最关键点就是两者如何达到平衡的同时,让孩子们赢在无限活力的起跑线上,这是最关键的。如果辅导班儿在家庭条件允许的情况下,能够帮助孩子们培养兴趣。还有个抗挫折的能力,增加社交密度,上那种辅导班是一个挺好的选择。如果孩子对于那些班有其他的抵触。或者家长让孩子上辅导班就是教育焦虑,害怕孩子掉队,就没有一丁点儿意义,更不要说如果那堆辅导班儿影响了家庭的生活质量,那就更是本末倒置。每周带孩子看整个电影,就在外面儿吃顿高质量的饭,在这个过程中呢,孩子感觉到你有多爱他,远远比让他上他不感兴趣的辅导班儿要重要的多。实际上,父母对于子女的教育应该是源源不断的,我们要把我们人生的理解和经验薪火相传,对孩子最好的教育是你成功的榜样,你通过一辈子的言传身教,让他知道成功与幸福的意义。我们让孩子重新学习,我们让孩子对事物充满兴趣和探索。我们自己做的又怎么样呢?我们现在还对啥有兴趣?我们每年都在提高吗?我们希望孩子在人际关系上成为大家能受欢迎的人,但我们自己呢?是哪儿都喜欢我们呢?是不是每天看好多人都不顺眼呢?我们希望孩子不担心失败,勇敢面对挫折,我们自己做又怎么样呢?我们在面对困难的时候就那么一点儿不怂吗?当我们自己找到了生活的兴趣,我们的生活就会璀璨发光,孩子就会知道追逐兴趣终身学习有多么美好,你也能够指导孩子将来如何更好的保持兴趣终身学习,当我们有了温暖的气场和得体的谈吐,有了尊重他人的同理心,孩子自然而然就耳濡目染了。总之,道理都想明白后,人生就不会活的那么拧巴了。


作者:Android茶话会
来源:juejin.cn/post/7244709476518527013
收起阅读 »

前端马农:抢不到消费券,我还不会自己做一张吗

web
前言 最近,政府为了刺激消费,发放了大量的消费券,大家应该参与了抢券大军吧。但是如果你是一个前端程序员,你有没有想过,这个消费券样式我能实现吗?今天给大家分享一下,常用的票券的样式实现。 抽象一下 对我们常用的票券进行抽象后,大概就是下面几种样式了,我们...
继续阅读 »

前言



最近,政府为了刺激消费,发放了大量的消费券,大家应该参与了抢券大军吧。但是如果你是一个前端程序员,你有没有想过,这个消费券样式我能实现吗?今天给大家分享一下,常用的票券的样式实现。



image.png


抽象一下


对我们常用的票券进行抽象后,大概就是下面几种样式了,我们来看看怎么实现吧


image.png


实现方案



对于内凹圆角或者镂空的这类样式,我们一般实现方案是使用mask(遮罩);mask语法很简单,我们就当成background来用就好了,可以是PNG图片、SVG图片、也可以是渐变绘制的图片,同时也支持多图片叠加。然后我们了解一下他的遮罩原理:最终效果只显示不透明的部分,透明部分将不可见,半透明类推。



<1>实现一个内凹圆角


image.png


class="content">

.content {    
width: 300px;    
height: 150px;    
margin: auto;    
-webkit-mask: radial-gradient(circle at left center, transparent 20px, red 0);    
background: red; }
ellipse


当前(2016.10.19)mask 处于 候选标准阶段(CR),还不是正式标准(REC),webkit/blink 内核加前缀 -webkit- 可使用



<2>实现两个内凹圆角之遮罩合成


image.png



  .content{           


    width:300px;           


    height:150px;           


    background:red ;           


   -webkit-mask:radial-gradient(circle at left center,transparent 20px,red 20px)  ,     radial-gradient(circle at right center,transparent 20px,red 20px) ;           


    }


上面的写法是没有效果的,此时使用为两个重合后,整个区域都是不透明的,导致没有效果,这个时候我们需要使用遮罩合成;我们通过ps了解一下遮罩合成




遮罩合成mask-composite


-webkit-mask-composite: destination-in; /只显示重合的地方/



image.png


<3>实现两个内凹圆角之平铺


image.png



 .content{           


         width:300px;           


         height:150px;           


         background:red ;           


          -webkit-mask:radial-gradient(circle at 20px center,transparent 20px,red 20px);           


         -webkit-mask-position: -20px;       


}



<4>实现四个内凹圆角


image.png



 .content{           


       width:300px;           


       height:150px;           


       background:red ;           


       -webkit-mask:radial-gradient(circle at 20px 20px,transparent 20px,red 20px);           


      -webkit-mask-position: -20px -20px;       


}



<5>实现六个内凹圆角


image.png



 .content{           


     width:300px;           


     height:150px;           


     background:red ;           


     -webkit-mask:radial-gradient(circle at 20px 20px,transparent 20px,red 20px);           


     -webkit-mask-position: -20px -20px;           


     -webkit-mask-size:50% 100%;       


}



<6>实现中间一排的镂空


image.png



.content{           


       width:300px;           


       height:150px;           


       background:red;           


        -webkit-mask:           


            radial-gradient(circle at 20px 20px,transparent 20px,red 20px) -20px -20px/50% 100% ,           


            radial-gradient(circle at center 5px,transparent 5px,red 5px) 0px -5px/100% 30px;           


      -webkit-mask-composite: destination-in;        }



<7>实现两边多个内凹圆角


image.png



其实很简单:只需把遮罩的高度,变小,让他们平铺就可以了




 .content{           


       width:300px;           


      height:150px;           


       background:red ;           


      -webkit-mask:radial-gradient(circle at 10px 10px,transparent 10px,red 10px);           


     -webkit-mask-position: -10px 5px;           


     -webkit-mask-size:100% 30px;       


}


作者:我们一起学前端
来源:juejin.cn/post/7155025450043965454
收起阅读 »

注意啦⚠️ 别让正则把你网站搞垮⚠️⚠️⚠️

web
引言 事情起源还得从一个需求讲起, 需求内容如下: 假设有串字符串如下: const str = `Pharmaceuticals progress events. JSON output: { "name": "moyuanjun", "a...
继续阅读 »

引言



事情起源还得从一个需求讲起, 需求内容如下:




  1. 假设有串字符串如下:


const str = `Pharmaceuticals progress events.

JSON output:
{
"name": "moyuanjun",
"age": 28
}`



  1. 现需要从字符串中, 提取到 JSON output: 后面的所有字符串, 后面还需要将其解析为对象(当然这不是本文的重点)



需求本身很简单, 实现起来也容易, 具体方案如下, 那么请问以下实现方法有啥问题呢?



const jsonStr = str.replace(/^(\s|\S|.)*?JSON output:/, '')


由于字符串是 gpt 返回的, 它是不可控的, 这里当字符串为 No, this text is not a transaction event. Therefore, the requested entities cannot be extracted. 时, 通过上文的正则进行匹配时就会导致页面卡住, 这里如果大家好奇的话, 可以尝试将下面代码复制到 浏览器控制台 并执行



'No, this text is not a transaction event. Therefore, the requested entities cannot be extracted.'.replace(/^(\s|\S|.)*?JSON output:/, '')

上面主要问题还是出在正则上, 执行上面正则匹配, 会陷入 回溯 陷阱, 我们可以看下上面正则在 regex101 的测试结果, 从测试结果来看正则的匹配次数是有点夸张的


image.png


下面我们来针对 回溯 问题进行展开....


一、正则引擎


传统正则引擎分为 NFA (非确定性有限状态自动机) 和 DFA(确定性有限状态自动机), 那么, 什么是确定型、非确定型、有限状态以及自动机呢?


确定型与非确定型: 假设有一个字符串 abc 需要匹配, 在没有编写正则表达式的前提下, 就能够确定 字符匹配顺序 的就是确定型, 不能确定字符匹配顺序的则为非确定型


有限状态: 所谓有限, 指的是在有限次数内能够得到结果


自动机: 自动机即自动完成, 在我们设置好匹配规则后由引擎自动完成, 不需要人为干预即为自动


根据上面的解释我们可得知 NFA 引擎和 DFA 引擎的主要区别就在于: 在没有编写正则表达式的前提下, 是否能确定字符执行顺序;, 下面我们来简单介绍下这两种引擎:


1.1 NFA 引擎


NFA(Nondeterministic finite automaton)又名 非确定性有限状态自动机, 主要特点如下:




  1. 表达式驱动: 由要执行的正则表达式进行驱动的算法, 正则引擎从正则表达式起始位置开始, 尝试与文本进行匹配, 如果匹配成功, 都前进一步, 否则文本一直前进到下一个字符, 直到匹配成功




  2. 会记录位置: 当正则表达式需要进行选择时, 它会 选择 一个 路径 进行匹配, 同时会 记录 当前的 位置, 如果选择的路径匹配不成功则需要回退回去, 重新选择一个路径进行尝试, 直到匹配完成, 如果所有可能情况全部匹配不成功, 则本次匹配失败




  3. 单个字符可能会检查多次: 从👆🏻可以看出, 字符串中一个字符可能会被多次匹配到, 因为当一条正则路径不通时, 会进行回退




  4. 支持零宽断言: 因为具有回退功能, 所以可以很容易实现零宽、断言、捕获、反向引用等功能





最后借用 猪哥 制作的一个小动画, 方便大家理解:



klx.pro.dbca29a199c308c6b588170ec4b2b475.gif


1.2 DFA


DFA(Deterministic finite automaton) 又名 确定性有限自动机, 主要特点如下:




  1. 文本驱动: 由要搜索的文本驱动的算法, 文本中的每个字符 DFA 引擎只会查看一次, 简单理解就是对字符串进行一次循环, 每次循环都和正则进行一次匹配, 匹配成功字符串和正则指针都相应的向下移动




  2. DFA 引擎会记得所有的匹配可能, 并且每次匹配都会返回其中 最长的匹配, 这么做的目的是为了让后面的匹配能够更加轻松, 正因为如此字符串 nfa not(nfa|nfa not) 中匹配结果为: nfa not




  3. 优点: 优点很明显, 由于只会会循环一直字符串、并且会提前记住所有可能情况, 所以相对来说匹配效率是很高的




  4. 缺点:





  • 它始终将返回最长匹配结果, 无法控制表达式来改变这个规则

  • 因为需要记住所以可能情况, 所以正则表达式预编译时间会更长, 占用更多内存

  • 没有回退, 所有重复的运算符 都是贪婪 的, 会尽可能匹配更多的内容

  • 因为不存在回退, 所以自然不支持零宽断言、捕获、反向引用等功能



最后借用 猪哥 制作的一个小动画, 方便大家理解:



klx.pro.e3c7e13fda2134e2024171f20eac6986.gif



补充说明: 上面只是对传统的两个正则引擎进行简单介绍, 在 JS 中正则引擎使用的则是 NFA 下面我们也只是对 JS 中的正则、以及 回溯 进行简单介绍, 同时在 regex101 中我们选用的语言则是 PHP, 主要是因为在 PHP 用的也是 NFA 引擎并且在 regex101 下会多一个 Regex Debugger 功能(不知道为什么 JS 没有 😭)



image.png


二、回溯


我们知道, NFA 引擎是用表达式去匹配文本, 而表达式又有若干 分支范围, 一个分支或者范围匹配失败并不意味着最终匹配失败, 正则引擎会进行回退去尝试 下一个 分支或者范围, 这种行为就被称之为 回溯


类比于迷宫, 想象一下, 面前有两条路, 我们选择了一条, 走到尽头发现是条死路, 只好原路返回尝试另一条路, 则这个原路返回的过程就被称之为 回溯, 它在正则中的含义是 吐出已经匹配过的文本, 同时 正则匹配位置也会进行回退


一般的, NFA,如果匹配失败, 会尝试进行 回溯, 因为它并不知道后面还有没有可能匹配成功, 他是蒙在鼓里的, 但是 DFA 从一开始就知道所有的可能匹配, 因为在预编译时就它就已经存储了所以可能情况, 所以正则编写的好坏对 NFA 来说是特别的重要的


引擎会真正按照正则表达式进行匹配, 让你选择达到完全匹配所需的每个步骤, 所以我们必须很谨慎地告诉它, 首先检查哪种选择才能达到您的期望, 你也有机会调整正则表达式, 以最大程度地减少回溯并尽早进行匹配


三、量词


3.1 在 JS 中量词表示要匹配的字符或表达式的数量, 常见的量词有:


字符含义
{n}n 是一个正整数, 匹配了前面一个字符刚好出现了 n
{n,}n 是一个正整数, 匹配前一个字符至少出现了 n
{n,m}n 和 m 都是整数。匹配前面的字符至少 n 次,最多 m 次, 如果 n 或者 m 的值是 0, 这个值被忽略
*匹配前一个表达式 0 次或 多次, 等价于 {0,}
+匹配前面一个表达式 1 次或者 多次, 等价于 {1,}
?匹配前面一个表达式 0 次或者 1 次, 等价于 {0,1}

3.2 贪婪 与 非贪婪


模式描述匹配规则
贪婪模式默认使用量词时就是贪婪模式尽可能多 的匹配内容
非贪婪模式量词后加 ?, 如: *?+???{n,m}?尽可能少 的匹配内容

3.3 贪婪模式下的回溯


现在我们看一个简单例子, 有如下正则 .*c 以及待匹配字符串 bbbcaaaaaaaaa, 下面我们使用 regex101 来进行测试


image.png


这里选择 Debugger 查看整个正则匹配流程(重点看 回溯)


klx.pro.91252cceab2190079775942648d23fb9.gif


从图中可以看出, .* 会优先匹配到所有内容, 然后在匹配字符串 c 时, 只要匹配失败, 字符串匹配位置就会进行回退(吐出一个字符), 然后再次进行匹配, 如此反复直到匹配到字符串 c


3.4 解决办法


针对上文回溯问题, 下面我们来简单优化下正则, 来避免 回溯



  1. 使用非贪婪模式: .*?c


klx.pro.399af59bb48824b11a2c939322d56d9f.gif



  1. 使用反向字符集: [^c]*c


klx.pro.1a8e7451e98a3b007f14ab21c8f29b66.gif


3.5 绝对不用「量词嵌套」


特别特别需要注意的是, 嵌套的量词 将会制造指数级的回溯, 下面我们就以 .*c 以及 (.+)*c 为例, 从 regex101 测试结果来看, 相同匹配字符串 .*c 需要 13 个步骤, (.+)*c 则直接飚到 61144 了, 但最终这两个表达式匹配到的结果却是一样的


image.png


image.png


四、多选分支


已知在 JS 中正则可使用 | 定义多个分支, 例如: x|y 可匹配 x 或者 y,


那么正则在匹配过程中如果遇到多选分支时, 引擎则会按照 从左到右 的顺序检查表达式中的多选分支, 如果某个分支匹配失败, 表达式和字符串都会进行回退(回溯), 然后选择另一个分支进行尝试... 这个过程会不断重复, 直到完成全局匹配,或所有的分支都尝试穷尽为止


4.1 回溯现象


假设有正则表达式 num(1234567890|1234567891) 待匹配字符串如下 num1234567891, 下面我们使用 regex101 来进行测试


image.png


这里选择 Debugger 查看整个正则匹配流程(重点看 回溯)


klx.pro.16e2d1cd7aa3d25ab46e801fb4713b05.gif


4.2 优化手段



  1. 提取多选分支中的必须元素: num123456789(0|1)


klx.pro.6ba0ee6c4bca48555392b6bbbfdf3f8e.gif



  1. 高优先级分支提前: num123456789(1|0)



由于正则引擎遇到分支是按照 从左到右 的顺序, 来选择分支进行匹配的, 所以我们可以通过调整分支的顺序来提高匹配效率



klx.pro.f6014ad5c88171023b1abe63284b4dbe.gif



  1. 使用字符组: num123456789[01]



这里我们还可以使用字符组 [], 和 | 不同的是它不存在分支选择问题, 本质上分支越多, 可能的回溯次数越多, 所以如果可以我们需要尽可能减少分支



klx.pro.825be0b2dc258ba51306126b7ec8df94.gif


五、其他正则优化手段



  • 使用非捕获型括号 (): 如果不需要引用括号内的文本, 请使用非捕获括号, 不但能节省捕获的时间, 而且会减少回溯使用的状态的数量, 从两方面提高速度

  • 不要滥用字符组 []: 不使用只包含一个字符的字符组, 需要付出处理字符组的代价

  • 分析待匹配字符串, 将最可能匹配的分支放在前面

  • 正则进行适当拆分: /最明确的规则/.test() && /更细的规则/.test(str)

  • 必要时可以考虑更换正则引擎, 比如使用 DFA

  • 使用检测工具进行测试, 比如: regex101

  • 使用有明显确定的特征的具体字符、字符组代替通配符, 说白了尽可能描述清楚你的正则


六、回到正文


回到我们最开始的那个正则, 可以优化如下


'No, this text is not a transaction event. Therefore, the requested entities cannot be extracted.'.replace(/^[\s\S]*?JSON output:/, '')

regex101 的测试结果如下, 从测试结果来看前后性能提升可不是一点两点


image.png


七、参考:



作者:墨渊君
来源:juejin.cn/post/7243413799347912760
收起阅读 »

一次移动端性能优化实践

web
背景 使用低代码接入了一个移动端老系统,新增页面(以及部分旧页面)使用低代码体系搭建。功能开发完成后才发现性能问题比较严重,所以进行了一次移动端的性能优化。本文就优化过程进行一个记录。 问题分析 为什么一接入低代码体系性能(主要是加载性能)就出现明显的下降,如...
继续阅读 »

背景


使用低代码接入了一个移动端老系统,新增页面(以及部分旧页面)使用低代码体系搭建。功能开发完成后才发现性能问题比较严重,所以进行了一次移动端的性能优化。本文就优化过程进行一个记录。


问题分析


为什么一接入低代码体系性能(主要是加载性能)就出现明显的下降,如果首屏访问的是低代码页面则更加明显



  • 最主要的原因是比之前额外加载了大量的 js 和 css,初步统计有 10 个 css 和 15 个 js

  • 老系统自身 js 资源过大,依赖包 vendor.js 有 8M 多

  • 低代码体系下,非静态资源的接口请求也成为影响页面渲染的因素。页面必须等待接口获取到 schema 后才由低代码渲染器进行渲染


低代码体系接入


有必要简单说明下低代码体系是如何接入的,这对后面的优化是有直接影响的



  • 低代码体系资源大概分为三方依赖、渲染引擎和组件库资源,都是独立的 npm 库,发布单独的 CDN

  • 三方依赖就是像 react、moment、lodash 等最基础的依赖资源

  • 渲染引擎要想渲染页面,又直接依赖于两个资源

    • 页面 schema:服务端接口返回,schema 本质上是一个 json,描述了一个组件树

    • 组件集合:由 CDN 引入的各个组件库集合,它需要先于页面 schema 加载




静态资源为何影响加载性能


静态资源加载如何影响性能,简单分析下,详细的原理可以参考 MDN



  • HTML 自上而下解析,遇到 script 标签(不带 defer 和 async 属性)就会暂停解析,等待 script 加载和执行完毕后才会继续

  • HTML 解析时如果遇到 css 资源,解析会继续进行。但是在 css 资源加载完成前,页面是不会渲染的,并且如果此时有 JavaScript 正在执行,也会被阻塞

  • 所以 js 或 css 体积越大,则在网络传输、下载、浏览器解析和执行上所花的时间就会相应的增加,而这些时间都是会阻塞页面渲染的

  • js 或者 css 的个数对于渲染的影响,很大程度上取决于项目和浏览器是否支持 http2

    • 如果使用了 http2,则静态资源个数对于加载性能影响不大,除非多到几百个资源

    • 如果还是 http1.1,静态资源个数对于加载有明显影响,因为此时浏览器存在并发限制,大概在 4-6 个左右,即一批次只能发送几个请求,等到请求完成后,再发下一批,是个同步的过程

    • 本项目已经支持 http2,所以优化加载性能的重点还是在减小总的资源体积上




优化指标


用户对于页面性能的感受是主观的,而优化工作则需要客观的数据。
更重要的是,有些优化措施是否有效果,有多少效果是需要数据说明的。举例来说,去除冗余资源几乎是可以预见性能提升。但是做 CDN 合并在移动端能够有多少优化效果,事前其实并不清楚
这里采用 2 种方式作为优化指标



  • 旧版本 chrome(69)的 perfomance

    • 使用这个版本是因为后台数据显示该引擎访问量较多

    • chrome 的 performance 不仅能获取性能数据,也有助于我们分析,找出具体问题



  • 使用 web-vitals 库获得具体的性能数据,主要关注

    • FCP,白屏时间

    • LCP,页面可视区域渲染完成时间




现状


image.png



  • 点击 performance 的刷新按钮,就会自动进行一次页面的加载

    • 建议使用无痕模式,排除其他干扰

    • network 中勾选 Disable cache,虽然最终用户会用到缓存,但在优化非缓存项时,建议先禁用缓存,获取最真实的数据



  • 静态资源的加载大概花了 3.5s

  • 而后续静态资源的解析则一直持续到页面加载完成,大概在 9 秒多

  • 使用 web-vitals 测量的平均数据

    • FCP: 5.5s

    • LCP: 9s




目标



  • performance 页面渲染完成:4s 以内

  • web-vitals 平均数据

    • FCP:3s 以内

    • LCP:4s 以内




如果从绝对性能看,这个目标只能是个中下水平。主要基于以下几点考虑



  • 策略上不会对原系统或者低代码体系进行大刀阔斧的改动

  • 老系统大概就是这么个性能情况,维持这个水平起码不会降低用户体验。作为内部系统,对性能没有极致的要求

  • 考虑到时间成本,性能优化是一项持续性的工作,而实际项目是有时间限制和上线压力的


优化措施


根据以上分析,最重要的就是要减小总的关键资源体积。
低代码体系所需要的直接资源都属于关键资源。因为用户是可能首次直接进入一个低代码页面的(也是本次主要的优化场景)


优化前包分析


CDN 三方库资源直接就能看出哪些是冗余的,或者是公共资源加载了多遍等问题,但是自己的仓库打包后就需要借助 webpack-bundle-analyzer 插件分析了
该项目中有多个 npm 仓库需要分析,这里就举老系统自己的例子,优化前的 bundle 分析图


image.png


三方依赖 vendor.min.js 8MB 左右,项目 JS 800 多 KB,下面分析下最严重的几点



  • 标 ① 部分, @ali_4ever 开头的是富文本依赖,有接近 2M 左右的大小,优化为懒加载

  • 标 ② 部分,echarts5 全量引入了,1M 左右大小,计划优化为按需加载

  • 标 ③ 部分,ali-oss,500 多 KB,ali-oss 不支持按需引入。这里因为多个低代码组件库中也用到了该依赖,所以计划提取为 CDN 作为公共依赖,但是大小还是 500 多 KB,只是去掉了重复加载部分

  • 标 ④ 部分,antd-mobile 加载了两个版本的全量仓库,按照官方推荐,考虑将 antd-mobile-v2 按需加载


一、移除冗余资源



  • 排查 CDN,是否引用了多余的 CDN,比如项目中移动端引用了 PC 端的组件库,引用了已经废弃(迁移)的工具库等等

  • 排查项目 bundle,正常情况下是不可能有冗余资源的,因为如果一点没用到这个库,webpack 也不会将其打包进去

    • 可能存在使用到了一小部分,却打包了整个库的情况,这个属于下一部分按需引入



  • 排查下线上 CDN 是否都使用生产版本或者压缩版本,这点事先没有想到,是在优化过程中意外发现存在非压缩版本


二、按需引入


按需引入即只引入三方库中项目用到的部分。现代的大部分三方库都已经支持 TreeShaking,正常打包即是按需引入。特殊情况在于 CDN、懒加载和一些老的库,这些刚好在项目中都有所实践


按需引入 和 CDN


项目中只用到了 ahooks 中的个别方法,却将整个包作为 CDN 引入,显然是不合理的



  • 需要按需引入的库,是不能使用 CDN 引入的,它们之间是互斥的

    • 因为 CDN 需要配置 external 才能在项目里使用,external 一般是将一个三方库作为整体配置的



  • CDN 自身作为一种优化手段,那是和将静态资源放置在业务服务器对比的。

    • 在该场景下,引入 ahooks CDN 导致 TreeShaking 失效,引入了全量包,同时增加了一次 http 请求,总的来看肯定是得不偿失的

    • 并且最终项目的 bundle 也会发布 CDN



  • 因此去掉了 ahooks 的 CDN,改为直接打进项目 bundle 就行了


按需引入 和 懒加载


在该项目中,echarts 也按需引入了,echarts 的按需引入总体效果就没有 ahooks 那么好了



  • echarts 无论绘制哪种类型图表,都需要引入核心库,就有 100 多 KB 的大小了

  • 所以 echarts 也可以选择懒加载,懒加载会让没有使用 echarts 的页面加载速度变快,但是最终浏览器解析的资源是全量的,可以根据实际情况选择

  • 懒加载 和 按需引入也无法并存。因为懒加载需要动态导入,动态导入 webpack 就没法做静态分析,这是 TreeShaking 的基础,所以就没法按需引入了


利用 babel-import-plugin


有一些老版本的库,可能还不支持按需引入,比方说 antd-mobile-v2,对于这种仓库,可以利用 babel-import-plugin 做按需引入
只需要做一下 babel 配置就行


{
"plugins": [
[
"import",
{
"libraryName": "antd-mobile-v2",
"style": "css"
},
"antd-mobile-v2"
]
]
}



  • 本项目最终没有那么做,因为体积几乎没有减小。对于一个完整的项目,需要使用到的组件是非常多的

  • 对于 antd-mobile 多个版本的问题,最终的优化方案还是合并为最新版,只是开发和测试的工作量大了点

  • 注意点:babel-import-plugin 插件并不能让所有仓库都支持按需。本质上还是三方库做了分包才行


三、懒加载


懒加载的资源不同,也可以分为多种类型



  • 三方库资源懒加载:比如之前说的,某个组件依赖于 echarts,那么就可以懒加载 echarts,只有页面中使用了该组件时才去请求和加载 echarts 依赖

  • 组件懒加载:将整个组件都懒加载,在本项目中没有做组件懒加载

    • 低代码体系下,组件本身不能懒加载,否则 schema 解析到这个组件时找不到会报错

    • 解决方案也可以给组件套一层,实际内容懒加载,导出的组件不懒加载

    • 更重要的原因是组件库本身不大,不是影响性能的关键因素

    • 另外低代码页面本身就是由各个组件拼凑而成,如果将组件都懒加载了,那么页面各个部分都会有 Loading 的中间态,效果不好把控



  • 路由懒加载:本质上它就是组件懒加载的一种,一个组件就是一个路由页面,项目中对于系统不太访问的页面做了路由懒加载


三方库资源懒加载


懒加载依赖也需要分析具体情况,比方说移动端使用了 antd-mobile 作为组件库,这个依赖就完全没必要等使用的时候再加载。因为几乎进入任意一个页面,都需要用到这个资源。什么情况下合适



  • 依赖资源比较大

  • 使用的频率较低,只在个别地方使用了


并且这个三方资源也是分两种情况引入,第一种是以 CDN 的形式外部引入,第二种是直接打包入库,这两种引入方式的懒加载处理是不同的,下面分别举例


CDN 引入的三方资源懒加载


比如低代码组件库中存在一个富文本组件,比较特殊,比较适合使用 CDN 的方式懒加载依赖资源



  • 富文本组件依赖于公司内部的一个富文本编辑器。鉴于富文本的复杂性,所以它的依赖很大,JS+css 将近有 3M 左右。

  • 但是其实只有极少的页面使用到了富文本,对于大多数用户来说,是不需要这个富文本的


下面介绍下具体实现,利用 ahooks 的 useExternal,动态注入 js 或 css 资源(也可以原生实现),封装一个高阶组件,方便调用


type LoadStatus = 'loading' | 'ready' | 'error';
interface LoadOptions {
url: string;
libraryName: string;
cssUrl?: string;
LoadingRender?: () => React.ReactNode;
errorRender?: () => React.ReactNode;
}
export const LazyLoad = (Component, { url, libraryName, cssUrl, LoadingRender, errorRender }: LoadOptions) => {
const LazyCom = (props) => {
const initStatus = typeof window[libraryName] === 'undefined' ? 'loading' : 'ready';
const [loadStatus, setStatus] = useState<LoadStatus>(initStatus);
const jsStatus = useExternal(url, {
keepWhenUnused: true,
});
const cssStatus = useExternal(cssUrl, {
keepWhenUnused: true,
});

useEffect(() => {
if (loadStatus === 'ready' || loadStatus === 'error') {
return;
}
if (jsStatus === 'error' || cssStatus === 'error') {
setStatus('error');
}
if (jsStatus === 'ready' && (cssStatus === 'ready' || cssStatus === 'unset')) {
setStatus('ready');
}
}, [jsStatus, cssStatus, loadStatus]);

const content = useMemo(() => {
switch (loadStatus) {
case 'loading':
return typeof LoadingRender === 'function' ? LoadingRender() : <div>加载中...</div>;
case 'ready':
return <Component {...props} />;
case 'error':
return typeof errorRender === 'function' ? errorRender() : <div>加载失败</div>;
default:
return null;
}
}, [loadStatus]);

return content;
};
return LazyCom;
};

// 使用示例,BaseEditor即需要懒加载的原组件,BaseEditor组件内部直接通过window取相应依赖
export const FormEditor = LazyLoad(BaseEditor, {
url: 'xxxx',
cssUrl: 'xxxxx',
libraryName: 'xxxxxx',
});

打包入 bundle 依赖懒加载


总体思路是一样的,只是这类资源利用 webpack 的 import 动态导入能力,import 动态导入的资源打包时会单独分包,只在使用到时才会加载
具体实现:


export const InnerLazyLoad = (Component, loadResource, LoadingRender?) => {
const LazyCom = (props) => {
const [loaded, setLoaded] = useState(false);
const [LazyResource, setResource] = useState({});

useEffect(() => {
if (loaded) {
return;
}
loadResource().then((resource) => {
setResource(resource);
setLoaded(true);
});
}, [loaded]);
const LoadingNode = typeof LoadingRender === 'function' ? LoadingRender() : <div>...加载中</div>;
return loaded ? <Component {...props} LazyResource={LazyResource} /> : LoadingNode;
};
return LazyCom;
};

// 具体使用
const loadResource = async () => {
// 动态导入的资源会单独分包,在使用到时才会加载
const echarts = await import('echarts/core');
const { PieChart } = await import('echarts/charts');
const { TitleComponent } = await import('echarts/components');
const { CanvasRenderer } = await import('echarts/renderers');
return {
echarts,
PieChart,
TitleComponent,
CanvasRenderer,
};
};

const AgentWork = InnerLazyLoad(BaseAgentWork, loadResource);

路由懒加载


路由懒加载原理和内部资源懒加载类似,分包然后首次进入该页面时才请求页面资源
本项目没有把所有页面都懒加载



  • 页面懒加载后,进入页面前会有一个短暂的加载过程,需要评估影响

  • 还是和通用懒加载一样,使用频率较低、页面 js 又比较大的比较适合懒加载


比如在该项目中



  • 应用上存在部分页面是给第三方使用的,不能通过导航点击到达,直接分享地址给第三方

  • 这些页面使用频率低,而且基本不影响本应用,因为无法通过导航点击切换到达,是通过 url 的形式直接访问,所以加载中的中间态和页面加载一起


路由懒加载的实现,不同框架都有些差异。本项目中只需在路由配置中增加配置项即可开启,就不再阐述具体代码实现


四、合并公共资源


合并公共资源,即不要重复加载相同资源
一般来说打包工具都会做依赖分析,只会打包一份相同路径的引用依赖。但是如果相同依赖分散在多个仓库中就有可能出现重复资源了
比如该项目中,老系统自身和多个组件库都使用了 ali-oss 库实现上传功能,并且还有一些条件使得将其提取为公共 CDN 是利益最大化的



  • ali-oss 打包后 500 多 KB 的大小,已经算是一个不小的包了

  • ali-oss 不支持按需引入,所以引用到它的多个仓库,无论引用了什么功能,都将全量打包入 ali-oss

  • 如果 ali-oss 支持按需引入,就需要计算是提取为公共 CDN 划算,还是将其按需打入各个仓库中划算


实现步骤比较简单



  • 在引用 ali-oss 的仓库配置 external,使仓库本身打包时不打入 ali-oss 依赖

  • 在项目 HTML 中提前引入 ali-oss CDN


五、缓存


静态资源缓存



  • 该项目静态资源使用 CDN+版本号,本身已经支持了缓存。CDN 的缓存时间是通过 Cache-Control 的 s-maxage 字段控制,这是 CDN 特有的字段

  • 如果静态资源是放置在自己的服务器上,需要考虑 http 缓存和缓存更新的事项,这个也是老生常谈的话题,这里不再赘述


如果想要详细了解 http 缓存,推荐看下这篇文章


options 请求缓存


在实际优化过程中发现,该项目的大部分 ajax 请求,都是跨域请求,所以伴随着大量的 options 请求
推动服务端做了这些预检请求的缓存,其原理就是通过 access-control-max-age 响应头设置预检请求的缓存时间


Service Worker


Service Worker 是一项很强大的技术,它能够对网络请求进行缓存和处理,它的最大应用场景是在弱网甚至离线环境下
一旦使用了 Service Worker 技术,用户在首次安装完成后,后续的访问相当于直接在本地读取静态资源,访问速度自然能够得到提升
虽然能够提升使用体验,但是使用 Service Worker 是存在一定限制和风险的



  • 必须运行在 https 协议下,调试时允许在 localhost、127.0.0.1

  • Service Worker 自身不能跨越,即主线程上注册的 Service Worker 必须在当前域名下

  • 一旦被安装成功就永远存在,除非线程被程序主动解除

  • Service Worker 的更新是比较复杂的,如果对其了解不深,建议还是只将不常更新的资源使用 Service Worker 缓存,降低风险


项目中直接使用 workbox(对 Service Worker 做了封装,并提供一些插件),以下为示例代码


主线程上注册 Service Worker


if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('./sw.js')
.then((reg) => {
navigator.serviceWorker.addEventListener('message', (event) => {
// 处理Worker传递的消息逻辑
});
console.log('注册成功:', reg);
})
.catch((err) => {
console.log('注册成功:', err);
});
}

Service Worker 线程处理缓存逻辑


//首先是异常处理
self.addEventListener('error', function (e) {
self.clients.matchAll().then(function (clients) {
if (clients && clients.length) {
clients[0].postMessage({
type: 'ERROR',
msg: e.message || null,
stack: e.error ? e.error.stack : null,
});
}
});
});

self.addEventListener('unhandledrejection', function (e) {
self.clients.matchAll().then(function (clients) {
if (clients && clients.length) {
clients[0].postMessage({
type: 'REJECTION',
msg: e.reason ? e.reason.message : null,
stack: e.reason ? e.reason.stack : null,
});
}
});
});

//然后引入workbox
importScripts('https://g.alicdn.com/kg/workbox/3.3.0/workbox-sw.js');

// 预缓存资源示例,不更新的资源使用预缓存
const resources = ['https://g.alicdn.com/dingding/dingtalk-jsapi/2.10.3/dingtalk.open.js'];

// 预缓存功能
workbox.precaching.precacheAndRoute(resources);

// 图片缓存 使用CacheFirst策略
workbox.routing.registerRoute(
/\.(jpe?g|png)/,
new workbox.strategies.CacheFirst({
cacheName: 'image-runtime-cache',
plugins: [
new workbox.expiration.Plugin({
// 对图片资源缓存 1 天
maxAgeSeconds: 24 * 60 * 60,
// 匹配该策略的图片最多缓存 20 张
maxEntries: 20,
}),
],
})
);

// 需要更新的js和css资源使用staleWhileRevalidate策略
workbox.routing.registerRoute(
new RegExp('https://g.alicdn.com/'),
workbox.strategies.staleWhileRevalidate({
cacheName: 'static-runtime-cache',
plugins: [
new workbox.expiration.Plugin({
maxEntries: 20,
}),
],
})
);


  • 预缓存功能:

    • 正常情况下,Service Worker 是在主程序首次请求时将资源拦截,在之后的请求中根据缓存策略处理

    • 预缓存功能是在 Service Worker 在安装阶段主动发起资源请求,并将其缓存下来

    • 当页面真正发起预缓存当中的资源请求时,资源已经被缓存了,就可以直接使用了

    • 预缓存是使用 Cache Only 策略,即在预缓存主动发起请求并获取缓存后,就只会在缓存中读取资源,不在进行缓存更新,所以适合项目中不更新的静态资源



  • 图片缓存:

    • 图片一般情况下是不更新的,所以采用 Cache First 缓存优先策略

    • 当有缓存时会优先读取缓存,读取成功直接使用本地缓存,不再发起请求

    • 读取失败时再发起网络请求,并将结果更新到缓存中



  • 对于需要更新的 JS 和 CSS

    • 使用 Stale While Revalidate 策略

    • 跟 Cache First 策略比较类似,都是优先返回本地缓存的资源

    • 区别在于 Stale While Revalidate 策略无论在缓存读取是否成功的时候都会发送网络请求更新本地缓存

    • 这是兼顾页面加载速度和缓存更新的策略,相对安全一些




六、其他


以下措施不具备通用性,但是在项目中用到了还是记录下来,仅供参考



  • 页面 schema 接口优化:低代码体系存在页面嵌套,每个页面单独请求自己的 schema,所以在嵌套层级较多的情况下,是以同步解析的顺序请求接口,页面渲染速度较慢,优化为服务端拼装完毕后直接返回

  • 部分接口的请求合并

  • 去除运行时 babel,低代码设计器中存在手写的代码,这部分代码最初在运行时由 babel 转化为 ES5(设计问题),优化为保存时转换


七、项目已经存在的措施



  • 静态资源放在 CDN

  • 启用 http2,并且浏览器支持,这一步很重要,是否使用 http2 对优化措施有直接的影响

  • js 和 css 的代码压缩,并且开启 gzip 压缩

  • 使用字体图标 iconfont 代替图片图标

  • CDN 合并:利用 CDN 的 combo 技术将多个 CDN 合并成一个发送(在 http2 中无明显效果)


最终优化效果



  • performance 表现:页面渲染完成在 3 秒以内


image.png



  • web-vitals 平均数据

    • FCP:2100

    • LCP:2400




参考文章



作者:萌鱼
来源:juejin.cn/post/7288981520946364475
收起阅读 »

从拼夕夕砍一刀链接漫谈微信落地页防封

写在前面 最近v2ex上一个话题火了,大概内容是有一个 好奇 摸鱼的程序员,在分析了拼夕夕发出的砍一刀短链接后,惊呼不可能。 是什么让一个见多识广的程序员如此惊讶呢?问题就出在拼夕夕发出的短链接上,经过测试发现,在微信内打开的短链,会出现二维码的页面,而在p...
继续阅读 »

写在前面


最近v2ex上一个话题火了,大概内容是有一个 好奇 摸鱼的程序员,在分析了拼夕夕发出的砍一刀短链接后,惊呼不可能。


image.png
是什么让一个见多识广的程序员如此惊讶呢?问题就出在拼夕夕发出的短链接上,经过测试发现,在微信内打开的短链,会出现二维码的页面,而在pc端浏览器打开时,则出现的另外一套界面。是什么导致了这样的情况呢?

微信落地页防封


谈到拼多多的短链分享,就不得不提一个很关键的名词微信落地页防封 ,说到微信落地页防封,那就需要知道,在什么情况下,会触发微信的域名拦截机制,一般来说,触发域名拦截有以下几个原因




  • 域名是新购入的老域名,在微信内之前有过违规记录,上过黑名单。




  • 网站流量太大,微信内同一域名被大量分享,比如分享赚类的平台某拼。




  • 诱导分享传播,即便是合法营销活动,也会触发拦截。




  • 网站内容违规,这个不必多说。




  • 被同行恶意举报。




为了让域名活的久一些,微信落地页防封这样的技术就应运而生,主要通过以下几点,来逃避微信的域名拦截机制



  • 大站域名【美团、京东...】

  • 不同主体各自备案域名【鸡蛋不放在一个篮子内】

  • 多级跳转+前置防火墙【通过前置防火墙中转页识别是否是机器扫描】

  • 随机Ip【cdn分发】

  • 图床 + 短链

  • 短链 + 自定义跳转 【稍后详细分析一下这种方式】


拼夕夕的防封技术猜测


经过测试,拼夕夕的防封应该采用的是图床+短链+自定义跳转的方式,接下来就听我一一道来



  • 图床
    图床是oss对象存储的昵称,通常是用来存放图片的,如果是用在防封里,那他其实是将一个html页面上传进了图床内,至于是怎么上传进去的。很简单啊,你只需要有一个阿里云,京东云,腾讯云的账号,购买了oss对象存储服务,设置公共读私有写,就可以访问了,这些不重要,你只需要知道图床所存储的是html就可以了。


我通过chrome的控制台抓取了通过短链转换而来地址,然后抓到了如下请求



  • 短链重定向


image.png
注意看第一个请求,第一个请求就是短链的自定义跳转,短链自定义跳转我们下一节详细去说,通过301重定向,将我们重定向到了图床的地址



  • 图床ua、地域、等判断
    图床内的html包含了对ua、地域、设备类型等的判断,不同的环境所打开的内容是不同的,通过对环境的判断,展示不同的内容去屏蔽微信的扫描,拼夕夕就是通过这样的方式来实现落地页防封的
    下面是我从落地页中拿到的一个函数,虽然我们很难完全还原这个函数,但是通过里面没被混淆的常量比如ke.HUAWEIke.OPPO等不难看出来,这是一个判断当前手机品牌的函数,针对不同的品牌下的浏览器,会做一些特殊的处理。


 const t = e(u().mark(function t (e) {
let r, n, o, i, c, s
return u().wrap(function (t) {
for (; ;) {
switch (t.prev = t.next) {
case 0:
if (r = e.brand,
n = e.payload,
o = a()(n, 'data', {}),
i = a()(n, 'isThirdBrowser'),
c = a()(n, 'data.fastAppDomains', ''),
s = Te(o),
Pe(o),
!i) {
t.next = 8
break
}
return t.abrupt('return')
case 8:
if (r !== ke.HUAWEI) {
t.next = 11
break
}
return t.next = 11,
R(c, {
cTime: s,
data: o
}).catch(fn)
case 11:
if (r !== ke.OPPO) {
t.next = 27
break
}
if (!j(A.OppoLeftScreen, o)) {
t.next = 17
break
}
return t.next = 15,
R(c, {
cTime: s,
data: o
}).catch(fn)
case 15:
case 20:
t.next = 27
break
case 17:
return t.prev = 17,
t.next = 20,
sn(c, {
cTime: s,
data: o
})
case 22:
if (t.prev = 22,
t.t0 = t.catch(17),
!j(A.banBrowserV2, o) && !j(A.oppoQAppPriority, o)) {
t.next = 27
break
}
return t.next = 27,
R(c, {
cTime: s,
data: o
}).catch(fn)
case 27:
if (r !== ke.VIVO) {
t.next = 30
break
}
return t.next = 30,
sn(c, {
cTime: s,
data: o
}).catch(fn)
case 30:
case 'end':
return t.stop()
}
}
}
, t, null, [[17, 22]])
}
))

再注意看接下来的一段代码片段,很明显针对上面获取到的手机品牌,会生成不同的图片,注意看下面混淆过的c函数,x.brandType, brand有品牌的意思,也就是上面函数获取到的手机品牌


o = new Promise((function(t) {
var r, o = document.createElement("img"), i = k(n), c = (f(r = {}, x.brandType, 1),
f(r, E.funcParams, i),
r), u = a()(e.split(","), "0");
o.onload = function(e) {
var r = a()(e, "path[0]") || a()(e, "target")
, n = gn(r);
t({
brand: n,
img: r
})
}
,
o.onerror = function() {
t({
brand: ke.OTHERS
})
}
;
var s = S(u).href;
o.src = m(c, s)
}
)),

得益于落地页开发者优秀的代码命名习惯,通过下面的片段,isWeChatPlatform,isIOSWeChatPlatform这两个字符串让我们知道落地页里面还有针对微信的一些判断,会判断是安卓还是ios微信


n = a()(r, "data", {}),
i = a()(r, "isWeChatPlatform"),
c = a()(r, "isIOSWeChatPlatform"),
f = a()(r, "data.mqCodeKey", ""),
l = a()(r, "data.websiteDomain", "").replace(/\/$/, ""),
p = a()(r, "data.fastAppDomains", ""),
d = v("image_url"),
h = v(f) || location.href,
!d) {
t.next = 15;
break
}

还有落地页内针对UA的判断的实现


((w = t.document),
(x = w ? w.title : ''),
(_ = navigator.userAgent.toLowerCase()),
(S = navigator.platform.toLowerCase()),
(O = !(!S.match('mac') && !S.match('win'))),
(A = _.indexOf('wxdebugger') != -1),
(E = _.indexOf('micromessenger') != -1),
(I = _.indexOf('android') != -1),
(T = _.indexOf('iphone') != -1 || _.indexOf('ipad') != -1),
(P = function () {
const t = _.match(/micromessenger\/(\d+\.\d+\.\d+)/) || _.match(/micromessenger\/(\d+\.\d+)/)
return t ? t[1] : ''

通过上面的代码片段,我们得以窥见拼夕夕落地页的逻辑设计,落地页内,至少实现了下面的能力



  • 针对手机品牌的处理

  • 针对安卓与ios系统的处理

  • 针对是否微信的处理


这些代码进一步的验证了我们的猜想,拼夕夕的确是通过oss内的html动态创建元素来规避微信拦截的!下面是短链智能跳转的一个例子,可以帮助大家更好的理解短链推广的内在逻辑


短链与智能跳转


我们以某平台的功能为例,演示如何通过短链实现自定义的跳转



  • 创建短链接


image.png



  • 配置智能跳转


image.png



  • 智能跳转的规则


可以看到,本身规则就支持按平台,按访问环境,按地域去进行智能跳转了,这也是为什么谷歌会想要将UA的信息进行加密或减少所提供的的信息。


image.png



  • 按地域的实现
    服务器可以看到当前访问的ip,通过ip去反向推断地域

  • 操作系统、访问环境 是通过判断UA来实现


console.log(navigator.userAgent)
// ua内会包含设备的关键信息,如果是微信浏览器内打开的,会携带微信浏览器特有的ua信息
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'

结语



技术本身都是为了解决现实存在的问题,技术没有好坏黑白,但是作为一个技术人,我们能做的就是做任何事情的时候,要坚守心中的底线。君子不立危墙之下,尽量少游走在黑白间的灰色地带。



作者:AprilKroc
来源:juejin.cn/post/7156548454502629384
收起阅读 »

四个有用的Android开发技巧,又来了

大家好,本篇文章会继续给大家分享一些Android常见的开发技巧,希望能对读者有所帮助。 一. 通过堆栈快速定位系统版本 这个地方主要分享大家两个个技巧,通过问题堆栈简快速定位当前系统版本: 1. 快速区分当前系统版本是Android10以下,还是Androi...
继续阅读 »

大家好,本篇文章会继续给大家分享一些Android常见的开发技巧,希望能对读者有所帮助。


一. 通过堆栈快速定位系统版本


这个地方主要分享大家两个个技巧,通过问题堆栈简快速定位当前系统版本:


1. 快速区分当前系统版本是Android10以下,还是Android10及以上;


首先Android10及以上引入了一个新的服务Service:ActivityTaskManagerService,将原本ActivityMangerService原本负责的一些职能拆分给了前者,所以当你的问题堆栈中出现了ActivityTaskManagerService相关的字眼,那肯定是Android10及以上了



大家在Android9及以下的源码中是找不到这个类的。


2. 快速区分当前系统版本是Android12以下,还是Android12及以上;


这个就得借助Looper了,给大家看下Android12上Looper的源码:



Looper分发消息的核心方法loop(),现在会转发给loopOnce()进行处理,这个可是Android12及以上特有的,而Looper又是Android处理消息必要的一环,是咱们问题堆栈的源头祖宗,类似于下面的:



所以这个技巧相信还是非常有必要的:当你从问题堆栈中一看有loopOnce() 这个方法,那必定是Android12无疑了。


二. 实现按钮间距的一种奇特方式


最近看了一个新的项目代码,发现该项目实现按钮之间、按钮与顶部底部之间间距实现了,用了一种我之前没了解过的方式,于是这里分享给大家瞧瞧。


这里就以TextView和屏幕顶部间设置间距为例,初始的效果如下:



接下来我们来进行一步步改造:


1. 首先TextView是有一个自定义的xml背景:


<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:height="70dp"
android:gravity="center_vertical">

<shape>
<solid android:color="#ff0000" />
</shape>
</item>
</layer-list>

核心就是定义了android:heightandroid:gravity这两个属性,来确保我们自定义背景在组件中的高度及居中位置。


2. 其次将布局中TextView的属性调整下:




  1. 首先height属性一定要调整为wrap_content保证最后TextView按钮的高度的测量最终取minHeight设置的属性值和背景设置的高度这两者的最大值



  1. 其次还要设置minHeight最小高度属性,注意一定要比背景设置的高度值大,保证能和屏幕顶部产生边距效果;



  1. 最后要设置字体的位置为垂直居中,保证字体位置和背景不发生错位


经过上面处理,效果就出来了:



其实上下空白的部分都是属于TextView,设置点击事件也会被响应,这算是其中的缺点之一,当前也可能在业务场景中认为这是一种合理表现。


上面实现的逻辑和TextView的测量逻辑密不可分,感兴趣的同学可以看下这块代码,这里就不带大家进行一一分析了:




三. logcat快速查看当前跳转的Activity类信息


忘了是在哪里看到的了,只要日志过滤start u0,就可以看到每次跳转的Activity信息,非常的有帮助,既不需要改动业务层,也不需要麻烦的安装一些插件啥的。


使用时记得将logcat右边的过滤条件置为,否则你就只能在左边切换到系统进程去看了:


这里我们演示下效果:


1. 跳转到Google浏览器



logcat界面会输出:



会打印一些跳转到包名类名等相关信息。


2. 跳转到系统设置界面



logcat输出:



可以说start u0还是相当好用的。


四. 项目gradle配置最好指向同一本地路径


最近开发中经常存在需要一次性检索多个项目的场景,而这样项目的gradle版本都是相同的,没啥区别。但每打开一个项目就得重新走一遍gradle下载流程,下载速度又是蜗牛一样的慢。


所以强烈建议大家,本地提前准备好几个gradle版本,然后通过设置将项目的gradle指向本地已存在好的gradle:



这样项目第一次打开的速度将是非常快的,而且按道理来说相同gradle版本的项目指向同一本地路径,也可以实现缓存共享。猜的


如果项目好好的编译运行着,突然没网了,可能会提示一些找不到依赖库资源啥的,其实你本地都已经缓存好依赖库资源了,只需要设置下off-mode,不走网络直接通过本地资源编译运行即可



总结


本篇文章主要是介绍了Android开发一些技巧,感觉都是项目中挺常用到的,算是我最近一个月收获的吧,后续准备研究研究compose了,毕竟看到大家们都在搞这个,羡慕的口水都流了一地了哈哈。


历史文章


两个Kotlin优化小技巧,你绝对用的上


Kotlin1.9.0-Beta,它来了!!


Kotlin1.8新增特性,进来了解一下


聊聊Kotlin1.7.0版本提供的一些特性


聊聊kotlin1.5和1.6版本提供的一些新特性


优化@BuilderInference注解,Kotlin高版本下了这些“毒手”!


@JvmDefaultWithCompatibility优化小技巧,了解一下~


作者:长安皈故里
来源:juejin.cn/post/7250080519069007933
收起阅读 »

我做梦都想不到😵,我被if(x)摆了一道!

web
读本文,可以收获什么? 字数:2494 花费时间:5min if(x)中的x为各种类型的数据或者值的时候会发生什么?x为各种数据类型、表达式、特殊值、位运算、函数等值的时,这些在if语句都充当了什么,实现了什么? 总结== === ≠三种情况特殊值的比较,如...
继续阅读 »


读本文,可以收获什么?


字数:2494 花费时间:5min


if(x)中的x为各种类型的数据或者值的时候会发生什么?x为各种数据类型、表达式、特殊值、位运算、函数等值的时,这些在if语句都充当了什么,实现了什么?


总结== === ≠三种情况特殊值的比较,如下图所示:



image.png


作为一个程序员的我们,相信我们写代码用的最多逻辑应该就是if语句了吧,其实我们真的了解if(x)究竟发生了什么?其实很简单,我们可能都知道中文有这样一个模板:"如果是什么,就会做什么",也就是说符合条件的某件事,才会去做某件事。同样的道理if(x)的意思就是如果符合x条件,我们就可以执行if语句块的代码了。而我们JavaScript中的哪个数据类型是涉及是否意思的?当然是Boolean类型啦,其实if内的x非布尔值都会做一次Boolean类型的转换的


1 x为一个值时


1.1 x为字符串:


x为一个空字符串时,这是一个假值,if语句会转换为false。


if ("") {
console.log("Hello World!");
}
console.log(Boolean(""));// false

x为一个非空字符串是,这是一个真值。if语句会转换为true。


if (!"") {
console.log("Hello World!");// Hello World!
}
console.log(Boolean(!""));// true

x为一个空格字符串,这是一个真值。if语句会转换为true。否则会转换为false


if (" ") {
console.log("Hello World!");// Hello World!
}
console.log(Boolean(" "));// true
if (!" ") {
console.log("Hello World!");
}
console.log(Boolean(!" "));// false

x为一个字符串,这是一个真值。if语句会转换为true。否则会转换为false


if ("JavaScript") {
console.log("Hello World!");// Hello World!
}
console.log(Boolean("JavaScript"));// true

if (!"JavaScript") {
console.log("Hello World!");
}
console.log(Boolean(!"JavaScript"));// false

1.2 x为数字


x为一个数字0时,这是一个假值,if语句会转换为false。x为一个数字!0时,这是一个真值,if语句会转换为true。


if (0) {
console.log("Hello World")
}
console.log(Boolean(0));// fasle

if (!0) {
console.log("Hello World");// Hello World
}
console.log(Boolean(!0));// true

if (1) {
console.log("Hello World") // Hello World
}
console.log(Boolean(1));// true

if (!1) {
console.log("Hello World")
}
console.log(Boolean(!1));// false

if (-0) {
console.log("Hello World")
}
console.log(Boolean(-0));// fasle
if (!-0) {
console.log("Hello World");// Hello World
}
console.log(Boolean(!-0));// true

1.3 x为数组


x为一个空数组,这是一个真值,if语句会转换为true。


if ([]) {
console.log("Hello World");// Hello World
}
console.log(Boolean([]));// true
if (![]) {
console.log("Hello World");
}
console.log(Boolean(![]));// false

x为一个嵌套空数组时,这是一个真值,if语句会转换为true。否则是假值。if语句会转换为false


if ([[]]) {
console.log("Hello World");// Hello World
}
console.log(Boolean([[]]));// true
if (![[]]) {
console.log("Hello World");
}
console.log(Boolean(![[]]));// false

x为一个有空字符串的数组时,这是一个真值,if语句会转换为true。否则是假值。if语句会转换为false。


if ([""]) {
console.log("Hello World");// Hello World
}
console.log(Boolean([""]));// true
if (![""]) {
console.log("Hello World");
}
console.log(Boolean(![""]));// false

x为一个有数字0的数组时,这是一个真值,if语句会转换为true。否则是假值。if语句会转换为false。


if ([0]) {
console.log("Hello World");// Hello World
}
console.log(Boolean([0]));// true
if (![0]) {
console.log("Hello World");
}
console.log(Boolean(![0]));// false

1.4 x为对象:


if ({}) {
console.log("Hello World") // Hello World
}
console.log(Boolean({}));// true

2 x为特殊值时


if (null) {
console.log("Hello World");
}
console.log(Boolean(null));// false

if (undefined) {
console.log("Hello World");
}
console.log(Boolean(undefined));// false

if (NaN) {
console.log("Hello World");
}
console.log(Boolean(NaN));// false

3 x为位运算时


if (true | false) {
// 按位或,只要有一个成立就为true
console.log("Hello World");
}
console.log(Boolean(true | false));// true

4 x为表达式时


比较的相方首先调用ToPrimitive(内部函数,不能自行调用)转换为原始值,如果出现非字符串,就根据ToNumber规则将双方强制转换为数字来进行比较。


const a = [42];
const b = ["43"];
console.log(a < b);// true

5 x为等式时


5.1 一个等号(=)


=: 一个等号代表的是赋值,即使x的值为a=2,也就是说变量的声明操作放在if判断位置上了,其实它还是一个变量并不是一个操作。


let a;
if (a = 2) {
console.log("条件成立!");// 条件成立!
}
console.log(a);// 2

let a;
if (a = 2) {
console.log("条件成立!");// 条件成立!
}
console.log(typeof (a = 2));// number
console.log(Boolean(a = 2));// true

let a;
if (a = 2 && (a = 3)) {
console.log("条件成立!");// 条件成立!
}

console.log(typeof (a = 2 && (a = 3)));// number;

5.2 两个等号(==)


==:宽松相等,我们可能都会这样想,==检查值是否相等,听起来蛮有道理,但不准确,真正的含义是==允许相等比较重进行强制类型转换



对于==符号尽量遵守两个原则:


如果两边的值中有true或者false,千万不要使用==


如果两边的值中有[]、""、0,尽量不要使用==





  • 两个值类型相同,则执行严格相等变量。


    🍟 都是字符串类型:


    const a = "";
    const b = "12";
    console.log(a == b);// false

    🍟 都是NaN类型:全称为not a number,理解为不是一个数值。JavaScript的规定, NaN表示的是非数字, 那么这个非数字可以是不同的数字,因此 NaN 不等于 NaN。


    const a = NaN;
    const b = NaN;
    console.log(a == b);// false

    🍟 都是Symbol类型:Symbol命名的属性都是独 一无二的,可以唯一标识变量值,不受是否相同变量值。


    const a = Symbol("1");
    const b = Symbol("1");
    console.log(a == b);// false

    🍟 都是对象类型。对象的比较是内存地址,因为对象是存储在堆中,当堆中有对象时,它会相对应内存中有一个存储的地址,在栈中其存储了其在堆中数据的地址,当调用数据时,去堆中调取对应堆中的数据的地址获取出来。也就是相同对象比较的是内存地址,变量不一样存储位置不一样。


    const a = { a: 1 };
    const b = {};
    console.log(a == b);// false

    const a = {};
    const b = {};
    console.log(a == b);// false
    console.log(Boolean(a));// true



  • 两个值类型不相同。


    🍟 一个值是null,一个是undefind。


    const a = undefined;
    const b = null;
    console.log(a == b);// true

    🍟 一个值是数字,一个值是字符串。字符串强制转换为数字在比较。


    const a = 12;
    const b = "12";
    console.log(a == b);// true

    🍟 一个值是布尔值,一个是其他类型的值。这种做法是不安全,不建议去使用,在开发中尽量不要这样使用。


    console.log("0" == false);// true
    console.log("" == false);// true
    console.log(0 == false);// true
    console.log([] == false);// true

    🍟 一个值是对象,一个值是字符串或数字。对象与非对象的比较,对象会被强制转换原始值(通过内部函数 ToPrimitive自动执行,这个是内部函数不能直接调用)再比较。


    const a = {};
    const b = "";
    console.log(a == b);// false



5.3 三个等号(===)


===: 严格相等,我们可能都会这样想,===检查值和类型是否相等,听起来蛮有道理,但不准确,真正的含义是===不允许相等比较重进行强制类型转换,也就是不做任何处理变量是什么就是什么。


const a = 0;
const b = "0";
console.log(a === b);// false

6 x为&&、||操作时


||和&&首先会对第一个操作数(a和c)执行条件判断,如果其不是布尔值(如上例)就先进行ToBoolean强制类型转换,然后再执行条件判断。


🍟 对于||来说,如果条件判断结果为true就返回第一个操作数(a和c)的值,如果为false就返回第二个操作数(b)的值。


const a = 12;
const b = "abc";
const c = null;
if (a || b) {
console.log("a||b");// a||b
}
console.log(typeof (a || b));// number
console.log(Boolean(a || b));// true
console.log(a || b);// 12

const b = "abc";
const c = null;
if (c || b) {
console.log("c||b");// c||b
}
console.log(typeof (c || b));// string
console.log(Boolean(c || b));// true
console.log(c || b);// abc

🍟 &&则相反,如果条件判断结果为true就返回第二个操作数(b)的值如果为false就返回第一个操作数(a和c)的值。


const a = 12;
const b = "abc";
if (a && b) {
console.log("a&&b");// a&&b
}
console.log(typeof (a && b));// string
console.log(Boolean(a && b));// true
console.log(a && b);// abc

const b = "abc";
const c = null;
if (c && b) {
console.log("c&&b");
}
console.log(typeof (c && b));// object
console.log(Boolean(c && b));// false
console.log(c && b);// null

7 x为函数判断时




  • typeof与instanceof的区别


    🍟 typeof:返回值是一个字符串,用来说明变量的数据类型。一般只能返回如下几个结果:number、string、function、object、undefined,对于Array、Null等特殊对象typeof一律返回object,这正是typeof的局限性。


    console.log(typeof undefined == 'undefined');// true
    console.log(typeof null);// object

    🍟instanceof:返回值为布尔值,用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性。用于判断一个变量是否某个对象的实例。,注意地,instanceof只能用来判断对象和函数,不能用来判断字符串和数字等


    const arr = new Array()
    if (arr instanceof Array) {
    console.log("arr instanceof Array");// arr instanceof Array
    }
    if (arr instanceof Object) {
    // 因为Array是Object的子类
    console.log("arr instanceof Object");// arr instanceof Array
    }
    console.log(typeof (arr instanceof Array));// boolean

    🍟 typeofinstanceof都有一定的弊端,并不能满足所有场景的需求。如果需要通用检测数据类型,可以使用Object.prototype.toString.call()方法:


    Object.prototype.toString.call({});// "[object Object]"
    Object.prototype.toString.call([]); // "[object Array]"
    Object.prototype.toString.call(666); // "[object Number]"
    Object.prototype.toString.call("xxx"); // "[object String]"



注意,该方法返回的是一个格式为"[object Object]"的字符串。




  • indexof与includes区别


    🍟 indexof:返回的是所含元素的下标,注意地,此函数是无法判断是否有NaN元素


    const str = "130212";
    if (str.indexOf("0")) {
    console.log("str中存在0!")
    }
    console.log(str.indexOf("0"));// 2

    🍟 includes:返回的是布尔值,代表是否存在此元素。


    const str = "130212";
    if (str.includes("0")) {
    console.log("str中存在0!")
    }
    console.log(str.includes("0"));// true



作者:路灯下的光
来源:juejin.cn/post/7154647954840616996
收起阅读 »

什么情况下Activity会被杀掉呢?

首先一个报错来作为开篇: Caused by androidx.fragment.app.Fragment$InstantiationException Unable to instantiate fragment xxx: could not find Fr...
继续阅读 »

首先一个报错来作为开篇:


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。


参考资料:
Android lowmemorykiller分析
解读Android进程优先级ADJ算法
http://www.jianshu.com/p/3233c33f6…
juejin.cn/post/706306…
Android可见APP的不可见任务栈(TaskRecord)销毁分析


作者:Yocn
来源:juejin.cn/post/7231742100844871736
收起阅读 »

当你按下方向键,电视是如何寻找下一个焦点的

我工作的第一家公司主要做的是一个在智能电视上面运行的APP,其实就是一个安卓APP,也是混合开发的应用,里面很多页面是H5开发的。 电视我们都知道,是通过遥控器来操作的,没有鼠标也不能触屏,所以“点击”的操作变成了按遥控器的“上下左右确定”键,那么必然需要一个...
继续阅读 »

我工作的第一家公司主要做的是一个在智能电视上面运行的APP,其实就是一个安卓APP,也是混合开发的应用,里面很多页面是H5开发的。


电视我们都知道,是通过遥控器来操作的,没有鼠标也不能触屏,所以“点击”的操作变成了按遥控器的“上下左右确定”键,那么必然需要一个“焦点”来告诉用户当前聚焦在哪里。


当时开发页面使用的是一个前人开发的焦点库,这个库会自己监听方向键并且自动计算下一个聚焦的元素。


为什么时隔多年会突然想起这个呢,其实是因为最近在给我开源的思维导图添加方向键导航的功能时,想到其实和电视聚焦功能很类似,都是按方向键,来计算并且自动聚焦到下一个元素或节点:



那么如何寻找下一个焦点呢,结合我当时用的焦点库的原理,接下来实现一下。


1.最简单的算法


第一种算法最简单,根据方向先找出当前节点该方向所有的其他节点,然后再找出直线距离最近的一个,比如当按下了左方向键,下面这些节点都是符合要求的节点:



从中选出最近的一个即为下一个聚焦节点。


节点的位置信息示意如下:



focus(dir) {
// 当前聚焦的节点
let currentActiveNode = this.mindMap.renderer.activeNodeList[0]
// 当前聚焦节点的位置信息
let currentActiveNodeRect = this.getNodeRect(currentActiveNode)
// 寻找的下一个聚焦节点
let targetNode = null
let targetDis = Infinity
// 保存并维护距离最近的节点
let checkNodeDis = (rect, node) => {
let dis = this.getDistance(currentActiveNodeRect, rect)
if (dis < targetDis) {
targetNode = node
targetDis = dis
}
}
// 1.最简单的算法
this.getFocusNodeBySimpleAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})
// 找到了则让目标节点聚焦
if (targetNode) {
targetNode.active()
}
}

无论哪种算法,都是先找出所有符合要求的节点,然后再从中找出和当前聚焦节点距离最近的节点,所以维护最近距离节点的函数是可以复用的,通过参数的形式传给具体的计算函数。


// 1.最简单的算法
getFocusNodeBySimpleAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
}
) {
// 遍历思维导图节点树
bfsWalk(this.mindMap.renderer.root, node => {
// 跳过当前聚焦的节点
if (node === currentActiveNode) return
// 当前遍历到的节点的位置信息
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
let match = false
// 按下了左方向键
if (dir === 'Left') {
// 判断节点是否在当前节点的左侧
match = right <= currentActiveNodeRect.left
// 按下了右方向键
} else if (dir === 'Right') {
// 判断节点是否在当前节点的右侧
match = left >= currentActiveNodeRect.right
// 按下了上方向键
} else if (dir === 'Up') {
// 判断节点是否在当前节点的上面
match = bottom <= currentActiveNodeRect.top
// 按下了下方向键
} else if (dir === 'Down') {
// 判断节点是否在当前节点的下面
match = top >= currentActiveNodeRect.bottom
}
// 符合要求,判断是否是最近的节点
if (match) {
checkNodeDis(rect, node)
}
})
}

效果如下:


基本可以工作,但是可以看到有个很大的缺点,比如按上键,我们预期的应该是聚焦到上面的兄弟节点上,但是实际上聚焦到的是子节点:



因为这个子节点确实是在当前节点上面,且距离最近的,那么怎么解决这个问题呢,接下来看看第二种算法。


2.阴影算法


该算法也是分别处理四个方向,但是和前面的第一种算法相比,额外要求节点在指定方向上的延伸需要存在交叉,延伸处可以想象成是节点的阴影,也就是名字的由来:



找出所有存在交叉的节点后也是从中找出距离最近的一个节点作为下一个聚焦节点,修改focus方法,改成使用阴影算法:


focus(dir) {
// 当前聚焦的节点
let currentActiveNode = this.mindMap.renderer.activeNodeList[0]
// 当前聚焦节点的位置信息
let currentActiveNodeRect = this.getNodeRect(currentActiveNode)
// 寻找的下一个聚焦节点
// ...
// 保存并维护距离最近的节点
// ...

// 2.阴影算法
this.getFocusNodeByShadowAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})

// 找到了则让目标节点聚焦
if (targetNode) {
targetNode.active()
}
}

// 2.阴影算法
getFocusNodeByShadowAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
}
) {
bfsWalk(this.mindMap.renderer.root, node => {
if (node === currentActiveNode) return
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
let match = false
if (dir === 'Left') {
match =
left < currentActiveNodeRect.left &&
top < currentActiveNodeRect.bottom &&
bottom > currentActiveNodeRect.top
} else if (dir === 'Right') {
match =
right > currentActiveNodeRect.right &&
top < currentActiveNodeRect.bottom &&
bottom > currentActiveNodeRect.top
} else if (dir === 'Up') {
match =
top < currentActiveNodeRect.top &&
left < currentActiveNodeRect.right &&
right > currentActiveNodeRect.left
} else if (dir === 'Down') {
match =
bottom > currentActiveNodeRect.bottom &&
left < currentActiveNodeRect.right &&
right > currentActiveNodeRect.left
}
if (match) {
checkNodeDis(rect, node)
}
})
}

就是判断条件增加了是否交叉的比较,效果如下:


可以看到阴影算法成功解决了前面的跳转问题,但是它也并不完美,比如下面这种情况按左方向键找不到可聚焦节点了:



因为左侧没有存在交叉的节点,但是其实可以聚焦到父节点上,怎么办呢,我们先看一下下一种算法。


3.区域算法


所谓区域算法也很简单,把当前聚焦节点的四周平分成四个区域,对应四个方向,寻找哪个方向的下一个节点就先找出中心点在这个区域的所有节点,再从中选择距离最近的一个即可:



focus(dir) {
// 当前聚焦的节点
let currentActiveNode = this.mindMap.renderer.activeNodeList[0]
// 当前聚焦节点的位置信息
let currentActiveNodeRect = this.getNodeRect(currentActiveNode)
// 寻找的下一个聚焦节点
// ...
// 保存并维护距离最近的节点
// ...

// 3.区域算法
this.getFocusNodeByAreaAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})

// 找到了则让目标节点聚焦
if (targetNode) {
targetNode.active()
}
}

// 3.区域算法
getFocusNodeByAreaAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
}
) {
// 当前聚焦节点的中心点
let cX = (currentActiveNodeRect.right + currentActiveNodeRect.left) / 2
let cY = (currentActiveNodeRect.bottom + currentActiveNodeRect.top) / 2
bfsWalk(this.mindMap.renderer.root, node => {
if (node === currentActiveNode) return
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
// 遍历到的节点的中心点
let ccX = (right + left) / 2
let ccY = (bottom + top) / 2
// 节点的中心点坐标和当前聚焦节点的中心点坐标的差值
let offsetX = ccX - cX
let offsetY = ccY - cY
if (offsetX === 0 && offsetY === 0) return
let match = false
if (dir === 'Left') {
match = offsetX <= 0 && offsetX <= offsetY && offsetX <= -offsetY
} else if (dir === 'Right') {
match = offsetX > 0 && offsetX >= -offsetY && offsetX >= offsetY
} else if (dir === 'Up') {
match = offsetY <= 0 && offsetY < offsetX && offsetY < -offsetX
} else if (dir === 'Down') {
match = offsetY > 0 && -offsetY < offsetX && offsetY > offsetX
}
if (match) {
checkNodeDis(rect, node)
}
})
}

比较的逻辑可以参考下图:



效果如下:


结合阴影算法和区域算法


前面介绍阴影算法时说了它有一定局限性,区域算法计算出的结果则可以对它进行补充,但是理想情况下阴影算法的结果是最符合我们的预期的,那么很简单,我们可以把它们两个结合起来,调整一下顺序,先使用阴影算法计算节点,如果阴影算法没找到,那么再使用区域算法寻找节点,简单算法也可以加在最后:


focus(dir) {
// 当前聚焦的节点
let currentActiveNode = this.mindMap.renderer.activeNodeList[0]
// 当前聚焦节点的位置信息
let currentActiveNodeRect = this.getNodeRect(currentActiveNode)
// 寻找的下一个聚焦节点
// ...
// 保存并维护距离最近的节点
// ...

// 第一优先级:阴影算法
this.getFocusNodeByShadowAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})

// 第二优先级:区域算法
if (!targetNode) {
this.getFocusNodeByAreaAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})
}

// 第三优先级:简单算法
if (!targetNode) {
this.getFocusNodeBySimpleAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})
}

// 找到了则让目标节点聚焦
if (targetNode) {
targetNode.active()
}
}

效果如下:


1.gif


是不是很简单呢,详细体验可以点击思维导图


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

从拉马努金的传奇,看AI发展的必要

大家好啊,我是董董灿。 讲一个印度传奇数学家——拉马努金的故事。 如果有个人跑过来告诉你,所有的自然数之和等于一个负数。你会有什么反应? 我的第一反应是:Are you kidding me? 而数学家拉马努金说,是真的,我可以证明。 印度传奇数学家——拉...
继续阅读 »

大家好啊,我是董董灿。


讲一个印度传奇数学家——拉马努金的故事。


如果有个人跑过来告诉你,所有的自然数之和等于一个负数。你会有什么反应?


图片


我的第一反应是:Are you kidding me? 而数学家拉马努金说,是真的,我可以证明。


图片


印度传奇数学家——拉马努金在他的著作中给出了很多关于无穷级数的等式,其中就包括上面的自然数之和恒等式。


这个等式看似不合理,但已经被很多数学家证明,其中就包括欧拉、黎曼还有拉马努金。(证明过程大家可以搜索下,肯定能看懂)


数学天才


我一度认为,欧拉公式是世界上最美的公式,因为只有神才能将无理数、有理数、虚数单位、圆周率以及最简单的两个自然数0和1,用一个简单的不能再简单的加法公式来表示,而且是恒等式!


图片


公式中透露着一种无法言说的美感和沧桑感,像在预示着世界末日来临时,万生万物相互作用,终归会趋于虚无。


直到某一天,我看了一部电影,印度传记片《知无涯者》,才知道,原来神不止有一个;原来,最美的公式,不止一个。


图片


自古天才出贫穷。


拉马努金也一样,出生在印度一个贫穷家庭。在去剑桥见到著名数学家哈代之前,拉马努金甚至都没有系统的学习过数学,没错,是个野路子出身。


但是,这不妨碍他已经靠直觉发现了整整两本数学公式了,而且,与民科不同的是,他的公式,都经受住了历史的考验。


只不过,他自己不会证明。


他只知道,这些公式是正确的。凭着直觉,想到一个公式,就写下来,整整记录了两本。


公式中有这样的


图片


有这样的


图片


还有这样的


图片


可以说,拉马努金将人类对于整数和无穷级数的直觉开发到了极致!


熟悉数学的人看到这些,估计和我刚看到的表情是一样的。就连当年哈代在剑桥第一次见到这些公式的时候,也怀疑这是个骗子。


图片


这些等式真的成立么?



“喂,最后一个,没错说的就是你,计算圆周率倒数的那个,你就用一堆加加乘乘的数,可以精确的表示一个圆周率么,那可是无限不循环的无理数啊!还有,你那分母上写着的 9801 的常数项是咋来的?靠直觉写的么?我用 9800 行不行?”



用9800还真不行!


对于第三个计算圆周率的公式,我们可以很轻松的验证其正确性。当我们取K为0时,计算出来的圆周率的值已经逼近了π=3.1415927,如果再让K =1, 那么精度直逼 π=3.14159265359。


你以为这就完了?


拉马努金总共写了14个计算圆周率的公式,个个令人匪夷所思。


图片


拉马努金的一生,一共发现了3000多个公式,以至于后世的很多科学家,靠证明拉马努金的公式,获得了很多数学大奖,包括数学界最有名的菲尔兹奖。


更可怕的是,在他去世的前一年,留下的一些公式,最近被证明其实与描述黑洞有关。


写到这里,我不由自主地膜拜起来——


如果不是神发现了他在泄漏宇宙秘密,会封了他的号,年仅30多岁就英年早逝么?


如果你也对他感兴趣了,可以Google一下,或者去b站观看他的纪录片。


为什么拉马努金的公式这么重要?


因为他的公式涉及到了大量的无穷级数和无理数的逼近等式,且逼近精度高的惊人,而且收敛速度很快。


由于目前的计算机架构都是冯诺依曼架构,任何的计算都需要取指、译码、读写内存、计算等步骤,如果计算所需要的中间数据过多,那么势必会拖慢计算机运行的效率。


图片


现代计算机体系里,对于的加减乘除四则运算,基本上只有加法器和乘法器来实现,其他的复杂运算,也是在加法和乘法的基础上,外加移位或者一些与或非的逻辑电路来组合实现的。


一个简单的除法,在计算机里,就可能会涉及到多条加法、与或非、移位的指令。更别提进行大量科学计算或者人工智能计算的运算量了。


大量的组合运算,会产生大量的中间数据。这些数据都是会访问内存,一旦有内存访问,就有延时开销。


一旦延时,计算就会被拖慢。


快速求平方根倒数


你可能听过一个著名的求快速平方根倒数算法的故事,计算下面的公式。


图片


在著名游戏《雷神之锤3》中,有一个程序员写出了令人费解的代码,来计算一个数的平方根倒数。


float Q_rsqrt(float number) {
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = *(long *) &y;
i = 0x5F3759DF - (i >> 1);
y = *(float *) &i;
y = y * (threehalfs - (x2 * y * y));
return y;
}

代码中有几处是常数,比如 0x5F3759DF,如果不深究计算机的内存分配以及浮点数的数据格式,我想,大部分人都是看不懂这个常数项的。


常数项的存在,在计算机的计算流中,仅仅有一步读内存操作,少了很多中间数据的计算。


这也是为什么,在现代高性能(HPC)计算场景下,人们大都倾向于把需要计算的数据先保存下来,随用随取,以提高计算性能。


空间换时间


拉马努金的公式,就有这样的作用。


而且,效果比要我们自己设计的空间换时间的方法好的多,因为这些公式,早已把需要参与计算的值都写在了公式里,而这些值,一般人是推不出来的。



"海洋学家要计算海啸模型,这需要非常复杂的数学计算,不用一些技巧是没法计算的。但只要用拉马努金提供的公式,海啸模型就能大大简化,把不能计算,变成可以计算。"



科学的进步,往往伴随着灵感的出现而有大跃进。就好像坐在苹果树下的牛顿一样,一个苹果,砸出了一个经典物理学。


图片


拉马努金就有这样的直觉和灵感。于是,Google认识到了这个问题,拉马努金机出现了。


拉马努金机


人会消亡,机器不会消亡。


拉马努金虽然英年早逝了,但他的思想要是能保存下来,人类一样会受益无穷。


图片


于是Google在2019年,立项成立了创建拉马努金机的项目。得益于近些年人工智能技术的发展,拉马努金的项目运行的还算不错。


所谓拉马努金机,其实就是训练一个人工智能算法来模仿拉马努金的思考方式,然后生成一堆的数学公式,让人类科学家们去证明这些公式的正确性。


人类科学家给AI当助手,去证明AI靠直觉写出来的公式的正确性。


据说,这个项目已经取得了不错的进展,拉马努金机已经写出了很多公式,其中就包括高斯一生所发现的关于π的一些经验公式。


或许在不久的将来,拉马努金机真的可以发现自然界中的秘密也未可知。


One More Thing


作为继牛顿之后最伟大的物理学家,爱因斯坦去世后,他的大脑被切分成240片,永久的保存下来供人研究。


如果当时有了更先进的AI技术,或许保存下来的不是爱因斯坦的大脑,而是他的思想。


本文作者原创,请勿随意转载,转载请联系作者哦,作者很好说话的


作者:董董灿是个攻城狮
来源:juejin.cn/post/7231553447940718651
收起阅读 »

蒙提霍尔问题

web
最近看韩国电视剧【D.P:逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题 意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余...
继续阅读 »

f1e232d158d085038667d793dad96dc5.jpeg


最近看韩国电视剧【D.P逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题


意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余两个门中的一个并且是山羊(去掉一个错误答案),这时,让你重新选择。那么你是会坚持原来的选择,还是换选另外一个未被打开过的门呢?


大家可以想一想如果是自己,我们是会换还是不会换?


好了,我当时看到后感觉很有意思,所以我简单写了一套代码,源码贴在下面,大家可以验证一下,先告诉大家,换赢得汽车的概率是2/3,不换赢得汽车的概率是1/3


<header>
<h1>请选择换不换?</h1><button class="refresh">刷新</button>
</header>
<section>
<div class="box">
<h2>1</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
<div class="box">
<h2>2</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
<div class="box">
<h2>3</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
</section>
<span>请选择号码牌</span>
<select name="" id="">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button class="confirm">确认</button>
<span class="confirm-text"></span>
<span class="opater">
<button class="change"></button>
<button class="no-change">不换</button>
</span>
<p>
<strong>游戏规则:</strong>
<span>
上面有三个号码牌,其中一个号码牌的里面有汽车,选中里面是汽车的号码牌,
你就可以赢得该辆汽车,另外两个号码牌里面则都是一只山羊,
你任意选择其中一个,然后打开其余两个号码牌中的一个并且是山羊(去掉一个错误答案),
这时,你有一个重新选择的机会,你选择换还是不换?
</span>
</p>

.prize {
width: 300px;
height: 100px;
background-color: pink;
font-size: 36px;
line-height: 100px;
text-align: center;
position: absolute;
}

canvas {
position: absolute;
z-index: 2;
}

section {
display: flex;
}

.box {
width: 300px;
height: 200px;
cursor: pointer;
}

.box+.box {
margin-left: 8px;
}

header {
display: flex;
align-items: center;
}

header button {
margin-left: 8px;
height: 24px;
}
p {
width: 400px;
background-color: pink;
}

function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function getRandomNumber() {
return Math.random() > 0.5 ? 1 : 2;
}
let a1 = [0, 1, 2]
let i1 = undefined
let i2 = undefined
let isChange = false
const opater = document.querySelector('.opater')
opater.style.display = 'none'
// 随机一个奖品
const prizes = document.querySelectorAll('.prize')
let a0 = [0, 1, 2]
a0 = shuffleArray(a0)
a0.forEach((v,i) => {
const innerText = !!v ? '山羊' : '汽车'
prizes[i].innerText = innerText
})

const canvas = document.querySelectorAll('canvas')
const confirmText = document.querySelector('.confirm-text')
canvas.forEach(c => {
// 使用canvas实现功能
// 1. 使用canvas绘制一个灰色的矩形
const ctx = c.getContext('2d')
ctx.fillStyle = '#ccc'
ctx.fillRect(0, 0, c.width, c.height)
// 2. 刮奖逻辑
// 鼠标按下且移动的时候,需要擦除canvas画布
let done = false
c.addEventListener('mousedown', function () {
if (i1 === undefined) return alert('请先选择号码牌,并确认!')
if (!isChange) return alert('请选择换不换!')
done = true
})
c.addEventListener('mousemove', function (e) {
if (done) {
// offsetX 和 offsetY 可以获取到鼠标在元素中的偏移位置
const x = e.offsetX - 5
const y = e.offsetY - 5
ctx.clearRect(x, y, 10, 10)
}
})
c.addEventListener('mouseup', function () {
done = false
})
})
const confirm = document.querySelector('.confirm')
const refresh = document.querySelector('.refresh')
confirm.onclick = function () {
let select = document.querySelector('select')
const options = Array.from(select.children)
confirmText.innerText = `您选择的号码牌是${select.value},请问现在换不换?`
// 选择后,去掉一个错误答案
// i1是下标
i1 = select.value - 1
// delValue是值
let delValue = undefined
// 通过下标找值
if (a0[i1] === 0) {
delValue = getRandomNumber()
} else {
delValue = a0[i1] === 1 ? 2 : 1
}
// 通过值找下标
i2 = a0.indexOf(delValue)
// 选择的是i1, 去掉的是
const ctx = canvas[i2].getContext('2d')
ctx.clearRect(0, 0, 300, 100)
options.map(v => v.disabled = true)
confirm.style.display = 'none'
opater.style.display = 'inline-block'
}
const change = document.querySelector('.change')
const noChange = document.querySelector('.no-change')
change.onclick = function () {
isChange = true
const x = a1.filter(v => v !== i1 && v !== i2)
confirmText.innerText = `您确认选择的号码牌是${x[0] + 1},请刮卡!`
opater.style.display = 'none'
}
noChange.onclick = function () {
isChange = true
confirmText.innerText = `您确认选择的号码牌是${i1 + 1},请刮卡!`
opater.style.display = 'none'
}
refresh.onclick = function () {
window.location.reload()
}

作者:JoyZ
来源:juejin.cn/post/7278684023757553727
收起阅读 »

js数组方法分类

web
js数组方法分类 0.前言 我们知道,js中数组方法非常多,MDN就列举了43个方法,就连常用方法都很多,比如forEach,filter,map,push等等等,可能我们见到方法认识这个方法,但要我们列举所知道的数组方法,我们可能会遗忘漏掉某些,为了帮助大家...
继续阅读 »

js数组方法分类


0.前言


我们知道,js中数组方法非常多,MDN就列举了43个方法,就连常用方法都很多,比如forEach,filter,map,push等等等,可能我们见到方法认识这个方法,但要我们列举所知道的数组方法,我们可能会遗忘漏掉某些,为了帮助大家更好更有规律地记住更多方法,在这里我特地将数组方法分俄为七大类,每一类都有其特定共同点和功能的标签,根据这些标签去记忆,相信大家读完可以感到醍醐灌顶的感觉。


一共2+4+9+7+6+3+2=33个,放心吧,足够啦!


1.创建数组方法



  • Array.from() :将可迭代对象或类数组对象转化为新的浅拷贝数组.

  • Array.of():将可变数量的参数转化为新的浅拷贝 数组.


//Array.from()
console.log(Array.from("foo")); // ['f', 'o', 'o']
function bar() {
 console.log(arguments); //Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ] 类数组
 console.log(Array.from(arguments)); // [1, 2, 3]
}
bar(1, 2, 3);
const set = new Set(["foo", "bar", "baz", "foo"]);
console.log(Array.from(set)); //从Set构建数组['foo', 'bar', 'baz'],Map也可以

//Array.of()
console.log(Array.of()); //[] 创建空数组
console.log(Array.of(1, 2, 3, 4)); //[1, 2, 3, 4]
//浅拷贝
const obj1 = { age: 18 };
const arr1 = [666, 777];
const arr = Array.of(obj1, arr1);
arr[0].age = 19;
arr[1][0] = 999;
console.log(arr); //[{age:19},[999,777]]


2.数组首端或尾端添加删除方法



  • Array.prototype.push():将指定的元素添加到数组的末尾,并返回新的数组长度.

  • Array.prototype.pop():从数组中删除最后一个元素,并返回该元素的值。此方法会更改数组的长度.

  • Array.prototype.shift():从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度.

  • Array.prototype.unshift():将指定的元素添加到数组的开头,并返回新的数组长度.


//Array.prototype.push()
const arr = [1, 2];
console.log(arr.push(3, 4, 5)); //5
console.log(arr); //[ 1, 2, 3, 4, 5 ]
//Array.prototype.pop()
console.log(arr.pop()); //数组最后一个元素:5
console.log(arr); //[ 1, 2, 3, 4 ]
//Array.prototype.shift()
console.log(arr.shift()); //1
console.log(arr); //[ 2, 3, 4 ]
//Array.prototype.unshift()
console.log(arr.unshift(66, 77, 88)); //6
console.log(arr); //[ 66, 77, 88, 2, 3, 4 ]

3.操作数组方法



  1. Array.prototype.concat():用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组.

  2. Array.prototype.copyWithin():浅复制数组的一部分到同一数组中的另一个位置,并返回该数组,不会改变原数组的长度.

  3. Array.prototype.fill():用一个固定值填充一个数组中从起始索引(默认为 0)到终止索引(默认为 array.length)内的全部元素。它返回修改后的数组。会改变原始数组.


// Array.prototype.concat()
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [7, 8, 9];
const arr4 = arr1.concat(arr2, arr3); //[1, 2, 3, 4, 5, 6, 7, 8, 9]
// Array.prototype.copyWithin()
const arr = [1, 2, 3, 4, 5, 6];
console.log(arr.copyWithin(2, 3, 5)); //[ 1, 2, 4, 5, 5, 6 ] 将 4,5替换到2索引位置
// Array.prototype.fill()
const array1 = [1, 2, 3, 4];
console.log(array1.fill(0, 2, 4)); //[ 1, 2, 0, 0 ]
console.log(array1.fill(5, 1)); //[ 1, 5, 5, 5 ]
console.log(array1.fill(6)); //[ 6, 6, 6, 6 ]
console.log(array1); //[ 6, 6, 6, 6 ]


  1. Array.prototype.flat():展开嵌套数组,默认嵌套深度为1,不改变原数组,返回新数组.

  2. Array.prototype.join():用逗号或指定分隔符将数组连接成字符串.

  3. Array.prototype.reverse():就地反转字符串,返回同一数组的引用,原数组改变.


// Array.prototype.flat()
const arr1 = [1, 2, [3, 4]];
console.log(arr1.flat()); //[ 1, 2, 3, 4 ]
console.log(arr1); // 不改变原数组 [ 1, 2, [ 3, 4 ] ]
const arr2 = [1, 2, [3, 4, [5, 6]]];
console.log(arr2.flat()); //默认展开嵌套一层数组[ 1, 2, 3, 4, [ 5, 6 ] ]
console.log(arr2.flat(2)); //展开嵌套二层数组 [ 1, 2, 3, 4, 5, 6 ]
// Array.prototype.join()
const elements = ["Fire", "Air", "Water"];
console.log(elements.join()); //"Fire,Air,Water"
console.log(elements.join("+++++")); //Fire+++++Air+++++Water
console.log(elements.join("-")); //Fire-Air-Water
// Array.prototype.reverse()
const arr = [1, 2, 3];
console.log(arr.reverse()); //[3,2,1]
console.log(arr); //[3,2,1]


  1. Array.prototype.slice():截取数组,返回一个新数组,不改变原数组.

  2. Array.prototype.sort():排序数组,改变原数组,默认排序规则是将数组每一项转化为字符串,根据utf-16码升值排序.

  3. Array.prototype.splice():对数组进行增加、删除、替换元素,改变原数组.


// Array.prototype.slice();
const animals = ["ant", "bison", "camel", "duck", "elephant"];
console.log(animals.slice(2)); //["camel", "duck", "elephant"]
console.log(animals.slice(2, 4)); //["camel", "duck"]
console.log(animals.slice(-2)); //["duck", "elephant"]
console.log(animals.slice(2, -1)); //["camel", "duck"]
console.log(animals.slice()); //浅复制数组 ["ant", "bison", "camel", "duck", "elephant"]
// Array.prototype.sort();
const months = ["March", "Jan", "Feb", "Dec"];
months.sort();
console.log(months); // ["Dec", "Feb", "Jan", "March"];
const array1 = [1, 30, 4, 21, 100000];
array1.sort();
console.log(array1); //[1, 100000, 21, 30, 4]
array1.sort((a, b) => a - b); //升序
console.log(array1);
//Array.prototype.splice();
const arr = [1, 2, 3, 4, 5];
arr.splice(2, 2); //从index为2的位置开始删除两个元素[1, 2, 5];
arr.splice(2, 0, 3, 4); //从index为2的位置增加34两个元素 [1,2,3,4,5]
arr.splice(2, 2, 7, 8); //删除index为2位置的两个元素,并添加89两个元素 [ 1, 2, 7, 8, 5 ]

4.查找元素或索引方法



  1. Array.prototype.at():返回索引位置对应的元素,负索引从数组最后一个元素倒数开始.

  2. Array.prototype.find():查找符合条件的第一个元素,未找到则返回undefined,回调函数返回值为真则符合条件.

  3. Array.prototype.findIndex():查找符合条件第一个元素的索引,未找到则返回**-1**,回调函数返回值为真则符合条件.

  4. Array.prototype.findLast():从后往前查找符合条件的第一个元素,其余同理Array.prototype.find().

  5. Array.prototype.findLastIndex():从后往前查找符合条件第一个元素的索引,其余同理Array.prototype.findIndex().


// Array.prototype.at()
const arr = [1, 2, 3, 4, 5];
console.log(arr.at(0)); //1
console.log(arr.at(-1)); //5
const array = [
{ name: "jack", age: 15 },
{ name: "tom", age: 29 },
{ name: "bob", age: 23 },
];
// Array.prototype.find()
const obj = array.find((item) => {
 if (item.age > 18) {
   return true;
} else {
   return false;
}
}); //{ name: 'tom', age: 29 }
//Array.prototype.findIndex()
const objIndex = array.findIndex((item) => {
 if (item.age > 18) {
   return true;
} else {
   return false;
}
}); //1
// Array.prototype.findLast()
const lastObj = array.findLast((item) => {
 if (item.age > 18) {
   return true;
} else {
   return false;
}
}); //{name: 'bob', age: 23}
// Array.prototype.findLast()
const lastIndex = array.findLastIndex((item) => {
 if (item.age > 18) {
   return true;
} else {
   return false;
}
}); //2


  1. Array.prototype.indexOf():返回数组中给定元素第一次出现的下标,如果不存在则返回-1.

  2. Array.prototype.includes():在数组中查找指定元素,如果找到则返回true,如果找不到则返回false.


//Array.prototype.indexOf()
const arr = [1, 2, 6, 8, 9];
console.log(arr.indexOf(6)); //2
console.log(arr.indexOf(10)); //-1
//Array.prototype.includes()
console.log(arr.includes(6)); //true
console.log(arr.includes(10)); //-false

5.迭代方法


迭代方法非常常用,这里就不列举例子了.



  1. Array.prototype.forEach():对数组每一项元素执行给定的函数,没有返回值.

  2. Array.prototype.filter():过滤数组,创建符合条件的浅拷贝数组.

  3. Array.prototype.map():对数组每个元素执行给定函数映射一个新值,返回新数组.

  4. Array.prototype.every():检查数组所有元素是否符合条件,如果符合返回true,不符合返回false;

  5. Array.prototype.some():检查数组中是否有元素符合条件,如果有则返回true,不符合返回false

  6. Array.prototype.reduce():用指定函数迭代数组每一项,上一次函数返回值作为下一次函数初始值,返回最后一次函数的最终返回值.


6. 迭代器方法


这里就不赘述迭代器对象了.



  1. Array.prototype.keys():返回数组索引迭代器对象.

  2. Array.prototype.values():返回数组元素的迭代器对象.

  3. Array.prototype.entries():返回数组索引和元素构成的迭代器对象.


7.额外重要方法



  1. Array.isArray():判断是否是数组.


//都返回true 都是数组
console.log(Array.isArray([]));
console.log(Array.isArray(new Array()));
console.log(Array.isArray(Array.of(1, 2, 3)));
// 也可以用instanceof:true
console.log([] instanceof Array);
console.log(new Array() instanceof Array);
console.log(Array.of(1, 2, 3) instanceof Array);
console.log([].toString());
//惊喜:最后还可以使用Object.prototype.toString()
console.log(Object.prototype.toString.call([])); //[object Array]


  1. Array.prototype.toString():将数组去掉左右括号转化为字符串.


const array1 = [1, 2, "a", "1a"];
console.log(array1.toString()); // "1,2,a,1a"

作者:樊阳子
来源:juejin.cn/post/7288234800563961917
收起阅读 »

我说ArrayList初始容量是10,面试官让我回去等通知

引言 在Java集合中,ArrayList是最常用到的数据结构,无论是在日常开发还是面试中,但是很多人对它的源码并不了解。下面提问几个问题,检验一下大家对ArrayList的了解程度。 ArrayList的初始容量是多少?(90%的人都会答错) ArrayL...
继续阅读 »

引言


在Java集合中,ArrayList是最常用到的数据结构,无论是在日常开发还是面试中,但是很多人对它的源码并不了解。下面提问几个问题,检验一下大家对ArrayList的了解程度。



  1. ArrayList的初始容量是多少?(90%的人都会答错)

  2. ArrayList的扩容机制

  3. 并发修改ArrayList元素会有什么问题

  4. 如何快速安全的删除ArrayList中的元素


接下来一块分析一下ArrayList的源码,看完ArrayList源码之后,可以轻松解答上面四个问题。


简介


ArrayList底层基于数组实现,可以随机访问,内部使用一个Object数组来保存元素。它维护了一个 elementData 数组和一个 size 字段,elementData数组用来存放元素,size字段用于记录元素个数。它允许元素是null,可以动态扩容。
image.png


初始化


当我们调用ArrayList的构造方法的时候,底层实现逻辑是什么样的?


// 调用无参构造方法,初始化ArrayList
List<Integer> list1 = new ArraryList<>();

// 调用有参构造方法,初始化ArrayList,指定容量为10
List<Integer> list1 = new ArraryList<>(10);

看一下底层源码实现:


// 默认容量大小
private static final int DEFAULT_CAPACITY = 10;

// 空数组
private static final Object[] EMPTY_ELEMENTDATA = {};

// 默认容量的数组对象
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 存储元素的数组
transient Object[] elementData;

// 数组中元素个数,默认是0
private int size;

// 无参初始化,默认是空数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 有参初始化,指定容量大小
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 直接使用指定的容量大小
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
}
}

可以看到当我们调用ArrayList的无参构造方法 new ArraryList<>() 的时候,只是初始化了一个空对象,并没有指定数组大小,所以初始容量是零。至于什么时候指定数组大小,接着往下看。


添加元素


再看一下往ArrayList种添加元素时,调用的 add() 方法源码:


// 添加元素
public boolean add(E e) {
// 确保数组容量够用,size是元素个数
ensureCapacityInternal(size + 1);
// 直接在下个位置赋值
elementData[size++] = e;
return true;
}

// 确保数组容量够用
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 计算所需最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果数组等于空数组,就设置默认容量为10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

// 确保容量够用
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果所需最小容量大于数组长度,就进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

看一下扩容逻辑:


// 扩容,就是把旧数据拷贝到新数组里面
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 计算新数组的容量大小,是旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);

// 如果扩容后的容量小于最小容量,扩容后的容量就等于最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;

// 如果扩容后的容量大于Integer的最大值,就用Integer最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);

// 扩容并赋值给原数组
elementData = Arrays.copyOf(elementData, newCapacity);
}

可以看到:



  • 扩容的触发条件是数组全部被占满

  • 扩容是以旧容量的1.5倍扩容,并不是2倍扩容

  • 最大容量是Integer的最大值

  • 添加元素时,没有对元素校验,允许为null,也允许元素重复。


再看一下数组拷贝的逻辑,这里都是Arrays类里面的方法了:


/**
* @param original 原数组
* @param newLength 新的容量大小
*/

public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
// 创建一个新数组,容量是新的容量大小
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
// 把原数组的元素拷贝到新数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

最终调用了System类的数组拷贝方法,是native方法:


/**
* @param src 原数组
* @param srcPos 原数组的开始位置
* @param dest 目标数组
* @param destPos 目标数组的开始位置
* @param length 被拷贝的长度
*/

public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length)
;

总结一下ArrayList的 add() 方法的逻辑:



  1. 检查容量是否够用,如果够用,直接在下一个位置赋值结束。

  2. 如果是第一次添加元素,则设置容量默认大小为10。

  3. 如果不是第一次添加元素,并且容量不够用,则执行扩容操作。扩容就是创建一个新数组,容量是原数组的1.5倍,再把原数组的元素拷贝到新数组,最后用新数组对象覆盖原数组。


需要注意的是,每次扩容都会创建新数组和拷贝数组,会有一定的时间和空间开销。在创建ArrayList的时候,如果我们可以提前预估元素的数量,最好通过有参构造函数,设置一个合适的初始容量,以减少动态扩容的次数。


删除单个元素


再看一下删除元素的方法 remove() 的源码:


public boolean remove(Object o) {
// 判断要删除的元素是否为null
if (o == null) {
// 遍历数组
for (int index = 0; index < size; index++)
// 如果和当前位置上的元素相等,就删除当前位置上的元素
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 遍历数组
for (int index = 0; index < size; index++)
// 如果和当前位置上的元素相等,就删除当前位置上的元素
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

// 删除该位置上的元素
private void fastRemove(int index) {
modCount++;
// 计算需要移动的元素的个数
int numMoved = size - index - 1;
if (numMoved > 0)
// 从index+1位置开始拷贝,也就是后面的元素整体向左移动一个位置
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 设置数组最后一个元素赋值为null,防止会导致内存泄漏
elementData[--size] = null;
}

删除元素的流程是:



  1. 判断要删除的元素是否为null,如果为null,则遍历数组,使用双等号比较元素是否相等。如果不是null,则使用 equals() 方法比较元素是否相等。这里就显得啰嗦了,可以使用 Objects.equals()方法,合并ifelse逻辑。

  2. 如果找到相等的元素,则把后面位置的所有元素整体相左移动一个位置,并把数组最后一个元素赋值为null结束。


可以看到遍历数组的时候,找到相等的元素,删除就结束了。如果ArrayList中存在重复元素,也只会删除其中一个元素。


批量删除


再看一下批量删除元素方法 removeAll() 的源码:


// 批量删除ArrayList和集合c都存在的元素
public boolean removeAll(Collection<?> c) {
// 非空校验
Objects.requireNonNull(c);
// 批量删除
return batchRemove(c, false);
}

private boolean batchRemove(Collection<?> c, boolean complement){
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
// 把需要保留的元素左移
elementData[w++] = elementData[r];
} finally {
// 当出现异常情况的时候,可能不相等
if (r != size) {
// 可能是其它线程添加了元素,把新增的元素也左移
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
// 把不需要保留的元素设置为null
if (w != size) {
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}

批量删除元素的逻辑,并不是大家想象的:



遍历数组,判断要删除的集合中是否包含当前元素,如果包含就删除当前元素。删除的流程就是把后面位置的所有元素整体左移,然后把最后位置的元素设置为null。



这样删除的操作,涉及到多次的数组拷贝,性能较差,而且还存在并发修改的问题,就是一边遍历,一边更新原数组。
批量删除元素的逻辑,设计充满了巧思,具体流程就是:



  1. 把需要保留的元素移动到数组左边,使用下标 w 做统计,下标 w 左边的是需要保留的元素,下标 w 右边的是需要删除的元素。

  2. 虽然ArrayList不是线程安全的,也考虑了并发修改的问题。如果上面过程中,有其他线程新增了元素,把新增的元素也移动到数组左边。

  3. 最后把数组中下标 w 右边的元素都设置为null。


所以当需要批量删除元素的时候,尽量使用 removeAll() 方法,性能更好。


并发修改的问题


当遍历ArrayList的过程中,同时增删ArrayList中的元素,会发生什么情况?测试一下:


import java.util.ArrayList;
import java.util.List;

public class Test {

public static void main(String[] args) {
// 创建ArrayList,并添加4个元素
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(2);
list.add(3);
// 遍历ArrayList
for (Integer key : list) {
// 判断如果元素等于2,则删除
if (key.equals(2)) {
list.remove(key);
}
}
}
}

运行结果:


Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at com.yideng.Test.main(Test.java:14)

报出了并发修改的错误,ConcurrentModificationException
这是因为 forEach 使用了ArrayList内置的迭代器,这个迭代器在迭代的过程中,会校验修改次数 modCount,如果 modCount 被修改过,则抛出ConcurrentModificationException异常,快速失败,避免出现不可预料的结果。


// ArrayList内置的迭代器
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = modCount;

// 迭代下个元素
public E next() {
// 校验 modCount
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E)elementData[lastRet = i];
}

// 校验 modCount 是否被修改过
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

如果想要安全的删除某个元素,可以使用 remove(int index) 或者 removeIf() 方法。


import java.util.ArrayList;
import java.util.List;

public class Test {

public static void main(String[] args) {
// 创建ArrayList,并添加4个元素
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(2);
list.add(3);
// 使用 remove(int index) 删除元素
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals(2)) {
list.remove(i);
}
}

// 使用removeIf删除元素
list.removeIf(key -> key.equals(2));
}

}

总结


现在可以回答文章开头提出的问题了吧:



  1. ArrayList的初始容量是多少?


答案:初始容量是0,在第一次添加元素的时候,才会设置容量为10。



  1. ArrayList的扩容机制


答案:



  1. 创建新数组,容量是原来的1.5倍。

  2. 把旧数组元素拷贝到新数组中

  3. 使用新数组覆盖旧数组对象

  4. 并发修改ArrayList元素会有什么问题


答案:会快速失败,抛出ConcurrentModificationException异常。



  1. 如何快速安全的删除ArrayList中的元素


答案:使用remove(int index)removeIf() 或者 removeAll() 方法。
我们知道ArrayList并不是线程安全的,原因是它的 add()remove() 方法、扩容操作都没有加锁,多个线程并发操作ArrayList的时候,会出现数据不一致的情况。
想要线程安全,其中一种方式是初始化ArrayList的时候使用 Collections.synchronizedCollection() 修饰。这样ArrayList所有操作都变成同步操作,性能较差。还有一种性能较好,又能保证线程安全的方式是使用 CopyOnWriteArrayList,就是下章要讲的。


// 第一种方式,使用 Collections.synchronizedCollection() 修饰
List<Integer> list1 = Collections.synchronizedCollection(new ArrayList<>());

// 第二种方式,使用 CopyOnWriteArrayList
List<Integer> list1 = new CopyOnWriteArrayList<>();

作者:一灯架构
来源:juejin.cn/post/7288963211071094842
收起阅读 »

前端代码重复度检测

web
在前端开发中,代码的重复度是一个常见的问题。重复的代码不仅增加了代码的维护成本,还可能导致程序的低效运行。为了解决这个问题,有许多工具和技术被用来检测和消除代码重复。其中一个被广泛使用的工具就是jscpd。 jscpd简介 jscpd是一款开源的JavaScr...
继续阅读 »

在前端开发中,代码的重复度是一个常见的问题。重复的代码不仅增加了代码的维护成本,还可能导致程序的低效运行。为了解决这个问题,有许多工具和技术被用来检测和消除代码重复。其中一个被广泛使用的工具就是jscpd


jscpd简介


jscpd是一款开源的JavaScript的工具库,用于检测代码重复的情况,针对复制粘贴的代码检测很有效果。它可以通过扫描源代码文件,分析其中的代码片段,并比较它们之间的相似性来检测代码的重复度。jscpd支持各种前端框架和语言,包括HTML、CSS和JavaScript等150种的源码文件格式。无论是原生的JavaScript、CSS、HTML代码,还是使用typescriptscssvuereact等代码,都能很好的检测出项目中的重复代码。


开源仓库地址:github.com/kucherenko/jscpd/tree/master


如何使用


使用jscpd进行代码重复度检测非常简单。我们需要安装jscpd。可以通过npmyarn来安装jscpd


npm install -g jscpd

yarn global add jscpd

安装完成后,我们可以在终端运行jscpd命令,指定要检测的代码目录或文件。例如,我们可以输入以下命令来检测当前目录下的所有JavaScript文件:


jscpd .

指定目录检测:


jscpd /path/to/code

在命令行执行成功后的效果如下图所示:



简要说明一下对应图中的字段内容:




  • Clone found (javascript):
    显示找到的重复代码块,这里是javascript文件。并且会显示重复代码在文件中具体的行数,便于查找。




  • Format:文件格式,这里是 javascript,还可以是 scss、markup 等。




  • Files analyzed:已分析的文件数量,统计被检测中的文件数量。




  • Total lines:所有文件的总行数。




  • Total tokens:所有的token数量,一行代码一般包含几个到几十个不等的token数量。




  • Clones found:找到的重复块数量。




  • Duplicated lines:重复的代码行数和占比。




  • Duplicated tokens:重复的token数量和占比。




  • Detection time:检测耗时。




工程配置


以上示例是比较简单直接检测单个文件或文件夹。当下主流的前端项目大多都是基于脚手架生成或包含相关前端工程化的文件,由于很多文件是辅助工具如依赖包、构建脚本、文档、配置文件等,这类文件都不需要检测,需要排除。这种情况下的工程一般使用配置文件的方式,通过选项配置规范 jscpd 的使用。


jscpd 的配置选项可以通过以下两种方式创建,增加的内容都一致无需区分对应的前端框架。


在项目根目录下创建配置文件 .jscpd.json,然后在该文件中增加具体的配置选项:


    {
"threshold": 0,
"reporters": ["html", "console", "badge"],
"ignore": ["**/__snapshots__/**"],
"absolute": true
}

也可直接在 package.json 文件中添加jscpd


    {
...
"jscpd": {
"threshold": 0.1,
"reporters": ["html", "console", "badge"],
"ignore": ["**/__snapshots__/**"],
"absolute": true,
"gitignore": true
}
...
}

简要介绍一下上述配置字段含义:



  • threshold:表示重复度的阈值,超过这个值,就会输出错误报警。如阈值设为 10,当重复度为18.1%时,会提示以下错误❌,但代码的检测会正常完成。


ERROR: jscpd found too many duplicates (18.1%) over threshold (10%)


  • reporters:表示生成结果检测报告的方式,一般有以下几种:

    • console:控制台打印输出

    • consoleFull:控制台完整打印重复代码块

    • json:输出 json 格式的报告

    • xml:输出 xml 格式的报告

    • csv:输出 csv 格式的报告

    • markdown:输出带有 markdown 格式的报告

    • html:生成html报告到html文件夹

    • verbose:输出大量调试信息到控制台



  • ignore:检测忽略的文件或文件目录,过滤一些非业务代码,如依赖包、文档或静态文件等

  • format:需要进行重复度检测的源代码格式,目前支持150多种,我们常用的如 javascript、typescript、css 等

  • absolute:在检测报告中使用绝对路径


除此之外还有很多其他的配置,有兴趣的可以看源码文档中有详细的介绍。


检测报告


完成以上jscpd配置后执行以下命令即可输出对应的重复检测报告。运行完毕后,jscpd会生成一个报告,展示每个重复代码片段的信息。报告中包含了重复代码的位置、相似性百分比和代码行数等详细信息。通过这些信息,我们可以有针对性的进行代码重构。


jscpd ./src -o 'report'

项目中的业务代码通常会选择放在 ./src 目录下,所以可以直接检测该目录下的文件,如果是放在其他目录下根据实际情况调整即可。
通过命令行参数-o 'report'输出检测报告到项目根目录下的 report 文件夹中,这里的report也可以自定义其他目录名称,输出的目录结构如下所示:



生成的报告页面如下所示:


项目概览数据:



具体重复代码的位置和行数:



默认检测重复代码的行数(5行)和tokens(50)比较小,所以产生的重复代码块可能比较多,在实际使用中可以针对检测范围进行设置,如下设置参数供参考:



  • 最小tokens:--min-tokens,简写 -k

  • 最小行数:--min-lines,简写 -l

  • 最大行数:--max-lines,简写 -x


jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'

为了更便捷的使用此命令,可将这段命令集成到 package.json 中的 scripts 中,后续只需执行 npm run jscpd 即可执行检测。如下所示:


"scripts": {
...
"jscpd": "jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'",
...
}

忽略代码块


上面所提到的ignore可以忽略某个文件或文件夹,还有一种忽略方式是忽略文件中的某一块代码。由于一些重复代码在实际情况中是必要的,可以使用代码注释标识的方式忽略检测,在代码的首尾位置添加注释,jscpd:ignore-startjscpd:ignore-end 包裹代码即可。


在js代码中使用方式:


/* jscpd:ignore-start */
import lodash from 'lodash';
import React from 'react';
import {User} from './models';
import {UserService} from './services';
/* jscpd:ignore-end */

在CSS和各种预处理中与js中的用法一致:


/* jscpd:ignore-start */
.style {
padding: 40px 0;
font-size: 26px;
font-weight: 400;
color: #464646;
line-height: 26px;
}
/* jscpd:ignore-end */

在html代码中使用方式:



<meta data-react-helmet="true" name="theme-color" content="#cb3837"/>
<link data-react-helmet="true" rel="stylesheet" href="https://static.npmjs.com/103af5b8a2b3c971cba419755f3a67bc.css"/>
<link data-react-helmet="true" rel="apple-touch-icon" sizes="120x120" href="https://static.npmjs.com/58a19602036db1daee0d7863c94673a4.png"/>
<link data-react-helmet="true" rel="icon" type="image/png" href="https://static.npmjs.com/b0f1a8318363185cc2ea6a40ac23eeb2.png" sizes="32x32"/>


总结


jscpd是一款强大的前端本地代码重复度检测工具。它可以帮助开发者快速发现代码重复问题,简单的配置即可输出直观的代码重复数据,通过解决重复的代码提高代码的质量和可维护性。


使用jscpd我们可以有效地优化前端开发过程,提高代码的效率和性能。希望本文能够对你了解基于jscpd的前端本地代码重复度检测有所帮助。




看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


作者:南城FE
来源:juejin.cn/post/7288699185981095988
收起阅读 »

程序员的精力管理

今天跟大家分享一个主题,就是程序员的精力管理。工作8年多,我发现在职场里面会看到各种各样人,不同的人有不同的状态。大部分时候,我会看到一些刚刚毕业的校招生入职一段时间内朝气蓬勃,身体非常好,有永远用不完的精力一样,时时刻刻都保持在一种兴奋的状态。 更明显的是我...
继续阅读 »


今天跟大家分享一个主题,就是程序员的精力管理。工作8年多,我发现在职场里面会看到各种各样人,不同的人有不同的状态。大部分时候,我会看到一些刚刚毕业的校招生入职一段时间内朝气蓬勃,身体非常好,有永远用不完的精力一样,时时刻刻都保持在一种兴奋的状态。


更明显的是我发现工作了一段时间之后的人状态和精力就大不相同了,我有个师兄工作七八年了,每次看起来可能特别的疲惫,好像一天都打不起精神来。还有我也能发现,尽管工作了十几年的同事依然精力旺盛,神采奕奕。特别是有一些比较特殊的角色,比如说主管或者总监,往往身居高位却依然精力旺盛,不管是在日常沟通的时候,还是发表演讲的时候,都充满精力,激情澎湃。当然也有很多总监级别的大佬,在台上讲话无精打采,在台下就直接打瞌睡。


我记得有一次我北京的总监出差到杭州拉了一个小群,就组织我们去跑步,跑的是10km。我们在杭师大就开始跑了起来,我们的总监年过40岁全程一马当先,最后跑完的时候基本上领先了我们几个30岁不到同事快一圈了。我跟其他几个同事就感叹说,想不到总监不仅位置也比我们高,身体比还要好,这上哪说理去。


由此而知,不管是什么样的一个层级,至少大部分人在刚开始参加工作的时候都是精力活跃,充满斗志的,但为什么越来越工作久了以后会发生如此大的分叉呢,到底是什么原因能够损耗我们的精力,我们又应该如何管理我们的精力呢?


首先要明白,一个人的精力是有限的,哪怕再厉害的人精力都有限的。工作久了后,整天都活力旺盛的人也几乎不存在的。从科学的角度看来,像类似于人这样的生物体,它的整体精力表现一定是呈现一个正态分布的。所以说大部分人的精力管理都在一个正常的水平上。那么是什么因素导致这些人在工作一段时间后经历表现的特别的不一样?特别是我总监这样的人,基本上天天感觉精力爆棚,仅仅是因为高管不干活吗?我觉得不是的,真正的答案就是精力管理。


有科学家说过,人的精力就跟我们的电池一样,需要反复的充放电。正常情况下来说,我们在休息完成以后,刚刚醒来之后的一段时间,精力是特别旺盛的,经过了一天的各种事务以后,我们会发现我们的精力会越来越少,直到最后完全打不起任何精神。比如我就是在上午精神很好,到了下午基本上就是打蔫的状态,完全做不了耗脑力的事情。同时每个人也有他不同的精力旺盛的时间段,比如有的人在下午会特别的精神,有的人会在晚上特别的精神。而大部分程序员晚上会特别精神,毕竟99%的程序员都有晚上经常加班的事。


第二个关键点就是我们要在我们精力最旺盛的时候做最重要的事情。很多人在次要的时间上耗费了大量的精力,所以在最重要事情的时候,比如说在做关键技术讨论的时候,或者关键会议的时候,就显得沉沉欲睡。这种就是典型的把精力分配到了错误的时间段上面。比如我的团队在述职的时候,我们就发现有些同学会精神亢奋,非常有斗志的分享完了所有东西,有的同学分享的时候则沉沉欲睡,昏昏沉的感觉,这个也是属于典型的精力分配出了问题。


其实,在我们在各种非常重要的场合,比如晋升和OKR述职的时候,我们应该保证一个尽量旺盛的状态,在这个时候有一些非必要的工作都可以往后延。而我们有的同学因为赶各种项目或者工程,往往会把精力用在了做其他项目上面,然后留给在重要的环节,比如说答辩的时候,精力已经是强弩之末了。对于高效人士来说,宁愿次要的工作延迟一点,也要保证这一两个小时内的精力充沛。


精力是有限的,在这个精力分配里面,我们一定要把最重要的经历,最好的精力分配给最重要的事情,同时我们要注意一定不要消耗额外的精力在不必要的事情或者琐碎的事情上面。我在淘宝工作的时候,很多同事和师兄经常挂在嘴边的话,就是“白天的杂七杂八的事情和会议特别多,只有晚上才有精力写代码”。然而作为一名工程师或者一名产品经理,只能在晚上的抽出时间去写代码或者画自己的产品的prd。而这个时候做的确实是最重要的事情,用的是最最剩余的那一点点燃料。长期来看,这种精力分配方式产出的代码或者产品的质量就可想而知。毕竟,在竞争空前激烈的现代社会,想抽空做出伟大的事业的人是不存在的。


所以在这种情况下,特别必要的时候,我们一定要注意,不要给一些琐事儿或者烦杂的事情分配过多的精力,甚至是要尽量减少接触这些杂事的机会。当然很多人说有些东西都是必要的,但实际上以我的经验来看,80%以上的会议都是无效会议,只不过我们碍于各种各样的因素,不得不参加,从我的经验上来看,实际上就算我们参加了这些会议或者相关的评审,我们也取得不了任何额外的结果,大部分和我们主线路无关的事情,往往都是可以忽略的事情。这里我有个小窍门,对于不重要的事情,我一般都会等一段时间处理,很多时候不是很着急的事情,对方都忘记或者找到其他办法了。


当我们工作了若干年以后成为了核心骨干,往心里面就会有一种冲动或者想法,那就是我要掌控所有的事情,我要了解所有的上下文,这样才会有一种全局的控制感。所以很多高级工程师在工作一段时间之后就会全量的参加所有的会议,所有的讨论。以至于大部分经历都损耗到了会议上、需求评审上或者讨论会上。而留下来思考最重要的事情是最核心的技术方案或者产品方案,就只剩下一点点精力了。这个我觉得就是完全一种错误的思想,所谓大而全大概率是拿不到任何结果的。在我们企业的项目推进里面,我们经常也发现很多事情都是试错型的,探索型的,甚至有些都是重复型的,如果你把你所有的时间都耗费在了和别人的讨论和沟通上,那么势必你的精力就会被分散到点点滴滴,很琐碎。


这种情况下,只能湮没在小事上,过分追求“全”,而忽略了“深”。


所以不管什么阶段,不管什么角色,都不应该有“面面俱到”的要求和控制感,也不用焦虑忽略了什么,而是应该找到里面最关键的几件事情,并且把核心的注意力放在这上面,这样是取得成功的唯一的通道。


第三个关键点就是除了精力的使用之外,我们还要非常关注精力的恢复。精力和我们的能源一样,并不是取之不尽,用之不竭的,也是需要不断的持续的去给它充电。当然,最好的方式就是睡眠。所以在精力的管理方面,睡眠是最好的方式。我记得我唯一一次跟家人大吵就是没有睡好的时候,唯一一次高考失常的时候,也是没有睡眠好的时候。睡眠是如此的重要,但却很少有一本书来讲解如何好好睡眠,我也觉得奇怪。


精力除了脑力之外,很大一部分是一种体力消耗。所以有一个好的体力才能够支撑有一个好的精力,好的体力除了睡眠之外,非常重要的一点就是运动。我的主管,也就是我们整个事业部的总监,管理的大概有五六十号人,他在工作日每天的早上7点~8点是他的健身时间,每天他会提前来到公司做一个小时的健身。在健身完成以后,我们会发现他经常会保持一个非常好的一种工作状态,不管在沟通和表达方面,你都能看到他的精力满满,这种总监就是大家想跟随一起奋斗的人,毕竟大家不想跟着病恹恹的老板。我想这就是一种非常好的体力的管理方式,通过运动使得全身的肌肉能保持一个非常好的状态。



当然除了健身之外,还有很多非常方便的运动,比如说打羽毛球,比如说跑步,其实我最推崇的就是通过跑步来恢复精力。跑步的好处是比较方便,随时随地都可以操作,不需要额外的设备或者其他什么的。而且一定程度的有氧运动会使得整个心肺功能都会变得更好,更加的强健,当有一个良好的体魄之后,你自然具有更好的精力去面对一些更加复杂,更加有深度的事情。


更多精彩内容,关注公众号:ali老蒋,或点击加我好友深度沟通:ali老蒋 - java开发者


作者:ali老蒋
来源:juejin.cn/post/7288238840460591144
收起阅读 »

如何看待程序员不写注释

如何看待程序员不写注释 大家好,我是Leo🫣🫣🫣,今天我们来聊一下关于代码注释的问题话不多说,让我们开始吧😎😎😎。 在开始阅读正文之前,你先想 3 个问题: 你平时写代码的时候会写注释嘛? 你的注释是怎么样写的,主要都表达些什么? 你一般会在什么样的代码...
继续阅读 »

如何看待程序员不写注释



大家好,我是Leo🫣🫣🫣,今天我们来聊一下关于代码注释的问题话不多说,让我们开始吧😎😎😎。



在开始阅读正文之前,你先想 3 个问题:



  1. 你平时写代码的时候会写注释嘛?

  2. 你的注释是怎么样写的,主要都表达些什么?

  3. 你一般会在什么样的代码里写注释?



好了,正文开始。


1.我对注释的看法


首先,我个人刚开始写代码的时候,非常喜欢写注释,我一般会把代码思路先用文字表述出来。然后分成 1 2 3 4 每一步要干什么,怎么干。


然后写完之后开始在每个步骤下边填代码,这个时期我的代码注释量是非常高的。


但是后来随着技术熟练程度的提高,以及代码水平的提高,我的注释量就逐渐减少了。


并不是我觉得自己牛逼了不用写代码了,也不是我想专门给后人挖坑,纯粹是我觉得不太有必要了。


因为一方面我认为当你可以写出相对比较好的代码的时候,你的代码就是你的注释,你的命名、你的日志以及你的单元测试等等所有东西会共同构建成你的完整注释,最终他们合在一起形成的注释远比你一字一句写出来的注释要更清楚更实用。


并不是只有 // 后写的才叫注释。



2.不写程序的后果(狗头)


我们来简单聊一聊之前的一个国外新闻


image-20231011085512598


大家可能平时开开玩笑说,你不写注释可能被同事杀了,大家都当成一个笑话来听,但是当时美国程序员不写注释是真的在现实生活中上演。




以下内容来自网络。



据云头条报道,周三上午10点20左右,43岁的安东尼·汤(Anthony Tong)出现在办公室,拿出一把事先藏起来的半自动手枪开火。他在威斯康星州米德尔顿的这家公司工作了一年多。


工作人员纷纷逃离办公楼,跑到附近的公司避难。


行凶者随后向短短几分钟内赶到WTS Paradigm现场的警察开枪。四名警察随后开火,击中了嫌犯。嫌犯一送到医院就被宣布死亡。


WTS Paradigm的业务分析员朱迪·拉默斯(Judy Lahmers)说,当时自己正伏案工作,突然听到“像是有人把木板扔在地上,声音很响很响”。她赶紧跑出大楼,躲在一辆汽车后面。她告诉美联社:“我头也不回地拼命跑。你只想知道‘该躲起来还是跑远?”


她不知道关于枪击案的任何其他信息,但表示“完全出人意料。我们都是搞软件的。我们是很好的团队。”


警方介绍,这名死者自去年4月以来一直在WTS工作,没有犯罪记录,枪击事件发生时独自作案。目前,没有任何迹象表明到底是什么原因引起这起流血事件。


img



从这这个新闻,来说说我的看法:


1、代码不规范,确实看着蛋疼,尤其命名看不懂时,接手过去的代码,要去猜测对方代码,可能只有事人才看得懂。所以一定要规范,在大公司写的不规范,别人会直接怼你的。搞不好就是对你能力怀疑。


2、代码这个事情,有些人有洁癖,容不得垃圾代码在项目中,那么什么代码是垃圾代码,如命名不规范,成员变量没有表示其含义,函数名字不能充分表示其功能,大量if else逻辑,一个方法几百上千行代码,这些都是不良的习惯。


3、git提交时,老是覆盖提交,没有解决冲突,还有一次性改100多个类文件,1周才提交,有些兼容特殊处理地方不写注释,只有上帝才看懂。


4、凶手几名同事,肯定没有看过《重构,改善既有代码的设计》这本书,推荐大家好好读一读。避免类似悲剧发生


当然,还有一种情况我是建议写注释的,那就是二笔产品非要提一个不合理的需求导致你有一个不合理的写法,这个时候我希望你能注明“不是我要这么写的,是产品需求要求这样的,我也没办法的”的无奈,免得下一任接受你代码的人骂娘,说你是个菜鸡。


好了,今天的内容就到这里了。


3.总结


以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是Leo,一个在互联网行业的小白,立志成为更好的自己。


如果你想了解更多关于Leo,可以关注下面这个公众号,后面文章会首先同步至公众号。


4.参考文章



作者:程序员Leo说
来源:juejin.cn/post/7288340985229230099
收起阅读 »

H5车牌输入软键盘

web
前言 公司的业务背景是个大型园区,不可避免的要接触太多与车辆收费相关的业务,于是就有了这个车牌输入软键盘。对于车牌,用户手动输入的是不可信的,而且车牌第一位的地区简称打字输入实在是太麻烦,所以界定用户的输入内容,才能让双方都更加方便。 预览: pxsgdsb...
继续阅读 »

前言


公司的业务背景是个大型园区,不可避免的要接触太多与车辆收费相关的业务,于是就有了这个车牌输入软键盘。对于车牌,用户手动输入的是不可信的,而且车牌第一位的地区简称打字输入实在是太麻烦,所以界定用户的输入内容,才能让双方都更加方便。



预览: pxsgdsb.github.io/licensePlat… (请使用移动端打开)


github:github.com/pxsgdsb/lic…


gitee:gitee.com/PxStrong/li…



screenshots.gif

实现


因为车牌内容是固定的,所以直接写死在元素内。但是,为了提高组件的复用性,需要做一些简单的封装


; (function ($) {
function LicensePlateSelector() {
// 输入框元素
this.input_dom = `<ul class="plate_input_box">
<li class="territory_key" data-type="territory_key"></li>
<li style="margin-right:.8rem;"></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li data-end="end"></li>
<li data-cls="new_energy" data-end="end" class="new_energy">
<span>新能源</span>
</li>
</ul>`

// 键盘元素
this.keyboard_dom = `...省略`
}
/**
* 初始化 车牌选择器
* @param {string} config.elem 元素
* @param {string} config.value 默认填充车牌
* @param {number} config.activeIndex 默认选中下标 (从0开始)
* @param {function} inputCallBack 输入事件回调
* @param {function} deleteCallBack 键盘删除事件回调
* @param {function} closeKeyCallBack 关闭键盘事件回调
*/

LicensePlateSelector.prototype.init = function (config) {
config = {
elem: config.elem,
value: config.value || "",
activeIndex: config.activeIndex || false,
inputCallBack: config.inputCallBack || false,
deleteCallBack: config.deleteCallBack || false,
closeKeyCallBack: config.closeKeyCallBack || false,
}
this.elemDom = $(config.elem);
this.elemDom.append(this.input_dom);
this.elemDom.append(this.keyboard_dom);
// 监听输入
this.watchKeyboardEvents(function(val){
// 键盘输入回调
if(config.inputCallBack){config.inputCallBack(val);}
},function(){
// 键盘删除事件回调
if(config.deleteCallBack){config.deleteCallBack();}
},function(){
// 关闭键盘事件回调
if(config.closeKeyCallBack){config.closeKeyCallBack();}
})
// 输入默认车牌
if (config.value) {
this.elemDom.find(".plate_input_box li").each(function (index) {
if (config.value[index]) {
$(this).text(config.value[index])
}
})
}
// 选中默认下标
if(config.activeIndex){
this.elemDom.find(".plate_input_box li").eq(config.activeIndex).click();
}
};
})(jQuery);

watchKeyboardEvents()函数用于在元素创建完成后创建事件监听


/**
* 监听键盘输入
* @param {function} inputCallBack 输入事件回调
* @param {function} deleteCallBack 键盘删除事件回调
* @param {function} closeKeyCallBack 关闭键盘事件回调
*/

LicensePlateSelector.prototype.watchKeyboardEvents = function(inputCallBack,deleteCallBack,closeKeyCallBack) {
let _this = this
// 输入框点击
_this.elemDom.find(".plate_input_box li").click(function (event) {
// 显示边框
$(".plate_input_this").removeClass("plate_input_this");
$(this).addClass("plate_input_this")
// 弹出键盘
// 关闭别的键盘
$(".territory_keyboard").css("display","none")
$(".alphabet_keyboard").css("display","none")
if ($(this).attr("data-type") && $(this).attr("data-type") == "territory_key") {
if (_this.elemDom.find(".territory_keyboard").css("display") == "none") {
_this.elemDom.find(".alphabet_keyboard").animate({ bottom: "-50rem" }).hide()
_this.elemDom.find(".territory_keyboard").show().animate({ bottom: 0 })
}
} else {
if (_this.elemDom.find(".alphabet_keyboard").css("display") == "none") {
_this.elemDom.find(".territory_keyboard").animate({ bottom: "-50rem" }).hide()
_this.elemDom.find(".alphabet_keyboard").show().animate({ bottom: 0 })
}
}
// 点击新能源
if ($(this).attr("data-cls") == "new_energy") {
$(this).empty().removeClass("new_energy").attr("data-cls", "")
}
event.stopPropagation(); // 阻止事件冒泡
})

// 地域键盘输入事件
......
}

使用时html只需要创建一个根元素,js输入配置项,自动渲染组件。


<div id="demo"></div>
<script>
let licensePlateSelector = new LicensePlateSelector();
// 初始化
licensePlateSelector.init({
elem: "#demo", // 根元素id
value: "湘A", // 默认填充车牌
activeIndex: 2, // 默认选中下标 (从0开始,不传时,默认不选中)
inputCallBack:function(val){ // 输入事件回调
console.log(val);
let plate_number = licensePlateSelector.getValue(); // 获取当前车牌
console.log(plate_number);
},
deleteCallBack:function(){ // 键盘删除事件回调
let plate_number = licensePlateSelector.getValue(); // 获取当前车牌
console.log(plate_number);
},
closeKeyCallBack:function(){ // 关闭键盘事件回调
console.log("键盘关闭");
},
})
</script>

参数


参数类型必填说明示例值
elemString指定元素选择器"#demo"
valueString默认填充车牌"湘A"
activeIndexnumber当前输入框下标,从0开始,不传时,默认不选中2
inputCallBackfunction输入事件回调函数,返回参数:当前输入的值
deleteCallBackfunction键盘删除事件回调函数
closeKeyCallBackfunction关闭键盘事件回调函数

方法


getValue 获取当前车牌


let plate_number = licensePlateSelector.getValue();

setValue 设置车牌


licensePlateSelector.setValue("粤A1E9Q3");

clearValue 清空车牌


licensePlateSelector.clearValue();

END


如果觉得对你还有些用,顺手点一下star吧。


作者:彭喜迎MAX
来源:juejin.cn/post/7288609174124576783
收起阅读 »

喂,鬼仔!你竟然还在瞒着我偷偷使用强制相等

web
我们都知道JavaScript有== (强制相等)和===(严格相等)运算符进行比较。但你可能不知道它们两个究竟有什么不同,并且更重要的是,在 js 引擎中使用它们的时候发生了什么? 前面我们提到 == 是强制比较。强制意味着 VM 试图将进行比较的双方强制...
继续阅读 »

我们都知道JavaScript有== (强制相等)和===(严格相等)运算符进行比较。但你可能不知道它们两个究竟有什么不同,并且更重要的是,在 js 引擎中使用它们的时候发生了什么?


前面我们提到 == 是强制比较。强制意味着 VM 试图将进行比较的双方强制为相同的类型然后查看它们是否相等。以下我们列举了一些自动被强制相等的例子:


"1" == 1 // true
1 == "1" // true
true == 1 // true
1 == true // true
[1] == 1 // true
1 == [1] // true


你要知道,强制是对称的,如果a == b为真,那么b == a也为真。另一方面,只有当两个操作数完全相同时===才为真(除了Number.NaN)。因此,上面的例子都真实的情况下都是假真 (即,在 === 的情况下是 false 的)。



为什么强制相等有这样的问题,这要归咎与强制相等的规则。


强制相等的规则


实际的规则很复杂(这也是不使用==的原因)。但是为了显示规则有多么复杂,我通过使用===实现了==,带大家看看强制相等的规则到底多复杂:


function doubleEqual(a, b) {
if (typeof a === typeof b) return a === b;
if (wantsCoercion(a) && isCoercable(b)) {
b = b.valueOf();
} else if (wantsCoercion(b) && isCoercable(a)) {
const temp = a.valueOf();
a = b;
b = temp;
}
if (a === b) return true;
switch (typeof a) {
case "string":
if (b === true) return a === "1" || a === 1;
if (b === false) return a === "0" || a === 0 || a == "";
if (a === "" && b === 0) return true;
return a === String(b);
case "boolean":
if (a === true) return b === 1 || String(b) === "1";
else return b === false || String(b) === "0" || String(b) === "";
case "number":
if (a === 0 && b === false) return true;
if (a === 1 && b === true) return true;
return a === Number(String(b));
case "undefined":
return b === undefined || b === null;
case "object":
if (a === null) return b === null || b === undefined;
default:
return false;
}
}

function wantsCoercion(value) {
const type = typeof value;
return type === "string" || type === "number" || type === "boolean";
}

function isCoercable(value) {
return value !== null && typeof value == "object";
}

这是不是太复杂了,我甚至不确定这是正确的! 也许有你知道更简单的算法。


但有趣的是,你会发现在上面的算法中,如果其中一个操作数是对象,VM 将调用. valueof()来允许对象将自身强制转换为基本类型。


强制转换的成本


上面的实现很复杂。那么===== 要多浪费多少性能呢? 看看下面这张图,我用基准测试做了一个对比:


image.png


其中,图表中越高表示越快(即,每秒操作次数越多)。


首先我们来讨论数字数组。当 VM 注意到数组是纯整数时,它将它们存储在一个称为PACKED_SMI_ELEMENTS的特殊数组中。在这种情况下,VM 知道将 == 处理为 === 是安全的,性能是相同的。这解释了为什么在数字的情况下,===== 之间没有区别。但是,一旦数组中包含了数字以外的内容,== 的情况就变得很糟糕了。


对于字符串,===== 的性能下降了 50%,看起来挺糟的是吧。


字符串在VM中是特殊的,但一旦我们涉及到对象,我们就慢了 4 倍。看看 mix 这栏,现在速度减慢了 4 倍!


但还有更糟的。对象可以定义 valueOf,这样在转换的时候可以将自己强制转换为原语。虽然在对象上定位属性可以通过内联缓存,内联缓存让属性读取变得快速,但在超大容量读取的情况下可能会经历 60 倍的减速,这可能会使情况更糟。如图中最坏情况(objectsMega)场景所示,===== 慢15 倍!


有其他使用 == 的理由吗


现在,=== 非常快! 因此,即使是使用 === 的15倍减速,在大多数应用程序中也不会有太大区别。尽管如此,我还是很难想出为什么要使用 == 而不是 === 的任何理由。强制规则很复杂,而且它存在一个性能瓶颈,所以在使用 == 之前请三思。


作者:编程轨迹
来源:juejin.cn/post/7216894387992477757
收起阅读 »

国庆,与山重逢

重庆多山,重庆的县城也多山。从主城回到重庆最东北,是横穿一座又一座山,是横跨一座又一座桥。桥不跨江,是用来连接两座山的。 每当向不很了解重庆的朋友分享我的回家路时,他们总不相信:“重庆不是一个直辖市么?你回家怎么可能要8小时的?” 我回家是真需要至少8小时的。...
继续阅读 »

重庆多山,重庆的县城也多山。从主城回到重庆最东北,是横穿一座又一座山,是横跨一座又一座桥。桥不跨江,是用来连接两座山的。


每当向不很了解重庆的朋友分享我的回家路时,他们总不相信:“重庆不是一个直辖市么?你回家怎么可能要8小时的?”


我回家是真需要至少8小时的。国庆第一天上午11点出发,晚上9点回到山上家中,除去路上堵车的两小时,全程整好8小时。


即便回家很远,回家的路很难走,我依然很喜欢回家。


我从没仔细想过自己为什么喜欢回家,只是每年国庆劝说阿妮回家用的说辞总一样:“爷爷奶奶外公外婆都在家。”


我的母亲今年也在家,所以今年国庆,绝大部分时间是在山上度过的。


大概是10年前,绝大部分“高山”——单纯的字面意思,山的高处——住户搬到低山,高山上的住户,只剩十几家。山上村子住的人家变少,便给了山很大的自由。


原有的山路,平日里少有人行走,路面长满各式各样我全不记得名字的草,郁郁葱葱,互相缠绕。路旁斜坡新长出许多很小的树,它们不管旁边是否有路,只向空旷处挤,挤着挤着,就没了路。如果从没走过这些路,是肯定看不出来曾经有过路的。稍远处老些的大树,掉落的枯的枝丫,触手可及,捡柴再不用去很远地方,只沿着路挨着捡便好。


原有的山田,在退耕还林时全种上了果树,核桃与板栗。不知是水土不服还是品种不佳,核桃树只剩下些印象,田中长起来的,只有板栗。十多年过去,板栗成了山田里的佼佼者,每一棵树的主干,都有大腿那么粗。


搬走的人家多了,没搬走的也大都外出打工只在过年时回家,于是还喂猪的人家更少,山中落叶不再被收集回家为猪铺床。再走远些,林间落叶铺了一层又一层,厚厚的,挡住菌子的冒头路线。


图片


母亲一大早沿路捡的菌子


菌子,是山中的特产,春天有,夏天有,秋天也有。母亲说:“秋天菌子不闹人(‘闹人’是无毒的意思),最好吃。春夏的菌子就要注意,有些吃不得,要挑一哈。”


捡菌子的最好时机,是下雨后的第二天,有些刚冒出头,有些刚长成型。长过(腐烂)生蛆?此时是不会的。


母亲是捡菌子的好手,似乎所有菌子她都认识。我没有学到捡菌子这门手艺,只在菌子回家后跟着母亲洗菌时认识几个品类。


石灰菌是白色的,山里最多,平均体型最大,吃起来脆脆的不爽口。


红菌子好吃,但需要仔细辨认,有许多其它红颜色的菌是不能吃的,能吃的要肥厚一些。


蜂窝菌伞把内部像蜂窝,伞面滑滑的,只在秋天有。它是我最喜欢吃的菌子,炒好的成品入口也滑滑的,一嗦就进了肚;如果吃的慢些,咀嚼两次,又会发现它也是脆脆的;蜂窝菌,只放油、盐、大蒜和辣椒,味道就已经很好。


我听过的名字,还有枞树菌、紫檀菌,它们并不多见,我暂且只记得名字不记得长相与口感。


我们三个帅的计划,是国庆第二天上山捡菌子。


计划依据天气预报——国庆第二天小雨,后面几天,要么是中雨要么是大雨——制定。天气预报不准确,真正的小雨,只在下午出现一小会儿。我极不愿意极不建议天黑走山路,于是宝帅的下山时间,定在下午6点。


雨真正变小的时间,是下午4点半,一个半小时时间,四个人一起,能从山中收获些什么呢?


答案是半背板栗与一碗菌子。


四个人,两个筐筐,一个装菌子一个装板栗;一把弯刀一把火钳,弯刀用来开路——砍去那些挤在路上的树枝与刺条,火钳用来捡板栗的有刺包子;再背一个背篓,万一筐筐装不下呢?


时间很紧,意犹未尽。


母亲将板栗硬塞给宝帅一行,留下的一碗菌子,是当晚桌上的一盘菜。


图片


炒熟的菌


菌的做法,是简单的。菌子去跟,摘掉树叶,洗净泥巴,煮半小时;捞出用凉水泡一泡,将大的菌撕成小的适合入口形状,再洗再煮再捞出;锅内放油放蒜放辣椒,炒香装盘。


菌的味道极好。


图片


八月瓜壳


我知道的能在山上长出果子的果树,有苹果、梨、杏、枣、桃、山楂、板栗和八月瓜。苹果、梨、杏、枣、桃和山楂,都需要人的维护——剪枝或是嫁接,不维护的果树,任它自然生长,要么过两年枯掉,要么果小不好吃。


不用维护的,是板栗和八月瓜。八月瓜纯野生,我见的不多,但板栗,是一直都存在的。


十几年前,高山上的人家很多,捡板栗需要走很远,走到悬崖边,走到“弯里”(山的最里面,很大一片山里只有一户人家),走到绝大部分人不愿去的地方。


我印象中的第一次全家捡板栗,是高中时的某个国庆,母亲和贵嬢嬢,带着各自小孩,背背篓提筐筐不带弯刀,一大群人去弯里。


弯里的板栗很小,不像新长起来的山田里的品种。


飞包土是我们家最远、最高的一块田,它是退耕还林时被最先“退”掉的,田里栽的树,是板栗。时间过去十几年,山田真的变回树林,板栗成了山里的树。


国庆离开家的那天上午不下雨,我和阿妮上飞包土,再捡半筐板栗。


图片


刚洗过的板栗


今年国庆,与山重逢。


作者:我要改名叫嘟嘟
来源:juejin.cn/post/7288163743035965440
收起阅读 »

唱衰这么多年,PHP 仍然还是你大爷!

web
PHP 是个庞然大物。 尽管有人不断宣称 PHP “即将消亡”。 但无法改变的事实是:互联网依然大量依赖 PHP。本文将通过大量的数据和事实告诉你为何 PHP 仍然在统治着互联网,你大爷仍然还是你大爷。 统计数据 PHP 仍然是首选编程语言 根据 W3 ...
继续阅读 »

PHP 是个庞然大物。


尽管有人不断宣称 PHP “即将消亡”。



但无法改变的事实是:互联网依然大量依赖 PHP。本文将通过大量的数据和事实告诉你为何 PHP 仍然在统治着互联网,你大爷仍然还是你大爷



统计数据


PHP 仍然是首选编程语言



根据 W3 Techs 对全球前 1000 万个网站使用的编程语言分析,我们可以看到:



  • PHP 占比 77.2%

  • ASP 占比 6.9%

  • Ruby 占比 5.4%


基于 PHP 的内容管理框架


绝大多数公共网站都是通过 PHP 和 CMS 来构建的。根据市场份额,12 大 CMS 软件中有 8 个是用 PHP 编写的。下面的数据来自 W3 Techs 对前 1000 万个网站的 CMS 使用情况调查,每个百分点代表前 1000 万个网站中的 10 万网站。



  • [PHP] WordPress 生态系统 (63%)

  • [Ruby] Shopify

  • Wix

  • Squarespace

  • [PHP] Joomla 生态系统 (3%)

  • [PHP] Drupal 生态系统 (2%)

  • [PHP] Adobe Magento (2%)

  • [PHP] PrestaShop (1%)

  • [Python] Google Blogger

  • [PHP] Bitrix (1%)

  • [PHP] OpenCart (1%)

  • [PHP] TYPO3 (1%)



不得不说,Wordpress 在内容管理领域依然站有绝对的统治地位。


PHP 在电商领域的应用


根据 BuiltWith 2023 年 8 月对在线商店的报告,我们可以看到 PHP 在电商领域仍然占统治地位:




趣闻轶事


Kinsta 发表了一篇文章,证明 PHP 仍然很快,仍然很活跃,仍然很流行:



早在 2011 年,人们就一直在宣称 PHP 已死。但事实是,PHP 7.3 的请求处理速度是 PHP 5.6 的 2-3 倍,而 PHP 8.1 则更快。正因为 PHP 的普及,我们可以很轻松地招聘到有经验的 PHP 开发者。



Vimeo 工程师 Matt Brown 在《这不是遗留代码,而是 PHP》一文中表示:



PHP 从未停止创新。尽管我们计划将 500,000 行的 PHP 代码划分为多个 [服务],但最终这些建议都没有被采纳。


Vimeo 自 2004 年以来规模扩大了数倍,我们的 PHP 代码库也是如此。



Ars Technica 发布了一个包含历史数据的 W3 Techs 报告,证明 PHP 仍然遥遥领先



尽管 PHP 有许多臭名昭著的怪癖,但它似乎还能活很久。从 2010 年的 72.5% 市场份额增长到今天的 78.9% 市场份额,目前还没有任何明显的竞争对手能让 PHP 感到威胁




针对 Python 创始人 Guido van Rossum 的一个采访播客中,Lex Fridman 如是说:



Lex: 目前互联网的大部分后端服务仍然是用 PHP 写的


Guido: 没错!



Daniel Stenberg 在其年度 Curl 用户调查(第 18 页)中统计了用户使用 curl 的方式。直接使用 curl 命令行的用户占比最高(78.4%),用户最熟悉的方式就是在 PHP 中使用 curl,自 2015 年调查开始以来一直都是这个结果。2023 年的调查报告显示有 19.6% 的用户在 PHP 中使用 curl。



curl (CLI) 78.4%, php-curl 19.6%, pycurl 13%, […], node-libcurl 4.1%.



Ember.js 虽然起源于 Ruby 社区,但作为一个前端框架,它可以与任何后端配合使用。Ember 的社区调查报告显示,PHP 是受访者第三喜欢的选项,仅次于 Ruby 和 Java。



Ember 的调查还询问了一些通用的行业问题。例如,有 24% 的受访者表示他们的基础设施都是“自托管”,而不是依赖于主流的云服务提供商。虽然这项调查本身不能完全代表整个行业,但结果仍可能会让人大吃一惊,特别是对那些依赖社交媒体和会议演讲来了解商业现状的人来说更是如此。对于企业来说,现在准备好云退出战略(例如 NHS)比以往任何时候都更加重要。你可以阅读 Basecamp 的文章了解云退出战略是如何为他们每年节省数百万美元的。


大规模 PHP 应用


上述统计数据衡量了不同网站和公司的数量,其中绝大多数是基于 PHP 构建的。但所有这些只告诉我们它们的规模在前 1000 万名之内。那前 500 名呢?


Jack Ellis 在《Laravel 能否扩展?》这篇文章中指出,你不应该仅根据每秒可以处理的请求数量来做选择。大部分业务都不太可能达到那个水平,而且还会面临很多其他瓶颈。但事实证明,PHP 是可以扩展到这一水平的语言之一。




当看到我们的软件(基于 Laravel 构建的 Fathom Analytics)增长迅猛时,我们从未怀疑过“这个框架是否能够扩展?”。


我与多家企业合作过,他们利用 Laravel 支撑整个业务运营。像 Twitch、Disney、New York Times、WWE 和 Warner Bros 这样的公司也在他们的多个项目中使用 Laravel。Laravel 能够轻松应对大规模的应用需求。



Vimeo 工程师 Matt Brown 在《这不是遗留代码,而是 PHP》一文中强调:




可以很明确地告诉你们,PHP 还是你大爷。Vimeo 在 PHP 方面的持续成功就是证明,在 2020 年它仍然是快速发展的公司的绝佳工具。



Vimeo 还以开发流行的 PHP 静态分析工具 Psalm 而闻名。


Slack 公司首席架构师 Keith Adams 在《认真对待 PHP》一文中提到:




Slack 服务端大部分应用逻辑都是由 PHP 来执行的。


相比于 PHP 的优势而言(通过故障隔离减少 bug 成本;安全并发;高吞吐量),PHP 存在的问题可以忽略不计。



我们再分析一下 W3 Techs 的报告,分析部分业务比较单一的公司的规模。规模最大的是 WordPress,它驱动着 Automattic 的 WordPress.com。每月有 200 亿次页面访问(Alexa 全球排名 55)。


如果我们继续往下看,来到占市场份额 0.1% 的条目,可以看到大量的网站都是靠 PHP 系统来支撑的,PHP 仍然是 10w 小网站的首选框架。



MediaWiki维基百科背后的平台,每月有 250 亿的页面浏览量(Alexa 排名 12)。同时 MediaWiki 还驱动着 Fandom(每月有 20 亿的页面浏览量,Similarweb 排名 44)和 WikiHow(每月有 1 亿访问者,Alexa 排名 215)。



除此之外还有一大批互联网公司由 PHP 驱动,例如 Facebook(Alexa 排名 7)、Etsy(Alexa 排名 66)、Vimeo(Alexa 排名 165)和 Slack(Similarweb 排名 362)。


Etsy 之所以引人关注,是因为它有高比例的活跃会话和动态内容。这与维基百科或 WordPress 不同,后者可以从静态缓存中提供大多数页面视图。这意味着尽管规模相似,但 Etsy 的 PHP 应用程序更容易受到高流量的影响。


Etsy 也是 PHP 创始人 Rasmus Lerdorf 的东家。他有时会在技术分享中展示 Etsy 的代码库片段。(极客旁注:他在 2021 年的现代 PHP 讲座中解释了 Etsy 是如何使用 rsync 进行部署的,就像 Wikipedia 在过去 10 年使用 Scap 一样)。Etsy 的官方博客偶尔会提到他们对模块化 PHP 单体的工作进展,例如 Plural 本地化。有时也会放出详细的 Etsy 站点性能报告



很高兴地告诉大家,升级到 PHP7 之后,本季度整个网站的性能都得到了提高,所有页面的性能都有了显著的提升。



我的观点


大多数人认为,PHP 社区似乎在公共舆论中占据的空间不大。无论是 PHP 核心开发者 , 还是 PHP 软件包(例如 Laravel、Symfony、WordPress、Composer 和 PHPUnit)的作者,亦或是日常工作中使用 PHP 的普通工程师,我们很少在社交媒体上的争论中看到他们的身影。


你也很少看到我们在会议上做演讲,宣称某个技术栈“绝对会”为你的公司带来裨益。如果你听了某些 JavaScript 框架粉丝的演讲,你可能会认为大多数公司今天都在使用他们的技术栈。


我不是说 JavaScript 不好,而是某些人在没有考虑技术或商业需求的前提下给出了“xxx 最好”的断言。这是一种过度营销,你怎么知道它最好?你跟别的语言比较过了吗?


我也不是说 JavaScript 没有用武之地,我们要辩证地看待世间万物。你可以分享你的经验和成果,比如哪些行得通,哪些行不通。要持续探索、持续创新、持续分享,持续推动人类前进。这就是自由软件的精神!


你可能看过《The Market for Lemons 》和《A Historical Reference of React Criticism》这两篇文章,他们都指出了 JS 的问题。但是 ... React 仅占有 3% 的市场份额。再加上其他的小框架(Vue、Angular、Svelte),这个数字才达到 5%。而基于 Node.js 的 Web 服务也仅占有 3% 的市场份额。这是否意味着超过 90% 的人都错过了 PHP?


别忘了,这 5% 代表了 50 万个主要网站,这是一个巨大的数字。Node.js 有自己的优势(实时消息流)。但是,Node.js 也有其弱点(阻塞主线程)。另外要强调一点:市场份额并不能完全反映规模。你可能驱动着排名前 1% 的几个大型组织,也可能驱动着排名后 1% 的组织。或者像 WordPress 那样同时支撑排名前 1% 和其他 4000 万个网站。


结论


无论是老公司还是小公司,无论其规模大小,可能都没有使用我们在公共场所经常听到的技术栈。如果不考虑个人项目和烧钱的初创公司,其他公司的这个现象更为明显。


对于正在成长和持续经营的企业来说,PHP 是否能够成为企业首选的前三名语言?当一个企业和其团队在扩大规模时,编程语言是否完全不重要?我们不得而知。


我只知道如今有许多企业都在使用 PHP,而 PHP 已被证明是一种可持续的选择,它经受住了时间的考验。例如,像 Fathom 这样的新公司,在短短三年内就实现了盈利。正如 Fathom 的文章所说,大部分公司的业务永远达不到那种规模。不过话又说回来,即使面对大规模的业务,PHP 仍然是一种经济可持续的选择


那么问题来了,PHP 是唯一的选择吗?当然不是。


有的语言速度更快(Rust),有的语言社区规模更大(Node.js),或者编译器更成熟(Java),但这往往会牺牲其他价值。


PHP 达到了某种柔中取刚的平衡点。它速度很快,社区规模较大语法现代化开发活跃,易于学习,易于扩展,并且拥有一个庞大的标准库。它可以在大规模场景下提供高效和安全的并发,而又没有异步复杂性或阻塞主线程的问题。由于平台稳定,加上社区重视兼容性和低依赖性,它的维护成本往往较低。


当然,每个人的需求不尽相同,但想要达到上述的这种平衡点,PHP 是少数几个能满足需求的软语言之一。除此之外还有哪个语言可以做到?


作者:米开朗基杨
来源:juejin.cn/post/7288963080855617573
收起阅读 »

离开了浪浪山,简直不要太爽

web
今年年初的时候,《中国奇谭》火了,与其说是《中国奇谭》火了,还不如说是这个动漫和普通打工人太有共鸣了,动漫里面的小猪妖是很多普通打工人的写照,毕业进入了父母亲戚以为很不错的工作,领着一份不多不少的工资,每天要处理各种工作上的事情,事情比较多的时候,还需要经常加...
继续阅读 »

今年年初的时候,《中国奇谭》火了,与其说是《中国奇谭》火了,还不如说是这个动漫和普通打工人太有共鸣了,动漫里面的小猪妖是很多普通打工人的写照,毕业进入了父母亲戚以为很不错的工作,领着一份不多不少的工资,每天要处理各种工作上的事情,事情比较多的时候,还需要经常加班。每个人都想和小猪妖一样离开浪浪山,不过最近我却离开了浪浪山。


公司裁员


准确的说是公司裁员了。人事通知我,说去下会议室,当时我就有预感到是要裁员了,因为之前公司就开始裁员了。一开始就是人事主管就说:最近工作怎么样?我就猜到了基本就是要裁员了。后面赔偿也符合我的预期。谈好了赔偿,做了工作的交接,和几个同事吃了一个饭,就和这个工作了几年的公司拜拜了。


开始的时候也是挺不适应的,自从大学毕业之后,一直都是有规律的上班生活,每个月都有一份固定的工资领,能维持日常开销,多余的钱投投资,日子过得也还行。忽然一下子没了工作,意味着就没有了收入了,要为下个月的房租担心了,不过好在赔偿金还能维持几个月。


今年行情普遍不太好,身边也有失业的朋友,有的找了几个月还没有找到工作。有的朋友还说好的公司面试基本都要二本以上和三年工作以下的面试,总体来说要求还是比较严格的,如果不出去面试,也不会意识到现在就业行情的严峻。后面索性就先玩玩吧,去周边走走、去附近的香港走走。


周边逛逛


首先就准备去盐田那边玩,经常看小红书有人分享那边的打卡地方,有海上图书馆,打定主意就出发。路过一个地铁口,看到一些可爱的动漫,灌篮高手、海贼王。



还有可爱的一个公交车,这个公交完美的贴合的墙壁上,门框刚好做成一个上下的车门,设计的比较巧妙。



之前上班的时候,走路的都是匆匆忙忙的,上班都比较辛苦,周末都基本就用来补觉休息。出门人也比较多,现在人都比较少,慢慢走,欣赏沿途的风景




坐了一个小时的地铁到了海山地铁站,映入眼帘就是清澈的海水,远离的城市的喧嚣,欣赏自然的美景。往里面走就看到了海上图书馆,环境还是挺不错的,海水比深圳湾的清澈多了。




沿着上面的海边一直散步,享受这海风吹拂的感觉,小雨淅淅沥沥的下,听着下雨的声音,一边走,一望无际的白云和天空,让人身体特别放松。






**程序员都是脑力工作为主,坐在工位上一坐就是几个小时,运动量比较少,都是固定的上下班,周末也基本是休息。**不过固定的生活模式过久了就会感觉很单调和平淡,每天都生活的都是复制,也会让人感觉很无聊,所以还是要多出去走走,体验一下不一样的生活。



雨天爬山


去玩海边之后,之后一直在下雨,之前也经常爬山,不过都是天气不错的时候爬的,这次就尝试一下雨天爬山吧。


因为开始爬山的是下午 2 点多,人不是很多,上山看到了很多下山的人。一路上也没什么人了。





快到山顶的时候,就开始下雨了,天也变暗了,雾也越来越大了。



上山的时候还能看到山下的房子,现在都看不请了。还以为误入衡山了。




在亭子上躲雨休息,随着天越来越暗,山下的灯光一点点打开,路上的车灯,路边的路灯。直到点亮所有的灯光,在马路上形成一道靓丽的风景线。



后面还去了各种公园,还去了一趟香港,再去了一趟香港大学。可以说这几周的经历比我上几年的经历都多


不上班真爽


不用每天上班,不用处理各种问题,也没有时间焦虑症(每天到哪个点就要上班,哪个点就要下班),这段时间完全不需要考虑时间的问题,想去哪里就可以立刻去哪里。不需要请假,不需要调休,晚上玩的比较晚了也不用担心第二天要早起上班起不来。不需要为工作而烦恼,只做自己想做的事情。


上面不是讲了去海边玩吗,走海边走路的时候,走着走着,竟然感觉到自己饿了,很难得有这种感觉。只有读书的时候,在外面运动了很久才会感觉的饥饿。


目的性不强的做事,也没有时间上的焦虑。没有压力的做事才是最自然的、最舒服、最享受的做事


失业焦虑吗


被通知裁员的时候,虽然心里有些准备,但是真的听到被裁的时候,心里还是有些焦虑,特别是现在就业行情也不太好,感觉找工作还是有些困难的。 习惯了每天按部就班的上班,完成各种工作上的任务。周末放假偶尔出去玩玩,休息。基本都没有太大的变化。不过心里也不是很焦虑,对自己的技术还是挺有信心,坚持写文章,写 Github,扩大自己的影响力。工作上也比较努力、认真。博客写了快两年了,每天都在积累,阅读量最高的都有十万多了,有了一些积累,心里也更有底气了。



其实给公司打工的同时也要给自己的打工,在工作中一般有问题就需要立刻去解决,解决之后及时的总结和归纳,做事的同时也要积累的自己的经验。积累的越多,自己做事也就更快,做事也更有章程了。


现在自己也是把简历改好,投投简历。没有工作也适当的放松放松,去周边城市旅旅游。有面试就去面试。


写在最后


现在就业行情不太好,打工人还是需要有被裁员的准备。现在可能很多公司给打工人更多的压力。这时候就需要放平自己的心态,尽量把自己的工作做好。同时也要多做积累,多做输出,未雨绸缪。有工作的就好好工作,尽量提高自己的能力,能力提高了,才有有更多的成长。失业的也不要气馁,多投简历,降低消费。


无论有没有离开浪浪山,都需要努力并自信的生活。


作者:小码A梦
来源:juejin.cn/post/7288602155111563264
收起阅读 »

某37岁程序员感叹:存款200万加一套房,却不敢辞职!

200万存款,一套房,房贷只剩30多万,这样的条件可以说很不错了,但一个拥有这些的程序员却依然压力很大,甚至患上了抑郁症。这名程序员今年37岁,薪资30k,有200万存款,一套房还有30多万房贷。他说自己很疲惫,有抑郁症,压力很大,想裸辞在家休息一段时间,又怕...
继续阅读 »

200万存款,一套房,房贷只剩30多万,这样的条件可以说很不错了,但一个拥有这些的程序员却依然压力很大,甚至患上了抑郁症。

这名程序员今年37岁,薪资30k,有200万存款,一套房还有30多万房贷。他说自己很疲惫,有抑郁症,压力很大,想裸辞在家休息一段时间,又怕出来不好找工作,很纠结要不要辞职。


许多网友都劝他休息一下,毕竟身体是革命的本钱,何况刚刚发生了字节程序员猝死事件,让自己舒服一点更重要。


有人说,200万存银行,4%的利率,每个月收益6600,欲望不高的话完全够生活,可以休息一段时间再出发。


有人说,卷了那么多年,可以躺平做咸鱼了。

把生命浪费在加班上不值得。


有人说,三代之后的重孙辈都不一定知道他的名字,更别提他的生活和情感了,活好自己这辈子就够了,想歇就歇着,人生路还长,不必争朝夕。


有人建议楼主去一个整体年龄偏大的公司,这样就不会那么焦虑了。


有人建议楼主找一个to B的公司,或者非核心部门,不会太忙。


有人建议楼主去一个非互联网企业,薪资降一半,就会特别轻松。


也有人建议楼主先在公司内躺平,同时找外面的机会,不要裸辞。


还有人说,楼主先摸摸鱼,实在没法做了就休息一段时间,然后可以跑外卖、滴滴、快递,或者做点小生意。


某45岁程序员说,等楼主过了40岁就不抑郁了,因为那时候就没人愿意接简历了。楼主可以思考一下自己五六十岁想做什么,现在就可以开始去做了。


另一部分网友劝说楼主继续卷,不然等失业了,想卷都没有平台。生活就是这样,坚强一点!


在这个高速发展、压力山大的社会,躺平成为许多人梦寐以求的目标。存够下半生的养老钱,提前退休,财富自由,这些正是如今许多年轻人拼命工作的动力,为此他们不惜消耗自己年轻的身体和健康,以期在中年时能够过上自己想要的生活。

但多少钱才能躺平呢?200万存款对有些人而言足够了,对有些人却远远不够,这取决于人们的现状和对未来的打算:在哪个城市发展?是否结婚?是否有孩子?每一个问题都会带来更多负担,也决定了多少钱才能让人停止内卷。

有一点很重要,无论能否躺平,都别搭上自己的健康。该休息的时候就休息,什么都没有生命重要。如果真的很累,可以考虑找一份清闲稳定的工作,实现生活和工作的动态平衡。缩减欲望,降低标准,对自己少一点要求,多一点爱惜。

作者:行者

来源:mp.weixin.qq.com/s/jjk5KVD4B0sdujxisR9cSw

收起阅读 »

面试官是自己前女友,全程被拷问~

开口我说了一句,好巧,没想到真是你😂,她一笑说别废话了来吧,自我介绍一下吧。 我说我还需要介绍吗?你不都知道??  她给我来了句同学分清场合哈,注意面试纪律。请你做一下自我介绍。另外,搜索公众号Linux就该这样学后台回复“猴子”...
继续阅读 »

xdm,这是什么狗血剧情,面试居然碰到了前女友,而且还是最后一面的面试官,真的人都麻了,这放在整个面试界也是相当的炸裂......


真的是第一次在看面经的时候追起了故事,看着这哥们反复被虐,真的太带劲了。


跟前女友在一起快五年因为一些原因分手一年多了,期间再没联系过。昨天最后一轮hr面,邮件看到面试官跟前女友重名,心里想应该不会这么巧吧😭  没想到进了面试链接还真是她!!!!

兄弟们,当时的心情真的是绝了,尴尬到飞起。然后还要继续面试!!!!

开口我说了一句,好巧,没想到真是你😂,她一笑说别废话了来吧,自我介绍一下吧。 我说我还需要介绍吗?你不都知道??  她给我来了句同学分清场合哈,注意面试纪律。请你做一下自我介绍。另外,搜索公众号Linux就该这样学后台回复“猴子”,获取一份惊喜礼包。

我简单介绍了一下,然后开始问我以前不是说想做xxx吗?怎么投这个岗了?说说原因吧。我巴拉巴拉说了一通,她说这可不像你吧,再给你次机会重新说

我实话实说了她满意的点点头。

算了其他的不说了,她太了解我了,全程面试被拷问,被挑刺,然鹅我一点办法都没有,因为她一句想清楚再回答哈,回答的内容有问题在我这里会减分的。😤😤😤 那个表情!!!!😭😭😭

估计凉咯,没戏了
她告诉我说本来是她同事面我然后她看到我简历很“开心”,就说她来对接我。

大概半个多小时面完之后,她说面试结果后续会通知你的,有问题可以打邮件电话或者发邮件。然后她说:哦,不好意思忘记了,你手机号被我拉黑了,算了别打了,你等着吧
解释一下为啥她面试官了我才毕业。我俩本科同学,她本科毕业工作了,我中间休学两年又读了研!所以读研读了个寂寞。 

后续:

今天有进度了,但是还不如没有,我快让她气死了!!想骂人


昨天有兄弟给我支了招说可以换个手机号打电话问,我一听,豁然开朗,一想哎哟不错哟,好主意啊!下午给她打电话,第一遍没打通估计在开会啥的,过了半个小时我又打过去,她接了。

我说喂你好,是xxx的面试官吗?

她TM听出我声音了,就在那哈哈哈笑,说:“xxx谁教你的,换手机给我打!还你好~是xxx面试官吗~(此处请自行想象讥讽,小人得志的样子哈图片)干嘛说吧!”  我一听也不装了我说:“对,就是我,你不是自己说面试就得有个面试的样子吗?我这么说咋了?不行?”  她说:“别废话,我上班呢说干嘛?”

我说:“你说我干嘛?我问进度啊!我发邮件问你,你不回,能不能给个痛快,是死是活抓紧好不?让人泡池子好玩吗?”

她就搁那笑:“哦~我还以为你跑来找素材更新你帖子来,写的真不错啊,啥时候还有这手艺了。”

我听完脑瓜子一懵!!!

我说:“卧槽,你咋知道!”

她说:“你管我咋知道,我反正就是知道!我那天面你是那样?还拷打你?把你香的还。你进度我告诉你,我面评还没传,等着吧。嘿嘿”,然后啪!就把电话挂了,再打回去就拉黑了!

今天又换了个同学手机打,打通了,第一遍,我喂~  对面啪挂断了,第二遍,直接拒接,第三遍,我:别挂!她:滚!再打就拉黑了

昨天中午打算午休一下,结果接到了她的电话,因为手机号这么多年了确实记得太清楚了。接了电话,我说:哎哟,你这咋用自己手机给我打了??把我从黑名单里爬出来了?  

她说:别误会,公司座机坏了,只能手机给你打,你放心一会你还会回到属于你的地方的图片

我说:打电话啥事?要给我发offer了?

她说:虽然很不想承认,但是确实是给你发offer的,一会自己看看邮箱,不想来就赶紧拒了。(就那种很不情愿的那种感觉,自己想象)

我说:哈哈哈哈,果然该是我的还是我的呀!

她说:我真该给你面评的时候写的差点!你又不会来还白白占一个oc,浪费公司成本!

我说:谁说我不来!我不来我面啥!我时间不是时间是吧?

她接话说:哟哟哟,你真敢来?我是你们部门的hrbp天天见面,你不尴尬?不怕我给你穿小鞋了?

我说:反正我不尴尬,我感觉挺好,哈哈哈哈,谁尴尬谁知道,你又不是我leader,我不怕。

她说:行,那等着吧。啪电话就挂了。

傍晚就收到录用意向了,去还是不去呢?

大家觉得呢?如果是你,你会不会去?


来源:牛客网;作者:offer拿到吐1111

收起阅读 »

姚期智:人类本身就是世界上相当理想的具身智能体

60s要点速读:1、人类本身就是世界上相当理想的一个具身智能体。它基本上具备三个方面,三个成分:第一方面是身体,第二方面是小脑,第三方面是大脑。身体的部分具身必须要有足够的硬件,具有传感器和执行器,小脑会主导视觉、触觉各种感知来控制身体,完成复杂的任务,最后大...
继续阅读 »

60s要点速读:

1、人类本身就是世界上相当理想的一个具身智能体。它基本上具备三个方面,三个成分:第一方面是身体,第二方面是小脑,第三方面是大脑。身体的部分具身必须要有足够的硬件,具有传感器和执行器,小脑会主导视觉、触觉各种感知来控制身体,完成复杂的任务,最后大脑部分,它主导上层的逻辑推理、决策、长时间的规划以用自然语言能够和其他的智能体、环境交流。

2、ChatGPT主要是对于语言的处理能力,如果真正的想要让通用人工智能发挥出它的力量,未来的AGI需要有具身的实体,同真实的物理世界相交互来完成各种任务,这样才能给产业带来真正更大的价值。

3、具身机器人目前遇到的主要有四大挑战:第一,机器人不能够像大语言模型一样有一个基础大模型直接一步到位,做到最底层的控制。第二,计算能力的挑战。即使谷歌研发的Robotics Transformer模型,要做到机器人控制,距离实际需要的控制水平仍有许多事情要做。第三,如何把机器人多模态的感官感知全部融合起来,仍面临诸多难题需要解决。第四,机器人的发展需要收集很多数据,其中也面临很多安全隐私等方面的问题。

正文:

最近,ChatGPT的出现,在人工智能在学术上是一个突破,同时它为各行各业也创造了许多新价值。所以人工智能的下一步是什么呢?ChatGPT主要是对于语言的处理能力,如果真正的想要让通用人工智能发挥出它的力量,未来的AGI需要有具身的实体,让它能够同真实的物理世界相交互来完成各种任务,这样才能够带来真正更大的一个价值。
那么,具身智能体长的应该是什么样子呢?人类本身就是世界上相当理想的一个具身智能体。它基本上具备三个方面,三个成分:第一方面是身体,第二方面是小脑,第三方面是大脑。身体的部分具身必须要有足够的硬件,具有传感器和执行器,小脑会主导视觉、触觉各种感知来控制身体,完成复杂的任务,最后大脑部分,它主导上层的逻辑推理、决策、长时间的规划以用自然语言能够和其他的智能体、环境交流。目前,清华大学交叉信研究院里有八九位老师近年来的工作都是在关于具身智能的方方面面。接下来我想从这些团队的一些进展和思考方面,和大家分享。
第一,关于身体部分。具身AGI最理想身体的形式,我们认为应该就是人形机器人。因为人类的社会环境主要是为人类而定制的,比如说楼梯的结构、门把手的高度、被子的形状等等,这些都是为了人类的形状而定制,所以如果我们能够打造一个有泛应用的通用机器人,人形是最好最适合的一个形态,人形机器人能够适应人类的各种环境。
在清华大学交叉信息研究院里,我们自主研发了人形机器人初步的造型,这个工作主要由陈建宇团队所完成的。目前我们已经有了两个形式的机器人,其中有一个是前几个月在世界人工智能大会上亮相的“小星”。它的高度是1米2,而这次我们在这个机器人大会里面亮相的是“小星MAX”,它的身高达到了1米6,这两款机器人在展区有进行展示。

关于它的技术:它所用的是新一代的本体感知驱动器技术方案,在算法方面采用了动态的双足行走,是世界上为数不多的,能够走通整个软硬件技术的团队之一。

其次,关于具身智能体第二方面的小脑如何体现呢?比如小星机器人实体上是一套机器人运动控制的算法,分成两层:上一层是固态规划层,下一层是基于动力学的实时全身运动控制,它用来计算发给电机关节精确的指令。我们再展示一下这几个机器人在户外运动的画面,可以看到左边小星可以在水泥地上很灵活的快速行走,在右边也可以在比较复杂的一个树林里面走,它具有一定的抗干扰的能力——在草地里、石子路上走的也具有稳定性。
在构建小脑的算法端,我们想到在未来需要给机器人更好的功能、更好的控制,所以我们也在研究灵活度更高的,利用人工智能、强化学习的方法去运用和强化学习框架。它的好处是没有一个模型的限制,所以它能够对于复杂的环境跟不确定的环境,能够展现出更强的适应的能力。另外还有一个方法来学习,就是能够利用人体运动实际的数据,我们把它放到这个框架里,给予强化学习更好的引导。
我们可以看到,通过强化学习,机器人能够用一种自然的方式来模拟人态的行走,在设计上我们可以使它消耗更低的能耗,我们把这个硬件参数代入仿真里,能够实现更高度的运动形态,比如在仿真里能够走到4米/秒。而除了这种方法以外,强化学习方面,清华大学交叉信息研究院里的队伍也来研究一些基础的核心技术,尤其是在机器人研究方面,能够使得强化学习更加有效。

第一是有关样本的效率方面,目前一直困扰着强化学习应用的难题它所需要的样本非常多。在这方面我们做了一些工作。比如Atari游戏作为标准测试的指标,Deepmind在2015年在自然上发表了DQN算法,需要花一千个小时去进行学习,才能够达到人类的水平,这在当时已经非常了不起,而高阳队伍提出了一个新的算法叫Efficient Zero,它能够在两小时时间里能够达到超过人类平均水平,比DQN提高了500倍的样本效率。
另外一个困扰着强化学习的难题是泛化性,就是对于这些任务及其环境中间的不确定性和干扰,能不能够泛化的更好,许华哲团队围绕着这个问题提出了一系列解决方案,比如应用到机器人包饺子的演示,我们可以看到在这个物理过程里面有些非常复杂的动作,使得算法适应性高,即使有人为干扰下也能够达到任务。
我们再看小脑方面。除了走路以外别的功能,其中一个重要的任务是视觉处理,赵行团队有一些最新工作:基于视觉机器人跑酷,在这里面四足机器人基于视觉信号能够识别路障,能够匍匐前进,能够跳高台,同时请注意到当这些跳跃失败的时候,这个机器人会不停的来尝试,一直到成功为主,未来我们也会把这类跑酷功能放到人形机器人来实现。

清华大学交叉信研究院赵行团队四足机器人
还有一个比较高端的感知就是触觉。人的皮肤吸收了很多的触觉信号,能够完成非常精细的物体抓取的动作来回避危险,所以我们希望给机器人能够有好的触觉的传感器,让它们能够触摸感受到这个世界。对此,许华哲队伍运用到一些非常好的材料,他设计了一个触觉传感器低成本、易操作,能够精确的感觉得到接触到物体三维的几何,还有能够捕捉到物体很细小的纹理,它和人工算法能够结合,能够达到物体的分割和最终的效果。并且,我们也做了一些下游的关于触觉物体操纵的触觉工作,希望机器人将来对于更小的物体能够操作。此外比较难的事情,就如何打造机器人灵活的双手,需要自由度非常高,接触和物件非常复杂,所以机器人想要做这些动作非常困难。弋力团队提出新的算法,可以用自动的方式来创建场景和建模仿真,使得机器人在仿真里学习到这些技术。
最后我们谈一谈关于机器人第三方面关于大脑。这一部分谷歌做了大量的工作,特别是Palm-e多模态的大语言,能够对机器人的任务进行规划,大语言模型就把他所做的事情调用到下沉的控制器,去按照这个顺序来做任务,这也是一个非常重要的,尤其是谷歌在具身大模型方面主要的技术路线。
不过,这个框架有一个主要的问题:它的下层不一定能够很好执行上一层的规划,尤其是中间如果发生一些意外的干扰。对此,陈建宇团队提出一个新的方案和新的框架,比如是否可以在任务执行中能够自动的判断是不是有异常,如果有异常的话怎么样解决,这些都是有一个语言模型和视觉模型自动的完成的。我们把这个方法用在了人形机器人上。首先我们需要像大语言模型一样,给这个机器人描述一下他所需要的任务,机器人按照任务来执行。在场景工作中,如果机器人做搬箱子的工作,它的视觉语言模型通过视角检测是否有意外发生,如果有的话如何能够纠正,如果看到这个箱子掉到地上,机器人能够想出一个方法最后把它捡起来,最后完成任务。
图片
除了上面谈到的以外,斯坦福大学的李飞飞团队,通过大语言模型有系统的去产生了一个代码来控制机器人,而清华大学交叉信息研究院的杨植麟团队也提出了CodeGeeX(多语言代码生成模型), 通过不同的大语言模型进行训练。
最后,我们谈谈目前还有很多挑战需要克服的方面。对于具身机器人,第一,我们能不能像大语言模型一样有一个具身的大模型,它能够直接的一步到位,能够控制最低层的效率。第二是关于计算能力的挑战,我们做一个比较,就像谷歌的Robotic Transformer做第一个到下沉的统一模型,目前只能达到三个赫兹的水平,和我们需要的500个赫兹差的很远,所以这里面还有很多的事情我们需要来克服困难。第三个挑战,怎么样把多模式的感官融合起来。第四个挑战,机器人要收集数据还需要很多的事情需要做,其中也面临很多安全隐私等方面的问题等。
(整理自姚期智于2023年9月20日在“2023世界机器人大会”上的发言,转载来源:清华大学人工智能国际治理研究院)
收起阅读 »

程序员工作建议

我正式踏入职场时间很短,对于工作有一些新的理解,主要是吸取的前辈建议和自己的教训,分享给大家。目标对象是以前的自己,审慎阅读。首先,工作就是用劳动成果换取劳动报酬的过程。这里一定要注意,是劳动成果而不是劳动。各位同学刚刚步入职场,可能会有种种抱怨、不满及对未来...
继续阅读 »

我正式踏入职场时间很短,对于工作有一些新的理解,主要是吸取的前辈建议和自己的教训,分享给大家。目标对象是以前的自己,审慎阅读。

首先,工作就是用劳动成果换取劳动报酬的过程。

这里一定要注意,是劳动成果而不是劳动。各位同学刚刚步入职场,可能会有种种抱怨、不满及对未来方向的迷茫。在职场上,甚至已经犯下一些错误而不自知。

在通往事业成功的路上,没有捷径。摆正心态,一步一个脚印,脚踏实地的努力工作,同时用心去体会个人成长的过程。借助工作,完成自己人生的阶段性目标


工作要务是摆正个人与公司的位置关系。一个稳定运行的公司,离开谁都可以正常运作,作为技术人员,更要有觉悟:是公司为自己能力的发挥提供了平台

我相信大多数同学入职时,公司的产品早已正常盈利。个人技术能力再强,也仅仅体现在个人的工作效率上;优秀的技术人员也就可以提升小团队的开发效率,而不只是自己。不要有怀才不遇的心态,这只能证明,个人的劳动成果不足以打动他人。


第二个想谈的问题是职业素质。工作的本质是交换,当一个任务下达时,就是一个新的契约签署过程,你有选择不接受任务并离开的权利;一旦选择接受,那只有一个选项:完成任务

编程与其他任何工作没有什么本质区别,最终产品的质量并不是靠各种流程来保障,而只取决与参与产品的所有技术人员的职业素质。

深入细节逐个把控消耗的精力随着人数会指数膨胀,要深刻认知到流程可以给予最低限度的保障,是在一个黑盒中添加探针,能保证在做事,没办法保证在做正确的事。项目容易成功,商业成功不好说。而恰恰,你这个职业是通过技术手段支持公司的商业目标

在这里,不想多说什么,只提出以下几个问题:

  • 你目前的工作是因为喜欢而选择吗?
  • 你对目前的薪资满意吗?
  • 你对目前参与的产品感兴趣吗?
  • 你对目前参与的产品有什么建设性的意见吗?
  • 你喜欢身边的同事吗?
  • 你敢于承担责任吗?

管理程序员非常简单:给他喜欢的项目,并让他决定一切。打造或者进入一个这样的团队是最好的,不然,也可以成为这样的程序员。

但是管理一个团队,一定会有不满足职业素质的程序员,出现的原因有很多,推论如何解决可以讨论更多。这里提到存在这个客观现象,是为了引以为戒。

希望同学们从不成为“摆烂”的程序员开始,不要向已经开始混的人学习,先为自己负责,不要忘记了自己的目标。


第三个问题是如何平衡工作与学习

工作以后,能看到大家有各种没时间学习的原因。我一直倡导思考问题先从客观上找原因,那么这些客观原因对谁来说都是一样的吗?大家的进步速度真的一样吗?

这里给大家一些建议,可以尝试一下:

  • 在经济允许的条件下,尽量住的离公司近一些,减少通勤时间
  • 尽量不在手机上学习
  • 设定短期目标
  • 多写博客记录学习、生活心得
  • 找伙伴一起探讨技术


第四个问题是随时保持紧迫感。年轻人需要敢干,不能因为年轻反而躺好放松。工作是为了在不太远的未来,让自己能够承担起该负的责任。不要等待,不要患得患失,随时做好准备,接受全新的挑战。

作者:杨鼎睿
来源:www.yuque.com/abser/talks/dtvbqfuh1efd4t87

收起阅读 »

类的布局——成员变量

日常开发中,我们定义的OC类,都会被编译成结构体类型:/// Represents an instance of a class.struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY...
继续阅读 »

日常开发中,我们定义的OC类,都会被编译成结构体类型:

/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

在类中定义的属性和成员变量,也会变成结构体的成员变量:

@interface ObjectA : NSObject
@property(nonatomic,assign)BOOL b;
@end

// 类ObjectA会转化为如下结构体
struct ObjectA_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 继承自NSObject
BOOL _b;
};

知道了类的真面目,可以在内存级别去做一些操作;例如:

@interface ObjectA : NSObject
@property(nonatomic,assign,readonly)BOOL b;
@end

...

- (void)viewDidLoad {
[super viewDidLoad];
ObjectA *aObj = [[ObjectA alloc] init];

NSLog(@"%@",aObj); // 在这一行打断点

// Do any additional setup after loading the view.
}


用代码实现如下:

- (void)viewDidLoad {
[super viewDidLoad];
ObjectA *aObj = [[ObjectA alloc] init];

void *aObj_ptr = (__bridge void *)aObj;
BOOL *_b_ptr = (BOOL *)((BytePtr)aObj_ptr + 0x8);
*_b_ptr = YES;

NSLog(@"%d",aObj.b); // 打印:1 证明已修改

// Do any additional setup after loading the view.
}


收起阅读 »

iOS面试题目——hook block(3)

// 题目:实现下面的函数,将任意参数 block 的实现修改成打印所有入参,并调用原始实现//// 比如// void(^block)(int a, NSString *b) = ^(int a, NSString *b){// NSLog(@"...
继续阅读 »
// 题目:实现下面的函数,将任意参数 block 的实现修改成打印所有入参,并调用原始实现
//
// 比如
// void(^block)(int a, NSString *b) = ^(int a, NSString *b){
// NSLog(@"block invoke");
// }
// HookBlockToPrintArguments(block);
// block(123,@"aaa");
// 这里输出 "123,aaa" 和 "block invoke"

// void(^block)(int a, double b) = ^(int a, double b){
// NSLog(@"block invoke");
// }
// HookBlockToPrintArguments(block);
// block(123,3.14);
// 这里输出 "123,3.14" 和 "block invoke"

分析题目:首先,题目的本意和上一个题目一样,就是hook block 的 invoke,然后将其所有的入参打印出来,再调用原实现。区别在于任意Block,这个任意block,就让我们无法对用来替换的函数有一个很合适的定义,因为我们定义的时候,根本就不知道即将hook的block有几个参数。

这个问题,可以用libffi来解决。

整个思路如下:

1、获取要hook的block的相关信息,例如返回值、参数列表。这些信息都存储在bkock的方法签名里。

2、通过上一步获取到的信息,利用libffi创建一个函数模板(ffi_prep_cif())。

3、创建动态调用函数,并替换block中的Invoke。

4、编写替换函数,并实现调用原函数。

代码实现:

  • 获取block的签名信息:

    struct Block_layout *layout = (__bridge struct Block_layout *)block;

if (! (layout->flags & BLOCK_HAS_SIGNATURE)){
NSLog(@"当前block没有签名");
return;
}

uint8_t *desc = (uint8_t *)layout->descriptor;

desc += sizeof(struct Block_descriptor_1);

if (layout->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
struct Block_descriptor_3 *desc_3 = (struct Block_descriptor_3 *)desc;

const char *signature = desc_3->signature;
NSMethodSignature *m_signature = [NSMethodSignature signatureWithObjCTypes:signature];
  • 创建函数模版:
    ffi_type **args = malloc(sizeof(ffi_type *)*[m_signature numberOfArguments]);

// 返回值类型
ffi_type *return_ffi;
const char *return_type = [m_signature methodReturnType];
if (*return_type == @encode(_Bool)[0]) {
return_ffi = &ffi_type_sint8;
}else if (*return_type == @encode(signed char)[0]){
return_ffi = &ffi_type_sint8;
}else if (*return_type == @encode(unsigned char)[0]){
return_ffi = &ffi_type_uint8;
}else if (*return_type == @encode(short)[0]){
return_ffi = &ffi_type_sint16;
}else if (*return_type == @encode(int)[0]){
return_ffi = &ffi_type_sint32;
}else if (*return_type == @encode(long)[0]){
return_ffi = &ffi_type_sint64;
}else if (*return_type == @encode(long long)[0]){
return_ffi = &ffi_type_sint64;
}else if (*return_type == @encode(id)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(Class)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(SEL)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(void *)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(char *)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(float)[0]){
return_ffi = &ffi_type_float;
}else if (*return_type == @encode(double)[0]){
return_ffi = &ffi_type_double;
}else if (*return_type == @encode(void)[0]){
return_ffi = &ffi_type_void;
}else{
NSLog(@"未找到合适的类型");
return;
}
// 初始化参数列表
for (int i=0; i<[m_signature numberOfArguments]; i++) {
const char *type = [m_signature getArgumentTypeAtIndex:i];
if (*type == @encode(_Bool)[0]) {
args[i] = &ffi_type_sint8;
}else if (*type == @encode(signed char)[0]){
args[i] = &ffi_type_sint8;
}else if (*type == @encode(unsigned char)[0]){
args[i] = &ffi_type_uint8;
}else if (*type == @encode(short)[0]){
args[i] = &ffi_type_sint16;
}else if (*type == @encode(int)[0]){
args[i] = &ffi_type_sint32;
}else if (*type == @encode(long)[0]){
args[i] = &ffi_type_sint64;
}else if (*type == @encode(long long)[0]){
args[i] = &ffi_type_sint64;
}else if (*type == @encode(id)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(Class)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(SEL)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(void *)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(char *)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(float)[0]){
args[i] = &ffi_type_float;
}else if (*type == @encode(double)[0]){
args[i] = &ffi_type_double;
}else{
NSLog(@"未知类型:注,结构体未处理");
return;
}
}

// _cif 定义的是全局变量 ffi_cif _cif;
ffi_status status = ffi_prep_cif(&_cif, FFI_DEFAULT_ABI, (int)[m_signature numberOfArguments], return_ffi, args);
if (status != FFI_OK) {
NSLog(@"初始化 cif 失败");
return;
}
  • 创建并绑定动态调用的函数:
    // 	_closure 定义的是全局变量		ffi_closure *_closure;
// _replacementInvoke 定义的是全局变量 void *_replacementInvoke;

_closure = ffi_closure_alloc(sizeof(ffi_closure), &_replacementInvoke);
if (!_closure) {
NSLog(@"hook 失败");
return;
}
ffi_status closure_loc_status = ffi_prep_closure_loc(_closure, &_cif, replace_bloke2_2, (__bridge void *)(NSObject.new), _replacementInvoke);
if (closure_loc_status != FFI_OK) {
NSLog(@"Hook failed! ffi_prep_closure returned %d", (int)status);
return;
}
  • 替换block中的invoke:
    //    修改内存属性
vm_address_t invoke_addr = (vm_address_t)&layout->invoke;
vm_size_t vmsize = 0;
mach_port_t object = 0;
vm_region_basic_info_data_64_t info;
mach_msg_type_number_t infoCnt = VM_REGION_BASIC_INFO_COUNT_64;
kern_return_t ret = vm_region_64(mach_task_self(), &invoke_addr, &vmsize, VM_REGION_BASIC_INFO, (vm_region_info_t)&info, &infoCnt, &object);
if (ret != KERN_SUCCESS) {
NSLog(@"获取失败");
return;
}
vm_prot_t protection = info.protection;
// 判断内存是否可写
if ((protection&VM_PROT_WRITE) == 0) {
// 修改内存属性 ===> 可写
ret = vm_protect(mach_task_self(), invoke_addr, sizeof(invoke_addr), false, protection|VM_PROT_WRITE);
if (ret != KERN_SUCCESS) {
NSLog(@"修改失败");
return;
}
}
// 保存原来的invoke
origin_blockInvoke2_2 = (void *)layout->invoke;
layout->invoke = (uintptr_t)_replacementInvoke;
  • 实现替换函数:
    void replace_bloke2_2(ffi_cif *cif, void *ret, void **args, void *userdata) {
struct Block_layout *layout = (struct Block_layout *)userdata;
uint8_t *desc = (uint8_t *)layout->descriptor;

desc += sizeof(struct Block_descriptor_1);

if (layout->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
struct Block_descriptor_3 *desc_3 = (struct Block_descriptor_3 *)desc;

const char *signature = desc_3->signature;
NSMethodSignature *m_signature = [NSMethodSignature signatureWithObjCTypes:signature];

NSLog(@"回调函数");
NSLog(@"%d",cif->nargs);
// 解析参数
for (int i=0; i<[m_signature numberOfArguments]; i++) {
ffi_type *arg = args[i];
const char *type = [m_signature getArgumentTypeAtIndex:i];
if (*type == @encode(_Bool)[0]) {
NSLog(@"%d",(bool)arg->size);
}else if (*type == @encode(signed char)[0]){
NSLog(@"%d",(char)arg->size);
}else if (*type == @encode(unsigned char)[0]){
NSLog(@"%d",(unsigned char)arg->size);
}else if (*type == @encode(short)[0]){
NSLog(@"%d",(short)arg->size);
}else if (*type == @encode(int)[0]){
NSLog(@"%d",(int)arg->size);
}else if (*type == @encode(long)[0]){
NSLog(@"%ld",(long)arg->size);
}else if (*type == @encode(long long)[0]){
NSLog(@"%lld",(long long)arg->size);
}else if (*type == @encode(id)[0]){
NSLog(@"%@",(__bridge id)((void *)arg->size));
}else if (*type == @encode(Class)[0]){
NSLog(@"%@",(__bridge Class)((void *)arg->size));
}else if (*type == @encode(SEL)[0]){
NSLog(@"%s",((char *)arg->size));
}else if (*type == @encode(void *)[0]){
NSLog(@"0x%llx",((long long)arg->size));
}else if (*type == @encode(char *)[0]){
NSLog(@"%s",((char *)arg->size));
}else if (*type == @encode(float)[0]){
NSLog(@"%f",((float)arg->size));
}else if (*type == @encode(double)[0]){
NSLog(@"%f",((double)arg->size));
}else{
NSLog(@"未知类型:注,结构体未处理");
}
}
// 调用原函数
ffi_call(&_cif,(void *)origin_blockInvoke2_2, ret, args);
}


收起阅读 »

适当给生活按下暂停键,出去放空一下自己

踏春,亦或是暂存的生日礼物,哈哈哈,管他呢,啥也不想,跟着老公就行了。 2023-4-7 去了趟苏州,从不明白老公为什么要选择这个城市旅游,到我舍不得离开。 风景很美,风景如画也不过如此吧。美的地方有很多,但是又美又有历史的地方,文化底蕴深厚,让人感觉到有韵味...
继续阅读 »

踏春,亦或是暂存的生日礼物,哈哈哈,管他呢,啥也不想,跟着老公就行了。


2023-4-7


去了趟苏州,从不明白老公为什么要选择这个城市旅游,到我舍不得离开。


风景很美,风景如画也不过如此吧。美的地方有很多,但是又美又有历史的地方,文化底蕴深厚,让人感觉到有韵味的美。


ff09df8fd73f9b055a458b7263730d7.jpg
早上走在平江路上,很安静,初次看到小桥流水人家,很江南,确实没见过,慢慢我开始斜坐在桥上,吹着小风,从眼前美景慢慢关闭眼睛,闻着湿润的空气,放空自己。风是柔柔的,空气没有腥味,周五的早上,很安静。脚下是一千多年的桥,仿佛再闭一会儿睁开,就会穿梭在明朝。


ab7bcab41003788dcaf4fa0ea80c0af.jpg


a67013c1b762837973625f91dd23abc.jpg


ab637dfdbaebbf3e5f5f2688e4fa704.jpg


421653b8c798168391ebb94e5e79fc2.jpg


走了一两公里,没觉得累,就是有点怕走到街道的尽头,就是喧哗刺眼的高楼。不用思考,就是跟着老公走,街道很干净,让人心情很好。


到了拙政园,好吧,今天真的是周五嘛。上次这么多人,还是去北京看漫展的时候,排了两条街。我们在门口租了一个讲解器,这个地方很适合自己带着耳机,听着讲解,看看古树,过着小桥,慢慢欣赏。跟着人群走了一圈,我们还了讲解器,慢慢的又走了一圈,回顾着这个石头的来源,这棵古树的年份,仔细的看了看最高的那几棵大树,园林的设计。这趟,很值。


bab1b7b7b04621890920c2615891da2.jpg
9f551fa4de1eb98fa52fcaf68bee7a3.jpg


eeaf2e7046a98e5ac7f3395c32954fe.jpg


9ec8e4f90d466e48936734893f25072.jpg


f707d4101abc97d854392444a3cccf4.jpg


90bf28243f871dda2092f4e1b523100.jpg


4cf82e3c871554c2f2b0789ede10a26.jpg
出去后我们找了一家饭店,吃了个饭,去了酒店。睡了两个小时,晚上被老公拉起来,说是有好玩的。马上就来了兴致,收拾收拾出了门。


酒店就在山塘街边上,走了一会就进入了人流中,早上的安静的街道,现在人山人海。老公拉着我穿过人群,左拐右拐,一会过桥一会下桥,穿过一条清吧街,来到了一个叫玉涵堂的园子。老板把我们领到中间的位置,有小圆桌,椅子,小长椅,对面是戏台,旁边有个小亭子,亭子旁边是棵开的正好的桃花树,亭子上空有个小星星。慢慢就是萧声,琵琶声,二胡声,昆曲,小调,歌声。西厢记,牡丹亭,游园惊梦,玉簪记等等一曲曲婉转悠扬,动人心弦,最后一首声声慢结束了。


015c5988018f84ff5f3e385de17c7a0.jpg


edb9486763cdfca1efd27d4827c1955.jpg


8e528e0f425f30544b4ef0cf210be18.jpg


886f8cd3b1dfcfe37219bb352e918fd.jpg


5779131c33602e31103dfd02865e072.jpg
出门就是热闹的山塘街,是有点热闹,走路都是人挤人的跟着走,说实话,有点害怕踩踏事件。但是上到桥上,又觉得挤点也值得。桥下是一条条慢慢划着的船,两边是挂着灯笼的小房子,两边的水边是灯笼和房屋的倒影,没想到这种画,现实中也可以看到。


03047e5588ffaaee5dcc3d2cc7c7403.jpg


e3c6ce5b41bb6b3e7bb1dc8ff193e1d.jpg


8226270efbe7d87290e8186cd3fa22b.jpg


2023-4-8


第二天,有点起不来,但是一想到古镇,就有了动力。老公定的大巴车,车上看见了一些老年人,是的,他们来旅游,现在的他们有时间,大把的时间,有点羡慕。司机师傅拿着二维码,慢慢教他们怎么买回来的票,说是可以直接8折买古镇的票,一切就是告知,没有极力推销,让人比较舒服。再加上昨天走了一天的街道,街道不管人多少,干干净净,对这里的好感又上升了一些。


古镇的人。。。有点多。一波一波的旅游团。到古镇门口这一路,我眼里只有三个字——“万三蹄”。哈哈哈,没办法,一条街都是卖这个的,颜色很诱人。等会出来我要尝尝,再带点给家人尝尝。等到了五年没见的友人,朋友就是不用太联系,见到的时候一切依旧。这是老公的初中同学,从昆山打车来见我们的,五年没见了,一点不生疏。进去逛了一会,我们去坐了船。大哥很会聊,跟我们唱了几首歌谣,介绍了两边的树,道边停着的结婚用的花桥。听着小调,看着两边浮动的柳树,拍照的人,也是一番惬意。古镇有很多宅子,为首的当然是沈府,沈万三的家,在饭厅的“八大碗‘得知了,”万三蹄“的来历,朱元璋在沈万三家吃饭的时候,朱元璋问沈万三,这是什么菜,沈万三老婆说”猪蹄啊“。古代老百姓都要避讳皇帝的姓氏,沈万三赶紧说,这是”万三蹄“,后来的人就把这道菜叫做”万三蹄“。据说这道菜的制作,是先泡水,然后煮2分熟,蒸两个小时,再用冰糖炖,所以这个是偏甜口的。出去后,我们找了一家店,尝了下,挺鲜的,不腻,因为不喜甜口菜,所以没有多吃,但是是好吃的,还有不起眼的外婆菜,酸甜味的,下粥应该是不错的。


5ac7ab1702d660bf80583aa7d2a0faf.jpg


dde295ef86295da9f040846f66e4023.jpg


f5fe5e4498d865e44d4ab0fdf68ffb4.jpg


deb015fee9b3cfa8154f2c93de66b71.jpg


da9a31925dbcaf2ae755b6352fa7955.jpg


2023-4-9


第三天,每天2万多步的脚程,实在是有点歇不过来了,我们决定今天摆烂游了。慢悠悠退了房打车去了寒山寺。据导游说,寒山寺三个字不是一个人写的,四大才子之一,祝枝山写了前两个字。因为方丈看中了祝枝山的字,想让他提寺名,但是祝枝山是个财迷,要3000两,方丈凑了2200两,祝枝山写了前两个字,然后退回去了200两。后来方丈去世了,不了了之。直到陶濬宣写了第三个字,但是他有个要求,要在后面落款,所以现在的寒山寺外面的寺字是有陶濬宣的名字的。他就是写“光绪通宝”的人。寺庙外面都有一堵墙,写着寺名,据说是不建议拍照的,此乃萧墙,寺庙的萧墙是挡污秽东西的。休息之余,去大运河看货船,没装货的船,显得高大,空旷,装完货后,吃水很深,船旁边挂了一些轮胎,不知道是不是拿来当游泳圈的,哈哈哈,反正两个男生倒是很感兴趣,还跑过去近距离看了一会儿~


07a48040e37bd9a8d5c46139f676971.jpg


558cfdf22b1ab24fbe5a6033a377cab.jpg


78c9578f1a7685d837b0adf58682c80.jpg


朋友找了一家店,松鼠桂鱼好看,也好吃。吃完饭就是别离。无论是别离友人,还是别离这风景如画的城市,别离无人打扰的短暂时光,都是那么的不舍。但是别离,也是为了下一次的相聚。这一路很美好。


647608be5b11f18ff057fea3075f95d.jpg


94901f4a9986ea358818a886f5d8090.jpg


作者:没错就是我哎呀
来源:juejin.cn/post/7220236377937887269
收起阅读 »

10分钟3个步骤集成使用SkyWalking

随着业务发展壮大,微服务越来越多,调用链路越来越复杂,需要快速建立链路跟踪系统,以及建立系统的可观测性,以便快速了解系统的整体运行情况。此时就非常推荐SkyWalking了,SkyWalking不仅仅是一款链路跟踪工具,还可以作为一个系统监控工具,还具有告警功...
继续阅读 »

随着业务发展壮大,微服务越来越多,调用链路越来越复杂,需要快速建立链路跟踪系统,以及建立系统的可观测性,以便快速了解系统的整体运行情况。此时就非常推荐SkyWalking了,SkyWalking不仅仅是一款链路跟踪工具,还可以作为一个系统监控工具,还具有告警功能。使用简便、上手又快。真可谓快、准、狠。


本文主要介绍如何快速集成使用SkyWalking,从3个方面入手:原理、搭建、使用。


1、原理


1.1、概括


SkyWalking整体分为4个部分:探针采集层、数据传输和逻辑处理层、数据存储层、数据展示层。



1.2、探针采集层


所谓探针,实际上是一种动态代理技术,只不过不是我们常用的Java代理类,而是在类加载时,就生成了增强过的代理类的字节码,增强了数据拦截采集上报的功能。


探针技术是在项目启动时通过字节码技术(比如JavaAgent、ByteBuddy)进行类加载和替换,生成新的增强过的Class文件,对性能的影响是一次性的。


探针技术,因为在类加载时进行转换,增强了部分功能,所以会增加项目启动时间,同时也会增加内存占用量和线程数量。但是对性能影响不大,官方介绍在5% ~ 10%之间。



探针层在类转换时,通过各种插件对原有的类进行增强,之后在运行时拦截请求,然后将拦截的数据上报给Skywalking服务端。同时再加上一些定时任务,去采集应用服务器的基础数据,比如JVM信息等。


1.3、数据传输和逻辑处理层


SkyWalking探针层使用了GRPC作为数据传输框架,将采集的数据上报到SkyWalking服务端。


SkyWalking服务端接收数据后,利用各种插件来进行数据的分析和逻辑处理。比如:JVM相关插件,主要用于处理上报上来的JVM信息,数据库插件用来分析访问数据库的信息。然后在将数据存入到数据存储层。


1.4、数据存储层


SkyWalking的数据存储层支持多种主流数据库,可以自行到配置文件里查阅。我推荐使用ElasticSearch,存储量大,搜索性能又好。


1.5、数据展示层


SkyWalking 通过 Rocketbot 进行页面UI展示。可以在页面的左上角看到这个可爱的Rocketbot



2、搭建


知道了原理,搭建就很轻松了,使用SkyWalking其实就3个步骤:



  1. 搭建数据存储部件。

  2. 搭建SkyWalking服务端。

  3. 应用通过agent探针技术将数据采集上报给SkyWalking服务端。


2.1、搭建数据存储部件


SkyWalking支持多种存储方式,此处推荐采用Elasticsearch作为存储组件,存储的数据量较大,搜索响应快。


快速搭建Elasticsearch:



  1. 安装java:yum install java-1.8.0-openjdk-devel.x86_64

  2. 下载Elasticsearch安装包:http://www.elastic.co/cn/download…

  3. 修改elasticsearch.yml文件的部分字段:cluster.namenode.namepath.datapath.logsnetwork.hosthttp.portdiscovery.seed_hostscluster.initial_master_nodes。将字段的值改成对应的值。

  4. 在Elasticsearch的bin目录下执行./elasticsearch启动服务。

  5. 访问http://es-ip:9200,看到如下界面就代表安装成功。


{
"name": "node-1",
"cluster_name": "my-application",
"cluster_uuid": "GvK7v9HhS4qgCvfvU6lYCQ",
"version": {
"number": "7.17.1",
"build_flavor": "default",
"build_type": "rpm",
"build_hash": "e5acb99f822233d6ad4sdf44ce45a454xxxaasdfas323ab",
"build_date": "2023-02-23T22:20:54.153567231Z",
"build_snapshot": false,
"lucene_version": "8.11.1",
"minimum_wire_compatibility_version": "6.8.0",
"minimum_index_compatibility_version": "6.0.0-beta1"
},
"tagline": "You Know, for Search"
}

2.2、搭建SkyWalking服务端


搭建SkyWalking服务端只需要4步:


1、下载并解压skywalking:archive.apache.org/dist/skywal…



2、进入到安装目录下的修改配置文件:config/apllication.yaml。将存储修改为elasticsearch。



3、进入到安装目录下的bin目录,执行./startup.sh启动SkyWalking服务端。


4、此时使用jps命令,应该可以看到如下2个进程。一个是web页面进程,一个是接受和处理上报数据的进程。如果没有jps命令,那自行查看下是否配置了Java环境变量。 同时访问http://ip:8080应该可以看到如下界面。




2.3、应用采集上报数据


应用采集并且上报数据,直接使用agent探针方式。分为以下3步:


1、下载解压agentarchive.apache.org/dist/skywal…,找到skywalking-agent.jar



2、添加启动参数



  • 应用如果是jar命令启动,则直接添加启动参数即可:


java -javaagent:/自定义path/skywalking-agent.jar -Dskywalking.collector.backend_service={{agentUrl}} -jar xxxxxx.jar 

此处的{{agentUrl}}是SkyWalking服务端安装的地址,再加上11800端口。比如:10.20.0.55:11800




  • 应用如果是Docker镜像的部署方式,则需要将skywalking-agent.jar打到镜像里,类似下图:



3、启动项目后,即可看到监控数据,如下图:



3、UI页面使用


原理和搭建已经介绍完毕,接下来快速介绍UI页面的功能。下图标红的部分是重点关注区域:


3.1、仪表盘



  • APM:以全局(Global)、服务(Service)、服务实例(Instance)、端点(Endpoint)的维度展示各项指标。

  • Database:展示数据库的各项指标。




  • 服务(Service):某个微服务,或者某个应用。

  • 服务实例(Instance):某个微服务或者某个应用集群的一台实例或者一台负载。

  • 端点(Endpoint):某个Http请求的接口,或者 某个接口名+方法名。




3.2、拓扑图



3.3、追踪



关于UI界面的使用,还可以参考这个链接:juejin.cn/post/710630…,这里写的比较详细。


总结


本文主要从3个方面入手:原理、搭建、使用,介绍如何快速集成使用SkyWalking。核心重点:



  • SkyWalking其实就4部分组成:探针采集上报数据分析和逻辑处理、数据存储数据展示。安装使用简单、易上手。

  • 探针技术是SkyWalking的基石,说白了就是:在类加载时进行字节码转换增强,然后去拦截请求,采集上报数据。

  • UI页面的使用,多用用就熟悉了。


本篇完结!感谢你的阅读,欢迎点赞 关注 收藏 私信!!!


原文链接: http://www.mangod.top/articles/20…mp.weixin.qq.com/s/5P6vYSOCy…


作者:不焦躁的程序员
来源:juejin.cn/post/7288604780382879796
收起阅读 »

你敢信?比 setTimeout 还快 80 倍的定时器

web
起因 很多人都知道,setTimeout是有最小延迟时间的,根据MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间中所说: 在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这...
继续阅读 »

起因


很多人都知道,setTimeout是有最小延迟时间的,根据MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间中所说:



在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度)。



HTML Standard规范中也有提到更具体的:



Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.



简单来说,5 层以上的定时器嵌套会导致至少 4ms 的延迟。


用如下代码做个测试:


let a = performance.now();
setTimeout(() => {
let b = performance.now();
console.log(b - a);
setTimeout(() => {
let c = performance.now();
console.log(c - b);
setTimeout(() => {
let d = performance.now();
console.log(d - c);
setTimeout(() => {
let e = performance.now();
console.log(e - d);
setTimeout(() => {
let f = performance.now();
console.log(f - e);
setTimeout(() => {
let g = performance.now();
console.log(g - f);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);

在浏览器中的打印结果大概是这样的,和规范一致,第五次执行的时候延迟来到了 4ms 以上。


2021-05-13-21-04-16-067254.png


探索


假设就需要一个「立刻执行」的定时器呢?有什么办法绕过这个 4ms 的延迟吗,在 MDN 文档的角落里有一些线索:



如果想在浏览器中实现 0ms 延时的定时器,可以参考这里所说的window.postMessage()



这篇文章里的作者给出了这样一段代码,用postMessage来实现真正 0 延迟的定时器:


(function () {
var timeouts = [];
var messageName = 'zero-timeout-message';

// 保持 setTimeout 的形态,只接受单个函数的参数,延迟始终为 0。
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, '*');
}

function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}

window.addEventListener('message', handleMessage, true);

// 把 API 添加到 window 对象上
window.setZeroTimeout = setZeroTimeout;
})();

由于postMessage的回调函数的执行时机和setTimeout类似,都属于宏任务,所以可以简单利用postMessageaddEventListener('message')的消息通知组合,来实现模拟定时器的功能。


这样,执行时机类似,但是延迟更小的定时器就完成了。


再利用上面的嵌套定时器的例子来跑一下测试:


2021-05-13-21-04-16-210864.png


全部在 0.1 ~ 0.3 毫秒级别,而且不会随着嵌套层数的增多而增加延迟。


测试


从理论上来说,由于postMessage的实现没有被浏览器引擎限制速度,一定是比 setTimeout 要快的。


设计一个实验方法,就是分别用postMessage版定时器和传统定时器做一个递归执行计数函数的操作,看看同样计数到 100 分别需要花多少时间。


实验代码:


function runtest() {
var output = document.getElementById('output');
var outputText = document.createTextNode('');
output.appendChild(outputText);
function printOutput(line) {
outputText.data += line + '\n';
}

var i = 0;
var startTime = Date.now();
// 通过递归 setZeroTimeout 达到 100 计数
// 达到 100 后切换成 setTimeout 来实验
function test1() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setZeroTimeout took ' +
(endTime - startTime) +
' milliseconds.'
);
i = 0;
startTime = Date.now();
setTimeout(test2, 0);
} else {
setZeroTimeout(test1);
}
}

setZeroTimeout(test1);

// 通过递归 setTimeout 达到 100 计数
function test2() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setTimeout(0) took ' +
(endTime - startTime) +
' milliseconds.'
);
} else {
setTimeout(test2, 0);
}
}
}

实验代码很简单,先通过setZeroTimeout也就是postMessage版本来递归计数到 100,然后切换成 setTimeout计数到 100。


直接放结论,这个差距不固定,在 mac 上用无痕模式排除插件等因素的干扰后,以计数到 100 为例,大概有 80 ~ 100 倍的时间差距。在硬件更好的台式机上,甚至能到 200 倍以上。


2021-05-13-21-04-16-326555.png


Performance 面板


只是看冷冰冰的数字还不够过瘾,打开 Performance 面板,看看更直观的可视化界面中,postMessage版的定时器和setTimeout版的定时器是如何分布的。


2021-05-13-21-04-16-602815.png


这张分布图非常直观的体现出了上面所说的所有现象,左边的postMessage版本的定时器分布非常密集,大概在 5ms 以内就执行完了所有的计数任务。


而右边的setTimeout版本相比较下分布的就很稀疏了,而且通过上方的时间轴可以看出,前四次的执行间隔大概在 1ms 左右,到了第五次就拉开到 4ms 以上。


作用


也许有同学会问,有什么场景需要无延迟的定时器?其实在 React 的源码中,做时间切片的部分就用到了。


const channel = new MessageChannel();
const port = channel.port2;

// 每次 port.postMessage() 调用就会添加一个宏任务
// 该宏任务为调用 scheduler.scheduleTask 方法
channel.port1.onmessage = scheduler.scheduleTask;

const scheduler = {
scheduleTask() {
// 挑选一个任务并执行
const task = pickTask();
const continuousTask = task();

// 如果当前任务未完成,则在下个宏任务继续执行
if (continuousTask) {
port.postMessage(null);
}
},
};

React 把任务切分成很多片段,这样就可以通过把任务交给postMessage的回调函数,来让浏览器主线程拿回控制权,进行一些更优先的渲染任务(比如用户输入)。


为什么不用执行时机更靠前的微任务呢?关键的原因在于微任务会在渲染之前执行,这样就算浏览器有紧急的渲染任务,也得等微任务执行完才能渲染。


总结


可以了解如下几个知识点:



  1. setTimeout的 4ms 延迟历史原因,具体表现。

  2. 如何通过postMessage实现一个真正 0 延迟的定时器。

  3. postMessage定时器在 React 时间切片中的运用。

  4. 为什么时间切片需要用宏任务,而不是微任务。


作者:睡醒想钱钱
来源:juejin.cn/post/7229520942668824633
收起阅读 »

游戏开发中不同性格特点的程序员,你属于哪一种?

点击上方亿元程序员+关注和★星标 引言 大家好,我是亿元程序员,一位有着8年游戏行业经验的主程。 在游戏开发领域,每个程序员都有自己独特的方式来编写代码,这反映了他们的个性和思维方式。虽然代码风格和程序员的性格之间存在差异,但这些差异却构成了一个多彩的编程社...
继续阅读 »

点击上方亿元程序员+关注和★星标



引言


大家好,我是亿元程序员,一位有着8年游戏行业经验的主程。


在游戏开发领域,每个程序员都有自己独特的方式来编写代码,这反映了他们的个性和思维方式。虽然代码风格和程序员的性格之间存在差异,但这些差异却构成了一个多彩的编程社区。本文将探讨一些常见的代码风格,并探讨它们背后可能对应的程序员性格特点。


你属于哪一种?


注重细节的程序员


注重细节


有些程序员对代码的细节极为敏感,他们喜欢确保每个括号都放在正确的位置,每个变量都有清晰的命名规范。这种注重细节的程序员通常具备以下性格特点:



  • 谨慎与耐心:他们在编写代码时会花更多的时间来确保一切都无懈可击。

  • 善于研究:他们喜欢深入研究文档和规范,以确保他们的代码符合最佳实践。

  • 注重文档和注释:他们会编写详细的注释和文档,以帮助其他人理解他们的代码。

  • 喜欢代码审查:他们乐于接受同事的审查,以确保代码质量达到最高标准。


创造性的程序员


创造性


创造性的程序员常常寻求新颖的解决方案,他们擅长思考问题的不同角度。这种类型的程序员通常表现出以下性格特点:



  • 创新思维:他们喜欢提出独特的解决方案,寻求创新的方法来解决问题。

  • 乐于尝试新技术:他们喜欢接触新技术和工具,以探索新的可能性。

  • 问题解决能力强:他们具备出色的问题解决能力,能够应对复杂的挑战。

  • 勇于失败:他们不怕尝试新方法,即使失败也视之为学习的机会。


团队合作的程序员


团队合作


团队合作是许多项目成功的关键,一些程序员特别擅长与他人协作。这种类型的程序员通常表现出以下性格特点:



  • 良好的沟通技巧:他们善于与团队成员沟通和合作,分享知识和经验。

  • 乐于分享:他们愿意分享自己的知识,帮助其他人成长。

  • 接受反馈:他们乐于接受他人的反馈和建议,以改进工作。

  • 协同工作:他们喜欢与其他人一起解决问题,借助集体智慧来实现共同目标。


独立的程序员


独立


独立的程序员通常喜欢独自工作,他们具备自我驱动力。这种类型的程序员通常表现出以下性格特点:



  • 自主性:他们有强烈的自主性,能够自我激励,独立完成任务。

  • 自学能力:他们喜欢自学新技术和概念,寻找解决方案。

  • 自信:他们相信自己的能力,对独立工作充满信心。

  • 目标导向:他们能够明确目标并专注于实现它们。


快速迭代的程序员


快速迭代


一些程序员喜欢快速开发和迭代,他们对持续改进有着强烈的渴望。这种类型的程序员通常表现出以下性格特点:



  • 快速反馈:他们喜欢快速获取反馈,并根据反馈进行改进。

  • 不怕失败:他们将失败视为学习的机会,勇敢尝试新方法。

  • 敏捷开发:他们倾向于采用敏捷开发方法,将项目分解为小块,以便更容易管理和优化。


安全意识的程序员


安全意识


在网络时代,安全性成为至关重要的问题,一些程序员专注于保障代码的安全性。这种类型的程序员通常表现出以下性格特点:



  • 关注安全:他们注重代码和系统的安全性,努力避免潜在的风险。

  • 安全测试:他们喜欢进行安全漏洞扫描和测试,以发现和修复问题。

  • 遵循安全实践:他们遵循最佳的安全实践,确保数据和隐私的保护。

  • 学习网络安全知识:他们不断学习有关网络安全的知识,以保持警惕。


坚持主义的程序员


坚持主义


坚持主义的程序员通常坚守自己的编码标准和实践,他们追求代码的一致性和可维护性。这种类型的程序员通常表现出以下性格特点:



  • 坚守标准:他们喜欢坚守自己的编码标准,不轻易妥协代码质量。

  • 辩论与辩护:他们乐于进行辩论,辩护自己的决策和实践。

  • 维护一致性:他们追求代码的一致性,以提高可读性和可维护性。

  • 关注质量:他们希望保持高质量的代码,以减少错误和问题。


结语


看完之后,有没有符合以上一种或者多种特点的小伙伴? 没有也没有关系,和我一起学习游戏开发中的设计模式,让糟糕的代码在潜移默化中升华。


我是"亿元程序员",一位有着8年游戏行业经验的主程。在游戏开发中,希望能给到您帮助, 也希望通过您能帮助到大家。


AD:笔者线上的小游戏《贪吃蛇掌机经典》《填色之旅》《重力迷宫球》大家可以自行点击搜索体验。


实不相瞒,想要个在看!请把该文章分享给你觉得有需要的其他小伙伴。谢谢!


作者:亿元程序员
来源:juejin.cn/post/7288228582693044235
收起阅读 »