注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

Android 侧滑布局逻辑解析

一、前言 测滑布局应用非常广泛,HorizontalScrollView 本身实现的滑动效果让实现变得很简单,实际上有很多种方式实现,有很多现有的方法可以直接调用。 二、逻辑实现 在Android 中,滑动分为2类,一类以ScrollView为代表布局,通过...
继续阅读 »

一、前言


测滑布局应用非常广泛,HorizontalScrollView 本身实现的滑动效果让实现变得很简单,实际上有很多种方式实现,有很多现有的方法可以直接调用。



二、逻辑实现


在Android 中,滑动分为2类,一类以ScrollView为代表布局,通过子View实现布局超出视区(ViewPort)之后,进行Scroll操作的,另一类事以修改Offset为代表的Recycler类,前者实时保持最大高度。形像的理解为前者是“齿轮传动派”,后者是“滑板派”,两派都有过出分头的时候,即便是个派弟子如NestedScrollView和RecyclerView争的你死我活,不过总体上齿轮传动派占在弱势地位。不过android的改版,让他们做了很多和平相处的事情,不如NestedScrolling机制的支持,让他们想传动就传动,想滑翔就滑翔。


齿轮传动派看家本领



  • scrollX,ScrollY,scrollTo等方法

  • 一个长得很长的独生子


滑板派的看家本领



  • offsetXXX方法

  • 被魔改的ScrollXXX

  • 一群会滑板的孩子

  • layout 方法也是他们的榜首


前者为了实现的简单的滑动,后者空间可以无限大,期间还可自由换孩子。


三、代码实现


有很多现成的example都是基于齿轮传动派的,但是如果使用,你得记住,齿轮传动派会的滑板派一定会,反过来就不一样了。


这里我们使用layout方法实现,核心代码


        View leftView = mWrapperView.getChildAt(0);
leftView.layout(l,t,l+leftView.getWidth(),t+leftView.getHeight());
maskAlpha = leftView.getLeft()*1.0f/leftView.getWidth();

之所以使用layout的原因是很多人都不记得ListView可以使用该方法实现吸顶效果,而RecyclerView因为为了兼容更多类型,导致他使用这个很难实现吸顶,但是没关系,child.layout和child.measure方法可以在类的任何地方调用,这个是必须要掌握的。


3.1 布局初始化


 @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if(isFirstLayout && getRealChildCount()==2){
View leftView = mWrapperView.getChildAt(0);
scrollTo(leftView.getWidth(),0); //初始化状态让右侧View展示处理
}
isFirstLayout = true;
}

3.2 相对运动


滑动时让左侧View保持同样的滑动距离和方向


   View leftView = mWrapperView.getChildAt(0);
leftView.layout(l,t,l+leftView.getWidth(),t+leftView.getHeight());
maskAlpha = leftView.getLeft()*1.0f/leftView.getWidth();

3.3 全部代码


public class SlidingFoldLayout extends HorizontalScrollView {


private TextPaint mPaint = null;
private LinearLayout mWrapperView = null;
private boolean isFirstLayout = true;
private float maskAlpha = 1.0f;

public SlidingFoldLayout(Context context) {
this(context, null);
}

public SlidingFoldLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

public SlidingFoldLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LinearLayout linearLayout = getWrapperLayout(context);
setOverScrollMode(View.OVER_SCROLL_NEVER);
setWillNotDraw(false);
mPaint = createPaint();
addViewInLayout(linearLayout, 0, linearLayout.getLayoutParams(), true);
mWrapperView = linearLayout;
}


public LinearLayout getWrapperLayout(Context context) {
LinearLayout linearLayout = new LinearLayout(context);
HorizontalScrollView.LayoutParams lp = generateDefaultLayoutParams();
lp.width = LayoutParams.WRAP_CONTENT;
linearLayout.setLayoutParams(lp);
linearLayout.setOrientation(LinearLayout.HORIZONTAL);
linearLayout.setPadding(0, 0, 0, 0);
return linearLayout;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int childCount = mWrapperView.getChildCount();
if (childCount == 0) {
return;
}
int leftMenuWidth = mWrapperView.getChildAt(0).getMeasuredWidth();
ViewGr0up.LayoutParams lp = (ViewGr0up.LayoutParams) getLayoutParams();
int width = getMeasuredWidth() - getPaddingRight() - getPaddingLeft();
if (lp instanceof ViewGr0up.MarginLayoutParams) {
width = width - ((MarginLayoutParams) lp).leftMargin - ((MarginLayoutParams) lp).rightMargin;
}
if (width <= leftMenuWidth) {
mWrapperView.getChildAt(0).getLayoutParams().width = (int) (width - dp2px(50));
measureChild(mWrapperView, widthMeasureSpec, heightMeasureSpec);
}
if (childCount != 2) {
return;
}
View rightView = mWrapperView.getChildAt(1);
int rightMenuWidth = rightView.getMeasuredWidth();
if (width != rightMenuWidth) {
rightView.getLayoutParams().width = width;
measureChild(mWrapperView, widthMeasureSpec, heightMeasureSpec);
rightView.bringToFront();
}
}

private float dp2px(int dp) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}

@Override
public void addView(View child) {

int childCount = mWrapperView.getChildCount();
if (childCount > 2) {
throw new IllegalStateException("SlidingFoldLayout should host only two child");
}
ViewGr0up.LayoutParams lp = child.getLayoutParams();
if (lp != null && lp instanceof LinearLayout.LayoutParams) {
lp = new LinearLayout.LayoutParams(lp);
child.setLayoutParams(lp);
}

mWrapperView.addView(child);

}



public int getRealChildCount() {
if (mWrapperView == null) {
return 0;
}
return mWrapperView.getChildCount();
}



@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
if(isFirstLayout && getRealChildCount()==2){
View leftView = mWrapperView.getChildAt(0);
scrollTo(leftView.getWidth(),0);
}
isFirstLayout = true;
}

@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
int realCount = getRealChildCount();
if(realCount!=2) return;
View leftView = mWrapperView.getChildAt(0);
leftView.layout(l,t,l+leftView.getWidth(),t+leftView.getHeight());
maskAlpha = leftView.getLeft()*1.0f/leftView.getWidth();
}


@Override
public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();

switch (action){
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_OUTSIDE:
super.onTouchEvent(ev);
scrollToTraget();
break;
}

return super.onTouchEvent(ev);
}

private void scrollToTraget() {

int count = getRealChildCount();
if(count!=2) return;
int with = getWidth();
if(with==0) return;

View leftView = mWrapperView.getChildAt(0);

float x = leftView.getLeft()*1.0f/leftView.getWidth();
if(x > 0.5f){
smoothScrollTo(leftView.getWidth(),0);
}else{
smoothScrollTo(0,0);
}

}

@Override
public void addView(View child, int index) {

int childCount = mWrapperView.getChildCount();
if (childCount > 2) {
throw new IllegalStateException("SlidingFoldLayout should host only two child");
}
ViewGr0up.LayoutParams lp = child.getLayoutParams();
if (lp != null && lp instanceof LinearLayout.LayoutParams) {
lp = new LinearLayout.LayoutParams(lp);
child.setLayoutParams(lp);
}

mWrapperView.addView(child, index);
}

@Override
public void addView(View child, ViewGr0up.LayoutParams params) {
int childCount = mWrapperView.getChildCount();
if (childCount > 2) {
throw new IllegalStateException("SlidingFoldLayout should host only two child");
}
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(params);
child.setLayoutParams(lp);
mWrapperView.addView(child, lp);

}

@Override
public void addView(View child, int index, ViewGr0up.LayoutParams params) {
int childCount = mWrapperView.getChildCount();
if (childCount > 2) {
throw new IllegalStateException("SlidingFoldLayout should host only two child");
}

LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(params);
child.setLayoutParams(lp);
mWrapperView.addView(child, index);
}

private TextPaint createPaint() {
// 实例化画笔并打开抗锯齿
TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
paint.setAntiAlias(true);
return paint;
}
RectF rectF = new RectF();
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);

int realCount = getRealChildCount();
if(realCount!=2) return;
View leftView = mWrapperView.getChildAt(0);
View rightView = mWrapperView.getChildAt(1);


rectF.top = leftView.getTop();
rectF.bottom = leftView.getBottom();
rectF.left = leftView.getLeft();
rectF.right = rightView.getLeft();
int alpha = (int) (153*maskAlpha);
mPaint.setColor(argb(alpha,0x00,0x00,0x00));
int saveId = canvas.save();
canvas.drawRect(rectF,mPaint);
canvas.restoreToCount(saveId);
}

public static int argb(
int alpha,
int red,
int green,
int blue) {
return (alpha << 24) | (red << 16) | (green << 8) | blue;
}

}

三、使用方式


使用方式简单清晰,没有看到ScrollView的独生子,原因是我们把他写到了类里面


  <com.cn.scrolllayout.view.SlidingFoldLayout
android:layout_width="match_parent"
android:layout_height="match_parent">

<LinearLayout
android:layout_width="300dp"
android:layout_height="match_parent"
android:gravity="center"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@mipmap/img_sample_text"
/>
</LinearLayout>
<LinearLayout
android:layout_width="500dp"
android:layout_height="match_parent"
android:background="@color/colorAccent"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="fitCenter"
android:src="@mipmap/img_sample_panda"
/>
</LinearLayout>
</com.cn.scrolllayout.view.SlidingFoldLayout>

四、总结


掌握ScrollX和OffsetX两种的滑动很重要,但是不能忘记layout的作用,本质上他属于一种OffsetX上层的封装。


作者:时光少年
来源:juejin.cn/post/7307989656288034851
收起阅读 »

个人独立开发者能否踏上敏捷之路?

很多软件开发团队都在使用Scrum、极限编程(XP)、看板等敏捷方法管理项目流程,持续迭代并更快、更高效地为客户持续交付可用的产品。除了团队,国内外很多个人独立开发者也在尝试将敏捷应用到自己的开发工作流程中,但大多数的结果都是收效甚微,个人践行敏捷是否可行? ...
继续阅读 »

很多软件开发团队都在使用Scrum、极限编程(XP)、看板等敏捷方法管理项目流程,持续迭代并更快、更高效地为客户持续交付可用的产品。除了团队,国内外很多个人独立开发者也在尝试将敏捷应用到自己的开发工作流程中,但大多数的结果都是收效甚微,个人践行敏捷是否可行? 


敏捷开发需要坚实的团队基础,以及团队文化的保障,方可有效地落地执行。  


什么是敏捷


敏捷是一种以用户需求为核心、采用不断迭代的方式进行的软件开发模式。它依靠自组织的跨职能小团队,在短周期内做出部分成果,通过快速、频繁的迭代,迅速地获取反馈,进而不断地完善产品,给用户带来更大的价值。践行敏捷的方式有很多,主要包括Scrum、XP、Kanban、精益生产、规模化敏捷等方法论。


敏捷的工作方式是将整个团队聚集在一起,理想情况下,敏捷团队的成员不超过10人。通过践行一系列简单的实践和足够的反馈,使团队能够感知目前的状态,并根据实际情况对实践进行调整。 


团队是敏捷的核心 


敏捷是一种团队驱动方法,团队可以简单的地定义为“为实现某一特定目标,包括两个或两个以上的人的相互协作的群体”。敏捷的核心是构建一个自组织的团队,团队的能力在于协作,即两个人或更多人相互交流与合作,以共同地产生一个结果。例如当两个程序员在结对编程时,他们在协作;每人持续集成当日的工作时,他们在协作;当团队开计划、站立、评审、回顾等会议时,他们在协作。协作的结果可以是有形的可交付物、决策或信息共享。 


而对于个人独立开发者,协作、互动、沟通都是无从谈起的: 


自己无法实践结对编程;


自己开站立会议是否很孤单;


自己玩估算扑克牌会不会很无聊;


评审演示没有观众,自然也就没有反馈;


…… 


这里有一个常见误区:独立开发者通常有一定的跨职能工作能力,于是想一人“饰演”多个不同角色,从需求计划整理到任务分解估时,从迭代开发到测试,再到发布、回顾总结。这是不是也在践行敏捷开发呢? 


当然不是。敏捷开发流程中任一环节都强调团队集体参与,并非由某个人独裁发号施令。例如项目计划制定、任务认领、工时评估,这些都不是某一个人的职责,而是需要团队成员来共同参与完成。然而,单个人的开发流程,很容易按部就班地走上了瀑布式开发模式(需求->设计->开发->测试->发布)。


001.png


[多重人格综合症,并确保精神上的新人是一个专业的“团队成员”]


敏捷团队中并不会要求每个人都成为全栈通才,在如今技术快速更新迭代的大环境下,期望一个人精通团队的所有技能是不现实的。取而代之的是重视具备跨职能的团队成员,这有助于管理各个工作岗位的平衡。例如,有时团队需要更多的测试人员,如果有一两个团队成员能转做测试工作,就能极大地提供帮助。 


敏捷是关于人,以及他们之间的协作交互,让每个人的能力得以充分的发挥并提升,从而创造优秀的产品。创造优秀产品的是人,而不是流程。所以,独立开发者即便一个人能跨职能走完整个开发流程,这跟敏捷强调的自组织团队中,成员之间高效地协作、交互以达到目标,完全不是一回事哦~


文化是敏捷的保障


很多个人独立开发者尝试引入敏捷的普遍思路,是从各种敏捷方法论中挑选一些个人能用,且有帮助的实践方法来用。这样确实能从中受益,但这真的是在践行敏捷么?


敏捷不只是一套方法论,敏捷也是一种思维模式。很多个人甚至团队尝试敏捷的过程中一个常见问题,是只取其方法实践,而未学其思维模式。这里说的思维模式,通俗讲就是指培养团队能够形成共识的文化,拥有一致的价值观和原则,塑造一个持续学习、自由、积极的团队氛围。以促使团队达到一种能够持续快速地交付有价值有质量的产品或服务的状态。


文化高于实践,成员能否融入团队文化,将会影响团队具体实践的高效程度。良好的团队文化,有利于促进团队内部的信息共享,从而产生更正确的决策。我们有时感觉自己已经引入敏捷了,但实则依旧保持着瀑布式思维,走的瀑布式开发流程,只是单纯学习并采用了一些好的敏捷实践,以至于最终达到的效果很有限。


这里引用《敏捷宣言》作者之一吉姆·海史密斯在他著作的《敏捷项目管理》中的一段总结: 


没有具体的实践,原则是贫瘠的;但是如果缺乏原则,实践则没有生命、没有个性、没有勇气。伟大的产品出自伟大的团队,而伟大的团队有原则、有个性、有勇气、有坚持、有胆量。


写在最后 


我们很难将整个敏捷的思维与方法流程应用到个人的独立开发工作中,因为敏捷需要坚实的团队基础,以及团队文化的保障,方可有效地落地执行。当然,我们并不否认个人可以尝试从敏捷中探索一些可借鉴学习的实践,并从中受益。


您如何看待这个问题呢,或者您是否有过将敏捷应用到个人的开发、工作、学习等方面的成功或失败的经验,欢迎在评论区一起分享交流。


 


参考资料:


《敏捷项目管理第2版》吉姆·海史密斯


敏捷开发网:http://www.minjiekaifa.com/


究竟什么是敏捷?http://www.zentao.net/blog/agile-…


作者:水牛GH
来源:juejin.cn/post/7308187262755061771
收起阅读 »

大屏可视化适配

web
如何适配屏幕 1.页面尺寸比与屏幕尺寸比的关系 首先设计稿的项目宽高比是16:9 大屏可视化需要用同一个页面,适配各种尺寸的屏幕。当显示屏幕的尺寸比与页面的尺寸比不一致时,则需要将页面尽可能放大居中显示,其余地方留白。 以16:9为例,当显示屏幕的尺寸比小于1...
继续阅读 »

如何适配屏幕


1.页面尺寸比与屏幕尺寸比的关系


首先设计稿的项目宽高比是16:9


大屏可视化需要用同一个页面,适配各种尺寸的屏幕。当显示屏幕的尺寸比与页面的尺寸比不一致时,则需要将页面尽可能放大居中显示,其余地方留白。

以16:9为例,当显示屏幕的尺寸比小于16:9时,
整个页面应该垂直居中,页面有效宽度与屏幕宽度相同。


image.png
当显示屏幕的尺寸比大于等于16:9 时,整个页面应该水平居中,页面有效高度应该与屏幕高度相同。


image.png


计算方法


image.png



  • Wp 为页面有效宽度

  • Hp 为页面有效高度

  • 页面左右居中,上下居中,四周留白即可

  • 然后在 head 里用 JS 设置 1rem = Wp / 100


* 2.动态 rem 方案



  • 为了适配不同的屏幕,在页面布局时要使用自适应布局,即不能使用固定像素,需要用相对单位 remem 是相对于父元素的字号的比例,rem 是相对于根元素 html 的字号的比例。
    为了使用上的方便,需要为根元素设置合适的字号。如果将页面有效宽度看成100份的话,我们希望 1rem=(Wp/100)px。因此将根元素的字号设置为Wp/100 即可。

    当我们根据设计图进行布局时,我们能得到的是每个区域的像素值 px,我们需要一个计算公式将 px 转换为 rem 单位。


适配一个div


div在设计稿的宽度:


image.png
换算公式封装成CSS函数


@function px($n) {
@return $n / 2420 * 100rem;
}

代码实现


<head> 中用 JS 获取到浏览器(即设备)的高度和宽度,并为根元素设置合适的字号。这部分可以定义为 initHTMLFontSize 函数


const clientHeight = document.documentElement.clientHeight
const clientWidth = document.documentElement.clientWidth

const pageWidth = (clientWidth / clientHeight < 16 / 9 && clientWidth > 500)? clientWidth : clientHeight * 16 / 9
const pageHeight = pageWidth * 9 / 16
window.pageWidth = pageWidth
window.pageHeight = pageHeight
document.documentElement.style.fontSize = `${pageWidth / 100}px`

<body> 底部用 JS 设置页面的有效高度和宽度,并使页面有效内容 #root 垂直居中。

这部分则定义为 initPagePosition 函数


const root = <HTMLElement>document.querySelector('#root')
root.style.width = window.pageWidth + 'px'
root.style.height = window.pageHeight + 'px'
root.style.marginTop = (document.documentElement.clientHeight - window.pageHeight) / 2 + 'px'

使页面有效内容 #root 水平居中只需用 CSS 设置margin-left: automargin-right: auto即可


3.Grid 布局划分各图表区域


在容器 <main>  标签里面用 grid-template-areas 给各图表写好分区,每一个栏位使用一个变量表示,对应的 item 内设置 grid-area 即可。

再用 grid-template-columnsgrid-template-rows 来设定每行每列的长度,设定长度时可以用 fr 来按比例分配。

grid-column-gapgrid-row-gap 用来设置列与列,行与行之间的间隙。


.home > main {
flex: 1;
display: grid;
grid-template-areas:
"box1 box2 box4 box5"
"box3 box3 box4 box5";
grid-template-columns: 366fr 361fr 811fr 747fr;
grid-template-rows: 755fr 363fr;
grid-column-gap: px(28);
grid-row-gap: px(28);
.section1 {
grid-area: box1;
}
.section2 {
grid-area: box2;
}
.section3 {
grid-area: box3;
}
.section4 {
grid-area: box4;
}
.section5 {
grid-area: box5;
}
}

作者:用户45275688681
来源:juejin.cn/post/7308434215811924018
收起阅读 »

实现一个简单的文本溢出提示效果

web
需求背景 写一段简单的HTML代码: <div class="container">超级无敌大怪兽在此!</div> 此时如果我们为其加上文本溢出处理,只需简单三行css代码即可搞定: .container { width:...
继续阅读 »

需求背景


写一段简单的HTML代码:


<div class="container">超级无敌大怪兽在此!</div>


此时如果我们为其加上文本溢出处理,只需简单三行css代码即可搞定:


.container {
width: 100px;

overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}


如果要加上hover显示完整文字的效果,也简单,各大组件库都有tooltip组件,套上就行了,我这里就简单加个title属性演示:


<div class="container" title="超级无敌大怪兽在此!">超级无敌大怪兽在此!</div>


可是这样不是很合理,如果我的文字本来就没有溢出,加这个提示没有意义,我只需要这段当文字不能完全展示时,才需要有这个提示,类似这种效果:



那么现在,别往下滑了,如果是聪明的你会怎么开发这个需求呢?先想一想,再往下看。









需求方案


其实比较简单哈,监听元素的mouseenter事件,然后判断元素的scrollWidth是不是大于clientWidth,就可以知道元素是否在水平方向上发生溢出,然后再加上tooltip就好了,完整代码如下:


<div class="container" onmouseover="handleMouseEnter(this)">超级无敌大怪兽在此!</div>
<div class="container large" onmouseenter="handleMouseEnter(this)">超级无敌大怪兽在此!</div>

.container {
width: 100px;

overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.large {
width: 500px;
}

const handleMouseEnter = (node) => {
if (node.scrollWidth > node.clientWidth) {
node.setAttribute('title', node.innerText);
}
};

然后就没了,emmmmmm。。。。


对,没了,就这么简单。


总结


这个需求呢,其实如果之前没接触过,一时半会还真不太能想到什么好的解法,但其实做过一遍或者看到过别人分享的思路,之后自己做的时候一下就能想到,所以就给大伙分享一下,万一就帮到你了呢。最重要的是,我又成功水了一篇文,嘿嘿。


作者:超级无敌大怪兽
来源:juejin.cn/post/7307468904732426267
收起阅读 »

携手15年,语雀创始人玉伯从蚂蚁离职,选择一个人远行

转载好文:雷锋网 本文作者:何思思 2023年4月28日,即4月28日凌晨,玉伯发朋圈称将要离开蚂蚁,今天也是玉伯在蚂蚁的最后一天。 他写道:“再见,山峰下的园区。一个人选择远行,并不一定是马云说的钱给少了或者心委屈了,也可以是为了远方有西湖般的美景。”下...
继续阅读 »

转载好文:雷锋网 本文作者:何思思



image.png


2023年4月28日,即4月28日凌晨,玉伯发朋圈称将要离开蚂蚁,今天也是玉伯在蚂蚁的最后一天。


他写道:“再见,山峰下的园区。一个人选择远行,并不一定是马云说的钱给少了或者心委屈了,也可以是为了远方有西湖般的美景。”下面的配图是园区风景,还有眺望远方的景色。


不愿做技术大佬,要做为产品服务的技术


“前端大牛、技术大佬”是业界给玉伯贴的标签,2008年加入淘宝后,玉伯先后做出了前端领域很火的框架 SeaJS、KISSY,之后带领团队通过开源做了很多技术产品。


但玉伯始终认为,技术只是工具,最终还是要为产品服务。所以当时在淘宝内部,玉伯一直是“折腾”的状态,加入淘宝那年,玉伯就参加了内部的赛马机制,跟团队做了几个月的创新产品,最后以失败告终,又回到了Java 团队做技术。


但这并有改变他要做创新产品的初心,于是2010 年到2011年,他一边做技术研发,一边继续摸索创新产品,但一直没做出能拿的出手的产品。直到2016年,在蚂蚁体验技术部的创新产品孵化机制策马扬鞭项目中,玉伯团队主导的语雀问世,并于2018年正式对公网提供服务。


也有内部人士称:玉伯当时和老板提了条件说,光做前端没意思,你要想留住我,就得给我一款产品做。所以当时玉伯自己要了一个团队,专门做一个闭环产品。


其实,从语雀诞生到现在经历了两次生死局:第一次是2018年,腾讯文档、钉钉文档、飞书文档相继亮相,文档产品迎来爆发期,当时阿里也想抓住这个风口,语雀最终把三分之二的人输送给了钉钉,作为钉钉文档的初始团队。在团队仅剩七八个人的时候,玉伯再次招人将团队扩充到二十人左右。


第二次是2020年,彼时,钉钉文档做了很久但并没达到预期效果,而语雀正值上升期,阿里云为了尽快把文档做起来,想把语雀、钉钉文档、阿里云笔记等内部各种文档团队聚集起来,成立一个独立的阿里文档事业部,由玉伯牵头,但却被无招反对,这也间接帮助了玉伯。


直到2021年,蚂蚁成立了智能协同事业部,其中语雀作为重点产品,以独立BU运作。


创业中的理想派,为了做好一件事而做


从2016年到现在,为了做好语雀,玉伯做了大量的工作。


玉伯曾回忆道,做语雀最大的一个感触是,啥都得做。最开始是半个PD,很快变成了客服,同时兼做运营,还需要去承担BD的工作,因为没有BD,只能逼着自己去做,一切为了产品往前跑。


也有用户在即刻分享道,自己曾经在语雀的付费用户群中提了一个文档的排序问题,当时玉伯就在群里,很快的响应了这个需求并做了优化。


image.png


此外,玉伯也背负了巨大的营收压力,尤其是近两年在阿里集团整体缩紧的状态下。雷峰网通过其他渠道了解到,集团也给语雀定了目标——“盈亏平衡”。


迫于压力,近两年语雀也调整了收费策略,2019年语雀开始尝试简单的商业化模式,即初级的团队语雀空间和语雀个人的收费版本;紧接着又重新设计了个人版价格策略,分为99元会员、199元超级会员、299元至尊会员三个档次,团队和空间版的收费则更高。


这对一个小团队来说并不容易,首先,较钉钉、飞书、腾讯文档而言,语雀强调的是知识管理的逻辑,其次,语雀服务的对象偏小众聚焦在侧重知识管理的用户,且这些目标对象比较分散,很难第一时间发掘到,这就意味着需要花很长时间去培养,没办法快速完成转化;再就是,虽然语雀团队不大,只有五六十人左右,但这部分人大都是互联网人才,成本也是一笔不小的支出。


雷峰网在之前拜访玉伯时听闻,目前语雀主要服务蚂蚁和阿里内部,在阿里内的日活已经达到了11万左右,商业化方面还比较单一,主要是通过发布会的方式宣传。由此可见,语雀的商业化路径还没完全打开。


无论选择出去创业还是集团内部创业,背负营收压力都是不可避免的。但抛开这个不谈,仅玉伯的个人角度出发,他曾谈过自己做语雀的初心,就是想把自己内心想做的事情做完,且这件事还能帮助到别人,就做了。


正是这种简单纯粹的心态,让玉伯在做语雀时只专注事情的本身以及这件事情创造的价值,而并非拼命地追求变现。


雷峰网(公众号:雷峰网)曾发表文章《留给飞书的时间》,他如此评论:



“现实主义者关注的是钱,理想主义者关注的是时间,当代这个社会,钱很重要。但更重要的,对个体来说,是如何提高时间的质量,对人类来说,不仅关注时间的质量,还关注整个人类时间的长短,是否可延续下去。赚钱是为了花钱,花钱是为了提升时间的品质甚至长度。围绕钱的现实主义者,最终会为围绕时间的理想主义者服务。”



从玉伯最新的朋友圈内容,不难看出,他的离开或许和钱权没有太大的关系,而是为了追求心目中的诗和远方。他也曾经说过自己有三个梦:“技术梦、产品梦、自由梦。”离开蚂蚁,或许是为了去实现他的“自由梦。”


作者:狗头大军之江苏分军
来源:juejin.cn/post/7299035378589040667
收起阅读 »

基于css3写出的底部导航栏效果(详细注释)

web
进行准备工作 这边对基本的样式进行了设置,首先在html部分设置了一个名为nav的div,推荐大家语义化来写不要学我这种,随后进行基本的默认样式的清除,并且设置盒子为ie盒子方便后续的计算,整体都设置为弹性盒,方便后续矢量文字的操作,对导航栏nav进行定位,...
继续阅读 »

进行准备工作



这边对基本的样式进行了设置,首先在html部分设置了一个名为nav的div,推荐大家语义化来写不要学我这种,随后进行基本的默认样式的清除,并且设置盒子为ie盒子方便后续的计算,整体都设置为弹性盒,方便后续矢量文字的操作,对导航栏nav进行定位,方便后续位置上的操作



image.png



<body>
<!-- 目前就一个简单的nav,推荐大家语义化来写 -->
<div class="nav"></div>
</body>

<style>
/* 清除一些默认样式 */
*{
margin: 0;
padding: 0;
box-sizing: border-box;
list-style: none;
}
a{
text-decoration: none;/*确保在浏览器中显示链接时,没有任何文本装饰,如下划线。 */
}
/* 对整体进行设置,并且都设置为弹性盒,方便进行操作 */
body{
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #222327;
}
/* 设置导航栏样式 */
.nav{
/* 对导航栏位置进行定位处理,方便后续的图标位置的设置 */
position: relative;
width: 400px;
height: 70px;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px;
}
</style>


引入矢量文字



这里面呢引用了阿里巴巴的矢量文字效果,具体如何使用请见http://www.iconfont.cn/manage/inde…
里面的教程,这边我挑了五个字体图标加入到了网页中,并且用ul和lil加入到了导航栏中,目前是竖着排列的,后续加入css样式之后会好起来,并且在第一个li上加入了active的css样式,用于设置选中效果



image.png


image.png


<link rel="stylesheet" href="//at.alicdn.com/t/c/font_4173165_2g4t5a6pg9v.css">
<div class="nav">
<ul>
<li class="active"> <span><i class="iconfont icon-shouye"></i></span></li>
<li > <span><i class="iconfont icon-liuyan"></i></span></li>
<li > <span><i class="iconfont icon-code"></i></span></li>
<li > <span><i class="iconfont icon-box-empty"></i></span></li>
<li > <span><i class="iconfont icon-gitee-fill-round"></i></span></li>
</ul>
</div>

对导航栏和ui li字体图标进行设置



这里面呢针对ul和li进行了设置,使之达到了图下的效果,对ul 和li进行了弹性盒的设置,li中的使用flex:1让这些矢量文字按等份划分容器宽度,使之达到了一个距离平均的样式,并且设置了这个zindex的叠加级别



image.png


    .nav{
/* 对导航栏位置进行定位处理,方便后续的图标位置的设置 */
position: relative;
width: 400px;
height: 70px;
background: #fff;
display: flex;
justify-content: center;
align-items: center;
border-radius: 10px;
}
.nav ul{
display: flex;
width: 350px;
}
.nav ul li{
height: 60px;
/* flex:1是让所有的li平均分nav这个容器 */
flex: 1;
position: relative;
z-index: 2;
display: flex;
justify-content: center;
}

继续设置i元素和span元素



这里呢针对了span元素和i元素进行了设置,通过span元素蒋i元素中的矢量图标设置到水平垂直都居中的位置,并且设置了圆角,加入了动画和动画延迟,针对i元素将文字大小设置了,并且在html中加入了对应图标的文字效果,并且为例美观在每个li元素中都添加了一个选中时候的不同的颜色,使用了变量--clr用于获取选中效果 行内样式是一种直接在HTML元素上指定样式的方法,在这种情况下,你使用 style 属性将 --clr 变量设为不同色



  .nav ul li span{
/* 进行定位,使之通过span元素带动矢量图标进行水平垂直到中心位置 */
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 55px;
height: 55px;
border-radius: 50%;
/* 设置鼠标移入的样式 */
cursor: pointer;
/* 设置动画过度事件以及延迟 */
transition: 0.5s;
transition-delay: 0s;
}
.nav ul li span i{
color: #222327;
font-size: 1.5em;
}

<body>
<!-- 目前就一个简单的nav,推荐大家语义化来写 -->
<div class="nav">
<ul>
<!-- 设置active效果,用于获取选中效果 用于获取选中效果 行内样式是一种直接在HTML元素上指定样式的方法,在这种情况下,你使用 style 属性将 --clr 变量设为不同色 -->
<li class="active" style="--clr:#f44336"><span><i class="iconfont icon-shouye"></i>首页</span></li>
<li style="--clr:#0fc70f"> <span><i class="iconfont icon-liuyan"></i>留言</span></li>
<li style="--clr:#2196f3"> <span><i class="iconfont icon-code"></i>代码</span></li>
<li style="--clr:#b145e9"> <span><i class="iconfont icon-box-empty"></i>盒子</span></li>
<li style="--clr:#ffa111"> <span><i class="iconfont icon-gitee-fill-round"></i>gitee</span></li>
<div class="indicator"></div>
</ul>
</div>
</body>

image.png


下面设置选中时候的样式,在这里呢针对span元素设置了选中的时候会向上位移到这个地方,并且在矢量图标的地方设置了开始选中的时候将文字颜色改为和背景颜色一样的颜色,这样当点击的那一刻,图标会出现消失的情况,当超出导航栏到黑色部分的时候,文字就会显示出来,在后面,设置了一个半圆的背景图,当背景图位移到文字的位置的时候,矢量文字就会显示出来


/* 下面是针对选中效果做的样式处理 */
.nav ul li.active span {
/* 设置了一开始的背景颜色,后面会被取代,设置了点击的时候会向上移动 */
background: orange;
transform: translateY(-27px);
transition-delay: 0.25s;
}

.nav ul li.active span i {
/* 设置了点击时候矢量图标的文字颜色 */
color: #fff;
}


image.png


设置模糊效果



这里呢加入了一个模糊的效果,配合后面的选中的时候图标颜色显示会形成一个类似于色彩过度的效果,并且将i元素上面设置的颜色显示出来



    .nav ul li span::before {
content: '';
position: absolute;
top: 10px;
left: 0;
width: 100%;
height: 100%;
background: orange;
border-radius: 50%;
filter: blur(40px);
opacity: 0;
transition: 0.5s;
transition-delay: 0s;
}
.nav ul li span::before {
opacity: 0.5;
transition-delay: 0.25s;
}
/* 这里将i元素设置的颜色显示出来 这两个样式块中都使用了 background: var(--clr); 属性,可以将背景颜色设置为clr 变量所表示的值。这种使用自定义变量的方式,可以在代码中统一定义颜色值,以便在需要时进行统一更改。*/
.nav ul li.active span {
background: var(--clr);
}

.nav ul li span::before {
background: var(--clr);
}


image.png


接下来设置背景圆



这里呢设置了背后的那个向下突兀的圆,其原理是通过位置的调整和颜色的与背景颜色的一致加上zindex的图册优先级的显示,构成了这么一个背景半圆形图



.indicator {
/* 这里进行了定位,并且设置了背景园的位置,同时将圆的背景颜色与背景颜色设为一致,会形成那种向下突兀的圆形,并且加入了动画 ps:这个过度的小圆弧我是真设置不好,凑合看吧,大佬们有能力的可以试试设置一下*/
position: absolute;
top: -35px;
width: 70.5px;
height: 70px;
background: #222327;
border-radius: 50%;
z-index: 1;
transition: 0.5s;
}
/* 设置左边半弧 */
.indicator::before {
content: '';
position: absolute;
top: 16px;
left: -34px;
width: 10px;
height: 5px;
background: transparent;
border-radius: 50%;
box-shadow: 20.5px 19px 0 4px #fff;
}
/* 设置右边半弧 */
.indicator::after {
content: '';
position: absolute;
top: 16px;
left: 54px;
width: 10px;
height: 5px;
background: transparent;
border-radius: 50%;
box-shadow: 20px 19px 0 4px #fff;
}

image.png


****动画设置,配合js形成点击的时候,active会移动到点击的目标身上



这里呢使用了nth-child选择器选中对应的i元素,注意,这里设置的平移效果是由clac函数计算而来,选中其中一个i元素,并且当且仅当具有active类之后的所有兄弟中的.indicator类元素,有一个指示器元素(.indicator)。指示器的位置会根据活动项目(具有active类的<li>元素)的位置进行调整。
根据活动项目的位置设置指示器的水平平移距离,实现一个在导航菜单中显示当前选中项目的效果。指示器的位置和平移距离是根据活动项目的索引和固定的长度单位(70px)进行计算的。



/*/* nth-child()选中低某个i元素,然后配合js完成背景圆的移动 
在CSS中,calc() 是一个用于执行计算的函数。它允许在CSS属性值中使用数学表达式。
这种计算函数通常用于允许动态计算和调整元素的尺寸、间距或位置。在 calc() 函数中,可以使用不同的运算符(如加号 +、减号 -、乘号 *、除号 /)来结合数值和单位进行计算。
它可以包含其他长度单位(如像素 px、百分比 % 等),并且可以与其他CSS属性值和变量一起使用。

当一个 `<li>` 元素具有 `active` 类时,对应的 `.indicator` 元素会相对于活动项目的位置水平平移一个特定的距离。每个 `.indicator` 元素的平移距离相对于其前面的活动项目索引和一个固定的长度单位(`70px`)计算得出。

*/
*/

.nav li:nth-child(1).active~.indicator{
transform: translateX(calc(70px*0));
}

.nav li:nth-child(2).active~.indicator {
transform: translateX(calc(70px*1));
}

.nav li:nth-child(3).active~.indicator {
transform: translateX(calc(70px*2));
}

.nav li:nth-child(4).active~.indicator {
transform: translateX(calc(70px*3));
}

.nav li:nth-child(5).active~.indicator {
transform: translateX(calc(70px*4));
}


这里配合js代码,通过foreach为点击的li或者为所有的li进行添加或者移入active样式


<script>
//通过 `lis.forEach(li => li.addEventListener('click', function () {...}))` 遍历 `lis` 数组中的每个元素,并为每个元素都添加一个 ‘click’ 事件监听器。
//在每次点击事件中,使用 `lis.forEach(item => {...})` 遍历 `lis` 数组中的每个元素,将它们的 `active` 类都移除,然后在当前被点击的元素上添加 `active` 类,
const lis = document.querySelectorAll('.nav li')
lis.forEach(li => li.addEventListener('click', function () {
lis.forEach(item => {
item.classList.remove('active');
this.classList.add('active');
})
}))
</script>

image.png


效果展示


ezgif.com-video-to-gif.gif


总结


这里配合js使用的动画是值得我学习的,通过js点击赋予不同的liactive样式,又根据active所在的li元素经过计算对.indicator元素进行平移,使之完成这个动画效果


已上传至gitee
gitee.com/wu-canhua/b…


作者:如意呀
来源:juejin.cn/post/7262334378759405605
收起阅读 »

2023年终总结:不想内卷要如何破局

年目标 算法刷了一丢丢,争取上班每天至少刷一题 往全栈方向转,Nest学习情况 待填坑: nginx复习(是的几年没用忘光了) docker k8s 理财计划 我是从大学就有存钱意识了,大二定定存基金,每个月固定存几百进去,虽然不多,但是积少成多,到了...
继续阅读 »

年目标


算法刷了一丢丢,争取上班每天至少刷一题


image.png


往全栈方向转,Nest学习情况


待填坑:



  • nginx复习(是的几年没用忘光了)

  • docker

  • k8s
    image.png


理财计划


我是从大学就有存钱意识了,大二定定存基金,每个月固定存几百进去,虽然不多,但是积少成多,到了大三暑假旅游时,存钱数量已经是同学里最多的(不到1万但是够旅游一次了)


《小狗钱钱》/《富爸爸穷爸爸》对我来说借鉴意义不大,对我影响比较大的理财书籍是《工作前5年,决定你一生的财富》,我工作5年存的钱也比作者多一点,由于股市即时抽身(疫情期间),赚了一丢丢


之前有个同事还会制定每年的理财收支表,总结收入存款情况,被卷到了


休闲


上半年感觉一直在上班,心态很差,4月周末去了一趟潮州


公司团建


sunset.jpg
luying.jpg


tuanjian2.jpg


6月内蒙古,他们那边都不吃蔬菜的,吃了一周的牛羊肉都不想吃肉了


neimeng.jpg


7月蔡依林演唱会,票没抢到,无奈去了票贩子那里高价收了看台票


jolin.jpg


8月张韶涵演唱会,很顺利抢到了内场票,就是心脏振动的有点不舒服


anglela.jpg


me.jpg


9月emo了去了苏杭,风景很秀丽


hanzghou1.jpg


suzhou2.jpg


suzhou1.jpg
大闸蟹个人感觉不好吃


wuzhen2.jpg


10月回家了,如果可以想一直待在家里


hometown.jpg


11月去了一趟腾冲,我果然是精神云南人,想每年去一次云南


烧肉米线、铜瓢牛肉、过桥米线、稀豆粉我都好喜欢,可惜过了吃野生菌的季节


tengchong1.jpg
银杏村


tengchong2.jpg


tengchong3.jpg


英雄联盟手游这个赛季卡在大师上不去,老是遇到抢位置的骂人的,搞我心态


接下来还有蔡健雅演唱会和邓紫棋演唱会


书籍


没有特意约束一年要看多少书,《长安的荔枝》/《撒哈拉的故事》/《小家越装越大》都不错


要存钱准备房子装修了


后疫情时代


疫情过后,降薪裁员,我们组也减员了,更少的工资更多的工作。很多同行应该也经历过节假日加班。上半年经常会因为工作的事情失眠,后面心态也放平了,不能把工作带到生活中,休假的时候就好好享受,上班再处理工作的事情。(对于心理健康大有裨益)


内卷是资源少了毫无意义的恶性竞争,现在我们这行就有这个情况,目前工作上内卷没有什么前途,总之我想试试往远程方向,躺平是不可能躺平的


豆瓣上有fire小组,基本上都是年龄35+/40+的人,20多岁fire的还是少数,不考虑结婚生子买房,其实人过完这一生不需要花太多


目前前端已经是老手了,怎么面对35岁危机,还没有好的思绪


作者:lyllovelemon
来源:juejin.cn/post/7308624619163009075
收起阅读 »

还在手打console.log?快来试试这个vscode插件 Quickly Log!!

背景 作为一枚前端开发人员,尤其是在写业务代码的时候,避免不了需要经常在控制台打印变量来查看数据(反正我是这样哈哈哈哈哈),那么就需要频繁的来写console.log(),然后在里面再输入自己想要查看的变量名。 思考 既然我们需要频繁的来进行这个操作,那么我们...
继续阅读 »

背景


作为一枚前端开发人员,尤其是在写业务代码的时候,避免不了需要经常在控制台打印变量来查看数据(反正我是这样哈哈哈哈哈),那么就需要频繁的来写console.log(),然后在里面再输入自己想要查看的变量名。


思考


既然我们需要频繁的来进行这个操作,那么我们是不是可以把它像代码片段一样来保存下来,然后配置一个激活他的快捷键来进行使用


在左下角这里选择用户代码片段


image.png


然后选择想要使代码片段生效的文件类型,比如我这里选择的tsx



选择了对应的文件类型,对应的代码片段只会在这个类型的文件里生效,想要在其他类型的文件里也使用同样的代码片段需要去对应的类型文件中复制一份



image.png


把对应的代码片段写入


"Print to console": {
// 说明
"description": "Log output to console",
// 快捷键
"prefix": "cl",
// 输出内容
"body": ["console.log($1)"]
},

这样我们就配置好了一个简易的代码片段,使用的时候只需要敲出来 ’cl‘就会出现我们的代码提示


image.png


这样我们就解决了这个问题(我自己也是使用这个方法很久)


更进一步


目前我们通过代码片段已经解决了这个问题,但是还是会有一些不方便的地方



  • 我们只能在写好代码片段的类型文件中使用,我们现在使用 .tsx,突然要写 .vue 了使用的时候发现没有生效,就又要再去配置一次

  • 如果我们目前的 console.log 比较多,那么控制台上就会看到输出了一堆的变量,根本搞不清哪个打印是对应的哪个变量

  • 有时候会遗忘删掉 console.log 语句 (也可以通过配置husky,在commit的时候进行校验解决)


为了解决这些问题,我们更进一步,来通过写一个vscode插件解决


Quickly Log


这个插件最开始只是对vscode插件开发的好奇,加上自己确实有这方面的需求才开始编写的。编写成插件就可以有效的解决了需要重复配置代码片段的问题。


这里介绍下插件的功能,不对代码具体介绍,感兴趣的可以去github上看下代码 github.com/Richard-Zha…


功能


提示配置


只需要将光标移动到变量附近,然后使用快捷键 Cmd + Shift + L,就会在下一行输出语句


image.png


这里也支持携带上变量所在的行号以及文件名


image.png


当然这些都是可以配置的,可以根据自己的喜好来配置输出的提示内容


image.png


如果是简洁党也可全都取消勾选,效果就和直接使用上面提到的代码片段一样,但是会支持自动将变量放入console.log()的括号内


一键clear


执行 Cmd + Shift + K 就会将当前页面匹配到的console.log语句自动删除


一键切换注释


执行 Cmd + Shift + J 就会将当前页面匹配到的console.log语句前面自动打上注释,再执行就会取消注释


快捷键都是可以更改的 vscode左下角的设置icon点开 点击键盘快捷方式 输入 Quickly Log进行更改


以上就是目前插件支持的功能了,欢迎大家去Vscode下载使用


image.png


TODO


目前有些场景的打印是有问题的


比如下面这样的换行场景,我们希望在光标放在a,b,c这里的时候,会在第21行这里插入console.log语句,但是目前只会在光标的下一行插入,还需要手动移动到下面


image.png


image.png


之前有试过通过判断是否在 {} 内来输入到整个语句之后,但是情况不太理想,后续再考虑解决


作者:Richard_Zhang
来源:juejin.cn/post/7306806944046678052
收起阅读 »

Android 14 适配的那些事情

大家好,我叫 Jack Darren,目前主要负责国内游戏发行 Android SDK 开发简介距离 Android 14 发布已经有一段时间了,趁着这次机会,了解和熟悉了 Android 14 更新的内容,现在来和大家分享一下,大家喜欢的话可以点个赞多多支持...
继续阅读 »
  • 大家好,我叫 Jack Darren,目前主要负责国内游戏发行 Android SDK 开发

简介

  • 距离 Android 14 发布已经有一段时间了,趁着这次机会,了解和熟悉了 Android 14 更新的内容,现在来和大家分享一下,大家喜欢的话可以点个赞多多支持一下,文章的内容按照适配内容的重要程度进行排序。

targetSdk 版本要求

  • 在 Android 14 上面,新增了一个要求,要求新安装的应用的 targetSdkVersion 需要大于等于 23(即 Android 6.0 及以上),如果小于这个值将无法在 Android 14 的设备上面安装,此时大家心里可能有疑惑了,谷歌为什么要求那么做呢?我们来看看谷歌的原话是什么
恶意软件通常会以较旧的 API 级别为目标平台,
以绕过在较新版本 Android 中引入的安全和隐私保护机制。
例如,有些恶意软件应用使用 targetSdkVersion 22
以避免受到 Android 6.0 Marshmallow(API 级别 23)在 2015 年引入的运行时权限模型的约束。
这项 Android 14 变更使恶意软件更难以规避安全和隐私权方面的改进限制。
  • 从上面这段话不难看出来谷歌的用意,其实为了保障用户的手机安全,如果用户安装应用的 targetSdkVersion 版本过低,有一些恶意软件会利用高系统会兼容旧软件这一特性(漏洞),故意绕过系统的安全检查,从而会导致 Android 高版本上面一些安全特性无法生效,没有了系统的管束,这些恶意软件可能就会肆意乱来。
  • 另外你如果想在 Android 14 系统上面,仍然要安装 targetSdkVersion 小于 23 的应用,可以通过以下 adb 命令来安装 apk,这样就能绕过系统的安装限制。
adb install --bypass-low-target-sdk-block xxx.apk

前台服务类型要求

  • 如果你的应用 targetSdkVersion 升级到了 34(即 Android 14),并且在 Service 中调用了 startForeground 方法,那么就需要进行适配了,否则系统会抛出 MissingForegroundServiceTypeException 异常,这是因为在 Android 14 上面,要求应用在开启前台服务的时候,需要注明这个前台服务的用途,谷歌给我们列举了以下几种用途:
用途说明清单文件权限要求运行时要求
摄像头继续在后台访问相机,例如支持多任务的视频聊天应用FOREGROUND_SERVICE_CAMERA请求 CAMERA 运行时权限
连接的设备与需要蓝牙、NFC、IR、USB 或网络连接的外部设备进行互动FOREGROUND_SERVICE_CONNECTED_DEVICE必须至少满足以下其中一个条件:

在清单中至少声明以下其中一项权限:

CHANGE_NETWORK_STATE
CHANGE_WIFI_STATE
CHANGE_WIFI_MULTICAST_STATE
NFC
TRANSMIT_IR
至少请求以下其中一项运行时权限:

BLUETOOTH_CONNECT
BLUETOOTH_ADVERTISE
BLUETOOTH_SCAN
UWB_RANGING
调用 UsbManager.requestPermission()

数据同步数据传输操作,例如:

数据上传或下载
备份和恢复操作
导入或导出操作
获取数据
本地文件处理
通过网络在设备和云端之间传输数据
FOREGROUND_SERVICE_DATA_SYNC
健康为健身类别的应用(例如锻炼追踪器)提供支持的所有长时间运行的用例FOREGROUND_SERVICE_HEALTH必须至少满足以下其中一个条件:

在清单中声明 HIGH_SAMPLING_RATE_SENSORS 权限。

至少请求以下其中一项运行时权限:

BODY_SENSORS
ACTIVITY_RECOGNITION
位置需要位置信息使用权的长时间运行的用例,
例如导航和位置信息分享
FOREGROUND_SERVICE_LOCATION至少请求以下其中一项运行时权限:

ACCESS_COARSE_LOCATION
ACCESS_FINE_LOCATION
媒体在后台继续播放音频或视频。
在 Android TV 上支持数字视频录制 (DVR) 功能。
FOREGROUND_SERVICE_MEDIA_PLAYBACK
媒体投影使用 MediaProjection API 将内容投影到非主要显示屏或外部设备。这些内容不必全都为媒体内容。不包括 Cast SDKFOREGROUND_SERVICE_MEDIA_PROJECTION调用 createScreenCaptureIntent() 方法。 无
麦克风在后台继续捕获麦克风内容,例如录音器或通信应用FOREGROUND_SERVICE_MICROPHONE请求 RECORD_AUDIO 运行时权限
打电话使用 ConnectionService API 继续当前通话FOREGROUND_SERVICE_PHONE_CALL在清单文件中声明 MANAGE_OWN_CALLS 权限。
消息服务将短信从一台设备转移到另一台设备。在用户切换设备时,帮助确保用户消息任务的连续性FOREGROUND_SERVICE_REMOTE_MESSAGING
短期服务快速完成不可中断或推迟的关键工作。

这种类型有一些独特的特征:

只能持续运行一小段时间(大约 3 分钟)。
不支持粘性前台服务。
无法启动其他前台服务。
不需要类型专用权限,不过它仍需要 FOREGROUND_SERVICE 权限。
正在运行的前台服务不能更改为 shortService 类型或从该类型更改为其他类型。
特殊用途涵盖其他前台服务类型未涵盖的所有有效前台服务用例。

除了声明 FOREGROUND_SERVICE_TYPE_SPECIAL_USE 前台服务类型之外,开发者还应在清单中声明用例。为此,他们会在 `` 元素内指定  元素。当您在 Google Play 管理中心内提交应用时,我们会审核这些值和相应的用例。
FOREGROUND_SERVICE_SPECIAL_USE
系统豁免为系统应用和特定系统集成预留,
使其能继续使用前台服务。

如需使用此类型,应用必须至少满足以下条件之一:

设备处于演示模式状态
应用是设备所有者
应用是性能分析器所有者
属于具有 ROLE_EMERGENCY 角色的安全应用
属于设备管理应用
否则,声明此类型会导致系统抛出 ForegroundServiceTypeNotAllowedException
FOREGROUND_SERVICE_SYSTEM_EXEMPTED
  • 介绍完这几种前台服务类型,接下来介绍如何适配它,适配前台服务类型的特性方式具体有两种方式,一种是注册清单属性,另外一种是代码动态注册
<service
android:name=".XxxService"
android:foregroundServiceType="dataSync"
android:exported="false">

service>
startForeground(xxx, xxx, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC);
  • 另外附上前台服务类型对应的适配属性
用途清单文件属性值Java 常量值
摄像头cameraServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
连接的设备connectedDeviceServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
数据同步dataSyncServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
健康healthServiceInfo.FOREGROUND_SERVICE_TYPE_HEALTH
位置locationServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION
媒体mediaPlaybackServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
媒体投影mediaProjectionServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
麦克风microphoneServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
打电话phoneCallServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
消息服务remoteMessagingServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
短期服务shortServiceServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
特殊用途specialUseServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
系统豁免systemExemptedServiceInfo.FOREGROUND_SERVICE_TYPE_SYSTEM_EXEMPTED

图片和视频的部分访问权限

  • 谷歌在 API 33(Android 13)上面引入了 READ_MEDIA_IMAGES 和 READ_MEDIA_VIDEO 这两个权限,目前针对这两个权限在 Android 14 上面有新的变动,具体的变动点就是新增了 READ_MEDIA_VISUAL_USER_SELECTED 权限,那么这个权限的作用是什么呢?我们都知道 READ_MEDIA_IMAGES 和 READ_MEDIA_VIDEO 是申请图片和视频权限的,但是这样会有一个问题,当第三方应用申请到权限后,就拥有了手机相册中所有照片和视频的访问权限,这是十分危险的,也是非常不可控的,因为用户也无法知道第三方应用会干什么,所以谷歌在 API 34(Android 14)引入了这个权限,这样用户拥有了更多的选择,可以将相册中所有的图片和视频授予给第三方应用,也可以将部分的图片和视频给第三方应用。
  • 讲完了这个特性的来龙去脉,那么接下来讲讲这个权限如何适配,如果你的应用申请了 READ_MEDIA_IMAGES 或者 READ_MEDIA_VIDEO 权限,并且 targetSdkVersion 大于等于 33(Android 13),那么需要在申请权限时携带上 READ_MEDIA_VISUAL_USER_SELECTED 权限方能正常申请,如果不携带上 READ_MEDIA_VISUAL_USER_SELECTED 权限就申请 READ_MEDIA_IMAGES 或者 READ_MEDIA_VIDEO 权限,会弹出权限询问对话框,但是如果用户是选择全部授予,那么 READ_MEDIA_IMAGES 或者 READ_MEDIA_VIDEO 权限状态是已授予的状态,如果用户是选择部分授予,那么 READ_MEDIA_IMAGES 或者 READ_MEDIA_VIDEO 权限状态是已拒绝的状态,假设此时有携带了 READ_MEDIA_VISUAL_USER_SELECTED 权限的情况下,那么 READ_MEDIA_VISUAL_USER_SELECTED 权限是已授予的状态。
  • 看到这里,脑洞大的同学可能有想法了,那我不申请 READ_MEDIA_IMAGES 或者 READ_MEDIA_VIDEO 权限,我就只申请 READ_MEDIA_VISUAL_USER_SELECTED 权限行不行啊?答案也是不行的,我替大家试验过了,这个权限申请会在不会询问用户的情况下,被系统直接拒绝掉。
  • 另外需要的一点是 READ_MEDIA_VISUAL_USER_SELECTED 属于危险权限,除了在运行时动态申请外,还需要在清单文件中进行注册。

registerReceiver 需要指定导出行为

  • 谷歌在 Android 12 (API 31)新增了四大组件需要指定 android:exported 属性的特性,这次在 Android 13 上面做了一些变动,因为谷歌之前只考虑到静态注册四大组件的情况,但是遗漏了一种情况,BroadcastReceiver 不仅可以静态注册,还可以动态注册,动态注册的广播不需要额外在 AndroidManifest.xml 中再进行静态注册,所以这次谷歌将这个规则漏洞补上了,并且要求开发者在动态注册广播的时候,能够指定 BroadcastReceiver 是否能支持导出,由此来保护应用免受安全漏洞的影响。
  • 到此,大家心中可能有一个疑惑,这里的支持导出是什么意思?有产生什么作用?可以先看一下谷歌官方的原话
为了帮助提高运行时接收器的安全性,Android 13 允许您指定您应用中的特定广播接收器是否应被导出以及是否对设备上的其他应用可见。
如果导出广播接收器,其他应用将可以向您的应用发送不受保护的广播。
此导出配置在以 Android 13 或更高版本为目标平台的应用中可用,有助于防止一个主要的应用漏洞来源。

在以前的 Android 版本中,设备上的任何应用都可以向动态注册的接收器发送不受保护的广播,除非该接收器受签名权限的保护。
  • 谷歌的解释很明了,如果广播支持导出,那么其他应用可以通过发送这个广播触发我们应用的逻辑,这可能会发生程序安全漏洞的问题。
  • 那么该如何适配这一特性呢?谷歌官方提供了一个 registerReceiver(BroadcastReceiver receiver, IntentFilter filter, int flags) API,flags 参数传入 Context.RECEIVER_EXPORTED(支持导出) 或 Context.RECEIVER_NOT_EXPORTED(不支持导出),具体的代码适配代码如下:
String action = "xxxxxx";
IntentFilter filter = new IntentFilter(action);
if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
context.registerReceiver(new LocaleChangeReceiver(), filter, Context.RECEIVER_EXPORTED);
} else {
context.registerReceiver(new LocaleChangeReceiver(), filter);
}
  • 还有一种情况,不需要指定 flag 参数,就是当要注册的广播 action 隶属系统的 action 时候,这个时候可以不需要指定导出行为。

更安全的动态代码加载

  • 如果我们应用有动态加载代码的需求,并且此时 targetSdk 升级到了 API 34(即 Android 14),那么需要注意一个点,动态加载的文件(Jar、Dex、Apk 格式)需要设置成可读的,具体案例的代码如下:
File jar = new File("xxxx.jar");
try (FileOutputStream os = new FileOutputStream(jar)) {
jar.setReadOnly();
} catch (IOException e) { ... }
PathClassLoader cl = new PathClassLoader(jar, parentClassLoader);
  • 至于谷歌这样做的原因,我觉得十分简单,是为了程序的安全,防止有人抢先在动态加载之前先把动态文件替换了,那么会导致执行到一些恶意的代码,间接导致应用被入侵或者篡改。
  • 另外需要注意的一个点的是,如果你的应用 targetSdk 大于等于 API 34(即 Android 14),如果不去适配这一特性,那么运行在 Android 14 的手机上面系统会抛出异常。

屏幕截图检测

  • Android 14 新增引入了屏幕截图检测的 API,方便开发者更好地检测到用户的操作,具体的使用案例如下:

    1. 在清单文件中静态注册权限
    <uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />
    1. 创建监听器对象
final Activity.ScreenCaptureCallback screenCaptureCallback = new Activity.ScreenCaptureCallback() {

@Override
public void onScreenCaptured() {
// 监听到截图了
}
};
  1. 在合适的时机注册监听
public final class XxxActivity extends Activity {

@Override
protected void onStart() {
super.onStart();
registerScreenCaptureCallback(executor, screenCaptureCallback);
}
}
  1. 在合适的时机取消注册监听
public final class XxxActivity extends Activity {

@Override
protected void onStop() {
super.onStop();
unregisterScreenCaptureCallback(screenCaptureCallback);
}
}
  • 需要注意的是,如果使用的是 adb 进行的截图,并不会触发 onScreenCaptured 监听方法。
  • 如果不想你的应用能被系统截图,可以考虑给当前的 Window 窗口加上 WindowManager.LayoutParams.FLAG_SECURE 标记位。
  • 最后表达一下我对这个 API 看法,这个 API 设计得不是很好,比如应用想知道用户是否截图了,应用可能需要知道的是,截图文件的存放路径,但是 onScreenCaptured 是一个空参函数,也就意味着没有携带任何参数,如果要实现获取截图文件存放路径的需求,可能还需要沿用之前的老方式,即使用 ContentObserver 监听媒体数据库的变化,然后从几个维度(文件时间维度、文件路径维度、图片尺寸维度)判断新增的图片是否为用户的截图,这种实现的方式相对是比较麻烦的,但是也无发现更好的实现方式。
  • 完结,撒花 ✿✿ヽ(°▽°)ノ✿


    作者:37手游移动客户端团队
    来源:juejin.cn/post/7308434314777772042
    收起阅读 »

    大厂程序员开摆!——在5A级景区写代码是一种什么样的体验?

    大厂程序员开摆!——在5A级景区写代码是一种什么样的体验? 欢迎关注,​分享更多原创技术内容~ 微信公众号:ByteRaccoon、知乎:一只大狸花啊、稀土掘金:浣熊say 微信公众号海量Java、数字孪生、工业互联网电子书免费送~ 怎么就去5A级景区了? ...
    继续阅读 »

    大厂程序员开摆!——在5A级景区写代码是一种什么样的体验?


    欢迎关注,​分享更多原创技术内容~


    微信公众号:ByteRaccoon、知乎:一只大狸花啊、稀土掘金:浣熊say


    微信公众号海量Java、数字孪生、工业互联网电子书免费送~


    怎么就去5A级景区了?



    在我们的刻板印象里面,程序员一般是穿着格子衬衫,蹲在某个公司的小格子里面,啪啪啪码着代码。


    但是,今天我必须对这种对这种刻板印象say no,其实除了在呆在大城市里面,很多程序员的岗位还是需要出差的,尤其是在工业互联网领域。


    现在随着国家对工业智能化的愈发重视,很多大型的制造业公司(大多数是大型央企,国企)都在推进智慧化建设,而这些项目一般都不是在城市里面的,作为开发有时候也需要到现场去了解情况,部署环境和现场开发等。


    比如说像中国石油,中国五矿这样的大型央企,很多的油田项目、矿山项目、冶炼厂项目等都需要进行智慧化管理和建设,因此,很多这种公司的岗位都要求程序员能够接受一定程度的出差驻场工作,这也就给了我们广大程序员去见识祖国大好河山的机会。


    而且不像真正的工程技术和施工人员需要一整个项目周期都长时间呆在项目上,作为一名程序员大多数时候就是去现场打个样,短期出差就搞定,也不影响你的都市丽人生活。但是,像矿山、油田这样的项目出差环境还是比较恶劣的,虽然有着丰厚的出差补助,但是让你在深山老林呆上几个月,作为一名宝宝可能还是很难接受这样的生活。


    作为运气比较好的一名央企员工,我就没有这样的烦恼,虽然我也曾出差驻场,但是出差的地方不是环境恶劣的油田、矿山,而是5A级景区——黄山。下面我就来掰扯掰扯作为一个程序员在5A级景区出差是什么体验,在景区摸鱼的时候都干了点儿啥事儿~


    为什么会在景区写代码?



    一张简简单单的机票就得飞去景区呆上几个月,其实以前我也从来没想到过说这种风景区还需要程序员,而且对于这种项目来说特别喜欢让人驻场开发,为了避免被定位,关键信息我就码住了。


    其实作为一名后台程序员来说,只要网络环境弄好了,在那里开发对我们来说也没啥区别,反而在宽敞的写字楼里面码代码的效率可能会高上不少。


    但是,事情就是这么不凑巧,年初项目刚开始的阶段景区当地真的是要啥啥没有,别说监控现场设备的IOT装置了,连专用的网络都没有,只有项目现场用的通讯网络。


    所以说,远程码代码的计划就此落空,现场还有一堆的IOT设备需要现场调试,消息收集和转发的逻辑也需要现场测试,所以没有办法,在今年早春冬装还没有脱去的时候就着急忙慌的去了景区现场。


    在景区写代码是一种什么样的体验?



    这是到某景区酒店下榻的酒店楼下吧,不得不说央企待遇还是拉满的,在景区住着小酒店,每天还拿着补助,冬天还能看看雪,还是非常nice的体验。



    在景区项目现场的工作量实际上是小很多的,每天早上8点半上山(景区入口),晚上4点半和上早班的工作人员一起下山,强度只能说比起互联网的996来说实在太低了。上面这张照片是每天中午遛弯的时候拍的,只能说景区的天就是蓝,现场的领导看起来比普通人年轻15岁,一方水土养一方人吧只能说!而且这才是真的工作不是打工!



    这就是我们工作现场了,透露着重重的的蒸汽朋克风格,为了防止被发现,这里简单打码了。我们现场去调试的IOT设备就是安装在这些机械上面的,还是费了好多功夫。最为痛苦的是跟我们一起到现场的施工同时,他们是真的要去爬这些支架的,而且还是晚上作业,想想就可怕。作为在现场不加班的程序员,甚至让他们产生了一些嫉妒的心态,还产生了一丢丢的不愉快的事情。



    大雪飘飘的景区,这种天气上山就有点儿冷和体验不好了,早上确实是不想上山的。



    没错,冬天可能回冷到又冰溜子,作为一个土生土长的南方人,没咋见过雪,对这种东西还是挺感兴趣的!


    作者:浣熊say
    来源:juejin.cn/post/7304538454875521051
    收起阅读 »

    Android 换种方式实现ViewPager

    一、可行性分析 ViewPager 是一款相对成熟的 Pager 切换 View,能够实现各种优秀的页面效果,也有不少问题,比如频繁会 requestLayout,另外的话如果是加载到 ListView 或者 RecyclerView 非固定头部,会偶现白屏或...
    继续阅读 »

    一、可行性分析


    ViewPager 是一款相对成熟的 Pager 切换 View,能够实现各种优秀的页面效果,也有不少问题,比如频繁会 requestLayout,另外的话如果是加载到 ListView 或者 RecyclerView 非固定头部,会偶现白屏或者 drawble 状态无法更新,还有就是 fragment 数量无法更新,需要重写 FragmentPagerAdapter 才行。


    使用 RecyclerView 相对 ViewPager 来说,会避免很多问题,比如如果是轮播组件 View 可以复用而且会避免白屏问题,当然今天我们使用 RecyclerView 代替 ViewPager 虽然也没有实现复用,但并不影响和 ViewPager 同样的体验。



    二、代码实现


    具体原理是我们在 RecyclerView.Adapter 的如下两个方法中实现 fragment 的 detach 和 attach,这样可以保证 Fragment 的生命周期得到准确执行。


    onViewAttachedToWindow

    onViewDetachedFromWindow

    FragmentPagerAdapter 源码如下(核心代码),另外需要指明的一点是我们使用 PagerSnapHelper 来辅助页面滑动:


    public abstract class FragmentPagerAdapter extends RecyclerView.Adapter<FragmentViewHolder> {

    private static final String TAG = "FragmentPagerAdapter";

    private final FragmentManager mFragmentManager;

    private Fragment mCurrentPrimaryItem = null;
    private PagerSnapHelper snapHelper;

    private RecyclerView.OnScrollListener onScrollListener = new RecyclerView.OnScrollListener() {
    @Override
    public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
    super.onScrollStateChanged(recyclerView, newState);
    if (newState != RecyclerView.SCROLL_STATE_IDLE) return;
    if (snapHelper == null) return;
    View snapView = snapHelper.findSnapView(recyclerView.getLayoutManager());
    if (snapView == null) return;
    FragmentViewHolder holder = (FragmentViewHolder) recyclerView.getChildViewHolder(snapView);
    setPrimaryItem(holder.getHelper().getFragment());

    }

    @Override
    public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    super.onScrolled(recyclerView, dx, dy);
    }
    };

    public FragmentPagerAdapter(FragmentManager fm) {
    this.mFragmentManager = fm;

    }

    @Override
    public FragmentViewHolder onCreateViewHolder(ViewGr0up parent, int position) {
    RecyclerView recyclerView = (RecyclerView) parent;

    if (snapHelper == null) {
    snapHelper = new PagerSnapHelper();
    recyclerView.addOnScrollListener(onScrollListener);
    snapHelper.attachToRecyclerView(recyclerView);
    }

    FragmentHelper host = new FragmentHelper(recyclerView, getItemViewType(position));
    return new FragmentViewHolder(host);
    }

    @Override
    public void onBindViewHolder(FragmentViewHolder holder, int position) {
    holder.getHelper().updateFragment();

    }


    public abstract Fragment getFragment(int viewType);

    @Override
    public abstract int getItemViewType(int position);


    public Fragment instantiateItem(FragmentHelper host, int position, int fragmentType) {

    FragmentTransaction transaction = host.beginTransaction(mFragmentManager);

    final long itemId = getItemId(position);

    String name = makeFragmentName(host.getContainerId(), itemId, fragmentType);
    Fragment fragment = mFragmentManager.findFragmentByTag(name);
    if (fragment != null) {
    if (BuildConfig.DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
    transaction.attach(fragment);
    } else {
    fragment = getFragment(fragmentType);
    if (BuildConfig.DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
    transaction.add(host.getContainerId(), fragment,
    makeFragmentName(host.getContainerId(), itemId, fragmentType));
    }
    if (fragment != mCurrentPrimaryItem) {
    fragment.setMenuVisibility(false);
    fragment.setUserVisibleHint(false);
    }

    return fragment;
    }


    @Override
    public abstract long getItemId(int position);

    @SuppressWarnings("ReferenceEquality")
    public void setPrimaryItem(Fragment fragment) {
    if (fragment != mCurrentPrimaryItem) {
    if (mCurrentPrimaryItem != null) {
    mCurrentPrimaryItem.setMenuVisibility(false);
    mCurrentPrimaryItem.setUserVisibleHint(false);
    }
    if (fragment != null) {
    fragment.setMenuVisibility(true);
    fragment.setUserVisibleHint(true);
    }
    mCurrentPrimaryItem = fragment;
    }
    }

    private static String makeFragmentName(int viewId, long id, int fragmentType) {
    return "android:recyclerview:fragment:" + viewId + ":" + id + ":" + fragmentType;
    }

    @Override
    public void onViewAttachedToWindow(FragmentViewHolder holder) {
    super.onViewAttachedToWindow(holder);
    FragmentHelper host = holder.getHelper();
    Fragment fragment = instantiateItem(holder.getHelper(), holder.getAdapterPosition(), getItemViewType(holder.getAdapterPosition()));
    host.setFragment(fragment);
    host.finishUpdate();
    if (BuildConfig.DEBUG) {
    Log.d("Fragment", holder.getHelper().getFragment().getTag() + " attach");
    }
    }


    @Override
    public void onViewDetachedFromWindow(FragmentViewHolder holder) {
    super.onViewDetachedFromWindow(holder);
    destroyItem(holder.getHelper(), holder.getAdapterPosition());
    holder.getHelper().finishUpdate();

    if (BuildConfig.DEBUG) {
    Log.d("Fragment", holder.getHelper().getFragment().getTag() + " detach");
    }
    }

    public void destroyItem(FragmentHelper host, int position) {
    FragmentTransaction transaction = host.beginTransaction(mFragmentManager);

    if (BuildConfig.DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + host.getFragment()
    + " v=" + ((Fragment) host.getFragment()).getView());
    transaction.detach((Fragment) host.getFragment());
    }

    }

    ViewHolder 源码,本类的主要作用是给 FragmentManager 打桩,其次还有个作用是连接 FragmentHelper(负责 Fragment 的事务)


    public class FragmentViewHolder extends RecyclerView.ViewHolder {

    private FragmentHelper mHelper;

    public FragmentViewHolder(FragmentHelper host) {
    super(host.getFragmentView());
    this.mHelper = host;
    }

    public FragmentHelper getHelper() {
    return mHelper;
    }
    }

    FragmentHelper 源码


    public class FragmentHelper {

    private final int id;
    private final Context context;
    private Fragment fragment;
    private ViewGr0up containerView;
    private FragmentTransaction fragmentTransaction;

    public FragmentHelper(RecyclerView recyclerView, int fragmentType) {
    this.id = recyclerView.getId() + fragmentType + 1;
    // 本id依赖于fragment,因此为防止fragmentManager将RecyclerView视为容器,直接将View加载到RecyclerView中,这种View缺少VewHolder,会出现空指针问题,这里加1
    Activity activity = getRealActivity(recyclerView.getContext());
    this.id = getUniqueFakeId(activity,this.id);

    this.context = recyclerView.getContext();
    this.containerView = buildDefualtContainer(this.context,this.id);
    }

    public FragmentHelper(RecyclerView recyclerView,int layoutId, int fragmentType) {

    this.context = recyclerView.getContext();
    this.containerView = (ViewGr0up) LayoutInflater.from( this.context).inflate(layoutId,recyclerView,false);
    Activity activity = getRealActivity(recyclerView.getContext());
    this.id = getUniqueFakeId(activity,this.id);

    this.containerView.setId(id);
    // 本id依赖于fragment,因此为防止fragmentManager多次复用同一个view,这里加1
    }


    private int getUniqueFakeId(Activity activity, int id) {
    if(activity==null){
    return id;
    }
    int newId = id;
    do{
    View v = activity.findViewById(id);
    if(v!=null){
    newId += 1;
    continue;
    }
    newId = id;
    break;
    }while (true);
    return newId;
    }


    public void setFragment(Fragment fragment) {
    this.fragment = fragment;
    }

    public View getFragmentView() {

    return containerView;
    }

    private static ViewGr0up buildDefualtContainer(Context context,int id) {
    FrameLayout frameLayout = new FrameLayout(context);
    RecyclerView.LayoutParams lp = new RecyclerView.LayoutParams(ViewGr0up.LayoutParams.MATCH_PARENT, ViewGr0up.LayoutParams.MATCH_PARENT);
    frameLayout.setLayoutParams(lp);
    frameLayout.setId(id);
    return frameLayout;
    }

    public int getContainerId() {
    return id;
    }

    public void updateFragment() {

    }

    public Fragment getFragment() {
    return fragment;
    }

    public void finishUpdate() {
    if (fragmentTransaction != null) {
    fragmentTransaction.commitNowAllowingStateLoss();
    fragmentTransaction = null;
    }
    }

    public FragmentTransaction beginTransaction(FragmentManager fragmentManager) {
    if (this.fragmentTransaction == null) {
    this.fragmentTransaction = fragmentManager.beginTransaction();
    }
    return this.fragmentTransaction;
    }
    }

    以上提供了一个非常完美的 FragmentPagerAdapter,来支持 RecyclerView 加载 Fragment


    三、新问题


    在 Fragment 使用 RecyclerView 列表时会出现如下问题


    1、交互不准确,比如垂直滑动会变成 Pager 滑动效果


    2、页面 fling 效果出现闪动


    3、事件冲突,导致滑动不了


    因此为了解决上述问题,进行了一下规避


    public class RecyclerPager extends RecyclerView {

    private final DisplayMetrics mDisplayMetrics;
    private int pageTouchSlop = 0;
    float startX = 0;
    float startY = 0;
    boolean canHorizontalSlide = false;

    public RecyclerPager(Context context) {
    this(context, null);
    }

    public RecyclerPager(Context context, @Nullable AttributeSet attrs) {
    this(context, attrs, 0);
    }

    public RecyclerPager(Context context, @Nullable AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
    pageTouchSlop = ViewConfiguration.get(context).getScaledPagingTouchSlop();
    mDisplayMetrics = getResources().getDisplayMetrics();

    }

    private int captureMoveAction = 0;
    private int captureMoveCounter = 0;

    @Override
    public boolean dispatchTouchEvent(MotionEvent e) {

    switch (e.getAction()) {
    case MotionEvent.ACTION_DOWN:
    startX = e.getX();
    startY = e.getY();
    canHorizontalSlide = false;
    captureMoveCounter = 0;
    Log.w("onTouchEvent_Pager", "down startY=" + startY + ",startX=" + startX);
    break;
    case MotionEvent.ACTION_MOVE:
    float currentX = e.getX();
    float currentY = e.getY();
    float dx = currentX - startX;
    float dy = currentY - startY;

    if (!canHorizontalSlide && Math.abs(dy) > Math.abs(dx)) {
    startX = currentX;
    startY = currentY;
    if (tryCaptureMoveAction(e)) {
    canHorizontalSlide = false;
    return true;
    }
    break;
    }

    if (Math.abs(dx) > pageTouchSlop && canScrollHorizontally((int) -dx)) {
    canHorizontalSlide = true;
    }

    //这里取相反数,滑动方向与滚动方向是相反的

    Log.d("onTouchEvent_Pager", "move dx=" + dx +",dy="+dy+ ",currentX=" + currentX+",currentY="+currentY + ",canHorizontalSlide=" + canHorizontalSlide);
    if (canHorizontalSlide) {
    startX = currentX;
    startY = currentY;

    if (captureMoveAction == MotionEvent.ACTION_MOVE) {
    return super.dispatchTouchEvent(e);

    }
    if (tryCaptureMoveAction(e)) {
    canHorizontalSlide = false;
    return true;
    }

    }
    break;
    }

    return super.dispatchTouchEvent(e);
    }

    /**
    * 尝试捕获事件,防止事件后被父/子View主动捕获后无法改变捕获状态,简单的说就是没有cancel掉事件
    *
    * @param e 当前事件
    * @return 返回ture表示发送了cancel->down事件
    */

    private boolean tryCaptureMoveAction(MotionEvent e) {

    if (captureMoveAction == MotionEvent.ACTION_MOVE) {
    return false;
    }
    captureMoveCounter++;

    if (captureMoveCounter != 2) {
    return false;
    }
    MotionEvent eventDownMask = MotionEvent.obtain(e);
    eventDownMask.setAction(MotionEvent.ACTION_DOWN);
    Log.d("onTouchEvent_Pager", "事件转换");
    super.dispatchTouchEvent(eventDownMask);

    return true;

    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {
    super.onInterceptTouchEvent(e); //该逻辑需要保留,因为recyclerView有自身事件处理
    captureMoveAction = e.getAction();

    switch (e.getActionMasked()) {
    case MotionEvent.ACTION_UP:
    case MotionEvent.ACTION_CANCEL:
    case MotionEvent.ACTION_OUTSIDE:
    canHorizontalSlide = false;//不要拦截该类事件
    break;

    }
    if (canHorizontalSlide) {
    return true;
    }
    return false;
    }

    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, int type) {
    consumed[1] = dy;
    return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type);
    }

    @Override
    public int getMinFlingVelocity() {
    return (int) (super.getMinFlingVelocity() * mDisplayMetrics.density);
    }

    @Override
    public int getMaxFlingVelocity() {
    return (int) (super.getMaxFlingVelocity()* mDisplayMetrics.density);
    }

    @Override
    public boolean fling(int velocityX, int velocityY) {
    velocityX = (int) (velocityX / mDisplayMetrics.scaledDensity);
    return super.fling(velocityX, velocityY);
    }
    }

    四、使用


    创建一个 fragment


        @SuppressLint("ValidFragment")
    public static class TestFragment extends Fragment{

    private final int color;
    private String name;

    private int[] colors = {
    0xffDC143C,
    0xff66CDAA,
    0xffDEB887,
    Color.RED,
    Color.BLACK,
    Color.CYAN,
    Color.GRAY
    };
    public TestFragment(int viewType) {
    this.name = "id#"+viewType;
    this.color = colors[viewType%colors.length];
    }

    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGr0up container, @Nullable Bundle savedInstanceState) {

    View convertView = inflater.inflate(R.layout.test_fragment, container, false);
    TextView textView = convertView.findViewById(R.id.text);
    textView.setText("fagment: "+name);
    convertView.setBackgroundColor(color);

    if(BuildConfig.DEBUG){
    Log.d("Fragment","onCreateView "+name);
    }
    return convertView;

    }


    @Override
    public void onResume() {
    super.onResume();

    if(BuildConfig.DEBUG){
    Log.d("Fragment","onResume");
    }
    }

    @Override
    public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    Log.d("Fragment","setUserVisibleHint"+name);
    }

    @Override
    public void onDestroyView() {
    super.onDestroyView();

    if(BuildConfig.DEBUG){
    Log.d("Fragment","onDestroyView" +name);
    }
    }
    }

    接着我们实现 FragmentPagerAdapter


     public static class MyFragmentPagerAdapter extends FragmentPagerAdapter{

    public MyFragmentPagerAdapter(FragmentManager fm) {
    super(fm);
    }

    @Override
    public Fragment getFragment(int viewType) {
    return new TestFragment(viewType);
    }

    @Override
    public int getItemViewType(int position) {
    return position;
    }

    @Override
    public long getItemId(int position) {
    return position;
    }

    @Override
    public int getItemCount() {
    return 3;
    }
    }

    下面设置 Adapter


     RecyclerView recyclerPagerView = findViewById(R.id.loopviews);
    recyclerPagerView.setLayoutManager(new
    LinearLayoutManager(this,LinearLayoutManager.HORIZONTAL,false));
    recyclerPagerView.setAdapter(new MyFragmentPagerAdapter(getSupportFragmentManager()));

    五、总结


    整个过程轻松而愉快,当然本篇主要学习的是RcyclerView事件冲突的解决,突发奇想然后就写了个轮子,看样子是没什么大问题。


    作者:时光少年
    来源:juejin.cn/post/7307887970664595456
    收起阅读 »

    代码刚上线,页面就白屏了

    web
    前言 白屏一直是一个前端开发谈之变色的问题。 “什么?我的页面刚上线就白屏了,是报错了,还是兼容性问题,还是性能问题,多刷新几次就好了,用户网络不行吧。” 简单来说,白屏就是用户打开前端页面什么有没有。 这是一个很重要的质量指标。 那么我们如何监控页面白屏异常...
    继续阅读 »

    前言


    白屏一直是一个前端开发谈之变色的问题。


    “什么?我的页面刚上线就白屏了,是报错了,还是兼容性问题,还是性能问题,多刷新几次就好了,用户网络不行吧。”


    简单来说,白屏就是用户打开前端页面什么有没有。


    这是一个很重要的质量指标。


    那么我们如何监控页面白屏异常呢?


    白屏异常检测主要分为两个部分,一个是如何检测,一个是什么时候检测,


    检测方案


    首先明确一点,页面打开慢,白屏时间长,不等于白屏;页面就是白色图,不等于白屏。


    关键节点是否渲染


    在当前SPA页面都是挂在根节点之下,通过查看关键dom是否渲染,如查看dom的高度heigt属性是否存在,如果存在,则证明关键dom已经渲染,页面不是白屏,反之,则判断页面是白屏


    实现思路


    在上面的代码中,我们首先使用querySelectorAll方法选中了具有 .critical-node类名的关键节点。然后,通过checkNodesRendered函数检测这些节点是否已经渲染,如果有任何一个节点的高度为0,即判断为未渲染,将返回false。最后,在页面加载完成后调用checkNodesRendered函数来判断页面状态。


    // 获取关键节点
    const criticalNodes = Array.from(document.querySelectorAll('.critical-node'));

    // 检测节点渲染
    function checkNodesRendered() {
    let allNodesRendered = true;

    for (const node of criticalNodes) {
    if (node.offsetHeight === 0 || node.clientHeight === 0) {
    allNodesRendered = false;
    break;
    }
    }

    return allNodesRendered;
    }

    // 判断页面状态
    if (checkNodesRendered()) {
    // 关键节点已经渲染,页面不是白屏
    console.log("页面不是白屏");
    // 可以进行后续操作
    } else {
    // 关键节点未渲染,页面是白屏
    console.log("页面是白屏");
    // 可以进行相应处理
    }

    // 在页面加载完成后调用检测函数
    window.addEventListener('load', checkNodesRendered);

    优点


    1.简单易懂:代码相对简洁,易于理解和实现。


    2.快速检测:代码通过检测关键节点的渲染状态来快速判断页面是否为白屏,方便进行后续处理。


    3.可扩展性:示例代码可以根据实际需求进行修改和扩展,例如添加其他检测条件或特定行为。


    缺点



    1. 局限性:示例代码仅仅关注关键节点是否渲染,但并不能涵盖所有可能的页面白屏情况。

    2. 不适用于异步加载:如果页面中的关键节点是通过异步加载或延迟加载的方式渲染的,示例代码可能无法正确判断页面状态。

    3. 可能的误判:某些情况下,即使关键节点已经渲染,它们的高度可能仍为0。这可能导致误判,将页面错误地视为白屏。


    观察FP/FCP


    PerformanceObserver观察FP/FCP指标,出现该指标判断为非白屏


    代码实现


    const observer = new PerformanceObserver((list) => {
    const entries = list.getEntries();
    entries.forEach(entry => {
    if (entry.name === 'first-paint') {
    // 处理FP指标
    console.log('First Paint:', entry.startTime);
    // 进行白屏判断
    if (/* 根据需求判断是否为白屏 */) {
    console.log('白屏!');
    }
    } else if (entry.name === 'first-contentful-paint') {
    // 处理FCP指标
    console.log('First Contentful Paint:', entry.startTime);
    // 进行白屏判断
    if (/* 根据需求判断是否为白屏 */) {
    console.log('白屏!');
    }
    }
    });
    });

    observer.observe({ entryTypes: ['paint'] });

    优点



    1. 通过观察FP和FCP指标,可以精确地确定页面加载过程中是否出现白屏,以及白屏持续的时间。这对于优化网页加载速度和用户体验非常有帮助。

    2. PerformanceObserver提供了一个直接的、标准化的接口来监测性能指标,使开发者能够更方便地收集和分析网页性能数据。


    缺点



    1. 依赖于浏览器对PerformanceObserver的支持,在某些旧版本或不常见的浏览器中可能无法正常工作。

    2. 只通过FP和FCP来判断白屏可能不够全面,因为白屏可能涉及其他因素,如网络延迟、脚本执行等。因此,需要结合其他性能指标和实际场景来综合评估页面的加载情况。


    基于视窗坐标采集法


    基于视窗坐标采集元素,如果所有元素是包裹元素,则判断是白屏


    1.页面中间取17个采样点(如下图),利用 elementsFromPoint api 获取该坐标点下的 HTML 元素


    2、定义属于容器元素的集合,如 ['html', 'body', '#app', '#root']


    3、判断17这个采样点是否在该容器集合中。说白了,就是判断采样点有没有内容;如果没有内容,该点的 dom 元素还是容器元素,若17个采样点都没有内容则算作白屏



    代码实现


    const samplePoints = [
    { x: 100, y: 100 }, // 示例采样点1
    { x: 200, y: 200 }, // 示例采样点2
    // 添加更多采样点...
    ];

    const containerElements = ['html', 'body', '#app', '#root']; // 定义容器元素集合

    function hasContentAtSamplePoints() {
    for (const point of samplePoints) {
    const elements = document.elementsFromPoint(point.x, point.y);
    const hasContent = elements.some(element => !isContainerElement(element));
    if (!hasContent) {
    return false; // 该采样点没有内容
    }
    }
    return true; // 所有采样点都有内容
    }

    function isContainerElement(element) {
    // 判断元素是否属于容器元素集合的逻辑,例如根据元素的标签名或选择器进行判断
    return containerElements.includes(element.tagName.toLowerCase()) || containerElements.includes(element.id);
    }

    // 调用函数判断是否存在白屏状态
    const isWhiteScreen = !hasContentAtSamplePoints();

    if (isWhiteScreen) {
    console.log('白屏状态');
    } else {
    console.log('非白屏状态');
    }

    优点



    1. 快速确定白屏状态:通过采样点的方式,可以快速检查页面中是否存在白屏状态,而无需遍历整个页面。

    2. 简单实现:实现起来相对简单,只需要使用 elementsFromPoint API 获取元素并进行判断。


    缺点



    1. 采样点数量和位置选择:在示例中,我们选择了固定数量和位置的采样点,但这可能并不能涵盖所有情况。正确选择采样点的数量和位置是必要的,以保证准确性和可靠性。

    2. 容器元素定义的准确性:需要准确定义容器元素集合,以确保正确判断哪些元素属于容器元素。容器元素集合的定义可能会因页面结构变化而需要定期更新维护。

    3. 采样点是否具有代表性:通过采样点判断白屏状态,需要确保采样点能够代表页面的关键区域和内容。如果关键区域未覆盖到,或者采样点无法代表页面的典型情况,可能会导致误判。


    图像检测


    基于图像像素色值对比方案,白色大于阈值判断为白屏


    代码实现


    function isWhiteScreen(imageData) {
    const threshold = 200;
    const pixels = imageData.data;
    const length = pixels.length;

    for (let i = 0; i < length; i += 4) {
    const red = pixels[i];
    const green = pixels[i + 1];
    const blue = pixels[i + 2];
    const alpha = pixels[i + 3];

    // 将 RGB 转换为灰度值
    const grayscale = (red + green + blue) / 3;

    // 如果灰度值低于阈值,则返回 false
    if (grayscale < threshold) {
    return false;
    }
    }

    // 如果所有像素的灰度值都高于阈值,则返回 true
    return true;
    }

    // 获取页面截图,可以通过其他方式获取 imageData
    const imageData = ...;

    // 调用函数判断页面是否为白屏
    const isWhite = isWhiteScreen(imageData);

    if (isWhite) {
    console.log("页面出现白屏");
    } else {
    console.log("页面正常");
    }

    优点



    1. 具有广泛适用性:该方法可以适用于各种类型的网页和应用程序,不受页面结构和布局的限制。

    2. 准确性较高:通过对页面截图进行像素色值对比,可以较为准确地判断页面是否呈现白色,避免了部分误判的可能性。


    缺点



    1. 截图准确性:该方法的准确性依赖于页面截图的质量和准确性。如果截图质量较低或者不准确,则可能导致判断结果不准确。

    2. 阈值选择:选择合适的阈值是关键。过高的阈值可能导致漏判,而过低的阈值可能导致误判。阈值的选择应该根据具体情况和实际测试进行调整。

    3. 页面动态性:对于动态页面或存在异步加载内容的页面,截图时可能无法捕获到完全加载的状态,从而导致判断结果不准确。

    4. 效率问题:对整个页面进行截图并处理像素色值对比可能会消耗较多的计算资源和时间,特别是对于复杂的页面或者移动端设备


    检测时机


    其实检测方案并不难,难的是什么时候检测。


    这里介绍三种方案。


    延迟检测


    通过设定延迟时间(如5s),在页面加载后的5s后开始检测


    代码实现


    // 设置延迟时间(单位:毫秒)
    const delay = 5000;

    // 在延迟时间后执行检测
    setTimeout(() => {
    // 在这里编写检测的代码,例如调用 isWhiteScreen() 函数进行白屏检测
    // 调用函数判断页面是否为白屏
    const isWhite = isWhiteScreen();

    if (isWhite) {
    console.log("页面在加载后的5秒后出现白屏");
    } else {
    console.log("页面正常");
    }
    }, delay);

    缺点



    1. 固定延迟时间:使用固定的延迟时间可能不适用于所有情况。页面加载时间的变化、网络速度的差异等因素可能导致延迟时间不准确,有可能延迟过长或过短。

    2. 不适用于快速加载的页面:如果您的页面加载速度很快,在延迟时间之内已经完成加载并呈现内容,延迟检测可能会错过白屏状态。

    3. 无法应对动态内容:如果页面内容是动态加载的,延迟检测可能在页面加载完成后立即触发,此时页面尚未呈现完全。


    轮询检测


    既然延迟检测时间不好定,那我们就去每秒都轮询页面,判断是否白屏。


    代码实现


    // 设置轮询时间间隔(毫秒)
    const pollInterval = 1000;

    // 启动轮询检测
    function startPolling() {
    // 设置一个定期执行的定时器
    setInterval(isWhiteScreen, pollInterval);
    }

    // 页面加载完成后开始轮询检测
    window.addEventListener('load', startPolling);

    缺点



    1. 资源消耗:频繁的轮询检测可能会增加浏览器的资源消耗,包括 CPU 和内存。这可能对性能产生一定的影响,特别是在较低性能的设备或者页面加载较慢的情况下。

    2. 不准确性:轮询检测往往基于时间间隔来判断页面加载状态,而不是依赖于实际的视觉变化。这可能导致在某些情况下误判页面加载完成,或者延迟较长时间才判断出白屏状态。

    3. 反应迟钝:由于轮询需要等待一定的时间间隔才能进行下一次检测,因此可能会导致对白屏状态的响应有一定的延迟。这对于需要快速捕捉白屏问题的场景可能不太理想。


    错误监听


    这是一种由果索因的方案


    发生白屏的原因无非以下几种



    1. 脚本错误:当页面中的 JavaScript 代码存在错误时,可能导致页面渲染中断,进而出现白屏情况。常见的错误包括语法错误、逻辑错误、资源加载错误等。

    2. 网络问题:如果页面所需的资源(如样式表、脚本、图片等)无法正确加载,或者网络连接不稳定,可能导致页面无法正确渲染,最终呈现为白屏。这种情况下,可能还会出现超时错误或网络请求失败的错误。

    3. HTML结构错误

    4. 样式问题

    5. 见兼容性问题。


    其中前两个原因占绝大多数,那么我们去监听以上错误,做白屏处理就好了。


    优点:



    1. 简单易实现:通过监听错误事件,可以比较简单地实现白屏检测逻辑。

    2. 可靠性较高:当页面发生未捕获的错误时,通常表明页面加载或解析出现了问题,可能导致白屏情况。


    缺点:



    1. 性能开销:错误处理函数可能会对页面性能产生一定的影响,尤其是在页面发生多个错误时。因此,需要注意错误处理逻辑的优化,避免性能问题。


    总结


    没有最完美的方案,只有最合适的方案。


    白屏方案的检测无非就是检测时机+判断方案做排列组合,找到那个投入产出比最合适的方案。


    作者:虎妞先生
    来源:juejin.cn/post/7302367564838567962
    收起阅读 »

    聊一聊自己的前端之路以及后面晋升的一些想法

     jym大家好,闲来无事写下从事前端这么多年的一些感想和心德方面的分享,大概也从事前端也差不多8年的时间了,从大火的jquery时代到vue、react的时代,差不多也见证了前端这个行业从大火到逐渐趋于稳定的一个时间段了,这之间也经历了很多从刚开始啥都不会到后...
    继续阅读 »

     jym大家好,闲来无事写下从事前端这么多年的一些感想和心德方面的分享,大概也从事前端也差不多8年的时间了,从大火的jquery时代到vue、react的时代,差不多也见证了前端这个行业从大火到逐渐趋于稳定的一个时间段了,这之间也经历了很多从刚开始啥都不会到后面一步一步慢慢了解的这么一个过程,今天主要就是想分享以及谈一谈从中遇到的一些问题及一些经验分享主要是包括个人走向及未来职业规划方向。


    相信各位最近两年也有这么个感觉这行感觉是越来越难了,从跳槽的同学以及要入行的同学的反应来都是说很难,焦虑感爆棚。其中有部分人在这一过程中就会迷茫主要有以下几种:


    1.骑驴找马型: 大家都在跳我要不要也找下工作机会但是都听说不好找。


    2.忐忑不安型:在一家公司呆了快3年左右了到了一个瓶颈期了基本上不上不下,技术没有什么好的扩展方向和发展久了不知道自己下个目标该做啥了,漫无目地。


    3.摆烂型:这种也是差不多在一家公司呆差不多2年左右各种业务都比较熟悉了,刚开始可能很有激情时间久了之后发现也就那样,特别发现上层领导有些技术 方向和自己想的方向不是很搭的于是就上面怎么说就怎么做的这种久而久之就慢慢摆烂了


    4.年龄焦虑型:这种可能就有点适合我了年龄大概在31以后的人,会 在思考自己后面的职业发展方向了,因为目前来看前端目前你想要只搞前端最大的职业方向可能就是前端主管了,不过这一步都比较困难很多公司没有这一职位基本上是后端大佬一并代替,可能很多都是小组长就到了尽头,这时就不得不思考自己未来的路了,是继续 在这行深耕到达顶峰(前端主管)混个管理岗,还是默默退场做点其它啥比如摆个地摊、开个超市、卖点卤肉之类的


    。。。。。以及其他种种想法有其它的jy们可以在下方讨论,我们针对以上几点具体展开说说,都是我个人的想法jy们可以在下面自由讨论。


    骑驴找马型:相比裸辞的小伙伴不一样这类人群更稳,一般不会轻易冒险,会试探行业风险,看看外面情况如何,这类型的小伙伴一般都是对公司有一定的想法的了无非就是公司的管理方面、技术方向发展还有最重要的就是钱没给够,要么不涨薪要么达不到自己的预期这种不用多说其实很正常这一类型的小伙伴没什么说的,这个比较正常一般都是刚入行几年内的人群


    忐忑不安型:这类型的同学其实就是失去了发展方向了,想要研究技术又苦于没有实战场景,看几天就坚持不下去了。想要优化公司自己目前自己负责的业务也不知道从那儿下手,上面好像也没有说啥优化了好像对自己也没啥好处公司也不会给自己啥奖励之类的就放弃了,要么就有时突然心血来潮搞个东西但是没有理论及数据支撑放弃要么就是代价太大久而久之就慢慢迷失了方向自爆自弃,其实这类人群只要找对了定位和方向对自己发展有很大优势。


    摆烂型:这类型小伙伴就不多探讨可能就是想找份工作混下去,或者是迷失了自我,或者是对自己公司丧失了信心,也有可能有其它方面的种种不满


    年龄焦虑型:这个其实是今天着想着重说的一种其实就是对应的我自己写这篇文章应该算不上文章就是个日记或者 分享吧就是探讨下我这类人群的现状。


    目前高不成低不就,团队小组长,不过我刚进入这个公司一年就成为了小组长也只是个挂名工资也没有实质性的上涨。目前就是想冲击下前端主管这个位置我总结一下想要冲击管理岗的经验


    任何事情要主动去找来做这一点可能大家都懂,但是很多人有各种原因不想去做无非就是觉得吃力不讨好,对自己也没有多大实质性的帮助有可能做得多后面就成了你自己的事了其实这个也要看你根据这个公司自己判断有些公司可能就主打一个白嫖习惯了就觉得你理所当然,但是其实这中间有一个很大的技巧以下展开说说


    第一点:事情做了你不能做得太快有些小伙伴解决事情 和做事情的能力很快,一个功能可能预估的工作量是2周左右结果你3-4天就做出来了并且报告 了给你的上级然后你可能会觉得这是你个人能力的体现殊不知有时候 可能适得其反,可能上级觉得这个功能太简单了久而久之就会慢慢缩短功能的评估时间,我就是吃了这样的亏正确的做法是你就按照评估的时候来做然后做好了之后在回过头来看一下慢慢检查到交项目的前一两天在交预测试即可,这样才是正确的做法。


    第二点:就是做了事情得会邀功,什么是邀功,其实之前我也不懂这个玩意儿这其中也很有学问,一个职场老油条后来告诉我的。相信大多数人做完功能都只是给自己上级说一声或者 直接不说就交给测试测了对吧,其实这一点做得不是很对,正确的做法是你要在其实找几个点在大群里说共同讨论比如后续可能会遇到什么问题之类的你是什么样的想法这样做能解决什么样的问题。或者对性能有优化之类的,切记你刚好想到这点了然后自己默默的把他做了还不说,这样的做法是很蠢的因为其实这个东西有价值 但是你觉得顺便就把他优化了是正常的,这样的做法是错误的大错特错。一般公司都会有钉钉群技术群之类的,总群内的人很多一般项目经理技术主管或者老板都会在里面你把你的问题抛出来并给也解决方法探讨多了久而久之相应的问题都会找你讨论自然的你在老板或者你的上一级心目中的位置就无形的提高了,但是切记只可讨论你觉得有价值的技术性问题不要什么 都抛不然会适得其反。还有就是比如特别在群里@你的事情解决之后切记不能私聊一定在大群说让大家都看到并且回复对应的人已解决其实工作中有很多小伙伴习惯私聊不习惯在大群说这些问题这是一种很不好的习惯切记一定要群聊!一定要群聊!一定要群聊!重要的事情说三遍


    第三点:这一点其实也很重要就是你得会理解业务,一般我们搞前端的可能不会去太多关心具体业务是怎么实现之类的只会把自己负责 的模块做好即可,其实你如果只安于做一个普通开发这个没有啥问题如果你想更近一步那么理解业务那是必须的试想一下你某天做上了管理别人问你一些业务的东西你不可能说不知道吧,所以这一点也是需要具备的,你不一定得全部去知道但是整个系统的业务流程你是必定要清楚


    第四点:想要晋升管理那么文档这东西肯定就必不可少了,各种ppt,文档能力你是必须要具备,设计到技术知识文档,新人文档、这些肯定都是要做的还有各种流程图都需要熟悉。


    第五点:得会安排人什么事情不是都需要你自己亲力亲为但是安排人也很有学问,怎么说呢就是得会圆滑,这儿举个例子我们之前公司有一个cto新来的应该是可能刚从技术转管理安排人就是叫xxx人做啥直接就说并且叫好久之前必须完成随便都是以命令式的口吻,其实这点对我们这样的技术人来说是大忌,都是干技术的我需要你教我怎么做?或者是听你那个口气就很不爽,其实可能委婉点不能用命令式的口吻,而应该是询问式的口吻。比如不要用你怎么怎么样要在好久给我实现实现不了就加班正确的是这个功能能做吗?有没有什么问题?大概什么时间呢你预估一下。这样就很舒服了,结果后面没多久就被干掉了。。对比之下我们目前这个项目经理这一块不要太老油条,法克。


    以上几点其实你如果达到了不需要你技术有很牛。你只要会安排人懂人心其实就比较稳了,当然技术不能丢只是说你不一定非得是技术大牛(感觉适用于中小型公司)不过我觉得大公司应该也适用技术牛的人有他的发挥价值。


    以上几点总结一下想要往上走就记住这几个关键字:参于感、邀功(特别重要)、人性化。


    好了今天先写到这儿,与大家共勉。大家可以下面一起讨论(要干活儿了就不排版了。。。)


    作者:浪里个浪里小白龙
    来源:juejin.cn/post/7308553288398995482
    收起阅读 »

    手搓微信小程序生日滑动选择😉

    web
    简单说一下功能点 微信小程序设置用户的生日,直接使用日历有些不太友好,所以选择手搓一个类似某音和某红书差不多的样式。 在实现该功能还是有一些小的注意点的,最主要的就是-->日期选择需要3级联动,因为要获取当前年份有多少月份、以及当前年份的月份有多少天。总...
    继续阅读 »

    简单说一下功能点


    微信小程序设置用户的生日,直接使用日历有些不太友好,所以选择手搓一个类似某音和某红书差不多的样式。


    在实现该功能还是有一些小的注意点的,最主要的就是-->日期选择需要3级联动,因为要获取当前年份有多少月份、以及当前年份的月份有多少天。总不能今天是2023年12月3号,但滑动选项里面有明天甚至以后的日期吧。


    使用的是VantWeapp组件实现的滑动效果,当然,使用其他组件的一样,结尾附源代码。


    功能样式图


    日期选择默认的打开样式


    image.png


    在选择最新日期时候


    image.png


    除了选择天数不会去重新拉取日期外,当滑动触发年和月的改变,都需要去拉取最新的日期。若拉取的日期的天数或月份不够上一次选择的时候,默认会选择最后一个日期等等小细节吧。


    主要代码功能


    自己封装的一个时间工具


    /**
    * 获取有多少年份[默认截至1949]
    * @param actYear 截至到多少年份
    * @returns 年份数组
    */

    export const getYear = (actYear?: number): Array<number> => {
    actYear = actYear || 1949;
    const date = new Date();
    if (actYear >= date.getFullYear()) return [1949];
    let yearArr = [];
    for (let i = actYear; i <= date.getFullYear(); i++) yearArr.push(i);
    return yearArr;
    };

    /**
    * 获取当前年份有多少月份
    * @param year 年份
    * @returns 月份数组
    */

    export const getMonthToYear = (year: number): Array<number> => {
    const date = new Date();
    const nowYear = date.getFullYear();
    if (year > nowYear) return [1];
    let monthArr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
    if (year == nowYear) {
    monthArr = [];
    for (let i = 1; i <= date.getMonth() + 1; i++) monthArr.push(i);
    }
    return monthArr;
    };

    /**
    * 获取当前年的月份有多少天
    * @param year 年份
    * @param month 月份
    * @returns 天数数组
    */

    export const getDayToMoYe = (year: number, month: number): Array<number> => {
    const date = new Date();
    const nowYear = date.getFullYear();
    const nowMonth = date.getMonth() + 1;
    if (year > nowYear) return [1];
    let monthArr = getMonthToYear(year);
    if (month > monthArr.length) return [1];
    let dayArr = [];
    if (year == nowYear && month == nowMonth) {
    for (let i = 1; i <= date.getDate(); i++) dayArr.push(i);
    return dayArr;
    }
    for (let i = 1; i <= new Date(year, month, 0).getDate(); i++) dayArr.push(i);
    return dayArr;
    };

    组件代码


    特别说明:手动删掉了不重要的代码,请勿直接复制


    <template>
    <van-popup
    class="pd-10"
    :show="showDateChoose"
    round
    position="bottom"
    @close="showDateChoose = false">

    <view class="mt-10 flex-center-zy pd-zy-15">
    <view class="ft-color-hui" @click="showDateChoose = false">取消</view>
    <view>选择你的生日</view>
    <view class="ft-big-4 ft-color-red" @click="saveDate">保存</view>
    </view>
    <van-picker
    :columns="initDate"
    @change="onDateChange"
    :visible-item-count="3"
    :loading="dateLoding" />

    </van-popup>
    </template>

    <script setup lang="ts">
    import { ref, onMounted, reactive } from 'vue';
    import FixVue from '@/common/pages/fix_vue/FixVue';
    import { getYear, getMonthToYear, getDayToMoYe } from '@/utils/TimeUtil';

    //展示日期选择框和日期加载
    let showDateChoose = ref(false);
    let dateLoding = ref(true);

    //打开日期选择
    const openDate = () => {
    dateLoding.value = true;
    showDateChoose.value = true;
    //测试数据,后需要修改为动态获取的用户生日,若用户生日没有则给默认值
    initDateMethod('2001-5-10');
    dateLoding.value = false;
    };

    //保存日期
    let newDate = ref('');
    const saveDate = () => {
    if (!newDate.value) return;
    //与原本日期进行对比若不同,调用修改生日的接口。。。
    };

    //选择新时间
    const onDateChange = (e: any) => {
    const { picker, index } = e.detail;
    if (index == 2) return (newDate.value = picker.getValues());
    const upDate = picker.getIndexes();
    const year = initDate.value[0].values[upDate[0]];
    const month = initDate.value[1].values[upDate[1]];
    const day = initDate.value[2].values[upDate[2]];
    initDate.value = [];
    const result = initDateMethod(year + '-' + month + '-' + day);
    newDate.value = picker.getValues();
    setTimeout(() => {
    picker.setColumnIndex(0, result[0]);
    picker.setColumnIndex(1, result[1]);
    picker.setColumnIndex(2, result[2]);
    }, 10);
    };

    //初始化年份
    let initDate = ref([]);
    const initDateMethod = (date: string) => {
    let dateSplit = date.split('-');
    const year = getYear();
    const month = getMonthToYear(+dateSplit[0]);
    const day = getDayToMoYe(+dateSplit[0], +dateSplit[1]);
    let yearIndex = year.indexOf(+dateSplit[0]) == -1 ? year.length - 1 : year.indexOf(+dateSplit[0]);
    initDate.value.push({
    values: year,
    defaultIndex: yearIndex
    });
    let monthIndex =
    month.indexOf(+dateSplit[1]) == -1 ? month.length - 1 : month.indexOf(+dateSplit[1]);
    initDate.value.push({
    values: month,
    defaultIndex: monthIndex
    });
    let dayIndex = day.indexOf(+dateSplit[2]) == -1 ? day.length - 1 : day.indexOf(+dateSplit[2]);
    initDate.value.push({
    values: day,
    defaultIndex: dayIndex
    });
    return [yearIndex, monthIndex, dayIndex];
    };
    </script>

    //主要的css样式,主要添加日期后面的一些提示字,如年、月、日
    <style lang="less" scoped>
    ::v-deep.van-picker-column__item--selected {
    color: black;
    }
    ::v-deep[data-index='0'] {
    .van-picker-column__item--selected::after {
    content: ' 年';
    }
    }
    ::v-deep[data-index='1'] {
    .van-picker-column__item--selected::after {
    content: ' 月';
    }
    }
    ::v-deep[data-index='2'] {
    .van-picker-column__item--selected::after {
    content: ' 日';
    }
    }
    ::v-deep.van-picker {
    height: 150px !important;
    margin-top: 20px;
    }
    </style>


    结束语


    至此功能就完结了,接下来已编写完成仿某信的聊天样式,如自动根据输入框弹起高度修改聊天内容触底,以及动态调整输入框的高度和最大限制等等。若有需求的小伙伴,可以聊聊,我会分享个人的想法以及做法,若需求大会写一篇文章以及源码分享。


    作者:你浩
    来源:juejin.cn/post/7307587537295851535
    收起阅读 »

    鸿蒙 Ark ui 视频播放组件 我不允许你不会

    前言: 各位同学有段时间没有见面 因为一直很忙所以就没有去更新博客。最近有在学习这个鸿蒙的ark ui开发 因为鸿蒙不是发布了一个鸿蒙next的测试版本 明年会启动纯血鸿蒙应用 所以我就想提前给大家写一些博客文章 概述 在手机、平板或是智慧屏这些终端设备上,媒...
    继续阅读 »

    前言:


    各位同学有段时间没有见面 因为一直很忙所以就没有去更新博客。最近有在学习这个鸿蒙的ark ui开发 因为鸿蒙不是发布了一个鸿蒙next的测试版本 明年会启动纯血鸿蒙应用 所以我就想提前给大家写一些博客文章


    概述


    在手机、平板或是智慧屏这些终端设备上,媒体功能可以算作是我们最常用的场景之一。无论是实现音频的播放、录制、采集,还是视频的播放、切换、循环,亦或是相机的预览、拍照等功能,媒体组件都是必不可少的。以视频功能为例,在应用开发过程中,我们需要通过ArkUI提供的Video组件为应用增加基础的视频播放功能。借助Video组件,我们可以实现视频的播放功能并控制其播放状态。常见的视频播放场景包括观看网络上的较为流行的短视频,也包括查看我们存储在本地的视频内容


    效果图


    image.png


    image.png


    具体实现:




    • 1 添加网络权限




    在module.json5 里面添加网络访问权限


    "requestPermissions": [
    {
    "name": "ohos.permission.INTERNET"
    }
    ]

    image.png
    如果你是播放本地视频那么可以不添加这个 为了严谨我这边就提一下


    我们要播放视频需要用到 video 组件


    video 组件里面参数说明


    参数名参数类型必填
    srcstringResource
    currentProgressRatenumberstringPlaybackSpeed8+
    previewUristringPixelMap8+Resource
    controllerVideoController
    其他属性说明 :
    .muted(false) //是否静音。默认值:false
    .controls(true)//不显示控制栏
    .autoPlay(false) // 手动点击播放
    .loop(false) // 关闭循环播放
    .objectFit(ImageFit.Cover) //设置视频显示模式。默认值:Cover

    具体代码


    @Entry
    @Component
    struct Index {


    @Styles
    customMargin() {
    .margin({ left: 20, right: 20 })
    }

    @State message: string = 'Hello World'
    private controller: VideoController = new VideoController();
    build() {
    Row() {
    Column() {
    Video({
    src: $rawfile('video1.mp4'),
    previewUri: $r('app.media.image3'),
    controller: this.controller
    })
    .muted(false) //是否静音。默认值:false
    .controls(true)//不显示控制栏
    .autoPlay(false) // 手动点击播放
    .loop(false) // 关闭循环播放
    .objectFit(ImageFit.Cover) //设置视频显示模式。默认值:Cover
    .customMargin()// 样式
    .height(200) // 高度
    }
    .width('100%')
    }
    .height('100%')
    }
    }

    最后总结


    鸿蒙的视频播放和安卓还有iOS .里面差不多都有现成的组件使用, 但是底层还是有ffmpeg 的支持。 我们作为上层开发者只需要熟练掌握api使用即可做出来 一个实用的播放器 app, 还有很多细节 由于篇幅有限我就展开讲了 我们下一期再见 最后呢 希望我都文章能帮助到各位同学工作和学习 如果你觉得文章还不错麻烦给我三连 关注点赞和转发 谢谢


    作者:坚果派_xq9527
    来源:juejin.cn/post/7308620787329105971
    收起阅读 »

    IT外传:下班路上

    正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。 老郑是一家IT公司的一线程序员,他在这个公司干了两年多了。 这一天晚上下班,老郑回家走到半路,收到一个电话,老郑用蓝牙耳机接通。 “走了吗?过来开一个紧急调度会!” 汽车的轰鸣声,伴随着行...
    继续阅读 »

    正式声明:以下内容完全为道听途说,肆意杜撰。请勿对号入座,自寻烦恼。



    老郑是一家IT公司的一线程序员,他在这个公司干了两年多了。


    这一天晚上下班,老郑回家走到半路,收到一个电话,老郑用蓝牙耳机接通。


    “走了吗?过来开一个紧急调度会!”


    汽车的轰鸣声,伴随着行人的嘈杂,老郑听得很艰难,心想这是谁啊:“哪位?”


    对方一愣,似乎诧异连我都不知道:“我是刘程啊,王总要开一个紧急调度会,你来会议室一趟吧”


    老郑说:“我回家,在路上了!”


    刘程说:“我知道,你能回来一趟吗?王总要开会”


    老郑说:“我今天有事情啊,回不去了”


    刘程说,那我看看,怎么跟王总说一下你来不来了。


    老郑知道刘程,他是王总的技术助理,也是一个技术部门的总监,但是看了看电话,好像没有存过他的号码。


    老郑看了看电话,摇了摇头,想起了这个公司的产品经理。


    老郑待过很多互联网公司,大多数公司都是开发人员的综合素质强于产品经理。好像唯独这家公司,产品经理的各方面都比技术强。


    从沟通方面,产品经理每次给老郑打电话,第一句话都是:“hello,我是李四……”。即便老郑早就把他的号码存成联系人了。


    从技术方面,是的,这里的产品经理比开发还懂技术。很多功能,开发说实现不了,都是产品经理帮忙找代码片段或者开源项目的GitHub地址,而且还都是可用的。从规则讨论上,产品经理一听就懂、一点就透,开发人员转半圈也不知道为什么会这样。


    但是有一点,技术领导很享受当领导的感觉。比如,刘程会觉得全公司都得具备能听出他声音的能力,最起码你得能分辨出电话的那头是他。


    这一点,老郑今天还不是第一次遇到。下午的时候,另一个技术领导风风火火地赶到老郑的工位,说给老郑打电话没接,有紧急的事情找他,于是他只能亲自过来了。问完事情,老郑发现手机上有一个被拦截的外地陌生号码。这个号码是第一次打,老郑查了查这也不是企业钉钉通讯录里面的号码。


    “开调度会……调度会,王总”,老郑上一次参加过一个王总主持的调度会。


    王总在会上说,他想不明白,每次做一个项目,自己问产品,产品说没问题;问开发,开发也说没问题;问测试,测试也说OK;问运维,运维也说可控。但是,一上线就问题百出。这次的项目,一定不要出问题。有什么疑问、难点,现在就抛出来,能给你解决的就解决,不能解决的会解释,后面再出问题,决不轻饶。


    老郑感觉,这个问题不应该是王总想不明白的,反而是王总需要解决的。这就如同一个省长说,问你们哪个市,你们都说很好,但是为什么我们省的经济、政治、文化就是很落后呢?你们几个市长都是责任人,一定要提高整个省的综合竞争力。


    当时,还真有个技术小组长抛出一个问题。


    小组长说:“王总,我们这边整个系统在重构,可能会有风险。”


    王总问:“重构?什么时候开始的?谁批准的?”


    小组长说:“这个我不清楚,我调过来当小组长时候就已经开始了”


    王总问:“你是什么时候调过来?”


    小组长说:“就是上次,上次公司整体组织架构调整”


    “整体组织架构调整多了,你说的是哪次?”


    “就……就最近一次”


    王总感觉这个小组长废话太多了:“什么叫最近一次!几月几号?”


    小组长的脸涨得通红,心理防线崩溃了:“具体日期我记不清了……”


    整个调度会大约一个小时,传达出一个思想:不要出问题。


    老郑心想,幸好今天确实有事,就算去,估计也是开类似的会。


    这个公司的会,90%的时间都是浪费的。即便老郑不是重要岗位,今天他也开了几个。


    上午的一个会,开了90分钟。基本跟老郑无关,他只是被直接领导安排过去听的。因为有可能会问到他问题。其实90分钟的时间,有20分钟在等上一个会议结束,有30分钟在看着发起人一个字一个字在敲会议共识总结。不能回去再敲,得会上现写,确保每一个字大家都认可,不然就不叫共识了。


    下午一个会,开了60分钟。起因是客户反馈过来一些问题,大家一起去分析下。会议发起人也是第一次看到这个问题,他现场打开问题,现场下载资料,这叫第一手新鲜资料,证明他没有加工过,就是客户反馈的。不过,大家都在等着……大约等了30分钟的时候,资料还没有下载完,有人实在等不及了,说下载好了再开吧。会议发起人说不行,大家来都来了,会议得有结论才行,大家都走了会议纪要不能写大家中途退场。最后,50分钟的时候,资料下载完了,结果显示文件被损坏,无法解压缩。


    老郑回到工位上坐下,又有另一个技术领导在钉钉群里找他。群里技术领导@老郑,让老郑自己给自己新建一个工作任务,建完了告诉他一下,他要知道老郑的工作安排。


    虽然老郑觉得任务应该是上级给新建,因为原来公司多是这样操作,有利于上级调度工作。但是,这个公司都是自己给自己建。因此,老郑早上就把这个任务建完了,而且平台操作记录有显示这个领导已阅读过。


    老郑就单独跟这个领导发消息,说自己早上已经建完任务了,并且把任务截图也给他了。


    这个领导回复说:“我从群里问的问题,你得从群里回复”


    其实,他早上自己看过了,现在又私聊告诉他任务建好了,这件事情他是知道的。因此,疑惑的老郑问为什么非要在群里回复?领导咔咔一通讲,有理有据,有软有硬,高谈阔论,又是共识又是示范,此时的老郑无心辩解,因为写代码的任务还没开始,而领导是不用写代码的。


    于是,老郑无奈又去群里@领导,说我的已经建完了。领导又@老郑,说好的,我知道了。老郑在家里时,经常和女儿一起玩这种过家家的游戏。


    好累,老郑一抬头看时间,快下班了。


    赶紧写钉钉日报,日报是每天要写的,要写今天干了啥,大任务是什么,小任务是什么,用了几个小时,干到了什么程度。


    除了日报,还有一个周报,除了周报,还有早会,除了早会,还有周会。这些主要是便于领导一层层向上汇报。高管们周五开周会,向集团汇报本周工作。分公司周四开周会,收集本周工作。部门周三开会,收集本周工作。小组周二开会,收集本周工作。


    写完日报,老郑还要填一个Gitee的工时管理系统,这个系统是登记工时用的,主要写今天干了什么,用了几个小时,还剩几个小时。虽然和日报是重复的,但是它有一个功能,就是能统计工时。


    这一天,老郑真的是很疲惫。感觉啥都干了,因为事情很多。又感觉啥都没干,因为作为一个程序员,没有写上几行代码。


    老郑是一家IT公司的一线程序员。下班了,他走到半路,收到一个电话,老郑用蓝牙耳机接通……


    作者:TF男孩
    来源:juejin.cn/post/7308782796952502322
    收起阅读 »

    微信小程序动态生成表单来啦!你再也不需要手写表单了!

    web
    dc-vant-form 由于我们在小程序上涉及到数据采集业务,需要经常使用表单,微信小程序的表单使用起来非常麻烦,数据和表单是分离的,每个输入框都需要做数据处理才能实现响应式数据,所以我开发了dc-vant-form,针对原生微信小程序+vant组件构建的自...
    继续阅读 »

    dc-vant-form


    由于我们在小程序上涉及到数据采集业务,需要经常使用表单,微信小程序的表单使用起来非常麻烦,数据和表单是分离的,每个输入框都需要做数据处理才能实现响应式数据,所以我开发了dc-vant-form,针对原生微信小程序+vant组件构建的自定义表单,开发者可以通过表单配置项来快速生成表单。


    1、🚩解决微信小程序表单双向绑定问题


    2、👍解决微信小程序下拉弹框值与表单绑定问题


    3、✨配置项自动生成表单


    4、🎉表单详情通过配置项控制详情回显


    5、🚀操作表单单项数据修改


    6、🔥提供9种输入组件覆盖表单的大部分业务场景


    说明


    1、在使用前需要保证项目中安装了vant


    2、在使用表单之前,你需要准备表单渲染的数据,以及当前用作回显的详情数据。


    3、该表单提供了9种输入组件,分别为:文本、小数、整数、级联选择器、文本域、数字间隔输入器、标准时间选择器、年月日时间选择器、年月时间选择器。


    4、初始化时配置参数必传,表单可传可不传,若只传配置参数,我们会根据配置参数自动生成表单。


    5、表单提供编辑回显、单条数据传入回显。


    6、通过getInit函数初始化表单,通过submit函数获取表单结果。




    开始


    npm i dc-vant-form

    自定义表单示例:


    初始化


    在初始化前,需要先定义初始化配置,配置项如下:


    key说明
    label表单label
    module表单绑定的数据key
    type表单组件类型,值对应:1文本、2小数、3整数、4级联选择器、5文本域、6时间选择器、7数字间隔输入器
    isRequired是否星号校验,值对应:true、false
    options表单下拉菜单项,值对应数组对象:[{label: '红色',value: 'red'}]
    dateType时间选择器类型,默认标准时间选择器,值对应:datetime标准时间、date年月日、year-month年月

    注意点


    类型说明
    type: 4必须配置options项,你可以给它默认值空数组[]
    type: 6必须配置dateType项,你可以选择三种对应值:datetime、date、year-month
    type: 7必须配置 beginModule、endModule,分别对应左侧、右侧输入框;type为7不需要配置module项

    下面是示例:


    "usingComponents": {
    "dc-vant-form": "/miniprogram_npm/dc-vant-form/dc-vant-form/index"
    }

    页面:


    <dc-vant-form id="dc-vant-form" />

    配置项:


    config: [
    {
    label: '详细地址',
    module: 'address',
    type: 1,
    isRequired: true
    },
    {
    label: '商品类型',
    module: 'goodsType',
    type: 4,
    isRequired: true,
    options: [
    {
    id: 1,
    label: '电子产品',
    value: 101
    },
    {
    id: 2,
    label: '儿童玩具',
    value: 102
    },
    {
    id: 3,
    label: '服装饰品',
    value: 103
    }
    ]
    },
    {
    label: '商品颜色',
    module: 'goodsColor',
    type: 4,
    isRequired: true,
    options: [
    {
    id: 1,
    label: '红色',
    value: 'red'
    },
    {
    id: 2,
    label: '青色',
    value: 'cyan'
    },
    {
    id: 3,
    label: '绿色',
    value: 'green'
    }
    ]
    },
    {
    label: '包装体积',
    module: 'packingVolume',
    type: 2,
    isRequired: false
    },
    {
    label: '商品重量',
    module: 'goodsWeight',
    type: 2,
    isRequired: true
    },
    {
    label: '商品结构',
    module: 'goodsStructure',
    type: 4,
    isRequired: true,
    options: [
    {
    id: 1,
    label: '成品',
    value: 2230
    },
    {
    id: 2,
    label: '组装',
    value: 2231
    }
    ]
    },
    {
    label: '商品数量',
    module: 'goodsNumber',
    type: 3,
    isRequired: false
    },
    {
    label: '可购范围',
    beginModule: 'beginLimit',
    endModule: 'endLimit',
    type: 7,
    isRequired: false
    },
    {
    label: '联系人',
    module: 'contact',
    type: 1,
    isRequired: false
    },
    {
    label: '创建时间',
    module: 'createDate',
    type: 6,
    dateType: 'date',
    isRequired: true
    },
    {
    label: '标准时间',
    module: 'createDate2',
    type: 6,
    dateType: 'datetime',
    isRequired: true
    },
    {
    label: '选区年月',
    module: 'createDate3',
    type: 6,
    dateType: 'year-month',
    isRequired: true
    },
    {
    label: '备注',
    module: 'remark',
    type: 5,
    isRequired: false
    }
    ]

    我们将上面的配置项传入init函数初始化表单


      // 数据初始化
    init() {
    let dom = this.selectComponent("#dc-vant-form");
    dom.getInit(this.data.config)
    },

    onLoad(options) {
    this.init();
    },

    image-20231118110736510




    获取表单数据


    我们通过submit函数获取表单数据


      // 提交
    sure() {
    let dom = this.selectComponent("#dc-vant-form");
    console.log(dom.submit());
    }

    image-20231118112342663


    image-20231118112407795




    表单回显


    在初始化时,可以传入表单详情,我们会根据配置项回显表单数据。


    // 表单详情数据
    form: {
    address: '浙江省杭州市',
    goodsType: 101,
    goodsColor: 'red',
    packingVolume: 10,
    goodsWeight: 5,
    goodsStructure: 2230,
    goodsNumber: 100,
    beginLimit: 1,
    endLimit: 10,
    contact: 'DCodes',
    createDate: '2023-01-01',
    createDate2: '2023-01-01 20:00:00',
    createDate3: '2023-01',
    remark: '这是一个动态的文本域'
    }

    init() {
    let { config,form } = this.data;
    let dom = this.selectComponent("#dc-vant-form");
    dom.getInit(config, form)
    },

    onLoad(options) {
    this.init();
    },

    image-20231118112138758




    单项数据修改


    我们提供onAccept函数,用于接收指定表单项的修改


    onAccept接收三个参数,依次为:value、key、place


    参数说明
    value更改的值
    key表单中对应的key
    place如果是数字间隔修改器,需要传入place,分为两个固定参数:left、right,表示需要修改间隔输入框的左侧和右侧

    bandicam 2023-11-16 16-14-16-944 00_00_00-00_00_30~1


    // 修改某项
    update() {
    let dom = this.selectComponent("#dc-vant-form");
    // 普通类型
    // dom.onAccept('浙江省杭州市', 'address')

    // 级联选择器-value为options中的key
    // dom.onAccept(103, 'goodsType')

    // 数字间隔输入器
    // dom.onAccept(1, 'beginLimit', 'left')
    // dom.onAccept(3, 'endLimit', 'right')
    }



    如果觉得该组件不错,欢迎点赞👍、收藏💖、转发✨哦~


    作者:DCodes
    来源:juejin.cn/post/7302359255331110947
    收起阅读 »

    学会XPath,轻松抓取网页数据

    web
    一、定义 XPath(XML Path Language)是一种用于在 XML 文档中定位和选择节点的语言。XPath的选择功能非常强大,可以通过简单的路径选择语法,选取文档中的任意节点或节点集。学会XPath,可以轻松抓取网页数据,提高数据获取效率。 二、X...
    继续阅读 »

    一、定义


    XPath(XML Path Language)是一种用于在 XML 文档中定位和选择节点的语言。XPath的选择功能非常强大,可以通过简单的路径选择语法,选取文档中的任意节点或节点集。学会XPath,可以轻松抓取网页数据,提高数据获取效率。


    二、XPath基础语法


    节点(Nodes): XML 文档的基本构建块,可以是元素、属性、文本等。
    路径表达式: 用于定位 XML 文档中的节点。路径表达式由一系列步骤组成,每个步骤用斜杠 / 分隔。


    XPath的节点是指在XML或HTML文档中被选择的元素或属性。XPath中有7种类型的节点,包括元素节点、属性节点、文本节点、命名空间节点、处理指令节点、注释节点以及文档节点(或称为根节点)。


    - 元素节点:表示XMLHTML文档中的元素。例如,在HTML文档中,<body>、<div>、<p>等都是元素节点。在XPath中,可以使用元素名称来选择元素节点,例如://div表示选择所有的<div>元素。

    - 属性节点:表示XMLHTML文档中元素的属性。例如,在HTML文档中,元素的classidsrc等属性都是属性节点。在XPath中,可以使用@符号来选择属性节点,例如://img/@src表示选择所有<img>元素的src属性。

    - 文本节点:表示XMLHTML文档中的文本内容。例如,在HTML文档中,<p>标签中的文本内容就是文本节点。在XPath中,可以使用text()函数来选择文本节点,例如://p/text()表示选择所有<p>元素中的文本内容。

    - 命名空间节点:表示XML文档中的命名空间。命名空间是一种避免元素命名冲突的方法。在XPath中,可以使用namespace轴来选择命名空间节点,例如://namespace::*表示选择所有的命名空间节点。

    - 处理指令节点:表示XML文档中的处理指令。处理指令是一种用来给处理器传递指令的机制。在XPath中,可以使用processing-instruction()函数来选择处理指令节点,例如://processing-instruction('xml-stylesheet')表示选择所有的xml-stylesheet处理指令节点。

    - 注释节点:表示XMLHTML文档中的注释。注释是一种用来添加说明和备注的机制。在XPath中,可以使用comment()函数来选择注释节点,例如://comment()表示选择所有的注释节点。

    - 文档节点:表示整个XMLHTML文档。文档节点也被称为根节点。在XPath中,可以使用/符号来选择文档节点,例如:/表示选择整个文档节点。

    本文使用XML示例如下


    <bookstore>
    <book category='fiction'>
    <title>活着</title>
    <author>余华</author>
    <press>作家出版社</press>
    <date>2012-8-1</date>
    <page>191</page>
    <price>20.00</price>
    <staple>平装</staple>
    <series>余华作品(2012版)</series>
    <isbn>9787506365437</isbn>
    </book>
    <book category='non-fiction'>
    <title>撒哈拉的故事</title>
    <author>三毛</author>
    <press>哈尔滨出版社</press>
    <date>2003-8</date>
    <page>217</page>
    <price>15.80</price>
    <staple>平装</staple>
    <series>三毛全集(华文天下2003版)</series>
    <isbn>9787806398791</isbn>
    </book>
    <book category='non-fiction'>
    <title>明朝那些事儿(1-9)</title>
    <author>当年明月</author>
    <press>中国海关出版社</press>
    <date>2009-4</date>
    <page>2682</page>
    <price>358.20</price>
    <staple>精装16开</staple>
    <series>明朝那些事儿(典藏本)</series>
    <isbn>9787801656087</isbn>
    </book>
    </bookstore>

    除了这些基本节点类型之外,XPath还支持使用通配符:


    通配符描述示例
    *匹配任何元素节点//book/* 选取<book>元素下的任意子元素节点
    @*匹配任何属性节点//book/@* 选取<book>元素上的任意属性节点,如<book category='fiction'>中的category属性
    node()匹配任何类型的节点//book/node() 选取<book>元素下的所有类型的子节点,包括元素节点、文本节点、注释节点等

    以及使用谓词来进一步筛选选择的节点集。谓词是一种用来对节点进行过滤和排序的机制,可以包含比较运算符、逻辑运算符和函数等,部分示例如下:


    谓语描述示例
    [position()=n]选取位于指定位置的节点。n 是节点的位置(从 1 开始计数)//book[position()=1] 选取第一个<book>元素
    [last()=n]选取位于指定位置的最后一个节点。n 是节点的位置(从 1 开始计数)//book[last()=1] 选取最后一个<book>元素
    [contains(string, substring)]选取包含指定子字符串的节点。string 是节点的文本内容,substring 是要查找的子字符串//book[contains(title, 'XML')] 选取标题中包含子字符串'XML'<book>元素
    [starts-with(string, prefix)]选取以指定前缀开始的节点。string 是节点的文本内容,prefix 是要匹配的前缀字符串//book[starts-with(title, 'The')] 选取标题以'The'开始的<book>元素
    [text()=string]选取文本内容完全匹配的节点。string 是要匹配的文本内容//book[text()='Book Title'] 选取文本内容为'Book Title'<book>元素
    [@category='non-fiction']选取具有指定属性值的节点。category 是属性名称,non-fiction 是要匹配的值//book[@category='non-fiction'] 选取具有属性category值为'non-fiction'<book>元素

    XPath使用路径表达式来选取XML或HTML文档中的节点或节点集。下面是一些常用的路径表达式:


    表达式描述示例
    nodename选取此节点的所有子节点//bookstore/book 选取<bookstore>元素下所有<book>子元素
    /从根节点选取直接子节点/bookstore 从根节点选取<bookstore>元素
    //从当前节点选取子孙节点//book 选取所有<book>元素,无论它们在文档中的位置
    .选取当前节点./title 选取当前节点的<title>子元素
    ..选取当前节点的父节点../price 选取当前节点的父节点的<price>子元素
    @选取属性//book/@id 选取所有<book>元素的id属性

    三、XPath使用示例


    选择所有名称为title的节点://title
    选择所有名称为title,同时属性lang的值为eng的节点://title[@lang='eng']
    选择id为bookstore的节点的所有子节点:/bookstore/*
    选择id为bookstore的节点的所有子孙节点:/bookstore//*
    选择id为bookstore的节点的直接子节点中的第一个节点:/bookstore/*[1]
    选择id为bookstore的节点的属性为category的值:/bookstore/@category


    四、XPath的高级用法


    XPath语言提供了一些高级的功能,包括:


    轴(Axes):XPath提供了几种轴,用于在文档中导航。包括child(子元素)、ancestor(祖先元素)、descendant(后代元素)和following-sibling(后续同级元素)等。


    函数:XPath提供了一些内置的函数,如count(),concat(),string(),local-name(),contains(),not(),string-length()等,可以用于处理和操作节点和属性3。


    条件语句:XPath提供了条件语句(如if-else语句),使得我们可以根据某些条件来选择性地提取元素或属性3。


    五、.NET中使用


    // XML 文档内容
    string xmlContent = @"
    <bookstore>
    <book category='fiction'>
    <title>活着</title>
    <author>余华</author>
    <press>作家出版社</press>
    <date>2012-8-1</date>
    <page>191</page>
    <price>20.00</price>
    <staple>平装</staple>
    <series>余华作品(2012版)</series>
    <isbn>9787506365437</isbn>
    </book>
    <book category='non-fiction'>
    <title>撒哈拉的故事</title>
    <author>三毛</author>
    <press>哈尔滨出版社</press>
    <date>2003-8</date>
    <page>217</page>
    <price>15.80</price>
    <staple>平装</staple>
    <series>三毛全集(华文天下2003版)</series>
    <isbn>9787806398791</isbn>
    </book>
    <book category='non-fiction'>
    <title>明朝那些事儿(1-9)</title>
    <author>当年明月</author>
    <press>中国海关出版社</press>
    <date>2009-4</date>
    <page>2682</page>
    <price>358.20</price>
    <staple>精装16开</staple>
    <series>明朝那些事儿(典藏本)</series>
    <isbn>9787801656087</isbn>
    </book>
    </bookstore>"
    ;

    // 创建 XPath 文档
    using (XmlReader reader = XmlReader.Create(new StringReader(xmlContent)))
    {
    XPathDocument xpathDoc = new XPathDocument(reader);

    // 创建 XPath 导航器
    XPathNavigator navigator = xpathDoc.CreateNavigator();

    // 使用 XPath 查询(选择所有位于bookstore下、其category属性值为'fiction'的book元素中的title元素)
    string xpathExpression = "//bookstore/book[@category='fiction']/title";
    XPathNodeIterator nodes = navigator.Select(xpathExpression);

    // 检查是否有匹配的节点
    if (nodes != null)
    {
    // 遍历结果
    while (nodes.MoveNext())
    {
    // 检查当前节点是否为空
    if (nodes.Current != null)
    {
    Console.WriteLine(nodes.Current.Value);
    }
    }
    }
    }

    运行结果


    微信截图_20231129223229.png


    六、XPath在自动化测试中的应用


    XPath最常用的场景之一就是在自动化测试中用来选择HTML DOM节点。例如,在Selenium自动化测试中,可以使用XPath作为选择web元素的主要方法之一。通过XPath选择器,可以方便地定位页面中的任意元素,进行自动化测试操作。


    七、XPath的优势与不足


    XPath的优势在于其强大的选择功能,可以通过简单的路径选择语法,选取文档中的任意节点或节点集。此外,XPath还支持超过100个内建函数,可用于字符串处理、数值计算、日期和时间比较等等。这些函数可以大大提高数据处理的效率。


    然而,XPath也有其不足之处。首先,XPath对于复杂的文档结构可能会变得非常复杂,导致选择语句难以理解和维护。其次,XPath在处理大量数据时可能会出现性能问题,因为它需要遍历整个文档来查找匹配的节点。因此,在使用XPath时需要注意优化查询语句,提高查询效率。


    八、总结


    学会XPath,可以轻松抓取网页数据,提高数据获取效率。本文介绍了XPath的定义、基础语法、使用示例、高级用法、.NET中使用举例以及在自动化测试中的应用场景,同时也讨论了XPath的优势与不足。希望本文能够帮助读者更好地理解和掌握XPath的使用方法。


    希望以上内容能够帮助你理解和学习XPath。欢迎点赞、关注、收藏,如果你还有其他问题,欢迎评论区交流。


    作者:GoodTime
    来源:juejin.cn/post/7306858863444623400
    收起阅读 »

    js终止程序,我常用throw 替代 return

    web
    js终止程序有两种方式(如果还有别的请告知我) throw return 这两个好像是两大阵营,前者我个人最推崇,但是很少见人用, 不知道啥原因(兴许是讨厌写try catch吧)。 刚入门那会,总觉得下面这样的验证好麻烦 const formValu...
    继续阅读 »

    js终止程序有两种方式(如果还有别的请告知我)



    1. throw

    2. return


    这两个好像是两大阵营,前者我个人最推崇,但是很少见人用, 不知道啥原因(兴许是讨厌写try catch吧)。


    刚入门那会,总觉得下面这样的验证好麻烦


      const formValues = {
    mobile: '',
    name: '',
    }

    function onSubmit() {
    if (!formValues.name) {
    alert('请输入用户名')
    return
    }

    if (!formValues.mobile) {
    alert('请输入手机号')
    return
    }

    // n个表单验证,return N次 alert N 次
    }

    后来发现,可以用throw改进一下



    const formValues = {
    mobile: '',
    name: '',
    }

    function onSubmit() {
    try {
    if (!formValues.name) throw ('请输入用户名')
    if (!formValues.mobile) throw String('请输入手机号')

    } catch (error) {
    alert(error)
    }
    }

    这样就好看多了(但很多人觉得,try catch 难看 😂)。
    后来验证多了,就把验证挪到单独一个函数里


      const formValues = {
    mobile: '',
    name: '',
    }

    function validateFormValues() {
    if (!formValues.name) throw ('请输入用户名')
    if (!formValues.mobile) throw String('请输入手机号')
    }

    function onSubmit() {
    try {
    validateFormValues()

    } catch (error) {
    alert(error)
    }
    }

    主函数看起来干净些了。这是throw才能做到的,报错跳出调用栈。


    由此引出了之前看过的一种写法,用return(我不喜欢)


      const formValues = {
    mobile: '',
    name: '',
    }

    function validateFormValues() {
    if (!formValues.name) {
    alert('请输入用户名')
    return
    }
    if (!formValues.mobile) {
    alert('请输入手机号')
    return
    }

    return true
    }

    function onSubmit() {
    const isValidateFormValuesSuccess = validateFormValues();

    // 这点我不喜欢,因为还要再写一次判断
    if (!isValidateFormValuesSuccess) return
    }


    如果是遇到嵌套深的复杂场景,函数套函数,是不是就很无力了,因为没法跳转函数栈,只能层层判断。


    但是throw就可以无视嵌套,直接报错,最晚层接住错误就可以了。当我们写代码的时候,想终止程序,就直接throw。


    看下面这段代码 promise async await 联合使用。可用空间就大了撒。
    从此随便造,函数大了就拆逻辑成小函数,想终止就throw


      // promise里面throw 错误 = reject(错误)
    async function onSubmit() {
    try {
    await new Promise((resolve) => {
    throw String('故意报错')
    })

    console.log('a'); // 不会执行
    } catch (error) {
    alert(error) // 结果:alert 故意报错
    }
    }

    // promise里面 catch 也可以直接抛错误
    async function onSubmit() {
    try {
    await new Promise((resolve, reject) => {
    reject('故意报错')
    }).catch(error => {
    throw error
    })

    console.log('b'); // 不会执行
    } catch (error) {
    alert(error)
    }
    }

    可能有的小伙伴会想,try catch 有性能问题。看下图,来源于经典书《高性能javaScript》


    image.png


    之前公司小伙伴也有这个疑问,我翻了书加上用chorme 微信小程序编辑器,去测过,最终差别不大,没问题的,使劲用。


    由此启发,这时候引入一个catchError函数,专门用来接收报错


    // 报错白名单,收到这个就不提示报错了,标明是主动行为
    const MANUAL_STOP_PROGRAM = "主动终止程序";

    /**
    * @feat < 捕获错误 >
    * @param { unknown } error 错误
    * @param { string } location 错误所在位置,标识用的
    * @param { Function } onParsedError 解析错误,可能需要把这个错误弄到页面上显示是啥错误
    */

    function catchError(error, location, { onParsedError } = {}) {
    try {
    const errorIsObj = error && typeof error === "object";

    if (errorIsObj) throw JSON.stringify(error);

    // 其他处理,比如判断是取消网络请求,错误集中上报等等,大家自由发挥,有啥好想法欢迎评论区留言

    throw error;
    } catch (error) {
    console.error(`${location || ""}-捕获错误`, error);

    if (new RegExp(MANUAL_STOP_PROGRAM).test(error)) throw MANUAL_STOP_PROGRAM;

    // 错误解析完毕
    onParsedError && onParsedError(error);

    alert(error) // 弹窗提示错误
    throw MANUAL_STOP_PROGRAM;
    }
    }


    在上面中 MANUAL_STOP_PROGRAM 就是个白名单了,专门用来标识是主动报错,但是不提示错误
    每次 catchError 之后,要把 MANUAL_STOP_PROGRAM 继续抛出来,因为我们可能业务调用链很深,需要多个地方使用到 catchError,但是只需要报错一次,而且需要报错告知外层不执行后续逻辑。


    再结合 location 参数,我们可以看到清晰的错误来源


    catchError 这个是我得初步设想,一直想做统一的错误收集中心。如果您有好的想法,欢迎告知评论区。


    上面执行后,控制台是有点难看的


    image.png


    通过window.onerror能收集到部分错误,但是异步的就收集不到了。(async promise 这些就没办法了),如果您有啥办法能收集到,麻烦告知一下。


    但是控制台难看有啥关系呢。(反正用户和老板也看不到 😂)


    /**
    * @param { string } message
    * @param { source } 表示发生错误的脚本文件的 URL
    * @param { lineno } 表示发生错误的行号
    */

    window.onerror = function(message, source, lineno) {
    // 错误处理逻辑
    };

    下面是一个比较极端的例子,演示一下深层级的报错效果


      // 生成假数据
    async function genrateMockList() {
    try {
    const list = await Promise.all(
    new Array(100).fill(',').map((item, index) => {
    try {
    if (index === 1) throw String('map 故意报错,嵌套比较深了')

    return {
    index: 'name'
    }
    } catch (error) {
    catchError(error, 'genrateMockList__item')
    }
    })
    )
    return list
    } catch (error) {
    catchError(error, 'genrateMockList')
    }
    }

    async function getDetails() {
    try {
    const dataList = await genrateMockList();
    console.log('a') // 不会执行
    } catch (error) {
    catchError(error, 'getDetails')
    }
    }

    getDetails();

    image.png


    笔者始终认为,写代码很重要的一点是 数据结构 和 程序流控制。


    结构清楚了,所有的东西都能一生二二生四一直延伸变化。


    程序流控制我们尽量做到简单清晰。


    别再纠结用了多少个try catch 多难看了,多一个try catch 就多一分安心,特别是复杂的业务逻辑,可能需要经过5-6个小函数,这时候加上try catch 就能把报错范围缩小,等到代码完全可靠后再移除 try 也不迟。


    兴许return 当初设计就只是为了返回值吧,我总觉得throw才是js设计者的终止程序的用意。这段历史有知道的也欢迎说一下。


    以上内容供大家参考。有啥看法欢迎评论区留言。


    预告:下周开始写一些组件设计思考。是一个系列,存货不多,顶多写几篇


    作者:闸蟹
    来源:juejin.cn/post/7307522662287556646
    收起阅读 »

    前端外描边完美实现

    web
    背景 最近在公司做画布相关的内容,涉及到了字体描边的方案选择,在三种方案对比和尝试下,最终选用了 SVG 作为字体描边的方案,今天就来讲讲这些方案的优缺点。 字体描边方案对比 1. text-stroke 优点: 实现效果好 缺点: 兼容性一般,需要配合 -...
    继续阅读 »

    背景


    最近在公司做画布相关的内容,涉及到了字体描边的方案选择,在三种方案对比和尝试下,最终选用了 SVG 作为字体描边的方案,今天就来讲讲这些方案的优缺点。


    字体描边方案对比


    1. text-stroke


    优点: 实现效果好


    缺点:



    • 兼容性一般,需要配合 -webkit-background-clip 属性来实现外描边,而市面上的截图库都不支持这个属性😅,也就是截图后描边效果会丢失(尝试过 html2canvas、html-to-image、dom-to-image,以及公司内部的一些截图库)

    • 有描边吞字的现象:描边宽度变大时,描边会向内扩展把文本覆盖,

    • 宽度为0px的时候也依旧存在描边


    效果:


    2. text-shadow


    优点: 兼容性好


    缺点: 实现效果不好,怎么说呢,很难评,有种锯齿的美,毕竟人家不是干这行的 😅


    效果:


    3. SVG


    优点: 兼容性好、实现效果好,整体上看比 text-stroke 效果还要好


    缺点: iOS 上同样存在描边吞字的现象,但是它的缺点都可以解决,还请看下文


    效果:


    image.png


    4. Canvas


    优点: 兼容性好


    缺点:



    • 字体整体比较模糊

    • 有描边吞字的现象

    • 需要通过 canvas api 来进行绘制


    效果:



    调试


    上面四种方案都可以在 CodeSandBox 中自行尝试一下:


    codesandbox.io/p/sandbox/s…


    SVG 实现字体描边


    通过 svg 的 paint-order 来实现字体描边,兼容性最好,并且实现效果也很不错,基本兼容市面上所有浏览器,并且截图库基本都支持这个属性,下面就来讲讲 SVG 字体描边方案的实现:


    <svg xmlns="http://www.w3.org/2000/svg" width="400" height="200">
    <text
    x="0"
    y="0"
    alignment-baseline="text-before-edge"
    text-anchor="start"
    >

    字体描边
    </text>
    </svg>

    text {
    font-size: 50px;
    font-weight: bold;
    stroke: red;
    stroke-width: 4px;
    paint-order: stroke;
    }

    通过 stroke-linejoin 属性,可以 对 svg 的描边有更灵活的控制:



    但是在 iOS 中,使用 paint-order 有一个坑:当 stroke-width 被设置成不同值的时候,描边有可能向文字内部扩展,导致字体被吞没,最终字体的颜色变成跟描边的颜色一致。


    解决这个问题当然也有一个办法:使用 svg 的 tspan


    tspan 可以控制一个 text 标签中多行文本的展示,通过设置 dxdy 属性来控制与上一个 tspan 的距离。那么对于 iOS 描边展示异常这个问题,我们就有了一个解决办法:



    1. text 内添加两个 tspan

    2. 第一个 tspan 用来控制描边展示,设置 stroke-width

    3. 第二个 tspan 用户展示字体主体,覆盖在第一个 tspan 上面(设置 dx="0" dy="0"


    <svg xmlns="http://www.w3.org/2000/svg" width="400" height="200">
    <text>
    <tspan
    x="0"
    y="0"
    style="stroke-width: 5px"
    alignment-baseline="text-before-edge"
    text-anchor="start"
    >

    文本
    </tspan>
    <tspan
    dx="0"
    dy="0"
    alignment-baseline="text-before-edge"
    text-anchor="start"
    >

    文本
    </tspan>
    </text>
    </svg>

    兼容性如下:



    总结



    • 整体上来看,通过 SVG 实现字体描边比其他三种方案效果都要好,并且兼容性也不错;

    • 同时,tspan 可以控制 text 中的文本换行,通过 tspan 可以解决字体被描边覆盖的问题


    作者:DAHUIAAAAAA
    来源:juejin.cn/post/7307544166446956556
    收起阅读 »

    2024年,Rust和Go学哪个更好?

    Rust vs. Go,在2024年,应该选择哪一个?或者说应该选择哪种语言——GoLang还是Rust。这可能是许多程序员在选择语言时考虑的一个问题。选择理想的编程语言被视为在这个不断变化的环境中取得成功的重要抉择。 GoLang和Rust是当今使用的最年轻...
    继续阅读 »

    Rust vs. Go,在2024年,应该选择哪一个?或者说应该选择哪种语言——GoLang还是Rust。这可能是许多程序员在选择语言时考虑的一个问题。选择理想的编程语言被视为在这个不断变化的环境中取得成功的重要抉择。


    GoLang和Rust是当今使用的最年轻的编程语言。Go于2009年在Google推出,而在Go之后,Rust于2010年在Mozilla推出。这两种语言在当前流行的编程语言工具中有一些相似之处和差异。


    通过本文,我们将讨论Rust和Go之间的基本差异和相似之处。


    关于Go


    Go是一门开源的计算机语言,可以更轻松地创建简单、高效和强大的软件。Go是精确、流畅和高效的。编写一个利用多核和网络机器的程序非常方便。


    Go或GoLang是由Google工程师创建的,他们希望创建一种既具有C++的效率,又更容易学习、编写、阅读和安装的语言。


    GoLang主要用于创建网络API和小型服务,特别是其goroutines,具有可扩展性。GoLang可以流畅地组装为机器代码,并提供舒适的垃圾回收和表示运行时的能力。


    Go是一种快速、静态类型的汇编语言,给人一种解释型和动态类型语言的感觉。Goroutines的语言使开发人员能够创建完全掌控并发的应用程序,例如大型电子商务网站,同时在多个CPU核心上调度工作负载。


    因此,准确地说,它非常适合并行计算环境。垃圾回收是Go的另一个特性,可以保证高效的内存管理。因此,未使用的内存可以用于新项目,而未使用的对象则从内存中“丢弃”。


    关于Rust


    Rust是一种静态类型的编译型编程语言,受到多种编程原型的支持。该语言最初的创建目标是优先考虑性能和安全性,其中安全性是主要目标。


    Rust主要用于处理CPU密集型的活动,例如执行算法和存储大量数据。因此,需要高性能的项目通常使用Rust而不是GoLang。


    理想情况下,Rust是C++的镜像。与GoLang和Java不同,Rust没有垃圾回收。相反,Rust使用借用检查器来确保内存安全。这个借用检查器强制执行数据所有权协议,以避免数据竞争。在这里,数据竞争意味着多个指针指向同一个内存位置。


    Rust是一种用于长时间大型或小型团队的计算机编程语言。对于这种类型的编程,Rust提供了高度并发和极其安全的系统。


    Rust现在被广泛用于Firefox浏览器的大部分部分。在2016年之后,Rust被宣称为最受欢迎的编程语言。Rust是一种非常基础的语言,可以在短短5分钟内学会。


    Rust vs. Go,优缺点


    要准确决定选择Go还是Rust,最好看一下GoLang和Rust的优势和劣势。上面我们已经对它们有了简单的了解,下面是它们的优点和缺点。


    GoLang的优点



    • 它是一种简洁和简单的编程语言。

    • 它是一种良好组合的语言。

    • 以其速度而闻名。

    • Go具有很大的灵活性,并且易于使用。

    • 它是可扩展的。

    • 它是跨平台的。

    • 它可以检测未使用的变量。

    • GoLang具有静态分析工具。


    GoLang的缺点



    • 没有手动内存管理。

    • 因为它太容易,所以感觉很表面。

    • 由于年轻,所以库较少。

    • 其中一些函数(如指针算术)是底层的。

    • GoLang的工具有一些限制。

    • 分析GoLang中的错误可能很困难。


    Rust的优点



    • 提供非凡的速度。

    • 由于编译器,提供最佳的内存安全性。

    • 零成本抽象的运行时更快。

    • 它也是跨平台的。

    • 它提供可预测的运行时行为。

    • 它提供了访问优秀模式和语法的方式。

    • 它具有特殊的所有权特性。

    • 它易于与C语言和其他语言结合使用。


    Rust的缺点



    • 尽管它确实很快,但有人声称它比F#慢。

    • 它具有基于范围的内存管理,可能导致内存泄漏的无限循环。

    • 在Rust中无法使用纯函数式数据框架,因为没有垃圾回收。

    • Rust没有Python和Perl语言支持的猴子补丁水平。

    • 由于语言还很新,可能会对语法感到担忧。

    • 编译时有时会很慢,因此学习变得困难。


    数据告诉我们什么?


    根据一份报告,GoLang语言被认为是参与者最喜欢的语言。


    我们对GoLang和Rust语言有了基本的了解,现在继续进行Rust vs. Go的比较,并清楚地认识到这两种语言之间的差异。


    Rust和Go的主要区别


    GoLang和Rust之间的主要区别包括:



    • 性能

    • 并发性

    • 内存安全性

    • 开发速度

    • 开发者体验


    (1) 性能


    Google推出Go作为易于编码和学习的C++替代品。Go提供Goroutines,通过其中一个可以通过简单地包含Go语法来运行函数。


    尽管Go具有这些有用的功能和对多核CPU的支持,但Rust占据上风,超过了Go。


    因此,Go vs Rust:性能是Rust在与GoLang的比较中获得更多分数的一个特点。这些编程语言都是为了与C++和C等价而创建的。然而,在Rust vs. Go的比较中,GoLang的开发速度略高于Rust的性能。


    虽然Rust在性能上优于Go,但在编译速度方面,Rust落后于Go。


    然而,人们对编译时间并不太在意,所以整体上Rust在这方面是胜利者。


    (2) 并发性


    GoLang支持并发,在这一因素上比Rust有优势。Go的并发模型允许开发人员在不同的CPU核心上安装工作负载,使Go成为一种连贯的语言。


    因此,在运行处理API请求的网站的情况下,GoLang goroutines将每个请求作为子进程运行。这个过程提高了效率,因为它将任务从所有CPU核心中卸载出来。


    另一方面,Rust只有一个原生的等待或同步语法。因此,程序员更喜欢使用Go的方式来处理并发问题。


    (3) 内存安全性


    Rust使用编译时头文件策略来实现零成本中断的内存安全性。如果不是内存安全的程序,Rust将无法通过编译阶段。实际上,Rust的好处之一就是提供了内存安全性。


    为了实现内存安全的并发,Rust使用类型安全性。Rust编译器调查你引用的每个内存地址和使用的每个变量。Rust的这个特性将通知你任何未定义行为和数据竞争。


    它确保程序员不会遇到缓冲区溢出的情况。


    相比之下,Go在运行时完全自动化。因此,开发人员在编写代码时不必担心内存释放。


    因此,无论是GoLang还是Rust都优先考虑内存安全特性,但在性能方面,GoLang具有数据竞争的可能性。


    (4) 开发速度


    在某些情况下,开发速度比性能和程序速度更重要。Go语言的直接性和清晰性使其成为一种开发速度较快的语言。Go语言具有更短的编译时间和更快的运行时间。


    尽管Go既提供了开发速度和简单性,但它缺少一些重要的功能。为了使语言更简单,Google删除了其他编程语言中可用的许多功能。


    另一方面,Rust比Go拥有更多的功能。Rust具有更长的编译时间。


    因此,如果项目的优先级是开发速度,Go比Rust要好得多。如果你不太关心开发速度和开发周期,但希望获得性能和内存安全性,那么Rust是你的最佳选择。


    (5) 开发者体验


    由于开发Go的主要动机是简单和易用性,大多数程序员认为它是一种“无聊的语言”或“简单的语言”。Go中的功能有限,使得学习和实现非常简单。


    相反,Rust具有更高的内存安全功能,使得代码更复杂,降低了程序员的生产力。所有权的概念使得Rust语言对许多人来说不是理想的选择。


    与Go相比,Rust的学习曲线要陡峭得多。然而,值得注意的是,与Python和JavaScript等语言相比,GoLang的学习曲线也较陡峭。


    Rust和Go的共同特点


    在Rust vs Go的比较中,这两者之间有很多共同之处。GoLang和Rust都是许多年轻开发人员使用的现代编程语言。


    GoLang和Rust都是编译语言,都是开源的,并且都是用于微服务的计算环境。


    此外,如果你对C++有一些了解,那么这两个程序都非常容易理解。


    交互性


    Rust能够与代码进行接口交互,例如直接与C库进行通信。Rust没有提供内存安全性的认证。


    交互性带来了速度。Go提供了与C语言配合使用的Go包。


    何时应该使用GoLang?


    Go语言可用于各种不同的项目。根据一份报告,Go的用例包括网页开发、数据库和Web编程。大多数GoLang开发人员声称,由于Go的并发性,它对Web服务有一些限制。


    不仅如此,Go还被列为后端Web开发的首选语言。Go语言还为Google Cloud Platform提供支持。因此,在高性能云应用中,Go确实是性能消耗大的语言。


    何时应该使用Rust?


    Rust是一种几乎可以在任何地方使用的计算机编程语言。然而,仍然有一些领域比其他领域更适合使用。系统编程就是其中之一,因为Rust在高性能方面表现出色。


    系统程序员基本上是在硬件侧开发的软件工程师。由于Rust处理硬件侧内存管理的复杂性,它经常用于设计操作系统或计算机应用程序。


    尽管在开发者社区内对什么构成中级语言存在一些争议,但Rust被视为具有面向机器的现代语言的特点。


    总结


    这两种语言,GoLang和Rust,由于它们非常相近的起源时间,被认为是彼此的竞争对手。Go的发展速度比Rust快。这两种语言有很多相似之处。


    GoLang和Rust之间的区别在于Go是简单的,而Rust是复杂的。然而,它们的功能和优先级在各种有意义的方面有所不同。


    Go与Rust并驾齐驱。这意味着这完全取决于你拥有的项目类型,主要取决于对你的业务来说什么是最好的。


    作者:程序新视界
    来源:juejin.cn/post/7307648485921980470
    收起阅读 »

    大专前端,三轮面试,终与阿里无缘

    web
    因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会 先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科 就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题...
    继续阅读 »

    因为一些缘故,最近一直在找工作,再加上这个互联网寒冬的大环境,从三月找到六月了,一直没有合适的机会



    先说一下背景,目前三年半年经验,base 杭州,大专学历+自考本科



    就在前几天,Boss 上收到了阿里某个团队的投递邀请(具体部门就不透露了),因为学历问题,基本上大厂简历都不会通过初筛,但还是抱着破罐子破摔的心态投递给了对方,出乎意料的是简历评估通过了,可能是因为有两个开源项目和一个协同文档加分吧。


    进入到面试环节,首先是两道笔试题,算是前置面试:


    第一道题目是算法题:


    提供了一个数组结构的 data,要求实现一个 query 方法,返回一个新的数组,query 方法内部有 过滤排序分组 等操作,并且支持链式调用,调用最终的 execute 方法返回结果:


    const result = query(list)
    .where(item => item.age > 18)
    .sortBy('id')
    .groupBy('name')
    .execute();

    console.log(result);

    具体实现这里就不贴了,过滤用原生的数组 filter 方法,排序用原生的数组 sort 方法,分组需要手写一下,类似 lodash/groupBy 方法。


    过滤和排序实现都比较顺利,在实现分组方法的时候不是很顺利,有点忘记思路了,不过最后还是写出来了,关于链式调用,核心是只需要在每一步的操作最后返回 this 即可。


    第二道题目是场景题:


    要求用 vue 或者 react 实现一个倒计时抢券组件,页面加载时从 10s 开始倒计时,倒计时结束之后点击按钮请求接口进行抢券,同时更新文案等等功能。因为我对 react 比较熟悉一点,所以这里就选择了 react。


    涉及到的知识点有 hook 中对 setTimeout 的封装、异步请求处理、状态更新CSS基本功 的考察等等……


    具体实现这里也不贴了,写了一堆自定义 hook,因为平时也在参与 ahooks 的维护工作,ahooks 源码背的滚瓜烂熟,所以直接搬过来了,这道题整体感觉没啥难度,算是比较顺利的。


    笔试题整个过程中唯一不顺利的是在线编辑器没有类似 vscode 这样的 自动补全 功能,不管是变量还是保留字,很多单词想不起来怎么拼写,就很尴尬,英文太差是硬伤 :(


    笔试过程最后中出现了一点小插曲,因为笔试有时间限制,需要在规定的时间内完成,但是倒计时还没结束,不知道为什么就自动交卷了,不过那个时候已经写的差不多了,功能全部实现了,还剩下卡片的样式没完成,css 还需要完善一下,于是就在 Boss 上跟对方解释了一下,说明了情况。


    过了几分钟,对面直接回复笔试过了,然后约了面试。


    一面:



    • 自我介绍


      这里大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。


    • 七层网络模型、和 DNS 啥的


      计网这方面属于知识盲区了,听到这个问题两眼一黑,思索了一会儿,直接说回答不上来。


    • 然后问了一些 host 相关的东西



      • 很遗憾也没回答上来,尴尬。对方问我是不是计算机专业的,我坦诚的告诉对方是建筑工程。



    • React 代码层的优化可以说一下么?



      • 大概说了 class 组件和 function 组件两种情况,核心是通过减少渲染次数达到优化目的,具体的优化手段有 PureComponentshouldComponentUpdateReact.memoReact.useMemoReact.useCallbackReact.useRef 等等。



    • 说一下 useMemouseCallback 有什么区别



      • 很基础的问题,这里就不展开说了。



    • 说一下 useEffectuseLayoutEffect 有什么区别



      • 很基础的问题,这里就不展开说了。



    • 问了一下 useEffect 对应在 class 中都生命周期怎么写?



      • 很基础的问题,这里就不展开说了。



    • 如果在 if 里面写 useEffect 会有什么表现?



      • 开始没听清楚,误解对方的意思了,以为他说的是在 useEffect 里面写 if 语句,所以胡扯了一堆,后面对方纠正了一下,我才意识到对方在问什么,然后回答了在条件语句里面写 useEffect 控制台会出现报错,因为 hook 的规则就是不能在条件语句或者循环语句里面写,这点在 react 官方文档里面也有提到。



    • 说一下 React 的 Fiber 架构是什么



      • 这里说了一下 Fiber 本质上就是一个对象,是 React 16.8 出现的东西,主要有三层含义:



        1. 作为架构来说,在旧的架构中,Reconciler(协调器)采用递归的方式执行,无法中断,节点数据保存在递归的调用栈中,被称为 Stack Reconciler,stack 就是调用栈;在新的架构中,Reconciler(协调器)是基于 fiber 实现的,节点数据保存在 fiber 中,所以被称为 fiber Reconciler。

        2. 作为静态数据结构来说,每个 fiber 对应一个组件,保存了这个组件的类型对应的 dom 节点信息,这个时候,fiber 节点就是我们所说的虚拟 DOM。

        3. 作为动态工作单元来说,fiber 节点保存了该节点需要更新的状态,以及需要执行的副作用。


        (这里可以参考卡颂老师的《自顶向下学 React 源码》课程)




    • 前面提到,在 if 语句里面写 hook 会报错,你可以用 fiber 架构来解释一下吗?



      • 这里说了一下,因为 fiber 是一个对象,多个 fiber 之间是用链表连接起来的,有一个固定的顺序…… 其实后面还有一些没说完,然后对方听到这里直接打断了,告诉我 OK,这个问题直接过了。



    • 个人方面有什么规划吗?



      • 主要有两个方面,一个是计算机基础需要补补,前面也提到,我不是科班毕业的,计算机底层这方面比起其他人还是比较欠缺的,尤其是计网,另一方面就是英文水平有待提高,也会在将来持续学习。



    • 对未来的技术上有什么规划呢?



      • 主要从业务转型工程化,比如做一些工具链什么的,构建、打包、部署、监控几个大的方向,node 相关的,这些都是我感兴趣的方向,未来都可以去探索,当然了现在也慢慢的在做这些事情,这里顺便提了一嘴,antd 的 script 文件夹里面的文件是我迁移到 esm + ts 的,其中一些逻辑也有重构过,比如收集 css token、生成 contributors 列表、预发布前的一些检查等等…… 所以对 node 这块也有一些了解。



    • 能不能从技术的角度讲一下你工作中负责业务的复杂度?



      • 因为前两份工作中做的是传统的 B 端项目和 C 端项目,并没有什么可以深挖的技术难点,所以这里只说了第三份工作负责的项目,这是一个协同文档,既不算 B 端,也不算 C 端,这是一款企业级的多人协作数据平台,竞品有腾讯文档、飞书文档、语雀、WPS、维卡表格等等。


        协同文档在前端的难点主要有两个方面:



        1. 实时协同编辑的处理:当两个人同时进入一个单元格编辑内容,如果保证两个人看到的视图是同步的?那么这个时候就要提到冲突处理了,冲突处理的解决方案其实已经相对成熟,包括:



          • 编辑锁:当有人在编辑某个文档时,系统会将这个单元格锁定,避免其他人同时编辑,这种方法实现方式最简单,但也会直接影响用户体验。

          • diff-patch:基于 Git 等版本管理类似的思想,对内容进行差异对比、合并等操作,也可以像 Git 那样,在冲突出现时交给用户处理。

          • 最终一致性实现:包括 Operational Transformation(OT)、 Conflict-free replicated data type(CRDT,称为无冲突可复制数据类型)。



        2. 性能问题



          • 众所周知,互联网一线大厂的协同文档工具都是基于 canvas 实现,并且有一套自己的 canvas 渲染引擎,但是我们没有,毕竟团队规模没法跟大厂比,这个项目前端就 2 个人,所以只能用 dom 堆起来(另一个同事已经跑路,现在就剩下我一个人了)。这导致页面卡顿问题非常严重,即使做了虚拟滚动,但是也没有达到很好的优化效果。老板的要求是做到十万量级的数据,但是实际上几千行就非常卡了,根本原因是数据量太大(相当于一张很大的 Excel 表格,里面的每一个单元格都是一个富文本编辑器),渲染任务多,导致内存开销太大。目前没有很好的解决方案,如果需要彻底解决性能问题,那么就需要考虑用 canvas 重写,但是这个基本上不太现实。

          • 因为卡顿的问题,暴露出来另一个问题,状态更新时,视图同步缓慢,所以这时候不得不提到另一个优化策略:乐观更新。乐观更新的思想是,当用户进行交互的时候,先更新视图,然后再向服务端发送请求,如果请求成功,那么什么都不用管,如果请求失败,那么就回滚视图。这样做的好处是,用户体验会好很多,在一些强交互的场景,不会阻塞用户操作,比如抖音的点赞就是这样做的。但是也会带来一些问题,比如:如果用户在编辑某个单元格时,另一个用户也在编辑这个单元格,那么就会出现冲突,这个时候就需要用到前面提到的冲突处理方案了。







    • 可以讲一下你在工作中技术上的建设吗?



      • 这里讲了一下对 hooks 仓库的建设,封装了 100 多个功能 hook业务 hook,把不变的部分隐藏起来,把变化的部分暴露出去,在业务中无脑传参即可,让业务开发更加简单,同时也提高了代码的复用性。然后讲了一下数据流重构之类的 balabala……



    • 你有什么想问我的吗?



      • 问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。





    结束之后不到 20 分钟,对方就在 Boss 上回复我说面试过了,然后约了二面。



    二面:



    • 自我介绍



      • 跟上一轮一样,大概说了两分钟,介绍了过往工作经历,做过的业务以及技术栈。



    • 在 js 中原型链是一个很重要的概念,你能介绍一下它吗?



      • 要介绍原型链,首先要介绍一下原型,原型是什么…… 这块是纯八股,懒得打字了,直接省略吧。



    • object 的原型指向谁?



      • 回答了 null。(我也不知道对不对,瞎说的)



    • 能说一下原型链的查找过程吗?



      • 磕磕绊绊背了一段八股文,这里省略吧。



    • node 的内存管理跟垃圾回收机制有了解过吗?



      • 暗暗窃喜,这个问题问到点子上了,因为两年前被问到过,所以当时专门写了一篇文章,虽然已经过去两年了,但还是背的滚瓜烂熟:

      • 首先分两种情况:V8 将内存分成 新生代空间老生代空间



        • 新生代空间: 用于存活较短的对象



          • 又分成两个空间: from 空间 与 to 空间

          • Scavenge GC 算法: 当 from 空间被占满时,启动 GC 算法



            • 存活的对象从 from space 转移到 to space

            • 清空 from space

            • from space 与 to space 互换

            • 完成一次新生代 GC





        • 老生代空间: 用于存活时间较长的对象



          • 新生代空间 转移到 老生代空间 的条件(这个过程称为对象晋升



            • 经历过一次以上 Scavenge GC 的对象

            • 当 to space 体积超过 25%



          • 标记清除算法:标记存活的对象,未被标记的则被释放



            • 增量标记:小模块标记,在代码执行间隙执,GC 会影响性能

            • 并发标记:不阻塞 js 执行









    • js 中的基础类型和对象类型有什么不一样?



      • 基础类型存储在栈中,对象类型存储在堆中。



    • 看你简历上是用 React,你能简单的介绍一下 hooks 吗?



      • 本质上就是一个纯函数,大概介绍了一下 hooks 的优点,以及 hooks 的使用规则等等。



    • 简单说一下 useEffect 的用法:



      • useEffect 可以代替 class 中的一些生命周期,讲了一下大概用法,然后讲了一下 useEffect 的执行时机,以及 deps 的作用。



    • 说一下 useEffect 的返回值用来做什么?



      • 返回一个函数,用来做清除副作用的工作,比如:清除定时器清除事件监听等等。



    • 你知道 useEffect 第二个参数内部是怎么比较的吗?



      • 说了一下内部是浅比较,源码中用 for 循环配合 Object.is 实现。(感觉这个问题就是在考察有没有读过 React 源码)



    • 前端的话可能跟网络打交道比较多,网络你了解多少呢?



      • 这里直接坦诚的说了一下,网络是我的弱项,前面一面也问到了网络七层模型,没回答出来。



    • 那你回去了解过七层模型吗?我现在再问你一遍,你能回答出来吗?



      • 磕磕绊绊回答出来了。



    • 追问:http 是在哪一层实现的?



      • 应用层。



    • 说一下 getpost 有什么区别?



      • 两眼一黑,脑子一片空白,突然不知道说什么了,挤了半天挤出来一句:get 大多数情况下用来查询,post 大多数情况下用来提交数据。get 的入参拼在 url 上,post 请求的入参在 body 里面。面试官问我还有其它吗?我说想不起来了……



    • 说一下浏览器输入 url 到页面加载的过程:



      • 输入网址发生以下步骤:



        1. 通过 DNS 解析域名的实际 IP 地址

        2. 检查浏览器是否有缓存,命中则直接取本地磁盘的 html,如果没有命中强缓存,则会向服务器发起请求(先进行下一步的 TCP 连接)

        3. 强缓存协商缓存都没有命中,则返回请求结果

        4. 然后与 WEB 服务器通过三次握手建立 TCP 连接。期间会判断一下,若协议是 https 则会做加密,如果不是,则会跳过这一步

        5. 加密完成之后,浏览器发送请求获取页面 html,服务器响应 html,这里的服务器可能是 server、也可能是 cdn

        6. 接下来是浏览器解析 HTML,开始渲染页面



      • 顺便说了渲染页面的过程:



        1. 浏览器会将 HTML 解析成一个 DOM 树,DOM 树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

        2. 将 CSS 解析成 CSS Rule Tree(css 规则树)。

        3. 解析完成后,浏览器引擎会根据 DOM 树CSS 规则树来构造 Render Tree。(注意:Render Tree 渲染树并不等同于 DOM 树,因为一些像 Headerdisplay:none 的东西就没必要放在渲染树中了。)

        4. 有了 Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的 CSS 定义以及他们的从属关系。下一步进行 layout,进入布局处理阶段,即计算出每个节点在屏幕中的位置。

        5. 最后一个步骤就是绘制,即遍历 RenderTree,层绘制每个节点。根据计算好的信息绘制整个页面。



      • 渲染完成之后,开始执行其它任务:



        1. dom 操作

        2. ajax 发起的 http 网络请求等等……

        3. 浏览器处理事件循环等异步逻辑等等……





    • 菜单左中右布局,两边定宽,中间自适应,说一下有几种实现方式



      • 比较经典的面试题,说了 flexfloat 两种方式。



    • 项目难点



      • 和一面一样,说了协同文档的两大难点,这里就不重复了。



    • 你有什么想问我的吗?



      • 和一面一样,问了一下面试结果大概多久能反馈给我,对方说两三天左右,然后就结束了。



    • 最后问了期望薪资什么的,然后就结束了。


    二面结束之后,大概过了几个小时,在 Boss 上跟对方说了一声,如果没过的话也麻烦跟我说一下,然后这时候,对方在 Boss 上问我,第一学历是不是专科?我说是的,感觉到不太妙的样子,


    然后又过了一会儿,对方说定级应该不会高,他后续看一下面试官的反馈如何……


    然后又追问我,换工作的核心诉求是涨薪还是能力的提升,这里我回答的比较委婉,其实两个都想要 QAQ


    今天已经是第二天了,目前没有下文,看起来二面是过了,但是因为学历不够,中止了三面的流程,基本上是失败了,我也不会报有什么希望了,所以写个面经记录一下。


    作者:三年没洗澡
    来源:juejin.cn/post/7239715208792342584
    收起阅读 »

    创新故事:食堂有了扫脸支付后,我扔掉了工牌

    今天跟大家分享一个创新故事。 我们公司原来一直是有工牌的,工牌除了刷门禁还能够刷各种餐饮,取夜宵等等。 当然这种也是最早时期的工牌了,随后公司就上线了一个方式,就是可以用扫脸来刷门禁,不管在阿里巴巴的大园区还是在各个分公司都有扫脸的工具。 那时候我就想对于科...
    继续阅读 »

    今天跟大家分享一个创新故事。



    我们公司原来一直是有工牌的,工牌除了刷门禁还能够刷各种餐饮,取夜宵等等。


    当然这种也是最早时期的工牌了,随后公司就上线了一个方式,就是可以用扫脸来刷门禁,不管在阿里巴巴的大园区还是在各个分公司都有扫脸的工具。


    那时候我就想对于科技公司来说,增加一个扫脸工具并不是什么难事儿,但是会解决了很多问题,比如工牌找不到了,或者忘记带工牌了,这个时候。匆匆忙忙又回家取工牌特别的麻烦,有的扫脸工具就24小时通行无忧了。


    但是还是有一点那个在食堂支付的时候还是需要刷工牌,这个从扫脸门禁上线后,持续了一年多还是没有扫脸支付餐饮。


    所以如果工牌没带刷门禁是可以进公司的,但是要吃饭或者拿夜宵,还是要去借一个临时的工牌再去刷卡。


    我当时就有这么一个疑问,为什么这种食堂或者吃夜宵的地方不能够也用刷脸工具呢?我当时自己给他解释是支付可能比较敏感,用人脸识别可能存在风险,另外一点就是食堂系统和公司的系统没有打通问题,毕竟食堂系统是公司找的外包服务商。


    总而言之,我把这个场景给预设很难了,用自己的想法去认为当前存在是合理的,而且是很难改变的,要不然也不至于一年多了还是依赖工牌。


    这个就是面对于创新的一个非常大的一个思维局限性,就是我们会预设当前的问题没有解决是存在瓶颈,把当前的困难合理化了,而不加以推测或者说去质疑当前的解决方案。


    随后过了差不多几个月之后,我发现食堂的刷餐饮的也支持扫脸支付了。


    所以这个就给我打了一个脸,我原来自己给出了一个理由和解释,其实完全的不成立,只是因为各种原因,比如说系统各方面的原因没有支持而已,并不是支持不了。而且真正要支持起来也非常的快,我发现在用扫脸支付餐饮的时候的人已经非常多了,说明了这个需求本来就是一个大众型的需求。


    但这个需求却足足等了至少有一两年才全部上线。


    所以从现在来看,这个问题其实是一个非常刚需的问题,也是被很多用户视为痛点的问题,但是却花了一到两年才覆盖和普及。当然这也说明了这个需求可能就不是那么特别阻断性的,因为假如你没有带卡,你也可以取临时工牌,甚至很多人干脆就不在这里支付了,有各种备选的方案。


    但这个例子还是告诉我们身边的创新点其实非常的多,就看我们愿不愿意做,就看有没有投入产出比。


    但是不得不说这样的一个微小的创新就能够使我完全不依赖工牌,轻轻松松无卡上班,吃饭,回家。有了这个扫脸工具以后,我就可以彻底扔掉了工牌。


    作者:ali老蒋
    来源:juejin.cn/post/7307090059354669066
    收起阅读 »

    简历中不写年龄、毕业院校、预期薪资会怎样?

    无意中看到一条视频,点赞、转发量都非常高,标题是“不管你有多自信,简历中的个人信息都不要这样写”。看完之后简直有些无语,不仅哗众取宠,甚至会误导很多人。 之所以想写这篇文章,主要是分享给大家一种思维方式:如果别人说的事实或观点,只有情绪、结论,没有事实依据和推...
    继续阅读 »

    无意中看到一条视频,点赞、转发量都非常高,标题是“不管你有多自信,简历中的个人信息都不要这样写”。看完之后简直有些无语,不仅哗众取宠,甚至会误导很多人。


    之所以想写这篇文章,主要是分享给大家一种思维方式:如果别人说的事实或观点,只有情绪、结论,没有事实依据和推导,那么这些事实和观点是不足信的,需要慎重对待。


    视频的内容是这样的:“不管你有多自信,简历中的个人信息都不要这样写。1、写了期望薪资,错!2、写了户籍地址,错!3、写了学历文凭,错!4、写了离职原因,错!5、写了生日年龄,错!6、写了自我评价,错!


    正确写法,只需要写姓名和手机号、邮箱及求职意向即可,简历个人信息模块的作用是让HR顺利联系到你,所有任何其他内容都不要写在这里……”


    针对这条视频的内容,有两个不同的表现:第一就是分享和点赞数量还可以,都破千了;第二就是评论区很多HR和求职着提出了反对意见。


    第一类反对意见是:无论求职者或HR都认为这样的简历是不合格的,如果不提供这些信息,根本没有预约面试的机会,甚至国内的招聘平台的简历模板都无法通过。第二类,反对者认为,如果不写这些信息,特别是预期薪资,会导致浪费双方的时间。


    针对上述质疑,作者的回复是:”看了大家的评论,我真的震惊,大家对简历的误解是如此至深……“


    仔细看完视频和评论,在视频的博主和评论者之间产生了一个信息差。博主说的”个人信息“不要写,给人了极大的误导。是个人信息栏不要写,还是完全不写呢?看评论,大多数人都理解成了完全不写。博主没有说清楚是不写,还是写在别处,这肯定是作者的锅。


    本人也筛选过近千份简历,下面分享一下对这则视频中提到的内容的看法:


    第一,户籍、离职原因可以不写


    视频中提到的第2项和第4项的确可以不写。


    户籍这一项,大多数情况下是可以不写的,只用写求职城市即可,方便筛选和推送。比如,你想求职北京或上海的工作,这个是必须有的,而你的户籍一般工作没有强制要求。但也有例外,比如财务、出纳或其他特殊岗位,出于某些原因,某些公司会要求是本地的。写不写影响没那么大。


    离职原因的确如他所说的,不建议写,是整个简历中都不建议写。这个问到了再说,或者填写登记表时都会提到,很重要,要心中有准备,但没必要提前体现。


    第二,期望薪资最好写上


    关于期望薪资这个有两种观点,有的说可以不写,有的说最好写上。其实都有道理,但就像评论中所说:如果不写,可能面试之后,薪资相差太多,导致浪费了双方的时间。


    其实,如果可以,尽量将期望薪资写上,不仅节省时间,这里还稍微有一个心理锚定效应,可以把薪资写成范围,而范围的下限是你预期的理想工资。就像讨价还价时先要一个高价,在简历中进行这么一个薪资的锚定,有助于提高最终的薪资水平。


    第三,学历文凭一定要写


    简历中一定要写学历文凭,如果没有,基本上是会默认为没有学历文凭的,是不会拿到面试邀约的。仔细想了一下,那则视频的像传达的意思可能是不要将学历文凭写作个人信息栏,而是单独写在教育经历栏中。但视频中没有明说,会产生极大的误导。


    即便是个人信息栏,如果你的学历非常漂亮,也一定要写到个人信息栏里面,最有价值,最吸引眼球的信息,一定要提前展现。而不是放在简历的最后。


    第四,年龄要写


    视频中提到了年龄,这个是招聘衡量面试的重要指标,能写尽量写上。筛选简历中有一项非常重要,就是年龄、工作经历和职位是否匹配。在供大于求的市场中,如果不写年龄,为了规避风险,用人方会直接放弃掉。


    前两个月在面试中,也有遇到因为年龄在30+,而在简历中不写年龄的。作为面试官,感觉是非常不好的,即便不写,在面试中也需要问,最终也需要衡量年龄与能力是否匹配的问题。


    很多情况下,不写年龄,要么认为简历是不合格的,拿不到面试机会,要么拿到了面试机会,但最终只是浪费了双方的时间。


    第五,自我评价


    这一项与文凭一样,作者可能传达的意思是不要写在个人信息栏中,但很容易让人误解为不要写。


    这块真的需要看情况,如果你的自我评价非常好,那一定要提前曝光,展现。


    比如我的自我评价中会写到”全网博客访问量过千万,CSDN排名前100,出版过《xxx》《xxx》书籍……“。而这些信息一定要提前让筛选简历的人感知到,而不是写在简历的最后。


    当然,如果没有特别的自我评价,只是吃苦耐劳、抗压、积极自主学习等也有一定的积极作用,此时可以考虑放在简历的后面板块中,而不是放在个人信息板块中。这些主观的信息,更多是一个自我声明和积极心态的表现。


    最后的小结


    经过上面的分析,你会看到,并不是所有的结论都有统一的标准的。甚至这篇文章的建议也只是一种经验的总结,一个看问题的视角而已,并不能涵盖和适用所有的场景。而像原始视频中那样,没有分析,没有推导,没有数据支撑,没有对照,只有干巴巴的结论,外加的煽动情绪的配音,就更需要慎重对待了。


    在写这篇文章的过程中,自己也在想一件事:任何一个结论,都需要在特定场景下才能生效,即便是牛顿的力学定律也是如此,这才是科学和理性的思维方式。如果没有特定场景,很多结论往往是不成立的,甚至是有害的。


    作者:程序新视界
    来源:juejin.cn/post/7268593569782054967
    收起阅读 »

    前几天有个雏鹰问我,说怎么创建Menu???

    这个很简单了哈,直接上代码算了 自己在这个路径下面创建一个这个的这个这个这个,很直观吧 <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.a...
    继续阅读 »

    这个很简单了哈,直接上代码算了
    自己在这个路径下面创建一个这个的这个这个这个,很直观吧


    截屏2023-11-29 22.13.12.png


    <?xml version="1.0" encoding="utf-8"?>
    <menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <item
    android:id="@+id/list_view"
    android:title="@string/listview">

    <menu>
    <item
    android:id="@+id/list_view_vertical_only"
    android:title="垂直标准"
    tools:ignore="DuplicateIds" />

    <item
    android:id="@+id/list_view_vertical_reverse"
    android:title="垂直反向" />

    <item
    android:id="@+id/list_view_horizontal_only"
    android:title="水平标准" />

    <item
    android:id="@+id/list_view_horizontal_reverse"
    android:title="水平反转" />

    </menu>
    </item>
    </menu>

    然后读取目录路面的条目的时候有一个过滤器,把你自己添加的目录放进来,点击事件也帮你写好了,里面想怎么整自己搞,


    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.menu, menu);
    return super.onCreateOptionsMenu(menu);
    }

    @SuppressLint("NonConstantResourceId")
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
    int itemId = item.getItemId();
    if (itemId != 0)
    switch (itemId){
    case R.id.list_view:
    break;
    case R.id.list_view_vertical_only:
    break;
    case R.id.list_view_vertical_reverse:
    break;
    case R.id.list_view_horizontal_only:
    break;
    case R.id.list_view_horizontal_reverse:
    break;
    }
    return super.onOptionsItemSelected(item);
    }

    结束结束,希望下次雏鹰可以自己看,或者自己搜下,很简单的东西


    作者:贾炬山
    来源:juejin.cn/post/7306706954678763556
    收起阅读 »

    可以,很6!微信这波改造,一刀斩断了一条“灰色”业务线。

    你好呀,我是歪歪。 微信官方今天“悄悄咪咪”的搞了一个小动作,而这个小动作我关注了接近两个月时间,今天终于是上线了。 对微信来说,这也许就是一个小迭代。 这个迭代对于用户来说,算是一个利好的动作,体现了微信订阅号“以用户为中心”的指导思想。但是对于一些行业来说...
    继续阅读 »

    你好呀,我是歪歪。


    微信官方今天“悄悄咪咪”的搞了一个小动作,而这个小动作我关注了接近两个月时间,今天终于是上线了。


    对微信来说,这也许就是一个小迭代。


    这个迭代对于用户来说,算是一个利好的动作,体现了微信订阅号“以用户为中心”的指导思想。但是对于一些行业来说,它直接是一刀斩断了一条业务线。


    看我说的这么牛逼,那么到底是啥迭代呢?


    我还是先给你上示例个图片吧:



    这个是充值话费之后,运营商的官方账号会推送一条消息给我,告知我话费充值成功。这条消息的官方叫法叫做:微信模板消息。


    但是你仔细一看,会发现这个模板消息里面夹带了一个私货“备注”字段。这个“备注”字段的内容和我本次充值的动作没有任何关系,是一个纯粹的营销动作。


    今天,我同样进行了充值的动作,但是收到的消息是这样的:



    一眼望去,全是变化。


    这样对比起来看更加明显:



    对于用户来说,确实是清爽了不少。微信官方完全屏蔽了一些“营销”推送对于用户的打扰。


    看起来确实是一个小改造,至少站在开发的角度,不过是隐藏了部分字段而已。


    但是我想聊一下关于这个改造,我知道的一点背后的故事。


    需要提前说明的是,由于我是在工作中密切接触微信模板消息的,所以洋洋洒洒写了这么一篇文章。


    如果你没有接触过这块内容的话,那么可能看起来觉得有点莫名其妙,很正常,顺便划拉两下走人得了。


    反复横跳


    因为我在公司里面就负责对客微信消息推送的功能,而且我们推送给客户的消息都是非常关键的业务消息,一旦这个对客触达环节整个断开,势必要领个生产事件的。


    所以对于微信官方的这个“小迭代”我是密切关注,非常害怕由于微信改动之后,我们配套改动不及时,导致对客微信消息推送失败。


    在整个过程中,如果让我用一个词来形容这次变更的话,那么就是四个字:反复横跳。


    光是关于这次变化的官方公告,就前后发了三次,后两次都是对于第一次的补丁:



    从官方推送上来看,只有两次公告,还有一次我一会说,我先给你盘一下这两次公告到底是在干啥。


    首先 3 月 24 日发布了第一条公告,公告里面第一段话是这样的:



    公众号的模板消息能力旨在方便开发者为用户提供服务凭证通知。近期平台发现,部分公众号下发的模板消息涉及包括但不限于商品营销、服务推广等内容。且利用颜色/表情符号诱导用户查看、点击。上述行为损害了用户消息接收体验,给用户带来极大困扰,引起大量用户投诉。



    然后官方提供了两个案例。


    第一个案例是这样的:



    这个案例和我刚刚举的充话费的案例其实是一回事儿。


    在用户触发了业务场景之后,对客户进行了微信模板消息的触达,但“备注”字段的内容是营销推广内容,同时在里面使用了自定义颜色、emoji 表情符号,意图诱导用户进行点击。


    u1s1,我作为微信用户,这种夹带私货的推送我勉勉强强能够接受。当然了,营销的部分完全被去掉了,当然是更好的。


    我不能接受的是官方下面给出的这种案例:



    你有没有看出什么端倪?


    使用的是官方的“退款成功通知”模板,但是内容是纯纯的营销推广内容,这不就是挂羊头卖狗肉吗?


    甚至更加过分的是,对于一个类型为服务号的微信公众号来说,他下发这种模板消息给用户,并不需要用户触发业务场景。换句话说,不需要经过用户同意直接发就完事了。


    你想想,你正在打团呢,突然微信弹出一个“退款成功通知”,你一脸懵逼的切过去,发现是营销内容,恶狠狠的骂几句之后切回游戏,发现已经团灭了。


    你说遭不遭得住?


    遭不住,对不对。



    但是,注意,我要说但是了。


    这个地方其实就是存在一条产业链的。有的品牌运营方,专门找服务号去推送这种模板消息,给号主一个难于拒绝的价格,然后推送一条“扰民的”、“灰色的”消息:



    我理解微信可能更想解决这类问题,斩断这个在“灰色地带”的产业链,因为它确实是扰民,而且营销的内容确实是灰色的,甚至违法的。


    但是微信采取的方案是:掀桌子,大家都别玩了。


    快刀斩乱麻,一刀切掉“备注字段”、“自定义颜色”、“emoji表情”,完事:



    同时官方说这个变更会在 3 月 30 日开始生效:



    我看到这个公告并仔细研读了公告的内容后,确定会对我们产生一定的业务影响,我第一时间拉了个小群,把这个消息同步给了对应负责的业务同事,他们在当天就在内部同步了这个信息。


    然后就开始盼星星,盼月亮的等 3 月 30 日的到来,想看看微信这波改造之后的效果是怎么样的。


    时间很快,来到了 3 月 30 日凌晨,我迫不及待的做了一个测试,发现并没有发生任何变化,备注字段还在:



    想着也正常,微信这么大的体量,肯定是有一个逐步覆盖的过程。


    于是我在 3 月 30 日中午 11 点又做了一个测试:



    发现还是没啥动静,开始觉得事情可能有变。果然在中午 12 点 33 分,收到的官方的补丁通知:



    这次的公告内容如下:



    带来一个好消息和坏消息。


    好消息是改造生效的时间推迟了一个月。


    坏消息是这次补丁公告直接把首行内容也噶了,一顿阉割之后,只剩下了最纯粹的通知属性。


    于是等啊等,终于等到了 5 月 4 日,早中晚几个时间段一顿测试之后,发现并没有生效。


    然后在晚上 22 点 38 分,等来一个通知:



    这个通知就是关于模板消息的第三次通知。通知里面第四点,也是打了一个补丁。


    然后,5 月 5 日,全面生效了。


    可以说是反复横跳了。


    另外,我还发现在“微信公众平台运营中心”公众号中针对“单个中间主内容不超过 20 字”对应的内容并没有单独推文说明,而是通过在 5 月 5 日修改 3 月 30 日推文的方式来进行“打补丁”:



    哦,对了,说到文章最开始的话费充值,由于我是直接用的微信钱包充值的,所以我同时收到了这样的一条推送:



    咱也不知道啥情况啊,为什么“腾讯手机充值”的头部信息和备注信息还在呢?


    我猜应该是还在逐步覆盖中吧,微信不可能因为是腾讯旗下自家的号,就区别对待的。


    你说对吧。



    一点思考


    我再带你捋一捋这个事情的全过程。


    首先,我们抛开字段颜色和 emoji 表情不说。


    3 月 24 日的时候,官方说要变,但是只动备注字段,3 月 30 日生效。


    3 月 30 日的时候,官方说我们再缓一缓吧,5 月 4 日生效,但是这次我要把首行内容也噶了。


    5 月 4 日的时候,官方说马上就生效了,对了,还有一个“小改动”,就是其他字段内容不超过 20 个字,且不支持换行哦。


    5 月 5 日,生效。


    最后一个补丁从晚上发出通知,到第二天一早生效,只过了一晚上的时间。


    但是这个补丁,刚好就是我们之前想到的应对之策。可以把业务字段扩长,然后达到类似的引导用户点击的效果:



    当时也测试了,确实是可以的。


    结果,突然一下,啪的一声,方案没用了。


    有一种被官方绝杀了的感觉。


    在整个过程中,我作为一个在工作中使用了模板消息的开发者,感受到的是“不专业”,感觉整个事件从提出到落地,都是拍脑子,没有想清楚细节就开始搞事情,导致一个事情反复打补丁,反复被开发者吐槽。


    但是据我考察,实际情况是,微信官方想动“模板消息”已经是想了很久了,比如我就找到了 2021 年 1 月 27 日官方发布的这个公告:



    developers.weixin.qq.com/community/d…




    这个灰度测试对应的方案吧,我只能说...


    算了,我还是不说了吧,截个该公告下的评论:



    既然两年前就打算动这个东西了,两年后真的动起手来的时候,还是搞成这样。


    哎,一言难尽,一言难尽啊。


    在这个过程中,我最害怕的还是微信突然发公告说,开发者也需要做对应的改动,比如如果调用接口的时候传递了备注字段,则不会发送给客户。幸好,这次并没有出现这种情况,不然我真的会好好的“谢谢”提出这个需求的 PM。


    这样“不专业”的感受更多的还体现在官方的接口文档中,在全过程中,截至我写文章的时候,官方的接口文档对于首行(first.DATA)和备注(remark.DATA)字段,一直在变化,但是一直都没有处理干净:



    developers.weixin.qq.com/doc/offiacc…





    同时,对于最后一次补丁公告中的“中间的主内容中,单个字段内容不超过 20 个字,且不支持换行”这部分描述,在接口文档中没有任何的体现。


    别问,问就是自己去试,或者靠得就是一个口口相传。



    然后还有一个感受是和大家的感受相同的:



    一刀切,确实很简单。但是对于开发者来说,里面少了一点关怀,多了一点躺枪。因为大部分开发者基于“备注”字段做的都是对于这次消息推送的进一步说明,而不是对客营销。


    对于这部分开发者来说,官方的这次阉割是比较致命的。


    所以更加人性的做法应该是谁滥用,就惩罚谁。而不是采取乌鸦哥行为:



    最后的一个感悟,也是最大的一个感悟:对于依托微信服务号模板消息来触达客户,开展业务的公司来说,还是咬咬牙做自己的 APP 吧。虽然开发和运营成本上去了,至少触达客户的时候,字段不会被说砍就砍。


    自主研发,总比卡脖子好。


    作者:why技术
    来源:juejin.cn/post/7229895584038305851
    收起阅读 »

    Android APP合规检查,你可能需要这个工具~

    虽迟但到,这是一个通过拦截Java方法调用用以检测应用是否合规的工具,如果你的APP正饱受监管部门或应用市场时不时下发整改通知的折磨,那么用它来检查你的代码以及引用的三方库是再好不过的选择了! 如何引入 Step 1. 添加 mavenCentral all...
    继续阅读 »

    logo.png


    虽迟但到,这是一个通过拦截Java方法调用用以检测应用是否合规的工具,如果你的APP正饱受监管部门或应用市场时不时下发整改通知的折磨,那么用它来检查你的代码以及引用的三方库是再好不过的选择了!


    如何引入


    Step 1. 添加 mavenCentral


    allprojects {
    repositories {
    ...
    mavenCentral()
    }
    }

    Step 2. 添加 Gradle 依赖


    dependencies {
    ...
    implementation 'io.github.loper7:miit-rule-checker:0.1.1'
    }

    如何使用


    检查APP内是否存在不合规的方法调用



    检查MIITRuleChecker内置的不合规的方法,具体可见下方方法列表



     MIITRuleChecker.checkDefaults()


    如果内置的方法不满足当前需求,可自定义方法添加到list中进行检查;

    比如新增一个 MainActivity 的 onCreate 方法的调用检查;



    val list = MIITMethods.getDefaultMethods()
    list.add(MainActivity::class.java.getDeclaredMethod("onCreate" , Bundle::class.java)) MIITRuleChecker.check(list)

    当然,如果你想检查多个内置方法外的方法,只需要创建一个新的集合,往集合里放你想检查的方法member,然后传入 MIITRuleChecker.check()内即可。


    log打印如下所示:


    method_androidid.png


    检查指定方法调用并查看调用栈堆


    //查看 WifiInfo classgetMacAddress 的调用栈堆
    MIITRuleChecker.check(MIITMethods.WifiInfo.getMacAddress)

    log打印如下所示:


    method_macaddress.png


    检查一定时间内指定方法调用次数统计


    //多个方法统计 (deadline 为从方法调用开始到多少毫秒后截至统计)
    val list = mutableListOf().apply {
    add(MIITMethods.LocationManager.getLastKnownLocation)
    add(MIITMethods.LocationManager.requestLocationUpdates)
    add(MIITMethods.Secure.getString)
    }
    MIITMethodCountChecker.startCount(list , 20 * 1000)

    //单个方法统计(deadline 为从方法调用开始到多少毫秒后截至统计)
    MIITMethodCountChecker.startCount(MIITMethods.LocationManager.getLastKnownLocation , deadline = 20 * 1000)

    log打印如下所示:


    log_count.png


    检查完成并完成整改后务必移除方法 miit-rule-checker 库内的所有方法调用,将库一起移除最好


    内置方法表


    内置常量对应的系统方法备注
    MIITMethods.WifiInfo.getMacAddressandroid.net.wifi.WifiInfo.getMacAddress()获取MAC地址
    MIITMethods.WifiInfo.getIpAddressandroid.net.wifi.WifiInfo.getIpAddress()获取IP地址
    MIITMethods.LocationManager.getLastKnownLocationandroid.location.LocationManager.getLastKnownLocation(String)获取上次定位的地址
    MIITMethods.LocationManager.requestLocationUpdatesandroid.location.LocationManager.requestLocationUpdates(String,Long,Float,LocationListener)
    MIITMethods.NetworkInterface.getHardwareAddressjava.net.NetworkInterface.getHardwareAddress()获取主机地址
    MIITMethods.ApplicationPackageManager.getInstalledPackagesandroid.app.ApplicationPackageManager.getInstalledPackages(Int)获取已安装的应用
    MIITMethods.ApplicationPackageManager.getInstalledApplicationsandroid.app.ApplicationPackageManager.getInstalledApplications(Int)获取已安装的应用
    MIITMethods.ApplicationPackageManager.getInstallerPackageNameandroid.app.ApplicationPackageManager.getInstallerPackageName(String)获取应用安装来源
    MIITMethods.ApplicationPackageManager.getPackageInfoandroid.app.ApplicationPackageManager.getPackageInfo(String,Int)获取应用信息
    MIITMethods.PackageManager.getInstalledPackagesandroid.content.pm.PackageManager.getInstalledPackages(Int)获取已安装的应用
    MIITMethods.PackageManager.getInstalledApplicationsandroid.content.pm.PackageManager.getInstalledApplications(Int)获取已安装的应用
    MIITMethods.PackageManager.getInstallerPackageNameandroid.content.pm.PackageManager.getInstallerPackageName(String)获取应用安装来源
    MIITMethods.PackageManager.getPackageInfoandroid.content.pm.PackageManager.getPackageInfo(String,Int)获取应用信息
    MIITMethods.PackageManager.getPackageInfo1android.content.pm.PackageManager.getPackageInfo(String,PackageInfoFlags)获取应用信息(版本号大于33)
    MIITMethods.PackageManager.getPackageInfo2android.content.pm.PackageManager.getPackageInfo(VersionedPackage,Int)获取应用信息(版本号大于26)
    MIITMethods.PackageManager.getPackageInfo3android.content.pm.PackageManager.getPackageInfo(VersionedPackage,PackageInfoFlags)获取应用信息(版本号大于33)
    MIITMethods.Secure.getStringandroid.provider.Settings.Secure.getString(ContentResolver,String)获取androidId
    MIITMethods.TelephonyManager.getDeviceIdandroid.telephony.TelephonyManager.getDeviceId()获取 DeviceId
    MIITMethods.TelephonyManager.getDeviceIdWithIntandroid.telephony.TelephonyManager.getDeviceId(Int)获取 DeviceId
    MIITMethods.TelephonyManager.getImeiandroid.telephony.TelephonyManager.getImei()获取 Imei
    MIITMethods.TelephonyManager.getImeiWithIntandroid.telephony.TelephonyManager.getImei(Int)获取 Imei
    MIITMethods.TelephonyManager.getSubscriberIdandroid.telephony.TelephonyManager.getSubscriberId()获取 SubscriberId

    作者:LOPER7
    来源:juejin.cn/post/7307470097663688731
    收起阅读 »

    这就是为什么我不接受你是Senior软件工程师

    软件行业的一个令人担忧的趋势 工程类职位的面试极其复杂, 压力大, 而且我必须为所有敢于接受面试以实现职业梦想的人鼓掌, 并对他们表示赞赏. 过去几年里, 我一直在为不同的公司面试工程师, 但最近, 我前所未有地拒绝了很多应聘者. 我还是用同样的问题, 没有任...
    继续阅读 »

    软件行业的一个令人担忧的趋势


    工程类职位的面试极其复杂, 压力大, 而且我必须为所有敢于接受面试以实现职业梦想的人鼓掌, 并对他们表示赞赏.


    过去几年里, 我一直在为不同的公司面试工程师, 但最近, 我前所未有地拒绝了很多应聘者. 我还是用同样的问题, 没有任何变化, 问题是人们根本不知道高级工程师意味着什么, 而这些专业人员比以往任何时候都更稀缺.


    编程 10 年并不能让你成为高级工程师. 这与时间无关.


    以下是我的努力, 只为探讨软件开发行业高级工程师究竟意味着什么.



    在互联网上发现的流行笑话.


    什么是高级工程师?


    我向 ChatGPT 询问了关于什么是高级软件工程师的通用描述, 结果它一语中的:



    高级工程师对编程语言, 软件设计原则和开发方法论有深刻的理解.


    — ChatGPT, 在抢走我们的饭碗之前.



    这正是经典高级面试的通用结构:


    开发方法论


    开发方法论是旨在提高团队效率的组织方法. 这些对我们来说可能很枯燥, 但我们希望你能掌握这方面的专业知识.


    在多年的工作中, 我对非敏捷开发方法产生了排斥心理, 不仅如此, 我认为 Scrum 还不够敏捷, 它的详尽使用最终会让项目经理而非程序员的自负增强.



    互联网上的笑话.


    在面试时, 我希望开发人员具备批判能力, 因为仅仅了解 Scrum 是不够的, 你还必须知道它的缺点, 并提出解决方案.


    我还想看看开发人员是否了解 Scrum 和 Kanban 之外的其他方法, 比如 RUP. 常识可以帮助你形成更好的观点, 并表明你愿意学习自己领域以外的知识.


    软件设计原则


    这些原则可能一辈子只读一次就会忘记, 但真正熟练的程序员每天都在使用.



    明星工程师之所以比同行更有价值, 原因并不局限于编程. 伟大的软件工程师具有惊人的创造力, 能看到别人看不到的概念模式.


    Reed Hastings. Netflix 联合创始人.



    每次面试结束后, 在给应聘者写反馈时, 我发誓每次都能复制粘贴同样的回复:



    我建议你多读读 Python 设计模式, 本指南就是一个很好的资源.



    用设计原则筛选候选人真是太容易了...只有在非常奇怪的情况下, 我才会发现有人能回答所有这些问题.



    这是一个笑话, 记录了大多数开发人员在被问及设计模式时的表情.


    当你熟悉了软件设计模式之后, 在实践中实现这些模式可能会相当具有挑战性. 我本人就曾面临过这样的挑战, 因为在编码时回忆并应用它们并非易事. 但是, 强迫自己在每次编程时进行这一检查, 一定会让你领先一步.


    编程语言


    为什么我们在 Python 中使用 len(array) 而在其他语言中使用 array.length()? 这背后有什么优化吗?


    你对自己的语言了解多少?


    在你成长的过程中, 阅读一本关于你所使用的语言的书是绝对必要的. 任何高级工程面试都会包括一些只有通过阅读才能了解的高级问题.


    态度不好扼杀了许多面试机会



    你们能快点提供反馈吗? 现在有其他公司向我发出邀请.



    • 一位候选人在面试时说的话, 当时他有时间向我们提出有关该职位的问题.



    公司在提供高薪的同时, 也在寻找优秀的人才, 而其中的一部分就是做个好人.


    面试时, 我们不仅要评估应聘者的知识, 还要评估她/他的态度. 在我几个月前做的一个具体工作中, 公司特别要求我们寻找"善良, 诚实"的人, 而不是优秀的程序员.


    与一个虚荣的人共事是非常困难的, 他们会让你一直感觉不好, 并在团队中造成不好的氛围. 从长远来看, 这样的人对公司造成的损害可能是非常大的.


    态度恶劣或撒谎可能意味着在选拔过程中被立即淘汰.


    我一直向大家推荐<如何赢得朋友和影响他人>一书, 这是一本每个人都应该读一读的书, 以便在生活中拥有更好的人际关系.



    互联网上找到的笑话.


    记住, 经验丰富的工程师应该能够领导一个团队, 你需要真正强大的社交能力才能胜任这个职位.


    初级/中级开发人员要成为高级开发人员, 应该读些什么?


    这里有一些让你成长为高级开发人员的绝佳资源:


    代码技能如何?


    几个月前, 我购买了LeetCode Premium, 这是我找到现在这份工作的一个伟大决定. 在使用过程中, 你会从其他用户那里发现有趣的数学奇闻, 模式和优化方法.


    模拟大公司的面试也有助于了解他们的组织结构.



    瞄着星星, 才有可能够着天空
    — Reinhold Niebuhr




    Leetcode 的大公司问题列表.


    还有几个网站与 LeetCode 类似, 如AlgoExpertCodeSignal.


    残酷的真相


    即使你阅读了大量的资料, 每天进行练习, 并积累了丰富的经验, 你也有可能因为不符合公司的要求而被拒绝.


    几天前, 我在听一个播客, 他们谈到了一个实验: 让孩子们解决问题, 并把他们分成两组:


    当让他们接触新问题时, 被表扬有毅力的孩子倾向于选择更难的问题来解决, 而那些被说成聪明的孩子最终在他们解决的问题上表现得更差.


    这说明, 你唯一能指望自己做到的就是坚韧不拔和坚持不懈. 这些不仅是保证你未来工作的技能, 也是我们对高级工程师的主要期望.


    作者:bytebeats
    来源:juejin.cn/post/7307723756410896411
    收起阅读 »

    华为自研的前端框架是什么样的?

    web
    大家好,我卡颂。 最近,华为开源了一款前端框架 —— openInula。根据官网提供的信息,这款框架有3大核心能力: 响应式API 兼容ReactAPI 官方提供6大核心组件 并且,在官方宣传视频里提到 —— 这是款大模型驱动的智能框架。 ...
    继续阅读 »

    大家好,我卡颂。


    最近,华为开源了一款前端框架 —— openInula。根据官网提供的信息,这款框架有3大核心能力:



    1. 响应式API




    1. 兼容ReactAPI




    1. 官方提供6大核心组件



    并且,在官方宣传视频里提到 —— 这是款大模型驱动智能框架


    那么,这究竟是款什么样的前端框架呢?我在第一时间体验了Demo,阅读了框架源码,并采访了框架核心开发者。本文将包括两部分内容:



    1. 对框架核心开发者陈超涛的采访

    2. 卡颂作为一个老前端,阅读框架源码后的一些分析

    采访核心开发者


    开发Inula的初衷是?


    回答:


    华为内部对于业务中强依赖的软件,考虑到竞争力,通常会开发一个内部使用的版本。


    Inula在华为内部,从立项到现在两年多,基本替换了公司内绝大部分React项目。



    卡颂补充背景知识:Inula兼容React 95% API,最初开发的目的就是为了替换华为内部使用的React。为了方便理解,你可以将Inula类比于华为内部的React



    为什么开源?


    回答:


    华为对于自研软件的公司策略,只要是公司内部做的,觉得还ok的自研都会开源。



    接下来的提问涉及到官网宣传的内容



    宣传片提到的大模型赋能、智能框架是什么意思?


    回答:


    这主要是Inula团队与其他外部团队在AI低代码方向的一些探索。比如:



    1. 团队与上海交大的一个团队在探索大模型赋能chrome调试业务代码方面有些合作,目的是为了自动定位问题

    2. 团队与华为内部的大模型编辑器团队合作,探索框架与编辑器定制可能性


    以上还都属于探索阶段。


    Inula未来有明确的发展方向么?


    回答:


    团队正在探索引入响应式API,相比于React的虚拟DOM方案,响应式API能够提高运行时性能。24年可能会从Vue composition API中寻求些借鉴。


    新的发展方向会在项目仓库以RFC的形式展开。



    补充:RFCRequest for Comments的缩写。这是一种协作模式,通常用于提出新的特性、规范或者改变现有的一些规则。RFC的目的是收集不同的意见和反馈,以便在最终确定一个决策前,考虑尽可能多的观点和影响。



    为什么要自研核心组件而不用社区成熟方案?



    卡颂补充:所谓核心组件,是指状态管理、路由、国际化、请求库、脚手架这样的框架生态相关的库。既然Inula兼容React,为什么不直接用React生态的成熟产品,而要自研呢?毕竟,这些库是没有软件风险的。




    回答:


    主要还是丰富Inula生态,根据社区优秀的库总结一套Inula官方推荐的最佳实践。至于开发者怎么选择,我们并不强求。


    卡颂的分析


    以上是我对Inula核心开发者陈超涛的采访。下面是我看了Inula源码后的一些分析。


    要分析一款前端框架,最重要的是明白他是如何更新视图的?这里我选择了两种触发时机来分析:



    1. 首次渲染


    触发的方式类似如下:


    Inula.render(<App />, document.getElementById("root"));


    1. 执行useState的更新方法触发更新


    触发的方式类似如下:


    function App() {
    const [num, update] = useState(0);
    // 触发更新
    update(xxx);
    // ...
    }

    顺着调用栈往下看,他们都会执行两步操作:



    1. 创建名为update的数据结构

    2. 执行launchUpdateFromVNode方法


    比如这是首屏渲染时:



    这是useState更新方法执行时:



    launchUpdateFromVNode方法会向上遍历到根结点(源码中遍历的节点叫VNode),再从根节点开始遍历树。由此可以判断,Inula的更新机制与React类似。


    所有主流框架在触发更新后,都不会立刻执行更新,中间还有个调度流程。这个流程的存在是为了解决:



    1. 哪些更新应该被优先执行?

    2. 是否有些更新是冗余的,需要合并在一块执行?


    Vue中,更新会在微任务中被调度并统一执行,在React中,同时存在微任务(promise)与宏任务(MessageChannel)的调度模式。


    Inula中,存在宏任务的调度模式 —— 当宿主环境支持MessageChannel时会使用它,不支持则使用setTimeout调度:



    同时,与这套调度机制配套的还有个简单的优先级算法 —— 存在两种优先级,其中:



    • ImmediatePriority:对应正常情况触发的更新

    • NormalPriority:对应useEffect回调


    每个更新会根据更新的ID(一个自增的数字)+ 优先级对应的数字 作为优先级队列中的排序依据,按顺序执行。


    假设先后触发2次更新,优先级分别是ImmediatePriorityNormalPriority,那么他们的排序依据分别是:



    1. 100(假设当前ID到100了)- 1(ImmediatePriority对应-1) = 99

    2. 101(100自增到101)+ 10000(NormalPriority对应10000)= 10101


    99 < 10101,所以前者会先执行。


    需要注意的是,Inula中对更新优先级的控制粒度没有React并发更新细,比如对于如下代码:


    useEffect(function cb() {
    update(xxx);
    update(yyy);
    })

    React中,控制的是每个update对应优先级。在Inula中,控制的是cb回调函数与其他更新所在回调函数之间的执行顺序。


    这意味着本质来说,Inula中触发的所有更新都是同步更新,不存在React并发更新中高优先级更新打断低优先级更新的情况。


    这也解释了为什么Inula兼容 95% 的React API,剩下 5% 就是并发更新相关API(比如useTransitionuseDeferredvalue)。


    现在我们已经知道Inula的更新方式类似React,那么官网提到的响应式API该如何实现呢?这里存在三条路径:



    1. 一套外挂的响应式系统,类似ReactMobx的关系

    2. 内部同时存在两套更新系统(当前一套,响应式一套),调用不同的API使用不同的系统

    3. 重构内部系统为响应式系统,通过编译手段,使所有API(包括当前的React API与未来的类 Vue Composition API)都走这套系统



    其中第一条路径比较简单,第二条路径应该还没框架使用,第三条路径想象空间最大。不知道Inula未来会如何发展。


    总结


    当前,Inula是一款类React的框架,功能上可以类比为React并发更新之前的版本


    下一步,Inula会引入响应式API,目的是提高渲染效率。


    对于未来的发展,主要围绕在:



    • 探索类 Vue Composition API的可能性

    • 迭代官方核心生态库


    对于华为出的这款前端框架,你怎么看?


    作者:魔术师卡颂
    来源:juejin.cn/post/7307451255432249354
    收起阅读 »

    程序员提高效率的 10 个方法

    1. 早上不要开会 📅 每个人一天是 24 小时,时间是均等的,但是时间的价值却不是均等的,早上 1 小时的价值是晚上的 4 倍。为什么这么说? 因为早晨是大脑的黄金时间,经过一晚上的睡眠,大脑经过整理、记录、休息,此时的状态是最饱满的,适合专注度高的工作,比...
    继续阅读 »

    1. 早上不要开会 📅


    每个人一天是 24 小时,时间是均等的,但是时间的价值却不是均等的,早上 1 小时的价值是晚上的 4 倍。为什么这么说?


    因为早晨是大脑的黄金时间,经过一晚上的睡眠,大脑经过整理、记录、休息,此时的状态是最饱满的,适合专注度高的工作,比如编程、学习外语等,如果把时间浪费在开会、刷手机等低专注度的事情上,那么就会白白浪费早上的价值。


    2. 不要使用番茄钟 🍅


    有时候在专心编程的时候,会产生“心流”,心流是一种高度专注的状态,当我们专注的状态被打破的时候,需要 15 分钟的时候才能重新进入状态。


    有很多人推荐番茄钟工作法,设定 25 分钟倒计时,强制休息 5 分钟,之后再进入下一个番茄钟。本人在使用实际使用这种方法的时候,经常遇到的问题就是刚刚进入“心流”的专注状态,但番茄钟却响了,打破了专注,再次进入这种专注状态需要花费 15 分钟的时间。


    好的替换方法是使用秒表,它跟番茄钟一样,把时间可视化,但却是正向计时,不会打破我们的“心流”,当我们编程专注度下降的时候中去查看秒表,确定自己的休息时间。


    3. 休息时间不要玩手机 📱


    大脑处理视觉信息需要动用 90% 的机能,并且闪烁的屏幕也会让大脑兴奋,这就是为什么明明休息了,但是重新回到工作的时候却还是感觉很疲惫的原因。


    那么对于休息时间内,我们应该阻断视觉信息的输入,推荐:



    • 闭目养神 😪

    • 听音乐 🎶

    • 在办公室走动走动 🏃‍♂️

    • 和同事聊会天 💑

    • 扭扭脖子活动活动 💁‍♂️

    • 冥想 or 正念 🧘


    4. 不要在工位上吃午饭 🥣


    大脑经过一早上的编程劳累运转之后,此时的专注度已经下降 40%~50%,这个时候我们需要去重启我们的专注度,一个好的方法是外出就餐,外出就餐的好处有:



    • 促进血清素分泌:我们体内有一种叫做血清素的神经递质,它控制着我们的睡眠和清醒,外出就餐可以恢复我们的血清素,让我们整个人神经气爽:

      • 日光浴:外出的时候晒太阳可以促进血清素的分泌

      • 有节奏的运动:走路是一种有节奏的运动,同样可以促进血清素分泌



    • 激发场所神经元活性:场所神经元是掌控场所、空间的神经细胞,它存在于海马体中,外出就餐时场所的变化可以激发场所神经元的活性,进而促进海马体活跃,提高我们的记忆力

    • 激活乙酰胆碱:如果外出就餐去到新的餐馆、街道,尝试新的事物的话,可以激活我们体内的乙酰胆碱,它对于我们的“创作”和“灵感”起到非常大的作用。


    5. 睡午觉 😴


    现在科学已经研究表现,睡午觉是非常重要的一件事情,它可以:



    • 恢复我们的身体状态:26 分钟的午睡,可以让下午的工作效率提升 34%,专注力提升 54%。

    • 延长寿命:中午不睡午觉的人比中午睡午觉的人更容易扑街

    • 预防疾病:降低老年痴呆、癌症、心血管疾病、肥胖症、糖尿病、抑郁症等


    睡午觉好处多多,但也要适当,15 分钟到 30 分钟的睡眠最佳,超过的话反而有害。


    6. 下午上班前运动一下 🚴


    下午 2 点到 4 点是人清醒度最低的时候,10 分钟的运动可以让我们的身体重新清醒,提高专注度,程序员的工作岗位和场所如果有限,推荐:



    • 1️⃣ 深蹲

    • 2️⃣ 俯卧撑

    • 3️⃣ 胯下击掌

    • 4️⃣ 爬楼梯(不要下楼梯,下楼梯比较伤膝盖,可以向上爬到顶楼,再坐电梯下来)


    7. 2 分钟解决和 30 秒决断 🖖


    ⚒️ 2 分钟解决是指遇到在 2 分钟内可以完成的事情,我们趁热打铁把它完成。这是一个解决拖延的小技巧,作为一个程序员,经常会遇到各种各样的突发问题,对于一些问题,我们没办法很好的决策要不要立即完成, 2 分钟解决就是一个很好的辅助决策的办法。


    💣 30 秒决断是指对于日常的事情,我们只需要用 30 秒去做决策就好了,这源于一个“快棋理论”,研究人员让一个著名棋手去观察一盘棋局,然后分别给他 30 秒和 1 小时去决定下一步,最后发现 30 秒和 1 小时做出的决定中,有 90% 都是一致的。


    8. 不要加班,充足睡眠 💤


    作为程序员,我们可能经常加班到 9 点,到了宿舍就 10 点半,洗漱上床就 12 点了,再玩会儿手机就可以到凌晨 2、3 点。


    压缩睡眠时间,大脑就得不到有效的休息,第二天的专注度就会降低,工作效率也会降低,这就是一个恶性循环。


    想想我们在白天工作的时候,其实有很多时间都是被无效浪费的,如果我们给自己强制设定下班时间,创新、改变工作方式,高效率、高质量、高密度的完成工作,那是否就可以减少加班,让我们有更多的自由时间去学习新的知识技术,进而又提高我们的工作效率,形成一个正向循环。


    9. 睡前 2 小时 🛌



    1. 睡前两小时不能做的事情:

      • 🍲 吃东西:空腹的时候会促进生长激素,生长激素可以提高血糖,消除疲劳,但如果吃东西把血糖提高了,这时候生长激素就停止分泌了

      • 🥃 喝酒

      • ⛹️ 剧烈运动

      • 💦 洗澡水温过高

      • 🎮 视觉娱乐(打游戏,看电影等)

      • 📺 闪亮的东西(看手机,看电脑,看电视)

      • 💡 在灯光过于明亮的地方



    2. 适合做的事情

      • 📖 读书

      • 🎶 听音乐

      • 🎨 非视觉娱乐

      • 🧘‍♂️ 使身体放松的轻微运动




    10. 周末不用刻意补觉 🚫


    很多人以周为单位进行休息,周一到周五压缩睡眠,周末再补觉,周六日一觉睡到下午 12 点,但这与工作日的睡眠节奏相冲突,造成的后果就是星期一的早上起床感的特别的厌倦、焦躁。


    其实周末并不需要补觉,人体有一个以天为单位的生物钟,打破当前的生物钟周期,就会影响到下一个生物钟周期,要调节回来也需要花费一定时间。


    我们应该要以天为单位进行休息,早睡早起,保持每天的专注度。


    参考


    以上大部分来源于书籍 《为什么精英都是时间控》,作者桦泽紫苑,是一个脑神经专家


    作者:吴楷鹏
    来源:juejin.cn/post/7253605936144285757
    收起阅读 »

    领导:我有个需求,你把我们项目的技术栈升级一下

    故事的开始 在一个风和日丽的下午,我正喝着女神请的9.9咖啡,逛着本站摸着🐟,一切的一切都是这么的美好。 霎那间,只见耳边响起了声音,”系统觉醒中,请。。“,啊?我都外挂到账了? 呸,是”帅哥,领导叫你去开会“。 ”哦“,某位帅哥站了起来,撇了撇帅气的刘海,走...
    继续阅读 »

    故事的开始


    在一个风和日丽的下午,我正喝着女神请的9.9咖啡,逛着本站摸着🐟,一切的一切都是这么的美好。


    霎那间,只见耳边响起了声音,”系统觉醒中,请。。“,啊?我都外挂到账了?


    呸,是”帅哥,领导叫你去开会“。


    ”哦“,某位帅哥站了起来,撇了撇帅气的刘海,走向了办公室。


    胖虎00002-我真的很不错.gif


    会议室情节


    ”咦,不是开会吗,怎么就只有领导一个人“。


    昏暗的灯光,发着亮光的屏幕,在幽闭的空间里气氛显得有那么一丝的~~~暧昧,不对,是紧张。


    ”帅哥你来了,那我直接说事情吧“,领导说到。


    突然我察觉到那么一丝不安,但是现在走好像来不及了,房门紧闭,领导又有三头六臂,凭着我这副一米八五,吴彦祖的颜值的身躯根本就逃不了。


    ”是这样的,上面有个新需求,我看完之后发现我们目前的项目技术包袱有点重,做起来比较麻烦,看看你做个项目技术栈升级提高的方案吧“。


    听到这里我松了一口气,还好只是升级提高的方案,不是把屁股抬高的方案。


    哆啦A梦00009-噢那你要试试看么.png


    进入正题


    分析了公司项目当前的技术栈,确实存在比较杂乱的情况,一堆的技术包袱,惨不忍睹,但还是能跑的,人和项目都能跑。



    技术栈:vue2全家桶 + vuetify + 某位前辈自己开发的组件库 + 某位前辈搞的半成品bff



    我用了一分钟的时间做足了思想功课,这次的升级决定采用增量升级的方案,为什么用增量升级,因为线上的项目,需求还在开发,为了稳定和尽量少的投入人力,所以采取增量升级。



    这里会有很多小伙伴问,什么是增量升级啊,我会说,自己百度



    又经过了一分钟的思想斗争,决定使用微前端的方案进行技术升级,框架选择阿里的qiankun



    这里又会有小伙伴说,为什么用qiankun啊,我会说,下面会说



    胖虎00004我大雄今天就是要刁难你胖虎.gif


    微前端---qiankun


    为什么使用微前端,最主要是考虑到他与技术栈无关,能够忽略掉一些历史的包袱。


    为什么要用qiankun,最主要还是考虑到稳定问题,qiankun目前的社区比较大,方案也多,出了问题能找到方案,本人之前也有使用过的经验,所以这次就决定是它。



    技术为业务服务,在面对技术选型的时候要考虑到现实的问题,不能一味的什么都用新的,稳是第一位



    那么应该如何在老项目中使用微前端去升级,我给出了我的方案步骤



    1. 对老项目进行改造(路由,登录,菜单)

    2. 编写子应用的开发模板

    3. 逐个逐个模块进行重构


    下面会和大家分析一下三个步骤的内容


    66b832f0c3f84bc99896d7f5c4367021_tplv-k3u1fbpfcp-zoom-in-crop-mark_4536_0_0_0.webp


    老项目的改造(下面大多数为代码)


    第一步,我们要将老项目改造成适合被子应用随便进入的公交车。🚗


    先对老项目进行分析,老项目是一个后台项目,大多数的后台项目布局都是上中左布局,头部导航栏,左边菜单栏,中间内容。那么我们只需要当用户选择的菜单属于微前端模块时,将中间内容变成微前端容器就好了。


    image.png


    那我们现在制作一个layout,里面的UI库我懒得换了,讲究看吧🌈


    // BasicLayout.vue

    <a-layout-sider collapsible>
    //菜单
    a-layout-sider>

    <a-layout>
    <a-layout-header>
    //头部
    a-layout-header>

    <a-layout-content>
    //内容
    <router-view/>
    <slot>slot>
    a-layout-content>
    a-layout>


    然后对App.vue进行一定的修改


    // App.vue
    "zh-cn">
    <component v-if="layout" :is="layout">

    <div id="SubappViewportWrapper">div>

    component>
    // 初始化子应用时传入容器,这个容器不能后续修改,目前方案是将下面的容器动态append到SubappViewportWrapper
    <div id="SubappViewport">div>


    import { BasicLayout, UserLayout } from '@/layouts'
    import { MICRO_APPS } from './qiankun'
    import { start } from 'qiankun';
    export default defineComponent({
    components: {
    BasicLayout,
    UserLayout
    },
    data () {
    return {
    locale: zhCN,
    layout: ''
    }
    },
    methods: {
    isMicroAppUrl (url) {
    let result = false;
    MICRO_APPS.forEach(mUrl => {
    if (url.includes(mUrl)) {
    result = true;
    }
    });
    return result;
    },
    checkMicroApp (val) {
    if (isMicroAppUrl(val.fullPath)) {
    // 展示微前端容器
    console.log('是微前端应用....');
    document.body.classList.toggle(cName, false);
    console.log(document.body.classList);
    } else {
    // 隐藏微前端容器
    console.log('不是微前端应用');
    document.body.classList.toggle(cName, true);
    }
    const oldLayout = this.layout;
    this.layout = val.meta.layout || 'BasicLayout';
    if (oldLayout !== this.layout) {
    const cNode = document.getElementById('SubappViewport');
    this.$nextTick(function () {
    const pNode = document.getElementById('SubappViewportWrapper');
    if (pNode && cNode) {
    pNode.appendChild(cNode);
    }
    });
    }
    }
    },
    watch: {
    $route (val) {
    this.checkMicroApp(val);
    }
    },
    mounted () {
    start()
    }
    })


    <style lang="less">
    style>



    修改目的,判断路由中是否为微前端模块,如果是的话,就插入微前端模块容器。


    然后新建一个qiankun.js文件


    // qiankun.js
    import { registerMicroApps, initGlobalState } from 'qiankun';
    export const MICRO_APPS = ['test-app']; // 子应用列表
    const MICRO_APPS_DOMAIN = '//localhost:8081'; // 子应用入口域名
    const MICRO_APP_ROUTE_BASE = '/test-app'; // 子应用匹配规则

    const qiankun = {
    install (app) {
    // 加载子应用提示
    const loader = loading => console.log(`加载子应用中:${loading}`);
    const registerMicroAppList = [];
    MICRO_APPS.forEach(item => {
    registerMicroAppList.push({
    name: item,
    entry: `${MICRO_APPS_DOMAIN}`,
    container: '#SubappViewport',
    loader,
    activeRule: `${MICRO_APP_ROUTE_BASE}`
    });
    });
    // 注册微前端应用
    registerMicroApps(registerMicroAppList);

    // 定义全局状态
    const { onGlobalStateChange, setGlobalState } = initGlobalState({
    token: '', // token
    });
    // 监听全局变化
    onGlobalStateChange((value, prev) => {
    console.log(['onGlobalStateChange - master'], value);
    });
    }
    };
    export default qiankun;

    这个文件我们引入了qiankun并对使用它的API进行子应用的注册,之后直接在main.js注册,


    // main.js
    ...
    import qiankun from './qiankun'
    Vue.use(qiankun)
    ...

    然后我们只需要对路由做一点点的修改就可以用了


    新建RouteView.vue页面,用于接受微前端模块的内容


    //RouteView.vue


    修改路由配置


    //router.js
    const routes = [
    {
    path: '/home',
    name: 'Home',
    meta: {},
    component: () => import('@/views/Home.vue')
    },
    // 当是属于微前端模块的路由, 使用RouteView组件
    {
    path: '/test-app',
    name: 'test-app',
    meta: {},
    component: () => import('@/RouteView.vue')
    }
    ]

    最后就新增一个名为test-app的子应用就可以了。


    关于子应用的内容,这次先不说了,码字码累了,下次再说吧。


    哆啦A梦00007-嘛你慢慢享受吧.png


    下集预告(子应用模板编写)


    你们肯定会说,不要脸,还搞下集预告。


    哼,我只能说,今天周五,准备下班,不码了。🌈


    胖虎00014-怎么了-我胖虎说的有毛病吗.gif


    作者:小酒星小杜
    来源:juejin.cn/post/7307469610423664655
    收起阅读 »

    Android启动优化实践 - 秒开率从17%提升至75%

    一、前言 启动性能是App使用体验的门面,耗时过长会降低用户使用兴趣。对于开发者来说,通过各种技术手段来提升启动性能缩减启动时长,对整站业务的各项指标提升都会有较大帮助。因此,秒开率优化也成为了各个客户端团队在体验优化方向上十分重要的一环。 本文将会结合我自己...
    继续阅读 »

    一、前言


    启动性能是App使用体验的门面,耗时过长会降低用户使用兴趣。对于开发者来说,通过各种技术手段来提升启动性能缩减启动时长,对整站业务的各项指标提升都会有较大帮助。因此,秒开率优化也成为了各个客户端团队在体验优化方向上十分重要的一环。


    本文将会结合我自己在项目中优化启动速度的经验,跟大家分享下,我眼里的科学的启动速度优化思路。


    在我的眼里,科学的优化策略是通用的,不管是针对什么性能指标不管是针对什么课题,思路是差不多的。比如这期的分享是启动优化,其实跟上期分享的 如何科学的进行Android包体积优化 - 掘金 (juejin.cn) 是类似的,在那篇分享里我总结了基本思想:




    1. 抓各个环节

    2. 系统化方案先行,再来实操

    3. 明确风险收益比及成本收益比

    4. 明确指标以及形成一套监控防劣化体系

    5. 把包体积这个指标刻在脑子里



    那放在启动优化里,基本思想长啥样呢?其实是差不多的,我认为唯一的差别就是启动优化会包含着许多前期调研和计划过程考虑不到或者甚至无法提前考虑的问题点和可优化点。




    1. 抓各个环节

    2. 系统化方案先行,再来实操

    3. 从各个角度死磕各个角落,寻找可优化点

    4. 明确风险收益比及成本收益比

    5. 明确指标以及形成一套监控防劣化体系

    6. 把秒开率这个指标刻在脑子里



    我们调研了市面上几乎所有的优化方案,并结合项目现有情况,针对启动阶段各方面、各阶段进行针对性的优化,加上后期不断的调优,将Android客户端的主版本秒开率由 17% 优化到了 75+%,90 分位 App的启动耗时从 2800ms 优化到 1500 ms,大幅提升app的启动体验。此外,我们还建立了一系列的线上监控、防劣化体系,以确保优化效果可持续。


    二、评估优化需求


    在开展工作前,我们首先得先有两点判断:



    1. 是否需要进行优化

    2. 优化到多少算符合预期


    那具体应该如何进行判断呢?有个比较简单的方法就是对标,找到对标对象进行对标。


    对于小厂来说,一般对标对象有:



    1. 竞品App

    2. 业内人气App


    基于对标对象,我们可以粗略的有如下判断:



    1. 如果我们App跟竞品App启动速度差不多甚至略高,那就有必要进行启动速度优化。当然,在许多厂不缺人力和资源的时候,并不需要这一步判断过程哈

    2. 优化到业内人气App中启动速度最快那一档即可


    上述判断还是基于用户视角,从用户使用App的意愿去出发,那从业务或者技术的视角出发的话,肯定是越快越好,不废话了,下面直接进入主题。


    三、秒开率定义 / 启动速度指标定义


    既然要讨论秒开率,必须得明确以下问题:



    1. 用户启动app的耗时如何定义?

    2. 多长时间内app启动完成才算秒开?


    3.1 用户启动app的耗时如何定义?



    我们这里的定义是:进程启动 -> 用户看见Feed流卡片/内容



    业内常见的app启动过程阶段一般分为「启动阶段」和「首刷阶段」。



    • 启动阶段:指用户点击icon到见到app的首页

    • 首刷阶段:指用户见到app的首页到首页列表内容展现



    很多厂在谈论启动优化时,只涉及到第一步。对用户而言,他并不知道所谓的启动阶段和首刷阶段都是些什么东西,他只知道 看没看到预期的内容和能不能开始消费内容 ,既然要讨论秒开率,必然是用户期待的内容秒开,所以仅以「启动阶段」作为秒开率的指标依据并不能代表用户的真实用户体验。假如用户1s就看到首页了,但是feed流过了10s才有内容的展现,这显然不能算作秒开,这也不能代表用户的体验就是好的。当然,以 启动阶段 + 首刷阶段 认定为需要秒开的时间段,挑战也自然而然增大许多,环节越多,不可控和不可抗因素就越多。


    3.2多长时间内app启动完成才算秒开?



    1秒



    既然谈论的目标是秒开率,那必然是需要 1s 内完成用户的启动流程。


    四、认识自家App的启动流程


    为什么要认识自家App的启动过程?



    知彼知己者,百战不殆。 --《孙子·谋攻》



    对启动过程作业务层面、系统层面的全路径分析,有利于我们发现全路径上各个阶段的耗时点及潜在耗时点。


    4.1 从业务角度看启动过程


    为什么要从业务角度看启动过程?这是因为我们既然要优化的是秒开率,而我们的秒开率指标又是与业务强相关的指标,所以我们必须从业务的角度出发,找到对启动流程影响最大的以及会block启动流程的业务,以他们为切入点尝试寻求启动速度更优的解法。


    启动过程最大的不确定性因素来自于网络请求,如果App启动过程中,需要等待网络请求完成才能进入下一阶段,当出现弱网、慢网等情况时,启动时长就无法预估了,我们从下图中可以看到两处网络依赖:开屏广告请求、Feed列表请求。其他的初始化任务都不依赖网络,自然而然的执行时长在同一机器、同一环境是比较稳定的,也是可观测的。


    根据业务流程,我们想要优化启动速度,需要进行如下考虑:



    • 开屏广告接口尽量早的发出请求

    • 等待开屏接口过程中,尽量完成更多的对启动流程有 block 的启动任务

    • feed列表的第一屏数据尽量走缓存


    (下图画的有点粗略,意会就行)



    4.2 从系统角度看启动过程


    从系统角度来看自家App的启动路径,与大多数App是类似的。整体分为 Application 阶段、Activity阶段、RN阶段(业务页面阶段)。



    4.2.1 Application阶段


    在Application阶段中,有两个子阶段需要我们重点关注:



    1. Install provider,现在许多的三方库为了追求使用的便利性以及能够轻松的获取到Application上下文,会选择通过注册ContentProvider来实现库的初始化,然而正是由于过于便利,导致我们在排查启动过程的三方库初始化情况时,容易忽略掉这些隐式初始化的三方库。

    2. Application#onCreate,一般来说,项目本身模块的初始化、各种三方库初始化、业务前置环境初始化都会在 Application#onCreate 这个生命周期里干,往往这个生命周期里的任务是非常臃肿的,我们优化Application流程的大部分目光也集中在这里,也是我们通过异步、按需、预加载等各种手段做优化的主要时机。


    4.2.2 Activity阶段


    Activity阶段的起点来自于 ActivityThread#performLaunchActivity 的调用,在 performLaunchActivity 方法中,将会创建Activity的上下文,并且反射创建Activity实例,如果是App的冷启动(即 Application 并未创建),则会先创建Application并调用Application的onCreate方法,再初始化Activity,创建Window对象(PhoneWindow)并实现Activity和Window相关联,最终调用到Activity的onCreate生命周期方法。


    在启动优化的专项中,Activity阶段最关键的生命周期是 Activity#onCreate,这个阶段中包含了大量的 UI 构建、首页相关业务初始化等耗时任务,是我们在优化启动过程中非常重要的一环,我们可以通过异步、预加载、延迟执行等手段做各方面的优化。


    4.2.3 RN阶段(首页页面业务阶段)


    在我们App中,有着非常大量的 react native(RN) 技术栈的使用,面首页也是采用 RN 开发,在目前客户端团队配置下,对 RN 这个大项目的源码进行优化的空间是比较小的,考虑到成本收益比,本文几乎不会涉及对 RN 架构的直接正向优化,尽管 RN 的渲染性能可能是启动流程中非常大的瓶颈点。


    五、衡量App启动时长


    5.1 线上大盘观测


    为了量化指标、找出线上用户瓶颈以及衡量后续的优化效果,我们对线上用户的启动时长进行了埋点统计,用来观测用户从点击icon到看到feed流卡片的总时长以及各阶段的时长。


    通过细化、量化的指标监控,能够很好的观测线上用户启动耗时大盘的版本变化以及各个阶段的分位数版本变化,同时我们也需要依赖线上的性能监控统计来判断我们在某个版本上上线的某个优化是否有效,是否能真实反映在大盘指标以及用户体验上,因为本地用测试机器去跑平均启动时间,受限于运行环境的不稳定,总是会有数据波动的。当进行了某项优化之后,能通过本地测试大概跑出来是否有正向优化、优化了大概多少毫秒,但是具体反映到秒开率指标上,只能依赖大盘,本地无法做上线前的优化预估。


    启动流程终点选取



    终点不需要完全准确,尽量准就足够了



    大多数的 App 在选择冷启动启动流程终点时,会选择首页 Activity 的 onWindowFocusChanged 时机,此时首页 Activity 已经可见但其内部的 View 还不可见,对于用户侧已经可以看见首页背景,同时会将首页内 View 绘制归入首刷过程中。


    但是我们期望的终点是用户看到 Feed 流卡片的时刻,上面也说了,我们优化目标就是 「启动阶段」 + 「首刷阶段」,由于首页里的feed tab是RN 开发的,我们无法轻易的去精准到卡片的绘制阶段,于是我们将终点的选取选在了「ReactScrollView 的子 View onViewAttachedToWindow 回调时」,指RN feed流卡片View ready并且添加到了 window 上,可以大致认为卡片用户可见。


    启动性能指标校准


    由于启动路径十分复杂,在添加了相应的埋点之后还需要进行额外的校准,确保启动性能面板能正确反映用户正常打开app看到首页的完整不间断流程的性能。因此,我们对于许多的边缘case进行了剔除:



    • 进程后台被唤起,这种情况进程在后台早早的被唤起,三方sdk以及一些其他模块也早早的初始化完成,只是MainActivity没有被启动,这种case下,我们通过进程启动时,读取进程当前的 importance 进行过滤。

    • 启动过程中App退后台

    • 用户未登录场景

    • 特殊场景下的开屏广告,比如有复杂的联动动效

    • push、deeplink拉起

    • 点开app第一个页面非首页,这种场景常见的就是 push、deeplink,但是还会有一些其他的站内逻辑进其他tab或者其他二级页面,所以这里统一做一个过滤。


    启动性能大盘建设


    首先要明确我们建设的性能大盘是以什么形式从什么角度监控什么指标,在启动速度这种场景下,分位数指标更加适合去进行全面监控,因为其波动较小,不会被极端的case影响曲线,所以我们在进行启动性能的优化时,最关注的就是分位数的性能。所以我们的整体监控面板分为:



    • 秒开率、2秒开率、3秒开率、5秒以上打开率

    • 90分位总体性能、90分位各阶段性能。

    • 分版本各阶段各项指标、整体各阶段各项指标、主版本各阶段各项指标。

    • 分场景,如有无广告等等

    • ...


    5.2 Method Trace


    除了线上对性能指标进行监控,在开发环境下我们想要去优化启动时长,必须得有方法知道瓶颈在哪儿,是哪个方法太耗时,还是哪些逻辑不合理,哪些能优化,哪些没法优化。Method Trace就是其中手段之一,我们通过 Method Trace能看到每个线程的运行情况,每个方法、方法栈耗时情况如何。


    Profiler


    看method trace有两种方式,一种是直接启动 Android Studio 自带的Profiler工具,attach上进程之后就可以选择 “sample java methods” 来进行 cpu profiling了,但是这种方式不支持release包使用,只能在debug包上面使用,且性能损耗大,会让问题的分析产生误差


    Debug.startMethodTracingSamping


    我们也可以通过代码去抓取 method trace:


    Application#onCreate:
    File file = new File(FileUtils.getCacheDir(application), "trace_" + System.currentTimeMillis());
    Debug.startMethodTracing(file.getAbsolutePath(), 200000000);
    Debug.startMethodTracingSamping(file.getAbsolutePath(), 200000000, 100);

    StartupFlow#afterStartup:
    Debug.stopMethodTracing();

    在开启 MethodTracing 时,更加推荐使用 startMethodTracingSamping,这样性能损耗比调用startMethodTracing进行的完全的 Method Tracing低非常非常多, 这样抓到的性能窗口误差也小很多。而且抓到的 trace 文件也小很多,用Android Studio直接打开的话,不会有啥问题,当文件足够大时,用Android Studio打开可能会失败或者直接卡死。



    5.3 System Trace


    Perfetto


    Perfetto 是 Android 10 中引入的全新平台级跟踪工具。这是适用于 Android、Linux 和 Chrome 的更加通用和复杂的开源跟踪项目。与 Systrace 不同,它提供数据源超集,可让你以 protobuf 编码的二进制流形式记录任意长度的跟踪记录。你可以在 Perfetto 界面中打开这些跟踪记录,可以理解成如果开发机器是 Android 10 以下,就用 Systrace,如果是 Android 10 及以上,就用 Perfetto,但是 Perfetto跟Systrace一样,抓取到的报告是不包含 App 进程的代码执行情况的。文章后续也会给用Perfetto找到待优化点的案例。


    六、优化实践


    经过上面的理论分析、现状分析以及大盘指标的建立之后,其实大致对哪些需要优化以及大致如何排查、如何优化会有一个整体认知。在小厂里,由于开发资源有限,所以实际上在优化实践阶段对能进行的但是进行人力成本比较高的优化项会做一轮筛查,我们通过调研市面公开的资料、技术博客了解各大场以及各个博主分享的启动优化思路和方案,再结合自身场景做出取舍,在做取舍的过程中,衡量一个任务是否要启动有两个关键指标:“投入产出比”、“产出风险比”



    1. 投入产出比:很容易理解,当优化一个点需要 3人/天,能收获90分位 100ms 的优化产出,另一个点需要 3人/天,但只能收获90分位 10ms 的优化产出,谁先谁后、谁要做谁不做其实显而易见,因为优化的前期追求的一个很重要的点必然是优化收益。等到后续开启二期任务、三期任务需要做到更加极致时,才会考虑成本高但收益低的任务。

    2. 产出风险比:在我们做启动优化过程中,必然会有一些方案有产出但是可能会有风险点的,可能是影响某个业务,也可能影响所有业务,我们需要在启动优化的过程中,不断的衡量一个优化任务是否有风险,风险影响范围如何,风险影响大小如何,最后整体衡量甚至跟业务方进行商讨对他们造成的劣化是否能够接受,最后才能敲定相关的任务是否要排上日程。


    所以大致的思路可以总结为:



    1. 前期低成本低风险快速降低大盘启动耗时

    2. 后期高成本突破各个瓶颈

    3. 全期加强监控,做好防劣化


    下面我们就将会按照文章一开始提过的启动流程顺序来分享在启动加速项目中的一些案例。


    6.1 Application流程


    6.1.1 启动任务删减与重排



    这里我多提两嘴。我个人觉得在启动优化中,删减和重排启动任务是最为复杂的,特别是对于中大型App,业务过于多,甚至过于杂乱。但是在小厂中,完全可以冲着 删除和延后所有首页无关业务、模块、SDK代码 的目标去,前提是能理清所有业务的表面和隐藏逻辑,这里需要对整个App启动阶段的业务代码和业务流程全盘掌控。




    你也许可以通过奇技淫巧让启动过程中业务B反序列化时间无限变短,而我可以从启动过程中删掉业务B逻辑



    在App的启动流程中,有非常多的启动任务全部在Application的onCreate里被执行,有主线程的有非主线程的,但是不可避免的是,二者都会对启动的性能带来损耗。所以我们需要做的第一件重要的事情就是 减少启动任务。
    我们通过逐个排查启动任务,同时将他们分为几类:



    • 刚需任务:不可延迟,必须第一时间最高优先级执行完成,比如网络库、存储库等基础库的初始化。如果不在启动阶段初始化完成,根本无法进入到后续流程。

    • 非刚需高优任务:这类任务的特征就是高优,但是并非刚需,并不是说不初始化完成后续首页就没法进没法用,比如拉取ab实验配置、ip直连、push、长链接相关非刚需基础建设项,这类可以高优在启动阶段执行,但是没必要放在 UI 线程 block 执行,就可以放到启动阶段的后台工作线程中去跑。

    • 非刚需低优任务:这类任务常见的特征就是对业务能否运作无决定性影响或者业务本身流程靠后,完全可以放在我们认为的启动阶段结束之后再后台执行,比如 x5内核初始化、在线客服sdk预初始化 之类的。

    • 可删除任务:这类任务完全不需要存在于启动流程,可能是任务本身无意义,也可能是任务本身可以懒加载,即在用到的时候再初始化都不迟。


    将任务分类之后,我们就能大概知道如何去进行优化。



    • 拉高刚需任务优先级

    • 非刚需高优 异步化

    • 非刚需低优任务 异步化+延迟化

    • 可删除任务 删除


    6.1.2 任务排布框架


    为了更加方便的对启动任务进行排布,我们自己实现了一套用于启动过程的任务排布框架TaskManager。TaskManager具有以下几个特性:



    1. 支持优先级

    2. 支持依赖关系

    3. 提供超时、失败机制以供 fallback

    4. 支持在关键时间阶段运行任务,如MainActivity某个生命周期、启动流程结束后


    大致使用方式为:


    TaskManager.getInstance().beginWith(A)
    .then(B)
    .then(C, D)
    .then(E)
    .enqueue();

    TaskManager.getInstance().runAfterStartup({ xxx; })

    通过任务的大致非精细化的排布,我们不仅仅可以对启动任务能够很好的进行监控,还可以更加容易的找出不合理项。


    6.1.3 实现runAfterStartup机制 + idleHandler



    这玩意儿十分重要,我通过昏天黑地的梳理业务,将启动流程中原先可能超过一半的代码任务非常方便的放到了启动流程之后



    我们通过提供 runAfterStartup 的API,用于更加容易的支持各种场景、各种业务把自己的启动过程任务或者非启动过程任务放在启动流程结束之后运行,这也有助于我们自己在优化的过程中,更加轻松的将上面的非刚需低优任务进行排布。


    runAfterStartup的那些任务,应该在什么时候去执行呢?
    这里我们认定的启动流程结束是有几个关键点的:



    1. 首页tab的feed流渲染完成

    2. 首页tab加载、渲染失败

    3. 用户进入了二级页面

    4. 用户退后台

    5. 用户在首页有 tab 切换操作


    通过TaskManager的使用以及我们对各业务的逐一排查分析,我们将原先在启动阶段一股脑无脑运行的任务进行了拆解和细化,该延后的延后,该异步的异步,该按需的按需。


    6.2 Activity流程


    接下来将分享一下 Activity 阶段的一些相关优化的典型案例。


    6.2.1 SplashActivity与MainActivity合并


    原先的 launcher activity 是SplashActivity,主要承载广告逻辑,当App启动时,先进入SplashActivity,死等广告接口判断是否有开屏广告,如果有则展示,没有则跳转进MainActivity,这里流程的不合理性影响最大的点是:SplashActivity在死等开屏接口时,根本无法对业务本身做一些预加载或者并发加载,首页的业务都在MainActivity里面,同时启动阶段需要连续启动两个Activity,至少带来 百毫秒 级别的劣化。



    当然,将SplashActivity承接的所有内容转移到MainActivity上,有哪几个挑战又该如何解决?


    1. MainActivity 作为launch activity之后的单实例问题



    • MainAcitvity 的 launch mode 需要设置为 singleTop,否则会出现 App从后台进前台,非MainActivity走生命周期的现象

    • 同时,作为首页,需要满足另一个条件就是跳转到首页之后,其他二级页面需要全部都关闭掉,站内跳转到 MainActivity 则附带 CLEAR_TOP | NEW_TASK 的标记


    2. 广告以什么形式展现



    • 广告原先是以Activity的形式在展现,当 launcher 换掉之后,广告被抽离成 View 的形式去承载逻辑,在 MainActivity#onCreate 中,通过将广告View添加进 DecorView中,完成对首页布局的遮罩,这种方式还有一个好处就是在广告View展示白屏logo图时,底下首页框架是可以进行预加载的。

    • 这里其实还需要实现以下配套设施:

      • 首页弹出层管理器,管理弹窗、页面跳转等各种可能弹出的东西,在广告View覆盖在上面时,先暂停弹出层管理器的生命周期,避免出现其他内容盖住广告




    6.2.2 异步预加载布局



    使用异步加载布局时,可以对AsyncLayoutInflater小小改造下,如单线程变线程池,调高线程优先级等,提升预加载使用率



    在Android中,其实有封装较好的 AsyncLayoutInflater 用于进行布局的异步加载,我们在App的启动阶段启动异步加载View的任务,同时调高工作线程优先级以尽量在使用View之前就 inflate 结束,这样在首页上要使用该布局时,就可以直接从内存中读取。


    异步加载布局容易出问题的点有:



    1. Context的替换



      • 使用MutableContextWrapper,在使用时替换为对应的 Activity 即可



    2. theme问题



      • 当异步创建 TabLayout 等Matrials组件时,由于Application的主题并没有所谓的 AppCompat 主题,会抛出异常 You need to use a Theme.AppCompat theme 。这时需要在 xml 中加上 android:theme="@style/AppCompatTheme" 即可。




    但是异步预加载布局有一个点是非常关键的:使用前一定要确认,异步创建的这个布局大部分时候或者绝大部分时候,都能在使用前创建好,不然的话不仅没有优化的效果,还会增加启动阶段的任务从而对启动性能带来一定程度上的劣化。


    6.2.3 多Tab懒加载


    我们App的首页结构非常复杂,一共有着三层的tab切换器。底部tab负责切换大tab,首页的tab还有着两层tab用于切换二三级子页面。原先由于代码设计使然,首页Tab的其他所有子页面都会在 App 的启动阶段被加载,只是说不进行 RN 页面的渲染,这其实会占据 UI 线程非常多的时间。


    我们做启动优化的过程中,将一二三级tab全部懒加载,同时由于 我们App存在首页其他 Tab 的预加载技术建设,目的是为了实现当用户切到其他tab时,完全去除加载过程,因此我们也将预加载的逻辑延迟到了启动流程之后,即:


    StartupFlow.runAfterStartup{ preload() }

    6.2.4 懒加载布局


    Tab懒加载其实也算是布局懒加载的一部分,但又不全是,所以给拆开了。这部分讲的布局懒加载是指:



    • 启动过程不一定会需要用上的布局,可以完全在需要时被加载,比如:

      • 广告View,完全可以在后端广告接口返回可用的广告数据结构且经过了素材校验等流程确定要开始渲染广告时,进行布局的加载。

      • 首页上其他非全量的布局,比如其他广告位、首页上并不一定会出现的搜索框、banner 组件等。这些组件的特性是根据不同的配置来决定是否展示,跟广告类似。




    我们用上的布局懒加载的手段分几种:



    • ViewStub,在开屏广告的布局中非常常见,因为广告有多种类型,如视频、图片、其他类型广告等,每次的开屏又是确定的只有一种,因此就可以将不同类型的广告容器布局用 ViewStub 来处理

    • Kotlin by lazy,这种就是适用于 布局是动态加载的场景,假如上面描述的开屏广告的各种不同类型素材的布局都是在代码中动态的 inflate 并且 add 到根View上的话,其实也可以通过 by lazy 的方式来实现。所以其实很多时候 by lazy 用起来会更加方便,比如下图,对 binding 使用 by lazy ,这样只有在真正要使用 binding 时,才会去 进行 inflate。
      image.png


    6.2.5 xml2Code


    用开源项目可以,自己实现也可以,当然,搭配异步加载更可以。


    6.2.6 减少布局层级、优化过度绘制


    这个就需要自己通过LayoutInspector和Android的调试工具去各自分析了,如果打开LayoutInspector肉眼可见都是红色,赶紧改。。


    6.3 RN流程



    这里也顺带吐槽下吧,用 RN 写首页 feed 流的app真的不多哈,一般来说随着公司的发展,往往会越来越注重关键页面的性能,我们项目是我见过的为数不多的进 App 第一个页面还是RN的。如果首页不是RN,上面提到的秒开率指标、启动耗时应该会更加好一些才对。



    image.png


    先直面 RN 页面作为首页的加载流程,在页面进行渲染前,会比 native 页面多几个前置任务:



    1. RN 框架初始化(load各种so,初始化各种东西),RN init

    2. RN bundle 文件准备,find RN bundle

    3. RN bundle 文件加载,load RN bundle

    4. 开启RN渲染流程,start react application


    又因为 RN 的 js 和 native 层进行通信,又有引擎开销和线程调度开销,所以我个人认为 RN 是不足以胜任主流 app 的搭载首页业务的任务的


    吐槽归吐槽,能尽量优化点是一点:


    6.3.1 无限提前 RN init


    如题,由于后续所有的RN流程都依赖RN环境的初始化,所以必须将这坨初始化调用能放多前面,就放多前面,当然,该子线程的还是子线程哈,不可能主线程去初始化这东西。


    6.3.2 前置 RN bundle 的完整性校验


    当我们在使用 RN 的 bundle 文件时,往往需要校验一下 md5,看看从CDN上下载或者更新的 bundle 文件是否完整,但是启动流程如果进行 bundle 文件的 md5 校验,往往是一个比较2的举动,因此我们通过调整下载流程和加载流程,让下载/更新流程进行完整性校验的保护,确保要加载的所有 bundle 都是完整的可用的就行了,就不需要在启动流程校验bundle完整性了。


    6.3.3 page cache


    给首页的feed流增加页面级别缓存,让首页首刷不依赖接口返回


    6.3.4 三方库的初始化优化


    部分的三方 RN package,会在RN初始化的时候,同步的去初始化一堆耗时玩意或者执行耗时逻辑,可以通过修改三方库代码,让他们变成异步执行,不要拖慢整体的RN流程。


    6.4 其他优化


    6.4.1 Webview非预期初始化


    在我们使用 Perfetto 进行性能观测时,在 UI 线程发现了一段 几十毫秒接近百毫秒 的非预期Webview初始化的耗时(机器环境:小米10 pro),在线上用户机器上这段代码执行时间可能会更长。为什么说非预期呢:



    • 首页没有WebView的使用、预加载

    • X5内核的初始化也在启动流程之后


    1701274423005.png


    我们从perfetto的时序图中可以看到,堆栈的调用入口是WebViewChromiumAwInit.startChromiumLocked,由于 Perfetto 并看不到 App 相关的堆栈信息,所以我们无法直接知道到底是哪行代码引起的。这里感谢一下 抖音团队 分享的启动优化案例解析中的案例,了解到 WebViewFactory 实现的 WebViewFactoryProvider 接口的 getStatics、 getGeolocationPermission、createWebView 等多个方法的首次调用都会触发 WebViewChromiumAwInit#ensureChromiumStartedLocked ,随之往主线程 post 一个 runnable,这个runnable的任务体就是 startChromiumLocked 函数的调用。


    所以只要我们知道谁在调用 WebViewFactoryProvider 的接口方法,就能知道调用栈来自于哪儿。于是乎我们开始对 WebViewFactoryProvider 进行动态代理,用代理对象去替换掉 WebViewFactory 内部的 sProviderInstance。同时通过断点、打堆栈的形式来查找调用源头。


        ##WebViewFactory
    @SystemApi
    public final class WebViewFactory{
    //...
    @UnsupportedAppUsage
    private static WebViewFactoryProvider sProviderInstance;
    //...
    }


    ##动态代理
    try {
    Class clas = Class.forName("android.webkit.WebViewFactory");
    Method method = clas.getDeclaredMethod("getProvider");
    method.setAccessible(true);
    Object obj = method.invoke(null);

    Object hookService = Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj.getClass().getSuperclass().getInterfaces(),
    new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    Log.d("zttt", "hookService method: " + method.getName());
    new RuntimeException(method.getName()).printStackTrace();
    return method.invoke(obj, args);
    }
    });

    Field field = clas.getDeclaredField("sProviderInstance");
    field.setAccessible(true);
    field.set(null, hookService);
    } catch (Exception e) {
    e.printStackTrace();
    }

    替换掉 sProviderInstance 之后,我们就可以在我们的代理逻辑中,加上断点来进行调试,最终找到了造成 WebView非预期初始化的始作俑者:WebSettings.getDefaultUserAgent



    事情到这里就好解决了,只需要对 WebSettings.getDefaultUserAgent 进行编译期的Hook重定向到带缓存defaultUserAgent 的相关方法就行了,本地有缓存则直接读取,本地没有则立即读取,得益于我们项目中使用方便的 配置化 Hook 框架,这种小打小闹的 Hook 工作不到一分钟就能完成。



    参考:


    基于 Booster ASM API的配置化 hook 方案封装 - 掘金 (juejin.cn)


    AndroidSimpleHook-github





    当然,这里还需要考虑一个问题,那就是当用户机器的 defaultUserAgent 发生变化之后,怎么才能及时的更新本地缓存以及网络请求中用上新的defaultUserAgent。我们的做法是:



    • 当本地没有缓存时,立刻调用 WebSettings.getDefaultUserAgent 拿值并更新缓存;

    • 每次App启动阶段结束之后,会在子线程中去调用WebSettings.getDefaultUserAgent 拿值并更新缓存。


    这样处理之后,将 defaultUserAgent 发生变化之后的影响最小化,系统 WebView 升级本身就是极度不频繁的事情,在这种 case 下我们舍弃了下一次 App 打开前几个网络请求的 defaultUserAgent 正确性也是合理的,这也是我们考量 「风险收益比」的一个经典case。


    6.4.2 启动阶段 IO 优化



    更加高级的做法是进行请求合并,当我们将50+个网络请求优化到十来个的时候,如果再对一些实时性不是极高的接口进行合并,会更加优雅。不过小厂没资源做。



    前面说的都是优化 UI 线程相关的耗时,实际上在启动阶段,不仅仅 UI 线程执行太多耗时任务会影响启动速度,工作线程执行太多的耗时任务也会影响到启动速度,特别是重IO的任务。


    在我们App的启动流程里,首页完成渲染前需要发送50+个网络请求,包含:



    • 业务预拉取数据

    • 业务拉取各种配置、开关、实验变量

    • 多次的badge请求

    • 多次的 IM 拉消息的请求

    • 各种首页阶段是否展示引导的请求,如push引导权限

    • 其他莫名奇妙的请求,可能业务都废弃了,还在发送请求


    在优化这一项时,还是秉承着前面所说的得谨慎的考虑 “收益风险比”,毕竟我们不是对所有业务都非常了解,且公司现有研发也不是对所有业务都非常了解。通过一波深入的调研和调整之后,我们将 App 首页渲染完成前的网络请求数量,控制在 10 个左右,大大的减少了启动阶段的网络请求量


    6.4.3 大对象反序列化优化


    我们App中,对象的反序列化、序列化用的是谷歌亲儿子 - Gson。Gson 是 Google 推出的一个 json 解析库,其具有接入成本低、使用便捷、功能扩展性良好等优点,但是其也有一个比较明显的弱点,那就是对于它在进行某个 Model 的首次解析时会比较耗时,并且随着 Model 复杂程度的增加,其耗时会不断膨胀。
    而我们在启动过程中,需要用 Gson 反序列化非常多的数据,特别是某些大对象,如:



    • Global config:顾名思义,是一个全局配置,里面包含着各个业务的配置信息,是非常大的

    • user info:这是用户信息的描述对象,里面包含着非常多的用户属性、标签,在App 启动过程中如果主线程去初始化,往往需要几十甚至上百毫秒。


    针对这种启动阶段必须进行复杂对象序列化,我们进行了:



    • 用Gson解的,自定义 TypeAdapter,属于正面优化反序列化操作本身(市面上有现成的一些通过添加注解自动生成TypeAdapter的框架,通过一个注解就能够很轻松的给对应的类生成 TypeAdapter并注册到 Gson 中)

    • 又大又乱又不好改的直接读磁盘然后 JSON 解析的大对象(没想到还有这种的吧),提前子线程预加载,避免在 UI 线程反序列化,能解决部分问题,并非完全的解法


    6.4.4 广告流程优化


    其实聊到启动优化,必然会涉及的肯定是 开屏广告 的业务流程。首先要搞清楚一个问题。


    启动优化优化的是什么?
    启动优化优化的是用户体验,优化的是用户看到内容的速度,那么开屏的内容怎么就不算呢?所以实际上加速用户看到开屏也能一定程度上让用户体感更加的好。而且由于我们进首页的时间依赖于广告流程结束,即需要等待广告流程结束,我们App才会跳过logo的全屏等待页面进入首页,那么优化广告流程耗时实际上也是在优化进入首页的速度,即用户可以更加快速的看到首页框架。



    原先的广告流程如上图,业务流程本身可能没什么问题,问题出在了两次的往主线程post runnable来控制流程前进。已知 App 启动流程是非常繁忙的,当一个 runnable 被post到 UI 线程的队列中之后不会立即执行,可能需要等上百甚至几百毫秒,而且由于启动过程中有着许多的耗时大的 runnable 在被执行,就算 mainHandler.postAtFrontOfQueue 都无济于事。



    因此我们对广告流程做了调整,去掉了其中一次的消息抛回主线程执行的逻辑,加快了广告业务的流程执行速度,同时,受益于我们前面说的 View 的异步预加载、懒加载等手段,广告流程的执行速度被全面优化。


    6.4.5 GC 抑制



    实现的花可以参考大佬博客: 速度优化:GC抑制 - 掘金 (juejin.cn)


    大家如果只是想本地测试下 GC 抑制在自己项目里的效果,反编译某些大厂App,从里面把相关 so 文件捞出来,copy 下JNI声明,放自己项目里测试用就行。



    自动垃圾回收的Java特性相对于C语言来说,确实是能够让开发人员在开发时提高效率,不需要去考虑手动分配内存和分配的内存什么时候去手动回收,但是对于Java程序来说,垃圾回收机制真实让人又爱又恨,不过如果开发人员仅在乎业务而不在乎性能的话,确实是不会垃圾回收恨起来。这里需要明确一点,垃圾回收是有代价的,会占 CPU 资源甚至导致我们线程被挂起。


    App在启动过程中,由于任务众多而且涉及到的sdk、模块也众多,非常容易在启动阶段发生一次或者多次的GC,这往往会带来比较多的性能损耗。


    针对这种启动容易触发 GC 的场景,我们有两种方式去减少 GC 次数以及降低 GC 发生的可能。



    • 通过 profiler 去排查启动过程中的对象创建、分配,找出分配过于频繁明显不正常的case

    • 影响 ART 虚拟机的默认垃圾回收行为和机制,进行 GC 抑制,这里由于我们 App 的 minSdk 是 Api 24,所以仅涉及 ART 虚拟机上的 GC 抑制。


    不过鉴于以下几点,我们最终并没有自己去实现和上线无稳定性、兼容性隐患的GC抑制能力:



    1. Android 10 及以上,官方实际上已经在 framework 中添加了 App 启动提高 GC 阈值的逻辑。cs.android.com/android/_/a…

    2. 由于我们在启动任务重排和删减用例很大,线上对 启动阶段的 GC 次数进行了统计,发现 80 分位的用户 GC 次数为0。也就是说启动优化上线之后线上至少 80% 的用户在启动阶段都不会发生 GC。监听 GC 发生可以简单的用 WeakReference 来包装重写了 finalize 方法的自定义对象。

    3. 不满足对于收益成本比、风险收益比的要求


    6.4.6 高频方法



    排查高频方法可以通过 method trace + 插桩记录函数调用来做



    比如在我们App的场景中,日志库、网络库上拼接公共参数时,会反复调用许多耗时且无缓存的方法去获取参数值,其中包括:



    • Binder调用,如 push enable 等

    • 每次都需要反序列化的取值,如 反序列化 global config,从中取得一个配置值。

    • ...


    针对这几类问题,我们做了如下优化:



    • 能加缓存加缓存

    • 拼接公参异步化


    6.4.7 binder 调用优化



    想要优化启动过程中的binder调用,必须得先知道有哪些binder调用,不考虑来自Framework代码的调用的话,可以通过hook工具来先检查一下。同时打印下调用耗时摸个底
    基于 Booster ASM API的配置化 hook 方案封装 - 掘金 (juejin.cn)



    binder是android提供的IPC的方式。android许多系统服务都是运行在system_server进程而非app进程,比如判断网络,获取电量,加密等,当通过binder调用去调用到相关的api之后,app线程会挂起等待binder驱动返回数据,因此IPC 调用是比较耗时的,而且可能会出现比预期之内的耗时更加耗时的现象。


    针对binder调用的耗时现象,主要做了:



    1. 对反复调用的 binder 调用结果进行缓存,合适的时机进行更新

    2. 通过 hook 框架统一收拢这些调用进缓存逻辑


    比如 push enable,这种总不能启动过程变来变去吧,再比如网络状态,也不能启动过程变来变去吧。


    当然,上面举的例子,也完全可以用于App全局场景,比如通知权限状态,完全可以app进前台检查下就行,比如网络状态,监听网络状态变化去更新就行。


    七、验收优化效果



    再次强调一下,我们统计的 App 启动耗时是「启动阶段」+ 「首刷阶段」



    7.1 App 启动耗时分位数


    90 分位 App的启动耗时从 2800 左右 下降到 1500 左右。降幅47%



    7.2 主版本秒开率



    从图中也能看到,整体稳定,但部分天波动较大,是因为开屏广告接入了程序化平台,接口时长、素材大小等都不是很好控制,尽管后端已经限制了请求程序化的超时时长,但是迫于无奈,无法将程序化平台接口请求超时时长设定在一个我满意的情况下,毕竟是收入部门。



    Android 主版本秒开率由原先的约 17% 提升到 76%



    7.3 两秒打开率


    Android 主版本两秒打开率由原先的 75% 提升到了 93%


    八、总结与展望


    回顾秒开率优化的这一期工作,是立项之后的第一期,在优化项选型过程中,除了优化效果之外,人力成本是我们考虑的最多的因素,由于团队人力不充裕,一些需要比较高成本去研究、去做的优化项,也都暂时搁置留做二期任务或者无限搁置。启动优化本身就是个需要持续迭代、持续打磨的任务,我们在优化的过程中始终秉承着 高收益成本比、低风险收益比 的思想,所以后续我们还会继续钻研,继续将之前没有开展的任务排上日程,技术之路,永无止境。


    防劣化跟优化一样重要


    在线上优化工作开展完成且取得了相应成果后,绝对不能放松警惕,优化这一下获得了效果并不是最重要的,最重要的是要有持续的、稳定的优化效果。对于线上用户来说,其实可能并不关心这个版本或者这几个版本是不是变快了,大家可能更需要的是长时间的良好体验,对于我们这些开发者来说,长时间的良好体验可能才能改变大家对 自家 App 的性能印象,让大家认可自家 App 的启动性能,这也是我们优化的初衷。因此,防劣化跟优化一样重要!


    其他


    做性能优化往往是比较枯燥的,可能很长时间没有进展,但是当有成果出来时,会收获到一些幸福感和满足感。希望大家都能在遇到瓶颈很难再往前迈步时,再努力挣扎一下,如果不出意外的话,这一路一定很精彩。


    作者:邹阿涛涛涛涛涛涛
    来源:juejin.cn/post/7306692609497546752
    收起阅读 »

    屠龙少年终成恶龙,前端转产品的我给前端挖了个坑

    前端转产品3周左右,把自己的一些感受通过《我转产品了-前端转产品是一种什么样的体验》这篇文章与大家分享,评论区惊现一波大佬。由于比较忙,不知不觉好像转眼间已经又过去一个多月,这次趁着周末没有开成会,给大家分享一下最近的『趣事』。 目前在并行中的一个项目主要是类...
    继续阅读 »

    前端转产品3周左右,把自己的一些感受通过《我转产品了-前端转产品是一种什么样的体验》这篇文章与大家分享,评论区惊现一波大佬。由于比较忙,不知不觉好像转眼间已经又过去一个多月,这次趁着周末没有开成会,给大家分享一下最近的『趣事』。


    目前在并行中的一个项目主要是类似文件管理的功能,在这个项目中主要是旁听的角色,这个项目的产品和需求是由公司的一产品朋友来做。在文章中产品功能描述部分由于众所周知的原因,我只会粗略的提一下。


    客户说的


    用于简化传统 OA 系统的某些繁复操作。最好能编辑、预览,如果没有的话也可以,先上一版。


    我们准备给客户做的


    我们给客户画了第一批原型,浏览器登录一个网址,注册一个账号、密码…… 然后在文件系统中可能在线预览图片、PDF、word、excel 之类的。


    客户其实想要的


    在演示的时候,第一个界面,客户质问为什么我们的系统还需要输入网址?还需要登录?大家一脸懵逼。然后解释因为数据是服务器器上的,在浏览器里输入地址,就能访问我们的系统了。然后如果不使用用户名和密码登录的话就不知道谁是谁。


    客户说:『我们云桌面对每个人都是唯一的啊,不需要再搞一个账户……』


    我们说:『因为我们系统是在浏览器里的,浏览器是访问不了操作系统里的用户信息的,所以需要注册一个用户密码……如果觉得麻烦,我们也可以后台先注册后,然后登录之后记住登录状态,就不用每次都登录了。』


    客户:『行吧。』


    然后讲业务流程……


    当讲到某文档的审核功能时,比如如果审核人需要对 word 进入批注,需要下载,然后添加批注,再上传到审核意见附件中。


    客户:『为什么要下载?』


    我们:『因为我们的是浏览器。』


    客户:『行吧。』


    矛盾分析


    经过了几周、几轮之后的演示,客户还经常时不时问一些为什么我们系统不能直接进行某操作的想法。想要不用写到这里大家都很清楚了,客户想做的东西其实是一个便于操作的文件管理系统。要实现最大的便利性,最好是与操作系统打通。


    但我们这边技术栈主要是 B/S 架构的经验较多,桌面程序的经验基本没有。并且应该我们这边认为浏览器里的文件管理操作也是很常见的,比如各大网盘都可以在浏览器里进行文件操作。


    我们调研的方向都甚至是通过各种 js sdk 实现浏览器中预览 word、excel 的功能。


    一些自己的想法,不知该如何讲


    这个产品讨论会来来回回5-6次到现在还没结束,虽然从第2次我基本就确定客户这种应该使用桌面端来实现比较好。但是这种东西不好讲,这是产品和技术人员去决定的东西。


    不能随便讲的原因在哪里:



    • 角色只是旁听者就不要去定一些方向性的东西。


    这个应该大家都明白,角色问题。



    • 每个方向最小和最大会有什么后果,团队是不是能承担?


    这个也比较清楚,难度问题。首先讲,如果做成桌面端,团队没有这方面的经验,遇到操作系统相关的 API 调不起来怎么搞?兼容性怎么搞?我了解的前端肯定是实现不了,虽然 node/electron 可以与操作系统交互,但当前前端团队无相关经验的人。虽然后端 java 可以与操作系统交互,但我不可能给客户说这东西我们这边后端 java 能做。


    所以我的想法是:如果他们与客户能达成使用浏览器完成这个系统的共识,清楚浏览器与桌面程序的能力边界。那么何乐不为呢?


    我方开始妥协


    第6次演示原型时,客户又讲到,那如果一个文件要下载也行。但使用这个系统的人可能年龄都比较大了,下载到哪里自己经常都找不到。


    这也确实,每个文件都要下载来操作再上传已经很麻烦了,再像在垃圾里去翻刚刚下载的文件,就更麻烦了。客户问能不能下载的位置我们系统可以指定的?


    我们再次说下载位置是浏览器规定的,系统指定不了。然后客户提到,实现不行像 ftp 这些工具一样,能把文件传到某个指定的位置也行……


    然后我们技术负责人说,那这样的话,看能不能把两台设备互通,当浏览器里要下载某文件时,向后端发起请求,后端从服务器上操作客户端电脑。客户说可以啊!


    我理解上一段话下来是这样:


    客户端与服务端有某个通道,允许服务端操作客户端,比如文件创建、删除。然后客户端的浏览器里要下载某个文件到 C 盘时,向服务器发起请求,服务端去后台下载文件到客户端的 C 盘。


    看起来到也可行,这对前端浏览器而言还是一样的。直接向服务端发请求就行了。


    但是我的想法是这样的:???


    准备好了吗?坑要来了


    上面说的功能结果是可行的,并且客户也是接受的,而且前端也是一样的只需向服务器发起请求即可。但是我有以下想法:



    • 有点绕。

    • 服务端要如何远程操作客户端?这是个问题。在客户端上装个自己写好的 curd 操作的程序?服务器通过 telnet 等现有工具远程后台操作客户端?


    据我所知有一种从天而降的掌法,叫 electron,主要是前端来弄,可是这东东体积太大了,当然体积在当前客户这里不是首要问题。首要问题是这东西虽然很厉害,但这里的前端不会 node。还有一些方案,浏览器插件配合、WPF 这些也都不用讲了。


    所以有没有一种方法,不需要会 node,不需要后端语言(比如java/C#),不需要安装依赖(比如运行库、浏览器插件)、兼容现有前端已写好的页面和接口,如果需要调用系统 API(例如文件、IO、进程),前端只需要调用 js 方法传参即可,有点像 JSBridge。


    就针对于我个人的见识层面和需求层面来说,约等于没有。所以我打算自己弄个(挖坑)。但我不能说我要挖个坑啊?


    开始挖坑


    我尝试性的问技术负责人,如果使用套壳浏览器的方案,前端正常写,如果要操作文件时,前台能直接调用 js 方法,例如创建文件、打开文件、定位文件等等。您看行不?负责人问,那要检测文件修改上传可以吗?我愣了两秒,说可以(就算不能监控修改,也还有通过获取文件MD5对比的方式)。然后我能说那我会后给您提供一些 demo 你看看。


    给出一些 demo,让坑看起来没有坑


    为了证明方案的可行性和便利性,我用团队当前技术栈 Vue 为当前文件系统可能用到的操作都提供了示例:



    • 进程操作。创造子进程、使用系统程序。

    • 文件定位。给定一个文件路径,让资源管理器直接定位到它。

    • 文件后台下载。后台下载文件到指定位置。

    • 文件后台上传。后台上传上件到指定接口。

    • 文件操作。文件创建、修改、重命名、删除……

    • 文件打开。以默认程序或指定的系统程序打开指定文件。


    定制好图标、描述。打包成这个单独的 exe,体积 1.2M,好像有点大了,将就吧。


    image.png


    实测几次没问题之后,发给技术负责人。


    技术负责人:『???』


    不知道当时负责人心里是不是在想:什么玩意?没事做吗?给我发个 exe?病毒吗?你很能吗?产品试用期过了吗?


    // 可以编程式创建窗口。
    const view = await new main.View(`http://baidu.com`)
    view.form.show() // 显示窗口

    前端对该方案的实测效果


    也可以指定本地文件,例如 desktop.html 。


    {
    "page": "desktop.html",
    "pageShow": true
    }

    过一会,前端同事转发负责人与他的沟通给我,然后一脸懵逼的问他到底要做什么?沟通一波之后,原来会议内容还没同步给前端同事,讲了好半天客户需求,才慢慢知道要搞一个客户端。但他又再次陷入震惊,我一前端,凭什么让我搞客户端?我不会啊!


    我突然感觉我有点难。然后我说:『不是我要你搞客户端。是客户要客户端。我只是提供了一个方案,你看得行不』。


    然后让其先把我提供的 dmeo 在他电脑上试试看各功能是否正常。


    image.png


    很感动,果然没有问题。


    然后同事问:『那这东西把我现在写的系统放进去能正常用吗?』


    真是个问题,我说:『不知道,你试试,建议路由 hash 模式。』


    然后同事直接把自己的系统的链接放进去,尝试了一下一些自己原有的功能,是正常的。


    image.png


    然后同事问:『这东西基于什么技术,关键词?』


    我答:『webview。』


    然后同事搜索了一下甩我一张截图,接着问:『和 edge 一样吗?』


    我说:『不严格等于,但约等于。』


    也不知道同事看了这句话有没有骂我,搁这给我玩哲学?


    同事又问:『客户电脑是 win7 的,客户电脑没装 edge 浏览器怎么办?』


    我:『拿我的 demo 上去,能打开就是支持的。』


    理论上,如果客户电脑上没有 webview 环境,会自动安装。但客户那边网络环境是不通外网的,所以我让直接试。


    image.png


    然后一会之后,实测下来,还好就是支持的。


    接下来就是前端同事尝试在已有的项目中去使用我提供的 api 去操作系统上的内容了。经过一波沟通,实测也是没有问题。


    疯狂暗示,我挖的坑,与我无关


    一边在沟通如何入坑我的轮子的同时,一边我又提供了前端实现桌面程序的其他方案供其选择,时时不忘提醒,你看哪个合适你用哪个。这个 exe 是我封装的,API 只有示例,详细文档来不及写,桌面程序我也没有太多的经验。


    image.png


    PS:意思就是,方案由你选,如果选择我的轮子,某些功能我也要先研究一会,某些功能我也可能解决不了。还有我现在主要重心在产品上,所以可能没时间研究新功能。 /手动狗头保命。


    1700618482469.png


    小惊喜


    虽然知道 electorn 麻烦,但我这确实没有文档,也麻烦,但是过了几天,突然收到消息:


    image.png


    image.png


    然后我邪魅一笑(哈哈哈哈,入坑了!入坑了!入坑了!)。


    后记


    这个坑不是我故意挖的,是有意挖的。因为有一个想法,开发一个简单的桌面程序,只使用前端语言开发,暂只考虑在 windows 上运行,希望开发体验像在浏览器中一样,然后程序的样子像是本地应用一样,调用本地文件、系统命令、后台运行、托盘菜单这些都没有问题。


    我调研了一些常见的方案,发现他们大多数都不喜欢,经常体积太大或要求其他语言,有一 neutralino 看起来实现上是想要的,但 api 太少,所以决定含泪造坑,在 api 设计上会考虑贴近 neutralino 便于两者迁移。


    代码仓库在这里 github.com/wll8/sys-sh… 


    作者:程序媛李李李李李蕾
    来源:juejin.cn/post/7303798788720115749
    收起阅读 »

    如何科学的进行Android包体积优化

    这篇文章会分享小厂如何做包体积优化相关主题,涉及内容包括:1) Android包体积优化的一种可能是比较标准的推进做法,2) 大致流程的心路历程和思考方式,3) 如何去总结和分享你们进行过的包体积优化项目。本文不仅仅是一篇分享,还是我个人比较喜欢的总结报告写法...
    继续阅读 »

    这篇文章会分享小厂如何做包体积优化相关主题,涉及内容包括:1) Android包体积优化的一种可能是比较标准的推进做法,2) 大致流程的心路历程和思考方式,3) 如何去总结和分享你们进行过的包体积优化项目。本文不仅仅是一篇分享,还是我个人比较喜欢的总结报告写法。



    一、前言


    移动 App 特别关注投放转化率指标,而 App 包体积是影响用户新增的重要因素,而 App 的包体积又是影响投放转化率的重要因素。


    Google 2016 年公布的研究报告显示,包体积每上升 6MB 就会带来下载转化率降低 1%, 当包体积增大到 100MB 时就会有断崖式的下跌 。对于应用商店,普遍有一个流量自动下载的最大阈值,如 应用宝,下载的app超过100M,用流量下载时,会弹窗提示用户是否继续下载,这对下载转化率影响是比较大的。


    现在流量虽然变得更廉价一点,但是用户的心理是不会变的,当 App 出现在应用市场的相同位置时,包体积越大,用户下载意愿可能越低。


    而且包体积或直接或间接地影响着下载转化率、安装时间、运行内存、磁盘空间等重要指标,所以投入精力扫除积弊、发掘更深层次的体积优化项是十分必要的。



    某手:



    • 1M = 一年kw级推广费


    某条极速版:



    • Google 2016 年公布的研究报告显示,包体积每上升 6MB 就会带来下载转化率降低 1%,当包体积增大到 100MB 时就会有断崖式的下跌。

    • 通过插件化,将常规优化后达 120M+的包体积降到了 13M 左右,最小版本降至 4M,包体积缩小至原先的 3.33%。


    某德:



    • 包体积大小,是俞xx直接拍的,就要求 x年x月x日 前削减到100M。


    某淘:



    • 包大小做得比较“霸权”“独裁”,新业务超过 1M 要总裁审批,一般在平台组都卡掉了。



    二、评估优化需求


    在开展工作前,我们首先得先有两点判断:



    1. 是否需要进行优化

    2. 优化到多少算符合预期


    那具体应该如何进行判断呢?有个比较简单的方法就是对标,找到对标对象进行对标。


    对于小厂来说,一般对标对象有:



    1. 竞品App

    2. 业内人气App


    基于对标对象,我们可以粗略的有如下判断:



    1. 如果我们App跟竞品App包体积差不多或略高,那就有必要进行包体积优化。

    2. 优化到跟业内人气App中包体积最小的那一档差不多即可。


    上述判断还是基于用户视角,假如你的 用户需求 比较简单,好几个app都可以满足, 你会安装200M的产品,还是50M的产品。再有,假如用户在App商店无意间看到你们的App,有点兴趣体验体验,但是看到包体积是200M,他有多大概率会继续下载,换成50M呢?


    三、包体积优化基本思想


    我们在做包体积优化前,一定要定好我们的大方向,我们怎么优化,能做哪些,哪些可以现在做,哪些没必要现在做,


    1. 抓各个环节


    我们最终是要优化App的包体积,那么App包组成部分有哪些,我们针对每一个部分去研究如何减少其体积,就可以达到我们最终的效果。


    2. 系统化方案先行,再来实操


    优化Android包体积这个事情,有一定的探索性,但是很多内容或者说手段都是业内在广为流传的,既然如此,我们应该总结业内可优化手段,并逐一进行分析,研究是否适合你们App,是否需要应用到你们App。如果没有方案的埋头扎进去,往往会因为陷入细节,而丢失了全局视野。


    3. 明确风险收益比及成本收益比


    方案与方案之间是有区别的。如果一个方案能减少包体积2M,但是线上可能会崩溃,你会做吗? 如果一个方案能减少包体积2M,但是开发成本要一个月,你会做吗?


    4. 明确指标以及形成一套监控防劣化体系


    干任何一件以优化为目标的事情时,一定要明确优化的指标,我们要做App的包体积优化,那么我们的指标为:减少App安装包 apk 的大小。


    当我们指标明确之后,我们还需要对现有指标进行监控,这样有两个好处:



    • 明确优化收益

    • 防止劣化


    那我们就可以在某个关键的时间节点进行包体积的统计和上报,一般时间节点有:



    • App发版打包时(粒度更粗)

    • 开发分支有新的 commit 合入时(粒度更细)


    两种粒度各有各的好处,但是目标是一样的:



    • 监控每次打包的包体积,可以行成指标曲线进行持续观察

    • 在包体积增加超出预期时进行及时报警


    5. 把包体积这个指标刻在脑子里


    自动化能发现已经发生的问题,但是把包体积这个指标刻在脑子里,能避免问题的发生。


    四、自家App包体积一览


    1. Android Apk结构


    APK 主要由五个部分组成,分别是:



    • Dex:.class 文件处理后的产物,Android 系统的可执行文件

    • Resource:资源文件,主要包括 layout、drawable、animator,通过 R.XXX.id 引用

    • Assets:资源文件,通过 AssetManager 进行加载

    • Library:so 库存放目录

    • META-INF:APK 签名有关的信息


    132d21f3-a1b6-43f6-99d0-fad7ffb81412.png


    2. 你们Apk各个部分都长啥样,长多大?


    这里选取示例App某个版本的安装包来做举例分析,下面是包结构图:


    QQ截图20231109021600.png


    浅浅分析一波包内内容


    成分体积备注
    assets文件夹77.8M下载.png 能看到Assets文件夹里,有着75M的RN bundle
    lib75.2Ma7d410c9-75ab-4c27-9d21-9c8953567d00.png 由于我们App是兼容包,即同时兼容64位、32位,所以lib目录很大
    dex16M这部分涉及到我们自己的代码及三方库代码
    res6.3M这里包含各种图片资源、布局文件等
    resources.arsc1.2M
    其他若干

    五、优化方案草案


    通过调研业内常规、非常规手段,以及结合你们App的包体积现状,可以提前对优化包体积做出比较详尽的可实现、低风险、高收益方案,注意这几个点非常重要:



    • 可实现 - 可实现可以简单理解为实现成本低,一般各种性能稳定性指标都是循序渐进的推进,所以往往一期优化时,选的实操方案都是实现成本比较低的,这样能相对快速的得到比较符合心里预期的优化效果。

    • 低风险 - 线上风险必须控制在可接受的程度,这里的风险不仅仅是说App运行的性能稳定性风险,还需要判断是否会增加线上问题排查的难度,当然还会有其他的我没提到的风险项。

    • 高收益 - 不解释


    所以基于我们需要的是可实现、低风险、高收益的方案,我们可以基于上面我贴的APK案例,来大致预演可能会采用哪些方案:


    1. 缩减动态化方案内置代码包、资源包


    一般的小厂都会比较大量的采用如RN、H5等动态化方案,不可避免的App内就会有一堆内置文件,我们看到我们示例的App中,RN内置包占了整个包体积超过 30%。当出现这种情况时,可以针对内置代码包、资源包单独立子项去推进。


    那么如何进行推进呢?有同学就会说了,业务方不让我把这些玩意儿从APK里面删掉,说影响他们打开页面速度,影响页面打开速度就会影响一级指标影响收入


    这时为了说服业务方,我们就得拿出一些证据,用来证明内置包的全部移除或者部分移除并不会对业务产生影响,或者说影响足够小。那就可以采取如下一些推进步骤:



    • 明确全部内置包或者部分内置包不内置的影响,假如内置包是 RN 的页面bundle,那给业务方两个数据基本上就能够说明影响



      • 页面bundle现下比例,假如因为本地没有内置的bundle,打开页面需要同步进行等待下载完成才能加载的话,现场下载比例是个比较有说服力的数据。

      • 线上bundle更新耗时,我们可以统计用户启动App后的一段指定时间,90分位能下载多少个bundle,50分位能下载多少个,10分位、5分位能下载多少个,来告诉业务方,老用户、新用户、老用户新登录等各种场景,到达业务页面的时候,有多少比例的用户能完成bundle更新。



    • 明确什么样的资源需要内置,同样用RN页面bundle举例,假如App的首页就是RN页面,那这玩意儿就必须内置了,假如一个页面在犄角旮旯,日pv才不到100,那就完全可以不需要内置。

    • 给出内置资源名单

    • 拿着内置名单和上面明确的不内置影响统计,找业务方拉会, 这一步最好是从上往下进行推进,而不是同级推进


    2. 分架构打包


    分架构打包能减少libs文件夹体积,libs文件夹里会包含不同架构的 so 库集合。


    首先我们最终apk包是要上传到应用商店的,应用商店得支持双包上传。答案确实是支持,且应用商店推荐双包上传。


    Android 官方也是有相关的api支持分架构打包:


    splits {
    // 基于不同的abi架构配置不同的apk
    abi {
    // 必须为true,打包才会为不同的abi生成不同的apk
    enable true
    // 默认情况下,包含了所有的ABI。
    // 所以使用reset()清空所有的ABI,再使用include指定我们想要生成的架构armeabi-v7a、arm-v8a
    reset()
    // 逗号分隔列表的形式指定 Gradle 应针对哪些 ABI 生成 APK。只与 reset() 结合使用,以指定确切的 ABI 列表。
    include "armeabi-v7a", "arm64-v8a"
    // 是否生成通用的apk,也就是包含所有ABI的apk。如果设为 true,那么除了按 ABI 生成的 APK 之外,Gradle 还会生成一个通用 APK。
    universalApk true
    }
    }

    这里需要注意的是,线上并不是所有的手机都支持 64位 的安装包,应用商店可以双包上传,线上灰度更新可以下发32位的安装包或者是 32/64 兼容包。


    3. So 压缩


    分架构打包是减少so的数量,so压缩是减少so的单个体积。


    android:extractNativeLibs="true"

    android:extractNativeLibs = true时,gradle打包时会对工程中的so库进行压缩,最终生成apk包的体积会减小。


    但用户在手机端进行apk安装时,系统会对压缩后的so库进行解压,从而造成用户安装apk的时间变长。


    若开发人员未对android:extractNativeLibs进行特殊配置,android:extractNativeLibs默认值:



    • minSdkVersion < 23 或 Android Gradle plugin < 3.6.0情况下,打包时 android:extractNativeLibs=true

    • minSdkVersion >= 23 并且 Android Gradle plugin >= 3.6.0情况下,打包时android:extractNativeLibs=false


    4. 大so动态下发


    我们能看到有些so库单个体积超大,放在apk里,就算能压缩,压缩后体积仍然很大,可能会占到 app体积超过 10%。针对这种情况,选择动态下发。


    a91934eb-420a-4e74-8b38-5e899876d89a.png


    动态下发的so如何进行加载


    我们采用ASM的方案,对代码中所有的 System.load、System.loadLibrary 进行hook,进入到我们自己的逻辑,这样我们就可以走下面流程:



    1. 下载so库

    2. 解压so库

    3. 校验so库

    4. 加载so库


    这里需要注意的一点就是,当动态下发的so没有下载、解压、校验、加载完之前,如果用户进入到了相关的业务场景,必须有兜底机制。比如在样例App的场景中,使用了 opencv 库来做图片的二维码识别,当so没下载下来时,要识别二维码就会被兜底到 zxing。


    而且由于我们有较好的Hook框架的封装,所以我们需要hook时,仅仅需要进行配置即可:



    这里可以参考我之前的博客和github上demo项目:


    基于 Booster ASM API的配置化 hook 方案封装 - 掘金 (juejin.cn)


    AndroidSimpleHook-github


    5. 大文件压缩优化,对内置的未压缩大文件进行,压缩文件用高压缩率的压缩算法


    假如apk里有内置的大文件,可以通过对其进行压缩从而减少包体积,压缩时可以选用高压缩率的算法。


    6. 代码优化



    • 去除无用代码、资源


    去除无用代码我们可以用官方的Lint检查工具



    • 去除无用三方库



    • 减少ENUM的使用


    每减少一个ENUM可以减少大约1.0到1.4 KB的大小,假如有10000个枚举对象,那不就减少了14M?美滋滋啊,但实际上具体还是要看项目代码情况来考虑,毕竟不是所有的项目里都有 10000 个枚举。


    7. 资源优化



    • 无用资源文件清理


    去除无用资源文件可以通过lint工具来做,也可以通过微信开源的 ApkChecker来完成。


    github.com/Tencent/mat…


    图片压缩、转webp

    图片压缩可以使用TinyPng,AndroidStudio也有相关插件,官方术语就是:


    使用智能的无损压缩技术来减少图片文件的大小,通过智能的选择颜色的数量,减少存储的字节,但是效果基本是和压缩前一样的。


    图片着色器

    相同图片只是颜色不同的话,完全可以只放一个图片,在内存里操作 Drawable,完成颜色替换。


    图片动态下发

    如果本地有大图,且使用要求为未压缩,或者压缩之后仍然很大,可以适当的选择动态下载该图。


    resources.arsc资源混淆

    resources.arsc这个文件是存放在APK包中的,他是由AAPT工具在打包过程中生成的,他本身是一个资源的索引表,里面维护者资源ID、Name、Path或者Value的对应关系,AssetManager通过这个索引表,就可以通过资源的ID找到这个资源对应的文件或者数据。


    通过对apk 中的resources.arsc进行内容修改,来对apk进行深度压缩。这里可以采用微信的AndResGuard方案。


    github.com/shwenzhang/…


    8. 三方库优化


    移除无用三方库

    移除无用三方库需要人肉扫描 build.gradle 文件,一个一个的去检查依赖的三方库是否被我们代码所使用。


    功能重复的三方库整合

    特别常见的case,RN 用的图片加载库是 Fresco,客户端用的图片加载库是 Glide,他们都是用来加载图片,可以通过删除一个库,让项目依赖的库少一个。



    • 修改三方库源码,不需要的代码进行剔除


    一个三方库往往不会被用到全部功能,比如曾经很火的 XUtils github.com/wyouflf/xUt…


    XUtils是一个工具大杂烩,但是假如我只用它来加载图片,其他工具是不是就完全无用,可以进行剔除。


    9. 去除 DebugItem 包含的 debug信息与行号信息


    在讲解什么是 deubg 信息与行号信息之前,我们需要先了解 Dex 的一些知识。


    我们都知道,JVM 运行时加载的是 .class 文件,而 Android 为了使包大小更加紧凑、运行时更加高效就发明了 Dalvik 和 ART 虚拟机,两种虚拟机运行的都是 .dex 文件,当然 ART 虚拟机还可以同时运行 oat 文件。


    所以 Dex 文件里的信息内容和 Class 文件包含的信息是一样的,不同的是 Dex 文件对 Class 中的信息做了去重,一个 Dex 包含了很多的 Class 文件,并且在结构上有比较大的差异,Class 是流式的结构,Dex 是分区结构,Dex 内部的各个区块间通过 offset 来进行索引。


    为了在应用出现问题时,我们能在调试的时候去显示相应的调试信息或者上报 crash 或者主动获取调用堆栈的时候能通过 debugItem 来获取对应的行号,我们都会在混淆配置中加上下面的规则:


    -keepattributes SourceFile, LineNumberTable

    这样就会保留 Dex 中的 debug 与行号信息。根据 Google 官方的数据,debugItem 一般占 Dex 的比例有 5% 左右


    10. ReDex


    ReDex 是 Facebook 开发的一个 Android 字节码的优化工具。它提供了 .dex 文件的读写和分析框架,并提供一组优化策略来提升字节码。官方提供预期优化效果:对dex文件优化为 8%


    github.com/facebook/re…


    11. R 文件瘦身


    当 Android 应用程序被编译,会自动生成一个 R 类,其中包含了所有 res/ 目录下资源的 ID。包括布局文件layout,资源文件,图片(values下所有文件)等。在写java代码需要用这些资源的时候,你可以使用 R 类,通过子类+资源名或者直接使用资源 ID 来访问资源。R.java文件是活动的Java文件,如MainActivity.java的和资源如strings.xml之间的胶水


    通过R文件常量内联,达到R文件瘦身的效果。


    github.com/bytedance/B…


    12. 可能的更多方案


    除了我上面列到的一些,市面上还有一些其他的方案,有复杂的有不复杂的,有收益高的有收益低的,大家可以在掘金上搜索Android包体积优化,就能搜到大部分了,当然,在大厂里,还会有很多很极致的方案,比如:



    • 去掉kotlin生成的许多模板代码、判空代码

    • 去除布局文件里不需要的冗余内容

    • ...


    思想是这么个思想,大家在实操的时候,思路就是先调研方案,调研完成之后再选型。


    六、基于风险收益比及成本收益比敲定最终实现方案


    这一步的重点是:明确风险收益比及成本收益比


    方案与方案之间是有区别的。



    • 如果一个方案能减少包体积2M,但是线上可能会崩溃,你会做吗?

    • 如果一个方案能减少包体积2M,但是开发成本要一个月,你会做吗?


    这里我们在示例App的基础上,对每个手段进行仔细分析,包括:



    1. 预期效果

    2. 成本

    3. 风险


    就这样,当我们制定完成我们的目标方案之后,就可以放手干了。


    手段预期效果成本是否要做进度备注
    重点优化项
    - 缩减RN 内置bundle预期效果:177.43M -> 114.43MRN 内置bundle缩减,xxxx版本带上
    - 分架构打包,64位、32位分开打包预期效果:32位:117.43M -> 71.9M 64位:117.43M -> 87.6Mxxxx
    - so压缩方案预期效果:32位:71.9M -> 55.5M 64位:87.6M -> 58.3Mxxxx
    - 大so文件动态下发预期效果:32位:55.5M -> 50.7M 64位:58.3M -> 51.7Mxxxx
    大文件优化
    - zip优化,对内置的压缩文件替换压缩算法预期针对 assets 文件针对不同类型文件选取不同高压缩率算法
    代码优化 (dex文件为 15.6M)
    - 去除无用代码Android Lintxxx
    - 减少ENUM的使用全部代码 enum类 一共60个,就算全删了也只是减少 84kxxx每减少一个ENUM可以减少大约1.0到1.4 KB的大小
    资源优化 (目前res目录大小为 6.3M,emoji目录大小为 770k)
    - 无用资源文件清理Android Lintxxx用ApkChecker再跑一次
    - 图片压缩、转webpTinyPngxxx
    - 图片着色器xxx
    - 图片动态下发主要是针对比较大的图,实际上经过TinyPng 压缩后,图片大小已经大大减小xxx
    - resources.arsc资源混淆AndResGuard两年不维护,花了一小时没完全跑起来,但看到了大致优化效果,1.3M -> 920kgithub.com/shwenzhang/…
    三方库优化 (dex文件为 15.6M)
    - 移除无用三方库检查一下
    - 移除无用三方so库
    - 功能重复三方库整合
    - 修改三方库源码,不需要的代码进行剔除
    极致优化,附 ByteX 插件情况
    - 去除 DebugItem 包含的 debug信息与行号信息mp.weixin.qq.com/s/_gnT2kjqp…
    - ReDex对dex文件优化为 8%,即在当前dex总和 15.6M的基础上,可以减少 1.2MDex 压缩,首次启动解压dexhttps://github.com/facebook/redexhttps://juejin.cn/post/7104228637594877965
    - R 文件瘦身现成方案:github.com/bytedance/B… failed for task ':app:transformClassesWithShrinkRFileForQaRelease'.> java.lang.RuntimeException: This feature requires ASM7

    七、确定优化效果


    当我们进行了一系列的或大或小的改动之后,如何描述最终优化效果?给两张对比图不就行了,无图言X。




    八、总结


    大家在进行一些有挑战性或者是比较有意义的项目时,其实可以多进行总结,总结的好处有什么我就不多解释了,懂的都懂哈。


    比如我们这里可以装模作样的这样总结一下:


    做的好的方面



    1. 足够系统化

    2. 前置调研足够充分

    3. 风险、收益、成本考虑足够充分

    4. 各方面沟通足够充分

    5. 优化决心足够大


    也可以告诉自己及读者几句话



    1. 这是一个系统的需要持续去投入人力的事情,万万不可有了一定结果之后放松警惕

    2. 别人能做的,我们也能做,只要有足够的决心去做

    3. 做事不能太讲究所谓的方法论,不然会掉入陷阱,但是确实要讲究方法论

    4. 有些事情你做好了,可能仅仅是因为做这个事情的人是你,如果是别人来做,也能将这件事情做好


    九、展望


    一般来说,进行总结之后,都得来一些展望,给未来的自己挖点坑,给总结的读者画点饼。比如我们这里就可以这样继续装模作样的展望一下:


    上面已经反复提及了,当前这一期的优化工作,重点考量的指标是风险收益比及成本收益比,所以一些极致的或者成本收益比较高的优化手段并没有被采用,所以后续还是有很多事情可以深入的干下去。



    1. resources.arsc资源混淆

    2. 去除 DebugItem 包含的 debug信息与行号信息

    3. ReDex

    4. R 文件瘦身

    5. So unwind 优化

    6. kotlin相关优化

    7. ...


    十、真正的总结


    这里我就发散性的随便总结下吧。。。也不深入纠结了。



    1. 包体积优化是个庞大的工程项目,不仅仅需要优化,还需要防劣化,优化过程中还会涉及到业务冲突,说白了就是某些东西从APK包中移除了,或多或少会有些影响,还需要去跟业务方达成意见一致。

    2. 大家不管在做什么优化课题时,最好是分步骤分工期的去进行,不要一口吃成胖子,如果上来就追求完全极致的优化效果,往往会带来两个负面风险:1)  优化工期拉长,时间成本成倍增加,2)可能影响线上App或者线下各种工具的运行稳定性。

    3. 系统化的调研、成本 + 风险 + 收益的总和考虑非常重要,任何优化项目开始进行或者进行过程中,都需要牢牢的印在脑子里,每日三省你身。

    4. 遇到困难不要畏惧,各种优化项目往往会遇到很多阻力,比如方案实现太难、业务沟通太难等等,一块石头硬磕磕不动的时候换个方向磕,换方向也磕不动那就换块石头磕,比如假设业务方沟通不动,那就换个角度,把你和业务方放在同一角色上,给业务方找收益或者。

    5. 做的项目是啥或者说研究的方向是啥其实不是最重要的,我们这种普通程序员更重要的是解决问题的能力,因为你们做的事情,换个人用同样的时间成本或者更多的时间成本,往往也能做好,所以是你做好的这件事情其实没那么重要,更重要的是遇到其他问题或者有其他的疑难杂症和系统性问题时,知道你一定能做好。


    作者:邹阿涛涛涛涛涛涛
    来源:juejin.cn/post/7302982924987039796
    收起阅读 »

    HarmonyOS 页面传值跳转

    介绍 本篇主要介绍如何在HarmonyOS中,在页面跳转之间如何传值 HarmonyOS 的页面指的是带有@Entry装饰器的文件,其不能独自存在,必须依赖UIAbility这样的组件容器 如下是官方关于State模型开发模式下的应用包结构示意图,Page就是...
    继续阅读 »

    介绍


    本篇主要介绍如何在HarmonyOS中,在页面跳转之间如何传值


    HarmonyOS 的页面指的是带有@Entry装饰器的文件,其不能独自存在,必须依赖UIAbility这样的组件容器


    如下是官方关于State模型开发模式下的应用包结构示意图,Page就是带有@Entry装饰器的文件


    0000000000011111111.20231123162458.56374277887047155204379708661912.png


    那么在页面跳转时,在代码层面最长路径其实是有两步 1,打开UIAbility 2. 打开Page


    整体交互效果


    页面传值demo.png


    传值理论



    1. 基于LocalStorage

    2. 基于EventHub

    3. 基于router


    准备


    请参照官方指导,创建一个Demo工程,选择Stage模型


    代码实践


    1.定制主入口页面


    功能



    1. 页面曝光停留时长计算

    2. 增加进入二级页面入口


    import systemDateTime from '@ohos.systemDateTime'
    import router from '@ohos.router'

    @Entry
    @Component
    struct Index {
    @State message: string = '页面跳转'

    private showDuration: number = 0

    onPageShow() {

    this.showDuration = 0
    systemDateTime.getCurrentTime(false, (error, data) => {
    if(!error){
    this.showDuration = data
    }
    })

    }

    build() {
    Row() {
    Column() {
    Text(this.message)
    .fontSize(50)
    .fontWeight(FontWeight.Bold)
    .onClick(()=>{
    systemDateTime.getCurrentTime(false, (error, data) => {
    router.pushUrl({ url: 'pages/OpenPage', params: {
    "from": "pages/Home.ets",
    "data": {
    "duration":(data - this.showDuration)
    }
    } })
    .then(() => {
    console.info('Succeeded in jumping to the second page.')
    }).catch((error) => {
    console.log(error)
    })
    })
    })
    }
    .width('100%')
    }
    .height('100%')
    }

    }

    2.添加二级页面


    注意

    OpenPage.ets需要在main_pages.json中的注册


    {
    "src": [
    "pages/Index" //主入口页面
    ,"pages/OpenPage" //二级页面
    ,"pages/Test" //三级页面
    ,"pages/LocalStorageAbilityPage" //三级页面
    ]
    }

    功能



    1. 展示主入口页面停留时间

    2. 添加通过UIAbility方式打开页面的入口

    3. 添加通过router.pushUrl方式打开页面的入口


    /**
    * 路由 3.1/4.0 文档
    * https://developer.harmonyos.com/cn/docs/documentation/doc-references-V3/js-apis-router-0000001478061893-V3#ZH-CN_TOPIC_0000001523808578__routerpushurl9
    *
    */

    import router from '@ohos.router';
    import common from '@ohos.app.ability.common';


    @Entry
    @Component
    struct OpenPageIndex{
    @State extParams: string = ''
    private expParamsO: Object
    private context = getContext(this) as common.UIAbilityContext;

    aboutToAppear(){
    this.expParamsO = router.getParams();
    this.extParams = JSON.stringify(this.expParamsO, null, '\t');
    }

    build(){
    Column(){

    List(){
    ListItemGr0up() {
    ListItem() {
    Text(this.extParams)
    .width('96%')
    .fontSize(18)
    .fontColor(Color.Green)
    .backgroundColor(Color.White)
    }.width('100%')
    .align(Alignment.Start)
    .backgroundColor(0xFFFFFF)
    .borderRadius('16vp')
    .padding('12vp')

    }.divider({
    strokeWidth: 1,
    startMargin: 0,
    endMargin: 0,
    color: '#ffe5e5e5'
    })

    ListItemGr0up() {

    ListItem() {
    Text('启动UIAbility页面')
    .width('96%')
    .fontSize(18)
    .fontColor(Color.Black)
    .backgroundColor(Color.White)
    }.width('100%')
    .height(50)
    .align(Alignment.Start)
    .backgroundColor(0xFFFFFF)
    .padding({ left: 10 })
    .onClick(() => {
    this.startAbilityTest('LocalStorageAbility')
    })

    ListItem() {
    Text('启动@Entry页面')
    .width('96%')
    .fontSize(18)
    .fontColor(Color.Black)
    .backgroundColor(Color.White)
    }.width('100%')
    .height(50)
    .align(Alignment.Start)
    .backgroundColor(0xFFFFFF)
    .padding({ left: 10 })
    .onClick(() => {
    router.pushUrl({ url: 'pages/Test', params: {
    "from": "pages/OpenPage.ets"
    } })
    .then(() => {
    console.info('Succeeded in jumping to the second page.')
    }).catch((error) => {
    console.log(error)
    })
    })

    }.divider({
    strokeWidth: 1,
    startMargin: 0,
    endMargin: 0,
    color: '#ffe5e5e5'
    })

    }.width('100%').height('90%')
    .divider({
    strokeWidth: px2vp(20),
    startMargin: 0,
    endMargin: 0,
    color: '#ffe5e5e5'
    })

    }.width('100%').height('100%')
    .padding({ top: px2vp(111) , left: '12vp', right: '12vp'})
    .backgroundColor('#ffe5e5e5')
    }

    async startAbilityTest(name: string) {
    try {
    let want = {
    deviceId: '', // deviceId为空表示本设备
    bundleName: 'com.harvey.testharmony',
    abilityName: name,
    parameters:{
    from: 'OpenPage.ets',
    data: {
    hello: 'word',
    who: 'please'
    }
    }
    };
    let context = getContext(this) as common.UIAbilityContext;
    await context.startAbility(want);
    console.info(`explicit start ability succeed`);
    } catch (error) {
    console.info(`explicit start ability failed with ${error.code}`);
    }

    }

    }


    3. 添加三级页面


    注意

    先要添加注册一个新的容器,这里命名为:LocalStorageAbility.ets
    容器需要在module.json5中声明


      {
    "name": "LocalStorageAbility",
    "srcEntry": "./ets/entryability/LocalStorageAbility.ets",
    "description": "$string:EntryAbility_desc",
    "icon": "$media:icon",
    "label": "$string:EntryAbility_label",
    "startWindowIcon": "$media:icon",
    "startWindowBackground": "$color:start_window_background"
    }

    import window from '@ohos.window';
    import UIAbility from '@ohos.app.ability.UIAbility';


    let para:Record<string,string> = { 'PropA': JSON.stringify({ 'from': 'LocalStorageAbility'}) };
    let localStorage: LocalStorage = new LocalStorage(para);

    export default class LocalStorageAbility extends UIAbility {

    storage: LocalStorage = localStorage

    onCreate(want, launchParam) {

    }

    onWindowStageCreate(windowStage: window.WindowStage) {
    super.onWindowStageCreate(windowStage)

    windowStage.loadContent('pages/LocalStorageAbilityPage', this.storage, (err, data) => {
    if (err.code) {
    return;
    }

    setTimeout(()=>{
    let eventhub = this.context.eventHub;
    console.log(para['PropA'])
    eventhub.emit('parameters', para['PropA']);
    }, 0)

    });
    }

    }

    Test.ets和LocalStorageAbilityPage.ets需要在main_pages.json中的注册


    {
    "src": [
    "pages/Index" //主入口页面
    ,"pages/OpenPage" //二级页面
    ,"pages/Test" //三级页面
    ,"pages/LocalStorageAbilityPage" //三级页面
    ]
    }

    功能



    1. 展示基于LocalStorage,EventHub,router 三种传值方式的数据


    LocalStorageAbilityPage.ets 文件



    • 展示LocalStorage,EventHub方式的数据


    import router from '@ohos.router';
    import common from '@ohos.app.ability.common';

    // 通过GetShared接口获取stage共享的LocalStorage实例
    let storage = LocalStorage.GetShared()

    @Entry(storage)
    @Component
    struct LocalStorageAbilityPageIndex {
    @State message: string = ''
    // can access LocalStorage instance using
    // @LocalStorageLink/Prop decorated variables
    @LocalStorageLink('PropA') extLocalStorageParms: string = '';

    context = getContext(this) as common.UIAbilityContext;

    aboutToAppear(){
    this.eventHubFunc()
    }

    build() {
    Row() {
    Column({space: 50}) {

    Column({space: 10}){
    Text('LocalStorage传值内容')
    Text(JSON.stringify(JSON.parse(this.extLocalStorageParms), null, '\t'))
    .fontSize(18)
    .fontColor(Color.Green)
    .backgroundColor(Color.White)
    .width('100%')
    .padding('12vp')
    .borderRadius('16vp')
    }

    Column({space: 10}){
    Text('eventHub传值内容')
    Text(this.message)
    .fontSize(18)
    .fontColor(Color.Green)
    .backgroundColor(Color.White)
    .width('100%')
    .padding('12vp')
    .borderRadius('16vp')
    }

    }.width('100%').height('100%')
    .padding({ top: px2vp(111) , left: '12vp', right: '12vp'})
    .backgroundColor('#ffe5e5e5')
    }
    .height('100%')

    }

    eventHubFunc() {
    this.context.eventHub.on('parameters', (...data) => {
    this.message = JSON.stringify(JSON.parse(data[0]), null, '\t')
    });
    }

    }

    作者:harvey_fly
    来源:juejin.cn/post/7306447457151942690
    收起阅读 »

    android之阿拉伯语适配及注意细节

    1.  AndroidManifest.xml配置文件中的 标签下,配置元素 android:supportsRtl="true"。此时当系统语言切换的时候,你的 App 也会跟着切换 UI 布局为镜像后的效果。若未增加该元素,在xml中切换语言时,...
    继续阅读 »

    1.  AndroidManifest.xml配置文件中的 标签下,配置元素 android:supportsRtl="true"。此时当系统语言切换的时候,你的 App 也会跟着切换 UI 布局为镜像后的效果。

    若未增加该元素,在xml中切换语言时,会提示 image.png 增加后,可在xml文件中查看反转后的效果 2.  新增value-ar文件夹

    image.png

    image.png

    image.png 把values/strings.xml文件复制到values-ar文件中,逐条翻译即可。

    1. layout中的Left/Right修改为Start/End

    可使用Android Studio中自带的工具:“工具栏”-“Refactor”-“Add right-to-Left(RTL)Support” image.png

    注意事项:

    • 1).此时会把所依赖gradle里的xml文件列出,记得删除,不要转换。

    image.png

    • 2). 该工具只适用于项目的app模块,无法直接应用于依赖模块。如果需要在依赖模块中进行RTL转换,要逐个打开并手动进行相应的修改。
    • 3). Start属性在LTR中对应Left,End属性在LTR中对应Right,在API 17开始支持,为了兼容低版本,可以同时有Left和Start。

        即在“Add right-to-Left(RTL)Support”工具中,不勾选“Replace Left/Right Properties with Start/End Properties”

    image.png

    1. 返回icon、下一个icon等,要针对阿拉伯语新建一个文件夹,放镜像后的图片,规则如下:

    mipmap-xhdpi->mipmap-ldrtl-xhdpi

    drawable->drawable-ldrtl

    最终镜像的图片要UI同事提供,临时修改看效果可以使用镜像图片的网站:http://www.lddgo.net/image/flip

    1. TextView、EditText:利用全局样式,在style.xml中定义,在xml里使用style=”@style/xxx”即可
    • 1). TextView
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
           ...
           <item name="android:textViewStyle">@style/TextViewStyle.TextDirectionitem>
           ...
    style>
    <style name="TextViewStyle.TextDirection" parent="android:Widget.TextView">
            <item name="android:textDirection">localeitem>
    style>
    • 2). EditText
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
           ...
           <item name="editTextStyle">@style/EditTextStyle.Alignmentitem>
           ...
    style>
    <style name="EditTextStyle.Alignment" parent="@android:style/Widget.EditText">
            <item name="android:textAlignment">viewStartitem>
            <item name="android:gravity">startitem>
            <item name="android:textDirection">localeitem>
    style>
    1. 其他细节
    • 1).固定ltr,如阿拉伯语下的“99%”要从左到右展示,可在xml中使用
    android:layoutDirection ="ltr"
    • 2).获取当前系统语言Locale.getDefault().getLanguage()

    判断是否为阿拉伯语:"ar".equals(Locale.getDefault().getLanguage())

    判断是否为英语:"en".equals(Locale.getDefault().getLanguage())

    • 3). drawable/xxx_selector.xml中item里有android:drawable,如勾选框。

    drawable有android:autoMirrored属性,将selector的该属性设置为true,就可以让drawable在RTL布局下进行反转

    image.png

    • 4).进度条的默认进度指示是从左到右,使用leftMargin;在阿拉伯语下,进度指示从右到左,使用rightMargin属性
    • 5).阿拉伯语环境下,使用SimpleDateFormat格式化时间字符串的时候,会显示为:٢٠١٥-٠٩-١٨ ٠٧:٠٣:٤٩。若要展示:2023-09-067:10:45,可以使用Locale.ENGLISH参数
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH);
    Date now=new Date();
    System.out.println(sdf .format(now));
    • 6). 加载html可用 tv.setText(Html.fromHtml(getResources().getString(R.String.xxx));
    • 7). 开机导航中设置了阿拉伯语,当前页面布局要刷新,可以重写activity的onConfigurationChanged()方法,如在该方法里重置下一步箭头、指示器样式等

    image.png

    • 8).ViewPager

    若是ViewPager,可使用第三方控件RtlViewPager替换: 521github.com/diego-gomez…,添加依赖,单纯替换原ViewPager即可

    implementation 'com.booking:rtlviewpager:1.0.1' 

    类似三方控件: 521github.com/duolingo/rt…

    或者使用androidx的ViewPager2替换: developer.android.google.cn/jetpack/and…,支持RTL布局

    image.png

    image.png

    • 9). 固定RTL字符串的顺序

    问题现象:EditText带hint,密码可见、不可见时,会调用如下方法进行设置

    image.png 此时会影响hint的展示:在勾选时,hint的结束字符在右侧;不勾选时,hint的结束字符在左侧。

    image.png

    image.png

    解决方法:此时要使用Unicode控制字符来限制整个字符串的显示方向:\u202B 和 \u202C。

    image.png

    有以下两种方法

    a.  java代码

    image.png

    b.  strings.xml

    image.png

    最终效果:

    image.png

    image.png

    10). Blankj的toast展示异常

    android工具类Blankj的toast工具类在展示阿拉伯语时为空或者部分展示,建议使用1.30.6 及以上版本

    image.png

    github.com/Blankj/Andr…

    11). RTL布局中出现双光标/光标截断的情形

    image.png

    在布局文件内加上如下两个属性即可:

    android:textDirection="anyRtl"
    android:textAlignment="viewStart"

    若还未解决

    1.可查看是否使用了android:textCursorDrawable=“@null”,若有,可尝试去掉该句。

    2.在AndroidManifest.xml中查看当前App/Activity的主题,比较老的项目可能使用了android:Theme.NotitleBar/android:Theme.Light等轻量级主题,如下所示:




    收起阅读 »

    谨以此文,纪念 查理·芒格

    今早起床刷手机,看到有位朋友发了芒格的在《穷查理宝典》中的一段话,当时以为只是普通的一句话。在中午的时候,才刷到,原来芒格竟然在今天去世了。 上班的时候,就一直在想,之前正好写过《穷查理宝典》的读书笔记,今天一定得发篇文章纪念下,奈何,加班到现在(晚上11点)...
    继续阅读 »

    今早起床刷手机,看到有位朋友发了芒格的在《穷查理宝典》中的一段话,当时以为只是普通的一句话。在中午的时候,才刷到,原来芒格竟然在今天去世了。


    上班的时候,就一直在想,之前正好写过《穷查理宝典》的读书笔记,今天一定得发篇文章纪念下,奈何,加班到现在(晚上11点),才开始动笔。


    其实也谈不上怎么崇拜了解芒格本人,但《穷查理宝典》却真的影响了我很多。


    记得20年的冬天,刚毕业的那段时光,还在北京的时候,经常坐了一个多小时的地铁,从大屯路到广渠门地铁站,去那里的中信书店,待上个一整天。那阵子,估计是我看书最多的时光。而《穷查理宝典》,也是花了恰好一个周末看完的,看到头晕眼花的那种。


    当时看完的时候,真的觉得整个人都被洗涤了一番,尤其是最后的误判心理学和多元思维模型,是真的精彩,还写了篇文章,记录了一些读书感悟。当时,公众号的简介也是写着:争做跨学科思维模型者。


    图片


    一晃,原来已经3年多过去了啊,虽然也买了《穷查理宝典》的实体书了,但这3年来,好像真就没怎么翻起来过。重新了解芒格,也是在看了上次成甲老师的直播后,开始看成甲老师写的《好好思考》,开始再一次了解跨学科思维模型。


    虽然说没有再翻起来过《穷查理宝典》,但今天再看到下面的这些内容,却意外的发现,原来很多内容,早已经在潜移默化的影响我了,或许,这就是好书的魅力吧。




    1 持续学习



    • 每天睡觉前都比当天早晨聪明一点。《巴菲特传》中一半醒来时间都在读书的巴菲特、《穷查理宝典》中追求跨学科思维模型的芒格、通过阅读《Redis》小论文就能理解缓存机制的周老师、不断在微信读书中分享读书感悟的子乾哥、喜欢晚上在办公室静静思考的立明哥,告诉我要多去问为什么并思考的曙光哥。芒格说的对,身边优秀的人,没有一个是不坚持学习的。

    • 持续学习不仅仅指读书,也包括在实践、工作中的思考,成长。《大国大民》中说: 知识包括“知道”和“见识”两部分,只知道读书充其量能当一个“知道”分子,需要在实践中去不断强化、塑造自己知道的东西,去增长见识,才能称之为“知识”分子。

    • 即使连最简单的读书,自己也没有做好,之前看书总有种囫囵吐枣,完成任务的感觉。看完一本书后,往往都没多深的印象,也没有系统的获得多少东西,甚至于说过一段时间后,连书中讲了什么都记不太清楚。当然,读书还是有个好处的,有什么会莫名的对书中的一两个点产生触动,然后你会在生活中去反复利用这些点,并收获一些东西。如《拥抱可能》中的“你就算不念博士,照样会52岁”、《一生的旅途》中的:”试一试呗,反正我又不会损失什么,万一成功了呢“。


    2 跨学科思维模型



    • 利用跨学科的思维模型并不断对其进行实践,以打破固有的意识形态和“铁锤人倾向“。

    • 人总有一种避免不一致的倾向,很容易只相信自己想要的东西(就像我在做读书笔记时,记录的往往是和自己想法一致的内容);b: 不愿意去做改变,在做出一个决定后,会坚信这决定是对的,会过高的高估自己所拥有的东西(其实就是禀赋效应,就好像很多父母会高看自己的孩子,很多人觉得自己拥有的东西的最宝贵)。

    • 作为一名工科生,之前总是采取一种工科的、直线型的思维去看待事情,在看《产品思维》时,发现里面涉及很多心理学和社会学的概念,甚至于产品大牛喻军直接说:“大学没有专门的产品经理专业,最适合当产品经理的是经济学和心理学的毕业生”。任重而道远!


    3 德行合一



    • 要想得到一样东西,最可靠的方式让你自己配得上它,无论是工作还是配偶。

    • 之前的《职场适应力》培训中也说到,当你想要某个职位的时候,不是期盼好运降临到你头上,而是去提高自己的能力,只有能力到了,时机到了才会有机会(这其实和芒格说的:“只要做好准备,在人生中抓住机会,迅速的采取适当的行动“不谋而合)。在当下这刚入职几个月的阶段,啥不用想,去使劲干就是了,先多后精。

    • “如果想拥有一个好配偶,得先让自己配得上才行,毕竟,好配偶可不会眼瞎”。努力吧,放平心态,先去提升自己。


      3年过去了,真的很庆幸,认识了现在的夫人,真的是这辈子做的最正确的决定了



    4 逆向思维



    • 如说如果想过的幸福,那就反过来想,去避免那些让生活变得更坏的(懒惰、嫉妒、言而无信)的行为。

    • 想一想自己身上其实还是存在很多坏习惯: 作息不规律、阅读浅尝辄止(缺乏系统性的、深入的学习。芒格说:“其实我们不用去了解很多复杂的理论知识,很多学科的基本概念就那么几个,如经济学的成本-效益原理,数学上的统计概率原理等”)、自我服务偏好(认为“自我”有资格去做它想做的事情,比如说超前透支去满足一些实际不需要的物质需求)、自怜倾向(为自己的失败找各种理由,觉得是任务太复杂之类的,其实这就是逃脱)。

    • 上面的这些坏习惯虽说不致命,但始终是拖累自己前进的障碍,有人说:发现问题比解决问题更重要。真希望这句话也适用于坏习惯上,既然意识到了这些坏习惯,就争取努力去改正吧。


    5 近朱者赤



    • 《乌合之众》提到社会群体具有两个重要的特性:传染性 和 容易被暗示性,传染性说简单点就是从众心理,个体总是不愿意去违背群体的状态,容易被暗示性则指个体总是做出与独处时完全想法的行动。群体之于个人,其实就是环境的重要性,要去塑造让自己成长、前进的环境,远离让自己沉沦的环境。

    • 烟台集训最大的收获就是:有幸和这么一群优秀的人认识,自己要多努力,保持开放、积极的状态,才能不至于落后。今天老妈也说到一个类似的观点:在成长期,要永远向前看,向比你优秀的看,这样到达更高的高度。在你老了,需要知足常乐的时候,才需要朝后看。沉稳型的人格的自己,需要更多的动力,更多的向掌控型人格学习,才能成就更好的自己。

    • 之前也喜欢阅读,不过大都是在家里看书,很容易就被其他事情分散注意力,来书店后,看到身边这么多人在认真的看书,你多玩会手机都会觉得自惭形愧(啊哈,这好像有点像芒格说的社会认同倾向)。从烟台回来周末,看了《产品思维》,虽说不能立马应用于工作中,但至少了解了一些概念,还是稍有收获的。而且,一杯咖啡、一本书就能过一天的日子,随着自己承担的工作越来越多,只会越来越少,好好珍惜吧。记录一句,kindle,手机啥的适合读小说类的书,复杂一点的还是纸质版舒服。

    • 吴老师在集训时说:为未来多挤出当下的一点时间。其实,对职业的规划或者未来,自己有个模糊的概念,但总归不够清晰,更谈不上为此去努力当下,将很多时间浪费在了不必要的、快餐娱乐上。

    • 现在在做的事情,让我遇见了很多优秀的朋友和榜样,确实很明显的能感觉一个厉害的圈子,能让我极大的成长。


    6 始终保持感恩之心



    • 恰巧今天是教师节,想一想到了25的这个年纪了,真正的去表达感恩的行动真是少之又少,连自己老师、导师都很少去表达感激之情,今早和高中老师发了个简单节日快乐,这显然是不够的,在此也立个flag吧:今年过年,去一一给老师们拜年!


      好的,现在看,这个flag也没实现,甚至于说,研究生导师已经与世长辞。


    • 感恩身边的每一个人,走到现在,觉得生命中碰到的每个人,每个人都有很有闪光点、优点,值得我去学习、去领悟。不仅仅包括父母、长辈、师长、领导、同事、朋友、室友。甚至于连在路上碰到的陌上人、或者素不相识的网友、明星等,每个人都拥有独特的特质,要始终保持开放的心,去接受、发现每一个人好。


           最后,感谢芒格先生和《穷查理宝典》,希望我也能在这辈子,输出一些真正有价值的,被世人铭记的东西。


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

    比亚迪面试,全程八股!

    比亚迪最近几年凭借着其新能源汽车的板块大火了一把,无论是名声还是股价都涨得嘎嘎猛,但是迪子招聘编程技术岗位的人员却有两个特点: 面试难度低,对学校有一定的要求。 薪资给的和面试难度一样低。 但不管怎么,迪子也算是国内知名公司了,所以今天咱们来看看,他的校招...
    继续阅读 »

    比亚迪最近几年凭借着其新能源汽车的板块大火了一把,无论是名声还是股价都涨得嘎嘎猛,但是迪子招聘编程技术岗位的人员却有两个特点:



    1. 面试难度低,对学校有一定的要求。

    2. 薪资给的和面试难度一样低。


    但不管怎么,迪子也算是国内知名公司了,所以今天咱们来看看,他的校招 Java 技术岗的面试题都问了哪些知识点?面试题目如下:
    image.png


    1.int和Integer有什么区别?


    参考答案:int 和 Integer 都是 Java 中用于表示整数的数据类型,然而他们有以下 6 点不同:



    1. 数据类型不同:int 是基础数据类型,而 Integer 是包装数据类型;

    2. 默认值不同:int 的默认值是 0,而 Integer 的默认值是 null;

    3. 内存中存储的方式不同:int 在内存中直接存储的是数据值,而 Integer 实际存储的是对象引用,当 new 一个 Integer 时实际上是生成一个指针指向此对象;

    4. 实例化方式不同:Integer 必须实例化才可以使用,而 int 不需要;

    5. 变量的比较方式不同:int 可以使用 == 来对比两个变量是否相等,而 Integer 一定要使用 equals 来比较两个变量是否相等;

    6. 泛型使用不同:Integer 能用于泛型定义,而 int 类型却不行。


    2.什么时候用 int 和 Integer?


    参考答案:int 和 Integer 的典型使用场景如下:



    • Integer 典型使用场景:在 Spring Boot 接收参数的时候,通常会使用 Integer 而非 int,因为 Integer 的默认值是 null,而 int 的默认值是 0。如果接收参数使用 int 的话,那么前端如果忘记传递此参数,程序就会报错(提示 500 内部错误)。因为前端不传参是 null,null 不能被强转为 0,所以使用 int 就会报错。但如果使用的是 Integer 类型,则没有这个问题,程序也不会报错,所以 Spring Boot 中 Controller 接收参数时,通常会使用 Integer。

    • int 典型使用场景:int 常用于定义类的属性类型,因为属性类型,不会 int 不会被赋值为 null(编译器会报错),所以这种场景下,使用占用资源更少的 int 类型,程序的执行效率会更高。


    3.HashMap 底层实现?


    HashMap 在 JDK 1.7 和 JDK 1.8 的底层实现是不一样的。



    • 在 JDK 1.7 中,HashMap 使用的是数组 + 链表实现的。

    • 而 JDK 1.8 中使用的是数组 + 链表或红黑树实现的


    HashMap 在 JDK 1.7 中的实现如下图所示:
    image.png
    HashMap 在 JDK 1.8 中的实现如下图所示:


    4.HashMap 如何取值和存值?


    参考答案:HashMap 使用 put(key,value) 方法进行存值操作,而存值操作的关键是根据 put 中的 key 的哈希值来确定存储的位置,如果存储的位置为 null,则直接存储此键值对;如果存储的位置有值,则使用链地址法来解决哈希冲突,找到新的位置进行存储。


    HashMap 取值的方法是 get(key),它主要是通过 key 的哈希值,找到相应的位置,然后通过 key 进行判断,从而获取到存储的 value 信息。


    5.SpringBoot 如何修改端口号?


    参考答案:在 Spring Boot 中的配置文件中设置“server.port=xxx”就可以修改端口号了。


    6.如何修改 Tomcat 版本号?


    参考答案:在 pom.xml 中添加 tomcat-embed-core 依赖就可以修改 Spring Boot 中内置的 Tomcat 版本号了,如下图所示:
    image.png
    但需要注意的是 Spring Boot 和 Tomcat 的版本是有对应关系的,要去 maven 上查询对应的版本关系才能正确的修改内置的 Tomcat 版本号,如下图所示:
    image.png


    7.SpringBoot如何配置Redis?


    参考答案:首先在 Spring Boot 中添加 Redis 的框架依赖,然后在配置文件中使用“spring.redis.xxx”来设置 Redis 的相关属性,例如以下这些:


    spring:
    redis:
    # Redis 服务器地址
    host: 127.0.0.1
    # Redis 端口号
    port: 6379
    # Redis服务器连接密码,默认为空,若有设置按设置的来
    password:
    jedis:
    pool:
    # 连接池最大连接数,若为负数则表示没有任何限制
    max-active: 8
    # 连接池最大阻塞等待时间,若为负数则表示没有任何限制
    max-wait: -1
    # 连接池中的最大空闲连接
    max-idle: 8


    8.MySQL 左连接和右连接有什么区别?


    参考答案:在 MySQL 中,左连接(Left Join)和右连接(Right Join)是两种用来进行联表查询的 SQL 语句,它们的区别如下:



    1. 左连接:左连接是以左边的表格(也称为左表)为基础,将左表中的所有记录和右表中匹配的记录联接起来。即使右表中没有匹配的记录,左连接仍然会返回左表中的记录。如果右表中有多条匹配记录,则会将所有匹配记录返回。左连接使用 LEFT JOIN 关键字来表示。

    2. 右连接:右连接是以右边的表格(也称为右表)为基础,将右表中的所有记录和左表中匹配的记录联接起来。即使左表中没有匹配的记录,右连接仍然会返回右表中的记录。如果左表中有多条匹配记录,则会将所有匹配记录返回。右连接使用 RIGHT JOIN 关键字来表示。


    例如以下图片,左连接查询的结果如下图所示(红色部分为查询到的数据):
    image.png
    右连接如下图红色部分:
    image.png


    9.内连接没有匹配上会怎么?


    参考连接:内连接使用的是 inner join 关键字来实现的,它会匹配到两张表的公共部分,如下图所示:
    image.png
    所以,如果内连接没有匹配上数据,则查询不到此数据。


    小结


    以上是比亚迪的面试题,但并不是说比亚迪的面试难度一定只有这么低。因为面试的难度通常是根据应聘者的技术水平决定的:如果应聘者的能力一般,那么通常面试官就会问一下简单的问题,然后早早结束面试;但如果应聘者的能力比较好,面试官通常会问的比较难,以此来探寻应聘者的技术能力边界,从而为后续的定薪、定岗来做准备,所以大家如果遇到迪子的面试也不要大意。


    作者:Java中文社群
    来源:juejin.cn/post/7306723594816733235
    收起阅读 »

    苹果M3系列登场,性能翻倍,M1、M2已成时代眼泪

    M3 芯片来了。 在苹果今天的 Scary Fast 发布会上,苹果正式发布了 M3、M3 Pro、M3 Max 芯片。苹果表示,这是首款采用 3 纳米工艺技术的 PC 芯片,允许将更多晶体管封装到更小的空间中,以提高速度和效率。除了芯片更新,苹果还带来了搭...
    继续阅读 »

    M3 芯片来了。



    在苹果今天的 Scary Fast 发布会上,苹果正式发布了 M3、M3 Pro、M3 Max 芯片。苹果表示,这是首款采用 3 纳米工艺技术的 PC 芯片,允许将更多晶体管封装到更小的空间中,以提高速度和效率。除了芯片更新,苹果还带来了搭载 M3 系列芯片的 MacBook Pro 以及 24 英寸版 iMac。


    图片


    除了提供更快、更高效的 CPU,这三款芯片还配备了全新的 GPU,支持光线追踪、网格着色和动态缓存。


    M3 芯片提供高达 128GB 的统一内存,其中最强大的 M3 Max 芯片配备多达 920 亿个晶体管、40 核 GPU 和 16 核 CPU。


    「Apple Silicon 彻底重新定义了 Mac 体验,其架构的每个方面都是为了性能和能效而设计的。」苹果硬件技术高级副总裁 Johny Srouji 说道。「凭借 3 纳米技术、全新 GPU 架构、更高性能的 CPU、更快的神经引擎以及对更多统一内存的支持,M3、M3 Pro 和 M3 Max 是迄今为止为个人 PC 打造的最先进的芯片。」


    图片


    M3 系列芯片采用业界领先的 3 纳米工艺


    动态缓存、网格着色和硬件加速光线追踪


    M3 系列芯片中的 GPU 代表了苹果芯片图形架构的最大飞跃。与传统 GPU 不同,它具有动态缓存功能,可以实时分配硬件中本地内存的使用,这是业界首创。


    借助 M3 系列芯片,硬件加速光线追踪也首次出现在 Mac 上。光线追踪可对光与场景交互时的属性进行建模,使应用程序能够创建极其逼真且物理精确的图像。再加上新的图形架构,专业应用程序的速度可达 M1 系列芯片的 2.5 倍。


    此外,新的 GPU 为 Mac 带来了硬件加速的网格着色功能,为几何处理提供了更强大的功能和效率,并在游戏和图形密集型应用程序中实现了视觉上更复杂的场景。事实上,M3 GPU 能够以近一半的功耗提供与 M1 相同的性能,并且在峰值时性能提高高达 65%。


    图片


    M3 系列芯片中的最新 GPU 代表了苹果芯片图形架构的最大飞跃,具有动态缓存,网格着色和硬件加速光线追踪功能。


    更快更高效的 CPU


    接下来介绍 CPU 部分。苹果对 M3 系列的最新 CPU 进行了改进。高性能核心比 M1 系列芯片快 30%。与 M1 芯片的能效核心相比,M3 中的能效核心带来的速度提升最高可达 50%。这些内核共同打造出一款 CPU,可提供与 M1 相同的多线程性能,而功耗仅为 M1 的一半,并且在峰值功率下性能提高高达 35%。


    图片


    图片


    统一内存架构,最高可达 128GB


    M3 系列中的每个芯片都采用统一的内存架构,可提供高带宽、低延迟和无与伦比的功效。此外,M3 芯片支持的内存容量最高达 128GB,从而使得过去无法在笔记本电脑上处理的工作流成为可能,例如 AI 开发者现在可运行包含数十亿个参数规模的 Transformer 模型。


    图片


    M3 的统一内存架构,支持高达 24GB 的高速统一内存。


    图片


    M3 Pro 芯片的统一内存架构支持高达 36GB 的高速统一内存,确保用户能够随时随地使用 MacBook Pro 处理更大型的项目。 图片


    M3 Max 芯片的统一内存架构支持高达 128GB 的高速统一内存,可以完成过去无法在笔记本电脑上完成的任务


    用于 AI 和视频的自定义引擎


    M3、M3 Pro 和 M3 Max 还具有增强的神经引擎,可加速 ML 模型。神经引擎比 M1 系列芯片快 60%,使 AI/ML 工作流程更快,同时还能将数据保留在设备上以保护隐私。


    M3 系列中的三款芯片还具有先进的媒体处理引擎,为最流行的视频编解码器提供硬件加速,包括 H.264、HEVC、ProRes 和 ProRes RAW。并且媒体引擎首次支持 AV1 解码,实现流媒体服务的节能播放,进一步延长电池寿命。


    图片


    最后,我们一起来看看 M3 系列芯片的各个配置。


    M3 拥有 250 亿个晶体管,比 M2 多 50 亿个,支持高达 24GB 的统一内存,标准 8 核 CPU+10 核 GPU 配置。


    图片


    M3 的最新 GPU 在游戏《Myst》上的表现


    M3 Pro 由 370 亿个晶体管和 18 核 GPU 组成,可在处理图形密集型任务时提供极快的性能。GPU 的速度比 M1 Pro 快 40%。对统一内存的支持高达 36GB,使用户在外出时也能在 MacBook Pro 上处理大型项目。12 核 CPU 设计有 6 个性能核心和 6 个能效核心,单线程性能比 M1 Pro 快 30%。


    图片


    借助 M3 Pro 的 GPU 和 CPU,在 Adobe Photoshop 中拼接和处理巨幅全景照片等任务的速度比以往任何时候都快。


    M3 Max 将晶体管数量提升到 920 亿个,将专业性能提升到新的水平。40 核的 GPU 比 M1 Max 快 50%,支持高达 128G 的统一内存,这使 AI 开发人员能够处理更大的、具有数十亿参数的 Transformer 模型。16 核 GPU 具有 12 个性能核心和 4 个能效核心,性能惊人,比 M1 Max 快 80%。M3 Max 拥有两个 ProRes 引擎,无论是使用 DaVinci Resolve、Adobe Premiere Pro 还是 Final Cut Pro,都能快速流畅地处理最高分辨率内容的视频后期制作工作。M3 Max 专为需要在 MacBook Pro 中获得最高性能和业界领先电池续航时间的专业人士而设计。


    图片


    参考链接:


    http://www.apple.com/newsroom/20…


    http://www.theverge.com/2023/10/30/…


    作者:机器之心
    来源:juejin.cn/post/7296016154408714294
    收起阅读 »

    只改了五行代码接口吞吐量提升了10多倍

    背景 公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。 当时一想,500/s吞吐量还不简单。Tomcat按照100个线...
    继续阅读 »

    背景


    公司的一个ToB系统,因为客户使用的也不多,没啥并发要求,就一直没有经过压测。这两天来了一个“大客户”,对并发量提出了要求:核心接口与几个重点使用场景单节点吞吐量要满足最低500/s的要求。


    当时一想,500/s吞吐量还不简单。Tomcat按照100个线程,那就是单线程1S内处理5个请求,200ms处理一个请求即可。这个没有问题,平时接口响应时间大部分都100ms左右,还不是分分钟满足的事情。


    然而压测一开,100 的并发,吞吐量居然只有 50 ...


    image.png


    而且再一查,100的并发,CPU使用率居然接近 80% ...




    从上图可以看到几个重要的信息。


    最小值: 表示我们非并发场景单次接口响应时长。还不足100ms。挺好!


    最大值: 并发场景下,由于各种锁或者其他串行操作,导致部分请求等待时长增加,接口整体响应时间变长。5秒钟。有点过分了!!!


    再一看百分位,大部分的请求响应时间都在4s。无语了!!!


    所以 1s钟的 吞吐量 单节点只有 50 。距离 500 差了10倍。 难受!!!!


    分析过程


    定位“慢”原因



    这里暂时先忽略 CPU 占用率高的问题



    首先平均响应时间这么慢,肯定是有阻塞。先确定阻塞位置。重点检查几处:



    • 锁 (同步锁、分布式锁、数据库锁)

    • 耗时操作 (链接耗时、SQL耗时)


    结合这些先配置耗时埋点。



    1. 接口响应时长统计。超过500ms打印告警日志。

    2. 接口内部远程调用耗时统计。200ms打印告警日志。

    3. Redis访问耗时。超过10ms打印告警日志。

    4. SQL执行耗时。超过100ms打印告警日志。


    上述配置生效后,通过日志排查到接口存在慢SQL。具体SQL类似与这种:


    <!-- 主要类似与库存扣减 每次-1 type 只有有限的几种且该表一共就几条数据(一种一条记录)-->
    <!-- 压测时可以认为 type = 1 是写死的 -->
    update table set field = field - 1 where type = 1 and filed > 1;

    上述SQL相当于并发操作同一条数据,肯定存在锁等待。日志显示此处的等待耗时占接口总耗时 80% 以上。


    二话不说先改为敬。因为是压测环境,直接先改为异步执行,确认一下效果。实际解决方案,感兴趣的可以参考另外一篇文章:大量请求同时修改数据库表一条记录时应该如何设计


    PS:当时心里是这么想的: 妥了,大功告成。就是这里的问题!绝壁是这个原因!优化一下就解决了。当然,如果这么简单就没有必要写这篇文章了...


    优化后的效果:


    image.png


    嗯...


    emm...


    好! 这个优化还是很明显的,提升提升了近2倍。




    此时已经感觉到有些不对了,慢SQL已经解决了(异步了~ 随便吧~ 你执行 10s我也不管了),虽然对吞吐量的提升没有预期的效果。但是数据是不会骗人的。


    最大值: 已经从 5s -> 2s


    百分位值: 4s -> 1s


    这已经是很大的提升了。


    继续定位“慢”的原因


    通过第一阶段的“优化”,我们距离目标近了很多。废话不多说,继续下一步的排查。


    我们继续看日志,此时日志出现类似下边这种情况:


    2023-01-04 15:17:05:347 INFO **.**.**.***.50 [TID: 1s22s72s8ws9w00] **********************
    2023-01-04 15:17:05:348 INFO **.**.**.***.21 [TID: 1s22s72s8ws9w00] **********************
    2023-01-04 15:17:05:350 INFO **.**.**.***.47 [TID: 1s22s72s8ws9w00] **********************

    2023-01-04 15:17:05:465 INFO **.**.**.***.234 [TID: 1s22s72s8ws9w00] **********************
    2023-01-04 15:17:05:467 INFO **.**.**.***.123 [TID: 1s22s72s8ws9w00] **********************

    2023-01-04 15:17:05:581 INFO **.**.**.***.451 [TID: 1s22s72s8ws9w00] **********************

    2023-01-04 15:17:05:702 INFO **.**.**.***.72 [TID: 1s22s72s8ws9w00] **********************

    前三行info日志没有问题,间隔很小。第4 ~ 第5,第6 ~ 第7,第7 ~ 第8 很明显有百毫秒的耗时。检查代码发现,这部分没有任何耗时操作。那么这段时间干什么了呢?



    1. 发生了线程切换,换其他线程执行其他任务了。(线程太多了)

    2. 日志打印太多了,压测5分钟日志量500M。(记得日志打印太多是有很大影响的)

    3. STW。(但是日志还在输出,所以前两种可能性很高,而且一般不会停顿百毫秒)


    按照这三个思路做了以下操作:


    首先,提升日志打印级别到DEBUG。emm... 提升不大,好像增加了10左右。


    然后,拆线程 @Async 注解使用线程池,控制代码线程池数量(之前存在3个线程池,统一配置的核心线程数为100)结合业务,服务总核心线程数控制在50以内,同步增加阻塞最大大小。结果还可以,提升了50,接近200了。


    最后,观察JVM的GC日志,发现YGC频次4/s,没有FGC。1分钟内GC时间不到1s,很明显不是GC问题,不过发现JVM内存太小只有512M,直接给了4G。吞吐量没啥提升,YGC频次降低为2秒1次。


    唉,一顿操作猛如虎。


    PS:其实中间还对数据库参数一通瞎搞,这里不多说了。




    其实也不是没有收获,至少在减少服务线程数量后还是有一定收获的。另外,已经关注到了另外一个点:CPU使用率,减少了线程数量后,CPU的使用率并没有明显的下降,这里是很有问题的,当时认为CPU的使用率主要与开启的线程数量有关,之前线程多,CPU使用率较高可以理解。但是,在砍掉了一大半的线程后,依然居高不下这就很奇怪了。


    此时关注的重点开始从代码“慢”方向转移到“CPU高”方向。


    定位CPU使用率高的原因


    CPU的使用率高,通常与线程数相关肯定是没有问题的。当时对居高不下的原因考虑可能有以下两点:



    1. 有额外的线程存在。

    2. 代码有部分CPU密集操作。


    然后继续一顿操作:



    1. 观察服务活跃线程数。

    2. 观察有无CPU占用率较高线程。


    在观察过程中发现,没有明显CPU占用较高线程。所有线程基本都在10%以内。类似于下图,不过有很多线程。


    image.png


    没有很高就证明大家都很正常,只是多而已...


    此时没有下一步的排查思路了。当时想着,算了打印一下堆栈看看吧,看看到底干了啥~


    在看的过程中发现这段日志:


    "http-nio-6071-exec-9" #82 daemon prio=5 os_prio=0 tid=0x00007fea9aed1000 nid=0x62 runnable [0x00007fe934cf4000]
    java.lang.Thread.State: RUNNABLE
    at org.springframework.core.annotation.AnnotationUtils.getValue(AnnotationUtils.java:1058)
    at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.resolveExpression(AbstractAspectJAdvisorFactory.java:216)
    at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory$AspectJAnnotation.<init>(AbstractAspectJAdvisorFactory.java:197)
    at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAnnotation(AbstractAspectJAdvisorFactory.java:147)
    at org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(AbstractAspectJAdvisorFactory.java:135)
    at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvice(ReflectiveAspectJAdvisorFactory.java:244)
    at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.instantiateAdvice(InstantiationModelAwarePointcutAdvisorImpl.java:149)
    at org.springframework.aop.aspectj.annotation.InstantiationModelAwarePointcutAdvisorImpl.<init>(InstantiationModelAwarePointcutAdvisorImpl.java:113)
    at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisor(ReflectiveAspectJAdvisorFactory.java:213)
    at org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory.getAdvisors(ReflectiveAspectJAdvisorFactory.java:144)
    at org.springframework.aop.aspectj.annotation.BeanFactoryAspectJAdvisorsBuilder.buildAspectJAdvisors(BeanFactoryAspectJAdvisorsBuilder.java:149)
    at org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator.findCandidateAdvisors(AnnotationAwareAspectJAutoProxyCreator.java:95)
    at org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator.shouldSkip(AspectJAwareAdvisorAutoProxyCreator.java:101)
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:333)
    at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:291)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:455)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1808)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:620)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:353)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:233)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1282)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1243)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:494)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
    at cn.hutool.extra.spring.SpringUtil.getBean(SpringUtil.java:117)
    ......
    ......

    上边的堆栈发现了一个点: 在执行getBean的时候,执行了createBean方法。我们都知道Spring托管的Bean都是提前实例化好放在IOC容器中的。createBean要做的事情有很多,比如Bean的初始化,依赖注入其他类,而且中间还有一些前后置处理器执行、代理检查等等,总之是一个耗时方法,所以都是在程序启动时去扫描,加载,完成Bean的初始化。


    而我们在运行程序线程堆栈中发现了这个操作。而且通过检索发现竟然有近200处。


    通过堆栈信息很快定位到执行位置:


    <!--BeanUtils 是 hutool 工具类。也是从IOC容器获取Bean 等价于 @Autowired 注解 -->
    RedisTool redisTool = BeanUtils.getBean(RedisMaster.class);

    而RedisMaster类


    @Component
    @Scope("prototype")
    public class RedisMaster implements IRedisTool {
    // ......
    }

    没错就是用了多例。而且使用的地方是Redis(系统使用Jedis客户端,Jedis并非线程安全,每次使用都需要新的实例),接口对Redis的使用还是比较频繁的,一个接口得有10次左右获取Redis数据。也就是说执行10次左右的createBean逻辑 ...


    叹气!!!


    赶紧改代码,直接使用万能的 new 。


    在看结果之前还有一点需要提一下,由于系统有大量统计耗时的操作。实现方式是通过:


    long start = System.currentTimeMillis();
    // ......
    long end = System.currentTimeMillis();
    long runTime = start - end;


    或者Hutool提供的StopWatch:


    这里感谢一下huoger 同学的评论,当时还误以为该方式能够降低性能的影响,但是实际上也只是一层封装。底层使用的是 System.nanoTime()。


    StopWatch watch = new StopWatch();
    watch.start();
    // ......
    watch.stop();
    System.out.println(watch.getTotalTimeMillis());

    而这种在并发量高的情况下,对性能影响还是比较大的,特别在服务器使用了一些特定时钟的情况下。这里就不多说,感兴趣的可以自行搜索一下。





    最终结果:



    image.png





    排查涉及的命令如下:



    查询服务进程CPU情况: top –Hp pid


    查询JVM GC相关参数:jstat -gc pid 2000 (对 pid [进程号] 每隔 2s 输出一次日志)


    打印当前堆栈信息: jstack -l pid >> stack.log


    总结


    结果是好的,过程是曲折的。总的来说还是知识的欠缺,文章看起来还算顺畅,但都是事后诸葛亮,不对,应该是事后臭皮匠。基本都是边查资料边分析边操作,前后花费了4天时间,尝试了很多。



    • Mysql : Buffer Pool 、Change Buffer 、Redo Log 大小、双一配置...

    • 代码 : 异步执行,线程池参数调整,tomcat 配置,Druid连接池配置...

    • JVM : 内存大小,分配,垃圾收集器都想换...


    总归一通瞎搞,能想到的都试试。


    后续还需要多了解一些性能优化知识,至少要做到排查思路清晰,不瞎搞。




    最后5行代码有哪些:



    1. new Redis实例:1

    2. 耗时统计:3

    3. SQL异步执行 @Async: 1(上图最终的结果是包含该部分的,时间原因未对SQL进行处理,后续会考虑Redis原子操作+定时同步数据库方式来进行,避免同时操数据库)


    TODO


    问题虽然解决了。但是原理还不清楚,需要继续深挖。



    为什么createBean对性能影响这么大?



    如果影响这么大,Spring为什么还要有多例?


    首先非并发场景速度还是很快的。这个毋庸置疑。毕竟接口响应时间不足50ms。


    所以问题一定出在,并发createBean同一对象的锁等待场景。根据堆栈日志,翻了一下Spring源码,果然发现这里出现了同步锁。相信锁肯定不止一处。


    image.png


    org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean


    image.png



    System.currentTimeMillis并发度多少才会对性能产生影响,影响有多大?



    很多公司(包括大厂)在业务代码中,还是会频繁的使用System.currentTimeMillis获取时间戳。比如:时间字段赋值场景。所以,性能影响肯定会有,但是影响的门槛是不是很高。



    继续学习性能优化知识




    • 吞吐量与什么有关?


    首先,接口响应时长。直接影响因素还是接口响应时长,响应时间越短,吞吐量越高。一个接口响应时间100ms,那么1s就能处理10次。


    其次,线程数。现在都是多线程环境,如果同时10个线程处理请求,那么吞吐量又能增加10倍。当然由于CPU资源有限,所以线程数也会受限。理论上,在 CPU 资源利用率较低的场景,调大tomcat线程数,以及并发数,能够有效的提升吞吐量。


    最后,高性能代码。无论接口响应时长,还是 CPU 资源利用率,都依赖于我们的代码,要做高性能的方案设计,以及高性能的代码实现,任重而道远。



    • CPU使用率的高低与哪些因素有关?


    CPU使用率的高低,本质还是由线程数,以及CPU使用时间决定的。


    假如一台10核的机器,运行一个单线程的应用程序。正常这个单线程的应用程序会交给一个CPU核心去运行,此时占用率就是10%。而现在应用程序都是多线程的,因此一个应用程序可能需要全部的CPU核心来执行,此时就会达到100%。


    此外,以单线程应用程序为例,大部分情况下,我们还涉及到访问Redis/Mysql、RPC请求等一些阻塞等待操作,那么CPU就不是时刻在工作的。所以阻塞等待的时间越长,CPU利用率也会越低。也正是因为如此,为了充分的利用CPU资源,多线程也就应运而生(一个线程虽然阻塞了,但是CPU别闲着,赶紧去运行其他的线程)。



    • 一个服务线程数在多少比较合适(算上Tomcat,最终的线程数量是226),执行过程中发现即使tomcat线程数量是100,活跃线程数也很少超过50,整个压测过程基本维持在20左右。


    作者:FishBones
    来源:juejin.cn/post/7185479136599769125
    收起阅读 »

    2023燃烧自己-外包仔的年终总结

    2023 年终总结 哈喽,大家好啊,深漂2年多的原子。今天下班,地铁上看手机时不自觉的看了眼日期,居然快12月了,思绪良多,年近末尾,2023即将过去,趁着拖延症还没犯,赶紧写下年终总结。今年是波澜曲折的一年,各种新闻怪事充斥在时间长河里,这一年中做成了些许事...
    继续阅读 »

    2023 年终总结


    哈喽,大家好啊,深漂2年多的原子。今天下班,地铁上看手机时不自觉的看了眼日期,居然快12月了,思绪良多,年近末尾,2023即将过去,趁着拖延症还没犯,赶紧写下年终总结。今年是波澜曲折的一年,各种新闻怪事充斥在时间长河里,这一年中做成了些许事情,也有特殊的经历...


    0x01 小小的成就



    • 读的书



      • 经济类



        • 《小岛经济学》



          • 阅读的章节:前六章

          • 推荐理由:以原始经济进化为现代经济,阐述经济中的基本概念,通俗易懂,经济入门的读物

          • 缺点:部分观点有失偏颇,需要阅读其他书籍做样本对比



        • 《1844经济学哲学手稿》



          • 阅读的章节:工资、资本利润、地租、异化劳动和私有财产

          • 推荐理由:作为上面《小岛经济学》的补充,独特的角度切入,如果想要找到为什么工作痛苦的答案,这本书就有,基本上是怼上面《小岛经济学》的





      • 计算机类



        • 《UNIX环境高级编程》



          • 阅读的章节:Unix 基础、标准与实现、文件目录、标准I/O库

          • 推荐理由:底层API,比如我阅读I/O相关章节之后,就很容易理解 Kafka、MySQL、Redis 等软件不同的I/O实现是为什么。标准之间的关系是什么,例如ISO C和 IEEE POSIX,以及Linux之间的关系



        • 《图解设计模式》



          • 阅读的章节:全部

          • 推荐理由:设计模式入门读物,有配套代码,缺点就是缺乏实际项目的示例(最好是带有优化前后对比的效果的书)



        • 《SQL经典实例》



          • 阅读的章节:基本上作为工具书,查对于写SQL的例子

          • 推荐理由:SQL示例比较齐全,遇到比较复杂的SQL可以参考





      • 英语(看的少)



        • 《赖氏经典英语语法》

        • 《英語魔法師之語法俱樂部》





    • 做的事



      • B站(学习方向)



        • 《图解设计模式》全章视频

        • 牛客网SQL专题全视频

        • 牛津书虫(两个故事)阅读视频、基本句型语法视频



      • smallchat 源码阅读

      • 工作一:



        • Spring Batch 项目落地,用于数据同步,解决了实际的使用问题,扩展些许接口,供组内同学编写标准化处理代码

        • 部分项目微服务化改造



      • 工作二:



        • Redis 缓存使用,高并发接口支持 QPS 30000 实现,涉及多级缓存和分布式锁的使用

        • Kafka MQ使用,理解高性能原理(需要理解 《UNIX环境高级编程》书中的I/O相关章节)

        • 树形SQL查询,with recursive + 索引优化查询,效果相差几十倍

        • 并发 version 插入问题,insert (select max(version) + 1) 解决,利用SQL的串行执行

        • 解析数据构建 ClickHouse SQL语句,动态参数,涉及一点点编译的问题(如何判断一串字符串是一个符合语法规则的表达式?),antlr4没研究明白,后面也没解决

        • 了解到主流数据清洗方案,物化视图,袋鼠云等






    0x02 特殊的经历


    由于工作的项目,每个迭代都需要做相应重复的工作,日积月累,处理事情的风格不一致,导致我差点失业。后来又经过自己准备的面试,第一个团队基金方向,面试说我太骄傲不好管理???,第二个团队是内容方向,面试也是顺利通过。


    这件事情的反思就是微信请屏蔽当前领导!不要过多投入,与领导只有工作利益的关系。如果工作不顺心应该乘早了断,如果不能够短期换工作,请做好情绪宣泄口(比如培养爱好,我今年就去学了游泳)


    以前总是天天加班到12点,后面被告知离场,当时是非常气愤的,觉得卸磨杀驴,后面想想如果当时不良的情绪不能控制,慢慢的整个团队都会充斥这种情绪,显然作为管理者的角度考虑,必须得弄走。


    前面说到差点失业是啥意思?因为是在外包公司,离场对于公司而言你就是一个负资产了。他们会想尽办法赶你走并且不给你任何赔偿,我当然是很气愤的,但气愤没用,后面咨询律师,研究劳动法。要考虑收益和风险,值不值得和公司硬刚,然后被通知的第二天(这里通知之后的有一个月缓冲,继续在客户驻场)我就开始收集证据(打卡记录、微信工作聊天、邮件等信息),准备跟公司硬刚,劳动仲裁和诉讼。在这里我想提醒的是各位至少要明白,如果开除你你赔偿能拿多少钱,这种小案子请律师处理大概在6k左右,如果起诉胜利后,可以要求用人单位承担5k的费用,就是说你用1K去博赔偿,而且还会有时间上的法律执行,所以要考虑风险与收益后在决定。还有就是社保、医保、公积金不实缴,离职后可以去申请要求实缴,这个申请不需要你跟公司刚,是公积金管理局跟公司之间的博弈,你只需要申请即可。


    外包公司第一招就是通知你待业,跟你谈待业,这个期间不要签署任何文件,电子邮件要明确回复拒绝待业,隔三岔五的发送邮件给公司要求提供劳动条件,作为你想要劳动的依据。可能是公司扛不住看我态度很坚决,后面又安排我面试,后面还是在原来的客户的公司转去了其他团队。


    所以总结一下就是:第一要有工作情绪的控制,要有职业规划(尽早离开),要屏蔽该屏蔽的人,第二是要有法律意识,拿不定注意可以咨询律师,再不济可以委托给律师,律师会告诉你怎么固定证据。


    0x03 反思总结


    技术上:计算机其实也就那些东西,计算机的素养四大件(算法数据结构、网络、编译原理、操作系统),那些东西就在那,没有什么学习路径,工作面试的无非就是八股文+场景题+算法+画饼艺术(让对方觉得你很值得)


    学习上:不要犹豫不觉,纠结于哪本书好,在样本不足的情况下听别人说其实没什么用,直接去豆瓣关键字搜索看起来,当你看了一部分有了样本才能辨别好坏,大部分人停留纠结在哪个好而原地踏步。


    从我自己的观察来看,我是一个受环境影响的人,周围的人很强,我就很想跟他们保持同一水平超过一点点就行了,所以要认识多一些优秀的人来激励自己,例如推特圈刘能大师、迟先生、等等,作为保持学习的目标,(我发现推上好多前 pingcap 的人啊,哈哈哈 pingcap 输送人才?


    0x04 未来展望


    想把英语这个穷人核心技能学好,早日脱离外包的苦难命运,向偏底层一点点的工作转变,让工作提供给自己情绪价值实现正向循环。


    对象?不存在的,来就来,不来就不来,其实一个人精神充裕,每天都有自己的事情做也是很开心的~


    我其实没什么生活的压力,所以在30岁之前尽可能多的去经历,去做想做的事情,像小虎一样燃烧自己(说个笑话)


    然后就是更多的书籍,经济学要看完,哲学要开始学起来


    作者:原子jk
    来源:juejin.cn/post/7303797715392708660
    收起阅读 »

    电话背调,我给他打了8分

    前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。 离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息...
    继续阅读 »

    前段时间招聘的一位开发,待了两三周,拿到了京东的offer,离职了。在离职的后一天,接到了他新公司的背调电话,几乎每项都给他打了8分。这个分数打的有点虚,单纯只是为了不影响他下家的入职。


    离职之前,收到他在飞书上查看电话号码的消息,大概也猜到是在填写背调人信息,但自始至终,他也没打一声招呼,让给个好评。


    离职最后一天,办完手续,没跟任何人打一个招呼,不知什么时候就消失了。


    当初他刚入职一周时,其实大家都已经看出他在沟通上有很大问题,还想着如何对他有针对性的安排工作和调整,发挥他的长处,避免他的短处。但没想到这么快就离职了。在他提离职时,虽没过多挽留,但给了一些过来人的建议,很明显也听不进去。


    站在旁观者的角度来看,他的职业生涯或即将面临到的事几乎能看得清清楚楚,但他有自己的坚持,别人是没办法的。


    就着这事,聊聊最近对职场上关于沟通的一些思考:


    第一,忌固执己见


    职场中最怕遇到的一种人就是固执己见的人。大多数聪明人,在遇到固执己见的人时,基本上都会在三言两语之后停止与其争辩。因为,人一旦在自己的思维层次形成思维闭环,是很难被说服的。


    而对于固执己见的人,失去的是新的思维、新的思想、纠错学习的机会,甚至是贵人的相助。试想一下,本来别人好像给你提建议,指出一条更好的路,结果换来的是争辩,是抬杠,聪明人都会敬而远之,然后默默地在旁边看着你掉坑里。


    真正牛的人,基本上都是兼听则明,在获得各类信息、建议之后,综合分析,为己所用。


    第二,不必说服,尊重就好


    站在另外一个方面,如果一件事与己无关,别人有不同的意见,或者这事本身就是别人负责,那么尊重就好,不必强行说服对方,不必表现自己。


    曾看到两个都很有想法的人,为一件事争论好几天,谁也无法说服谁。一方想用权力压另一方,另一方也不care,把简单的事情激化,急赤白脸的。


    其实争论的核心只是展现形式不同而已,最终只是在争情绪、争控制感、争存在感而已,大可不必。


    对于成年人,想说服谁都非常难的。而工作中的事,本身就没有对错,只有优劣,大多数时候试一下就知道了。


    有句话说的非常好,“成年人的世界只做筛选,不做教育”。如果说还能做点什么,那就是潜移默化的影响别人而已。


    第三,不懂的领域多听少说


    如果自己对一个领域不懂,最好少发表意见,多虚心学习、请教即可。任正非辞退写《万言书》的员工的底层逻辑就是这个,不懂,不了解情况,还草率提建议,只是哗众取宠、浪费别人时间。


    如果你不懂一个领域,没有丰富的背景知识和基础理论支撑,在与别人沟通的过程中,强行提建议,不仅露怯,还会惹人烦。即便是懂,也需要先听听别人的看法和视角解读。


    站在另一个角度,如果一个不懂的人来挑战你的权威,质疑你的决定,笑一笑就好,不必与其争辩。


    郭德纲的一段相声说的好:如果你跟火箭专家说,发射火箭得先抱一捆柴,然后用打火机把柴点着,发射火箭。如果火箭专家看你一眼,就算他输。


    第四,没事多夸夸别人


    在新公司,学到的最牛的一招就是夸人。之前大略知道夸人的效果,但没有太多的去实践。而在新公司,团队中的几个大佬,身体力行的在夸人。


    当你完成一件事时,夸“XXX,真牛逼!”,当你解决一个问题时,夸“还得是XXX,不亏是这块的专家”。总之,每当别人有好的表现时,总是伴随着夸赞和正面响应。于是整个团队的氛围就非常好。


    这事本身也不需要花费什么成本,就是随口一句话的事,而效果却非常棒。与懂得“人捧人,互相成就彼此,和气生财”的人相处,是一种非常愉悦的体验。


    前两天看到一条视频,一位六七岁的小姑娘指派正在玩游戏的父亲去做饭,父亲答应了。她妈妈问:你是怎么做到的?她说:夸他呀。


    看看,这么小的小孩儿都深谙的人性,我们很多成人却不懂,或不愿。曾经以为开玩笑很好,现在发现“夸”才是利器,同时一定不要开贬低性的玩笑。


    其实,职场中还有很多基本的沟通规则,比如:分清无效沟通并且及时终止谈话、适当示弱、认真倾听,积极反馈、少用反问等等。


    当你留意和思考这些成型的规则时,你会发现它们都是基于社会学和心理学的外在呈现。很有意思,也很有用。


    作者:程序新视界
    来源:juejin.cn/post/7265978883123298363
    收起阅读 »

    副业奇谈

    本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。 楔子 在一家名为叹佬鸡煲餐馆的小桌子上,坐着我和他,榴莲鸡煲溢出的香味,让人垂涎...
    继续阅读 »

    本人是95前端菜鸟一枚,目前在广州打工混口饭吃。刚好换了工作,感觉生活节奏变得慢了下来,打了这么多年工总觉得想纪录些什么,怕以后自己老了忘记自己还有这么一些风流往事。书接上回。



    楔子


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


    小聚


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


    “难道你不行了?”


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


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


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


    空手套白狼


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


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


    微信图片_20230307134118.jpg


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


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


    “那你说个der啊”


    微信图片_20230307134254.jpg


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


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


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


    QA



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



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



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



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



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



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



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



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



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



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


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

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

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


    政治问题...



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


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


    dianche.png


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


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


    省流


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


    作者:很饿的男朋友
    来源:juejin.cn/post/7207634883988635705
    收起阅读 »

    得物App安卓冷启动优化-Application篇

    前言 冷启动指标是App体验中相当重要的指标,在电商App中更是对用户的留存意愿有着举足轻重的影响。通常是指App进程启动到首页首帧出现的耗时,但是在用户体验的角度来看,应当是从用户点击App图标,到首页内容完全展示结束。 将启动阶段工作分配为任务并构造出有向...
    继续阅读 »

    前言


    冷启动指标是App体验中相当重要的指标,在电商App中更是对用户的留存意愿有着举足轻重的影响。通常是指App进程启动到首页首帧出现的耗时,但是在用户体验的角度来看,应当是从用户点击App图标,到首页内容完全展示结束。


    将启动阶段工作分配为任务并构造出有向无环图的设计已经是现阶段组件化App的启动框架标配,但是受限于移动端的性能瓶颈,高并发度的设计使用不当往往会让锁竞争、磁盘IO阻塞等耗时问题频繁出现。如何百尺竿头更进一步,在启动阶段有限的时间里,将有限的资源最大化利用,在保障业务功能稳定的前提下尽可能压缩主线程耗时,是本文将要探讨的主题。


    本文将介绍我们是如何通过对启动阶段的系统资源做统一管控,按需分配和错峰加载等手段将得物App的线上启动指标降低10%,线下指标降低34%,并在同类型的电商App中提升至Top3


    一、指标选择


    传统的性能监控指标,通常是以Application的attachBaseContext回调作为起点,首页decorView.postDraw任务执行作为结束时间点,但是这样并不能统计到dex加载以及contentProvider初始化的耗时。


    因此为了更贴近用户真实体验,在启动速度监控指标的基础上,我们添加了一个线下的用户体感指标,通过对录屏文件逐帧分析,找到App图标点击动画开始播放(图标变暗)作为起始帧,首页内容出现的第一帧作为结束帧,计算出结果作为启动耗时。


    例:启动过程为03:00 - 03:88,故启动耗时为880ms。


    1.png


    二、Application优化


    App在不同的业务场景下可能会落到不同的首页(社区/交易/H5),但是Application运行的流程基本是固定的,且很少变更,因此Application优化是我们的首要选择。


    得物App的启动框架任务在近几年已经先后做过多轮优化,常规的抓trace寻找耗时点并异步化已经不能带来明显的收益,得从锁竞争,CPU利用率的角度去挖掘优化点,这类优化可能短期收益不会特别明显,但从长远来看能够提前规避很多劣化问题。


    1.WebView优化


    App在首次调用webview的构造方法时会拉起系统对webview的初始化流程,一般会耗时200+ms,如此耗时的任务常规思路都是直接丢到子线程去执行,但是chrome内核中加入了非常多的线程检查,使得webview只能在构造它的线程中使用。


    01.png


    为了加速H5页面的启动,App通常会选择在Application阶段就初始化webview并缓存,但是webview的初始化涉及跨进程交互和读文件,因此CPU时间片,磁盘资源和binder线程池中任何一种不足都会导致其耗时膨胀,而Application阶段任务繁多,恰恰很容易出现以上资源短缺的情况。


    02.png


    因此我们将webview拆分成三个步骤,分散到启动的不同阶段来执行,这样可以降低因为竞争资源导致的耗时膨胀问题,同时还可以大幅度降低出现ANR的几率。


    04.png


    1.1 任务拆分


    a. provider预加载


    WebViewFactoryProvider是用于和webview渲染进程交互的接口类,webview初始化的第一步就是加载系统webview的apk文件,构建出classloader并反射创建了WebViewFactoryProvider的静态实例,这一操作并没有涉及线程检查,因此我们可以直接将其交给子线程执行。


    10.png


    b. 初始化webview渲染进程


    这一步对应着chrome内核中的WebViewChromiumAwInit.ensureChromiumStartedLocked()方法,是webview初始化最耗时的部分,但是和第三步是连续执行的。走码分析发现WebViewFactoryProvider暴露给应用的接口中,getStatics这个方法会正好会触发ensureChromiumStartedLocked方法。


    至此,我们就可以通过执行WebSettings.getDefaultUserAgent()来达到仅初始化webview渲染进程的目的。


    150.png


    c. 构造webview


    即new Webview()


    1.2 任务分配


    为了最大程度缩短主线程耗时,我们的任务安排如下:



    • a. provider预加载,可以异步执行,且没有任何前置依赖,因此放在Application阶段最早的时间点异步执行即可。

    • b. 初始化webview渲染进程,必须在主线程,因此放到首页首帧结束之后。

    • c. 构造webview,必须在主线程,在第二步完成时post到主线程执行。这样可以确保和第二步不在同一个消息中,降低ANR的几率。


    160.png


    1.3 小结


    尽管我们已经将webview初始化拆分为了三个部分,但是耗时占比最高的第二步在低端机或者极端情况还是可能触达ANR的阈值,因此我们做了一些限制,例如当前设备会统计并记录webview完整初始化的耗时,仅当耗时低于配置下发的阈值时,开启上述的分段执行优化。


    App如果是通过推送、投放等渠道打开,一般打开的页面大概率是H5营销页,因此这类场景不适用于上述的分段加载,所以需要hook主线程的messageQueue,解析出启动页面的intent信息,再做判断。


    受限于开屏广告功能,我们目前只能对无开屏广告的启动场景开启此优化,后续将计划利用广告倒计时的间隙执行步骤2,来覆盖有开屏广告的场景。


    170.png


    2.ARouter优化


    在当下组件化流行的时代,路由组件已经几乎是所有大型安卓App必备的基础组件,目前得物使用的是开源的ARouter框架。


    ARouter 框架的设计是它默认会将注解中注册path路径中第一个路由层级 (例如 "/trade/homePage"中的trade)作为该路由信息所的Gr0up, 相同Gr0up路径的路由信息会合并到最终生成的同一个类 的注册函数中进行同步注册。在大型项目中,对于复杂业务线同一个Gr0up下可能包含上百个注册信息,注册逻辑执行过程耗时较长,以得物为例,路由最多的业务线在初始化路由上的耗时已经来到了150+ms。


    190.png


    路由的注册逻辑本身是懒加载的,即对应Gr0up之下的首个路由组件被调用时会触发路由注册操作。然而ARouter通过SPI(服务发现)机制来帮助业务组件对外暴露一些接口,这样不需要依赖业务组件就可以调用一些业务层的视线,在开发这些服务时,开发者一般会习惯性的按照其所属的组件为其设置路由path,这使得首次构造这些服务的时候也会触发同一个Gr0up下的路由加载。


    而在Application阶段肯定需要用到业务模块的服务中的一些接口,这就会提前触发路由注册操作,虽然这一操作可以在异步线程执行,但是Application阶段的绝大部分工作都需要访问这些服务,所以当这些服务在首次构造的耗时增大时,整体的启动耗时势必会随之增长。


    2.1 ARouter Service路由分离


    ARouter采用SPI设计的本意是为了解耦,Service的作用也应该只是提供接口,所以应当新增一个空实现的Service专门用于触发路由加载,而原先的Service则需要更换一个Gr0up,后续只用于提供接口,如此一来Application阶段的其他任务就不需要等待路由加载任务的完成。


    001.png


    2.2 ARouter支持并发装载路由


    我们在实现了路由分离之后,发现现有的热点路由装载耗时总和是大于Application耗时,而为了保证在进入闪屏页之前完成对路由的加载,主线程不得不sleep等待路由装载完毕。


    分析可知ARouter的路由装载方法加了类锁,因为他需要将路由装载到仓库类中的map,这些map是线程不安全的HashMap,相当于所有的路由装载操作其实都是在串行执行,而且存在锁竞争的情况,最终导致耗时累加大于Application耗时。


    002.png


    分析trace可知耗时主要来自频繁调用装载路由的loadInto操作,再分析这里锁的作用,可知加类锁是主要是为了确保对仓库WareHouse中map操作的线程安全。


    003.png


    因此我们可以将类锁降级对Gr0upMeta这个class对象加锁(这个class是ARouter apt生成的类,对应apk中的ARouterProviderProviderxxx类),来确保路由装载过程中的线程安全,至于在此之前对map操作的线程安全问题,则完全可以通过将这些map替换为concurrentHashMap解决,在极端并发情况下会有一些线程安全问题,也可以按照图中添加判空来解决。


    009.png


    010.png


    至此,我们就实现了路由的并发装载,随后我们根据木桶效应对要预载的service进行合理分组,再放到协程中并发执行,确保最终整体耗时最短。


    011.png


    012.png


    3.锁优化


    Application阶段执行的任务多为基础SDK的初始化,其运行的逻辑通常相对独立,但是SDK之间会有依赖关系(例如埋点库会依赖于网络库),且大部分都会涉及读文件,加载so库等操作,Application阶段为了压缩主线程的耗时,会尽可能地将耗时操作放到子线程中并发运行,充分利用CPU时间片,但是这也不可避免的会导致一些锁竞争的问题。


    3.1 Load so锁


    System.loadLibrary()方法用于加载当前apk中的so库,这个方法对Runtime对象加了锁,相当于一个类锁。


    基础SDK在设计上通常会将load so的操作写到类的静态代码块中,确保在SDK初始化代码执行之前就准备好了so库。如果这个基础SDK恰巧是网络库这类基础库,会被很多其他SDK调用,就会出现多个线程同时竞争这个锁的情况。那么在最坏的情况下,此时IO资源紧张,读so文件变慢,并且主线程是锁等待队列中最后一个,那么启动耗时将远超预期。


    034.png


    为此,我们需要将loadSo的操作统一管控并收敛到一个线程中执行,强制他们以串行的方式运行,这样就可以避免以上情况的出现。值得一提的是,前面webview的provider预加载的过程中也会加载webview.apk中的so文件,因此需要确保preloadProvider的操作也放到这个线程。


    so的加载操作会触发native层的JNI_onload方法,一些so可能会在其中执行一些初始化工作,因此我们不能直接调用System.loadLibrary()方法来进行so加载,否则可能会重复初始化出现问题。


    我们最终采用了类加载的方式,即将这些so加载的代码全部挪到相关类的静态代码块中,然后再去触发这些类的加载即可,利用类加载的机制确保这些so的加载操作不会重复执行,同时这些类加载的顺序也要按照这些so使用的顺序来编排。


    78.png


    除此之外,so的加载任务不建议和其他需要IO资源的任务并发执行,在得物App中实测这两种情况下该任务的耗时相差巨大。


    4.启动框架优化


    目前常见的启动框架设计是将启动阶段的工作分配到一组任务节点中,再由这些任务节点的依赖关系构造出一个有向无环图,但是随着业务迭代,一些历史遗留的任务依赖已经没有存在的必要,但是他会拖累整体的启动速度。


    启动阶段大部分工作都是基础SDK的初始化,他们之间往往有着复杂的依赖关系,而我们在做启动优化时为了压缩主线程的耗时,通常都会找出主线程的耗时任务并丢到子线程去执行,但是在依赖关系复杂的Application阶段,如果只是将其丢到异步执行未必能有预期的收益。


    99.png


    我们在做完webview优化之后发现启动耗时并没有和预期一样直接减少了webview初始化的耗时,而是只有预期的一半左右,经分析发现我们的主线程任务依赖着子线程的任务,所以当子线程任务没有执行完时,主线程会sleep等待。


    并且webview之所以放在这个时间点初始化不是因为有依赖限制这它,而是因为这段时间主线程正好有一段比较长的sleep时间可以利用起来,但是异步的任务工作量是远大于主线程的,即便是七个子线程并发在跑,其耗时也是大于主线程的任务。


    因此想进一步扩大收益,就得对启动框架中的任务依赖关系做优化。


    66.png


    671.jpeg


    以上第一张图为优化之前得物App启动阶段任务的有向无环图,红框表示该任务在主线程执行。我们着重关注阻塞主线程任务执行的任务。


    可以观察到主线程任务的依赖链路上存在几个出口和入口特别多的任务,出口多表明这类任务通常是非常重要的基础库(例如图中的网络库),而入口多表明这个任务的前置依赖太多,他开始执行的时间点波动较大。这两点结合起来就说明这个任务执行结束的时间点很不稳定,并且将直接影响到后续主线程的任务。


    这类任务优化的思路主要是:



    • 拆解任务自身,将可以提前执行或者延后执行的操作分出去,但是分出去之前要考虑到对应的时间段还有没有时间片余量,或者会不会加重IO资源竞争的情况出现;

    • 优化该任务的前置任务,让该任务执行结束的时间点尽可能提早,就可以降低后续任务等待该任务的耗时;

    • 移除非必要的依赖关系,例如埋点库初始化只是需要注册一个监听器到网络库,并非发起网络请求。(推荐)


    可以看到我们在优化之后的第二张有向无环图里,任务的依赖层级明显变少,入口和出口特别多的任务也都基本不再出现。


    044.png


    320.png


    对比优化前后的trace,也可以看到子线程的任务并发度明显提高,但是任务并发度并不是越高越好,在时间片本身就不足的低端机上并发度越高表现可能会越差,因为更容易出锁竞争,IO等待之类的问题,因此要适当留下一定空隙,并在中低端机上进行充分的性能测试之后再上线,或者针对高中低端机器使用不同的任务编排。


    三、首页优化


    1.通用布局耗时优化


    系统解析布局是通过inflate方法读取布局xml文件并解析构建出view树,这一过程涉及IO操作,很容易受到设备状态影响,因此我们可以在编译期通过apt解析布局文件生成对应的view构建类。然后在运行时提前异步执行这些类的方法来构建并组装好view树,这样可以直接优化掉页面inflate的耗时。


    601.png


    602.png


    2.消息调度优化


    在启动阶段我们通常会注册一些ActivityLifecycleListener来监听页面生命周期,或者是往主线程post了一些延时任务,如果这些任务中有耗时操作,将会影响到启动速度,因此可以通过hook主线程的消息队列,将页面生命周期回调和页面绘制相关的msg移动到消息队列的队头,这样就可以加快首页首帧内容展示的速度。


    102.png


    详情可期待本系列后续内容。


    四、稳定性


    性能优化对App只能算作锦上添花,稳定性才是生命红线,而启动优化改造的又都是执行时机非常早的Application阶段,稳定性风险程度非常高,因此务必要在准备好崩溃防护的前提下做优化,即便有不可避免的稳定性问题,也要将负面影响降到最低。


    1.崩溃防护


    由于启动阶段执行的任务都是重要的基础库初始化,因此发生崩溃时将异常识别并吃掉的意义不大,因为大概率会导致后续崩溃或功能异常,因此我们主要的防护工作都是发生问题之后的止血


    配置中心SDK的设计通常都是从本地文件中读出缓存的配置使用,待接口请求成功后再刷新。所以如果当启动阶段命中了配置之后发生了crash,是拉不到新配置的。这种情况下只能清空App缓存或者卸载重装,会造成非常严重的用户流失。


    109.png
    崩溃回退


    对所有改动点加上try-catch保护,捕捉到异常之后上报埋点并往MMKV中写入崩溃标记位,这样该设备在当前版本下都不会再开启启动优化相关的变更,随后再抛出原异常让他崩溃掉。至于native crash则是在Crash监控的native崩溃回调里执行同样操作即可。


    1100.png
    运行状态检测


    Java Crash我们可以通过注册unCaughtExceptionHandler来捕捉到,但是native crash则需要借助crash监控SDK来捕捉,但是crash监控未必能在启动最早的时间点初始化,例如Webview的Provider的预加载,以及so库的预加载都是早于crash监控,而这些操作都涉及native层的代码。


    为了规避这种场景下的崩溃风险,我们可以在Application的起始点埋入MMKV标记位,在结束点改为另一个状态,这样一些执行时间早于配置中心的代码就可以通过获取这个标记位来判断上一次运行是否正常,如果上次启动发生了一些未知的崩溃(例如发生在crash监控初始化之前的native崩溃),那么通过这个标记位就可以及时关闭掉启动优化的变更。


    结合崩溃之后自动重启的操作,在用户视角其实是观察不到闪退的,只是会感觉到启动的耗时约是平时的1-2倍。


    0456.png
    配置有效期


    线上的技改变更通常都会配置采样率,结合随机数实现逐渐放量,但是配置下发SDK的设计通常都是默认取上次的本地缓存,在发生线上崩溃等故障时,尽管及时回滚了配置,但是缓存的设计会导致用户还会因为缓存遭遇至少一次的崩溃。


    为此,我们可以为每一个开关配置加一个配套的过期时间戳,限制当前放量的开关只在该时间戳之前生效,这样在遇到线上崩溃等故障时确保可以及时止血,而且时间戳的设计也可以避免线上配置生效的滞后性导致的crash。


    457.png


    用户视角下,添加配置有效期前后对比:


    678.jpeg


    五、总结


    至此,我们已经对安卓App中比较通用的冷启动耗时案例做了分析,但是启动优化最大的痛点往往还是App自身的业务代码,应当结合业务需求合理的进行任务分配,如果一味的靠预加载,延迟加载和异步加载是不能从根本上解决耗时问题的,因为耗时并没有消失只是转移,随之而来的可能是低端机启动劣化或功能异常。


    做性能优化不仅需要站在用户的视角,还要有全局观,如果因为启动指标算是首页首帧结束就把耗时任务都丢到首帧之后,势必会造成用户后续的体验有卡顿甚至ANR。所以在拆分任务时不仅需要考虑是否会和与其并发的任务竞争资源,还需要考虑启动各个阶段以及启动后一段时间内的功能稳定性和性能是否会受之影响,并且需要在高中低端机器上都验证下,至少要确保都没有劣化的表现。


    1.防劣化


    启动优化绝不是一次性的工作,它需要长时间的维护和打磨,基础库的一次技改可能就会让指标一夜回到解放前,因此防劣化必须要尽早落地。


    通过在关键点添加埋点,可以做到在发现线上指标劣化时迅速定位到劣化代码大概位置(例如xxActivity的onCreate)并告警,这样不仅可以帮助研发迅速定位问题,还可以避免线上特定场景指标劣化线下无法复现的情况,因为单次启动的耗时波动范围最高能有20%,如果直接去抓trace分析可能连劣化的大概范围都难以定位。


    例如两次启动做trace对比时,其中一次因为遇到IO阻塞导致某次读文件的操作都明显变慢,而另一次IO正常,这就会误导开发者去分析这些正常的代码,而实际导致劣化的代码可能因为波动正好被掩盖。


    2.展望


    对于通过点击图标启动的普通场景,默认会在Application执行完整的初始化工作,但是一些层级比较深的功能,例如客服中心,编辑收货地址这类,即使用户以最快速度直接进入这些页面,也是需要至少1s以上的操作时间,所以这些功能相关的初始化工作也是可以推迟到Application之后的,甚至改为懒加载,视具体功能的重要性而定。


    通过投放,push来做召回/拉新的启动场景通常占比较少,但是其业务价值要远大于普通场景。由于目前启动耗时主要来源于webview初始化以及一些首页预载相关的任务,如果启动落地页并不需要所有基础库(例如H5页面),那么这些我们就可以将它不需要的任务统统延迟加载,这样启动速度可以得到大幅度增长,做到真正意义上的秒开。


    *文/Jordas


    作者:得物技术
    来源:juejin.cn/post/7306447634204770319
    收起阅读 »

    终于把国外大佬的跨窗口量子纠缠粒子效果给肝出来

    web
    前言 上篇文章 尝试实现了国外大佬用Web做出来跨窗口渲染动画效果反响很大,但是仅仅只是实现了跨窗口动画效果,严格说就没有动画,还有些bug和遗憾,尤其是粒子效果,得入three.js坑,怎么办?跳啊! 硬肝了两天,实在肝不动了,看效果吧。 第一版v2效果,大...
    继续阅读 »

    前言


    上篇文章 尝试实现了国外大佬用Web做出来跨窗口渲染动画效果反响很大,但是仅仅只是实现了跨窗口动画效果,严格说就没有动画,还有些bug和遗憾,尤其是粒子效果,得入three.js坑,怎么办?跳啊!


    硬肝了两天,实在肝不动了,看效果吧。


    第一版v2效果,大粒子,粒子数量较少:
    v2 (1).gif


    第二版v2.1,小粒子,粒子数量多:
    v2.1 (1).gif


    three.js


    官方文档:threejs.org/,中文文档:three.js docs


    我第一次接触three.js,之前只是听说过,比如能弄车展啥的,感觉很厉害,就想借此机会学习下,跳进坑里才发现,这坑也太深了。随便找个教程,里面各种名词就给我弄吐了。


    先按文档 three.js docs 画个立方体,跑起来了,但是我想要球体啊,还有粒子围着中心转,这么多api学不起啊,搜教程也是杂乱无章,无从学起,咋整?找现成的啊!


    看到官网有很多现成的例子,找了一个相近的:threejs.org/examples/#w…,截了个静态图长这样:
    image.png


    找源码:three.js/examples/we…,copy到本地,代码就200行,用到的three api就几个,搜下api大概了解代码各部分的功能,基本都能看懂,然后删了多余功能,一个粒子绕球中心旋转功能就出来了。


    现在关于粒子移动、渲染、球体旋转缩放等变化api都已经基本搞懂了,然后就是痛苦折磨的计算调试了,不想再回忆了。


    动画效果的移动都是靠循环对象、计算坐标,改变粒子的position来实现的,感觉应该会有更好的现成api能简化这个过程,而且有缓冲、阻尼效果等。如果有更好的例子,欢迎大佬分享。


    总结下用到的api吧,就几个:


    构造方法



    1. THREE.PerspectiveCamera:透视投影相机,3D场景的渲染中使用得最普遍的投影模式。

    2. THREE.SceneTHREE.WebGLRenderer:场景和渲染器。

    3. THREE.TextureLoader:创建纹理,用于加载粒子贴图。

    4. THREE.SpriteMaterial:创建精灵材质。

    5. THREE.Sprite:创建精灵,用于表示粒子。

    6. THREE.Gr0up:创建对象容器,用于整体控制多个粒子,达到旋转等效果。


    属性



    1. .position.x\y\z:坐标位移;

    2. .rotation.x\y\z:粒子绕球体旋转;

    3. .position.multiplyScalar(radius):对三个向量x\y\z上分别乘以给定标量radius,用于设置粒子距球体中心距离;

    4. .scale.set:设置粒子自身的缩放

    5. .visible:控制Gr0up或粒子显隐;


    难点


    THREE.PerspectiveCamera透视投影相机下,由于是模拟人的眼睛从远处看的,所以会导致坐标上的单位跟html里的像素单位是不一致的,有一定的比例。但是判断浏览器窗口位置都是像素单位的,所以得算出这个比例、或者找到一种办法让两个单位是一致的。在外网搜到一个方案:forum.babylonjs.com/t/how-to-se…


    const perspective = 800;
    const fov = (180 * (2 * Math.atan(window.innerHeight / 2 / perspective))) / Math.PI;
    const camera = new THREE.PerspectiveCamera(fov, window.innerWidth / window.innerHeight, 1, 1000);
    camera.position.set(0, 0, perspective);

    这样设置相机后,俩个单位就是一致的。至于原理。。。看见Math.atan就头大,过!


    BroadcastChannel


    BroadcastChannel的api很简单,在一个窗口中postMessage,另一个窗口就会通过message事件接受到了。


    const channel = new BroadcastChannel('editor_channel');
    channel.postMessage({ aa: '123' });
    channel.addEventListener('message', ({ data }) => {
    console.log(data);
    });

    在此例逻辑是,进页面初始化时、或者坐标改变时,需要把当前窗口坐标postMessage发送到别的窗口中,然后再把所有窗体坐标数据都存在js全局变量里使用。


    但是这里有个问题,如果刷新其中一个窗口时,没办法立即获取别的窗口数据,因为别的窗口只有在坐标变化时才会发送数据(为了提高效率,不会在requestAnimationFrame里一直发数据),这样就得主动postMessage一个标记到别的窗口,然后别的窗口再把自己的数据postMessage回来,是个异步过程,有些繁琐。


    使用上不比LocalStorage简单多少,不过BroadcastChannel确实可以解决LocalStorage的全局影响和缓存不自动清空问题。有兴趣可以自己实现下。(可以重写storage.js里方法)


    优化窗口数据监听与更新



    • 注册window storage事件,监听storage变化时(当其它窗口位置变化时),判断最新窗口总数,当数量变化时,在当前窗口重新实例化所有球体及粒子对象。

    • 注册window resize事件,更新摄像机比例和渲染器size。

    • 将所有窗口数据保存在js全局变量里,用于在requestAnimationFrame中读取渲染动画,并且只在需要时更新:

      • 其它窗口位置变化时(通过window storage事件);

      • requestAnimationFrame中判断当前窗口位置变化时(比较全局变量与当前window位置),更新全局变量和storage;




    通过以上逻辑优化,可以有效提高渲染速度,减少代码重复执行,减小客户端压力。


    待改进



    1. three.js实现上:学习的还是太浅了,有些动画效果应该会有更好的实现方式,希望有大佬能指点下。

    2. three.js效果:跟国外原大佬比不了,他那是粒子,我这个就是个球。

    3. 拖动窗口位置时的球体移动阻尼效果,这个实现了下,有了个效果,但是卡顿感明显,不顺畅,而且在连线动画下效果更差。

    4. 当改变窗口大小时,球体大小会随着窗口大小变化,想固定大小没找到解决方法,然后计算球体位置也没有考虑窗体大小,所以现在多窗口要求窗口大小必须是一样的。

    5. 球体之间的连线粒子移动效果不佳,特别在窗口移动时,还需优化算法。


    总结


    总结下相比之前的例子 尝试实现了国外大佬用Web做出来跨窗口渲染动画效果,有以下提升:



    1. 引入three.js,画出了球体、粒子旋转动画,多窗口球体,球体间粒子连线动画效果。

    2. BroadcastChannel代替LocalStorage。(技术选型没选上,未实现)

    3. 支持多个窗口(理论上没有限制),并且窗口重叠时不会有连线缺失。


    跨窗口通信、存储窗口坐标、在每个窗口画出所有球体和连线,这个机制流程已经很成熟了,没有太大的优化提升空间了,所以要实现国外大佬视频效果,就只剩three.js了,实在是学不动了,水太深。


    源码已上传至GitHub,代码里有详细注释,希望能有所帮助:github.com/markz-demo/…


    做了两版效果,可以通过代码里注释查看效果,README.md 中有说明。


    Demo:markz-demo.github.io/mark-cross-…


    作者:Mark大熊
    来源:juejin.cn/post/7307057492059471899
    收起阅读 »

    拼多多股价首超阿里,电商行业的史诗级时刻

    拼多多股价首超阿里,这绝对算是电商行业史诗级的颠覆时刻,以下图为证: 还记得2004年,将近20年前的那个年度经济人物颁奖现场吗?马云说出了那句很经典的话:“我就是打着望远镜也找不到对手。” 现在,马云已经不需要那个望远镜了,可以在闲鱼二手交易平台把望远镜卖...
    继续阅读 »

    拼多多股价首超阿里,这绝对算是电商行业史诗级的颠覆时刻,以下图为证:



    还记得2004年,将近20年前的那个年度经济人物颁奖现场吗?马云说出了那句很经典的话:“我就是打着望远镜也找不到对手。”


    现在,马云已经不需要那个望远镜了,可以在闲鱼二手交易平台把望远镜卖掉了。


    其实早在11月28日,拼多多股价暴涨18%的时候,有个阿里员工在内网发帖,说了如下言论:


    此刻难眠,也不敢想,拼多多市值直接来到1855亿美金,相比我们的1943亿,差距仅80亿,着实吓一跳。那个看不起眼的砍一刀,快成老大哥了。本不想发出这个没内容营养的帖子,但思绪下,还是留下这个帖子,用作备忘,也用作自己的勉励。期望和集团兄弟一起努力贡献点滴,超越回来。


    随后,有阿里同事进行跟帖说:


    简单买、简单退,少一点套路、多一点实惠,从未感觉用天猫、京东比用拼多多就显得高端了。让天下没有难做的生意的初衷,本就应该是服务和成就更广大的人民群众么?


    我相信,此时此刻老阿里人的心情是复杂且沉重的。后来,马云也在内网进行了回帖打气,原话如下:


    特别好。请大家多提建设性意见和建议。特别是创新想法。我相信今天的阿里人大家都在看都在听。我更坚信阿里会变,阿里会改。所有伟大的公司都诞生在冬天里。AI电商时代刚刚开始,对谁都是机会,也是挑战。


    要祝贺pdd过去几年的决策,执行和努力。谁都牛x过,但能为了明天后天牛而改革的人,并且愿意付出任何代价和牺牲的组织才令人尊重。回到我们的使命和愿景,阿里人,加油!合伙人马云。


    不得不说,这时的马云还能祝贺拼多多,至少企业家的格局和底蕴还是有的。


    今年完全不一样


    前几年,中国这四大电商巨头(阿里、拼多多、京东和美团),它们的股价走势基本上都是一个模子刻出来的,一涨俱涨,一跌俱跌,无非是涨跌幅上下差一点儿而已。


    但是今年完全不一样,原本是小老弟的拼多多忽然发力,不仅把美团和京东远远甩在身后,甚至去试图掀翻阿里在电商行业的霸主地位了。


    甚至开始有股民说:“阿里是电商界的诺基亚,而拼多多则是后来居上的颠覆者——苹果。”


    还有的股民出来现身说法:“拼多多,淘宝,京东用户质量对比,同一款鞋,同样价格,淘宝评论200+,带图39,京东评论500+,带图10+,多多评论1146,带图883。多多的用户质量和配合度是最高的,多多赢。”


    总之就是一句话,成者王侯败者寇,股价涨了,怎么看怎么顺眼。


    外行看热闹,内行看门道,作为一个在电商行业浸淫多年的老枪,我还是尽量从更多的角度来分析一下。


    (1)消费降级的大环境


    我们可以看下,保证正品和用户体验的京东股价跌得最狠,而主打低端廉价的拼多多股价涨得最猛,不得不说,这离不开经济放缓,消费降级的大环境,以及用户对未来的悲观态度。


    现在的年轻人,不但房子不买了,汽车不买了,奢侈品的包包香水不买了,日餐西餐也都不吃了,甚至前两天,还出了个个这样的新闻,“涨价的羽绒服把市场让给了军大衣”,此中缘由自不必多说。



    在此大环境下,虽然阿里也出了个“淘特”,但整体的动作和布局显然已经慢了很多,拼多多的廉价的烙印已经深深地刻在了用户的心中,并不是那么轻易就被撼动的。


    (2)员工的驱动欲望


    拼多多员工的平均年龄是27岁,而阿里员工则是31岁,四年之差,做事情的驱动欲望是完全不一样的。



    有人会说,别逗了,不就差个四年吗,让你说的跟70后和90后的差距似的。


    但别忘了,从25岁硕士毕业到35岁的职场魔咒,也仅仅不过十年而已,这样算起来,你还觉得四年时间短吗?


    27岁的时候,大多数互联网人还没结婚,父母的年龄也基本在60岁以下,正是心无旁骛地一门心思奔事业的时候,往往就是吃饭睡觉干工作,公司家里两点一线的状态。


    而31岁的时候,互联网人的结婚比例会增加很多,其中有相当一部分人孩子正好一两岁,又有一部分的父母开始往医院跑。这些都是非常牵扯精力的事情,再想达到“生死看淡,不服就干”的工作状态,已经是完全不可能了。


    况且30岁+的年龄,应该充分地体验了职场的冷暖百态,年轻时的鸡血已经褪去,老板的那些洗脑言论也已经是左耳听风。


    取而代之的是,他们开始对并不久远的35岁现象有了哲学性的思考,“到了那个时候我要去干什么”、“到了那个时候我应该到哪里去”等等。


    尤其是看到公司里一波又一波裁员的时候,这种思考甚至变成了穷极所思。而所有的这些,都大大地影响了他们的战斗力。


    (3)创始人的势能和心力


    2023年,黄铮43岁,马云59岁。再牛逼的人,也终究敌不过时间。在黄铮的狼顾鹰视下,马云也会有力不从心的一天。


    就像麦克阿瑟在西点军校的告别演说中,最经典的那一句:“老兵永远不死,只会慢慢凋零(Old soldiers never die,they just fade away)。”



    在唯快不破的互联网行业,能够称霸电商领域20年,其实马云的成就已经旷古烁今了。


    结语


    阿里和拼多多的这场双雄争霸究竟鹿死谁手,将会以怎样的形态进行演变,让我们拭目以待吧。


    作者:库森学长
    来源:juejin.cn/post/7307026637823066122
    收起阅读 »

    突发:鸿蒙之祖华为在 openInula 官网声称可“避免重复运行组件”但网友挖出“组件渲染次数”是写死的

    消息来源 看到群里这个消息,我的想法是:群里怎么炸锅了?华为之前的鸿蒙被指疑似安卓二开不说,现在出个 openInula 好像是什么欧拉又被人挖出幺蛾子?哦有 la 后缀但好像又不像欧拉。 到底怎么回事?我也不敢说话甚至都不敢参与讨论。 求真过程 不过华为好...
    继续阅读 »


    消息来源



    看到群里这个消息,我的想法是:群里怎么炸锅了?华为之前的鸿蒙被指疑似安卓二开不说,现在出个 openInula 好像是什么欧拉又被人挖出幺蛾子?哦有 la 后缀但好像又不像欧拉。


    到底怎么回事?我也不敢说话甚至都不敢参与讨论。


    求真过程


    不过华为好不好是个大企业,并且又是风口浪尖一样的存在,褒贬两级分化。真,搞得我一直到现在我都不知道遥遥领先到底是一个什么词语,时常怀疑我自己是不是出轨了。


    官网现象


    我打开 openInula 的官网 http://www.openinula.net/ ,看样子还是很高大尚的。


    Alt text


    上来就是 相比传统虚拟DOM方式,提升渲染效率30%以上。完全兼容React API,支持React应用无缝切换至openInula。 这种兼容传统但吊打传统的描述,很难让人不把他是电、他是光、他是唯一的希望等联想在一起。


    继续向下:我们看网友说的造假现象到底是不是存在。


    我小心翼翼的向下滑动页面,目不转睛的注视着每一个窗口,全神贯注的查找目标文字组件渲染次数,内心忐忑不安的希望这不是真的。


    但是,现实总是这么残酷,左右两栏的对比里,左边的数据是动态的,右边为什么就要写死?难道页面在跳动开发测试都看不出来吗?为了避免是我的错觉问题,我决定对其 GIF 录制:



    注意看

    注意看

    注意看,组件渲染次数1与源码中定死的1。



    Alt text


    好了,网友没说错。从结果上来看,数据确实就是写死的。这种行为如果用在官网和对比上,确实很难让人接受。


    但是我还注意到一个问题,官网上并没有华为的大 logo,那么这东西到底是不是华为的?别啥脏水都往华为身上泼好吧!


    然后我又再次陷入沉思:与华为什么关系?


    inula 与华为的关系




    9月21日,在华为全联接大会2023开源分论坛上,华为表示国内软件根技术创新之势已起,目前处于战略机遇期,有较大的市场空间。在这一契机下,华为发布了国内首个全面自研密码套件openHiTLS及全场景智慧前端框架openInula。这两款开源基础中间件助力软件根技术自主创新,对构筑业务数字化的核心竞争力有重要意义。



    开发团队合影:


    Alt text



    华为ICT开源产业与生态发展部总经理周俊懿在发布会上表示,“国内软件根技术创新之势已起,正处于发展战略机遇期。在此我们发布更快、更简单、更智能的新一代智慧前端框架openInula,构筑前端生态创新底座,共建国内前端繁荣生态。”



    Alt text



    目前,华为公司内部已有多个产品采用openInula作为其前端框架运用于实际商用项目,支撑上千万行前端业务代码。openInula带来的性能提升解决了产品较多的前端性能瓶颈问题,保证了项目健康、高效地运行。



    功能说明PPT:


    Alt text


    根据以上图文,我暂且觉得可以理解为 openInula 和华为有一定关系,是华为公司或旗下的团队。


    简直不敢相信自己的眼睛!


    Alt text


    肯定是什么地方弄错了,openInula 就是有这么牛笔,人家发布会在那,官网在那,仓库在那,地面在那,还有假?BUG!肯定是BUG!


    于是我开始从试图从代码上来实践看看,他确实 比传统虚拟DOM渲染效率更高、渲染次数更少


    代码实践一


    根据官网的步骤,我开始 npx create-inula <项目名>,这样就完全使用官方的脚手架模板生成项目,避免误差。



    然后根据官方指示运行命令 npm run start



    这怎么回事?我还没进入状态你就结束了?


    Alt text


    咦?不对,好像打开方式不对。


    看到了 node:path 勾起了我仅有的记忆,大概是 node 版本过低。


    这个我熟啊!于是我直接打开 package.json 文件,并加入以下代码:


    "env": {
    "node": "18.18.2"
    }

    然后再次运行命令,这时候项目就在 node v18 的环境下运行成功啦。




    注意看

    注意看

    注意看,上图中在 package.json 中声明了当前项目依赖的环境。



    当我打开控制台 url 时,页面并没有问题(没有使用官网声明的响应式API)。然后当我把官网上的响应式API示例代码放过来的时候:



    啊啊啊!页面一片空白。


    然后发现官网声明中的代码:


    import { useRef, useReactive, useComputed } from 'openinula';

    function ReactiveComponent() {
    const renderCount = ++useRef(0).current;

    const data = useReactive({ count: 0 });
    const countText = useComputed(() => {
    return `计时: ${data.count.get()}`;
    });

    setInterval(() => {
    data.count.set(c => c + 1);
    }, 1000);

    return (
    <div>
    <div>{countText}</div>
    <div>组件渲染次数:{renderCount}</div>
    </div>

    );
    }

    其中 openinula 的 useReactive, useComputed 两个 api 都是 undefined。


    官网 api 文档中全局搜索 useReactive 也无任何讯息。


    Alt text


    好了,我累了,毁灭吧。因为它可能还在 ppt 里或者还没有发布。


    然后我就开始相关如何结束本文,如何思考如何更中立一些,应放入哪些参考材料。


    比如我准备去找代码仓库、去找 npm 官方以及其他镜像仓库来大概看看 openinula 的下载量时:



    这是什么情况?



    • 版本 0.0.1,总历史版本 2个 -- 代表很新很新。难道是内网发布?

    • 周下载量 65 -- 代表 npm 上很少很少人用。 说好的支撑上千万行前端项目呢?难道是内网人用?

    • 代码仓库为空 -- ???

    • readem 为空 -- ???


    有一说一,如果就上面这样子的话,真的是一点都不像官网所描述的光环拉满的样子。


    真的,说实话到这里我的失望值已经累计到 0.2 了。


    但是我真的还想再挣扎一下:难道是因为这是国产框架,所以大都使用国内镜像,所以 npm 上的下载量少?


    然后去大家都在用的淘宝镜像上看了一下:



    我再次被震惊了,请看图片左边是淘宝镜像,为 0.0.4 版,右边是 npm 惊喜,为 0.0.1 版本。大家都知道,通常都是 taobao 镜像比 npm 仓库更新要慢一些。但 openinula 在这里怎么就直接遥遥领先了 3 个版本?



    虽然不理解,但大受震撼。不过,如果这是假的,我希望这是真的(因为我可以试试新的0.0.4版本)。如果这是真的,我希望这是假的,因为这太反常了。


    image-5.png


    所以,那就再试试淘宝镜像上的 0.0.4 是不是遥遥领先,吊打传统虚拟 dom hook api 吧。


    代码实践二


    这下我们直接:


    nrm use taobao
    npm i openinula@latest
    npm run start


    然后仍然是:



    Alt text


    页面一片空白,直接报错:jsx-dev-runtime 404 。



    注意看

    注意看

    注意看:上图浏览器网络控制台。



    真的,我累了,毁灭吧。


    结论


    确实在官网上,传统 Hook API 一栏中组件渲染次数是动态改变的,而在响应式 API声称响应式 API 避免重复运行组件,无需对比虚拟DOM树,可精准更新DOM,大幅提升网页性能。中组件渲染次数是写死的


    但是,这么做的原因到底是什么,有有意为之,还是不小心写错了?就得继续等待后续了。


    我斗胆猜测一下,有几个可能:


    一:还没有准备好


    虽然从发布会上和官网上来看,光环拉满,但从已发布的包、仓库、文档来看,还亟待完善……


    二:失误


    失误就是,真的那个地方是要再加个 t,让其出现动态渲染的效果。不过我尝试修复了这个问题,把 t 加上,也发现只是简单的定时器,所以应该不是少加个 t 的事情。




    注意看

    注意看

    注意看上面的动图中,响应式 API 真的动起来了!



    那种有没有可能是,这个地方是真的想做个动态渲染效果比较,但还没做出来?


    另外,根据官方代码仓库中的源码(是活跃分支,最近提交2小时前)看来:


    readme 中的开发工具都还是随手写的错误网址:


    Alt text


    package.json 声明当前版本为 0.0.1 (那 taobao 上的 0.0.4 是怎么回事)。
    Alt text


    三:有意为之


    在不少关于 Inula 的文章,以及发表大会中,都是以响应式、区别传统虚拟DOM、提高效率号称的。并且代码也是开源的,如果是如实开源的情况下,到底效果是不是如官网所说,大佬们一挖代码便知。虽然目前官网也没有提供详细评测仓库,或三方评测结果。


    可能真是有那么强的,并且也是有意在官网中用渲染次数1来体现效果以达到视觉要求,但没想到有些程序员非要扒代码看说这个地方一定要写个实例动态渲染出来。


    参考



    作者:程序媛李李李李李蕾
    来源:juejin.cn/post/7307125383786119209
    收起阅读 »

    历时一个月,终于找到自己满意的工作了

    由于公司经营遇到了巨大问题,出现严重亏损。 不得不忍痛告诉全体员工团队解散一事 衷心感谢全体小伙伴们在公司付出努力与汗水 目前待定截止今日,后续事宜人事小姐姐会逐步跟进安排。 再次感谢各位伙伴们 希望大家都能有一个好的前程,在未来的时候一定要努力奋斗,前程似锦...
    继续阅读 »

    由于公司经营遇到了巨大问题,出现严重亏损。
    不得不忍痛告诉全体员工团队解散一事
    衷心感谢全体小伙伴们在公司付出努力与汗水
    目前待定截止今日,后续事宜人事小姐姐会逐步跟进安排。
    再次感谢各位伙伴们
    希望大家都能有一个好的前程,在未来的时候一定要努力奋斗,前程似锦



    2023年6月16日我正在开开心心的写着代码,突然来了这么一条消息;我直接原地呆住了,沉思良久我才发现办公室内寂静的可怕,没有了键盘的敲击声;有的只有同事们的呼吸声,大家都依靠在椅子上,像是在思考什么。很快大家也都接受了这个不愿看到的实事。过了一两天就在开始办理离职一类的事情了。


    投递简历


    在离职以后,我们同事之间约好一起打了2个小时的球,吃了一个饭;在我们经常唱歌的地方,唱了几个小时歌,我们玩的很开心、吃的很开心,就像从来没有经历过这件事情一样。


    在过了一段时间以后,我开始修改简历投递简历了。沟通的第一天就给我狠狠的打了一次脸,我在沟通了20/30个公司的时候,恢复我消息的不足5个,已读的大约有一半左右;后面我就开始加大了沟通的力度;大约在沟通了140家以后,收到了第一个面试机会。


    然后第二个面试机会是在人才市场去找的。
    第三个面试机会是在沟通了60多家以后得到的机会也是我满意的一家。


    第一家


    该公司是一家外包公司在成都的分公司,我进去面试的时候没有笔试题,直接是主管来面试的,主要的问题还是围绕业务层面以及上家的一些工作经历,然后就是一些关于vue的一些原理以及简单的算法问题。当时在面试完以后我自己感觉很好;觉得肯定能面试上,结果真的面试上了;下午的时候这家公司就给我打电话了,给出的工资是11k;但是我觉得外包不是很喜欢,而且是单双休,后面就给拒绝了,没想到周一的时候人事又给我打电话了,说主管这边商量了以后决定给你涨一千;请问你愿意来吗?说实话,当时是真的心动了;我考虑了一天以后还是拒绝了,因为我实在是不喜欢外包。


    第二家


    该公司属于半外包性质的,经历了两面,第一面是技术面(超级简单),第二面是主管过来的(主管是后端)面试的,第二面主要就是业务方面的,当时说的是智慧数字一类的产品,我当时确实被该概念吸引了,后面再谈了以后,发现并没有给我offer,我就发了一个消息过去问,然后人事告诉我说是:工资要的太高了,公司给不了,然后我说可以调薪;在多次沟通后给到了9.5k;然后半年有一次涨薪的机会、年底双薪以及试用期交社保一类的。我进去待了几天发现他们并不是什么智慧数字,而且技术用的不是很好的,所以我就放弃了,我个人觉得对我的技术提升没有太多的帮助


    第三家


    该公司是一家完全自研的公司,并且产品已经上线,用户量达到了千万,日活也有10多20万的样子;让我觉得很不错,所以在知晓了之后就对自己说一定要好好面一定要进去,哈哈哈


    在该公司经历了三次面试吧!


    第一次是技术面,问的问题也是一些业务问题;然后会涉及到一些js的基础原理以及vue中的一些实现原理等等问题(主要是大部分都忘了,哈哈哈);


    然后第二次就是人事小姐姐问了一些问题,问了问题以后,人事小姐姐叫来了一个领导然后跟我谈,主要谈的话就是一些收获啊、自豪感啊、研究等等问题。


    在面试完以后都过去了4个小时了,又遇到了下大雨;我骑上我的小电驴穿梭在城市的街头,却充满了一点小小的期待。


    过了几个小时以后,通知我说面试通过了,试用期也是有社保、有年终、也有涨薪制度等等


    补充:在这家公司中可能工资不高,但是这家公司的技术能力比较强而且用的很多技术我都不会,并且我也很想去学习此类的技术,刚好有这个机会,所以我是很开心的。


    总结


    以上是我这段时间面试的一些经历;但是工作确实并不好找。


    主要原因还是面试机会少,很多公司都要求本科以及本专业等等,其实面试的话都还好基本上跟原来差别不大,还有就是对刚出来的这些小伙伴可能不是很友好。所以希望大家如果有工作的话,就先好好上班吧!目前大环境都是这样的,加油哦!


    作者:雾恋
    来源:juejin.cn/post/7263274550074769465
    收起阅读 »

    滴滴崩溃,损失几个亿的k8s 方案

    起因从震惊吃瓜开始 从 2023 年 11 月 27 日晚上 10 点左右截止 2023 年 11 月 28 日中午 12 点期间,DD发生了长达12小时的p0级bug,造成的影响大家通过各种平台或者亲身经历如何我就不多说了,单说对企业造成的损失超千万单和超4...
    继续阅读 »

    起因从震惊吃瓜开始


    从 2023 年 11 月 27 日晚上 10 点左右截止 2023 年 11 月 28 日中午 12 点期间,DD发生了长达12小时的p0级bug,造成的影响大家通过各种平台或者亲身经历如何我就不多说了,单说对企业造成的损失超千万单和超4个亿的交易额。我只想说不愧是大企业,这也太狠了


    简单整理下崩溃原因


    DD自己在微博上说的是底层系统软件发生故障,身为底层开发的我对此还是挺感兴趣的,所以简单吃了下瓜,网传是滴滴未正常升级k8s导致集群崩溃,且由于集群规模过大(相信这么大规模集群一定跑着相当多的业务)导致造成影响肯定很大


    滴滴.png


    DD在微博的致歉中说是底层系统软件故障


    网传.png


    网传是因为升级导致的故障


    恰巧DD技术在公众号上曾经发布过一篇# DD弹性云基于 K8S 的调度实践文章,文章里介绍了他们选择的升级方案,以及如此选择升级方案的原因


    image.png


    DD的升级方案


    dd 不愧是大厂,还有这么老版本的k8s集群,估计是很早就开始引入k8s集群了。

    通用的解决方案


    首先两种方案的对比,DD已经在他们的技术文章中给明了优缺点,身为一个菜鸟我估计是不适合评论别人的方案,所以我只从我实际工作中遇到类似的问题是如何解决的,


    问题一 集群规模过大


    kubernetes 官方推荐了5000个node 上限,虽然并不代表超出上限一定会出问题,但是此次事故明显告诉我们超出上限的集群一旦发生事故有多可怕了


    通用的方案


    实际生产环境当集群规模达到上限我们一般是怎么处理的呢,很简单——联邦集群,让多个集群打通成联邦集群,网络和k8s资源互通,提高了业务容纳的上限,同时将风险分摊给多个集群。增加了些许运维压力,但是明显要比疯狂给单个集群加节点要安全多了


    问题二 如何选择升级方案


    目前我遇到的大规模集群,基本上都是像dd 这样选择晚上的窗口期升级的,这点倒是没什么可说的,但是很少有直接原地升级的,基本上都是有备份升级的,流量也不会直接全部涌入升级后的集群的,要经过逐步验证才会切换到新集群的,原地升级我只能说是艺高人胆大了。


    通用的方案


    从dd 的技术博文上能猜出来,原地升级的方案肯定是经过他们内部验证了,最起码短期内是没出问题,才敢拿到生产集群上实践,但是很抱歉生产集群的扛风险能力还是太小了,所以还是建议老老实实选择替换升级的方案吧


    问题三多控制节点


    最后一点就是网传的控制节点崩溃的问题,我觉得这太离谱了,这种大厂应该知道多master 节点,以及master 不在同一机房的问题吧,不说多数据中心方案,基本的灾备思想还是要有的吧


    胡言乱语


    最近好像很多大厂的产品崩溃,先是阿里后是滴滴,加上最近的裁员潮,网上流出了很多笑话最知名的莫过开猿节流,降本增笑。诚然互联网企业最大成本就是人力成本,当业务成熟后开掉开发人员来降低成本似乎是一个不错的方案。但是当企业剩下的大部分都是ppt高手,真正干活的人黯然退场。如此这般难免会遇到这样那样的技术问题。希望老板领导们能慎重裁员,尊重技术。


    最后希望各位程序员技术越来越稳,默默奉献的同时也能有自己的收获


    作者:萌萌酱
    来源:juejin.cn/post/7306832876381437991
    收起阅读 »

    别再担心数据丢失了!学会使用MySQL事务,保障数据安全!

    在日常开发中我们经常会遇到需要同时处理多个操作的情况,比如在购物时,我们需要同时完成支付和更新库存两个操作。这时,如果其中一个操作失败了,我们就需要进行回滚,以保证数据的一致性。那么,如何在MySQL中实现这样的功能呢?答案就是——事务。下面我们就来介绍一下M...
    继续阅读 »

    在日常开发中我们经常会遇到需要同时处理多个操作的情况,比如在购物时,我们需要同时完成支付和更新库存两个操作。这时,如果其中一个操作失败了,我们就需要进行回滚,以保证数据的一致性。

    那么,如何在MySQL中实现这样的功能呢?答案就是——事务。下面我们就来介绍一下MySQL事务是什么?它是如何使用的?

    一、什么是事务?

    事务定义

    • 事务是一个最小的不可再分的工作单元;通常一个事务对应一个完整的业务(例如银行账户转账业务,该业务是一个最小的工作单元)。

    • 一个完整的业务需要批量的DML(insert、update、delete)语句共同联合完成。

    • 事务只和DML语句有关,或者说DML语句才有事务。这个和业务逻辑有关,业务逻辑不同,DML语句的个数不同。

    事务是什么?

    往通俗的讲就是,事务就是一个整体,里面的内容要么都执行成功,要么都不成功。不可能存在部分执行成功而部分执行不成功的情况。

    就是说如果单元中某条sql语句一旦执行失败或者产生错误,那么整个单元将会回滚(返回最初状态)。所有受到影响的数据将返回到事务开始之前的状态,但是如果单元中的所有sql语句都执行成功的话,那么该事务也就被顺利执行。

    比如有一个订单业务:

    1.订单表当中添加一条记录 2.商品数量数据更新(减少) 3…

    当多个任务同时进行操作的时候,这些任务只能同时成功,或者同时失败。

    二、事务的特性

    事务有四个特性:一致性、持久性、原子性、隔离性。下面分别来解释一下这四个特性都有什么含义。

    原子性

    事务是一个不可分割的工作单位,要么同时成功,要么同时失败。例:当两个人发起转账业务时,如果A转账发起,而B因为一些原因不能成功接受,事务最终将不会提交,则A和B的请求最终不会成功。

    持久性

    一个事务一旦提交成功,它对数据库中数据的改变将是永久性的,接下来的其他操作或故障不应对其有任何影响。

    隔离性

    一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。

    一致性

    事务执行接收之后,数据库完整性不被破坏。

    注意:只有当前三条性质都满足了,才能保证事务的一致性。

    Description

    刷脏: Mysql为了保证存储效率,于是每次将要读写的文件是先存储在缓存池中,对于数据的操作是在缓存池中,而mysql将会定期的刷新到磁盘中。

    三、事务的使用

    事务是如何保证操作的完整性的呢?

    其实事务执行中间出错了,只需要让事务中的这些操作恢复成之前的样子即可, 这里涉及到的一个操作,回滚(rollback)。

    事务处理是一种对必须整批执行的 MySQL 操作的管理机制,在事务过程中,除非整批操作全部正确执行,否则中间的任何一个操作出错,都会回滚 (rollback)到最初的安全状态以确保不会对系统数据造成错误的改动。

    相关语法:

    -- 开启事务
    start transaction;

    -- 若干条执行sql
    -- 提交/回滚事务
    commit/rollback;

    注意:在开启事务之后,执行sql不会立即去执行,只有等到commit操作后才会统一执行(保证原子性)。

    在这里给大家分享一下【云端源想】学习平台,无论你是初学者还是有经验的开发者,这里都有你需要的一切。包含课程视频、在线书籍、在线编程、一对一咨询等等,现在功能全部是免费的,欢迎大家点这里免费体验哦!

    示例:

    首先创建一个账户表并初始化数据,

    Description

    首先看正常情况下的转账操作,

    Description

    如果操作中出现异常情况,比如sql语句中所写的注释格式错误导致sql执行中断。

    Description

    观察结果发现了张三的账户少了2000元,但李四的账户余额并没有增加,在实际操作中这种涉及钱的操作发生这种失误可能会造成很大的损失。

    为了防止这种失误的出现我们就可以使用事务来打包这些操作。

    Description

    观察这里的结果发现在当前的数据库用户查询到的account表中的账户余额发生了变化,但开启了事务之后在commit之前只是临时的预操作并不会真的去修改表中的数据。

    可以退出数据库再打开重新查询表中数据或者切换用户去查询去验证表中数据是否发生改变,这里就不作演示了。

    发现操作结果异常之后,当前用户需要恢复到事务之前的状态,即进行回滚操作。

    Description

    如果开启事务之后发现预操作的结果是预期的效果,此时我们就可以提交事务, 当我们提交完事务之后, 数据就是真的修改了,也就是硬盘中存储的数据真的改变了。

    Description

    要注意事务也不是万能的,不能保证你删表删库之后可以完全恢复,只是在适量的数据和操作下使用事务可以避免一些问题。

    回滚(rollback)操作,实际上是我们把事务中的操作再进行逆操作,前面是插入, 回滚就是删除…

    这些操作是有很大开销的,可以保存,但不能够无限保存,最多是将正再执行的事务保存下来,额外的内容就不好再保存了;数据库要是有几十亿条数据, 占据了几百G硬盘空间,不可能去花费几个T甚至更多的空间用来记录这些数据是如何来的。

    四、事务的并发异常

    但是呢,因为某一刻不可能总只有一个事务在运行,可能出现A在操作text表中的数据,B也同样在操作text表。

    那么就会出现并发问题,对于同时运行的多个事务,当这些事务访问数据库中相同的数据时,如果没有采用必要的隔离机制,就会发生以下各种并发问题。

    1、脏读(读未提交)

    脏读:事务A读取到了事务已经修改但未提交的数据,这种数据就叫脏数据,是不正确的。

    Description

    2、不可重复读(读已提交)

    不可重复读:对于事务A多次读取同一个数据时,由于其他是事务也在访问这个数据,进行修改且提交,对于事务A,读取同一个数据时,有可能导致数据不一致,叫不可重复读。

    Description

    3、幻读(可重复读)

    幻读:因为mysql数据库读取数据时,是将数据放入缓存中,当事务B对数据库进行操作:例如删除所有数据且提交时,事务A同样能访问到数据,这就产生了幻读。

    Description

    解决幻读问题的办法是串行化,也就是彻底的舍弃并发,此时只要李四在读代码,张三就不能进行任何操作。

    4、串行化

    串行化:事务A和事务B同时访问时,在事务A修改了数据,而没有提交数据时,此时事务B想增加或修改数据时,只能等待事务A的提交,事务B才能够执行。

    Description

    所以,为了避免以上出现的各种并发问题,我们就必然要采取一些手段。mysql数据库系统提供了四种事务的隔离级别,用来隔离并发运行各个事务,使得它们相互不受影响,这就是数据库事务的隔离性。

    五、MySQL的四个隔离级别

    MySQL中有 4 种事务隔离级别, 由低到高依次为:读未提交 Read Uncommitted、读已提交 Read Committed、可重复读 Repeatable Read、串行化 Serializable。
    Description

    1. read uncommitted(读未提交数据)

    允许事务读取未被其他事务提交的变更。(脏读、不可重复读和幻读的问题都会出现)。

    2. read committed(读已提交数据)

    只允许事务读取已经被其他事务提交的变更。(可以避免脏读,但不可重复读和幻读的问题仍然可能出现)

    3. repeatable read(可重复读)

    确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新(update)。(可以避免脏读和不可重复读,但幻读仍然存在)

    4. serializable(串行化)

    确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作,所有并发问题都可避免,但性能十分低下(因为你不完成就都不可以弄,效率太低)

    一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性就越差。

    串行化的事务处理方式是最安全的,但不能说用这个就一定好,应该是根据实际需求去选择合适的隔离级别,比如银行等涉及钱的场景,就需要确保准确性,速度慢一点也没什么;

    而比如抖音、B站、快手等上面的点赞数,收藏数就没必要那么精确了,这个场景下速度提高一点体验会更好一些。

    总结

    MySQL事务具有原子性、一致性、隔离性和持久性四大特性,通过合理地管理事务,能够帮助我们保证数据的完整性和一致性。希望通过这篇文章,大家对MySQL事务有了更深入的了解,也希望大家在今后的工作中能够更好地运用事务来处理数据。

    收起阅读 »