普通视图
摩尔线程回应“闲置募集资金现金管理”:不影响募投项目实施,实际现金管理金额将明显小于上限
纳斯达克100指数调整,纳入6家公司
美银认为美联储国库券购买操作或令10年期美债收益率承压
9点1氪丨小米公司辟谣不让卖小米;ChatGPT成人模式或于明年一季度上线;罗马仕公司被罚没超124万元
今日热点导览
国家出手:卖车须明码标价,比亚迪、小鹏、北汽集体表态
十堰通报“全网最忙五人组”事件
澳大利亚青少年“社媒禁令”遭美在线平台起诉
ChatGPT遭与谋杀关联的诉讼
日本年度汉字在京都清水寺揭晓:熊
TOP3大新闻
小米辟谣“小米公司不让卖小米”
12月12日,“小米公司发言人”发布澄清说明,回应所谓“小米公司不让卖小米”一事。小米方面表示,相关视频内容,完全失实,是彻头彻尾的歪曲与污蔑。此前,公司对该账号部分内容的投诉,针对的是其恶意模仿、捏造污蔑,对公司及公司高管名誉的侵害行为,并非针对“小米”二字或“小米”农产品的正常使用。将投诉行为曲解为“不让用‘小米’二字”,是偷换概念,以“助农”议题为名,行污蔑造谣、博取流量之实。(界面新闻)
ChatGPT成人模式或于明年一季度上线
OpenAI应用部门CEO Fidji Simo在GPT-5.2的媒体简报会上表示,ChatGPT的成人模式预计将于2026年第一季度推出。Fidji Simo指出,公司希望先提升年龄预测能力,再正式引入成人内容功能,以确保可自动识别未成年用户并启用相应内容限制。
罗马仕公司被罚没超124万
深圳市市场监督管理局网站显示,近日,深圳罗马仕科技有限公司因违反强制认证规定、虚假宣传案,被深圳市市场监督管理局南山监管局没收违法所得1.2万余元、罚款123万元。(界面新闻)
大公司/大事件
市场监督总局发文:卖车须明码标价,比亚迪、小鹏、北汽集体表态
12月12日,国家市场监督管理总局就《汽车行业价格行为合规指南(征求意见稿)》(下称“意见稿”)公开征求意见。意见稿第十四条提到,汽车销售企业销售商品或者提供服务应按规定进行明码标价。有可选配件的,要标明可选配件的名称、价格、规格、产地等内容。不能现场交付车辆的,要在交易前明确告知交付时间。征求意见稿发布后,比亚迪、小鹏、北汽多家车企表态响应。(第一财经)
十堰通报“全网最忙五人组”事件
近日,“最忙五人组”的名字出现在湖北竹溪县一中标金额数千万元的采购项目评审小组名单中。中国政府采购网12月3日发布的“竹溪县住房和城乡建设局本级机械设备租赁采购中标(成交)结果公告”中,评审小组成员为“张吉惟、林国瑞、林玟书、林雅南、江奕云”。湖北省十堰市竹溪县相关项目中标公告存在评审小组成员姓名套用网络人名问题引发社会关注。
十堰市委、市政府高度重视,迅速成立联合调查组对事件开展深入调查,12日,联合调查组发布情况通报。经查,2025年2月和12月,竹溪县属国有企业竹溪大楚城市投资建设有限公司、竹溪创拓劳务有限公司,因企业资金困难,虚构“鄂陕大道提档升级”和“机械设备租赁”项目,获取资金500万元。竹溪县住建局帮助上述县属国有企业弄虚作假。竹溪县属国有企业湖北润土招投标代理有限公司套用网络人名,发布虚假中标公告。情况通报说,调查中未发现个人利益输送问题。相关资金用于其他工程建设,现已全部归还。(新华社)
用东方树叶养出“茶菌宠物”?农夫山泉回应
近期,社交媒体上流传的东方树叶“培育攻略”引发热议。网友晒出的图片显示,开瓶后的东方树叶中出现白色半透明球形菌团,最大直径达乒乓球大小,不少人将其当作“天然茶宠”把玩。12月11日,农夫山泉客服人员对此回应称,茶叶饮料开瓶后应当天尽快饮用,产品若出现“茶菌”不建议继续饮用。若消费者在未开瓶产品中发现异物,可直接与客服联系。
对于这种“茶菌”是如何形成的,天津师范大学生命科学学院教师陈炫同直播指出,这类菌团多由空气中的霉菌孢子萌发形成,可能混杂曲霉等有害菌种,伴随茶汤浑浊、产生异味。他特别强调,民间传统发酵的“红茶菌”是人工可控环境下的醋酸菌与酵母菌复合体,与自然滋生的不明菌团有本质区别,后者误食可能引发腹泻、过敏等问题,尤其需避免老人儿童接触。(界面新闻、经视直播)
澳大利亚青少年“社媒禁令”遭美国在线平台起诉
总部位于美国的在线平台红迪12月12日就澳大利亚针对16岁以下人群的社交媒体禁令向澳高等法院提起诉讼,要求法院宣布相关法律无效,或者宣布该禁令不适用于该平台。据澳大利亚广播公司报道,澳政府发言人在回应这一诉讼时表示,政府站在澳大利亚父母和孩子一边,不是站在平台一边,“我们将坚定不移地保护澳大利亚年轻人免受社交媒体的伤害。”(新华社)
日本年度汉字在京都清水寺揭晓:熊
12月12日,日本年度汉字在京都清水寺揭晓,2025年的年度汉字为“熊”。近些年来,日本全国范围内熊出没和伤人事件数量呈增长趋势。特别是今年以来,日本频繁发生熊袭击人事件,致死人数已创历史新高。日本各地,熊频繁出没于人类居住区,引发民众不安。(澎湃新闻)
ChatGPT遭遇一起谋杀关联的诉讼
据美国媒体12月11日报道,美国开放人工智能研究中心(OpenAI)及微软公司遭遇一起与谋杀相关的诉讼,诉讼指控OpenAI的人工智能聊天机器人ChatGPT“加剧用户妄想”,并引发命案。媒体称,这是美国首起将AI聊天工具与谋杀直接关联的诉讼。报道援引提交至加利福尼亚州旧金山高等法院的诉讼文件称,ChatGPT在与康涅狄格州一名56岁男子对话过程中放大并强化其偏执妄想,间接导致此人今年8月杀害其83岁母亲后自杀。(新华社)
OpenAI首席执行官回应与迪士尼合作
迪士尼周四宣布将向OpenAI投资10亿美元,并将允许这家人工智能初创公司在其Sora AI视频生成器中使用星球大战、皮克斯和漫威系列电影中的角色。OpenAI首席执行官萨姆·奥特曼在社交平台X上发帖称:“真的很高兴能与迪士尼合作,为Sora和Image带来一些魔力!迪士尼是世界上讲故事最好的公司,我们的用户非常非常想用他们的角色创造内容。”(新浪财经)
雷军回应小米上架准新车
12月12日上午,小米汽车微博官方账号发布消息称,小米汽车“现车选购”新增准新车上架开售。消费者可在小米汽车APP选购。现车包含全新现车、官方展车、准新车,已通过严格质检,可享快速提车、完整原厂质保和售后,部分车型可享优惠。面向所有用户开放,已锁单未交付的用户可改配。消费者可在小米汽车APP选购。同日上午,小米集团创始人雷军在其微博账号转发该条信息并回应小米准新车开售,称准新车就是因运输等原因产生维修项的原厂新车,这些车经过官方修复及附加检验。(界面新闻)
京东:未来5年投220亿新供应15万套“小哥之家”
36氪获悉,据“京东黑板报”微信公众号消息,随着最新一批京东全职骑手入住位于北京通州区的“京东小哥之家”,京东已面向一线员工提供了2.8万套住房。近日,京东宣布,未来5年还将投入220亿,通过租赁、自建以及住房保障基金支持等方式,提供15万套“小哥之家”。京东透露,过去20多年,京东已在改善职工居住条件上累计投入220亿元。
全国首个全自主无人化人形机器人导览解决方案发布
12月11日,北京人形机器人创新中心发布全国首个全自主无人化人形机器人导览解决方案。该方案深度整合全自主导览、拟人化交互、多机调度、全局IOT联动等关键能力,可覆盖展厅导览、商场导购、业务讲解、文旅景区等多元场景。此次解决方案的发布,标志着人形机器人导览正式迈入“全自主、无人化”的新阶段。未来,该方案可广泛应用于展厅导览、商场导购、业务讲解、文旅景区等场景,通过多机协同与全域联动,为用户带来更专业、更生动的智能服务体验。(央视新闻)
礼来发布新一代减肥针,平均帮助受试者减掉近四分之一体重
12月11日,礼来在官网公布了retatrutide的Ⅲ期临床试验研究数据,最高剂量组(12毫克)在68周内体重减轻了23.7%,膝关节疼痛分量表评分下降了62.6%。礼来表示,此次结果超出预期,有些患者甚至因为体重减轻过多,主动退出了试验。
retatrutide的作用机制是结合三种不同的肠道激素——GIP、GLP-1和胰高血糖素——这让它相比礼来的Zepbound和诺和诺德的Wegovy具有优势。礼来表示,有些患者在试验结束时已完全不再感到膝痛。礼来制药心脏代谢健康部门总裁Kenneth Custer在声明中写道:“我们认为,retatrutide有望成为体重减轻需求显著、并伴有某些并发症(包括膝关节骨关节炎)患者的重要选择。”(财联社)
荣耀品牌营销总裁离职
12月12日,荣耀管理团队(EMT)成员、公司高级副总裁、品牌营销总裁兼首席营销官郭锐确认从荣耀离职。对于离职后的去向,郭锐对第一财经记者表示,“还没定,先出来”,不排除将在未来投身于科技领域二次创业。
英伟达CEO黄仁勋:人工智能将使全球GDP增长五倍
北京时间12月11日晚间,《时代》周刊官方宣布,2025年度人物授予“AI构建者”。该杂志为其“年度人物”特刊发布了两张封面图片。图片对1932年的照片《摩天楼顶上的午餐》进行了重新演绎,将照片中的铁匠换成了来自顶尖科技和AI公司的高管,包括马克·扎克伯格、苏姿丰、埃隆·马斯克、黄仁勋、萨姆·奥特曼、德米斯·哈萨比斯、达里奥·阿莫迪以及李飞飞。英伟达首席执行官黄仁勋表示:“有人认为,全球GDP被某种力量限制在100万亿美元。人工智能将推动这个数字从100万亿增长到500万亿。”(新浪财经)
摩尔线程:拟使用不超75亿元闲置募集资金进行现金管理
12月12日晚,摩尔线程公告,为提高募集资金使用效率,合理利用部分闲置募集资金,在不影响募集资金投资项目建设实施、募集资金使用计划和保证募集资金安全的情况下,合理使用部分闲置募集资金进行现金管理,提高募集资金使用效益。
根据公告,公司及实施募投项目的子公司计划使用最高不超过75亿元(含本数)的闲置募集资金进行现金管理,使用期限自董事会审议通过之日起12个月内有效。在上述额度及期限范围内,资金可以循环滚动使用。(第一财经)
葡萄牙全国总罢工导致约400架次航班取消
当地时间12月12日,葡萄牙全国民航飞行人员工会在一份声明中表示,11日葡萄牙开启的全国总罢工活动导致该国约400架次航班被取消,除部分航空公司外,其余航司仅维持最低限度的航班服务。(央视新闻)
司美格鲁肽在印度上市,定价落在每周24美元低位区间
据报道,诺和诺德旗下药物司美格鲁肽在印度上市,定价落在每周24美元的低位区间。(第一财经)
进出境免税店项目中标候选人公示,日上免税店或退出上海机场
12月11日,上海机场集团浦东、虹桥国际机场进出境免税店项目中标候选人公示,杜福睿(上海)商业有限公司和中国免税品(集团)有限责任公司中标。日上免税行(上海)有限公司最终无缘上海机场免税店项目。
上海机场集团浦东、虹桥国际机场进出境免税店项目共有三个标段,即上海浦东机场T1航站楼及S1卫星厅国际区域、上海浦东国际机场T2航站楼及S2卫星厅国际区域、上海虹桥国际机场T1航站楼国际区域等三个场地的进出境免税店合格运营商招标。(财新网)
去哪��:2026年元旦假期,冰雪大世界周边民宿预订量在全国商圈中位居第一
36氪获悉,12月12日,全国多地将迎来初雪,哈尔滨冰雪大世界园区也在日前启动了门票预售。据去哪儿旅行数据,预售首日,冰雪大世界门票预订量暴涨,单日销量在国内景区中断层领先,同比去年增长2.8倍。其中,12月31日入园的跨年票最为火爆,票量占比超过总数的55%。数据显示,2026年元旦假期,冰雪大世界周边民宿预订量在全国商圈中位居第一。
南开大学研究团队提出广谱抗甲流新策略
12月11日,记者从南开大学获悉,该校化学学院刘书琳研究员团队开发出一种全新的广谱抗甲型流感病毒策略。该策略基于一种名为“多路复用蛋白降解靶向嵌合分子(PROTAC)”的技术,能精准锁定并同步瓦解流感病毒复制的核心——病毒核糖核蛋白复合体,实现对多个关键病毒组分的“一网打尽”。该成果为克服当前抗流感药物疗效单一、易产生耐药性等局限,提供了强效、持久且广谱的治疗新方案。相关成果近期发表于国际期刊《美国化学会志》。(科技日报)
上市进行时
鹏辉能源
36氪获悉,鹏辉能源公告,公司目前正在筹划境外发行股份(H股)并在香港联交所上市事项。公司正在与相关中介机构就本次H股发行上市的具体推进工作进行商讨,相关细节尚未确定。本次H股发行上市不会导致公司控股股东和实际控制人发生变化。
固德电材
36氪获悉,深圳证券交易所上市审核委员会定于2025年12月19日召开2025年第32次上市审核委员会审议会议,审议固德电材系统(苏州)股份有限公司(首发)。
宏明电子
36氪获悉,深交所上市审核委员会公告,成都宏明电子股份有限公司(简称“宏明电子”)首发获通过。
AI最前沿
马斯克宣布xAI将在萨尔瓦多推出全球首个全国性AI教育项目
当地时间周四,马斯克旗下人工智能初创公司xAI和萨尔瓦多政府宣布达成合作伙伴关系,在未来两年内开发和部署一个全国性的人工智能教育系统,使萨尔瓦多成为第一个在整个公立学校系统中实施个性化人工智能辅导的国家。(新浪财经)
微软高管:若AI威胁人类,将立刻停止研发
微软消费人工智能主管苏莱曼(Mustafa Suleyman)目前正致力于打造一款“符合人类利益”的超级智能。在本周最新的一场访谈栏目中他承诺,若该技术对人类构成威胁,将立即停止相关研发工作。苏莱曼在节目中表示:“我们不会继续开发可能失控的系统。”他强调这种理念应当成为普遍共识,“但坦白说,我认为这在当前行业中仍属新颖立场。”(财联社)
特朗普签署联邦人工智能行政命令
美国总统特朗普签署了一项关于人工智能政策的行政命令,称美国需要一个人工智能审批的中央授权机构。特朗普在白宫活动中发表讲话。特朗普的人工智能事务负责人David Sacks表示,行政命令赋予美国对抗各州人工智能监管的工具。(新浪财经)
大公司财报
lululemon:2025财年第三季度全球业务净营收同比增长7%至26亿美元
36氪获悉,lululemon发布了2025财年第三季度财报。在第三季度,公司全球净营收同比增长7%至26亿美元,其中国际业务净营收同比增长33%。
投融资
“伯汇生物”完成近亿元A+轮融资
36氪获悉,“伯汇生物”近日成功完成近亿元人民币A+轮融资。本轮融资由亦庄国投领投,北创投跟投,老股东龙磐投资持续加码。自此,公司完成超亿元的A轮融资。本轮融资将主要用于加速核心项目的临床推进与临床前管线的拓展,全面助力公司迈向临床与国际化的新阶段。
“安澜动力”完成千万元天使+轮融资
36氪获悉,“安澜动力”近日完成千万元天使+轮融资,这也是公司在半年内完成的第二轮融资。投资方为香港X科技基金(HKX)、清水湾二期基金、韧行投资。资金将重点用于产品功能样机的迭代研发与测试,团队人才完善。
“耐德佳”完成C4轮融资
36氪获悉,继9月份完成北京市石景山区产业基金C3轮融资之后,“耐德佳”宣布又完成由北创投旗下北文科基金投资的C4轮融资。本轮融资由C3轮投资方石景山区产业基金协同北创投投资。
“斯北图”完成C轮融资
36氪获悉,近期,“斯北图”完成超4亿元C轮融资,投资方包括中金资本、洪泰基金、卓远基金、锡创投、南山战新投等投资机构。本轮资金将加速斯北图测控、高速数传、QV馈电、相控阵、NTN基带、星间星地通信机、路由等卫星互联网载荷产品的研发迭代与核心芯片布局,同步推动工业航天智能制造线升级扩能,为巨型星座的快速组网建设提供可靠支撑。
酷产品
OpenAI发布最新升级版本GPT-5.2
美国开放人工智能研究中心(OpenAI)11日发布其人工智能模型GPT的最新升级版本GPT-5.2,以应对生成式人工智能领域日趋激烈的竞争。
为回应谷歌公司11月所发布人工智能模型双子座3的出色表现,OepnAI首席执行官萨姆·奥尔特曼近日启动“红色警报”,加快GPT升级版本的发布速度。OpenAI今年8月推出GPT-5后,11月即发布升级版本GPT-5.1,眼下不到一个月就再次更新。业界认为,此举凸显人工智能行业目前面临的竞争压力。(财联社)
三星首款三折叠手机Z Trifold正式在韩开售
三星电子旗下首款三折叠手机——Galaxy Z Trifold于12日在韩国本土正式开售。韩国本土销售仅通过三星商城官网和三星江南等全国20家实体店进行,售价359.04万韩元(约合人民币1.72万元)。Z Trifold将陆续在中国大陆、中国台湾、新加坡、阿联酋和美国等地发售,具体上市日期尚未敲定。(新浪财经)
年轻人App使用小调查
当代年轻人的手机里,都装着自己的赛博“两室一厅”。而这个空间是否完美,外看屏幕、续航、处理器,内看系统、UI、APP。众多App,在手机里悄然构建出我们的生活、工作、娱乐和运动习惯。
它们有的记录了一段激情健身;有的见证了打工人在职场摸爬滚打的点点滴滴;还有的被氪以重金,升咖为了年度会员,最后却被彻底吃灰。总之,你的每一次安装、点击、卸载,都是一个有趣的故事。
也正因为此,App好不好用直接决定了你的赛博冲浪体验,你手机里有多少App?最心水的App是哪些、为什么?一个好的App生态对你来说有多重要?欢迎扫描二维码,和我们分享你的App鉴赏小技巧!
整理|晶晶
本文来自微信公众号“36氪”,36氪经授权发布。
SpaceX敲定内部股票交易,估值约达8000亿美元
早报|苹果推送iOS 26.2正式版/微信群崩了,官方回应/TGA获奖名单公布,《光与影:33 号远征队》横扫九项大奖
![]()
《时代》年度人物公布,封面模仿经典影像被骂惨
![]()
「微信群崩了」冲上热搜,官方回复
![]()
iOS 26.2 正式版推送:锁屏字体 Liquid Glass 透明度可调,隔空投送验证码上线
![]()
小米回应「不让卖小米」传闻
![]()
美国上诉法院撤回苹果与 Epic Games 案部分禁令:iOS 外链支付可收「适当费用」
![]()
央视起底汽车行业网络「黑嘴」:法院判赔最高 201.87 万元
![]()
京东高薪招募端侧 AI 芯片人才,薪酬最高可达 100K
![]()
Reddit 起诉澳大利亚社媒禁令
![]()
李彦宏:2025 年是 AI 应用普及关键年,机会在应用层
![]()
苹果 AirTag 2 曝光:优化移动场景定位,或于 2026 年初发布
![]()
山姆 680 元年卡推销成员工核心 KPI,未完成或被点名检讨
![]()
TGA 2025 获奖名单公布,《光与影:33 号远征队》横扫九项大奖
![]()
《F1:狂飙飞车》登陆 Apple TV
周末也值得一看的新闻
《时代》年度人物公布,封面模仿经典影像被骂惨
![]()
昨天,美国《时代》杂志公布了 2025 年「年度人物」—— 一组被称作「AI 架构师」的科技行业领军者,体现人工智能对当今世界的深远影响。
封面模仿了 1932 年经典摄影作品「摩天楼上的午餐」,将 8 位科技领袖并排置于钢梁之上,象征共同搭建新时代的技术结构。
然而,这一设计在社交媒体上引发争议,不少网友批评其「亵渎工人阶级的传世影像」,甚至讽刺《时代》成为「科技垄断巨头的『舔狗』」。
![]()
封面出现的八位人物从左至右依次为:Meta CEO 扎克伯格、AMD CEO 苏姿丰、xAI 创始人马斯克、英伟达 CEO 黄仁勋、OpenAI CEO 山姆 · 奥特曼、DeepMind CEO 戴米斯 · 哈萨比斯、Anthropic CEO 达里奥 · 阿莫代伊,以及斯坦福大学教授李飞飞。
《时代》称入选者「掌握着历史的转轮」,其技术与决策正在重塑资讯结构、气候治理与生活方式;英伟达凭借前沿 AI 芯片几乎垄断全球供给并跃升为全球市值最高公司,黄仁勋在专访中强调「每个产业都需要 AI」,并判断 AI 将显著抬升全球经济上限。
《时代》年度人物评选始于 1927 年,历年既有政治人物,也曾以非个人主题呈现,如 1982 年的个人电脑、1988 年的濒危的地球与 2006 年的「你」,反映时代变迁与公共关注核心议题。
「微信群崩了」冲上热搜,官方回复
![]()
昨天,多位网民集中反映「微信群」消息出现无法发送、接收延迟等异常,相关话题一度登顶微博热搜。
对此,腾讯微信团队当天发帖致歉,并解释为「服务器轻微抖动」,目前服务已恢复。从各平台用户反馈看,短暂卡顿后消息陆续正常显示,故障影响时间较短、范围以群聊为主。
iOS 26.2 正式版推送:锁屏字体 Liquid Glass 透明度可调,隔空投送验证码上线
昨天,苹果正式推送 iOS / iPadOS 26.2、macOS 26.2 Tahoe 等系统的正式版更新,在锁屏界面新增时间字体 Liquid Glass 透明度调节滑杆,用户可根据需求降低玻璃感以提升可读性。
Liquid Glass 自 iOS 26 引入,曾因透明度过高影响阅读而引发争议,苹果在 iOS 26.1 已提供「磨砂」选项,此次进一步开放自定义调节,设计负责人 Alan Dye 已离职前往 Meta,由交互设计主管 Stephen Lemay 接任。
其他功能方面:
- 隔空投送新增验证码,可与非联系人建立 30 天「已知」关系,便于临时分享;
- 提醒事项加入「紧急」选项,到期自动响铃;
- Apple Music 支持离线歌词;
- 播客应用获得自动章节与定时链接功能;
- AirPods 实时翻译在欧盟上线;
- 美国地区新增「增强安全警报」,在洪水、自然灾害等威胁来临时推送地图与安全指引。
此外,苹果还推送了 macOS 26.2、watchOS 26.2、visionOS 26.2、tvOS 26.2 系统的正式版。
安全层面,苹果修复超过 20 项漏洞,其中两项 WebKit 漏洞已被复杂攻击在旧版 iOS 中利用,可能导致恶意网页执行任意代码或内存破坏。
其他修复包括 App Store 支付令牌泄露风险、恶意图片处理导致内存破坏、隐藏相册绕过认证、FaceTime 控制下密码意外移除等。苹果建议用户尽快升级至最新版本以降低风险。
小米回应「不让卖小米」传闻
![]()
昨天,小米公司发言人回应网传「小米公司不让卖小米」的视频内容,称该视频完全失实,属歪曲与污蔑。
小米公司发言人表示,其此前对相关账号的投诉,针对的是恶意模仿、捏造污蔑,侵害公司及公司高管名誉的行为,并非针对小米二字或小米农产品的正常使用。
小米强调,将投诉曲解为不让用小米二字,是偷换概念,以助农议题为名,行污蔑造谣、博取流量之实。公司将依法维权,对虚假网络信息与刻意挑动网络舆情行为说不,共同营造风清气正的网络舆论空间。
美国上诉法院撤回苹果与 Epic Games 案部分禁令:iOS 外链支付可收「适当费用」
![]()
据 The Verge 报道,美国第九巡回上诉法院昨天对苹果与 Epic Games 的长期法律纠纷作出最新裁决。
法院确认苹果在 2021 年外链支付禁令中存在藐视法庭行为,但推翻了此前「全面禁止苹果收取佣金」的判决,允许苹果未来对通过 iOS 应用内外链完成的购买收取「合理费用」。
不过,目前具体费率尚未确定,案件已发回地方法院重新审理或由双方协商决定。在合理费率出台前,苹果不得就第三方支付系统(包括 Epic)完成的销售抽取佣金。
法院指出,地方法院此前的「零佣金」裁决更像是惩罚性措施,而非合理的民事制裁。
新裁决强调,合理费用应基于苹果为外链管理产生的必要成本,并对其知识产权使用给予一定补偿,但不得包含安全与隐私方面的费用。
同时,法院部分放宽了对外链设计的限制,允许苹果禁止开发者使用比应用内购按钮更醒目的字体、尺寸和位置,但必须至少允许开发者使用与苹果自有按钮相同的规格放置外链支付选项。
此外,据 engadget 报道,在上月与 Google 达成和解后,《堡垒之夜》已于昨天重新上架美国 Google Play 商店。今年稍早,该游戏也在 iOS 平台回归。
Epic Games CEO Tim Sweeney 表示,他不会同意与苹果分享通过 iOS 应用外部链接产生的收入。
Sweeney 称该裁决「彻底否定了」 苹果以往关于访问费用的理论,并表示此举可能影响海外监管态度。
他接受与外部链接审核工作量直接相关的固定费用,但拒绝任何对商店外交易的比例分成。他还批评苹果曾刻意让外部链接「难以使用」,并表示 Epic 不会为外部购买支付佣金。
央视起底汽车行业网络「黑嘴」:法院判赔最高 201.87 万元
![]()
中央电视台新闻频道「法治在线」栏目昨天起底汽车行业网络「黑嘴」运作模式。
央视从「发黑稿」「敲竹杠」「扮专家」「带货」「搞对立」「泼脏水」等手法入手,梳理自媒体通过编造负面、恶意测评、制造舆情后再以「删除负面」「商业合作」为要挟索取所谓「保护费」的链条。
报道点名「龙猪集车」长期针对比亚迪发布侮辱、抹黑内容,甚至用「移花接木」的起火视频造谣「车辆自爆」,法院二审判决其构成名誉侵权,责令删除内容、公开致歉,并赔偿比亚迪经济损失 201.87 万元;
「大眼哥说车」打着「爱国」「正义」旗号恶意辱骂长城汽车,被判删除视频、公开道歉并赔偿 20 万元;「龙老师讲电车」长期散布未经核实的不实信息,恶意抹黑小鹏汽车,被判立即删除全部侵权视频、公开致歉并赔偿 10 万元。
近期,国家网信办会同工业和信息化部等部门持续开展汽车行业网络乱象专项整治,重拳治理恶意抹黑攻击、散布不实信息、开展虚假测评等违法违规行为,并连续通报两批典型案例:
包括「车曝台」「暗夜曰车」「智驾安全榜」「奢见财经」「糯人糖糖」等账号集纳负面、发布不实测评、歪曲解读涉企公开信息、同质化扰乱市场秩序的问题,涉案账号已依法依约处置。
相关负责人表示,汽车行业网络乱象严重破坏市场公平竞争、污染网络生态,「饭圈化」倾向加剧粉丝间相互攻击、贴污名化标签,必须以零容忍态度严厉打击。
京东高薪招募端侧 AI 芯片人才,薪酬最高可达 100K
![]()
据《科创板日报》报道,京东近期启动端侧 AI 芯片人才招募,重点集中在「存算一体」芯片设计工程师岗位,招聘方向涉及机器人、智能家电等硬件应用场景。
薪酬方案为「40K – 100K × 20 薪」,具体区间取决于候选人的技术背景与项目经验。
京东强调,候选人需在顶级会议或期刊发表过高质量论文,或拥有技术专利、赛事奖项,以体现技术实力与创新能力。
据新浪财经报道,此前,京东已推出「顶尖青年技术天才计划」(TGT),面向全球高校本硕博学生及毕业两年内的技术人才开放。
该计划涵盖多模态大模型、机器学习、搜索推荐广告、空间与具身智能、高性能与云计算、大数据、AI 基础设施、安全等 8 个方向。该计划不设薪酬上限,已发放上百个 offer,并为 2025 届毕业生提供 1.8 万个核心岗位。
财务层面,京东财报显示,2025 年三季度收入达 2991 亿元,研发投入 56 亿元,为高薪招聘提供保障。
Reddit 起诉澳大利亚社媒禁令
![]()
据新华网报道,生效不到三天,澳大利亚针对 16 岁以下人群的「社媒禁令」遭美国社交平台 Reddit 起诉。
Reddit 于昨天向澳大利亚高等法院递交法律文书,要求法院宣告相关法律无效,或裁定该禁令不适用于其平台。
在文书中,Reddit 强调自身是「公共论坛」,而非主要供用户进行在线社交互动的平台,并称对 16 岁以下用户的访问设置已限制可能有害的特定类型内容。
澳政府发言人在回应起诉时表示,政府立场是「站在澳大利亚父母和孩子一边,而不是平台一边」,并称将「坚定不移地保护澳大利亚年轻人免受社交媒体的伤害」。
此前,澳联邦议会于去年 11 月通过修正案,要求特定社交媒体采取「合理措施」阻止未满 16 岁的人在其平台拥有账户;不配合的企业最高将面临 4950 万澳元(约合 23 亿元人民币)罚款,但违规的未成年人及其监护人不受处罚。
自修正案通过以来,澳政府根据具体情况动态调整需执行禁令的平台名单,在列的除 Reddit 外,还包括 Facebook、YouTube、TikTok、X、Instagram 等 10 个在线平台。
澳政府电子安全机构研究显示,96% 的 10 岁至 15 岁青少年使用社交媒体,其中 70% 的人接触过有害内容。政府强调,最低年龄限制措施可减少社交媒体带来的负面影响。
不过,据此前彭博社报道,在禁令生效之际,Reddit 曾提前在全球范围内为未满 18 岁用户推出新的安全功能,包括更严格的聊天设置和个性化广告限制。
该功能将自动应用于被识别为未成年用户的账户,并在澳大利亚配合年龄预测模型进行验证。
拓竹接入腾讯混元 3D 3.0,可将 AI 手办变成实物
![]()
昨天,腾讯混元宣布拓竹科技旗下 MakerWorld 正式接入腾讯混元 3D 3.0 大模型,双方联合推出名为「印你」的手办生成器,旨在通过 AI 技术帮助普通用户完成 3D 建模,进一步降低消费级 3D 打印的创作门槛。
相比过往需要依赖昂贵扫描设备或耗时的人工作建,「印你」将流程极大简化为 3 步,仅需上传 – 设计 – 建模即可完成。
具体来看,用户仅需上传一张人像图片,系统可自动生成 2D 立体图、执行自动去背景与风格化处理,并进一步转换为高质量、可直接打印的 3D 模型,精确还原面容、衣着和姿态。
lululemon 第三季度营收增长 7%,中国大陆营收同比大增 46%
![]()
lululemon 于昨天发布 2025 财年第三季度业绩报告,其中中国大陆市场表现亮眼。
数据显示,公司全球净营收同比增长 7%,达到 26 亿美元,其中国际市场表现尤为突出,净营收同比增长 33%,中国大陆市场则录得 46% 的增幅。若以固定美元计算,中国大陆市场增幅更达 47%。
lululemon CEO Calvin McDonald 在财报电话会议中表示:
第三季度,我们持续聚焦提升美国市场表现,同时保持国际市场的稳健增长势头。随着节日季的到来,我们对目前取得的成绩感到满意,并感谢团队对客人与社区的持续投入。
他特别提到,中国大陆业务在本季度保持强劲态势,预计全年净营收增速将达到或优于此前给出的 20% 至 25% 指引高区间。
她补充称,公司预计今年新增约 46 家直营门店,并完成约 36 家门店优化,其中大部分新店将落地中国市场。
此外,lululemon 于 10 月 27 日将中国门店支持中心迁至上海西岸中环,全面入驻五层独栋办公楼。
公司预计 2025 财年第四季度净营收将在 35.75 亿美元至 35.85 亿美元之间,全年净营收预计在 109.62 亿美元至 110.47 亿美元之间,全年每股摊薄收益预计在 12.92 美元至 13.02 美元之间。
李彦宏:2025 年是 AI 应用普及关键年,机会在应用层
![]()
据上观新闻报道,百度创始人李彦宏在昨天《时代》周刊「AI 架构师」专题采访中表示,2025 年将是 AI 应用普及的关键一年。
他判断,基础模型层最终会留下少数几家,但应用层的各个方向将涌现众多成功参与者,「我认为那里才是机会最多的地方」。
他强调,百度采取「应用驱动」策略,针对搜索、数字人等重点领域定向训练模型以形成优势,而非追求面向所有人的「万能模型」。
李彦宏表示,全球 AI 竞争态势趋于白热化。与美国科技界主流投入巨资发展 AGI 不同,中国更关注应用,并拥有制造业等独特场景与低成本高效率的现实需求,「我们需要利用 AI 来解决这些挑战」。
他进一步提出,百度面向真实产业场景发布可商用自我演化超级智能体「伽谋」,以寻求「全局最优解」,并在公开性能基准测试与多项权威评测中展现算法推理优势与技术竞争力。
谈及技术趋势,他预计行业的决定性突破将在多模态,尤其在药物研发领域希望以 AI 推动革命性变革。
此外,他重申百度训练模型旨在解决应用层问题,「我不认为我们应该去创造一个能为所有人做所有事的超级智能 AI」。
在更广泛的行业语境中,李彦宏多次强调「应用驱动」:他在此前接受《极客公园》采访时指出,「昨天大家在卷芯片、卷模型等等,我一直是说要卷应用,应用才是真正创造价值的地方」。
苹果 AirTag 2 曝光:优化移动场景定位,或于 2026 年初发布
![]()
据 MacRumors 报道,苹果正在为 AirTag 2 带来一系列功能升级,重点包括更顺畅的配对流程、更精准的「Precision Finding」 定位功能、细化的电量显示,以及在移动场景和人群密集环境下的追踪优化。
爆料显示,iOS 26 内部测试代码中出现了「2025AirTag」 的标识,显示该产品原本计划在 2025 年发布,但目前更可能在 2026 年初亮相。
硬件层面,新 AirTag 预计会采用全新的 UWB 芯片,以扩展精确查找的有效范围,外观延续初代的圆盘设计与可更换电池,但扬声器将升级并更难被拆除,以提升可靠性和防篡改能力。
值得注意的是,同一批代码还出现了下一代 HomePod mini(搭载 S10 芯片)以及早前网传 AI Siri 的迹象。
蚂蚁开源业内首个 100B 扩散语言模型 LLaDA2.0
![]()
昨天,蚂蚁技术研究院联合人大、浙大、西湖大学推出扩散语言模型系列「LLaDA2.0」,包含 MoE 架构的 16B「mini」与 100B「flash」两版,宣称为业内首个 100B 级扩散语言模型(dLLM)。
团队表示,模型在知识、推理、编码、数学与智能体等多维评测中与强自回归(AR)模型持平或展现优势,并已在 Hugging Face 开源权重与相关技术报告。
官方公开的模型卡显示,LLaDA2.0-flash 为 100B 总参数的 MoE 扩散语言模型,推理仅激活约 6.1B 专家参数,显著降低计算成本;在编码与复杂推理任务上表现突出,工具调用与智能体任务亦有较好能力。
蚂蚁集团资深技术专家赵俊博强调,扩散架构在推理过程中可「直接修改与控制 token」,无需像自回归(AR)模型重新生成整段内容,理论上更有望实现更快生成速度与更低计算成本。
技术报告:https://github.com/inclusionAI/LLaDA2.0/blob/main/tech_report.pdf
Hugging Face:https://huggingface.co/collections/inclusionAI/llada-20
ModelScope:https://modelscope.cn/collections/LLaDA-20-6d3266b0308b41
Runway 推出 GWM‑1 通用世界模型,AI 视频生成跨越至「世界建模」
![]()
近日,Runway 宣布推出通用「世界模型」家族 GWM-1,并同步更新其旗舰视频生成模型 Gen‑4.5。
GWM-1 基于 Gen‑4.5 构建,可逐帧生成、实时运行,并通过摄像机姿态、机器人指令、音频等动作进行交互式控制,包含「GWM Worlds」「GWM Avatars」「GWM Robotics」三种后训练变体:
- GWM Worlds 能在长时间移动序列中维持空间连贯性,允许定义环境的几何、光照和物理规则,并对输入动作作出准确响应;
- GWM Robotics 被描述为基于机器人数据训练的「学习型模拟器」,可生成动作条件的视频滚动,支持「反事实」生成以探索不同轨迹与结果,以降低昂贵的真实数据采集与线下测试风险;
- GWM Avatars 为音频驱动的交互式视频生成模型,可在长时对话中稳定呈现自然的人类动作与表情(面部表情、眼球运动、口型同步、手势),适用于实时辅导与教育、客户支持与服务、培训模拟、互动娱乐与游戏等场景。
Runway 在直播中称,其战略正从影视制作扩展至机器人、物理与生命科学,并强调世界模型位于 AI 进步前沿:仅靠语言模型难以解决机器人技术、疾病、科学发现等问题,真正的进步需要模型像人类一样在模拟环境中体验世界并从错误中学习。
Gen‑4.5 的更新集中在画面保真度与创意控制,并新增原生音频生成与编辑、多镜头视频编辑等能力;官方同时承认现阶段视频生成仍存在因果推理、客体恒存性与成功偏差等常见局限,这些问题将作为世界模型研究的重点方向持续迭代。
山姆 680 元年卡推销成员工核心 KPI,未完成或被点名检讨
![]()
据中新网报道,近期山姆会员店在全国门店内对「680 元年费『卓越卡』」的推销明显升级,部分会员反映从入口到结账环节屡次拒绝仍被推荐。
有员工透露,卖卡已被纳入一线员工核心KPI,未完成将面临在会议上被公开点名、书写检讨等处理,每成功推广一张卡员工可获约 30 元提成。
在运营流程上,推销需与民生银行信用卡推广人员协同,对每位结账顾客进行尝试;为减少新办会员退卡,规定需完成至少两次线上消费方可退卡。伴随这一「扩张、提效」策略推进,山姆今年新开设 9 家门店,全国总数达 61 家。
除推销强度外,消费者对产品质量与选品标准的质疑升温。
截止发稿,黑猫投诉平台针对山姆的投诉量已突破 1.3 万条,集中于食品安全与虚假宣传等问题;自有品牌 Member’s Mark(MM)被指出现「肉眼可见的降级」,如有机大豆等级由 1 级降至 3 级但价格未变,选品「大众化」争议亦增多。
此前,山姆方面回应「原味麻薯包装箱内出现活老鼠」事件称已完成全链路核查,初步判断为取货点周边虫害偶然侵入导致,并与消费者沟通致歉;同时,上海市场监管部门曾因销售不合格儿童用品对山姆处以罚款,引发会员对其品控的进一步担忧。
年初,山姆中国管理层调整后,新任代理总裁 Jane Ewing 推动以「加速规模扩张、强化数据驱动、压降成本提升效率」为核心的策略转型。
在供应链与新品开发周期压缩、MM 品类结构占比变化、进口商品比例下调等举措的背景下,山姆一方面实现中国区净销售额显著增长,另一方面也面临约 900 万付费会员的「是否仍然值得」再评估。
TGA 2025 获奖名单公布,《光与影:33 号远征队》横扫九项大奖
![]()
昨天,被称为「游戏奥斯卡」的 TGA 2025 正式落幕,法国工作室 Sandfall Interactive 打造的《光与影:33 号远征队》成为最大赢家。
这款融合艺术、动作与 RPG 要素的独立作品在 11 个组别中获得 13 项提名,并最终斩获年度最佳游戏、最佳游戏指导、最佳叙事、最佳美术、最佳配乐、最佳演出、最佳独立游戏、最佳首秀游戏、最佳角色扮演游戏九冠王,创下纪录。
除年度游戏外,其他奖项也各有亮点:
- 《战地风云 6》获最佳音效设计
- 《黑帝斯 II》获最佳动作游戏
- 《空洞骑士:丝之歌》拿下最佳动作冒险
- 《饿狼传说 群狼之城》获最佳格斗游戏
- 《ARC Raiders》获最佳多人游戏
- 《Final Fantasy 战略版:伊瓦利斯编年史》获最佳模拟/策略游戏
- 《咚奇刚 蕉力全开》获最佳家庭游戏
- 《马力欧赛车世界》获最佳运动/竞速游戏
- 《赛马娘 Pretty Derby》获最佳移动游戏
- 《无人深空》获最佳持续运营游戏
- 《午夜之南》获最具影响力游戏
- 《最后生还者 第二季》获最佳改编内容
- 《博德之门 3》获最佳社群支持
- 《午夜漫步》获最佳 VR/AR 游戏
- 《GTA VI》获最受期待游戏
- 《鸣潮》获玩家之声
- 《CS2》获最佳电竞游戏
- Chovy 郑智勋获最佳电竞选手,Team Vitality 获最佳电竞战队,MoistCr1TiKaL 获年度内容创作者。
今年的 TGA 被形容为「神仙打架」,续作稳健,新 IP 惊喜不断,AAA 与独立作品齐头并进,展现了创作者们的热情与坚持。
相关阅读:TGA 年度游戏汇总:《33 号远征队》包揽 9 项大奖,《影之刃零》定档明年9月
《F1:狂飙飞车》登陆 Apple TV
![]()
昨天,布拉德 · 皮特主演的《F1:狂飙飞车》正式通过 Apple TV 在全球范围上线流媒体服务。
影片讲述曾在 1990 年代因比赛事故被迫退役的车手 Sonny Hayes(布拉德 · 皮特 饰)在 30 年后加入濒临解散的 F1 APXGP 车队,再度挑战巅峰,并与被视为车队未来的新秀 Joshua Pearce(达蒙森 · 伊德里斯 饰)形成微妙的竞争与合作关系。
影片以极具真实感的速度与音效设计再现 F1 赛场氛围,并兼顾赛道外叙事,因高度完成度与沉浸感获得好评。除苹果设备外,影片还可通过 Android 版「Apple TV」应用观看。
是周末啊!
One Fun Thing|腾讯元宝现在可以锐评你的王者战绩了
![]()
昨天,腾讯元宝上线《王者荣耀》对局复盘与战绩锐评能力,从游戏中心对话框里可直接调取战绩卡片,元宝会自动生成复盘分析,帮你拆解 KDA、输出占比、参团率等数据,甚至给出出装和阵容建议。
目前,该功能仅支持 QQ。打得好时你能收获花式夸夸,打得菜时也会被毫不留情地「锐评」指出问题:
比如小乔全场输出带飞,元宝会直呼「实至名归」;如果你家打野参团率低,元宝则会提醒「阵容缺控缺爆发,输得不冤」。它的点评既有数据支撑,也带点调侃意味。
具体来看,该功能可对个人与队友的 KDA、输出占比、承伤占比、参团率等数据进行结构化解读,结合对手英雄与阵容特性给出控场、爆发、保奶等策略倾向,强调不同打野强度、兵线压力与对面支援速度等因素对出装与游走路径的影响。
周末看什么|《星际穿越》
![]()
《星际穿越》的剧情设定在近未来的地球。由于环境恶化与农作物大面积枯萎,人类面临严重的生存危机。故事核心围绕土星附近出现的神秘虫洞展开。
库珀与布兰德教授的女儿艾米莉亚 · 布兰德、罗米利、多伊尔等人组成探险队,乘坐宇宙飞船前往遥远星系,考察三颗可能适合人类居住的星球。
《星际穿越》获第 87 届奥斯卡「最佳视觉效果」,并曾获第 72 届金球奖电影类「最佳原创配乐」提名。截至发稿,本片在豆瓣已有 2140094 人评价,评分为 9.4。
买书不读指南|《从此世间有稀土》
![]()
《从此世间有稀土》以通俗而生动的笔触、稀土元素的发现史为主线,结合科学探索与人文叙事,呈现了科学家们在 160 年间不断试错、逐步走向真理的过程。
书中详细讲述了 17 种稀土元素的发现历程,强调稀土在现代产业中的战略地位,被称为「工业维生素」。作者指出,稀土在电子信息、新能源汽车、军工等领域发挥着不可替代的作用。
羊顿在书中引用理查德 · 费曼的观点,强调科学理论始终处于「错误与真理之间的某个刻度」,没有绝对的正确或错误,只有不断进化的过程。
游戏推荐|《霍格沃茨之遗》原价 384 元免费领
![]()
原价 384 元人民币的《霍格沃茨之遗》现已在 Epic 商店开启限免,12 月 19 日前,所有玩家可在 Epic 平台免费领取并永久拥有该游戏。
《霍格沃茨之遗》是一款由 Warner Bros. Games 出品的沉浸式开放世界动作角色扮演游戏,基于《哈利·波特》系列书籍设定,玩家可在魔法世界中自定义角色、调配魔药、施放咒语并探索野兽与场景。
本作于 2023 年 2 月 10 日登陆 Steam 平台,目前玩家好评率为 90%「特别好评」。媒体 IGN 亦给出 9/10 分「奇佳 」的评价。
#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。
美股三大指数集体收跌,大型科技股多数下跌
TGA上宣布定档,梁其伟终于把《影之刃零》端上来了
文丨贝果树
编辑丨果脯
今年的TGA上,《光与影:33号远征队》可谓风头无两。它狂揽包括年度最佳游戏在内的9项大奖,成为今年行业内的最大高光点。而B站作为官方直播合作方,在年度最佳游戏公布时刻的“断播”也因此显得相当抢戏。
![]()
TGA颁奖典礼
但对中国玩家来说,2025 TGA极其具有记忆点的事情还有两件:一个是《明日方舟:终末地》定档明年1月22日,比苹果官方“剧透”的时间还早了几天。另一个则是前戏做足的《影之刃零》,终于宣布将于明年9月9日上线,并于今天上架Steam商店。
![]()
《影之刃零》Steam页面
作为继《黑神话:悟空》后,最受关注的3A级国单,《影之刃零》其实已经吸引了相当多的目光与期望。
今年ChinaJoy、科隆展等大型展会上,《影之刃零》摊位的试玩队伍基本都是人满为患。而官方灵游坊一头持续释放内容,一头也借定档时间进行多次宣发,如他们在科隆展上放出的技术演示,曾被玩家调侃是“预告的预告”。甚至有网友分析其设计难度,推测游戏要真做好说不定得到2027年。
这么一看,《影之刃零》要是2026下半年能准时上线,并且把游戏打磨好,对国单市场而言倒也是比较理想的节点。
自2022年首曝至今,《影之刃零》已经能给玩家提供足够具体的产品形象。它继承了灵游坊一贯的暗黑朋克武侠等,且不继承前三部IP手游的剧情,而是其CEO兼制作人梁其伟首款系列产品“雨血”的续作——或者更严格地说,它传承于梁其伟于2008年推出的RPG《雨血:死镇》。
出走十五载,梁其伟仍想做回当初的少年。
01
2008年,尚在就读大学的梁其伟掏出了一款独自制作的RPG《雨血:死镇》。
这款游戏使用RPGMaker引擎开发,以武侠世界为背景,讲述了主角“魂”被朋友陷害后,被组织追杀的故事。故事开篇,身手重伤的魂为了躲避追杀,找寻阴谋背后的真相,动用秘法恢复身体,结果导致自己生命仅剩六十四天。这种注定走向灭亡的基调,也贯穿了梁其伟后续的多部作品。
![]()
《雨血:死镇》
尽管《雨血:死镇》的流程只有4小时,但它所呈现的世界观却吸引了大量玩家的挖掘与创作,也因此收获了大量认可。在游戏点评网站RPGfan上,这款游戏评分高达8.8——不久前发售的《宝可梦ZA》在该网站也只获得了8.1分。
但这里面最激励梁其伟的,更多是那些来自多方态度的转变与现实支持。
因为开发游戏,梁其伟曾受过包括父母、女朋友乃至女朋友父母等人的多方“劝阻”,希望他悬崖勒马。而他自己,也真的因为做游戏挂掉一门课,从而错失了清华建筑系研究生的保送资格。为此,他不得不用硬盘将做到一半的《雨血:死镇》封存。
![]()
《雨血》系列草稿
直到后来,他在耶鲁大学课程实在太少,百无聊赖之下想起这事,才重新拿出来开发。
完整的《雨血:死镇》,也让梁其伟接收到了与过去截然不同的社会反馈——看到那些看不懂中文的同学,对着游戏画面大呼“my god”;教授们看到以后,不仅没有劝阻梁其伟,还抓着他大谈东方美学。更夸张的是,后续小半个学院都知道他们这届出了个梁其伟,“Chinese guy making cool games”。
而网络上的反馈更夸张。《雨血:死镇》上传第二天,就收获了超10万的下载量,后续各平台累计下载更是超过了400万次。2010年,他将《雨血:死镇》翻译成英文丢到一些小RPGmaker的论坛后,还有网友主动找过来,希望免费给游戏重写一遍代码语言——当然,最后梁其伟还是坚持给了对方600刀,并在游戏发售后的1小时内赚了回来。
这段经历,不仅让梁其伟有了动力,继续开发制作了第一章的《雨血2:烨城》,也成了他未来成立灵游坊的重要契机。
2011年,当时留学美国的梁其伟因为一些机缘巧合回国,他见了之前做《雨血:死镇》时就帮他做游戏的网友,组建了团队。而出于对梁其伟本人的认可,真格基金创始人徐小平也给了他100万投资。
于是乎,灵游坊就这么成立,并于同年十二月推出了第二款游戏《雨血2:烨城》。
![]()
《雨血2:烨城》
到了2013年,灵游坊发布他们的第三部作品,《雨血前传:蜃楼》,相比前作,它们都拥有了更加明显的进步,这个系列从回合制RPG游戏进化为了Unity3D引擎制作的横板动作游戏。梁其伟的创作重心,慢慢开始从“剧情至上”,转变到了钻研游戏性。
《雨血前传:蜃楼》游戏的首发表现还算不错,仅用3个月就突破了30W的销量。同年12月中旬,该游戏的英文单机版本也在仅发行一个月后在Steam平台卖出了13万套。但技术的进步也意味着游戏开发的成本在变大。这时,灵游坊的团队扩展到了7人。
02
此时,梁其伟也意识到,做单机并非长久之计。《雨血前传:蜃楼》30万的销量最终只给灵游坊带来了两三百万的收入,发行商拿了其余的大头,团队实际上处于入不敷出的状态。
一段时间里,梁其伟甚至没法拿出团队的工资,大家开始讨论散伙,怎么才能找到下一份工作。直到这时,理想与现实才开始碰撞,他们第一次开始考虑除了“做一款好游戏”之外的事:市场、管理运营,财务问题,要怎样才能继续生存下去。
幸运的是,同期网易正逢求变时期,丁磊看中了灵游坊的独特气质与创作精神,大手一挥就给其投资了数百万美金——甚至是在梁其伟主动为他们敲警钟的前提下。
他直白地告诉丁磊,之前的PC单机游戏做得再怎么样,其实仍旧不赚钱。进军手游,灵游坊也会采取一贯的研发,可能会错过许多机会。对此,丁磊给他的回应只有“死磕”。
基于这种开发背景,2017年9月,灵游坊推出ARPG手游《影之刃1》。作为一款横板格斗游戏,《影之刃1》继承了雨血系列的武侠特色,凭借独特的美术风格和优良的动作设计收获了许多玩家的喜爱。在2014德国科隆展上,它获得了最佳移动游戏的提名,与任天堂的《全明星大乱斗》同台竞争。
![]()
《影之刃1》
更重要的是,《影之刃1》让灵游坊真正意义上赚到了钱。《影之刃1》月流水超4000万,团队不仅不用考虑是否散伙,整体人数还从20人增长到70人。
此后,他们陆续发行了《影之刃2》和《影之刃3》两部续作。《影之刃2》的预算比前作高了十倍,刚上线时就获得了App Store中国区首页十个推荐位。《影之刃3》公测当天,游戏iOS端登顶AppStore角色扮演游戏榜首。在不断提高的预算和技术之下,《影之刃》系列似乎在不断进化,但也伴随着不少玩家的质疑。
因为急于变现,《影之刃2》发售首月表现不错,但随后便迅速下滑。不少玩家质疑这部作品为了追求商业化改变了原作风格。平衡性问题突出,只有氪金才能获得好的游戏体验。而《影之刃3》在开服后,TapTap上的评分也迅速下滑。尽管梁其伟称他们做的是“带着独立精神的商业游戏”,但显然无法靠此说服不满的玩家,不少人坚信他是为了赚钱放弃了独立游戏精神。另一件在玩家群体里被津津乐道的事是,在转型手游并运营不佳后,梁其伟被玩家发现在微博上关注兰博基尼推文并拿出汽车模型,才有了后续说他“赚了钱先买兰博基尼”的声音。
直到他们决定开始做《影之刃零》,玩家的声音逐渐从质疑转变为期待。
一方面,他们在思考后决定继续做自己想做的游戏。另一方面,他们拿到了腾讯的投资。
2021年,灵游坊发生工商信息变更,新增股东腾讯,后者持股25%。在梁其伟发布的内部信中,他说:“腾讯得知了我们制作大型PC/主机游戏的想法,随即前来了解情况。在深入探讨后,腾讯提出在不干涉公司经营,不绑定产品合作,不影响作品创作的前提下,对我们进行资金支持和其它必要的帮助。”而他也透露了拿到这笔投资后的后续计划,在未来五年内,将开发两款以PC/主机为主,使用UE5引擎开发的大型ARPG游戏,其中之一就是今天的《影之刃零》。而又为了能全力开发,他们砍掉了当时计划的另一款游戏《群星守卫》,专注于《影之刃零》的开发。
而《影之刃零》的剧情延续了灵游坊初期的雨血系列。但在那时,《雨血:死镇》还是一款仅由一人开发的独立游戏。《雨血》初期的人物和剧情诞生于他政治课上的草稿纸。梁其伟当时学的是建筑,为了做这款游戏,他选修了程序相关的课程。那些课程其实很简单,但对于没有任何基础的梁其伟来说,很难。最终,他一个人花了三年的时间,做出了这款只能玩几小时的游戏。
从课堂上的草稿纸到《影之刃》手游,再到今天的《影之刃零》,同样的武侠世界一路跌宕起伏,因为坚持独游失利过,为了赚钱妥协过。但最终,《影之刃零》出现了,他还是想做一款单机游戏,捡起自己昔日最初的梦想——更何况,现在做好单机游戏,是真的能挣到钱了。
2008年的《血雨:死镇》里,身负重伤的主角只剩下六十四天的生命,而在十余年后的《影之刃零》中,主角“魂”再次出现在玩家眼前,生命剩下六十六天,故事继续从这里开始,他要在这剩余的时间里,寻找这个江湖和自己的结局。就是不知道,这多出的2天,对魂,对梁其伟,对灵游坊,具体意味着什么。
03
如果只能用一个词形容《影之刃零》,那可能是从独立游戏时期就一直继承下来的“武侠”。
今年7月,《影之刃零》在北京首钢园举行了线下试玩会,36氪游戏也在现场体验了这款国单3A的标杆作品。从游戏玩法、背景设定和战斗机制等方面综合来看,《影之刃零》都有着继《黑神话:悟空》之后第二款标杆级3A游戏的潜力。
这既不是一款动作模组简单低速的魂类游戏,也不是鬼泣那样的高速ACT,而是结合了两者优点的一款ARPG——它想为玩家提供的,是一种属于中国武侠韵律的战斗体验。
而在今天的TGA年度颁奖典礼上,《影之刃零》发布的全新的PV视频,也充斥着“暗黑武侠”的风格。
故事从主角的一段回忆展开,怀抱着婴儿的覆面人出现,一边保护手中的孩子一边抵御一群黑衣人的追杀。随后主角惊醒,乘坐着马车驶向远方。在之后的片段中,也能了解到主角被追杀的现况和一位女性NPC。在PV的后端,也出现了形似在《血雨》系列中就出现过多次的角色冷荼。
![]()
《影之刃零》PV
在战斗方面,新PV中也展示了众多新BOSS战以及武器招式。比如在之前的预告中就出现过的舞狮,以跳桩的方式出场,融合中国武术美学和暗黑风格。同时,在PV中也展示了一些新的战斗机制,比如在其中一个BOSS向主角攻击,丢出锁链后,主角可以抓住锁链,并通过连续按击按键完成对BOSS的反击,形式可能类似《怪猎荒野》中大剑的角力。
![]()
《影之刃零》PV
在另一些场景中,主角则可以通过引发落石、爆桶对敌人造成伤害。而在PV中,也展现了如长枪、大锤之类的新武器。
在最新上架的Steam页面中,也介绍到《影之刃零》借助了最新的动作捕捉技术,游戏的动作指导是甄子丹“甄家班”的的谷垣健治,曾经担任过《卧龙:苍天陨落》的董总指导。
此前在36氪游戏与制作人梁其伟的交流过程中,我们就有感受到,《影之刃零》团队对中国传统武侠和功夫文化进行了长期且深入的研究,以求更好还原每个角色动作、个性,以及那个血雨腥风的江湖时代。
而在实机体验中,角色的高速动作设计确已与中国武术做了融合,游玩起来像功夫大片一样充满视觉观赏性。结合黄金年代的武侠电影和朋克幻想元素,冷兵器、义肢、武侠秘笈,共同构筑起了《影之刃零》的世界。
![]()
《影之刃零》
在《雨血》以及《影之刃零》中,主角要在这剩余六十六天的生命中,找回自己的内心,这个故事似乎也能和灵游坊的发展形成一些互文,他们做了六年单机,七年手游,最后回归单机。他们经历了一开始对独立游戏的热情,也经历过手游时期为了赚钱导致过度商业化,而在《影之刃零》中,他们最后还是想讲一个有头有尾的好故事。
但可能与游戏不同的是,他们最后找回的东西也许也改变了,《影之刃零》还是一款需要赚钱的商业游戏,他们还是需要考虑市场,考虑赚钱。
不过,「做出一款好游戏」依然是灵游坊《影之刃零》团队的核心目标,或许我们能抱着这样的期待,等待《影之刃零》发售的那天。
![]()
《影之刃零》
本文首发自“36氪游戏”
告别字体闪烁 / 首屏卡顿!preload 让关键资源 “高优先级” 提前到
⚡️ 浏览器“未卜先知”的秘密:资源提示符,让你的页面加载速度快人一步!
前端性能优化专栏 - 第四篇
在前端性能优化的战场上,时间就是金钱,尤其是在页面加载的关键时刻。我们上一篇讲到 PerformanceObserver 可以精准地测量性能,但测量只是第一步,更重要的是主动出击,让浏览器在用户需要资源之前,就提前做好准备。
今天,我们就来揭秘浏览器“未卜先知”的秘密武器——资源提示符(Resource Hints) 。
💡 什么是资源提示符?
资源提示符(Resource Hints)是 <link> 标签 rel 属性的一组特殊值,用于告诉浏览器未来即将发生的资源处理策略,让它提前做准备。
简单来说,它们是开发者给浏览器下达的“预处理指令”,让浏览器在空闲或关键时刻,提前完成一些耗时的网络操作,从而:
- 提高网页的首屏加载性能
- 减少 DNS、TCP、TLS 等连接延迟
- 预加载关键或预测性资源
<!-- 资源提示符示例 -->
<link rel="preconnect" href="//cdn.example.com">
🔧 四大金刚:资源提示符的家族成员
资源提示符家族主要有四个核心成员,它们各有神通,针对不同的优化场景:
1. dns-prefetch:最小开销的“打听”
<link rel="dns-prefetch" href="//api.example.com">
-
作用: 仅提前解析 DNS,将域名解析为 IP 地址,不建立连接。
-
开销: 最小,兼容性最好。
-
使用场景:
- 非关键的第三方资源(如分析脚本、广告、插件)。
- 可作为
preconnect的降级方案。
专业名词解释:DNS 解析 DNS(Domain Name System)解析是将人类可读的域名(如
www.google.com)转换为机器可读的 IP 地址(如142.250.190.14)的过程。这是一个网络请求的起点,通常需要几十到几百毫秒。
2. preconnect:提前握手的“老朋友”
<link rel="preconnect" href="//cdn.example.com" crossorigin>
-
作用: 完成 DNS 解析 + TCP 握手 + TLS 加密握手,全流程建立连接。
-
效果: 极大地消除了后续资源请求的网络延迟。
-
使用时机:
- 字体库、核心 API、CDN 静态资源等关键第三方域名。
- 注意: 建立连接会消耗资源,建议控制数量(一般建议 ≤6 个)。
![]()
3. preload:高优先级的“快递”
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin>
-
作用: 直接以高优先级下载关键资源,但下载后暂不执行。
-
特点: 提前触发关键资源的加载,确保资源在需要时立即可用。
-
常见场景:
- CSS 定义的字体文件(避免文本闪烁 FOUT/FOIT)。
- 背景图或 LCP 元素图片(加速最大内容绘制)。
- 首屏必需的动态脚本。
注意:
preload必须配合as属性指定资源类型,否则浏览器会重复下载。
4. prefetch:空闲时的“下一站”
<link rel="prefetch" href="next-page.js">
-
作用: 在当前页加载完成后,利用浏览器空闲时间请求资源。
-
特点: 优先级最低,不会与当前页面的关键资源竞争带宽。
-
使用场景:
- 优化“下一个页面”的加载体验。
-
SPA 路由中,预取用户可能访问的下一个
chunk。 - 基于用户行为预测的预加载。
💡 总结:让资源“早一步”准备好
资源提示符家族的目标一致:让资源“早一步”准备好。
它们的核心区别在于时机与深度:
| 提示符 | 深度(提前到哪一步) | 时机(何时触发) | 优先级 | 适用场景 |
|---|---|---|---|---|
| dns-prefetch | 仅 DNS 解析 | 尽早 | 低 | 非关键第三方资源 |
| preconnect | DNS + TCP + TLS | 尽早 | 中 | 关键第三方域名 |
| preload | 下载资源 | 尽早(高优先级) | 高 | 当前页面的关键资源 |
| prefetch | 下载资源 | 页面空闲时 | 最低 | 下一个页面的资源 |
![]()
重要提醒: 资源提示符虽好,但过度使用可能导致浪费带宽或建立过多连接,反而拖慢性能。请务必根据实际的性能数据(比如 RUM 采集的数据)来合理规划和使用。
下一篇预告: 既然资源都提前加载了,如何让它们在下次访问时更快出现呢?下一篇我们将深入探讨前端性能优化的“节流大师”——HTTP 缓存机制。敬请期待!
性能数据别再瞎轮询了!PerformanceObserver 异步捕获 LCP/CLS,不卡主线程
🚀 性能监控的“最强大脑”:PerformanceObserver API,如何让你告别轮询的噩梦?
前端性能优化专栏 - 第三篇
在上一篇中,我们聊到了 RUM(真实用户监控)是如何帮助我们打破“薛定谔的 Bug”魔咒的。既然 RUM 是性能监控的“雷达”,那么谁来负责实时、精准地采集数据呢?
答案就是今天的主角——PerformanceObserver API。它就像是浏览器内置的“高性能数据采集器”,彻底改变了我们获取性能数据的方式。
⚠️ 为什么需要 PerformanceObserver?告别“老黄历”
在 PerformanceObserver 出现之前,我们获取性能数据的方式,简直就是一场“噩梦”:
传统方式:性能监控的“老黄历”
-
performance.timing与performance.getEntries():- 问题: 这些 API 只能获取页面加载完成那一刻的静态数据。对于像 First Input Delay (FID) 这种发生在用户交互过程中的动态指标,它们就无能为力了。
- 痛点: 想要获取实时数据?你只能轮询(不断地去问:“数据好了吗?好了吗?”)。这种方式不仅时机难以掌握,还会带来额外的性能开销,甚至可能阻塞主线程,让页面更卡!
专业名词解释:轮询 (Polling) 轮询是一种计算机通信技术,指客户端程序或设备不断地向服务器程序或设备发送请求,以查询是否有新的数据或状态更新。在前端性能监控中,轮询意味着需要定时检查性能数据是否生成,效率低下且消耗资源。
✨ 优化方案:事件驱动的“高性能引擎”
PerformanceObserver 的出现,彻底解决了轮询的痛点。它提供了一种事件驱动、异步回调的机制:
- 高效、非阻塞: 它在浏览器记录到性能事件时,会异步通知你,不会阻塞主线程。
- 实时性: 能够实时捕获动态指标,如用户首次输入延迟(FID)和布局偏移(CLS)。
- 可订阅: 你可以像订阅报纸一样,选择你感兴趣的性能事件类型。
🔄 PerformanceObserver 的工作原理:三步走战略
PerformanceObserver 的使用流程非常简洁,可以概括为“创建、指定、接收”三步走战略:
步骤 1:创建观测器(Observer)
首先,我们需要创建一个 PerformanceObserver 实例,并传入一个回调函数 (callback) 。
const observer = new PerformanceObserver((list) => {
// 浏览器在记录到性能条目时,会自动异步触发这个回调函数
// list.getEntries() 包含了所有被观测到的性能数据
})
工作原理揭秘: 浏览器在内部记录性能数据时,会检查是否有 PerformanceObserver 在监听。如果有,它就会将最新的性能条目(Performance Entry)打包,并在下一个空闲时机(异步)调用你提供的回调函数。
步骤 2:指定观测目标(Observe)
创建好观测器后,你需要明确告诉它:“我想看哪些数据? ” 这通过 observer.observe() 方法实现,你需要指定一个或多个 entryTypes。
observer.observe({
entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift']
})
常见的核心观测指标:
entryType |
对应指标 | 含义 |
|---|---|---|
largest-contentful-paint |
LCP | 最大内容绘制时间,衡量加载速度。 |
first-input |
FID | 首次输入延迟,衡量交互响应速度。 |
layout-shift |
CLS | 累积布局偏移,衡量视觉稳定性。 |
resource |
Resource Timing | 资源加载(图片、CSS、JS)的详细耗时。 |
![]()
步骤 3:接收和处理数据(Callback)
在回调函数中,你可以通过 list.getEntries() 获取到所有新产生的性能条目。每个条目(Entry)都是一个包含详细信息的对象。
示例:基础用法
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('指标名称:', entry.name)
console.log('开始时间:', entry.startTime)
console.log('持续时间:', entry.duration)
// 针对不同指标进行特殊处理,例如获取 CLS 的具体值
if (entry.entryType === 'layout-shift') {
console.log('CLS 值:', entry.value)
}
// 在这里将数据上报到 RUM 服务器
}
})
observer.observe({
entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift']
})
总结:PerformanceObserver 的核心优势
PerformanceObserver 是前端性能监控领域的一次重大飞跃,它的核心优势在于:
- 实时性: 事件驱动,性能数据一产生就能被捕获,无需低效的轮询。
- 低开销: 异步执行,不占用主线程资源,对用户体验影响极小。
-
可扩展: 通过
entryTypes,可以轻松订阅未来浏览器新增的各种性能事件。 - 易集成: 它是现代 RUM 监控体系中,最核心、最可靠的数据采集组件。
结论: PerformanceObserver 是构建前端性能可观测性的核心组件,它让我们从“猜测性能”迈向了 “数据驱动的性能优化” ,让性能数据采集变得高效、优雅。
下一篇预告: 既然我们能精准地测量性能了,下一步就是如何主动出击,让浏览器提前加载资源。下一篇我们将深入讲解前端性能优化的“预加载神器”——浏览器资源提示符。敬请期待!
GDAL 读取KML数据
前言
❝
KML是一种基于XML的地理数据格式,最初有Keyhole公司开发,后来被OGC标准。在GIS开发中,属于一种重要的数据格式,使用GDAL读取KML数据,有助于认识、了解KML数据结构与特点,从而提高开发效率。
本篇教程在之前一系列文章的基础上讲解
- GDAL 简介[1]
- GDAL 下载安装[2]
- GDAL 开发起步[3]
如果你还没有看过,建议从以上内容开始。
1. 开发环境
本文使用如下开发环境,以供参考。
时间:2025年
系统:Windows 11
Python:3.11.7
GDAL:3.11.1
2. 导入依赖
KML作为一种矢量数据格式,可以使用GDAL直接读取或者使用其矢量库OGR进行处理,以实现KML图层和属性数据读取。
from osgeo import ogr,gdal
import os
3. 读取KML数据
(一)使用GDAL读取
定义一个方法ReadKMLOfGDAL(kmlPath)用于读取KML数据,其中kmlPath为数据路径。在读取KML数据之前,需要检查数据路径是否存在。
# 检查文件是否存在
if os.path.exists(kmlPath):
print("文件存在")
else:
print("文件不存在,请重新选择文件!")
return
若KML数据路径正确,则可以使用OpenEx方法打开KML文件。需要判断KML数据集是否正常,若无法打开,则退出数据读取程序。
# 打开KML文件
dataset = gdal.OpenEx(kmlPath)
if dataset is None:
print("KML 文件打开异常,请检查文件路径!")
return
通过数据集方法GetLayerCount可以获取图层数量。
# 获取图层数量
layerCount = dataset.GetLayerCount()
print(f"图层数量:{layerCount}")
图层数量信息显示如下:![]()
之后通过遍历图层获取图层字段数量、字段名称以及字段类型等信息,在输出结果中读取要素属性信息和几何对象并限制要素输出数量。
# 遍历图层
for i in range(layerCount):
print(f"################开始打印第【{i+1}】个图层################n")
# 根据索引获取目标图层
layer = dataset.GetLayerByIndex(i)
# 获取图层名称
layerName = layer.GetName()
# 获取图层要素数量
layerFeatureCount = layer.GetFeatureCount()
print(f"图层名称:{layerName}")
print(f"要素数量:{layerFeatureCount}")
# 获取图层属性
layerProperty = layer.GetLayerDefn()
# 获取图层字段数量
fieldCount = layerProperty.GetFieldCount()
print(f"字段数量:{fieldCount}")
# 获取字段信息
for j in range(fieldCount):
# 获取字段属性对象
fieldProperty = layerProperty.GetFieldDefn(j)
# 获取字段属性名称
fieldName = fieldProperty.GetName()
# 获取字段属性类型
fieldType = fieldProperty.GetTypeName()
print(f"第 【{j}】 个字段名称:{fieldName},字段类型:{fieldType}")
# 获取要素
feature = layer.GetNextFeature()
limitCount = 0
# 限制打印前十个要素
while feature and limitCount < 10:
print(f"打印第【{limitCount+1}】个要素")
# print(f"打印要素类型:{type(feature)},{feature}")
# 读取要素属性
for k in range(fieldCount):
# 属性字段名
fieldName = layerProperty.GetFieldDefn(j).GetName()
# 属性字段值
fieldValue = feature.GetField(k)
# fieldValue = feature.GetField(fieldName)
print(f"第 【{k}】 个字段名:{fieldName},字段值:{fieldValue}")
# 读取几何属性
geom = feature.GetGeometryRef()
if geom:
# 获取几何类型
geomType = geom.GetGeometryName()
# 获取WKT格式几何对象,打印前100个字符
geomWKT = geom.ExportToWkt()[:100]
print(f"第 【{limitCount}】 个几何对象类型:{geomType},几何对象:{geomWKT}")
feature = layer.GetNextFeature()
limitCount += 1
# 重置读取位置
layer.ResetReading()
print(f"n################结束打印第【{i+1}】个图层################n")
图层要素属性信息显示如下:![]()
(二)使用OGR读取
定义一个方法ReadKMLOfOGR(kmlPath)用于读取KML数据,其中kmlPath为数据路径。在读取KML数据之前,需要检查数据路径是否存在。
# 检查文件是否存在
if os.path.exists(kmlPath):
print("文件存在")
else:
print("文件不存在,请重新选择文件!")
return
若KML数据路径正确,则可以注册KML数据驱动用于读取KML数据,如使用RegisterAll方法注册所有矢量驱动。然后调用ogr对象Open方法打开KML数据源,若其不存在,则退出数据读取程序。
# 注册所有驱动
ogr.RegisterAll()
# 打开KML数据源
dataSource = ogr.Open(kmlPath)
# 检查数据源是否正常
if dataSource is None:
print("文件打开出错,请重新选择文件!")
return
之后通过遍历图层获取图层空间参考、字段名称以及字段类型等信息,在输出结果中读取要素属性信息。
# 遍历图层
for i in range(dataSource.GetLayerCount()):
# 根据索引获取目标图层
layer = dataSource.GetLayer(i)
# 获取图层名称
layerName = layer.GetName()
print(f"第【{i}】个图层名称:{layerName}")
# 获取空间参考
spatialReference = layer.GetSpatialRef()
if spatialReference:
print(f"空间参考:{spatialReference.GetName()}")
else:
print(f"图层【{layerName}】空间参考不存在")
# 读取几何属性
for feature in layer:
# 读取几何属性
geom = feature.GetGeometryRef()
if geom:
# 获取四至范围
envelope = geom.GetEnvelope()
print(f"几何范围:{envelope}")
# 读取要素属性
for field in feature.keys():
# 获取属性字段值
fieldValue = feature.GetField(field)
print(f"属性字段名称:{field},属性字段值:{fieldValue}")
# 关闭数据源
dataSource = None
图层要素属性信息显示如下:![]()
4. 注意事项
注1:数据路径读取异常
在windows系统中建议使用"\"定义数据路径。
注2:中文数据读取异常(中文乱码)
在GIS开发中,涉及属性数据读取时经常会遇到中文乱码问题,需要根据图层编码设置正确的字符集。
# 设置Shapefile的编码为GBK
os.environ['SHAPE_ENCODING'] = "GBK"
注3:代码运行异常
需要开启代码异常处理
# 启用异常处理(推荐)
ogr.UseExceptions()
注4:坐标读取异常
在读取坐标参考时报错已安装PostgreSQL数据库中的投影文件版本与GDAL中的投影文件不兼容,此时需要为GDAL单独指定投影文件,在代码开头添加以下代码指定目标投影文件路径。
# 找到proj文件路径
os.environ['PROJ_LIB'] = r'D:\Programs\Python\Python311\Lib\site-packages\osgeo\data\proj'
5. 完整代码
from osgeo import ogr,gdal
import os
# 如果是通过 pip 安装的,可能需要找到对应位置
os.environ['PROJ_LIB'] = r'D:ProgramsPythonPython311Libsite-packagesosgeodataproj'
# 设置Shapefile的编码为GBK
os.environ['SHAPE_ENCODING'] = "GBK"
# 启用异常处理(推荐)
ogr.UseExceptions()
# 注册所有驱动
ogr.RegisterAll()
"""
使用GDAL读取KML数据
"""
def ReadKMLOfGDAL(kmlPath):
# 检查文件是否存在
if os.path.exists(kmlPath):
print("文件存在")
else:
print("文件不存在,请重新选择文件!")
return
# 打开KML文件
dataset = gdal.OpenEx(kmlPath)
if dataset is None:
print("KML 文件打开异常,请检查文件路径!")
return
# 获取图层数量
layerCount = dataset.GetLayerCount()
print(f"图层数量:{layerCount}")
# 遍历图层
for i in range(layerCount):
print(f"################开始打印第【{i+1}】个图层################n")
# 根据索引获取目标图层
layer = dataset.GetLayerByIndex(i)
# 获取图层名称
layerName = layer.GetName()
# 获取图层要素数量
layerFeatureCount = layer.GetFeatureCount()
print(f"图层名称:{layerName}")
print(f"要素数量:{layerFeatureCount}")
# 获取图层属性
layerProperty = layer.GetLayerDefn()
# 获取图层字段数量
fieldCount = layerProperty.GetFieldCount()
print(f"字段数量:{fieldCount}")
# 获取字段信息
for j in range(fieldCount):
# 获取字段属性对象
fieldProperty = layerProperty.GetFieldDefn(j)
# 获取字段属性名称
fieldName = fieldProperty.GetName()
# 获取字段属性类型
fieldType = fieldProperty.GetTypeName()
print(f"第 【{j}】 个字段名称:{fieldName},字段类型:{fieldType}")
# 获取要素
feature = layer.GetNextFeature()
limitCount = 0
# 限制打印前十个要素
while feature and limitCount < 10:
print(f"打印第【{limitCount+1}】个要素")
# print(f"打印要素类型:{type(feature)},{feature}")
# 读取要素属性
for k in range(fieldCount):
# 属性字段名
fieldName = layerProperty.GetFieldDefn(j).GetName()
# 属性字段值
fieldValue = feature.GetField(k)
# fieldValue = feature.GetField(fieldName)
print(f"第 【{k}】 个字段名:{fieldName},字段值:{fieldValue}")
# 读取几何属性
geom = feature.GetGeometryRef()
if geom:
# 获取几何类型
geomType = geom.GetGeometryName()
# 获取WKT格式几何对象,打印前100个字符
geomWKT = geom.ExportToWkt()[:100]
print(f"第 【{limitCount}】 个几何对象类型:{geomType},几何对象:{geomWKT}")
feature = layer.GetNextFeature()
limitCount += 1
# 重置读取位置
layer.ResetReading()
print(f"n################结束打印第【{i+1}】个图层################n")
"""
使用OGR读取KML数据
"""
def ReadKMLOfOGR(kmlPath):
# 检查文件是否存在
if os.path.exists(kmlPath):
print("文件存在")
else:
print("文件不存在,请重新选择文件!")
return
# 注册所有驱动
ogr.RegisterAll()
# 打开KML数据源
dataSource = ogr.Open(kmlPath)
# 检查数据源是否正常
if dataSource is None:
print("文件打开出错,请重新选择文件!")
return
# 遍历图层
for i in range(dataSource.GetLayerCount()):
# 根据索引获取目标图层
layer = dataSource.GetLayer(i)
# 获取图层名称
layerName = layer.GetName()
print(f"第【{i}】个图层名称:{layerName}")
# 获取空间参考
spatialReference = layer.GetSpatialRef()
if spatialReference:
print(f"空间参考:{spatialReference.GetName()}")
else:
print(f"图层【{layerName}】空间参考不存在")
# 读取几何属性
for feature in layer:
# 读取几何属性
geom = feature.GetGeometryRef()
if geom:
# 获取四至范围
envelope = geom.GetEnvelope()
print(f"几何范围:{envelope}")
# 读取要素属性
for field in feature.keys():
# 获取属性字段值
fieldValue = feature.GetField(field)
print(f"属性字段名称:{field},属性字段值:{fieldValue}")
# 关闭数据源
dataSource = None
if __name__ == "__main__":
# 数据路径
kmlPath = "E:\data\test_data\四姑娘山三峰.kml"
# GDAL读取KML数据
ReadKMLOfGDAL(kmlPath)
# OGR读取KML数据
ReadKMLOfOGR(kmlPath)
6. KML示例数据
<?xml version="1.0" encoding="utf-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2">
<Document>
<name>Track 登顶四姑娘山三峰 :wikiloc.com</name>
<visibility>1</visibility>
<LookAt>
<longitude>102.8793075</longitude>
<latitude>31.0426283</latitude>
<altitude>0</altitude>
<heading>3</heading>
<tilt>66</tilt>
<range>15000</range>
</LookAt>
<StyleMap id="m1367020">
<Pair>
<key>normal</key>
<styleUrl>#n1367020</styleUrl>
</Pair>
<Pair>
<key>highlight</key>
<styleUrl>#h1367020</styleUrl>
</Pair>
</StyleMap>
<Style id="h1367020">
<IconStyle>
<Icon>
<href>http://s1.wklcdn.com/wikiloc/images/pictograms/ge/1.png</href>
</Icon>
</IconStyle>
<BalloonStyle>
<text>$[description]</text>
</BalloonStyle>
</Style>
<Style id="lineStyle">
<LineStyle>
<color>f03399ff</color>
<width>4</width>
</LineStyle>
</Style>
<Style id="n1367020">
<LabelStyle>
<scale>0</scale>
</LabelStyle>
<BalloonStyle>
<text>$[description]</text>
</BalloonStyle>
<Icon>
<href>http://s1.wklcdn.com/wikiloc/images/pictograms/ge/1.png</href>
</Icon>
</Style>
<Style id="waypointStyle">
<IconStyle>
<Icon>
<href>http://sc.wklcdn.com/wikiloc/images/pictograms/ge/wpt.png</href>
</Icon>
</IconStyle>
<BalloonStyle>
<text>$[description]</text>
</BalloonStyle>
</Style>
<Folder>
<name>Trails</name>
<visibility>1</visibility>
<Folder>
<name>登顶四姑娘山三峰</name>
<visibility>1</visibility>
<Placemark>
<name>Path</name>
<visibility>1</visibility>
<LookAt>
<longitude>102.8862617</longitude>
<latitude>31.052715</latitude>
<altitude>0</altitude>
<heading>0</heading>
<tilt>0.00779126500014642</tilt>
<range>5250.96911517065</range>
</LookAt>
<Style>
<IconStyle>
<color>ffffffff</color>
<scale>1</scale>
<Icon>
<href/>
</Icon>
</IconStyle>
<LabelStyle>
<color>ffffffff</color>
<scale>1</scale>
</LabelStyle>
<LineStyle>
<color>f00000ff</color>
<width>4</width>
</LineStyle>
<PolyStyle>
<color>ffffffff</color>
<fill>1</fill>
<outline>1</outline>
</PolyStyle>
</Style>
<LineString>
<altitudeMode>clampToGround</altitudeMode>
<coordinates>
102.8527267,31.0061667,3255.400146
102.8530967,31.00604,3254.899902
102.8537967,31.0060883,3256.899902
102.8547817,31.0064133,3270.100098
102.8558183,31.0071067,3271.100098
102.8575333,31.00785,3271.699951
102.8588867,31.0093867,3278.899902
102.8599,31.0099067,3281.5
102.8605217,31.01093,3289.899902
102.8613217,31.0128967,3298.899902
102.863045,31.014905,3307.199951
102.8638983,31.016515,3313.100098
102.8639067,31.01642,3306.699951
102.86423,31.0168667,3317.199951
102.8645867,31.017765,3330.000244
102.8655283,31.0190083,3314.100342
102.86643,31.0211683,3324.100098
102.8665367,31.0217183,3321.300049
102.86754,31.0228467,3328.399902
102.8682333,31.023345,3331.699951
102.868495,31.02422,3338.399902
102.86873,31.0245367,3336.199951
102.8697533,31.0251667,3343.100098
102.870035,31.0256033,3345.800049
102.86997,31.02594,3350.099854
102.870195,31.0265117,3357.800049
102.8706917,31.0273617,3360.300049
102.8717183,31.0284717,3374
102.8735067,31.0298317,3377.699951
102.8744233,31.0310767,3382.300049
102.8748283,31.0321567,3378.699951
102.8747833,31.0328433,3391.800049
102.8756183,31.0336933,3406.399902
102.875455,31.034915,3408
102.8754967,31.0361467,3406.399902
102.8759333,31.037405,3412
102.8763117,31.0379283,3415.999756
102.87597,31.0385567,3416.199951
102.8757067,31.0415767,3399.100098
102.87552,31.0419067,3415.999756
102.8758433,31.0423217,3424.100098
102.8762517,31.0425117,3439.200195
102.8762617,31.04284,3444
102.8764567,31.0430117,3450.199951
102.8766917,31.0436783,3461.399902
102.8771717,31.0439417,3481.399902
102.876935,31.04407,3486.899902
102.8771133,31.04414,3494.399902
102.8772133,31.0444317,3502.300049
102.8782383,31.0450583,3541.100098
102.878835,31.045955,3559.100098
102.8790667,31.0470883,3574.699951
102.8792533,31.0472867,3574.5
102.8790733,31.04746,3574.199951
102.8791133,31.0475933,3575.300049
102.879595,31.0479917,3586
102.8803283,31.0490267,3626.399902
102.8804683,31.0489483,3627.600098
102.880595,31.049135,3626.800049
102.8807983,31.0491317,3629.199951
102.8807333,31.0493933,3629.800049
102.88088,31.04944,3629.100098
102.880855,31.049585,3628.699951
102.8811167,31.0496783,3629
102.8812417,31.049575,3629.600098
102.8814083,31.049755,3632.600098
102.881335,31.0500367,3634.5
102.8811333,31.0499417,3638.800049
102.88138,31.05021,3638.699951
102.8812683,31.0501417,3639
102.8813417,31.0499933,3637.499756
102.8813383,31.0501217,3642.600098
102.8822067,31.050155,3652.599854
102.8823317,31.050305,3655.699951
102.8827433,31.0501883,3663.399902
102.882945,31.0503983,3691
102.8835383,31.0504067,3708.600098
102.883635,31.0504717,3713.199707
102.88357,31.0509167,3720.699951
102.8834217,31.0509483,3723.000244
102.8837983,31.0511317,3728.600342
102.8841217,31.0509617,3733
102.8840783,31.0516483,3760.400146
102.8844567,31.0517517,3780.399902
102.8844183,31.0518767,3795.699951
102.884775,31.0518117,3818.499756
102.8848583,31.0522,3863
102.885575,31.051965,3896.800049
102.88583,31.05217,3908.600098
102.885545,31.0519417,3948.100098
102.88575,31.0519467,3951.500244
102.8857867,31.0521417,3960.899902
102.8861367,31.0522567,3973.300293
102.8862617,31.052715,3985.5
102.8865033,31.0528033,3996.699707
102.8865233,31.0531233,4007.399902
102.886855,31.053565,4025.600098
102.8878733,31.0542133,4081.300049
102.888465,31.0543383,4096.399902
102.8887633,31.05476,4105.5
102.8889883,31.0546883,4115.200195
102.8891233,31.0549117,4131
102.8893483,31.0548067,4143.200195
102.8900367,31.055275,4164.200195
102.8902983,31.0563283,4190.399902
102.8902633,31.0578033,4191.899902
102.890535,31.05789,4203.200195
102.89051,31.058235,4225.799805
102.8909267,31.0584983,4262.799805
102.8911817,31.05891,4273.899902
102.8913883,31.05877,4285
102.8913233,31.0584617,4289.399902
102.89199,31.0583817,4299
102.8919,31.058545,4308.200195
102.8920433,31.05873,4319.299805
102.8924917,31.05891,4352
102.8927133,31.0588033,4365.200195
102.8930267,31.059215,4373.200195
102.89327,31.0590433,4388.899902
102.8934967,31.0592717,4391.299805
102.8934583,31.0594417,4395.899902
102.8937567,31.0595283,4406.299805
102.8940683,31.0601267,4421
102.8943233,31.06027,4429.5
102.8943667,31.0605067,4435.600098
102.8941,31.0606483,4444
102.89444,31.0607917,4452.799805
102.89331,31.0618433,4485.899902
102.893345,31.061985,4489.799805
102.8938833,31.0621483,4498.399902
102.8937483,31.0619783,4499
102.89363,31.0620033,4499.399902
102.8937967,31.062175,4499.799805
102.8943467,31.0621867,4503.899902
102.8943433,31.062095,4504.700195
102.8943767,31.0622417,4504.5
102.8948533,31.062295,4503.600098
102.8957933,31.0629667,4506.299805
102.8959517,31.0628633,4506.399902
102.89649,31.0635683,4509.799805
102.8966483,31.063565,4509.399902
102.8967717,31.0639033,4511.600098
102.8974033,31.0641033,4518.100098
102.8982783,31.0652517,4530.399902
102.8985533,31.0661067,4556.299805
102.899115,31.0666583,4589.600098
102.8990783,31.0670983,4620.700195
102.8994317,31.0674483,4636
102.8997217,31.068335,4650.799805
102.9004533,31.0686783,4657.799805
102.90056,31.0690317,4672.100098
102.9008217,31.069215,4664.5
102.9005883,31.0696883,4677.399902
102.9007033,31.0700017,4692.100098
102.9013133,31.070325,4701.100098
102.9020567,31.0710117,4716.899902
102.902175,31.0713983,4738.899902
102.9026167,31.0719533,4748
102.903125,31.0721467,4758.299805
102.9036383,31.0726467,4757.299805
102.9035233,31.072715,4757.399902
102.9036517,31.0728533,4759.5
102.9047917,31.0735717,4823.5
102.905155,31.07431,4862.299805
102.9062583,31.0745867,4891.799805
102.9065483,31.07534,4962.100098
102.906415,31.075375,4966
102.906495,31.0755583,4993.700195
102.9062583,31.0755983,4994.899902
102.9066633,31.0755817,4990.700195
102.9064633,31.0757367,5003.000488
102.9069417,31.0759117,5031.500488
102.9069833,31.0760817,5034.899902
102.9068167,31.076175,5040.100098
102.9069583,31.0762483,5041.700195
102.9070367,31.0766883,5058.5
102.906675,31.0769033,5078.899902
102.906895,31.0768783,5081.200195
102.90672,31.0772267,5096.200195
102.9071467,31.0774933,5137.5
102.9072017,31.07771,5142.200195
102.90558,31.0791683,5322.200195
102.905505,31.0793567,5341.899902
102.905815,31.0797233,5358.100098
102.9054383,31.07938,5345.500488
102.9055167,31.07932,5349.5
102.90543,31.0794,5349.100098
</coordinates>
</LineString>
</Placemark>
</Folder>
</Folder>
</Document>
</kml>
❝
OpenLayers示例数据下载,请回复关键字:ol数据
全国信息化工程师-GIS 应用水平考试资料,请回复关键字:GIS考试
❝
【GIS之路】 已经接入了智能助手,欢迎关注,欢迎提问。
欢迎访问我的博客网站-长谈GIS:
http://shanhaitalk.com
都看到这了,不要忘记点赞、收藏 + 关注 哦 !
本号不定时更新有关 GIS开发 相关内容,欢迎关注 !
iOS SwiftUI 布局容器详解
SwiftUI 布局容器详解
SwiftUI 提供了多种布局容器,每种都有特定的用途和行为。以下是对主要布局容器的全面详解:
一、基础布局容器
1. VStack - 垂直堆栈
VStack(alignment: .leading, spacing: 10) {
Text("顶部")
Text("中部")
Text("底部")
}
- 功能:垂直排列子视图
-
参数:
-
alignment:水平对齐方式(.leading,.center,.trailing) -
spacing:子视图间距
-
- 布局特性:根据子视图大小决定自身高度
2. HStack - 水平堆栈
HStack(alignment: .top, spacing: 20) {
Text("左")
Text("中")
Text("右")
}
- 功能:水平排列子视图
-
参数:
-
alignment:垂直对齐方式(.top,.center,.bottom,.firstTextBaseline,.lastTextBaseline) -
spacing:子视图间距
-
3. ZStack - 重叠堆栈
ZStack(alignment: .topLeading) {
Rectangle()
.fill(Color.blue)
.frame(width: 200, height: 200)
Text("覆盖文本")
.foregroundColor(.white)
}
- 功能:子视图重叠排列
-
参数:
-
alignment:对齐方式,控制所有子视图的共同对齐点
-
- 渲染顺序:后添加的视图在上层
二、惰性布局容器(Lazy Containers)
4. LazyVStack - 惰性垂直堆栈
ScrollView {
LazyVStack(pinnedViews: .sectionHeaders) {
ForEach(0..<1000) { index in
Text("行 \(index)")
.frame(height: 50)
}
}
}
- 特性:仅渲染可见区域的视图,提高性能
-
参数:
-
pinnedViews:固定视图(.sectionHeaders,.sectionFooters)
-
- 使用场景:长列表,性能敏感场景
5. LazyHStack - 惰性水平堆栈
ScrollView(.horizontal) {
LazyHStack {
ForEach(0..<100) { index in
Text("列 \(index)")
.frame(width: 100)
}
}
}
6. LazyVGrid - 惰性垂直网格
let columns = [
GridItem(.fixed(100)),
GridItem(.flexible()),
GridItem(.adaptive(minimum: 50))
]
ScrollView {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(0..<100) { index in
Color.blue
.frame(height: 100)
.overlay(Text("\(index)"))
}
}
.padding()
}
-
GridItem类型:
-
.fixed(CGFloat):固定宽度 -
.flexible(minimum:, maximum:):灵活宽度 -
.adaptive(minimum:, maximum:):自适应,尽可能多放置
-
7. LazyHGrid - 惰性水平网格
let rows = [GridItem(.fixed(100)), GridItem(.fixed(100))]
ScrollView(.horizontal) {
LazyHGrid(rows: rows, spacing: 20) {
ForEach(0..<50) { index in
Color.red
.frame(width: 100)
}
}
}
三、特殊布局容器
8. ScrollView - 滚动视图
ScrollView(.vertical, showsIndicators: true) {
VStack {
ForEach(0..<50) { index in
Text("项目 \(index)")
.frame(maxWidth: .infinity)
.padding()
.background(Color.gray.opacity(0.2))
}
}
}
-
滚动方向:
.vertical,.horizontal -
参数:
-
showsIndicators:是否显示滚动条
-
9. List - 列表
List {
Section(header: Text("第一部分")) {
ForEach(1..<5) { index in
Text("行 \(index)")
}
}
Section(footer: Text("结束")) {
ForEach(5..<10) { index in
Text("行 \(index)")
}
}
}
.listStyle(.insetGrouped) // 多种样式可选
-
样式:
.plain,.grouped,.insetGrouped,.sidebar - 特性:自带滚动、优化性能、支持分节
10. Form - 表单
Form {
Section("个人信息") {
TextField("姓名", text: $name)
DatePicker("生日", selection: $birthday)
}
Section("设置") {
Toggle("通知", isOn: $notifications)
Slider(value: $volume, in: 0...1)
}
}
- 特性:自动适配平台样式,适合设置界面
11. NavigationStack (iOS 16+) - 导航栈
NavigationStack(path: $path) {
List {
NavigationLink("详情", value: "detail")
NavigationLink("设置", value: "settings")
}
.navigationDestination(for: String.self) { value in
switch value {
case "detail":
DetailView()
case "settings":
SettingsView()
default:
EmptyView()
}
}
}
12. TabView - 标签视图
TabView {
HomeView()
.tabItem {
Label("首页", systemImage: "house")
}
.tag(0)
ProfileView()
.tabItem {
Label("我的", systemImage: "person")
}
.tag(1)
}
.tabViewStyle(.automatic) // 或 .page(页面式)
13. Grid (iOS 16+) - 网格布局
Grid {
GridRow {
Text("姓名")
Text("年龄")
Text("城市")
}
.font(.headline)
Divider()
.gridCellUnsizedAxes(.horizontal)
GridRow {
Text("张三")
Text("25")
Text("北京")
}
}
四、布局辅助视图
14. Spacer - 间距器
HStack {
Text("左")
Spacer() // 将左右视图推向两端
Text("右")
}
VStack {
Text("顶部")
Spacer(minLength: 20) // 最小间距
Text("底部")
}
15. Divider - 分割线
VStack {
Text("上部分")
Divider() // 水平分割线
Text("下部分")
}
16. Group - 分组容器
VStack {
Group {
if condition {
Text("条件1")
} else {
Text("条件2")
}
}
.padding()
.background(Color.yellow)
}
-
作用:
- 突破10个子视图限制
- 统一应用修饰符
- 条件逻辑分组
17. ViewBuilder - 视图构建器
@ViewBuilder
func createView(showDetail: Bool) -> some View {
Text("基础")
if showDetail {
Text("详情")
Image(systemName: "star")
}
}
五、自定义布局容器
18. Layout 协议 (iOS 16+)
struct MyCustomLayout: Layout {
func sizeThatFits(
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) -> CGSize {
// 计算布局所需大小
CGSize(width: proposal.width ?? 300, height: 200)
}
func placeSubviews(
in bounds: CGRect,
proposal: ProposedViewSize,
subviews: Subviews,
cache: inout ()
) {
// 放置子视图
var point = bounds.origin
for subview in subviews {
subview.place(at: point, proposal: .unspecified)
point.x += 100
}
}
}
19. AnyLayout (iOS 16+)
@State private var isVertical = true
var body: some View {
let layout = isVertical ? AnyLayout(VStackLayout()) : AnyLayout(HStackLayout())
layout {
Text("视图1")
Text("视图2")
}
}
六、布局修饰符
20. frame - 尺寸约束
Text("Hello")
.frame(
maxWidth: .infinity, // 最大宽度
minHeight: 50, // 最小高度
alignment: .center // 对齐方式
)
21. padding - 内边距
Text("内容")
.padding() // 所有方向
.padding(.horizontal, 10) // 水平方向
.padding(.top, 20) // 顶部
.padding(EdgeInsets(top: 10, leading: 20, bottom: 10, trailing: 20))
22. position & offset - 位置调整
Text("绝对定位")
.position(x: 100, y: 100) // 相对于父视图
Text("相对偏移")
.offset(x: 10, y: -5) // 相对当前位置
七、布局优先级
23. 布局优先级
HStack {
Text("短文本")
.layoutPriority(1) // 高优先级,先分配空间
Text("这是一个非常长的文本,可能会被压缩")
.layoutPriority(0) // 低优先级
}
.frame(width: 200)
八、布局选择指南
| 容器 | 适用场景 | 性能特点 |
|---|---|---|
| VStack/HStack | 简单布局,子视图数量少 | 立即布局所有子视图 |
| LazyVStack/LazyHStack | 长列表,滚动视图 | 惰性加载,高性能 |
| List | 数据列表,需要交互 | 高度优化,支持选择、删除等 |
| Grid/LazyVGrid | 网格布局,瀑布流 | 灵活的多列布局 |
| ZStack | 重叠布局,层叠效果 | 适合覆盖、浮动元素 |
| ScrollView | 自定义滚动内容 | 需要手动管理性能 |
九、最佳实践
- 选择合适的容器:根据需求选择最合适的布局容器
- 避免过度嵌套:简化布局层级,提高性能
- 使用惰性容器:处理大量数据时使用Lazy容器
- 利用Spacer:灵活控制视图间距
- 组合使用:合理组合多个容器实现复杂布局
- 测试多尺寸:在不同设备尺寸和方向上测试布局
这些容器可以灵活组合使用,创造出各种复杂的用户界面。掌握这些布局容器的特性和适用场景,是成为SwiftUI布局专家的关键。
一次弹窗异常引发的思考:iOS present / push 底层机制全解析
这篇文章从一个真实线上问题讲起: 在弹窗VC 里点了一行cell,结果直接跳回了UITabBarController。 借着排查这个 Bug 的过程,我系统梳理了一遍 iOS 中与导航相关的底层机制:present/dismiss、push/pop、“获取顶层 VC(getTopVC)”、以及 UITableView 的选中/取消逻辑。
一、视图控制器层级:Navigation 栈 vs Modal 链
1. 两套完全独立的层级体系
Navigation 栈(push/pop)
- 结构:
UINavigationController.viewControllers = [VC0, VC1, VC2, ...] - 行为:
-
pushViewController::追加到数组尾部 -
popViewControllerAnimated::从数组尾部移除
-
- 只影响 导航栈 中的顺序,不改变谁
present了谁。
Modal 链(present/dismiss)
- 结构:由
presentingViewController/presentedViewController串联成一条链:A.presentedViewController = BB.presentedViewController = C
- 行为:
-
presentViewController::在当前 VC 上方展示一个新 VC -
dismissViewControllerAnimated::从某个 VC 开始,把它和它上面所有通过它present出来的 VC 一起收回
-
记忆方式:
- push/pop 操作的是 “数组”(导航栈)
- present/dismiss 操作的是 “链表”(模态链)
2. 组合层级的典型例子
A(Tab 内业务页)
└─ present → B(弹窗或二级页,带导航)
└─ push → C(B 的导航栈里再 push 出来的 VC)- 导航栈(以 B 的导航控制器为例):[B, C]
- 模态链:
A -(present)-> B
关键结论:
dismiss B ⇒ B 和 B 承载的那棵 VC 树一起消失 ⇒ 导航回到 A(B 的 presentingViewController)。
UIKit 不支持 “只 dismiss B 保留 C” 这种结构。
二、dismissViewControllerAnimated: 的真实含义
[vc dismissViewControllerAnimated:YES completion:nil];核心点:
- 这个调用作用在 “
vc所在的模态链” 上,而不是导航栈。 - 如果
vc是被某个 VC 通过presentViewController:推出来的,那么:- 系统会找到它的
presentingViewController - 把从
vc起到链尾的所有 VC 都 dismiss 掉 - 显示回到
presentingViewController
- 系统会找到它的
1. 谁调用 vs 谁被 dismiss
很多人容易混淆这两种写法:
[self dismissViewControllerAnimated:YES completion:nil];
[[self getTopVC] dismissViewControllerAnimated:YES completion:nil];
只要这两种写法最终作用到的是同一个 VC,它们的行为完全一致。
- 决定回到哪里的,是「被 dismiss 的那个 VC 的
presentingViewController」,而不是“谁来触发这次调用”。 - 这也是为什么单纯把
self改成[self getTopVC]并不能改变 dismiss 之后的落点。
2. presentingViewController 的生命周期
[parentVC presentViewController:childVC animated:YES completion:nil];
- 在这行代码执行完成时:
-
childVC.presentingViewController = parentVC被永久确定
-
- 后续不管从哪里、什么时候触发:
- 只要 dismiss 的对象是
childVC,最终都会回到同一个parentVC
- 只要 dismiss 的对象是
三、“顶层 VC” 工具(如 getTopVC)的时序问题
很多项目中都会有类似如下工具方法:
@implementation UIViewController(Additions)
- (UIViewController*)getTopVC {
if (self.presentedViewController) {
return [self.presentedViewController getTopVC];
}
if ([self isKindOfClass:UITabBarController.class]) {
return [[(UITabBarController*)self selectedViewController] getTopVC];
}
else if ([self isKindOfClass:UINavigationController.class]) {
return [[(UINavigationController*)self visibleViewController] getTopVC];
}
return self;
}
@end
@implementation UIApplication (Additions)
+ (UIViewController *)getCurrentTopVC{
UIViewController *currentVC = [UIApplication sharedApplication].delegate.window.rootViewController;
return [currentVC getTopVC];
}
@end
关键:这类函数对「调用时机」极度敏感。
情况 1:在“弹窗 VC 还在屏幕上”时调用
比如某个present出来的弹窗 VC 还没有被 dismiss,这时调用 getTopVC(),返回的就是这个弹窗 VC。
情况 2:在“弹窗 VC 已经被 dismiss 掉”之后调用
当 弹窗 VC 已经执行过 dismissViewControllerAnimated:,不再显示在屏幕上,这时再调用 getTopVC(),返回的就是它下面那一层控制器(例如列表页、TabBar 下当前选中的子控制器),而不再是 弹窗 VC 本身。
情况3: 一个典型的 Bug 时序
- 子类在 cell 点击时,调了父类的
didSelectRowAtIndexPath: - 父类内部逻辑(伪代码):
[self dismissViewControllerAnimated:YES completion:^{
if (self.didSelectedIndex) {
self.didSelectedIndex(indexPath.row); // 触发外层 block
}
}];
也就是说:先 dismiss 自己,再回调外层 block
- 外层 block 中再执行:
[[UIApplication getCurrentTopVC] dismissViewControllerAnimated:YES completion:nil];
由于这时 弹框VC 已经被 dismiss 掉,getCurrentTopVC() 拿到的是 下层 VC(例如一个筛选页或 TabBar)
于是第二次 dismiss 把下层页面也关掉了
4. 用户看到的效果就是:
点击弹窗里的一个 cell ⇒ 弹窗消失 ⇒ 当前页面也被关闭 ⇒ 直接回到了 TabBar
根本原因:
第二次调用 getTopVC() 的时机太晚,此时“顶层 VC”已经不是弹窗,而是它下面的页面。
四、UITableView 的选中/取消逻辑
1. 系统接口的作用
-
selectRowAtIndexPath:animated:scrollPosition:会:- 更新 tableView 内部的选中状态;
- 调用 cell 的
setSelected:YES; - 触发
tableView:didSelectRowAtIndexPath:回调。
-
deselectRowAtIndexPath:animated:会:- 清除选中状态;
- 调用 cell 的
setSelected:NO; - 触发
tableView:didDeselectRowAtIndexPath:回调。
也就是说,单靠 deselectRowAtIndexPath:,就已经隐含执行了很多事情,不必再额外手工写 cell.selected = NO。
2. 单选列表中的推荐顺序
UITableView 在单选模式下,用户点一个新 row 时,系统内部的默认顺序是:先调用 didDeselectRowAtIndexPath:(旧 row)→ 再调用 didSelectRowAtIndexPath:(新 row)。 所以在自定义 cell 中的选中/取消逻辑时, 推荐顺序调用这2个方法
例如: 你维护了一个 selectedIndex,在代码中手动切换选中行时,可以这样写:
NSIndexPath *oldIndexPath = [NSIndexPath indexPathForRow:self.selectedIndex inSection:0];
NSIndexPath *newIndexPath = indexPath;
// 1. 先取消旧的
[tableView deselectRowAtIndexPath:oldIndexPath animated:YES];
// 2. 再选中新的
[tableView selectRowAtIndexPath:newIndexPath
animated:YES
scrollPosition:UITableViewScrollPositionNone];这样能确保:
- 旧 cell 的
setSelected:NO/didDeselect逻辑先执行; - 新 cell 的
setSelected:YES/didSelect后执行; - 对自定义 cell(在
setSelected:里更换图标、颜色等)尤为友好; - 不会出现两个 cell 同时高亮的瞬间状态。
3. 何时可以不再调用父类 tableView:didDeselectRowAtIndexPath:
如果父类的 tableView:didDeselectRowAtIndexPath: 实现只是:
-
(void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; [cell setSelected:NO]; }, 而你在子类里已经调用了deselectRowAtIndexPath:animated:,那么: -
系统内部已经帮你执行了
setSelected:NO; -
再手动调用父类
didDeselectRowAtIndexPath:属于重复操作,可以安全省略。
五、“到 C 后不能回 B”:通过修改导航栈实现
用户需求
当前导航栈:A -> B -> C
期望: 在C上点击返回时直接回到 A,不能再回到 B。
代码实现
-- push 到新 VC,并从栈中移除当前 VC
-- 修改 `viewControllers` 数组, 重置导航栈
- (void)deleteCurrentVCAndPush:(UIViewController *)viewController animated:(BOOL)animated {
UIViewController* top = self.topViewController;
[self pushViewController:viewController animated:animated];
NSMutableArray* viewControllers = [self.viewControllers mutableCopy];
[viewControllers removeObject:top];
[self setViewControllers:viewControllers animated:NO];
}
与 dismiss 的区别
- 修改导航栈:
- 仅操作
navigationController.viewControllers数组; - 不改变 modal 链,
presentingViewController关系保持不变;
- 仅操作
- dismiss 某个 VC:
- 只看 modal 链;
- 会回到
presentingViewController; - 无法仅移除 B 而让 C 留在界面上。
六、整体总结
-
理解 Navigation 栈与 Modal 链是所有导航问题的基础:
- push/pop 只改数组
- present/dismiss 只改链表
-
dismissViewControllerAnimated:的返回点由presentingViewController决定:- 谁调用不重要,谁被 dismiss 才重要。
-
“获取顶层 VC” 的工具对调用时机非常敏感:
- 在 VC 被 dismiss 前后调用,返回的完全是不同的对象;
- 在错误的时机用它再发起一次 dismiss,往往会“多退一层”。
-
手动控制 UITableView 的选中状态时,优先使用 select/deselect 接口,并保持“先取消旧选中,再选中新行”的顺序。
-
“到 C 后不能回 B”这类需求,本质是对导航栈的重写,而非 dismiss 某个 VC:
- 正确做法是修改
viewControllers数组,或使用封装好的 “deleteCurrentVCAndPush” 类方法。
- 正确做法是修改
掌握这些底层规则,遇到类似“弹窗关闭顺序错乱”、“页面一点击就跳回根控制器”、“导航上跳过某一层”等问题时,就能更快定位根因,设计出行为可控、易维护的解决方案。
iOS逆向-哔哩哔哩增加3倍速播放(2)-[横屏视频-半屏播放]增加3倍速播放
前言
作为一名 哔哩哔哩的重度用户,我一直期待官方推出 3 倍速播放 功能。然而等了许久,这个功能始终没有上线 😮💨。
修改前效果:
![]()
刚好我自己熟悉 iOS 逆向工程,于是决定 亲自动手,为 B 站加入 3 倍速播放 😆。
修改后效果:
![]()
由于整个过程涉及 多处逻辑修改与多个模块的反汇编分析,为了让内容更加清晰易读,我将会分成多篇文章,逐步拆解 如何为 B 站增加 3 倍速播放能力。
场景
[横屏视频-半屏播放]的播放页面
![]()
开发环境
-
哔哩哔哩版本:
8.41.0 -
IDA Professional 9.0 -
安装IDA插件:
patching -
Lookin
目标
[横屏视频-半屏播放]增加三倍速播放
分析
- 从
Lookin可以知道,播放速度组件叫做VKSettingView.SelectContent
![]()
- 从
Mach-O文件导出的VKSettingView.SelectContent的swift文件可以知道,它的model叫做VKSettingView.SelectModel
class VKSettingView.SelectContent: VKSettingView.TitleBaseContent {
/* fields */
var model: VKSettingView.SelectModel ?
var lazy selecter: VKSettingView.VKSelectControl ?
}
-
VKSettingView.SelectModel有个items属性,有可能是播放速度数组。我们从IDA依次查看方法的实现,找到items的setter方法叫做sub_10D8ACB88
import Foundation
class VKSettingView.SelectModel: VKSettingView.BaseModel {
/* fields */
var icon: String
var items: [String]
var reports: [String]
var selectedIndex: Int
var dynamicSelectedString: String?
var enableRepeatSelect: Swift.Bool
var selectChangeCallback: ((_:_:))?
var preferScrollPosition: VKSettingView.VKSelectControlScrollPosition
/* methods */
func sub_10d8aca08 // getter (instance)
func sub_10d8acac4 // setter (instance)
func sub_10d8acb20 // modify (instance)
func sub_10d8acb70 // getter (instance)
func sub_10d8acb88 // setter (instance)
func sub_10d8acb94 // modify (instance)
func sub_10d8acc48 // getter (instance)
func sub_10d8acd10 // setter (instance)
func sub_10d8acd68 // modify (instance)
func sub_10d8acf6c // getter (instance)
func sub_10d8acff8 // setter (instance)
func sub_10d8ad040 // modify (instance)
func sub_10d8ad138 // getter (instance)
func sub_10d8ad234 // setter (instance)
func sub_10d8ad2a0 // modify (instance)
func sub_10d8ad328 // getter (instance)
func sub_10d8ad3b4 // setter (instance)
func sub_10d8ad3fc // modify (instance)
}
![]()
- 我们在
Xcode添加符号断点sub_10D8ACB88,看到底谁设置了items的值
![]()
-
sub_10D8ACB88断点触发,我们打印参数的值,证明items确实是播放速度数组
(lldb) p (id)$x0
(_TtGCs23_ContiguousArrayStorageSS_$ *) 0x00000001179c8370
(lldb) expr -l Swift -- unsafeBitCast(0x00000001179c8370, to: Array<String>.self)
([String]) $R4 = 6 values {
[0] = "0.5"
[1] = "0.75"
[2] = "1.0"
[3] = "1.25"
[4] = "1.5"
[5] = "2.0"
}
- 我们打印方法的调用堆栈,发现是
sub_10A993E14修改了items的值
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
* frame #0: 0x000000010de78b88 bili-universal`sub_10D8ACB88
frame #1: 0x000000010af5fea0 bili-universal`sub_10A993E14 + 140
frame #2: 0x000000010af5f15c bili-universal`sub_10A992320 + 3644
frame #3: 0x000000010af5db20 bili-universal`sub_10A9916B4 + 1132
frame #4: 0x000000010af6714c bili-universal`sub_10A99B130 + 28
frame #5: 0x000000010af6859c bili-universal`sub_10A99C1A0 + 1020
frame #6: 0x000000010af67128 bili-universal`sub_10A99B118 + 16
...
- 我们从
IDA看下sub_10A993E14的伪代码实现
_QWORD *__fastcall sub_10A993E14(void *a1, id a2)
{
...
v3 = a2;
if ( a2 && (v4 = v2, v6 = type metadata accessor for SelectModel(0LL), (v7 = swift_dynamicCastClass(v3, v6)) != 0) )
{
v9 = (_QWORD *)v7;
v10 = sub_107C8B79C(&unk_116BB42E8, v8);
inited = swift_initStaticObject(v10, &unk_116E60370);
v12 = *(void (__fastcall **)(__int64))((swift_isaMask & *v9) + 0x1C0LL);
v13 = objc_retain(v3);
...
- 我们直接搜索
sub_10A993E14的伪代码,看是否有直接调用sub_10D8ACB88,很遗憾并没有 - 我们添加
sub_10A993E14符号断点,断点触发后打印方法的参数,发现x1的值是_TtC13VKSettingView11SelectModel,也就是VKSettingView.SelectModel
(lldb) p (id)$x0
(BAPIPlayersharedSettingItem *) 0x0000000282c0f880
(lldb) p (id)$x1
(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790
- 我们打印
x1(VKSettingView.SelectModel)(0x0000000283b51790)的items的值,发现是个空数组
(lldb) p (id)$x1
(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790
(lldb) p [(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790 items]
(_TtCs19__EmptyArrayStorage *) 0x00000001dd92e978
(lldb) expr -l Swift -- unsafeBitCast(0x00000001dd92e978, to: Array<String>.self)
([String]) $R2 = 0 values {}
- 我们在
sub_10A993E14方法返回之前添加一个断点,看下x1(VKSettingView.SelectModel)(0x0000000283b51790)的items的值
![]()
(lldb) register read
General Purpose Registers:
x0 = 0x0000000283b51790
x1 = 0x00000002819eb700
x2 = 0x0000000000000003
...
x23 = 0x0000000283b51790
x24 = 0x0000000283b51790
x25 = 0x0000000116e17f28 (void *)0x00000001173e6b88: OBJC_METACLASS_$__TtC16BBUGCVideoDetail13VDUGCMoreBloc
x26 = 0x00000001142906d8 bili-universal`type_metadata_for_ToolCell + 784
x27 = 0x000000010a552534 bili-universal`sub_109F86534
x28 = 0x0000000116718000 "badge_control"
fp = 0x000000016f832610
lr = 0x000000010af5f15c bili-universal`sub_10A992320 + 3644
sp = 0x000000016f832510
pc = 0x000000010af5ffe4 bili-universal`sub_10A993E14 + 464
cpsr = 0x60000000
- 因为
x0的值是0x0000000283b51790,所以打印x0的值,看到x0(VKSettingView.SelectModel)(0x0000000283b51790)的items有值了,就是播放速度数组,这也证明sub_10A993E14修改了VKSettingView.SelectModel的items的值。x0通常拿来存放函数的返回值。
(lldb) p (id)$x0
(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790
(lldb) p [(_TtC13VKSettingView11SelectModel *) 0x0000000283b51790 items]
(_TtCs22__SwiftDeferredNSArray *) 0x0000000280743120 6 values
(lldb) po 0x0000000280743120
<Swift.__SwiftDeferredNSArray 0x280743120>(
0.5,
0.75,
1.0,
1.25,
1.5,
2.0
)
- 我们将
sub_10A993E14的伪代码,参数a1的类型是BAPIPlayersharedSettingItem,a2的类型是VKSettingView.SelectModel一起给chatgpt分析,chatgpt叫我们查看swift_initStaticObject的参数&unk_116E60370的值是什么。- 如果
chatgpt的分析结果没用,我们就自己打断点,看是哪些汇编代码更改了items的值,再看汇编代码对应的伪代码是怎样的。
- 如果
检查 inited = swift_initStaticObject(...) 对象
inited 很可能是 SelectModel 或其内部配置对象(例如某个静态配置结构体或 Swift 字典/数组)被初始化。你可以在反汇编中查看 swift_initStaticObject 的参数 &unk_116E60370 看看该静态对象是什么,它可能携带 items 的初始数据。若你在数据段或只读段中找到与 “items” 相关的字符串数组、常量字符串列表、NSStringPointer 等,那可能就是 items 的来源。
- 查看
&unk_116E60370的值,发现是在数据段(__data)中
![]()
- 查看
&unk_116E60370的值的16进制视图,发现它旁边的地址存放着播放速度,所以&unk_116E60370保存着播放速度数组
![]()
- 我们知道数据段(
__data)存放着全局变量,所以播放速度数组应该是放在一个全局变量里面,类似:
var playbackRates = ["0.5", "0.75", "1.0", "1.25", "1.5", "2.0"]
说明
比如0000000116E789B0,保存的值是0.75
0000000116E789B0 30 2E 37 35 00 00 00 00 00 00 00 00 00 00 00 E4 0.75............
各个字节的解析如下,特别是最后一个字节E4,代表要读取4个字节的数据,如果是E3代表要读取3个字节的数据
30 : 0
2E : .
37 : 7
35 : 5
E4 : 读取四个字节的数据
越狱解决方案
- 修改下面地址存储的值
0000000116E60390 30 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 0.5.............
0000000116E603A0 30 2E 37 35 00 00 00 00 00 00 00 00 00 00 00 E4 0.75............
0000000116E603B0 31 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.0.............
0000000116E603C0 31 2E 32 35 00 00 00 00 00 00 00 00 00 00 00 E4 1.25............
0000000116E603D0 31 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.5.............
0000000116E603E0 32 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 2.0.............
- 具体代码
/// 将速度写入到内存地址
/// - Parameters:
/// - dest_addr: 目标内存地址
/// - str: 速度字符串,比如"1.0"
static int write_rate_string_to_address(uintptr_t dest_addr, NSString *str) {
if (str == nil) {
return -1;
}
// UTF8 字符串
const char *utf8Str = [str UTF8String];
size_t strLength = strlen(utf8Str); // 字符数(不含 \0)
if (strLength > (NJ_RATE_BLOCK_SIZE - 1)) {
// 只能容纳前15字节 + 最后一字节用于 E0+strLength
strLength = NJ_RATE_BLOCK_SIZE - 1;
}
uint8_t block[NJ_RATE_BLOCK_SIZE];
memset(block, 0, NJ_RATE_BLOCK_SIZE);
// 前 strLength 字节写入字符串
memcpy(block, utf8Str, strLength);
// 最后一个字节写入:E0 + 长度
block[NJ_RATE_BLOCK_SIZE - 1] = 0xE0 + (uint8_t)strLength;
// 将 block 写到目标地址
memcpy((void *)dest_addr, block, NJ_RATE_BLOCK_SIZE);
return 0;
}
/// 将速度写入到内存地址
/// - Parameter baseAddress: 起始内存地址
static void write_rate_to_address(uintptr_t baseAddress) {
NSArray<NSString *> *playbackRates = @[@"0.5", @"1.0", @"1.25", @"1.5", @"2.0", @"3.0"];
NSInteger count = playbackRates.count;
for (NSInteger i = 0; i < count; i++) {
uintptr_t currentAddress = baseAddress + i * NJ_RATE_BLOCK_SIZE;
write_rate_string_to_address(currentAddress, playbackRates[i]);
}
}
// [横屏视频-半屏播放]的播放速度
static void changePlaybackRates_LandscapeVideo_HalfScreenPlayback() {
/*
0000000116E60390 30 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 0.5.............
0000000116E603A0 30 2E 37 35 00 00 00 00 00 00 00 00 00 00 00 E4 0.75............
0000000116E603B0 31 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.0.............
0000000116E603C0 31 2E 32 35 00 00 00 00 00 00 00 00 00 00 00 E4 1.25............
0000000116E603D0 31 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.5.............
0000000116E603E0 32 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 2.0.............
*/
uintptr_t baseAddress = g_slide + 0x116E60390;
write_rate_to_address(baseAddress);
}
非越狱解决方案
修改Mach-O文件的汇编指令
目标
- 修改下面地址存储的值
0000000116E60390 30 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 0.5.............
0000000116E603A0 30 2E 37 35 00 00 00 00 00 00 00 00 00 00 00 E4 0.75............
0000000116E603B0 31 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.0.............
0000000116E603C0 31 2E 32 35 00 00 00 00 00 00 00 00 00 00 00 E4 1.25............
0000000116E603D0 31 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.5.............
0000000116E603E0 32 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 2.0.............
示例
比如修改0000000116E603A0
0000000116E603A0 30 2E 37 35 00 00 00 00 00 00 00 00 00 00 00 E4 0.75............
- 鼠标点击
0000000116E603A0 -
IDA->Edit->Patch program->Change byte
![]()
- 显示
Patch Bytes弹框
![]()
- 从
Origin value:- 30 2E 37 35 00 00 00 00 00 00 00 00 00 00 00 E4
- 修改 Values 为:
- 31 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3
- 点击
OK,真正修改
修改结果
- 当前的值:
0.5 → 30 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3
0.75 → 30 2E 37 35 00 00 00 00 00 00 00 00 00 00 00 E4
1.0 → 31 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3
1.25 → 31 2E 32 35 00 00 00 00 00 00 00 00 00 00 00 E4
1.5 → 31 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3
2.0 → 32 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3
- 新的播放速度对应的值:
0.5 → 30 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3
1.0 → 31 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3
1.25 → 31 2E 32 35 00 00 00 00 00 00 00 00 00 00 00 E4
1.5 → 31 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3
2.0 → 32 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3
3.0 → 33 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3
- 全部修改完后
0000000116E60390 30 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 0.5.............
0000000116E603A0 31 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.0.............
0000000116E603B0 31 2E 32 35 00 00 00 00 00 00 00 00 00 00 00 E4 1.25............
0000000116E603C0 31 2E 35 00 00 00 00 00 00 00 00 00 00 00 00 E3 1.5.............
0000000116E603D0 32 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 2.0.............
0000000116E603E0 33 2E 30 00 00 00 00 00 00 00 00 00 00 00 00 E3 3.0.............
保存
保存到Mach-O文件中
-
IDA->Edit->Patch program->Apply patches to input file->OK
![]()
![]()
-
保存后,底部会显示
log:Patch successful: /Users/touchworld/Documents/iOSDisassembler/hook/bilibili/IDA_max_0/bili-universal
效果
![]()
代码
记住这张时间线图,你再也不会乱用 useEffect / useLayoutEffect
useEffect 和 useLayoutEffect 的区别:别背定义,按“什么时候上屏”来选
以前一直写vue,现在写react了,写react代码的时候有时候会碰到一个选择题:
- 这个副作用用
useEffect还是useLayoutEffect? - 为什么我用
useEffect量 DOM 会闪一下? - Next.js 里
useLayoutEffect为什么会给我一个 warning?
这俩 Hook 的差别,说穿了就一句:它们跑在“上屏(paint)”的前后。
一句话结论(先拿走)
-
默认用
useEffect:不会挡住浏览器绘制。 -
只有在“必须读布局/写布局且不能闪”的时候用
useLayoutEffect:它会在浏览器 paint 之前同步执行。
如果你脑子里只留两句话,就留这两句。
它们到底差在哪:在浏览器 paint 的前后
把 React DOM 的一次更新粗暴拆成四步,你就不会混了:
flowchart LR
A[render 计算 JSX] --> B[commit 写入 DOM]
B --> C[useLayoutEffect 同步执行]
C --> D[浏览器 paint 上屏]
D --> E[useEffect 执行]
classDef info fill:#cce5ff,stroke:#0d6efd,color:#004085
classDef warning fill:#fff3cd,stroke:#ffc107,color:#856404
class C warning
class E info
-
useLayoutEffect:DOM 已经变了,但还没 paint。它会阻塞本次 paint。 -
useEffect:页面已经 paint 了。它不会阻塞上屏(但也意味着你在里面改布局可能会“先错后改”,肉眼看到就是闪)。
注意我在说的是“commit 后”的那个时间点,不是 render 阶段。
一个很真实的例子:测量 DOM 决定位置(useEffect 会闪)
比如你做一个 Tooltip:初始不知道自己宽高,得先 render 出来,然后用 getBoundingClientRect() 量一下,再把位置修正。
如果你用 useEffect:
- 第一次 paint:Tooltip 先用默认位置上屏
- effect 里量完 -> setState
- 第二次 paint:位置修正
用户看到的就是“闪一下”。如果你用 useLayoutEffect,修正发生在 paint 之前,第一帧就是对的。
下面这段代码可以直接在 React DOM 里跑(为了不违反 Hooks 规则,我写成两个组件,用 checkbox 切换时会 remount):
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
type TooltipPosition = {
anchorRef: React.RefObject<HTMLButtonElement | null>;
tipRef: React.RefObject<HTMLDivElement | null>;
left: number;
};
function calcLeft(anchor: HTMLButtonElement, tip: HTMLDivElement) {
const a = anchor.getBoundingClientRect();
const t = tip.getBoundingClientRect();
return Math.round(a.left + a.width / 2 - t.width / 2);
}
function useTooltipPositionWithEffect(): TooltipPosition {
const anchorRef = useRef<HTMLButtonElement | null>(null);
const tipRef = useRef<HTMLDivElement | null>(null);
const [left, setLeft] = useState(0);
useEffect(() => {
const anchor = anchorRef.current;
const tip = tipRef.current;
if (!anchor || !tip) return;
setLeft(calcLeft(anchor, tip));
}, []);
return { anchorRef, tipRef, left };
}
function useTooltipPositionWithLayoutEffect(): TooltipPosition {
const anchorRef = useRef<HTMLButtonElement | null>(null);
const tipRef = useRef<HTMLDivElement | null>(null);
const [left, setLeft] = useState(0);
useLayoutEffect(() => {
const anchor = anchorRef.current;
const tip = tipRef.current;
if (!anchor || !tip) return;
setLeft(calcLeft(anchor, tip));
}, []);
return { anchorRef, tipRef, left };
}
function TooltipFrame({ pos }: { pos: TooltipPosition }) {
return (
<>
<button ref={pos.anchorRef} style={{ marginLeft: 120 }}>
Hover me
</button>
<div
ref={pos.tipRef}
style={{
position: "fixed",
top: 80,
left: pos.left,
padding: "8px 10px",
borderRadius: 8,
background: "#111827",
color: "#fff",
fontSize: 12,
whiteSpace: "nowrap",
}}
>
I am a tooltip
</div>
</>
);
}
function DemoUseEffect() {
return <TooltipFrame pos={useTooltipPositionWithEffect()} />;
}
function DemoUseLayoutEffect() {
return <TooltipFrame pos={useTooltipPositionWithLayoutEffect()} />;
}
export function Demo() {
const [layout, setLayout] = useState(false);
return (
<div style={{ padding: 40 }}>
<label style={{ display: "block", marginBottom: 12 }}>
<input
type="checkbox"
checked={layout}
onChange={(e) => setLayout(e.target.checked)}
/>{" "}
用 useLayoutEffect(勾上后更不容易闪)
</label>
{layout ? <DemoUseLayoutEffect /> : <DemoUseEffect />}
</div>
);
}
真实项目里你可能还会处理 resize、内容变化(ResizeObserver)、字体加载导致的宽度变化等;但对理解这两个 Hook 的差别,上面这个例子够用了。
怎么选:我自己用的“决策口诀”
1)只要不读/写布局,就用 useEffect
典型场景:
- 请求数据、上报埋点
- 订阅/取消订阅(WebSocket、EventEmitter)
-
document.title、localStorage同步 - 给 window/document 绑事件
这些东西不需要卡在 paint 之前完成,useEffect 更合适。
2)你要读布局(layout read)并且会影响第一帧渲染,就用 useLayoutEffect
典型场景:
-
getBoundingClientRect()/offsetWidth/scrollHeight这种 - 计算初始滚动位置、同步滚动
- 需要避免视觉抖动的“测量 -> setState”
- focus / selection(输入框聚焦、光标定位)对首帧体验敏感
一句话:“不想让用户看到中间态”。
3)别在 useLayoutEffect 里干重活
因为它会阻塞 paint:
- 你在里面做重计算,页面就掉帧
- 你在里面频繁 setState,可能放大卡顿
如果你只是“想早点跑一下”,但并不依赖布局,别用它。
Next.js / SSR 里那个 warning 怎么回事
在服务端渲染(SSR)时:
-
useEffect本来就不会执行(它只在浏览器跑) -
useLayoutEffect也不会执行,但 React 会提示你:它在服务端没意义,可能导致你写出“依赖布局但 SSR 不存在布局”的代码
如果你写的是“浏览器才有意义的 layout effect”,又不想看到 warning,常见做法是包一层:
import { useEffect, useLayoutEffect } from "react";
export const useIsomorphicLayoutEffect =
typeof window !== "undefined" ? useLayoutEffect : useEffect;
然后把需要 layout 的地方用 useIsomorphicLayoutEffect。
容易踩的坑(顺手说两句)
-
Strict Mode 下 effect 会在开发环境额外执行一次:
useEffect和useLayoutEffect都一样,别拿这个现象判断线上行为。 -
“我在
useEffect里 setState 为什么会闪?”:因为你改的是布局相关内容,第一帧已经 paint 了。 -
不要把数据请求塞进
useLayoutEffect:它既不需要 paint 前完成,还可能拖慢上屏。
简单总结一下
-
useEffect:大多数副作用的默认选择。 -
useLayoutEffect:只在“必须卡在 paint 前解决”的那一小撮场景里用。
真要说区别,其实就是一句:你愿不愿意为了“第一帧正确”去挡住 paint。
如果你觉得这篇文章有帮助,欢迎关注我的 GitHub,下面是我的一些开源项目:
Claude Code Skills(按需加载,意图自动识别,不浪费 token,介绍文章):
- code-review-skill - 代码审查技能,覆盖 React 19、Vue 3、TypeScript、Rust 等约 9000 行规则(详细介绍)
- 5-whys-skill - 5 Whys 根因分析,说"找根因"自动激活
- first-principles-skill - 第一性原理思考,适合架构设计和技术选型
全栈项目(适合学习现代技术栈):
- prompt-vault - Prompt 管理器,Next.js 15 + React 19 + tRPC 11 + Supabase 全栈示例,clone 下来配个免费 Supabase 就能跑
- chat_edit - 双模式 AI 应用(聊天+富文本编辑),Vue 3.5 + TypeScript + Vite 5 + Quill 2.0 + IndexedDB