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

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


  • 首页

  • 归档

  • 搜索

ansible自动化部署copy复制模块

发表于 2024-04-25

Ansible 中的 copy 模块用于将文件或目录从本地计算机或远程主机复制到远程主机上的特定位置。它是一个功能强大的模块,可用于各种文件传输任务.

作用

将配置文件复制到远程服务器
将应用程序部署到远程服务器
将日志文件从远程服务器复制到本地计算机
备份和恢复文件和目录

copy 模块的主要特性

简单高效:轻松复制文件和目录,维护文件系统元信息,例如权限和所有权。
灵活的源和目标:支持从本地或远程源复制到远程目标,从而实现跨各种机器的文件传输。
递归复制:有效地处理目录,复制整个目录结构及其内容。
内容替换:允许用更新的内容替换现有文件,确保最新的配置。
强制覆盖:提供强制覆盖现有文件的选项,确保无论以前的内容如何都会应用更改。
条件执行:根据目标文件的存在启用条件执行,防止不必要的复制。
模板支持:与 Ansible 模板功能集成,允许在文件传输期间插入动态内容。

copy 模块的常见用例

分发配置文件:将 .conf 或 .ini 文件等配置文件部署到远程计算机以获得一致的系统设置。

管理应用程序文件:将应用程序文件、脚本或库复制到远程服务器以进行应用程序部署或更新。

传输日志文件:从远程计算机收集日志文件以进行集中分析或故障排除。

备份关键文件:创建重要文件或目录的备份以确保数据完整性。

实验环境及要求

在三台centos中操作

一台安装ansible的服务器来控制另外两台客户端

1.进行上传文件

2.对于文件给予用户,组权限

3.对于文件做备份防止数据覆盖丢失

服务器操作

1.定义一个组

vim /etc/ansible/host
通过以下配置就是创建一个webServers组 并且加入两个主机 再给组赋予变量 用户名和密码 这样组下的所有主机都可以访问 更加的便捷

1
2
3
4
5
js复制代码[webServers]
host1 host2
[webServers:vars]
ansible_ssh_user='root'
ansible_ssh_pass='abc-123'

请在此添加图片描述

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
js复制代码[root@ansible ~]# ansible webServers -m copy -a 'src=/etc/hosts dest='/mnt/1.txt''
host1 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"checksum": "567c665328a352fc66fb407c6202846af636f321",
"dest": "/mnt/1.txt",
"gid": 0,
"group": "root",
"md5sum": "b6a7bc016a776f6ce50625fc6cffb176",
"mode": "0644",
"owner": "root",
"secontext": "system_u:object_r:mnt_t:s0",
"size": 225,
"src": "/root/.ansible/tmp/ansible-tmp-1713862721.36-10115-179845013773750/source",
"state": "file",
"uid": 0
}
host2 | CHANGED => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": true,
"checksum": "567c665328a352fc66fb407c6202846af636f321",
"dest": "/mnt/1.txt",
"gid": 0,
"group": "root",
"md5sum": "b6a7bc016a776f6ce50625fc6cffb176",
"mode": "0644",
"owner": "root",
"secontext": "system_u:object_r:mnt_t:s0",
"size": 225,
"src": "/root/.ansible/tmp/ansible-tmp-1713862721.32-10117-26808343561370/source",
"state": "file",
"uid": 0
}

先来大致分析一下每行的内容

  1. “ansible_facts”: { … } :此部分包含 Ansible 发现的有关远程计算机的信息。在本例中,它显示发现的 Python 解释器路径 ( /usr/bin/python )。
  2. “changed”: true :这表示复制操作导致远程计算机发生更改。这意味着该文件以前不存在或内容不同。
  3. “checksum” 和 “md5sum” :它们提供复制文件的校验和,可用于验证目的。
  4. “dest”: “/mnt/1.txt” :这会确认远程计算机上复制文件的目标路径。
  5. “owner”: “root” 、 “group”: “root” 、 “mode”: “0644” :这显示复制文件的所有权、组所有权和权限。在这里,它表示该文件由 root 用户和组拥有,并且对所有者(root)具有读/写权限,对其他人具有只读权限。
  6. “secontext” :这提供有关文件的安全增强型 Linux (SELinux) 上下文的信息。
  7. “size”: 225 :这显示复制文件的大小(以字节为单位)。
  8. “src”: “/root/.ansible/tmp/ansible-tmp-1713862721.36-10115-179845013773750/source” :这会显示 Ansible 在将源文件复制到远程计算机之前暂存源文件的临时位置。 Ansible 通常会出于处理目的创建临时文件。
  9. “state”: “file” :表示对文件进行操作的模块。
  10. 总之,输出确认 /etc/hosts 文件(或任何源文件)已成功复制到 webServers 组中所有目标计算机上的 /mnt/1.txt 。

客户端检测是否成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
js复制代码[root@localhost ~]# ls /mnt
[root@localhost ~]# ls
anaconda-ks.cfg
[root@localhost ~]# cd /mnt
[root@localhost mnt]# ls
[root@localhost mnt]# ls
1.txt
[root@localhost mnt]# cat 1.txt
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6

192.168.93.111 ansible

192.168.93.112 host1
192.168.93.113 host2

请在此添加图片描述

3.复制用户组权限文件

再次传输分发一个文件 这次附带用户和组以及权限

1
js复制代码[root@ansible ~]# ansible webServers -m copy -a 'src=/etc/hosts dest=/mnt/2.txt owner=root group=bin mode=770'

src=/etc/hosts : 此选项指定源文件,在本例中为位于控制机上的 /etc/hosts 。该文件包含重要的网络配置信息。

dest=/mnt/2.txt : 此选项定义远程计算机上的目标路径和文件名。此处,该文件将被复制到所有目标主机上的 /mnt 目录并重命名为 2.txt 。

owner=root : 此选项将复制文件的所有权设置为远程计算机上的 root 用户。这意味着只有 root 用户才拥有该文件的完全读取、写入和执行权限。

group=bin : 此选项将复制文件的组所有权设置为远程计算机上的 bin 组。 bin 组的成员将具有与 mode 选项指定的相同权限。

mode=770 : 此选项确定复制文件的文件权限。 770 值表示二进制数字的组合,用于定义所有者、组和其他人的权限。

请在此添加图片描述

客户端查看

可以看出权限确实改变了

1
2
3
4
5
js复制代码
[root@localhost mnt]# ll
总用量 8
-rw-r--r--. 1 root root 225 4月 23 16:58 1.txt
-rwxrwx---. 1 root bin 227 4月 23 17:14 2.txt

请在此添加图片描述

如果文件未更改任何数据的时候再次执行命令上传

1
js复制代码[root@ansible ~]# ansible webServers -m copy -a 'src=/etc/hosts dest=/mnt/2.txt owner=root group=bin

请在此添加图片描述

修改一下复制的源文件 再里边随便添加内容我们再次上传查看是否成功

1
2
js复制代码[root@ansible ~]# vim /etc/hosts
[root@ansible ~]# ansible webServers -m copy -a 'src=/etc/hosts dest=/mnt/2.txt owner=root group=bin mode=770'

请在此添加图片描述
客户端查看

请在此添加图片描述
这样的操作数据会被覆盖 文件名重复的时候也会覆盖掉 文件 造成数据丢失

这时候我们需要使用copy模块的一个选项

  1. backup=yes (重要补充):在复制新内容之前在远程计算机上创建原始文件的备份。
1
js复制代码[root@ansible ~]# ansible webServers -m copy -a 'src=/etc/hosts dest=/mnt/2.txt owner=root group=bin mode=770 backup=yes'

请在此添加图片描述

客户端查看

使用ls -la查看 可以发现上传的文件并没有被覆盖掉 而是形成一个备份存放

请在此添加图片描述

请在此添加图片描述

现在就有了两个文件数据测试成功

copy常用的参数

参数 描述 数据类型 默认值
src 指定要复制的文件或目录的源路径。 字符串 无

dest 指定远程主机上要复制到的目标路径。 字符串 无

force 强制覆盖目标文件或目录,即使它已经存在。 布尔值 yes

content 指定要写入目标文件的内容(用于创建新文件或覆盖现有文件)。 字符串 无

remote_src 指定远程主机上的源路径(用于从远程主机复制文件)。 字符串 无

delimiter 指定要用于分隔 content 参数中的键值对的字符。 字符串 =

directory_mode 指定要应用于目标目录的权限。 字符串 0755

file_mode 指定要应用于目标文件的权限。 字符串 0644

owner 指定目标文件的用户所有者。 字符串 源文件所有者

group 指定目标文件的组所有者。 字符串 源文件组所有者

preserve_timestamps 保留源文件或目录的时间戳。 布尔值 yes

backup 创建目标文件或目录的备份。 布尔值 no

checksum 使用指定算法计算源文件或目录的校验和。 字符串 md5

validate_checksum 验证目标文件或目录的校验和是否与源文件或目录的校验和匹配。 布尔值 no

本文转载自: 掘金

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

探索 MotionLayout 动画世界

发表于 2024-04-25

探索 MotionLayout 动画世界

先附上一个简单的动画效果图:

demo.gif

MotionLayout是什么 ?

MotionLayout是ConstraintLayout的子类,具有ConstraintLayout的所有功能。

一、使用步骤

1. 添加依赖
1
2
3
4
5
6
7
8
9
ini复制代码dependencies {
implementation(libs.constraintlayout)
}

[versions]
constraintlayout = "2.1.4"

[libraries]
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
1
复制代码ConstraintLayout 的版本需要更新到2.0以上。
2. 创建布局

创建一个布局文件名为view_nav.xml根布局为ConstraintLayout。

1
2
3
4
5
6
7
8
9
10
xml复制代码    <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">

......

</androidx.constraintlayout.widget.ConstraintLayout>

然后将布局转换为MotionLayout,如下图。

1.png

转换之后布局如下:

1
2
3
4
5
6
7
8
9
xml复制代码    <androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="match_parent"
app:layoutDescription="@xml/view_nav_scene">

......

</androidx.constraintlayout.motion.widget.MotionLayout>

根布局会自动转换为MotionLayout并且添加了一个属性app:layoutDescription,这个属性所引用的文件就是我们要编写的动画描述文件。

转换之后在Design面板会多出一个预览窗口。

2.png

3. 创建动画资源文件

AndroidStudio自动为我们创建的MotionScene 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码    <?xml version="1.0" encoding="utf-8"?>
<MotionScene
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">

<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
<KeyFrameSet>
</KeyFrameSet>
</Transition>

<ConstraintSet android:id="@+id/start">
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
</ConstraintSet>
</MotionScene>

这也是一个MotionScene文件的基本结构。

  • ConstrainSet描述了开始或结束时页面控件的状态
  • Transtion指定了动画要使用的ConstrainSet,动画的触发方式等。

如果我们不使用AndroidStudio来转换布局为MotionLayout的话,就需要自己在res\xml文件夹下创建一个根节点为MotionScene的xml文件。

4. 预览

预览画面如下图所示:

preview.png

点击箭头1所指的start可以预览动画start状态。
点击箭头2所指的end可以预览动画end状态。
点击箭头3所指的start和end之间的连线可以在下方的面板中点击播放查看动画。

二、MotionScene常用属性

1. MotionLayout标签
  • layoutDescription :设置布局的MotionScene文件。
  • applyMotionScene :表示是否应用 MotionScene。此属性的默认值为 true。
  • currentState :设置当前的状态,值对应MotionScene中的ConstraintSet节点的id。比如我们可以将其设置为@+id/end(结束状态)。
  • motionProgress :值为0到1之间的小数,用来设置页面开始时的动画进度。例如,将motionProgress设置为0.5,那么页面将以动画进行一半的状态开始。
  • showPaths :是否显示动画路径,为true话会显示view运动的轨迹线。
  • motionDebug :显示何种调试信息,设置的话会在界面的下方显示一些动画调试信息。

如果我们只设置了motion:showPaths="true"的话那么就会显示轨迹线,如果同时设置了motion:showPaths和motion:motionDebug的话,showPaths的设置会失效。以motionDebug的设置为准。

2. MotionScene标签
  • <Transition> :指定动画的开始和结束状态、触发动画的方式、动画中间的关键帧。
  • <ConstraintSet> :节点用来定义开始或是结束时控件的状态。我们需要在这个节点下重新为想要动画的控件进行布局。这里的设置会覆盖之前布局xml文件中的设置,可以将这个节点想象为ConstraintLayout布局,其中的子节点Constraint可以想象为每一个View,其中的android:id=""属性对应着原layout.xml中的view的id。
  • defaultDuration:所有过渡动画的默认持续时间(以毫秒为单位)。
3. Transition标签
  • constraintSetStart :设置动画的开始状态,这里对应一个ConstraintSet的id。
  • constraintSetEnd :设置动画的结束状态,这里对应一个ConstraintSet的id。
  • duration :动画的持续时间,如果没有设置会使用MotionScene元素的defaultDuration。
  • staggered :在多个View之间应用过渡效果时,指定是否应该依次进行过渡。可以设置为true或false。
  • autoTransition :指定是否在布局文件加载时自动开始过渡动画。可以设置为
    • animateToStart :切换到开始状态,有动画效果。
    • animateToEnd :切换到结束状态,有动画效果。
    • jumpToStart :切换到开始状态,无动画效果。
    • jumpToEnd :切换到结束状态,无动画效果。
  • motionInterpolator :动画的差值器,其值为常用的几种效果bounce,easeIn,easeOut,easeInOut,linear。
  • transitionDisable :指定是否禁用过渡动画。可以设置为true或false。
  • pathMotionArc :指定过渡动画中路径的弧度。可以设置为none、flip、startVertical、startHorizontal。

Transition标签定义的可处理事件有三种:OnClick、OnSwipe、KeyFrameSet。

4. OnClick

用于处理用户点击事件。

  • targetId :被点击后触发动画的视图id。
  • clickAction :点击时要执行的操作。
    • toggle :在transitionToStart和constraintSetEnd状态之间使用动画反复切换。每次点击从一种状态切换到另一种状态。
    • transitionToStart :从当前状态切换到 constraintSetStart 属性指定的状态,有动画效果。
    • transitionToEnd :从当前状态切换到 constraintSetEnd 属性指定的状态,有动画效果。
    • jumpToStart :从当前状态切换到 constraintSetStart 属性指定的状态,无动画效果。
    • jumpToEnd :从当前状态切换到 constraintSetEnd 属性指定的状态,无动画效果。
5. OnSwipe

用户处理用户拖拽事件。

  • touchAnchorId :可以滑动并拖动的视图id(目标视图)。
  • touchAnchorSide :定义触摸点在目标视图中的位置。
    • top :触摸点位于目标视图上方。
    • bottom :触摸点位于目标视图下方。
    • left/start :触摸点位于目标视图左方。
    • right/end :触摸点位于目标视图又方。
    • middle :触摸点位于目标视图中心。
  • dragDirection :拖拽的方向。
    • dragUp :上滑
    • dragDown :下滑
    • dragLeft/dragStart :左滑
    • dragRight/dragEnd :右滑
  • touchRegionId :触摸区域的视图id。
  • onTouchUp :手指离开屏幕时的行为。
    • autoComplete :自动完成拖拽。
    • autoCompleteToStart :自动完成拖拽并回到开始位置。
    • autoCompleteToEnd :自动完成拖拽并回到结束位置。
    • stop :停止拖拽。
    • decelerate :减速拖拽。
    • decelerateAndComplete :减速拖拽并完成拖拽。
    • neverCompleteToStart :永远不要自动完成到开始位置。
    • neverCompleteToEnd :永远不要自动完成到结束位置。
  • dragScale :定义拖拽操作的缩放比例。这个属性通常用于实现一些放大缩小的效果,可以让用户通过手势对视图进行缩放。
  • dragThreshold :定义拖拽的最小阈值,当拖拽距离小于该值时,视图不会响应拖拽事件。这个属性可以用于控制视图响应拖拽事件的灵敏度。
  • autoCompleteMode :定义自动完成的模式。
    • continuousVelocity :使用连续的速度自动完成。
    • spring :使用弹簧效果自动完成。
  • maxVelocity :定义最大速度,当拖拽速度超过该值时,视图将不再响应拖拽事件。
  • maxAcceleration :定义最大加速度,当拖拽加速度超过该值时,视图将不再响应拖拽事件。
  • springMass :定义弹簧质量。
  • springStiffness :定义弹簧刚度。
  • springDamping :定义弹簧阻尼。
  • springStopThreshold :定义弹簧停止的阈值,当速度小于该值时,弹簧将停止弹动。
  • springBoundary :定义弹簧边界,可以有以下几种取值:
    • overshoot :超出边界时弹簧会继续弹动。
    • bounceStart :当拖拽到开始位置时弹簧会弹动。
    • bounceEnd :当拖拽到结束位置时弹簧会弹动。
    • bounceBoth :当拖拽到开始或结束位置时弹簧会弹动。
  • rotationCenterId :定义旋转中心的视图 ID。
  • touchRegionId :定义触摸区域的视图 ID。
  • limitBoundsTo :定义限制边界的视图 ID。
  • nestedScrollFlags :定义嵌套滚动的标志位,可以有以下几种取值:
    • none :不支持嵌套滚动。
    • disablePostScroll :禁止滚动结束后的滚动。
    • disableScroll :禁止滚动。
    • supportScrollUp :支持向上滚动。
  • moveWhenScrollAtTop :定义是否在滚动到顶部时允许拖拽。
6. KeyFrameSet

用来描述一系列运动过程中的关键帧。可以利用它使动画效果变的更复杂。其子元素包含KeyPosition、KeyAttribute、KeyCycle、KeyTimeCycle、KeyTrigger。

6.1. KeyPosition

指定动画序列中特定时刻的位置(中间状态的位置),用于调整默认的运动路径。

  • motionTarget :定义应用此关键帧的运动目标,可以是一个视图或者一个运动场景。
  • framePosition :定义关键帧在动画中的位置。关键帧的位置取值为0 到 100 之间的整数,这个值相当于动画过程(时间)的百分比。(什么时候关键帧起作用)。
  • percentX、percentY :定义关键帧在 X 和 Y 轴上的位置。表示相对参考系的横向和纵向的比例。可以设置为 0 到 1 之间的浮点数。这两个数值的具体意义和keyPositionType 属性的设定有关。
  • keyPositionType :坐标系类型,可以取值为:
+ `parentRelative`
表示以MotionLayout布局为参考系,布局左上角为(0,0),右下角为(1,1)。例如:
1
2
3
4
5
6
xml复制代码<KeyFrameSet>
<KeyPosition
motion:keyPositionType="parentRelative"
motion:percentX="0.5"
motion:percentY="0.2" />
</KeyFrameSet>

motion:percentX=”0.5”,motion:percentY=”0.2”就是下图中(0.5,0.2)的位置。

parentRelative.png

+ `deltaRelative`
表示以View的起始点作为坐标原点(0,0),结束点作为(1,1)。
1
2
3
4
5
6
xml复制代码 <KeyFrameSet>
<KeyPosition
motion:keyPositionType="deltaRelative"
motion:percentX="0.5"
motion:percentY="0.2" />
</KeyFrameSet>

motion:percentX=”0.5”,motion:percentY=”0.2”就是下图中(0.5,0.2)的位置。

deltaRelative.png

+ `pathRelative`
表示以View的起始点作为坐标起点,起始点(0,0)与结束点(1,0)之间的连线作为x轴。
x轴顺时针旋转90°作为y轴,等长距离作为刻度。
1
2
3
4
5
6
xml复制代码 <KeyFrameSet>
<KeyPosition
motion:keyPositionType="pathRelative"
motion:percentX="0.5"
motion:percentY="0.2" />
</KeyFrameSet>

motion:percentX=”0.5”,motion:percentY=”0.2”就是下图中(0.5,0.2)的位置。

pathRelative.png

  • percentWidth、percentHeight :定义宽度和高度的变化量。可以设置为 0 到 1 之间的浮点数,表示从开始状态到结束状态之间的相对变化量。注意,如果宽度或高度没有变化,则这些属性将没有任何效果。

以下代码代表动画开始时View的宽高为 200px() (percentWidth = 0, percentHeight = 0),动画结束时的宽高为 600px(percentWidth = 1, percentHeight = 1),当framePosition = 50 时,view位于坐标系中(0.5,0.2)的位置,宽度变为 0.8,高度变为0.8,相当于此时view的宽高为 600px x 0.8。

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
xml复制代码    <?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">

<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="3000">
<KeyFrameSet>
<KeyPosition
motion:framePosition="50"
motion:keyPositionType="pathRelative"
motion:motionTarget="@id/view_test"
motion:percentX="0.5"
motion:percentY="0.2"
motion:percentWidth="0.8"
motion:percentHeight="0.8"/>
</KeyFrameSet>
<OnClick
motion:clickAction="toggle"
motion:targetId="@id/view_test" />
</Transition>

<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@id/view_test"
android:layout_width="200px"
android:layout_height="200px"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@id/view_test"
android:layout_width="600px"
android:layout_height="600px"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
</MotionScene>
  • transitionEasing :定义关键帧的过渡缓动效果。可以使用 Android 系统中提供的各种缓动函数,比如 standard, accelerate、decelerate、linear等。
  • pathMotionArc :定义关键帧在路径上的运动方式。可以设置为 none、startVertical、startHorizontal、flip之一。
  • curveFit :定义关键帧的插值方式。可以设置为 linear或spline。
  • sizePercent :定义宽度和高度的百分比。可以设置为 0 到 1 之间的浮点数,表示相对于视图父级的百分比。
6.2. KeyAttribute

指定动画序列中特定时刻的视图属性。

  • framePosition :定义关键帧在动画中的位置。可以设置为 0 到 100 之间的整数,表示从动画开始到结束之间的相对位置。
  • motionTarget :定义应用此关键帧的运动目标。可以是一个视图或者一个运动场景。
  • transitionEasing :定义关键帧的过渡缓动效果。可以使用 Android 系统中提供的各种缓动函数,比如 easeIn, easeOut 等。
  • curveFit :定义关键帧的插值方式。可以设置为 linear、spline 或 discrete 等三种取值之一。
  • motionProgress :定义关键帧的运动进度,即从开始状态到结束状态之间的进度百分比。
  • alpha :定义视图的不透明度。可以设置为 0 到 1 之间的浮点数,表示视图的透明度。
  • elevation :定义视图的高度。可以设置为一个浮点数,表示视图的高度。
  • rotation、rotationX、rotationY :定义视图的旋转角度,可以分别设置 X、Y、Z 轴上的旋转角度。
  • transformPivotX、transformPivotY :定义视图的变换中心点坐标。
  • transformPivotTarget :定义变换中心点的目标视图。
  • transitionPathRotate :定义视图在路径上的旋转角度。
  • scaleX、scaleY :定义视图的缩放比例。
  • translationX、translationY、translationZ :定义视图的位置偏移量。

如设置动画到一半时透明度为0.2,缩放为原控件的0.2倍:

1
2
3
4
5
6
7
8
xml复制代码    <KeyFrameSet>
<KeyAttribute
motion:framePosition="50"
motion:motionTarget="@id/view_test"
android:alpha="0.2"
android:scaleX="0.2"
android:scaleY="0.2" />
</KeyFrameSet>
6.3. KeyCycle

根据给定的函数对设定的属性进行周期性变化。

  • motionTarget :想要控制的控件id。
  • wavePeriod :表示运动的周期数,
  • waveShape :表示周期的函数,这里支持的函数有sin,cos,square,triangle,sawtooth,reverseSawtooth和bounce
    动画会根据上面的wavePeriod,waveShape对如下支持的属性进行周期变化
    • alpha
    • elevation
    • rotation
    • rotationX
    • rotationY
    • scaleX
    • scaleY
    • translationX
    • translationY
    • translationZ
6.4. KeyTimeCycle

KeyTimeCycle 可以在关键帧上按照一些周期函数,周期性的改变其属性值。
KeyTimeCycle 是在帧上做周期性,KeyCycle 是在动画过程中做周期性。

6.5. KeyTrigger

在动画中调用控件的指定方法。

  • motionTarget :想要控制的控件id
  • framePosition :取值范围和意义与在KeyPosition元素中相同,当动画执行到framePosition设定的位置时会执行onCross,onPositiveCross,onNegativeCros指定的方法。
  • onCross :指定需要调用的方法名,控件中必须有和此属性指定方法名同名的方法。无论动画正向还是反相只要当动画执行到framePosition设置的位置都会执行指定方法。
  • onPositiveCross :作用同onCross,但只有正向执行动画到达framePosition 设置的位置时才会执行指定的方法。
  • onNegativeCross :作用同onCross,但只有反向执行动画到达framePosition 设置的位置时才会执行指定的方法。
    如在AppCompatToggleButton中添加toggle方法用来控制开关。
1
2
3
4
5
6
7
8
9
10
11
java复制代码public class CustomButton extends AppCompatToggleButton {

......

/**
* 添加的方法
*/
public void toggle() {
setChecked(!isChecked());
}
}

MotionScene :

1
2
3
4
5
6
xml复制代码    <KeyFrameSet>
<KeyTrigger
motion:framePosition="50"
motion:motionTarget="@id/view_btn"
motion:onCross="toggle" />
</KeyFrameSet>

效果图: 开关由关闭状态变为开启状态

toggle.gif

7. ConstraintSet(类似于Constrainlayout)

用来设置视图在开始或者结束时各个控件的位置和大小等状态。只有一个id标签,可以有多个Constraint元素。

  • id :唯一标识符(start/end)
7.1 Constraint

每一个Constraint元素对应一个id属性所指向的View。
Constraint元素中我们可以设置控件的大小并使用ConstraintLayout的属性来设置控件位置。还可以插入以下属性:

  • alpha
  • visibility
  • elevation
  • rotation、rotationX、rotationY
  • translationX、translationY、translationZ
  • scaleX、scaleY
7.1.1 CustomAttribute

包含在Constraint元素中,一个 本身包含两个属性:

  • attributeName :必需属性,与控件中具有 getter 和 setter 方法的属性相对应。
    比如填写motion:attributeName = “backgroundColor“那么我们的控件中就必须有基本的 getBackgroundColor() 和 setBackgroundColor() 方法。
  • 第二个属性需要基于上面填写的属性来决定。
    比如上面填写的backgroundColor这里我们就需要使用customColorValue。以下是支持的数据类型:
  • customColorValue 适用于颜色
  • customColorDrawableValue 适用于颜色的drawable
  • customPixelDimension 适用于像素
  • customIntegerValue 适用于整数
  • customFloatValue 适用于浮点值
  • customStringValue 适用于字符串
  • customDimension 适用于尺寸
  • customBoolean 适用于布尔值

指定自定义属性时,必须在开始和结束的 ConstraintSet 元素中都为其指定。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码    <ConstraintSet android:id="@+id/start">
<Constraint
......
<CustomAttribute
motion:attributeName="backgroundColor"
motion:customColorValue="@color/viewColor" />
</Constraint>
</ConstraintSet>

<ConstraintSet android:id="@+id/end">
<Constraint
......
<CustomAttribute
motion:attributeName="backgroundColor"
motion:customColorValue="@color/transformColor" />
</Constraint>
</ConstraintSet>

三、实现开头动画的效果

1. 顶部大图及背景

效果图如下:

1.gif

1.1 首先在布局文件中添加一个ImageView来显示图片,添加一个等大小的View作为背景。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
xml复制代码<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/view_fullscreen_scene">

<View
android:id="@+id/view_top"
android:layout_width="match_parent"
android:layout_height="360px"
android:background="@color/black" />

<ImageView
android:id="@+id/iv_top"
android:layout_width="0px"
android:layout_height="0px"
android:scaleType="centerCrop"
android:src="@mipmap/img" />

</androidx.constraintlayout.motion.widget.MotionLayout>
1.2 接下来为图片ImageView和背景View在MotionScene中添加动画,首先设置图片和背景的开始状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
xml复制代码<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/view_top"
android:layout_width="match_parent"
android:layout_height="360px"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/iv_top"
android:layout_width="match_parent"
android:layout_height="0px"
motion:layout_constraintBottom_toBottomOf="@id/view_top"
motion:layout_constraintEnd_toEndOf="@id/view_top"
motion:layout_constraintStart_toStartOf="@id/view_top"
motion:layout_constraintTop_toTopOf="@+id/view_top" />
</ConstraintSet>
1.3 然后设置图片和背景的结束状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/view_top"
android:layout_width="match_parent"
android:layout_height="120px"
motion:layout_constraintBottom_toTopOf="@+id/nav"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent" />
<Constraint
android:id="@+id/iv_top"
android:layout_width="0px"
android:layout_height="0px"
android:layout_marginTop="10px"
android:layout_marginBottom="10px"
motion:layout_constraintBottom_toBottomOf="@id/view_top"
motion:layout_constraintDimensionRatio="W, 4:3"
motion:layout_constraintStart_toStartOf="@id/view_top"
motion:layout_constraintTop_toTopOf="@+id/view_top" />
</ConstraintSet>

ConstraintSet标签用来设置界面一种状态下的布局(ConstraintLayout)
Constraint标签用来描述控件的位置和属性等。
这里设置ImageView和View的开始是宽度为match_parent,高度为360px,结束时宽度不变,高度为120px。并设置ImageView结束时的尺寸比为 4 :3。

需要在开始和结束的两个Constraint中为控件设置大小,即使控件大小没有改变也需要在两边都设置好大小。

1.4 然后设置开始和结束状态,动画执行时间。
1
2
3
4
5
xml复制代码<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
</Transition>
1.5 然后对上方View添加手势响应动作。在Transition标签中添加OnSwipe标签即可。
1
2
3
4
5
6
7
8
9
xml复制代码<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
<OnSwipe
motion:dragDirection="dragDown"
motion:touchAnchorId="@+id/view_top"
motion:touchAnchorSide="bottom" />
</Transition>

OnSwipe表示拖动执行动画。motion:dragDirection="dragDown"表示向下边拖动执行动画。

1.6 完善ImageView的动画

动画执行到85的进度时,保持宽度及x位置不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xml复制代码<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
<OnSwipe
motion:dragDirection="dragDown"
motion:touchAnchorId="@+id/view_top"
motion:touchAnchorSide="bottom" />
<KeyFrameSet>
<KeyPosition
motion:framePosition="85"
motion:motionTarget="@id/iv_top"
motion:percentWidth="0"
motion:percentX="0" />
</KeyFrameSet>
</Transition>
2. 三个按钮

效果图

2.gif
按钮动画较为简单,平移+渐变。

2.1 开始状态
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
xml复制代码<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/iv_previous"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="100px"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="@id/view_top"
motion:layout_constraintEnd_toStartOf="@id/iv_pause"
motion:layout_constraintTop_toTopOf="@id/view_top" />
<Constraint
android:id="@+id/iv_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="100px"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="@id/view_top"
motion:layout_constraintEnd_toStartOf="@id/iv_next"
motion:layout_constraintTop_toTopOf="@id/view_top" />
<Constraint
android:id="@+id/iv_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="50px"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="@id/view_top"
motion:layout_constraintEnd_toEndOf="@id/view_top"
motion:layout_constraintTop_toTopOf="@id/view_top" />
</ConstraintSet>
2.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
xml复制代码<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/iv_previous"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="100px"
android:alpha="1"
motion:layout_constraintBottom_toBottomOf="@id/view_top"
motion:layout_constraintEnd_toStartOf="@id/iv_pause"
motion:layout_constraintTop_toTopOf="@id/view_top" />
<Constraint
android:id="@+id/iv_pause"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="100px"
android:alpha="1"
motion:layout_constraintBottom_toBottomOf="@id/view_top"
motion:layout_constraintEnd_toStartOf="@id/iv_next"
motion:layout_constraintTop_toTopOf="@id/view_top" />
<Constraint
android:id="@+id/iv_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="50px"
android:alpha="1"
motion:layout_constraintBottom_toBottomOf="@id/view_top"
motion:layout_constraintEnd_toEndOf="@id/view_top"
motion:layout_constraintTop_toTopOf="@id/view_top" />
</ConstraintSet>
2.3 完善动画效果

动画执行到85的进度时,透明度变为0.1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xml复制代码<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
<OnSwipe
motion:dragDirection="dragDown"
motion:touchAnchorId="@+id/view_top"
motion:touchAnchorSide="bottom" />
<KeyFrameSet>
<KeyAttribute
android:alpha="0.10"
motion:framePosition="85"
motion:motionTarget="@id/iv_previous" />
<KeyAttribute
android:alpha="0.10"
motion:framePosition="85"
motion:motionTarget="@id/iv_pause" />
<KeyAttribute
android:alpha="0.10"
motion:framePosition="85"
motion:motionTarget="@id/iv_next" />
</KeyFrameSet>
</Transition>
3. Test文字

效果图:

3.gif

3.1 开始状态
1
2
3
4
5
6
7
8
9
10
xml复制代码<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/tv_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0"
motion:layout_constraintBottom_toBottomOf="@id/view_top"
motion:layout_constraintStart_toEndOf="@id/view_top"
motion:layout_constraintTop_toTopOf="@id/view_top" />
</ConstraintSet>
3.2 结束状态
1
2
3
4
5
6
7
8
9
10
11
xml复制代码<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/tv_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30px"
android:alpha="1"
motion:layout_constraintBottom_toBottomOf="@+id/view_top"
motion:layout_constraintStart_toEndOf="@+id/iv_top"
motion:layout_constraintTop_toTopOf="@+id/view_top" />
</ConstraintSet>
3.3 完善文字动画效果

动画执行到85的进度时,透明度为0.1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
<OnSwipe
motion:dragDirection="dragDown"
motion:touchAnchorId="@+id/view_top"
motion:touchAnchorSide="bottom" />
<KeyFrameSet>
<KeyAttribute
android:alpha="0.10"
motion:framePosition="85"
motion:motionTarget="@id/tv_text" />
</KeyFrameSet>
</Transition>
4. 上方圆形图片

效果图:

4.gif

4.1 开始状态
1
2
3
4
5
6
7
8
9
xml复制代码<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/iv_round"
android:layout_width="0px"
android:layout_height="0px"
android:alpha="0"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent" />
</ConstraintSet>
4.2 结束状态
1
2
3
4
5
6
7
8
9
10
11
xml复制代码<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/iv_round"
android:layout_width="450px"
android:layout_height="450px"
android:alpha="1"
motion:layout_constraintBottom_toTopOf="@id/view_top"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
</ConstraintSet>
4.3 完善圆形图片效果

动画执行到50的进度时,透明度为0。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<Transition
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="1000">
<OnSwipe
motion:dragDirection="dragDown"
motion:touchAnchorId="@+id/view_top"
motion:touchAnchorSide="bottom" />
<KeyFrameSet>
<KeyAttribute
android:alpha="0"
motion:framePosition="50"
motion:motionTarget="@id/iv_round" />
</KeyFrameSet>
</Transition>
5. 下方导航栏

屏幕外–>屏幕底部,就不放图了。

5.1 开始状态
1
2
3
4
5
6
7
xml复制代码<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
motion:layout_constraintTop_toBottomOf="parent" />
</ConstraintSet>
5.2 结束状态
1
2
3
4
5
6
7
xml复制代码<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
motion:layout_constraintBottom_toBottomOf="parent" />
</ConstraintSet>

代码整合之后文章开头的动画效果就实现了。

到这里一些基本动画效果及属性就介绍完了。

代码地址

Thanks。

本文转载自: 掘金

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

如何应对Android面试官 -> PKMS 安装与签名校验

发表于 2024-04-25

前言


image.png

本章主要围绕以上几个知识点进行讲解,最后会手写一个 权限申请框架;

PKMS


PackageManagerService 的作用:

  1. PackageManagerService(简称 PKMS),是 Android 系统中核心服务之一,负责应用程序的安装,卸载,信息查询,等工作;
  2. 解析AndroidNanifest.xml清单文件,解析清单文件中的所有节点信息;
  3. 扫描.apk文件,安装系统应用,安装本地应用等;
  4. 管理本地应用,主要有, 安装,卸载,应用信息查询 等;

PKMS 概述来说:Android 系统启动时,会启动(应用程序管理服务器 PKMS ),此服务负责扫描系统中特定的目录,寻找里面的APK格式的文件,并对这些文件进行解析,然后得到应用程序相关信息,最后完成应用程序的安装PKMS在安装应用过程中, 会全面解析应用程序的 AndroidManifest.xml 文件, 来得到 Activity, Service, BroadcastReceiver, ContextProvider 等信息, 在结合 PKMS 服务就可以在 Android OS 中正常的使用应用程序了,在 Android 系统中, 系统启动时由 SystemServer 启动 PKMS 服务, 启动该服务后会执行应用程序的安装过程;

PKMS 的角色位置,我们来看一张图

image.png

客户端可通过 Context.getPackageManager() 获得 ApplicationPackageManager 对象, 而 mPM 指向的是 Proxy 代理,当调用到 mPM. 方法后,将会调用到 IPackageManager 的 Proxy 代理方法,然后通过Binder 机制中的 mRemote 与服务端 PackageManagerService 通信 并调用到 PackageManagerService 的方法;

PKMS 启动过程分析


启动过程,依然从 SystemServer 的 run 看起,我们进入这个 run 方法的 startBootstrapServices() 方法,这个方法主要用来启动引导服务;

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
scss复制代码private void startBootstrapServices(@NonNull TimingsTraceAndSlog t) {
// 第一步:创建安装器,并启动安装器
Installer installer = mSystemServiceManager.startService(Installer.class);

// 第二步:获取设置是否加密「例如设置了密码」
String cryptState = VoldProperties.decrypt().orElse("");

// 第三步:调用 PKMS 的 main 方法,实例化 PKMS
mPackageManagerService = PackageManagerService.main(mSystemContext, installer,
domainVerificationService, mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF,
mOnlyCore);

// 第四步:如果设备没有加密,操作它,管理 A/B OAT dexopting
if (!mOnlyCore) {
boolean disableOtaDexopt = SystemProperties.getBoolean("config.disable_otadexopt",
false);
if (!disableOtaDexopt) {
try {
//
OtaDexoptService.main(mSystemContext, mPackageManagerService);
} catch (Throwable e) {
reportWtf("starting OtaDexOptService", e);
} finally {
Watchdog.getInstance().resumeWatchingCurrentThread("moveab");
t.traceEnd();
}
}
}

// 第五步:如果设备没有加密,执行performDexOptUpdate 完成dex优化
// 构造PackageDexOptimizer及DexManager类,处理dex优化;
mPackageManagerService.updatePackagesIfNeeded();

// 第六步:执行performStrim 完成磁盘优化
mPackageManagerService.performFstrimIfNeeded();

// 第七步:PKMS准备就绪
mPackageManagerService.systemReady();
}

重点一在第五步,手机开机慢的主要原因:

手机开机,BootRoom 拉起BootLoader程序,BootLoader 会拉起 Linux 内核驱动,驱动会初始化init进程,init进程会创建zygote进程,zygote进程会fork出它的第大儿子 系统服务进程「system server」系统服务进程会初始化 PKMS,PKMS 在构造方法中 就会扫描手机上的所有应用,手机上应用越多,时间越长,扫描完之后 还会进行 dex 优化;

重点二在第三步,我们进入第三步的 main 方法看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public static PackageManagerService main(Context context, Installer installer,
@NonNull DomainVerificationService domainVerificationService, boolean factoryTest,
boolean onlyCore) {
// 省略部分代码

// 实例化 PackageManagerService
PackageManagerService m = new PackageManagerService(injector, onlyCore, factoryTest,
Build.FINGERPRINT, Build.IS_ENG, Build.IS_USERDEBUG, Build.VERSION.SDK_INT,
Build.VERSION.INCREMENTAL);
// 将 PMS 注册到 SystemServer 中
ServiceManager.addService("package", m);
//
final PackageManagerNative pmn = m.new PackageManagerNative();
ServiceManager.addService("package_native", pmn);
}

这里主要是调用 PackageManagerService 的构造方法,进行实例化,并初始化相关信息,这里主要分为 5 个阶段;

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
ini复制代码public PackageManagerService(Injector injector, boolean onlyCore, boolean factoryTest,
final String buildFingerprint, final boolean isEngBuild,
final boolean isUserDebugBuild, final int sdkVersion, final String incrementalVersion) {
// 省略部分代码

// 阶段1 BOOT_PROGRESS_PMS_START
EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_START,
SystemClock.uptimeMillis());
mMetrics = injector.getDisplayMetrics();
mInstaller = injector.getInstaller();
mPermissionManager = injector.getPermissionManagerServiceInternal();
mSettings = injector.getSettings();
// 这里的目的是:如果想安装系统应用,必须得到系统厂商的签名,这些 UID 是由 Linux 来管理的,系统应用在安装的时候,权限校验就能通过;
mSettings.addSharedUserLPw("android.uid.system", Process.SYSTEM_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw("android.uid.phone", RADIO_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw("android.uid.log", LOG_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw("android.uid.nfc", NFC_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw("android.uid.bluetooth", BLUETOOTH_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw("android.uid.shell", SHELL_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw("android.uid.se", SE_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw("android.uid.networkstack", NETWORKSTACK_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
mSettings.addSharedUserLPw("android.uid.uwb", UWB_UID,
ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
// 构造PackageDexOptimizer及DexManager类,处理dex优化;
mPackageDexOptimizer = injector.getPackageDexOptimizer();
mDexManager = injector.getDexManager();
// ART虚拟机管理服务
mArtManagerService = injector.getArtManagerService();
mMoveCallbacks = new MoveCallbacks(FgThread.get().getLooper());
mViewCompiler = injector.getViewCompiler();
// 权限变化监听器
mProtectedPackages = new ProtectedPackages(mContext);
mApexManager = injector.getApexManager();
}
  1. BOOT_PROGRESS_PMS_START,PKMS 的构造方法中进行;
  • mMetrics = new DisplayMetrics();初始化屏幕相关类;
  • mInstaller = installer;赋值安装器;
  • mPermissionManager = injector.getPermissionManagerServiceInternal();创建PermissionManager 进行权限管理;
  • 创建 settings 保存安装包信息;

这个 Settings 比较关键,我们进入这个 Settings 的构造方法看下;

1
2
3
4
5
6
7
8
9
10
less复制代码Settings(File dataDir, RuntimePermissionsPersistence runtimePermissionsPersistence,
LegacyPermissionDataProvider permissionDataProvider,
@NonNull DomainVerificationManagerInternal domainVerificationManager,
@NonNull PackageManagerTracedLock lock) {
// 读取 dat/system/packages.xml 主要是为了读取系统应用并加载
mSettingsFilename = new File(mSystemDir, "packages.xml");
// 备份读取,为了加载过程中被打断之后可以继续加载
mBackupSettingsFilename = new File(mSystemDir, "packages-backup.xml");
mPackageListFilename = new File(mSystemDir, "packages.list");
}

packages.xml中 存放着所有的系统权限 以及 安装在手机上的每一个应用程序信息。每次开机都会执行这个 PKMS 的构造方法,进行应用的安装处理,如果手机上安装的应用很多很多,开机时间也会相应的变长;

  1. EventLogTags.BOOT_PROGRESS_PMS_SYSTEM_SCAN_START;扫描系统的 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
24
25
26
27
28
29
30
31
32
33
34
35
36
arduino复制代码public PackageManagerService(Injector injector, boolean onlyCore, boolean factoryTest,
final String buildFingerprint, final boolean isEngBuild,
final boolean isUserDebugBuild, final int sdkVersion, final String incrementalVersion) {
// 阶段2:扫描系统的 apk 应用
EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_SYSTEM_SCAN_START,
startTime);
// 开始扫描
for (int i = mDirsToScanAsSystem.size() - 1; i >= 0; i--) {
final ScanPartition partition = mDirsToScanAsSystem.get(i);
if (partition.getOverlayFolder() == null) {
continue;
}
//
scanDirTracedLI(partition.getOverlayFolder(), systemParseFlags,
systemScanFlags | partition.scanFlag, 0,
packageParser, executorService);
}
// 扫描 framework
File frameworkDir = new File(Environment.getRootDirectory(), "framework");
scanDirTracedLI(frameworkDir, systemParseFlags,
systemScanFlags | SCAN_NO_DEX | SCAN_AS_PRIVILEGED, 0,
packageParser, executorService);

//
for (int i = 0, size = mDirsToScanAsSystem.size(); i < size; i++) {
final ScanPartition partition = mDirsToScanAsSystem.get(i);
if (partition.getPrivAppFolder() != null) {
scanDirTracedLI(partition.getPrivAppFolder(), systemParseFlags,
systemScanFlags | SCAN_AS_PRIVILEGED | partition.scanFlag, 0,
packageParser, executorService);
}
scanDirTracedLI(partition.getAppFolder(), systemParseFlags,
systemScanFlags | partition.scanFlag, 0,
packageParser, executorService);
}
}

扫描系统的应用,主要扫描下面几个目录的 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复制代码private static int scanFlagForPartition(PackagePartitions.SystemPartition partition) {
switch (partition.type) {
//
case PackagePartitions.PARTITION_SYSTEM:
return 0;
// vendor/overlay
case PackagePartitions.PARTITION_VENDOR:
return SCAN_AS_VENDOR;
// odm/overlay
case PackagePartitions.PARTITION_ODM:
return SCAN_AS_ODM;
// oem/overlay
case PackagePartitions.PARTITION_OEM:
return SCAN_AS_OEM;
// product/overlay
case PackagePartitions.PARTITION_PRODUCT:
return SCAN_AS_PRODUCT;
//
case PackagePartitions.PARTITION_SYSTEM_EXT:
return SCAN_AS_SYSTEM_EXT;
default:
throw new IllegalStateException("Unable to determine scan flag for "
+ partition.getFolder());
}
}
  1. BOOT_PROGRESS_PMS_DATA_SCAN_START,扫描data目录下的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
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
scss复制代码mAppInstallDir = new File(Environment.getDataDirectory(), "app");

if (!mOnlyCore) {
EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_DATA_SCAN_START,
SystemClock.uptimeMillis());
scanDirTracedLI(mAppInstallDir, 0, scanFlags | SCAN_REQUIRE_KNOWN, 0,
packageParser, executorService);
// 移除通过OTA删除的更新系统应用程序的禁用package设置
// 如果更新不再存在,则完全删除该应用。否则,撤消其系统权限
for (int i = possiblyDeletedUpdatedSystemApps.size() - 1; i >= 0; --i) {
final String packageName = possiblyDeletedUpdatedSystemApps.get(i);
final AndroidPackage pkg = mPackages.get(packageName);
final String msg;

// remove from the disabled system list; do this first so any future
// scans of this package are performed without this state
mSettings.removeDisabledSystemPackageLPw(packageName);
// ...
}

// 确保期望在userdata分区上显示的所有系统应用程序实际显示
// 如果从未出现过,需要回滚以恢复系统版本
for (int i = 0; i < mExpectingBetter.size(); i++) {
final String packageName = mExpectingBetter.keyAt(i);
if (!mPackages.containsKey(packageName)) {
final File scanFile = mExpectingBetter.valueAt(i);
// ...
mSettings.enableSystemPackageLPw(packageName);

// 扫描 APK
try {
final AndroidPackage newPkg = scanPackageTracedLI(
scanFile, reparseFlags, rescanFlags, 0, null);
// We rescanned a stub, add it to the list of stubbed system packages
if (newPkg.isStub()) {
stubSystemApps.add(packageName);
}
} catch (PackageManagerException e) {
Slog.e(TAG, "Failed to parse original system package: "
+ e.getMessage());
}
}
}
// 解压缩并安装任何存根系统应用程序。必须最后执行此操作以确保替换或禁用所有存根
installSystemStubPackages(stubSystemApps, scanFlags);
// 获取storage manager包名
mStorageManagerPackage = getStorageManagerPackageName();
// 解决受保护的action过滤器。只允许setup wizard(开机向导)为这些action设置高优先级过滤器
mSetupWizardPackage = getSetupWizardPackageNameImpl();
// 更新客户端以确保持有正确的共享库路径
updateAllSharedLibrariesLocked(null, null, Collections.unmodifiableMap(mPackages));
// 读取并更新要保留的package的上次使用时间
mPackageUsage.read(packageSettings);
mCompilerStats.read();
}

处理 data 目录的应用信息,及时更新,祛除不必要的数据;

  1. BOOT_PROGRESS_PMS_SCAN_END OTA 升级后首次启动要清除不必要的缓存数据,权限等默认项更新后要清理相关数据,将阶段2 阶段3扫描后的数据更新到 packages.xml 文件中;
1
2
ini复制代码EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_SCAN_END,
SystemClock.uptimeMillis());
  1. BOOT_PROGRESS_PMS_READY GC 内存回收
1
2
3
4
5
6
7
8
scss复制代码EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_READY, SystemClock.uptimeMillis());
// PermissionController 主持 缺陷许可证的授予和角色管理,所以这是核心系统的一个关键部分。
mRequiredPermissionControllerPackage = getRequiredPermissionControllerLPr();
updateInstantAppInstallerLocked(null);
// 打开应用之后,及时回收处理
Runtime.getRuntime().gc();
// 上面的初始扫描在持有mPackage锁的同时对installd进行了多次调用
mInstaller.setWarnIfHeld(mPackages);

APK 的扫描


PKMS的构造函数中调用了 scanDirTracedLI 方法 来扫描某个目录的 apk 文件;

1
2
3
4
5
6
7
8
arduino复制代码private void scanDirTracedLI(File scanDir, final int parseFlags, int scanFlags,
long currentTime, PackageParser2 packageParser, ExecutorService executorService) {
try {
scanDirLI(scanDir, parseFlags, scanFlags, currentTime, packageParser, executorService);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}

从这里执行 APK 的扫描操作,我们进入这个 scanDirLI 看下;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
arduino复制代码private void scanDirLI(File scanDir, int parseFlags, int scanFlags, long currentTime,
PackageParser2 packageParser, ExecutorService executorService) {
final File[] files = scanDir.listFiles();
if (ArrayUtils.isEmpty(files)) {
return;
}

ParallelPackageParser parallelPackageParser =
new ParallelPackageParser(packageParser, executorService);

int fileCount = 0;
for (File file : files) {
final boolean isPackage = (isApkFile(file) || file.isDirectory())
&& !PackageInstallerService.isStageName(file.getName());
if (!isPackage) {
// Ignore entries which are not packages
continue;
}
// 收集apk文件 交给submit方法处理
parallelPackageParser.submit(file, parseFlags);
fileCount++;
}
}

这里主要是 收集 apk 文件 交给 submit 方法处理;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码public void submit(File scanFile, int parseFlags) {
mExecutorService.submit(() -> {
ParseResult pr = new ParseResult();
try {
pr.scanFile = scanFile;
pr.parsedPackage = parsePackage(scanFile, parseFlags);
} catch (Throwable e) {
pr.throwable = e;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
try {
mQueue.put(pr);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
mInterruptedInThread = Thread.currentThread().getName();
}
});
}

submit 把 PackageParser 封装好的信息交给 parsePackage 方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码public ParsedPackage parsePackage(File packageFile, int flags, boolean useCaches)
throws PackageParserException {
if (useCaches && mCacher != null) {
ParsedPackage parsed = mCacher.getCachedResult(packageFile, flags);
if (parsed != null) {
return parsed;
}
}

long parseTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;
ParseInput input = mSharedResult.get().reset();
// 解析 apk 文件
ParseResult<ParsingPackage> result = parsingUtils.parsePackage(input, packageFile, flags);

ParsedPackage parsed = (ParsedPackage) result.getResult().hideAsParsed();
return parsed;
}

解析 apk 文件

1
2
3
4
5
6
7
8
9
10
arduino复制代码public ParseResult<ParsingPackage> parsePackage(ParseInput input, File packageFile,
int flags)
throws PackageParserException {
if (packageFile.isDirectory()) {
return parseClusterPackage(input, packageFile, flags);
} else {
// 解析
return parseMonolithicPackage(input, packageFile, flags);
}
}

我们进入 parseMonolithicPackage 这个方法看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码private ParseResult<ParsingPackage> parseMonolithicPackage(ParseInput input, File apkFile,
int flags) throws PackageParserException {

final SplitAssetLoader assetLoader = new DefaultSplitAssetLoader(lite, flags);
try {
// 通过 parseBaseApk 解析 apk 中的清单文件
final ParseResult<ParsingPackage> result = parseBaseApk(input,
apkFile,
apkFile.getCanonicalPath(),
assetLoader, flags);
if (result.isError()) {
return input.error(result);
}

return input.success(result.getResult()
.setUse32BitAbi(lite.isUse32bitAbi()));
} catch (IOException e) {
return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
"Failed to get path: " + apkFile, e);
} finally {
IoUtils.closeQuietly(assetLoader);
}
}

通过 parseBaseApk 解析 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
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
ini复制代码private ParseResult<ParsingPackage> parseBaseApk(ParseInput input, File apkFile,
String codePath, SplitAssetLoader assetLoader, int flags)
throws PackageParserException {
final String apkPath = apkFile.getAbsolutePath();

String volumeUuid = null;
if (apkPath.startsWith(MNT_EXPAND)) {
final int end = apkPath.indexOf('/', MNT_EXPAND.length());
volumeUuid = apkPath.substring(MNT_EXPAND.length(), end);
}

if (DEBUG_JAR) Slog.d(TAG, "Scanning base APK: " + apkPath);

final AssetManager assets = assetLoader.getBaseAssetManager();
final int cookie = assets.findCookieForPath(apkPath);
if (cookie == 0) {
return input.error(INSTALL_PARSE_FAILED_BAD_MANIFEST,
"Failed adding asset path: " + apkPath);
}
// 解析 apk 中的清单文件
try (XmlResourceParser parser = assets.openXmlResourceParser(cookie,
ANDROID_MANIFEST_FILENAME)) {
final Resources res = new Resources(assets, mDisplayMetrics, null);
// 获取应用的相关信息,并存储起来
ParseResult<ParsingPackage> result = parseBaseApk(input, apkPath, codePath, res,
parser, flags);
if (result.isError()) {
return input.error(result.getErrorCode(),
apkPath + " (at " + parser.getPositionDescription() + "): "
+ result.getErrorMessage());
}

final ParsingPackage pkg = result.getResult();
if (assets.containsAllocatedTable()) {
final ParseResult<?> deferResult = input.deferError(
"Targeting R+ (version " + Build.VERSION_CODES.R + " and above) requires"
+ " the resources.arsc of installed APKs to be stored uncompressed"
+ " and aligned on a 4-byte boundary",
DeferredError.RESOURCES_ARSC_COMPRESSED);
if (deferResult.isError()) {
return input.error(INSTALL_PARSE_FAILED_RESOURCES_ARSC_COMPRESSED,
deferResult.getErrorMessage());
}
}

ApkAssets apkAssets = assetLoader.getBaseApkAssets();
boolean definesOverlayable = false;
try {
definesOverlayable = apkAssets.definesOverlayable();
} catch (IOException ignored) {
// Will fail if there's no packages in the ApkAssets, which can be treated as false
}

if (definesOverlayable) {
SparseArray<String> packageNames = assets.getAssignedPackageIdentifiers();
int size = packageNames.size();
for (int index = 0; index < size; index++) {
String packageName = packageNames.valueAt(index);
Map<String, String> overlayableToActor = assets.getOverlayableMap(packageName);
if (overlayableToActor != null && !overlayableToActor.isEmpty()) {
for (String overlayable : overlayableToActor.keySet()) {
pkg.addOverlayable(overlayable, overlayableToActor.get(overlayable));
}
}
}
}

pkg.setVolumeUuid(volumeUuid);

if ((flags & PARSE_COLLECT_CERTIFICATES) != 0) {
pkg.setSigningDetails(getSigningDetails(pkg, false));
} else {
pkg.setSigningDetails(SigningDetails.UNKNOWN);
}

return input.success(pkg);
} catch (Exception e) {
return input.error(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,
"Failed to read manifest from " + apkPath, e);
}
}

通过 XmlResourceParser parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME) 解析 apk 中的清单文件;

通过 parseBaseApk 获取应用的相关信息,并存储起来;

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
ini复制代码private ParseResult<ParsingPackage> parseBaseApk(ParseInput input, String apkPath,
String codePath, Resources res, XmlResourceParser parser, int flags)
throws XmlPullParserException, IOException {
final String splitName;
final String pkgName;

ParseResult<Pair<String, String>> packageSplitResult =
ApkLiteParseUtils.parsePackageSplitNames(input, parser);
if (packageSplitResult.isError()) {
return input.error(packageSplitResult);
}

Pair<String, String> packageSplit = packageSplitResult.getResult();
pkgName = packageSplit.first;
splitName = packageSplit.second;

if (!TextUtils.isEmpty(splitName)) {
return input.error(
PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME,
"Expected base APK, but found split " + splitName
);
}

final TypedArray manifestArray = res.obtainAttributes(parser, R.styleable.AndroidManifest);
try {
final boolean isCoreApp =
parser.getAttributeBooleanValue(null, "coreApp", false);
// 获取清单文件中的标签 例如「application」「permission」「package」「manifest」
final ParsingPackage pkg = mCallback.startParsingPackage(
pkgName, apkPath, codePath, manifestArray, isCoreApp);
// 获取application标签中的中的「activity」「service」「receiver」「provider」
final ParseResult<ParsingPackage> result =
parseBaseApkTags(input, pkg, manifestArray, res, parser, flags);
if (result.isError()) {
return result;
}

return input.success(pkg);
} finally {
manifestArray.recycle();
}
}

startParsingPackage 获取清单文件中的标签 例如「application」「permission」「package」「manifest」

parseBaseApkTags 获取application标签中的中的「activity」「service」「receiver」「provider」;

所以,静态广播是什么时候注册的?Activity的启动模式是在什么时候获取的呢? 答案就显而易见了;

开机的时候,在 PKMS 的构造方法中 会扫描所有的 apk,包括系统 apk 和应用 apk,并解析它们的清单文件,获取到 receiver 的时候就会注册,获取到 activity 的时候 一并获取了 launchMode;

扫描外部apk直接通过接口,获取 Package 对象,PackageParserCompat.parsePackage(context, apk, PackageParser.PARSE_MUST_BE_APK);

安装 APK


用户点击 xxx.apk 文件进行安装,从「开始安装」到「完成安装」到过程;这里分为两个步骤,一个是复制流程,一个是安装流程;

复制流程

点击xxx.apk执行onClick事件,通过Intent 发起startActivity 跳转到 PackageInstallerActivity 这个界面,这个界面是以 Dialog 形式展示,存在「安装」和「取消」事件,进入「安装」事件看下;

从 PackageInstallerActivity 的 bindUI 发起流程,会调用 startInstall 方法进行整个的安装流程;

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
scss复制代码private void bindUi() {
mAlert.setIcon(mAppSnippet.icon);
mAlert.setTitle(mAppSnippet.label);
mAlert.setView(R.layout.install_content_view);
mAlert.setButton(DialogInterface.BUTTON_POSITIVE, getString(R.string.install),
(ignored, ignored2) -> {
if (mOk.isEnabled()) {
if (mSessionId != -1) {
mInstaller.setPermissionsResult(mSessionId, true);
finish();
} else {
// 开始安装
startInstall();
}
}
}, null);
mAlert.setButton(DialogInterface.BUTTON_NEGATIVE, getString(R.string.cancel),
(ignored, ignored2) -> {
// Cancel and finish
setResult(RESULT_CANCELED);
if (mSessionId != -1) {
mInstaller.setPermissionsResult(mSessionId, false);
}
finish();
}, null);
setupAlert();

mOk = mAlert.getButton(DialogInterface.BUTTON_POSITIVE);
mOk.setEnabled(false);

if (!mOk.isInTouchMode()) {
mAlert.getButton(DialogInterface.BUTTON_NEGATIVE).requestFocus();
}
}

调用 startInstall 发起安装

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
scss复制代码private void startInstall() {
// Start subactivity to actually install the application
Intent newIntent = new Intent();
newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO,
mPkgInfo.applicationInfo);
newIntent.setData(mPackageURI);
newIntent.setClass(this, InstallInstalling.class);
String installerPackageName = getIntent().getStringExtra(
Intent.EXTRA_INSTALLER_PACKAGE_NAME);
if (mOriginatingURI != null) {
newIntent.putExtra(Intent.EXTRA_ORIGINATING_URI, mOriginatingURI);
}
if (mReferrerURI != null) {
newIntent.putExtra(Intent.EXTRA_REFERRER, mReferrerURI);
}
if (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN) {
newIntent.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid);
}
if (installerPackageName != null) {
newIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME,
installerPackageName);
}
if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
}
newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
if (mLocalLOGV) Log.i(TAG, "downloaded app uri=" + mPackageURI);
startActivity(newIntent);
finish();
}

跳转到 安装中 InstallInstalling.class 界面,在 onResume 执行安装逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
scss复制代码protected void onResume() {
super.onResume();

// This is the first onResume in a single life of the activity
if (mInstallingTask == null) {
PackageInstaller installer = getPackageManager().getPackageInstaller();
PackageInstaller.SessionInfo sessionInfo = installer.getSessionInfo(mSessionId);

if (sessionInfo != null && !sessionInfo.isActive()) {
mInstallingTask = new InstallingAsyncTask();
// 执行 execute 方法进行安装
mInstallingTask.execute();
} else {
// we will receive a broadcast when the install is finished
mCancelButton.setEnabled(false);
setFinishOnTouchOutside(false);
}
}
}

mInstallingTask.execute(); 执行 execute 方法进行安装;这里是一个 AsyncTask,要处理 doInBackground 和 onPostExecute

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
ini复制代码private final class InstallingAsyncTask extends AsyncTask<Void, Void,
PackageInstaller.Session> {
volatile boolean isDone;

@Override
protected PackageInstaller.Session doInBackground(Void... params) {
PackageInstaller.Session session;
try {
session = getPackageManager().getPackageInstaller().openSession(mSessionId);
} catch (IOException e) {
synchronized (this) {
isDone = true;
notifyAll();
}
return null;
}

session.setStagingProgress(0);

try {
File file = new File(mPackageURI.getPath());

try (InputStream in = new FileInputStream(file)) {
long sizeBytes = file.length();
try (OutputStream out = session
.openWrite("PackageInstaller", 0, sizeBytes)) {
byte[] buffer = new byte[1024 * 1024];
while (true) {
int numRead = in.read(buffer);

if (numRead == -1) {
session.fsync(out);
break;
}

if (isCancelled()) {
session.close();
break;
}

out.write(buffer, 0, numRead);
if (sizeBytes > 0) {
float fraction = ((float) numRead / (float) sizeBytes);
session.addProgress(fraction);
}
}
}
}

return session;
} catch (IOException | SecurityException e) {
Log.e(LOG_TAG, "Could not write package", e);

session.close();

return null;
} finally {
synchronized (this) {
isDone = true;
notifyAll();
}
}
}

@Override
protected void onPostExecute(PackageInstaller.Session session) {
if (session != null) {
Intent broadcastIntent = new Intent(BROADCAST_ACTION);
broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND);
broadcastIntent.setPackage(getPackageName());
broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, mInstallId);

PendingIntent pendingIntent = PendingIntent.getBroadcast(
InstallInstalling.this,
mInstallId,
broadcastIntent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);

session.commit(pendingIntent.getIntentSender());
mCancelButton.setEnabled(false);
setFinishOnTouchOutside(false);
} else {
getPackageManager().getPackageInstaller().abandonSession(mSessionId);

if (!isCancelled()) {
launchFailure(PackageInstaller.STATUS_FAILURE,
PackageManager.INSTALL_FAILED_INVALID_APK, null);
}
}
}
}

doInBackground方法中:通过IO流 将apk文件写入session中;

onPostExecute方法中:通过 session.commit 方法,跨进程操作,将 session 交给 PKMS,最终走到 PKMS 的 installStage 方法;

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码void installStage(InstallParams params) {
final Message msg = mHandler.obtainMessage(INIT_COPY);
params.setTraceMethod("installStage").setTraceCookie(System.identityHashCode(params));
msg.obj = params;

Trace.asyncTraceBegin(TRACE_TAG_PACKAGE_MANAGER, "installStage",
System.identityHashCode(msg.obj));
Trace.asyncTraceBegin(TRACE_TAG_PACKAGE_MANAGER, "queueInstall",
System.identityHashCode(msg.obj));

mHandler.sendMessage(msg);
}

msg.what == INIT_COPY; handleMessage 的这个 case 我们进入看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scala复制代码class PackageHandler extends Handler {

void doHandleMessage(Message msg) {

switch (msg.what) {
case INIT_COPY: {
HandlerParams params = (HandlerParams) msg.obj;
if (params != null) {
params.startCopy();
}
break;
}
}
}
}

INIT_COPY 中最终执行的是 HandlerParams 的 startCopy 方法,将 apk 复制到 /data/app 目录下;

最终走到 PKMS 的 handleStartCopy() 方法 和 handleReturnCode 方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
scss复制代码public void handleStartCopy() {
if ((installFlags & PackageManager.INSTALL_APEX) != 0) {
mRet = INSTALL_SUCCEEDED;
return;
}
PackageInfoLite pkgLite = PackageManagerServiceUtils.getMinimalPackageInfo(mContext,
mPackageLite, origin.resolvedPath, installFlags, packageAbiOverride);

// For staged session, there is a delay between its verification and install. Device
// state can change within this delay and hence we need to re-verify certain conditions.
boolean isStaged = (installFlags & INSTALL_STAGED) != 0;
if (isStaged) {
mRet = verifyReplacingVersionCode(
pkgLite, requiredInstalledVersionCode, installFlags);
if (mRet != INSTALL_SUCCEEDED) {
return;
}
}

mRet = overrideInstallLocation(pkgLite);
}

主要是为了创建 InstallArgs,在 handleReturnCode 中执行 copyApk 方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scss复制代码void handleReturnCode() {
processPendingInstall();
}

private void processPendingInstall() {
// 这里创建的是 FileInstallArgs
InstallArgs args = createInstallArgs(this);
if (mRet == PackageManager.INSTALL_SUCCEEDED) {
mRet = args.copyApk();
}
if (mRet == PackageManager.INSTALL_SUCCEEDED) {
F2fsUtils.releaseCompressedBlocks(
mContext.getContentResolver(), new File(args.getCodePath()));
}
if (mParentInstallParams != null) {
mParentInstallParams.tryProcessInstallRequest(args, mRet);
} else {
PackageInstalledInfo res = createPackageInstalledInfo(mRet);
processInstallRequestsAsync(
res.returnCode == PackageManager.INSTALL_SUCCEEDED,
Collections.singletonList(new InstallRequest(args, res)));
}
}

handleReturnCode 执行到 args.copyApk() 的时候,才真正的将 apk 文件复制到了 /data/app 目录下;

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
ini复制代码int copyApk() {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "copyApk");
try {
return doCopyApk();
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}

private int doCopyApk() {
if (origin.staged) {
if (DEBUG_INSTALL) Slog.d(TAG, origin.file + " already staged; skipping copy");
codeFile = origin.file;
return PackageManager.INSTALL_SUCCEEDED;
}

try {
final boolean isEphemeral = (installFlags & PackageManager.INSTALL_INSTANT_APP) != 0;
final File tempDir =
mInstallerService.allocateStageDirLegacy(volumeUuid, isEphemeral);
codeFile = tempDir;
} catch (IOException e) {
Slog.w(TAG, "Failed to create copy file: " + e);
return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
}

int ret = PackageManagerServiceUtils.copyPackage(
origin.file.getAbsolutePath(), codeFile);
if (ret != PackageManager.INSTALL_SUCCEEDED) {
Slog.e(TAG, "Failed to copy package");
return ret;
}

final boolean isIncremental = isIncrementalPath(codeFile.getAbsolutePath());
final File libraryRoot = new File(codeFile, LIB_DIR_NAME);
NativeLibraryHelper.Handle handle = null;
try {
handle = NativeLibraryHelper.Handle.create(codeFile);
ret = NativeLibraryHelper.copyNativeBinariesWithOverride(handle, libraryRoot,
abiOverride, isIncremental);
} catch (IOException e) {
Slog.e(TAG, "Copying native libraries failed", e);
ret = PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
} finally {
IoUtils.closeQuietly(handle);
}

return ret;
}

通过 IO 流 执行 copy 操作;这也就能解释为什么 apk 安装包可以删除的原因了;

扫描流程大致可以理解为:

  1. 在 PackageParser 扫描完一个 APK 后,此时系统已经根据该 APK 中 AndroidManifest.xml,创建了一个完整的 Package 对象;
  2. 扫描APK,解析AndroidManifest.xml文件,得到清单文件各个标签内容;
  3. 解析清单文件的信息由 Package 保存,从该类的成员变量可看出,和 Android 四大组件相关
    的信息分别由 activites、receivers、providers、services 保存,由于一个 APK 可声明多个组件,因此 activites 和 receivers 等均声明为 ArrayList;

安装流程

安装流程也是有 PKMS 发起的,它发起的起点就是 handleReturnCode 的回调中调用了processPendingInstall 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
scss复制代码void handleReturnCode() {
processPendingInstall();
}

private void processPendingInstall() {
InstallArgs args = createInstallArgs(this);
if (mRet == PackageManager.INSTALL_SUCCEEDED) {
mRet = args.copyApk();
}
if (mRet == PackageManager.INSTALL_SUCCEEDED) {
F2fsUtils.releaseCompressedBlocks(
mContext.getContentResolver(), new File(args.getCodePath()));
}
if (mParentInstallParams != null) {
mParentInstallParams.tryProcessInstallRequest(args, mRet);
} else {
PackageInstalledInfo res = createPackageInstalledInfo(mRet);
// 执行安装逻辑
processInstallRequestsAsync(
res.returnCode == PackageManager.INSTALL_SUCCEEDED,
Collections.singletonList(new InstallRequest(args, res)));
}
}

通过调用 processInstallRequestsAsync 执行安装逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码private void processInstallRequestsAsync(boolean success,
List<InstallRequest> installRequests) {

mHandler.post(() -> {
// 省略部分代码
if (success) {
// 省略部分代码
synchronized (mInstallLock) {
installPackagesTracedLI(apkInstallRequests);
}
}
})
}

这里直接调用 installPackagesTracedLI 进行安装;

1
2
3
4
5
6
7
8
typescript复制代码private void installPackagesTracedLI(List<InstallRequest> requests) {
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackages");
installPackagesLI(requests);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}

这里直接调用 installPackagesLI 方法进行 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
26
27
28
29
30
31
32
33
34
35
36
ini复制代码private void installPackagesLI(List<InstallRequest> requests) {
//
// 省略部分代码
final ScanResult result = scanPackageTracedLI(
prepareResult.packageToScan, prepareResult.parseFlags,
prepareResult.scanFlags, System.currentTimeMillis(),
request.args.user, request.args.abiOverride);

// 构建安装请求,然后调用 executePostCommitSteps 进行安装
CommitRequest commitRequest = null;
synchronized (mLock) {
Map<String, ReconciledPackage> reconciledPackages;
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "reconcilePackages");
reconciledPackages = reconcilePackagesLocked(
reconcileRequest, mSettings.getKeySetManagerService(), mInjector);
} catch (ReconcileFailure e) {
for (InstallRequest request : requests) {
request.installResult.setError("Reconciliation failed...", e);
}
return;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
try {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "commitPackages");
commitRequest = new CommitRequest(reconciledPackages,
mUserManager.getUserIds());
commitPackagesLocked(commitRequest);
success = true;
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}
executePostCommitSteps(commitRequest);
}

构建安装请求,然后调用 executePostCommitSteps 进行安装;

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
ini复制代码private void executePostCommitSteps(CommitRequest commitRequest) {
// 省略部分代码
final ArraySet<IncrementalStorage> incrementalStorages = new ArraySet<>();
for (ReconciledPackage reconciledPkg : commitRequest.reconciledPackages.values()) {
final boolean instantApp = ((reconciledPkg.scanResult.request.scanFlags
& PackageManagerService.SCAN_AS_INSTANT_APP) != 0);
final AndroidPackage pkg = reconciledPkg.pkgSetting.pkg;
final String packageName = pkg.getPackageName();
final String codePath = pkg.getPath();
final boolean onIncremental = mIncrementalManager != null
&& isIncrementalPath(codePath);
if (onIncremental) {
IncrementalStorage storage = mIncrementalManager.openStorage(codePath);
if (storage == null) {
throw new IllegalArgumentException(
"Install: null storage for incremental package " + packageName);
}
incrementalStorages.add(storage);
}
prepareAppDataAfterInstallLIF(pkg);

// 省略部分代码
}

// 省略部分代码
}

这里调用 prepareAppDataAfterInstallLIF 最终调用到 prepareAppDataLeaf 中的 batch.createAppData

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
less复制代码private @NonNull CompletableFuture<?> prepareAppDataLeaf(@NonNull Installer.Batch batch,
@NonNull AndroidPackage pkg, int userId, int flags) {
return batch.createAppData(volumeUuid, packageName, userId, flags, appId, seInfo,
targetSdkVersion).whenComplete((ceDataInode, e) -> {

if (e != null) {

destroyAppDataLeafLIF(pkg, userId, flags);
try {
ceDataInode = mInstaller.createAppData(volumeUuid, packageName, userId,
flags, appId, seInfo, pkg.getTargetSdkVersion());

} catch (InstallerException e2) {

}
} else if (e != null) {

}
if (mIsUpgrade || mFirstBoot || (userId != UserHandle.USER_SYSTEM)) {
mArtManagerService.prepareAppProfiles(pkg, userId,
/* updateReferenceProfileContent= */ false);
}

if ((flags & StorageManager.FLAG_STORAGE_CE) != 0 && ceDataInode != -1) {
synchronized (mLock) {
if (ps != null) {
ps.setCeDataInode(ceDataInode, userId);
}
}
}

prepareAppDataContentsLeafLIF(pkg, ps, userId, flags);
});
}

Batch 的 createAppData 最终调用到的是其 execute 方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ini复制代码public synchronized void execute(@NonNull Installer installer) throws InstallerException {
if (mExecuted) throw new IllegalStateException();
mExecuted = true;

final int size = mArgs.size();
for (int i = 0; i < size; i += CREATE_APP_DATA_BATCH_SIZE) {
final CreateAppDataArgs[] args = new CreateAppDataArgs[Math.min(size - i,
CREATE_APP_DATA_BATCH_SIZE)];
for (int j = 0; j < args.length; j++) {
args[j] = mArgs.get(i + j);
}
final CreateAppDataResult[] results = installer.createAppDataBatched(args);
for (int j = 0; j < args.length; j++) {
final CreateAppDataResult result = results[j];
final CompletableFuture<Long> future = mFutures.get(i + j);
if (result.exceptionCode == 0) {
future.complete(result.ceDataInode);
} else {
future.completeExceptionally(
new InstallerException(result.exceptionMessage));
}
}
}
}

通过 installer.createAppDataBatched(args) 方法进行 app 的创建;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
less复制代码public @NonNull CreateAppDataResult[] createAppDataBatched(@NonNull CreateAppDataArgs[] args)
throws InstallerException {
if (!checkBeforeRemote()) {
final CreateAppDataResult[] results = new CreateAppDataResult[args.length];
Arrays.fill(results, buildPlaceholderCreateAppDataResult());
return results;
}
try {
// 进入 linux 底层的安装逻辑
return mInstalld.createAppDataBatched(args);
} catch (Exception e) {
throw InstallerException.from(e);
}
}

最终 mInstalld.createAppDataBatched(args); 进入 linux 底层的安装逻辑,底层的安装逻辑我也不太清楚,只能到这里了;

权限扫描


PKMS 的构造方法中会调用到 SystemConfig systemConfig = SystemConfig.getInstance(); 方法,此方法会执行权限的扫描/system/etc/permissions

image.png

SytestemConfig 的构造方法中 会执行权限的读取操作;

image.png

readPermissions 和扫描到的AndroidManifest文件中声明的权限会和这里进行一个匹配,匹配上了给予权限;

image.png

好了,本章的讲解就到这里吧,静默安装和权限申请框架我们放在了下一章;

下一章预告


PMS 权限管理

欢迎三连


来都来了,点个关注点个赞吧,你的支持是我最大的动力~~

本文转载自: 掘金

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

CapRover 让独立开发没有后顾之忧

发表于 2024-04-25

现代应用往往需要多种技术,如前端框架、后端语言、数据库、DevOps 工具等;独立开发者需要学习和掌握这些技术,才能进行产品的发布和迭代;既要负责开发新功能,又要处理线上问题、进行版本更新、维护服务器等运维工作,怎么才能让开发者专注于应用的功能本身,而不用去忙于 DevOps?

今天给大家介绍一款开源的 PaaS 平台:CapRover,它是一个用于部署和管理 Web 应用程序的开源、自我托管平台。它提供了一个简单、直观和自动化的方式,使您可以轻松部署和管理应用程序,而无需深入了解 DevOps 或容器化。

CapRover 架构

CapRover 的主要特点

  1. 容易部署: 使用 CapRover 可以使用多种方式轻松部署 Web 应用程序,而无需担心底层基础设施。
    • 从控制面板上传源文件
    • 使用命令行 caprover deploy
    • 使用代码托管的webhooks,你只需要 git push,新应用自动构建部署

从 http://localhost:3000 到 https://awesome.com 只需几秒

  1. 自动容器化: 使用 Docker 自动将您的应用程序容器化,可以适用于任何语言的应用,并使其易于管理和扩展。
  2. 内置负载平衡: 提供内置Nginx负载平衡功能,确保您的应用程序可以处理高流量并有效扩展。
  3. Https: CapRover 使用 Let’s Encrypt 为你的每个应用提供自动 Https,并自动更新证书。
  4. 滚动更新: 支持滚动更新,允许您在不中断或影响用户的情况下更新应用程序。
  5. 监控和日志记录: 提供内置监控和日志记录功能,方便您排查故障和优化应用程序。
  6. 支持多种环境: 支持多种环境(例如开发、测试和生产),使您可以轻松管理应用程序的不同版本。
  7. 广泛的集成: 与流行服务(如 GitHub、GitLab 和 Bitbucket)以及数据库(如 MySQL、PostgreSQL 和 MongoDB)等广泛集成。
  8. 丰富的应用仓库: 提供官方维护的可部署应用 300+ 款(内含之前介绍过的 Appwirte、各种数据库和低代码平台等等),并且还在不停的适配新的应用,当然你也可以自定义一个可部署应用仓库,可以参考定制你的一键部署仓库
  9. 一键备份: CapRover 工作的所有配置文件都在/captain中,提供配置的一键备份( 注意:这里只包含服务的配置信息如域名、应用配置和 https 证书等,不含应用的数据,如: volumes 和 images 等 ),可以通过这些恢复你的服务。

CapRover 的工作原理

  1. 创建 CapRover 实例: 可以在您的服务器或云提供商上创建 CapRover 实例。
  2. 连接代码仓库: 可以将您的 GitHub、GitLab 或 Bitbucket 存储库连接到 CapRover。
  3. 定义应用程序: 可以使用 CapRover 定义应用程序,包括环境变量、依赖项和其他设置。
  4. 部署应用程序: CapRover 会自动构建、容器化和部署您的应用程序。
  5. 管理和扩展: 可以使用 CapRover 仪表板或 API 管理和扩展您的应用程序。

CapRover工作流

CapRover 的优点

  1. 简化部署: 简化了部署过程,减少了启动和运行应用程序所需的时间和精力。
  2. 提高效率: 自动化许多任务,让您有更多时间专注于开发应用程序。
  3. 改善可扩展性: CapRover 的内置负载平衡和滚动更新确保您的应用程序可以处理高流量并有效扩展。
  4. 增强协作: CapRover 与流行服务的集成使您可以轻松地与团队成员协作并管理不同的环境。
  5. 不锁定应用: 移除 CapRover,您的应用仍可继续运行。

总的来说,CapRover 是一个创新的 DevOps 平台,可以简化应用程序的部署和管理,是独立开发者和小团队的理想选择。它让你更专注于应用!而不是仅仅为了运行应用而关注 DevOps 的那些花哨的功能!让你写更多的 showResults(getUserList())而不是 apt-get install libstdc++6 > /dev/null 。

本文转载自: 掘金

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

7K star!开源免费、高颜值,这个数据库设计器最近很火

发表于 2024-04-25

开发中很多时候都会使用到数据库,所以选择一个好用的数据库设计工具会让工作效率翻倍。

今天我们分享一个开源项目,帮你把复杂的数据库内容直观化,更加有效的设计数据库而且它颜值极高,它就是:drawdb

DrawDB 是什么

DrawDB是一款多功能且用户友好的在线工具,允许用户轻松设计数据库实体关系。通过简单直观的界面,DrawDB使用户能够创建图表、导出SQL脚本、自定义编辑环境,而无需创建账户。

DrawDB通过提供数据库模式中实体关系的视觉表现,简化了数据库设计过程。用户可以轻松创建表格、定义列、建立关系,并且仅需点击几下就可以生成SQL脚本。该工具直观的界面适合初学者和经验丰富的数据库设计师,提供了一个设计复杂数据库结构的流畅体验。

DrawDB的能力包括:

  • 导出:导出 DDL 脚本以在数据库上运行,或将图表导出为 JSON 或图像。
  • 导入:导入 DDL 脚本或要生成的 JSON 文件或图表。
  • 可定制的工作空间:自定义 UI 以适合您的喜好。选择视图中所需的组件。
  • 键盘快捷键:使用键盘快捷键加快开发速度。
  • 模板:从预先构建的模板开始。快速开始或获得设计灵感。
  • 问题检测:检测并解决图中的错误,以确保脚本正确。
  • 强大的编辑器:撤销、重做、复制、粘贴、复制等。添加表格、主题区域和注释。
  • 关系数据库:我们支持 5 种关系数据库 - MySQL、PostgreSQL、SQLite、MariaDB、SQL Server。
  • 对象关系数据库:为对象关系数据库添加自定义类型,或创建自定义 JSON 方案和别名类型。
  • 演示模式:在团队会议和讨论期间在大屏幕上展示您的图表。
  • 追踪全部:跟踪任务并在完成后将其标记为已完成。

安装 DrawDB

在线环境

DrawDB 提供了在线环境,是最方便的试用环境,而且非常体贴的一点是无需注册,直接就能够开始使用。

这里提示一下,由于是部署在vercel上的,似乎直接访问会不行,所以需要一些霍格沃兹魔法了。

本地部署

作为一个开源项目,我们当然还是要来看一下它的本地部署怎么样。这里有两种方式来部署,一种是源码部署,一种是docker。

因为项目是一个JS项目,所以源码的部署方式很简单,如下指令:

1
2
3
4
bash复制代码git clone <https://github.com/drawdb-io/drawdb>
cd drawdb
npm install
npm run dev

另外还可以只用docker来运行,如下:

1
2
3
yaml复制代码#拉代码还是一样的
docker build -t drawdb .
docker run -p 3000:80 drawdb

部署完成后,在5173端口,即可访问到服务。

使用 DrawDB

接下来,一起快速体验一下DrawDB的功能。

编辑器

编辑器肯定是整个产品的核心,但是这里其实也不需要太多的介绍,因为大家如果使用过数据库设计器的话,那实际上都大同小异。

左侧的导航中,会给出5种类型的对象,table,relationship,subject area,note 和type。这里最关键的肯定是table和relationship了。

整体使用上来说,还是挺顺畅的。但是也有一点不太好的地方,目前的版本不能框选多个表一起移动,但不影响使用,估计这类功能应该会很快增加。

快捷键

DrawDB虽然是一个纯web的系统,但是也配备了全面的快捷键。在帮助里可以查看到全部支持的快捷键。

导入/导出

导入的话支持diagram和source两种方式,这也是同类产品的标配了。

导出的话,支持导出到5种数据库:MySQL、PostgreSQL、SQLite、MariaDB、SQL Server。

另外还支持导出为图片、JSON、PDF和自己的格式等。

导入和导出方面基本符合了正常使用的需要。

模版

DrawDB 一个比较不错的内容就是提供了模版的支持,可以使用系统自带的模版来快速创建架构图,也可以将自己的设计转化为模版。这里我觉得模版应该会成为这个产品在线版的一个小核心,围绕模版、用户空间和共享,应该会成为这个产品的一个变现点。

样式

我看到支持的样式修改暂时只有dark mode。

演示模式

提供了一个演示模式,可以直接隐藏全部的工具栏,进入一种全屏展示的状态,适合开会的时候或者 用户沟通的时候做演示。

ToDo

这算是项目的一个小功能,可以快速创建ToDo,但是现在还比较简陋,也许未来会增强。

时间线

提供了一个时间线功能,感觉还蛮不错,可以看到都做过哪些操作,如果有问题可以方便查找。

问题检测

在左下角会有一个问题检测,会显示出存在的错误,提示解决图中的错误,以确保脚本正确。

总结

整体用下来,感觉DrawDB是一个不错的数据库设计器,它用起来颜值很高,而且作为新生代的web应用,在团队协作等方面都给人非常大的想象空间。希望项目可以持续严谨,变得越来越好用。另外DrawDB目前可以在以下这些场景中使用。

  • 个人开发者:快速搭建和测试数据库。
  • 中小企业:简化数据库管理流程,提高团队协作效率。
  • 教育和研究:作为教学工具,帮助学生理解数据库概念。
  • 数据分析师:进行数据探索和分析,发现数据价值。

项目信息

  • 项目名称: drawdb
  • GitHub 链接:github.com/drawdb-io/d…
  • Star 数:7K

本文转载自: 掘金

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

腾讯云 Lighthouse 服务器 Docker 容器跨主

发表于 2024-04-25

之前一直在向大家介绍基于k3s实现的腾讯云Lighthouse集群使用方法,也有声音表示k8s太重了,能不能更轻量一些?答案当然是:可以!

今天,本文将探讨如何利用腾讯云Lighthouse服务器,在 Docker 应用镜像上创建一个 Swarm 集群的,并实现容器跨主机通信的能力。

概念解析

Swarm 是 Docker 官方提供的容器编排工具之一,用于管理多个 Docker 主机上运行的容器。它允许将多个 Docker 主机组成一个集群,并通过统一的接口来管理这些主机上的容器。Swarm 提供了高可用性、伸缩性和容错性,使得在生产环境中部署和管理容器化应用变得更加简单和可靠。

Overlay 网络是 Docker 提供的一种网络模型,采用了多种技术来实现容器间的通信,其中包括了本文中将使用的 VXLAN(Virtual Extensible LAN)技术,VXLAN 可以将容器的数据包封装在 UDP 数据包中,并在底层网络中传输,从而实现跨主机的容器通信。

环境配置

节点名 节点IP 节点服务
dnode0 10.0.0.11 重装为 Docker 应用镜像, 并初始化Swarm集群
dnode1 10.0.0.12 重装为 Docker 应用镜像
dnode2 10.0.0.13 重装为 Docker 应用镜像

在配置过程中,请确保将每个节点的hostname设置为不同的值,本例分别设置为 dnode0、dnode1、dnode2,并放行防火墙所需的端口:

  • TCP端口2376:用于安全的 Docker 客户端通信,这对于Docker Machine(用于编排Docker的主机)的正常运行是必需的。
  • TCP端口2377:用于Docker Swarm或集群中节点之间的通信,只需要在管理节点上打开。
  • UDP端口4789:用于覆盖网络流量(容器入口网络)。
  • TCP和UDP端口7946:用于节点之间的通信(容器网络发现)。

初始化Swarm集群服务

在dnode0节点上执行以下命令来初始化Swarm集群:

1
shell复制代码docker swarm init

如果没有记住加入集群的token,可以通过以下命令重新获取:

1
shell复制代码docker swarm join-token worker

其他节点分别加入Swarm集群

在其他节点上执行加入Swarm集群的命令。假设已经获得了加入集群的token,然后在各节点上执行如下命令:

1
shell复制代码docker swarm join --token SWMTKN-1-tokenxxxxxxx 10.0.0.11:2377

确保将token替换为实际获得的值,并将IP地址替换为dnode0节点的IP地址。

在节点上创建网络

在Swarm集群中,可以创建overlay网络以实现容器间的跨主机通信。在主节点上执行以下命令:

1
shell复制代码docker network create --driver overlay --subnet=172.22.0.0/24 --gateway=172.22.0.1 --attachable ovnet

这将创建一个名为ovnet的overlay网络,其中包括了一个子网范围为172.22.0.0/24,并指定网关为172.22.0.1。

测试跨主机网络连通性

在各个节点上创建容器,这些容器可以在overlay网络上互相通信。在各节点上执行如下命令:

1
2
3
shell复制代码docker run -it --rm --network ovnet --ip 172.22.0.5 alpine
docker run -it --rm --network ovnet --ip 172.22.0.6 alpine
docker run -it --rm --network ovnet --ip 172.22.0.7 alpine

这些命令将分别在overlay网络上创建了三个容器,它们分别具有IP地址为172.22.0.5、172.22.0.6和172.22.0.7,并且它们可以在Swarm集群中的不同节点上互相通信。

文章思路来自 若海の技术写真,有问题请留言。

本文转载自: 掘金

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

基于Redission 之Caffeine和Redis集成工

发表于 2024-04-25

概述:

要将Caffeine本地缓存和Redisson分布式缓存结合起来使用,可以创建一个工具类,它首先尝试从本地Caffeine缓存中获取数据,如果本地缓存中没有找到,则从Redisson分布式缓存中获取,并在获取后将数据回填到本地缓存中。

注意点:

  1. 并发处理:Caffeine 已经是线程安全的,所以本地缓存的并发访问不是问题。对于 Redisson,客户端本身也是线程安全的,但是在处理写回策略和缓存穿透时,可能需要额外的并发控制。
  2. 写回策略(Write-Back / Write-Behind):在这种策略下,数据首先写入本地缓存,然后异步地写入后端存储(例如 Redis)。这可以通过一个队列和后台线程来实现,该线程定期将更改写入后端存储。
  3. 缓存穿透保护:缓存穿透是指查询不存在的数据,导致请求直接打到数据库上。为了防止缓存穿透,可以使用空对象模式或布隆过滤器。空对象模式是指即使值不存在也在缓存中存储一个特殊的空对象,而布隆过滤器可以在请求到达缓存之前过滤掉不存在的键。
  4. 同步写入Redis和本地缓存:当本地缓存被写入时,同时将数据同步写入Redis。这样可以确保两者的数据一致性。但这种方法会增加每次写入操作的延迟。
  5. 使用锁或同步机制:在更新本地缓存的同时,使用锁或其他同步机制来确保数据也被写入Redis。如果本地缓存失效,可以通过锁来保证在读取Redis之前数据已经被写入。
  6. 设置合理的过期时间:在Redis中为缓存数据设置一个比本地缓存更长的过期时间,这样即使本地缓存失效,数据仍然可以从Redis中获取。
  7. 延迟本地缓存的过期时间:可以在本地缓存的基础上添加一个短暂的延迟时间,以确保Redis中的数据在本地缓存失效前已经更新。
  8. 使用缓存刷新策略:定期或在本地缓存即将失效时,异步刷新本地缓存的数据。这样可以确保本地缓存中的数据在大多数时间都是最新的。

工具类:

以下是这样一个工具类的简单示例:

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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
java复制代码import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.RemovalCause;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

import com.github.benmanes.caffeine.cache.stats.CacheStats;
import com.github.benmanes.caffeine.cache.RemovalListener;

/**
* @Author derek_smart
* @Date 202/4/24 14:55
* @Description caffeine和redis 缓存组合工具类
* <p>
* 写回策略(Write-Back / Write-Behind):在这种策略下,数据首先写入本地缓存,然后异步地写入后端存储(例如 Redis)。这可以通过一个队列和后台线程来实现,该线程定期将更改写入后端存储。
* 一个特殊的空对象 `NULL_PLACEHOLDER` 存储到本地缓存和 Redis 中,这样下次查询相同的键时就能直接从缓存中获取到空对象,从而防止缓存穿透。
*/
public class HybridCache<K, V> {

private final Cache<K, V> localCache;
private final RedissonClient redissonClient;
private final ExecutorService writeBehindExecutor;
private final long redisExpiration; // Redis 缓存过期时间,单位:秒
private static final Object NULL_PLACEHOLDER = new Object();
// 使用 ConcurrentHashMap 来存储锁
private final ConcurrentHashMap<K, Lock> locks = new ConcurrentHashMap<>();

public HybridCache(RedissonClient redissonClient, long maxSize, long expireAfterWrite, TimeUnit timeUnit, long redisExpiration) {
this.redissonClient = redissonClient;
this.redisExpiration = redisExpiration;
this.writeBehindExecutor = Executors.newSingleThreadExecutor(); // 用于写回策略的单线程执行器

RemovalListener<K, V> writeBehindRemovalListener = (K key, V value, RemovalCause cause) -> {
if (cause.wasEvicted()) {
writeBehindExecutor.submit(() -> redissonClient.getBucket(key.toString()).set(value, redisExpiration, TimeUnit.SECONDS));
}
};
this.localCache = Caffeine.newBuilder()
.maximumSize(maxSize)
.expireAfterWrite(expireAfterWrite, timeUnit)
.removalListener((K key, V value, RemovalCause cause) -> {
System.out.printf("Key %s was removed (%s)%n", key, cause);
})
.recordStats()
.build();
}

public V get(K key) {
// 尝试从本地缓存获取数据
V value = localCache.getIfPresent(key);
if (value != null) {
return value;
}

// 本地缓存没有找到,尝试从 Redis 获取
RBucket<V> bucket = redissonClient.getBucket(key.toString());
value = bucket.get();
if (value != null) {
// 将数据回填到本地缓存
localCache.put(key, value);
}
return value;
}

/**
* 添加了一个`ConcurrentHashMap`来存储锁对象,并在获取数据时使用了一个双重检查锁定模式。
* 当本地缓存中没有数据时,首先获取一个锁,然后再次检查本地缓存以确保数据在获取锁的过程中没有被其他线程填充。
* 如果本地缓存仍然没有数据,会从Redis获取数据,如果Redis也没有数据,则从数据源加载数据并更新Redis和本地缓存。
* 这样的策略可以减少缓存击穿的风险。
*
* @param key
* @param mappingFunction
* @return
*/
public V get4Consistency(K key, Function<? super K, ? extends V> mappingFunction) {
// 先尝试从本地缓存获取数据
V value = localCache.getIfPresent(key);
if (value != null && value != NULL_PLACEHOLDER) {
return value;
}

// 获取锁对象,如果不存在则创建一个新的
Lock lock = locks.computeIfAbsent(key, k -> new ReentrantLock());
try {
// 锁定当前键,以便同步更新操作
lock.lock();
// 再次检查本地缓存,以防在获取锁的过程中数据被更新
value = localCache.getIfPresent(key);
if (value != null && value != NULL_PLACEHOLDER) {
return value;
}

// 尝试从 Redis 获取
RBucket<V> bucket = redissonClient.getBucket(key.toString());
V redisValue = bucket.get();

if (redisValue != null) {
// 如果在 Redis 中找到了,将其回填到本地缓存
localCache.put(key, redisValue);
return redisValue;
}

// 从数据源加载数据
V loadedValue = mappingFunction.apply(key);
if (loadedValue == null) {
// 存储空对象到本地缓存和 Redis 防止缓存穿透
localCache.put(key, (V) NULL_PLACEHOLDER);
bucket.set((V) NULL_PLACEHOLDER, redisExpiration, TimeUnit.SECONDS);
} else {
// 先将加载的数据写入 Redis
bucket.set(loadedValue, redisExpiration, TimeUnit.SECONDS);
// 然后回填到本地缓存
localCache.put(key, loadedValue);
}
return loadedValue;
} finally {
// 释放锁
lock.unlock();
// 移除锁对象,避免内存泄漏
locks.remove(key);
}
}

public V get(K key, Function<? super K, ? extends V> mappingFunction) {
// 尝试从本地缓存获取数据
V value = localCache.get(key, k -> {
// 尝试从 Redis 获取
RBucket<V> bucket = redissonClient.getBucket(k.toString());
V redisValue = bucket.get();
if (redisValue != null) {
return redisValue;
}

// 从数据源加载数据
V loadedValue = mappingFunction.apply(k);
if (loadedValue == null) {
// 存储空对象到本地缓存和 Redis 防止缓存穿透
localCache.put(k, (V) NULL_PLACEHOLDER);
bucket.set((V) NULL_PLACEHOLDER, redisExpiration, TimeUnit.SECONDS);
return null;
}

// 将加载的数据写入 Redis
bucket.set(loadedValue, redisExpiration, TimeUnit.SECONDS);
return loadedValue;
});

return value == NULL_PLACEHOLDER ? null : value;
}

public void put(K key, V value) {
// 同时更新本地缓存和 Redis 缓存
localCache.put(key, value);
redissonClient.getBucket(key.toString()).set(value, redisExpiration, TimeUnit.SECONDS);
}

public void invalidate(K key) {
// 同时移除本地缓存和 Redis 缓存中的数据
localCache.invalidate(key);
redissonClient.getBucket(key.toString()).delete();
}

public void refresh(K key, Function<? super K, ? extends V> mappingFunction) {
// 从数据源重新加载数据并更新缓存
V value = mappingFunction.apply(key);
if (value != null) {
put(key, value);
}
}

// 批量获取数据
public Map<K, V> getAll(Iterable<? extends K> keys) {
Map<K, V> allValues = localCache.getAllPresent(keys);
if (allValues.size() == keys.spliterator().getExactSizeIfKnown()) {
return allValues;
}

// 获取缺失的键
List<K> missingKeys = StreamSupport.stream(keys.spliterator(), false)
.filter(key -> !allValues.containsKey(key))
.collect(Collectors.toList());

// 从 Redis 批量获取缺失的键
Map<K, V> redisValues = missingKeys.stream()
.collect(Collectors.toMap(Function.identity(), key -> redissonClient.<V>getBucket(key.toString()).get()));

// 将 Redis 中获取的值回填到本地缓存
localCache.putAll(redisValues);

// 合并两个 map 并返回
allValues.putAll(redisValues);
return allValues;
}

// 批量写入数据
public void putAll(Map<? extends K, ? extends V> map) {
localCache.putAll(map);
map.forEach((key, value) -> redissonClient.getBucket(key.toString()).set(value, redisExpiration, TimeUnit.SECONDS));
}

// 批量移除数据
public void invalidateAll(Iterable<? extends K> keys) {
localCache.invalidateAll(keys);
keys.forEach(key -> redissonClient.getBucket(key.toString()).delete());
}

// 获取缓存统计信息
//这里的统计信息只来自Caffeine本地缓存,因为Redisson不提供原生的缓存统计信息
public CacheStats stats() {
return localCache.stats();
}

// 关闭方法需要关闭写回策略的线程池
public void shutdown() {
writeBehindExecutor.shutdown();
try {
if (!writeBehindExecutor.awaitTermination(60, TimeUnit.SECONDS)) {
writeBehindExecutor.shutdownNow();
}
} catch (InterruptedException e) {
writeBehindExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

1713948338171.png

重要功能:

  • getAll:批量从缓存获取数据。首先从本地缓存获取,如果本地缓存中缺失,则从Redis中获取,并回填到本地缓存。
  • putAll:批量写入数据到本地和Redis缓存。
  • invalidateAll:批量从本地和Redis缓存中移除数据。
  • stats:获取Caffeine缓存的统计信息。

Note:

添加了一个ConcurrentHashMap来存储锁对象,并在获取数据时使用了一个双重检查锁定模式。当本地缓存中没有数据时,首先获取一个锁,然后再次检查本地缓存以确保数据在获取锁的过程中没有被其他线程填充。如果本地缓存仍然没有数据,会从Redis获取数据,如果Redis也没有数据,则从数据源加载数据并更新Redis和本地缓存。这样的策略可以减少缓存击穿的风险。

请注意,这种锁的使用会增加系统的复杂性,并可能导致性能开销,特别是在高并发场景下。因此,在实现这种机制时,需要仔细衡量其潜在的性能影响。

加了一个单线程的 ExecutorService 用于处理写回策略。当本地缓存中的条目因为驱逐策略被移除时,会将这个条目异步地写入 Redis。还添加了一个 shutdown 方法来关闭线程池。

对于缓存穿透保护,可以在 get 方法中加入逻辑来返回空对象或者使用布隆过滤器来预先检查键是否可能存在,一个特殊的空对象 NULL_PLACEHOLDER 存储到本地缓存和 Redis 中,这样下次查询相同的键时就能直接从缓存中获取到空对象,从而防止缓存穿透。

测试类:

使用示例:

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
java复制代码
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* @Author derek_smart
* @Date 202/4/24 15:25
* @Description caffeine和redis 缓存组合测试类
*/
public class HybridCacheExample {

public static void main(String[] args) {
// 配置 Redisson
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redissonClient = Redisson.create(config);
// 创建混合缓存实例
HybridCache<String, String> hybridCache = new HybridCache<>(
redissonClient,
1000, // 本地缓存最大条目数
10, // 本地缓存过期时间
TimeUnit.MINUTES, // 本地缓存时间单位
60 * 60 // Redis 缓存过期时间(秒)
);

// 模拟数据加载函数
Function<String, String> dataLoader = key -> {
// 模拟数据库或其他数据源的加载过程
System.out.println("Loading data for key: " + key);
return "Value for " + key;
};

// 尝试获取缓存数据,如果没有,则使用提供的函数从数据源加载
String key = "key1";
String value = hybridCache.get(key, dataLoader);
System.out.println("Value: " + value);

// 更新缓存数据
String newValue = "Updated value";
hybridCache.put(key, newValue);

// 从缓存获取更新后的数据
String updatedValue = hybridCache.get(key);
System.out.println("Updated Value: " + updatedValue);

// 刷新缓存数据
hybridCache.refresh(key, dataLoader);

// 使缓存的数据失效
hybridCache.invalidate(key);

// 关闭 Redisson 客户端和混合缓存
hybridCache.shutdown();
redissonClient.shutdown();
}
}

企业微信截图_17139484338926.png

测试说明:

在这个示例中,首先配置了 Redisson 客户端并连接到本地运行的 Redis 服务器。然后,创建了一个 HybridCache 实例,设置了本地缓存的大小和过期时间,以及 Redis 缓存的过期时间。

定义了一个 dataLoader 函数,它模拟了从数据库或其他数据源加载数据的过程。接着,使用 hybridCache.get 方法尝试从缓存中获取数据。如果本地缓存和 Redis 缓存中都没有找到数据,将调用 dataLoader 函数加载数据并存入缓存。

然后,更新了缓存中的数据,并再次从缓存中获取更新后的数据。之后,使用 refresh 方法来手动刷新缓存中的数据。最后,使用 invalidate 方法使缓存中的数据失效,并关闭 Redisson 客户端和混合缓存。

Note:

请注意,在实际应用中,可能需要根据实际业务逻辑和数据源来实现数据加载函数。此外,还需要确保 Redis 服务器正在运行且可访问。

优化点1: 关于老铁提出问题再次优化

在使用 Caffeine 缓存时,确实可能需要设置总大小上限或键的个数上限,并且有时候需要为每个键设置不同的过期时间。

以下是如何在配置 Caffeine 缓存时实现这两个优化:

1. 设置 Caffeine 缓存的总大小上限或键的个数上限

Caffeine 提供了两种方式来限制缓存的大小:基于权重的限制和基于最大条目数的限制。权重可以通过实现 Weigher 接口来定义,这里是一个基于键和值的大小设置权重的示例:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码Caffeine.newBuilder()
.maximumWeight(10000) // 设置缓存的最大权重
.weigher(new Weigher<KeyType, ValueType>() {
@Override
public int weigh(KeyType key, ValueType value) {
// 自定义计算权重的逻辑
// 例如,我们可以假设每个缓存项的权重为1
return 1;
}
})
// 其他配置...
.build();

如果只需要限制缓存的最大条目数,可以使用 maximumSize 方法:

1
2
3
4
java复制代码Caffeine.newBuilder()
.maximumSize(1000) // 设置缓存的最大条目数
// 其他配置...
.build();

2. 为每个键设置不同的过期时间

Caffeine 允许你通过实现 Expiry 接口来为每个键定义不同的过期策略。

下面是一个示例,展示如何为不同的键设置不同的过期时间:

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
java复制代码Caffeine.newBuilder()
.expireAfter(new Expiry<KeyType, ValueType>() {
@Override
public long expireAfterCreate(KeyType key, ValueType value, long currentTime) {
// 创建后的过期时间逻辑
// 例如,根据键的特定属性决定过期时间
return TimeUnit.MINUTES.toNanos(getCustomExpiryForKey(key));
}

@Override
public long expireAfterUpdate(KeyType key, ValueType value, long currentTime, long currentDuration) {
// 更新后的过期时间逻辑
// 可以选择保留当前的过期时间或者重新计算
return currentDuration;
}

@Override
public long expireAfterRead(KeyType key, ValueType value, long currentTime, long currentDuration) {
// 读取后的过期时间逻辑
// 可以选择保留当前的过期时间或者重新计算
return currentDuration;
}
})
// 其他配置...
.build();

在这个 Expiry 实现中,expireAfterCreate 方法定义了每个键在创建时的过期时间。expireAfterUpdate 和 expireAfterRead 方法允许你在键被更新或读取后调整它们的过期时间

优化点2 分布式环境本地缓存和Redis一致性问题:

在分布式环境中,确保本地缓存(如 Caffeine)与分布式缓存(如 Redis)之间的一致性是一个常见的挑战。
为了减少中间件的引入并使用 Redis 自身的消息推送功能,我们可以依赖于 Redis 的发布/订阅机制来通知各个应用实例缓存的变动。

解决方案:

  1. 本地缓存一致性: 当一个应用实例更新了 Redis 中的一个键值对时,它可以通过 Redis 的发布/订阅系统发布一个消息。其他应用实例订阅了这个消息,可以据此来清除或更新本地 Caffeine 缓存中相应的键。
  2. 键值变更处理: 如果一个键的值在 Redis 中发生变更,我们不直接在本地缓存中更新这个值,而是删除本地缓存中的这个键。下次访问这个键时,如果本地缓存中没有找到,就会从 Redis 中加载并重新放入 Caffeine 缓存。

实现步骤:

  1. 发布消息: 当一个键在 Redis 中被更新或删除时,发布一个消息到特定的频道。
1
2
3
java复制代码public void publishKeyInvalidation(String key) {
redissonClient.getTopic("cache-invalidation-channel").publish(key);
}
  1. 订阅消息: 在应用启动时,订阅 Redis 频道,监听键失效的消息。
1
2
3
4
5
6
7
java复制代码public void subscribeToCacheInvalidationChannel() {
RTopic topic = redissonClient.getTopic("cache-invalidation-channel");
topic.addListener(String.class, (channel, key) -> {
// 当接收到消息时,从本地缓存中移除相应的键
caffeineCache.invalidate(key);
});
}
  1. 处理键值变更: 当需要更新键值时,先在 Redis 中进行更新,然后发布消息。
1
2
3
4
5
6
java复制代码public void updateValueInCache(String key, ValueType value) {
// 更新 Redis 中的值
redissonClient.getMap("distributed-cache").fastPut(key, value);
// 发布失效消息
publishKeyInvalidation(key);
}
  1. 处理本地缓存: 当本地缓存尝试访问一个键时,如果缓存中没有,则从 Redis 加载。
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public ValueType getValueFromCache(String key) {
// 尝试从本地缓存获取值
ValueType value = caffeineCache.getIfPresent(key);
if (value == null) {
// 本地缓存未命中,从 Redis 加载
value = (ValueType) redissonClient.getMap("distributed-cache").get(key);
if (value != null) {
// 将加载的值放入本地缓存
caffeineCache.put(key, value);
}
}
return value;
}

注意事项:

  • 这种方案假设 Redis 的读写操作比本地缓存的操作要慢,因此在 Redis 缓存中更新数据后,我们只是删除本地缓存中的数据,而不是更新它。
  • 在高并发环境中,可能会出现短暂的不一致,因为消息传递和处理需要时间。
  • 为了避免不必要的 Redis 访问,可以在本地缓存中设置一个短暂的过期时间,这样即使不立即从 Redis 加载数据,本地缓存中的数据也会很快过期并被刷新。
    通过以上步骤,可以构建一个既利用了本地缓存速度优势,又保持了与分布式缓存一致性的混合缓存策略。

优化点3:如果每个KEY的VALUE非常大,该如何处理

当每个键的值非常大时,仅仅通过设置 maximumSize 来限制键的数量可能是不够的,因为这样做不能有效控制占用的总内存大小。
在这种情况下,需要考虑整体的内存使用情况,并使用基于权重的缓存大小限制,
这可以通过 Caffeine 的 maximumWeight 和 weigher 配置来实现。这里是如何使用 maximumWeight 和 weigher 来限制缓存的总内存使用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码Caffeine.newBuilder()
.maximumWeight(estimatedMaxMemory)
.weigher(new Weigher<KeyType, ValueType>() {
@Override
public int weigh(KeyType key, ValueType value) {
// 估算每个缓存项的内存占用大小,例如,可以通过某种方式估算对象的大小
int valueSize = estimateSizeOf(value);
// 如果你有键和值的大小,可以将它们加起来
int keySize = estimateSizeOf(key);
return keySize + valueSize;
}
})
// 可以结合使用expireAfterWrite或expireAfterAccess来设置过期策略
.expireAfterWrite(10, TimeUnit.MINUTES)
// 其他配置...
.build();

// 估算对象大小的方法示例
public int estimateSizeOf(Object object) {
// 实现对象大小估算逻辑,可能需要使用一些启发式方法或者工具库
// 例如,使用java.lang.instrument.Instrumentation来获取对象大小
// 或者使用某些估算规则,比如一个字符串大约占用其长度 * 2字节(因为Java中的char是2字节)
}

使用 maximumWeight 和 weigher 的组合,你可以更好地控制缓存的内存占用,而不仅仅是缓存项的数量。这样可以保证即使

本文转载自: 掘金

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

基于Redission高级应用5-RLocalCachedM

发表于 2024-04-25

概述

原理

RLocalCachedMap 是 Redisson 提供的一个特殊的 Map 实现,它在 Java 应用内存中提供了一个本地缓存层,同时保持与远程 Redis 服务器上的数据同步。这样做的目的是减少对 Redis 服务器的网络访问次数,从而提高性能。

在 RLocalCachedMap 中,当您读取一个键时,它首先会在本地缓存中查找。如果找到了,它将直接返回本地的值,这样可以非常快速地获取数据,因为不涉及网络通信。如果本地缓存中没有找到,它会从 Redis 服务器获取数据,并更新本地缓存。

此外,RLocalCachedMap 还提供了一致性保证和缓存失效策略。例如,您可以配置它在本地缓存中使用 LRU(最近最少使用)算法来自动淘汰旧的数据,或者在数据更新时通过发布/订阅机制来失效本地缓存。

优点

  1. 性能提升:由于减少了对 Redis 服务器的网络访问,对于读取操作,RLocalCachedMap 可以显著提高性能。
  2. 降低延迟:本地缓存可以提供更低的数据访问延迟,特别是在高延迟网络环境中。
  3. 减轻 Redis 服务器负担:本地缓存减少了对 Redis 服务器的请求,有助于降低服务器的负载和带宽消耗。
  4. 灵活的缓存策略:支持多种缓存失效策略,如 LRU、软引用、弱引用等,可以根据应用需求灵活配置。
  5. 数据一致性:通过发布/订阅机制,RLocalCachedMap 可以在分布式环境中保持本地缓存与 Redis 服务器之间的数据一致性。

缺点

  1. 内存使用:由于数据被缓存到本地 JVM 堆内存中,这可能会增加 Java 应用的内存使用量。
  2. 一致性挑战:尽管 RLocalCachedMap 提供了机制来保持一致性,但在高并发或网络分区的情况下,仍然可能遇到一致性问题。
  3. 复杂性增加:引入本地缓存层可能会增加系统的复杂性,需要更多的配置和管理。
  4. 热点问题:如果不同节点频繁访问相同的数据,本地缓存可能不会带来太多性能提升,因为 Redis 服务器仍然需要处理大量的写操作。
  5. 数据恢复:在应用重启或缓存丢失的情况下,需要从 Redis 服务器重新加载数据,这可能会导致暂时的性能下降。

在使用 RLocalCachedMap 时,应该根据应用的具体需求和资源限制来权衡其优缺点,并合理配置本地缓存的大小和失效策略

总结:

RLocalCachedMap 是 Redisson 提供的一种本地缓存集成的分布式映射表(Map),它结合了 Redis 的分布式能力和本地 JVM 缓存的高性能读取。这种数据结构特别适合于读取操作远多于写入操作的场景,因为它可以大幅减少网络延迟和Redis服务器的读取压力。

实战应用示例

配置数据缓存

在应用程序中,经常需要读取配置数据,这些数据变化不频繁,但是需要快速访问。

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
java复制代码@Autowired
private RedissonClient redissonClient;

// 初始化配置缓存
public RLocalCachedMap<String, String> initializeConfigCache() {
LocalCachedMapOptions<String, String> options = LocalCachedMapOptions.<String, String>defaults()
.cacheSize(1000) // 本地缓存大小
.timeToLive(60 * 60 * 1000) // 数据有效期
.maxIdle(30 * 60 * 1000) // 最大空闲时间
.invalidationPolicy(LocalCachedMapOptions.InvalidationPolicy.ON_CHANGE) // 缓存失效策略
.reconnectionStrategy(LocalCachedMapOptions.ReconnectionStrategy.CLEAR) // 重新连接策略
.syncStrategy(LocalCachedMapOptions.SyncStrategy.UPDATE) // 同步策略
.evictionPolicy(EvictionPolicy.LFU); // 淘汰策略

return redissonClient.getLocalCachedMap("configMap", options);
}

// 从缓存中获取配置项
public String getConfig(String key) {
RLocalCachedMap<String, String> configMap = initializeConfigCache();
return configMap.get(key);
}

// 更新配置项
public void updateConfig(String key, String value) {
RLocalCachedMap<String, String> configMap = initializeConfigCache();
configMap.fastPut(key, value);
}

用户会话缓存

在Web应用中,用户会话信息通常需要快速读取,同时保证分布式环境下的一致性。

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 RLocalCachedMap<String, UserSession> initializeSessionCache() {
LocalCachedMapOptions<String, UserSession> options = LocalCachedMapOptions.<String, UserSession>defaults()
.cacheSize(10000)
.invalidationPolicy(LocalCachedMapOptions.InvalidationPolicy.ON_CHANGE)
.syncStrategy(LocalCachedMapOptions.SyncStrategy.UPDATE);
return redissonClient.getLocalCachedMap("sessionMap", options);
}

// 获取用户会话
public UserSession getUserSession(String sessionId) {
RLocalCachedMap<String, UserSession> sessionMap = initializeSessionCache();
return sessionMap.get(sessionId);
}

// 更新用户会话
public void updateUserSession(String sessionId, UserSession userSession) {
RLocalCachedMap<String, UserSession> sessionMap = initializeSessionCache();
sessionMap.fastPut(sessionId, userSession);
}

商品库存缓存

在电商平台中,商品库存信息需要频繁读取,同时更新要保证高一致性。

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

// 初始化库存缓存
public RLocalCachedMap<String, Integer> initializeInventoryCache() {
LocalCachedMapOptions<String, Integer> options = LocalCachedMapOptions.<String, Integer>defaults()
.cacheSize(5000)
.invalidationPolicy(LocalCachedMapOptions.InvalidationPolicy.ON_CHANGE)
.syncStrategy(LocalCachedMapOptions.SyncStrategy.INVALIDATE);

return redissonClient.getLocalCachedMap("inventoryMap", options);
}

// 获取商品库存
public Integer getInventory(String productId) {
RLocalCachedMap<String, Integer> inventoryMap = initializeInventoryCache();
return inventoryMap.get(productId);
}

// 更新商品库存
public void updateInventory(String productId, Integer amount) {
RLocalCachedMap<String, Integer> inventoryMap = initializeInventoryCache();
inventoryMap.fastPut(productId, amount);
}

在这些示例中,RLocalCachedMap 的本地缓存能力大幅提高了读取速度,而通过不同的失效和同步策略,它能够在多个节点之间保持数据的一致性。这些特性使得 RLocalCachedMap 在需要高性能读取和数据一致性的分布式应用中非常有用。

Note:

需要注意的是,在使用 RLocalCachedMap 时,应当根据具体的应用场景合理配置本地缓存的大小、失效策略、同步策略等选项,以达到最优的性能和一致性平衡。

RLocalCachedMap 的高级应用

通常涉及对数据一致性、读写性能和本地缓存行为的精细控制。在这些场景中,开发者可能需要结合 Redisson 的特性来实现复杂的业务逻辑。
以下是一个高级应用的示:

分布式缓存的数据加载和预热

在大型应用中,可能需要在应用启动时预加载和预热缓存数据,以确保应用可以快速响应用户请求。以下是一个数据预加载和预热的示例:

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
java复制代码@Autowired
private RedissonClient redissonClient;

// 缓存配置选项
private LocalCachedMapOptions<String, Product> options = LocalCachedMapOptions.<String, Product>defaults()
.cacheSize(10000)
.timeToLive(24 * 60 * 60 * 1000) // 24小时
.maxIdle(12 * 60 * 60 * 1000) // 12小时
.invalidationPolicy(LocalCachedMapOptions.InvalidationPolicy.ON_CHANGE)
.reconnectionStrategy(LocalCachedMapOptions.ReconnectionStrategy.CLEAR)
.syncStrategy(LocalCachedMapOptions.SyncStrategy.UPDATE)
.evictionPolicy(EvictionPolicy.LRU);

// 初始化商品信息缓存
public RLocalCachedMap<String, Product> initializeProductCache() {
return redissonClient.getLocalCachedMap("productCache", options);
}

// 数据预加载
public void preloadProductData() {
RLocalCachedMap<String, Product> productCache = initializeProductCache();

// 假设这个方法从数据库或其他数据源加载所有产品信息
Map<String, Product> allProducts = loadAllProductsFromDataSource();

// 使用 readAllMap 方法一次性加载所有数据到本地缓存
productCache.readAllMap().putAll(allProducts);
}

// 从缓存获取商品信息
public Product getProduct(String productId) {
RLocalCachedMap<String, Product> productCache = initializeProductCache();
return productCache.get(productId);
}

// 更新商品信息
public void updateProduct(String productId, Product product) {
RLocalCachedMap<String, Product> productCache = initializeProductCache();
productCache.fastPut(productId, product);
}

在这个示例中,定义了一个 RLocalCachedMap 来存储商品信息,并提供了数据预加载的功能。在应用启动时,从数据源加载所有商品信息并填充到本地缓存中,这样就可以在不需要每次从数据源加载的情况下快速获取商品信息。

注意事项

  • preloadProductData 方法应该在应用启动时调用,以确保本地缓存被预热。- 本地缓存的大小、存活时间和最大空闲时间应该根据实际情况进行配置。
  • 缓存失效策略 ON_CHANGE 表示当缓存的数据在 Redis 中被修改时,本地缓存将被失效。
  • 同步策略 UPDATE 表示本地缓存将定期与 Redis 中的数据同步更新。

这个高级应用示例展示了如何使用 RLocalCachedMap 来提高数据访问的性能,并通过预加载和预热来减少启动初期的延迟。此外,它也展示了如何通过配置不同的策略来控制本地缓存的行为。在实际开发中,开发者可以根据业务需求和系统资源情况,调整和优化这些配置。

本文转载自: 掘金

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

可视化配置 Nginx 代理:功能完备,使用简单 开源日

发表于 2024-04-25

picture

NginxProxyManager/nginx-proxy-manager

Stars: 18.0k License: MIT

picture

nginx-proxy-manager 是一个用于管理 Nginx 代理主机的 Docker 容器,具有简单而强大的界面。
该项目旨在提供用户一种简单易用的方式来实现反向代理主机和 SSL 证书,无需太多关于 Nginx 或 Let’s Encrypt 的知识。

其主要功能和核心优势包括:

  • 美观且安全的基于 Tabler 的管理员界面
  • 轻松创建转发域、重定向、流以及 404 主机,无需了解任何关于 Nginx 的知识
  • 使用 Let’s Encrypt 免费 SSL 或提供自定义 SSL 证书
  • 访问列表和基本 HTTP 身份验证
  • 高级 Nginx 配置可供超级用户使用
  • 用户管理、权限和审计日志

argoproj/argo-cd

Stars: 15.7k License: Apache-2.0

picture

argo-cd 是为 Kubernetes 提供声明式持续部署的工具。

  • Argo CD 是一个声明式的 GitOps 连续交付工具,适用于 Kubernetes。
  • 应用程序定义、配置和环境应该是声明性的,并且应该进行版本控制。
  • 应用程序部署和生命周期管理应该是自动化、可审计且易于理解的。

actions/runner-images

Stars: 8.8k License: MIT

runner-images 是 GitHub Actions 和 Azure Pipelines 使用的虚拟机镜像源代码库。
该项目主要功能、关键特性和核心优势包括:

  • 包含了用于 GitHub-hosted runners 和 Microsoft-hosted agents 的虚拟机镜像源代码
  • 提供了各种操作系统版本的镜像,如 Ubuntu 22.04、Ubuntu 20.04、macOS 14 [beta] 等
  • 支持 Beta 和 GA(General Availability)两种类型的镜像发布,以及最新版本迁移流程和预告变更。

ente-io/ente

Stars: 3.4k License: AGPL-3.0

picture

ente 是一个完全开源的端到端加密替代品,可用于存储数据。

  • 提供完全开源的端到端加密平台
  • 包括两款示例应用:Ente Photos(类似于苹果和谷歌相册)和 Ente Auth(2FA 替代 Authy)
  • 客户端覆盖 iOS / Android / F-Droid / Web / Linux / macOS / Windows
  • 数据复制三份、设备机器学习、跨平台等功能
  • 通过 Cure53、Symbolic Software 和 Fallible 进行外部审核

wasmerio/winterjs

Stars: 2.5k License: MIT

picture

WinterJS 是一个快速的 JavaScript 服务器,根据 Winter Community Group 规范运行 Service Workers 脚本。

  • 可以处理高达每秒 100,000 次请求
  • 使用 SpiderMonkey、Spiderfire 和 hyper 技术实现
  • 支持 WASIX 标准编译为 WebAssembly
  • 兼容 WinterCG 规范的 API 包括 console、fetch、URL 等等,并且支持一些其他非 WinterCG 的 API 如 Service Workers Caches API。

本文转载自: 掘金

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

FFmpeg开发——基础篇(一) 前言 核心结构体 总结

发表于 2024-04-25

前言

书接上回,我们介绍了ffmpeg的一些基础知识,使用方法,接下来介绍如何使用ffmpeg进行开发,所谓使用ffmpeg进行开发,就是依赖它的基础库,调用它的API来实现我们的功能。

当然要看懂文章需要一些C++的基础知识,能看懂基本语法,了解指针(一级指针/二级指针)的基本知识。

image.png

我们在上篇文章中也介绍了ffmpeg对于音视频操作的主要流程:

image.png

其实差不多每个阶段都通过对应的关键函数以及关键结构体来对应,因此,接下来先重点介绍一下这些重要的结构体,以及它的用法。

核心结构体

AVFormatContext

媒体信息存储结构,同时管理了IO音视频流对文件进行读写,相当于保存了音视频信息的上下文。

它在ffmpeg中的作用是非常重要的,在封装/解封装/编解码过程中都需要用到它。在程序中关于某个音视频的所有信息归根结底都来自于AVFormatContext。

结构体信息

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
objectivec复制代码
typedef struct AVFormatContext {
// 针对输入逻辑的结构体
const struct AVInputFormat *iformat;
// 针对输出逻辑的结构体
const struct AVOutputFormat *oformat;

//字节流IO操作 结构体
AVIOContext *pb;
...
...
unsigned int nb_streams; // 视音频流的个数

AVStream **streams; // 视音频流


char *url; //输入或输出地址 替换原有的filename

int64_t duration; // 时长,微秒(1s/1000_000)

int64_t bit_rate; // 比特率(单位bps,转换为kbps需要除以1000)

...
...
} AVFormatContext;

以上只是一个简略的结构体成员信息展示,但是已经能体现它管理IO,数据流,保存媒体信息的的功能了,对于初学者而言,只需要关注nb_streams和streams这两个成员,表示流的数量以及流数组。后面我们需要通过流来获取对应的信息。

使用的基本方法

输入过程

  • 申请内存创建结构体
1
2
ini复制代码// 可以,但一般没必要
AVFormatContext *formatContext = avformat_alloc_context();

但是一般在开发中并不需要开发者手动创建结构体,而是在读取文件的接口中通过库自动创建即可

1
2
3
4
5
6
7
8
c复制代码AVFormatContext *formatContext = NULL;


// 注意 即使传入的formatContext为NULL,avformat_open_input内部也会为formatContext申请内存空间的
if (avformat_open_input(&formatContext, "xxx.mp4", NULL, NULL) != 0) {
fprintf(stderr, "Failed to open input file\n");
return 1;
}

注意我们需要在avformat_open_input函数中传入正确的媒体文件路径,这样才能正确读取到文件头的信息。

  • 为防止获取不到文件头信息,可以尝试进一步获取音视频流的信息
1
2
3
4
c复制代码if (avformat_find_stream_info(formatContext, NULL) < 0) {
fprintf(stderr, "Failed to find stream information\n");
return 1;
}

avformat_open_input即使成功调用,也不一定能获取到文件头信息,因为可能有的媒体格式没有文件头?哈哈,所以一般继续调用avformat_find_stream_info可以获取正确的信息

  • 读取数据包:确认有音视频数据之后,可以通过av_read_frame把数据流读取到AVPacket中
1
2
3
4
5
6
7
8
scss复制代码// 此处主要涉及解码过程,可以略过,知道解码过程也需要传递formatContext信息即可
AVPacket packet;
while (av_read_frame(formatContext, &packet) == 0) {
// 处理 packet 中的数据

// 在使用完 packet 后释放引用
av_packet_unref(&packet);
}
  • 关闭输入
1
scss复制代码avformat_close_input(&formatContext);

avformat_close_input会关闭输入流,同时释放AVFormatContext结构体

输出过程

上面介绍的主要输入过程中使用AVFormatContext的基本方式,那么输出过程是否一致呢?函数调用上略有区别。

  • 创建输出音视频格式的AVFormatContext
1
2
3
4
5
objectivec复制代码// 通常在你需要进行音视频编码并生成一个新的音视频文件时使用
AVFormatContext *output_format_context = NULL;

//
avformat_alloc_output_context2(&output_format_context, NULL, NULL, out_filename);
  • 写入数据
1
2
3
4
5
6
7
8
scss复制代码//先写入头文件
ret = avformat_write_header(output_format_context, &opts);

//再写入帧数据
ret = av_interleaved_write_frame(output_format_context, &packet);

// 写入收尾(同时刷新缓冲区)
av_write_trailer(output_format_context);
  • 释放avformatcontext结构体
1
scss复制代码  avformat_free_context(output_format_context);

AVStream

AVStream是AVFormatContext结构体中的一个成员(数组结构),它表示媒体文件中某一种数据的流以及对应的媒体信息,比如该流表示视频流,则同时也会含有视频相关的宽高,帧率等信息,以及time_base等基础信息。

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
objectivec复制代码
typedef struct AVStream {


int index; /**< stream index in AVFormatContext */
// stream ID
int id;

// 与流关联的编解码器的参数结构
AVCodecParameters *codecpar;

//time_base AVRational结构体有两个成员,组成一个分数(有理数)
AVRational time_base;

...
...

int64_t duration;

int64_t nb_frames; ///< number of frames in this stream if known or 0
...
...
/**
* sample aspect ratio (0 if unknown)
* - encoding: Set by user.
* - decoding: Set by libavformat.
*/
AVRational sample_aspect_ratio;
...
...
} AVStream;

对于初学者而言,可以先重点关注time_base和codecpar这两个成员,time_base不用讲,是ffmpeg中的时间基本单位,codecpar则表示了当前流的解码信息。

我们可以看一下AVCodecParameters这个结构体的成员情况

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
objectivec复制代码typedef struct AVCodecParameters {
/**
* General type of the encoded data.
*/
enum AVMediaType codec_type;
/**
* Specific type of the encoded data (the codec used).
*/
enum AVCodecID codec_id;
...

/**
* - video: the pixel format, the value corresponds to enum AVPixelFormat.
* - audio: the sample format, the value corresponds to enum AVSampleFormat.
*/
int format;
...
...
/**
* 视频帧相关的一些参数
* Video only. The dimensions of the video frame in pixels.
*/
int width;
int height;

AVRational sample_aspect_ratio;

enum AVColorRange color_range;
enum AVColorPrimaries color_primaries;
enum AVColorTransferCharacteristic color_trc;
enum AVColorSpace color_space;
enum AVChromaLocation chroma_location;

/**
* Audio only. The number of audio samples per second.
*/
int sample_rate;

// Audio only. Audio frame size
int frame_size;

// 声道配置情况(音频)
AVChannelLayout ch_layout;


AVRational framerate;

} AVCodecParameters;

可以看到AVCodecParameters是把音频和视频这两种信息混合在一起了,在流属于不同类型时使用不同的字段,或者同一个字段表达不同的含义。比如format,如果是视频,则表示AVPixelFormat枚举类型,如果是音频,则表示AVSampleFormat枚举类型。

  • 如何获取AVStream
1
2
3
4
5
6
7
8
9
10
ini复制代码// formatContext->nb_streams表示流的个数
int stream_size = formatContext->nb_streams;

for(int i=0;i<stream_size;i++){
//获取一个AVStream
AVStream *in_stream = formatContext->streams[i];
// 从AVStream中获取AVCodecParameters
AVCodecParameters *av_in_codec_param = in_stream->codecpar;
...
}

AVCodec

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
objectivec复制代码//编解码器
typedef struct AVCodec {
// 编解码器的名称
const char *name;
const char *long_name;
enum AVMediaType type; // 媒体类型(视频,音频,字幕等)
enum AVCodecID id; // 编解码器的ID

// 编解码器所支持的一些参数
const AVRational *supported_framerates; ///< array of supported framerates, or NULL if any, array is terminated by {0,0}
const enum AVPixelFormat *pix_fmts; ///< array of supported pixel formats, or NULL if unknown, array is terminated by -1
const int *supported_samplerates; ///< array of supported audio samplerates, or NULL if unknown, array is terminated by 0
const enum AVSampleFormat *sample_fmts; ///< array of supported sample formats, or NULL if unknown, array is terminated by -1

/**
* Array of supported channel layouts, terminated with a zeroed layout.
*/
const AVChannelLayout *ch_layouts;
} AVCodec;

AVCodec可以表示一个编解码器,里面包含了编解码的一些基本信息。

一般而言,媒体文件中的音频流,视频流中都保存有解码器ID等信息,通过这个ID可以获取对应AVCodec,从而获取该解码器的比较全面的信息。

  • 获取AVCodec
1
2
3
4
5
6
rust复制代码// formatContext即 AVFormatContext的结构体对象,此时应该已经创建并读取了信息
// codecpar是AVStream中的结构体成员,表示该流数据对应的解码器信息
// 从流中找到对应编解码器信息和id
enum AVCodecID id = formatContext->streams[videoStreamIndex]->codecpar->codec_id
// 通过ID找到对应的编解码器
AVCodec *av_codec = avcodec_find_decoder(id);

获取到AVCodec之后,需要通过它构建一个可用编解码器上下文(提供编解码过程中待解码数据的背景和配置)

AVCodecContext

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
objectivec复制代码
typedef struct AVCodecContext {

enum AVMediaType codec_type;// 数据类型(音频、视频、字幕、等)
const struct AVCodec *codec; // 对应的编解码器
enum AVCodecID codec_id; //编解码器ID

// time_base,编码时必须设置
    AVRational time_base;
   
    /*视频使用*/
int width, height;
// 像素格式,告诉解码器你想要把数据解码成哪个像素格式,不设置的话ffmpeg会有默认值
enum AVPixelFormat pix_fmt;

    /* audio only */
    int sample_rate; ///< samples per second
...
...
enum AVSampleFormat sample_fmt; ///< 采样格式

// AVFrame中每个声道的采样数,音频时使用
int frame_size;
//也是time_base,解码时设置
    AVRational pkt_timebase;
}

AVCodecContext就是我们前面说的编解码上下文,主要包含待解码数据的一些特性,便于在解码过程中解码器正确解析数据。比如等待解码的是视频数据,那么解码器需要知道time_base(关于time_base的概念不懂可以看前一篇文章)统一时间单位;每帧图片的宽高;视频帧的像素格式(关于像素格式见(移动开发中关于视频的一些基本概念),了解像素排列方式….

有了以上信息,解码器才可以正确的对数据进行解码。

AVCodecContext中音频的的frame_size,在解码器中可能不存在,因此在解码过程避免使用这个字段,可以找decoded_frame中的nb_samples来替代

  • 创建(解码为例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码// id 是从前文中通过AVStream中获取的
// 获取到对应的编解码器
AVCodec *av_codec = avcodec_find_decoder(id);

// 创建AVCodecContext的结构,此时还没有对应的参数(都是默认参数)
AVCodecContext *pCodecCtx = avcodec_alloc_context3(av_codec);

// 从数据流AVStream中得到的AVCodecParameter中的相关信息复制到AVCodecContext
// 此时AVCodecContext就有了正确的信息了
if(avcodec_parameters_to_context(pCodecCtx,av_codec_parameters) < 0) {
fprintf(stderr, "Couldn't copy codec context");
return -1; // Error copying codec context
}
//初始化并启动解码器
if(avcodec_open2(pCodecCtx, pCodec, NULL)<0){
return -1; // Could not open codec
}

利用AVCodec构建AVCodecContext,然后把AVStream中已知的一些信息复制到AVCodecContext中,接着初始化并开启编解码器。

  • 销毁
1
scss复制代码avcodec_free_context(&pCodecCtx)

AVCodecContext在编解码过程中都会被用到。

AVPacket

读取文件获取AVFormatContext结构体,并且获取了解码器上下文

AVPacket是存储压缩编码数据相关信息的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
arduino复制代码
typedef struct AVPacket {
int64_t pts; // 显示时间戳
int64_t dts; // 解码时间戳
uint8_t *data; // 压缩编码的数据
int size; // data的大小
int stream_index; // 当前packet所属的流(视频流或者音频流等)
...
...
...

AVRational time_base;
}

AVPacket的成员主要包括time_base,pts,dts等一些在解码时可能被用到的参数以及编码数据data。

创建过程

  • 在正常的编解码过程中,AVPacket手动申请内存,则需要手动释放内存,如果自动申请内存则不需要。
  • 从编解码器中接收数据放到packet中,使用完之后,需要释放引用av_packet_unref 即可
1
2
3
4
5
6
7
8
scss复制代码AVPacket pkt; // 自动申请出内存
// 此时只是申请了AVPacket结构体的内存空间,其所指向的数据内存区域还没有创建
AVPacket *pkt2 = av_packet_alloc(); // 如果手动申请内存,泽需要和后续av_packet_free释放
...
// do something with avpacket
...
// 在使用完 pkt 后释放内存
av_packet_free(&pkt2);

使用方式

1
2
3
4
5
6
7
8
9
scss复制代码// 从AVFormatContext中读取数据到avpacket中
// 创建avpacket指向的数据内存区域的函数是av_new_packet
if (av_read_frame(formatContext, &pkt) == 0) {
...
//解码器解码处理 pkt中的数据
...
//在使用完 pkt 后释放引用(引用数到0),从而释放其指向的数据内存区域
av_packet_unref(&pkt);
}

AVFrame

AVFrame结构体一般用于存储原始数据(即非压缩数据,例如对视频来说是YUV,RGB,对音频来说是PCM),除此之外就是数据对应的一些属性:时长,格式等

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
arduino复制代码视频和音频共用一个结构体,因此有的属性是双方公用,有的可能主要用于一方
typedef struct AVFrame {
#define AV_NUM_DATA_POINTERS 8
// *data[]是一个成员为指针的数组
// 原始数据(对视频来说是YUV,RGB,对音频来说是PCM)
uint8_t *data[AV_NUM_DATA_POINTERS];

// data中“一行”数据的大小。注意:未必等于图像的宽,一般大于图像的宽
int linesize[AV_NUM_DATA_POINTERS];

uint8_t **extended_data;

int width, height;

/**
* number of audio samples (per channel) described by this frame
*/
// 音频类型中,AVFrame包含的多少个采样
int nb_samples;


// 音视频的格式,
int format;

//帧类型,I帧,P帧,B帧等
enum AVPictureType pict_type;
...

/**
* Presentation timestamp in time_base units (time when frame should be shown to user).
*/
int64_t pts;

/**
* DTS copied from the AVPacket that triggered returning this frame. (if frame threading isn't used)
* This is also the Presentation time of this AVFrame calculated from
* only AVPacket.dts values without pts values.
*/
int64_t pkt_dts;

AVRational time_base;

int sample_rate;

AVChannelLayout ch_layout;

int64_t duration;
} AVFrame;

data与linesize

关于data和linesize这两个字段,分别表示原始数据存储数组和每一行的大小。但是数据是如何排列的我们并不清楚。

之前讲视频的基础知识时,我们讲到YUV的数据排列有多种方式,因此想要知道data中的YUV数据排列,我们还需要知道AVFrame的format,这个format来自于AVCodecContext->pix_fmt,这个解码器的参数设置成什么,最终解码出来的杨素格式就是什么。假如不指定的话,默认会解码为YUV420p。

我们假设视频数据解码出来的AVFrame,format是YUV420P,那么data和linesize的数据在ffmpeg中的内存示意图可能是这样的:

image.png

类似的音频数据解码出来的AVFrame,format是AV_SAMPLE_FMT_FLTP,双声道,那么对应的数据在ffmpeg中的内存示意图可能是这样的:

image.png

以上都是planar的存储模式,如果是packed(关于planar/packed的解释见文章)存储模式呢?

YUV422 packed存储格式的视频,ffmpeg中的内存示意图大概是这样的:

image.png

当然,其实对于初学者而言,一般不需要直接操作data和linesize,但是能够把ffmpeg中的数据结构和所学的音视频知识做一个对应理解会更深刻。

对于linsize,音频类型。一般只有linesize[0]会被设置;视频则需要看存储方式的不同,常用的planar模式下,linsize数组一般会用到前三个。
对于data指针数组而言,音频数据占用数组几个的指针要看声道数和存储格式(palnar/packed);视频则只看存储格式,planar一般占3个,packed占

使用方式

  • 创建,申请内存空间
1
2
ini复制代码  // 申请内存空间
AvFrame *pFrame = av_frame_alloc();
  • 解码

解码就是AVPakcet=>AVFrame的过程。

1
2
3
4
5
6
7
scss复制代码/********一次循环************/
// 从输入文件的流中读取数据到packet中
av_read_frame(pFormatCtx, &packet)
// 把AVPacket中的数据发送到解码器
avcodec_send_packet(pCodecCtx,&packet);
// 从解码器中读取数据到AVFrame中
avcodec_receive_frame(pCodecCtx,pFrame);
  • 编码

编码则是AVFrame=>AVPacket的过程(解码的逆过程)。

1
2
3
4
5
6
scss复制代码// av_encode_ctx 编码器的上下文
// pFrame 已获得的原始数据帧
int ret = avcodec_send_frame(av_encode_ctx,pFrame); // 把原始数据发送到编码器

// 从编码器中读取编码后的数据到av_out_packet
ret = avcodec_receive_packet(av_encode_ctx,av_out_packet);
  • 销毁
1
2
scss复制代码// 使用完之后
av_frame_free(&pFrame);

手动填充AVFrame->data

ffmpeg中,我们是通过av_frame_alloc函数来获得AVFrame,但是这个函数只是开辟了AVFrame结构体空间,而avframe->data是一个成员为指针的数组,这些成员指针和它们指向的内存空间并未被开辟出来。我们从源码实现中也能看到:

1
2
3
4
5
6
7
8
9
10
11
12
objectivec复制代码AVFrame *av_frame_alloc(void)
{
// 为AVFrame的结构体开辟空间
AVFrame *frame = av_malloc(sizeof(*frame));

if (!frame)
return NULL;
// 未某些成员赋默认值值(不包括data)
get_frame_defaults(frame);

return frame;
}

是因为在编解码过程中编解码器会帮助我们开辟这块空间,所以我们不必管。

但是假如我们在编解码之外使用AVFrame,比如把YUV类型的AVFrame转换为RGB类型的AVFrame,那么AVFrame->data的空间就需要我们自己开辟了,也需要我们进行释放。

  • 填充AVFrame->data

以视频帧为例

1
2
3
4
5
6
7
8
9
10
11
12
13
scss复制代码// Allocate an AVFrame structure
pFrameRGB=av_frame_alloc();

// 通过宽高以及像素格式来计算获得新的帧所需要的缓冲区大小
numBytes= av_image_get_buffer_size(AV_PIX_FMT_RGB24, width,height,1);

// 假设 buffer = 1024byte 表示buffer是指向一个1024个uint_8数据的内存区域的指针
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));

// 让pFrameRGB->data数组中几个指针分别指向buffer这块空间(不同位置),
// 然后可以向这块空间填充数据
av_image_fill_arrays(pFrameRGB->data,pFrameRGB->linesize, buffer,AV_PIX_FMT_RGB24,
pCodecCtx->width, pCodecCtx->height,1);

总结

本文主要详细介绍了ffmpeg中比较重要的几个结构体,他们都伴随着音视频处理的某个阶段而存在的,因此了解他们有助于我们理解音视频的处理流程。

image.png

我们把前面的音视频解码播放流程图添加关键API和搭配关键结构体就会发现ffmpeg的处理流程还是比较简洁的。接下来我们尝试用一个较完整的demo来熟悉ffmpeg的使用方式。

本文转载自: 掘金

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

1…202122…956

开发者博客

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