注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

IM 聊天组件

web
IM 消息通常分为文本、图片、文件等 3 类,会对应不同的展示 传入参数 自定义内容:标题(title)、内容(children)、底部(footer) 弹框组件显隐控制: 一般通过一个变量控制显示或隐藏(visible); 并且暴露出一个事件,控制该变量(...
继续阅读 »

IM 消息通常分为文本、图片、文件等 3 类,会对应不同的展示


im_3.png


传入参数


自定义内容:标题(title)、内容(children)、底部(footer)


弹框组件显隐控制:


一般通过一个变量控制显示或隐藏(visible);


并且暴露出一个事件,控制该变量(setVisible)


interface iProps {
title?: string // 标题
maskClose?: boolean // 点击 x 或 mask 回调
visible?: boolean // 是否显示
setVisible: (args) => void // 设置是否显示
children?: React.ReactNode | Array<React.ReactNode> // 自定义内容
footer?: React.ReactNode | Array<React.ReactNode> // 自定义底部
}

基础结构


IM 聊天组件基础结构包含:头部、内容区、尾部


function wsDialog(prop: iProps) {
const wsContentRef = useRef(null); // 消息区
const { title = "消息", maskClose, visible, setVisible } = prop; // 传入参数
const [message, setMessage] = useState(""); // 当前消息
const imMessage = useSelector(
(state: rootState) => state.mediaReducer.imMessage
); // 消息列表 全局管理

return (
<Modal
className={styles.ws_modal}
visible={visible}
transparent
onClose={handleMaskClose}
popup
animationType="slide-up"
>

<div className={styles.ws_modal_widget}>
{/* 头部 */}
<div className={styles.ws_header}></div>
{/* 内容区 */}
<div ref={wsContentRef} className={styles.ws_content}></div>
{/* 尾部区域 */}
<div className={styles.ws_footer}></div>
</div>
</Modal>

);
}

头部区


头部区域主要展示标题和关闭图标


标题内容可以自定义


不仅可以点击“右上角关闭图标”进行关闭


也可以通过点击“遮罩”进行关闭


// 头部关闭事件
function handleClose() {
slLog.log("[wsDialog]点击了关闭按钮");
setVisible(false);
}

// 弹框遮罩关闭事件
function handleMaskClose() {
if (maskClose) {
slLog.log("[wsDialog]点击了遮罩关闭");
setVisible(false);
}
}

// 头部区域
<div className={styles.ws_header}>
<div>{title}</div>
<div className={styles.ws_header_close} onClick={handleClose}>
<Icon type="cross" color="#999" size="lg" />
</div>

</div>;

内容区


消息内容分类展示:



  1. 文本:直接展示内容

  2. 图片:通过 a 标签包裹展示,可以在新标签页中打开,通过target="_blank"控制

  3. 文件:不同类型文件展示不同的图标,包括 zip、rar、doc、docx、xls、xlsx、pdf、txt 等;文件还可以进行下载


<div ref={wsContentRef} className={styles.ws_content}>
{imMessage &&
imMessage.length &&
imMessage.map((o, index) => {
return (
<div
key={index}
className={`${styles.item} ${
o.category === "send" ? styles.self_item : ""
}`}
>

<div className={styles.title}>{o.showName + " " + o.showNum}</div>
{/* 消息为图片 */}
{o.desc === "img" ? (
<a
className={`${styles.desc} ${styles.desc_image}`}
href={o.fileUrl}
title={o.fileName}
target="_blank"
>

<img src={o.fileUrl} />
</a>
) : o.desc === "file" ? (
// 消息为文件
<div className={`${styles.desc} ${styles.desc_file}`}>
<img
className={styles.file_icon}
src={handleSuffix(o.fileSuffix)}
/>

<div className={styles.file_content}>
<a title={o.fileName}>{o.fileName}</a>
<div>{o.fileSize}</div>
</div>
<img
className={styles.down_icon}
src={downIcon}
onClick={() =>
handleDownload(o)}
/>
</div>
) : (
// 消息为文本
<div className={`${styles.desc} ${styles.desc_message}`}>
{o.message}
</div>
)}
</div>

);
})}
</div>

文件下载通过 a 标签模拟实现


// 下载文件
function handleDownload(o) {
slLog.log("[SLIM]下载消息文件", o.fileUrl);
const a = document.createElement("a");
a.href = o.fileUrl;
a.download = o.fileName;
document.body.appendChild(a);
a.target = "_blank";
a.click();
a.remove();
}

监听消息内容,自动滚动到最底部处理


useEffect(() => {
if (visible && imMessage && imMessage.length) {
// 滚动到底部
wsContentRef.current.scrollTop = wsContentRef.current.scrollHeight;
}
}, [visible, imMessage]);

尾部区


主要是操作区,用于展示和发送文本、图片、文件等消息。


图片和文件通过原生input实现,通过accept属性控制文件类型


<div className={styles.ws_footer}>
<div className={styles.tools_panel}>
{/* 上传图片 */}
<div className={styles.tool}>
<img src={imageIcon} />
<input type="file" accept="image/*" onChange={handleChange("img")} />
</div>
{/* 上传文件 */}
<div className={styles.tool}>
<img src={fileIcon} />
<input
type="file"
accept=".doc,.docx,.pdf,.txt,.xls,.xlsx,.zip,.rar"
onChange={handleChange("file")}
/>

</div>
</div>

<div className={styles.input_panel}>
{/* 输入框,上传文本 */}
<input
placeholder="输入文本"
value={message}
onChange={handleInputChange}
className={`${styles.message} ${styles.mMessage}`}
onKeyUp={handleKeyUp}
/>

{/* 消息发送按钮 */}
<div onClick={handleMessage} className={styles.btn}>
发送
</div>
</div>

</div>

获取图片、文件信息:


// 消息处理
function handleChange(type) {
return (ev) => {
switch (type) {
case "img":
case "file":
msgObj.type = type === "img" ? 4 : 7;
const e = window.event || ev;
const files = e.target.files || e.dataTransfer.files;
const file = files[0];
msgObj.content = file;
break;
}
};
}

实现回车键发送消息:


通过输入框,发送文本消息时,一般需要监听回车事件(onKeyUp 事件中的 event.keyCode 为 13),也能发送消息


// 回车事件
function handleKeyUp(event) {
const value = event.target.value;
if (event.keyCode === 13) {
slLog.log("[wsDialog]onKeyUp", value, event.keyCode);
handleInputChange(event);
handleMessage();
}
}

组件封装


组件级别:公司级、系统级、业务级


组件封装优势:



  1. 提升开发效率,组件化、统一化管理

  2. 考虑发布成 npm 形式,远程发布通用


组件封装考虑点:



  1. 组件的分层和分治

  2. 设置扩展性(合理预留插槽)

  3. 兼容性考虑(向下兼容)

  4. 使用对象考虑

  5. 适用范围考虑


组件封装步骤:



  1. 建立组件的模板:基础架子,UI 样式,基本逻辑

  2. 定义数据输入:分析逻辑,定义 props 里面的数据、类型

  3. 定义数据输出:根据组件逻辑,定义要暴露出来的方法,$emit 实现等

  4. 完成组件内部的逻辑,考虑扩展性和维护性

  5. 编写详细的说明文档


作者:时光足迹
来源:juejin.cn/post/7249286405025022009
收起阅读 »

关于正则表达式,小黄人有话要说!!!

web
引言(关于正则表达式,小黄人有话要说!!!) 掌握 JavaScript 正则表达式:从基础到高级,十个实用示例带你提升编程效率! 本文将带你逐步学习正则表达式的基础知识和高级技巧,从基本的元字符到实用的正则表达式示例,让你轻松掌握这一重要的编程技能。无论你是...
继续阅读 »

38dbb6fd5266d016a9ef9caf912bd40734fa3546.jpeg


引言(关于正则表达式,小黄人有话要说!!!)


掌握 JavaScript 正则表达式:从基础到高级,十个实用示例带你提升编程效率!


本文将带你逐步学习正则表达式的基础知识和高级技巧,从基本的元字符到实用的正则表达式示例,让你轻松掌握这一重要的编程技能。无论你是初学者还是有一定经验的开发者,这篇文章都能帮助你更好地理解和应用正则表达式。


如果您认为这篇文章对您有帮助或有价值,请不吝点个赞支持一下。如果您有任何疑问、建议或意见,欢迎在评论区留言。


image.png


如果你想快速入门 JavaScript 正则表达式,不妨点击这里阅读文章 "点燃你的前端技能!五分钟掌握JavaScript正则表达式"


字面量和构造函数


在 JavaScript 中,我们可以使用正则表达式字面量构造函数来创建正则表达式对象。


// 使用字面量
let regexLiteral = /pattern/;
// 使用构造函数
let regexConstructor = new RegExp('pattern');

正则表达式的方法


在 JavaScript 中,你可以使用正则表达式的方法进行模式匹配和替换。以下是一些常用的方法:




  • test():测试一个字符串是否匹配正则表达式。


    const regex = /pattern/;
    regex.test('string'); // 返回 true 或 false



  • exec():在字符串中执行正则表达式匹配,返回匹配结果的数组。


    const regex = /pattern/;
    regex.exec('string'); // 返回匹配结果的数组或 null



  • match():在字符串中查找匹配正则表达式的结果,并返回匹配结果的数组。


    const regex = /pattern/;
    'string'.match(regex); // 返回匹配结果的数组或 null



  • search():在字符串中搜索匹配正则表达式的结果,并返回匹配的起始位置。


    const regex = /pattern/;
    'string'.search(regex); // 返回匹配的起始位置或 -1



  • replace():在字符串中替换匹配正则表达式的内容。


    const regex = /pattern/;
    'string'.replace(regex, 'replacement'); // 返回替换后的新字符串



  • split():将字符串根据匹配正则表达式的位置分割成数组。


    const regex = /pattern/;
    'string'.split(regex); // 返回分割后的数组



u=1690536536,1627515251&fm=253&fmt=auto&app=138&f=JPEG.webp


基本元字符


正则表达式由字母、数字和特殊字符组成。其中,特殊字符被称为元字符,具有特殊的意义和功能。以下是一些常见的基本元字符及其作用:


元字符及其作用




  • 字符类 []



    • [abc]:匹配任意一个字符 a、b 或 c。

    • [^abc]:匹配除了 a、b 或 c 之外的任意字符。

    • [0-9]:匹配任意一个数字。

    • [a-zA-Z]:匹配任意一个字母(大小写不限)。




  • 转义字符 \



    • \d:匹配任意一个数字字符。

    • \w:匹配任意一个字母、数字或下划线字符。

    • \s:匹配任意一个空白字符。




  • 量词 {}



    • {n}:匹配前一个元素恰好出现 n 次。

    • {n,}:匹配前一个元素至少出现 n 次。

    • {n,m}:匹配前一个元素出现 n 到 m 次。




  • 边界字符 ^



    • ^pattern:匹配以 pattern 开头的字符串。

    • pattern$:匹配以 pattern 结尾的字符串。

    • \b:匹配一个单词边界。




  • 其他元字符



    • .:匹配任意一个字符,除了换行符。

    • |:用于模式的分组和逻辑 OR。

    • ():捕获分组,用于提取匹配的子字符串。

    • ?::非捕获分组,用于匹配但不捕获子字符串。




实例演示


现在,让我们通过一些实例来演示正则表达式中元字符的实际作用:


u=3528014621,1838675307&fm=253&fmt=auto&app=138&f=JPEG.webp



  • 字符类 []


let regex = /[abc]/;
console.log(regex.test("apple")); // true
console.log(regex.test("banana")); // false


  • 转义字符 \


let regex = /\d{3}-\d{4}/;
console.log(regex.test("123-4567")); // true
console.log(regex.test("abc-1234")); // false


  • 量词 {}


let regex = /\d{2,4}/;
console.log(regex.test("123")); // true
console.log(regex.test("12345")); // false
console.log(regex.test("12")); // true


  • 边界字符 ^


// 以什么开头
let regex = /^hello/;
console.log(regex.test("hello world")); // true
console.log(regex.test("world hello")); // false

// 单词边界
const pattern = /\bcat\b/;
console.log(pattern.test("The cat is black.")); // 输出:true
console.log(pattern.test("A cat is running.")); // 输出:true
console.log(pattern.test("The caterpillar is cute.")); // 输出:false


  • 其他元字符


// 捕获分组与模式分组
let regex = /(red|blue) car/;
console.log(regex.test("I have a red car.")); // true
console.log(regex.test("I have a blue car.")); // true
console.log(regex.test("I have a green car.")); // false

// 点号元字符
const pattern = /a.b/;
console.log(pattern.test("acb")); // 输出:true
console.log(pattern.test("a1b")); // 输出:true
console.log(pattern.test("a@b")); // 输出:true
console.log(pattern.test("ab")); // 输出:false

修饰符的使用


修饰符用于改变正则表达式的匹配行为,常见的修饰符包括 g(全局)、i(不区分大小写)和 m(多行)。


// 使用 `g` 修饰符全局匹配
const regex = /a/g;
const str = "abracadabra";
console.log(str.match(regex)); // 输出:['a', 'a', 'a', 'a']

// 使用 `i` 修饰符进行不区分大小写匹配
const pattern = /abc/i;
console.log(pattern.test("AbcDef")); // 输出:true
console.log(pattern.test("XYZ")); // 输出:false

十个高度实用的正则表达式示例


u=4075901265,1581553886&fm=253&fmt=auto&app=120&f=JPEG.webp



  1. 验证电子邮件地址:


const emailPattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/;
console.log(emailPattern.test("example@example.com")); // 输出:true
console.log(emailPattern.test("invalid.email@com")); // 输出:false


  1. 验证手机号码:


const phonePattern = /^\d{11}$/;
console.log(phonePattern.test("12345678901")); // 输出:true
console.log(phonePattern.test("98765432")); // 输出:false


  1. 提取 URL 中的域名:


const url = "https://www.example.com";
const domainPattern = /^https?://([^/?#]+)(?:[/?#]|$)/i;
const domain = url.match(domainPattern)[1];
console.log(domain); // 输出:"www.example.com"


  1. 验证日期格式(YYYY-MM-DD):


const datePattern = /^\d{4}-\d{2}-\d{2}$/;
console.log(datePattern.test("2023-05-12")); // 输出:true
console.log(datePattern.test("12/05/2023")); // 输出:false


  1. 验证密码强度(至少包含一个大写字母、一个小写字母和一个数字):


const passwordPattern = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$/;
console.log(passwordPattern.test("Password123")); // 输出:true
console.log(passwordPattern.test("weakpassword")); // 输出:false


  1. 提取文本中的所有链接:


const text = "Visit my website at https://www.example.com. For more information, check out http://www.example.com/docs.";
const linkPattern = /https?://\S+/g;
const links = text.match(linkPattern);
console.log(links); // 输出:["[https://www.example.com](https://www.example.com/)", "<http://www.example.com/docs>"]


  1. 替换字符串中的所有数字为特定字符:


const text = "I have 3 apples and 5 oranges.";
const digitPattern = /\d/g;
const modifiedText = text.replace(digitPattern, "*");
console.log(modifiedText); // 输出:"I have * apples and * oranges."


  1. 匹配 HTML 标签中的内容:


const html = "<p>Hello, <strong>world</strong>!</p>";
const tagPattern = /<[^>]+>/g;
const content = html.replace(tagPattern, "");
console.log(content); // 输出:"Hello, world!"


  1. 检查字符串是否以特定后缀结尾:


const filename = "example.txt";
const suffixPattern = /.txt$/;
console.log(suffixPattern.test(filename)); // 输出:true


  1. 验证邮政编码(5 位或 5+4 位数字):


const zipCodePattern = /^\d{5}(?:-\d{4})?$/;
console.log(zipCodePattern.test("12345")); // 输出:true
console.log(zipCodePattern.test("98765-4321")); // 输出:true
console.log(zipCodePattern.test("1234")); // 输出:false

u=3763318279,485967013&fm=253&fmt=auto&app=138&f=JPEG.webp


通过正则表达式的核心概念和用法,结合实例和讲解。在实际开发中,不难发现正则表达式是一个强大的工具,可用于字符串处理、模式匹配和验证输入等方面。掌握正则表达式的技巧,可以大大提升 JavaScript 编程的效率和灵活性。


结语


感谢您的阅读!希望本文带给您有价值的信息。


如果对您有帮助,请「点赞」支持,并「关注」我的主页获取更多后续相关文章。同时,也欢迎「收藏」本文,方便以后查阅。


写作不易,我会继续努力,提供有意义的内容。感谢您的支持和关注!


290be963d171f8b42f347d7e97b62252.jpg.source.jpg


作者:Sailing
来源:juejin.cn/post/7249231231967232037
收起阅读 »

Android 冷启动优化的3个小案例

背景 为了提高App的冷启动耗时,除了在常规的业务侧进行耗时代码优化之外,为了进一步缩短启动耗时,需要在纯技术测做一些优化探索,本期我们从类预加载、Retrofit 、ARouter方面进行了进一步的优化。从测试数据上来看,这些优化手段的收益有限,可能在中端机...
继续阅读 »

背景


为了提高App的冷启动耗时,除了在常规的业务侧进行耗时代码优化之外,为了进一步缩短启动耗时,需要在纯技术测做一些优化探索,本期我们从类预加载、Retrofit 、ARouter方面进行了进一步的优化。从测试数据上来看,这些优化手段的收益有限,可能在中端机上加起来也不超过50ms的收益,但为了冷启动场景的极致优化,给用户带来更好的体验,任何有收益的优化手段都是值得尝试的。


类预加载


一个类的完整加载流程至少包括 加载、链接、初始化,而类的加载在一个进程中只会触发一次,因此对于冷启动场景,我们可以异步加载原本在启动阶段会在主线程触发类加载过程的类,这样当原流程在主线程访问到该类时就不会触发类加载流程。


Hook ClassLoader 实现


在Android系统中,类的加载都是通过PathClassLoader 实现的,基于类加载的父类委托机制,我们可以通过Hook PathClassLoader 修改其默认的parent 来实现。


首先我们创建一个MonitorClassLoader 继承自PathClassLoader,并在其内部记录类加载耗时


class MonitorClassLoader(
dexPath: String,
parent: ClassLoader, private val onlyMainThread: Boolean = false,
) : PathClassLoader(dexPath, parent) {

val TAG = "MonitorClassLoader"

override fun loadClass(name: String?, resolve: Boolean): Class<*> {
val begin = SystemClock.elapsedRealtimeNanos()
if (onlyMainThread && Looper.getMainLooper().thread!=Thread.currentThread()){
return super.loadClass(name, resolve)
}
val clazz = super.loadClass(name, resolve)
val end = SystemClock.elapsedRealtimeNanos()
val cost = end - begin
if (cost > 1000_000){
Log.e(TAG, "加载 ${clazz} 耗时 ${(end - begin) / 1000} 微秒 ,线程ID ${Thread.currentThread().id}")
} else {
Log.d(TAG, "加载 ${clazz} 耗时 ${(end - begin) / 1000} 微秒 ,线程ID ${Thread.currentThread().id}")
}
return clazz;

}
}

之后,我们可以在Application attach阶段 反射替换 application实例的classLoader 对应的parent指向。


核心代码如下:


    companion object {
@JvmStatic
fun hook(application: Application, onlyMainThread: Boolean = false) {
val pathClassLoader = application.classLoader
try {
val monitorClassLoader = MonitorClassLoader("", pathClassLoader.parent, onlyMainThread)
val pathListField = BaseDexClassLoader::class.java.getDeclaredField("pathList")
pathListField.isAccessible = true
val pathList = pathListField.get(pathClassLoader)
pathListField.set(monitorClassLoader, pathList)

val parentField = ClassLoader::class.java.getDeclaredField("parent")
parentField.isAccessible = true
parentField.set(pathClassLoader, monitorClassLoader)
} catch (throwable: Throwable) {
Log.e("hook", throwable.stackTraceToString())
}
}
}

主要逻辑为



  • 反射获取原始 pathClassLoader 的 pathList

  • 创建MonitorClassLoader,并反射设置 正确的 pathList

  • 反射替换 原始pathClassLoader的 parent指向 MonitorClassLoader实例


这样,我们就获取启动阶段的加载类了



基于JVMTI 实现


除了通过 Hook ClassLoader的方案实现,我们也可以通过JVMTI 来实现类加载监控。关于JVMTI 可参考之前的文章 juejin.cn/post/694278…


通过注册ClassPrepare Callback, 可以在每个类Prepare阶段触发回调。




当然这种方案,相比 Hook ClassLoader 还是要繁琐很多,不过基于JVMTI 还可以做很多其他更强大的事。


类预加载实现


目前应用通常都是多模块的,因此我们可以设计一个抽象接口,不同的业务模块可以继承该抽象接口,定义不同业务模块需要进行预加载的类。


/**
* 资源预加载接口
*/

public interface PreloadDemander {
/**
* 配置所有需要预加载的类
* @return
*/

Class[] getPreloadClasses();
}

之后在启动阶段收集所有的 Demander实例,并触发预加载


/**
* 类预加载执行器
*/

object ClassPreloadExecutor {


private val demanders = mutableListOf<PreloadDemander>()

fun addDemander(classPreloadDemander: PreloadDemander) {
demanders.add(classPreloadDemander)
}

/**
* this method shouldn't run on main thread
*/

@WorkerThread fun doPreload() {
for (demander in localDemanders) {
val classes = demander.preloadClasses
classes.forEach {
val classLoader = ClassPreloadExecutor::class.java.classLoader
Class.forName(it.name, true, classLoader)
}
}
}

}

收益


第一个版本配置了大概90个类,在终端机型测试数据显示 这些类的加载需要消耗30ms左右的cpu时间,不同类加载的消耗时间差异主要来自于类的复杂度 比如继承体系、字段属性数量等, 以及类初始化阶段的耗时,比如静态成员变量的立即初始化、静态代码块的执行等。


方案优化思考


我们目前的方案 配置的具体类列表来源于手动配置,这种方案的弊端在于,类的列表需要开发维护,在版本快速迭代变更的情况下 维护成本较大, 并且对于一些大型App,存在着非常多的AB实验条件,这也可能导致不同的用户在类加载上是会有区别的。


在前面的小节中,我们介绍了使用自定义的 ClassLoader可以手动收集 启动阶段主线程的类列表,那么 我们是否可以在端上 每次启动时 自动收集加载的类,如果发现这个类不在现有 的名单中 则加入到名单,在下次启动时进行预加载。 当然 具体的策略还需要做详细设计,比如 控制预加载名单的列表大小, 被加入预加载名单的类最低耗时阈值, 淘汰策略等等。


Retrofit ServiceMethod 预解析注入


背景


Retrofit 是目前最常用的网络库框架,其基于注解配置的网络请求方式及Adapter的设计模式大大简化了网络请求的调用方式。 不过其并没有采用类似APT的方式在编译时生成请求代码,而是采用运行时解析的方式。


当我们调用Retrofit.create(final Class service) 函数时,会生成一个该抽象接口的动态代理实例。



接口的所有函数调用都会被转发到该动态代理对象的invoke函数,最终调用loadServiceMethod(method).invoke 调用。



在loadServiceMethod函数中,需要解析原函数上的各种元信息,包括函数注解、参数注解、参数类型、返回值类型等信息,并最终生成ServiceMethod 实例,对原接口函数的调用其实最终触发的是 这个生成的ServiceMethod invoke函数的调用。


从源码实现上可以看出,对ServiceMethod的实例做了缓存处理,每个Method 对应一个ServiceMethod。


耗时测试


这里我模拟了一个简单的 Service Method, 并调用archiveStat 观察首次调用及其后续调用的耗时,注意这里的调用还未触发网络请求,其返回的是一个Call对象。




从测试结果上看,首次调用需要触发需要消耗1.7ms,而后续的调用 只需要消耗50微妙左右。



优化方案


由于首次调用接口函数需要触发ServiceMethod实例的生成,这个过程比较耗时,因此优化思路也比较简单,收集启动阶段会调用的 函数,提前生成ServiceMethod实例并写入到缓存中。


serviceMethodCache 的类型本身是ConcurrentHashMap,所以它是并发安全的。



但是源码中 进行ServiceMethod缓存判断的时候 还是以 serviceMethodCache为Lock Object 进行了加锁,这导致 多线程触发同时首次触发不同Method的调用时,存在锁等待问题



这里首先需要理解为什么这里需要加锁,其目的也是因为parseAnnotations 是一个好事操作,这里是为了实现类似 putIfAbsent的完全原子性操作。 但实际上这里加锁可以以 对应的Method类型为锁对象,因为本身不同Method 对应的ServiceMethod实例就是不同的。 我们可以修改其源码的实现来避免这种场景的锁竞争问题。




当然针对我们的优化场景,其实不修改源码也是可以实现的,因为 ServiceMethod.parseAnnotations 是无锁的,毕竟它是一个纯函数。 因此我们可以在异步线程调用parseAnnotations 生成ServiceMethod 实例,之后通过反射 写入 Retrofit实例的 serviceMethodCache 中。这样存在的问题是 不同线程可能同时触发了一个Method的解析注入,但 由于serviceMethodCache 本身就是线程安全的,所以 它只是多做了一次解析,对最终结果并无影响。


ServiceMethod.parseAnnotations是包级私有的,我们可以在当前工程创建一个一样的包,这样就可以直接调用该函数了。 核心实现代码如下


package retrofit2

import android.os.Build
import timber.log.Timber
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.lang.reflect.Modifier

object RetrofitPreloadUtil {
private var loadServiceMethod: Method? = null
var initSuccess: Boolean = false
// private var serviceMethodCacheField:Map<Method,ServiceMethod<Any>>?=null
private var serviceMethodCacheField: Field? = null

init {
try {
serviceMethodCacheField = Retrofit::class.java.getDeclaredField("serviceMethodCache")
serviceMethodCacheField?.isAccessible = true
if (serviceMethodCacheField == null) {
for (declaredField in Retrofit::class.java.declaredFields) {
if (Map::class.java.isAssignableFrom(declaredField.type)) {
declaredField.isAccessible =true
serviceMethodCacheField = declaredField
break
}
}
}
loadServiceMethod = Retrofit::class.java.getDeclaredMethod("loadServiceMethod", Method::class.java)
loadServiceMethod?.isAccessible = true
} catch (e: Exception) {
initSuccess = false
}
}

/**
* 预加载 目标service 的 相关函数,并注入到对应retrofit实例中
*/

fun preloadClassMethods(retrofit: Retrofit, service: Class<*>, methodNames: Array<String>) {
val field = serviceMethodCacheField ?: return
val map = field.get(retrofit) as MutableMap<Method,ServiceMethod<Any>>

for (declaredMethod in service.declaredMethods) {
if (!isDefaultMethod(declaredMethod) && !Modifier.isStatic(declaredMethod.modifiers)
&& methodNames.contains(declaredMethod.name)) {
try {
val parsedMethod = ServiceMethod.parseAnnotations<Any>(retrofit, declaredMethod) as ServiceMethod<Any>
map[declaredMethod] =parsedMethod
} catch (e: Exception) {
Timber.e(e, "load method $declaredMethod for class $service failed")
}
}
}

}

private fun isDefaultMethod(method: Method): Boolean {
return Build.VERSION.SDK_INT >= 24 && method.isDefault;
}

}

预加载名单收集


有了优化方案后,还需要收集原本在启动阶段会在主线程进行Retrofit ServiceMethod调用的列表, 这里采取的是字节码插桩的方式,使用的LancetX 框架进行修改。



目前名单的配置是预先收集好,在配置中心进行配置,运行时根据配置中写的配置 进行预加载。 这里还可以提供其他的配置方案,比如 提供一个注解用于标注该Retrofit函数需要进行预解析,



之后,在编译期间收集所有需要预加载的Service及函数,生成对应的名单,不过这个方案需要一定开发成本,并且需要去修改业务模块的代码,目前的阶段还处于验证收益阶段,所以暂未实施。


收益


App收集了启动阶段20个左右的Method 进行预加载,预计提升10~20ms。


ARouter


背景


ARouter框架提供了路由注册跳转 及 SPI 能力。为了优化冷启动速度,对于某些服务实例可以在启动阶段进行预加载生成对应的实例对象。


ARouter的注册信息是在预编译阶段(基于APT) 生成的,在编译阶段又通过ASM 生成对应映射关系的注入代码。



而在运行时以获取Service实例为例,当调用navigation函数获取实例最终会调用到 completion函数。



当首次调用时,其对应的RouteMeta 实例尚未生成,会继续调用 addRouteGroupDynamic函数进行注册。



addRouteGroupDynamic 会创建对应预编译阶段生成的服务注册类并调用loadInto函数进行注册。而某些业务模块如何服务注册信息比较多,这里的loadInto就会比较耗时。



整体来看,对于获取Service实例的流程, completion的整个流程 涉及到 loadInto信息注册、Service实例反射生成、及init函数的调用。 而completion函数是synchronized的,因此无法利用多线程进行注册来缩短启动耗时。


优化方案


这里的优化其实和Retroift Service 的注册机制类似,不同的Service注册时,其对应的元信息类(IRouteGroup)其实是不同的,因此只需要对对应的IRouteGroup加锁即可。


在completion的后半部分流程中,针对Provider实例生产的流程也需要进行单独加锁,避免多次调用init函数。



收益


根据线下收集的数据 配置了20+预加载的Service Method, 预期收益 10~20ms (中端机) 。


其他


后续将继续结合自身业务现状以及其他一线大厂分享的样例,在 x2c、class verify、禁用JIT、 disableDex2AOT等方面继续尝试优化。


如果通过本文对你有所收获,可以来个点赞、收藏、关注三连,后续将分享更多性能监控与优化相关的文章。


也可以关注个人公众号:编程物语


image.png


本文相关测试代码已分享至github: github.com/Knight-ZXW/…


APM性能监控与优化专栏


性能优化专栏历史文章:


作者:卓修武K
来源:juejin.cn/post/7249228528573513789
tbody>
文章地址
Android平台下的cpu利用率优化实现juejin.cn/post/724324…
抖音消息调度优化启动速度方案实践juejin.cn/post/721766…
扒一扒抖音是如何做线程优化的juejin.cn/post/721244…
监控Android Looper Message调度的另一种姿势juejin.cn/post/713974…
Android 高版本采集系统CPU使用率的方式juejin.cn/post/713503…
Android 平台下的 Method Trace 实现及应用juejin.cn/post/710713…
Android 如何解决使用SharedPreferences 造成的卡顿、ANR问题juejin.cn/post/705476…
基于JVMTI 实现性能监控juejin.cn/post/694278…
收起阅读 »

Flutter卡片分享功能实现:将你的内容分享给世界

前言 在app中,在实现分享功能的时候,通常会有一种以卡片形式展示和分享内容的分享方式。这种功能可以将信息以整洁、易读的方式呈现给用户,使他们能够快速了解内容的关键信息,并将其分享给其他人。那么在这篇文章中,就一起来探索下,如何使用Flutter来实现这卡片...
继续阅读 »

前言



在app中,在实现分享功能的时候,通常会有一种以卡片形式展示和分享内容的分享方式。这种功能可以将信息以整洁、易读的方式呈现给用户,使他们能够快速了解内容的关键信息,并将其分享给其他人。那么在这篇文章中,就一起来探索下,如何使用Flutter来实现这卡片分享功能吧~


源代码:http://www.aliyundrive.com/s/FH7Xc2vyL…


效果图:



实现方案


为了卡片的样式的灵活性和可定制性,本文采用对组件进行截图的方式来实现卡片保存分享的功能,选择这个方案还有一点好处就是充分利用了flutter跨平台的优势。当然也会有一定的缺点,例如对于性能的考虑,当对复杂的嵌套卡片组件截图时,渲染和图像转换的计算量是需要考虑的,当然也可以选择忽略不计~


创建弹窗&卡片布局


在生成分享卡片的同时还会有其他的操作选项,例如保存图片、复制链接、浏览器打开等等,所以通常分享卡片的形式为弹窗形式,中间为分享卡片主体,剩余空间为操作项。



操作项组件封装:


class ImageDialog extends StatelessWidget {
const ImageDialog({
Key? key,
required this.items,
...
}) : super(key: key);
final List<ItemLittleView> items;
...

@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
...
child: Row(
children: items
.map((e) => itemLittleView(
label: e.label,
icon: e.icon,
onTap: () {
Navigator.pop(context);
e.onTap?.call();
}))
.toList()),
),
],
);
}

Widget itemLittleView({
required String label,
required String icon,
Function()? onTap,
}) =>
InkWell(
onTap: onTap,
child: Container(
margin: EdgeInsets.only(right: 10),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Container(
//图标
),
Container(
//文字
),
],
),
),
);
}
}

class ItemLittleView {
final String label;
final String icon;
final Function()? onTap;

ItemLittleView({required this.label, required this.icon, this.onTap});
}

需要加入新的操作项时,只需要简单的添加一个ItemLittleView即可。


ImageDialog(
items: [
ItemLittleView(
label: "生成图片 ",
icon: "assets/images/icon/ic_down.png",
onTap: () => doSaveImage(),
),
...
],
),

卡片的布局则根据业务的需求自定义即可,本文也只是一个简单的例子。


渲染并截取组件截图


在flutter中可以使用RepaintBoundary将将组件渲染为图像。



  • 第一步:定义全局的GlobalKey,用于获取卡片布局组件的引用


var repaintKey = GlobalKey();

RepaintBoundary(
key: repaintKey,
//分享卡片
child: shareImage(),
),


  • 第二步:使用RenderRepaintBoundary的toImage方法将其转换为图像


Future<Uint8List> getImageData() async {
BuildContext buildContext = repaintKey.currentContext!;
//用于存储截取的图片数据
var imageBytes;
//通过 buildContext 获取到 RenderRepaintBoundary 对象,表示要截取的组件边界
RenderRepaintBoundary boundary =
buildContext.findRenderObject() as RenderRepaintBoundary;

//这行代码获取设备的像素密度,用于设置截取图片的像素密度
double dpr = ui.window.devicePixelRatio;
//将边界对象 boundary 转换为图像,使用指定的像素密度。
ui.Image image = await boundary.toImage(pixelRatio: dpr);
// image.width
//将图像转换为ByteData数据,指定了数据格式为 PNG 格式。
ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
//将ByteData数据转换为Uint8List 类型的图片数据。
imageBytes = byteData!.buffer.asUint8List();
return imageBytes;
}


  • 第三步:获取权限&保存截图


//获取权限
_requestPermission() async {
Map<Permission, PermissionStatus> statuses = await [
Permission.storage,
].request();

final info = statuses[Permission.storage].toString();
}

Future<String> saveImage(Uint8List imageByte) async {
//将回调拿到的Uint8List格式的图片转换为File格式
//获取临时目录
var tempDir = await getTemporaryDirectory();
//生成file文件格式
var file =
await File('${tempDir.path}/image_${DateTime.now().millisecond}.png')
.create();
//转成file文件
file.writeAsBytesSync(imageByte);
print("${file.path}");
String path = file.path;
return path;
}

//最后通过image_gallery_saver来保存图片
/// 执行存储图片到本地相册
void doSaveImage() async {
await _requestPermission();
Uint8List data = await getImageData();
String path = await saveImage(data);
final result = await ImageGallerySaver.saveFile(path);
showDialog(
context: context,
builder: (_) {
return AlertDialog(
title: Text("保存成功!"),
);
});
}

到这里,分享卡片的功能就实现啦~


总结


在本文中,我们探索了使用Flutter实现卡片分享功能的过程。在开发app时,卡片分享功能可以为用户提供更好的交互和共享体验,我猜大家在开发的过程中也会有很大的概率碰上这样的需求。通过设计精美的卡片样式,可以帮助更快速的推广APP。


关于我


Hello,我是Taxze,如果您觉得文章对您有价值,希望您能给我的文章点个❤️,有问题需要联系我的话:我在这里 ,也可以通过掘金的新的私信功能联系到我。如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章~万

作者:编程的平行世界
来源:juejin.cn/post/7249347871564300345
一哪天我进步了呢?😝

收起阅读 »

某外包面试官:你还不会uniapp?😲😲

uniapp主要文件夹 pages.json 配置文件,全局页面路径配置,应用的状态栏、导航条、标题、窗口背景色设置等 main.js 入口文件,主要作用是初始化vue实例、定义全局组件、使用需要的插件如 vuex,注意uniapp无法使用vue-router...
继续阅读 »

uniapp主要文件夹


pages.json


配置文件,全局页面路径配置,应用的状态栏、导航条、标题、窗口背景色设置等


main.js


入口文件,主要作用是初始化vue实例、定义全局组件、使用需要的插件如 vuex,注意uniapp无法使用vue-router,路由须在pages.json中进行配置。如果开发者坚持使用vue-router,可以在插件市场找到转换插件。


App.vue


是uni-app的主组件,所有页面都是在App.vue下进行切换的,是页面入口文件。但App.vue本身不是页面,这里不能编写视图元素。除此之外,应用生命周期仅可在App.vue中监听,在页面监听无效。


pages


页面管理部分用于存放页面或者组件


manifest.json


文件是应用的配置文件,用于指定应用的名称、图标、权限等。HBuilderX 创建的工程此文件在根目录,CLI 创建的工程此文件在 src 目录。


package.json


配置扩展,详情内容请见官网描述package.json概述


uni-app属性的绑定


vue和uni-app动态绑定一个变量的值为元素的某个属性的时候,会在属性前面加上冒号":";


uni-app中的本地数据存储和接收


// 存储:
uni.setStorage({key:“属性名”,data:“值”}) //异步
ni.setStorageSync(KEY,DATA) //同步
//接收:
ni.getStorage({key:“属性名”,success(res){res.data}}) //异步
uni.getStorageSync(KEY) //同步
//移除:
uni.removeStorage(OBJECT) //从本地缓存中异步移除指定 key。
uni.removeStorageSync(KEY) //从本地缓存中同步移除指定 key。
//清除:
uni.clearStorage() //清理本地数据缓存。
ni.clearStorageSync() //同步清理本地数据缓存。

页面调用接口



  • getApp() 函数 用于获取当前应用实例,一般用于获取globalData

  • getCurrentPages() 函数 用于获取当前页面栈的实例,以数组形式按栈的顺序给出,第一个元素为首页,最后一个元素为当前页面。

  • uni.emit(eventName,OBJECT) uni.emit(eventName,OBJECT)uni.emit(eventName,OBJECT) uni.on(eventName,callback) :触发和监听全局的自定义事件

  • uni.once(eventName,callback):监听全局的自定义事件。uni.once(eventName,callback):监听全局的自定义事件。

  • 事件可以由 uni.once(eventName,callback):监听全局的自定义事件。

  • 事件可以由uni.emit 触发,但是只触发一次,在第一次触发之后移除监听器。

  • uni.$off([eventName, callback]):移除全局自定义事件监听器。


uni-app的生命周期


  beforeCreate(创建前)
created(创建后)
beforeMount(载入前,挂载)
mounted(载入后)
beforeUpdate(更新前)
updated(更新后)
beforeDestroy(销毁前)
destroyed(销毁后)

路由与页面跳转



  1. uni.navigateTo 不关闭当前页的情况下跳转其他页面

  2. uni.redirectTo 关闭当前页的情况下跳转其他页面

  3. uni.switchTab 跳转去tabBar,关闭其他非tabBar页面

  4. uni.reLaunch 关闭所有页面,跳转到其他页面

  5. uni.navigateBack 返回

  6. edxit 退出app


跨端适配—条件编译


1. #ifdef APP-PLUS
需条件编译的代码 //app
#endif
2. #ifndef H5
需条件编译的代码 //H5
endif
3. #ifdef H5 || MP-WEIXIN
需条件编译的代码 //小程序
#endif

uniapp上传文件时使用的api


uni.uploadFile({
url: '要上传的地址',
fileType:'image',
filePath:'图片路径',
name:'文件对应的key',
success: function(res){
console.log(res)
},})

uniapp选择文件、图片上传


选择文件


uni.chooseFile({
count: 6, //默认100
extension:['.zip','.doc'],
success: function (res) {
console.log(JSON.stringify(res.tempFilePaths));
}
});

选择图片文件


uni.chooseFile({
count: 10,
type: 'image',
success (res) {
// tempFilePath可以作为img标签的src属性显示图片
const tempFilePaths = res.tempFiles
}
})

uni-app的页面传参方式


第一种:
直接在跳转页面的URL路径后面拼接,如果是数组或者json格式记得转成字符串格式哦。然后再目的页面onload里面接受即可


//现页面
uni.navigateTo({
url:'/pages/notice/notice?id=1'
})
//目的页面接收
//这里用onshow()也可以
onLoad(options) {
var data = options.id;
console.log(data)
}

第二种:
直接在main.js注册全局变量



  • 例如我用的是vue框架,先在main.js文件注册变量myName

  • Vue.prototype.myName= '玛卡巴卡';

  • 在目标文件读取全局变量,注意全局变量不要与我们在当前页声明的变量名重复

  • let name = this.myName; // 玛卡巴卡


第三种:设置本地存储也比较方便



  • 这里建议使用uni.setStorageSync这个是同步,不会出现去了目标页面取值取不到的问题

  • uni.setStorage是异步存值,获取值也是一样建议使用uni.getStorageSync


uniapp实现下拉刷新


实现下拉刷新需要用到uni.onPullDownRefresh和uni.stopPullDownRefresh这个两个函数,函数与生命周期同等级可以监听页面下拉动作


uniapp实现上拉加载


uniapp中的上拉加载是通过onReachBottom()这个生命周期函数实现,当下拉触底时就会触发。我们可以在此函数内调用分页接口请求数据,用以获取更多的数据


scroll-view吸顶问题



  • 问题:
    scroll-view 是常会用到的一个标签,我们可以使用 position:sticky 加一个边界条件例如top:0
    属性实现一个粘性布局,在容器滚动的时候,如果我们的顶部标签栏触碰到了顶部就不会再滚动了,而是固定在顶部。但是在小程序中如果你在scroll-view元素中直接为子元素使用sticky属性,你给予sticky的元素在到达父元素的底部时会失效。

  • 解决:
    在scroll-view元素中,再增加一层view元素,然后在再将使用了sticky属性的子元素放入view中,就可以实现粘贴在某个位置的效果了


ios输入框字体移动bug



  • 问题:在IOS端有时,当输入框在输入后没有点击其他位置使输入框失焦的话,如果滚动窗口内部的字体也会跟着滚动

  • 解决:



  1. 尝试了下,发现textarea不会和input一样出现字体随着页面滚动的情况,这是一个兼容方案

  2. 还有个不优雅的方案是输入完成后使用其他事件让其失焦或者disable,例如弹窗或者弹出层出来的时候可以暂时让input禁止,然后弹窗交互完成后再放开


rpx、px、em、rem、%、vh、vw的区别是什么?



  • rpx 相当于把屏幕宽度分为750份,1份就是1rpx

  • px 绝对单位,页面按精确像素展示

  • em 相对单位,相对于它的父节点字体进行计算

  • rem 相对单位,相对根节点html的字体大小来计算

  • % 一般来说就是相对于父元素

  • vh 视窗高度,1vh等于视窗高度的1%

  • vw 视窗宽度,1vw等于视窗宽度的1%


uni-app的优缺点



  • 优点:



  1. 一套代码可以生成多端

  2. 学习成本低,语法是vue的,组件是小程序的

  3. 拓展能力强

  4. 使用HBuilderX开发,支持vue语法

  5. 突破了系统对H5条用原生能力的限制



  • 缺点:



  1. 问世时间短,很多地方不完善

  2. 社区不大

  3. 官方对问题的反馈不及时

  4. 在Android平台上比微信小程序和iOS差

  5. 文件命
    作者:margin_100px
    来源:juejin.cn/post/7245936314851622970
    名受限

收起阅读 »

日常开发中,提升技术的13个建议

前言 大家好,我是田螺。 最近有位读者问我:田螺哥,日常开发中,都是在做业务需求,如何提升自己的技术呢? 所以,本文田螺哥整理了提升技术的13个建议,小伙伴们,一起加油。 1. 打好基础,深入学习语言特性 比如,对于Java程序员来说,要了解Java语言的基...
继续阅读 »

前言


大家好,我是田螺


最近有位读者问我:田螺哥,日常开发中,都是在做业务需求,如何提升自己的技术呢? 所以,本文田螺哥整理了提升技术的13个建议,小伙伴们,一起加油。



1. 打好基础,深入学习语言特性


比如,对于Java程序员来说,要了解Java语言的基本概念和核心特性,包括面向对象编程、集合框架、异常处理、多线程等等。可以通过阅读Java的官方文档、教程、参考书籍或在线资源来学习。


如果最基本的基础都不扎实,就不要谈什么提升技术啦。 比如说:



  • 你知道HashMap和ConcurrentHashMap的区别嘛?

  • 在什么时候使用ConcurrentHashMap?操作文件的时候

  • 你知道在finally块中释放资源嘛?

  • 你知道在哪些场景适合用泛型嘛?


因此,要提升自身技术,首先就是要把基础打扎实。 有些小伙伴说,上班没时间学基础呀,其实不是这样的,基础这玩意,每天地铁上下班看看,下班后回到家在看看,周末在家看看,多点写写代码,一般一两个月,你的基础就很好啦。


又有些小伙伴说,如何提升Java基础呢? 可以:



  • 阅读Java相关书籍或教程,如Java编程思想、Java核心技术、Java虚拟机、菜鸟教程等

  • 阅读Java博客和社区参与讨论:关注Java领域的博客、论坛和社区,了解最新的技术动态和解决方案,与其他开发者交流。

  • 多实践,多敲代码:在B站找个Java基础视频看,平时多实践、多敲代码



2. 熟悉掌握常用的开发工具


工欲善其事,必先利其器. 所以一位好的程序员,往往编码效率就更高。而提升编码效率,一般要求熟悉并灵活应用工具.比如Eclipse、IntelliJ IDEA、Maven、Navicat等。熟悉运用这些工具,可以提高开发效率。


我举个例子,比如你熟悉掌握IntelliJ IDEA的快捷键,三两下就把实体类的setter和getter方法生成了,而有些的程序员,还在一行一行慢慢敲。。



3. 日常工作中,总结你踩过的坑


优秀的程序员,之所以优秀,是因为他会总结踩过的坑,避免重蹈覆辙。所以,田螺哥建议你,日常开发中,如果你踩了哪些坑,就需要总结下来.茶余饭后,再温习温习.


比如,你知道:



  • Redis分布式锁使用,可能会有哪些坑嘛?

  • 线程池使用有哪些坑?

  • Java日期处理又又哪些坑嘛?

  • Arrays.asList使用可能有哪些坑?


如果一时间忘记的话,可以看下我以前的这些文章:



这些都是我工作总结出来的,也希望你们日常开发中,遇到哪些坑,都总结下来哈。



4.工作中,阅读你项目优秀的代码和设计文档


孔子说,三人行,必有我师。大家平时在看代码的时候,不要总吐槽着项目的烂代码。其实,可以多点关注写得优秀的代码,然后看懂别人为什么这些写,仿造着来写。


当然,一些好的设计文档也是:人家为什么这么设计,好处在哪里,不足又在哪里,如果是你来设计,你如何思考等等。把好的设计,读懂后,记录下来,变成自己的知识.



5.日常工作中,总结一些通用的技术方案.


在日常工作中呢,注意整理一些通用的技术方案。


比如幂等设计、分布式锁如何设计、分布式事务设计、接口优化、限流设计、分库分表设计、深分页问题解决等等. 大家可以看下我之前的一些通用方案设计的文章哈:



当然,田螺哥也建议你,日常开发中,把自己遇到的一些通用设计方案总结下来,熟悉掌握这些通用技术方案。



6.参与技术讨论,积极技术分享


参与技术讨论和交流,可以有助于你与其他Java开发者分享经验、解决问题和学习新知识。进行技术分享,可以加深自己的理解、建立专业声誉、促进个人成长、为技术社区做贡献等等。


比如你做需求遇到的一些难题,都可以跟有经验的同事、或者技术leader讨论讨论。一些常见的难题,讨论完可以记录下来,然后做技术分享



7. 主人翁意识,积极攻克项目的难题


作为一名开发工程师,具备主人翁意识并积极攻克项目的难题,是非常重要的。遇到项目中的比较棘手问题时,先不管是谁的问题,我们都要持有主人翁意识,积极主动地找到解决方案并采取行动。


而在技术找解决方案的过程,我们也就成长了。当攻克问题后,你也获得领导的认可,好绩效不远了,一举多得



8. 思考项目中,哪些可以提升效率


日常开发中,几乎大多数程序员都是在进行增删改查。如何如何避免自己成为平凡的增删改查程序员呢。


我觉得可以这样做:平时工作中,思考项目中,有哪些可以提升的效率。包括熟悉开发工具、掌握适当的调试技巧、熟悉常用框架、持续学习和关注技术发展等等。


比如:



  • 好的的debug调试技巧,可以让你快速找到问题

  • 再比如一个插件easyyapi可以一键让你快速生成yapi接口文档,而不用一个一个字段手工敲接口文档。


当然,日常开发中,还有很多可以提升效率的技巧/工具,等待我们去发现



9. 熟悉你的业务,让自己不容易被替代


我们普通程序员,多数都是做业务的。一般工作个五年以上,水平差不了太多。如何避免自己被淘汰呢?我个人建议是,尽量做到熟悉你们做的业务,让你变得不容易被替代。



10. 多看看你的系统,可能存在哪些问题,如接口耗时、慢SQL等等


一般的系统,多多少少都有些问题。比如接口耗时过长、慢SQL、fullGC频繁等等。


首先需要掌握这些技能,比如如何优化接口,如何优化慢SQl、fullGC如何排查等等。大家可以看下这几篇文章哈:



11. 学以致用,将理论知识应用到实际项目中


很多小伙伴说,看过很多计算机相关的书,阅读过很多博客,背了很多八股文,依然做不好一个系统。


我觉得,大家可以多点思考,把平时积累的东西,应用到实际项目中。背八股文不是没用,你可以把它应用到实际开发中的。比如说,你看了田螺哥的文章,IO模型详解


这个表面看起来就是一个常见的八股文知识点,工作中似乎没用到。但是我在工作中,就用到这种类似的异步思想



比如发起一笔批量转账,但是批量转账处理比较耗时,这时候后端可以先告知前端转账提交成功,等到结果处理完,再通知前端结果即可。



再比如,你看完田螺哥的:MySQL索引15连问,抗住!,你是不是可以回头看看,你的系统中,那些sql的索引加的是否合理呢?是不是可以思考一下如何优化,对吧。因此,就是要学以致用



12. 阅读一些优秀框架的源码,如spring、rockectMq等等


如果你有空余的时间,就建议你看看一些优化框架的源码,比如spring、rockectMq等等。


对于spring源码的话,可以按模块来呀,比如aop,控制反转,spring事务等,你先写个demo,然后debug跟踪流程,通过调试器逐步跟踪源码执行过程,观察各个方法的调用关系和数据变化。最好是结合电子书一起,如(Spring源码深度解析这本书一起)


优秀框架的源码,我们可以学习到很多编码思想的,加油。



13. 多编码,少偷懒,养成编程的好习惯


作为程序员,一定要多打代码,不要偷懒,代码敲多了,你就会了。还有就是,少点偷懒,坚持!努力!养成热爱编程的好习惯


总之,提升技术需要不断学习、实践、总结和积累经验



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

来这公司一年碰到的问题比我过去10年都多

无意间发现我们 Kafka 管理平台的服务的 open files 和 CPU 监控异常,如下图,有一台机器 CPU 和 opfen files 指标持续在高位,尤其是 open files 达到了4w+。 原因分析 第一反应是这个服务请求很高?但是这个服...
继续阅读 »

无意间发现我们 Kafka 管理平台的服务的 open files 和 CPU 监控异常,如下图,有一台机器 CPU 和 opfen files 指标持续在高位,尤其是 open files 达到了4w+。




原因分析


第一反应是这个服务请求很高?但是这个服务是一个管理服务不应该有很高的请求量才对,打开监控一看,QPS少的可怜。



既然机器还在就找 devops 同学帮忙使用 Arthas 简单看下是什么线程导致的,竟然是 GC 线程,瞬时 CPU 几乎打满了。



查看了 GC 监控,每分钟 5~6 次相比其他的正常节点要多很多,并且耗时很长。


问题节点GC Count



正常节点GC Count



应该是代码出问题了,继续求助 devops 将线上有问题的机器拉了一份 dump,使用 MAT 工具分析了下,打开 dump 就提示了两个风险点,两个都像是指标相关的对象。



查看详情发现两个可疑对象,一个是 60+M 的 byte[], 一个是 60+M 的 map,都是指标相关的对象,问题应该出在指标上。



初步去排查了下代码,看是否有自定义指标之类的,发现一个 job 会对指标进行操作,就把 job 停了一段时间,GC 少了很多,但是 open files 只减少了一点点, 很明显不是根本原因。




继续深入,将 byte[] 保存成字符串查看(确实文本也有60+M),发现全是 JMX 的指标数据,我们的系统使用了两种指标一种是Micrometer,一种是 prometheus-jmx-exporter,这个 byte[] 数组就是第二种指标的数据。



并且这些指标中发现有非常多的 kafka_producer 开头的指标。



为了验证是否属于 JMX 的指标数据,再次求助 devops 拉取线上有问题机器的 JMX 指标接口, 看返回的是否是 60M+ 的指标数据,发现根本拉不下来。



到此基本确认问题出在 JMX 指标上, 那这些指标谁注册的呢?


通过指标名称在源代码里搜索,发现是来自org.apache.kafka.common.network.Selector.SelectorMetrics,是 kafka-client注册的指标。


具体的创建顺序如下,每创建一个KafkaProducer,就会以 client id 为唯一标识创建一个SelectorMetrics, 而创建 KafkaProducer 会创建一个守护线程,并开启一个长连接定时去 Broker 拉取/更新 Metadata 信息,这个就是open files飙高的根本原因。


KafkaProducer -> Sender -> Selector -> SelectorMetrics



难道创建了很多 KafkaProducer???查看构造方法调用的地方,找到了真凶。。。



这段代码是为了支持延迟消息,业务服务每发一个延迟消息,就会执行一次这段逻辑, 就会创建一个 KafkaProducer,并且会随着收到的消息越来越多导致创建的 KafkaProducer 越来越多,直至系统无法承受。。。


庆幸的是我们延迟消息并不是太多,没有直接把系统给打挂掉


那为什么只有一个节点会有问题,其他节点没有问题呢?这个比较简单直接说结果了,就是这段消费逻辑消费的 topic 只有一个分区....


解决方案:


由于 Kafka 管理平台会连接多个 Broker,所以此处将创建的 KafkaProducer 根据 Cluster 缓存起来进行复用。


问题总结:



  1. KafkaProducer 本身是一个很重的对象,并且线程安全,创建的时候注意考虑场景

  2. 此次问题完全是凭运气提前发现了,证明监控系统也存在不够完善的地方, 我们使用 Prometheus 的标准差函数 (stddev() by()) 配置了资源严重倾斜的监控告警,防止出现类似的问题。

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

三分钟,趁同事上厕所的时间,我覆盖了公司的正式环境数据

大家好啊,又跟大家见面了,最近有个需求就是批量修改公司的数据报表,正式环境!! 而且要执行update!!update it_xtgnyhcebg I set taskStatus = XXX 而且是没有加where条件的,相当于全表更新,这可马虎不得,我们...
继续阅读 »

大家好啊,又跟大家见面了,最近有个需求就是批量修改公司的数据报表,正式环境!!
而且要执行update!!

update it_xtgnyhcebg I set taskStatus = XXX

而且是没有加where条件的,相当于全表更新,这可马虎不得,我们在任何操作正式数据库之前一定一定要对数据库备份!!不要问我怎么知道的,因为我就因为有一次把测试环境的数据覆盖到正式环境去了。。。


在这里插入图片描述


别到时候就后悔莫及,那是没有用的!


在这里插入图片描述
在这里插入图片描述


由于这个需求是需要在跨库操作的,所以我们在查询数据的时候需要带上库的名称,例如这样

SELECT
*
FROM
BPM00001.ACT_HI_PROCINST P
LEFT JOIN BPM00001.ACT_HI_VARINST V ON V.PROC_INST_ID_ = P.ID_
AND V.NAME_ = '__RESULE'


这样如果我们在任何一个库里面,只要在一个mysql服务里面都可以访问到这个数据
查出这个表之后
在这里插入图片描述
我们需要根据这里的内容显示出不同的东西
就例如说是“APPROVAL”我就显示“已通过”
这就类似与java中的Switch,其实sql也能实现这样的效果
如下:
在这里插入图片描述
这就是sql的case语句的使用
有了这些数据之后我们就可以更新数据表了,回到我们之前讨论过的,这是及其危险的操作
我们先把要set的值给拿出来
在这里插入图片描述


在这里插入图片描述
但是我们怎么知道这个里面的主键呢?
你如果直接这么加,肯定是不行的
在这里插入图片描述
所以我们需要在sql后面加入这样的一条语句
在这里插入图片描述
注意,这个语句一定要写在set语句的里面,这样sql就能依据里面判断的条件进行一一赋值
最后,将这个sql语句执行到生产库中


拓展:


作为查询语句的key绝对不能重复,否则会失败(找bug找了半天的人的善意提醒)
例如上面的语句中P.BUSINESS_KEY_必须要保证是唯一的!!


在这里插入图片描述
成功执行!!!
怎么样,这些sql的小妙招你学会了吗?


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

安卓知识点-应届生扫盲安卓WebView

作者 大家好,我叫Jack冯; 本人20年硕士毕业于广东工业大学,于2020年6月加入37手游安卓团队; 目前主要负责海外游戏发行安卓相关开发。 背景 最近在接触活动相关需求,其中涉及到一个安卓的WebView; 刚毕业的我,对安卓知识积累比较少,所以在这里对...
继续阅读 »

作者


大家好,我叫Jack冯;


本人20年硕士毕业于广东工业大学,于2020年6月加入37手游安卓团队;


目前主要负责海外游戏发行安卓相关开发。


背景


最近在接触活动相关需求,其中涉及到一个安卓的WebView;


刚毕业的我,对安卓知识积累比较少,所以在这里对Webview进行相关学习,希望自己可以在安卓方面逐步积累。


Webview介绍


1、关于MockView


( 1 ) 在targetSdkVersion 28/29的工程里面查看WebView继承关系

java.lang.Object
↳ android.view.View
↳ android.view.ViewGroup
↳ android.widget.FrameLayout
↳ android.layoutlib.bridge.MockView
↳ android.webkit.WebView


( 2 ) 使用26/27等低版本SDK,查看源码中的WebView 继承关系

java.lang.Object
↳ android.view.View
↳ android.view.ViewGroup
↳ android.widget.AbsoluteLayout
↳ android.webkit.WebView

( 3 )对比


两种方式对比,AbsoluteLayout和FrameLayout都是重写ViewGroup的方法,如与布局参数配置相关的 generateDefaultLayoutParams()、checkLayoutParams()等。两种方式明显不同的是多了一层MockView 。这里来看看MockView是什么:

public class MockView extends FrameLayout{
...
//创建方式
public MockView(Context context) {...}
public MockView(Context context,AttributeSet attrs) {...}
public MockView(Context context,AttributeSet attrs,int defStyleRes) {...}
//重写添加view方法
@Override
public void addView(View child){...}
@Override
public void addView(View child,int index){...}
@Override
public void addView(View child,int width,int height){...}
@Override
public void addView(View child,ViewGroup.LayoutParams params){...}
@Override
public void addView(View child,int index,ViewGroup.LayoutParams params){...}
public void setText(CharSequence text){...}
public void setGravity(int gravity){...}
}

MockView,译为"虚假的view"。


谷歌发布的Sdk其实只是为了提供App开发运行接口,实际运行时候替换为当前系统的Sdk。


具体说就是当谷歌在新的系统(Framework)版本上准备对WebView实现机制进行改动,同时又希望把新的sdk提前发出来,不影响用到WebView的App开发,于是谷歌提供给Android开发的sdk中让WebView继承自MockView,这个WebView只是暴露了接口,没有具体实现;这样当谷歌关于WebView新的实现做好,利用WebView,app也就做好了


2、基本使用


(1)创建


①一般方式:

WebView webView = findViewById(R.id.webview);

②建议方式:

LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
mWebView = new WebView(getApplicationContext());
mWebView.setLayoutParams(params);

好处:构建不用依赖本地xml文件,自定义页面参数;手动销毁避免内存泄露;


③更多方式 : 继承Webview和主要API等进行拓展

public class BaseWebView extends WebView {...}
public class BaseWebClient extends WebClient {...}
public class BaseWebChromeClient extends WebChromeClient {...}

(2)加载


① 加载某个网页

webView.loadUrl("http://www.google.com/");

②新建assets目录,将html文件放到目录下,通过路径加载本地页面

 webView.loadUrl("file:///android_asset/loadFailed.html");

③使用evaluateJavascript(String script, ValueCallback resultCallback)方法加载,(Android4.4+)

mWebView.evaluateJavascript("file:///android_asset/javascript.html",new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
Log.e("测试", "onReceiveValue:"+value );
}
});

3、WebViewClient


当URL即将加载到当前窗口,如果没有提供WebViewClient,默认情况下WebView将使用Activity管理器为URL选择适当的处理器。


如果提供了WebViewClient,按自定义配置要求来继续加载URL。


(1)常用方法

//加载过程对url的处理(webview加载、系统浏览器加载、其他操作等)
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
super.shouldOverrideUrlLoading(view, url);
}
//加载失败页面
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl){
view.loadUrl("file:///android_asset/js_error.html");
}
//证书错误处理
@Override
public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
}
//开始加载页面(可自定义页面加载计时等)
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Log.e(TAG, "onPageStarted:" + url);
}
//结束加载页面
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.e(TAG, "onPageFinished: " + url);
}

(2)关于shouldOverrideUrlLoading


如果在点击链接加载过程需要更多的控制,就可以在WebViewClient()中重写shouldOverrideUrlLoading()方法。


涉及shouldOverrideUrlLoading()的情形,大概分为三种:


(1)没有设定setWebViewClient(),点击链接使用默认浏览器打开;


(2)设定setWebViewClient(new WebViewClient()),默认shouldOverrideUrlLoading()返回false,点击链接在Webview加载;


(3)设定、重写shouldOverrideUrlLoading()


返回true:可由应用代码处理该 url,WebView 中止处理(若重写方法没加上view.loadUrl(url),不加载);


返回false:由 WebView 处理加载该 url。(即使没加上view.loadUrl(url),也会在当前Webview加载)


【一般应用】

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
super.shouldOverrideUrlLoading(view, url);
if (url != null) {
if (!(url.startsWith("http") || url.startsWith("https"))) {
return true;
}
//重定向到别的页面
//view.loadUrl("file:///android_asset/javascript.html");
//区别不同链接加载
view.loadUrl(url);
}
return true;
}

(3)常见误区


【误区1】 : 需要重写 shouldOverrideUrlLoading 方法才能阻止浏览器打开页面。


解释:WebViewClient 源码中 shouldOverrideUrlLoading 方法已经返回 false,不设定setWebViewClient(),默认使用系统浏览器加载。如果重写该方法并返回true, 就可以实现在app页面中加载新链接而不去打开浏览器。


【误区2】 : 每一个url加载过程都会经过 shouldOverrideUrlLoading 方法。


Q1:加载一定会触发shouldOverrideUrlLoading?


Q2:触发时机一定在onPageStarted调用之前?


解释:关于shouldOverrideUrlLoading的触发


1)如果在点击页面链接时通过标签跳转,触发方法如下:


shouldOverrideUrlLoading() —> onPageStarted()—> onPageFinished()


2)如果使用loadUrl加载时,触发方法如下:


onPageStarted()—>onPageFinished()


3)如果使用loadUrl加载重定向地址时,触发方法如下:


shouldOverrideUrlLoadings—>onPageStarted —> onPageFinished


ps:多次重定向的过程,


onPage1Started


—>shouldOverrideUrlLoadings


—>onPage2Started —> xxx...


—> onPageNFinished


结论:shouldOverrideUrlLoading()方法不是每次加载都会调用,WebView的前进、后退等不会调用shouldOverrideUrlLoading方法;非loadUrl方式加载 或者 是重定向的,才会调用shouldOverrideUrlLoading方法。


【误区3 】: 重写 shouldOverrideUrlLoading 方法返回true比false的区别,多调用一次onPageStarted()和onPageFinished()。


解释:返回True:应用代码处理url;返回False,则由 WebView 处理加载 url。


ps:低版本系统(华为6.0),测试 False比True会多调用一次onPageStarted()和onPageFinished(),这点还在求证中。


4、WebChromeClient


对比WebviewClient , 添加了处理JavaScript对话框,图标,标题和进度等。


处理对象 : 影响浏览器的事件


(1)常用方法:

//alert弹出框
public boolean onJsAlert(WebView view, String url, String message,JsResult result){
return true;//true表示拦截
}

//confirm弹出框
public boolean onJsConfirm(WebView view, String url, String message,JsResult result){
return false;//false则允许弹出
}

public boolean onJsPrompt(WebView view, String url, String message,String defaultValue, JsPromptResult result)

//打印 console 信息。return true只显示log,不显示js控制台的输入;false则都显示出来
public boolean onConsoleMessage(ConsoleMessage consoleMessage){
Log.e("测试", "consoleMessage:"+consoleMessage.message());
}

//通知程序当前页面加载进度,结合ProgressBar显示
public void onProgressChanged(WebView view, int newProgress){
if (newProgress < 100) {
String progress = newProgress + "%";
Log.e("测试", "加载进度:"+progress);
webProgress.setProgress(newProgress);
}
}

(2)拦截示例:


JsResult.comfirm() --> 确定按钮的调用方法


JsResult.cancle() --> 取消按钮


示例:拦截H5的弹框,并显示自定义弹框,点击按钮后重定向页面到别的url

@Override
public boolean onJsConfirm(final WebView view, String url, String message, final JsResult result) {
Log.e("测试", "onJsConfirm:"+url+",message:"+message+",jsResult:"+result.toString());
new AlertDialog.Builder(chromeContext)
.setTitle("拦截JsConfirm显示!")
.setMessage(message)
.setPositiveButton(android.R.string.ok,
new AlertDialog.OnClickListener() {
public void onClick(DialogInterface dialog,int which) {
//重定向页面
view.loadUrl("file:///android_asset/javascript.html");
result.confirm();
}
}).setCancelable(false).create().show();
return true;
}

5、WebSettings


用于页面状态设置\插件支持等配置.


(1)常用方法

WebSettings webSettings = webView.getSettings();
/**
* 设置缓存模式、支持Js调用、缩放按钮、访问文件等
*/
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
webSettings.setJavaScriptEnabled(true);
webSettings.setSupportZoom(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setDisplayZoomControls(true);

//允许WebView使用File协议,访问本地私有目录的文件
webSettings.setAllowFileAccess(true);

//允许通过file url加载的JS页面读取本地文件
webSettings.setAllowFileAccessFromFileURLs(true);

//允许通过file url加载的JS页面可以访问其他来源内容,包括其他的文件和http,https等来源
webSettings.setAllowUniversalAccessFromFileURLs(true);
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
webSettings.setLoadsImagesAutomatically(true);
webSettings.setDefaultTextEncodingName("utf-8")

if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
}

结束语


过程中有问题或者需要交流的同学,可以扫描二维码加好友,然后进群进行问题和技术的交流等;


作者:37手游移动客户端团队
链接:https://juejin.cn/post/7245084484756144186
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

鹅厂组长,北漂 10 年,有房有车,做了一个违背祖宗的决定

前几天是 10 月 24 日,有关注股票的同学,相信大家都过了一个非常难忘的程序员节吧。在此,先祝各位朋友们身体健康,股票基金少亏点,最重要的是不被毕业。 抱歉,当了回标题党,不过在做这个决定之前确实纠结了很久,权衡了各种利弊,就我个人而言,不比「3Q 大战」...
继续阅读 »

前几天是 10 月 24 日,有关注股票的同学,相信大家都过了一个非常难忘的程序员节吧。在此,先祝各位朋友们身体健康,股票基金少亏点,最重要的是不被毕业。


抱歉,当了回标题党,不过在做这个决定之前确实纠结了很久,权衡了各种利弊,就我个人而言,不比「3Q 大战」时腾讯做的「艰难的决定」来的轻松。如今距离这个决定过去了快 3 个月,我也还在适应着这个决定带来的变化。


按照工作汇报的习惯,先说结论:



在北漂整整 10 年后,我回老家合肥上班了



做出这个决定的唯一原因:



没有北京户口,积分落户陪跑了三年,目测 45 岁之前落不上



户口搞不定,意味着孩子将来在北京只能考高职,这断然是不能接受的;所以一开始是打算在北京读几年小学后再回老家,我也能多赚点钱,两全其美。


因为我是一个人在北京,如果在北京上小学,就得让我老婆或者让我父母过来。可是我老婆的职业在北京很难就业,我父母年龄大了,北京人生地不熟的,而且那 P 点大的房子,住的也憋屈。而将来一定是要回去读书的,这相当于他们陪着我在北京折腾了。


或者我继续在北京打工赚钱,老婆孩子仍然在老家?之前的 6 年基本都是我老婆在教育和陪伴孩子,我除了逢年过节,每个月回去一到两趟。孩子天生过敏体质,经常要往医院跑,生病时我也帮不上忙,所以时常被抱怨”丧偶式育儿“,我也只能跟渣男一样说些”多喝热水“之类的废话。今年由于那啥,有整整 4 个多月没回家了,孩子都差点”笑问客从何处来“了。。。


5月中旬,积分落户截止,看到贴吧上网友晒出的分数和排名,预计今年的分数线是 105.4,而实际分数线是 105.42,比去年的 100.88 多了 4.54 分。而一般人的年自然增长分数是 4 分,这意味着如果没有特殊加分,永远赶不上分数线的增长。我今年的分数是 90.8,排名 60000 左右,每年 6000 个名额,即使没有人弯道超车,落户也得 10 年后了,孩子都上高一了,不能在初二之前搞到户口,就表示和大学说拜拜了。


经过我的一番仔细的测算,甚至用了杠杆原理和人品守恒定理等复杂公式,最终得到了如下结论:



我这辈子与北京户口无缘了



所以,思前想后,在没有户口的前提下,无论是老婆孩子来北京,还是继续之前的异地,都不是好的解决方案。既然将来孩子一定是在合肥高考,为了减少不必要的折腾,那就只剩唯一的选择了,我回合肥上班,兼顾下家里。


看上去是个挺自然的选择,但是:



我在腾讯是组长,团队 20 余人;回去是普通工程师,工资比腾讯打骨折



不得不说,合肥真的是互联网洼地,就没几个公司招人,更别说薪资匹配和管理岗位了。因此,回合肥意味着我要放弃”高薪“和来之不易的”管理“职位,从头开始,加上合肥这互联网环境,基本是给我的职业生涯判了死刑。所以在 5 月底之前就没考虑过这个选项,甚至 3 月份时还买了个显示器和 1.6m * 0.8m 的大桌子,在北京继续大干一场,而在之前的 10 年里,我都是用笔记本干活的,从未用过外接显示器。


5 月初,脉脉开始频繁传出毕业的事,我所在的部门因为是盈利的,没有毕业的风险。但是营收压力巨大,作为底层的管理者,每天需要处理非常非常多的来自上级、下级以及甲方的繁杂事务,上半年几乎都是凌晨 1 点之后才能睡觉。所以,回去当个普通工程师,每天干完手里的活就跑路,貌似也不是那么不能接受。毕竟自己也当过几年 leader 了,leader 对自己而言也没那么神秘,况且我这还是主动激流勇退,又不是被撸下来的。好吧,也只能这样安慰自己了,中年人,要学会跟自己和解。后面有空时,我分享下作为 leader 和普通工程师所看到的不一样的东西。


在艰难地说服自己接受之后,剩下的就是走各种流程了:

1. 5月底,联系在合肥工作的同学帮忙内推;6月初,通过面试。我就找了一家,其他家估计性价比不行,也不想继续面了
2. 6月底告诉总监,7月中旬告诉团队,陆续约或被约吃散伙饭
3. 7月29日,下午办完离职手续,晚上坐卧铺离开北京
4. 8月1日,到新公司报道

7 月份时,我还干了一件大事,耗时两整天,历经 1200 公里,不惧烈日与暴雨,把我的本田 125 踏板摩托车从北京骑到了合肥,没有拍视频,只能用高德的导航记录作为证据了:


北京骑摩托回合肥


这是导航中断的地方,晚上能见度不行,在山东花了 70 大洋,随便找了个宾馆住下了,第二天早上出发时拍的,发现居然是水泊梁山附近,差点落草为寇:


水泊梁山


骑车这两天,路上发生了挺多有意思的事,以后有时间再分享。到家那天,是我的结婚 10 周年纪念日,我没有提前说我要回来,更没说骑着摩托车回来,当我告诉孩子他妈时,问她我是不是很牛逼,得到的答复是:



我觉得你是傻逼



言归正传,在离开北京前几天,我找团队里的同学都聊了聊,对我的选择,非常鲜明的形成了两个派系:

1. 未婚 || 工作 5 年以内的:不理解,为啥放弃管理岗位,未来本可以有更好的发展的,太可惜了,打骨折的降薪更不能接受

2. 已婚 || 工作 5 年以上的:理解,支持,甚至羡慕;既然迟早都要回去,那就早点回,多陪陪家人,年龄大了更不好回;降薪很正常,跟房价也同步,不能既要又要

确实,不同的人生阶段有着不同的想法,我现在是第 2 阶段,需要兼顾家庭和工作了,不能像之前那样把工作当成唯一爱好了。


在家上班的日子挺好的,现在加班不多,就是稍微有点远,单趟得 1 个小时左右。晚上和周末可以陪孩子玩玩,虽然他不喜欢跟我玩🐶。哦,对了,我还有个重要任务 - 做饭和洗碗。真的是悔不当初啊,我就不应该说会做饭的,更不应该把饭做的那么好吃,现在变成我工作以外的最重要的业务了。。。


比较难受的是,现在公司的机器配置一般,M1 的 MBP,16G 内存,512G 硬盘,2K 显示器。除了 CPU 还行,内存和硬盘,都是快 10 年前的配置了,就这还得用上 3 年,想想就头疼,省钱省在刀刃上了,属于是。作为对比,腾讯的机器配置是:



M1 Pro MBP,32G 内存 + 1T SSD + 4K 显示器


客户端开发,再额外配置一台 27寸的 iMac(i9 + 32G内存 + 1T SSD)



由奢入俭难,在习惯了高配置机器后,现在的机器总觉得速度不行,即使很多时候,它和高配机没有区别。作为开发,尤其是客户端开发,AndroidStudio/Xcode 都是内存大户,16G 实在是捉襟见肘,非常影响搬砖效率。公司不允许用自己的电脑,否则我就自己买台 64G 内存的 MBP 干活用了。不过,换个角度,编译时间变长,公司提供了带薪摸鱼的机会,也可以算是个福利🐶


另外,比较失落的就是每个月发工资的日子了,比之前少了太多了,说没感觉是不可能的,还在努力适应中。不过这都是小事,毕竟年底发年终奖时,会更加失落,hhhh😭😭😭😭


先写这么多吧,后面有时间的话,再分享一些有意思的事吧,工作上的或生活上的。


遥想去年码农节时,我还在考虑把房子从昌平换到海淀,好让孩子能有个“海淀学籍”,当时还做了点笔记:


买房笔记


没想到,一年后的我回合肥了,更想不到一年后的腾讯,股价竟然从 500 跌到 206 了(10月28日,200.8 了)。真的是世事难料,大家保重身体,好好活着,多陪陪家人,一起静待春暖花开💪🏻💪🏻


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

分享Android App的几个核心概念

Application启动 点击桌面图标启动App(如下流程图) 针对以上流程图示: ActivityManagerService#startProcessLocked() Process#start() ActivityThread#main(),入口分...
继续阅读 »

Application启动


点击桌面图标启动App(如下流程图)


1240.jpg


针对以上流程图示:



  • ActivityManagerService#startProcessLocked()

  • Process#start()

  • ActivityThread#main(),入口分析的地方

  • ActivityThread#attach(),这个里面的逻辑很核心 ActivityManagerService#attachApplication(),通过Binder机制调用了ActivityManagerService的attachApplication

  • ActivityManagerService#attachApplicationLocked(),整个应用进程已经启动起来

  • ActivityManagerService#thread.bindApplication,具体回到ActivityThread

  • ActivityThread.ApplicationThread#bindApplication(),最后看到sendMessage处理bind逻辑

  • ActivityThread#handleBindApplication(),设置进程的pid,初始化进程信息

  • ActivityThread#mInstrumentation.callApplicationOnCreate,看到Application进入onCreate()方法中,这就是从最开始main()方法开始到最后的Application的onCreate()的创建过程


Window创建


如何创建Window


在创建Activity实例的同时,会调用Activity的内部方法attach方法完成window的初始化。Activity类中相关源码如下所示:

final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
//创建 PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
}


  • Window是一个抽象类,具体实现是PhoneWindow。PhoneWindow中有个内部类DecorView,通过创建DecorView来加载Activity中设置的布局R.layout.activity_main

  • 创建Window需要通过WindowManager创建,通过WindowManager将DecorView加载其中,并将DecorView交给ViewRoot,进行视图绘制以及其他交互


Android组件设计


ActivityManagerService




  • 启动组件



    • 组件启动时,检查其所要运行在的进程是否已创建。如果已经创建,就直接通知它加载组件。否则,先将该进程创建起来,再通知它加载组件。




  • 关闭组件



    • 组件关闭时,其所运行在的进程无需关闭,这样就可以让组件重新打开时得到快速启动。




  • 维护组件状态



    • 维护组件在运行过程的状态,这样组件就可以在其所运行在的进程被回收的情况下仍然继续生存。




  • 进程管理




    • 在适当的时候主动回收空进程和后台进程,以及通知进程自己进行内存回收




    • 组件的UID和Process Name唯一决定了其所要运行在的进程。




    • 每次组件onStop时,都会将自己的状态传递给AMS维护。




    • AMS在以下四种情况下会调用trimApplications来主动回收进程:



      • A.activityStopped,停止Activity

      • B.setProcessLimit,设置进程数量限制

      • C.unregisterReceiver,注销Broadcast Receiver

      • D.finishReceiver,结束Broadcast Receiver






Binder




  • 为组件间通信提供支持



    • 进程间;进程内都可以




  • 高效的IPC机制



    • 进程间的组件通信时,通信数据只需一次拷贝

    • 进程内的组件通信时,跳过IPC进行直接的通信




说一说DecorView


DecorView是什么




  • DecorView是FrameLayout的子类,它是Android视图树的根节点视图



    • DecorView作为顶级View,一般情况下内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下三个部分,上面是个ViewStub,延迟加载的视图(设置ActionBar,根据Theme设置),中间的是标题栏(根据Theme设置,有的布局没有),下面的是内容栏。
    <LinearLayout >
    <ViewStub
    android:id="@+id/action_mode_bar_stub"/>
    <FrameLayout>
    <TextView
    android:id="@android:id/title"/>
    </FrameLayout>

    <FrameLayout
    android:id="@android:id/content"/>
    </LinearLayout>



  • 上面的id为content的FrameLayout中,在代码中可以通过content来得到对应加载的布局

    ViewGroup content = (ViewGroup)findViewById(android.R.id.content);
    ViewGroup rootView = (ViewGroup) content.getChildAt(0);



Activity 与 PhoneWindow 与 DecorView 关系


12401.jpg


一个 Activity 对应一个 PhoneWindow,一个 PhoneWindow 持有一个 DecorView 实例,DecorView 本身是一个 FrameLayout。


如何创建DecorView




  • 从Activity中的setContentView()开始



    • 在Activity中的attach()方法中,生成了PhoneWindow实例。既已有Window对象,那么就可以设置DecorView给Window对象了。

    • 从中获取mContentParent。获得到后,通过installDecor方法生成DecorView,源码中操作比较复杂,大概先从主题中获取样式,根据样式加载对应的布局到DecorView中,为mContentParent添加View,即Activity中的布局。

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

三本学渣大厂之路

故事的开始 故事从 16 年开始,那年高三。和许多高三的学生一样,每天家和学校两点一线。本来应该老老实实备战高考,争取考个好大学,走向美好的未来。但一次意外的受伤,生活的轨迹开始发生了偏移。 由于备考压力大,学校也管的严,平时的消遣只剩了跑跑步活动下筋骨。但在...
继续阅读 »

故事的开始


故事从 16 年开始,那年高三。和许多高三的学生一样,每天家和学校两点一线。本来应该老老实实备战高考,争取考个好大学,走向美好的未来。但一次意外的受伤,生活的轨迹开始发生了偏移。


由于备考压力大,学校也管的严,平时的消遣只剩了跑跑步活动下筋骨。但在一次跑步的时候拉伤了肌腱,当时便疼痛难忍请假去了医院。医生说需要静养一周再去上学。本来也没啥,但是学校每周有周考,父亲认为这点小伤有啥好养的,耽误了学习怎么办?于是矛盾产生了,本来压力就大,有些厌学情绪在。再被要求一定要去周考,自然就不乐意了。


家乡是个四川的十八线城市,父亲是个正常的厂里员工,在他看来没有什么比好好学习考上一个好的大学更重要。只要没到性命攸关的时候,没啥理由可以放弃。十多岁正是逆反心理严重的时候,让怎么样偏不想怎么样。


于是在跟父亲大吵一架后,离家出走了。出走这段时间,也没走多远。因为没钱,白天找个书店蹭书看,晚上找个黑网吧没人的角落对付一下也就过了。那几天想了很多,关于人生或是其它的什么,但唯独没有想过回去读书。想着看能不能打个工,买票离开家乡。但由于当时没满十八,没有老板收。就这样过了几天,在一天晚上打算在网吧睡觉时,被警察通过监控找到了。于是被送回了家里,回家后自然还是较着劲,不管怎么说就是不回学校。


半年后直接参加了 16 年的高考,成绩自然不理想。考了 400 + 分,也就过了三本线,离一本线差一百多。选来选去在一堆三本中选了 成都东软学院 这个民办大学,当年说是取消三本于是学校就变成了二本。选东软的原因很简单,这是当时的分数能上的有计算机专业的学校。而选择计算机的原因也很简单,这是相对来说比较感兴趣的专业。


这个结果父亲自然是不满意的,反复让回去复读。但始终没能说动,于是他说不复读可以,反正就养大学几年,到时候该干嘛干嘛去,别回去找他。如果能妥协当时也就不会离家出走了,于是欣然答应,心中想着就算没能上一个好的大学,也不代表这辈子就完了。


开学后发现事情没有那么简单,总的来说除了环境不错,东软没啥值得说道的。学习的氛围不能说没有,只能说很少。于是矮个子里选高个去了学校里为数不多的实验室,到了实验室才开始了正式接触编程的生活。


浑浑噩噩


刚进实验室就发现,在这个不怎么样的学校,有不少竟然在大学前接触过编程了。没办法只好从头开始学起。实验室主攻的是算法,说来也好笑,到现在我的算法水平都是吃的那个时候的老本。


每天有空就去实验室,上课的时候就刷题(ACM),经常刷题刷到深夜。刚开学半个学期就刷了 300+ 算法题,虽然日子充实却逐渐开始迷茫了。每天除了刷题不知道出路在哪里,也看不到未来。


迷茫的情况在第一次参加算法比赛时就更严重了,本来在学校里凭着多刷的题,在新生里面水平还不错。但和电子科大之类的 985、211比起来啥也不是。到现在还记得,那次别人队里就一个人,时间没到就A完了。而我到比赛时间结束也就 A 了 5 道题。



A题:提交代码通过这道算法题所有数据即 Accept



比赛后,虽然拿了个省级三等奖,但我开始认识到算法这条路不太适合继续走下去。有很多大佬在高中的时候就参加了信息竞赛,虐电子科大都跟玩儿似的,更不用说我一个三本学渣了。


没多久就退出了ACM实验室。退出过后天天琢磨怎么样才能毕业后顺利找到工作,有人说多参加比赛拿奖,评奖评优对找工作有帮助(这个时候根本没有想着能进大厂),于是我又去参加各种比赛。


忙活一年,零零散散混了几个国奖,也拿到了奖学金。但我始终觉得这条路不太合适,因为学校已经落后一截了,在好的学校别人也能评奖评优,含金量不知道比三本的年级前几高了多少,到了毕业同台竞争肯定没戏。


虽然意识到了问题,却没有明确的方向,只能先做好手头的事情。


日子一天天浑浑噩噩地过去,有很多次想过要不就这样算了,跟室友们在召唤师峡谷交流交流感情不香嘛?每天忙东忙西,却仍旧一无所获。


直到那件事的发生,状况才开始了改变~


找到方向



虽然大学的生活一言难尽,但幸运的是遇见了相守一生的人


高考最美好的地方不是得偿所愿,而是阴差阳错



17年12月 女朋友过生,前面也说过,父亲十分反对我上三本,所以生活费也可想而知。为了准备礼物,绞尽脑汁,终于在一天刷 B 站的时候有了个主意。当时刷到一个用代码实现一个 3D 爱心的视频,于是跟着视频自己做了一个,在生日那天送给了女友。


虽然到现在那个爱心不知道丢到了哪个角落,但是那颗爱心让我发现了不同于算法的另一条道路。难以否认的是,除了搞算法起点太低之外,放弃算法的另一个重要原因就是打代码天天对着命令行(C语言),实在提不起兴趣。


前端的所见即所得深深吸引了我,于是决定走上前端的路子。


有了方向,剩的就是一路前行。


当时拿出了之前拿的所有奖学金,给自己报了个前端培训班。在三本这样的学校老师教的与行业脱节严重,只好寻求外援。


于是生活又回到了之前从早到晚,脚不沾地的样子。白天上课,晚上上培训班。虽然日子很忙,但心里有了方向也就能咬牙坚持了。


顺带说一句,虽然那个时候上了培训班,但是没有放下正常的学业。我的方向是单片机,虽然学校前沿的技术不太行,但计算机专业应有的计算机基础还是在教的。


有了算法和计算机基础,才使我跟上培训班的有了区分。也是后面能走的更远的基础。


屡败屡战


忙碌的日子持续到了18年,期间每年只有过年回家,暑假就呆在学校里自学。


过了一年苦修的日子,虽然知道还有很多知识没掌握,但毕竟还是少年,想掂量掂量自己的斤两。于是开始投起了简历,意料之中的是,所有简历都没有回音,毕竟大学实在有点拉,好多成都人,都不知道成都附近有成都东软这么个学校。(直到22年疫情封控,东软给成都做的健康码挂了)


当时想了想,反正也没人要,不如去看看大厂究竟要什么样的人,认清差距也好。于是把当时的BAT投了个遍(B那个时候还是百度)


讽刺的是之前算法的那些奖,小公司的简历筛选没过,反而过了阿里和腾讯的筛选(后来去了腾讯才知道,算法竞赛的简历会被标注)没多久就收到了笔试的邀请,凭借之前的算法和基础积累,竟然都通过了笔试,进入了第一轮面试。


还没来得及高兴,两盆凉水就泼到了脸上。不出意外,阿里和腾讯的一面挂了。在面试时能明显的感觉到面试官已经没啥能问的了(啥都不会还问啥)


面试挂了过后,消沉了一小段时间。但生活还得继续,痛定思痛之下开始反思失败的原因。主要在两点:



  1. 缺少项目经验

  2. 对知识的深度理解有限


于是针对这两点,我开始有意识地补充这方面地能力。缺少经验就去给老师打黑工,就为了能多一些实战的项目经验。知识深度不够就写博客,坚持日更,写每天学的内容(感兴趣的朋友可以去我的主页瞅瞅之前的博客园)


此外继续面试积累经验(又被腾讯挂了一次)


就这样又一年过去了。


柳暗花明


时间来到了19年,我继续投几家大厂的春招。相比之前已经有了肉眼可见的进步,最多的一次面到了美团的四面,但还是没能上岸(后面才知道跟美团八字不合)


没办法,继续沉淀吧。


终于,经过五轮面试拿到了腾讯 CSIG(腾讯云) 19年暑期实习的 offer。


现在已经记不清当时到底有多开心了,但想来应该是挺开心的。


拿到 offer 过后,有一个小小的问题。那个时候我其实是不能去实习的,因为才大三。没办法只好去找学校沟通。期间经过一堆的纠缠,终于学校还是愿意开个口子,让我去深圳实习。


19年7月坐上了成都飞往深圳的飞机,这也是我第二次坐飞机。也是在这个时候跟家里的关系才略微缓和了一些。但关系还是不好,因为我逐渐意识到,人的成长伴随着离开,只有离开家庭,依靠自己的力量在社会立足,才能真正长大(如果是富二代,那当我没说)


苦逼实习


去到深圳后,第一件事情就是找住的地方。本来在成都上学的时候我觉得像成都这样的物价房价已经很高了,在深圳才知道什么是小巫见大巫。



好笑的是刚开始跟女朋友在一起时,最纠结一点的就是她是成都人,我怎么在成都买得起房子,娶得起她



为了省钱,只好租在离公司很远的地方(20公里) 好在腾讯有班车,交通倒是省下一笔。即使这样也过得穷困潦倒(压三付一是真的离谱)。


生活上的困苦倒还好,工作上也不太顺心,主要有三个原因:



  1. 学校太差,感觉和同事格格不入(就没有一个不是重本的)

  2. 虽然做过项目,但没有工作经验,很多事情都要学

  3. 上班太卷(连着上了20天班,早9晚10)


虽然困难比较多,但还是十分珍惜这个机会,想着多提升自己也就没太在意。


很快就到了 9 月份,决定实习留用的时间。如果能留用,相当于就拿到了来年的校招offer,20年毕业就不用再找了。


也是到这个时候才知道,部门并没有 HC 就是找廉价实习劳动力的。


没办法,求人不如求己,于是乎又开始了面试。


正式上岸


虽然实习的时候被压榨的比较狠,但还是学到了东西。


凭着实习的经验和之前的积累,拿到了两个 offer



  • 京东凹凸实验室的 SP Offer

  • 腾讯 IEG 的白菜 Offer


由于腾讯钱给的比较多,还是去了腾讯。


拿到 Offer 后为了避免 Offer 被撕,10月份就去新部门报道了。去了新部门才知道,不是所有的班都是朝9晚10。


上班期间抽空做了个毕设,回去答辩的时候正好遇上疫情爆发。本来打算来个毕业旅游啥的,也只能云毕业了。


答完辩在家里呆了两个月调整了下,20年9月正式回到深圳,成为当当当(深圳修路的声音)的受害者之一


职场沉浮


回到公司一来就被派去接手一个边缘业务,那个业务本来是其它团队的,但由于那个团队去做新业务了,老的业务就丢给了我们团队。


其它同事早就有了自己负责的业务,没得选我只好去捡起这个烂摊子。


这个业务之前的负责同学都转走了,因此剩下交接的人员是之前负责团队的外包同学,于是上手变得十分困难。



  1. 腾讯的光荣传统口口相传,没有文档

  2. 外包同学没有一个全面的系统的了解

  3. 在上手之余还得管理外包


对于一个刚毕业的菜鸡,一下子就被砸得晕头转向,每天忙到很晚却没啥用。一直帮外包擦屁股,有些时候觉得还不如自己一个人写。


但是由于我之前的经历,我其实对自己的技术能力没有什么信心。于是在带外包的时候CR也没怎么做,让外包同学觉得我不管他们,有了意见。此外,这个业务原本的团队除了这几个外包同学还有三个正职,这才维持了业务的正常运转。


但现在只有我一个菜鸡正职,当然不怎么玩得转。于是当时的业务负责人找我谈话,希望我在做需求之余,还得抓外包的管理。我以自己的能力还不足为理由拒绝了。



当时的我没意识到这是个机会,由于之前的经历让我有点技术至上论,一心只想提升技术,觉得管理没用,换家公司就不行了,现在想来小丑就是我自己



于是在腾讯的两年就搞搞技术,混个辛苦绩效(三星堆),后面招了个高T来接手我原来负责的部分和外包的管理,次次四五星。



腾讯绩效1-5星,3星合格



因为腾讯是百分比绩效,所以我以一个相当离谱的理由背了低星。说我产出低,因为我 Git 提交的代码行数最少,没想到段子成为了现实。


不过幸好当时早有预感(有意无意的不给我活干),提前找好了下家(字节),才没有被搞得措手不及。


几经辗转,又回到了成都。


在字节卷了一阵后,逐渐开始觉得这样的生活没啥意思。付出自己所有的时间、健康就为了卷某次的绩效?


在最近减肥的过程中逐渐找到了答案,其实工作学习和减肥一样,重要的是习惯,而不是靠毅力。之前靠毅力一天只吃两顿、天天骑动感单车瘦了十斤,但由于阳了,两礼拜不能运动,直接反弹打回原形。


后来渐渐佛系了,不要求每天要运动多少,节多少食,只是养成一个有空运动运动的习惯,不知不觉间体重慢慢降到了之前心心念念的目标,也没有反弹。


想来工作学习也是一样,不必刻意追求结果。现在每天准点下班,到点就走,多去思考而不是多做,反而取得了不错的绩效。


写在最后


从离家出走的叛逆到决定证明自己,从浑浑噩噩到找到方向,从屡败屡战到佛系和解,其实人就是在经历的一件件事情中,找到自己真正想追寻的。


如果想要达到一个目标,好的习惯比坚韧毅力重要,好的方向比闷头努力重要,好的方法比恶意竞争重要。


种一棵树最好的时间就是现在,祝好~


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

一天约了4个面试,复盘一下面试题和薪资福利

前言 昨天我的交流群里,有位宝藏群友分享了自己的面试经历:一天约了4个线上面试,收获满满。 为大家分享了面试题和每个公司给出的薪酬范围。 简单说下这位群友的情况:3年开发经验,最近2年做Go语言开发,还有1年Java/PHP工作经验。手撕CSAPP选手,每天打...
继续阅读 »

前言


昨天我的交流群里,有位宝藏群友分享了自己的面试经历:一天约了4个线上面试,收获满满。


为大家分享了面试题和每个公司给出的薪酬范围。


简单说下这位群友的情况:3年开发经验,最近2年做Go语言开发,还有1年Java/PHP工作经验。手撕CSAPP选手,每天打卡分享手写的学习笔记。


注意:是每天手写笔记学习!每天+手写!


image.png


也有群友反馈,有人海投200多份简历没人回复,boss直聘上都是已读不回。对比宝藏群友一天约4个面试可以说是云泥之别。


很重要的一个原因,就是简历不行。或者你海投的公司都不靠谱,如果你海投200家都是已读不回,大概率不是这200家公司的问题......


我的经验分享


这是我之前找工作和简历优化的经验总结,希望对大家有帮助:


【求职复盘】我是怎么做到面试一次就拿到offer的?


【简历优化】怎么引导面试官提问自己准备好的问题?


【简历优化】如何在简历中最大化体现出自己的学习能力?


也欢迎大家关注我在掘金的 # 简历优化专栏


群友面经分享


再次感谢宝藏群友的分享,给了大家刷题的方向、市场的薪酬行情、更重要的是给了大家信心


在求职市场哀鸿遍野的情况下,还能做到一天能约到4个面试,不说别人,起码给了我很大的信心,对市场还是看好的。(如果这篇文章能给10个人带来信心,我就心满意足了)


(相同的事情,不同的人看会有不同的反馈:比如对于这件事情我是看到了市场的信心,还是有不少公司在招聘的,并且待遇不差,要求确实不低,没有像网传中说只读不会连面试机会都没有那么悲观;有的群友看到面试题的反馈是太卷,会的不多;有的群友看到的反馈是找开发岗位,Docker CNI 的实现都要考吗.....)


20K-30K 深圳


Docker 底层、多阶段构建、原子指令你怎么理解、CSP和Actor分布式模型的区别、内存对齐、Channel 和select的基本用法、赋值你认为有多少条汇编指令、比较出名的开源项目pr、Redis持久化、GMP模型、一致性算法


13-20K * 13 深圳


3个算法题,暴力1道,2道有思路,一个贪心、一个动态规划、一个冒泡。聊异步、业务、持久化


13-20K * 13 厦门


TCP 粘包怎么解决、同步控制 waitgroup 、数据库索引优化、TCP 如何实现可靠性、队列,树,栈的应用场景和区别、TCP 在 linux 中一些参数的含义、一些十进制转换二进制、十六进制、如何定位死锁,链表简单题


16-20K 上海


战争迷雾怎么实现、共识算法、Channel、Make和New的区别、GMP、数组类型算法、UDP实现可靠协议、分布式模型、Panic没被Recover怎么处理、切片扩容、Docker CNI 的实现、数据落盘怎么做的、Lua


总结


看到这里大家心里应该有个数了,可以看看自己的期望薪资,再看看目前市场上考察的这些知识点,查漏补缺。


也欢迎大家私信我,或者关注我的公众号 程序员升职加薪之旅,后面会持续更新面试题、面试复盘相关的文章,希望对大家有帮助,更欢迎大家的投稿分享。


需要做简历指导的也可以关注公众号,加我微信。


大厂面经


受高启强的影响,我也在读《孙子兵法》,分享这段话给大家:求其上,得其中;求其中,得其下,求其下,必败。


映射一下目前互联网的就业市场,道理简单明了:如果你想进中厂,就要做进大厂的准备。如果你想找到月薪1W+的工作,就需要做月薪1W5+的准备。如果你的目标就是找到工作,起码要做冲洗中小厂的准备。如果你的目标就是找个小公司混日子,大概率找不到工作。


为了更好的帮助到大家,我还整理了网络上很有价值的大厂面经:字节、腾讯、滴滴、腾讯云、小米、小米游戏。


希望对大家有帮助,建议收藏,并且转发给好朋友。



下面先分享一下我 学习小圈子 里字节嘉宾关于求职面试的答疑,大厂更看重的是什么? 给大家指指方向,少走弯路。



有问必答


提问


大佬好,最近我要去面试试水了,想问一下 有没有关于java或者go遇到的生产案例分享 最好是关于jdk或者第三方包的bug,容易加分。感谢!


回答


你是面校招还是社招?一般面试官会根据你的简历中项目经历、实现细节来展开逐层递进,你说的生产案例最好还是自己实战经历过的,不然很容易就发现不是你的项目或者会被打上项目参与不深入的标签。


每一次面试都尽量准备充分,不要抱着水水的心态,大公司面试都会留痕和面评的,如果你是想丰富下面试经验,建议你先找一些小公司或者不太想去的公司面一面找找感觉,自己心仪的公司和岗位一定要准备充分再去发起面试流程!


群友


我是属于社招,一般面试官会问处理过的最亮的技术点,目前是游戏平台后端开发 但实话实说 所用技术和闪光点太普通。


不是项目造假的意思,就是准备几个生产上处理过的几个难度较高的技术问题


嘉宾


建议你可以仔细盘一盘负责项目的文档、代码等资源,即使很多东西不是从0到1自己做的,也可以借鉴和领悟下其中的技术实现细节;平时也可以多写写技术文章,输出些自己工作内容中有技术特色的地方。


群友


我负责的项目就是我从0到1弄好的,包括文档和代码,里面确实没有拿高薪的技术亮点。


嘉宾


技术亮点是客观的项目经历,除非面试官也做过类似东西,能和你产生互动否则是不太感兴趣和深入聊的,技术栈是共同语言也便于考察个人技术能力,面试的时候也要学会主导话题,扬长避短多聊自己的优势点。说到“拿高薪”,这里说一句大白话:有多大本事拿多少钱。能力和薪资是正相关匹配的,一家公司招聘人才的能力模型会参考专业知识、工作阅历、个人性格等多方面,而面试的招聘过程双向是有信息差的,最终影响你薪酬水平的是面试结果(带有信息差的能力评价)+ 你当前的薪资水平、职级(自身当前的社会客观能力反馈)+ HR可操作的涨幅空间。


群友


理解,谢谢大佬的诚恳的解答,我还是从技术栈下功夫,这样和面试官的共鸣会高一些,也不再执迷于某个技术亮点。


嘉宾


不客气,加油!技术栈扎实绝对没问题💪


重点干货已经加粗标记了,上面这个问答建议再看一遍,很经典的问题。



以下面经来自网络,感谢大佬们的分享,非本人,我只是做了面经的搬运工,希望对大家有帮助。



字节面经


一面


自我介绍+算法题:



  1. leetcode-cn.com/problems/fi…

  2. leetcode-cn.com/problems/be…

  3. leetcode-cn.com/problems/be…


问答



  1. 索引,倒排索引,切词,如何根据 doc id 有没有出现某个 token

  2. 服务高可用是怎么做的

  3. MySQL 可重复读、读提交区别、原理

  4. 爬虫 URL 去重,设计存储结构(FST,前缀树+后缀树) MySQL (a,b,c) 索引,几条 SQL 走索引的情况

  5. 思考题:概率 p 生成 0,1-p 生成 1,如何 1/2 概率生成 1


二面


算法题:



  1. leetcode-cn.com/problems/be…

  2. leetcode-cn.com/problems/be…

  3. leetcode-cn.com/problems/co…


技术问题



  1. 讲一下 es 索引的过程

  2. 切词怎么切,切词算法,降噪

  3. 让你带应届生,怎么带,

  4. 有什么工程经验可以分享

  5. Redis 缓存淘汰有哪些


三面


自我介绍


算法题:



  1. leetcode-cn.com/problems/fi…


技术面



  1. 文章下面的评论,按点赞数排序,SQL 怎么写

  2. 把所有评论放到内存里,怎么设计数据结构,存储并排序

  3. select * 会有什么问题

  4. 缓存热 key 怎么解决

  5. 职业发展

  6. 领导如何评价你

  7. 项目难点,亮点


滴滴面经


一面



  1. 介绍项目

  2. 问我为什么选择GO,看我有Java从业经历。

  3. 介绍一下java 和 go 区别,我猜是让我说一些他们的不同点,go 比java 哪里好。我说了一些 特性
    3.1 问我协程比进程好在哪里? 我自己顺便说了进程线程 协程三者关系 4. 问我想从事什么

  4. 项目中有bloom介绍了一下怎么使用的,精度,损失

  5. GPM模型

  6. redis使用模式 主从 哨兵 巴拉巴拉

  7. 接着聊项目,然后问了算法

  8. 渐进式的聊面试,很轻松

  9. 问我能不能接受看php? 反问时候,聊了一下GORM,应用情况。他们的go-spring,还有他们的夜莺系统。因为看过一点点所以想问问。有培养体系,教我如何写GO(这个我很欣慰),说有大佬内部课程。


二面



  1. 自我介绍(面试官也不看我,一脸严肃我特害怕。然后自我介绍磕磕绊绊的)

  2. 问我看源码吗?

  3. 问了问GC 发展史,都怎么玩的 每次优化了啥

  4. 问了问我go 内存 优化了那些东⻄(这题我忘了咋问的了)

  5. 问了一下我项目里nodejs 升级为 java 为啥会快了那么多。 6.问了红黑树特性,哪个数据结构用到了。我介绍了一下 红黑树 一些特性 比如 平均查找时间 低 插入删除需要 左旋右旋调平衡。 我想到 java里 hashmap 用到了这个结构 7.问了一下map的底层结构 顺便介绍了一下 sync map

  6. 找出两个大文件交集

  7. 算法 leetcode 两棵树 b 是 a子集那道题思路 怎么做 dfs 然后比较 值和 指针

  8. 聊了一下 我的项目 召回相关的 和 nodejs java 迁移 效率提升问题

  9. 聊了一下 go-spring 夜莺 还有 didi 有个 写sql的github 项目 想问一下应用情况。问我能不能 接受 看看php 之类的

  10. 问了问我为啥离职


腾讯面经


一面



  • 算法题二选一


  • MySQL 隔离级别

  • MySQL 锁

  • MySQL 存储结构(b+树)

  • 索引 回表 是什么

  • 消息队列,rabbitmq

  • rabbitmq 如何保证可靠性(生产者可靠性、消费者可靠性、存储可靠性) - rabbitmq 几种模式

  • es 索引的过程

  • 线上是如何分表分库的,用什么做分表分库的策略,跨表查询

  • MySQL 如何同步到 es

  • 线上 Redis 用的是什么模式

  • 缓存热 key 怎么办


二面



  • 介绍项目

  • defer 、go 继承,手写快排

  • 登录流程,JWT、session、cookie


三面



四面(面委)



  • 项目为主

  • tcp quick_ack 、 nodelay ,socket 编程

  • 职业规划

  • 为什么换工作


五面(GM)



  • 项目

  • go 协程机制


腾讯云


这个面经来源于网络,这位朋友主要技术方向是k8s、容器、云计算。


有服务上云的实践经历,了解cicd基本流程,求知意向是容 器研发、基础架构研发、运维研发之类的(主要还是研发方向)。


项目方向:


项目的话我不多说什么,就是自己的项目细节自己肯定清楚,如果项目中不是自己做的
部分,建议不要在简历上写太多,写清楚自己做了什么,容易被抠细节问,项目一般都会抠细节,特别细的那种!!!


语言栈:


因为主要语言栈是go,所以一般都比较少问python。


golang


1、gin框架路由怎么实现的,具体正则怎么匹配?限流中间件怎么实现? 2、go的slice 与数组的区别,slice的实现原理,源码? 3、golang的协程调度,gpm模型。协程调度 过程中的锁。 4、golang的channel实现,channel有缓存和无缓存,一般会直接撸码 (三个goroutine顺序打印)。 5、golang的关键字defer、recover、pannic之类的实现 原理。 6、sync包里面的锁、原子操作、waitgroup之类的。 7、make和new的区别, 引用类型和非引用类型,值传递之类的。


python


1、python多线程、多进程。 2、python的装饰器怎么实现的?


操作系统


1、进程、线程、协程间的区别以及他们间的切换之类的,有时候会问到语言级别的协 程。 2、io复用、用户态/内核态转换 3、awk命令 4、linux查看端口占用 5、top命 令,free命令中的各个参数表示什么,buff/cache都表示什么?


k8s & 容器:


1、简单聊一下什么是云原生、什么是k8s、容器,容器与虚机相比优势。 2、k8s组 件,pod创建的过程,operator是什么? 3、docker是怎么实现的,底层基石 namespace和cgroup。 4、k8s的workload类型,使用场景,statefulset你们是怎么用 的? 5、limit和request,探针,一般怎么排查pod问题,查看上次失败的pod日志。 6、sidecar是什么,怎么实现的? 7、pv,pvc,动态pv怎么实现 8、k8s的声明式api 怎么实现的,informar源码。 9、cicd,发布模式。 10、svc的负载均衡、服务发现, ipvs与iptables。 以上基本是会被问的点(虽然有一些问题我也不是很熟),另外很多 会被问k8s的网络之类的,因为我比较菜,这块被问的比较少。


计算机网络:


1、tcp三次握手四次挥手,为什么不能是两次握手,三次挥手?握手和挥手过程中的状 态。 2、time_wait作用,为什么是2msl,close_wait作用,time_wait过多怎么办? 3、http请求的过程,浏览器输入网址请求过程?dns解析的详细过程? 4、https与http 的区别,https第一次服务端回传是否加密? 5、tcp与udp区别,tcp怎么保证可靠性。 6、http请求头、分隔符、⻓连接怎么实现


数据库:


1、mysql的事务,事务使用场景。 2、mysql的索引,什么情况下索引失效,聚簇索引 与非聚簇索引,索引的存储b+树与b-树区别。 3、join的内外连接,最左匹配原则。 4、redis的数据结构,hmap怎么实现的,持久化怎么做,go操作redis的方式。 数据库 方向有被问到,我基本没答上来(一般都告诉他只会基础,开发直接使用gorm)。


数据结构与算法:


1、倒排索引和B+树 2、判断链表是否有环,时间复杂度要求0(1) 3、LeetCode上合并 区间的题 4、leetcode的股票买卖的题 5、二叉树的最近公共祖先 6、有序数组合并 7、什么是平衡二叉树、最小堆 8、大文件的top10问题 9、golang实现栈、队列


其他:


1、git 的相关操作,合并commit,合并之类的。 2、场景设计(比较多)


小米面经


一面



  1. innodb MVCC实现

  2. b+树是怎么组织数据的,数据的顺序一定是从左到右递增的么

  3. ⻚分裂伪代码,b+树的倒数底层层可以⻚分裂么

  4. 合并k个有序链表

  5. redis的hashtable是怎么扩容的

  6. select poll epoll,epoll具体是怎么实现的

  7. GMP是怎么调度,channel是怎么收发消息的,channel的recq和g是怎么建立关系

  8. innodb二次写是什么

  9. undo里面具体存的是什么

  10. b+树节点具体存的是什么

  11. mysql一⻚最大能存多少数据

  12. myisam和innodb索引上的区别

  13. innodb commit之前,redo 的prepare然后binlog commit,然后redo再commit有
    什么缺点?5.6之后是怎么优化的? 14. redo和binlog的区别

  14. 读锁和写锁区别


二面



  1. 蛇形打印二叉树

  2. myisam为什么不支持事务,如果要支持事务要怎么做

  3. 函数只能返回1-7的随机数,请用这个函数返回1-5,要求平均 4. 聊项目


三面



  1. go的协程调度和os的线程调度有什么区别

  2. 只有写锁实现读写锁

  3. go的调度是怎么实现的

  4. go的网络IO为什么快?还有优化空间么

  5. epoll为什么这么快,还有优化空间么?如果要你实现一个网络IO应该怎么实现

  6. 设计一个每秒80万qps的过滤器

  7. 过滤器用redis实现,宕机期间数据怎么恢复

  8. 设计一个下单 扣减库存的分布式应用,请求超时了怎么办,一直重试超时了怎么办

  9. 数组A1 2和数组B2 3是一个关系圈,A能通过2找到3,数组A1 2和数组B2 3和数组
    C 3 5也是一个关系圈,给一个二维数组求关系数


小米游戏面经


一、 介绍连接池项目



  1. 介绍连接池常用的参数,最大连接数,最小存活数这些意义,为什么要有这些

  2. 当链接超过最大连接数怎么处理,等待有空闲连接还是创建一个继续给出,比较两
    者的优劣

  3. 连接池清理链接的逻辑,如何优化的

  4. 当连接池中有一些链接不可用了怎么办,如何保证这些连接的可用

  5. 当出现下游某个实例挂掉了,连接池应该怎么处理

  6. 对比 mysql redis http 连接池的实现


二、 介绍负载均衡算法



  1. 介绍平滑负载均衡算法,实现

  2. 当出现下游出现不可用,负载均衡算法怎么处理


三、 介绍聊天室项目



  1. 介绍实现原理的,互相通信的逻辑

  2. 聊天室服务端如何把消息下发给用户

  3. 介绍websocket包的字段

  4. 当有用户掉线怎么处理


四、 redis相关



  1. redis的数据结构

  2. 各个数据结构的操作

  3. 各个数据结构的使用场景

  4. 如何保证 Redis 的高可用

  5. 当有一个key读取的频率非常高怎么办


五、 算法相关



  1. 介绍快速排序 优先队列的实现


总结+鸡汤


就业环境再好,也有人找不到工作。


就业环境再差,也有人能找到工作。


要么学历🐂🍺,要么技术🐂🍺,要么都🐂🍺。


如果学历无法改变,请让技术🐂🍺,其他的都是扯淡~


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

我有一刀,可斩全栈

引言 夜谈性的文章,思考篇幅会比较啰嗦,篇幅基本会以概念、发展、思考、未来这几个内容主题进行,最近结合软环境地狱,再到看到社区的很多未来思考,做一些总结和预测,去年的一些总结,今年基本应验了一部分,希望能起到警示和思考吧。 概念 什么是全栈 全栈(Full-...
继续阅读 »

引言


夜谈性的文章,思考篇幅会比较啰嗦,篇幅基本会以概念、发展、思考、未来这几个内容主题进行,最近结合软环境地狱,再到看到社区的很多未来思考,做一些总结和预测,去年的一些总结,今年基本应验了一部分,希望能起到警示和思考吧。


概念


什么是全栈



全栈(Full-Stack)是指一种解决问题域全局性技术的能力模型。


很多现代项目开发,需要掌握多种技术,以减少沟通成本、解决人手不够资源紧张、问题闭环的问题。全栈对业务的价值很大,如对于整个业务的统筹、技术方案的判断选型、问题的定位解决等,全栈技术能力有重要影响。另外对于各种人才配套不是很齐全的创业公司,全栈能解决各种问题,独挡多面,节省成本,能在早期促进业务快速发展。


技术有两个发展方向,一种是纵向一种是横向的,横向的是瑞士军刀,纵向的是削铁如泥的干将莫邪。这两个方向都没有对与错,发展到一定程度都会相互融合,就好比中国佛家禅修的南顿北渐,其实到了最后,渐悟与顿悟是一样的,顿由渐中来。可以说全栈什么都会,但又什么都不会。



全栈定义


狭义


全栈 = 前端 / 终端 + 后端


广义(问题全域)


全栈 = 呈现端(硬件 + 操作系统(linux/windows/android/ios/..) + 浏览器/宿主环境+端差异【机型、定制】) +H5+小程序(多端统一框架)+ 前端开发/终端开发 + 网络 + 后端开发(架构/算法) + 数据(SQL/NoSQL/半结构/时序/图特性) + 测试 + 运维


+软实力=文档能力+UI能力+业务能力+设计能力+技术视角(前瞻性)选型+不同语言掌握能力+项目管理能力+架构设计能力+客户沟通能力+技术撕逼能力+运营能力


价值


全局性思维


一个交付项目的全周期,除了传统的软件过程,需求调研、规划、商务、合同签订、立项、软件过程、交付、实施运维等,麻雀虽小,五脏俱全,如果对并发、相应、扩展性、并行开发等有硬性要求,软件过程会变得异常复杂,因此后来又拆前端架构、后端架构定向的解决某个领域内的技术规划岗位,因为人力反倒是小问题,要的是快和结果稳定,项目可以迅速肢解投入,每个岗位注重领域和边界问题,以做沟通的核心基础,对于一个团队特别是互联网企业来说,有一个全局性思维的人非常非常重要,这个角色常常会被赋予(产品/项目)或其他Tile,什么事业线、军团之类的,本质上也是对人员的细节化和边界的扩充。
回到本质问题,当人成为问题的时候,以3个人为例,一般开发层的东西,3个合理偏重的 【狭义全栈】,做事的效率和执行沟通结果和3个1+2的分端是完全不同的,一个是以业务块沟通的,一个是以功能块沟通的,一个是对业务块结果负责,一个是对功能块结果负责。


其实刚入职那会儿,就有人和我说,服务是看不到的,端是直面的,这其中有个度的问题,不过度设计、不过度随意,保持需求和设计在合理区间内,有适度的前瞻性即可。
我之前接触的单端普遍会犯在业务不可能的场景下,纯粹讨论逻辑性的问题,导致的无休止的无意义讨论,最终的反思是 我想把这个东西做好, 举个不太恰当的例子叫 "有一种冷,叫妈妈觉得你冷",我把这种归结起来就是不对结果负责,只对自己负责,这也多半是因为岗位边界的问题导致的。


沟通成本


项目越大,沟通成本越高,做过项目管理的都知道,项目中的人力是1+1<2的,人越多效率越低。因为沟通是需要成本的,不同技术的人各说各话,前端和后端是一定会掐架的。每个人都会为自己的利益而战,毫不为己的人是不存在的。


而全栈工程师的沟通成本会主要集中在业务上,因为各种技术都懂,胸有成竹,自己就全做了。即使是在团队协作中,与不同技术人员的沟通也会容易得多,让一个后端和一个前端去沟通,那完全是鸡同鸭讲,更不用说设计师与后端了。但如果有一个人懂产品懂设计懂前端懂后端,那沟通的结果显然不一样,因为他们讲的,彼此都能听得懂,相信经历过(纯业务/纯管理/纯产品)蹂躏过的开发应该有体会。


性价比与结果控制


创业公司不可能像大公司一样,各方面的人才都有。所以需要一个多面手,各种活都能一肩挑,独挡多面的万金油。对于创业公司,不可能说DBA前端后端客户端各种人才全都备齐了,很多工作请人又不饱和,不请人又没法做,外包又不放心质量,所以全栈工程师是省钱的一妙招,大公司不用担心人力,小公司绕不过的就是人力,当人力被卡住,事情被挡住了,独当一面可不只是说说而已,此时的价值就会被凸显,技术解决问题的途径很多样。


这里说个题外话,性价比是对企业的,那对个人来说,意味着个人的能量和价值会放大,如果你细心观察开源的趋势,会发现整体性的项目趋势变多了,而且基本在微小的时候可能只是单人支撑的,这个趋势从百度技术领跑再到阿里转换时有过方向和风格的转换。


困境


说得不好听一点,全栈工程师就是什么都会,什么都不会,但有需求,结果、时间、风险都会被很好的评估,因为思路和理念是完全不同的,全栈天然的就必然会重视执行结果,单端只注重过程,事情做了,坏的结果跟我一点儿关系都没有,其中甘苦,经历了才知道,所以也注定面试是不占优势的,而且全栈根本没有啥标准的划分,也注定游离在小公司才能如鱼得水,当然,如果你的目标是星辰大海,工作自由,这个事就另当别论了。


发展


天下大事分久必合,合久必分,最开始的没有前端,到分出前端,没有安卓/IOS到分出岗位,再到手机端合到前端,pc到前端,”大前端“的概念,不管技术怎么进步或者变化,总归是要为行业趋势负责的,就好比你为300人的企业用户考虑高并发,完全不计较实施和人力成本,很多的事情都是先试水再铺开的,没那么技术死板。


感觉整个软件生态发展至今,提供便利的同时,也用框架把每个人往工具这个方向上在培养,这本就是符合企业利益的事,但减量环境下,螺丝钉的支撑意义被无限的减弱和消磨,很多的单端从业一段时间后,想做事儿,发现另外领域的空白,也开始往横向考虑,这本就是危机思考和方向驱动的结果,一个大周期的循环又开始了,特别是在java国内的一家独大,再到个体开始挣扎的时候,多态的语言开始反噬,反噬的驱动力也从服务器这个层级开始了挣扎,亦如当年的java跨平台先机一样。


前端的框架随着框架的便捷性和易用性越来越完善,其竞争力变得隐形了,回归了工程化问题的解决能力,去年也提过,变化中思考,稳定中死亡,到了思考自己的核心竞争力是什么的时候了,这何尝不是自由工作者的春天。


端扩散


软件的路程发展已经有了很长一段路,概念和业务层级的提升服务有限,自动化、半自动化、AI的概念渐渐的可以走向技术成熟,端的发展又有了去处,只不过这个过程很慎重,需要打通很多封闭的东西,再加上工业信息化的政策加持,单纯的信息录入或者业务系统已经掀不起多大风浪,而纯互联网的金融、物联网也被玩的渣都不剩,突围和再上一层的变革,短时间内,公司级的突破已经很难找到出路,从收缩阵地,裁剪人员可见一斑。


复杂度提升


如果说有确切的变化,那基本就是我机器上的编译器环境和用的工具越来越多样,解决问题的途径和手段越来越多,不再是原来的一个整合ide解决所有问题,这就好比,我原先手上只有木棍,武器用它、做房子用它、生火也用它,挖掘的它所有的应用途径,那有一天,我有了刀、有了席梦思的床、有了大别墅,却因为害怕放着不用。当然,我之前听别人说过一个理论:”只要能解决好结果,哪怕你徒手,我也无所谓“,他站在老板的角度上,至于你是累死也好,花10倍的工作量也好,都无所谓。作为个体来说,既然只要结果,那就别怪我偷工作量了,个体的掌握技能的多样性,背后可是有语言生态支持的,因此复杂度的提升,也带来了生态支持,并非一边倒的情况。


人心异化


我依然怀念头几年的环境,都是集中在解决问题,目标一致,各自解决各自的问题,拼到一起,就是整体结果,各自的同事关系轻松和谐,上线前的交付大家一起搞的1点多,下班宵夜美滋滋,现在端分离和职责明确,天然存在利益冲突,摸鱼划水,撕逼的情况,虽说可能是部分老鼠屎引起的,但谁说这不是热情消退的结果呢,生活归生活,工作归工作,但生活真的归了生活,工作真的只归了工作吗?


思考


全栈的title就跟我参与了xxx开源项目一样,貌似也成为提升竞争力,标签化的一种,架构师、小组长、技术经理、总监,这些title,在离职那一刻其实都毫无意义,有意义的也只是待遇和自身的能力,如果你怀着高title在另外一家公司风生水起的想法,那很多3个月离职的经历,再一家还是3个月,难道不是面试能力和自身的能力出现不对等了嘛,可能是所有的公司都坑,那有没有可能是我们韧性太低,选择不慎呢。


好像刚工作那会儿,经常会被问到职业规划,之后很少被问到,却不停的在想,我能干嘛,今后想干嘛,之后就是无休止的躁动和不停的学习,不停的接项目,不停的用新技术,10年多的坚持,平均12点,找的工作基本也都是相对轻松的,那我能干啥,好像貌似什么也做不了,想法创意不停的被对比否认,找到合适的却不停的为盈利性的项目让路,貌似什么都会,貌似什么都没做成,原本以为是觉得自己修炼不够,没法实现自己的项目,后来发现,其实自己的第二职业,只需要一条路,一往无前的坚持,最终会有结果,尽管这个结果可能不好,但事情实践了,回想起刚工作那会儿”先理顺环节,再开发,还是先出东西再说“的争论,这会儿我完全认同了 ”先结果,再谈未来“


因此,别管什么 ”前端已死“”java已死“,大环境不好,行业低迷,去行动吧,亲手埋葬也许,焕发新生也好,回到内心,做好与行业诀别的决心,背水一战。即便是为了生活被迫转行,也可毫不顾忌的说,努力过,没戏,直面内心,回想起18年看到的新闻,”程序猿直播7天0观众“,我想我能够做的也只能是武装与坚持,至于大环境怎样,行业怎样,到那一天再说吧,套用领导的话”别想那些有的没的,做好自己的事“,至少,我人为,当软件公司不易时,恰恰是个体的机会,当个体的力量开始有竞争力,那全栈的优势会有很好的发挥,这个场景在我有意识的5人实践和2人优势互补中已经得到了长效的验证。


未来


也许从当前的公司离职那天,就是我职业生涯结束那天,我已经做好了心里预期,但我希望可以作为一个自由工作者,这是我后半段反复思考的结果,至于结果怎样,我只能说,预期的努力我已经做了,时机和后续有待生活的刀斩我不屈之心。


PS


认清内心、从容面对,不要有什么鸵鸟心态,事实不逃避,行动不耽误,这是斩龙之刀,破除未知的迷雾,我所能提的也只是从心和认知,没啥发展途径和规划,因为技术的发展,总是未知和充满惊喜的,这也正是它的魅力所在。


最后


我深怕自己本非美玉,故而不敢加以刻苦琢磨,却又半信自己是块美玉,故又不肯庸庸碌碌,与瓦砾为伍。于是我渐渐地脱离凡尘,疏远世人,结果便是一任愤懑与羞恨日益助长内心那恬弱的自尊心。


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

程序员为了少加班想了这几招

天天开会,我还有时间写代码吗? 接口自测过没,怎么联调还有这么多问题? 需求怎么又变了,PD到底有没有个准信? 又是版本发布倒排,需求这么多能做的完吗? 以上各种场景大家在日常的工作中是不是经常碰到,今天慕枫就和大家聊一个非常有意思的话题,程序员为什么总是要加...
继续阅读 »

天天开会,我还有时间写代码吗?
接口自测过没,怎么联调还有这么多问题?
需求怎么又变了,PD到底有没有个准信?
又是版本发布倒排,需求这么多能做的完吗?
以上各种场景大家在日常的工作中是不是经常碰到,今天慕枫就和大家聊一个非常有意思的话题,程序员为什么总是要加班,到底谁偷走了程序员的时间,我们应该怎么做才能避免不必要的加班。


谁偷走了程序员的时间


要想少加班就要搞清楚是什么因素导致了程序员加班才能完成工作,也就是说我们得先弄明白程序员的时间到底被谁偷走了。我们先分析下程序员每天的工作主要包含了哪些内容,因为只有搞清楚程序员时间都用在哪里了,我们才能对症下药制定相应的优化计划,避免时间黑洞浪费精力资源在一些不重要的事情上,把时间和精力更多投入到重要的事情上以及需要完成的任务上。


有的同学可能会说,程序员嘛,工作不就是码代码嘛。如果真的只是这样,我想大家真的要烧高香了,实际上程序员的日常工作远远不至于写代码一个事情,甚至有的时候一天代码没写几行都在开会。


慕枫将程序员的工作主要划分到以下三大块内容中,接下来我们来具体分析下如何在这些繁杂的事项中把本该属于程序员编码、思考设计的时间给抢回来。


源源不断的会议


业务需求KO会议


在阿里,一般当业务方需求过来之后,PD或者产品经理会组织会议对技术同学进行产品需求进行KO或者澄清。在会议中PD或者产品经理需要讲清楚为什么要做这些需求、需求的内容是什么以及想要达到的业务效果,同时接受来自技术同学汹涌而来的挑战,一般这种会议打底要1到两个小时,有的时候讨论激烈的话可能都不止,甚至可能需要多次会议来回讨论才能确定需求。


技术评审会议


需求确认过之后,技术同学就需要进行对应的方案设计,我们想清楚如何做这个需求,同时输出相应的设计文档,设计文档中主要包含实现逻辑、修改点、时间计划、灰度策略等等,设计方案完成之后组内会先过下方案,把关下质量。同时负责这次整体需求的技术PM需要组织业务全链路节点的技术同学来对焦各自的技术方案,看看有没有什么遗漏之处,需不需要上下游业务节点的支撑,这个会议基本上1到两个小时。


测试评审会议


技术方案确认之后,测试同学就需要根据PRD文档以及设计方案来编写测试用例,也会组织会议找技术同学来看看测试方案合不合理,测试用例还有没有遗漏的地方。这个会议基本上1到两个小时。


故障复盘会议


这个会议大概是程序员最不想参加的会议了,在互联网公司出现故障是需要进行复盘的,一旦要开故障复盘会议就意味着出现了线上故障。程序员最担心的就是出现线上故障,如果搞出来一个P0级别的,基本一年就白干了,提前预定本财年3.25。


其他会议


还有一些其他会议,比如项目KO会议、平台对接会议、方案讨论会议、交互评审会议等等。


开发


技术同学开发编写代码以及功能自测,完了之后再和前端同学或者上下游业务的同学进行联调,这个过程也是比较耗时间的。因为在联调的时候总是会遇到这样或者那样的问题,比如环境问题、数据问题、代码Bug等等。其中最主要的就是代码Bug问题,因为有的技术同学可能由于时间关系根本没来得及自测就和大家进行联调,直接把联调当作自己的单元测试和功能自测了,所以这种情况下基本都是一边联调一边修改的状态。另外在联调的时候可能也会发现一些设计上对不齐的地方,这个时候也需要进行修改,同样需要耗费时间。


碎片化的杂事


杂事就很多了,比如别的团队的技术同学来向你咨询业务问题,你得花费时间向别人解答。小组内每个同学都需要进行技术分享,那你就得准备PPT,还得认真准备,因为我们需要不断建立自己的技术影响力,如果讲的东西太水了,实际上浪费自己的时间也浪费别人的时间。功能发布上线之前,TL以及组内的同学需要review你的代码,这也会耗费一个小时左右的时间。线上如果出现报警,我们就需要排查定位问题修复bug。类似这种碎片化的杂事充斥在我们的工作中。


如何避免不必要的加班


我们分析完了每天的时间都花在哪里之后,就可以进行针对性的进行时间压缩。


不必要的会议能不参加就不参加


这里举个栗子,在需求澄清会议中,一般会对一批需求进行澄清,这些需求很多时候只有一小部分和我们相关,所以在会议上我们对和我们相关的需求进行重点讨论和理解,在确认完需求内容之后就可以撤了,其他和自己不相关的需求没有必要再耗时间在会议上,这样可以大大压缩参加会议的时间。


避免表面一致


在整个研发活动中我们需要避免表面一致情况的发生,什么意思呢?就是我们需要真正把需求弄明白想清楚再动手写代码,否则在没搞清楚需求情况下写代码,很容易出现做出来的功能和产品要求的出现偏差的情况,那么在验收的时候就容易不通过,然后就是互相扯皮,研发说产品当时没有讲清楚,产品说是研发没有理解透需求。最后就是返工重新修改,白白浪费了时间。所以我们不如在需求确认阶段,多花点时间和产品经理进行需求的深入沟通,把需求吃透了确保没有理解上的偏差,避免表面一致,这样可以帮助我们节约很多时间。


琐碎事情统一处理


我们所负责的业务不可避免的会有业务合作方来咨询你如何用平台以及对接的事情,比如平台如何使用,有哪些开放能力等等。来咨询的同学一般呈现散点状,也就是说不固定时间来咨询,有的时候你正在奋笔疾书写代码,但是别人过来咨询就会打断你的编码思路占用开发时间。所以这个时候你可以考虑专门下午四点到五点固定时间段拉个群来统一回复别人的问题,或者说如何对接的写一篇文档,有别人过来问的就先让文档,这就好比智能客服一样,这样可以帮助你挡掉70%左右的问题。不免这种业务答疑打乱你原本的开发工作。


加强代码质量管理


自己写的代码的时候要注重代码质量控制,这样在和别人联调的时候可以保证自己没问题节约一部分时间,另外上线后没有问题,更加节省了后续在向上定位排查解决问题以及复盘的时间,如果代码质量不好匆忙上线可能会浪费自己更多的时间。


总结


本文主要和大家分析了下程序员的时间都花费在哪些事项当中,我们怎么做才能避免不必要的加班。如果文中有适合大家使用的小技巧,可以在实际工作中试一试。我一直觉得工作是为了更好的生活,所以我们努力减少不必要的加班,多陪陪家人,多分给生活多一点时间,相信可以减少我们的精神内耗。


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

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的而且消息已读后需要多次刷新会话列表才会清空未读消息

为什么接收消息成功后调用聊天记录列表和会话列表返回的最后一条消息不是最新的
而且消息已读后需要多次刷新会话列表才会清空未读消息

如果你的同事还不会配置commit提交规范,请把这篇文章甩给他

Git
前言 首先问问大家在日常工作中喜欢哪种commit提交?git commit -m "代码更新" git commit -m "解决公共样式问题" git commit -m "feat: 新增微信自定义分享" 如果你是第三种,那我觉得你肯定了解过com...
继续阅读 »

前言


首先问问大家在日常工作中喜欢哪种commit提交?

git commit -m "代码更新"

git commit -m "解决公共样式问题"

git commit -m "feat: 新增微信自定义分享"

如果你是第三种,那我觉得你肯定了解过commit提交规范,可能是刷到过同类文章也可能是在工作中受到的要求


我自己是在刚出来实习的一家公司了解到的,依稀记得“冒号要用英文的,冒号后面要接空格...”


虽然我一直保持这种习惯去提交代码,但是后面遇到的同事大部分都是放飞自我的提交,看的我很难受


因此这篇文章就教还不会配置的小伙伴如何配置被业界广泛认可的 Angular commit message 规范以及呼吁大家去使用。


先来了解下commit message的构成

<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>

对应的commit记录如下图


微信截图_20230608114515.png




  • type: 必填 commit 类型,有业内常用的字段,也可以根据需要自己定义



    • feat 增加新功能

    • fix 修复问题/BUG

    • style 代码风格相关无影响运行结果的

    • perf 优化/性能提升

    • refactor 重构

    • revert 撤销修改

    • test 测试相关

    • docs 文档/注释

    • chore 依赖更新/脚手架配置修改等

    • workflow 工作流改进

    • ci 持续集成

    • types 类型定义文件更改

    • wip 开发中

    • undef 不确定的分类




  • scope: commit 影响的范围, 比如某某组件、某某页面




  • subject: 必填 简短的概述提交的代码,建议符合 50/72 formatting




  • body: commit 具体修改内容, 可以分为多行, 建议符合 50/72 formatting




  • footer: 其他备注, 包括 breaking changes 和 issues 两部分




git cz使用


只需要输入 git cz ,就能为我们生成规范代码的提交信息。


一、安装工具

npm install -g commitizen // 系统将弹出上述type、scope等来填写
npm install -g cz-conventional-changelog // 用来规范提交信息

ps:如果你是拉取别人已经配置好git cz的项目,记得也要在自己环境安装


然后将cz-conventional-changelog添加到package.json中

commitizen init cz-conventional-changelog --save --save-exact

微信截图_20230608155514.png


二、使用git cz提交


安装完第一步的工具后,就可以使用git cz命令提交代码了


微信图片_20230608092741.png


微信图片_20230608092732.png


如图,输入完git cz命令后,系统将会弹出提交所需信息,只需要依次填写就可以


commitlint使用


如果你不想使用git cz命令去提交代码,还是习惯git commit的方式去提交


那么接下来就教大家怎么在git commit命令或者vscode工具中同样规范的提交代码


一、安装工具

npm install --save-dev husky
npm install --save-dev @commitlint/cli
npm install --save-dev @commitlint/config-conventional

二、配置



  • 初始化husky
npx husky install


  • 添加hooks
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit \$1'


  • 在项目根目录下创建commitlint.config.js,并配置如下
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-case': [2, 'always', ['lower-case', 'upper-case']],
'type-enum': [2, 'always',[
'feat', // 增加新功能
'fix', // 修复问题/BUG
'style', // 代码风格相关无影响运行结果的
'perf', // 优化/性能提升
'refactor', // 重构
'revert', // 撤销修改
'test', // 测试相关
'docs', // 文档/注释
'chore', // 依赖更新/脚手架配置修改等
'workflow', // 工作流改进
'ci', // 持续集成
'types', // 类型定义文件更改
'wip', // 开发中
'undef' // 不确定的分类
]
]
}
}

三、验证


没配置前能直接提交


微信图片_20230608092753.png


配置之后就会规范提交


微信图片_20230608092757.png


总结


以上两种方式都很简单,几个步骤下来就可以配置好,希望大家都能养成一个开发好习惯~


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

Handler真的难?看完这篇文章你就懂了!

在Android开发中,Handler是一个非常重要的组件,它可以用来实现线程之间的通信和任务调度。本篇文章将介绍Handler的使用方式和原理,帮助读者更好地理解Android开发中的线程处理。 什么是Handler? Handler是Android中的一个...
继续阅读 »

在Android开发中,Handler是一个非常重要的组件,它可以用来实现线程之间的通信和任务调度。本篇文章将介绍Handler的使用方式和原理,帮助读者更好地理解Android开发中的线程处理。


什么是Handler?


Handler是Android中的一个消息处理器,它可以接收并处理其他线程发来的消息。简单来说,Handler就是一个用来处理消息的工具类,它可以将消息发送给其他线程,也可以接收其他线程发送的消息进行处理。


Handler的使用方式


使用Handler的基本流程为:创建Handler对象 -> 发送消息 -> 处理消息。


在使用 Handler 之前,需要了解一些相关概念:



  • 线程:是独立运行的程序段,执行的代码是一个单独的任务。

  • 消息队列:是一种存储消息的数据结构,支持先进先出的队列操作。

  • Looper:可以让线程不停地从消息队列中取出消息并处理,是线程与消息队列交互的桥梁。

  • Message:是 Android 中处理消息的基本类,可以携带一些数据,用于在 Handler 中进行处理。


创建Handler对象


在使用Handler之前,需要先创建一个Handler对象。创建Handler对象的方式有两种:




  • 在主线程中创建Handler对象:


    在主线程中创建Handler对象非常简单,只需要在主线程中创建一个Handler对象即可:

    Handler handler = new Handler();



  • 在子线程中创建Handler对象:


    在子线程中创建Handler对象需要先获取到主线程的Looper对象,然后使用Looper对象来创建Handler对象:

    Handler handler = new Handler(Looper.getMainLooper());



发送消息


创建Handler对象之后,就可以使用它来发送消息了。发送消息的方式有两种:




  • 使用Handler的post()方法:


    使用Handler的post()方法可以将一个Runnable对象发送到Handler所在的消息队列中。Runnable对象中的代码会在Handler所在的线程中执行。

    handler.post(new Runnable() {
    @Override
    public void run() {
    // 在Handler所在的线程中执行的代码
    }
    });



  • 使用Handler的sendMessage()方法:


    使用Handler的sendMessage()方法可以将一个Message对象发送到Handler所在的消息队列中。Message对象中可以携带一些数据,用于在Handler中进行处理。

    Message message = new Message();
    message.what = 1;
    message.obj = "Hello World!";
    handler.sendMessage(message);



除了基本用法,Handler还有一些高级用法,下面列举了几个常用的:



  • 使用HandlerThread创建带有消息队列的线程,避免频繁地创建线程;

  • 使用Message.obtain()来获取Message对象,避免频繁地创建对象;

  • 使用Handler的sendEmptyMessage()方法来发送空消息。


处理消息


当其他线程发送消息到Handler所在的消息队列中时,Handler就会接收到这些消息并进行处理。处理消息的方式有两种:




  • 重写Handler的handleMessage()方法:


    重写Handler的handleMessage()方法可以处理其他线程发送的消息。handleMessage()方法中的代码会在Handler所在的线程中执行。

    Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
    switch (msg.what) {
    case 1:
    String message = (String) msg.obj;
    // 处理消息的代码
    break;
    default:
    break;
    }
    }
    };



  • 实现Handler.Callback接口:


    实现Handler.Callback接口可以处理其他线程发送的消息。Callback接口中的方法会在Handler所在的线程中执行。

    Handler.Callback callback = new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
    switch (msg.what) {
    case 1:
    String message = (String) msg.obj;
    // 处理消息的代码
    break;
    default:
    break;
    }
    return true;
    }
    };
    Handler handler = new Handler(callback);



Handler的原理


在Handler的背后,实际上是使用了消息队列和线程通信的机制。当其他线程发送消息时,消息会被加入到Handler所在的消息队列中。然后,Handler会从消息队列中取出消息进行处理。


消息队列和Looper


消息队列和Looper是 Handler 实现的基础。每个线程都有一个消息队列(Message Queue),消息队列中存储着队列的所有消息(Message)。线程通过一个 Looper 来管理它的消息队列,通过不断地从消息队列中读取消息,实现了线程的消息循环 (Message Loop) 的功能。

public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
// 一旦有消息,就会返回Message对象
msg.target.dispatchMessage(msg);
}
}

如上所示,一个线程中的消息无限循环直到队列里没有消息为止(MessageQueue.next())。消息通过 Message.target 属性来找到它想要执行的 Handler,从而被分配到正确的线程中并且得到执行。一旦有消息,就会调用 dispatchMessage(Message) 方法进行分发。


消息分发


消息分发是Handler 的核心部分,在它的内部逻辑中,也是最为关键的部分。


在 Handler 中,消息分发的流程如下:


1.1. 发送消息


由其他线程调用 Handler 的方法向消息队列中发送消息。

Handler handler = new Handler() ;
handler.post(new Runnable(){
@Override
public void run() {
// 在其他线程发送消息
}
});

1.2. 创建 Message 对象


将需要传输的数据封装成 Message 类型的对象,然后将该对象塞入消息队列中。

Message msg = new Message();
msg.obj = "消息内容";
handler.sendMessage(msg);

1.3. 将消息加入消息队列


Handler 将消息放入消息队列中。


在 Handler 内部,新构建的消息通过 enqueueMessage() 方法被加入到 MessageQueue 相应的内存块中,并且会在该内存块的标记 next 表示下一个内存块的索引号。

public void sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return;
}
msg.target = this;
queue.enqueueMessage(msg, uptimeMillis);
}

enqueueMessage() 方法的核心逻辑,就是紧接着找到消息队列中最近的一个时间戳比当前时间小的消息,将新消息插入到这个消息之后。

boolean enqueueMessage(Message msg, long when) {
synchronized (this) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}

if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}

boolean needWake;
if (mBlocked) {
// If the queue is blocked, then we don't need to wake
// any waiters since there can be no waiters.
msg.markInUse();
needWake = false;
} else {
msg.markInUse();
needWake = mMessagesForQueue.enqueueMessage(msg, when);
}

if (needWake) {
nativeWake(mPtr);
}
}

return true;
}

1.4. Looper 开启消息循环


Looper 不断轮询内部 MessageQueue 中的消息,获取消息后在 Handler 中进行分发处理。


在 Looper 类中,强制让当前线程创建一个 Looper 对象,并通过调用 QualityLooper 构造函数 create 方法捕获该对象(一般用于构建线程的消息循环)。接下来,通过调用 run 方法被延迟1秒钟来启动上下文中的消息循环。

public static void prepare() {
prepare(true);
}

public static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}

public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
continue;
}
msg.target.dispatchMessage(msg);
}
}

这个方法是一个无限循环方法,在每个循环中,Looper 都会从自己的消息队列中获取一个消息,如果队列为空,则一直循环等待新的消息到来,直到被调用 quit() 方法,才终止循环。在获取到消息之后,调用 msg.target.dispatchMessage(msg) 进行消息的分发处理。


1.5. 查找目标 Handler


Looper 不断轮询消息队列,获取消息后,注意到 MessageQueue.next() 方法中有这样一行代码:

msg.target.dispatchMessage(msg);

1.6. 传递 Message 对象


从消息中获取到 target 属性,它就是当前这个Message对象所属的 Handler,并执行该Handler的 handleMessage(Message) 方法。


dispatchMessage(Message) 的核心代码是判断 Message.target 是否为 null,不为 null 则将消息传递给目标 Handler,如果为 null,则直接抛出异常。

void dispatchMessage(Message msg) {
if (msg.callback != null) {
// 消息带有回调方法,如果 callback 不为空,那么就直接执行
handleCallback(msg);
} else {
if (mCallback != null) {
// 尝试将消息抛给 mCallback
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg); // 如果消息中没有 callback,那就执行 handleMessage(msg)
}
}

// 处理具体的 Message
public void handleMessage(Message msg) {
switch (msg.what) {
// 根据消息类型分发处理
default:
break;
}
}

当 Handler 接收到消息时,它会回调自己的 handleMessage(Message) 方法处理消息。


handleMessage(Message) 方法中,我们可以编写各种不同的逻辑,并对当前情况下的消息进行处理。这通常包括对消息类型的检查以及消息携带的数据的解析和操作。


当我们在 handleMessage(Message) 方法中完成了所有处理后,我们就可以将数据发送回发送消息的线程,或将数据传递给其他线程进行进一步处理。


总结


本篇文章深入探讨了 Handler 的原理,主要包括了消息队列和 Looper 的相关概念,以及消息的发送和处理。除此之外,还讲了当不同线程的消息需要在 Handler 中处理时,需要用到 Looper、MessageQueue 和 Handler 这三个关键组件的协同工作。


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

如何深入掌握 Android 系统开发的拦路虎 Binder

0. 为什么要深入学习 Binder Binder 是整个 Android 的基石 所有的系统服务都是基于 Binder,比如 AMS WMS PMS SurfaceFlinger Audiofilinger 以及硬件操作服务等等 Android 四大组件...
继续阅读 »

0. 为什么要深入学习 Binder



  • Binder 是整个 Android 的基石

    • 所有的系统服务都是基于 Binder,比如 AMS WMS PMS SurfaceFlinger Audiofilinger 以及硬件操作服务等等

    • Android 四大组件的底层实现离不开 Binder



  • 做系统开发需要自定义一些系统服务,这些工作需要我们了解 Binder

  • Android O 以后的 Treble 计划,基于 Binder 魔改出了 HwBinder VndBinder。

  • ANR 冻屏 卡顿 卡死等偶现 BUG 可能与 Binder 相关


1. 学习 Binder 的四个阶段



  • 会用,能添加 Java Native 系统服务

  • 熟读应用层各种情景下的源码

  • 熟读内核里面的数据结构和流程

  • 能解决各种奇奇怪怪的 bug


2. 准备工作


下载编译好 AOSP + Kernel,能通过自定义内核的方式启动虚拟机。


这部分内容比较简单,可以参考:



3. 预备基础知识


预备基础知识快速过一遍,忘了再回头再看



4. Binder 基本原理


首先要明确一点 Binder 是一个 RPC(Remote Procedure Call) 框架,也就是说借助于 Binder,我们可以在 A 进程中访问 B 进程中的函数。


4.1 IPC 原理


RPC 一般基于 IPC 来实现的,IPC 就是跨进程数据传输,大白话就是在 A 进程可以访问到 B 进程中的数据,或者说 B 进程中的数据可以传递给 A 进程,都是一个意思。


在 Linux 中,每个进程都有自己的虚拟内存地址空间。虚拟内存地址空间又分为了用户地址空间和内核地址空间。



不同进程之间用户地址空间的变量和函数是不能相互访问的。


使得 A 进程能访问到 B 进程中数据的手段我们就称之为 IPC。


虽然用户地址空间是不能互相访问的,但是不同进程的内核地址空间是相同和共享的,我们可以借助内核地址空间作为中转站来实现进程间数据的传输。


具体的我们在 B 进程使用 copy_from_user 将用户态数据 int a 拷贝到内核态,这样就可以在 A 进程的内核态中访问到 int a



更进一步,可以在 A 进程中调用 copy_to_user 可以将 int a 从内核地址空间拷贝到用户地址空间。至此,我们的进程 A 用户态程序就可以访问到进程 B 中的用户地址空间数据 int a



为了访问 int a ,需要拷贝两次数据。能不能优化一下?我们可以通过 mmap 将进程 A 的用户地址空间与内核地址空间进行映射,让他们指向相同的物理地址空间:



完成映射后,B 进程只需调用一次 copy_from_user,A 进程的用户空间中就可以访问到 int a了。这里就优化到了一次拷贝。


4.2 RPC 原理


接着我们来看以下,Binder 的 RPC 是如何实现的:


一般来说,A 进程访问 B 进程函数,我们需要:



  • 在 A 进程中按照固定的规则打包数据,这些数据包含了:

    • 数据发给那个进程,Binder 中是一个整型变量 Handle

    • 要调用目标进程中的那个函数,Binder 中用一个整型变量 Code 表示

    • 目标函数的参数

    • 要执行具体什么操作,也就是 Binder 协议



  • 进程 B 收到数据,按照固定的格式解析出数据,调用函数,并使用相同的格式将函数的返回值传递给进程 A。



Binder 要实现的效果就是,整体上看过去,进程 A 执行进程 B 中的函数就和执行当前进程中的函数是一样的。


5. Binder 应用层工作流程


Binder 是一个 RPC(Remote Procedure Call) 框架,翻译成中文就是远程过程调用。也就是说通过 Binder:



  • 可以在 A 进程中访问 B 进程中定义的函数

  • 进程 B 中的这些等待着被远程调用的函数的集合,我们称其为 Binder 服务(Binder Service)

  • 进程 A 称之为 Binder 客户端(Binder Client),进程 B 称之为 Binder 服务端(Binder Server)

  • 通常,系统中的服务很多,我们需要一个管家来管理它们,服务管家(ServiceManager) 是 Android 系统启动时,启动的一个用于管理 Binder 服务(Binder Service) 的进程。通常,服务(Service) 需要事先注册到服务管家(ServiceManager),其他进程向服务管家(ServiceManager) 查询服务后才能使用服务。

  • Binder 的 RPC 能力通过 Binder 驱动实现


通常一个完整的 Binder 程序涉及 4 个流程:



  1. 在 Binder Server 端定义好服务

  2. 然后向 ServiceManager 注册服务

  3. 在 Binder Client 中向 ServiceManager 获取到服务

  4. 发起远程调用,调用 Binder Server 中定义好的服务


整个流程都是建立在 Binder 驱动提供的跨进程调用能力之上:



6. Android Binder 整体架构


从源码实现角度来说,Binder 整体架构实现如下:



有点复杂,我们一点点说:




  • VFS 是内核中的一个中间层,向上对应用层提供统一的系统调用函数,这些系统调用函数主要是 open mmap ioctl write read ioctl 等,向下封装不同的外设(字符设备,块设备),系统文件,文件系统的操作。Binder 是一个字符驱动,当应用层调用到 binder 的 open mmap ioctl release 系统调用时,经过 vfs 的一层包装后,就会调用到 Binder 驱动中的 binder_open bider_mmap binder_ioctl binder_release 函数。




  • 不同于一般的驱动,Binder 应用层的使用要复杂不少,如果直接使用 open mmap ioctl release 系统调用会使得应用程序非常复杂且难以复用相同功能的代码,刚开始 google 的工程师做了一套简单的封装,把常用的操作封装为一系列的函数,这些函数都在 binder.c 中,ServiceManger 的就是通过 binder.c 中封装的函数实现的(Android10及以前)。源码中还存在一个 bctest.c 的程序,这个是 binder.c 的一个测试程序。C 语言级别的封装虽然简单,但使用起来还是稍显麻烦,很多细节也没有考虑进去,所以 google 的工程师又封装了一个叫 libbinder 的库,我们 native 层的 binder 服务端与客户端都是基于这个库来实现的,Java 层的 binder 服务端与客户端都是通过 JNI 间接使用 libbinder 库实现的,从使用上来说 libbinder 更为简单,但是 libbinder 本身比 binder.c 复杂了不少。




7. C 层实现分析


很多博客教程会忽略这一层的分析,相比 libbinder 库的封装,binder.c 会简单不少 ,方便初学者理解 binder 应用层工作流程。


我们可以模仿 bctest.c 写一个完整的 Binder 应用层 demo。


这个工作已经有大佬完成了:


github.com/weidongshan…


但是也有一些问题,这个代码是基于 Android5 的,稍微有点老了,我在以上实现的基础上做了一些修改和适配工作,使得代码可以在 Android10 上跑起来:


github.com/yuandaimaah…


关于这个示例程序的分析,可以参考以下几篇文章:



8. 驱动分析


驱动分析这部分结合 C 层应用的实现来分析驱动的实现,主要搞清楚:



  • 三个情景的流程:注册,获取,使用

  • 三个情景下内核中各种数据结构的变化


这部分内容可以参考之前分享的:



9. C++ 层分析


首先我们要写一个基于 libbinder 库的 Demo,能跑起来就行:



接着分析三个情景下的执行过程与各个函数的功能:



当然还有两个特殊的场景也需要进行分析:



  • 死亡通知

  • 多线程


这部分内容会陆续在公众号和掘金平台推送。


10. Java 层分析


学习这部分的前提是了解 JNI 编程。这个可以参考系列文章:



我们先写一个 Demo,能跑起来就行:



接着我们分析三个情景下的执行过程与各个函数的功能:



当然还有一些其他高级特性也需要我们分析,这部分内容会在后续推送:



  • AIDL 中 in out inout oneway 的分析

  • Parcel 数据结构分析

  • Java 层死亡通知

  • Java 层多线程分析


11. 疑难问题


不论是应用开发还是系统开发我们都会遇到一些棘手的 bug,很多时候这些 bug 都和 binder 有关,总结起来,大概可以分为几类:



  • 死锁

  • 线程池满了

  • 代理对象内存泄露

  • 传输数据过大

  • 关键方法内发起 Binder 同步调用导致卡顿

  • Android O 异步远程调用无限阻塞冻屏 bug


这类 bug 很多都难以复现,很多时候都不了了之了,导致拥有这部分经验的同学很少。


这部分内容工作量巨大,我会在接下来的时间陆续在公众号和掘金推送相关的文章。


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

我的前端开发学习之路,从懵懂到自信

前端开发,刚开始学的时候,我感觉自己就像个孩子,一脸懵懂。当时,我非常迷茫,不知道该从何开始学习。但是,我并没有放弃,因为我对前端开发充满了热情和兴趣。 刚开始学习时,我觉得 HTML、CSS 和 JavaScript 这些基础知识就已经够难了,但是当我开始接...
继续阅读 »

前端开发,刚开始学的时候,我感觉自己就像个孩子,一脸懵懂。当时,我非常迷茫,不知道该从何开始学习。但是,我并没有放弃,因为我对前端开发充满了热情和兴趣。


刚开始学习时,我觉得 HTML、CSS 和 JavaScript 这些基础知识就已经够难了,但是当我开始接触一些流行的框架和库时,我才发现自己真正的水平有多菜。当时,我就像一只踩在滑板上的小猪,不断地摔倒,但是我并没有放弃,我一直在努力地学习和实践。


在学习的过程中,我遇到了许多困难和挑战,但是也有很多有趣的体验和经历。有一次,我在编写一个简单的网页时,花了一整天的时间,结果发现自己的代码有一个很小的错误,导致整个网页无法正常显示。当时我感觉自己就像一个猴子在敲打键盘,非常无助和懊恼。但是,通过不断地调试和修改,我最终找到了错误,并且成功地将网页显示出来。当时,我感觉自己就像一只成功攀爬上树的小猴子,非常自豪和兴奋。


除了遇到困难和挑战,我在学习前端开发过程中也经历了许多有趣的体验。有一次,我在编写一个小型的应用程序时,发现我的代码出现了一个非常有趣的小 bug。当用户在页面上进行操作时,页面上的一些元素会突然出现在屏幕的右侧,然后又突然消失不见。当时我还担心这个 bug 会影响用户的正常使用,但是后来发现这个 bug 其实很有趣,而且还能给用户带来一些意外的乐趣。于是我就把这个 bug 留了下来,并且在用户操作时添加了一些特效,让这个小 bug 变成了一个有趣的亮点。


12.jpg
总结一波:
第一点,学习前端开发需要有耐心。前端开发不是一个短时间内可以学会的技能,它需要大量的时间和精力。尤其是在学习的早期,你可能会觉得有些技术和概念非常难以理解。但是,只要你有耐心,坚持不懈地学习,最终你一定会掌握这些技能。


第二点,建立一个良好的学习计划非常重要。前端开发有很多不同的技术和概念,你需要有一个清晰的学习计划来帮助你系统地学习和掌握这些知识。首先,你需要了解 HTML、CSS 和 JavaScript 这三大基本技术。其次,你可以学习一些流行的框架和库,如 React、Vue、jQuery 等,这些技术可以帮助你更快捷地构建网站和应用程序。


第三点,实践是学习前端开发的关键。你可以通过练习编写代码来更好地理解前端开发的技术和概念。在学习的过程中,你可以尝试编写一些小项目,比如一个简单的网页或者一个小型的应用程序。通过实践,你可以更深入地了解前端开发的各个方面,并且提高你的编程技能。


第四点,不要害怕向他人寻求帮助。前端开发是一个非常开放和社交的领域,你可以通过参加社区活动、参与在线讨论、向他人寻求帮助等方式来更好地学习和成长。有时候,你可能会遇到一些困难,或者对某些概念不是很理解,这时候向他人寻求帮助是非常重要的。你可以参加一些线上或线下的前端开发社区,与其他开发者交流经验和技巧,也可以在 GitHub 等平台上查看其他人的代码,从中学习和借鉴。


第五点,不断更新自己的知识和技能。前端开发是一个不断发展和变化的领域,新技术和新概念层出不穷。因此,你需要不断地更新自己的知识和技能,跟上前端开发的最新动态。你可以通过阅读博客、参加培训课程、观看技术视频等方式来学习新的技术和概念。


总之,学习前端开发需要有耐心、建立一个良好的学习计划、实践、寻求帮助和不断更新自己的知识和技能。这些都是非常重要的,也是我在学习前端开发过程中得到的宝贵经验。通过不断地学习和实践,相信你我可以成为一名优秀的前端开发工程师。


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

闲聊:从我那二百多人的技术群讲起

随着我不断输出原创作品,我的公众号粉丝数量也迎来了漂亮的增长曲线。 我4个月前创建的技术群,也很快的突破了微信“扫码入群”的人数上限(200人),来到了250+。 于是,事情便开始有意思了。 虽然我从建群之初就早早给群定了一个基调「学习交流群」,可是随着五...
继续阅读 »

随着我不断输出原创作品,我的公众号粉丝数量也迎来了漂亮的增长曲线。


图片


我4个月前创建的技术群,也很快的突破了微信“扫码入群”的人数上限(200人),来到了250+。


图片


于是,事情便开始有意思了。


虽然我从建群之初就早早给群定了一个基调「学习交流群」,可是随着五湖四海的人涌入群之后,群里的聊天内容渐渐有了失控的迹象,毕竟人多嘴杂。


眼看着我辛辛苦苦拉起来的小群就快废了,于是我一次次站出来对全员喊话,控制局面,一次,两次,三次……无数次。


图片


经常是,我好不容易把话题拉回正轨,没过多久就会有人带头水群,这可把我头疼坏了。


图片


我每天下班路上就在想,要怎么把这二百多人构成的群体有序组织起来,不产生「坏味道」呢?于是我开始潜水到别人的粉丝群去观摩,结果非常令人失望。


几乎所有人数超过100的粉丝群,不管是金融领域,还是技术领域,都无一例外的出现了两个现象:


要么没人说话,集体沉默。要么全员飙车,集体水群


前者全员沉默的群,群主多半是来「养鱼」的,他们长期玩消失,很少在群里说话,也自然不会组织大家进行一些探讨和交流。


后者全员飙车的群,群主也是玩消失的,但是群成员活跃,要么一起骂街骂社会骂公司,要么一起聊骚聊八卦,话题早就背离了入群的初衷。


其中有一个群,群主还算负责,但是没什么管理能力,群友天天刷屏,严重影响其它人的正常交流。于是群主不知从哪想了一个招:新建一个VIP群,这是个收费群,每个月收群员10块钱,其中受邀优质群员可以免收(我是其中之一)。


这个操作一出来,他的大群和公众号就炸锅了……一堆人骂他,什么难听的都有。


我把这一切默默的看在眼里,记在心里,每当我想要重新拉一个「核心技术交流群」的时候,我就摇摇头,不行,不能这么干,你这是在把所谓的「精英阶层」抽离出来区别对待,看似获得了一小群人的簇拥,但是你失去的是「普通的大多数」


于是,我换了个思路:堵不如疏,我新建了一个「聊骚摸鱼群」,把群二维码直接丢大群里,来吧,兄弟姐妹们,要开车要八卦的,上车!


图片


同时,所有人禁止在大群里聊骚摸鱼水群,忍不住的,到隔壁群去,主群只做技术交流。


这个操作一出,效果极好!


主群里每天稳定高频的交流着技术和职场信息。


图片


而隔壁群里也默默的开着车。


图片


各取所需,相得益彰!


一个两百多人的组织尚且如此,我们的社会何尝不是呢?


根据熵增定律能量是不会凭空消失的,只会进行转移


人的本性是动物性,而非人性。人的本能是欲望,而非克制。人天生就有物欲,喜欢吃喝,美色,钱财,权力……是因为人类群居生活中需要安全和稳定,所以产生了道德,法律,伦理等约束力,对人的动物性进行管控。


可是,一味的克制约束不发泄,人是会出事的!因为能量释放不出去是会原地「爆炸」的。

所以你以为所谓的「奶头乐」文化是怎么来的?



奶头乐理论


由于生产力的不断上升,世界上的一大部分人口将不必也无法积极参与产品和服务的生产。为了安慰这些“被遗弃”的人,避免阶级冲突,方法之一就是制造“奶头”、喂之以“奶头”——使令人陶醉的消遣娱乐和充满感官刺激的产品(比如网络、电视和游戏)填满人们的生活、转移其注意力和不满情绪,令其沉浸在“快乐”中不知不觉丧失思考能力、无心挑战现有的统治阶级。



如果不给人类群体一些「释放能量」的渠道和方式,那么这股巨大的能量迟早会产生毁灭性的打击,造成组织倾覆。


这便是堵不如疏


不过很可惜,我们身边充斥着大量无能的管理者,对待团队只会一招:忍,狠,滚


他们较少关注团队的情绪和诉求,自然也就无法获得团队成员的充分信任,毕竟这年头,人们逐渐意识到「情绪价值」对自己非常重要!


老话说得好:“吃得亏,吃不得气”,便是这个道理。


好啦,今天就闲聊到这里吧,本来我是准备再发一篇技术文的,但是下班路上突然想到这件事,越想越有意思,就分享出来给大家。


后面,我的「沐洒布吉岛」还会努力的维持着技术文化氛围,给众多支持我的粉丝一个交代。


而隔壁群嘛……


图片


还是赶紧转让群主保命吧,哈哈哈哈。


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

35岁愿你我皆向阳而生

35岁是一个让程序员感到焦虑的年龄。这个年纪往往意味着成熟和稳定,但在技术领域,35岁可能意味着过时和被淘汰。在这篇文章中,我将探讨35岁程序员的思考,以及如何应对这种感受,这里我不想贩卖焦虑,只是对现实的一些思考。 当我在20多岁研究生刚刚毕业的时候,恰逢互...
继续阅读 »

35岁是一个让程序员感到焦虑的年龄。这个年纪往往意味着成熟和稳定,但在技术领域,35岁可能意味着过时和被淘汰。在这篇文章中,我将探讨35岁程序员的思考,以及如何应对这种感受,这里我不想贩卖焦虑,只是对现实的一些思考。


当我在20多岁研究生刚刚毕业的时候,恰逢互联网蓬勃发展,到处都是机会,那时候年轻气盛,我充满了能量和热情。我渴望学习新的技术和承担具有挑战性的项目。我花费了无数的时间编码、测试和调试,常常为了追求事业目标而牺牲了个人生活。


慢慢的当我接近30岁的时候,我开始意识到我的优先事项正在转变。我在职业生涯中取得了很多成就,但我也想专注于我的个人生活和关系。我想旅行、和亲人朋友共度时光,并追求曾经被工作忽视的爱好。


现在,35岁的我发现自己处于一个独特的位置。我在自己的领域中获得了丰富的经验,受到同事和同行的尊重。然而,我也感到一种不安和渴望尝试新事物的愿望。这时候我很少在代码上花费时间,而是更多的时间花到了项目管理上,一切似乎很好,但是疫情这几年,行业了很大的影响,公司的运营也变得步履维艰,在安静的会常常想到未来的的规划。


一、焦虑情绪的来源


35岁程序员的焦虑情绪源于其所处的行业环境。技术不断发展,新的编程语言、框架、工具层出不穷,要跟上这些变化需要付出大量的时间和精力。此外,随着年龄的增长,身体和心理健康也会面临各种问题。这些因素加在一起,让35岁程序员感到无从下手,不知道该如何面对未来。


二、面对焦虑情绪的方法


1学习新技能


学习新技能是应对技术革新的必经之路。与其等待公司提供培训或者等待机会,35岁程序员应该主动寻找新技术,并投入时间和精力去学习。通过参加课程、阅读文献,甚至是找到一位 mentor,35岁程序员可以更快地适应新技术,保持竞争力。


2关注行业动态


35岁程序员要时刻关注技术行业的最新动态。阅读技术博客、参加社区活动,以及了解公司的发展方向和战略规划,这些都是成为行业领跑者所必须的。通过增强对行业趋势的了解,35岁程序员可以更好地做出决策,同时也可以通过分享经验获得他人的认可和支持。


3 与年轻人合作


与年轻的程序员合作可以带来许多好处。他们可能拥有更新的知识和技能,并且乐于探索新事物。35岁的程序员应该通过与年轻人合作,学习他们的思考方式和方法论。这样不仅可以加速学习新技能,还可以提高自己的领导能力。


每周我都会组织公司内部的技术交流活动,并积极号召大家发表文章,通过这些技术分享,我发现每个人擅长的东西不同,交流下来大家的收获都很大。


4重新审视个人价值观


在35岁之后,程序员可能会重新审视自己的职业生涯和个人发展方向。当面临焦虑情绪时,建议去回顾一下自己的愿景和目标。这有助于确定下一步的工作方向和计划。此外,35岁程序员也应该考虑个人的非技术技能,例如领导力、沟通能力和团队合作精神,这些技能对长期职业成功至关重要。


5 敞开心扉学会沟通


 程序员给大家的一个刻板印象就是不爱沟通,刻板木讷,大家都是干活的好手,但是一道人际关系处理上就显得有些不够灵活,保持竞争力的一个很关键的点也在于多沟通,多社交,让自己显得更有价值,有一句老话说的好:多一个朋友,多一条路。沟通需要技巧,交朋友更是,这也是我们需要学习的。


三、总结


35岁是程序员生涯中的一个重要节点,同时也是一个充满挑战和机会的时期。如何应对焦虑情绪,保持竞争力并保持个人发展的连续性,这需要程序员深入思考自己的职业规划和发展方向。


通过学习新技能、关注行业动态、与年轻人合作以及审视个人价值观,35岁程序员可以在未来的职业生涯中不断成长和发展。


归根到底,无论如何生活的好与坏都在于我们对待生活的态度,幸福是一种感受,相由心生,无论你处于何种生活状态,都希望大家向阳而生。


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

我裸辞了,但是没走成!

人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了! 1.为什么突然想不干了? 1.奇葩的新组长 我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑! 今年过年前,...
继续阅读 »

人在国企,身不由己!公司福利和薪资还可以,但有个难顶的组长就不可以,说走就走!如果把这个组长换了的话就另说了!


1.为什么突然想不干了?


1.奇葩的新组长


我的前组长辞职了,然后被安排到这个自研项目组,而这个新组长我之前得罪过,老天爷真爱开玩笑!


今年过年前,我主开发的平台要嵌入到他负责的项目里面,一切对接都很顺利,然而某天,有bug,我修复了,在群里面发消息让他合并分支更新一下。他可能没看到,然后我下班后一个小时半,我还在公司,在群里问有没有问题,没回应!


然后我就坐车回家,半路,产品经理组长、大组长和前组长一个个轮流call我,让我处理一下bug,我就很无语!然后我就荣获在家远程办公,发现根本没问题!然后发现是对方没更新的问题!后面我修复完直接私聊他merge分支更新,以免又这样大晚上烦人!
而类似的事情接连发生,第三次之后,我忍不住了,直接微信怼了他,他还委屈自己晚上辛苦加班,我就无语大晚有几个人用,晚上更新与第二天早上更新有什么区别?然后就这样彻底闹掰了!


我就觉得这人很奇葩,有什么问题不能直接跟我沟通,一定要找我的上级一个个间接联系我呢?而且,这更新流程就很有问题,我之前在别的组支援修bug,是大早上发布更新,一整天测试,保证不是晚上的时候出现要紧急处理的问题!


然后,我跟这人有矛盾后,我就没继续对接这个项目了,前组长安排了别人代替我!


结果兜兜转转,竟然调到他这里来!作孽啊!


2.项目组乱糟糟


新项目组可以看出新组长管理水平很糟糕!


新组长给自己的定位是什么都管!产品、前后端、测试、业务等,什么都往自己身上揽!他自己觉得很努力,但他不是那部分的专业人员,并不擅长,偏偏还没那个金刚钻揽一堆瓷器活!老爱提建议!


项目组就两个产品,其中一个是UI设计刚转,还没成长为专业的产品经理,而那个主要负责的产品经理根本忙不过来!


然后,他一个人搞不定,就开始了PUA大法,周会的时候就会说:“希望大家要把这个项目当成自己的事业来奋斗,一起想,更好地做这个产品!”


“这个项目集成了那么多的模块功能,如果大家能够做好,对自己有很大的历练和成长!”


“我们项目是团队的重点项目,好多领导都看好,开发不要仅限于开发,要锻炼产品思维……”


……


简而言之就是,除了本职工作也要搞点产品的工作!


然后建模师开始写市场调研文档,前后端开发除了要敲代码,还得疯狂想新功能。


整个组开始陷入搞新东西的奇怪旋涡!


某次需求评审的时候,因为涉及到大量的文件存储,我提出建议,使用minio,fastdfs,这样就不用每次部署的时候,整体文件还要迁移,结果对方一口拒绝,坚决使用本地存储,说什么不要用XX平台的思想来污染他这个项目,他这个项目就要不需要任何中间件都能部署。


就很无语!那个部署包又大又冗余,微服务都不用,必须要整包部署整套系统,只想要某几个功能模块都不行,还坚持说这样可以快速整包部署比较好!


一直搞新功能的问题就是版本更新频繁!一堆新功能都没测清楚就发布,导致产品质量出现严重问题,用户体验极差!终于用户积攒怨气爆发了,在使用群里面@了我们领导,产品质量问题终于被彻底揭开了遮羞布!


领导开始重视这个产品质量的问题,要求立即整改!


然后这个新组长开始新一轮搞事!


“大家保证新功能进度的同时,顺便测试旧功能,尽量不要出bug!”


意思就是你开发进度也要赶,测试也要搞!


就不能来点人间发言吗?


3.工作压力剧增


前组长是前端的,他带我入门3D可视化的,相处还算融洽!然而他辞职了,去当自由职业者了!


新组长是后端的,后端组长问题就是习惯以后端思维评估前端工作,给任务时间很紧。时间紧就算了,还要求多!


因为我之前主开发的项目是可视化平台,对方不太懂,但不妨碍他喜欢异想天开,加个这个,加个那个,他说一句话,就让你自行想象,研究竞品和评估开发时间!没人没资源,空手套白狼,我当时就很想爆他脑袋了!


我花一个星期集成了可视化平台的SDK,连接入文档都写好了,然后他验收的时候提出一堆动态配置的要求,那么大的可视化平台,他根本没考虑项目功能模块关联性和同步异步操作的问题,他只会提出问题,让你想解决方案!


然后上个月让我弄个web版的Excel表格,我看了好多开源项目,也尝试二开,增加几个功能,但效率真的好低!于是我就决定自己开发一个!


我开发了两个星期,他就问我搞定没?我说基本功能可以,复杂功能还在做!


更搞笑的是,我都开发两个星期了,对方突然中午吃饭的时候微信我,怕进度赶不上,建议我还是用开源的进行二开,因为开源有别人帮忙一起搞。


我就很无语,人家搞的功能又不是一定符合你的需求,开源不等于别人给你干活,大家都是各干各的,自己还得花精力查看别人代码,等价于没事找事,给自己增加工作量!别人开发的有隐藏问题,出现bug排查也很难搞,而自己开发的,代码熟悉,即便有问题也能及时处理!


我就说他要是觉得进度赶不上就派个人来帮忙,结果他说要我写方案文档,得到领导许可才能给人。


又要开发赶进度,又要写文档,哪有那么多时间!最终结果就是没资源,没人手,进度依旧要赶!


因为我主开发的那个可视化平台在公司里有点小名气,好多平台想要嵌入,然后,有别的平台找到他要加上这个可视化平台,但问题是我很忙,又要维护又要开发,哪搞得了那么多?还说这个很赶!赶你个头!明知道时间没有,就别答应啊!工作排期啊!


新组长不帮组员解决问题,反而把问题抛给组员,压榨组员就很让人反感!


2.思考逃离


基于以上种种!我觉得这里不是一个长久之地,于是想要逃离这里!


我联系了认识的其他团队的人,别人表示只要领导愿意放人,他们愿意接收我,然后我去咨询一些转团队的流程,那些转团队成功的同事告诉我,转团队最难的是领导放人这关,而且因为今年公司限制招聘,人手短缺,之前有人提出申请,被拒绝了!并且转团队的交接的一两个月内难免要承受一些脸色!有点麻烦!


我思虑再三,我放弃了转团队这条路,因为前组长走了之后,整个团队只剩下我一个搞3D开发的,估计走不掉!


3.提出辞职


忍了两个月,还是没忍住,工作最重要的是开开心心!赚钱是一回事,要是憋出个心理疾病就是大事了!于是我为了自己的身心健康,决定走人!拜拜了喂!老娘不奉陪了!


周一一大早,我就提交了辞职信,大组长表示很震惊,然后下午的时候,领导和大组长一起来跟我谈话,聊聊我为什么离职?问我有没有意愿当个组长之类的,我拒绝了,我只想好好搞技术!当然我不会那么笨去说别人的坏话得罪人!


我拿前组长当挡箭牌,说自己特别不习惯这个新组长的管理方式!前组长帮我扛着沟通之类的问题,我只要专心搞开发就好了!


最终,我意志坚定地挡住了领导和大组长的劝留谈话,并且开始刷面试题,投简历准备寻找新东家!


裸辞后真的很爽,很多同事得知消息都来关心我什么情况,我心里挺感动的!有人说我太冲动了,可以找好下家再走!但我其实想得很清楚,我没可能要求一个组长委屈自己来适应我,他有他的管理方式,我有我的骄傲!我不喜欢麻烦的事,更不喜欢委屈自己,一个月后走人是最快解决的方案!


4. 转机


其实我的离开带来了一点影响,然后加上新组长那个产品质量问题警醒了领导,然后新组长被调去负责别的项目了,换个人来负责现在的项目组,而这个人就是我之前支援过的项目组组长,挺熟悉的!


新新组长管理项目很有条理也很靠谱,之前支援的项目已经处于稳定运行的状态了,于是让他来接手这个项目!他特意找我谈话,劝我留下来,并且承诺以后我专心搞技术,他负责拖住领导催进度等问题!


我本来主要就是因为新组长的问题才走人的,现在换了个不错的组长!可以啊!还能苟苟!


5.反思



  1. 其实整件事情中,我也有错,因为跟对方闹掰了,就拒绝沟通,所以导致很多问题的发生,如果我主动沟通去说明开发难度的问题,并且争取时间,就不至于让自己处于一个精神内耗的不快乐状态。

  2. 发现问题后,我没有尝试去跟大组长反馈,让大组长去治治对方,或者让大组长帮忙处理这个矛盾,我真的太蠢了!

  3. 我性格其实挺暴躁的,看不顺眼就直接怼,讨厌的人就懒得搭理,这样的为人处世挺不讨喜的,得改改这坏脾气!

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

北京十年,来深圳了

离开北京是计划 2013年去的北京,至今整十年,来去匆匆。 几年前就计划好了,赶在孩子上幼儿园之前离开北京,选一个城市定居。 给孩子一个稳定的环境,在这儿上学成长,建立稳定的、属于他自己的朋友圈。人一生中最珍贵的友谊都是在年少无知、天真烂漫的时候建立的。 我们...
继续阅读 »

离开北京是计划


2013年去的北京,至今整十年,来去匆匆。


几年前就计划好了,赶在孩子上幼儿园之前离开北京,选一个城市定居。


给孩子一个稳定的环境,在这儿上学成长,建立稳定的、属于他自己的朋友圈。人一生中最珍贵的友谊都是在年少无知、天真烂漫的时候建立的。


我们希望孩子从他有正式的社交关系开始-幼儿园阶段,尽早适应一个省市的教育理念和节奏,不要等到中小学、甚至高中阶段突然的打断孩子的节奏,插班到一个陌生的班级。他同时要面临环境和学业的压力,不是每个孩子都能很快调整过来的。


我自己小学阶段换了好几次学校,成绩的波动很明显,不希望孩子再面临同样的风险。


另一方面,基于我们年龄的考虑,也要尽快离开,岁数太大了,换城市跳槽不一定能找到合适的岗位。


19年,基于对移动端市场的悲观,我开始考虑换一个技术方向。2020年公司内转岗,开始从事图形相关技术开发,计划2023年离开北京,是考虑要留给自己3年的时间从零开始积累一个领域的技术。


来深圳市是意外


这几年一直在关注其他城市的"落户政策"、"互联网市场"、"房价"、"政府公共服务"。有几个城市,按优先级:杭州、广州、武汉、深圳。这些都是容易落户的城市,我们想尽快解决户口的困扰。


看几组数据:




2023年5月份数据


可以看到,杭州的房价排在第6位,但是收入和工作机会排进前4,所以首选杭州,性价比之王。


广州的房价和工作收入都排第5,中策。


武汉的工作机会排进前10,但是房价在10名开外,而且老家在那边,占尽地利,下策。


深圳的房价高的吓人,和这个城市提供的医疗、教育太不匹配,下下策。


最后选择深圳是形势所逼,今年行情史上最差,外面的机会很少。我和老婆都有机会内部转岗到深圳,所以很快就决定了。


初识深圳


来之前做了基本的调研,深圳本科45岁以内 + 1个月社保可以落户。我公司在南山,老婆的在福田,落户只能先落到对应的区。


我提前来深圳,一个星期租好了房子,确定了幼儿园。


老婆步行15分钟到公司,孩子步行500米到幼儿园,我步行 + 地铁1小时到公司。


福田和南山的教育资源相对充足,有些中小学名校今年都招不满,租房也能上,比龙华、宝安、龙岗等区要好很多。


听朋友说,在龙华一个很差的公立小学1000个小孩报名,只有500个学位。


有不少房东愿意把学位给租户使用,办理起来也不麻烦,到社区录入租房信息即可。和北京一样,采取学区划分政策,按积分排名录取,非常好的学校也要摇号碰运气。


租房


中介小哥陪我看了三四天房子,把这一片小区都看了个遍。考虑近地铁、近幼儿园、有电梯、装修良好等因素。


我本来想砍200房租,中介说先砍400,不行再加。结果我说少400,房东直接说好。我原地愣住了,之前排练的戏份都用不上了,或许今年行情不好,租房市场也很冷淡吧。


小区后面是小山,比较安静。


小区附近-0


小区附近-1


小区附近-2


小区附近-3


外出溜达,路过一所小学


深圳的很多小区里都有泳池
小区-泳池


夜晚的深圳,高楼林立,给人一种压迫感,和天空格格不入。明亮的霓虹灯,和北京一样忙碌。


晚上8点的深圳


晚上10点的深圳


对教育的看法



幸运的人一生都被童年治愈,不幸的人一生都在治愈童年--阿德勒



身边的朋友,有不少对孩子上什么学校有点焦虑,因为教育和高考的压力,有好友极力劝阻我来深圳。我认为在能力的范围内尽力就好,坦然面对一切。


焦虑是对自己无能为力的事情心存妄念。 如果一个人能坦然面对结果,重视当下,不虚度每一分每一秒,人生就不应该有遗憾。人生是来看风景的,结局都是一把灰,躺在盒子里,所以不要太纠结一定要结果怎么样。


学校是培养能力的地方,学历决定一个人的下限,性格和价值观决定上限,你终究成要为你想成为的人,不应该在自我介绍时除了学历能拿出手,一无是处。


不少人不能接受孩子比自己差。可是并没有什么科学依据能证明下一代的基因一定优于上一代吧,或许他们只是不能接受孩子比他们差,他们没有面子,老无所依。我天资一般,我也非常能接受孩子天资平庸,这是上天的旨意。


有些父母根本没有做好家庭教育,试图通过卷学校、一次性的努力把培养的责任寄托于学校。挣钱是成就自己,陪伴是成就孩子,成功的父母居中取舍。


陪伴是最好的家庭教育,如果因为工作而疏忽了孩子,我认为这个家庭是失败的,失败的家庭教育会导致家庭后半生矛盾重重,断送了全家人的幸福。


一个人缺少父爱,就缺少勇敢和力量,缺少母爱就缺少细腻与温和,孩子的性格很容易不健全。除非他自己很有天赋,能自己走出童年的阴影。


因为他长大面对的社会关系是复杂的,他需要在性格层面能融入不同的群体。性格不健全的孩子更容易走向偏激、自私、虚伪、或者懦弱,很多心理学家都是自我治疗的过程中,成为心理学大师。


一个人的一生中,学历不好带来的困扰是非常局部的,但是性格带来的问题将困扰其一生,包括工作、交朋结友、娶妻生子,并且还会传染给下一代。


榜样是最好的教育方法,没有人会喜欢听别人讲大道理,言传不如身教。有些人自己过的很可怜,拼命去鸡娃,那不是培养孩子,那是转移压力,过度投资,有赌棍的嫌疑。你自己过的很苦逼,你如何能说服孩子人生的意义是幸福?鸡娃的尽头是下一代鸡娃。


你只有自己充满能量,积极面对人生,你的孩子才会乐观向上;你只有自己持续的阅读、成长,你的孩子才会心悦诚服的学习;你只有自己做到追求卓越,你的孩子才会把优秀当成习惯。


不要给孩子传递一种信号,人生是苦的,要示范幸福的能力,培养孩子积极地入世观。


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

Gson与Kotlin的老生常谈的空安全问题

问题出现 偶然在一次debug中发现了一个按常理不该出现的NPE,用以下简化示例为例:Exception in thread "main" java.lang.NullPointerException: Cannot invoke "kotlin.Lazy.g...
继续阅读 »

问题出现


偶然在一次debug中发现了一个按常理不该出现的NPE,用以下简化示例为例:

Exception in thread "main" java.lang.NullPointerException: Cannot invoke "kotlin.Lazy.getValue()" because "<local1>" is null

对应的数据模型如下:

class Book(  
val id: Int,
val name: String?
) {
val summary by lazy { id.toString() + name }
}

发生在调用book.summary中。第一眼我是很疑惑了,怎么by lazy也能是null,因为summary本身就是一个委托属性,所以看看summary是怎么初始化的吧,反编译为java可知,在构造函数初始化,这完全没啥问题。

public final class Book {
@NotNull
private final Lazy summary$delegate;
private final int id;
@Nullable
private final String name;

@NotNull
public final String getSummary() {
Lazy var1 = this.summary$delegate;
Object var3 = null;
return (String)var1.getValue();
}

...略去其他

public Book(int id, @Nullable String name) {
this.id = id;
this.name = name;
this.summary$delegate = LazyKt.lazy((Function0)(new Function0() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke() {
return this.invoke();
}

@NotNull
public final String invoke() {
return Book.this.getId() + Book.this.getName();
}
}));
}
}


所以唯一的可能性就是构造函数并未执行。而这块逻辑是存在json的解析的,而Gson与kotlin的空安全问题老生常谈了,便立马往这个方向排查。


追根溯源


直接找到Gson里的ReflectiveTypeAdapterFactory类,它是用于处理普通 Java 类的序列化和反序列化。作用是根据对象的类型和字段的反射信息,生成相应的 TypeAdapter 对象,以执行序列化和反序列化的操作。
然后再看到create方法,这也是TypeAdapterFactory的抽象方法

  @Override
public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) {
Class<? super T> raw = type.getRawType();

if (!Object.class.isAssignableFrom(raw)) {
return null; // it's a primitive!
}

FilterResult filterResult =
ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, raw);
if (filterResult == FilterResult.BLOCK_ALL) {
throw new JsonIOException(
"ReflectionAccessFilter does not permit using reflection for " + raw
+ ". Register a TypeAdapter for this type or adjust the access filter.");
}
boolean blockInaccessible = filterResult == FilterResult.BLOCK_INACCESSIBLE;

// If the type is actually a Java Record, we need to use the RecordAdapter instead. This will always be false
// on JVMs that do not support records.
if (ReflectionHelper.isRecord(raw)) {
@SuppressWarnings("unchecked")
TypeAdapter<T> adapter = (TypeAdapter<T>) new RecordAdapter<>(raw,
getBoundFields(gson, type, raw, blockInaccessible, true), blockInaccessible);
return adapter;
}

ObjectConstructor<T> constructor = constructorConstructor.get(type);
return new FieldReflectionAdapter<>(constructor, getBoundFields(gson, type, raw, blockInaccessible, false));
}

最后到了ObjectConstructor<T> constructor = constructorConstructor.get(type);这一句,这很明显是一个类的构造器,继续走到里面的get方法

  public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {
final Type type = typeToken.getType();
final Class<? super T> rawType = typeToken.getRawType();

// ...省略其他部分逻辑

// First consider special constructors before checking for no-args constructors
// below to avoid matching internal no-args constructors which might be added in
// future JDK versions
ObjectConstructor<T> specialConstructor = newSpecialCollectionConstructor(type, rawType);
if (specialConstructor != null) {
return specialConstructor;
}

FilterResult filterResult = ReflectionAccessFilterHelper.getFilterResult(reflectionFilters, rawType);
ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType, filterResult);
if (defaultConstructor != null) {
return defaultConstructor;
}

ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
if (defaultImplementation != null) {
return defaultImplementation;
}

...

// Consider usage of Unsafe as reflection,
return newUnsafeAllocator(rawType);
}

先来看看前三个Constructor,



  • newSpecialCollectionConstructor

    • 注释说是提供给特殊的无参的集合类构造函数创建的构造器,里面的也只是判断了是否为EnumSet和EnumMap,未匹配上,跳过



  • newDefaultConstructor

    • 里面直接调用的Class.getDeclaredConstructor(),使用默认构造函数创建,很明显看最上面的结构是无法创建的,抛出NoSuchMethodException



  • newDefaultImplementationConstructor

    • 里面都是集合类的创建,如Collect和Map,也不是




最后,只能走到了newUnsafeAllocator()

  private <T> ObjectConstructor<T> newUnsafeAllocator(final Class<? super T> rawType) {
if (useJdkUnsafe) {
return new ObjectConstructor<T>() {
@Override public T construct() {
try {
@SuppressWarnings("unchecked")
T newInstance = (T) UnsafeAllocator.INSTANCE.newInstance(rawType);
return newInstance;
} catch (Exception e) {
throw new RuntimeException(("Unable to create instance of " + rawType + ". "
+ "Registering an InstanceCreator or a TypeAdapter for this type, or adding a no-args "
+ "constructor may fix this problem."), e);
}
}
};
} else {
final String exceptionMessage = "Unable to create instance of " + rawType + "; usage of JDK Unsafe "
+ "is disabled. Registering an InstanceCreator or a TypeAdapter for this type, adding a no-args "
+ "constructor, or enabling usage of JDK Unsafe may fix this problem.";
return new ObjectConstructor<T>() {
@Override public T construct() {
throw new JsonIOException(exceptionMessage);
}
};
}
}

缘由揭晓


方法内部调用了UnsafeAllocator.INSTANCE.newInstance(rawType);
我手动尝试了一下可以创建出对应的实例,而且和通常的构造函数创建出来的实例有所区别


image.png
很明显,summary的委托属性是null的,说明该方法是不走构造函数来创建的,里面的实现是通过Unsafe类的allocateInstance来直接创建对应ClassName的实例。


解决方案


看到这便已经知道缘由了,那如何解决这个问题?


方案一


回到上面的Book反编译后的java代码,可以看到只要调用了构造函数即可,所以添加一个默认的无参构造函数便是一个可行的方案。改动如下:

class Book(
val id: Int = 0,
val name: String? = null
) {
val summary by lazy { id.toString() + name }
}

或者手动加一个无参构造函数

class Book(
val id: Int,
val name: String?
) {
constructor() : this(0, null)

val summary by lazy { id.toString() + name }
}

而且要特别注意一定要提供默认的无参构造函数,不然通过newUnsafeAllocator创建的实例就导致kotlin的空安全机制就完全失效了


方案二


用moshi吧,用一个对kotlin支持比较好的json解析库即可。


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

两个Kotlin优化小技巧,你绝对用的上

数据对象data object的支持 @Repeatable注解的优化 接下来就带大家介绍下上面三个特性。 一. 数据对象data object的支持 该特性由kotlin1.7.20插件版本提供,并处于实验阶段。 这个特性主要是和原来的object声明的...
继续阅读 »
  1. 数据对象data object的支持

  2. @Repeatable注解的优化


接下来就带大家介绍下上面三个特性。


一. 数据对象data object的支持


该特性由kotlin1.7.20插件版本提供,并处于实验阶段。



这个特性主要是和原来的object声明的单例类的toString()方法输出有关,在了解这个特性之前,我们先看下下面一个例子:

object Single1

fun main() {
println(Single1)
}

输出:



这个输出本质上就是一个类名、@、地址的拼接,有时候你想要打印输出的仅仅是类名,就得需要重写下toString()方法:

object Single1 {

override fun toString(): String {
return "Single1"
}
}

然后再看一个密封类的例子:

sealed interface Response {

data class Success(val response: String): Response

data class Fail(val error: String): Response

object Loading : Response

}

fun main() {
println(Response.Success("{code: 200}"))
println(Response.Fail("no net"))
println(Response.Loading)
}

输出:



可以看到,大家都是密封子类,但就这个Loading类的输出比较"丑陋",没有上面两个兄弟类的输出简洁清爽。


接下来我们就要介绍下主人公数据对象data object了,这个东西其实使用起来和object一模一样,核心的区别就是前者的toString() 更加简洁。


接下来从一个例子一探究竟:

data object Single2

fun main() {
println(Single2)
}

看下输出:



输出是不是比上面的object Single1更加简单明了。最重要的是在密封类中使用效果更加,我们把上面密封类Loading声明为data object

    data object Loading : Response

看下最终的输出结果:



这下子输出结果是不是清爽更多!!


讲完了应用,我们再java的角度看下其背后的实现机制,相比较于objectdata object会多了下面这三个重写方法:

public final class Single2 {

@NotNull
public String toString() {
return "Single2";
}

public int hashCode() {
return -535782198;
}

public boolean equals(@Nullable Object var1) {
if (this != var1) {
if (!(var1 instanceof Single2)) {
return false;
}

Single2 var2 = (Single2)var1;
}

return true;
}
}

我们需要关心的toString()方法就是直接重写返回了当前的类名。


如果想要使用这个特性,我们只需要增加如下配置即可:

compileKotlin.kotlinOptions {
languageVersion = "1.9"
}

二. @Repeatable注解优化


该特性由kotlin1.6.0插件版本提供优化。



在了解这个特性之前,我们先回忆下@Repeatable这个注解在java中的使用:


如果一个注解在某个方法、类等等上面需要重复使用,那就需要@Repeatable帮助。



  • 首先定义需要重复使用的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(Fruits.class)
public @interface Fruit {
String name();
String color();
}


  • 然后定义注解容器,用来指定可重复使用的注解类型
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Fruits {
Fruit[] value();
}

然后就可以在代码中这样使用:

@Fruits({
@Fruit(name = "apple", color = "red"),
@Fruit(name = "origin", color = "yellow"),
})
public class Detail {
}

大家有没有发现,可重复注解定义起来还是由一丢丢的麻烦,接下来轮到我们kotlin重磅出击了。先看下面一个例子:

@Repeatable 
annotation class Animal(val name: String)

在kotlin中我们只要声明一个需要重复使用的注解即可,kotlin编译器会自动帮助我们生成注解容器@Animal.Container,然后我们就能在代码中这样使用:

@Animal(name = "dog")
@Animal(name = "horse")
public class Detail {
}

是不是非常简单便捷了。


如果你偏要显示指明一个包含注解,也可以,通过以下方式即可实现:

@JvmRepeatable(Animals::class)
annotation class Animal(val name: String)

annotation class Animals(val value: Array<Animal>)

然后除了上面的使用方式,你在kotlin中还可以这样使用:

@Animals([Animal(name = "dog"), Animal(name = "dog")])
class Detail {
}

请注意:



  1. 如果非要显示声明一个注解容器,其属性的名称一定要为value

  2. 其次,注解容器和可重复性直接不能同时声明在同一个元素上;


另外,其实这个特性kotlin早就支持了,只不过kotlin1.6.0插件版本之前,kotlin这个特性只只支持RetentionPolicy.SOURCE生命周期的注解,并且还和java的可重复注解不兼容。


总结


这两个小技巧相信在大家日常开发中还是比较实用的,希望本篇能对你有所帮助。


参考文章:


Improved string representations for singletons and sealed class hierarchies with data objects


Repeatable annotations with runtime retention for 1.8 JVM target


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

你不常用的 FileReader 能干什么?

web
前言 欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群! 本文灵感源于上周小伙伴遇到一个问题: "一个本该返回 Blob 类型的下载接口,却返回了 JSon 类型的内容!!!" 这会有什么问题呢? 按原逻辑就是调用该接口后,就会一股脑...
继续阅读 »

前言



欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!



本文灵感源于上周小伙伴遇到一个问题:


"一个本该返回 Blob 类型的下载接口,却返回了 JSon 类型的内容!!!"


1C306E8E.jpg


这会有什么问题呢?


按原逻辑就是调用该接口后,就会一股脑把该接口接返回过来的内容,直接经过 Blob 对象 转换后再通过隐藏的 <a> 标签实现下载。


但是有一个问题,那就是接口也是需要进行各种逻辑处理、判断等等,然后再决定是给前端响应一个正常的 Blob 格式的文件流,还是返回相应 JSon 格式的异常信息 等等。


如果返回了 JSon 格式的异常信息,那前端应该给用户展示信息内容,而不是将其作为下载的内容!


1C3802FC.gif


FileReader 实现 Blob 从 String 到 JSON


复现问题


为了更直观看到对应的效果,我们这里来简单模拟一下前后端的交互过程吧!


前端


由于小伙伴发送请求时使用的是 Axios,并且设置了其对应的 responsetype:blob | arraybuffer,所以这里我们也使用 Axios 即可,具体如下:


    // 发起请求
const request = () => {
axios({
method: 'get',
url: 'http://127.0.0.1:3000',
responseType: 'arraybuffer'
})
.then((res) => {

// 转换为 bloc 对象
const blob = new Blob([res.data])

// 获取导出文件名,decodeURIComponent为中文解码方法
const fileName = decodeURIComponent(res.headers["content-disposition"].split("filename=")[1])

// 通过a标签进行下载
let downloadElement = document.createElement('a');
let href = window.URL.createObjectURL(blob);
downloadElement.href = href;
downloadElement.download = fileName;
document.body.appendChild(downloadElement);
downloadElement.click();
document.body.removeChild(downloadElement);
window.URL.revokeObjectURL(href);
});
}

后端


这里我们就简单通过 koa 来实现将一个表格文件响应给前端,具体如下:


    const xlsx = require("node-xlsx");

const Koa = require("koa");
const app = new Koa();

const cors = require("koa2-cors");

// 处理跨域
app.use(
cors({
origin: "*", // 允许来自指定域名请求
maxAge: 5, // 本次预检请求的有效期,单位为秒
methods: ["GET", "POST"], // 所允许的 HTTP 请求方法
credentials: true, // 是否允许发送 Cookie
})
);

// 响应
app.use(async (ctx) => {
// 文件名字
const filename = "人员信息";

// 数据
const data = [
{ name: "赵", age: 16 },
{ name: "钱", age: 20 },
{ name: "孙", age: 17 },
{ name: "李", age: 19 },
{ name: "吴", age: 18 },
];

// 表格样式
const oprions = {
"!cols": [{ wch: 24 }, { wch: 20 }, { wch: 100 }, { wch: 20 }, { wch: 10 }],
};

// JSON -> Buffer
const buffer = JSONToBuffer(data, oprions);

// 设置 content-type
ctx.set("Content-Type", "application/vnd.openxmlformats");

// 设置文件名,中文必须用 encodeURIComponent 包裹,否则会报异常
ctx.set(
"Content-Disposition",
"attachment; filename=" + encodeURIComponent(filename) + ".xlsx"
);

// 文件必须设置该请求头,否则前端拿不到 Content-Disposition 响应头信息
ctx.set("Access-Control-Expose-Headers", "Content-Disposition");

// 将 buffer 返回给前端
ctx.body = buffer;
});

// 将数据转成 Buffer
const JSONToBuffer = (data, options = {}) => {
let xlsxObj = [
{
name: "sheet",
data: [],
},
];

data.forEach((item, idx) => {
// 处理 excel 表头
if (idx === 0) {
xlsxObj[0].data.push(Object.keys(item));
}

// 处理其他 excel 数据
xlsxObj[0].data.push(Object.values(item));
});

// 返回 buffer 对象
return xlsx.build(xlsxObj, options);
};

// 启动服务
app.listen(3000);

正常效果展示


1.gif


异常效果展示


可以看到当返回的内容为 JSON 格式 的内容时,原本逻辑在获取 filename 处就发生异常了,即使这一块没有发生异常,被正常下载下来也是不对的,因为这种情况应该要进行提示。


1.gif


并且此时直接去访问 res.data 得到的也不是一个 JSON 格式 的内容,而是一个 ArrayBuffer


image.png


返回的明明是 JSON ,但是拿到的却是 ArrayBuffer?


responseType 惹的祸


还记得我们在通过 Axios 去发起请求时设置的 responseType:'arraybuffer' 吗?


没错,就是因为这个配置的问题,它会把得到的结果给转成设置的类型,所以看起是一个 JSON 数据,但实际上拿到的是 Arraybuffer



这个 responseType 实际上就是 XMLHttpRequest.responseType,可点击该链接自行查看。



不设置 responseType 行不行?


那么既然是这个配置的问题,那么我们不设置不就好了!


确实可行,如下是未设置 responseType 获取到的结果:


image.png


但也不行,如果不设置 responseType 或者设置的类型不对,那么在 正常情况 下(即 文件被下载)时 会导致文件格式被损坏,无法正常打开,如下:


image.png


FileReader 来救场


实际上还有个比较直接的解决方案,那就是把接收到的 Arraybuffer 转成 JSON 格式不就行了吗?


1CB04D6B.jpg


没错,我们只需要通过 FileReader 来完成这一步即可,请看如下示例:


// json -> blob
const obj = { hello: "world" };

const blob = new Blob([JSON.stringify(obj, null, 2)], {
type: "application/json",
});

console.log(blob) // Blob {size: 22, type: 'application/json'}

// blob -> json
const reader = new FileReader()

reader.onload = () => {
console.log(JSON.parse(reader.result)) // { hello: "world" }
}

reader.readAsText(blob, 'utf-8')

是不是很简单啊!


值得注意的是,并不是任何时候都需要转成 JSON 数据,就像并不是任何时候都要下载一样,我们需要判断什么时候该走下载逻辑,什么时候该走转换成 JSON 数据。


怎么判断当前是该下载?还是该转成 JSON?


这个还是比较简单的,换个说法就是判断当前返回的是不是文件流,下面列举较常见的两种方式。


根据 filename 判断


正常情况来讲,再返回文件流的同时会在 Content-Disposition 响应头中添加和 filename 相关的信息,换句话说,如果当前没有返回 filename 相关的内容,那么就可以将其当做异常情况,此时就应该走转 JSON 的逻辑。


不过需要注意,有时候后端返回的某些文件流并不会设置 filename 的值,此时虽然符合异常情况,但是实际上返回的是一个正常的文件流,因此不太推荐这种方式


208EA3E8.gif


根据 Content-Type 判断


这种方式更合理,毕竟后端无论是返回 文件流 或是 JSON 格式的内容,其响应头中对应的 Content-Type,必然不同,这里的判断更简单,我们直接判断其是不是 JSON 类型即可。


更改后的代码,如下:


axios({
method: 'get',
url: 'http://127.0.0.1:3000',
responseType: 'arraybuffer'
})
.then(({headers, data}) => {
console.log("FileReader 处理前:", data)

const IsJson = headers['content-type'].indexOf('application/json') > -1;

if(IsJson){
const reader = new FileReader()

// readAsText 只接收 blob 类型,因此这里需要先将 arraybuffer 变成 blob
// 若后端直接返回的就是 blob 类型,则直接使用即可
reader.readAsText(new Blob([data], {type: 'application/json'}), 'utf-8')

reader.onload = () => {
// 将字符内容转为 JSON 格式
console.log("FileReader 处理后:", JSON.parse(reader.result))
}
return
}

// 下载逻辑
download(data)
});

值得注意的是,readAsText 只接收 blob 类型,因此这里需要先将 arraybuffer 变成 blob,若后端直接返回的就是 blob 类型,则直接使用即可。


image.png


FileReader 还能干什么?


以上是使用 FileReader 解决一个实际问题的例子,那么除此之外它还有什么应用场景呢?


不过我们还是先来了解一下 FileReader 的一些相关内容吧!!!


FileReader 是什么?


FileReader 对象允许 Web 应用程序 异步读取 存储在用户计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。


不过还要注意如下两条规则:



  • FileReader 仅用于以安全的方式从用户(远程)系统读取文件内容,它不能用于从文件系统中按路径名简单地读取文件

  • 要在 JavaScript 中按路径名读取文件,应使用标准 Ajax 解决方案进行 服务器端文件读取


总结起来就是,FileReader 只能读取 FileBlob 类型的文件内容,并且不能直接按路径的方式读取文件,如果需要以路径方式读取,最好要通过 服务端 返回流的形式。


四种读取方式


FileReader 可以如下四种方式读取目标文件:




  • FileReader.readAsArrayBuffer()



    • 开始读取指定的 Blob中的内容,读取完成后,result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象




  • FileReader.readAsBinaryString() (非标准



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含所读取文件的 原始二进制数据




  • FileReader.readAsDataURL()



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含一个 data: URL 格式的 Base64 字符串以表示所读取文件的内容




  • FileReader.readAsText()



    • 开始读取指定的Blob中的内容,读取完成后,result 属性中将包含一个 字符串 以表示所读取的文件内容




如上对应的方法命名十分符合顾名思义的特点,因此可以很容易看出来在不同场景下应该选择什么方法,并且如上方法一般都会配合 FileReader.onload 事件FileReader.result 属性 一起使用。


FileReader 的其他应用场景


预览本地文件


通常情况下,前端选择了相应的本地文件(图片、音/视频 等)后,需要通过接口发送到服务端,接着服务端在返回一个相应的预览地址,前端在实现支持预览的操作。


如果说现在有一个需要省略掉中间过程的需求,那么你就可以通过 FileReader.readAsDataURL() 方法来实现,但是要考虑文件大小带来转换时间快慢的问题。


这一部分比较简单,就不贴代码占篇幅了,效果如下:


1.gif


传输二进制格式数据


通常在上传文件时,前端直接将接收到的 File 对象以 FormData 发送给后端,但如果后端需要的是二进制的数据内容怎么办?


此时我们就可以使用 FileReader.readAsArrayBuffer() 来配合,为啥不用 FileReader.readAsBinaryString(),因为它是非标准的,而且 ArrayBuffer 也是原始的 二进制数据


具体代码如下:


// 文件变化
const fileChange = (e: any) => {
const file = e.target.files[0]
const reader = new FileReader()
reader.readAsArrayBuffer(file)

reader.onload = () => {
upload(reader.result, 'http://xxx')
}
}

// 上传
const upload = (binary, url) => {
var xhr = new XMLHttpRequest();
xhr.open("POST", url);
xhr.overrideMimeType("application/octet-stream");

//直接发送二进制数据
xhr.send(binary);

// 监听变化
xhr.onreadystatechange = function (e) {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// 响应成功
}
}
}
}

最后



欢迎关注同名公众号《熊的猫》,文章会同步更新,也可快速加入前端交流群!



上面我们通过 FileReader 解决了一个实际问题,同时也简单介绍了其相应的使用场景,但这个场景具体是否是用于你的需求还要具体分析,不能盲目使用。


以上就是本文的全部内容了,希望本文对你有所帮助!!!


21E0754A.jpg

收起阅读 »

数组去重你想到几种办法呢?

web
前言 你是否在面试的过程中被考到过给你一个数组让你去掉重复项呢?当时你的脑海里除了用Set实现之外,你还与面试官讲了什么去重的方法呢?你能否封装来一个可复用的数组去重api呢?依稀记得当时我被问到这个问题的时候,我也没回答出很多种解决办法。那下面我来总结一下对...
继续阅读 »

前言


你是否在面试的过程中被考到过给你一个数组让你去掉重复项呢?当时你的脑海里除了用Set实现之外,你还与面试官讲了什么去重的方法呢?你能否封装来一个可复用的数组去重api呢?依稀记得当时我被问到这个问题的时候,我也没回答出很多种解决办法。那下面我来总结一下对于数组去重这道简单的面试题时,我们可以回答的方法有什么吧。


数组去重


1. 不使用数组API方法


首先我来介绍一种不是用数组身上的API的去重解法,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = []
for(let i = 0; i < array.length; i++){
for( var j = 0; j < res.length; j++){
if(array[i] === res[j]){
break;
}
}
if(j === res.length){
res.push(array[i])
}
}
return res
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]


既然不使用数组自带的API方法,那我们首先考虑的就是用双重for循环了,如上述代码:



  1. 我们准备了一个空的结果数组

  2. 我们对需要去重的数组进行循环

  3. 在第一层数据中再套一层循环,根据下标判断结果数组内是否有重复项。


我们调用该方法,打印结构如上述代码的注解处,成功的实现了对数组的去重。


2. 使用 indexOf


既然有不使用数组API的,那就肯定有使用数组API的,下面看我使用indexOf完成数组的去重,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = []
for (let i = 0; i < array.length; i++) {
if (res.indexOf(array[i]) === -1) { // 返回找到的第一个值得下标
res.push(array[i])
}
}
return res
}
console.log(unique(array))// [ '1', 1, '2', 2 ]


如上述代码, 我们巧妙了使用了indexOf查找结果数组中是否已经存在,如果不存在才向结果数组中添加,实现了数组去重。


在上述代码的基础上,我们还可以转变一下,将for循环内的语句改为


if (array.indexOf((array[i])) == array.lastIndexOf(array[i])) {
i++
} else {
array.splice(array.lastIndexOf(array[i]), 1)
}

不新增其他变量,直接通过indexOf和lastIndexOf判断该值是否在原数组内为唯一值,从而直接修改原数组,实现数组的去重。


3. 使用 sort


对于数组去重,我们除了通过下标找出是否有重复项之外,我们还可以先排序,然后在判断前后项是否相同来实现去重,代码如下:


var  array = [1, 3, 5, 4, 2, 1, 2, 4, 4, 4]
function unique(array) {
let res = []
let sortedArray = array.concat().sort() //concat() 返回新的数组
let seen;
for (let i = 0; i < sortedArray.length; i++) {
if (!i || seen !== sortedArray[i]) {
res.push(sortedArray[i])
}
seen = sortedArray[i]
}
return res
}
console.log(unique(array)); // [ 1, 2, 3, 4, 5 ]

如上述代码, 我们先获取一个排好序的新数组,再对新数组进行循环,判断保存前一个值的seen与当前值是否相同来实现数组去重。


温馨小提示: 由于数组的排序方法不能区分数组和字符串,所以想要使用此方法必须要保证数组的值的类型相同,不然会出bug


4. 使用 filter


既然都用到了sort排序了,那我直接抬出ES6数组新增的filter过滤器API也不过分吧,代码如下:


var array = ['1', 1, '1', '1', '2', 2]
function unique(array) {
let res = array.filter((item, index, array) => {
return array.indexOf(item) === index
})
return res
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]

如上述代码,filter直接使用array.indexOf(item) === index作为过滤条件返回出一个新的数组,实现数组去重。


如上述代码,我们结合了 indexOf方法作为过滤条件,那我们也可以结合一下sort方法吧,直接使用一行代码就解决了数组的去重。代码如下:


function unique(array) {
return array.concat().sort().filter((item, index, array) => !index || item !== array[item - 1])
}
console.log(unique(array)); // [ '1', 1, '2', 2 ]

5. 使用Set、Map、或者对象


除了上述的通过数组API和不使用数组API的方法外,我们还能想到的就是借助对象来实现数组的去重。使用Set数据结构是我们最容易想到的办法,使用Map与对象方法的相似,都是以数组的值作为key,再将所有的可以取出来组成一个数组。 我就不给小伙伴们演示代码了,感兴趣的小伙伴可以自己动手试试。


(对于对象的key只能为字符串这个问题,我们可以换个思路,将下标存为key,值存为value,判断不同key的值相不相同来实现数组去重。我们还可以在存key时加上其类型,然后进行一次转换。)


自己封装一个去重API


在介绍上述数组去重的方法后,我们再来总结一下,将其融合成一个有复用性,而且还可以适用不同情况的API方法。


我来介绍一下如下我封装的一个数组去重的API方法,



  1. 该方法可接受三个参数,第一个参数为需要去重的数组,第二个参数为该数组是否为排好序的数组,第三个参数为一个回调函数

  2. 该回调函数也有三个参数,分别为值,下标,需要去重数组。该回调函数的作用是方便用户对数组进行一些额外的处理(例如将大写转为小写)

  3. 第二,三参数可不传递。


var array = [1, 2, '1', 'a', 'A', 2, 1]
var array2 = [1, 1, '1', 2, 2]
function uniq(array, isSorted, iteratee) {
let seen = []
let res = []
for(let i = 0; i < array.length; i++){
let computed = iteratee ? iteratee(array[i], i,array) : array[i]
if(isSorted) {
if(!i || seen !== array[i]){
res.push(array[i])
}
seen = array[i]
}else if(iteratee) {
if(seen.indexOf(computed) === -1){
seen.push(computed)
res.push(computed)
}
}
else {
if(res.indexOf(array[i]) === -1) {
res.push(array[i])
}
}
}
return res
}
let result = uniq(array, false, function(item, index, arr){
return typeof item == 'string' ? item.toLowerCase() : item
})
console.log(result); // [ 1, 2, '1', 'a' ]
console.log(uniq(array2, true)); // [ 1, 2 ]

总结


对于数组的去重,当我们能在面试中说到这个多方法的话,这道面试题也就过了,虽然这道面试不难,但如果我们想要想到这个多方法的话,还是

作者:潘小七
来源:juejin.cn/post/7248835844659970105
需要许多知识储备的。

收起阅读 »

Kotlin1.8新增特性,进来了解一下

大家好,之前我已经写过了分析kotlin1.5、1.6、1.7、1.9插件版本新增的一些特性,唯独kotlin1.8的特性还没好好讲讲,本篇文章就带大家好好分析下kotlin1.8新增了那些特性,能对我们日常开发带来哪些帮助。 其中Kotlin1.8.0提供的...
继续阅读 »

大家好,之前我已经写过了分析kotlin1.5、1.6、1.7、1.9插件版本新增的一些特性,唯独kotlin1.8的特性还没好好讲讲,本篇文章就带大家好好分析下kotlin1.8新增了那些特性,能对我们日常开发带来哪些帮助。


其中Kotlin1.8.0提供的特性有限,本篇文章主要是分析Kotlin1.8.20提供的一些新特性。下面是支持该插件的IDE对应版本:



一. 提供性能更好的Enum.entries替代Enum.values()



在之前,如果我们想遍历枚举内部元素,我们通常会写出以下代码:


enum class Color(val colorName: String, val rgb: String) {
RED("Red", "#FF0000"),
ORANGE("Orange", "#FF7F00"),
YELLOW("Yellow", "#FFFF00")
}

fun main() {
Color.values().forEach {
println("${it.rgb}--${it.colorName}--${it.name}")
}
}

但是不知道大家是否清楚,Color.values() 其实存在性能问题,换句话说,每调用一次该方法,就会触发重新分配一块内存,如果调用的频率过高,就很可能引发内存抖动


我们可以反编译下枚举类简单看下原因:



Color.values()每次都会调用Object.clone()方法重新创建一个新的数组,这就是上面说的潜在的性能问题,github上也有相关的问题链接,感兴趣的可以看下:HttpStatus.resolve allocates HttpStatus.values() once per invocation


同时Color.values()返回的是一个数组,而在我们大多开发场景中,可能集合使用的频率更高,这就可能涉及到一个数组转集合的操作。


基于以上考虑,Kotlin1.8.20官方提供了一个新的属性:Color.entries这个方法会预分配一块内存并返回一个不可变集合,多次调用也不会产生潜在的性能问题


我们简单看下使用:


fun main() {
Color.entries.forEach {
println("${it.rgb}--${it.colorName}--${it.name}")
}
}

输出:



同时我们也可以从反编译的代码中看出区别:



不会每次调用都重新分配一块内存并返回。


如果想要使用这个特性,可以加上下面配置:


compileKotlin.kotlinOptions {
languageVersion = "1.9"
}

另外多说一下,IntelliJ IDEA 2023.1版本也会检测代码中是否存在Enum.values()的使用,存在就提示使用Enum.entries代替。


二. 允许内联类声明次级构造函数



内联类在Kotlin1.8.20之前是不允许带body的次级构造函数存在的,也就是说下面的代码运行会报错:


@JvmInline
value class Person( val fullName: String) {
constructor(name: String, lastName: String) : this("$name $lastName") {
check(lastName.isNotBlank()) {
"Last name shouldn't be empty"
}
}
}

fun main() {
println(Person("a", "b").fullName)
}

运行看下结果:



如果没有次级构造函数body,下面这样写是没问题的:


    constructor(name: String, lastName: String) : this("$name $lastName") 

如果想要支持带body的次级构造函数,只需要在kotlin1.8.20插件版本上和上一个特性一样增加languageVersion = "1.9"配置即可。


然后上面的代码块运行就没问题了,我们看下输出:


fun main() {
println(Person("a", "").fullName)
}


准确的执行了次级构造函数body内的逻辑。


三. 支持java synthethic属性引用



这个特性用文字不好解释,我们直接通过代码去学习下该特性。


当前存在一个类Person1


public class Person1 {
private String name;
private int age;

public Person1(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}
}

在Kotlin1.8.20之前,以下这种写法是会报错的:



而是必须改成sortedBy(Person1::getAge)才能运行通过。


和上面特性一样,如果想要支持Person1::age这种引用方式,只需要在kotlin1.8.20插件版本上和上一个特性一样增加languageVersion = "2.1"配置即可。



PS:请注意,Kotlin官方网站提示配置languageVersion = "1.9" 就能使用上面的实验特性,但是编译器还是提示报错,然后你找报错提示信息改成了languageVersion = "2.1" 就正常了。




四. 新Kotlin K2编译器的更新



就是说目前Kotlin K2编译器还是一个实验阶段,不过Kotlin官方在其stable的路上又增加了一些更新:



  1. 序列化插件的预览版本;

  2. JS IR编译器的alpha支持;

  3. Kotlin2.0版本特性的引入;


如果大家想要体验下最新版的Kotlin K2编译器,增加配置:languageVersion ="2.0"即可。


五. Kotlin标准库支持AutoCloseable



这个AutoCloseable 接口就是用来支持资源关闭的,搭配提供的use扩展函数,就能帮助我们在资源流使用完毕后自动关闭。


Kotlin之所以在标准库中支持,应该是想要支持多平台吧。


六. Kotlin标准库支持Base64编解码


这里不做太多介绍,看下面的使用例子即可:



七. Kotlin标准库@Volatile支持Kotlin/Native


@Volatile注解在Kotlin/JVM就是保证线程之间可见性以及有序性的,kotlin官方在Kotlin/Native中也支持了该注解使用,有兴趣的可以实战试下效果。


总结


本篇文章主要是介绍了Kotlin1.8版本新增的一些特性,主要挑了一些我能理解的、常用的一些特性拉出来介绍,希望能对你有所帮助。


历史文章


两个Kotlin优化小技巧,你绝对用的上


浅析一下:kotlin委托背后的实现机制


Kotlin1.9.0-Beta,它来了!!


聊聊Kotlin1.7.0版本提供的一些特性


聊聊kotlin1.5和1.6版本提供的一些新特性


kotlin密封sealed class/interface的迭代之旅


优化@BuilderInference注解,Kotlin高版本下了这些“毒手”!


@JvmDefaultWithCompatibility优化小技巧

,了解一下~

收起阅读 »

在高德地图实现卷帘效果

web
介绍 今天介绍一个非常简单的入门级小案例,就是地图的卷帘效果实现,各大地图引擎供应商都有相关示例,很奇怪高德居然没有,我看了下文档发现其实也是可以简单实现的,演示代码放到文末。本文用到了图层掩模,即图层遮罩,让图层只在指定范围内显示。 实现思路 1.创建目标图...
继续阅读 »

介绍


今天介绍一个非常简单的入门级小案例,就是地图的卷帘效果实现,各大地图引擎供应商都有相关示例,很奇怪高德居然没有,我看了下文档发现其实也是可以简单实现的,演示代码放到文末。本文用到了图层掩模,即图层遮罩,让图层只在指定范围内显示。


实现思路


1.创建目标图层,这里除了有一个默认的底图,还增加了卫星影像图和路网图层,后两者是可以被掩模的。因此在创建图层时通过设置rejectMapMask(默认值false)让图层是否允许被掩模。


2.提供实时设置掩模的方法renderMask,核心代码只需要map.setMask(mask)。


3.实现拖拽交互逻辑,监听拖拽过程,实时触发 renderMask


实现代码


1.创建目标图层


// 基础底图
const baseLayer = new AMap.TileLayer({
zIndex: 1,
//拒绝被掩模
rejectMapMask: true,
})

map = new AMap.Map('container', {
center:[116.472804,39.995725],
viewMode:'3D',
labelzIndex:130,
zoom: 5,
cursor:'pointer',
layers:[
// 底图,不掩模
baseLayer,
// 路网图层
new AMap.TileLayer.RoadNet({
zIndex:7
}),
// 卫星影像图层
new AMap.TileLayer.Satellite()
]
});

2.提供实时设置掩模的方法


function renderMask(){
// 当前地图范围
const {northEast, southWest} = map.getBounds()
// 地理横向跨度
const width = northEast.lng - southWest.lng
// 拖拽条位置占比例
const dom = document.querySelector('#dragBar')
const ratio = Math.ceil(parseInt(dom.style.left) + 5) / map.getSize().width

let mask = [[
[northEast.lng, northEast.lat],
[southWest.lng+ width * ratio, northEast.lat],
[southWest.lng+ width * ratio, southWest.lat],
[northEast.lng, southWest.lat]
]]

map.setMask(mask)
}

3.实现拖拽交互逻辑


// 拖拽交互
function initDrag(){

const dom = document.querySelector('#dragBar')
dom.style.left = `${map.getSize().width/2}px`

// const position = {x:0, y:0}
interact('#dragBar').draggable({
listeners: {
start (event) {
// console.log(event.type, event.target)
},
move (event) {
// 改变拖拽条位置
const left = parseFloat(dom.style.left)
const targetLeft = Math.min(Math.max(left + event.dx, 0), map.getSize().width - 10)
dom.style.left = `${targetLeft}px`

if(event.dx !== 0){
renderMask()
//必须!强制地图重新渲染
map.render()
}
},
end(event){
// console.log(event.type, event.target)
}
}
})
}


  1. 启动相关方法,完善交互逻辑


initDrag()
renderMask()
map.on('mapmove', renderMask)
map.on('zoomchange', renderMask)
window.addEventListener('resize', renderMask)

相关链接


本文代码演示


jsfiddle.net/gyratesky/z…


maptalks 图层卷帘效果


maptalks.org/examples/cn…


卫星+区域掩模


lbs.amap.com/demo/j

avasc…

收起阅读 »

2023一只前端菜鸡的年中总结

关于一只前端菜鸡陈平安的年中总结 第一次写笔记还是多多少少有点紧张的 其实说实话总是感觉自己还在2022年 一转眼2023也过了一半了 不多bb 直接开说 平安的前半年的魔幻经历 裁员 虽然知道今年互联网行业工作不是特别好找 但是我当时也没想到裁员会跟我有关...
继续阅读 »

关于一只前端菜鸡陈平安的年中总结


第一次写笔记还是多多少少有点紧张的
其实说实话总是感觉自己还在2022年 一转眼2023也过了一半了 不多bb 直接开说 平安的前半年的魔幻经历


微信图片_20230626103447.jpg


裁员


虽然知道今年互联网行业工作不是特别好找 但是我当时也没想到裁员会跟我有关 时间还要回到今年5月
22日,当时的平安刚从老家回到公司(当时因为家里人出了点事情 所以请了几天假回了趟老家)大概就是在24日的时候,平安还在工位上安安心心的当码农码代码呢 然后就看见我对面的后端大哥(他说自己是李小白 那我们这里姑且也叫他小白大哥)被叫人事叫出去,当时平安还没想那么多 以为就是单纯的让搬个东西之类的(平安和小白大哥经常干的事情,毕竟大部分同事都是女生嘛)就在平安认真工作的时候技术负责人(其他板块的,因为我们公司就一个前端一个后端 没有技术负责人,让我去交接工作 我当时心里暗道不妙 会不会是平安经常摸鱼被谁告诉老板了(开个玩笑)然后平安正打算开始准备交接,小白大哥回来了,然后平安就被人事叫过去谈话了,巴拉巴拉一大堆(就是公司不做互联网这一块儿了 所以不需要技术人员了),然后就是喜提裁员offer一份,交接工作差不多进行到25号才彻底交接完。


放松


其实被裁当时没啥感觉,当时想的就是变相的给自己放个小长假,然后当时跟小白大哥久违的吃了顿饭(其实也是第一次)然后在吃饭附近的河边和小白大哥一起敞开心扉的聊天散步,顺便附上当时拍的照片(平安也是喜欢记录生活热爱生活的前端菜鸟)


微信图片_20230626105940.jpg


微信图片_20230626105947.jpg


微信图片_20230626105953.jpg


微信图片_20230626105959.jpg


投简历 面试


之后几天也就跟大家想的一样,在boss直骗上投简历、约面试 短短几天大概约了四五家面试,然后就是去面试 不面还好,面完之后平安都快怀疑人生了,其中有一家面的不错 也在约二面 可惜面完之后就没有消息了 所谓的二面也可能是当时的说辞吧,然后就是现在平安待的这家公司(当时面试的时候状态不太好,面试过程的话不太那啥 你们懂的)面完之后我对现在这家公司已经不抱太大希望了 当时面试心态出了点问题就想着摆烂一段时间再继续面试吧 然而在我刚开始摆的第二天,突然看见boss上有一条消息发过来,点开一看是之前面试的这家公司,老板说我的面试通过了 然后要加我vx说发一下offer和入职需要的材料(只能说平安的运气还是不错的 哈哈哈)


庆祝 准备入职


当时收到offer的时候,那种开心的喜悦,当天晚上也不想睡觉了 就直接叫小白大哥去海边(当时已经晚上10点多了) 附上聊天截图 哈哈哈


微信图片_20230626113622.jpg


微信图片_20230626113645.jpg


微信图片_20230626113648.jpg
然后就是去找小白大哥 一起去青岛的三浴(该说不说青岛的三浴风景还是不错的 就是大半夜视线不太好)


微信图片_20230626113925.jpg


微信图片_20230626113935.jpg


微信图片_20230626113942.jpg


微信图片_20230626113946.jpg


微信图片_20230626113957.jpg


微信图片_20230626114002.jpg


微信图片_20230626114007.jpg


入职


6月5日平安在现在这家公司入职了 总得来说这周是在公司的第四个周 马上就快一个月了(时间过的真快啊) 公司人都挺好的 出了问题问的话也都会跟自己讲 总的来说 希望自己在这家公司ke


作者:前端菜鸟陈平安
来源:juejin.cn/post/7248812926176362554
收起阅读 »

正则别光想着抄,看懂用法下次你也会写

web
前言 大家好,我是 simple ,我的理想是利用科技手段来解决生活中遇到的各种问题。 日常开发中,应该很多人都经常会使用正则表达式去校验字符串。但是总是遇到复杂的表达式就从网上抄了就结束了,下次写还是不会,今天我们就来看两个稍微复杂一点的案例,从案例中学会一...
继续阅读 »

前言


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


日常开发中,应该很多人都经常会使用正则表达式去校验字符串。但是总是遇到复杂的表达式就从网上抄了就结束了,下次写还是不会,今天我们就来看两个稍微复杂一点的案例,从案例中学会一些高级的正则表达式用法。


校验字符串是否包含大小写字母+数字+特殊字符,并且长度为8-12。


如果想要使用单个正则表达式就解决上述问题,就需要稍微学习一下正则的一些高级用法了。


^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[a-zA-Z\d!@#$%^&*()_+]{8,12}$

先行断言(预搜索)


先行断言中不会获取任何内容,只是做一次筛查



  • 正向先行 指在某个位置向右看,该位置必须能匹配该表达式(?=表达式)。

  • 反向先行 指在某个位置往右看,该位置保证不能出现的表达式。

  • 正向后行 指在某个位置向左看,该位置必须能匹配该表达式,但不会获取表达式的内容(?<=表达式)

  • 反向后行 指在某个位置往左看,该位置保证不能出现的表达式(?<!表达式)


这个正则表达式使用了正向先行断言来同时检查字符串中是否包含大小写字母、数字和特殊符号。它的含义如下:



  • ^:匹配字符串的开头。

  • (?=.*[a-z]):正向先行断言,要求字符串中至少包含一个小写字母。

  • (?=.*[A-Z]):正向先行断言,要求字符串中至少包含一个大写字母。

  • (?=.*\d):正向先行断言,要求字符串中至少包含一个数字。

  • (?=.*[!@#$%^&*()_+]):正向先行断言,要求字符串中至少包含一个特殊符号(这里列出了一些常见的特殊符号,你可以根据需要添加或修改)。

  • [a-zA-Z\d!@#$%^&*()_+]:匹配允许的字符集合,包括大小写字母、数字和特殊符号。

  • {8,12}:限定字符串的长度在 8 到 12 位之间。

  • $:匹配字符串的结尾。


使用这个正则表达式可以对目标字符串进行检查,判断是否满足包含大小写、数字和特殊符号,并且长度为 8 到 12 位的要求。例如:


let regex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*()_+])[a-zA-Z\d!@#$%^&*()_+]{8,12}$/;
let str = "Password123!";
let isMatch = regex.test(str);
console.log(isMatch); // 输出: true

获取ip地址


当处理日志文件时,有时需要从日志文本中提取特定的信息。一个常见的场景是提取日志中的 IP 地址。


假设我们有一个日志文件,其中包含了多行日志记录,每行记录的格式如下:


[2023-06-26 10:15:25] [INFO] Access from IP: 192.168.0.1 to URL: /home

在上述示例中,我们使用 match 方法来执行正则表达式匹配,并将匹配的结果存储在 match 变量中。如果有匹配结果,我们可以从数组中取得第一个元素 match[0],即提取到的 IP 地址。


let logText = "[2023-06-26 10:15:25] [INFO] Access from IP: 192.168.0.1 to URL: /home";
let regex = /\b(?:\d{1,3}\.){3}\d{1,3}\b/;
let match = logText.match(regex);
if (match) {
let ipAddress = match[0];
console.log(ipAddress); // 输出: 192.168.0.1
} else {
console.log("No IP address found.");
}

非捕获型分组


非捕获型分组是正则表达式中的一种分组语法,用于对一组子表达式进行逻辑组合,但不会捕获匹配的结果。它以 (?: 开始,并以 ) 结束。


/\b(?:\d{1,3}\.){3}\d{1,3}\b/

解释一下这个正则表达式:



  • \b:单词边界,用于确保 IP 地址被完整匹配。

  • (?:\d{1,3}\.){3}:非捕获型分组,匹配由 1 到 3 个数字和一个点组成的序列,重复匹配 3 次,用于匹配 IP 地址的前三个数字和点的部分。

  • \d{1,3}:匹配由 1 到 3 个数字组成的序列,用于匹配 IP 地址的最后一个数字。

  • \b:单词边界,用于确保 IP 地址被完整
    作者:simple_lau
    来源:juejin.cn/post/7248832185808617509
    匹配。

收起阅读 »

阿里巴巴高管换血,吴永明接替张勇

🩸 阿里巴巴高管换血,吴永明接替张勇 Alibaba announced that Eddie Yongming Wu would replace Daniel Zhang as chief executive in September. Mr. Zhang...
继续阅读 »

🩸 阿里巴巴高管换血,吴永明接替张勇


Eddie Yongming Wu & Joe Tsai


Alibaba announced that Eddie Yongming Wu would replace Daniel Zhang as chief executive in September. Mr. Zhang will retain control of the cloud division. Mr. Wu is one of the founders of the Chinese internet giant and is a close friend of Jack Ma, another founder, as is Joe Tsai, who takes over Mr. Zhang’s other role as chairman. In March the company announced plans to turn itself into a holding company overseeing six divisions.


阿里巴巴宣布吴永明(Eddie Yongming Wu)将在 9 月份接替张勇(Daniel Zhange)成为首席执行官。张勇将保留对云事业部的控制权。吴永明是中国互联网巨头阿里巴巴的创始人之一,也是马云的好朋友,另外一个创始人,蔡崇信(Joe Tsai),将会接替张勇的另外一个董事长职位。 在三月份,阿里巴巴宣布计划将自身转化为管理六个事业部的控股公司。




阿里巴巴今年 3 月份的重组计划是?

阿里巴巴在今年3月份宣布了一项重大的重组计划。该公司计划将其业务拆分为六个独立的业务单元,每个单元都将由自己的首席执行官和董事会负责管理。这六个新的业务单元包括:云智能集团、淘宝天猫商业集团、本地服务集团、菜鸟智能物流、全球数字商务集团,以及数字媒体和娱乐集团。

阿里巴巴表示,这次重组的目的是为了提高公司的敏捷性,加快决策速度,以及更快地应对市场变化。此外,除了淘宝天猫商业集团将完全由阿里巴巴所有外,其他五个新的业务组都将有可能寻求外部资金,并有可能进行自己的首次公开募股(IPO)。




🌄 孙正义再出山


Son Masayoshi


Son Masayoshi used his first public appearance in seven months to talk up SoftBank’s investments in artificial intelligence. Displaying his typical exuberance, the boss of the Japanese tech conglomerate told shareholders that “a huge revolution is coming” in AI and that SoftBank would “rule the world” in its development. After a period of reflection following heavy losses at its flagship Vision Funds, Mr. Son said he now wants to become “an architect for the future of humanity”.


孙正义(Son Masayoshi)在七个月来首次公开露面时,大肆宣扬软银在人工智能方面的投资,这家日本科技集团的老板告诉股东,人工智能领域“一场巨大的革命即将到来”,软银将在其发展过程中“统治世界”。在旗舰愿景基金遭受重大损失后,经过一段时间的反思,孙正义表示,他现在想成为“人类未来的建筑师”。



  • exuberance /ɪɡˈzuːbərəns/
    源自拉丁词 "exuberare",意为 "充溢"。在英语中,"exuberance" 通常用来描述非常活泼、充满活力、兴奋或者情绪高昂的状态。
    当用于描述人时,"exuberance" 可以指一个人充满活力,情绪高涨,非常乐观和充满热情。例如,"a child's exuberance"(一个孩子的活力)或者 "his exuberance was infectious"(他的热情感染了所有人)。




软银是一家什么公司?

软银集团(SoftBank Group Corp.)是一家总部位于日本的全球领先的科技公司,由孙正义于 1981 年创立。起初,软银主要从事电脑软件销售和发布,而现在,软银已经扩展到各种不同的科技领域,包括电信、互联网服务、人工智能和机器人技术等。

软银最为人所知的可能是其投资活动。它设立了一系列的投资基金,最有名的就是其愿景基金,这是全球最大的科技领域私募股权基金,投资了包括阿里巴巴、滴滴、今日头条、饿了吗、贝壳找房、Uber、Slack 等在内的许多知名科技公司。






孙正义是谁?

孙正义(Masayoshi Son)是一位杰出的商业领袖和投资者,他是软银集团的创始人、董事长和首席执行官。他在科技投资领域有着重要影响力,并以他的大胆投资和对未来的远见而闻名。

孙正义于 1957 年出生在日本九州的一个朝鲜族移民家庭。他在青少年时期就展现出了对企业家精神的热情和对科技的兴趣。他在 17 岁时移居到美国,并在加利福尼亚大学伯克利分校学习计算机科学和经济学。

1981 年,孙正义创立了软银,起初是一个软件分销公司。然而,他很快看到了互联网可能带来的巨大机会,于是转向了投资领域。在 90 年代,他进行了一系列投资,其中最有影响力的就是对阿里巴巴的早期投资,使软银从阿里巴巴的 IPO 中获得了巨大收益。

在他的个人生活中,孙正义是一位已婚人士,他与他的妻子有两个孩子。他是世界上最富有的人之一,他的财富主要来源于他在软银和其投资的公司中的股权。




🐿️ 英特尔加码德国!投资330亿美元建两座芯片工厂!


Intel


Intel doubled the amount it is investing in two new chip-making factories in Germany to $33bn after the German government agreed to increase subsidies for the project to €10bn ($11bn). It is the largest-ever foreign investment in Germany and the biggest bid by a European country to enter the chip war, following America’s enticement of chipmakers.


在德国政府同意增加到 100 亿欧元(约合 110 亿美元)补贴后,英特尔将在德国的的两座新芯片制造工厂投资翻倍。这是德国有史以来最大的外国投资,也是欧洲国家进入芯片战争的最大努力,相应美国吸引芯片制造商的橘洲。



  • subsidy /ˈsʌbsədi/
    在经济和政策语境中通常被翻译为“补贴”。补贴是政府给予企业或个人的一种经济援助,目的是为了鼓励特定的经济活动或保护特定的行业。补贴可以以各种形式出现,例如税收优惠、低息贷款、直接现金支付等。补贴可以帮助降低生产成本,使得企业能够以低于市场价的价格生产商品或提供服务,或者鼓励企业进行某种特定的活动,例如研发、环保改造等。

  • bid /bɪd/
    在上文中,"bid" 的意思是尝试或竞争。这里的 "biggest bid" 指的是欧洲国家在尝试进入芯片战争方面的最大努力。
    在其他上下文中,"bid" 可能有不同的含义。例如,在商业环境中,"bid" 可以指出价或竞标,即在拍卖或合同竞标中提出的价格。在桥牌或其他一些卡牌游戏中,"bid" 可以指出牌或叫牌。在股票市场中,"bid" 可以指买方愿意支付的价格。

  • enticement /ɪnˈtaɪsmənt/
    指吸引或诱惑某人做某事的行为或方法。这个词通常带有积极的含义,描述的是通过提供奖励或激励来吸引某人做某事。在上文中,“America’s enticement of chipmakers” 可以理解为美国通过各种方式(例如税收优惠、补贴、优惠政策等)吸引芯片制造商的行为。




英特尔是一家什么公司?

英特尔(Intel)是一家总部位于美国加利福尼亚州圣克拉拉的跨国科技公司,成立于 1968 年。该公司是全球最大的半导体芯片设计和制造公司之一,以及计算机技术领域的领导者之一。

英特尔的产品范围包括中央处理器(CPU)、芯片组、网络处理器、闪存存储设备、以太网产品、无线通信产品等。其中,英特尔的处理器产品在全球范围内广泛应用于个人电脑、服务器、笔记本电脑、移动设备等领域。它们以高性能、低功耗、高可靠性和安全性著称,并且在业界有着广泛的应用和支持。

除了芯片设计和制造,英特尔还在计算机技术领域进行了广泛的研究和开发,包括人工智能、物联网、高性能计算、自动驾驶技术等。此外,英特尔还积极开展社会责任活动,致力于推动数字包容、可持续性和 STEM 教育等方面的发展。

作为一家跨国公司,英特尔在全球范围内设有多个研发中心、工厂和办事处,拥有来自各个国家和地区的员工。




🌊 亚马逊被指控强加 Prime 服务


Amazon


America’s Federal Trade Commission sued Amazon for allegedly enrolling customers into its Prime service without their consent and for making it hard for them to cancel.


美国联邦贸易委员会起诉亚马逊未经用户同意就擅自将用户注册到它的 Prime 服务中,并且让用户很难去取消这项服务。




Prime 服务是什么?

Prime服务是亚马逊提供的订阅服务,主要涵盖了快速免费配送、免费电子书借阅、电影和电视节目的流媒体服务、以及其他一些特权。Prime服务需要用户每年支付一定的订阅费用,不同国家和地区的价格可能会有所不同。





  • allegedly /əˈledʒɪdli/ 指某件事情尚未被证实或确定,仅仅是根据某些证据或指称所做的推测或猜测。因此,当我们说某事“allegedly”发生时,我们并不肯定它是否真的发生了,但是有足够的理由相信它发生了。在法律文书或新闻报道中,"allegedly"常被用来描述涉嫌犯罪的人或行为,以表明尚未被证实的情况。


✈️ 印度航空市场成长速度惊人,IndiGo 下单 500 架空客客机


IndiGo


IndiGo, a low­cost Indian airline, placed an order for 500 Airbus A320 passenger jets, the biggest ever deal in commercial aviation. India has become the fastest­-growing aviation market and IndiGo is the biggest domestic player.


IndiGo,一家印度廉价航空公司,订购 500 架空客 A320 乘客飞机,这是商业航空史上最大的交易。印度已经成为增长最快的航空市场,并且 IndiaGo 是国内市场最大的参与者。



  • place an order 是一个商业用语,指某个企业或个人向供应商或生产商提交了一份正式的订单,表示要购买其产品或服务。在这个语境中,IndiGo 向空客订购了 500 架客机,意味着他们已经向空客提交了一份正式的订单,表示要购买这些客机。

  • aviation /ˌeɪviˈeɪʃn/
    指航空,包括所有与飞行相关的事物。它涵盖了航空器设计、制造、维护和操作等方面的知识和技术。航空业是一个广泛的领域,包括商业航空、军事航空、私人航空、航空运输和通用航空等。




IndiGo 是印度一家知名的低成本航空公司,总部位于新德里。它成立于 2006 年,是印度最大的航空公司之一,也是全球最大的低成本航空公司之一。IndiGo 的航班网络覆盖印度国内以及亚洲、中东和东南亚等地区,运营着数百个航班航班,包括许多国内和国际航点。

IndiGo 的机队主要由空客 A320 系列飞机组成,这些飞机被广泛认为是最先进、最经济的单通道客机之一。IndiGo 以其高效的运营和优质的服务而闻名,旅客可以享受到舒适的座椅、免费的餐饮和便捷的登机体验。

IndiGo 在过去几年中一直保持着强劲的增长势头,成为了印度航空市场的领导者之一。它也曾多次获得国内和国际航空业的奖项和认可,包括 Skytrax 颁发的“印度最佳低成本航空公司”等奖项。




👑 丹麦再夺全球竞争力榜冠军


Annual World Competitiveness ranking


Denmark retained the top spot in the annual World Competitiveness ranking from the International Institute for Management Development (IMD). The criteria behind the ranking include international trade, government and business efficiency, and technological infrastructure. Ireland leaped from 11th place to 2nd while Britain tumbled from 23rd to 29th, dragged down by worsening productivity and rising bureaucracy.


丹麦在国际管理世界研究所的年度世界竞争力排行榜中位列第一。排名背后的标准包含国际贸易、政府和商业效率以及科技基础设施。爱尔兰从第 11 名跃升至第 2 名,同时英国从第 23 名跌至 29 名,受累于变糟糕的生产力和抬头的官僚主义。



  • top spot 英语中的一个习惯用语,意思是 "第一名" 或 "最高位置"。




年度世界竞争力排行榜(Annual World Competitiveness Ranking)是什么?

年度世界竞争力排行榜是由瑞士洛桑国际管理发展学院(IMD)发布的一份报告,旨在评估全球经济体的竞争力。IMD的竞争力排名采用了一系列指标,包括国际贸易、劳动力市场、财政政策、企业效率、技术基础设施、教育、健康、环境和创新等多个方面。

这些指标通常被认为是衡量一个国家经济实力和潜力的重要因素。通过对这些因素进行分析和比较,IMD的排名可以帮助企业和政府了解全球经济环境的趋势和变化,并制定相应的战略和政策。




🍀 国际能源署新报告:清洁能源投资需大幅增加,中国等新兴市场成为投资热点


The International Energy Agency


The International Energy Agency published a report on clean energy in developing and emerging­market economies. It said that investments in those countries would have to rise from $770bn a year to between $2.2trn and $2.8trn by the early 2030s if the Paris Agreement’s goals on carbon emissions are to be met. China accounts for two-­thirds of the current spending on clean energy, with Brazil and India taking a large bite of the rest.


国际能源署发布了一份关于发展中和新兴市场经济体中的清洁能源的报告。报告指出,如果要实现巴黎协议关于碳排放的目标,那么这些国家的投资必须从每年 7700 亿美元增加到 2030 年初的 2.2 至 2.8 万亿美元。中国占据了目前清洁能源投资的三分之二,巴西和印度占据了其余的大部分。


🤒 英国通胀率创近 31 年新高


Britain


Britain’s headline annual rate of inflation remained elevated at 8.7% in May, defying expectations that it would fall again. Core inflation, which strips out food and energy prices, rose to 7.1%, the highest it has been since March 1992. The government has promised to cut inflation by half this year from the 10.1% registered in January, but higher prices persist in food, clothing, recreation, health, communications, and travel.


在五月,英国的年度通胀率仍保持高位,达到 8.7%,违背了通胀率会再次下降的预期。剔除食物和能源价格的核心通胀率,升至 7.1%, 这是从 1997 年三月以来的最高水平。政府承诺今年将会把从一月份的 10.1% 的通胀率降至一半,但食物、服装、娱乐、健康、通信和旅行的价格仍然居高不下。




  • defy /dɪˈfaɪ/
    意思是“违抗;不服从;挑战”。在这个句子中,defy 的意思是“违反了预期”,也就是说,英国的年通胀率没有像预期那样下降,而是保持在一个较高的水平。在这种情况下,defy 表示通胀率的表现与人们的预期相反,形成了一种挑战或违反预期的局面。




  • headline 指的是新闻报道或文章的标题。通常情况下,headline 是用来吸引读者的注意力,传达文章或新闻的主要内容或亮点。在这个句子中,headline annual rate of inflation 指的是通胀率的年度平均值,是该新闻报道或文章的主要话题。




💰 最后


最新的文章会发在公众号「

作者:吴楷鹏
来源:juejin.cn/post/7248819906917122085
楷鹏」,欢迎关注 🤩

收起阅读 »

从张鑫旭大佬文章中发现了我前端知识的匮乏

web
最近翻看张鑫旭大佬的博客,发现了一篇叫《前端原生API实现条形码二维码的JS解析识别》的文章,觉得很不错,于是就把大佬的代码拷贝下来学习了下,结果就是看的我一脸懵,自信息大大受打击了。痛定思痛,于是把其中觉得有意思的地方记录下,整理成此文。 我们先看下页面是怎...
继续阅读 »

最近翻看张鑫旭大佬的博客,发现了一篇叫《前端原生API实现条形码二维码的JS解析识别》的文章,觉得很不错,于是就把大佬的代码拷贝下来学习了下,结果就是看的我一脸懵,自信息大大受打击了。痛定思痛,于是把其中觉得有意思的地方记录下,整理成此文。


我们先看下页面是怎么样的:


chrome-capture-2023-5-26.gif


功能很简单,就是复制下面的二维码图片,然后粘贴到文本框中,最后点击识别按钮,把识别二维码的结果展示到下面。


源代码:


<!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>qrcode</title>
<style>
.area {
height: 200px;
border: 1px dashed skyblue;
background-color: #fff;
display: grid;
place-items: center;
margin-top: 20px;
}
.area:focus {
border-style: solid;
}
.area:empty::before {
content: '或粘贴图片到这里';
color: gray;
}
.button {
margin: 1rem auto;
width: 160px;
height: 40px;
font-size: 112.5%;
background-color: #eb4646;
color: #fff;
border: 0;
border-radius: 0.25rem;
margin-top: 1.5rem;
}
</style>
</head>
<body>
<div class="container">
<input id="file" class="file" type="file" accept="image/png" />
<div id="area" class="area" tabindex="-1"></div>
</div>
<p align="center">
<button id="button" class="button">识别</button>
</p>

<p id="result" align="center"></p>

<p align="center">
方便大家复制的示意图:<br /><img
src="./qrcode.png"
style="margin-top: 10px"
/>

</p>

<script>
var reader = new FileReader()
reader.onload = function (event) {
area.innerHTML = '<img src="' + event.target.result + '">'
}
document.addEventListener('paste', function (event) {
var items = event.clipboardData && event.clipboardData.items
var file = null
if (items && items.length) {
// 检索剪切板items
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
break
}
}
}
// 此时file就是剪切板中的图片文件
if (file) {
reader.readAsDataURL(file)
}
})

file.addEventListener('change', function (event) {
const file = event.target.files && event.target.files[0]
if (file) {
reader.readAsDataURL(file)
}
})

button.addEventListener('click', function () {
if ('BarcodeDetector' in window) {
// 创建检测器
const barcodeDetector = new BarcodeDetector({
formats: ['qr_code']
})

const eleImg = document.querySelector('#area img')
if (eleImg) {
barcodeDetector
.detect(eleImg)
.then(barcodes => {
console.log('barcodes', barcodes)
barcodes.forEach(barcode => {
result.innerHTML = `<span class="success">解析成功,结果是:</span>${barcode.rawValue}`
})
})
.catch(err => {
result.innerHTML = `<span class="error">解析出错:${err}</span>`
})
} else {
result.innerHTML = `<span class="error">请先粘贴二维码图片</span>`
}
} else {
result.innerHTML = `<span class="error">当前浏览器不支持二维码识别</span>`
}
})
</script>
</body>
</html>

背景交代完成,现在就一点一点的来分析其中代码的精妙之处。


CSS部分


tabindex = -1


<div id="area" class="area" tabindex="-1"></div>

当我看到tabindex这个属性时,完全不知道它的用法,于是我继续在张鑫旭大佬的博客中搜索,找到一篇叫《HTML tabindex属性与web网页键盘无障碍访问》的文章,这里简要说下这个属性的用法和作用。


tabindex属性是一个全局属性,也就是所有 HTML 标签都可以用的属性,比方说idclass属性等。所以,可以在div上使用。同时,这个属性是一个非常老的属性,没有兼容性问题,放心使用。


tabindex属性是一个与键盘访问行为息息相关的属性。平常可能感觉不到它的价值,但是一旦我们的鼠标坏掉了或者没电了,我们就只能使用键盘。亦或者在电视机上,或者投影设备上访问我们的网页的时候,我们只能使用遥控器。就算设备都完全正常,对于资深用户而言,键盘访问可以大大提高我们的使用效率。


当一个元素设置tabindex属性值为-1的时候,元素会变得focusable,所谓focusable指的是元素可以被鼠标或者JS focus,在 Chrome 浏览器下表现为会有outline发光效果,IE浏览器下是虚框,同时能够响应focus事件。默认的focusable元素有<a>, <area>, <button>, <input>, <object>, <select> 以及 <textarea>


但是,tabindex = -1不能被键盘的tab键进行focus。这种鼠标可以focus,但是键盘却不能focus的状态,只要tabindex属性值为负值就可以了。


因此,我们可以设置divfocus的样式,当鼠标点击div时,我们可以改变它的边框,如下:


.area:focus {
border-style: solid;
}

tabindex属性值是一个整数,它来决定被tabfocus的顺序,顺序越小越先被focus,但是 0除外,如下divfocus的顺序依次是:1,2,3。


<div id="area" class="area" tabindex="1"></div>
<div class="area" tabindex="3"></div>
<div class="area" tabindex="2"></div>

tabindex="0"又是怎么回事呢?


元素设置tabindex="-1",可以鼠标和JS可以focus,但键盘不能focus


tabindex="0"tabindex="-1"的唯一区别就是键盘也能focus,但是被focus的顺序是最后的。或者你可以这么理解,<div>设置了tabindex="0",从键盘访问的角度来讲,相对于<div>元素变成了<button>元素。


垂直居中


垂直居中是一个常用的需求了,我经常使用flex来完成:


display: flex;
align-items: center;
justify-content: center;

在大佬的文章中使用了一个新的用法:


display: grid;
place-items: center;

place-items 属性是以下属性的简写:align-itemsjustify-items


:empty::before


div元素没有内容时,.area:empty样式会生效,同时为了显示一段提示内容,使用了伪元素::before,在content写入提示内容。


.area:empty::before {
content: '或粘贴图片到这里';
color: gray;
}

JS部分


copy paste 事件


document.addEventListener('paste', function (event) {
var items = event.clipboardData && event.clipboardData.items
var file = null
if (items && items.length) {
// 检索剪切板items
for (var i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
file = items[i].getAsFile()
break
}
}
}
// 此时file就是剪切板中的图片文件
if (file) {
reader.readAsDataURL(file)
}
})

这两个事件都属于ClipboardEvent事件(剪切板事件),还有一个cut剪切事件。


wrap.oncopy = function(event){}
wrap.oncut = function(event){}
wrap.onpaste = function(event) {}

任何软件上的内容,可以被复制粘贴,是因为软件对操作系统复制粘贴操作的实现,软件都会把复制剪切的内容存入操作系统的剪切板上。同样,浏览器也对操作系统的剪切板进行了实现,属于浏览器的自身的实现。


浏览器复制操作的默认行为是触发浏览器的 copy 事件,将 copy 的内容存入操作系统的剪切板中。


那如何干预浏览器的这种默认的复制粘贴操作呢?


可以通过event.preventDefault阻止事件的默认行为,即当触发这三个事件时,阻止对系统剪切板的数据操作。然后,我们对数据进行加工后,重新写入到剪贴板。


比如,当用户复制我们网站的内容时,可以在数据后面加一个版权的相关信息。


<div id="wrap">这是复制的复制内容</div>
<script>
var wrap = document.getElementById('wrap')
wrap.oncopy = function (event) {
// 通过copy事件监听,阻止将选中内容复制到系统剪切板上
event.preventDefault()
// 获取选中内容对象
const selection = document.getSelection()
// selection对象重构了toSring()方法,获取selection对象的选中内容
var selectContent = selection.toString()
var dealContent =
selectContent +
'转载请联系作者,内容地址:xxxxx'
// 把重写后的内容写入到剪贴板
event.clipboardData.setData('text/plain', dealContent)
}
</script>


ClipboardEvent 事件有个最重要的属性clipboardData,该属性值是DataTransfer对象,这个对象在拖拽场景中经常使用,后面会专门写一篇文章来说说这个对象。


new BarcodeDetector解析二维码


// 创建检测器
const barcodeDetector = new BarcodeDetector({
formats: ['qr_code']
})
barcodeDetector.detect(eleImg)
.then(barcodes => {
console.log('barcodes', barcodes)
barcodes.forEach(barcode => {
result.innerHTML = `<span class="success">解析成功,结果是:</span>${barcode.rawValue}`
})
})
.catch(err => {
result.innerHTML = `<span class="error">解析出错:${err}</span>`
})

浏览器提供了原生的API来解析二维码和条形码,即 Barcode Detection API


formats表示要解析那种码,如下图所示:


image.png


总结


通过学习上面的代码,可以发现自己在 css,js 方面上的不足,原因是缺乏探索性,老是用已有的知识来解决问题,或者直接去 github 上找第三方库,其实可以使

作者:小p
来源:juejin.cn/post/7248874230862233655
用最简单的方式实现。

收起阅读 »

借点钱来“救急”【多图】

背景 为什么来谈借钱这个话题呢? 是因为博主刚刚结束了追债的过程,很不愉快,而且追债跨度很长。而欠款人是自己的大学同学,这里我为了不侵犯 TA 的姓名权,称呼其为张三。 本文为了纪录整个过程,以及关于借钱话题的反思。欢迎读者留言,表达关于自己借钱的看法。 张三...
继续阅读 »

背景


为什么来谈借钱这个话题呢?


是因为博主刚刚结束了追债的过程,很不愉快,而且追债跨度很长。而欠款人是自己的大学同学,这里我为了不侵犯 TA 的姓名权,称呼其为张三


本文为了纪录整个过程,以及关于借钱话题的反思。欢迎读者留言,表达关于自己借钱的看法。


张三事件


我有一个习惯,借钱给别人或者我借别人的钱,我会有记录:记录借钱的日期,借钱的金额和归还的日期。 下面是借钱给张三的清单:


序号借钱的日期借钱的金额理应归还的日期实际归还
12019-11-27¥2200.00过两天还2020-11-29 还
22019-12-01¥1000.00下个月还2020-01-07 还
32019-12-03¥2500.00下个月还2020-01-07 还 ¥1000.00,2021-06-29 还 ¥800.00,2022-02-20 还款 ¥100【失信】
42019-12-14¥3500.00下个月还【严重失信】
52019-12-29¥2000.00下个月还【严重失信】
62020-01-17¥200.00下个月还【严重失信】
72020-04-07¥900.00(没借)--

在张三向我借第六次钱 - ¥200.00 的时候,我隐约感觉不对劲。但是他的理由是:项目款没下发,借 ¥200.00 当项目款的凑数,月底项目款到了转我。这个理由没毛病,疫情刚刚爆发,行情突然不好,可以理解;再加上大学玩得好的(一个小插曲:大学的时候,张三也借过我钱,不过都如约还我了 - 守信),所以我借给张三了。


2020-04-07.jpeg


但是,在 2020-04-07 日,张三第七次向我借款的时候,我直接明确拒绝,因为之前的钱逾期太严重了。可看下面 Deadline 系列图



中途因为延期太久,我直接按照银行大约的存款利息进行计算,不矫情了:约定2021年12月10日前一次性还清,带上利息。按照银行最低年利率1.7%, 算上一年的利息 6400 + 108 = 6508元。但是,最后的还款日期还是不能如期进行。很无语~



Deadline 一推再推:


timeline-01.jpeg


timeline-02.jpeg


timeline-03.jpeg


timeline-04.jpeg


timeline-05.jpeg


timeline-06.jpeg


timeline-07.jpeg


timeline-08.jpeg


timeline-09.jpeg


timeline-10.jpeg


timeline-11.jpeg


timeline_12.jpeg


timeline-13.jpeg


timeline-14.jpeg


timeline-15.jpeg


timeline-16.jpeg


timeline-17.jpeg


timeline__18.jpeg


timeline-19.jpeg


timeline-20.jpeg


在分期还款的过程也不是很顺利,总会或多或少延期几天半个月。而且还款的时候不主动,逾期了也不会主动跟你联系说原因,整个过程很不愉快。


好在~ 他终于按照一年期的时间还完给我了。这让我联想到了花呗、京东白条的分期付款牛逼🐮~


俗话说得好:借钱见人心,还钱见人品。 我不知道张三到底是做了什么,借了身边人不少人,而且惹得他哥和爸妈反感。自己也不想去了解,想便在之后人生漫漫路,自己应该和张三没有什么交集了~


延伸事件


也许读者会问,借钱给以前的同学而已,关系又不是很亲近。借给亲戚很安全的啦。


我再举个自家的真实例子:


俺妈在 2010 年左右借钱给她姐姐的儿子 两万八,暂且称他为李四


在借钱的一段时间内(2018年之前),这个 李四 过年都会过来给我妈 - 他亲姨 拜年。然后,在 2018 年的时候,我妈想着已经借钱过去给 李四 有七八年了,而且 李四 家里面已经盖起了新房子。我妈就跟她姐说,叫孩子也该还钱了,很久了。怎料,她姐来了句:



  • 她姐:没钱,才盖完楼;你怎么在我这个时候叫我还钱

  • 我妈:现在我也不是很富裕,啊弟借了这么多年了。你跟啊弟商量商量?



啊弟 -> 李四



双方挂了电话,过了几个小时~



  • 李四:我妈跟我讲,你叫她还钱

  • 我妈:我叫她跟你提下,不是叫她还,是叫她跟你商量,毕竟你借的

  • 李四:边度有钱,刚建楼,生意今年又不好做

  • 我妈:我这边也要用钱的,你多少还点?

  • 李四:我看你是妒忌我家建了楼吧,都说没有了...


我妈一听到 妒忌我家建了楼,气不打一处来,我要是嫉妒你,我还会借钱给你。你自己几斤几两你不知道吗。跌定心要他全部还钱...


期间还聊了什么话题我也不是很清楚。因为我妈是用方言跟娘家沟通的,我不是很懂该方言。总之,电话后,我发现我妈眼眶都红红的。


这还不是最过分的。在不久后,我妈回娘家探亲。竟然被那边的流言蜚语气哭回来了:她哥:你为什么管姐要回两万八。而且啊弟说,都还了钱给你了,打你很多次电话你都不接,你大牌吗?以后你不要回来~


完全不赞成,我妈根本不是不接电话,而是因为她忙。而且李四打我妈电话的时候,手机是放在家里面的,被我接到了:



  • 李四:我转了,你看到账没?

  • 我:我是*

  • 李四:你妈呢,叫她接电话

  • 我:在外面干活,不方便

  • 李四:我转了两万八给你妈卡里面了,叫她看看到账没

  • 我:我知道了,待会跟她说下


我可以作证,我妈不是大牌,而是忙。丟,这些人脑袋和嘴巴怎么长的~


打脸.webp


2018 年后,李四 过年再也没来过我家拜年过,在某年的国庆节来过一次,不过我不在家~本来自己就对他没什么好感,现在是心生厌恶~


当然,还是有很多准守约定人的案例:比如我妈借钱给她另外一个姐的女儿,人家就主动还款,并且每过年会来看看她的姨姨;比如我借钱给另外一个同学,TA 也很主动如约还款...


该不该借


所以,来到本文的最重点的内容了:钱,我们该不该借出去呢?


该借:



  • 对方的人品你了解

  • 对方的经济实力你清楚

  • 俗话说:借急不借穷

  • 不能影响自己的生活


不借:



  • 对方人品差,好吃懒做的人不要搭理

  • 对方信用差,比如在周围人眼里口碑不行

  • 对方跟你不常联系,一上来就管你借钱

  • 借了钱,不主动说还的人;不能再借第二次,及时止损

  • 不能被血缘关系影响你对一个人的判断,不好就是不好


pexels-karolina-grabowska-4968395.jpg

收起阅读 »

这一曲终落泉城,也始于泉城

22年的春季站在岔路口四顾茫然,灵魂里的信念仿佛已被层层迷雾遮蔽。与大多人一样,我开始了考研的复习,但是到暑假之前的种种原因,让我深刻认识到自己未来想走的路是什么,我坚信无论怎样未来还是会开启自己的代码人生。也许是冥冥之中的巧合,随意投了一份简历之后,见鬼了一...
继续阅读 »

22年的春季站在岔路口四顾茫然,灵魂里的信念仿佛已被层层迷雾遮蔽。与大多人一样,我开始了考研的复习,但是到暑假之前的种种原因,让我深刻认识到自己未来想走的路是什么,我坚信无论怎样未来还是会开启自己的代码人生。也许是冥冥之中的巧合,随意投了一份简历之后,见鬼了一样收到了一个offer,经过权衡我更加坚定了自己的选择提前入海。随后梁溪城(无锡)下开始了自己的第一份实习。


f4f07a90ecd1bfb57309dcc4b4cd243.jpg
仔细室下的春季


梁溪城内边余尺,姑苏城外不姑苏。


初出茅庐,满腔热血腾。初入社会的心态是充满着好奇和壮志满怀的,有点类似于刘姥姥进大观园的样子,但这也是大多数从农村走入社会该有的样子。


2158fb3d035ce57708e85a34b30fffc.jpg
梁溪城晴


然而实习的日子没过多久,就出现了疫情开始了居家办公的日子,居家办公的生活还挺好,哈哈,自己一直处于电脑从来没有开启的状态。但疫情下的生活也并不是很好,吃饭只能去吃盒饭,供应的关系失衡使得饭的质量以及价格都不尽人意,好在没过多久疫情就结束了,线下上班,开始接触微服务,此时此刻java狗也算是开始见识了传呼其神的微服务。

d59cf7d2e8dc4d2ab01833fc670cdb4.jpg
梁溪城雨


渴望改变的灵魂,安逸是禁锢。舒适的日子久了,总想着外面更广阔的天空,这也许是大多数青年的想法。由于长时间没有机会接触到实际的开发中,感觉天天浑浑噩噩,想着要改变的决心。第一份实习在草草的两个月结束,下一站钱塘(杭州)下看西湖。

7f0c0e7d6e1d1f29f8c735f54e15292.jpg
梁溪城黄昏


钱塘西湖波光粼,芜湖花街夜亮灯。


西湖悠悠水溢情,钱塘无情终相别。当一个人努力过后,最后没有得到自己想要的,会有少许的悲伤,微微一笑过去的终将过去。好在实习的最后一站遇到很多好的人,也算是不虚此行。


bfd71a4c180e34a9e3b8071e534ed1d.jpg
钱塘西湖黄昏


南行的最后一站,芜湖。偶然的巧合有机会去到芜湖,此次的经历也是让我想出行的心付出了行动,迈出了步伐(之前是一直很宅那种)。

961bea13150d5bc498a5634b8b80a7d.jpg
芜湖花街


白日炎炎游北平,夜幕潇潇离别行。


毕业最后一站,北京。这一站也是收获满满。现在也可以在别人面前zb,我可是见过北京城的人,虽然没有去到长城,我不是好汉,哈哈哈。


5079c035d17d5bb1e8d2c17153b8620.jpg
天坛晴


ef520aeac9d951a89abad90c78fe9d1.jpg
北京夜


曲终人落泉阳城,大明湖畔少年情。


毕业后的第一站,大明湖畔泉城。


08e656a0990b0f61557c4275163c6c8.jpg
大明湖夜


回首向来萧瑟处,归去,也无风雨也无晴。


回望过去一年,入世从懵懵懂懂到懵懂,个人的心态,观念都在改变。只要是自己想做的,认为可做的,毫不手软的作出选择,不再迷茫,不再畏手畏脚,同时也是一直坚信自己的选择。看待问题的角度,不能只看那些不好的一面,也要看到好的一面,这样的心境才会是乐观豁达。一个人真正的走向成熟的标志,就是他不愿意越来越多说,

作者:海贼梦想家
来源:juejin.cn/post/7247881750314451001
而是学会适当的闭嘴。

收起阅读 »

从 0 到 1 实现一个 Terminal 终端

web
前言 之前在我自己的项目中 打造属于你自己的 Mac(Next.js+Nest.js TS全栈项目)有同学问Terminal 组件是怎么实现的呢,现在我们就用 React+TS 写一个支持多种命令的 Terminal 终端吧。 每一步骤后都有对应的 com...
继续阅读 »

前言



之前在我自己的项目中 打造属于你自己的 Mac(Next.js+Nest.js TS全栈项目)有同学问Terminal 组件是怎么实现的呢,现在我们就用 React+TS 写一个支持多种命令的 Terminal 终端吧。



每一步骤后都有对应的 commit 记录;


源码地址:github.com/ljq0226/my-… 欢迎 Star ⭐️⭐️⭐️


体验地址: my-terminal.netlify.app/



搭建环境


我们使用 vite 构建项目,安装所需要的依赖库:



  • @neodrag/react (拖拽)

  • tailwindcss

  • lucide-react (图标)
    步骤:

  • pnpm create vite

  • 选择 React+TS 模版

  • 安装依赖:pnpm install @neodrag/react lucide-react && pnpm install -D tailwindcss postcss autoprefixer && npx tailwindcss init -p
    配置 tailwind.config.js:


/** @type {import('tailwindcss').Config} */
export default {
content: [
'./index.html',
'./src/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
},
plugins: [],
}


仓库代码:commit1


开发流程


搭建页面


image.png


以上是终端的静态页面,样式这里就不在详细展开了,此次代码仓库 commit2 。 接下来我们为该终端添加拖拽效果:


//App.tsx
···
import type { DragOptions } from '@neodrag/react'
import { useRef, useState } from 'react'

function APP(){
const [position, setPosition] = useState({ x: 0, y: 0 })
const draggableRef = useRef(null)
// 初始化 dragable 拖拽设置
const options: DragOptions = {
position,
onDrag: ({ offsetX, offsetY }) => setPosition({ x: offsetX, y: offsetY }),
bounds: { bottom: -500, top: 32, left: -600, right: -600 },
handle: '.window-header',
cancel: '.traffic-lights',
}
useDraggable(draggableRef, options)

}

return (
<div ref={draggableRef}> //将 draggableRef 挂在到节点上

</div>

)
···

这样我们的 Terminal 终端就有了拖拽效果,其它 API 方法在@neodrag/react 官网中,代码仓库 commit3


terminal2.gif


输入命令


一个终端最重要的当然是输入命令了,在这我们使用 input 框来收集收集输入命令的内容。
由于我们每次执行完一次命令之后,都会生成新的行,所以我们将新行封装成一个组件,Row 组件接收两个参数(id:当前 Row 的唯一标识;onkeydown:监听 input 框的操作):


// components.tsx
interface RowProps {
id: number
onkeydown: (e: React.KeyboardEvent<HTMLInputElement>) => void
}
const Row: React.FC<RowProps> = ({ id, onkeydown }) => {

return (
<div className='flex flex-col w-full h-12'>
<div>
<span className="mr-2 text-yellow-400">funnycoder</span>
<span className="mr-2 text-green-400">@macbook-pro</span>
<span className="mr-2 text-blue-400">~{dir}</span>
<span id={`terminal-currentDirectory-${id}`} className="mr-2 text-blue-400"></span>
</div>
<div className='flex'>
<span className="mr-2 text-pink-400">$</span>
<input
type="text"
id={`terminal-input-${id}`}
autoComplete="off"
autoFocus={true}
className="flex-1 px-1 text-white bg-transparent outline-none"
onKeyDown={onkeydown}
/>

</div>

</div>

)
}

一开始的时候,我们通过初始化一个 Row 进行操作,我们所有生成的 Row 通过


//app.tsx
const [content, setContent] = useState<JSX.Element[]>(
[<Row
id={0}
key={key()} // React 渲染列表时需要key
onkeydown={(e: React.KeyboardEvent<HTMLInputElement>
) => executeCommand(e, 0)}
/>,
])

content 变量来存储,在后续我们经常要修改 content 的值,为了简化代码我们为 setContent 封装成 generateRow 方法:


// 生成内容
const generateRow = (row: JSX.Element) => {
setContent(s => [...s, row])
}

问题来了,当我们获取到了输入的命令时,怎么执行对应的方法呢?


每一个 Row 组件都有 onKeyDown事件监听,当按下按键时就调用 executeCommand 方法,通过 input 框的 id 获取该 input 框 dom 节点, const [cmd, args] = input.value.trim().split(' ') 获取执行命令 cmd 和 参数 args,此时根据 event.key 按键操作执行对应的方法:


 // 执行方法
function executeCommand(event: React.KeyboardEvent<HTMLInputElement>, id: number) {
const input = document.querySelector(`#terminal-input-${id}`) as HTMLInputElement
const [cmd, args] = input.value.trim().split(' ')
if (event.key === 'ArrowUp')
alert(`ArrowUp,Command is ${cmd} Args is ${args}`)

else if (event.key === 'ArrowDown')
alert(`ArrowDown,Command is ${cmd} Args is ${args}`)

else if (event.key === 'Tab')
alert(`Tab,Command is ${cmd} Args is ${args}`)

else if (event.key === 'Enter')
alert(`Enter,Command is ${cmd} Args is ${args}`)
}

接下来我们测试一下,输入cd desktop,按下 Enter 键:
terminal3.gif


代码仓库 commit3


构建文件夹系统


终端的最常用的功能就是操作文件,所以我们需要构建一个文件夹系统,起初,在我的项目中使用的是一个数组嵌套,类似下面这种


image.png


这种数据结构的话,每次寻找子项的都需要递归计算,非常麻烦。在这我们采用 map 进行存储,将数据扁平化:


image.png


代码仓库 commit4


执行命令


准备工作


我们先介绍一下几个变量:



  • currentFolderId :当前文件夹的 id,默认为 0 也就是最顶层的文件夹

  • currentDirectory : 当前路径

  • currentId : input 输入框的 id 标识


  const [currentId, setCurrentId] = useState<number>(0)
const [currentFolderId, setCurrentFolderId] = useState(0)
const [currentDirectory, setCurrentDirectory] = useState<string>('')

并把一些静态组件封装在 components.tsx 文件中:


image.png


核心介绍


我们用一个对象来存储需要执行对应的方法:


  const commandList: CommandList = {
cat,
cd,
clear,
ls,
help,
mkdir,
touch,
}

executeCommand 方法中,如果用户按下的是'Enter' 键,我们首先判断下输入的 cmd 是否在 commandlist 中,如果存在,就直接执行该方法,如果不存在,就生成一个 CommandNotFound
行:


//app.js 
function executeCommand(){
//...
else if (event.key === 'Enter') {
// 将新输入 command 加入 commandHistory 中
const newArr = commandHistory
newArr.push(input.value.trim())
setCommandHistory(newArr)
// 如果输入 command 符合就执行 ⭐️⭐️⭐️
if (cmd && Object.keys(commandList).includes(cmd))
commandList[cmd](args)
else if (cmd !== '')
generateRow(<CommandNotFound key={key()} command={input.value.trim()} />)
// 每次无论 command 符不符合,都需要生成一行新的 Row,并且 curentId++
setCurrentId(id => id + 1)
setTimeout(() => {
generateRow(
<Row
key={key()}
id={commandHistory.length}
onkeydown={(e: React.KeyboardEvent<HTMLInputElement>
) => executeCommand(e, commandHistory.length)}
/>,
)
}, 100)
}
//...
}

help


当输入的 cmd 识别为'help'时就会调用该方法,生成在 components.tsx 里 Help()中定义好的静态数据:


  // help 命令
const help = () => {
generateRow(<Help key={key()} />)
}

代码仓库:commit5


cd


首先,默认的currentFolderId为 0,也就是指向我们的根文件夹,我们可以通过 folderSysteam.get(currentFolderId) 来获取当前文件夹下的信息,包括该文件夹的 title,子文件的 id 数组 childIds
当我们获取到了参数 arg 时,首先要判断 是否为空或者'..',若是的话,即返回上一层目录,
如果是正常参数的话,通过 folderSysteam.get(currentFolderId) 获取子目录的 childIds 数组,遍历当前目录下的子目录,找到子目录中 title 和 arg 一样的目录并返回该子目录 id,将 currentFolderId 设置为该子目录 id 并且拼接文件路径:


  // cd 命令
const cd = (arg = '') => {
const dir: string = localStorage.getItem(CURRENTDIRECTORY) as string
//判断是否返回上一层目录
if (!arg || arg === '..') {
// 处理文件路径
const dirArr = dir.split('/')
dirArr.length = Math.max(0, dirArr.length - 2)
//区分是否是root层
if (!dirArr.length)
setCurrentDirectory(`${dirArr.join('')}`)
else
setCurrentDirectory(`${dirArr.join('')}/`)
// 将当前目录设置为上一层目录
setCurrentFolderId(folderSysteam.get(`${currentFolderId}`)?.parentId as number)
return
}
//若是正常的跳转子目录
//根据 arg 参数获取需跳转目录的 id
const id = searchFile(arg)
// 如果子目录存在,设置路径、更新当前目录id
if (id) {
const res = `${dir + folderSysteam.get(`${id}`)?.title}/`
setCurrentFolderId(id)
setCurrentDirectory(res)
}
// 否则返回 NoSuchFileOrDirectory
else { generateRow(<NoSuchFileOrDirectory key={key()} command={arg}/>) }
}
const searchFile = (arg: string) => {
// 对输入做一个优化,例如文件夹名为 Desktop,只要我们输入'Desktop'|'desktop'|'DESKTOP'都行
const args = [arg, arg.toUpperCase(), arg.toLowerCase(), arg.charAt(0).toUpperCase() + arg.slice(1)]
// 获取当前目录下子目录
const childIds = getStorage(CURRENTCHILDIDS)
// 遍历子目录,找到title 为 arg 的目录
for (const item of folderSysteam.entries()) {
if (childIds.includes(item[1].id) && args.includes(item[1].title))
return item[1].id
}
}


ls


  // ls 命令
const ls = () => {
let res = ''
// 获取当前目录下所有子目录 id
const ids = getStorage(CURRENTCHILDIDS)
// 遍历 id 进行拼接
for (const id of ids)
res = `${res + folderSysteam.get(`${id}`)?.title} `
if (!res) {
generateRow(<div key={key()} >There are no other folders or files in the current directory.</div>)
}
else {
res.split(' ').map((item: string) =>
generateRow(<div key={key()} className={item.includes('.') ? 'text-blue-500' : ''}>{item}</div>),
)
}
}

terminal6.gif


代码仓库:commit6| commit6.1


mkdir、touch


创建文件或文件夹,我们只需要创建该文件或文件夹对象,新对象的 parentId 指向当前目录,其新 id 加入到当前目录的 childIds 数组中,最后再更新一下 folderSysteam 变量:


  // mkdir 命令
const mkdir = (arg = '') => {
const currentFolderId = getStorage(CURRENTFOLDERID)
const size = folderSysteam.size.toString()
// 创建新对象
const newFolderSysteam = folderSysteam.set(`${size}`, {
id: +size,
title: arg,
childIds: [],
parentId: currentFolderId,
})
// 更新 当前文件夹下的 childIds
const childIds = (folderSysteam.get(`${currentFolderId}`) as FolderSysteamType).childIds as number[]
childIds && childIds.push(+size)
setStorage(CURRENTCHILDIDS, childIds)
setFolderSysteam(newFolderSysteam)
}
// touch 命令
const touch = (arg = '') => {
const currentFolderId = getStorage(CURRENTFOLDERID)
const size = folderSysteam.size.toString()
// 创建新对象
const newFolderSysteam = folderSysteam.set(`${size}`, {
id: +size,
title: arg,
content: <div ><h1>
This is <span className='text-red-400 underline'>{arg}</span> file!
</h1>
<p>Imagine there's a lot of content here...</p>
</div>
,
parentId: currentFolderId,
})
// 更新 当前文件夹下的 childIds
const childIds = (folderSysteam.get(`${currentFolderId}`) as FolderSysteamType).childIds as number[]
childIds && childIds.push(+size)
setStorage(CURRENTCHILDIDS, childIds)
setFolderSysteam(newFolderSysteam)
}


terminal7.gif


代码仓库:commit7


cat、clear


cat 命令只需要展示子文件的 content 属性值即可:


  // cat 命令
const cat = (arg = '') => {
//获取当前目录下 childIds 进行遍历
const ids = getStorage(CURRENTCHILDIDS)
ids.map((id: number) => {
const item = folderSysteam.get(`${id}`) as FolderSysteamType
//生成 title 为 arg 文件的 content Row 行
return item.title === arg ? generateRow(<div key={key()}>{item.content}</div> as JSX.Element) : ''
})
}

clear 命令只需要调用 setContent():


  // clear 命令
const clear = () => {
setContent([])
//清空 input 框内容
const input = document.querySelector('#terminal-input-0') as HTMLInputElement
input.value = ''
}

terminal8.gif
代码仓库:commit8


其它操作


准备工作


我们先介绍一下几个变量:



  • commandHistory : 用于存储输入过的 command数组

  • changeCount : 用来切换 command 计数


  const [changeCount, setChangeCount] = useState<number>(0)
const [commandHistory, setCommandHistory] = useState<string[]>([])

上下键切换 command


上面定义的 changeCount 变量默认为 0,当我们按上🔼键时,changeCount-1,当我们按下🔽键时,changeCount+1。
而当 changeCount 变量变化时,获取当前 input dom 节点,设置其值为commandHistory[commandHistory.length + changeCount],这样我们的上下键切换 command 就实现了:


    // 当按下上下键时 获取历史 command
useEffect(() => {
const input = document.querySelector(`#terminal-input-${commandHistory.length}`) as HTMLInputElement
if (commandHistory.length)
input.value = commandHistory[commandHistory.length + changeCount]
if (!changeCount) {
input.value = ''
setChangeCount(0)
}
}, [changeCount])

// 按向上🔼键
function handleArrowUp() {
setChangeCount(prev => Math.max(prev - 1, -commandHistory.length))
}
// 按向下🔽键
function handleArrowDown() {
setChangeCount(prev => Math.min(prev + 1, 0))
}
// 执行方法
function executeCommand(...) {
//...
if (event.key === 'ArrowUp') {
handleArrowUp()
}
else if (event.key === 'ArrowDown') {
handleArrowDown()
}
//...

Tab 键补全 command


根据历史记录补全 command ,利用 Array.filter() 和 String.startsWith() 就行:


  // 匹配历史 command 并补充
const matchCommand = (inputValue: string): string | null => {
// 遍历历史command 返回以当前输入 command 值开头(startsWith)的 command
const matchedCommands = commandHistory.filter(command => command.startsWith(inputValue))
return matchedCommands.length > 0 ? matchedCommands[matchedCommands.length - 1] : null
}


代码仓库:commit9


最后


大家有兴趣的话可以自己再去二次改造或添加一些新玩法,此组件已通过 Netlify 部署上线,地址为 my-terminal.netlify.app/
项目源代码:github.com/ljq0226/my-… 欢迎 S

作者:Aphelios_
来源:juejin.cn/post/7248599585735098405
tar ⭐️⭐️⭐️

收起阅读 »

前端面试题 - 96. hash 和 history 的区别?

web
hash和history是Web开发中常用的两个概念,它们都与浏览器URL相关。 Hash(哈希) URL中以#符号开始的部分被称为哈希部分。在Web开发中,通常使用哈希来实现页面内的导航或锚点定位。当浏览器的哈希发生变化时,页面不会重新加载,而是触发一个ha...
继续阅读 »

hashhistory是Web开发中常用的两个概念,它们都与浏览器URL相关。


Hash(哈希)


URL中以#符号开始的部分被称为哈希部分。在Web开发中,通常使用哈希来实现页面内的导航或锚点定位。当浏览器的哈希发生变化时,页面不会重新加载,而是触发一个hashchange事件。


// 监听 hashchange 事件
window.addEventListener('hashchange', function() {
var currentHash = window.location.hash;

// 根据不同的哈希值执行相应的操作
if (currentHash === '#section1') {
console.log('显示第一部分的内容')
} else if (currentHash === '#section2') {
console.log('显示第二部分的内容')
} else {
console.log('其他操作')
}
});

通过监听此事件,你可以根据哈希的变化来执行相应的操作,例如显示不同的内容或调用特定的函数。哈希可以直接通过JavaScript进行修改,例如window.location.hash = "section2",URL将变为(此时hashchange事件也会触发):


https://example.com/page.html#section2
// 输出 显示第二部分的内容

History(历史记录)


历史记录是浏览器跟踪用户访问过的URL的一种机制。通过history对象,你可以在JavaScript中操作浏览器的历史记录。一些常用的方法包括history.pushState()history.replaceState()history.back()。这些方法允许你添加、替换和移动浏览器的历史记录,并且不会导致页面的实际刷新。当历史记录发生变化时,浏览器不会重新加载页面,但可以通过popstate事件来捕获这些变化并做出响应。


示例:


// 添加新的历史记录
history.pushState({ page: "page2" }, "Page 2", "page2.html");

// 监听 popstate 事件
window.addEventListener('popstate', function(event) {
var state = event.state;
console.log(state)
// 根据历史记录的变化执行相应的操作
if (state.page === "page1") {
console.log('显示第一页的内容')
} else if (state.page === "page2") {
console.log('显示第二页的内容')
} else {
console.log('其他操作')
}
});

需要注意的是,使用pushState()方法修改历史记录并不会触发popstate事件。只有在用户点击浏览器的前进或后退按钮时,或者通过JavaScript代码调用history.back()history.forward()history.go()方法导致历史记录变化时,popstate

作者:总瓢把子
来源:juejin.cn/post/7248608019851755575
e>事件才会被触发。

收起阅读 »

面试官: 既然有了 cookie 为什么还要 localStorage?😕😕😕

web
Web Storage Web Storage 最终是网页超文本应用技术工作组在 Web Applications 1.0 规范中提出的。这个规范中的草案最终成为了 HTML5 的一部分,后来有独立称为自己的规范。Web Storage 的目的是解决通过客户端...
继续阅读 »

Web Storage


Web Storage 最终是网页超文本应用技术工作组在 Web Applications 1.0 规范中提出的。这个规范中的草案最终成为了 HTML5 的一部分,后来有独立称为自己的规范。Web Storage 的目的是解决通过客户端存储不需要频繁发送回服务器的数据时使用 cookie 的问题。


Web Storage 规范最新的版本是第 2 版,这一版规范主要有两个目标:



  1. 提供在 cookie 之外的存储会话数据的途径;

  2. 提供跨会话持久化存储大量数据的机制;


Web Storage 定义了两个对象: localStoragesessionStorage。前者是永久存储机制,而后者是跨会话的存储机制。这两个浏览器存储 API 提供了在浏览器中不收页面刷新影响而存储数据的两种方式。


Storage 类型


Storage 类型用于保存 名/值 对数据,直至存储空间上限(由浏览器决定)。Storage 的实例与其他对象一样,但增加了以下方法:



  1. clear(): 删除所有值;

  2. getItem(name): 取得给定 name 值;

  3. key(index): 取得给定数值位置的名称;

  4. removeItem(name): 删除给定 name名/值 对;

  5. setItem(name,value): 设置给定 name 的值;


getItem()removeItem(name)setItem() 方法可以直接或间接通过 Storage 对象调用。因为每个数据项都作为属性存储在该对象上,所以可以使用点或括号操作符访问这些属性,统统同样的操作来设置值,也可以使用 delete 操作符来删除属性。即便如此,通常还是建议使用方法而非属性来执行这些操作,以免意外重写某个已存在的对象成员。


localStorage 对象


在修订的 HTML5 规范里,localStorage 对象取代了 globalStorage,作为在客户端持久存储数据的机制,要访问同一个 localStorage 对象,页面必须来自同一个域(子域不可以)、在想用的端口上使用相同的协议。


因为 localStorageStorage 的实例,所以可以像使用 sessionStorage 一样使用 localStorage。具体实例请看下面几个例子:


// 使用方法存储数据
localStorage.setItem("moment", 777);

// 使用属性存储数据
localStorage.nickname = "moment";

// 使用方法获取数据
const name = localStorage.getItem("moment");

// 使用属性获得数据
const nickname = localStorage.nickname;

两种存储方法的区别在于,存储在 localStorage 中的数据会保留到通过 JavaScript 删除或者用户清除浏览器缓存。localStorage 数据不受页面刷新影响,也不会因关闭窗口,标签也或重新启动浏览器而丢失。


存储事件


每当 Storage 对象发生变化时,都会在文档上触发 storage 事件,使用属性或者 setItem() 设置值、使用 deleteremoveItem() 删除值,以及每次调用 clean() 时都会触发这个事件,这个事件的事件对象有如下四个属性:



  1. domain: 存储变化对应的域;

  2. key: 被设置或删除的键;

  3. newValue: 键被设置的新值,若键被删除则为 null;

  4. oldValue: 键变化之前的值。


我们可以使用如下代码监听 storage 事件:


window.addEventListener("storage", function (e) {
document.querySelector(".my-key").textContent = e.key;
});

对于 sessionStoragelocalStorage 上的任何更改都会触发 storage 事件,但 storage 事件不会区分这两者。


这是一道面试题


在不久前,被问到这样一个问题,我们通过后端返回来的 token 为什么是存储在 localStorage 而不是存储在 cookie 中?


考虑这个问题的首先我们应该知道,token 就是一个字符串,而使用 cookie 的话,大小是满足的,所以考察的点就不在这个内存上面了。


之所以使用 localStorage 存储 token,而不是使用 cookie,这可能基于以下几个方面考虑:



  1. 前后端分离架构: 在一些现代的 Web 应用程序中,前端和后端通常是通过 API 进行通信的,而不是使用传统的服务器端渲染。在这种情况下,前端可能是一个独立的应用程序,如基于 JavaScript 的单页应用或移动应用程序。由于前端和后端是分离的,Cookie 在这种架构中不太容易管理,因为跨域请求可能会遇到一些限制。localStorage 提供了一种更方便的解决方案,前端应用程序可以直接访问和管理存储在本地的令牌;

  2. 安全性需求: 在某些情况下,开发者可能认为将令牌存储在 Cookie 中存在一些安全风险,尤其是在面对跨站脚本攻击 XSS 时。使用 localStorage 可以减少某些安全风险,因为 LocalStorage 中的数据不会自动发送到服务器,且可以通过一些安全措施(如加密)来增强数据的安全性;

  3. 令牌过期处理: 使用 localStorage 存储令牌可以让令牌在浏览器关闭后仍然保持有效,这在某些应用场景下是有用的。例如,用户可能关闭了浏览器,然后再次打开时仍然保持登录状态,而不需要重新输入凭据;


值得注意的是,使用 localStorage 存储 token 也不是说百分百安全的,依然会存在一些问题和风险,如容易收到 XSS 攻击、不支持跨域贡献等。因此,在使用 localStorage 存储令牌时,开发者需要采取适当的安全措施,如加密存储数据、定期更新令牌等,以确保令牌的安全性和有效性。


localStorage 如何实现跨域


localStorage 是一直域限制的存储机制,通常只能在同一域名下的页面中访问。这意味着默认情况下,localStorage 的数据在不同域名或跨域的情况下是无法直接访问的。然而,有几种方法可以实现跨域访问 localStorage 中的数据:



  1. 域名映射(Domain Mapping): 将不同域名都指向同一个服务器 IP 地址。这样不同域名下的页面就可以共享同一个 localStorage 中的数据;

  2. postMessage API: postMessage 是一种浏览器提供的 API,用于在不同窗口或跨域的 iframe 之间进行安全的消息传递。你可以在不同域名的页面中使用 postMessage 将数据从一个窗口传递到另一个窗口,并在目标窗口中将数据存储到 localStorage 中;


使用 postMessage 将数据从一个窗口传递到另一个窗口,并在目标窗口中将数据存储到 localStorage 中,实例代码如下:


// 发送消息到目标窗口
window.postMessage(
{ key: "token", value: "1233211234567" },
"https://liangzai.com"
);

在接收消息的窗口中:


// 监听消息事件
window.addEventListener("message", function (event) {
if (event.origin === "https://sourcedomain.com") {
// 存储数据到 LocalStorage
localStorage.setItem(event.data.key, event.data.value);
}
});

这些方法提供了一些途径来实现跨域访问 localStorage 中的数据。具体选择哪种方法取决于你的需求和应用场景,以及你对目标域名的控制程度。需要注意的是,安全性是非常重要。


cookie 和 localStorage 的区别


CookieLocalStorage 是两种用于在浏览器中存储数据的机制,它们在以下方面有一些区别:



  1. 存储容量: Cookie 的存储容量通常较小,每个 Cookie 的大小限制在几 KB 左右。而 LocalStorage 的存储容量通常较大,一般限制在几 MB 左右。因此,如果需要存储大量数据,LocalStorage 通常更适合;

  2. 数据发送: Cookie 在每次 HTTP 请求中都会自动发送到服务器,这使得 Cookie 适合用于在客户端和服务器之间传递数据。而 localStorage 的数据不会自动发送到服务器,它仅在浏览器端存储数据,因此 LocalStorage 适合用于在同一域名下的不同页面之间共享数据;

  3. 生命周期:Cookie 可以设置一个过期时间,使得数据在指定时间后自动过期。而 LocalStorage 的数据将永久存储在浏览器中,除非通过 JavaScript 代码手动删除;

  4. 安全性:Cookie 的安全性较低,因为 Cookie 在每次 HTTP 请求中都会自动发送到服务器,存在被窃取或篡改的风险。而 LocalStorage 的数据仅在浏览器端存储,不会自动发送到服务器,相对而言更安全一些;


总结


Cookie 适合用于在客户端和服务器之间传递数据、跨域访问和设置过期时间,而 LocalStorage 适合用于在同一域名下的不同页面之间共享数据、存储大量数据和永久存储数据。选择使用哪种机制应根据具体的需

作者:Moment
来源:juejin.cn/post/7248623545219825723
求和使用场景来决定。

收起阅读 »

在高德地图中实现降雨图层

web
前言 有一天老板跑过来跟我说,我们接到一个水利局的项目,需要做一些天气效果,比如说降雨、河流汛期、洪涝灾害影响啥的,你怎么看。欸,我觉得很有意思,马上开整。 需求说明 在地图上实现降雨效果,画面尽量真实,比如天空、风云的变化与降雨场景契合; 可以结合当地天气预...
继续阅读 »

前言


有一天老板跑过来跟我说,我们接到一个水利局的项目,需要做一些天气效果,比如说降雨、河流汛期、洪涝灾害影响啥的,你怎么看。欸,我觉得很有意思,马上开整。


需求说明


在地图上实现降雨效果,画面尽量真实,比如天空、风云的变化与降雨场景契合;


可以结合当地天气预报情况,自动调节风速、风向、降雨量等参数。


需求分析


方案一:全局降雨


在用户视口面前加一层二维的降雨平面层。


优点: 只管二维图层就行了,不需要与地图同步坐标,实现起来比较简单,界面是全局的一劳永逸。


缺点:只适合从某些角度观看,没法再做更多定制了。


Honeycam_2023-06-16_11-10-37.gif


方案二:局部地区降雨


指定降雨范围,即一个三维空间,坐标与地图底图同步,仅在空间内实现降雨。


优点:降落的雨滴有远近关系,比较符合现实场景;可适用各种地图缩放程度。


缺点:需要考虑的参数比较多,比如降雨范围一项就必须考虑这个三维空间是什么形状,可能是立方体、圆柱体或者多边形挤压体;需要外部图层的配合,比如说下雨了,那么天空盒子的云层、建筑图层的明度是否跟着调整。


Honeycam_2023-06-16_11-20-08.gif


实现思路


根据上面利弊权衡,我选择了方案二进行开发,并尽量减少输入参数,降雨影响范围初步定为以地图中心为坐标中心的立方体,忽略风力影响,雨滴采用自由落体方式运动。


降雨采用自定义着色器的方式实现,充分利用GPU并行计算能力,刚好在网上搜到一位大佬写的three演示代码,改一下坐标轴(threejs空间坐标轴y轴朝上,高德GLCustomLayer空间坐标z轴朝上)就可以直接实现最基础的效果。这里为了演示方便增加坐标轴和影响范围的辅助线。


1.创建影响范围,并在该范围内创建降雨层的几何体Geometry,该几何体的构成就是在影响范围内随机位置的1000个平面,这些平面与地图底面垂直;


Honeycam_2023-06-24_15-40-31.gif


2.创建雨滴材质,雨滴不受光照影响,这里使用最基础的MeshBasicMaterial材质即可,半透明化且加上一张图片作为纹理;


Honeycam_2023-06-24_15-50-32.gif


3.为实现雨滴随着时间轴降落的动画效果,需要调整几何体的形状尺寸,并对MeshBasicMaterial材质进行改造,使其可以根据当前时间time改变顶点位置;


Honeycam_2023-06-24_16-01-39.gif



  1. 调整顶点和材质,使其可以根据风力风向改变面的倾斜角度和移动轨迹;


Honeycam_2023-06-24_16-16-52.gif



  1. 将图层叠加到地图3D场景中


Honeycam_2023-06-24_16-28-46.gif


基础代码实现


为降低学习难度,本模块只讲解最基础版本的降雨效果,雨滴做自由落体,忽略风力影响。这里的示例以高德地图上的空间坐标轴为例,即z轴朝上,three.js默认空间坐标系是y轴朝上。我把three.js示例代码演示放到文末链接中。


1.创建影响范围,并在该范围内创建降雨层的几何体Geometry


createGeometry () {
// 影响范围:只需要设定好立方体的size [width/2, depth/2, height/2]
//
const { count, scale, ratio } = this._conf.particleStyle
// 立方体的size [width/2, depth/2, height/2]
const { size } = this._conf.bound
const box = new THREE.Box3(
new THREE.Vector3(-size[0], -size[1], 0),
new THREE.Vector3(size[0], size[1], size[2])
)

const geometry = new THREE.BufferGeometry()
// 设置几何体的顶点、法线、UV
const vertices = []
const normals = []
const uvs = []
const indices = []

// 在影响范围内随机位置创建粒子
for (let i = 0; i < count; i++) {
const pos = new THREE.Vector3()
pos.x = Math.random() * (box.max.x - box.min.x) + box.min.x
pos.y = Math.random() * (box.max.y - box.min.y) + box.min.y
pos.z = Math.random() * (box.max.z - box.min.z) + box.min.z

const height = (box.max.z - box.min.z) * scale / 15
const width = height * ratio

// 创建当前粒子的顶点坐标
const rect = [
pos.x + width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z - height / 2,
pos.x + width,
pos.y,
pos.z - height / 2
]

vertices.push(...rect)

normals.push(
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z,
pos.x,
pos.y,
pos.z
)

uvs.push(1, 1, 0, 1, 0, 0, 1, 0)

indices.push(
i * 4 + 0,
i * 4 + 1,
i * 4 + 2,
i * 4 + 0,
i * 4 + 2,
i * 4 + 3
)
}

// 所有顶点的位置
geometry.setAttribute(
'position',
new THREE.BufferAttribute(new Float32Array(vertices), 3)
)
// 法线信息
geometry.setAttribute(
'normal',
new THREE.BufferAttribute(new Float32Array(normals), 3)
)
// 设置UV属性与顶点顺序一致
geometry.setAttribute(
'uv',
new THREE.BufferAttribute(new Float32Array(uvs), 2)
)
// 设置基本单元的顶点顺序
geometry.setIndex(new THREE.BufferAttribute(new Uint32Array(indices), 1))

return geometry
}

2.创建材质


createMaterial () {
// 粒子透明度、贴图地址
const { opacity, textureUrl } = this._conf.particleStyle
// 实例化基础材质
const material = new THREE.MeshBasicMaterial({
transparent: true,
opacity,
alphaMap: new THREE.TextureLoader().load(textureUrl),
map: new THREE.TextureLoader().load(textureUrl),
depthWrite: false,
side: THREE.DoubleSide
})

// 降落起点高度
const top = this._conf.bound.size[2]

material.onBeforeCompile = function (shader, renderer) {
const getFoot = `
uniform float top; // 天花板高度
uniform float bottom; // 地面高度
uniform float time; // 时间轴进度[0,1]
#include <common>
float angle(float x, float y){
return atan(y, x);
}
// 让所有面始终朝向相机
vec2 getFoot(vec2 camera,vec2 normal,vec2 pos){
vec2 position;
// 计算法向量到点的距离
float distanceLen = distance(pos, normal);
// 计算相机位置与法向量之间的夹角
float a = angle(camera.x - normal.x, camera.y - normal.y);
// 根据点的位置和法向量的位置调整90度
pos.x > normal.x ? a -= 0.785 : a += 0.785;
// 计算投影值
position.x = cos(a) * distanceLen;
position.y = sin(a) * distanceLen;

return position + normal;
}
`

const begin_vertex = `
vec2 foot = getFoot(vec2(cameraPosition.x, cameraPosition.y), vec2(normal.x, normal.y), vec2(position.x, position.y));
float height = top - bottom;
// 计算目标当前高度
float z = normal.z - bottom - height * time;
// 落地后重新开始,保持运动循环
z = z + (z < 0.0 ? height : 0.0);
// 利用自由落体公式计算目标高度
float ratio = (1.0 - z / height) * (1.0 - z / height);
z = height * (1.0 - ratio);
// 调整坐标参考值
z += bottom;
z += position.z - normal.z;
// 生成变换矩阵
vec3 transformed = vec3( foot.x, foot.y, z );
`

shader.vertexShader = shader.vertexShader.replace(
'#include <common>',
getFoot
)
shader.vertexShader = shader.vertexShader.replace(
'#include <begin_vertex>',
begin_vertex
)
// 设置着色器参数的初始值
shader.uniforms.cameraPosition = { value: new THREE.Vector3(0, 0, 0) }
shader.uniforms.top = { value: top }
shader.uniforms.bottom = { value: 0 }
shader.uniforms.time = { value: 0 }
material.uniforms = shader.uniforms
}

this._material = material

return material
}

3.创建模型



createScope () {
const material = this.createMaterial()
const geometry = this.createGeometry()

const mesh = new THREE.Mesh(geometry, material)

this.scene.add(mesh)

// 便于调试,显示轮廓
// const box1 = new THREE.BoxHelper(mesh, 0xffff00)
// this.scene.add(box1)
}

4.更新参数


// 该对象用于跟踪时间
_clock = new THREE.Clock()

update () {
const { _conf, _time, _clock, _material, camera } = this

// 调整时间轴进度,_time都值在[0,1]内不断递增循环
// particleStyle.speed为降落速度倍率,默认值1
// _clock.getElapsedTime() 为获取自时钟启动后的秒数
this._time = _clock.getElapsedTime() * _conf.particleStyle.speed / 2 % 1

if (_material.uniforms) {
// 更新镜头位置
_material.uniforms.cameraPosition.value = camera.position
// 更新进度
_material.uniforms.time.value = _time
}
}

animate (time) {
if (this.update) {
this.update(time)
}
if (this.map) {
// 叠加地图时才需要
this.map.render()
}
requestAnimationFrame(() => {
this.animate()
})
}

优化调整


修改场景效果


通过对图层粒子、风力等参数进行封装,只需简单地调整配置就可以实现额外的天气效果,比如让场景下雪也是可以的,广州下雪这种场景,估计有生之年只能在虚拟世界里看到了。


Honeycam_2023-06-24_17-00-11.gif


以下是配置数据结构,可供参考


const layer = new ParticleLayer({
map: getMap(),
center: mapConf.center,
zooms: [4, 30],
bound: {
type: 'cube',
size: [500, 500, 500]
},
particleStyle: {
textureUrl: './static/texture/snowflake.png', //粒子贴图
ratio: 0.9, //粒子宽高比,雨滴是长条形,雪花接近方形
speed: 0.04, // 直线降落速度倍率,默认值1
scale: 0.2, // 粒子尺寸倍率,默认1
opacity: 0.5, // 粒子透明度,默认0.5
count: 1000 // 粒子数量,默认值10000
}
})

添加风力影响


要实现该效果需要添加2个参数:风向和风力,这两个参数决定了粒子在降落过程中水平面上移动的方向和速度。



  1. 首先调整一下代码实际那一节步骤2运动的相关代码


const begin_vertex = `
...
// 利用自由落体公式计算目标高度
float ratio = (1.0 - z / height) * (1.0 - z / height);
z = height * (1.0 - ratio);
// 增加了下面这几行
float x = foot.x+ 200.0 * ratio; // 粒子最终在x轴的位移距离是200
float y = foot.y + 200.0 * ratio; // 粒子最终在y轴的位移距离是200
...
// 生成变换矩阵
vec3 transformed = vec3( foot.x, y, z );


  1. 如果粒子是长条形的雨滴,那么它在有风力影响的运动时,粒子就不是垂直地面的平面了,而是与地面有一定倾斜角度的平面,如图所示。


Untitled.png


我们调整调整一下代码实际那一节步骤1的代码,实现方式就是让每个粒子平面在创建之后,所有顶点绕着平面的法线中心轴旋转a角度。


本示例旋转轴(x, y, 1)与z轴(0,0,1)平行,这里有个技巧,我们在做平面绕轴旋转的时候先把平面从初始位置orgPos移到坐标原点,绕着z轴旋转后再移回orgPos,会让计算过程简单很多。


// 创建当前粒子的顶点坐标
const rect = [
pos.x + width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z + height / 2,
pos.x - width,
pos.y,
pos.z - height / 2,
pos.x + width,
pos.y,
pos.z - height / 2
]

// 定义旋转轴
const axis = new THREE.Vector3(0, 0, 1).normalize();
//定义旋转角度
const angle = Math.PI / 6;
// 创建旋转矩阵
const rotationMatrix = new THREE.Matrix4().makeRotationAxis(axis, angle);

for(let index =0; index< rect.length; index +=3 ){
const vec = new THREE.Vector3(rect[index], rect[index + 1], rect[index + 2]);
//移动到中心点
vec.sub(new THREE.Vector3(pos.x, pos.y,pos.z))
//绕轴旋转
vec.applyMatrix4(rotationMatrix);
//移动到原位
vec.add(new THREE.Vector3(pos.x, pos.y, pos.z))
rect[index] = vec.x;
rect[index + 1] = vec.y;
rect[index + 2] = vec.z;
}

待改进的地方


本示例中有个需要完善的地方,就是加入了风力影响之后,如果绕垂直轴旋转一定的角度,会看到如下图的异常,雨点的倾斜角度和运动倾斜角度是水平相反的。


Honeycam_2023-06-24_21-06-51.gif


问题的原因是材质着色器中的“让所有面始终朝向相机”方法会一直维持粒子的倾斜状态不变,解决这个问题应该是调整这个方法就可以了。然而作为学渣的我还没摸索出来,果然可视化工程的尽头全是数学Orz。


相关链接


1.THREE.JS下雨进阶版,面只旋转Y轴朝向相机


http://www.wjceo.com/blog/threej…


2.演示代码在线DEMO


jsfiddle.net/gyrate

sky/5…

收起阅读 »

何处是吾乡?(前端人年中总结)

前言 我的老家是湖北黄州,对,就是子瞻兄被贬的那个黄州,我很喜欢苏轼的豪放派诗词,为他积极乐观的生活态度和人格魅力着迷。 本人不知不觉干前端快**5个年头**了,虽辗转了**3个城市**,依次在厦门,武汉,杭州历经了**4份工作...
继续阅读 »

微信图片_20230625193606.jpg


前言


我的老家是湖北黄州,对,就是子瞻兄被贬的那个黄州,我很喜欢苏轼的豪放派诗词,为他积极乐观的生活态度和人格魅力着迷。


本人不知不觉干前端快**5个年头**了,虽辗转了**3个城市**,依次在厦门,武汉,杭州历经了**4份工作**。但依然喜欢那句**少年的征途是星辰大海**,我不在乎之后我会在那儿,也正是苏轼说的,此心安处便是吾乡!


半年已过,有失有得:


第一: 被裁员。今年开年2月份,公司资金链断裂,开始大规模裁员,那段时间,整个公司都笼罩在裁员的黑色恐怖下,各种新闻又提醒我们大环境不好找工作,前端已死等等,我当时也有所预感我会在名单之类(没有安排工作任务),内心很难过,是那种感觉自己要被抛弃的悲伤,尽管室友安慰我:“说我来公司这么久(一年半),要裁也是裁你之后来的,就算被裁,你也还年轻,肯定能找到”,但那一天终归是来了,部门经理走过来,无奈的拍我肩膀说:“兄弟,实在是不好意思,我也没办法了”,叫我去办公室谈,我心里的靴子终于落地了,脸上苦笑,在一群同事不可思议的目送下,跟着经理来到办公室,总监也早早坐在里面了,看到我,一声长叹息,接着一些诸如上面的决定,我也没办法这类的话后,我一阵愣神,也在这无奈中被迫接受了。离职流程走的真快,赔偿n+1方案我也妥协了!


微信图片_20220606201942.png


第二: 失去了一位朋友。去年,一次同学聚会,认识了一个新朋友,是我同学的同学,聚会结束拍合照的时候,站在我身边,我看清她了,脸蛋圆圆的,双马尾,鼻子挺,戴个眼镜,长相中等,看到她的笑容,我内心OS:“我要找的不就是她吗?”,随后找我同学打听了下,单身可聊,我拿着零食,上去一番勇敢搭讪后,然后顺利要到了微信,向有经验的朋友请教怎么追女生。后来,我经常邀请她周末出来玩,看电影,动物园,火锅,也快速熟络起来,但在一个晚上,我突发奇想的向她表白了,不出意外被拒了,然后关系就慢慢地变得尴尬起来,找她聊微信也常常爱答不理了,我(纯情小处男)表示很受伤,问她原因,她说对我没感觉。我想大概是考验我?我得更加主动些,约她出来玩,找她聊天,可约不出来,聊天也不怎么回消息了,不知不觉到年底了,关系越来越拉胯了。


那段时间,我内心经历了什么,我已不想回忆了,至于她,今年2月15号那天发了一条朋友圈,她官宣脱单,算是在告诉我,别再烦她了,恰恰这天,也是我在公司的last day。。。我本以为我已经释怀了,不曾想我的心头还是有一股说不出的酸楚,晚上和几个要好的同事攒了个酒局,算是离别晚宴吧,期间,酒桌上说了些什么我已经记不得,只记得我多喝了几杯,有些醉。我隐隐的感觉,我心中追逐的光熄灭了, 就像是夜幕下,在茫茫的大海中孤独航行的船,突然看到远方有一座灯塔,于是乎努力的朝着那微弱的灯光划去,使出了浑身解数,精疲力竭,却看到了灯塔骤然熄灭了,又重回黑暗!我也发了一条朋友圈:失之东隅收之桑榆,塞翁失马焉知非福! 算是为 被公司裁员的自我安慰和鼓励,也是对失去了这个段关系的自我劝解,罢了~


src=http___5b0988e595225.cdn.sohucs.com_images_20180819_98b3c15d372a43fe9ad8a2b71444c7b0.png&refer=http___5b0988e595225.cdn.sohucs.webp
第三:股票亏3w。麻绳专挑细处断,噩运只找苦命,如果说上面两件事情给我的是暴击,那投资的股票连续的亏损就是给我的魔法伤害,一直出血。。。那些人说的没错,股票就是放大你的贪婪和恐惧。前年本着价值投资,加入这个血雨腥风的二级市场,没曾想,这个市场就是个无情的机器,不断收割你的财富和精力,劝那些心存幻想的佳人们,别来沾边,离得越远越好!


微信图片_20230625213901.jpg



第一:找到工作!在离职了2个多月后,我一直赋闲在家,调整心态、写简历,复习前端知识。外公带给我一只拉布拉多犬,我每天牵着它溜达,照顾它,给它洗澡,在它陪伴下,我内心得到了不少治愈。后来我一起被裁的同事内推了我一家公司,去杭州,内心还犹豫了一下,同父亲聊,他鼓励我去,告诉我还年轻,多出去走走也不是坏事,三轮面试,都还挺顺利的过了,薪资也在我的意料之外,确认上班时间后,我和朋友开车去了趟恩施自驾游,回来就收拾行李去杭州入职了。不过,这次我更多地是运气占了上风,被裁后,自己投递的都没有面试机会,内推还是给力!后面再周末,下班回家,还得多提升自己~


微信图片_20230625193610.jpg
第二:鼻骨矫正手术。大学时,打篮球,意外受伤导致鼻骨偏曲了一些,一直没时间做手术,这次终于是有机会了,在离职后的几天,我母亲就为我安排了一个在鼻子整形方面很有名的医生为我做手术。开始我是拒绝的,一个男人做整形方面的手术,作为直男,我很抗拒,后来在我母亲和父亲的说服下,我还是同意了,躺在手术台,我整个紧张到冒汗,局麻,五针打在上嘴唇和鼻翼周围,还有鼻梁上,疼的眼泪直流,整体手术下来,我已经虚脱了。在医院住了几天后就回家调养,等半个月拆线后,看到我的新鼻子,顿时感觉这手术没白做。鼻子整个都挺起来了,山根变高了,脸也小了很多。


微信图片_20230625223222.jpg


总结


今年上半年,算是我的转折点, 有一些人离开了我,有一些人又进入到了我的世界中,只是在经历了失与得的之后,我又站在了新的起点,开始新的征程,此时又正如东坡的那句:回首向来萧瑟去,归去,也无风雨也无晴

作者:掘金爱佛森
来源:juejin.cn/post/7248606482014732345

收起阅读 »

我有个气人的同事......

web
前段时间看到掘金上好几个 console 自定义的仓库玩法,就想到自己曾经也这么玩过。就想着把自己故事写出来。 曾经,我有个气人的同事,总是喜欢用 console.error() 来调试代码,搞得我和他合作,看到控制台老难受了,就为他特殊定制了一个工具库 ...
继续阅读 »

前段时间看到掘金上好几个 console 自定义的仓库玩法,就想到自己曾经也这么玩过。就想着把自己故事写出来。




曾经,我有个气人的同事,总是喜欢用 console.error() 来调试代码,搞得我和他合作,看到控制台老难受了,就为他特殊定制了一个工具库 console-custom。沉寂在个人仓库很久,前段时间看到别人也有类似仓库,也就想着把自己的也发出来。




其实,我个人不是很推荐在代码里 写 console.log 之类的来调试代码,更推荐去浏览器控制台去打断点来调试,更好的理清数据的流转,事件的先后顺序等。



背景


官方背景:



  • 方便大家调试代码的时候,在浏览器控制台输出自定义个性化日志。

  • 防止控制台输出密密麻麻的 console.log,一眼看不到想看的。

  • 防止某个气人的小伙伴老是使用 console.error,强迫症不允许。

  • ......


真实背景:


其实,是我之前有个小伙伴同事——“小白菜”(也是为啥函数名叫 blog 的原因之一,下边会看到),他调试代码,打印输出总是喜欢 console.error(),用完了还不自己清理,大家协同开发的时候,git pull 他的代码后,总是让人就很难受!看着一堆报错,一时半会看不清是程序自己的报错,还是调试的输出!强迫症就犯了!想骂街......


不......不......要冷静!



  • 编码千万行

  • 调试要输出

  • log不规范

  • 同事两行泪


效果


浏览器页面 page


tu1.jpg


浏览器控制台 console


image.png



有个重点、痛点是这个, console.log(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333); 打印的数据多的时候不换行,需要找半天,我专门给处理成 分行, 一行一行展示了,这样好看清数据。



这个工具库有以下几个特点:



  1. 支持输入多个数据,并分行打印出来,并能看出是个整体

  2. 支持自己修改自己的默认样式部分,配置自己的默认部分

  3. 支持额外的自定义部分,拓展用户更多的玩法

  4. ......


其实 console 由于可以自定义,其实会有很多玩法,我个人在此主要的思路是



  1. 一定要简单,因为 console.log 本身就很简单,尽量不要造成使用者心智负担。

  2. 就简单的默认定制一个彩色个性化的部分,能区分出来,解决那个气人同事所谓的痛点就好。

  3. 代码要少,不要侵入,不要影响用户的业务代码


源码


此处源码有借鉴 github 开源代码:github.com/Redstone-1/…


大家如有更多、更丰富的需求场景可去参考使用。


// src/utils/console-custom.js
const GourdBabyColorMap = new Map([
["1", "#FF0000"],
["2", "#FFA500"],
["3", "#FFFF00"],
["4", "#008000"],
["5", "#00FFFF"],
["6", "#0000FF"],
["7", "#800080"],
]);

const createBLog = (config) => {
const logType = config.logType || "default";
const username = config.username || "";
const logName = config.logName || "";
const usernameColor = config.usernameColor || "#41b883";
const logNameColor = config.logNameColor || "#35495e";
const padding = config.padding || 6;
const borderRadius = config.borderRadius || 6;
const fontColor = config.fontColor || "#FFFFFF";
const usernameStyle = config.usernameStyle || "";
const logNameStyle = config.logNameStyle || "";

const logTemplate = (username = "myLog", logName = "") =>
`${username ? '%c' + username : ''} ${logName ? '%c' + logName : ''} `;

const customLog = (...data) => {
console.log(
logTemplate(username, logName),
usernameStyle ? usernameStyle : `background: ${usernameColor}; padding: 6px; border-radius: 6px 0 0 6px; color: #fff`,
logNameStyle ? logNameStyle : `background: ${logNameColor}; padding: 6px; border-radius: 0 6px 6px 0; color: #fff`,
...data
);
};

const defaultLog = (...data) => {
const len = data.length;
if (len > 1) {
data.map((item, index) => {
let firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
border-radius: 0 0;
color: ${fontColor}
`
;
let secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
border-radius: 0 0;
color: ${fontColor}
`
;
if (index === 0) {
firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
margin-top: ${padding * 2}px;
border-radius: ${borderRadius}px 0 0 0;
color: ${fontColor}
`
;
secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
margin-top: ${padding * 2}px;
border-radius: 0 ${borderRadius}px 0 0;
color: ${fontColor}
`
;
} else if (index === len -1) {
firstStyle = `
background: ${GourdBabyColorMap.get(index % 7 + 1 +'')};
padding: ${padding}px;
margin-bottom: ${padding * 2}px;
border-radius: 0 0 0 ${borderRadius}px;
color: ${fontColor}
`
;
secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
margin-bottom: ${padding * 2}px;
border-radius: 0 0 ${borderRadius}px 0;
color: ${fontColor}
`
;
}
console.log(
logTemplate(username, `数据${index+1}`),
firstStyle,
secondStyle,
item
);
});
} else {
const firstStyle = `
background: ${usernameColor};
padding: ${padding}px;
border-radius: ${borderRadius}px 0 0 ${borderRadius}px;
color: ${fontColor}
`
;

const secondStyle = `
background: ${logNameColor};
padding: ${padding}px;
border-radius: 0 ${borderRadius}px ${borderRadius}px 0;
color: ${fontColor}
`
;

console.log(
logTemplate(username, logName),
firstStyle,
secondStyle,
...data
);
}
};

const log = (...data) => {
switch(logType) {
case 'custom':
customLog(...data)
break;
default:
defaultLog(...data)
}
};

return {
log,
};
};

export default createBLog

API


唯一API createBLog(对!简单!易用!用起来没有负担!)


import createBLog from '@/utils/console-custom'

const myLog = createBLog(config)

配置 config: Object


一次配置,全局使用。(该部分是借鉴开源代码重构了配置内容)


配置项说明类型默认值
logTypelog 日志类型default、customdefault
usernamelog 的主人,也就是谁打的日志string-
logNamelog 的名字,也就是打的谁的日志string-
usernameColorusername 自定义背景颜色,接受 CSS background 的其他书写形式,例如渐变string#41b883
logNameColorlogName 自定义背景颜色,接受 CSS background 的其他书写形式,例如渐变string#35495e
paddingusername 和 logName 内边距,单位 pxnumber6
borderRadiususername 和 logName 圆角边框,单位 pxnumber6
fontColorusername 和 logName 字体颜色string#FFFFFF
usernameStyleusername 自定义样式,logType 为 custom 时候设置才生效,设置后则 usernameColor 的设置会失效string-
logNameStylelogName 自定义样式,logType 为 custom 时候设置才生效,设置后则 usernameColor 的设置会失效string-

基本用法 default



也是默认用法(default),同时也是最推荐大家用的一种方法。



vue2 版本


// main.js
import createBLog from '@/utils/console-custom'

const myLog = createBLog({
username: "bigger",
logName: "data",
usernameColor: "orange",
logNameColor: "#000000",
padding: 6,
borderRadius: 12,
fontColor: "#aaa",
});

// 不需要使用时单独自定义 logName 的全局绑定
Vue.prototype.$blog = myLog.log;

// 需要使用时单独自定义 logName 的全局绑定
Vue.prototype.$nlog = (logName, ...data) => {
myLog.logName = logName;
myLog.log(...data);
};

// vue2 组件里边使用
// 同时输入多个日志数据,可帮用户按照行的形式分开,好一一对应看清 log
this.$blog(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333);
this.$blog(111231231231231);

this.$nlog("logName", 2212121212122);

vue3 版本


// main.ts
import createBLog from '@/utils/console-custom'

const myLog = createBLog({
username: "bigger",
logName: "data",
usernameColor: "orange",
logNameColor: "#000000",
padding: 6,
borderRadius: 12,
fontColor: "#aaa",
});

app.config.globalProperties.$blog = myLog.log;

// vue3 组件里边使用
import { getCurrentInstance } from 'vue'

export default {
setup () {
const { proxy } = getCurrentInstance()

proxy.$blog(111, 222, 333, 444, 555, 666, 777, 888, 999, 1111, 2222, 3333);
proxy.$blog(111231231231231);

proxy.$nlog("logName", 2212121212122);
}
}

自定义用法 custom



这部分我没有很多玩法,下边的例子也是借鉴别人的,主要全靠用户自己扩展 css 样式了。做一套自己喜欢的样式。



// main.js

// ....
Vue.prototype.$clog = (logName, ...data) => {
myLog.logType = "custom";
myLog.logName = logName;
myLog.usernameStyle = `text-align: center;
padding: 10px;
background-image: -webkit-linear-gradient(left, blue,
#66ffff 10%, #cc00ff 20%,
#CC00CC 30%, #CCCCFF 40%,
#00FFFF 50%, #CCCCFF 60%,
#CC00CC 70%, #CC00FF 80%,
#66FFFF 90%, blue 100%);`
;
myLog.logNameStyle = `background-color: #d2d500;
padding: 10px;
text-shadow: -1px -1px 0px #e6e600,-2px -2px 0px #e6e600,
-3px -3px 0px #e6e600,1px 1px 0px #bfbf00,2px 2px 0px #bfbf00,3px 3px 0px #bfbf00;`
;
myLog.log(...data);
};

// 提供的其他 css 样式
myLog.usernameStyle = `background-color: darkgray;
color: white;
padding: 10px;
text-shadow: 0px 0px 15px #00FFFF,0px 0px 15px #00FFFF,0px 0px 15px #00FFFF;`
;
myLog.logNameStyle = `background-color: gray;
color: #eee;
padding: 10px;
text-shadow: 5px 5px 0 #666, 7px 7px 0 #eee;`
;

myLog.usernameStyle = `background-color: darkgray;
color: white;
padding: 10px;
text-shadow: 1px 1px 0px #0000FF,2px 2px 0px #0000FF,-1px -1px 0px #E31B4E,-2px -2px 0px #E31B4E;`
;
myLog.logNameStyle = `font-family: "Arial Rounded MT Bold", "Helvetica Rounded", Arial, sans-serif;
text-transform: uppercase;/* 全开大写 */
padding: 10px;
color: #f1ebe5;
text-shadow: 0 8px 9px #c4b59d, 0px -2px 1px #fff;
font-weight: bold;
letter-spacing: -4px;
background: linear-gradient(to bottom, #ece4d9 0%,#e9dfd1 100%);`
;
// ....

其中渐变色的玩法


myLog.usernameStyle = `background-image: linear-gradient(to right, #ff0000, #ff00ff); padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; text-transform: uppercase; font-weight: 600;`;
myLog.logNameStyle = `background-image: linear-gradient(to right, #66ff00 , #66ffff); padding: 6px 12px; border-radius: 2px; font-size: 14px; color: #fff; text-transform: uppercase; font-weight: 600;`;

其中输出 emoji 字符


this.$nlog("😭", 2212121212122);
this.$nlog("🤡", 2212121212122);
this.$nlog("💩", 2212121212122);
this.$nlog("🚀", 2212121212122);
this.$nlog("🎉", 2212121212122);
this.$nlog("🐷", 2212121212122);

小伙伴们你肯定还有什么好玩的玩法!尽情发挥吧!


最后


还是想极力劝阻那些用 console.error() 调试代码的人,同时也能尽量少用 console 来调试,可以选择控制台断点、编译器断点等。还是不是很推荐使用 console 来调试,不过本文也可以让大家知道,其实 console 还有这种玩法。如果写 JS 库的时候也可以使用,让自己

作者:Bigger
来源:juejin.cn/post/7248448028297855035
的库极具自己的特色。

收起阅读 »

Compose + Fragment是一个不错的选择

Compose很好用,但是在真正应用到项目时,我们还需要解决一些问题。 我要开发一个这样的页面,外层用Bottom Navigation Activity,每个tab对应的一个fragment,页面内容我用Compose来填充,不使用xml来布局,因为Comp...
继续阅读 »

Compose很好用,但是在真正应用到项目时,我们还需要解决一些问题。


我要开发一个这样的页面,外层用Bottom Navigation Activity,每个tab对应的一个fragment,页面内容我用Compose来填充,不使用xml来布局,因为Compose太好用了,如果不是页面缓存的原因,我可能会选择全部使用Compose来写。


我曾尝试使用material3的Navigation Bar来处理tab,但是每次点击tab后,页面总会重新加载,虽然功能实现了,但是这不是我想要的结果,我希望重新点回页面时,页面的位置、状态还是之前的样子。


这是最终的效果


Screenshot_20221221_192042.png


创建 Bottom Navigation Activity


New Project时,选择Bottom Navigation Activity,系统就会帮你创建一个带有3个tab页面的应用,此时都是正常的,但是我的应用需要有4个tab,当我将第4个tab加上去之后,我发现tab item,只有选中的,才会展示文字标题,就像下面这个样子。


Screenshot_20221221_1941322.png


那为什么会这样呢,其实是因为Android有意而为之,关于Bottom Navigation Item的设计规范,可以参考这里Bottom Navigation,那要怎么样,才能让所有的tab item都能够展示文字标题呢,只需要在onCreate中设置下显示模式即可

val navView: BottomNavigationView = binding.navView
navView.labelVisibilityMode = NavigationBarView.LABEL_VISIBILITY_LABELED

添加 Compose 到 fragment 里


现在的代码,又臭又长,让我们先把Frament对应的xml布局文件删掉,不需要在xml中写布局了,另外把Fragment中onCreateView也清理一下,填充上简洁的代码片段,如下

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
Text("HomeFragment")
}
}
}

瞬间整个世界都清净了,值得一提的是,ComposeView是普通视图和Compose视图的桥梁,起着至关重要的作用。


添加这段代码后,会报错,因为没有添加对应Compose库,等待Android Studio自动补全需要的库文件,补全之后,错误会消失,并且会在build.gradle添加好依赖。


这个时候运行项目,我们发现一个奇怪的问题,页面顶部会出现一部分空白,有一块白色区域


Screenshot_20221221_2012272.png


我们需要找到activity_main.xml文件,删除其中的这行代码,这个高度为56dp的空白就可以消失了。

android:paddingTop="?attr/actionBarSize"

填充列表页面


不得不说Compose实在比xml布局好用多了,写起来更像是写SwiftUI和Flutter。列表页面填充完之后,我发现列表最后一项,并不能完全展示,被Bottom Nav View给挡住了


Screenshot_20221221_2025262.png


怎么解决呢,我们还是需要找到activity_main.xml文件,将fragment的高度,由match_parent改为默认0dp,不要过早的撑满容器,这样页面就能够显示正常了

android:layout_height="0dp"

替换布局文件中的fragment


解决掉上面的那么些问题之后,这里还有一个黄色的小提示,Android Studio建议我们把fragment替换成FragmentContainerView,当我们根据建议点击替换后,再运行项目,我们发现应用崩溃了


Caused by: java.lang.IllegalStateException: Activity xx.xx.xx.MainActivity@7b7a278 does not have a NavController set on 2131231026


此时我们需要找到MainActivity,将其中的一行代码

val navController = findNavController(R.id.nav_host_fragment_activity_main)

替换为

val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_main) as NavHostFragment
val navController = navHostFragment.navController

此时就一切都正常了,可以进行下一步了。


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

Kotlin 函数接口与普通接口的区别

记一次编写Demo时SonarLint提示警告而关注到的kotlin1.4新增的接口声明方式.// SonarLint警告: Make this interface functional or replace it with a function type. ...
继续阅读 »

记一次编写Demo时SonarLint提示警告而关注到的kotlin1.4新增的接口声明方式.

// SonarLint警告: Make this interface functional or replace it with a function type.
interface GitHubService {

@GET("search/repositories?sort=stars&q=Android")
suspend fun searchRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): GitHubResponse

}


// 声明为函数接口后修复警告
fun interface GitHubService {

@GET("search/repositories?sort=stars&q=Android")
suspend fun searchRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): GitHubResponse

}

普通接口


当使用 interface 关键字定义接口时,可以声明抽象方法和默认方法。抽象方法是没有具体实现的方法,需要在实现接口的类中提供具体的实现。默认方法是在接口中提供了一个默认的实现,实现类可以选择重写或者直接使用默认实现。

interface GitHubService {
fun getUser(username: String): User // 抽象方法

fun getRepositories(username: String): List<Repository> { // 默认方法
val user = getUser(username)
// 通过用户获取仓库列表的具体实现
// ...
return repositories
}
}

函数接口


使用 fun interface 声明的接口只能包含一个抽象方法,并且不能包含默认方法。这种类型的接口通常用于函数式编程和 lambda 表达式的场景。实现这个接口的类可以通过 lambda 表达式或者函数引用来提供方法的具体实现。

fun interface GitHubService {
fun getUser(username: String): User
}

// 通过 lambda 表达式为 getUser 方法提供了具体的实现。
// lambda 表达式接收一个用户名参数,并返回对应的用户对象。
val service = GitHubService { username ->
// 通过用户名获取用户的具体实现
// ...
return user
}

常见使用场景


interface



  1. 定义回调接口:接口可以用作定义回调函数的契约。一个类可以实现接口并提供回调方法的具体实现,然后将实现类的实例传递给其他需要回调的组件。




  2. 实现多态行为:接口可以作为多态的手段,使得不同的类可以以不同的方式实现相同的接口。这种多态的特性允许在运行时根据对象的具体类型调用相应的方法。




  3. 定义服务接口:接口可以定义服务契约,描述系统的服务功能,并规定服务方法的签名。其他模块或组件可以实现接口,并提供具体的服务实现。




  4. 定义插件机制:接口可以用于定义插件的扩展点。主应用程序定义接口,并提供默认实现,而插件可以实现这个接口并提供自定义的行为。




  5. 实现策略模式:接口可以用于实现策略模式,其中不同的类实现相同的接口,并提供不同的算法或策略。




fun interface



  1. 定义函数式接口:函数式接口只包含一个抽象方法,通常用于表示某个操作或行为。这样的接口可以作为函数类型的参数或返回值,使得函数可以被传递、组合和使用。




  2. 使用 lambda 表达式:函数式接口可以通过 lambda 表达式提供方法的具体实现。这种方式使得代码更加简洁、易读,并支持函数式编程的风格。




  3. 支持函数引用:函数式接口可以与函数引用一起使用,允许直接引用已有的函数作为接口的实现。这样可以减少冗余的代码,并提高代码的可读性。




总而言之,interface 关键字适用于一般的接口定义和多态行为,而 fun interface 关键字则适用于函数式编程和 lambda 表达式的场景。
总结一下,interface 关键字用于定义常规的接口,可以包含抽象方法和默认方法。而 fun interface 关键字用于定义函数式接口,只能包含一个抽象方法,并且不能包含默认方法。


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

🎉学算法在业务开发中到底有没有用?

前言 作为一名大学多次打铁的前ACM-ICPC竞赛选手,对这个问题应该算多少有点话语权,首先先说一下结论:有用。抛开面试不谈,算法的收益可能没有学某项技术那么明显,它算是潜移默化的增强你的思维方式,拓展思路,分析业务逻辑的时候可能会更加迅速,处理起复杂业务相对...
继续阅读 »

前言


作为一名大学多次打铁的前ACM-ICPC竞赛选手,对这个问题应该算多少有点话语权,首先先说一下结论:有用。抛开面试不谈,算法的收益可能没有学某项技术那么明显,它算是潜移默化的增强你的思维方式,拓展思路,分析业务逻辑的时候可能会更加迅速,处理起复杂业务相对更加得心应手一些。


如果要说在业务开发中使用过哪些算法,那基本可以说是使用不上,使用上的也是一些相对基础的算法,像什么并查集、最小生成树、最短路径、图论等等当时学的时候抓耳挠腮的高级算法基本都用不到。


一些基础的算法还是能够经常遇到的,今天来盘一下业务开发中常见的算法。


桶排序


维基百科上的解释为,桶排序是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序)。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间O(n)


可以说是一堆废话了,根本看不懂,翻译成人话就是利用数组下标的有序性,通过空间换时间的思想,只要出现一次数就在数组对应的下标上+1,然后遍历数组中那些大于1即可。

public static void main(String[] args) {
int[] source = new int[]{8,3,7,12,5,6,9};
bucketSort(source);
}

public static void bucketSort(int[] array){
int[] reslut = new int[13];
for (int i = 0; i < array.length; i++) {
reslut[array[i]]++;
}

for (int i = 0; i < reslut.length; i++) {
for (int j = 1; j <= reslut[i]; j++) {
System.out.println(i);
}
}
}

这种思想在业务开发中经常会用到,可能不是在排序的场景,由于本身就很简单就不再举例子了。


DFS


Depth First Search深度优先搜索,简称DFS,通常会把数据抽象为一个树形结构,从根节点出发,按预定的顺序扩展到子节点,如果子节点还有子节点则继续递归这个过程,直到当前节点没有子节点。当到达最深的一个叶子结点后处理完当前节点逻辑,则需要返回上一个节点重新寻找一个新的扩展节点。如此搜索下去,直到找到目标节点,或者搜索完所有节点为止。


image.png


以这个树为例。假如要找的值为节点2,DFS会首先按一个路线走到不能再走,也就是0 -> 1 -> 3。因为节点3没有子节点了,DFS会回到上一级,也就是节点1的位置,然后按照另一条路走到黑。也就是0 -> 1 -> 3 -> 4。由于4没有子节点,DFS会回到节点1,然后节点1所有的子节点都已经去过了,于是乎再回到节点0,然后去到节点2,最终找到它,路线就是0 -> 1 -> 3 -> 4 -> 2。


我们可以用在一个真实的场景里,就拿文件夹与文件的结构,根据数据库三范式,我们简单定义一下文件夹与文件和文件夹关联关系

// 文件夹
public class Folder {
private String folderId;
private String parentId;
}

// 文件
public class File {
private String fileId;
private String fileName;
}

// 文件夹关联关系
public class FolderFielRel {
private String folderId;
private String fileId;
}


这样就定义出一个简单的文件夹结构实体,从结构上来看实现一个树形的逻辑还是很简单的。联表查询即可,用FolderFielRel关联出每个文件夹的文件,然后文件夹通过parentId组成树。


逻辑很清晰,但是如果产品老哥说每一层文件夹需要展示当前文件夹数量。很多同学肯定会脱口而出,让前端拿每一层的文件数量不完了吗。但如果老哥拿出不仅要当前文件夹的数量,还要知道本级文件夹及子级文件夹的文件数量,阁下应该怎么应对呢。


这时候就要DFS出手了,按照DFS的思路,如果要求文件夹本级及子级的文件数量,我们需要从以这个文件夹为根节点的子树的叶子结点开始处理。


我们假设最终VO为FolderTreeModel,并且已经组成了一个完整的树

public class FolderTreeModel {
private String folderId;
private String parentId;
private Integer fileCount;
private List<File> fileList;
private List<FolderTreeModel> subFolders;
}

dfs

private void dfsBuildFolderFileCount(Map<String, Integer> statBook,
FolderTreeModel rootTreeNode) {

List<Folder> sonTopicTrees = rootTreeNode.getSubFolders;
for (FolderTreeModel folderTreeModel : sonTopicTrees) {

this.dfsBuildFolderFileCount(statBook, folderTreeModel);
}

rootTreeNode.setFileCount(statBook.getOrDefault(rootTreeNode.getFolderId(), 0) + rootTreeNode.getFileCount());
statBook.put(rootTreeNode.getParentId(), statBook.getOrDefault(rootTreeNode.getParentId(), 0) + rootTreeNode.getFileCount());
}

statBook为记录每个文件夹的文件树,通过folder_id映射,当我们dfs到第一个叶子节点,我们把当前节点的文件数累加到当前节点的父级节点的id对应的文件数上。当整个dfs搜索结束之后每个节点的本级及子级的文件数都存在了statBook中。


BFS


bfs虽然好用,但他有个致命的弱点,时间复杂度高,按刚才的文件夹例子需要n*logn的复杂度。在一些性能要求较高的查询场景下基本上都不会用。


既然有深度优先搜索,那当然就得有广度优先搜索,广度优先搜索,顾名思义,跟深度的区别为处理数据以广度往外扩散,通常会借助队列先进先出的数据结构。


image.png


以上图为例也就是说,访问数据的过程为,1 -> 2,3 -> 4,5 -> 6。那么我们使用bfs来改写dfs中的统计文件夹树的代码。

private void buildFolderFileCount(Map<String, Integer> statBook,
FolderTreeModel rootTreeNode) {

LinkedBlockingQueue<FolderTreeModel> bfsQueue = new LinkedBlockingQueue<>(rootTreeNode.getSubFolders());

while (!bfsQueue.isEmpty()) {
FolderTreeModel firstObj = bfsQueue.poll();
statBook.put(firstObj.getParentId(), statBook.getOrDefault(firstObj.getParentId(), 0) + firstObj.getFileCount());
bfsQueue.addAll(firstObj.getSubFolders());
}
}

一波BFS下来,statBook的结果与DFS的结果一样。效率从n*logn直接飙升到n。除了这三个简单的算法之外实在想不到还有什么算法在日常的业务开发中使用的了。


算法的魅力还是很大的,大就大在学的时候难受的一比,用的时候拍案称奇,有时候在代码里露两手心里的成就感直接彪到Integer.MAX_VALUE


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

什么是序列化和反序列化?

1. 什么是序列化和反序列化? 序列化和反序列化是计算机科学中两个重要的概念,主要应用在数据存储和网络传输等场景。 序列化是将数据结构或对象状态转换为可以存储或传输的形式的过程。这种形式要求在重新构建原始对象时能够在其他环境或在程序运行的后续时间点使用。这个过...
继续阅读 »

1. 什么是序列化和反序列化?


序列化和反序列化是计算机科学中两个重要的概念,主要应用在数据存储和网络传输等场景。


序列化是将数据结构或对象状态转换为可以存储或传输的形式的过程。这种形式要求在重新构建原始对象时能够在其他环境或在程序运行的后续时间点使用。这个过程主要通过将对象的数据转化为字节流来实现,也可以将其转化为格式如 XML 或 JSON 的数据,以便在网络上进行传输或在磁盘上进行存储。


举个例子,假设你有一个复杂的数据结构,如一个包含多个字段和数组的对象。你不能直接将这个对象写入文件或通过网络发送。因此,你需要先将其转换为可以写入或发送的格式,这就是序列化。


反序列化是序列化的逆过程,也就是从一系列字节中提取出数据结构。在接收到序列化的数据(如从文件或网络)后,通过反序列化,可以将数据恢复为原始的对象或数据结构,从而可以在程序中使用。


以上述的例子,反序列化就是读取该文件或接收到的数据,并根据序列化时的格式将其恢复为原始的对象。


这两个过程在很多编程语言中都有内置的支持,例如在 Java 中,你可以使用 java.io.Serializable 接口来对对象进行序列化和反序列化;在 Python 中,你可以使用 pickle 模块进行序列化和反序列化;在 JavaScript 中,你可以使用 JSON 的 stringifyparse 方法进行序列化和反序列化等。


2. 在java中实现序列化和反序列化,为什么要实现Serializable接口?


Serializable 接口是一种标记接口,本身并没有定义任何方法,但是它向 JVM 提供了一个指示,表明实现该接口的类可以被序列化和反序列化。这意味着你可以将该类的对象转换为字节流(序列化),然后再将这个字节流转回为对象(反序列化)。


序列化的过程是 JVM 通过反射来完成的,它会查看对象的类是否实现了 Serializable 接口。如果没有实现,将会抛出一个 NotSerializableException 异常。


实现 Serializable 接口的主要原因如下:




  1. 允许 JVM 序列化对象:如上所述,JVM 只会序列化实现 Serializable 接口的对象。




  2. 表示类的实例可以被安全地序列化:实现 Serializable 接口的类表示它满足 JVM 对于序列化的要求。这不仅仅是类的实例可以被转换为字节流,还包括这个类的实例可以被反序列化,而且反序列化后的对象保持了原始对象的状态。




  3. 允许类的实例在 JVM 之间进行传输:序列化的一个重要应用是在网络应用或分布式系统中,允许对象在 JVM 之间进行传输。只有实现 Serializable 接口的对象才能通过网络进行传输。




  4. 持久化:序列化也被用于将对象的状态持久化,即将对象存储在数据库、文件或内存中,然后在需要的时候再进行恢复。实现 Serializable 接口的对象可以被持久化。




综上,实现 Serializable 接口是为了使对象可以被序列化和反序列化,以便在不同的环境或时间点恢复对象的状态,或在 JVM 之间传输对象,或将对象的状态持久化。


3. 案例


当然可以。下面这个简单的例子中,我们将创建一个实现了 Serializable 接口的类 Person,然后进行序列化和反序列化:


首先,我们创建一个实现了 Serializable 接口的 Person 类:

import java.io.Serializable;

public class Person implements Serializable {
private static final long serialVersionUID = 1L;

private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public int getAge() {
return age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

接下来,我们创建一个序列化这个 Person 对象的类 SerializeDemo

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class SerializeDemo {
public static void main(String[] args) {
Person p1 = new Person("John Doe", 30);

try {
FileOutputStream fileOut = new FileOutputStream("/tmp/person.ser");
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(p1);
out.close();
fileOut.close();
System.out.println("Serialized data is saved in /tmp/person.ser");
} catch (Exception e) {
e.printStackTrace();
}
}
}

现在我们来反序列化这个 Person 对象,创建一个类 DeserializeDemo

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class DeserializeDemo {
public static void main(String[] args) {
Person p = null;

try {
FileInputStream fileIn = new FileInputStream("/tmp/person.ser");
ObjectInputStream in = new ObjectInputStream(fileIn);
p = (Person) in.readObject();
in.close();
fileIn.close();
} catch (Exception e) {
e.printStackTrace();
}

System.out.println("Deserialized Person...");
System.out.println("Name: " + p.getName());
System.out.println("Age: " + p.getAge());
}
}

以上就是一个完整的 Java 序列化和反序列化的例子。首先我们创建了一个 Person 对象并序列化到一个文件中,然后我们从这个文件中读取数据并反序列化回 Person 对象。


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

为什么开发者应该多关注海外市场

这篇文章主要是分享一下为什么我认为作为国内的开发者,应该多关注海外市场、做面向海外用户的产品。 早在 2000 年(点 com 泡沫那阵),国外就有了 indie hacker / indie maker 这个名词来称呼做“独立”产品的人。这里的 indie...
继续阅读 »

这篇文章主要是分享一下为什么我认为作为国内的开发者,应该多关注海外市场、做面向海外用户的产品。




早在 2000 年(点 com 泡沫那阵),国外就有了 indie hacker / indie maker 这个名词来称呼做“独立”产品的人。这里的 indie 是 independent 的意思,意在独立(解脱)于各种束缚,比如:朝九晚五的工作时间、固定的办公室、领导、或者是投资人。


而国内在最近几年也涌现了一拨独立开发者,多数以工程师为主,当然做的产品也是面向国内的市场。有做地不错的,像 Baye 的熊猫吃短信、vulgur 的极简时钟、Kenshin 的简阅等;但综合我这两年来对海外一些独立产品的研究,海外市场或许是更好的选择。


当然凡是都有个前提,就是你没有一个豪华的创始团队或者是顶级投资人的背书,就是个人或者两三人的小团队。这个条件我觉得可以覆盖 90% 的中国的开发者;对于另外 10% 的拥有资源或者金主爸爸靠山的个人或者团队,不仅“可以”还“应该”去磕中国市场。但这不是今天要讨论的主题。


在 BAT TMD 等巨头和背靠资源的精英创业者们的夹缝里,我觉得只有做面向海外市场的小产品是更有胜率一点;做国内市场面临的四个问题:


第一、不存在足够的空间给个人/小团队做独立产品存活。


Slack 大家应该都知道,在美国已经上市了,市值 200 亿刀。Slack 一直是被 qiang 的,但是为什么国内没有出现 Slack 这样的产品作为一个信息中心来连接各个办公工具?


其实有,还不少,但都没活太久。一部分原因是腾讯阿里都非常重视这个“商业流量入口”,不想有可能被对方占有了。另外是国内互联网生态,从 BAT TMD 巨头到小软件公司,都太封闭;不仅不开放,还相互制约,都想把自己流量的守住,所以就同时出现了三个 Slack:



  • 微信出个企业微信(还封杀了 wetools)

  • 阿里出个钉钉

  • 字节出个飞书


在这种巨头虎视眈眈且相互对抗的格局里,作为三缺(缺钱、缺资源、缺核心门槛)的个人或者团队是无法存活的。或许在 2010 年至 2016 年间还有草根产品团队依靠“热钱”注入有爆发的可能性,时至今日,特别是这个蜜汁 2020 的局势,是不太可能的了。


即使,你找到了一个空白的利基市场(niche),你接下来面对三个问题:需求验证(试错)、推广、和商业化。


第二点、需求验证或者叫“试错”成本高。


由于国情不同,咱们需要经过一些不可避免的审核流程来保证互联网的干净。这个没话说,在哪做事就守哪的规矩。但这“需求验证”的门槛可就提高了不少。


比如要做个网站吧,备案最快也两周。做游戏?有没有版号?做 app ?有没有软著?小程序(从用户端来讲)是个不错的创新,但是你最烦看到的是不是“审核不通过,类目不符合”?稍微做点有用户互动的功能都需要公司主体。公司注册、银行开户、做帐、以及各种实名制等;这些虽然都不是不可达到的门槛,但是去完成这些要耗费大量的精力,对于本身就单打独斗的开发者来说 - 太累了。


再看看海外,简直不要太爽。做 app 还是需要经过苹果和谷歌的审核,但几乎不会对程序本身以外的东西设置门槛。网站注册个域名,30 秒改个 DNS 指到你的 IP,Netlify 或 Vercel 代码一推,就自动构建、部署、上线了。哪怕你不会写代码或者会写代码但是想先验证一下需求,看看潜在用户的响应如何,国外有不少非常值得一样的 no code 或 low code 平台。这个以后可以单独写一篇。


OK,国内你也通过重重难关项目终于上线了,你面临剩下的两个问题:推广和商业化。


第三点、推广渠道少 && 门槛高。


海外市场的推广渠道更多元,比如 ProductHunt, IndieHackers, BetaList 等。这些平台不仅国内没有,我想表达的更重要一点是,这些平台用户都比较真诚和热心会实在地给你提建议,给你写反馈。国内也有几个论坛/平台,但是用户氛围和友好度就和上述几个没法比了。


在付费推广方面,国内门槛(资质、资金)都挺高,感觉只有大品牌才能投得起广告,这对于缺资金的团队来讲,又是闭门羹。而国外,facebook 和 google 拿个 50、100 刀去做个推广计划都没问题。


可以以非常低的成本来获取种子用户或者验证需求。


行吧,推广也做得不错,得到了批种子用户并且涨势还不错;就最后一步了,商业化。


第四点、商业化选择少。


说商业化选择少可能说过了。国内是由于巨头间的竞争太激烈,出现各种补贴手段,导致互联网用户习惯于免费的互联网产品,甚至觉得应该倒贴给他来使用;伸手党、白 piao 党挺多;付费以及版权意识都还有改善空间。


想想你都给哪些浏览器插件付费过?“插件还需要付钱?!”


而海外用户的付费意愿足够强烈,在以后的案例分享中就能体会到。一个小小的浏览器插件,做得精美,触碰到了用户的购买欲望,解决了他一个痛点,他就愿意购买。


下一篇就分享国外浏览器插件的产品案例。


顺便再说一个「免费 vs 付费」的问题,这个不针对哪个国家,全世界都一样。免费(不愿意付费)的用户是最难伺候的,因为他们不 value 你的产品,觉得免费的就是创造者轻易做出来的、廉价的。如果依着这部分不愿意付费的客户来做需求,产品只会越做越难盈利。


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

一个悄然成为世界最流行的操作系统

一个悄然成为世界最流行的操作系统 1987 的今天,Minix 诞生了 Minix 介绍 Minix 是 Mini Unix 的缩写,一个迷你版类 Unix 操作系统(约 300MB)。 Minix 原来是荷兰阿姆斯特丹的 Vrije 大学计算机科学系的安德...
继续阅读 »

一个悄然成为世界最流行的操作系统


1987 的今天,Minix 诞生了


Minix 介绍


Minix 是 Mini Unix 的缩写,一个迷你版类 Unix 操作系统(约 300MB)。



Minix 原来是荷兰阿姆斯特丹的 Vrije 大学计算机科学系的安德鲁・塔能鲍姆(Andrew S. Tanenbaum )教授所开发的一个类 UNIX 操作系统,开发初衷是方便教学使用(因为 AT&T 推出 Version 7 Unix 之后,将 Unix 源码进行了私有化)。Minix 全部的源代码共约 12,000 行,并置于他的著作Operating Systems: Design and Implementation(ISBN 0-13-637331-3)的附录里作为范例。Minix 的系统要求在当时来说非常简单,只要三片磁片就可以启动。


安德鲁・塔能鲍姆(Andrew S. Tanenbaum,1944 年 3 月 16 日 ——)计算机科学家,阿姆斯特丹自由大学教授,专精操作系统,类 Unix 教学操作系统 Minix 作者,出版多部计算机科学教科书,如《现代操作系统》《计算机组成》等。



img



Minix 一开始向使用者收取极低的授权费,直到 2004 年,塔能鲍姆重新架构与设计了整个系统,更进一步的将程序模块化,推出 MINIX 3。重新以 BSD 许可协议发布,成为开放源代码软件。



MINIX 3 的目标是比 Windows 或 Linux 更安全,在当时塔能鲍姆那份获得欧盟研究委员会(EuropeanResearchCouncil)5 年 250 万欧元资助的研究计划书里,Tanenbaum 解释了为何他认为现有的操作系统不安全:



最严重的可靠性及安全问题是与操作系统相关的那些。核心问题在于现有操作系统都不符合 POLA —— 最低授权原则 (PrincipleOfLeastAuthority)。POLA 说的是系统划分组件的方式,应当使必然存在于某个组件中的缺陷,不至于波及其他组件。每个组件仅应该得到完成它本身工作所需的权限,不多不少。具体来说,它应该无权读写属于其他组件的数据,无权读取它自身地址空间之外的任何计算机内存,无 权执行与它无关的敏感操作指令,无权访问不该访问的 I/O 设备,诸如此类。现有操作系统完全违反以上原则,结果就是造成众多可靠性及安全问题。



Minix 的流行与威胁



说起最流行的操作系统,我们也许会下意识地想到 Linux、Windows、macOS、iOS 和 Android 等一些当下主流的操作系统。但事实恐怕不是我们以为的那样,你可能不知道,但在英特尔近些年推出的所有处理器中都运行着一个操作系统。



没错,这个系统正是MINIX,就是因为英特尔,它成了世界上最流行的操作系统,不过这引起了人们的注意和担忧。


img



之所以引起人们的担忧是因为现代英特尔处理器中都有一个核心部件 —— 英特尔管理引擎 (Intel ME-Intel's Management Engine),用来管理协调内部的诸多模块,尤其是传统芯片组整合进入之后,处理器已经差不多成了 SoC 单芯片系统,更需要一个 “总管”,MINIX 正是负责这个工作。


而一旦英特尔管理引擎受到危及,有可能给攻击者留下严重的后门。研究人员特别指出,由于其在初始化硬件、电源管理和启动主处理器等方面扮演重要角色,无法完全被禁用。这让安全研究人员甚为担忧,因为除了英特尔外,谁都无法审查有无后门(毕竟英特尔使用自己修改过的 MINIX 3 没有开源)



MINIX 在处理器内部拥有自己的 CPU 内核和专属固件,完全独立于其他部分,而且完全隐形,操作系统和用户均不可见,运行权限更是达到了 Ring -3。


img



要知道,我们日常使用的应用程序权限级别都是 Ring 3,操作系统内核的是 Ring 0,这也是一般用户能够接触到的最低权限,MINIX 竟然深入到了 Ring -3。


事实上,即便是在休眠乃至关机状态下,MINIX 都在不间断运行,因为英特尔管理引擎要在处理器启动的同时就开始执行管理工作,还要负责芯片级的安全功能。



这就使得 MINIX 拥有至高无上的地位,而且只要你的电脑使用的是英特尔近些年推出的处理器,都有一个它在默默运行,这使得它成为名副其实的世界上最流行的系统。


Minix 和 Linux



Linux 是Linus Torvalds受到 Minix 的影响而作成的(Linus 不喜欢他的 386 计算机上的 MS-DOS 操作系统,而安装了 Minix,并以它为样本开发了原始的 Linux 核心)。但是这种影响更多在于非技术层面,确切地说是一种精神上的 “鼓舞”。在设计上,Linux 则和 Minix 相差很大,在 Linux 系统还没有自己的原生文件系统之前,曾采用 Minix 的文件系统。Minix 在核心设计上采用微核心,即将操作系统分成微核心和其上的提供文件系统、存储器管理、驱动程序等服务的服务程序;而 Linux 则和原始的 Unix 都采用宏内核。在 Linux 发展之初,双方还于 1992 年在新闻组上有过一场精彩的争论,被称为塔能鲍姆 - 林纳斯辩论。Minix 的作者和支持者认为使用宏内核是技术上的退步,而 Linux 的支持者认为 Minix 本身没有实用性。


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

流量思维的觉醒,互联网原来是这么玩的

流量就是钱,这是一个很原始的认知。但最开始我并不清楚流量和钱之间是如何相互转化的。 微创业,认知很低 大学时期,不管是出于积累项目经验、还是折腾新技术的需要,我有做过一个相对完整的项目。 没记错的话,应该是在20年10月份启动的。当时在宿舍里买了一台激光打印机...
继续阅读 »

流量就是钱,这是一个很原始的认知。但最开始我并不清楚流量和钱之间是如何相互转化的。


微创业,认知很低


大学时期,不管是出于积累项目经验、还是折腾新技术的需要,我有做过一个相对完整的项目。


没记错的话,应该是在20年10月份启动的。当时在宿舍里买了一台激光打印机,做起了点小买卖。所以就发现如果我手动给同学处理订单会非常麻烦。他们把文件通过qq发给我,我这边打开,排版,确认格式没有问题之后算一个价格,然后打印。


所以根据痛点,我打算开发一个线上自助下单,商户自动打印的一整套系统。


百折不挠,项目终于上线


21年年中克服各种困难终于实现整套系统,提供了小程序端,商户客户端,web端。


用户在手机或网页上上传文件后会自动转换为pdf,还提供了在线预览,避免因为格式与用户本地不同的纠纷。可以自由调节单双面、打印范围、打印分数、色彩等参数。实时算出价格,自助下单。下单后服务器会通知商户客户端拉取新任务,拉取成功后将文件丢入打印队列中。打印完成后商户客户端发送信息,并由服务器转发,告知用户取件。


image.png


image.png


大三下学期,宿舍里通过线上平台,在期末考试最忙那段期间经过了“订单高峰”的考验,成交金额上千块钱。看着我商户端里面一个个跳动的文件,就像流入口袋里的💰,开心。


商业化的很失败


没想到,我自己就是我最大的客户。


期末考完,其实想拉上我的同学大干一场,让校里校外的所有的商户,都用上我们的软件,多好的东西啊。对于盈利模式的概念非常模糊,同时也有很强的竞品。我的同学并不看好我。


我对商业化的理解也源自美团模式,美团是外卖的流量入口,所以对商户抽佣很高。滴滴是打车的流量入口,对司机的抽佣也很高。所以我认为,假设我未来成为了自助打印的流量入口,那应该也可以试试抽佣模式。


而且就算我不能为商户引流,也能解放他们的双手。


当时的我,一个人做技术,做UI,还要做商业计划,去地推,真的搞得我精疲力尽。反正后面觉得短期内变现无望,就去腾讯实习了。


其实也推广了2个商户,但是他们因为各种原因不愿意用。一个是出于隐私合规风险的考虑,一个是订单量少,不需要。


所以基本这个自助打印只能框死在高校。大学生打印的文件私密性很低,但是单价低,量多,有自助打印的需求。还有一部分自助打印的场景是在行政办事大厅,这种估计没点门门道道是开不进去的。


看不懂的竞品玩法


商户通过我的平台走,我这边并不无本万利。


因为开通了微信支付、支付宝支付,做过的小伙伴应该都知道办这些手续也会花一些钱,公司还要每年花钱养。还有需要给用户的文档成转换成pdf,提供在线预览,这很消耗算力和带宽,如果用户的成交单价非常低,哪怕抽佣5%都是亏的。比如用户打印了100份1页的内容,和打印了1份100页的内容,对我来说成本差别很大,前者很低,后者很高。


当时学校里已经有一部分商户用上自助打印了。一共有3个竞品。


竞品A:不抽佣,但是每笔订单对用户收取固定的服务费,界面简陋,有广告。


竞品B:不抽佣,不收用户的服务费,界面清爽无广告。


竞品C:彻彻底底走无人模式,店铺内基本没有老板,店铺是自营或加盟的。


前期缺乏市场调研,后期缺乏商业认知


当时我在没有摸清自己商业模式,市场调研也没怎么做好的情况下。一心想的就是先把东西做出来再说,卖不成自己还能学到技术。毕竟技术这个玩意不在项目里历练,永远都是纸上谈兵。所以对于商业化的设想就是搞不成就不搞了。


我当时的想法就是要“轻”运营,就是最好我的利润是稳定的,不会亏损的。商户如果要用就得每笔订单都给我一笔钱。


后面为了补齐和竞品的功能差距,也耗费了大量心力。让我把项目从一个大学课程设计,变成了一个有商业化潜力的产品。


竞品玩法的底层逻辑


商业化的时候,就发现这个市场还是蛮卷的,不可能直接和商户收钱。竞品B不仅免费,还想着帮商户创造额外收入,做“增益”。那我确实是没有精力去对抗的。


我当时也没搞懂自己的定位,我究竟是tob还是toc。当时想着我精心设计的界面,怎么可以被广告侵蚀?那可是我的心血。所以一心想把产品体验做的比竞品好,就会有人用。但这个定位也很模糊,因为如果商户不用你的,用户怎么可能用你的下单呢。


其实应该to rmb。面向利润开发。美,是奢侈品,那是属于我内心的一种追求,但他很难具有说服力让商户使用。在国内的各种互联网产品,不盈利的产品最后都是越来越粗糙,越来越丑的,都要降本增效。而rmb是必需品,如果不能为各方创造价值,那就没有竞争力。


所以后续分析了一下各家的玩法:


竞品A:传统商业模式,依靠用户强制付费和广告,市占率一般,和第一差了10倍数量级。


竞品B:烧钱模式,免费给商户用,免费给用户用,自己想办法别的渠道做增益,还要补贴商户。市占率第一。先圈地,再养鱼,变现的事之后再说。


竞品C:不单单做打印软件,卖的是项目。一整套自助打印店的解决方案,不知道店铺能不能赚钱,但是可以先赚加盟商的钱。这个对商业运作的要求会很高,我一时半会做不了。


大佬指点了一下我


他说,你看现在什么自助贩卖机,其实就是一个流量入口。至于别的盈利不盈利再说,但是流量是值钱的。


我最近去查阿拉丁指数,了解到了买量和卖量的观念,重新认识了流量,因为知道价格了。


买量和卖量是什么?


买量说的就是你做了一个app,花钱让别人给你引流。


卖量就是你有一个日活很高的平台,可以为别人引流。


买量和卖量如何结算?


一般分为cpc和cpa两种计价方式。前者是只要用户点击了我的引流广告,广告主就得掏钱。后者是用户可能还需要注册并激活账号,完成一系列操作才掏钱。


一般价格在0.1-0.3元,每次引流。


后面我查了一下竞品B在卖量,每天可以提供10-30w的uv,单次引流报价0.1元。也就是理想情况下,每天可以有1-3w的广告费收入。


侧面说明了竞品B的市占率啊,在这个细分市场做到这个DAU……


关于流量,逆向思维的建立


流量是实现商业利益的工具。


工具类应用通过为别人引流将流量变现,内容类应用通过电商将流量变现的更贵。


依靠流量赚钱有两种姿势,主动迎合需求,和培养需求。前者就是你可以做一些大家必须要用的东西,来获得流量。比如自助打印小程序,只要商户接入了,那么他的所有顾客都会为这个小程序贡献流量。比如地铁乘车码,所有坐地铁的人都会用到,比如广州地铁就在卖量,每天有几百万的日活。


培养需求就是做自己看好的东西,但是当下不明朗,尝试发掘用户潜在的需求。


流量,如果不能利用好,那就是无效流量。所以正确的姿势是,发掘目标人群 -> 设计变现方案 -> 针对性的开发他们喜欢的内容或工具 -> 完成变现。而不是 自己发现有个东西不错 -> 开发出来 -> 测试一下市场反应 -> 期盼突然爆红,躺着收钱。


研究报告也蛮有意思,主打的就是一个研究如何将用户口袋里的钱转移到自己口袋里。做什么产品和个人喜好无关,和有没有市场前景相关。


互联网是基于实体的


互联网并不和实体脱钩,大部分平台依赖广告收入,但广告基本都是实体企业来掏钱。还有电商也是,消费不好,企业赚不到钱,就不愿意投更多推广费。


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

程序员的快乐与苦恼

随着大环境的下行,互联网行业也受到一定的冲击,哀鸿遍野。 笔者也没有幸免,培养起来的人马陆续被优化,留下一丢光杆司令,我也回到的业务一线,心里很不是滋味。留下来的人,也不知道这艘船什么时候会沉没… 为了活命而拼命挣扎(内卷) 负面情绪和焦虑不停侵扰,以至于怀疑...
继续阅读 »

随着大环境的下行,互联网行业也受到一定的冲击,哀鸿遍野。


笔者也没有幸免,培养起来的人马陆续被优化,留下一丢光杆司令,我也回到的业务一线,心里很不是滋味。留下来的人,也不知道这艘船什么时候会沉没… 为了活命而拼命挣扎(内卷)


负面情绪和焦虑不停侵扰,以至于怀疑,当初选的这条路是不是正确的。


捡起买了多年,但是一直没看的《人月神话》, 开篇就讲了程序员这个职业的乐趣和苦恼,颇有共鸣,所以拿出来给大家分享


不管过去多少年,不管你的程序载体是纸带、还是 JavaScript,不管程序跑在高对比(high contract)的终端、还是 iPhone,程序员的快乐和烦恼并没有变化。


尽管国内软件行业看起来不是那么健康。我相信很多人真正热爱的是编程,而不仅仅是一份工作,就是那种纯粹的热爱。你有没有:



  • 为了修改一个 Bug,茶饭不思

  • 为了一个 idea,可以凌晨爬起来,决战到天亮

  • 我们享受没有人打扰的午后

  • 梦想着参与到一个伟大的开源项目

  • 有强烈的分享欲,希望我们的作品可以帮助到更多人, 希望能得到用户的反馈,即使是一个点赞







我们的快乐



《人月神话》:


首先,这种快乐是一种创建事物的纯粹快乐。如同小孩在玩泥巴时感到快乐一样,成年人喜欢创建事物,特别是自己进行设计。我想这种快乐是上帝创造世界的折射,一种呈现在每片独特的、崭新的树叶和雪花上的喜悦。


其次,这种快乐来自于开发对他人有用的东西。内心深处,我们期望我们的劳动成果能够被他人使用,并能对他们有所帮助。从这一角度而言,这同小孩用粘士为“爸爸的办公室”捏制铅笔盒没有任何本质的区别。


第三,快乐来自于整个过程体现出的一股强大的魅力——将相互啮合的零部件组装在一起,看到它们以精妙的方式运行着,并收到了预期的效果。比起弹球游戏机或自动电唱机所具有的迷人魅力,程序化的计算机毫不逊色。


第四,这种快乐是持续学习的快乐,它来自于这项工作的非重复特性。人们所面临的问题总有这样那样的不同,因而解决问题的人可以从中学习新的事物,有时是实践上的,有时是理论上的,或者兼而有之。


最后,这种快乐还来自于在易于驾驭的介质上工作。程序员,就像诗人一样,几乎仅仅在单纯的思考中工作。程序员凭空地运用自己的想象,来建造自己的“城堡”。很少有创造介质如此灵活,如此易于精炼和重建,如此容易实现概念上的设想(不过我们将会看到,容易驾驭的特性也有它自己的问题)。


然而程序毕竞同诗歌不同,它是实实在在的东西;它可以移动和运行,能独立产生可见的输出;它能打印结果,绘制图形,发出声音,移动支架。神话和传说中的魔术在我们的时代已变成现实。在键盘上键入正确的咒语,屏幕会活动、变幻,显示出前所未有的也不可能存在的事物。





编程就是一种纯粹创造的快乐,而且它的成本很低,我们只需要一台电脑,一个趁手的编辑器,一段不被人打扰的整块时间,然后进入心流状态,脑海中的想法转换成屏幕上闪烁的字符。
这是多巴胺带给我们的快乐。


飞机引擎






我们也有「机械崇拜」,软件不亚于传统的机械的复杂构造。 它远比外界想象的要复杂和苛刻,而我们享受将无数零部件有机组合起来,点击——成功运行的快感。


我们享受复杂的问题,被抽象、拆解成一个个简单的问题, 认真描绘分层的弧线以及每个模块轮廓,谨慎设计它的每个锯齿和接口。


我们崇尚有序,赞赏清晰的边界, 为的就是我们创造的世界能够稳定发展。




我们认为懒惰是我们的优点,我们也崇拜自动化,享受我们数据通过我们建设的管道在不同模块、系统或者机器中传递和加工;享受程序像多米诺骨牌一样,自动构建、测试、发布、部署、分发到每个用户的手中,优雅地跑起来。


因为懒,我们时常追求创造出能够取代自己的工具,让我们能腾出时间在新的世界探索。比如可以制造出我们的 Moss,帮我们治理让每个程序的生命周期,让它们优雅地死去又重生。




我们是一群乐于分享和学习的群体,有繁荣的技术社区、各种技术大会、技术群…


不管是分享还是编程本身,其实都是希望我们的作品能被其他人用到,能产生价值:



  • 我们都有开源梦,多少人梦想着能参与那些广为人知开源项目。很少有哪个行业,有这么一群人, 能够自我组织,用爱发电、完全透明地做出一个个伟大的作品。

  • 我们总会怀揣着乐观的设想,基于这种设想,我们会趋向打造更完美的作品,想象未来各种高并发、极端的场景,我们的程序能够游刃有余。

  • 我们总是不满足于现有的东西,乐于不停地改进,造出更多的轮子,甚至不惜代价推翻重来

  • 我们更会懊恼,自己投入大量精力的项目,无人问津,甚至胎死腹中。




看着它们,从简单到繁杂,这是一种迭代的快乐。








我们的苦恼



《人月神话》
然而这个过程并不全都是快乐的。我们只有事先了解一些编程固有的苦恼,这样,当它们真的出现时,才能更加坦然地面对。


首先,苦恼来自追求完美。因为计算机是以这样的方式来变戏法的: 如果咒语中的一个字符、一个停顿,没有与正确的形式一致,魔术就不会出现(现实中,很少有人类活动会要求如此完美,所以人类对它本来就不习惯)。实际上,我认为,学习编程最困难的部分,是将做事的方式向追求完美的方向调整"。




其次, 苦恼来自由他人来设定目标、供给资源和提供信息。编程人员很少能控制工作环境和工作目标。用管理的术语来说,个人的权威和他所承担的责任是不相配的。不过,似乎在所有的领域中,对要完成的工作,很少能提供与责任相一致的正式权威。而现实情况中,实际(相对于形式)的权威来自于每次任务的完成。


对于系统编程人员而言,对其他人的依赖是一件非常痛苦的事情。他依靠其他人的程序,而这些程序往往设计得并不合理、实现拙劣、发布不完整(没有源代码或测试用例)或者文档记录得很糟。所以,系统编程人员不得不花费时间去研究和修改,而它们在理想情况下本应该是可拿的、完整的。




下一个苦恼 —— 概念性设计是有趣的,但寻找琐碎的bug却是一项重复性的活动。伴随着创造性活动的,往往是枯燥沉闷的时间和艰苦的劳动。程序编制工作也不例外。




另外,人们发现调试和查错往往是线性收敛的,或者更糟糕的是,具有二次方的复杂度。结果,测试一拖再拖,寻找最后一个错误比第一个错误将花费更多的时间。




最后一个苦恼,有时也是一种无奈 —— 当投入了大量辛苦的劳动,产品在即将完成或者终于完成的时候,却己显得陈旧过时。可能是同事和竞争对手己在追逐新的、更好的构思;也许替代方案不仅仅是在构思,而且己经在安排了。





前阵子读到了 @doodlewind全职开源,出海创业:我的 2022,说的是他 all in 去做 AFFiNE 。我眼里只有羡慕啊,能够找到 all in 的事业…






这些年 OKR 也很火,我们公司也跟风了一年; 后面又回到了 KPI,轰轰烈烈搞全员KPI, 抓着每个人, 要定自己的全年KPI; 再后来裁员,KPI 就不再提起了…


这三个阶段的演变很有意思,第一个阶段,期望通过 OKR 上下打通,将目标捆在一起,让团队自己驱动自己。实际上实施起来很难,让团队和个人自我驱动起来并不是一件容易的事情,虽然用的是 OKR,但内核还是 KPI,或者说 OKR 变成了领导的 OKR。


后面就变成了 KPI, 限定团队要承担多少销售额,交付多少项目;


再后来 KPI 都没有了,换成要求每个人设定自己工作日历,不能空转,哪里项目缺资源,就调配到哪里,彻底沦为了人矿…




能让我们 all in 的事情,首先得是我们认同的事情,其次我们能在这件事情上深度参与和发挥价值,并获得预期的回报。这才能实现「自我驱动」


对于大部分人来说,很少有这种工作机会,唯一值得 all in的,恐怕就只有自己了。






所以程序员的苦恼很多,虽然编程是一个创造性的工作,但是我们的工作是由其他人来设定目标和提供资源的。


也就是说我们只不过是困在敏捷循环里面的一颗螺丝钉,每天在早会上机械复读着:昨天干了什么,今天要干什么。


企业总会想法设法量化我们的工作,最好是像流水线一样透明、可预测。




培训机构四个月就能将高中生打造成可以上岗敲代码的程序员。我们这个行业已经不存在我们想象中高门槛。


程序员可能就是新时代的蓝领工人,如果我们的工作是重复的、可预见的,那本质上就没什么区别了。






追求完美是好事,也是坏事。苛刻的编译器会提高开发的门槛,但同样可以降低我们犯错的概率。


计算机几乎不会犯错的,只是我们不懂它,而人经常会犯错。相比苛刻的计算机,人更加可怕:



  • 应付领导或产品拍脑袋的需求

  • 接手屎山代码

  • 浪费时间的会议

  • 狼性文化











还有一个苦恼是技术的发展实在太快了,时尚的项目生命周期太短,而程序员又是一群喜新厌旧的群体。


比如在前端,可能两三年前的项目就可以被定义为”老古董”了,上下文切换到这种项目会比较痛苦。不幸的是,这些老古董可能会因为某些程序员的偏见,出现破窗效应,慢慢沦为屎山。


我们虽然苦恼于项目的腐败,而大多数情况我们也是推手。




我们还有很多苦恼:



  • 35 岁危机,继续做技术还是转管理

  • 面试的八股文

  • 内卷

  • 被 AI 取代







对于读者来说,是快乐多一些呢?还是苦恼多一些呢?


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