开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

不学点设计模式,你真当我是切图仔啊?【单例模式】

发表于 2024-04-28

设计模式总觉得自己似懂非懂,直到最近开始阅读一些框架的源码,发现懂设计模式太重要了,方便理解业务,也能够给更快更合理得设计代码,那就写一下总结一下,方便自己以后复习

汇总

创建型模式:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式、并发型模式和线程池模式。

什么是单例模式?

全局仅有一个实例以供访问操作,确保一个类只有一个实例,并提供一个全局访问点来访问该实例

优点&缺点

优点:

1、减少内存消耗,某些情况下可以提高性能。特别是在需要频繁访问或操作某个对象时,单例模式可以减少内存占用和计算开销

2、简单,对于需要全局访问的资源或状态,单例模式提供了一种简洁的方式。它使得开发者能够轻松地管理和维护这些资源或状态,而无需担心它们的创建和销毁

缺陷:

1、单例对象一般都是长生命周期对象,使用不当可能会导致内存泄漏

欢迎评论补充

使用场景

全局状态管理:单例模式可用于管理全局状态,如用户登录状态、应用配置等。通过单例模式,可以确保整个应用中只有一个状态管理实例,从而避免状态混乱和冲突。

弹窗管理:弹窗(如警告框、确认框等)是常见的交互元素。使用单例模式可以确保在同一时间只有一个弹窗实例存在,从而避免多个弹窗重叠或冲突的情况。

连接池管理:对于需要与后端服务器建立连接的前端应用(如WebSocket连接),单例模式可用于管理连接池。这样可以确保整个应用中只有一个连接池实例,从而避免资源浪费和连接冲突。

全局唯一的服务或工具类:例如,有些应用中可能需要一个全局唯一的日期格式化工具或请求拦截器等。这些工具或服务在整个应用中只需要一个实例,因此可以使用单例模式来实现。

实现方式

1、ES6的class实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
js复制代码// 单例模式
class Single {
constructor() {
if (!Single.instance) {
Single.instance = this;
}
return Single.instance;
}

createInstance() {
const obj = { name: "xxx" };
return obj;
}

getInstance() {
if (!Single.instance) {
Single.instance = this.createInstance();
}
return Single.instance;
}
}

const instance1 = new Single();
const instance2 = new Single();

console.log(instance1 === instance2); // true

2、闭包实现

1
2
3
4
5
6
7
8
9
10
js复制代码let Single = (function() {
// 闭包保存的唯一实例对象
let instance;
return function(name) {
if (instance) return instance
instance = { name: 'xxx' };
return instance;
}
})();
console.log(new Single() === new Single()); // true

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Hugging Face 新开源Parler-TTS,高质量

发表于 2024-04-28

前言

Parler-TTS 是大名鼎鼎的huggingface推出的一款轻量级文本到语音(TTS)模型,它能够生成高质量、自然流畅的语音,并且能够模仿特定说话者的风格,包括性别、音高、说话风格等。这款模型是由Dan Lyth和Simon King创建,他们分别来自Stability AI和爱丁堡大学,共同撰写了论文《 Natural language guidance of high-fidelity **text-to-speech with synthetic annotations 》。这个模型即是这一论文的复现。

  • Huggingface模型下载: huggingface.co/parler-tts/…
  • AI快站模型免费加速下载:aifasthub.com/models/parl…

多语言支持与定制化能力

Parler-TTS的关键优势之一是其支持多种语言,包括英语、西班牙语、法语、德语等,非常适用于在全球范围内运营的企业或正在学习新语言的个人用户。同时,Parler-TTS还提供了高度定制化的功能,用户可以选择多种语音和口音风格,满足个性化需求,尤其适用于希望为品牌建立统一声音形象的企业。

技术创新与开源共享

Parler-TTS的核心在于其先进的机器学习算法。通过分析45,000小时的大规模语音数据,Parler-TTS建立起逼真自然的语音模型,生成的语音几乎无法与人类区分。

值得一提的是,Parler-TTS是一个完全开源的项目,Hugging Face团队公开发布了所有数据集、预处理、训练代码以及权重文件,为广大开发者提供了丰富的资源。这将极大地推动TTS技术在更广泛领域的应用和突破。

广泛的应用场景

Parler-TTS具有极高的灵活性,可以应用于语音助手、电子学习工具、有声书甚至游戏等各类应用程序。这使其成为一款功能全面的多用途工具,满足不同行业和场景的需求。

未来发展规划

Parler-TTS目前已经推出了600M参数的初版模型,未来还将不断升级扩容,计划基于50,000小时的大规模训练数据构建更加强大、普适的文本转语音系统。

总的来说,Parler-TTS是一项真正颠覆性的技术创新,不仅在TTS性能上取得了突破,而且通过开源共享的方式,也为AI技术的发展注入了持续动力。相信它必将在未来的内容创作和互动领域发挥重要作用。

模型下载

Huggingface模型下载

huggingface.co/parler-tts/…

AI快站模型免费加速下载

aifasthub.com/models/parl…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

没有网络限制!超简单本地部署 Llama3 的方法

发表于 2024-04-27

我们利用 LM Studio 这款软件来可视化部署 Llama3。

官网地址

选择好对应的操作系统下载安装包,在下载好之后进行安装。在安装好之后我们就可以打开软件并使用了:

我们在中间的输入框部分输入 llama 来搜索并安装 llama 系列的模型,不过在进行搜索时会发现搜索功能失效了:

查阅了一下资料发现是收 hc 的影响导致的:

社区也给出了解决方案:

也就是说如果要搜 Llama3,那么直接搜 lmstudio-community,而如果要搜 Phi,那就搜 microsoft:

不过虽然解决了搜索的问题,但对于大多数小伙伴来说也依然有诸多限制,最大的限制就是网络问题,因为国内无法正常从 Hugging Face 上拉取模型,好多小伙伴可能在这一步就被 block 住了。今天三金给大家介绍一种魔改的方法,通过 hf 镜像站来做到即使在国内也能正常搜索并下载模型!!

MacOS 系统

我们打开「访达-应用程序-LM Studio」,然后右击 LM Studio 程序,在二级菜单中选中「显示包内容」:

然后我们将里面的文件夹使用 VS Code 或者任意一款编辑器打开:

如上图,以 VS Code 为例:

  1. 我们点击编辑器左侧的搜索
  2. 然后在第一个输入框中输入 huggingface.co,这会将 LM Studio 程序中所有使用到 huggingface.co链接的地方都搜索出来
  3. 紧接着我们在第二个输入框中输入 hf 的镜像网站地址 hf-mirror.com,这表示我们将要把所有匹配到的内容都替换为镜像站点的网址
  4. 最后点击右侧的替换按钮进行替换

完成上述四步之后,我们重启 LM Studio 就可以在国内正常进行搜索和下载模型了

Windows 系统

流程都是一样的,唯一的不同就是文件的存储位置。

  1. 在 Windows 系统上安装 LM Studio 时不能指定目录,只会安装到 C:\Users/[你的 Windows 电脑用户名]\AppData\LM-Studio
  2. 进入到这个目录之后可以看到你下载的 LM Studio 程序都包含哪些文件,我们以 0.2.21 版本为例,在当前目录下应该会有一个 app-0.2.21的文件夹
  3. 通过 VS Code 打开这个文件夹,然后执行之前说的四个步骤将里面的 huggingface.co 都替换成 hf-mirror.com 即可
  4. 重启 LM Studio 就可以正常使用了

选择本地模型开始对话

当我们下载好模型之后,就可以:

  1. 点击 LM Studio 左侧菜单中的 AI Chat
  2. 选择刚刚下载好的模型
  3. 开始对话!!

我们先让它来介绍一下自己:

生成回答的速度很快,就是语言还是英文,不过没关系,我们只要让它使用中文回答就可以了:

并且它写代码也贼快:

注意

大家在选择模型时,一定要考虑好自己的电脑配置,尤其是 GPU 不太行的电脑,最好不要安装一下子就跑满甚至跑超 CPU 的模型,这样对电脑本身的损耗是非常大的!!

可以通过在 AI Chat 中「选择模型下拉框」左边展示 CPU 的部分来监控使用模型时 CPU 的使用率,如果太大的话建议立即停止,或者通过降低模型精度和分块等方式来减少计算要求。

感兴趣的小伙伴快去试试吧~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

内网开发提效指南

发表于 2024-04-27

❝

工欲善其事必先利其器,使用过内网开发的小伙伴都知道,CV大法在内网基本就废了,查资料也是非常的不便。对于一名程序员来说,如果把搜索引擎和CV键给他ban了,遇到问题后那基本是寸步难行。今天给大家介绍几种帮助内网开发提效的方法,希望能够帮助到大家。

一、文档站点内网部署

可以把项目中所用技术和框架的文档部署到公司内网中。

以elementPlus为例:

1、访问gh-pages分支https://github.com/element-plus/element-plus/tree/gh-pages,下载文档站源码。

2、将文档站部署到内网服务器(以nginx为例)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码    server {
        listen       9800;
        server_name  localhost;

        location / {
            root   html/element-plus-gh-pages;
            index  index.html index.htm;
            try_files $uri $uri/ /element-plus-gh-pages/index.html;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }

部署后的访问速度也是非常快的

使用这种方式,随着部署的站点增多,后续框架、文档更新的时候,维护起来相对是比较麻烦的。且只能查看文档,遇到问题需要求助度娘还是不太方便。

下面介绍两种物理外挂,可以直接访问外网。

二、USB跨屏穿越器数据线

个人感觉此方案的体验是最好的,缺点是需要两台电脑,并且需要花钱买一根线,价格在80-200之间。

购买

某宝、某鱼都有销售,我是在某宝85块买的。

使用

连接两台电脑的USB端口即可,会自动安装驱动,那根线实际上就相当于是一个文件中转器,可以实现剪切板、文件的互传。使用体验就跟一台电脑连接了两台显示器一样。如下图所示:

三、手机投屏

本文重点介绍此方案,因为可以白嫖且不需要第二台电脑。一部安卓手机+数据线即可,缺点是文件传输不太方便。它就是一个开源投屏项目scrcpy。可以看到,此项目在github上拥有高达102k的star数量。

✨亮点

  • 亮度 (原生,仅显示设备屏幕)
  • 表演 (30~60fps)
  • 质量 (1920×1080或以上)
  • 低延迟 (70~100ms)
  • 启动时间短 (显示第一张图像约1秒)
  • 非侵入性 (设备上没有安装任何东西)
  • 不需要 ROOT
  • 有线无线都可连接
  • 可以随便调整界面和码率
  • 画面随意裁剪,自带录屏(手游直播利器)
  • 支持多设备同时投屏
  • 利用电脑的键盘和鼠标可以控制手机
  • 把 APK 文件拖拽到电脑窗口即可安装应用到手机,把普通文件拖拽到窗口即可复制到手机
  • 手机电脑共享剪贴板
  • 自动检测USB连接的设备
  • 可直接添加设备的局域网IP,达到无线控制的效果
  • 将自动保存连接过的IP地址,下次输入时,自动提醒
  • 支持设备别名
  • 支持中英两种语言
  • Tray menu
  • 等等等…

安装

根据不同系统直接去release页面下载对应版本即可:github.com/Genymobile/…

使用

下载解压完,进入软件目录,点击下图按钮打开命令行界面,输入启动命令即可。

命令行输入scrcpy,按回车, 猿神,起洞!

启动之后,即可使用鼠标操作手机,非常的丝滑

1、手机复制文本到电脑

2、电脑复制文本到手机

可以看到,使用投屏的方式,也可以实现CV大法。并且可以使用手机端的外网搜索资料、解决问题等。以下是该项目的快捷键,熟练使用,即可达到人机合一的地步。

快捷键

操作 快捷键 快捷键 (macOS)
切换全屏模式 Ctrl+f Cmd+f
将窗口调整为 1:1 Ctrl+g Cmd+g
调整窗口大小以删除黑色边框 Ctrl+x 双击黑色背景
设备HOME键 Ctrl+h 鼠标中键
设备BACK键 Ctrl+b 鼠标右键
设备任务管理键 Ctrl+s Cmd+s
设备菜单键 Ctrl+m Ctrl+m
设备音量+键 Ctrl+↑ Cmd+↑
设备音量-键 Ctrl+↓ Cmd+↓
设备电源键 Ctrl+p Cmd+p
点亮手机屏幕 鼠标右键 鼠标右键
关闭设备屏幕(保持镜像) Ctrl+o Cmd+o
展开通知面板 Ctrl+n Cmd+n
折叠通知面板 Ctrl+Shift+n Cmd+Shift+n
将设备剪贴板中的内容复制到计算机 Ctrl+c Cmd+c
将计算机剪贴板中的内容粘贴到设备 Ctrl+v Cmd+v
将计算机剪贴板中的内容复制到设备 Ctrl+Shift+v Cmd+Shift+v
安装APK 将APK文件拖入投屏 将APK文件拖入投屏
传输文件到设备 将文件拖入投屏 将文件拖入投屏
启用/禁用FPS计数器(stdout) Ctrl+i Cmd+i

使用小技巧

经过笔者几天的使用,总结出几个小技巧。

1、电脑键盘控制手机进行中文输入,必须使用正确的输入法组合。

手机端:讯飞输入法(搜狗输入法不支持)

电脑端:ENG(使用英文键盘)

2、手机熄屏状态下投屏。 在scrcpy命令后加上熄屏参数即可:scrcpy --turn-screen-off

这样就可以在手机熄屏的状态下,仍可以被电脑操作,达到节省电量和减轻发热的目的。

诸如此类的命令参数还有很多,执行scrcpy --help就可查看详细的帮助文档。

衍生项目

因为开源的特性,scrcpy也衍生了一些相关项目,列举其中一些:

  • QtScrcpy 使用qt重新实现的桌面端,并加强了对游戏的支持。
  • scrcpy-gui 为scrcpy的命令行提供了gui界面。
  • guiscrcpy 另一个scrcpy的gui界面。
  • scrcpy-docker docker版本的scrcpy。
  • scrcpy-go go语言版本的scrcpy,增强对游戏的支持。

总结

第二第三种方法虽然建立了内网开发电脑和外网设备的联系,但是是不会被公司的安全系统检测到一机双网的,因为其本质就类似于设计模式中的发布订阅模式,用数据线充当了中间人,两台设备之间方便传输数据了而已,不会涉及到联网。

内网开发的痛点,无非就是复制粘贴、文件传输不便,只要打通这个链路,就能解决此问题。以上三个方法,笔者在实际工作中都用到了,确实极大的提高了工作效率。如果你也在饱受内网开发的折磨,不妨试试这几个方法。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

艺术与智能的交汇--你的Coze+Kimi+GPT+SD

发表于 2024-04-27

探索无限创意,让想象翱翔天际!以 Coze 的舒适界面,Kimi 的深度分析,GPT 的智慧洞察,加上 SD 的画面魔法,打造了一个无与伦比的创意伙伴。用 Coze+Kimi+GPT+SD,你将拥有最强大的工具,在文字与图像的海洋中畅游无阻。来吧,让每一个想法都将变为现实,每一次灵感都会照亮未来!

哎呀呀,我的朋友,刚进入神秘的Coze世界,首先迎接你的是一头雾水吗?没关系,放轻松,让我们一起揭开Coze的神秘面纱!它就是一个构建专属智能机器人的神器,简单又好用,小白也能轻松驾驭!我这人话儿不多,但说到保证,那可是板上钉钉的!只需要跟着一步步来,你就能获得一个听话又能干的机器小助手。不用担心,Coze的国内版和国际版的差异,咱们也一网打尽,对比让你一目了然。眼看就要开启属于你的创造之旅,你准备好了吗?跟我走,Coze大门,向你敞开!

Coze是个啥?有啥用啊?

Coze是一个强大的机器人开发平台,它集成了先进的人工智能技术,提供了丰富的功能和灵活的定制选项,让你能够轻松创建和管理自己的聊天机器人。🚀

Coze不仅能够理解和回应用户的问题,还能够根据用户的需求进行个性化定制,提供独特的互动体验。🌟

无论你是企业用户还是个人用户,无论你是希望提升工作效率,优化客户服务,还是想要创建自己的虚拟伙伴,Coze都能满足你的需求。💼🎈

Coze强大的功能包括:智能问答,任务自动化,个性化推荐,多语言支持,以及与主流社交平台的无缝集成等等。🔧

Coze的开发环境友好,易于上手,无论你是否有编程背景,都能快速掌握Coze的使用,打造专属于你的智能机器人。🔰

Coze正在改变人们与技术的交互方式,打开了无限可能。现在就跟着小窝一起加入Coze,一起探索人工智能的奇妙世界吧!🎉

Coze国内版使用教程

第一部分已经对Coze有了初步了解,如果还是觉得抽象,没关系,下面小窝用图文带你揭开Coze这层神秘的面纱,先来看看大门(官网地址:www.coze.cn/home),长这样:

添加图片注释,不超过 140 字(可选)

界面清晰干净,如果没有什么特别需求可以直接在底部输入框输入任何问题,然后就等待机器人回复即可,不过呢,小窝今天是带你创建自己专属机器人的,入口在这里:

添加图片注释,不超过 140 字(可选)

点击创建Bot就会看到下面这个界面:

添加图片注释,不超过 140 字(可选)

按照图中箭头和文本提示,填写信息,然后点–确认:

添加图片注释,不超过 140 字(可选)

机器人还算听话,经过不断的调试优化(这里假设机器人很复杂,中间省略了调试过程,哈哈哈,这个机器人功能简单,所以基本不用调试,如果你想让他赋予更多的能力,或者严格按照你的要求来,还是需要好好调教的,这个过程是少不了的)输入的内容确实给转换成表情包了,那咱们继续:

添加图片注释,不超过 140 字(可选)

点击发布,需要填写一些基本的发布信息:

添加图片注释,不超过 140 字(可选)

这些填写完成之后,点击–确认,按照下图的解释,勾选自己所需要的平台,选择完之后点击–发布:

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

点击–完成之后,手机就会有通知了,点击进入到会话界面,就可以使用自己的专属机器人了,就像这样:

添加图片注释,不超过 140 字(可选)

对了,要想在手机上体验自己的专属机器人,需要借助豆包这款APP,有飞书也行,按照授权步骤配置即可,还是很方便的。

到这里,基本的创建流程就走完了,激动不?不对,等一下,是不是落下什么东西了?哈哈哈~还真是呢,中间区域,注入核心灵魂的区域还没开始介绍呢,继续往下看,先说插件,就是能够让你的机器人能够调用外部的API,例如搜索信息、浏览网页、生成图片等,扩展 Bot 的能力和使用场景,先看看都有什么吧,点击右边的加号就能看到了:

添加图片注释,不超过 140 字(可选)

你想赋予机器人什么能力,就添加对应的插件,是不是很灵活呢?比如,加一个SD图生成的吧,就是这个样子:

添加图片注释,不超过 140 字(可选)

对了,Kimi也跟Coze打配合了,在添加插件的时候,如果你习惯了使用Kimi,可以直接添加Kimi插件使用,瞬间让你的机器人赋予Kimi的能力

接下来是工作流,这个其实也很好理解,所谓的工作流,简单的描述就是把你要实现的步骤串联起来,方便下次使用,我举个例子:

想象一下,你正在准备一次自驾游。这个过程就像是一个工作流,它涵盖了从行程的规划、车辆的准备,到旅途中的各种活动,每一步都是必不可少的。

首先,你需要确定目的地,这就好比工作流的开始,我们可以称它为”启动自驾游项目”。接着,你可能需要查看地图,规划路线,这就像是工作流的下一步,我们可以称之为”规划行程”。

然后,你需要检查你的车辆,确保油箱已加满,轮胎气压正常,这就是”车辆准备”步骤。接下来,你可能需要打包行李,准备必要的旅行用品,如帐篷、食物和水,这就是”打包行李”步骤。

出发当天,你按照规划的路线出发,开始你的自驾游,这就是”开始旅行”步骤。在旅途中,你可能会停下来观赏风景,参加各种活动,这些都是旅行工作流中的一部分。

最后,当你到达目的地,享受完你的旅行后,你需要安全地驾车返回家中。这就是工作流的结束,我们可以称之为”结束旅行”。

所以,一个自驾游的全过程就像是一个完整的工作流,每一步都有其特定的任务和目标,所有步骤协同工作,共同创造出一次愉快的旅行体验。

你可以按照这个逻辑选择适合自己的工作流,你可以自己创建:

添加图片注释,不超过 140 字(可选)

具体如何创建,我还在研究,如果你感兴趣,就关注小窝吧,不定期分享AI干货知识,除了自己创建,还有现成的,比如这样:

添加图片注释,不超过 140 字(可选)

总有一款适合你,喜欢小窝也可以多多支持鼓励一下,当然也可以跟小窝成为朋友 tongquxiaowo1688,咱们一起成长

添加图片注释,不超过 140 字(可选)

到这里,国内版的创建流程就算是分享完了,不过这里没说知识库,没关系,继续往下看,我在国际版里有介绍,什么?你说你不想了解知识库?那我再给你看点儿不一样的,国际版用到的模型有GPT-4 Turbo(128K),这是免费的,算是白嫖了,如果你现在就在用付费的,那每个月20刀呢

添加图片注释,不超过 140 字(可选)

不知道这个能不能刺激到你继续往下看呢?

小窝不定期更新AI实用工具,最近一直在学SD和ComfyUI,如果凑巧你也在学或者感兴趣,那不如交个朋友认识一下,掌握新技巧就要有伙伴同行!速速加小窝:tongquxiaowo1688 (童趣小窝的拼音全拼+1688),驶入AI探索之旅的快车道,这里有一群志同道合的小伙伴。别忘啦,务必备注一下“绘画”,我们不见不散!

Coze国际版使用教程

国际版官网地址,先注册吧:www.coze.com/home

添加图片注释,不超过 140 字(可选)

先看看Bot市场提供的机器人有没有自己喜欢的,有的话是可以直接拿来用的哈,下面小窝还是继续带你创建一个自己专属的机器人:

添加图片注释,不超过 140 字(可选)

接着选择一个模型:

添加图片注释,不超过 140 字(可选)

功能区域跟国内版的一样,为了不让你往回翻,我这里再放一张图,给你再解释一遍:

添加图片注释,不超过 140 字(可选)

接着给自己的机器人添加插件:

添加图片注释,不超过 140 字(可选)

选择一个你想赋予机器人能力的插件:

添加图片注释,不超过 140 字(可选)

这是添加成功之后的样子:

添加图片注释,不超过 140 字(可选)

注意了哈,小窝又敲黑板了,国内版落下的知识库要开始讲解了,按照图中箭头所指进行操作,每一步的说明都在图片中有标出:

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

这里提个醒,如果你的机器人,需要回复固定的答案,可以通过下边的方法进行添加:

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

点击保存,就可以了,我这里上传的文档很简单,为了方便大家理解我设置了三个问题:

添加图片注释,不超过 140 字(可选)

添加完知识库,我再解释一下左边的区域,主要就是职责描述和调试,不知道怎么写就让AI帮你写,具体看下图:

添加图片注释,不超过 140 字(可选)

下面测试一下,输入我的测试知识库文档中的问题–问题一,此时机器人就会优先从知识库里选择回复内容:

添加图片注释,不超过 140 字(可选)

具体的调试步骤我就省略了,你可以根据回答的内容修改左边区域的信息,这个过程或许会很麻烦,但谁让这个机器人是自己专属的呢,麻烦就麻烦吧,调教的听话了倒是能给自己省下不少时间,假设刚才调试没有问题了,那咱们就继续吧:

添加图片注释,不超过 140 字(可选)

添加图片注释,不超过 140 字(可选)

如果只是在网页端使用,发布完成就可以了,如果想在手机上使用,那就需要借助Telegram、Lark以及其他应用了,我使用的是Telegram和Lark,我先说一下将机器人发布到Telegram上的步骤:

添加图片注释,不超过 140 字(可选)

接下来要去获取token,这个token保存好,不能外泄哈

添加图片注释,不超过 140 字(可选)

获取到之后呢,就回到Telegram界面:

添加图片注释,不超过 140 字(可选)

这样就可以在手机上使用了,这个过程需要魔法,并且这个Telegram账号注册现在不太友好,+86的手机号收不到验证码,接码平台也不好使,没有的话还是用Lark代替吧,这个其实就是国内版的飞书,细心的你肯定已经发现,飞书和Lark的应用Logo都是一样的,但是对应关系可是一对一哈,国内的授权飞书,国际版的授权Lark。

对了,国际版的Lark下载,国内的应用市场搜不到,所以还需要一个能下载的谷歌账号或者appleId。当然这就是另外的话题了,小窝这里就不过多的介绍了。

如果你在使用过程中有任何疑惑,可以直接在文章底部留言,当然小窝很乐意跟大家成为朋友威信(tongquxiaowo1688),一起学习,共同进步。

今天小窝要厚着脸皮求攒了哈,为了给大家分享这波干货,小窝可是肝了将近一周了,大家伙动动发财的小手,多多支持鼓励一下呗,感谢~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

把私有数据接入 LLMs:应用程序轻松集成 开源日报 N

发表于 2024-04-27

picture

run-llama/llama_index

Stars: 29.9k License: MIT

llama_index 是用于 LLM 应用程序的数据框架。
该项目解决了如何最佳地利用私有数据增强 LLMs,并提供以下工具:

  • 提供数据连接器,以摄取现有的数据源和各种格式(API、PDF、文档、SQL 等)。
  • 提供结构化数据的方式(索引、图形),使这些数据可以轻松与 LLMs 一起使用。
  • 在您的数据上提供高级检索/查询接口:输入任何 LLM 输入提示,返回检索到的上下文和知识增强输出。
  • 允许与外部应用程序框架轻松集成(例如 LangChain, Flask, Docker, ChatGPT 等)。

reactjs/react.dev

Stars: 10.6k License: CC-BY-4.0

react.dev 是 React 文档网站。

  • 包含 React 文档的源代码和文档
  • 提供贡献指南,方便用户参与项目贡献
  • 支持本地运行和测试改动

snipe/snipe-it

Stars: 9.4k License: AGPL-3.0

picture

snipe-it 是一个免费的开源 IT 资产/许可证管理系统。

  • 基于 Laravel 8 构建,用于 IT 运营中的资产管理。
  • 可追踪谁拥有哪台笔记本电脑、何时购买以便正确折旧、处理软件许可证等。
  • Web-based 软件,必须在 web 服务器上运行并通过 web 浏览器访问。
  • 支持 Mac OSX、Linux 和 Windows 系统,并提供 Docker 镜像。
  • 提供安装指南、用户手册和升级文档。

antonbabenko/pre-commit-terraform

Stars: 2.9k License: MIT

pre-commit-terraform 是用于处理 Terraform 配置的 pre-commit git 钩子。

  • 为 Terraform 提供一套预提交框架的 git 钩子集合
  • 支持自动化配置 Terraform 工作流和基础设施即代码
  • 包含多种钩子,如 terraform_fmt、terraform_tflint 等,用于检查、格式化和验证 Terraform 代码
  • 提供 Docker 使用说明,方便快速部署和使用

gcui-art/suno-api

Stars: 37 License: LGPL-3.0

sunu-api 是一个使用 API 调用 suno.ai 的音乐生成 AI,并轻松集成到诸如 GPTs 等代理程序中的项目。

  • 完美实现了从 app.suno.ai 创建 API
  • 支持自定义模式
  • 一键部署到 Vercel
  • 除了标准 API 外,还适配于 GPTs 和 Coze 等代理平台的 API Schema,可作为 LLMs 的工具/插件/操作,并集成到任何 AI 代理中。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Apk安装之谜 欢迎关注我的公众号–牛晓伟(搜索或者点击牛晓

发表于 2024-04-27

用心坚持输出易读、有趣、有深度、高质量、体系化的技术文章

本文概要

本文同样还是延续自述、对话这种轻松的方式,带您揭开apk安装的谜底,apk它到底是如何安装的。(为什么没有继续写系统native进程的文章,而来写apk安装的文章,主要原因是在看installd代码的时候也结合了apk安装的代码,如果放到后面写apk安装的文章,怕时间久了很多就忘记了,因此才有了此篇文章。文中的代码是基于android13)

Android系统进程系列的前五篇文章如下:
Android系统native进程之我是init进程
Android系统native进程之属性能力的设计魅力
Android系统native进程之进程杀手–lmkd
Android系统native进程之日志系统–logd、logcat
Android系统native进程之我是installd进程

开场白

“大家好,我是今天的主角apk,今天的给大家带来的主题是apk安装之谜,我请到了PackageManagerService、Settings、PackageInstallerSession、PackageInstallerService、InstallPackageHelper、Installer作为嘉宾,那就让嘉宾先做下自我介绍吧。”

PackageManagerService:“大家对我一定非常的了解了,我是一个服务管理所有的已经安装的apk,运行于systemserver进程,可以称呼我PMS哦。”
Settings:“大家好啊,看到我的名字有可能会有人认为我是为设置app服务的,其实不然,我是为apk的安装服务的,哪些apk安装了、安装的时间信息等等我都保存着,并且会持久化到内部存储空间。”
PackageInstallerService:“大家好,我和PMS一样也是运行于systemserver进程的,一看我的名字就能知道我是和apk安装有关系的,如果谁有安装apk的需求,可以直接通过binder的方式’呼我哦’,可以称呼我PIS哦。”
PackageInstallerSession:“大家好,我的名字和PIS是不是很像啊,可别听PIS忽悠啊,真正进行apk安装的工作都是由我完成的,可以称呼我为Session哦。”
InstallPackageHelper:“大家好,我的主要工作是负责apk安装中期的工作,后面到了我的工作内容的时候,会着重在介绍。”
Installer:“大家好,我是installd进程的代理,在java世界如果需要使用installd的能力的话,直接调用我即可,我会把相关的请求‘转告’给installd,我在apk的安装中也起了很大的作用。”

既然嘉宾都介绍完自己了,因为今天我是主角,我有主角光环,那我非常有必要隆重、浓墨重彩的介绍下我自己,让大家对我有一个非常深刻的了解。

我是apk

apk它是 **Android Package **的缩写。我是一个zip格式的压缩文件,只不过为了能让大家从文件名上一眼认出我来,我的文件后缀是 .apk。

一个apk内主要包含了dex文件、so文件、res目录、resources.arsc、META-INF目录(它里面的CERT.SF、CERT.RSA文件主要是和签名证书有关的) 、AndroidManifest.xml等文件和目录,对于dex等文件大家肯定都熟悉了,我就不在这赘述了,我着重来介绍下AndroidManifest.xml(Android清单文件)。

估计会有人说AndroidManifest文件有啥好说的,我们都知道它,它会把定义了的四大组件及Application声明在内,同时声明需要用到的权限、meta-data等信息。这么简单的大家都熟知的知识就不用介绍了。但是我想要介绍的是为啥要有AndroidManifest以及它是被谁使用的?

拿你们人类去餐馆吃饭作对比,去餐馆吃饭的时候会有菜单,菜单的作用就是告诉顾客我餐馆都提供哪些菜品,对于这些菜是由哪个厨师加了哪些配料做成的,顾客都不需要关心。而AndroidManifest的作用如菜单一样,它会把自己apk声明的信息展示出来,餐馆的菜单是展示给顾客的,而AndroidManifest是展示给PMS,也就是说PMS相关的解析代码会从apk的AndroidManifest中解析出所有的信息,从而知道apk声明了哪些四大组件、apk的包名是啥、声明了哪些权限等信息。AndroidManifest的作用就是告诉PMS:如果想了解我都声明了哪些四大组件,以及声明了哪些权限、meta-data等,直接读我都可以知道。

我的“归宿”
我的“归宿”就是成功的安装到各种安卓设备上某个目录,只有在那里我才能在这台设备上充分的发挥我的价值。

像人类一样有三六九等之分,而apk也是存在不同类别划分的,大致划分为系统apk(如launcher、dialer、setting等)和** 普通apk**(如微信、抖音)。当然系统apk还可以进一步的划分为核心系统apk、厂商apk等。不同类型的apk,它们所在的父目录也是不同的,系统apk它们的父目录是 /system/priv-app、/system/app 等,而普通apk它们的父目录是 /data/app。如下:

1
2
yaml复制代码drwxr-xr-x 39 root root   4096 2023-08-20 21:26 system/priv-app
drwxr-xr-x 24 root root 4096 2023-08-20 21:25 system/app

/system/priv-app和/system/app目录,它们的user和group都是 root,也就是只有root用户可以对这俩目录进行读写执行操作,其他用户只有执行权限。关于/data/app目录的详细信息会在下面介绍。

你们人类有句谚语:条条大路通罗马,而有的人却生在罗马。系统apk,它们就是出生在“罗马”,一“出生“就有“归宿”。而作为普通apk的我,却是在通往“罗马”的路上,因为我一“出生“会被放置于服务器上或者电脑上的某个黑暗的目录,如果想要到达我的“罗马”就需要通过apk安装把我安装到设备上。

关于我的介绍就到此吧,进入咱们今天的正题吧。

安装apk这件事

PMI:“因为我制定了安装apk相关的大体架构和流程上的事情,因此就由我来介绍下安装apk这件事情吧,关于具体实现细节还需要Session在后面介绍。”

apk的安装方式有 adb install命令、应用商店、安装apk的程序。它们的区别首先在于apk的来源不同:adb install的apk来源于电脑、应用商店的apk是应用商店从服务器下载成功后进行安装、通过安装apk的程序的apk来源于设备上已经存在的apk;其次是是否提供友好的交互界面。不管是哪种方式它们的安装流程基本上是一致的。

我把安装apk总结为三阶段:前期准备、安装、后期收尾。
前期准备的工作有拷贝、完整性校验、解析apk、提取native libs、版本号验证;安装的工作有准备 (Prepare) 、扫描 (Scan) 、调和 (Reconcile) 、提交 (Commit) ; 后期收尾的工作有创建app data根目录、dex优化、移除已有apk、发送安装成功广播。

那我们就按上面的三阶段来给大家揭开apk安装的谜底吧。


前期准备


PIS:“谁要想安装apk,首先需要调用我的PackageInstallerService的createSession方法创建一个PackageInstallerSession,一次安装会对应唯一的一个PackageInstallerSession,PackageInstallerSession才是真正干活的主力,后面的安装环节就由PackageInstallerSession来给大家介绍了,PackageInstallerSession简称Session。”

Session:“我会给使用者一个sessionId,通过这个sessionId可以从PIS找到我,拷贝是安装apk的第一步,那我们就从拷贝开始。”

拷贝

Session:“安装apk第一步是需要把apk(不管apk来源于哪)进行拷贝,拷贝到 /data/app/xxxx.tmp(xxxx是一个随机的字符串)目录下面,拷贝的apk的名字一般被命名为:base.apk,拷贝完后的apk文件的路径是**/data/app/xxxx.tmp/base.apk**这样的。”


apk:“一上来就要拷贝,这一下子把我搞懵逼了,能说说为啥要拷贝吗?我的理解是拷贝会增加apk的安装时长,如果apk特别大,安装时长更会加长,不拷贝不行吗?”

Session:“不拷贝还真不行,那我就来说下原因。“

拿adb install或者应用市场安装apk的方式来说明问题吧,Session我是运行于systemserver进程。通过adb install安装的话,apk是位于pc上,pc上的apk对于Session是肯定不能拿来直接用的;通过应用市场安装的话,apk是被应用市场进程所存储的,而Session我也是基本不可以访问的(除非apk被下载到可共享的目录)。因此我需要先把apk拷贝到我可以访问的目录下面,这样我就可以直接操作apk了。

apk:“我同意你的说法,对于adb install安装确实需要拷贝,因为apk是存储于pc上。但是通过应用市场安装是不是可以这样做:就是应用市场在从服务器下载apk的时候直接下载到一个约定好的目录中,Session你可以直接从这个目录来操作apk了,这样就不需要拷贝的过程了,安装速度肯定可以提升。”

Session:“对于你的提议是存在几个问题,约定好的目录这个目录应该是一个共享目录吧,第一个问题是:怎么样做到只有我和应用市场进程才能访问这个目录?第二个问题是:即使可以做到共享还需要对该目录进行保护,在安装期间应用市场进程是不可以对该目录进行任何修改的,也就是在安装期间只有Session我才可以操作这个共享目录。解决上面的两个问题是不是比较麻烦啊。”

apk:“说的极是,我确实没想到你说的这些问题。”

还有非常重要的一点,拷贝到的 /data/app/xxxx.tmp **目录,这个目录有一个非常重要的特性,这个目录的user和group都是system**,也就是只有systemserver进程对此目录具有读写执行权限,而其他进程只有读权限,这样就可以保证被拷贝apk的安全性了,只有Session我才可以访问、修改该目录。(如下图)
image.png

对apk拷贝可以调用我的write方法,调用我所有的方法都需要进行binder跨进程调用。

该步的产物是 /data/app/xxxx.tmp/base.apk ,后面的安装流程都基于此apk进行。 关于拷贝就介绍到此,咱们接着介绍完整性验证。

完整性验证

Session:“第二步是对 /data/app/xxxx.tmp/base.apk 进行完整性验证。完整性验证 用一句话概括就是:验证apk有没有被改过。这一步肯定是要最先进行的,只有我先确认apk是一个完整的apk才有必要进行后面的安装流程。“

apk问到:“apk被改动会存在哪些危害?”

Session:“比如有个高人下载了微信的apk,抛开加固等黑科技,这位高人解压了微信apk,并且在其中插入了自己的代码(比如把聊天信息上传到自己的服务器上)在重新打包成微信apk,那这个时候的微信apk被用户安装上的话,你可以想想这有多危险,用户和别人的聊天信息他都可以知道了。apk完整性验证就是要验证apk有没有被改过,改过的话那就完全认为这个apk是被动过手脚的,肯定不允许安装的。”

apk:“那又是如何能验证apk没有被改动过呢?”

Session:”我先从雏形说起,这样可以更容易理解从雏形到最终方案是如何一步一步形成的。刚开始的验证雏形是这样的:我Session需要从apk内拿到一个信息,这个信息是与apk是一一对应关系,也就是apk内不管发生任何变化,那这个信息也需要发生变化,并且我需要根据apk能推导或计算出这个信息,如果推导或计算的信息与apk内拿到的信息一致就可以证明apk是没有被修改过的。那怎么样可以做到呢?答案是使用摘要算法。“


apk迷惑的问到:“摘要算法,这又是啥子嘛?”

举个例子人类读完一篇文章后,这篇文章总会有个中心思想之类的总结,那这个总结就是一个摘要。摘要算法:会接受一个输入,不论输入的内容是多长都会输出一个固定长度的内容,输入内容一样才会有一样的输出,输入内容不一样输出内容也不会一样,并且这个输出内容是不可逆的。
可以使用摘要算法对apk的各种文件生成摘要,这些摘要信息会写入apk。验证apk完整性的进一步思路是这样的:使用摘要算法对apk的各种文件生成摘要,如果生成的摘要与apk内存的摘要信息一致则证明apk是没有被修改过的。

Session:”apk老兄,你觉得上面的的思路有啥问题吗?“

apk:“我想想啊,想到了,这些摘要信息没有加密,如若改动了apk内的内容,则也可以重新把摘要内容改了,重新打包到apk内。因此需要对摘要信息进行加密。”

你说的非常的对,对摘要信息进行加密需要用到非对称加密(https中就用到它),非对称加密是一种加密算法,分为公钥和私钥,公钥是可以公开的,私钥是不能公开的,用私钥对信息加密,是可以用公钥解密的。需要用私钥对摘要信息进行加密,把加密后的摘要信息和证书(证书存储了公钥和开发者的一些信息)一同打包到apk内。我把这个过程起了一个很好听的名字apk签名,就如人类在合同上签名一样,每个apk也是需要签名的,签了名后这个apk就和开发者绑定了。

总结下apk签名的过程:首先用摘要算法对apk内的各种文件生成摘要;其次使用非对称加密的私钥对这些摘要信息加密;最后把加密的摘要信息和证书(公钥和开发者信息)写入apk内。这只是对apk v1签名算法的一个简单总结,签名算法有v1、v2、v3、v4四个版本,每个版本都是为了解决前者存在的问题而诞生的。

基于apk签名,终极apk完整性验证流程如下(下面主要介绍的是签名v1版本的验证流程):

  1. 从apk中拿到证书信息,拿到加密的摘要信息
  2. 从证书中用公钥对加密的摘要信息解密,解密出摘要信息
  3. 对apk的各文件用摘要算法生成摘要,并与解密出的摘要信息进行对比,如若一致则证明没有被改动,否则发生了改动。

除了验证apk的完整性外,还会从apk中的提取签名信息,签名信息保存在SigningDetails对象中,在后面的安装流程中是要用到SigningDetails信息的,如果apk没有获取到签名信息,则会停止安装(正常咱们开发的debug版的app是已经默认进行了签名)。
apk完整性验证是安装的必要环节,如果apk完整性验证失败,则停止安装;否则继续下一步的安装流程。

该步的产物是SigningDetails对象以及apk是否完整的,SigningDetails对象会在后面的安装流程用到。

完整性验证的部分代码如下,有兴趣可以看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
less复制代码//文件路径:frameworks/base/core/java/android/content/pm/parsing/FrameworkParsingPackageUtils.java

//获取签名信息,签名信息存储在SigningDetails
public static ParseResult<SigningDetails> getSigningDetails(ParseInput input,
String baseCodePath, boolean skipVerify, boolean isStaticSharedLibrary,
@NonNull SigningDetails existingSigningDetails, int targetSdk) {

省略代码......

//跳过验证,走这
if (skipVerify) {
省略代码......
} else {
//验证并且返回签名信息,会对apk的完整性进行校验,并且返回签名信息
verified = ApkSignatureVerifier.verify(input, baseCodePath, minSignatureScheme);
}

......省略代码
}

解析apk

Session:“有没有发现,前两步我对于安装的apk是知之甚少的,我不知道安装apk的包名、它的名字、版本号等基础信息。但是这些信息是非常非常重要的,因此这一步需要把这些信息解析出来,为后面的安装流程做准备。我把这一步称为解析apk,解析apk说的更具体点就是解析apk中的AndroidManifest(清单文件),从AndroidManifest文件中把包名、版本号、安装路径、是否是debug、是否是多架构、是否提取native libs等信息提取出来放入PackageLite对象。并不会提取四大组件信息、权限等信息,因为还暂时用不到这些信息,多解析这些信息就需要多花时间,我秉持一个用时才去解析的原则。”

该步的产物是PackageLite对象,该步的产物会在后面的安装流程用到**,**下一步就需要做提取native libs的操作。

PackageLite类的关键属性如下,有兴趣可以看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
arduino复制代码//文件路径: frameworks/base/core/java/android/content/pm/parsing/PackageLite.java
public class PackageLite {
//包名
private final @NonNull String mPackageName;

//base apk的路径
private final @NonNull String mBaseApkPath;

//版本号
private final int mVersionCode;

//app是否是debug版本
private final boolean mDebuggable;

//是否是多架构(32位和64位)
private final boolean mMultiArch;

//是否提取native libs
private final boolean mExtractNativeLibs;


......省略其他属性
}

提取native libs

Session:“这一步所要做的事情是提取native libs(native libs指的是apk中的so库),提取native libs:也就是把apk中的so库提取到 /data/app/xxxx.tmp/lib/cpuabi/ **(cpuabi是当前设备的cpu架构比如arm、arm64)目录下面。abi是Application Binary Interface的缩写,应用程序二进制接口。但是并不是所有的apk都包含了so,如果没有包含则不会执行此步。提取native libs会用到解析apk这一步解析出的PackageLite**信息”

apk中so库的所处的目录如下:

1
2
3
4
5
6
7
8
bash复制代码//base.apk,该apk中包含了两个abi:arm、arm64,(为了减小apk的大小,现在的apk都只保留一个abi)
lib/arm/xx.so
lib/arm/xxx.so
lib/arm/xxxx.so

lib/arm64/xx.so
lib/arm64/xxx.so
lib/arm64/xxxx.so

在提取native libs的时候,会检测apk中的cpu abi是否与当前设备的cpu abi是否匹配,如果不匹配比如当前设备cpu abi是x86_64的,而apk中的cpu abi只有arm、arm64,那这种情况肯定是不能继续安装的,因为so库是与cpu abi强相关的,arm下面的so库在x86_64上面运行肯定是出问题的。为了考虑性能和方便性,整个提取native libs都是委托给native的代码执行的。

提前native libs可以提前检测当前设备的cpu abi是否与apk中的so库匹配,不匹配则不安装,并且还可以提升app的启动速度,如果不提取的话,每次app启动都需要从apk中解析出这些so库,这速度肯定慢啊,该步的产物是apk中的so库提取到 **/data/app/xxxx.tmp/lib/cpuabi/ **目录下面。下一步就来看下版本号验证吧。

对应的代码如下,有兴趣可以看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
scss复制代码//文件路径:frameworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.java

private void parseApkAndExtractNativeLibraries() throws PackageManagerException {
synchronized (mLock) {
省略代码......

final PackageLite result;
if (!isApexSession()) {
//走这,解析apk信息
result = getOrParsePackageLiteLocked(stageDir, /* flags */ 0);
} else {
result = getOrParsePackageLiteLocked(mResolvedBaseFile, /* flags */ 0);
}
if (result != null) {
mPackageLite = result;
if (!isApexSession()) {
省略代码......
//提取so库
extractNativeLibraries(
mPackageLite, stageDir, params.abiOverride, mayInheritNativeLibs());
}
}
}
}

版本号验证

Session:“到了版本号验证这一步了,但是这步不是必须,如果设备上已经安装了相同包名的apk,则该步是必须的,版本号验证所要做的事情非常简单:正在安装的apk的版本号是否比已经安装的apk的版本号小,小的话就停止安装。正在安装的apk的版本号从解析apk中的PackageLite拿到。”

对应的代码如下,有兴趣可以看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码//文件路径:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java
Pair<Integer, String> verifyReplacingVersionCode(PackageInfoLite pkgLite,
long requiredInstalledVersionCode, int installFlags) {
if ((installFlags & PackageManager.INSTALL_APEX) != 0) {
return verifyReplacingVersionCodeForApex(
pkgLite, requiredInstalledVersionCode, installFlags);
}

String packageName = pkgLite.packageName;
synchronized (mPm.mLock) {

省略代码......

//dataOwnerPkg代表设备已经安装对应的apk了
if (dataOwnerPkg != null && !dataOwnerPkg.isSdkLibrary()) {
//只有debug版本才允许版本降级
if (!PackageManagerServiceUtils.isDowngradePermitted(installFlags,
dataOwnerPkg.isDebuggable())) {
try {
//检测是否存在版本降级,是的话会报错
PackageManagerServiceUtils.checkDowngrade(dataOwnerPkg, pkgLite);
} catch (PackageManagerException e) {
String errorMsg = "Downgrade detected: " + e.getMessage();
Slog.w(TAG, errorMsg);
return Pair.create(
PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE, errorMsg);
}
}
}
}
return Pair.create(PackageManager.INSTALL_SUCCEEDED, null);
}

总结

前期准备阶段又划分为拷贝、完整性验证、解析apk、提取native libs、版本号验证五步,每一步都在为后一步做准备。

拷贝会把安装的apk拷贝到**/data/app/xxxx.tmp/base.apk**。
完整性验证会对**/data/app/xxxx.tmp/base.apk进行验证,如果修改过则停止安装,同时还会提取签名信息到SigningDetails对象,如果apk没有签名信息则会停止安装,SigningDetails对象会在后面的安装流程用到。
**解析apk
会从/data/app/xxxx.tmp/base.apk的AndroidManifest中把包名、版本号、安装路径、是否是debug等信息提取出来放入PackageLite对象,若解析中发生错误也会停止安装。
提取native libs的时候会用到 PackageLite对象,会把/data/app/xxxx.tmp/base.apk**中的so库提取到 /data/app/xxxx.tmp/lib/cpuabi/ 目录(若apk存在so库),若发生错误则也会停止安装。
**版本号验证
的工作内容是正在安装的apk的版本号是否比已经安装的apk的版本号小,小的话就停止安装。这步不是必须的,只有设备上已经安装了相同包名的apk才执行。

前期准备的各步都能正常执行的话,就进入正式的安装阶段,那我们来看下安装阶段的内容。

安装

Session:“具体安装阶段的工作内容由InstallPackageHelper来完成的,那就有请它来给大家介绍。”
InstallPackageHelper:“大家好啊,终于轮到我出场了,安装阶段也可以称为正式安装,在这阶段才真正开始apk的安装工作。那我就来介绍下吧。”

安装阶段可以分为四步:准备 (Prepare) 、扫描 (Scan)、调和 (Reconcile)、提交 (Commit),这四步整体是原子化操作,也就是只要有一个出问题,整体的安装就停止,下面就来介绍下这四步。

准备 (Prepare)

完全解析apk

还记得解析apk那步会把apk的基础信息存放到PackageLite对象吗,这只是解析了比较少的基础信息。完全解析apk就是从**/data/app/xxxx.tmp/base.apk的AndroidManifest中把所有的信息都解析出来,包含了声明的四大组件、权限、meta-data、shareLibs等,这些信息会存放在ParsedPackage对象中,如果解析发生错误,则停止安装。解析出ParsedPackage后,后面的工作都是围绕ParsedPackage**展开的。

保存签名

在完整性验证那步是保存了签名信息到SigningDetails对象的,如果SigningDetails不为null的话会把SigningDetails存入ParsedPackage中;否则从apk中解析出SigningDetails存入ParsedPackage。

签名验证

签名验证的工作内容是对正在安装的apk的证书信息与设备上已经安装的相同包名的apk的证书信息进行对比,如果不一致,则停止安装。如果设备上不存在相同包名的apk则这一步是不会进行的。比如设备上安装了微信,如果有一个apk它的包名与微信一样,签名肯定不一样的情况下。这时候往设备上安装此apk肯定是安装不上的。

权限验证

权限验证就是根据ParsedPackage里的getPermissions()方法获取的权限,来判断哪些权限是存在问题的,比如声明了只有系统app才能使用的权限,如果存在问题则停止安装。

重命名

还记得拷贝第一步的时候生成的临时目录 /data/app/xxxx.tmp/ 吗?这毕竟是个临时目录,是有必要给它一个正式的名字的,那重命名所做的事情就是把 /data/app/xxxx.tmp/ 重命名为 /data/app/~~[randomStrA]/[packageName]-[randomStrB](其中randomStrA、randomStrB是随机生成的字符串,packageName是包名),这个名字看上去确实不是很正规,但是它确实是一个非常正式的名字。


apk:“那我有个问题啊,为什么重命名的名字没有用包名,而是用一个随机字符串呢?”
InstallPackageHelper:“用随机字符串的原因是,在 **/data/app/ **目录下面会存在两个同一包名apk的情况,如果用包名的话会出现问题。比如当前设备上已经安装了一个微信apk,则在 **/data/app/com.weixin/ **目录下会存在微信的apk。这时候安装一个高版本的微信apk的,这时候重命名的话就出现问题,因为已经有com.weixin目录存在了。”
如果重命名失败也会停止安装。下面是重命名的例子,可以看到它们的user、group都是system
image.png

如下正式apk父目录的相关代码,有兴趣可以看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
arduino复制代码//文件路径:services/core/java/com/android/server/pm/PackageManagerServiceUtils.java
/**
* 返回的目录结构样子: targetDir/~~[randomStrA]/[packageName]-[randomStrB]
*/
public static File getNextCodePath(File targetDir, String packageName) {
SecureRandom random = new SecureRandom();
byte[] bytes = new byte[16];
File firstLevelDir;
do {
random.nextBytes(bytes);
String firstLevelDirName = RANDOM_DIR_PREFIX
+ Base64.encodeToString(bytes, Base64.URL_SAFE | Base64.NO_WRAP);
firstLevelDir = new File(targetDir, firstLevelDirName);
} while (firstLevelDir.exists());

random.nextBytes(bytes);
String dirName = packageName + RANDOM_CODEPATH_PREFIX + Base64.encodeToString(bytes,
Base64.URL_SAFE | Base64.NO_WRAP);
final File result = new File(firstLevelDir, dirName);
if (DEBUG && !Objects.equals(tryParsePackageName(result.getName()), packageName)) {
throw new RuntimeException(
"codepath is off: " + result.getName() + " (" + packageName + ")");
}
return result;
}

总结

当然除了上面的这些工作外,还做了尝试杀死当前同包名的app进程(如果设备上已经有相同包名的apk并且处于运行状态),构造需要移除的信息PackageRemovedInfo对象(如果设备上已经有相同包名的apk,则需要把它的信息在后面的流程中移除掉,因为这些信息毕竟是老apk的信息)。

准备阶段所做的主要事情有:把 /data/app/xxxx.tmp/base.apk 的AndroidManifest中把所有的信息都解析出来,存在ParsedPackage对象中,进行了签名、权限等验证,把 /data/app/xxxx.tmp/ 目录重命名为 /data/app/~~[randomStrA]/[packageName]-[randomStrB] 目录。若准备阶段发生了错误,则会停止安装。准备阶段的产物是ParsedPackage(它在后面的安装流程会用到),咱们进入扫描阶段。

准备阶段对应的一部分源码如下(源码实在是太多了,只列出一部分),有兴趣可以看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
java复制代码private PrepareResult preparePackageLI(InstallArgs args, PackageInstalledInfo res)
throws PrepareFailure {

省略代码......
final ParsedPackage parsedPackage;
try (PackageParser2 pp = mPm.mInjector.getPreparingPackageParser()) {
//完全解析apk
parsedPackage = pp.parsePackage(tmpPackageFile, parseFlags, false); //niu 解析apk中更具体的信息 放入ParsedPackage
AndroidPackageUtils.validatePackageDexMetadata(parsedPackage);
} catch (PackageManagerException e) {
throw new PrepareFailure("Failed parse during installPackageLI", e);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}

省略代码......

//设置签名信息
if (args.mSigningDetails != SigningDetails.UNKNOWN) {
parsedPackage.setSigningDetails(args.mSigningDetails);
} else {
final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
final ParseResult<SigningDetails> result = ParsingPackageUtils.getSigningDetails(
input, parsedPackage, false /*skipVerify*/);
if (result.isError()) {
throw new PrepareFailure("Failed collect during installPackageLI",
result.getException());
}
parsedPackage.setSigningDetails(result.getResult());
}
省略代码......

}

扫描 (Scan)

InstallPackageHelper:“扫描这步主要的作用就是完善ParsedPackage的信息,同时用ParsedPackage的信息创建或者更新已有的PackageSetting。关于PackageSetting还需要有请Settings来介绍下。”

Settings:“在安装apk后,肯定需要把安装的apk相关的信息记录下来,这些信息比如包名、版本号、apk路径、native code路径、appid、签名相关信息等,这些信息都是与安装的apk是一一对应并且不会变化的。而还有一些信息是与当前设备的用户有关的(比如当前设备存在多用户),则是需要记录下每个用户是否安装了这个apk、安装apk的时间等信息。 上面的这些信息肯定是需要记录并且需要持久化到内部存储空间的。这些信息被放在PackageSetting对象中,一个已安装的apk会对应自己的PackageSetting,也就是说PackageSetting存储了已安装apk相关的信息。而这些信息会最终持久化到packages.xml文件中。”

生成appId
每个被安装的apk都会有自己的appId,appId它是一个整数,如果在AndroidManifest中配置了android:sharedUserId则配置了相同sharedUserId的apk的appId是一样的。扫描的最后一步是为apk生成它的appId,这样被安装的apk就有了“正式身份”。

生成appId的代码如下,有兴趣看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码//文件路径:Settings.java
boolean registerAppIdLPw(PackageSetting p, boolean forceNew) throws PackageManagerException {
final boolean createdNew;
Slog.i(TAG, "niulog install registerAppIdLPw p:" + p + " forceNew:" + forceNew + " appid:" + p.getAppId());
if (p.getAppId() == 0 || forceNew) {
// Assign new user ID
p.setAppId(mAppIds.acquireAndRegisterNewAppId(p));
createdNew = true;
} else {
// Add new setting to list of user IDs
createdNew = mAppIds.registerExistingAppId(p.getAppId(), p, p.getPackageName());
}
if (p.getAppId() < 0) {
PackageManagerService.reportSettingsProblem(Log.WARN,
"Package " + p.getPackageName() + " could not be assigned a valid UID");
throw new PackageManagerException(INSTALL_FAILED_INSUFFICIENT_STORAGE,
"Package " + p.getPackageName() + " could not be assigned a valid UID");
}
return createdNew;
}

扫描这步会为apk生成appId,同时会完善ParsedPackage的信息,扫描过程如果发生错误也会停止安装。这一步的产物是PackageSetting,它会被后面的安装流程用到。下面来介绍下调和这步。

调和 (Reconcile)

调和这步主要是利用准备、扫描的产物来验证当前apk使用到的shared libs是否存在、真实有效、是否重复申请等,如果验证失败则停止安装,比如在apk的AndroidManifest文件中用申明了一个不存在的lib则肯定是不能安装的;同时还会创建DeletePackageAction(它会把设备上相同包名的apk(称老apk)信息包含进来)如果设备上存在老apk,创建DeletePackageAction的目的是为了在后面的安装阶段可以把老apk的信息删除。

下面是它的源码(由于源码篇幅太长,只把方法名展示出来),有兴趣可以看下

1
2
3
4
5
6
7
8
arduino复制代码//文件名:ReconcilePackageUtils.java
public static Map<String, ReconciledPackage> reconcilePackages(
final ReconcileRequest request, SharedLibrariesImpl sharedLibraries,
KeySetManagerService ksms, Settings settings)
throws ReconcileFailure {

省略代码......
}

提交 (Commit)

InstallPackageHelper:“提交是安装的最后一步了,提交的主要工作内容就是对上面准备、扫描、调和的产物PackageSetting和ParsedPackage提交给Settings和PMS,让它们把各自更新自己的状态。那就由它们来介绍吧。”

Settings:“首先由我来介绍吧,调用我的insertPackageSettingLPw方法可以把PackageSetting和ParsedPackage更新我的mPackages属性 (它以包名为key,PackageSetting为value存放所有的已安装apk)。并且会把它们持久化到packages.xml文件,这样当下次设备重新启动的时候,就可以从packages.xml中把所有已安装apk的信息都读取到,每个已安装apk对应自己的PackageSetting,如果想知道当前设备已安装了哪些apk,可以‘呼我哦’。”

PMS:“该轮到我了,我有个非常重要的属性mPackages(它同样以包名为key,AndroidPackage为value存放所有已安装的apk,ParsedPackage是AndroidPackage的子类) ,我会把ParsedPackage添加到mPackages属性中。同时我还有个属性mComponentResolver,它可以把ParsedPackage中的四大组件‘收拢’起来。只有经过这些操作,在运行该apk的时候才能从我这检索到对应apk里面的四大组件信息,进而apk才能运行。”

总结

InstallPackageHelper郑重的对apk说:“恭喜你啊,经过安装阶段,你终于找到了你的‘归宿 ’/data/app/~~[randomStrA]/[packageName]-[randomStrB]目录,从此你就可以在这台设备上发挥你的价值了。这个目录它的user和group都是system,也就是说只有systemserver进程才有权读写执行该目录,而其他用户只能读的权限,这样就可以保证该目录的安全性。这也就是为啥在apk运行时候,是可以把该目录下的apk文件和lib下的各种so文件加载到自己进程的ClassLoader的原因。”

InstallPackageHelper又说:“虽然apk你找到了自己的‘归宿’,但是你的AndroidManifest声明的各种数据还没有传递给PMS,因为PMS是包管理者它管理着系统里的所有的apk信息,系统中谁想知道哪个apk安装了?哪个apk都声明了哪些组件?哪个apk声明了哪些权限等等这些信息都需要向PMS来要。因此需要把从apk中的AndroidManifest中解析出来的ParsedPackage信息传递给PMS,这样其他查询者比如ActivityManagerService就可以从PMS查到这些信息了。”

InstallPackageHelper:“系统里面安装了哪些apk,这都是需要记录并且持久化到内部存储空间的,而Settings就负责这个事件,新安装的apk会生成一个PackageSetting对象(它记录了apk的包名、版本号、签名信息、apk路径、哪些user安装了、安装时间等信息),PackageSetting对象会传递给Settings,Settings把它加入内存并且持久化到packages.xml文件中。”

下面是安装阶段的代码,有兴趣看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
dart复制代码//文件:InstallPackageHelper.java
private void installPackagesLI(List<InstallRequest> requests) {
final Map<String, ScanResult> preparedScans = new ArrayMap<>(requests.size());
final Map<String, InstallArgs> installArgs = new ArrayMap<>(requests.size());
final Map<String, PackageInstalledInfo> installResults = new ArrayMap<>(requests.size());
final Map<String, PrepareResult> prepareResults = new ArrayMap<>(requests.size());
final Map<String, Settings.VersionInfo> versionInfos = new ArrayMap<>(requests.size());
final Map<String, Boolean> createdAppId = new ArrayMap<>(requests.size());
boolean success = false;
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackagesLI");
for (InstallRequest request : requests) {
// TODO(b/109941548): remove this once we've pulled everything from it and into
// scan, reconcile or commit.
final PrepareResult prepareResult;
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "preparePackage");
//1.prepare阶段,会解析apk中的信息主要是AndroidManifest,解析出来的实体是ParsedPackage(解析的信息更全包含了四大组件等),若不是一个正确的apk则不会继续下面的步骤;若是正确的apk,则会对apk的签名、shareuserid以及是替换老apk还是新apk做处理
prepareResult =
preparePackageLI(request.mArgs, request.mInstallResult);
} catch (PrepareFailure prepareFailure) {
request.mInstallResult.setError(prepareFailure.error,
prepareFailure.getMessage());
request.mInstallResult.mOrigPackage = prepareFailure.mConflictingPackage;
request.mInstallResult.mOrigPermission = prepareFailure.mConflictingPermission;
return;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
request.mInstallResult.setReturnCode(PackageManager.INSTALL_SUCCEEDED);
request.mInstallResult.mInstallerPackageName =
request.mArgs.mInstallSource.installerPackageName;

final String packageName = prepareResult.mPackageToScan.getPackageName();
Slog.i(TAG,"niulog install installPackagesLI prepare request = "+request+" packageName = "+packageName);
prepareResults.put(packageName, prepareResult);
installResults.put(packageName, request.mInstallResult);
installArgs.put(packageName, request.mArgs);
try {
// 2.扫描阶段,扫描阶段主要是构造或者使用原有的PkgSetting
final ScanResult result = scanPackageTracedLI(
prepareResult.mPackageToScan, prepareResult.mParseFlags,
prepareResult.mScanFlags, System.currentTimeMillis(),
request.mArgs.mUser, request.mArgs.mAbiOverride);
if (null != preparedScans.put(result.mPkgSetting.getPkg().getPackageName(),
result)) {
request.mInstallResult.setError(
PackageManager.INSTALL_FAILED_DUPLICATE_PACKAGE,
"Duplicate package "
+ result.mPkgSetting.getPkg().getPackageName()
+ " in multi-package install request.");
return;
}
if (!checkNoAppStorageIsConsistent(
result.mRequest.mOldPkg, result.mPkgSetting.getPkg())) {
// TODO: INSTALL_FAILED_UPDATE_INCOMPATIBLE is about incomptabible
// signatures. Is there a better error code?
request.mInstallResult.setError(
INSTALL_FAILED_UPDATE_INCOMPATIBLE,
"Update attempted to change value of "
+ PackageManager.PROPERTY_NO_APP_DATA_STORAGE);
return;
}
createdAppId.put(packageName, optimisticallyRegisterAppId(result)); //niu 生成或者使用原有appid
versionInfos.put(result.mPkgSetting.getPkg().getPackageName(),
mPm.getSettingsVersionForPackage(result.mPkgSetting.getPkg()));
Slog.i(TAG,"niulog install installPackagesLI scan request = "+request+" ScanResult.result = "+result+" versionInfo:"+mPm.getSettingsVersionForPackage(result.mPkgSetting.getPkg()));
} catch (PackageManagerException e) {
request.mInstallResult.setError("Scanning Failed.", e);
return;
}
}
ReconcileRequest reconcileRequest = new ReconcileRequest(preparedScans, installArgs,
installResults, prepareResults,
Collections.unmodifiableMap(mPm.mPackages), versionInfos); //niu 用prepare和scan阶段的数据构造ReconcileRequest
CommitRequest commitRequest = null;
synchronized (mPm.mLock) {
Map<String, ReconciledPackage> reconciledPackages;
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "reconcilePackages");
// 调和阶段
reconciledPackages = ReconcilePackageUtils.reconcilePackages(
reconcileRequest, mSharedLibraries,
mPm.mSettings.getKeySetManagerService(), mPm.mSettings);
printPkg(reconciledPackages,"niulog install installPackagesLI reconcile");
} catch (ReconcileFailure e) {
for (InstallRequest request : requests) {
request.mInstallResult.setError("Reconciliation failed...", e);
}
return;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "commitPackages");
commitRequest = new CommitRequest(reconciledPackages,
mPm.mUserManager.getUserIds()); //niu 构建CommitRequest(把前面各种阶段的信息都收集起来)
//进入commit阶段
commitPackagesLocked(commitRequest);
success = true;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}

} finally {
省略代码......
}
}

后期收尾

终于到了后期收尾阶段,为啥要叫后期收尾呢?是因为这一阶段所做的事情即使出现了错误也不会影响上面apk安装成功的结果,那就来看下后期收尾都做了哪些事情。

创建app data根目录

关于为什么创建app data根目录以及都创建了哪些目录可以参考installd进程,在这篇就不赘述了。创建app data根目录是委托了Installer,Installer在通过binder通信的方式让installd进程帮忙创建的。只有创建app data根目录成功后,apk才可以运行起来。

dex优化

关于dex优化可以参考installd进程,同样dex优化也是委托Installer实现的,最终也是转交由installd进程帮忙实现的。dex优化即使不成功也不会影响apk的运行,但是会影响apk的运行速度。

创建app data根目录和dex优化的源代码如下,有兴趣可以看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
ini复制代码//文件:InstallPackageHelper.java
private void executePostCommitSteps(CommitRequest commitRequest) {
final ArraySet<IncrementalStorage> incrementalStorages = new ArraySet<>();
for (ReconciledPackage reconciledPkg : commitRequest.mReconciledPackages.values()) {
final boolean instantApp = ((reconciledPkg.mScanResult.mRequest.mScanFlags
& SCAN_AS_INSTANT_APP) != 0);
final AndroidPackage pkg = reconciledPkg.mPkgSetting.getPkg();
final String packageName = pkg.getPackageName();
final String codePath = pkg.getPath();
final boolean onIncremental = mIncrementalManager != null
&& isIncrementalPath(codePath);

省略代码......

//创建app data根目录
mAppDataHelper.prepareAppDataPostCommitLIF(pkg, 0); //niu 创建 data目录

省略代码......

final boolean performDexopt =
(!instantApp || android.provider.Settings.Global.getInt(
mContext.getContentResolver(),
android.provider.Settings.Global.INSTANT_APP_DEXOPT_ENABLED, 0) != 0)
&& !pkg.isDebuggable()
&& (!onIncremental)
&& dexoptOptions.isCompilationEnabled();

//并不是所有的apk都需要dex优化,如果需要优化,进入下面逻辑
if (performDexopt) {
省略代码......

//开始优化
mPackageDexOptimizer.performDexOpt(pkg, realPkgSetting,
null /* instructionSets */,
mPm.getOrCreateCompilerPackageStats(pkg),
mDexManager.getPackageUseInfoOrDefault(packageName),
dexoptOptions);
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}

省略代码......
}
PackageManagerServiceUtils.waitForNativeBinariesExtractionForIncremental(
incrementalStorages);
}

移除已有apk

如果设备上已经安装了相同包名的apk(称它为老apk),则在新apk安装成功后是需要把老apk删除的,删除过程也同样是委托Installer,最终转交由installd进程来实现。即使老apk删除失败也不会影响新apk。

下面是对应源码,有兴趣看下

1
2
3
4
5
6
7
8
9
10
arduino复制代码//文件:Installer.java
public void rmPackageDir(String packageName, String packageDir) throws InstallerException {
if (!checkBeforeRemote()) return;
BlockGuard.getVmPolicy().onPathAccess(packageDir);
try {
mInstalld.rmPackageDir(packageName, packageDir);
} catch (Exception e) {
throw InstallerException.from(e);
}
}

发送安装成功广播

既然一个apk安装成功了,那肯定是需要通知关注者的,采用的方式是发广播,比如桌面在收到安装成功的广播后,修改正在安装apk的状态。

下面是发送广播源码,有兴趣看下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ini复制代码//文件:PackageInstallerSession.java
private void dispatchSessionFinished(int returnCode, String msg, Bundle extras) {
sendUpdateToRemoteStatusReceiver(returnCode, msg, extras);

synchronized (mLock) {
mFinalStatus = returnCode;
mFinalMessage = msg;
}

final boolean success = (returnCode == INSTALL_SUCCEEDED);


final boolean isNewInstall = extras == null || !extras.getBoolean(Intent.EXTRA_REPLACING);
if (success && isNewInstall && mPm.mInstallerService.okToSendBroadcasts()) {
//收集apk的信息,把这些信息通过广播发送出去
mPm.sendSessionCommitBroadcast(generateInfoScrubbed(true /*icon*/), userId);
}

mCallback.onSessionFinished(this, success);
if (isDataLoaderInstallation()) {
logDataLoaderInstallationSession(returnCode);
}
}

总结

到此apk的安装之谜算是揭开了,apk的安装会经过前期准备、安装、后期收尾这三个阶段,前期准备成功后才会进入安装阶段,安装阶段成功后才会进入后期收尾阶段。除了后期收尾外,前两个阶段只要发生错误就会停止apk的安装。
apk的安装可以总结为下面几步:

  1. 不管apk是通过adb安装的(apk存储于PC的磁盘)还是应用市场安装的(apk存储于设备),首先apk会被拷贝到 /data/app/xxx.tmp目录下面(xxx是一个随机生成的字符串)
  2. 在经过重重的验证、校验(签名、版本号),/data/app/xxx.tmp目录会重命名为 /data/app/~~[randomStrA]/[packageName]-[randomStrB] 目录,也就是被拷贝的apk最终路径是 /data/app/~~[randomStrA]/[packageName]-[randomStrB]/base.apk。同时会为apk生成一个唯一的id又称appid
  3. 解析apk的AndroidManifest中的内容为ParsedPackage,ParsedPackage中的权限等信息经过验证通过后,ParsedPackage传递给PMS,这样其他使用者比如ActivityManagerService就可以从PMS获取刚安装apk的信息了。
  4. 刚安装的apk的安装信息比如包名、版本、签名证书、安装时间等会存储到PackageSetting,PackageSetting会传递给Settings,Settings会把它持久化到packages.xml文件。
  5. 创建app data根目录,app data根目录是apk运行期间数据存储的根目录,并且app data根目录只有当前apk程序有读写执行权,其他不用没有任何权限。
  6. 对apk的dex进行优化,优化即使不成功也不影响apk的安装,dex优化可以保证app运行性能上的提升。
  7. 发送安装成功广播。

apk越大包含的so越多,安装apk的时间越长。主要时长体现在拷贝、提取native libs、dex优化这几项工作。

欢迎关注我的公众号–牛晓伟(搜索或者点击牛晓伟链接)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

安卓webrtc在P2P场景下实现屏幕+系统声音共享 背景

发表于 2024-04-27

背景

用webrtc实现P2P的屏幕+声音共享,不使用云端信令服务器,分享屏幕的同时,推送当前系统播放的声音,实现类似云渲染的效果。

分析

  1. 信令
  • 信令服务是必须存在的,否则无法在设备间交换WEBRTC的SDP和ICE Candidate以及业务的通话指令,在局域网,可以在其中一个设备上,把自己当成信令服务器,让其它端通过IP地址或者扫码连接的方式,连接上这个信令服务器,建立socket或者websocket连接。
  • 在github可以找到现成的,局域网内直接能用的扫码连接P2P通话的工程:github.com/meshenger-a…
  1. 编码
  • 低端手机上,性能不够,所以需要选择合适的硬编码器,提升编码效率,最新版本的官方SDK已经支持H265硬编码了,所以在编码器的优先级设置上,把H265设置为优先选项,具体逻辑参考工程内的:
    HardwareExtendedVideoEncoderFactory.kt
  1. 延迟
  • webrtc内部使用jitter做抖动缓冲,但是也间接的导致了延迟的增加,在部分场景下,宁愿牺牲流畅性,换来更低的延时,之前测试过在webrtc的m115版本中,可以通过字段设置,减少延迟,实测收益能达到30ms以上
1
2
3
4
5
6
kotlin复制代码strFieldTrails += "WebRTC-ForcePlayoutDelay/min_ms:0,max_ms:50/"

PeerConnectionFactory.InitializationOptions.builder(callActivity!!.getContext())
.setEnableInternalTracer(true)
.setFieldTrials(strFieldTrails)
.createInitializationOptions()

这里需要注意的是,之前在iOS上设置这个字段,实际效果远不如Android,主要有几个方面

  • 延迟并没有明显降低 — 可以通过修改C++层代码解决,实测能达到和Android接近的效果
  • 导致开画速度很慢 — 可以通过修改C++层代码解决,实测能达到和Android接近的效果
  • 导致渲染掉帧(objc层的渲染是保留最后一帧,但是渲染的触发是按刷新率来的,例如33ms渲染一次,减少延迟导致这33ms期间有多帧已经准备好了,但是没有渲染的机会) — 可以通过修改objc层代码解决,实测能达到和Android接近的效果
  1. 系统声音
  • 直接用webrtc的API,就能很方便的创建本地音频
1
rust复制代码MediaConstraints->AudioSource->AudioTrack->RtpSender

但是这样创建的本地音轨,使用的是设备的麦克风,在通话场景中是合适的,但是在类似云渲染的场景下,要同时把屏幕和设备内部的播放声音一起推流出来,就需要做定制(不修改官方的SDK源码),在业务层,通过ForegroundService + mediaProjection + AudioRecord 录制系统内部音频。我们不生产代码,我们只是代码的搬运工,前面所说的录制系统声音部分,直接从github.com/ant-media/W… 这个代码仓库搬运过来即可。核心代码参考工程内的:
WebRtcAudioRecord.java

其它

  1. 原工程使用的是摄像头呼叫的效果,把新代码中的isScreencast改成false就能回滚到原工程效果,使用摄像头呼叫通话
  2. demo直接运行起来,就能进行点对点的屏幕+声音分享,代码仓库:github.com/baihua666/m…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go详解常见设计模式之(一)-工厂模式 简单工厂: 工厂方法

发表于 2024-04-27

该系列文章总结常见设计模式的概念、使用场景与Go的实现方案,,但实际上Go语言并不需要刻意地去过度使用设计模式,反而与Go大道至简地思想冲突。

本篇介绍工厂模式(简单工厂、工厂方法、抽象工厂)


简单工厂:

先提出一个场景:

假设现目前有一个水果的类,该类下有一个show方法,不同的水果show的逻辑不同,与之对应的初始化逻辑也不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
go复制代码//水果类
type Fruit struct {
//...
//...
//...
}

func (f *Fruit) Show(name string) {
if name == "apple" {
fmt.Println("apple show")
} else if name == "banana" {
fmt.Println("banana show")
} else if name == "pear" {
fmt.Println("pear show")
}
}

//创建一个Fruit对象
func NewFruit(name string) *Fruit {
fruit := new(Fruit)

if name == "apple" {
// newAppleFunc()
} else if name == "banana" {
//newBananaFunc()
} else if name == "pear" {
//newPearFunc
}

return fruit
}

func newAppleFunc(){}
func newBananaFunc()P{}
func newPearFunc(){}

现在看着可能还好,但是不光是show方法还是初始化方法,在每多一个水果时,都需要去方法里面修改,其违反了设计模式中的开闭原则。

其次show方法和NewFruit中代码量会越来越多,职责也会越来越大,导致代码耦合越来越严重,不论是阅读难度还是代码重用都非常的差。

而如果使用工厂模式
在业务方法和类之间添加一个中间层—工厂类,这样做能够带来的好处是:

1.实现类和业务方法之间的解耦,如果类的构造过程发生变更,可以统一收口在工厂类中进行处理,从而对业务方法屏蔽相关细节

2.倘若有多个类都聚拢在工厂类中进行构造,这样各个类的构造流程中就天然形成了一个公共的切面,可以进行一些公共逻辑的执行

例如使用简单工厂对上述代码进行重构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
go复制代码type Fruit interface {
   show()
}

type Apple struct {
    name string
}


func NewApple(name string) Fruit {
    return &Orange{
        name: name,
    }
}


func (a *Apple) show() {
    fmt.Printf("i am apple: %s", a.name)
}

type Bannana struct {
    name string
}


func NewBannana(name string) Fruit {
    return &Bannana{
        name: name,
    }
}


func (b *Bannana) show() {
    fmt.Printf("i am Bannana : %s",b.name)
}

//============factory========================

type FruitFactory struct {
}


func NewFruitFactory() *FruitFactory {
    return &FruitFactory{}
}


func (f *FruitFactory) CreateFruit(typ string) (Fruit, error) {
    src := rand.NewSource(time.Now().UnixNano())
    rander := rand.New(src)
    name := strconv.Itoa(rander.Int())


    switch typ {
    case "apple":
        return NewApple(name), nil
    case "strawberry":
        return NewBanana(name), nil
    case "cherry":
        return NewCherry(name), nil
    default:
        return nil, fmt.Errorf("fruit typ: %s is not supported yet", typ)
    }
}

简单工厂uml:

image.png

如上述简单工厂的设计模式,业务代码就无序与水果类进行耦合去关心水果类,它只与工厂模块进行依赖;

但是虽然这样设计简单清晰,但是也存在弊端。

例如当每增加一个水果,依然需要去修改createFruit方法的业务逻辑,依然违背了开闭原则,后续水果过多该方法也会因为过多的分支造成复杂度过高。其次对工厂类职责过重,一旦不能工作,系统受到影响

简单工厂总结

综上,简单工厂主要是在类层与业务层中间创建了一个工厂分离对象的创建与使用

大致创建步骤:

  • • 对于拟构造的组件,需要依据其共性,抽离出一个公共 interface
  • • 每个具体的组件类型对 interface 加以实现
  • • 定义一个具体的工厂类,在构造器方法接受具体的组件类型,完成对应类型组件的构造

而优缺点就如我上述所说:

  • 优点:实现了对象创建和使用的分离,简单直观
  • 缺点:组件类扩展时,需要直接修改工厂的组件构造方法,不符合开闭原则,对工厂类职责过重,一旦不能工作,系统受到影响。构造函数复杂度会越来越高

工厂方法:

工厂方法主要是针对简单工厂模式存在的劣势进行的优化。

其不同的地方在于,工厂方法会将水果工厂这个中间人也抽象为一个接口,而creteFruit则为接口下的方法。而每个水果都会有自己的工厂类去实现水果工厂的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
go复制代码
type FruitFactory interface {
    CreateFruit() Fruit
}


type AppleFactory struct {
}


func NewAppleFactory() FruitFactory {
    return &AppleFactory{}
}


func (a *AppleFactory) CreateFruit() Fruit {
    return NewApple("")
}


type StrawberryFactory struct {
}


func NewStrawberryFactory() FruitFactory {
    return &StrawberryFactory{}
}


func (s *StrawberryFactory) CreateFruit() Fruit {
    return NewStrawberry("")
}


type BananaFactory struct {
}


func NewBananaFactory() FruitFactory {
    return &BananaFactory{}
}


func (b *BananaFactory) BananaFruit() Fruit {
    return NewBanana("")
}

这样去设计工厂类的话,与简单工厂不同的是,你会发现这样的设计模式下,即便后续有频繁扩展水果实现类的需求,也无须对老模块的代码进行修改,而是需要扩展实现一个水果 Fruit 的实现类以及对应的水果工厂实现类即可

简而言之其实工厂方法就是在简单工厂的基础上再对工厂进行了一层抽象。

优点:

  • 系统的可扩展性也就变得非常好,无需修改接口和原类。
  • 对于新产品的创建,符合开闭原则。

缺点:

  • 增加系统中类的个数,复杂度和理解度增加。
  • 增加了系统的抽象性和理解难度。

工厂方法UML:

image.png

抽象工厂模式:

在工厂方法中通过对工厂进行抽象,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。因此,可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产。


例如:水果有不同类型:苹果、香蕉。假设只有这三种水果,不会扩展

对应的每个水果都会有自己的一个生产工厂。

这时候如果引入了不同的工厂,例如重庆、四川、北京。而对应的苹果就需要三个不同的工厂(重庆、四川、北京),那使用工厂方法就会导致工厂就会非常多,代码非常繁琐。这种场景就可以使用抽象工厂模式。


抽象工厂方法模式”引出了产品族和产品等级结构概念,其目的是为了更加高效的生产同一个产品组产品。

  • 首先,我们把种类相对稳定,不需要频繁扩展变更的维度定义为产品等级. 比如水果中的 Fruit,我们需要固定明确后续 Fruit 只包含Apple 和 Banana 两类,不会进行扩展
  • 针对于种类需要频繁变更的维度,我们将其定义为产品族. 比如上述的产地(例如重庆、四川、北京),后续也许会扩展新疆、海南……
  • 每次需要扩展产品族时,都需要实现对应产品族的工厂 factory 实现类,而无需对老的实现方法直接进行修改,符合开闭原则
  • 针对于不频繁变动的产品等级,如水果中Apple与Banana,每个产地都会有一个具体的 factory 工厂实现类. 其中会统一声明对应于每种水果的构造方法,此时具备实现公共切面的能力

UML如图:

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

基于Redission高级应用6-RSemaphoreRPe

发表于 2024-04-27

RSemaphore 实现原理

RSemaphore 是 Redisson 提供的一个分布式信号量(Semaphore),它是基于 Redis 的 SETNX、LPUSH、LPOP 等原子操作实现的。信号量用于控制对有限资源的访问,确保同一时间只有有限数量的进程或线程可以访问。

工作原理如下:

  1. 在 Redis 中,使用一个键来表示信号量,该键关联的值表示剩余的许可数。
  2. 当一个进程或线程尝试获取许可时,Redisson 会通过 Redis 的原子减操作(如 DECRBY)尝试减少一个许可。
  3. 如果减操作后的许可数不小于 0,获取许可成功;如果小于 0,获取许可失败,并且进程或线程可能会被阻塞,直到其他进程或线程释放许可。
  4. 当释放许可时,Redisson 会通过 Redis 的原子加操作(如 INCRBY)来增加许可数。

RPermitExpirableSemaphore 实现原理

RPermitExpirableSemaphore 是 Redisson 提供的一个分布式可过期许可信号量。与 RSemaphore 类似,但是每个许可都有一个过期时间,在这个时间后许可会自动释放。

工作原理如下:

  1. 每个许可都是通过一个唯一的 ID 标识的,这个 ID 与一个 Redis 键关联,该键的生存时间(TTL)就是许可的过期时间。
  2. 当一个进程或线程尝试获取许可时,如果信号量的计数器大于 0,Redisson 会创建一个新的 Redis 键,并设置其 TTL。
  3. 许可的 ID 被返回给调用者,用来标识持有的许可。
  4. 如果持有许可的进程或线程完成了任务,它可以通过提供许可的 ID 来释放许可,这将删除对应的 Redis 键。
  5. 如果许可未被显式释放,它将在到达 TTL 后自动释放。

优点

  1. 分布式协调:可以在多个进程或服务之间协调对共享资源的访问。
  2. 高可用性:由于基于 Redis,这些信号量具有 Redis 的高可用性和持久性特性。
  3. 可过期许可:RPermitExpirableSemaphore 支持许可的自动释放,避免了死锁的问题。
  4. 公平性:Redisson 可以配置信号量来确保公平的获取许可,按请求的顺序授予许可。

缺点

  1. 性能开销:与本地信号量相比,分布式信号量因为网络通信和 Redis 操作会有更高的性能开销。
  2. 网络依赖:信号量的操作依赖于网络和 Redis 服务器的稳定性,网络问题可能导致许可获取失败或延迟。
  3. 时钟同步:在使用可过期许可时,需要确保系统时钟同步,否则可能会导致许可提前过期或过期时间不一致。
  4. 资源浪费:如果许可频繁地被获取和释放,可能会导致 Redis 的资源浪费和性能下降。

在使用 RSemaphore 或 RPermitExpirableSemaphore 时,需要考虑到实际的业务场景和系统架构,以及它们的优缺点,来决定是否适合在您的应用中使用这些分布式信号量。

实战示例:

Redisson 提供了 RSemaphore 和 RPermitExpirableSemaphore,这两种信号量可以帮助你在分布式系统中实现限流和资源控制。以下是这些信号量在实战应用中的一些例子:

RSemaphore 实战应用示例

分布式限流

假设你正在构建一个微服务架构的系统,你需要确保一个特定的服务在任何给定时间内不会被过多的并发请求所压垮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Autowired
private RedissonClient redissonClient;

public void initSemaphore(int permits) {
RSemaphore semaphore = redissonClient.getSemaphore("serviceLimitSemaphore");
semaphore.trySetPermits(permits);
}

public boolean acquire() {
RSemaphore semaphore = redissonClient.getSemaphore("serviceLimitSemaphore");
return semaphore.tryAcquire();
}

public void release() {
RSemaphore semaphore = redissonClient.getSemaphore("serviceLimitSemaphore");
semaphore.release();
}

在这个例子中,你首先设置了信号量的许可数,这个数目代表了你的服务可以同时处理的最大请求数。然后,每当有新的请求进来时,你会尝试获取一个许可。如果成功,服务将处理请求,处理完成后释放许可。如果无法获取许可,说明服务已达到最大负载,请求可以被拒绝或排队。

RPermitExpirableSemaphore 实战应用示例

分布式定时任务锁

在分布式系统中,可能需要确保定时任务在同一时间只能在一个节点上执行。RPermitExpirableSemaphore 可以发放一个可过期的许可,确保任务不会在多个节点上同时执行。

1
2
3
4
5
6
7
8
9
10
java复制代码@Autowired
private RedissonClient redissonClient;

public String acquireJobPermit(long leaseTime, TimeUnit timeUnit) {
RPermitExpirableSemaphore semaphore = redissonClient.getPermitExpirableSemaphore("jobLockSemaphormitId);
}
} else {
// 获取许可失败,另一个节点可能正在执行任务
}
}

在这个例子中,每次尝试执行定时任务之前,我们尝试获取一个带有租约时间的许可。如果获取成功,当前节点将执行任务,并在完成后释放许可。如果许可无法获取,表示有其他节点正在执行任务,当前节点可以跳过任务执行。租约时间确保即使在节点失败的情况下,许可也会在一定时间后自动释放,防止死锁。

注意事项

  • RSemaphore 和 RPermitExpirableSemaphore 在分布式环境中非常有用,但需要注意网络延迟和时钟同步问题。
  • 在使用可过期许可时,确保租约时间足够长,以覆盖任务的执行时间,防止在任务执行过程中许可过期。
  • 释放许可时,确保正确处理异常,并在必要时释放许可,避免资源泄露。
  • 在高并发场景下,信号量的操作可能会成为瓶颈,需要合理设置许可数量并监控系统性能。

这些实战应用示例展示了如何使用 Redisson 的信号量来控制分布式系统中的资源访问和任务执行,从而实现限流和同步。在实际应用中,开发者需要根据具体的业务场景和系统架构来选择合适的信号量类型和策略。

RSemaphore 和 RPermitExpirableSemaphore 是 Redisson 提供的两种分布式信号量实现,它们允许在分布式环境中限制对共享资源的访问。以下是这两种信号量的高级用法和相应的实战示例。

RSemaphore 高级用法

公平信号量

RSemaphore 可以配置为公平模式,这意味着 Redisson 会按照请求许可的顺序来分配许可,确保先到先得。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Autowired
private RedissonClient redissonClient;

public RSemaphore initFairSemaphore(String semaphoreName, int permits) {
RSemaphore semaphore = redissonClient.getFairSemaphore(semaphoreName);
semaphore.trySetPermits(permits);
return semaphore;
}

public void performActionWithFairSemaphore(String semaphoreName) {
RSemaphore semaphore = redissonClient.getFairSemaphore(semaphoreName);
try {
semaphore.acquire();
// 执行受限制的操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release();
}
}

在这个示例中,初始化信号量时,我们使用 getFairSemaphore 方法获取一个公平的信号量。这适用于那些需要保证处理顺序的场景,例如打印任务队列。

RPermitExpirableSemaphore 高级用法

可过期许可

RPermitExpirableSemaphore 允许发放带有过期时间的许可。如果持有许可的服务因任何原因未能释放许可,许可将在指定时间后自动释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Autowired
private RedissonClient redissonClient;

public String acquireExpirablePermit(String semaphoreName, long leaseTime, TimeUnit timeUnit) {
RPermitExpirableSemaphore semaphore = redissonClient.getPermitExpirableSemaphore(semaphoreName);
try {
return semaphore.acquire(leaseTime, timeUnit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}

public void performActionWithExpirablePermit(String semaphoreName, long leaseTime, TimeUnit timeUnit) {
String permitId = acquireExpirablePermit(semaphoreName, leaseTime, timeUnit);
if (permitId != null) {
try {
// 执行受限制的操作
} finally {
semaphore.release(permitId);
}
}
}

在这个示例中,我们获取了一个带有过期时间的许可,这适用于那些需要在特定时间内完成操作,或者需要在操作未能正常完成时自动放弃许可的场景。

扩展实战示例

分布式任务调度

在分布式系统中,我们可能需要确保定时任务或后台作业在任何时刻只能在一个节点上运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码@Autowired
private RedissonClient redissonClient;

public void scheduleJob(String jobName) {
RPermitExpirableSemaphore semaphore = redissonClient.getPermitExpirableSemaphore("jobSemaphore:" + jobName);
String permitId = null;
try {
// 尝试获取许可,设置合适的过期时间
permitId = semaphore.acquire(5, TimeUnit.MINUTES);
if (permitId != null) {
// 执行任务
executeJob(jobName);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (permitId != null) {
// 释放许可
semaphore.release(permitId);
}
}
}

private void executeJob(String jobName) {
// 实际的作业逻辑
}

在这个实战示例中,我们使用 RPermitExpirableSemaphore 来确保定时任务不会在多个节点上同时执行。每个任务都尝试获取一个许可,并在执行完成后释放许可。如果许可无法获取,任务将被跳过,以防止在另一个节点上重复执行。

注意事项

  • 在使用这些信号量时,确保所有节点的系统时间同步,避免因时间偏差导致的问题。
  • 使用 RPermitExpirableSemaphore 时,应该设置合理的许可过期时间,以便在节点故障时能够自动释放许可。
  • 需要处理好中断异常,确保在获取许可失败时,线程的中断状态得到正确处理。
  • 释放许可时,确保传递正确的许可 ID,避免错误地释放其他节点的许可。

这些高级用法和实战示例展示了如何在分布式环境中使用 Redisson 的信号量来控制对共享资源的并发访问,以及如何实现任务调度的同步。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…345…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%