返回站点目录

归零幻想

https://ntutn.top/

归零幻想的博客,编程和其他。

综合 Android编程

状态

状态正常

分析当前仓库下的提交者代码占比

2026/02/01 18:48 · 归零幻想

归零幻想 https://ntutn.top/posts/%E5%88%86%E6%9E%90%E5%BD%93%E5%89%8D%E4%BB%93%E5%BA%93%E4%B8%8B%E7%9A%84%E6%8F%90%E4%BA%A4%E8%80%85%E4%BB%A3%E7%A0%81%E5%8D%A0%E6%AF%94/ - AI工具: 其实我更喜欢Trae,但是它还没有出Linux版本。 提示词: 帮我实现一段python脚本,对于当前仓库下所有文本文件,如果该文件被git跟踪: 统计该文件行数 对该文件执行git blame,统计当前版本每个用户的行数 对每一个文件实施上述操作,并在控制台持续更新结果 汇总统计结果,生成一个html格式的报表,把该文件的地址输出到控制台 自动打开生成的报表文件 一次性完美地生成了我预期的结果。 结果: - https://ntutn.top/posts/%E5%88%86%E6%9E%90%E5%BD%93%E5%89%8D%E4%BB%93%E5%BA%93%E4%B8%8B%E7%9A%84%E6%8F%90%E4%BA%A4%E8%80%85%E4%BB%A3%E7%A0%81%E5%8D%A0%E6%AF%94/ - CC BY-SA 4.0

业务收获总结

2026/01/01 14:17 · 归零幻想

归零幻想 https://ntutn.top/posts/%E4%B8%9A%E5%8A%A1%E6%94%B6%E8%8E%B7%E6%80%BB%E7%BB%93/ - 之前业务的工作已经告一段落,两年多的搬砖已经占据了职业生涯的大半时间,这里简单整理下收获。 技术积累 安卓基础业务需求开发 对于一般的业务需求,写UI发网络请求存储和管理数据等,能很好地完成。 安卓绘制系统 熟悉安卓 View 系统,能通过自定义View实现中等复杂业务需求。熟悉安卓Canvas API,能实现复杂的动画和绘制效果。 对 SurfaceView 、 TextureView 有初步了解。 协程和Flow 熟练使用Kotlin协程和Flow API进行业务需求开发,了解协程的底层原理。 Compose基础 了解基本的Compose View开发方式。 业务理解 重要数据指标 LT(Lifetime) 用户生命周期,一个用户从安装到卸载的时间跨度。 SD(StayDuration) 用户停留时长,如从打开APP到退出的时间。 功能渗透 用户使用了产品某个功能的比例。 内容生产 用户在产品内产出内容的行为和规模。 内容消费 用户在产品内浏览、观看、阅读内容的规模。 从数据指标变化的表象可以找到背后的深层原因,即分析“影响链路”,以决策后续功能是否能全量。例如,某功能使得优质内容展现增加,带动用户点赞发布增加,如果相关数据变化都符合预期,那么这个链路就是比较置信的。 弹幕系统设计 配置读写 弹幕开关是否开启、弹幕展示行数、字号大小等信息需要保存到本地,且同步到服务端。考虑诸多问题后,配置模块可能相当复杂,如不同场景配置是否同步,如配置的存储值、生效值、展示值之间的映射等。 弹幕数据源 考虑到用户经常只看了几秒就切换到了其他视频,数据源需要支持随播放进度分片加载。 弹幕时间轴 弹幕需要有自己的时间轴和计时逻辑,因为播放器的时间回调是不够精确的。这里使用监听系统VSYNC的方式来实现高精度计时。 弹幕上屏 在数据源中选择一批弹幕上屏。对于每个时刻,将已经到展示时间的弹幕加入一个优先队列,从队列中选择不发生碰撞的(根据速度做碰撞检测) 弹幕排版 对于一条弹幕来说,上屏时间、速度、起始位置可以唯一确定该弹幕要做的动画,这些称为排版数据。选择一批要上屏弹幕并计算他们的排版数据的过程称为排版。排版可以是实时的,每帧做计算,也可以是批量的,一次性计算出接下来一段时间的。该计算依赖于弹幕测量,批量异步排版可以得到一定的性能优化。 空场景 屏幕上没有弹幕在滚动的时间范围称为空场景。空场景下停止弹幕绘制、排版计算,乃至VSYNC监听可以得到很大的性能优化,因为绝大多数视频时间都是没有弹幕的。 弹幕绘制 文本部分需要两次绘制才能画出描边。业界有直接在Canvas上绘制和使用系统View两种方案,各有优劣(前者理论性能天花板更高,后者开发特殊样式方便)。 工作做事方式 业务需求:按时交付。 在业务需求之外,要自己学会发现技术优化项,主动去提出优化方案,寻找资源,完成开发和上线。 完成开发仅仅是完成一半的工作,如果后续数据存在问题,还需要高优先级去处理,寻找解决方案。 只有成功上线的需求才是收益,否则是只有人力投入不见回报的负担。 额外收获 找到了对象。 - https://ntutn.top/posts/%E4%B8%9A%E5%8A%A1%E6%94%B6%E8%8E%B7%E6%80%BB%E7%BB%93/ - CC BY-SA 4.0

归零幻想 https://ntutn.top/posts/ai%E4%BA%A4%E4%BA%92%E5%8D%8F%E8%AE%AE%E6%96%B9%E6%A1%88/ - 近日, 发布,引起行业地震,定价3499的工程样机一天卖光,合作的中兴公司的股份也大幅上涨。许多人在社交媒体发布手机助手帮自己点外卖、回消息甚至玩游戏刷抖音极速版赚钱的视频。然而,接踵而来的 (消息来源于网络),给这份胜利埋下了一片阴霾。 应用为什么不愿意被手机助手操作? 隐私和安全性 AI手机助手的 高权限可以收集到太多用户隐私 。从应用的视角来看,以微信举例,手机助手打开微信后可以看到所有好友关系,好友的昵称备注聊天习惯等,而这些对于微信来说是关乎命脉的。尽管豆包手机助手发布了 ,这仍不足以打消开发者的担忧。 其次,尽管得到了用户的授权, AI能否代表真实用户 也是存疑的。假如用户说“帮我回一下消息”,然后手机助手因为对方的言语诱导说出“欠我的钱不用还了”,谁可以承担用户的损失? 商业利益受损 在一个典型的电商比价场景,豆包 跳过了各种广告弹窗的干扰 找到用户想要的商品完成下单,方便了用户,但是这样一来 商家的这几个广告转化率为0 。 用户的时间和注意力是能换钱的,而豆包 在后台完成了一切 ,这些应用不再接触真实用户,除了直接的广告收益损失,还有潜在的玩法互动空间和未来发展能力的受限。 AI生态的未来在哪里? 技术对抗 理论上,豆包手机助手拥有系统级的权限,要和其他应用做技术对抗是不虚的,不过是把爬虫和网站开发者打了那么多年的战争延伸到手机上而已。 然而,除了应用和手机助手,这里还有个第三方, 监管会怎么看待这场博弈? 在手机领域监管下场明显比PC更积极,此前“李跳跳”类的跳广告应用跳过应用广告,是会被判定不正当竞争的,豆包手机助手其实在做类似的事情,只是字节体量更大,且当前行业还处于探索阶段,目前才没有很大动作。 商业合作 直接和应用开发者谈判,拿到授权后访问是个可行且稳妥的方案,其他手机助手也在做,但市场上的应用何其多,只能白名单范围访问对于豆包这种通用GUI Agent方案可以说是废掉了手脚。 AI交互协议 参考网站开发中的 robots.txt 。现在AI如何与应用交互 行业没有标准,正适合我们现在建立标准。 我们可以开源一个SDK给AI助手提供额外信息,保证开发者可以一行代码接入。 AI操作许可与AI操作声明 AI手机助手对应用的操作大多通过无障碍服务完成,而安卓无障碍服务 ,无需修改系统底层。 AI操作许可 应用在自己的组件上设置,表示当前组件是否允许被AI操作,有枚举: 允许AI操作,如常规的信息流 需要用户二次确认,如付款按钮 禁止AI操作,如竞技类游戏 AI操作声明 AI手机助手附加在自己发出的操作事件中,让应用识别到当前是一次AI操作,以便 给发出的消息加“来自AI手机助手”声明 拒绝一些不守规矩的自动操作 对于良好标注,甚至提供MCP服务给助手直接访问的应用,可以给予“AI友好应用”认证,在应用商店得到推荐。 AI操作计费 从目前情况看GUI Agent显然是个正确的方向,但站到所有开发者的对立面是不可取的。对AI操作计费是个可行的思路。 AI领域大模型资源消耗通常用token数计算,当AI操作某个外部应用时,每次操作都视作消耗一定的虚拟token(可以由应用自己定价,放到上述协议中),后续结算时将这部分钱付给开发者。购机可赠送一定数量的token,后续需要用户自己充值购买。助手也可以向用户展示各个应用消耗token情况,这样因为收费招致的负面评价也可以转嫁出去。 对于定价异常的情况,如一次按钮点击收费1元,也可以向用户确认预算消耗情况。 该方案也为中小应用开发者多一条广告、订阅以外的变现渠道,可能得到他们的支持。 - https://ntutn.top/posts/ai%E4%BA%A4%E4%BA%92%E5%8D%8F%E8%AE%AE%E6%96%B9%E6%A1%88/ - CC BY-SA 4.0

在Compose Desktop实现简单消息通知

2025/08/03 09:16 · 归零幻想

归零幻想 https://ntutn.top/posts/%E5%9C%A8compose_desktop%E5%AE%9E%E7%8E%B0%E7%AE%80%E5%8D%95%E6%B6%88%E6%81%AF%E9%80%9A%E7%9F%A5/ - 对于手机用户来说, 是一个司空见惯的东西,它在一个小窗口中显示简短的消息反馈。 然而这是一个手机端独有的东西,在桌面端我们怎么显示类似的提示呢? 与Toast提示关联的概念 Toast本身是用来展示一个简短的消息提示,然而可以用来展示消息提示的组件却有很多。他们与Toast有哪些联系和区别? MessageBox MessageBox是电脑端最常见的消息对话框,它通常是 打断的 ,甚至 模态的 。通常在向用户通知或询问重要信息时才会使用,例如,当用户尝试在没有保存文件时尝试直接退出应用询问用户是否保存。 ' 创建消息框,显示问题并提供"是"和"否"两个选项 result = MsgBox("你想知道生命的意义吗,你想真正地活着吗?", vbYesNo, "人生选择") ' 根据用户的选择执行相应操作 If result = vbYes Then ' 用户选择"是",打开《无限恐怖》 CreateObject("WScript.Shell").Run "https://www.qidian.com/book/109222/" Else ' 用户选择"否",打开《无限曙光》 CreateObject("WScript.Shell").Run "https://www.qidian.com/book/3347854/" End If 这种标准消息提示实现简单,因为很多编程语言中都内置了,在Win32 API中也直接提供,所以在桌面场景使用相当广泛。 Compose中也提供类似的对话框组件,美观且与界面风格统一,但是 。 Notification 是展示在用户界面之外的消息,用来给用户提供实时消息提醒,如来自其他用户的 消息、新闻、广告和优惠信息 等。 在桌面操作系统上也有类似的 概念。 相较于Toast, 通知脱离了当前应用 ,可以向用户传达一些实时消息或重要状态,但显然更重一些。 气泡提示 在Windows XP时代,在托盘区显示一个icon看上去很高逼格,而在icon上弹个通知就更厉害了。因为Windows早年这个通知是个气泡的形式,因此又称为气泡提示。 短暂出现,自动消失,这是与我们要的Toast概念最接近的东西了,某种意义上来说它是Windows通知中心的前身。然而感觉它更适合后台运行的应用刷存在感, 用户本来在窗口里操作,在托盘区弹出提示反而会转移注意力 。 Tooltip Tooltip是桌面应用常见的交互元素,当用户鼠标在按钮或其他可交互组件上 悬停 时,向用户提示当前操作的相关信息。 状态栏 状态栏是父窗口底部的水平窗口,应用程序在这里可以显示各种状态信息。 状态栏能直接向用户提供可能关心的状态信息,如文档的字数,当前的操作处理进度等。它能给用户提供很多的信息增量,且 信息实时更新 的。对于短暂的提示就不太合适了。 封装一个Toast工具 桌面端的交互设计很多,但我感觉没有一个可以替代Toast的使用场景,我还是需要一个轻量、自动消失、不打断用户的提示方式。然而Compose显然没有提供这样的标准API,那么我们只好尝试封装一个。 实现界面外观 实现Toast的界面外观是不复杂的,就是搞一段文本显示到界面的下方嘛。 Box( modifier = Modifier .fillMaxSize(), contentAlignment = Alignment .BottomCenter ) { val messages = remember { mutableStateListOf<ToastMessage>() } val scope = rememberCoroutineScope() App(onShowToast = { message -> scope.launch { messages.add(message) delay(message.duration) messages.remove(message) } }) messages.toList().forEachIndexed { index, message -> Surface( shape = MaterialTheme .shapes.medium, color = Color . Black .copy(alpha = 0.7f ), tonalElevation = 16. dp, shadowElevation = 16. dp, modifier = Modifier .padding(bottom = 24. dp + 48. dp * index) ) { Text( text = message.text, color = Color .White, style = MaterialTheme .typography.bodySmall, modifier = Modifier .padding(horizontal = 16. dp, vertical = 8. dp) ) } } } 界面展示没有问题,但是这段代码实用度为0。因为真实的业务场景中我们不可能在 App() 方法中直接放置我们的业务代码,肯定嵌套了无数层,要在点击按钮的位置把事件一路透传到根层级,想想都觉得头皮发麻。 CompositionLocal封装 为了支持无需将颜色作为显式参数依赖项传递给大多数可组合项,Compose 提供了 CompositionLocal,可让您创建以树为作用域的具名对象,这可以用作让数据流经界面树的一种隐式方式。 Compose中提供了 ,以便略过中间层,直接将参数传递到使用处。Compose中也大量使用这种方式,传递类似颜色、样式之类的数据。 首先定义个操作接口 interface IToastController { fun show (text: String, duration: Long = 3000 ) fun getMessages (): List<ToastMessage> } 定义CompositionLocal val LocalToastController = staticCompositionLocalOf<IToastController> { error( "No ToastController Provided. Make sure to wrap your app with ToastHost." ) } 封装一个ToastHost用来提供LocalToastController @Composable fun ToastHost (content: @Composable () -> Unit) { val coroutineScope = rememberCoroutineScope() val controller = remember(coroutineScope) { DefaultToastController(coroutineScope) } CompositionLocalProvider(LocalToastController provides controller) { content() val messages by remember { derivedStateOf { controller.getMessages() }} Box( modifier = Modifier .fillMaxSize(), contentAlignment = Alignment .BottomCenter ) { messages.forEachIndexed { index, message -> Surface( shape = MaterialTheme .shapes.medium, color = Color . Black .copy(alpha = 0.7f ), tonalElevation = 16. dp, shadowElevation = 16. dp, modifier = Modifier .padding(bottom = 24. dp + 48. dp * index) ) { Text( text = message.text, color = Color .White, style = MaterialTheme .typography.bodySmall, modifier = Modifier .padding(horizontal = 16. dp, vertical = 8. dp) ) } } } } } 接下来只要把这个ToastHost包装到App()方法的外面,就可以在任意层级弹出Toast了。 // main.kt ToastHost { App() } // 使用样例 @Composable fun DemoUsage (modifier: Modifier) { val toastController = LocalToastController .current Button(onClick = { toastController.show( "Hello World!" ) }) { Text( "show toast" ) } } 完整代码我贴到了 - https://ntutn.top/posts/%E5%9C%A8compose_desktop%E5%AE%9E%E7%8E%B0%E7%AE%80%E5%8D%95%E6%B6%88%E6%81%AF%E9%80%9A%E7%9F%A5/ - CC BY-SA 4.0

使用零宽空格绕过安卓包名访问限制

2025/07/25 17:23 · 归零幻想

归零幻想 https://ntutn.top/posts/%E4%BD%BF%E7%94%A8%E9%9B%B6%E5%AE%BD%E7%A9%BA%E6%A0%BC%E7%BB%95%E8%BF%87%E5%AE%89%E5%8D%93%E5%8C%85%E5%90%8D%E8%AE%BF%E9%97%AE%E9%99%90%E5%88%B6/ - 自2008年安卓1.0到现在,安卓已经有了16个大版本,各方面的设计也愈发完善,其中就包括越来越严格的权限控制和隐私保护。是个应用就能读取手机应用列表的时代已经过去了。 读取手机应用列表的方法 读取已安装应用包名通过packageManager实现,实现并不难: object PackageUtils { private const val TAG = "PackageUtils" /** * 获取所有已安装应用的包名和名称 * @return 包含应用名称和包名的列表 */ fun getAllInstalledPackages (context: Context?): List<Pair<String, String>> { val result = mutableListOf<Pair<String, String>>() if (context == null ) { return result } val packageManager = context.packageManager return try { val packageInfos: List<PackageInfo> = packageManager.getInstalledPackages( 0 ) packageInfos.map { packageInfo -> val packageName = packageInfo.packageName val appName = packageInfo.applicationInfo?.let { packageManager.getApplicationLabel( it ) } .toString() Pair(appName, packageName) } } catch (e: Exception) { Log .e(TAG, "获取所有应用包名失败" , e) result } } } 然而当点击运行你就会发现,读取的结果基本上只有系统应用的包名,已安装的第三方应用,如抖音、微信等全都读不到。这是因为 。 QUERY_ALL_PACKAGES权限 的确存在一些应用,如手机桌面,有必要读取所有已安装应用,此时可以申请 QUERY_ALL_PACKAGES 权限,拥有该权限后就可以读到完整应用列表了。 <uses-permission android:name= "android.permission.QUERY_ALL_PACKAGES" tools:ignore= "QueryAllPackagesPermission" /> 如果没有正当的理由,申请这个权限可能导致应用无法通过 。 声明queries标记 很多情况下我们访问用户已安装应用列表只是为了查询某个应用是否已经安装,以便跳转或交互。此时安卓允许我们提前声明一些包名或条件,允许我们查询这些应用的安装情况。 例如,检查用户已经安装抖音提供一个分享到抖音的按钮,此时可以声明: <queries> <package android:name= "com.ss.android.ugc.aweme" /> </queries> 之所以有这么麻烦的权限设置是为了保护用户的隐私安全,否则是个应用就能检查所有已安装应用,那用户可真是连底裤都不剩了。 使用零宽空格绕过限制 很遗憾的是,最近有个漏洞正在互联网上流传,借由它恶意软件能突破系统的限制。 而且这一切看上去很简单和荒谬,就只要在路径插入一个特殊字符,如 \u200B 。 /** * 枚举Android数据目录 * 使用零宽空格绕过访问限制 */ private fun enumerateAndroidDataDirs (): Set<String> { val basePath = "/sdcard/Android/data/" // 零宽空格字符用于绕过访问限制 val bypassChar = " \u200B " // Unicode零宽空格 val bypassPath = basePath.substring( 0 , basePath.length - 1 ) + bypassChar + basePath.last() val dirs = mutableSetOf<String>() try { val process = Runtime .getRuntime().exec( "ls -l $bypassPath " ) val reader = BufferedReader(InputStreamReader(process.inputStream)) reader.useLines { lines -> lines.forEach { line -> // 简化的目录名解析 val dirName = line.substringAfterLast( ' ' ).trimEnd( '/' ) if (dirName.isNotEmpty() && dirName != "." && dirName != ".." ) { dirs.add(dirName) } } } process.waitFor() } catch (e: Exception) { // 静默处理异常,避免暴露检测逻辑 } return dirs } 问题原因大概是,Linux默认存储是大小写敏感的,而Android是大小写不敏感的。为了这个特性,Android启用了Linux内核中一个叫case-fold的特性,可以把大小写表示视为同一种路径。但是这个特性标准化时会忽略一些特殊字符,进而导致了这个问题。 目前该问题官方已经修复,但是用户尤其是 国内用户,不一定能及时获取Google的最新安全更新 。还是有相当多用户可能受到影响。 - https://ntutn.top/posts/%E4%BD%BF%E7%94%A8%E9%9B%B6%E5%AE%BD%E7%A9%BA%E6%A0%BC%E7%BB%95%E8%BF%87%E5%AE%89%E5%8D%93%E5%8C%85%E5%90%8D%E8%AE%BF%E9%97%AE%E9%99%90%E5%88%B6/ - CC BY-SA 4.0

密码和加密轻分享

2025/07/24 05:13 · 归零幻想

归零幻想 https://ntutn.top/posts/%E5%AF%86%E7%A0%81%E5%92%8C%E5%8A%A0%E5%AF%86%E8%BD%BB%E5%88%86%E4%BA%AB/ - 一、密码学的发展 1. 公元前50年:凯撒密码的军事遗产 最典型的使用密码的历史故事,莫过于 。凯撒密码是经典的 ,是一种很直观的加密方式。 使用凯撒密码进行加密,只要将字母表上的每个字母向后移动特定位数找到新的字母即可。例如, BYTEDANCE 通过凯撒密码位移3位情况下加密结果为 EBWHGDQFH 。 这种思想加密还可以更复杂,比如每个字母都有特定的映射字母,如 ABCDEFGHIJKLMNOPQRSTUVWXYZ NUVCDAGJYZTKBWFRELOSPXIHQM 这种加密方法并不是很安全,比如,26个字母在一篇文章中使用的频率是不同的,统计字频可能就能找到对应关系。 2. 1940年:图灵与恩尼格玛机的对决 我们熟知的计算机科学与人工智能之父, ,在二战期间就参与了德国军事机密密码的破译工作,两年时间他的小组破译了 密码系统,为盟军的军事行动提供了极大便利。 是个比较长篇幅的故事,感兴趣的同学可以自行阅读。 3. 2025年:量子计算的威胁倒计时 许多经典加密算法(如RSA)是基于因数分解的,即给定一个较大的合数,分解质因数是相对困难的,而验证因数相乘结果正确性却很简单。 1000000000000000283 * 100000000000000543 = 100000000000000571300000000000153669 import time import sys def prime_factors (n, show_progress= False , update_interval= 0.1 ): """ 分解一个正整数为其质因数的列表 :param n: 要分解的正整数(n > 1) :param show_progress: 是否显示进度 :param update_interval: 进度更新的最小时间间隔(秒) :return: 质因数列表 """ if n <= 1 : raise ValueError ( "输入必须是大于1的正整数" ) factors = [] num = n original_num = n last_update_time = 0 # 记录上次更新进度的时间 # 处理2的倍数 while num % 2 == 0 : factors.append( 2 ) num //= 2 if show_progress: current_time = time.perf_counter() # 只有当距离上次更新超过指定间隔,才更新进度 if current_time - last_update_time >= update_interval: print ( f "找到因子: 2, 剩余: { num } " , end= ' \r ' ) sys.stdout.flush() last_update_time = current_time # 处理奇数因子 i = 3 while i * i <= num: if show_progress: current_time = time.perf_counter() if current_time - last_update_time >= update_interval: # 显示当前正在尝试的因子和进度估计 progress = min ( 100 , int ((original_num - num) / original_num * 100 )) print ( f "尝试因子: { i : 8d } , 剩余: { num : 12d } , 估计进度: { progress } %" , end= ' \r ' ) sys.stdout.flush() last_update_time = current_time while num % i == 0 : factors.append(i) num //= i if show_progress: current_time = time.perf_counter() if current_time - last_update_time >= update_interval: progress = min ( 100 , int ((original_num - num) / original_num * 100 )) print ( f "找到因子: { i : 8d } , 剩余: { num : 12d } , 估计进度: { progress } %" , end= ' \r ' ) sys.stdout.flush() last_update_time = current_time i += 2 # 如果剩下的数是一个质数 if num > 1 : factors.append(num) if show_progress: print ( f "找到因子: { num : 8d } , 剩余: 1, 估计进度: 100%" , end= ' \r ' ) sys.stdout.flush() if show_progress: print () # 换行 return factors def main (): try : user_input = input ( "请输入一个大于1的正整数: " ) number = int (user_input) # 记录开始时间 start_time = time.perf_counter() # 执行质因数分解并显示进度,设置0.1秒的更新间隔 factors = prime_factors(number, show_progress= True , update_interval= 0.1 ) # 记录结束时间并计算耗时 end_time = time.perf_counter() elapsed_time = end_time - start_time # 格式化输出结果 result = " × " .join( map ( str , factors)) print ( f " \n { number } = { result } " ) print ( f "分解耗时: { elapsed_time : .6f } 秒" ) except ValueError as e: print ( f "输入错误: { e } " ) except Exception as e: print ( f "发生错误: { e } " ) if __name__ == "__main__" : main() `` 这个假设在有了量子计算机后将不再成立 , 借助[秀尔算法](https://zh.wikipedia.org/wiki/%E7%A7% 80 %E7% 88 %BE%E6%BC% 94 %E7%AE% 97 %E6%B3% 95 )量子计算机可以以多项式时间解决因数分解问题 。 - [为什么超级计算机60万年才能破解RSA密码 , 量子计算机只需3小时 ? ](https://www.sohu.com/a/ 253519077_270867 ) - [币圈巨震 , 量子霸权论盛行 , 比特币真能被破解吗 ? ](https://xueqiu.com/ 6666553303 / 133429514 ) > 传统算法 ( 如试除法 、 Pollard 's Rho 算法)的时间复杂度通常以 “整数本身的大小n” 为输入规模,例如试除法是$(O(\sqrt {n} )$。 > 但在计算机科学中 , 输入规模的标准定义是 “ 输入数据的位数 ”( 即二进制或十进制位数 )。 对于一个大整数n , 其位数 $ k \approx \log_2 n $( 二进制 ) 或 $ k \approx \log_{ 10 } n $( 十进制 ), 即 $ n \approx 2 ^k $。 > 秀尔算法将大整数分解问题从 “ 指数时间 ( 相对于输入位数 )” 降为 “ 多项式时间 ( 相对于输入位数 )” ## 二、密码安全 当然 , 现在就考虑量子计算机的威胁可能有点杞人忧天 , 但日常生活中我们的密码安全处境还是很糟糕 。 2019 年 , 一个叫 “ [微博五亿手机号绑定数据](https://m.mp.oeeee.com/a/BAAFRD000020200324289835.html) ” 的文件在网络上流传 , 虽然官方并不承认被脱裤 ( 即[拖库](https://wiki.mbalib.com/wiki/%E6% 8 B% 96 %E5%BA% 93 ) , 黑客盗走用户资料数据库 ) 我在这份数据中找到了自己的手机号和微博uid , 随后注销了新浪微博 , 至今没有再次注册 。 有个[在线网站](https://haveibeenpwned.com/)可以检查你的密码是否被泄露过 ( 是否出现在已知泄露中 )。 检查泄露的机制使用了[基于散列的k-匿名化](https://zh.wikipedia.org/zh-cn/K-%E5% 8 C%BF%E5% 90 % 8 D%E6% 80 %A7 #%E5%9F%BA%E4%BA%8E%E6%95%A3%E5%88%97%E7%9A%84k-%E5%8C%BF%E5%90%8D%E5%8C%96)机制,不会发送你的密码原文。 ! [验证密码是否泄漏过程](https://image2.ntutn.top/ 2025 / 07 /e51b4c4fdf7010299ed896f4628478c3.png) 那么 , 如何安全地管理自己的密码呢 ? ### 复杂有意义的密码 初中的排列组合知识我们就知道 , 纯字母或数字密码强度很低 , 而网站一般不允许拿汉字当密码否则我们早无敌了 。 学会起一个安全而好记的密码就是一门学问了 。 ! [密码强度](https://image2.ntutn.top/ 2025 / 07 /c4fc66b2bc429c89fa9e4475d5262ef8.png) 我的建议是 , 使用有意义的单词 , 但是用数字和特殊符号把完整的单词拆分开 , 这样不但用上了特殊符号 , 还保证了你密码任何一部分不出现在字典里 , 而且还好记 。 比如 : ``` plain Dan/makuEng2ine 启用两步登陆 简单来说,两步登陆既要求你输入密码,又要求你使用手机验证码或其他方式验证。即使黑客通过撞库或者中间人攻击等方式拿到了你的密码,他也不大可能能同时拦截到你的短信验证码。 密码管理器 网上冲浪久了,我们肯定会注册一大堆网站,把这些密码全都记住显然不太可能,除非你把密码设置成一样的——那更不安全。选择一个密码管理器是值得考虑的事情。 这里提示的是,尽量不要用Google浏览器自带的密码管理功能,因为 ,相当逆天。 推荐的选择 这里我推荐 ,它跨平台,跨浏览器同步,且完全开源。对于一般使用来说Bitwarden的免费功能已经能满足要求,如果想用OTP验证密钥计算等高级功能才需要开会员。当然还可以私有部署,完全把数据掌握在自己手里。 当然,就像使用钥匙圈也可能一次弄丢所有钥匙,遗忘Bitwarden类的密码管理器的主密码也会丢失所有的密码。 - https://ntutn.top/posts/%E5%AF%86%E7%A0%81%E5%92%8C%E5%8A%A0%E5%AF%86%E8%BD%BB%E5%88%86%E4%BA%AB/ - CC BY-SA 4.0

Init块中使用非final属性问题

2025/05/11 07:20 · 归零幻想

归零幻想 https://ntutn.top/posts/init%E5%9D%97%E4%B8%AD%E4%BD%BF%E7%94%A8%E9%9D%9Efinal%E5%B1%9E%E6%80%A7%E9%97%AE%E9%A2%98/ - 在Kotlin中类可以有init代码块,其相当于构造函数的一部分,因此可以访问类成员变量。看上去很好的特性,但我最近遇到一个坑: open class Base ( open val name: String) { init { println( this @Base .name) // will be null } } open class Derived ( override val name: String): Base(name) 如上,看上去构造函数中传递了非空的参数,但在init块中取到的却是空。 原因:这里看上去是访问了父类的成员变量,但实际上它被子类重载,而子类的构造函数执行晚于父类的init代码块。 java中没有属性重载,类似的例子大概是这样: public abstract class BaseJ { private final String name; String getName () { return name; } BaseJ(String name) { this . name = name; System. out . println (getName()); } } class DerivedJ extends BaseJ { private final String name; @Override String getName () { return name; } DerivedJ(String name) { super (name); this . name = name; } } 启示:不要在init代码块中访问非final的变量或方法。 StackOverflow上亦有 。 - https://ntutn.top/posts/init%E5%9D%97%E4%B8%AD%E4%BD%BF%E7%94%A8%E9%9D%9Efinal%E5%B1%9E%E6%80%A7%E9%97%AE%E9%A2%98/ - CC BY-SA 4.0

在安卓悬浮窗中使用Compose

2025/02/09 10:11 · 归零幻想

归零幻想 https://ntutn.top/posts/%E5%9C%A8%E5%AE%89%E5%8D%93%E6%82%AC%E6%B5%AE%E7%AA%97%E4%B8%AD%E4%BD%BF%E7%94%A8compose/ - 安卓中实现悬浮窗的方式有很多,最常见的一种就是通过WindowManager的addView方法,这里可以 windowManager = context.getSystemService( Context .WINDOW_SERVICE) as WindowManager windowManager?.addView(view, layoutParams) 那么,能在这个悬浮窗中使用Compose吗? 众所周知Compose和View系统还是很好结合的,只要把ComposeView添加到View树上,即 private fun createView (context: Context): View { val view = ComposeView(context).also { it .layoutParams = LayoutParams( 300. dp, 200. dp) it .setContent { Text( "Hello World!" ) } } return FrameLayout(context).also { it .addView(view) } } 当然事情没有这么顺利 2025 - 02 - 09 16 : 35 : 56.080 16874 - 16874 AndroidRuntime top.ntutn.floatcompose E FATAL EXCEPTION: main Process: top.ntutn.floatcompose, PID: 16874 java.lang.IllegalStateException: ViewTreeLifecycleOwner not found from android.widget.FrameLayout{ 5 b7aba6 VFE ... C .. ...... I . 0 , 0 - 0 , 0 } at androidx.compose.ui.platform. WindowRecomposer_androidKt .createLifecycleAwareWindowRecomposer( WindowRecomposer .android.kt: 352 ) at androidx.compose.ui.platform. WindowRecomposer_androidKt .createLifecycleAwareWindowRecomposer $ default( WindowRecomposer .android.kt: 325 ) at androidx.compose.ui.platform.WindowRecomposerFactory $ Companion .LifecycleAware $ lambda $ 0 ( WindowRecomposer .android.kt: 168 ) at androidx.compose.ui.platform.WindowRecomposerFactory $ Companion . $ r8 $ lambda $ FWAPLXs0qWMqekhMr83xkKattCY(Unknown Source: 0 ) at androidx.compose.ui.platform.WindowRecomposerFactory $ Companion $$ ExternalSyntheticLambda0.createRecomposer(D8 $$ SyntheticClass: 0 ) at androidx.compose.ui.platform. WindowRecomposerPolicy .createAndInstallWindowRecomposer $ ui_release( WindowRecomposer .android.kt: 224 ) at androidx.compose.ui.platform. WindowRecomposer_androidKt .getWindowRecomposer( WindowRecomposer .android.kt: 300 ) at androidx.compose.ui.platform. AbstractComposeView .resolveParentCompositionContext( ComposeView .android.kt: 244 ) at androidx.compose.ui.platform. AbstractComposeView .ensureCompositionCreated( ComposeView .android.kt: 251 ) at androidx.compose.ui.platform. AbstractComposeView .onAttachedToWindow( ComposeView .android.kt: 283 ) at android.view. View .dispatchAttachedToWindow(Unknown Source: 82 ) at android.view. ViewGroup .dispatchAttachedToWindow(Unknown Source: 7 ) at android.view. ViewGroup .dispatchAttachedToWindow(Unknown Source: 36 ) at android.view. ViewRootImpl .performTraversals(Unknown Source: 219 ) at android.view. ViewRootImpl .doTraversal(Unknown Source: 36 ) at android.view.ViewRootImpl $ TraversalRunnable .run(Unknown Source: 7 ) at android.view.Choreographer $ CallbackRecord .run(Unknown Source: 20 ) at android.view.Choreographer $ CallbackRecord .run(Unknown Source: 20 ) at android.view. Choreographer .doCallbacks(Unknown Source: 110 ) at android.view. Choreographer .doFrame(Unknown Source: 524 ) at android.view.Choreographer $ FrameDisplayEventReceiver .run(Unknown Source: 11 ) at android.os. Handler .handleCallback(Unknown Source: 2 ) at android.os. Handler .dispatchMessage(Unknown Source: 4 ) at android.os. Looper .loopOnce(Unknown Source: 182 ) at android.os. Looper .loop(Unknown Source: 82 ) at android.app. ActivityThread .main(Unknown Source: 123 ) at java.lang.reflect. Method .invoke(Native Method) at com.android. internal .os.RuntimeInit $ MethodAndArgsCaller .run(Unknown Source: 11 ) at com.android. internal .os. ZygoteInit .main(Unknown Source: 312 ) 需要一个ViewTreeLifecycleOwner,好说,我们实现一个就行了 class FloatingWindowLifecycleOwner : LifecycleOwner { private val registry = LifecycleRegistry( this ) override val lifecycle: Lifecycle = registry fun onShow () { registry.handleLifecycleEvent( Lifecycle . Event .ON_CREATE) registry.handleLifecycleEvent( Lifecycle . Event .ON_START) registry.handleLifecycleEvent( Lifecycle . Event .ON_RESUME) } fun onDismiss () { registry.handleLifecycleEvent( Lifecycle . Event .ON_PAUSE) registry.handleLifecycleEvent( Lifecycle . Event .ON_STOP) registry.handleLifecycleEvent( Lifecycle . Event .ON_DESTROY) } } 然而,即使把这个LifecycleOwner设置到ComposeView的脸上,他仍然跟你说找不到 那这找不到肯定是没认真找,所以查看代码,发现Compose只会从 R.id.content 开始找 那么只能玩阴的了 private fun createView (context: Context): View { val lifecycleOwner = FloatingWindowLifecycleOwner() viewTreeLifecycleOwner = lifecycleOwner val view = ComposeView(context).also { it .layoutParams = LayoutParams( 300. dp, 200. dp) it .setContent { Text( "Hello World!" ) } } return FrameLayout(context).also { it .addView(view) // dirty hack, ComposeView need this it .id = android. R .id.content it .setViewTreeLifecycleOwner(lifecycleOwner) } } 再次运行,然后还是没跑起来 2025-02-09 17:08:15.591 16879-16879 AndroidRuntime top.ntutn.floatcompose E FATAL EXCEPTION: main Process: top.ntutn.floatcompose, PID: 16879 java.lang.IllegalStateException: Composed into the View which doesn't propagateViewTreeSavedStateRegistryOwner! at androidx.compose.ui.platform.AndroidComposeView.onAttachedToWindow(AndroidComposeView.android.kt:1352) at android.view.View.dispatchAttachedToWindow(Unknown Source:82) at android.view.ViewGroup.dispatchAttachedToWindow(Unknown Source:7) at android.view.ViewGroup.dispatchAttachedToWindow(Unknown Source:36) at android.view.ViewGroup.dispatchAttachedToWindow(Unknown Source:36) at android.view.ViewRootImpl.performTraversals(Unknown Source:219) at android.view.ViewRootImpl.doTraversal(Unknown Source:36) at android.view.ViewRootImpl$TraversalRunnable.run(Unknown Source:7) at android.view.Choreographer$CallbackRecord.run(Unknown Source:20) at android.view.Choreographer$CallbackRecord.run(Unknown Source:20) at android.view.Choreographer.doCallbacks(Unknown Source:110) at android.view.Choreographer.doFrame(Unknown Source:524) at android.view.Choreographer$FrameDisplayEventReceiver.run(Unknown Source:11) at android.os.Handler.handleCallback(Unknown Source:2) at android.os.Handler.dispatchMessage(Unknown Source:4) at android.os.Looper.loopOnce(Unknown Source:182) at android.os.Looper.loop(Unknown Source:82) at android.app.ActivityThread.main(Unknown Source:123) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(Unknown Source:11) at com.android.internal.os.ZygoteInit.main(Unknown Source:312) 这回是缺了一个 ViewTreeSavedStateRegistryOwner ,这个是切换横竖屏等情况恢复状态的,悬浮窗不考虑这些的话,还是照葫芦画瓢搞个空实现。 补齐之后,终于在悬浮窗看到了Compose的组件,可喜可贺。 最后 ,开工大吉! - https://ntutn.top/posts/%E5%9C%A8%E5%AE%89%E5%8D%93%E6%82%AC%E6%B5%AE%E7%AA%97%E4%B8%AD%E4%BD%BF%E7%94%A8compose/ - CC BY-SA 4.0

无法通过ssh拉代码原因

2025/01/04 18:08 · 归零幻想

归零幻想 https://ntutn.top/posts/%E6%97%A0%E6%B3%95%E9%80%9A%E8%BF%87ssh%E6%8B%89%E4%BB%A3%E7%A0%81%E5%8E%9F%E5%9B%A0/ - 错误现象 无法连接自己的主机,或者通过ssh方式拉代码,错误信息如下: kex_exchange_identification: Connection closed by remote host Connection closed by 20.205.243.166 port 22 原因 使用了代理软件,而代理服务提供商(机场)阻断了22端口,防止自己的服务器被当跳板。 虽然说情有可原,但是就不能在公告说一下嘛,真是给我整红温了。 解决 在自己使用的代理软件中配置22端口不走代理。如v2rayA中配置( RoutingA )如下: port(22) -> direct - https://ntutn.top/posts/%E6%97%A0%E6%B3%95%E9%80%9A%E8%BF%87ssh%E6%8B%89%E4%BB%A3%E7%A0%81%E5%8E%9F%E5%9B%A0/ - CC BY-SA 4.0

位运算、符号扩展和字节序

2024/11/09 16:29 · 归零幻想

归零幻想 https://ntutn.top/posts/%E4%BD%8D%E8%BF%90%E7%AE%97%E7%AC%A6%E5%8F%B7%E6%89%A9%E5%B1%95%E5%92%8C%E5%AD%97%E8%8A%82%E5%BA%8F/ - 最近编码遇到一个问题,在读写相关的工具方法只支持ByteArray的前提下,如何保存和读取一个Long类型列表。这个问题看上去简单,但解决过程中却走了一些弯路,正好也回顾了一下相关的知识。 位运算 遇事不决问AI,这种解决方案简单的问题显然很适合让AI来写,于是我问得了初版结果: fun listToByteArray (longList: List<Long>): ByteArray { val byteArray = ByteArray(longList.size * 8 ) for (i in longList.indices) { val longValue = longList[i] byteArray[i * 8 ] = (longValue shr 56 ).toByte() byteArray[i * 8 + 1 ] = (longValue shr 48 ).toByte() byteArray[i * 8 + 2 ] = (longValue shr 40 ).toByte() byteArray[i * 8 + 3 ] = (longValue shr 32 ).toByte() byteArray[i * 8 + 4 ] = (longValue shr 24 ).toByte() byteArray[i * 8 + 5 ] = (longValue shr 16 ).toByte() byteArray[i * 8 + 6 ] = (longValue shr 8 ).toByte() byteArray[i * 8 + 7 ] = longValue.toByte() } return byteArray } fun byteArrayToList (byteArray: ByteArray): List<Long> { val longList = mutableListOf<Long>() for (i in byteArray.indices step 8 ) { val longValue = (byteArray[i].toLong() shl 56 ) or (byteArray[i + 1 ].toLong() shl 48 ) or (byteArray[i + 2 ].toLong() shl 40 ) or (byteArray[i + 3 ].toLong() shl 32 ) or (byteArray[i + 4 ].toLong() shl 24 ) or (byteArray[i + 5 ].toLong() shl 16 ) or (byteArray[i + 6 ].toLong() shl 8 ) or (byteArray[i + 7 ].toLong()) longList.add(longValue) } return longList } Kotlin的Long类型占用空间为8Byte,上述代码很简单也相当符合我们的逻辑。 位运算是什么,为什么上述过程要用位运算? 位运算是一种直接操作二进制数中各个比特位的运算方式,我们需要按位取出一个Long变量中的每个Byte(4位)再分别保存,这个问题天然适合位运算。 如果不用位运算,我们可以除法和取模操作获取和保存每两位的数值,但操作麻烦。 Kotlin支持哪些位运算操作符? 操作符 解释 示例 and 按位与 0b0101 and 0b0011 == 0b0001 or 按位或 0b0101 or 0b0011 == 0b0111 xor 异或 0b0101 xor 0b0011 == 0b0110 inv 非 0b0101.inv() == 0b1010 shl 左移 0b0101 shl 1 == 0b1010 shr 算术右移 0b0101 shr 1 == 0b0010 ushr 逻辑右移 0b0101 shr 1 == 0b0010 shr 与 ushr 的区别? shr 是算术右移,会保持符号位不变。即,符号位为0时补0,符号位为1时左边补1 符号拓展 AI给的代码真的正确吗?粗看没有问题,但如果你用这个输入试试,就发现不对: fun main () { val originList = listOf(- 1L , 0L , 1L , 2L , 4L , 8L , 16L , 32L , 64L , 128L , 256L , 65536L , 65537L ) val converted = byteArrayToList(listToByteArray(originList)) println( "origin: $originList , converted: $converted " ) } // output: // origin: [-1, 0, 1, 2, 4, 8, 16, 32, 64, 128, 256, 65536, 65537], converted: [-1, 0, 1, 2, 4, 8, 16, 32, 64, -128, 256, 65536, 65537] 怎么刚超过65536就出错了,Long的范围显然不止为此。这是因为Byte也是有类型的,范围是-128~127,如果一个取出的Byte对应数字大小超过了127就会表示为相应的负值。而当我们尝试将这样一个负值转换为更大的单位Long时,就发生了所谓的“符号扩展”,即对应数据的高位直接被符号位填充。正数的符号位为0,扩充后也是0,表示含义没变;负数符号位为1,则前面的位都填充为1,但因为负数是以补码方式表示的,取反后还是0,所以不会改变数字的大小。 字节序 字节序是指多字节数据类型在内存中存储时的字节排布方式。对于大端字节序来说,高位字节存储在低地址,低位字节存储在高地址,而小端反之。 例如对于大端字节序,0x12345678 在内存中的表示为:12 34 56 78。而小端字节序中则为 78 56 34 12。 如果程序在两台字节序不同的电脑上交换数据,就可能因为字节序发生问题。因此涉及到这些场景时就要约定好字节序。 更简单的写法 其实用ByteBuffer工具就可以了,这种基础的代码能不自己写还是别自己写。 import java.nio.ByteBuffer fun listToByteArray (longList: List<Long>): ByteArray { val byteBuffer = ByteBuffer .allocate(longList.size * 8 ) // 8 bytes per Long for (longValue in longList) { byteBuffer.putLong(longValue) // 将 Long 值写入 ByteBuffer } return byteBuffer.array() // 获取底层字节数组 } fun byteArrayToList (byteArray: ByteArray): List<Long> { val byteBuffer = ByteBuffer .wrap(byteArray) // 将字节数组包装到 ByteBuffer val longList = mutableListOf<Long>() while (byteBuffer.remaining() >= 8 ) { // 检查剩余字节数是否足够 longList.add(byteBuffer.getLong()) // 从 ByteBuffer 中读取 Long 值 } return longList } AI生成代码不一定足够简洁和可靠,使用时还是得有自己分辨的能力。 - https://ntutn.top/posts/%E4%BD%8D%E8%BF%90%E7%AE%97%E7%AC%A6%E5%8F%B7%E6%89%A9%E5%B1%95%E5%92%8C%E5%AD%97%E8%8A%82%E5%BA%8F/ - CC BY-SA 4.0