注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

序列化和反序列化

序列化隐秘的吭,你踩过了没? 序列化和反序列化 Java序列化的目的主要有2个: 网络传输 对象持久化 当2个相对独立的进程,需要进行跨进程服务调用时,就需要把被传输的Java对象编码为字节数组或者ByteBuffer对象。 接收方只需要把这些字节数...
继续阅读 »

序列化隐秘的吭,你踩过了没?


序列化和反序列化



Java序列化的目的主要有2个:




  • 网络传输

  • 对象持久化


image-20230301144505527


当2个相对独立的进程,需要进行跨进程服务调用时,就需要把被传输的Java对象编码为字节数组或者ByteBuffer对象


接收方只需要把这些字节数组或者Bytebuf对象重新解码成内存对象即可实现通信、调用的作用。


image-20230301145117301


那么在我们使用序列化的时候有哪些需要注意的,避免的坑呢?


成员变量不能以is开头



阿里的《Java开发手册》明文规定了:成员变量禁止使用类似 isXxxx 的命名方式,也不要有isXxx命名的方法



image-20230301150018030


image-20230301145401694


大概的意思就是:不要加is前缀,因为部分框架序列化的时候,会以为对应的字段名是isXxxx后面的Xxxx



  • 比如:isSucceed序列化成Succeed,前端读取isSucceed的时候就会发现没有这个字段,然后出错了。


u=4214115302,3196714167&fm=253&fmt=auto&app=120&f=JPEG


这里面的序列化框架其实就是fastjson,我们可以直接去看他的源码


fastjson源码分析:computeGetters



去找get前缀的方法,然后进行字符串切割找到get后面的



image-20230301161434898



去找is前缀的方法,然后进行字符串切割



image-20230301161413220



  • 这里还进行了驼峰命名的判断:ixXxx,第三个字符是否是大写等判断


所以isSucceed字段会被fastjson框架认为Succeed字段。


image.png


默认值



成员变量的默认值同样会带来坑



同样是阿里的《Java开发手册》里面也是规定了:POJO类、RPC方法必须使用包装类型


image.png


关于包装类型和基本类型的区别,如果还有不清楚的,赶紧去看,这是最基础的面试知识点..


POJO类必须使用包装类型



尽量让错误暴露在编译期,不要拖到运行期



基本类型具有初始值,比如:



  • Int:0

  • float:0.0f

  • boolean:false


一个统计点赞的接口里面的返回值包含一个表示点赞数变化的字段,当发生错误的时候,这个字段没有进行赋初始值,就会出现以下情况:



  • 基本类型:读默认值,0,表达的意思就是没有点赞数变化,程序上并不知道是服务器那边出了错。

  • 包装类型:读到了个null,程序上是知道服务器那边出错了,可以进行对应的显示,比如显示 - ,表示读取不到等操作。


u=3180711090,4079282331&fm=253&fmt=auto&app=138&f=JPEG


总的来说就是:如果字段设置为基础类型并且基础类型的默认值具有业务意义,那么就会出错,并且无法感知错误


RPC方法的返回值和参数必须使用包装类型



RPC调用常常具有超时导致调用失败的情况



如果用包装类型,那么在接收方,就能感知到,这次RPC调用是成功,还是失败。


包装数据类型的null值具有表示额外的信息功能



彦祖来都来了,点个赞👍再走吧,这对我来说真的非常重要



作者:Ashleejy
来源:juejin.cn/post/7205478140914843709
收起阅读 »

扯什么 try-catch 性能问题?

“yes,你看着这鬼代码,竟然在 for 循环里面搞了个 try-catch,不知道try-catch有性能损耗吗?”老陈煞有其事地指着屏幕里的代码: for (int i = 0; i < 5000; i++) { try { ...
继续阅读 »

“yes,你看着这鬼代码,竟然在 for 循环里面搞了个 try-catch,不知道try-catch有性能损耗吗?”老陈煞有其事地指着屏幕里的代码:


 for (int i = 0; i < 5000; i++) {
try {
dosth
} catch (Exception e) {
e.printStackTrace();
}
}

我探过头去看了眼代码,“那老陈你觉得该怎么改?”


“当然是把 try-catch 提到外面啊!”老陈脑子都不转一下,脱口而出。


“你是不是傻?且不说性能,这代码的目的明显是让循环内部单次调用出错不影响循环的运行,你其到外面业务逻辑不就变了吗!”


老陈挠了挠他的地中海,“好像也是啊!”



“回过头来,catch 整个 for 循环和在循环内部 catch,在不出错的情况下,其实性能差不多。” 我喝一口咖啡不经意地提到,准备在老陈前面秀一下。


“啥意思?”老陈有点懵地看着我,“try-catch是有性能损耗的,我可是看过网上资料的!”


果然,老陈上钩了,我二话不说直接打开 idea,一顿操作敲了以下代码:


public class TryCatchTest {

@Benchmark
public void tryfor(Blackhole blackhole) {
try {
for (int i = 0; i < 5000; i++) {
blackhole.consume(i);
}
} catch (Exception e) {
e.printStackTrace();
}
}

@Benchmark
public void fortry(Blackhole blackhole) {
for (int i = 0; i < 5000; i++) {
try {
blackhole.consume(i);
} catch (Exception e) {
e.printStackTrace();
}
}
}

}

“BB 不如 show code,看到没,老陈,我把 try-catch 从 for 循环里面提出来跟在for循环里面做个对比跑一下,你猜猜两个差多少?”


“切,肯定 tryfor 性能好,想都不用想,不是的话我倒立洗头!”老陈信誓旦旦道。


我懒得跟他BB,直接开始了 benchmark,跑的结果如下:



可以看到,两者的性能(数字越大越好)其实差不多:



  • fortry: 86,261(100359-14098) ~ 114,457(100359+14098)

  • tryfor: 95,961(103216-7255) ~ 110,471(103216+7255)


我再调小(一般业务场景 for 循环次数都不会很多)下 for 循环的次数为 1000 ,结果也是差不多:



老陈一看傻了:“说好的性能影响呢?怎么没了?”


我直接一个javap,让老陈看看,其实两个实现在字节码层面没啥区别:



tryfor 的字节码



异常表记录的是 0 - 20 行,如果这些行里面的代码出现问题,直接跳到 23 行处理




fortry 的字节码



差别也就是异常表的范围小点,包的是 9-14 行,其它跟 tryfor 都差不多。



所以从字节码层面来看,没抛错两者的执行效率其实没啥差别。


“那为什么网上流传着try-catch会有性能问题的说法啊?”老陈觉得非常奇怪。


这个说法确实有,在《Effective Java》这本书里就提到了 try-catch 性能问题:



并且还有下面一段话:



正所谓听话不能听一半,以前读书时候最怕的就是一知半解,因为完全理解选择题能选对,完全不懂蒙可能蒙对,一知半解必定选到错误的选项!


《Effective Java》书中说的其实是不要用 try-catch 来代替正常的代码,书中的举例了正常的 for 循环肯定这样实现:



但有个卧龙偏偏不这样实现,要通过 try-catch 拐着弯来实现循环:



这操作我只能说有点逆天,这两个实现的对比就有性能损耗了


我们直接再跑下有try-catch 的代码和没 try-catch的 for 循环区别,代码如下:



结果如下:



+-差不多,直接看前面的分数对比,没有 ry-catch 的性能确实好些,这也和书中说的 try-catch 会影响 JVM 一些特定的优化说法吻合,但是具体没有说影响哪些优化,我猜测可能是指令重排之类的。


好了,我再总结下有关 try-catch 性能问题说法:



  1. try-catch 相比较没 try-catch,确实有一定的性能影响,但是旨在不推荐我们用 try-catch 来代替正常能不用 try-catch 的实现,而不是不让用 try-catch

  2. for循环内用 try-catch 和用 try-catch 包裹整个 for 循环性能差不多,但是其实两者本质上是业务处理方式的不同,跟性能扯不上关系,关键看你的业务流程处理。

  3. 虽然知道try-catch会有性能影响,但是业务上不需要避讳其使用,业务实现优先(只要不是书中举例的那种逆天代码就行),非特殊情况下性能都是其次,有意识地避免大范围的try-catch,只 catch 需要的部分即可(没把握全 catch 也行,代码安全执行第一)。


“好了,老陈你懂了没?”


“行啊yes,BB是一套一套的,走请你喝燕麦拿铁!” 老陈一把拉起我,我直接一个挣脱,“少来,我刚喝过咖啡,你那个倒立洗头,赶紧的!”我立马意识到老陈想岔开话题。


“洗洗洗,我们先喝个咖啡,晚上回去给你洗!”


晚上22点,老陈发来一张图片:



你别说,这头发至少比三毛多。


我是yes,我们下篇见~


作者:yes的练级攻略
来源:juejin.cn/post/7204121228016091197
收起阅读 »

实战:一天开发一款内置游戏直播的国产版Discord应用【附源码】(下)

上篇:https://www.imgeek.net/article/825362923声网RTC接入, 直播与语音实现接入在views/Channel/components文件夹下新增一个组件StreamHandler, 该组件为后续我们处理游戏房间的组件, ...
继续阅读 »

上篇:https://www.imgeek.net/article/825362923

声网RTC接入, 直播与语音实现

接入

views/Channel/components文件夹下新增一个组件StreamHandler, 该组件为后续我们处理游戏房间的组件, 先初步编写声网接入逻辑

// views/Channel/components/StreamHandler/index.js

const options = {
appId:
process.env.REACT_APP_AGORA_APPID || "default id",
channel: process.env.REACT_APP_AGORA_CHANNEL || "test",
token:
process.env.REACT_APP_AGORA_TOKEN ||
"default token",
uid: process.env.REACT_APP_AGORA_UID || "default uid",
};

const StreamHandler = (props) => {
// 组件参数: 用户信息, 当前频道所有消息, 当前频道id, 是否开启本地语音
const { userInfo, messageInfo, channelId, enableLocalVoice = false } = props;

const [rtcClient, setRtcClient] = useState(null);
// 声网client连接完成
const [connectStatus, setConnectStatus] = useState(false);

// RTC相关逻辑
useEffect(() => {
AgoraRTC.setLogLevel(3);
const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
// TODO: use right channel
client
.join(options.appId, options.channel, options.token, userInfo?.username)
.then(() => {
setConnectStatus(true);
console.log("[Stream] join channel success");
})
.catch((e) => {
console.log(e);
});

setRtcClient(client);
return () => {
// 销毁时, 自动退出RTC频道
client.leave();
setRtcClient(null);
};
}, []);

return (
<>
{!connectStatus && <Spin tip="Loading" size="large" />}
</>
);
}


// 我们需要全局状态中的userinfo, 映射一下到当前组件的props中
const mapStateToProps = ({ app }) => {
return {
userInfo: app.userInfo,
};
};
export default memo(connect(mapStateToProps)(StreamHandler));

然后回到Channel中, 在之前的renderStreamChannel函数中添加上StreamHandler组件

// view/Channel/index.js
const [enableVoice, setEnableVoice] = useState(false);
const toggleVoice = () => {
setEnableVoice((enable) => {
return !enable;
});
}

// 保留了输入窗口, 可以在它的菜单栏中添加游戏频道独有的一些逻辑,
// 这里我加入了开关本地语音的逻辑, 拓展Input的细节可以参照完整版代码
const renderStreamChannel = () => {
return (
<>
<div className={s.messageRowWrap}>
<StreamHandler messageInfo={messageInfo} channelId={channelId} enableLocalVoice={enableVoice} />
</div>
<div className={s.iptWrap}>
<Input chatType={CHAT_TYPE.groupChat} fromId={channelId} extraMenuItems={renderStreamMenu()} />
</div>
</>
);
}

const renderStreamMenu = () => {
return [
{
key: "voice",
label: (
<div
className="circleDropItem"
onClick={toggleVoice}
>
<Icon
name="person_wave_slash"
size="24px"
iconClass="circleDropMenuIcon"
/>
<span className="circleDropMenuOp">
{enableVoice ? "关闭语音" : "开启语音"}
</span>
</div>
),
}
];
}

此时我们创建一个video-开题的游戏频道, 应该可以看到命令行中输出了RTC连接成功信息. [Stream] join channel success

音视频推流

接下来我们继续做实质的RTC推流逻辑, 及用户上下播的入口. 但在那之前, 先简单过一下声网RTC中的一些概念.

参考以下步骤实现音视频通话的逻辑:

  1. 调用 createClient 方法创建 AgoraRTCClient 对象。
  2. 调用 join 方法加入一个 RTC 频道,你需要在该方法中传入 App ID 、用户 ID、Token、频道名称。
  3. 先调用 createMicrophoneAudioTrack 通过麦克风采集的音频创建本地音频轨道对象,调用 createCameraVideoTrack 通过摄像头采集的视频创建本地视频轨道对象;然后调用 publish 方法,将这些本地音视频轨道对象当作参数即可将音视频发布到频道中。
  4. 当一个远端用户加入频道并发布音视频轨道时:
  5. 监听 client.on(“user-published”) 事件。当 SDK 触发该事件时,在这个事件回调函数的参数中你可以获取远端用户 AgoraRTCRemoteUser 对象 。
  6. 调用 subscribe 方法订阅远端用户 AgoraRTCRemoteUser 对象,获取远端用户的远端音频轨道 RemoteAudioTrack 和远端视频轨道 RemoteVideoTrack 对象。

在这里插入图片描述

(以上内容来自声网官方文档)

在上面的接入中, 我们已经完成了创建对象并加入频道两步.
在RTC中, 可以传输音频和视频信号, 由于单个RTC客户端要传输不同种类的数据, 每个单独的音视频源被分成不同的track(由于它们都是实时不断产生的, 我们称作流), 随后通过publish方法, 将我们本地的信号源交付给RTC客户端传输.
随后通过user-published事件的回调来在其他用户发布信号源时进行处理, 首先需要subscribe该用户来获取后续数据, 随后根据不同类型的信号流做处理.
离开时需要关闭本地当前的信号源, 并退出RTC客户端.
最后通过user-unpublished事件监听其他用户退出, 移除它们对应的信号流.

逻辑理清楚后代码就很容易看懂了

// views/Channel/components/StreamHandler/index.js
const StreamHandler = (props) => {
...
// 本地视频元素
const localVideoEle = useRef(null);
// 远程视频元素
const canvasEle = useRef(null);
const [rtcClient, setRtcClient] = useState(null);
const [connectStatus, setConnectStatus] = useState(false);
// 当前直播的用户
const [remoteUser, setRemoteUser] = useState(null);
// 远程音视频track
const [remoteVoices, setRemoteVoices] = useState([]);
const [remoteVideo, setRemoteVideo] = useState(null);

// RTC相关逻辑
useEffect(() => {
...
// client.join 后

// 监听新用户加入
client.on("user-published", async (user, mediaType) => {
// auto subscribe when users coming
await client.subscribe(user, mediaType);
console.log("[Stream] subscribe success on user ", user);
if (mediaType === "video") {
// 获取直播流
if (remoteUser && remoteUser.uid !== user.uid) {
// 只能有一个用户推视频流
console.error(
"already in a call, can not subscribe another user ",
user
);
return;
}
// 播放并记录下视频流
const remoteVideoTrack = user.videoTrack;
remoteVideoTrack.play(localVideoEle.current);
setRemoteVideo(remoteVideoTrack);
// can only have one remote video user
setRemoteUser(user);
}
if (mediaType === "audio") {
// 获取音频流
const remoteAudioTrack = user.audioTrack;
// 去重
if (remoteVoices.findIndex((item) => item.uid === user.uid) == -1) {
remoteAudioTrack.play();
// 添加到数组中
setRemoteVoices([
...remoteVoices,
{ audio: remoteAudioTrack, uid: user.uid },
]);
}
}
});

client.on("user-unpublished", (user) => {
// 用户离开, 去除流信息
console.log("[Stream] user-unpublished", user);
removeUserStream(user);
});
setRtcClient(client);
return () => {
client.leave();
setRtcClient(null);
};
}, []);

const removeUserStream = (user) => {
if (remoteUser && remoteUser.uid === user.uid) {
setRemoteUser(null);
setRemoteVideo(null);
}
setRemoteVoices(remoteVoices.filter((voice) => voice.uid !== user.uid));
};
}

接着我们根据之前提到的自定义消息判断当前在播状态, 以最后一条自定义消息为准.

// views/Channel/components/StreamHandler/index.js
const StreamHandler = (props) => {
const { userInfo, messageInfo, channelId, enableLocalVoice = false } = props;

// 第一条 stream 消息, 用于判断直播状态
const firstStreamMessage = useMemo(() => {
return messageInfo?.list?.find(
(item) => item.type === "custom" && item?.ext?.type === "stream"
);
}, [messageInfo]);

// 是否有直播
const hasRemoteStream =
firstStreamMessage?.ext?.status === CMD_START_STREAM &&
firstStreamMessage?.ext?.user !== userInfo?.username;
// 本地直播状态
const [localStreaming, setLocalStreaming] = useState(
firstStreamMessage?.ext?.status === CMD_START_STREAM &&
firstStreamMessage?.ext?.user === userInfo?.username
);

// 本地直播流状态
const toggleLocalGameStream = () => {
if (hasRemoteStream) {
return;
}
setLocalStreaming(!localStreaming);
};
// 根据直播状态选择渲染
return (
<>
{!connectStatus && <Spin tip="Loading" size="large" />}
{hasRemoteStream ? (
<RemoteStreamHandler
remoteUser={firstStreamMessage?.ext?.user}
localVideoRef={localVideoEle}
channelId={channelId}
userInfo={userInfo}
rtcClient={rtcClient}
/>
) : (
<LocalStreamHandler
localStreaming={localStreaming}
canvasRef={canvasEle}
toggleLocalGameStream={toggleLocalGameStream}
rtcClient={rtcClient}
userInfo={userInfo}
channelId={channelId}
/>
)}
</>
);
}

我们根据hasRemoteStream分成两种逻辑RemoteStreamHandlerLocalStreamHandler(可以先用div+文字的空实现占位), 首先我们来看本地游戏的逻辑

// view/Channel/components/StreamHandler/local_stream.js
const LocalStreamHandler = (props) => {

const {
toggleLocalGameStream,
canvasRef,
localStreaming,
rtcClient,
userInfo,
channelId,
} = props;

const [localVideoStream, setLocalVideoStream] = useState(false);
const localPlayerContainerRef = useRef(null);

// 开启本地视频流
useEffect(() => {
if (!localPlayerContainerRef.current) return;
const f = async () => {
// 暂时使用视频代替游戏流
let lgs = await AgoraRTC.createCameraVideoTrack();
lgs.play(localPlayerContainerRef.current);
setLocalGameStream(lgs);
}
f();
}, [localPlayerContainerRef])

const renderLocalStream = () => {
return (
<div style={{ height: "100%" }} ref={localPlayerContainerRef}>
</div>
)
}

// 控制上下播
const renderFloatButtons = () => {
return (
<FloatButton.Group
icon={<DesktopOutlined />}
trigger="click"
style={{ left: "380px" }}
>
<FloatButton
onClick={toggleLocalGameStream}
icon={
localStreaming ? <VideoCameraFilled /> : <VideoCameraOutlined />
}
tooltip={<div>{localStreaming ? "停止直播" : "开始直播"}</div>}
/>
</FloatButton.Group>
);
};

// 渲染: 悬浮窗和本地流
return (
<>
<div style={{ height: "100%" }}>
{renderFloatButtons()}
{renderLocalStream()}
</div>
</>
);
}

现在我们进入直播房间已经可以看到本地摄像头的内容了, 但我们还没有将视频流投放到RTC中, 且上播逻辑也没有处理

// view/Channel/components/StreamHandler/local_stream.js
useEffect(() => {
// 发布直播推流
if (!localStreaming || !rtcClient || !localVideoStream) {
return;
}
console.log("height", canvasRef.current.height);
console.log("publishing local stream", localVideoStream);
// 将流publish到rtc中
rtcClient.publish(localVideoStream).then(() => {
// 频道中发布一条消息, 表示开始直播
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_START_STREAM,
},
channelId
).then(() => {
message.success({
content: "start streaming",
});
});
});
return () => {
// 用户退出的清理工作,
// unpublish流(远程), 停止播放流(本地), 发送直播关闭消息(频道)
if (localVideoStream) {
rtcClient.unpublish(localVideoStream);
localVideoStream.stop();
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
message.info({
content: "stop streaming",
});
}
};
}, [rtcClient, localStreaming, canvasRef, userInfo, channelId, localVideoStream]);

为了测试直播效果, 我们需要登录第二个账号(使用浏览器的匿名/开其他的浏览器, 此时cookie没有共享, 可以多账号登录), 进入相同频道, 开启直播, 此时第一个账号应该会自动刷新状态(如果没有则手动切换一下频道), 进入到RemoteStreamHandler, 说明我们直播的逻辑已经完成.

本地语音的逻辑也是类似的, 这里就不再重复.

接下来是远程流的渲染逻辑, 它的逻辑相对简单, 观看者可以选择开始/停止观看直播流

// view/Channel/components/StreamHandler/remote_stream.js
const RemoteStreamHandler = (props) => {

const {
remoteUser,
localVideoRef,
toggleRemoteVideo,
channelId,
userInfo,
rtcClient,
} = props;

// 这里加一个强制t人的开关, 由于debug
const enableForceStop = true;
const forceStopStream = () => {
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
};
const renderRemoteStream = () => {
return (
<div style={{ height: "100%" }}>
<div
id="remote-player"
style={{
width: "100%",
height: "90%",
border: "1px solid #fff",
}}
ref={localVideoRef}
/>
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: "10px",
}}
>
<span style={{ color: "#0ECD0A" }}>{remoteUser}</span>
&nbsp; is playing{" "}
</div>
</div>
);
};
const renderFloatButtons = () => {
return (
<FloatButton.Group
icon={<DesktopOutlined />}
trigger="click"
style={{ left: "380px" }}
>
<FloatButton
onClick={toggleRemoteVideo}
icon={<VideoCameraAddOutlined />}
tooltip={<div>观看/停止观看直播</div>}
/>
{enableForceStop && (
<FloatButton
onClick={forceStopStream}
icon={<VideoCameraAddOutlined />}
tooltip={<div>强制停止直播</div>}
/>
)}
</FloatButton.Group>
);
};
return (
<>
<div style={{ height: "100%" }}>
{renderFloatButtons()}
{renderRemoteStream()}
</div>
</>
);
}

开关远程流的代码在StreamHander中, 作为参数传给RemoteStream

// views/Channel/components/StreamHandler/index.js
const toggleRemoteVideo = () => {
if (!hasRemoteStream) {
return;
}
console.log("[Stream] set remote video to ", !enableRemoteVideo);
// 当前是关闭状态,需要打开
// 开关远程音频的逻辑也与此类型.
if (enableRemoteVideo) {
remoteVideo?.stop();
} else {
remoteVideo?.play(localVideoEle.current);
}
setEnableRemoteVideo(!enableRemoteVideo);
};

ok, 现在我们已经实现了基于声网RTC, 在环信超级社区集成视频直播的功能.

直播替换为游戏流

接下来我们来将直播流升级一下, 替换成模拟器包, 为了方便测试, 我们直接使用打包好的版本(https://github.com/a71698422/web-0.1.1), pkg包解压后直接放置到项目根目录,

RustNESEmulator 是一个基于Rust语言的NES模拟器, 我们在web平台可以使用它编译好的wasm版本

并将mario.nes文件放到src/assets目录下, 这是初代马里奥游戏的ROM文件(你也可以使用你喜欢的nes游戏, 如果遇到问题, 欢迎到RustNESEmulator中提issue)

加入前端的模拟器适配代码

// views/Channel/components/StreamHandler
// from tetanes.

import * as wasm from "@/pkg";
class State {
constructor() {
this.sample_rate = 44100;
this.buffer_size = 1024;
this.nes = null;
this.animation_id = null;
this.empty_buffers = [];
this.audio_ctx = null;
this.gain_node = null;
this.next_start_time = 0;
this.last_tick = 0;
this.mute = false;
this.setup_audio();
console.log("[NES]: create state");
}

load_rom(rom) {
this.nes = wasm.WebNes.new(rom, "canvas", this.sample_rate);
this.run();
}

toggleMute() {
this.mute = !this.mute;
}

setup_audio() {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) {
console.error("Browser does not support audio");
return;
}
this.audio_ctx = new AudioContext();
this.gain_node = this.audio_ctx.createGain();
this.gain_node.gain.setValueAtTime(1, 0);
}

run() {
const now = performance.now();
this.animation_id = requestAnimationFrame(this.run.bind(this));
if (now - this.last_tick > 16) {
this.nes.do_frame();
this.queue_audio();
this.last_tick = now;
}
}

get_audio_buffer() {
if (!this.audio_ctx) {
throw new Error("AudioContext not created");
}

if (this.empty_buffers.length) {
return this.empty_buffers.pop();
} else {
return this.audio_ctx.createBuffer(1, this.buffer_size, this.sample_rate);
}
}

queue_audio() {
if (!this.audio_ctx || !this.gain_node) {
throw new Error("Audio not set up correctly");
}

this.gain_node.gain.setValueAtTime(1, this.audio_ctx.currentTime);

const audioBuffer = this.get_audio_buffer();
this.nes.audio_callback(this.buffer_size, audioBuffer.getChannelData(0));
if (this.mute) {
return;
}
const source = this.audio_ctx.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.gain_node).connect(this.audio_ctx.destination);
source.onended = () => {
this.empty_buffers.push(audioBuffer);
};
const latency = 0.032;
const audio_ctxTime = this.audio_ctx.currentTime + latency;
const start = Math.max(this.next_start_time, audio_ctxTime);
source.start(start);
this.next_start_time = start + this.buffer_size / this.sample_rate;
}
// ...
}

export default State;

改造local_stream

// view/Channel/components/StreamHandler/local_stream.js

import mario_url from "@/assets/mario.nes";
import * as wasm_emulator from "@/pkg";
import State from "./state";

const LocalStreamHandler = (props) => {
// 模拟器 state
const stateRef = useRef(new State());

// 注意要将原来的代码注释掉
/*
const [localVideoStream, setLocalVideoStream] = useState(false);
const localPlayerContainerRef = useRef(null);

// 开启本地视频流
useEffect(() => {
if (!localPlayerContainerRef.current) return;
const f = async () => {
// 暂时使用视频代替游戏流
let lgs = await AgoraRTC.createCameraVideoTrack();
lgs.play(localPlayerContainerRef.current);
setLocalGameStream(lgs);
}
f();
}, [localPlayerContainerRef])

// 推流的函数也暂时注释
useEffet...
*/


useEffect(() => {
// 本地游戏
if (!canvasRef) {
return;
}
// 开启键盘监听等全局事件
wasm_emulator.wasm_main();
fetch(mario_url, {
headers: { "Content-Type": "application/octet-stream" },
})
.then((response) => response.arrayBuffer())
.then((data) => {
let mario = new Uint8Array(data);
// 加载 rom数据
stateRef.current.load_rom(mario);
});
}, [canvasRef]);

// 更新本地流渲染
const renderLocalStream = () => {
return (
<div style={{ height: "100%" }}>
<canvas
id="canvas"
style={{ width: 600, height: 500 }}
width="600"
height="500"
ref={canvasRef}
/>
</div>
);
};
}

这一步完成后, 我们就可以在本地试玩马里奥游戏了, 键盘绑定为

A      = J
B = K
Select = RShift
Start = Return
Up = W
Down = S
Left = A
Right = D

将推本地视频流改为游戏流

  useEffect(() => {
// 发布直播推流
if (!localStreaming || !rtcClient) {
return;
}
// 只修改了流获取部分
// canvas的captureStream接口支持获取视频流
// 我们用这个视频流构造一个声网的自定义视频流
let stream = canvasRef.current.captureStream(30);
let localVideoStream = AgoraRTC.createCustomVideoTrack({
mediaStreamTrack: stream.getVideoTracks()[0],
});
console.log("height", canvasRef.current.height);
console.log("publishing local stream", localVideoStream);
rtcClient.publish(localVideoStream).then(() => {
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_START_STREAM,
},
channelId
).then(() => {
message.success({
content: "start streaming",
});
});
});
return () => {
if (localVideoStream) {
rtcClient.unpublish(localVideoStream);
localVideoStream.stop();
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
message.info({
content: "stop streaming",
});
}
};
}, [rtcClient, localStreaming, canvasRef, userInfo, channelId]);

最后总结一下房间的流程图
在这里插入图片描述

至此该项目的完整流程就算结束啦,如果有哪些步骤细节不太明确, 可以参照完整版项目
环信超级社区项目
注册环信
模拟器直播项目github源码获取

收起阅读 »

实战:一天开发一款内置游戏直播的国产版Discord应用【附源码】(上)

游戏直播是Discord产品的核心功能之一,本教程教大家如何1天内开发一款内置游戏直播的国产版Discord应用,用户不仅可以通过IM聊天,也可以进行语聊,看游戏直播,甚至自己进行游戏直播,无任何实时音视频底层技术的Web开发者同样适用,效果如下: 开整!St...
继续阅读 »

游戏直播是Discord产品的核心功能之一,本教程教大家如何1天内开发一款内置游戏直播的国产版Discord应用,用户不仅可以通过IM聊天,也可以进行语聊,看游戏直播,甚至自己进行游戏直播,无任何实时音视频底层技术的Web开发者同样适用,效果如下:



开整!

Step1 初始化

本项目基于环信超级社区的实例项目, 所以我们先从Circle-Demo-Web这个仓库开启做初始化

  1. 克隆项目 git clone https://github.com/easemob/Circle-Demo-Web.git
  2. 安装依赖 npm install
  3. 设置appKey src/utils/WebIM.js 中设置appKey,AppKey为环信后台项目对应的key,注册环信,https://console.easemob.com/user/register,登录console后台获取Appkey
  4. 运行项目 npm run start

运行后, 登录完毕效果如下,
在这里插入图片描述

与discord设计逻辑相似, 左边功能区有

  • 个人信息页
  • 好友会话页
  • 当前加入的频道
  • 创建新频道
  • 加入服务器

超级社区的逻辑为

社区(Server)、频道(Channel) 和子区(Thread) 三层结构

社区为一个独立的结构, 不同社区直接相互隔离, 社区包含不同的频道, 代表了不同的话题, 用户在频道中聊天, 而针对一条单独信息产生的回复为子区.

我们本次的项目主要集中在频道部分, 需要加入一个服务器后, 创建一个测试社区, 保证你具有管理员权限.

Step2 协议设置

在这里插入图片描述

我们的目标是尽量利用现有api扩展功能, 有几个问题需要解决

  1. 如何区分普通频道和游戏频道?
  2. 如何区分当前频道是否有玩家直播, 如果有直播如何获取玩家信息, 第二玩家的状态?
  3. 多人聊天的状态?

如何区分普通频道和游戏频道

这里直接简单采用频道前缀做特殊区分, 创建频道前缀带video-的识别为游戏频道, 同时将渲染内容做替换

// views/Channel/index.js


const isVideoChannel = useMemo(() => {
return currentChannelInfo?.name?.startsWith("");
}, [currentChannelInfo]);

const renderTextChannel = () => {
// 原来的渲染逻辑
return (
<>
<MessageList
messageInfo
={messageInfo}
channelId
={channelId}
handleOperation
={handleOperation}
className
={s.messageWrap}
/>
<div className={s.iptWrap}>
<Input chatType={CHAT_TYPE.groupChat} fromId={channelId} />
</div>
</>
);
}

const renderStreamChannel = () => {
// 先填充一个占位符
return (
<>This is a Stream Channel<>
);
}

return (
...
<div className={s.contentWrap}>
{isVideoChannel ? renderStreamChannel() : renderTextChannel()}
</div>
...
);

如果需要区分图标, 可以搜索channelNameWrap, 分别在channelItemChannel/components/Header中添加一个css类, 通过这个类设置图标图片

如何区分当前频道是否有玩家直播, 如果有直播如何获取玩家信息, 第二玩家的状态?

我们可以复用在频道中发送消息的机制, 直播开始, 结束都可以当做一条特殊的消息发送, 只不过这条消息不承载用户的信息, 而是表达用户上下播的行为

当然这个机制存在一定实时性的问题, 不过大致是可行的.

首先我们来看一条普通的消息是如何发送的

  // components/input

//发消息
const sendMessage = useCallback(() => {
if (!text) return;
getTarget().then((target) => {
let msg = createMsg({
chatType,
type: "txt",
to: target,
msg: convertToMessage(ref.current.innerHTML),
isChatThread: props.isThread
});
setText("");
deliverMsg(msg).then(() => {
if (msg.isChatThread) {
setThreadMessage({
message: { ...msg, from: WebIM.conn.user },
fromId: target
});
} else {
insertChatMessage({
chatType,
fromId: target,
messageInfo: {
list: [{ ...msg, from: WebIM.conn.user }]
}
});
scrollBottom();
}
});
});
}, [text, props, getTarget, chatType, setThreadMessage, insertChatMessage]);

去除掉与输入框逻辑耦合的部分, 可以分为两步, createMsg创建消息, deliverMsg发送消息, 这两个功能都是环信SDK功能的封装, 经过查阅文档, 它支持发送自定义消息.
在utils中新建一个stream.js文件来封装直播的逻辑

// utils/stream.js
const sendStreamMessage = (content, channelId) => {
let msg = createMsg({
chatType: CHAT_TYPE.groupChat,
type: "custom",
to: channelId,
ext: {
type: "stream",
...content,
},
});
return deliverMsg(msg)
.then(() => {
console.log("发送成功");
})
.catch(console.error);
};

它接收content表示我们的额外信息, 用户名和上下播状态, channelId区分不同的channel, 对它的调用可以如下

// 定义在 utils/stream.js 中
const CMD_START_STREAM = "start";
const CMD_END_STREAM = "end";

// 上播
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_START_STREAM,
},
channelId
);
// 下播
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);

第二玩家的状态可以类比第一个玩家用额外的自定义消息实现, 这里不做重复.

关于自定义消息, 原本它的作用是邀请用户加入频道, 你可以在components/CustomMsg中找到, 我们要额外识别一下直播消息(可以渲染在消息列表里, 也可以直接屏蔽掉).

// components/CustomMsg/index.js
const isStream = message?.ext?.type === "stream";


// 屏蔽
const renderStream = () => {
return (<>)
}
if (isStream) {
return renderStream();
} else {
...
}

多人聊天的状态?

我们引入声网RTC sdk, 每个进入直播房间的用户都对应维护一个声网客户端,
通过on事件感知远端视频/音频流.

根据文档 进行如下操作,

  1. 注册声网开发者, 并在后台创建一个测试项目
  2. 项目根目录创建.env文件, 存放api token等信息
# channel, uid 暂时设置为固定
REACT_APP_AGORA_APPID = your app id
REACT_APP_AGORA_CHANNEL = test
REACT_APP_AGORA_TOKEN = your token
REACT_APP_AGORA_UID = 123xxx
  1. 添加声网sdk依赖 npm install agora-rtc-sdk-ng

我们在下一章中编写接入逻辑

声网RTC接入, 直播与语音实现

收起阅读 »

迎接35岁,我在美团的22年总结及23年规划

22年10月,喜提二胎,同时儿子还不到两岁,工作中已经苦练写作基本功了,很难有心力在工作外写东西。 但作为一个技术从业者,定期写技术博客和总结规划,是确保高效工作的好习惯。工作已经十年了,越来越认可“选择比努力更重要”。 多数人都不喜欢思考,经常用战术上的勤奋...
继续阅读 »

22年10月,喜提二胎,同时儿子还不到两岁,工作中已经苦练写作基本功了,很难有心力在工作外写东西。


但作为一个技术从业者,定期写技术博客和总结规划,是确保高效工作的好习惯。工作已经十年了,越来越认可“选择比努力更重要”。


多数人都不喜欢思考,经常用战术上的勤奋掩饰自己战略上的懒惰,我也是如此,人生才如此被动。


本文不仅会分享自己在2022年所作出的重大人生选择及2023年的规划,还会在字里行间反思自己从高考后一些重大选择的得失。希望自己能以此洗心革面、痛改前非和开启新生,也希望能给有缘人在人生选择上有一些参考。


自我简介



本节主要是流水账的自我回顾,枯燥而乏味,建议读者有选择地看,或者跳过看后面的总结及规划哈~



从小爱学习


我出生于广西一个瑶族自治县的农村家庭,虽然家庭很不富裕,但童年回忆起来还是有很多快乐。但在青少年时期,父母吵闹较多,邻里关系不和,让看似阳光开朗的我,其实在内心已经种下了忧郁的种子,即使成年后,远离家乡,也时常会做相关的噩梦。‘


从小我就喜欢学习,记得小学一二年级,我经常6点多就到学校,有时周末也去学校,还被人笑话,但成绩只有数学还可以。直到四年级,我才开始获得奖状,印象中四至六年级的班主任,总是让我感觉自己很优秀,于是我的成绩真的变好了。


求学之路


初中,通过二婶大姐的帮助,把我安排到了县城最好中学的农村班,开始了住校的生活。初二开始,经常能考全班第一,全校前30。虽然中考不是很好,但还是考上了市里最好的高中。


从高二开始,成绩不时能考全班第一。高考虽然自我感觉发挥不好,但总分还是能排到全校应届第10,全广西一千六百多名。


由于家庭不富裕,高考后我优先考虑读军校,这样大学几年不会让家里负担过重。但因为各种原因,提前批我居然录取上。


于是我就来到了当时随手填的大学及专业,大一时发现可以选拔在校国防生,毕业后和军校生一样直接派到部队任职。于是大二时,我通过选拔加入了国防生队伍。


大学四年的成绩不是很好,因此没有资格保研。但我心中还是很想读研的,但因为国防生的身份,清华不允许报考,丧失了动力后自然考不上,这也成了后来我主动离开部队的原因之一。


军旅生涯


毕业后,我被分配到了北京军区某部的作战部队,在基层连队体验了一个多月后,就被派到南京某军校进行学习培训。军校培训期满后,我回到了北京的部队,开始了指挥军官的工作。


同多数国防生一样,我对基层部队的生活很不适应,被冻晕过几次,加上单位不同意我报考国防大学的研究生,我在13年底提出了复员转业申请。整体还算顺利,提出申请两个多月后,审批就通过了,然后我就离开了部队。


虽然从军入伍有减轻家庭负担的考虑,但离开部队后我还是会经常梦回连队,内心依然很向往部队的工作,也许还有壮志未酬,更有愧疚之情。即使不在部队了,我相信在国防建设上,还是有很多我可以参与的。


离开部队后,我全身心地投入了考研,因为计算机专业考公务员的岗位比较多,所以我选择了跨专业考清华的计算机专业。但没想到我专业课差得太多,只过了国家线,虽有一些院校联系我调剂,但我还是放弃了。


工作履历


然后,我就准备找C++相关的工作,通过大二时就认识的国防生朋友,内推到了一个做3G&4G 通信卡的外企gemalto,正式开启了程序员的生活。


由于做嵌入式操作系统的开发比较乏味,所以我们主动参与了一个小组的 Android 开发工作,学得差不多后,我们都先后离开了外企,他去了360做安卓开发,我去了搜狐做RN开发。


从搜狐开始,我从RN入门大前端,逐步掌握了react web开发、node 中间层或后端开发、vue web 开发、flutter 或 uni-app 跨端开发、PHP、Java、Go和Ruby后端开发。


从2017下半年开始,我的工作主要是管理团队和推进重点项目,团队最多时接近30人,职能上不仅管理过前端、安卓和iOS,也负责过设计团队。


疫情开始的半年多时间,尝试过在线教育创业,后来因为进展不符合预期和需要结婚生娃,在20年下半年入职了美团的前端基建团队。


回顾2022年


2022年初,因为老婆怀上了二胎,我终于做出了两个人生非常重要的选择:一是离开北京;二是从管理者向技术专家转变。


离京的选择


现在想想,有时选对城市,比选对行业或公司都重要。从2007年来到北京上大学,到2022年挥别北京,转眼间,我已经在北京度过了15个春秋。


离开北京时,我居然没有一丝伤感,仔细一想,15年来自己都没有真正考虑过要留在北京。虽然2007年时,就把户口迁到了北京。因为入伍地是北京,离开部队后,只要找到国企接收,也能把户口留在北京。但当时报考的是清华计算机深圳分院,索性就把户口迁回老家了。


21年12月底,父母都在北京,于是全家在北京过了年,但父亲还是不适应北京的气候,春节后就回老家了。而我亲弟在上海,老家只有我爸和爷爷,出了意外不能及时回到他们身边处理,加上老婆怀上了二胎,未来两个孩子的户口及上学问题,使得我不得不考虑离京的问题。


因为我所在的团队,在上海也有很多研发,有时我也需要去上海出差,加上我弟、堂妹、表姐和表弟等都在上海,所以当时优先考虑去上海,这样也不需要换部门或公司。


但由于年初上海的疫情比较严重且排外严重,同时上海离老家还是太远,看到公司在深圳也有岗位,聊了两个部门都有意向后,我选择了深圳,代价是需要换个部门,绩效和调薪都会受影响。选择深圳还有另外一个考虑,去上海的话,至少要缴满一年社保才能有户口,而深圳只需要缴满一个月即可。


三月底发起活水申请并逐步交接工作,四月中旬我就来到了深圳,五一后就把全家接到深圳安定了下来,6月初就办理好了全家的户口迁移。


当时在知乎上,反复看了深圳、上海、广州和北京的城市对比。对我来说,还是深圳最适合我。相比北京,深圳空气质量好很多,我老婆来了深圳后皮肤变好了很多,还有深圳的政务办事效率很高,日常生活很方便。


回想在北京时,早上我得六点前开车上班,否则不知道被五环堵到什么时候,下班时我得八点半前走,否则不知道什么时候可以离开望京。来到深圳后,我就住在公司附近,出门到工位不到10分钟就可以了,有了更多工作时间和关注生活。


回顾城市的选择,我真的很后悔没有早点来到深圳,若不是因为老婆执意要离开北京,因为22年初申请到了工作居住证,很有可能我下半年就在北京买房继续麻木地北漂了。


工作的选择


2022年,全世界所有的互联网公司都不好过,裁员消息层出不穷。作为一个即将35岁的大龄码农,我也是危机感满满,做好了随时被裁的心理建设。


对我来说,留在原部门原团队是最保险的,也是最好的:之前的工作以管理沟通为主,比较得心应手,领导们也比较认可,绩效和调薪也都有保障,同时负责的基本是中后台技术项目或前端基建项目,如组件库、物料管理、提效工具等,既有技术深度又有较好的工作节奏。


但因为原部门不能在深圳放团队,所以我只能通过活水到在深圳有岗位的团队,结合自己长远的职业规划,我选择了人工智能方向的前端团队。虽然团队只有十人上下,但可以做的事情却很有技术深度,同时团队的学历也比较好,60%都是硕士,还有一个是北大的硕士。


来到新团队后,我先加入近期很火的人工智能创作项目,使用 vite2 + vue3 从0开发了一个以图片处理为主要功能的web应用,经常加班赶项目进度,业余时间自学图形渲染。


然而,来到深圳不到半个月,就有一个前端伙伴要离职了,需要开发维护他从零开发的渲染引擎,工作难度比较大加上来深圳后因为燥热一直休息不好,我萌生了先辞职休息一段时间,再重新找工作的想法。


期间虽然没有好好准备面试,抱着了解招聘行情的心态,也和腾讯、字节、虾皮及一些传统行业的公司聊了聊,能拿到offer的基本是管理岗,传统行业一般是大前端总监,管理五十人以上团队,直接向CTO或CEO汇报。


虽然没有调薪,但领导们多次挽留,并给我争取了一些股票,特别是我的直接leader,相当nice,可以让我选择自己想做的事情,并允许我休个长假调整身心,所以我选择留了下来。


做出这个选择的考虑:一是管理岗可遇不可求,毕业后80%的工作都是管理,技术沉淀不够;二是web端的图形渲染和AI推理技术门槛高且非常有趣,并且近几年的人才缺口大,以后即使做管理,招不到人时,自己也得能搞定。


休假回来后:在业务支持上,我调整到了模型训练和管理相关的中后台项目,便于更好地掌握AI应用开发相关的知识技能;在研发提效上,我基于在前端基建团队的建设成果,修订了我们团队的前端工程规范和推进了项目的工程改造;在技术产出上,我主导了web推理引擎的立项,从零实现了 WebAssembly 计算方案,推进了在人脸验证和智能创作等项目的落地。


在新的团队,因为少了很多管理相关的会议,让我有了更多的机会和时间,结合项目需要,系统学习图形渲染和AI应用开发相关的知识技能。


截屏2023-01-27 18.53.30.png


过去的一年,因为两个孩子比较小需要更多精力放在家庭,同时也因为变换城市和岗位,还有新冠的影响,工作产出应该只达到了我预期的70%。但很幸运,让我遇见了一个很好的团队,有了领导的信任和优秀的伙伴们,相信新的一年,我一定会收获满满。


2023年规划


2023年,我首先要养成三一习惯:每周跑一十公里强体魄、每周看一好书启智慧、每周做一公益得快乐。


其次,在生活上,我要好好研究做饭和带娃。虽然我很想为公司的外卖业务贡献力量,但是对孩子们来说,能选择的很少,而且安全和营养都不好保证。生娃养娃容易,但教好孩子很不容易,童年的创伤,我相信一定可以在养育孩子的过程中治愈。同时,带娃也能帮助我理解一些人工智能相关的理论,现在感觉模型训练和教小孩真的很相似~


最后,在工作上:上半年,我要带领小伙伴们进一步完善Web推理引擎,同时提供一系列面向web应用研发同学的AI入门教程;下半年,我将从提升模型部署易用性出发,规划并建设一个全端的AI推理系统。


截屏2023-01-27 18.55.28.png


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

【架构师李肯】成为了公司股东,而我却失眠了!

提笔的现在是2023年1月13日农历腊月廿二,凌晨四点半,我刚失眠醒来不久,头脑里的思绪万千,无法入睡,索性把它写下来,以便有个回忆。 惊喜与荣誉 说起这次失眠,源于昨天发生的一件事,下午上着班,突然收到微信群邀请,点进去一看,都是公司BOSS、高管、中层l...
继续阅读 »

提笔的现在是2023年1月13日农历腊月廿二,凌晨四点半,我刚失眠醒来不久,头脑里的思绪万千,无法入睡,索性把它写下来,以便有个回忆。


图片


惊喜与荣誉


说起这次失眠,源于昨天发生的一件事,下午上着班,突然收到微信群邀请,点进去一看,都是公司BOSS、高管、中层leader、以及各研发部门的绝对核心,你还别说,真有一种【误入藕花深处】的感觉。我一想,可能有事要发生,且很有可能事儿还不小。


果然,随后群主简单说了下,今晚BOSS要请大家喝酒撸串,请大家务必抽空。也没说啥主题,就发了时间和地点,大家按时到达即可。


直到饭局上,才从BOSS口中得到了确切的消息,也的确算得上是一个好消息:今晚是公司新旧股东的感谢会加欢迎会。其中,我和几个小伙伴就是新加入的那一批股东。


我一听这好消息,有些喜出望外,但又感觉合乎常理,因为我知道公司每年都有配股的传统,也算是公开的小秘密,只要你表现足够优秀,就一定能被高层看见,这也许就是【小庙】的好处吧。


席上,我端起来了闲置了大半年的酒杯,是因为身体原因,也是本身并不嗜酒,所以每次拿开车作为借口搪塞一下也就过去了。但这次有些不一样,毕竟BOSS端着酒在你面前,你说能不能不喝?


有人的地方,就有江湖。烤串局上,一个个推杯换盏,很是热闹,大家笑说旧年的总结,展望新年的目标。


之余,让我印象最深的是公司分管销售的副总裁的敬酒。对,没错,就是公司业绩扛把子的那位。他跟我说,恭喜你加入公司的股东会,恭喜你在2年内就得到了公司的认可,但你知道我花了多久才被这个行业认可吗?答案是【20年】!


哗哦,不真不令人竖起大拇指,绝对对得起他的那些远赴盛名,值得我辈学习。


图片


满载的收获


聊到这,乘着这次机会,再次聊聊2022年的收获吧。


2022年在工作上始终不遗余力,有领导的绝对核心信任,工作方面得心应手,架构设计,疑难攻关,前沿预研,传帮分享,一个没落下,也终于在自己的努力下,再次拿下了年度绩效S考核的优异成绩,不出什么幺蛾子的话,【优秀XX人】的奖杯应该正在制作中!


工作上,至少在我负责的研发领域,可以说收获到了极致,其他平行能力领域还需进一步拓展。


同时,2022年是我致力于打造【架构师李肯】这个技术IP的元年,这一年我在工作之余,沉心创作了100+的技术原创博文,谈不上有多高产,比起那种年入上W篇的量产大户,我这基本拿不上台面。但必须要承认的是,我自己的好些博文是真的用心在这的,有时候为了写清楚一个问题,前前后后耗时都接近一周。很荣幸,这些博文都得到了行业大佬们的认可,比如在RT-Thread技术论坛上,这些博文都被追加了【优秀】,这是一种肯定与荣耀,也是基于这些优异的社区贡献,RT-Thread在2022年度的开发者大会上授予了我【2022年度RT-Thread社区杰出布道者】。这又是更高一级的认可,反正这个牛批可以拿出去吹好一会了!又比如在电子发烧友论坛上,我也收获了很多,技术专栏是我一个亮点,也是因为内容够硬,很荣幸收到了电子发烧友的年度表彰,成为了【年度优秀专栏创作者】。


技术创作是一方面,参加课外大赛是另一方面。期间参加的国民技术应用设计大赛和瑞萨电子应用设计大赛都收获了第一名的好成绩,还有一笔不错的奖金,真是美滋滋。


2022年一共受邀参加了4场在线演讲,写了4份PPT,谈不上多亮点,但只要我用心,有那么一页PPT能够帮助到有心人,这就够了!


还有一个最重要的,今年收获了很多技术道路上志同道合的朋友,我们一起打怪升级,感谢你们的支持,你们就是【架构师李肯】这个技术IP持续发展的最大动力。


图片


追忆与迷茫


这部分话题,本来想再写写的,看着天快亮了,留个记号吧 // TODO


图片


新年新希望


2022年注定是不平凡的一年,有着太多的不堪回首和刻骨铭心,而对于我来说,却也是我收获的一年。最后,新的一年里,祝大家在技术的路上兔飞猛进,在职场的发展上兔步青云;愿大家都能收获心中所想,实现新的远大目标!


图片


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

iOS 3年开发迷茫随心聊

iOS 3年开发迷茫随心聊 从毕业开始做iOS,到现在已经是第4个年头了。第一家公司,做了一年,项目没上线就倒闭了,导致找第二家公司的时候也没有一个项目能拿的出手。第二家公司误入一家游戏公司,每天工作就是将H5小游戏做成一个App,想办法上线,一年过去了,技术...
继续阅读 »

iOS 3年开发迷茫随心聊


从毕业开始做iOS,到现在已经是第4个年头了。第一家公司,做了一年,项目没上线就倒闭了,导致找第二家公司的时候也没有一个项目能拿的出手。第二家公司误入一家游戏公司,每天工作就是将H5小游戏做成一个App,想办法上线,一年过去了,技术其实也没什么长进,但是通过这个过程了解一些苹果上架的知识。也有了几个上线项目经验了。由于想找个正经互联网公司做App,也离职找工作。


找工作头一个月,发现面试面的问题都是与底层相关,一问三不知,在家埋头学了2个月底层相关知识(可以理解背题)。原理是懂一点了,但是没有在实际项目中运用,工资也上不太去。在面了20多家公司后,终于找到现在第三份工作。


由于有第二份工作的经历,在第三家公司上班的时候一直在学习,有意识的去面试的原理去解决一些开发中的问题,例如使用runtime解决一些问题,却发现runtime如果没有很强的理解,还是不要用在项目里,因为可能出现未知的风险。例如使用交换方法,全局做了修改,但是后期项目需求更改,保持全局修改的前提下,对其他情况要做不同处理。也没有太多需求会使用到原理的内容,性能也不需要优化。


小公司对技术不太感冒,能完成需求就行,虽然自己力所能及的去做一些规范,但觉得做的还是不够,也不清楚其他公司到底是如何做的。小公司个人感觉对员工做事的责任心更加看重。需求就是写页面,页面还原的好,做的快一点,bug少一点就行了。不是理想的一个团队有什么方案,规范,让开发更有效率。最大感触是还好没有成为一个油腻的开发~。


在现在的公司,做了几个项目,也没有大的bug。学会了Swift进行开发。也许也算是一种收获吧。但是公司不加薪,今年的目标是想学点东西换一份工作。


学了1个月RxSwift,感觉也快学不下去了,公司是不可能用了,网上也有人说这个架构太重了。语言是个问题,自己英语水平有限,学习速度太慢了。如果有看到的这篇文章的小伙伴,也可以给我点意见。


最近想学一点提高开发效率的技能。和面试相关的内容。如果有大神经历过我这个时期,还麻烦给点建议。建议退iOS坑的就不必留言了。个人虽然菜,但是如果还没有做到小公司天花板的话,目前不考虑退坑。


第一次发文章也不知道说啥,后面会更新一些学习笔记啥的。感谢包容。


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

Android:面向单Activity开发

记得前一两年很多人都跟风面向单Activity开发,顾名思义,就是整个项目只有一个Activity。一个Activity里面装着N多个Fragment,再给Fragment加上转场动画,效果和多Activity跳转无异。其实想想还比较酷,以前还需要关注多个Ac...
继续阅读 »

记得前一两年很多人都跟风面向单Activity开发,顾名思义,就是整个项目只有一个Activity。一个Activity里面装着N多个Fragment,再给Fragment加上转场动画,效果和多Activity跳转无异。其实想想还比较酷,以前还需要关注多个Acitivity之间的生命周期,现在只需关注一个,但还是需要对Fragment的生命周期进行关注。



其实早在六七年前GitHub上就有单Activity的开源库Fragmentation,后来谷歌也出了一个库Navigation。本来以为官方出品必为经典,当时跟着官方文档一步一步踩坑,最后还是放弃了该方案。理由大概如下:



  1. 需要创建XML文件,配置导航关系和跳转参数等

  2. 页面回退是重新创建,需要配合livedata使用

  3. 貌似还会存在卡顿,一些栈内跳转处理等问题


而Github上Fragmentation库已经停止维护,所幸的是再lssuse中发现了一个基于它继续维护的SFragmentation,于是正是开启了面向单Activity的开发。


提供了可滑动返回的版本


dependencies {
//请使用最新版本
implementation 'com.github.weikaiyun.SFragmentation:fragmentation:latest'
//滑动返回,可选
implementation 'com.github.weikaiyun.SFragmentation:fragmentation_swipeback:latest'
}

由于是Fragment之间的跳转,我们需要将原有的Activity跳转动画在框架初始化时设置到该框架中


Fragmentation.builder() 
//设置 栈视图 模式为 (默认)悬浮球模式 SHAKE: 摇一摇唤出 NONE:隐藏, 仅在Debug环境生效
.stackViewMode(Fragmentation.BUBBLE)
.debug(BuildConfig.DEBUG)
.animation(
R.anim.public_translate_right_to_center, //进入动画
R.anim.public_translate_center_to_left, //隐藏动画
R.anim.public_translate_left_to_center, //重新出现时的动画
R.anim.public_translate_center_to_right //退出动画
)
.install()

因为只有一个Activity,所以需要在这个Activity中装载根Fragment


loadRootFragment(int containerId, SupportFragment toFragment)

但现在的APP几乎都是一个页面多个Tab组成的怎么办呢?


loadMultipleRootFragment(int containerId, int showPosition, SupportFragment... toFragments);

有了多个Fragment的显示,我们需要切换Tab实际也很简单


showHideFragment(ISupportFragment showFragment);

是不是使用起来很简单,首页我们解决了,关于跳转和返回、参数的接受和传递呢?


//启动目标fragment
start(SupportFragment fragment)
//带返回的启动方式
startForResult(SupportFragment fragment,int requestCode)
//接收返回参数
override fun onFragmentResult(requestCode: Int, resultCode: Int, data: Bundle?) {
super.onFragmentResult(requestCode, resultCode, data)
}
//返回到上个页面,和activity的back()类似
pop()

对于单Activity而言,我们其实也可以注册一个全局的Fragment监听,这样就能掌控当前的Fragmnet


supportFragmentManager.registerFragmentLifecycleCallbacks(
object : FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentAttached(fm: FragmentManager, f: Fragment, context: Context) {
super.onFragmentAttached(fm, f, context)
}
override fun onFragmentCreated(
fm: FragmentManager,
f: Fragment,
savedInstanceState: Bundle?
) {
super.onFragmentCreated(fm, f, savedInstanceState)
}
override fun onFragmentStarted(fm: FragmentManager, f: Fragment) {
super.onFragmentStarted(fm, f)
}
override fun onFragmentResumed(fm: FragmentManager, f: Fragment) {
super.onFragmentResumed(fm, f)
}
override fun onFragmentDestroyed(fm: FragmentManager, f: Fragment) {
super.onFragmentDestroyed(fm, f)
}
},
true
)

接下来我们看看Pad应用。对于手机应用来说,一般不会存在局部页面跳转的情况,但是Pad上是常规操作。


image.png


如图,点击左边列表的单个item,右边需要显示详情,这时候再点左边的其他item,此时的左边页面是保持不动的,但右边的详情页需要跳转对应的页面。使用过Pad的应该经常见到这种页面,比如Pad的系统设置等页面。这时只使用Activty应该是不能实现的,必须配合Fragment,左右分为两个Fragment。


但问题又出现了,这时候点击back怎么区分局部返回和整个页面返回呢?


//整个页面回退,主要是用于当前装载了Fragment的页面回退
_mActivity.pop()
//局部回退,被装载的Fragment之间回退
pop()

如下图,这样的页面我们又应该怎么装载呢?
image.png


可以分析,页面最外面是一个Activty,要实现单Activity其内部必装载了一个根Fragment。接着这个根Fragment中使用ViewPage和tablayout完成主页框架。当前tab页要满足右边详情页的单独跳转,还得将右边页面作为主页面,以此装载子Fragment才能实现。


image.png


总结


单Activity开发在手机和平板上使用都一样,但在平板上注意的地方更多,尤其是平板一个页面可能是多个页面组成,其局部还能单独跳转的功能,其中涉及到参数回传和栈的回退问题。使用下来,我还是觉得某些页面对硬件要求很高的使用单Activity会出现体验不好的情况,有可能是优化不到位。手机应用我还是使用多Activity方式,平板应用则使用该框架实现单Activity方式。


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

2023也该知道了:kotlin协程取消和异常传播机制

什么是结构化并发? 说好的异常传播为啥失效了? 怎么还有async不抛异常的问题? 1 结构化并发(Structured Concurrency) 1.1 java的"离散性并发" kotlin 的Coroutine是【结构化并发】,与结构化并发对应的方式是...
继续阅读 »
  • 什么是结构化并发?

  • 说好的异常传播为啥失效了?

  • 怎么还有async不抛异常的问题?


1 结构化并发(Structured Concurrency)


1.1 java的"离散性并发"


kotlin 的Coroutine是【结构化并发】,与结构化并发对应的方式是【fire and forget 】姑且称之为【离散性并发】吧,可能不太准确。一个例子解释下离散性并发,java里我们开启一个线程之后,是不具备跟踪管理这个线程的能力的。如下


    public void javaThreadFun() {
       Thread thread = new Thread(new Runnable() {
           @Override
           public void run() {
               //do some work
          }
      });
       thread.setName("child-thread");
       thread.start();
  }

这个例子中,调用javaThreadFun()方法所在的线程,创建并启动child-thread线程之后两个线程没有明确的父子关系,avaThreadFun()方法所在的线程不能天然的感知在自己线程里启动的"子线程",子线程发生异常之后也不会影响到自己。如果父线程要取消中止在自己线程里启动的那些线程也没有现成的方式去供使用。总之,层级关系管理上很离散。


1.2 kotlin 协程的的结构化并发


image.png


kotlin的协程天然的具备父协程管理取消子协程、子协程的异常失败影响父协程或者父协程感知子协程错误和失败的能力。如下示例


      GlobalScope.launch {
           val parentJob = launch {
               val childJob = launch {
                   delay(1_000)//子任务做一些事情
                   throw NullPointerException() //会导致父协程任务和兄弟协程任务都会被取消
              }
               delay(5_000)
          }
      }


  • childJob失败抛出异常,会影响到父job,进而父job会取消掉其所有的子job。

  • 另外,父job也会等待所有的子任务结束后自己才会结束。


与传统的相比



  • 有跟踪:在协程左右域里启动协程会作为该协程的子协程,该协程会跟踪这些协程的状态。而不是像线程那些开启之后就忘记没有跟踪。父协程的结束也是要在所有子协程都完成之后自己才会完成,颇有家长负责制的感觉。

  • 可取消:取消父协程也会,把其子协程一并取消掉。如上图,取消掉parent-job会导致从属于他的所有子协程取消。

  • 能传播:这特性体现在,子协程发生异常,会通知其父协程,父协程会取消掉自己所有的子协程然后再向上传递直到根协程,或者supervisorJob.(这个下文我们会展开分析)


2 取消机制


2.1 父协程的取消会取消子协程


这一章节我们展开聊下Kotlin协程的取消机制,上一节我们提到,父协程/作用域的取消也会取消其子协程我们看个例子。


 GlobalScope.launch {
       val mParentJOb: Job = this.launch {
           val child1Job: Job = this.launch {
               this.launch {
                   delay(300)
              }.invokeOnCompletion { throwable ->
                   println("child1Job 执行完毕,收到了${throwable}")
              }
               val child2Job = this.launch {
                   delay(500)
              }.invokeOnCompletion { throwable ->
                   println("child2Job 执行完毕,收到了${throwable}")
              }
          }
           delay(100)
      }
       mParentJOb.invokeOnCompletion { throwable ->
           println("mParentJOb 执行完毕,收到了${throwable}")
      }
       println("发起取消 mParentJOb")
       mParentJOb.cancel()
  }.join()

运行结果:


发起取消 mParentJOb
child1Job 执行完毕,收到了kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@100b06de
child2Job 执行完毕,收到了kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@100b06de
mParentJOb 执行完毕,收到了kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelled}@100b06de

2.2 兄弟协程取消不影响


private suspend fun brotherCoroutine() {
   coroutineScope {
       launch {
           delay(500)
           println("is running")
      }
       launch {
           delay(100)
           cancel()
      }.invokeOnCompletion {
           println("job2 is canceled")
      }
  }
}

这似乎没有什么可解释的,某个协程的取消并不会影响到其兄弟协程。


2.3 携程的取消是协作式的


协程的取消是协作式的体现在,对取消的通知需要主动的需要主动嗅探感知做出处理。举个例子


private suspend fun coroutineCanceling() {
   coroutineScope {
       val job = launch {
           var i = 0
           while (true) {//1
               println(" is running ${i++}")
          }
      }
       job.invokeOnCompletion {
           println("job is completion ${it}")
      }
       delay(50)
       job.cancel()
  }
}

会发现上面这个段代码并不能被取消,原因就是协程并没有感知到自己已经被取消了。这一点跟java thead interrupt机制类似,需要我们感知取消。感知取消的方式有



  • 可以使用CoroutineScope.isActive()的方法check是否已经被取消做出反应,代码一处可改成while (isActive)

  • 所有的suspend方法内部也会感知cancel。比如delay()方法就是一个suspend方法。


2.4 做好善后取消


协程取消后我们可能会做一些诸如回收资源的动作,但在一个已经处于取消状态的协程里再调用suspend方法就抛出CancellationException异常。此时我们要使用 withContext(NonCancellable) 做取消后的工作


private suspend fun handleCanceling() {
   coroutineScope {
       val job = launch {
           try {
               delay(100)//do Something
          } finally {
               withContext(NonCancellable) {
                   delay(100)
              }
          }
      }
       job.invokeOnCompletion {
           println("job is completion ${it}")
      }
       delay(50)
       job.cancel()
  }
}

另外,还有特别注意的一点是,被取消的协程会向外抛出异常如果使用try-catch捕获但不抛出异常CancellationException,会影响到异常的传播,也就破坏了协程的异常传播机制,具体下一节异常传播机制展开。


2.5 kotlin协程的父子结构


看下面这段代码,思考一个问题,2处字符串会被打印出出来吗,为什么?


private suspend fun parentChildStructTest() {
   coroutineScope {
      val job1 =  launch {
        val job2 =  launch(Job()) {//1
               delay(500)
               println("job2 is finish")//2
          }
           delay(100)
           this.cancel()
      }
  }
}

不会打印,不知道你有没有答对。


不是说好的,取消父协程的时候会取消掉其子协程吗?而且子协程里还调用了delay()方式,也会响应取消。问题的关键点在于,job1和job2的父子结构被破坏了。示例代码里1处传入了一个Job对象,此时job2的父层级已经变成了传入的job对象。我们稍加改造下,这里只是为了理解,不建议这么用,会发现job2可以被取消了。


private suspend fun parentChildStructTest() {
   coroutineScope {
       val job1 = launch {
           val j = Job()
           val job2 = launch(j) {
               delay(500)
               println("job2 is finish")
          }.invokeOnCompletion {
               println("job2 OnCompletion $it")
          }
           delay(100)
           j.cancel() //1
      }
  }
}

新协程的context的组成有两个公式


parentContext = scopeContext + AddionalContext(launch方法传入的context)

childContext = parentConxtext + job(新建)

1_zuX5Ozc2TwofXlmDajxpzg.webp(图来自[Roman Elizarov])



  • 新协程的context是【parent context】和【新建job】的相加操作而来。

  • 【parent context】是由父层级的context和传入的参数context相加操作而来。

  • 子协程的job会和父层级中context的job建立一个父子关系。


当我们使用coroutineScope.launch(Job()){}传入了一个job实例的时候,其实子协程的job和传入的job实例建立了父子结构,破坏了原本的父子结构。


3 异常传播机制


3.1 异常的传播


private suspend fun destroyCoroutineScope() {
   coroutineScope {
       launch {
           launch {
               delay(500)
               throw NullPointerException()
          }.invokeOnCompletion {
               println("job-1 invokeOnCompletion $it")
          }

           launch {
               delay(800)
          }.invokeOnCompletion {
               println("job-2 invokeOnCompletion $it")
          }
      }.invokeOnCompletion {
           println("job-parent completion $it")
      }
  }
}


  • 子job异常后,传播到父协程,父协程会取消到自己所有的子协程,然后再往上传播

  • 如果是一个取消异常(CancellationException)并不会被取消协程,父协程的处理器会忽略他。也就是在子协程上抛出异常之后,父协程接收到不会做处理。


3.2 监督作用域异常传播(Supervision)


基本表现:使用supervisorScope启动的子协程发生异常时,不影响父协程和兄弟协程。


private suspend fun supervisorJobTest() {
   supervisorScope {
       launch {
           delay(100)
           throw NullPointerException()
      }
       launch {
           delay(800)
           println("job 2 is running")
      }
  }
}

如上代码,supervisor范围内第一个job抛出异常后,并不会影响第二个job;把错误异常控制在范围内。



  • SupervisorCoroutine的子协程发生了异常之后不会影响父协程自身,也不会向上传播。

  • 如果 CoroutineContext没有设置CoroutineExceptionHandle,最终异常会传播到ExceptionHandler


但其他的结构化并发特性仍然存在



  • 当父协程取消,他的协程也被取消。

  • 子协程取消不影响父协程。

  • 父协程抛出异常,子协程也会被取消。

  • 父协程要等所有子协程完成后才结束。


简单的讲,监督协程具备单向传播的特性,即子协程的异常和取消不影响父协程,父协程的异常和取消会影响子协程


两种方式:



  • 构建CoroutineScope时传入SupervisorJob()

  • 使用supervisorScope{}产生


注意


监督协程中的每一个子作业应该通过异常处理机制处理自身的异常。如果不处理异常会被吞掉。


3.3 CoroutineExceptionHandler


用于捕获协程执行过程中未捕获的异常,被用来定义一个全局的异常处理器。



  • 不能恢复异常,只是打印、记录、重启应用。

  • 只能在【根作用域】或者【supervisorScope的直接子协程】启动协程是传入才生效。


举个例子


suspend fun coroutineExceptionHandlerTest() {
   supervisorScope {
       val handler = CoroutineExceptionHandler { _, _ -> println("handleException in coroutineExceptionHandler") }
       launch(handler) {
           delay(100)
           throw NullPointerException()
      }
  }
}  

3.4 浅看源码


主从作用域和协作作用域的表现区别上文已经讲到了,通常我们构建一个协程作用域两种方式


val scope = CoroutineScope(Job())
val supervisorJob = CoroutineScope(SupervisorJob())


  • CourotineScope()方法(没错这是个方法),通过传入Job()SupervisorJob生成的对象最终获得主从作用域和协同作用域。


  supervisorScope { scope -> xx }
 coroutineScope { scope ->xx }


  • 通过supervisorScope()或者coroutineScope()构建作用域。



private class SupervisorCoroutine<in T>(
   context: CoroutineContext,
   uCont: Continuation<T>
) : ScopeCoroutine<T>(context, uCont) {
   override fun childCancelled(cause: Throwable): Boolean = false
}

private class SupervisorJobImpl(parent: Job?) : JobImpl(parent) {
   override fun childCancelled(cause: Throwable): Boolean = false
}

两种作用域在代码上的区别是 fun childCancelled(cause: Throwable) 方法的实现不同,监督作用域直接返回fasle表示不处理子协程的错误异常。让其自己处理


//JobSupport
   private fun cancelParent(cause: Throwable): Boolean {
...
       return parent.childCancelled(cause) || isCancellation //1
  }

   private fun finalizeFinishingState(state: Finishing, proposedUpdate: Any?): Any? {
...
      val handled = cancelParent(finalException) || handleJobException(finalException)//2
      if (handled) (finalState as CompletedExceptionally).makeHandled()
    ...
  }

源代码中的核心逻辑,



  • 1处的parent.childCanceled的值的最终来源其实就是我们实现的childCancelled方法的返回值

  • 2处当我们是一个监督作用域起cancelParent的返回值为false,这种情况下代码就会执行后半句handleJobException(),这半句的内部其实最终是执行了我们设置的CoroutineExceptionHandler。

  • 2处cancelParent除了在我们监督作用域的时候返回fasle,在根协程下会返回fasle,这也就是为什么CoroutineExceptionHandler设置在根协程下生效的原因。


代码很多细节不展开有兴趣的自行研究。


4 异常传播需注意问题


4.1 supervisorScope的孙子协程


private suspend fun childChildSupervisorJob() {
   supervisorScope { // SupervisorCouroutine
       launch {  // ScopeCoroutine
          val job1 =  launch {
               delay(100)
               throw NullPointerException()
          }
          val job2 = launch {
               delay(800)
               println("job 2 is running")
          }.invokeOnCompletion {
               println("job2 is completion $it")
          }
      }
  }
}


  • 看上面这个例子job1抛出空指针异常后,job2会不会受影响。

  • 是正常的coroutineScope而非supervisorScope,因此supervisorScope的“孙子协程”不遵循互不影响原则


4.2 注意不要破坏父子结构


private suspend fun textSupervisorJob() {
   supervisorScope {
       launch(SupervisorJob()) {//1
           launch {
               delay(100)
               throw NullPointerException()
          }
           launch {
               delay(800)
               println("job 2 is running")
          }.invokeOnCompletion {
               println("job2 is completion $it")
          }
      }
  }
}


  • job1抛出异常也会影响job2,原因1处虽然传入了SupervisorJob,但是这个实例其实是作为父context的job传入的,真是job1和job2的parentContext还是job类型,而不是SupervisorJob。具体原理可以看2.5小节


5 关于async的误会


通常构建一个协程除了使用CoroutineScope.launch{}还会使用CoroutineScope.async{}。


经常看到这种说法,async方式启动的协程返回一个Deferred对象,当调用deffered的await()方法的时候才会抛出异常


private suspend fun asyncSample() {
   val h = CoroutineScope(CoroutineExceptionHandler { _, _ -> println("发生了异常") })
   val d = h.launch {
       async {
           delay(100)
           throw NullPointerException()
      }
       launch { //job2
           delay(500)
           println("job 2 is finish")
      }
  }.join()
}

这个例子没有调用await(),实际发现也会立马抛出异常,导致jo2都没执行完。跟我们认为的不一样。


实际情况是这样的:当async被用作构建根协程(由协程作用域直接管理的协程)或者监督作用域直接管理协程时,异常不会主动抛出,而是在调用.await()时抛出。其他情况不等待await就会抛出异常。


6 总结


本文梳理了Kotlin的协程的取消和异常传播处理机制。机制的设置总的来说是服务于结构化并发的。本文应该能让我们了解掌握以下问题才算合格



  • kotlin协程结构化并发的特性

  • 协程的context是怎么来的?怎么构成的?父协程的context和协程的parentContext是同一个概念吗?

  • kotlin的协程是怎么传播的?主从作用域监督作用域的区别?怎么实现

  • async方式启动的协程要await()的时候才抛出异常?

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

Java 中为什么要设计 throws 关键词,是故意的还是不小心

我们平时在写代码的时候经常会遇到这样的一种情况 提示说没有处理xxx异常 然后解决办法可以在外面加上try-catch,就像这样 所以我之前经常这样处理 //重新抛出 RuntimeException public class ThrowsDemo { ...
继续阅读 »

我们平时在写代码的时候经常会遇到这样的一种情况


throws.png


提示说没有处理xxx异常


然后解决办法可以在外面加上try-catch,就像这样


trycatch.png


所以我之前经常这样处理


//重新抛出 RuntimeException
public class ThrowsDemo {

public void demo4throws() {
try {
new ThrowsSample().sample4throws();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

//打印日志
@Slf4j
public class ThrowsDemo {

public void demo4throws() {
try {
new ThrowsSample().sample4throws();
} catch (IOException e) {
log.error("sample4throws", e);
}
}
}

//继续往外抛,但是需要每个方法都添加 throws
public class ThrowsDemo {

public void demo4throws() throws IOException {
new ThrowsSample().sample4throws();
}
}

但是我一直不明白


这个方法为什么不直接帮我做


反而要让我很多余的加上一步


我处理和它处理有什么区别吗?


而且变的好不美观


本来缩进就多,现在加个try-catch更是火上浇油


public class ThrowsDemo {

public void demo4throws() {
try {
if (xxx) {
try {
if (yyy) {

} else {

}
} catch (Throwable e) {
}
} else {

}
} catch (IOException e) {

}
}
}

上面的代码,就算里面没有业务,看起来也已经比较乱了,分不清哪个括号和哪个括号是一对


还有就是对Lambda很不友好


lambda.png


没有办法直接用::来优化代码,所以就变成了下面这样


lambdatry.png


本来看起来很简单很舒服的Lambda,现在又变得又臭又长


为什么会强制 try-catch


为什么我们平时写的方法不需要强制try-catch,而很多jdk中的方法却要呢


那是因为那些方法在方法的定义上添加了throws关键字,并且后面跟的异常不是RuntimeException


一旦你显式的添加了这个关键字在方法上,同时后面跟的异常不是RuntimeException,那么使用这个方法的时候就必须要显示的处理


比如使用try-catch或者是给调用这个方法的方法也添加throws以及对应的异常


throws 是用来干什么的


那么为什么要给方法添加throws关键字呢?


给方法添加throws关键字是为了表明这个方法可能会抛出哪些异常


就像一个风险告知


这样你在看到这个方法的定义的时候就一目了然了:这个方法可能会出现什么异常


为什么 RuntimeException 不强制 try-catch


那为什么RuntimeException不强制try-catch呢?


因为很多的RuntimeException都是因为程序的BUG而产生的


比如我们调用Integer.parseInt("A")会抛出NumberFormatException


当我们的代码中出现了这个异常,那么我们就需要修复这个异常


当我们修复了这个异常之后,就不会再抛出这个异常了,所以try-catch就没有必要了


当然像下面这种代码除外


public boolean isInteger(String s) {
try {
Integer.parseInt(s);
return true;
} catch (NumberFormatException e) {
return false;
}
}

这是我们利用这个异常来达成我们的需求,是有意为之的


而另外一些异常是属于没办法用代码解决的异常,比如IOException


我们在进行网络请求的时候就有可能抛出这类异常


因为网络可能会出现不稳定的情况,而我们对这个情况是无法干预的


所以我们需要提前考虑各种突发情况


强制try-catch相当于间接的保证了程序的健壮性


毕竟我们平时写代码,如果IDE没有提示异常处理,我们完全不会认为这个方法会抛出异常


我的代码怎么可能有问题.gif


我的代码怎么可能有问题!


不可能绝对不可能.gif


看来Java之父完全预判到了程序员的脑回路


throws 和 throw 的区别


java中还有一个关键词throw,和throws只有一个s的差别


throw是用来主动抛出一个异常


public class ThrowsDemo {

public void demo4throws() throws RuntimeException {
throw new RuntimeException();
}
}

两者完全是不同的功能,大家不要弄错了


什么场景用 throws


我们可以发现我们平时写代码的时候其实很少使用throws


因为当我们在开发业务的时候,所有的分支都已经确定了


比如网络请求出现异常的时候,我们常用的方式可能是打印日志,或是进行重试,把异常往外抛等等


所以我们没有那么有必要去使用throws这个关键字来说明异常信息


但是当我们没有办法确定异常要怎么处理的时候呢?


比如我在GitHub上维护了一个功能库,本身没有什么业务属性,主要就是对于一些复杂的功能做了相应的封装,提供给自己或别人使用(如果有兴趣可以看看我的库,顺便给Star,嘿嘿


对我来说,当我的方法中出现异常时,我是不清楚调用这个方法的人是想要怎么处理的


可能有的想要重试,有的想要打印日志,那么我干脆就往外抛,让调用方法的人自己去考虑,自己去处理


所以简单来说,如果方法主要是给别人用的最好用throws把异常往外抛,反之就是可加可不加


结束


很多时候你的不理解只是因为你还不够了解


作者:不够优雅
来源:juejin.cn/post/7204594495996100664
收起阅读 »

ProtoBuf 动态拆分Gradle Module

预期 当前安卓的所有proto都生成在一个module中,但是其实业务同学需要的并不是一个大杂烩, 只需要其中他们所关心的proto生成的类则足以。所以我们希望能将这样一个大杂烩的仓库打散,拆解成多个module。 buf.yaml Protobuf是Pr...
继续阅读 »

预期


当前安卓的所有proto都生成在一个module中,但是其实业务同学需要的并不是一个大杂烩, 只需要其中他们所关心的proto生成的类则足以。所以我们希望能将这样一个大杂烩的仓库打散,拆解成多个module


结构图.png


buf.yaml



Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,用于描述一种轻便高效的结构化数据存储格式,并于2008年对外开源。Protobuf可以用于结构化数据串行化,或者说序列化。它的设计非常适用于在网络通讯中的数据载体,很适合做数据存储或 RPC 数据交换格式,它序列化出来的数据量少再加上以 K-V 的方式来存储数据,对消息的版本兼容性非常强,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。开发者可以通过Protobuf附带的工具生成代码并实现将结构化数据序列化的功能。



在我司proto相关的都是由后端大佬们来维护的,然后这个协议仓库会被android/ios/后端/前端 依赖之后生成对应的代码,然后直接使用。


而proto文件中允许导入对于其他proto文件的依赖,所以这就导致了想要把几个proto转化成一个java-library工程,还需要考虑依赖问题。所以由
我们的后端来定义了一个buf.yaml的数据格式。


version: v1
name: buf.xxx.co/xxx/xxxxxx
deps:
- buf.xxxxx.co/google/protobuf
build:
excludes:
- setting
breaking:
use:
- FILE
lint:
use:
- DEFAULT

name代表了这个工程的名字,deps则表示了他依赖的proto的工程名。基于这份yaml内容,我们就可以大概确定一个proto工程编译需要的基础条件。然后我们只需要一个工具或者插件来帮助我们生成对应的工程就够了。


模板工程


现在我们基本已经有了一个单一的proto工程的输入模型了,其中包含工程名依赖的工程还有对应文件夹下的proto文件。然后我们就可以基于这部分输入的模型,生成出第一个模板工程。


plugins {
id 'java-library'
id 'org.jetbrains.kotlin.jvm'
id 'com.google.protobuf'
}


java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}

sourceSets {
def dirs = new ArrayList<String>()
dirs.add("src/main/proto")
main.proto.srcDirs = dirs
}

protobuf {
protoc {
if (System.getProperty("os.arch").compareTo("aarch64") == 0) {
artifact = "com.google.protobuf:protoc:$version_protobuf_protoc:osx-x86_64"
} else {
artifact = "com.google.protobuf:protoc:$version_protobuf_protoc"
}
}
plugins {
grpc {
if (System.getProperty("os.arch").compareTo("aarch64") == 0) {
artifact = 'io.grpc:protoc-gen-grpc-java:1.36.1:osx-x86_64'
} else {
artifact = 'io.grpc:protoc-gen-grpc-java:1.36.1'
}
}
}
generateProtoTasks {
all().each { task ->
task.generateDescriptorSet = true
task.builtins {
// In most cases you don't need the full Java output
// if you use the lite output.
java {

}

}
task.plugins {
grpc { option 'lite' }
}
}
}
}
afterEvaluate {
project.tasks.findByName("compileJava").dependsOn(tasks.findByName("generateProto"))
project.tasks.findByName("compileKotlin").dependsOn(tasks.findByName("generateProto"))
}
dependencies {
implementation "org.glassfish:javax.annotation:10.0-b28"
def grpcJava = '1.36.1'
compileOnly "io.grpc:grpc-protobuf-lite:${grpcJava}"
compileOnly "io.grpc:grpc-stub:${grpcJava}"
compileOnly "io.grpc:grpc-core:${grpcJava}"
File file = new File(projectDir, "depend.txt")
if (!file.exists()) {
return
}
def lines = file.readLines()
if (lines.isEmpty()) {
return
}
lines.forEach {
logger.lifecycle("project:" + name + " implementation: " + it)
implementation(it)
}
}

如果需要将proto编译成java代码,就需要依赖于com.google.protobuf插件,依赖于上面的build.gradle基本就可以将一个proto输入编译成一个jar工程。


另外我们需要把所有的proto文件拷贝到这个壳工程的src/main/proto文件夹下,最后我们会将buf.yaml中的name: buf.xxx.co/xxx/xxxxxx/xxx/xxxxxx转化成工程名,去除掉一些无法识别的字符。


我们生成的模板工程如下:


image.png


其中proto.version会记录proto内的gitsha值还有文件的lastModified时间,如果输入发生变更则会重新进行一次文件拷贝操作,避免重复覆盖的风险。


input.txt则包含了所有proto文件路径,方便我们进行开发调试。


deps 转化


由于proto之间存在依赖,没有依赖则会导致无法将proto转化成java。所以这里我讲buf.yaml中读取出的deps转化成了一个depend.txt.


com.xxxx.api:google-protobuf:7.7.7

depend.txt内会逐行写入当前模块的依赖,我们会对name进行一次转化,变成一个可读的gradle工程名。其中7.7.7的版本只是一个缺省而已,并没有实际的价值。


多线程操作


这里我们出现了一点点的性能问题, 如果可以gradle插件中尽量多使用点多线程,尤其是这种需要io的操作中。


这里我通过ForkJoinPool,这个是ExecutorService的实现类。其中submit方法中会返回一个ForkJoinTask,我们可以将获取gitsha值和lastModified放在这个中。之后把所有的ForkJoinTask放到一个数组中。


fun await() {
forkJoins.forEach {
it.join()
}
}

然后最后暴露一个await方法,来做到所有的获取方法完成之后再继续向下执行。


另外则就是壳module的生成,我们也放在了子线程内执行。我们这次使用了线程池的invokeAll方法。


protoFileWalk.hashMap.forEach { (_, pbBufYaml) ->
callables.add(Callable<Void> {
val root = FileUtils.getRootProjectDir(settings.gradle)
try {
val file = pbBufYaml.copyLib(File(root, "bapi"))
projects[pbBufYaml.projectName()] = file.absolutePath ?: ""
} catch (e: Exception) {
e.printStackTrace()
e.message.log()
}
null
})
}
executor.invokeAll(callables)

这里有个面试经常出现的考点,多线程操作Hashmap,之后我在测试环节随机出现了生成工程和include不匹配的问题。所以最后我更换了ConcurrentHashMap就没有出现这个问题了。


加载壳Module


这部分就和源码编译插件基本是一样的写法。


projects.forEach { (s, file) ->
settings.include(":${s}")
settings.project(":${s}").projectDir = File(file)
}

把工程插入settings 即可。


结尾


最终结果大概就是原先一个Module,现在被拆分成100+的Module,而且基于buf.yaml 文件动态生成,基本符合第一期需求。


这部分方案这样也就大概完成了一半,剩下的一半我们需要逐一把生层业务的依赖进行一次变更,这样就可以做到依赖最小化,然后也可以去除掉一部分无用的代码块。


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

拥有思想,你就是高级、资深、专家、架构师

当然要想成为高级工程师或者架构师,光看书是不行的,书本上来的东西都是工具型编程的体现,何为工具型编程呢? 就是说可以依据书本、网络等渠道就能完成的编程就是工具型编程,那怎么解决呢? 为什么要提升编程思想 这个问题我想大家都有答案,编程思想就是一个程序员的灵魂...
继续阅读 »

当然要想成为高级工程师或者架构师,光看书是不行的,书本上来的东西都是工具型编程的体现,何为工具型编程呢?


就是说可以依据书本、网络等渠道就能完成的编程就是工具型编程,那怎么解决呢?


为什么要提升编程思想



这个问题我想大家都有答案,编程思想就是一个程序员的灵魂,没有灵魂的程序员,只配ctrl + C/V.



专业一点来讲,提升编程思想的重要性在于它能够帮助开发者更好地解决问题、提高效率、减少错误,并提高代码的可读性、可维护性和可扩展性,而这些点位就是成为一个高级Android工程师或者架构师必不可少的技能,也是每一个程序员应该具备的技能。在国外,很多面试更看重的是学习能力和编程思想,其实也是,一个10年的经验丰富的程序员学习一门新的语言或者技术如同探囊取物,对于一个公司、一个团队、一个业务成本来讲,有这样一个人是最经济的。更具体来讲:



  1. 解决问题能力:良好的编程思想能够帮助开发者更好地理解问题,设计出高效、可靠、可扩展的解决方案。

  2. 代码质量提升:优秀的编程思想可以帮助开发者写出易于阅读、易于维护的代码,并使其更加健壮、可靠、可测试。

  3. 工作效率提高:合理的编程思想可以使开发者更加高效地编写代码,并降低代码调试和修复的时间。

  4. 技术实力提升:良好的编程思想可以使开发者更加深入地理解编程语言和计算机科学原理,并在实践中掌握更多的技能和技巧。

  5. 职业发展:具备良好编程思想的开发者在技术水平和职业发展方面具有更好的竞争力和前景。


良好的编程思想可以帮助开发者更好地解决问题、提高效率、提高代码质量和可维护性,并在职业发展中具有更好的前景和竞争力,这也就成了了中、高、架构等分级程序员的区别之分。


如何提升自己的编程思想



  1. 练习算法和数据结构:熟悉算法和数据结构可以帮助你更好地理解和解决问题,优化你的代码并提高你的代码质量。

  2. 阅读源代码:阅读其他优秀项目的源代码可以帮助你学习其他开发人员的编程思想,理解他们是如何解决问题的,进而提高自己的编程思维。

  3. 学习设计模式:设计模式是一种常用的编程思想,它可以帮助你更好地组织你的代码,提高代码的可维护性和可扩展性。

  4. 参与开源项目:参与开源项目可以帮助你学习其他开发人员的编程思想,理解他们是如何解决问题的,同时也可以帮助你获取更多的开发经验和知识。

  5. 持续学习:跟上Android开发的最新技术和趋势可以让你更好地了解开发环境和市场需求,并提升自己的编程思想。

  6. 经常review自己的代码:经常review自己的代码可以帮助你发现自己代码中的问题并及时改进,同时也可以帮助你学习其他开发人员的编程思想。


接下来,我们对这些步骤的项目进行分析和说明,


练习算法和数据结构



熟悉算法和数据结构可以帮助你更好地理解和解决问题,优化你的代码并提高你的代码质量, Android 这种移动平台,性能要求是非常高的,但是他的机型众多,系统不一,所以我们应该从编程角度就减少不必要的麻烦。怎么练习呢?




  1. 选择适合自己的算法练习平台:例如LeetCode、HackerRank、Codeforces等。这些平台都提供了大量的算法题目,可以帮助你提高算法水平。

  2. 学习基础算法:如排序、查找、树、图等算法,这些算法是其他算法的基础,掌握好基础算法对于提高算法能力非常有帮助。

  3. 练习算法的具体类型:例如贪心算法、动态规划、分治算法等,这些算法在实际开发中非常常见,掌握好这些算法可以让你更好地解决实际问题。

  4. 尝试实现算法:通过手写实现一些经典算法,你可以更好地理解算法的思想和实现方式,并加深对算法的理解。

  5. 参与算法竞赛:参与算法竞赛可以帮助你提高算法能力,同时也可以认识到其他优秀的算法工程师。


很多人在开发过程中公司是不会要求有算法参与的,特别是在Android端,也很少有人在开发中精心设计一款算法用于Android业务,Android的数据量级都在可控范围内,但是优秀的程序员不是应公司要求而编程的,我们因该在面对数据的时候,自然而然的想到算法,想到时间复杂度、空间复杂度的问题算法-时间复杂度 这是之前我在公司分享过的一篇文章,大家可以参考一下,基本涵盖了时间复杂度及在不同场景中的计算方式以及在特殊场景下的计算概念。


举几个例子,怎么将算法运用到平时的开发中呢?



  1. 优化算法复杂度:在实际开发中,我们常常需要处理大量数据,如果算法复杂度高,就容易导致程序运行缓慢。因此,优化算法复杂度是非常重要的。比如,在ListView或RecyclerView中使用二分查找算法可以快速查找到指定位置的数据。

  2. 应用动态规划算法:动态规划算法可以用于解决一些经典问题,例如最长公共子序列、背包问题等。在实际开发中,我们也可以应用动态规划算法解决一些问题,例如路径规划、字符串匹配等。

  3. 应用贪心算法:贪心算法是一种可以获得近似最优解的算法,可以用于一些优化问题。在Android开发中,例如布局优化、图片压缩等方面,也可以应用贪心算法来达到优化的效果。

  4. 应用其他常用算法:除了上述算法外,其他常用算法也可以应用于Android开发中,例如图像处理算法、机器学习算法等。对于一些比较复杂的问题,我们也可以引入其他算法来解决。


反正就是学之前要理解对应算法的大致用途,在类似场景中,直接尝试搬套,先在伪代码中演算其结果,结果正趋向时果断使用。


阅读源码



阅读其他优秀项目的源代码可以帮助你学习其他开发人员的编程思想,理解他们是如何解决问题的,进而提高自己的编程思维



前边不是说了,工具型编程可以不用看吗,是的,但是阅读源码不是查看工具,而是提升你的编程思想,借鉴思想是人类进化的最主要体现之一,多个思想的碰撞也能造就更成功的事件。那怎么阅读源码呢?这个每个人都有自己的方法,阅读后要善于变通,运用到自己的项目中,我是这么做的。



  1. 选择合适的开源项目:选择一个合适的开源项目非常重要。你可以选择一些知名度比较高的项目,例如Retrofit、OkHttp、Glide等,这些项目通常质量比较高,也有一定的文档和教程。

  2. 确定目标和问题:在阅读源码前,你需要明确自己的目标和问题。例如,你想了解某个库的实现原理,或者你想解决一个具体的问题。

  3. 仔细阅读源码:在阅读源码时,需要仔细阅读每一个类、方法、变量的注释,了解每一个细节。同时也需要了解项目的整体结构和运行流程。

  4. 了解技术背景和思路:在阅读源码时,你需要了解作者的技术背景和思路,了解为什么选择了某种实现方式,这样可以更好地理解代码。

  5. 实践运用:通过阅读源码,你可以学到许多好的编程思想和技巧,你需要将这些思想和技巧运用到自己的开发中,并且尝试创新,将这些思想和技巧进一步发扬光大。


阅读源码需要持之以恒,需要不断地实践和思考,才能真正学习到他人的编程思想,并将其运用到自己的开发中。


学习设计模式



设计模式本就是编程思想的总结,是先辈们的经验绘制的利刃,它可以帮助你更好地组织你的代码,提高代码的可维护性和可扩展性。




  1. 学习设计模式的基本概念:学习设计模式前,需要了解面向对象编程的基本概念,例如继承、多态、接口等。同时也需要掌握一些基本的设计原则,例如单一职责原则、开闭原则等。

  2. 学习设计模式的分类和应用场景:学习设计模式时,需要了解每个设计模式的分类和应用场景,例如创建型模式、结构型模式、行为型模式等。你需要了解每个模式的特点,以及何时应该选择使用它们。

  3. 练习设计模式的实现:练习实现设计模式是学习设计模式的关键。你可以使用一些例子,例如写一个简单的计算器、写一个文件读写程序等,通过练习来加深对设计模式的理解。

  4. 将设计模式应用到实际项目中:将设计模式应用到实际项目中是学习设计模式的最终目标。你需要从项目需求出发,结合实际场景选择合适的设计模式。举例来说,下面是一些在Android开发中常用的设计模式:

    • 单例模式:用于创建全局唯一的实例对象,例如Application类和数据库操作类等。

    • 适配器模式:用于将一个类的接口转换成客户端期望的另一个接口,例如ListView的Adapter。

    • 工厂模式:用于创建对象,例如Glide中的RequestManager和RequestBuilder等。

    • 观察者模式:用于实现事件机制,例如Android中的广播机制、LiveData等。




学习设计模式需要不断练习和思考,通过不断地练习和实践,才能真正将设计模式灵活运用到自己的项目中。


参与开源或者尝试商业SDK开发



参与开源,很多同学是没有时间的,并且国内缺少很多开发团队项目,都是以公司或者团队模式开源的,个人想在直接参与比较困难,所以有条件的同学可以参与商业SDK的开发,
商业SDK比较特殊的点在于受众不同,但是他所涉及的编程思想较为复杂,会涉及到很多设计模式和架构模式。



比如,Android商业SDK开发涉及到很多方面,下面列举一些常见的考虑点以及经常使用的架构和设计模式:



  1. 安全性:SDK需要考虑用户隐私保护和数据安全,确保不会泄露敏感信息。

  2. 稳定性:SDK需要保证在不同的环境下运行稳定,不会因为异常情况而崩溃。

  3. 可扩展性:SDK需要考虑未来的扩展和升级,能够方便地添加新的功能和支持更多的设备和系统版本。

  4. 性能:SDK需要保证在各种设备和网络条件下,响应速度和性能都有足够的表现。

  5. 兼容性:SDK需要考虑在不同版本的Android系统和各种厂商的设备上,都能够正常运行。


经常用到的架构和设计模式包括:



  1. MVVM架构:MVVM是Model-View-ViewModel的简称,通过将视图、模型和视图模型分离,可以实现更好的代码组织和更容易的测试。

  2. 单例模式:单例模式是一种创建全局唯一对象的模式,在SDK中常用于创建全局的配置、管理器等。

  3. 工厂模式:工厂模式是一种创建对象的模式,SDK中常用于创建和管理复杂的对象。

  4. 观察者模式:观察者模式是一种事件机制,SDK中常用于通知应用程序有新的数据或事件到达。

  5. 适配器模式:适配器模式用于将一个接口转换成另一个接口,SDK中常用于将SDK提供的接口适配成应用程序需要的接口。

  6. 策略模式:策略模式是一种动态地改变对象的行为的模式,SDK中常用于在运行时选择不同的算法实现。


Android商业SDK开发需要综合考虑多个方面,选择适合的架构和设计模式能够提高代码质量、开发效率和维护性。


了解市场、了解业务,不要埋头敲代码



掌握最新的市场需求,比如网络框架的发展历程,从开始的HttpURLConnection的自己封装使用,到okhttp,再到retrofit, 再后来的结构协程、Flow等等,其实核心没有变化就是网络请求,但是,从高内聚到,逐层解耦,变的是其编程的思想。



CodeReview



可以参考该文章,此文章描述了CodeReview 的流程和方法,值得借鉴,CodeReview 是一个天然的提升自己业务需求的过程,
zhuanlan.zhihu.com/p/604492247



经常写开发文档



设计和编写开发文档是一个很重要的工作,它不仅能够提升自己的编程思想,也能够帮助团队提高协作效率和减少沟通成本.



如果要求你在开发一个需求前对着墙或者对着人讲一遍开发思路,你可能讲不出来,也不好意思,且没有留存,开发文档可以满足你,当你写开发文档时,你记录了你的对整个需求的开发,以及你编程的功底,日益累积后,你的思想自然会水涨船高,因为你写开发文档的过程就是在锻炼自己,比如我在前公司开发国际化适配时写的文档(当然只是我的粗鄙想法国际化ICU4J 适配及SDK设计,我会先分析问题,为什么?然后设计,并且会思考可能遇到的问题,也一并解决了。时间长了,设计模式、思想也会得到提升。


当然,也要分场景去设计,按需求去设计,可以采纳以下建议:
设计和编写开发文档是一个很重要的工作,它不仅能够提升自己的编程思想,也能够帮助团队提高协作效率和减少沟通成本。下面是一些关于如何设计一份好的开发文档的建议:



  1. 明确文档的目标和受众:在编写文档之前,需要明确文档的目标和受众,确定文档需要包含的内容和写作风格

  2. 使用清晰的语言和示例:使用简洁、清晰的语言描述问题,使用示例代码和截图帮助读者理解问题。

  3. 分层次组织文档:文档应该按照逻辑和功能分层次组织,每一层都有明确的目标和内容。

  4. 使用图表和图形化工具:图表和图形化工具能够有效地展示复杂的概念和数据,帮助读者更好地理解文档内容。

  5. 定期更新和维护文档:开发文档需要定期更新和维护,以反映最新的代码和功能。


通过设计一份好的开发文档,可以提升自己的编程思想,使得代码更加清晰和易于维护,同时也能够提高团队的协作效率和代码质量。


向上有组织的反馈



经常向领导有组织的汇报开发进度、问题、结果,不仅可以提升编程思想,还能够提高自己的工作效率和沟通能力



首先,向领导汇报开发进度、问题和结果,可以让自己更加清晰地了解项目的进展情况和任务的优先级,帮助自己更好地掌控项目进度和管理时间。


其次,通过向领导汇报问题,可以促使自己更加深入地了解问题的本质和解决方案,同时也能够得到领导的反馈和指导,帮助自己更快地解决问题。


最后,向领导汇报开发结果,可以帮助自己更好地总结经验和教训,促进自己的成长和提高编程思想。同时,也能够让领导更清晰地了解自己的工作成果,提高领导对自己的认可和评价。


向领导有组织地汇报开发进度、问题和结果,不仅能够提升编程思想,还能够提高工作效率和沟通能力,促进自己的成长和发展。


总结



  1. 编程思想的提升



  • 学习数据结构和算法,尤其是常见的算法类型和实际应用

  • 阅读优秀开源代码,理解代码架构和设计思想,学习开发最佳实践

  • 学习设计模式,尤其是常见的设计模式和应用场景



  1. 实际项目开发中的应用



  • 通过代码重构,优化代码质量和可维护性

  • 运用算法解决实际问题,例如性能优化、数据处理、机器学习等

  • 运用设计模式解决实际问题,例如代码复用、扩展性、灵活性等



  1. 沟通与协作能力的提高



  • 与团队成员保持良好的沟通,及时反馈问题和进展情况

  • 向领导有组织地汇报开发进度、问题和结果,以提高工作效率和沟通能力

  • 参加技术社区活动,交流分享经验和知识,提高团队的技术实力和协作能力


以上是这些方面的核心点,当然每个方面都有很多细节需要关注和完善,需要持续学习和实践。


附件



以下是我之前为项目解决老项目的图片框架问题而设计的文档,因名称原因只能图片展示



首先,交代了背景,存在的问题


image.png


针对问题,提出设计思想


image.png


开始设计,从物理结构到架构


image.png


image.png


作者:狼窝山下的青年
来源:juejin.cn/post/7200944831114264637
收起阅读 »

Flutter webview_flutter滑动监听

前言 当需要使用webview时,常用的插件有webview_flutter、flutter_webview_plugin、flutter_inappwebview等。 项目中已经使用的是官方的webview_flutter 。 问题: 总的来说webview...
继续阅读 »

前言


当需要使用webview时,常用的插件有webview_flutterflutter_webview_pluginflutter_inappwebview等。


项目中已经使用的是官方的webview_flutter


问题:


总的来说webview_flutter可以满足大部分的webview相关需求,直到有一天产品小可爱说网页在顶部时,顶部标题栏背景为白色,之后网页滑动到一定距离的时候需要变成透明。我一听,so easy啊,WebViewController里有相关的方法监听:


image.png


我只需要加个Listener手势监听,在move里判断getScrollY()的距离就OK了。


Listener(
child: WebView(initialUrl: "http://www.baidu.com"),
onPointerMove: (PointerMoveEvent event) {
webViewController?.getScrollY().then((value) {
if (value > 200) {
//标题栏背景变白色
} else {
////标题栏背景变透明
}
});
},
);

然而....


image.png


手势滑动是有惯性的,当快速滑动的时候,手指离开屏幕,会因为惯性继续滑动一段距离。而由于我们监听的是手势操作,当手指离开屏幕,手势监听也就从onPointerMove走到了onPointerUp,也就没办法再监听webViewController?.getScrollY(),惯性滚动的距离就没有监听到,那此时所展现的效果自然就不理想了:


快速下滑屏幕的时候,假设手指离开屏幕的时候监听到getScrollY()=500,但由于惯性网页继续滚动,直到滚回网页顶部。但由于手指离开了屏幕没有继续监听getScrollY(),getScrollY()的最后取值就是500,可此时网页已经惯性滚回到顶部了,此时标题栏依然是白色,而不是变回透明。


image.png


解决


遇到问题,咱就解决问题。
反复翻看了webview_flutter的代码,确实没有这个监听,然后联想到即便是在做Android原生开发的时候,webview也是没有提供公开的方法或设置让我们对网页的滚动进行监听的,如果要监听那就需要在原生webview的onScrollChanged增加监听。


修改FlutterWebViewClient,增加方法


void getOffsetY(int offsetY) {
Map<String, Object> args = new HashMap<>();
args.put("offsetY", offsetY);
methodChannel.invokeMethod("getOffsetY", args);
}

image.png


修改FlutterWebView


@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@SuppressWarnings("unchecked")
FlutterWebView(
final Context context,
BinaryMessenger messenger,
int id,
Map<String, Object> params,
View containerView) {

///增加监听
if (webView instanceof CustomWebView){
((CustomWebView)webView).setOnScrollChangedCallback(new CustomWebView.OnScrollChangedCallback() {
@Override
public void onScroll(int dx, int dy) {
flutterWebViewClient.getOffsetY(dy);
Log.d("onScroll",dy+"");
}
});
}

image.png


platform_interface.dart里修改WebViewPlatformCallbacksHandler


增加供外部调用的getOffsetY()


void getOffsetY(int offsetY);

image.png


webview_method_channel.dart修改MethodChannelWebViewPlatform


_onMethodCall里新增getOffsetY类型


case 'getOffsetY':
_platformCallbacksHandler.getOffsetY(call.arguments['offsetY']);
return null;

webview_flutter.dart修改WebView


新增getOffsetY


image.png


_PlatformCallbacksHandler里新增


@override
void getOffsetY(int offsetY){
if (_widget.getOffsetY != null) {
_widget.getOffsetY!(offsetY);
}
}

image.png


至此,就成功增加了webview的滑动监听


image.png


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

我发现了 Android 指纹认证 Api 内存泄漏

我发现了 Android 指纹认证 Api 内存泄漏 目前很多市面上的手机基本都有指纹登陆功能。Google 也提供了调用相关功能 API,安全类的App 也基本都在使用。接下来就一起捋一捋今天的主角 BiometricPrompt 先说问题,使用Biome...
继续阅读 »

我发现了 Android 指纹认证 Api 内存泄漏


目前很多市面上的手机基本都有指纹登陆功能。Google 也提供了调用相关功能 API,安全类的App 也基本都在使用。接下来就一起捋一捋今天的主角 BiometricPrompt


先说问题,使用BiometricPrompt 会造成内存泄漏,目前该问题试了 Android 11 到 13 都发生,而且没有什么好的办法。目前想到的最好的方法是漏的少一点。当然谁有好的办法欢迎留言。


问题再现


先看动画


在这里插入图片描述


动画中操作如下



  1. MainAcitivity 跳转到 SecondActivity

  2. SecondActivity 调用 BiometricPrompt 三次

  3. 从SecondActivity 返回到 MainAcitivity


以下是使用 BiometricPrompt 的代码


public fun showBiometricPromptDialog() {
val keyguardManager = getSystemService(
Context.KEYGUARD_SERVICE
) as KeyguardManager;

if (keyguardManager.isKeyguardSecure) {
var biometricPromptBuild = BiometricPrompt.Builder(this).apply {// this is SecondActivity
setTitle("verify")
setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL or BiometricManager.Authenticators.BIOMETRIC_WEAK)
}
val biometricPromp = biometricPromptBuild.build()
biometricPromp.authenticate(CancellationSignal(), mExecutor, object :
BiometricPrompt.AuthenticationCallback() {

})
}
else {
Log.d("TAG", "showLockScreen: isKeyguardSecure is false");
}
}

以上逻辑 biometricPromp 是局部变量,应该没有问题才对。


内存泄漏如下


在这里插入图片描述
可以看到每启动一次生物认证,创建的 BiometricPrompt 都不会被回收。


规避方案:


修改方案也简单


方案一:



  1. biometricPromp 改为全局变量。

  2. this 改为 applicationContext


方案一存在的问题,SecondActivity 可能频繁创建,所以 biometricPromp 还会存在多个实例。


方案二(目前想到的最优方案):



  1. biometricPromp 改为单例

  2. this 改为 applicationContext


修改后,App memory 中只存在一个 biometricPromp ,且没有 Activity 被泄漏。


想到这里,应该会觉得奇怪,biometricPromp 为什么不会被回收?提供的 API 都看过了,没有发现什么方法可以解决这个问题。直觉告诉我这个可能是系统问题,下来分析下BiometricPrompt 吧。


BiometricPrompt 源码分析


在这里插入图片描述


App 相关信息通过 BiometricPrompt 传递到 System 进程,System 进程再通知 SystemUI 显示认证界面。


App 信息传递到 System 进程,应该会使用 Binder。这个查找 BiometricPrompt 使用哪些 Binder。


private final IBiometricServiceReceiver mBiometricServiceReceiver =
new IBiometricServiceReceiver.Stub() {

......
}

源码中发现 IBiometricServiceReceiver 比较可疑,IBiometricServiceReceiver 是匿名内部类,内部是持有 BiometricPrompt 对象的引用。


接下来看下 System Server 进程信息(注:系统是 UserDebug 的手机,才可以查看,买的手机版本是不支持的)


在这里插入图片描述



😂 App 使用优化后(方案二)App 只存在一个 IBiometricServiceReceiver ,而 system 进程中存在三个 IBiometricServiceReceiver 的 binder proxy。 每次启动 BiometricPrompt 都会创建一个。这个就不解释为什么会出现三个binder proxy,感兴趣可以看下面推荐的文章。GC root 是 AuthSession。

再看下 AuthSession 的实例数


在这里插入图片描述


果然 AuthSession 也存在三个。


在这里插入图片描述


这里有个知识点,binder 也是有生命周期的,三个 Proxy 这篇文章也是解释了的。有兴趣的可以了看下。


Binder | 对象的生命周期


一开始,我以为 AuthSession 没有被置空,看下代码,发现 AOSP 的代码,还是比较严谨的,有置空的操作。


细心的同学发现,上图中 AuthSession 没有被任何对象引用,AuthSession 就是 GC Root,哈哈哈。


问题解密


一个实例什么情况可以作为GC Root,有兴趣的同学,可以自行百度,这里就不卖关子了,直接说问题吧。


Binder.linkToDeath()


public void linkToDeath(@NonNull DeathRecipient recipient, int flags) {
}

需要传递 IBinder.DeathRecipient ,这个 DeathRecipient 会被作为 GC root。当调用 unlinkToDeath(@NonNull DeathRecipient recipient, int flags),GC root 才被收回。


AuthSession 初始化的时候,会调用 IBiometricServiceReceiver .linkToDeath。


public final class AuthSession implements IBinder.DeathRecipient {
AuthSession(@NonNull Context context,
......
@NonNull IBiometricServiceReceiver clientReceiver,
......
) {
Slog.d(TAG, "Creating AuthSession with: " + preAuthInfo);
......
try {
mClientReceiver.asBinder().linkToDeath(this, 0 /* flags */);//this 变成 GC root
} catch (RemoteException e) {
Slog.w(TAG, "Unable to link to death");
}

setSensorsToStateUnknown();
}
}

Jni 中 通过 env->NewGlobalRef(object),告诉虚拟机 AuthSession 是 GC Root。


core/jni/android_util_Binder.cpp

static void android_os_BinderProxy_linkToDeath(JNIEnv* env, jobject obj,
jobject recipient, jint flags)
// throws RemoteException
{
if (recipient == NULL) {
jniThrowNullPointerException(env, NULL);
return;
}

BinderProxyNativeData *nd = getBPNativeData(env, obj);
IBinder* target = nd->mObject.get();

LOGDEATH("linkToDeath: binder=%p recipient=%p\n", target, recipient);

if (!target->localBinder()) {
DeathRecipientList* list = nd->mOrgue.get();
sp<JavaDeathRecipient> jdr = new JavaDeathRecipient(env, recipient, list);//java 中 DeathRecipient 会被封装为 JavaDeathRecipient
status_t err = target->linkToDeath(jdr, NULL, flags);
if (err != NO_ERROR) {
// Failure adding the death recipient, so clear its reference
// now.
jdr->clearReference();
signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/);
}
}
}

JavaDeathRecipient(JNIEnv* env, jobject object, const sp<DeathRecipientList>& list)
: mVM(jnienv_to_javavm(env)), mObject(env->NewGlobalRef(object)),// object -> DeathRecipient 变为 GC root
mObjectWeak(NULL), mList(list)
{
// These objects manage their own lifetimes so are responsible for final bookkeeping.
// The list holds a strong reference to this object.
LOGDEATH("Adding JDR %p to DRL %p", this, list.get());
list->add(this);

gNumDeathRefsCreated.fetch_add(1, std::memory_order_relaxed);
gcIfManyNewRefs(env);
}

unlinkToDeath 最终会在 Jni 中 通过 env->DeleteGlobalRef(mObject),告诉虚拟机 AuthSession 不是GC root。


virtual ~JavaDeathRecipient()
{
//ALOGI("Removing death ref: recipient=%p\n", mObject);
gNumDeathRefsDeleted.fetch_add(1, std::memory_order_relaxed);
JNIEnv* env = javavm_to_jnienv(mVM);
if (mObject != NULL) {
env->DeleteGlobalRef(mObject);// object -> DeathRecipient GC root 被撤销
} else {
env->DeleteWeakGlobalRef(mObjectWeak);
}
}

解决方式


AuthSession 置空的时候调用 IBiometricServiceReceiver 的 unlinkToDeath 方法。


总结


以上梳理的其实就是 Binder 的造成的内存泄漏。


问题严重性来看,也不算什么大问题,因为调用 BiometricPrompt 的进程被杀,system 进程相关实例也就回收释放了。一般 app 也不太可能出现,常驻进程,而且还频繁调用手机认证的。


这里主要介绍了一种容易被忽略的内存泄漏,Binder.linktoDeath()。
Google issuetracker


参考资料


Binder | 对象的生命周期


作者:Jingle_zhang
来源:juejin.cn/post/7202066794299129914
收起阅读 »

简单回顾位运算

前言 位运算其实是一个比较常用的操作,有的人可能说,并没有啊,我好像基本就没怎么用过位运算,但如果你经常看源码,你就会发现源码里面有很多位运算的操作,而且这些操作我个人觉得确实是有很意思,所以位运算很重要,平时用不多,也需要经常去回顾复习。 因为临时写的,有些...
继续阅读 »

前言


位运算其实是一个比较常用的操作,有的人可能说,并没有啊,我好像基本就没怎么用过位运算,但如果你经常看源码,你就会发现源码里面有很多位运算的操作,而且这些操作我个人觉得确实是有很意思,所以位运算很重要,平时用不多,也需要经常去回顾复习。


因为临时写的,有些源码的操作我不太记得是出自哪里了,如果以后碰到了,会在评论区进行补充。比如会有一些取反计算再取反回来的操作,比如左移和右移,我现在不太记得是出自哪里了,反正比较经典的。我的个人可能就记得用位运算来表示状态,因为我会经常用这个。


位运算基础


简单来回顾一下基础的计算,位运算会分为一元运算和二元运算。


一元有左移<<,右移>>,无符号右移动>>>和取反~


左移是是什么?比如 0010 左移动一位就变成 0100 (注意这里是二进制的表示),右移动就是 0100 变成 0010。 当然没这么简单啦,二进制也有表示正负值的标志位,右移之后,左边会补标志位的数。而无符号右移动是左边补0。也就是说正数的右移和无符号右移动的结果相同,而负数就不同了。这样说应该好理解吧?那思考一下为什么没有无符号左移


还是不好懂,没关系,我们讲慢些,假如8转成2进制是00001000

而-8就是先取反码,反码就是取反,所以8的反码是11110111,然后再用反码取补码,补码就是+1,所以这里得到补码1111000。即-8转成二进制是11111000

先看看我们对11111000取反码,得00000111,再取补码得00001000,看到从-8到8也是同样的操作。


然后我们看右移一位和无符号右移一位的效果。

-8的二进制11111000,右移一位是11111100,这是多少啊?我们可以用上面的计算看它的正数是多少,反码00000011,补码00000100,这是4吧,所以11111100是-4。

同理看无符号右移,得01111100,反码10000011,补码10000100,这是多少?2的7次方+4,所以是不同的。


取反就好理解了,取反就是逻辑非,比如0010取反就是1101


二元有与&&、或||、异或^


这些都好理解吧,与运算 1010 && 1001 = 1000 , 或运算 1010 || 1001 = 1010 , 异或 1010 ^ 1001 = 0011 ,这没什么好讲的,很基础。


位运算很简单?


一看,哎哟,真的简单,就这?1分钟学会了。学会?那会用吗?


没关系,我们来看一题算法:


一个整型数组里除了两个数字只出现一次,其他的数字都出现了两次。请写程序找出这两个只出现一次的数字。例如:


输入:


[1,4,1,6]

返回值:


[4,6]

明确说了,这题是用位运算,但是怎么做?


开发中位运算的使用


这个我因为临时写的,android源码里确实是有些比较经典的位运算使用场景,我现在不完全记得,只能先列举几个,其它的如果以后想起来,会在评论区中做补充。


用来表示状态

位运算可以用来表示状态,我之前也写过一篇文章 juejin.cn/post/715547… 差不多也是这个意思。


比如window的flags,看看它的定义


public static final int FLAG_LAYOUT_IN_SCREEN   = 0x00000100;
public static final int FLAG_LAYOUT_NO_LIMITS = 0x00000200;
public static final int FLAG_FULLSCREEN = 0x00000400;
public static final int FLAG_FORCE_NOT_FULLSCREEN = 0x00000800;
......

那他这样做有什么好处,他能一个变量表示多个维度的状态。比如FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_NO_LIMITS|FLAG_FULLSCREEN|FLAG_FORCE_NOT_FULLSCREEN就是 1111 (二进制表示)


如果你要判断这个window是不是同时设置了这4个flag,要怎么判断,就直接if(flags == 15)啊,多简单


但是如果你用多个变量存flag要怎么判断, if(isScreen && isNoLimits && usFullscreen && isForceNot),这样写就很难看,很不方便,我window的flag多着呢,难道你要排火车?


数组扩容

来看看ArrayList的扩容源码


private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}

这里的int newCapacity = oldCapacity + (oldCapacity >> 1);就是阔人操作,这个右移是什么?看不懂也没关系,自己套一个数字进去算就知道了,是除2吧,那ArrayList的扩容就是旧容量的一半。


看着简单是吧?那有没有想过一个问题,他写这个代码,为什么不写oldCapacity/2,而写oldCapacity >> 1


那既然右移一位 >> 1 是除2,那左移一位 << 1 是什么操作呢?是什么计算呢?


总结


我这里确实是很久没有用位运算,所以需要复习一下。这个东西对开发来说很重要,比如你是开发应用层的,你觉得这个是底层用到的,你用不到,并不是这样。就拿那个表示状态的来说,自从我看到源码用这一招之后,只要有合适的场景,我也会这样用。


不管是做数学运算,还是逻辑运算,位运算都能适用,它是很简单就能学会,但是学会用,那就是另外一回事,当然不是说看完我这篇文章就开始瞎用,能在合适的场合去使用,那效果十分的好,用不上也没关系,至少要有个意识,这样不管在看源码还是其它时候,都是能帮到你的。


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

Flutter动态化调研实践

一,前言 1,什么是动态化? 目前移动端应用的版本更新, 最常见的方式是定期发版,无论是安卓还是iOS,都需要提交新的安装包到应用市场进行审核。审核通过后,用户在应用市场进行App的下载更新。 而动态化, 就是不依赖更新程序安装包, 就能动态实时更新页面的技术...
继续阅读 »

一,前言


1,什么是动态化?


目前移动端应用的版本更新, 最常见的方式是定期发版,无论是安卓还是iOS,都需要提交新的安装包到应用市场进行审核。审核通过后,用户在应用市场进行App的下载更新。


而动态化, 就是不依赖更新程序安装包, 就能动态实时更新页面的技术。


2,动态化的必要性


为什么需要动态化技术呢? 因为上述定期发版更新应用的方式存在一些问题,比如:



  1. 审核周期长, 且可能审核不通过。 周期长导致发版本不够灵活, 紧急的业务需求不能及时上线。

  2. 线上出现急需修复的bug时,需要较长修复周期,影响用户体验。

  3. 安装包过大, 动辄几十兆几百兆的应用升级可能会让用户比较抗拒。

  4. 即使上线了,也无法达到全部用户升级, 服务端存在兼容多版本App的问题。


面对这些问题,如果能实现app增量、无感知更新,实现功能同步。无论是对公司还是用户都是非常重要的需求,能实现app动态化更新就显得非常重要,能很好的解决以上问题:



  1. 随时实现功能升级,不存在应用市场长时间审核和拒绝上线问题,达到业务需求快速上线的目的。

  2. 线上bug可以实时修复,提高用户体验。

  3. 可以减小发版功能包体积,只需要替换新增功能即可。

  4. 功能保持一致,类似网页一样,发版后用户同步更新,不存在旧版本兼容问题。


接下来,我们就来分析一下,目前业内主要的Flutter动态化更新方式。


二,动态化方案调研


在Flutter实践层面,简单来说分为三个流派:




  • 方案一:JavaScript是最好的语言(🤣碰瓷PHP)
    主要思路:利用Flutter做渲染,开发使用js,逻辑层通过v8/jscore解释运行。代表框架是腾讯的MXFlutter。这个框架是开源的,大写的👍。




  • 方案三:布局,逻辑,一把梭


    主要思路:与方案一最主要的区别是,逻辑层也是使用dart,增加了一层语法解析和运行时。有一个代表,美团的MTFlutter,然而没有开源动向,无从考察更多。




  • 方案二:DSL + JS


    主要思路:基于模板实现动态化,主要布局层采用Dart转DSL的方式,逻辑层使用JS。代表框架是58同城开源的Fair




MXFlutter


项目简介



MXFlutter 是一套基于 JavaScript 的 Flutter 框架,可以用极其类似 Dart 的开发方式,通过编写 JavaScript 代码,来开发 Flutter 应用,或者使用 mxjsbuilder 编译器,把现有Flutter 工程编译为JS,运行在 mxflutter 之上。



核心思想



核心思路是把 Flutter 的渲染逻辑中的三棵树中的第一棵,放到 JavaScript 中生成。用 JavaScript 完整实现了 Flutter 控件层封装,可以使用 JavaScript,用极其类似 Dart 的开发方式,开发Flutter应用,利用JavaScript版的轻量级Flutter Runtime,生成UI描述,传递给Dart层的UI引擎,UI引擎把UI描述生产真正的 Flutter 控件。



MxFlutter 目前已经停止维护,具体请看MXFlutter
MxFlutter通过JavaScript编写Dart,加载线上js文件,通过引擎在运行时转化并显示,从而达到动态化效果。 官方在0.7.0版本开始接入TypeScript,引入npm生态,优化了js开发的成本,向前端生态进一步靠拢。
很遗憾,在对比各大厂的方案时,发现MxFlutter的性价比极低,学习成本也高,而且又抛弃Dart生态。开发及维护成本都很高。


MTFlutter


项目简介



美团的MTFlutter团队flap项目采用的静态生产DSL方案,通过对Dart语言注解,保证平台一致性。实现了动态下发与解释的逻辑页面一体化的 Flutter 动态化方案。Flap 的出现让 Flutter 动态化和包大小这两个短板得到了一定程度的弥补,促进了 Flutter 生态的发展。



核心思想



通过静态生产 DSL+Runtime 解释运行的思路,实现了动态下发与解释的逻辑页面一体化的 Flutter 动态化方案,建设了一套 Flap 生态体系,涵盖了开发、发布、测试、运维各阶段。



布局和逻辑层都使用Dart, 增加了一层语法解析和运行时。然而没有开源动向,无从考察更多。


Fair


项目简介



Fair是为Flutter设计的动态化框架,通过Fair Compiler工具对原生Dart源文件的自动转化,使项目获得动态更新Widget Tree和State的能力。


创建Fair的目标是支持不发版(Android、iOS、Web)的情况下,通过业务bundle和JS下发实现更新,方式类似于React Native。与Flutter Fair集成后,您可以快速发布新的页面,而无需等待应用的下一个发布日期。Fair提供了标准的Widget,它可以被用作一个新的动态页面或作为现有Flutter页面的一部分,诸如运营位的排版/样式修改,整页面替换,局部替换等都可以使用。



核心思想



Fair是58自研的的动态化框架,通过Fair Compiler工具对原生Dart源文件的自动转化,使项目获得动态更新Widget Tree和State的能力。



pic_d3WbXUd1d1V9d1WcXU37U7U75aXdd17b


三,方案对比


经过上述三个方案的调研,我们来大概对比一下上述框架


方案开源方核心思想优点缺点
MXFlutter腾讯用js编写Dart,动态拉取js脚本目前相对最完整的Flutter使用JS开发方案采用js方式编写Dart,维护困难
MTFlutter美团布局,逻辑都使用Dart,增加语法解析和运行时支持布局动态化和逻辑动态化未开源
Fair58通过bundle和js实现热更新支持布局动态化和逻辑动态化开源社区活跃, 开发工具丰富部分语法不支持

可以看到, MXFlutter需要使用js写Dart, 官方已经停止更新,而这种方式我们不能接受, MTFlutter目前未开源,无从继续研究。 接下来着重看一下 Fair


四,Fair接入过程


1,添加依赖


推荐使用pub形式引入


# add Fair dependency
dependencies:
fair: 2.7.0

# add compiler dependency
dev_dependencies:
build_runner: ^2.0.0
fair_compiler: ^1.2.0

# switch "fair_version" according to the local Flutter SDK version
dependency_overrides:
fair_version: 3.0.0

Flutter版本切换


通过切换 flutter_version 版本进行版本兼容。例如,将本机切换为 flutter 2.0.6 后,Fair 需要同步切换


# switch to another stable flutter version
dependency_overrides:
fair_version: 2.0.6

2,使用 Fair


在App中接入Fair步骤如下:


将 FairApp 添加为需要动态化部分的顶级节点

常见做法是作为 App 的根节点,如果不是全局采用也可以作为子页面的根节点


void main() {
WidgetsFlutterBinding.ensureInitialized();

FairApp.runApplication(
_getApp(),
plugins: {
},
);
}

dynamic _getApp() => FairApp(
modules: {
},
delegate: {
},
child: MaterialApp(
home: FairWidget(
name: 'DynamicWidget',
path: 'assets/bundle/lib_src_page_dynamic_widget.fair.json',
data: {"fairProps": json.encode({})}),
),
);

添加动态组件

每一个动态组件由一个FairWidget表示。


FairWidget(
name: 'DynamicWidget',
path: 'assets/bundle/lib_src_page_dynamic_widget.fair.json',
data: {"fairProps": json.encode({})}),

根据不同场景诉求,FairWidget可以混合和使用



  1. 可以作为不同组件混合使用

  2. 一般作为一个全屏页面

  3. 支持嵌套使用,即可以局部嵌套在普通Widget下,也可以嵌套在另一个FairWidget下


五,Fair接入体验


1,fork,下载工程


将官方Github工程fork到自己仓库后, 下载工程。使用官方提供的 test_case/best_ui_templates工程体验fair的体验。


2, 执行 pub get


在 best_ui_templates工程中,执行 pub get命令获取依赖。


3,开发业务


接下来正式开始开发流程。 把一个页面改写为 用Fair 编写:



  1. 创建需要动态化的 componnet, 并添加 @FairPatch() 注解。添加上注解后,在Fair生成产物时,会把此Component build生成 FairWidget加载的产物。


image-20221116173528472


2, 执行 Fair工具链插件的命令生成产物, 如图:


<u>image-20221116173837910</u>


3, 最终生成的产物,拷贝到 assets/bundle目录下(配置config.json后,会自动拷贝)


<u><u>image-20221116182132104</u></u>


4, 看效果, 下图为使用 Fair 改造后的页面:



Screenshot_2022_1116_172859
Screenshot_2022_1116_192940

六,Fair优势


1,社区活跃度高


官方对Fair维护力度大,版本更新较快,问题修复及时,活跃的开发者社区氛围。


使得开发者在开发Fair过程中遇到的问题, 能够及时反馈给官方, 并能得到快速的帮助和解决。


2, 一份代码,灵活使用


Fair的区别于MTFlutter和MXFlutter这2种动态化方案,Fair能让同一份代码在Flutter原生和动态之间随意切换。在开发跟版本需求时,使用原生代码发布,以此持续保持Flutter的性能优势;而在热更新场景可以通过下发动态文件来达到动态更新Widget的目的。使用方式更加灵活。


3,配套开发工具丰富


Faircli配套工具链

官方为了让开发者快速上手,降低接入门槛, 解决在接入过程中的痛点。 Fair团队开发了Faircli配套工具链,主要包含三个部分:



  • 工程创建:快速搭建Fair载体工程及动态化工程。

  • 模板代码:提供页面及组件模板。

  • 本地热更新:线下开发使用,实现开发阶段快速预览Fair动态化功能。


在安装了工具链提供的dart命令行工具及AS插件后, 通过创建模板, 构建产物, 本地启服务,体验热更新功能,开发者可以轻松接入并体验Fair。


Fair语法检测插件

官方为了让开发者在Fair开发过程中,出现不正确或者不支持的语法问题。 开发了配套插件去提示用户使用Fair语法糖。


查看以下示例:


1,build方法下if的代码检测,及提示引导信息


44b58320-e608-420f-854f-799b5bf03cf5image


2,点击more action 或者 AS代码提示快捷键


41094a86-2aea-43e6-b7f0-69aef1c653c0image


3,根据提示点击替换


image.png


通过插件,在编写fair过程中,可以快速识别并解决不支持的语法问题。 提高开发Fair效率。


Fair Web代码编辑器

Fair其中一个方向是在线动态化平台,即在网页中编辑dart代码,在线预览Flutter效果和Fair动态化效果,并且发布Fair动态化产物。


通过在Fair Web代码编辑器,开发者可以在没有复杂的IDE配置的情况下,在网页端开发Fair并预览。 这无疑是降低了接入成本, 为开发者可以快速体验Fair提供了非常便捷的方式。


七,总结


通过近期对各大互联网公司在Flutter动态化方向上的探究方案。 发现这些方案都还没有达到成熟阶段,想在实际业务上落地, 还得看各团队后期的维护力度和开发投入程度。


MXFlutter使用js编写Dart的方式, 抛弃了原本Flutter的开发模式, 导致开发成本大,以及后续维护成本也大,官方已停止维护。


MTFlutter采用布局,逻辑都是使用Dart, 通过静态生产 DSL+Runtime 解释运行的思路,解决布局和逻辑的动态化,然而并没有开源计划,无从深入研究。


Fair通过Fair Compiler工具对原生Dart源文件的自动转化,使项目获得动态更新Widget Tree和State的能力。目前官方维护力度较大, 社区活跃,并且有比较全面的Fair生态工具。 期待 Fair 团队可以解决在开发Fair过程中一些体验问题,如语法支持不全等, 让Fair成为真正能够让开发者可以快速接入,能够达到和正常开发Flutter接近的体验。 为广大Flutter开发人员解决动态化的痛点。


支持我们


欢迎大家使用 Fair,也欢迎大家为我们点亮star

Github地址:github.com/wuba/fair

Fair官网:fair.58.com/


欢迎贡献


通过Issue提交问题,贡献代码请提交Pull Request,管理员将对代码进行审核。


作者:王猛猛
来源:juejin.cn/post/7174978087879671865
收起阅读 »

全网最优雅安卓控件可见性检测

引子 view.setOnClickListener { // 当控件被点击时触发的逻辑 } 正是因为 View 对控件点击采用了策略模式,才使得监听任何控件的点击事件变得易如反掌。 我有一个愿望。。。 如果 View 能有一个可见性监听该多好啊! view...
继续阅读 »

引子


view.setOnClickListener { // 当控件被点击时触发的逻辑 }

正是因为 View 对控件点击采用了策略模式,才使得监听任何控件的点击事件变得易如反掌。


我有一个愿望。。。


如果 View 能有一个可见性监听该多好啊!


view.setOnVisibilityChangeListener { isVisible: Boolean ->   }

系统并未提供这个方法。。。


但业务上有可见性监听的需要,比如曝光埋点。当某控件可见时,上报XXX。


数据分析同学经常抱怨曝光数据不准确,有的场景曝光多报了,有的场景曝光少报了。。。


开发同学看到曝光埋点也很头痛,不同场景的曝光检测有不同的方法,缺乏统一的可见性检测入口,存在一定重复开发。


本文就试图为单个控件以及列表项的可见性提供统一的检测入口。


控件的可见性受到诸多因素的影响,下面是影响控件可见性的十大因素:



  1. 手机电源开关

  2. Home 键

  3. 动态替换的 Fragment 遮挡了原有控件

  4. ScrollView, NestedScrollView 的滚动

  5. ViewPager, ViewPager2 的滚动

  6. RecyclerView 的滚动

  7. 被 Dialog 遮挡

  8. Activity 切换

  9. 同一 Activity 中 Fragment 的切换

  10. 手动调用 View.setVisibility(View.GONE)

  11. 被输入法遮盖


能否把这所有的情况都通过一个回调方法表达?目标是通过一个 View 的扩展方法完成上述所有情况的检测,并将可见性回调给上层,形如:


fun View.onVisibilityChange(block: (view: View, isVisible: Boolean) -> Unit) {}

若能实现就极大化简了上层可见性检测的复杂度,只需要如下代码就能实现任意控件的曝光上报埋点:


view.onVisibilityChange { view, isVisible ->
if(isVisible) { // 曝光埋点 }
else {}
}

控件全局可见性检测


可见性检测分为两步:



  1. 捕获时机:调用检测算法检测控件可见性的时机。

  2. 检测算法:描述如何检测控件是否对用户可见。


拿“手动调用 View.setVisibility(View.GONE)”举例,得先捕获 View Visibility 发生变化的时机,并在此刻检测控件的可见性。


下面是View.setVisibility()的源码:


// android.view.View.java
public void setVisibility(@Visibility int visibility) {
setFlags(visibility, VISIBILITY_MASK);
}

系统并未在该方法中提供类似于回调的接口,即一个 View 的实例无法通过回调的方式捕获到 visibility 变化的时机。


难道通过自定义 View,然后重写 setVisibility() 方法?


这个做法接入成本太高且不具备通用性。


除了“手动调用 View.setVisibility(View.GONE)”,剩下的影响可见性的因素大多都可找到对应回调。难道得在fun View.onVisibilityChange()中对每个因素逐个添加回调吗?


这样实现太过复杂了,而且也不具备通用性,假设有例外情况,fun View.onVisibilityChange()的实现就得修改。


上面列出的十种影响控件可见性的因素都是现象,不同的现象背后可能对应相同的本质。


经过深挖,上述现象的本质可被收敛为下面四个:



  1. 控件全局重绘

  2. 控件全局滚动

  3. 控件全局焦点变化

  4. 容器控件新增子控件


下面就针对这四个本质编程。


捕获全局重绘时机


系统提供了ViewTreeObserver


public final class ViewTreeObserver {
public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener) {
checkIsAlive();
if (mOnGlobalLayoutListeners == null) {
mOnGlobalLayoutListeners = new CopyOnWriteArray();
}
mOnGlobalLayoutListeners.add(listener);
}
}

ViewTreeObserver 是一个全局的 View 树变更观察者,它提供了一系列全局的监听器,全局重绘即是其中OnGlobalLayoutListener


public interface OnGlobalLayoutListener {
public void onGlobalLayout();
}

当 View 树发生变化需要重绘的时候,就会触发该回调。


调用 View.setVisibility(View.GONE) 之所以能将控件隐藏,正是因为整个 View 树触发了一次重绘。(任何一次微小的重绘都是从 View 树的树根自顶向下的遍历并触发每一个控件的重绘,不需要重绘的控件会跳过,关于 Adroid 绘制机制的分析可以点击Android自定义控件 | View绘制原理(画多大?)


在可见性检测扩展方法中捕获第一个时机:


fun View.onVisibilityChange(block: (view: View, isVisible: Boolean) -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener {}
}

其中viewTreeObserver是 View 的方法:


// android.view.View.java
public ViewTreeObserver getViewTreeObserver() {
if (mAttachInfo != null) {
return mAttachInfo.mTreeObserver;
}
if (mFloatingTreeObserver == null) {
mFloatingTreeObserver = new ViewTreeObserver(mContext);
}
return mFloatingTreeObserver;
}

getViewTreeObserver() 用于返回当前 View 所在 View 树的观察者。


全局重绘其实覆盖了上述的两个场景:



  1. 同一 Activity 中 Fragment 的切换

  2. 手动调用 View.setVisibility(View.GONE)

  3. 被输入法覆盖


这两个场景都会发生 View 树的重绘。


捕获全局滚动时机



  1. ScrollView, NestedScrollView 的滚动

  2. ViewPager, ViewPager2 的滚动

  3. RecyclerView 的滚动


上述三个时机的共同特点是“发生了滚动”。


每个可滚动的容器控件都提供了各自滚动的监听


// android.view.ScrollView.java
public interface OnScrollChangeListener {
void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY);
}

// androidx.viewpager2.widget.ViewPager2.java
public abstract static class OnPageChangeCallback {
public void onPageScrolled(int position, float positionOffset, @Px int positionOffsetPixels) {}
public void onPageSelected(int position) {}
public void onPageScrollStateChanged(@ScrollState int state) {}
}

// androidx.recyclerview.widget.RecyclerView.java
public abstract static class OnScrollListener {
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {}
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {}
}

难道要针对不同的滚动控件设置不同的滚动监听器?


这样可见性检测就和控件耦合了,不具有通用性,也愧对View.onVisibilityChange()这个名字。


还好又在ViewTreeObserver中找到了全局的滚动监听:


public final class ViewTreeObserver {
public void addOnScrollChangedListener(OnScrollChangedListener listener) {
checkIsAlive();

if (mOnScrollChangedListeners == null) {
mOnScrollChangedListeners = new CopyOnWriteArray();
}

mOnScrollChangedListeners.add(listener);
}
}

public interface OnScrollChangedListener {
public void onScrollChanged();
}

在可见性检测扩展方法中捕获第二个时机:


fun View.onVisibilityChange(block: (view: View, isVisible: Boolean) -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener {}
viewTreeObserver.addOnScrollChangedListener {}
}

捕获全局焦点变化时机


下面这些 case 都是焦点发生了变化:



  1. 手机电源开关

  2. Home 键

  3. 被 Dialog 遮挡

  4. Activity 切换


同样借助于 ViewTreeObserver 可以捕获到焦点变化的时机。


到目前为止,全局可见性扩展方法中已经监听了三种时机,分别是全局重绘、全局滚动、全局焦点变化:


fun View.onVisibilityChange(block: (view: View, isVisible: Boolean) -> Unit) {
viewTreeObserver.addOnGlobalLayoutListener {}
viewTreeObserver.addOnScrollChangedListener {}
viewTreeObserver.addOnWindowFocusChangeListener {}
}

捕获新增子控件时机


最后一个 case 是最复杂的:动态替换的 Fragment 遮挡了原有控件。


该场景如下图所示:
1668323952343.gif


界面中有一个底边栏,其中包含各种 tab 标签,点击其中的标签会以 Fragment 的形式从底部弹出。此时,底边栏各 tab 从可见变为不可见,当点击返回时,又从不可见变为可见。


一开始的思路是“从被遮挡的 View 本身出发”,看看某个 View 被遮挡后,其本身的属性是否会发生变化?


View 内部以is开头的方法如下所示:


微信截图_20221113152433.png


我把其中名字看上去可能和被遮挡有关联的方法值全都打印出来了,然后触发 gif 中的场景,观察这些值在触发前后是否会发生变化。


几十个属性,一一比对,在看花眼之前,log 告诉我,被遮挡之后,这些都没有发生任何变化。。。。


绝望。。。但还不死心,继续寻找其他方法:


微信截图_20221113152922.png


我又找了 View 内部所有has开头的方法,也把其中看上去和被遮挡有关的方法全打印出来了。。。你猜结果怎么着?依然是徒劳。。。。


我开始质疑出发点是否正确。。。此时一声雷鸣劈醒了我。


视图只可能了解其自身以及其下层视图的情况,它无法得知它的平级甚至是父亲的绘制情况。而 gif 中的场景,即是在底边栏的同级有一个 Fragment 的容器。而且当视图被其他层级的控件遮挡时,整个绘制体系也不必通知那个被遮挡的视图,否则多低效啊(我yy的,若有大佬知道内情,欢迎留言指点一二。)


经过这层思考之后,我跳出了被遮挡的那个视图,转而去 Fragment 的容器哪里寻求解决方案。


Fragment 要被添加到 Activity 必须提供一个容器控件,容器控件提供了一个回调用于监听子控件被添加:


// android.view.ViewGroup.java
public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
mOnHierarchyChangeListener = listener;
}

public interface OnHierarchyChangeListener {
void onChildViewAdded(View parent, View child);
void onChildViewRemoved(View parent, View child);
}

为了监听 Fragment 被添加的这个瞬间,得为可见性检测扩展方法添加一个参数:


fun View.onVisibilityChange(
viewGroup:
ViewGroup? = null, // 容器
block: (
view: View, isVisible: Boolean) -> Unit
)
{ }

其中 viewGroup 表示 Fragment 的容器控件。


既然 Fragment 的添加也是往 View 树中插入子控件,那 View 树必定会重绘,可以在全局重绘回调中进行分类讨论,下面是伪代码:


fun View.onVisibilityChange(
viewGroup:
ViewGroup? = null,
block: (
view: View, isVisible: Boolean) -> Unit
)
{
var viewAdded = false
// View 树重绘时机
viewTreeObserver.addOnGlobalLayoutListener {
if(viewAdded){
// 检测新插入控件是否遮挡当前控件
}
else {
// 检测当前控件是否出现在屏幕中
}
}
// 监听子控件插入
viewGroup?.setOnHierarchyChangeListener(object : OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
viewAdded = true
}
}
override fun onChildViewRemoved(parent: View?, child: View?) {
viewAdded = false
}
})
}

子控件的插入回调总是先于 View 树重绘回调。所以先在插入时置标志位viewAdded = true,以便在重绘回调中做分类讨论。(因为检测子控件遮挡和是否出现在屏幕中是两种不同的检测方案)


可见性检测算法


检测控件的可见性的算法是:“判断控件的矩形区域是否和屏幕有交集”


为此新增扩展属性:


val View.isInScreen: Boolean
get() = ViewCompat.isAttachedToWindow(this) && visibility == View.VISIBLE && getLocalVisibleRect(Rect())

val 类名.属性名: 属性类型这样的语法用于为类的实例添加一个扩展属性,它并不是真地给类新增了一个成员变量,而是在类的外部新增属性值的获取方法。


当前新增的属性是 val 类型的,即常量,所以只需要为其定义个 get() 方法来表达如何获取它的值。


View 是否在屏幕中由三个表达式共同决定。



  1. 先通过 ViewCompat.isAttachedToWindow(this) 判断控件是否依附于窗口。

  2. 再通过 visibility == View.VISIBLE 判断视图是否可见。

  3. 最后调用getLocalVisibleRect()判断它的矩形相对于屏幕是否可见:


// android.view.View.java
public final boolean getLocalVisibleRect(Rect r) {
final Point offset = mAttachInfo != null ? mAttachInfo.mPoint : new Point();
if (getGlobalVisibleRect(r, offset)) {
r.offset(-offset.x, -offset.y);
return true;
}
return false;
}

该方法会先获取控件相对于屏幕的矩形区域并存放在传入的 Rect 参数中,然后再将其偏移到控件坐标系。如果矩形区域为空,则返回 false 表示不在屏幕中,否则为 true。


刚才捕获的那一系列时机,有可能会被多次触发。为了只将可见性发生变化的事件回调给上层,得做一次过滤:


val KEY_VISIBILITY = "KEY_VISIBILITY".hashCode()

val checkVisibility = {
// 获取上一次可见性
val lastVisibility = getTag(KEY_VISIBILITY) as? Boolean
// 获取当前可见性
val isInScreen = this.isInScreen() && visibility == View.VISIBLE
// 无上一次可见性,表示第一次检测
if (lastVisibility == null) {
if (isInScreen) {
// 回调可见性回调给上层
block(this, true)
// 更新可见性
setTag(KEY_VISIBILITY, true)
}
}
// 当前可见性和上次不同
else if (lastVisibility != isInScreen) {
// 回调可见性给上层
block(this, isInScreen)
// 更新可见性
setTag(KEY_VISIBILITY, isInScreen)
}
}

过滤重复事件的方案是记录上一次可见性(记录在 View 的 tag 中),如果这一次可见性检测结果和上一次相同则不回调给上层。


将可见性检测定义为一个 lambda,这样就可以在捕获不同时机时复用。


以下是完整的可见性检测代码:


fun View.onVisibilityChange(
viewGroups:
List<ViewGroup> = emptyList()
, // 会被插入 Fragment 的容器集合
needScrollListener: Boolean = true,
block: (view: View, isVisible: Boolean) -> Unit
) {
val KEY_VISIBILITY = "KEY_VISIBILITY".hashCode()
val KEY_HAS_LISTENER = "KEY_HAS_LISTENER".hashCode()
// 若当前控件已监听可见性,则返回
if (getTag(KEY_HAS_LISTENER) == true) return

// 检测可见性
val checkVisibility = {
// 获取上一次可见性
val lastVisibility = getTag(KEY_VISIBILITY) as? Boolean
// 判断控件是否出现在屏幕中
val isInScreen = this.isInScreen
// 首次可见性变更
if (lastVisibility == null) {
if (isInScreen) {
block(this, true)
setTag(KEY_VISIBILITY, true)
}
}
// 非首次可见性变更
else if (lastVisibility != isInScreen) {
block(this, isInScreen)
setTag(KEY_VISIBILITY, isInScreen)
}
}

// 全局重绘监听器
class LayoutListener : ViewTreeObserver.OnGlobalLayoutListener {
// 标记位用于区别是否是遮挡case
var addedView: View? = null
override fun onGlobalLayout() {
// 遮挡 case
if (addedView != null) {
// 插入视图矩形区域
val addedRect = Rect().also { addedView?.getGlobalVisibleRect(it) }
// 当前视图矩形区域
val rect = Rect().also { this@onVisibilityChange.getGlobalVisibleRect(it) }
// 如果插入视图矩形区域包含当前视图矩形区域,则视为当前控件不可见
if (addedRect.contains(rect)) {
block(this@onVisibilityChange, false)
setTag(KEY_VISIBILITY, false)
} else {
block(this@onVisibilityChange, true)
setTag(KEY_VISIBILITY, true)
}
}
// 非遮挡 case
else {
checkVisibility()
}
}
}

val layoutListener = LayoutListener()
// 编辑容器监听其插入视图时机
viewGroups.forEachIndexed { index, viewGroup ->
viewGroup.setOnHierarchyChangeListener(object : ViewGroup.OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View?, child: View?) {
// 当控件插入,则置标记位
layoutListener.addedView = child
}

override fun onChildViewRemoved(parent: View?, child: View?) {
// 当控件移除,则置标记位
layoutListener.addedView = null
}
})
}
viewTreeObserver.addOnGlobalLayoutListener(layoutListener)
// 全局滚动监听器
var scrollListener:ViewTreeObserver.OnScrollChangedListener? = null
if (needScrollListener) {
scrollListener = ViewTreeObserver.OnScrollChangedListener { checkVisibility() }
viewTreeObserver.addOnScrollChangedListener(scrollListener)
}
// 全局焦点变化监听器
val focusChangeListener = ViewTreeObserver.OnWindowFocusChangeListener { hasFocus ->
val lastVisibility = getTag(KEY_VISIBILITY) as? Boolean
val isInScreen = this.isInScreen
if (hasFocus) {
if (lastVisibility != isInScreen) {
block(this, isInScreen)
setTag(KEY_VISIBILITY, isInScreen)
}
} else {
if (lastVisibility == true) {
block(this, false)
setTag(KEY_VISIBILITY, false)
}
}
}
viewTreeObserver.addOnWindowFocusChangeListener(focusChangeListener)
// 为避免内存泄漏,当视图被移出的同时反注册监听器
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) {
}

override fun onViewDetachedFromWindow(v: View?) {
v ?: return
// 有时候 View detach 后,还会执行全局重绘,为此退后反注册
post {
try {
v.viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
} catch (_: java.lang.Exception) {
v.viewTreeObserver.removeGlobalOnLayoutListener(layoutListener)
}
v.viewTreeObserver.removeOnWindowFocusChangeListener(focusChangeListener)
if(scrollListener !=null) v.viewTreeObserver.removeOnScrollChangedListener(scrollListener)
viewGroups.forEach { it.setOnHierarchyChangeListener(null) }
}
removeOnAttachStateChangeListener(this)
}
})
// 标记已设置监听器
setTag(KEY_HAS_LISTENER, true)
}

该控件可见性检测方法,最大的用处在于检测 Fragment 的可见性。详细讲解可以点击 页面曝光难点分析及应对方案


Talk is cheap,show me the code


上述源码可以在这里找到。


推荐阅读


业务代码参数透传满天飞?(一)


业务代码参数透传满天飞?(二)


全网最优雅安卓控件可见性检测


全网最优雅安卓列表项可见性检测


页面曝光难点分析及应对方案


你的代码太啰嗦了 | 这么多对象名?


你的代码太啰嗦了 | 这么多方法调用?


作者:唐子玄
来源:juejin.cn/post/7165427955902971918
收起阅读 »

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

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

自我介绍


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


学习


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


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


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


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


生活&实习


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


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


上路


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


日常挤地铁


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


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


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


又上路啦


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


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


杭州租的房


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


双十一零食自助


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


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


超乖的猫猫


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


忘记叫啥地了


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


感情


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


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


展望


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


学习



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

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

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

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

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


工作



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

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

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


生活



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

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

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

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


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


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

iOS 3年开发迷茫随心聊

iOS 3年开发迷茫随心聊 从毕业开始做iOS,到现在已经是第4个年头了。第一家公司,做了一年,项目没上线就倒闭了,导致找第二家公司的时候也没有一个项目能拿的出手。第二家公司误入一家游戏公司,每天工作就是将H5小游戏做成一个App,想办法上线,一年过去了,技术...
继续阅读 »

iOS 3年开发迷茫随心聊


从毕业开始做iOS,到现在已经是第4个年头了。第一家公司,做了一年,项目没上线就倒闭了,导致找第二家公司的时候也没有一个项目能拿的出手。第二家公司误入一家游戏公司,每天工作就是将H5小游戏做成一个App,想办法上线,一年过去了,技术其实也没什么长进,但是通过这个过程了解一些苹果上架的知识。也有了几个上线项目经验了。由于想找个正经互联网公司做App,也离职找工作。


找工作头一个月,发现面试面的问题都是与底层相关,一问三不知,在家埋头学了2个月底层相关知识(可以理解背题)。原理是懂一点了,但是没有在实际项目中运用,工资也上不太去。在面了20多家公司后,终于找到现在第三份工作。


由于有第二份工作的经历,在第三家公司上班的时候一直在学习,有意识的去面试的原理去解决一些开发中的问题,例如使用runtime解决一些问题,却发现runtime如果没有很强的理解,还是不要用在项目里,因为可能出现未知的风险。例如使用交换方法,全局做了修改,但是后期项目需求更改,保持全局修改的前提下,对其他情况要做不同处理。也没有太多需求会使用到原理的内容,性能也不需要优化。


小公司对技术不太感冒,能完成需求就行,虽然自己力所能及的去做一些规范,但觉得做的还是不够,也不清楚其他公司到底是如何做的。小公司个人感觉对员工做事的责任心更加看重。需求就是写页面,页面还原的好,做的快一点,bug少一点就行了。不是理想的一个团队有什么方案,规范,让开发更有效率。最大感触是还好没有成为一个油腻的开发~。


在现在的公司,做了几个项目,也没有大的bug。学会了Swift进行开发。也许也算是一种收获吧。但是公司不加薪,今年的目标是想学点东西换一份工作。


学了1个月RxSwift,感觉也快学不下去了,公司是不可能用了,网上也有人说这个架构太重了。语言是个问题,自己英语水平有限,学习速度太慢了。如果有看到的这篇文章的小伙伴,也可以给我点意见。


最近想学一点提高开发效率的技能。和面试相关的内容。如果有大神经历过我这个时期,还麻烦给点建议。建议退iOS坑的就不必留言了。个人虽然菜,但是如果还没有做到小公司天花板的话,目前不考虑退坑。


第一次发文章也不知道说啥,后面会更新一些学习

作者:MissSunRise
来源:juejin.cn/post/7071892765763698719
笔记啥的。感谢包容。

收起阅读 »

乱打日志的男孩运气怎么样我不知道,加班肯定很多!

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

前言


线上出现问题,你的第一反应是什么?


如果是我的话,第一时间想的应该是查日志:



  1. if…else 到底进入了哪个分支?

  2. 关键参数是不是有缺失?

  3. 入参是不是有问题,没做好校验放进去了?


良好的日志能帮我们快速定位到问题所在,坑你的东西往往最为无形,良好的日志就是要让这些玩意无所遁形!


日志级别


Java应用中,日志一般分为以下5个级别:



  • ERROR 错误信息

  • WARN 警告信息

  • INFO 一般信息

  • DEBUG 调试信息

  • TRACE 跟踪信息


1)ERROR


ERROR 级别的日志一般在 catch 块里面出现,用于记录影响当前线程正常运行的错误,出现 Exception 的地方就可以考虑打印 ERROR 日志,但不包括业务异常。


需要注意的是,如果你抛出了异常,就不要记录 ERROR 日志了,应该在最终的地方处理,下面这样做就是不对的:


try {
   int i = 1 / 0;
} catch (Exception e) {
   log.error("出错了,什么错我不知道,啊哈哈哈!", e);
   throw new CloudBaseException();
}

2)WARN


不应该出现,但是不会影响当前线程执行的情况可以考虑打印 WARN 级别的日志,这种情况有很多,比如:



  • 各种池(线程池、连接池、缓存池)的使用超过阈值,达到告警线

  • 记录业务异常

  • 出现了错误,但是设计了容错机制,因此程序能正常运行,但需要记录一下


3)INFO


使用最多的日志级别,使用范围很广,用来记录系统的运行信息,比如:



  • 重要模块中的逻辑步骤呈现

  • 客户端请求参数记录

  • 调用第三方时的参数和返回结构


4)DEBUG


Debug 日志用来记录自己想知道的所有信息,常常是某个功能模块运行的详细信息,已经中间的数据变化,以及性能信息。


Debug 信息在生产环境一般是关闭状态的,需要使用开关管理(比如 SpringBoot Admin 可以做到),一直开启会产生大量的 Debug,而 Debug 日志在程序正常运行时大部分时间都没什么用。


if (log.isDebugEnabled()) {
   log.debug("开始执行,开始时间:[{}],参数:[{}]", startTime, params);
   log.debug("通过计算,得到参数1:[{}],参数2:[{}]", param1, param2);
   log.debug("最后处理结果:[{}]", result);
}

5)TRACE


特别详细的系统运行完成信息,业务代码中一般不使用,除非有特殊的意义,不然一般用 DEBUG 代替,事实上,我编码到现在,也没有用过这个级别的日志。


使用正确的格式


如果你是这样打印日志的:


log.info("根据条件id:{}" + id + "查询用户信息");

不要这样做,会产生大量的字符串对象,占用空间的同时也会影响性能。


正确的做法是使用参数化信息的方式:


log.info("根据条件id:[{}],查询用户信息", id);

这样做除了能避免大量创建字符串之外,还能明确的把参数隔离出去,当你需要把参数复制出来的时候,只需要双击鼠标即可,而不是用鼠标慢慢对准再划拉一下。


这样打出来的日志,可读性强,对排查问题的帮助也很大!


小技巧


1)多线程


遇到多个线程一起执行的日志怎么打?


有些系统,涉及到并发执行,定时调度等等,就会出现多次执行的日志混在一起,出问题不好排查,我们可以把线程名打印进去,或者加一个标识用来表明这条日志属于哪一次执行:


if (log.isDebugEnabled()) {
   log.debug("执行ID=[{}],处理了ID=[{}]的消息,处理结果:[{}]", execId, id, result);
}

2)使用 SpringBoot Admin 灵活开关日志级别


image-20220727155526217


写在最后


一开始写代码的时候,没有规范日志的意识,不管哪里,都打个 INFO,打印出来的东西也没有思考过,有没有意义,其实让自己踩了不少坑,加了不少班,回过头,我想对学习时期的我说一句:”能让你加班的东西,都藏在各种细节里!写代码之前,先好好学习如何打日志!“


作者:你算哪块小蛋糕
来源:juejin.cn/post/7124958610123128839
收起阅读 »

你可能忽略的10种JavaScript快乐写法

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

你可能忽略的10种JavaScript快乐写法


前言



  • 代码的简洁、美感、可读性等等也许不影响程序的执行,但是却对人(开发者)的影响非常之大,甚至可以说是影响开发者幸福感的重要因素之一;

  • 了解一些有美感的代码,不仅可以在一定程度上提高程序员们的开发效率,有些还能提高代码的性能,可谓是一举多得;


笔者至今难以忘记最开始踏入程序员领域时接触的一段List内嵌for的Python代码:


array = [[16, 3, 7], [2, 24, 9], [4, 1, 12]]
row_min = [min(row) for row in array ]
print(row_min)

这可能就是动态语言非常优秀的一点,而JavaScript同样作为动态语言,其中包含的优秀代码片段也非常之多,比如我们通过JavaScript也可以非常轻松地实现上述的功能:


const array = [[16, 3, 7], [2, 24, 9], [4, 1, 12]]
const row_min = array.map(item => Math.min(...item))
console.log(row_min)

能写出优秀的代码一直是笔者所追求的,以下为笔者在开发阅读过程积累的一些代码片段以及收集了互联网上一些优秀代码片段,希望对你有所帮助


概述


这里,考虑到有些技巧是大家见过的或者说是已经烂熟于心的,但总归有可能有些技巧没有留意过,为了让大家更加清楚的找到自己想要查阅的内容以查漏补缺,所以这里笔者贴心地为大家提供了一张本文内容的索引表,供大家翻阅以快速定位,如下:


应用场景标题描述补充1补充2
数组去重通过内置数据解构特性进行去重[] => set => []通过遍历并判断是否存在进行去重[many items].forEach(item => (item <不存在于> uniqueArr) && uniqueArr.push(item))
数组的最后一个元素获取数组中位置最后的一个元素使用at(-1)
数组对象的相关转换对象到数组:Object.entries()数组到对象:Obecjt.fromEntries()
短路操作通过短路操作避免后续表达式的执行a或b:a真b不执行a且b:a假b不执行
基于默认值的对象赋值通过对象解构合并进行带有默认值的对象赋值操作{...defaultData, ...data}
多重条件判断优化单个值与多个值进行对比判断时,使用includes进行优化[404,400,403].includes
交换两个值通过对象解构操作进行简洁的双值交换[a, b] = [b, a]
位运算通过位运算提高性能和简洁程度
replace()的回调通过传入回调进行更加细粒度的操作
sort()的回调通过传入回调进行更加细粒度的操作根据字母顺序排序根据真假值进行排序

数组去重


这不仅是我们平常编写代码时经常会遇到的一个功能实现之一,也是许多面试官在考查JavaScript基础时喜欢考查的题目,比较常见的基本有如下两类方法:


1)通过内置数据结构自身特性进行去重


主要就是利用JavaScript内置的一些数据结构带有不包含重复值的特性,然后通过两次数据结构转换的消耗[] => set => []从而达到去重的效果,如下演示:


const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = Array.from(new Set(arr));
// const uniqueArr = [...new Set(arr)];

2)通过遍历并判断是否存在进行去重


白话描述就是:通过遍历每一项元素加入新数组,新数组存在相同的元素则放弃加入,伪代码:[many items].forEach(item => (item <不存在于> uniqueArr) && uniqueArr.push(item))


至于上述的<不存在于>操作,可以是各种各样的方法,比如再开一个for循环判断新数组是否有相等的,或者说利用一些数组方法判断,如indexOfincludesfilterreduce等等


const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = [];
arr.forEach(item => {
// 或者!uniqueArr.includes(item)
if(uniqueArr.indexOf(item) === -1){
uniqueArr.push(item)
}
})

结合filter(),判断正在遍历的项的index,是否是原始数组的第一个索引:


const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = arr.filter((item, index) => {
return arr.indexOf(item, 0) === index;
})

结合reduce(),prev初始设为[],然后依次判断cur是否存在于prev数组,如果存在则加入,不存在则不动:


const arr = ['justin1go', 'justin2go', 'justin2go', 'justin3go', 'justin3go', 'justin3go'];
const uniqueArr = arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);

数组的最后一个元素


对于获取数组的最后一个元素,可能平常见得多的就是arr[arr.length - 1],我们其实可以使用at()方法进行获取


const arr = ['justin1go', 'justin2go', 'justin3go'];
console.log(arr.at(-1)) // 倒数第一个值
console.log(arr.at(-2)) // 倒数第二个值
console.log(arr.at(0)) // 正数第一个
console.log(arr.at(1)) // 正数第二个


注:node14应该是不支持的,目前笔者并不建议使用该方法,但获取数组最后一个元素是很常用的,就应该像上述语法一样简单...



数组对象的相互转换



    const entryified = [
["key1", "justin1go"],
["key2", "justin2go"],
["key3", "justin3go"]
];

const originalObject = Object.fromEntries(entryified);
console.log(originalObject);

短路操作


被合理运用的短路操作不仅非常的优雅,还能减少不必要的计算操作


1)基本介绍


主要就是||或操作、&&且操作当第一个条件(左边那个)已经能完全决定整个表达式的值的时候,编译器就会跳过该表达式后续的计算



  • 或操作a || b:该操作只要有一个条件为真值时,整个表达式就为真;即a为真时,b不执行;

  • 且操作a && b:该操作只要有一个条件为假值时,整个表达式就为假;即a为假时,b不执行;


2)实例


网络传输一直是前端的性能瓶颈,所以我们在做一些判断的时候,可以通过短路操作减少请求次数:


const nextStep = isSkip || await getSecendCondition();
if(nextStep) {
openModal();
}

还有一个经典的代码片段:


function fn(callback) {
// some logic
callback && callback()
}

基于默认值的对象赋值



  • 很多时候,我们在封装一些函数或者类时,会有一些配置参数。

  • 但这些配置参数通常来说会给出一个默认值,而这些配置参数用户是可以自定义的

  • 除此之外,还有许许多多的场景会用到的这个功能:基于默认值的对象赋值。


function fn(setupData) {
const defaultSetup = {
email: "justin3go@qq.com",
userId: "justin3go",
skill: "code",
work: "student"
}
return { ...defaultSetup, ...setupData }
}

const testSetData = { skill: "sing" }
console.log(fn(testSetData))

如上{ ...defaultSetup, ...setupData }就是后续的值会覆盖前面key值相同的值。


多重条件判断优化


if(condtion === "justin1go" || condition === "justin2go" || condition === "justin3go"){
// some logic
}

如上,当我们对同一个值需要对比不同值的时候,我们完全可以使用如下的编码方式简化写法并降低耦合性:


const someConditions = ["justin1go", "justin2go", "justin3go"];
if(someConditions.includes(condition)) {
// some logic
}

交换两个值


一般来说,我们可以增加一个临时变量来达到交换值的操作,在Python中是可以直接交换值的:


a = 1
b = 2
a, b = b, a

而在JS中,也可以通过解构操作交换值;


let a = 1;
let b = 2;
[a, b] = [b, a]

简单理解一下:



  • 这里相当于使用了一个数组对象同时存储了a和b,该数组对象作为了临时变量

  • 之后再将该数组对象通过解构操作赋值给a和b变量即可


同时,还有种比较常见的操作就是交换数组中两个位置的值:


const arr = ["justin1go", "justin2go", "justin3go"];
[arr[0], arr[2]] = [arr[2], arr[0]]

位运算


关于位运算网上的讨论参差不齐,有人说位运算性能好,简洁;也有人说位运算太过晦涩难懂,不够易读,这里笔者不发表意见,仅仅想说的是尽量在使用位运算代码的时候写好注释!


下面为一些常见的位运算操作,参考链接


1 ) 使用&运算符判断一个数的奇偶


// 偶数 & 1 = 0
// 奇数 & 1 = 1
console.log(2 & 1) // 0
console.log(3 & 1) // 1

2 ) 使用~, >>, <<, >>>, |来取整


console.log(~~ 6.83)    // 6
console.log(6.83 >> 0) // 6
console.log(6.83 << 0) // 6
console.log(6.83 | 0) // 6
// >>>不可对负数取整
console.log(6.83 >>> 0) // 6

3 ) 使用^来完成值交换


var a = 5
var b = 8
a ^= b
b ^= a
a ^= b
console.log(a) // 8
console.log(b) // 5

4 ) 使用&, >>, |来完成rgb值和16进制颜色值之间的转换


/**
* 16进制颜色值转RGB
*
@param {String} hex 16进制颜色字符串
*
@return {String} RGB颜色字符串
*/

function hexToRGB(hex) {
var hexx = hex.replace('#', '0x')
var r = hexx >> 16
var g = hexx >> 8 & 0xff
var b = hexx & 0xff
return `rgb(${r}, ${g}, ${b})`
}

/**
* RGB颜色转16进制颜色
*
@param {String} rgb RGB进制颜色字符串
*
@return {String} 16进制颜色字符串
*/

function RGBToHex(rgb) {
var rgbArr = rgb.split(/[^\d]+/)
var color = rgbArr[1]<<16 | rgbArr[2]<<8 | rgbArr[3]
return '#'+ color.toString(16)
}
// -------------------------------------------------
hexToRGB('#ffffff') // 'rgb(255,255,255)'
RGBToHex('rgb(255,255,255)') // '#ffffff'

replace()的回调函数


之前写过一篇文章介绍了它,这里就不重复介绍了,F=>传送


sort()的回调函数


sort()通过回调函数返回的正负情况来定义排序规则,由此,对于一些不同类型的数组,我们可以自定义一些排序规则以达到我们的目的:



  • 数字升序:arr.sort((a,b)=>a-b)

  • 按字母顺序对字符串数组进行排序:arr.sort((a, b) => a.localeCompare(b))

  • 根据真假值进行排序:


const users = [
{ "name": "john", "subscribed": false },
{ "name": "jane", "subscribed": true },
{ "name": "jean", "subscribed": false },
{ "name": "george", "subscribed": true },
{ "name": "jelly", "subscribed": true },
{ "name": "john", "subscribed": false }
];

const subscribedUsersFirst = users.sort((a, b) => Number(b.subscribed) - Number(a.subscribed))

最后



  • 个人能力有限,并且代码片段这类东西每个人的看法很难保持一致,不同开发者有不同的代码风格,这里仅仅整理了一些笔者自认为还不错的代码片段;

  • 可能互联网上还存在着许许多多的优秀代码片段,笔者也不可能全部知道;

  • 所以,如果你有一些该文章中没有包含的优秀代码片段,就不要藏着掖着了,分享出来吧~


同时,如本文有所错误,望不吝赐教,友善指出🤝


Happy Coding!🎉🎉🎉


QQ图片20230223164124.gif


QQ图片20230223164124.gif


QQ图片20230223164124.gif


参考


作者:Justin3go
来源:juejin.cn/post/7203243879255277623
收起阅读 »

一次线上OOM问题分析

OOM
现象 线上某个服务有接口非常慢,通过监控链路查看发现,中间的 GAP 时间非常大,实际接口并没有消耗很多时间,并且在那段时间里有很多这样的请求。 原因分析 先从监控链路分析了一波,发现请求是已经打到服务上了,处理之前不知道为什么等了 3s,猜测是不是机器当时...
继续阅读 »

现象


线上某个服务有接口非常慢,通过监控链路查看发现,中间的 GAP 时间非常大,实际接口并没有消耗很多时间,并且在那段时间里有很多这样的请求。



原因分析


先从监控链路分析了一波,发现请求是已经打到服务上了,处理之前不知道为什么等了 3s,猜测是不是机器当时负载太大了,通过 QPS 监控查看发现,在接口慢的时候 CPU 突然增高,同时也频繁的 GC ,并且时间很长,但是请求量并不大,并且这台机器很快就因为 Heap满了而被下掉了。



去看了下日志,果然有 OOM 的报错,但是从报错信息上并没办法找到 Root Cause。


system error: org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Java heap space   at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1055)   at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)   at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)   at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)   at javax.servlet.http.HttpServlet.service(HttpServlet.java:681) 

另外开发同学提供了线索,在发生问题的时候在跑一个大批量的一次性 JOB,怀疑是不是这个 JOB 导致的,马上把 JOB 代码拉下来分析了下,JOB 做了分批处理,代码也没有发现什么问题。


虽然我们系统加了下面的 JVM 参数,但是由于容器部署的原因,这些文件在 pod 被 kill 掉之后没办法保留下来。


-XX:+HeapDumpOnOutOfMemoryError -XX:ErrorFile=/logs/oom_dump/xxx.log -XX:HeapDumpPath=/logs/oom_dump/xxx.hprof

这个现象是最近出现的,猜测是最近提交的代码导致的,于是去分析了最近提交的所有代码,很不幸的都没有发现问题。。。


在分析代码的过程中,该服务又无规律的出现了两次 OOM,只好联系运维同学优先给这个服务加了 EFS (Amazon 文件系统)等待下次出现能抓住这个问题。


刚挂载完 EFS,很幸运的就碰到了系统出现 OOM 的问题。


dump 出来的文件足有 4.8G,话不多说祭出 jvisualvm 进行分析,分析工具都被这个dump文件给搞挂了也报了个java.lang.OutOfMemoryError: Java heap space,加载成功之后就给出了导致OOM的线程。



找到了具体报错的代码行号,翻一下业务代码,竟然是一个查询数据库的count操作,这能有啥问题?


仔细看了下里面有个foreach遍历userId的操作,难道这个userId的数组非常大?



找到class按照大小排序,占用最多的是一个 byte 数组,有 1.07G,char 数组也有1.03G,byte 数组都是数字,直接查看 char 数组吧,点进去查看具体内容,果然是那条count语句,一条 SQL 1.03G 难以想象。。。




这个userId的数据完全是外部传过来的,并没有做什么操作,从监控上看,这个入参有 64M,马上联系对应系统排查为啥会传这么多用户过来查询,经过一番排查确认他们有个bug,会把所有用户都发过来查询。。。到此问题排查清楚。


解决方案


对方系统控制传入userId的数量,我们自己的系统也对userId做一个限制,问题排查过程比较困难,修改方案总是那么的简单。


别急,还有一个


看到这个问题,就想起之前我们还有一个同样类似的问题导致的故障。


也是突然收到很多告警,还有机器 down 机的告警,打开 CAT 监控看的时候,发现内存已经被打满了。



操作和上面的是一样的,拿到 dump 文件之后进行分析,不过这是一个漫长的过程,因为 down了好几台机器,最大的文件有12GB。


通过 MAT 分析 dump 文件发现有几个巨大的 String(熟悉的味道,熟悉的配方)。



接下来就是早具体的代码位置了,去查看了下日志,这台机器已经触发自我保护机制了,把代码的具体位置带了出来。


经过分析代码发现,代码中的逻辑是查询 TIDB(是有同步延迟的),发现在极端情况下会出现将用户表全部数据加载到内存中的现象。



于是找 DBA 拉取了对应时间段 TIDB 的慢查询,果然命中了。



总结


面对 OOM 问题如果代码不是有明显的问题,下面几个JVM参数相当有用,尤其是在容器化之后。


-XX:+HeapDumpOnOutOfMemoryError -XX:ErrorFile=/logs/oom_dump/xxx.log -XX:HeapDumpPath=/logs/oom_dump/xxx.hprof

另外提一个参数也很有用,正常来说如果程序出现 OOM 之后,就是有代码存在内存泄漏的风险,这个时候即使能对外提供服务,其实也是有风险的,可能造成更多的请求有问题,所以该参数非常有必要,可以让 K8S 快速的再拉起来一个实例。


-XX:+ExitOnOutOfMemoryError

另外,针对这两个非常类似的问题,对于 SQL 语句,如果监测到没有where条件的全表查询应该默认增加一个合适的limit作为限制,防止这种问题拖垮整个系统。


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

程序媛员的博客之旅

写博客困境 自从成为了一名程序媛,就一直有很多前辈,苦口婆心的告诉我:一定要写博客,好处多多!而我,作为一枚勤奋好学(haochilanzuo)的程序媛,其实心里一直埋藏着一颗写博客的小小种子。 无奈的是,每次冲动的热情都只能持续到更新两三篇技术文章,然后就没...
继续阅读 »

写博客困境


自从成为了一名程序媛,就一直有很多前辈,苦口婆心的告诉我:一定要写博客,好处多多!而我,作为一枚勤奋好学(haochilanzuo)的程序媛,其实心里一直埋藏着一颗写博客的小小种子。


无奈的是,每次冲动的热情都只能持续到更新两三篇技术文章,然后就没办法继续更新下去了。所以工作了这么多年,自己都成为老学姐了,还是没有拿得出手的个人博客,实在是惭愧。


经过深刻的自我反省之后,我觉得阻碍我更新博客的原因,主要有以下几个方面:


1. 工作太忙。


大家都知道,相比其他工种,作为程序员的工作强度还是蛮大的。每天都有做不完的需求,开不完的会议。经常要到晚上快下班了才有时间写代码,于是加班就成了家常便饭。下了班回家也感觉很累,不想再打开电脑,只能刷刷手机、看看综艺,做一些不费脑子的娱乐活动。


2. 文字功底太差。


作为一名理科生,上学的时候语文成绩就很差,作文都靠模板以及背的素材拼凑起来。高中毕业之后,几乎没有完整写过什么。而写博客,需要高强度大量输出内容,还要有组织有架构,逻辑条理清晰,这个对我来说简直太难了。所以经常写两篇之后,发现自己写的东西惨不忍睹,于是就暂停了更新博客的计划。


3. 没什么内容可写。


虽然每天都在写代码,但是很多时候做的都是重复性工作,并没有太多有技术含量、技术深度的内容,可以支撑我写出高大上的博客。


我个人更新不下去博客的主要原因就是上面几点,相信有很多想要更新博客却坚持不下去的同学,也都有同样的感受。


如何突破困境


我想说一下,我为什么觉得自己这次能克服这几个问题,以及克服这几个问题的方法。如果大家也和我有类似的问题,可以往下读一读,看有没有什么可以借鉴的地方。主要还是给我自己未来的日更之旅打打鸡血。



一、工作太忙,没时间。



每个人的一天都是24小时,为什么有些人能做更多的事情,实现更高的成就呢?我觉得这和每个人的时间管理方式是息息相关的。掌握高效的时间管理策略,是每个高效能人士的必备技能。


我以前觉得是因为程序员的工作比其他行业更忙,所以没有时间。但是看周围,把博客或者副业运营很好的那群人,工作也不闲。所以说,这个理由只是一个对自己时间管理无能的借口而已。真正的强者,从来不会找没有时间的借口,而不去做一些尝试。


当下,为了能够实现工作、写作(其实是搞副业)和生活之间的平衡,我决定先从这几个角度来优化我的时间使用效率。


1. 为任务分配合理的优先级


事情是永远做不完的,如果想做的太多,那么时间永远都不够。我准备用重要紧急四象限法来管理任务。每拿到一个任务后,先决策这个任务是属于哪个象限的,然后再安排做的时间。
image.png


我们之所以感觉每天忙忙碌碌,却没有什么进步,主要是因为在“紧急-不重要”的事情上,浪费了太多的时间。仔细想想,上班时间有多少是浪费在了,对未来成长没有任何意义的所谓“紧急”的事情上了。而真正“重要”的事情,却被我们以没有时间做,而一直推迟。


前段时间看到的一句话,对我触动很大:
Today you do things others don't do.
Tomorrow you do things others can't do.


做“重要-不紧急”的事情,不会对你的人生产生立竿见影的效果。但是长期下去,效果一定是惊人的,而且能给你带去很多别人没有的机会。


“人们总是容易高估一天的影响,而低估长期的影响”。比如学英语、写作,可能努力了一个月都没有效果,很多人就开始放弃了,转而去寻找其他的方法。但有些人坚持了下来,于是这些人坚持了一年、两年甚至几年之后,最后到达了很高的高度,才发现原来每一天的坚持都没有浪费,最后都是有效果的。


2. 减少任务切换,提高做事情的效率


提高做事情的效率,最好的办法就是进入“心流”的状态。不管是写代码、写文字还是看书学习,在“心流”的状态下,效率比平时要提高好几倍。


“心流”的状态,就是一种忘我的境界,忘记了时间、忘记了周围所处的环境,甚至忘记了身体上的痛苦,专心沉浸在当下所做的事情上。我相信这种状态,大家多多少少都有体会,比如在废寝忘食打游戏的时候。这种状态下,人所爆发出来的潜能是巨大的。


要达到“心流”的状态,最简单易行的方法,就是减少任务的切换。就像CPU线程切换,需要缓存上一个任务的执行状态,加载下一个任务的运行环境,效率很低。人脑也是,在上下文切换的时候,需要耗费很多的时间和精力。


而工作中,经常会被工作软件的消息提醒所打断,很难进入”心流“状态。比如,正在尝试解决一个疑难的问题,但是突然来了一条工作上的消息,于是不得不中断当前的工作,去看这个消息。等处理完消息后,在回到工作,可能已经忘记之前做到哪里了,又需要花时间才能重新进入状态。


可以尝试”番茄钟"的方法。在每个番茄钟开始的时候,屏蔽消息,集中精神工作25分钟,然后再花5分钟处理这25分钟到达的消息。处理完后,进入下一个番茄钟。


3. 不要给自己定太高的目标


之前我写博客,总是一篇文章写很长,想要在一篇文章中讲完和标题有关的所有知识点。但是这样会让自己很累,每次写一篇文章都要花很长的时间和精力,到后面甚至排斥写文章这件事情。


所以这次,我决定不给自己设太高的目标,每篇技术文章,争取讲完一个知识点就可以,如果内容特别多,可以采用连载的形式。最后可以新建一篇索引的文章,将各个连载的文章串起来。


PS:时间管理是一个复杂的事情,我之前也看过一些相关的书籍,后续我也想通过更系统的文章分享出来。先在这里挖个坑,如果想看就先关注我吧,后续我会慢慢把坑都填上。



二、文字功底太差



另一个困扰我的因素,就是自己的文字功底太差了。几乎没怎么写过文章的我,不知道怎么表达自己。有时候心里有很多想说的话,但是一写起来就读不通,没办法完整表达自己的意思。


为了能顺利完成日更的目标,我决定尝试下面的方法。


1. 先写起来,自然而然就会有进步


第一个就是不管怎么样,不管写得有多烂,先写起来,以量变来引起质变。我现在的写作量,可能连那些大V一个月的量都不到,凭什么觉得自己的水平就能和人家一样。如果每天输出500字,一年就是18.25万字。坚持写,我相信写一年之后,水平肯定会有进步。


没有什么是刻意练习不能达成的,如果有,那肯定是练习不够多。


2. 多看多模仿


写文章也是有方法可以借鉴的。去看好的文章是什么样的,向优秀的文章和作者学习。


比如,我之前看一个技术博主,会在每篇文章的开头放一个脑图,描述整片文章的整体架构,我觉得这个方法就很好。首先自己可以根据这篇脑图往里填充资料,速度更快也更清晰,同时,读者也可以在看文章之前对文章的内容有一个整体的感知,很快就能定位到自己需要的内容上。之后我的文章也可以借鉴这个方法。



三、没什么内容可写



关于没什么内容可写,以前做业务开发的时候,确实有这个问题,但是现在做系统开发了,几乎每天都在学习新的知识,所以完全不愁没有内容可写了。


如果有同学想开始写博客,但是又觉得没有内容可写,可以从以下几个方面去尝试:


1. 提前想一些topic,主动积累


在开始写博客之前,提前收集一些topic。我现在就有一个文档,专门用来放我想写的文章topic,现在这个文档里面已经有几十个可以写的topic了。


提前脑暴一些topic,或者列一个知识图谱,到时候如果发现没什么内容可写,直接去list里面找一个topic就好了。


2. 主动去学习一些新的东西


对于一些业务开发的同学,可以在开发之余,主动push自己去学一些新的技术。比如看一些技术书籍和博客。


博客内容


之后我的博客,主要会围绕下面这些方向:


Android性能优化


作为一名Android开发,更新的内容主要还是在Android相关的技术点上。由于我近期工作的重点主要在性能优化方面,所以前期的文章主要会更新性能优化相关的文章,包括启动时间、存储空间、稳定性、ANR等优化方案,以及一些相关的技术原理。


Android面试集锦


等把Android性能优化相关的内容写完,会再写一些面试相关的内容。作为一个拿过各大互联网offer、一毕业就当上面试官的学姐,在面试方面还是有不少经验的。


算法题解


算法题可能也会写一些,写一些我觉得好的题目的题解(主要是算法题比较好水文章,实在不行了就来篇题解)


读书笔记


我平常也会读一些技术之外的书籍,会写一些读书笔记,到时候会更新一些这方面的内容。


新的技术方向


除了Android开发以外,未来想学习一些新的技术方向,到时候也会更新到这个博客里,比如Web3相关的内容。


杂七杂八的思考


一些思考想法,对当下事件的看法,对未来的思考,个人成长、时间管理、投资理财等等相关内容,都会记录在博客里。


总结


说了这么多,也不知道会不会有人看我写的文章,毕竟现在Android开发已经不流行了。而且ChatGPT兴起之后,普通的内容生产者,会受到非常大的冲击。可能以后查东西都不需要去搜博客文章了,直接问ChatGPT就好了。之后我的博客文章,说不定也会让ChatGPT帮我写一部分。


总之不管有没有人看,不管AI是否会把我的工作取代,我还是会把日更坚持下去。如果对我更新的内容感兴趣,欢迎点点关注呀~~


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

【2022年终总结】北漂两年,领证,换工作

很高兴见到你 👋 我是 Flywith24~ 这是我第三篇个人年终总结。由于最近在处理工作,搬家等事情,所以拖到现在才发布。 2022 年几乎没输出什么内容,以至于前些天收到这样的评论~ 在之前两篇年终总结中,除了常规的流水账,我都会写一些当年的思考: 2...
继续阅读 »

很高兴见到你 👋 我是 Flywith24~


这是我第三篇个人年终总结。由于最近在处理工作,搬家等事情,所以拖到现在才发布。


image.png


2022 年几乎没输出什么内容,以至于前些天收到这样的评论~


47546a0a1e9bb52b90b06c374bad155f.jpg


在之前两篇年终总结中,除了常规的流水账,我都会写一些当年的思考:


2020年 分享了学习 Android 的经历和感悟;


2021年 分享了如何高效学习/工作的思考;


这一次我想分享关于工作中快速融入团队,社交关系这两部分内容。


工作方面


性能优化


Q1 参加了一个冷启动性能优化的专项,学了很多相关的知识


image.png


晋升


5 月通过了晋升答辩,提升了画图与 PPT 能力 🤪


image.png


技术栈扩容


6 月中旬由于工作需要,学习了 RN 并开发了一个独立的模块。快手动态化基建做的不错,所以熟悉了 TypeScript 的语法就开始进行需求开发了。


8 月使用 ts 写了一个在 Github 个人主页显示最近文章列表的小工具,详情请参考 开源项目:Github 主页显示最近发布的掘金文章(支持动态更新)



这也是 2022年在掘金发的唯一一篇文章 😳



image.png


之后学习了心心念念的 Jetpack Compose,将个人项目用 Compose 进行了重写。


面试


11 月开始居家办公,这期间面了几家公司,最后入职了小红书。



关于入职时间拉扯了一番,最终于 2023.02.08 入职。(02.08 也是我 2020 年入职快手的日子)



image.png


生活方面


读书


image.png

今年读了很多技术方面的书,不过都没有读完,倒是读完几本杂书,其中我最喜欢的是这本:


image.png

这本书另一个译名是:狗屁工作(bullshit jobs),它的译者在播客里表示由于过审等因素用了现在的译名



知识星球人数破千


image.png

2022 年知识星球人数破千 🥳 该星球永久免费,详情可点此查看


全拼改双拼


一直以来,我都在追求更高效的工作方式,减少无用且重复的活动。


2022 年在朋友的建议下使用双拼打字,过程中伴随着阵痛,但结果令人满意。


关于什么是双拼,可以看下这个 up 主的视频


双拼这点东西,up居然讲了十分钟——十分钟了解双拼到底是啥


image.png

西部大善人


2018 年买了《荒野大镖客2》,但一直停留在第一章。2022 年打了 100 多个小时,体验了亚瑟的一生。


image.png


领证


2022.06.06 人生清单迎来一个里程碑的节点,领证啦~


image.png

旅行


2022 年点亮了三座城市,作为东北人好喜欢云南的天气


image.png
image.png
image.png
image.png


快速融入新团队


在北京我两次面临这个问题,希望我的经验对各位小伙伴有所帮助。


上手项目


快速上手公司项目,对于新人在团队内第一时间建立影响力是非常重要的。这里站在客户端开发的角度聊下我的经验。



  • 在 app 里体验所在团队负责内容(拿到 offer 后就可以做,这点很重要)

  • 认真阅读团队内提供的文档(如果有的话)

  • 将项目里自研库和没用过的第三方库通过写 demo 的方式学习其使用流程(了解原理更佳,后面会避免很多坑)

  • 从负责业务的入口开始分析,用 CodeLocator 这类工具抓一下主要界面的视图结构,形成文档,这对未来的新人很有帮助

  • 有些细节可以在做相关需求时再去了解,不要心急


了解同事


融入团队另一个重要的事是了解同事。作为新人会有相互介绍的环节,一次记住二十几个人是不容易的(对于有些脸盲的我则更加困难)。


所以我会制作一份座位表,罗列每个人工位,名字,Android 还是 iOS。实践了两次,对于我很有效。


之后我会利用需求管理平台查看每个人最近的需求以及代码,对同事主要负责的业务有一个初步了解。


不懂就问


在项目里如果遇到用了很大力气却没搞懂的问题可以果断向同事提问。


不过需要注意,不懂就问是建立在「正确提问」的基础上的


提问时可将自己的尝试和猜测说出来。


image.png



摘自 2021 年终总结



对于应届或实习生来说,如果遇到的 mentor 比较内向不爱说话,需要自己更加主动,认真思考并整理遇到的问题大胆提问。


社交关系



声明:本节只是分享我的思考与选择


我尊重每个人的选择与喜好。例如有的人喜欢工作与生活有明确边界的同事关系。选择没有好坏之分,自己喜欢即可。



选大学,我们选择的是专业,老师,同学。


同样,我们在工作中选择的是公司,团队,同事。


本质上,我们选择的是圈子,环境。


即使去一家上万人的公司,对自己影响最大的也只有身边的人。


因此我喜欢这样的社交关系:



  • 工作时我们是一起奋战的战友

  • 摸鱼时我们时一起讨论的群友

  • 用餐时我们是一起行动的饭友

  • 假期时我们是一起玩耍的好友

  • 分别时我们是一起相送的朋友


如果你想与我有这样的社交关系


芝麻掉进针眼儿里


image.png


小红书内容发布客户端组 Android 和 iOS 还有 hc


点击该链接查看内推详情


关于我


人总是喜欢做能够获得正反馈(成就感)的事情,如果感觉本文内容对你有帮助的话,麻烦点亮一下👍,这对我很重要哦~


我是 Flywith24人只有通过和别人的讨论,才能知道我们自己的经验是否是真实的,加我微信交流,让我们共同进步。



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

从自由职业到小公司再到港企再到国企,辛酸2022

🍊作者简介: 18年毕业,至今在创业公司,国企,港企(也算是外企),中间有做过9个月的自由职业,但是失败返回职场,现在专心研究技术,目标一线大厂。 🍊支持作者: 点赞👍、关注💖、留言💌~ 今天是工作最后一天,认真撰写一下我的年终总结,以及今年我的所思所想。 ...
继续阅读 »

🍊作者简介: 18年毕业,至今在创业公司,国企,港企(也算是外企),中间有做过9个月的自由职业,但是失败返回职场,现在专心研究技术,目标一线大厂。


🍊支持作者: 点赞👍、关注💖、留言💌~



今天是工作最后一天,认真撰写一下我的年终总结,以及今年我的所思所想。


自由职业总结


在此之前我先捋一下时间线,我是我从2021年6月份决定出来自由职业,起初跟朋友一起合作做点小生意,后来因为一些意见不合,从2022年2月份就退出了,2月份到5月份也在做很多尝试,比如做剧本杀写作,自媒体卖书,接私单,但是经过考虑,做这些收入不稳定,而且回本周期比较长,有回款压力,而且之前创业的9个月里面,我也没赚到什么钱,倒是把自己的老本都吃光了,还透支了xy卡,还有一些wd,每个月都有还款压力。


所以在这里奉劝各位想做自由职业的朋友,如果不想打工,想出来自己创业,要三思啊,要起码满足以下几个条件:



1、有稳定发展的副业,而且副业的收入连续三个月以上超过主业收入

2、副业因为主业的影响而发展受限 3、自身有起码一年以上的周转资金,起码保证哪怕一年没收入也不至于饿死



而我很明显以上三点都不满足,到了后面实在没啥钱,业务也没有做起来,就动用了网贷,不过幸好及时止损,回归职场,现在细想,这是非常危险的,也是非常不理智的。


有条件千万不要dk创业,不要负z创业,到了后面心态真的绷不住,压力太大了,人很焦虑不说,事情还总办不好。



后来回来上班,第二个月领到第一笔工资,有时候摸鱼一天都有钱进账,多爽啊哈哈。


当然此次创业也给了一个教训就是尽量不要合伙创业,做成了还好,做不成就连朋友都真做不了,一地鸡毛,有机会好好讲一下这一年我自由职业的个人心得。


自由职业告一段落,现在进入职场时间。


回归职场


2022年6月到2022年12月这段时间也是比较动荡的。


不过也在意料之内,突然从自由职业回归到职场,还是会有阵痛期。


2022年6月-2022年9月,在一家小创业公司做前端负责人,薪资16k(我直接喊得,老板很爽快地答应了,怀疑是叫少了)。



这家公司技术栈是Vue2.x,业务有PC端应用,小程序应用,还有flutter开发桌面端。



但是因为技术生疏和对于业务方面不够娴熟,达不到老板的要求,9月8日被辞退了。


但是我没有气馁,心想要不再尝试一下自由职业吧,于是又花了14天时间去写剧本杀,想着靠剧本杀来翻盘,但是我的稿子被编剧无情打回来修改后,看着日进逼近的还款日期,还有自己手上不多的余粮,妈呀,立马又屁颠屁颠去准备面试,宝宝心里苦啊。



于是又火急火燎地边准备面试题边去面试,好在上天眷顾,10月22日入职了一家港企,也算是外企吧,薪资是16k,但是加班费奇高,就是我之前说的100元/小时。


因为公司是千人以上的大公司,所以业务线非常庞杂,技术栈也非常繁杂:



Vue3.0开发表单引擎 

React Native开发业务汇报APP 

Vue2.x+Electron开发桌面端应用 

Angular 

......



真可谓是前端大杂烩,不过眼下要还钱,虽然没有争取到涨薪,但起码有加班费,还好还好,再看一眼我的存款还有还款日期,没办法,就你了。


于是开始了疯狂卷模式,我在这家公司也是出了名的卷,以至于我现在离职快一个月了,公司还留存着我的光辉事迹......


为什么我又双叒离职了呢?



原因是我进去没多久,就由自愿加班转变成强制加班了,强制加班到9点半。


不过为了还钱,这点也可以接受吧。


不过最可怕的是,他们会给超出你工作能力的工作量,而且狂砍开发周期,比如我用react native从零开发一个系统,我原本估计要20天时间(保守一点),但是上层直接砍半,直接给10天!!


我艹,从入门到项目上线只给10天,没得办法,谁让我还在试用期,也不敢造次。


于是就开始跟另一个小伙伴开始摸天黑地的开发工作,连续10天都是凌晨1点才到家,第二天8点还得起床去上班。


然而10天根本不可能完整完成一个系统,我们连react native的基本知识都没搞懂,但是另外一个小伙伴说,尽力而为,实在不行就跑路。


听他这么说,我表面不说什么,内心那叫一个苦啊。


原来一个人有了负债就不再是你自己了,失去了那么多选择权,幸好这点负债对我来说压力不算太大,真想不懂那些有房贷车贷的人是怎么想的,那压力真的翻倍啊。


以后买房真的要慎之又慎!!



10天之后,我们两个人拼尽全力了,都还是没有办法按时上线,于是领导又给多了一周时间开发,并且放出狠话:



这一次要是再延期上线,就有人要被释放了!!



哎,没办法,再难受也要顶硬上。但是我转念一想,要是实在没办法完成,要拿我开刀,那怎么办??


不行,我不能做砧板上的鱼肉,我要选好退路,那就是继续去面试找下家,即使没办法上线他们要开掉我,我有offer在身,我也不需要担心那么多。


于是我从12月10日开始,屏蔽掉了现公司,开始了BOSS上海投之旅。


我当时是这么打算的,下一家公司要满足以下条件:



1、薪资必须要能够覆盖掉我的日常开支+还贷,还能存下一点钱抵抗后续风险 

2、至少稳定工作一年以上 

3、正常上下班,或者加班不多,多出来时间提升技术(技术真的跟不上了)



综上只有两种公司满足我的条件:



1、国企 

2、外企



有点讽刺,在大陆,最遵守劳动法的公司反而是外企。


但是面试我是不管那么多的,外面行情也不是那么好,但是幸运的是我比较注重简历包装,以及对于简历上可能问道的项目技术难点或者重点,甚至可能延伸出去的技术点,我都有做好非常严谨的准备,谁让我一路以来都在准备面试(其实是工作不稳定),所以还是很幸运在一周之内拿了不少offer,除了大厂(估计大厂嫌弃我简历太花了,没让我过,也可能是太菜了)


大厂,等我这波缓过来,一年以后或者两年以后我还是会冲的。


后来一周开发结束之后,急急忙忙上线,因为时间紧急,所以我们内部测试流程基本跑通就匆匆上线了,上线的当天测试测出60多个bug!!



企业微信被测试疯狂轰炸,我的另一个伙伴前几天跑路了,就只剩我一个人,有点难顶,于是领导又给我安排了另一个前端来帮忙,正好,等我把tapd上面的bug全部修复,二次测试通过之后,就甩锅给新来的前端,留下一纸技术交接文档还有离职申请,拍拍屁股去下家公司入职了,也算是对得起前公司了吧。



说实话,不是我扛不住压力,而是我真的不喜欢领导总是以释放,开除等等来给我们施压,我不是牛马,我也是人,是人就应该得到尊重!


万一我下次项目真的没办法上线,就把我开了,那我的处境就会非常被动了。


介绍一下我的新公司,大型的国企,流程正规,即使项目需求再赶也不至于把人给逼进绝路,正常上下班,大家都是到点走,有一次我稍微坐久一点,技术经理还过来提醒我没事可以早点走,劳逸结合,真正的人性化哈哈。


薪资也提高了一点,加班也是1:1,而且加班机会非常少,那多出来的时间,我可以有条不紊地提升技术。


一切都朝着好的方向发展,而且会越来越好。


说了那么多2022年,下面是我对于2023年的新年期望。


2023年新年期望


第一,当然是早日还清债务,现在的钱还不是我的,等还清贷款后,才是属于我的,起码现在我是这么认为的;


第二,从零开始重新钻研技术,这段时间也在根据自己的定位重新制定职业规划,后续会公布到这里;


经历过这次自由职业,我深刻地意识到,要想做成事,能力,经验,人脉,资本,缺一不可,而这些资源,都集中在大厂,只有去大厂,才可以完成自己的各项积累,即使进去之后,也不可以躺平,得过且过,要自己牢牢把握主动权。


所以2023年所做的一切都是为了进大厂做储备;


第三,当然是希望收获一段有结果的感情啦,虽然不知道是不是你,但是我还是会用心去经营,不辜负任何一个人,毕竟你有一点很吸引我,就是你身上闪烁着女性独立之光;


第四,就是把自己的技术沉淀到公众号,视频号,小红书,做自媒体输出,要是能够做成像月哥,神光,卡颂这种小网红就更好了哈哈,当然做这些注定前期是不赚钱的,降低期望值,逐步提升个人影响力,赚以后的钱吧。


而且我这个人天生脸皮厚,有旺盛的表达欲,又充满了乐观主义色彩,尽管去做吧,做技术输出,这绝对是稳赚不赔的买卖。


祝大家新年快快乐,万事如意,早日实现自己的人生目标!


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

有多少人忘记了gb2312

本想新周摸新鱼,却是早早入坑。看到群友千元求解一个叫当当网的索引瞬间来了兴趣 网站地址,大体一看没什么特别的地方就是一个关键字编码问题,打眼一看url编码没跑直接拿去解码无果 -有点惊讶看似url编码实则url编码只是这,滋滋滋... 有点东西,开始...
继续阅读 »

本想新周摸新鱼,却是早早入坑。看到群友千元求解一个叫当当网的索引瞬间来了兴趣



  1. 网站地址,大体一看没什么特别的地方就是一个关键字编码问题,打眼一看url编码没跑直接拿去解码无果


image.png


image.png
-有点惊讶看似url编码实则url编码只是这,滋滋滋...
VeryCapture_20230227174156.gif


有点东西,开始抓包,断点,追踪的逆向之路
VeryCapture_20230227170913.gif
2. 发现是ajax加载(不简单呀纯纯的吊胃口)先来一波关键字索引(keyword)等一系列基操轻而易举的找到了他
VeryCapture_20230227171325.gif


从此开始走向了一条不归路,经过一上午的时间啥也没追到,午休之后继续战斗,经过了一两个半小时+三支长白山牌香烟的努力终于


VeryCapture_20230227173627.gif
VeryCapture_20230227173457.gif


VeryCapture_20230227171715.gif


cihui = '哈哈哈'
js = open("./RSAAA.js", "r", encoding="gbk", errors='ignore')
line = js.readline()
htmlstr = ''
while line:
htmlstr = htmlstr + line
line = js.readline()
ctx = execjs.compile(htmlstr)
result = ctx.call('invokeServer', cihui)
print(result)

const jsdom = require("jsdom");
const {JSDOM} = jsdom;
const dom = new JSDOM('<head>\n' +
' <base href="//search.dangdang.com/Standard/Search/Extend/hosts/">\n' +
'<link rel="dns-prefetch" href="//search.dangdang.com">\n' +
'<link rel="dns-prefetch" href="//img4.ddimg.cn">\n' +
'<title>王子-当当网</title>\n' +
'<meta http-equiv="Content-Type" content="text/html; charset=GB2312">\n' +
'<meta name="description" content="当当网在线销售王子等商品,并为您购买王子等商品提供品牌、价格、图片、评论、促销等选购信息">\n' +
'<meta name="keywords" content="王子">\n' +
'<meta name="ddclick_ab" content="ver:429">\n' +
'<meta name="ddclick_search" content="key:王子|cat:|session_id:0b69f35cb6b9ca3e7dee9e1e9855ff7d|ab_ver:G|qinfo:119800_1_60|pinfo:_1_60">\n' +
'<link rel="canonical" href="//search.dangdang.com/?key=%CD%F5%D7%D3\&amp;act=input">\n' +
' <link rel="stylesheet" type="text/css" href="css/theme_1.css">\n' +
' <!--<link rel="Stylesheet" type="text/css" href="css/model/home.css" />-->\n' +
' <link rel="stylesheet" type="text/css" href="css/model/search_pub.css?20211117"> \n' +
'<style>.shop_button {height: 0px;}.children_bg01 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 630px;\n' +
'}\n' +
'.children_bg02 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 660px;\n' +
'}\n' +
'.children_bg03 a {\n' +
'margin-left: 0px;\n' +
'padding-left: 304px;\n' +
'width: 660px;\n' +
'}\n' +
'.narrow_page .children_bg01 a{\n' +
'width: 450px;\n' +
'}\n' +
'.narrow_page .children_bg02 a{\n' +
'width: 450px;\n' +
'}\n' +
'.narrow_page .children_bg03 a{\n' +
'width: 450px;\n' +
'}.price .search_e_price span {font-size: 12px;font-family: 微软雅黑;display: inline-block;background-color: #739cde;color: white;padding: 2px 3px;line-height: 12px;border-radius: 2px;margin: 0 4px 0 5px;}\n' +
'.price .search_e_price:hover {text-decoration: none;}</style> <link rel="stylesheet" href="http://product.dangdang.com/js/lib/layer/3.0.3/skin/default/layer.css?v=3.0.3.3303" id="layuicss-skinlayercss"><script id="temp_script" type="text/javascript" src="//schprompt.dangdang.com/suggest_new.php?keyword=好好&amp;pid=20230227105316030114015279129895799&amp;hw=1&amp;hwps=12&amp;catalog=&amp;guanid=&amp;0.918631418357919"></script><script id="json_script" type="text/javascript" src="//static.dangdang.com/js/header2012/categorydata_new.js?20211105"></script></head>');

window = dom.window;
document = window.document;
function invokeServer(url) {

var scriptOld = document.getElementById('temp_script');
if(scriptOld!=null && document.all)
{
scriptOld.src = url;
return script;
}
var head=document.documentElement.firstChild,script=document.createElement('script');
script.id='temp_script';
script.type = 'text/javascript';
script.src = url;
if(scriptOld!=null)
head.replaceChild(script,scriptOld);
else
head.appendChild(script);
return script
}



  1. 完事!当我以为都要结束了的时候恍惚直接看到了源码中的gb2312突然想起了之前做的一个萍乡房产网的网站有过类似经历赶快去尝试结果我**
    image.png
    image.png
    VeryCapture_20230227172815.gif




  2. 总结:提醒各位大佬在逆向之路中还是要先从基操开始,没必要一味的去搞攻克扒源码,当然还是要掌握相对全面的内容,其实除了个别大厂有些用些贵的东西据说某数5要20个W随着普遍某数不知道那些用了20w某数的大厂心里是什么感觉或许并不在乎这点零头哈哈毕竟是大厂,小网站的反扒手段并不是很难,俗话说条条大道通北京。


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

Android事件冲突解决-悬浮窗拖拽处理

需求场景 最近项目中要做一个音乐播放悬浮按钮的功能,最终实现效果如下: 问题暴露 悬浮窗布局文件就不放了,就是水平LinearLayout里面放几个ImageView。 做的过程当中遇到一个问题,就是悬浮窗是可以任意拖拽的,悬浮窗里面的按钮是可以点击的,比如...
继续阅读 »

需求场景


最近项目中要做一个音乐播放悬浮按钮的功能,最终实现效果如下:



问题暴露


悬浮窗布局文件就不放了,就是水平LinearLayout里面放几个ImageView


做的过程当中遇到一个问题,就是悬浮窗是可以任意拖拽的,悬浮窗里面的按钮是可以点击的,比如暂停,下一曲,关闭悬浮窗等。


按常规思路,先给整个悬浮窗setOnTouchListener(),然后再给你里面的按钮setOnClickListener(),点击运行,结果发现,点击事件是可以响应,拖拽也没问题,但是当手指放在ImageView上拖拽时,onTouchListener事件无法响应。


此时第一感觉就是setOnTouchListener()setOnClickListener()冲突了,需要解决一下冲突。无奈自己对Android事件分发消费机制一直都是一知半解的,一般都是出了问题需要解决,第一时间先百度,没有解决方案就只能去研究Android事件分发消费机制了,但是研究完也都是懵懵懂懂的,今天就决定把这个难点彻底消化掉。


主要研究了这篇文章Android事件分发消费机制,然后对照着写了个demo,一一去验证,加深了自己的理解,最后终于解决了我的问题。


解决思路


先说下解决思路,自定义LinearLayout,当手指处于滑动时,直接拦截事件,交给自己的onTouchEvent处理即可,核心代码如下:


/**
* @author:Jason
* @date:2021/8/24 19:49
* @email:1129847330@qq.com
* @description:可拖拽的LinearLayout,解决子View设置OnClickListener之后无法拖拽的问题
*/
class DraggerbleLinearLayout : LinearLayout {
constructor(context: Context, attr: AttributeSet) : this(context, attr, 0)
constructor(context: Context, attr: AttributeSet, defStyleAttr: Int) : this(context, attr, defStyleAttr, 0)
constructor(context: Context, attr: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attr, defStyleAttr, defStyleRes)

override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
when (ev?.action) {
MotionEvent.ACTION_MOVE -> {
return true
}
}
return super.onInterceptTouchEvent(ev)
}
}

很简单,就是在onInterceptTouchEvent()里面拦截move事件即可,这里你也可以重写onTouchEvent(),在里面实现拖拽功能,但是这样就固定死了,所以我选择在外面setOnTouchListener(),需要拖拽功能时才去实现


    /**
* 创建悬浮窗
*/
@SuppressLint("ClickableViewAccessibility")
private fun showWindow() {
//获取WindowManager
windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
val outMetrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(outMetrics)
var layoutParam = WindowManager.LayoutParams().apply {
/**
* 设置type 这里进行了兼容
*/
type = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
} else {
WindowManager.LayoutParams.TYPE_PHONE
}
format = PixelFormat.RGBA_8888
flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
//位置大小设置
width = WRAP_CONTENT
height = WRAP_CONTENT
gravity = Gravity.LEFT or Gravity.TOP
//设置剧中屏幕显示
x = outMetrics.widthPixels / 2 - width / 2
y = outMetrics.heightPixels / 2 - height / 2
}
//在这里设置触摸监听,以实现拖拽功能
floatRootView?.setOnTouchListener(ItemViewTouchListener(layoutParam, windowManager))
// 将悬浮窗控件添加到WindowManager
windowManager.addView(floatRootView, layoutParam)
isAdded = true
}

主要是这行代码


//在这里设置触摸监听,以实现拖拽功能
floatRootView?.setOnTouchListener(ItemViewTouchListener(layoutParam, windowManager))

ItemViewTouchListener.kt文件内容


/**
* @author:Jason
* @date: 2021/8/23 19:27
* @email:1129847330@qq.com
* @description:
*/
class ItemViewTouchListener(val layoutParams: WindowManager.LayoutParams, val windowManager: WindowManager) : View.OnTouchListener {
private var lastX = 0.0f
private var lastY = 0.0f
override fun onTouch(view: View, event: MotionEvent): Boolean {
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//这里接收不到Down事件,不要在这里写逻辑
}
MotionEvent.ACTION_MOVE -> {
//重写LinearLayout的OnInterceptTouchEvent之后,这里的Down事件会接收不到,所以初始位置需要在Move事件里赋值
if (lastX == 0.0f || lastY == 0.0f) {
lastX = event.rawX
lastY = event.rawY
}
val nowX: Float = event.rawX
val nowY: Float = event.rawY
val movedX: Float = nowX - lastX
val movedY: Float = nowY - lastY

layoutParams.apply {
x += movedX.toInt()
y += movedY.toInt()
}
//更新悬浮球控件位置
windowManager?.updateViewLayout(view, layoutParams)
lastX = nowX
lastY = nowY
}
MotionEvent.ACTION_UP -> {
lastX = 0.0f
lastY = 0.0f
}
}
return true
}
}

这里有一点需要注意的是,重写了LinearLayoutonInterceptTouchEvent()后会导致setOnTouchListener()里面的ACTION_DOWN事件接收不到,所以不要在down事件里面写逻辑。然后onTouch一定要返回true,表示要消费事件,否则当拖拽非ImageView区域时会拖不动。


好了,花了一整天,就解决了这个小问题。


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

NativeBridge:我在Pub上发布的第一个插件

前沿 坦白地说,在发布 native_bridge 之前,自己从没有想过会在 pub 上发布一款自己开发的 Flutter 插件。一方面是觉得自己水平有限,很难产出比较有实用价值的内容。虽然之前有写过 Plugin 开发相关的文章,但那也只是用来自我学习的 D...
继续阅读 »

前沿


坦白地说,在发布 native_bridge 之前,自己从没有想过会在 pub 上发布一款自己开发的 Flutter 插件。一方面是觉得自己水平有限,很难产出比较有实用价值的内容。虽然之前有写过 Plugin 开发相关的文章,但那也只是用来自我学习的 Demo,对大家来说没有太多的应用价值。另一方面也是因为对 pub 的发布流程非常陌生,尤其对大陆开发者来说还会遇到一些额外的问题。


而这次能够促使我去发布 NativeBridge 插件,主要还是之前的 NativeBridge 系列文章在掘金的受欢迎度较高,让我感受到了它存在的价值。另一个原因是之前对掘友的承诺,之前掘友希望 NativeBridge 能够适配 webview_flutter ^4.0.1 以上的版本。


WX20230226-151759.png


虽然距离上次的回复已经过去了一个多月,但我最终还是完成了许下的承诺^ ^。


NativeBridge 插件


NativeBridge 是基于 webview_flutter 插件来实现 App端 和 H5端 JS 互调能力的插件。 其详细功能说明请跳转 Readme 查看,这里不做过多的介绍。


NativeBridge 目前已成功发布到 pub,需要使用时直接引入即可。


WX20230226-152710.png


版本更新


这里简述下插件的版本更新内容:


v0.0.2 版本


1. 优化了消息回复后还会导致接收端再次发一条确认消息的问题


原来当 App 收到了 H5 回复的消息后,还会再发送一条消息确认消息。这是没有必要的,这里进行了优化。


通过在消息实体 Message 中新增 isResponseFlag 字段来进行区分,对于 isResponseFlagtrue 的消息,将不再发送确认消息。


if (isResponseFlag) {
// 是返回的请求消息,则处理H5回调的值
NativeBridgeHelper.receiveMessage(messageJson);
} else {
...
}

2. 对于未识别的 Api 新增容错机制处理


随着 App版本 的迭代,可能出现某个新的 Api 老的版本无法支持的情况,对于之前的版本来说,当 Api 不匹配时会直接丢弃消息,等待超时机制处理。这会导致发起端出现 await 情况,直到超时触发回调。这是不适宜的,这次增加了兼容处理。


// 不是返回的请求消息,处理H5端的请求
var callMethod = callMethodMap[messageItem.api];
if (callMethod != null) {
// 有相应的JS方法,则处理
...
} else {
// 若没有对应方法,则返回null,避免低版本未支持Api阻塞H5
messageItem.data = null;
}

v1.0.0版本


1. 适配webview_flutter ^4.0.5版本


目前插件引入的 webview_flutter 版本为: ^4.0.5,后续也会随着 webview_flutter 插件的迭代而升级,直到其能力被官方取代为止。


WX20230226-155038.png


2. 对实现方式进行重构


由于新版本的 webview_flutter 将大部分的 WebView 操作功能移入 WebViewController 处理,因此我们的 NativeBridge 插件也同步进行调整。


现在使用只需要新增 AppBridgeController 继承 NativeBridgeController 实现其对应方法即可正常使用。


class AppBridgeController extends NativeBridgeController {
AppBridgeController(WebViewController controller) : super(controller);

/// 指定JSChannel名称
@override
get name => "nativeBridge";

@override
Map<String, Function?> get callMethodMap => <String, Function?>{
// 版本号
"getVersionCode": (data) async {
return await AppUtil.getVersion();
},
...
};
}

// 初始化AppBridgeController
_appBridgeController = AppBridgeController(_controller);

总结


对于还在使用 webview_flutter 3.0+ 版本的项目,引用 ^0.0.2版本:


native_bridge: ^0.0.2

对于使用 webview_flutter 4.0+ 版本的项目,引用 ^1.0.0版本:


native_bridge: ^1.0.0

感悟


NativeBridge插件的成功发布,这个过程中也让我受益良多。


开始学会从 SDK 开发者的角度去思考问题,尝试思考如何简化插件的使用、丰富插件的能力,更关注插件的内在结构和对外的协议。也了解到在开源项目中包括开源协议、pub 的 scores 标准、README、CHANGELOG的定义等。


这也是自我的一次成长,愿你我一起共勉!


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

Android通知栏适配调研-放弃RemoteViews

问题描述: 项目的通知栏的RemoteViews背景图有些厂商颜色不对,需要一个适配方案,来适配所有机型。 项目现状: 目前的方案是,创建一个新的通知,通过这个通知来获取底色。 其中有一个字段是获取是否是暗黑模式,这段代码是先用按位或忽略掉透明度,系统的字体...
继续阅读 »

问题描述:


项目的通知栏的RemoteViews背景图有些厂商颜色不对,需要一个适配方案,来适配所有机型。


项目现状:


目前的方案是,创建一个新的通知,通过这个通知来获取底色。


image.png


其中有一个字段是获取是否是暗黑模式,这段代码是先用按位或忽略掉透明度,系统的字体颜色不会有特别高的同名度,所以这里理论上没什么问题,然后计算两个三维向量的距离,距离越近,颜色越接近。


image.png


这里是有误差的,目标向量距离灰色向量rgb(127.5,127.5,127.5)的最大值约为220,而不是180,就会造成纯白色rgb(255,2555,255)的标题字体颜色,理应是黑色背景才能看到,但却被识别成非暗黑模式。


image.png


误判了暗黑模式,这里的背景就颜色就会出问题了。


image.png


目前项目的解决方案是通过设置RemoteViews的background颜色去做,这里需要适配的问题由RemoteViews实现机制产生:RemoteViews被设计具有跨进城通信的属性,通过Binder传递到SystemService进程,根据它的包名来拿相应的应用的资源,为了提高效率,系统没有直接通过Binder去支持所有的View和View操作,那这里对普通view生效的xml属性,很有可能厂商对系统修改源码后没有完全的适配,可能就不能对其全部生效。


比如对android:background属性进行设置在部分机型上就不会生效,但下面这段代码可以。


RemoteView?.setInt(R.id.ll_root, "setBackgroundColor", ContextCompat.getColor(context, R.color.color_367AF6))

接下来来看项目如何设置颜色。


image.png


然后来看拿到暗黑模式的布尔字段后,会设置为#FFFFFFFF/#00000000这两个颜色,这两个颜色都是有问题的,首先暗黑模式情况下,#00000000(百分百透明的黑色)不一定会正确解析为透明色,类似问题为启动icon解析时,透明色解析为黑色背景,具体还要观察各厂商系统对源码的修改。


image.png


#FFFFFFFF这个颜色也是不对的,不是每个通知栏的背景都是白色,而且这个颜色也不一定会作为背景色,下面鸿蒙系统上就没有生效为白色。


image.png


来观察下不同时期的通知样式,可以发现不是每个时期都是固定颜色底色,有白有黑有灰,那就要动态的获取背景色来设置RemoteVies的背景色,下图以安卓1-7为示例。


image.png


接着还要考虑不同的系统版本对Notification的改动,如安卓8.0以上加入了Channel这一特性,项目的通知最后是要有一步渠道检查的。这里或许要额外对安卓13的有权限上的适配。


image.png


同时如果要考虑通知栏高度的问题的话,以安卓12为例,自定义通知的显示区域比安卓11有调整,折叠态高度上限为64dp,展开时高度上限为256dp,且系统强制显示通知的小图标。所以要考虑到ui层面对高度是否有要求,再来适配各个版本的RemoteViews的高度。下图为展开时示例。


image.png


以上,这个方案做不到适合所有机型,要适配所有机型确实是不是个简单的事情。


尝试过程:


核心问题为RemoteViews的背景色不能完美适配所有厂商。首先原安卓系统(非厂商自定义)不需要对其有特定的设置,自动跟随主题颜色;厂商自定义安卓系统,则各不相同,如小米则不会自动更改RemoteViews的颜色。


首先尝试获取背景色,我开始的尝试方式是沿用之前的方式,即build一个Notification,通过遍历其中的text,发现不是所有的都能拿到,参考其他文章列出的类似的表格如下:



虎牙项目目前的方案是,如果没拿到字体,默认就是字体为黑色,这个方法安卓7.0之后就失效了。安卓7.0+修改了Notification,采用 @android:color/primary_text_dark已经获取不到颜色值了。


那既然不能完美解决,这里我使用反编译工具,观察各厂商的逆向工程(以小米为例),这里需要花费大量的时间来阅读源码(前公司的代码真的看不懂= =),最后看到其他文章给出一个方案是在manifest加上这一句才会生效。


<item name="android:forceDarkAllowed" tools:targetApi="q">false</item>
复制代码

以上,RemoteViews的确拥有高自定义ui的特性,但很难找到一个方案来适配所有国内厂商的机型,因为Google设计通知栏,或者说其他任何的系统组件的伊始就没考虑过自定以rom的因素。



解决方案:


1.  不使用RemoteViews,改用其他方案解决


2.  使用RemoteViews


先说第一个,如果仅仅是虎牙目前的通知栏用途的话,确实没有使用复杂ui,使用RemoteViews的收益好像不足以掩盖bug多这个缺点。直播间的通知栏提醒完全可以用addAction方法来代替:


image.png


其他简单场景的情况下:



第二个,如果坚持使用RemoteViews,基本上网络上大部分的相关的文章我都翻过了,我也没看到业界更好的办法,大概只能反编译去看看抖音和微信是怎么做的吧。


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

如何开发一个人人爱的组件?

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

组件,是前端最常打交道的东西,对于 React、Vue 等应用来说,万物皆组件毫不为过。


有些工作经验的同学都知道,组件其实也分等级的,有的组件可以被上万开发者复用,有些组件就只能在项目中运行,甚至挪动到自己的另外一个项目都不行。


如何考察一个前端的水平,首先可以看看他有没有对团队提供过可复用的组件,一个前端如果一直只能用自己写的东西,或者从没有对外提供过可复用的技术,那么他对于一个团队的贡献一定是有限的。


所以开始写一个能开放的组件应该考虑些什么呢?🤔


本篇文章类似一个菜谱,比较零碎的记录一些组件设计的内容,我分别按照 1~5 ⭐️ 区分其重要性。


意识


首先在意识层面,我们需要站在使用组件的开发者角度来观察这个组件,所以下面几点需要在组件开发过程中种在意识里面:



  1. 我应该注重 TypeScript API 定义,好用的组件 API 都应该看上去 理所应当绝不多余

  2. 我应该注重 README 和 Mock ,一个没有文档的组件 = 没有,最好不要使用 link 模式去开发组件

  3. 我不应引入任何副作用依赖,比如全局状态(Vuex、Redux),除非他们能自我收敛

  4. 我在开发一个开放组件,以后很有可能会有人来看我的代码,我得写好点


接口设计


好的 Interface 是开发者最快能搞清楚组件入参的途径,也是让你后续开发能够有更好代码提示的前提。


type Size = any; // 😖 ❌

type Size = string; // 🤷🏻‍♀️

type Size = "small" | "medium" | "large"; // ✅

`


DOM 属性(⭐️⭐️⭐️⭐️⭐️)

组件最终需要变成页面 DOM,所以如果你的组件不是那种一次性的,请默认顺手定义基础的 DOM 属性类型。className 可以使用 classnames 或者 clsx 处理,别再用手动方式处理 className 啦!


export interface IProps {
className?: string;
style?: React.CSSProperties;
}

对于内部业务来说,还会有 data-spm 这类 dom 属性,主要用于埋点上报内容,所以可以直接对你的 Props 类型做一个基础封装:


export type CommonDomProps = {
className?: string;
style?: React.CSSProperties;
} & Record<`data-${string}`, string>

// component.tsx
export interface IComponentProps extends CommonDomProps {
...
}

// or

export type IComponentProps = CommonDomProps & {
...
}

类型注释(⭐️⭐️⭐️)


  1. export 组件 props 类型定义

  2. 为组件暴露的类型添加 规范的注释


export type IListProps{
/**
* Custom suffix element.
* Used to append element after list
*/

suffix?: React.ReactNode;
/**
* List column definition.
* This makes List acts like a Table, header depends on this property
* @default []
*/

columns?: IColumn[];
/**
* List dataSource.
* Used with renderRow
* @default []
*/

dataSource?: Array<Record<string, any>>;
}

上面的类型注释就是一个规范的类型注释,清晰的类型注释可以让消费者,直接点击进入你的类型定义中查看到关于这个参数的清晰解释。


同时这类符合 jsdoc 规范的类型注释,也是一个标准的社区规范。利用 vitdoc 这类组件 DEMO 生成工具也可以帮你快速生成美观的 API 说明文档。



Tips:如果你非常厌倦写这些注释,不如试试著名的 AI 代码插件:Copilot ,它可以帮你快速生成你想要表达的文字。



以下是 ❌ 错误示范:


  toolbar?: React.ReactNode; // List toolbar.

// 👇🏻 Columns
// defaultValue is "[]"
columns?: IColumns[];

组件插槽(⭐️⭐️⭐️)

对于一个组件开发新手来说,往往会犯 string 类型替代 ReactNode 的错误。


比如要对一个 Input 组件定义一个 label 的 props ,许多新手开发者会使用 string 作为 label 类型,但这是错误的。


export type IInputProps = {
label?: string; // ❌
};

export type IInputProps = {
label?: React.ReactNode; // ✅
};

遇到这种类型时,需要意识到我们其实是在提供一个 React 插槽类型,如果在组件消费中仅仅是让他展示出来,而不做其他处理的话,就应当使用 ReactNode 类型作为类型定义。



受控 与 非受控(⭐️⭐️⭐️⭐️⭐️)

如果要封装的组件类型是 数据输入 的用途,也就是存在双向绑定的组件。请务必提供以下类型定义:


export type IFormPropsstring> = {
value?: T;
defaultValue?: T;
onChange?: (value: T, ...args) => void;
};

并且,这类接口定义不一定是针对 value, 其实对于所有有 受控需求 的组件都需要,比如:


export type IVisibleProps = {
/**
* The visible state of the component.
* If you want to control the visible state of the component, you can use this property.
* @default false
*/

visible?: boolean;
/**
* The default visible state of the component.
* If you want to set the default visible state of the component, you can use this property.
* The component will be controlled by the visible property if it is set.
* @default false
*/

defaultVisible?: boolean;
/**
* Callback when the visible state of the component changes.
*/

onVisibleChange?: (visible: boolean, ...args) => void;
};

具体原因请查看: 《受控组件和非受控组件》
消费方式推荐使用:ahooks useControllableValue


表单类常用属性(⭐️⭐️⭐️⭐️)

如果你正在封装一个表单类型的组件,未来可能会配合 antd/ fusionForm 组件来消费,以下这些类型定义你可能会需要到:


export type IFormProps = {
/**
* Field name
*/

name?: string;

/**
* Field label
*/

label?: ReactNode;

/**
* The status of the field
*/

state?: "loading" | "success" | "error" | "warning";

/**
* Whether the field is disabled
* @default false
*/

disabled?: boolean;

/**
* Size of the field
*/

size?: "small" | "medium" | "large";

/**
* The min value of the field
*/

min?: number;

/**
* The max value of the field
*/

max?: number;
};

选择类型(⭐️⭐️⭐️⭐️)

如果你正在开发一个需要选择的组件,可能以下类型你会用到:


export interface ISelectionextends object = Record<string, any>> {
/**
* The mode of selection
* @default 'multiple'
*/

mode?: "single" | "multiple";
/**
* The selected keys
*/

selectedRowKeys?: string[];
/**
* The default selected keys
*/

defaultSelectedRowKeys?: string[];
/**
* Max count of selected keys
*/

maxSelection?: number;
/**
* Whether take a snapshot of the selected records
* If true, the selected records will be stored in the state
*/

keepSelected?: boolean;
/**
* You can get the selected records by this function
*/

getProps?: (
record: T,
index: number
) =>
{ disabled?: boolean; [key: string]: any };
/**
* The callback when the selected keys changed
*/

onChange?: (
selectedRowKeys: string[],
records?: Array,
...args: any[]
) =>
void;
/**
* The callback when the selected records changed
* The difference between `onChange` is that this function will return the single record
*/

onSelect?: (
selected: boolean,
record: T,
records: Array,
...args: any[]
) =>
void;
/**
* The callback when the selected all records
*/

onSelectAll?: (
selected: boolean,
keys: string[],
records: Array,
...args: any[]
) =>
void;
}

另外,单选与多选存在时,组件的 value 可能会需要根据下传的 mode 自动变化数据类型。


比如,在 Select 组件中就会有以下区别:


mode="single" -> value: string | number
mode="multiple" -> value: string[] | number[]

所以对于需要 多选、单选 的组件来说,value 的类型定义会有更多区别。


对于这类场景可以使用 Merlion UI - useCheckControllableValue 进行抹平。


组件设计


服务请求(⭐️⭐️⭐️⭐️⭐️)

这是一个在业务组件设计中经常会遇到的组件设计,对于很多场景来说,或许我们只是需要替换一下请求的 url ,于是便有了类似下面这样的 API 设计:


export type IAsyncProps {
requestUrl?: string;
extParams?: any;
}

后面接入方增多后,出现了后端的 API 结果不符合组件解析逻辑,或者出现了需要请求多个 API 组合后才能得到组件所需的数据,于是一个简单的请求就出现了以下这些参数:


export type IAsyncProps {
requestUrl?: string;
extParams?: any;
beforeUpload?: (res: any) => any
format?: (res: any) => any
}

这还只是其中一个请求,如果你的业务组件需要 2 个、3 个呢?组件的 API 就会变得越来越多,越来越复杂,这个组件慢慢的也就变得没有易用性 ,也慢慢没有了生气。


对于异步接口的 API 设计最佳实践应该是:提供一个 Promise 方法,并且详细定义其入参、出参类型。


export type ProductList = {
total: number;
list: Array<{
id: string;
name: string;
image: string;
...
}>
}
export type AsyncGetProductList = (
pageInfo: { current: number; pageSize: number },
searchParams: { name: string; id: string; },
) =>
Promise<ProductList>;


export type IComponentProps = {
/**
* The service to get product list
*/

loadProduct?: AsyncGetProductList;
}

通过这样的参数定义后,对外只暴露了 1 个参数,该参数类型为一个 async 的方法。开发者需要下传一个符合上述入参和出参类型定义的函数。


在使用时组件内部并不关心请求是如何发生的,使用什么方式在请求,组件只关系返回的结果是符合类型定义的即可。


这对于使用组件的开发者来说是完全白盒的,可以清晰的看到需要下传什么,以及友好的错误提示等等。


Hooks(⭐️⭐️⭐️⭐️⭐️)

很多时候,或许你不需要组件!


对于很多业务组件来说,很多情况我们只是在原有的组件基础上封装一层浅浅的业务服务特性,比如:



  • Image Uploader:Upload + Upload Service

  • Address Selector: Select + Address Service

  • Brand Selector: Select + Brand Service

  • ...


而对于这种浅浅的胶水组件,实际上组件封装是十分脆弱的。


因为业务会对 UI 有各种调整,对于这种重写成本极低的组件,很容易导致组件的垃圾参数激增。


实际上,对于这类对服务逻辑的状态封装,更好的办法是将其封装为 React Hooks ,比如上传:


export function Page() {
const lzdUploadProps = useLzdUpload({ bu: "seller" });

return <Upload {...lzdUploadProps} />;
}

这样的封装既能保证逻辑的高度可复用性,又能保证 UI 的灵活性。


Consumer(⭐️⭐️⭐️)

对于插槽中需要使用到组件上下文的情况,我们可以考虑使用 Consumer 的设计进行组件入参设计。


比如 Expand 这个组件,就是为了让部分内容在收起时不展示。



对于这种类型的组件,明显容器内的内容需要拿到 isExpand 这个关键属性,从而决定索要渲染的内容,所以我们在组件设计时,可以考虑将其设计成可接受一个回调函数的插槽:


export type IExpandProps = {
children?: (ctx: { isExpand: boolean }) => React.ReactNode;
};

而在消费侧,则可以通过以下方式轻松消费:


export function Page() {
return (
<Expand>
{({ isExpand }) => {
return isExpand ? <Table /> : <AnotherTable />;
}}
Expand>

);
}

文档设计


package.json(⭐️⭐️⭐️⭐️⭐️)

请确保你的 repository是正确的仓库地址,因为这里的配置是很多平台溯源的唯一途径,比如: github.com



请确保 package.json 中存在常见的入口定义,比如 main \ module \ types \ exports,以下是一个标准的 package.json 的示范:


{
"name": "xxx-ui",
"version": "1.0.0",
"description": "Out-of-box UI solution for enterprise applications from B-side.",
"author": "yee.wang@xxx.com",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
},
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/cjs/index.d.ts",
"repository": {
"type": "git",
"url": "git@github.com:yee94/xxx.git"
}
}

README.md(⭐️⭐️⭐️⭐️)

如果你在做一个库,并希望有人来使用它,请至少为你的库提供一段描述,在我们的脚手架模板中已经为你生成了一份模板,并且会在编译过程中自动加入在线 DEMO 地址,但如果可以请至少为它添加一段描述。


这里的办法有很多,如果你实在不知道该如何写,可以找一些知名的开源库来参考,比如 antd \ react \ vue 等。


还有一个办法,或许你可以寻求 ChatGPT 的帮助,屡试不爽😄。


作者:YeeWang
来源:juejin.cn/post/7189158794838933565
收起阅读 »

Vue2 Diff 算法

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

Diff 算法


写在前面


因为之前看面试直播也经常问到 Diff 算法,然后作者本人用 Vue2 比较多,所以打算研究一下 Vue2 的 Diff 算法,其实很早就想学的,但是可能因为感觉 Diff 算法比较深奥,就一直拖着没学,但是最近在准备面试,就想着迟早都要学的,趁这个机会把 Diff 算法搞懂吧 🧐,作者就花了一天的时间研究了一下,可能没有完全理解 Diff 算法的精髓,请各位见谅。



💡 这个其实算作者的学习笔记,而且作者水平有限,改文章仅代表作者个人观点,如果有错误可以评论区指出来,会不断完善;同时本文很长,所以请读者们有耐心的看完,看完后作者相信你们会对 Diff 算法有更深的了解。本人觉得本文比目前网上讲解 Diff 算法的大部分文章要更好,因为本文从问题出发,教会大家如何思考,而不是直接从答案出发,就像读答案一样,这样感觉没什么意思,本文一步一步的引导大家去感受 Diff 算法的精妙,同时最后也做了一下小实验,让大家对 Diff 算法有更加直观的感受 🎉。



为什么要用 Diff 算法


虚拟 DOM


因为 Vue2 底层是用虚拟 DOM 来表示页面结构的,虚拟 DOM其实就是一个对象,如果想知道怎么生成的,其实大概流程就是:



  • 首先解析模板字符串,也就是 .vue 文件

  • 然后转换成 AST 语法树

  • 接着生成 render 函数

  • 最后调用 render 函数,就能生成虚拟 DOM


最小量更新


其实框架为了性能才使用的虚拟 DOM,因为 js 生成 DOM 然后展示页面是很消耗性能的,如果每一次的更新都把整个页面重新生成一遍那体验肯定不好,所以需要找到两个页面中变化的地方,然后只要把变化的地方用 js 更新 (可能是增加、删除或者更新) 一下就行了,也就是最小量更新。
那么怎么实现最小量更新呢?那么就要用 Diff 算法了,那么 Diff 算法对比的到底是什么呢?可能这是刚学 Diff 算法比较容易误解的地方,其实比对的是新旧虚拟 DOM,所以 Diff 算法就是找不同,找到两次虚拟 DOM 的不同之处,然后将不同反应到页面上,这就实现了最小量更新,如果只更新变化的地方那性能肯定更好。


页面更新流程


其实这个比较难解释,作者也就大致说一下,学了 Vue 的都知道这个框架的特点之一就有数据响应式,什么是响应式,也就是数据更新页面也更新,那么页面是怎么知道自己要更新了呢?其实这就是这个框架比较高明的地方了,大致流程如下:



  • 之前也说了会运行 render 函数,那么运行 render 函数的时候会被数据劫持,也就是进入 Object.definePropertyget,那么在这里收集依赖,那么是谁收集依赖呢?是每个组件,每个组件就是一个 Watcher,会记录这个组件内的所有变量 (也就是依赖),当然每个依赖 (Dep) 也会收集自己所在组件的 Watcher;

  • 然后当页面中的数据发生变化,那么就会出发 Object.definePropertyset,在这个函数里面数据就会通知每个 Watcher 更新页面,然后每个页面就会调用更新方法,这个方法就是 patch

  • 接着,就要找到两个页面之间的变化量,那么就要用到 Diff 算法了

  • 最后找到变化量后就可以进行更新页面了



其实是边找边更新的,为了让大家理解容易就将这两个部分分开了



Diff 算法简单介绍


面试问到 Diff 算法是什么,大家肯定会说两句,比如 头头、尾尾、尾头、头尾深度优先遍历(dfs)同层比较 类似这些话语,虽然能说一两句其实也是浅尝辄止。
其实作者看了 CSDN 上发的关于 Diff 算法的文章,就是阅读量很高的文章,作者觉得他也没讲明白,可能他自己没明白,或者自己明白了但是没讲清楚,那么作者会用自己的感悟和大家分享一下。


Diff 算法的前提


为了让大家能够了解清楚,这里先说明一下函数调用流程:



  • patch

  • patchVnode

  • updateChildren


Diff 算法的 前提 这个是很重要的,可能大家会问什么是前提?不就是之前说的那些比较嘛?说的没错但也不对,因为 Diff 算法到达之前说的 头头、尾尾、尾头、头尾 这一步的前提就是两次对比的节点是 相同的,这里的相同不是大家想的完全相同,只是符合某些条件就是相同了,为了简化说明,文章就只考虑一个标签只包含 key标签名(tag),那么之前说的相同就是 key 相同以及 tag 相同,为了证明作者的说法是有点正确的,那么这里也贴上源码:


// https://github.com/vuejs/vue/blob/main/src/core/vdom/patch.ts
// 36行
function sameVnode(a, b) {
return (
a.key === b.key &&
a.asyncFactory === b.asyncFactory &&
((a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)) ||
(isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error)))
)
}

如果怕乱了,下面的可以省略不看也没事不影响整体了解,下面只是为了考虑所有情况才加的一个判断:
那么如果两个虚拟 DOM 不相同其实就不用继续比较了,而且如果相同也不用比较了,这里的相同是真的完全相同,也就是两个虚拟 DOM 的地址是一样的,那么也贴上源码:


function patchVnode(......) {
if (oldVnode === vnode) {
return
}
......
}

到目前为止大家可能会比较乱,现在总结一下:



  • patch 函数里比较的是新老虚拟 DOM 是否是 key 相同以及 tag 相同,如果不相同那么就直接替换,如果相同用 patchVnode


说了这么多,其实作者也就想表达一个观点,就是只有当两次比较的虚拟 DOM 是 相同的 才会考虑 Diff 算法,如果不符合那直接把原来的删除,替换新的 DOM 就行了。


patchVnode 函数


这个函数里的才是真正意义上的 Diff 算法,那么接下来会结合源码向大家介绍一下。



源码中核心代码在 patch.ts 的 638 行至 655 行。



其实,目前介绍 patchVnode 的都是直接对着源码来介绍的,但是大家可能不清楚为啥要这么分类,那么作者在这里就让大家明白为什么这么分类,首先在这里说一个结论:



  • 就是 text 属性和 children 属性不可能同时存在,这个需要大家看模板解析源码部分


那么在对比新旧节点的情况下,主要比较的就是是否存在 textchildren 的情况,那么会有如下九种情况


情况老节点 text老节点 children新节点 text新节点 children
1
2
3
4
5
6
7
8
9

按照上面的表格,因为如果新节点有文本节点,其实老节点不管是什么都会被替换掉,那么就可以按照 新节点 text 是否存在来分类,其实 Vue 源码也是这么来分类的:


if (isUndef(vnode.text)) {
// 新虚拟 DOM 有子节点
} else if (oldVnode.text !== vnode.text) {
// 如果新虚拟 DOM 是文本节点,直接用 textContent 替换掉
nodeOps.setTextContent(elm, vnode.text)
}

那么如果有子节点的话,那应该怎么分类呢?我们可以按照每种情况需要做什么来进行分类,比如说:



  • 第一种情况,我们啥都不用做,因此也可以不用考虑

  • 第二种情况,我们应该把原来 DOM 的 textContent 设置为 ''

  • 第三种情况,我们也应该把原来 DOM 的 textContent 设置为 ''

  • 第四种情况,我们应该加入新的子节点

  • 第五种情况,这个情况比较复杂,需要对比新老子节点的不同

  • 第六种情况,我们应该把原来的 textContent 设置为 '' 后再加入新的子节点


那么通过以上六种情况 (新虚拟 DOM 不含有 text,也就是不是文本节点的情况),我们可以很容易地进行归类:



  • 分类 1️⃣: 第二种情况第三种情况。进行的是操作是:把原来 DOM 的 textContent 设置为 ''

  • 分类 2️⃣: 第四种情况第六种情况。进行的是操作是:如果老虚拟 DOM 有 text,就置空,然后加入新的子节点

  • 分类 3️⃣:第五种情况。进行的是操作是:需要进行精细比较,即对比新老子节点的不同


其实源码也是这么来进行分类的,而且之前说的 同层比较 也就得出来了,因为每次比较都是比较的同一个父节点每一个子元素 (这里的子元素包括文本节点和子节点) 是否相同,而 深度优先遍历(dfs) 是因为每次比较中,如果该节点有子节点 (这里的子节点指的是有 children 属性,而不包括文本节点) 的话需要进行递归遍历,知道最后到文本节点结束。



⭕️ 这里需要搞清楚子节点和子元素的区别和联系



然后我们来看看源码是怎么写吧,只看新虚拟 DOM 不含有 text,也就是不是文本节点的情况:


if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch)
// 递归处理,精细比较
// 对应分类 3️⃣
updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (__DEV__) {
checkDuplicateKeys(ch) // 可以忽略不看
}
// 对应分类 2️⃣
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 对应分类 1️⃣
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 对应分类 1️⃣
nodeOps.setTextContent(elm, '')
}
}

❓我们可以看到源码把分类 1️⃣ 拆开来了,这是因为如果老虚拟 DOM 有子节,那么可能绑定了一些函数,需要进行解绑等一系列操作,作者也没自信看,大致瞄了一眼,但是如果我们要求不高,如果只是想自己手动实现 Diff 算法,那么没有拆开的必要。


作者觉得这么讲可能比网上其他介绍 Diff 算法的要好,其他的可能直接给你说源码是怎么写的,可能没有说明白为啥这么写,但是通过之前这么分析讲解后可能你对为什么这么写会有更深的理解和帮助吧。


updateChildren 函数



同层比较



因为当都含有子节点,即都包含 children 属性后,需要精细比较不同,不能像之前那些情况一样进行简单处理就可以了
那么这个函数中就会介绍大家经常说的 头头、尾尾、尾头、头尾 比较了,其实这不是 Vue 提出来的,是很早就提出来的算法,就一直沿用至今,大家可以参考【snabbdom 库】


🌟 在这之前我们要定义四个指针 newStartIdxnewEndIdxoldStartIdxoldEndIdx,分别指向 新头节点新尾节点旧头节点旧尾节点


循环条件如下:


while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
......
}

其实这个比较也就是按人类的习惯来进行比较的,比较顺序如下 :



  • 1️⃣ 新头节点旧头节点++newStartIdx++oldStartIdx

  • 2️⃣ 新尾节点旧尾节点--newEndIdx--oldEndIdx

  • 3️⃣ 新尾节点旧头节点:需要将 旧头节点 移动到 旧尾节点之前,为什么要这么做,讲起来比较复杂,记住就好,然后 --newEndIdx++oldStartIdx

  • 4️⃣ 新头节点旧尾节点:需要将 旧尾节点 移动到 旧头节点之前,为什么要这么做,讲起来比较复杂,记住就好,然后 ++newStartIdx--oldEndIdx

  • 5️⃣ 如果都没有匹配的话,就把 新头节点 在旧节点列表 (也就是 children 属性的值) 中进行查找,查找方式按照如下:

    • 如果有 key 就把 keyoldKeyToIdx 进行匹配,oldKeyToIdx 根据旧节点列表中元素的 key 生成对应的下标

    • 如果没有,就按顺序遍历旧节点列表找到该节点所在的下标

    • 如果在旧节点列表是否找到也分为两种情况:

      • 找到了,那么只要将 新头节点 添加到 旧头节点 之前即可

      • 没找到,那么需要创建新的元素然后添加到 旧头节点 之前

      • 然后把这个节点设置为 undefined






根据循环条件我们可以得到两种剩余情况,如下:



  • 6️⃣ 如果 oldStartIdx > oldEndIdx 说明老节点先遍历完成,那么新节点比老节点多,就要把 newStartIdxnewEndIdx 之间的元素添加

  • 7️⃣ 如果 newStartIdx > newEndIdx 说明新节点先遍历完成,那么老节点比新节点多,就要把 oldStartIdxoldEndIdx 之间的元素删除


其实我们上面还没有考虑如果节点为 undefined 的情况,因为在上面也提到过,如果四种都不匹配后会将该节点置为 undefined,也只有旧节点列表中才有,因此要在开头考虑这两种情况:



  • 8️⃣ 当 oldStartVnodeundefined++oldStartIdx

  • 9️⃣ 当 oldEndVnodeundefined--oldEndIdx


那么我们来看源码怎么写的吧,其中用到的函数可以查看源码附录


// https://github.com/vuejs/vue/blob/main/src/core/vdom/patch.ts
// 439 行至 556 行
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
// 情况 8️⃣
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
// 情况 9️⃣
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 情况 1️⃣
patchVnode(...)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 情况 2️⃣
patchVnode(...)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// Vnode moved right
// 情况 3️⃣
patchVnode(...)
canMove &&
nodeOps.insertBefore(
parentElm,
oldStartVnode.elm,
nodeOps.nextSibling(oldEndVnode.elm)
)
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
// 情况 4️⃣
patchVnode(...)
canMove &&
nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 情况 5️⃣
if (isUndef(oldKeyToIdx)) // 创建 key -> index 的 Map
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 找到 新头节点 的下标
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) {
// New element
// 如果没找到
createElm(...)
} else {
// 如果找到了
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(...)
oldCh[idxInOld] = undefined
canMove &&
nodeOps.insertBefore(
parentElm,
vnodeToMove.elm,
oldStartVnode.elm
)
} else {
// same key but different element. treat as new element
createElm(...)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
// 情况 6️⃣
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(...)
} else if (newStartIdx > newEndIdx) {
// 情况 7️⃣
removeVnodes(...)
}


如果问为什么这么比较,回答就是经过很多人很多年的讨论得出的,其实只要记住过程就行了,如果想要更深了解 Diff 算法,可以去 B 站看【尚硅谷】Vue源码解析之虚拟DOM和diff算法



v-for 中为什么要加 key


这个问题面试很常见,但是可能大部分人也就只会背八股,没有完全理解,那么经过以上的介绍,我们可以得到自己的理解:



  • 首先,如果不加 key 的话,那么就不会去 Map 里匹配 (O(1)),而是循环遍历整个列表 (O(n)),肯定加 key 要快一点,性能更高

  • 其次,如果不加 key 那么在插入或删除的时候就会出现,原本不是同一个节点的元素被认为是相同节点,上面也有说过是 sameVnode 函数判断的,因此可能会有额外 DOM 操作



为什么说可能有额外 DOM 操作呢?这个和插入的地方有关,之后会讨论,同理删除也一样



证明 key 的性能


我们分为三个实验:没有 key、key 为 index、key 唯一,仅证明加了 key 可以进行最小化更新操作。


实验代码


有小伙伴评论说可以把代码贴上这样更好,那么作者就把代码附上 🥳:


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<style>
.box {
display: flex;
flex-direction: row;
}
.item {
flex: 1;
}
</style>
</head>

<body>
<div id="app">
<div class="box">
<div class="item">
<h3>没有 key</h3>
<p v-for="(item, index) in list">{{ item }}</p>
</div>
<div class="item">
<h3>key 为 index</h3>
<p v-for="(item, index) in list" :key="index">{{ item }}</p>
</div>
<div class="item">
<h3>key 唯一</h3>
<p v-for="(item, index) in list" :key="item">{{ item }}</p>
</div>
</div>
<button @click="click1">push(4)</button>
<button @click="click2">splice(1, 0, 666)</button>
<button @click="click3">unshift(999)</button>
<br /><br />
<button @click="click4">pop()</button>
<button @click="click5">splice(1, 1)</button>
<button @click="click6">shift()</button>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
show: false,
list: [1, 2, 3],
},
methods: {
click1() {
this.list.push(4);
},
click2() {
this.list.splice(1, 0, 666);
},
click3() {
this.list.unshift(999);
},
click4() {
this.list.pop();
},
click5() {
this.list.splice(1, 1);
},
click6() {
this.list.shift();
}
},
})
</script>
</body>

</html>

增加实验


实验如下所示,我们首先更改原文字,然后点击按钮**「观察节点发生变化的个数」**:


在队尾增加


在这里插入图片描述


在队内增加


在这里插入图片描述


在队首增加


在这里插入图片描述


删除实验


在队尾删除


在这里插入图片描述


在队内删除


在这里插入图片描述


在队首删除


在这里插入图片描述


实验结论


增加实验


表格为每次实验中,每种情况的最小更新量,假设列表原来的长度为 n


实验没有 keykey 为 indexkey 唯一
在队尾增加111
在队中增加n - i + 1n - i + 11
在队首增加n + 1n + 11

删除实验


表格为每次实验中,每种情况的最小更新量,假设列表原来的长度为 n


实验没有 keykey 为 indexkey 唯一
在队尾删除111
在队中删除n - in - i1
在队首删除nn1

通过以上实验和表格可以得到加上 key 的性能和最小量更新的个数是最小的,虽然在 在队尾增加在队尾删除 的最小更新量相同,但是之前也说了,如果没有 key 是要循环整个列表查找的,时间复杂度是 O(n),而加了 key 的查找时间复杂度为 O(1),因此总体来说加了 key 的性能要更好。


写在最后


本文从源码和实验的角度介绍了 Diff 算法,相信大家对 Diff 算法有了更深的了解了,如果有问题可私信交流或者评论区交流,如果大家喜欢的话可以点赞 ➕ 收藏 🌟


源码函数附录



列举一些源码中出现的简单函数



setTextContent


function setTextContent(node: Node, text: string) {
node.textContent = text
}

isUndef


function isUndef(v: any): v is undefined | null {
return v === undefined || v === null
}

isDef


function isDef<T>(v: T): v is NonNullable<T> {
return v !== undefined && v !== null
}

insertBefore


function insertBefore(
parentNode: Node,
newNode: Node,
referenceNode: Node
) {
parentNode.insertBefore(newNode, referenceNode)
}

nextSibling


function nextSibling(node: Node) {
return node.nextSibling
}

createKeyToOldIdx


function createKeyToOldIdx(children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}

作者:Karl_fang
来源:juejin.cn/post/7204752219915747388
收起阅读 »

技术管理者应有的 4 种基本思维模式

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

在看文章之前先思考两个问题:



  • 人和人的差别究竟在哪?

  • 人和人之间为什么会有差别?


在各种场合我们经常听到这样一句话:「听懂了很多道理,却依然过不好这一生」。
这里有两个逻辑,一个是知行合一的逻辑,另一个是人的成长在于是否超越了昨天的自己。


今天我们聊一聊作为一个技术管理者应该要有的 4 种基本思维:掌控者思维、杠杆思维,终局思维,闭环思维


掌控者思维


在聊掌控者思维之前我们先看一下掌控者思维的反面:受害者思维。


受害者思维又称为弱者思维,有受害者思维的人在任何时候都会把自己当作一个受害者或弱者,极端者会把整个世界都投射为「伤害者」,「都是别人不对,都是别人对不起我」。


受害者思维是一种思维定势,其本质上是一种忽视自己的主观能动性的行为。


在《乡村爱情 13》里面,王木生是老总儿子,富二代,但是干啥啥不行,还总是给自己找借口,说大环境不好,其他没这背景的都比他强。赵本山饰演的他爹王大拿就教训他:「自己没能力就说没能力,怎么你到哪,哪都大环境不好,咋,你是破坏大环境的人啊?」


这就是比较明显的受害者思维,也就是我们常说的「甩锅」。
受害者思维不仅仅只有这一种表现,在《拆掉思维里的墙》里面,作者总结了 5 种受害者思维的表现。



  1. 推卸责任,保住面子: 上面王木生就是明显的例子,还有像一些网上的评论说的「便秘都怪地球没引力」都是类似的表现。在这种情况下,受害者思维就会认为自己没有责任,不需要承担责任,相信地球没引力,相信大环境不好……

  2. 安心做坏事:这种表现就是为自己的行为找到一个自洽的逻辑,让自己有一个完美的故事,从而在做坏事的时候能心安理得。比如 2001 被判死刑的「杀人魔头张君」(实施团伙抢劫,犯故意杀人罪 2 2次,致 28 人死亡、22 人重伤)在 2001.4.17 一审审理结束后对公众说的话:「我还要向没有枪的受害者和你们的家庭说声‘对不起’,现在想起来,以前有些事情确实做错了,但我没办法,因为我要生存。」

  3. 让我们一起分享凄惨故事会:寻找同伴,和他们分享你的受害过程,在他们那里得到安慰。比如有个女孩失恋了,她的闺蜜会陪她喝酒,说男人没有一个好东西;

  4. 用受害获得同情和帮助:第 4 种和第 3 种有些类似,其区别是上面是通过群体得到安慰,得到是一种情感或者心理上的安慰,而这种是更直接的帮助或者说利益,如我们在职场里面看到的一些人会假装自己不会,从而让热心的同事帮忙解决,自己偷懒,或者在路上我们看到的一些乞丐,可能他们的收入比你想象中多很多;

  5. 自我伤害,绑架他人:这种就更极端了,生活中常见于情侣分手,某一方说,你要是离开我,我就自杀,或者电视剧中的男主站在女主楼下淋雨以求得原谅或者求得表白成功。


如此种种,这些可以让你有短暂的快乐和安全感,但是慢慢就会失去自信、勇气以及自我改进的能力,就会真的变成一个受害者,一个弱者。


我们活在这个尘世,很多事情已经无法掌握,如古罗马哲学家爱比克泰德(Epictetus, 约55~约135年)所说:「我们登上的并非我们所选择的舞台,演出并非我们所选择的剧本」,如果连我们自己也无法掌控,让自己的快乐、成功都掌控在别人手里,这会是怎样一个光景呢?


当你带一个技术团队的时候,当你的团队遇到问题的时候,当你的下属遇到他认为过不去的坎的时候,当发生线上事故的时候,当老板怼到脸上的时候,你会怎么办呢?


当一个受害者吗?是大环境不好?是技术太难?还是坏人太多?


不,绝对不是这样,一个技术管理者应该是强于面对现实,敢于直面问题,像汪涵在我是歌手第三季总决赛面对大型事故时所说的那样:「没事儿不惹事,事儿来了也不要怕事」,以一种掌控的状态来扛起事情,解决问题。


这就是我们所说的掌控者思维,一种强者思维,一种充分发挥自己主观能动性的行为。


掌控者思维的核心思考是:「如果把所有经历过的事情重新倒推一遍,所有条件都不改变,只有自己改变,你能否得到一个更好的结果呢


掌控者思维是从责任心,自我成长,主动改变等方面来提升一个技术管理者的修养,以达到自我的精进。
并且不管什么情况,你都可以负全责。只要你愿意,你就可以做得更好。


除了不怕事,负起责任,还有一些是我们必须要做的。



  1. 学习,提升自己的认知,把自己从知道自己不知道变成知道自己知道,换句话说,保持有计划的学习;

  2. 自省,持续的自省,比如当你有一件事做得不好,自我反思一下哪里做得不好,下一次如何改进,或者更深入一些读一些相关的系统的书,写一篇文章或文档来复盘,掌控自己,从卷自己开始;

  3. 计划,凡事预则立,不预则废,如下棋一般,走一步,算三步,把事情想在前头,如美国首席数据科学家 DJ Patil 所说流传很广的那个便签上的话:「Dream in Years. Plan in Months Evaluate in Weeks. Ship Daily. 」

  4. 知行合一,只有做到,才是真的知道。


当然,做一个掌控者会累一些,但是与自由相比,累又算得了什么呢?


杠杆思维


阿基米德说:「给我一个支点,我就能撬起整个地球。」。


一个优秀的人,都有「杠杆思维」,懂得利用杠杆原理,以较小的付出,撬动更大的回报。这里我们所要说的是杠杆思维中的团队杠杆。


当我们从一个开发变成一个技术管理者时,就不再是一个人在战斗了,你的主要责任就不再是写好代码,而是「使众人行」。


时间对于每个人都是公平的,一个人一天都有 24 小时,它是有限的,而我们所面对的这个世界,工作这些都是无限的。


在一个健康经营的互联网企业中,需求也是无限的,而开发同学的时间都是有限的,除了优先级,我们还能做什么呢?此时有人会说:加人啊。是的,加人。但是加人不是随便加的,他有一个底层逻辑,这个就是今天我们要说的团队杠杆。


团队杠杆的本质是叠加效率,人的时间是有限的,而事情是无限的。


什么是团队杠杆?


通过团队,团队的管理者确定好方向,通过良好的机制和人才梯队,系统化成功的路径,叠加个体的优势,创造出远超个人贡献总和的价值,让 1+1>2,这就是团队杠杆。


既然是杠杆,就一定有一个支点,咱们的支点是什么呢?


对一个团队来说,其支点是已经达成了共识的目标和价值观。


目标是什么,是方向,是团队成员能看到的远方。


为什么是达成了共识?是因为只有达成了共识才能保证目标的统一,才能有意愿,有内在的动力去做事情,才能起到杠杆的效果。


目标和价值观只是最基础的,对于一个团队来说,不同的时候其支点也会不同。



  • 在野蛮生长的团队中,核心岗位上的同学的能力提升是主要支点,这里的核心岗位上的能力提升有两层意思,一个是培养员工,给他们训练的机会,培训,从而快速成长;另一个层面是考虑换一个人,当你这认为这个岗位上的员工的能力、态度差不多到顶了,那就可以考虑用更好、更合适的人来替换,这里的逻辑是选拔大于培养

  • 在快速扩张的团队中,流程和效率是主要支点,通过流程提升协同的效率,通过流程让新人快速融入团队;

  • 在稳定发展的团队中,标准和系统是主要支点,当业务趋于稳定,通过标准和系统固化操作,以系统代码流程和人工。


有了支点,我们就需要开始寻找杠杆,对于一个技术团队来说,团队的杠杆有多大,由人决定。
一个团队里面的人是怎样的,人才梯队是怎样的,人才密度是怎样的?


像麦肯锡,每年都会从全球各个顶尖大学,比如哈佛大学、斯坦福大学、麻省理工学院等,招一大批刚毕业的年轻人,不管是不是商学院毕业的。这些绝顶聪明的年轻人,就是麦肯锡充沛而有效的「团队杠杆」。


对于我们一个普通的互联网创业公司来说,作为一个技术团队的负责人,在有限的范围内选出符合要求的同学,同时尽我所能的带好团队。对于如何来带好团队,我们可以从如下 4 个大的方面来



  1. 信任:不管是平级,还是上下级,信任是必须品,通过良好的沟通,多次的合作,一起的奋斗达成信任,这样才是一个好的团队;

  2. 习惯:管理者以身作则培养团队良好的工作习惯,如做事闭环,分级处理,不越红线等等;

  3. 标准:制定能落地的标准,让大家知道什么是好的,什么是对的;

  4. 能力:流程机制决定事情的下限,人才梯队和个人能力决定事情的上限,管理者要帮忙团队成员成长,发现他们的短板,坦率的沟通并提出改进期望,提升成员的技能,以达成更好的结果。


以上带好团队的表述有些虚,但是是这么个逻辑,各团队各公司术法不同,但方向一致。


用好团队杠杆,一个人走得更快,一群人走得更远。


终局思维


什么是终局思维?


所谓终局思维,是指从终点或者未来的某个时间节点出发,回看现在所做选择,并进行推演的一种思考模式。


这里可能有人会问,什么是终局,是否有终局?这就有点哲学的味道了,先假定有吧,至少在某个未来的时间段是会有一个确定的终局。


我们经常听到人们感叹,如果这辈子能够再来一回,很多错误就不会犯,很多人也不会错过,这算是终局思维的一种表现。只是这个终局思维是等到了终局再提起,有些晚了,我们可以再早一些。


我们在做一件事情的时候,如果能思考一下这件事情的最终达成的目标,或者把最终的形态在脑海里面勾画出来,以终为始,站在未来看当下,修正当下正在做的事情,那么这件事情成功的概率可能就不一样了。


终局思维有 4 个核心点:



  1. 认清方向,所谓的终局思维,一定是你要知道终局是什么,一定要有目标有方向;

  2. 拉长时间周期看问题,在一个较长时间维里来反复推演的逻辑;

  3. 从历史或者更宏观的层面看问题,不局限于当下,不局限于眼前的一亩三分地,把视野拉到历史长河等更大的层面;

  4. 反推机制,终局思维中非常重要的点,从未来反向推演现在要做什么,或者从现在推演未来怎样。


终局思维有什么用?


当一个企业的老板大概知道了未来是什么样子,又能不断地判断和复盘自己的能力,他自然就有了非常强的战略驱动能力,也非常敢为未来投资。


当一个技术管理者知道了团队未来是怎么样的,又能反思当前的情况,从未来形态反思当下的状态,他自然就知道现在最重要的是做什么。


当一个开发同学知道了自己在职场上想成为什么样的人,又能反思自己提升自己,他自然知道现在应该做什么,应该学什么,应该选择什么样的团队,什么样的路。


但是,这里一定会有一个痛苦的过程,甚至不止一个,可能是一直在痛苦中,折腾自己,反思自己,提升自己,一直有危机感,一直在学习中。


心理学研究说,人类对于外部世界的认识可分为三个区域:舒适区(comfort zone),学习区(stretch zone),和恐慌区(stress zone)。我们反思自己、提升自己的过程基本都是在学习区,人只有在学习区的时候,才会是进步的时候。


闭环思维


经常听到人人说:「凡事有交代,件件有着落,事事有回音」,这是典型的闭环思维,是一个职场人的必备思维。作为一个技术管理者更应该具备闭环思维,因为这将是你带团队的核心逻辑之一,它强调责任、敏感性和团队协作,是使众人行的必备。


以「逆向」的逻辑来推演一下,假设你带的团队成员都具备闭环思维,将是什么样的场景?



  • 高效率:闭环思维要求在一定时间内,无论任务完成的效果如何,都要认真、负责的反馈给发起人。这可以帮助团队及时发现问题并加以解决,从而提高工作效率。

  • 强协作:闭环思维需要团队成员之间相互配合,洞察干系人每个阶段的需求,并在截止期内给对方反馈。这可以增强团队协作,提高团队的工作效率和成果。

  • 靠谱:闭环思维要求做事靠谱,做事清楚明白,不会出现意外差错,就算出了问题也会提前告知,让对方可以启用方案 B。这样的技术团队会让人觉得可靠和可信,提高团队在公司中的信任度。

  • 好形象:闭环思维是一种良好的职业素养,可以让你和你的团队建立良好的职业形象。在职场中,「闭环思维」不仅仅说明了一个员工拥有完成任务的能力,还体现出「良好的合作意识」,这样的你会让上级、兄弟部门觉得很「靠谱」,能够放心的将工作交给你和你的团队。


那么如何做到闭环思维呢?


闭环思维有两层逻辑,一个是完整,即有完整的闭环,做事情的时候需要考虑事情的起点、目标、影响因素和结果,明确需求和关键点,以及可能遇到的问题,在过程中设置检查点,让事情可控;另一个是主动反馈,在我们完成一项工作或参加一项事情,不管执行效果如何,都要及时将结果反馈给发起人。


第一层逻辑我们可以使用 PDCA 环来辅助达成,第二层反馈逻辑还是需要刻意去练习,甚至给自己设置一些任务提醒来达到反馈的目的,并且在反馈时尽量做到 STAR 的表述。


番外


最近 chatGPT 一直在火,于是就今天的题目我问了 chatGPT 一些问题:



  • 技术管理者必备的4种基本思维模式

  • 技术管理者必备的思维模式

  • 技术管理的四种思维

  • 技术管理者必备的4种思维


大概的答案如下:


openAI 的回答


问:技术管理者必备的思维模式


作为一名技术管理者,以下是一些必备的思维模式:



  1. 系统思维模式:技术管理者需要具备系统思维模式,能够理解和处理复杂系统的各种关系和相互作用,从而在整个系统中发现问题并提供解决方案。

  2. 创新思维模式:技术管理者需要能够持续地推动创新,寻找新的解决方案并将其应用到业务中,从而实现业务目标。

  3. 问题解决思维模式:技术管理者需要能够分析和解决各种问题,包括技术问题、组织问题和人员问题等。

  4. 客户导向思维模式:技术管理者需要关注客户需求和体验,从客户的角度出发思考问题,为客户提供高质量的产品和服务。

  5. 团队合作思维模式:技术管理者需要具备团队合作意识,能够有效地管理和领导团队,激发员工的潜力,协调各种资源,实现业务目标。

  6. 风险管理思维模式:技术管理者需要对业务中可能出现的风险进行预测和管理,制定相应的应对措施,确保业务的可持续发展。

  7. 数据驱动思维模式:技术管理者需要依据数据进行决策和管理,从数据中发现问题和机会,制定相应的战略和计划,提高业务效率和效果。

  8. 学习和成长思维模式:技术管理者需要保持学习和成长的态度,不断学习新知识和技能,从他人的经验中汲取营养,不断提高自身的领导能力和管理水平。


phind 的回答


问:技术管理者必备的4种基本思维模式


答案1


作为技术管理者,必备的4种基本思维模式如下:




  1. 系统思维:技术管理者需要具备系统思维,可以将复杂的问题分解为简单的部分,然后逐一解决。例如,当公司需要开发一个新产品时,技术管理者需要思考每个阶段的具体步骤,确定每个步骤的时间表和责任人,确保整个过程的顺利进行。




  2. 创新思维:技术管理者需要具备创新思维,可以在技术方面寻找新的解决方案,以提高公司的竞争力。例如,当公司需要开发一项新技术时,技术管理者需要思考如何利用现有技术来开发出更好的产品,或者如何开发出全新的技术来满足市场需求。




  3. 沟通思维:技术管理者需要具备良好的沟通能力,可以与员工、客户和其他部门进行有效的沟通。例如,当公司需要与客户沟通产品需求时,技术管理者需要了解客户的需求,并将这些需求传达给开发团队。




  4. 领导思维:技术管理者需要具备领导思维,可以激励员工,推动项目进展,达成公司目标。例如,当公司需要完成一个重要项目时,技术管理者需要领导开发团队,确保项目按时完成,并向管理层报告项目进展情况。




技术管理者不仅需要具备以上4种基本思维模式,还需要具备自我驱动、架构把握、目标管理等核心能力,才能胜任管理岗位。在实际工作中,技术管理者应该灵活运用这些思维模式和能力,不断提升自己的管理水平。


答案 2


根据多个来源,以下是技术管理必备的四种思维和其他相关思维模型:




  1. 抽象思维:这种思维能力可以帮助技术人快速抽取面对问题的关键要素和本质,从而更好地分析和解决问题。




  2. 系统思维:这种思维能力可以帮助技术人将问题看作一个整体,理解各个组成部分之间的相互作用和影响,从而更好地进行系统设计和管理。




  3. 创新思维:这种思维能力可以帮助技术人寻找新的解决方案和创新点,从而更好地推动技术进步和发展。




  4. 逻辑思维:这种思维能力可以帮助技术人分析和推理问题,从而更好地发现问题根源和解决方案。




除了以上四种思维,以下是其他常用的思维模型:




  • PEST分析:这是一种常用的管理咨询框架,可以帮助企业从政治、经济、社会和技术等多个角度分析市场环境和竞争对手,从而更好地制定战略和决策。




  • PDCA循环:这是一种管理模型,可以帮助企业进行持续改进和优化,包括计划、执行、检查和行动四个环节。




  • SMART原则:这是一种目标管理模型,可以帮助企业制定具体、可衡量、可达成、有意义和时限的目标,从而更好地推动业务发展。




  • STAR法:这是一种面试技巧,可以帮助面试者回答问题更具体、更有条理、更有说服力,包括情境(situation)、任务(task)、行动(action)和结果(result)四个要素。




  • 5W2H法:这是一种问题分析和解决模型,可以帮助企业和个人从五个W(what、why、who、when、where)和两个H(how、how much)多个角度分析问题和制定解决方案。




  • PERT 技术:这是一种项目管理工具,可以帮助企业规划和安排整个项目行程,跟踪实施阶段,从而更好地控制项目进度和成本。




总之,技术管理必备的思维能力和模型是多种多样的,需要根据具体情况选择和应用。除了上述思维能力和模型外,还有很多其他的思维工具和技术,需要技术人员不断学习和掌握,以更好


答案 3


根据多个来源的信息,以下是四种技术管理必备的思维能力:




  1. 抽象思维:这种思维能力可以帮助技术人员快速抽取问题的关键要素和本质,从而更快地理解问题和解决问题。例如,当面对一个新的编程语言时,抽象思维可以帮助技术人员快速理解其核心概念和语法规则。




  2. 产品思维:这种思维能力可以帮助技术人员更好地理解用户需求,并将技术解决方案转化为实际的产品。例如,一个架构师需要了解产品的功能需求,以便设计出最佳的技术架构来支持这些需求。




  3. 创新思维:这种思维能力可以帮助技术人员不断创新,寻找新的解决方案和技术。例如,当面对一个复杂的技术问题时,创新思维可以帮助技术人员找到一种独特的解决方案,从而提高效率和质量。




  4. 商业思维:这种思维能力可以帮助技术人员更好地理解商业环境和商业需求,以便为企业提供最佳的技术支持和解决方案。例如,在设计一个新的软件系统时,商业思维可以帮助技术人员理解客户的商业需求,并设计出最佳的系统架构来支持这些需求。




除了以上四种思维能力外,还有其他常见的思维模型和方法可以帮助技术人员更好地管理和解决问题,例如:



  • PDCA 循环:这是一种常见的问题解决方法,包括计划、实施、检查和行动四个阶段,可以帮助技术人员系统地解决问题。

  • SMART 原则:这是一种有效的目标设定方法,包括具体、可衡量、可实现、相关和时间限制五个要素,可以帮助技术人员设定清晰的目标和指标。

  • 5W2H 法:这是一种问题分析方法,包括什么、为什么、谁、何时、何地、如何和多少七个问题,可以帮助技术人员全面地分析和解决问题。


作者:潘锦
来源:juejin.cn/post/7203880677195661373
收起阅读 »

三十岁前端的破冰尝试

@charset "UTF-8";.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:15px;overflow-x:hidden;color:#2b2...
继续阅读 »

大多数人没有意识到精力的有限而盲目学习,从没有停下来认真咀嚼已有的东西。



本人简介


JavaScrip码农,今年三十,宿主是非互联网类型的外企,提供的内存虽然不大,但也基本够存活。


工作之余,我的主题就是咸鱼。但或许是我的咸度不够,最近开始腐烂了,尤其是夜深人静,主要的信息输入被关闭之后,我就感觉内在的信息流在脑海里乱窜,各种健康指数开始飙升。就像是一台老旧的电脑,非要带最新的显卡游戏,发出嘤嘤嘤的EMO声,最后在卡死在昏睡页面。


大多时候醒来会一切安好,像是被删去了前一晚的日志。但有时也会存有一些没删除干净的缓存,它们就像是病毒,随着第二天的重启复苏。我会感到无比的寒冷,冷到我哪怕是饥饿也不敢出门,只有戴上口罩会给我一丝丝的勇气。


这种寒冷会刺激着我无病呻吟,我会感到惊恐和害怕,害怕某天被宿主的回收机制发现这里的不正常,然后被文明的光辉抹除,就如新冠背后那鲜红的死亡人数一样。


或许是幼年求学寄人篱下时烙下的病根,但那时候心田干涸了还可以哭泣。如今呢,心田之上早已是白雪皑皑。


这些年也有人帮助过我,我也努力挣扎过,但大多时候毫无章法,不仅伤了别人的心,也盲目地消耗着心中的热血,愧疚与自责的泪水最终只是让冰层越积越深。


今天也不知哪根筋抽抽了,想着破冰。


嗯,就是字面上的意思,满脑子都是“破冰”二字……


破冰项目


发表这个稿子算是破冰的第一步~


项目的组织架构初步定为凌凌漆,敏捷周期为一周,其中周日进行复盘和制定新计划,其余作为执行日。由于项目长期且紧迫,年假就不予考虑了,病假可以另算,津贴方面目前只考虑早餐,其他看项目发展情况再做调整。


硬件层面


目前作息相当紊乱,供电稳定性差,从近几年的硬件体验报告可以看出,总体运行还算正常,但小毛病层出不穷,电压不稳是当前主要矛盾。OKR如下:


O:保持一个良好的作息

KR1: 保证每天八小时的睡眠。

KR2:保证每天凌晨前关灯睡下。

KR3:保证每天早上九点前起床。


软件层面


英语是硬伤,其次是底层算法需要重写,不然跑着跑着还是会宕机。


翻译是个不错的路子,但数据源是个头痛的问题……肯定得找和技术相关的东西来翻译,并且可以有反馈。嗯…… 想到可以找掘金里已经有的翻译文章,截取其中一小段来进行快速试错。


至于底层算法的问题,此前在leetcode练过一段时间,但仅停留在已知的变得熟练,未知的依旧不会。


因此我觉得有必要先梳理出关于算法的个人认知的知识体系……


总结下来下一阶段任务:



  1. 选择一篇翻译文章,找到其原文,选其中完整的一段进行翻译。

  2. 根据当前认知画个关于算法的思维导图。


下周日会出这周的运行报告以及新一期的计划表。


最后随想


若是觉得我这样的尝试也想试一试,欢迎在评论附上自己的链接,一起尝试,

作者:行僧
来源:juejin.cn/post/7152143987225133086
相互借鉴,共同进步~

收起阅读 »

聊一聊过度设计!

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

  新手程序员在做设计时,因为缺乏经验,很容易写出欠设计的代码,但有一些经验的程序员,尤其是在刚学习过设计模式之后,很容易写出过度设计的代码,而这种代码比新手程序员的代码更可怕,过度设计的代码不仅写出来时的成本很高,后续维护的成本也高。因为相对于毫无设计的代码,过度设计的代码有比较高的理解成本。说这么多,到底什么是过度设计?


什么是过度设计?


  为了解释清楚,我这里用个类比,假如你想拧一颗螺丝,正常的解决方案是找一把螺丝刀,这很合理对吧。 但是有些人就想:“我就要一个不止能拧螺丝的工具,我想要一个可以干各种事的工具!”,于是就花大价钱搞了把瑞士军刀。在你解决“拧螺丝”问题的时候,重心早已从解决问题转变为搞一个工具,这就是过度设计。

在这里插入图片描述
  再举个更技术的例子,假设你出去面试,面试官让你写一个程序,可以实现两个数的加减乘除,方法出入参都给你提供好了 int calc(int x, int y, char op),普通程序员可能会写出以下实现。


    public int calc(int x, int y, int op) {
if (op == '+') {
return x + y;
} else if (op == '-') {
return x - y;
} else if (op == '*') {
return x * y;
} else {
return x / y;
}
}
复制代码

  而高级程序员会运用设计模式,写出这样的代码:


public interface Strategy {
int calc(int x, int y);
}

public class AddStrategy implements Strategy{
@Override
public int calc(int x, int y) {
return x + y;
}
}

public class MinusStrategy implements Strategy{
@Override
public int calc(int x, int y) {
return x - y;
}
}
/**
* 其他实现
*/

public class Main {
public int calc(int x, int y, int op) {
Strategy add = new AddStrategy();
Strategy minux = new MinusStrategy();
Strategy multi = new MultiStrategy();
Strategy div = new DivStrategy();
if (op == '+') {
return add.calc(x, y);
} else if (op == '-') {
return minux.calc(x, y);
} else if (op == '*') {
return multi.calc(x, y);
} else {
return div.calc(x, y);
}
}
}
复制代码

  策略模式好处在于将计算(calc)和具体的实现(strategy)拆分,后续如果修改具体实现,也不需要改动计算的逻辑,而且之后也可以加各种新的计算,比如求模、次幂……,扩展性明显增强,很是牛x。 但光从代码量来看,复杂度也明显增加。回到我们原始的需求上来看,如果我们只是需要实现两个整数的加减乘除,这明显过度设计了。


过度设计的坏处


  个人总结过度设计有两大坏处,首先就是前期的设计和开发的成本问题。过度设计的方案,首先设计的过程就需要投入额外的时间成本,其次越复杂的方案实现成本也就越高、耗时越长,如果是在快速迭代的业务中,这些可能都会决定到业务的生死。其次即便是代码正常上线后,其复杂度也会导致后期的维护成本高,比如当你想将这些代码交接给别人时,别人也需要付出额外的学习成本。


  如果成本问题你都可以接受,接下来这个问题可能影响更大,那就是过度设计可能会影响到代码的灵活性,这点听起来和做设计的目的有些矛盾,做设计不就是为了提升代码的灵活性和扩展性吗!实际上很多过度设计的方案搞错了扩展点,导致该灵活的地方不灵活,不该灵活的地方瞎灵活。在机器学习领域,有个术语叫做“过拟合”,指的是算法模型在测试数据上表现完美,但在更广泛的数据上表现非常差,模式缺少通用性。 过度设计也会出现类似的现象,就是缺少通用性,在面对稍有差异的需求上时可能就需要伤筋动骨级别的改造了。


如何避免过度设计


  既然过度设计有着成本高和欠灵活的问题,那如何避免过度设计呢!我这里总结了几个方法,希望可以帮到大家。


充分理解问题本身


  在设计的过程中,要确保充分理解了真正的问题是什么,明确真正的需求是什么,这样才可以避免做出错误的设计。


保持简单


  过度设计毫无例外都是复杂的设计,很多时候未来有诸多的不确定性,如果过早的针对某个不确定的问题做出方案,很可能就白做了,等遇到真正问题的时候再去解决问题就行。


小步快跑


  不要一开始就想着做出完美的方案,很多时候优秀的方案不是设计出来的,而是逐渐演变出来的,一点点优化已有的设计方案比一开始就设计出一个完美的方案容易得多。


征求其他人的意见


  如果你不确定自己的方案是不是过度设计了,可以咨询下其他人的,尤其是比较资深的人,交叉验证可以快速让你确认问题。


总结


  其实在业务的快速迭代之下,很难判定当前的设计是欠设计还是过度设计,你当前设计了一个简单的方案,未来可能无法适应更复杂的业务需求,但如果你当前设计了一个复杂的方案,有可能会浪费时间……。 在面对类似这种不确定性的时候,我个人还是比较推崇大道至简的哲学,当前用最简单的方案,等需要复杂性扩展的时候再去重构代码。

作者:xindoo
来源:juejin.cn/post/7204423284905738298

收起阅读 »

互联网大裁员原因分析

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

继谷歌、微软之后,Zoom、eBay、波音、戴尔加入最新一波“裁员潮”中。


2 月 7 日,美国在线会议平台 Zoom 宣布将裁减 1300 名员工,成为最新一家进行裁员的公司,大约 15% 的员工受到影响。


同日,总部位于亚特兰大的网络安全公司 Secureworks 在一份提交给美国证券交易委员会( SEC )的文件中宣布,将裁员 9%,因为该公司希望在“一些世界经济体处于不确定时期”减少开支。据数据供应商 PitchBook估计,该公司近 2500 名员工中约有 225 人将在此轮裁员中受到影响。


此外,电商公司eBay于2月7日在一份SEC文件中宣布,计划裁员500人,约占其员工总数的4%。据悉,受影响的员工将在未来24小时内得到通知。


2月6日,飞机制造商波音公司证实,今年计划在财务和人力资源部门裁减约 2000 个职位,不过该公司表示,将增加 1 万名员工,“重点是工程和制造”。


个人电脑制造商戴尔的母公司,总部位于美国德克萨斯州的戴尔科技,2 月 6 日在一份监管文件中表示,公司计划裁减约 5% 的员工。戴尔有大约 13.3 万名员工,在这个水平上,约 6650 名员工将在新一轮裁员中受到影响。


除了 Zoom、eBay、波音、戴尔 等公司,它们的科技同行早已经采取了同样的行动。


从去年 11 月开始,许多硅谷公司员工就开始增加了关注邮箱的频率,害怕着某封解除自己公司内网访问权限的邮件来临,在仅仅在 2022 年 11 月,裁员数字就达到了近 6 万人,而如果从 2022 开始计算,各家已经陆续裁员了近十五万人。



但本次裁员并非是因为营收的直接下降:事实上,硅谷各家 2022 年营收虽然略有下跌,但总体上仍然保持了平稳,甚至部分业务略有上涨,看起来并没有到「危急存亡之秋」,需要动刀进行大规模裁员,才能在寒冬中存活的程度。


相较于备受瞩目的裁员,一组来自美国政府的就业数据就显得比较有意思了。据美国劳工统计局 2 月 3 日公布的数据,美国失业率 1 月份降至 3.4%,为 1969 年 5 月以来最低。



美国 1 月份非农就业人数新增 51.7 万人,几乎是经济学家预期的三倍,即使最近主要在科技行业裁员,但建筑、酒店和医疗保健等行业带来了就业增长。


一方面是某些企业的大规模裁员,仅 1 月份就影响超过 10 万人;而另一方面,政府报告显示就业市场强劲。这样来看,美国的就业市场情况似乎有些矛盾。


2022 年 12 月初,多名B站员工在社交媒体上表示,B站开始了新一轮裁员,B端、漫画、直播、主站、Goods等部门均有涉及,整体裁员比例在30%左右。12月19日,小米大规模裁员的消息又有曝出,裁员涉及手机部、互联网部、中国部等多部门,个别部门裁员比例高达75%。12月20日,知乎又传裁员10%。


似乎全球的科技公司都在裁员,而我们想要讨论裁员的问题,肯定绕不开两个大方向:经济下行和人员问题。


下行环境


联合国一月发布的《2023年世界经济形势与展望》中指出,2022 年,一系列相互影响的严重冲击,包括新冠疫情、乌克兰战争及其引发的粮食和能源危机、通胀飙升、债务收紧以及气候紧急状况等,导致世界经济遭受重创。美国、欧盟等发达经济体增长势头明显减弱,全球其他经济体由此受到多重不利影响。与新冠疫情相关的反复封锁以及房地产市场的长期压力,延缓了中国的经济复苏进程。


在此背景下,2023 年世界经济增速预计将从 2022 年估计的 3.0% 下降至 1.9%。2024 年,由于部分不利因素将开始减弱,预计全球经济增速将适度回升至 2.7%。不过,这在很大程度上将取决于货币持续紧缩的速度和顺序、乌克兰战争的进程和后果以及供应链进一步中断的可能性。


在通货膨胀高企、激进的货币紧缩政策以及不确定性加剧的背景下,当前全球经济低迷,导致全球经济从新冠疫情的危机中复苏的步伐减缓,对部分发达国家和发展中国家均构成威胁,使其 2023 年可能面临衰退的前景。


2022 年,美国、欧盟等发达经济体增长势头明显减弱。报告预计,2023 年美国和欧盟的经济增速分别为 0.4% 和 0.2%,日本为 1.5%,英国和俄罗斯的经济则将分别出现 0.8% 和 2.9% 的负增长。


与此同时,全球金融状况趋紧,加之美元走强,加剧了发展中国家的财政和债务脆弱性。自 2021 年末以来,为抑制通胀压力、避免经济衰退,全球超过85%的央行纷纷收紧货币政策并上调利率。


报告指出,2022 年,全球通胀率约为 9%,创数十年来的新高。2023 年,全球通胀率预计将有所缓解,但仍将维持在 6.5% 的高水平。


据美国商务部经济分析局(BEA)统计,第二、三季度,美国私人投资分别衰退 14.1% 和 8.5%。加息不仅对美国企业活动产生抑制作用,而且成为美国经济复苏的最主要阻力。尤其是,非住宅类建筑物固定投资已连续六个季度衰退。预计 2023 年美国联邦基金利率将攀升至 4.6%,远远超过 2.5% 的中性利率水平,经济衰退风险陡增,驱动对利率敏感的金融、房地产和科技等行业采取裁员等必要紧缩措施。


发展上限


美国企业的业务增长和经营利润出现问题。据美国多家媒体报道,第三季度,谷歌利润率急剧下滑,Meta 等社交媒体的广告收入迅速降温,微软等其他科技企业业务增长也大幅放缓。自7月以来,美国服务业PMI已连续5个月陷入收缩区间,制造业 PMI 也于 11 月进入收缩区间。在美国经济前景和行业增长空间出现问题的背景下,部分行业采取裁员、紧缩开支等“准备过冬”计划也就在意料之中了。


2022年,在市值方面,作为中概股的代表阿里、腾讯、快手等很多企业的市值都跌了 50%,甚至70%、80%。在收入方面,BAT 已经停止增长几个季度了,阿里和腾讯为代表的企业已经开始负增长。在经济下行的背景下,向内开刀、降本增效成为企业生存的必然之举。除了裁员,收缩员工福利、业务调整,也是企业降本增效的举动之一。


如果说 2021 年的裁员,很多是由于业务受到冲击,比如字节跳动的教育业务,以及滴滴、美团等公司的社区团购项目。但到了 2022 年,更多企业裁员的背后是降本增效、去肥增肌。


全球宏观经济表现不佳,由产业资本泡沫引发的危机感传导到科技企业的经营层,科技企业不得不面对现实。科技行业处在重要的结构转型期。iPhone 的横空出世开创了一个移动互联网的新时代,而当下的科技巨头也都是移动互联网的大赢家。但十多年过去了,随着智能手机全球高普及率的完成,移动互联网的时代红利逐渐消失,也再没有划时代的创新和新的热点。


这两年整个移动互联网时代的赢家都在焦急地寻找新的创新增长点。比如谷歌和 Meta 多年来一直尝试投资新业务,如谷歌云、Web3.0等,但实际收入仍然依赖于广告业务,未能找到真正的新增长点。这使得其中一些公司容易受到持有突破性技术的初创公司影响。


科技企业倾力“烧钱”打造新赛道,但研发投入和预期产出始终不成正比,不得不进行战略性裁员。


我们这里以这两年爆火的元宇宙举例:


各大券商亦争相拥抱元宇宙,不仅元宇宙研究团队在迅速组建,元宇宙首席分析师也纷纷诞生。 2021 年下半年,短短半年内便有数百份关于元宇宙的专题研报披露。


可以说,在扎克伯格和Meta的带领下,全世界的大厂小厂都在跟着往元宇宙砸钱。


根据麦肯锡的计算,自2021年以来,全世界已经向虚拟世界投资了令人瞠目结舌的数字——1770亿美元。


但即使作为元宇宙领军的 Meta 现实实验室(Reality Labs)2022 年三季度收入 2.85 亿美元,运营亏损 36.7 亿美元,今年以来已累计亏损 94 亿美元,去年亏损超过 100亿 美元。显然,Meta 的元宇宙战略还未成为 Meta的机遇和新增长点。


虽然各 KOL 高举“元宇宙是未来”的大旗,依旧无法改写“元宇宙未至”的局面。刨除亟待解决的关键性技术问题,如何兼顾技术、成本与可行性,实现身临其境的体验,更是为之尚远。元宇宙还在遥远的未来。


早在 2021 年12 月底,人民日报等官方媒体曾多次下场,呼吁理性看待“元宇宙”。中央纪委网站发布的《元宇宙如何改写人类社会生活》提及“元宇宙”中可能会涉及资本操纵、舆论吹捧、经济风险等多项风险。就连春晚的小品中,“元宇宙”也成为“瞎忽悠”的代名词。


2022 年 2月18日,中国银保监会发布了《关于防范以“元宇宙”名义进行非法集资的风险提示》,并指出了四种常见的犯罪手法,包括编造虚假元宇宙投资项目、打着元宇宙区块链游戏旗号诈骗、恶意炒作元宇宙房地产圈钱、变相从事元宇宙虚拟币非法谋利。


2022 年 2月7日,英国《金融时报》报道称,随着《网络安全法案》逐步落实,元宇宙将会受到严格的英国监管,部分公司可能面临数十亿英镑的潜在罚款。


2022 年 2月6日,据今日俄罗斯电视台(RT)报道,俄罗斯监管机构正在研究对虚拟现实技术实施新限制的可能性,他们担心应用该技术可能会协助非法活动。


各个国家的法律监管的到来,使得元宇宙的泡沫迅速炸裂。无数的元宇宙公司迅速破产,例如白鹭科技从 H5 游戏引擎转型到元宇宙在泡沫破裂的情况下个人举债 4000 万,公司破产清算。


本质上来说如今互联网行业已经到了一个明显的发展瓶颈,大家吃的都是移动网络和智能手机的普及带来的红利。在新的设备和交互方式诞生前,大家都没了新故事可讲,过去的圈地跑马模式在这样的大环境下行不通了。


法律监管


过去十年时间,互联网世界的马太效应越来越明显。一方面,几大巨头们在各自领域打造了占据了主导份额的互联网平台,不断推出包罗万象的全生态产品与服务,牢牢吸引着绝大多数用户与数据。他们的财务业绩与股价市值急剧增长,苹果、谷歌、亚马逊的市值先后突破万亿甚至是两万亿美元。


而另一方面,诸多规模较小的互联网公司却面临着双重竞争劣势。他们不仅财力与体量都无法与网络巨头抗衡,还要在巨头们打造的平台上,按照巨头制定偏向自己的游戏规则,与包括巨头产品在内的诸多对手激烈竞争用户。


2020 年 10 月,在长达 16 个月的调查之后,美国众议院司法委员会发布了一份长达 449 页的科技反垄断调查报告,直指谷歌、苹果、Facebook、亚马逊四大科技巨头滥用市场支配地位、打压竞争者、阻碍创新,并损害消费者利益。


2020 年 10 月 20 日,美国司法部连同美国 11 个州的检察长向 Google 发起反垄断诉讼,指控其在搜索和搜索广告市场通过反竞争和排他性行为来非法维持垄断地位。


2021 年明尼苏达州民主党参议员艾米·克洛布查尔(Amy Klobuchar)和爱荷华州共和党参议员查克·格拉斯利(Chuck Grassley)共同提出的《美国创新与选择在线法案》和 《开放应用市场法案》旨在打击谷歌母公司 Alphabet、亚马逊、Facebook 母公司 Meta 和苹果公司等科技巨头的一些垄断行为,这将是互联网向公众开放近30年来的首次重要法案。


《美国创新与选择在线法案》的内容包括禁止占主导地位的平台滥用把关权,给予营产品服务特权,使竞争对手处于不利地位;禁止施行对小企业和消费者不利,有碍于竞争的行为,例如要求企业购买平台的商品或服务以获得在平台上的优先位置、滥用数据进行竞争、以及操纵搜索结果偏向自身等。


不公平地限制大平台内其他商业用户的产品、服务或业务与涵盖平台经营者自己经营的产品、服务或业务相竞争能力,从而严重损害涵盖平台中的竞争。


除了出于大平台安全或功能的需要,严重限制或阻碍平台用户卸载预装的软件应用程序,将大平台用户使用大平台经营者提供的产品或服务设置为默认或引导为默认设置。


《开放应用市场法案》针对“守门人”执行,预计将会在应用商店、定向广告、互联操作性,以及垄断并购等方面,对相应企业做出一系列规范要求。此外欧盟方面还曾透露,如“守门人”企业不遵守上述规则,将按其上一财政年度的全球总营业额对其处以“不低于 4%、但不超过20%”的罚款。法案允许应用程序侧载(在应用商店之外下载应用程序),旨在打破应用商店对应用程序的垄断能力,将对苹果、谷歌的应用商店商业模式产生重要影响。


大型科技公司们史无前例搁置竞争,并且很有默契地联合起来。他们和他们的贸易团体在两年内耗费大约 1 亿美元进行游说,超过了制药和国防等高支出行业。他们向政界人士捐赠了 500 多万美元,科技游说人士向负责捍卫民主党多数席位的政治行动委员会(PAC)捐赠了 100 多万美元。他们还向不需要披露资金来源的黑钱组织、非营利组织和行业协会投入了数百万美元。几位国会助手表示,他们收到的有关这些法案的宣传比他们多年来处理的任何其他法案都要多。


这两项法案已通过国会相关委员会的审查,依然在等待众议院和参议院的表决。而美国即将开始中期选举。Deese 称,共和党已经明确表示,如果共和党重新控制国会两院,他们将不会支持这些法案。但如果民主党当选的话,科技巨头们估计不好过了。


很遗憾的是,2023年,新一届美国国会开幕后,众议院议长的选举经多轮投票仍然“难产”,导致新一届国会众议院无法履职。开年的这一乱象凸显美国政治制度失灵与破产,警示美国党争极化的趋势恐正愈演愈烈;


欧盟也多次盯上四大公司,仅谷歌一家,欧盟近三年来对其开出的反垄断处罚的金额已累计超过 90 亿美元。


而中国的举措也不小。


2020 年年初,实施了近 12 年的《反垄断法》(2008 年 8 月 1 日生效)首次进入“大修”——国家市场监督管理总局在其官网公布了《反垄断法修订草案(公开征求意见稿)》(以下简称“征求意见稿”)。


《法制日报》报道指出,征求意见稿中共有 8 章 64 条,较现行法要多出 7 条。可见,这次修法,已与另立新法有同等规模。


值得注意的是,征求意见稿还首次将互联网业态纳入其中,新增互联网领域的反垄断条款,针对性地列明相关标准和适用规程。


以市场支配地位认定为例,征求意见稿根据互联网企业的特点,新增了包括网络效应、规模经济、锁定效应、掌握和处理相关数据的能力等因素。


11 月 10 日,赶在双 11 前一天,国家市场监管管理总局再次出手,发布了《关于平台经济领域的反垄断指南(征求意见稿)》(以下简称《指南》)公开征求意见的公告。


《指南》不仅对“互联网平台”做了进一步界定,还结合个案更为具体详尽地对垄断协议,滥用市场支配地位行为,经营者集中,滥用行政权力排除、限制竞争四个方面作出分析和规定。


国家在平台经济领域、反垄断领域的法律规范,在《反垄断指南》出台以后,已经有了相当程度的完善。后续随着《反垄断法》修正案的通过,二者结合基本构建了我国反垄断领域的法律框架。


随着《反垄断法》的完善,在互联网领域的处罚案例逐渐浮出水面,针对阿里巴巴、美团等互联网公司都开出了大额罚单。


2021年我国在网络安全方面也加速发展。2021年6月10日颁布《中华人民共和国数据安全法》,2021年8月20日颁布《中华人民共和国个人信息保护法》。有关部门相继出台了《网络安全审查办法》《常见类型移动互联网应用程序必要个人信息范围规定》《数据出境安全评估办法(征求意见稿)》等部门规章和政策性文件。


可以预见的是,未来监管部门的监管措施更能兼顾互联网行业发展特征和社会整体福利,监管部门会不断完善规章、政策文件和标准文件,提供给企业明确和细化的指引。同时,相关部门的监管反应速度会越来越及时,监管层面对违法查处的力度也会越来越严。


人口红利


我们依然处在人口规模巨大的惯性中,人口规模巨大意味着潜在市场规模巨大,伴随经济持续发展、收入水平提高、消费能力强劲,由此带来的超大市场规模不可估量。而现在人口红利没了。


中国国家统计局 1 月 17 日公布,2022年末全国人口(包括 31 个省、自治区、直辖市和现役军人的人口,不包括居住在 31 个省、自治区、直辖市的港澳台居民和外籍人员) 141175 万人,比上年末减少 85 万人。这是近61年来中国首次人口负增长。人口负增长的早期阶段是一种温和的人口减少,所以依然会沿袭人口规模巨大的惯性;但在人口负增长的远期阶段,如果生育率仍未有所回升的话,就有可能导致一种直线性的减少。


目前所有行业都不得不面临从人口红利转向素质红利的转变。


人员过剩


微软在过去两年员工数新增 6 万,Google 则是新增了 7 万,Meta 则是直接从疫情之前的 4 万翻倍至 2022 年的 8.7 万。而依赖物流服务的亚马逊则最为激进,两年时间全球全职员工数增长了令人咂舌的 8.1 万,全职员工数近乎翻倍。



高盛的经济学家在一份报告中指出“那些正在裁员的科技公司有一些共同点,希望重新平衡业务的结构性转变,并为更好的利润开路。我们发现,许多最近宣布大规模裁员的公司都有三个共同特征。首先,许多都是在科技领域。其次,许多公司在疫情期间大肆招聘。第三,它们的股价出现了更大幅度的下跌,从峰值平均下跌了 43%。”


平均而言,那些进行裁员的公司在疫情期间的员工数量增长了 41%,此举往往是因为他们过度推断了与疫情相关的趋势,比如商品需求或在线时间的增长。


行裁员的公司并不能代表更广泛的情况,最近许多裁员公告并不一定意味着需求状况会减弱。与此一致的是,高盛预计更具代表性的实时估计的裁员率最近虽有所增加,但仅恢复到疫情前的水平,以历史标准衡量,裁员率水平较低。


结论


全球经济下行是大势,层层增加的法律监管是推动,没有人口红利和新玩法股价要大跌。


全球通胀激增,激进的货币紧缩政策以及不确定性加剧、俄乌战争等影响,全球经济低迷。新冠疫情带来的影响难以快速恢复。而中国还得面临人口红利消失、房地产饮鸩止渴的深远影响。而法律的层层监管和反垄断的推进在逐步打压科技巨头的已有市场,没有新技术的突破和新玩法让科技巨头们也没了新增和突破的空间。对于未来的经济发展的错误预估和疫情特殊时期的大量增长让科技巨头们大肆招聘,这些都成为了股价下跌和缩减利润的元凶。目前的大裁员可以算是一种虚假繁荣的泡沫爆裂后的回调,虽然不知道这个回调什么时候结束,但是随着人工智能的出圈和将来新技术的突破,也许整个行业可以浴火重生。


作者:Andy_Qin
来源:juejin.cn/post/7201047960825774139
收起阅读 »

你在公司混的差,可能和组织架构有关!

@charset "UTF-8";.markdown-body{line-height:1.75;font-family:-apple-system-font,BlinkMacSystemFont,Helvetica Neue,PingFang SC,Hira...
继续阅读 »

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,非公众号转载保留此声明。



如果你接触过公司的面试工作,一定见过很多来自大公司的渣渣。这些人的薪资和职位,比你高出很多,但能力却非常一般。


如果能力属实,我们大可直接把这些大公司的员工打包接收,也免了乱七八糟的面试工作。但可惜的是,水货的概率通常都比较大,新的公司也并不相信他们的能力。尤其是这两年互联网炸了锅,猪飞的日子不再,这种情况就更加多了起来。


反过来说也一样成立,就像是xjjdog在青岛混了这么多年,一旦再杀回北上广,也一样是落的下乘的评价。


除了自身的努力之外,你在上家公司混的差,还与你在组织架构中所处于的位置和组织架构本身有关。


一般公司会有两种组织架构方式:垂直化划分层级化划分


1. 垂直划分


垂直划分,多以业务线为模型进行划分。各条业务线共用公司行政资源,相互之间关联不大。


各业务线之间,内部拥有自治权。


image.png


如上图所示,公司共有四个业务线。




  • 业务线A,有前端和后端开发。因为成员能力比较强,所以没有测试运维等职位;




  • 业务线B倡导全栈技能,开发后台前端一体化;




  • 业务线C的管理能力比较强,仅靠少量自有研发,加上大量的外包,能够完成一次性工作。




  • 业务线D是传统的互联网方式,专人专岗,缺什么招什么,不提倡内部转岗




运行模式




  1. 业务线A缺人,缺项目,与业务线BCD无任何关系,不允许借调




  2. 业务线发展良好,会扩大规模;其他业务线同学想要加入需要经过复杂的流程,相当于重新找工作




  3. 业务线发展萎靡,会缩减人员,甚至会整体砍掉。优秀者会被打散吸收进其他业务线




好处




  1. 业务线之间存在竞争关系,团队成员有明确的奋斗目标和危机意识




  2. 一条业务线管理和产品上的失败,不会影响公司整体运营




  3. 可以比较容易的形成单向汇报的结构,避免成本巨大且有偏差的多重管理




  4. 便于复制成功的业务线,或者找准公司的发展重点




坏处




  1. 对业务线主要分管领导的要求非常高




  2. 多项技术和产品重复建设,容易造成人员膨胀,成本浪费




  3. 部门之间隔阂加大,共建、合作困难,与产品化相逆




  4. 业务线容易过度自治,脱离掌控




  5. 太激进,大量过渡事宜需要处理




修订


为了解决上面存在的问题,通常会有一个协调和监管部门,每个业务线,还需要有响应的协调人进行对接。以以往的观察来看,效果并不会太好。因为这样的协调,多陷于人情沟通,不好设计流程规范约束这些参与人的行为。


image.png


在公司未摸清发展方向之前,并不推荐此方式的改革。它的本意是通过竞争增加部门的进取心,通过充分授权和自治发挥骨干领导者的作用。但在未有成功案例之前,它的结果变成了:寄希望于拆分成多个小业务线,来解决原大业务线存在的问题。所以依然是处于不太确定的尝试行为。


2. 水平划分


水平划分方式,适合公司有确定的产品,并能够形成持续迭代的团队。


它的主要思想,是要打破“不会做饭的项目经理不是好程序员”的思维,形成专人专业专岗的制度。


这种方式经历了非常多的互联网公司实践,可以说是最节约研发成本,能动性最高的组织方式。主要是因为:




  • 研发各司其职,做好自己的本职工作可以避免任务切换、沟通成本,达到整体最优




  • 个人单向汇报,组织层级化,小组扁平化。“替领导负责,就是替公司负责”




  • 任何职位有明确的JD,可替换性高,包括小组领导




这种方式最大的问题就是,对团队成员的要求都很高。主动性与专业技能都有要求,需要经过严格的面试筛选。


坏处




  • 是否适合项目类公司,存疑




  • 存在较多技术保障部门,公共需求 下沉容易造成任务积压




  • 需要对其他部门进行整合,才能发挥更大的价值




分析


image.png


如上图,大体会分为三层。




  • 技术保障,保障公司的底层技术支撑,问题处理和疑难问题解决。小组多但人少,职责分明




  • 基础业务,公司的旗舰业务团队,需求变更小但任何改动都非常困难。团队人数适中




  • 项目演化,纯项目,可以是一锤子买卖,也可以是服务升级,属于朝令夕改类需求的聚居地。人数最多




可以看到项目演化层,多是脏活,有些甚至是尝试性的项目-----这是合理的。




  1. 技术保障和基础业务的技术能力要求高,业务稳定,适合长期在公司发展,发展属性偏技术的人群,流动性小,招聘困难




  2. 项目演化层,业务多变,项目奖金或者其他回报波动大,人员流动性高,招聘容易




成功的孵化项目,会蜕变成产品,或者基础业务,并入基础业务分组。


从这种划分可以看出,一个人在公司的命运和发展,在招聘入职的时候就已经确定了。应聘人员可以根据公司的需求进行判断,提前预知自己的倾向。


互联网公司大多数将项目演化层的人员当作炮灰,因为他们招聘容易,团队组件迅速,但也有很多可能获得高额回报,这也是很多人看中的。


3.组合


组合一下垂直划分和层级划分,可以是下面这种效果。


image.png


采用层级+垂直方式进行架构。即:首选层级模式,然后在项目演化层采用垂直模式,也叫做业务线,拥有有限的自治权。


为每一个业务线配备一个与下层产品化或者技术保障对接的人员。


绩效方面,上层的需求为下层的实现打分。基础业务和技术保障,为绿色的协调人员打分。他们的利益是一致的。


End


大公司出来的并不一定是精英,小公司出来的也并不一定是渣渣。这取决于他在公司的位置和所从事的内容。核心部门会得到更多的利益,而边缘的尝试性部门只能吃一些残羹剩饭。退去公司的光环,加上平庸的项目经历,竞争力自然就打上一个折扣。


以上,仅限IT行业哦。赵家人不在此列。

作者:小姐姐味道
来源:https://juejin.cn/post/7203651773622452261

收起阅读 »

Android 手写热修复dex

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

现有的热修复框架很多,尤以AndFix 和Tinker比较多



具体的实现方式和项目引用可以参考网络上的文章,今天就不谈,也不是主要目的



今天就来探讨,如何手写一个热修复的功能



对于简单的项目,不想集成其他修复框架的SDK,也不想用第三方平台,只是紧急修复一些bug
还是挺方便的



言归正传,如果一个或多个类出现bug,导致了崩溃或者数据显示异常,如果修复呢,如果熟悉jvm dalvik 类的加载机制,就会清楚的了解 ClassLoader的 双亲委托机制 就可以通过这个


什么是双亲委托机制



  1. 当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类。
    每个类加载器都有自己的加载缓存,当一个类被加载了以后就会放入缓存,等下次加载的时候就可以直接返回了。

  2.  当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.

  3. 当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。



突破口来了,看1(如果已经加载则直接返回原来已经加载的类)
对于同一个类,如果先加载修复的类,当后续在加载未修复的类的时候,直接返回修复的类,这样bug不就解决了吗?



Nice ,多看源码和jvm 许多问题可以从framework和底层去解决


话不多说,提出了解决方法,下面着手去实现


public class InitActivity extends FragmentActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//这里默认在SD卡根目录,实际开发过程中可以把dex文件放在服务器,在启动页下载后加载进来
//第二次进入的时候可以根据目录下是否已经下载过,处理,避免重新下载
//最后根据当前app版本下载不同的修复dex包 等等一系列处理
String dexFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/fix.dex";
DexFile dexFile = null;
try {
dexFile = DexFile.loadDex(dexFilePath, null, Context.MODE_PRIVATE);
} catch (IOException e) {
e.printStackTrace();
}

patchDex(dexFile);

startActivity(new Intent(this, MainActivity.class));
}

/**
* 修复过程,可以放在启动页,这样在等待的过程中,网络下载修复dex文件
*
* @param dexFile
*/

public void patchDex(DexFile dexFile) {
if (dexFile == null) return;
Enumeration<String> enumeration = dexFile.entries();
String className;
//遍历dexFile中的类
while (enumeration.hasMoreElements()) {
className = enumeration.nextElement();
//加载修复后的类,只能修复当前Activity后加载类(可以放入Application中执行)
dexFile.loadClass(className, getClassLoader());
}
}
}
复制代码

方法很简单在启动页,或者Application中提前加载有bug的类



这里写的很简单,只是展示核心代码,实际开发过程中,dex包下载的网络请求,据当前app版本下载不同的修复dex,文件存在的时候可以在Application中先加载一次,启动页就不用加载,等等,一系列优化和判断处理,这里就不过多说明,具体一些处理看github上的代码



###ok 代码都了解了,这个 fix.dex 文件哪里来的呢
熟悉Android apk生成的小伙伴都知道了,跳过这个步骤,不懂的小伙伴继续往下看


上面的InitActivitystartActivity(new Intent(this, MainActivity.class)); 启动了一个MainActivity
看看我的MainActivity


public class MainActivity extends FragmentActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//0不能做被除数,这里会报ArithmeticException异常
Toast.makeText(this, "结果" + 10 / 0, Toast.LENGTH_LONG).show();
}
}
复制代码

哎呀不小心,写了一个bug 0 咋能做除数呢,app已经上线了,这里必崩啊,咋办
不要急,按照以下步骤:



  1. 我们要修复这个类MainActivity,先把bug解决


 Toast.makeText(this, "结果" + 10 / 2, Toast.LENGTH_LONG).show();
复制代码


  1. 把修复类生成.class文件(可以先run一次,之后在 build/intermediates/javac/debug/classes/com开的的文件夹,找到生成的class文件,也可以通过javac 命令行生成,也可以通过右边的gradle Task生成)
    class 路径图

  2. 把修复类.class文件 打包成dex (其他.class删除,只保留修复类) 打开cmd命令行,输入下面命令


D:\Android\sdk\build-tools\28.0.3\dx.bat --dex --output C:\Users\pei\Desktop\dx\fix.dex C:\Users\pei\Desktop\dx\
复制代码

D:\Android\sdk 为自己sdk目录 28.0.3build-tools版本,可以根据自己已经下载的版本更换
后面两个目录分别是生成.dex文件目录,和.class文件目录



切记 .class文件的目录必须是包名一样的,我的目录是 C:\Users\pei\Desktop\dx\com\pei\test\MainActivity.class,不然会报 class name does not match path




  1. 这样dx文件夹下就会生成fix.dex文件了,把fix.dex放进手机根目录试试吧


再次打开App,完美Toast 结果5,完美解决


总结



  1. 修复方法要在bug类之前执行

  2. 适合少量bug,太多bug影响性能

  3. 目前只能修复类,不能修复资源文件

  4. 目前只能适配单dex的项目,多dex的项目由于当前类和所有的引用类在同一个dex会 当前类被打上CLASS_ISPREVERIFIED标记,被打上这个标记的类不能引用其他dex中的类,否则就会报错
    解决办法是在构造方法里引用一个单独的dex中的类,这样不符合规则就不会被标记了
作者:one裴s
来源:https://juejin.cn/post/7203989318271483960
收起阅读 »

介绍一个令强迫症讨厌的小红点组件

@charset "UTF-8";.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:15px;overflow-x:hidden;color:#333...
继续阅读 »

前言


在 App 的运营中,活跃度是一个重要的指标,日活/月活……为了提高活跃度,就发明了小红点,然后让强迫症用户“没法活”。


image.png


小红点虽然很讨厌,但是为了 KPI,程序员也不得不屈从运营同学的逼迫(讨好),得想办法实现。这一篇,来介绍一个徽标(Badge)组件,能够快速搞定应用内的小红点。


Badge 组件


Badge 组件被 Flutter 官方推荐,利用它让小红点的实现非常轻松,只需要2个步骤就能搞定。



  1. 引入依赖


pubspec.yaml文件种引入相应版本的依赖,如下所示。


badges: ^2.0.3
复制代码


  1. 将需要使用小红点的组件使用 Badge 作为上级组件,设置小红点的位置、显示内容、颜色(没错,也可以改成小蓝点)等参数,示例代码如下所示。


Badge(
badgeContent: Text('3'),
position: BadgePosition.topEnd(top: -10, end: -10),
badgeColor: Colors.blue,
child: Icon(Icons.settings),
)
复制代码

position可以设置徽标在组件的相对位置,包括右上角(topEnd)、右下角(bottomEnd)、左上角(topStart)、左下角(bottomStart)和居中(center)等位置。并可以通过调整垂直方向和水平方向的相对位置来进行位置的细微调整。当然,Badge 组件考虑了很多应用场景,因此还有其他的一些参数:



  • elevation:阴影偏移量,默认为2,可以设置为0消除阴影;

  • gradient:渐变色填充背景;

  • toAnimate:徽标内容改变后是否启用动效哦,默认有动效。

  • shape:徽标的形状,默认是原型,也可以设置为方形,设置为方形的时候可以使用 borderRadius 属性设置圆角弧度。

  • borderRadius:圆角的半径。

  • animationType:内容改变后的动画类型,有渐现(fade)、滑动(slide)和缩放(scale)三种效果。

  • showBadge:是否显示徽标,我们可以利用这个控制小红点的显示与否,比如没有提醒的时候该值设置为 false 即可隐藏掉小红点。


总的来说,这些参数能够满足所有需要使用徽标的场景了。


实例


我们来看一个实例,我们分别在导航栏右上角、内容区和底部导航栏使用了三种类型的徽标,实现效果如下。


badge.gif


其中导航栏的代码如下,这是 Badge 最简单的实现方式了。


AppBar(
title: const Text('Badge Demo'),
actions: [
Badge(
showBadge: _badgeNumber > 0,
padding: const EdgeInsets.all(4.0),
badgeContent: Text(
_badgeNumber < 99 ? _badgeNumber.toString() : '99+',
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 11.0,
),
),
position: BadgePosition.topEnd(top: 4, end: 4),
child: IconButton(
onPressed: () {},
icon: const Icon(
Icons.message_outlined,
color: Colors.white,
),
),
),
],
),
复制代码

内容区的徽标代码如下,这里使用了渐变色填充,动画形式为缩放,并且将徽标放到了左上角,注意如果使用了渐变色那么会覆盖 badgeColor 指定的背景色。


Badge(
showBadge: _badgeNumber > 0,
padding: const EdgeInsets.all(6.0),
badgeContent: Text(
_badgeNumber < 99 ? _badgeNumber.toString() : '99+',
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 10.0,
),
),
position: BadgePosition.topStart(top: -10, start: -10),
badgeColor: Colors.blue,
animationType: BadgeAnimationType.scale,
elevation: 0.0,
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.red,
Colors.orange,
Colors.green,
],
),
child: Image.asset(
'images/girl.jpeg',
width: 200,
height: 200,
),
),
复制代码

底部导航栏的代码如下所示,这里需要注意,Badge 组件会根据内容区的尺寸自动调节大小,底部导航栏的显示控件有限,推荐使用小红点(不用数字标识)即可。


BottomNavigationBar(items: [
BottomNavigationBarItem(
icon: Badge(
showBadge: _badgeNumber > 0,
padding: const EdgeInsets.all(2.0),
badgeContent: Text(
_badgeNumber < 99 ? _badgeNumber.toString() : '99+',
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.white,
fontSize: 11.0,
),
),
position: BadgePosition.topEnd(top: -4, end: -6),
animationType: BadgeAnimationType.fade,
child: const Icon(Icons.home_outlined)),
label: '首页',
),
const BottomNavigationBarItem(
icon: Icon(
Icons.star_border,
),
label: '推荐',
),
const BottomNavigationBarItem(
icon: Icon(
Icons.account_circle_outlined,
),
label: '我的',
),
]),
复制代码

总结


本篇介绍了使用 Badge 组件实现小红点徽标组件。可以看到,Badge 组件的使用非常简单,相比我们自己从零写一个 Badge 组件来说,使用它可以让我们省时省力、快速地完成运营同学要的小红点。本篇源码已上传至:实用组件相关代码

作者:岛上码农
来源:https://juejin.cn/post/7188124857958137911

收起阅读 »

运动APP视频轨迹回放分享实现

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

喜欢户外运动的朋友一般都应该使用过运动APP(keep, 咕咚,悦跑圈,国外的Strava等)的一项功能,就是运动轨迹视频分享,分享到朋友圈或是运动群的圈子里。笔者本身平常也是喜欢户外跑、骑行、爬山等户外运动,也跑过半马、全马,疫情原因之前报的杭州的全马也延期了好几次了。回归正题,本文笔者基于自己的思想实现运动轨迹回放的一套算法策略,实现本身是基于Mapbox地图的,但是其实可以套用在任何地图都可以实现,基本可以脱离地图SDK的API。Mapbox 10 版本之后的官方给出的Demo里已经有类似轨迹回放的Case了,但是深度地依赖地图SDK本身的API,倘若在高德上实现很难可以迁移的。


这里先看下gif动图的效果,这是我在奥森跑的10KM的一个轨迹:


轨迹视频回放_AdobeExpress .gif


整个的实现包含了轨迹的回放,视频的录制,然后视频的录制这块不再笔者这篇文章的介绍的范畴内。所以这里主要介绍轨迹的回放,这个回放过程其实也是包含了大概10多种动画在里面的,辅助信息距离的文字跳转动画;距离下面配速、运动时间等的flap in 及 out的动画;播放button,底部button的渐变Visibility; 地图的缩放以及视觉角度的变化等;以上的这些也不做讨论。主要介绍轨迹回放、整公里点的显示(起始、结束), 回放过程中窗口控制等,作为主要的讲解范畴。


首先介绍笔者最开始的一种实现,假如以上轨迹List 有一百个点,每相邻的两个点做Animation之后,在AnimationEnd的Listener里开起距离下一个点的Animation,直到所有点结束,这里有个问题每次的运动轨迹的点的数量不一样,所以开起Animation的次数也不一样,整个轨迹回放的时间等于所有的Animation执行的时间和,每次动画启动需要损耗20~30ms。倘若要分享到微信朋友圈,视频的时间是限制的,但之前的那种方式时间上显然不可控,每次动画启动的损耗累加导致视频播放不完。


紧接着换成AnimationSet, 将各个线段Animation的动画放入Set里,然后playSequentially执行,同样存在上面的问题。假如只执行一次动画,那么这次动画start的损耗在整个视频播放上时长上的占比就可以忽略不计了,那如何才能将整个List的回放在一个Animation下执行完呢?假如轨迹只是一个普通的 Path,那么我们就可以基于Path的 length一个属性动画了,当转化到地图运动轨迹上去时,又如何去实现呢?


基于Path Length的属性动画



  1. 计算List对应的Path

  2. 通过PathMeasure获取 Path 的 Length

  3. 对Path做 Length的属性动画


这里有两套Point体系,一个是View的Path对应的Points, 然后就是Map上的List对应的Points,运动轨迹原始数据是Map上的List 点,上面的第一步就是将Map上的Points 转成屏幕Pixel对应的点并生成Path; 第二部通过PathMeasure 计算Path的Length; 最后在Path Length上做属性动画,然而这里并非将属性动画中每次渐变的值(这里对应的是View的Point点)绘制成View对应的Path,而是将渐变中的点又通过Map的SDK转成地图Location点,绘制地图轨迹。这里一共做了两道转换,中间只是借助View的Path做了一个依仗Length属性做的一个动画。因为基本上每种地图SDK都有Pixel 跟Location Point点互相transform的API,所以这个可以直接迁移到其它地图上,例如高德地图等。


下面具体看下代码,先将Location 转成View的Point体系,这里保存了总的一个Path,以及List 中两两相邻点对应的分段Path的一个list.



  • 生成Path:


1.1 生成Path2.png


其中用到 Mapbox地图API Location 点转View的PointF 接口API toScreenLocation(LatLng latlng), 这里生成List, 然后计算得到Path.




  • 基于Length做属性动画:


1.3 Path length 属性动画.png


首先创建属性动画的 Instance:


ValueAnimator.ofObject(new DstPathEvaluator(), 0, mPathMeasure.getLength());
复制代码

将每次渐变的值经过 calculateAnimPathData(value) 计算后存入到 以下的四个变量中,这里除了Length的渐变值,还附带有角度的一个二元组值。


dstPathEndPoint[0] = 0;//x坐标
dstPathEndPoint[1] = 0;//y坐标
dstPathTan[0] = 0;//角度值
dstPathTan[1] = 0;//角度值
复制代码

然后将dstPathEndPoint 的值转成Mapbox的 Location的 Latlng 经纬度点,


PointF lastPoint = new PointF(dstPathEndPoint[0], dstPathEndPoint[1]);
LatLng lastLatLng = mapboxMap.getProjection().fromScreenLocation(lastPoint);
Point point = Point.fromLngLat(lastLatLng.getLongitude(), lastLatLng.getLatitude());
复制代码

过滤掉一些动画过程中可能产生的异常点,最后加入到Mapbox的轨迹绘制的Layer中形成轨迹的一个渐变:


Location curLocation = mLocationList.get(animIndex);
float degrees = MapBoxPathUtil.getRotate(curLocation, point);
if (animIndex < 5 || Math.abs(degrees - curRotate) < 5) {//排除异常点
setMarkerRecord(point);
}
复制代码

setMarkerRecord(point) 方法调用加入到 Map 轨迹的绘制Layer中


1.4 加入到Map轨迹绘制.png


动画过程中,当加入到Path中的点超过一定占比时,做了一个窗口显示的动画,窗口List跟整个List的一个计算:


//这里可以取后半段的数据,滑动窗口,保持 moveCamera 的窗口值不变。
int moveSize = passedPointList.size();
List windowPassList = passedPointList.subList(moveSize - windowLength, moveSize);
复制代码

接下来看整公里点的绘制,看之前先看下上面的calculateAnimPathData()方法的逻辑


1.5 Path渐变的计算.png


如上,length为当前Path走过的距离,假设轨迹一共100点,当前走到 49 ~ 50 点之间,那么calculateLength就是0到50这个点的Path的长度,它是大于length的,offsetLength = calculateLength - length; 记录的是 当前点到50号点的一个长度offsetLength,animIndex值当前值对应50,recordPathList为一开始提到的跟计算总Path时一个分段Path的List, 获取到49 ~ 50 这个Path对应的一个model.


RecordPathBean recordPathBean = recordPathList.get(animIndex);
复制代码

获得Path(49 ~ 50) 的长度减去 当前点到 50的Path(cur ~ 50)的到 Path(49 ~ cur) 的长度


float stopD = (float) (pathMeasure.getLength() - offsetLengthCur);
复制代码

然后最终通过PathMeasure的 getPosTan 获得dstPathEndPoint以及dstPathTan数据。


pathMeasure.getSegment(0, stopD, dstPath, false);
mDstPathMeasure = new PathMeasure(dstPath, false);
//这里有个参数 tan
mDstPathMeasure.getPosTan(mDstPathMeasure.getLength(), dstPathEndPoint, dstPathTan);
复制代码


  • 整公里点的绘制


原始数据中的List的Location中存储了一个字段kilometer, 当某个Location是整公里点时该字段就有对应的值,每次Path属性渐变时,上面的逻辑里记录了lastAnimIndex, animIndex。当 animIndex > lastAnimIndex时, 上面的calculateAnimPathData() 方法里分析animIndex有可能还没走到,所以在animIndex > lastAnimIndex时lastAnimIndex肯定走到了。


1.6 整公里点动画.png


当lastAnimIndex对应的点是 整公里时,做一个响应的属性动画。


至此,运动轨迹回放的一个动画执行逻辑分析完了,如文章开始所说,整个过程中其实还包含了好多种其它的动画,处理它们播放的一个时序问题,如何编排实现等等也是一个难点。另外还就是轨迹播放时的一个Camera的一个视觉跟踪的效果没有实现,这个用地图本身的Camera 的API是一种实现,但是如何跟上面的这些结合到一块;然后就是自行通过计算角度偏移,累计到一定的旋转角度时,转移地图的指南针;以上是笔者想到的方案,以上有计算角度的,但需要找准那个累计的角度值,然后大量实际数据适配。


最后,有需要了解轨迹回放功能其它实现的,可留言或私信笔者进行一起探讨。

作者:cxy107750
来源:https://juejin.cn/post/7183602475591548986

收起阅读 »

Android再探全面屏适配

.markdown-body{color:#383838;font-size:15px;line-height:30px;letter-spacing:2px;word-break:break-word;font-family:-apple-system,Bl...
继续阅读 »

前言


简单来说,以前是做app的,然后转去做了终端几年,现在又做回了app,然后就涉及到了全面屏的适配,但是很多年前做的适配也不记得了,所以来重新再探究一遍。


以前做终端的时候,适配?我不知道什么叫适配,就一个机型,想怎么玩就怎么玩,自己就是爹。现在做应用,不好意思,手机厂商才是大爹,我们都是孙子。


我简单的回顾了一下,其实全面屏的适配一开始是因为刘海屏才开始这条路线,然后就出现一大堆奇奇怪怪的东西。幸好谷歌也是做人,在28之后就提出一套规范。


Android P前后


对于Android P,其实也就android 8.0和android 9.0两个版本,因为是从android 8.0开始流行的,各做各的,然后在9.0的时候google给出了一套规范。


对于Android 9.0也就是28,google推出了DisplayCutout,它统一了android凹凸屏的处理,使用起来也很方便。


WindowManager.LayoutParams wlp = getWindow().getAttributes();
wlp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
getWindow().setAttributes(wlp);
复制代码

给WindowManager.LayoutParams设置layoutInDisplayCutoutMode就行,是不是很简单。

它有几个参数可供选择


(1)LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT:默认值,一般效果和LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER相同。

(2)LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES:内容显示到凹凸屏区域。

(3)LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER:内容不会显示到凹凸屏区域。


对于Android 28以下的适配


这个比较麻烦,因为在28以下是没有layoutInDisplayCutoutMode的,所以要单独去调,网上也有很多说如何去对不同的厂商去做适配,但其实这东西还是要调的。哪怕你是相同的机型,不同的系统版本都可能会产生不同的效果,没错,就是这么恐怖。基本都是只能做if-else单独对不同的机型做适配。要么就是让28以下的统一不做全面屏的效果,比如说把内容显示到凹凸屏区域,你就判断在28的时候不做这种操作,但一般不是你说的算,多多少少还是需要做适配,只能具体情况具体调试。


对不同的场景做适配


你觉得你说你就对28做适配,28以下就不管了,我就设置layoutInDisplayCutoutMode一行代码就行。可事情哪有这么简单。


系统的Bar主要分为3种,一种是在屏幕上方的状态栏,一种是在屏幕底端的导航栏,还是一直是仿IOS的底部横条代替导航栏,这在和导航栏一起分析但会有些许不同。


而这个过程中又会区分为横屏和竖屏的情况,多少也会又些许差异,当然我也没办法把全部特殊的常见列举出来。不同的手机厂商之间也会存在有不同的情况,还有上面说的android28前后,这里主要是对android28之后进行分析。


状态栏


假如要实现全屏显示的效果,我们要如何去对状态栏做适配。


为了方便调试,我把window的颜色设置为橙色,把布局的颜色设置成绿色

作者:流浪汉kylin

来源:juejin.cn/post/7201332537338806328

收起阅读 »

Android自定义View绘制进阶-水波浪温度刻度表

.markdown-body{color:#595959;font-size:15px;font-family:-apple-system,system-ui,BlinkMacSystemFont,Helvetica Neue,PingFang SC,Hira...
继续阅读 »

波浪形温度刻度表实现


前言


之前的绘制圆环,我们了解了如何绘制想要的形状和进度的一些特点,那么此篇文章我们更近一步,绘制一个稍微复杂一点的刻度与波浪。来一起复习一下Android的绘制。


相对应的这种类型的自定义View网上并不少见,但是如果我们要做一些个性化的效果,最好还是自己绘制一份,也相对的比较容易控制效果,如果想实现上面的效果,我们一般来说分为以下几个步骤:



  1. 重写测量方法,确保它是一个正方形

  2. 绘制刻度

  3. 绘制中心的圆与文字

  4. 水波纹的动画

  5. 设置进度与动画,一起动起来


思路我们已经有了,下面一步一步的来实现吧。


话不多说,Let's go


300.png


1、onMeasure重新测量


之前的圆环进度,我们并没有重写 onMeasure 方法,而是在布局中指定为固定的宽高,其实兼容性和健壮性并不好,万一写错了就会变形导致显示异常。


最好的办法是不管xml中设置为什么值,这里都能保证为一个正方形,要么是取宽度为准,让高度和宽度一致,要么就是宽度高度取最大值,让他们保持一致。由于我们是竖屏的应用,所以我就取宽度为准,让高度和宽度一致。


前面我们只是讲了 onDraw 并没有讲到 onMeasure , 这里简单的说一下。


我们为什么要重写 onMeasure ?



  1. 为了自定义View尺寸的规则,如果你的自定义View的尺寸是根据父控件行为一致,就不需要重写onMeasure()方法。

  2. 如果不重写onMeasure方法,那么自定义view的尺寸默认就和父控件一样大小,当然也可以在布局文件里面写死宽高,而重写该方法可以根据自己的需求设置自定义view大小。


一般来说我们重写的 onMeasure 长这样:


 override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec,heightMeasureSpec)
}
复制代码

widthMeasureSpec ,heightMeasureSpec 并不是真正的宽高,看名字就知道,它只是宽高测量的规格,我们通过 MeasureSpec 的一些静态方法,通过它们拿到一些信息。


static int getMode(int measureSpec):根据提供的测量值(规格)提取模式(上述三个模式之一)


测量的 Model 一共有三种



  1. UNSPECIFIED(未指定),父元素部队自元素施加任何束缚,子元素可以得到任意想要的大小;

  2. EXACTLY(完全),父元素决定自元素的确切大小,子元素将被限定在给定的边界里而忽略它本身大小;

  3. AT_MOST(至多),子元素至多达到指定大小的值。


我们常用的就是 EXACTLY 和 AT_MOST ,EXACTLY 对应的就是我们设置的match_parent或者300这样的精确值,而 AT_MOST 对应的就是wrap_content。


static int getSize(int measureSpec):根据提供的测量值(规格)提取大小值(这个大小也就是我们通常所说的大小)


通过此方法就能获取控件的宽度和高度值。


static int makeMeasureSpec(int size,int mode):根据提供的大小值和模式创建一个测量值(规格)


通过具体的宽高和model,创建对应的宽高测量规格,用于确定View的测量


onMeasure 的最终设置确定宽度的测量有两种方式,



  1. setMeasuredDimension(width, height)

  2. super.onMeasure(widthMeasureSpec,heightMeasureSpec)


实战:


比如我们的自定义温度刻度View,我们整个View要确保一个正方形,那么就拿到宽度,设置同样的高度,然后确定测量,流程如下:


    //重新测量
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//获取控件的宽度,高度
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int newWidthMeasureSpec = widthMeasureSpec;

//如果没有指定宽度,默认给200宽度
if (widthMode != MeasureSpec.EXACTLY) {
newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(200, MeasureSpec.EXACTLY);
}

//获取到最新的宽度
int width = MeasureSpec.getSize(newWidthMeasureSpec) - getPaddingLeft() - getPaddingRight();

//我们要的是矩形,不管高度是多高,让它总是和宽度一致
int height = width;

centerPosition.x = width / 2;
centerPosition.y = height / 2;
radius = width / 2f;
mRectF.set(0f, 0f, width, height);


//最后设置生效-下面两种方式都可以
// setMeasuredDimension(width, height);

super.onMeasure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
);

}
复制代码

这里有详细的注释,大致实现的效果如下:


image.png


2、绘制刻度


由于原本的 Canvas 内部没有绘制刻度这么一说,所以我们只能用绘制线条的方式,就是 drawLine 方法。


为了了解到坐标系和方便实现,我们可以先绘制一个圆环,定位我们刻度需要绘制的位置。


    @Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

//画圆环
canvas.drawArc(
mRectF.left + 2f, mRectF.top + 2f, mRectF.right - 2f, mRectF.bottom - 2f,
mStartAngle, mSweepAngle, false, mDegreeCirPaint
);
}
复制代码

这个圆环是之前讲到过了,就不过多赘述了,实现效果如下:


image.png


由于开始绘制的地方在左上角位置,我们要移动到圆的中心点开始绘制,也就是红色点移动到蓝色点。


我们就需要x轴和y轴做一下偏移 canvas.translate(radius, radius);


默认的 drawLine 都是横向绘制,我们想要实现效果图的效果,就需要旋转一下画笔,也就是用到 canvas.rotate(rotateAngle);


那么旋转多少了,如果说最底部是90度,我们的起始角度是120度开始的,我们就起始旋转30度。后面每一次旋转就按照百分比来,比如我们100度的温度,那么就相当于要画100个刻度,我们就用需要绘制的角度除以100,就是每一个刻度的角度。


具体的刻度实现代码:



private float mStartAngle = 120f; // 圆弧的起始角度
private float mSweepAngle = 300f; //绘制的起始角度和滑过角度(绘制300度)
private float mTargetAngle = 300f; //刻度的角度(根据此计算需要绘制有色的进度)

private void drawDegreeLine(Canvas canvas) {
//先保存
canvas.save();

// 移动画布
canvas.translate(radius, radius);
// 旋转坐标系,需要确定旋转角度
canvas.rotate(30);

// 每次旋转的角度
float rotateAngle = mSweepAngle / 100;
// 累计叠加的角度
float currentAngle = 0;
for (int i = 0; i <= 100; i++) {

if (currentAngle <= mTargetAngle && mTargetAngle != 0) {
// 计算累计划过的刻度百分比
float percent = currentAngle / mSweepAngle;

//动态的设置颜色
mDegreelinePaint.setColor(evaluateColor(percent, Color.GREEN, Color.RED));

canvas.drawLine(0, radius, 0, radius - 20, mDegreelinePaint);

// 画过的角度进行叠加
currentAngle += rotateAngle;

} else {
mDegreelinePaint.setColor(Color.WHITE);
canvas.drawLine(0, radius, 0, radius - 20, mDegreelinePaint);
}

//画完一个刻度就要旋转移动位置
canvas.rotate(rotateAngle);
}

//再恢复
canvas.restore();

}
复制代码

加上圆环与刻度的效果图:
image.png


3. 设置刻度动画


前面的一篇我们使用的是属性动画不停的绘制从而实现进度的效果,那么这一次我们使用定时任务的方式也是可以实现动画的效果。


由于我们之前的 drawDegreeLine 方法内部控制绘制进度的变量就是 targetAngle 来控制的,所以我们通过入口方法设置温度的时候通过定时任务的方式来控制。


代码如下:



//动画状态
private boolean isAnimRunning;
// 手动实现越来越慢的效果
private int[] slow = {10, 10, 10, 8, 8, 8, 6, 6, 6, 6, 4, 4, 4, 4, 2};
// 动画的下标
private int goIndex = 0;

//设置温度,入口的开始
public void setupTemperature(float temperature) {
mCurPercent = 0f;
totalAngle = (temperature / 100) * mSweepAngle;
targetAngle = 0f;
mCurPercent = 0f;
mCurTemperature = "0.0";
mWaveUpValue = 0;

startTimerAnim();
}

//使用定时任务做动画
private void startTimerAnim() {

if (isAnimRunning) {
return;
}

mAnimTimer = new Timer();
mAnimTimer.schedule(new TimerTask() {

@Override
public void run() {

isAnimRunning = true;
targetAngle += slow[goIndex];
goIndex++;
if (goIndex == slow.length) {
goIndex--;
}
if (targetAngle >= totalAngle) {
targetAngle = totalAngle;
isAnimRunning = false;
mAnimTimer.cancel();
}

// 计算的温度
mCurPercent = targetAngle / mSweepAngle;
mCurTemperature = mDecimalFormat.format(mCurPercent * 100);

// 水波纹的高度
mWaveUpValue = (int) (mCurPercent * (mSmallRadius * 2));

postInvalidate();
}
}, 250, 30);

}
复制代码

那么刻度动画的效果如下:


rote-02.gif


4. 绘制中心的圆与文字


我们再动画中记录动画的百分比进度,和动画当前的温度。


    ...    
// 计算的温度
mCurPercent = targetAngle / mSweepAngle;
mCurTemperature = mDecimalFormat.format(mCurPercent * 100);

postInvalidate();

...
复制代码

我们记录一下小圆的半径和文本的画笔资源


   private float mSmallRadius = 0f;
private Paint mTextPaint;
private Paint mSmallCirclePaint;
private float mCurPercent = 0f; //进度
private String mCurTemperature = "0.0";
private DecimalFormat mDecimalFormat;

private void init() {
...

mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setColor(Color.WHITE);

mSmallCirclePaint = new Paint();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

...

//画小圆
drawSmallCircle(canvas, evaluateColor(mCurPercent, Color.GREEN, Color.RED));

//画中心的圆与文本
drawTemperatureText(canvas);

}
复制代码

具体的文本与小圆的绘制


    private void drawSmallCircle(Canvas canvas, int evaluateColor) {
mSmallCirclePaint.setColor(evaluateColor);
mSmallCirclePaint.setAlpha(65);
canvas.drawCircle(centerPosition.x, centerPosition.y, mSmallRadius, mSmallCirclePaint);
}

private void drawTemperatureText(Canvas canvas) {

//提示文字
mTextPaint.setTextSize(mSmallRadius / 6f);
canvas.drawText("当前温度", centerPosition.x, centerPosition.y - mSmallRadius / 2f, mTextPaint);

//温度文字
mTextPaint.setTextSize(mSmallRadius / 2f);
canvas.drawText(mCurTemperature, centerPosition.x, centerPosition.y + mSmallRadius / 4f, mTextPaint);

//绘制单位
mTextPaint.setTextSize(mSmallRadius / 6f);
canvas.drawText("°C", centerPosition.x + (mSmallRadius / 1.5f), centerPosition.y, mTextPaint);

}
复制代码

由于进度和温度都是动画在 invalidate 之前赋值的,所以我们的文本和小圆天然就支持动画的效果了。


效果如下:


rote-03.gif


5. 水波纹动画


水波纹的效果,我们不能直接用 Canvas 来绘制,我们可以用刻度的方法用 drawLine的方式来绘制,如何绘制呢?相信大家也有了解,就是正弦函数了。


由于我们的效果是两个水波纹相互叠加起起伏伏的效果,所以我们定义两个函数。


总体的思路是:我们定义两个数组来管理我们的Y轴的值,通过正弦函数给Y轴赋值,然后在drawLine的时候取出对应的x轴的y值就可以绘制出来。


x轴其实就是我们的控件宽度,我们先用一个数组保存起来


    private float[] mFirstWaterLine;
private float[] mSecondWaterLine;

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

//获取控件的宽度,高度
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int newWidthMeasureSpec = widthMeasureSpec;

//如果没有指定宽度,默认给200宽度
if (widthMode != MeasureSpec.EXACTLY) {
newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(200, MeasureSpec.EXACTLY);
}

//获取到最新的宽度
int width = MeasureSpec.getSize(newWidthMeasureSpec) - getPaddingLeft() - getPaddingRight();

//我们要的是矩形,不管高度是多高,让它总是和宽度一致
int height = width;


mFirstWaterLine = new float[width];
mSecondWaterLine = new float[width];


super.onMeasure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
);

}

复制代码

然后我们再绘制之前就先对x轴对应的y值赋值,然后绘制的时候就取出对应的y值来 drawLine,具体的代码如下:


动画的时候先对横向运动和垂直运动的变量做一个赋值:


    private int mWaveUpValue = 0;
private float mWaveMoveValue = 0f;


//使用定时任务做动画
private void startTimerAnim() {

if (isAnimRunning) {
return;
}
mAnimTimer = new Timer();
mAnimTimer.schedule(new TimerTask() {

@Override
public void run() {

...

// 计算的温度
mCurPercent = targetAngle / mSweepAngle;
mCurTemperature = mDecimalFormat.format(mCurPercent * 100);

// 水波纹的高度
mWaveUpValue = (int) (mCurPercent * (mSmallRadius * 2));

postInvalidate();
}
}, 250, 30);

}

public void moveWaterLine() {
mWaveTimer = new Timer();
mWaveTimer.schedule(new TimerTask() {

@Override
public void run() {
mWaveMoveValue += 1;
if (mWaveMoveValue == 100) {
mWaveMoveValue = 1;
}
postInvalidate();
}
}, 500, 200);
}
复制代码

拿到了对应的变量值之后,然后开始绘制:


 /**
* 绘制水波
*/

private void drawWaterWave(Canvas canvas, int color) {

int len = (int) mRectF.right;

// 将周期定为view总宽度
float mCycleFactorW = (float) (2 * Math.PI / len);

// 得到第一条波的峰值
for (int i = 0; i < len; i++) {
mFirstWaterLine[i] = (float) (10 * Math.sin(mCycleFactorW * i + mWaveMoveValue) - mWaveUpValue);
}
// 得到第一条波的峰值
for (int i = 0; i < len; i++) {
mSecondWaterLine[i] = (float) (15 * Math.sin(mCycleFactorW * i + mWaveMoveValue + 10) - mWaveUpValue);
}

canvas.save();

// 裁剪成圆形区域
Path path = new Path();
path.addCircle(len / 2f, len / 2f, mSmallRadius, Path.Direction.CCW);
canvas.clipPath(path);
path.reset();

// 将坐标系移到底部
canvas.translate(0, centerPosition.y + mSmallRadius);

mSmallCirclePaint.setColor(color);

for (int i = 0; i < len; i++) {
canvas.drawLine(i, mFirstWaterLine[i], i, len, mSmallCirclePaint);
}
for (int i = 0; i < len; i++) {
canvas.drawLine(i, mSecondWaterLine[i], i, len, mSmallCirclePaint);
}

canvas.restore();

}
复制代码

一个是对Y轴赋值,一个是取出x轴对应的y轴进行绘制,这里需要注意的是我们裁剪出了一个小圆的图形,并且覆盖在小圆上面实现出效果图的样子。


运行的效果如下:


rote-04.gif


要记得对定时器进行资源你的关闭哦。


    @Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mWaveTimer != null) {
mWaveTimer.cancel();
}
if (mAnimTimer != null && isAnimRunning) {
mAnimTimer.cancel();
}
}
复制代码

使用的时候我们只需要设置温度即可开始动画。


       findViewById(R.id.set_progress).click {

val temperatureView = findViewById(R.id.temperature_view)
temperatureView .setupTemperature(70f)
}
复制代码

后记


由于是自用定制的,本人也比较懒,所以并没有对一些配置的属性做自定义属性的抽取,比如圆环的间距,大小,颜色,波纹的间距,动画的快慢等等。


内部加了一点点测量的用法,但是主要还是绘制的流程,基本上把常用的几种绘制方式都用到了。以后有类似的效果大家也可以按需修改即可。


由于是自用的一个View,相对圆环进度没有那么多场景使用,就没有抽取出来上传到Maven,如果大家有兴趣可以查看源码点击【传送门】


同时,你也可以关注我的这个Kotlin项目,我有时间都会持续更新。


惯例,我如有讲解不到位或错漏的地方,希望同学们可以指出交流。


如果感觉本文对你有一点点的启发,还望你能点赞支持一下,你的支持是我最大的动力。


Ok,这一期就此完结。


作者:newki
来源:https://juejin.cn/post/7166151382154608670

收起阅读 »

不过是享受了互联网的十年红利期而已。

.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:16px;overflow-x:hidden;color:#252933}.markdown-bod...
继续阅读 »

你好呀,我是歪歪。


去年的最后一天,我在 B 站发布了这个视频:



我真没想到这个长达一个小时的视频的播放量能到这么多,而且居然是一个投币比点赞和收藏还多的视频。


评论区加上弹幕,有上千条观众的留言。每一条留言都代表一个观众的感受,里面极大部分的感受总结起来大多是表示对于我个人这十年经历感叹和羡慕,但是我是觉得十年的时间这么长,谁还不能提炼出几两故事和几段感悟呢?


觉得感叹的,只不过是在我的视频里面看到了几分自己的影子。觉得羡慕的,只不过是以另外一种我羡慕的方式生活着。


还是有人说是逆袭,我并不觉得这是逆袭。逆袭一般是说绝地反击的故事,但是我觉得这十年,我还没有真正的走到过“绝地”,更多的只是随着时代随波逐流,我个人的努力,在时代的浪潮前,微不足道,只不过在一系列的机缘巧合之下,我使劲的方向恰好和浪潮的方向一致而已。


我当时没有想到一个好的词语来形容这个“和浪潮的方向一致”,所以过年期间我也一直在仔细的思考这个问题。


直到过年期间,我坐在火炉前听家里的长辈聊天,一个长辈问另外一个晚辈:你什么时候把女朋友带回来给我们看看呢?


晚辈戏谑的回答说:我现在自己都过的不好呢,怕是没有女孩子愿意跟我哦。


长辈说:我以前嫁过来的时候,你爷爷以前还不是啥都没有,就一间土巴屋,一个烂瓦房。结婚嘛,两个人一起努力不就把日子过好了。


我当时好想说一句:那个时代过去了,现在不一样了。


然而终究还是没说出口,但是就在把这句话咽下去的瞬间,我想明白了前面关于“浪潮”的问题,其实就一句话:


我只不过是有幸享受到了时代的红利罢了。有时候的看起来让人羡慕的人、成功的人,只不过是享受到了时代的红利罢了,与个人的努力程度关系真的不大。


我说的时代的红利,就是互联网技术、计算机专业野蛮发展的这十年光景。


在视频里面,我说了一句话:我是被调剂到计算机专业的。


然后有一些弹幕表示非常的震惊:



是的,在 2012 年,计算机专业并不是一个被看好的热门专业,甚至有点被“淘汰”的感觉。


我记得那一年录取之后,给一个亲戚说是就读计算机专业,他说:怎么学了个这专业啊,以后每个家庭里面都会有一台计算机,到时候人人都会使用计算机,还学它干啥?


这句话虽然现在看起来很搞笑,但是在那个时候,我还没有接触到它的时候,我觉得很有道理。


虽然我是调剂到“计算机”的,但是前提也得是我填报志愿的时候填写了“计算机专业”,对吧。


所以问题就来了:我当年是怎么高瞻远瞩,怎么深思熟虑,怎么推演计算,怎么预测未来,想着要填报一个计算机专业呢?


为了回答这个问题,我今年回到老家,专门把这个东西翻了出来:



这是我高考结束那年,学校发的 4 本和填报志愿相关的书,书里面主要的内容就是过去三年各个批次,各个学校,各个专业的报考人数、录取人数、录取最低分数、录取平均分数、录取最高分数的信息统计:



我当年填报志愿,就是通过翻阅这四本书来找到自己可以填报的大学。但是我的高考志愿特别好填,因为我高考成绩只超过二本线 13 分,所以我直接看二本院校里面每年卡着分数线招收学生的学校就可以了。在这个条件下,没有多少学校可以选择。


最后录取我的大学,是 2012 年那一年刚刚由专科学校升级为二本院校的一所大学。所以那一年是它第一次招本科生,没有过往的数据可以参考,报它的原因是因为我感觉它刚刚从专科升级为本科,录取分数应该不会太高。


填报志愿的时候一个学校可以填写六个专业,刚好它也只有六个本科专业,所以我就按照报纸上的顺序,挨个填写,而且还勾选了“服从调剂”。


而这六个专业,我也通过前面的书翻到了:



当时对于这六个专业,我完全没有任何了解。根本不知道要学习什么内容,更加不知道毕业之后会从事什么工作。


后来入校之后我才知道,报材料成型及控制工程和机械电子工程专业的人最多,计算机科学与技术由于报的人没有报满,所以我被调剂过去了。


可以肯定的说,如果当年这个学校没有计算机的本科专业,我就不会走上计算机的道路。


其实我填报志愿的状态,和当年绝大部分高考学生的状态一样,非常的茫然。在高中,学校也只教了高考考场上要考的东西,为了这点东西,我们准备了整整三年。但是现在回头去看,如何填报志愿,其实也是一件非常值得学习了解的事情,而不是高考结束之后,学校发几本资料就完事的。


我当时填报志愿时最核心的想法是,只要有大学录取就行了,至于专业什么的,不重要。


在志愿填报指南的书里面,我发现有大量的篇幅站在 2012 年视角去分析未来的就业形势。



这部分,我仔细的读了一遍,发现关于计算机部分提到的并不多,只有寥寥数语,整体是持看好态度,但是大多都是一些正确的“废话”,对于当年的我来说,很难提炼出有价值的信息,来帮助我填写志愿。


后来得知被计算机录取了之后的第一反应是,没关系,入校之后可以找机会转专业,比如转到机械。


为什么会想着机械呢?


因为那一年,或者说那几年,最火的专业是土木工程,紧随其后的大概就是机械相关的专业:



而这个学校没有土木专业,那就是想当然的想往人多的,也是学校的王牌专业“机械”转了。


计算机专业,虽然也榜上有名,但是那几年的风评真的是非常一般,更多的是无知,就像是亲戚说的那句:以后人人都有一台计算机,你还去学它干啥?


我也找到了一份叫做《2011年中国大学生就业报告》的报告,里面有这样一句话:



真的如同弹幕里面一个小伙伴说的:土木最火,计算机下水道。


所以我在十年前被调剂到计算机专业,也就不是一个什么特别奇怪的事情了。


你说这是什么玩意?


这里面没有任何的高瞻远瞩、深思熟虑、推演计算、预测未来,就是纯粹的运气。


就是恰好站在时代的大潮前,撅着屁股,等着时代用力的拍上那么一小下,然后随着浪花飘就完事了吗?


我也曾经想过,如果我能把它包装成一个“春江水暖鸭先知”的故事,来体现我对于未来精准的预判就好了,但是现实情况就是这么的骨感和魔幻,没有那么多的预判。


所以有很多人,特别是一些在校的或者刚刚毕业的大学生,通过视频找到我,来请教我关于职业发展,关于未来方向,关于人生规划的问题。



说真的,我有个屁的资格和能力来帮你分析这些问题啊。我自己这一摊子事情都没有搞清楚,我的职业前路也是迷雾重重,我何德何能给别人指出人生的方向?


当然,我也能给出一些建议,但是我能给出的所有的回复,纯粹是基于个人有限的人生阅历和职业生涯,加上自己的一些所见所闻,给出的自己角度的回答。


同样的问题,你去问另外一个人,由于看问题的角度不同,可能最终得出的答案千差万别。


甚至同样的职场相关的问题,我可以给你分析的头头是道,列出一二三四点,然后说出每一点的利益得失,但是当我在职场上遇到一模一样的问题时,我也会一时慌张,乱了阵脚,自然而然的想要去寻求帮助。


在自媒体的这三年,我写过很多观点输出类的文章,也回答过无数人的“迷茫”。对于这一类求助,有时是答疑,常常是倾听,总是去鼓励。


我并不是一个“人生导师”,或者说我目前浅薄的经验,还不足以成为一个“人生导师”,我只不过是一个有幸踩到了时代红利的幸运儿而已。


在这十年间,我踩到了计算机的红利,所以才有了后面看起来还算不错的故事。


踩到了 Java 的红利,所以才能把这个故事继续写下去。


踩到了自媒体的红利,所以才有机会把这些故事写出来让更多的人看到。


现在还有很多很多人摩肩擦踵的往计算机行业里面涌进来,我一个直观的感受就是各种要求都变高了,远的就不说了,如果是三年前我回到成都的时候,市场情况和现在一样的话,我是绝对不可能有机会进入到现在这家公司,我只不过是恰好抓住了一个窗口期而已。


还有很多很多的人,义无反顾的去学 Java,往这个卷得没边的细分领域中冲的不亦乐乎,导致就业岗位供不应求,从而企业提升了面试难度。我记得 2016 年我毕业的时候,在北京面试,还没有“面试造火箭”的说法,当年我连 JVM 是啥玩意都不知道,更别提分布式相关的技术了,听都没听过。然而现在,这些都变成了“基础题”。


还有很多人,看到了自媒体这一波流量,感觉一些爆款文章,似乎自己也能写出来,甚至写的更好。或者感觉一些非常火的视频,似乎自己也能拍出来,甚至拍的跟好。


然而真正去做的话,你会发现这是一条“百死一生”的道路,想要在看起来巨大的流量池中挖一勺走,其实很难很难。


但是如果把时间线拉回到 2014 年,那是公众号的黄金时代,注册一个公众号,每天甚至不需要自己写文章,去各处搬运转载,只需要把排版弄好看一点,多宣传宣传,然后坚持下去,就能积累非常可观的关注数量,有关注,就有流量。有流量,就有钱来找你。从一个公众号,慢慢发展为一个工作室,然后成长为一个公司的故事,在那几年,太多太多了。


诸如此类,很多很多的现象都在表明则一个观点:时代不一样了。


我在刚刚步入社会的时候,看过一本叫做《浪潮之巅》的书,书里面的内容记得不多了,但是知道这是一本把计算机领域中的一些值得记录的故事写出来的好书。


虽然书的内容记得不多了,但是书的封面上写的一段话我就很喜欢。


就用它来作为文章的结尾吧:



近一百多年来,总有一些公司很幸运地、有意识或者无意识地站在技术革命的浪尖之上。一旦处在了那个位置,即使不做任何事,也可以随着波浪顺顺当当地向前漂个十年甚至更长的时间。在这十几年间,它们代表着科技的浪潮,直到下一波浪潮的来临。这些公司里的人,无论职位高低,在外人看来,都是时代的幸运儿。因为,虽然对一个公司来说,赶上一次浪潮不能保证其长盛不衰;但是,对一个人来说,一生赶上一次这样的浪潮就足够了。一个弄潮的年轻人,最幸运的,莫过于赶上一波大潮。



以上。








如果我这篇文章结束在这个地方,那么你先简单的想一想,你看完之后那一瞬间之后的感受是什么?


会不会有一丝丝的失落感,或者说是一丢丢的焦虑感?


是的,如果我的文章就结束在这个地方,那么这就是一篇试图“贩卖焦虑”的文章。


我在不停的暗示你,“时代不一样了”,“还是以前好啊”,“以前做同样的事情容易的多”。


这样的暗示,对于 00 后、90 后的人来说,极小部分感受是在缅怀过去,更多的还是让你产生一种对当下的失落感和对未来的焦虑感。


比如我以前看到一些关于 90 年代下海经商的普通人的故事。就感觉那个时代,遍地是黄金,处处是机会,只要稍稍努力就能谱写一个逆天改命的故事,继而感慨自己的“生不逢时”。


只是去往回看过去的时代,而没有认真审视自己的时代,当我想要去形容我所处的时代的时候,负面的形容词总是先入为主的钻进我的脑海中。


我之前一直以为是运气一直站在我这边,但是我真的是发布了前面提的到视频,然后基于视频引发了一点讨论之后,我才开始更加深层次的去思考这个问题,所以我是非常后知后觉的才感受到,我运气好的大背景是因为遇到了时代的红利。


要注意前面这一段话,我想强调的是“后知后觉”这个词。这个词代表的时间,是十年有余的时间。


也就是说在这十年有余的时间中,我没有去刻意的追求时代的红利、也没有感知到时代的红利。


这十年间,概括起来,我大部分时间只是做了一件事:努力成长,提升自我。


所以在我的视频的评论区里面还有一句话出现的频率特别高:越努力,越幸运。


我不是一个能预判未来的人,但是我并不否认,我是一个努力的人,然而和我一样努力,比我更加努力的人也大有人在。


你要坚信,你为了自己在社会上立足所付出的任何努力是不可能会白费的,它一定会以某种形式来回报你。


当回报到来的时候,也许你认为是运气,其实是你也正踩在时代的红利之上,只不过还没到你“后知后觉”的时候,十年后,二十年后再看看吧。


在这期间,不要囿于过去,不要预测未来,你只管努力在当下就好了。迷茫的时候,搞一搞学习,总是没错的。



(特么的,这味道怎么像是鸡汤了?不写了,收。)



最后,用我在网上看的一句话作为结尾吧:



我未曾见过一个早起、勤奋,谨慎,诚实的人抱怨命运不公;我也未曾见过一个认真负责、努力好学、心胸开阔的年轻人,会一直没有机会的。



以上就是我对于处于“迷茫期”的一些大学生朋友的一点点个人的拙见,也是我个人的一些自省。


共勉。

作者:why技术
来源:https://juejin.cn/post/7193678951670087739

收起阅读 »

做软件开发20年,我学到的20件事

写在前面 你即将读的这篇文章会给你很多建议,向前辈学习对于获得成功是很有帮助的,但我们经常会忘记很重要的一点,就是几乎所有的建议都是有其特定场景和上下文的,但当人们给出建议的时候却很少带着上下文。 一家一直以来以“收费低”而获得成功,并经营了二十年的公司可能会...
继续阅读 »

写在前面

 你即将读的这篇文章会给你很多建议,向前辈学习对于获得成功是很有帮助的,但我们经常会忘记很重要的一点,就是几乎所有的建议都是有其特定场景和上下文的,但当人们给出建议的时候却很少带着上下文。 一家一直以来以“收费低”而获得成功,并经营了二十年的公司可能会给出的建议是“你只需要多收些钱!”。 “你需要把所有应用都构建成微服务”这个建议可能来自于一个靠快速构建的单体应用获得成千上万客户,在开始遇到规模问题时转向微服务的团队。

 如果不理解上下文,这些建议就毫无意义,甚至是有害的。如果这些人早些时候听从了自己的建议,那结果如何也很难讲。我们很可能处在自己经历的顶峰,但却在用现在的视角看待别人的问题。

 首先我想介绍一下我的建议从哪儿来,我职业生涯的前半段是一名软件工程师,为各种小型企业和初创企业工作,然后进入咨询行业,并在一些非常大的企业工作。后来自己创建了Simple Thread,团队从2人发展到25人。10年前,我们主要与中小型企业合作,现在与大大小小不同的企业合作。

我的建议来自于这些人:

  1. 几乎总是在小而精干的团队中,必须用很少的资源做很多事情。

  2. 重视可工作软件而不是特定的工具。

  3. 在维护多个系统的同时,一直有新的项目要启动。

  4. 把工程师的生产力看得比大多数其他因素都重要。 我过去20年的经历塑造了我对软件的看法,并引导我形成了一些信念,我试图将这些信念精简并整理成一个列表,希望你会觉得它对你有所帮助。

我的列表

1.“我依然知道的不够多”

 “你怎么会不知道BGP是什么?“ “你从来没听说过Rust吗?”。我们很多人经常听到过类似的话。很多人喜欢软件开发的一个重要的原因是我们是终身学习者,软件开发中,无论你从哪个方向看,都有广阔的知识前景在各个方向发展,并且每天都在扩大。这意味着与其他职业中花费几十年的人相比,你即使已经花费了数十年,但可能仍然有巨大的知识断层,有很多新知识需要学习,你可能因为担心不能胜任而陷入焦虑。你越早意识到这一点,你就能越早摆脱这种时常的焦虑,从而放平心态,乐于向别人学习以及教授他人。

2.软件最难的部分是构建正确的东西

 我知道这已经是陈词滥调了,但是还是有很多软件工程师不相信这一点,因为他们认为这会贬低他们所做的工作。我个人认为这是无稽之谈。相反,它强调了我们工作环境的复杂性和非理性,这更突出了我们所面临的挑战。你可能可以设计出全世界技术上最牛的东西,但却没有人愿意使用它,这种事经常发生。设计软件主要是一种倾听活动,我们经常不得不一半是软件工程师,一半是心理学家,一半是人类学家。在这个设计过程中投资自己,无论是通过专门的用户体验团队的成员还是通过简单的自学,都会带来巨大的回报。因为构建错误软件的成本可不仅仅是浪费了工程师的时间。

3.最好的软件工程师会像设计师一样思考

 优秀的软件工程师会深入考虑他们代码的用户体验。他们可能不会用这些术语来考虑它,而是考虑它是外部API、编程式API、用户界面、协议还是任何其他接口;优秀的工程师会考虑谁会使用它,为什么会使用它,如何使用它,以及对这些用户来说什么是重要的。牢记用户的需求才是好的用户体验的核心。

4.最好的代码是没有代码,或者不需要维护的代码

 任何职业的人解决问题的过程中都会在自己擅长的方面犯错误,这是人的本性。大多数软件工程师在编写代码免不了会犯错误,尤其是当还没有可行的非技术性解决方案时。工程团队总是倾向于在已经有很多轮子的时候重新发明轮子。有很多原因让你自己重新做一个轮子,但一定要警惕有毒的“Not invented here”综合症,不能闭门造车,妄自尊大,尽量复用和寻找非技术性解决方案。

5.软件是达到目的的一种手段

 任何软件工程师的主要工作都是交付价值。很少有软件开发人员能理解这一点,更少人能内化它。真正的内在化会带来一种不同的解决问题的方式,以及一种不同的看待工具的方式。如果你真的相信软件是屈从于结果的,你就会准备好真正找到“适合这项工作的工具”,而这个工具可能根本不是软件。

6.有时候你不得不停止磨刀,开始切东西

 有些人倾向于一头扎进问题中,然后开始编写代码解决问题。有些人却倾向于花大量时间研究和调查,但却让自己陷进问题中。在这种情况下,给自己设定一个最后期限,然后开始探索解决方案。当你开始解决这个问题的时候,你会很快学到更多的东西,这将引导你迭代形成一个更好的解决方案。

7.如果你不能很好地把握全局的可能性,你就无法设计出一个好的系统

 这是我在每天的工作中不断努力的事情。与开发者生态保持同步是一项巨大的工作,但了解开发者生态中的可能性却是至关重要的。如果你不了解在一个给定的生态系统中什么是可能的,什么是可用的,那么你就不可能设计出一个合理的解决方案来解决所有的问题,除非是最简单的问题。总而言之,要警惕那些很长时间没有编写任何代码的系统设计者。

8.每个系统最终都很糟糕,克服它吧

 比雅尼·斯特劳斯特鲁普(Bjarne Stroustrup)有一句话是这样说的: “世界上只有两种语言,一种是人们抱怨的语言,另一种是没人用的语言。”。这也可以扩展到大型系统。不存在“正确”的架构,你永远无法偿还所有的技术债务,你永远无法设计出完美的界面,你的测试总是太慢。这不是个能让事情变得更好的借口,而是一种让你看问题的方式。少担心优雅和完美;相反,要努力持续改进,创建一个你的团队喜欢并可持续提供价值的环境。

9.没人去问“为什么”

 抓住任何机会去质疑那些“一直以来都是这样做的”假设和方法。有新队员加入?那就注意他们在哪里感到困惑,他们问了什么问题。有一个没有意义的新功能需求?确保你理解了目标,以及是什么驱动了这种功能的需求。如果你得不到一个明确的答案,继续问为什么,直到你明白。

10.我们应该更加关注如何避免0.1x程序员,而不是寻找10x程序员

 10倍的程序员其实是一个愚蠢说法。认为一个人可以在一天内完成另一个有能力、勤奋、同样有经验的程序员可以在两周内完成的任务是愚蠢的。我见过程序员抛出10倍的代码量,然后你必须用10倍的时间来修正它。一个人成为10倍程序员的唯一方法就是将他与0.1倍程序员进行比较。有些人浪费时间,不寻求反馈,不测试他们的代码,不考虑边界情况等等。我们更应该关心的是让0.1x程序员远离我们的团队,而不是找到神秘的10x程序员。

11.高级工程师和初级工程师之间最大的区别之一就是他们对事情应该如何发展形成了自己的观点

 没有什么比高级工程师对他们的工具或如何构建软件一无所知更让我担心的了。我宁愿有人给我一些强烈的反对观点,也不愿他们没有任何观点。如果你正在使用你的工具,并且你并不喜欢或讨厌它们,那么你就需要体验更多。您需要探索其他语言、库和范式。没有什么方法比积极地寻找别人如何用不同的工具和技术来完成任务能更快地提升你的技能了。

12.人们不是真的想要创新

 人们经常谈论创新,但他们通常寻找的是廉价的胜利和新奇的东西。如果你真的在创新,改变人们做事的方式,那么大部分的反馈都是负面的。如果你相信你正在做的事情,并且知道它真的会改善事情,那么就做好长期斗争的准备。

13.数据是系统中最重要的部分

 我见过许多对数据完整性要求很高的系统。在这样的系统中,任何发生在关键路径之外的事情都会创建部分数据或脏数据。将来处理这些数据可能会成为一场噩梦。请记住,您的数据可能比代码库存在的时间更长。把精力花在保持它的有序和清洁上,从长远来看它会得到很好的回报。

14.寻找技术”鲨鱼“

 许多留下来的老技术是”鲨鱼“,而不是”恐龙“。他们能够很好地解决问题,并在技术不断快速变化的今天生存了下来。只有在有一个很好的理由的情况下,再去替换它们。这些工具不会华而不实,也不会令人兴奋,但是它们可以完成工作,避免很多不必要的不眠之夜。

15.不要把谦卑误认为无知

 有很多软件工程师在没有被提问的时候,是不怎么发表意见的。永远不要以为别人没有他们的观点摆在你面前,你就觉得他们没有什么观点。有时候最吵的人恰恰是我们最不想听的人。与你周围的人交谈,寻求他们的反馈和建议。你会有意外收获。

16.软件工程师应该定期写作

 软件工程师应该定期写博客,写日志,写文档,去做任何保持书面沟通技能的事情。写作可以帮助你思考问题,并帮助你与团队和未来的自己更有效地沟通。良好的书面沟通能力是任何软件工程师都需要掌握的最重要的技能之一。

17.保持流程尽可能精简

 如今,每个人都想变得敏捷,“敏捷”就是把事情分成小块,学习,然后迭代。如果有人试图把更多的东西塞进去,那他很可能是在卖东西。想想你有多少次听到来自你最喜欢的技术公司或大型开源项目的人在吹嘘他们的Scrum流程有多棒?在你知道你需要更多的东西之前,请依靠流程。相信你的团队,他们会完成任务。

18.软件工程师,像所有人一样,需要有归属感

 如果你把某人和他的工作成果分开,他就不会那么在乎他的工作。我认为这几乎是同义反复。归属感是跨职能团队工作得如此出色的主要原因,也是DevOps变得如此流行的原因。这并不全是关于交接和低效的问题,而是关于从开始到结束去参与和享受整个过程,并直接负责交付价值。让一群充满激情的人完全拥有设计、构建和交付一个软件(或者其他任何东西)的所有权,奇妙的事情就会发生。

19.面试对于判断一个团队成员是否优秀几乎毫无价值

 面试最好是试着了解对方是谁,以及他们对某一特定专业领域有多大兴趣。试图弄清楚一个团队成员会有多好是徒劳的努力。相信我,一个人有多聪明或多有知识也不能很好地表明他们将是一个优秀的团队成员。没有人会在面试中告诉你,他们会不可靠,会骂人,会夸夸其谈,或者从不准时出席会议。人们可能会说他们在这些事情上有“信号”……“如果他们在第一次面试时就问请假,那么他们就永远不会在那里了!” 但这些都是胡扯。如果你使用这样的信号,你只是在猜测,并将优秀的候选人拒之门外。

20.始终努力构建一个更小的系统

 有很多的力量将推动你预先建立更大的系统。预算分配,无法决定哪些功能应该被削减,希望交付系统的“最佳版本”。所有这些事情会迫使我们过度建设。你应该与之抗争。在构建系统的过程中,你会学到很多东西,最终迭代得到的系统将比你最初设计的系统要好得多。令人惊讶的是,这很难说服大多数人。

原文:http://www.simplethread.com/20-things-ive-learned-in-my-20-years-as-a-software-engineer/

作者:沉默的小河马
来源:juejin.cn/post/7195472887586193467

收起阅读 »

Vue动态表单组件的一点点小想法

web
Vue动态表单组件封装 本文章基于Vue2版本,使用的UI库为 elementUI。源于日常开发。 使用到的Vue技巧: 定义v-model <component is="componentName"></component> 动...
继续阅读 »

Vue动态表单组件封装



本文章基于Vue2版本,使用的UI库为 elementUI。源于日常开发。



使用到的Vue技巧:



  1. 定义v-model

  2. <component is="componentName"></component> 动态组件

  3. v-onv-bind$attrs$listeners、透传attribute

  4. slot插槽


1、关于组件的猜想


 <my-component config="config"></my-component>
复制代码

对于一个完美的组件,如上代码所示:丢入一堆config配置,组件输出我想要的页面。


1676516217126.png


那么对于一个表单组件,会需要什么呢?

基于elementUI官网中Form组件的第一个实例进行分析。
1676516277759.jpg

得出结论



  1. 表单左侧的文字每一行左侧的文字:得出属性label。

  2. 表单组件的渲染如图中的 el-input、el-select、el-radio等组件的名称:属性component

  3. 表单中 el-input、el-select、el-radio-group等组件双向绑定的值: 属性key。



(el-checkbox-group 或 el-radio-group) 类的组件,尽量使用组合的模式便于双向绑定



基于最简单的需求,总结出:


// 数据模型
const config = [
{
label: "活动名称",
component: "el-input",
key: "name",
},
{
label: "活动区域",
component: "el-select",
key: "area",
},
]
// 组件使用
<my-form config="config"></my-component>
复制代码

<template>
<el-form class="dynamic-form" ref="form" :model="formModel" label-width="80px">
<el-form-item v-for="(item, idx) in config" :key="idx" :label="item.label" :prop="item.key">
<el-input v-model="formModel[item.key]" v-if="item.component === 'el-input'"></el-input>
<el-select v-model="formModel[item.key]" v-if="item.component === 'el-select'"></el-select>
</el-form-item>
</el-form>
</template>

<script>
export default {
name: 'DynamicForm',
props: {
config: {
type: Array,
default: () => []
}
},
data() {
return {
formModel: {
name: '',
area: ''
}
}
}
}
</script>
复制代码

收获页面渲染结果如下:
1676537480111.jpg


基于以上的输出结果得出以下痛点:



  1. props参数只读,v-model需要内部变量去处理(这里指formModel要先定义好变量)。

  2. 使用v-for + v-if的判断去处理,如果思考缺少了部分组件,需要在组件内追加,繁琐。

  3. input、select等组件不添加参数。

  4. 组件与外部没有通信。

  5. 表单没办法添加校验

  6. 数据没办法回填

    ............


2、二次分析功能


基于上一节的痛点,对于组件的需求进行二次分析。



  1. 表单组件的结果要在外部方便获取。

    需要在外部修改数值时回填到组件内部 (添加自定义v-model)

  2. input、select等组件不能添加参数,

    el-form-item、el-form也需要参数配置的添加。

    (v-on, v-bind的批量绑定 以及透传Attributes)。

  3. 组件内部需要写大量的判断当前组件是什么类型,考虑不足是会造成后续组件的追加。(Vue动态组件解决)
    1676537323360.jpg


由此展开第二轮配置信息数据:















































属性字段
labellabel值
key需要绑定的内容
slot具名插槽
component组件名称
options列表数据: 如 el-select、el-cascader 需要使用到的子节点数据
formItemAttr表单item事件
formItemEven表单item属性
componentAttr表单控件属性
componentEvent表单控件事件

3、产出


组件使用部分


<template>
<div>
<MYform style="margin:60px" label-width="100px" v-model="formData" :config="config">
<template #slider>
<el-slider v-model="formData.slot"></el-slider>
</template>
</MYform>
</div>
</template>

<script>
import MYform from "./components/myForm.vue"
export default {
name: "app",
components: {
MYform
},
data() {
return {
formData: {}
};
},
mounted() {
},
computed: {
config() {
return [
{
label: "活动名称", // label值
key: "name", // 需要绑定的内容
component: "el-input", // 组件名称
formItemAttr: {
rules: [{ required: true, message: '请输入邮箱地址', trigger: 'blur' }],
}, // 表单item属性
formItemEven: {}, // 表单item事件
componentAttr: {
clearable: true,
prefixIcon: 'el-icon-search',
}, // 表单控件属性
componentEvent: {},
},
{
label: "活动内容", // label值
key: "type", // 需要绑定的内容
component: "el-select", // 组件名称
options: [{ label: "活动1", value: 1 }, { label: "活动2", value: 2 }],
formItemAttr: {}, // 表单item属性
formItemEven: {}, // 表单item事件
componentAttr: {
clearable: true,
}, // 表单控件属性
componentEvent: {},// 表单控件事件
}, {
label: "使用slot", // label值
key: "slot", // 需要绑定的内容
slot: "slider",
formItemAttr: {}, // 表单item属性
formItemEven: {}, // 表单item事件
componentAttr: {
clearable: true,
}, // 表单控件属性
componentEvent: {},// 表单控件事件
}
]
}
},
};
</script>
复制代码

最终输出的结果如下:
动画1.gif
组件代码:


<template>
<!-- v-bind="$attrs" 用于 透传属性的接收 v-on="$listeners" 方法的接收 -->
<el-form
class="dynamic-form"
ref="form"
v-bind="$attrs"
v-on="$listeners"
:model="formModel">
<el-form-item
v-for="(item, idx) in config"
:key="idx" :label="item.label"
:prop="item.key"
v-bind="item.formItemAttr">
<!-- 具名插槽 -->
<slot v-if="item.slot" :name="item.slot"></slot>
<!-- 1、动态组件(用于取代遍历判断。 is直接赋值为组件的名称即可) -->
<component v-else :is="item.component"
v-model="formModel[item.key]"
v-bind="item.componentAttr"
v-on="item.componentEvent"
@change="onUpdate"
>
<!-- 单独处理 select 的options(当然也可以基于 el-select进行二次封装,去除options遍历这一块 ) -->
<template v-if="item.component === 'el-select'">
<el-option v-for="option in item.options" :key="option.value" :label="option.label" :value="option.value">
</el-option>
</template>
</component>
<!-- 默认插槽 -->
<slot></slot>
</el-form-item>
</el-form>
</template>

<script>
export default {
name: 'MyForm',
props: {
config: {
type: Array,
default: () => []
},
modelValue: {}
},
model: {
prop: 'modelValue', // v-model绑定的值,因为v-model也是props传值,所以props要存在该变量
event: 'change' // 需要在v-model绑定的值进行修改时的触发事件。
},
computed: {

},
data() {
return {
formModel: {},
}
},
watch: {
// v-model的值发生改变时,同步修改form内部的值
modelValue(val) {
// 更新formModel
this.updateFormModel(val);
},
},
created() {
// 初始化
this.initFormModel();
},
methods: {
// 初始化表单数值
initFormModel() {
let formModelInit = {};
this.config.forEach((item) => {
// el-checkbox-group 必须为数组,否则会报错
if (item.componentName === "el-checkbox-group") {
formModelInit[item.key] = [];
} else {
formModelInit[item.key] = null;
}
});
this.formModel = Object.assign(formModelInit, this.modelValue);
this.onUpdate();
},
// 更新内部值
updateFormModel(modelValue) {
// 合并初始值和传入值
const sourceValue = modelValue ? modelValue : this.formModel;
this.formModel = Object.assign(this.formModel, sourceValue);
},
onUpdate() {
// 触发v-model的修改
this.$emit("change", this.formModel);
},
},
};
</script>
复制代码

4、结束


当然,动态组件并不是万能的,但是可以减少CV,以上代码只是一个概念篇的思想输出。但是在一定程度上也能够使用。

对于组件的完善,还是需要个人喜好来处理。

比如说:



  1. 添加methods的方法,像element一样 this.$refs[formName].resetFields(); 去重置数据或清空校验。(当然有了v-model, 其实可以直接修改v-model的值也可以完成重置数据)。

  2. 对el-select进一步封装,就可以避免去写 el-options 的遍历判断。

  3. el-checkbox-group、el-radio-group 这类型的组件尽量不使用单个的,用group便于双向绑定。

  4. el-checkbox-group、el-radio-group也可以进一步的进行封装,通过添加options配置的方式,去除内部额外添加 v-for的遍历。

  5. 还可以添加el-row、el-col的layout布局。

  6. 还有添加 form-item 的显示隐藏

  7. 甚至还可以把数据进行抽离成JSON的格式。

    ........

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

setTimeout与setInterval的区别

web
setTimeout与setInterval有什么区别,这是我6年前面试腾讯的一道面试题,上面是chatgpt的回答。简单来说,setTimeout是一次性定时器,setInterval是周期性定时器,如果你的回答也停留在api的字面解释,那chatgpt很可...
继续阅读 »

13771677231027_.pic.jpg


setTimeout与setInterval有什么区别,这是我6年前面试腾讯的一道面试题,上面是chatgpt的回答。简单来说,setTimeout是一次性定时器,setInterval是周期性定时器,如果你的回答也停留在api的字面解释,那chatgpt很可能会取代你的工作。递归地调用setTimeout,也能像setInterval一样实现周期性定时器,如下:


// start函数中调用了setTimeout,会在100ms后递归调用start,实现周期性定时器
let index = 1
const start = () => setTimeout(() => {
// 终止条件,最多调用5次
if(index++ >= 5) {
return
}
// 递归调用
start()
}, 100)
start()
复制代码

为了更直观在性能看板观察运行情况,增加了两个逻辑,调用delay函数拉长定时任务执行时长,并调用performance.mark和performance.measure标记间隔时长


let index = 1
const delay = () => {
const now = Date.now()
while(Date.now() - now < 200);
}
const start = () => {
setTimeout(() => {
// 为了方便在性能看板观察间隔时长
performance.measure(`setTimeout间隔${index}`, `setTimeout间隔${index}`)

// 耗时操作200ms
delay()

if(index++ >= 5) {
return
}


performance.mark(`setTimeout间隔${index}`)
// 递归调用
start()
}, 100)
}
performance.mark(`setTimeout间隔${index}`)
start()
复制代码

image.png
通过面板发现,定时任务的间隔时长是相等的,但是一个周期的总耗时是300ms,也就是执行耗时 + 间隔耗时,这没什么特别的,我们再使用setInterval实现相同的逻辑。


let index = 1
const delay = () => {
const now = Date.now()
while(Date.now() - now < 200);
}
const start = () => {
const ticker = setInterval(() => {
// 为了方便在性能看板观察间隔时长
performance.measure(`setTimeout间隔${index}`, `setTimeout间隔${index}`)

// 耗时操作200ms
delay()

if(index++ >= 5) {
clearInterval(ticker)
return
}

performance.mark(`setTimeout间隔${index}`)
}, 100)
}
performance.mark(`setTimeout间隔${index}`)
start()
复制代码

image.png
发现除了第一个间隔是100ms,后面其他间隔的耗时都可以忽略不计,定时器出现一个连续执行的现象,每一个周期的总耗时是200ms,也就是Math.max(执行耗时, 间隔耗时),当执行耗时大于间隔耗时,间隔失效连续执行。


js在单线程环境中执行,定时任务在指定时间加入事件队列,等待主线程空闲时,事件队列中的任务再加入执行栈执行。setInterval回调函数加入事件队列的时间点是固定的,当队列中存在重复的定时任务会进行丢弃。比如上面的例子,理论上每100ms会往事件队列中加入定时任务,由于每个周期主线程执行耗时是200ms,期间可以加入两个定时任务,由于第二个定时任务加入时,第一个定时任务还在事件队列中,重复的定时任务会被丢弃,200ms后主线程空闲,事件队列中只有一个定时任务,会立刻加入执行栈由主线程执行,由于定时任务的执行耗时大于间隔耗时,每次主线程执行完定时任务,事件队列中总会有一个新的任务在等待,所以出现了连续执行。而setTimeout的定时任务依赖上一次定时任务执行结束再调用定时器,所以定时任务之间的间隔是固定的,但是整个定时任务的周期会大于设置的间隔时长。


小结


setInterval加入事件队列的时间是固定的,setTimeout加入事件队列的时间是执行耗时 + 间隔耗时
setInterval任务间的间隔是 Math.max(执行耗时, 间隔耗时),setTimeout任务间的间隔是固定的。


这两个特性在实际开发中有什么影响吗?


轮询场景:当我们需要轮询查询某一个接口时,比如支付成功后查询订单的支付状态,为了提升性能,最好根据返回结果判断是否触发下一次查询,如果订单状态更新了,停止发送查询请求,避免不必要的开销。这个场景使用setTimeout更适合,因为它可以根据请求返回结果判断是否触发新的定时任务,而setInterval会在固定的间隔去触发请求,某一次查询请求的响应时长大于定时器间隔时长,将会发送多余的请求。


动画场景:比如像倒计时,使用setInterval会比setTimeout更稳定,因为定时任务的间隔更接近设置的间隔。当然实现动画用requestAnimationFrame性能更佳。


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

面试题:Android 中 Intent 采用了什么设计模式?

答案是采用了原型模式。 原型模式的好处在于方便地拷贝某个实例的属性进行使用、又不会对原实例造成影响,其逻辑在于对 Cloneable 接口的实现。 话不多说看下 Intent 的关键源码:  // frameworks/base/core/java/...
继续阅读 »

答案是采用了原型模式


原型模式的好处在于方便地拷贝某个实例的属性进行使用、又不会对原实例造成影响,其逻辑在于对 Cloneable 接口的实现。


话不多说看下 Intent 的关键源码:


 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     private static final int COPY_MODE_ALL = 0;
     private static final int COPY_MODE_FILTER = 1;
     private static final int COPY_MODE_HISTORY = 2;
 
     @Override
     public Object clone() {
         return new Intent(this);
    }
 
     public Intent(Intent o) {
         this(o, COPY_MODE_ALL);
    }
 
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
         this.mData = o.mData;
         this.mType = o.mType;
         this.mIdentifier = o.mIdentifier;
         this.mPackage = o.mPackage;
         this.mComponent = o.mComponent;
         this.mOriginalIntent = o.mOriginalIntent;
        ...
 
         if (copyMode != COPY_MODE_FILTER) {
            ...
             if (copyMode != COPY_MODE_HISTORY) {
                ...
            }
        }
    }
    ...
 }

可以看到 Intent 实现的 clone() 逻辑是直接调用了 new 并传入了自身实例,而非调用 super.clone() 进行拷贝。


默认的拷贝策略是 COPY_MODE_ALL,顾名思义,将完整拷贝源实例的所有属性进行构造。其他的拷贝策略是 COPY_MODE_FILTER 指的是只拷贝跟 Intent-filter 相关的属性,即用来判断启动目标组件的 actiondatatypecomponentcategory 等必备信息。无视启动 flagbundle 等数据。


 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     public @NonNull Intent cloneFilter() {
         return new Intent(this, COPY_MODE_FILTER);
    }
 
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
        ...
 
         if (copyMode != COPY_MODE_FILTER) {
             this.mFlags = o.mFlags;
             this.mContentUserHint = o.mContentUserHint;
             this.mLaunchToken = o.mLaunchToken;
            ...
        }
    }
 }

还有中拷贝策略是 COPY_MODE_HISTORY,不需要 bundle 等历史数据,保留 action 等基本信息和启动 flag 等数据。


 // frameworks/base/core/java/android/content/Intent.java
 public class Intent implements Parcelable, Cloneable {
    ...
     public Intent maybeStripForHistory() {
         if (!canStripForHistory()) {
             return this;
        }
         return new Intent(this, COPY_MODE_HISTORY);
    }
 
     private Intent(Intent o, @CopyMode int copyMode) {
         this.mAction = o.mAction;
        ...
 
         if (copyMode != COPY_MODE_FILTER) {
            ...
             if (copyMode != COPY_MODE_HISTORY) {
                 if (o.mExtras != null) {
                     this.mExtras = new Bundle(o.mExtras);
                }
                 if (o.mClipData != null) {
                     this.mClipData = new ClipData(o.mClipData);
                }
            } else {
                 if (o.mExtras != null && !o.mExtras.isDefinitelyEmpty()) {
                     this.mExtras = Bundle.STRIPPED;
                }
            }
        }
    }
 }

总结起来:































Copy Modeaction 等数据flags 等数据bundle 等历史
COPY_MODE_ALLYESYESYES
COPY_MODE_FILTERYESNONO
COPY_MODE_HISTORYYESYESNO

除了 Intent,Android 源码中还有很多地方采用了原型模式。




  • Bundle 也实现了 clone(),提供了 new Bundle(this) 的处理:


     public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
        ...
         @Override
         public Object clone() {
             return new Bundle(this);
        }
     }



  • 组件信息类 ComponentName 也在 clone() 中提供了类似的实现:


     public final class ComponentName implements Parcelable, Cloneable, Comparable<ComponentName> {
        ...
         public ComponentName clone() {
             return new ComponentName(mPackage, mClass);
        }
     }



  • 工具类 IntArray 亦是如此:


     public class IntArray implements Cloneable {
        ...
         @Override
         public IntArray clone() {
             return new IntArray(mValues.clone(), mSize);
        }
     }



原型模式也不一定非得实现 Cloneable,提供了类似的实现即可。比如:




  • Bitmap 没有实现该接口但提供了 copy(),内部将传递原始 Bitmap 在 native 中的对象指针并伴随目标配置进行新实例的创建:


     public final class ComponentName implements Parcelable, Cloneable, Comparable<ComponentName> {
        ...
         public Bitmap copy(Config config, boolean isMutable) {
            ...
             noteHardwareBitmapSlowCall();
             Bitmap b = nativeCopy(mNativePtr, config.nativeInt, isMutable);
             if (b != null) {
                 b.setPremultiplied(mRequestPremultiplied);
                 b.mDensity = mDensity;
            }
             return b;
        }
     }



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

Android斩首行动——接口预请求

前言 开发同学应该都很熟悉我们页面的渲染过程一般是从Activity#onCreate开始,再发起网络请求,等请求回调回来后,再基于网络数据渲染页面。可以用下面这幅图来粗略描述这个过程: 可以看到,目标页面渲染完成前必须得等待网络请求,导致渲染速度并没有那么...
继续阅读 »

前言


开发同学应该都很熟悉我们页面的渲染过程一般是从Activity#onCreate开始,再发起网络请求,等请求回调回来后,再基于网络数据渲染页面。可以用下面这幅图来粗略描述这个过程:


image.png


可以看到,目标页面渲染完成前必须得等待网络请求,导致渲染速度并没有那么快。尤其是当网络并不好的时候感受会更加明显。并且,当目标页面是H5页面或者是Flutter页面的时候,因为涉及到H5容器与Flutter容器的创建,白屏时间会更长。


那么有没有可能提前发起请求,来缩短网络请求这一部分的等待时间呢?这就是我们今天要讲的部分,接口预请求。


目标


我们要达到的目标很简单,就是提前异步发起目标页面的网络请求,从而加快目标页面的渲染速度。改善后的过程可以用下图表示:


image.png


并且,我们的预请求能力需要尽量少地侵入业务,与业务解耦,并保证能力的通用性,适用于工程内的任意页面(Android页面、H5页面、Flutter页面)。


方案


整体链路


首先给大家看一下整体链路,具体的细节可以先不用去抠,下面会一一讲到。


image.png


预请求时机


预请求时机一般有三种选择:



  1. 由业务层自行选择时机进行异步预请求

  2. 点击控件时进行异步预请求

  3. 路由最终跳转前进行异步预请求


第1种选择,由业务层自行选择时机进行预请求,需要涉及到业务层的改造,以及对时机合理性的把握。一方面是存在改造成本,另一方面是无法保证业务侧调用时机的合理性。


第2种选择,点击控件时进行预请求。若点击时进行预请求,点击事件监听并不是业务域统一的,无法形成有效封装。并且,若后续路由拦截器修改了参数,或是终止了跳转,这次预请求就失去了意义。


因此这里我们选择第3种,基于统一路由框架,在路由最终跳转前进行预请求。既保证了良好的封装性,也实现了对业务的零侵入,同时也做到了懒请求,即用户必然要发起该请求时才会去预请求。这里需要注意的是必须是在最终跳转前进行预请求,可以理解为是路由的最后一个前置异步拦截器。


预请求规则配置


我们通过本地的json文件(当然,有需要也可以上云通过配置后台下发),对预请求的规则进行配置,并将这份配置在App启动阶段异步读入到内存。后续在路由过程中,只有命中了预请求规则,才能发起预请求。配置demo如下:


{
"routeConfig":{
"scheme://domain/path?param1=true&itemId=123":["prefetchKey"],
"route2":["prefetchKey2"],
"route3":["prefetchKey3","prefetchKey4"]
},
"prefetcher":{
"prefetchKey":{
"prefetchType":"network",
"prefetchInfo":{
"api":"network.api.name",
"apiVersion":"1.0",
"method":"post",
"needLogin":"false",
"showLoginUI":"false",
"params": {
"itemId":"$route.itemId",
"firstTime":"true"
},
"headers": {

},
"prefetchImgInResponse": [
{
"imgUrl":"$data.imgData.img",
"imgWidth":"$data.imgData.imgWidth",
"imgHeight":150
}
]
}
},
"prefetchKey2":{
"prefetchType":"network",
"prefetchInfo":{
"api":"network.api.name2",
"apiVersion":"1.0",
"method":"post",
"needLogin":"false",
"showLoginUI":"false",
"params": {
"itemId":"$route.productId",
"firstTime":"false"
},
"headers": {

}
},
"prefetchKey3":{
"prefetchType":"image",
"prefetchInfo":{
"imgUrl":"$route.imgUrl",
"imgWidth":"$route.imgWidth",
"imgHeight": 150
}
},
"prefetchKey4":{
"prefetchInfo":{}
}
}
}

规则解读




















































参数名描述备注
routeConfig路由配置配置路由到预请求的映射
prefetcher预请求配置记录所有的预请求
prefetchKey预请求的key
prefetchType预请求类型分为network类型与image类型,两种类型所需要的参数不同
prefetchInfo预请求所需要的信息其中value若为route.param格式,那么该值从路由中获取;若为route.param格式,那么该值从路由中获取;若为data.param格式,则从响应数据中获取。
paramsnetwork请求所需要的请求params
headersnetwork请求所需要的请求headers
prefetchImgFromResponse预请求的响应返回后,需要预加载的图片用于需要预加载图片时,无法确定图片url,图片url只能从预请求响应中获取的场景。

举例说明


网络预请求


例如跳转目标页面,它的路由是scheme://domain/path?param1=true&itemId=123


首先我们在跳转路由时,若跳转的路由是这个目标页面,我们就会尝试去发起预请求。根据上面的demo配置文件,它将匹配到prefetchKey这个预请求。


那么我们详细看prefetchKey这个预请求,预请求类型prefetchTypenetwork,是一个网络预请求,prefetchInfo中具备了请求的基本参数(如apiName、apiVersion、method、请求params与请求headers,不同工程不一样,大家可以根据自己的工程项目进行修改)。具体看params中,有一个参数为itemId:$route.itemId。以$route.开头的意思,就是这个value值要从路由中获取,即itemId=123,那么这个值就是123。


图片预请求


在做网络预请求的过程中,我忽然想到图片做预请求也是可以大大提升用户体验的,尤其是当大图片首次下载到内存中渲染需要的时间会比较长。图片预请求分为url已知url未知两种场景,下面各举两个例子。


图片url已知

什么是图片url已知呢?比如我们在首页跳转首页的二级页面时,如果二级页面需要预加载的图片跟首页的某张图是一样的(尺寸可能不同),那么首页跳转路由时我们是能够提前知道这个图片的url的,所以我们看到prefetchKey3中配置了prefetchTypeimage的预请求。image的信息来自于路由参数,需要在跳转时将图片url和宽高作为路由参数之一。


比如scheme://domain/path?imgUrl=${encodeUrl}&imgWidth=200,那么根据配置项,我们将提前将encodeUrl这个图片以宽200,高150的尺寸,加载到内存中去。当目标页面用到这个图片时,将能很快渲染出来。


图片url未知

相反,当跳转目标页面时,目标页面所要加载的图片url没法取到,就对应了图片url未知的场景。


例如闪屏页跳转首页时,如果需要预加载首页顶部的图片,此时闪屏页是无法获取到图片的url的,因为这个图片url是首页接口返回的。这种情况下,我们只能依赖首页的预请求进行。


在demo配置文件中,我们可以看到prefetchImgFromResponse字段。这个字段代表着,当这个预请求响应回来之后,我需要去预请求某张图片。其中,imgUrl$data.param格式,以$data.开头,代表着这份数据是来自于响应数据的。响应数据就是一串json串,可以凭此,索引到预请求响应中图片url的位置,就能实现图片的提前加载了。


至于图片怎么提前加载到内存中,以及真实图片的加载怎么匹配到内存中的图片,这一部分是通过glide已有的preload机制实现的,感兴趣的同学可以去看一下源码了解一下,这里就不展开了。后面讲的预请求的方案细节,都只限于网络请求。


预请求匹配


预请求匹配指的是实际的业务请求怎样与已经执行的预请求匹配上,从而节省请求的空中时间,直接返回预请求的结果。


首先网络预请求执行前先在内存中生成一份PrefetchRecord,代表着已经执行的预请求,其中的字段跟配置文件中差不多,主要就是记录预请求相关的信息:


class PrefetchRecord {
// 请求信息
String api;
String apiVersion;
String method;
String needLogin;
String showLoginUI;
JSONObject params;
JSONObject headers;

// 预请求状态
int status;
// 预请求结果
ResponseModel response;
// 生成的请求id
String requestId;

boolean isMatch(RealRequest realRequest) {
requestId.equals(realRequest.requestId)
}
}

每一个PrefetchRecord生成时,都会生成一个requestId,用于跟实际业务请求进行匹配。requestId的生成规则可以自行制定,比如将所有请求信息包一起做一下md5处理之类。


在实际业务请求发起之前,也会根据同样的规则生成requestId。若内存中存在相同requestId对应的PrefetchRecord,那么就相当于匹配成功了。匹配成功后,再根据预请求的状态进行进一步的处理。


预请求状态


预请求状态分为START、FINISH、ABORT,对应“正在发起预请求”、“已经获得预请求结果”、“预请求被抛弃”。ABORT状态下一节再讲。


为什么要记录这个状态呢?因为我们无法保证,预请求的响应一定在实际请求之前。用图来表示:


image.png


因为预请求是一个并发行为。当预请求的空中时间特别长,长到目标页面已经发出实际请求了,预请求的响应还没回来,即预请求状态为START,而非FINISH。那么此时该怎么办?我们就需要让实际请求在一旁等着(记录到内存中,RealRequestRecord),等预请求接收到响应了,再根据requestId去进行匹配,匹配到RealRequestRecord了,就触发RealRequestRecord中的回调,返回数据。


另外,在匹配过程中需要注意一点,因为每次路由跳转,如果发起预请求了,总会生成一个Record在内存中等待匹配。因此在匹配结束后,不管是匹配成功还是匹配失败,都要及时释放将Record从内存中释放掉。


超时重试机制


基于实际请求等待预请求响应的场景,我们再延伸一下。若预请求请求超时,迟迟拿不到响应,该怎么办?用图表示:


image.png


假设目前的网络请求,端上默认的超时时间是30s。那么在超时场景下,实际的业务请求在30s内若拿不到预请求的结果,就需要重新发起业务请求,抛弃预请求,并将预请求的状态置为ABORT,这样即使后面预请求响应回来了也不做任何处理。


image.png


忽然想到一个很贴切的场景来比喻这个预请求方案。


我们把跳转页面理解为去柜台取餐。


预请求代表着我们人还没到柜台,就先远程下单让柜员去准备食物。


如果柜员准备得比较快,那么我们到柜台后就能直接把食物拿走了,就能快点吃上了(代表着页面渲染速度变快)。


如果柜员准备得比较慢,那么我们到柜台后还是得等一会儿才能取餐,但总体上吃上食物的速度还是要比到柜台后再点餐来得快。


但如果这个柜员消极怠工准备得太慢了,我们到柜台等了很久都没拿到食物,那么我们就只能换个柜员重新点了(超时后发起实际的业务请求),同时还不忘投诉一把(预请求空中时间太慢了)。


总结


通过这篇文章,我们知道了什么是接口预请求,怎么实现接口预请求。我们通过配置文件+统一路由处理+预请求发起、匹配、回调,实现了与业务解耦的,可适用于任意页面的轻量级预请求方案,从而提升页面的渲染速度。


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

2022被裁员两次的应届毕业生的年终总结

前言 “生活的苦可以被疲劳麻痹、被娱乐转移,最终变得习以为常、得过且过,可以称之为钝化。学习的苦在于,始终要保持敏锐而清醒的认知,乃至丰沛的感情,这不妨叫锐化。” 1. 二月,初到上海 1.1 第一段艰辛的实习生涯 从学校到校园,仿佛好像是一瞬之间。现在回想...
继续阅读 »

前言


“生活的苦可以被疲劳麻痹、被娱乐转移,最终变得习以为常、得过且过,可以称之为钝化。学习的苦在于,始终要保持敏锐而清醒的认知,乃至丰沛的感情,这不妨叫锐化。”



1. 二月,初到上海


1.1 第一段艰辛的实习生涯


从学校到校园,仿佛好像是一瞬之间。现在回想起21年的秋招,也算是收获满满,拿下了不少大大小小好几家公司的offer。那时候对于面试题和自己的项目都有较为深刻的印象,由于本人表达能力还不错(面试一堆胡吹),经过五轮面试最终接下了壹药网的offer。 仿佛一切美好都在向我招手,世间是如此的美好。


image.png在2.17入职之后,也算是正式开启了社畜的角色。奈何在公司工作不到半个月之后,开始迎来了为期三个多月的疫情,疫情不仅是对公司有着强烈的冲击,对打工人也是晴天霹雳。


由于是第一次实习,Git工具根本就不会用 (此时省略一万点艰辛,以致于我们老大叫带我的导师,专门给我先培训好我的Git技能,在这里也是超级感谢我的导师,在远程办公 事务繁忙 我还贼菜的情况下,历时一个多月我的GIT终于出师)。

不过在实习期间,也学到了很多中型的公司的开发流程以及代码规范等等,也是宝贵的实习经历让我逐渐过渡到一个标准的社畜。


2.六月,第一次被裁员


在疫情解封的第一周的第三天下午,领导把我叫到会议室,通知所有的校招生全部解约。那时已是6.13号,校招已经结束,并且我已经答辩结束顺利 “毕业” 了。此时陷入了非常被动的局势,校招已经过了时间,社招没有工作经验。
那时候让我真真切切感受到互联网公司的不稳定,也让我感受到找工作的不容易。此时我也是被迫开始了海投模式,每天都在刷BOSS直聘,每天都在EMO ,并且面试题根本看不进去啊,谁能懂?

此时逃离上海成为了我最大的想法,奈何疫情当下,去哪里都要隔离 并且杭州的公司是一家回应的都没有,此时我内心是奔腾的。 有那种陷入谷底的绝望(没敢和家里面人说,只能自己硬抗)


2.1 试用期两个月,正式工两个月


肯定是上天眷顾我,觉得我自己硬扛着太不容易了。给我了个机会,在海投十天之后,那天上午突然一个电话打给我,问我下午有没有时间面试,此时我内心的感觉(只要你们愿意要我,我愿意当牛做马,工资啥的都无所谓,主要是给老板打工)。 当时也算是比较幸运,在我的再一次胡说海吹之下,拿到了第二家公司的offer。 试用期两个月,工资打八折。这家公司入职之后,公司全是年轻人,技术用的也很新,主要是都是河南人 真的亲切啊。我也是很快就融入了公司的氛围里面,开始称兄道弟的。两个月后在我的班门弄斧之下,顺利转正了,虽然自己陆陆续续也弄出了好几个线上较为严重的BUG 但还是在大家的努力下成功补救了回来。超级感谢当时公司里面的雷哥,权哥,昊哥等等,帮我帮了超级多。同时也督促我要一直看书一直学习来着。


xuexi.png


于是乎,周六周日有时间都会去公司熟悉业务,精进自己的代码能力。



早上上班拍的公司照片,真的超级好看鸭。


3.十一月,第二次被裁员


就这样在公司一直干着,经常会加班(1.5倍的加班费,真的超级香),可是后来也陆陆续续有些消息说公司业绩不太行,疫情(再一次给我送来了惊喜),然后11.25号又被老大 再一次叫到了办公室里面,开门见山,立马滚蛋。

就是如此狗血,就是这么残忍。我现在依稀记得,就在上周我又弄出了一个超级大的BUG,导致业务受到了极大的影响。

业务改版,对之前老的数据迁移有问题,并且新的数据也有部分问题(还是太不认真,太年轻了),导致投诉电话不断,产品直接都要崩溃。没办法,又有好多人给我擦屁股。然后第二周老板宣布裁员,我和一个前端都被开除了。那天上海降温超级明显,并且还下着小雨和我的心情是一样一样的。


dierci.jpg


那天拍的最后一次公司的图片


2.十二月,开启第三份工作


在第二次被裁员之后,我是真的对自己产生了深深地怀疑,也觉得为什么我一个应届毕业生要被裁员两次。不得不否认,我的技术水平是真的菜,代码水平也是真的烂,运气也真的好差劲。

对啊,为什么幸福不是我,我没有乱七八糟的圈子,不出去乱玩每天不是上班就是下班,下班就回去煮饭吃,看看书就睡觉,周末休息就回家,我不明白生活为什么要给予我如此重重的打击,可是生活总得继续下去,我也只能收拾好行李,再出发。
不过还好,在我摆烂了大概几天之后,我又开始再一次的海投模式 同样收到的回复很少,很少有需要2022届毕业生的,简历都不太好包装。好像上帝给我关了一扇门,总会给我开一扇窗。那个本来可以不认真对待的面试题,在我认认真真对待之后,成功收到了一面通知,然后线下的面试(我不得不承认有被打击到,但是我的胡说海吹的功夫也不是盖得),最终成功拿下了两家公司的offer(另一家没有细讲,因为没去,为什么没去,钱没给到位))。


zijie.jpg


面试路上,路过字节


2023年一月,找到对象


哈哈哈哈哈哈哈哈哈哈哈嗝,虽然2022年职场过得比较坎坷。但是我想告诉大家的是,大年初一我就遇到了我对象。哈哈哈哈哈哈哈哈哈哈嗝。她真的超级超级好,我也超级超级喜欢她。2023除了升职加薪,那就是好好爱她,带她吃好多好吃的,玩好多好玩的。

送大家一句话: 没娶的别慌,待嫁的别忙, 经营好自己,珍惜当下时光。一切该来的总会到。 怕什么,岁月漫长,你心地善良 终会有一人陪你骑马喝酒走四方


seeMovie.png


一起看的第一场电影


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

聊一聊过度设计!

  新手程序员在做设计时,因为缺乏经验,很容易写出欠设计的代码,但有一些经验的程序员,尤其是在刚学习过设计模式之后,很容易写出过度设计的代码,而这种代码比新手程序员的代码更可怕,过度设计的代码不仅写出来时的成本很高,后续维护的成本也高。因为相对于毫无设计的代码...
继续阅读 »

  新手程序员在做设计时,因为缺乏经验,很容易写出欠设计的代码,但有一些经验的程序员,尤其是在刚学习过设计模式之后,很容易写出过度设计的代码,而这种代码比新手程序员的代码更可怕,过度设计的代码不仅写出来时的成本很高,后续维护的成本也高。因为相对于毫无设计的代码,过度设计的代码有比较高的理解成本。说这么多,到底什么是过度设计?


什么是过度设计?


  为了解释清楚,我这里用个类比,假如你想拧一颗螺丝,正常的解决方案是找一把螺丝刀,这很合理对吧。 但是有些人就想:“我就要一个不止能拧螺丝的工具,我想要一个可以干各种事的工具!”,于是就花大价钱搞了把瑞士军刀。在你解决“拧螺丝”问题的时候,重心早已从解决问题转变为搞一个工具,这就是过度设计。

在这里插入图片描述
  再举个更技术的例子,假设你出去面试,面试官让你写一个程序,可以实现两个数的加减乘除,方法出入参都给你提供好了 int calc(int x, int y, char op),普通程序员可能会写出以下实现。


    public int calc(int x, int y, int op) {
if (op == '+') {
return x + y;
} else if (op == '-') {
return x - y;
} else if (op == '*') {
return x * y;
} else {
return x / y;
}
}

  而高级程序员会运用设计模式,写出这样的代码:


public interface Strategy {
int calc(int x, int y);
}

public class AddStrategy implements Strategy{
@Override
public int calc(int x, int y) {
return x + y;
}
}

public class MinusStrategy implements Strategy{
@Override
public int calc(int x, int y) {
return x - y;
}
}
/**
* 其他实现
*/
public class Main {
public int calc(int x, int y, int op) {
Strategy add = new AddStrategy();
Strategy minux = new MinusStrategy();
Strategy multi = new MultiStrategy();
Strategy div = new DivStrategy();
if (op == '+') {
return add.calc(x, y);
} else if (op == '-') {
return minux.calc(x, y);
} else if (op == '*') {
return multi.calc(x, y);
} else {
return div.calc(x, y);
}
}
}

  策略模式好处在于将计算(calc)和具体的实现(strategy)拆分,后续如果修改具体实现,也不需要改动计算的逻辑,而且之后也可以加各种新的计算,比如求模、次幂……,扩展性明显增强,很是牛x。 但光从代码量来看,复杂度也明显增加。回到我们原始的需求上来看,如果我们只是需要实现两个整数的加减乘除,这明显过度设计了。


过度设计的坏处


  个人总结过度设计有两大坏处,首先就是前期的设计和开发的成本问题。过度设计的方案,首先设计的过程就需要投入额外的时间成本,其次越复杂的方案实现成本也就越高、耗时越长,如果是在快速迭代的业务中,这些可能都会决定到业务的生死。其次即便是代码正常上线后,其复杂度也会导致后期的维护成本高,比如当你想将这些代码交接给别人时,别人也需要付出额外的学习成本。


  如果成本问题你都可以接受,接下来这个问题可能影响更大,那就是过度设计可能会影响到代码的灵活性,这点听起来和做设计的目的有些矛盾,做设计不就是为了提升代码的灵活性和扩展性吗!实际上很多过度设计的方案搞错了扩展点,导致该灵活的地方不灵活,不该灵活的地方瞎灵活。在机器学习领域,有个术语叫做“过拟合”,指的是算法模型在测试数据上表现完美,但在更广泛的数据上表现非常差,模式缺少通用性。 过度设计也会出现类似的现象,就是缺少通用性,在面对稍有差异的需求上时可能就需要伤筋动骨级别的改造了。


如何避免过度设计


  既然过度设计有着成本高和欠灵活的问题,那如何避免过度设计呢!我这里总结了几个方法,希望可以帮到大家。


充分理解问题本身


  在设计的过程中,要确保充分理解了真正的问题是什么,明确真正的需求是什么,这样才可以避免做出错误的设计。


保持简单


  过度设计毫无例外都是复杂的设计,很多时候未来有诸多的不确定性,如果过早的针对某个不确定的问题做出方案,很可能就白做了,等遇到真正问题的时候再去解决问题就行。


小步快跑


  不要一开始就想着做出完美的方案,很多时候优秀的方案不是设计出来的,而是逐渐演变出来的,一点点优化已有的设计方案比一开始就设计出一个完美的方案容易得多。


征求其他人的意见


  如果你不确定自己的方案是不是过度设计了,可以咨询下其他人的,尤其是比较资深的人,交叉验证可以快速让你确认问题。


总结


  其实在业务的快速迭代之下,很难判定当前的设计是欠设计还是过度设计,你当前设计了一个简单的方案,未来可能无法适应更复杂的业务需求,但如果你当前设计了一个复杂的方案,有可能会浪费时间……。 在面对类似这种不确定性的时候,我个人还是比较推崇大道至简的哲学,当前用最简单的方案,等需要复杂性扩展的时候再去重构代码。


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

你在公司混的差,可能和组织架构有关!

如果你接触过公司的面试工作,一定见过很多来自大公司的渣渣。这些人的薪资和职位,比你高出很多,但能力却非常一般。 如果能力属实,我们大可直接把这些大公司的员工打包接收,也免了乱七八糟的面试工作。但可惜的是,水货的概率通常都比较大,新的公司也并不相信他们的能力。尤...
继续阅读 »

如果你接触过公司的面试工作,一定见过很多来自大公司的渣渣。这些人的薪资和职位,比你高出很多,但能力却非常一般。


如果能力属实,我们大可直接把这些大公司的员工打包接收,也免了乱七八糟的面试工作。但可惜的是,水货的概率通常都比较大,新的公司也并不相信他们的能力。尤其是这两年互联网炸了锅,猪飞的日子不再,这种情况就更加多了起来。


反过来说也一样成立,就像是xjjdog在青岛混了这么多年,一旦再杀回北上广,也一样是落的下乘的评价。


除了自身的努力之外,你在上家公司混的差,还与你在组织架构中所处于的位置和组织架构本身有关。


一般公司会有两种组织架构方式:垂直化划分层级化划分


1. 垂直划分


垂直划分,多以业务线为模型进行划分。各条业务线共用公司行政资源,相互之间关联不大。


各业务线之间,内部拥有自治权。


image.png


如上图所示,公司共有四个业务线。




  • 业务线A,有前端和后端开发。因为成员能力比较强,所以没有测试运维等职位;




  • 业务线B倡导全栈技能,开发后台前端一体化;




  • 业务线C的管理能力比较强,仅靠少量自有研发,加上大量的外包,能够完成一次性工作。




  • 业务线D是传统的互联网方式,专人专岗,缺什么招什么,不提倡内部转岗




运行模式




  1. 业务线A缺人,缺项目,与业务线BCD无任何关系,不允许借调




  2. 业务线发展良好,会扩大规模;其他业务线同学想要加入需要经过复杂的流程,相当于重新找工作




  3. 业务线发展萎靡,会缩减人员,甚至会整体砍掉。优秀者会被打散吸收进其他业务线




好处




  1. 业务线之间存在竞争关系,团队成员有明确的奋斗目标和危机意识




  2. 一条业务线管理和产品上的失败,不会影响公司整体运营




  3. 可以比较容易的形成单向汇报的结构,避免成本巨大且有偏差的多重管理




  4. 便于复制成功的业务线,或者找准公司的发展重点




坏处




  1. 对业务线主要分管领导的要求非常高




  2. 多项技术和产品重复建设,容易造成人员膨胀,成本浪费




  3. 部门之间隔阂加大,共建、合作困难,与产品化相逆




  4. 业务线容易过度自治,脱离掌控




  5. 太激进,大量过渡事宜需要处理




修订


为了解决上面存在的问题,通常会有一个协调和监管部门,每个业务线,还需要有响应的协调人进行对接。以以往的观察来看,效果并不会太好。因为这样的协调,多陷于人情沟通,不好设计流程规范约束这些参与人的行为。


image.png


在公司未摸清发展方向之前,并不推荐此方式的改革。它的本意是通过竞争增加部门的进取心,通过充分授权和自治发挥骨干领导者的作用。但在未有成功案例之前,它的结果变成了:寄希望于拆分成多个小业务线,来解决原大业务线存在的问题。所以依然是处于不太确定的尝试行为。


2. 水平划分


水平划分方式,适合公司有确定的产品,并能够形成持续迭代的团队。


它的主要思想,是要打破“不会做饭的项目经理不是好程序员”的思维,形成专人专业专岗的制度。


这种方式经历了非常多的互联网公司实践,可以说是最节约研发成本,能动性最高的组织方式。主要是因为:




  • 研发各司其职,做好自己的本职工作可以避免任务切换、沟通成本,达到整体最优




  • 个人单向汇报,组织层级化,小组扁平化。“替领导负责,就是替公司负责”




  • 任何职位有明确的JD,可替换性高,包括小组领导




这种方式最大的问题就是,对团队成员的要求都很高。主动性与专业技能都有要求,需要经过严格的面试筛选。


坏处




  • 是否适合项目类公司,存疑




  • 存在较多技术保障部门,公共需求 下沉容易造成任务积压




  • 需要对其他部门进行整合,才能发挥更大的价值




分析


image.png


如上图,大体会分为三层。




  • 技术保障,保障公司的底层技术支撑,问题处理和疑难问题解决。小组多但人少,职责分明




  • 基础业务,公司的旗舰业务团队,需求变更小但任何改动都非常困难。团队人数适中




  • 项目演化,纯项目,可以是一锤子买卖,也可以是服务升级,属于朝令夕改类需求的聚居地。人数最多




可以看到项目演化层,多是脏活,有些甚至是尝试性的项目-----这是合理的。




  1. 技术保障和基础业务的技术能力要求高,业务稳定,适合长期在公司发展,发展属性偏技术的人群,流动性小,招聘困难




  2. 项目演化层,业务多变,项目奖金或者其他回报波动大,人员流动性高,招聘容易




成功的孵化项目,会蜕变成产品,或者基础业务,并入基础业务分组。


从这种划分可以看出,一个人在公司的命运和发展,在招聘入职的时候就已经确定了。应聘人员可以根据公司的需求进行判断,提前预知自己的倾向。


互联网公司大多数将项目演化层的人员当作炮灰,因为他们招聘容易,团队组件迅速,但也有很多可能获得高额回报,这也是很多人看中的。


3.组合


组合一下垂直划分和层级划分,可以是下面这种效果。


image.png


采用层级+垂直方式进行架构。即:首选层级模式,然后在项目演化层采用垂直模式,也叫做业务线,拥有有限的自治权。


为每一个业务线配备一个与下层产品化或者技术保障对接的人员。


绩效方面,上层的需求为下层的实现打分。基础业务和技术保障,为绿色的协调人员打分。他们的利益是一致的。


End


大公司出来的并不一定是精英,小公司出来的也并不一定是渣渣。这取决于他在公司的位置和所从事的内容。核心部门会得到更多的利益,而边缘的尝试性部门只能吃一些残羹剩饭。退去公司的光环,加上平庸的项目经历,竞争力自然就打上一个折扣。


以上,仅限IT行业哦。赵家人不在此列。



作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。


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