注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

一次移动端性能优化实践

web
背景 使用低代码接入了一个移动端老系统,新增页面(以及部分旧页面)使用低代码体系搭建。功能开发完成后才发现性能问题比较严重,所以进行了一次移动端的性能优化。本文就优化过程进行一个记录。 问题分析 为什么一接入低代码体系性能(主要是加载性能)就出现明显的下降,如...
继续阅读 »

背景


使用低代码接入了一个移动端老系统,新增页面(以及部分旧页面)使用低代码体系搭建。功能开发完成后才发现性能问题比较严重,所以进行了一次移动端的性能优化。本文就优化过程进行一个记录。


问题分析


为什么一接入低代码体系性能(主要是加载性能)就出现明显的下降,如果首屏访问的是低代码页面则更加明显



  • 最主要的原因是比之前额外加载了大量的 js 和 css,初步统计有 10 个 css 和 15 个 js

  • 老系统自身 js 资源过大,依赖包 vendor.js 有 8M 多

  • 低代码体系下,非静态资源的接口请求也成为影响页面渲染的因素。页面必须等待接口获取到 schema 后才由低代码渲染器进行渲染


低代码体系接入


有必要简单说明下低代码体系是如何接入的,这对后面的优化是有直接影响的



  • 低代码体系资源大概分为三方依赖、渲染引擎和组件库资源,都是独立的 npm 库,发布单独的 CDN

  • 三方依赖就是像 react、moment、lodash 等最基础的依赖资源

  • 渲染引擎要想渲染页面,又直接依赖于两个资源

    • 页面 schema:服务端接口返回,schema 本质上是一个 json,描述了一个组件树

    • 组件集合:由 CDN 引入的各个组件库集合,它需要先于页面 schema 加载




静态资源为何影响加载性能


静态资源加载如何影响性能,简单分析下,详细的原理可以参考 MDN



  • HTML 自上而下解析,遇到 script 标签(不带 defer 和 async 属性)就会暂停解析,等待 script 加载和执行完毕后才会继续

  • HTML 解析时如果遇到 css 资源,解析会继续进行。但是在 css 资源加载完成前,页面是不会渲染的,并且如果此时有 JavaScript 正在执行,也会被阻塞

  • 所以 js 或 css 体积越大,则在网络传输、下载、浏览器解析和执行上所花的时间就会相应的增加,而这些时间都是会阻塞页面渲染的

  • js 或者 css 的个数对于渲染的影响,很大程度上取决于项目和浏览器是否支持 http2

    • 如果使用了 http2,则静态资源个数对于加载性能影响不大,除非多到几百个资源

    • 如果还是 http1.1,静态资源个数对于加载有明显影响,因为此时浏览器存在并发限制,大概在 4-6 个左右,即一批次只能发送几个请求,等到请求完成后,再发下一批,是个同步的过程

    • 本项目已经支持 http2,所以优化加载性能的重点还是在减小总的资源体积上




优化指标


用户对于页面性能的感受是主观的,而优化工作则需要客观的数据。
更重要的是,有些优化措施是否有效果,有多少效果是需要数据说明的。举例来说,去除冗余资源几乎是可以预见性能提升。但是做 CDN 合并在移动端能够有多少优化效果,事前其实并不清楚
这里采用 2 种方式作为优化指标



  • 旧版本 chrome(69)的 perfomance

    • 使用这个版本是因为后台数据显示该引擎访问量较多

    • chrome 的 performance 不仅能获取性能数据,也有助于我们分析,找出具体问题



  • 使用 web-vitals 库获得具体的性能数据,主要关注

    • FCP,白屏时间

    • LCP,页面可视区域渲染完成时间




现状


image.png



  • 点击 performance 的刷新按钮,就会自动进行一次页面的加载

    • 建议使用无痕模式,排除其他干扰

    • network 中勾选 Disable cache,虽然最终用户会用到缓存,但在优化非缓存项时,建议先禁用缓存,获取最真实的数据



  • 静态资源的加载大概花了 3.5s

  • 而后续静态资源的解析则一直持续到页面加载完成,大概在 9 秒多

  • 使用 web-vitals 测量的平均数据

    • FCP: 5.5s

    • LCP: 9s




目标



  • performance 页面渲染完成:4s 以内

  • web-vitals 平均数据

    • FCP:3s 以内

    • LCP:4s 以内




如果从绝对性能看,这个目标只能是个中下水平。主要基于以下几点考虑



  • 策略上不会对原系统或者低代码体系进行大刀阔斧的改动

  • 老系统大概就是这么个性能情况,维持这个水平起码不会降低用户体验。作为内部系统,对性能没有极致的要求

  • 考虑到时间成本,性能优化是一项持续性的工作,而实际项目是有时间限制和上线压力的


优化措施


根据以上分析,最重要的就是要减小总的关键资源体积。
低代码体系所需要的直接资源都属于关键资源。因为用户是可能首次直接进入一个低代码页面的(也是本次主要的优化场景)


优化前包分析


CDN 三方库资源直接就能看出哪些是冗余的,或者是公共资源加载了多遍等问题,但是自己的仓库打包后就需要借助 webpack-bundle-analyzer 插件分析了
该项目中有多个 npm 仓库需要分析,这里就举老系统自己的例子,优化前的 bundle 分析图


image.png


三方依赖 vendor.min.js 8MB 左右,项目 JS 800 多 KB,下面分析下最严重的几点



  • 标 ① 部分, @ali_4ever 开头的是富文本依赖,有接近 2M 左右的大小,优化为懒加载

  • 标 ② 部分,echarts5 全量引入了,1M 左右大小,计划优化为按需加载

  • 标 ③ 部分,ali-oss,500 多 KB,ali-oss 不支持按需引入。这里因为多个低代码组件库中也用到了该依赖,所以计划提取为 CDN 作为公共依赖,但是大小还是 500 多 KB,只是去掉了重复加载部分

  • 标 ④ 部分,antd-mobile 加载了两个版本的全量仓库,按照官方推荐,考虑将 antd-mobile-v2 按需加载


一、移除冗余资源



  • 排查 CDN,是否引用了多余的 CDN,比如项目中移动端引用了 PC 端的组件库,引用了已经废弃(迁移)的工具库等等

  • 排查项目 bundle,正常情况下是不可能有冗余资源的,因为如果一点没用到这个库,webpack 也不会将其打包进去

    • 可能存在使用到了一小部分,却打包了整个库的情况,这个属于下一部分按需引入



  • 排查下线上 CDN 是否都使用生产版本或者压缩版本,这点事先没有想到,是在优化过程中意外发现存在非压缩版本


二、按需引入


按需引入即只引入三方库中项目用到的部分。现代的大部分三方库都已经支持 TreeShaking,正常打包即是按需引入。特殊情况在于 CDN、懒加载和一些老的库,这些刚好在项目中都有所实践


按需引入 和 CDN


项目中只用到了 ahooks 中的个别方法,却将整个包作为 CDN 引入,显然是不合理的



  • 需要按需引入的库,是不能使用 CDN 引入的,它们之间是互斥的

    • 因为 CDN 需要配置 external 才能在项目里使用,external 一般是将一个三方库作为整体配置的



  • CDN 自身作为一种优化手段,那是和将静态资源放置在业务服务器对比的。

    • 在该场景下,引入 ahooks CDN 导致 TreeShaking 失效,引入了全量包,同时增加了一次 http 请求,总的来看肯定是得不偿失的

    • 并且最终项目的 bundle 也会发布 CDN



  • 因此去掉了 ahooks 的 CDN,改为直接打进项目 bundle 就行了


按需引入 和 懒加载


在该项目中,echarts 也按需引入了,echarts 的按需引入总体效果就没有 ahooks 那么好了



  • echarts 无论绘制哪种类型图表,都需要引入核心库,就有 100 多 KB 的大小了

  • 所以 echarts 也可以选择懒加载,懒加载会让没有使用 echarts 的页面加载速度变快,但是最终浏览器解析的资源是全量的,可以根据实际情况选择

  • 懒加载 和 按需引入也无法并存。因为懒加载需要动态导入,动态导入 webpack 就没法做静态分析,这是 TreeShaking 的基础,所以就没法按需引入了


利用 babel-import-plugin


有一些老版本的库,可能还不支持按需引入,比方说 antd-mobile-v2,对于这种仓库,可以利用 babel-import-plugin 做按需引入
只需要做一下 babel 配置就行


{
"plugins": [
[
"import",
{
"libraryName": "antd-mobile-v2",
"style": "css"
},
"antd-mobile-v2"
]
]
}



  • 本项目最终没有那么做,因为体积几乎没有减小。对于一个完整的项目,需要使用到的组件是非常多的

  • 对于 antd-mobile 多个版本的问题,最终的优化方案还是合并为最新版,只是开发和测试的工作量大了点

  • 注意点:babel-import-plugin 插件并不能让所有仓库都支持按需。本质上还是三方库做了分包才行


三、懒加载


懒加载的资源不同,也可以分为多种类型



  • 三方库资源懒加载:比如之前说的,某个组件依赖于 echarts,那么就可以懒加载 echarts,只有页面中使用了该组件时才去请求和加载 echarts 依赖

  • 组件懒加载:将整个组件都懒加载,在本项目中没有做组件懒加载

    • 低代码体系下,组件本身不能懒加载,否则 schema 解析到这个组件时找不到会报错

    • 解决方案也可以给组件套一层,实际内容懒加载,导出的组件不懒加载

    • 更重要的原因是组件库本身不大,不是影响性能的关键因素

    • 另外低代码页面本身就是由各个组件拼凑而成,如果将组件都懒加载了,那么页面各个部分都会有 Loading 的中间态,效果不好把控



  • 路由懒加载:本质上它就是组件懒加载的一种,一个组件就是一个路由页面,项目中对于系统不太访问的页面做了路由懒加载


三方库资源懒加载


懒加载依赖也需要分析具体情况,比方说移动端使用了 antd-mobile 作为组件库,这个依赖就完全没必要等使用的时候再加载。因为几乎进入任意一个页面,都需要用到这个资源。什么情况下合适



  • 依赖资源比较大

  • 使用的频率较低,只在个别地方使用了


并且这个三方资源也是分两种情况引入,第一种是以 CDN 的形式外部引入,第二种是直接打包入库,这两种引入方式的懒加载处理是不同的,下面分别举例


CDN 引入的三方资源懒加载


比如低代码组件库中存在一个富文本组件,比较特殊,比较适合使用 CDN 的方式懒加载依赖资源



  • 富文本组件依赖于公司内部的一个富文本编辑器。鉴于富文本的复杂性,所以它的依赖很大,JS+css 将近有 3M 左右。

  • 但是其实只有极少的页面使用到了富文本,对于大多数用户来说,是不需要这个富文本的


下面介绍下具体实现,利用 ahooks 的 useExternal,动态注入 js 或 css 资源(也可以原生实现),封装一个高阶组件,方便调用


type LoadStatus = 'loading' | 'ready' | 'error';
interface LoadOptions {
url: string;
libraryName: string;
cssUrl?: string;
LoadingRender?: () => React.ReactNode;
errorRender?: () => React.ReactNode;
}
export const LazyLoad = (Component, { url, libraryName, cssUrl, LoadingRender, errorRender }: LoadOptions) => {
const LazyCom = (props) => {
const initStatus = typeof window[libraryName] === 'undefined' ? 'loading' : 'ready';
const [loadStatus, setStatus] = useState<LoadStatus>(initStatus);
const jsStatus = useExternal(url, {
keepWhenUnused: true,
});
const cssStatus = useExternal(cssUrl, {
keepWhenUnused: true,
});

useEffect(() => {
if (loadStatus === 'ready' || loadStatus === 'error') {
return;
}
if (jsStatus === 'error' || cssStatus === 'error') {
setStatus('error');
}
if (jsStatus === 'ready' && (cssStatus === 'ready' || cssStatus === 'unset')) {
setStatus('ready');
}
}, [jsStatus, cssStatus, loadStatus]);

const content = useMemo(() => {
switch (loadStatus) {
case 'loading':
return typeof LoadingRender === 'function' ? LoadingRender() : <div>加载中...</div>;
case 'ready':
return <Component {...props} />;
case 'error':
return typeof errorRender === 'function' ? errorRender() : <div>加载失败</div>;
default:
return null;
}
}, [loadStatus]);

return content;
};
return LazyCom;
};

// 使用示例,BaseEditor即需要懒加载的原组件,BaseEditor组件内部直接通过window取相应依赖
export const FormEditor = LazyLoad(BaseEditor, {
url: 'xxxx',
cssUrl: 'xxxxx',
libraryName: 'xxxxxx',
});

打包入 bundle 依赖懒加载


总体思路是一样的,只是这类资源利用 webpack 的 import 动态导入能力,import 动态导入的资源打包时会单独分包,只在使用到时才会加载
具体实现:


export const InnerLazyLoad = (Component, loadResource, LoadingRender?) => {
const LazyCom = (props) => {
const [loaded, setLoaded] = useState(false);
const [LazyResource, setResource] = useState({});

useEffect(() => {
if (loaded) {
return;
}
loadResource().then((resource) => {
setResource(resource);
setLoaded(true);
});
}, [loaded]);
const LoadingNode = typeof LoadingRender === 'function' ? LoadingRender() : <div>...加载中</div>;
return loaded ? <Component {...props} LazyResource={LazyResource} /> : LoadingNode;
};
return LazyCom;
};

// 具体使用
const loadResource = async () => {
// 动态导入的资源会单独分包,在使用到时才会加载
const echarts = await import('echarts/core');
const { PieChart } = await import('echarts/charts');
const { TitleComponent } = await import('echarts/components');
const { CanvasRenderer } = await import('echarts/renderers');
return {
echarts,
PieChart,
TitleComponent,
CanvasRenderer,
};
};

const AgentWork = InnerLazyLoad(BaseAgentWork, loadResource);

路由懒加载


路由懒加载原理和内部资源懒加载类似,分包然后首次进入该页面时才请求页面资源
本项目没有把所有页面都懒加载



  • 页面懒加载后,进入页面前会有一个短暂的加载过程,需要评估影响

  • 还是和通用懒加载一样,使用频率较低、页面 js 又比较大的比较适合懒加载


比如在该项目中



  • 应用上存在部分页面是给第三方使用的,不能通过导航点击到达,直接分享地址给第三方

  • 这些页面使用频率低,而且基本不影响本应用,因为无法通过导航点击切换到达,是通过 url 的形式直接访问,所以加载中的中间态和页面加载一起


路由懒加载的实现,不同框架都有些差异。本项目中只需在路由配置中增加配置项即可开启,就不再阐述具体代码实现


四、合并公共资源


合并公共资源,即不要重复加载相同资源
一般来说打包工具都会做依赖分析,只会打包一份相同路径的引用依赖。但是如果相同依赖分散在多个仓库中就有可能出现重复资源了
比如该项目中,老系统自身和多个组件库都使用了 ali-oss 库实现上传功能,并且还有一些条件使得将其提取为公共 CDN 是利益最大化的



  • ali-oss 打包后 500 多 KB 的大小,已经算是一个不小的包了

  • ali-oss 不支持按需引入,所以引用到它的多个仓库,无论引用了什么功能,都将全量打包入 ali-oss

  • 如果 ali-oss 支持按需引入,就需要计算是提取为公共 CDN 划算,还是将其按需打入各个仓库中划算


实现步骤比较简单



  • 在引用 ali-oss 的仓库配置 external,使仓库本身打包时不打入 ali-oss 依赖

  • 在项目 HTML 中提前引入 ali-oss CDN


五、缓存


静态资源缓存



  • 该项目静态资源使用 CDN+版本号,本身已经支持了缓存。CDN 的缓存时间是通过 Cache-Control 的 s-maxage 字段控制,这是 CDN 特有的字段

  • 如果静态资源是放置在自己的服务器上,需要考虑 http 缓存和缓存更新的事项,这个也是老生常谈的话题,这里不再赘述


如果想要详细了解 http 缓存,推荐看下这篇文章


options 请求缓存


在实际优化过程中发现,该项目的大部分 ajax 请求,都是跨域请求,所以伴随着大量的 options 请求
推动服务端做了这些预检请求的缓存,其原理就是通过 access-control-max-age 响应头设置预检请求的缓存时间


Service Worker


Service Worker 是一项很强大的技术,它能够对网络请求进行缓存和处理,它的最大应用场景是在弱网甚至离线环境下
一旦使用了 Service Worker 技术,用户在首次安装完成后,后续的访问相当于直接在本地读取静态资源,访问速度自然能够得到提升
虽然能够提升使用体验,但是使用 Service Worker 是存在一定限制和风险的



  • 必须运行在 https 协议下,调试时允许在 localhost、127.0.0.1

  • Service Worker 自身不能跨越,即主线程上注册的 Service Worker 必须在当前域名下

  • 一旦被安装成功就永远存在,除非线程被程序主动解除

  • Service Worker 的更新是比较复杂的,如果对其了解不深,建议还是只将不常更新的资源使用 Service Worker 缓存,降低风险


项目中直接使用 workbox(对 Service Worker 做了封装,并提供一些插件),以下为示例代码


主线程上注册 Service Worker


if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('./sw.js')
.then((reg) => {
navigator.serviceWorker.addEventListener('message', (event) => {
// 处理Worker传递的消息逻辑
});
console.log('注册成功:', reg);
})
.catch((err) => {
console.log('注册成功:', err);
});
}

Service Worker 线程处理缓存逻辑


//首先是异常处理
self.addEventListener('error', function (e) {
self.clients.matchAll().then(function (clients) {
if (clients && clients.length) {
clients[0].postMessage({
type: 'ERROR',
msg: e.message || null,
stack: e.error ? e.error.stack : null,
});
}
});
});

self.addEventListener('unhandledrejection', function (e) {
self.clients.matchAll().then(function (clients) {
if (clients && clients.length) {
clients[0].postMessage({
type: 'REJECTION',
msg: e.reason ? e.reason.message : null,
stack: e.reason ? e.reason.stack : null,
});
}
});
});

//然后引入workbox
importScripts('https://g.alicdn.com/kg/workbox/3.3.0/workbox-sw.js');

// 预缓存资源示例,不更新的资源使用预缓存
const resources = ['https://g.alicdn.com/dingding/dingtalk-jsapi/2.10.3/dingtalk.open.js'];

// 预缓存功能
workbox.precaching.precacheAndRoute(resources);

// 图片缓存 使用CacheFirst策略
workbox.routing.registerRoute(
/\.(jpe?g|png)/,
new workbox.strategies.CacheFirst({
cacheName: 'image-runtime-cache',
plugins: [
new workbox.expiration.Plugin({
// 对图片资源缓存 1 天
maxAgeSeconds: 24 * 60 * 60,
// 匹配该策略的图片最多缓存 20 张
maxEntries: 20,
}),
],
})
);

// 需要更新的js和css资源使用staleWhileRevalidate策略
workbox.routing.registerRoute(
new RegExp('https://g.alicdn.com/'),
workbox.strategies.staleWhileRevalidate({
cacheName: 'static-runtime-cache',
plugins: [
new workbox.expiration.Plugin({
maxEntries: 20,
}),
],
})
);


  • 预缓存功能:

    • 正常情况下,Service Worker 是在主程序首次请求时将资源拦截,在之后的请求中根据缓存策略处理

    • 预缓存功能是在 Service Worker 在安装阶段主动发起资源请求,并将其缓存下来

    • 当页面真正发起预缓存当中的资源请求时,资源已经被缓存了,就可以直接使用了

    • 预缓存是使用 Cache Only 策略,即在预缓存主动发起请求并获取缓存后,就只会在缓存中读取资源,不在进行缓存更新,所以适合项目中不更新的静态资源



  • 图片缓存:

    • 图片一般情况下是不更新的,所以采用 Cache First 缓存优先策略

    • 当有缓存时会优先读取缓存,读取成功直接使用本地缓存,不再发起请求

    • 读取失败时再发起网络请求,并将结果更新到缓存中



  • 对于需要更新的 JS 和 CSS

    • 使用 Stale While Revalidate 策略

    • 跟 Cache First 策略比较类似,都是优先返回本地缓存的资源

    • 区别在于 Stale While Revalidate 策略无论在缓存读取是否成功的时候都会发送网络请求更新本地缓存

    • 这是兼顾页面加载速度和缓存更新的策略,相对安全一些




六、其他


以下措施不具备通用性,但是在项目中用到了还是记录下来,仅供参考



  • 页面 schema 接口优化:低代码体系存在页面嵌套,每个页面单独请求自己的 schema,所以在嵌套层级较多的情况下,是以同步解析的顺序请求接口,页面渲染速度较慢,优化为服务端拼装完毕后直接返回

  • 部分接口的请求合并

  • 去除运行时 babel,低代码设计器中存在手写的代码,这部分代码最初在运行时由 babel 转化为 ES5(设计问题),优化为保存时转换


七、项目已经存在的措施



  • 静态资源放在 CDN

  • 启用 http2,并且浏览器支持,这一步很重要,是否使用 http2 对优化措施有直接的影响

  • js 和 css 的代码压缩,并且开启 gzip 压缩

  • 使用字体图标 iconfont 代替图片图标

  • CDN 合并:利用 CDN 的 combo 技术将多个 CDN 合并成一个发送(在 http2 中无明显效果)


最终优化效果



  • performance 表现:页面渲染完成在 3 秒以内


image.png



  • web-vitals 平均数据

    • FCP:2100

    • LCP:2400




参考文章



作者:萌鱼
来源:juejin.cn/post/7288981520946364475
收起阅读 »

从拼夕夕砍一刀链接漫谈微信落地页防封

写在前面 最近v2ex上一个话题火了,大概内容是有一个 好奇 摸鱼的程序员,在分析了拼夕夕发出的砍一刀短链接后,惊呼不可能。 是什么让一个见多识广的程序员如此惊讶呢?问题就出在拼夕夕发出的短链接上,经过测试发现,在微信内打开的短链,会出现二维码的页面,而在p...
继续阅读 »

写在前面


最近v2ex上一个话题火了,大概内容是有一个 好奇 摸鱼的程序员,在分析了拼夕夕发出的砍一刀短链接后,惊呼不可能。


image.png
是什么让一个见多识广的程序员如此惊讶呢?问题就出在拼夕夕发出的短链接上,经过测试发现,在微信内打开的短链,会出现二维码的页面,而在pc端浏览器打开时,则出现的另外一套界面。是什么导致了这样的情况呢?

微信落地页防封


谈到拼多多的短链分享,就不得不提一个很关键的名词微信落地页防封 ,说到微信落地页防封,那就需要知道,在什么情况下,会触发微信的域名拦截机制,一般来说,触发域名拦截有以下几个原因




  • 域名是新购入的老域名,在微信内之前有过违规记录,上过黑名单。




  • 网站流量太大,微信内同一域名被大量分享,比如分享赚类的平台某拼。




  • 诱导分享传播,即便是合法营销活动,也会触发拦截。




  • 网站内容违规,这个不必多说。




  • 被同行恶意举报。




为了让域名活的久一些,微信落地页防封这样的技术就应运而生,主要通过以下几点,来逃避微信的域名拦截机制



  • 大站域名【美团、京东...】

  • 不同主体各自备案域名【鸡蛋不放在一个篮子内】

  • 多级跳转+前置防火墙【通过前置防火墙中转页识别是否是机器扫描】

  • 随机Ip【cdn分发】

  • 图床 + 短链

  • 短链 + 自定义跳转 【稍后详细分析一下这种方式】


拼夕夕的防封技术猜测


经过测试,拼夕夕的防封应该采用的是图床+短链+自定义跳转的方式,接下来就听我一一道来



  • 图床
    图床是oss对象存储的昵称,通常是用来存放图片的,如果是用在防封里,那他其实是将一个html页面上传进了图床内,至于是怎么上传进去的。很简单啊,你只需要有一个阿里云,京东云,腾讯云的账号,购买了oss对象存储服务,设置公共读私有写,就可以访问了,这些不重要,你只需要知道图床所存储的是html就可以了。


我通过chrome的控制台抓取了通过短链转换而来地址,然后抓到了如下请求



  • 短链重定向


image.png
注意看第一个请求,第一个请求就是短链的自定义跳转,短链自定义跳转我们下一节详细去说,通过301重定向,将我们重定向到了图床的地址



  • 图床ua、地域、等判断
    图床内的html包含了对ua、地域、设备类型等的判断,不同的环境所打开的内容是不同的,通过对环境的判断,展示不同的内容去屏蔽微信的扫描,拼夕夕就是通过这样的方式来实现落地页防封的
    下面是我从落地页中拿到的一个函数,虽然我们很难完全还原这个函数,但是通过里面没被混淆的常量比如ke.HUAWEIke.OPPO等不难看出来,这是一个判断当前手机品牌的函数,针对不同的品牌下的浏览器,会做一些特殊的处理。


 const t = e(u().mark(function t (e) {
let r, n, o, i, c, s
return u().wrap(function (t) {
for (; ;) {
switch (t.prev = t.next) {
case 0:
if (r = e.brand,
n = e.payload,
o = a()(n, 'data', {}),
i = a()(n, 'isThirdBrowser'),
c = a()(n, 'data.fastAppDomains', ''),
s = Te(o),
Pe(o),
!i) {
t.next = 8
break
}
return t.abrupt('return')
case 8:
if (r !== ke.HUAWEI) {
t.next = 11
break
}
return t.next = 11,
R(c, {
cTime: s,
data: o
}).catch(fn)
case 11:
if (r !== ke.OPPO) {
t.next = 27
break
}
if (!j(A.OppoLeftScreen, o)) {
t.next = 17
break
}
return t.next = 15,
R(c, {
cTime: s,
data: o
}).catch(fn)
case 15:
case 20:
t.next = 27
break
case 17:
return t.prev = 17,
t.next = 20,
sn(c, {
cTime: s,
data: o
})
case 22:
if (t.prev = 22,
t.t0 = t.catch(17),
!j(A.banBrowserV2, o) && !j(A.oppoQAppPriority, o)) {
t.next = 27
break
}
return t.next = 27,
R(c, {
cTime: s,
data: o
}).catch(fn)
case 27:
if (r !== ke.VIVO) {
t.next = 30
break
}
return t.next = 30,
sn(c, {
cTime: s,
data: o
}).catch(fn)
case 30:
case 'end':
return t.stop()
}
}
}
, t, null, [[17, 22]])
}
))

再注意看接下来的一段代码片段,很明显针对上面获取到的手机品牌,会生成不同的图片,注意看下面混淆过的c函数,x.brandType, brand有品牌的意思,也就是上面函数获取到的手机品牌


o = new Promise((function(t) {
var r, o = document.createElement("img"), i = k(n), c = (f(r = {}, x.brandType, 1),
f(r, E.funcParams, i),
r), u = a()(e.split(","), "0");
o.onload = function(e) {
var r = a()(e, "path[0]") || a()(e, "target")
, n = gn(r);
t({
brand: n,
img: r
})
}
,
o.onerror = function() {
t({
brand: ke.OTHERS
})
}
;
var s = S(u).href;
o.src = m(c, s)
}
)),

得益于落地页开发者优秀的代码命名习惯,通过下面的片段,isWeChatPlatform,isIOSWeChatPlatform这两个字符串让我们知道落地页里面还有针对微信的一些判断,会判断是安卓还是ios微信


n = a()(r, "data", {}),
i = a()(r, "isWeChatPlatform"),
c = a()(r, "isIOSWeChatPlatform"),
f = a()(r, "data.mqCodeKey", ""),
l = a()(r, "data.websiteDomain", "").replace(/\/$/, ""),
p = a()(r, "data.fastAppDomains", ""),
d = v("image_url"),
h = v(f) || location.href,
!d) {
t.next = 15;
break
}

还有落地页内针对UA的判断的实现


((w = t.document),
(x = w ? w.title : ''),
(_ = navigator.userAgent.toLowerCase()),
(S = navigator.platform.toLowerCase()),
(O = !(!S.match('mac') && !S.match('win'))),
(A = _.indexOf('wxdebugger') != -1),
(E = _.indexOf('micromessenger') != -1),
(I = _.indexOf('android') != -1),
(T = _.indexOf('iphone') != -1 || _.indexOf('ipad') != -1),
(P = function () {
const t = _.match(/micromessenger\/(\d+\.\d+\.\d+)/) || _.match(/micromessenger\/(\d+\.\d+)/)
return t ? t[1] : ''

通过上面的代码片段,我们得以窥见拼夕夕落地页的逻辑设计,落地页内,至少实现了下面的能力



  • 针对手机品牌的处理

  • 针对安卓与ios系统的处理

  • 针对是否微信的处理


这些代码进一步的验证了我们的猜想,拼夕夕的确是通过oss内的html动态创建元素来规避微信拦截的!下面是短链智能跳转的一个例子,可以帮助大家更好的理解短链推广的内在逻辑


短链与智能跳转


我们以某平台的功能为例,演示如何通过短链实现自定义的跳转



  • 创建短链接


image.png



  • 配置智能跳转


image.png



  • 智能跳转的规则


可以看到,本身规则就支持按平台,按访问环境,按地域去进行智能跳转了,这也是为什么谷歌会想要将UA的信息进行加密或减少所提供的的信息。


image.png



  • 按地域的实现
    服务器可以看到当前访问的ip,通过ip去反向推断地域

  • 操作系统、访问环境 是通过判断UA来实现


console.log(navigator.userAgent)
// ua内会包含设备的关键信息,如果是微信浏览器内打开的,会携带微信浏览器特有的ua信息
'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1'

结语



技术本身都是为了解决现实存在的问题,技术没有好坏黑白,但是作为一个技术人,我们能做的就是做任何事情的时候,要坚守心中的底线。君子不立危墙之下,尽量少游走在黑白间的灰色地带。



作者:AprilKroc
来源:juejin.cn/post/7156548454502629384
收起阅读 »

四个有用的Android开发技巧,又来了

大家好,本篇文章会继续给大家分享一些Android常见的开发技巧,希望能对读者有所帮助。 一. 通过堆栈快速定位系统版本 这个地方主要分享大家两个个技巧,通过问题堆栈简快速定位当前系统版本: 1. 快速区分当前系统版本是Android10以下,还是Androi...
继续阅读 »

大家好,本篇文章会继续给大家分享一些Android常见的开发技巧,希望能对读者有所帮助。


一. 通过堆栈快速定位系统版本


这个地方主要分享大家两个个技巧,通过问题堆栈简快速定位当前系统版本:


1. 快速区分当前系统版本是Android10以下,还是Android10及以上;


首先Android10及以上引入了一个新的服务Service:ActivityTaskManagerService,将原本ActivityMangerService原本负责的一些职能拆分给了前者,所以当你的问题堆栈中出现了ActivityTaskManagerService相关的字眼,那肯定是Android10及以上了



大家在Android9及以下的源码中是找不到这个类的。


2. 快速区分当前系统版本是Android12以下,还是Android12及以上;


这个就得借助Looper了,给大家看下Android12上Looper的源码:



Looper分发消息的核心方法loop(),现在会转发给loopOnce()进行处理,这个可是Android12及以上特有的,而Looper又是Android处理消息必要的一环,是咱们问题堆栈的源头祖宗,类似于下面的:



所以这个技巧相信还是非常有必要的:当你从问题堆栈中一看有loopOnce() 这个方法,那必定是Android12无疑了。


二. 实现按钮间距的一种奇特方式


最近看了一个新的项目代码,发现该项目实现按钮之间、按钮与顶部底部之间间距实现了,用了一种我之前没了解过的方式,于是这里分享给大家瞧瞧。


这里就以TextView和屏幕顶部间设置间距为例,初始的效果如下:



接下来我们来进行一步步改造:


1. 首先TextView是有一个自定义的xml背景:


<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:height="70dp"
android:gravity="center_vertical">

<shape>
<solid android:color="#ff0000" />
</shape>
</item>
</layer-list>

核心就是定义了android:heightandroid:gravity这两个属性,来确保我们自定义背景在组件中的高度及居中位置。


2. 其次将布局中TextView的属性调整下:




  1. 首先height属性一定要调整为wrap_content保证最后TextView按钮的高度的测量最终取minHeight设置的属性值和背景设置的高度这两者的最大值



  1. 其次还要设置minHeight最小高度属性,注意一定要比背景设置的高度值大,保证能和屏幕顶部产生边距效果;



  1. 最后要设置字体的位置为垂直居中,保证字体位置和背景不发生错位


经过上面处理,效果就出来了:



其实上下空白的部分都是属于TextView,设置点击事件也会被响应,这算是其中的缺点之一,当前也可能在业务场景中认为这是一种合理表现。


上面实现的逻辑和TextView的测量逻辑密不可分,感兴趣的同学可以看下这块代码,这里就不带大家进行一一分析了:




三. logcat快速查看当前跳转的Activity类信息


忘了是在哪里看到的了,只要日志过滤start u0,就可以看到每次跳转的Activity信息,非常的有帮助,既不需要改动业务层,也不需要麻烦的安装一些插件啥的。


使用时记得将logcat右边的过滤条件置为,否则你就只能在左边切换到系统进程去看了:


这里我们演示下效果:


1. 跳转到Google浏览器



logcat界面会输出:



会打印一些跳转到包名类名等相关信息。


2. 跳转到系统设置界面



logcat输出:



可以说start u0还是相当好用的。


四. 项目gradle配置最好指向同一本地路径


最近开发中经常存在需要一次性检索多个项目的场景,而这样项目的gradle版本都是相同的,没啥区别。但每打开一个项目就得重新走一遍gradle下载流程,下载速度又是蜗牛一样的慢。


所以强烈建议大家,本地提前准备好几个gradle版本,然后通过设置将项目的gradle指向本地已存在好的gradle:



这样项目第一次打开的速度将是非常快的,而且按道理来说相同gradle版本的项目指向同一本地路径,也可以实现缓存共享。猜的


如果项目好好的编译运行着,突然没网了,可能会提示一些找不到依赖库资源啥的,其实你本地都已经缓存好依赖库资源了,只需要设置下off-mode,不走网络直接通过本地资源编译运行即可



总结


本篇文章主要是介绍了Android开发一些技巧,感觉都是项目中挺常用到的,算是我最近一个月收获的吧,后续准备研究研究compose了,毕竟看到大家们都在搞这个,羡慕的口水都流了一地了哈哈。


历史文章


两个Kotlin优化小技巧,你绝对用的上


Kotlin1.9.0-Beta,它来了!!


Kotlin1.8新增特性,进来了解一下


聊聊Kotlin1.7.0版本提供的一些特性


聊聊kotlin1.5和1.6版本提供的一些新特性


优化@BuilderInference注解,Kotlin高版本下了这些“毒手”!


@JvmDefaultWithCompatibility优化小技巧,了解一下~


作者:长安皈故里
来源:juejin.cn/post/7250080519069007933
收起阅读 »

我做梦都想不到😵,我被if(x)摆了一道!

web
读本文,可以收获什么? 字数:2494 花费时间:5min if(x)中的x为各种类型的数据或者值的时候会发生什么?x为各种数据类型、表达式、特殊值、位运算、函数等值的时,这些在if语句都充当了什么,实现了什么? 总结== === ≠三种情况特殊值的比较,如...
继续阅读 »


读本文,可以收获什么?


字数:2494 花费时间:5min


if(x)中的x为各种类型的数据或者值的时候会发生什么?x为各种数据类型、表达式、特殊值、位运算、函数等值的时,这些在if语句都充当了什么,实现了什么?


总结== === ≠三种情况特殊值的比较,如下图所示:



image.png


作为一个程序员的我们,相信我们写代码用的最多逻辑应该就是if语句了吧,其实我们真的了解if(x)究竟发生了什么?其实很简单,我们可能都知道中文有这样一个模板:"如果是什么,就会做什么",也就是说符合条件的某件事,才会去做某件事。同样的道理if(x)的意思就是如果符合x条件,我们就可以执行if语句块的代码了。而我们JavaScript中的哪个数据类型是涉及是否意思的?当然是Boolean类型啦,其实if内的x非布尔值都会做一次Boolean类型的转换的


1 x为一个值时


1.1 x为字符串:


x为一个空字符串时,这是一个假值,if语句会转换为false。


if ("") {
console.log("Hello World!");
}
console.log(Boolean(""));// false

x为一个非空字符串是,这是一个真值。if语句会转换为true。


if (!"") {
console.log("Hello World!");// Hello World!
}
console.log(Boolean(!""));// true

x为一个空格字符串,这是一个真值。if语句会转换为true。否则会转换为false


if (" ") {
console.log("Hello World!");// Hello World!
}
console.log(Boolean(" "));// true
if (!" ") {
console.log("Hello World!");
}
console.log(Boolean(!" "));// false

x为一个字符串,这是一个真值。if语句会转换为true。否则会转换为false


if ("JavaScript") {
console.log("Hello World!");// Hello World!
}
console.log(Boolean("JavaScript"));// true

if (!"JavaScript") {
console.log("Hello World!");
}
console.log(Boolean(!"JavaScript"));// false

1.2 x为数字


x为一个数字0时,这是一个假值,if语句会转换为false。x为一个数字!0时,这是一个真值,if语句会转换为true。


if (0) {
console.log("Hello World")
}
console.log(Boolean(0));// fasle

if (!0) {
console.log("Hello World");// Hello World
}
console.log(Boolean(!0));// true

if (1) {
console.log("Hello World") // Hello World
}
console.log(Boolean(1));// true

if (!1) {
console.log("Hello World")
}
console.log(Boolean(!1));// false

if (-0) {
console.log("Hello World")
}
console.log(Boolean(-0));// fasle
if (!-0) {
console.log("Hello World");// Hello World
}
console.log(Boolean(!-0));// true

1.3 x为数组


x为一个空数组,这是一个真值,if语句会转换为true。


if ([]) {
console.log("Hello World");// Hello World
}
console.log(Boolean([]));// true
if (![]) {
console.log("Hello World");
}
console.log(Boolean(![]));// false

x为一个嵌套空数组时,这是一个真值,if语句会转换为true。否则是假值。if语句会转换为false


if ([[]]) {
console.log("Hello World");// Hello World
}
console.log(Boolean([[]]));// true
if (![[]]) {
console.log("Hello World");
}
console.log(Boolean(![[]]));// false

x为一个有空字符串的数组时,这是一个真值,if语句会转换为true。否则是假值。if语句会转换为false。


if ([""]) {
console.log("Hello World");// Hello World
}
console.log(Boolean([""]));// true
if (![""]) {
console.log("Hello World");
}
console.log(Boolean(![""]));// false

x为一个有数字0的数组时,这是一个真值,if语句会转换为true。否则是假值。if语句会转换为false。


if ([0]) {
console.log("Hello World");// Hello World
}
console.log(Boolean([0]));// true
if (![0]) {
console.log("Hello World");
}
console.log(Boolean(![0]));// false

1.4 x为对象:


if ({}) {
console.log("Hello World") // Hello World
}
console.log(Boolean({}));// true

2 x为特殊值时


if (null) {
console.log("Hello World");
}
console.log(Boolean(null));// false

if (undefined) {
console.log("Hello World");
}
console.log(Boolean(undefined));// false

if (NaN) {
console.log("Hello World");
}
console.log(Boolean(NaN));// false

3 x为位运算时


if (true | false) {
// 按位或,只要有一个成立就为true
console.log("Hello World");
}
console.log(Boolean(true | false));// true

4 x为表达式时


比较的相方首先调用ToPrimitive(内部函数,不能自行调用)转换为原始值,如果出现非字符串,就根据ToNumber规则将双方强制转换为数字来进行比较。


const a = [42];
const b = ["43"];
console.log(a < b);// true

5 x为等式时


5.1 一个等号(=)


=: 一个等号代表的是赋值,即使x的值为a=2,也就是说变量的声明操作放在if判断位置上了,其实它还是一个变量并不是一个操作。


let a;
if (a = 2) {
console.log("条件成立!");// 条件成立!
}
console.log(a);// 2

let a;
if (a = 2) {
console.log("条件成立!");// 条件成立!
}
console.log(typeof (a = 2));// number
console.log(Boolean(a = 2));// true

let a;
if (a = 2 && (a = 3)) {
console.log("条件成立!");// 条件成立!
}

console.log(typeof (a = 2 && (a = 3)));// number;

5.2 两个等号(==)


==:宽松相等,我们可能都会这样想,==检查值是否相等,听起来蛮有道理,但不准确,真正的含义是==允许相等比较重进行强制类型转换



对于==符号尽量遵守两个原则:


如果两边的值中有true或者false,千万不要使用==


如果两边的值中有[]、""、0,尽量不要使用==





  • 两个值类型相同,则执行严格相等变量。


    🍟 都是字符串类型:


    const a = "";
    const b = "12";
    console.log(a == b);// false

    🍟 都是NaN类型:全称为not a number,理解为不是一个数值。JavaScript的规定, NaN表示的是非数字, 那么这个非数字可以是不同的数字,因此 NaN 不等于 NaN。


    const a = NaN;
    const b = NaN;
    console.log(a == b);// false

    🍟 都是Symbol类型:Symbol命名的属性都是独 一无二的,可以唯一标识变量值,不受是否相同变量值。


    const a = Symbol("1");
    const b = Symbol("1");
    console.log(a == b);// false

    🍟 都是对象类型。对象的比较是内存地址,因为对象是存储在堆中,当堆中有对象时,它会相对应内存中有一个存储的地址,在栈中其存储了其在堆中数据的地址,当调用数据时,去堆中调取对应堆中的数据的地址获取出来。也就是相同对象比较的是内存地址,变量不一样存储位置不一样。


    const a = { a: 1 };
    const b = {};
    console.log(a == b);// false

    const a = {};
    const b = {};
    console.log(a == b);// false
    console.log(Boolean(a));// true



  • 两个值类型不相同。


    🍟 一个值是null,一个是undefind。


    const a = undefined;
    const b = null;
    console.log(a == b);// true

    🍟 一个值是数字,一个值是字符串。字符串强制转换为数字在比较。


    const a = 12;
    const b = "12";
    console.log(a == b);// true

    🍟 一个值是布尔值,一个是其他类型的值。这种做法是不安全,不建议去使用,在开发中尽量不要这样使用。


    console.log("0" == false);// true
    console.log("" == false);// true
    console.log(0 == false);// true
    console.log([] == false);// true

    🍟 一个值是对象,一个值是字符串或数字。对象与非对象的比较,对象会被强制转换原始值(通过内部函数 ToPrimitive自动执行,这个是内部函数不能直接调用)再比较。


    const a = {};
    const b = "";
    console.log(a == b);// false



5.3 三个等号(===)


===: 严格相等,我们可能都会这样想,===检查值和类型是否相等,听起来蛮有道理,但不准确,真正的含义是===不允许相等比较重进行强制类型转换,也就是不做任何处理变量是什么就是什么。


const a = 0;
const b = "0";
console.log(a === b);// false

6 x为&&、||操作时


||和&&首先会对第一个操作数(a和c)执行条件判断,如果其不是布尔值(如上例)就先进行ToBoolean强制类型转换,然后再执行条件判断。


🍟 对于||来说,如果条件判断结果为true就返回第一个操作数(a和c)的值,如果为false就返回第二个操作数(b)的值。


const a = 12;
const b = "abc";
const c = null;
if (a || b) {
console.log("a||b");// a||b
}
console.log(typeof (a || b));// number
console.log(Boolean(a || b));// true
console.log(a || b);// 12

const b = "abc";
const c = null;
if (c || b) {
console.log("c||b");// c||b
}
console.log(typeof (c || b));// string
console.log(Boolean(c || b));// true
console.log(c || b);// abc

🍟 &&则相反,如果条件判断结果为true就返回第二个操作数(b)的值如果为false就返回第一个操作数(a和c)的值。


const a = 12;
const b = "abc";
if (a && b) {
console.log("a&&b");// a&&b
}
console.log(typeof (a && b));// string
console.log(Boolean(a && b));// true
console.log(a && b);// abc

const b = "abc";
const c = null;
if (c && b) {
console.log("c&&b");
}
console.log(typeof (c && b));// object
console.log(Boolean(c && b));// false
console.log(c && b);// null

7 x为函数判断时




  • typeof与instanceof的区别


    🍟 typeof:返回值是一个字符串,用来说明变量的数据类型。一般只能返回如下几个结果:number、string、function、object、undefined,对于Array、Null等特殊对象typeof一律返回object,这正是typeof的局限性。


    console.log(typeof undefined == 'undefined');// true
    console.log(typeof null);// object

    🍟instanceof:返回值为布尔值,用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性。用于判断一个变量是否某个对象的实例。,注意地,instanceof只能用来判断对象和函数,不能用来判断字符串和数字等


    const arr = new Array()
    if (arr instanceof Array) {
    console.log("arr instanceof Array");// arr instanceof Array
    }
    if (arr instanceof Object) {
    // 因为Array是Object的子类
    console.log("arr instanceof Object");// arr instanceof Array
    }
    console.log(typeof (arr instanceof Array));// boolean

    🍟 typeofinstanceof都有一定的弊端,并不能满足所有场景的需求。如果需要通用检测数据类型,可以使用Object.prototype.toString.call()方法:


    Object.prototype.toString.call({});// "[object Object]"
    Object.prototype.toString.call([]); // "[object Array]"
    Object.prototype.toString.call(666); // "[object Number]"
    Object.prototype.toString.call("xxx"); // "[object String]"



注意,该方法返回的是一个格式为"[object Object]"的字符串。




  • indexof与includes区别


    🍟 indexof:返回的是所含元素的下标,注意地,此函数是无法判断是否有NaN元素


    const str = "130212";
    if (str.indexOf("0")) {
    console.log("str中存在0!")
    }
    console.log(str.indexOf("0"));// 2

    🍟 includes:返回的是布尔值,代表是否存在此元素。


    const str = "130212";
    if (str.includes("0")) {
    console.log("str中存在0!")
    }
    console.log(str.includes("0"));// true



作者:路灯下的光
来源:juejin.cn/post/7154647954840616996
收起阅读 »

什么情况下Activity会被杀掉呢?

首先一个报错来作为开篇: Caused by androidx.fragment.app.Fragment$InstantiationException Unable to instantiate fragment xxx: could not find Fr...
继续阅读 »

首先一个报错来作为开篇:


Caused by androidx.fragment.app.Fragment$InstantiationException
Unable to instantiate fragment xxx: could not find Fragment constructor

这个报错原因就是Fragment如果重载了有参的构造方法,没有实现默认无参构造方法。Activity被回收又回来尝试重新恢复Fragment的时候报错的。


那如何模拟Activity被回收呢?

可能有人知道,一个方便快捷的方法就是:打开 开发者选项 - 不保留活动,这样每次Activity回到后台都会被回收,也就可以很方便的测试这种case。


但抛开这种方式我怎么来复现这种情况呢?

这里我提出一种方式:我是不是可以打开我的App,按Home回到后台,然后疯狂的打开手机里其他的大型应用或者游戏这类的能占用大量手机内存的App,等手机内存占用大的时候是不是可以复现这种情况呢?


结论是不可以,不要混淆两个概念,系统内存不足App内存不足,两者能引起的后果也是不同的



  • 系统内存不足 -> 杀掉应用进程

  • App内存不足 -> 杀掉后台Activity


首先明确一点,Android框架对进程创建与管理进行了封装,对于APP开发者只需知道Android四大组件的使用。当Activity, Service, ContentProvider, BroadcastReceiver任一组件启动时,当其所承载的进程存在则直接使用,不存在则由框架代码自动调用startProcessLocked创建进程。所以说对APP来说进程几乎是透明的,但了解进程对于深刻理解Android系统是至关关键的。


1. 系统内存不够 -> 杀掉应用进程


1.1. LKM简介

Android底层还是基于Linux,在Linux中低内存是会有oom killer去杀掉一些进程去释放内存,而Android中的lowmemorykiller就是在此基础上做了一些调整来的。因为手机上的内存毕竟比较有限,而Android中APP在不使用之后并不是马上被杀掉,虽然上层ActivityManagerService中也有很多关于进程的调度以及杀进程的手段,但是毕竟还需要考虑手机剩余内存的实际情况,lowmemorykiller的作用就是当内存比较紧张的时候去及时杀掉一些ActivityManagerService还没来得及杀掉但是对用户来说不那么重要的进程,回收一些内存,保证手机的正常运行。


lowmemkiller中会涉及到几个重要的概念:

/sys/module/lowmemorykiller/parameters/minfree:里面是以”,”分割的一组数,每个数字代表一个内存级别

/sys/module/lowmemorykiller/parameters/adj: 对应上面的一组数,每个数组代表一个进程优先级级别


比如:

/sys/module/lowmemorykiller/parameters/minfree:18432, 23040, 27648, 32256, 55296, 80640

/sys/module/lowmemorykiller/parameters/adj: 0, 100, 200, 300, 900, 906


代表的意思是两组数一一对应:



  • 当手机内存低于80640时,就去杀掉优先级906以及以上级别的进程

  • 当内存低于55296时,就去杀掉优先级900以及以上的进程


可能每个手机的配置是不一样的,可以查看一下手头的手机,需要root。


1.2. 如何查看ADJ

如何查看进程的ADJ呢?比如我们想看QQ的adj


-> adb shell ps | grep "qq" 
UID PID PPID C STIME TTY TIME CMD
u0_a140 9456 959 2 10:03:07 ? 00:00:22 com.tencent.mobileqq
u0_a140 9987 959 1 10:03:13 ? 00:00:07 com.tencent.mobileqq:mini3
u0_a140 16347 959 0 01:32:48 ? 00:01:12 com.tencent.mobileqq:MSF
u0_a140 21475 959 0 19:47:33 ? 00:01:25 com.tencent.mobileqq:qzone

# 看到QQ的PID为 9456,这个时候打开QQ,让QQ来到前台
-> adb shell cat /proc/9456/oom_score_adj
0

# 随便打开一个其他的App
-> adb shell cat /proc/9456/oom_score_adj
700

# 再随便打开另外一个其他的App
-> adb shell cat /proc/9456/oom_score_adj
900

我们可以看到adj是在根据用户的行为不断变化的,前台的时候是0,到后台是700,回到后台后再打开其他App后是900

常见ADJ级别如下:


ADJ级别取值含义
NATIVE_ADJ-1000native进程
SYSTEM_ADJ-900仅指system_server进程
PERSISTENT_PROC_ADJ-800系统persistent进程
PERSISTENT_SERVICE_ADJ-700关联着系统或persistent进程
FOREGROUND_APP_ADJ0前台进程
VISIBLE_APP_ADJ100可见进程
PERCEPTIBLE_APP_ADJ200可感知进程,比如后台音乐播放
BACKUP_APP_ADJ300备份进程
HEAVY_WEIGHT_APP_ADJ400重量级进程
SERVICE_ADJ500服务进程
HOME_APP_ADJ600Home进程
PREVIOUS_APP_ADJ700上一个进程
SERVICE_B_ADJ800B List中的Service
CACHED_APP_MIN_ADJ900不可见进程的adj最小值
CACHED_APP_MAX_ADJ906不可见进程的adj最大值

So,当系统内存不足的时候会kill掉整个进程,皮之不存毛将焉附,Activity也就不在了,当然也不是开头说的那个case。


2. App内存不足 -> 杀掉后台Activity


上面分析了是直接kill掉进程的情况,一旦出现进程被kill掉,说明内存情况已经到了万劫不复的情况了,抛开内存泄漏的情况下,framework也需要一些策略来避免无内存可用的情况。下面我们来找一找fw里面回收Activity的逻辑(代码Base Android-30)。



Android Studio查看源码无法查看com.android.internal包名下的代码,双击Shift,勾选右上角Include non-prject Items.



入口定位到ActivityThreadattach方法,ActivityThread是App的入口程序,main方法中创建并调用atttach


// ActivityThread.java
private void attach(boolean system, long startSeq) {
...
// Watch for getting close to heap limit.
BinderInternal.addGcWatcher(new Runnable() {
@Override public void run() {
// mSomeActivitiesChanged在生命周期变化的时候会修改为true
if (!mSomeActivitiesChanged) {
return;
}
Runtime runtime = Runtime.getRuntime();
long dalvikMax = runtime.maxMemory();
long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
if (dalvikUsed > ((3*dalvikMax)/4)) {
mSomeActivitiesChanged = false;
try {
ActivityTaskManager.getService().releaseSomeActivities(mAppThread);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
});
...
}

这里关注BinderInternal.addGcWatcher, 下面有几个点需要理清:



  1. addGcWatcher是干嘛的,这个Runnable什么时候会被执行。

  2. 这里的maxMemory() / totalMemory() / freeMemory()都怎么理解,值有什么意义

  3. releaseSomeActivities()做了什么事情,回收Activity的逻辑是什么。


还有一个小的点是这里还用了mSomeActivitiesChanged这个标记位来标记让检测工作不会过于频繁的执行,检测到需要releaseSomeActivities后会有一个mSomeActivitiesChanged = false;赋值。而所有的mSomeActivitiesChanged = true操作都在handleStartActivity/handleResumeActivity...等等这些操作Activity声明周期的地方。控制了只有Activity声明周期变化了之后才会继续去检测是否需要回收。


2.1. GcWatcher

BinderInternal.addGcWatcher是个静态方法,相关代码如下:


public class BinderInternal {
private static final String TAG = "BinderInternal";
static WeakReference<GcWatcher> sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
static ArrayList<Runnable> sGcWatchers = new ArrayList<>();
static Runnable[] sTmpWatchers = new Runnable[1];

static final class GcWatcher {
@Override
protected void finalize() throws Throwable {
handleGc();
sLastGcTime = SystemClock.uptimeMillis();
synchronized (sGcWatchers) {
sTmpWatchers = sGcWatchers.toArray(sTmpWatchers);
}
for (int i=0; i<sTmpWatchers.length; i++) {
if (sTmpWatchers[i] != null) {
sTmpWatchers[i].run();
}
}
sGcWatcher = new WeakReference<GcWatcher>(new GcWatcher());
}
}

public static void addGcWatcher(Runnable watcher) {
synchronized (sGcWatchers) {
sGcWatchers.add(watcher);
}
}
...
}

两个重要的角色:sGcWatcherssGcWatcher



  • sGcWatchers保存了调用BinderInternal.addGcWatcher后需要执行的Runnable(也就是检测是否需要kill Activity的Runnable)。

  • sGcWatcher是个装了new GcWatcher()的弱引用。


弱引用的规则是如果一个对象只有一个弱引用来引用它,那GC的时候就会回收这个对象。那很明显new出来的这个GcWatcher()只会有sGcWatcher这一个弱引用来引用它,所以每次GC都会回收这个GcWatcher对象,而回收的时候会调用这个对象的finalize()方法,finalize()方法中会将之前注册的Runnable来执行掉。
注意哈,这里并没有移除sGcWatcher中的Runnable,也就是一开始通过addGcWatcher(Runnable watcher)进来的runnable一直都在,不管执行多少次run的都是它。


为什么整个系统中addGcWatcher只有一个调用的地方,但是sGcWatchers确实一个List呢?我在自己写了这么一段代码并且想着怎么能反射搞到系统当前的BinderInternal一探究竟的时候明白了一点点,我觉着他们就是怕有人主动调用了addGcWatcher给弄了好多个GcWatcher导致系统的失效了才搞了个List吧。。


2.2. App可用的内存

上面的Runnable是如何检测当前的系统内存不足的呢?通过以下的代码


        Runtime runtime = Runtime.getRuntime();
long dalvikMax = runtime.maxMemory();
long dalvikUsed = runtime.totalMemory() - runtime.freeMemory();
if (dalvikUsed > ((3*dalvikMax)/4)) { ... }

看变量名字就知道,在使用的内存到达总内存的3/4的时候去做一些事情,这几个方法的注释如下:


    /**
* Returns the amount of free memory in the Java Virtual Machine.
* Calling the gc method may result in increasing the value returned by freeMemory.
* @return an approximation to the total amount of memory currently available for future allocated objects, measured in bytes.
*/

public native long freeMemory();

/**
* Returns the total amount of memory in the Java virtual machine.
* The value returned by this method may vary over time, depending on the host environment.
* @return the total amount of memory currently available for current and future objects, measured in bytes.
*/

public native long totalMemory();

/**
* Returns the maximum amount of memory that the Java virtual machine will attempt to use.
* If there is no inherent limit then the value java.lang.Long#MAX_VALUE will be returned.
* @return the maximum amount of memory that the virtual machine will attempt to use, measured in bytes
*/

public native long maxMemory();

首先确认每个App到底有多少内存可以用,这些Runtime的值都是谁来控制的呢?


可以使用adb shell getprop | grep "dalvik.vm.heap"命令来查看手机给每个虚拟机进程所分配的堆配置信息:


yocn@yocn ~ % adb shell getprop | grep "dalvik.vm.heap"
[dalvik.vm.heapgrowthlimit]: [256m]
[dalvik.vm.heapmaxfree]: [8m]
[dalvik.vm.heapminfree]: [512k]
[dalvik.vm.heapsize]: [512m]
[dalvik.vm.heapstartsize]: [8m]
[dalvik.vm.heaptargetutilization]: [0.75]

这些值分别是什么意思呢?



  • [dalvik.vm.heapgrowthlimit]和[dalvik.vm.heapsize]都是当前应用进程可分配内存的最大限制,一般heapgrowthlimit < heapsize,如果在Manifest中的application标签中声明android:largeHeap=“true”,APP直到heapsize才OOM,否则达到heapgrowthlimit就OOM

  • [dalvik.vm.heapstartsize] Java堆的起始大小,指定了Davlik虚拟机在启动的时候向系统申请的物理内存的大小,后面再根据需要逐渐向系统申请更多的物理内存,直到达到MAX

  • [dalvik.vm.heapminfree] 堆最小空闲值,GC后

  • [dalvik.vm.heapmaxfree] 堆最大空闲值

  • [dalvik.vm.heaptargetutilization] 堆目标利用率


比较难理解的就是heapminfree、heapmaxfree和heaptargetutilization了,按照上面的方法来说:
在满足 heapminfree < freeMemory() < heapmaxfree的情况下使得(totalMemory() - freeMemory()) / totalMemory()接近heaptargetutilization


所以一开始的代码就是当前使用的内存到达分配的内存的3/4的时候会调用releaseSomeActivities去kill掉某些Activity.


2.3. releaseSomeActivities

releaseSomeActivities在API 29前后差别很大,我们来分别看一下。


2.3.1. 基于API 28的版本的releaseSomeActivities实现如下:

// step①:ActivityManagerService.java
@Override
public void releaseSomeActivities(IApplicationThread appInt) {
synchronized(this) {
final long origId = Binder.clearCallingIdentity();
try {
ProcessRecord app = getRecordForAppLocked(appInt);
mStackSupervisor.releaseSomeActivitiesLocked(app, "low-mem");
} finally {
Binder.restoreCallingIdentity(origId);
}
}
}

// step②:ActivityStackSupervisor.java
void releaseSomeActivitiesLocked(ProcessRecord app, String reason) {
TaskRecord firstTask = null;
ArraySet<TaskRecord> tasks = null;
for (int i = 0; i < app.activities.size(); i++) {
ActivityRecord r = app.activities.get(i);
// 如果当前有正在销毁状态的Activity,Do Nothing
if (r.finishing || r.state == DESTROYING || r.state == DESTROYED) {
return;
}
// 只有Activity在可以销毁状态的时候才继续往下走
if (r.visible || !r.stopped || !r.haveState || r.state == RESUMED || r.state == PAUSING
|| r.state == PAUSED || r.state == STOPPING) {
continue;
}
if (r.task != null) {
if (firstTask == null) {
firstTask = r.task;
} else if (firstTask != r.task) {
// 2.1 只有存在两个以上的Task的时候才会到这里
if (tasks == null) {
tasks = new ArraySet<>();
tasks.add(firstTask);
}
tasks.add(r.task);
}
}
}
// 2.2 只有存在两个以上的Task的时候才不为空
if (tasks == null) {
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Didn't find two or more tasks to release");
return;
}
// If we have activities in multiple tasks that are in a position to be destroyed,
// let's iterate through the tasks and release the oldest one.
// 2.3 遍历找到ActivityStack释放最旧的那个
final int numDisplays = mActivityDisplays.size();
for (int displayNdx = 0; displayNdx < numDisplays; ++displayNdx) {
final ArrayList<ActivityStack> stacks = mActivityDisplays.valueAt(displayNdx).mStacks;
// Step through all stacks starting from behind, to hit the oldest things first.
// 从后面开始遍历,从最旧的开始匹配
for (int stackNdx = 0; stackNdx < stacks.size(); stackNdx++) {
final ActivityStack stack = stacks.get(stackNdx);
// Try to release activities in this stack; if we manage to, we are done.
// 尝试在这个stack里面销毁这些Activities,如果成功就返回。
if (stack.releaseSomeActivitiesLocked(app, tasks, reason) > 0) {
return;
}
}
}
}

上面代码都加了注释,我们来理一理重点需要关注的点。整个流程可以观察tasks的走向



  • 2.1 & 2.2: 第一次循环会给firstTask赋值,当firstTask != r.task的时候才会给tasks赋值,后续会继续对tasks操作。所以单栈的应用不会回收,如果tasks为null,就直接return了,什么都不做

  • 2.3: 这一大段的双重for循环其实都没有第一步遍历出来的tasks参与,真正释放Activity的操作在ActivityStack中,所以尝试找到这些tasks对应的ActivityStack,让ActivityStack去销毁tasks,直到成功销毁。


继续查看releaseSomeActivitiesLocked:


// step③ ActivityStack.java
final int releaseSomeActivitiesLocked(ProcessRecord app, ArraySet<TaskRecord> tasks, String reason) {
// Iterate over tasks starting at the back (oldest) first.
int maxTasks = tasks.size() / 4;
if (maxTasks < 1) {
maxTasks = 1;
}
// 3.1 maxTasks至少为1,至少清理一个
int numReleased = 0;
for (int taskNdx = 0; taskNdx < mTaskHistory.size() && maxTasks > 0; taskNdx++) {
final TaskRecord task = mTaskHistory.get(taskNdx);
if (!tasks.contains(task)) {
continue;
}
int curNum = 0;
final ArrayList<ActivityRecord> activities = task.mActivities;
for (int actNdx = 0; actNdx < activities.size(); actNdx++) {
final ActivityRecord activity = activities.get(actNdx);
if (activity.app == app && activity.isDestroyable()) {
destroyActivityLocked(activity, true, reason);
if (activities.get(actNdx) != activity) {
// Was removed from list, back up so we don't miss the next one.
// 3.2 destroyActivityLocked后续会调用TaskRecord.removeActivity(),所以这里需要将index--
actNdx--;
}
curNum++;
}
}
if (curNum > 0) {
numReleased += curNum;
// 移除一个,继续循环需要判断 maxTasks > 0
maxTasks--;
if (mTaskHistory.get(taskNdx) != task) {
// The entire task got removed, back up so we don't miss the next one.
// 3.3 如果整个task都被移除了,这里同样需要将获取Task的index--。移除操作在上面3.1的destroyActivityLocked,移除Activity过程中,如果task为空了,会将task移除
taskNdx--;
}
}
}
return numReleased;
}



  • 3.1: ActivityStack利用maxTasks 保证,最多清理tasks.size() / 4,最少清理1个TaskRecord,同时,至少要保证保留一个前台可见TaskRecord,比如如果有两个TaskRecord,则清理先前的一个,保留前台显示的这个,如果三个,则还要看看最老的是否被有效清理,也就是是否有Activity被清理,如果有则只清理一个,保留两个,如果没有,则继续清理次老的,保留一个前台展示的,如果有四个,类似,如果有5个,则至少两个清理。一般APP中,很少有超过两个TaskRecord的。




  • 3.2: 这里清理的逻辑很清楚,for循环,如果定位到了期望的activity就清理掉,但这里这个actNdx--是为什么呢?注释说activity从list中移除了,为了能继续往下走,需要index--,但在这个方法中并没有将activity从lsit中移除的操作,那肯定是在destroyActivityLocked方法中。继续追进去可以一直追到TaskRecord.java#removeActivity(),从当前的TaskRecord的mActivities中移除了,所以需要index--。




  • 3.3: 我们弄懂了上面的actNdx--之后也就知道这里为什么要index--了,在ActivityStack.java#removeActivityFromHistoryLocked()中有




	if (lastActivity) {
removeTask(task, reason, REMOVE_TASK_MODE_DESTROYING);
}

如果task中没有activity了,需要将这个task移除掉。


以上就是基于API 28的releaseSomeActivities分析。


2.3.2. 基于29+的版本的releaseSomeActivities实现如下:

// ActivityTaskManagerService.java
@Override
public void releaseSomeActivities(IApplicationThread appInt) {
synchronized (mGlobalLock) {
final long origId = Binder.clearCallingIdentity();
try {
final WindowProcessController app = getProcessController(appInt);
app.releaseSomeActivities("low-mem");
} finally {
Binder.restoreCallingIdentity(origId);
}
}
}

// WindowProcessController.java
void releaseSomeActivities(String reason) {
// Examine all activities currently running in the process. Candidate activities that can be destroyed.
// 检查进程里所有的activity,看哪些可以被关掉
ArrayList<ActivityRecord> candidates = null;
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Trying to release some activities in " + this);
for (int i = 0; i < mActivities.size(); i++) {
final ActivityRecord r = mActivities.get(i);
// First, if we find an activity that is in the process of being destroyed,
// then we just aren't going to do anything for now; we want things to settle
// down before we try to prune more activities.
// 首先,如果我们发现一个activity正在执行关闭中,在关掉这个activity之前什么都不做
if (r.finishing || r.isState(DESTROYING, DESTROYED)) {
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Abort release; already destroying: " + r);
return;
}
// Don't consider any activities that are currently not in a state where they can be destroyed.
// 如果当前activity不在可关闭的state的时候,不做处理
if (r.mVisibleRequested || !r.stopped || !r.hasSavedState() || !r.isDestroyable()
|| r.isState(STARTED, RESUMED, PAUSING, PAUSED, STOPPING)) {
if (DEBUG_RELEASE) Slog.d(TAG_RELEASE, "Not releasing in-use activity: " + r);
continue;
}

if (r.getParent() != null) {
if (candidates == null) {
candidates = new ArrayList<>();
}
candidates.add(r);
}
}

if (candidates != null) {
// Sort based on z-order in hierarchy.
candidates.sort(WindowContainer::compareTo);
// Release some older activities
int maxRelease = Math.max(candidates.size(), 1);
do {
final ActivityRecord r = candidates.remove(0);
r.destroyImmediately(true /*removeFromApp*/, reason);
--maxRelease;
} while (maxRelease > 0);
}
}

新版本的releaseSomeActivities放到了ActivityTaskManagerService.java这个类中,这个类是API 29新添加的,承载部分AMS的工作。
相比API 28基于Task栈的回收Activity策略,新版本策略简单清晰, 也激进了很多。


遍历所有Activity,刨掉那些不在可销毁状态的Activity,按照Activity堆叠的顺序,也就是Z轴的顺序,从老到新销毁activity。


有兴趣的读者可以自行编写测试代码,分别在API 28和API 28+的手机上测试看一下回收策略是否跟上面分析的一致。

也可以参考我写的TestKillActivity,单栈和多栈的情况下在高于API 28和低于API 28的手机上的表现。


总结:



  1. 系统内存不足时LMK会根据内存配置项来kill掉进程释放内存

  2. kill时会按照进程的ADJ规则来kill

  3. App内存不足时由GcWatcher来决定回收Activity的时机

  4. 可以使用getprop命令来查看当前手机的JVM内存分配和OOM配置

  5. releaseSomeActivities在API 28和API 28+的差别很大,低版本会根据Task数量来决定清理哪个task的。高版本简单粗暴,遍历activity,按照z order排序,优先release掉更老的activity。


参考资料:
Android lowmemorykiller分析
解读Android进程优先级ADJ算法
http://www.jianshu.com/p/3233c33f6…
juejin.cn/post/706306…
Android可见APP的不可见任务栈(TaskRecord)销毁分析


作者:Yocn
来源:juejin.cn/post/7231742100844871736
收起阅读 »

当你按下方向键,电视是如何寻找下一个焦点的

我工作的第一家公司主要做的是一个在智能电视上面运行的APP,其实就是一个安卓APP,也是混合开发的应用,里面很多页面是H5开发的。 电视我们都知道,是通过遥控器来操作的,没有鼠标也不能触屏,所以“点击”的操作变成了按遥控器的“上下左右确定”键,那么必然需要一个...
继续阅读 »

我工作的第一家公司主要做的是一个在智能电视上面运行的APP,其实就是一个安卓APP,也是混合开发的应用,里面很多页面是H5开发的。


电视我们都知道,是通过遥控器来操作的,没有鼠标也不能触屏,所以“点击”的操作变成了按遥控器的“上下左右确定”键,那么必然需要一个“焦点”来告诉用户当前聚焦在哪里。


当时开发页面使用的是一个前人开发的焦点库,这个库会自己监听方向键并且自动计算下一个聚焦的元素。


为什么时隔多年会突然想起这个呢,其实是因为最近在给我开源的思维导图添加方向键导航的功能时,想到其实和电视聚焦功能很类似,都是按方向键,来计算并且自动聚焦到下一个元素或节点:



那么如何寻找下一个焦点呢,结合我当时用的焦点库的原理,接下来实现一下。


1.最简单的算法


第一种算法最简单,根据方向先找出当前节点该方向所有的其他节点,然后再找出直线距离最近的一个,比如当按下了左方向键,下面这些节点都是符合要求的节点:



从中选出最近的一个即为下一个聚焦节点。


节点的位置信息示意如下:



focus(dir) {
// 当前聚焦的节点
let currentActiveNode = this.mindMap.renderer.activeNodeList[0]
// 当前聚焦节点的位置信息
let currentActiveNodeRect = this.getNodeRect(currentActiveNode)
// 寻找的下一个聚焦节点
let targetNode = null
let targetDis = Infinity
// 保存并维护距离最近的节点
let checkNodeDis = (rect, node) => {
let dis = this.getDistance(currentActiveNodeRect, rect)
if (dis < targetDis) {
targetNode = node
targetDis = dis
}
}
// 1.最简单的算法
this.getFocusNodeBySimpleAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})
// 找到了则让目标节点聚焦
if (targetNode) {
targetNode.active()
}
}

无论哪种算法,都是先找出所有符合要求的节点,然后再从中找出和当前聚焦节点距离最近的节点,所以维护最近距离节点的函数是可以复用的,通过参数的形式传给具体的计算函数。


// 1.最简单的算法
getFocusNodeBySimpleAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
}
) {
// 遍历思维导图节点树
bfsWalk(this.mindMap.renderer.root, node => {
// 跳过当前聚焦的节点
if (node === currentActiveNode) return
// 当前遍历到的节点的位置信息
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
let match = false
// 按下了左方向键
if (dir === 'Left') {
// 判断节点是否在当前节点的左侧
match = right <= currentActiveNodeRect.left
// 按下了右方向键
} else if (dir === 'Right') {
// 判断节点是否在当前节点的右侧
match = left >= currentActiveNodeRect.right
// 按下了上方向键
} else if (dir === 'Up') {
// 判断节点是否在当前节点的上面
match = bottom <= currentActiveNodeRect.top
// 按下了下方向键
} else if (dir === 'Down') {
// 判断节点是否在当前节点的下面
match = top >= currentActiveNodeRect.bottom
}
// 符合要求,判断是否是最近的节点
if (match) {
checkNodeDis(rect, node)
}
})
}

效果如下:


基本可以工作,但是可以看到有个很大的缺点,比如按上键,我们预期的应该是聚焦到上面的兄弟节点上,但是实际上聚焦到的是子节点:



因为这个子节点确实是在当前节点上面,且距离最近的,那么怎么解决这个问题呢,接下来看看第二种算法。


2.阴影算法


该算法也是分别处理四个方向,但是和前面的第一种算法相比,额外要求节点在指定方向上的延伸需要存在交叉,延伸处可以想象成是节点的阴影,也就是名字的由来:



找出所有存在交叉的节点后也是从中找出距离最近的一个节点作为下一个聚焦节点,修改focus方法,改成使用阴影算法:


focus(dir) {
// 当前聚焦的节点
let currentActiveNode = this.mindMap.renderer.activeNodeList[0]
// 当前聚焦节点的位置信息
let currentActiveNodeRect = this.getNodeRect(currentActiveNode)
// 寻找的下一个聚焦节点
// ...
// 保存并维护距离最近的节点
// ...

// 2.阴影算法
this.getFocusNodeByShadowAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})

// 找到了则让目标节点聚焦
if (targetNode) {
targetNode.active()
}
}

// 2.阴影算法
getFocusNodeByShadowAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
}
) {
bfsWalk(this.mindMap.renderer.root, node => {
if (node === currentActiveNode) return
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
let match = false
if (dir === 'Left') {
match =
left < currentActiveNodeRect.left &&
top < currentActiveNodeRect.bottom &&
bottom > currentActiveNodeRect.top
} else if (dir === 'Right') {
match =
right > currentActiveNodeRect.right &&
top < currentActiveNodeRect.bottom &&
bottom > currentActiveNodeRect.top
} else if (dir === 'Up') {
match =
top < currentActiveNodeRect.top &&
left < currentActiveNodeRect.right &&
right > currentActiveNodeRect.left
} else if (dir === 'Down') {
match =
bottom > currentActiveNodeRect.bottom &&
left < currentActiveNodeRect.right &&
right > currentActiveNodeRect.left
}
if (match) {
checkNodeDis(rect, node)
}
})
}

就是判断条件增加了是否交叉的比较,效果如下:


可以看到阴影算法成功解决了前面的跳转问题,但是它也并不完美,比如下面这种情况按左方向键找不到可聚焦节点了:



因为左侧没有存在交叉的节点,但是其实可以聚焦到父节点上,怎么办呢,我们先看一下下一种算法。


3.区域算法


所谓区域算法也很简单,把当前聚焦节点的四周平分成四个区域,对应四个方向,寻找哪个方向的下一个节点就先找出中心点在这个区域的所有节点,再从中选择距离最近的一个即可:



focus(dir) {
// 当前聚焦的节点
let currentActiveNode = this.mindMap.renderer.activeNodeList[0]
// 当前聚焦节点的位置信息
let currentActiveNodeRect = this.getNodeRect(currentActiveNode)
// 寻找的下一个聚焦节点
// ...
// 保存并维护距离最近的节点
// ...

// 3.区域算法
this.getFocusNodeByAreaAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})

// 找到了则让目标节点聚焦
if (targetNode) {
targetNode.active()
}
}

// 3.区域算法
getFocusNodeByAreaAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
}
) {
// 当前聚焦节点的中心点
let cX = (currentActiveNodeRect.right + currentActiveNodeRect.left) / 2
let cY = (currentActiveNodeRect.bottom + currentActiveNodeRect.top) / 2
bfsWalk(this.mindMap.renderer.root, node => {
if (node === currentActiveNode) return
let rect = this.getNodeRect(node)
let { left, top, right, bottom } = rect
// 遍历到的节点的中心点
let ccX = (right + left) / 2
let ccY = (bottom + top) / 2
// 节点的中心点坐标和当前聚焦节点的中心点坐标的差值
let offsetX = ccX - cX
let offsetY = ccY - cY
if (offsetX === 0 && offsetY === 0) return
let match = false
if (dir === 'Left') {
match = offsetX <= 0 && offsetX <= offsetY && offsetX <= -offsetY
} else if (dir === 'Right') {
match = offsetX > 0 && offsetX >= -offsetY && offsetX >= offsetY
} else if (dir === 'Up') {
match = offsetY <= 0 && offsetY < offsetX && offsetY < -offsetX
} else if (dir === 'Down') {
match = offsetY > 0 && -offsetY < offsetX && offsetY > offsetX
}
if (match) {
checkNodeDis(rect, node)
}
})
}

比较的逻辑可以参考下图:



效果如下:


结合阴影算法和区域算法


前面介绍阴影算法时说了它有一定局限性,区域算法计算出的结果则可以对它进行补充,但是理想情况下阴影算法的结果是最符合我们的预期的,那么很简单,我们可以把它们两个结合起来,调整一下顺序,先使用阴影算法计算节点,如果阴影算法没找到,那么再使用区域算法寻找节点,简单算法也可以加在最后:


focus(dir) {
// 当前聚焦的节点
let currentActiveNode = this.mindMap.renderer.activeNodeList[0]
// 当前聚焦节点的位置信息
let currentActiveNodeRect = this.getNodeRect(currentActiveNode)
// 寻找的下一个聚焦节点
// ...
// 保存并维护距离最近的节点
// ...

// 第一优先级:阴影算法
this.getFocusNodeByShadowAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})

// 第二优先级:区域算法
if (!targetNode) {
this.getFocusNodeByAreaAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})
}

// 第三优先级:简单算法
if (!targetNode) {
this.getFocusNodeBySimpleAlgorithm({
currentActiveNode,
currentActiveNodeRect,
dir,
checkNodeDis
})
}

// 找到了则让目标节点聚焦
if (targetNode) {
targetNode.active()
}
}

效果如下:


1.gif


是不是很简单呢,详细体验可以点击思维导图


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

从拉马努金的传奇,看AI发展的必要

大家好啊,我是董董灿。 讲一个印度传奇数学家——拉马努金的故事。 如果有个人跑过来告诉你,所有的自然数之和等于一个负数。你会有什么反应? 我的第一反应是:Are you kidding me? 而数学家拉马努金说,是真的,我可以证明。 印度传奇数学家——拉...
继续阅读 »

大家好啊,我是董董灿。


讲一个印度传奇数学家——拉马努金的故事。


如果有个人跑过来告诉你,所有的自然数之和等于一个负数。你会有什么反应?


图片


我的第一反应是:Are you kidding me? 而数学家拉马努金说,是真的,我可以证明。


图片


印度传奇数学家——拉马努金在他的著作中给出了很多关于无穷级数的等式,其中就包括上面的自然数之和恒等式。


这个等式看似不合理,但已经被很多数学家证明,其中就包括欧拉、黎曼还有拉马努金。(证明过程大家可以搜索下,肯定能看懂)


数学天才


我一度认为,欧拉公式是世界上最美的公式,因为只有神才能将无理数、有理数、虚数单位、圆周率以及最简单的两个自然数0和1,用一个简单的不能再简单的加法公式来表示,而且是恒等式!


图片


公式中透露着一种无法言说的美感和沧桑感,像在预示着世界末日来临时,万生万物相互作用,终归会趋于虚无。


直到某一天,我看了一部电影,印度传记片《知无涯者》,才知道,原来神不止有一个;原来,最美的公式,不止一个。


图片


自古天才出贫穷。


拉马努金也一样,出生在印度一个贫穷家庭。在去剑桥见到著名数学家哈代之前,拉马努金甚至都没有系统的学习过数学,没错,是个野路子出身。


但是,这不妨碍他已经靠直觉发现了整整两本数学公式了,而且,与民科不同的是,他的公式,都经受住了历史的考验。


只不过,他自己不会证明。


他只知道,这些公式是正确的。凭着直觉,想到一个公式,就写下来,整整记录了两本。


公式中有这样的


图片


有这样的


图片


还有这样的


图片


可以说,拉马努金将人类对于整数和无穷级数的直觉开发到了极致!


熟悉数学的人看到这些,估计和我刚看到的表情是一样的。就连当年哈代在剑桥第一次见到这些公式的时候,也怀疑这是个骗子。


图片


这些等式真的成立么?



“喂,最后一个,没错说的就是你,计算圆周率倒数的那个,你就用一堆加加乘乘的数,可以精确的表示一个圆周率么,那可是无限不循环的无理数啊!还有,你那分母上写着的 9801 的常数项是咋来的?靠直觉写的么?我用 9800 行不行?”



用9800还真不行!


对于第三个计算圆周率的公式,我们可以很轻松的验证其正确性。当我们取K为0时,计算出来的圆周率的值已经逼近了π=3.1415927,如果再让K =1, 那么精度直逼 π=3.14159265359。


你以为这就完了?


拉马努金总共写了14个计算圆周率的公式,个个令人匪夷所思。


图片


拉马努金的一生,一共发现了3000多个公式,以至于后世的很多科学家,靠证明拉马努金的公式,获得了很多数学大奖,包括数学界最有名的菲尔兹奖。


更可怕的是,在他去世的前一年,留下的一些公式,最近被证明其实与描述黑洞有关。


写到这里,我不由自主地膜拜起来——


如果不是神发现了他在泄漏宇宙秘密,会封了他的号,年仅30多岁就英年早逝么?


如果你也对他感兴趣了,可以Google一下,或者去b站观看他的纪录片。


为什么拉马努金的公式这么重要?


因为他的公式涉及到了大量的无穷级数和无理数的逼近等式,且逼近精度高的惊人,而且收敛速度很快。


由于目前的计算机架构都是冯诺依曼架构,任何的计算都需要取指、译码、读写内存、计算等步骤,如果计算所需要的中间数据过多,那么势必会拖慢计算机运行的效率。


图片


现代计算机体系里,对于的加减乘除四则运算,基本上只有加法器和乘法器来实现,其他的复杂运算,也是在加法和乘法的基础上,外加移位或者一些与或非的逻辑电路来组合实现的。


一个简单的除法,在计算机里,就可能会涉及到多条加法、与或非、移位的指令。更别提进行大量科学计算或者人工智能计算的运算量了。


大量的组合运算,会产生大量的中间数据。这些数据都是会访问内存,一旦有内存访问,就有延时开销。


一旦延时,计算就会被拖慢。


快速求平方根倒数


你可能听过一个著名的求快速平方根倒数算法的故事,计算下面的公式。


图片


在著名游戏《雷神之锤3》中,有一个程序员写出了令人费解的代码,来计算一个数的平方根倒数。


float Q_rsqrt(float number) {
long i;
float x2, y;
const float threehalfs = 1.5F;
x2 = number * 0.5F;
y = number;
i = *(long *) &y;
i = 0x5F3759DF - (i >> 1);
y = *(float *) &i;
y = y * (threehalfs - (x2 * y * y));
return y;
}

代码中有几处是常数,比如 0x5F3759DF,如果不深究计算机的内存分配以及浮点数的数据格式,我想,大部分人都是看不懂这个常数项的。


常数项的存在,在计算机的计算流中,仅仅有一步读内存操作,少了很多中间数据的计算。


这也是为什么,在现代高性能(HPC)计算场景下,人们大都倾向于把需要计算的数据先保存下来,随用随取,以提高计算性能。


空间换时间


拉马努金的公式,就有这样的作用。


而且,效果比要我们自己设计的空间换时间的方法好的多,因为这些公式,早已把需要参与计算的值都写在了公式里,而这些值,一般人是推不出来的。



"海洋学家要计算海啸模型,这需要非常复杂的数学计算,不用一些技巧是没法计算的。但只要用拉马努金提供的公式,海啸模型就能大大简化,把不能计算,变成可以计算。"



科学的进步,往往伴随着灵感的出现而有大跃进。就好像坐在苹果树下的牛顿一样,一个苹果,砸出了一个经典物理学。


图片


拉马努金就有这样的直觉和灵感。于是,Google认识到了这个问题,拉马努金机出现了。


拉马努金机


人会消亡,机器不会消亡。


拉马努金虽然英年早逝了,但他的思想要是能保存下来,人类一样会受益无穷。


图片


于是Google在2019年,立项成立了创建拉马努金机的项目。得益于近些年人工智能技术的发展,拉马努金的项目运行的还算不错。


所谓拉马努金机,其实就是训练一个人工智能算法来模仿拉马努金的思考方式,然后生成一堆的数学公式,让人类科学家们去证明这些公式的正确性。


人类科学家给AI当助手,去证明AI靠直觉写出来的公式的正确性。


据说,这个项目已经取得了不错的进展,拉马努金机已经写出了很多公式,其中就包括高斯一生所发现的关于π的一些经验公式。


或许在不久的将来,拉马努金机真的可以发现自然界中的秘密也未可知。


One More Thing


作为继牛顿之后最伟大的物理学家,爱因斯坦去世后,他的大脑被切分成240片,永久的保存下来供人研究。


如果当时有了更先进的AI技术,或许保存下来的不是爱因斯坦的大脑,而是他的思想。


本文作者原创,请勿随意转载,转载请联系作者哦,作者很好说话的


作者:董董灿是个攻城狮
来源:juejin.cn/post/7231553447940718651
收起阅读 »

蒙提霍尔问题

web
最近看韩国电视剧【D.P:逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题 意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余...
继续阅读 »

f1e232d158d085038667d793dad96dc5.jpeg


最近看韩国电视剧【D.P逃兵追缉令】里面提到一个有趣的数学概率游戏 -> 蒙提霍尔问题


意思是:参赛者会看见三扇门,其中一扇门的里面有一辆汽车,选中里面是汽车的那扇门,就可以赢得该辆汽车,另外两扇门里面则都是一只山羊,让你任意选择其中一个,然后打开其余两个门中的一个并且是山羊(去掉一个错误答案),这时,让你重新选择。那么你是会坚持原来的选择,还是换选另外一个未被打开过的门呢?


大家可以想一想如果是自己,我们是会换还是不会换?


好了,我当时看到后感觉很有意思,所以我简单写了一套代码,源码贴在下面,大家可以验证一下,先告诉大家,换赢得汽车的概率是2/3,不换赢得汽车的概率是1/3


<header>
<h1>请选择换不换?</h1><button class="refresh">刷新</button>
</header>
<section>
<div class="box">
<h2>1</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
<div class="box">
<h2>2</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
<div class="box">
<h2>3</h2>
<canvas width="300" height="100"></canvas>
<div class="prize">奖品</div>
</div>
</section>
<span>请选择号码牌</span>
<select name="" id="">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button class="confirm">确认</button>
<span class="confirm-text"></span>
<span class="opater">
<button class="change"></button>
<button class="no-change">不换</button>
</span>
<p>
<strong>游戏规则:</strong>
<span>
上面有三个号码牌,其中一个号码牌的里面有汽车,选中里面是汽车的号码牌,
你就可以赢得该辆汽车,另外两个号码牌里面则都是一只山羊,
你任意选择其中一个,然后打开其余两个号码牌中的一个并且是山羊(去掉一个错误答案),
这时,你有一个重新选择的机会,你选择换还是不换?
</span>
</p>

.prize {
width: 300px;
height: 100px;
background-color: pink;
font-size: 36px;
line-height: 100px;
text-align: center;
position: absolute;
}

canvas {
position: absolute;
z-index: 2;
}

section {
display: flex;
}

.box {
width: 300px;
height: 200px;
cursor: pointer;
}

.box+.box {
margin-left: 8px;
}

header {
display: flex;
align-items: center;
}

header button {
margin-left: 8px;
height: 24px;
}
p {
width: 400px;
background-color: pink;
}

function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function getRandomNumber() {
return Math.random() > 0.5 ? 1 : 2;
}
let a1 = [0, 1, 2]
let i1 = undefined
let i2 = undefined
let isChange = false
const opater = document.querySelector('.opater')
opater.style.display = 'none'
// 随机一个奖品
const prizes = document.querySelectorAll('.prize')
let a0 = [0, 1, 2]
a0 = shuffleArray(a0)
a0.forEach((v,i) => {
const innerText = !!v ? '山羊' : '汽车'
prizes[i].innerText = innerText
})

const canvas = document.querySelectorAll('canvas')
const confirmText = document.querySelector('.confirm-text')
canvas.forEach(c => {
// 使用canvas实现功能
// 1. 使用canvas绘制一个灰色的矩形
const ctx = c.getContext('2d')
ctx.fillStyle = '#ccc'
ctx.fillRect(0, 0, c.width, c.height)
// 2. 刮奖逻辑
// 鼠标按下且移动的时候,需要擦除canvas画布
let done = false
c.addEventListener('mousedown', function () {
if (i1 === undefined) return alert('请先选择号码牌,并确认!')
if (!isChange) return alert('请选择换不换!')
done = true
})
c.addEventListener('mousemove', function (e) {
if (done) {
// offsetX 和 offsetY 可以获取到鼠标在元素中的偏移位置
const x = e.offsetX - 5
const y = e.offsetY - 5
ctx.clearRect(x, y, 10, 10)
}
})
c.addEventListener('mouseup', function () {
done = false
})
})
const confirm = document.querySelector('.confirm')
const refresh = document.querySelector('.refresh')
confirm.onclick = function () {
let select = document.querySelector('select')
const options = Array.from(select.children)
confirmText.innerText = `您选择的号码牌是${select.value},请问现在换不换?`
// 选择后,去掉一个错误答案
// i1是下标
i1 = select.value - 1
// delValue是值
let delValue = undefined
// 通过下标找值
if (a0[i1] === 0) {
delValue = getRandomNumber()
} else {
delValue = a0[i1] === 1 ? 2 : 1
}
// 通过值找下标
i2 = a0.indexOf(delValue)
// 选择的是i1, 去掉的是
const ctx = canvas[i2].getContext('2d')
ctx.clearRect(0, 0, 300, 100)
options.map(v => v.disabled = true)
confirm.style.display = 'none'
opater.style.display = 'inline-block'
}
const change = document.querySelector('.change')
const noChange = document.querySelector('.no-change')
change.onclick = function () {
isChange = true
const x = a1.filter(v => v !== i1 && v !== i2)
confirmText.innerText = `您确认选择的号码牌是${x[0] + 1},请刮卡!`
opater.style.display = 'none'
}
noChange.onclick = function () {
isChange = true
confirmText.innerText = `您确认选择的号码牌是${i1 + 1},请刮卡!`
opater.style.display = 'none'
}
refresh.onclick = function () {
window.location.reload()
}

作者:JoyZ
来源:juejin.cn/post/7278684023757553727
收起阅读 »

js数组方法分类

web
js数组方法分类 0.前言 我们知道,js中数组方法非常多,MDN就列举了43个方法,就连常用方法都很多,比如forEach,filter,map,push等等等,可能我们见到方法认识这个方法,但要我们列举所知道的数组方法,我们可能会遗忘漏掉某些,为了帮助大家...
继续阅读 »

js数组方法分类


0.前言


我们知道,js中数组方法非常多,MDN就列举了43个方法,就连常用方法都很多,比如forEach,filter,map,push等等等,可能我们见到方法认识这个方法,但要我们列举所知道的数组方法,我们可能会遗忘漏掉某些,为了帮助大家更好更有规律地记住更多方法,在这里我特地将数组方法分俄为七大类,每一类都有其特定共同点和功能的标签,根据这些标签去记忆,相信大家读完可以感到醍醐灌顶的感觉。


一共2+4+9+7+6+3+2=33个,放心吧,足够啦!


1.创建数组方法



  • Array.from() :将可迭代对象或类数组对象转化为新的浅拷贝数组.

  • Array.of():将可变数量的参数转化为新的浅拷贝 数组.


//Array.from()
console.log(Array.from("foo")); // ['f', 'o', 'o']
function bar() {
 console.log(arguments); //Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ] 类数组
 console.log(Array.from(arguments)); // [1, 2, 3]
}
bar(1, 2, 3);
const set = new Set(["foo", "bar", "baz", "foo"]);
console.log(Array.from(set)); //从Set构建数组['foo', 'bar', 'baz'],Map也可以

//Array.of()
console.log(Array.of()); //[] 创建空数组
console.log(Array.of(1, 2, 3, 4)); //[1, 2, 3, 4]
//浅拷贝
const obj1 = { age: 18 };
const arr1 = [666, 777];
const arr = Array.of(obj1, arr1);
arr[0].age = 19;
arr[1][0] = 999;
console.log(arr); //[{age:19},[999,777]]


2.数组首端或尾端添加删除方法



  • Array.prototype.push():将指定的元素添加到数组的末尾,并返回新的数组长度.

  • Array.prototype.pop():从数组中删除最后一个元素,并返回该元素的值。此方法会更改数组的长度.

  • Array.prototype.shift():从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度.

  • Array.prototype.unshift():将指定的元素添加到数组的开头,并返回新的数组长度.


//Array.prototype.push()
const arr = [1, 2];
console.log(arr.push(3, 4, 5)); //5
console.log(arr); //[ 1, 2, 3, 4, 5 ]
//Array.prototype.pop()
console.log(arr.pop()); //数组最后一个元素:5
console.log(arr); //[ 1, 2, 3, 4 ]
//Array.prototype.shift()
console.log(arr.shift()); //1
console.log(arr); //[ 2, 3, 4 ]
//Array.prototype.unshift()
console.log(arr.unshift(66, 77, 88)); //6
console.log(arr); //[ 66, 77, 88, 2, 3, 4 ]

3.操作数组方法



  1. Array.prototype.concat():用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组.

  2. Array.prototype.copyWithin():浅复制数组的一部分到同一数组中的另一个位置,并返回该数组,不会改变原数组的长度.

  3. Array.prototype.fill():用一个固定值填充一个数组中从起始索引(默认为 0)到终止索引(默认为 array.length)内的全部元素。它返回修改后的数组。会改变原始数组.


// Array.prototype.concat()
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [7, 8, 9];
const arr4 = arr1.concat(arr2, arr3); //[1, 2, 3, 4, 5, 6, 7, 8, 9]
// Array.prototype.copyWithin()
const arr = [1, 2, 3, 4, 5, 6];
console.log(arr.copyWithin(2, 3, 5)); //[ 1, 2, 4, 5, 5, 6 ] 将 4,5替换到2索引位置
// Array.prototype.fill()
const array1 = [1, 2, 3, 4];
console.log(array1.fill(0, 2, 4)); //[ 1, 2, 0, 0 ]
console.log(array1.fill(5, 1)); //[ 1, 5, 5, 5 ]
console.log(array1.fill(6)); //[ 6, 6, 6, 6 ]
console.log(array1); //[ 6, 6, 6, 6 ]


  1. Array.prototype.flat():展开嵌套数组,默认嵌套深度为1,不改变原数组,返回新数组.

  2. Array.prototype.join():用逗号或指定分隔符将数组连接成字符串.

  3. Array.prototype.reverse():就地反转字符串,返回同一数组的引用,原数组改变.


// Array.prototype.flat()
const arr1 = [1, 2, [3, 4]];
console.log(arr1.flat()); //[ 1, 2, 3, 4 ]
console.log(arr1); // 不改变原数组 [ 1, 2, [ 3, 4 ] ]
const arr2 = [1, 2, [3, 4, [5, 6]]];
console.log(arr2.flat()); //默认展开嵌套一层数组[ 1, 2, 3, 4, [ 5, 6 ] ]
console.log(arr2.flat(2)); //展开嵌套二层数组 [ 1, 2, 3, 4, 5, 6 ]
// Array.prototype.join()
const elements = ["Fire", "Air", "Water"];
console.log(elements.join()); //"Fire,Air,Water"
console.log(elements.join("+++++")); //Fire+++++Air+++++Water
console.log(elements.join("-")); //Fire-Air-Water
// Array.prototype.reverse()
const arr = [1, 2, 3];
console.log(arr.reverse()); //[3,2,1]
console.log(arr); //[3,2,1]


  1. Array.prototype.slice():截取数组,返回一个新数组,不改变原数组.

  2. Array.prototype.sort():排序数组,改变原数组,默认排序规则是将数组每一项转化为字符串,根据utf-16码升值排序.

  3. Array.prototype.splice():对数组进行增加、删除、替换元素,改变原数组.


// Array.prototype.slice();
const animals = ["ant", "bison", "camel", "duck", "elephant"];
console.log(animals.slice(2)); //["camel", "duck", "elephant"]
console.log(animals.slice(2, 4)); //["camel", "duck"]
console.log(animals.slice(-2)); //["duck", "elephant"]
console.log(animals.slice(2, -1)); //["camel", "duck"]
console.log(animals.slice()); //浅复制数组 ["ant", "bison", "camel", "duck", "elephant"]
// Array.prototype.sort();
const months = ["March", "Jan", "Feb", "Dec"];
months.sort();
console.log(months); // ["Dec", "Feb", "Jan", "March"];
const array1 = [1, 30, 4, 21, 100000];
array1.sort();
console.log(array1); //[1, 100000, 21, 30, 4]
array1.sort((a, b) => a - b); //升序
console.log(array1);
//Array.prototype.splice();
const arr = [1, 2, 3, 4, 5];
arr.splice(2, 2); //从index为2的位置开始删除两个元素[1, 2, 5];
arr.splice(2, 0, 3, 4); //从index为2的位置增加34两个元素 [1,2,3,4,5]
arr.splice(2, 2, 7, 8); //删除index为2位置的两个元素,并添加89两个元素 [ 1, 2, 7, 8, 5 ]

4.查找元素或索引方法



  1. Array.prototype.at():返回索引位置对应的元素,负索引从数组最后一个元素倒数开始.

  2. Array.prototype.find():查找符合条件的第一个元素,未找到则返回undefined,回调函数返回值为真则符合条件.

  3. Array.prototype.findIndex():查找符合条件第一个元素的索引,未找到则返回**-1**,回调函数返回值为真则符合条件.

  4. Array.prototype.findLast():从后往前查找符合条件的第一个元素,其余同理Array.prototype.find().

  5. Array.prototype.findLastIndex():从后往前查找符合条件第一个元素的索引,其余同理Array.prototype.findIndex().


// Array.prototype.at()
const arr = [1, 2, 3, 4, 5];
console.log(arr.at(0)); //1
console.log(arr.at(-1)); //5
const array = [
{ name: "jack", age: 15 },
{ name: "tom", age: 29 },
{ name: "bob", age: 23 },
];
// Array.prototype.find()
const obj = array.find((item) => {
 if (item.age > 18) {
   return true;
} else {
   return false;
}
}); //{ name: 'tom', age: 29 }
//Array.prototype.findIndex()
const objIndex = array.findIndex((item) => {
 if (item.age > 18) {
   return true;
} else {
   return false;
}
}); //1
// Array.prototype.findLast()
const lastObj = array.findLast((item) => {
 if (item.age > 18) {
   return true;
} else {
   return false;
}
}); //{name: 'bob', age: 23}
// Array.prototype.findLast()
const lastIndex = array.findLastIndex((item) => {
 if (item.age > 18) {
   return true;
} else {
   return false;
}
}); //2


  1. Array.prototype.indexOf():返回数组中给定元素第一次出现的下标,如果不存在则返回-1.

  2. Array.prototype.includes():在数组中查找指定元素,如果找到则返回true,如果找不到则返回false.


//Array.prototype.indexOf()
const arr = [1, 2, 6, 8, 9];
console.log(arr.indexOf(6)); //2
console.log(arr.indexOf(10)); //-1
//Array.prototype.includes()
console.log(arr.includes(6)); //true
console.log(arr.includes(10)); //-false

5.迭代方法


迭代方法非常常用,这里就不列举例子了.



  1. Array.prototype.forEach():对数组每一项元素执行给定的函数,没有返回值.

  2. Array.prototype.filter():过滤数组,创建符合条件的浅拷贝数组.

  3. Array.prototype.map():对数组每个元素执行给定函数映射一个新值,返回新数组.

  4. Array.prototype.every():检查数组所有元素是否符合条件,如果符合返回true,不符合返回false;

  5. Array.prototype.some():检查数组中是否有元素符合条件,如果有则返回true,不符合返回false

  6. Array.prototype.reduce():用指定函数迭代数组每一项,上一次函数返回值作为下一次函数初始值,返回最后一次函数的最终返回值.


6. 迭代器方法


这里就不赘述迭代器对象了.



  1. Array.prototype.keys():返回数组索引迭代器对象.

  2. Array.prototype.values():返回数组元素的迭代器对象.

  3. Array.prototype.entries():返回数组索引和元素构成的迭代器对象.


7.额外重要方法



  1. Array.isArray():判断是否是数组.


//都返回true 都是数组
console.log(Array.isArray([]));
console.log(Array.isArray(new Array()));
console.log(Array.isArray(Array.of(1, 2, 3)));
// 也可以用instanceof:true
console.log([] instanceof Array);
console.log(new Array() instanceof Array);
console.log(Array.of(1, 2, 3) instanceof Array);
console.log([].toString());
//惊喜:最后还可以使用Object.prototype.toString()
console.log(Object.prototype.toString.call([])); //[object Array]


  1. Array.prototype.toString():将数组去掉左右括号转化为字符串.


const array1 = [1, 2, "a", "1a"];
console.log(array1.toString()); // "1,2,a,1a"

作者:樊阳子
来源:juejin.cn/post/7288234800563961917
收起阅读 »

我说ArrayList初始容量是10,面试官让我回去等通知

引言 在Java集合中,ArrayList是最常用到的数据结构,无论是在日常开发还是面试中,但是很多人对它的源码并不了解。下面提问几个问题,检验一下大家对ArrayList的了解程度。 ArrayList的初始容量是多少?(90%的人都会答错) ArrayL...
继续阅读 »

引言


在Java集合中,ArrayList是最常用到的数据结构,无论是在日常开发还是面试中,但是很多人对它的源码并不了解。下面提问几个问题,检验一下大家对ArrayList的了解程度。



  1. ArrayList的初始容量是多少?(90%的人都会答错)

  2. ArrayList的扩容机制

  3. 并发修改ArrayList元素会有什么问题

  4. 如何快速安全的删除ArrayList中的元素


接下来一块分析一下ArrayList的源码,看完ArrayList源码之后,可以轻松解答上面四个问题。


简介


ArrayList底层基于数组实现,可以随机访问,内部使用一个Object数组来保存元素。它维护了一个 elementData 数组和一个 size 字段,elementData数组用来存放元素,size字段用于记录元素个数。它允许元素是null,可以动态扩容。
image.png


初始化


当我们调用ArrayList的构造方法的时候,底层实现逻辑是什么样的?


// 调用无参构造方法,初始化ArrayList
List<Integer> list1 = new ArraryList<>();

// 调用有参构造方法,初始化ArrayList,指定容量为10
List<Integer> list1 = new ArraryList<>(10);

看一下底层源码实现:


// 默认容量大小
private static final int DEFAULT_CAPACITY = 10;

// 空数组
private static final Object[] EMPTY_ELEMENTDATA = {};

// 默认容量的数组对象
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

// 存储元素的数组
transient Object[] elementData;

// 数组中元素个数,默认是0
private int size;

// 无参初始化,默认是空数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

// 有参初始化,指定容量大小
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 直接使用指定的容量大小
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
}
}

可以看到当我们调用ArrayList的无参构造方法 new ArraryList<>() 的时候,只是初始化了一个空对象,并没有指定数组大小,所以初始容量是零。至于什么时候指定数组大小,接着往下看。


添加元素


再看一下往ArrayList种添加元素时,调用的 add() 方法源码:


// 添加元素
public boolean add(E e) {
// 确保数组容量够用,size是元素个数
ensureCapacityInternal(size + 1);
// 直接在下个位置赋值
elementData[size++] = e;
return true;
}

// 确保数组容量够用
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

// 计算所需最小容量
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果数组等于空数组,就设置默认容量为10
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}

// 确保容量够用
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// 如果所需最小容量大于数组长度,就进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

看一下扩容逻辑:


// 扩容,就是把旧数据拷贝到新数组里面
private void grow(int minCapacity) {
int oldCapacity = elementData.length;
// 计算新数组的容量大小,是旧容量的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);

// 如果扩容后的容量小于最小容量,扩容后的容量就等于最小容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;

// 如果扩容后的容量大于Integer的最大值,就用Integer最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);

// 扩容并赋值给原数组
elementData = Arrays.copyOf(elementData, newCapacity);
}

可以看到:



  • 扩容的触发条件是数组全部被占满

  • 扩容是以旧容量的1.5倍扩容,并不是2倍扩容

  • 最大容量是Integer的最大值

  • 添加元素时,没有对元素校验,允许为null,也允许元素重复。


再看一下数组拷贝的逻辑,这里都是Arrays类里面的方法了:


/**
* @param original 原数组
* @param newLength 新的容量大小
*/

public static <T> T[] copyOf(T[] original, int newLength) {
return (T[]) copyOf(original, newLength, original.getClass());
}

public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
// 创建一个新数组,容量是新的容量大小
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
// 把原数组的元素拷贝到新数组
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

最终调用了System类的数组拷贝方法,是native方法:


/**
* @param src 原数组
* @param srcPos 原数组的开始位置
* @param dest 目标数组
* @param destPos 目标数组的开始位置
* @param length 被拷贝的长度
*/

public static native void arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length)
;

总结一下ArrayList的 add() 方法的逻辑:



  1. 检查容量是否够用,如果够用,直接在下一个位置赋值结束。

  2. 如果是第一次添加元素,则设置容量默认大小为10。

  3. 如果不是第一次添加元素,并且容量不够用,则执行扩容操作。扩容就是创建一个新数组,容量是原数组的1.5倍,再把原数组的元素拷贝到新数组,最后用新数组对象覆盖原数组。


需要注意的是,每次扩容都会创建新数组和拷贝数组,会有一定的时间和空间开销。在创建ArrayList的时候,如果我们可以提前预估元素的数量,最好通过有参构造函数,设置一个合适的初始容量,以减少动态扩容的次数。


删除单个元素


再看一下删除元素的方法 remove() 的源码:


public boolean remove(Object o) {
// 判断要删除的元素是否为null
if (o == null) {
// 遍历数组
for (int index = 0; index < size; index++)
// 如果和当前位置上的元素相等,就删除当前位置上的元素
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
// 遍历数组
for (int index = 0; index < size; index++)
// 如果和当前位置上的元素相等,就删除当前位置上的元素
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}

// 删除该位置上的元素
private void fastRemove(int index) {
modCount++;
// 计算需要移动的元素的个数
int numMoved = size - index - 1;
if (numMoved > 0)
// 从index+1位置开始拷贝,也就是后面的元素整体向左移动一个位置
System.arraycopy(elementData, index+1, elementData, index, numMoved);
// 设置数组最后一个元素赋值为null,防止会导致内存泄漏
elementData[--size] = null;
}

删除元素的流程是:



  1. 判断要删除的元素是否为null,如果为null,则遍历数组,使用双等号比较元素是否相等。如果不是null,则使用 equals() 方法比较元素是否相等。这里就显得啰嗦了,可以使用 Objects.equals()方法,合并ifelse逻辑。

  2. 如果找到相等的元素,则把后面位置的所有元素整体相左移动一个位置,并把数组最后一个元素赋值为null结束。


可以看到遍历数组的时候,找到相等的元素,删除就结束了。如果ArrayList中存在重复元素,也只会删除其中一个元素。


批量删除


再看一下批量删除元素方法 removeAll() 的源码:


// 批量删除ArrayList和集合c都存在的元素
public boolean removeAll(Collection<?> c) {
// 非空校验
Objects.requireNonNull(c);
// 批量删除
return batchRemove(c, false);
}

private boolean batchRemove(Collection<?> c, boolean complement){
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
// 把需要保留的元素左移
elementData[w++] = elementData[r];
} finally {
// 当出现异常情况的时候,可能不相等
if (r != size) {
// 可能是其它线程添加了元素,把新增的元素也左移
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
// 把不需要保留的元素设置为null
if (w != size) {
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}

批量删除元素的逻辑,并不是大家想象的:



遍历数组,判断要删除的集合中是否包含当前元素,如果包含就删除当前元素。删除的流程就是把后面位置的所有元素整体左移,然后把最后位置的元素设置为null。



这样删除的操作,涉及到多次的数组拷贝,性能较差,而且还存在并发修改的问题,就是一边遍历,一边更新原数组。
批量删除元素的逻辑,设计充满了巧思,具体流程就是:



  1. 把需要保留的元素移动到数组左边,使用下标 w 做统计,下标 w 左边的是需要保留的元素,下标 w 右边的是需要删除的元素。

  2. 虽然ArrayList不是线程安全的,也考虑了并发修改的问题。如果上面过程中,有其他线程新增了元素,把新增的元素也移动到数组左边。

  3. 最后把数组中下标 w 右边的元素都设置为null。


所以当需要批量删除元素的时候,尽量使用 removeAll() 方法,性能更好。


并发修改的问题


当遍历ArrayList的过程中,同时增删ArrayList中的元素,会发生什么情况?测试一下:


import java.util.ArrayList;
import java.util.List;

public class Test {

public static void main(String[] args) {
// 创建ArrayList,并添加4个元素
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(2);
list.add(3);
// 遍历ArrayList
for (Integer key : list) {
// 判断如果元素等于2,则删除
if (key.equals(2)) {
list.remove(key);
}
}
}
}

运行结果:


Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
at java.util.ArrayList$Itr.next(ArrayList.java:861)
at com.yideng.Test.main(Test.java:14)

报出了并发修改的错误,ConcurrentModificationException
这是因为 forEach 使用了ArrayList内置的迭代器,这个迭代器在迭代的过程中,会校验修改次数 modCount,如果 modCount 被修改过,则抛出ConcurrentModificationException异常,快速失败,避免出现不可预料的结果。


// ArrayList内置的迭代器
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = modCount;

// 迭代下个元素
public E next() {
// 校验 modCount
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E)elementData[lastRet = i];
}

// 校验 modCount 是否被修改过
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

如果想要安全的删除某个元素,可以使用 remove(int index) 或者 removeIf() 方法。


import java.util.ArrayList;
import java.util.List;

public class Test {

public static void main(String[] args) {
// 创建ArrayList,并添加4个元素
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(2);
list.add(3);
// 使用 remove(int index) 删除元素
for (int i = 0; i < list.size(); i++) {
if (list.get(i).equals(2)) {
list.remove(i);
}
}

// 使用removeIf删除元素
list.removeIf(key -> key.equals(2));
}

}

总结


现在可以回答文章开头提出的问题了吧:



  1. ArrayList的初始容量是多少?


答案:初始容量是0,在第一次添加元素的时候,才会设置容量为10。



  1. ArrayList的扩容机制


答案:



  1. 创建新数组,容量是原来的1.5倍。

  2. 把旧数组元素拷贝到新数组中

  3. 使用新数组覆盖旧数组对象

  4. 并发修改ArrayList元素会有什么问题


答案:会快速失败,抛出ConcurrentModificationException异常。



  1. 如何快速安全的删除ArrayList中的元素


答案:使用remove(int index)removeIf() 或者 removeAll() 方法。
我们知道ArrayList并不是线程安全的,原因是它的 add()remove() 方法、扩容操作都没有加锁,多个线程并发操作ArrayList的时候,会出现数据不一致的情况。
想要线程安全,其中一种方式是初始化ArrayList的时候使用 Collections.synchronizedCollection() 修饰。这样ArrayList所有操作都变成同步操作,性能较差。还有一种性能较好,又能保证线程安全的方式是使用 CopyOnWriteArrayList,就是下章要讲的。


// 第一种方式,使用 Collections.synchronizedCollection() 修饰
List<Integer> list1 = Collections.synchronizedCollection(new ArrayList<>());

// 第二种方式,使用 CopyOnWriteArrayList
List<Integer> list1 = new CopyOnWriteArrayList<>();

作者:一灯架构
来源:juejin.cn/post/7288963211071094842
收起阅读 »

前端代码重复度检测

web
在前端开发中,代码的重复度是一个常见的问题。重复的代码不仅增加了代码的维护成本,还可能导致程序的低效运行。为了解决这个问题,有许多工具和技术被用来检测和消除代码重复。其中一个被广泛使用的工具就是jscpd。 jscpd简介 jscpd是一款开源的JavaScr...
继续阅读 »

在前端开发中,代码的重复度是一个常见的问题。重复的代码不仅增加了代码的维护成本,还可能导致程序的低效运行。为了解决这个问题,有许多工具和技术被用来检测和消除代码重复。其中一个被广泛使用的工具就是jscpd


jscpd简介


jscpd是一款开源的JavaScript的工具库,用于检测代码重复的情况,针对复制粘贴的代码检测很有效果。它可以通过扫描源代码文件,分析其中的代码片段,并比较它们之间的相似性来检测代码的重复度。jscpd支持各种前端框架和语言,包括HTML、CSS和JavaScript等150种的源码文件格式。无论是原生的JavaScript、CSS、HTML代码,还是使用typescriptscssvuereact等代码,都能很好的检测出项目中的重复代码。


开源仓库地址:github.com/kucherenko/jscpd/tree/master


如何使用


使用jscpd进行代码重复度检测非常简单。我们需要安装jscpd。可以通过npmyarn来安装jscpd


npm install -g jscpd

yarn global add jscpd

安装完成后,我们可以在终端运行jscpd命令,指定要检测的代码目录或文件。例如,我们可以输入以下命令来检测当前目录下的所有JavaScript文件:


jscpd .

指定目录检测:


jscpd /path/to/code

在命令行执行成功后的效果如下图所示:



简要说明一下对应图中的字段内容:




  • Clone found (javascript):
    显示找到的重复代码块,这里是javascript文件。并且会显示重复代码在文件中具体的行数,便于查找。




  • Format:文件格式,这里是 javascript,还可以是 scss、markup 等。




  • Files analyzed:已分析的文件数量,统计被检测中的文件数量。




  • Total lines:所有文件的总行数。




  • Total tokens:所有的token数量,一行代码一般包含几个到几十个不等的token数量。




  • Clones found:找到的重复块数量。




  • Duplicated lines:重复的代码行数和占比。




  • Duplicated tokens:重复的token数量和占比。




  • Detection time:检测耗时。




工程配置


以上示例是比较简单直接检测单个文件或文件夹。当下主流的前端项目大多都是基于脚手架生成或包含相关前端工程化的文件,由于很多文件是辅助工具如依赖包、构建脚本、文档、配置文件等,这类文件都不需要检测,需要排除。这种情况下的工程一般使用配置文件的方式,通过选项配置规范 jscpd 的使用。


jscpd 的配置选项可以通过以下两种方式创建,增加的内容都一致无需区分对应的前端框架。


在项目根目录下创建配置文件 .jscpd.json,然后在该文件中增加具体的配置选项:


    {
"threshold": 0,
"reporters": ["html", "console", "badge"],
"ignore": ["**/__snapshots__/**"],
"absolute": true
}

也可直接在 package.json 文件中添加jscpd


    {
...
"jscpd": {
"threshold": 0.1,
"reporters": ["html", "console", "badge"],
"ignore": ["**/__snapshots__/**"],
"absolute": true,
"gitignore": true
}
...
}

简要介绍一下上述配置字段含义:



  • threshold:表示重复度的阈值,超过这个值,就会输出错误报警。如阈值设为 10,当重复度为18.1%时,会提示以下错误❌,但代码的检测会正常完成。


ERROR: jscpd found too many duplicates (18.1%) over threshold (10%)


  • reporters:表示生成结果检测报告的方式,一般有以下几种:

    • console:控制台打印输出

    • consoleFull:控制台完整打印重复代码块

    • json:输出 json 格式的报告

    • xml:输出 xml 格式的报告

    • csv:输出 csv 格式的报告

    • markdown:输出带有 markdown 格式的报告

    • html:生成html报告到html文件夹

    • verbose:输出大量调试信息到控制台



  • ignore:检测忽略的文件或文件目录,过滤一些非业务代码,如依赖包、文档或静态文件等

  • format:需要进行重复度检测的源代码格式,目前支持150多种,我们常用的如 javascript、typescript、css 等

  • absolute:在检测报告中使用绝对路径


除此之外还有很多其他的配置,有兴趣的可以看源码文档中有详细的介绍。


检测报告


完成以上jscpd配置后执行以下命令即可输出对应的重复检测报告。运行完毕后,jscpd会生成一个报告,展示每个重复代码片段的信息。报告中包含了重复代码的位置、相似性百分比和代码行数等详细信息。通过这些信息,我们可以有针对性的进行代码重构。


jscpd ./src -o 'report'

项目中的业务代码通常会选择放在 ./src 目录下,所以可以直接检测该目录下的文件,如果是放在其他目录下根据实际情况调整即可。
通过命令行参数-o 'report'输出检测报告到项目根目录下的 report 文件夹中,这里的report也可以自定义其他目录名称,输出的目录结构如下所示:



生成的报告页面如下所示:


项目概览数据:



具体重复代码的位置和行数:



默认检测重复代码的行数(5行)和tokens(50)比较小,所以产生的重复代码块可能比较多,在实际使用中可以针对检测范围进行设置,如下设置参数供参考:



  • 最小tokens:--min-tokens,简写 -k

  • 最小行数:--min-lines,简写 -l

  • 最大行数:--max-lines,简写 -x


jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'

为了更便捷的使用此命令,可将这段命令集成到 package.json 中的 scripts 中,后续只需执行 npm run jscpd 即可执行检测。如下所示:


"scripts": {
...
"jscpd": "jscpd ./src --min-tokens 200 --min-lines 20 -o 'report'",
...
}

忽略代码块


上面所提到的ignore可以忽略某个文件或文件夹,还有一种忽略方式是忽略文件中的某一块代码。由于一些重复代码在实际情况中是必要的,可以使用代码注释标识的方式忽略检测,在代码的首尾位置添加注释,jscpd:ignore-startjscpd:ignore-end 包裹代码即可。


在js代码中使用方式:


/* jscpd:ignore-start */
import lodash from 'lodash';
import React from 'react';
import {User} from './models';
import {UserService} from './services';
/* jscpd:ignore-end */

在CSS和各种预处理中与js中的用法一致:


/* jscpd:ignore-start */
.style {
padding: 40px 0;
font-size: 26px;
font-weight: 400;
color: #464646;
line-height: 26px;
}
/* jscpd:ignore-end */

在html代码中使用方式:



<meta data-react-helmet="true" name="theme-color" content="#cb3837"/>
<link data-react-helmet="true" rel="stylesheet" href="https://static.npmjs.com/103af5b8a2b3c971cba419755f3a67bc.css"/>
<link data-react-helmet="true" rel="apple-touch-icon" sizes="120x120" href="https://static.npmjs.com/58a19602036db1daee0d7863c94673a4.png"/>
<link data-react-helmet="true" rel="icon" type="image/png" href="https://static.npmjs.com/b0f1a8318363185cc2ea6a40ac23eeb2.png" sizes="32x32"/>


总结


jscpd是一款强大的前端本地代码重复度检测工具。它可以帮助开发者快速发现代码重复问题,简单的配置即可输出直观的代码重复数据,通过解决重复的代码提高代码的质量和可维护性。


使用jscpd我们可以有效地优化前端开发过程,提高代码的效率和性能。希望本文能够对你了解基于jscpd的前端本地代码重复度检测有所帮助。




看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~


作者:南城FE
来源:juejin.cn/post/7288699185981095988
收起阅读 »

程序员的精力管理

今天跟大家分享一个主题,就是程序员的精力管理。工作8年多,我发现在职场里面会看到各种各样人,不同的人有不同的状态。大部分时候,我会看到一些刚刚毕业的校招生入职一段时间内朝气蓬勃,身体非常好,有永远用不完的精力一样,时时刻刻都保持在一种兴奋的状态。 更明显的是我...
继续阅读 »


今天跟大家分享一个主题,就是程序员的精力管理。工作8年多,我发现在职场里面会看到各种各样人,不同的人有不同的状态。大部分时候,我会看到一些刚刚毕业的校招生入职一段时间内朝气蓬勃,身体非常好,有永远用不完的精力一样,时时刻刻都保持在一种兴奋的状态。


更明显的是我发现工作了一段时间之后的人状态和精力就大不相同了,我有个师兄工作七八年了,每次看起来可能特别的疲惫,好像一天都打不起精神来。还有我也能发现,尽管工作了十几年的同事依然精力旺盛,神采奕奕。特别是有一些比较特殊的角色,比如说主管或者总监,往往身居高位却依然精力旺盛,不管是在日常沟通的时候,还是发表演讲的时候,都充满精力,激情澎湃。当然也有很多总监级别的大佬,在台上讲话无精打采,在台下就直接打瞌睡。


我记得有一次我北京的总监出差到杭州拉了一个小群,就组织我们去跑步,跑的是10km。我们在杭师大就开始跑了起来,我们的总监年过40岁全程一马当先,最后跑完的时候基本上领先了我们几个30岁不到同事快一圈了。我跟其他几个同事就感叹说,想不到总监不仅位置也比我们高,身体比还要好,这上哪说理去。


由此而知,不管是什么样的一个层级,至少大部分人在刚开始参加工作的时候都是精力活跃,充满斗志的,但为什么越来越工作久了以后会发生如此大的分叉呢,到底是什么原因能够损耗我们的精力,我们又应该如何管理我们的精力呢?


首先要明白,一个人的精力是有限的,哪怕再厉害的人精力都有限的。工作久了后,整天都活力旺盛的人也几乎不存在的。从科学的角度看来,像类似于人这样的生物体,它的整体精力表现一定是呈现一个正态分布的。所以说大部分人的精力管理都在一个正常的水平上。那么是什么因素导致这些人在工作一段时间后经历表现的特别的不一样?特别是我总监这样的人,基本上天天感觉精力爆棚,仅仅是因为高管不干活吗?我觉得不是的,真正的答案就是精力管理。


有科学家说过,人的精力就跟我们的电池一样,需要反复的充放电。正常情况下来说,我们在休息完成以后,刚刚醒来之后的一段时间,精力是特别旺盛的,经过了一天的各种事务以后,我们会发现我们的精力会越来越少,直到最后完全打不起任何精神。比如我就是在上午精神很好,到了下午基本上就是打蔫的状态,完全做不了耗脑力的事情。同时每个人也有他不同的精力旺盛的时间段,比如有的人在下午会特别的精神,有的人会在晚上特别的精神。而大部分程序员晚上会特别精神,毕竟99%的程序员都有晚上经常加班的事。


第二个关键点就是我们要在我们精力最旺盛的时候做最重要的事情。很多人在次要的时间上耗费了大量的精力,所以在最重要事情的时候,比如说在做关键技术讨论的时候,或者关键会议的时候,就显得沉沉欲睡。这种就是典型的把精力分配到了错误的时间段上面。比如我的团队在述职的时候,我们就发现有些同学会精神亢奋,非常有斗志的分享完了所有东西,有的同学分享的时候则沉沉欲睡,昏昏沉的感觉,这个也是属于典型的精力分配出了问题。


其实,在我们在各种非常重要的场合,比如晋升和OKR述职的时候,我们应该保证一个尽量旺盛的状态,在这个时候有一些非必要的工作都可以往后延。而我们有的同学因为赶各种项目或者工程,往往会把精力用在了做其他项目上面,然后留给在重要的环节,比如说答辩的时候,精力已经是强弩之末了。对于高效人士来说,宁愿次要的工作延迟一点,也要保证这一两个小时内的精力充沛。


精力是有限的,在这个精力分配里面,我们一定要把最重要的经历,最好的精力分配给最重要的事情,同时我们要注意一定不要消耗额外的精力在不必要的事情或者琐碎的事情上面。我在淘宝工作的时候,很多同事和师兄经常挂在嘴边的话,就是“白天的杂七杂八的事情和会议特别多,只有晚上才有精力写代码”。然而作为一名工程师或者一名产品经理,只能在晚上的抽出时间去写代码或者画自己的产品的prd。而这个时候做的确实是最重要的事情,用的是最最剩余的那一点点燃料。长期来看,这种精力分配方式产出的代码或者产品的质量就可想而知。毕竟,在竞争空前激烈的现代社会,想抽空做出伟大的事业的人是不存在的。


所以在这种情况下,特别必要的时候,我们一定要注意,不要给一些琐事儿或者烦杂的事情分配过多的精力,甚至是要尽量减少接触这些杂事的机会。当然很多人说有些东西都是必要的,但实际上以我的经验来看,80%以上的会议都是无效会议,只不过我们碍于各种各样的因素,不得不参加,从我的经验上来看,实际上就算我们参加了这些会议或者相关的评审,我们也取得不了任何额外的结果,大部分和我们主线路无关的事情,往往都是可以忽略的事情。这里我有个小窍门,对于不重要的事情,我一般都会等一段时间处理,很多时候不是很着急的事情,对方都忘记或者找到其他办法了。


当我们工作了若干年以后成为了核心骨干,往心里面就会有一种冲动或者想法,那就是我要掌控所有的事情,我要了解所有的上下文,这样才会有一种全局的控制感。所以很多高级工程师在工作一段时间之后就会全量的参加所有的会议,所有的讨论。以至于大部分经历都损耗到了会议上、需求评审上或者讨论会上。而留下来思考最重要的事情是最核心的技术方案或者产品方案,就只剩下一点点精力了。这个我觉得就是完全一种错误的思想,所谓大而全大概率是拿不到任何结果的。在我们企业的项目推进里面,我们经常也发现很多事情都是试错型的,探索型的,甚至有些都是重复型的,如果你把你所有的时间都耗费在了和别人的讨论和沟通上,那么势必你的精力就会被分散到点点滴滴,很琐碎。


这种情况下,只能湮没在小事上,过分追求“全”,而忽略了“深”。


所以不管什么阶段,不管什么角色,都不应该有“面面俱到”的要求和控制感,也不用焦虑忽略了什么,而是应该找到里面最关键的几件事情,并且把核心的注意力放在这上面,这样是取得成功的唯一的通道。


第三个关键点就是除了精力的使用之外,我们还要非常关注精力的恢复。精力和我们的能源一样,并不是取之不尽,用之不竭的,也是需要不断的持续的去给它充电。当然,最好的方式就是睡眠。所以在精力的管理方面,睡眠是最好的方式。我记得我唯一一次跟家人大吵就是没有睡好的时候,唯一一次高考失常的时候,也是没有睡眠好的时候。睡眠是如此的重要,但却很少有一本书来讲解如何好好睡眠,我也觉得奇怪。


精力除了脑力之外,很大一部分是一种体力消耗。所以有一个好的体力才能够支撑有一个好的精力,好的体力除了睡眠之外,非常重要的一点就是运动。我的主管,也就是我们整个事业部的总监,管理的大概有五六十号人,他在工作日每天的早上7点~8点是他的健身时间,每天他会提前来到公司做一个小时的健身。在健身完成以后,我们会发现他经常会保持一个非常好的一种工作状态,不管在沟通和表达方面,你都能看到他的精力满满,这种总监就是大家想跟随一起奋斗的人,毕竟大家不想跟着病恹恹的老板。我想这就是一种非常好的体力的管理方式,通过运动使得全身的肌肉能保持一个非常好的状态。



当然除了健身之外,还有很多非常方便的运动,比如说打羽毛球,比如说跑步,其实我最推崇的就是通过跑步来恢复精力。跑步的好处是比较方便,随时随地都可以操作,不需要额外的设备或者其他什么的。而且一定程度的有氧运动会使得整个心肺功能都会变得更好,更加的强健,当有一个良好的体魄之后,你自然具有更好的精力去面对一些更加复杂,更加有深度的事情。


更多精彩内容,关注公众号:ali老蒋,或点击加我好友深度沟通:ali老蒋 - java开发者


作者:ali老蒋
来源:juejin.cn/post/7288238840460591144
收起阅读 »

如何看待程序员不写注释

如何看待程序员不写注释 大家好,我是Leo🫣🫣🫣,今天我们来聊一下关于代码注释的问题话不多说,让我们开始吧😎😎😎。 在开始阅读正文之前,你先想 3 个问题: 你平时写代码的时候会写注释嘛? 你的注释是怎么样写的,主要都表达些什么? 你一般会在什么样的代码...
继续阅读 »

如何看待程序员不写注释



大家好,我是Leo🫣🫣🫣,今天我们来聊一下关于代码注释的问题话不多说,让我们开始吧😎😎😎。



在开始阅读正文之前,你先想 3 个问题:



  1. 你平时写代码的时候会写注释嘛?

  2. 你的注释是怎么样写的,主要都表达些什么?

  3. 你一般会在什么样的代码里写注释?



好了,正文开始。


1.我对注释的看法


首先,我个人刚开始写代码的时候,非常喜欢写注释,我一般会把代码思路先用文字表述出来。然后分成 1 2 3 4 每一步要干什么,怎么干。


然后写完之后开始在每个步骤下边填代码,这个时期我的代码注释量是非常高的。


但是后来随着技术熟练程度的提高,以及代码水平的提高,我的注释量就逐渐减少了。


并不是我觉得自己牛逼了不用写代码了,也不是我想专门给后人挖坑,纯粹是我觉得不太有必要了。


因为一方面我认为当你可以写出相对比较好的代码的时候,你的代码就是你的注释,你的命名、你的日志以及你的单元测试等等所有东西会共同构建成你的完整注释,最终他们合在一起形成的注释远比你一字一句写出来的注释要更清楚更实用。


并不是只有 // 后写的才叫注释。



2.不写程序的后果(狗头)


我们来简单聊一聊之前的一个国外新闻


image-20231011085512598


大家可能平时开开玩笑说,你不写注释可能被同事杀了,大家都当成一个笑话来听,但是当时美国程序员不写注释是真的在现实生活中上演。




以下内容来自网络。



据云头条报道,周三上午10点20左右,43岁的安东尼·汤(Anthony Tong)出现在办公室,拿出一把事先藏起来的半自动手枪开火。他在威斯康星州米德尔顿的这家公司工作了一年多。


工作人员纷纷逃离办公楼,跑到附近的公司避难。


行凶者随后向短短几分钟内赶到WTS Paradigm现场的警察开枪。四名警察随后开火,击中了嫌犯。嫌犯一送到医院就被宣布死亡。


WTS Paradigm的业务分析员朱迪·拉默斯(Judy Lahmers)说,当时自己正伏案工作,突然听到“像是有人把木板扔在地上,声音很响很响”。她赶紧跑出大楼,躲在一辆汽车后面。她告诉美联社:“我头也不回地拼命跑。你只想知道‘该躲起来还是跑远?”


她不知道关于枪击案的任何其他信息,但表示“完全出人意料。我们都是搞软件的。我们是很好的团队。”


警方介绍,这名死者自去年4月以来一直在WTS工作,没有犯罪记录,枪击事件发生时独自作案。目前,没有任何迹象表明到底是什么原因引起这起流血事件。


img



从这这个新闻,来说说我的看法:


1、代码不规范,确实看着蛋疼,尤其命名看不懂时,接手过去的代码,要去猜测对方代码,可能只有事人才看得懂。所以一定要规范,在大公司写的不规范,别人会直接怼你的。搞不好就是对你能力怀疑。


2、代码这个事情,有些人有洁癖,容不得垃圾代码在项目中,那么什么代码是垃圾代码,如命名不规范,成员变量没有表示其含义,函数名字不能充分表示其功能,大量if else逻辑,一个方法几百上千行代码,这些都是不良的习惯。


3、git提交时,老是覆盖提交,没有解决冲突,还有一次性改100多个类文件,1周才提交,有些兼容特殊处理地方不写注释,只有上帝才看懂。


4、凶手几名同事,肯定没有看过《重构,改善既有代码的设计》这本书,推荐大家好好读一读。避免类似悲剧发生


当然,还有一种情况我是建议写注释的,那就是二笔产品非要提一个不合理的需求导致你有一个不合理的写法,这个时候我希望你能注明“不是我要这么写的,是产品需求要求这样的,我也没办法的”的无奈,免得下一任接受你代码的人骂娘,说你是个菜鸡。


好了,今天的内容就到这里了。


3.总结


以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是Leo,一个在互联网行业的小白,立志成为更好的自己。


如果你想了解更多关于Leo,可以关注下面这个公众号,后面文章会首先同步至公众号。


4.参考文章



作者:程序员Leo说
来源:juejin.cn/post/7288340985229230099
收起阅读 »

H5车牌输入软键盘

web
前言 公司的业务背景是个大型园区,不可避免的要接触太多与车辆收费相关的业务,于是就有了这个车牌输入软键盘。对于车牌,用户手动输入的是不可信的,而且车牌第一位的地区简称打字输入实在是太麻烦,所以界定用户的输入内容,才能让双方都更加方便。 预览: pxsgdsb...
继续阅读 »

前言


公司的业务背景是个大型园区,不可避免的要接触太多与车辆收费相关的业务,于是就有了这个车牌输入软键盘。对于车牌,用户手动输入的是不可信的,而且车牌第一位的地区简称打字输入实在是太麻烦,所以界定用户的输入内容,才能让双方都更加方便。



预览: pxsgdsb.github.io/licensePlat… (请使用移动端打开)


github:github.com/pxsgdsb/lic…


gitee:gitee.com/PxStrong/li…



screenshots.gif

实现


因为车牌内容是固定的,所以直接写死在元素内。但是,为了提高组件的复用性,需要做一些简单的封装


; (function ($) {
function LicensePlateSelector() {
// 输入框元素
this.input_dom = `<ul class="plate_input_box">
<li class="territory_key" data-type="territory_key"></li>
<li style="margin-right:.8rem;"></li>
<li></li>
<li></li>
<li></li>
<li></li>
<li data-end="end"></li>
<li data-cls="new_energy" data-end="end" class="new_energy">
<span>新能源</span>
</li>
</ul>`

// 键盘元素
this.keyboard_dom = `...省略`
}
/**
* 初始化 车牌选择器
* @param {string} config.elem 元素
* @param {string} config.value 默认填充车牌
* @param {number} config.activeIndex 默认选中下标 (从0开始)
* @param {function} inputCallBack 输入事件回调
* @param {function} deleteCallBack 键盘删除事件回调
* @param {function} closeKeyCallBack 关闭键盘事件回调
*/

LicensePlateSelector.prototype.init = function (config) {
config = {
elem: config.elem,
value: config.value || "",
activeIndex: config.activeIndex || false,
inputCallBack: config.inputCallBack || false,
deleteCallBack: config.deleteCallBack || false,
closeKeyCallBack: config.closeKeyCallBack || false,
}
this.elemDom = $(config.elem);
this.elemDom.append(this.input_dom);
this.elemDom.append(this.keyboard_dom);
// 监听输入
this.watchKeyboardEvents(function(val){
// 键盘输入回调
if(config.inputCallBack){config.inputCallBack(val);}
},function(){
// 键盘删除事件回调
if(config.deleteCallBack){config.deleteCallBack();}
},function(){
// 关闭键盘事件回调
if(config.closeKeyCallBack){config.closeKeyCallBack();}
})
// 输入默认车牌
if (config.value) {
this.elemDom.find(".plate_input_box li").each(function (index) {
if (config.value[index]) {
$(this).text(config.value[index])
}
})
}
// 选中默认下标
if(config.activeIndex){
this.elemDom.find(".plate_input_box li").eq(config.activeIndex).click();
}
};
})(jQuery);

watchKeyboardEvents()函数用于在元素创建完成后创建事件监听


/**
* 监听键盘输入
* @param {function} inputCallBack 输入事件回调
* @param {function} deleteCallBack 键盘删除事件回调
* @param {function} closeKeyCallBack 关闭键盘事件回调
*/

LicensePlateSelector.prototype.watchKeyboardEvents = function(inputCallBack,deleteCallBack,closeKeyCallBack) {
let _this = this
// 输入框点击
_this.elemDom.find(".plate_input_box li").click(function (event) {
// 显示边框
$(".plate_input_this").removeClass("plate_input_this");
$(this).addClass("plate_input_this")
// 弹出键盘
// 关闭别的键盘
$(".territory_keyboard").css("display","none")
$(".alphabet_keyboard").css("display","none")
if ($(this).attr("data-type") && $(this).attr("data-type") == "territory_key") {
if (_this.elemDom.find(".territory_keyboard").css("display") == "none") {
_this.elemDom.find(".alphabet_keyboard").animate({ bottom: "-50rem" }).hide()
_this.elemDom.find(".territory_keyboard").show().animate({ bottom: 0 })
}
} else {
if (_this.elemDom.find(".alphabet_keyboard").css("display") == "none") {
_this.elemDom.find(".territory_keyboard").animate({ bottom: "-50rem" }).hide()
_this.elemDom.find(".alphabet_keyboard").show().animate({ bottom: 0 })
}
}
// 点击新能源
if ($(this).attr("data-cls") == "new_energy") {
$(this).empty().removeClass("new_energy").attr("data-cls", "")
}
event.stopPropagation(); // 阻止事件冒泡
})

// 地域键盘输入事件
......
}

使用时html只需要创建一个根元素,js输入配置项,自动渲染组件。


<div id="demo"></div>
<script>
let licensePlateSelector = new LicensePlateSelector();
// 初始化
licensePlateSelector.init({
elem: "#demo", // 根元素id
value: "湘A", // 默认填充车牌
activeIndex: 2, // 默认选中下标 (从0开始,不传时,默认不选中)
inputCallBack:function(val){ // 输入事件回调
console.log(val);
let plate_number = licensePlateSelector.getValue(); // 获取当前车牌
console.log(plate_number);
},
deleteCallBack:function(){ // 键盘删除事件回调
let plate_number = licensePlateSelector.getValue(); // 获取当前车牌
console.log(plate_number);
},
closeKeyCallBack:function(){ // 关闭键盘事件回调
console.log("键盘关闭");
},
})
</script>

参数


参数类型必填说明示例值
elemString指定元素选择器"#demo"
valueString默认填充车牌"湘A"
activeIndexnumber当前输入框下标,从0开始,不传时,默认不选中2
inputCallBackfunction输入事件回调函数,返回参数:当前输入的值
deleteCallBackfunction键盘删除事件回调函数
closeKeyCallBackfunction关闭键盘事件回调函数

方法


getValue 获取当前车牌


let plate_number = licensePlateSelector.getValue();

setValue 设置车牌


licensePlateSelector.setValue("粤A1E9Q3");

clearValue 清空车牌


licensePlateSelector.clearValue();

END


如果觉得对你还有些用,顺手点一下star吧。


作者:彭喜迎MAX
来源:juejin.cn/post/7288609174124576783
收起阅读 »

喂,鬼仔!你竟然还在瞒着我偷偷使用强制相等

web
我们都知道JavaScript有== (强制相等)和===(严格相等)运算符进行比较。但你可能不知道它们两个究竟有什么不同,并且更重要的是,在 js 引擎中使用它们的时候发生了什么? 前面我们提到 == 是强制比较。强制意味着 VM 试图将进行比较的双方强制...
继续阅读 »

我们都知道JavaScript有== (强制相等)和===(严格相等)运算符进行比较。但你可能不知道它们两个究竟有什么不同,并且更重要的是,在 js 引擎中使用它们的时候发生了什么?


前面我们提到 == 是强制比较。强制意味着 VM 试图将进行比较的双方强制为相同的类型然后查看它们是否相等。以下我们列举了一些自动被强制相等的例子:


"1" == 1 // true
1 == "1" // true
true == 1 // true
1 == true // true
[1] == 1 // true
1 == [1] // true


你要知道,强制是对称的,如果a == b为真,那么b == a也为真。另一方面,只有当两个操作数完全相同时===才为真(除了Number.NaN)。因此,上面的例子都真实的情况下都是假真 (即,在 === 的情况下是 false 的)。



为什么强制相等有这样的问题,这要归咎与强制相等的规则。


强制相等的规则


实际的规则很复杂(这也是不使用==的原因)。但是为了显示规则有多么复杂,我通过使用===实现了==,带大家看看强制相等的规则到底多复杂:


function doubleEqual(a, b) {
if (typeof a === typeof b) return a === b;
if (wantsCoercion(a) && isCoercable(b)) {
b = b.valueOf();
} else if (wantsCoercion(b) && isCoercable(a)) {
const temp = a.valueOf();
a = b;
b = temp;
}
if (a === b) return true;
switch (typeof a) {
case "string":
if (b === true) return a === "1" || a === 1;
if (b === false) return a === "0" || a === 0 || a == "";
if (a === "" && b === 0) return true;
return a === String(b);
case "boolean":
if (a === true) return b === 1 || String(b) === "1";
else return b === false || String(b) === "0" || String(b) === "";
case "number":
if (a === 0 && b === false) return true;
if (a === 1 && b === true) return true;
return a === Number(String(b));
case "undefined":
return b === undefined || b === null;
case "object":
if (a === null) return b === null || b === undefined;
default:
return false;
}
}

function wantsCoercion(value) {
const type = typeof value;
return type === "string" || type === "number" || type === "boolean";
}

function isCoercable(value) {
return value !== null && typeof value == "object";
}

这是不是太复杂了,我甚至不确定这是正确的! 也许有你知道更简单的算法。


但有趣的是,你会发现在上面的算法中,如果其中一个操作数是对象,VM 将调用. valueof()来允许对象将自身强制转换为基本类型。


强制转换的成本


上面的实现很复杂。那么===== 要多浪费多少性能呢? 看看下面这张图,我用基准测试做了一个对比:


image.png


其中,图表中越高表示越快(即,每秒操作次数越多)。


首先我们来讨论数字数组。当 VM 注意到数组是纯整数时,它将它们存储在一个称为PACKED_SMI_ELEMENTS的特殊数组中。在这种情况下,VM 知道将 == 处理为 === 是安全的,性能是相同的。这解释了为什么在数字的情况下,===== 之间没有区别。但是,一旦数组中包含了数字以外的内容,== 的情况就变得很糟糕了。


对于字符串,===== 的性能下降了 50%,看起来挺糟的是吧。


字符串在VM中是特殊的,但一旦我们涉及到对象,我们就慢了 4 倍。看看 mix 这栏,现在速度减慢了 4 倍!


但还有更糟的。对象可以定义 valueOf,这样在转换的时候可以将自己强制转换为原语。虽然在对象上定位属性可以通过内联缓存,内联缓存让属性读取变得快速,但在超大容量读取的情况下可能会经历 60 倍的减速,这可能会使情况更糟。如图中最坏情况(objectsMega)场景所示,===== 慢15 倍!


有其他使用 == 的理由吗


现在,=== 非常快! 因此,即使是使用 === 的15倍减速,在大多数应用程序中也不会有太大区别。尽管如此,我还是很难想出为什么要使用 == 而不是 === 的任何理由。强制规则很复杂,而且它存在一个性能瓶颈,所以在使用 == 之前请三思。


作者:编程轨迹
来源:juejin.cn/post/7216894387992477757
收起阅读 »

国庆,与山重逢

重庆多山,重庆的县城也多山。从主城回到重庆最东北,是横穿一座又一座山,是横跨一座又一座桥。桥不跨江,是用来连接两座山的。 每当向不很了解重庆的朋友分享我的回家路时,他们总不相信:“重庆不是一个直辖市么?你回家怎么可能要8小时的?” 我回家是真需要至少8小时的。...
继续阅读 »

重庆多山,重庆的县城也多山。从主城回到重庆最东北,是横穿一座又一座山,是横跨一座又一座桥。桥不跨江,是用来连接两座山的。


每当向不很了解重庆的朋友分享我的回家路时,他们总不相信:“重庆不是一个直辖市么?你回家怎么可能要8小时的?”


我回家是真需要至少8小时的。国庆第一天上午11点出发,晚上9点回到山上家中,除去路上堵车的两小时,全程整好8小时。


即便回家很远,回家的路很难走,我依然很喜欢回家。


我从没仔细想过自己为什么喜欢回家,只是每年国庆劝说阿妮回家用的说辞总一样:“爷爷奶奶外公外婆都在家。”


我的母亲今年也在家,所以今年国庆,绝大部分时间是在山上度过的。


大概是10年前,绝大部分“高山”——单纯的字面意思,山的高处——住户搬到低山,高山上的住户,只剩十几家。山上村子住的人家变少,便给了山很大的自由。


原有的山路,平日里少有人行走,路面长满各式各样我全不记得名字的草,郁郁葱葱,互相缠绕。路旁斜坡新长出许多很小的树,它们不管旁边是否有路,只向空旷处挤,挤着挤着,就没了路。如果从没走过这些路,是肯定看不出来曾经有过路的。稍远处老些的大树,掉落的枯的枝丫,触手可及,捡柴再不用去很远地方,只沿着路挨着捡便好。


原有的山田,在退耕还林时全种上了果树,核桃与板栗。不知是水土不服还是品种不佳,核桃树只剩下些印象,田中长起来的,只有板栗。十多年过去,板栗成了山田里的佼佼者,每一棵树的主干,都有大腿那么粗。


搬走的人家多了,没搬走的也大都外出打工只在过年时回家,于是还喂猪的人家更少,山中落叶不再被收集回家为猪铺床。再走远些,林间落叶铺了一层又一层,厚厚的,挡住菌子的冒头路线。


图片


母亲一大早沿路捡的菌子


菌子,是山中的特产,春天有,夏天有,秋天也有。母亲说:“秋天菌子不闹人(‘闹人’是无毒的意思),最好吃。春夏的菌子就要注意,有些吃不得,要挑一哈。”


捡菌子的最好时机,是下雨后的第二天,有些刚冒出头,有些刚长成型。长过(腐烂)生蛆?此时是不会的。


母亲是捡菌子的好手,似乎所有菌子她都认识。我没有学到捡菌子这门手艺,只在菌子回家后跟着母亲洗菌时认识几个品类。


石灰菌是白色的,山里最多,平均体型最大,吃起来脆脆的不爽口。


红菌子好吃,但需要仔细辨认,有许多其它红颜色的菌是不能吃的,能吃的要肥厚一些。


蜂窝菌伞把内部像蜂窝,伞面滑滑的,只在秋天有。它是我最喜欢吃的菌子,炒好的成品入口也滑滑的,一嗦就进了肚;如果吃的慢些,咀嚼两次,又会发现它也是脆脆的;蜂窝菌,只放油、盐、大蒜和辣椒,味道就已经很好。


我听过的名字,还有枞树菌、紫檀菌,它们并不多见,我暂且只记得名字不记得长相与口感。


我们三个帅的计划,是国庆第二天上山捡菌子。


计划依据天气预报——国庆第二天小雨,后面几天,要么是中雨要么是大雨——制定。天气预报不准确,真正的小雨,只在下午出现一小会儿。我极不愿意极不建议天黑走山路,于是宝帅的下山时间,定在下午6点。


雨真正变小的时间,是下午4点半,一个半小时时间,四个人一起,能从山中收获些什么呢?


答案是半背板栗与一碗菌子。


四个人,两个筐筐,一个装菌子一个装板栗;一把弯刀一把火钳,弯刀用来开路——砍去那些挤在路上的树枝与刺条,火钳用来捡板栗的有刺包子;再背一个背篓,万一筐筐装不下呢?


时间很紧,意犹未尽。


母亲将板栗硬塞给宝帅一行,留下的一碗菌子,是当晚桌上的一盘菜。


图片


炒熟的菌


菌的做法,是简单的。菌子去跟,摘掉树叶,洗净泥巴,煮半小时;捞出用凉水泡一泡,将大的菌撕成小的适合入口形状,再洗再煮再捞出;锅内放油放蒜放辣椒,炒香装盘。


菌的味道极好。


图片


八月瓜壳


我知道的能在山上长出果子的果树,有苹果、梨、杏、枣、桃、山楂、板栗和八月瓜。苹果、梨、杏、枣、桃和山楂,都需要人的维护——剪枝或是嫁接,不维护的果树,任它自然生长,要么过两年枯掉,要么果小不好吃。


不用维护的,是板栗和八月瓜。八月瓜纯野生,我见的不多,但板栗,是一直都存在的。


十几年前,高山上的人家很多,捡板栗需要走很远,走到悬崖边,走到“弯里”(山的最里面,很大一片山里只有一户人家),走到绝大部分人不愿去的地方。


我印象中的第一次全家捡板栗,是高中时的某个国庆,母亲和贵嬢嬢,带着各自小孩,背背篓提筐筐不带弯刀,一大群人去弯里。


弯里的板栗很小,不像新长起来的山田里的品种。


飞包土是我们家最远、最高的一块田,它是退耕还林时被最先“退”掉的,田里栽的树,是板栗。时间过去十几年,山田真的变回树林,板栗成了山里的树。


国庆离开家的那天上午不下雨,我和阿妮上飞包土,再捡半筐板栗。


图片


刚洗过的板栗


今年国庆,与山重逢。


作者:我要改名叫嘟嘟
来源:juejin.cn/post/7288163743035965440
收起阅读 »

唱衰这么多年,PHP 仍然还是你大爷!

web
PHP 是个庞然大物。 尽管有人不断宣称 PHP “即将消亡”。 但无法改变的事实是:互联网依然大量依赖 PHP。本文将通过大量的数据和事实告诉你为何 PHP 仍然在统治着互联网,你大爷仍然还是你大爷。 统计数据 PHP 仍然是首选编程语言 根据 W3 ...
继续阅读 »

PHP 是个庞然大物。


尽管有人不断宣称 PHP “即将消亡”。



但无法改变的事实是:互联网依然大量依赖 PHP。本文将通过大量的数据和事实告诉你为何 PHP 仍然在统治着互联网,你大爷仍然还是你大爷



统计数据


PHP 仍然是首选编程语言



根据 W3 Techs 对全球前 1000 万个网站使用的编程语言分析,我们可以看到:



  • PHP 占比 77.2%

  • ASP 占比 6.9%

  • Ruby 占比 5.4%


基于 PHP 的内容管理框架


绝大多数公共网站都是通过 PHP 和 CMS 来构建的。根据市场份额,12 大 CMS 软件中有 8 个是用 PHP 编写的。下面的数据来自 W3 Techs 对前 1000 万个网站的 CMS 使用情况调查,每个百分点代表前 1000 万个网站中的 10 万网站。



  • [PHP] WordPress 生态系统 (63%)

  • [Ruby] Shopify

  • Wix

  • Squarespace

  • [PHP] Joomla 生态系统 (3%)

  • [PHP] Drupal 生态系统 (2%)

  • [PHP] Adobe Magento (2%)

  • [PHP] PrestaShop (1%)

  • [Python] Google Blogger

  • [PHP] Bitrix (1%)

  • [PHP] OpenCart (1%)

  • [PHP] TYPO3 (1%)



不得不说,Wordpress 在内容管理领域依然站有绝对的统治地位。


PHP 在电商领域的应用


根据 BuiltWith 2023 年 8 月对在线商店的报告,我们可以看到 PHP 在电商领域仍然占统治地位:




趣闻轶事


Kinsta 发表了一篇文章,证明 PHP 仍然很快,仍然很活跃,仍然很流行:



早在 2011 年,人们就一直在宣称 PHP 已死。但事实是,PHP 7.3 的请求处理速度是 PHP 5.6 的 2-3 倍,而 PHP 8.1 则更快。正因为 PHP 的普及,我们可以很轻松地招聘到有经验的 PHP 开发者。



Vimeo 工程师 Matt Brown 在《这不是遗留代码,而是 PHP》一文中表示:



PHP 从未停止创新。尽管我们计划将 500,000 行的 PHP 代码划分为多个 [服务],但最终这些建议都没有被采纳。


Vimeo 自 2004 年以来规模扩大了数倍,我们的 PHP 代码库也是如此。



Ars Technica 发布了一个包含历史数据的 W3 Techs 报告,证明 PHP 仍然遥遥领先



尽管 PHP 有许多臭名昭著的怪癖,但它似乎还能活很久。从 2010 年的 72.5% 市场份额增长到今天的 78.9% 市场份额,目前还没有任何明显的竞争对手能让 PHP 感到威胁




针对 Python 创始人 Guido van Rossum 的一个采访播客中,Lex Fridman 如是说:



Lex: 目前互联网的大部分后端服务仍然是用 PHP 写的


Guido: 没错!



Daniel Stenberg 在其年度 Curl 用户调查(第 18 页)中统计了用户使用 curl 的方式。直接使用 curl 命令行的用户占比最高(78.4%),用户最熟悉的方式就是在 PHP 中使用 curl,自 2015 年调查开始以来一直都是这个结果。2023 年的调查报告显示有 19.6% 的用户在 PHP 中使用 curl。



curl (CLI) 78.4%, php-curl 19.6%, pycurl 13%, […], node-libcurl 4.1%.



Ember.js 虽然起源于 Ruby 社区,但作为一个前端框架,它可以与任何后端配合使用。Ember 的社区调查报告显示,PHP 是受访者第三喜欢的选项,仅次于 Ruby 和 Java。



Ember 的调查还询问了一些通用的行业问题。例如,有 24% 的受访者表示他们的基础设施都是“自托管”,而不是依赖于主流的云服务提供商。虽然这项调查本身不能完全代表整个行业,但结果仍可能会让人大吃一惊,特别是对那些依赖社交媒体和会议演讲来了解商业现状的人来说更是如此。对于企业来说,现在准备好云退出战略(例如 NHS)比以往任何时候都更加重要。你可以阅读 Basecamp 的文章了解云退出战略是如何为他们每年节省数百万美元的。


大规模 PHP 应用


上述统计数据衡量了不同网站和公司的数量,其中绝大多数是基于 PHP 构建的。但所有这些只告诉我们它们的规模在前 1000 万名之内。那前 500 名呢?


Jack Ellis 在《Laravel 能否扩展?》这篇文章中指出,你不应该仅根据每秒可以处理的请求数量来做选择。大部分业务都不太可能达到那个水平,而且还会面临很多其他瓶颈。但事实证明,PHP 是可以扩展到这一水平的语言之一。




当看到我们的软件(基于 Laravel 构建的 Fathom Analytics)增长迅猛时,我们从未怀疑过“这个框架是否能够扩展?”。


我与多家企业合作过,他们利用 Laravel 支撑整个业务运营。像 Twitch、Disney、New York Times、WWE 和 Warner Bros 这样的公司也在他们的多个项目中使用 Laravel。Laravel 能够轻松应对大规模的应用需求。



Vimeo 工程师 Matt Brown 在《这不是遗留代码,而是 PHP》一文中强调:




可以很明确地告诉你们,PHP 还是你大爷。Vimeo 在 PHP 方面的持续成功就是证明,在 2020 年它仍然是快速发展的公司的绝佳工具。



Vimeo 还以开发流行的 PHP 静态分析工具 Psalm 而闻名。


Slack 公司首席架构师 Keith Adams 在《认真对待 PHP》一文中提到:




Slack 服务端大部分应用逻辑都是由 PHP 来执行的。


相比于 PHP 的优势而言(通过故障隔离减少 bug 成本;安全并发;高吞吐量),PHP 存在的问题可以忽略不计。



我们再分析一下 W3 Techs 的报告,分析部分业务比较单一的公司的规模。规模最大的是 WordPress,它驱动着 Automattic 的 WordPress.com。每月有 200 亿次页面访问(Alexa 全球排名 55)。


如果我们继续往下看,来到占市场份额 0.1% 的条目,可以看到大量的网站都是靠 PHP 系统来支撑的,PHP 仍然是 10w 小网站的首选框架。



MediaWiki维基百科背后的平台,每月有 250 亿的页面浏览量(Alexa 排名 12)。同时 MediaWiki 还驱动着 Fandom(每月有 20 亿的页面浏览量,Similarweb 排名 44)和 WikiHow(每月有 1 亿访问者,Alexa 排名 215)。



除此之外还有一大批互联网公司由 PHP 驱动,例如 Facebook(Alexa 排名 7)、Etsy(Alexa 排名 66)、Vimeo(Alexa 排名 165)和 Slack(Similarweb 排名 362)。


Etsy 之所以引人关注,是因为它有高比例的活跃会话和动态内容。这与维基百科或 WordPress 不同,后者可以从静态缓存中提供大多数页面视图。这意味着尽管规模相似,但 Etsy 的 PHP 应用程序更容易受到高流量的影响。


Etsy 也是 PHP 创始人 Rasmus Lerdorf 的东家。他有时会在技术分享中展示 Etsy 的代码库片段。(极客旁注:他在 2021 年的现代 PHP 讲座中解释了 Etsy 是如何使用 rsync 进行部署的,就像 Wikipedia 在过去 10 年使用 Scap 一样)。Etsy 的官方博客偶尔会提到他们对模块化 PHP 单体的工作进展,例如 Plural 本地化。有时也会放出详细的 Etsy 站点性能报告



很高兴地告诉大家,升级到 PHP7 之后,本季度整个网站的性能都得到了提高,所有页面的性能都有了显著的提升。



我的观点


大多数人认为,PHP 社区似乎在公共舆论中占据的空间不大。无论是 PHP 核心开发者 , 还是 PHP 软件包(例如 Laravel、Symfony、WordPress、Composer 和 PHPUnit)的作者,亦或是日常工作中使用 PHP 的普通工程师,我们很少在社交媒体上的争论中看到他们的身影。


你也很少看到我们在会议上做演讲,宣称某个技术栈“绝对会”为你的公司带来裨益。如果你听了某些 JavaScript 框架粉丝的演讲,你可能会认为大多数公司今天都在使用他们的技术栈。


我不是说 JavaScript 不好,而是某些人在没有考虑技术或商业需求的前提下给出了“xxx 最好”的断言。这是一种过度营销,你怎么知道它最好?你跟别的语言比较过了吗?


我也不是说 JavaScript 没有用武之地,我们要辩证地看待世间万物。你可以分享你的经验和成果,比如哪些行得通,哪些行不通。要持续探索、持续创新、持续分享,持续推动人类前进。这就是自由软件的精神!


你可能看过《The Market for Lemons 》和《A Historical Reference of React Criticism》这两篇文章,他们都指出了 JS 的问题。但是 ... React 仅占有 3% 的市场份额。再加上其他的小框架(Vue、Angular、Svelte),这个数字才达到 5%。而基于 Node.js 的 Web 服务也仅占有 3% 的市场份额。这是否意味着超过 90% 的人都错过了 PHP?


别忘了,这 5% 代表了 50 万个主要网站,这是一个巨大的数字。Node.js 有自己的优势(实时消息流)。但是,Node.js 也有其弱点(阻塞主线程)。另外要强调一点:市场份额并不能完全反映规模。你可能驱动着排名前 1% 的几个大型组织,也可能驱动着排名后 1% 的组织。或者像 WordPress 那样同时支撑排名前 1% 和其他 4000 万个网站。


结论


无论是老公司还是小公司,无论其规模大小,可能都没有使用我们在公共场所经常听到的技术栈。如果不考虑个人项目和烧钱的初创公司,其他公司的这个现象更为明显。


对于正在成长和持续经营的企业来说,PHP 是否能够成为企业首选的前三名语言?当一个企业和其团队在扩大规模时,编程语言是否完全不重要?我们不得而知。


我只知道如今有许多企业都在使用 PHP,而 PHP 已被证明是一种可持续的选择,它经受住了时间的考验。例如,像 Fathom 这样的新公司,在短短三年内就实现了盈利。正如 Fathom 的文章所说,大部分公司的业务永远达不到那种规模。不过话又说回来,即使面对大规模的业务,PHP 仍然是一种经济可持续的选择


那么问题来了,PHP 是唯一的选择吗?当然不是。


有的语言速度更快(Rust),有的语言社区规模更大(Node.js),或者编译器更成熟(Java),但这往往会牺牲其他价值。


PHP 达到了某种柔中取刚的平衡点。它速度很快,社区规模较大语法现代化开发活跃,易于学习,易于扩展,并且拥有一个庞大的标准库。它可以在大规模场景下提供高效和安全的并发,而又没有异步复杂性或阻塞主线程的问题。由于平台稳定,加上社区重视兼容性和低依赖性,它的维护成本往往较低。


当然,每个人的需求不尽相同,但想要达到上述的这种平衡点,PHP 是少数几个能满足需求的软语言之一。除此之外还有哪个语言可以做到?


作者:米开朗基杨
来源:juejin.cn/post/7288963080855617573
收起阅读 »

离开了浪浪山,简直不要太爽

web
今年年初的时候,《中国奇谭》火了,与其说是《中国奇谭》火了,还不如说是这个动漫和普通打工人太有共鸣了,动漫里面的小猪妖是很多普通打工人的写照,毕业进入了父母亲戚以为很不错的工作,领着一份不多不少的工资,每天要处理各种工作上的事情,事情比较多的时候,还需要经常加...
继续阅读 »

今年年初的时候,《中国奇谭》火了,与其说是《中国奇谭》火了,还不如说是这个动漫和普通打工人太有共鸣了,动漫里面的小猪妖是很多普通打工人的写照,毕业进入了父母亲戚以为很不错的工作,领着一份不多不少的工资,每天要处理各种工作上的事情,事情比较多的时候,还需要经常加班。每个人都想和小猪妖一样离开浪浪山,不过最近我却离开了浪浪山。


公司裁员


准确的说是公司裁员了。人事通知我,说去下会议室,当时我就有预感到是要裁员了,因为之前公司就开始裁员了。一开始就是人事主管就说:最近工作怎么样?我就猜到了基本就是要裁员了。后面赔偿也符合我的预期。谈好了赔偿,做了工作的交接,和几个同事吃了一个饭,就和这个工作了几年的公司拜拜了。


开始的时候也是挺不适应的,自从大学毕业之后,一直都是有规律的上班生活,每个月都有一份固定的工资领,能维持日常开销,多余的钱投投资,日子过得也还行。忽然一下子没了工作,意味着就没有了收入了,要为下个月的房租担心了,不过好在赔偿金还能维持几个月。


今年行情普遍不太好,身边也有失业的朋友,有的找了几个月还没有找到工作。有的朋友还说好的公司面试基本都要二本以上和三年工作以下的面试,总体来说要求还是比较严格的,如果不出去面试,也不会意识到现在就业行情的严峻。后面索性就先玩玩吧,去周边走走、去附近的香港走走。


周边逛逛


首先就准备去盐田那边玩,经常看小红书有人分享那边的打卡地方,有海上图书馆,打定主意就出发。路过一个地铁口,看到一些可爱的动漫,灌篮高手、海贼王。



还有可爱的一个公交车,这个公交完美的贴合的墙壁上,门框刚好做成一个上下的车门,设计的比较巧妙。



之前上班的时候,走路的都是匆匆忙忙的,上班都比较辛苦,周末都基本就用来补觉休息。出门人也比较多,现在人都比较少,慢慢走,欣赏沿途的风景




坐了一个小时的地铁到了海山地铁站,映入眼帘就是清澈的海水,远离的城市的喧嚣,欣赏自然的美景。往里面走就看到了海上图书馆,环境还是挺不错的,海水比深圳湾的清澈多了。




沿着上面的海边一直散步,享受这海风吹拂的感觉,小雨淅淅沥沥的下,听着下雨的声音,一边走,一望无际的白云和天空,让人身体特别放松。






**程序员都是脑力工作为主,坐在工位上一坐就是几个小时,运动量比较少,都是固定的上下班,周末也基本是休息。**不过固定的生活模式过久了就会感觉很单调和平淡,每天都生活的都是复制,也会让人感觉很无聊,所以还是要多出去走走,体验一下不一样的生活。



雨天爬山


去玩海边之后,之后一直在下雨,之前也经常爬山,不过都是天气不错的时候爬的,这次就尝试一下雨天爬山吧。


因为开始爬山的是下午 2 点多,人不是很多,上山看到了很多下山的人。一路上也没什么人了。





快到山顶的时候,就开始下雨了,天也变暗了,雾也越来越大了。



上山的时候还能看到山下的房子,现在都看不请了。还以为误入衡山了。




在亭子上躲雨休息,随着天越来越暗,山下的灯光一点点打开,路上的车灯,路边的路灯。直到点亮所有的灯光,在马路上形成一道靓丽的风景线。



后面还去了各种公园,还去了一趟香港,再去了一趟香港大学。可以说这几周的经历比我上几年的经历都多


不上班真爽


不用每天上班,不用处理各种问题,也没有时间焦虑症(每天到哪个点就要上班,哪个点就要下班),这段时间完全不需要考虑时间的问题,想去哪里就可以立刻去哪里。不需要请假,不需要调休,晚上玩的比较晚了也不用担心第二天要早起上班起不来。不需要为工作而烦恼,只做自己想做的事情。


上面不是讲了去海边玩吗,走海边走路的时候,走着走着,竟然感觉到自己饿了,很难得有这种感觉。只有读书的时候,在外面运动了很久才会感觉的饥饿。


目的性不强的做事,也没有时间上的焦虑。没有压力的做事才是最自然的、最舒服、最享受的做事


失业焦虑吗


被通知裁员的时候,虽然心里有些准备,但是真的听到被裁的时候,心里还是有些焦虑,特别是现在就业行情也不太好,感觉找工作还是有些困难的。 习惯了每天按部就班的上班,完成各种工作上的任务。周末放假偶尔出去玩玩,休息。基本都没有太大的变化。不过心里也不是很焦虑,对自己的技术还是挺有信心,坚持写文章,写 Github,扩大自己的影响力。工作上也比较努力、认真。博客写了快两年了,每天都在积累,阅读量最高的都有十万多了,有了一些积累,心里也更有底气了。



其实给公司打工的同时也要给自己的打工,在工作中一般有问题就需要立刻去解决,解决之后及时的总结和归纳,做事的同时也要积累的自己的经验。积累的越多,自己做事也就更快,做事也更有章程了。


现在自己也是把简历改好,投投简历。没有工作也适当的放松放松,去周边城市旅旅游。有面试就去面试。


写在最后


现在就业行情不太好,打工人还是需要有被裁员的准备。现在可能很多公司给打工人更多的压力。这时候就需要放平自己的心态,尽量把自己的工作做好。同时也要多做积累,多做输出,未雨绸缪。有工作的就好好工作,尽量提高自己的能力,能力提高了,才有有更多的成长。失业的也不要气馁,多投简历,降低消费。


无论有没有离开浪浪山,都需要努力并自信的生活。


作者:小码A梦
来源:juejin.cn/post/7288602155111563264
收起阅读 »

某37岁程序员感叹:存款200万加一套房,却不敢辞职!

200万存款,一套房,房贷只剩30多万,这样的条件可以说很不错了,但一个拥有这些的程序员却依然压力很大,甚至患上了抑郁症。这名程序员今年37岁,薪资30k,有200万存款,一套房还有30多万房贷。他说自己很疲惫,有抑郁症,压力很大,想裸辞在家休息一段时间,又怕...
继续阅读 »

200万存款,一套房,房贷只剩30多万,这样的条件可以说很不错了,但一个拥有这些的程序员却依然压力很大,甚至患上了抑郁症。

这名程序员今年37岁,薪资30k,有200万存款,一套房还有30多万房贷。他说自己很疲惫,有抑郁症,压力很大,想裸辞在家休息一段时间,又怕出来不好找工作,很纠结要不要辞职。


许多网友都劝他休息一下,毕竟身体是革命的本钱,何况刚刚发生了字节程序员猝死事件,让自己舒服一点更重要。


有人说,200万存银行,4%的利率,每个月收益6600,欲望不高的话完全够生活,可以休息一段时间再出发。


有人说,卷了那么多年,可以躺平做咸鱼了。

把生命浪费在加班上不值得。


有人说,三代之后的重孙辈都不一定知道他的名字,更别提他的生活和情感了,活好自己这辈子就够了,想歇就歇着,人生路还长,不必争朝夕。


有人建议楼主去一个整体年龄偏大的公司,这样就不会那么焦虑了。


有人建议楼主找一个to B的公司,或者非核心部门,不会太忙。


有人建议楼主去一个非互联网企业,薪资降一半,就会特别轻松。


也有人建议楼主先在公司内躺平,同时找外面的机会,不要裸辞。


还有人说,楼主先摸摸鱼,实在没法做了就休息一段时间,然后可以跑外卖、滴滴、快递,或者做点小生意。


某45岁程序员说,等楼主过了40岁就不抑郁了,因为那时候就没人愿意接简历了。楼主可以思考一下自己五六十岁想做什么,现在就可以开始去做了。


另一部分网友劝说楼主继续卷,不然等失业了,想卷都没有平台。生活就是这样,坚强一点!


在这个高速发展、压力山大的社会,躺平成为许多人梦寐以求的目标。存够下半生的养老钱,提前退休,财富自由,这些正是如今许多年轻人拼命工作的动力,为此他们不惜消耗自己年轻的身体和健康,以期在中年时能够过上自己想要的生活。

但多少钱才能躺平呢?200万存款对有些人而言足够了,对有些人却远远不够,这取决于人们的现状和对未来的打算:在哪个城市发展?是否结婚?是否有孩子?每一个问题都会带来更多负担,也决定了多少钱才能让人停止内卷。

有一点很重要,无论能否躺平,都别搭上自己的健康。该休息的时候就休息,什么都没有生命重要。如果真的很累,可以考虑找一份清闲稳定的工作,实现生活和工作的动态平衡。缩减欲望,降低标准,对自己少一点要求,多一点爱惜。

作者:行者

来源:mp.weixin.qq.com/s/jjk5KVD4B0sdujxisR9cSw

收起阅读 »

面试官是自己前女友,全程被拷问~

开口我说了一句,好巧,没想到真是你😂,她一笑说别废话了来吧,自我介绍一下吧。 我说我还需要介绍吗?你不都知道??  她给我来了句同学分清场合哈,注意面试纪律。请你做一下自我介绍。另外,搜索公众号Linux就该这样学后台回复“猴子”...
继续阅读 »

xdm,这是什么狗血剧情,面试居然碰到了前女友,而且还是最后一面的面试官,真的人都麻了,这放在整个面试界也是相当的炸裂......


真的是第一次在看面经的时候追起了故事,看着这哥们反复被虐,真的太带劲了。


跟前女友在一起快五年因为一些原因分手一年多了,期间再没联系过。昨天最后一轮hr面,邮件看到面试官跟前女友重名,心里想应该不会这么巧吧😭  没想到进了面试链接还真是她!!!!

兄弟们,当时的心情真的是绝了,尴尬到飞起。然后还要继续面试!!!!

开口我说了一句,好巧,没想到真是你😂,她一笑说别废话了来吧,自我介绍一下吧。 我说我还需要介绍吗?你不都知道??  她给我来了句同学分清场合哈,注意面试纪律。请你做一下自我介绍。另外,搜索公众号Linux就该这样学后台回复“猴子”,获取一份惊喜礼包。

我简单介绍了一下,然后开始问我以前不是说想做xxx吗?怎么投这个岗了?说说原因吧。我巴拉巴拉说了一通,她说这可不像你吧,再给你次机会重新说

我实话实说了她满意的点点头。

算了其他的不说了,她太了解我了,全程面试被拷问,被挑刺,然鹅我一点办法都没有,因为她一句想清楚再回答哈,回答的内容有问题在我这里会减分的。😤😤😤 那个表情!!!!😭😭😭

估计凉咯,没戏了
她告诉我说本来是她同事面我然后她看到我简历很“开心”,就说她来对接我。

大概半个多小时面完之后,她说面试结果后续会通知你的,有问题可以打邮件电话或者发邮件。然后她说:哦,不好意思忘记了,你手机号被我拉黑了,算了别打了,你等着吧
解释一下为啥她面试官了我才毕业。我俩本科同学,她本科毕业工作了,我中间休学两年又读了研!所以读研读了个寂寞。 

后续:

今天有进度了,但是还不如没有,我快让她气死了!!想骂人


昨天有兄弟给我支了招说可以换个手机号打电话问,我一听,豁然开朗,一想哎哟不错哟,好主意啊!下午给她打电话,第一遍没打通估计在开会啥的,过了半个小时我又打过去,她接了。

我说喂你好,是xxx的面试官吗?

她TM听出我声音了,就在那哈哈哈笑,说:“xxx谁教你的,换手机给我打!还你好~是xxx面试官吗~(此处请自行想象讥讽,小人得志的样子哈图片)干嘛说吧!”  我一听也不装了我说:“对,就是我,你不是自己说面试就得有个面试的样子吗?我这么说咋了?不行?”  她说:“别废话,我上班呢说干嘛?”

我说:“你说我干嘛?我问进度啊!我发邮件问你,你不回,能不能给个痛快,是死是活抓紧好不?让人泡池子好玩吗?”

她就搁那笑:“哦~我还以为你跑来找素材更新你帖子来,写的真不错啊,啥时候还有这手艺了。”

我听完脑瓜子一懵!!!

我说:“卧槽,你咋知道!”

她说:“你管我咋知道,我反正就是知道!我那天面你是那样?还拷打你?把你香的还。你进度我告诉你,我面评还没传,等着吧。嘿嘿”,然后啪!就把电话挂了,再打回去就拉黑了!

今天又换了个同学手机打,打通了,第一遍,我喂~  对面啪挂断了,第二遍,直接拒接,第三遍,我:别挂!她:滚!再打就拉黑了

昨天中午打算午休一下,结果接到了她的电话,因为手机号这么多年了确实记得太清楚了。接了电话,我说:哎哟,你这咋用自己手机给我打了??把我从黑名单里爬出来了?  

她说:别误会,公司座机坏了,只能手机给你打,你放心一会你还会回到属于你的地方的图片

我说:打电话啥事?要给我发offer了?

她说:虽然很不想承认,但是确实是给你发offer的,一会自己看看邮箱,不想来就赶紧拒了。(就那种很不情愿的那种感觉,自己想象)

我说:哈哈哈哈,果然该是我的还是我的呀!

她说:我真该给你面评的时候写的差点!你又不会来还白白占一个oc,浪费公司成本!

我说:谁说我不来!我不来我面啥!我时间不是时间是吧?

她接话说:哟哟哟,你真敢来?我是你们部门的hrbp天天见面,你不尴尬?不怕我给你穿小鞋了?

我说:反正我不尴尬,我感觉挺好,哈哈哈哈,谁尴尬谁知道,你又不是我leader,我不怕。

她说:行,那等着吧。啪电话就挂了。

傍晚就收到录用意向了,去还是不去呢?

大家觉得呢?如果是你,你会不会去?


来源:牛客网;作者:offer拿到吐1111

收起阅读 »

姚期智:人类本身就是世界上相当理想的具身智能体

60s要点速读:1、人类本身就是世界上相当理想的一个具身智能体。它基本上具备三个方面,三个成分:第一方面是身体,第二方面是小脑,第三方面是大脑。身体的部分具身必须要有足够的硬件,具有传感器和执行器,小脑会主导视觉、触觉各种感知来控制身体,完成复杂的任务,最后大...
继续阅读 »

60s要点速读:

1、人类本身就是世界上相当理想的一个具身智能体。它基本上具备三个方面,三个成分:第一方面是身体,第二方面是小脑,第三方面是大脑。身体的部分具身必须要有足够的硬件,具有传感器和执行器,小脑会主导视觉、触觉各种感知来控制身体,完成复杂的任务,最后大脑部分,它主导上层的逻辑推理、决策、长时间的规划以用自然语言能够和其他的智能体、环境交流。

2、ChatGPT主要是对于语言的处理能力,如果真正的想要让通用人工智能发挥出它的力量,未来的AGI需要有具身的实体,同真实的物理世界相交互来完成各种任务,这样才能给产业带来真正更大的价值。

3、具身机器人目前遇到的主要有四大挑战:第一,机器人不能够像大语言模型一样有一个基础大模型直接一步到位,做到最底层的控制。第二,计算能力的挑战。即使谷歌研发的Robotics Transformer模型,要做到机器人控制,距离实际需要的控制水平仍有许多事情要做。第三,如何把机器人多模态的感官感知全部融合起来,仍面临诸多难题需要解决。第四,机器人的发展需要收集很多数据,其中也面临很多安全隐私等方面的问题。

正文:

最近,ChatGPT的出现,在人工智能在学术上是一个突破,同时它为各行各业也创造了许多新价值。所以人工智能的下一步是什么呢?ChatGPT主要是对于语言的处理能力,如果真正的想要让通用人工智能发挥出它的力量,未来的AGI需要有具身的实体,让它能够同真实的物理世界相交互来完成各种任务,这样才能够带来真正更大的一个价值。
那么,具身智能体长的应该是什么样子呢?人类本身就是世界上相当理想的一个具身智能体。它基本上具备三个方面,三个成分:第一方面是身体,第二方面是小脑,第三方面是大脑。身体的部分具身必须要有足够的硬件,具有传感器和执行器,小脑会主导视觉、触觉各种感知来控制身体,完成复杂的任务,最后大脑部分,它主导上层的逻辑推理、决策、长时间的规划以用自然语言能够和其他的智能体、环境交流。目前,清华大学交叉信研究院里有八九位老师近年来的工作都是在关于具身智能的方方面面。接下来我想从这些团队的一些进展和思考方面,和大家分享。
第一,关于身体部分。具身AGI最理想身体的形式,我们认为应该就是人形机器人。因为人类的社会环境主要是为人类而定制的,比如说楼梯的结构、门把手的高度、被子的形状等等,这些都是为了人类的形状而定制,所以如果我们能够打造一个有泛应用的通用机器人,人形是最好最适合的一个形态,人形机器人能够适应人类的各种环境。
在清华大学交叉信息研究院里,我们自主研发了人形机器人初步的造型,这个工作主要由陈建宇团队所完成的。目前我们已经有了两个形式的机器人,其中有一个是前几个月在世界人工智能大会上亮相的“小星”。它的高度是1米2,而这次我们在这个机器人大会里面亮相的是“小星MAX”,它的身高达到了1米6,这两款机器人在展区有进行展示。

关于它的技术:它所用的是新一代的本体感知驱动器技术方案,在算法方面采用了动态的双足行走,是世界上为数不多的,能够走通整个软硬件技术的团队之一。

其次,关于具身智能体第二方面的小脑如何体现呢?比如小星机器人实体上是一套机器人运动控制的算法,分成两层:上一层是固态规划层,下一层是基于动力学的实时全身运动控制,它用来计算发给电机关节精确的指令。我们再展示一下这几个机器人在户外运动的画面,可以看到左边小星可以在水泥地上很灵活的快速行走,在右边也可以在比较复杂的一个树林里面走,它具有一定的抗干扰的能力——在草地里、石子路上走的也具有稳定性。
在构建小脑的算法端,我们想到在未来需要给机器人更好的功能、更好的控制,所以我们也在研究灵活度更高的,利用人工智能、强化学习的方法去运用和强化学习框架。它的好处是没有一个模型的限制,所以它能够对于复杂的环境跟不确定的环境,能够展现出更强的适应的能力。另外还有一个方法来学习,就是能够利用人体运动实际的数据,我们把它放到这个框架里,给予强化学习更好的引导。
我们可以看到,通过强化学习,机器人能够用一种自然的方式来模拟人态的行走,在设计上我们可以使它消耗更低的能耗,我们把这个硬件参数代入仿真里,能够实现更高度的运动形态,比如在仿真里能够走到4米/秒。而除了这种方法以外,强化学习方面,清华大学交叉信息研究院里的队伍也来研究一些基础的核心技术,尤其是在机器人研究方面,能够使得强化学习更加有效。

第一是有关样本的效率方面,目前一直困扰着强化学习应用的难题它所需要的样本非常多。在这方面我们做了一些工作。比如Atari游戏作为标准测试的指标,Deepmind在2015年在自然上发表了DQN算法,需要花一千个小时去进行学习,才能够达到人类的水平,这在当时已经非常了不起,而高阳队伍提出了一个新的算法叫Efficient Zero,它能够在两小时时间里能够达到超过人类平均水平,比DQN提高了500倍的样本效率。
另外一个困扰着强化学习的难题是泛化性,就是对于这些任务及其环境中间的不确定性和干扰,能不能够泛化的更好,许华哲团队围绕着这个问题提出了一系列解决方案,比如应用到机器人包饺子的演示,我们可以看到在这个物理过程里面有些非常复杂的动作,使得算法适应性高,即使有人为干扰下也能够达到任务。
我们再看小脑方面。除了走路以外别的功能,其中一个重要的任务是视觉处理,赵行团队有一些最新工作:基于视觉机器人跑酷,在这里面四足机器人基于视觉信号能够识别路障,能够匍匐前进,能够跳高台,同时请注意到当这些跳跃失败的时候,这个机器人会不停的来尝试,一直到成功为主,未来我们也会把这类跑酷功能放到人形机器人来实现。

清华大学交叉信研究院赵行团队四足机器人
还有一个比较高端的感知就是触觉。人的皮肤吸收了很多的触觉信号,能够完成非常精细的物体抓取的动作来回避危险,所以我们希望给机器人能够有好的触觉的传感器,让它们能够触摸感受到这个世界。对此,许华哲队伍运用到一些非常好的材料,他设计了一个触觉传感器低成本、易操作,能够精确的感觉得到接触到物体三维的几何,还有能够捕捉到物体很细小的纹理,它和人工算法能够结合,能够达到物体的分割和最终的效果。并且,我们也做了一些下游的关于触觉物体操纵的触觉工作,希望机器人将来对于更小的物体能够操作。此外比较难的事情,就如何打造机器人灵活的双手,需要自由度非常高,接触和物件非常复杂,所以机器人想要做这些动作非常困难。弋力团队提出新的算法,可以用自动的方式来创建场景和建模仿真,使得机器人在仿真里学习到这些技术。
最后我们谈一谈关于机器人第三方面关于大脑。这一部分谷歌做了大量的工作,特别是Palm-e多模态的大语言,能够对机器人的任务进行规划,大语言模型就把他所做的事情调用到下沉的控制器,去按照这个顺序来做任务,这也是一个非常重要的,尤其是谷歌在具身大模型方面主要的技术路线。
不过,这个框架有一个主要的问题:它的下层不一定能够很好执行上一层的规划,尤其是中间如果发生一些意外的干扰。对此,陈建宇团队提出一个新的方案和新的框架,比如是否可以在任务执行中能够自动的判断是不是有异常,如果有异常的话怎么样解决,这些都是有一个语言模型和视觉模型自动的完成的。我们把这个方法用在了人形机器人上。首先我们需要像大语言模型一样,给这个机器人描述一下他所需要的任务,机器人按照任务来执行。在场景工作中,如果机器人做搬箱子的工作,它的视觉语言模型通过视角检测是否有意外发生,如果有的话如何能够纠正,如果看到这个箱子掉到地上,机器人能够想出一个方法最后把它捡起来,最后完成任务。
图片
除了上面谈到的以外,斯坦福大学的李飞飞团队,通过大语言模型有系统的去产生了一个代码来控制机器人,而清华大学交叉信息研究院的杨植麟团队也提出了CodeGeeX(多语言代码生成模型), 通过不同的大语言模型进行训练。
最后,我们谈谈目前还有很多挑战需要克服的方面。对于具身机器人,第一,我们能不能像大语言模型一样有一个具身的大模型,它能够直接的一步到位,能够控制最低层的效率。第二是关于计算能力的挑战,我们做一个比较,就像谷歌的Robotic Transformer做第一个到下沉的统一模型,目前只能达到三个赫兹的水平,和我们需要的500个赫兹差的很远,所以这里面还有很多的事情我们需要来克服困难。第三个挑战,怎么样把多模式的感官融合起来。第四个挑战,机器人要收集数据还需要很多的事情需要做,其中也面临很多安全隐私等方面的问题等。
(整理自姚期智于2023年9月20日在“2023世界机器人大会”上的发言,转载来源:清华大学人工智能国际治理研究院)
收起阅读 »

程序员工作建议

我正式踏入职场时间很短,对于工作有一些新的理解,主要是吸取的前辈建议和自己的教训,分享给大家。目标对象是以前的自己,审慎阅读。首先,工作就是用劳动成果换取劳动报酬的过程。这里一定要注意,是劳动成果而不是劳动。各位同学刚刚步入职场,可能会有种种抱怨、不满及对未来...
继续阅读 »

我正式踏入职场时间很短,对于工作有一些新的理解,主要是吸取的前辈建议和自己的教训,分享给大家。目标对象是以前的自己,审慎阅读。

首先,工作就是用劳动成果换取劳动报酬的过程。

这里一定要注意,是劳动成果而不是劳动。各位同学刚刚步入职场,可能会有种种抱怨、不满及对未来方向的迷茫。在职场上,甚至已经犯下一些错误而不自知。

在通往事业成功的路上,没有捷径。摆正心态,一步一个脚印,脚踏实地的努力工作,同时用心去体会个人成长的过程。借助工作,完成自己人生的阶段性目标


工作要务是摆正个人与公司的位置关系。一个稳定运行的公司,离开谁都可以正常运作,作为技术人员,更要有觉悟:是公司为自己能力的发挥提供了平台

我相信大多数同学入职时,公司的产品早已正常盈利。个人技术能力再强,也仅仅体现在个人的工作效率上;优秀的技术人员也就可以提升小团队的开发效率,而不只是自己。不要有怀才不遇的心态,这只能证明,个人的劳动成果不足以打动他人。


第二个想谈的问题是职业素质。工作的本质是交换,当一个任务下达时,就是一个新的契约签署过程,你有选择不接受任务并离开的权利;一旦选择接受,那只有一个选项:完成任务

编程与其他任何工作没有什么本质区别,最终产品的质量并不是靠各种流程来保障,而只取决与参与产品的所有技术人员的职业素质。

深入细节逐个把控消耗的精力随着人数会指数膨胀,要深刻认知到流程可以给予最低限度的保障,是在一个黑盒中添加探针,能保证在做事,没办法保证在做正确的事。项目容易成功,商业成功不好说。而恰恰,你这个职业是通过技术手段支持公司的商业目标

在这里,不想多说什么,只提出以下几个问题:

  • 你目前的工作是因为喜欢而选择吗?
  • 你对目前的薪资满意吗?
  • 你对目前参与的产品感兴趣吗?
  • 你对目前参与的产品有什么建设性的意见吗?
  • 你喜欢身边的同事吗?
  • 你敢于承担责任吗?

管理程序员非常简单:给他喜欢的项目,并让他决定一切。打造或者进入一个这样的团队是最好的,不然,也可以成为这样的程序员。

但是管理一个团队,一定会有不满足职业素质的程序员,出现的原因有很多,推论如何解决可以讨论更多。这里提到存在这个客观现象,是为了引以为戒。

希望同学们从不成为“摆烂”的程序员开始,不要向已经开始混的人学习,先为自己负责,不要忘记了自己的目标。


第三个问题是如何平衡工作与学习

工作以后,能看到大家有各种没时间学习的原因。我一直倡导思考问题先从客观上找原因,那么这些客观原因对谁来说都是一样的吗?大家的进步速度真的一样吗?

这里给大家一些建议,可以尝试一下:

  • 在经济允许的条件下,尽量住的离公司近一些,减少通勤时间
  • 尽量不在手机上学习
  • 设定短期目标
  • 多写博客记录学习、生活心得
  • 找伙伴一起探讨技术


第四个问题是随时保持紧迫感。年轻人需要敢干,不能因为年轻反而躺好放松。工作是为了在不太远的未来,让自己能够承担起该负的责任。不要等待,不要患得患失,随时做好准备,接受全新的挑战。

作者:杨鼎睿
来源:www.yuque.com/abser/talks/dtvbqfuh1efd4t87

收起阅读 »

类的布局——成员变量

日常开发中,我们定义的OC类,都会被编译成结构体类型:/// Represents an instance of a class.struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY...
继续阅读 »

日常开发中,我们定义的OC类,都会被编译成结构体类型:

/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

在类中定义的属性和成员变量,也会变成结构体的成员变量:

@interface ObjectA : NSObject
@property(nonatomic,assign)BOOL b;
@end

// 类ObjectA会转化为如下结构体
struct ObjectA_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 继承自NSObject
BOOL _b;
};

知道了类的真面目,可以在内存级别去做一些操作;例如:

@interface ObjectA : NSObject
@property(nonatomic,assign,readonly)BOOL b;
@end

...

- (void)viewDidLoad {
[super viewDidLoad];
ObjectA *aObj = [[ObjectA alloc] init];

NSLog(@"%@",aObj); // 在这一行打断点

// Do any additional setup after loading the view.
}


用代码实现如下:

- (void)viewDidLoad {
[super viewDidLoad];
ObjectA *aObj = [[ObjectA alloc] init];

void *aObj_ptr = (__bridge void *)aObj;
BOOL *_b_ptr = (BOOL *)((BytePtr)aObj_ptr + 0x8);
*_b_ptr = YES;

NSLog(@"%d",aObj.b); // 打印:1 证明已修改

// Do any additional setup after loading the view.
}


收起阅读 »

iOS面试题目——hook block(3)

// 题目:实现下面的函数,将任意参数 block 的实现修改成打印所有入参,并调用原始实现//// 比如// void(^block)(int a, NSString *b) = ^(int a, NSString *b){// NSLog(@"...
继续阅读 »
// 题目:实现下面的函数,将任意参数 block 的实现修改成打印所有入参,并调用原始实现
//
// 比如
// void(^block)(int a, NSString *b) = ^(int a, NSString *b){
// NSLog(@"block invoke");
// }
// HookBlockToPrintArguments(block);
// block(123,@"aaa");
// 这里输出 "123,aaa" 和 "block invoke"

// void(^block)(int a, double b) = ^(int a, double b){
// NSLog(@"block invoke");
// }
// HookBlockToPrintArguments(block);
// block(123,3.14);
// 这里输出 "123,3.14" 和 "block invoke"

分析题目:首先,题目的本意和上一个题目一样,就是hook block 的 invoke,然后将其所有的入参打印出来,再调用原实现。区别在于任意Block,这个任意block,就让我们无法对用来替换的函数有一个很合适的定义,因为我们定义的时候,根本就不知道即将hook的block有几个参数。

这个问题,可以用libffi来解决。

整个思路如下:

1、获取要hook的block的相关信息,例如返回值、参数列表。这些信息都存储在bkock的方法签名里。

2、通过上一步获取到的信息,利用libffi创建一个函数模板(ffi_prep_cif())。

3、创建动态调用函数,并替换block中的Invoke。

4、编写替换函数,并实现调用原函数。

代码实现:

  • 获取block的签名信息:

    struct Block_layout *layout = (__bridge struct Block_layout *)block;

if (! (layout->flags & BLOCK_HAS_SIGNATURE)){
NSLog(@"当前block没有签名");
return;
}

uint8_t *desc = (uint8_t *)layout->descriptor;

desc += sizeof(struct Block_descriptor_1);

if (layout->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
struct Block_descriptor_3 *desc_3 = (struct Block_descriptor_3 *)desc;

const char *signature = desc_3->signature;
NSMethodSignature *m_signature = [NSMethodSignature signatureWithObjCTypes:signature];
  • 创建函数模版:
    ffi_type **args = malloc(sizeof(ffi_type *)*[m_signature numberOfArguments]);

// 返回值类型
ffi_type *return_ffi;
const char *return_type = [m_signature methodReturnType];
if (*return_type == @encode(_Bool)[0]) {
return_ffi = &ffi_type_sint8;
}else if (*return_type == @encode(signed char)[0]){
return_ffi = &ffi_type_sint8;
}else if (*return_type == @encode(unsigned char)[0]){
return_ffi = &ffi_type_uint8;
}else if (*return_type == @encode(short)[0]){
return_ffi = &ffi_type_sint16;
}else if (*return_type == @encode(int)[0]){
return_ffi = &ffi_type_sint32;
}else if (*return_type == @encode(long)[0]){
return_ffi = &ffi_type_sint64;
}else if (*return_type == @encode(long long)[0]){
return_ffi = &ffi_type_sint64;
}else if (*return_type == @encode(id)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(Class)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(SEL)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(void *)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(char *)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(float)[0]){
return_ffi = &ffi_type_float;
}else if (*return_type == @encode(double)[0]){
return_ffi = &ffi_type_double;
}else if (*return_type == @encode(void)[0]){
return_ffi = &ffi_type_void;
}else{
NSLog(@"未找到合适的类型");
return;
}
// 初始化参数列表
for (int i=0; i<[m_signature numberOfArguments]; i++) {
const char *type = [m_signature getArgumentTypeAtIndex:i];
if (*type == @encode(_Bool)[0]) {
args[i] = &ffi_type_sint8;
}else if (*type == @encode(signed char)[0]){
args[i] = &ffi_type_sint8;
}else if (*type == @encode(unsigned char)[0]){
args[i] = &ffi_type_uint8;
}else if (*type == @encode(short)[0]){
args[i] = &ffi_type_sint16;
}else if (*type == @encode(int)[0]){
args[i] = &ffi_type_sint32;
}else if (*type == @encode(long)[0]){
args[i] = &ffi_type_sint64;
}else if (*type == @encode(long long)[0]){
args[i] = &ffi_type_sint64;
}else if (*type == @encode(id)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(Class)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(SEL)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(void *)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(char *)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(float)[0]){
args[i] = &ffi_type_float;
}else if (*type == @encode(double)[0]){
args[i] = &ffi_type_double;
}else{
NSLog(@"未知类型:注,结构体未处理");
return;
}
}

// _cif 定义的是全局变量 ffi_cif _cif;
ffi_status status = ffi_prep_cif(&_cif, FFI_DEFAULT_ABI, (int)[m_signature numberOfArguments], return_ffi, args);
if (status != FFI_OK) {
NSLog(@"初始化 cif 失败");
return;
}
  • 创建并绑定动态调用的函数:
    // 	_closure 定义的是全局变量		ffi_closure *_closure;
// _replacementInvoke 定义的是全局变量 void *_replacementInvoke;

_closure = ffi_closure_alloc(sizeof(ffi_closure), &_replacementInvoke);
if (!_closure) {
NSLog(@"hook 失败");
return;
}
ffi_status closure_loc_status = ffi_prep_closure_loc(_closure, &_cif, replace_bloke2_2, (__bridge void *)(NSObject.new), _replacementInvoke);
if (closure_loc_status != FFI_OK) {
NSLog(@"Hook failed! ffi_prep_closure returned %d", (int)status);
return;
}
  • 替换block中的invoke:
    //    修改内存属性
vm_address_t invoke_addr = (vm_address_t)&layout->invoke;
vm_size_t vmsize = 0;
mach_port_t object = 0;
vm_region_basic_info_data_64_t info;
mach_msg_type_number_t infoCnt = VM_REGION_BASIC_INFO_COUNT_64;
kern_return_t ret = vm_region_64(mach_task_self(), &invoke_addr, &vmsize, VM_REGION_BASIC_INFO, (vm_region_info_t)&info, &infoCnt, &object);
if (ret != KERN_SUCCESS) {
NSLog(@"获取失败");
return;
}
vm_prot_t protection = info.protection;
// 判断内存是否可写
if ((protection&VM_PROT_WRITE) == 0) {
// 修改内存属性 ===> 可写
ret = vm_protect(mach_task_self(), invoke_addr, sizeof(invoke_addr), false, protection|VM_PROT_WRITE);
if (ret != KERN_SUCCESS) {
NSLog(@"修改失败");
return;
}
}
// 保存原来的invoke
origin_blockInvoke2_2 = (void *)layout->invoke;
layout->invoke = (uintptr_t)_replacementInvoke;
  • 实现替换函数:
    void replace_bloke2_2(ffi_cif *cif, void *ret, void **args, void *userdata) {
struct Block_layout *layout = (struct Block_layout *)userdata;
uint8_t *desc = (uint8_t *)layout->descriptor;

desc += sizeof(struct Block_descriptor_1);

if (layout->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
struct Block_descriptor_3 *desc_3 = (struct Block_descriptor_3 *)desc;

const char *signature = desc_3->signature;
NSMethodSignature *m_signature = [NSMethodSignature signatureWithObjCTypes:signature];

NSLog(@"回调函数");
NSLog(@"%d",cif->nargs);
// 解析参数
for (int i=0; i<[m_signature numberOfArguments]; i++) {
ffi_type *arg = args[i];
const char *type = [m_signature getArgumentTypeAtIndex:i];
if (*type == @encode(_Bool)[0]) {
NSLog(@"%d",(bool)arg->size);
}else if (*type == @encode(signed char)[0]){
NSLog(@"%d",(char)arg->size);
}else if (*type == @encode(unsigned char)[0]){
NSLog(@"%d",(unsigned char)arg->size);
}else if (*type == @encode(short)[0]){
NSLog(@"%d",(short)arg->size);
}else if (*type == @encode(int)[0]){
NSLog(@"%d",(int)arg->size);
}else if (*type == @encode(long)[0]){
NSLog(@"%ld",(long)arg->size);
}else if (*type == @encode(long long)[0]){
NSLog(@"%lld",(long long)arg->size);
}else if (*type == @encode(id)[0]){
NSLog(@"%@",(__bridge id)((void *)arg->size));
}else if (*type == @encode(Class)[0]){
NSLog(@"%@",(__bridge Class)((void *)arg->size));
}else if (*type == @encode(SEL)[0]){
NSLog(@"%s",((char *)arg->size));
}else if (*type == @encode(void *)[0]){
NSLog(@"0x%llx",((long long)arg->size));
}else if (*type == @encode(char *)[0]){
NSLog(@"%s",((char *)arg->size));
}else if (*type == @encode(float)[0]){
NSLog(@"%f",((float)arg->size));
}else if (*type == @encode(double)[0]){
NSLog(@"%f",((double)arg->size));
}else{
NSLog(@"未知类型:注,结构体未处理");
}
}
// 调用原函数
ffi_call(&_cif,(void *)origin_blockInvoke2_2, ret, args);
}


收起阅读 »

适当给生活按下暂停键,出去放空一下自己

踏春,亦或是暂存的生日礼物,哈哈哈,管他呢,啥也不想,跟着老公就行了。 2023-4-7 去了趟苏州,从不明白老公为什么要选择这个城市旅游,到我舍不得离开。 风景很美,风景如画也不过如此吧。美的地方有很多,但是又美又有历史的地方,文化底蕴深厚,让人感觉到有韵味...
继续阅读 »

踏春,亦或是暂存的生日礼物,哈哈哈,管他呢,啥也不想,跟着老公就行了。


2023-4-7


去了趟苏州,从不明白老公为什么要选择这个城市旅游,到我舍不得离开。


风景很美,风景如画也不过如此吧。美的地方有很多,但是又美又有历史的地方,文化底蕴深厚,让人感觉到有韵味的美。


ff09df8fd73f9b055a458b7263730d7.jpg
早上走在平江路上,很安静,初次看到小桥流水人家,很江南,确实没见过,慢慢我开始斜坐在桥上,吹着小风,从眼前美景慢慢关闭眼睛,闻着湿润的空气,放空自己。风是柔柔的,空气没有腥味,周五的早上,很安静。脚下是一千多年的桥,仿佛再闭一会儿睁开,就会穿梭在明朝。


ab7bcab41003788dcaf4fa0ea80c0af.jpg


a67013c1b762837973625f91dd23abc.jpg


ab637dfdbaebbf3e5f5f2688e4fa704.jpg


421653b8c798168391ebb94e5e79fc2.jpg


走了一两公里,没觉得累,就是有点怕走到街道的尽头,就是喧哗刺眼的高楼。不用思考,就是跟着老公走,街道很干净,让人心情很好。


到了拙政园,好吧,今天真的是周五嘛。上次这么多人,还是去北京看漫展的时候,排了两条街。我们在门口租了一个讲解器,这个地方很适合自己带着耳机,听着讲解,看看古树,过着小桥,慢慢欣赏。跟着人群走了一圈,我们还了讲解器,慢慢的又走了一圈,回顾着这个石头的来源,这棵古树的年份,仔细的看了看最高的那几棵大树,园林的设计。这趟,很值。


bab1b7b7b04621890920c2615891da2.jpg
9f551fa4de1eb98fa52fcaf68bee7a3.jpg


eeaf2e7046a98e5ac7f3395c32954fe.jpg


9ec8e4f90d466e48936734893f25072.jpg


f707d4101abc97d854392444a3cccf4.jpg


90bf28243f871dda2092f4e1b523100.jpg


4cf82e3c871554c2f2b0789ede10a26.jpg
出去后我们找了一家饭店,吃了个饭,去了酒店。睡了两个小时,晚上被老公拉起来,说是有好玩的。马上就来了兴致,收拾收拾出了门。


酒店就在山塘街边上,走了一会就进入了人流中,早上的安静的街道,现在人山人海。老公拉着我穿过人群,左拐右拐,一会过桥一会下桥,穿过一条清吧街,来到了一个叫玉涵堂的园子。老板把我们领到中间的位置,有小圆桌,椅子,小长椅,对面是戏台,旁边有个小亭子,亭子旁边是棵开的正好的桃花树,亭子上空有个小星星。慢慢就是萧声,琵琶声,二胡声,昆曲,小调,歌声。西厢记,牡丹亭,游园惊梦,玉簪记等等一曲曲婉转悠扬,动人心弦,最后一首声声慢结束了。


015c5988018f84ff5f3e385de17c7a0.jpg


edb9486763cdfca1efd27d4827c1955.jpg


8e528e0f425f30544b4ef0cf210be18.jpg


886f8cd3b1dfcfe37219bb352e918fd.jpg


5779131c33602e31103dfd02865e072.jpg
出门就是热闹的山塘街,是有点热闹,走路都是人挤人的跟着走,说实话,有点害怕踩踏事件。但是上到桥上,又觉得挤点也值得。桥下是一条条慢慢划着的船,两边是挂着灯笼的小房子,两边的水边是灯笼和房屋的倒影,没想到这种画,现实中也可以看到。


03047e5588ffaaee5dcc3d2cc7c7403.jpg


e3c6ce5b41bb6b3e7bb1dc8ff193e1d.jpg


8226270efbe7d87290e8186cd3fa22b.jpg


2023-4-8


第二天,有点起不来,但是一想到古镇,就有了动力。老公定的大巴车,车上看见了一些老年人,是的,他们来旅游,现在的他们有时间,大把的时间,有点羡慕。司机师傅拿着二维码,慢慢教他们怎么买回来的票,说是可以直接8折买古镇的票,一切就是告知,没有极力推销,让人比较舒服。再加上昨天走了一天的街道,街道不管人多少,干干净净,对这里的好感又上升了一些。


古镇的人。。。有点多。一波一波的旅游团。到古镇门口这一路,我眼里只有三个字——“万三蹄”。哈哈哈,没办法,一条街都是卖这个的,颜色很诱人。等会出来我要尝尝,再带点给家人尝尝。等到了五年没见的友人,朋友就是不用太联系,见到的时候一切依旧。这是老公的初中同学,从昆山打车来见我们的,五年没见了,一点不生疏。进去逛了一会,我们去坐了船。大哥很会聊,跟我们唱了几首歌谣,介绍了两边的树,道边停着的结婚用的花桥。听着小调,看着两边浮动的柳树,拍照的人,也是一番惬意。古镇有很多宅子,为首的当然是沈府,沈万三的家,在饭厅的“八大碗‘得知了,”万三蹄“的来历,朱元璋在沈万三家吃饭的时候,朱元璋问沈万三,这是什么菜,沈万三老婆说”猪蹄啊“。古代老百姓都要避讳皇帝的姓氏,沈万三赶紧说,这是”万三蹄“,后来的人就把这道菜叫做”万三蹄“。据说这道菜的制作,是先泡水,然后煮2分熟,蒸两个小时,再用冰糖炖,所以这个是偏甜口的。出去后,我们找了一家店,尝了下,挺鲜的,不腻,因为不喜甜口菜,所以没有多吃,但是是好吃的,还有不起眼的外婆菜,酸甜味的,下粥应该是不错的。


5ac7ab1702d660bf80583aa7d2a0faf.jpg


dde295ef86295da9f040846f66e4023.jpg


f5fe5e4498d865e44d4ab0fdf68ffb4.jpg


deb015fee9b3cfa8154f2c93de66b71.jpg


da9a31925dbcaf2ae755b6352fa7955.jpg


2023-4-9


第三天,每天2万多步的脚程,实在是有点歇不过来了,我们决定今天摆烂游了。慢悠悠退了房打车去了寒山寺。据导游说,寒山寺三个字不是一个人写的,四大才子之一,祝枝山写了前两个字。因为方丈看中了祝枝山的字,想让他提寺名,但是祝枝山是个财迷,要3000两,方丈凑了2200两,祝枝山写了前两个字,然后退回去了200两。后来方丈去世了,不了了之。直到陶濬宣写了第三个字,但是他有个要求,要在后面落款,所以现在的寒山寺外面的寺字是有陶濬宣的名字的。他就是写“光绪通宝”的人。寺庙外面都有一堵墙,写着寺名,据说是不建议拍照的,此乃萧墙,寺庙的萧墙是挡污秽东西的。休息之余,去大运河看货船,没装货的船,显得高大,空旷,装完货后,吃水很深,船旁边挂了一些轮胎,不知道是不是拿来当游泳圈的,哈哈哈,反正两个男生倒是很感兴趣,还跑过去近距离看了一会儿~


07a48040e37bd9a8d5c46139f676971.jpg


558cfdf22b1ab24fbe5a6033a377cab.jpg


78c9578f1a7685d837b0adf58682c80.jpg


朋友找了一家店,松鼠桂鱼好看,也好吃。吃完饭就是别离。无论是别离友人,还是别离这风景如画的城市,别离无人打扰的短暂时光,都是那么的不舍。但是别离,也是为了下一次的相聚。这一路很美好。


647608be5b11f18ff057fea3075f95d.jpg


94901f4a9986ea358818a886f5d8090.jpg


作者:没错就是我哎呀
来源:juejin.cn/post/7220236377937887269
收起阅读 »

10分钟3个步骤集成使用SkyWalking

随着业务发展壮大,微服务越来越多,调用链路越来越复杂,需要快速建立链路跟踪系统,以及建立系统的可观测性,以便快速了解系统的整体运行情况。此时就非常推荐SkyWalking了,SkyWalking不仅仅是一款链路跟踪工具,还可以作为一个系统监控工具,还具有告警功...
继续阅读 »

随着业务发展壮大,微服务越来越多,调用链路越来越复杂,需要快速建立链路跟踪系统,以及建立系统的可观测性,以便快速了解系统的整体运行情况。此时就非常推荐SkyWalking了,SkyWalking不仅仅是一款链路跟踪工具,还可以作为一个系统监控工具,还具有告警功能。使用简便、上手又快。真可谓快、准、狠。


本文主要介绍如何快速集成使用SkyWalking,从3个方面入手:原理、搭建、使用。


1、原理


1.1、概括


SkyWalking整体分为4个部分:探针采集层、数据传输和逻辑处理层、数据存储层、数据展示层。



1.2、探针采集层


所谓探针,实际上是一种动态代理技术,只不过不是我们常用的Java代理类,而是在类加载时,就生成了增强过的代理类的字节码,增强了数据拦截采集上报的功能。


探针技术是在项目启动时通过字节码技术(比如JavaAgent、ByteBuddy)进行类加载和替换,生成新的增强过的Class文件,对性能的影响是一次性的。


探针技术,因为在类加载时进行转换,增强了部分功能,所以会增加项目启动时间,同时也会增加内存占用量和线程数量。但是对性能影响不大,官方介绍在5% ~ 10%之间。



探针层在类转换时,通过各种插件对原有的类进行增强,之后在运行时拦截请求,然后将拦截的数据上报给Skywalking服务端。同时再加上一些定时任务,去采集应用服务器的基础数据,比如JVM信息等。


1.3、数据传输和逻辑处理层


SkyWalking探针层使用了GRPC作为数据传输框架,将采集的数据上报到SkyWalking服务端。


SkyWalking服务端接收数据后,利用各种插件来进行数据的分析和逻辑处理。比如:JVM相关插件,主要用于处理上报上来的JVM信息,数据库插件用来分析访问数据库的信息。然后在将数据存入到数据存储层。


1.4、数据存储层


SkyWalking的数据存储层支持多种主流数据库,可以自行到配置文件里查阅。我推荐使用ElasticSearch,存储量大,搜索性能又好。


1.5、数据展示层


SkyWalking 通过 Rocketbot 进行页面UI展示。可以在页面的左上角看到这个可爱的Rocketbot



2、搭建


知道了原理,搭建就很轻松了,使用SkyWalking其实就3个步骤:



  1. 搭建数据存储部件。

  2. 搭建SkyWalking服务端。

  3. 应用通过agent探针技术将数据采集上报给SkyWalking服务端。


2.1、搭建数据存储部件


SkyWalking支持多种存储方式,此处推荐采用Elasticsearch作为存储组件,存储的数据量较大,搜索响应快。


快速搭建Elasticsearch:



  1. 安装java:yum install java-1.8.0-openjdk-devel.x86_64

  2. 下载Elasticsearch安装包:http://www.elastic.co/cn/download…

  3. 修改elasticsearch.yml文件的部分字段:cluster.namenode.namepath.datapath.logsnetwork.hosthttp.portdiscovery.seed_hostscluster.initial_master_nodes。将字段的值改成对应的值。

  4. 在Elasticsearch的bin目录下执行./elasticsearch启动服务。

  5. 访问http://es-ip:9200,看到如下界面就代表安装成功。


{
"name": "node-1",
"cluster_name": "my-application",
"cluster_uuid": "GvK7v9HhS4qgCvfvU6lYCQ",
"version": {
"number": "7.17.1",
"build_flavor": "default",
"build_type": "rpm",
"build_hash": "e5acb99f822233d6ad4sdf44ce45a454xxxaasdfas323ab",
"build_date": "2023-02-23T22:20:54.153567231Z",
"build_snapshot": false,
"lucene_version": "8.11.1",
"minimum_wire_compatibility_version": "6.8.0",
"minimum_index_compatibility_version": "6.0.0-beta1"
},
"tagline": "You Know, for Search"
}

2.2、搭建SkyWalking服务端


搭建SkyWalking服务端只需要4步:


1、下载并解压skywalking:archive.apache.org/dist/skywal…



2、进入到安装目录下的修改配置文件:config/apllication.yaml。将存储修改为elasticsearch。



3、进入到安装目录下的bin目录,执行./startup.sh启动SkyWalking服务端。


4、此时使用jps命令,应该可以看到如下2个进程。一个是web页面进程,一个是接受和处理上报数据的进程。如果没有jps命令,那自行查看下是否配置了Java环境变量。 同时访问http://ip:8080应该可以看到如下界面。




2.3、应用采集上报数据


应用采集并且上报数据,直接使用agent探针方式。分为以下3步:


1、下载解压agentarchive.apache.org/dist/skywal…,找到skywalking-agent.jar



2、添加启动参数



  • 应用如果是jar命令启动,则直接添加启动参数即可:


java -javaagent:/自定义path/skywalking-agent.jar -Dskywalking.collector.backend_service={{agentUrl}} -jar xxxxxx.jar 

此处的{{agentUrl}}是SkyWalking服务端安装的地址,再加上11800端口。比如:10.20.0.55:11800




  • 应用如果是Docker镜像的部署方式,则需要将skywalking-agent.jar打到镜像里,类似下图:



3、启动项目后,即可看到监控数据,如下图:



3、UI页面使用


原理和搭建已经介绍完毕,接下来快速介绍UI页面的功能。下图标红的部分是重点关注区域:


3.1、仪表盘



  • APM:以全局(Global)、服务(Service)、服务实例(Instance)、端点(Endpoint)的维度展示各项指标。

  • Database:展示数据库的各项指标。




  • 服务(Service):某个微服务,或者某个应用。

  • 服务实例(Instance):某个微服务或者某个应用集群的一台实例或者一台负载。

  • 端点(Endpoint):某个Http请求的接口,或者 某个接口名+方法名。




3.2、拓扑图



3.3、追踪



关于UI界面的使用,还可以参考这个链接:juejin.cn/post/710630…,这里写的比较详细。


总结


本文主要从3个方面入手:原理、搭建、使用,介绍如何快速集成使用SkyWalking。核心重点:



  • SkyWalking其实就4部分组成:探针采集上报数据分析和逻辑处理、数据存储数据展示。安装使用简单、易上手。

  • 探针技术是SkyWalking的基石,说白了就是:在类加载时进行字节码转换增强,然后去拦截请求,采集上报数据。

  • UI页面的使用,多用用就熟悉了。


本篇完结!感谢你的阅读,欢迎点赞 关注 收藏 私信!!!


原文链接: http://www.mangod.top/articles/20…mp.weixin.qq.com/s/5P6vYSOCy…


作者:不焦躁的程序员
来源:juejin.cn/post/7288604780382879796
收起阅读 »

你敢信?比 setTimeout 还快 80 倍的定时器

web
起因 很多人都知道,setTimeout是有最小延迟时间的,根据MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间中所说: 在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这...
继续阅读 »

起因


很多人都知道,setTimeout是有最小延迟时间的,根据MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时间中所说:



在浏览器中,setTimeout()/setInterval() 的每调用一次定时器的最小间隔是 4ms,这通常是由于函数嵌套导致(嵌套层级达到一定深度)。



HTML Standard规范中也有提到更具体的:



Timers can be nested; after five such nested timers, however, the interval is forced to be at least four milliseconds.



简单来说,5 层以上的定时器嵌套会导致至少 4ms 的延迟。


用如下代码做个测试:


let a = performance.now();
setTimeout(() => {
let b = performance.now();
console.log(b - a);
setTimeout(() => {
let c = performance.now();
console.log(c - b);
setTimeout(() => {
let d = performance.now();
console.log(d - c);
setTimeout(() => {
let e = performance.now();
console.log(e - d);
setTimeout(() => {
let f = performance.now();
console.log(f - e);
setTimeout(() => {
let g = performance.now();
console.log(g - f);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);
}, 0);

在浏览器中的打印结果大概是这样的,和规范一致,第五次执行的时候延迟来到了 4ms 以上。


2021-05-13-21-04-16-067254.png


探索


假设就需要一个「立刻执行」的定时器呢?有什么办法绕过这个 4ms 的延迟吗,在 MDN 文档的角落里有一些线索:



如果想在浏览器中实现 0ms 延时的定时器,可以参考这里所说的window.postMessage()



这篇文章里的作者给出了这样一段代码,用postMessage来实现真正 0 延迟的定时器:


(function () {
var timeouts = [];
var messageName = 'zero-timeout-message';

// 保持 setTimeout 的形态,只接受单个函数的参数,延迟始终为 0。
function setZeroTimeout(fn) {
timeouts.push(fn);
window.postMessage(messageName, '*');
}

function handleMessage(event) {
if (event.source == window && event.data == messageName) {
event.stopPropagation();
if (timeouts.length > 0) {
var fn = timeouts.shift();
fn();
}
}
}

window.addEventListener('message', handleMessage, true);

// 把 API 添加到 window 对象上
window.setZeroTimeout = setZeroTimeout;
})();

由于postMessage的回调函数的执行时机和setTimeout类似,都属于宏任务,所以可以简单利用postMessageaddEventListener('message')的消息通知组合,来实现模拟定时器的功能。


这样,执行时机类似,但是延迟更小的定时器就完成了。


再利用上面的嵌套定时器的例子来跑一下测试:


2021-05-13-21-04-16-210864.png


全部在 0.1 ~ 0.3 毫秒级别,而且不会随着嵌套层数的增多而增加延迟。


测试


从理论上来说,由于postMessage的实现没有被浏览器引擎限制速度,一定是比 setTimeout 要快的。


设计一个实验方法,就是分别用postMessage版定时器和传统定时器做一个递归执行计数函数的操作,看看同样计数到 100 分别需要花多少时间。


实验代码:


function runtest() {
var output = document.getElementById('output');
var outputText = document.createTextNode('');
output.appendChild(outputText);
function printOutput(line) {
outputText.data += line + '\n';
}

var i = 0;
var startTime = Date.now();
// 通过递归 setZeroTimeout 达到 100 计数
// 达到 100 后切换成 setTimeout 来实验
function test1() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setZeroTimeout took ' +
(endTime - startTime) +
' milliseconds.'
);
i = 0;
startTime = Date.now();
setTimeout(test2, 0);
} else {
setZeroTimeout(test1);
}
}

setZeroTimeout(test1);

// 通过递归 setTimeout 达到 100 计数
function test2() {
if (++i == 100) {
var endTime = Date.now();
printOutput(
'100 iterations of setTimeout(0) took ' +
(endTime - startTime) +
' milliseconds.'
);
} else {
setTimeout(test2, 0);
}
}
}

实验代码很简单,先通过setZeroTimeout也就是postMessage版本来递归计数到 100,然后切换成 setTimeout计数到 100。


直接放结论,这个差距不固定,在 mac 上用无痕模式排除插件等因素的干扰后,以计数到 100 为例,大概有 80 ~ 100 倍的时间差距。在硬件更好的台式机上,甚至能到 200 倍以上。


2021-05-13-21-04-16-326555.png


Performance 面板


只是看冷冰冰的数字还不够过瘾,打开 Performance 面板,看看更直观的可视化界面中,postMessage版的定时器和setTimeout版的定时器是如何分布的。


2021-05-13-21-04-16-602815.png


这张分布图非常直观的体现出了上面所说的所有现象,左边的postMessage版本的定时器分布非常密集,大概在 5ms 以内就执行完了所有的计数任务。


而右边的setTimeout版本相比较下分布的就很稀疏了,而且通过上方的时间轴可以看出,前四次的执行间隔大概在 1ms 左右,到了第五次就拉开到 4ms 以上。


作用


也许有同学会问,有什么场景需要无延迟的定时器?其实在 React 的源码中,做时间切片的部分就用到了。


const channel = new MessageChannel();
const port = channel.port2;

// 每次 port.postMessage() 调用就会添加一个宏任务
// 该宏任务为调用 scheduler.scheduleTask 方法
channel.port1.onmessage = scheduler.scheduleTask;

const scheduler = {
scheduleTask() {
// 挑选一个任务并执行
const task = pickTask();
const continuousTask = task();

// 如果当前任务未完成,则在下个宏任务继续执行
if (continuousTask) {
port.postMessage(null);
}
},
};

React 把任务切分成很多片段,这样就可以通过把任务交给postMessage的回调函数,来让浏览器主线程拿回控制权,进行一些更优先的渲染任务(比如用户输入)。


为什么不用执行时机更靠前的微任务呢?关键的原因在于微任务会在渲染之前执行,这样就算浏览器有紧急的渲染任务,也得等微任务执行完才能渲染。


总结


可以了解如下几个知识点:



  1. setTimeout的 4ms 延迟历史原因,具体表现。

  2. 如何通过postMessage实现一个真正 0 延迟的定时器。

  3. postMessage定时器在 React 时间切片中的运用。

  4. 为什么时间切片需要用宏任务,而不是微任务。


作者:睡醒想钱钱
来源:juejin.cn/post/7229520942668824633
收起阅读 »

游戏开发中不同性格特点的程序员,你属于哪一种?

点击上方亿元程序员+关注和★星标 引言 大家好,我是亿元程序员,一位有着8年游戏行业经验的主程。 在游戏开发领域,每个程序员都有自己独特的方式来编写代码,这反映了他们的个性和思维方式。虽然代码风格和程序员的性格之间存在差异,但这些差异却构成了一个多彩的编程社...
继续阅读 »

点击上方亿元程序员+关注和★星标



引言


大家好,我是亿元程序员,一位有着8年游戏行业经验的主程。


在游戏开发领域,每个程序员都有自己独特的方式来编写代码,这反映了他们的个性和思维方式。虽然代码风格和程序员的性格之间存在差异,但这些差异却构成了一个多彩的编程社区。本文将探讨一些常见的代码风格,并探讨它们背后可能对应的程序员性格特点。


你属于哪一种?


注重细节的程序员


注重细节


有些程序员对代码的细节极为敏感,他们喜欢确保每个括号都放在正确的位置,每个变量都有清晰的命名规范。这种注重细节的程序员通常具备以下性格特点:



  • 谨慎与耐心:他们在编写代码时会花更多的时间来确保一切都无懈可击。

  • 善于研究:他们喜欢深入研究文档和规范,以确保他们的代码符合最佳实践。

  • 注重文档和注释:他们会编写详细的注释和文档,以帮助其他人理解他们的代码。

  • 喜欢代码审查:他们乐于接受同事的审查,以确保代码质量达到最高标准。


创造性的程序员


创造性


创造性的程序员常常寻求新颖的解决方案,他们擅长思考问题的不同角度。这种类型的程序员通常表现出以下性格特点:



  • 创新思维:他们喜欢提出独特的解决方案,寻求创新的方法来解决问题。

  • 乐于尝试新技术:他们喜欢接触新技术和工具,以探索新的可能性。

  • 问题解决能力强:他们具备出色的问题解决能力,能够应对复杂的挑战。

  • 勇于失败:他们不怕尝试新方法,即使失败也视之为学习的机会。


团队合作的程序员


团队合作


团队合作是许多项目成功的关键,一些程序员特别擅长与他人协作。这种类型的程序员通常表现出以下性格特点:



  • 良好的沟通技巧:他们善于与团队成员沟通和合作,分享知识和经验。

  • 乐于分享:他们愿意分享自己的知识,帮助其他人成长。

  • 接受反馈:他们乐于接受他人的反馈和建议,以改进工作。

  • 协同工作:他们喜欢与其他人一起解决问题,借助集体智慧来实现共同目标。


独立的程序员


独立


独立的程序员通常喜欢独自工作,他们具备自我驱动力。这种类型的程序员通常表现出以下性格特点:



  • 自主性:他们有强烈的自主性,能够自我激励,独立完成任务。

  • 自学能力:他们喜欢自学新技术和概念,寻找解决方案。

  • 自信:他们相信自己的能力,对独立工作充满信心。

  • 目标导向:他们能够明确目标并专注于实现它们。


快速迭代的程序员


快速迭代


一些程序员喜欢快速开发和迭代,他们对持续改进有着强烈的渴望。这种类型的程序员通常表现出以下性格特点:



  • 快速反馈:他们喜欢快速获取反馈,并根据反馈进行改进。

  • 不怕失败:他们将失败视为学习的机会,勇敢尝试新方法。

  • 敏捷开发:他们倾向于采用敏捷开发方法,将项目分解为小块,以便更容易管理和优化。


安全意识的程序员


安全意识


在网络时代,安全性成为至关重要的问题,一些程序员专注于保障代码的安全性。这种类型的程序员通常表现出以下性格特点:



  • 关注安全:他们注重代码和系统的安全性,努力避免潜在的风险。

  • 安全测试:他们喜欢进行安全漏洞扫描和测试,以发现和修复问题。

  • 遵循安全实践:他们遵循最佳的安全实践,确保数据和隐私的保护。

  • 学习网络安全知识:他们不断学习有关网络安全的知识,以保持警惕。


坚持主义的程序员


坚持主义


坚持主义的程序员通常坚守自己的编码标准和实践,他们追求代码的一致性和可维护性。这种类型的程序员通常表现出以下性格特点:



  • 坚守标准:他们喜欢坚守自己的编码标准,不轻易妥协代码质量。

  • 辩论与辩护:他们乐于进行辩论,辩护自己的决策和实践。

  • 维护一致性:他们追求代码的一致性,以提高可读性和可维护性。

  • 关注质量:他们希望保持高质量的代码,以减少错误和问题。


结语


看完之后,有没有符合以上一种或者多种特点的小伙伴? 没有也没有关系,和我一起学习游戏开发中的设计模式,让糟糕的代码在潜移默化中升华。


我是"亿元程序员",一位有着8年游戏行业经验的主程。在游戏开发中,希望能给到您帮助, 也希望通过您能帮助到大家。


AD:笔者线上的小游戏《贪吃蛇掌机经典》《填色之旅》《重力迷宫球》大家可以自行点击搜索体验。


实不相瞒,想要个在看!请把该文章分享给你觉得有需要的其他小伙伴。谢谢!


作者:亿元程序员
来源:juejin.cn/post/7288228582693044235
收起阅读 »

说出来你可能不信,分布式锁竟然这么简单...

大家好,我是小❤。 作为一个后台开发,不管是工作还是面试中,分布式一直是一个让人又爱又恨的话题。它如同一座神秘的迷宫,时而让你迷失方向,时而又为你揭示出令人惊叹的宝藏。 今天,让我们来聊聊分布式领域中那位不太引人注意却功不可没的角色,它就像是分布式系统的守卫,...
继续阅读 »

大家好,我是小❤。


作为一个后台开发,不管是工作还是面试中,分布式一直是一个让人又爱又恨的话题。它如同一座神秘的迷宫,时而让你迷失方向,时而又为你揭示出令人惊叹的宝藏。


今天,让我们来聊聊分布式领域中那位不太引人注意却功不可没的角色,它就像是分布式系统的守卫,保护着资源不被随意访问——这就是分布式锁!


想象一下,如果没有分布式锁,多个分布式节点同时涌入一个共享资源的访问时,就像一群饥肠辘辘的狼汇聚在一块肉前,谁都想咬一口,最后弄得肉丢了个精光,大家都吃不上。



而有了分布式锁,就像给这块肉上了道坚固的城墙,只有一只狼能够穿越,享受美味。


那它具体是怎么做的呢?这篇文章中,小❤将带大家一起了解分布式锁是如何解决分布式系统中的并发问题的。


什么是分布式锁?


在分布式系统中,分布式锁是一种机制,用于协调多个节点上的并发访问共享资源。


这个共享资源可以是数据库、文件、缓存或任何需要互斥访问的数据或资源。分布式锁确保了在任何给定时刻只有一个节点能够对资源进行操作,从而保持了数据的一致性和可靠性。


为什么要使用分布式锁?


1. 数据一致性


在分布式环境中,多个节点同时访问共享资源可能导致数据不一致的问题。分布式锁可以防止这种情况发生,确保数据的一致性。


2. 防止竞争条件


多个节点并发访问共享资源时可能出现竞争条件,这会导致不可预测的结果。分布式锁可以有效地防止竞争条件,确保操作按照预期顺序执行


3. 限制资源的访问


有些资源可能需要限制同时访问的数量,以避免过载或资源浪费。分布式锁可以帮助控制资源的访问


分布式锁要解决的问题


分布式锁的核心问题是如何在多个节点之间协调,以确保只有一个节点可以获得锁,而其他节点必须等待。



这涉及到以下关键问题:


1. 互斥性


只有一个节点能够获得锁,其他节点必须等待。这确保了资源的互斥访问。


2. 可重入性


指的是在同一线程内,外层函数获得锁之后,内层递归函数仍然可以获取到该锁。


说白了就是同一个线程再次进入同样代码时,可以再次拿到该锁。它的作用是:防止在同一线程中多次获取锁产生竞性条件而导致死锁发生


3. 超时释放


确保即使节点在业务过程中发生故障,锁也会被超时释放,既能防止不必要的线程等待和资源浪费,也能避免死锁。


分布式锁的实现方式


在分布式系统中,有多种方式可以实现分布式锁,就像是锁的品种不同,每种锁都有自己的特点。




  • 有基于数据库的锁,就像是厨师们用餐具把菜肴锁在柜子里,每个人都得排队去取。




  • 还有基于 ZooKeeper 的锁,它像是整个餐厅的门卫,只允许一个人进去,其他人只能在门口等。




  • 最后,还有基于缓存的锁,就像是一位服务员用号码牌帮你占座,先到先得。




1. 基于数据库的分布式锁


使用数据库表中的一行记录作为锁,通过事务来获取和释放锁。


例如,使用 MySQL 来实现事务锁。首先创建一张简单表,在某一个字段上创建唯一索引(保证多个请求新增字段时,只有一个请求可成功)。


CREATE TABLE `user` (  
  `id` bigint(20NOT NULL AUTO_INCREMENT,  
  `uname` varchar(255) DEFAULT NULL,  
  PRIMARY KEY (`id`),  
  UNIQUE KEY `name` (`uname`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4

当需要获取分布式锁时,执行以下语句:


INSERT INTO `user` (uname) VALUES ('unique_key')

由于 name 字段上加了唯一索引,所以当多个请求提交 insert 语句时,只有一个请求可成功。


使用 MySQL 实现分布式锁的优点是可靠性高,但性能较差,而且这把锁是非重入的,同一个线程在没有释放锁之前无法获得该锁


2. 基于ZooKeeper的分布式锁


Zookeeper(简称 zk)是一个为分布式应用提供一致性服务的中间组件,其内部是一个分层的文件系统目录树结构。


zk 规定其某一个目录下只能有唯一的一个文件名,其分布式锁的实现方式如下:



  1. 创建一个锁目录(ZNode) :首先,在 zk 中创建一个专门用于存储锁的目录,通常称为锁根节点。这个目录将包含所有获取锁的请求以及用于锁协调的节点。

  2. 获取锁:当一个节点想要获取锁时,它会在锁目录下创建一个临时顺序节点(Ephemeral Sequential Node)。zk 会为每个节点分配一个唯一的序列号,并根据序列号的大小来确定锁的获取顺序。

  3. 查看是否获得锁:节点在创建临时顺序节点后,需要检查自己的节点是否是锁目录中序列号最小的节点。如果是,表示节点获得了锁;如果不是,则节点需要监听比它序列号小的节点的删除事件。

  4. 监听锁释放:如果一个节点没有获得锁,它会设置一个监听器来监视比它序列号小的节点的删除事件。一旦前一个节点(序列号小的节点)释放了锁,zk 会通知等待的节点。

  5. 释放锁:当一个节点完成了对共享资源的操作后,它会删除自己创建的临时节点,这将触发 zk 通知等待的节点。


zk 分布式锁提供了良好的一致性和可用性,但部署和维护较为复杂,需要仔细处理各种边界情况,例如节点的创建、删除、网络分区等。


而且 zk 实现分布式锁的性能不太好,主要是获取和释放锁都需要在集群的 Leader 节点上执行,同步较慢。


3. 基于缓存的分布式锁


使用分布式缓存,如 Redis 或 Memcached,来存储锁信息,缓存方式性能较高,但需要处理分布式缓存的高可用性和一致性。


接下来,我们详细讨论一下在 Redis 中如何设计一个高可用的分布式锁以及可能会遇到的几个问题,包括:




  1. 死锁问题




  2. 锁提前释放




  3. 锁被其它线程误删




  4. 高可用问题




1)死锁问题


早期版本的 redis 没有 setnx 命令在写 key 时直接设置超时参数,需要用 expire 命令单独对锁设置过期时间,这可能会导致死锁问题。


比如,设置锁的过期时间执行失败了,导致后来的抢锁都会失败。


Lua脚本或SETNX


为了保证原子性,我们可以使用 Lua 脚本,保证SETNX + EXPIRE两条指令的原子性,我们还可以巧用RedisSET 指令扩展参数:SET key value[EX seconds][PX milliseconds][NX|XX],它也是原子性的。



SET key value [EX seconds] [PX milliseconds] [NX|XX]



  • NX:表示 key 不存在的时候,才能 set 成功,即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等待锁释放后,才能获取

  • EX seconds :设定 key 的过期时间,默认单位时间为秒

  • PX milliseconds: 设定 key 的过期时间,默认单位时间为毫秒

  • XX: 仅当 key 存在时设置值



在 Go 语言里面,关键代码如下所示:


func getLock() {    
   methodName := "getLock"    
   val, err := client.Do("set", methodName, "lock_value""nx""ex"100
   if err != nil {        
       zaplog.Errorf("%s set redis lock failed, %s", methodName, err)
       return
  }    
   if val == nil { 
       zaplog.Errorf("%s get redis lock failed", methodName)        
       return 
  }
   ... // 执行临界区代码,访问公共资源
   client.Del(lock.key()).Err() // 删除key,释放锁
}

2)锁提前释放


上述方案解决了加锁过期的原子性问题,不会产生死锁,但还是可能存在锁提前释放的问题。


如图所示,假设我们设置锁的过期时间为 5 秒,而业务执行需要 10 秒。



在线程 1 执行业务的过程中,它的锁被过期释放了,这时线程 2 是可以拿到锁的,也开始访问公共资源。


很明显,这种情况下导致了公共资源没有被严格串行访问,破坏了分布式锁的互斥性


这时,有爱动脑瓜子的小伙伴可能认为,既然加锁时间太短,那我们把锁的过期时间设置得长一些不就可以了吗?


其实不然,首先我们没法提前准确知道一个业务执行的具体时间。其次,公共资源的访问时间大概率是动态变化的,时间设置得过长也不好。


Redisson框架


所以,我们不妨给加锁线程一个自动续期的功能,即每隔一段时间检查锁是否还存在,如果存在就延长锁的时间,防止锁过期提前释放


这个功能需要用到守护线程,当前已经有开源框架帮我们解决了,它就是——Redisson,它的实现原理如图所示:



当线程 1 加锁成功后,就会启动一个 Watch dog 看门狗,它是一个后台线程,每隔 1 秒(可配置)检查业务是否还持有锁,以达到线程未主动释放锁,自动续期的效果。


3)锁被其它线程误删


除了锁提前释放,我们可能还会遇到锁被其它线程误删的问题。



如图所示,加锁线程 1 执行完业务后,去释放锁。但线程 1 自己的锁已经释放了,此时分布式锁是由线程 2 持有的,就会误删线程 2 的锁,但线程 2 的业务可能还没执行完毕,导致异常产生。


唯一 Value 值


要想解决锁被误删的问题,我们需要给每个线程的锁加一个唯一标识。


比如,在加锁时将 Value 设置为线程对应服务器的 IP。对应的 Go 语言关键代码如下:


const (  
   // HostIP,当前服务器的IP  
   HostIP = getLocalIP()
)

func getLock() {    
   methodName := "getLock"    
   val, err := client.Do("set", methodName, HostIP, "nx""ex"100
   if err != nil {        
       zaplog.Errorf("%s redis error, %s", methodName, err)
       return
  }    
   if val == nil { 
       zaplog.Errorf("%s get redis lock error", methodName)        
       return 
  }
   ... // 执行临界区代码,访问公共资源
   if client.Get(methodName) == HostIP {
       // 判断为当前服务器线程加的锁,才可以删除
       client.Del(lock.key()).Err()
  }
}

这样,在删除锁的时候判断一下 Value 是否为当前实例的 IP,就可以避免误删除其它线程锁的问题了。


为了保证严格的原子性,可以用 Lua 脚本代替以上代码,如下所示:


if redis.call('get',KEYS[1]) == ARGV[1] then
  return redis.call('del',KEYS[1])
else
  return 0
end;

4)Redlock高可用锁


前面几种方案都是基于单机版考虑,而实际业务中 Redis 一般都是集群部署的,所以我们接下来讨论一下 Redis 分布式锁的高可用问题。


试想一下,如果线程 1 在 Redis 的 master 主节点上拿到了锁,但是还没同步到 slave 从节点。


这时,如果主节点发生故障,从节点升级为主节点,其它线程就可以重新获取这个锁,此时可能有多个线程拿到同一个锁。即,分布式锁的互斥性遭到了破坏。


为了解决这个问题,Redis 的作者提出了专门支持分布式锁的算法:Redis Distributed Lock,简称 Redlock,其核心思想类似于注册中心的选举机制。



Redis 集群内部署多个 master 主节点,它们相互独立,即每个主节点之间不存在数据同步。


且节点数为单数个,每次当客户端抢锁时,需要从这几个 master 节点去申请锁,当从一半以上的节点上获取成功时,锁才算获取成功。


优缺点和常用实现方式


以上是业界常用的三种分布式锁实现方式,它们各自的优缺点如下:



  • 基于数据库的分布式锁:可靠性高,但性能较差,不适合高并发场景。

  • 基于ZooKeeper的分布式锁:提供良好的一致性和可用性,适合复杂的分布式场景,但部署和维护复杂,且性能比不上缓存的方式。

  • 基于缓存的分布式锁:性能较高,适合大部分场景,但需要处理缓存的高可用性。


其中,业界常用的分布式锁实现方式通常是基于缓存的方式,如使用 Redis 实现分布式锁。这是因为 Redis 性能优秀,而且可以满足大多数应用场景的需求。


小结


尽管分布式世界曲折离奇,但有了分布式锁,我们就像是看电影的观众,可以有条不紊地入场,分布式系统里的资源就像胶片一样,等待着我们一张一张地观赏。


这就是分布式的魅力!它或许令人又爱又恨,但正是科技世界的多样复杂性,才让我们的技术之旅变得更加精彩。



最后,希望这篇文章能够帮助大家更深入地理解分布式锁的重要性和实际应用。



想了解更多分布式相关的话题,可以看我另一篇文章,深入浅出:分布式、CAP和BASE理论



如果大家觉得有所收获或者启发,不妨动动小手关注我,然后把文章分享、点赞、加入在看哦~



xin猿意码


公众号


我是小❤,我们下期再见!


点个在看** 你最好看


作者:xin猿意码
来源:juejin.cn/post/7288166472131133474
收起阅读 »

一个全新的 Android 组件化通信工具

GitHub Gitee ComponentBus 这个项目已经内部使用了一段时间, 经过几次迭代. 他非常小巧, 且功能强大, 并且配有 IDEA 插件作为辅助. ComponentBus 利用 ASM、KSP, 使组件间的通信变得简单且高效. 第一步组件间...
继续阅读 »

GitHub

Gitee


ComponentBus 这个项目已经内部使用了一段时间, 经过几次迭代.

他非常小巧, 且功能强大, 并且配有 IDEA 插件作为辅助.

ComponentBus 利用 ASM、KSP, 使组件间的通信变得简单且高效.


第一步组件间通信


新建一个 Module, 我们给他添加一个接口


@Component(componentName = "Test")
object ComponentTest {

@Action(actionName = "init")
fun init(debug: Boolean) {
...
}

@Action(actionName = "getId")
fun getId(): String {
return "id-001"
}

@Action(actionName = "openUserPage", interceptorName = ["LoginInterceptor"])
fun openUserPage() {
val newIntent = Intent(MyApplication.application, UserActivity::class.java)
newIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
MyApplication.application.startActivity(newIntent)
}
}

我们可以看到, 任何方法、参数、返回值都可作为通信 Action, 只要给他加上 Action 注解.

并且我们可以给他添加拦截器, 当条件不满足时进行拦截, 并做其他操作.



由于 module 间没有依赖, 返回值应该是所有 module 都可以引用到的类型.

组件间调用, 参数默认值目前不支持使用.



第二部调用其他组件API


新建一个 Module, 我们调用另一个 Module 的 API


ComponentBus.with("Test", "init")
.params("debug", true)
.callSync<Unit>()

val result = ComponentBus.with("Test", "getId")
.callSync<String>()
if (result.isSuccess) {
val id = result.data!!
}

就是这么简单, 不需要接口下沉.



这里有个问题, 那就是 componentName、actionName 都是字符串, 使用上不方便, 需要查看名称、复制.

为了解决这个问题, 我专门开发了一款 IDEA 插件, 辅助使用.



IDEA 插件


插件搜索 componentBus


ComponentBusPlugin.gif


拦截器


全局拦截器


/**  
* 全局日志拦截器
*/

object LogGlobalInterceptor : GlobalInterceptor() {
override suspend fun <T> intercept(chain: Chain) = chain.proceed<T>().apply {
UtilsLog.log("Component: ${chain.request.componentName}${Utils.separatorLine}Action: ${chain.request.action}${Utils.separatorLine}Result: ($code) $msg $data", "Component")
}
override fun <T> interceptSync(chain: Chain) = chain.proceedSync<T>().apply {
UtilsLog.log("Component: ${chain.request.componentName}${Utils.separatorLine}Action: ${chain.request.action}${Utils.separatorLine}Result: ($code) $msg $data", "Component")
}
}

普通拦截器


/**  
* 判断是否是登录的拦截器
* 未登录会进入登录页面
*/

object LoginInterceptor : IInterceptor {
override suspend fun <T> intercept(chain: Chain): Result<T> {
return if (UsercenterComponent.isLoginLiveData.value == true) {
chain.proceed()
} else {
showLogin()
Result.resultError(-3, "拦截, 进入登录页")
}
}

override fun <T> interceptSync(chain: Chain): Result<T> {
return if (UsercenterComponent.isLoginLiveData.value == true) {
chain.proceedSync()
} else {
showLogin()
Result.resultError(-3, "拦截, 进入登录页")
}
}
}

END


更多详情在 GitHub

欢迎感兴趣的朋友提供反馈和建议。


作者:WJ
来源:juejin.cn/post/7287817398315892777
收起阅读 »

如何将pdf的签章变成黑色脱密

前言 事情是这样的,前段时间同事接到一个需求,需要将项目系统的签章正文脱密下载。不经意间听到同事嘀咕找不到头绪,网上的相关资料也很少,于是帮忙研究研究。 实现的思路: 首先,我们必须要明白一个PDF中存在哪些东西?PDF可以存储各种类型的内容,包括文本、图片、...
继续阅读 »

前言


事情是这样的,前段时间同事接到一个需求,需要将项目系统的签章正文脱密下载。不经意间听到同事嘀咕找不到头绪,网上的相关资料也很少,于是帮忙研究研究。


实现的思路:


首先,我们必须要明白一个PDF中存在哪些东西?PDF可以存储各种类型的内容,包括文本、图片、图形、表格、注释、标记和多媒体元素。那么印章在我们的PDF中其实就是存储的一个图片,然后这个图片附加的有印章信息,可用于文件的有效性验证,说白了其实就是一种【特殊的图片】,那么我们需要做的就是如何找到这个图片并如何将这个图片变成黑色最后插入到pdf的原始位置。下面我们就分析一下其处理的过程。


准备工作


我们使用apache 提供的 pdfbox用来处理和操作。


<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.24</version>
</dependency>

过程分析


查找印章定义


印章定义通常存储在 PDF 的资源文件中,例如字体、图像等。因此,我们需要找到印章定义所对应的 PDAnnotation(签名列表)。不同厂商对 签名信息 的标识可能不同,因此我们需要查找 PDF 文件中的 PDAnnotation。在这一步中,我们需要使用一些调试技巧和定向猜测,通过debug的模式我们去找或者猜测一下厂商的印章签名是什么,比如金格的就是:GoldGrid:AddSeal 。这个签名就带了金格的厂商名。



  • 首先是加载文档:PDDocument document = PDDocument.load(new File("test.pdf"));



  • 其次是遍历文档,查找每一个页中是否含有印章签名信息


List<PDAnnotation> annotations = page.getAnnotations();
for (PDAnnotation annotation : annotations) {
if (KG_SIGN.equals(annotation.getSubtype()) || NTKO_SIGN.equals(annotation.getSubtype())) {
// todo
}
}

上诉步骤我们就完成了查询信息的全过程,接下来我们需要获取印章图片信息。


获取印章流


一旦我们找到了印章定义所对应的 PDAnnotation,我们就可以获取到印章图片信息中相关的附加信息,比如印章的位置信息,字体,文字等等信息。


PDRectangle rectangle = annotation.getRectangle();
float width = rectangle.getWidth();
float height = rectangle.getHeight();

上诉代码我们获取了印章图片的大小信息,用于后续我们填充印章时的文件信息。PDRectangle 对象定义了矩形区域的左下角坐标、宽度和高度等属性。


PDAppearanceDictionary appearanceDictionary = annotation.getAppearance();
PDAppearanceEntry normalAppearance = appearanceDictionary.getNormalAppearance();
PDAppearanceStream appearanceStream = normalAppearance.getAppearanceStream();
PDResources resources = appearanceStream.getResources();
PDImageXObject xObject = (PDImageXObject)resources.getXObject(xObjectName);

那么上面代码就是我们获取到的原始图片对象信息。通过对PDImageXObject进行操作以完成我们的目的。


PDResources 资源对象包含了注释所需的所有资源,例如字体、图像等。可以使用资源对象进行进一步的操作,例如替换资源、添加新资源等。


在PDF文件中,图像通常被保存为一个XObject对象,该对象包含了图像的信息,例如像素数据、颜色空间、压缩方式等。对于一个PDF文档中的图像对象,通常需要从资源(Resources)对象中获取。


处理原始图片


一旦我们找到了印章图片对象,我们需要将其变成黑色。印章通常是红色的,因此我们可以遍历图像的像素,并将红色像素点变成黑色像素点。在这一步中,我们需要使用一些图像处理技术,例如使用 Java 的 BufferedImage 类来访问和修改图像的像素。


public static void replaceRed2Black(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
// 获取图片的像素信息
int[] pixels = image.getRGB(0, 0, width, height, null, 0, width);
// 循环遍历每一个像素点
for (int i = 0; i < pixels.length; i++) {
// 获取当前像素点的颜色
Color color = new Color(pixels[i]);
// 如果当前像素点的颜色为白色 rgb(255, 255, 255),颜色不变
if (color.getRed() == 255 && color.getGreen() == 255 && color.getBlue() == 255) {
pixels[i] &= 0x00FFFFFF;
}else{
// 其他颜色设置为黑色 :rgb(0, 0, 0)
pixels[i] &= 0xFF000000;
}
}
image.setRGB(0, 0, width, height, pixels, 0, width);
}

代码逻辑:首先获取图片的宽高信息,然后获取图片的像素信息,循环每一个像素,然后判断像素的颜色是什么色,如果不是白色那么就将颜色替换为黑色。


tips:这里其实有个小插曲,当时做的时候判断条件是如果为红色则将其变换为黑色,但是这里有个问题就是在红色边缘的时候,其颜色的rgb数字是一个区间,这样去替换的话,图片里面就会存在模糊和替换不全。所以后来灵光一现,改成现在这样。


插入处理后的图片


最后,我们需要将新的印章图像插入到 PDF 文件中原始印章的位置上,代码如下:


PDAppearanceStream newAppearanceStream = new PDAppearanceStream(appearanceStream.getCOSObject());
PDAppearanceContentStream newContentStream = new PDAppearanceContentStream(newAppearanceStream);
newContentStream.addRect(0, 0, width, height);
File file = new File("image.png");
PDImageXObject image = PDImageXObject.createFromFileByContent(file, document);
// 在内容流中绘制图片
newContentStream.drawImage(image, 0, 0, width, height);
// 关闭外观流对象和内容流对象
newContentStream.close();

这段代码是在Java语言中使用PDFBox库操作PDF文件时,创建一个新的外观流(Appearance Stream)对象,并在该流中绘制一张图片。


首先,通过调用PDAppearanceStream类的构造方法,创建一个新的外观流对象,并将其初始化为与原有外观流对象相同的COS对象。这里使用appearanceStream.getCOSObject()方法获取原有外观流对象的COS对象。然后,创建一个新的内容流(AppearanceContent Stream)对象,将其与新的外观流对象关联起来。


接下来,使用addRect()方法向内容流中添加一个矩形,其左下角坐标为(0,0),宽度为width,高度为height。该操作用于确定图片在外观流中的位置和大小。


然后,通过PDImageXObject类中的createFromFileByContent()方法创建一个PDImageXObject对象,该对象表示从文件中读取的图片。这里使用一个File对象和PDF文档对象document作为参数创建PDImageXObject对象。


接下来,使用drawImage()方法将读取的图片绘制到内容流中。该方法以PDImageXObject对象、x坐标、y坐标、宽度、高度作为参数,用于将指定的图片绘制到内容流中的指定位置。


最后,通过调用close()方法关闭内容流对象,从而生成一个完整的外观流对象。


到此我们就完成了印章的脱密下载的全过程,这个任务的难点在于怎么查找不同厂商对印章的签名定义以及对pdf的理解和工具API的理解。


作者:Aqoo
来源:juejin.cn/post/7221131955201687607
收起阅读 »

产品:能实现长列表的滚动恢复嘛?我:... 得加钱

web
前言 某一天,产品经理找到我,他希望我们能够给用户更好的体验,提供长列表的滚动记忆功能。就是说当鼠标滚轮滚动到长列表的某个位置时,单击一个具体的列表项,就切换路由到了这个列表项的详情页;当导航返回到长列表时,还能回到之前滚动到的位置去。 思路 我低头思考了一阵...
继续阅读 »

前言


某一天,产品经理找到我,他希望我们能够给用户更好的体验,提供长列表的滚动记忆功能。就是说当鼠标滚轮滚动到长列表的某个位置时,单击一个具体的列表项,就切换路由到了这个列表项的详情页;当导航返回到长列表时,还能回到之前滚动到的位置去。


思路


我低头思考了一阵儿,想到了history引入的scrollRestoration属性,也许可以一试。于是我回答,可以实现,一天工作量吧😂。产品经理听到后,满意地走了,但是我后知后觉,我为数不多的经验告诉我,这事儿可能隐隐有风险😨。但是没办法,no zuo no die。


scrollRestoration


Chrome46之后,history引入了scrollRestoration属性。该属性提供两个值,auto(默认值),以及manual。当设置为auto时,浏览器会原生地记录下window中某个元素的滚动位置。此后不管是刷新页面,还是使用pushState等方法改变页面路由,始终可以让元素恢复到之前的屏幕范围中。但是很遗憾,他只能记录下在window中滚动的元素,而我的需求是某个容器中滚动。

完犊子😡,实现不了。

其实想想也是,浏览器怎么可能知道开发者想要保存哪个DOM节点的滚动位置呢?这事只有开发者自己知道,换句话说,得自己实现。于是乎,想到了一个大致思路是:



发生滚动时将元素容器当时的位置保存起来,等到长列表再次渲染时,再对其重新赋值scrollTop和scrollLeft



真正的开发思路


其实不难想到,滚动恢复应该属于长列表场景中的通用能力,既然如此,那...,夸下的海口是一天,所以没招,只能根据上述的简单思路实现了一个,很low,位置信息保存在localStorage中,算是交了差。但作为一个有追求的程序员,这事必须完美解决,既然通用那么公共组件提上日程😎。在肝了几天之后,出炉的完美解决方案:



在路由进行切换、元素即将消失于屏幕前,记录下元素的滚动位置,当元素重新渲染或出现于屏幕时,再进行恢复。得益于React-Router的设计思路,类似于Router组件,设计滚动管理组件ScrollManager,用于管理整个应用的滚动状态。同理,类似于Route,设计对应的滚动恢复执行者ScrollElement,用以执行具体的恢复逻辑。



滚动管理者-ScrollManager


滚动管理者作为整个应用的管理员,应该具有一个管理者对象,用来设置原始滚动位置,恢复和保存原始的节点等。然后通过Context,将该对象分发给具体的滚动恢复执行者。其设计如下:


export interface ScrollManager {
/**
* 保存当前的真实DOM节点
* @param key 缓存的索引
* @param node
* @returns
*/

registerOrUpdateNode: (key: string, node: HTMLElement) => void;
/**
* 设置当前的真实DOM节点的元素位置
* @param key 缓存的索引
* @param node
* @returns
*/

setLocation: (key: string, node: HTMLElement | null) => void;
/**
* 设置标志,表明location改变时,是可以保存滚动位置的
* @param key 缓存的索引
* @param matched
* @returns
*/

setMatch: (key: string, matched: boolean) => void;
/**
* 恢复位置
* @param key 缓存的索引
* @returns
*/

restoreLocation: (key: string) => void;
/**
* 清空节点的缓存
* @param key
* @returns
*/

unRegisterNode: (key: string) => void;
}


  • 上述Manager虽然提供了各项能力,但是缺少了缓存对象,也就是保存这些位置信息的地方。使用React.useRef,其设计如下:


//缓存位置的具体内容
const locationCache = React.useRef<{
[key: string]: { x: number; y: number };
}>({});
//原生节点的缓存
const nodeCache = React.useRef<{
[key: string]: HTMLElement | null;
}>({});
//标志位的缓存
const matchCache = React.useRef<{
[key: string]: boolean;
}>({});
//清空节点方法的缓存
const cancelRestoreFnCache = React.useRef<{
[key: string]: () => void;
}>({});


  • 有了缓存对象,我们就可以实现manager,使用key作为缓存的索引,关于key会在ScrollElement中进行说明。


const manager: ScrollManager = {
registerOrUpdateNode(key, node) {
nodeCache.current[key] = node;
},
unRegisterNode(key) {
nodeCache.current[key] = null;
//及时清除
cancelRestoreFnCache.current[key] && cancelRestoreFnCache.current[key]();
},
setMatch(key, matched) {
matchCache.current[key] = matched;
if (!matched) {
//及时清除
cancelRestoreFnCache.current[key] && cancelRestoreFnCache.current[key]();
}
},
setLocation(key, node) {
if (!node) return;
locationCache.current[key] = { x: node?.scrollLeft, y: node?.scrollTop };
},
restoreLocation(key) {
if (!locationCache.current[key]) return;
const { x, y } = locationCache.current[key];
nodeCache.current[key]!.scrollLeft = x;
nodeCache.current[key]!.scrollTop = y;
},
};


  • 之后,便可以通过Context将manager对象向下传递


<ScrollManagerContext.Provider value={manager}>
{props.children}
</ScrollManagerContext.Provider>


  • 除了上述功能外,manager还有一个重要功能:获知元素在导航切换前的位置。在React-Router中一切路由状态的切换都由history.listen来发起,由于history.listen可以监听多个函数。所以可以在路由状态切换前,插入一段监听函数,来获得节点相关信息。


location改变 ---> 获得节点位置信息 ---> 路由update


  • 在实现中,使用了一个状态shouldChild,来确保监听函数一定在触发顺序上先于Router中的监听函数。实现如下:


const [shouldChild, setShouldChild] = React.useState(false);

//利用useLayoutEffect的同步,模拟componentDidMount,为了确保shouldChild在Router渲染前设置
React.useLayoutEffect(() => {
//利用history提供的listen监听能力
const unlisten = props.history.listen(() => {
const cacheNodes = Object.entries(nodeCache.current);
cacheNodes.forEach((entry) => {
const [key, node] = entry;
//如果matchCache为true,表明从当前路由渲染的页面离开,所以离开之前,保存scroll
if (matchCache.current[key]) {
manager.setLocation(key, node);
}
});
});

//确保该监听先入栈,也就是监听完上述回调函数后才实例化Router
setShouldChild(true);
//销毁时清空缓存信息
return () => {
locationCache.current = {};
nodeCache.current = {};
matchCache.current = {};
cancelRestoreFnCache.current = {};
Object.values(cancelRestoreFnCache.current).forEach((cancel) => cancel());
unlisten();
};
}, []);

//改造context传递
<ScrollManagerContext.Provider value={manager}>
{shouldChild && props.children}
</ScrollManagerContext.Provider>



  • 真正使用时,管理者组件要放在Router组件外侧,来控制Router实例化:


<ScrollRestoreManager history={history}>
<Router history={history}>
...
</Router>

</ScrollRestoreManager>

滚动恢复执行者-ScrollElement


ScrollElement的主要职责其实是控制真实的HTMLElement元素,决定缓存的key,包括决定何时触发恢复,何时保存原始HTMLElement的引用,设置是否需要保存的位置等等。ScrollElement的props设计如下:


export interface ScrollRestoreElementProps {
/**
* 必须缓存的key,用来标志缓存的具体元素,位置信息以及状态等,全局唯一
*/

scrollKey: string;
/**
* 为true时触发滚动恢复
*/

when?: boolean;
/**
* 外部传入ref
* @returns
*/

getRef?: () => HTMLElement;
children?: React.ReactElement;
}


  • ScrollElement本质上可以看作为一个代理,会拿到子元素的Ref,接管其控制权。也可以自行实现getRef传入组件中。首先要实现的就是滚动发生时,记录位置能力:


useEffect(() => {
const handler = function (event: Event) {‘
//nodeRef就是子元素的Ref
if (nodeRef.current === event.target) {
//获取scroll事件触发target,并更新位置
manager.setLocation(props.scrollKey, nodeRef.current);
}
};

//使用addEventListener的第三个参数,实现在window上监听scroll事件
window.addEventListener('scroll', handler, true);
return () => window.removeEventListener('scroll', handler, true);
}, [props.scrollKey]);


  • 接下来处理路由匹配以及DOM变更时处理的能力。注意,这块使用了对useLayoutEffectuseEffect执行时机的理解处理:


//使用useLayoutEffect主要目的是为了同步处理DOM,防止发生闪动
useLayoutEffect(() => {
if (props.getRef) {
//处理getRef获取ref
//useLayoutEffect会比useEffect先执行,所以nodeRef一定绑定的是最新的DOM
nodeRef.current = props.getRef();
}

if (currentMatch) {
//设置标志,表明当location改变时,可以保存滚动位置
manager.setMatch(props.scrollKey, true);
//更新ref,代理的DOM可能会发生变化(比如key发生了变化,remount元素)
nodeRef.current && manager.registerOrUpdateNode(props.scrollKey, nodeRef.current);
//恢复原先滑动过的位置,可通过外部props通知是否需要进行恢复
(props.when === undefined || props.when) && manager.restoreLocation(props.scrollKey);
} else {
//未命中标志设置,不要保存滚动位置
manager.setMatch(props.scrollKey, false);
}

//每次update注销,并重新注册最新的nodeRef,解决key发生变化的情况
return () => manager.unRegisterNode(props.scrollKey);
});


  • 上述代码,表示在初次加载或者每次更新时,会根据当前的Route匹配结果与否来处理。如果匹配,则表示ScrollElement组件应是渲染的,此时在effect中执行更新Ref的操作,为了解决key发生变化时DOM发生变化的情况,所以需要每次更新都处理。

  • 同时设置标识位,相当于告诉manager,node节点此刻已经渲染成功了,可以在离开页面时保存位置信息;如果路由不匹配,那么则不应该渲染,manager此刻也不用保存这个元素的位置信息。主要是为了解决存在路由缓存的场景。

  • 也可以通过when来控制恢复,主要是用来解决异步请求数据的场景。

  • 最后判断ScrollElement的子元素是否是合格的


//如果有getRef,直接返回children
if (props.getRef) {
return props.children as JSX.Element;
}

const onlyOneChild = React.Children.only(props.children);
//代理第一个child,判断必须是原生的tag
if (onlyOneChild && onlyOneChild.type && typeof onlyOneChild.type === 'string') {
//利用cloneElement,绑定nodeRef
return React.cloneElement(onlyOneChild, { ref: nodeRef });
} else {
console.warn('-----滚动恢复失败,ScrollElement的children必须为单个html标签');
}

return props.children as JSX.Element;

多次尝试机制


在某些低版本的浏览器中,可能存在一次恢复并不如预期的情况。所以实现多次尝试能力,其原理就是用一个定时器多次执行callback,同时设定时间上限,并返回一个取消函数给外部,如果最终结果理想则取消尝试,否则再次尝试直到时间上限内达到理想位置。更改恢复函数:


restoreLocation(key) {
if (!locationCache.current[key]) return;
const { x, y } = locationCache.current[key];
//多次尝试机制
let shouldNextTick = true;
cancelRestoreFnCache.current[key] = tryMutilTimes(
() => {
if (shouldNextTick && nodeCache.current[key]) {
nodeCache.current[key]!.scrollLeft = x;
nodeCache.current[key]!.scrollTop = y;
//如果恢复成功,就取消
if (nodeCache.current[key]!.scrollLeft === x && nodeCache.current[key]!.scrollTop === y) {
shouldNextTick = false;
cancelRestoreFnCache.current[key]();
}
}
},
props.restoreInterval || 50,
props.tryRestoreTimeout || 500
);
},

至此,滚动恢复的组件全部完成。具体源代码可以到github查看,欢迎star。 github.com/confuciusth…


效果


scroll-restore.gif


总结


一个滚动恢复功能,如果想要健壮,完善地实现。其实需要掌握Router,Route相关的原理、history监听路由变化原理、React Effect的相关执行时机以及一个好的设计思路。而这些都需要我们平时不断的研究,不断的追求完美。虽然这并不能“加钱”,但这种能力以及追求是我们成为技术大牛的路途中,最宝贵的财富。当然,能够加钱最好了😍。


创作不易,欢迎点赞!


作者:青春地平线
来源:juejin.cn/post/7186600603936620603
收起阅读 »

关于浏览器的一个逆天bug

web
1.问题描述: 这个bug是我在做一个二次元项目(vue+vite+mysql)的时候,最开始都没有问题,但是后来有一天我的这个项目打开控制台后出现了资源无法加载的问题,包括图片,组件等,但是我只要不打开控制台就没有问题,所以当时我觉得这个问题非常的逆天,...
继续阅读 »

1.问题描述:




这个bug是我在做一个二次元项目(vue+vite+mysql)的时候,最开始都没有问题,但是后来有一天我的这个项目打开控制台后出现了资源无法加载的问题,包括图片,组件等,但是我只要不打开控制台就没有问题,所以当时我觉得这个问题非常的逆天,


bug如图


bug效果





2.解决思路:


先说正确答案:浏览器抽风,把我默认的网络限制改成了离线,而我之前一直是无限制,因此导致了我一打开控制台就断网,最主要的惑因就是不止我常用的edg浏览器这样了,捏吗连谷歌浏览器都跟着抽风,导致我误判了




  1. 首先我遇到这种问题想的肯定先是我的代码有没有问题,因为这个bug是突然出现的。所以我检查了我的代码问题,例如图片我把原来的静态的src:“巴拉巴拉.jpg”换成了import动态引入的方法


     import src1 from "../assets/movie/miaonei/miaonei.aac";
     ​
     export default {
      name: "profile",
      components: { userTop },
      data() {
        return {
          src1,
        };
      },
      }

    但是问题依然没有得到解决。


    2.接下来我考虑到了浏览器本身的问题,但是因为我浏览器网络那里是默认,我的默认一直是无限制,接下来我就用谷歌打开了项目结果也是一样的,所以我就排除了是控制台网络的原因


    3.接下来就考虑是我nodel_modles或者vue,npm版本有问题,所以就开始检测各种的版本,但是也没有发现问题


    4.最后我就先放弃的一段时间,毕竟不用控制台也只是开发效率降低,不是不能写,后来我突然想到这种样子不就是断网吗,所以我认定了就是控制台打开导致的断网,所以一定是network那里的默认不是我之前的东西了,虽然我根本没有改过,但只有这一种可能了


    5.问题解决。


    3.解决后效果




    结语:



    山重水复疑无路,柳暗花明又一村。


    做项目遇到bug是很正常的事,对于在读生来说,遇到bug反而是一件是好事,我可以通过自己思考,结合所学的东西来解决问题,这样可以提升我们的能力,巩固我们的境界。


    就上面这个bug而言,在我成功解决这个问题之前,我都是不知道原来浏览器自己能修改我默认的东西。





作者:BittersweetYao
来源:juejin.cn/post/7189295826366103589
收起阅读 »

为什么同一表情'🧔‍♂️'.length==5但'🧔‍♂'.length==4?本文带你深入理解 String Unicode UTF8 UTF16

web
背景 为什么同样是男人,但有的男人'🧔‍♂️'.length === 5,有的男人'🧔‍♂'.length === 4呢? 这二者都是JS中的字符串,要理解本质原因,你需要明白JS中字符串的本质,你需要理解 String Unicode UTF8 UTF16 ...
继续阅读 »

背景


为什么同样是男人,但有的男人'🧔‍♂️'.length === 5,有的男人'🧔‍♂'.length === 4呢?


这二者都是JS中的字符串,要理解本质原因,你需要明白JS中字符串的本质,你需要理解 String Unicode UTF8 UTF16 的关系。本文,深入二进制,带你理解它!


从 ASCII 说起


各位对这张 ASCII 表一定不陌生:


image.png


因为计算机只能存储0和1,如果要让计算机存储字符串,还是需要把字符串转成二进制来存。ASCII就是一直延续至今的一种映射关系:把8位二进制(首位为0)映射到了128个字符上。


从多语言到Unicode


但是世界上不止有英语和数字,还有各种各样的语言,计算机也应该能正确的存储、展示它们。


这时候,ASCII的128个字符,就需要被扩充。有诸多扩充方案,但思路都是一致的:把一个语言符号映射到一个编号上。有多少个语言符号,就有多少个编号。


至今,Unicode 已经成为全球标准。



The Unicode Consortium is the standards body for the internationalization of software and services. Deployed on more than 20 billion devices around the world, Unicode also provides the solution for internationalization and the architecture to support localization.


Unicode 联盟是软件和服务国际化的标准机构。 Unicode 部署在全球超过 200 亿台设备上,还提供国际化解决方案和支持本地化的架构。



Unicode是在ASCII的128个字符上扩展出来的。


例如,英文「z」的Unicode码是7A(即十进制的122,跟ASCII一致)。


Unicode中80(即128号)字符是€,这是ASCII的128个字符(0-127)的后一个字符。


汉字「啊」的Unicode码是554A


Emoji「🤔」的Unicode码是1F914


从Unicode到Emoji


随着时代发展,人们可以用手机发短信聊天了,常常需要发送表情,于是有人发明了Emoji。Emoji其实也是一种语言符号,所以Unicode也收录了进来。


image.png


Unicode一共有多少


现在,Unicode已经越来越多了,它的编码共计111万个!(有实际含义的编码并没这么多)


目前的Unicode字符分为17组编排,每组称为平面(Plane),而每平面拥有65536(即2^4^4=2^16)个代码点。目前只用了少数平面。


平面始末字符值中文名称英文名称
0号平面U+0000 - U+FFFF基本多文种平面Basic Multilingual Plane,简称BMP
1号平面U+10000 - U+1FFFF多文种补充平面Supplementary Multilingual Plane,简称SMP
2号平面U+20000 - U+2FFFF表意文字补充平面Supplementary Ideographic Plane,简称SIP
3号平面U+30000 - U+3FFFF表意文字第三平面Tertiary Ideographic Plane,简称TIP
4号平面 至 13号平面U+40000 - U+DFFFF(尚未使用)
14号平面U+E0000 - U+EFFFF特别用途补充平面Supplementary Special-purpose Plane,简称SSP
15号平面U+F0000 - U+FFFFF保留作为私人使用区(A区)Private Use Area-A,简称PUA-A
16号平面U+100000 - U+10FFFF保留作为私人使用区(B区)Private Use Area-B,简称PUA-B

以前只有ASCII的时候,共128个字符,我们统一用8个二进制位(因为log(2)128=7,取整得8),就一定能存储一个字符。


现在,Unicode有16*65536=1048576个字符,难道必须用log(2)1048576=20 向上取整24位(3个字节)来表示一个字符了吗?


那样的话,字母z就是00000000 00000000 01111010了,而之前用ASCII的时候,我们用01111010就可以表示字母z。也就是说,同样一份纯英文文件,换成Unicode后,扩大了3倍!1GB变3GB。而且大部分位都是0。这太糟糕了!


因此,Unicode只是语言符号和一些自然数的映射,不能直接用它做存储。


UTF8如何解决「文本大小变3倍问题」


答案就是:「可变长编码」,之前我在文章《太卷了!开发象棋,为了减少40%存储空间,我学了下Huffman Coding》提到过。


使用「可变长编码」,每个字符不一定都要用统一的长度来表示,针对常见的字符,我们用8个二进制位,不常见的字符,我们用16个二进制位,更不常见的字符,我们用24个二进制位。


这样,能够减少大部分场景的文件体积。这也是哈夫曼编码的思想。


要设计一套高效的「可变长编码」,你必须满足一个条件:它是「前缀码」。即通过前缀,我就能知道这个字符要占用多少字节。


而UTF8,就是一种「可变长编码」。


UTF8的本质



  1. UTF8可以把2^21=2097152个数字,映射到1-4个字节(这个范围能够覆盖所有Unicode)。

  2. UTF8完全兼容ASCII。也就是说,在UTF8出现之前的所有电脑上存储的老的ASCII文件,天然可以被UTF8解码。


具体映射方法:



  • 0-127,用0xxxxxxx表示(共7个x)

  • 128-2^11-1,用110xxxxx 10xxxxxx表示(共11个x)

  • 2^11-2^16-1,用1110xxxx 10xxxxxx 10xxxxxx表示(共16个x)

  • 2^16-2^21-1,用11110xxx 10xxxxxx 10xxxxxx 10xxxxxx表示(共21个x)


不得不承认,UTF8确实有冗余,还有压缩空间。但考虑到存储不值钱,而且考虑到解析效率,它已经是最优解了。


UTF16的本质


回到本文开头的问题,为什么'🧔‍♂️'.length === 5,但'🧔‍♂'.length === 4呢?


你需要知道在JS中,字符串使用了UTF16编码(其实本来是UCS-2,UTF16是UCS-2的扩展)。



为什么JS的字符串不用UTF8?


因为JS诞生(1995)时,UTF8还没出现(1996)。



UTF16不如UTF8优秀,因为它用16个二进制位或32个二进制位映射一个Unicode。这就导致:



  1. 它涉及到大端、小端这种字节序问题。

  2. 它不兼容ASCII,很多老的ASCII文件都不能用了。


UTF16的具体映射方法:


16进制编码范围(Unicode)UTF-16表示方法(二进制)10进制码范围字节数量
U+0000 - U+FFFFxxxxxxxx xxxxxxxx (一共16个x)0-655352
U+10000 - U+10FFFF110110xx xxxxxxxx 110111xx xxxxxxxx (一共20个x)65536-11141114


细心的你有没有发现个Bug?UTF16不是前缀码? 遇到110110xx xxxxxxxx 110111xx xxxxxxxx,怎么判断它是1个大的Unicode字符、还是2个连续的小的Unicode字符呢?


答案:其实,在U+0000 - U+FFFF范围内,110110xx xxxxxxxx110111xx xxxxxxxx都不是可见字符。也就是说,在UTF16中,遇到110110一定是4字节UTF16的前2字节的前缀,遇到110111一定是4字节UTF16的后2字节的前缀,其它情况,一定是2字节UTF16。这样,通过损失了部分可表述字符,UTF16也成为了「前缀码」。



JS中的字符串


在JS中,'🧔‍♂️'.length算的就是这个字符的UTF16占用了多少个字节再除以2。


我开发了个工具,用于解析字符串,把它的UTF8二进制和UTF16二进制都展示了出来。


工具地址:tool.hullqin.cn/string-pars…


我把2个男人,都放进去,检查一下他们的Unicode码:


image.png


image.png


发现区别了吗?


长度为4的,是1F9D4 200D 2642;长度为5的,是1F9D4 200D 2642 FE0F


都是一个Emoji,但是它对应了多个Unicode。这是因为200D这个零宽连字符,一些复杂的emoji,就是通过200D,把不同的简单的emoji组合起来,展示的。当然不是任意都能组合,需要你字体中定义了那个组合才可以。


标题中的Emoji,叫man: beard,是胡子和男人的组合。


末尾的FE0F变体选择符,当一个字符一定是emoji而非text时,它其实是可有可无的。


于是,就有的'🧔‍♂️'长,有的'🧔‍♂'短了。



作者:HullQin
来源:juejin.cn/post/7165859792861265928
收起阅读 »

2022 年:我在死亡边缘走过

当我躺在核磁共振机器里,就像科幻电影中的冷冻仓,我希望自己被封印在里面,睡个几百年。 我并没有写年终总结的习惯,以前也从来没写过。 一来是因为我总是觉得农历新年才是一年的开始,另外就是觉得给自己定新年目标也是一定完不成的~ 今年有点例外,我想写点东西,总结下...
继续阅读 »

image.png



当我躺在核磁共振机器里,就像科幻电影中的冷冻仓,我希望自己被封印在里面,睡个几百年。



我并没有写年终总结的习惯,以前也从来没写过。


一来是因为我总是觉得农历新年才是一年的开始,另外就是觉得给自己定新年目标也是一定完不成的~


今年有点例外,我想写点东西,总结下 2022 年,让它赶紧过去。Never see you 2022~


1. 死亡


十二月的某个周日晚上,我正在快乐的玩手机玩电脑,慢慢的发现胳膊没有力气,拿不动手机了,手指也几乎打不出来字了。大约 10 分钟之后,全身已经没有力气了,从椅子上站起来都吃力。


然后就喊我爸爸开车带我去医院急诊,住院了一星期。


当时的感受就像一只充满气的气球,被戳了一个大口子,气在飞快的跑,气球越来越软,但气球没有任何办法。


过去 30 年,我的身体一直很健康,完全没有任何征兆。不夸张的说,我当时觉得自己要完蛋了,甚至和我老婆交代了一些事情。


这件事情对我的影响非常大,我希望 2022 年赶快过去,走好不送。


经历过这件事情之后,想和大家分享一些我的想法。


1.1 及时享乐


上帝给了我们几十年的健康时光,我们碌碌无为。
上帝给了我们一周的痛苦,我们开始后悔没有好好享受生活。


这件事情给我最大的一个感受就是,珍惜健康的时间,玩好享受好。


我列了一份人生想做的事情清单,也会让家里人每人列一份。如果能把清单处理完,那以后出现最坏的事情,也不会后悔了。


1.2 透明


夫妻之间要完全透明,这样在意外到来的时候,没有后顾之忧。


第一,我会经常和老婆交流我对生死的看法,我对死亡这种事情看的很淡,死了说不定比活着舒服。


第二,我每个月会统计自己的资产状况,并记录在某软件上,我老婆可以很清楚的知道我有多少钱、分别放在哪里。同时我的各种账号密码,都对我老婆透明公开。


当时我给老婆说了一句话:“如果出现最坏的结果,我的钱在哪里你都知道。另外就是这种事情我看的很淡”。


1.3 莫生气


电视剧《天道》(小说《遥远的救世主》改编)中有这样一个情节:


男主丁元英在路边小摊吃饭,已经付过了一元饭钱,但吃完后摊主说没给钱。丁元英呆了一下,又付了一块钱。


当时看到这里,我大受震撼,不要和不值当的人生气。


之前在杭州租房子,物业女打电话说卫生间渗水到隔壁去了,让我打电话给隔壁房东处理。


我的意思是让隔壁房东加我微信,看看怎么处理。


物业女没有理我,第二天打电话指责我为什么没有联系隔壁房东,并且再次强调让我联系隔壁房东。


按我以前的性格,我 100% 不会主动联系隔壁房东,并且会继续和物业女吵几次架。


后来我想了想,我为什么要和物业女生气呢?这样搞下去未来几天的心情都会很糟糕的,于是我直接联系了隔壁房东,再也不用和物业女打交道了。


有个小伙伴说不敢写文章,因为每次发出去都会被喷。我自己以前也会膈应,但现在没啥特殊的感觉了,很多喷子评论我看都不看。


去年和好几个朋友分享过上面的故事,这件事情过后,感触更深,不要和不值当的人生气,当成空气忽略掉就可以了。


1.4 健康第一


以前每次下定决心锻炼,能坚持 2 天就算不错了。


经历过这个事情之后,锻炼已经不用下决心了。


我热爱运动~


2. 工作


工作上,今年是主动求变的一年,也终于想清楚了未来几年的发展方向。


年初,团队大调整,由于我们组的业务夕阳红,所以整个组被拆掉了,我被调动去做一块新业务,但个人兴趣不是很大。


在这个契机下,我好好思考了自己想做什么,确定了「区块链」行业大方向。


于是转岗到了蚂蚁链团队,花了好长时间学习行业知识,也算入了门。


半年以后,因为找到了「区块链」行业更前线的机会,并且是梦寐以求的「远程办公」,所以从蚂蚁离职,加入了新的公司。


目前入职两个月,整体感受超出预期,希望未来几年可以火力全开。


工作 7 年以来,今年是最特殊的一年,也是变化最大的一年,希望没有选错路。生命不息,折腾不止~


3. 生活


生活上今年最大的变化就是离开了杭州,回到了老家。


在老家农村办公了两个多月,发现并没有想象中的那么美好,有几点原因吧:



  1. 村里冬天光秃秃的,没啥好玩的

  2. 冷,没啥地方去

  3. 没有外卖,没有好吃的,每天只能在家吃饭

  4. 技术上交流机会比较少,有点憋得慌


明年还是得去大城市折腾折腾~~~~~~


其它的就是带父母去了一次海边。疫情结束,希望明年可以带家里人去更多的地方旅游。


4. 折腾


工作之余,一直在尝试折腾各种事情。


4.1 自媒体


前几年开始搞公众号,我只是想转发一些别人的文章,吸引一些关注,然后接点广告挣钱。没想到后来慢慢发展成了原创公众号,又累又不赚钱~~


今年听了朋友的建议,试试短视频。本来只想做每期十几秒,回答一个问题那种超短抖音视频,轻松不累。没想到阴差阳错 B 站渠道给火了,一个月涨粉接近一万个。承蒙 B 站粉丝厚爱,为了不丢脸,只能硬着头皮做了几期长视频,效果也都还可以。就是视频做起来太累了,慢慢的就鸽子了~~~


今年整体的创作输出上,技术内容更少了,思考感悟类内容更多了。也符合自己去年的想法,希望沉淀更多的方法论出来,授人以鱼不如授人以渔。


在自媒体上,我没给自己太大压力,想写了就写一篇,不想写就不写了,经常几个月不更新。距离上一次更文过了快两个月了,o(╥﹏╥)o


明年希望能多一些输出,不只是文字的,视频也希望多出几期。同时仍然不会在自媒体上给自己太多压力,开心就好~


4.2 创业


今年为了学习 web3,利用闲暇时间主动找了一些项目参与。和阿里同事深度玩了一个 web3 项目,投入了一个多月空余时间,虽然最后没有结果,但让自己真正的入门了 web3,也认识了一些牛逼的人,很充实很刺激~


这个项目 GG 之后,又和蚂蚁的几个小伙伴折腾另外的项目玩,虽然 99% 可能不会有结果,但过程真的很有趣。


也希望在新的一年,自己可以保持热情,让空余时间发挥更大的价值,折腾就对了~


4.3 看书


今年看了大约 15 本书,各种方向的都有,已经养成了看书的习惯,非常不错~
以前觉得看书,看完也记不住,太累。现在的心态就是记不住就记不住,看的时候爽就行~~


今年再接再厉,空余时间多看书~


4.4 理财


理财上,典型的韭菜一枚。
今年基金和股票应该在 -30% 以上,你们挣的钱都是我亏的~
目前投资属于放养状态,完全不管了,亏吧~
新的一年会多看一些投资类的书籍,让自己的亏的明白一些。


5. 总结


2022 年就这样吧,给 2023 年定一些方向:



  1. 工作上,保持热情,持续折腾,高标准要求自己

  2. 生活上,愿望清单开始清理,多带家里人出去玩玩

  3. 锻炼身体

  4. 自媒体继续努力

  5. 学习英语

  6. 空闲时间多折腾折腾各种项目

  7. 认识更多朋友



关于作者


砖家,brickspert


前蚂蚁集团前端技术专家


开源库 ahooks 作者,10k+ star ⭐️


开源库 antd mobile 前负责人,10k+ star ⭐️


你可以在以下渠道找到我:


公众号:前端技术砖家


B 站:前端技术砖家


知乎:砖家


掘金:前端技术砖家


Github:brickspert



作者:前端技术砖家
来源:juejin.cn/post/7184012075411177527
收起阅读 »

URL刺客现身,竟另有妙用!

web
工作中大家会接触到形形色色的 url,有些完美遵循格式,有些却像刺客一样,冷不丁的给你一刀。 先介绍下我的惨痛经历,给大家避避坑,最后告诉大家一个 url 刺客的妙用。 刺客介绍 1. iOS WKWebview 刺客 此类刺客手段单一,只会影响 iOS WK...
继续阅读 »

工作中大家会接触到形形色色的 url,有些完美遵循格式,有些却像刺客一样,冷不丁的给你一刀。


先介绍下我的惨痛经历,给大家避避坑,最后告诉大家一个 url 刺客的妙用。


刺客介绍


1. iOS WKWebview 刺客


此类刺客手段单一,只会影响 iOS WKWebview



  • 空格


运营人员由于在通讯工具中复制粘贴,导致前面多了一个空格,没有仔细检查,直接录入了后台管理系统。



  • 中文


运营人员为了方便自身统计,直接在url中加入中文,录入了后台管理系统。


现象均为打开一个空白页,常见的处理手段如下:



  • 将参数里的中文URIEncode

  • 去掉首尾空格


const safeUrl = (url: string) => {
const index = url.indexOf('?');

if (index === -1) return url.trim();

// 这行可以用任意解析参数方法替代,仅代表要拿到参数,不考虑兼容性的简单写法
const params = new URLSearchParams(url.substring(index));
const paramStr = Object.keys(params)
.map((key: string) => {
return `${key}=${encodeURIComponent(params[key])}`;
})
.join('&');

const formatUrl = url.substring(0, index + 1) + paramStr;

return formatUrl.trim();
};

可以看到虽然这里提出了一个 safeUrl 方法,但如果业务中大量使用 window.location.href , window.location.replace, 之类的方法进行跳转,替换起来会比较繁琐.


再比如在 Hybrid App 的场景中,虽然都是跳转,打开新的 webview ,还是在本页面跳转会是不同的实现,所以在业务内提取一个公共的跳转方法更有利于健壮性和拓展性。


值得注意的是,如果链接上的中文可能是用于统计的,在上报打点时,应该将其值(前端/服务端处理均可)进行 URIDecode,否则运营人员会在后台看到一串串莫名其妙的 %XX ,会非常崩溃(别问我怎么知道的,可能只是伤害过太多运营)


2. 格式刺客


格式刺客指的是,不管何种原因,不知何种场景,就是不小心配错了,打错了,漏打了等。


比如:https://www.baidu.com 就被打成了 htps://www.baidu.com、www.baidu.com 等。


// 检查URL格式是否正确
function isValidUrl(url: string): boolean {
const urlPattern = new RegExp(
"^(https?:\/\/)?" + // 协议
"(([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,})" + // 域名
"(:[0-9]{1,5})?" + // 端口号
"(\/.*)?$", // 路径
"i"
);
return urlPattern.test(url);
}

以上是一个很基础的判断,但是实际的应用场景中,有可能会需要填写相对路径,或者自定义的 scheme ,比如 wx:// ,所以检验的宽松度可以自行把握。


在校验到 url 配置可能存在问题时,可以上报到 sentry 或者其他异常监控平台,这样就可以比用户上报客服更早的发现潜在问题,避免长时间的运营事故。


3. 异形刺客


这种刺客在视觉上让人无法察觉,只有在跳转后才会让人疑惑不已。他也是最近被产品同学发现的,以下是当时的现场截图:



一段平平无奇的文本,跟着一段链接,视觉上无任何异常。


经过对跳转后的地址进行分析,发现了前面居然有一个这样的字符%E2%80%8B,好奇的在控制台中进行了尝试。




一个好家伙,这是什么,两个单引号吗?并不是,对比了很常用的 '%2B' ,单引号是自带的,那么我到底看到了什么,魔鬼嘛~


在进行了一番检索后知道了这种字符被称为零宽空格,他还有以下兄弟:



  • \u202b-\u202f

  • \ufeff

  • \u202a-\u202e


具体含义可以看看参考资料,这一类字符完全看不见,但是却对程序的运行产生了恶劣的影响。


可以使用这个语句去掉


str.replace(/[\u200b-\u200f\uFEFF\u202a-\u202e]/g, "");

刺客的妙用


头一天还被刺客气的瑟瑟发抖。第二天居然发现刺客的妙用。


场景:




  • 产品要求在微信环境隐藏标题




我方前端工程师:



  • 大手一挥,发功完毕,准备收工


document.title = '';

测试:




  • 来看看,页面A标题隐藏不了




我方前端工程师:


啊?怎么回事,本地调试还是好的,发上去就不行了,为什么页面A不可以,另外一个页面B只是参数变了变就行。


架构师出手:


页面A包含了开放标签,导致设置空Title失效,空,猛然想起了刺客,快用起来!


function setTitle(title: string) {
if (title) {
document.title = title;
} else {
document.title = decodeURIComponent('%E2%80%8B');
}
}

果然有效,成功解决了一个疑难杂症,猜测是微信里有不允许设置标题为空的机制,会在某些标签存在的时候被触发。(以上场景在 Android 微信 Webview 中可复现)


小结


以上只是工作中碰到 url 异常的部分场景和处理方案,如果小伙伴们也有类似的经历,可以在评论区中分享,帮助大家避坑,感谢朋友们的阅读,笔芯~


参考资料:


零宽字符 - 掘金LvLin


什么零宽度字符,以及零宽度字符在JavaScript中的应用 - 掘金whosmeya


作者:windyrain
来源:juejin.cn/post/7225133152490094651
收起阅读 »

3个bug导致Kafka消息丢失,我人麻了

近期修复了几个线上问题,其中一个问题让我惊讶不已,发个Kafka消息居然出现了三个bug!我给jym细数下这三个bug 发送MQ消息居然加了超时熔断 在封装的发送消息工具方法中竟然添加了Hystrix熔断策略,超过100毫秒就会被视为超时。而熔断策略则是在QP...
继续阅读 »

近期修复了几个线上问题,其中一个问题让我惊讶不已,发个Kafka消息居然出现了三个bug!我给jym细数下这三个bug


发送MQ消息居然加了超时熔断


在封装的发送消息工具方法中竟然添加了Hystrix熔断策略,超过100毫秒就会被视为超时。而熔断策略则是在QPS超过20且失败率大于5%时触发熔断。这意味着当QPS=20时,只要有一条消息发送超时,整个系统就会熔断,无法继续发送MQ消息。
hystrix.command.default.circuitBreaker.errorThresholdPercentage=5


HystrixCommand(
commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "100"),
@HystrixProperty(name = "execution.timeout.enabled", value = "true")})
public void doSendMessage(Message message){
// 发送消息
}

之前系统一直运行正常,直到最近系统请求量上升才触发了这个bug。现在已经找不到是谁配置了这个过于激进的熔断策略了。真的非常气人!


一般情况下,发送MQ消息不会失败。但是在服务刚启动且未预热时,可能会有少量请求超过100毫秒,被Hystrix判断为失败。而恰好当时QPS超过了20,导致触发了熔断。


为什么发送MQ消息还需要加入熔断机制呢? 我很不理解啊


MQ(消息队列)本身就是用来削峰填谷的,可以支持非常高的并发量。无论是低峰期还是高峰期,只要给MQ发送端添加熔断机制都会导致数据严重不一致!我真的不太明白,为什么要在发送MQ消息时加入熔断机制。


另外,为什么要设定这么激进的熔断策略呢?仅有5%的失败率就导致服务100%不可用,这是哪个天才的逻辑呢?至少在失败率超过30%且QPS超过200的情况下,才需要考虑使用熔断机制吧。在QPS为20的情况下,即使100%的请求都失败了,也不会拖垮应用服务,更何况只是区区5%的失败率呢。


这是典型的为了熔断而熔断!把熔断变成政治正确的事情。不加熔断反而变成异类,会被人瞧不起!


吞掉了异常


虽然添加熔断策略,会导致发送MQ失败抛出熔断异常,但是上层代码考虑了消息发送失败的情况。流程中包含分布式重试方案,但是排查问题时我才发现,重试策略居然没有生效!这是什么原因?


在一番排查后我发现,发送MQ的代码 吞掉了异常信息,没有向上抛出!


去掉无用的业务逻辑后,我把代码粘贴到下面。


try{
doSendMessage(msg);
}catch(Exception e){
log.error("发送MQ异常:{}", msg, e);
//发送失败MQ消息到公司故障群!
}

消息发送异常后,仅仅在系统打印了ERROR日志,并将失败消息发送到了公司的IM群里。然而,这样的处理方式根本无法让上层方法意识到消息发送失败的情况,更别提察觉到由于熔断而导致的发送失败了。在熔断场景下,消息根本没有被发送给MQ,而是直接失败。因此,可以确定消息一定丢失了。


面试时我们经常会被问到”如何保证消息不丢“。大家能够滔滔不绝地说出七八个策略来确保消息的可靠性。然而当写起代码时,为什么会犯下如此低级的错误呢?


仅仅打印ERROR日志就能解决问题吗?将故障消息上报到公司的群里就有人关注吗?考虑到公司每天各种群里都会涌现成千上万条消息,谁能保证一定有人会关注到!国庆节放假八天,会有人关注公司故障群的消息吗?


很多人在处理异常时习惯性的吞掉异常,害怕把异常抛给上游处理。系统应该处理Rpc调用失败、MQ发送失败的场景,不应该吞掉异常,而是应该重试!一般流程都会有整体的分布式重试机制,出问题不怕、出异常也不怕,只要把问题抛出,由上游发起重试即可。


悄咪咪的把异常吞掉,不是处理问题的办法!


于是我只能从日志中心捞日志,然后把消息手动发送到MQ中。我真的想问,这代码是人写的吗?


服务关闭期间,生产者先于消费者关闭,导致消息发送失败


出问题的系统流程是 先消费TopicA ,然后发送消息到Topic B。但是服务实例关闭期间,发送TopicB消息时,报错 producer has closed。为什么消费者还未关闭,生产者先关闭呢?


这个问题属于服务优雅发布范畴,一般情况下都应该首先关闭消费者,切断系统流量入口,然后再关闭生产者实例。


经过排查,发现问题的原因是生产者实例注册了shutdown hook钩子程序。也就是说,只要进程收到Kill信息,生产者就会启动关闭流程。这解释了为什么会出现这个问题。


针对这个问题,我修改了策略,删除了生产者注册shutdown hook钩子的逻辑。确保消费者先关闭!生产者后关闭。


总结


如果有人问我:消息发送失败的可能原因,我是肯定想不到会有这三个原因的。也是涨见识了。


很多人滔滔不绝的谈着 消息不丢不重,背后写的代码却让人不忍直视!


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

Linux当遇到kill -9杀不掉的进程怎么办?

web
前言 在Linux中,我们经常使用kill或者kill -9来杀死特定的进程,但是有些时候,这些方法可能无法终止某些进程。本文将详细解释为什么会出现这种情况,以及如何处理这种问题。 无法被杀死的进程: 首先,我们来理解一下为什么有些进程无法被杀死。通常,这是因...
继续阅读 »

前言


在Linux中,我们经常使用kill或者kill -9来杀死特定的进程,但是有些时候,这些方法可能无法终止某些进程。本文将详细解释为什么会出现这种情况,以及如何处理这种问题。


无法被杀死的进程:


首先,我们来理解一下为什么有些进程无法被杀死。通常,这是因为这些进程处于以下两种状态之一:


僵尸进程(Zombie Process):


当一个进程已经完成了它的运行,但是其父进程还没有读取到它的结束状态,那么这个进程就会成为僵尸进程。僵尸进程实际上已经结束了,所以你无法使用kill命令来杀掉它。



内核态进程:


如果一个进程正在执行某些内核级别的操作(即进程处在内核态),那么这个进程可能无法接收到kill命令发送的信号。


查找和处理僵尸进程:


如果你怀疑有僵尸进程存在,你可以使用以下命令来查找所有的僵尸进程:


ps -A -ostat,ppid,pid,cmd | grep -e '^[Zz]'

这个命令实际上是由两个命令通过管道(|)连接起来的。管道在Linux中的作用是将前一个命令的输出作为后一个命令的输入。命令的两部分是 ps -A -ostat,ppid,pid,cmd 和 grep -e '^[Zz]'。



  • ps -A -ostat,ppid,pid,cmd:这是ps命令,用来显示系统中的进程信息。

    • -A:这个选项告诉ps命令显示系统中的所有进程。

    • -o:这个选项允许你定义你想查看的输出格式。在这里,你定义的输出格式是stat,ppid,pid,cmd。这会让ps命令输出每个进程的状态(stat)、父进程ID(ppid)、进程ID(pid)以及进程运行的命令(cmd)。



  • grep -e '^[Zz]':这是grep命令,用来在输入中查找匹配特定模式的文本行。

    • -e:这个选项告诉grep命令接下来的参数是一个正则表达式。

    • '^[Zz]':这是你要查找的正则表达式。^符号表示行的开始,[Zz]表示匹配字符“Z”或者“z”。因此,这个正则表达式会匹配所有以“Z”或者“z”开头的行。在ps命令的输出中,状态为“Z”或者“z”的进程是僵尸进程。




因为僵尸进程已经结束了,所以你无法直接杀掉它。但是,你可以试图杀掉这些僵尸进程的父进程。杀掉父进程之后,僵尸进程就会被init进程(进程ID为1)接管,然后被清理掉。


你可以使用以下命令来杀掉父进程:


kill -HUP [父进程的PID]

请注意,在杀掉父进程之前,你需要确定这样做不会影响到系统的其他部分。另外,这个方法并不保证能够清理掉所有的僵尸进程。


查找和处理内核态进程:


如果一个进程处在内核态,那么这个进程可能无法接收到kill命令发送的信号。在这种情况下,你需要首先找到这个进程的父进程,然后试图杀掉父进程。你可以使用以下命令来查找进程的父进程:


cat /proc/[PID]/status | grep PPid

这个命令会输出进程的父进程的ID,由两个独立的命令组成,通过管道(|)连接起来。我会分别解释这两个命令,然后再解释整个命令:



  • cat /proc/[PID]/status :

    • 这是一个cat命令,用于显示文件的内容。在这个命令中,它用于显示一个特殊的文件/proc/[PID]/status。

    • /proc是一个特殊的目录,它是Linux内核和用户空间进行交互的一种方式。在/proc目录中,每个正在运行的进程都有一个与其PID对应的子目录。每个子目录中都包含了关于这个进程的各种信息。

    • /proc/[PID]/status文件包含了关于指定PID的进程的各种状态信息,包括进程状态、内存使用情况、父进程ID等等;



  • grep PPid :

  • 这是一个grep命令,用于在输入中查找匹配特定模式的文本行。在这个命令中,它用于查找包含PPid的行。在/proc/[PID]/status文件中,PPid一行包含了这个进程的父进程的PID;
    然后,你可以使用以下命令来杀掉父进程:


kill -9 [父进程的PID]

同样,你需要在杀掉父进程之前确定这样做不会影响到系统的其他部分。另外,这个方法并不保证能够杀掉所有的内核态进程。


结论:


在Linux系统中,处理无法被杀死的进程可以是一项挑战,尤其是当你无法确定进程状态或者无法影响父进程的时候。以上的方法并不保证能够解决所有问题。如果你尝试了所有的方法,但问题仍然存在,或者你不确定如何进行,那么你可能需要联系系统管理员,或者寻求专业的技术支持。


总的来说,处理无法被杀死的进程需要对Linux的进程管理有深入的理解,以及足够的耐心和谨慎。希望这篇文章能够帮助你更好地理解这个问题,以及如何解决这个问题。


作者:泽南Zn
来源:juejin.cn/post/7288116632785420303
收起阅读 »

h5调用手机摄像头踩坑

web
1. 背景 一般业务也很少接触摄像头,有也是现成的工具库扫个二维码。难得用一次,记录下踩坑。 2.调用摄像头的方法 2.1. input <!-- 调用相机 --> <input type="file" accept="image/*" ca...
继续阅读 »

1. 背景


一般业务也很少接触摄像头,有也是现成的工具库扫个二维码。难得用一次,记录下踩坑。


2.调用摄像头的方法


2.1. input


<!-- 调用相机 -->
<input type="file" accept="image/*" capture="camera">
<!-- 调用摄像机 -->
<input type="file" accept="video/*" capture="camcorder">
<!-- 调用录音机 -->
<input type="file" accept="audio/*" capture="microphone">

这个就不用多说了,缺点就是没办法自定义界面,它是调用的系统原生相机界面。


2.2. mediaDevices


由于我需要自定义界面,就像下面这样:
image.png


所以我选择了这个方案,这个api使用起来其实很简单:


<!-- 创建一个video标签用来播放摄像头的视屏流 -->
<video id="video" autoplay="autoplay" muted width="200px" height="200px"></video>
<button onclick="getMedia()">开启摄像头</button>

async getMedia() {
// 获取设备媒体的设置,通常就video和audio
const constraints = {
// video配置,具体配置可以看看mdn
video: {
height: 200,
wdith: 200,
},
// 关闭音频
audio: false
};
this.video = document.getElementById("video");
// 使用getUserMedia获取媒体流
// 媒体流赋值给srcObject
this.video.srcObject = await window.navigator.mediaDevices.getUserMedia(constraints);
// 直接播放就行了
this.video.play();
}

image.png
可以看到这个效果。


这个api的配置可以参考MDN


// 截图拍照
takePhoto() {
const video = document.getElementById("video");
// 借助canvas绘制视频的一帧
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext('2d');
ctx.drawImage(this.video, 0, 0, 300, 300);
},
// 停止
stopMedia() {
// 获取媒体流
const stream = this.video.srcObject;
const tracks = stream.getTracks();
// 停止所有轨道
tracks.forEach(function (track) {
track.stop();
})
this.video.srcObject = null;
}

3.坑


如果你复制我的代码,在localhost上肯定能运行,但是你想在手机上试试的时候就会发现很多问题。


3.1. 需要https


由于浏览器的安全设置,除了localhosthttps连接,你都没办法获取到navigator.mediaDevices,打印出来是undefined。如果要在手机上测试,你要么用内网穿透代理一个https,要么部署在https域名的服务器上测试。


3.2. 设置前后摄像头


默认是使用user设备,也就是前摄像头,想要使用后摄像头也是有配置的,


async getMedia() {
// ...
let constraints = {
video: {
height: 200,
wdith: 200,
// environment设备就是后置
facingMode: { exact: "environment" },
},
audio: false
};
// ...
}

3.3. 设置显示区域大小


我的需求是铺满整个设备,所以我想当然的直接把video样式宽高设置成容器大小:


#video {
width: 100%;
height: 100%;
}

async getMedia() {
// ....
// 将宽高设置成容器大小
const pageSize = document.querySelector('.page').getBoundingClientRect()
let constraints = {
video: {
height: pageSize.height,
width: pageSize.width,
facingMode: { exact: "environment" },
},
audio: false
};
//....
}

image.png
发现这个视频横着而且没有铺满屏幕。


通过输出video的信息可以看到,设备返回的视频流宽高是反的:


image.png


所以配置换一下就行了:


    let constraints = {  
video: {
height: pageSize.width,
width: pageSize.height,
},
};

作者:头上有煎饺
来源:juejin.cn/post/7287965561035210771
收起阅读 »

叫声【全栈工程师】,你敢应吗?

上面是我打开百度百科,写着对全栈工程师的解释:是指掌握多种技能,可以胜任前端和后端,能用多种技能独立完成产品的人。 对于这个答案我是保持观望的态度。如果说能同时开发前端和后端,还能独立完成产品,它就是全栈工程师的话,那计算机专业的大学生做完毕业设计之后就都是全...
继续阅读 »

图片


上面是我打开百度百科,写着对全栈工程师的解释:是指掌握多种技能,可以胜任前端和后端,能用多种技能独立完成产品的人。


对于这个答案我是保持观望的态度。如果说能同时开发前端和后端,还能独立完成产品,它就是全栈工程师的话,那计算机专业的大学生做完毕业设计之后就都是全栈了。


对于百科的这个定义,我感觉确实有点宽泛了,于是我就重新编辑了这个百度百科:


图片


小伙伴们可能不知道百度百科是可以随意编辑的,我整整花了一分钟的时间,精心编辑了一个百度百科的概念:全栈工程师是指在web项目开发中,独立掌握web前端、安卓开发、ios开发、后端技术(Java,PHP,Node,关系型数据库,分布式开发等技术)的综合性、高素质软件工程师。 目前为止这个词条的审批还没有通过。


再回到全栈工程师这个称呼上,我第一次听到这个词是在2015年,那时候前后端分离的开发模式刚刚开始被普及。因为2015年之前的web开发项目,前端几乎都是用模板套jQuery来做的。像ember、backbone、angularjs这些框架,小公司几乎用不起来。


但是在2015年这个节点,web项目井喷式地增长,像react这样新兴的轻量级框架,开始走进了中小公司。前后端分离的开发模式也越来越多的被大家使用起来了,在这个技术背景之下,全栈工程师这个词被提的就越来越多,而被称为全栈工程师的人主要分成以下三类。


第一类:


在公司的技术部门独挡一面,被同事称为问题终结者或者是bug收割机,这样的全栈工程师其实也是所有对技术追求的程序员而奋斗的目标,我确实很佩服这样的全栈。


第二类:


主要分布在中小公司,名头是【全栈工程师】,其实是【全干工程师】。小公司为了节约人力成本,前后端就找一个人干,甚至可能一个公司就一个程序员把所有的活都干了。


图片


各种的压榨劳动力,然后还给了一个好的名头(全栈工程师),没事再画画饼,说公司上市之后,你就是技术总监,然后享受各种股份,期权,你就财富自由了。现实情况就是你累倒了,老板财富自由了。


但是在2015年前后那个时候大家还是很吃这一套的,所以很多人愿意天天加班,最后大多数人也是什么也没得到。现在大家看招聘网站上,小公司招聘的全栈工程师基本都是这个套路:


图片


就是说想用更少的钱去招人干更多的活,这工作基本上干起来就是一地鸡毛。


第三类:


相比上面的【全干工程师】,这一类才坑,培训机构。借着【全栈工程师】的这个名号忽悠大学生。比如一个机构,它以前是教java的,后面就加了点前端的课程,又或者以前是教前端的,然后加了点Node课程,就说我们是全栈工程师培训机构。


许多学生们纷纷交钱报班,等毕业了才知道自己学的这个【全栈工程师】只能去小作坊企业,996公司,或者是去一些非软件,非互联网干一些辅助工作。


总结:


真正正规的软件公司或者互联网公司都是专人专岗的,就算它招了全栈工程师,也是高新的技术专家,怎么可能招一个培训班刚毕业,包装2年工作经验的职场新人呢。所以大部分情况下,我确实不太喜欢全栈工程师这个岗位(称呼),因为这个词总是能和【忽悠大学生】,【压榨劳动力】这些联系到一起。


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

天天摸鱼度日,34岁的我被裁了。。。

程序猿的35岁危机 程序猿的35岁危机已经不是个新鲜话题了,不管行内还是行外多少都听过了解过。行外的一听:咦~😒,程序猿狗都不干!、行内的一听笑一笑也就过去了,大部分都是当成个笑点,并未真正思考过(当然也可能是逃避)。 程序员毕竟是有点脑力活的职业,到了3...
继续阅读 »

程序猿的35岁危机



程序猿的35岁危机已经不是个新鲜话题了,不管行内还是行外多少都听过了解过。行外的一听:咦~😒,程序猿狗都不干!、行内的一听笑一笑也就过去了,大部分都是当成个笑点,并未真正思考过(当然也可能是逃避)。


程序员毕竟是有点脑力活的职业,到了35岁基本上就无法在一线开发上熬了,不像拧螺丝,我感觉我60岁拧的应该也不慢哈哈哈。所以如果能预测到35岁升不到管理岗或者未转行,那就要开始思考如果被裁之后能干什么了。



  • 卖煎饼(有贷在身的就别想了,卖不完根本卖不完

  • 短视频等副业(有点搞头,趁早

  • 独立AI应用工程师(有精力才行)

  • ......


我预想这个问题的时候,发现我竟然什么都不会,舒适圈待久了,也没有动力去学新的东西,我意识到这样下去肯定不行,得找点后路。



屌丝现状



我:从小到大都是那种处在中间层的人物,没有做过领头羊,也没有拖过别人的腿。成绩属于中层;身高处于中层(180🤪);家庭属于中层。当然肉夹馍我只吃中间那层。


正是因为都是夹心饼干,让我一直处于舒适圈,既拿不出寒门学子的冲劲,家底也不支持我躺的安静,导致我高不成低不就的状况,没错,就是这款冰红茶,屌丝款极具性价比。


image.png


虽然有点技术追求,也看过写过不少源码,算法题也做了不少,但是就是缺少一鞭子让我跳出那个圈子,刚校招时,信誓旦旦的觉得自己技术牛逼,肯定能进大厂,但是现实总会给你迎头一棒,于是就进了一家中厂,刚开始的时候内心还是满船清梦,心态良好,路漫漫其修远兮,吾将上下而求索。


到了后面,才发现满腔热血早已被各种业务琐事冷却,许多精力都用来应对客户以及各种内部无厘头,加上加班,回到茅屋根本无力再精进功力,慢慢地,曾经想在大厂大显身手的念头也羞于提起,毕业两年未到就想着养老躺平,各种摸鱼技术倒是进步了不少,哈哈哈,这让我想起老父亲常常训我的话:


你学习要有打游戏这份劲,早就上清华啦🤡你学习要有打游戏这份劲,早就上清华啦🤡


回想起来,已经许久未跟家里通过一通电话了,筹划许久带父母国庆去旅行也没有实现,只寄了几个老母亲下不了口的双流老妈麻辣兔头,估计也是得等我回去消灭了。


image.png


我呢,属于是人小穷志不穷,志虽然不穷,但是也不多,有点杞人忧天,喜欢想很远很远的可能存在的隐患,差不多是时态里面的一般过去现在将来时(最近在从头梳理英语哈哈)。想的东西特别多,行动的屈指可数。



  • 35岁危机之前一直在想

  • 健身

  • 博客写作

  • 骑行,吉他 还算过得去

  • ......


以俺目前的情况来看,如果不努力一把进大厂搞点青春血汗钱,大概率是浑浑噩噩,凄凄惨惨戚戚的在中小厂熬到35岁,然后一大把年纪,卷不过小鲜肉,熬不过鸡汤,然后下岗。当然这对于有点咸鱼觉悟的我来说是万万不可能让它发生的,摸鱼我也要可持续性摸鱼!


咱就是说现今这种社会夹心饼的情况,不知道有多少Javaer跟我一样,每天上班💼就是打几行代码,摸摸鱼,像你们一样看看掘金。每天就在掘金找找别人生活失意的文章来抚慰内心的荒芜不安,看到别人过的不咋样,自己也就烂的心安理得了;看到别人凭自己的努力进大厂进外企又心痒痒,打开leetcode,肯触c肯触v,搞定,今天又骗过了自己。


我相信应该有不少人就像我这样,别否认,说的就是你


image.png


内心虽有鸿鹄之志,怎耐无破而后立之决心\color{green}{内心虽有鸿鹄之志,怎耐无破而后立之决心
}


image.png


都怪自己没有穷的叮当响,激发不出意志潜力哈哈哈哈


话说回来,虽然知道这样不好,自欺欺人,结果不会帮着你一起行骗,但是就是少了份冲劲,没有一个小镇做题家该有的决心觉悟,内心又渴望自己能够提升进步,却一而再的给自己留退路找借口。



痛定思痛



为了自己的前途着想(持续摸鱼😀),彻夜冥想,找了三条可以应对危机又不那么累的路。


退路一:考公


考公现在很卷,2022年212.3万人通过了资格审查,实际录用人数只有3.12万,报录比高达68:1,虽然但是,只要我们心态放好,别去选那些几千个人招一个的,趁早考,多考几次,希望还是很大的,最重要的是坚持以及学习方法,再不行还可以考事业编。


退路二:外企


程序员去外企主要是英语能力要跟的上,虽然也可能有危机,但是概率小很多,况且英语能力摆在那,就算去干其他的也不至于入不敷出(关键在于把英语学好,过程重要‼️,最终能不能去外企放倒是其次)


退路三:放弃自我,精神小伙


其实为什么现在很多网红都是学历比较低,因为学历低的人接受教育比较少,没那么多包袱,许多想做的事情不会有心理负担,而高学历的人,很多都不愿意抛头露面,认为那是丢脸的事情。另外高学历的人道德感比较强,不愿意做一些违心的话题,特别是不愿意炒作。


加上如今畸形的社会审美,低俗的短视频反而更能获得注目,优秀的产出只有少数人驻足。


如果我们能放好心态,告诉自己赚钱不寒碜,跟网红们卷起来,高等的教育能让我们经受住网络看客的审察,这也许是优势🖖



总结



像我这样处在中间高不成低不就的程序员,心态放好就是一种幸福,每天打打代码偶尔旅旅游,陪陪父母,不会大富大贵也不会食不果腹,摆烂会不安,努力又泡汤,我想这应该就是属于我的道。


最后:如果你已经为自己选好了退路,就全力以赴,不要每天想起来就学一点,造成退路已经妥了的假象。我希望你可以两手抓,而不是说你在备考公务员,你觉得你有退路了,目前的工作就不重视,态度散漫,到时还没开始考试你就失业了。




这是我在掘金的第一篇文章,主要是记录下自己的所思所想,当然如果这篇文章能引起读者们的一些思考,那也是掘掘子啊👍🏽,哈哈哈哈。




但使龙城飞将在,不教胡马度阴山


作者:lvresse
来源:juejin.cn/post/7287788617916448802
收起阅读 »

开源框架 NanUI 作者转行卖钢材,项目暂停开发

NanUI 作者在国庆节发布了停更公告,称该项目将暂停开发,原因是去年被裁员失业后,他已转行销售钢材,现在很难腾出时间来开发和维护 NanUI 项目。他说道:为了生存,本人只能花费更多的时间和精力去谈单,去销售,去收款,因此已经很难再腾出时间来开发和维护 Na...
继续阅读 »

NanUI 作者在国庆节发布了停更公告,称该项目将暂停开发,原因是去年被裁员失业后,他已转行销售钢材,现在很难腾出时间来开发和维护 NanUI 项目。

他说道:

为了生存,本人只能花费更多的时间和精力去谈单,去销售,去收款,因此已经很难再腾出时间来开发和维护 NanUI 项目,对此我深感无奈,也希望后面生活和工作稳定后能腾出时间来继续维护 NanUI。

NanUI 作者表示,他所在公司因疫情于去年(2022 年)初彻底宣布裁减所有开发岗位,因此他也只能顺应大流在 36 岁这个尴尬的年纪失业。


via https://github.com/XuanchenLin/NanUI/discussions/367

NanUI 界面组件是一个开放源代码的 .NET/.NET Core 窗体应用程序(WinForms)界面框架。它适用于希望使用 HTML5/CSS3 等前端技术来构建 Windows 窗体应用程序用户界面的 .NET 开发人员。

NanUI 基于谷歌可嵌入的浏览器框架 Chromium Embedded Framework (CEF),因此用户可以使用各种前端技术 HTML5/CSS3/JavaScript 和流行前端框架 React/Vue/Angular/Blazor 设计和开发 .NET 桌面应用程序的用户界面。

同时,NanUI 独创的 JavaScript Bridge 可以方便地实现浏览器端与 .NET 之间的通信和数据交换。

使用 NanUI 界面框架将为传统的 WinForm 应用程序的用户界面设计和开发工作带来无限种可能!

作者:oschina
来源:www.oschina.net/news/261033

收起阅读 »

东野圭吾:我的人生就像在白夜里走路。

东野圭吾在他的小说《白夜行》中写道:“我的人生就像在白夜里走路。” 这个比喻意味着主人公的生活充满了复杂、模糊和充满挑战的情境。 无尽的白昼与黑夜的迷雾 在北极圈的白夜中,太阳在天空中持续存在,使白昼看似无尽。然而,这并不意味着光明总是清晰可见。黑夜的迷雾可以...
继续阅读 »

塞尔维亚,麦田里小麦的特写.jpg


东野圭吾在他的小说《白夜行》中写道:“我的人生就像在白夜里走路。” 这个比喻意味着主人公的生活充满了复杂、模糊和充满挑战的情境。


无尽的白昼与黑夜的迷雾


在北极圈的白夜中,太阳在天空中持续存在,使白昼看似无尽。然而,这并不意味着光明总是清晰可见。黑夜的迷雾可以让人看不清事物的真相,正如生活中的挑战和不确定性常常让我们感到困惑。


想象一个人在职场中努力工作,但却不确定自己是否能获得升职的机会。尽管工作充满了希望和努力,但不确定性就像白夜中的迷雾一样,感到困惑和不安。


不断前行的决心


白夜行走的比喻也表达了主人公面对人生困难时的坚韧和决心。尽管光明可能模糊不清,但主人公依然坚持前行,不放弃。


与小说中的主人公一样,许多人在面临困境时也表现出坚定的决心。一个创业者可能会面对种种困难,但他仍然坚持前进,努力实现自己的梦想,这就像白夜行走一样,充满了挑战但仍然坚定不移。


坚韧、勇气和希望


东野圭吾的隐喻提醒我们,生活中的挑战和困难虽然充满不确定性,但我们可以通过坚韧、勇气和希望来克服它们,就像在白夜中行走一样,坚持不懈,永不放弃。


无法逃避的内心挣扎


生活中的白夜可以理解为内心的无法抽离的挣扎与矛盾。人们往往在自己的内心与欲望之间产生冲突,这是不可避免的。这一内心挣扎如同白夜里无法躲避的光线,照亮了我们内心的深处。一个人可能在事业与家庭之间感到分裂,不知道如何平衡,这种内心挣扎让他感到仿佛在白夜中走路,无法找到前进的方向。


生活中的无尽选择


白夜中走路也可以看作是生活中的无尽选择。在现代社会,人们面临着诸多选择,这些选择需要我们思考、决策和承担责任。每一个选择都可能影响我们的人生道路,就像白夜中的每一步都可能改变我们的方向。人们需要在无数个可能性中寻找自己的道路,这种选择的过程就像在白夜中摸索前进。


不确定性与未知的未来


白夜行走也表现了生活的不确定性和未知性。无论我们做多少计划,未来仍然充满了变数和未知的因素。就像在白夜中,我们无法预测下一步会有什么,生活中的未来同样充满了谜团。这种不确定性让人们感到焦虑和无助,需要在不确定的环境中前行。


生活中的很多成功故事都展示了人们在困境中坚持不懈,战胜了巨大的挑战。这些人的故事启示我们,尽管人生可能充满了白夜的迷雾,但通过坚强的意志和积极的态度,我们可以找到前行的道路,并最终达到成功的彼岸。


作者:晒晒心里话
来源:juejin.cn/post/7286127842421047332
收起阅读 »

如何在10分钟内让Android应用大小减少 60%?

一个APP的包之所以大,主要包括一下文件 代码 lib so本地库 资源文件(图片,音频,字体等) 瘦身就主要瘦这些。 一、打包的時候刪除不用的代码 buildTypes {        debug {            ...        ...
继续阅读 »

一个APP的包之所以大,主要包括一下文件



  • 代码

  • lib

  • so本地库

  • 资源文件(图片,音频,字体等)


瘦身就主要瘦这些。


一、打包的時候刪除不用的代码


buildTypes {
       debug {
           ...
           shrinkResources true // 是否去除无效的资源文件(如果你的Debug也需要瘦身)
      }
       release {
           ...
           shrinkResources true // 是否去除无效的资源文件
      }
  }

二、减少不必要的打包


defaultConfig {
   ...
   //打包的语言类型(语种的翻译)
   resConfigs "en", "de", "fr", "it"
   //打包的文件夹
   resConfigs "nodpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"
}

或者


android {
 ...
 splits {
   density {
     enable true
     exclude "ldpi", "tvdpi", "xxxhdpi"
     compatibleScreens 'small', 'normal', 'large', 'xlarge'

     //reset()
     //include 'x86', 'armeabi-v7a', 'mips'
     //universalApk true
  }
}

三、lib


尽量不用太复杂的lib,轻量级lib是首选。如果你的应用没用到兼容库,可以考虑去掉support包。


四、资源文件


我们可以通过Lint工具找到没有使用的资源(在Android Studio的“Analyze”菜单中选择“Inspect Code…”)


五、把现有图片转换为webP


我们可以通过 智图 或者isparta将其它格式的图片转换成webP格式,isparta可实现批量转换。


五、图片相关



  • 在Android 5.0及以上的版本可以通过tintcolor实现只提供一张按钮的图片,在程序中实现按钮反选效果,前提是图片的内容一样,只是正反选按钮的颜色不一样。


Drawable.setColorFilter( 0xffff0000, Mode.MULTIPLY )


  • 在Android 5.0及以上的版本,可以使用VectorDrawable和SVG图片来替换原有图片


六、混淆


1 构建多个版本



  • 在gradle中的buildTypes中增加不同的构建类型,使用applicationSuffixversionNameSuffix可以生成多个版本在同一设备上运行

  • 创建src/[buildType]/res/设置不同的ic_launcher以区别不同版本


2 混淆参数


{ 
   debug { minifyEnabled false }
   release {
     signingConfig signingConfigs.release
     minifyEnabled true
     proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  }
}

minifyEnabled true



  • 是否要启用通过 ProGuard 实现的代码压缩(true启用)

  • 请注意,代码压缩会拖慢构建速度,因此您应该尽可能避免在调试构建中使用。 :Android Studio 会在使用Instant Run时停用 ProGuard。


proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'



  • getDefaultProguardFile(‘proguard-android.txt')方法可从 Android SDKtools/proguard/文件夹获取默认 ProGuard 设置。

  • 提示: 要想做进一步的代码压缩,可尝试使用位于同一位置的proguard-android-optimize.txt文件。它包括相同的 ProGuard 规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和帮助提高其运行速度。

  • proguard-rules.pro文件用于添加自定义 ProGuard 规则。默认情况下,该文件位于模块根目录(build.gradle文件旁)。

  • 要添加更多各构建变体专用的 ProGuard 规则,请在相应的productFlavor代码块中再添加一个proguardFiles属性。例如,以下 Gradle 文件会向flavor2产品风味添加flavor2-rules.pro。现在flavor2使用所有三个 ProGuard 规则,因为还应用了来自release代码块的规则。

  • 每次构建时 ProGuard 都会输出下列文件 dump.txt 说明 APK 中所有类文件的内部结构。mapping.txt:提供原始与混淆过的类、方法和字段名称之间的转换。seeds.txt:列出未进行混淆的类和成员。usage.txt:列出从 APK 移除的代码。这些文件保存在/build/outputs/mapping/release/

  • 要修正错误并强制 ProGuard 保留特定代码,请在 ProGuard 配置文件中添加一行-keep代码。例如: -keeppublicclassMyClass

  • 您还可以向您想保留的代码添加[@Keep] (developer.android.com/reference/a…)注解。在类上添加@Keep可原样保留整个类。在方法或字段上添加它可完整保留方法/字段(及其名称)以及类名称。请注意,只有在使用注解支持库时,才能使用此注解。

  • 在使用-keep选项时,有许多事项需要考虑;如需了解有关自定义配置文件的详细信息,请阅读ProGuard 手册问题排查一章概述了您可能会在混淆代码时遇到的其他常见问题。

  • 请注意,您每次使用 ProGuard 创建发布构建时都会覆盖mapping.txt文件,因此您每次发布新版本时都必须小心地保存一个副本。通过为每个发布构建保留一个mapping.txt文件副本,您就可以在用户提交的已混淆堆叠追踪来自旧版本应用时对问题进行调试。

  • 在每次添加库的时候,需要及时进行make a release build

  • DexGuard时Proguard同一个团队开发的软件, 优化代码,分离dex文件从而解决65k方法限制的文件


关于proguard-android.txt文件:


-dontusemixedcaseclassnames: 表示混淆时不使用大小写混淆类名。 -dontskipnonpubliclibraryclasses:不跳过library中的非public方法。 -verbose: 打印混淆的详细信息。 -dontoptimize: 不进行优化,优化可能会造成一些潜在风险,不能保证在所有版本的Dalvik上都正常运行。 -dontpreverify: 不进行预校验。 -keepattributes Annotation :对注解参数进行保留。 -keep public class com.google.vending.licensing.ILicensingService -keep public class com.android.vending.licensing.ILicensingService: 表示不混淆上述声明的两个类。


proguard中一共有三组六个keep关键字的含义


keep  保留类和类中的成员,防止它们被混淆或移除。
keepnames 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。
keepclassmembers  只保留类中的成员,防止它们被混淆或移除。
keepclassmembernames  只保留类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。
keepclasseswithmembers  保留类和类中的成员,防止它们被混淆或移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。
keepclasseswithmembernames  保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。

keepclasseswithmember和keep关键字的区别: 如果这个类没有native的方法,那么这个类会被混淆


-keepclasseswithmember class * {
   native <methods>;
}

不管这个类有没有native的方法,那么这个类不会被混淆


-keep class * {
   native <methods>;
}



另外、 你可以使用 APK Analyser 分解你的 APK


Android Studio 提供了一个有用的工具:APK Analyser。APK Analyser 将会拆解你的应用并让你知道 .apk 文件中的那个部分占据了大量空间。让我们看一下 Anti-Theft 在没有经过优化之前的截图。


img


从 Apk Analyser 的输出来看,应用的原大小是 3.1MB。经过 Play 商店的压缩,大致是 2.5MB。


从截图中可以看出主要有 3 个文件夹占据了应用的大多数空间。



classes.dex —— 这是 dex 文件,包含了所有会运行在你的 DVM 或 ART 里的字节码文件。 res —— 这个文件夹包含了所有在 res 文件夹下的文件。大部分情况下它包含所有图片,图标和源文件,菜单文件和布局。



img



resources.arsc —— 这个文件包含了所有 value 资源。这个文件包含了你 value 目录下的所有数据。包括 strings、dimensions、styles、intergers、ids 等等。



img


你有两个默认的混淆文件。


proguard-android-optimize.txt proguard-android.txt 就像文件名写的那样,“proguard-android-optimize.txt”是更积极的混淆选项。我们将这个作为默认的混淆配置。你可以在 /app 目录下的 proguard-rules.pro 里添加自定义的混淆配置。


 release {
//Enable the proguard
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), "proguard-rules.pro"

//Other parameters
debuggable false
jniDebuggable false
renderscriptDebuggable false
signingConfig playStoreConfig //Add your own signing config
pseudoLocalesEnabled false
zipAlignEnabled true
}

通过设置 minifyEnabled 为 true,混淆将会移除所有未使用的方法、指令以减小 classes.dex 文件。


这是启用了 minify 之后的 APK。


七、AndroidStudio使用lint清除无用的资源文件 在使用AndroidStudio进行App开发的时候,我们经常会在项目中引用多种资源文件,包括图片,布局文件,常量引用定义。随着项目版本开发的迭代,每一期的资源会有变动必定会留下一些无用的资源这个时候我们手动去一个一个寻找效率就会很低下。这个时候我们就要学会AndroidStudio使用lint清除无用的资源文件。



  • 打开AndroidStudio在项目中,点击最上方的菜单栏Analyze -> Run Inspection by Name 如下图:


img



  • 点击 Run Inspection by Name会弹出一个对话框。在对话框里面输入unused resource 如下图:


img



  • 然后点击下拉列表中的unused resource。 之后会弹出一个对话框如下图


img


结尾


好啦,如此文章到这里就结束了,希望这篇文章能够帮到正在看的你们,能够解决Android小伙伴们应用内存问题~


更多Android进阶指南 可以详细Vx关注公众号:Android老皮 解锁               《Android十大板块文档》


1.Android车载应用开发系统学习指南(附项目实战)


2.Android Framework学习指南,助力成为系统级开发高手


3.2023最新Android中高级面试题汇总+解析,告别零offer


4.企业级Android音视频开发学习路线+项目实战(附源码)


5.Android Jetpack从入门到精通,构建高质量UI界面


6.Flutter技术解析与实战,跨平台首要之选


7.Kotlin从入门到实战,全方面提升架构基础


8.高级Android插件化与组件化(含实战教程和源码)


9.Android 性能优化实战+360°全方面性能调优


10.Android零基础入门到精通,高手进阶之路


敲代码不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔


作者:花海blog
来源:juejin.cn/post/7287473826060763197
收起阅读 »

实现转盘抽奖功能

web
1、实现转盘数据动态配置(可通过接口获取) 2、背景色通过分隔配置 3、转动速度慢慢减速,最后停留在每一项的中间,下一次开始从本次开始 4、当动画停止后在对应事件中自定义生成中奖提示。 5、本次中奖概率随机生成,也可自定义配置 实现代码 html <te...
继续阅读 »

1、实现转盘数据动态配置(可通过接口获取)


2、背景色通过分隔配置


3、转动速度慢慢减速,最后停留在每一项的中间,下一次开始从本次开始


4、当动画停止后在对应事件中自定义生成中奖提示。


5、本次中奖概率随机生成,也可自定义配置


实现代码


html


<template>
<div class="graph-page">
<div class="plate-wrapper" :style="`${bgColor};`">
<div class="item-plate" :style="plateCss(index)" v-for="(item, index) in plateList" :key="index" >
<img :src="item.pic" alt="">
<p>{{item.name}}</p>
</div>
</div>
<div @click="handleClick" class="btn"></div>
</div>
</template>


css


<style lang="less" scoped>
.graph-page {
width: 540px;
height: 540px;
margin: 100px auto;
position: relative;
}
.plate-wrapper {
width: 100%;
height: 100%;
border-radius: 50%;
border: 10px solid #98d3fc;
overflow: hidden;
}
.item-plate {
position: absolute;
left: 0;
right: 0;
top: -10px;
margin: auto;
}
.item-plate img {
width: 30%;
height: 20%;
margin: 40px auto 10px;
display: block;
}
.item-plate p {
color: #fff;
font-size: 12px;
text-align: center;
line-height: 20px;
}
.btn {
width: 160px;
height: 160px;
background: url('https://www.jq22.com/demo/jquerylocal201912122316/img/btn_lottery.png') no-repeat center / 100% 100%;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
cursor: pointer;
}
.btn::before {
content: "";
width: 41px;
height: 39px;
background: url('https://www.jq22.com/demo/jquerylocal201912122316/img/icon_point.png') no-repeat center / 100% 100%;
position: absolute;
left: 0;
right: 0;
top: -33px;
margin: auto;
}
</style>


js


其中背景色采用间隔配置,扇形背景采用锥形渐变函数conic-gradient可实现。


每个转项的宽度和高度可参照以下图片,所有奖品的div都定位在圆心以上,根据圆心转动,所以旋转点为底部中心,即:transform-origin: 50% 100%;


可采用监听transitionend事件判断动画是否结束,可自定义中奖提示。


lADPJwKt5iekh_DNA1bNBJI_1170_854.jpg_720x720g.jpg


<script>
export default {
data() {
return {
plateList: [],
isRunning: false, //判断是否正在转动
rotateAngle: 0, //转盘每项角度
baseRunAngle: 360 * 5, //总共转动角度,至少5圈
totalRunAngle: 0, //要旋转的总角度
activeIndex: 0, //中奖index
wrapDom: null //转盘dom
}
},
computed: {
bgColor(){ //转盘的每项背景
let len = this.plateList.length
let color = ['#5352b3', '#363589']
let colorVal = ''
this.plateList && this.plateList.forEach((item, index)=>{
colorVal += `${color[index % 2]} ${(360/len)*index}deg ${(360/len)*(index+1)}deg,`
})
return `background: conic-gradient(${colorVal.slice(0, -1)})`
},
plateCss(){ //转盘的每项样式
if(this.plateList && this.plateList.length){
return (i) => {
return `
width: ${Math.floor(2 * 270 * Math.sin(this.rotateAngle / 2 * Math.PI / 180))}px;
height: 270px;
transform: rotate(${this.rotateAngle * i + this.rotateAngle / 2}deg);
transform-origin: 50% 100%;
`

}
}
return ()=>{''}
},
},
created(){
this.plateList= [
{ name: '手机', pic: 'https://bkimg.cdn.bcebos.com/pic/3801213fb80e7bec54e7d237ad7eae389b504ec23d9e' },
{ name: '手表', pic: 'https://img1.baidu.com/it/u=2631716577,1296460670&fm=253&fmt=auto&app=120&f=JPEG' },
{ name: '苹果', pic: 'https://img2.baidu.com/it/u=2611478896,137965957&fm=253&fmt=auto&app=138&f=JPEG' },
{ name: '棒棒糖', pic: 'https://img2.baidu.com/it/u=576980037,1655121105&fm=253&fmt=auto&app=138&f=PNG' },
{ name: '娃娃', pic: 'https://img2.baidu.com/it/u=4075390137,3967712457&fm=253&fmt=auto&app=138&f=PNG' },
{ name: '木马', pic: 'https://img1.baidu.com/it/u=2434318933,2727681086&fm=253&fmt=auto&app=120&f=JPEG' },
{ name: '德芙', pic: 'https://img0.baidu.com/it/u=1378564582,2397555841&fm=253&fmt=auto&app=120&f=JPEG' },
{ name: '玫瑰', pic: 'https://img1.baidu.com/it/u=1125656938,422247900&fm=253&fmt=auto&app=120&f=JPEG' }
]
this.rotateAngle = 360 / this.plateList.length
this.totalRunAngle = this.baseRunAngle + 360 - this.activeIndex * this.rotateAngle - this.rotateAngle / 2
},
mounted(){
this.$nextTick(()=>{
this.wrapDom = document.getElementsByClassName('plate-wrapper')[0]
})
},
beforeDestroy(){
this.wrapDom.removeEventListener('transitionend', this.stopRun)
},
methods:{
handleClick(){
if(this.isRunning) return
this.isRunning = true
const ind = Math.floor(Math.random() * this.plateList.length)//通过随机数返回奖品编号
this.activeIndex = ind
this.startRun()
},
startRun(){
// 设置动画
this.wrapDom.setAttribute('style', `
${this.bgColor};
transform: rotate(${this.totalRunAngle}deg);
transition: all 4s ease;
`
)
this.wrapDom.addEventListener('transitionend', this.stopRun) // 监听transition动画停止事件
},
stopRun(){
this.isRunning = false
this.wrapDom.setAttribute('style', `
${this.bgColor};
transform: rotate(${this.totalRunAngle - this.baseRunAngle}deg);
`
)
}
}
}
</script>

参考来源:juejin.cn/post/718031…


作者:李某某的学习生活
来源:juejin.cn/post/7287125076369801279
收起阅读 »

听说你会架构设计?来,弄一个公交&地铁乘车系统

1. 引言 1.1 上班通勤的日常 “叮铃铃”,“叮铃铃”,早上七八点,你还在温暖的被窝里和闹钟“斗智斗勇”。 突然,你意识到已经快迟到了,于是像个闪电侠一样冲进卫生间,速洗漱,急穿衣,左手抄起手机,右手拿起面包,边穿衣边啃早餐。 这个时候,通勤的老难题又摆...
继续阅读 »

1. 引言


1.1 上班通勤的日常


“叮铃铃”,“叮铃铃”,早上七八点,你还在温暖的被窝里和闹钟“斗智斗勇”。



突然,你意识到已经快迟到了,于是像个闪电侠一样冲进卫生间,速洗漱,急穿衣,左手抄起手机,右手拿起面包,边穿衣边啃早餐。


这个时候,通勤的老难题又摆在了你面前:要不要吃完这口面包、刷牙和洗脸,还是先冲出门赶车?


好不容易做出了一个艰难的决定——放下面包,快步冲出门。你拿出手机,点开了熟悉的地铁乘车 App 或公交地铁乘车码小程序。


然后,一张二维码在屏幕上亮了起来,这可是你每天通勤的“敲门砖”。





你快步走到地铁站,将手机二维码扫描在闸机上,"嗖"的一声,闸机打开,你轻松通过,不再需要排队买票,不再被早高峰的拥挤闹心。


你走进地铁车厢,挤到了一个角落,拿出手机,开始计划一天的工作。


1.2 公交&地铁乘车系统


正如上文所说,人们只需要一台手机,一个二维码就可以完成上班通勤的所有事项。


那这个便捷的公交或地铁乘车系统是如何设计的呢?它背后的技术和架构是怎样支撑着你我每天的通勤生活呢?


今天让我们一起揭开这个现代都市打工人通勤小能手的面纱,深入探讨乘车系统的设计与实现


在这个文章中,小❤将带你走进乘车系统的世界,一探究竟,看看它是如何在短短几年内从科幻电影中走出来,成为我们日常生活不可或缺的一部分。


2. 需求设计


2.1 功能需求





  • 用户注册和登录: 用户可以通过手机应用或小程序注册账号,并使用账号登录系统。




  • 路线查询: 用户可以查询地铁的线路和站点信息,包括发车时间、车票价格等。




  • 获取乘车二维码: 系统根据用户的信息生成乘车二维码。




  • 获取地铁实时位置: 用户可以查询地铁的实时位置,并查看地铁离当前站台还有多久到达。




  • 乘车扫描和自动支付: 用户在入站和出站时通过扫描二维码来完成乘车,系统根据乘车里程自动计算费用并进行支付。




  • 交易记录查询: 用户可以查询自己的交易历史记录,包括乘车时间、金额、线路等信息。




2.2 乘车系统的非功能需求


乘车系统的用户量非常大,据《中国主要城市通勤检测报告-2023》数据显示,一线城市每天乘公交&地铁上班的的人数普遍超过千万,平均通勤时间在 45-60 分钟,并集中在早高峰和晚高峰时段。


所以,设计一个热点数据分布非均匀、人群分布非均匀的乘车系统时,需要考虑如下几点:




  • 用户分布不均匀,一线城市的乘车系统用户,超出普通城市几个数量级。




  • 时间分布不均匀,乘车系统的设计初衷是方便上下班通勤,所以早晚高峰的用户数会高出其它时间段几个数量级。




  • 高并发: 考虑到公交车/地铁系统可能同时有大量的用户在高峰时段使用,系统需要具备高并发处理能力。




  • 高性能: 为了提供快速的查询和支付服务,系统需要具备高性能,响应时间应尽可能短。




  • 可扩展性: 随着用户数量的增加,系统应该容易扩展,以满足未来的需求。




  • 可用性: 系统需要保证24/7的可用性,随时提供服务。




  • 安全和隐私保护: 系统需要确保用户数据的安全和隐私,包括支付信息和个人信息的保护。




3. 概要设计


3.1 核心组件





  • 前端应用: 开发手机 App 和小程序,提供用户注册、登录、查询等功能。




  • 后端服务: 设计后端服务,包括用户管理、路线查询、二维码管理、订单处理、支付系统等。




  • 数据库: 使用关系型数据库 MySQL 集群存储用户信息、路线信息、交易记录等数据。




  • 推送系统: 将乘车后的支付结果,通过在线和离线两种方式推送给用户手机上。




  • 负载均衡和消息队列: 考虑使用负载均衡和消息队列技术来提高系统性能。




3.2 乘车流程


1)用户手机与后台系统的交互


交互时序图如下:



1. 用户注册和登录: 用户首先需要在手机应用上注册并登录系统,提供个人信息,包括用户名、手机号码、支付方式等。


2. 查询乘车信息: 用户可以使用手机应用查询公交车/地铁的路线和票价信息,用户可以根据自己的出行需求选择合适的线路。


3. 生成乘车二维码: 用户登录后,系统会生成一个用于乘车的二维码,这个二维码可以在用户手机上随时查看。这个二维码是城市公交系统的通用乘车二维码,同时该码关联到用户的账户和付款方式,用户可以随时使用它乘坐任何一辆公交车或地铁。


2)用户手机与公交车的交互


交互 UML 状态图如下:





  1. 用户进站扫码: 当用户进入地铁站时,他们将手机上的乘车码扫描在进站设备上。这个设备将扫描到的乘车码发送给后台系统。




  2. 进站数据处理: 后台系统接收到进站信息后,会验证乘车码的有效性,检查用户是否有进站记录,并记录下进站的时间和地点。




  3. 用户出站扫码: 用户在乘车结束后,将手机上的乘车码扫描在出站设备上。




  4. 出站数据处理: 后台系统接收到出站信息后,会验证乘车码的有效性,检查用户是否有对应的进站记录,并记录下出站的时间和地点。




3)后台系统的处理




  1. 乘车费用计算: 基于用户的进站和出站地点以及乘车规则,后台系统计算乘车费用。这个费用可以根据不同的城市和运营商有所不同。




  2. 费用记录和扣款: 系统记录下乘车费用,并从用户的付款方式(例如,支付宝或微信钱包)中扣除费用。




  3. 乘车记录存储: 所有的乘车记录,包括进站、出站、费用等信息,被存储在乘车记录表中,以便用户查看和服务提供商进行结算。




  4. 通知用户: 如果有需要,系统可以向用户发送通知,告知他们的乘车费用已被扣除。




  5. 数据库交互: 在整个过程中,系统需要与数据库交互来存储和检索用户信息、乘车记录、费用信息等数据。




3. 详细设计


3.1 数据库设计



  • 用户信息表(User) ,包括用户ID、手机号、密码、支付方式、创建时间等。

  • 二维码表 (QRCode) ,包括二维码ID、用户ID、城市ID、生成时间、有效期及二维码数据等。

  • 车辆&地铁车次表 (Vehicle) ,包括车辆ID、车牌或地铁列车号、车型(公交、地铁)、扫描设备序列号等。

  • 乘车记录表 (TripRecord) ,包括记录ID、用户ID、车辆ID、上下车时间、起止站点等。

  • 支付记录表 (PaymentRecord) ,包括支付ID、乘车记录ID、交易时间、交易金额、支付方式、支付状态等。


以上是一些在公交车&地铁乘车系统中需要设计的数据库表及其字段的基本信息,后续可根据具体需求和系统规模,还可以进一步优化表结构和字段设计,以满足性能和扩展性要求。


详细设计除了要设计出表结构以外,我们还针对两个核心问题进行讨论:



  • 最短路线查询




  • 乘车二维码管理




3.2 最短路线查询


根据交通部门给的公交&地铁路线,我们可以绘制如下站点图:



假设图中的站点有 A-F,涉及到的交通工具有地铁 1 号线和 2 路公交,用户的起点和终点分别为 A、F 点。我们可以使用 Dijkstra 算法来求两点之间的最短路径,具体步骤为:


步骤已遍历集合未遍历集合
1选入A,此时最短路径 A->A = 0,再以 A 为中间点,开始寻找下一个邻近节点{B、C、D、E、F},其中与 A 相邻的节点有 B 和 C,AB=6,AC=3。接下来,选取较短的路径节点 C 开始遍历
2选取C,A->C=3,此时已遍历集合为{A、C},以 A 和 C 为中间点,开始寻找下一个邻近节点{B、D、E、F},其中与 A、C 相邻的节点有 B 和 D,AB=6,ACD=3+4=7。接下来,选取较短的路径节点 B 开始遍历
3选取B,A->B=6,此时已遍历集合为{A、C、B},A 相邻的节点已经遍历结束,开始寻找和 B、C 相近的节点{D、E、F},其中与 B、C 相邻的节点有 D,节点 D 在之前已经有了一个距离记录(7),现在新的可选路径是 ABD=6+5=11。显然第一个路径更短,于是将 D 的最近距离 7 加入到集合中
4选取D,A->D=7,此时已遍历集合为{A、C、B、D},寻找 D 相邻的节点{E、F},其中 DE=2,DF=3,选取最近路径的节点 E 加入集合
5选取 E,A->E=7+2=9,此时已遍历集合为{A、C、B、D、E},继续寻找 D 和 E 相近的节点{F},其中 DF=3,DEF=2+5=7,于是F的最近距离为7+3=10.
6选取F,A->F=10,此时遍历集合为{A、C、B、D、E、F}所有节点已遍历结束,从 A 点出发,它们的最近距离分别为{A=0,C=3,B=6,D=7,E=9,F=10}

在用户查询路线之前,交通部门会把公交 & 地铁的站点经纬度信息输入到路线管理系统,并根据二维的空间经纬度编码存储对应的站点信息。


我们设定西经为负,南纬为负,所以地球上的经度范围就是[-180, 180],纬度范围就是[-90,90]。如果以本初子午线、赤道为界,地球可以分成 4 个部分。



根据这个原理,我们可以先将二维的空间经纬度编码成一个字符串,来唯一标识用户或站点的位置信息。再通过 Redis 的 GeoHash 算法,来获取用户出发点附近的所有站点信息。


GeoHash 算法的原理是将一个位置的经纬度换算成地址编码字符串,表示在某个矩形区域,通过这个算法可以快速找到同一个区域的所有站点


一旦获得了起始地点的经纬度,系统就可以根据附近的站点信息,调用路线管理系统来查找最佳的公交或地铁路线。


一旦用户选择了一条路线,导航引擎启动并提供实时导航指引。导航引擎可能会使用地图数据和 GPS 定位来指导用户前往起止站点。


3.3 乘车二维码管理


乘车码是通过 QR 码(Quick Response Code)技术生成的,它比传统的 Bar Code 条形码能存更多的信息,也能表示更多的数据类型,如图所示:



二维码的生成非常简单,拿 Go 语言来举例,只需引入一个三方库:


import "github.com/skip2/go-qrcode"

func main() {
    qr,err:=qrcode.New("https://mp.weixin.qq.com",qrcode.Medium)
if err != nil {
    log.Fatal(err)
else {
    qr.BackgroundColor = color.RGBA{50,205,50,255//定义背景色
    qr.ForegroundColor = color.White //定义前景色
    qr.WriteFile(256,"./wechatgzh_qrcode.png"//转成图片保存
    }
}

以下是该功能用户和系统之间的交互、二维码信息存储、以及高并发请求处理的详细说明:



  1. 用户与系统交互: 用户首先在手机 App 上登录,系统会验证用户的身份和付款方式。一旦验证成功,系统根据用户的身份信息和付款方式,动态生成一个 QR 码,这个 QR 码包含了用户的标识信息和相关的乘车参数。

  2. 二维码信息存储: 生成的二维码信息需要在后台进行存储和关联。通常,这些信息会存储在一个专门的数据库表中,该表包含以下字段:



    • 二维码ID:主键ID,唯一标识一个二维码。

    • 用户ID:与乘车码关联的用户唯一标识。

    • 二维码数据:QR码的内容,包括用户信息和乘车参数。

    • 生成时间:二维码生成的时间戳,用于后续的验证和管理。

    • 有效期限:二维码的有效期,通常会设置一个时间限制,以保证安全性。



  3. 高并发请求处理: 在高并发情况下,大量的用户会同时生成和扫描二维码,因此需要一些策略来处理这些请求:



    • 负载均衡: 后台系统可以采用负载均衡技术,将请求分散到多个服务器上,以分担服务器的负载。

    • 缓存优化: 二维码的生成是相对耗时的操作,可以采用 Redis 来缓存已生成的二维码,避免重复生成。

    • 限制频率: 为了防止滥用,可以限制每个用户生成二维码的频率,例如,每分钟只允许生成 5  次,这可以通过限流的方式来实现。




总之,通过 QR 码技术生成乘车码,后台系统需要具备高并发处理的能力,包括负载均衡、缓存和频率限制等策略,以确保用户能够快速获得有效的乘车二维码。


同时,二维码信息需要被安全地存储和管理,比如:加密存储以保护用户的隐私和付款信息。



不清楚如何限流的,可以看我之前的这篇文章:若我问到高可用,阁下又该如何应对呢?



4. 乘车系统的发展


4.1 其它设计


除此之外,公交车或地铁的定位和到站时间计算可能还涉及定位设备、GPS 系统、NoSQL 数据库、用户 TCP 连接管理系统等核心组件,并通过实时数据采集、位置处理、到站时间计算和信息推送等流程来为用户提供准确的乘车信息。


同时,自动支付也是为了方便用户的重要功能,可以通过与第三方支付平台的集成来实现。


4.2 未来发展


公交车/地铁乘车系统的未来发展可以包括以下方向:



  • 智能化乘车: 引入智能设备,如人脸自动识别乘客、人脸扣款等。

  • 大数据分析: 利用大数据技术分析乘车数据,提供更好的服务。


在设计和发展过程中,也要不断考虑用户体验、性能和安全,确保系统能够满足不断增长的需求。


由于篇幅有限,文章就到此结束了。


希望读者们能对公交&地铁乘车系统的设计有更深入的了解,并和小❤一起期待未来更多的交通创新解决方案叭~


作者:xin猿意码
来源:juejin.cn/post/7287495466514055202
收起阅读 »

少一点功利主义,多一点傻逼似的坚持

感谢你观看本文,希望在未来的时光中,我们都能找到真正的自己,做真正的自己 坚持只需要一个理由,而放弃则有无数个接口,坚持很难,而放弃就是一刹那的时间,作为普通人的我们,其实只要能坚持做一件事,那么其实是很了不起的,可能它暂时不能给你带来经济价值,但是经过时间的...
继续阅读 »

感谢你观看本文,希望在未来的时光中,我们都能找到真正的自己,做真正的自己


坚持只需要一个理由,而放弃则有无数个接口,坚持很难,而放弃就是一刹那的时间,作为普通人的我们,其实只要能坚持做一件事,那么其实是很了不起的,可能它暂时不能给你带来经济价值,但是经过时间的酝酿,它会迸发处惊人的力量!


不过有一关是很难过的,这一关基本上可以刷掉百分之九十五的人,那就是否有长期主义,是否能够忍受“没有回报”,因为人的本性就是贪婪,而我们从小受到的教育就是“付出就有收获”,所以我们在做每一件事的时候,心里第一反应是我做这件事能给我带来多少收获。


比如读一本书,其实很多时候我们都是带有目的性的,比如觉得事业不顺,人生失意,或者想赚快钱,那么这时候就会去快速翻阅一些诸如《快速致富》的书籍,然后加满鸡血后,第二天依旧是十二点起,起来又卷入精神内耗中,反反复复,最终宝贵是时光!


又比如你看到别人赚到了钱,于是眼睛一红,就问他怎么赚的,别人稍微指点后,你就暗下决心要搞钱,前几天到几个月期间赚了几块钱,你就失落了,你在想,这条路子行不通,于是就放弃了,又去折腾其它的了。


上述的例子是百分之九十的人的真实写照,那么我觉得可以总结为两点:


1.只要没有得到应有的回报,就觉得是损失


2.极强的功利主义


首先对于这一点,我觉得是我们最容易犯的错,比如当一个人说你去坚持做这件事情,一个月会有一千的附加收入,你去做了,而实际上只拿到了50元的收入,这时候你就会极度的不平衡,感到愤怒,你会觉得花了这么多时间才得到50元,老子不干了,实际上你在这个过程中学到的东西远比1000块多,不过你不会觉得,这时候你宁愿去刷短视频,追剧,你也不会去做这件事了。


所以当你心中满是“付出多少就应该得到多少回报”的时候,你不可能做好事,也不会得到更好的回报,因为你心中总是在想“会不会0回报”,“这玩意究竟靠谱不靠谱”,克服这种心态是一件十分难的事情!


第二点,我觉得我们应该少一点功利主义,多一点傻逼似的坚持,这说得有点理想主义了,人本质就是贪婪的,如果赚不到钱,我就不做,对我没好处,我也不会做,我有写文章的习惯其实从大学就开始了,以前没发公众号,之前朋友经常说我,你写的有什么卵用?能赚钱吗?有人看吗?


一开始我还会在乎,在问自己,你干嘛写这些,因为写个人的感悟和生活这种文章确实会有一定的心里压力,朋友说:”你自己都是这个鸟样,有什么资格去给别人说教“,不过随着时间的推移,我不再去在乎这些了。


就单拿写文章这件事来说,虽然没赚到钱,不过在这个过程中,我逐渐不再浮躁,能静下心来写,也结实了朋友,这是一种对自己的总结,对技术的总结,也是一种锻炼,虽然现在文笔依然很差,不过我依然会像一个傻逼一样去坚持。


时间是最奇妙的东西,你的一些坚持一定会在相应的时间点迸发处惊人的力量!


回头想一下,你没写文章,没看书,没学习,没出去看世界,而是拿着个手机躺在床上刷短视频,像个清朝抽鸦片的人一样,那么你又收获了多少呢?


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

转全栈之路,会比想象中的艰难

背景 我于22年校招入职字节安全方向大前端部门,支持公司安全Tob产品的前端开发工作。今年8月,因为组织架构调整,很多同事都直接划入了业务部门,我也和另一名北京的同事互换了业务,划入业务部门。 在新部门工作2-3个月,因为种种原因,工作体验上的差别大到像是换了...
继续阅读 »

背景


我于22年校招入职字节安全方向大前端部门,支持公司安全Tob产品的前端开发工作。今年8月,因为组织架构调整,很多同事都直接划入了业务部门,我也和另一名北京的同事互换了业务,划入业务部门。


在新部门工作2-3个月,因为种种原因,工作体验上的差别大到像是换了一家公司,也很想记录一下到底有什么不同。


大前端部门业务部门
组织人数近30人,纯前端方向近40人,分为不同方向,前端背景1人
工作模式由于同事都在天南海北,需要通过视频会议进行沟通纯下线沟通,所有同事都base深圳
沟通效率较低,每次沟通都需要调试设备,共享屏幕等,并且见不到面很多信息会失真高,直接面谈,肢体语言这些信息不会丢失
工作节奏有排期压力,有承诺客户交付时间。如果排期不合理会很疲惫。没有排期压力,前端工作量相比之前轻松
设计资源有专门的UED团队出图,前端不需要思考如何进行交互,这部分工作由设计师承担无设计资源,交互的好坏完全取决于研发的审美水平与自我要求
前端技术建设每个季度会有横向建设,有组件库共建等机会,前端技术相对先进部门内部无前端建设,依赖公司基建与之前经验
同事组成深圳base全员年轻化,校招生为主,因为年龄相同且技术方向相同,天然就有很多话题资深员工多,校招生占比很低,且划分不同方向,一般自己方向的人自己内部沟通较多
和+1的关系base不同,沟通频率很低。因为主要是做业务方的需求,沟通内容主要在支持工作的进展上。base相同,沟通频率比以前高5-10倍,除同步开发进展,还会针对产品迭代方向,用户体验等问题进行沟通
技术成长受限于部门性质以及绩效评价体系,员工需要在前端技术领域保持专业且高效,但工作一定年限后有挑战性的业务需求不足,容易遇到职业发展瓶颈。因为前端人数多,所以存在横向建设的空间,可以共建组件库等基建,非常自然的会接触这些需求。角色划分不明确,前后端可以相互支援彼此,大家摘掉前后端的标签,回归通用开发的角色。技术成长依赖自驱力与公司技术水平。研发人少,没有内部的横向建设机会。

纠结


为什么要转全栈?究竟有什么收益?我会在心里时不时问自己这个问题。任何一门技能,从入门到精通,都需要很多时间的学习与实践,在初期都会经历一段相当痛苦的时光。除了学习不轻松,能否创造出更大的价值也是一个问号。


但这次转全栈,有天时地利人和的作用,总结下来就是:



  1. Leader支持:和Leader沟通过,Leader觉得在我们团队多做多学对个人,对团队都有益处,欢迎我大胆尝试

  2. 后端同学支持:我们团队的细分项目多,后端工作饱和,可以分一个相对独立的活给我

  3. 全栈化背景:原先的大前端部门已经有部分前端转为全栈开发职能,部门层面鼓励全栈化发展

  4. 需求清晰:有些开发之所以忙碌是因为开会和对齐耗时太多。但是我目前拿到的prd都非常清晰,拿到就能直接开发,对齐扯皮的时间几乎不计,我只需要完成开发工作即可。这节约了我大量时间成本。想到之前经常是一天开个1-2小时会,搞得很疲惫。

  5. 工作熟练:从实习开始算起,我已经有2年多的开发经验,可以在预期时间内完成需求开发和bugfix,因此安全的预留时间精力转全栈。


其实不仅仅是我,和很多做前端的同事、朋友也都聊过,其实内心各有各的纠结。基本上大家的内心想法就是想在有限的条件下学习后端,并在有限的条件下承担一部分后端开发。


想学后端的原因:



  1. 纯属好奇心,想研究一下后端技术栈

  2. 前端作为最终的执行角色,话语权低

  3. 业务参与度低,可以半游离业务的存在,较边缘化。未来如果希望成长为管理,难以做业务管理,只能做技术管理,想象空间天花板就是成为管理一批前端的技术管理。

  4. 工作遇到天花板,想多了解一下其他的内容


想在有限条件下学习后端的原因:



  1. 工作比较忙碌,没那么多时间学习

  2. 学习一门技能要算ROI,学太多了如果既不能升职也不能加薪就没有意义

  3. 不确定市场对于全栈人才的反应,不想all in


想承担一部分后端开发的原因:



  1. 学习任何一门技能只有理论没有实践遗忘速度最快,马上就会回归到学习之前

  2. 掌握后端技能但没有企业级实战经验,说服力很弱


不想学习后端的原因:



  1. 国内市场上的全栈岗位数量稀少,如果后端岗位有10个,前端岗位有3个,那么可能就只有1个全栈岗位

  2. 普通前后端开发薪酬基本上没有区别,未来谁更好晋升在当前的经济背景也难说

  3. 大概率前端依然是自己的职业发展主线,学多一门技能可能会分摊本可以提升前端能力的时间精力

  4. 做舒适圈里面的事情很舒服,谁知道多做会不会有好处


我就是在这种纠结中一路学过来,从8月开始,痛苦且挣扎,不过到目前为止还可以接受。学到现在甚至已经有点麻木。但我也确实不知道继续在前端领域还能专精什么技能,现有的业务没有那么大的挑战性让我快速成长,所以想跳脱出来看看更大的世界。


学习路线


曲线学习


如果说做前端开发我是如鱼得水,那做后端开发就是经常呛到水。


记得我刚开始做前端实习的时候,真心感到前端知识好像黑洞,永远也学不完。由此非常佩服之前的同事,怎么把这些代码写出来的,有些代码后面一看写的还不错,甚至可能会感觉脊背发凉,是自己太弱还是自己太强?


在实习的时候,我的学习曲线可以说是一个向外扩散的圆。比如我第一次接触webpack的时候,根本不了解这是什么工具,之前一直在用jQuery写项目,所有的js都是明文写好,然后通过script引入到html中。所以一开始我会去查这个webpack到底是什么内容,但脑海中对他的印象还是非常模糊。接着我又因为webpack了解到了babel,css-loader这些概念,又去学习。又发现这需要利用到node,又去学习了《深入浅出node.js》。再后来又了解到了sourcemap等概念。直到正式加入字节半年后,我自己配了一次webpack,并且阅读了他的源码。进行了debug,进行了一次webpack插件开发的分享,才有信心说自己是真的弄明白了。不过这个弄明白,也仅限于排查bug,配项目,进行plugin和loader的开发,如果遇到更难的领域,那又将解锁一块黑洞。


怎么学


学习后端,要学的内容也一点都不少,作为新人会遇到非常多的问题。



  1. 怎么学 - 是死皮赖脸的逮住后端同学使劲问,还是多自己研究研究?遇到所有同事都不会的问题怎么处理?

  2. 学到什么程度 - 究竟要学到怎样的程度才能进入项目开发,而不犯下一些非常愚蠢的问题呢?

  3. 学习顺序 - 最简单的办法就是去看项目,看到不懂的分析一下这是什么模块的,看看要不要系统性的去了解。


我比较喜欢一开始就系统性的学,学完后再查缺补漏,再开启第二轮学习。


比如Go,官网就有很详细的文档,但未必适合新人去学。我跟着官网学了一阵子之后跑b站找视频学习了。然后又Google了一些资料,大致讲了一下反射、切片的原理,以及一些错误用法。学习Go大概用了2-3周。刚学完直接去看项目还是会觉得非常不适应,需要不断的让自己去阅读项目代码,找到Go的那种感觉。


然后需要学习很多公司内部的基建



  • 微服务架构 - 公司内部所有服务都是微服务架构,需要了解服务发现、服务治理、观测、鉴权这些基本概念以及大致的原理。为了在本地开发环境使用微服务,还需要在本地安装doas,用来获取psm的token。

  • RDS - 公司内的项目分为了各种环境,非常复杂。可以自己先创建一个MySQL服务自测,看看公司的云平台提供了哪些能力。

  • Redis - 大致了解即可,简单用不难

  • RPC - 微服务通过RPC传递,RPC协议通过IDL来定义接口传输格式,像字节会在api管理平台做封装。你定义好的IDL可以直接生成一个gopkg上传到内部镜像,然后其他用户直接go get这个库就能调用你的服务。但如果你是node服务,就可以在本地通过字节云基建的工具库自动生成代码。

  • Gorm - 所有的MySQL最终如果通过go程序调用,都需要经过gorm的封装,来避免一些安全问题。同时也可以规避一些低级错误。还需要了解gen怎么使用,将MySQL库的定义自动生成为orm代码。


还要好好学习一下MySQL的用法,这边花了一周看完了《MySQL必知必会》,然后去leetcode刷题。国庆节刷了大概80道MySQL的题目,很爽。从简单的查询,到连接、子查询、分组、聚合,再到比较复杂的窗口函数、CTE全刷了个遍,刷上瘾了。


接着就可以去看项目代码了,这一部分还是蛮折腾的,对于新人来说。本身阅读别人的代码,对于很多开发者来说就是一件痛苦的事情,何况是去阅读自己不熟悉的语言的别人的代码。


我最近接手的一个半废弃项目,就很离谱。开发者有的已经离职了,提交记录是三四年前的。PRD也找不全,到今天权限还没拿齐,明天再找人问问。这边可能是真的上下文就是要丢失的,没法找了。只能自己创建一个新的文档,把相关重点补充一下。


明天找一下这个项目的用户,演示一下怎么使用,然后根据对用法的理解进行开发……


收获


新鲜感


一直写前端真的有点腻,虽然现在技术还在迭代,但也万变不离其宗。而且真的是有点过分内卷了,像一个打包工具从webpack -> esbuild -> vite -> turbopack -> rspack。不可否认的是这些开发者的努力,为前端生态的繁荣做出了贡献。但对于很多业务来说,其实并没有太大的性能问题,对于这部分项目来说升级的收益很小。比如云服务的控制台,基本都是微前端架构,每个前端项目都非常小,就算用webpack热更新也不会慢。而且webpack使用下来是最稳定的,我现在的项目用的是vite,会存在样式引入顺序的问题,导致开发环境和生产环境的页面区别。


后端技术栈不管好还是不好,反正对我来说是很新鲜的。虽然我之前Python、Go也都用过,也用Python写出了完整的项目,但论企业级开发这算第一次~各方面都更正规


Go写起来很舒服,虽然写同样的需求代码量比TypeScript多一堆……习惯之后还是可以感受到Go的简单与安心。Go打包就没那么多事,你本地怎么跑服务器那边就怎么跑,不像前端可能碰到一堆兼容性问题。


真的有学到


我前几个月买了掘金大佬神说要有光的小课《Nest 通关秘籍》,据我了解我的几个同事也买了。不过我没坚持下来,因为工作上实在是没有使用到Nest的机会。我无法接受学了两三个月却无法在工作里做出产出的感觉。


但这一次学了可以立马去用,可以在工作中得到检验,可以接受用户的检验。我就会得到价值感与成就感。


而且字节的Go基建在我认知里很牛叉,一家以Go为主的大厂,养得起很多做基建的人。比如张金柱Gorm的作者,竟然就在字节,我前几天知道感觉牛人竟然……


Go的学习资料也非常多,还有很多实战的,真的像突然打开了新世界的大门~


与业务更近,以及更平和的心态


如果我没有学后端,会在“前端已死”的氛围里胡思乱想,忽略了前端的业务价值,前端依旧是很重要的岗位。让后端来写前端不是不行,但只有分工才能达到最高的效率。对于一个正常的业务团队来说,也完全没必要让后端去硬写前端,好几个后端配一个前端,也不是什么事。


就我目前的工作经验来看,后端可以和业务的使用者更近的对接。我们这里的后端开发会和非常多用户对接需求,了解他们的真实使用场景,思考他们背后的需求,可能还能弥补一下产品思考上的不周。和用户对齐数据传递、转换、存储、查询、以及需要不需要定时任务等等,这些后端会去负责。


而前端负责最终的交互,基本可以不用碰到使用者,基本上只需要根据后端给的接口文档,调用接口把内容渲染在表格上即可。碰到用户提反馈一般在于,加载慢(往往是数据请求很慢,但是用户会觉得是前端的问题)、交互不满意(交互美不美真的是一个很难量化的问题,按理说这属于UI的绩效)、数据请求失败(前后端接口对齐虽然体验越来越好,但是开发阶段经常改动还是免不了,最后导致前后端没有同步)。


之前开周会的时候,我基本上说不上什么话。一个是刚转岗,确实不熟。另一个是前端半游离于业务的状态,单纯的把接口内容渲染出来也很难有什么思考,导致开会比较尴尬。基本是后端在谈解决了什么oncall,解决了什么技术问题,有什么业务建设的思考等等。


这次看了别人代码之后非常期盼未来能独立owner一个方向,享受闭环一个小功能的乐趣。


职业安全感


我学的这项技能能够立马投入到工作中进行自我检验,因此我相信自己学的是“有效技能”。我理解的无效技能指学了用不上,然后忘了,花了很多时间精力最后不升职不加薪。之前看李运华大佬的网课《大厂晋升指南》里面有提到,有人花了半年把编译原理这个看似非常重要的计算机基础课学的很扎实,但因为业务不需要,不产生业务价值,也不可能获得提拔的机会。


其实内部全栈化我的理解,还有一个原因,那就是灵活调度。现在这个背景下,老板更希望用有限的人力去做更多事情。有些业务前端过剩了,但是缺后端,这个时候如果直接去招后端,一方面增加成本,再就是没有解决剩的前端,反之也是。在盘点hc的时候就容易出现调整。


多学一些有效技能,提高解决问题的深度、广度,让自己更值钱。我想不管是什么职能,最终都要回归到为业务服务的目标上。


End


写到这里,我依旧在转全栈的路上,只是想给自己一个阶段性的答案。


脱离舒适圈,进入拉伸区,需要付出,需要勇气,也需要把握机遇。给自己多一种可能,去做,去挑战自己不会的。我相信他山之石可以攻玉,越往深处走,就越能触类旁通。


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

聊聊2023年怎么入局小游戏赛道?

web
一、微信小游戏赛道发展史 第一阶段:轻度试水期,2017~2019年 微信小游戏于2017年底上线,初期以轻度休闲为主,例如棋牌、合成消除以及益智相关游戏类型。一是开发门槛不高,产品可以快速上线; 二是大部分厂商并无计划投入过多资金,仅试水。在变现方式上,极大...
继续阅读 »

一、微信小游戏赛道发展史


第一阶段:轻度试水期,2017~2019年


微信小游戏于2017年底上线,初期以轻度休闲为主,例如棋牌、合成消除以及益智相关游戏类型。一是开发门槛不高,产品可以快速上线;


二是大部分厂商并无计划投入过多资金,仅试水。在变现方式上,极大部分以IAA为主。


第二阶段:官方孵化期,2019~2021年


2019年官方推出“游戏优选计划",为符合标准的产品提供全生命周期服务,包括前期产品的立项和调优,以及后期的增长、变现等。


20050414514227.jpg


出现了一批《三国全明星》、《房东模拟器》、《乌冬的旅店》等这样的精品游戏。


第三阶段:快速爆发期,2022年至今


在官方鼓励精品化下,手游大厂开始进入,产品逐渐开始偏向中重度化。三国、仙侠、神话、西游以及传奇等传统中重度游戏占比逐渐加大。


全流量拓展投放,库存近百亿。腾讯全域流量、字节系、快手、百度、B站等基本全部渠道均可进行买量,真正进入前所未有的爆发期!


WechatIMG3428.jpg


二、该赛道持续高增长的原因


1、小游戏的链路相比于APP更加顺畅,无需下载,点击即玩。游戏买量中用户损失最大的部分就是“点击-下载-激活"。而在小游戏的链路中,用户可一键拉起微信直达小游戏登录页面,无需等待,导流效率极高。

2、微信生态提供的统一的实名认证底层能力。

3、小游戏链路可以绕开IOS的IDFA获取率不足的问题,实现IOS平台的高效精准归因。

4、各大游戏开发引擎特别是unity对小游戏平台的优化和适配能力提升。

5、顺畅的微信支付链路。

6、高效开放的社交裂变自然流量来源和社群运营能力。


三、小游戏和app游戏的买量核心差异


1、买量技术框架


小游戏在多数广告平台的技术链路都是从H5改进而来的。APP付费游戏在安卓常见的SDK数据上报在小游戏链路因为无法进行分包而彻底被API上报所取代。


API上报不同于SDK上报,有着成熟且空间巨大的广告主自主策略优化玩法。


2、买量目标


小游戏买首次付费、每次付费的比例要高于买 ROI。这一点和APP游戏也有明显不同,小游戏品类分散,人群宽泛且行业刚起步缺乏历史数据,对广告系统来说ROI买量的达成难度要高,效果相对不稳定。


3、素材打法


APP游戏大盘消耗以重度为主,素材中洗重度用户的元素较多;小游戏则是轻中度玩法为主,素材多面向泛人群,更注重对真实核心玩法的表现。


四、广告买量为什么在小游戏赛道中很重要


1、买量红利巨大,再好的产品都要靠买量宣发


微信小游戏链路在转化率上有着明显的优势。这会让小游戏产品在相同的前端出价上,要比同类型的APP产品买量竞争力更强。


而小游戏的研发成本并不算高,一旦跑出一款好产品,跟进者众多。在产品同质化比较严重时,快速买量拿量能力就决定了产品和业务的规模,除非大家有信心做出一款不怕被抄的爆品中的精品。


2、技术能力及格不难,做好很难


小游戏的买量技术相关的问题,如果只想将就用,可能一两个研发简单做一个月就能做到跑通。


但是如果想把买量技术能力做完善,这里依然有很大的门槛,而且会成为拉开规模差距的核心能力之一。这里我们给出几个细节,篇幅原因不具体展开。


归因方式


不同于APP生态已经比较成熟统一的设备ID和IP+ UA模糊匹配,小游戏链路因为微信生态、平台能力和开发成本不同,在不同平台存在多种归因方式,主要有平台自采集,启动参数,监测匹配等。


有效触点


因为小游戏不用去应用商店或者落地页下载,因而看广告但是不直接点击,而是选择去微信自己搜索启动的流量占比要高一些。为了适应这一情况,有些媒体平台会选择在小游戏链路将之前 APP的默认有效触点前置到播放或者视频浏览上。这里会让监测归因方式需要处理的数据提升两个数量级,对归因触点识别的难度也会加大。


效果衡量


因为支付通道的原因,腾讯系的平台和小游戏后台都只能收集安卓的付费数据,不能收集ios的数据,导致IAP类型的产品追踪完整ROI需要自建中台或者采买三方,打通归因和付费埋点数据。


数据口径


因为数据 采集来源不同,时间统计口径不同,小游戏链路下数据分析对运营和投放人员有着较高的要求,需要科学成熟的数据分析工具作为辅助。


3、渠道分散且需要掌控节奏


因为小游戏更为顺畅的用户链路,导致其转化率要比APP链路更高。因此小游戏在一些腰部平台甚至尾部平台都能有很好的跑量能力。APP游戏很多规模不大的产品可能只需要在巨量、腾讯和快手进行买量,现在小游戏完全可以尝试在百度信息流、B站、微博甚至是知乎等平台进行买量。


除了大家熟知的流量平台以外,长尾流量渠道往往是很多小游戏能闷声发财的致胜法宝。比如:陌陌、番茄、书旗等具有大量用户流量的非主流流量平台,一方面这些流量渠道取决于发行商的商务能力,另一方面也需要具备相应的技术能力。以业内新晋的小游戏发行技术 FinClip 来说,以嵌入SDK的方式,就可以让任何APP流量平台具备运行微信小游戏的能力。这意味着,小游戏在平台无需做任何跳转,用户转化链路降到最短。当然,腰尾部流量平台对小游戏在落地页资产、微信数据授权、链路技术支持等方面都还不是完全成熟,还属于比较小众的渠道方式。


小游戏发行领域,达人营销和自然裂变也是重要的渠道手段。通过合适的技术手段,达人营销和裂变也可以做到精准的效果追踪和按效果付费。


五、怎么入局小游戏赛道?


小游戏=变现方式游戏品质玩法受众裂变运营买量能力


以IAP或者混合变现的形式入局成功率会更大一些。


游戏品质主要和研发成本正相关:



  • 50万成本以下的小游戏往往因为玩法过于休闲,长线留存天花板低,美术品质不够,同质化竞争过于严重等原因导致很难获得预期的规模。

  • 200万成本以上的游戏又会因为试错成本太高,研发周期过长,不够紧跟市场热点等原因不被看好。

  • 因此,一般推荐50万到200万的成本,通过自有产研团队从APP转型,或者与稳定合作CP定制的方式获取第一款试水的产品。


具体的玩法和受众:



  • 一些在APP赛道被验证的轻中度的合成、抽卡和挂机类玩法都是在小游戏领域被广泛看好证的。

  • 在APP受限于受众规模小和付费渗透率低的小众玩法,如女性向Q萌养成,解密等玩法都有着亮眼的表现。

  • 整个小游戏的生态从开发者侧也更偏向于中长尾,多种垂直品类共存发展的趋势。


小游戏有着顺畅的玩家加群和裂变分享路径:



  • 持续运营私域群流量可以显著拉升核心用户的留存活跃,配合节日礼包等活动也可以提升付费率。

  • 小游戏无需应用商店下载,也不会有H5官网下载被微信拦截的情况,配合一些魔性和话题性的分享引导,很容易在已有一定用户规模的前提下实现比APP更快的自然增长,让用户规模更上一层。


作者:Finbird
来源:juejin.cn/post/7287494827701682176
收起阅读 »

也谈一下 30+ 程序员的出路

前言 前两天和一个前端同学聊天,他说不准备再做前端了,准备去考公。不过难度也很大。 从 2015 2016 年那会儿开始互联网行业爆发,到现在有 7、8 年了,当年 20 多岁的小伙子们,现在也都 30+ 了 大量的人面临这个问题:大龄程序员就业竞争力差,未...
继续阅读 »

前言


前两天和一个前端同学聊天,他说不准备再做前端了,准备去考公。不过难度也很大。


3.png


从 2015 2016 年那会儿开始互联网行业爆发,到现在有 7、8 年了,当年 20 多岁的小伙子们,现在也都 30+ 了


大量的人面临这个问题:大龄程序员就业竞争力差,未来该如何安身立命?


先说我个人的看法:



  • 除非你有其他更好的资源,否则没有更好的出路

  • 认真搞技术,保持技术能力,你大概率不会失业(至少外包还在招人,外包也不少挣...)


考公之我见


如果真的上岸了,极大概率不会失业,这是最大的优势。


有优势肯定也有劣势,要考虑全面。凡事都符合能量守恒定律。


你得到什么,你就得付出什么。或者你爸爸、爷爷提前付出为你过了,或者你儿子、孙子到最后为你买单。


任何一个企业、单位,无论什么形式,无论效率高低,总是需要人干活的,甚至有很多脏活累活。


你有依靠当然好。但你如果孤零零的进去,这些活你猜会是谁干?


什么,努力就一定能有收获?—— 对,肯定有收货。但收件人不一定是谁。(也符合能量守恒定律)


转岗,转什么?


去干产品经理,那不跟程序员一样吗?只是不写代码了而已。文档,不一定就比代码好写。


努力晋升转管理岗,那得看公司有没有坑。当下环境中,公司业务不增长的话,也不可能多出管理岗位。


其他没啥可转的岗位了,总不能转岗做 HR 吧~ 木讷的程序员也干不了 HR 。


副业,红利期早已过去


做自媒体,做讲师,红利期早就过去了。我去年开始在某音上做小视频,到现在也就积累不到 2000 粉丝,播放量非常少。


接外包,这得看你本事了。这不单单是一个技术活,你这是一个人干了一个公司所有角色的活:推广、需求、解决方案、开发、测试、部署、维护、升级…


不过,虽然现在副业情况不好,但我还是建议大家,在业余时候多输出技术内容(博客、视频、开源等),看能否积累一些流量和粉丝。以后说不定副业情况会好起来,到时候你临时抱佛脚可来不及。


回归二线城市


相比于一线城市的互联网公司,二线城市对于年龄的容忍度更高一些。我认识很多 35-40 岁的人,在二线城市做开发工作也非常稳定。


在二线城市最好能找一个传统行业的软件公司,如做医疗,财务,税务,制造业等软件产品的。这种软件的特点是,不要求有多么高精尖的技术,也不要求什么大数据、极致性能,它对业务流程和功能的依赖更多一些。你只要能尽快把业务功能熟悉起来(挺多专业知识,不是那么容易的),你在公司就基本稳定了,不用去卷技术。


二线城市是非常适合安家定居的。房价便宜,生活节奏慢 —— 当然,工资也会相对低一些。


另外,回归二线城市也不是说走就走的,你得提前准备、规划,把路铺好。


总结


当前互联网、软件行业,已经没有了前些年的增量,但依然有大量的存量,依然需要大量技术人员去维护当前的系统和功能。


所以别总想着去转行(除非有其他好的资源),其他行业也不会留着好位子等着你。有那个精力多给自己充充电,有竞争力是不会失业的。只要互联网和软件行业还存在,就一直需要前端工作。


作者:前端双越老师
来源:juejin.cn/post/7287020579831267362
收起阅读 »