普通视图

发现新文章,点击刷新页面。
昨天以前首页

VuePress网站接入Google AdSense广告位

作者 千古壹号
2021年6月30日 19:00

前言

如果你的网站每月有一定的访问流量,可以考虑通过广告变现来获取一些收入。在自媒体的所有收入来源中,广告是最可观的收入方式。

博客网站想要接入广告位时,可以优先考虑接入Google AdSense,这是全球最成熟的广告系统。

本文会完整记录Google AdSense的账号注册、广告配置、收入提现等全流程的操作,希望能给有需要的读者一些帮助。

一、注册 Google AdSense 账号

1、注册账号

进入Google AdSense 官网 https://www.google.com/adsense/,点击右上角的”登录“按钮,登录Google 账号:

登录Google账号之后,然后在上图中,点击“开始使用”,出现如下界面:

继续:(国家选“中国”)

填写邮寄地址:

注意,这里的地址,一定要填写准确。因为以后你会收到 Google官方邮寄过来的 Pin码。

上图中,点击提交之后,会弹出如下界面:

2、将广告代码插入到vuepress网站中

安装上面的步骤注册完成后,我的 广告代码是:

1
<script data-ad-client="ca-pub-1601618516206303" async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>

回到 VuePress的项目代码里,配置 docs/.vuepress/config.js文件:

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
head: [
[
"script",
{
"data-ad-client": "ca-pub-1601618516206303",
async: true,
src: "https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"
}
]
]
};

然后把上面的代码部署到服务器。打开网站后,就可以看到广告代码生效了:

我这里配置的是二级域名web.qianguyihao.com。但是稍后Google 官方在审核我的网站的时候,审的是一级域名。所以,我还需要在qianguyihao.com里插入广告代码,这部分的操作过程是类似的,读者可以自行研究下。比如说,假如你的网站是 hexo框架搭建的,那可以研究下 hexo配置是怎么加入广告代码。

3、审核网站

将上面的广告代码插入到博客网站之后,我们再回到 Google Adsense官网的首页:

上图中,勾选“我已将代码粘贴到自己的网站”,然后点击“大功告成”按钮,之后会出现下面的弹窗:

上面的这张图表示:我们的广告账户,已经提交申请了。接下来,我们就可以安安静静地等待审核结果的邮件了。

审核时长一般需要一周左右。运气好的话,三天就能通过;运气不好的话,要等两周左右。

补充说明:只能用一级域名(如 qianguyihao.com)申请广告。申请通过后,广告代码可直接用于到该主域名下的任何子域名下, 而不需要对子域名再次审核。

4、审核通过

审核通过后,我们会收到邮件通知:

访问 Google AdSense 官网,会发现首页已经提示可以投放广告了:

20210626_1530

参考链接

二、 广告配置

配置ads.txt文件

在网站的申请通过之后,需要继续配置ads.txt文件。即:先下载ads.txt文件到本地,之后上传到网站域名的根目录下。

20210626_1531

上图中,点击”设置广告“按钮后,会自动跳到如下页面:

20210626_1558

上图中,点击”立即修正“,会出现如下界面:

20210626_1600

上图中,点击”下载“按钮,然后我们需要把下载下来的ads.txt文件,上传到服务器的指定目录下。

比如说,我的博客网站页面,是托管在阿里云服务器的/home/www/hexo的目录下,那我就把ads.txt文件上传到/home/www/hexo目录下就行了。

上传完成后,输入qianguyihao.com/ads.txt,如果成功加载到内容,说明配置成功。效果如下:

ads.txt 文件生效之后,在 AdSense官网的首页,就可以看到下图所示的界面:

20210627_2219

广告出现的位置

Google 广告分为两种:

  • 自动广告:Google 即会自动在所有最佳位置展示广告。这种广告应用之后,我们的网站会出现铺天盖地的广告,很影响阅读体验。所以本文不打算采用这种广告。
  • 在自定义位置插入广告:可以按照个人需要,在网站的指定位置插入广告。正好是本文想要采用的广告。

自动广告:

20210626_1710

在自定义位置插入广告:

20210626_1711

VuePress 添加 Google AdSense

本段讲述如何为 vuepress 站点配置”按广告单元”申请的 adsense 谷歌广告,从而可以在文章的任意位置插入广告。这里采用的技术方案是 vue-google-adsense 依赖库。具体步骤如下。

1、申请广告代码

进入Google AdSense官网:

20210627_2233

上图中,选择“按广告单元”展示,然后选择其中一个展示方式(这里我选的是左边第一个,红框处所示)。

20210627_2234

上图中,选择自己想要的展示尺寸之后,点击“创建”,出现如下界面:

20210627_2235

上图表示,成功申请到了广告代码:

1
2
3
4
5
6
7
8
9
10
11
<script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<!-- Web项目广告位 -->
<ins class="adsbygoogle"
style="display:block"
data-ad-client="ca-pub-1601618516206303"
data-ad-slot="1053166290"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>

记下上方广告代码中的 data-ad-client 和 data-ad-slot,稍后要用到。

2、安装 vue-google-adsense

(1)在项目的根目录下,执行如下命令,安装 vue-google-adsense:

1
npm i --save vue-script2 vue-google-adsense

(2)在docs/.vuepress目录下,新建文件enhanceApp.js,在这文件中添加如下内容,载入vue-google-adsense:

1
2
3
4
5
6
7
import Vue from 'vue'
import Ads from 'vue-google-adsense'

Vue.use(require('vue-script2'))
Vue.use(Ads.Adsense)
Vue.use(Ads.InArticleAdsense)
Vue.use(Ads.InFeedAdsense)

3、在文章内插入 AdSense 广告

编辑想要插入广告的 markdown 文档,在合适的位置插入如下代码即可:

1
2
3
4
<InArticleAdsense
data-ad-client="ca-pub-XXXXXXXXXXXXXXXX"
data-ad-slot="XXX">
</InArticleAdsense>

上面的代码中,第一行和第四行是固定的。第二行填的是你的ca-pu-id(发布商ID),第三行填的是你的slot_id(客户ID),这两行内容,在我们刚刚申请到的广告代码中可以获取。

配置完成后,稍等大概一个小时,就可以在我们的网站上看到广告投放了:

20210627_2331

上图中,用箭头处围起来的地方,就是我们投放的广告位,可以看到,广告已经成功生效了。我在对应的 markdown 文件中,是这样写的:

20210627_2346

大功告成。

4、自定义广告组件

为了方便文档的书写,以及方便广告的管理,建议将广告代码进一步封装为广告位组件。

我们可以定义多个广告位组件, 每个广告位组件唯一对应 adsense 广告的 1 个 slotId。这样就可以把 data-ad-client 和 data-ad-slot 的取值都封装到组件中。

当站点申请了很多的 slotId 时,通过”自定义广告位组件”的方法实现插入广告, 管理或修改广告数据会非常方便。

具体操作步骤如下。

(1)创建 vue 组件:

创建目录docs/.vuepress/components,在 components 目录下新建文件,比如ArticleTopAd.vue,在里面写入如下内容:

1
2
3
4
5
6
7
<template>
<InArticleAdsense data-ad-client="ca-pub-1601618516206303" data-ad-slot="1053166290"></InArticleAdsense>
</template>

<script>
export default {};
</script>

上面的代码中,需要将 data-ad-client 和 data-ad-slot 的参数值替换为你自己的实际取值。

(2)插入广告:

在md文档的对应广告位处, 注入广告位组件 ArticleTopAd 即可:

1
2
3
4
5
6
7
# js 模板引擎 mustache 用法

<ArticleTopAd></ArticleTopAd>

## 二级标题

### 三级标题

参考链接

设置收款方式

当收益达到100$之后就可以提现了。可以使用招商银行进行电汇收款。也就是说,办一张招行一卡通就可以直接收Google的外汇(美元), 然后一键换汇为人民币。我们来详细看看,具体要怎么操作。

验证身份 & 验证邮寄地址

大概过了一个月之后,我的 Google AdSense收入达到了10美元,所以首页出现了这么个提示,让我验证地址:

20211029_1621

上图中,点击「操作」,出现了如下界面:

20211029_1627

上图中,可以看出:

(1)当您的收入达到进行验证所需的最低限额时,Google官方会将个人识别码(PIN 码)邮寄到您的付款地址。

(2)自 PIN 码生成之日算起,您可以在 4 个月内将该码输入帐号。如果您在 4 个月后还未输入该码,我们会停止在您的页面上展示广告。

(3)如果幸运的话可能会收到pin码,当然也可能收不到。

详情可查看如下链接:

上面这个链接里的内容,我截图存了个档:

20211029_1637_2

付款最低限额

关于付款最低限额相关的信息,详情可以看这个链接:

也就是说,收入达到10美元之后,需要我们验证邮寄地址;收入达到100美元之后,就可以开始提现了。指日可待。

上面这个链接里的内容,我也截图存档:

20211029_1638_2

参考链接

参考链接

VuePress+阿里云搭建在线知识库

作者 千古壹号
2021年6月16日 19:00

前言

开发时间

  • 2021-06-16:折腾一下午,终于完成初步目标。输入域名,可以打开我的 vuepress项目了。
  • 2021-06-16:晚上,接入了 Google Adsense 广告位。
  • 2021-09-17:折腾一晚上,完成了CDN加速。

开发环境

  • Node版本:v12.18.4
  • 服务器:阿里云
  • CDN加速:七牛云
  • 图床:七牛云
  • 项目的效果展示:web.qianguyihao.com

实现效果

  • 自定义域名
  • 知识库支持:左侧目录的导航+右侧标题的导航。
  • 所有文件和静态资源托管在个人的私有服务器
  • 自动化部署
  • 首屏渲染完成时间控制在2秒以内。
  • 支持CDN缓存,支持PWA本地缓存。
  • 接入 Google AdSense广告。

前端:该选哪一个知识库平台

知识库(非技术平台)

  • notion

颜值高,功能强大。但国内网络不太好,且有一定的学习成本。如果你能高效上网,notion是不错的选择。
·

  • 语雀

阿里的蚂蚁金服团队出品,几乎没有学习成本。如果想搭建一个公开的知识库,但又不想折腾技术,语雀是首选。

知识库(技术平台)

在技术领域,现在流行的知识库平台主要是这四个:(颜值都不错)

  • GitBook

GitBook分为两种:一种是开源的 GitBook,另一种是 GitBook.com。开源的GitBook 自从2018年之后,官方就不再维护了。所以最佳选择是 GitBook.com。

优点:GitBook.com几乎没有操作成本,直接导入 md 文件,或者导入 GitHub项目,就能生成知识库,而且可以绑定自定义域名。在所有技术类知识库平台中,GitBook.com的的颜值是最为美观的(至少我认为如此),目录结构的样式深得我心。

缺点:无法自定义配置(只能按官方的模板来)、国内的访问速度很慢(因为页面只能托管在 GitBook.com,不能托管在私有服务器)、无法接入广告位。

操作成本很低,配置也简单,支持接入广告位。但对 SEO 不友好。总的来说,适合做轻量级的知识库。

  • hugo-book

属于 hugo博客的一种主题,样式美观。但 hugo-book 才刚推出没多久,生态不成熟,要踩的坑也不少。

  • VuePress

尤雨溪大大推出的平台。

优点:支持自定义的配置非常多,甚至支持插入自定义的 JS 代码。渲染性能好、SEO友好、支持接入广告位。

缺点:官方文档写的很烂。上手门槛较高,需要自己花很多时间折腾。

总结:

没有最好的知识库平台,只有最合适的。

最终,我选定了 VuePress 作为我的前端教程的知识库平台。因为 VuePress 具有非常好的加载性能和搜索引擎优化(SEO),也支持接入广告位,满足我的需求。

VuePress官网对几个主流的平台也做了对比:

一、本地安装 VuePress 环境

1、安装 git 环境

2、安装Node.js环境

3、安装 VuePress

安装 VuePress:

1
npm install -g vuepress

4、初始化项目

(1)新建文件夹blog

1
2
mkdir blog
cd blog

(2)初始化项目:

1
npm init -y

初始化完成后,blog文件夹内会自动生成一个package.json文件,默认的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name": "blog",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

在package.json中,新增如下内容,配置启动命令:

1
2
3
4
"scripts": {
"docs:dev": "vuepress dev docs",
"docs:build": "vuepress build docs"
}

上方内容的意思是:

  • 启动项目: npm run docs:dev,这条命令就等于vuepress dev docs

  • 打包项目: npm run build 这条命令就等于 vuepress build docs

(3)新增.gitignore文件,将默认的临时目录和缓存目录添加到这个文件中:

1
2
3
node_modules
.temp
.cache

5、创建第一篇文档

(1)新建docs文件夹,这里面可以存放我们写的.md类型的文章以及.vuepress相关的配置:

1
mkdir docs

(2)创建第一篇.md格式的文档:

1
2
3
4
cd docs

# 这行命令的意思是,新建文件 readme.md,并写入内容 ## Hello VuePress
echo '## Hello VuePress' > README.md

6、在本地预览项目

输入如下命令,在浏览器预览项目:

1
2
3
4
$ npm run docs:dev

success [20:34:58] Build 5dfce5 finished in 4843 ms!
VuePress dev server listening at http://localhost:8080/

在浏览器输入 http://localhost:8080 ,就能看到 VuePress 的默认主题下的主页了:

7、打包项目

运行npm run docs:build将项目打包,打包文件会在docs/.vuepress/dist目录下自动生成。稍后,我们把这个目录下的文件,部署到服务器端,然后配置 nginx代理,就可以在网上上正常访问了。

二、页面配置

基本配置

(1)在docs文件夹中创建.vuepress文件夹:

1
mkdir .vuepress

这个文件存放的是vuepress相关的配置

.vuepress 目录下,新建一个总的配置文件config.js, 这个文件的名字是固定的:

1
2
cd .vuepress
touch config.js

config.js中最基础的配置文件内容如下:

1
2
3
4
module.exports = {
title: '千古前端图文教程',
description: '从零开始学前端,超详细的前端入门到进阶学习笔记。',
}

设置封面页

我们可以在 之前新建的 readme.md文件中,设置封面页。 官方也给我们提供了封面页的模板,比较实用的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
---
home: true
heroImage: https://vuepress.vuejs.org/hero.png
heroText: VuePress
tagline: Vue 驱动的静态网站生成器
actionText: 快速上手 →
actionLink: /zh/guide/
features:
- title: 简洁至上
details: 以 Markdown 为中心的项目结构,以最少的配置帮助你专注于写作。
- title: Vue驱动
details: 享受 Vue + webpack 的开发体验,在 Markdown 中使用 Vue 组件,同时可以使用 Vue 来开发自定义主题。
- title: 高性能
details: VuePress 为每个页面预渲染生成静态的 HTML,同时在页面被加载的时候,将作为 SPA 运行。
footer: MIT Licensed | Copyright © 2018-present Evan You
---

效果图如下:

支持PWA

vuepress还有一个我比较看重的优势, 就是支持PWA, 当用户没有网的情况下,一样能继续的访问我们的网站。

0.x 版本中我们只要配置serviceWorker: true 即可, 但是我们现在使用的是1.2.0版本, 这个版本中已经将这个功能抽离出来作为插件的方式使用, 下面就看一下具体如何使用的:

首先需要安装插件:

1
2
yarn add -D @vuepress/plugin-pwa
# 或者 npm install -D @vuepress/plugin-pwa

config.js中配置:

1
2
3
4
5
6
module.exports = {
plugins: ['@vuepress/pwa', {
serviceWorker: true,
updatePopup: true
}]
}

注意,为了让你的网站完全地兼容 PWA,你需要:

  • 在 .vuepress/public 提供 Manifest 和 icons
  • .vuepress/config.js 添加正确的 head links
1
2
3
4
5
6
7
8
// 配置
module.exports = {
head: [
['link', { rel: 'icon', href: `/favicon.ico` }],
//增加manifest.json
['link', { rel: 'manifest', href: '/manifest.json' }],
],
}

manifest.json 文件

1
2
3
4
5
6
7
{
"name": "qianguyihao_blog",
"short_name": "blog",
"version": "1.0.0",
"description": "qianguyihao的博客",
"manifest_version": 2
}

三、服务器端配置

配置nginx代理

首先,要确保你满足下面几个条件:

  • 你有一台服务器
  • 已经安装好nginx
  • 有一个已备案的域名

通过ssh工具远程连接服务器端,然后开始配置 nginx代理。

(1)为 vuepress 创建一个部署目录 /home/www/vuepress

1
mkdir -p /home/www/vuepress

(2)进入 /usr/local/nginx/conf 目录,并对 nginx.conf 配置文件进行相关配置:

1
2
3
cd /usr/local/nginx/conf
ls
vim nginx.conf

打开nginx.conf文件后,按 i 键由命令模式切换到编辑模式,修改三个地方:

  • 首先将最顶端的用户改为 root。
  • 其次,将 server_name 改为自己的域名。如果没有备案,可以先填写自己的公网 IP(在阿里云控制台的 ECS 实例中查看),访问时暂时用公网 IP 进行访问。
  • 最后,将location中的 root 项中的值改为 /home/www/vuepress;。如果 server 中的端口号不是 80,则改为 80

修改结束之后,先按 Esc 由编辑模式切换到命令模式,再输入 :wq 命令保存并退出编辑器。

我们需要在 nginx.conf 中 添加下面的配置:

1
2
3
4
5
6
7
8
9
server {
listen 80;
server_name web.qianguyihao.com;
location / {
root /home/www/vuepress;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
}

(3)上传静态资源文件:

将静态资源文件放置到服务器上,路径为配置的 /home/www/vuepress, 可以借助xftp工具上传也可以通过git克隆, 选择适合自己的方式就可以。

稍后,我们将介绍如何进行自动化部署(即自动化上传文件),这种方式最科学,最高效。

(4)修改nginx后,重启nginx:

1
2
3
cd /usr/local/nginx/sbin
ls
./nginx -s reload

参考链接:

新建远程git仓库,为自动化部署做准备

为了使我们能够在本地向服务器实现自动部署,需要在服务器端新建一个 Git 用户。然后使用公钥连接成功之后,就可以方便地随时进行自动部署了。

具体操作,可以看我写的另外一篇blog:hexo+阿里云搭建博客网站

现在,我们开始在服务器端配置 Git 仓库。

(1)在服务器端使用 Git 用户 创建 git 仓库:

1
2
3
su git
cd ~
sudo git init --bare vuepress.git

(2)接着上一步,准备配置hooks(钩子)。hooks的作用是:当代码在本地执行 git push后,服务端会自动执行一些操作。

命令如下:

1
2
3
4
5
6
7
8
9
cd /home/git/vuepress.git/hooks
# 通过copy 新建post-update 文件
sudo cp post-update.sample post-update

# 修改文件权限
sudo chmod +x ~/vuepress.git/hooks/post-update

# 编辑 post-update 文件
sudo vim post-update

更改 post-update 文件为如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
#exec git update-server-info
echo "Im update"
# 代码仓库目录
GIT_REPO=/home/git/vuepress.git
# 临时目录
TMP_GIT_CLONE=/home/tmp/vuepress
# nginx的root目录(存放编译打包后的资源文件)
PUBLIC_WWW=/home/www/vuepress

rm -rf ${TMP_GIT_CLONE}
git clone $GIT_REPO $TMP_GIT_CLONE
rm -rf ${PUBLIC_WWW}/*
mv -t ${PUBLIC_WWW} ${TMP_GIT_CLONE}/*

上方配置中,GIT_REPO就是我们服务器端git仓库的地址,TMP_GIT_CLONE就是临时存放上传的资源的路径,PUBLIC_WWW是项目最后存放的地方,对于这个博客来说,这个PUBLIC_WWW就是之前Nginx配置的root。

这个脚本的含义就是,当我们在本地进行提交的时候,服务器接受后,会将其复制到临时存放目录,然后转移到项目路径下,从而使得我们:只需要在本地把生成的项目push到远程服务器,服务器就可以自动帮我们部署到对应的文件夹啦。

(3)创建临时目录:(这里其实是以root身份创建的)

1
2
sudo mkdir /home/tmp
sudo mkdir /home/tmp/vuepress

让git用户拥有这个目录的操作权限:

1
2
3
sudo chown git:git -R /home/tmp

sudo chown git:git -R /home/www/vuepress

(4)重启 ECS 实例。

文件的权限问题,遇到的坑

后来在本地上传文件到服务器的时候,每次都提示权限不足。原因是:我们用到的诸如/home/git/vuepress.git之类的文件,都是以root身份创建的,其所有者为root用户,所以git用户没有权限进行读写操作,因此我们可以使用如下命令,让git用户拥有这个目录/文件的权限:

1
sudo chown git:git -R xxx //xxx为对应的文件或目录

发布到服务器端

回到本地的vuepress项目,在根目录下,执行如下命令将项目的静态文件发布到服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
# 编译生成静态文件
npm run build

# 进入生成的文件夹
cd docs/.vuepress/dist

git init
git add -A
git commit -m 'deploy'

# 以git用户的身份,发布到阿里云服务器。这里的xxx是服务器ip
git push -f git@xxx.xxx.xxx.xxx:/home/git/vuepress.git master

补充:当你执行上方的最后一行 git push命令时,如果提示下面这个错误,那可以确定,就是权限的问题:

1
2
3
4
5
6
7
8
# case1
remote: fatal: 不能为 '/home/tmp/vuepress' 创建先导目录: 权限不够
remote: cp: 无法获取'/home/tmp/vuepress' 的文件状态(stat): 没有那个文件或目录
To xxx.xxx.xxx.xxx:/home/git/vuepress.git

# case2
remote: rm: 无法删除'/home/www/vuepress': 权限不够
remote: cp: 无法创建目录 '/home/www/vuepress/vuepress': 权限不够

如果每次发布时,都要执行上面的命令,那就太麻烦了。所以,我们可以把这些命令,放到 deploy.sh下,实现自动化部署。具体做法如下。

自动化发布到服务器

在本地项目的根目录,创建deploy.sh文件来运行自动部署命令,文件里的内容如下:

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
#!/usr/bin/env sh

# 确保脚本抛出遇到的错误
set -e

# 生成静态文件
npm run build

# 进入生成的文件夹
cd docs/.vuepress/dist

git init
git add -A
git commit -m 'deploy'

# 如果发布到 https://<USERNAME>.github.io USERNAME=你的用户名
# git push -f git@github.com:<USERNAME>/<USERNAME>.github.io.git master

# 如果发布到 https://<USERNAME>.github.io/<REPO> REPO=github上的项目
# git push -f git@github.com:qianguyihao/web.git master

# 以git用户的身份,发布到阿里云服务器
git push -f git@47.112.XXX.XXX:/home/git/vuepress.git master

cd -

上面的内容配置完成后,以后,我们只要输入输入npm run deploy,并输入服务器的密码,即可一键将本地的vuepress文件部署到服务器端。

到此,我们就完成了服务端的配置。

参考链接

把上面的三个链接,结合起来看,最终实现了我的自动化部署的目标。

二级域名的 DNS 解析

在此之前,需要先将域名进行备案。域名备案成功之后,我们就有能力使用域名登陆自己的博客了。

我的项目是二级域名 web.qianguyihao.com,所以dns解析的配置是这样的:

上面的红框部分,就是我这次要加的dns配置。其他的配置,是我以前在搭建 qianguyihao.com 博客的时候做的配置。

访问页面

上面的内容配置完成后,我们就可以通过http://web.qianguyihao.com访问前端应用了:

注意,修改完nginx,然后重启nginx之后,如果网站打不开,可能是浏览器缓存的问题,建议重新开一个无痕模式的浏览器窗口;也可能是 https 的问题,因为暂时还没有开启 https,所以只能通过 http 来访问,访问https是打不开的。我就是在这个地方,卡了很久。

给二级域名安装 https证书

在阿里云的搜索框里搜“ssl证书”,然后进入管理控制台,申请免费证书:

上图中,点击“证书申请”之后,弹出如下内容:

上图中,填写自己的二级域名,然后点击下一步:

上图中,点击验证,然后提交审核。

审核通过后,就可以下载证书,并配置 nginx了,这部分的操作流程,可以参照我之前写的博客:hexo+阿里云搭建博客网站

二级域名开启 https 的nginx配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 443 ssl;
server_name web.qianguyihao.com;

ssl_certificate /usr/local/nginx/cert/5808232_web.qianguyihao.com.pem;
ssl_certificate_key /usr/local/nginx/cert/5808232_web.qianguyihao.com.key;

ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;

ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

location / {
root /home/www/vuepress;
index index.html index.htm;
}
}

证书安装完成后,重启 nginx,然后就可以通过 https://web.qianguyihao.com 来访问了:

三、自定义主题:vuepress-theme-reco

我用的是 vuepress-theme-reco 主题。

主题安装和使用

安装:

1
npm install vuepress-theme-reco --save-dev

引用:

1
2
3
4
5
// .vuepress/config.js

module.exports = {
theme: 'reco'
}

自动生成 sidebar

在vuepress配置sidebar时,每篇文章都要配置对应的位置。正常情况下咱们会这样配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// .vuepress/config.js
module.exports = {
themeConfig: {
sidebar: [
{
title: 'vue', // 必要的
collapsable: false, // 是否展开分组 可选的, 默认值是 true,
sidebarDepth: 2, // 可选的, 默认值是 1
children: [
'document/vue/','document/vue/vue1.md','document/vue/vue2.md'
]
},
{
title: 'js',
children: [ /* ... */ ]
}
]
}
}

但是显而易见,当我们日后文章数量增加,又或者我们需要更改名称,这时候就又得找到位置更改名称。相当的麻烦。

我们可以写一段代码,对 sidebar 进行自动配置。做法如下。

首先,我们先整合下目录,根据不同文章分类进行分组,如下:

1
2
3
4
5
6
7
8
9
10
.
├─document/
│ ├─ vue/
│ │ ├─ README.md
│ │ ├─ vue1.md
│ │ └─ vue2.md
│ └─ js/
│ ├─ README.md
│ ├─ js1.md
│ └─ js2.md

接着我们在.vuepress创建两个文件 一个是sidebarConf.js,用来生成对应的侧边栏列表 另一个是getDocPath.js文件,用来获取所有的文章名。

1
2
3
4
5
6
.
├─ docs
│ └─.vuepress
│ ├─config.js
│ ├─sidebarConf.js
│ └─getDocPath.js

(1)获取文件名:

getDocPath.js 获取一个目录下的所有文件名。注意需要排除.DS_Store文件。

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
/**
* 获取目录下的所有文件的相对路径
* 解决路由名称枚举问题
*/
const fs = require('fs')
const path = require('path')
function getDocPath(title,collapsable,relateivePath) {
const absolutePath = path.join(__dirname, '../' + relateivePath)
const files = fs.readdirSync(absolutePath)
const components = []
// 排除检查的文件
var excludes = ['.DS_Store']
let arr = files.sort(function(a, b) {
// 截取'.'之前的数字进行排序 例如 1.vue 2.vue 3.vue
return a.split('.')[0] - b.split('.')[0];
});
arr.forEach(function (item) {
if (excludes.indexOf(item) < 0) {
let stat = fs.lstatSync(absolutePath + '/' + item)
if (item == 'README.md') {
components.unshift(relateivePath + '/')
} else if (!stat.isDirectory()) {
components.push(relateivePath + '/' + item)
} else {
console.log(relateivePath + '/' + item)
getDocPath(relateivePath + '/' + item)
}
}
})
let frame = {
title:title,
collapsable:collapsable,
children:components
}
return frame
}
module.exports = getDocPath

(2)配置侧边栏:

sidebarConf.js 调用getDocPath()方法,组成侧边栏的数据列表,对应文章开头的原始配置格式。

1
2
3
4
5
const getDocPath = require('./getDocPath')
module.exports = [
getDocPath('vue',true,'document/vue'),
getDocPath('js',true,'document/js')
];

(3)挂载进config:

1
2
3
themeConfig: {
sidebar: require('./sidebarConf'),
}

至此完整的功能已全部写完, 如果此配置还满足不了你的需求,想配置成多个侧边栏,在每个不同的分类生成对应的自己想要的侧边栏。

生成的效果如下:

本段的参考链接:

我也尝试了按照下面两个链接里的方法,自动生成sidebar,但是并没有生效:

设置侧边栏的标题

不需要在文件中写一级标题,我们可以在Front Matter中设置tittle。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# XXXX.md
---
title: Kaldi声纹识别代码详解|egs/aishell
categories:
- 声纹识别
tags:
- Kaldi
publish: true
---

::: tip
尝试添加摘要
:::

<!-- more -->

## 正文的一级标题
(正文)XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

参考链接:

参考链接

bug修复

1、标题无法选中的问题:

临时的解决办法:打开node_modules/vuepress-theme-reco/styles/theme.styl文件,然后将h2的样式加上z-index -1,将h3的样式加上z-index -2

参考案例

vovo-docs

网上的很多案例,讲的都是搭建博客,却没有讲搭建知识库、文档、wiki。我想要的效果是:左边显示多个文档的结构和目录导航,右侧显示单篇文章的目录导航。下面这几个案例,就很不错。

我搜遍了整个 google 和github才找到的。我发现,如果要找源码的话,搜 github 比搜google高效多了。

全网就这一个案例,提供了完整的demo和源码。我fork了一下。

很美观,很规范。

冴羽的TS教程

上面这篇文章,作者@冴羽也是用的vuepress-theme-reco主题,操作步骤写的比较详细。缺点是,没有讲如何自动生成侧边栏(左侧)目录。

项目效果如下:https://ts.yayujs.com/

Apifox的官方文档

做的很漂亮,也是用的vuepress-theme-reco主题,可惜没有开源。

好看的主题推荐

网站性能优化:开启gzip压缩

要知道,网站的打开速度取决于浏览器打开下载的网页文件大小。如果传输的页面内容文件减少,那你网站的打开速度一定会加快。特别是手机端的用户,打开网站速度受限于移动端网络,所以压缩网站页面内容显得至关重要。

在 nginx配置中开启 gzip压缩之后,可以将网页文件至少压缩50%,极大的提高网页的打开速度和网站性能。具体做法如下。

进入服务器的 /usr/local/nginx/conf 目录,并对 nginx.conf 文件进行相关配置。找到#gzip on;这行配置(这行配置是在http层的),并在其下方加入如下内容:

1
2
3
4
5
6
7
8
9
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_min_length 256;
gzip_types application/atom+xml application/geo+json application/javascript application/x-javascript application/json application/ld+json application/manifest+json application/rdf+xml application/rss+xml application/xhtml+xml application/xml font/eot font/otf font/ttf image/svg+xml text/css text/javascript text/plain text/xml;

然后重启nginx。

压缩前:

压缩后:

除此之外,我们可以使用站长工具,打开「网页GZIP压缩检测」,查看检测结果:

上图中可以看到, gzip的缩率高达78%,压缩效果显著。

参考链接:

网站性能优化:CDN加速

CDN 加速就是把原服务器上数据复制到其他的诸多服务器上,用户访问时,哪台服务器近就访问那台服务器上的数据。CDN 加速优点是成本低,速度快。适合访问量比较大的网站。而且,如果你的博客所在的主机是限制流量、限制带宽的,那么,一个很好的办法就是把图片、js文件等静态文件部署到其他服务器(即cdn所在的服务器),这样就可以极大地减少主机流量消耗,并提升网站的访问性能。

科普概念的参考链接:

下面来讲一下我的 web.qianguyihao.com 网站是怎么进行CDN加速的。我是把 网站的 js 静态文件部署到了七牛云上。cdn文件的域名采用的是 web.smyhvae.com

七牛云 cdn 优化步骤

在这之前,请先确保你的七牛云账号里,已经新建好了一个自己的「对象存储空间」。

(1)打开七牛云,新建域名:

新建完成后,等待几分钟,就审核通过了:

(2)配置DNS域名解析:

(3)给七牛云上的 web.smyhvae.com 申请 https证书。配置方法详见我的另外一篇文章:

(4)把 vuepress项目编译后的文件(即docs/.vuepress/dist/assets目录下的所有文件) ,挨个上传到七牛云。

文件列表如下:

上传截图如下:

如上图所示,上传的时候,注意路径的前缀。比如, assets/css目录下的文件,前缀需要设置为assets/css/

优化成果

(1)开启 gzip压缩之后:页面首次渲染完成时间,从55秒变成了10秒。

(2)开启CDN加速之后,页面首次渲染完成时间,从10秒变成了2秒以内。

(3)再加上 VuePress 框架本身支持 pwa本地缓存,二次访问速度贼快。

修改静态资源的访问路径

做了cdn加速之后,所有的静态资源文件,url链接会从 https://web.qianguyihao.com/assets/js/app.53a9121a.js 这样的格式变为https://web.smyhvae.com/assets/js/app.53a9121a.js这样的格式。这里,我们把前者称之为链接1,把后者称之为链接2。

当我们输入 网址 web.qianguyihao.com时,要怎么确保网站加载的是 链接2的资源而非链接1的资源呢?这就要我们继续修改 vuepress 项目的配置。继续往下看。

一开始,我想的是修改 vuepress项目配置文件的base 参数:

如上图所示,当我尝试把 base参数的值改为https://web.smyhvae.com/之后,网站首页的效果符合预期,但点击其他tab之后,效果竟然是这样的:

如上图所示,页面的url里,竟然多了个 https。把这个页面刷新之后,就提示“打不开了”:

那要怎么办呢?可以这样做:

(1)首先,base参数不用改,继续保持 \即可:

(2)其次,打开 vuepress项目编译后的目录web-vuepress/docs/.vuepress/dist,然后手动里面的所有的引用链接,改动内容如下:

改动前:

1
2
3
4
5
href="/assets/

src="/assets/

"url": "assets/

改动后:

1
2
3
4
5
href="https://web.smyhvae.com/assets/

src="https://web.smyhvae.com/assets/

"url": "https://web.smyhvae.com/assets/

差不多有5000个地方要改。改完之后的效果如下:

(3)将改完之后的内容进行发布:

  • web-vuepress/docs/.vuepress/dist/assets目录发布到七牛云的cdn加速空间。
  • web-vuepress/docs/.vuepress/dist目录发布到阿里云服务器。

发布完成后,就达到了我们的预期效果:

参考链接

使用的是vuepress框架。

vuepress接入广告、统计,写得很清楚。

如果只搭建一个页面,可以参考这个。

hexo 博客的常见配置

作者 千古壹号
2020年9月21日 11:53

hexo常见命令

新建文章草稿

Hexo 提供了 draft 机制,草稿里将建立在 source/_drafts 目录下。当执行 hexo generate 时,并不会将其编译到 public 目录下,所以 hexo deploy 发布之后,草稿不会显示在页面中。

新建草稿:(草稿不会显示在页面中)

1
hexo new draft <title>

本地预览草稿:

1
$ hexo S --draft

Hexo server 提供了 --draft 参数,搭配 hexo-browsersync 这个插件,就可以一边编辑 markdown 文章,一边使用浏览器预览。

新建一篇文章

新建的文章,会自动存放在 source/_posts目录下。

新建文章:

1
hexo new  "my-article"

本地预览:

1
hexo serve

新建文件夹blog,然后初始化项目:

1
2
3
cd blog
hexo init
npm install

Hexo 自动部署和发布

我们可以在本地新建一个 xxx.md 文件放在 blog\source\_posts 目录中。然后在本地的blog目录下,执行如下命令,就可以将文章发布到服务器端了:

方式1:

1
2
3
hexo clean
hexo generate
hexo deploy

方式2:

1
2
3
hexo cl
hexo g
hexo d

方式3:

1
hexo cl && hexo g && hexo d

hexo 文章格式

文章格式是选填的,不是必须的;但最好加上尽可能多的文章格式,让文章的信息更完整。

hexo 文章的简略格式

1
2
3
4
5
6
7
8
9
---
title: 我是文章标题
date: 2020-09-19 11:30:30
author: qianguyihao
categories: 我是分类
tags:
- 标签1
- 标签2
---

hexo 文章的较完整格式

1
2
3
4
5
6
7
8
9
10
---
title: 我是文章标题
date: 2020-09-19 11:30:30
author: qianguyihao
urlname:
categories: 我是分类
tags:
- 标签1
- 标签2
---

hexo 文章模板的自定义

每次使用 hexo new "my-article"新建一篇文章时,默认只有title、date、tags这几个属性。

我们可以修改scaffolds/post.md文件,自定义文章格式的模板,我修改后的内容如下:

1
2
3
4
5
6
7
8
9
---
title: {{ title }}
date: {{ date }}
update: {{ date }}
author:
urlname:
categories:
tags:
---

hexo 文章的全部属性

参考链接:https://www.dazhuanlan.com/2019/11/30/5de154d0810af/

自定义文章的url地址

(1)修改 hexo/_config.yml 文件:

1
2
#permalink: :year/:month/:day/:title/
permalink: :urlname/

(2)然后,我们就可以单独在具体某篇文章里的头部,通过 urlname字段 自定义这篇文章的url了:

1
2
3
4
5
---
title: 我是文章标题
date: 2020-09-21 11:53:36
urlname: xxx-url
---

参考链接:

给hexo博客生成RSS订阅

(1)在 hexo 项目根目录下执行如下命令:

1
npm install hexo-generator-feed --save

(2)在 hexo 根目录下的 _config.yml 文件中添加如下配置:

1
2
3
4
5
#订阅RSS
feed:
type: atom
path: atom.xml
limit: false

(3)在 theme 目录下的 _config.yml 文件中添加如下配置:

1
rss: /atom.xml

添加上面这行之后,就可以确保在网站的菜单栏展示出“RSS”这几个字(也就是说,露出了RSS订阅的入口)。

当然,你也可以把RSS订阅的入口放在“社交图标”的位置。不同的主题,配置方式不同。比如,就拿hexo-theme-melody主题来说,它的配置方式很简单,在melody主题的 _config.yml 文件中配置如下内容:

1
2
social:
rss fa: https://qianguyihao.com/atom.xml

(4)重新编译,生成博客的静态文件:

1
hexo clean && hexo g

此时,在 public 文件夹中会自动生成一个 atom.xml 文件。

这个atom.xml 就是的 RSS 订阅文件了,以后只需要访问 qianguyihao.com/atom.xml 就可以实现 RSS 订阅了。

参考链接:

hexo-theme-melody 主题配置

官方文档:https://molunerfinn.com/hexo-theme-melody-doc/zh-Hans/

图片无法自适应的问题

解决办法:https://github.com/Molunerfinn/hexo-theme-melody/issues/285

接入 Google Adsense

配置 melody.yml文件:

1
2
3
4
5
google_adsense:
enable: true
js: //pagead2.googlesyndication.com/pagead/js/adsbygoogle.js
client: ca-pub-1601618516206303
enable_page_level_ads: true

常见问题积累

nginx 重启失败

修改nginx后,重启nginx:

1
2
3
4
5
6
7
8
9
# 修改nginx
cd /usr/local/nginx/conf
ls
vim nginx.conf

# 重启nginx
cd /usr/local/nginx/sbin
ls
./nginx -s reload

结果提示下面这个错误:

1
2
[root@iZw9 sbin]# ./nginx -s reload
nginx: [error] open() "/usr/local/nginx/logs/nginx.pid" failed (2: No such file or directory)

进入到logs目录发现确实没有nginx.pid文件。

解决办法:

使用指定nginx.conf文件的方式重启nginx:

1
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf

此时去logs目录下查看发现nginx.pid文件已经生成了,而且也完成了自动重启的事情。

hexo+阿里云搭建博客网站

作者 千古壹号
2020年9月19日 19:00

2020-09-19折腾时间:

  • 15:15~18:00
  • 20:45~22:15
  • 23:50~00:20

经过5个小时的折腾(含笔记整理),输入公网ip之后,终于可以打开自己的博客网站了。

一、本地安装Hexo环境

1、安装 git 环境

2、安装Node.js环境

3、安装 Hexo,并初始化项目

安装 hexo:

1
npm install -g hexo-cli

新建文件夹blog,然后初始化项目:

1
2
3
cd blog
hexo init
npm install

初始化完成后,blog文件夹内包括如下内容:

在blog文件夹内安装插件:

1
2
npm install hexo-deployer-git --save
npm install hexo-server

在blog文件夹内,配置git提交的账号邮箱:

1
2
git config user.email "youremail@mail.com"
git config user.name "yourname"

4、新建一篇文章

新建文章:

1
hexo new  "my-article"

新建的文章,会自动存放在 source/_posts目录下。

然后,我们可以开始在 source/_posts/my-article.md 文件里,写 markdown 格式的文章了。

5、在本地预览项目

输入如下命令,在浏览器预览项目:

1
2
3
4
5
6
7
$ hexo server
或者
$ hexo s

INFO Validating config
INFO Start processing
INFO Hexo is running at http://localhost:4000 . Press Ctrl+C to stop.

在浏览器输入 http://localhost:4000 ,就能看到 Hexo 的默认主题下的主页了:

至此我们就完成了在本地的配置工作。

二、域名注册、服务器购买

1、域名注册

2、购买阿里云服务器 ECS

进入阿里云主页 https://www.aliyun.com/,点击“云服务器ECS ”进行购买:

购买服务器ECS时,可以选择如下配置:

  • 地域:选择离经常访问你网站的用户近一些的地域
  • 内存:1G
  • 云盘:40G
  • 网络:专有网络
  • 公网IP:包含
  • 带宽:1Mbps

按照上面的配置,2020-09-19这天的价格如下:

  • 一年:700
  • 三年:1600

我选择了三年的。

3、域名备案

域名备案时,需要先准备一个ECS服务器,我们可以直接用上面购买的服务器。

备案时间较长,请耐心等待。

三、阿里云ECS配置

重置实例密码

由于 ECS 服务器对 root 用户没有设置初始密码,因此我们需要对 root 密码进行重置:

温馨提示:记得妥善保管自己的 root 用户密码哦。并且在搭建的过程中如遇到不可挽回的局面可以考虑重置 ECS 实例,相当于重装系统。操作如下:

设置安全组

阿里云的服务器默认不开放端口号,这样使得我们在网站部署完成之后仍然无法访问。

有一个基本原因是没有开启端口号,因此我们需要新建安全组并添加 80 端口,再将安全组添加到 ECS 实例中。具体操作如下。

在控制台的 ECS 实例中点击「网络与安全–>安全组–>创建安全组–>快速添加」。在访问规则的入方向添加如下几个端口(尤其是80端口):

然后回到 ECS 服务器实例,将刚刚配置的安全组加入到实例中:

备注:安全组的出方向不用配置,默认对外都是放行的。

四、服务器端配置

此步骤是博客搭建过程中最容易出错的地方,提出以下几点建议:

  • 为了避免出错,推荐直接复制粘贴命令行代码。
  • 分清是在本地计算机上操作,还是连接服务器在服务器上操作。
  • 分清在服务器上使用 Git 用户还是使用 root 用户进行操作。

本地通过 ssh 连接服务器

我用的是Mac电脑,推荐使用 Royal TSX 软件进行ssh连接。

参考链接:在Mac上使用Royal TSX,替代 xshell 和 item2、SecureCRT,可以 SSH 也能 FTP

(如果你用的是 Windows电脑,推荐使用Xshell软件进行ssh连接。)

安装 nginx

参考链接:centos8平台编译安装nginx1.18.0

我们使用 nginx 作为 web 服务器和反向代理工具。

(1)安装 nginx 依赖环境(安装期间有提示一律选 yes):

1
2
3
4
yum install gcc-c++
yum install -y pcre pcre-devel
yum install -y zlib zlib-devel
yum install -y openssl openssl-devel

(2)下载 nginx 安装包:

1
wget -c https://nginx.org/download/nginx-1.18.0.tar.gz

将安装包解压到 /usr/local 目录下:

1
tar -zxvf nginx-1.18.0.tar.gz -C /usr/local

(3)进入 /usr/local 目录,确认 nginx 解压到该目录下:

1
cd /usr/local

进入 nginx-1.18.0 目录,会发现该目录下有一个 configure 文件,执行该配置文件:

1
2
3
cd nginx-1.18.0/
ls
./configure --prefix=/usr/local/soft/nginx --with-http_stub_status_module --with-http_ssl_module

解释:

  • –prefix 指定安装路径

  • –with-http_stub_status_module 允许查看nginx状态的模块

  • –with-http_ssl_module 支持https的模块

编译并安装 nginx:

1
2
make
make install

(4)查找nginx安装目录:

1
2
$ whereis nginx
/usr/local/nginx

进入安装目录:

1
2
3
4
$ cd /usr/local/nginx
$ ls
# 有下面这几个文件
# conf html logs sbin

由于 nginx 默认通过 80 端口访问,而 Linux 默认情况下不会开发该端口号,因此需要开放 linux 的 80 端口供外部访问:

1
/sbin/iptables -I INPUT -p tcp --dport 80 -j ACCEPT

(9)进入 nginx安装目录的sbin 目录,启动 nginx:

1
2
3
cd /usr/local/nginx
cd sbin
./nginx

没有任何消息,代表启动成功。此时输入公网 IP 即可进入 nginx 的欢迎页面了:

备注:注意要保证 nginx 服务处于 运行状态 才可以访问博客网站。nginx 相关命令如下:(在 cd /usr/local/nginx/sbin 目录下执行)

1
2
3
4
5
6
7
8
9
10
cd /usr/local/nginx/sbin

# 停止 nginx 服务
./nginx -s stop

# 启动 nginx 服务
./nginx

# 重启 nginx 服务
./nginx -s reload

配置 nginx 路由

(1)为 hexo 创建一个部署目录 /home/www/hexo

1
mkdir -p /home/www/hexo

(2)进入 /usr/local/nginx/conf 目录,并对 nginx.conf 配置文件进行相关配置:

1
2
3
cd /usr/local/nginx/conf
ls
vim nginx.conf

打开nginx.conf文件后,按 i 键由命令模式切换到编辑模式,修改三个地方:

  • 首先将最顶端的用户改为 root。
  • 其次,将 server_name 改为自己的域名。如果没有备案,可以先填写自己的公网 IP(在阿里云控制台的 ECS 实例中查看),访问时暂时用公网 IP 进行访问。
  • 最后,将location中的 root 项中的值改为 /home/www/hexo;。如果 server 中的端口号不是 80,则改为 80

修改结束之后,先按 Esc 由编辑模式切换到命令模式,再输入 :wq 命令保存并退出编辑器。

nginx.conf 修改前:

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
#user  nobody;
worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;


events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

#access_log logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

server {
listen 80;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root html;
index index.html index.htm;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}


# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;

# location / {
# root html;
# index index.html index.htm;
# }
#}


# HTTPS server
#
#server {
# listen 443 ssl;
# server_name localhost;

# ssl_certificate cert.pem;
# ssl_certificate_key cert.key;

# ssl_session_cache shared:SSL:1m;
# ssl_session_timeout 5m;

# ssl_ciphers HIGH:!aNULL:!MD5;
# ssl_prefer_server_ciphers on;

# location / {
# root html;
# index index.html index.htm;
# }
#}

}

修改后:

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
user  root;
worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;


events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

#access_log logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

server {
listen 80;
server_name www.qianguyihao.com;
return 301 https://www.qianguyihao.com$request_uri;

#charset koi8-r;

#access_log logs/host.access.log main;

location / {
root /home/www/hexo;
index index.html index.htm;
}

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}

# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}

# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}


# another virtual host using mix of IP-, name-, and port-based configuration
#
#server {
# listen 8000;
# listen somename:8080;
# server_name somename alias another.alias;

# location / {
# root html;
# index index.html index.htm;
# }
#}


# 开启 HTTPS
#
server {
listen 443 ssl;
server_name www.qianguyihao.com qianguyihao.com;

ssl_certificate /usr/local/nginx/cert/4523958_www.qianguyihao.com.pem;
ssl_certificate_key /usr/local/nginx/cert/4523958_www.qianguyihao.com.key;

ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;

ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

location / {
root /home/www/hexo;
index index.html index.htm;
}
}

}

需要修改的位置如下:

修改nginx后,重启nginx:

1
2
3
cd /usr/local/nginx/sbin
ls
./nginx -s reload

安装 node.js

安装 node.js的命令如下:

1
2
3
cd ~
curl -sL https://rpm.nodesource.com/setup_10.x | bash -
yum install -y nodejs

过查看版本号验证是否安装成功:

1
2
node -v
npm -v

效果如下:

在服务器端创建 Git 用户

为了使我们能够在本地向服务器实现自动部署,需要在服务器端另外新建一个 Git 用户。然后使用公钥连接成功之后,就可以方便地随时进行自动部署了。

执行如下命令,在阿里云安装git环境:(有提示时,选择 yes 即可)

1
yum install git

安装结束之后,通过查看版本号验证是否安装成功:

1
2
$ git --version
git version 2.18.4

创建 Git 用户:

1
adduser git

修改 Git 用户权限为 740:

1
chmod 740 /etc/sudoers

在配置文件中增加 Git 用户。首先打开文件:

1
vim /etc/sudoers

进入文件后,后按 i 键由命令模式切换到编辑模式。如下图所示,在 root 下添加一行 Git 信息:

修改结束后,先按 Esc 由编辑模式切换到命令模式,再输入:wq 命令保存并退出编辑器。

将 Git 用户的权限改回去:

1
chmod 400 /etc/sudoers

设置 Git 用户密码:

1
2
3
4
5
$ sudo passwd git
更改用户 git 的密码 。
新的 密码:
重新输入新的 密码:
passwd:所有的身份验证令牌已经成功更新。

以上我们就完成了 Git 用户的创建,接下来我们向 Git 用户添加公钥,就像配置 Github 那样。

给服务器端的 Git 用户配置 ssh 公钥

这样做的目的就是,以后由本地向服务器提交资源,就不需要再进行身份验证了。

流程大致如下:

  • 先在本地的C:\Users\用户名.ssh目录生成公钥id_rsa.pub和私钥id_rsa
  • 然后使用 FTP 上传工具,将公钥文件id_rsa.pub上传到服务器端的 .ssh 文件夹;
  • 最后将公钥文件id_rsa.pub内容拷贝到 authorized_keys 文件中。

温馨提示:使用 ctrl + c 复制命令,然后在终端点击右键就可以直接粘贴上去了,避免手动输入的麻烦。

如果你之前配置过公钥到 Github、Gitlab 等仓库,那你直接使用之前的公钥即可。

另外,我们要注意在本地操作还是在服务器端操作;在服务器端的时候,是使用 root 用户还是使用 git 用户操作。

(1)在服务器端切换到 git 用户,在根目录下创建 .ssh文件夹:

1
2
3
su git
cd ~
mkdir .ssh

此时,命令行信息中的 # 变成了 $,且 root 变成了 git,表示我们切换成功。

(2)在本地生成生成公钥、私钥:(注意是在本地)

1
2
3
4
5
$ cd ~
$ cd .ssh
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/smyhvae/.ssh/id_rsa):

上面的命令中,如果有询问,直接回车即可。结束之后,会在 C:\Users\用户名\.ssh 里生成两个文件:公钥文件 id_rsa.pub、私钥文件id_rsa

注意,.ssh 为隐藏文件夹,你可能需要显示隐藏文件夹之后才可以看到。

(3)在本地终端输入以下命令,为私钥设置权限:

1
2
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_rsa

(4)使用ftp工具,将本地的 id_rsa.pub 文件上传到服务器端的/home/git/.ssh目录下:

在使用ftp工具登录远程服务器时,登录项如下:

  • ip:公网ip。
  • 端口:22
  • 协议:sftp
  • 用户名:git
  • 密码:xx

注意,此时服务器端 .ssh 文件夹里还没有 authorized_keys 文件,只有 id_rsa.pub 这一个文件。

(5)回到 服务器端,以 Git 用户身份在 .ssh 文件夹内新建 authorized_keys 文件,并将公钥内容拷贝到该文件中:

1
2
3
cd ~/.ssh
cp id_rsa.pub authorized_keys
cat id_rsa.pub >> ~/.ssh/authorized_keys

修改文件权限:

1
2
chmod 600 ~/.ssh/authorized_keys
chmod 700 ~/.ssh

(6) 确保设置了正确的SELinux上下文:

1
restorecon -Rv ~/.ssh

现在我们来验证一下,在本地输入如下命令,是否能正常连接到远程服务器:(不用输入密码,就能直接连上)

1
ssh -v git@xxx.xxx.xxx.xxx(你的公网 IP)

如果显示欢迎界面,表示本地连接远程服务器的git用户时,连接成功:

但是我们一般不使用 Git 用户进行服务端操作,而是使用 root 用户。Git 用户只是作为自动部署特意新建的。

比如说,在本地输入如下命令,可以连接远程服务器的root用户:(需要输入密码)

1
ssh -v root@xxx.xxx.xxx.xxx(你的公网 IP)

在服务端配置 Git 仓库

(1)在服务器端使用 Git 用户 创建 git 仓库,并新建 post-receive 钩子文件:

1
2
3
4
5
6
7
8
9
su git
cd ~
sudo git init --bare hexo.git

# 新建文件
touch ~/hexo.git/hooks/post-receive

# 修改文件权限
chmod +x ~/hexo.git/hooks/post-receive

备注:注意ls、touch、cat、vi/vim的区别。

输入如下命令编辑文件:

1
vim ~/hexo.git/hooks/post-receive

进入文件后,后按 i 键由命令模式切换到编辑模式。输入以下命令:

1
git --work-tree=/home/www/hexo --git-dir=/home/git/hexo.git checkout -f

修改完成后,先按 Esc 由编辑模式切换到命令模式,再输入 :wq 命令保存并退出编辑器。

(2)在服务器端使用root用户,修改文件权限:

1
2
3
su root
cd ~
sudo chmod -R 777 /home/www/hexo

(3)重启 ECS 实例。

到此,我们就完成了服务端的配置。

五、hexo本地部署和发布

本地 Hexo 配置连接和域名

主要是修改 _config.yml 文件。进入本地计算机 blog 文件夹的根目录,找到 _config.yml 文件并打开。

(1)把 deploy 参数改成如下方式:(注意,xxx 的地方是填写你自己的公网 IP )

1
2
3
4
deploy:
type: git
repo: git@xxx.xxx.114.110:/home/git/hexo.git
branch: master

(2)URL 配置项需要改为自己的域名:(如果没有备案,则可以先填写公网 IP)

1
url: http://www.qianguyihao.com

Hexo 自动部署和发布

我们可以在本地新建一个 xxx.md 文件放在 blog\source\_posts 目录中。然后在本地的blog目录下,执行如下命令,就可以将文章发布到服务器端了:

方式1:

1
2
3
hexo clean
hexo generate
hexo deploy

方式2:

1
2
3
hexo cl
hexo g
hexo d

方式3:

1
hexo cl && hexo g && hexo d

此时,在浏览器中输入自己的公网 IP,你就可以打开你的博客网站了。惊不惊喜?意不意外?

备注:在浏览器中输入自己的公网 IP之后,如果网站打不开,请记得检查之前的步骤是否正确,尤其是检查一下 nginx 服务是否已经启动。

域名DNS解析

当我们的域名备案成功之后,我们就有能力使用域名登陆自己的博客了。在此之前,需要在阿里云 控制台-域名 中设置域名解析。

点击“解析”:

在DNS解析设置里同时添加两条指向公网ip的主机记录:一条@记录,一条www记录。如下:

此时,在浏览器输入www.qianguyihao.com或者qianguyihao.com,就可以打开我的博客网站了。它们都是基于http协议的,等同于http://www.qianguyihao.com

将网站域名支持https

在上面的步骤中,网站域名只支持了 http,还没有支持https。所以,当我输入https://www.qianguyihao.com时,网站是不打开的。

那要怎么让网站域名支持 https呢?我们可以为域名添加免费证书,添加证书后,网站将变成安全的 https。

整体流程如下:(图1)

整体流程如下:(图2)

具体配置步骤如下。

购买免费证书

在阿里云主页搜索 SSL证书,然后点击“立即购买”:

按照下图所示的选项进行选择,可以看到证书是免费的:

按照步骤的流程点击之后,域名解析里会自动多出下面这一条解析:

下载证书

解析完成后,马上会收到两条短信:

短信1:

1
【阿里云】尊敬的smy****@163.com:您为域名www.qianguyihao.com购买的SSL证书已签发成功,现可前往 SSL证书控制台 下载并安装至Web服务器或一键部署到阿里云云产品,详情可参考https://c.tb.cn/I3.ZW3uZ,如需人工帮助请拨打95187-2。

短信2:

1
【阿里云】尊敬的smy****@163.com:您的云盾证书服务实例:cas-cn-xx 开通成功。请登录云盾证书服务控制台查看及管理。

备注:阿里云控制台网址:https://console.aliyun.com

由于我们的 web 服务器是 ngxin,因此下载时选择 nginx:

在服务器端安装证书

(1)按照上面的步骤下载完成后,会得到一个xxx.zip压缩包,将压缩包解压后,会看到两个文件:452xxx_www.qianguyihao.com.pem452xxx_www.qianguyihao.com.key

(2)连接服务器,以 root 用户进入 nginx 配置页面:

1
cd /usr/local/nginx/

创建 cert 文件夹用来存放证书:

1
2
mkdir cert
ls

然后使用ftp工具将刚才的两个证书文件上传到 cert 文件夹。

修改 nginx 配置文件:

1
2
cd /usr/local/nginx/conf
vim nginx.conf

i 进入编辑模式,拉到最下方,开放 443 端口,并填写ssl证书的文件名:

修改结束后,先按 Esc 退出编辑模式,然后输入 :wq 保存并退出。

修改前:

# HTTPS server##server {#    listen       443 ssl;#    server_name  localhost;#    ssl_certificate      cert.pem;#    ssl_certificate_key  cert.key;#    ssl_session_cache    shared:SSL:1m;#    ssl_session_timeout  5m;#    ssl_ciphers  HIGH:!aNULL:!MD5;#    ssl_prefer_server_ciphers  on;#    location / {#        root   html;#        index  index.html index.htm;#    }

修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 443 ssl;
server_name www.qianguyihao.com;

ssl_certificate /usr/local/nginx/cert/452xxx_www.qianguyihao.com.pem;
ssl_certificate_key /usr/local/nginx/cert/452_www.qianguyihao.com.key;

ssl_session_cache shared:SSL:1m;
ssl_session_timeout 5m;

ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;

location / {
root /home/www/hexo;
index index.html index.htm;
}
}

(3)修改nginx后,重启nginx:

1
2
3
cd /usr/local/nginx/sbin
ls
./nginx -s reload

http 访问时,自动重定向到 https

继续修改 nginx 文件,修改原有端口 80 的监听,加一行配置:

1
2
3
4
server {
listen 80;
return 301 https://www.qianguyihao.com$request_uri;
}

修改之后,当用户使用 http 协议访问网站时,会自动进行 301 跳转,以 https 协议访问网站。

修改nginx后,重启nginx:

1
2
3
cd /usr/local/nginx/sbin
ls
./nginx -s reload

购买和配置付费证书

免费证书只有一年期限。一年到期后,不能再免费了:

image-20210927165112443

所以,接下来,只能购买和配置付费证书了。

(1)我按照下图的配置,选了一个最便宜的证书:

image-20210927165152188

image-20210927165221923

image-20210927165318768

(2)进入证书管理页,配置证书:

image-20210927170021805

上图中,点击“证书申请”,然后输入域名,然后点击“确定”,会进入下方的页面:

image-20210927170128710

上图中,点击“证书申请”,进入下方的页面:

image-20210927170216847

上图中,点击“下一步”,进入下方页面:

image-20210927170506086

上图中,按照步骤操作。先去域名控制台配置dns,然后回到当前页面点击“验证”,最后点击“提交审核”。

dns解析的配置信息如下:

image-20210927170716824

点击“提交审核”之后,会弹窗:

image-20210927170758859

Hexo 主题自定义

我用的是hexo-theme-melody 主题,官方文档上有详细的配置指南。

遇到的问题

Console expects a writable stream instance

问题描述:执行hexo init时报错Console expects a writable stream instance

解决办法:是Node.js版本的问题,v8.xx的版本太低了,建议升级到V10.x.x以上(我升级到了V12.18.4版本)。

参考链接:hexo与github结合

ssh连接中断的问题

Mac 连接服务器保持ssh会话:https://mp.weixin.qq.com/s/ylMPjyVnmptcEq12Gi-Nzg

想要通过 xxx.com 和 www.xxx.com 都能正常访问,怎么做

问:想要通过 xxx.com 和 www.xxx.com 都能正常访问博客网站,应该怎么做?

解决办法如下:

需要在dns域名解析中,同时添加两条指向公网ip的主机记录:一条@记录,一条www记录。如下:

添加完这两条记录后,通过 xxx.comwww.xxx.com,都可以访问你的服务器。

在支持https的情况下,如果你只添加了www记录,那么,只能通过以下网址访问:

1
2
3
www.qianguyihao.com
https://www.qianguyihao.com
http://www.qianguyihao.com

如果继续添加了@记录,还可以通过一下网址访问:

1
2
3
https://qianguyihao.com
http://qianguyihao.com
qianguyihao.com

补充:为了达到上面这个目标,nginx配置中,server_name只需要设置www.qianguyihao.com即可,不需要设置www.qianguyihao.com qianguyihao.com。不要多此一举。

参考链接:

参考链接

提到了如何将md文件进行管理。

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

作者 Gracker
2023年5月14日 20:15

前一段时间有个 App 很火,是 Android App 利用了 Android 系统漏洞,获得了系统权限,做了很多事情。想看看这些个 App 在利用系统漏洞获取系统权限之后,都干了什么事,于是就有了这篇文章。由于准备仓促,有些 Code 没有仔细看,感兴趣的同学可以自己去研究研究,多多讨论,对应的文章和 Code 链接都在下面:

  1. 深蓝洞察:2022 年度最 “不可赦” 漏洞
  2. XXX apk 内嵌提权代码,及动态下发 dex 分析
  3. Android 反序列化漏洞攻防史话

关于这个 App 是如何获取这个系统权限的,Android 反序列化漏洞攻防史话,这篇文章讲的很清楚,就不再赘述了,我也不是安全方面的专家,但是建议大家多读几遍这篇文章

序列化和反序列化是指将内存数据结构转换为字节流,通过网络传输或者保存到磁盘,然后再将字节流恢复为内存对象的过程。在 Web 安全领域,出现过很多反序列化漏洞,比如 PHP 反序列化、Java 反序列化等。由于在反序列化的过程中触发了非预期的程序逻辑,从而被攻击者用精心构造的字节流触发并利用漏洞从而最终实现任意代码执行等目的。

这篇文章主要来看看 XXX apk 内嵌提权代码,及动态下发 dex 分析 这个库里面提供的 Dex ,看看 App 到底想知道用户的什么信息?总的来说,App 获取系统权限之后,主要做了下面几件事(正常 App 无法或者很难做到的事情),各种不把用户当人了。

  1. 自启动、关联启动相关的修改,偷偷打开或者默认打开:与手机厂商斗智斗勇。
  2. 开启通知权限。
  3. 监听通知内容。
  4. 获取用户的使用手机的信息,包括安装的 App、使用时长、用户 ID、用户名等。
  5. 修改系统设置。
  6. 整一些系统权限的工具方便自己使用。

另外也可以看到,这个 App 对于各个手机厂商的研究还是比较深入的,针对华为、Oppo、Vivo、Xiaomi 等终端厂商都有专门的处理,这个也是值得手机厂商去反向研究和防御的。

最好我还加上了这篇文章在微信公众号发出去之后的用户评论,以及知乎回答的评论区(问题已经被删了,但是我可以看到:如何评价拼多多疑似利用漏洞攻击用户手机,窃取竞争对手软件数据,防止自己被卸载? - Gracker的回答 - 知乎 https://www.zhihu.com/question/587624599/answer/2927765317,目前为止是 2471 个赞)可以说是脑洞大开(关于 App 如何作恶)。

0. Dex 文件信息

本文所研究的 dex 文件是从 XXX apk 内嵌提权代码,及动态下发 dex 分析 这个仓库获取的,Dex 文件总共有 37 个,不多,也不大,慢慢看。这些文件会通过后台服务器动态下发,然后在 App 启动的时候进行动态加载,可以说是隐蔽的很,然而 Android 毕竟是开源软件,要抓你个 App 的行为还是很简单的,这些 Dex 就是被抓包抓出来的,可以说是人脏货俱全了。

由于是 dex 文件,所以直接使用 https://github.com/tp7309/TTDeDroid 这个库的反编译工具打开看即可,比如我配置好之后,直接使用 showjar 这个命令就可以

showjar 95cd95ab4d694ad8bdf49f07e3599fb3.dex

默认是用 jadx 打开,就可以看到反编译之后的内容,我们重点看 Executor 里面的代码逻辑即可

打开后可以看到具体的功能逻辑,可以看到一个 dex 一般只干一件事,那我们重点看这件事的核心实现部分即可

1. 通知监听和通知权限相关

1.1 获取 Xiaomi 手机通知内容

  1. 文件 : 95cd95ab4d694ad8bdf49f07e3599fb3.dex
  2. 功能 :获取用户的 Active 通知
  3. 类名 :com.google.android.sd.biz_dynamic_dex.xm_ntf_info.XMGetNtfInfoExecutor

1. 反射拿到 ServiceManager

一般我们会通过 ServiceManager 的 getService 方法获取系统的 Service,然后进行远程调用

2. 通过 NotificationManagerService 获取通知的详细内容

通过 getService 传入 NotificationManagerService 获取 NotificationManager 之后,就可以调用 getActiveNotifications 这个方法了,然后具体拿到 Notification 的下面几个字段

  1. 通知的 Title
  2. 发生通知的 App 的包名
  3. 通知发送时间
  4. key
  5. channelID :the id of the channel this notification posts to.

可能有人不知道这玩意是啥,下面这个图里面就是一个典型的通知

其代码如下

可以看到 getActiveNotifications 这个方法,是 System-only 的,普通的 App 是不能随便读取 Notification 的,但是这个 App 由于有权限,就可以获取

当然微信的防撤回插件使用的一般是另外一种方法,比如辅助服务,这玩意是合规的,但是还是推荐大家能不用就不用,它能帮你防撤回,他就能获取通知的内容,包括你知道的和不知道的

1.2. 打开 Xiaomi 手机上的通知权限(Push)

  1. 文件 :0fc0e98ac2e54bc29401efaddfc8ad7f.dex
  2. 功能 :可能有的时候小米用户会把 App 的通知给关掉,App 想知道这个用户是不是把通知关了,如果关了就偷偷打开
  3. 类名 :com.google.android.sd.biz_dynamic_dex.xm_permission.XMPermissionExecutor

这么看来这个应该还是蛮实用的,你个调皮的用户,我发通知都是为了你好,你怎么忍心把我关掉呢?让我帮你偷偷打开吧

App 调用 NotificationManagerService 的 setNotificationsEnabledForPackage 来设置通知,可以强制打开通知
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

然后查看 NotificationManagerService 的 setNotificationsEnabledForPackage 这个方法,就是查看用户是不是打开成功了
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

还有针对 leb 的单独处理~ 细 !

1.3. 打开 Vivo 机器上的通知权限(Push)

  1. 文件 :2eb20dc580aaa5186ee4a4ceb2374669.dex
  2. 功能 :Vivo 用户会把 App 的通知给关掉,这样在 Vivo 手机上 App 就收不到通知了,那不行,得偷偷打开
  3. 类名 :com.google.android.sd.biz_dynamic_dex.vivo_open_push.VivoOpenPushExecutor

核心和上面那个是一样的,只不过这个是专门针对 vivo 手机的

1.4 打开 Oppo 手机的通知权限

  1. 文件 :67c9e686004f45158e94002e8e781192.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.oppo_notification_ut.OppoNotificationUTExecutor

没有反编译出来,看大概的逻辑应该是打开 App 在 oppo 手机上的通知权限

1.5 Notification 监听

  1. 文件 :ab8ed4c3482c42a1b8baef558ee79deb.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.ud_notification_listener.UdNotificationListenerExecutor

这个就有点厉害了,在监听 App 的 Notification 的发送,然后进行统计

监听的核心代码

这个咱也不是很懂,是时候跟做了多年 SystemUI 和 Launcher 的老婆求助了....@史工

1.6 App Notification 监听

  1. 文件 :4f260398-e9d1-4390-bbb9-eeb49c07bf3c.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.notification_listener.NotificationListenerExecutor

上面那个是 UdNotificationListenerExecutor , 这个是 NotificationListenerExecutor,UD 是啥?

这个反射调用的 setNotificationListenerAccessGranted 是个 SystemAPI,获得通知的使用权,果然有权限就可以为所欲为

1.7 打开华为手机的通知监听权限

  1. 文件 :a3937709-b9cc-48fd-8918-163c9cb7c2df.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.hw_notification_listener.HWNotificationListenerExecutor

华为也无法幸免,哈哈哈

1.8 打开华为手机通知权限

  1. 文件 :257682c986ab449ab9e7c8ae7682fa61.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.hw_permission.HwPermissionExecutor

2. Backup 状态

2.1. 鸿蒙 OS 上 App Backup 状态相关,保活用?

  1. 文件 :6932a923-9f13-4624-bfea-1249ddfd5505.dex
  2. 功能 :Backup 相关

这个看了半天,应该是专门针对华为手机的,收到 IBackupSessionCallback 回调后,执行 PackageManagerEx.startBackupSession 方法

查了下这个方法的作用,启动备份或恢复会话

2.2. Vivo 手机 Backup 状态相关

  1. 文件 :8c34f5dc-f04c-40ba-98d4-7aa7c364b65c.dex
  2. 功能 :Backup 相关

3. 文件相关

3.1 获取华为手机 SLog 和 SharedPreferences 内容

  1. 文件 : da03be2689cc463f901806b5b417c9f5.dex
  2. 类名 :com.google.android.sd.biz_dynamic_dex.hw_get_input.HwGetInputExecutor

拿这个干嘛呢?拿去做数据分析?

获取 SharedPreferences

获取 slog

4. 用户数据

4.1 获取用户使用手机的数据

  1. 文件 : 35604479f8854b5d90bc800e912034fc.dex
  2. 功能 :看名字就知道是获取用户的使用手机的数据
  3. 类名 :com.google.android.sd.biz_dynamic_dex.usage_event_all.UsageEventAllExecutor

看核心逻辑是同 usagestates 服务,来获取用户使用手机的数据,难怪我手机安装了什么 App、用了多久这些,其他 App 了如指掌

那么他可以拿到哪些数据呢?应有尽有~,包括但不限于 App 启动、退出、挂起、Service 变化、Configuration 变化、亮灭屏、开关机等,感兴趣的可以看一下:

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
frameworks/base/core/java/android/app/usage/UsageEvents.java
private static String eventToString(int eventType) {
switch (eventType) {
case Event.NONE:
return "NONE";
case Event.ACTIVITY_PAUSED:
return "ACTIVITY_PAUSED";
case Event.ACTIVITY_RESUMED:
return "ACTIVITY_RESUMED";
case Event.FOREGROUND_SERVICE_START:
return "FOREGROUND_SERVICE_START";
case Event.FOREGROUND_SERVICE_STOP:
return "FOREGROUND_SERVICE_STOP";
case Event.ACTIVITY_STOPPED:
return "ACTIVITY_STOPPED";
case Event.END_OF_DAY:
return "END_OF_DAY";
case Event.ROLLOVER_FOREGROUND_SERVICE:
return "ROLLOVER_FOREGROUND_SERVICE";
case Event.CONTINUE_PREVIOUS_DAY:
return "CONTINUE_PREVIOUS_DAY";
case Event.CONTINUING_FOREGROUND_SERVICE:
return "CONTINUING_FOREGROUND_SERVICE";
case Event.CONFIGURATION_CHANGE:
return "CONFIGURATION_CHANGE";
case Event.SYSTEM_INTERACTION:
return "SYSTEM_INTERACTION";
case Event.USER_INTERACTION:
return "USER_INTERACTION";
case Event.SHORTCUT_INVOCATION:
return "SHORTCUT_INVOCATION";
case Event.CHOOSER_ACTION:
return "CHOOSER_ACTION";
case Event.NOTIFICATION_SEEN:
return "NOTIFICATION_SEEN";
case Event.STANDBY_BUCKET_CHANGED:
return "STANDBY_BUCKET_CHANGED";
case Event.NOTIFICATION_INTERRUPTION:
return "NOTIFICATION_INTERRUPTION";
case Event.SLICE_PINNED:
return "SLICE_PINNED";
case Event.SLICE_PINNED_PRIV:
return "SLICE_PINNED_PRIV";
case Event.SCREEN_INTERACTIVE:
return "SCREEN_INTERACTIVE";
case Event.SCREEN_NON_INTERACTIVE:
return "SCREEN_NON_INTERACTIVE";
case Event.KEYGUARD_SHOWN:
return "KEYGUARD_SHOWN";
case Event.KEYGUARD_HIDDEN:
return "KEYGUARD_HIDDEN";
case Event.DEVICE_SHUTDOWN:
return "DEVICE_SHUTDOWN";
case Event.DEVICE_STARTUP:
return "DEVICE_STARTUP";
case Event.USER_UNLOCKED:
return "USER_UNLOCKED";
case Event.USER_STOPPED:
return "USER_STOPPED";
case Event.LOCUS_ID_SET:
return "LOCUS_ID_SET";
case Event.APP_COMPONENT_USED:
return "APP_COMPONENT_USED";
default:
return "UNKNOWN_TYPE_" + eventType;
}
}

4.2 获取用户使用数据

  1. 文件:b50477f70bd14479a50e6fa34e18b2a0.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.usage_event.UsageEventExecutor

上面那个是 UsageEventAllExecutor,这个是 UsageEventExecutor,主要拿用户使用 App 相关的数据,比如什么时候打开某个 App、什么时候关闭某个 App,6 得很,真毒瘤

4.3 获取用户使用数据

  1. 文件:1a68d982e02fc22b464693a06f528fac.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.app_usage_observer.AppUsageObserver

看样子是注册了 App Usage 的权限,具体 Code 没有出来,不好分析

5. Widget 和 icon 相关

经吃瓜群众提醒,App 可以通过 Widget 伪造一个 icon,用户在长按图标卸载这个 App 的时候,你以为卸载了,其实是把他伪造的这个 Widget 给删除了,真正的 App 还在 (不过我没有遇到过,这么搞真的是脑洞大开,且不把 Android 用户当人)

5.1. Vivo 手机添加 Widget

  1. 文件:f9b6b139-4516-4ac2-896d-8bc3eb1f2d03.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_widget.VivoAddWidgetExecutor

这个比较好理解,在 Vivo 手机上加个 Widget

5.2 获取 icon 相关的信息

  1. 文件:da60112a4b2848adba2ac11f412cccc7.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.get_icon_info.GetIconInfoExecutor

这个好理解,获取 icon 相关的信息,比如在 Launcher 的哪一行,哪一列,是否在文件夹里面。问题是获取这玩意干嘛???迷

5.3 Oppo 手机添加 Widget

  1. 文件:75dcc8ea-d0f9-4222-b8dd-2a83444f9cd6.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.oppoaddwidget.OppoAddWidgetExecutor

5.4 Xiaomi 手机更新图标?

  1. 文件:5d372522-b6a4-4c1b-a0b4-8114d342e6c0.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.xm_akasha.XmAkashaExecutor

小米手机上的桌面 icon 、shorcut 相关的操作,小米的同学来认领

6. 自启动、关联启动、保活相关

6.1 打开 Oppo 手机自启动

  1. 文件:e723d560-c2ee-461e-b2a1-96f85b614f2b.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.oppo_boot_perm.OppoBootPermExecutor

看下面这一堆就知道是和自启动相关的,看来自启动权限是每个 App 都蛋疼的东西啊

6.2 打开 Vivo 关联启动权限

  1. 文件:8b56d820-cac2-4ca0-8a3a-1083c5cca7ae.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_association_start.VivoAssociationStartExecutor

看名字就是和关联启动相关的权限,vivo 的同学来领了

直接写了个节点进去

6.3 关闭华为耗电精灵

  1. 文件:7c6e6702-e461-4315-8631-eee246aeba95.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.hw_hide_power_window.HidePowerWindowExecutor

看名字和实现,应该是和华为的耗电精灵有关系,华为的同学可以来看看

6.4 Vivo 机型保活相关

  1. 文件:7877ec6850344e7aad5fdd57f6abf238.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_get_loc.VivoGetLocExecutor

猜测和保活相关,Vivo 的同学可以来认领一下

7. 安装卸载相关

7.1 Vivo 手机回滚卸载

  1. 文件:d643e0f9a68342bc8403a69e7ee877a7.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_rollback_uninstall.VivoRollbackUninstallExecutor

这个看上去像是用户卸载 App 之后,回滚到预置的版本,好吧,这个是常规操作

7.2 Vivo 手机 App 卸载

  1. 文件:be7a2b643d7e8543f49994ffeb0ee0b6.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_official_uninstall.OfficialUntiUninstallV3

看名字和实现,也是和卸载回滚相关的

7.3 Vivo 手机 App 卸载相关

  1. 文件:183bb87aa7d744a195741ce524577dd0.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_official_uninstall.VivoOfficialUninstallExecutor

同上

其他

SyncExecutor

  1. 文件:f4247da0-6274-44eb-859a-b4c35ec0dd71.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.sync.SyncExecutor

没看懂是干嘛的,核心应该是 Utils.updateSid ,但是没看到实现的地方

UdParseNotifyMessageExecutor

  1. 文件:f35735a5cbf445c785237797138d246a.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.ud_parse_nmessage.UdParseNotifyMessageExecutor

看名字应该是解析从远端传来的 Notify Message,具体功能未知

6.3 TDLogcatExecutor

  1. 文件
    1. 8aeb045fad9343acbbd1a26998b6485a.dex
    2. 2aa151e2cfa04acb8fb96e523807ca6b.dex
  2. 类名
    1. com.google.android.sd.biz_dynamic_dex.td.logcat.TDLogcatExecutor
    2. com.google.android.sd.biz_dynamic_dex.td.logcat.TDLogcatExecutor

没太看懂这个是干嘛的,像是保活又不像,后面有时间了再慢慢分析

6.4 QueryLBSInfoExecutor

  1. 文件:74168acd-14b4-4ff8-842e-f92b794d7abf.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.query_lbs_info.QueryLBSInfoExecutor

获取 LBS Info

6.5 WriteSettingsExecutor

  1. 文件:6afc90e406bf46e4a29956aabcdfe004.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.write_settings.WriteSettingsExecutor

看名字应该是个工具类,写 Settings 字段的,至于些什么应该是动态下发的

6.6 OppoSettingExecutor

  1. 文件:61517b68-7c09-4021-9aaa-cdebeb9549f2.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.opposettingproxy.OppoSettingExecutor

Setting 代理??没看懂干嘛的,Oppo 的同学来认领,难道是另外一种形式的保活?

6.7 CheckAsterExecutor

  1. 文件:561341f5f7976e13efce7491887f1306.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.check_aster.CheckAsterExecutor

Check aster ?不是很懂

6.8 OppoCommunityIdExecutor

  1. 文件:538278f3-9f68-4fce-be10-12635b9640b2.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.oppo_community_id.OppoCommunityIdExecutor

获取 Oppo 用户的 ID?要这玩意干么?

6.9 GetSettingsUsernameExecutor

  1. 文件:4569a29c-b5a8-4dcf-a3a6-0a2f0bfdd493.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.oppo_get_settings_username.GetSettingsUsernameExecutor

获取 Oppo 手机用户的 username,话说你要这个啥用咧?

6.10 LogcatExecutor

  1. 文件:218a37ea-710d-49cb-b872-2a47a1115c69.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.logcat.LogcatExecutor

配置 Log 的参数

6.11 VivoBrowserSettingsExecutor

  1. 文件:136d4651-df47-41b4-bb80-2ec0ab1bc775.dex
  2. 类名:com.google.android.sd.biz_dynamic_dex.vivo_browser_settings.VivoBrowserSettingsExecutor

Vivo 浏览器相关的设置,不太懂要干嘛

评论区比文章更精彩

微信公众号评论区

image-20230514203931411

image-20230514203940833

image-20230514203951666

image-20230514204055973

image-20230514204002395

image-20230514204022808

image-20230514204042836

image-20230514204123412

image-20230514204200492

知乎评论区

知乎回答已经被删了,我通过主页可以看到,但是点进去是已经被删了:如何评价拼多多疑似利用漏洞攻击用户手机,窃取竞争对手软件数据,防止自己被卸载? - Gracker的回答 - 知乎 https://www.zhihu.com/question/587624599/answer/2927765317

image-20230514205638861

image-20230514205909534

image-20230514205857945

image-20230514205937705

image-20230514205947268

image-20230514210010062

image-20230514210020926

image-20230514210040479

image-20230514210107839

image-20230514210122906

image-20230514210141653

image-20230514210152755

image-20230514210226176

image-20230514210235233

image-20230514210255912

image-20230514210344475

iOS 和 Android 哪个更安全?

这里就贴一下安全大佬 sunwear 的评论

img

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

作者 Gracker
2022年3月13日 08:38

本文是 Systrace 线程 CPU 运行状态分析技巧系列的第三篇,本文主要讲了使用 Systrace 分析 CPU 状态时遇到的 SleepUninterruptible Sleep 状态的原因排查方法与优化方法,这两个状态导致性能变差概率非常高,而且排查起来也比较费劲,网上也没有系统化的文档。

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。Systrace 基础和实战系列大家可以在 Systrace 基础知识 - Systrace 预备知识 或者 博客文章目录 这里看到完整的目录

  1. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  2. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  3. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

Linux 中的 Sleep 状态是什么

TASK_INTERUPTIBLE vs TASK_UNINTERRUPTIBLE

一个线程的状态不属于 Running 或者 Runnable 的时候,那就是 Sleep 状态了(严谨来说,还有其他状态,不过对性能分析来说不常见,比如 STOP、Trace 等)。

在 Linux 中的Sleep 状态可以细分为 3 个状态:

  • TASK_INTERUPTIBLE → 可中断
  • TASK_UNINTERRUPTIBLE → 不可中断
  • TASK_KILLABLE → 等同于 TASK_WAKEKILL | TASK_UNINTERRUPTIBLE

 "图 1: 性能之巅 2 CPU 优化"

在 Systrace/Perfetto 中,Sleep 状态指的是 Linux 中的TASK_INTERUPTIBLE,trace 中的颜色为白色。Uninterruptible Sleep 指的是 Linux 中的 TASK_UNINTERRUPTIBLE,trace 中的颜色为橙色。

本质上他们都是处于睡眠状态,拿不到 CPU的时间片,只有满足某些条件时才会拿到时间片,即变为 Runnable,随后是 Running。

TASK_INTERRUPTIBLE 与 TASK_UNINTERRUPTIBLE 本质上都是 Sleep,区别在于前者是可以处理 Signal 而后者不能,即使是 Kill 类型的Signal。因此,除非是拿到自己等待的资源之外,没有其他方法可以唤醒它们。 TASK_WAKEKILL 是指可以接受 Kill 类型的Signal 的TASK_UNINTERRUPTIBLE。

Android 中的 Looper、Java/Native 锁等待都属于 TAKS_INTERRUPTIBLE,因为他们可以被其他进程唤醒,应该说绝大部分的程序都处于 TAKS_INTERRUPTIBLE 状态,即 Sleep 状态。 看看 Systrace 中的一大片进程的白色状态就知道了(trace 中表现为白色块),它们绝大部分时间都是在 Runnning 跟 Sleep 状态之间转换,零星会看到几个 Runnable 或者 UninterruptibleSleep,即蓝色跟橙色。

TASK_UNINTERRUPTIBLE 作用

似乎看来 TASK_INTERUPTIBLE 就可以了,那为什么还要有 TASK_UNINTERRUPTIBLE 状态呢?

中断来源有两个,一个是硬件,另一个就是软件。硬件中断是外围控制芯片直接向 CPU 发送了中断信号,被 CPU 捕获并调用了对应的硬件处理函数。软件中断,前面说的 Signal、驱动程序里的 softirq 机制,主要用来在软件层面触发执行中断处理程序,也可以用作进程间通讯机制。

一个进程可以随时处理软中断或者硬件中断,他们的执行是在当前进程的上下文上,意味着共享进程的堆栈。但是在某种情况下,程序不希望有任何打扰,它就想等待自己所等待的事情执行完成。比如与硬件驱动打交道的流程,如 IO 等待、网络操作。 这是为了保护这段逻辑不会被其他事情所干扰,避免它进入不可控的状态

Linux 处理硬件调度的时候也会临时关闭中断控制器、调度的时候会临时关闭抢占功能,本质上为了 防止程序流程进入不可控的状态。这类状态本身执行时间非常短,但系统出异常、运行压力较大的时候可能会影响到性能。

https://elixir.bootlin.com/linux/latest/ident/TASK_UNINTERRUPTIBLE

可以看到内核中使用此状态的情况,典型的有 Swap 读数据、信号量机制、mutex 锁、内存慢路径回收等场景。

分析时候的注意点

首先要认识到 TASK_INTERUPTIBLE、TASK_UNINTERRUPTIBLE 状态的出现是正常的,但是如果这些这些状态的累计占比达到了一定程度,就要引起注意了。特别是在关键操作路径上这类状态的占比较多的时候,需要排查原因之后做相应的优化。 分析问题以及做优化的时候需要牢牢把握两个关键点,它类似于内功心法一样:

  1. 原因的排查方法
  2. 优化方法论

你需要知道是什么原因导致了这次睡眠,是主动的还是被动的?如果是主动的,通过走读代码调查是否是正常的逻辑。如果是被动的,故事的源头是什么? 这需要你对系统有足够多的认识,以及分析问题的经验,你需要经常看案例以增强自己的知识。

以下把 TASK_INTERUPTIBLE 称之为 Sleep,TASK_UNINTERRUPTIBLE称之为 UninterruptibleSleep,目的是与 Systrac 中的用词保持一致。

初期分析 Sleep 与 UninterruptibleSleep 状态的经验不足时你会感到困惑,这种困惑主要是来自于对系统的不了解。你需要读大量的框架层、内核层的代码才能从 Trace 中找出蛛丝马迹。目前并没有一种 Trace 工具能把整个逻辑链路描述的很清楚,而且他们有时候还有不准的时候,比如 Systrace 中的 wakeup_from 信息。只有广泛的系统运行原理做为支持储备,再结合 Trace 工具分析问题,才能做到准确定位问题根因。否则就是我经常说的「性能优化流氓」,你说什么是什么,别人也没法证伪。反复折磨测试同学复测,没测出来之后,这个问题也就不了了之了。

本文没办法列举完所有状态的原因,因此只能列举最为常见的类型,以及典型的实际案例。更重要的是,你需要掌握诊断方法,并结合源代码来定位问题。

Trace 中的可视化效果

Pefetto 中支持显示的状态

Systrace 支持显示的状态

Sleep 状态分析

图 1: UIThread 等待 RenderThread

图 2: Binder 调用等待

诊断方法

通过 wakeup from tid: ***查看唤醒线程

Sleep 最常见的有图 1(UIThread 与 RenderThread 同步)的情况与图 2(Binder 调用)的情况。 Sleep 状态一般是由程序主动等待某个事件的发生而造成的,比如锁等待,因此它有个比较明确的唤醒源。比如图 1,UIThread 等待的是 RenderThread,你可以通过阅读代码来了解这种多线程之间的交互关系。虽然最直接,但是对开发者的要求非常高,因为这需要你熟读图形栈的代码。这可不是一般的难度,是追求的目标,但不具备普适性。

更简单的方法是通过所谓的 wakeup from tid: *** 来调查线程之间的交互关系。从前面的 Runnable 文章 中讲过,任何线程进入 Running 之前会先进入到 Runnable 状态,由此再转换成 Running。从 Sleep 状态切换到 Running,必然也要经过 Runnable。

进入到 Runnable 有两种方式,一种是 Running 中的程序被抢占了,暂时进入到 Runnable。还有一种是由另外一个线程将此线程(处于 Sleep 的线程)变成了 Runnable。

我们在调查Sleep 唤醒线程关系的时候,应用到的原理是第二种情况。在 Systrace 中这种是被 wakeup from tid: *** 信息所呈现。线程被抢占之后变成 Runnable,在 Systrace 中是被 Running Instead 呈现。

需要特别注意的是 wakeupfrom 这个有时候不准,原因是跟具体的 tracepoint 类型有关。分析的时候要注意甄别,不要一味地相信这个数据是对的。

其他方法

  1. Simpleperf 还原代码执行流
  2. 在 Systrace 寻找时间点对齐的事件

方法 1 适合用来看程序到底在执行什么操作进入到这种状态,是 IO 还是锁等待?球里连载 Simpleperf 工具的使用方法,其中「Simpleperf 分析篇 (1): 使用 Firefox Profiler 可视分析 Simpleperf 数据」介绍了可以按时间顺序看函数调用的可视化方法。其他使用也会陆续更新,直接搜关键字即可。

方法 2 是个比较笨的方法,但有时候也可以通过它找到蛛丝马迹,不过缺点是错误率比较高。

耗时过长的常见原因

  • Binder 操作 → 通过打开 Binder 对应的 trace,可方便地观察到调用到远端的 Binder 执行线程。如果 Binder 耗时长,要分析远端的 Binder 执行情况,是否是锁竞争?得不到CPU 时间片?要具体问题具体分析
  • Java\futex锁竞争等待 → 最常见也是最容易引起性能问题,当负载较高时候特别容易出现,特别是在 SystemServer 进程中。这是 Binder 多线程并行化或抢占公共资源导致的弊端。
  • 主动等待 → 线程主动进入 Sleep 状态,等待其它线程的唤醒,比如等待信号量的释放。优化建议:需要看代码逻辑分析等待是否合理,不合理就要优化掉。
  • 等待 GPU 执行完毕 → 等 GPU 任务执行完毕,Trace 中可以看到等 GPU fence 时间。常见的原因有渲染任务过重、 GPU 能力弱、GPU 频率低等。优化建议:提升 GPU 频率、降低渲染任务复杂度,比如精简 Shader、降低渲染分辨率、降低Texture 画质等。

UninterruptibleSleep 状态分析

诊断方法

本质上UninterruptibleSleep 也是一种 Sleep,因此分析 Sleep 状态时用到的方法也是通用的。不过此状态有两个特殊点与 Sleep 不同,因此在此特别说明。

  1. UninterruptibleSleep 分为 IOWait 与 Non-IOWait
  2. UninterruptibleSleep 有 Block reason

UninterruptibleSleep 分为 IOWait 与 Non-IOWait

IO 等待好理解,就是程序执行了 IO 操作。最简单的,程序如果没法从 PageCache 缓存里快速拿到数据,那就要与设备进行 IO 操作。CPU 内部缓存的访问速度是最快的,其次是内存,最后是磁盘。它们之间的延迟差异是数量级差异,因此系统越是从磁盘中读取数据,对整体性能的影响就越大。

非 IO 等待主要是指内核级别的锁等待,或者驱动程序中人为设置的等待。Linux 内核中某些路径是热点区域,因此不得不拿锁来进行保护。比如Binder 驱动,当负载大到一定程度,Binder 的内部的锁竞争导致的性能瓶颈就会呈现出来。

Block Reason

谷歌的 Riley Andrews(riandrews@google.com) 15年左右往内核里提交了一个 tracepoint 补丁,用于记录当发生 UninterruptibleSleep 的时候是否是 IO 等待、调用函数等信息。Systrace 中的展示的 IOWait 与 BlockReason,就是通过解析这条 tracepoint 而来的。这条代码提交的介绍如下(由于这笔提交未合入到 Linux 上游主线,因此要注意你用的内核是否单独带了此补丁):

1
2
3
4
5
6
7
sched: add sched blocked tracepoint which dumps out context of sleep.
Decare war on uninterruptible sleep. Add a tracepoint which
walks the kernel stack and dumps the first non-scheduler function
called before the scheduler is invoked.

Change-Id: [I19e965d5206329360a92cbfe2afcc8c30f65c229](https://android-review.googlesource.com/#/q/I19e965d5206329360a92cbfe2afcc8c30f65c229)
Signed-off-by: Riley Andrews [riandrews@google.com](mailto:riandrews@google.com)

在 ftrace(Systrace 使用的数据抓取机制) 中的被记录为

1
sched_blocked_reason: pid=30235 iowait=0 caller=get_user_pages_fast+0x34/0x70 

这句话被 Systrace 可视化的效果为:

主线程中有一段 Uninterruptible Sleep 状态,它的 BlockReason 是 get_user_pages_fast。它是一个 Linux 内核中函数的名字,代表着是线程是被它切换到了 UninterruptibleSleep 状态。为了查看具体的原因,需要查看这个函数的具体实现。

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
/**
* get_user_pages_fast() - pin user pages in memory
* @start: starting user address
* @nr_pages: number of pages from start to pin
* @gup_flags: flags modifying pin behaviour
* @pages: array that receives pointers to the pages pinned.
* Should be at least nr_pages long.
*
* Attempt to pin user pages in memory without taking mm->mmap_lock.
* If not successful, it will fall back to taking the lock and
* calling get_user_pages().
*
* Returns number of pages pinned. This may be fewer than the number requested.
* If nr_pages is 0 or negative, returns 0. If no pages were pinned, returns
* -errno.
*/
int get_user_pages_fast(unsigned long start, int nr_pages,
unsigned int gup_flags, struct page **pages)
{
if (!is_valid_gup_flags(gup_flags))
return -EINVAL;

/*
* The caller may or may not have explicitly set FOLL_GET; either way is
* OK. However, internally (within mm/gup.c), gup fast variants must set
* FOLL_GET, because gup fast is always a "pin with a +1 page refcount"
* request.
*/
gup_flags |= FOLL_GET;
return internal_get_user_pages_fast(start, nr_pages, gup_flags, pages);
}
EXPORT_SYMBOL_GPL(get_user_pages_fast);

从函数解释上可以看到,函数首先是通过无锁的方式pin 应用侧的 pages,如果失败的时候不得不尝试持锁后走慢速执行路径。此时,无法持锁的时候那就要等待了,直到先前持锁的人释放锁。那之前被谁持有了呢?这时候可以利用之前介绍的Sleep 诊断方法,如下图。

UninterruptibleSleep 状态相比 Sleep 有点复杂,因为它涉及到 Linux 内部的实现。可能是内核本身的机制有问题,也有可能是应用层使用不对,因此要联合上层的行为综合诊断才行。毕竟内核也不是万能的,它也有自己的能力边界,当应用层的使用超过其边界的时候,就会出现影响性能的现象。

IOWait 常见原因与优化方法

1. 主动IO 操作

  • 程序进行频繁、大量的读或者写 IO 操作,这是最常见的情况。
  • 多个应用同时下发 IO 操作,导致器件的压力较大。同时执行的程序多的时候 IO 负载高的可能性也大。
  • 器件本身的 IO 性能较差,可通过 IO Benchmark 来进行排查。 常见的原因有磁盘碎片化、器件老化、剩余空间较少(越是低端机越明显)、读放大、写放大等等。
  • 文件系统特性,比如有些文件系统的内部操作会表现为 IO 等待。
  • 开启 Swap 机制的内核下,数据从 Swap 中读取。

优化方法

  • 调优 Readahead 机制
  • 指定文件到 PageCache,即 PinFile 机制
  • 调整 PageCache 回收策略
  • 调优清理垃圾文件策略

2. 低内存导致的 IO 变多

内存是个非常有意思的东西,由于它的速度比磁盘快,因此 OS 设计者们把内存当做磁盘的缓存,通过它来避免了部分IO操作的请求,非常有效的提升了整体 IO 性能。有两个极端情况,当系统内存特别大的时候,绝大部分操作都可以在内存中执行,此时整体 IO 性能会非常好。当系统内存特别低,以至于没办法缓存 IO 数据的时候,几乎所有的 IO 操作都直接与器件打交道,这时候整体性能相比内存多的时候而言是非常差的。

所以系统中的内存较少的时候 IO 等待的概率也会变高。所以,这个问题就变成了如何让系统中有足够多的内存?如何调节磁盘缓存的淘汰算法?

优化方法

  • 关键路径上减少 IO 操作
  • 通过Readahead 机制读数据
  • 将热点数据尽量聚集在一起,使被 Readahead 机制命中的概率高
  • 最后一个老生常谈的,减少大量的内存分配、内存浪费等操作

系统中的内存是被各个进程所共用。当app 只考虑自己,肆无忌惮的使用计算资源,必然会影响到其他程序。这时候系统还是会回来压制你,到头来亏损的还是自己。 不过能想到这一步的开发者比较少,也不现实。明文化的执行系统约定,可能是个终极解决方案。

Non-IOWait 常见原因

  • 低内存导致等待 → 低内存的时候要回收其他程序或者缓存上的内存。
  • Binder 等待 → 有大量 Binder 操作的时候出现概率较高。
  • 各种各样的内核锁,不胜枚举。结合「诊断方法」来分析。

系统调度与 UninterruptibleSleep 耦合的问题

当线程处于 UninterruptibleSleep 非 IO等待状态(即内核锁),而持有该锁的其他线程因 CPU 调度原因,较长时间处于 Runnable 状态。这时候就出现了有意思的现象,即使被等待的线程处于高优先级,它的依赖方没有被调度器及时的识别到,即使是非常短的锁持有,也会出现较长时间的等待。

规避或者彻底解决这类问题都是件比较难的事情,不同厂家实现了不同的解决方案,也是比较考虑厂家技术能力的一个问题。

附录

Linux 线程状态释义

线程状态描述
SSLEEPING
R、R+RUNNABLE
DUNINTR_SLEEP
TSTOPPED
tDEBUG
ZZOMBIE
XEXIT_DEAD
xTASK_DEAD
KWAKE_KILL
WWAKING
DK
DW

案例: 从 Swap 读取数据时的等待

案例: 同进程的多个线程进行 mmap

共享同一个 mm_struct 的线程同时执行 mmap() 系统调用进行 vma 分配时发生锁竞争。

mmap_write_lock_killable() 与 mmap_write_unlock() 包起来的区域就是由锁受保护的区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned long vm_mmap_pgoff(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flag, unsigned long pgoff)
{
unsigned long ret;
struct mm_struct *mm = current->mm;
unsigned long populate;
LIST_HEAD(uf);

ret = security_mmap_file(file, prot, flag);
if (!ret) {
if (mmap_write_lock_killable(mm))
return -EINTR;
ret = do_mmap(file, addr, len, prot, flag, pgoff, &populate,
&uf);
mmap_write_unlock(mm);
userfaultfd_unmap_complete(mm, &uf);
if (populate)
mm_populate(ret, populate);
}
return ret;
}

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Systrace 线程 CPU 运行状态分析技巧 - Running 篇

作者 Gracker
2022年3月13日 08:38

本文是 Systrace 线程 CPU 运行状态分析技巧系列的第二篇,主要分析了 Systrace 中 cpu 的 Running 状态出现的原因和 Running 过长时的一些优化思路。

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。Systrace 基础和实战系列大家可以在 Systrace 基础知识 - Systrace 预备知识 或者 博客文章目录 这里看到完整的目录

  1. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  2. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  3. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

Running 时间长

显示方式

Trace 中显示绿色,表示线程处于运行态

原因 1: 代码本身复杂度高,执行耗时久

这是最常见的一种方式,当然不排除平台有bug,有时候厂商在libc、syscal等高频核心函数,加了一些逻辑导致了代码运行时间长。

优化建议: 优化逻辑、算法,降低复杂度。为了进一步判断具体是哪个函数耗时,可使用 AS CPU Profiler、simpleperf,或者自己通过 Trace.begin/end() API 添加更多 tracepoint 点

当然不排除有的时候平台有bug,在关键的libc或内核函数加了一些逻辑

原因 2: 代码以解释方式执行

Trace 中看到 「Compiling」字眼时可能意味着它是解释执行方式进行。刚安装的应用(未做 odex)的程序经常会出现这种情况

优化建议: 使用 dex2oat 之后的版本试试,解释执行方式下的低性能暂无改善方法,除非执行 dex2oat 或者提高代码效率本身

除此之外,使用了编程语言的某种特性,如频繁的调用 JNI,反复性反射调用。除了通过积攒经验方式之外,通过工具解决的方法就是通过 CPU Profiler、simpleperf 等工具进行诊断

原因 3: 线程跑小核,导致执行时间长

对 CPU Bound 的操作来说跑在小核可能没法满足性能需求,因为小核的定位是处理非UX 强相关的线程。不过 Android 没办法保证这一点,有时候任务就是会安排在小核上执行。

优化建议:线程绑核、SchedBoost 等操作,让线程跑尽量跑更高算力的核上,比如大核。有时候即使迁核了也不见效,这时候要看看频率是否拉得足够高,见“原因 4”

原因 4: 线程所跑的大核运行频率太低

优化建议:

  1. 优化代码逻辑,主动降低运行负载,CPU 频率低也能流畅运行
  2. 修改调度器升频相关的参数,让 CPU 根据负载提频更激进
  3. 用平台提供的接口锁定 CPU 频率(俗称的「锁频」)

原因 5: 温升导致 CPU 关核、限频

优化建议:

手机因结构原因导致散热能力差或温升参数过于激进时,为了保护体验跟不烫伤人,几乎所有手机厂家的系统会限制 CPU 频率或者直接关核。排查思路是首先需要找到触发温升的原因。

温升的排查的第一步,首先要看是外因导致还是内因导致。外因是指是否由外部高温导致,如太阳底下,火炉边;往往夏天的时候导致手机发热的情况越严重

内因主要由 CPU、Modem、相机模组或者其他发热比较厉害的器件导致的。以 CPU 为例,如果后台某个线程吃满 CPU,那就首先要解决它。如果是前台应用负载高导致大电流消耗,同样道理,那就降低前台本身的负载。其他器件也是同样道理,首先要看是否是无意义的运行,其次是优化业务逻辑本身

除此之外,温升参数过于激进的话导致触发限频关核的概率也会提高,因此通过与竞品对比等方式调优温升参数本身来达到优化目的

原因 6: CPU 算力弱

优化建议:

ARM 处理器在相同频率下不同微架构的配置导致的性能差异是非常明显的,不同运行频率、L1/L2 Cache 的容量均能影响 CPU 的 MIPS(Million Instructions Per Second) 执行结果。

优化思路有两条:

  1. 编译器参数
  2. 优化代码逻辑

第一条比较难,大部分应用开发者来说也不太现实,系统厂商如华为,方舟编译器优化 JNI 的思路本质是不改应用代码情况下提高代码执行效率来达到性能上的提升

第二条可以通过 simpleperf 等工具,找到热点代码或者观察 CPU 行为后做进一步的改善,如:

  • Cache miss 率过高导致执行耗时,就要优化内存访问相关逻辑
  • 代码复杂指令过多导致耗时,就要优化代码逻辑,降低代码复杂度
  • 设计好业务缓存,尽量提高缓存命中率,避免抖动(反复地申请与释放)

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇

作者 Gracker
2022年1月21日 10:40

本文是 Systrace 线程 CPU 运行状态分析技巧系列的第一篇,主要分析了 Systrace 中 cpu 的 runnable 状态出现的原因和 Runnable 过长时的一些优化思路。

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。Systrace 基础和实战系列大家可以在 Systrace 基础知识 - Systrace 预备知识 或者 博客文章目录 这里看到完整的目录

  1. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  2. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  3. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

Runnable 状态说明

Runnable 状态在 Trace 中的显示方式

Perfetto/Systrace: 不同 CPU 运行状态异常原因 101 - Running 长 中讲解了导致 CPU 的 Running 状态耗时久的原因与优化方法,这一节介绍 Runnable 状态切换原理与对应的排查与优化思路。在 Systrace 中显示为蓝色,表示线程处于 Runnable,等待被 CPU 调度执行。

图 1: Systrace 中 Runnable 的可视化效果展示

图 2: 性能之巅 2 CPU 优化

从图 2 可知,一个 CPU 核在某个时刻只能执行一个线程,因此所有待执行的任务都在一个「可执行队列」里排队,一个 CPU 核就有一个队列。能插入到这个队列里排队的,代表着这个线程除了 CPU 资源,其他资源均已获取,如 IO、锁、信号量等。处于「可执行队列」的时候,线程的状态就会被置为 RUNNABLE,也就是 Systrace 里看到的 Runnable 状态。

Linux 内核是通过赋予不同线程执行时间片并通过轮转的方式来达到同时执行多个线程的效果,因此当一个 Running 中的线程的时间片用完时(通常是 ms 级别)将此线程置为 Runnable,等待下一次被调度。也有比较特殊的情况,那就是抢占。有些高优先级的线程可以抢占当前执行的线程,而不必等到此线程的时间片到期。

当一个 CPU 有多个核的时候显然可以多个核同时工作,这时候不必都在一个 CPU 核上排队,根据负载情况(也就是排队情况),将线程迁移到其他核执行是必要的操作。掌管这些调度策略的,是通过 Linux 的调度器来实现的,它具体通过多个调度类(Schedule Class)来管理不同线程的优先级,常见的有:

  1. SCHED_RR、SCHED_FIFO: 实时调度类,整体优先级上高于 NORMAL。
  2. SCHED_NORMAL: 普通调度类,目前常用的是 CFS(Complete Fair Scheduler)调度器。
    实时类的优先级高于普通调度类,高优先级的能抢占低优先级,并且要等待高优先级的执行完才能执行低优先级的。一般情况下 Runnable 的时间都很短,但出异常的的时候它会影响关键线程的关键任务在指定时间内完成。

图 3: AOSP 渲染架构

这个可能不止是一个线程,甚至是多个。特别是涉及到 UI 相关的任务,这种情况就更为复杂了。AOSP 体系下典型的一帧绘制是经过 UI Thread → Render Thread → SurfaceFlinger → HWC(参考 图 3),其中任何一个线程被 Runnable 阻塞导致没有在规定时间内完成渲染任务,都将会导致界面的卡顿(也就是掉帧)。

Runnable 过长的原因和优化思路

我们从实践中总结出以下 5 大门类,系统层面出异常的原因较多,但也见过应用自身逻辑导致 Runnable 过长情况。

原因 1: 优先级设置错误

  • 应用设置了过高的优先级:至于抢占了其他线程的任务,对后者来说显得自己优先级太低了。
  • 应用设置了过低的优先级:当此线程处于「关键链路」时,以 Runnable 执行的概率就越高,导致卡顿概率也高。
  • 系统出 Bug 时把线程优先级设为过高或者过低。

优化思路:

  1. 应用视情况调整线程优先级,可从 Trace 中可以看到是被哪个线程抢占了。
  2. 系统将关键线程调度策略设置成 FIFO。

我们在实践中见到过不少应用因为设置错了优先级反而导致更卡。原因比较复杂,可能开发者所使用的机器用当时的优先级策略没问题,但是在别的厂商的调度器(头部大厂基本都有自己改动调度器)下就会出现水土不兼容的情况。一般情况下,三方应用开发者不建议直接调用这类 API,弄巧成拙,屡见不鲜。

长远看来更靠谱的方式是合理安排自己的任务模型,不要把对实时性要求很高的任务放到 worker 线程上。

原因 2: 绑核不合理

有时候为了让线程运行得更快,会把线程绑定到大核,在前面解决 Running 时间长时也有建议绑大核,但是绑核一定要谨慎,因为一旦把线程绑定在某个核,表示线程只能运行在这个核上即使其它核很空闲。如果多个线程都绑定在某个核,当这个核很繁忙调度不过来时,这些线程就会出现 Runnable 时间很长的情况。所以绑核一定要谨慎!下面是绑核需要注意的一些事项:

  1. 线程绑核不要绑定在单个核上,这样容错率会特别低,因为一旦这个核被其它线程抢占绑定这个核的线程就要等着,所以尽量以 CPU 簇为单位进行绑核,比如线程要绑定大核,可以指定 4-7 大核而不是指定某个一大核。
  2. 2 个大核平台尽可能减少绑定大的核线程数目,不然会使得大核很容易繁忙,把绑核会变成「负优化」。
  3. 要正确区分大小核,比如 8 个核的平台,4-7 不一定就是大核,有的平台可能 0-3 才是大核。
  4. 只能在 CPUSET 允许范围内绑核,如果 CPUSET 只允许进程跑 0-3,如果进程试图绑定在 4-7 会绑核失败,甚至会有一些意料之外的致命错误。

原因 3: 软件架构设计不合理

重申下,Runnable 是指在 CPU 核上的排队耗时,按常识可可知道排队长、频繁排队时出问题概率也就越高。一个绘制任务所依赖的线程数量越多,出问题的概率也越高,因为排队次数变多了嘛。

软件架构不止要满足业务需求,也要在性能、扩展性方面上做思考,从上面推导可知,如果你程序编程模型需要大量线程协同运行来完成关键操作,如绘制,那出问题的概率就越高。

最常见的有,两个线程之间有频繁的有通讯与等待(线程 A 把任务转移到线程 B 执行,A 等待 B 任务执行完后被唤醒), CPU 繁忙时很容易打出 Runnable 等待状态,CPU 越忙概率越高。

优化思路:

  1. 应用调整线程优先级,见「原因 1」。
  2. 优化代码架构/逻辑,免频繁等待其他线程的唤醒,在 Trace 中可以看到线程的依赖关系。可借助 CPU Profiler 探查代码执行逻辑,提高分析唤醒关系的效率。
  3. 平台通过修改调度器来识别有关系链的线程组,优先调度这个组里的线程。

原因 4: 应用自己或系统整体负载高导致排队的任务非常多

从上述的调度原理可知,如果大量任务挤在一个核的「可执行队列」上,显然越是后面,优先级越低的任务排队时间就越长。

排查的时候你可以在 Perfetto/Systrace 的 CPU 核维度任务上,即使在放大后的界面看到排满了密密麻麻的任务,这基本上就意味着系统整体负载较高了。通过计算,可算出 CPU 每时刻的使用量,基本上都会在 90%以上。 你可以通过选择一个区间,以时间来排序,看看都在执行什么任务,以此来逐个排查同时执行大量程序的原因是什么。

简单总结就是,同时执行的任务太多了,主要原因来自两方面:

1.应用自身高占用

应用自身就把 CPU 资源都给占满了,狂开十来个线程来做事情,即使是头部大厂也会做这种事。

优化建议:

  1. 找出应用所有占用高的线程,看看各线程此刻跑起来的行为是否异常,如果异常则要优化它。
  2. 优化线程负载本身,可使用 simpleperf 等工具进行函数级别的定位。
  3. 调整优先级,使用比 CFS 更高优先级的调度器,如设置为 RT。不过它带来的隐患也较多,需要慎重。
  4. 优化软件架构,区分关键与非关键线程,通过合理设置「绑核 & 优先级」来为关键线程让出资源。 如,不重要线程绑到小核运行或设置低优先级、渲染相关线程设置高优先级等,让渲染线程相关的线程能占用到更多的 CPU 资源。设计架构的时候一定要考虑运行环境恶劣的情况,因为安卓从设计上就不敢保证所有资源都优先供给你,肯定有别人跟你抢资源。

2.系统服务高占用

有的厂商 ROM 自己本身就有很多任务,设计不合理的话自己家程序就吃满了大量资源,导致留给应用运行的资源较少。还有些是管控措施设计的一般,以至于留给了大量流氓应用可乘之机,各路神仙利用自己的「黑科技」在后台保活后进行各种拉活同步操作。

3.平台厂家的黑科技

厂家除了要优化自身服务,以做到「点到为止」外,可以实现如下功能来尽可能把资源分配合理化,让出更多资源给前台应用。

  1. 通过 CGROUP 的 CPUSET 子系统,让不同优先级的线程运行在不同的 CPU 核心。AOSP 自带了 CPUSET 分组功能,不过有些缺陷如:
    1. 分组不够精细,很多后台都可以跑满所有核
    2. 没有考虑进程的工作状态,如 音乐、导航、录音、视频、通话、下载
    3. 对 Java 进程 fork 的子进程放任不管
  2. 通过 CGROUP 的 CPUCTL 子系统,进行资源配额,如限制异常进程、普通后台进程的不同量级的 CPU 最高使用量。
  3. 通过线程&进程级别的冻结技术,在应用退出后台之后冻结进程让其拿不到 CPU 资源,类似 iOS 的做法。难点在于:
    1. 切断和恢复各跨进程通信
    2. 进程关系的梳理
    3. 兼容性问题,需要有大量的测试验证
  4. 按需启动系统进程与管控好后台进程自启动。

每一个优化说简单也简单,说难也难,依赖厂家的技术积累。

原因 5: CPU 算力限制、锁频、锁核、状态异常

排队做核酸检测一样,检测窗口多的队列排队时间少。CPU 算力差、关核、限频,导致 Runnable 的概率也更高。通常的原因有:

  1. 场景控制
    • 不同场景模式下的不同频率、核心策略
    • 高温下的锁频锁核
  2. CPU 省电模式:如高通的 Low Power Mode。
  3. CPU 状态切换:如 C2/C1 切换到 C0 耗时久。
  4. CPU 损坏,概率小但也有可能会出现。
  5. 低端机 :安卓上的低端机。

其中:

  1. 原因 1 场景控制, 考验厂家的能力与各自的标准,应用程序能做的还是那句名言 → 降低自己负载,少惹平台。 厂家为了设计好「场景控制」,需要有精细化的场景识别与合理的控制能力,将功耗与性能的平衡做到全局最优化,不同场景下应突出不同的业务能力,而不是一杆子拍死。
  2. 高温下的优化建议请参考「Perfetto/Systrace: 不同 CPU 运行状态异常原因 101 - Running 长」中的「原因 5: 温升导致 CPU 关核、限频」。
  3. 原因 3 CPU 状态切换 是芯片固有的特性,出现的概率小,但也不是不可能,每个芯片架构升级换代的时候就时不时遇到「妥协」版的 CPU 产品。厂家对芯片的评估是个比较隐性的能力,很少会被大众提及,但是非常重要的一个能力。电子消费品历史中,也总是重演关键器件选错了,导致厂家走入万劫不复境地的真实案例。
  4. 原因 5,安卓上的低端机,真的就指配备里低算力的 CPU,这与苹果的做法不一样,它的 CPU 至少跟当期旗舰是一样的。同样参考 「Perfetto/Systrace: 不同 CPU 运行状态异常原因 101 - Running 长」中的「原因 6: 算力弱」。

原因 6: 调度器异常

几乎所有的厂家都做了调度器优化方面的工作,虽然概率小,但也有可能会出异常。场景锁频锁核机制有问题、内核各种 governor 的出问题的时候,会出现明明 CPU 的其他核都很闲,但任务都挤在某几个核上。

系统开发者能做的就是把基础「可观测性技术」建好,出问题时可以快速诊断,因为这类问题一是不好复现,二是现象出现时机较短,可能立马就恢复了。

原因 7: 处理器区分执行 32 位与 64 位进程

有些过渡期的芯片,如最近推出的骁龙 8Gen1 与 天玑 9000,会有非常奇葩的运行限制。32 位的程序只能运行某个特定微架构上,64 位的则畅通无阻。且先不说这种「脑残设计」是处于什么所谓「平衡」,他带来的问题是,当你用的应用大量还是 32 位的时候,很多任务(以进程为单位)都挤在某个核心上运行,结合前面的理论,都挤在一起,出现 Runnable 的概率就更高。

  1. 对应用开发者,建议尽快升级至 64 位程序。如果你用的是第三方方案,尽早通知改进或者改用其他方案。
  2. 对系统开发者,一是根据问题联系应用厂商做更新,二是特殊加强后台管理功能,进一步降低 32 位程序的运行负载。

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android Systrace 基础知识 - SurfaceFlinger 解读

作者 Gracker
2020年2月14日 10:25

本文是 Systrace 系列文章的第五篇,主要是对 SurfaceFlinger 的工作流程进行简单介绍,介绍了 SurfaceFlinger 中几个比较重要的线程,包括 Vsync 信号的解读、应用的 Buffer 展示、卡顿判定等,由于 Vsync 这一块在 Systrace 基础知识 - Vsync 解读Android 基于 Choreographer 的渲染机制详解 这两篇文章里面已经介绍过,这里就不再做详细的讲解了。

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

正文

这里直接上官方对于 SurfaceFlinger 的定义

  1. 大多数应用在屏幕上一次显示三个层:屏幕顶部的状态栏、底部或侧面的导航栏以及应用界面。有些应用会拥有更多或更少的层(例如,默认主屏幕应用有一个单独的壁纸层,而全屏游戏可能会隐藏状态栏)。每个层都可以单独更新。状态栏和导航栏由系统进程渲染,而应用层由应用渲染,两者之间不进行协调。
  2. 设备显示会按一定速率刷新,在手机和平板电脑上通常为 60 fps。如果显示内容在刷新期间更新,则会出现撕裂现象;因此,请务必只在周期之间更新内容。在可以安全更新内容时,系统便会收到来自显示设备的信号。由于历史原因,我们将该信号称为 VSYNC 信号。
  3. 刷新率可能会随时间而变化,例如,一些移动设备的帧率范围在 58 fps 到 62 fps 之间,具体要视当前条件而定。对于连接了 HDMI 的电视,刷新率在理论上可以下降到 24 Hz 或 48 Hz,以便与视频相匹配。由于每个刷新周期只能更新屏幕一次,因此以 200 fps 的帧率为显示设备提交缓冲区就是一种资源浪费,因为大多数帧会被舍弃掉。SurfaceFlinger 不会在应用每次提交缓冲区时都执行操作,而是在显示设备准备好接收新的缓冲区时才会唤醒。
  4. 当 VSYNC 信号到达时,SurfaceFlinger 会遍历它的层列表,以寻找新的缓冲区。如果找到新的缓冲区,它会获取该缓冲区;否则,它会继续使用以前获取的缓冲区。SurfaceFlinger 必须始终显示内容,因此它会保留一个缓冲区。如果在某个层上没有提交缓冲区,则该层会被忽略。
  5. SurfaceFlinger 在收集可见层的所有缓冲区之后,便会询问 Hardware Composer 应如何进行合成。」

—- 引用自SurfaceFlinger 和 Hardware Composer

下面是上述流程所对应的流程图, 简单地说, SurfaceFlinger 最主要的功能:SurfaceFlinger 接受来自多个来源的数据缓冲区,对它们进行合成,然后发送到显示设备。

那么 Systrace 中,我们关注的重点就是上面这幅图对应的部分

  1. App 部分
  2. BufferQueue 部分
  3. SurfaceFlinger 部分
  4. HWComposer 部分

这四部分,在 Systrace 中都有可以对应的地方,以时间发生的顺序排序就是 1、2、3、4,下面我们从 Systrace 的这四部分来看整个渲染的流程

App 部分

关于 App 部分,其实在Systrace 基础知识 - MainThread 和 RenderThread 解读这篇文章里面已经说得比较清楚了,不清楚的可以去这篇文章里面看,其主要的流程如下图:

从 SurfaceFlinger 的角度来看,App 部分主要负责生产 SurfaceFlinger 合成所需要的 Surface。

App 与 SurfaceFlinger 的交互主要集中在三点

  1. Vsync 信号的接收和处理
  2. RenderThread 的 dequeueBuffer
  3. RenderThread 的 queueBuffer

Vsync 信号的接收和处理

关于这部分内容可以查看 Android 基于 Choreographer 的渲染机制详解 这篇文章,App 和 SurfaceFlinger 的第一个交互点就是 Vsync 信号的请求和接收,如上图中第一条标识,Vsync-App 信号到达,就是指的是 SurfaceFlinger 的 Vsync-App 信号。应用收到这个信号后,开始一帧的渲染准备

RenderThread 的 dequeueBuffer

dequeue 有出队的意思,dequeueBuffer 顾名思义,就是从队列中拿出一个 Buffer,这个队列就是 SurfaceFlinger 中的 BufferQueue。如下图,应用开始渲染前,首先需要通过 Binder 调用从 SurfaceFlinger 的 BufferQueue 中获取一个 Buffer,其流程如下:

App 端的 Systrace 如下所示
-w1249

SurfaceFlinger 端的 Systrace 如下所示
-w826

RenderThread 的 queueBuffer

queue 有入队的意思,queueBuffer 顾名思义就是讲 Buffer 放回到 BufferQueue,App 处理完 Buffer 后(写入具体的 drawcall),会把这个 Buffer 通过 eglSwapBuffersWithDamageKHR -> queueBuffer 这个流程,将 Buffer 放回 BufferQueue,其流程如下

App 端的 Systrace 如下所示
-w1165

SurfaceFlinger 端的 Systrace 如下所示
-w1295

通过上面三部分,大家应该对下图中的流程会有一个比较直观的了解了
-w410

BufferQueue 部分

BufferQueue 部分其实在Systrace 基础知识 - Triple Buffer 解读 这里有讲,如下图,结合上面那张图,每个有显示界面的进程对应一个 BufferQueue,使用方创建并拥有 BufferQueue 数据结构,并且可存在于与其生产方不同的进程中,BufferQueue 工作流程如下:

上图主要是 dequeue、queue、acquire、release ,在这个例子里面,App 是生产者,负责填充显示缓冲区(Buffer);SurfaceFlinger 是消费者,将各个进程的显示缓冲区做合成操作

  1. dequeue(生产者发起) : 当生产者需要缓冲区时,它会通过调用 dequeueBuffer() 从 BufferQueue 请求一个可用的缓冲区,并指定缓冲区的宽度、高度、像素格式和使用标记。
  2. queue(生产者发起):生产者填充缓冲区并通过调用 queueBuffer() 将缓冲区返回到队列。
  3. acquire(消费者发起) :消费者通过 acquireBuffer() 获取该缓冲区并使用该缓冲区的内容
  4. release(消费者发起) :当消费者操作完成后,它会通过调用 releaseBuffer() 将该缓冲区返回到队列

SurfaceFlinger 部分

工作流程

从最前面我们知道 SurfaceFlinger 的主要工作就是合成:

当 VSYNC 信号到达时,SurfaceFlinger 会遍历它的层列表,以寻找新的缓冲区。如果找到新的缓冲区,它会获取该缓冲区;否则,它会继续使用以前获取的缓冲区。SurfaceFlinger 必须始终显示内容,因此它会保留一个缓冲区。如果在某个层上没有提交缓冲区,则该层会被忽略。SurfaceFlinger 在收集可见层的所有缓冲区之后,便会询问 Hardware Composer 应如何进行合成。

其 Systrace 主线程可用看到其主要是在收到 Vsync 信号后开始工作
-w1296

其对应的代码如下,主要是处理两个 Message

  1. MessageQueue::INVALIDATE — 主要是执行 handleMessageTransaction 和 handleMessageInvalidate 这两个方法
  2. MessageQueue::REFRESH — 主要是执行 handleMessageRefresh 方法

frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

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
void SurfaceFlinger::onMessageReceived(int32_t what) NO_THREAD_SAFETY_ANALYSIS {
ATRACE_CALL();
switch (what) {
case MessageQueue::INVALIDATE: {
......
bool refreshNeeded = handleMessageTransaction();
refreshNeeded |= handleMessageInvalidate();
......
break;
}
case MessageQueue::REFRESH: {
handleMessageRefresh();
break;
}
}
}

//handleMessageInvalidate 实现如下
bool SurfaceFlinger::handleMessageInvalidate() {
ATRACE_CALL();
bool refreshNeeded = handlePageFlip();

if (mVisibleRegionsDirty) {
computeLayerBounds();
if (mTracingEnabled) {
mTracing.notify("visibleRegionsDirty");
}
}

for (auto& layer : mLayersPendingRefresh) {
Region visibleReg;
visibleReg.set(layer->getScreenBounds());
invalidateLayerStack(layer, visibleReg);
}
mLayersPendingRefresh.clear();
return refreshNeeded;
}

//handleMessageRefresh 实现如下, SurfaceFlinger 的大部分工作都是在handleMessageRefresh 中发起的
void SurfaceFlinger::handleMessageRefresh() {
ATRACE_CALL();

mRefreshPending = false;

const bool repaintEverything = mRepaintEverything.exchange(false);
preComposition();
rebuildLayerStacks();
calculateWorkingSet();
for (const auto& [token, display] : mDisplays) {
beginFrame(display);
prepareFrame(display);
doDebugFlashRegions(display, repaintEverything);
doComposition(display, repaintEverything);
}

logLayerStats();

postFrame();
postComposition();

mHadClientComposition = false;
mHadDeviceComposition = false;
for (const auto& [token, displayDevice] : mDisplays) {
auto display = displayDevice->getCompositionDisplay();
const auto displayId = display->getId();
mHadClientComposition =
mHadClientComposition || getHwComposer().hasClientComposition(displayId);
mHadDeviceComposition =
mHadDeviceComposition || getHwComposer().hasDeviceComposition(displayId);
}

mVsyncModulator.onRefreshed(mHadClientComposition);

mLayersWithQueuedFrames.clear();
}

handleMessageRefresh 中按照重要性主要有下面几个功能

  1. 准备工作
    1. preComposition();
    2. rebuildLayerStacks();
    3. calculateWorkingSet();
  2. 合成工作
    1. begiFrame(display);
    2. prepareFrame(display);
    3. doDebugFlashRegions(display, repaintEverything);
    4. doComposition(display, repaintEverything);
  3. 收尾工作
    1. logLayerStats();
    2. postFrame();
    3. postComposition();

由于显示系统有非常庞大的细节,这里就不一一进行讲解了,如果你的工作在这一部分,那么所有的流程都需要熟悉并掌握,如果只是想熟悉流程,那么不需要太深入,知道 SurfaceFlinger 的主要工作逻辑即可

掉帧

通常我们通过 Systrace 判断应用是否掉帧的时候,一般是直接看 SurfaceFlinger 部分,主要是下面几个步骤

  1. SurfaceFlinger 的主线程在每个 Vsync-SF 的时候是否没有合成?
  2. 如果没有合成操作,那么需要看没有合成的原因:
    1. 因为 SurfaceFlinger 检查发现没有可用的 Buffer 而没有合成操作?
    2. 因为 SurfaceFlinger 被其他的工作占用(比如截图、HWC 等)?
    3. 因为 SurfaceFlinger 在等 presentFence ?
    4. 因为 SurfaceFlinger 在等 GPU fence?
  3. 如果有合成操作,那么需要看 你关心的 App 的 可用 Buffer 个数是否正常:如果 App 此时可用 Buffer 为 0,那么看 App 端为何没有及时 queueBuffer(这就一般是应用自身的问题了),因为 SurfaceFlinger 合成操作触发可能是其他的进程有可用的 Buffer

关于这一部分的 Systrace 怎么看,在 Systrace 基础知识 - Triple Buffer 解读-掉帧检测 部分已经有比较详细的解读,大家可以过去看这一段

HWComposer 部分

关于 HWComposer 的功能部分我们就直接看 官方的介绍 即可

  1. Hardware Composer HAL (HWC) 用于确定通过可用硬件来合成缓冲区的最有效方法。作为 HAL,其实现是特定于设备的,而且通常由显示设备硬件原始设备制造商 (OEM) 完成。
  2. 当您考虑使用叠加平面时,很容易发现这种方法的好处,它会在显示硬件(而不是 GPU)中合成多个缓冲区。例如,假设有一部普通 Android 手机,其屏幕方向为纵向,状态栏在顶部,导航栏在底部,其他区域显示应用内容。每个层的内容都在单独的缓冲区中。您可以使用以下任一方法处理合成(后一种方法可以显著提高效率):
    1. 将应用内容渲染到暂存缓冲区中,然后在其上渲染状态栏,再在其上渲染导航栏,最后将暂存缓冲区传送到显示硬件。
    2. 将三个缓冲区全部传送到显示硬件,并指示它从不同的缓冲区读取屏幕不同部分的数据。
  3. 显示处理器功能差异很大。叠加层的数量(无论层是否可以旋转或混合)以及对定位和叠加的限制很难通过 API 表达。为了适应这些选项,HWC 会执行以下计算(由于硬件供应商可以定制决策代码,因此可以在每台设备上实现最佳性能):
    1. SurfaceFlinger 向 HWC 提供一个完整的层列表,并询问“您希望如何处理这些层?”
    2. HWC 的响应方式是将每个层标记为叠加层或 GLES 合成。
    3. SurfaceFlinger 会处理所有 GLES 合成,将输出缓冲区传送到 HWC,并让 HWC 处理其余部分。
  4. 当屏幕上的内容没有变化时,叠加平面的效率可能会低于 GL 合成。当叠加层内容具有透明像素且叠加层混合在一起时,尤其如此。在此类情况下,HWC 可以选择为部分或全部层请求 GLES 合成,并保留合成的缓冲区。如果 SurfaceFlinger 返回来要求合成同一组缓冲区,HWC 可以继续显示先前合成的暂存缓冲区。这可以延长闲置设备的电池续航时间。
  5. 运行 Android 4.4 或更高版本的设备通常支持 4 个叠加平面。尝试合成的层数多于叠加层数会导致系统对其中一些层使用 GLES 合成,这意味着应用使用的层数会对能耗和性能产生重大影响。

——– 引用自SurfaceFlinger 和 Hardware Composer

我们继续接着看 SurfaceFlinger 主线程的部分,对应上面步骤中的第三步,下图可以看到 SurfaceFlinger 与 HWC 的通信部分
-w1149

这也对应了最上面那张图的后面部分
-w563

不过这其中的细节非常多,这里就不详细说了。至于为什么要提 HWC,因为 HWC 不仅是渲染链路上重要的一环,其性能也会影响整机的性能,Android 中的卡顿丢帧原因概述 - 系统篇 这篇文章里面就有列有 HWC 导致的卡顿问题(性能不足,中断信号慢等问题)

想了解更多 HWC 的知识,可以参考这篇文章Android P 图形显示系统(一)硬件合成HWC2,当然,作者的Android P 图形显示系这个系列大家可以仔细看一下

参考文章

  1. Android P 图形显示系统(一)硬件合成HWC2
  2. Android P 图形显示系统
  3. SurfaceFlinger 的定义
  4. surfacefliner

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远
微信扫一扫

Android Systrace 基础知识 - CPU Info 解读

作者 Gracker
2019年12月21日 15:16

本文是 Systrace 系列文章的第十二篇,主要是对 Systrace 中的 CPU 信息区域(Kernel)进行简单介绍,简单介绍了如何在 Systrace 中查看 Kernel 模块输出的 CPU 相关的信息,了解 CPU 频率、调度、锁频、锁核相关的信息

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

CPU 区域图例

下面是高通骁龙 845 手机 Systrace 对应的 Kernel 中的 CPU Info 区域(底下的一些这里不讲,主要是讲 Kernel CPU 信息)

CPU 区域图例

Systrace 中 CPU Info 一般在最上面,里面经常会用到的信息包括:

  1. CPU 频率变化情况
  2. 任务执行情况
  3. 大小核的调度情况
  4. CPU Boost 调度情况

总的来说,Systrace 中的 Kernel CPU Info 这里一般是看任务调度信息,查看是否是频率或者调度导致当前任务出现性能问题,举例如下:

  1. 某个场景的任务执行比较慢,我们就可以查看是不是这个任务被调度到了小核?
  2. 某个场景的任务执行比较慢,当前执行这个任务的 CPU 频率是不是不够?
  3. 我的任务比较特殊,比如指纹解锁,能不能把我这个任务放到大核去跑?
  4. 我这个场景对 CPU 要求很高,我能不能要求在我这个场景运行的时候,限制 CPU 最低频率?

与 CPU 运行信息相关的内容在 Systrace 基础知识 – 分析 Systrace 预备知识 这篇文章里面有详细的讲解,不熟悉的同学可以配合这篇文章一起食用

核心架构

简单来说目前的手机 CPU 按照核心数和架构来说,可以分为下面三类:

  1. 非大小核架构
  2. 大小核架构
  3. 大中小核架构

目前的大部分 CPU 都是大小核架构,当然也有一些 CPU 是大中小核架构,比如高通骁龙 855\865,也有少部分 CPU 是非大小核架构

下面就来说说各种架构的区别,方便大家后续查看 Systrace

大小核架构

非大小核架构

很早的机器 CPU 只有双核心或者四核心的时候,一般只有一种核心架构,也就是说这四个核心或者两个核心是同构的,相同的频率,相同的功耗,一起开启或者关闭;有些高通的中低端处理器也会使用同构的八核心处理器,比如高通骁龙 636

现在的大部分机器已经不使用非大小核的架构了

大小核架构

现在的 CPU 一般采用 8 核心,八个核心中,CPU 0-3 一般是小核心,CPU 4-7,如下图中 Systrace 中就是按照这个排列的

小核心一般来说主频低,功耗也低,使用的一般是 arm A5X 系列,比如高通骁龙 845,小核心是由四个 A55 (最高主频 1.8GHz ) 组成

大核心一般来说最高主频比较高,功耗相对来说也会比较高,使用的一般是 arm A7X 系列,比如高通骁龙 845,大核心就是由四个 A75(最高主频 2.8GHz)组成

下图就是 845 的 CPU

845

当然大小核架构中还有一些变种,比如高通骁龙 636 (4 小核 + 2 大核)或者高通骁龙 710 (6 小核 + 2 大核),宗旨还是不变,大核心用来支持高负载场景,小核心用来日常使用,至于够不够用,就看你舍不舍得花银子,毕竟一分价钱一分货,高通爸爸也不是做福利的

下面这些高通的主流大小核处理器的参数如下

大中小核架构

部分 CPU 比较另辟蹊径,选择了大中小核的架构,比如高通骁龙 855 8 核 (1 个 A76 的大核+3 个 A76 的中核 + 4 个 A55 的小核)和之前的的 MTK X30 10 核 (2 个 A73 的大核 + 4 个 A53 的中核 + 4 个 A35 的小核)以及麒麟 980 8 核 (2 个 A76 的大核 + 2 个 A76 的中核 + 4 个 A55 的小核)

相比大小核架构,大中小核架构中的大核可以理解为超大核(高通称之为 Gold +) ,这个超大核的个数一般比较少(1-2 个),主频一般会比较高,功耗相对也会高很多,这个是用来处理一些比较繁重的任务

下图是 855、845 和麒麟 980 的对比

顺带提一嘴,今年的高通骁龙 865 依然是大中小核的架构,大核和中核用的是 A77 架构,小核用的是 A55,大核和中核最高频率不一样,大核只有一个,主频到 2.8GHz,不知道 865 Plus 会不会搞到 3GHz

绑核

绑核,顾名思义就是把某个任务绑定到某个或者某些核心上,来满足这个任务的性能需求

  1. 任务本身负载比较高,需要在大核心上面才能满足时间要求
  2. 任务本身不想被频繁切换,需要绑定在某一个核心上面
  3. 任务本身不重要,对时间要求不高,可以绑定或者限制在小核心上面运行

上面是一些绑核的例子,目前 Android 中绑核操作一般是由系统来实现的,常用的有三种方法

配置 CPUset

使用 CPUset 子系统可以限制某一类的任务跑在特定的 CPU 或者 CPU 组里面,比如下面,Android 中会划分一些默认的 CPU 组,厂商可以针对不同的 CPU 架构进行定制,目前默认划分

  1. system-background 一些低优先级的任务会被划分到这里,只能跑到小核心里面
  2. foreground 前台进程
  3. top-app 目前正在前台和用户交互的进程
  4. background 后台进程
  5. foreground/boost 前台 boost 进程,通常是用来联动的,现在已经没有用到了,之前的时候是应用启动的时候,会把所有 foreground 里面的进程都迁移到这个进程组里面

每个 CPU 架构对应的 CPUset 的配置都不一样,每个厂商也会有不同的策略在里面,比如下面就是一个 Google 官方默认的配置,各位也可以查看对应的节点来查看自己的 CPUset 组的配置

1
2
3
4
5
6
7
8
9
//官方默认配置
write /dev/CPUset/top-app/CPUs 0-7
write /dev/CPUset/foreground/CPUs 0-7
write /dev/CPUset/foreground/boost/CPUs 4-7
write /dev/CPUset/background/CPUs 0-7
write /dev/CPUset/system-background/CPUs 0-3
// 自己查看
adb shell cat /dev/CPUset/top-app/CPUs
0-7

对应的,可以在每个 CPUset 组的 tasks 节点下面看有哪些进程和线程是跑在这个组里面的

1
2
3
4
5
$ adb shell cat /dev/CPUset/top-app/tasks
1687
1689
1690
3559

需要注意每个任务跑在哪个组里面,是动态的,并不是一成不变的,有权限的进程就可以改

部分进程也可以在启动的时候就配置好跑到哪个进程里面,下面是 lmkd 的启动配置,writepid /dev/CPUset/system-background/tasks 这一句把自己安排到了 system-background 这个组里面

1
2
3
4
5
6
7
8
service lmkd /system/bin/lmkd
class core
user lmkd
group lmkd system readproc
capabilities DAC_OVERRIDE KILL IPC_LOCK SYS_NICE SYS_RESOURCE BLOCK_SUSPEND
critical
socket lmkd seqpacket 0660 system system
writepid /dev/CPUset/system-background/tasks

大部分 App 进程是根据状态动态去变化的,在 Process 这个类中有详细的定义

android/os/Process.java

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
/**
* Default thread group -
* has meaning with setProcessGroup() only, cannot be used with setThreadGroup().
* When used with setProcessGroup(), the group of each thread in the process
* is conditionally changed based on that thread's current priority, as follows:
* threads with priority numerically less than THREAD_PRIORITY_BACKGROUND
* are moved to foreground thread group. All other threads are left unchanged.
* @hide
*/
public static final int THREAD_GROUP_DEFAULT = -1;

/**
* Background thread group - All threads in
* this group are scheduled with a reduced share of the CPU.
* Value is same as constant SP_BACKGROUND of enum SchedPolicy.
* FIXME rename to THREAD_GROUP_BACKGROUND.
* @hide
*/
public static final int THREAD_GROUP_BG_NONINTERACTIVE = 0;

/**
* Foreground thread group - All threads in
* this group are scheduled with a normal share of the CPU.
* Value is same as constant SP_FOREGROUND of enum SchedPolicy.
* Not used at this level.
* @hide
**/
private static final int THREAD_GROUP_FOREGROUND = 1;

/**
* System thread group.
* @hide
**/
public static final int THREAD_GROUP_SYSTEM = 2;

/**
* Application audio thread group.
* @hide
**/
public static final int THREAD_GROUP_AUDIO_APP = 3;

/**
* System audio thread group.
* @hide
**/
public static final int THREAD_GROUP_AUDIO_SYS = 4;

/**
* Thread group for top foreground app.
* @hide
**/
public static final int THREAD_GROUP_TOP_APP = 5;

/**
* Thread group for RT app.
* @hide
**/
public static final int THREAD_GROUP_RT_APP = 6;

/**
* Thread group for bound foreground services that should
* have additional CPU restrictions during screen off
* @hide
**/
public static final int THREAD_GROUP_RESTRICTED = 7;

在 OomAdjuster 中会动态根据进程的状态修改其对应的 CPUset 组, 详细可以自行查看 OomAdjuster 中 computeOomAdjLocked、updateOomAdjLocked、applyOomAdjLocked 的执行逻辑(Android 10)

配置 affinity

使用 affinity 也可以设置任务跑在哪个核心上,其系统调用的 taskset, taskset 用来查看和设定“CPU 亲和力”,其实就是查看或者配置进程和 CPU 的绑定关系,让某进程在指定的 CPU 核上运行,即是“绑核”。

taskset 的用法

显示进程运行的CPU

1
taskset -p pid

注意,此命令返回的是十六进制的,转换成二进制后,每一位对应一个逻辑 CPU,低位是 0 号CPU,依次类推。如果每个位置上是1,表示该进程绑定了该 CPU。例如,0101 就表示进程绑定在了 0 号和 3 号逻辑 CPU 上了

绑核设定

1
2
taskset -pc 3  pid    表示将进程pid绑定到第3个核上
taskset -c 3 command   表示执行 command 命令,并将 command 启动的进程绑定到第3个核上。

Android 中也可以使用这个系统调用,把任务绑定到某个核心上运行。部分较老的内核里面不支持 CPUset,就会用 taskset 来设置

调度算法

在 Linux 的调度算法中修改调度逻辑,也可以让指定的 task 跑在指定的核上面,部分厂家的核调度优化就是使用的这种方法,这里就不具体来讲了

锁频

正常情况下,CPU 的调度算法都可以满足日常的使用,但是在 Android 中的部分场景里面,单纯依靠调度器,可能会无法满足这个场景对性能的要求。比如说应用启动场景,如果让调度器去拉频率迁核,可能就会有一定的延迟,比如任务先在小核跑,发现小核频率不够,那就把小核频率往上拉,拉上去之后发现可能还是不够,经过几次一直拉到最高发现还是不够,然后把这个任务迁移到中核,频率也是一次一次拉,拉到最高发现还是不够,最好迁移到大核去做。这样一套下来,时间过去不少不说,启动速度也不是最快的

基于这种情况的考虑,系统中一般都会在这种特殊场景直接暴力拉核,将硬件资源直接拉到最高去运行,比如 CPU、GPU、IO、BUS 等;另外也会在某些场景把某些资源限制使用,比如发热太严重的时候,需要限制 CPU 的最高频率,来达到降温的目的;有时候基于功耗的考虑,也会限制一些资源在某些场景的使用

目前 Android 系统一般会在下面几个场景直接进行锁频(不同厂家也会自己定制)

  1. 应用启动
  2. 应用安装
  3. 转屏
  4. 窗口动画
  5. List Fling
  6. Game

以 高通平台为例,在 CPU Info 中我们也可以看到锁频的情况

CPU 状态

CPU info 中还有标识 CPU 状态的标记,如下图所示,CPU 状态有 0 ,1,2,3 这四种

之前的 CPU 支持热插拔,即不用的时候可以直接关闭,不过目前的 CPU 都不支持热插拔,而是使用 C-State

下面是摘抄的其他平台的支持 C0-C4 的处理器的状态和功耗状态,Android 中不同的平台表现不一致,大家可以做一下参考

  1. C0 状态(激活)
  2. 这是 CPU 最大工作状态,在此状态下可以接收指令和处理数据
  3. 所有现代处理器必须支持这一功耗状态
  4. C1 状态(挂起)
  5. 可以通过执行汇编指令“ HLT (挂起)”进入这一状态
  6. 唤醒时间超快!(快到只需 10 纳秒!)
  7. 可以节省 70% 的 CPU 功耗
  8. 所有现代处理器都必须支持这一功耗状态
  9. C2 状态(停止允许)
  10. 处理器时钟频率和 I/O 缓冲被停止
  11. 换言之,处理器执行引擎和 I/0 缓冲已经没有时钟频率
  12. 在 C2 状态下也可以节约 70% 的 CPU 和平台能耗
  13. 从 C2 切换到 C0 状态需要 100 纳秒以上
  14. C3 状态(深度睡眠)
  15. 总线频率和 PLL 均被锁定
  16. 在多核心系统下,缓存无效
  17. 在单核心系统下,内存被关闭,但缓存仍有效可以节省 70% 的 CPU 功耗,但平台功耗比 C2 状态下大一些
  18. 唤醒时间需要 50 微妙

Systrace 中的详细信息

Systrace 我们一般用 Chrome 打开,转换成图形化信息之后更加方便从整体去看,但其实 Systrace 也可以以文本的方式打开,也可以看到一些详细的信息。

比如下面就是一条标识 CPU 调度的 Message,解析的时候,里面的信息会被解析到各个模块

1
appEventThread-8193  [001] d..2 1638545.400415: sched_switch: prev_comm=appEventThread prev_pid=8193 prev_prio=97 prev_state=S ==> next_comm=swapper/1 next_pid=0 next_prio=120

详细来看

1
2
3
4
5
appEventThread-8193    -- 标识 TASK-PID
[001] -- 标识是哪个 CPU ,这里是 cpu0
d..2 -- 这是四个位,每个位分别对应 irqs-off、need-resched、hardirq/softirq、preempt-depth
1638545.400415 -- 标识 delay TIMESTAMP
sched_switch ...到最后 -- 标识信息区,里面包含前一个任务描述,前一个任务的 pid,前一个任务的优先级 ,当前任务,当前任务 pid,当前任务优先级

另外里面仔细看也可以看到许多有趣的输出,可以加深对调度的理解

  1. sched_waking: comm=kworker/u16:4 pid=17373 prio=120 target_cpu=003
  2. sched_blocked_reason: pid=17373 iowait=0 caller=rpmh_write_batch+0x638/0x7d0
  3. cpu_idle: state=0 cpu_id=3
  4. softirq_raise: vec=6 [action=TASKLET]
  5. cpu_frequency_limits: min=1555200 max=1785600 cpu_id=0
  6. cpu_frequency_limits: min=710400 max=2419200 cpu_id=4
  7. cpu_frequency_limits: min=825600 max=2841600 cpu_id=7

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

参考

  1. 绑定CPU逻辑核心的利器——taskset
  2. CPU 电源状态

春笋

拍了张照片,觉得还不错,分享给大家

Android Systrace 基础知识 - Triple Buffer 解读

作者 Gracker
2019年12月15日 23:31

本文是 Systrace 系列文章的第十一篇,主要是对 Systrace 中的 Triple Buffer 进行简单介绍,简单介绍了如何在 Systrace 中判断卡顿情况的发生,进行初步的定位和分析,以及介绍 Triple Buffer 的引入对性能的影响

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

怎么定义掉帧?

Systrace 中可以看到应用的掉帧情况,我们经常看到说主线程超过 16.6 ms 就会掉帧,其实不然,这和我们这一篇文章讲到的 Triple Buffer 和一定的关系,一般来说,Systrace 中我们从 App 端和 SurfaceFlinger 端一起来判断掉帧情况

App 端判断掉帧

如果之前没有看过 Systrace 的话,仅仅从理论上来说,下面这个 Trace 中的应用是掉帧了,其主线程的绘制时间超过了 16.6ms ,但其实不一定,因为 BufferQueue 和 TripleBuffer 的存在,此时 BufferQueue 中可能还有上一帧或者上上一帧准备好的 Buffer,可以直接被 SurfaceFlinger 拿去做合成,当然也可能没有

所以从 Systrace 的 App 端我们是无法直接判断是否掉帧的,需要从 Systrace 里面的 SurfaceFlinger 端去看

SurfaceFlinger 端判断掉帧

SurfaceFlinger 端可以看到 SurfaceFlinger 主线程和合成情况和应用对应的 BufferQueue 中 Buffer 的情况。如上图,就是一个掉帧的例子。App 没有及时渲染完成,且此时 BufferQueue 中也没有前几帧的 Buffer,所以这一帧 SurfaceFlinger 没有合成对应 App 的 Layer,在用户看来这里就掉了一帧

而在第一张图中我们说从 App 端无法看出是否掉帧,那张图对应的 SurfaceFlinger 的 Trace 如下, 可以看到由于有 Triple Buffer 的存在, SF 这里有之前 App 的 Buffer,所以尽管 App 测一帧超过了 16.6 ms, 但是 SF 这里依然有可用来合成的 Buffer, 所以没有掉帧

SurfaceFlinger

逻辑掉帧

上面的掉帧我们是从渲染这边来看的,这种掉帧在 Systrace 中可以很容易就发现;还存在一种掉帧情况叫逻辑掉帧

逻辑掉帧指的是由于应用自己的代码逻辑问题,导致画面更新的时候,不是以均匀或者物理曲线的方式,而是出现跳跃更新的情况,这种掉帧一般在 Systrace 上没法看出来,但是用户在使用的时候可以明显感觉到

举一个简单的例子,比如说列表滑动的时候,如果我们滑动松手后列表的每一帧前进步长是一个均匀变化的曲线,最后趋近于 0,这样就是完美的;但是如果出现这一帧相比上一帧走了 20,下一帧相比这一帧走了 10,下下一帧相比下一帧走了 30,这种就是跳跃更新,在 Systrace 上每一帧都是及时渲染且 SurfaceFlinger 都及时合成的,但是用户用起来就是觉得会卡. 不过我列举的这个例子中,Android 已经针对这种情况做了优化,感兴趣的可以去看一下 android/view/animation/AnimationUtils.java 这个类,重点看下面三个方法的使用

1
2
3
public static void lockAnimationClock(long vsyncMillis)
public static void unlockAnimationClock()
public static long currentAnimationTimeMillis()

Android 系统的动画一般不会有这个问题,但是应用开发者就保不齐会写这种代码,比如做动画的时候根据**当前的时间(而不是 Vsync 到来的时间)**来计算动画属性变化的情况,这种情况下,一旦出现掉帧,动画的变化就会变得不均匀,感兴趣的可以自己思考一下这一块

另外 Android 出现掉帧情况的原因非常多,各位可以参考下面三篇文章食用:

  1. Android 中的卡顿丢帧原因概述 - 方法论
  2. Android 中的卡顿丢帧原因概述 - 系统篇
  3. Android 中的卡顿丢帧原因概述 - 应用篇

BufferQueue 和 Triple Buffer

BufferQueue

首先看一下 BufferQueue,BufferQueue 是一个生产者(Producer)-消费者(Consumer)模型中的数据结构,一般来说,消费者(Consumer) 创建 BufferQueue,而生产者(Producer) 一般不和 BufferQueue 在同一个进程里面

其运行逻辑如下

  1. 当生产者(Producer) 需要 Buffer 时,它通过调用 dequeueBuffer()并指定 Buffer 的宽度,高度,像素格式和使用标志,从 BufferQueue 请求释放 Buffer
  2. 生产者(Producer) 将填充缓冲区,并通过调用 queueBuffer()将缓冲区返回到队列。
  3. 消费者(Consumer) 使用 acquireBuffer()获取 Buffer 并消费 Buffer 的内容
  4. 使用完成后,消费者(Consumer)将通过调用 releaseBuffer()将 Buffer 返回到队列

Android 通过 Vsync 机制来控制 Buffer 在 BufferQueue 中的流动时机,如果对 Vsync 机制不了解,可以参考下面这两篇文章,看完后你会有个大概的了解

  1. Systrace 基础知识 - Vsync 解读
  2. Android 基于 Choreographer 的渲染机制详解

上面的流程比较抽象,这里举一个具体的例子,方便大家理解上面那张图,对后续了解 Systrace 中的 BufferQueue 也会有帮助。

在 Android App 的渲染流程里面,App 就是个生产者(Producer) ,而 SurfaceFlinger 是一个消费者(Consumer),所以上面的流程就可以翻译为

  1. App 需要 Buffer 时,它通过调用 dequeueBuffer()并指定 Buffer 的宽度,高度,像素格式和使用标志,从 BufferQueue 请求释放 Buffer
  2. App 可以用 cpu 进行渲染也可以调用用 gpu 来进行渲染,渲染完成后,通过调用 queueBuffer()将缓冲区返回到 App 对应的 BufferQueue(如果是 gpu 渲染的话,这里还有个 gpu 处理的过程)
  3. SurfaceFlinger 在收到 Vsync 信号之后,开始准备合成,使用 acquireBuffer()获取 App 对应的 BufferQueue 中的 Buffer 并进行合成操作
  4. 合成结束后,SurfaceFlinger 将通过调用 releaseBuffer()将 Buffer 返回到 App 对应的 BufferQueue

理解了 BufferQueue 的作用后,接下来来讲解一下 BufferQueue 中的 Buffer

从上面的图可以看到,BufferQueue 中的生产者和消费者通过 dequeueBuffer、queueBuffer、acquireBuffer、releaseBuffer 来申请或者释放 Buffer,那么 BufferQueue 中需要几个 Buffer 来进行运转呢?下面从单 Buffer,双 Buffer 和 Triple Buffer 的角度分析(注意这里只是从 Buffer 的角度来做分析的, 比如 App 测涉及到 Buffer 的是 RenderThread , 不过由于 RenderThread 与 MainThread 有一定的联系, 比如 unBlockUiThread 执行的时机, MainThread 也会因为 RenderThread 执行慢而被 Block 住)

Single Buffer

单 Buffer 的情况下,因为只有一个 Buffer 可用,那么这个 Buffer 既要用来做合成显示,又要被应用拿去做渲染

Single Buffer

理想情况下,单 Buffer 是可以完成任务的(有 Vsync-Offset 存在的情况下)

  1. App 收到 Vsync 信号,获取 Buffer 开始渲染
  2. 间隔 Vsync-Offset 时间后,SurfaceFlinger 收到 Vsync 信号,开始合成
  3. 屏幕刷新,我们看到合成后的画面

Single Buffer

但是很不幸,理想情况我们也就想一想,这期间如果 App 渲染或者 SurfaceFlinger 合成在屏幕显示刷新之前还没有完成,那么屏幕刷新的时候,拿到的 Buffer 就是不完整的,在用户看来,就有种撕裂的感觉

Single Buffer

当然 Single Buffer 已经没有在使用,上面只是一个例子

Double Buffer

Double Buffer 相当于 BufferQueue 中有两个 Buffer 可供轮转,消费者在消费 Buffer的同时,生产者也可以拿到备用的 Buffer 进行生产操作

Double Buffer

下面我们来看理想情况下,Double Buffer 的工作流程

DoubleBufferPipline_NoJank

但是 Double Buffer 也会存在性能上的问题,比如下面的情况,App 连续两帧生产都超过 Vsync 周期(准确的说是错过 SurfaceFlinger 的合成时机) ,就会出现掉帧情况

Double Buffer

Triple Buffer

Triple Buffer 中,我们又加入了一个 BackBuffer ,这样的话 BufferQueue 里面就有三个 Buffer 可以轮转了,当 FrontBuffer 在被使用的时候,App 有两个空闲的 Buffer 可以拿去生产,就算生产过程中有 GPU 超时,CPU 任然可以拿到新的 Buffer 进行生产(即 SurfaceFling 消费 FrontBuffer,GPU 使用一个 BackBuffer,CPU使用一个 BackBuffer)

Triple Buffer

下面就是引入 Triple Buffer 之后,解决了 Double Buffer 中遇到的由于 Buffer 不足引起的掉帧问题

TripleBufferPipline_NoJank

这里把两个图放到一起来看,方便大家做对比(一个是 Double Buffer 掉帧两次,一个是使用 Triple Buffer 只掉了一帧)

TripleBuffer_VS_DoubleBuffer

Triple Buffer 的作用

缓解掉帧

从上一节 Double Buffer 和 Triple Buffer 的对比图可以看到,在这种情况下(出现连续主线程超时),三个 Buffer 的轮转有助于缓解掉帧出现的次数(从掉帧两次 -> 只掉帧一次)

所以从第一节如何定义掉帧这里我们就知道,App 主线程超时不一定会导致掉帧,由于 Triple Buffer 的存在,部分 App 端的掉帧(主要是由于 GPU 导致),到 SurfaceFlinger 这里未必是掉帧,这是看 Systrace 的时候需要注意的一个点

缓解掉帧

减少主线程和渲染线程等待时间

双 Buffer 的轮转, App 主线程有时候必须要等待 SurfaceFlinger(消费者)释放 Buffer 后,才能获取 Buffer 进行生产,这时候就有个问题,现在大部分手机 SurfaceFlinger 和 App 同时收到 Vsync 信号,如果出现App 主线程等待 SurfaceFlinger(消费者)释放 Buffer ,那么势必会让 App 主线程的执行时间延后,比如下面这张图,可以明显看到:Buffer B 并不是在 Vsync 信号来的时候开始被消费(因为还在使用),而是等 Buffer A 被消费后,Buffer B 被释放,App 才能拿到 Buffer B 进行生产,这期间就有一定的延迟,会让主线程可用的时间变短

减少主线程和渲染线程等待时间

我们来看一下在 Systrace 中的上面这种情况发生的时候的表现

减少主线程和渲染线程等待时间

而 三个 Buffer 轮转的情况下,则基本不会有这种情况的发生,渲染线程一般在 dequeueBuffer 的时候,都可以顺利拿到可用的 Buffer (当然如果 dequeueBuffer 本身耗时那就不是这里的讨论范围了)

降低 GPU 和 SurfaceFlinger 瓶颈

这个比较好理解,双 Buffer 的时候,App 生产的 Buffer 必须要及时拿去让 GPU 进行渲染,然后 SurfaceFlinger 才能进行合成,一旦 GPU 超时,就很容易出现 SurfaceFlinger 无法及时合成而导致掉帧

在三个 Buffer 轮转的时候,App 生产的 Buffer 可以及早进入 BufferQueue,让 GPU 去进行渲染(因为不需要等待,就算这里积累了 2 个 Buffer,下下一帧才去合成,这里也会提早进行,而不是在真正使用之前去匆忙让 GPU 去渲染),另外 SurfaceFlinger 本身的负载如果比较大,三个 Buffer 轮转也会有效降低 dequeueBuffer 的等待时间

比如下面两张图,就是对应的 SurfaceFlinger 和 App 的双 Buffer 掉帧情况,由于 SurfaceFlinger 本身就比较耗时(特定场景),而 App 的 dequeueBuffer 得不到及时的响应,导致发生了比较严重的掉帧情况。在换成 Triple Buffer 之后,这种情况就基本上没有了

Debug Triple Buffer

Dumpsys SurfaceFlinger

dumpsys SurfaceFlinger 可以查看 SurfaceFlinger 输出的众多当前的状态,比如一些性能指标、Buffer 状态、图层信息等,后续有篇幅的话可以单独拿出来讲,下面是截取的 Double Buffer 情况下和 Triple Buffer 情况下的各个 App 的 Buffer 使用情况,可以看到不同的 App,在负载不一样的情况下,对 Triple Buffer 的使用率是不一样的;Double Buffer 则完全使用的是双 Buffer

关闭 Triple Buffer

不同 Android 版本属性设置不一样(这是 Google 的一个逻辑 Bug,Android 10 上面已经修复了)

Android 版本 <= Android P

1
2
3
4
//控制代码
property_get("ro.sf.disable_triple_buffer", value, "1");
mLayerTripleBufferingDisabled = atoi(value);
ALOGI_IF(mLayerTripleBufferingDisabled, "Disabling Triple Buffering");

修改对应的属性值,然后重启 Framework

1
2
3
4
//按顺序执行下面的语句(需要 Root 权限)
adb root
adb shell setprop ro.sf.disable_triple_buffer 0
adb shell stop && adb shell start

Android 版本 > Android P

1
2
3
4
//控制代码
property_get("ro.sf.disable_triple_buffer", value, "0");
mLayerTripleBufferingDisabled = atoi(value);
ALOGI_IF(mLayerTripleBufferingDisabled, "Disabling Triple Buffering");

修改对应的属性值,然后重启 Framework

1
2
3
4
//按顺序执行下面的语句(需要 Root 权限)
adb root
adb shell setprop ro.sf.disable_triple_buffer 1
adb shell stop && adb shell start

参考

  1. https://source.android.google.cn/devices/graphics

附件

本文涉及到的附件也上传了,各位下载后解压,使用 Chrome 浏览器打开即可
点此链接下载文章所涉及到的 Systrace 附件

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远
微信扫一扫

Android Systrace 基础知识 - Binder 和锁竞争解读

作者 Gracker
2019年12月6日 19:12

本文是 Systrace 系列文章的第十篇,主要是对 Systrace 中的 Binder 和锁信息进行简单介绍,简单介绍了 Binder 的情况,介绍了 Systrace 中 Binder 通信的表现形式,以及 Binder 信息查看,SystemServer 锁竞争分析等

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

Binder 概述

Android 的大部分进程间通信都使用 Binder,这里对 Binder 不做过多的解释,想对 Binder 的实现有一个比较深入的了解的话,推荐你阅读下面三篇文章

  1. 理解Android Binder机制1/3:驱动篇
  2. 理解Android Binder机制2/3:C++层
  3. 理解Android Binder机制3/3:Java层

之所以要单独讲 Systrace 中的 Binder 和锁,是因为很多卡顿问题和响应速度的问题,是因为跨进程 binder 通信的时候,锁竞争导致 binder 通信事件变长,影响了调用端。最常见的就是应用渲染线程 dequeueBuffer 的时候 SurfaceFlinger 主线程阻塞导致 dequeueBuffer 耗时,从而导致应用渲染出现卡顿; 或者 SystemServer 中的 AMS 或者 WMS 持锁方法等待太多, 导致应用调用的时候等待时间比较长导致主线程卡顿

这里放一张文章里面的 Binder 架构图 , 本文主要是以 Systrace 为主,所以会讲 Systrace 中的 Binder 表现,不涉及 Binder 的实现

Binder 调用图例

Binder 主要是用来跨进程进行通信,可以看下面这张图,简单显示了在 Systrace 中 ,Binder 通信是如何显示的

图中主要是 SystemServer 进程和 高通的 perf 进程通信,Systrace 中右上角 ViewOption 里面勾选 Flow Events 就可以看到 Binder 的信息

点击 Binder 可以查看其详细信息,其中有的信息在分析问题的时候可以用到,这里不做过多的描述

对于 Binder,这里主要介绍如何在 Systrace 中查看 Binder 锁信息锁等待这两个部分,很多卡顿和响应问题的分析,都离不开这两部分信息的解读,不过最后还是要回归代码,找到问题后,要读源码来理顺其代码逻辑,以方便做相应的优化工作

Systrace 显示的锁的信息

monitor contention with owner Binder:1605_B (4667) at void com.android.server.wm.ActivityTaskManagerService.activityPaused(android.os.IBinder)(ActivityTaskManagerService.java:1733) waiters=2 blocking from android.app.ActivityManager$StackInfo com.android.server.wm.ActivityTaskManagerService.getFocusedStackInfo()(ActivityTaskManagerService.java:2064)

上面的话分两段来看,以 blocking 为分界线 

第一段信息解读

monitor contention with owner Binder:1605_B (4667) at void com.android.server.wm.ActivityTaskManagerService.activityPaused(android.os.IBinder)(ActivityTaskManagerService.java:1733) waiters=2

Monitor 指的是当前锁对象的池,在 Java 中,每个对象都有两个池,锁(monitor)池和等待池:

锁池(同步队列 SynchronizedQueue ):假设线程 A 已经拥有了某个对象(注意:不是类 )的锁,而其它的线程想要调用这个对象的某个 synchronized 方法(或者 synchronized 块),由于这些线程在进入对象的 synchronized 方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程 A 拥有,所以这些线程就进入了该对象的锁池中。

这里用了争夺(contention)这个词,意思是这里由于在和目前对象的锁正被其他对象(Owner)所持有,所以没法得到该对象的锁的拥有权,所以进入该对象的锁池

Owner : 指的是当前拥有这个对象的锁的对象。这里是 Binder:1605_B,4667 是其线程 ID。

at 后面跟的是拥有这个对象的锁的对象正在做什么。这里是在执行 void com.android.server.wm.ActivityTaskManagerService.activityPaused 这个方法,其代码位置是 :ActivityTaskManagerService.java:1733 其对应的代码如下:

com/android/server/wm/ActivityTaskManagerService.java

1
2
3
4
5
6
7
8
9
10
11
@Override
public final void activityPaused(IBinder token) {
final long origId = Binder.clearCallingIdentity();
synchronized (mGlobalLock) { // 1733 是这一行
ActivityStack stack = ActivityRecord.getStackLocked(token);
if (stack != null) {
stack.activityPausedLocked(token, false);
}
}
Binder.restoreCallingIdentity(origId);
}

可以看到这里 synchronized (mGlobalLock) ,获取了 mGlobalLock 锁的拥有权,在他释放这个对象的锁之前,任何其他的调用 synchronized (mGlobalLock) 的地方都得在锁池中等待

waiters 值得是锁池里面正在等待锁的操作的个数;这里 waiters=2 表示目前锁池里面已经有一个操作在等待这个对象的锁释放了,加上这个的话就是 3 个了

第二段信息解读

blocking from android.app.ActivityManager$StackInfo com.android.server.wm.ActivityTaskManagerService.getFocusedStackInfo()(ActivityTaskManagerService.java:2064)

第二段信息相对来说简单一些,就是标识了当前被阻塞等锁的方法 , 这里是 ActivityManager 的 getFocusedStackInfo 被阻塞,其对应的代码

com/android/server/wm/ActivityTaskManagerService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public ActivityManager.StackInfo getFocusedStackInfo() throws RemoteException {
enforceCallerIsRecentsOrHasPermission(MANAGE_ACTIVITY_STACKS, "getStackInfo()");
long ident = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) { // 2064 是这一行
ActivityStack focusedStack = getTopDisplayFocusedStack();
if (focusedStack != null) {
return mRootActivityContainer.getStackInfo(focusedStack.mStackId);
}
return null;
}
} finally {
Binder.restoreCallingIdentity(ident);
}
}

可以看到这里也是调用了 synchronized (ActivityManagerService.this) ,从而需要等待获取 ams 对象的锁拥有权

总结

上面这段话翻译过来就是

ActivityTaskManagerService 的 getFocusedStackInfo 方法在执行过程中被阻塞,原因是因为执行同步方法块的时候,没有拿到同步对象的锁的拥有权;需要等待拥有同步对象的锁拥有权的另外一个方法 ActivityTaskManagerService.activityPaused 执行完成后,才能拿到同步对象的锁的拥有权,然后继续执行

可以对照原文看上面的翻译

monitor contention with owner Binder:1605_B (4667)
at void com.android.server.wm.ActivityTaskManagerService.activityPaused(android.os.IBinder)(ActivityTaskManagerService.java:1733)
waiters=2
blocking from android.app.ActivityManager$StackInfo com.android.server.wm.ActivityTaskManagerService.getFocusedStackInfo()(ActivityTaskManagerService.java:2064)

等锁分析

还是上面那个 Systrace,Binder 信息里面显示 waiters=2,意味着前面还有两个操作在等锁释放,也就是说总共有三个操作都在等待 Binder:1605_B (4667) 释放锁,我们来看一下 Binder:1605_B 的执行情况

从上图可以看到,Binder:1605_B 正在执行 activityPaused,中间也有一些其他的 Binder 操作,最终 activityPaused 执行完成后,释放锁

下面我们就把这个逻辑里面的执行顺序理顺,包括两个 waiters

锁等待

上图中可以看到 mGlobalLock 这个对象锁的争夺情况

  1. Binder_1605_B 首先开始执行 activityPaused,这个方法中是要获取 mGlobalLock 对象锁的,由于此时 mGlobalLock 没有竞争,所以 activityPaused 获取对象锁之后开始执行
  2. android.display 线程开始执行 checkVisibility 方法,这个方法也是要获取 mGlobalLock 对象锁的,但是此时 Binder_1605_B 的 activityPaused 持有 mGlobalLock 对象锁 ,所以这里 android.display 的 checkVisibility 开始等待,进入 sleep 状态
  3. android.anim 线程开始执行 relayoutWindow 方法,这个方法也是要获取 mGlobalLock 对象锁的,但是此时 Binder_1605_B 的 activityPaused 持有 mGlobalLock 对象锁 ,所以这里 android.display 的 checkVisibility 开始等待,进入 sleep 状态
  4. android.bg 线程开始执行 getFocusedStackInfo 方法,这个方法也是要获取 mGlobalLock 对象锁的,但是此时 Binder_1605_B 的 activityPaused 持有 mGlobalLock 对象锁 ,所以这里 android.display 的 checkVisibility 开始等待,进入 sleep 状态

经过上面四步,就形成了 Binder_1605_B 线程在运行,其他三个争夺 mGlobalLock 对象锁失败的线程分别进入 sleep 状态,等待 Binder_1605_B 执行结束后释放 mGlobalLock 对象锁

锁释放

上图可以看到 mGlobalLock 锁的释放和后续的流程

  1. Binder_1605_B 线程的 activityPaused 执行结束,mGlobalLock 对象锁释放
  2. 第一个进入等待的 android.display 线程开始执行 checkVisibility 方法 ,这里从 android.display 线程的唤醒信息可以看到,是被 Binder_1605_B(4667) 唤醒的
  3. android.display 线程的 checkVisibility 执行结束,mGlobalLock 对象锁释放
  4. 第二个进入等待的 android.anim 线程开始执行 relayoutWindow 方法 ,这里从 android.anim 线程的唤醒信息可以看到,是被 android.display(1683) 唤醒的
  5. android.anim 线程的 relayoutWindow 执行结束,mGlobalLock 对象锁释放
  6. 第三个进入等待的 android.bg 线程开始执行 getFocusedStackInfo 方法 ,这里从 android.bg 线程的唤醒信息可以看到,是被 android.anim(1684) 唤醒的

经过上面 6 步,这一轮由于 mGlobalLock 对象锁引起的等锁现象结束。这里只是一个简单的例子,在实际情况下,SystemServer 中的 BInder 等锁情况会非常严重,经常 waiter 会到达 7 - 10 个,非常恐怖,比如下面这种:

这也就可以解释为什么 Android 手机 App 安装多了、用的久了之后,系统就会卡的一个原因;另外重启后也会有短暂的时候出现这种情况

如果不知道怎么查看唤醒信息,可以查看: Systrace中查看进程信息唤醒 这篇文章

相关代码

Monitor 信息

art/runtime/monitor.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
std::string Monitor::PrettyContentionInfo(const std::string& owner_name,
pid_t owner_tid,
ArtMethod* owners_method,
uint32_t owners_dex_pc,
size_t num_waiters) {
Locks::mutator_lock_->AssertSharedHeld(Thread::Current());
const char* owners_filename;
int32_t owners_line_number = 0;
if (owners_method != nullptr) {
TranslateLocation(owners_method, owners_dex_pc, &owners_filename, &owners_line_number);
}
std::ostringstream oss;
oss << "monitor contention with owner " << owner_name << " (" << owner_tid << ")";
if (owners_method != nullptr) {
oss << " at " << owners_method->PrettyMethod();
oss << "(" << owners_filename << ":" << owners_line_number << ")";
}
oss << " waiters=" << num_waiters;
return oss.str();
}

Block 信息

art/runtime/monitor.cc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (ATRACE_ENABLED()) {
if (owner_ != nullptr) { // Did the owner_ give the lock up?
std::ostringstream oss;
std::string name;
owner_->GetThreadName(name);
oss << PrettyContentionInfo(name,
owner_->GetTid(),
owners_method,
owners_dex_pc,
num_waiters);
// Add info for contending thread.
uint32_t pc;
ArtMethod* m = self->GetCurrentMethod(&pc);
const char* filename;
int32_t line_number;
TranslateLocation(m, pc, &filename, &line_number);
oss << " blocking from "
<< ArtMethod::PrettyMethod(m) << "(" << (filename != nullptr ? filename : "null")
<< ":" << line_number << ")";
ATRACE_BEGIN(oss.str().c_str());
started_trace = true;
}
}

参考

  1. 理解Android Binder机制1/3:驱动篇
  2. 理解Android Binder机制2/3:C++层
  3. 理解Android Binder机制3/3:Java层

附件

本文涉及到的附件也上传了,各位下载后解压,使用 Chrome 浏览器打开即可
点此链接下载文章所涉及到的 Systrace 附件

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远
微信扫一扫

Android Systrace 基础知识 - Vsync 解读

作者 Gracker
2019年12月1日 06:38

本文是 Systrace 系列文章的第七篇,主要是是介绍 Android 中的 Vsync 机制。文章会从 Systrace 的角度来看 Android 系统如何基于 Vsync 每一帧的展示。Vsync 是 Systrace 中一个非常关键的机制,虽然我们在操作手机的时候看不见,摸不着,但是在 Systrace 中我们可以看到,Android 系统在 Vsync 信号的指引下,有条不紊地进行者每一帧的渲染、合成操作,使我们可以享受稳定帧率的画面。

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

正文

Vsync 信号可以由硬件产生,也可以用软件模拟,不过现在基本上都是硬件产生,负责产生硬件 Vsync 的是 HWC,HWC 可生成 VSYNC 事件并通过回调将事件发送到 SurfaceFlinge , DispSync 将 Vsync 生成由 Choreographer 和 SurfaceFlinger 使用的 VSYNC_APP 和 VSYNC_SF 信号

Android 基于 Choreographer 的渲染机制详解 这篇文章里面,我们有提到 :Choreographer 的引入,主要是配合 Vsync,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机. 目前大部分手机都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每个 16.6 ms,Vsync 信号唤醒 Choreographer 来做 App 的绘制操作 ,这就是引入 Choreographer 的主要作用

渲染层(App)与 Vsync 打交道的是 Choreographer,而合成层与 Vsync 打交道的,则是 SurfaceFlinger。SurfaceFlinger 也会在 Vsync 到来的时候,将所有已经准备好的 Surface 进行合成操作

下图显示在 Systrace 中,SurfaceFlinger 进程中的 VSYNC_APP 和 VSYNC_SF 的情况

Android 图形数据流向

首先我们要大概了解 Android 中的图形数据流的方向,从下面这张图,结合 Android 的图像流,我们大概把从 App 绘制到屏幕显示,分为下面几个阶段:

  1. 第一阶段:App 在收到 Vsync-App 的时候,在主线程进行 measure、layout、draw(构建 DisplayList , 里面包含 OpenGL 渲染需要的命令及数据) 。这里对应的 Systrace 中的主线程 doFrame 操作
  2. 第二阶段:CPU 将数据上传(共享或者拷贝)给 GPU, 这里 ARM 设备 内存一般是 GPU 和 CPU 共享内存。这里对应的 Systrace 中的渲染线程的 flush drawing commands 操作
  3. 第三阶段:通知 GPU 渲染,真机一般不会阻塞等待 GPU 渲染结束,CPU 通知结束后就返回继续执行其他任务,使用 Fence 机制辅助 GPU CPU 进行同步操作
  4. 第四 阶段:swapBuffers,并通知 SurfaceFlinger 图层合成。这里对应的 Systrace 中的渲染线程的 eglSwapBuffersWithDamageKHR 操作
  5. 第五阶段:SurfaceFlinger 开始合成图层,如果之前提交的 GPU 渲染任务没结束,则等待 GPU 渲染完成,再合成(Fence 机制),合成依然是依赖 GPU,不过这就是下一个任务了.这里对应的 Systrace 中的 SurfaceFlinger 主线程的 onMessageReceived 操作(包括 handleTransaction、handleMessageInvalidate、handleMessageRefresh)SurfaceFlinger 在合成的时候,会将一些合成工作委托给 Hardware Composer,从而降低来自 OpenGL 和 GPU 的负载,只有 Hardware Composer 无法处理的图层,或者指定用 OpenGL 处理的图层,其他的 图层偶会使用 Hardware Composer 进行合成
  6. 第六阶段 :最终合成好的数据放到屏幕对应的 Frame Buffer 中,固定刷新的时候就可以看到了

下面这张图也是官方的一张图,结合上面的阶段,从左到右看,可以看到一帧的数据是如何在各个进程之间流动的

Systrace 中的图像数据流

了解了 Android 中的图形数据流的方向,我们就可以把上面这个比较抽象的数据流图,在 Systrace 上进行映射展示

上图中主要包含 SurfaceFlinger、App 和 hwc 三个进程,下面就来结合图中的标号,来进一步说明数据的流向

  1. 第一个 Vsync 信号到来, SurfaceFlinger 和 App 同时收到 Vsync 信号
  2. SurfaceFlinger 收到 Vsync-sf 信号,开始进行 App 上一帧的 Buffer 的合成
  3. App 收到 Vsycn-app 信号,开始进行这一帧的 Buffer 的渲染(对应上面的第一、二、三、四阶段)
  4. 第二个 Vsync 信号到来 ,SurfaceFlinger 和 App 同时收到 Vsync 信号,SurfaceFlinger 获取 App 在第二步里面渲染的 Buffer,开始合成(对应上面的第五阶段),App 收到 Vsycn-app 信号,开始新一帧的 Buffer 的渲染(对应上面的第一、二、三、四阶段)

Vsync Offset

文章最开始有提到,Vsync 信号可以由硬件产生,也可以用软件模拟,不过现在基本上都是硬件产生,负责产生硬件 Vsync 的是 HWC,HWC 可生成 VSYNC 事件并通过回调将事件发送到 SurfaceFlinge , DispSync 将 Vsync 生成由 Choreographer 和 SurfaceFlinger 使用的 VSYNC_APP 和 VSYNC_SF 信号.

disp_sync_arch

其中 app 和 sf 相对 hw_vsync_0 都有一个偏移,即 phase-app 和 phase-sf,如下图

Vsync Offset 我们指的是 VSYNC_APP 和 VSYNC_SF 之间有一个 Offset,即上图中 phase-sf - phase-app 的值,这个 Offset 是厂商可以配置的。如果 Offset 不为 0,那么意味着 App 和 SurfaceFlinger 主进程不是同时收到 Vsync 信号,而是间隔 Offset (通常在 0 - 16.6ms 之间)

目前大部分厂商都没有配置这个 Offset,所以 App 和 SurfaceFlinger 是同时收到 Vsync 信号的.

可以通过 Dumpsys SurfaceFlinger 来查看对应的值

Offset 为 0:(sf phase - app phase = 0)

1
2
3
4
5
6
Sync configuration: [using: EGL_ANDROID_native_fence_sync EGL_KHR_wait_sync]
DispSync configuration:
app phase 1000000 ns, sf phase 1000000 ns
early app phase 1000000 ns, early sf phase 1000000 ns
early app gl phase 1000000 ns, early sf gl phase 1000000 ns
present offset 0 ns refresh 16666666 ns

Offset 不为 0 (SF phase - app phase = 4 ms)

1
2
3
4
5
6
7
Sync configuration: [using: EGL_ANDROID_native_fence_sync EGL_KHR_wait_sync]

VSYNC configuration:
         app phase:   2000000 ns         SF phase:   6000000 ns
   early app phase:   2000000 ns   early SF phase:   6000000 ns
GL early app phase:   2000000 nsGL early SF phase:   6000000 ns
    present offset:         0 ns     VSYNC period:  16666666 ns

下面以 Systrace 为例,来看 Offset 在 Systrace 中的表现

Offset 为 0

首先说 Offset 为 0 的情况, 此时 App 和 SurfaceFlinger 是同时收到 Vsync 信号 , 其对应的 Systrace 图如下:

这个图上面也有讲解,这里就不再详细说明,大家只需要看到,App 渲染好的 Buffer,要等到下一个 Vsync-SF 来的时候才会被 SurfaceFlinger 拿去做合成,这个时间大概在 16.6 ms。这时候大家可能会想,如果 App 的 Buffer 渲染结束,Swap 到 BufferQueue 中 ,就触发 SurfaceFlinger 去做合成,那岂不是省了一些时间(0-16.6ms )?

答案是可行的,这也就引入了 Offset 机制,在这种情况下,App 先收到 Vsync 信号,进行一帧的渲染工作,然后过了 Offset 时间后,SurfaceFlinger 才收到 Vsync 信号开始合成,这时候如果 App 的 Buffer 已经 Ready 了,那么 SurfaceFlinger 这一次合成就可以包含 App 这一帧,用户也会早一点看到。

Offset 不为 0

下图中,就是一个 Offset 为 4ms 的案例,App 收到 Vsync 4 ms 之后,SurfaceFlinger 才收到 Vsync 信号

Offset 的优缺点

Offset 的一个比较难以确定的点就在于 Offset 的时间该如何设置,这也是众多厂商默认都不进行配置 Offset 的一个原因,其优缺点是动态的,与机型的性能和使用场景有很大的关系

  1. 如果 Offset 配置过短,那么可能 App 收到 Vsync-App 后还没有渲染完成,SurfaceFlinger 就收到 Vsync-SF 开始合成,那么此时如果 App 的 BufferQueue 中没有之前累积的 Buffer,那么 SurfaceFlinger 这次合成就不会有 App 的东西在里面,需要等到下一个 Vsync-SF 才能合成这次 App 的内容,时间相当于变成了 Vsync 周期+Offset,而不是我们期待的 Offset
  2. 如果 Offset 配置过长,就起不到作用了

HW_Vsync

这里需要说明的是,不是每次申请 Vsync 都会由硬件产生 Vsync,只有此次请求 vsync 的时间距离上次合成时间大于 500ms,才会通知 hwc,请求 HW_VSYNC

以桌面滑动为例,看 SurfaceFlinger 的进程 Trace 可以看到 HW_VSYNC 的状态

后续 App 申请 Vsync 时候,会有两种情况,一种是有 HW_VSYNC 的情况,一种是没有有 HW_VSYNC 的情况

不使用HW_VSYNC

使用 HW_VSYNC

HW_VSYNC 主要是利用最近的硬件 VSYNC 来做预测,最少要 3 个,最多是 32 个,实际上要用几个则不一定, DispSync 拿到 6 个 VSYNC 后就会计算出 SW_VSYNC,只要收到的 Present Fence 没有超过误差,硬件 VSYNC 就会关掉,不然会继续接收硬件 VSYNC 计算 SW_VSYNC 的值,直到误差小于 threshold.关于这一块的计算具体过程,可以参考这篇文章: S W-VS YN C 的生成与传递 ,关于这一块的流程大家也可以参考这篇文章,里面有更细节的内容,这里摘录了他的结论

SurfaceFlinger 通过实现了 HWC2::ComposerCallback 接口,当 HW-VSYNC 到来的时候,SurfaceFlinger 将会收到回调并且发给 DispSync。DispSync 将会把这些 HW-VSYNC 的时间戳记录下来,当累计了足够的 HW-VSYNC 以后(目前是大于等于 6 个),就开始计算 SW-VSYNC 的偏移 mPeriod。计算出来的 mPeriod 将会用于 DispSyncThread 用来模拟 HW-VSYNC 的周期性起来并且通知对 VSYNC 感兴趣的 Listener,这些 Listener 包括 SurfaceFlinger 和所有需要渲染画面的 app。这些 Listener 通过 EventThread 以 Connection 的抽象形式注册到 EventThread。DispSyncThread 与 EventThread 通过 DispSyncSource 作为中间人进行连接。EventThread 在收到 SW-VSYNC 以后将会把通知所有感兴趣的 Connection,然后 SurfaceFlinger 开始合成,app 开始画帧。在收到足够多的 HW-VSYNC 并且在误差允许的范围内,将会关闭通过 EventControlThread 关闭 HW-VSYNC。

本文其他地址

待更新

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远
微信扫一扫

参考

  1. VSYNC
  2. https://juejin.im/post/5b6948086fb9a04fb87771fb
  3. http://gityuan.com/2017/02/05/graphic_arch/
  4. SW-VSYNC 的生成与传递
  5. http://echuang54.blogspot.com/2015/01/dispsync.html

Android Systrace 基础知识 - MainThread 和 RenderThread 解读

作者 Gracker
2019年11月6日 17:11

本文是 Systrace 系列文章的第九篇,主要是是介绍 Android App 中的 MainThread 和 RenderThread,也就是大家熟悉的主线程渲染线程。文章会从 Systrace 的角度来看 MainThread 和 RenderThread 的工作流程,以及涉及到的相关知识:卡顿、软件渲染、掉帧计算等

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识

正文

这里以滑动列表为例 ,我们截取主线程和渲染线程一帧的工作流程(每一帧都会遵循这个流程,不过有的帧需要处理的事情多,有的帧需要处理的事情少) ,重点看 “UI Thread ” 和 RenderThread 这两行

这张图对应的工作流程如下

  1. 主线程处于 Sleep 状态,等待 Vsync 信号
  2. Vsync 信号到来,主线程被唤醒,Choreographer 回调 FrameDisplayEventReceiver.onVsync 开始一帧的绘制
  3. 处理 App 这一帧的 Input 事件(如果有的话)
  4. 处理 App 这一帧的 Animation 事件(如果有的话)
  5. 处理 App 这一帧的 Traversal 事件(如果有的话)
  6. 主线程与渲染线程同步渲染数据,同步结束后,主线程结束一帧的绘制,可以继续处理下一个 Message(如果有的话,IdleHandler 如果不为空,这时候也会触发处理),或者进入 Sleep 状态等待下一个 Vsync
  7. 渲染线程首先需要从 BufferQueue 里面取一个 Buffer(dequeueBuffer) , 进行数据处理之后,调用 OpenGL 相关的函数,真正地进行渲染操作,然后将这个渲染好的 Buffer 还给 BufferQueue (queueBuffer) , SurfaceFlinger 在 Vsync-SF 到了之后,将所有准备好的 Buffer 取出进行合成(这个流程在讲 SurfaceFlinger 的时候会提到)

上面这个流程在 Android 基于 Choreographer 的渲染机制详解 这篇文章里面已经介绍的很详细了,包括每一帧的 doFrame 都在做什么、卡顿计算的原理、APM 相关. 没有看过这篇文章的同学,建议先去扫一眼

那么这篇文章我们主要从 Android 基于 Choreographer 的渲染机制详解 这篇文章没有讲到的几个点来入手,帮你更好地理解主线程和渲染线程

  1. 主线程的发展
  2. 主线程的创建
  3. 渲染线程的创建
  4. 主线程和渲染线程的分工
  5. 游戏的主线程与渲染线程
  6. Flutter 的主线程和渲染线程

主线程的创建

Android App 的进程是基于 Linux 的,其管理也是基于 Linux 的进程管理机制,所以其创建也是调用了 fork 函数

frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

1
pid_t pid = fork();

Fork 出来的进程,我们这里可以把他看做主线程,但是这个线程还没有和 Android 进行连接,所以无法处理 Android App 的 Message ;由于 Android App 线程运行基于消息机制 ,那么这个 Fork 出来的主线程需要和 Android 的 Message 消息绑定,才能处理 Android App 的各种 Message

这里就引入了 ActivityThread ,确切的说,ActivityThread 应该起名叫 ProcessThread 更贴切一些。ActivityThread 连接了 Fork 出来的进程和 App 的 Message ,他们的通力配合组成了我们熟知的 Android App 主线程。所以说 ActivityThread 其实并不是一个 Thread,而是他初始化了 Message 机制所需要的 MessageQueue、Looper、Handler ,而且其 Handler 负责处理大部分 Message 消息,所以我们习惯上觉得 ActivityThread 是主线程,其实他只是主线程的一个逻辑处理单元。

ActivityThread 的创建

App 进程 fork 出来之后,回到 App 进程,查找 ActivityThread 的 Main函数

com/android/internal/os/ZygoteInit.java

1
2
3
4
5
static final Runnable childZygoteInit(
int targetSdkVersion, String[] argv, ClassLoader classLoader) {
RuntimeInit.Arguments args = new RuntimeInit.Arguments(argv);
return RuntimeInit.findStaticMain(args.startClass, args.startArgs, classLoader);
}

这里的 startClass 就是 ActivityThread,找到之后调用,逻辑就到了 ActivityThread的main函数

android/app/ActivityThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
//1. 初始化 Looper、MessageQueue
Looper.prepareMainLooper();
// 2. 初始化 ActivityThread
ActivityThread thread = new ActivityThread();
// 3. 主要是调用 AMS.attachApplicationLocked,同步进程信息,做一些初始化工作
thread.attach(false, startSeq);
// 4. 获取主线程的 Handler,这里是 H ,基本上 App 的 Message 都会在这个 Handler 里面进行处理
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
// 5. 初始化完成,Looper 开始工作
Looper.loop();
}

注释里面都很清楚,这里就不详细说了,main 函数处理完成之后,主线程就算是正式上线开始工作,其 Systrace 流程如下:

ActivityThread 的功能

另外我们经常说的,Android 四大组件都是运行在主线程上的,其实这里也很好理解,看一下 ActivityThread 的 Handler 的 Message 就知道了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class H extends Handler { //摘抄了部分
public static final int BIND_APPLICATION = 110;
public static final int EXIT_APPLICATION = 111;
public static final int RECEIVER = 113;
public static final int CREATE_SERVICE = 114;
public static final int STOP_SERVICE = 116;
public static final int BIND_SERVICE = 121;
public static final int UNBIND_SERVICE = 122;
public static final int DUMP_SERVICE = 123;
public static final int REMOVE_PROVIDER = 131;
public static final int DISPATCH_PACKAGE_BROADCAST = 133;
public static final int DUMP_PROVIDER = 141;
public static final int UNSTABLE_PROVIDER_DIED = 142;
public static final int INSTALL_PROVIDER = 145;
public static final int ON_NEW_ACTIVITY_OPTIONS = 146;
}

可以看到,进程创建、Activity 启动、Service 的管理、Receiver 的管理、Provider 的管理这些都会在这里处理,然后进到具体的 handleXXX 

渲染线程的创建和发展

主线程讲完了我们来讲渲染线程,渲染线程也就是 RenderThread ,最初的 Android 版本里面是没有渲染线程的,渲染工作都是在主线程完成,使用的也都是 CPU ,调用的是 libSkia 这个库,RenderThread 是在 Android Lollipop 中新加入的组件,负责承担一部分之前主线程的渲染工作,减轻主线程的负担

软件绘制

我们一般提到的硬件加速,指的就是 GPU 加速,这里可以理解为用 RenderThread 调用 GPU 来进行渲染加速 。 硬件加速在目前的 Android 中是默认开启的, 所以如果我们什么都不设置,那么我们的进程默认都会有主线程和渲染线程(有可见的内容)。我们如果在 App 的 AndroidManifest 里面,在 Application 标签里面加一个

1
android:hardwareAccelerated="false"

我们就可以关闭硬件加速,系统检测到你这个 App 关闭了硬件加速,就不会初始化 RenderThread ,直接 cpu 调用 libSkia 来进行渲染。其 Systrace 的表现如下

与这篇文章开头的开了硬件加速的那个图对比,可以看到主线程由于要进行渲染工作,所以执行的时间变长了,也更容易出现卡顿,同时帧与帧直接的空闲间隔也变短了,使得其他 Message 的执行时间被压缩

硬件加速绘制

正常情况下,硬件加速是开启的,主线程的 draw 函数并没有真正的执行 drawCall ,而是把要 draw 的内容记录到 DIsplayList 里面,同步到 RenderThread 中,一旦同步完成,主线程就可以被释放出来做其他的事情,RenderThread 则继续进行渲染工作

渲染线程初始化

渲染线程初始化在真正需要 draw 内容的时候,一般我们启动一个 Activity ,在第一个 draw 执行的时候,会去检测渲染线程是否初始化,如果没有则去进行初始化

android/view/ViewRootImpl.java

1
2
mAttachInfo.mThreadedRenderer.initializeIfNeeded(
mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);

后续直接调用 draw

android/graphics/HardwareRenderer.java

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
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
choreographer.mFrameInfo.markDrawStart();

updateRootDisplayList(view, callbacks);

if (attachInfo.mPendingAnimatingRenderNodes != null) {
final int count = attachInfo.mPendingAnimatingRenderNodes.size();
for (int i = 0; i < count; i++) {
registerAnimatingRenderNode(
attachInfo.mPendingAnimatingRenderNodes.get(i));
}
attachInfo.mPendingAnimatingRenderNodes.clear();
attachInfo.mPendingAnimatingRenderNodes = null;
}

int syncResult = syncAndDrawFrame(choreographer.mFrameInfo);
if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) {
setEnabled(false);
attachInfo.mViewRootImpl.mSurface.release();
attachInfo.mViewRootImpl.invalidate();
}
if ((syncResult & SYNC_REDRAW_REQUESTED) != 0) {
attachInfo.mViewRootImpl.invalidate();
}
}

上面的 draw 只是更新 DIsplayList ,更新结束后,调用 syncAndDrawFrame ,通知渲染线程开始工作,主线程释放。渲染线程的核心实现在 libhwui 库里面,其代码位于 frameworks/base/libs/hwui

frameworks/base/libs/hwui/renderthread/RenderProxy.cpp

1
2
3
int RenderProxy::syncAndDrawFrame() {
return mDrawFrameTask.drawFrame();
}

关于 RenderThread 的工作流程这里就不细说了,后续会有专门的篇幅来讲解这个,目前 hwui 这一块的流程也有很多优秀的文章,大家可以对照文章和源码来看,其核心流程在 Systrace 上的表现如下:

主线程和渲染线程的分工

主线程负责处理进程 Message、处理 Input 事件、处理 Animation 逻辑、处理 Measure、Layout、Draw ,更新 DIsplayList ,但是不涉及 SurfaceFlinger 打交道;渲染线程负责渲染渲染相关的工作,一部分工作也是 CPU 来完成的,一部分操作是调用 OpenGL 函数来完成的

当启动硬件加速后,在 Measure、Layout、Draw 的 Draw 这个环节,Android 使用 DisplayList 进行绘制而非直接使用 CPU 绘制每一帧。DisplayList 是一系列绘制操作的记录,抽象为 RenderNode 类,这样间接的进行绘制操作的优点如下

  1. DisplayList 可以按需多次绘制而无须同业务逻辑交互
  2. 特定的绘制操作(如 translation, scale 等)可以作用于整个 DisplayList 而无须重新分发绘制操作
  3. 当知晓了所有绘制操作后,可以针对其进行优化:例如,所有的文本可以一起进行绘制一次
  4. 可以将对 DisplayList 的处理转移至另一个线程(也就是 RenderThread)
  5. 主线程在 sync 结束后可以处理其他的 Message,而不用等待 RenderThread 结束

RenderThread 的具体流程大家可以看这篇文章 : http://www.cocoachina.com/articles/35302

游戏的主线程与渲染线程

游戏大多使用单独的渲染线程,有单独的 Surface ,直接跟 SurfaceFlinger 进行交互,其主线程的存在感比较低,绝大部分的逻辑都是自己在自己的渲染线程里面实现的。

大家可以看一下王者荣耀对应的 Systrace ,重点看应用进程和 SurfaceFlinger 进程(30fps)

可以看到王者荣耀主线程的主要工作,就是把 Input 事件传给 Unity 的渲染线程,渲染线程收到 Input 事件之后,进行逻辑处理,画面更新等。

Flutter 的主线程和渲染线程

这里提一下 Flutter App 在 Systrace 上的表现,由于 Flutter 的渲染是基于 libSkia 的,所以它也没有 RenderThread ,而是他自建的 RenderEngine , Flutter 比较重要的两个线程是 ui 线程和 gpu 线程,对应到下面提到的  Framework 和 Engine 两层

Flutter 中也会监听 Vsync 信号 ,其 VsyncView 中会以 postFrameCallback 的形式,监听 doFrame 回调,然后调用 nativeOnVsync ,将 Vsync 到来的信息传给 Flutter UI 线程,开始一帧的绘制。

可以看到 Flutter 的思路跟游戏开发的思路差不多,不依赖具体的平台,自建渲染管道,更新快,跨平台优势明显。

Flutter SDK 自带 Skia 库,不用等系统升级就可以用到最新的 Skia 库,而且 Google 团队在 Skia 上做了很多优化,所以官方号称性能可以媲美原生应用

Flutter 的框架分为 Framework 和 Engine 两层,应用是基于 Framework 层开发的,Framework 负责渲染中的 Build,Layout,Paint,生成 Layer 等环节。Engine 层是 C++实现的渲染引擎,负责把 Framework 生成的 Layer 组合,生成纹理,然后通过 Open GL 接口向 GPU 提交渲染数据。

当需要更新 UI 的时候,Framework 通知 Engine,Engine 会等到下个 Vsync 信号到达的时候,会通知 Framework,然后 Framework 会进行 animations, build,layout,compositing,paint,最后生成 layer 提交给 Engine。Engine 会把 layer 进行组合,生成纹理,最后通过 Open Gl 接口提交数据给 GPU,GPU 经过处理后在显示器上面显示。整个流程如下图:

性能

如果主线程需要处理所有任务,则执行耗时较长的操作(例如,网络访问或数据库查询)将会阻塞整个界面线程。一旦被阻塞,线程将无法分派任何事件,包括绘图事件。主线程执行超时通常会带来两个问题

  1. 卡顿:如果主线程 + 渲染线程每一帧的执行都超过 16.6ms(60fps 的情况下),那么就可能会出现掉帧。
  2. 卡死:如果界面线程被阻塞超过几秒钟时间(根据组件不同 , 这里的阈值也不同),用户会看到 “应用无响应” (ANR) 对话框(部分厂商屏蔽了这个弹框,会直接 Crash 到桌面)

对于用户来说,这两个情况都是用户不愿意看到的,所以对于 App 开发者来说,两个问题是发版本之前必须要解决的,ANR 这个由于有详细的调用栈,所以相对来说比较好定位;但是间歇性卡顿这个,可能就需要使用工具来进行分析了:Systrace + TraceView,所以理解主线程和渲染线程的关系和他们的工作原理是非常重要的,这也是本系列的一个初衷

另外关于卡顿,可以参考下面三篇文章,你的 App 卡顿不一定是你 App 的问题,也有可能是系统的问题,不过不管怎么说,首先要会分析卡顿问题。

  1. Android 中的卡顿丢帧原因概述 - 方法论
  2. Android 中的卡顿丢帧原因概述 - 系统篇
  3. Android 中的卡顿丢帧原因概述 - 应用篇

参考

  1. https://juejin.im/post/5a9e01c3f265da239d48ce32
  2. http://www.cocoachina.com/articles/35302
  3. https://juejin.im/post/5b7767fef265da43803bdc65
  4. http://gityuan.com/2019/06/15/flutter_ui_draw/
  5. https://developer.android.google.cn/guide/components/processes-and-threads

附件

本文涉及到的附件也上传了,各位下载后解压,使用 Chrome 浏览器打开即可

点此链接下载文章所涉及到的 Systrace 附件

本文其他地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎或者掘金页面
掘金 - Systrace 基础知识 - MainThread 和 RenderThread 解读

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远
微信扫一扫

Android Systrace 基础知识 - Input 解读

作者 Gracker
2019年11月4日 09:38

本文是 Systrace 系列文章的第六篇,主要是对 Systrace 中的 Input 进行简单介绍,介绍其 Input 的流程; Systrace 中 Input 信息的体现 ,以及如何结合 Input 信息,分析与 Input 相关的问题

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些。

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. Systrace 响应速度实战 3 :响应速度延伸知识
  19. Systrace 线程 CPU 运行状态分析技巧 - Runnable 篇
  20. Systrace 线程 CPU 运行状态分析技巧 - Running 篇
  21. Systrace 线程 CPU 运行状态分析技巧 - Sleep 和 Uninterruptible Sleep 篇

正文

Android 基于 Choreographer 的渲染机制详解 这篇文章中,我有讲到,Android App 的主线程运行的本质是靠 Message 驱动的,这个 Message 可以是循环动画、可以是定时任务、可以是其他线程唤醒,不过我们最常见的还是 Input Message ,这里的 Input 是以 InputReader 这里的分类,不仅包含触摸事件(Down、Up、Move) , 可包含 Key 事件(Home Key 、 Back Key) . 这里我们着重讲的是触摸事件

由于 Android 系统在 Input 链上加了一些 Trace 点,且这些 Trace 点也比较完善,部分厂家可能会自己加一些,不过我们这里以标准的 Trace 点来讲解,这样不至于你换了个手机抓的 Trace 就不一样了

Input 在 Android 中的地位是很高的,我们在玩手机的时候,大部分应用的滑动、跳转这些都依靠 Input 事件来驱动,后续我会专门写一篇文章,来介绍 Android 中基于 Input 的运行机制。这里是从 Systrace 的角度来看 Input 。看下面的流程之前,脑子里先有个关于 Input 的大概处理流程,这样看的时候,就可以代入:

  1. 触摸屏每隔几毫秒扫描一次,如果有触摸事件,那么把事件上报到对应的驱动
  2. InputReader 读取触摸事件交给 InputDispatcher 进行事件派发
  3. InputDispatcher 将触摸事件发给注册了 Input 事件的 App
  4. App 拿到事件之后,进行 Input 事件分发,如果此事件分发的过程中,App 的 UI 发生了变化,那么会请求 Vsync,则进行一帧的绘制

另外在看 Systrace 的时候,要牢记 Systrace 中时间是从左到右流逝的,也就是说如果你在 Systrace 上画一条竖直线,那么竖直线左边的事件永远比右边的事件先发生,这也是我们分析源码流程的一个基石。我希望大家在看基于 Systrace 的源码流程分析之后,脑子里有一个图形化的、立体的流程图,你跟的代码走到哪一步了在图形你在脑中可以快速定位出来

Input in Systrace

下面这张图是一个概览图,以滑动桌面为例 (滑动桌面包括一个 Input_Down 事件 + 若干个 Input_Move 事件 + 一个 Input_Up 事件,这些事件和事件流都会在 Systrace 上有所体现,这也是我们分析 Systrace 的一个重要的切入点),主要牵扯到的模块是 SystemServer 和 App 模块,其中用蓝色标识的是事件的流动信息,红色的是辅助信息。

InputReaderInputDispatcher 是跑在 SystemServer 里面的两个 Native 线程,负责读取和分发 Input 事件,我们分析 Systrace 的 Input 事件流,首先是找到这里。下面针对上图中标号进行简单说明

  1. InputReader 负责从 EventHub 里面把 Input 事件读取出来,然后交给 InputDispatcher 进行事件分发
  2. InputDispatcher 在拿到 InputReader 获取的事件之后,对事件进行包装和分发 (也就是发给对应的)
  3. OutboundQueue 里面放的是即将要被派发给对应 AppConnection 的事件
  4. WaitQueue 里面记录的是已经派发给 AppConnection 但是 App 还在处理没有返回处理成功的事件
  5. PendingInputEventQueue 里面记录的是 App 需要处理的 Input 事件,这里可以看到已经到了应用进程
  6. deliverInputEvent 标识 App UI Thread 被 Input 事件唤醒
  7. InputResponse 标识 Input 事件区域,这里可以看到一个 Input_Down 事件 + 若干个 Input_Move 事件 + 一个 Input_Up 事件的处理阶段都被算到了这里
  8. App 响应 Input 事件 : 这里是滑动然后松手,也就是我们熟悉的桌面滑动的操作,桌面随着手指的滑动更新画面,松手后触发 Fling 继续滑动,从 Systrace 就可以看到整个事件的流程

下面以第一个 Input_Down 事件的处理流程来进行详细的工作流说明,其他的 Move 事件和 Up 事件的处理是一样的(部分不一样,不过影响不大)

InputDown 事件在 SystemServer 的工作流

放大 SystemServer 的部分,可以看到其工作流(蓝色),滑动桌面包括 Input_Down + 若干个 Input_Move + Input_Up ,我们这里看的是 Input_Down 这个事件

InputDown 事件在 App 的工作流

应用在收到 Input 事件后,有时候会马上去处理 (没有 Vsync 的情况下),有时候会等 Vsync 信号来了之后去处理,这里 Input_Down 事件就是直接去唤醒主线程做处理,其 Systrace 比较简单,最上面有个 Input 事件队列,主线程则是简单的处理

App 的 Pending 队列

主线程处理 Input 事件

主线程处理 Input 事件这个大家比较熟悉,从下面的调用栈可以看到,Input 事件传到了 ViewRootImpl,最终到了 DecorView ,然后就是大家熟悉的 Input 事件分发机制

关键知识点和流程

从上面的 Systrace 来看,Input 事件的基本流向如下:

  1. InputReader 读取 Input 事件
  2. InputReader 将读取的 Input 事件放到 InboundQueue 中
  3. InputDispatcher 从 InboundQueue 中取出 Input 事件派发到各个 App(连接) 的 OutBoundQueue
  4. 同时将事件记录到各个 App(连接) 的 WaitQueue
  5. App 接收到 Input 事件,同时记录到 PendingInputEventQueue ,然后对事件进行分发处理
  6. App 处理完成后,回调 InputManagerService 将负责监听的 WaitQueue 中对应的 Input 移除

通过上面的流程,一次 Input 事件就被消耗掉了(当然这只是正常情况,还有很多异常情况、细节处理,这里就不细说了,自己看相关流程的时候可以深挖一下) , 那么本节就从上面的关键流中取几个重要的知识点讲解(部分流程和图参考和拷贝了 Gityuan 的博客的图,链接在最下面参考那一节)

InputReader

InputReader 是一个 Native 线程,跑在 SystemServer 进程里面,其核心功能是从 EventHub 读取事件、进行加工、将加工好的事件发送到 InputDispatcher

InputReader Loop 流程如下

  1. getEvents:通过 EventHub (监听目录 /dev/input )读取事件放入 mEventBuffer ,而mEventBuffer 是一个大小为256的数组, 再将事件 input_event 转换为 RawEvent 
  2. processEventsLocked: 对事件进行加工, 转换 RawEvent -> NotifyKeyArgs(NotifyArgs) 
  3. QueuedListener->flush:将事件发送到 InputDispatcher 线程, 转换 NotifyKeyArgs -> KeyEntry(EventEntry)

核心代码 loopOnce 处理流程如下:

InputReader 核心 Loop 函数 loopOnce 逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void InputReader::loopOnce() {
    int32_t oldGeneration;
    int32_t timeoutMillis;
    bool inputDevicesChanged = false;
    std::vector<InputDeviceInfo> inputDevices;
    { // acquire lock
......
//获取输入事件、设备增删事件,count 为事件数量
    size_t count = mEventHub ->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
    {
......
        if (count) {//处理事件
            processEventsLocked(mEventBuffer, count);
        }

    }
......
    mQueuedListener->flush();//将事件传到 InputDispatcher,这里getListener 得到的就是 InputDispatcher
}

InputDispatcher

上面的 InputReader 调用 mQueuedListener->flush 之后 ,将 Input 事件加入到InputDispatcher 的 mInboundQueue ,然后唤醒 InputDispatcher , 从 Systrace 的唤醒信息那里也可以看到 InputDispatch 线程是被 InputReader 唤醒的

InputDispatcher 的核心逻辑如下:

  1. dispatchOnceInnerLocked(): 从 InputDispatcher 的 mInboundQueue 队列,取出事件 EventEntry。另外该方法开始执行的时间点 (currentTime) 便是后续事件 dispatchEntry 的分发时间 (deliveryTime)
  2. dispatchKeyLocked():满足一定条件时会添加命令 doInterceptKeyBeforeDispatchingLockedInterruptible;
  3. enqueueDispatchEntryLocked():生成事件 DispatchEntry 并加入 connection 的 outbound 队列
  4. startDispatchCycleLocked():从 outboundQueue 中取出事件 DispatchEntry, 重新放入 connection 的 waitQueue 队列;
  5. InputChannel.sendMessage 通过 socket 方式将消息发送给远程进程;
  6. runCommandsLockedInterruptible():通过循环遍历的方式,依次处理 mCommandQueue 队列中的所有命令。而 mCommandQueue 队列中的命令是通过 postCommandLocked() 方式向该队列添加的。

其核心处理逻辑在 dispatchOnceInnerLocked 这里

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
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    // Ready to start a new event.
    // If we don't already have a pending event, go grab one.
    if (! mPendingEvent) {
        if (mInboundQueue.isEmpty()) {
        } else {
            // Inbound queue has at least one entry.
            mPendingEvent = mInboundQueue.dequeueAtHead();
            traceInboundQueueLengthLocked();
        }

        // Poke user activity for this event.
        if (mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER) {
            pokeUserActivityLocked(mPendingEvent);
        }

        // Get ready to dispatch the event.
        resetANRTimeoutsLocked();
    }
    case EventEntry::TYPE_MOTION: {
        done = dispatchMotionLocked(currentTime, typedEntry,
                &dropReason, nextWakeupTime);
        break;
    }

    if (done) {
        if (dropReason != DROP_REASON_NOT_DROPPED) {
            dropInboundEventLocked(mPendingEvent, dropReason);
        }
        mLastDropReason = dropReason;
        releasePendingEventLocked();
        *nextWakeupTime = LONG_LONG_MIN;  // force next poll to wake up immediately
    }
}

InboundQueue

InputDispatcher 执行 notifyKey 的时候,会将 Input 事件封装后放到 InboundQueue 中,后续 InputDispatcher 循环处理 Input 事件的时候,就是从 InboundQueue 取出事件然后做处理

OutboundQueue

Outbound 意思是出站,这里的 OutboundQueue 指的是要被 App 拿去处理的事件队列,每一个 App(Connection) 都对应有一个 OutboundQueue ,从 InboundQueue 那一节的图来看,事件会先进入 InboundQueue ,然后被 InputDIspatcher 派发到各个 App 的 OutboundQueue

WaitQueue

当 InputDispatcher 将 Input 事件分发出去之后,将 DispatchEntry 从 outboundQueue 中取出来放到 WaitQueue 中,当 publish 出去的事件被处理完成(finished),InputManagerService 就会从应用中得到一个回复,此时就会取出 WaitQueue 中的事件,从 Systrace 中看就是对应 App 的 WaitQueue 减少

如果主线程发生卡顿,那么 Input 事件没有及时被消耗,也会在 WaitQueue 这里体现出来,如下图:

整体逻辑

图来自 Gityuan 博客

Input 刷新与 Vsync

Input 的刷新取决于触摸屏的采样,目前比较多的屏幕采样率是 120Hz 和 160Hz ,对应就是 8ms 采样一次或者 6.25ms 采样一次,我们来看一下其在 Systrace 上的展示

可以看到上图中, InputReader 每隔 6.25ms 就可以读上来一个数据,交给 InputDispatcher 去分发给 App ,那么是不是屏幕采样率越高越好呢?也不一定,比如上面那张图,虽然 InputReader 每隔 6.25ms 就可以读上来一个数据给 InputDispatcher 去分发给 App ,但是从 WaitQueue 的表现来看,应用并没有消耗这个 Input 事件,这是为什么呢?

原因在于应用消耗 Input 事件的时机是 Vsync 信号来了之后,刷新率为 60Hz 的屏幕,一般系统也是 60 fps ,也就是说两个 Vsync 的间隔在 16.6ms ,这期间如果有两个或者三个 Input 事件,那么必然有一个或者两个要被抛弃掉,只拿最新的那个。也就是说:

  1. 在屏幕刷新率和系统 FPS 都是 60 的时候,盲目提高触摸屏的采样率,是没有太大的效果的,反而有可能出现上面图中那样,有的 Vsync 周期中有两个 Input 事件,而有的 Vsync 周期中有三个 Input 事件,这样造成事件不均匀,可能会使 UI 产生抖动
  2. 在屏幕刷新率和系统 FPS 都是 60 的时候,使用 120Hz 采样率的触摸屏就可以了
  3. 如果在屏幕刷新率和系统 FPS 都是 90 的时候 ,那么 120Hz 采样率的触摸屏显然不够用了,这时候应该采用 180Hz 采样率的屏幕

Input 调试信息

Dumpsys Input 主要是 Debug 用,我们也可以来看一下其中的一些关键信息,到时候遇到了问题也可以从这里面找 , 其命令如下:

1
adb shell dumpsys input

其中的输出比较多,我们终点截取 Device 信息、InputReader、InputDispatcher 三段来看就可以了

Device 信息

主要是目前连接上的 Device 信息,下面摘取的是 touch 相关的

1
2
3
4
5
6
7
8
9
10
11
12
13
    3: main_touch
      Classes: 0x00000015
      Path: /dev/input/event6
      Enabled: true
      Descriptor: 4055b8a032ccf50ef66dbe2ff99f3b2474e9eab5
      Location: main_touch/input0
      ControllerNumber: 0
      UniqueId: 
      Identifier: bus=0x0000, vendor=0xbeef, product=0xdead, version=0x28bb
      KeyLayoutFile: /system/usr/keylayout/main_touch.kl
      KeyCharacterMapFile: /system/usr/keychars/Generic.kcm
      ConfigurationFile: 
      HaveKeyboardLayoutOverlay: false

Input Reader 状态

InputReader 这里就是当前 Input 事件的一些展示

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
  Device 3: main_touch
    Generation: 24
    IsExternal: false
    HasMic:     false
    Sources: 0x00005103
    KeyboardType: 1
    Motion Ranges:
      X: source=0x00005002, min=0.000, max=1079.000, flat=0.000, fuzz=0.000, resolution=0.000
      Y: source=0x00005002, min=0.000, max=2231.000, flat=0.000, fuzz=0.000, resolution=0.000
      PRESSURE: source=0x00005002, min=0.000, max=1.000, flat=0.000, fuzz=0.000, resolution=0.000
      SIZE: source=0x00005002, min=0.000, max=1.000, flat=0.000, fuzz=0.000, resolution=0.000
      TOUCH_MAJOR: source=0x00005002, min=0.000, max=2479.561, flat=0.000, fuzz=0.000, resolution=0.000
      TOUCH_MINOR: source=0x00005002, min=0.000, max=2479.561, flat=0.000, fuzz=0.000, resolution=0.000
      TOOL_MAJOR: source=0x00005002, min=0.000, max=2479.561, flat=0.000, fuzz=0.000, resolution=0.000
      TOOL_MINOR: source=0x00005002, min=0.000, max=2479.561, flat=0.000, fuzz=0.000, resolution=0.000
    Keyboard Input Mapper:
      Parameters:
        HasAssociatedDisplay: false
        OrientationAware: false
        HandlesKeyRepeat: false
      KeyboardType: 1
      Orientation: 0
      KeyDowns: 0 keys currently down
      MetaState: 0x0
      DownTime: 521271703875000
    Touch Input Mapper (mode - direct):
      Parameters:
        GestureMode: multi-touch
        DeviceType: touchScreen
        AssociatedDisplay: hasAssociatedDisplay=true, isExternal=false, displayId=''
        OrientationAware: true
      Raw Touch Axes:
        X: min=0, max=1080, flat=0, fuzz=0, resolution=0
        Y: min=0, max=2232, flat=0, fuzz=0, resolution=0
        Pressure: min=0, max=127, flat=0, fuzz=0, resolution=0
        TouchMajor: min=0, max=512, flat=0, fuzz=0, resolution=0
        TouchMinor: unknown range
        ToolMajor: unknown range
        ToolMinor: unknown range
        Orientation: unknown range
        Distance: unknown range
        TiltX: unknown range
        TiltY: unknown range
        TrackingId: min=0, max=65535, flat=0, fuzz=0, resolution=0
        Slot: min=0, max=20, flat=0, fuzz=0, resolution=0
      Calibration:
        touch.size.calibration: geometric
        touch.pressure.calibration: physical
        touch.orientation.calibration: none
        touch.distance.calibration: none
        touch.coverage.calibration: none
      Affine Transformation:
        X scale: 1.000
        X ymix: 0.000
        X offset: 0.000
        Y xmix: 0.000
        Y scale: 1.000
        Y offset: 0.000
      Viewport: displayId=0, orientation=0, logicalFrame=[0, 0, 1080, 2232], physicalFrame=[0, 0, 1080, 2232], deviceSize=[1080, 2232]
      SurfaceWidth: 1080px
      SurfaceHeight: 2232px
      SurfaceLeft: 0
      SurfaceTop: 0
      PhysicalWidth: 1080px
      PhysicalHeight: 2232px
      PhysicalLeft: 0
      PhysicalTop: 0
      SurfaceOrientation: 0
      Translation and Scaling Factors:
        XTranslate: 0.000
        YTranslate: 0.000
        XScale: 0.999
        YScale: 1.000
        XPrecision: 1.001
        YPrecision: 1.000
        GeometricScale: 0.999
        PressureScale: 0.008
        SizeScale: 0.002
        OrientationScale: 0.000
        DistanceScale: 0.000
        HaveTilt: false
        TiltXCenter: 0.000
        TiltXScale: 0.000
        TiltYCenter: 0.000
        TiltYScale: 0.000
      Last Raw Button State: 0x00000000
      Last Raw Touch: pointerCount=1
        [0]: id=0, x=660, y=1338, pressure=44, touchMajor=44, touchMinor=44, toolMajor=0, toolMinor=0, orientation=0, tiltX=0, tiltY=0, distance=0, toolType=1, isHovering=false
      Last Cooked Button State: 0x00000000
      Last Cooked Touch: pointerCount=1
        [0]: id=0, x=659.389, y=1337.401, pressure=0.346, touchMajor=43.970, touchMinor=43.970, toolMajor=43.970, toolMinor=43.970, orientation=0.000, tilt=0.000, distance=0.000, toolType=1, isHovering=false
      Stylus Fusion:
        ExternalStylusConnected: false
        External Stylus ID: -1
        External Stylus Data Timeout: 9223372036854775807
      External Stylus State:
        When: 9223372036854775807
        Pressure: 0.000000
        Button State: 0x00000000
        Tool Type: 0

InputDispatcher 状态

InputDispatch 这里的重要信息主要包括

  1. FocusedApplication :当前获取焦点的应用
  2. FocusedWindow : 当前获取焦点的窗口
  3. TouchStatesByDisplay
  4. Windows :所有的 Window
  5. MonitoringChannels :Window 对应的 Channel
  6. Connections :所有的连接
  7. AppSwitch: not pending
  8. Configuration
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
Input Dispatcher State:
  DispatchEnabled: 1
  DispatchFrozen: 0
  FocusedApplication: name='AppWindowToken{ac6ec28 token=Token{a38a4b ActivityRecord{7230f1a u0 com.meizu.flyme.launcher/.Launcher t13}}}', dispatchingTimeout=5000.000ms
  FocusedWindow: name='Window{3c007ad u0 com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher}'
  TouchStatesByDisplay:
    0: down=true, split=true, deviceId=3, source=0x00005002
      Windows:
        0: name='Window{3c007ad u0 com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher}', pointerIds=0x80000000, targetFlags=0x105
        1: name='Window{8cb8f7 u0 com.android.systemui.ImageWallpaper}', pointerIds=0x0, targetFlags=0x4102
  Windows:
    2: name='Window{ba2fc6b u0 NavigationBar}', displayId=0, paused=false, hasFocus=false, hasWallpaper=false, visible=true, canReceiveKeys=false, flags=0x21840068, type=0x000007e3, layer=0, frame=[0,2136][1080,2232], scale=1.000000, touchableRegion=[0,2136][1080,2232], inputFeatures=0x00000000, ownerPid=26514, ownerUid=10033, dispatchingTimeout=5000.000ms
    3: name='Window{72b7776 u0 StatusBar}', displayId=0, paused=false, hasFocus=false, hasWallpaper=false, visible=true, canReceiveKeys=false, flags=0x81840048, type=0x000007d0, layer=0, frame=[0,0][1080,84], scale=1.000000, touchableRegion=[0,0][1080,84], inputFeatures=0x00000000, ownerPid=26514, ownerUid=10033, dispatchingTimeout=5000.000ms
    9: name='Window{3c007ad u0 com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher}', displayId=0, paused=false, hasFocus=true, hasWallpaper=true, visible=true, canReceiveKeys=true, flags=0x81910120, type=0x00000001, layer=0, frame=[0,0][1080,2232], scale=1.000000, touchableRegion=[0,0][1080,2232], inputFeatures=0x00000000, ownerPid=27619, ownerUid=10021, dispatchingTimeout=5000.000ms
  MonitoringChannels:
    0: 'WindowManager (server)'
  RecentQueue: length=10
    MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (524.5, 1306.4)]), policyFlags=0x62000000, age=61.2ms
    MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (543.5, 1309.4)]), policyFlags=0x62000000, age=54.7ms
  PendingEvent: <none>
  InboundQueue: <empty>
  ReplacedKeys: <empty>
  Connections:
    0: channelName='WindowManager (server)', windowName='monitor', status=NORMAL, monitor=true, inputPublisherBlocked=false
      OutboundQueue: <empty>
      WaitQueue: <empty>
    5: channelName='72b7776 StatusBar (server)', windowName='Window{72b7776 u0 StatusBar}', status=NORMAL, monitor=false, inputPublisherBlocked=false
      OutboundQueue: <empty>
      WaitQueue: <empty>
    6: channelName='ba2fc6b NavigationBar (server)', windowName='Window{ba2fc6b u0 NavigationBar}', status=NORMAL, monitor=false, inputPublisherBlocked=false
      OutboundQueue: <empty>
      WaitQueue: <empty>
    12: channelName='3c007ad com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher (server)', windowName='Window{3c007ad u0 com.meizu.flyme.launcher/com.meizu.flyme.launcher.Launcher}', status=NORMAL, monitor=false, inputPublisherBlocked=false
      OutboundQueue: <empty>
      WaitQueue: length=3
        MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (634.4, 1329.4)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=2, age=17.4ms, wait=16.8ms
        MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (647.4, 1333.4)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=2, age=11.1ms, wait=10.4ms
        MotionEvent(deviceId=3, source=0x00005002, action=MOVE, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, displayId=0, pointers=[0: (659.4, 1337.4)]), policyFlags=0x62000000, targetFlags=0x00000105, resolvedAction=2, age=5.2ms, wait=4.6ms
  AppSwitch: not pending
  Configuration:
    KeyRepeatDelay: 50.0ms
    KeyRepeatTimeout: 500.0ms

参考

本文部分图文参考和拷贝自下面几篇文章,同时下面几篇文章讲解了 Input 流程的细节部分,推荐大家在看完这篇文章后,如果对代码细节感兴趣,可以仔细研读下面这几篇非常棒的文章。

  1. http://gityuan.com/2016/12/11/input-reader/
  2. http://gityuan.com/2016/12/10/input-manager/
  3. http://gityuan.com/2016/12/17/input-dispatcher/
  4. https://zhuanlan.zhihu.com/p/29386642

附件

本文涉及到的附件也上传了,各位下载后解压,使用 Chrome 浏览器打开即可
点此链接下载文章所涉及到的 Systrace 附件

本文其他地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎或者掘金页面
掘金 - Systrace 基础知识 - Input 解读

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远
微信扫一扫

Android 中的卡顿丢帧原因概述 - 应用篇

作者 Gracker
2019年9月5日 20:35

Android 中的卡顿丢帧原因概述 - 系统篇 这篇文章中我们列举了系统自身原因导致的手机卡顿问题 , 这一篇文章我们主要列举一些由于 App 自身原因导致的卡顿问题. 各位用户在使用 App 的时候 , 如果遇见卡顿现象 , 先别第一时间骂手机厂商优化烂 , 先想想是不是这个 App 自己的问题.

Android 手机使用中的卡顿问题 , 一般来说手机厂商和 App 开发商都会非常重视 , 所以不管是手机厂商还是 App 开发者 , 都会对卡顿问题非常重视 , 内部一般也会有专门的基础组或者优化组来进行优化 . 目前市面上有一些非常棒的第三方性能监控工具 , 比如腾讯的 Matrix ; 手机厂商一般也会有自己的性能监控方案 , 由于可以修改源码和避免权限问题 , 所以手机厂商可以拿到更多的数据 , 分析起来也会更方便一些.

说回流畅度 , 其实就是操作过程中的丢帧 , 本来一秒中画面需要更新 60 帧,但是如果这期间只更新了 55 帧 , 那么在用户看来就是丢帧了 , 主观感觉就是卡了 , 尤其是帧率波动 , 用户的感知会更明显. 引起丢帧的原因非常多, 有硬件层面的 , 有软件层面的 , 也有 App 自身的问题. 所以这一部分我分为四篇文章去讲 , 会简单讲一下哪些原因会用户觉得卡顿丢帧 :

0. Android 中的卡顿丢帧原因概述 - 方法论
1. Android 中的卡顿丢帧原因概述 - 系统篇
2. Android 中的卡顿丢帧原因概述 - 应用篇
3. Android 中的卡顿丢帧原因概述 - 低内存篇

Android App 自身导致的性能问题

Android 中的卡顿丢帧原因概述 - 系统篇 这篇文章中我们列举了系统自身原因导致的手机卡顿问题 , 这一篇文章我们主要列举一些由于 App 自身原因导致的卡顿问题. 各位用户在使用 App 的时候 , 如果遇见卡顿现象 , 先别第一时间骂手机厂商优化烂 , 先想想是不是这个 App 自己的问题.

这些实际的案例 , 很多都可以在 Systrace 上看出来 , 所以我的很多贴图都是 Systrace 上实际被发现的问题 , 如果你对 Systrace 不了解 , 可以查看这个 Systrace 系列 , 这里你只需要知道 , Systrace 从系统全局的角度 , 来展示当前系统的运行状况 , 通常被用来 Debug Android 性能问题 .

1.App 主线程执行时间长

主线程执行 Input \ Animation \ Measure \ Layout \ Draw \ decodeBitmap 等操作超时都会导致卡顿 , 下面就是一些真实的案例

Measure \ Layout 耗时\超时 (或者没有调度到)

Draw 耗时

Animation回调耗时

View 初始化耗时 (PlayStore)

List Item 初始化耗时(WeChat)

decodeBitmap 耗时 (或者没有调度到)

2.uploadBitmap 耗时

这里的 uploadBitmap 主要是 upload bitmap to gpu 的操作 , 如果 bitmap 过大 , 或者每一帧内容都在变化 , 那么就需要频繁 upload , 导致渲染线程耗时.

3.BuildDrawingCache 耗时

应用本身频繁调用 buildDrawingCache 会导致主线程执行耗时从而导致卡顿 , 从下图来看, 主线程每一帧明显超过了 Vsync 周期

微信对话框有多个动态表情的时候, 也会出现这种情况导致的卡顿

4.使用 CPU 渲染而不是 GPU 渲染

如果应用在 Activity 中设置了软件渲染, 那么就不会走 hwui , 直接走 skia, 纯 cpu 进程渲染, 由于这么做会加重 UI Thread 的负载, 所以大部分情况下这种写法都会导致卡顿 , 详细技术分析可以看这篇文章 Android 中的 Hardware Layer 详解

5.主线程 Binder 耗时

Activity resume 的时候, 与 AMS 通信要持有 AMS 锁, 这时候如果碰到后台比较繁忙的时候, 等锁操作就会比较耗时, 导致部分场景因为这个卡顿, 比如多任务手势操作

6.游戏 SurfaceView 内容绘制不均匀

这一项指的是游戏自身的绘制问题, 会导致总是不能满帧去跑, 如下图, 红框部分是SurfaceFlinger 显示掉帧, 原因是底下的游戏在绘制的时候, 刚好这一帧超过了 Vsync SF 的信号.这种一般是游戏自身的问题.

7.WebView 性能不足

应用里面涉及到 WebView 的时候, 如果页面比较复杂, WebView 的性能就会比较差, 从而造成卡顿

8.帧率与刷新率不匹配

如果屏幕帧率和系统的 fps 不相符 , 那么有可能会导致画面不是那么顺畅. 比如使用 90 Hz 的屏幕搭配 60 fps 的动画

9.应用性能跟不上高帧率屏幕和系统

部分应用由于设计比较复杂, 每一帧绘制的耗时都比较长 , 这么做的话在 60 fps 的机器上可能没有问题 , 但是在 90 fps 的机器上就会很卡, 因为从 60 -> 90 , 每帧留给应用的绘制时间从 16.6 ms 变成了 11.1 ms , 如果没有在 11.1 ms 内完成, 就会出现掉帧的情况.

如下图, 这个 App 的性能比较差, 每一帧耗时都很长

10.主线程 IO 操作

主线程操作数据库
使用 SharedPerforence 的 Commit 而不是 Apply

11.WebView 与主线程交互

与 WebView 进行交互的时候, 如果 WebView 出现问题, 那么也会出现卡顿

微信文章页卡顿

12.RenderThread 耗时

RenderThread 自身比较耗时, 导致一帧的时长超过 Vsync 间隔.

渲染线程耗时过长阻塞了主线程的下一次 sync

13.多个 RenderThread 同步导致主线程卡顿

有的 App 会产生多个 RenderThread ,在某些场景下 RenderThread 在 sync 的时候花费比较多的时间,导致主线程卡顿

1
2
3
4
5
6
7
8
9
10
adb shell ps -AT | grep 10300 | grep RenderThread
u0_a170 10300 16228 6709 2693260 305172 SyS_epoll_wait 0 S RenderThread
u0_a170 10300 17394 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170 10300 17395 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170 10300 17396 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170 10300 17397 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170 10300 17399 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170 10300 17400 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170 10300 17401 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread
u0_a170 10300 17402 6709 2693260 305172 futex_wait_queue_me 0 S RenderThread

总结

Android 原生系统是一个不断进化的过程 , 目前已经进化到了 Android Q , 每个版本都会解决非常多的性能问题 , 同时也会引进一些问题 ; 到了手机厂商这里 , 由于硬件差异和软件定制 , 会在系统中加入大量的自己的代码 , 这无疑也会影响系统的性能 . 同样由于 Android 的开放 , App 的质量和行为也影响着整机的用户体验.

本篇主要列出了 App 自身的实现问题导致的流畅性问题 , Android App 最大的问题就是质量良莠不齐 , 不同于 App Store 这样的强力管理市场 , Android App 不仅可以在 Google Play 上面进行安装 , 也可以在其他的软件市场上面安装 , 甚至可以下载安装包自行安装 , 可以说上架的门槛非常低 , 那么质量就只能由 App 开发者自己来把握了.

许多大厂的 App 质量自然不必多说 , 他们对性能和用户体验都是非常关注的 , 但也会有需求和功能过多导致的性能问题 , 比如微信就非常占内存 ; 新版本的 QQ 要比之前版本的使用起来流畅性差好多 . 中小厂的 App 就更不用说了. 再加上 Android 平台的开放性 , 需要 App 玩起来黑科技 , 什么保活 \ 相互唤醒 \ 热更新 \ 跑后台任务等 . 站在 App 开发者的角度来说这无可厚非 , 但是系统开发者则希望系统能在用户使用的时候 , 前后台 App 都能有正常的行为 , 来保证前台 App 的用户体验 . 也希望 App 开发者能重视自己 App 的性能体验 , 给用户一个好印象.

系统这边发现 App 自身的性能问题 , 且在其他厂商的手机上也是一样的表现的时候 , 通常会与 App 开发者进行联系 , 沟通一起解决 .

大家可以看看这个问题 : 当手机厂商说安卓手机性能优化的时候,他们到底在做什么

这也是流畅性的一个系列文章中的一篇 , 可以点击下面的链接查看本系列的其他文章.

0. Android 中的卡顿丢帧原因概述 - 方法论
1. Android 中的卡顿丢帧原因概述 - 系统篇
2. Android 中的卡顿丢帧原因概述 - 应用篇

本文知乎地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 中的卡顿丢帧原因概述 - 应用篇

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 中的卡顿丢帧原因概述 - 系统篇

作者 Gracker
2019年9月5日 20:35

Android 中的卡顿丢帧原因概述 - 应用篇这篇文章中我们列举了应用自身原因导致的手机卡顿问题 , 这一篇文章我们主要列举一些由 Android 平台自身原因导致的卡顿问题. 各大国内 Android 厂商的产品由于硬件性能有高有低 , 功能实现各有差异 , 团队技术能力各有千秋 , 所以其系统的质量也有高有低 , 这里我们就来列举一下 , 由于系统的硬件和软件原因导致的性能问题.

Android 手机使用中的卡顿问题 , 一般来说手机厂商和 App 开发商都会非常重视 , 所以不管是手机厂商还是 App 开发者 , 都会对卡顿问题非常重视 , 内部一般也会有专门的基础组或者优化组来进行优化 . 目前市面上有一些非常棒的第三方性能监控工具 , 比如腾讯的 Matrix ; 手机厂商一般也会有自己的性能监控方案 , 由于可以修改源码和避免权限问题 , 所以手机厂商可以拿到更多的数据 , 分析起来也会更方便一些.

说回流畅度 , 其实就是操作过程中的丢帧 , 本来一秒中画面需要更新 60 帧,但是如果这期间只更新了 55 帧 , 那么在用户看来就是丢帧了 , 主观感觉就是卡了 , 尤其是帧率波动 , 用户的感知会更明显. 引起丢帧的原因非常多, 有硬件层面的 , 有软件层面的 , 也有 App 自身的问题. 所以这一部分我分为四篇文章去讲 , 会简单讲一下哪些原因会用户觉得卡顿丢帧 :

0. Android 中的卡顿丢帧原因概述 - 方法论
1. Android 中的卡顿丢帧原因概述 - 系统篇
2. Android 中的卡顿丢帧原因概述 - 应用篇
3. Android 中的卡顿丢帧原因概述 - 低内存篇

Android 平台性能导致的性能案例

下面我会列出来一些实际的卡顿案例 , 这些导致卡顿的原因都是由于 Android 系统平台的一些问题导致的 , 有些问题在开发阶段就会暴露出来 , 这一类通常会在发给用户之前就解决掉 ; 有些问题是用户在长时间使用之后才会暴露出来 , 这一类问题最多 , 但是也比较难以解决 ; 还有一些问题 , 只有非常特殊的场景或者特殊的硬件才会暴露出来 .

这些实际的案例 , 很多都可以在 Systrace 上看出来 , 所以我的很多贴图都是 Systrace 上实际被发现的问题 , 如果你对 Systrace 不了解 , 可以查看这个 Systrace 系列 , 这里你只需要知道 , Systrace 从系统全局的角度 , 来展示当前系统的运行状况 , 通常被用来 Debug Android 性能问题 .

1.SurfaceFlinger 主线程耗时

SurfaceFlinger 负责 Surface 的合成 , 一旦 SurfaceFlinger 主线程调用超时 , 就会产生掉帧 .

SurfaceFlinger 主线程耗时会也会导致 hwc service 和 crtc 不能及时完成, 也会阻塞应用的 binder 调用, 如 dequeueBuffer \ queueBuffer 等.

下图中的 SurfaceFlinger 主线程在后半部分明显超时:

SurfaceFlinger 主线程处理不及时导致应用卡顿(第一帧卡顿,后续都为黄帧)

2.屏下光感截图导致 SurfaceFlinger 渲染不及时

有的 Android 机型使用了屏下光感 , 屏下光感的实现方法也会影响 SurfaceFlinger 主线程的运行 . 屏下指纹需要频繁截图 , 来区分光线和屏幕的变化 , 进行对应的亮度变化, 但是其主线程截图的方法会导致 SurfaceFlinger 主线程被截图操作所耽误, 从而导致卡顿

3.WHC Service 执行耗时

hwc Service 耗时也会导致 SurfaceFlinger 下一帧不会做合成操作, 导致应用的 dequeueBuffer 和 setTransationState 方法被阻塞, 导致卡顿.
如下图, 可以看到 SurfaceFlinger 的掉帧情况, Binder 的阻塞情况 和 CRTC 的耗时情况

hwc 耗时

crtc 等待 hwc

4.CRTC 执行耗时

crtc 执行耗时的结果就是 SurfaceFlinger 下一帧不会做合成操作, 导致应用的 dequeueBuffer 和 setTransationState 方法被阻塞, 导致卡顿.
如下图, 可以看到 SurfaceFlinger 的掉帧情况, Binder 的阻塞情况 和 CRTC 的耗时情况

5.CPU 调度问题

重要任务跑小核性能不足导致卡顿

如下图 , RenderThread 跑到了小核, 导致这一帧执行时间过长,造成卡顿图片:

如下图 , cpu 频率对性能的影响图片:

优先级低未能及时获取 cpu 时间片导致卡顿

在调度器看来的低优先级任务 , 在用户这里未必是低优先级任务 , 他可能正在和 App 的主线程交互 , 或者正在和 system_server 进行交互

被 RT 进程抢占

App 主线程或者渲染线程被 RT 进程抢占也会导致系统卡顿或者响应慢 , Google 也意识到了这个问题 , 也在尝试在应用启动的时候 , 把 App 主线程和渲染线程的优先级也设置为 RT , 不过这个属性一直没开 , 因为会导致应用启动速度变慢.

大小核调度导致

大小核调度的问题通常表现在该跑在大核的任务跑到了小核 , 或者该在小核运行的任务却持续跑到大核 ,或者错误的被绑定在了某一个核心上 .

如下图, 这是一个 CTS 问题, CTS 主线程由于被绑定到了 cpu7 , 由于 cpu7 在执行 RenderThread , 所以主线程没有调度到, 导致 CTS 失败

6.触发 Thermal 导致限频

触发 Thermal 发热限频也有可能导致卡顿 , 这算是一种硬件级别的保护 , 如果手机已经过热 , 此时如果不进行干涉 , 那么可能会导致用户手机太烫而无法持续使用手机. 一般这个时候都会对系统的资源进行一些限制 , 比如降低 cpu\gpu 的最高频率之类的 , 这么做的话 , 势必也会对流畅性造成影响.

如果你手机非常热 , 而且变卡了 , 那么放下手机休息一会 , 查杀一下后台 , 或者重启一下手机 .

7.后台活动进程太多导致系统繁忙

后台进程活动太多,会导致系统非常繁忙, cpu \ io \ memory 等资源都会被占用, 这时候很容易出现卡顿问题 , 这也是系统这边经常会碰到的问题

CPU 繁忙

dumpsys cpuinfo 可以查看一段时间内 cpu 的使用情况

主线程调度不到 , 处于 Runnable 状态

当线程为 Runnable 状态的时候 , 调度器如果迟迟不能对齐进行调度 , 那么就会产生长时间的 Runnable 线程状态 , 导致错过 Vsync 而产生流畅性问题.

无关进程活跃耗时

无关进程通常是人为定义的 , 指的是与当前前台 App 运行无关的进程 , 这些活跃进程势必会对 App 主线程的调度产生影响 , 不管这些无关进程是系统的还是 App 自身的 , 或者是其他三方 App 的.

cpu 被占用

原因同上 , 当后台任务过多的时候 , cpu 资源就会异常紧缺 , 如下图就是在系统低内存的时候 , HeapTask 和 kswapD 几乎占满了整个 cpu , 在疯狂地向系统申请内存 .

System 锁

system_server 的 AMS 锁和 WMS 锁 , 在系统异常的情况下 , 会变得非常严重 , 如下图所示 , 许多系统的关键任务都被阻塞 , 等待锁的释放 , 这时候如果有 App 发来的 Binder 请求带锁 , 那么也会进入等待状态 , 这时候 App 就会产生性能问题 ; 如果此时做 Window 动画 , 那么 system_server 的这些锁也会导致窗口动画卡顿

8.Layer过多导致 SurfaceFlinger Layer Compute 耗时

Android P 修改了 Layer 的计算方法 , 把这部分放到了 SurfaceFlinger 主线程去执行, 如果后台 Layer 过多, 就会导致 SurfaceFlinger 在执行 rebuildLayerStacks 的时候耗时 , 导致 SurfaceFlinger 主线程执行时间过长.

所以在使用 Android 系统的时候 , 记得多用多任务清理后台任务.

9.Input 报点不均匀

如果出现 Input 报点不均匀或者没有报点的情况, 那么主线程由于没有收到 Input 事件, 所以不去做绘制, 也会导致卡顿
如下图 , 这是一个连续滑动的 Systrace 图 , 最下面两行是 InputReader 和 InputDispatcher , 可以看到在滑动的过程中, InputReader 和 InputDispatcher 没有读出来 Input 事件, 导致卡顿

10.LMK 频繁工作抢占 cpu

LMK 工作时, 会占用 cpu 资源 , 其表现主要有下面几点

  1. CPU 资源 : 由于 LMK 杀掉的进程通常都是一些 Cache 或者 Service , 这些进程由于低内存被杀之后 , 通常会很快就被其主进程拉起来, 然后又被 LMK 杀掉, 从而进入了一种循环. 由于起进程是一件很消耗 cpu 的操作, 所以如果后台一直有进程被杀和重启, 那么前台的进程很容易出现卡顿
  2. Memory : 由于低内存的原因, 很容易触发各个进程的 GC , 如下图的 CPU 状态可以看到, 用于内存回收的 HeapTaskDeamon 出现非常频繁
  3. IO : 低内存会导致磁盘 IO 变多, 如果频繁进行磁盘 IO , 由于磁盘IO 很慢, 那么主线程会有很多进程处于等 IO 的状态, 也就是我们经常看到的 Uninterruptible Sleep

11.低内存导致 IO 耗时

低内存情况下, 很容易出现主线程 IO 从而导致应用卡顿

主线程 IO 导致卡顿

主线程 IO 导致应用启动速度慢

滑动列表时候 IO 导致卡顿

12.GPU 合成导致 SurfaceFlinger 耗时

当 SurfaceFlinger 有 GPU 合成时, 其主线程的执行时间就会变长, 也会导致合成不及时而卡顿

13.KSWAPD 跑大核

低内存时, kswapd 由于负载比较高 , 其 cpu 占用比较高, 且经常会跑到大核上 , 导致机器发热限频, 或者抢占主线程的 cpu 时间片

14.SurfaceFlinger Vsync 不均匀

SurfaceFlinger 有时候会出现 Vsync 不均匀的情况, 不均匀指的是 Vsync 间隔会持续地变化, 一会大一会小, 就会导致用户看到的画面不均匀, 有卡顿感
如下图 , 可以明显看到 SurfaceFlinger 的 VSYNC-sf 这一行间隔是不一样的. 这种问题一般是由于 SurfaceFlinger 这边的修改或者 HWC 的修改导致的 .

15.三方应用使用 Accessibility 服务导致系统卡顿

三方应用如果使用 Accessibility 服务监听了 Input 事件的话, InputDispatcher 的行为就会与预期的出现偏差, 导致 InputDispatcher 没有及时把事件传给主线程导致卡顿

总结

Android 原生系统是一个不断进化的过程 , 目前已经进化到了 Android Q , 每个版本都会解决非常多的性能问题 , 同时也会引进一些问题 ; 到了手机厂商这里 , 由于硬件差异和软件定制 , 会在系统中加入大量的自己的代码 , 这无疑也会影响系统的性能 .

上面列出的这些影响流畅性的案例 , 只是 Android 系统开发中遇到的性能问题的冰山一角 , 任何一个问题都会对用户的使用产生影响 , 这也是为什么手机厂商越来越重视系统优化 . 手机厂商非常重视开发过程中和用户使用过程中遇到的性能问题 , 并开发和提出各项优化措施 , 从硬件到软件 , 从用户行为优化到系统策略动态学习 . 这也是为什么现在的手机厂商的系统越做越好 , 质量越来越高的一个原因 , 那些不重视质量只重视设计和产品的手机厂商 , 都渐渐地被消费者淘汰了.

大家可以看看这个问题 : 当手机厂商说安卓手机性能优化的时候,他们到底在做什么

这也是流畅性的一个系列文章中的一篇 , 可以点击下面的链接查看本系列的其他文章.

0. Android 中的卡顿丢帧原因概述 - 方法论
1. Android 中的卡顿丢帧原因概述 - 系统篇
2. Android 中的卡顿丢帧原因概述 - 应用篇
3. Android 中的卡顿丢帧原因概述 - 低内存篇

本文知乎地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 中的卡顿丢帧原因概述 - 系统篇

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 中的卡顿丢帧原因概述 - 方法论

作者 Gracker
2019年9月5日 12:56

Android 手机使用中的卡顿问题 , 一般来说手机厂商和 App 开发商都会非常重视 , 所以不管是手机厂商还是 App 开发者 , 都会对卡顿问题非常重视 , 内部一般也会有专门的基础组或者优化组来进行优化 .

目前市面上有一些非常棒的第三方性能监控工具 , 比如腾讯的 Matrix ; 手机厂商一般也会有自己的性能监控方案 , 由于可以修改源码和避免权限问题 , 所以手机厂商可以拿到更多的数据 , 分析起来也会更方便一些.

说回流畅度 , 其实就是操作过程中的丢帧 , 本来一秒中画面需要更新 60 帧,但是如果这期间只更新了 55 帧 , 那么在用户看来就是丢帧了 , 主观感觉就是卡了 , 尤其是帧率波动 , 用户的感知会更明显. 引起丢帧的原因非常多, 有硬件层面的 , 有软件层面的 , 也有 App 自身的问题. 所以这一部分我分为四篇文章去讲 , 会简单讲一下哪些原因会用户觉得卡顿丢帧 :

0. Android 中的卡顿丢帧原因概述 - 方法论
1. Android 中的卡顿丢帧原因概述 - 系统篇
2. Android 中的卡顿丢帧原因概述 - 应用篇
3. Android 中的卡顿丢帧原因概述 - 低内存篇

1. 流畅度相关工作内容概述

作为手机厂商优化组的一员 , 我有必要在开始之前简单描述一下我们工作的流程 . 系统开发的过程中, 有很多引起 Android 卡顿的原因,但是用户和测试感受最直观的是正在使用的应用掉帧和不流畅 . 由于测试和用户没有办法直接确定卡顿的原因, 所以一般会直接将 Bug 提到我们这边, 所以我们的角色更像是一个卡顿问题接口人, 负责分析引起卡顿的原因, 再把 Bug 分配给对应的模块负责人去解决 , 如框架 \ App \ 多媒体 \ Display \ BSP 等.

所以直接由我们来解决的问题并不是很多, 我们更多的时候是通过专门的分析工具 , 结合源码来定位和分析问题 , 最多使用的工具如下:

  1. Systrace\strace\ftrace : 从整个系统的层面来看问题的大致原因
  2. MethodTrace : 可以从进程的角度 , 以详细调用栈的形式来显示
  3. Android Studio 的 Profile 工具
  4. MAT : 用来分析内存问题
  5. Log : LogReport 抓取或者录制的 Log , 里面包含大量的信息 , 包括各种常规 Log (Main Log , System Log , Event Log , Kernel Log , Crash Log 等) , 也包含了厂商自己加的一些 Log ( Power Log , Performance Log 等) , 也包含事故发生时候的截图 \ 录制的视频等
  6. 复现视频
  7. 本地复现等

确定卡顿的根本原因 , 这需要对 Android App 开发 \ Android Framework 知识 \ Display 知识 \ Linux Kernel 知识有一定的了解 , 知道基本的工作流程 , 并能熟练使用对应的工具 , 区分不同的场景 , 迅速找到问题的原因 , 然后和相关模块的负责人一起讨论优化.

对于一些系统全局性的方案则需要与对应的模块负责人一起分析和解决, 必要的时候我们也会开发一些 Feature 来解决问题 .

2. 性能问题分析的一些工具和套路

应用卡顿问题的原因比较多, 在数据埋点还没有完善的情况下, 更多的依赖 Systrace 来从全局的角度来分析卡顿的具体原因:

  1. Systrace 分析
    1. 首先确认卡顿的 App
    2. 通过 App 的主线程和 SurfaceFlinger 的主线程信息可以确定卡顿的现场
    3. 分析 Systrace , Systrace 的分析需要一定的知识储备 : 需要知道 Systrace 每一个模块展示的内容是如何与用户感受到的内容相对应的 ; 需要知道 Systrace 上各个模块的交互式如何展示的 ; 需要知道 Binder 调用信息 ; 需要会看 Kernel 信息 (后续会继续完善 Systrace 系列)
      1. 如果是 App 主线程耗时, 则分析 App 主线程的原因 ( 案例里有 App 的卡顿原因 )
      2. 如果是 System 的问题, 则需要分析 System_Server \ SurfaceFlinger \ HWC \ CRTC \ CPU 等 ( 详细参考下面系统卡顿原因)
  2. TraceView + 源码分析
    1. 使用 Systrace 确定原因后, 可以使用 TraceView 结合源码查看对应的代码逻辑 , Android Studio 的 Profile 工具可以以进程为单位 , 进行 Method 的 Profile , 可以打出非常详细的函数调用栈 , 并且可以与 Systrace 相对应
    2. 源码分析可以使用 Android Studio 进行断点调试 App 或者 Framework , 观察 Debug 信息是否与预期相符
  3. 很多问题也需要借助 Log 工具抓上来的 Log 进行分析 , Log 分析 Log 里面一些比较重要的点 (一般从 Log 里面很难确定卡顿的原因, 但是可以结合 Systrace 做一定的辅助分析)
    1. 截图 : 确定卡顿发生的时间点 \ 卡顿的界面 (如果没有尽量提供)
    2. dumpsys meminfo 信息
    3. dumpsys cpuinfo 信息
    4. “Slow dispatch” 和 “Slow delivery” Log 信息
    5. 卡顿发生的一段时间内的 EventLog , 还原卡顿时候用户的操作
  4. 本地尝试复现
    1. 可以录高速录像, 观察细节,如果必现,可以让测试这边提供录像.
    2. 过滤 Log , 找到卡顿时候的异常 Log
    3. 多抓几份 Systrace , 有助于确定原因
  5. 可以让测试提供 LogReport 中没有的一些信息, 来分析当时用户的手机的整体的状态.
    1. adb shell dumpsys activity oom
    2. adb shell dumpsys meminfo
    3. adb shell cat /proc/buddyinfo
    4. adb shell dumpsys cpuinfo
    5. adb shell dumpsys input
    6. adb shell dumpsys window

3. 通过性能数据数据分析

由于用户反馈的不确定性 , 和内部测试的不完备性 , 通过系统或者 App 的性能埋点数据来做分析 , 是改进系统的一个好的方法 . 一方面不用用户主动参与 , 一方面有大量的数据可以来做分析 , 看趋势 .

目前国内各大手机厂商和 App 厂商基本都有自己的 APM 平台 , 负责监控 App 或者系统的监控程度 , 来做对应的优化方案 , 比如腾讯的 Matrix 平台已经监控了下面这些内容 , 其他的 App 厂商可以直接接入

-w1256

手机厂商由于有代码权限 , 所以可以采集到更多的数据 , 比如 Kernel 相关的数据 : cpu 负载 \ io 负载 \ Memory 负载 \ FSync \ 异常监控 \ 温度监控 \ 存储大小监控 等 , 每一个大项又都有几十个小项 . 所以可以监控的数据会非常多 , 遇到问题也可以从多个技术指标去分析 . 这就需要在这方面经验非常丰富的团队 , 去定义这些监控指标 , 确定最终要收集那些信息 , 收集上来的数据如何去分析等.

至于后续的优化工作 , 就考验各个厂商的研发能力了 , 正如伟琳在这篇文章:那些年,我们一起经历过的 Android 系统性能优化 所说 , 目前能力比较强的手机厂商 , 都在底层各个模块 , 结合硬件做优化 , 因为归根结底都是资源的分配 ; 而一些研发实力不是很强的厂商 , 则重点还是围绕在根据场景分配资源.

4. 总结

这里简单概述了一下流畅性问题的一般分析思路和分析工具 , 而且由于我的方向主要在 Framework 和 App , 所以很多东西都是从上层的角度来说的 , 想必 Kernel 优化团队会有更好的角度和分析 .

各个厂商的优化大家可以看看这篇总结 , 那些年,我们一起经历过的 Android 系统性能优化 , 华米 OV 都有涉及 , 下面摘录了一段总结 , 大家可以看看

展望一下,这里想把手机厂商分为三类:

  1. 一类是苹果,自己研发芯片和核心元件,有自己的OS和生态;
  2. 二类是三星、华为,自己研发芯片和核心元件(当然华为和三星还是有所区别),共享 Android OS 和生态,当然三星在本土化这一块做的是不如华为和其他 Top 厂商的;
  3. 三类是其他 Android 手机厂商,芯片和核心元件来自于不同供应商,共享 Android OS和生态;

从技术层面看:

  1. 苹果始终会是在性能的第一阵营,可以顺利推行从硬件到 OS 到 APP 级别的任何性能保障方案;
  2. 三星、华为属于第二阵营,可以实现芯片-OS层面的整合优化;
  3. 其他 Top Android 手机厂商差距不会太大,他们有多个不同的 SoC 供应商,方案有差异,非常芯片底层的地方,往往不会去涉及,更多是做纯软件层面的策略性的优化,有价值但是不容易形成壁垒,注意这个不容易形成壁垒指的是在 top 厂商中间,一些小的厂商往往还是心有余而力不足。不过还是很期待看到有更多的突破出现。

这也是流畅性的一个系列文章中的一篇 , 可以点击下面的链接查看本系列的其他文章.

0. Android 中的卡顿丢帧原因概述 - 方法论
1. Android 中的卡顿丢帧原因概述 - 系统篇
2. Android 中的卡顿丢帧原因概述 - 应用篇

本文知乎地址

由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎界面
知乎 - Android 中的卡顿丢帧原因概述 - 方法论

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

Android 中的 Activity Launch Mode 详解

作者 Gracker
2019年9月1日 12:37

Android 中的 Activity 有几种比较重要的启动模式,Standard\SingleTop\SingleTask\SingleInstance , 每一种启动模式有不同的使用场景, 网上也有许多分析这个的文章, 这里我以 Demo 的模式, 从 Activity 栈的角度来展示不同启动模式下的 Activity 的行为.

Activity 栈是一个先进后出的数据结构, 各位可以关注在每一步操作之后, 栈内容那一栏 , 可以更好地帮助理解不同的启动模式.

Demo 比较简单, 我也放到了 Github 上 , https://github.com/Gracker/AndroidLaunchModeTest , 有兴趣的可以自己跑一下 , 看看结果 , 只需要修改 StandardActivity 里面的跳转 Activity 就可以了.

Standard 标准模式

1
android:launchMode="standard"

最基本的模式,每次启动都会创建一个新的 Activity

模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. 启动 Activity
MainActivity

//栈内容
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 2. 启动 StandardActivity
MainActivity -> StandardActivity

//栈内容
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 3. 启动 StandardActivity
MainActivity -> StandardActivity -> StandardActivity

//栈内容
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

SingleTop 栈顶复用模式

1
android:launchMode="singleTop"

如果当前 Activity 已经在栈顶,那么其 onNewIntent 会被调用;否则会重新创建 Activity

测试1 : SingleTopActivity 不在栈顶

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
// 1. 启动 MainActivity
MainActivity

//栈内容
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 2. 启动 StandardActivity
MainActivity -> StandardActivity

//栈内容
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 3. 启动 SingleTopActivity
MainActivity -> StandardActivity -> SingleTopActivity
//栈内容
com.example.launchmodetest/.SingleTopActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------

// 4. 启动 StandardActivity
MainActivity -> StandardActivity -> SingleTopActivity -> StandardActivity
//栈内容
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.SingleTopActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 5. 启动 SingleTopActivity:
MainActivity -> StandardActivity -> SingleTopActivity -> StandardActivity -> SingleTopActivity
//栈内容
com.example.launchmodetest/.SingleTopActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.SingleTopActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

//这里由于第三个 SingleTopActivity 不在栈顶,栈顶是 StandardActivity ,所以启动新的 SingleTopActivity 时会重新创建 SingleTopActivity

测试2 : SingleTopActivity 在栈顶

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
// 1. 启动 MainActivity
MainActivity
//栈内容
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 2. 启动 StandardActivity
MainActivity -> StandardActivity
//栈内容
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 3. 启动 SingleTopActivity
MainActivity -> StandardActivity -> SingleTopActivity
//栈内容
com.example.launchmodetest/.SingleTopActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 4. 启动 SingleTopActivity
MainActivity -> StandardActivity -> SingleTopActivity -> SingleTopActivity
//栈内容
com.example.launchmodetest/.SingleTopActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

//SingleTopActivity 收到 am_new_intent ,而不是创建新的 Activity

SingleTask 栈内复用模式

1
android:launchMode="singleTask"
  1. 如果不加 Affinity , 那么 SingleTask 标记的 Activity 创建还是在当前的 Task 中
  2. SingleTask 标记的 Activity 是栈内复用模式,如果当前 Task 内没有这个 Activity,那么创建新的 Activity,如果当前 Task 内有这个 Activity,不管他在 Task 的哪个位置,都会直接复用这个 Activity (收到 onNewIntent)
  3. 如果栈内复用,那么会 Clear Task 中这个 Activity 上面的其他的 Activity

测试1:SingleTask(Without Affinity)

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
// 1. 启动 MainActivity:
MainActivity
//栈内容:
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 2. 启动 StandardActivity
MainActivity -> StandardActivity
//栈内容:
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 3. 启动 SingleTaskActivity
MainActivity -> StandardActivity -> SingleTaskActivity
//栈内容:
com.example.launchmodetest/.SingleTaskActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 4. 启动 StandardActivity
MainActivity -> StandardActivity -> SingleTaskActivity -> StandardActivity
//栈内容:
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.SingleTaskActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 5. 启动 SingleTaskActivity
MainActivity -> StandardActivity -> SingleTaskActivity -> StandardActivity -> SingleTaskActivity
//栈内容:
com.example.launchmodetest/.SingleTaskActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

//备注:SingleTaskActivity 收到 am_new_intent ,将其上面的 StandardActivity Clear 调

测试2:SingleTask(WithAffinity)

在 Manifest 中设置了 android:taskAffinity=”” 之后,启动 SingleTask 会启动一个新的 Task

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
// 1. 启动 MainActivity
MainActivity

//栈0内容:
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 2. 启动 StandardActivity
MainActivity -> StandardActivity

//栈0内容:
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 3. 启动 SingleTaskWithAffinity
MainActivity -> StandardActivity -> SingleTaskWithAffinity

//栈1内容:
com.example.launchmodetest/.SingleTaskWithAffinity
//栈0内容:
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 4. 启动 StandardActivity
MainActivity -> StandardActivity -> SingleTaskWithAffinity -> StandardActivity

//栈1内容:
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.SingleTaskWithAffinity
//栈0内容:
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 5. 启动 SingleTaskWithAffinity
MainActivity -> StandardActivity -> SingleTaskWithAffinity -> StandardActivity -> SingleTaskWithAffinity

//栈1内容:
com.example.launchmodetest/.SingleTaskWithAffinity
//栈0内容:
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------

总结

  1. 与 SingleTask 相比, SingleTaskWithAffinity 会创建新的 Stack
  2. 在 SingleTaskWithAffinity 启动 StandardActivity , 这个 StandardActivity 与 SingleTaskWithAffinity 在同一个栈
  3. 在栈 0 里面再启动 SingleTaskWithAffinity ,不会创建新的 Task
  4. 多任务里面会出现 SingleTaskWithAffinity

SingleInstance 单实例模式

1
android:launchMode="singleInstance"

单示例模式顾名思义,启动时,无论从哪里启动都会给 A 创建一个唯一的任务栈,后续的创建都不会再创建新的 A,除非 A 被销毁了

测试1:SingleInstance (Without Affinity)

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
taskAffinity=com.example.launchmodetest

// 1. 启动 MainActivity
MainActivity

//栈0内容
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 2. 启动 StandardActivity
MainActivity -> StandardActivity

//栈0内容
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 3. 启动 SingleInstanceActivity
MainActivity -> StandardActivity -> SingleInstanceActivity

//栈1内容(多任务里面没有 Task)
com.example.launchmodetest/.SingleInstanceActivity
//栈0内容
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 4. 启动 StandardActivity
MainActivity -> StandardActivity -> SingleInstanceActivity -> StandardActivity

//栈1内容(多任务里面没有 Task)
com.example.launchmodetest/.SingleInstanceActivity
//栈0内容:
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 5. 启动 SingleInstanceActivity
MainActivity -> StandardActivity -> SingleInstanceActivity -> StandardActivity -> SingleInstanceActivity

//栈1内: (多任务里面没有 Task)
com.example.launchmodetest/.SingleInstanceActivity
//栈0内容
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 6. 启动 StandardActivity
MainActivity -> StandardActivity -> SingleInstanceActivity -> StandardActivity -> SingleInstanceActivity -> StandardActivity

//栈1内: (多任务里面没有 Task)
com.example.launchmodetest/.SingleInstanceActivity
//栈0内容
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 7. 启动 SingleInstanceActivity
MainActivity -> StandardActivity -> SingleInstanceActivity -> StandardActivity -> SingleInstanceActivity -> StandardActivity -> SingleInstanceActivity

//栈1内容 (多任务里面没有 Task)
com.example.launchmodetest/.SingleInstanceActivity
//栈0内容
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

总结1

  1. SingleInstanceActivity 会创建新的 Task ,但是不会在多任务中出现
  2. SingleInstanceActivity 是全局唯一的,如果复用,其 onNewIntent 会被调用
  3. SingleInstanceActivity 启动新的 Activity,新的 Activity 不会在当前的 Task 里面,而是会回到上一个 Task 里面

测试2: SingleInstance (With Affinity)

在 Manifest 中设置了 android:taskAffinity=”” 之后,启动 SingleInstanceActivity 会出现在多任务中 ,其余的表现与没有设置 Affinity 一致

1
2
taskAffinity=null

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
// 1. 启动 MainActivity
MainActivity

//栈0内容:
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 2. 启动 StandardActivity
MainActivity -> StandardActivity

//栈0内容:
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 3. 启动 SingleInstanceWithAffinityActivity
MainActivity -> StandardActivity -> SingleInstanceWithAffinityActivity

//栈1内容:(多任务里面有 Task)
com.example.launchmodetest/.SingleInstanceWithAffinityActivity

//栈0内容:
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 4. 启动 StandardActivity
MainActivity -> StandardActivity -> SingleInstanceWithAffinityActivity -> StandardActivity

//栈1内容:(多任务里面有 Task)
com.example.launchmodetest/.SingleInstanceWithAffinityActivity

//栈0内容:
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 5. 启动 SingleInstanceWithAffinityActivity
MainActivity -> StandardActivity -> SingleInstanceWithAffinityActivity -> StandardActivity -> SingleInstanceWithAffinityActivity

//栈1内容:(多任务里面有 Task)
com.example.launchmodetest/.SingleInstanceWithAffinityActivity

//栈0内容:
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 6. 启动 StandardActivity
MainActivity -> StandardActivity -> SingleInstanceWithAffinityActivity -> StandardActivity -> SingleInstanceWithAffinityActivity -> StandardActivity

//栈1内容:(多任务里面有 Task)
com.example.launchmodetest/.SingleInstanceWithAffinityActivity
//栈0内容
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

-------------------------------------------------------------------
// 7. 启动 SingleInstanceWithAffinityActivity
MainActivity -> StandardActivity -> SingleInstanceWithAffinityActivity -> StandardActivity -> SingleInstanceWithAffinityActivity -> StandardActivity -> SingleInstanceWithAffinityActivity

//栈1内容:(多任务里面有 Task)
com.example.launchmodetest/.SingleInstanceWithAffinityActivity
//栈0内容:
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.StandardActivity
com.example.launchmodetest/.MainActivity

一些概念

TaskAffinity

1
taskAffinity=null

与 Activity 有着亲和关系的任务。从概念上讲,具有相同亲和关系的 Activity 归属同一Task(从用户的角度来看,则是归属同一“ Application ”)。 Task 的亲和关系由其根 Activity 的亲和关系确定。

亲和关系确定两件事 - Activity 更改到的父项 Task(请参阅 allowTaskReparenting 属性)和通过 FLAG_ACTIVITY_NEW_TASK 标志启动 Activity 时将用来容纳它的 Task。
默认情况下,应用中的所有 Activity 都具有相同的亲和关系。您可以设置该属性来以不同方式组合它们,甚至可以将在不同应用中定义的 Activity 置于同一 Task 内。 要指定 Activity 与任何 Task 均无亲和关系,请将其设置为空字符串。

如果未设置该属性,则 Activity 继承为应用设置的亲和关系(请参阅 元素的 taskAffinity 属性)。 应用默认亲和关系的名称是 元素设置的软件包名称。

ActivityRecord、TaskRecord、ActivityStack 之间的关系

  1. 一个 ActivityRecord 对应一个 Activity 实例,保存了一个 Activity 的所有信息 ; 但是一个 Activity可能会有多个 ActivityRecord ,因为 Activity 可以被多次启动,这个主要取决于其启动模式。
  2. 一个 TaskRecord 由一个或者多个 ActivityRecord 组成,这就是我们常说的任务栈,具有后进先出的特点
  3. ActivityStack 则是用来管理 TaskRecord 的,包含了多个 TaskRecord

(From http://gityuan.com/2017/06/11/activity_record/)

  1. 一般地,对于没有分屏功能以及虚拟屏的情况下,ActivityStackSupervisor 与ActivityDisplay 都是系统唯一;
  2. ActivityDisplay 主要有 Home Stack 、 App Stack、Recents Stack 这三个栈;
  3. 每个 ActivityStack 中可以有若干个 TaskRecord 对象;
  4. 每个 TaskRecord 包含如果若干个 ActivityRecord 对象;
  5. 每个 ActivityRecord记 录一个 Activity 信息。

下面是一个 dump 的例子,可以看到当前手机的 ActivityRecord、TaskRecord、ActivityStack
(adb shell dumpsys activity containers)

Activity 的几种类型

1
2
3
4
5
6
7
8
9
10
/** Activity type is currently not defined. */
public static final int ACTIVITY_TYPE_UNDEFINED = 0;
/** Standard activity type. Nothing special about the activity... */
public static final int ACTIVITY_TYPE_STANDARD = 1;
/** Home/Launcher activity type. */
public static final int ACTIVITY_TYPE_HOME = 2;
/** Recents/Overview activity type. There is only one activity with this type in the system. */
public static final int ACTIVITY_TYPE_RECENTS = 3;
/** Assistant activity type. */
public static final int ACTIVITY_TYPE_ASSISTANT = 4;

如果觉得文章有帮助, 欢迎分享到社交网站 , 希望能帮到大家.

关于我 && 博客

  1. 关于我 , 非常希望和大家一起交流 , 共同进步 .
  2. 博客内容导航
  3. 优秀博客文章记录 - Android 性能优化必知必会

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

❌
❌