注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

我是埋点SDK,看我如何让甲方爸爸的页面卡顿10s+

web
背景音: Sir,收到線報啦,今日喺生產環境用戶訪問網頁嘅時候,竟然感受到咁卡卡地!完全冇得爽啊!已經導致唔少用戶投訴。根據推斷,昨日更新咗埋點SDK... 昨日,一位前端程序员在优化公司的埋点SDK使用方式后,出了一些小插曲。不知道是什么原因,更新之后就...
继续阅读 »

背景音:



Sir,收到線報啦,今日喺生產環境用戶訪問網頁嘅時候,竟然感受到咁卡卡地!完全冇得爽啊!已經導致唔少用戶投訴。根據推斷,昨日更新咗埋點SDK...



昨日,一位前端程序员在优化公司的埋点SDK使用方式后,出了一些小插曲。不知道是什么原因,更新之后就开始有用户反馈说网页卡卡地,走得比蜗牛还慢。


六点二十分,第一个用户提交了投诉工单,但这只是个开始。


今天早上九点十分,公司的运维团队已经接到了一大堆反馈工单,许多用户都遭受到了同样的问题。这是一个巨大的问题,一旦得不到解决,可能导致数万的用户受到影响。运维人员立即开始了排查工作,想要找出问题所在。


经过一个小时的紧急排查,他们终于想到了昨日的这名前端程序员,一经沟通发现是SDK版本更新引起的问题。在新的版本中,有一些不稳定的代码导致了性能问题。


然而,这不仅仅是个技术问题,因为接下来,他们要开始着手写事故报告,准备给上层领导交代。


接下来,进入正题:


一、问题排查定位


根据更新的版本体量,可以缩小和快速定位问题源于新引入埋点SDK



  1. 打开 开发者工具-性能分析,开始记录

  2. 刷新页面,重现问题

  3. 停止记录,排查分析性能问题


性能分析


如上图,按照耗时排序,可以快速定位找到对应的代码问题。


首先把编译压缩后的代码整理一下,接下来,深入代码一探究竟。


代码耗时.png


⏸️暂停一下,不妨猜猜看这里是为了干嘛?


🍵喝口茶,让我们沿着事件路径,反向继续摸清它的意图吧。


image.png


这里列举了231个字体名称,调用上文的 detect() 来分析。


⏸️暂停一下,那么这个操作为什么会耗时且阻塞页面渲染呢?


...


休息一下,让我们去看看这段代码的来龙去脉。


上面我们大概猜到代码是用于获取用户浏览器字体,那就简单检索一下 js get browser font


搜索结果.png


代码示例.png


证据确凿,错在对岸。


二、解决问题


相信大家也看出来了,我不是埋点SDK,我也不是甲方爸爸,我只能是一位前端开发。


联系反馈至SDK方,需要走工单,流程,而这一切要多少时间?


我唔知啊!领导也不接受啊!


👐没办法,只能自己缝补缝补了。


那么如何解决呢?



  1. 尝试修复 getFonts detect 字体检测逻辑,避免多次重绘。

  2. 缩短待检测字体目录。


人生苦短,我选方案3,直接修改返回值,跳过检测


getFonts () { return 'custom_font' }

那么让我们继续搬砖吧。



  1. 寻根


image.png


首先找到 SDK 加载对应 JS 的加载方式,看看能不能动点手脚。
这里可以看到,是采用很常见的 通过 appendScript loadJs 的方案,那么就可以复写拦截一下 appendChild 函数。



  1. 正源


通过拦截 appendChild,将SDK加载的JS改为加载修复后的JS文件。


核心代码如下:


var tempCAppend = document.head.appendChild
document.head.appendChild = function (t) {
if (t.tagName === 'SCRIPT' && t.src.includes('xxx.js')) {
t.src = 'custom_fix_xxx.js'
}
return tempCAppend.bind(this)(t)
}

三、后续


这件事情发生在21年底,今天为什么拿出来分享一下呢?


近期排查 qiankun 部分子应用路由加载异常的时候,定位到与 document.head.appendChild 被复写有关,于是去看SDK方是否修复,结果纹丝未动....


结合近期境遇,不得不感慨,业务能不能活下去,真的和代码、技术什么的毫无关系。


其他


❄️下雪了,简单看了几眼文心一言的发布会,更凉了。


作者:夏无凉风冬有雪
来源:juejin.cn/post/7211020974023868475
收起阅读 »

这几个群,程序员可千万不要进!

震惊!某摸鱼网站惊现肾结石俱乐部! (图源V2EX) 无关地域、无关性别,各位程序员们在肾结石这个病上面有着出奇一致的反应。诸如此类的各种职业病在我们的生活中更是十分常见。 也可能是到年纪了,在办公室里放眼望去,一群处于亚健康状态的同事们格外显眼:手上戴着护...
继续阅读 »

震惊!某摸鱼网站惊现肾结石俱乐部!
在这里插入图片描述


(图源V2EX)


无关地域、无关性别,各位程序员们在肾结石这个病上面有着出奇一致的反应。诸如此类的各种职业病在我们的生活中更是十分常见。


也可能是到年纪了,在办公室里放眼望去,一群处于亚健康状态的同事们格外显眼:手上戴着护腕的,脖子上贴着膏药的,闲着没事锤两下后背的,甚至还有站着办公时不时嘶哈两声的……


悄悄问了那位站着办公的同事,结果平白遭受了一个白眼:你见过花季少女割痔疮的吗?


没有,不过现在见到了。


不愧是万物生长的季节,那些困扰程序员们的各类职业病也开始逐渐冒头,像“肾结石俱乐部”这样的群,程序员们也许建了成百上千个,不信往下看看?


1、腱鞘炎相亲相爱一家人


在这里插入图片描述


专家建议


腱鞘炎对程序员群体来说已成为一种常见疾病,虽然不会对我们的生命构成威胁,但都或多或少地影响了我们的生活质量:


手部运动不灵活,手腕无力太软弱。
疼痛一来闹心窝,让人意乱难工作。


想知道自己有没有腱鞘炎,分享给大家一个小方法:
在这里插入图片描述


腱鞘炎保养可以从这几个方面入手:


休息:保证手腕足够的休息时间,比如每30分钟活动下手腕,避免过多接触冷水。
热敷:温热水泡手,或是热水袋热敷,每天2次,每次30~45分钟。
按摩:对侧拇指沿手指掌面按摩,按摩范围包括整个手掌指背以及手指的掌侧。


如果腱鞘炎比较严重,需要配合一些药物治疗、局部封闭注射治疗或手术治疗,具体需咨询医生。对于还没有腱鞘炎的小伙伴,以下几个手部操送给你,开启腱鞘炎预防之旅吧!


在这里插入图片描述


2、颈椎患者大本营


在这里插入图片描述


专家建议


其实随着与手机、电脑相处的时间越来越长,颈椎病的发病人群也逐渐年轻化。当我们在低头玩手机时,脊椎正在承受原本无法承受之重。


举个例子:一个人的头部重约5kg,当我们前倾看手机或低头时,颈部的负重就会大大增加。
在这里插入图片描述


看到这里,请大家立刻挺胸抬头并阅读、实践以下内容,减轻颈椎负担!
在这里插入图片描述


3、干眼症娱乐部


在这里插入图片描述


说起干眼症,大多数程序员都不陌生。引起干眼症的原因比较多,比如过劳致“干”、药物致“干”环境致“干”等等……干眼症很难根治,只能靠日常保养加以改善,从这个意义上讲,干眼症确实可以称为“绝症”。来听听专家的建议吧!


专家建议


1)调位置


电脑显示器的位置和字体大小,让眼睛距离屏幕50-60厘米,屏幕中心在视线水平或稍向下10-15厘米。
在这里插入图片描述


2)多眨眼


多眨眼可以放松睫状肌,滋润眼球。建议每分钟眨眼次数 >15-20次。


3)少看屏幕,多远眺


用电脑或看手机都要注意时间,避免长时间盯着屏幕。参考20原则:用眼20分钟可以盯着20米以外的地方,休息至少20秒。


4)其他小窍门



  • 使用蒸汽眼罩或热毛巾热敷,每天1-2次,每次5-10分钟;

  • 在医生的建议下使用人工泪液;

  • 佩戴隐形眼镜每天应控制在8小时以内,不超过12小时;

  • 使用空气加湿器,让空气湿度适宜;

  • 增加户外运动。


不说了,眼睛发干,需要摸会儿鱼休息一下(误)~


4、防脱治疗所


身边的程序员们说:一个程序员一生能写的程序行数是有限的,写一行代码就会掉一根头发,直到青丝落尽,就是这个程序员隐退江湖的时候。


当然,这只是玩笑话。引起脱发的原因可能是遗传、压力大、熬夜通宵、 频繁烫染或饮食不均衡等。怎么确定自己的史密斯夫妇还有约翰逊家族们安全,两个步骤测试一下:


在这里插入图片描述


保护我们的史密斯夫妇还有约翰逊家族们有这三个妙招:


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


这些我们眼中的“小病小灾”就像技术债务,短期内选择了忽视,累计到一定程度就会变成身体里的“定时炸弹”。所以,当我们开始长时间的久坐、低头盯手机,当我们压力变大,习惯于加班、熬夜、饮食不规律……更需要停下来想想。



在非洲有个部落
人如果连续赶路三天
就一定要停下来休息一天
因为人们害怕灵魂跟不上自己的脚步
……
——阮靖《风会记得你走过的路》



如果一直忙着赶路,也要记得停下休息,别让健康跟不上我们的脚步。


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

“ChatGPT 们” 所需算力真是“贵滴夸张”!

先抛几个数据: 当下,每天有超过 2 亿的人在疯狂地抛出各式各样的问题请求 ChatGPT 回复 如果要完成这 2 亿+ 的咨询量,初始投入需要 3 万+ 块英伟达 A100 GPU 来计算 而 A100 是当下这个星球拥有最强 AI 算力的芯片,买一块至少要...
继续阅读 »

先抛几个数据:


当下,每天有超过 2 亿的人在疯狂地抛出各式各样的问题请求 ChatGPT 回复


如果要完成这 2 亿+ 的咨询量,初始投入需要 3 万+ 块英伟达 A100 GPU 来计算


而 A100 是当下这个星球拥有最强 AI 算力的芯片,买一块至少要 7W+ RMB💴


image.png


意思就是:光计算这些有意义/无意义的问题,就要花费:21 亿+ RMB 💴


这还不算每天的电费/维护费等,众所周知,这种庞大计算类似矿机,很费电


一块 A100,功率 400W,30000 块 A100 ,就是 12_000_000 W,是 12 MW(12 个 100 万瓦)


image.png


目前,“GPT们”就像是井喷一样出现了各类产品,先不管算的结果怎么样,大家先支棱起来、先算起来


有预测:十年后,“GPT们” 一天所需算力的生产功率相当于半个核电站产生的功率,这是离谱且夸张的!全球算力几乎快要无法满足“GPT们”了


image.png


所以,以 ChatGPTPlus(每月20刀) 为代表的 “GPT们” 很贵,因为它本来算力消费就很贵,“用 GPT 编程比招一个普通程序员更贵”的段子并非玩笑。


所以,为什么算力如此重要?为什么微软要和韩国SK集团布局自建核电站?为什么咱们要强调西数东算、云计算等等?从这里也能窥见一斑。


不夸张的说,在未来,国力强弱一方面将通过算力强弱来体现。


image.png


2016年6月的不同国家之间的超级计算机500强的分布


image.png


超级计算机模拟风洞实验


小思考:在未来,算力的瓶颈将如何突破?


—— 目前芯片仍处在传统冯·诺伊曼架构之下,存储和计算区域是分离的,搬运数据“从存储到计算”花费巨大的功耗,如果能实现“存算一体”(就像大脑一样)将提升算力进入新的量级。


所以“存算一体”可能是个方向~


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

在国内如何不用梯子就使用ChatGPT

首先看效果图 如何国内访问 由于OpenAI官方服务器在国内被限制,所以想要在国内直接访问ChatGPT并不容易。但是,今天格子衫将介绍一种无需使用梯子的方法,就可以通过API接口访问ChatGPT。 要使用这种方法,您需要获取一个ApiKey。下面是如何...
继续阅读 »

首先看效果图


1678790956947.jpg


1678790977126.jpg


如何国内访问


由于OpenAI官方服务器在国内被限制,所以想要在国内直接访问ChatGPT并不容易。但是,今天格子衫将介绍一种无需使用梯子的方法,就可以通过API接口访问ChatGPT。


要使用这种方法,您需要获取一个ApiKey。下面是如何获取ApiKey的步骤:


获取 Api Key


首先,您需要注册一个OpenAI的账号(文末有其他方式)。



  1. 使用浏览器打开Account API Keys - OpenAI API,如果没有登录,会跳转到登录页面,按照页面登录即可。

  2. 登录完成以后,再次访问刚才的 Account API Keys - OpenAI API,就可以获取ApiKey了,按照下图操作即可获取ApiKey。


Create api key


点击 Create new secret key,会生成如下图所示的秘钥。


请点击右侧的“复制”按钮将ApiKey复制到剪贴板中,然后立即将它保存到一个安全的地方。这样,即使您的剪贴板内容被覆盖或丢失,您仍然可以访问到ApiKey。你应该跟保存密码一样保存它。


Create api key success


**注意:**ApiKey只会显示一次。如果您不小心丢失了它,您需要重新生成一个ApiKey。这可能需要您再次使用梯子访问OpenAI官方服务器,有点麻烦。


接下来,拿着这个 ApiKey,去访问国内版ChatGPT,把ApiKey输入到下图所示的指定位置;


image-20230312143953871


然后无需梯子就可以直接访问 ChatGPT了,直接在聊天窗口输入即可,可以和ChatGPT进行对话了!如下图所示:


image-20230312144941252


感兴趣的小伙伴们快行动起来吧!


地址是 chatv.fu520.top/


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

我为什么用TheRouter而不玩Arouter了呢

TheRouter 简介   TheRouter是货拉拉开源的路由框架,针对Android平台实现组件化、跨模块调用、动态化等功能的集成框架。 TheRouter 掘金:juejin.cn/post/713971… Github: github.com/Huo...
继续阅读 »

TheRouter 简介


  TheRouter是货拉拉开源的路由框架,针对Android平台实现组件化、跨模块调用、动态化等功能的集成框架。

TheRouter 掘金:juejin.cn/post/713971…

Github: github.com/HuolalaTech…

官网:therouter.cn/


为什么要用TheRouter


功能TheRouterARouterWMRouter
Fragment路由✔️✔️✔️
支持依赖注入✔️✔️✔️
加载路由表无运行时扫描无反射运行时扫描dex反射实例类性能损耗大运行时读文件反射实例类性能损耗中
注解正则表达式✔️✖️✔️
Activity指定拦截器✔️(四大拦截器可根据业务定制)✖️✔️
导出路由文档✔️(路由文档支持添加注释描述)✔️✖️
动态注册路由信息✔️✔️✖️
APT支持增量编译✔️✔️(开启文档生成则无法增量编译)✖️
plugin支持增量编译✔️✖️✖️
多 Path 对应同一页面(低成本实现双端path统一)✔️✖️✖️
远端路由表下发✔️✖️✖️
支持单模块独立初始化✔️✖️✖️
支持使用路由打开第三方库页面✔️✖️✖️


上图是从官方获取的介绍,结合自己使用,从以下几点介绍他的好处



  • 使用简易

  • 针对后台startActivity的兼容处理

  • 直接获取Intent对象

  • 动态下发路由表的骚操作

  • 和Arouter性能相比


- 使用简易


  针对刚入手的同学,很多同学其实需要的功能不多,主要就是跳转页面,原先使用过的Arouter,也能快速适应。想深入学习更多的同学可以再去官网或者官方掘金see see


// 简单的传参和路由跳转
TheRouter.build("test://webview/home")
.withString("url", "http://www.baidu.com")
.navigation(act)

// 如果要打开的是fragment,需要使用 .createFragment();

- 针对后台startActivity的兼容处理



Android 10 (级别 29) 及更高版本对后台应用可启动 Activity 的时间施加限制。这些限制有助于最大限度地减少对用户造成的中断(可以更省电),并且可以让用户更好地控制其屏幕上显示的内容。



  官方针对Android10以及以上版本调用startActivity的限制,也就是在后台调用该方法是无法跳转的,TheRoute提供兼容处理,先暂停跳转,等APP重新启动了再继续跳转。


// 以下代码只是功能介绍,大家按需自取
// 后台跳转页面
fun goBackActivity(activity: Activity) {
val navigator: Navigator =
TheRouter.build("test://webview/home")
.withString("paramStr", paramStr)
.withInt("paramInt", 0)
if (AndroidVersion.hasQ() && !AppUtils.isAppForeground()) {
// pending() 会标记存入集合LinkedList
navigator.pending().navigation(activity)
} else {
navigator.navigation(activity)
}
}

// Application监听
class ApplicationObserver : DefaultLifecycleObserver {

override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
// 应用重进onResume,把原先pending()存入集合的navigation,重新取出全部执行
// sendPendingNavigator是Navigator的顶级函数
sendPendingNavigator()
}
}

- 直接获取Intent对象


  大部分的路由框架都不提供直接返回Intent,但是有时候我们又只需要对应其他模块的intent对象并不需要跳转,例如构建系统通知栏的点击需要传入PendingIntent,可能需要通过Intent对象构建。


// 获取intent
val intent = TheRouter.build("test://login/hello").createIntent(activity)

- 动态下发路由表的『骚操作』


   为什么我个人说这是『骚操作』呢?我们以前写一个页面,页面崩溃了,那么我们就想办法修复,让用户先忍忍。可是用你这个APP的人真的能忍受你的崩溃吗?答案是不能的,因此我们想尽各种办法例如热修复等等,但是还是有没有经过热修复的人崩溃了,还是大面积的崩溃。


  那动态下发路由表,让这个崩溃页面的路由改下其他页面,例如这个崩溃页面的路由跳转H5页面,告知用户正在紧急修复,这样的过渡会比直接崩溃使用感受好。


  当然不止这个场景,再举例一个我们最常见的,例如A页面忘了给相机权限,然后上线了跳转A页面的时候才发现少了这个权限,咋整?重发版呗。但是如果用TheRouter,可以在原工程预留一个过渡页面,我们动态下发路由表,跳到这个过渡页获取权限了再重新跳转原来A页面。


// 获取远端的路由map
/**
[
{
"path""test://home/webview/pre",
"action""test://action/start_activity?path=test://app/alert?type=-1&permissionJson={"android.permission.READ_PHONE_STATE": "需要获取您的手机网络状态"}",
"description""",
"className""com.test.main.PreWebViewV2Activity",
"params": {
}
}
]
// action说明: 可以写一个test://app/alter权限弹窗页面,传入permissionJson参数授权,授权成功之后再重新执行原来的路由地址
*/
fun getServiceRouterMap() {
try {
val gson = "注释提供的格式"
if (gson.isNullOrEmpty()) {
return
}
Kv.putEnv(LOCAL_ROUTER_MAP, gson) // 本地存储,方便下次取出
mRouteItem = GsonUtils.fromJson(gson, object : TypeToken?>() {}.type)
} catch (e: Exception) {
Log.d("updateRouterMap", e?.toString())
}
}

// 把远端路由遍历添加到本地
fun addMdapRouteMap() {
Log.d("HllMarsConfigTask", "asyncInitRouteMap")
var list = mRouteItem
if (list != null && list.size > 0) {
// 建议远端下发路由表差异部分,用远端包覆盖本地更合理
addRouteMap(list) // TheRouter库RouteMap.kt提供顶级函数
}
}

// 以下是TheRouter库RouteMap.kt类
// 具体实现:如何把远端路由添加到本地
@Synchronized
fun addRouteMap(routeItemArray: Collection?) {
if (routeItemArray != null && !routeItemArray.isEmpty()) {
for (entity in routeItemArray) {
addRouteItem(entity)
}
}
}

@Synchronized
fun addRouteItem(routeItem: RouteItem) {
var path = routeItem.path
if (path.endsWith("/")) {
path = path.substring(0, path.length - 1)
}
debug("addRouteItem", "add $path")
ROUTER_MAP[path] = routeItem
onRouteMapChangedListener?.onChanged(routeItem)
}

- 和Arouter性能相比


  以阿里的Arouter来说,它是有比较沉重的历史包袱,虽然后面有迭代几个版本来优化,但是更多的还是在历史基础上优化,TheRouter却没有历史包袱。下面是我们自己工程跑的对比效果


therouter和arouter性能对比.png


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

一个app到底会创建多少个Application对象

问题背景 最近跟群友讨论一个技术问题: 一个应用开启了多进程,最终到底会创建几个application对象,执行几次onCreate()方法? 有的群友根据自己的想法给出了猜想 甚至有的群友直接咨询起了ChatGPT 但至始至终都没有一个最终的结论。于是...
继续阅读 »

问题背景


最近跟群友讨论一个技术问题:


交流1


一个应用开启了多进程,最终到底会创建几个application对象,执行几次onCreate()方法?


有的群友根据自己的想法给出了猜想


交流2


甚至有的群友直接咨询起了ChatGPT


chatgpt1.jpg


但至始至终都没有一个最终的结论。于是乎,为了弄清这个问题,我决定先写个demo测试得出结论,然后从源码着手分析原因


Demo验证


首先创建了一个app项目,开启多进程


<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
android:name=".DemoApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Demo0307"
tools:targetApi="31">
<!--android:process 开启多进程并设置进程名-->
<activity
android:name=".MainActivity"
android:exported="true"
android:process=":remote">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

</activity>
</application>

</manifest>

然后在DemoApplication的onCreate()方法打印application对象的地址,当前进程名称


public class DemoApplication extends Application {
private static final String TAG = "jasonwan";

@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "Demo application onCreate: " + this + ", processName=" + getProcessName(this));
}

private String getProcessName(Application app) {
int myPid = Process.myPid();
ActivityManager am = (ActivityManager) app.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> runningAppProcesses = am.getRunningAppProcesses();
for (ActivityManager.RunningAppProcessInfo runningAppProcess : runningAppProcesses) {
if (runningAppProcess.pid == myPid) {
return runningAppProcess.processName;
}
}
return "null";
}
}

运行,得到的日志如下


2023-03-07 11:15:27.785 19563-19563/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote

查看当前应用所有进程


查看进程1


说明此时app只有一个进程,且只有一个application对象,对象地址为@fb06c2d


现在我们将进程增加到多个,看看情况如何


<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
android:name=".DemoApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Demo0307"
tools:targetApi="31">
<!--android:process 开启多进程并设置进程名-->
<activity
android:name=".MainActivity"
android:exported="true"
android:process=":remote">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

</activity>
<activity
android:name=".TwoActivity"
android:process=":remote2" />
<activity
android:name=".ThreeActivity"
android:process=":remote3" />
<activity
android:name=".FourActivity"
android:process=":remote4" />
<activity
android:name=".FiveActivity"
android:process=":remote5" />
</application>

</manifest>

逻辑是点击MainActivity启动TwoActivity,点击TwoActivity启动ThreeActivity,以此类推。最后我们运行,启动所有Activity得到的日志如下


2023-03-07 11:25:35.433 19955-19955/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote
2023-03-07 11:25:43.795 20001-20001/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote2
2023-03-07 11:25:45.136 20046-20046/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote3
2023-03-07 11:25:45.993 20107-20107/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote4
2023-03-07 11:25:46.541 20148-20148/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote5

查看当前应用所有进程


查看进程2


此时app有5个进程,但application对象地址均为@fb06c2d,地址相同意味着它们是同一个对象。


那是不是就可以得出结论,无论启动多少个进程都只会创建一个application对象呢?并不能妄下此定论,我们将MainActivityprocess属性去掉再运行,得到的日志如下


2023-03-07 11:32:10.156 20318-20318/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@5d49e29, processName=com.jason.demo0307
2023-03-07 11:32:15.143 20375-20375/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote2
2023-03-07 11:32:16.477 20417-20417/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote3
2023-03-07 11:32:17.582 20463-20463/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote4
2023-03-07 11:32:18.882 20506-20506/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote5

查看当前应用所有进程


查看进程3


此时app有5个进程,但有2个application对象,对象地址为@5d49e29和@fb06c2d,且子进程的application对象都相同。


上述所有进程的父进程ID为678,而此进程正是zygote进程


zygote进程


根据上面的测试结果我们目前能得出的结论:



  • 结论1:单进程只创建一个Application对象,执行一次onCreate()方法;

  • 结论2:多进程至少创建2个Application对象,执行多次onCreate()方法,几个进程就执行几次;


结论2为什么说至少创建2个,因为我在集成了JPush的商业项目中测试发现,JPush创建的进程跟我自己创建的进程,Application地址是不同的。


jpush进程


这里三个进程,分别创建了三个Application对象,对象地址分别是@f31ba9d,@2c586f3,@fb06c2d


源码分析


这里需要先了解App的启动流程,具体可以参考《App启动流程》


Application的创建位于frameworks/base/core/java/android/app/ActivityThread.javahandleBindApplication()方法中


	@UnsupportedAppUsage
private void handleBindApplication(AppBindData data) {
long st_bindApp = SystemClock.uptimeMillis();
//省略部分代码

// Note when this process has started.
//设置进程启动时间
Process.setStartTimes(SystemClock.elapsedRealtime(), SystemClock.uptimeMillis());

//省略部分代码

// send up app name; do this *before* waiting for debugger
//设置进程名称
Process.setArgV0(data.processName);
//省略部分代码

// Allow disk access during application and provider setup. This could
// block processing ordered broadcasts, but later processing would
// probably end up doing the same disk access.
Application app;
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();
try {
// If the app is being launched for full backup or restore, bring it up in
// a restricted environment with the base application class.
//此处开始创建application对象,注意参数2为null
app = data.info.makeApplication(data.restrictedBackupMode, null);

//省略部分代码
try {
if ("com.jason.demo0307".equals(app.getPackageName())){
Log.d("jasonwan", "execute app onCreate(), app=:"+app+", processName="+getProcessName(app)+", pid="+Process.myPid());
}
//执行application的onCreate方法()
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
} finally {
// If the app targets < O-MR1, or doesn't change the thread policy
// during startup, clobber the policy to maintain behavior of b/36951662
if (data.appInfo.targetSdkVersion < Build.VERSION_CODES.O_MR1
|| StrictMode.getThreadPolicy().equals(writesAllowedPolicy)) {
StrictMode.setThreadPolicy(savedPolicy);
}
}
//省略部分代码
}

实际创建过程在frameworks/base/core/java/android/app/LoadedApk.java中的makeApplication()方法中,LoadedApk顾名思义就是加载好的Apk文件,里面包含Apk所有信息,像包名、Application对象,app所在的目录等,这里直接看application的创建过程


	@UnsupportedAppUsage
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
if ("com.jason.demo0307".equals(mApplicationInfo.packageName)) {
Log.d("jasonwan", "makeApplication: mApplication="+mApplication+", pid="+Process.myPid());
}
//如果已经创建过了就不再创建
if (mApplication != null) {
return mApplication;
}

Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "makeApplication");

Application app = null;

String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}

try {
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals("android")) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
"initializeJavaContextClassLoader");
initializeJavaContextClassLoader();
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
//反射创建application对象
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
if ("com.jason.demo0307.DemoApplication".equals(appClass)){
Log.d("jasonwan", "create application, app="+app+", processName="+mActivityThread.getProcessName()+", pid="+Process.myPid());
}
appContext.setOuterContext(app);
} catch (Exception e) {
Log.d("jasonwan", "fail to create application, "+e.getMessage());
if (!mActivityThread.mInstrumentation.onException(app, e)) {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
throw new RuntimeException(
"Unable to instantiate application " + appClass
+ ": " + e.toString(), e);
}
}
mActivityThread.mAllApplications.add(app);
mApplication = app;

if (instrumentation != null) {
try {
//第一次启动创建时,instrumentation为null,不会执行onCreate()方法
instrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!instrumentation.onException(app, e)) {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
}

// 省略部分代码
return app;
}

为了看清application到底被创建了几次,我在关键地方埋下了log,TAG为jasonwan的log是我自己加的,编译验证,得到如下log


启动app,进入MainActivity
03-08 17:20:29.965 4069 4069 D jasonwan: makeApplication: mApplication=null, pid=4069
//创建application对象,地址为@c2f8311,当前进程id为4069
03-08 17:20:29.967 4069 4069 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307, pid=4069
03-08 17:20:29.988 4069 4069 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307, pid=4069
03-08 17:20:29.989 4069 4069 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307, pid=4069
03-08 17:20:36.614 4069 4069 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4069

点击MainActivity,跳转到TwoActivity
03-08 17:20:39.686 4116 4116 D jasonwan: makeApplication: mApplication=null, pid=4116
//创建application对象,地址为@c2f8311,当前进程id为4116
03-08 17:20:39.687 4116 4116 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote2, pid=4116
03-08 17:20:39.688 4116 4116 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote2, pid=4116
03-08 17:20:39.688 4116 4116 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote2, pid=4116
03-08 17:20:39.733 4116 4116 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4116

点击TwoActivity,跳转到ThreeActivity
03-08 17:20:41.473 4147 4147 D jasonwan: makeApplication: mApplication=null, pid=4147
//创建application对象,地址为@c2f8311,当前进程id为4147
03-08 17:20:41.475 4147 4147 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote3, pid=4147
03-08 17:20:41.475 4147 4147 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote3, pid=4147
03-08 17:20:41.476 4147 4147 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote3, pid=4147
03-08 17:20:41.519 4147 4147 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4147

点击ThreeActivity,跳转到FourActivity
03-08 17:20:42.966 4174 4174 D jasonwan: makeApplication: mApplication=null, pid=4174
//创建application对象,地址为@c2f8311,当前进程id为4174
03-08 17:20:42.968 4174 4174 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote4, pid=4174
03-08 17:20:42.969 4174 4174 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote4, pid=4174
03-08 17:20:42.969 4174 4174 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote4, pid=4174
03-08 17:20:43.015 4174 4174 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4174

点击FourActivity,跳转到FiveActivity
03-08 17:20:44.426 4202 4202 D jasonwan: makeApplication: mApplication=null, pid=4202
//创建application对象,地址为@c2f8311,当前进程id为4202
03-08 17:20:44.428 4202 4202 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote5, pid=4202
03-08 17:20:44.429 4202 4202 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote5, pid=4202
03-08 17:20:44.430 4202 4202 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote5, pid=4202
03-08 17:20:44.473 4202 4202 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4202

结果很震惊,我们在5个进程中创建的application对象,地址均为@c2f8311,也就是至始至终创建的都是同一个Application对象,那么上面的结论2显然并不成立,只是测试的偶然性导致的。


可真的是这样子的吗,这也太颠覆我的三观了,为此我跟群友讨论了这个问题:


不同进程中的多个对象,内存地址相同,是否代表这些对象都是同一个对象?


群友的想法是,java中获取的都是虚拟内存地址,虚拟内存地址相同,不代表是同一个对象,必须物理内存地址相同,才表示是同一块内存空间,也就意味着是同一个对象,物理内存地址和虚拟内存地址存在一个映射关系,同时给出了java中获取物理内存地址的方法Android获取对象地址,主要是利用Unsafe这个类来操作,这个类有一个作用就是直接访问系统内存资源,具体描述见Java中的魔法类-Unsafe,因为这种操作是不安全的,所以被标为了私有,但我们可以通过反射去调用此API, 然后我又去请教了部门搞寄存器的大佬,大佬肯定了群友的想法,于是我添加代码,尝试获取对象的物理内存地址,看看是否相同


public class DemoApplication extends Application {
public static final String TAG = "jasonwan";

@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "DemoApplication=" + this + ", address=" + addressOf(this) + ", pid=" + Process.myPid());
}

//获取对象的真实物理地址
public static long addressOf(Object o) {
Object[] array = new Object[]{o};
long objectAddress = -1;
try {
Class cls = Class.forName("sun.misc.Unsafe");
Field field = cls.getDeclaredField("theUnsafe");
field.setAccessible(true);
Object unsafe = field.get(null);
Class unsafeCls = unsafe.getClass();
Method arrayBaseOffset = unsafeCls.getMethod("arrayBaseOffset", Object.class.getClass());
int baseOffset = (int) arrayBaseOffset.invoke(unsafe, Object[].class);
Method size = unsafeCls.getMethod("addressSize");
int addressSize = (int) size.invoke(unsafe);
switch (addressSize) {
case 4:
Method getInt = unsafeCls.getMethod("getInt", Object.class, long.class);
objectAddress = (int) getInt.invoke(unsafe, array, baseOffset);
break;
case 8:
Method getLong = unsafeCls.getMethod("getLong", Object.class, long.class);
objectAddress = (long) getLong.invoke(unsafe, array, baseOffset);
break;
default:
throw new Error("unsupported address size: " + addressSize);
}
} catch (Exception e) {
e.printStackTrace();
}
return objectAddress;
}
}

运行后得到如下日志


2023-03-10 11:01:54.043 6535-6535/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@930d275, address=8050489105119022792, pid=6535
2023-03-10 11:02:22.610 6579-6579/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119027136, pid=6579
2023-03-10 11:02:36.369 6617-6617/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119029912, pid=6617
2023-03-10 11:02:39.244 6654-6654/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119032760, pid=6654
2023-03-10 11:02:40.841 6692-6692/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119036016, pid=6692
2023-03-10 11:02:52.429 6729-6729/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119038720, pid=6729

可以看到,虽然Application的虚拟内存地址相同,都是331b3b9,但它们的真实物理地址却不同,至此,我们可以得出最终结论



  • 单进程,创建1个application对象,执行一次onCreate()方法

  • 多进程(N),创建N个application对象,执行N次onCreate()方法

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

Android 可视化预览及编辑Json

项目中涉及到广告开发, 广告的配置是从API动态下发, 广告配置中,有很多业务相关参数,例如关闭或开启、展示间隔、展示时间、重试次数、每日最大显示次数等。 开发时单个广告可能需要多次修改配置来测试,为了方便测试,广告配置的json文件,有两种途径修改并生效 ...
继续阅读 »

项目中涉及到广告开发, 广告的配置是从API动态下发, 广告配置中,有很多业务相关参数,例如关闭或开启、展示间隔、展示时间、重试次数、每日最大显示次数等。


开发时单个广告可能需要多次修改配置来测试,为了方便测试,广告配置的json文件,有两种途径修改并生效





    1. 每次抓包修改配置





    1. 本地导入配置,从磁盘读取




但两种方式都有一定弊端



  • 首先测试时依赖电脑修改配置

  • 无法直观预览广告配置


考虑到开发时经常使用的Json格式化工具,既可以直观的预览Json, 还可以在线编辑


那么就考虑将Json格式化工具移植到项目测试模块中


web网页可以处理Json格式化,同理在Android webView 中同样可行, 只需要引入处理格式化的JS代码即可。


查找资料,发现一个很实用的文章可视化编辑json数据——json editor


开始处理


首先准备好WebView的壳子


    //初始化
@SuppressLint("SetJavaScriptEnabled")
private fun initWebView() {
binding.webView.settings.apply {
javaScriptEnabled = true
javaScriptCanOpenWindowsAutomatically = true
setSupportZoom(true)
useWideViewPort = true
builtInZoomControls = true
}
binding.webView.addJavascriptInterface(JsInterface(this@MainActivity), "json_parse")
}

//webView 与 Android 交互
inner class JsInterface(context: Context) {
private val mContext: Context

init {
mContext = context
}

@JavascriptInterface
fun configContentChanged() {
runOnUiThread {
contentChanged = true
}
}

@JavascriptInterface
fun toastJson(msg: String?) {
runOnUiThread { Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show() }
}

@JavascriptInterface
fun saveConfig(jsonString: String?) {
runOnUiThread {
contentChanged = false
Toast.makeText(mContext, "verification succeed", Toast.LENGTH_SHORT).show()
}
}

@JavascriptInterface
fun parseJsonException(e: String?) {
runOnUiThread {
e?.takeIf { it.isNotBlank() }?.let { alert(it) }
}
}
}

加载json并在WebView中展示



viewModel.jsonData.observe(this) { str ->
if (str?.isNotBlank() == true) {
binding.webView.loadUrl("javascript:showJson($str)")
}
}

WebView 加载预览页面


        binding.webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
viewModel.loadAdConfig(this@MainActivity)
}
}

binding.webView.loadUrl("file:///android_asset/preview_json.html")


Json 预览页, preview_json.html实现



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="jquery.json-viewer.css"
rel="stylesheet" type="text/css">
</head>
<style type="text/css">
#json-display {
margin: 2em 0;
padding: 8px 15px;
min-height: 300px;
background: #ffffff;
color: #ff0000;
font-size: 16px;
width: 100%;
border-color: #00000000;
border:none;
line-height: 1.8;
}
#json-btn {
display: flex;
align-items: center;
font-size: 18px;
width:100%;
padding: 10;

}
#format_btn {
width: 50%;
height: 36px;
}
#save_btn {
width: 50%;
height: 36px;
margin-left: 4em;
}

</style>
<body>
<div style="padding: 2px 2px 2px 2px;">
<div id="json-btn" class="json-btn">
<button type="button" id="format_btn" onclick="format_btn();">Format</button>
<button type="button" id="save_btn" onclick="save_btn();">Verification</button>

</div>
<div>
<pre id="json-display" contenteditable="true"></pre>
</div>
<br>
</div>

<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="jquery.json-viewer.js"></script>
<script>

document.getElementById("json-display").addEventListener("input", function(){
console.log("json-display input");
json_parse.configContentChanged();
}, false);
function showJson(jsonObj){
$("#json-display").jsonViewer(jsonObj,{withQuotes: true});//format json and display
}
function format_btn() {
var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();
var jsonval = my_json_val.text();
var jsonObj = JSON.parse(jsonval); //parse string to json
$("#json-display").jsonViewer(jsonObj,{withQuotes: true});//format json and display
}


function save_btn() {
var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();
var jsonval = my_json_val.text();
var saveFailed = false;
try {
var jsonObj = JSON.parse(jsonval); //parse
} catch (e) {
console.error(e.message);
saveFailed = true;
json_parse.parseJsonException(e.message); // throw exception
}
if(!saveFailed) {
json_parse.saveConfig(jsonval);
}
}

</script>
</body>
</html>


这其中有两个问题需注意





    1. 如果value的值是url, 格式化后缺少引号
      从json-viewer.js源码可以发现,源码中会判断value是否是url,如果是则直接输出




处理方式:在json 左右添加上双引号


    if (options.withLinks && isUrl(json)) {
html += '<a href="' + json + '" class="json-string" target="_blank">' + '"' +json + '"' + '</a>';
} else {
// Escape double quotes in the rendered non-URL string.
json = json.replace(/&quot;/g, '\\&quot;');
html += '<span class="json-string">"' + json + '"</span>';
}




    1. 如果折叠后json-viewer会增加<a>标签,即使使用text()方法获取到纯文本数据,这里面也包含了“n items”的字符串,那么该如何去除掉这些字符串呢?




 var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();

总结


使用时只需将json文件读取,传入preview_json.html的showJson方法


编辑结束后, 点击Save 即可保存


示例代码 Android 可视化编辑json JsonPreviewer


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

线程池也会导致OOM的原因

OOM
1. 前言 我这边从一个问题引出这次的话题,我们可能会在开中碰到一种OOM问题,java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again 相信很多人碰到过这个错误,很...
继续阅读 »

1. 前言


我这边从一个问题引出这次的话题,我们可能会在开中碰到一种OOM问题,java.lang.OutOfMemoryError: pthread_create (1040KB stack) failed: Try again


相信很多人碰到过这个错误,很容易从网上搜索到出现这个问题的原因是线程过多,那线程过多为什么会导致OOM?线程什么情况下会释放资源?你又能如何做到让它不释放资源?


有的人可能会想到,那既然创建线程过多会导致OOM,那我用线程池不就行了。但是有没有想过,线程池,也可能会造成OOM。其实这里有个很经典的场景,你使用OkHttp的时候不注意,每次请求都创建OkHttpClient,导致线程池过多出现OOM


2. 简单了解线程池


如何去了解线程池,看源码,直接去看是很难看得懂的,要先了解线程池的原理,对它的设计思想有个大概的掌握之后,再去看源码,就会轻松很多,当然这里只了解基础的原理还不够,还需要有一些多线程相关的基础知识。


本篇文章只从部分源码的角度去分析,线程池如何导致OOM的,而不会全部去看所有线程池的源码细节,因为太多了


首先,要了解线程池,首先需要从它的参数入手:



  • corePoolSize:核心线程数量

  • maximumPoolSize:最大线程数量

  • keepAliveTime,unit:非核心线程的存活时间和单位

  • workQueue:阻塞队列

  • ThreadFactory:线程工厂

  • RejectedExecutionHandler:饱和策略


然后你从网上任何一个地方搜都能知道它大致的工作流程是,当一个任务开始执行时,先判断当前线程池数量是否达到核心线程数,没达到则创建一个核心线程来执行任务,如果超过,放到阻塞队列中等待,如果阻塞队列满了,未达到最大线程数,创建一条非核心线程执行任务,如果达到最大线程数,执行饱和策略。在这个过程中,核心线程不会回收,非核心线程会根据keepAliveTime和unit进行回收。


**这里可以多提一嘴,这个过程用了工厂模式ThreadFactory和策略模式RejectedExecutionHandler,关于策略模式可以看我这篇文章 ** juejin.cn/post/719502…


其实从这里就可以看出为什么线程池也会导致OOM了:核心线程不会回收,非核心线程使用完之后会根据keepAliveTime和unit进行回收 ,那核心线程就会一直存活(我这不考虑shutdown()和shutdownNow()这些情况),一直存活就会占用内存,那你如果创建很多线程池,就会OOM。


所以我这篇文章要分析:核心线程不会释放资源的过程,它内部怎么做到的。 只从这部分的源码去进行分析,不会全部都详细讲。


先别急,为了照顾一些基础不太好的朋友,涉及一些基础知识感觉还是要多讲一下。上面提到的线程回收和shutdown方法这些是什么意思?线程执行完它内部的代码后会主动释放资源吗?


我们都知道开发中有个概念叫生命周期,当然线程池和线程也有生命周期(这很重要),在开发中,我们称之为lifecycle。


生命周期当然是设计这个东西的开发者所定义的,我们先看线程池的生命周期,在ThreadPoolExecutor的注释中有写:


*
* The runState provides the main lifecycle control, taking on values:
*
* RUNNING: Accept new tasks and process queued tasks
* SHUTDOWN: Don't accept new tasks, but process queued tasks
* STOP: Don't accept new tasks, don't process queued tasks,
* and interrupt in-progress tasks
* TIDYING: All tasks have terminated, workerCount is zero,
* the thread transitioning to state TIDYING
* will run the terminated() hook method
* TERMINATED: terminated() has completed
*

看得出它的生命周期有RUNNING,SHUTDOWN,STOP,TIDYING和TERMINATED。而shutdown()和shutdownNow()方法会改变生命周期,这里不是对线程池做全面解析,所以先有个大概了解就行,可以暂时理解成这篇文章的所有分析都是针对RUNNING状态下的。


看完线程池的,再看看线程的生命周期。线程的生命周期有:



  • NEW:创建,简单来说就是new出来没start

  • RUNNABLE:运行,简单来说就是start后执行run方法

  • TERMINATED:中止,简单来说就是执行完run方法或者进行中断操作之后会变成这个状态

  • BLOCKED:阻塞,就是加锁之后竞争锁会进入到这个状态

  • WAITING、TIMED_WAITING:休眠,比如sleep方法


这个很重要,需要了解,你要学会线程这块相关的知识点的话,这些生命周期要深刻理解 。比如BLOCKED和WAITING有什么不同?然后学这块又会涉及到锁那一块的知识。以后有时间可以单独写几篇这类的文章,这里先大概有个概念,只需要能先看懂后面的源码就行。


从生命周期的概念你就能知道线程执行完它内部的代码后会主动释放资源,因为它run执行完之后生命周期会到TERMINATED,那这又涉及到了一个知识点,为什么主线程(ActivityThread),执行完run的代码后不会生命周期变成TERMINATED,这又涉及到Looper,就得了解Handler机制,可以看我这篇文章 juejin.cn/post/715882…


扯远了,现在进入正题,先想想,如果是你,你怎么做让核心线程执行完run之后不释放资源,很明显,只要让它不执行到TERMINATED生命周期就行,如何让它不变成TERMINATED状态,只需要让它进入BLOCKED或者WAITING状态就行。所以我的想法是这样的,当这个核心线程执行完这个任务之后,我让它WAITING,等到有新的任务进来的时候我再唤醒它进入RUNNABLE状态。 这是我从理论这个角度去分析的做法,那看看实际ThreadPoolExecutor是怎么做的


3. 线程池部分源码分析


前面说了,不会全部都讲,这里涉及到文章相关内容的流程就是核心线程的任务执行过程,所以这里主要分析核心线程。


当我们使用线程池执行一个任务时,会调用ThreadPoolExecutor的execute方法


public void execute(Runnable command) {
......

int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}

// 我们只看核心线程的流程,所以后面的代码不用管
......
}

这个ctl是一个状态相关的代码,可以先不用管,我后面会简单统一做个解释,这里不去管它会比较容易理解,我们现在主要是为了看核心线程的流程。从这里可以看出,当前线程的数量小于核心线程的话执行addWorker方法


private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}

boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());

if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}

这个addWorker分为上下两部分,我们分别来做解析


private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);

// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;

for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}

// 下半部分
......
}

这里主要是做了状态判断的一些操作,我说过状态相关的我们可以先不管,但是这里的写法我觉得要单独讲一下为什么会这么写。不然它内部很多代码是这样的,我怕劝退很多人。


首先retry: ...... break retry; 这个语法糖,平常我们开发很少用到,可以去了解一下,这里就是为了跳出循环。 其次,这里的compareAndIncrementWorkerCount内部的代码是AtomicInteger ctl.compareAndSet(expect, expect + 1) ,Atomic的compareAndSet操作搭配死循环,这叫自旋,所以说要看懂这个需要一定的java多线程相关的基础。自旋的目的是为了什么?这就又涉及到了锁的分类中有乐观锁,有悲观锁。不清楚的可以去学一下这些知识,你就知道为什么它要这么做了,这里就不一一解释。包括你看它的源码,能看到,它会很多地方用自旋,很多地方用ReentrantLock,但它就是不用synchronized ,这些都是多线程这块基础的知识,这里不多说了。


看看下半部分


private boolean addWorker(Runnable firstTask, boolean core) {

// 上半部分
......



boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
......
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
......
}
return workerStarted;
}

看到它先创建一个Worker对象,再调用Worker对象内部的线程的start方法,我们看看Worker


private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable
{

private static final long serialVersionUID = 6138294804551838833L;

final Thread thread;
Runnable firstTask;

Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}

public void run() {
runWorker(this);
}

// 其它方法
......
}

看到它内部主要有两个对象firstTask就是任务,thread就是执行这个任务的线程,而这个线程是通过getThreadFactory().newThread(this)创建出来的,这个就是我们创建ThreadPoolExecutor时传的“线程工厂”

外部调t.start();之后就会执行这里的run方法,因为newThread传了this进去,你可以先简单理解调这个线程start会执行到这个run,然后run中调用runWorker(this);


注意,你想想runWorker(this)方法,包括之后的流程,都是执行在哪个线程中?都是执行在子线程中,因为这个run方法中的代码,都是执行在这个线程中。你一定要理解这一步,不然你自己看源码会可能看懵。 因为有些人长期不接触多线程环境的情况下,你会习惯单线程的思维去看问题,那就很容易出现理解上的错误。


我们继续看看runWorker,时刻提醒你自己,之后的流程都是在子线程中进行,这条子线程的生命周期变为RUNNABLE


final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {s
w.lock();

// 中断相关的操作
......

try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
......
} finally {
afterExecute(task, thrown);
}
} finally {
......
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}

先讲讲这里的一个开发技巧,task.run()就是执行任务,它前面的beforeExecute和afterExecute就是模板方法设计模式,方便扩展用。

执行完任务后,最后执行processWorkerExit方法


private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}

tryTerminate();

......
}

workers.remove(w)后执行tryTerminate方法尝试将线程池的生命周期变为TERMINATED


final void tryTerminate() {
for (;;) {
int c = ctl.get();
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
if (workerCountOf(c) != 0) { // Eligible to terminate
interruptIdleWorkers(ONLY_ONE);
return;
}

final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
try {
terminated();
} finally {
ctl.set(ctlOf(TERMINATED, 0));
termination.signalAll();
}
return;
}
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}

先不用管状态的变化,一般一眼都能看得出这里是结束的操作了,我们追踪的核心线程正常在RUNNING状态下是不会执行到这里的。 那我们期望的没任务情况下让线程休眠的操作在哪里?

看回runWorker方法


final void runWorker(Worker w) {
......
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {s
......
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}

看到它的while中有个getTask()方法,认真看runWorker方法其实能看出,核心线程执行完一个任务之后会getTask()拿下一个任务去执行,这就是当核心线程满的时候任务会放到阻塞队列中,核心线程执行完任务之后会从阻塞队列中拿下一个任务执行。 getTask()从抽象上来看,就是从队列中拿任务。


private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?

for (;;) {
......

try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}

先把timed当成正常情况下为false,然后会执行workQueue.take(),这个workQueue是阻塞队列BlockingQueue, 注意,这里又需要有点基础了。正常有点基础的人看到这里,已经知道这里就是当没有任务会让核心线程休眠的操作,看不懂的,可以先了解下什么是AQS,可以看看我这篇文章 juejin.cn/post/716801…


如果你说你懒得看,行吧,我随便拿个ArrayBlockingQueue给你举例


public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}

notEmpty是Condition,这里调用了Condition的await()方法,然后想想执行这步操作的是在哪条线程上?线程进入WAITING状态了吧,不会进入TERMINATED了吧。


然后当有任务添加之后会唤醒它,它继续在循环中去执行任务。


这就验证了我们的猜想,通过让核心线程进入WAITING状态以此来达到执行完run方法中的任务也不会主动TERMINATED而释放线程。所以核心线程一直占用资源,这里说的资源指的是空间,而cpu的时间片是会让出的。


4. 部分线程池的操作解读


为什么线程池也会导致OOM,上面已经通过源码告诉你,核心线程不会释放内存空间,导致线程池多的情况下也会导致OOM。这里为了方便新手阅读ThreadPoolExecutor相关的代码,还是觉得写一些它内部的设计思想,不然没点基础的话确实很难看懂。


首先就是状态,上面源码中都有关线程池的生命中周期状态(ctl字段),可以看看它怎么设计的


private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3; // Integer.SIZE是32
private static final int CAPACITY = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

它这里用了两个设计思想,第一个就是用位来表示状态,关于这类型的设计,可以看我这2篇文章 juejin.cn/post/715547…juejin.cn/post/720550…


另外一个设计思想是:用一个变量的高位置表示状态,低位表示数量。 这里就是用高3位来表示生命周期,剩下的低位表示线程的数量。和这个类似的操作有view中的MeasureSpec,也是一个变量表示两个状态。


然后关于设计模式,可以看到它这里最经典的就是用了策略模式,如果你看饱和策略那块的源码,可以好好看看它是怎么设计的。其它的还有工厂、模板之类的,这些也不难,就是策略还是建议学下它怎么去设计的。


然后多线程相关的基础,这个还是比较重要的,这块的基础不好,看ThreadPoolExecutor的源码会相对吃力。比如我上面提过的,线程的生命周期,锁相关的知识,还有AQS等等。如果你熟悉这些,再看这个源码就会轻松很多。


对于总体的设计,你第一看会觉得它的源码很绕,为什么会这样?因为有中断操作+自旋锁+状态的设计 ,它的这种设计就基本可以说是优化代码到极致,比如说状态的设计,就比普通的能省内存,能更方便通过CAS操作。用自旋就是乐观锁,能节省资源等。有中断操作,能让整个系统更灵活。相对的缺点就是不安全,什么意思呢?已是就是这样写代码很容易出BUG,所以这里的让人觉得很绕的代码,就是很多的状态的判断,这些都是为了保证这个流程的安全。


5. 总结


从部分源码的角度去分析,得到的结论是线程池也可能会导致OOM


那再思考一个问题:不断的创建线程池,“一定”会导致OOM吗? 如果你对线程池已经有一定的了解,相信你也知道这个问题的答案。


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

当我把ChatGPT拉进群聊里,我的朋友都玩疯了

前言 近期ChatGPT可以说是太火了,问答、写论文、写诗、写代码,只要输入精确的prompt,他的表现总是让人惊喜。本着打不过就加入的原则。要是把ChatGPT拉入群聊中,会是怎样一番场景?说做就做,花了1个晚上捣鼓了一个小Demo【ChatGPT群聊助手】...
继续阅读 »

前言


近期ChatGPT可以说是太火了,问答、写论文、写诗、写代码,只要输入精确的prompt,他的表现总是让人惊喜。本着打不过就加入的原则。要是把ChatGPT拉入群聊中,会是怎样一番场景?说做就做,花了1个晚上捣鼓了一个小Demo【ChatGPT群聊助手】,凭借它的“聪明才智”,应该可以搞定我的网友、女朋友、老妈的提问...


温馨提示:如果你从没体验过ChatGPT,给大家准备了一个新手体验Demo,免注册!免登陆!免代理!!!!!!,拉到文末可以快速查看噢。


使用效果


效果可看下图


微信图片_20230308154658.png


应用前景


虽Demo仅在小范围的群聊中测试,但ChatGPT语义理解和交互能力确实强大,不仅能联系对话的上下文,还能及时纠正代码bug。不经让人想到,若能将ChatGPT应用于聊天机器人软件,完成回答问题、提供服务、甚至解决问题的任务,帮助人们解决重复性或大量的人工工作,代替传统聊天机器人应用于客服、电商、教育和金融等行业。


相对于传统聊天机器人,ChatGPT可根据用户的要求和特性,及时调整回答的策略以便更准确的回答问题,有更人性化的体验。现在被广泛使用的智能客服还不够智能,ChatGPT所具备的能力,正是客服领域所需要的。


1 准备工作


在国内无法注册ChatGPT账户,因此需要准备如下:


能接收短信的国外手机号: 只需花几块钱,使用国外虚拟号码在线接收短信。可以去一些第三方平台如:sms-activate.org/cn


国外IP: 可以通过一些工具来实现,如通过工具使用美国节点IP。


这里需要注意的是,sms-activate.org选取手机号码国家的时候,建议选择印度,如果选择印度尼西亚,会在openAI报如下错误:


You’ve made too many phone verification requests. Please try again later or contact us through our help center at help.openai.com

微信图片_20230308154704.png


以上是必须的前提工作,有了以上准备工作后,就可以去chat.openai.com/auth/login注册账号了。


2 实现思路


2.1 技术现状


chatGPT提供了基于Web版的交互界面,不便于编程式调用。于是,我们可以通过模拟浏览器去登录,然后把交互过程封装成API接口。


2.2 实现过程


ChatGPT作为一个机器人角色加入群聊,需要在PC端转发ChatGPT问答。因此,我们可以在PC电脑上完成ChatGPT接口的封装,并加入群聊。然后通过即构IM(群聊)将数据实时传输,实现群聊里面与ChatGPT聊天。


微信图片_20230308154709.png


3 PC端封装代码实现


3.1 封装chatGPT调用


我们使用chatgpt-api库来封装调用chatGPT,因此先要安装好依赖库:


npm install chatgpt

安装好chtgpt库后,使用起来就非常简单了:


var ChatGPT, ConversationId, ParentMessageId;
var API_KEY = ;//这里填写KEY
(async () => {
const { ChatGPTAPI } = await import('chatgpt');
ChatGPT = new ChatGPTAPI({ apiKey: API_KEY})
})();
//向ChatGPT发出提问
function chat(text, cb) {
console.log("正在向ChatGPT发送提问:",text)
ChatGPT.sendMessage(text, {
conversationId: ConversationId,
parentMessageId: ParentMessageId
}).then(
function (res) {
ConversationId = res.conversationId
ParentMessageId = res.id
cb && cb(true, res.text)
console.log(res)
}
).catch(function (err) {
cb && cb(false, err);
});
}

注意到,在第二行需要填写API_KEY,登录OpenAI后,打开链接platform.openai.com/account/api…即可获取,如下图所示


微信图片_20230308154713.png


3.2 收发群聊消息


关于即构IM,如果大家感兴趣可以进入官网doc-zh.zego.im了解更多。总所周知,在即时聊天和实时音视频方面,即构IM是个人开发者或者中小型企业首选。因为我们只关注一对一私聊或者群聊,因此,在官方提供的SDK的基础上,我们做了二次封装。具体的封装代码请看附件,这里只贴出封装后的使用代码:


const Zego = require('./zego/Zego.js');

var zim;
function onError(err) {
console.log("on error", err);
}
//发送消息
function sendZegoMsg(isToGroup, text, toID){
Zego.sendMsg(zim, isToGroup, text, toID, function (succ, err) {
if (!succ) {
console.log("回复即构消息发送失败:", msg, err);
}
})
}
//收到消息回调
function onRcvZegoMsg(isFromGroup, msg, fromUID) {
var rcvText = msg.message ;

}
function main() {
let zegoChatGPTUID = "chatgpt"
zim = Zego.initZego(onError, onRcvZegoMsg, zegoChatGPTUID);

}
main();

在收到消息时,判断是否有@chatgpt关键字,如果有的话提取消息内容,然后去调用chatGPT封装好的接口等待ChatGPT回复,并将回复的内容往聊天群里发送。


4 手机端加入群聊与ChatGPT聊天


有了PC端实现后,接下来在手机端只需通过即构IM SDK向群里面@chatgpt发送提问消息即可,当然了,也可以在一对一私聊的时候@chatgpt然后调用chatGPT接口。这些都是可以根据实际需求定制开发,篇幅原因,这里我们只将群聊。


同样的,我们只关注收发消息,因此对即构官方提供的SDK做了二次封装。如果想了解更多细节可以前往官方文档阅读。


对登录ZIM、创建Token等代码这里不详细描述,感兴趣读者可以查看代码附件,代码很简单容易看懂。


首先封装Msg对象,表示消息实体类:


public class Msg {
public String msg;
public long time;
public String toUID;
public String fromUID;
public MsgType type;

public enum MsgType {
P2P,
GROUP
}
}

发送消息二次封装,同一群聊和一对一聊天接口:


public static void sendMsg(ZIM zim, Msg msg, ZIMMessageSentCallback cb) {
// 发送“单聊”通信的信息

ZIMTextMessage zimMessage = new ZIMTextMessage();
zimMessage.message = msg.msg;

ZIMMessageSendConfig config = new ZIMMessageSendConfig();
// 消息优先级,取值为 低:1 默认,中:2,高:3
config.priority = ZIMMessagePriority.LOW;
// 设置消息的离线推送配置
ZIMPushConfig pushConfig = new ZIMPushConfig();
pushConfig.title = "离线推送的标题";
pushConfig.content = "离线推送的内容";
pushConfig.extendedData = "离线推送的扩展信息";
config.pushConfig = pushConfig;
if (msg.type == Msg.MsgType.P2P)
zim.sendPeerMessage(zimMessage, msg.toUID, config, cb);
else
zim.sendGroupMessage(zimMessage, msg.toUID, config, cb);
}

二次封装接收消息,统一通过onRcvMsg函数接收消息。


private void onRcvMsg(ArrayList<ZIMMessage> messageList) {
if (lsArr == null) return;
for (ZIMMessage zimMessage : messageList) {
if (zimMessage instanceof ZIMTextMessage) {
ZIMTextMessage zimTextMessage = (ZIMTextMessage) zimMessage;
if (zimMessage.getTimestamp() < this.startTime)
continue;
String fromUID = zimTextMessage.getSenderUserID();
ZIMConversationType ztype = zimTextMessage.getConversationType();
String toUID = zimTextMessage.getConversationID();
Msg.MsgType type = Msg.MsgType.P2P;
if (ztype == ZIMConversationType.PEER) type = Msg.MsgType.P2P;
else if (ztype == ZIMConversationType.GROUP) type = Msg.MsgType.GROUP;
String data = zimTextMessage.message;
Msg msg = new Msg(type, data, zimMessage.getTimestamp(), fromUID, toUID);
for (MsgCenterListener l : lsArr) l.onRcvMsg(msg);
}
}
}
private ZIMEventHandler handler = new ZIMEventHandler() {

@Override
public void onReceivePeerMessage(ZIM zim, ArrayList<ZIMMessage> messageList, String fromUserID) {
onRcvMsg(messageList);
}



@Override
public void onReceiveGroupMessage(ZIM zim, ArrayList<ZIMMessage> messageList, String fromGroupID) {
onRcvMsg(messageList);
}

@Override
public void onTokenWillExpire(ZIM zim, int second) {
onRenewToken();
}
};

需要注意的是,因为我们目前场景只需关注文本消息,因此没有图片、文件之类的消息做过多考虑。如果有类似需求的读者可以根据官方文档进一步封装。


另外,为了简化,避免每次用户主动拉chatgpt进入一个新群,我们先约好一个超大群ID:group_chatgpt。每次新用户登录就加入这个大群就好。如果有更加细粒度控制需求,可以根据不同用户来创建不同群,然后向chatgpt机器人发送群ID,在PC端开发对应的自动加入对应群功能就好。


对于加群逻辑,也做了二次封装:


public void joinGroup(String groupId) {
zim.joinGroup(groupId, new ZIMGroupJoinedCallback() {
@Override
public void onGroupJoined(ZIMGroupFullInfo groupInfo, ZIMError errorInfo) {
for (MsgCenterListener l : lsArr)
l.onJoinGroup(groupId);
}
});

至此,整个流程开发完成,尽情享受ChatGPT吧。


5 开发者福利


除ChatGPT之外,Demo中使用的开发者工具ZIM SDK也是提升工作效率的利器,ZIM SDK提供了全面的 IM 能力,满足文本、图片、语音等多种消息类型,在线人数无上限,支持亿量级消息并发。同时支持安全审核机制,确保消息安全合规。


ZIM SDK提供了快速集成、接口丰富、成熟的即时通讯解决方案。满足多种业务场景通讯需求,适用于打造大型直播、语聊房、客服系统等场景。即构即时通讯产品 IM 开春钜惠低至1折,限时折扣专业版1200元http://www.zego.im/activity/ze…,也可搭配元宇宙和直播间其他产品组合使用。感兴趣的开发者可到即构官网去注册体验doc-zh.zego.im/article/115…


6 完整代码



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

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

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

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



「随想录」


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



最近是怎么了


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



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



回忆那个时候


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



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


个人看法


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


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



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



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



做技术既然这样了


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



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



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


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


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


个人总结


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



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



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



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



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


回答问题


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


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



写在最后


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


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

25岁在培训班学java个人看法见解

前言 hello,大家好,我是小江。前段时间我发了一篇我在培训java感受的文章,大家都挺感兴趣的,也问了很多问题。这段时间我们刚好在写项目,昨天项目已经结束验收,现在有空在写一篇文章解答你们问的各种问题。首先说明,文章仅代表我个人观点,我个人的真实感受,而且...
继续阅读 »

前言


hello,大家好,我是小江。前段时间我发了一篇我在培训java感受的文章,大家都挺感兴趣的,也问了很多问题。这段时间我们刚好在写项目,昨天项目已经结束验收,现在有空在写一篇文章解答你们问的各种问题。首先说明,文章仅代表我个人观点,我个人的真实感受,而且我不会指明说出任何一个培训机构名,上篇文章大家要是看了应该知道,我全篇没有说任何一个培训机构名,我自己所在的也不会说。好了不多说,开始解答常见的问题吧。


1、行情不好了,还能入这行吗?


说实话,这个问题我也不知道怎么回答。确实现在行情越来越差了,要求越来越高了,学完找不到工作的肯定是有的,但我觉得吧,海投一直找不到工作的,要么就是技术太差,要么就是要求高了。可能前几年这技术和学历可以轻松找到1w的,现在技术要求高了点还只给6k,落差太大,就会不想去,然后发现其他的都是这样了,就抱怨起来了贩卖焦虑了。


为什么之前那些年好找工作,要求又低工资又高,只是躺在了时代的红利上。人都不是傻子,知道什么行业赚钱,必然会往这行拥入。人多了,那肯定是这样的结果。那到底现在能不能入这行呢?我个人觉得吧,你需要满足:


①学历要达到大专以上,最好是本科;

②年龄不要超过28岁,最好是26岁以下;

③自己能坚持,如果学技术要学好,能放下游戏等各种娱乐;


这是我个人看法,不知道大家有没有不同的意见,我认为一个24岁左右本科学历的刻苦学完编程,技术也可以,找个开发工作问题不大的。


2、入行是自学还是报班?


这个问题我不会提到任何一个培训机构名字,以免大家认为我是托。我来说说我的看法,我认为自制力强的、学习力OK的、时间充足的,完全可以自学的,我身边也有自学朋友。我入这行还是受我那朋友的启发和推荐的,我和我那朋友之前是在一个公司上班,不是it,干了大半年,他辞职了,说要搞编程,我没在意,觉得编程离我太远和我没有交集,又是将近半年过去了,我和他联系,他说他已经上班干开发了税后除去社保7K,我才开始和他聊这行,我也心动了。他说他自己就是自学的,大学专业搭点边,学了一点编程,但都忘了。他说他辞职后在家天天学习,除了吃饭就是学习,这点我是相信的,之前和他一个公司他也确实做事很认真。我后来不是也准备入行编程,我也想着学他自学,真正开始后,才知道人和人是有差距的,我心静不下,学一个小时就看不下去,所以我后来报了班。


这个问题,我的总结就是:

①自学可以也可行,关键看你有没有那毅力,每天能学8小时以上,并且是真正有效率的学了8小时,那你可以自学,网上资料都全,不会的可以查资料百度,或者花点小钱让别的大神帮你看看。


②自制力不行,又想入这行的,培训班也是一个选择,只是学费不便宜,成本大。培训班肯定是有用的,能学到东西,系统化都给你规划好了,资源齐全,讲的也细,也会讲很多企业实际开发需要注意的东西,练习题和项目都恰到好处的提升自己,不会不懂的可以随时问老师,最后模拟面试、指导简历什么都安排好了,确实能做到速成。不然也不会这么多培训出来的占满了这行业,使这行越来越卷。


③如果报班了,最好选个靠外那座位坐,不要坐里面,我就是坐里面靠墙,问老师问题都不好问,很坑。如果同桌是个技术很好的,那你就很幸运了,一个技术好的同桌太重要了。还有就是多问老师问题,你是花钱的,有问题就问,不要怕。


3、我在培训这两个多月后现在学的咋样?


我这一共四个阶段,我现在刚好学完第二个阶段,第一阶段基础javase,第二阶段javaweb,刚刚结束第二阶段做完项目,每个阶段结束都会有将近一个星期写项目。这次项目是小组一起开发,使用SVN工具小组一起自己分工一个写一个模块,最后整合,模拟企业开发。项目还可以,刚开始觉得很难,熟悉后就好了,难点不多,大都是crud,我感觉思路很重要,不要一上来就噼里啪啦开始写,先分析好,后面会越来越顺。等写完真的有成就感,项目确实很能锻炼自己,升华自己,查漏补缺。放几张照片看看我写的项目吧


页面显示效果截选:


image.png


后端代码截选:
image.png
前端代码截选:
image.png


结束语


我相信大家入行这个的,应该都和我一样,觉得工资还可以,工作看起来还体面。但入这行越久了解越深,就知道也有很多不容易,要付出很大的努力。如果刚踏入这行开始学的,一定要好好学,基础蛮重要,可能学着学着会迷茫,不要怀疑自己,多学多敲多思考,没有学不会的。最后送大家一句诗:路漫漫其修远兮,吾将上下而求索。


作者:学习编程的小江
来源:juejin.cn/post/7203294201312657469
收起阅读 »

改行后我在做什么?(2022-9-19日晚)

闲言碎语 今天回了趟家里,陪父母一起吃了个饭。父母照例是在唠叨,这个年纪了还不结婚,也没个稳定的工作,巴拉巴拉的一大堆。吃完饭我匆匆的就回到了我租住的地方。在现阶段,其实我对于父母所诉说的很多东西,我都是认同的。 但在我这个年纪,这个阶段,看似有很多选择...
继续阅读 »

闲言碎语



今天回了趟家里,陪父母一起吃了个饭。父母照例是在唠叨,这个年纪了还不结婚,也没个稳定的工作,巴拉巴拉的一大堆。吃完饭我匆匆的就回到了我租住的地方。在现阶段,其实我对于父母所诉说的很多东西,我都是认同的。




但在我这个年纪,这个阶段,看似有很多选择,但其实我没有选择。能做的也只是多挣点钱。




在这个信息爆炸的时代,我们知道更高的地方在哪里。但当你想要再往上走一步的时候,你发现你的上限,其实从出生或从你毕业的那一刻就已经注定了。可能少部分人通过自身的努力,的确能突破壁垒达到理想的高度。但这只是小概率事件罢了。在我看来整个社会的发展,其实早就已经陷入了一种怪圈。




在我,早些年刚刚进入社会的时候。那时的想法特别简单。就想着努力工作,努力提升自身的专业素养。被老板赏识,升职加薪成为一名管理者。如果,被淘汰了那应该是自己不够优秀,不够努力,专业技能不过硬,自己为人处事不够圆滑啥的。




内卷这个词语引爆网络的时候;当35岁被裁员成为常态的时候。再回头看我以前的那些想法那真的是一个笑话。(我觉得我可能是在为自己被淘汰找借口)



当前的状态



游戏工作室的项目,目前基本处于停滞的状态。我不敢加机器也不敢关机。有时候我都在想,是不是全中国那3-4亿的人都在搞这个?一个国外的游戏,金价直接拉成这个逼样。




汽配这边的话,只能说喝口稀饭。(我花了太多精力在游戏工作室上了)



梦想破灭咯



其实按照正常情况来说,游戏工作室最开始的阶段,我应该是能够稍微挣点钱的。我感觉我天时、地利、人和。我都占的。现在来看的话,其实我只占了人和。我自己可以编码,脚本还是从驱动层模拟键鼠,写的一套脚本。这样我都没赚钱,我擦勒。



接下来干嘛



接下来准备进厂打螺丝。(开玩笑的)
还是老老实实跟着我弟学着做生意吧。老老实实做汽配吧!在这个时代,好像有一技之长(尤其是IT)的人,好像并不能活得很好。除非,你这一技之长,特别特别长。(当下的中国不需要太多的这类专业技术人员吧。)



我感受到的大环境



我身边有蛮多的大牛。从他们的口中和我自己看到的。我感觉在IT这个领域,国内的环境太恶劣了。在前端,除开UI库,我用到的很多多的库全是老外的。为什么没有国人开源呢?因为,国人都忙着996了。我们可以在什么都不知道的情况下,通过复制粘贴,全局搜索解决大部分问题。 机械视觉、大数据分析、人工智能 等很多东西。这一切的基石很多年前就有了,为什么没人去研究他?为什么我们这波人,不断的在学习:这样、那样的框架。搭积木虽然很好玩。但创造一个积木,不应该也是一件更有挑战性的事情么?




在招聘网站还有一个特别奇怪的现象。看起来这家公司是在招人,但其实是培训机构。 看起来这家公司正儿八经是在招聘兼职,但其实只想骗你去办什么兼职卡。看起来是在招送快递,送外卖的,招聘司机的,但其实只是想套路你买车。我擦勒。这是怎样的一个恶劣的生存环境。这些个B人就不能干点,正经事?




卖菜的、拉车的、搞电商的、搞短视频、搞贷款的、卖保险的、这些个公司市值几百亿。很难看到一些靠创新,靠创造,靠产品质量,发展起来的公司。


作者:wjt
来源:juejin.cn/post/7144770465741946894

收起阅读 »

从framework角度看app保活问题

问题背景 最近在群里看到群友在讨论app保活的问题,回想之前做应用(运动类)开发时也遇到过类似的需求,于是便又来了兴趣,果断加入其中,和群友展开了激烈的讨论 不少群友的想法和我当初的想法一样,这特么保活不是看系统的心情么,系统想让谁活谁才能活,作为app开发...
继续阅读 »

问题背景


最近在群里看到群友在讨论app保活的问题,回想之前做应用(运动类)开发时也遇到过类似的需求,于是便又来了兴趣,果断加入其中,和群友展开了激烈的讨论


保活


不少群友的想法和我当初的想法一样,这特么保活不是看系统的心情么,系统想让谁活谁才能活,作为app开发者,根本无能为力,可真的是这样的吗?


保活方案


首先,我整理了从古到今,app开发者所使用过的以及当前还在使用的保活方式,主要思路有两个:保活和复活


保活的方案有:



  • 1像素惨案




  • 后台无声音乐




  • 前台service




  • 心跳机制




  • socket长连接




  • 无障碍服务




  • ......




复活的方案有:


  • 双进程守护(java层和native层)

  • JobScheduler定时任务

  • 推送/相互唤醒

  • ......


不难看出,app开发者为了能让自己的应用多存活一会儿,可谓是绞尽脑汁,但即使这样,随着Android系统升级,尤其是进入8.0之后,系统对应用的限制越来越高,传统的保活方式已经不生效,这让Android开发者手足无措,于是乎,出现了一种比较和谐的保活方式:



  • 引导用户开启手机白名单


这也是目前绝大多数应用所采用的的方式,相对于传统黑科技而言,此方式显得不那么流氓,比较容易被用户所接受。


但跟微信这样的国民级应用比起来,保活效果还是差了一大截,那么微信是怎么实现保活的呢?或者回到我们开头的问题,应用的生死真的只能靠系统调度吗?开发者能否干预控制呢?


进程调度原则


解开这个疑问之前,我们需要了解一下Android系统进程调度原则,主要介绍framework中承载四大组件的进程是如何根据组件状态而动态调节自身状态的。进程有两个比较重要的状态值:




  • oom_adj,定义在frameworks/base/services/core/java/com/android/server/am/ProcessList.java当中




  • procState,定义在frameworks/base/core/java/android/app/ActivityManager.java当中




OOM_ADJ

以Android10的源码为例,oom_adj划分为20级,取值范围[-10000,1001],Android6.0以前的取值范围是[-17,16]




  • oom_adj值越大,优先级越低




  • oom_adj<0的进程都是系统进程。




public final class ProcessList {
static final String TAG = TAG_WITH_CLASS_NAME ? "ProcessList" : TAG_AM;

// The minimum time we allow between crashes, for us to consider this
// application to be bad and stop and its services and reject broadcasts.
static final int MIN_CRASH_INTERVAL = 60 * 1000;

// OOM adjustments for processes in various states:

// Uninitialized value for any major or minor adj fields
static final int INVALID_ADJ = -10000;

// Adjustment used in certain places where we don't know it yet.
// (Generally this is something that is going to be cached, but we
// don't know the exact value in the cached range to assign yet.)
static final int UNKNOWN_ADJ = 1001;

// This is a process only hosting activities that are not visible,
// so it can be killed without any disruption.
static final int CACHED_APP_MAX_ADJ = 999;
static final int CACHED_APP_MIN_ADJ = 900;

// This is the oom_adj level that we allow to die first. This cannot be equal to
// CACHED_APP_MAX_ADJ unless processes are actively being assigned an oom_score_adj of
// CACHED_APP_MAX_ADJ.
static final int CACHED_APP_LMK_FIRST_ADJ = 950;

// Number of levels we have available for different service connection group importance
// levels.
static final int CACHED_APP_IMPORTANCE_LEVELS = 5;

// The B list of SERVICE_ADJ -- these are the old and decrepit
// services that aren't as shiny and interesting as the ones in the A list.
static final int SERVICE_B_ADJ = 800;

// This is the process of the previous application that the user was in.
// This process is kept above other things, because it is very common to
// switch back to the previous app. This is important both for recent
// task switch (toggling between the two top recent apps) as well as normal
// UI flow such as clicking on a URI in the e-mail app to view in the browser,
// and then pressing back to return to e-mail.
static final int PREVIOUS_APP_ADJ = 700;

// This is a process holding the home application -- we want to try
// avoiding killing it, even if it would normally be in the background,
// because the user interacts with it so much.
static final int HOME_APP_ADJ = 600;

// This is a process holding an application service -- killing it will not
// have much of an impact as far as the user is concerned.
static final int SERVICE_ADJ = 500;

// This is a process with a heavy-weight application. It is in the
// background, but we want to try to avoid killing it. Value set in
// system/rootdir/init.rc on startup.
static final int HEAVY_WEIGHT_APP_ADJ = 400;

// This is a process currently hosting a backup operation. Killing it
// is not entirely fatal but is generally a bad idea.
static final int BACKUP_APP_ADJ = 300;

// This is a process bound by the system (or other app) that's more important than services but
// not so perceptible that it affects the user immediately if killed.
static final int PERCEPTIBLE_LOW_APP_ADJ = 250;

// This is a process only hosting components that are perceptible to the
// user, and we really want to avoid killing them, but they are not
// immediately visible. An example is background music playback.
static final int PERCEPTIBLE_APP_ADJ = 200;

// This is a process only hosting activities that are visible to the
// user, so we'd prefer they don't disappear.
static final int VISIBLE_APP_ADJ = 100;
static final int VISIBLE_APP_LAYER_MAX = PERCEPTIBLE_APP_ADJ - VISIBLE_APP_ADJ - 1;

// This is a process that was recently TOP and moved to FGS. Continue to treat it almost
// like a foreground app for a while.
// @see TOP_TO_FGS_GRACE_PERIOD
static final int PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ = 50;

// This is the process running the current foreground app. We'd really
// rather not kill it!
static final int FOREGROUND_APP_ADJ = 0;

// This is a process that the system or a persistent process has bound to,
// and indicated it is important.
static final int PERSISTENT_SERVICE_ADJ = -700;

// This is a system persistent process, such as telephony. Definitely
// don't want to kill it, but doing so is not completely fatal.
static final int PERSISTENT_PROC_ADJ = -800;

// The system process runs at the default adjustment.
static final int SYSTEM_ADJ = -900;

// Special code for native processes that are not being managed by the system (so
// don't have an oom adj assigned by the system).
static final int NATIVE_ADJ = -1000;

// Memory pages are 4K.
static final int PAGE_SIZE = 4 * 1024;

//省略部分代码
}

ADJ级别取值说明(可参考源码注释)
INVALID_ADJ-10000未初始化adj字段时的默认值
UNKNOWN_ADJ1001缓存进程,无法获取具体值
CACHED_APP_MAX_ADJ999不可见activity进程的最大值
CACHED_APP_MIN_ADJ900不可见activity进程的最小值
CACHED_APP_LMK_FIRST_ADJ950lowmemorykiller优先杀死的级别值
SERVICE_B_ADJ800旧的service的
PREVIOUS_APP_ADJ700上一个应用,常见于应用切换场景
HOME_APP_ADJ600home进程
SERVICE_ADJ500创建了service的进程
HEAVY_WEIGHT_APP_ADJ400后台的重量级进程,system/rootdir/init.rc文件中设置
BACKUP_APP_ADJ300备份进程
PERCEPTIBLE_LOW_APP_ADJ250受其他进程约束的进程
PERCEPTIBLE_APP_ADJ200可感知组件的进程,比如背景音乐播放
VISIBLE_APP_ADJ100可见进程
PERCEPTIBLE_RECENT_FOREGROUND_APP_ADJ50最近运行的后台进程
FOREGROUND_APP_ADJ0前台进程,正在与用户交互
PERSISTENT_SERVICE_ADJ-700系统持久化进程已绑定的进程
PERSISTENT_PROC_ADJ-800系统持久化进程,比如telephony
SYSTEM_ADJ-900系统进程
NATIVE_ADJ-1000native进程,不受系统管理

可以通过cat /proc/进程id/oom_score_adj查看目标进程的oom_adj值,例如我们查看电话的adj


dialer_oom_adj


值为935,处于不可见进程的范围内,当我启动电话app,再次查看


dialer_oom_adj_open


此时adj值为0,也就是正在与用户交互的进程


ProcessState

process_state划分为23类,取值范围为[-1,21]


@SystemService(Context.ACTIVITY_SERVICE)
public class ActivityManager {
//省略部分代码
/** @hide Not a real process state. */
public static final int PROCESS_STATE_UNKNOWN = -1;

/** @hide Process is a persistent system process. */
public static final int PROCESS_STATE_PERSISTENT = 0;

/** @hide Process is a persistent system process and is doing UI. */
public static final int PROCESS_STATE_PERSISTENT_UI = 1;

/** @hide Process is hosting the current top activities. Note that this covers
* all activities that are visible to the user. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_TOP = 2;

/** @hide Process is hosting a foreground service with location type. */
public static final int PROCESS_STATE_FOREGROUND_SERVICE_LOCATION = 3;

/** @hide Process is bound to a TOP app. This is ranked below SERVICE_LOCATION so that
* it doesn't get the capability of location access while-in-use. */

public static final int PROCESS_STATE_BOUND_TOP = 4;

/** @hide Process is hosting a foreground service. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 5;

/** @hide Process is hosting a foreground service due to a system binding. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 6;

/** @hide Process is important to the user, and something they are aware of. */
public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 7;

/** @hide Process is important to the user, but not something they are aware of. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 8;

/** @hide Process is in the background transient so we will try to keep running. */
public static final int PROCESS_STATE_TRANSIENT_BACKGROUND = 9;

/** @hide Process is in the background running a backup/restore operation. */
public static final int PROCESS_STATE_BACKUP = 10;

/** @hide Process is in the background running a service. Unlike oom_adj, this level
* is used for both the normal running in background state and the executing
* operations state. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_SERVICE = 11;

/** @hide Process is in the background running a receiver. Note that from the
* perspective of oom_adj, receivers run at a higher foreground level, but for our
* prioritization here that is not necessary and putting them below services means
* many fewer changes in some process states as they receive broadcasts. */

@UnsupportedAppUsage
public static final int PROCESS_STATE_RECEIVER = 12;

/** @hide Same as {@link #PROCESS_STATE_TOP} but while device is sleeping. */
public static final int PROCESS_STATE_TOP_SLEEPING = 13;

/** @hide Process is in the background, but it can't restore its state so we want
* to try to avoid killing it. */

public static final int PROCESS_STATE_HEAVY_WEIGHT = 14;

/** @hide Process is in the background but hosts the home activity. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_HOME = 15;

/** @hide Process is in the background but hosts the last shown activity. */
public static final int PROCESS_STATE_LAST_ACTIVITY = 16;

/** @hide Process is being cached for later use and contains activities. */
@UnsupportedAppUsage
public static final int PROCESS_STATE_CACHED_ACTIVITY = 17;

/** @hide Process is being cached for later use and is a client of another cached
* process that contains activities. */

public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 18;

/** @hide Process is being cached for later use and has an activity that corresponds
* to an existing recent task. */

public static final int PROCESS_STATE_CACHED_RECENT = 19;

/** @hide Process is being cached for later use and is empty. */
public static final int PROCESS_STATE_CACHED_EMPTY = 20;

/** @hide Process does not exist. */
public static final int PROCESS_STATE_NONEXISTENT = 21;
//省略部分代码
}

state级别取值说明(可参考源码注释)
PROCESS_STATE_UNKNOWN-1不是真正的进程状态
PROCESS_STATE_PERSISTENT0持久化的系统进程
PROCESS_STATE_PERSISTENT_UI1持久化的系统进程,并且正在操作UI
PROCESS_STATE_TOP2处于栈顶Activity的进程
PROCESS_STATE_FOREGROUND_SERVICE_LOCATION3运行前台位置服务的进程
PROCESS_STATE_BOUND_TOP4绑定到top应用的进程
PROCESS_STATE_FOREGROUND_SERVICE5运行前台服务的进程
PROCESS_STATE_BOUND_FOREGROUND_SERVICE6绑定前台服务的进程
PROCESS_STATE_IMPORTANT_FOREGROUND7对用户很重要的前台进程
PROCESS_STATE_IMPORTANT_BACKGROUND8对用户很重要的后台进程
PROCESS_STATE_TRANSIENT_BACKGROUND9临时处于后台运行的进程
PROCESS_STATE_BACKUP10备份进程
PROCESS_STATE_SERVICE11运行后台服务的进程
PROCESS_STATE_RECEIVER12运动广播的后台进程
PROCESS_STATE_TOP_SLEEPING13处于休眠状态的进程
PROCESS_STATE_HEAVY_WEIGHT14后台进程,但不能恢复自身状态
PROCESS_STATE_HOME15后台进程,在运行home activity
PROCESS_STATE_LAST_ACTIVITY16后台进程,在运行最后一次显示的activity
PROCESS_STATE_CACHED_ACTIVITY17缓存进程,包含activity
PROCESS_STATE_CACHED_ACTIVITY_CLIENT18缓存进程,且该进程是另一个包含activity进程的客户端
PROCESS_STATE_CACHED_RECENT19缓存进程,且有一个activity是最近任务里的activity
PROCESS_STATE_CACHED_EMPTY20空的缓存进程,备用
PROCESS_STATE_NONEXISTENT21不存在的进程

进程调度算法

frameworks/base/services/core/java/com/android/server/am/OomAdjuster.java中,有三个核心方法用于计算和更新进程的oom_adj值



  • updateOomAdjLocked():更新adj,当目标进程为空,或者被杀则返回false,否则返回true。

  • computeOomAdjLocked():计算adj,计算成功返回true,否则返回false。

  • applyOomAdjLocked():应用adj,当需要杀掉目标进程则返回false,否则返回true。


adj更新时机

也就是updateOomAdjLocked()被调用的时机。通俗的说,只要四大组件被创建或者状态发生变化,或者当前进程绑定了其他进程,都会触发adj更新,具体可在源码中查看此方法被调用的地方,比较多,这里就不列举了


adj的计算过程

computeOomAdjLocked()计算过程相当复杂,将近1000行代码,这里就不贴了,有兴趣可自行查看,总体思路就是根据当前进程的状态,设置对应的adj值,因为状态值很多,所以会有很多个if来判断每个状态是否符合,最终计算出当前进程属于哪种状态。


adj的应用

计算得出的adj值将发送给lowmemorykiller(简称lmk),由lmk来决定进程的生死,不同的厂商,lmk的算法略有不同,下面是源码中对lmk的介绍


/* drivers/misc/lowmemorykiller.c
*
* The lowmemorykiller driver lets user-space specify a set of memory thresholds
* where processes with a range of oom_score_adj values will get killed. Specify
* the minimum oom_score_adj values in
* /sys/module/lowmemorykiller/parameters/adj and the number of free pages in
* /sys/module/lowmemorykiller/parameters/minfree. Both files take a comma
* separated list of numbers in ascending order.
*
* For example, write "0,8" to /sys/module/lowmemorykiller/parameters/adj and
* "1024,4096" to /sys/module/lowmemorykiller/parameters/minfree to kill
* processes with a oom_score_adj value of 8 or higher when the free memory
* drops below 4096 pages and kill processes with a oom_score_adj value of 0 or
* higher when the free memory drops below 1024 pages.
*
* The driver considers memory used for caches to be free, but if a large
* percentage of the cached memory is locked this can be very inaccurate
* and processes may not get killed until the normal oom killer is triggered.
*
* Copyright (C) 2007-2008 Google, Inc.
*
* This software is licensed under the terms of the GNU General Public
* License version 2, as published by the Free Software Foundation, and
* may be copied, distributed, and modified under those terms.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
*/


保活核心思路


根据上面的Android进程调度原则得知,我们需要尽可能降低app进程的adj值,从而减少被lmk杀掉的可能性,而我们传统的保活方式最终目的也是降低adj值。而根据adj等级分类可以看出,通过应用层的方式最多能将adj降到100~200之间,我分别测试了微信、支付宝、酷狗音乐,启动后返回桌面并息屏,测试结果如下


微信测试结果:


weixin_oom_adj


微信创建了两个进程,查看这两个进程的adj值均为100,对应为adj等级表中的VISIBLE_APP_ADJ,此结果为测试机上微信未登录状态测试结果,当换成我的小米8测试后发现,登录状态下的微信有三个进程在运行


weixin_login_oom_adj


后查阅资料得知,进程名为com.tencent.soter.soterserver的进程是微信指纹支付,此进程的adj值居然为-800,上面我们说过,adj小于0的进程为系统进程,那么微信是如何做到创建一个系统进程的,我和我的小伙伴都惊呆了o.o,为此,我对比了一下支付宝的测试结果


支付宝测试结果:


alipay_oom_adj


支付宝创建了六个进程,查看这六个进程的adj值,除了一个为915,其余均为0,怎么肥事,0就意味着正在与用户交互的前台进程啊,我的世界要崩塌了,只有一种可能,支付宝通过未知的黑科技降低了adj值。


酷狗测试结果:


kugou_oom_adj.png


酷狗创建了两个进程,查看这两个进程的adj值分别为700、200,对应为adj等级表中的PREVIOUS_APP_ADJPERCEPTIBLE_APP_ADJ,还好,这个在意料之中。


测试思考


通过上面三个app的测试结果可以看出,微信和支付宝一定是使用了某种保活手段,让自身的adj降到最低,尤其是微信,居然可以创建系统进程,简直太逆天了,这是应用层绝对做不到的,一定是在native层完成的,但具体什么黑科技就不得而知了,毕竟反编译技术不是我的强项。


正当我郁郁寡欢之时,我想起了前两天看过的一篇文章《当 App 有了系统权限,真的可以为所欲为?》,文章讲述了第三方App如何利用CVE漏洞获取到系统权限,然后神不知鬼不觉的干一些匪夷所思的事儿,这让我茅塞顿开,或许这些大厂的app就是利用了系统漏洞来保活的,不然真的就说不通了,既然都能获取到系统权限了,那创建个系统进程不是分分钟的事儿吗,还需要啥厂商白名单。


总结


进程保活是一把双刃剑,增加app存活时间的同时牺牲的是用户手机的电量,内存,cpu等资源,甚至还有用户的忍耐度,作为开发者一定要合理取舍,不要为了保活而保活,即使需要保活,也尽量采用白色保活手段,别让用户手机变板砖,然后再来哭爹骂娘。


参考资料:


探讨Android6.0及以上系统APP常驻内存(保活)实现-争宠篇


探讨Android6.0及以上系统APP常驻内存(保活)实现-复活篇


探讨一种新型的双进程守护应用保活


史上最强Android保活思路:深入剖析腾讯TIM的进程永生技术


当 App 有了系统权限,真的可以为所欲为?


「 深蓝洞察 」2022 年度最“不可赦”漏洞


作者:小迪vs同学
来源:juejin.cn/post/7210375037114138680
收起阅读 »

上千行代码的输入框的逻辑是什么?

web
需求 我们要做一个前端需求:需要一个输入框,支持 KQL 语法,支持智能匹配,前提条件纯前端实现。 该功能详见 kibana es7版本。有条件的可以去使用一下,感受一番。 需求分析 使用了一下该功能,感觉还是挺复杂的。不好实现啊,我,我,我。。。 不过因为...
继续阅读 »

需求


我们要做一个前端需求:需要一个输入框,支持 KQL 语法,支持智能匹配,前提条件纯前端实现。


该功能详见 kibana es7版本。有条件的可以去使用一下,感受一番。


image.png


需求分析


使用了一下该功能,感觉还是挺复杂的。不好实现啊,我,我,我。。。


不过因为 kibana 是开源的,我就去 github 上看了一下源码。



  • 首先人家是 React 版本,我的项目是 Vue 版本,我不能行使拿来主义。

  • 一个 input 框的核心代码写了一千多行,不包括一些工具函数,公共组件之类。


方案




  1. 我先研究源码,再把研究好的源码转成 vue 版本输出?


    该方案短时间内看不到效果,需要好好梳理其源码。是一个 0 或者 1 的问题,如果研究好了并实现转化出来,那就是 1,如果期间遇到问题阻塞了,那就是短时间看不到产出效果。不敢冒险。




  2. 创建一个 React 项目,把相关的这部分代码拆分出来,以微前端的方式内嵌到我的项目中?


    不知道在拆分代码和组装代码的过程中会遇到什么问题?未知,不敢冒险去耽误时间,也是一个 0 或者 1 的问题。




  3. 自己研究 KQL 语法,自己摸索规则,自己实现其逻辑?


    由于项目排期紧张,不敢太过冒险,就选择了自研。起码 ld 能看到进度。😁




image.png


image.png


image.png


我最后选择的是方案3:自研。但是如果有时间,我更倾向的想去尝试方案1 和方案2。


针对自研方案,我们就开干吧!撸起袖子加油干!😄


准备


首先,我们需要一些准备工作,我需要了解 KQL 语法是什么?然后使用它,研究其规则,梳理其逻辑。


Kibana 查询语言 (Kibana Query Language、简称 KQL) 是一种使用自由文本搜索或基于字段的搜索过滤 Elasticsearch 数据的简单语法。 KQL 仅用于过滤数据,并没有对数据进行排序或聚合的作用。


KQL 能够在您键入时建议字段名称、值和运算符。 建议的性能由 Kibana 设置控制。


KQL 能够查询嵌套字段和脚本字段。 KQL 不支持正则表达式或使用模糊术语进行搜索。


更为详细的可以看官方文档 Kibana Query Language



  • key method value 标准单个语句

  • key method value OR/AND key method value OR/AND key method value .... 标准多个语句

  • key OR/AND key OR/AND key OR/AND key method value .... 不标准多个语句

  • ......



Tips:key(字段名称)、method(运算符)、value(值)



实现


textarea



  • 由于用户可以输入多行文本信息,所以需要 textarea。type="textarea"

  • 为了用户能清楚看到输入内容,以及input 的美观,初始行数 :rows="2"

  • 因为我们能支持关键字和KQL两种情况,所以 placeholder="KQL/关键字"

  • 获取焦点需要打开下拉展示框 @focus="dropStatus = true"

  • 失去焦点且没有操作下拉选项则关闭下拉框 @blur="changeDrop"

  • 由于下拉框的位置需要跟着 textarea 高度变化,所以 v-resizeHeight="inputResizeHeight"


<el-input
v-resizeHeight="inputResizeHeight"
id="searchInputID"
ref="searchInputRef"
v-model="input"
:rows="2"
type="textarea"
placeholder="KQL/关键字"
class="searchInput"
@blur="changeDrop"
@focus="dropStatus = true"
>

</el-input>

changeDrop 需要判断用户是否正在操作下拉框内容,如果是,就不要关闭。这块你会怎么实现呢?可以先思考自己的实现方式,再看下边是我个人的实现方式。


其实理论就是给下拉框操作的时候增加标记,在失去焦点要关闭的时候,判断是否有这个标记,如果有,就不要关闭,否则就关闭。但这个标记又不能影响真正的失焦状态下关闭动作。


我想到的就是定时器,定时器能增加一个变量,同时还能自动销毁。具体的实现方式:


// 不关闭下拉框标记
noCloseInput() {
this.$refs.searchInputRef.focus()
if (this.timer) clearInterval(this.timer)
let time = 500
this.timer = setInterval(() => {
time -= 100
if (time === 0) {
clearInterval(this.timer)
this.timer = null
}
}, 100)
}

// 失焦操作
changeDrop() {
setTimeout(() => {
if (!this.timer) this.dropStatus = false
}, 200)
}

这么做需要有以下几点注意:



  • 失焦操作因为需要切换到下拉框有一定延迟需要定时器,而定时器的时间必须小于标记里边的定时器时间

  • 定时器 this.timer = setInterval() 中 this.timer 是定时器的 id

  • clearInterval(this.timer) 只会清除定时器,不会清空 this.timer


v-resizeHeight="inputResizeHeight" 这个是我写的一个自定义指令来检测元素的高度变化的,不知道你有什么好的方法吗?有的话请请共享一下,😍


const resizeHeight = {
// 绑定时调用
bind(el, binding) {
let height = ''
function isResize() {
// 可根据需求,调整内部代码,利用 binding.value 返回即可
const style = document.defaultView.getComputedStyle(el)
if (height !== style.height) {
// 此处关键代码,通过此处代码将数据进行返回,从而做到自适应
binding.value({ height: style.height })
}
height = style.height
}
// 设置调用函数的延时,间隔过短会消耗过多资源
el.__vueSetInterval__ = setInterval(isResize, 100)
},
unbind(el) {
clearInterval(el.__vueSetInterval__)
}
}

export default resizeHeight

下拉面板


image.png


下拉框是左右布局,右侧是检索语法说明的静态文案,可忽略。左侧是语句提示内容。


语句提示内容经过研究其实有四种:


key(字段名称)、method(运算符)、value(值)、connectionSymbol(连接符)


由于可能会有多个语句,其实我们是只对当前语句进行提示的,所以我们只分析当前语句的情况。


// 当前语句详情
{
cur_fields: '', // 当前 key
cur_methods: '', // 当前 method
cur_values: '', // 当前 value
cur_input: '' // 当前用户输入内容,可模糊匹配检索
}

有四部分,肯定就是需要在符合条件的情况下分别展示对应的 options 面板内容。


那判断条件就是如下图,其中后续需要注意的就是这几个判断条件的值赋值场景要准确。


image.png


语法分析器


想处理输入内容,做一个语法分析器,首先需要去监听用户的输入,那么就用 vue 提供的 watch。


watch: {
input: debounce(function(newValue, oldValue) {
if (newValue !== oldValue) this.dealInputShow(newValue)
}, 500)
}

基本大概思路如下:


KQL语法分析器.png


其中获取输入框的光标位置的方法如下:


const selectionStart = this.$refs.searchInputRef.$el.children[0].selectionStart

修改完了之后,光标会自动跑的最后,这样有点违反用户操作逻辑,所以需要设置一下光标位置:


if (this.endValue) {
this.$nextTick(() => {
const dom = this.$refs.searchInputRef.$el.children[0]
dom.setSelectionRange(this.input.length, this.input.length)
this.input += this.endValue
})
}

还有面板里边有四项内容,那每一项内容选择都可以通过鼠标点击选择,点击选择后,就需要按照规则处理一下,进行最终的字符串 this.input 拼接,得到最终结果。


// 当前 key 点击选择
curFieldClick(str) {},

// 当前 method 点击选择
curMethodClick(str) {},

// 当前 value 点击选择
curValueClick(str) {},

// 当前 链接符 点击选择
curConnectClick(str) {},

这部分需要注意的就是点击面板 input 会失去焦点,就加上前边说到的 noCloseInput() 不关闭下拉面板标记。


键盘快捷操作


必备的目前就 3 个事件 enter、up、down,其他算是锦上添花,由于排期紧张,暂时只做了必备的 3 个 事件:


<el-input
v-resizeHeight="inputResizeHeight"
id="searchInputID"
ref="searchInputRef"
v-model="input"
:rows="2"
type="textarea"
placeholder="KQL/关键字"
class="searchInput"
@blur="changeDrop"
@focus="dropStatus = true"
@keydown.enter.native.capture.prevent="getSearchEnter($event)"
@keydown.up.native.capture.prevent="getSearchUp($event)"
@keydown.down.native.capture.prevent="getSearchDown($event)"
>

</el-input>

那么,我们的这几个键盘事件都需要怎么处理呢??接下来就直接上代码简单分析一下:


// 键盘 enter 事件,有两种情况
// 一种就是 选择内容,第二种就是 相当于回车事件直接触发接口
getSearchEnter(event) {
event.preventDefault()

// 当前下拉面板的展示的 options
const suggestions = this.get_suggestions()

// 满足可以选的条件
if (this.dropStatus && this.dropIndex !== null && suggestions[this.dropIndex]) {
// 光标之后是否有内容,有就需要截取处理
// ......

// 当前项是否是手动输入的,需要做截取处理
// .......

// 拼接 enter 键选择的选项
this.input += suggestions[this.dropIndex] + ' '

// 光标之后是否有内容,就需要设置光标在当前操作位置,并拼接之前截取掉的光标后的内容
// .......

// 设置当前语法区域的各个当前项 cur_fields、cur_methods、cur_values
// ......

// 恢复键盘 up、down 选择初始值
this.dropIndex = 0
} else {
// 不满足选的条件,就关闭选择面板,并触发检索查询接口
this.dropStatus = false
this.$emit('getSearchData', 2)
}
},

// 键盘 up 事件
getSearchUp(event) {
event.preventDefault()

// 满足上移,就做 dropIndex 减法
if (this.dropStatus && this.dropIndex !== null) {
this.decrementIndex(this.dropIndex)
}
},

// 键盘 down 事件
getSearchDown(event) {
event.preventDefault()

// 满足下移,就做 dropIndex 加法
if (this.dropStatus && this.dropIndex !== null) {
this.incrementIndex(this.dropIndex)
}
},

// 加法,注意边界问题
incrementIndex(currentIndex) {
let nextIndex = currentIndex + 1
const suggestions = this.get_suggestions()
// 到最后边,重置到第一个,形成循环
if (currentIndex === null || nextIndex >= suggestions.length) {
nextIndex = 0
}
this.dropIndex = nextIndex

// 被选择的选项如果不在可视范围之内,需要滚动到可视区
this.$nextTick(() => this.scrollToOption())
},

// 减法,注意边界问题
decrementIndex(currentIndex) {
const previousIndex = currentIndex - 1
const suggestions = this.get_suggestions()
// 到最前边,重置到最后,形成循环
if (previousIndex < 0) {
this.dropIndex = suggestions.length - 1
} else {
this.dropIndex = previousIndex
}

// 被选择的选项如果不在可视范围之内,需要滚动到可视区
this.$nextTick(() => this.scrollToOption())
},

键盘事件的核心逻辑上述基本说清楚了,那么其中需要注意的一个点,那就是被选择的选项如果不在可视范围之内,需要滚动到可视区,这样可提高用户体验。那这块到底怎么做呢?其实实现起来还挺有意思的。


import scrollIntoView from './scroll-into-view'

// 滚动 optiosns 区域,保持在可视区域
scrollToOption() {
if (this.dropStatus === true) {
const target = document.getElementsByClassName('drop-active')[0]
const menu = document.getElementsByClassName('search-drop__left')[0]
scrollIntoView(menu, target)
}
},

scroll-into-view.js 内容如下:


export default function scrollIntoView(container, selected) {
// 如果当前激活 active 元素不存在
if (!selected) {
container.scrollTop = 0
return
}

const offsetParents = []
let pointer = selected.offsetParent
while (pointer && container !== pointer && container.contains(pointer)) {
offsetParents.push(pointer)
pointer = pointer.offsetParent
}

const top = selected.offsetTop + offsetParents.reduce((prev, curr) => (prev + curr.offsetTop), 0)
const bottom = top + selected.offsetHeight
const viewRectTop = container.scrollTop
const viewRectBottom = viewRectTop + container.clientHeight

if (top < viewRectTop) {
container.scrollTop = top
} else if (bottom > viewRectBottom) {
container.scrollTop = bottom - container.clientHeight
}
}

针对上述内容几个技术点做出简单解释:


offsetParent:就是距离该子元素最近的进行过定位的父元素,如果其父元素中不存在定位则 offsetParent为:body元素。


offsetParent 根据定义分别存在以下几种情况:



  1. 元素自身有 fixed 定位,父元素不存在定位,则 offsetParent 的结果为 null(firefox 中为:body,其他浏览器返回为 null)

  2. 元素自身无 fixed 定位,且父元素也不存在定位,offsetParent 为 <body> 元素

  3. 元素自身无 fixed 定位,且父元素存在定位,offsetParent 为离自身最近且经过定位的父元素

  4. <body>元素的 offsetParent 是 null


offsetTop:元素到 offsetParent 顶部的距离


image.png


offsetHeight:是一个只读属性,它返回该元素的像素高度,高度包含内边距(padding)和边框(border),不包含外边距(margin),是一个整数,单位是像素 px。


通常,元素的 offsetHeight 是一种元素 CSS 高度的衡量标准,包括元素的边框、内边距和元素的水平滚动条(如果存在且渲染的话),不包含 :before或 :after 等伪类元素的高度。


image.png


scrollTop:可以获取或设置一个元素的内容垂直滚动的像素数。


一个元素的 scrollTop 值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop 值为0。


clientHeight:是一个只读属性,它返回该元素的像素高度,高度包含内边距(padding),不包含边框(border),外边距(margin)和滚动条,是一个整数,单位是像素 px。


clientHeight 可以通过 CSS height + CSS padding - 水平滚动条高度 (如果存在)来计算。


image.png


最全各个属性相关图如下:


image.png


效果


效果怎么说呢,也算顺利上线生产环境了,在此截图几张,给大家看看效果。


image.png


image.png


image.png


image.png


小结


做这个需求,最难的点是要求自己去研究 KQL 的语法规则,以及使用方式,然后总结规则,写出自己的词法分析器。


其中有什么技术难点吗?似乎并没有,都是各种判断条件,最简单的 if-else。


所以想告诉大家的是,不要一心只钻研技术,在做业务的时候也需要好好梳理业务,做一个懂业务的技术人。业务和技术互相成就!


最后,如果感到本文还可以,请给予支持!来个点赞、评论、收藏三连,万分感谢!😄🙏


作者:Bigger
来源:juejin.cn/post/7210593177820676154
收起阅读 »

ChatGPT API入门探索,制作AI孙子

最近ChatGPT的热度非常高,OPEN AI也是在不久前刚刚宣布开放ChatGPT商用API。 写这篇文章的契机在于前几天一位群友分享了一个日本程序员基于ChatGPT开发了一个AI佛:HOTOKE AI 例如当用户提问“我不知道生存的目的”,AI佛会做出如...
继续阅读 »

最近ChatGPT的热度非常高,OPEN AI也是在不久前刚刚宣布开放ChatGPT商用API。


写这篇文章的契机在于前几天一位群友分享了一个日本程序员基于ChatGPT开发了一个AI佛:HOTOKE AI


例如当用户提问“我不知道生存的目的”,AI佛会做出如下回答:


image.png


感觉还是挺有意思的,他会从佛教及心理学的角度去给你分析与解释。


但作为一名程序员,我们肯定对背后的原理更感兴趣,于是我又问了他和GPT有什么区别:


image.png


这个回答还是有点出乎我的意料的,于是我更感兴趣了,直接网上找起了教程,也研读了一下OPEN API的文档。所以这篇文章更像是一个学习笔记,也希望能吸引到一些同样感兴趣的小伙伴。


文章最后我们会开发一个基于ChatGPT的AI孙子(就是培养出高启强的那个孙子)


ChatGPT


相信大家能点进来的都知道ChatGPT是啥, 这part就过了吧。


OPEN AI API


Examples


OPEN AI API提供了很多种模型供用户选择,在官网上也提供了很多示例,包括各种各样的模型及其生成的结果。
这是官网示例的地址:platform.openai.com/examples


image.png


我们以Q&A为例:


image.png
可以看到它用的模型是text-davinci-003,然后他说"我是一个高度智能的问答机器人。如果你问的问题有确切答案,我就会给出答案,如果你问的问题是在搞我或者没有明确答案,我会回答不知道"。然后下面就是一些示例。


提示工程(Prompt Engineering)


我们还可以点右上角的Open in Playground来自己尝试下


image.png
可以看到右侧侧边栏有许多参数可以调节,根据调节的参数不同,生成的答案也会有所区别。参数就不细说了,感兴趣的同学可以自己去看,当你鼠标移到那个参数上时,会有对应的解释框弹出来解释这个参数的作用。


这里简单演示了如何去通过提示,引导AI之后回答出你所想要的答案,所以提示(Prompt)就变的尤为重要。事实上,现在提示工程师(Prompt Engineer)已经成为一个炙手可热的岗位,他们的主要任务就是引导AI去学习及训练。
官方文档也用了相当大的篇幅去介绍如何去提示:platform.openai.com/docs/guides…


这里就简单翻译一下开头:


概述

我们的模型可以做任何事情,从生成原创故事到执行复杂的文本分析。因为它们可以做这么多事情,所以你必须明确描述你想要什么。告诉他该怎么做,而不只是简单陈述,这就是一个好提示的秘籍。


创作提示有三个基本准则。




  • 展示和讲述。通过指示、例子或两者的结合,明确你想要什么。如果你想让模型按字母顺序排列一个项目清单,或按情绪对一个段落进行分类,就向它展示你想要的东西。




  • 提供高质量的数据。如果你试图建立一个分类器或让模型遵循一个模式,确保有足够的例子。确保校对你的例子 —— 模型足够聪明,它可以识别拼写错误,并且告诉你,但它也可能认为这是你故意写错的,这都会影响他的回答。




  • 检查你的设置。temperature和top_p参数控制了模型在产生响应方面的确定性。如果你要求它产生一个只有一个正确答案的响应,那么你就想把这些设置得低一些。如果你在寻找更多不同的反应,那么你可能想把它们设置得更高。人们使用这些设置的第一大错误是,假定它们是对"聪明"或"创造性"控制。




到这里大家应该也就知道了,那个AI佛就是通过程序员选择合适的模型,以及对他加以训练,比如告诉他当用户提问时,他需要从佛教及心理学的角度去回答用户的问题。


模型(Models)


模型可以说是非常重要的一部分,也是Open AI的核心。


image.png


ChatGPT用的就是GPT-3.5模型,他主要是面向大众,所以被训练的更加安全,不会输出一些出格的内容。


在GPT-3.5中又包含了几个子模型:


image.png


目前官方推荐的是gpt-3.5-turbo,称此模型能生成最好的结果,并且只需要text-davinci-003十分之一的算力。


价格


目前gpt-3.5-turbo的价格是每一千个token需要0.002🔪。所谓token可以理解为他处理的单词数,1000个token大概可以生成750个单词。
image.png


AI孙子


接下来我们尝试简单写一个AI孙子。


首先当然是创建一个项目,我用的就是简单的create-react-app去创建一个react应用:
npx create-react-app ai-sunzi


使用express写一个简单的服务端:


image.png


重写App.js,我们只需要一个textarea和一个提交按钮


image.png


访问http://localhost:3000 确保可以访问服务端,返回“Hello World!”:


image.png


接下来我们需要去Open AI官方文档中查看如何使用它的API,文档中有一个例子,如何在nodejs中引入包:


image.png


我们把他复制到我们代码中,有些地方还是需要改动一下,比如包的引入不能直接使用import。再将API Key复制到configuration中(API Key需要注册并登录OpenAI,可以免费领取):


image.png


然后我们再查看文档中Completion部分:


completion.png


将其复制到我们代码中并测试:


image.png


此处我将max_token修改为100,因为中文字符所占字节数长一些。随后将prompt参数改为“无论用户输入什么,只需回答这是一个测试”
再次访问localhost:3000可以看到确实可以返回“这是一个测试”。


说明我们的后端已经从OPEN AI得到了返回的数据。


再接下来就简单了,我们只需要把前端和后端连上,再修改prompt,让这个AI表现的像孙子:


image.png


将textarea中的message传入至prompt参数中,再从OPEN AI得到返回的答案,就大功告成了。
以下为一些测试:


image.png


image.png


image.png


Reference: http://www.youtube.com/watch?v=bB7…


作者:RyanGG
来源:juejin.cn/post/7209862607976644645
收起阅读 »

60分钟的文心一言发布会:我带你5分钟看完

李彦宏缓缓走入会场。 亮了,他亮了。 文心一言具有五大功能: 文学创作(写诗歌,写小说) 商业文案创作(给公司起名,写宣传语) 数理逻辑推算(做题) 中文理解(理解华夏文化) 多模态生成(文字、图片、音频、视频) 其中,前三个功能ChatGPT都有,我...
继续阅读 »

李彦宏缓缓走入会场。


live_capture_05_35.png


亮了,他亮了。


live_capture_05_59.png


文心一言具有五大功能:



  1. 文学创作(写诗歌,写小说)

  2. 商业文案创作(给公司起名,写宣传语)

  3. 数理逻辑推算(做题)

  4. 中文理解(理解华夏文化)

  5. 多模态生成(文字、图片、音频、视频)


其中,前三个功能ChatGPT都有,我感觉百度搞不过它。


比如:


live_capture_10_47.png


再比如:


live_capture_12_45.png


但是,后两个功能是ChatGPT不具备的。


首先说中文理解。


老李说文心一言的重点是用中文训练。AI模型能深入中国文化到什么程度呢?


拿洛阳纸贵举例子,他连着问了模型几个问题。


live_capture_15_31.png


尤其是第二个问题,洛阳纸贵,那么当时到底多少钱呢?


live_capture_14_03.png


模型可以结合唐朝时期的物价以及相关文献记载,给出来答案:当时洛阳的纸由八百文涨到两三千文。


甚至让它以“洛阳纸贵”写一篇藏头诗,模型也写了出来。虽然比不上我写的,但是起码它藏头。


我们再来看看ChatGPT对于中文的理解是什么效果。


问ChatGPT洛阳纸贵什么意思,然后再问洛阳的纸多少钱。


123.png


很明显,它对第二个问题回答的不是很好。它没有清楚的理解我的问题,反而还指出了我的错误。它没有说具体多少钱,它说这不是贵的问题,只是一种现象。


而对于藏头诗,ChatGPT也是无能为力。


456.png


它可能知道什么是诗,但并不知道什么是藏头诗。因为,这就有点深入中国文化了。


但是,我相信,你让它解释藏头诗,它凭借搜索引擎是能找到并打印出来的。但是,你让它写,很遗憾。


ChatGPT没有的,文心一言有的,第二个功能就是多模态。


多模态是AI的一个专业名词,比如文本是一种模态,图片是一种模态。多模态就是多种形式。


我们知道,ChatGPT是一个文本单模态的语言模型。


我李哥演示了一个从文字到图片到语音再到视频的例子。


live_capture_17_40.png


海报图片设计出来了。


live_capture_15_52.png


然后再用四川方言讲出来。


live_capture_16_28.png


最后生成视频。


live_capture_17_13.png


其实,后面的几种模态并非是首发,只是一种整合。


语音合成是老技术了,这不用说。


图文转视频的功能,去年开始,在各大自媒体平台也纷纷上线了。包括头条号在内,写完一篇文章,可以自动生成视频。


但是,大多都是素材库的标签拼凑。生成的效果较差,有时候百度自己生成的,自己都无法通过审核。


但是,让ChatGPT干这些,它除了文本模态之外,它都会回复你它只是一个语言模型。


789.png


讲完了五大功能。后面说了三大产业机会。


live_capture_26_30.png


中间休息,亮出了很多AIGC的案例。


live_capture_00_13.png


live_capture_00_22.png


live_capture_00_32.png


live_capture_00_42.png


休息完了,百度首席技术官,王海峰老师对文心一言的技术做了简单解读。


live_capture_01_24.png


其实主要还是说了利用飞桨开源平台实现的。


live_capture_01_57.png


live_capture_04_57.png


然后介绍了文心一言模型的功能组成。


live_capture_10_40.png


live_capture_12_47.png


live_capture_14_10.png


live_capture_19_05.png


live_capture_21_41.png


最后说,飞桨平台好!


live_capture_21_47.png


结束前,官方平台宣布,企业用户可以申请内测。


live_capture_24_06.png


方法在图片里面了,搜索关键词“百度智能云”填写表单就可以。


有个邀请体验说明是这样的:



我们期待与您尽快展开合作,但由于初期名额有限,目前暂不能满足所有人的申请,因此请您仔细填写需求,我们会结合业务场景、访问量等级等信息综合评估,并在您通过评估后尽快给您反馈,本次邀测仅面向企业用户,谢谢您的理解。



最后,我感觉李哥和王老师在讲的时候,声音有些发颤。可能理工男面对大型发布会都有怯场的情况,也可能他们对平台不自信。


整个发布会,都是在播放视频,并没有现场实际操作平台。这也是为了达到最好的发布效果。李哥说,实际操作的话会比较慢,大家需要等。


我感觉,抛去技术不谈,文心一言在中国肯定是有市场的。


因为,我在直播间发布了很多信息,都没有显示。但是,当我和大家万众一心时,我的评论赫然出现在字幕上。


2023-03-16_140740.png


这一点,ChatGPT无论如何是无法做到的!


我们不黑不吹,后续的情况,只能等待用户的反馈了。


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

ChatGPT前端领域初探

什么是ChatGPT 官方解释:ChatGPT是一个智能聊天机器人,来自于OpenAI,它能够使用人工智能技术进行对话,并回答用户提出的问题和请求。它由GPT(Generative Pre-trained Transformer)算法支持,可以模拟人类对话和回...
继续阅读 »

什么是ChatGPT


官方解释:ChatGPT是一个智能聊天机器人,来自于OpenAI,它能够使用人工智能技术进行对话,并回答用户提出的问题和请求。它由GPT(Generative Pre-trained Transformer)算法支持,可以模拟人类对话和回答各种问题,包括日常生活、科技、娱乐、健康、财经等领域。ChatGPT可以通过各种渠道进行访问,如网站、社交媒体或移动应用程序。


ChatGPT优势


ChatGPT有以下几个优势:



  1. 可以模拟真人对话:ChatGPT使用GPT算法,可以生成自然语言,使得对话非常流畅和自然,就好像在与一个真人交流一样。

  2. 能够自我学习:ChatGPT使用机器学习技术,可以通过不断的学习来提高自己的答案和回复质量。

  3. 24小时在线:ChatGPT可以在任何时间回答用户的问题,不需要等待人类客服的接待时间。

  4. 处理大量请求:ChatGPT可以处理大量请求,在同一时间内可以同时与多个用户进行对话。

  5. 提高客户满意度:ChatGPT可以回答用户的问题并提供有用的信息,这可以提高用户的满意度和忠诚度。

  6. 提高效率:ChatGPT可以快速响应和解答用户问题,减少人工客服的工作量和时间。


接下来,我们来聊一聊它对前端开发产生了什么样的影响


体验流程


我们需要先拥有一个相应的账户才能体验,这里我直接放上体验流程的链接:sms-activate.org,按照本流程对于没有接触过ChatGPT的童鞋们可以体验一下,这里我就不展开详细解释了。tips:电脑需要科学上网哦~~


辅助开发


重点来了,我们需要先分析我们在日常开发中哪些方面可以用到它,根据开发的流程我们可以从以下几点分析:




  1. 需求阶段:我们做业务开发的前提是以需求为准,对于需求而言,实现的方式多种多样,我们应该分析一下,应该用什么技术去实现,具体对应到哪种框架、第三方依赖库等等。




  2. 编码阶段:这个阶段是业务逻辑的实现阶段,要完成需求中的功能。举个🌰:做登录注册模块,输入账号和密码时一般都需要校验格式(复杂情况),这时正则表达式不失为一个好的办法,此时我们的主角就该登场了~~ 话不多说,直接上图:


    image.png


    经验证:^[a-zA-Z0-9]{6,}$ 符合预期
      let reg = /^[a-zA-Z0-9]{6,}$/
    let str = '0203'
    let str2 = 'yk0203'
    console.log(reg.test(str), 'str') // false
    console.log(reg.test(str2), 'str2') // true



这个正则限制比较简单,来个复杂点的吧:


image.png


image.png tips:解释满分。


这里我就不做test了,更复杂的大家可以自己尝试~~



  1. 代码优化:我们在在编码阶段完成功能后,应该考虑代码优化之道,优化代码包括多个层面,提高代码的复用性就是其中一个方面,直接上demo说明一下:


image.png
在这个demo中,我们定义了两个函数,一个用于计算两个数的和,另一个用于计算两个数的差。通过调用这两个函数,我们可以实现复用性,避免在不同的地方重复编写相同的计算代码。此外,我们还可以通过将这些函数放入一个模块中,从而实现更高程度的复用性。将这两个函数放入一个名为"calculator.py"的模块中,然后在其他文件中引入这个模块,就可以在其他地方轻松地重用这些函数。然后我又向它发起提问:


image.png


image.png
我们使用JavaScript语言实现了与前面Python demo相同的功能。将这两个函数放入一个名为"Calculator.js"的模块中,然后在其他文件中引入这个模块,就可以在其他地方轻松地重用这些函数。其实这种思想在前端已经老生常谈了。



  1. 代码重构:一般来说,一个项目经过多个人先后开发并维护,每个人的开发习惯也不尽相同,最终导致代码中很多逻辑不是很清晰,尤其是那些很久之前的项目,再加上当时没有添加注释,维护效率不高,这个时候我们可以考虑下重构下部分代码,用图来说话:


image.png
这里只是一个简单的demo,项目中重构代码要比这个复杂(慎重考虑是否进行代码重构),说到这里,养成良好的编码习惯就显得特别重要啦,比如定义方法时语义化,适当给方法添加注释等,我们的主角也是也可以帮忙添加注释的,请看:


image.png


function factorial(n) { 
if (n === 0) return 1;
else return n * factorial(n-1);
}
//示例输出
console.log(factorial(5)); //120
console.log(factorial(10)); //3628800

image.png


// 定义递归函数,计算阶乘 
function factorial(n) {
if (n === 0) {
// 当 n 等于 0 时,阶乘结果为 1
return 1;
} else {
// 当 n 不等于 0 时,调用函数自身来计算阶乘
return n * factorial(n-1);
}
}
// 示例输出
console.log(factorial(5)); //120
console.log(factorial(10)); //3628800

然后我又追问:


image.png
综上,我们想要主角帮我们处理问题,就应该把条件罗列清晰,否则主角也给不了最完善的回答,它有时在处理基本数学问题时出错,有时无法全面理解逻辑,并且甚至会提供完全错误的事实来支持其答案。OpenAI 已经承认了这个问题,并表示“ChatGPT 有时会写出听起来合理但是不正确或无意义的答案”。ChatGPT 是一种辅助工具,能够理解复杂的问题。但最好也不要完全相信它所说的一切。


tips:提问和回答的截图均来自VScode的ChatGPT中文版扩展插件(站在了大佬的肩膀上),有兴趣的童鞋可以自行搜索体验一波~


ChatGPT的现状与未来


目前,OpenAI官方已经官宣了GPT-4,只不过还没完全开放,并且门槛较高,发展前景还是相当nice的。


总结


总体看来,未来可期~~,对于希望在工作中提升效率的开发人员来说是一柄利剑。然而,它的回答也不是100%准确的,因此在将其用于更高级的任务之前,需进行深究。到此,ChatGPT的初探到此结束,感谢各位看官。有问题欢迎评论区留言。


作者:青灬河
来源:juejin.cn/post/7210653822849548346
收起阅读 »

前端已死?铜三铁四?你收到offer了吗?

背景 今年是应届生就业最难的一年。说实话,每一届毕业生都这么说,而且每一届毕业生说的都没有问题。疫情刚开始那一年,王兴说过一句话 2019 年可能会是过去十年最差的一年,却是未来十年里最好的一年。因为难,所以很多人将目光放向了金三银四,无论是应届的,还是找新机...
继续阅读 »

背景


今年是应届生就业最难的一年。说实话,每一届毕业生都这么说,而且每一届毕业生说的都没有问题。疫情刚开始那一年,王兴说过一句话 2019 年可能会是过去十年最差的一年,却是未来十年里最好的一年。因为难,所以很多人将目光放向了金三银四,无论是应届的,还是找新机会的。


金三银四就是往年最好的求职时期。三四月份刚好又是春招的时期,过年后,每个企业又刚好制定好下一年的计划。刚发完年终奖,职场的小同学们又蠢蠢欲动,空出来不少位置。


现况


但是对于今年来说,金三银四就是纯骗局。即使你通过了很多简历初选,发现最后拿到手没有什么好offer,你发现好像同样的岗位,去年学长学姐,硬件指标不需要这么严格。辞职前聊的好好的offer,裸辞之后就被缩紧了。


我之前看过一段话,我一直觉得欧洲特别拉胯,今天搞游行,明天搞罢工,昨天嫌工作时间多了,今天嫌假期少了。直到有一天,我改变了看法,我毕业了。我们以为疫情过去,所有的一切都会好转,因为这是艰难的最主要来源。但是好像不是这样。因为疫情指很多深层问题的表面借口罢了。


原因


疫情只是一方面。国内资源开发过度,房地产集体不拿地,导致房企上下游的企业、广告公司、活动公司、印刷公司、工程公司对外招聘收紧。国际局势紧张,投资愈加谨慎,金融行业都卷得飞起,依赖投资的入不敷出的互联网公司,更无法支出如此大的人力成本。由此牵扯的行业计算机产品、自媒体运营、招聘收紧。教育本来就人走茶凉,牵扯出来的行业也多的不行。甚至俄乌战争能通过大量蝴蝶效应影响着我们。


那我们能怎么办呢?


混乱的背景,只会使富人更富,穷人更穷。没办法了吗?那我们能怎么办呢?


我们无法改变局势,我们只能在夹缝中求生存。但我们起码要成为在这个环境下人群中的最优解。求职也好,职业规划也好,面对现在困境,主要有两个生存的角度避坑主动措施


避坑


想要避免被坑,就得了解面试官和公司。他们到底怎么想的?


这两年最主要的坑分别是赛马机制表面繁荣白嫖逻辑


赛马机制


面试公司让你以为你很优秀,其实你已经掉入公司的陷阱了。赛马机制就是强势烘托起所有人的竞争欲,最后选出最优秀的一匹马。赛马本无问题,但是大面积的赛马是对求职者的极其不负责任。以前的公司经常会搞 PUA 这一套来实现控制员工的目的。现在公司用的方法更加高级 NLP 教练技巧。 NLP 教练技巧可以理解为正向PUA。


你以为面试官在夸你?你以为公司很器重你?


但你已经掉进公司糖衣炮弹的陷阱了。 NLP 本来是良性的引导,但在大面积的竞争驱使下,赛马机制应运而生。 NLP 带来的自我催眠会让你适应恶劣的竞争,像养蛊一样愿意吃掉所有的竞争者。


如何判断一个公司是否在赛马?


最简单的逻辑是



  1. 第一、看是否是需要通过试用期多人的竞争来确定留用。不用可惜这样子的机会,因为转正后一定会迎来更为内卷的赛马机制。

  2. 第二、 不是面试前,而是所有面试后还有一堆面试官加你微信的,大概率是想拉着你再赛马几次,或者我只能请你保护好自己的私生活。

  3. 第三、 画饼的公司必然在赛马。


就业难在哪里?因为人力资源充沛这件事既是红利也是诅咒。人力资源看起来充沛,就会带来第二个坑,


表面繁荣,


实际上企业现阶段根本没有什么需求,裁员都来不及呢。这和你们现在面对的很多情况是类似的。一个岗位有超多人竞聘,很多人认为这个岗位热门就是行业趋势了。其实并不是,其实只是只是企业可能需要到面率等一系列人力资源指标,为了让这个看起来很繁荣罢了。这也是为什么你们很多人通过简历关,但是最后都拿不到 offer 的原因


如何避免?


几乎无法避免,毕竟海头是你我们常干的事情。但是可以做好心理预设,就是招聘人数需求很大的岗位,不要对这种岗位抱有太高的希望,避免影响自己招聘热情和状态。


白嫖逻辑


当你跟面试官运筹帷幄,展示自己的专业性时,你已经失去了工作的机会。公司招不起人才,但想白嫖。人才的创意和专业知识,


怎么嫖?让人才参加面试?


我认识的一个大厂的面试官跟我聊天,说今年他们的招生名额少了很多,但是面试数量增加很多。我很好奇的问他,逻辑有点矛盾,他贱兮兮的跟我说,他们内部叫做给奖励,主要给那些很厉害的人奖励,但是并不会招他们。比如一些 35 岁以上很专业的人,比如有一些硬伤但真的有东西的人。这些人在求职市场处处碰壁,给的奖励就是面试的机会。他们刚好也可以从这些面试者那里了解到一些打破信息壁垒的专业知识或信息,顺便给一些刚入职的人或者项目经理培训,但是他们肯定是不会招的。说实话,我听到这里已经开始气了。应届生常见的类似的坑,就是拿一些内部案例作为面试题进行面试。本质就是白嫖,却堂而皇之的说是奖励,谁稀罕你的奖励。所以,如果你碰到公司,一拿非常具体的案例来去进行考核,让你演绎操作细节的。并且不断询问你对行业的认识,跟你讨论战略并显得他毫无理解的。或者面试官相较于你过于年轻的这一类公司,扭头就走就好了


相反的,如一个公司不断的给你输出他们的信息,并且真实的拿这些信息和你比对,这类公司大概率是比较靠谱的。


其实听你看完上面的,你基本能面对在金三银四的大粪坑了,还有一些很明显的坑,大部分你在网上也能搜索到,我就不扩展讲了。但是下面你要仔细看了,因为这是你在避坑之后能够有可能找到工作的重要手段。


主动措施


我们如果把找工作分成找工作前、找工作中和找工作后,我们分别要做的东西就是信息获取个人展示职业规划。我不断在文章里面强调信息的重要性。各个公司官网的 drop description 一定需要好好看看。如果可以用爬虫把很多公司的求职信息 down 下来,可以在他们的官网,也可以在智联招聘一类的网站,不用担心太难。你在 b 站或者其他平台搜索一下。爬虫找工作。现在已经有很多打包的工具或者软件,


金三银四


还有金三银四,不是三四月份开始,春节后就已经开启了。非要再说一些信息,新能源医药现在形势的确不错,按照之前教给你们的思维,他们的上下游求职都会比较顺利。应面试之前去企查查天眼查之类的,好好查一下公司的相关情况,什么情况,成立时间,公司规模,组织架构,法律风险是否存在信誉问题,股东信息是否正常,是否有财务纠纷。查完了,门清了你再冲,不然给你拐缅北去。再看一圈。脉脉拉钩,牛客网、看准网之类的,里面有大量吐槽到起飞的评价,可以让种草的你立马拔草。还有一些关于签署协议、劳动合同、三方协议等等一系列你需要了解,保护自己权益的相关信息,


个人展示


重要的大家理解的还是简历。简历其实针对不同的行业,需要针对定向的简历。因为你要展示的东西暴露问题是不一样的。如果你没有简历模板,或者简历做得很拉,可以使用木及简历,他是一款免费的简历模板网站。还可以使用Markdown编辑特别方便,不要用网上那些花里胡哨的剪辑模板,谁用谁死。往年我们在各大网站搜索找工作,都是各种干货方法。今年你在知乎搜一下 2023 找工作试试。只有一个统一的话题 2023 找工作好难。


你要准备无非是从找工作的环节入手。简历、笔试、面试、二面、终面。简历跟大家说了,笔试自己准备,唯一能准备的就是面试了。牛客网有不少念经,可以去看看,剩下的就是最后一个。


心态


最重要的心态随缘。我不是让大家摆烂。很多公司会组织无领导小组面试,很多公司会有压力面。如果让我给你讲分别,有什么技巧,我是能讲很多的,毕竟我也参加过招聘招聘。但是没必要随缘这个词很重要,因为它恰恰是器中了面试的核心表现状态。当你随缘的时候,无领导小组面试就没必要非要去争取leader,压力面也不会有什么压力,我不会。要知道 leader 通过率并不会比其他角色高。争取表现机会的行为其实并不会帮到什么。很多人总跟你讲放平心态,什么叫做放平心态?随缘就好。工作暂时找不到,并不会饿死并不是你一个人现在这么艰难。削弱焦虑还没到最后一刻


最后职业规划在这里。我只想跟应届生和已经工作的同学说两条。



  • 一、应届生转行并没有什么壁垒,去现在好一些的行业。

  • 二、别跳槽,市场太不景气了。不要觉得疫情过去,一切都好了。求职很重要,只是人生的一小步,并不会因为你应届的求职受挫而影响那么深远。从大厂离开的决定,那一刻,如果你也不想找工作,就就当你给自己放个长假。最重要的是要
    作者:zayyo
    来源:juejin.cn/post/7207314488243585081
    开心。

收起阅读 »

Android记一次JNI内存泄漏

记一次JNI内存泄漏 前景 在视频项目播放界面来回退出时,会触发内存LeakCanary内存泄漏警告。 分析 查看leakCanary的日志没有看到明确的泄漏点,所以直接取出leakCanary保存的hprof文件,保存目录在日志中有提醒,需要注意的是如果是a...
继续阅读 »

记一次JNI内存泄漏


前景


在视频项目播放界面来回退出时,会触发内存LeakCanary内存泄漏警告。


分析


查看leakCanary的日志没有看到明确的泄漏点,所以直接取出leakCanary保存的hprof文件,保存目录在日志中有提醒,需要注意的是如果是android11系统及以上的保存目录和android11以下不同,android11保存的目录在:


   /data/media/10/Download/leakcanary-包名/2023-03-14_17-19-45_115.hprof 

使用Memory Analyzer Tool(简称MAT) 工具进行分析,需要讲上面的hrof文件转换成mat需要的格式:


   hprof-conv -z 转换的文件 转换后的文件

hprof-conv -z 2023-03-14_17-19-45_115.hprof mat115.hprof

打开MAT,导入mat115文件,等待一段时间。


在预览界面打开Histogram,搜索需要检测的类,如:VideoActivity


screenshot-20230314-204413.png


搜索结果查看默认第一栏,如果没有泄漏,关闭VideoActivity之后,Objects数量一般是零,如果不为零,则可能存在泄漏。


右键Merge Shortest Paths to GC Roots/exclude all phantom/weak/soft etc,references/ 筛选出强引用的对象。


image.png


筛选出结果后,出现com.voyah.cockpit.video.ui.VideoActivity$1 @0x3232332 JIN Global 信息,且无法继续跟踪下去。


screenshot-20230314-205257.png


筛选出结果之后显示有六个VideoActivity对象没有释放,点击该对象也无法看到GC对象路径。(正常的java层内存泄漏能够看到泄漏的对象具体是哪一个)


正常的内存泄漏能够看到具体对象,如图:


image.png
这个MegaDataStorageConfig就是存在内存泄漏。


而我们现在的泄漏确实只知道VideoActivity$1 对象泄漏了,没有具体的对象,这样就没有办法跟踪下去了。


解决办法:


虽然无法继续跟踪,但泄漏的位置说明就是这个VideoActivity1,我们可以解压apk,在包内的class.dex中找到VideoActivity1 ,我们可以解压apk,在包内的class.dex中找到VideoActivity1这个Class类(class.dex可能有很多,一个个找),打开这个class,查看字节码(可以android studio中快捷打开build中的apk),根据【 .line 406 】等信息定位代码的位置,找到泄漏点。


screenshot-20230314-205442.png


screenshot-20230314-205600.png
screenshot-20230314-205523.png


根据方法名、代码行数、类名,直接定位到了存在泄漏的代码:


screenshot-20230314-205730.png


红框区内就是内存泄漏的代码,这个回调是一个三方sdk工具,我使用时进行了注册,在onDestory中反注册,但还是存在内存泄漏。(该对象未使用是我代码修改之后的)


修改方法


将这个回调移动到Application中去,然后进行事件或者回调的方式通知VideoActivity,在VideoActivity的onDestory中进行销毁回调。


修改完之后,多次进入VideoAcitivity然后在退出,导出hprof文件到mat中筛选查看,如图:


image.png


VideoActiviyty的对象已经变成了零,说明开始存在的内存泄漏已经修改好了,使用android proflier工具也能看到在退出videoactivity界面之后主动进行几次gc回收,内存使用量会回归到进入该界面之前。


总结:



  1. LeakCanary工具为辅助,MAT工具进行具体分析。因为LeakCanary工具的监听并不准确,如触发leakcanary泄漏警告时代码已经泄漏了很多次。

  2. 如果能够直接查看泄漏的对象,那是最好修改的,如果不能直接定位泄漏的对象,可以通过泄漏的Class对象在apk解压中找到改class,查看字节码定位具体的代码泄漏位置。

  3. 使用第三方的sdk时,最好使用Application Context,统一分发统一管理,减少内存泄漏。


作者:懵逼树上懵逼果
来源:juejin.cn/post/7210574525665771557
收起阅读 »

GPT-4 就要来了,你准备好了吗

GPT-4 即将面世,它到底是擎天柱还是威震天? 呆鸟说:“ChatGPT 是具备人机对话能力的人工智能,一经问世,就给世人带来了前所未有的新奇体验。短短几个月时间,它就占据了人们的视野。有人好奇,有人担忧,有人看到了致富的机会,也有人担心 AI 会夺走我们...
继续阅读 »

GPT-4 即将面世,它到底是擎天柱还是威震天?



呆鸟说:“ChatGPT 是具备人机对话能力的人工智能,一经问世,就给世人带来了前所未有的新奇体验。短短几个月时间,它就占据了人们的视野。有人好奇,有人担忧,有人看到了致富的机会,也有人担心 AI 会夺走我们的饭碗。ChatGPT 的升级版 GPT-4 也即将问世,面对更强大的 AI,我们应当何去何从呢”



GPT-3 是 ChatGPT 的核心,它是一个超大规模的大语言模型(large language models,LLMs)。GPT-3 的英文全称是生成式预训练转换器-3(generative pre-trained transformer 3),是基于 Transformer 架构开发的大型语言模型,拥有超过 1750 亿个参数。公众可以通过 OpenAI API 访问。ChatGPT 的 API 用户界面简单易用,只需“输入文本”,即可“输出文本”,无需用户具备任何专业技术知识。


据传,OpenAI 正在开发 GPT-4,该模型的参数可能高达 100 万亿,但 OpenAI 的首席执行官 Sam Altman 反驳了这一说法。


那么,GPT-4 与 GPT-3 到底有何不同,它是否会颠覆我们现有的认知?本文将一一对此进行说明。


预计 GPT-4 将在 2023 年发布


汽车人首领 ~ 擎天柱
汽车人首领 ~ 擎天柱


虽然尚未正式官宣,但 IT 界对 GPT-4 的传言数不胜数,预计 OpenAI 很有可能会在今年发布 GPT-4。尽管有传言称微软必应的聊天功能中使用了新版 GPT,但目前测试用户必须排队等待才能使用由 ChatGPT 支持的新必应,而且也无法确定新必应的聊天功能是否使用了新版本的 GPT。


在 ChatGPT 发布之前,即 2022 年底,OpenAI 只允许指定的合作伙伴、付费客户和学术机构使用 GPT-3。预计在GPT-4 发布之前,OpenAI 也会采取类似的方式限制 GPT-4 的使用人员。


GPT-4 可能不会比 GPT-3 训练更多的数据


有传言称,GPT-4 的参数规模将比 GPT-3 大 100 倍,即 17 万亿个参数。但 Altman 曾表示,GPT-4 的参数规模可能并不会比 GPT-3 大很多,因为改进的重点应该是利用现有数据的能力,而不是添加更多的数据。


与 GPT-3 竞争的 Megatron 3(注:Megatron 是变形金刚里霸天虎的首领,中文名为威震天)也是一种大语言模型,它训练的数据比 GPT-3 多,但在测试效果并没有超越 GPT-3,这表明在 AI 领域,规模更大并不一定意味着效果更好。改进算法将降低 GPT-4 和 ChatGPT 的运行成本,这将是 ChatGPT 想要取代谷歌成为最流行搜索引擎的重要因素。


霸天虎首领 ~ 威震天
霸天虎首领 ~ 威震天


GPT-4 将生成更好的编程语言代码


ChatGPT 令人印象最深刻的一点是它不仅可以生成人类语言,还可以生成 Javascript、Python、C++ 等编程语言的代码,可以说是软件开发、Web 开发和数据分析统统都能搞定。


有消息称,目前,OpenAI 正在积极招聘擅长使用人类语言描述代码功能的程序员,预计 GPT-4 将推动 AI 突破生成编程代码的新境界。这种趋势将进一步推动开发工具的革命,例如,微软旗下的 Github 推出的 Copilot 使用的就是经过微调的 GPT-3,提供了把人类自然语言转换为代码的能力。


GPT-4 将使 AI 生成编程代码的能力迈向新的境界。除了生成人类语言,ChatGPT 还能生成 JavaScript、Python、C++ 等编程语言的代码,在软件开发、Web开发和数据分析等领域都将有广泛的应用。


据称,OpenAI 正在招聘能以人类语言描述代码功能的程序员,这将进一步推动开发工具的革命。例如,微软旗下的 Github 推出的 Copilot 使用的就是经过微调的 GPT-3,能够把人类自然语言转换为代码。


总之一句话,就是让程序员开发干掉程序员的程序


GPT-4 不会添加图形功能


有人曾预测 GPT-4 将整合 GPT-3 的文本生成和 Dall-E 2(OpenAI 的另一款 AI 产品)的图像创作功能,如果 能提供数据可视化功能,ChatGPT 将更加完美。但 Altman 否认了这一点,并表示 GPT-4 仍只提供文本生成的功能。


Dall-E 生成的图像
Dall-E 生成的图像


有人会对 GPT-4 失望


虽然 GPT-3 的闪亮登场让整个世界都兴奋不已,但 GPT-4 的表现可能并不会让人再次惊艳。计算机第一次写诗的时候,你可能会觉得震撼,但几年后,即使能让 AI 把诗写得更优美,也不会再给人带来同样的震撼。


Altman 在今年一月的一次采访中曾说过,“有关 GPT-4 的传言很荒谬,都是空穴来风,只怕是希望越大,失望越大。”


那么,我亲爱的读者们,你们对此是怎么看的呢?


有兴趣写书的联系我


Nuxt3 小册推荐


作者:呆鸟
来源:juejin.cn/post/7205842390842114085
收起阅读 »

做一个文件拖动到文件夹的效果

web
在我的电脑中,回想一下我们想要把一个文件拖动到另一个文件夹是什么样子的呢 1:鼠标抓起文件 2:拖动文件到文件夹上方 3:文件夹高亮,表示到达指定位置 4:松开鼠标将文件夹放入文件 下面就来一步步实现它吧👇 一:让我们的元素可拖动 方式一: dragg...
继续阅读 »

在我的电脑中,回想一下我们想要把一个文件拖动到另一个文件夹是什么样子的呢



1:鼠标抓起文件

2:拖动文件到文件夹上方

3:文件夹高亮,表示到达指定位置

4:松开鼠标将文件夹放入文件



Kapture 2023-03-10 at 08.30.34.gif


下面就来一步步实现它吧👇


一:让我们的元素可拖动


方式一: draggable="true"


`<div draggable="true" class="dragdiv">拖动我</div>`

方式二:-webkit-user-drag: element;


  .dragdiv {

width: 100px;

height: 100px;

background-color: bisque;

-webkit-user-drag: element;

}


效果


Kapture 2023-03-10 at 08.55.25.gif


二:让文件夹有高亮效果


给文件夹添加伪类?


🙅如果你直接给文件夹设置伪类:hover,会发现当拖动元素时,文件夹的:hover是不会触发的


Kapture 2023-03-10 at 09.08.54.gif


🧘这是因为在拖拽元素时,拖拽操作和悬停操作是不同的事件类型,浏览器在处理拖拽操作时,会优先处理拖拽事件,而不会触发悬停事件。拖拽操作是通过鼠标点击和拖拽来触发的,而悬停事件是在鼠标指针停留在一个元素上时触发的。


所以我们就来对拖拽操作的事件类型做功课吧🫱



  • dragstart:拖拽开始

  • dragend:拖拽结束

  • dragover:拖拽的元素在可放置区域内移动时触发,即鼠标指针在可放置区域内移动时持续触发

  • dragenter:拖拽的元素首次进入可放置区域时触发

  • dragleave:拖拽的元素离开可放置区域时触发

  • drop:当在可放置区域时,松开鼠标放置元素时触发


什么是可放置元素?
当你给元素设置事件:dragover、dragenter、dragleave、drop的时候
它就变成了可放置元素,特点是移到上面有绿色的➕号

拖动高亮实现


1:我们给files文件夹添加两个响应事件:dragoverdragleave


ps: 这里用dragover事件而不用dragenter事件是为了后续能够成功触发drop事件

2:当拖动元素进入可放置区域时,动态的给files添加类,离开时则移除类


// 显示高亮类
.fileshover {
background-color: rgba(0, 255, 255, 0.979);
}
// 添加dragover事件处理程序,在可放置区域触发

files.addEventListener('dragover', (event) => {

event.target.classList.add('fileshover');

});

// 添加dragleave事件处理程序,离开可放置区域触发

files.addEventListener('dragleave', (event) => {

event.target.classList.remove('fileshover');

});

🥳 恭喜你成功实现了移动到元素高亮的效果了


Kapture 2023-03-14 at 11.54.14.gif


三:文件信息传递


文件拖过去,是为了切换文件夹,在这里你可能会进行一些异步的操作,比如请求后端更换文件在数据库中的路径等。我们的需求多种多样,但是归根到底都是获取到文件的数据,并传递到文件夹中


DataTransfer对象


DragEvent.dataTransfer: 在拖放交互期间传输的数据


我们主要使用它的两个方法:



  • DataTransfer.setData(format, data):就是设置键值对,把我们要传的数据添加到drag object

  • DataTransfer.getData(format):根据键获取保存的数据


知道了这两个方法,相信你一定就有实现思路了 👊


拖拽开始 --> setData添加数据 --> 进入可放置区域 --> 放置时getData获取数据 --> 完成


1:给文件设置dragstart事件


// 开始拖拽事件

draggable.addEventListener('dragstart', (event) => {

const data = event.target.innerText;

event.dataTransfer.setData('name', data); //添加数据

})

2:在dragover事件中用event.preventDefault()阻止默认行为,允许拖拽元素放置到该元素上,否则无法触发drop事件


// 添加dragover事件处理程序

files.addEventListener('dragover', (event) => {

event.target.classList.add('fileshover');

event.preventDefault(); //新增

});

3:给文件夹设置放置事件drop


// 添加drop事件处理程序

files.addEventListener('drop', (event) => {

const data = event.dataTransfer.getData('name'); // 获取文件的数据

const text = document.createTextNode(data);

files.appendChild(text);

event.target.classList.remove('fileshover'); // 记得放置后也要移除类

});

实现效果:


Kapture 2023-03-14 at 14.46.45.gif


四:完整代码:


<!DOCTYPE html>

<html lang="en">

<head>

<meta charset="UTF-8">

<meta http-equiv="X-UA-Compatible" content="IE=edge">

<meta name="viewport" content="width=device-width, initial-scale=1.0">

<title>Document</title>

<style>

.dragdiv {

width: 100px;

height: 100px;

background-color: bisque;

-webkit-user-drag: element;

}

.files {

width: 200px;

height: 200px;

background-color: rgba(0, 255, 255, 0.376);

margin-top: 100px;

}

.fileshover {

background-color: rgba(0, 255, 255, 0.979);

}

</style>

</head>

<body>

<div draggable="true" class="dragdiv">我是文件1</div>

<div class="files">

<p>文件夹</p>

拖动文件名称:

</div>

<script>

const draggable = document.querySelector('.dragdiv');

const files = document.querySelector('.files');

// 开始拖拽事件

draggable.addEventListener('dragstart', (event) => {

const data = event.target.innerText;

event.dataTransfer.setData('name', data);

})

// 添加dragover事件处理程序

files.addEventListener('dragover', (event) => {

event.target.classList.add('fileshover')

event.preventDefault()

});

// 添加dragleave事件处理程序

files.addEventListener('dragleave', (event) => {

event.target.classList.remove('fileshover')

});

// 添加drop事件处理程序

files.addEventListener('drop', (event) => {

const data = event.dataTransfer.getData('name')

const text = document.createTextNode(data)

files.appendChild(text);

event.target.classList.remove('fileshover')


});

</script>

</body>

</html>

总结:以上只是简单的熟悉拖拽事件的整个过程,你可以在此拓展更多自己想要功能,欢迎分享👏


作者:隐兮
来源:juejin.cn/post/7210256070299549755
收起阅读 »

Android 指纹识别(给应用添加指纹解锁)

使用指纹 说明 : 指纹解锁在23 的时候,官方就已经给出了api ,但是由于Android市场复杂,无法形成统一,硬件由不同的厂商开发,导致相同版本的软件系统,搭载的硬件千变万化,导致由的机型不支持指纹识别,但是,这也挡不住指纹识别在接下来的时间中进入An...
继续阅读 »

使用指纹



说明 : 指纹解锁在23 的时候,官方就已经给出了api ,但是由于Android市场复杂,无法形成统一,硬件由不同的厂商开发,导致相同版本的软件系统,搭载的硬件千变万化,导致由的机型不支持指纹识别,但是,这也挡不住指纹识别在接下来的时间中进入Android市场的趋势,因为它相比较输入密码或图案,它更加简单,相比较密码或者图案,它更炫酷 ,本文Demo 使用最新的28 支持的androidx 库中的API及最近火热的kotlin语言完成的



需要知道的



  • FingerprintManager : 指纹管理工具类

  • FingerprintManager.AuthenticationCallback :使用验证的时候传入该接口,通过该接口进行验证结果回调

  • FingerprintManager.CryptoObject: FingerprintManager 支持的分装加密对象的类



以上是28以下API 中使用的类 在Android 28版本中google 宣布使用Androidx 库代替Android库,所以在28版本中Android 推荐使用androidx库中的类 所以在本文中我 使用的是推荐是用的FingerprintManagerCompat 二者的使用的方式基本相似



如何使用指纹



  • 开始验证 ,系统默认的每段时间验证指纹次数为5次 次数用完之后自动关闭验证,并且30秒之内不允行在使用验证


验证的方法是authenticate()


/**
*
*@param crypto object associated with the call or null if none required.
* @param flags optional flags; should be 0
* @param cancel an object that can be used to cancel authentication
* @param callback an object to receive authentication events
* @param handler an optional handler for events
**/

@RequiresPermission(android.Manifest.permission.USE_FINGERPRINT)
public void authenticate(@Nullable CryptoObject crypto, int flags,
@Nullable CancellationSignal cancel, @NonNull AuthenticationCallback callback,
@Nullable Handler handler)
{
if (Build.VERSION.SDK_INT >= 23) {
final FingerprintManager fp = getFingerprintManagerOrNull(mContext);
if (fp != null) {
android.os.CancellationSignal cancellationSignal = cancel != null
? (android.os.CancellationSignal) cancel.getCancellationSignalObject()
: null;
fp.authenticate(
wrapCryptoObject(crypto),
cancellationSignal,
flags,
wrapCallback(callback),
handler);
}
}
}



arg1: 用于通过指纹验证取出AndroidKeyStore中key的值
arg2: 系统建议为0




arg3: 取消指纹验证 手动关闭验证 可以调用该参数的cancel方法




arg4:返回验证结果




arg5: Handler fingerprint 中的
消息都是通过handler来传递的 如果不需要则传null 会自动默认创建一个主线程的handler来传递消息



使用指纹识别的条件



  • 添加权限(这个权限不需要在6.0中做处理)

  • 判断硬件是否支持

  • 是否已经设置了锁屏 并且已经有一个被录入的指纹

  • 判断是否至少存在一条指纹信息




通过零碎的知识完成一个Demo


这里写图片描述


指纹识别通过之后跳转到 指定页面


进入之后首先弹出对话框,进行指纹验证


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">


<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/fingerprint" />


<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="20dp"
android:text="验证指纹" />


<TextView
android:id="@+id/fingerprint_error_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:maxLines="1" />


<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:layout_marginLeft="5dp"
android:layout_marginTop="10dp"
android:layout_marginRight="5dp"
android:background="#696969" />


<TextView
android:id="@+id/fingerprint_cancel_tv"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_gravity="center"
android:gravity="center"
android:text="取消"
android:textSize="16sp" />


</LinearLayout>


使用DialogFragment 完成对话框 新建一个DialogFragment 并且初始化相关的api


 override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//获取fingerprintManagerCompat对象
fingerprintManagerCompat = FingerprintManagerCompat.from(context!!)
setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog)
}


在界面显示在前台的时候开始扫描


override fun onResume() {
super.onResume()
startListening()
}
@SuppressLint("MissingPermission")
private fun startListening() {
isSelfCancelled = false
mCancellationSignal = CancellationSignal()
fingerprintManagerCompat.authenticate(FingerprintManagerCompat.CryptoObject(mCipher), 0, mCancellationSignal, object : FingerprintManagerCompat.AuthenticationCallback() {
//验证错误
override fun onAuthenticationError(errMsgId: Int, errString: CharSequence?) {
if (!isSelfCancelled) {
errorMsg.text = errString
if (errMsgId == FingerprintManager.FINGERPRINT_ERROR_LOCKOUT) {
Toast.makeText(mActivity, errString, Toast.LENGTH_SHORT).show()
dismiss()
mActivity.finish()
}
}
}
//成功
override fun onAuthenticationSucceeded(result: FingerprintManagerCompat.AuthenticationResult?) {
MainActivity.startActivity(mActivity, true)
}
//错误时提示帮助,比如说指纹错误,我们将显示在界面上 让用户知道情况
override fun onAuthenticationHelp(helpMsgId: Int, helpString: CharSequence?) {
errorMsg.text = helpString
}
//验证失败
override fun onAuthenticationFailed() {
errorMsg.text = "指纹验证失败,请重试"
}
}, null)
}

在不可见的时候停止验证


if (null != mCancellationSignal) {
mCancellationSignal.cancel()
isSelfCancelled = true
}

在MainActivity 中首先判断是否验证成功 是 跳转到目标页 否则的话需要进行验证
在这个过程中我们需要做的就是判断是否支持,判断是否满足指纹验证的条件(条件在上面)


if (intent.getBooleanExtra("isSuccess", false)) {
WelcomeActivity.startActivity(this)
finish()
} else {
//判断是否支持该功能
if (supportFingerprint()) {
initKey() //生成一个对称加密的key
initCipher() //生成一个Cipher对象
}
}


验证条件


 if (Build.VERSION.SDK_INT < 23) {
Toast.makeText(this, "系统不支持指纹功能", Toast.LENGTH_SHORT).show()
return false
} else {
val keyguardManager = getSystemService(KeyguardManager::class.java)
val managerCompat = FingerprintManagerCompat.from(this)
if (!managerCompat.isHardwareDetected) {
Toast.makeText(this, "系统不支持指纹功能", Toast.LENGTH_SHORT).show()
return false
} else if (!keyguardManager.isKeyguardSecure) {
Toast.makeText(this, "屏幕未设置锁屏 请先设置锁屏并添加一个指纹", Toast.LENGTH_SHORT).show()
return false
} else if (!managerCompat.hasEnrolledFingerprints()) {
Toast.makeText(this, "至少在系统中添加一个指纹", Toast.LENGTH_SHORT).show()
return false
}
}

必须生成一个加密的key 和一个Cipher对象


//生成Cipher
private fun initCipher() {
val key = keyStore.getKey(DEFAULT_KEY_NAME, null) as SecretKey
val cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7)
cipher.init(Cipher.ENCRYPT_MODE, key)
showFingerPrintDialog(cipher)
}
//生成一个key
private fun initKey() {
keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val builder = KeyGenParameterSpec.Builder(DEFAULT_KEY_NAME,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
keyGenerator.init(builder.build())
keyGenerator.generateKey()
}

Demo 是kotlin 写的
Demo地址


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

面向 ChatGPT 开发 ,我是如何被 AI 从 “逼疯” 到 “觉悟” ,未来又如何落地

对于 ChatGPT 如今大家应该都不陌生,经过这么长时间的「调戏」,相信大家应该都感受用 ChatGPT 「代替」搜索引擎的魅力,例如写周报、定位 Bug、翻译文档等等,而其中不乏一些玩的很「花」的场景,例如: ChatPDF :使用 ChatPDF...
继续阅读 »

对于 ChatGPT 如今大家应该都不陌生,经过这么长时间的「调戏」,相信大家应该都感受用 ChatGPT 「代替」搜索引擎的魅力,例如写周报、定位 Bug、翻译文档等等,而其中不乏一些玩的很「花」的场景,例如:




  • ChatPDF :使用 ChatPDF 读取 PDF 之后,你可以和 PDF 文件进行「交谈」,就好像它是一个完全理解内容的「人」一样,通过它可以总结中心思想,解读专业论文,生成内容摘要,翻译外籍,并且还支持中文输出等





  • BiBiGPT : 一键总结视频内容,主要依赖字幕来做总结,绝对是「二创」作者的摸鱼利器。





所以把 ChatGPT 理解为「搜索引擎」其实并不正确,从上述介绍的两个落地实现上看, ChatGPT 不是单纯的统计模型,它的核心并不是完全依赖于它的「语料库」,更多来自于临场学习的能力「 in-context learning」,这就是 ChatGPT 不同于以往传统 NLP「一切都从语料的统计里学习」的原因



当然,我本身并非人工智能领域的开发者,而作为一个普通开发者,我更关心的是 ChatGPT 可以如何提升我的开(mo)发(yu)效率,只是没想到随手一试,我会被 ChatGPT 的 「 in-context learning」 给「逼疯」。



ChatGPT & UI


相信大家平时「面向」 ChatGPT 开发时,也是通过它来输出「算法」或者「 CURD」 等逻辑居多,因为这部分输出看起来相对会比较直观,而用 ChatGPT 来绘制前端 UI 的人应该不多,因为 UI 效果从代码上看并不直观 ,而且 ChatGPT 对与 UI 的理解目前还处于 「人工智障」的阶段。



但是我偏偏不信邪。。。。。



因为近期开发需求里恰好需要绘制一个具有动画效果的 ⭐️ 按键,面对这么「没有挑战性」的工作我决定尝试交给 ChatGPT 来完成,所以我向 ChatGPT 发起了第一个命令:



「用 Flutter 画一个黄色的五角星」




结果不负众望,关键部分如下代码所示,Flutter 很快就提供了完整的 Dart 代码,并且还针对代码提供了代码相关实现的讲解,不过运行之后可以看到,这时候的 ⭐️ 的样式并不满足我们的需求。



此时顶部的角也太「肥」了 。




所以我随着提出了调整,希望五角星的五个角能够一样大,只是没想到我的描述,开始让 ChatGPT 放飞自我



也许是我的描述并不准确?




在我满怀期待的 cv 代码并运行之后,猝不及防的「五角星」差点没让我喷出一口老血,虽然这也有五个角,但是你管这个叫 「五角星」 ???



这难道不是某个红白机游戏里的小飞机??




甚至于在看到后续 ChatGPT 关于代码的相关讲解时,我觉得它已经开始在「一本正经的胡说八道」,像极了今天早上刚给我提需求的产品经理



哪里可以看出五个角相同了???




接着我继续纠正我的需求,表示我要的是 「一个五个角一样大的黄色五角星」 ,我以为这样的描述应过比较贴切,须不知·····



如下代码所示,其实在看到代码输出 for 循环时我就觉得不对了,但是秉承着「一切以实物为准」的理念,在运行后不出意外的发生了意外,确实是五个角一样大,不过是一个等边五边形。



算一个发胖的 ⭐️ 能解(jiao)释(bian)过去不?




再看 ChatGPT 对于代码的描述,我发现我错了,原来它像的是「理解错需求还在嘴硬的我」,只是它在说「这是一个五角星」的时候眼皮都不会眨一下



AI:确实五个角一样大,五个角一样大的五边形为什么就不能是五角星?你这是歧视体型吗?




所以我继续要求:「我要的是五角星,不是五边形」,还好 ChatGPT 的临场学习能力不错,他又一次「重新定义五角星」,不过我此时我也不抱希望,就是单纯想看看它还能给出什么「惊喜」



不出意外,这个「离谱」的多边形让我心头一紧,就在我想着是否放弃的时候,身为人类无法驯服 AI 「既爱又恨」的复杂情绪,让我最终坚持一定要让 ChatGPT 给我画出一个 ⭐️。



不过心灰意冷之下,我选择让 ChatGPT 重新画一个黄色五角星,没想道这次却有了意外的惊喜,从下面的图片可以看到,此时的 ⭐️ 除了角度不对,形状已经完全满足需求。



所以一个问题我多问几遍,也许就能接近我要的答案?




事实上这也是目前 ChatGPT 的现状,因为「临场学力」能力等因素影响,同一个问题它可能会给出不同的答案,而有的答案其实和我们要的根本不沾边



那么,接下来只要让 ChatGPT 把 ⭐️ 旋转一下角度,应该就可以完成需求····了吧?所以我提出「帮我旋转 180度」的要求。



结果不出意外的还是发生了意外,可能 ChatGPT 理解的 180 度和我不大一样,如下图所示,确实旋转了,只是还是歪的,而基于前面的尝试,我觉得有必要再给它一次机会。



终于,在我换了以下描述之后,ChatGPT 似乎「开窍」了,终于让我如愿以偿的得到了我想要的 ⭐️ 代码。



可以看到,其实这段代码并不复杂,你自己写或者去搜索引擎扒一扒大概会比用 ChatGPT 更快,但是 ChatGPT 的魅力就在于:它也许会错,但是总有一次它会「蒙对」,如果现在还不对,那么未来也会有算对的时候,这就是我感受到的 AI 魅力所在



既然五角星都出来,那么接下来我们来完成最后一步的动画效果,我们希望的是点击 ⭐️ 的时候可以有一个爆炸粒子的动画,所以我提出了:



「希望加上一个动画效果,就是点击的时候星星爆炸成颗粒的动画效果」




结果还是「一口老血」,如下图所示,「星星确实爆炸了」,只是这个爆炸动画看起来是那么的「一言难尽」,感兴趣的可以 cv 下面的源代码自己体验下,不出意外接下来又是新的一轮人类和 AI 的「斗智斗勇」。




class YellowStar extends StatefulWidget {
@override
_YellowStarState createState() => _YellowStarState();
}

class _YellowStarState extends State<YellowStar> with TickerProviderStateMixin {
bool _isExploded = false;
late AnimationController _animationController;
late Animation<double> _animation;

@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
}

@override
void dispose() {
_animationController.dispose();
super.dispose();
}

void _handleTapDown(TapDownDetails details) {
setState(() {
_isExploded = true;
_animationController.forward(from: 0.0);
});
}

void _resetStar() {
setState(() {
_isExploded = false;
_animationController.reset();
});
}

@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: _handleTapDown,
onTap: _resetStar,
child: AnimatedContainer(
duration: const Duration(milliseconds: 500),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: _isExploded
? BorderRadius.circular(50)
: BorderRadius.zero,
),
child: _isExploded
? _buildParticles()
: Container(
width: 100,
height: 100,
child: CustomPaint(
painter: YellowStarPainter(),
),
),
),
);
}

Widget _buildParticles() {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: 100,
height: 100,
child: Stack(
children: List.generate(
50,
(index) {
double radius = _animation.value * 50;
double angle = 2 * pi * index / 50;
double x = 50 + cos(angle) * radius;
double y = 50 + sin(angle) * radius;
return Positioned(
left: x,
top: y,
child: Container(
width: 4,
height: 4,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.yellow,
),
),
);
},
),
),
);
},
);
}
}



class YellowStarPainter extends CustomPainter {
final double starSizeRatio = 0.4;
final double centerOffsetRatio = 0.2;
final double rotationOffset = -pi / 2;

@override
void paint(Canvas canvas, Size size) {
double starSize = min(size.width, size.height) * starSizeRatio;
double centerX = size.width / 2;
double centerY = size.height / 2;
double centerOffset = starSize * centerOffsetRatio;

Path path = Path();
Paint paint = Paint()
..color = Colors.yellow
..style = PaintingStyle.fill;

for (int i = 0; i < 5; i++) {
double radians = 2 * pi / 5 * i + rotationOffset;
double x = centerX + cos(radians) * starSize / 2;
double y = centerY + sin(radians) * starSize / 2;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}

radians += 2 * pi / 10;
x = centerX + cos(radians) * centerOffset;
y = centerY + sin(radians) * centerOffset;
path.lineTo(x, y);
}

path.close();
canvas.drawPath(path, paint);
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

经过一轮「鏖战」如下图所示,面对同一个问题,ChatGPT 总能找到不同的实现方式来回答你,只是这些实现里有的无法运行的,有的是运行没有效果的,有的甚至是给你输出了一个 //TODO 爆炸动画 ,总之就是让你「血压飙升」。





当然,「胜利终将属于人类」,如下图所示,在经历了无数次「血压飙升」的场景之后,最终我还是得到了我想要的:点击 ⭐️,然后出现了💥的动画效果,尽管还是不完美,但是基本满足了我后续开发的需求。



这个代码完完全全由 ChatGPT 输出,我一句没改,完整代码贴在了后面。





class YellowStarPage extends StatefulWidget {
@override
_YellowStarPageState createState() => _YellowStarPageState();
}

class _YellowStarPageState extends State<YellowStarPage>
with SingleTickerProviderStateMixin
{
late AnimationController _animationController;
bool _isExploded = false;

@override
void initState() {
super.initState();
_animationController =
AnimationController(vsync: this, duration: Duration(milliseconds: 500))
..addListener(() {
setState(() {});
});
}

@override
void dispose() {
_animationController.dispose();
super.dispose();
}

void _handleStarTap() {
if (!_isExploded) {
_isExploded = true;
_animationController.forward(from: 0);
Future.delayed(Duration(milliseconds: 500), () {
setState(() {
_isExploded = false;
});
});
}
setState(() {});
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Yellow Star')),
body: Center(
child: GestureDetector(
onTap: _handleStarTap,
child: Container(
width: 300,
height: 300,
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return CustomPaint(
painter: YellowStarPainter(_animationController.value,
isExploded: _isExploded),
);
},
),
),
),
),
);
}
}

class YellowStarPainter extends CustomPainter {
final double starSizeRatio = 0.4;
final double centerOffsetRatio = 0.2;
final double rotationOffset = -pi / 2;

final double animationValue;
final bool isExploded;

YellowStarPainter(this.animationValue, {this.isExploded = false});

@override
void paint(Canvas canvas, Size size) {
double starSize = min(size.width, size.height) * starSizeRatio;
double centerX = size.width / 2;
double centerY = size.height / 2;
double centerOffset = starSize * centerOffsetRatio;

Path path = Path();
Paint paint = Paint()
..color = Colors.yellow
..style = PaintingStyle.fill;

if (isExploded) {
double particleSize = starSize / 30;
paint.strokeWidth = 1;
paint.style = PaintingStyle.fill;
paint.color = Colors.yellow;
Random random = Random();

for (int i = 0; i < 30; i++) {
double dx = random.nextDouble() * starSize - starSize / 2;
double dy = random.nextDouble() * starSize - starSize / 2;
double x = centerX + dx * (1 + animationValue);
double y = centerY + dy * (1 + animationValue);

canvas.drawCircle(Offset(x, y), particleSize, paint);
}
} else {
for (int i = 0; i < 5; i++) {
double radians = 2 * pi / 5 * i + rotationOffset;
double x = centerX + cos(radians) * starSize / 2;
double y = centerY + sin(radians) * starSize / 2;
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}

radians += 2 * pi / 10;
x = centerX + cos(radians) * centerOffset;
y = centerY + sin(radians) * centerOffset;
path.lineTo(x, y);
}

path.close();
canvas.drawPath(path, paint);
}
}

@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}


最后,给大家欣赏一下我让 ChatGPT 画一只米老鼠的「心路历程」,很明显这一次「人类一败涂地」,从目前的支持上看,让 ChatGPT 输出复杂图像内容并不理想,因为它不的笔画「不会拐弯」。




真的是又爱又恨。



最后


经过上面的一系列「折腾」,可以看到 ChatGPT 并没有我们想象中智能,如果面向 GPT 去开发,甚至可能并不靠谱,因为它并不对单一问题给出固定答案,甚至很多内容都是临场瞎编的,这也是因为大语言模型本身如何保证「正确」是一个复杂的问题,但是 ChatGPT 的魅力也来自于此:



它并不是完全基于语料来的统计来给答案



当然这也和 ChatGPT 本身的属性有关系, ChatGPT 目前的火爆有很大一部分属于「意外」,目前看它不是一个被精心产品化后的 2C 产品,反而 ChatPDFBiBiGPT 这种场景化的包装落地会是它未来的方向之一。


而现在 OpenAI 发布了多模态预训练大模型 CPT-4GPT-4 按照官方的说法是又得到了飞跃式提升:强大的识图能力;文字输入限制提升至 2.5 万字;回答准确性显著提高;能够生成歌词、创意文本,实现风格变化等等



所以我很期待 ChatGPT 可以用 Flutter 帮我画出一只米老鼠, 尽管 ChatGPT 现在可能会让你因为得到 1+1=3 这样的答案而「发疯”」,但是 AI 的魅力在于,它终有一天能得到准确的结果


作者:恋猫de小郭
来源:juejin.cn/post/7210605626501595195
收起阅读 »

如何写一个炫酷的大屏仿真页

web
前言 之前我写过一遍文章《从阅读页仿真页看贝塞尔曲线》,简要的和大家介绍了仿真页的具体实现思路,正好写完文章的时候,看到 OPPO 发布会里面提到了仿真页,像这样: 看着确实有点炫酷,我平时也接触了很多跟阅读器相关的代码,就零零碎碎花了一些时间撸了一个双页仿...
继续阅读 »

前言


之前我写过一遍文章《从阅读页仿真页看贝塞尔曲线》,简要的和大家介绍了仿真页的具体实现思路,正好写完文章的时候,看到 OPPO 发布会里面提到了仿真页,像这样:


OPPO折叠屏


看着确实有点炫酷,我平时也接触了很多跟阅读器相关的代码,就零零碎碎花了一些时间撸了一个双页仿真。


看效果:


11.gif


由于使用录屏,所以看着有点卡顿,实际效果非常流畅!


一、基础知识具备


仿生页里面用到很多自定义 View 的知识,比如:



  1. 贝塞尔曲线

  2. 熟悉 Canvas、Paint 和 Path 等常用的Api

  3. Matrix


具备这些知识以后,我们就可以看懂绝大部分的代码了。这一篇同样并不想和大家过多的介绍代码,具体的可以看一下代码。


二、双仿真和单仿真有什么不同


我写双仿真的时候,感觉和单仿真有两点不同:



  • 绘制的页数

  • 背部的贴图处理


首先,单仿真只要准备两页的数据:


QQ20230312-0.jpg


背部的内容也是第一页的内容,需要对第一页内容进行翻转再平移。


而双仿真需要准备六页的内容,拿左边来说:


QQ20230312-1.jpg


我们需要准备上层图片(柯基)、背部图片(阿拉斯加)和底部图片(吉娃娃,看不清),因为我们不知道用户会翻页哪侧,所以两侧一共需要准备六页的数据。


由于翻转机制的不一样,双仿真对于背部的内容只需要平移就行,但是需要新的一页内容,这里相对来说比单仿真简单。


三、我做了哪些优化


主要对翻页的思路进行了优化,


正常的思路是这样的,手指落下的点即页脚:


QQ20230312-2.jpg


这样写起来更加简单,但是对于用户来说,可操作的区域比较小,相对来说有点难用。


另外一种思路就是,手指落下的点即到底部同等距离的边:


QQ20230312-4.jpg


即手指落位的位置到当前页页脚距离 = 翻动的位置到当前页脚的距离


使用这种方式的好处就是用户可以操作的区域更大,翻书的感觉跟翻实体书的感觉更类似,也更加跟手。


总结


这篇文章就讲到这了,这个 Demo 其实是一个半成品,还有一些手势没处理,阴影的展示还有一些问题。


写仿真比较难的地方在于将一些场景转化成代码,有些地方确实很难去想。


talk is cheap, show me code:


仓库地址:github.com/mCyp/Double…


如果觉得本文不错,点赞是对本文最好的肯定,如果你还有任何问题,欢迎评论区讨论!


作者:九心
来源:juejin.cn/post/7209625823581978680
收起阅读 »

Android 可视化预览及编辑Json

Android 可视化编辑json JsonPreviewer 项目中涉及到广告开发, 广告的配置是从API动态下发, 广告配置中,有很多业务相关参数,例如关闭或开启、展示间隔、展示时间、重试次数、每日最大显示次数等。 开发时单个广告可能需要多次修改配置来测试...
继续阅读 »


Android 可视化编辑json JsonPreviewer


项目中涉及到广告开发, 广告的配置是从API动态下发, 广告配置中,有很多业务相关参数,例如关闭或开启、展示间隔、展示时间、重试次数、每日最大显示次数等。


开发时单个广告可能需要多次修改配置来测试,为了方便测试,广告配置的json文件,有两种途径修改并生效





    1. 每次抓包修改配置





    1. 本地导入配置,从磁盘读取




但两种方式都有一定弊端



  • 首先测试时依赖电脑修改配置

  • 无法直观预览广告配置


考虑到开发时经常使用的Json格式化工具,既可以直观的预览Json, 还可以在线编辑


那么就考虑将Json格式化工具移植到项目测试模块中


web网页可以处理Json格式化,同理在Android webView 中同样可行, 只需要引入处理格式化的JS代码即可。


查找资料,发现一个很实用的文章可视化编辑json数据——json editor


开始处理


首先准备好WebView的壳子


    //初始化
@SuppressLint("SetJavaScriptEnabled")
private fun initWebView() {
binding.webView.settings.apply {
javaScriptEnabled = true
javaScriptCanOpenWindowsAutomatically = true
setSupportZoom(true)
useWideViewPort = true
builtInZoomControls = true
}
binding.webView.addJavascriptInterface(JsInterface(this@MainActivity), "json_parse")
}

//webView 与 Android 交互
inner class JsInterface(context: Context) {
private val mContext: Context

init {
mContext = context
}

@JavascriptInterface
fun configContentChanged() {
runOnUiThread {
contentChanged = true
}
}

@JavascriptInterface
fun toastJson(msg: String?) {
runOnUiThread { Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show() }
}

@JavascriptInterface
fun saveConfig(jsonString: String?) {
runOnUiThread {
contentChanged = false
Toast.makeText(mContext, "verification succeed", Toast.LENGTH_SHORT).show()
}
}

@JavascriptInterface
fun parseJsonException(e: String?) {
runOnUiThread {
e?.takeIf { it.isNotBlank() }?.let { alert(it) }
}
}
}


加载json并在WebView中展示



viewModel.jsonData.observe(this) { str ->
if (str?.isNotBlank() == true) {
binding.webView.loadUrl("javascript:showJson($str)")
}
}


WebView 加载预览页面


        binding.webView.webViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
viewModel.loadAdConfig(this@MainActivity)
}
}

binding.webView.loadUrl("file:///android_asset/preview_json.html")



Json 预览页, preview_json.html实现



<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="jquery.json-viewer.css"
rel="stylesheet" type="text/css">

</head>
<style type="text/css">
#json-display {
margin: 2em 0;
padding: 8px 15px;
min-height: 300px;
background: #ffffff;
color: #ff0000;
font-size: 16px;
width: 100%;
border-color: #00000000;
border:none;
line-height: 1.8;
}
#json-btn {
display: flex;
align-items: center;
font-size: 18px;
width:100%;
padding: 10;

}
#format_btn {
width: 50%;
height: 36px;
}
#save_btn {
width: 50%;
height: 36px;
margin-left: 4em;
}

</style>
<body>
<div style="padding: 2px 2px 2px 2px;">
<div id="json-btn" class="json-btn">
<button type="button" id="format_btn" onclick="format_btn();">Format</button>
<button type="button" id="save_btn" onclick="save_btn();">Verification</button>

</div>
<div>
<pre id="json-display" contenteditable="true"></pre>
</div>
<br>
</div>

<script type="text/javascript" src="jquery.min.js"></script>
<script type="text/javascript" src="jquery.json-viewer.js"></script>
<script>

document.getElementById("json-display").addEventListener("input", function(){
console.log("json-display input");
json_parse.configContentChanged();
}, false);
function showJson(jsonObj){
$("#json-display").jsonViewer(jsonObj,{withQuotes: true});//format json and display
}
function format_btn() {
var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();
var jsonval = my_json_val.text();
var jsonObj = JSON.parse(jsonval); //parse string to json
$("#json-display").jsonViewer(jsonObj,{withQuotes: true});//format json and display
}


function save_btn() {
var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();
var jsonval = my_json_val.text();
var saveFailed = false;
try {
var jsonObj = JSON.parse(jsonval); //parse
} catch (e) {
console.error(e.message);
saveFailed = true;
json_parse.parseJsonException(e.message); // throw exception
}
if(!saveFailed) {
json_parse.saveConfig(jsonval);
}
}

</script>
</body>
</html>


这其中有两个问题需注意





    1. 如果value的值是url, 格式化后缺少引号
      从json-viewer.js源码可以发现,源码中会判断value是否是url,如果是则直接输出




处理方式:在json 左右添加上双引号


    if (options.withLinks && isUrl(json)) {
html += '<a href="' + json + '" class="json-string" target="_blank">' + '"' +json + '"' + '</a>';
} else {
// Escape double quotes in the rendered non-URL string.
json = json.replace(/&quot;/g, '
\\&quot;');
html += '<span class="json-string">"' + json + '"</span>';
}




    1. 如果折叠后json-viewer会增加<a>标签,即使使用text()方法获取到纯文本数据,这里面也包含了“n items”的字符串,那么该如何去除掉这些字符串呢?




 var my_json_val = $("#json-display").clone(false);
my_json_val.find("a.json-placeholder").remove();

总结


使用时只需将json文件读取,传入preview_json.html的showJson方法


编辑结束后, 点击Save 即可保存


示例代码 Android 可视化编辑json JsonPreviewer




(可视化编辑json数据——json editor)[blog.51cto.com/u_56500

11/5…]

收起阅读 »

我本可以忍受黑暗,如果我未曾见过光明

随想录】我本可以忍受黑暗,如果我未曾见过光明 随想录 这是师叔对自我现状的剖析和寻找了一些 “新的方向” “新的视角” 来重新审视自我的思想录,希望我的家银们在文章中得到思想启发或以我为鉴,不去做无谓思想内耗! 老文章? 这篇文章大体结构早已在我语雀...
继续阅读 »

随想录】我本可以忍受黑暗,如果我未曾见过光明



随想录


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



老文章?


这篇文章大体结构早已在我语雀里写完了很久很久~~~


假期就有构思了,现在埋坑


因为这篇文章写的时候太过于冲劲十足,太过于理想主义,但是反顾现实我当时正在经历考试挂科,没错,就是你理解的大三挂科了(这也就意味着我开学要经历补考,如果没过的话,可能大四不能实习,还要和下一届同学一起上课,而且下一届还是我带的班级,想想那种感觉“咦,武哥你怎么在这上课”而我,内心qs:杀了我把,太羞辱了,脚指头已经扣除一套四合院了)


朋友问我成绩,当时孩子都傻了


所以这段时间我正在经历自我内耗,就向是欠了谁东西,到了deadline,到了审判的日子才能释怀!也至于最近心理一直在想着这个事情,导致最近焦虑的一批,最近几天自己都不正常了,但是终于结束了~~~(非常感谢老师)



言归正传


好了好了,又跑题了,书归正题,你可能会疑惑我为什么用这个标题,难道我经历了什么涩会黑暗,被潜规则,被PUA......(给你个大逼斗子,停止瞎想,继续向下看)



这篇文章灵感来源于我很喜欢的B站一位高中语文老师讲解《琵琶行》,突然我被这个短短 3分51秒的视频搞得愣住了,直接神游五行外,大脑开始快速的回顾自己最近的生活~~~(再次表白真的很爱这摸温柔的语文老师,他的课真的让我感觉到什么叫“腹有诗书气自华”)



视频链接:https://www.bilibili.com/video/BV1bW4y1j7Un/
复制代码

最爱的语文老师


其实人生当中很残忍的一个事儿是什么呢?就是你一直以为未来有无限可能的时候,就像琵琶女觉得她能够过上那样的生活一直下去。一直被“五陵年少争缠头”,一直被簇拥着的时候,突然有一天你意识到好像这辈子就只能这样,就只能去来江头守空船,守着这一这艘空船,默默的度过慢慢的长夜。
就是如果如果你不曾体验过那样的生活,你会觉得好像“我”最终嫁给了一个商人,然后至少衣食不愁,至少也能活得下去,好像也还算幸福。但是如果我曾经经历过那样的生活,我此刻内心多多少少是有些不甘的。


很喜欢的一幅油画


亦或者是像白居易,如果他是从平民起身,然后一直一步一步做到了江州司马可能觉得也还是不错,但是你要知道他在起点就是在京城为官,所以这里其实是有很明显的,一种落差。那也同样,如果此刻你回到我们说所有的文学都是在读自己,你想想看你自己,此刻你可能没有这种感觉。


30公里鲜啤



哈哈哈,兄弟们不要emo啊,让我们珍惜当下,还是那句话,我们还年轻,谁都不怕。(但是遇到刀枪棍棒还是躲一躲呀,毕竟还是血肉之躯)



其实反思反思人生中最大的挑战,就是接受自己生来平凡。自己没有出色的外表,我也没有过人的才华,我可能也少了些许少年时的锐意。但是这个emo点我并不care,因为我还在拥有选择的阶段,我也在尝试探索不一样的人生,这也许就是喜欢记录生活和写下灵机一动时候想法的意义。但是也就向UP主@peach味的桃子记录自己第44次开学,也是最后一次开学表达自己点点滴滴,也同样是不同的感受;我们同样有应届生的迷茫,但是想想也没什么可怕,还在学习,还在向目标奔跑,也还在享受校园生活~~~


打卡老馆子-群乐饭店


啊呀,好像又唠跑偏了,就是说我对这个视频那么的不一样,尤其是这个主题,因为自己的寒假的实习给我带来了新的视野,哦不,应该是旷野,很有幸能去华为在我们省份的办事处,又被出差派往华为在一个某市分部工作了半个月。这短短的实习经历,让我在大三这个迷茫的时期多了份坚定,在这个期间和大佬们一起工作,真的看到了人家的企业文化和那种行动力,最主要被军团的大佬们很牛掰技术折服,在相处这段时间真的知道了什么是向往的生活,这个学历门槛迈过去,你将会迎来什么样的明天~~~


(谁说我去卖手机去了,我揍他啊[凶狠])


游客打卡照


所以我可能对之前年终总结看法有了些改变,我之前年终总结写到,薪资又不会增加多少,浪费三年那不纯属XX嘛,没错,今天我被打脸了,为我之前的幼稚想法感到可笑;写到这里脑子已经开始疼了,最近甲流,朋友圈注意身体,这个东西真的会影响我们的战斗力,好吧,这也只是一个随想录,留点内容给年中总结,要不到时候就词穷了,哈哈~~


很nice的江景房


近期反思


其实每个人的出发点不一样不能一概而论,就向我自己出发,一个来自十八线农村的孩子,父母通过自己一代人的努力从农村到乡镇,而我就通过自己的求学之路一直到,貌似能够在这个省份的省会立足,这也就是我能做的进步,不管怎么说,我们都是从自身出发,其实谈到这个问题,我自身也很矛盾,小城市就真的不好吗,人的一生除了衣食无忧,在向下追求的不就是快乐,如果真的能和一个爱的人,在做一些自己喜欢做的事情,难道不就是“人生赢家”,城市在这种维度下考虑貌似也不重要~~(如果你想diss这种想法,没有考虑子女的教育问题,其实我想到了,但是我目前的年龄和所处的位置吧,感觉很片面,所以就不对这个点展开讨论了)


过度劳累,小酌一杯


回复问题


有人怕别人看到自己以往的文章写的很幼稚,就不想写了,我有不同的看法,只有看到曾经的对事情的看法和处理方式幼稚了,才能证明自己的成长呀,谁能一下子从孩子成为一个大人!(但是某些时候谁还是不是一个孩子[挑眉])



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

你没有必要完全辞去工作

我认为我们可以而且应该探索生活中的许多事情,我写这篇文章是为了展示成为一名创客和拥有一份全职工作不仅是可能的,而且使得你可用的机会多样化,这可以让你更加敏锐,务实与坚定。 在这篇文章中,我想解决三个关键概念。 首先是针对那些认为自己时间不够的人,以及为什么我觉...
继续阅读 »

我认为我们可以而且应该探索生活中的许多事情,我写这篇文章是为了展示成为一名创客和拥有一份全职工作不仅是可能的,而且使得你可用的机会多样化,这可以让你更加敏锐,务实与坚定。


在这篇文章中,我想解决三个关键概念。


首先是针对那些认为自己时间不够的人,以及为什么我觉得这种观念往往是错误的。


第二是强调坚持工作的好处,以及为什么成功人士最擅长的是降低风险,而不是最大化。


最后,第三部分将指出出一些我认为我们都可以在思维方式上做出的改进--超越单纯地创业和全职工作,这些概念希望能帮助你在一个更大的空间里进行优化,或者也许完全去除这个限制。


1.“我没有足够的时间”


美国人平均工作时长为 8.8 小时,这源于工业革命,并一直延续到 21 世纪,这处于一般的考量,而不是深思熟的考虑。罗伯特·欧文 (Robert Owen) 精心设计了“八小时劳动、八小时娱乐、八小时休息”的说法,努力让人们在合理的时间内工作,同时仍然能有效地运营工厂。


尽管世界和劳动力发生了翻天覆地的变化,但这种“工作时间”和“我的时间”的概念至今仍然存在。我在这里不是要质疑 40 小时模型(已经有太多资源了 - 谁还没有听说过 4 小时工作周?),而是质疑对“我的时间”的看法。


对许多人来说,长时间的工作意味着他们有权享受这种 "我的时间",并将 "我的时间 "设计得与 "工作时间 "尽可能地不同。对许多人来说,它看起来很像这样。Netflix and chill


但是,如果我们不再将“我的时间”想象成放松时间,而是完全按照它的标题:是时候专注于自己并与您的目标保持一致,那会怎样呢?如果你需要休息,那就休息吧。但如果你的目标是有朝一日成为一名企业家,那么应该投入大量的“我的时间”来实现这一目标,因为它不会自行发生。 “我的时间”不应该只是不累人的活动,而是任何可以帮助个人达到他们希望进入的未来状态的活动。


每天大约有 16 个小时分配给工作和睡眠,每个人大约有 8 个小时可以分配给“我的时间”,如果使用得当,每年将近 3000 个小时可以取得很多成就。


Sleep, commute, work, repeat. 睡觉、通勤、工作,如此重复。


Sleep, commute, work, repeat. 睡觉、通勤、工作,如此重复。



“Most people overestimate what they can do in a day, but underestimate what they can do in a year.”

“大多数人高估了他们一天能做的事情,却低估了他们一年能做的事情。”



还有一种误解是,为了建立一个可持续的业务,你需要花费大量的时间才能达到目的。虽然确实需要付出大量的努力,但最重要的是长期持续的努力。大多数人低估了复利的这个概念。


compound-interest.jpg


The power of compound interest. 复利的力量。


看看下面的等式:



  • 1.01³⁶⁵ = 37.8

  • 1.10³⁰ = 17.5


在一年内每天坚持改善你的业务(或生活)1%,比一个月内每天改善 10% 的效果要好一倍。坚持不懈加上复利的力量是强大的。



“如果一切都是最重要的,那么就没有什么是重要的。”



我认为,大多数人在生活中要么没有清楚地确定优先事项,要么将所有事情都考虑在内。虽然我相信雄心壮志,但成功的一个关键步骤是确定核心的优先事项,并消除在此之外的噪音。


最主要的优先事项是动态的,可以随着时间的推移而改变,但我认为,在一个特定的时间,你真的不能有超过 3 个核心重点。


设定这些重点之后,就是要改变行为,按照这些重点生活。再次,如果大多数人要客观地反思他们是如何花费时间的,他们会得到这样的结果。


1_nALgHXJAKmPcyqO10XvvCw.jpg


典型的一天。


对我来说,这就是我的个人优先事项随着时间的推移而发生的变化:



  • 2017 年:工作、旅行、人际关系

  • 2018 年:工作、学习编码、构建副业

  • 2019 年:工作、扩展副业、分享想法(写作、演讲)


为了在创作项目的同时维持一份全职工作,我不得不排除干扰。例如,我不看电视。我不通勤。我目前没有恋爱。这些都是主动的选择。


当然,其中一些东西将是暂时的(例如:人际关系),但我也注意到我在生活中重新引入的东西,以及它是否会促进、带走,或成为我的北极星之一。


我认为这个概念也可以被认为是分层的时间投资。对于你所做的任何事情,如果它有助于你的北极星,就把它看作是一级投资。对于那些对你的成长完全没有贡献的事情,也许可以把它标记为第四级。这并不意味着你不能跨层花费时间,但你花在每个层级上的时间应该反映出你对它们的关心程度。


示例(这对任何人来说都是一个独立的练习):



2. 坚持工作的好处


希望上一节有助于说服你,你有足够的时间全职工作,同时创建副业,或者说,如果你调整你的价值观→优先事项→行为,就可以在你的生活中融入更多的东西。在这一节中,我希望能表达为什么保持全职工作可以是一件美好的事情。


付费学习



“Some workplaces are definitely broken, but the entire workforce isn’t.”



我经常听到有人说 "我等不及要出去 "这样的话,指的是辞掉工作,最终自己当老板。在辞职之前,要考虑为什么要辞掉工作。通常情况下,这不是他们有全职工作的问题,而是他们所从事的特定工作,或许是他们所汇报的特定人员。


所有的人都应该努力找到一份能够赋予他们权力、激励他们并让他们在某些方面得到成长的工作。大公司实际上保证了这一点--你很少是公司里最聪明的人,你当然也不会是公司里每个方面都最有能力的人


在我的 "日常工作 "中,我可以不断向比我更聪明的人学习,并为此获得报酬。我还面临着我的副业项目根本不会遇到的挑战,我经常需要学习如何与他人一起解决这些挑战。我鼓励人们有意识地设计他们的职业道路,以掌握从硬到软的新技能。如果你最终决定在未来自立门户,那么这两者都会很重要。


随着劳动力变得更加活跃,在协同处理自己的项目的同时向他人学习的能力是许多人正在开发的。事实上,我在 Twitter 上对数百人进行了调查,发现相当多的人都在这样做。


保持新鲜的想法和清晰的头脑


在学习之外,保持一份 FT 工作还有其他实实在在的好处,可以帮助你建立一个更可持续的副业。


根据个人经验,我发现把我的工作和副业分开,使我仍能在两者中找到独立的乐趣。每当我从一个环境切换到另一个环境,特别是在创业方面,它仍然是 "有趣的"。


我认为这特别是因为在目前的状态下,创业不是我的生命线。我希望有一天它确实成为更有意义的东西,但就目前而言,我可以在不受立即赚钱需求影响的情况下就我的项目做出决定。


更重要的是,我可以专注于通过我真正关心的项目来表达自己,而不是专注于可能产生美元的东西,通过这个过程,我贴近我的价值观。换句话说,我可以专注于创造价值,而不是专门去获取价值,类似于 Gumroad 的创始人 Sahil Lavingia 如何转向做这件事,或者 Warby Parker 的创始人如何确保金钱不会战胜他们的价值观。



“开始之前我们是四个朋友,我们承诺公平对待彼此比成功更重要。” —— 亚当·格兰特



结合以上几点,当我意识到一个项目没有任何价值时,我可以放弃一个项目或理性思考,我也不需要拿 VC 的钱或倾向于我不相信的投资者。



“在一个领域拥有安全感让我们可以自由地在另一个领域独创。通过在财务上覆盖我们的基地,我们摆脱了出版半生不熟的书籍、出售劣质艺术品或开展未经考验的业务的压力。” —— 亚当·格兰特



最后,我可以在技能学习方面投入适当的时间。我把这比喻为这样一个概念:上市公司不太关注通过创新创造长期价值,而是关注下一个季度的收入数字。我是一只私人股票,可以专注于我自己和我的技能,目的是为了长期建设它们。


换句话说,我的表达和创意之间的明确区分与我的生命线分离,我认为这有助于做出更有效的决定。


进行大量试验,然后全力以赴



“企业家这个词,正如经济学家理查德·坎蒂隆创造的那样,字面意思是“风险承担者”。 —— 亚当·格兰特



有一个普遍的误解,认为企业家都是“冒险者”,你需要“全力以赴”才能成功。在亚当·格兰特的著作 Originals (中文名:《离经叛道:不按常理出牌的人如何改变世界》)中,这两者都被证明是错误的;企业家不一定是冒险者,而是更善于评估风险和对冲他们的赌注。



“当 Pierre Omidyar 创立 eBay 时,这只是一种爱好;在接下来的九个月里,他一直以程序员的身份工作,直到他的在线市场为他赚的钱比他的工作还多才离开。最好的企业家不是风险最大化者。 “他们在冒险中承担了风险。” —— 亚当·格兰特



Grant 还引用了 Joseph Raffiee 和 Jie Feng 的另一项研究,该研究从 1994 年到 2008 年对 5000 多名美国人提出了以下问题:“当人们开始创业时,他们最好是继续工作还是辞掉日常工作?”


结果呢?他们发现,那些离开工作岗位的人这样做不是出于经济需要,而是出于纯粹的自信。然而,那些更不确定的人比更喜欢冒险的人失败几率要低 33%。


另一项研究表明,那些在 Fast Company 最具创新力排行榜上名列前茅的企业家也倾向于坚持他们的日常工作,包括著名企业家 Phil Knight(耐克)、Steve Wozniak(苹果)以及谷歌创始人 Larry Page 和 Sergey Brin。


奈特当了 5 年的会计师,同时从他的后备箱里卖鞋,沃兹尼亚克继续在惠普工作,谷歌人继续在斯坦福大学攻读博士学位。这些只是书中的一些原作——Grant 还引用了类似的故事,包括 Brian May 在加入 Queen 之前研究天体物理学,John Legend 即使在发行他的第一张专辑后仍然担任管理顾问,Spanx 创始人 Sara Blakely 销售传真机,她的公司原型和规模最终成为世界上最年轻的白手起家的亿万富翁,著名作家斯蒂芬金在他的第一个故事发表后担任了 7 年的看门人、教师和加油站服务员。


我们都有多种激情,我认为生活就是在有意义的时候进行战略转型。无需立即从一个场景切换到另一个场景。人们可能认为冒险者很酷,但在另一边取得成功更酷。


3. 重构你的思维方式


无论你是否选择全职工作,同时探索副业项目,我认为我们都可以更有效地打开我们的思想,接受不同的思维方式。本节将涉及一些我认为我们可以停止限制自己和他人的方式。


世间安得两全法


人们喜欢把东西装进盒子里。你会听到人们总是使用名词或形容词作为明确的标签:



  • 技术还是非技术

  • 快乐或悲伤

  • 职员或企业家


看到我要去哪里了吗?尽管有这些标签,但我相信几乎所有东西都可以用某种曲线表示;特别是在技能习得方面。例如,你什么时候真正“成为”程序员?


1_MEZ99GbXHnSq27aDQaBxcQ.jpeg


真正的创造性思维者不再用二元思维,而是能够将这些曲线的概念内化。他们把事情看成一个斜坡、楼梯或维恩图,而不是一系列的盒子。当你消除二元对立时,你就能更清楚地看到其他选择,比如慢慢增加你对副业的时间投入,而不是立即辞职。


合理规划您的生活


我认为,如果有人认为自己的工作效率已经达到全球最高水平,那是非常天真的。事实是,我们都有改进的余地,不仅是在更快/更干净方面,而且是在做出更好地决定,删除那些首先不应该出现在我们盘子里的工作。


如果您选择从事多项工作,请确保您对所有这些都有独立的 KPI。人们倾向于在企业中这样做,但这个概念在我们的个人生活中却很少见。你能量化过去一年你在自己身上投入了多少时间吗?大多数人做不到。


如果两者都没有 KPI,那么没有明确 KPI 的那个自然会被搁置一旁,或者得不到应有的关注。


我还认为理解“元工作”的概念很重要。我对元工作的定义如下:“如果你连续一年做那个活动,你的生活会有什么不同吗?”


让我详细说明。


如果明年我每天都回复电子邮件,我的生活会不会发生重大变化?换句话说,我会从 A 搬到 B 吗?答案是不。


洗衣服、买杂货或做指甲等事情也是如此。哦,是的,Netflix 也一样。


还有第二种类型的任务,我将其标记为绝对任务。如果始终如一地完成,您可能会看到您的技能或生活发生重大变化。例如:如果你每天阅读一年,你的知识储备、创造力和阅读速度都可能会提高。如果你每天锻炼,你的健康无疑会有所改善。同样,如果您每天花 1 小时学习编码,到年底您将拥有全新的技能组合。


虽然元任务在生活中是不可避免的,但要确保你的生活目标不是元的,它们需要是绝对的。当你创建当天的待办事项清单时,确保至少有一件事是绝对的(记住:1.01³⁶⁵=37.8)。当然,当你可以时:尽可能多地将元任务自动化。元任务在很多方面都可以成为分心的代名词,除非它们给你的生活带来某种独立的快乐。


一夜成名的神话


最后,我想澄清最后一个误解:没有一夜成名这回事。这种误解源于媒体的运作方式。


TechCrunch 永远不会写 X 人如何用 Y 年时间引导一个可持续的非独角兽企业,遵守其价值观并尊重人们的隐私。离群索居者很耀眼,但他们仍然是离群索居者。


直到几年前,我才真正理解持续攀登的概念。我以为每一个成功的人都说要付出很多工作和努力,这只是在为他们的运气自我辩护。



"当我们惊叹于那些为创造力提供动力并推动世界变革的原创者时,我们往往会认为他们是由不同的布料剪成的。" -- 亚当-格兰特



现实情况是,构建任何有价值的东西都需要时间。当然,全职工作可能需要更长的时间来构建,但这没关系。


如果你目前有全职工作,不要把自己放在一个框框里,而是要开始为你觉得有趣的想法工作。完美的想法永远不会出现,,所以我鼓励每个人开始每周花 1 小时来研究他们认为有吸引力的想法,并逐渐增加,直到你处于一个可以让他们全职工作的地方。将你的生命线(你的工作)与你的项目分开,这种精神上的清醒可能是最健康和最周到的做法。


记住,你没有成为企业家的时刻,所以没有必要为了将自己定义为企业家而辞掉工作。


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

想退更多税?看完这篇就够了

前言 最近又到了一年一度的退税了,最近发了一个沸点,发现大家都是和我之前一样不知道如何能退更多税,我也是无意中和小伙伴聊的时候才发现这个,如何能退更多税取决于 专项附加扣除,这个合计值越多,退的也会更多,下面咱们来详细说说这个专项附加扣除,怎么能够让这个合计值...
继续阅读 »

前言


最近又到了一年一度的退税了,最近发了一个沸点,发现大家都是和我之前一样不知道如何能退更多税,我也是无意中和小伙伴聊的时候才发现这个,如何能退更多税取决于 专项附加扣除,这个合计值越多,退的也会更多,下面咱们来详细说说这个专项附加扣除,怎么能够让这个合计值变多。


image.png


专项附加扣除填报


首先打开个人所得税APP,点击下面的tabbar办税,然后点击专项附加扣除填报,就会出现如下图,通过填报上面的这些去进行增加专项附加扣除合计值。


0b957f5ecc7afa419053f890511d9aa.jpg


租房租金(和房贷只能二选一)



  • 要求:工作租房子了 (不需要合同也能填哦)

  • 扣除标准:省会或者直辖市 1500 / 月,二线城市 1100 /月,小城市 800 /月

  • 年总计:省会或者直辖市 18000,二线城市 13200,小城市 9600

  • 划重点: 多段租房经历可以多填几个租房,一定要填满!!!,一定要填,没租房子也可以填,随便填一个地址也行,这个不需要租房合同,也无法追查到的,但是和贷款只能二选一,具体选哪个请继续看。


image.png


住房贷款利息(和租房只能二选一)



  • 要求:首套房贷款

  • 扣除标准:1000/月

  • 年总计:12000

  • 划重点: 和租房二选一哦,具体选哪个看你所在的城市,哪个钱多选哪个~


image.png


子女教育



  • 要求:子女处于全日制学历教育阶段(幼儿园+小学+中学+大学),年龄>3岁。

  • 扣除标准:每人每月 1000 元 (两个孩子是一个月2000哦)

  • 年总计:12000(一个孩子的情况下)


image.png


继续教育



  • 要求:考证或者学历提升

  • 扣除标准:考证拿到证的当年一次性扣除:3600,学历提升 400/月

  • 年总计:考证:3600,学历提升:4800

  • 划重点: 这里的证必须是收录在 国家资格目录 的证书哦~


image.png


大病医疗


如果可以,我希望这个没有人申请,身体健康比什么都重要。



  • 要求:医保报销后个人花费15k+

  • 扣除标准:8w内根据你个人花费,花多少这个就是多少。

  • 年总计:根据实际。


image.png


赡养老人



  • 要求:父母 60 岁以上

  • 扣除标准:独生子女2000/月,非独生子女:和兄弟姐妹分摊 2000

  • 年总计:独生子女:24000,非独生子女: 0 ~ 12000


image.png


3岁以下婴幼儿照护



  • 要求:孩子 3 岁以下

  • 扣除标准:每月每人 1000 (两个孩子就是2000/月哦)

  • 年总计:12000(一个孩子的情况下)


如何能退更多的税


打开个人所得税APP → 我要查询 → 申报查询 → 已完成→ 选择当年的综合年度汇算→ 点击专项附加扣除合计,查看自己当年的专项附加扣除合计,可以简单计算下自己填报的专项附加扣除是否都已经填写。



PS:画重点!!!注意检查时间,我开始就是租房没填满,只写了一段租房经历,所以差点错过好几个亿,如果是2023年的填报,一定要注意你专项附加扣除是否涵盖整个2022年的日期。



113b48e415b80b8861261f3b3d961ab.jpg


有年终奖的情况


可能有些小伙伴公司会发年终奖 (如果和我一样没发年终奖,那这里可以跳过了,发年终奖的公司给我来一打),最好选择单独计税,不过都可以试一下,综合并入和单独合计,哪个退的多用哪个。


历史申报


以前的申报如果有没补充的,现在还是可以填哦,快去看看之前的申报,说不定会有惊喜哦。


最后


如果是需要补税的情况,如果少于400是不需要补的哦,当然如果大于400,一定要快点去缴纳哦,国家都留有档案,防止影响以后征信,哈哈哈,祝大家都能退几个亿~


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

保姆级JAVA对接ChatGPT教程,实现自己的AI对话助手

1.前言 大家好,我是王老狮,近期OpenAI开放了chatGPT的最新gpt-3.5-turbo模型,据介绍该模型是和当前官网使用的相同的模型,如果你还没体验过ChatGPT,那么今天就教大家如何打破网络壁垒,打造一个属于自己的智能助手把。本文包括API K...
继续阅读 »

1.前言


大家好,我是王老狮,近期OpenAI开放了chatGPT的最新gpt-3.5-turbo模型,据介绍该模型是和当前官网使用的相同的模型,如果你还没体验过ChatGPT,那么今天就教大家如何打破网络壁垒,打造一个属于自己的智能助手把。本文包括API Key的申请以及网络代理的搭建,那么事不宜迟,我们现在开始。


2.对接流程


2.1.API-Key的获取


首先第一步要获取OpenAI接口的API Key,该Key是你用来调用接口的token,主要用于接口鉴权。获取该key首先要注册OpenAi的账号,具体可以见我的另外一篇文章,ChatGPT保姆级注册教程



  1. 打开platform.openai.com/网站,点击view API Key,


image.png



  1. 点击创建key


image.png



  1. 弹窗显示生成的key,记得把key复制,不然等会就找不到这个key了,只能重新创建。


image.png


将API Key保存好以备用


2.2.API用量的查看


这里可以查看API的使用情况,新账号注册默认有5美元的试用额度,之前都是18美元,API成本降了之后试用额度也狠狠地砍了一刀啊,哈哈。


image.png


2.3.核心代码实现


2.3.1.pom依赖


http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
com.webtap
webtap
0.0.1
jar


org.springframework.boot
spring-boot-starter-parent
2.1.2.RELEASE




org.springframework.boot
spring-boot-starter-web


org.springframework.boot
spring-boot-starter-thymeleaf


nz.net.ultraq.thymeleaf
thymeleaf-layout-dialect


org.springframework.boot
spring-boot-starter-data-jpa


org.springframework.boot
spring-boot-devtools


org.springframework.boot
spring-boot-starter-test


org.springframework.boot
spring-boot-starter-mail



mysql
mysql-connector-java


org.apache.commons
commons-lang3
3.4


commons-codec
commons-codec


org.jsoup
jsoup
1.9.2



com.alibaba
fastjson
1.2.56


net.sourceforge.nekohtml
nekohtml
1.9.22


com.github.pagehelper
pagehelper-spring-boot-starter
1.4.1


org.projectlombok
lombok


org.apache.httpcomponents
httpasyncclient
4.0.2


org.apache.httpcomponents
httpcore-nio
4.3.2



org.apache.httpcomponents
httpclient
4.3.5


commons-codec
commons-codec




commons-httpclient
commons-httpclient
3.1


commons-codec
commons-codec




org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.1


com.github.ulisesbocchio
jasypt-spring-boot-starter
2.0.0







org.springframework.boot
spring-boot-maven-plugin






2.3.2.实体类ChatMessage.java


用于存放发送的消息信息,注解使用了lombok,如果没有使用lombok可以自动生成构造方法以及get和set方法


@Data
@NoArgsConstructor
@AllArgsConstructor
public class ChatMessage {
//消息角色
String role;
//消息内容
String content;
}

2.3.3.实体类ChatCompletionRequest.java


用于发送的请求的参数实体类,参数释义如下:


model:选择使用的模型,如gpt-3.5-turbo


messages :发送的消息列表


temperature :温度,参数从0-2,越低表示越精准,越高表示越广发,回答的内容重复率越低


n :回复条数,一次对话回复的条数


stream :是否流式处理,就像ChatGPT一样的处理方式,会增量的发送信息。


max_tokens :生成的答案允许的最大token数


user :对话用户


@Data
@Builder
public class ChatCompletionRequest {

String model;

List messages;

Double temperature;

Integer n;

Boolean stream;

List stop;

Integer max_tokens;

String user;
}

2.3.4.实体类ExecuteRet .java


用于接收请求返回的信息以及执行结果



/**
* 调用返回
*/

public class ExecuteRet {

/**
* 操作是否成功
*/

private final boolean success;

/**
* 返回的内容
*/

private final String respStr;

/**
* 请求的地址
*/

private final HttpMethod method;

/**
* statusCode
*/

private final int statusCode;

public ExecuteRet(booleansuccess, StringrespStr, HttpMethodmethod, intstatusCode) {
this.success =success;
this.respStr =respStr;
this.method =method;
this.statusCode =statusCode;
}

@Override
public String toString()
{
return String.format("[success:%s,respStr:%s,statusCode:%s]", success, respStr, statusCode);
}

/**
*@returnthe isSuccess
*/

public boolean isSuccess() {
return success;
}

/**
*@returnthe !isSuccess
*/

public boolean isNotSuccess() {
return !success;
}

/**
*@returnthe respStr
*/

public String getRespStr() {
return respStr;
}

/**
*@returnthe statusCode
*/

public int getStatusCode() {
return statusCode;
}

/**
*@returnthe method
*/

public HttpMethod getMethod() {
return method;
}
}

2.3.5.实体类ChatCompletionChoice .java


用于接收ChatGPT返回的数据


@Data
public class ChatCompletionChoice {

Integer index;

ChatMessage message;

String finishReason;
}

2.3.6.接口调用核心类OpenAiApi .java


使用httpclient用于进行api接口的调用,支持post和get方法请求。


url为配置文件open.ai.url的值,表示调用api的地址:https://api.openai.com/ ,token为获取的api-key。
执行post或者get方法时增加头部信息headers.put("Authorization", "Bearer " + token); 用于通过接口鉴权。



@Slf4j
@Component
public class OpenAiApi {

@Value("${open.ai.url}")
private String url;
@Value("${open.ai.token}")
private String token;

private static final MultiThreadedHttpConnectionManagerCONNECTION_MANAGER= new MultiThreadedHttpConnectionManager();

static {
// 默认单个host最大链接数
CONNECTION_MANAGER.getParams().setDefaultMaxConnectionsPerHost(
Integer.valueOf(20));
// 最大总连接数,默认20
CONNECTION_MANAGER.getParams()
.setMaxTotalConnections(20);
// 连接超时时间
CONNECTION_MANAGER.getParams()
.setConnectionTimeout(60000);
// 读取超时时间
CONNECTION_MANAGER.getParams().setSoTimeout(60000);
}

public ExecuteRet get(Stringpath, Map headers) {
GetMethod method = new GetMethod(url +path);
if (headers== null) {
headers = new HashMap<>();
}
headers.put("Authorization", "Bearer " + token);
for (Map.Entry h : headers.entrySet()) {
method.setRequestHeader(h.getKey(), h.getValue());
}
return execute(method);
}

public ExecuteRet post(Stringpath, Stringjson, Map headers) {
try {
PostMethod method = new PostMethod(url +path);
//log.info("POST Url is {} ", url + path);
// 输出传入参数
log.info(String.format("POST JSON HttpMethod's Params = %s",json));
StringRequestEntity entity = new StringRequestEntity(json, "application/json", "UTF-8");
method.setRequestEntity(entity);
if (headers== null) {
headers = new HashMap<>();
}
headers.put("Authorization", "Bearer " + token);
for (Map.Entry h : headers.entrySet()) {
method.setRequestHeader(h.getKey(), h.getValue());
}
return execute(method);
} catch (UnsupportedEncodingExceptionex) {
log.error(ex.getMessage(),ex);
}
return new ExecuteRet(false, "", null, -1);
}

public ExecuteRet execute(HttpMethodmethod) {
HttpClient client = new HttpClient(CONNECTION_MANAGER);
int statusCode = -1;
String respStr = null;
boolean isSuccess = false;
try {
client.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET, "UTF8");
statusCode = client.executeMethod(method);
method.getRequestHeaders();

// log.info("执行结果statusCode = " + statusCode);
InputStreamReader inputStreamReader = new InputStreamReader(method.getResponseBodyAsStream(), "UTF-8");
BufferedReader reader = new BufferedReader(inputStreamReader);
StringBuilder stringBuffer = new StringBuilder(100);
String str;
while ((str = reader.readLine()) != null) {
log.debug("逐行读取String = " + str);
stringBuffer.append(str.trim());
}
respStr = stringBuffer.toString();
if (respStr != null) {
log.info(String.format("执行结果String = %s, Length = %d", respStr, respStr.length()));
}
inputStreamReader.close();
reader.close();
// 返回200,接口调用成功
isSuccess = (statusCode == HttpStatus.SC_OK);
} catch (IOExceptionex) {
} finally {
method.releaseConnection();
}
return new ExecuteRet(isSuccess, respStr,method, statusCode);
}

}

2.3.7.定义接口常量类PathConstant.class


用于维护支持的api接口列表


public class PathConstant {
public static class MODEL {
//获取模型列表
public static String MODEL_LIST = "/v1/models";
}

public static class COMPLETIONS {
public static String CREATE_COMPLETION = "/v1/completions";
//创建对话
public static String CREATE_CHAT_COMPLETION = "/v1/chat/completions";

}
}

2.3.8.接口调用调试单元测试类OpenAiApplicationTests.class


核心代码都已经准备完毕,接下来写个单元测试测试下接口调用情况。



@SpringBootTest
@RunWith(SpringRunner.class)
public class OpenAiApplicationTests {

@Autowired
private OpenAiApi openAiApi;
@Test
public void createChatCompletion2() {
Scanner in = new Scanner(System.in);
String input = in.next();
ChatMessage systemMessage = new ChatMessage('user', input);
messages.add(systemMessage);
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model("gpt-3.5-turbo-0301")
.messages(messages)
.user("testing")
.max_tokens(500)
.temperature(1.0)
.build();
ExecuteRet executeRet = openAiApi.post(PathConstant.COMPLETIONS.CREATE_CHAT_COMPLETION, JSONObject.toJSONString(chatCompletionRequest),
null);
JSONObject result = JSONObject.parseObject(executeRet.getRespStr());
List choices = result.getJSONArray("choices").toJavaList(ChatCompletionChoice.class);
System.out.println(choices.get(0).getMessage().getContent());
ChatMessage context = new ChatMessage(choices.get(0).getMessage().getRole(), choices.get(0).getMessage().getContent());
System.out.println(context.getContent());
}

}


  • 使用Scanner 用于控制台输入信息,如果单元测试时控制台不能输入,那么进入IDEA的安装目录,修改以下文件。增加最后一行增加-Deditable.java.test.console=true即可。


image.png
image.png




  • 创建ChatMessage对象,用于存放参数,role有user,system,assistant,一般接口返回的响应为assistant角色,我们一般使用user就好。




  • 定义请求参数ChatCompletionRequest,这里我们使用3.1日发布的最新模型gpt-3.5-turbo-0301。具体都有哪些模型大家可以调用v1/model接口查看支持的模型。




  • 之后调用openAiApi.post进行接口的请求,并将请求结果转为JSON对象。取其中的choices字段转为ChatCompletionChoice对象,该对象是存放api返回的具体信息。


    接口返回信息格式如下:


    {
    "id": "chatcmpl-6rNPw1hqm5xMVMsyf6PXClRHtNQAI",
    "object": "chat.completion",
    "created": 1678179420,
    "model": "gpt-3.5-turbo-0301",
    "usage": {
    "prompt_tokens": 16,
    "completion_tokens": 339,
    "total_tokens": 355
    },
    "choices": [{
    "message": {
    "role": "assistant",
    "content": "\n\nI. 介绍数字孪生的概念和背景\n A. 数字孪生的定义和意义\n B. 数字孪生的发展历程\n C. 数字孪生在现代工业的应用\n\nII. 数字孪生的构建方法\n A. 数字孪生的数据采集和处理\n B. 数字孪生的建模和仿真\n C. 数字孪生的验证和测试\n\nIII. 数字孪生的应用领域和案例分析\n A. 制造业领域中的数字孪生应用\n B. 建筑和城市领域中的数字孪生应用\n C. 医疗和健康领域中的数字孪生应用\n\nIV. 数字孪生的挑战和发展趋势\n A. 数字孪生的技术挑战\n B. 数字孪生的实践难点\n C. 数字孪生的未来发展趋势\n\nV. 结论和展望\n A. 总结数字孪生的意义和价值\n B. 展望数字孪生的未来发展趋势和研究方向"
    },
    "finish_reason": "stop",
    "index": 0
    }]
    }



  • 输出对应的信息。




2.3.9.结果演示


image.png


2.4.连续对话实现


2.4.1连续对话的功能实现


基本接口调通之后,发现一次会话之后,没有返回完,输入继续又重新发起了新的会话。那么那么我们该如何实现联系上下文呢?其实只要做一些简单地改动,将每次对话的信息都保存到一个消息列表中,这样问答就支持上下文了,代码如下:


List messages = new ArrayList<>();
@Test
public void createChatCompletion() {
Scanner in = new Scanner(System.in);
String input = in.next();
while (!"exit".equals(input)) {
ChatMessage systemMessage = new ChatMessage(ChatMessageRole.USER.value(), input);
messages.add(systemMessage);
ChatCompletionRequest chatCompletionRequest = ChatCompletionRequest.builder()
.model("gpt-3.5-turbo-0301")
.messages(messages)
.user("testing")
.max_tokens(500)
.temperature(1.0)
.build();
ExecuteRet executeRet = openAiApi.post(PathConstant.COMPLETIONS.CREATE_CHAT_COMPLETION, JSONObject.toJSONString(chatCompletionRequest),
null);
JSONObject result = JSONObject.parseObject(executeRet.getRespStr());
List choices = result.getJSONArray("choices").toJavaList(ChatCompletionChoice.class);
System.out.println(choices.get(0).getMessage().getContent());
ChatMessage context = new ChatMessage(choices.get(0).getMessage().getRole(), choices.get(0).getMessage().getContent());
messages.add(context);
in = new Scanner(System.in);
input = in.next();
}
}

因为OpenAi的/v1/chat/completions接口消息参数是个list,这个是用来保存我们的上下文的,因此我们只要将每次对话的内容用list进行保存即可。


2.4.2结果如下:


image.png


image.png


4.常见问题


4.1.OpenAi接口调用不通


因为https://api.openai.com/地址也被限制了,但是接口没有对地区做校验,因此可以自己搭建一个香港代理,也可以走科学上网。


我采用的是香港代理的模式,一劳永逸,具体代理配置流程如下:



  1. 购买一台香港的虚拟机,反正以后都会用得到,作为开发者建议搞一个。搞活动的时候新人很便宜,基本3年的才200块钱。

  2. 访问nginx.org/download/ng… 下载最新版nginx

  3. 部署nginx并修改/nginx/config/nginx.conf文件,配置接口代理路径如下


server {
listen 19999;
server_name ai;

ssl_certificate /usr/local/nginx/ssl/server.crt;
ssl_certificate_key /usr/local/nginx/ssl/server.key;

ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;

ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

#charset koi8-r;


location /v1/ {
proxy_pass ;
}
}


  1. 启动nginx

  2. 将接口访问地址改为nginx的机器出口IP+端口即可


如果代理配置大家还不了解,可以留下评论我单独出一期教程。


4.2.接口返回401


检查请求方法是否增加token字段以及key是否正确


5.总结


至此JAVA对OpenAI对接就已经完成了,并且也支持连续对话,大家可以在此基础上不断地完善和桥接到web服务,定制自己的ChatGPT助手了。我自己也搭建了个平台,不断地在完善中,具体可见下图,后续会开源出来,想要体验的可以私信我获取地址和账号哈


image.png


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

Jetpack:Android新一代导航管理Navigation

前言 不知道小伙伴们是否注意到,用AS创建一个默认的新项目后,MainActivity已经有了很大的不同,最大的区别就是新增加了两个Fragment,同时我们注意到这两个Fragment之间跳转的时候并没有使用之前FragmentTransaction这种形式...
继续阅读 »

前言


不知道小伙伴们是否注意到,用AS创建一个默认的新项目后,MainActivity已经有了很大的不同,最大的区别就是新增加了两个Fragment,同时我们注意到这两个Fragment之间跳转的时候并没有使用之前FragmentTransaction这种形式,而是使用了NavController和NavHostFragment,这就是新一代导航管理————Navigation。


项目中依赖Navigation:


implementation 'androidx.navigation:navigation-fragment-ktx:2.3.5'
implementation 'androidx.navigation:navigation-ui-ktx:2.3.5'

创建导航视图


新建一个Android Resource File,类型选择Navigation即可,输入名称后我们就创建了一个导航视图。


在导航试图中,我们可以通过添加activity/fragment等标签手动添加页面,也支持在Design页面中通过界面添加,如下:


B88C2B4A-4900-41DF-9BDC-7972F73190D2.png


注意:这样添加后手动修改一下label。如果我们将Navigation与ToolBar连接,会在标题栏这个label。


示例中添加了两个页面,添加后代码如下:


<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<fragment
android:id="@+id/FirstFragment"
android:name="com.xxx.xxx.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first">
</fragment>
<fragment
android:id="@+id/SecondFragment"
android:name="com.xxx.xxx.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second">
</fragment>
</navigation>

除了添加Fragment和Activity,Google还提供了一个占位符placeholder,添加加完代码如下:


<fragment android:id="@+id/placeholder" />

用于暂时占位以便后面可以替换为Fragment和Activity


添加完页面后,我们还需要添加页面之间的导航,可以手动添加action标签,当然也可以通过拖拽来实现,如下:


ABE13B79-D136-4450-A454-B4C905733284.png


这样我们就添加了一个从FirstFragment导航到SecondFragment的动作,我们再添加一个逆向的动作,最终的代码如下:


<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">

<fragment
android:id="@+id/FirstFragment"
android:name="com.xxx.xxx.FirstFragment"
android:label="@string/first_fragment_label"
tools:layout="@layout/fragment_first">

<action
android:id="@+id/action_FirstFragment_to_SecondFragment"
app:destination="@id/SecondFragment" />
</fragment>
<fragment
android:id="@+id/SecondFragment"
android:name="com.xxx.xxx.SecondFragment"
android:label="@string/second_fragment_label"
tools:layout="@layout/fragment_second">

<action
android:id="@+id/action_SecondFragment_to_FirstFragment"
app:destination="@id/FirstFragment" />
</fragment>
</navigation>

注意占位符placeholder同样支持添加导航。


这样就实现了两个页面间的导航,最后还需要为这个navigation设置id和默认页面startDestination,如下:


<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/FirstFragment">

这样导航视图就创建完成了。可以看到Google力图通过可视化工具来简化开发工作,这对我们开发者来说非常有用,可以省去大量编写同质化代码的时间。


添加NavHost


下一步我们需要向Activity中添加导航宿主,导航宿主是一个空页面,必须实现NavHost接口,我们使用Navigation提供的默认NavHost————NavHostFragment即可。如下:


<fragment
android:id="@+id/nav_host_fragment_content_main"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:navGraph="@navigation/nav_graph" />

在Activity的视图中添加一个fragment标签,android:name设置为实现类,即NavHostFragment;app:navGraph设置为刚才新建的导航视图。


注意app:defaultNavHost="true",设置为true后表示将这个NavHostFragment设置为默认导航宿主,这样就会拦截系统的返回按钮事件。同一布局中如果有多个导航宿主(比如双窗口)则必须制定一个为默认的导航宿主。


这时候我们运行应用,就可以发现Activity中已经可以展示FirstFragment了。


导航


我们还需要为两个fragment添加按钮,是其点击跳转到另外一个页面,代码如下:


binding.buttonFirst.setOnClickListener {
findNavController().navigate(R.id.action_FirstFragment_to_SecondFragment)
}

示例中是FirstFragment中的一个按钮,点击时执行了id为action_FirstFragment_to_SecondFragment的动作,这个是我们之前在导航视图中配置好的,会导航到SecondFragment。


注意首先通过findNavController()来获取一个NavController对象,然后调用它的navigate函数即可,当然这个函数有多种重载,比如可以传递参数,如下:


public void navigate(@IdRes int resId, @Nullable Bundle args) {

这里不一一列举了,大家自行查看源码即可。


可以看到使用Navigation代码精简了很多,只需要一行代码执行一个函数即可。


findNavController


我们重点来看看findNavController(),它是一个扩展函数,如下:


fun Fragment.findNavController(): NavController =
NavHostFragment.findNavController(this)

实际上是NavHostFragment的一个静态函数findNavController:


@NonNull
public static NavController findNavController(@NonNull Fragment fragment) {
...
View view = fragment.getView();
if (view != null) {
return Navigation.findNavController(view);
}

// For DialogFragments, look at the dialog's decor view
Dialog dialog = fragment instanceof DialogFragment
? ((DialogFragment) fragment).getDialog()
: null;
if (dialog != null && dialog.getWindow() != null) {
return Navigation.findNavController(dialog.getWindow().getDecorView());
}

throw new IllegalStateException("Fragment " + fragment
+ " does not have a NavController set");
}

通过源码可以看到最终是执行了Navigation的findNavController函数,它的代码如下:


@NonNull
public static NavController findNavController(@NonNull View view) {
NavController navController = findViewNavController(view);
...
return navController;
}

这里是通过findViewNavController函数来获取NavController的,它的代码如下:


@Nullable
private static NavController findViewNavController(@NonNull View view) {
while (view != null) {
NavController controller = getViewNavController(view);
if (controller != null) {
return controller;
}
ViewParent parent = view.getParent();
view = parent instanceof View ? (View) parent : null;
}
return null;
}

这里可以看到通过view来获取NavController,如果没有则向上层查找(父view)直到找到或到根结点。getViewNavController代码如下:


@Nullable
private static NavController getViewNavController(@NonNull View view) {
Object tag = view.getTag(R.id.nav_controller_view_tag);
NavController controller = null;
if (tag instanceof WeakReference) {
controller = ((WeakReference<NavController>) tag).get();
} else if (tag instanceof NavController) {
controller = (NavController) tag;
}
return controller;
}

看到这里获取view中key为R.id.nav_controller_view_tag的tag,这个tag就是NavController,那么这个tag又从哪来的?


其实就是上面我们提到导航宿主————NavHostFragment,在他的onViewCreated中可以看到如下代码:


@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (!(view instanceof ViewGroup)) {
throw new IllegalStateException("created host view " + view + " is not a ViewGroup");
}
Navigation.setViewNavController(view, mNavController);
// When added programmatically, we need to set the NavController on the parent - i.e.,
// the View that has the ID matching this NavHostFragment.
if (view.getParent() != null) {
mViewParent = (View) view.getParent();
if (mViewParent.getId() == getId()) {
Navigation.setViewNavController(mViewParent, mNavController);
}
}
}

这里的mNavController是在NavHostFragment的onCreate中创建出来的,是一个NavHostController对象,它继承NavController,所以就是NavController。


可以看到onViewCreated中调用了Navigation的setViewNavController函数,它的代码如下:


public static void setViewNavController(@NonNull View view,
@Nullable NavController controller) {
view.setTag(R.id.nav_controller_view_tag, controller);
}

这样就将NavController加入tag中了,通过findNavController()就可以得到这个NavController来执行导航了。


注意在onViewCreated中不仅为Fragment的View添加了tag,同时还为其父View也添加了,这样做的目的是在Activity中也可以获取到NavController,这点下面就会遇到。


ToolBar


Google提供了Navigation与ToolBar连接的功能,代码如下:


val navController = findNavController(R.id.nav_host_fragment_content_main)
appBarConfiguration = AppBarConfiguration(navController.graph)
setupActionBarWithNavController(navController, appBarConfiguration)

上面我们提到,如果Navigation与ToolBar连接,标题栏会自动显示在导航视图中设定好的label。


注意这里的findNavController是Activity的扩展函数,它最终一样会调用Navigation的对应函数,所以与Fragment的流程是一样的。而上面我们提到了,在NavHostFragment中给上层View也设置了tag,所以在这里才能获取到NavController。


除了这个,我们还可以发现当在切换页面的时候,标题栏的返回按钮也会自动显示和隐藏。当导航到第二个页面SecondFragment,返回按钮显示;当回退到首页时,返回按钮隐藏。


但是此时返回按钮点击无效,因为我们还需要重写一个函数:


override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment_content_main)
return navController.navigateUp(appBarConfiguration)
|| super.onSupportNavigateUp()
}

这样当点击标题栏的返回按钮时,会执行NavController的navigateUp函数,就会退回到上一页面。


总结


可以看出通过Google推出的这个Navigation,可以让开发者更加优雅管理导航,同时也简化了这部分的开发工作,可视化功能可以让开发者更直观的进行管理。除此之外,Google还提供了Safe Args Gradle插件,该插件可以生成简单的对象和构建器类,这些类支持在目的地之间进行类型安全的导航和参数传递。关于这个大家可以参考官方文档developer.android.google.cn/guide/navig… 即可。


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

Android将so库封装到jar包中并加载其中的so库

说明 因为一些原因,我们提供给客户的sdk,只能是jar包形式的,一些情况下,sdk里面有native库的时候,就不太方便操作了,此篇文章主要解决如何把so库放入jar包里面,如何打包成jar,以及如何加载。 1.如何把so库放入jar包 so库放入jar参考...
继续阅读 »

说明


因为一些原因,我们提供给客户的sdk,只能是jar包形式的,一些情况下,sdk里面有native库的时候,就不太方便操作了,此篇文章主要解决如何把so库放入jar包里面,如何打包成jar,以及如何加载。


1.如何把so库放入jar包


so库放入jar参考此文章ANDROID将SO库封装到JAR包中并加载其中的SO库
放置路径
将so库改成.jet后缀,放置和加载so库的SoLoader类同一个目录下面。



2.如何使用groovy打包jar


打包jar
先把需要打包的class放置到同一个文件夹下面,然后打包即可,利用groovy的copy task完成这项工作非常简单。


3.如何加载jar包里面的so


3.1.首先判断当前jar里面是否存在so

InputStream inputStream = SoLoader.class.getResourceAsStream("/com/dianping/logan/arm64-v8a/liblogan.jet");

如果inputStream不为空就表示存在。


3.2.拷贝

判断是否已经把so库拷贝到手机里面了,如果没有拷贝过就进行拷贝,这个代码逻辑很简单。


public class SoLoader {
private static final String TAG = "SoLoader";

/**
* so库释放位置
*/
public static String getPath() {
String path = GlobalCtx.getApp().getFilesDir().getAbsolutePath();
//String path = GlobalCtx.getApp().getExternalFilesDir(null).getAbsolutePath();
return path;
}

public static String get64SoFilePath() {
String path = SoLoader.getPath();
String v8a = path + File.separator + "jniLibs" + File.separator +
"arm64-v8a" + File.separator + "liblogan.so";
return v8a;
}

public static String get32SoFilePath() {
String path = SoLoader.getPath();
String v7a = path + File.separator + "jniLibs" + File.separator +
"armeabi-v7a" + File.separator + "liblogan.so";
return v7a;
}

/**
* 支持两种模式,如果InputStream inputStream = SoLoader.class.getResourceAsStream("/com/dianping/logan/arm64-v8a/liblogan.jet");
* 返回了空,表示可能此库是aar接入的,普通加载so库就行,不为空,需要拷贝so库,动态加载
*/
public static boolean jarMode() {
boolean jarMode = false;
InputStream inputStream = SoLoader.class.getResourceAsStream("/com/dianping/logan/arm64-v8a/liblogan.jet");
if (inputStream != null) {
jarMode = true;
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return jarMode;
}

/**
* 是否已经拷贝过so了
*/
public static boolean alreadyCopySo() {
String v8a = SoLoader.get64SoFilePath();
File file = new File(v8a);
if (file.exists()) {
String v7a = SoLoader.get32SoFilePath();
file = new File(v7a);
return file.exists();
}
return false;
}

/**
* 拷贝logan的so库
*/
public static boolean copyLoganJni() {
boolean load;
File dir = new File(getPath(), "jniLibs");
if (!dir.exists()) {
load = dir.mkdirs();
if (!load) {
return false;
}
}
File subdir = new File(dir, "arm64-v8a");
if (!subdir.exists()) {
load = subdir.mkdirs();
if (!load) {
return false;
}
}
File dest = new File(subdir, "liblogan.so");
//load = copySo("/lib/arm64-v8a/liblogan.so", dest);
load = copySo("/com/dianping/logan/arm64-v8a/liblogan.jet", dest);
if (load) {
subdir = new File(dir, "armeabi-v7a");
if (!subdir.exists()) {
load = subdir.mkdirs();
if (!load) {
return false;
}
}
dest = new File(subdir, "liblogan.so");
//load = copySo("/lib/armeabi-v7a/liblogan.so", dest);
load = copySo("/com/dianping/logan/armeabi-v7a/liblogan.jet", dest);
}
return load;
}

public static boolean copySo(String name, File dest) {
InputStream inputStream = SoLoader.class.getResourceAsStream(name);
if (inputStream == null) {
Log.e(TAG, "inputStream == null");
return false;
}
boolean result = false;
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(dest);
int i;
byte[] buf = new byte[1024 * 4];
while ((i = inputStream.read(buf)) != -1) {
outputStream.write(buf, 0, i);
}
result = true;
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return result;
}

}

3.3.加载

首先判断当前应用是32位还是64位Process.is64Bit();。然后加载对应的32或者64位的so。


static {
try {
if (SoLoader.jarMode()) {
if (SoLoader.alreadyCopySo()) {
sIsCloganOk = loadLocalSo();
} else {
boolean copyLoganJni = SoLoader.copyLoganJni();
if (copyLoganJni) {
sIsCloganOk = loadLocalSo();
}
}
} else {
System.loadLibrary(LIBRARY_NAME);
sIsCloganOk = true;
}
} catch (Throwable e) {
e.printStackTrace();
sIsCloganOk = false;
}
}

static boolean loadLocalSo() {
boolean bit = Process.is64Bit();
if (bit) {
String v8a = SoLoader.get64SoFilePath();
try {
System.load(v8a);
return true;
} catch (Throwable e) {
e.printStackTrace();
return false;
}
} else {
String v7a = SoLoader.get32SoFilePath();
try {
System.load(v7a);
return true;
} catch (Throwable e) {
e.printStackTrace();
return false;
}
}
}

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

[崩溃] Android应用自动重启

背景 在App开发过程中,我们经常需要自动重启的功能。比如: 登录或登出的时候,为了清除缓存的一些变量,比较简单的方法就是重新启动app。 crash的时候,可以捕获到异常,直接自动重启应用。 在一些debug的场景中,比如设置了一些测试的标记位,需要重启才...
继续阅读 »

背景


在App开发过程中,我们经常需要自动重启的功能。比如:



  • 登录或登出的时候,为了清除缓存的一些变量,比较简单的方法就是重新启动app。

  • crash的时候,可以捕获到异常,直接自动重启应用。

  • 在一些debug的场景中,比如设置了一些测试的标记位,需要重启才能生效,此时可以用自动重启,方便测试。


那我们如何实现自动重启的功能呢?我们都知道如何杀掉进程,但是当我们的进程被杀掉之后,如何唤醒呢?


这篇文章就来和大家介绍一下,实现应用自动重启的几种方法。


方法1 AlarmManager


    private void setAlarmManager(){
Intent intent = new Intent();
intent.setClass(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis()+100, pendingIntent);
Process.killProcess(Process.myPid());
System.exit(0);
}

使用AlarmManager实现自动重启的核心思想:创建一个100ms之后的Alarm任务,等Alarm任务到执行时间了,会自动唤醒App。


缺点:



  • 在App被杀和拉起之间,会显示系统Launcher桌面,体验不好。

  • 在高版本不适用


方法2 直接启动Activity


private void restartApp(){
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Process.killProcess(Process.myPid());
System.exit(0);
}

缺点:



  • MainActivity必须是Standard模式


方法3 ProcessPhoenix


JakeWharton大神开源了一个叫ProcessPhoenix的库,这个库可以实现无缝重启app。


实现原理其实很简单,我们先讲怎么使用,然后再来分析源码。


使用方法


首先引入ProcessPhoenix库,这个库不需要初始化,可以直接使用。


implementation 'com.jakewharton:process-phoenix:2.1.2'

使用1:如果想重启app后进入首页:


ProcessPhoenix.triggerRebirth(context);

使用2:如果想重启app后进入特定的页面,则需要构造具体页面的intent,当做参数传入:


Intent nextIntent = //...
ProcessPhoenix.triggerRebirth(context, nextIntent);

有一点需要特别注意。



  • 我们通常会在ApplicationonCreate方法中做一系列初始化的操作。

  • 如果使用Phoenix库,需要在onCreate方法中判断,如果当前进程是Phoenix进程,则直接return,跳过初始化的操作。


if (ProcessPhoenix.isPhoenixProcess(this)) {
return;
}

源码


ProcessPhoenix的原理:



  • 当调用triggerRebirth方法的时候,会启动一个透明的Activity,这个Activity运行在:phoenix进程

  • Activity启动后,杀掉主进程,然后用:phoenix进程拉起主进程的Activity

  • 关闭当前Activity,杀掉:phoenix进程


先来看看ManifestActivity的注册代码:


 <activity
android:name=".ProcessPhoenix"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:process=":phoenix"
android:exported="false"
/>

可以看到这个Activity确实是在:phoenix进程启动的,且是Translucent透明的。


整个ProcessPhoenix的代码只有不到120行,非常简单。我们来看下triggerRebirth做了什么。


  public static void triggerRebirth(Context context) {
triggerRebirth(context, getRestartIntent(context));
}

不带intenttriggerRebirth,最后也会调用到带intenttriggerRebirth方法。


getRestartIntent会获取主进程的Launch Activity


  private static Intent getRestartIntent(Context context) {
String packageName = context.getPackageName();
Intent defaultIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
if (defaultIntent != null) {
return defaultIntent;
}
}

所以要调用不带intenttriggerRebirth,必须在当前Appmanifest里,指定Launch Activity,否则会抛出异常。


接着来看看真正的triggerRebirth方法:


  public static void triggerRebirth(Context context, Intent... nextIntents) {
if (nextIntents.length < 1) {
throw new IllegalArgumentException("intents cannot be empty");
}
// 第一个activity添加new_task标记,重新开启一个新的stack
nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);

Intent intent = new Intent(context, ProcessPhoenix.class);
// 这里是为了防止传入的context非Activity
intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context.
// 将待启动的intent作为参数,intent是parcelable的
intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents)));
// 将主进程的pid作为参数
intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
// 启动ProcessPhoenix Activity
context.startActivity(intent);
}

triggerRebirth方法,主要的功能是启动ProcessPhoenix Activity,相当于启动了:phoenix进程。同时,会将nextIntents和主进程的pid作为参数,传给新启动的ProcessPhoenix Activity


下面我们再来看看,ProcessPhoenix ActivityonCreate方法,看看新进程启动后做了什么。


  @Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 首先杀死主进程
Process.killProcess(getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1)); // Kill original main process

ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS);
// 再启动主进程的intents
startActivities(intents.toArray(new Intent[intents.size()]));
// 关闭当前Activity,杀掉当前进程
finish();
Runtime.getRuntime().exit(0); // Kill kill kill!
}

:phoenix进程主要做了以下事情:



  • 杀死主进程

  • 用传入的Intent启动主进程的Activity(也可以是Service)

  • 关闭phoenix Activity,杀掉phoenix进程


总结


如果App有自动重启的需求,比较推荐使用ProcessPhoenix的方法。


原理其实非常简单:



  • 启动一个新的进程

  • 杀掉主进程

  • 用新的进程,重新拉起主进程

  • 杀掉新的进程


我们可以直接在工程里引入ProcessPhoenix开源库,也可以自己用代码实现这样的机制,总之都比较简单。


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

Android:我是如何优化APP体积的

前言 在日常开发中,随着APP功能迭代发现打出的安装包体积越来越大,这里说的大是猛增的那种大,而并非一点一点增大。从最开始的几兆到后面的几十兆,虽然市面上的很多APP甚至达到上百兆,但毕竟别人功能强大,用到的一些底层库就特别占面积,流量也多所以也可理解。但自...
继续阅读 »

前言



在日常开发中,随着APP功能迭代发现打出的安装包体积越来越大,这里说的大是猛增的那种大,而并非一点一点增大。从最开始的几兆到后面的几十兆,虽然市面上的很多APP甚至达到上百兆,但毕竟别人功能强大,用到的一些底层库就特别占面积,流量也多所以也可理解。但自研的一些APP可经不住这些考验,所以能压缩就压缩,能优化就尽量优化,以得到用户最好的体验,下面就来说说我在项目中是如何优化APP体积的。



1. 本地资源优化


这里主要是压缩一些图片和视频。项目中本地资源用到最多的应该就是图片,几乎每个页面都离不开图标,甚至一些页面采用大图片的形式。你可知道,正常不经压缩的图片大的可以上大几十兆,小则也是一兆起步。这里做了个实验,同一个文件分别采用svg、png、使用tiny压缩后的png、webp四种类型图片进行展示(顺序是从左到右,从上到下):


image.png


可以看到,加载出来的效果几乎没有什么区别,但体积却有很大的差别(其中webp是采取的默认75%转换):


image.png


所以,别再使用png格式图片,太浪费资源了,就算经过压缩还是不及svg和webp,这里的webp其实还可以加大转换力度,但个人还是比较喜欢svg。


至于音视频文件也是可以通过其他工具进行压缩再放入本地,如非必要,尽量还是使用网络资源。


2. lib优化


一些三方库会使用底层so文件,一般在配置的时候我们尽量选择一种cpu类型,这里选择armeabi-v7a,其实几乎都兼容


ndk {
//设置支持的SO库架构 armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips、mips64
abiFilters 'armeabi-v7a'
}

可以看看APK体积分析,每种cpu占用体积都比较大,少配置一种就能省下不少空间。
image.png


3. 代码混淆、无用资源的删除


在bulid.gradle中配置minifyEnabled true开启代码混淆,还需要配置混淆规则,否则无法找到目标类。shrinkResources true则是打包时不会将无用资源打入包内,这里有个小坑。之前使用腾讯地图时,某些第三方的静态资源会因为这个操作不被打入包内,导致无法找到资源,所以根据具体情况使用。


 release {
buildConfigField "boolean", "LOG_DEBUG", "false"
minifyEnabled true
// shrinkResources true 慎用,可能会导致第三方资源文件找不到
zipAlignEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}

4. 代码复用,剔除无用代码


项目中由于多人协同开发会出现各写各的情况,需要抽出一些公共库之类的工具,方便代码复用。一些注释掉的代码该删除就删除。其实这一部分优化的体积相当少,但也得做,也是对代码质量的一种提升。


总结


其实只要做到了以上四步,APP体积优化已经得到了很大程度的提升了,其他再怎么优化效果也不是很明显了,最主要的就是本地资源和第三方so包体积占用较多。图片的使用我们尽量做到:小图标用svg,全屏类的大图可以考虑webp,最好不要使用png。ndk配置最好只配置一款cpu,几乎都可兼容,万不得已再加一个。


以上便是全部内容,希望对大家有所帮助。


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

【自定义 View】Android 实现物理碰撞效果的徽章墙

前言 在还没有疫情的年代,外出经常会选择高铁,等高铁的时候我就喜欢打开 掌上高铁 的成就,签到领个徽章,顺便玩一下那个类似碰撞小球的徽章墙,当时我就在想这东西怎么实现的,但是吧,实在太懒了/doge,这几年都没尝试去自己实现过。最近有时间倒逼自己做了一些学习和...
继续阅读 »

前言


在还没有疫情的年代,外出经常会选择高铁,等高铁的时候我就喜欢打开 掌上高铁 的成就,签到领个徽章,顺便玩一下那个类似碰撞小球的徽章墙,当时我就在想这东西怎么实现的,但是吧,实在太懒了/doge,这几年都没尝试去自己实现过。最近有时间倒逼自己做了一些学习和尝试,就分享一下这种功能的实现。



不过,当我为写这篇文章做准备的时候,据不完全考古发现,似乎摩拜的 app 更早就实现了这个需求,但有没有更早的我就不知道了/doge



x762f-m3kvm.gif


其实呢,我想起来做这个尝试是我在一个 Android 自定义 View 合集的库 里看到了一个叫 PhysicsLayout 的库,当时我就虎躯一震,我心心念念的徽章墙不就是这个效果嘛,于是也就有了这篇文章。这个 PhysicsLayout 其实是借助 JBox2D 来实现的,但不妨先借助 PhysicsLayout 实现徽章墙,然后再来探索 PhysicsLayout 的实现方式。


实现




  1. 添加依赖,sync


    implementation("com.jawnnypoo:physicslayout:3.0.1")



  2. 在布局文件中添加PhysicsLinearLayout,并添加一个 子 Viewrun 起来


    这里我给ImageView设置 3 个Physic的属性



    • layout_shape设置模拟物理形状为圆形

    • layout_circleRadius设置圆形的半径为25dp

    • layout_restitution设置物体弹性的系数,范围为 [0,1],0 表示完全不反弹,1 表示完全反弹




  3. 看上去好像效果还行,我们再多加几个试试 子 View 试试





  4. 有下坠效果了,但是还不能随手机转动自由转动,在我阅读了 PhysicsLayout 之后发现其并未提供随陀螺仪自由晃动的方法,那我们自己加一个,在 MainActivityPhysicsLayout 添加一个扩展方法


        /**
    * 随手机的转动,施加相应的矢量
    * @param x x 轴方向的分量
    * @param y y 轴方向的分量
    */

    fun PhysicsLinearLayout.onSensorChanged(x: Float, y: Float) {
    for (i in 0..this.childCount) {

    Log.d(this.javaClass.simpleName, "input vec2 value : x $x, y $y")

    val impulse = Vec2(x, y)
    val view: View? = this.getChildAt(i)
    val body = view?.getTag(com.jawnnypoo.physicslayout.R.id.physics_layout_body_tag) as? Body
    body?.applyLinearImpulse(impulse, body.position)
    }
    }



  5. MainActivityonCreate() 中获取陀螺仪数据,并将陀螺仪数据设置给我们为 PhysicsLayout 扩展的方法,run


        val physicsLayout = findViewById(R.id.physics_layout)

    val sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
    val gyroSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE)

    gyroSensor?.also { sensor ->
    sensorManager.registerListener(object : SensorEventListener {
    override fun onSensorChanged(event: SensorEvent?) {
    event?.also {
    if (event.sensor.type == Sensor.TYPE_GYROSCOPE) {

    Log.d(this@MainActivity.javaClass.simpleName, "sensor value : x ${event.values[0]}, y ${event.values[1]}")
    physicsLayout.onSensorChanged(-event.values[0], event.values[1])
    }
    }
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
    }
    }, sensor, SensorManager.SENSOR_DELAY_UI)
    }

    动了,但是好像和预期的效果不太符合呀,而且也不符合用户直觉。




  6. 那不知道这时候大家是怎么处理问题的,我是先去看看这个库的 issue,搜索一下和 sensor 相关的提问,第二个就是关于如何让子 view 根据加速度计的数值进行移动,作者给出的答复是使用重力传感器,并在AboutActivity中给出了示例代码。


    那我们这里就换用重力传感器来试一试。


        val gyroSensor: Sensor? = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY)

    gyroSensor?.also { sensor ->
    sensorManager.registerListener(object : SensorEventListener {
    override fun onSensorChanged(event: SensorEvent?) {
    event?.also {
    if (event.sensor.type == Sensor.TYPE_GRAVITY) {
    Log.d(this@MainActivity.javaClass.simpleName, "sensor value : x ${event.values[0]}, y ${event.values[1]}")
    physicsLayout.physics.setGravity(-event.values[0], event.values[1])
    }
    }
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
    }
    }, sensor, SensorManager.SENSOR_DELAY_UI)
    }

    这下碰撞效果就正常了,但是好像会卡住不动啊!





  7. 不急,回到 issue,看第一个提问:物理效果会在子 view 停止移动后结束 和这里遇到的问题一样,看一下互动,有人提出是由于物理模拟引擎在物体移动停止后将物体休眠了。给出的修改方式是设置 bodyDef.allowSleep = false


    这个属性,是由 子 View 持有,所有现在需要获取 子 View 的实例并设置对应的属性,这里我就演示修改其中一个的方式,其他类似。


        findViewById(R.id.iv_physics_a).apply {
    if (layoutParams is PhysicsLayoutParams) {
    (layoutParams as PhysicsLayoutParams).config.bodyDef.allowSleep = false
    }
    }

    ···



  8. 到这里,这个需求基本就算实现了。




原理


看完了徽章墙的实现方式,我们再来看看 PhysicsLayout 是如何实现这种物理模拟效果的。




  1. 初看一下代码结构,可以说非常简单


    image.png




  2. 那我们先看一下我上面使用到的 PhysicsLinearLayout


    class PhysicsLinearLayout : LinearLayout {

    lateinit var physics: Physics

    constructor(context: Context) : super(context) {
    init(null)
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
    init(attrs)
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
    init(attrs)
    }

    @TargetApi(21)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) {
    init(attrs)
    }

    private fun init(attrs: AttributeSet?) {
    setWillNotDraw(false)
    physics = Physics(this, attrs)
    }

    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    physics.onSizeChanged(w, h)
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    super.onLayout(changed, l, t, r, b)
    physics.onLayout(changed)
    }

    override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)
    physics.onDraw(canvas)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
    return physics.onInterceptTouchEvent(ev)
    }

    @SuppressLint("ClickableViewAccessibility")
    override fun onTouchEvent(event: MotionEvent): Boolean {
    return physics.onTouchEvent(event)
    }

    override fun generateLayoutParams(attrs: AttributeSet): LayoutParams {
    return LayoutParams(context, attrs)
    }

    class LayoutParams(c: Context, attrs: AttributeSet?) : LinearLayout.LayoutParams(c, attrs), PhysicsLayoutParams {
    override var config: PhysicsConfig = PhysicsLayoutParamsProcessor.process(c, attrs)
    }
    }

    主要有下面几个重点



    1. 首先是在构造函数创建了 Physics 实例

    2. 然后把 View 的绘制,位置,变化,点击事件的处理统统交给了 physics 去处理

    3. 最后由 PhysicsLayoutParamsProcessor 创建 PhysicsConfig 的实例




  3. 那我们先来看一下简单一点的 PhysicsLayoutParamsProcessor


    object PhysicsLayoutParamsProcessor {

    /**
    * 处理子 view 的属性
    *
    * @param c context
    * @param attrs attributes
    * @return the PhysicsConfig
    */

    fun process(c: Context, attrs: AttributeSet?): PhysicsConfig {
    val config = PhysicsConfig()
    val array = c.obtainStyledAttributes(attrs, R.styleable.Physics_Layout)
    processCustom(array, config)
    processBodyDef(array, config)
    processFixtureDef(array, config)
    array.recycle()
    return config
    }

    /**
    * 处理子 view 的形状属性
    */

    private fun processCustom(array: TypedArray, config: PhysicsConfig) {
    if (array.hasValue(R.styleable.Physics_Layout_layout_shape)) {
    val shape = when (array.getInt(R.styleable.Physics_Layout_layout_shape, 0)) {
    1 -> Shape.CIRCLE
    else -> Shape.RECTANGLE
    }
    config.shape = shape
    }
    if (array.hasValue(R.styleable.Physics_Layout_layout_circleRadius)) {
    val radius = array.getDimensionPixelSize(R.styleable.Physics_Layout_layout_circleRadius, -1)
    config.radius = radius.toFloat()
    }
    }

    /**
    * 处理子 view 的刚体属性
    * 1. 刚体类型
    * 2. 刚体是否可以旋转
    */

    private fun processBodyDef(array: TypedArray, config: PhysicsConfig) {
    if (array.hasValue(R.styleable.Physics_Layout_layout_bodyType)) {
    val type = array.getInt(R.styleable.Physics_Layout_layout_bodyType, BodyType.DYNAMIC.ordinal)
    config.bodyDef.type = BodyType.values()[type]
    }
    if (array.hasValue(R.styleable.Physics_Layout_layout_fixedRotation)) {
    val fixedRotation = array.getBoolean(R.styleable.Physics_Layout_layout_fixedRotation, false)
    config.bodyDef.fixedRotation = fixedRotation
    }
    }

    /**
    * 处理子 view 的刚体描述
    * 1. 刚体的摩擦系数
    * 2. 刚体的补偿系数
    * 3. 刚体的密度
    */

    private fun processFixtureDef(array: TypedArray, config: PhysicsConfig) {
    if (array.hasValue(R.styleable.Physics_Layout_layout_friction)) {
    val friction = array.getFloat(R.styleable.Physics_Layout_layout_friction, -1f)
    config.fixtureDef.friction = friction
    }
    if (array.hasValue(R.styleable.Physics_Layout_layout_restitution)) {
    val restitution = array.getFloat(R.styleable.Physics_Layout_layout_restitution, -1f)
    config.fixtureDef.restitution = restitution
    }
    if (array.hasValue(R.styleable.Physics_Layout_layout_density)) {
    val density = array.getFloat(R.styleable.Physics_Layout_layout_density, -1f)
    config.fixtureDef.density = density
    }
    }
    }

    这个类比较简单,就是一个常规的读取设置并创建一个对应的 PhysicsConfig 的属性




  4. 现在我们来看最关键的 Physics,这个类代码相对比较长,我就不完全贴出来了,一段一段的来分析



    1. 首先定义了一些伴生对象,主要是预设了几种重力值,模拟世界的边界尺寸,渲染帧率
      companion object {
      private val TAG = Physics::class.java.simpleName
      const val NO_GRAVITY = 0.0f
      const val MOON_GRAVITY = 1.6f
      const val EARTH_GRAVITY = 9.8f
      const val JUPITER_GRAVITY = 24.8f

      // Size in DP of the bounds (world walls) of the view
      private const val BOUND_SIZE_DP = 20
      private const val FRAME_RATE = 1 / 60f

      /**
      * 在创建 view 对应的刚体时,设置配置参数
      * 当布局已经被渲染之后改变 view 的配置需要调用 ViewGroup.requestLayout,刚体才能使用新的配置创建
      */

      fun setPhysicsConfig(view: View, config: PhysicsConfig?) {
      view.setTag(R.id.physics_layout_config_tag, config)
      }
      }


    2. 然后定义了很多的成员变量,这里挑几个重要的说一说吧
      /**
      * 模拟世界每一步渲染的计算速度,默认是 8
      */

      var velocityIterations = 8

      /**
      * 模拟世界每一步渲染的迭代速度,默认是 3
      */

      var positionIterations = 3

      /**
      * 模拟世界每一米对应多少个像素,可以用来调整模拟世界的大小
      */

      var pixelsPerMeter = 0f

      /**
      * 当前控制着 view 的物理状态的模拟世界
      */

      var world: World? = null
      private set


    3. init 方法中主要是读取一些 Physics 配置,另外初始化了一个拖拽手势处理的实例
      init {
      viewDragHelper = TranslationViewDragHelper.create(viewGroup, 1.0f, viewDragHelperCallback)

      density = viewGroup.resources.displayMetrics.density
      if (attrs != null) {
      val a = viewGroup.context
      .obtainStyledAttributes(attrs, R.styleable.Physics)
      ···
      a.recycle()
      }
      }


    4. 然后提供了一些物理长度,角度的换算方法

    5. onLayout 中创建了模拟世界,根据边界设置决定是否启用边界,设置碰撞处理回调,根据子 view 创建刚体
      private fun createWorld() {
      // Null out all the bodies
      val oldBodiesArray = ArrayList()
      for (i in 0 until viewGroup.childCount) {
      val body = viewGroup.getChildAt(i).getTag(R.id.physics_layout_body_tag) as? Body
      oldBodiesArray.add(body)
      viewGroup.getChildAt(i).setTag(R.id.physics_layout_body_tag, null)
      }
      bounds.clear()
      if (debugLog) {
      Log.d(TAG, "createWorld")
      }
      world = World(Vec2(gravityX, gravityY))
      world?.setContactListener(contactListener)
      if (hasBounds) {
      enableBounds()
      }
      for (i in 0 until viewGroup.childCount) {
      val body = createBody(viewGroup.getChildAt(i), oldBodiesArray[i])
      onBodyCreatedListener?.onBodyCreated(viewGroup.getChildAt(i), body)
      }
      }


    6. onInterceptTouchEventonTouchEvent 中处理手势事件,如果没有开启滑动拖拽,时间继续传递,如果开启了,则由 viewDragHelper 来处理手势事件。
      fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
      if (!isFlingEnabled) {
      return false
      }
      val action = ev.actionMasked
      if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
      viewDragHelper.cancel()
      return false
      }
      return viewDragHelper.shouldInterceptTouchEvent(ev)
      }

      fun onTouchEvent(ev: MotionEvent): Boolean {
      if (!isFlingEnabled) {
      return false
      }
      viewDragHelper.processTouchEvent(ev)
      return true
      }


    7. onDraw 中绘制 view 的物理效果


      1. 先设置世界的物理配置


        val world = world
        if (!isPhysicsEnabled || world == null) {
        return
        }
        world.step(FRAME_RATE, velocityIterations, positionIterations)



      2. 遍历 子 view 并获取此前在创建刚体时设置的刚体对象,对于正在被拖拽的 view 将其移动到对应的位置


        translateBodyToView(body, view)
        view.rotation = radiansToDegrees(body.angle) % 360f



      3. 否则的话,设置 view 的物理位置,这里的 debugDraw 一直是 false 所以并不会走这段逻辑,且由于是私有属性,外部无法修改,似乎永远不会走这里


         view.x = metersToPixels(body.position.x) - view.width / 2f
        view.y = metersToPixels(body.position.y) - view.height / 2f
        view.rotation = radiansToDegrees(body.angle) % 360f
        if (debugDraw) {
        val config = view.getTag(R.id.physics_layout_config_tag) as PhysicsConfig
        when (config.shape) {
        Shape.RECTANGLE -> {
        canvas.drawRect(
        metersToPixels(body.position.x) - view.width / 2,
        metersToPixels(body.position.y) - view.height / 2,
        metersToPixels(body.position.x) + view.width / 2,
        metersToPixels(body.position.y) + view.height / 2,
        debugPaint
        )
        }
        Shape.CIRCLE -> {
        canvas.drawCircle(
        metersToPixels(body.position.x),
        metersToPixels(body.position.y),
        config.radius,
        debugPaint
        )
        }
        }
        }



      4. 最后提供了一个接口便于我们在需要的时候修改 JBox2D 处理 view 对应的刚体的物理状态


        onPhysicsProcessedListeners.forEach { it.onPhysicsProcessed(this, world) }




    8. 还有一个测试物理碰撞效果的随机碰撞方法
      fun giveRandomImpulse() {
      var body: Body?
      var impulse: Vec2
      val random = Random()
      for (i in 0 until viewGroup.childCount) {
      impulse = Vec2((random.nextInt(1000) - 1000).toFloat(), (random.nextInt(1000) - 1000).toFloat())
      body = viewGroup.getChildAt(i).getTag(R.id.physics_layout_body_tag) as? Body
      body?.applyLinearImpulse(impulse, body.position)
      }
      }





Bonus




  1. 在上面分析代码的时候,多次提到手势拖拽,那怎么实现这个手势的效果,目前好像对手是没反应嘛~


    其实也很简单,将 physicsisFlingEnabled 属性设置为 true 即可。


    val physicsLayout = findViewById(R.id.physics_layout).apply {
    physics.isFlingEnabled = true
    }




  2. 在浏览 PhysicsLayout issue 的时候还意外的发现已经有国人实现了 Compose 版本的
    JetpackComposePhysicsLayout


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

临近研三毕业的一些思考

秋招 2023届秋招可谓是诸神之战,算法岗、开发岗、数据分析岗等都是岗位紧缺,但却有一堆985高校毕业生来竞争。我海投了接近150家公司,最终收获了3个offer:一个上海创业公司(年薪43万元)、一个半国企(年薪25万元)以及一个真正的国企(年薪15-18万...
继续阅读 »

秋招


2023届秋招可谓是诸神之战,算法岗、开发岗、数据分析岗等都是岗位紧缺,但却有一堆985高校毕业生来竞争。我海投了接近150家公司,最终收获了3个offer:一个上海创业公司(年薪43万元)、一个半国企(年薪25万元)以及一个真正的国企(年薪15-18万元)。


在找工作方面,实力占据30%,而运气和机遇则占据了70%。即便是同一家公司,不同的面试官也有着不同的面试风格,因此会影响不同的结果。虽然运气很重要,但是实力永远都是基础。没有实力,就没有后续的好运气和机遇。


秋招的准备流程大体上包括以下几个步骤:刷题、背诵八股、准备项目、深入挖掘项目难点、准备简历、投递简历、面试复盘、简历修订以及谈薪水。每一个过程都十分重要,缺一不可。


春招


我已经签了一个半国企的offer,因此就没有再关注其他公司的信息。我基本上可以称为放弃了春招。一方面,如果我拿到了大厂的offer,我担心自己会承受不住试用期,或者会面临毕业的问题。另一方面,如果我拿到了银行的offer,我也不喜欢银行死板的工作氛围、轮岗的机制以及勾心斗角的同事关系。我参加过一次国考,但是基本上是裸考,完全没有复习。虽然在国家体制内工作很好,但是对于经济条件不好的家庭来说,去体制内不是最优的选择。


毕业季


在毕业前,我开始思考自己的第一份工作。实际上,我更多的是感到焦虑,因为放弃了年薪43万元的offer需要很大的勇气。我一直在思考如何弥补自己失去这份工作的损失。因此,我萌生了一些想法:


想法一:接私活


接外包项目对于程序员来说可能相对较为简单,因为这类项目通常技术含量较低,但是与此同时,与客户的沟通和协商则显得比较繁琐。但是,这种方式往往容易让人陷入代码的搬运中,短期收益是可观的,可以轻松获得外快薪资,但从长远来看,这种做法得不偿失。


想法二:转型产品经理


也许是看多了脉脉,认为程序员35岁危机是必然存在的,但是各行各业都存在淘汰现象。比较幼稚的想法是觉得产品经理生命周期更长,更稳定,在咨询相关学长后才知道,我才发现,原来产品经理的竞争比技术岗还要激烈,失业后重新获得一份工作的几率要比技术岗更低。


想法三:技术积淀,做自媒体


有些人会认为,做自媒体的目的就是要从中牟利,尽管这种想法在大部分人中很普遍。但实际上,成功的自媒体人通常是因为一开始就热爱分享或热爱自己所从事的行业,最终才获得了第二桶金。虽然技术积淀很重要,但如果抱着功利性的心态去做自媒体,那么注定会经历种种落差、打击以及困难。


如何破局?


思来想去,无法得知后续的解决方法,于是想到了《刻意练习》这本书,当我遇到一个无法解决问题的时候,我应该把我的问题和不成熟解决方案向教练反馈,让有经验的教练来指导。我联系了实验室优秀的师兄,师兄给出了很有建设性的意见:


意见一:技术是生存之道


进入公司,要多看技术文档,多看源码,理解原理,刚开始要多写、多思考。只有思考,才能成长起来。互联网是有35危机,但是大部分行业都有35危机,你有技能,就算被毕业,也会找到一个不错的工作。


意见二:要有全局思维


在接到一个需求时,我们不能简单地完成它,而应该深入思考这个需求为什么被提出,采用什么方式解决会更好。一个高价值的程序员不应该以编写的代码量为标准来衡量,而应该根据他在团队中不可或缺的地位来衡量。从了解业务到产品,从产品到自己负责的模块,我们应该对整个公司的架构有粗粒度和细粒度的理解。在未来,我们也将独当一面,成为负责人。因此,我们要为自己工作,而不是做一个单纯的打工仔。公司的转正述职报告是一个很好的机会,我们可以在这个机会中展示我们所学的一切,不仅限于我们负责的模块。


意见三:真诚的职场态度


互联网有嫡系的说法,那么为甚么会存在嫡系呢???存在即合理,肯定有背后的原因。假如,你有一个能力强、有主见、会沟通的的下属,你会不会青睐他?让他升职为你创造更多的业绩呢。答案是肯定的。师兄谈到了怎么样成为嫡系的方法,总结来说就是真诚的职场态度。第一,交代的任务要有责任感,能完成的不能完成的,都要带着结果和理由汇报,这是工作,不是随便搞一搞,那么怎么才能算是认真的完成呢?要知道任务背后的目的,任务背后蕴含的底层实现或者原理,这块任务交给你,你对这个任务的理解应该比谁都要透彻;第二,要主动汇报,可以两周或者一周就和上级交流自己的工作任务情况自己的难点等等,和优秀的人交流你才能进步。程序员的通病也许就是太沉迷于自己的代码世界,而忘记了和上级和其它业务部门交流,导致你做的和老板想要的根本不是一回事;第三,多和其它部门的人沟通,不要占用人家的工作时间,可以茶余饭后聊聊天,了解一下人家在做什么,沟通也是一个重要技能。


总结


师兄给的建议非常perfect,让我醍醐灌顶。未来的道路还很长,我想避免焦虑的最好办法,就是每天进步一点点,每个阶段都能朝着规划前进一步。最后,希望

作者:Javan Lu
来源:juejin.cn/post/7208359036273098811
顺利毕业,未来可期。

收起阅读 »

中年程序员写给36岁的自己

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪__胡乱写一通,篇幅较长 _,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。_ 回顾  忙忙碌碌又一年,看着自己的发量在逐渐的减少,深感焦虑,今天终于有时间可以回顾自己...
继续阅读 »

笔者是一名程序员老司机,局限于笔者文笔一般,想到哪写到哪__胡乱写一通,篇幅较长


_,希望通过文章的方式简单的回顾过去、总结现在和展望未来,顺便记录一下,方便以后总结。_


回顾 


忙忙碌碌又一年,看着自己的发量在逐渐的减少,深感焦虑,今天终于有时间可以回顾自己过去一年的得与失,去年是写给35岁的自己,今年该+1了,还是随笔的方式,想到哪写到哪。


2022年1月30日的时候 ,给自己过去的工作做一次简单的总结,主要还是写给自己,就像现在的时候可以回过头去看,也收获了许多朋友的关注,回去看一年前写的文章,以及大家的留言反馈,深有感触。


回看去年的flag,首先需要检讨一下,基本上都没有完成,但是自己也朝着这个目标在做,收获也是有的:



读书


每个月一本书,一年完成至少10本书的学习计划,学以致用,而不是读完就忘。


写文章


一周完成一篇原创文章,不限类别


早睡早起


每天不晚于11:30休息



关于读书


上半年的时候,自己也是有点焦虑和迷茫,想成长想进步,所以焦虑心情自然就会出现,所以看了一些鸡汤书籍,什么《被讨厌的勇气》、《程序员的自我修养》、《情商》等等。实话说看完之后,确实能够缓解缓解内心的焦虑情绪,但是这些书籍能给到自己的,更多是一些方式和方法,对于内心的空洞和不充实带来的焦虑是没办法缓解的。


所以还需要对症下药,我自己所感受到的空洞和不充实,很多是来自自己对技术知识技能的缺乏和退步,说白了就是作为技术人,不能把技术给弄丢了 ,同样也想不断的跟上时代的步伐。


想要快速解决这种“焦虑”,我需要快速的制定一个短期、中期、长期的目标,围绕着目标去充实自己的知识体系。这里所说的目标还是比较容易制定的,毕竟是关乎自己的成长,也就是自己接下来想要成为什么样的人,什么样的知识体系能够让自己在当前以及未来几年的工作中都有帮助。从这个方面去想,首先未来还是想从事前端,所以我给自己制定的短期目标是算法成长 、中期目标是计算机图形学方面的知识掌握、长期目标是成为一名地图领域的技术专家(ps:说到这里先立个flag,我后面想写一个小册,专门关于地图领域相关的,我也是比较期待自己能写出来什么样的小册,不为别的,就是想把自己的知识沉淀下来)。


讲讲为什么要这么去规划目标,算法算是现在任何技术面试都会涉及的,但是我不是为了面试去看,而是为了提升自己在团队内部的技术影响力,《算法图解》这本书写的简单好理解,作者的思路非常清晰 ,看完之后给团队内部的同学分享,不仅能提升自己,还能带动团队一起学习,一举多得。计算机图形学知识是目前工作中会碰到的,比如渲染、大数据可视化、自动驾驶等等都会涉及,这一部分不建议大家先去看书,没有一本书能够说明白,推荐大家去搜《闫令琪》,非常厉害的大佬,上班路上每天花半个小时-1小时足够了,一个月基本上能够学完,之后再运用到工作中,融会贯通。


单独再讲讲长远目标,我之前并不是搞地图方向的,但是近期这份工作有机会接触到了这方面的工作,让我又重新燃起了工作中的那种欲望,很久没有工作中的那种成就感,这也许是10年前才会有的那种热情,所以我比较坚信未来几年自己希望能够深入投入这个方向,不一定是地图,但一定是和这个方向相关的领域,因为知识都是想通的。


关于写文章


写文章这件事情,我非常佩服一位前同事,也或许是因为我没有做到 ,但是别人坚持每天每日的在做 ,他连续两年每天都能产出一篇原创,关于各个方面的,这是值得我学习的地方,今年争取突破自己。


关于早睡


头发卡卡掉,感觉都是因为没有按时睡觉引起的,还是在能有条件的时候,尽量早睡。


工作


今年的工作可以用“黑暗”、“光明”两个词来概括。


黑暗


2022年经历疫情最严重的一年,大部分时间都是居家办公状态,这也导致和同事们的交流变得很少,很多想要推进的工作变得没那么顺利,徒增了不少压力。


2022年也是“财源滚滚”的一年,看着同事一个个离开,也有不少同事询问工作机会,也确实给自己内心带来不小的冲击,同时危机感也很明显。


在一个地方工作一段时间之后,多少都会遇到各种各样的问题,技术上是最省心的问题,解决就好。有江湖的地方就会有各种复杂到不敢想的关系网,谁是谁的小弟,谁是谁的心腹、谁是大老板招来的等等,遇到这种问题我更多的是做好自己,但我更多还是更愿意沉浸在技术的知识中,享受解决问题带来的快感。面对频繁换老板,技术人的通病,不善于抱大腿,当然我也不想在这方便再去做过多改变或者违背内心去做一些事情,保持好内心的底线,不突破我的底线则相安无事。


光明


呵护好内心的明灯


今年工作最大的动力是来自于自身能力的成长,规划的短中长目标基本上都在按照正确的方向在行进,这也是在排除各种各样的干扰后带来的好的结果,也是抱着一种积极向上的心态在努力,工作中最让人糟心的,无非就是背锅、背指标、裁员,最坏的情况也就这样了,守好内心的方向,做自己想做的事情就对了,自己左右不了的事情不去想太多,行业不景气的时候,我基本上是以这种心态在工作,人生并不是只有工作。


人情往来


工作中


今年在和外部部门的合作当中,收获了许多的认可,也建立了许多新的人脉关系,这也是人生中比较宝贵的资源。与合作方合作共赢一直都是我做事的指导方法 ,提前思考好双方的目标和边界,剩下的就是努力合作完成目标了。相信他人,他人也会给予你同样的信任。


生活中


生活中的关系会比工作中的关系更加的牢靠,当然工作中的关系发展的好的话,也可以沉淀到生活中,而不是换个工作全没了,今年工作中积累的关系,确实是可以有这方面的转换的,这也是一种收获。


技术成长


我一直都不太赞成技术人转纯管理这个方向,管好人其实可以很简单,丑话在前,用心对待,以诚相待,能做好这三点感觉都不会有太大问题,但技术丢了就很难再捡起来了,切记切记。


今年反尝试不直接带团队,更多的是以技术顾问、专家视角,甚至是一线coding的方式在工作,看似管人但又不管人,所以在技术上成长也是非常快的,少了很多其他的琐事,能够更加投入。


渲染


第一次接触这个词的时候是在2021年,公司专门配了一个渲染团队做这个事情,用前端白话讲,就是能把各种各样的图像画到canvas上,一个好的渲染引擎可以呈现任何想要呈现的物体。


为了学习渲染是做什么的,怎么做,当时把简单的数学知识重新学习了一下,看闫令琪大佬的课,看openGL、webGPU等等相关的知识,过程是比较辛苦的,但收获也是很多的。现在再看一些框架就能够理解为什么代码会这么写了,比如Threejs、deckgl等等,我们自己也用c++实现了一套底层的跨端渲染框架,虽然不全面,但内部够用同时也能提升自身技术水平。


架构


架构能力是随着工作中不断积累起来能力,当然这也需要在工作中不断的打磨和锻炼,如果一直是以完成任务的心态在工作那是很难练出来的。我所推崇的架构能力是以解决业务问题为主,提升产研的效率为辅。所以在工作中不会刻意去做架构,而是围绕着如何解决问题去架构,如何才能控制好不至于过度设计。


举个简单例子,假如我们已经有各种完善的点餐业务,需要做一个邀请大家一起喝奶茶的这么一个功能,从业务上我们先考虑两个核心逻辑:


1、用户点餐之后回到邀请页面,点完的所以人实时能看到其他人下单状态
2、队长确认所有人点完之后,下单付款,所有人的页面切换到送餐状态

如果是快速实现这个功能的话,其实是比较简单的,起一个轮询任务实时问服务端要数据,拿到数据后,根据状态决定下一步显示什么状态的页面


但是随着业务发展,会加入很多奇怪的逻辑,比如要支持修改、删除、踢人等等,这就会导致这个页面逻辑及其的复杂起来,如果不去思考的话,很容易就写出一堆面条代码,最后自己都不愿意去改。


所以针对这个功能  ,我自己会抽象成几部分去思考:


1、store该如何拆解,拆成几个,每个store对应哪个组件
2、store该如何去更新
3、与服务端如何通信,websocket、轮询都可以,看当下实际情况,保证稳定性即可
4、可以写几个js类去做这个事情,每个类的职责是什么

我觉得思考完这几个问题 ,大家对于这个页面该怎么去写应该能有一个很清晰的架构图在脑海中了吧,这里我就不过多展开了 ,有兴趣的话私聊,核心是要说架构其实也可以很简单。


总结


今年就不立flag了,目标能把去年的flag实现好,2023年是疫情结束的一年 ,我认为这是一个好的开始,努力工作是一方面,享受生活我认为也同样重要,今年更需要做好工作和生活的平衡,工作以外能有一些其他的成就。


写给36岁的自己,简单地回顾过去、总结现在、展望未来,希望当37岁的自己回过头来看的时候,能够鄙视现在的自己,写出更好的《写给37岁的自己》。


附上去年总结《写给35岁的自己》

trong>

收起阅读 »

Android 插件化:插件内部跳转

在Android 插件化(加载插件)中,简单的用一个demo 讲了如何加载一个插件,并使用插件里的资源。 那如果我们的插件中有多个页面呢,要怎么办? 其实,也是很简单,还是通过外部 PluginActivity 的 startActivity来实现 一、Lif...
继续阅读 »

在Android 插件化(加载插件)中,简单的用一个demo 讲了如何加载一个插件,并使用插件里的资源。


那如果我们的插件中有多个页面呢,要怎么办?

其实,也是很简单,还是通过外部 PluginActivitystartActivity来实现


一、LifeActivitystartActivity


LifeActivity 这个插件类中定义一个 startActivity 方法,用宿主的 context 调用 startActivity 方法


public void startActivity(Intent intent) {
if (context != null) {
if (intent==null||intent.getComponent()==null)return;
Intent newIntent=new Intent();
String className=intent.getComponent().getClassName();
if (TextUtils.isEmpty(className))return;
Log.e("startActivity","className="+className);
newIntent.putExtra("className",intent.getComponent().getClassName());
context.startActivity(newIntent);
}
}

而对于第一个插件中的页面 TestActivity


image.png


可以看到,插件中的第一个页面 TestActivity 点击打开 插件页面 Test2Activity 时。写法跟我们在Android中的风格是一模一样的。其中的 findViewById 等,只要是用到上下文的,全部替换成宿主的,这里不多赘述了。


image.png


二、重写 PluginActivitystartActivity


注意:由于 Test2Activity 不是一个真正 Activity ,PluginActivitystartActivity 中,就不能打开这个页面,只能再重新打开一个PluginActivity,并将Test2Activity 类的信息再重新加载实例化一次,跟我们第一个加载TestActivity 是一样的。

override fun startActivity(intent: Intent?) {
val className = intent?.getStringExtra("className")
if (className.isNullOrBlank()) return
val newIntent = Intent(this, PluginActivity::class.java)
newIntent.putExtra("className", className)
super.startActivity(newIntent)
}

传入进去的 className 就是 Test2Activity ,在PluginActivity 走生命周期onCreate 时,loadClassActivity ,至此就完成了插件内部的跳转,是不是非常简单。


9b2ee0aed19ba5e77698cb1f9582b93d.gif


三、同理,有 Activity 就会有其他组件


我们可以在插件中自己实现 serviceContentProviderBroadcastReceiver
等等组件,并重写生命周期等方法。原理都非常简单,难的是思想,这些都是插件化中的冰山一脚,我自己的项目中的更加复杂。


由于这样的方式,需要手动创建生命周期管理,和后续Activity启动模式,入栈出栈的管理等等。其实可以使用ASM等字节码来转换,将四大组件转成普通类,这样,开发过程中容易调试,插件生成也相对简单。


四、动态加载


由于插件apk 是可以从外部sdk 等地方加载的,给我们带来很多便利。而且插件部分的资源都是动态的,可以做到热更新的效果,只要我修改了再重新打包下发就行了。后续可以自己实现一套插件管理,用于加载外部apk ,做到热插拔的作用。


作者:大强Dev
来源:juejin.cn/post/7209971268483825722
收起阅读 »

[崩溃] Android应用自动重启

背景 在App开发过程中,我们经常需要自动重启的功能。比如: 登录或登出的时候,为了清除缓存的一些变量,比较简单的方法就是重新启动app。 crash的时候,可以捕获到异常,直接自动重启应用。 在一些debug的场景中,比如设置了一些测试的标记位,需要重启才...
继续阅读 »

背景


在App开发过程中,我们经常需要自动重启的功能。比如:



  • 登录或登出的时候,为了清除缓存的一些变量,比较简单的方法就是重新启动app。

  • crash的时候,可以捕获到异常,直接自动重启应用。

  • 在一些debug的场景中,比如设置了一些测试的标记位,需要重启才能生效,此时可以用自动重启,方便测试。


那我们如何实现自动重启的功能呢?我们都知道如何杀掉进程,但是当我们的进程被杀掉之后,如何唤醒呢?


这篇文章就来和大家介绍一下,实现应用自动重启的几种方法。


方法1 AlarmManager


    private void setAlarmManager(){
Intent intent = new Intent();
intent.setClass(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_ONE_SHOT);
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis()+100, pendingIntent);
Process.killProcess(Process.myPid());
System.exit(0);
}

使用AlarmManager实现自动重启的核心思想:创建一个100ms之后的Alarm任务,等Alarm任务到执行时间了,会自动唤醒App。


缺点:



  • 在App被杀和拉起之间,会显示系统Launcher桌面,体验不好。

  • 在高版本不适用


方法2 直接启动Activity


private void restartApp(){
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
Process.killProcess(Process.myPid());
System.exit(0);
}

缺点:



  • MainActivity必须是Standard模式


方法3 ProcessPhoenix


JakeWharton大神开源了一个叫ProcessPhoenix的库,这个库可以实现无缝重启app。


实现原理其实很简单,我们先讲怎么使用,然后再来分析源码。


使用方法


首先引入ProcessPhoenix库,这个库不需要初始化,可以直接使用。


implementation 'com.jakewharton:process-phoenix:2.1.2'

使用1:如果想重启app后进入首页:


ProcessPhoenix.triggerRebirth(context);

使用2:如果想重启app后进入特定的页面,则需要构造具体页面的intent,当做参数传入:


Intent nextIntent = //...
ProcessPhoenix.triggerRebirth(context, nextIntent);

有一点需要特别注意。



  • 我们通常会在ApplicationonCreate方法中做一系列初始化的操作。

  • 如果使用Phoenix库,需要在onCreate方法中判断,如果当前进程是Phoenix进程,则直接return,跳过初始化的操作。


if (ProcessPhoenix.isPhoenixProcess(this)) {
return;
}

源码


ProcessPhoenix的原理:



  • 当调用triggerRebirth方法的时候,会启动一个透明的Activity,这个Activity运行在:phoenix进程

  • Activity启动后,杀掉主进程,然后用:phoenix进程拉起主进程的Activity

  • 关闭当前Activity,杀掉:phoenix进程


先来看看ManifestActivity的注册代码:


 <activity
android:name=".ProcessPhoenix"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:process=":phoenix"
android:exported="false"
/>


可以看到这个Activity确实是在:phoenix进程启动的,且是Translucent透明的。


整个ProcessPhoenix的代码只有不到120行,非常简单。我们来看下triggerRebirth做了什么。


  public static void triggerRebirth(Context context) {
triggerRebirth(context, getRestartIntent(context));
}

不带intenttriggerRebirth,最后也会调用到带intenttriggerRebirth方法。


getRestartIntent会获取主进程的Launch Activity


  private static Intent getRestartIntent(Context context) {
String packageName = context.getPackageName();
Intent defaultIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
if (defaultIntent != null) {
return defaultIntent;
}
}

所以要调用不带intenttriggerRebirth,必须在当前Appmanifest里,指定Launch Activity,否则会抛出异常。


接着来看看真正的triggerRebirth方法:


  public static void triggerRebirth(Context context, Intent... nextIntents) {
if (nextIntents.length < 1) {
throw new IllegalArgumentException("intents cannot be empty");
}
// 第一个activity添加new_task标记,重新开启一个新的stack
nextIntents[0].addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK);

Intent intent = new Intent(context, ProcessPhoenix.class);
// 这里是为了防止传入的context非Activity
intent.addFlags(FLAG_ACTIVITY_NEW_TASK); // In case we are called with non-Activity context.
// 将待启动的intent作为参数,intent是parcelable的
intent.putParcelableArrayListExtra(KEY_RESTART_INTENTS, new ArrayList<>(Arrays.asList(nextIntents)));
// 将主进程的pid作为参数
intent.putExtra(KEY_MAIN_PROCESS_PID, Process.myPid());
// 启动ProcessPhoenix Activity
context.startActivity(intent);
}

triggerRebirth方法,主要的功能是启动ProcessPhoenix Activity,相当于启动了:phoenix进程。同时,会将nextIntents和主进程的pid作为参数,传给新启动的ProcessPhoenix Activity


下面我们再来看看,ProcessPhoenix ActivityonCreate方法,看看新进程启动后做了什么。


  @Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 首先杀死主进程
Process.killProcess(getIntent().getIntExtra(KEY_MAIN_PROCESS_PID, -1)); // Kill original main process

ArrayList<Intent> intents = getIntent().getParcelableArrayListExtra(KEY_RESTART_INTENTS);
// 再启动主进程的intents
startActivities(intents.toArray(new Intent[intents.size()]));
// 关闭当前Activity,杀掉当前进程
finish();
Runtime.getRuntime().exit(0); // Kill kill kill!
}

:phoenix进程主要做了以下事情:



  • 杀死主进程

  • 用传入的Intent启动主进程的Activity(也可以是Service)

  • 关闭phoenix Activity,杀掉phoenix进程


总结


如果App有自动重启的需求,比较推荐使用ProcessPhoenix的方法。


原理其实非常简单:



  • 启动一个新的进程

  • 杀掉主进程

  • 用新的进程,重新拉起主进程

  • 杀掉新的进程


我们可以直接在工程里引入ProcessPhoenix开源库,也可以自己用代码实现这样的机

作者:尹学姐
来源:juejin.cn/post/7207743145999024165
制,总之都比较简单。

收起阅读 »

抛弃trycatch,用go的思想去处理js异常

web
errors 错误处理在编程中是不可避免的一部分,在程序开发过程中,不可必要的会出现各种的错误,是人为也可能是失误,任何不可预料的可能都会发生 为了更好的保证程序的健壮性和稳定性 我们必须要对错误处理有更好的认识 最近迷上了golang的错误处理哲学,希望由浅...
继续阅读 »

errors


错误处理在编程中是不可避免的一部分,在程序开发过程中,不可必要的会出现各种的错误,是人为也可能是失误,任何不可预料的可能都会发生


为了更好的保证程序的健壮性和稳定性


我们必须要对错误处理有更好的认识


最近迷上了golang的错误处理哲学,希望由浅入深的总结一下自己的思考和看得见的思考


👀 用 error 表示可能出现的错误,用throw强制抛出错误


通常情况下 错误处理的方式无非不过两种



  • 泛处理

  • 精处理


其实都很好理解


// 伪代码

try {
const file = await readFile('../file')

const content = filterContent(file)

} catch (e) {
alert('xxxx',e.msg)
}

泛处理指的是对所有可能的错误都使用相同的处理方式,比如在代码中使用相同统一的错误提示信息


这种方式适用于一些简单的,不太可能发生的错误,不如文件不存在,网络连接超时等。


对于更加复杂的错误,应该使用精处理,即根据具体情况对不同类型的错误进行特定的处理



const [readErr,readFile] = await readFile('../file')

if (readErr) {
// 处理读取错误
return
}

const [filterErr,filterContent] = filterContent(readFile)

if (filterErr) {
// 处理过滤错误
return
}

精处理可以让我们把控计划的每一步,问题也很显然暴露了出来,过于麻烦


在实际开发当中,我们需要根据实际情况选择适当的方式进行错误处理。


由于本人是精处理分子,基于此开发了一个js版本的errors


如何定义一个错误


import { Errors } from '@memo28/utils'

Errors.News('err')

如何给一个错误分类


import { Errors } from '@memo28/utils'

Errors.News('err', { classify: 1 })

如何判断两个错误是相同类型


import { Errors } from '@memo28/utils'

Errors.As(Errors.News('err', { classify: 1 }),
Errors.News('err2', { classify: 1 })) // true

Errors.As(Errors.News('err', { classify: 1 }),
Errors.News('err2', { classify: 2 })) // false

一个错误包含了哪些信息?


Errors.New('as').info() // { msg: 'as' , classify: undefined }

Errors.New('as').trace() // 打印调用栈

Errors.New('as').unWrap() // 'as'

最佳实践


import { AnomalousChain, panicProcessing } from '@memo28/utils'

class A extends AnomalousChain {
// 定义错误处理函数,
// 当每次执行的被 panicProcessing 装饰器包装过的函数都会检查 是否存在 this.errors 是否为 ErrorsNewResult 类型
// 如果为 ErrorsNewResult 类型则 调用 panicProcessing的onError回调 和 skip函数
skip(errors: ErrorsNewResult | null): this {
console.log(errors?.info().msg)
return this
}

@panicProcessing()
addOne(): this {
console.log('run one')
super.setErrors(Errors.New('run one errors'))
return this
}

@panicProcessing({
// 在 skip 前执行
onError(error) {
console.log(error.unWrap())
},
// onRecover 从错误中恢复, 当返回的是true时 继续执行addTwo内逻辑,反之
// onRecover(erros) {
// return true
// },
})
addTwo(): this {
console.log('run two')
return this
}
}
new A().addOne().addTwo()



// output
run one
run one errors // in onError
run one errors // in skip fn

复制代码
作者:我开心比苦恼多
来源:juejin.cn/post/7207707775774031930
>
收起阅读 »

“ChatGPT 们” 所需算力真是“贵滴夸张”!

先抛几个数据: 当下,每天有超过 2 亿的人在疯狂地抛出各式各样的问题请求 ChatGPT 回复 如果要完成这 2 亿+ 的咨询量,初始投入需要 3 万+ 块英伟达 A100 GPU 来计算 而 A100 是当下这个星球拥有最强 AI 算力的芯片,买一块至少要...
继续阅读 »

先抛几个数据:


当下,每天有超过 2 亿的人在疯狂地抛出各式各样的问题请求 ChatGPT 回复


如果要完成这 2 亿+ 的咨询量,初始投入需要 3 万+ 块英伟达 A100 GPU 来计算


而 A100 是当下这个星球拥有最强 AI 算力的芯片,买一块至少要 7W+ RMB💴


image.png


意思就是:光计算这些有意义/无意义的问题,就要花费:21 亿+ RMB 💴


这还不算电费/维护费等,众所周知,这种庞大计算类似矿机,很费电


一块 A100,功率 400W,30000 块 A100 ,就是 12_000_000 W,是 12 MW(12 个 100 万瓦)


image.png


目前,“GPT们”就像是井喷一样出现了各类产品,先不管算的结果怎么样,大家先支棱起来、先算起来


有预测:十年后,“GPT们” 一天所需算力的生产功率相当于半个核电站产生的功率,这是离谱且夸张的!全球算力几乎快要无法满足“GPT们”了


image.png


所以,以 ChatGPTPlus(每月20刀) 为代表的 “GPT们” 很贵,因为它本来算力消费就很贵,“用 GPT 编程比招一个普通程序员更贵”的段子并非玩笑。


所以,为什么算力如此重要?为什么微软要和韩国SK集团布局自建核电站?为什么咱们要强调西数东算、云计算等等?从这里也能窥见一斑。


不夸张的说,在未来,国力强弱一方面将通过算力强弱来体现。


image.png


2016年6月的不同国家之间的超级计算机500强的分布


image.png


超级计算机模拟风洞实验


小思考:在未来,算力的瓶颈将如何突破?


—— 目前芯片仍处在传统冯·诺伊曼架构之下,存储和计算区域是分离的,搬运数据“从存储到计算”花费巨大的功耗,如果能实现“存算一体”(就像大脑一样)将提升算力进入新的量级。


所以“存算一体”可能是个方向~


推荐阅读:


# 计算型存储/存算一体如何实现? - bonnie的回答


# 5分钟新知关注芯片算力|存算一体为什么是AI时代主流计算架构?



作者:掘金安东尼
来源:juejin.cn/post/7210028417617395772
收起阅读 »

为什么是14px?

web
字号与体验 肉眼到物体之间的距离,物体的高度以及这个三角形的角度,构成了一个三角函数的关系。 h=2d*tan(a/2) 而公式中的 h 的值和我们要解决的核心问题『主字号』有着很大的关系。 关于这个 a 的角度,有机构和团队做过研究,当大于 0.3 度时的...
继续阅读 »

字号与体验


1609983492683.png


肉眼到物体之间的距离,物体的高度以及这个三角形的角度,构成了一个三角函数的关系。


h=2d*tan(a/2)


而公式中的 h 的值和我们要解决的核心问题『主字号』有着很大的关系。


关于这个 a 的角度,有机构和团队做过研究,当大于 0.3 度时的阅读效率是最好的。


同时我们在操作电脑时,一般来说眼睛距离电脑屏幕的平均值大概会在 50 厘米左右。


然而,公式中的距离和高度的单位都是厘米,字体的单位是 pixel。


因此我们还需要将二者之间做一轮转换,完成转换所需的两个数值分别是 2.54(cm 到 inch)和 PPI(inch 到 pixel)。           


*PPI(Pixels Per Inch):像素密度,所表示的是每英寸所拥有的像素数量。


公式表达为_ppi_=√(x2+y2)/z(x:长度像素数;y:宽度像素数;z:屏幕大小)  


我们假定 PPI 为 120。通过计算便可以得出在显示器的 PPI 为 120 的情况下,理论上大于 12px 的字体能够满足用户的最佳阅读效率。基于这样的思路,确定主流 PPI 的范围,就很容易锁定主字体的大小了。


根据网络上的数据来源,我们发现只有大约 37.6% 的显示器 PPI 是小于 120 的,而 PPI 在 120-140 的显示器的占比大约为 40%。换句话说 12px 的字体只能满足 37.6% 用户的阅读体验,但如果我们将字体放大到 14px,就可以保证大约 77% 的显示器用户处于比较好的阅读体验。


作者:IDuxFE
来源:juejin.cn/post/7209967260899147834
收起阅读 »

从零开始构建用户模块:前端开发实践

web
场景 在大多数前端应用中都会有自己的用户模块,对于前端应用中的用户模块来说,需要从多个方面功能考虑,以掘金为例,可能需要下面这些功能: 多种登录方式,账号密码,手机验证码,第三方登录等 展示类信息,用户头像、用户名、个人介绍等 用户权限控制,可能需要附带角色...
继续阅读 »

场景


在大多数前端应用中都会有自己的用户模块,对于前端应用中的用户模块来说,需要从多个方面功能考虑,以掘金为例,可能需要下面这些功能:



  1. 多种登录方式,账号密码,手机验证码,第三方登录等

  2. 展示类信息,用户头像、用户名、个人介绍等

  3. 用户权限控制,可能需要附带角色信息等

  4. 发起请求可能还需要带上token等


接下来我们来一步步实现一个简单的用户模块


需求分析


用户模型


针对这些需求我们可以列出一个用户模型,包括下面这些参数


展示信息:



  • username 用户名

  • avatar 头像

  • introduction 个人介绍


角色信息:



  • role


鉴权:




  • token




这个user模型对于前端应用来说应该是全局唯一的,我们这里可以用singleton,标注为全局单例


import { singleton } from '@clean-js/presenter';

@singleton()
export class User {
username = '';
avatar = '';
introduction = '';

role = 'member';
token = '';

init(data: Partial<Omit<User, 'init'>>) {
Object.assign(this, data);
}
}

用户服务


接着可以针对我们的用户场景来构建用户服务类。


如下面这个UserService:



  • 注入了全局单例的User

  • loginWithMobile 提供了手机号验证码登录方法,这里我们用一个mock代码来模拟请求登录

  • updateUserInfo 用来获取用户信息,如头像,用户名之类的。从后端拉取信息之后我们会更新单例User


import { injectable } from '@clean-js/presenter';
import { User } from '../entity/user';


@injectable()
export class UserService {
constructor(private user: User) {}


/**
* 手机号验证码登录
*/

loginWithMobile(mobile: string, code: string) {
// mock 请求接口登录
return new Promise((resolve) => {
setTimeout(() => {
this.user.init({
token: 'abcdefg',
});


resolve(true);
}, 1000);
});
}


updateUserInfo() {
// mock 请求接口登录
return new Promise<User>((resolve) => {
setTimeout(() => {
this.user.init({
avatar:
'https://p3-passport.byteimg.com/img/user-avatar/2245576e2112372252f4fbd62c7c9014~180x180.awebp',
introduction: '欢乐堡什么都有,唯独没有欢乐',
username: '鱼露',
role: 'member',
});


resolve(this.user);
}, 1000);
});
}
}

界面状态


我们以登录界面和个人中心页面为例


登录界面


在登录界面需要这些页面状态和方法


View State:



  • loading: boolean; 页面loading

  • mobile: string; 输入手机号

  • code: string; 输入验证码


methods:



  • showLoading

  • hideLoading

  • login


import { history } from 'umi';
import { UserService } from '@/module/user/service/user';
import { LockOutlined, UserOutlined } from '@ant-design/icons';
import { injectable, Presenter } from '@clean-js/presenter';
import { usePresenter } from '@clean-js/react-presenter';
import { Button, Form, Input, message, Space } from 'antd';

interface IViewState {
loading: boolean;
mobile: string;
code: string;
}
@injectable()
class PagePresenter extends Presenter<IViewState> {
constructor(private userService: UserService) {
super();
this.state = {
loading: false,
mobile: '',
code: '',
};
}

_loadingCount = 0;

showLoading() {
if (this._loadingCount === 0) {
this.setState((s) => {
s.loading = true;
});
}
this._loadingCount += 1;
}

hideLoading() {
this._loadingCount -= 1;
if (this._loadingCount === 0) {
this.setState((s) => {
s.loading = false;
});
}
}

login = () => {
const { mobile, code } = this.state;
this.showLoading();
return this.userService
.loginWithMobile(mobile, code)
.then((res) => {
if (res) {
message.success('登录成功');
}
})
.finally(() => {
this.hideLoading();
});
};
}

export default function LoginPage() {
const { p } = usePresenter(PagePresenter);

return (
<div>
<Form
name="normal_login"
initialValues={{ email: 'admin@admin.com', password: 'admin' }}
onFinish={() => {
console.log(p, '==p');
p.login().then(() => {
setTimeout(() => {
history.push('/profile');
}, 1000);
});
}}
>
<Form.Item
name="email"
rules={[{ required: true, message: 'Please input your email!' }]}
>
<Input
prefix={<UserOutlined className="site-form-item-icon" />}
placeholder="email"
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: 'Please input your Password!' }]}
>
<Input
prefix={<LockOutlined className="site-form-item-icon" />}
type="password"
placeholder="Password"
/>
</Form.Item>

<Form.Item>
<Space>
<Button
type="primary"
htmlType="submit"
className="login-form-button"
>
Log in
</Button>
</Space>
</Form.Item>
</Form>
</div>
);
}

如上代码所示,一个登录页面就完成了,接下来我们实现一下个人中心页面


个人中心


import { UserService } from '@/module/user/service/user';
import { injectable, Presenter } from '@clean-js/presenter';
import { usePresenter } from '@clean-js/react-presenter';
import { Image, Spin } from 'antd';
import { useEffect } from 'react';

interface IViewState {
loading: boolean;
username: string;
avatar: string;
introduction: string;
}

@injectable()
class PagePresenter extends Presenter<IViewState> {
constructor(private userS: UserService) {
super();
this.state = {
loading: false,
username: '',
avatar: '',
introduction: '',
};
}

_loadingCount = 0;

showLoading() {
if (this._loadingCount === 0) {
this.setState((s) => {
s.loading = true;
});
}
this._loadingCount += 1;
}

hideLoading() {
this._loadingCount -= 1;
if (this._loadingCount === 0) {
this.setState((s) => {
s.loading = false;
});
}
}

/**
* 拉取用户信息
*/

getUserInfo() {
this.showLoading();
this.userS
.updateUserInfo()
.then((u) => {
this.setState((s) => {
s.avatar = u.avatar;
s.username = u.username;
s.introduction = u.introduction;
});
})
.finally(() => {
this.hideLoading();
});
}
}
const ProfilePage = () => {
const { p } = usePresenter(PagePresenter);

useEffect(() => {
p.getUserInfo();
}, []);

return (
<Spin spinning={p.state.loading}>
<p>
avatar: <Image src={p.state.avatar} width={100} alt="avatar"></Image>
</p>
<p>username: {p.state.username}</p>
<p>introduction: {p.state.introduction}</p>
</Spin>

);
};

export default ProfilePage;

在这个ProfilePage中,我们初始化时会执行p.getUserInfo();


期间会切换loading的状态,并映射到页面的Spin组件中,执行完成后,更新页面的用户信息,用于展示


总结


至此,一个简单的用户模块就实现啦,整个用户模块以及页面的依赖关系可以查看下面这个UML图,



状态库仓库

仓库


各位大佬,记得一键三连,给个star,谢谢



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

Android必知必会-Stetho调试工具

一、背景 Stetho是 Facebook 出品的一个强大的 Android 调试工具,使用该工具你可以在 Chrome Developer Tools查看APP的布局, 网络请求(仅限使用Volle, okhttp的网络请求库), Sqlite, Pref...
继续阅读 »

一、背景



Stetho是 Facebook 出品的一个强大的 Android 调试工具,使用该工具你可以在 Chrome Developer Tools查看APP的布局, 网络请求(仅限使用Volle, okhttp的网络请求库), Sqlite, Preference, 一切都是可视化的操作,无须自己在去使用adb, 也不需要root你的设备



本人使用自己的Nubia Z9 Mini作为调试机,由于牵涉到Sqlite数据库,所以尝试了很多办法把它Root了,然而Root之后就无法正常升级系统。
今天得知一调试神器Stetho,无需Root就能查看数据库以及APP的布局(这一点没有Android Device Monitor使用方便,但是Android Device Monitor在Mac上总是莫名其妙出问题),使用起来很方便,大家可以尝试一下。


二、配置流程


1.引入主库


使用Gradle方式:


// Gradle dependency on Stetho 
dependencies {
compile 'com.facebook.stetho:stetho:1.3.1'
}

此外还支持Maven方式,这里不做介绍。


2.引入网络请求库


如果需要调试网络且你使用的网络请求库是Volle或者Okhttp,那么你才需要配置,否则跳过此步。
以下根据自己使用的网络请求库情况来导入相应的库:
1.使用okhttp 2.X


 dependencies { 
compile 'com.facebook.stetho:stetho-okhttp:1.3.1'
}

2.使用okhttp 3.X


dependencies { 
compile 'com.facebook.stetho:stetho-okhttp3:1.3.1'
}

3.使用HttpURLConnection


dependencies { 
compile 'com.facebook.stetho:stetho-urlconnection:1.3.1'
}

3.配置代码


配置Application


public class XXX extends Application {
public void onCreate() {
super.onCreate();
Stetho.initializeWithDefaults(this);
}
}

配置网络请求库:
OkHttp 2.2.x+ 或 3.x


//方案一
OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(new StethoInterceptor());

//方案二
new OkHttpClient.Builder()
.addNetworkInterceptor(new StethoInterceptor())
.build();

如果使用的是HttpURLConnection,请查阅相关文档。


4.使用


运行重新编译后的APP程序,保持手机与电脑的连接,然后打开Chrome浏览器,在地址栏里输入:chrome://inspect然后选择自己的设备下运行的APP进程名下的Inspect链接 即可进行调试。


三、遇到的问题


1.okhttp版本问题:


可能你还在使用okhttp 2.x的版本,在引入网络库的时候,你需要去查看一下Stetho当前版本使用的okhttp版本,避免在项目中使用多个不同版本的okhttp


PSokhttp2.x和3.x的引入方式略有不同,不可以直接修改版本号来导入:


//2.x
compile 'com.squareup.okhttp:okhttp:2.x.x'
//3.x
compile 'com.squareup.okhttp3:okhttp:3.x.x'

2.配置okhttp代码方案一报错:


//方案一
OkHttpClient client = new OkHttpClient();
client.networkInterceptors().add(new StethoInterceptor());

//方案二
OkHttpClient client = new OkHttpClient.Builder()
.addNetworkInterceptor(new StethoInterceptor())
.build();

我在使用方案一进行配置okhttp的时候,会报错:


 Caused by: java.lang.UnsupportedOperationException

不知道是不是兼容的问题,大家在使用的时候请注意。



Stetho官网


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

一个app到底会创建多少个Application对象

问题背景 最近跟群友讨论一个技术问题: 一个应用开启了多进程,最终到底会创建几个application对象,执行几次onCreate()方法? 有的群友根据自己的想法给出了猜想 甚至有的群友直接咨询起了ChatGPT 但至始至终都没有一个最终的结论。于是...
继续阅读 »

问题背景


最近跟群友讨论一个技术问题:


交流1


一个应用开启了多进程,最终到底会创建几个application对象,执行几次onCreate()方法?


有的群友根据自己的想法给出了猜想


交流2


甚至有的群友直接咨询起了ChatGPT


chatgpt1.jpg


但至始至终都没有一个最终的结论。于是乎,为了弄清这个问题,我决定先写个demo测试得出结论,然后从源码着手分析原因


Demo验证


首先创建了一个app项目,开启多进程


<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">


<application
android:name=".DemoApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Demo0307"
tools:targetApi="31">

<!--android:process 开启多进程并设置进程名-->
<activity
android:name=".MainActivity"
android:exported="true"
android:process=":remote">

<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

</activity>
</application>

</manifest>

然后在DemoApplication的onCreate()方法打印application对象的地址,当前进程名称


public class DemoApplication extends Application {
private static final String TAG = "jasonwan";

@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "Demo application onCreate: " + this + ", processName=" + getProcessName(this));
}

private String getProcessName(Application app) {
int myPid = Process.myPid();
ActivityManager am = (ActivityManager) app.getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> runningAppProcesses = am.getRunningAppProcesses();
for (ActivityManager.RunningAppProcessInfo runningAppProcess : runningAppProcesses) {
if (runningAppProcess.pid == myPid) {
return runningAppProcess.processName;
}
}
return "null";
}
}

运行,得到的日志如下


2023-03-07 11:15:27.785 19563-19563/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote

查看当前应用所有进程


查看进程1


说明此时app只有一个进程,且只有一个application对象,对象地址为@fb06c2d


现在我们将进程增加到多个,看看情况如何


<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">


<application
android:name=".DemoApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Demo0307"
tools:targetApi="31">

<!--android:process 开启多进程并设置进程名-->
<activity
android:name=".MainActivity"
android:exported="true"
android:process=":remote">

<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

</activity>
<activity
android:name=".TwoActivity"
android:process=":remote2" />

<activity
android:name=".ThreeActivity"
android:process=":remote3" />

<activity
android:name=".FourActivity"
android:process=":remote4" />

<activity
android:name=".FiveActivity"
android:process=":remote5" />

</application>

</manifest>

逻辑是点击MainActivity启动TwoActivity,点击TwoActivity启动ThreeActivity,以此类推。最后我们运行,启动所有Activity得到的日志如下


2023-03-07 11:25:35.433 19955-19955/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote
2023-03-07 11:25:43.795 20001-20001/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote2
2023-03-07 11:25:45.136 20046-20046/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote3
2023-03-07 11:25:45.993 20107-20107/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote4
2023-03-07 11:25:46.541 20148-20148/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote5

查看当前应用所有进程


查看进程2


此时app有5个进程,但application对象地址均为@fb06c2d,地址相同意味着它们是同一个对象。


那是不是就可以得出结论,无论启动多少个进程都只会创建一个application对象呢?并不能妄下此定论,我们将MainActivityprocess属性去掉再运行,得到的日志如下


2023-03-07 11:32:10.156 20318-20318/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@5d49e29, processName=com.jason.demo0307
2023-03-07 11:32:15.143 20375-20375/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote2
2023-03-07 11:32:16.477 20417-20417/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote3
2023-03-07 11:32:17.582 20463-20463/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote4
2023-03-07 11:32:18.882 20506-20506/com.jason.demo0307 D/jasonwan: Demo application onCreate: com.jason.demo0307.DemoApplication@fb06c2d, processName=com.jason.demo0307:remote5

查看当前应用所有进程


查看进程3


此时app有5个进程,但有2个application对象,对象地址为@5d49e29和@fb06c2d,且子进程的application对象都相同。


上述所有进程的父进程ID为678,而此进程正是zygote进程


zygote进程


根据上面的测试结果我们目前能得出的结论:



  • 结论1:单进程只创建一个Application对象,执行一次onCreate()方法;

  • 结论2:多进程至少创建2个Application对象,执行多次onCreate()方法,几个进程就执行几次;


结论2为什么说至少创建2个,因为我在集成了JPush的商业项目中测试发现,JPush创建的进程跟我自己创建的进程,Application地址是不同的。


jpush进程


这里三个进程,分别创建了三个Application对象,对象地址分别是@f31ba9d,@2c586f3,@fb06c2d


源码分析


这里需要先了解App的启动流程,具体可以参考《App启动流程》


Application的创建位于frameworks/base/core/java/android/app/ActivityThread.javahandleBindApplication()方法中


	@UnsupportedAppUsage
private void handleBindApplication(AppBindData data) {
long st_bindApp = SystemClock.uptimeMillis();
//省略部分代码

// Note when this process has started.
//设置进程启动时间
Process.setStartTimes(SystemClock.elapsedRealtime(), SystemClock.uptimeMillis());

//省略部分代码

// send up app name; do this *before* waiting for debugger
//设置进程名称
Process.setArgV0(data.processName);
//省略部分代码

// Allow disk access during application and provider setup. This could
// block processing ordered broadcasts, but later processing would
// probably end up doing the same disk access.
Application app;
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskWrites();
final StrictMode.ThreadPolicy writesAllowedPolicy = StrictMode.getThreadPolicy();
try {
// If the app is being launched for full backup or restore, bring it up in
// a restricted environment with the base application class.
//此处开始创建application对象,注意参数2为null
app = data.info.makeApplication(data.restrictedBackupMode, null);

//省略部分代码
try {
if ("com.jason.demo0307".equals(app.getPackageName())){
Log.d("jasonwan", "execute app onCreate(), app=:"+app+", processName="+getProcessName(app)+", pid="+Process.myPid());
}
//执行application的onCreate方法()
mInstrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
} finally {
// If the app targets < O-MR1, or doesn't change the thread policy
// during startup, clobber the policy to maintain behavior of b/36951662
if (data.appInfo.targetSdkVersion < Build.VERSION_CODES.O_MR1
|| StrictMode.getThreadPolicy().equals(writesAllowedPolicy)) {
StrictMode.setThreadPolicy(savedPolicy);
}
}
//省略部分代码
}

实际创建过程在frameworks/base/core/java/android/app/LoadedApk.java中的makeApplication()方法中,LoadedApk顾名思义就是加载好的Apk文件,里面包含Apk所有信息,像包名、Application对象,app所在的目录等,这里直接看application的创建过程


	@UnsupportedAppUsage
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation)
{
if ("com.jason.demo0307".equals(mApplicationInfo.packageName)) {
Log.d("jasonwan", "makeApplication: mApplication="+mApplication+", pid="+Process.myPid());
}
//如果已经创建过了就不再创建
if (mApplication != null) {
return mApplication;
}

Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "makeApplication");

Application app = null;

String appClass = mApplicationInfo.className;
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}

try {
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals("android")) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
"initializeJavaContextClassLoader");
initializeJavaContextClassLoader();
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
//反射创建application对象
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
if ("com.jason.demo0307.DemoApplication".equals(appClass)){
Log.d("jasonwan", "create application, app="+app+", processName="+mActivityThread.getProcessName()+", pid="+Process.myPid());
}
appContext.setOuterContext(app);
} catch (Exception e) {
Log.d("jasonwan", "fail to create application, "+e.getMessage());
if (!mActivityThread.mInstrumentation.onException(app, e)) {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
throw new RuntimeException(
"Unable to instantiate application " + appClass
+ ": " + e.toString(), e);
}
}
mActivityThread.mAllApplications.add(app);
mApplication = app;

if (instrumentation != null) {
try {
//第一次启动创建时,instrumentation为null,不会执行onCreate()方法
instrumentation.callApplicationOnCreate(app);
} catch (Exception e) {
if (!instrumentation.onException(app, e)) {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
throw new RuntimeException(
"Unable to create application " + app.getClass().getName()
+ ": " + e.toString(), e);
}
}
}

// 省略部分代码
return app;
}

为了看清application到底被创建了几次,我在关键地方埋下了log,TAG为jasonwan的log是我自己加的,编译验证,得到如下log


启动app,进入MainActivity
03-08 17:20:29.965 4069 4069 D jasonwan: makeApplication: mApplication=null, pid=4069
//创建application对象,地址为@c2f8311,当前进程id为4069
03-08 17:20:29.967 4069 4069 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307, pid=4069
03-08 17:20:29.988 4069 4069 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307, pid=4069
03-08 17:20:29.989 4069 4069 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307, pid=4069
03-08 17:20:36.614 4069 4069 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4069

点击MainActivity,跳转到TwoActivity
03-08 17:20:39.686 4116 4116 D jasonwan: makeApplication: mApplication=null, pid=4116
//创建application对象,地址为@c2f8311,当前进程id为4116
03-08 17:20:39.687 4116 4116 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote2, pid=4116
03-08 17:20:39.688 4116 4116 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote2, pid=4116
03-08 17:20:39.688 4116 4116 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote2, pid=4116
03-08 17:20:39.733 4116 4116 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4116

点击TwoActivity,跳转到ThreeActivity
03-08 17:20:41.473 4147 4147 D jasonwan: makeApplication: mApplication=null, pid=4147
//创建application对象,地址为@c2f8311,当前进程id为4147
03-08 17:20:41.475 4147 4147 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote3, pid=4147
03-08 17:20:41.475 4147 4147 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote3, pid=4147
03-08 17:20:41.476 4147 4147 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote3, pid=4147
03-08 17:20:41.519 4147 4147 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4147

点击ThreeActivity,跳转到FourActivity
03-08 17:20:42.966 4174 4174 D jasonwan: makeApplication: mApplication=null, pid=4174
//创建application对象,地址为@c2f8311,当前进程id为4174
03-08 17:20:42.968 4174 4174 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote4, pid=4174
03-08 17:20:42.969 4174 4174 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote4, pid=4174
03-08 17:20:42.969 4174 4174 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote4, pid=4174
03-08 17:20:43.015 4174 4174 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4174

点击FourActivity,跳转到FiveActivity
03-08 17:20:44.426 4202 4202 D jasonwan: makeApplication: mApplication=null, pid=4202
//创建application对象,地址为@c2f8311,当前进程id为4202
03-08 17:20:44.428 4202 4202 D jasonwan: create application, app=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote5, pid=4202
03-08 17:20:44.429 4202 4202 D jasonwan: execute app onCreate(), app=:com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote5, pid=4202
03-08 17:20:44.430 4202 4202 D jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@c2f8311, processName=com.jason.demo0307:remote5, pid=4202
03-08 17:20:44.473 4202 4202 D jasonwan: makeApplication: mApplication=com.jason.demo0307.DemoApplication@c2f8311, pid=4202

结果很震惊,我们在5个进程中创建的application对象,地址均为@c2f8311,也就是至始至终创建的都是同一个Application对象,那么上面的结论2显然并不成立,只是测试的偶然性导致的。


可真的是这样子的吗,这也太颠覆我的三观了,为此我跟群友讨论了这个问题:


不同进程中的多个对象,内存地址相同,是否代表这些对象都是同一个对象?


群友的想法是,java中获取的都是虚拟内存地址,虚拟内存地址相同,不代表是同一个对象,必须物理内存地址相同,才表示是同一块内存空间,也就意味着是同一个对象,物理内存地址和虚拟内存地址存在一个映射关系,同时给出了java中获取物理内存地址的方法Android获取对象地址,主要是利用Unsafe这个类来操作,这个类有一个作用就是直接访问系统内存资源,具体描述见Java中的魔法类-Unsafe,因为这种操作是不安全的,所以被标为了私有,但我们可以通过反射去调用此API, 然后我又去请教了部门搞寄存器的大佬,大佬肯定了群友的想法,于是我添加代码,尝试获取对象的物理内存地址,看看是否相同


public class DemoApplication extends Application {
public static final String TAG = "jasonwan";

@Override
public void onCreate() {
super.onCreate();
Log.d(TAG, "DemoApplication=" + this + ", address=" + addressOf(this) + ", pid=" + Process.myPid());
}

//获取对象的真实物理地址
public static long addressOf(Object o) {
Object[] array = new Object[]{o};
long objectAddress = -1;
try {
Class cls = Class.forName("sun.misc.Unsafe");
Field field = cls.getDeclaredField("theUnsafe");
field.setAccessible(true);
Object unsafe = field.get(null);
Class unsafeCls = unsafe.getClass();
Method arrayBaseOffset = unsafeCls.getMethod("arrayBaseOffset", Object.class.getClass());
int baseOffset = (int) arrayBaseOffset.invoke(unsafe, Object[].class);
Method size = unsafeCls.getMethod("addressSize");
int addressSize = (int) size.invoke(unsafe);
switch (addressSize) {
case 4:
Method getInt = unsafeCls.getMethod("getInt", Object.class, long.class);
objectAddress = (int) getInt.invoke(unsafe, array, baseOffset);
break;
case 8:
Method getLong = unsafeCls.getMethod("getLong", Object.class, long.class);
objectAddress = (long) getLong.invoke(unsafe, array, baseOffset);
break;
default:
throw new Error("unsupported address size: " + addressSize);
}
} catch (Exception e) {
e.printStackTrace();
}
return objectAddress;
}
}

运行后得到如下日志


2023-03-10 11:01:54.043 6535-6535/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@930d275, address=8050489105119022792, pid=6535
2023-03-10 11:02:22.610 6579-6579/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119027136, pid=6579
2023-03-10 11:02:36.369 6617-6617/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119029912, pid=6617
2023-03-10 11:02:39.244 6654-6654/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119032760, pid=6654
2023-03-10 11:02:40.841 6692-6692/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119036016, pid=6692
2023-03-10 11:02:52.429 6729-6729/com.jason.demo0307 D/jasonwan: DemoApplication=com.jason.demo0307.DemoApplication@331b3b9, address=8050489105119038720, pid=6729

可以看到,虽然Application的虚拟内存地址相同,都是331b3b9,但它们的真实物理地址却不同,至此,我们可以得出最终结论



  • 单进程,创建1个application对象,执行一次onCreate()方法

  • 多进程(N),创建N个application对象,执行N次onCreate()方法


作者:小迪vs同学
来源:juejin.cn/post/7208345469658415159
收起阅读 »

回顾下小城市十年的经历:努力过,躺平过。

【回顾】 十年,目睹了时代的改变,也见证了技术的迭代。 【2011】大学毕业前的实习 这一年,大学即将毕业毕业,寒假回到了家乡的小县城,歪打正着的找了一份家乡县级市ZF的实习工作(合同工)。 技术:Ps Html Css 内容:内网新闻专题 薪资:150...
继续阅读 »

【回顾】



十年,目睹了时代的改变,也见证了技术的迭代。



【2011】大学毕业前的实习


这一年,大学即将毕业毕业,寒假回到了家乡的小县城,歪打正着的找了一份家乡县级市ZF的实习工作(合同工)。



  • 技术:Ps Html Css

  • 内容:内网新闻专题

  • 薪资:1500

  • 房价:3000/平


【2012】大学毕业了


这一年,大学毕业了,毕业那天喝了很多,并豪情壮志的和室友预定了四个五年计划。(一五:年薪10万。二五,年薪20万,三五,年薪30万,四五,财富自由。)


于是我放弃了ZF的安稳,只身来到了当地的十八线小城市,为了安身立命,于是随便找了一家只有2人的公司做网站~



  • 职位:美工

  • 技术:Ps Html Css Asp

  • 内容:企业网站

  • 薪资:1000

  • 房租:600

  • 房价:3500/平


这段时间,由于工资过低,每天只吃1-2顿饭,而且每天熬夜学习,体重从大学期间的100Kg降到了65Kg。


终于,在2012年的下半年找到了自己的第一份正式工作--所在城市日报集团。



  • 职位:美工

  • 技术:Ps Html Css JQ PHP

  • 门户网站、专题、企业网站

  • 薪资:3500

  • 房租:0(包吃住)

  • 房价:3500/平


【2014】开始学H5+C3


由于不知道多少线的小城市,技术需求极低,大部分工作内容还要考虑IE6的兼容性。技术部老大跟我说H5和C3咱们一时半会是用不到的。但我还是学了,我觉得早晚会用到。



  • 职位:美工

  • 技术:Ps Html Css JQ PHP

  • 门户网站、专题、企业网站

  • 薪资:3800

  • 房租:0(包吃住)

  • 房价:3500/平


【2015】辞职-新工作-公司解散-新工作


公司来了新领导,开始大刀阔斧的改革,并要求技术部承担经营任务,技术部老大一气之下带着我和另外一个程序W 离职了,大家决定自己干!


老大带我们组个个外包团队,包了一个公司的H5封装APP的活,签了3年的外包合同。我负责H5页面,W负责程序,老大负责服务器。我们仨找了个孵化器开始干了起来。



  • 职位:美工?前端?

  • 技术:Ps Html5 Css3 JQ

  • H5封装APP

  • 薪资:7000

  • 房租:500

  • 房价:5500/平


三个月后甲方撤资了~~ 突如其来的大家没了收入。老大还在努力的找新单子,我们也发现这不是长久之计,于是边接小单子边找新的出路。


新工作来的很意外,之前报社的老领导去了临市的大众网记者站当副站长,又被调回我们城市成立新的记者站。于是老领导找到了我,他们需要一个全栈员工。老领导于我也算是有知遇之恩,所以我决定去帮帮她。虽然给了我主任级别的待遇,但是工资依然不高。



  • 职位:全栈

  • 技术:Ps Html5 Css3 JQ 内部cms

  • 门户网站、专题、企业网站 微信H5

  • 薪资:4000

  • 房租:500

  • 房价:5500/平


【2016】 微信小程序来了


这一年,微信小程序的风吹进了我们这座小城市,开始有人问我会不会做。于是开始学习,并成功为一个经典做了一个卖票小程序,赚了3000.



  • 职位:全栈

  • 技术:Ps Html5 Css3 JQ 内部cms

  • 门户网站、专题、企业网站 微信H5

  • 薪资:4500

  • 房租:500

  • 房价:6500/平


【2018】结婚-辞职-新工作


这一年,和相恋5年的女朋友结婚了。忽然之间有了责任的压力,看看工资卡的工资,现在的工作显然是不行了,于是我决定辞职。


当我决定辞职之后我收到了两个橄榄枝,分别是老东家报社,和老同事W的。


老东家的一个同事已经升为主任,邀请我回去,工资开到了7500,在一个报社这样的单位我知道这是她的极限了。


W则是在我们三人组解散之后去了一家小公司,老板是个富二代,人也很好。现在业务增多想要叫我过去。


最终我选择了W的公司



  • 职位:美工?前端?

  • 技术:Ps Html5 Css3 JQ PHP

  • 商城类网站

  • 薪资:13000

  • 房租:0(买房了,朋友的顶账房,没贷款欠朋友的,慢慢还。)

  • 房价:8000/平


【2019】vue来了我却躺平了


这一年,老婆的公司开始使用VUE2了,老婆让我跟她一起学,我却沉迷游戏躺平了。


现在公司的环境很好,老板有钱人也很好。技术部的电脑都是i9-11代的处理器+2060的显卡,这哪里是公司简直是网吧。


公司也是经常几周甚至一个月没有新的业务,只要保证手里的几个网站正常运行就可以了,于是大家开始沉沦,每天上班也几乎都是在看电影和打游戏,还有免费的零食和饮料。


大家每天也都是在讨论哪个3A大作发售了,哪个电影上映了,很少能听到关于技术的讨论了。



  • 职位:美工?前端?

  • 技术:Ps Html5 Css3 JQ PHP

  • 商城类网站

  • 薪资:15000

  • 房价:9000/平


【2020】疫情来了,小天使来了,危机也在慢慢靠近。


这一年,疫情开始蔓延,小宝宝也降生了。


公司还是一如既往,虽然我们知道业务在下滑,但是老板有钱,工资不降反升。


老婆打算辞掉工作专心把孩子带到3岁再工作。我考量了一下我的收入,觉得在这个不知道多少线的小城也算可以,就同意了。



  • 职位:美工?前端?

  • 技术:Ps Html5 Css3 JQ PHP

  • 商城类网站

  • 薪资:17000

  • 房价:10000/平


【2021】终于开始学VUE和React


这一年,终于开始学习VUE和React,虽然公司依然没有使用新框架的打算,主要是因为后端不想写接口。想继续使用现在的程序。好像大家都躺平了,也可能是小城市的惬意。



  • 职位:美工?前端?

  • 技术:Ps Html5 Css3 JQ PHP

  • 商城类网站

  • 薪资:18000

  • 房价:9000/平


【2022-上】听说大厂裁员,我们老板“毕业”了。


这一年,听说大厂都在裁员,小城市却依然风平浪静。大家日复一日的摸鱼。


某一天,噩耗传来,老板进去了,公司解散。WTF。


晴天霹雳,没想到我们还没毕业,老板却“毕业”了



  • 职位:无

  • 薪资:0

  • 房价:9000/平


【2022-中】现实的社会和迟到的技术


2022精彩的一年,被狠狠的从舒适圈里踢了出来。一脸懵逼。


开始重新找工作却发现,自己的技术早已落伍,不得已又要从头学起。


无奈重新学了Vue2和Vue3,学了node.js,学了webpack,又学了ES6和TypeScript。


终于补完了前两年欠下的学习,却发现这个城市的前端工资普遍在6K-9K


落差感又一次让我一脸懵逼


【2022-下】焦虑和心虚


最终我进入了一家实体企业,HR看着我的简历对我十年的工作经验还算满意,我却很心虚。


总觉得自己这十年的工作经验水分很大。


看着比我年轻的同事熟练的使用着各种框架,而我却还在查着各种API。


我陷入了深深的自我怀疑,我是不是蹉跎了我的青春。



  • 职位:前端

  • 技术:Html5 Css3 Vue2

  • 内部平台系统、微信小程序

  • 薪资:9000

  • 房价:9000/平


【总结】



小城市没有那么多的技术需求,也没有那么多的996,大家朝九晚五,周末双休过得很惬意。



刚毕业那会用着Table切图用着Asp做后台,大家只知道你是做网站的,公司招人也是招美工,要求设计、切图、程序都会一些。小城市就是要求你什么都会一点,但不用很精通,够用就行。


慢慢的随着技术的发展前端才被定义出来,但是很多公司招聘的时候写着需要使用Vue或者React,但事实上还是干这设计切图的活,前后端不分家。


小城市技术渗透的慢,但是依然在渗透,四年的惬意生活,让我慢慢的忘记自己最初的梦想,惊醒的时候却发现我已经掉队了。


【未来可期】



被踢出舒适圈,也被踢醒了



想想大学时期的四个五年计划一个也没实现,还剩最后一个,或许还能抢救一下,虽然很不切实际,但是总有个盼头。


重新规划自己的未来,躺也躺过了,现在想想躺平的日子也就那样。虽然自己已经30+但是还是可以继续搏一搏的。


重新定制一个三年计划吧



  • 继续学习、阅读源码,做1-2个长期维护的项目。

  • 研究下混合基金,适量投入。

  • 看一些游戏开发相关的书,研究下Unreal引擎,争取做出一个游戏Demo。(游戏剧本已经写好了2个)。


作者:不知君
来源:juejin.cn/post/7143404185738805262
收起阅读 »

想退更多税?看完这篇就够了

前言 最近又到了一年一度的退税了,最近发了一个沸点,发现大家都是和我之前一样不知道如何能退更多税,我也是无意中和小伙伴聊的时候才发现这个,如何能退更多税取决于 专项附加扣除,这个合计值越多,退的也会更多,下面咱们来详细说说这个专项附加扣除,怎么能够让这个合计值...
继续阅读 »

前言


最近又到了一年一度的退税了,最近发了一个沸点,发现大家都是和我之前一样不知道如何能退更多税,我也是无意中和小伙伴聊的时候才发现这个,如何能退更多税取决于 专项附加扣除,这个合计值越多,退的也会更多,下面咱们来详细说说这个专项附加扣除,怎么能够让这个合计值变多。


image.png


专项附加扣除填报


首先打开个人所得税APP,点击下面的tabbar办税,然后点击专项附加扣除填报,就会出现如下图,通过填报上面的这些去进行增加专项附加扣除合计值。


0b957f5ecc7afa419053f890511d9aa.jpg


租房租金(和房贷只能二选一)



  • 要求:工作租房子了 (不需要合同也能填哦)

  • 扣除标准:省会或者直辖市 1500 / 月,二线城市 1100 /月,小城市 800 /月

  • 年总计:省会或者直辖市 18000,二线城市 13200,小城市 9600

  • 划重点: 多段租房经历可以多填几个租房,一定要填满!!!,一定要填,没租房子也可以填,随便填一个地址也行,这个不需要租房合同,也无法追查到的,但是和贷款只能二选一,具体选哪个请继续看。


image.png


住房贷款利息(和租房只能二选一)



  • 要求:首套房贷款

  • 扣除标准:1000/月

  • 年总计:12000

  • 划重点: 和租房二选一哦,具体选哪个看你所在的城市,哪个钱多选哪个~


image.png


子女教育



  • 要求:子女处于全日制学历教育阶段(幼儿园+小学+中学+大学),年龄>3岁。

  • 扣除标准:每人每月 1000 元 (两个孩子是一个月2000哦)

  • 年总计:12000(一个孩子的情况下)


image.png


继续教育



  • 要求:考证或者学历提升

  • 扣除标准:考证拿到证的当年一次性扣除:3600,学历提升 400/月

  • 年总计:考证:3600,学历提升:4800

  • 划重点: 这里的证必须是收录在 国家资格目录 的证书哦~


image.png


大病医疗


如果可以,我希望这个没有人申请,身体健康比什么都重要。



  • 要求:医保报销后个人花费15k+

  • 扣除标准:8w内根据你个人花费,花多少这个就是多少。

  • 年总计:根据实际。


image.png


赡养老人



  • 要求:父母 60 岁以上

  • 扣除标准:独生子女2000/月,非独生子女:和兄弟姐妹分摊 2000

  • 年总计:独生子女:24000,非独生子女: 0 ~ 12000


image.png


3岁以下婴幼儿照护



  • 要求:孩子 3 岁以下

  • 扣除标准:每月每人 1000 (两个孩子就是2000/月哦)

  • 年总计:12000(一个孩子的情况下)


如何能退更多的税


打开个人所得税APP → 我要查询 → 申报查询 → 已完成→ 选择当年的综合年度汇算→ 点击专项附加扣除合计,查看自己当年的专项附加扣除合计,可以简单计算下自己填报的专项附加扣除是否都已经填写。



PS:画重点!!!注意检查时间,我开始就是租房没填满,只写了一段租房经历,所以差点错过好几个亿,如果是2023年的填报,一定要注意你专项附加扣除是否涵盖整个2022年的日期。



113b48e415b80b8861261f3b3d961ab.jpg


有年终奖的情况


可能有些小伙伴公司会发年终奖 (如果和我一样没发年终奖,那这里可以跳过了,发年终奖的公司给我来一打),最好选择单独计税,不过都可以试一下,综合并入和单独合计,哪个退的多用哪个。


历史申报


以前的申报如果有没补充的,现在还是可以填哦,快去看看之前的申报,说不定会有惊喜哦。


最后


如果是需要补税的情况,如果少于400是不需要补的哦,当然如果大于400,一定要快点去缴纳哦,国家都留有档案,防止影响以后征信,哈哈哈,祝大家都能退几个亿~



作者:我亚索贼六丶
来源:juejin.cn/post/7208793045479522364
收起阅读 »

从技术到管理:我是如何教导技术总监做事的?

很多朋友都知道我技术很强,但实际上除了技术之外我的管理能力同样很出众。 虽然我年纪不大,但做了很多年实际的技术管理相关的工作。 前段时间李哥向我付费取经,他已经 30 多岁了,工作了很多年,技术也很强。但今年却刚当上公司的技术总监。原来他是技术部三把手,升职的...
继续阅读 »

很多朋友都知道我技术很强,但实际上除了技术之外我的管理能力同样很出众。


虽然我年纪不大,但做了很多年实际的技术管理相关的工作。


前段时间李哥向我付费取经,他已经 30 多岁了,工作了很多年,技术也很强。但今年却刚当上公司的技术总监。原来他是技术部三把手,升职的原因是前任技术总监带着副总监一起创业去了。


因为吸收了教训,老板就找到李哥,允诺给李哥一段时间的考察期,让李哥管理整个技术团队。通过考察期后会给李哥一些股份,让李哥成为技术合伙人。李哥在这家公司干了好几年了,技术能力有目共睹,确实不错,但管理经验确实比较缺乏。老板选择他,一个是出于信任,另一个也是想培养李哥。


现在李哥虽然管着几十号人,可他并没有多少实际管理经验。很多事情总觉得做得很吃力,力不从心。


所以我就和他分享了一些自己从过去的实际操作中获得的经验。


授权


第一点就是,怎么授权?


很多刚做技术管理者的人并不喜欢放权,李哥就是这类人。


这么做有几个原因。


第一个原因是怕下属能力强,功高盖主抢风头。


第二是原因是贪权,掌控欲强,什么事都要过问。


第三个原因是怕下属能力差,怕下属做砸了。


这样就会导致一个现象,很多人拿着很高的工资,担任很高的职位,却担负不了多少责任。


李哥公司每周二、周四是发布日。发布会由李哥主持,每次发布完都要等两三个小时的线上人工验证。发布完后,李哥还要担惊受怕,生怕线上出问题。


这种事不应该让李哥具体负责,因为即使哪个服务出现故障,李哥又不懂具体的业务,也做不了什么。


李哥手底下副总监、研发经理、组长十几号人,这事应该分派下去。


所有的事情都集中到了企业主要的几个负责人身上,很容易出事。


而底下的人很容易习惯这种作风,事事找领导,没有主见和主动性。


甚至于有一次李哥不在,负责的研发经理取消了发布日当天的发布计划,因为他怕发布出问题但责任。


权利高度集中,人人不作为的公司,一定会逐渐走向衰败。


所以,权利收得太紧,就会死。


那该怎么放权?


其实很简单。


员工职责内的事情要放,员工职责外的事情不要放。


就像一个员工请半天假去医院做核酸,先汇报组长,组长通过后汇报到研发经理,再汇报到技术副总监,再报备到人事专员,同时抄送人力总监和技术总监。


这么长的流程有什么用呢?


可以我直接让李哥把流程改成员工汇报给组长,组长同步给人事专员。


出事了,直接找组长,因为人是组长带的,组长要负责。


流成越复杂,就越容易扯皮甩锅撇清责任。最后背锅的估计就是李哥这个大领导。


发布的事情也是这样,谁负责的项目,什么时候发,发布过程可能出现的问题,发布失败的后果。都应该由他承担。


这样就能做到责任清晰、效率高、权利透明。


但这样不是没有隐患,组长可能不负责任,乱批假、乱报销等等。


所以权利放得太松,就容易乱。


那如何找到合适的、对工作负责的人担任组长呢?这又是一个新的问题。我后面再讲。


激励


第二点是,怎么激励?


很多新晋管理人很容易忽略激励这一点,其实它非常重要。


团队人气的高低会直接影响工作效率和工作质量。


给大家激励,让大家朝着一个方向努力,实现共同的目标很有意义和价值。虽然它不是很明显的投入,但隐形利益真的非常巨大。


那该如何激励呢?很简单,给员工争取自己能力范围内最大的福利。


简单粗暴的说,就是奖钱。


项目提前结束,给公司节省资源。


客户满意,按时付款,没有拖欠款项。


客户非常满意,计划签订下一个合作项目的合同。


这些都是给公司赚钱。


而这些赚到的钱再拿出一部分奖励给员工,本来就合情合理。


同时还可以形成良性循环。


但也不是每个老板都愿意付出这一部分钱。


有些情况下,团队加班加点、辛苦付出完成了项目。但老板就是不愿意给大家一些奖励怎么办?


精神奖励,虽然画饼差不多,但有效。


就像学生的奖状、小红花、三好学生、优秀标兵。


这是一种荣誉。


组织部门举行一场表彰会,定制个优秀员工奖牌、奖状。也花不了多少钱。


被表扬的人当真就当真,不当真也没什么。


这也谈不上忽悠人。做得好,被表扬,被夸奖,本来就是正常的事。


但是这种激励有时也能有意想不到的效果。


奖罚分明


第三点是,怎么奖罚分明?


激励对应的就是惩罚。


哪些事该奖?哪些事不该奖?


哪些事该罚?哪些事不该罚?


分不清楚这些事就很容易出事。


很多年前刚做管理不太懂事,因为迟到处罚问题失去了一个非常优秀的下属。


技术部门的工作职责其实很明确,他是生产型的岗位,按时保质保量完成任务就可以,很多事情都没必要管理的那么死。


比如李哥公司的制度是九七五,但前技术总监给大家培养了一个潜规则。就是九九五,即使很多人早就完成了工作也不能回家,不然第二个月指定被扣绩效。


按照公司规定,加班超过三个小时才有加班费。但这多出来的两个小时就等于白干了。


而且很多人这两个小时并不是真的在干活,而是在摸鱼、刷短视频,或者楼下抽烟。


所以我让李哥和老板商量,公司改成弹性制度。


按工作产出计算绩效,而不是工时。


李哥公司有个运行多年的老系统,负责处理订单和支付的。但由于年久失修,没人愿意去改。因为稍有不慎就可能导致公司金钱受损。


所以我就告诉李哥,这事必须得有人干。而且只奖不罚。


搞砸了也不能罚,甚至还得发些安慰奖。


为什么这么做?


因为这是个烂摊子,出力不讨好的事。安排人去做,不奖就等于罚了。


干好了没奖励,干不好还得被罚。谁愿意干呢?


难用就难用些,出了问题用脚本修复下数据。


这也是这个系统这么些年一直勉勉强强运行,而无人敢修改的原因。


难的事,没人敢做的事,一定不能拖着。付出大代价也得完成。


另外,李哥公司里面工时最长的几个员工会得到劳模奖和全勤奖。


但这里面有人比较混,仗着家里公司近,下班不打卡,回家吃个饭,溜个弯,到十一点多再来公司打个下班卡。而且这货不经常这么干,按照公司 OA 系统的工时,正好把自己工时卡在前三名。


影响很不好,但又不好说他。


我让李哥直接让技术部门改变奖励方式,还是按照绩效和产出发奖,同时对 Bug 最多、产出最低的员工进行处罚,即使他工时最长。


因为正常上下班只是员工的分内之事。


这种事情不该奖励。


总结来说,就是分内的事、简单的事,不能奖、只能罚。分外的事、难的事,只能奖、不能罚。


篇幅有限,先记录以上三点。


希望对你有所帮助。


作者:代码与野兽
来源:juejin.cn/post/7209914417641996343

未完待续。


收起阅读 »

Android 获取IP和UA

最近接入了一个新的SDK,初始化接口需要传入当前设备的IP和UA作为参数。本文介绍如何获取设备的IP和UA。 获取IP 使用WIFI联网与不使用WIFI,获取到的IP地址不同。因此,需要先判断当前设备通过哪种方式联网,然后再获取对应的IP地址。 判断网络连接...
继续阅读 »

最近接入了一个新的SDK,初始化接口需要传入当前设备的IP和UA作为参数。本文介绍如何获取设备的IP和UA。


获取IP


使用WIFI联网与不使用WIFI,获取到的IP地址不同。因此,需要先判断当前设备通过哪种方式联网,然后再获取对应的IP地址。



  • 判断网络连接类型


通过ConnectivityManager判断网络连接类型,代码如下:


private fun checkCurrentNetworkType() {
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
connectivityManager.run {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
when (activeNetworkInfo?.type) {
ConnectivityManager.TYPE_MOBILE -> {
// 通过手机流量
}
ConnectivityManager.TYPE_WIFI -> {
// 通过WIFI
}
else -> {}
}
} else {
// Android M 以上建议使用getNetworkCapabilities API
activeNetwork?.let { network ->
getNetworkCapabilities(network)?.let { networkCapabilities ->
if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)) {
when {
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> {
// 通过手机流量
}
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> {
// 通过WIFI
}
}
}
}
}
}
}
}


  • 获取手机卡联网 IP


通过NetworkInterface获取IPV4地址,代码如下:


NetworkInterface.getNetworkInterfaces().let {
loo@ for (networkInterface in Collections.list(it)) {
for (inetAddresses in Collections.list(networkInterface.inetAddresses)) {
if (!inetAddresses.isLoopbackAddress && !inetAddresses.isLinkLocalAddress) {
// IP地址
val mobileIp = inetAddresses.hostAddress
break@loo
}
}
}
}


  • 获取WIFI联网 IP


通过ConnectivityManagerWifiManager来获取IP地址,代码如下:


private fun getWIFIIp() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
val wifiManager = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
// IP 地址
val wifiIp = Formatter.formatIpAddress(wifiManager.connectionInfo.ipAddress)
} else {
// Android Q 以上建议使用getNetworkCapabilities API
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
connectivityManager.run {
activeNetwork?.let { network ->
(getNetworkCapabilities(network)?.transportInfo as? WifiInfo)?.let { wifiInfo ->
// IP 地址
val wifiIp = Formatter.formatIpAddress(wifiInfo.ipAddress)
}
}
}
}
}

获取UA


获取设备的UserAgent比较简单,代码如下:


// 系统 UA
System.getProperty("http.agent")

// WebView UA
WebSettings.getDefaultUserAgent(context)

示例


在示例Demo中添加了相关的演示代码。


ExampleDemo github


ExampleDemo gitee


效果如图:


device-2023-03-12-09 -original-original.gif
作者:ChenYhong
来源:juejin.cn/post/7209272192852148282
收起阅读 »

玩转Canvas——给坤坤变个颜色

web
Canvas可以绘制出强大的效果,让我们给坤坤换个色。 先看看效果图: 要怎么实现这样一个可以点击换色的效果呢? 话不多说,入正题。 第一步,创建基本元素,无须多言: <body> <canvas></canvas&...
继续阅读 »

Canvas可以绘制出强大的效果,让我们给坤坤换个色。


先看看效果图:





要怎么实现这样一个可以点击换色的效果呢?
话不多说,入正题。


第一步,创建基本元素,无须多言:


<body>
<canvas></canvas>
</body>

我们先加载坤坤的图片,然后给canvas添加基础事件:


const cvs = document.querySelector('canvas');
const ctx = cvs.getContext('2d', {
willReadFrequently: true
});
//加载图片并绘制
const img = new Image();
img.src = './img/cxk.png';
img.onload = () => {
cvs.width = img.width;
cvs.height = img.height;
ctx.drawImage(img, 0, 0);
};

再给canvas注册一个点击事件:


//监听canvas点击
cvs.addEventListener('click', clickCb);

function clickCb(e) {
const x = e.offsetX;
const y = e.offsetY;
}

这样就拿到了点击的坐标,接下来的问题是,我们要如何拿到点击坐标的颜色值呢?


其实,canvas早就给我们提供了一个强大的api:getImageData


我们可以通过它获取整个canvas上每个像素点的颜色信息,一个像素点对应四个值,分别为rgba


ctx.getImageData返回数据结构如下:





所以我们便可以利用它拿到点击坐标的颜色值:


function clickCb(e) {
//省略之前代码
//...

const imgData = ctx.getImageData(0, 0, cvs.width, cvs.height);
//获取点击坐标的rgba信息
const clickRgba = getColor(x, y, imgData.data);
}

//通过坐标获取rgba数组
function getColor(x, y, data) {
const i = getIndex(x, y);
return [data[i], data[i + 1], data[i + 2], data[i + 3]];
}

//通过坐标x,y获取对应imgData数组的索引
function getIndex(x, y) {
return (y * cvs.width + x) * 4;
}

接下来便是在点击处绘制我们的颜色值:


//为了方便,这里将变色值写死为原谅绿
const colorRgba = [0, 255, 0, 255];

function clickCb(e) {
//省略之前代码
//...

//坐标变色
function changeColor(x, y, imgData) {
imgData.data.set(colorRgba, getIndex(x, y));
ctx.putImageData(imgData, 0, 0);
}
changeColor(x, y, imgData);
}

此时如果点击坤坤的头发,会发现头发上仅仅带一点绿。要如何才能绿得彻底呢?


我们新增一个判断rgba值变化幅度的方法getDeff,当两者颜色相差过大,则视为不同区域。


//简单地根据绝对值之和判断是否为同颜色区域
function getDiff(rgba1, rgba2) {
return (
Math.abs(rgba2[0] - rgba1[0]) +
Math.abs(rgba2[1] - rgba1[1]) +
Math.abs(rgba2[2] - rgba1[2]) +
Math.abs(rgba2[3] - rgba1[3])
);
}

再新增一个判断坐标是否需要变色的方法:


function clickCb(e) {
//省略之前代码
//...

//判断该坐标是否无需变色
function stopChange(x, y, imgData) {
if (x < 0 || y < 0 || x > cvs.width || y > cvs.height) {
//超出canvas边界
return true
}
const rgba = getColor(x, y, imgData.data);
if (getDiff(rgba, clickRgba) >= 100) {
//色值差距过大
return true;
}
if (getDiff(rgba, colorRgba) === 0) {
//同颜色,不用改
return true;
}
}
}

我们更改changeColor方法,接下来便可以绿得彻底了:


function clickCb(e) {
//省略之前代码
//...

//坐标变色
function changeColor(x, y, imgData) {
if (stopChange(x, y, imgData)) {
return
}
imgData.data.set(colorRgba, getIndex(x, y));
ctx.putImageData(imgData, 0, 0);
//递归变色
changeColor(x - 1, y, imgData);
changeColor(x + 1, y, imgData);
changeColor(x, y + 1, imgData);
changeColor(x, y - 1, imgData);
}

//省略之前代码
//...
}

效果已然实现。但是上面通过递归调用函数去变色,如果变色区域过大,可能会导致栈溢出报错。


为了解决这个问题,我们得改用循环实现了。


这一步的实现需要一定的想象力。读者可以自己试试,看看能不能改用循环方式实现出来。


鉴于循环实现的代码略多,这里不再解释,直接上最终代码:


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
.color-box {
margin-bottom: 20px;
}
.canvas-box {
text-align: center;
}
</style>
</head>
<body>
<div class="color-box">设置色值: <input type="color" /></div>
<div class="canvas-box">
<canvas></canvas>
</div>
<script>
let color = '#00ff00';
let colorRgba = getRGBAColor();
//hex转rgba数组
function getRGBAColor() {
const rgb = [color.slice(1, 3), color.slice(3, 5), color.slice(5)].map((item) =>
parseInt(item, 16)
);
return [...rgb, 255];
}

const input = document.querySelector('input[type=color]');
input.value = color;
input.addEventListener('change', function (e) {
color = e.target.value;
colorRgba = getRGBAColor();
});

const cvs = document.querySelector('canvas');
const ctx = cvs.getContext('2d', {
willReadFrequently: true,
});
cvs.addEventListener('click', clickCb);

const img = new Image();
img.src = './img/cxk.png';
img.onload = () => {
cvs.width = 240;
cvs.height = (cvs.width * img.height) / img.width;
//图片缩放
ctx.drawImage(img, 0, 0, img.width, img.height, 0, 0, cvs.width, cvs.height);
};

function clickCb(e) {
let x = e.offsetX;
let y = e.offsetY;
const pointMark = {};

const imgData = ctx.getImageData(0, 0, cvs.width, cvs.height);
const clickRgba = getColor(x, y, imgData.data);
//坐标变色
function changeColor(x, y, imgData) {
imgData.data.set(colorRgba, getIndex(x, y));
ctx.putImageData(imgData, 0, 0);
markChange(x, y);
}

//判断该坐标是否无需变色
function stopChange(x, y, imgData) {
const rgba = getColor(x, y, imgData.data);
if (getDiff(rgba, clickRgba) >= 150) {
//色值差距过大
return true;
}
if (getDiff(rgba, colorRgba) === 0) {
//同颜色,不用改
return true;
}
if (hasChange(x, y)) {
//已变色
return true;
}
}
function hasChange(x, y) {
const pointKey = `${x}-${y}`;
return pointMark[pointKey];
}
function markChange(x, y) {
const pointKey = `${x}-${y}`;
pointMark[pointKey] = true;
}
//添加上下左右方向的点到等待变色的点数组
function addSurroundingPoint(x, y) {
if (y > 0) {
addWaitPoint(`${x}-${y - 1}`);
}
if (y < cvs.height - 1) {
addWaitPoint(`${x}-${y + 1}`);
}
if (x > 0) {
addWaitPoint(`${x - 1}-${y}`);
}
if (x < cvs.width - 1) {
addWaitPoint(`${x + 1}-${y}`);
}
}
function addWaitPoint(key) {
waitPoint[key] = true;
}
function deleteWaitPoint(key) {
delete waitPoint[key];
}
//本轮等待变色的点
const waitPoint = {
[`${x}-${y}`]: true,
};
while (Object.keys(waitPoint).length) {
const pointList = Object.keys(waitPoint);
for (let i = 0; i < pointList.length; i++) {
const key = pointList[i];
const list = key.split('-');
const x1 = +list[0];
const y1 = +list[1];

if (stopChange(x1, y1, imgData)) {
deleteWaitPoint(key);
continue;
}
changeColor(x1, y1, imgData);
deleteWaitPoint(key);
addSurroundingPoint(x1, y1);
}
}
}

//通过坐标x,y获取对应imgData数组的索引
function getIndex(x, y) {
return (y * cvs.width + x) * 4;
}

//通过坐标获取rgba数组
function getColor(x, y, data) {
const i = getIndex(x, y);
return [data[i], data[i + 1], data[i + 2], data[i + 3]];
}

////简单地根据绝对值之和判断是否为同颜色区域
function getDiff(rgba1, rgba2) {
return (
Math.abs(rgba2[0] - rgba1[0]) +
Math.abs(rgba2[1] - rgba1[1]) +
Math.abs(rgba2[2] - rgba1[2]) +
Math.abs(rgba2[3] - rgba1[3])
);
}
</script>
</body>
</html>

若有疑问,欢迎评论区讨论。


完整demo地址:canvas-change-color


作者:TRIS
来源:juejin.cn/post/7209226686372937789
收起阅读 »

自从学会这几个写代码的技巧后,摸鱼时间又长了!!!

web
嘿!👋 今天,我们将介绍 5 个 JavaScript 自定义的实用函数,它们可以在您的大多数项目中派上用场。 目录 01.console.log() 02.querySelector() 03.addEventListener() 04.random() ...
继续阅读 »

嘿!👋


今天,我们将介绍 5 个 JavaScript 自定义的实用函数,它们可以在您的大多数项目中派上用场。


目录



  • 01.console.log()

  • 02.querySelector()

  • 03.addEventListener()

  • 04.random()

  • 05.times()




01.console.log()


在项目的调试阶段,我们都很喜欢用console.log()来打印我们的值、来进行调试。那么,我们可不可以缩短它,以减少我们的拼写方式,并且节省一些时间呢?


//把`log`从`console.log`中解构出来
const { log } = console;

//log等同于console.log

log("Hello world!");
// 输出: Hello world!

// 等同于 //

console.log("Hello world!");
// 输出: Hello world!

说明:


我们使用解构赋值logconsole.log中解构出来




02.querySelector()


在使用 JavaScript 时,当我们对DOM进行操作时经常会使用querySelector()来获取DOM元素,原生的获取DOM的操作过程写起来又长阅读性又差,那我们能不能让他使用起来更加简单,代码看起来更加简洁呢?


//把获取DOM元素的操作封装成一个函数
const select = (selector, scope = document) => {
return scope.querySelector(selector);
};

//使用
const title = select("h1");
const className = select(".class");
const message = select("#message", formElem);

// 等同于 //

const title = document.querySelector("h1");
const className = document.querySelector(".class");
const message = formElem.querySelector("#message");

说明:


我们在select()函数需要接收 2 个参数:



  • 第一个:您要选择的DOM元素

  • 第二:您访问该元素的范围(默认 = document);




03.addEventListener()


对click、mousemove等事件的处理大多是通过addEventListener()方法实现的。原生实现的方法写起来又长阅读性又差,那我们能不能让他使用起来更加简单,代码看起来更加简洁呢?


const listen = (target, event, callback, ...options) => {
return target.addEventListener(event, callback, ...options);
};

//监听buttonElem元素点击事件,点击按钮打印Clicked!
listen(buttonElem, "click", () => console.log("Clicked!"));

//监听document的鼠标移上事件,当鼠标移动到document上时打印Mouse over!
listen(document, "mouseover", () => console.log("Mouse over!"));

//监听formElem上的submit事件,当提交时打印Form submitted!
listen(formElem, "submit", () => {
console.log("Form submitted!");
}, { once: true }
);

说明:


我们在listen()函数需要接收 4 个参数:



  • 第一个:你要定位的元素(例如“窗口”、“文档”或特定的 DOM 元素)

  • 第二:事件类型(例如“点击”、“提交”、“DOMContentLoaded”等)

  • 第三:回调函数

  • 第 4 个:剩余的可选选项(例如“捕获”、“一次”等)。此外,如有必要,我们使用传播语法来允许其他选项。否则,它可以像在addEventListener方法中一样被省略。




04. random()


你可能知道Math.random()是可以随机生成从 0 到 1 的函数。例如Math.random() * 10,它可以生成从 0 到 10 的随机数。但是,问题是尽管我们知道生成数字的范围,我们还是无法控制随机数的具体范围的。那我们要怎么样才能控制我们生成随机数的具体范围呢?


const random = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
};

random(5, 10);
// 5/6/7/8/9/10

这个例子返回了一个在指定值之间的随机数。这个值大于于 min(有可能等于),并且小于(有可能等于)max




05. times()


有时,我们经常会有运行某个特定功能或者函数的需求。应对这种需求,我们可以使用setInterval()每间隔一段时间运行一次:


setInterval(() => {
randomFunction();
}, 5000); // runs every 5 seconds

但是问题是setInterval()是无限循环的,当我们有限定运行次数要求时,我们无法指定它要运行它的次数。所以,让我们来解决它!


const times = (func, n) => {
Array.from(Array(n)).forEach(() => {
func();
});
};

times(() => {
randomFunction();
}, 3); // runs 3 times

解释:



  • Array(n)- 创建一个长度为n的数组.


Array(5); // => [,,]


  • Array.from()- 从创建一个浅拷贝的Array(n)数组。它可以帮助对数组进行操作,并用“undefined”填充数组里面的值。您也可以使用Array.prototype.fill()方法获得相同的结果。


Array.from(Array(3)); // => [undefined,undefined,undefined]


注意: 在封装这个函数时,我发现到有些程序员更喜欢先传参数n,再传函数times(n, func)。但是,我觉得有点奇怪,所以我决定交换它们的位置,从而使语法更类似于函数setInterval()



setInterval(func, delay);

times(func, n);

此外,您还可以可以使用setTimes()来代替times()。来代替setInterval()``setTimeout()的功能




写在最后


伙伴们,如果你觉得我写的文章对你有帮助就给zayyo点一个赞👍或者关注➕都是对我最大的支持。当然你也可以加我微信:IsZhangjianhao,邀你进我的前端学习交流群,一起学习前端,成为更优秀的工程

作者:zayyo
来源:juejin.cn/post/7209861267715817509
师~


收起阅读 »