随着Android 版本的升级,rom越来越大,功能基础的越来越多,Android 启动时间随之也越来越长,本文将围绕Android 10.0 的开机时长优化展开探讨,软件环境如下:
平台:qcom 8970 Android 10.0
DDR/Flash: 4G /64G
开机时长:21s
为了更好地分析开机耗时原因,有必要了解系统在开机过程中执行了哪些动作,在结合数据找出哪些过程耗时较长,再进一步分析耗时原因。
Android 开机流程大致如下:
为了搞清楚开机时长到底耗时多久,什么任务耗时,措施优化程度如何,需要使用合适的方法和工具进行分析和验证,下面将从以下几个方面展开分析。
通过秒表手动掐表计时能粗略得到手机开机时长,由于设备开机过程存在一定偶然和不确定性,为了提高结果准确性,于是适当增加样本数量。
计时方法:设备关机后,从按下power键唤醒设备,出现画面开始秒表计时,到设备启动显示Launcher画面结束秒表计时,取这段时间为开机时长。
采集样本数据如下:
序号 | 开机时长(秒) |
---|---|
1 | 20.0 |
2 | 17.5 |
3 | 17.7 |
4 | 18.12 |
5 | 17.5 |
6 | 17.4 |
7 | 18.0 |
8 | 17.7 |
9 | 18.3 |
10 | 17.9 |
平均时长 | 17.8 |
样本1由于是烧录完系统第一次开机,很多程序进行首次初始化,因此开机时长会更长一点,从数据上可以看出,后续结果基本稳定,因此这里舍弃第一次次结果,取后9次数据
平均值作为参考结果。
bootchart是一个用于Linux启动过程性能分析的开源工具软件,在系统启动过程中自动手机CPU占用率,磁盘吞吐率,进程等信息,并以图形方式显示分析结果,指导优化开机启动过程。
bootchart在android源码的路径为
system\core\init\bootchart.cpp
static Result do_bootchart_start() { //启动bootchart
std::string start;
///data/bootchart/enabled标志文件存在才能启动bootchart
if (!android::base::ReadFileToString("/data/bootchart/enabled", &start)) {
return Success();
}
启动bootchart主线程
g_bootcharting_thread = new std::thread(bootchart_thread_main);
return Success();
}
static void bootchart_thread_main() {
// Open log files.//在/data/bootchart/路径下创建proc_stat.log、proc_ps.log、proc_diskstats.log
auto stat_log = fopen_unique("/data/bootchart/proc_stat.log", "we");
if (!stat_log) return;
auto proc_log = fopen_unique("/data/bootchart/proc_ps.log", "we");
if (!proc_log) return;
auto disk_log = fopen_unique("/data/bootchart/proc_diskstats.log", "we");
if (!disk_log) return;
log_header();
while (true) {
{
std::unique_lock lock(g_bootcharting_finished_mutex);
g_bootcharting_finished_cv.wait_for(lock, 200ms);
if (g_bootcharting_finished) break;
}
log_file(&*stat_log, "/proc/stat");
log_file(&*disk_log, "/proc/diskstats");
log_processes(&*proc_log);
}
}
从上面代码可以看出需要在设备中创建/data/bootchart/enabled
xxx:/ # su
xxx:/ # touch /data/bootchart/enabled //创建/data/bootchart/enabled文件
xxx:/ # ls -al /data/bootchart/
total 11
drwxr-xr-x 2 shell shell 3488 2022-12-01 09:05 .
drwxrwx--x 52 system system 4096 2022-12-01 08:54 ..
-rw-rw-rw- 1 root root 0 2022-12-01 09:06 enabled
重启设备可以发现生成以下文件:
xxx:/ # ls -al /data/bootchart/
total 4072
drwxr-xr-x 2 shell shell 3488 2022-12-01 09:09 .
drwxrwx--x 52 system system 4096 2022-12-01 09:09 ..
-rw-rw-rw- 1 root root 0 2022-12-01 09:06 enabled
-rw-rw-rw- 1 root root 1326 2022-12-01 09:09 header
-rw-rw-rw- 1 root root 214560 2022-12-01 09:09 proc_diskstats.log
-rw-rw-rw- 1 root root 3892696 2022-12-01 09:09 proc_ps.log
-rw-rw-rw- 1 root root 39506 2022-12-01 09:09 proc_stat.log
首先需要在源码的系统环境中安装bootchart,由于不同ubuntu版本差异的原因,推荐使用ubuntu16.04
sudo apt upgrade
sudo apt update
//ubuntu16.04
sudo apt install bootchart
//ubuntu18.04以上
sudo apt install bootchart pybootchartgui
可以通过源码的脚本自动生成bootchart.png:
mart!nhu@xxx:~/repo_work/AOSP$ system/core/init/grab-bootchart.sh
parsing '/tmp/android-bootchart/bootchart.tgz'
parsing 'header'
parsing 'proc_stat.log'
parsing 'proc_ps.log'
warning: no parent for pid '2' with ppid '0'
parsing 'proc_diskstats.log'
merged 0 logger processes
pruned 142 process, 0 exploders, 19 threads, and 0 runs
False
bootchart written to 'bootchart.png'
system/core/init/grab-bootchart.sh: 21: system/core/init/grab-bootchart.sh: gnome-open: not found
Clean up /tmp/android-bootchart/ and ./bootchart.png when done
mart!nhu@xxx:~/repo_work/AOSP$
脚本源码解读
system\core\init\grab-bootchart.sh
#!/bin/sh
#
# This script is used to retrieve a bootchart log generated by init.
# All options are passed to adb, for better or for worse.
# See the readme in this directory for more on bootcharting.
#临时存放bootchart.tgz和bootchart.png路径
TMPDIR=/tmp/android-bootchart
rm -rf $TMPDIR
mkdir -p $TMPDIR
LOGROOT=/data/bootchart
TARBALL=bootchart.tgz
FILES="header proc_stat.log proc_ps.log proc_diskstats.log"
#获取设备/data/bootchart/文件,保存至临时路径
for f in $FILES; do
adb "${@}" pull $LOGROOT/$f $TMPDIR/$f 2>&1 > /dev/null
done
#打包为bootchart.tgz
(cd $TMPDIR && tar -czf $TARBALL $FILES)
#--android 9.0--#
#bootchart ${TMPDIR}/${TARBALL}
#gnome-open ${TARBALL%.tgz}.png
#--android 10.0--解析bootchart.tgz生成bootchart.png#
pybootchartgui ${TMPDIR}/${TARBALL}
#打开bootchart.png
xdg-open ${TARBALL%.tgz}.png
echo "Clean up ${TMPDIR}/ and ./${TARBALL%.tgz}.png when done"
从脚本中不难看出:
如果grab-bootchart.sh脚本运行出错,需要自行分析处理,或者按以下步骤尝试手动bootchart
处理
$sudo apt install bootchart
$bootchart --version
bootchart v0.0.0
$cd /data/bootchart/
$ tar -czf bootchart.tgz header *.log //将/data/bootchart/ header和3个log文件打包为bootchart.tgz
mart!nhu:/data/bootchart $ ls -al
total 4692
drw-r--r-- 2 shell shell 3488 2022-12-01 10:13 .
drwxrwx--x 52 system system 4096 2022-12-01 09:09 ..
-rw-rw-rw- 1 root root 630905 2022-12-01 10:13 bootchart.tgz
-rw-r--r-- 1 root root 0 2022-12-01 09:06 enabled
-rw-r--r-- 1 root root 1326 2022-12-01 09:09 header
-rw-r--r-- 1 root root 214560 2022-12-01 09:09 proc_diskstats.log
-rw-r--r-- 1 root root 3892696 2022-12-01 09:09 proc_ps.log
-rw-r--r-- 1 root root 39506 2022-12-01 09:09 proc_stat.log
将bootchart.tgz放至ubuntu(推荐16.04)环境下,使用bootchart生成bootchart.png
$ bootchart bootchart.tgz parsing 'bootchart.tgz'
parsing 'bootchart.tgz'
parsing 'header'
parsing 'proc_diskstats.log'
parsing 'proc_ps.log'
warning: no parent for pid '2' with ppid '0'
parsing 'proc_stat.log'
warning: path 'parsing' does not exist, ignoring.
parsing 'bootchart.tgz'
parsing 'header'
parsing 'proc_diskstats.log'
parsing 'proc_ps.log'
warning: no parent for pid '2' with ppid '0'
parsing 'proc_stat.log'
merged 0 logger processes
pruned 231 process, 0 exploders, 8 threads, and 1 runs
False
bootchart written to 'bootchart.png'
bootchart.png分为四部分:
图表以时间线为轴,每一小格表示1秒。由于bootchart的测量时间段是init进程启动之后,不包含uboot和kernel的启动时间,time记录的时间为bootchart从init启动后至boot.completed=1所耗时间。
默认无优化措施软件10次bootchart time数据如下:
序号 | time(秒) |
---|---|
1 | 14.94 |
2 | 15.20 |
3 | 15.39 |
4 | 15.04 |
5 | 15.26 |
6 | 15.46 |
7 | 15.46 |
8 | 14.90 |
9 | 15.22 |
10 | 15.22 |
平均 | 15.21 |
这里的平均结果将与优化措施进行对比。
结合3.1的数据结果,为了更好的分析开机耗时原因,可以抓取对应的log进行分析。
adb 无法抓到完整的开机log,如果有条件可以通过串口抓到完成的开机log,分为以下几个模块分析开机耗时情况:
使用adb 获取开机日志dmesg
adb shell "dmesg" >boot.log
分析dmesg log查找一些重复且可疑的打印:
avc: denied表示SeLinux安全策略拒绝了某些进程的某些动作,例如访问系统熟悉,读写app文件数据等。频繁的avc denied占用了一定系统资源,增加了开机耗时。
例如avc log如下:
12-29 10:08:14.769 638 638 I [email protected]: type=1400 audit(0.0:79): avc: denied { read } for name="present" dev="sysfs" ino=42693 scontext=u:r:hal_health_default:s0 tcontext=u:object_r:sysfs:s0 tclass=file permissive=1
12-29 10:08:14.769 638 638 I [email protected]: type=1400 audit(0.0:80): avc: denied { open } for path="/sys/devices/platform/soc/4a84000.i2c/i2c-0/0-000b/power_supply/battery/present" dev="sysfs" ino=42693 scontext=u:r:hal_health_default:s0 tcontext=u:object_r:sysfs:s0 tclass=file permissive=1
avc: denied { read } for name=“present” dev=“sysfs” ino=42693 scontext=u:r:hal_health_default:s0 tcontext=u:object_r:sysfs:s0 tclass=file permissive=1
表示hal_health_default进程对sysfs的file文件类型缺少read权限
avc: denied { open } for path=“/sys/devices/platform/soc/4a84000.i2c/i2c-0/0-000b/power_supply/battery/present” dev=“sysfs” ino=42693 scontext=u:r:hal_health_default:s0 tcontext=u:object_r:sysfs:s0 tclass=file permissive=1
表示hal_health_default进程对sysfs的file文件类型缺少open权限
修改公式如下:
通常需要在${scontext}.te添加 allow scontext tcontext:tclass denied
带入内容:修改hal_health_default.te
allow hal_health_default sysfs:fie { read }
allow hal_health_default sysfs:fie { open}
可以合并为:
allow hal_health_default sysfs:fie { read open }
优化sepolicy后bootchart获取的开机时长数据如下:
序号 | 开机时长(秒) |
---|---|
1 | 14.97 |
2 | 15.25 |
3 | 15.28 |
4 | 15.25 |
5 | 15.51 |
6 | 15.19 |
7 | 14.73 |
8 | 15.21 |
9 | 15.04 |
10 | 14.81 |
平均 | 15.12 |
根据频繁[kworke]打印,在源码中搜索可知该打印埋在kernel 代码中,可通过以下命令查看kernel log level:
$ cat /pro/sys/kernel/printk
6 6 1 7
通过修改源码 设置默认kernel log level
device/qcom/common/rootdir/etc/init.qcom.sh
case "$buildvariant" in
"userdebug" | "eng")
#set default loglevel to KERN_INFO
- echo "6 6 1 7" > /proc/sys/kernel/printk
+ echo "0 6 0 7" > /proc/sys/kernel/printk
;;
*)
#set default loglevel to KERN_WARNING
echo "4 4 1 4" > /proc/sys/kernel/printk
;;
esac
关闭kernel log后,bootchart获取的开机时长数据如下:
序号 | 开机时长(秒) |
---|---|
1 | 15.28 |
2 | 15.28 |
3 | 14.77 |
4 | 15.01 |
5 | 15.22 |
6 | 15.21 |
7 | 14.96 |
8 | 15.39 |
9 | 15.35 |
10 | 14.61 |
平均 | 15.11 |
对比默认15.21,关闭kernle log节省开机时长0.1
秒
kernel log level详细介绍参考文档:Android 10 设置kernel log level
PackageManagerService.java(PKMS)会在开机过程中通过其构造方法把设备安装的app进行扫描,检查apk的合法性,扫描apk的AndroidManifest.xml的一些属性,以及读取apk的asset等,并记录到mSettings结构体中。
在Android 8.0之前,是单线程扫描,8.0之后通过ParallelPackageParser类多线程扫描。
frameworks\base\services\core\java\com\android\server\pm\PackageManagerService.java
private void scanDirLI(File scanDir, int parseFlags, int scanFlags, long currentTime) {
//获取apk扫描路径列表
final File[] files = scanDir.listFiles();
if (ArrayUtils.isEmpty(files)) {
Log.d(TAG, "No files in app dir " + scanDir);
return;
}
//创建parallelPackageParser并行安装包解析类实例,进行多线程扫描apk
try (ParallelPackageParser parallelPackageParser = new ParallelPackageParser(
mSeparateProcesses, mOnlyCore, mMetrics, mCacheDir,
mParallelPackageParserCallback)) {
// Submit files for parsing in parallel
int fileCount = 0;
for (File file : files) {
final boolean isPackage = (isApkFile(file) || file.isDirectory())
&& !PackageInstallerService.isStageName(file.getName());
if (!isPackage) {
// Ignore entries which are not packages
continue;
}
//解析apk
parallelPackageParser.submit(file, parseFlags);
fileCount++;
}
// Process results one by one,处理apk解析结果
for (; fileCount > 0; fileCount--) {
ParallelPackageParser.ParseResult parseResult = parallelPackageParser.take();
Throwable throwable = parseResult.throwable;
int errorCode = PackageManager.INSTALL_SUCCEEDED;
if (throwable == null) {
// TODO(toddke): move lower in the scan chain
// Static shared libraries have synthetic package names
if (parseResult.pkg.applicationInfo.isStaticSharedLibrary()) {
renameStaticSharedLibraryPackage(parseResult.pkg);
}
try {
scanPackageChildLI(parseResult.pkg, parseFlags, scanFlags,
currentTime, null);
} catch (PackageManagerException e) {
errorCode = e.error;
Slog.w(TAG, "Failed to scan " + parseResult.scanFile + ": " + e.getMessage());
}
} else if (throwable instanceof PackageParser.PackageParserException) {
PackageParser.PackageParserException e = (PackageParser.PackageParserException)
throwable;
errorCode = e.error;
Slog.w(TAG, "Failed to parse " + parseResult.scanFile + ": " + e.getMessage());
} else {
throw new IllegalStateException("Unexpected exception occurred while parsing "
+ parseResult.scanFile, throwable);
}
// Delete invalid userdata apps
if ((scanFlags & SCAN_AS_SYSTEM) == 0 &&
errorCode != PackageManager.INSTALL_SUCCEEDED) {
logCriticalInfo(Log.WARN,
"Deleting invalid package at " + parseResult.scanFile);
removeCodePathLI(parseResult.scanFile);
}
}
}
}
frameworks\base\services\core\java\com\android\server\pm\ParallelPackageParser.java
class ParallelPackageParser implements AutoCloseable {
private static final int QUEUE_CAPACITY = 10;//多线程队列容量
private static final int MAX_THREADS = 4;//最大线程数
private final String[] mSeparateProcesses;
private final boolean mOnlyCore;
private final DisplayMetrics mMetrics;
private final File mCacheDir;
private final PackageParser.Callback mPackageParserCallback;
private volatile String mInterruptedInThread;
private final BlockingQueue mQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY);
private final ExecutorService mService = ConcurrentUtils.newFixedThreadPool(MAX_THREADS,
"package-parsing-thread", Process.THREAD_PRIORITY_FOREGROUND);
ParallelPackageParser(String[] separateProcesses, boolean onlyCoreApps,
DisplayMetrics metrics, File cacheDir, PackageParser.Callback callback) {
mSeparateProcesses = separateProcesses;
mOnlyCore = onlyCoreApps;
mMetrics = metrics;
mCacheDir = cacheDir;
mPackageParserCallback = callback;
}
为了进一步加快开机时apk扫描,
class ParallelPackageParser implements AutoCloseable {
private static final int QUEUE_CAPACITY = 10;
- private static final int MAX_THREADS = 4;
+ private static final int MAX_THREADS = 8;
private final String[] mSeparateProcesses;
优化后结果如下:
序号 | 开机时长(秒) |
---|---|
1 | 14.70 |
2 | 14.98 |
3 | 13.86 |
4 | 14.46 |
5 | 13.99 |
6 | 13.69 |
7 | 14.04 |
8 | 13.90 |
9 | 14.43 |
10 | 14.96 |
平均 | 14.30 |
如上所述Android8.0之前PKMS是单线程扫描apk,优化措施如下:
新建多线程处理类MultiTaskDealer.java
frameworks/base/services/core/java/com/android/server/pm/MultiTaskDealer.java
package com.android.server.pm;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
import android.util.Log;
public class MultiTaskDealer {
public static final String TAG = "MultiTaskDealer";
public static final String PACKAGEMANAGER_SCANER = "packagescan";
private static final boolean DEBUG_TASK = false;
private static HashMap> map = new HashMap>();
public static MultiTaskDealer getDealer(String name) {
WeakReference ref = map.get(name);
MultiTaskDealer dealer = ref!=null?ref.get():null;
return dealer;
}
public static MultiTaskDealer startDealer(String name,int taskCount) {
MultiTaskDealer dealer = getDealer(name);
if(dealer==null) {
dealer = new MultiTaskDealer(name,taskCount);
WeakReference ref = new WeakReference(dealer);
map.put(name,ref);
}
return dealer;
}
public void startLock() {
mLock.lock();
}
public void endLock() {
mLock.unlock();
}
private ThreadPoolExecutor mExecutor;
private int mTaskCount = 0;
private boolean mNeedNotifyEnd = false;
private Object mObjWaitAll = new Object();
private ReentrantLock mLock = new ReentrantLock();
public MultiTaskDealer(String name,int taskCount) {
final String taskName = name;
ThreadFactory factory = new ThreadFactory()
{
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(final Runnable r) {
if (DEBUG_TASK) Log.d(TAG, "create a new thread:" + taskName);
return new Thread(r, taskName + "-" + mCount.getAndIncrement());
}
};
mExecutor = new ThreadPoolExecutor(taskCount, taskCount, 5, TimeUnit.SECONDS,
new LinkedBlockingQueue(), factory){
protected void afterExecute(Runnable r, Throwable t) {
if(t!=null) {
t.printStackTrace();
}
MultiTaskDealer.this.TaskCompleteNotify(r);
if (DEBUG_TASK) Log.d(TAG, "end task");
super.afterExecute(r,t);
}
protected void beforeExecute(Thread t, Runnable r) {
if (DEBUG_TASK) Log.d(TAG, "start task");
super.beforeExecute(t,r);
}
};
}
public void addTask(Runnable task) {
synchronized (mObjWaitAll) {
mTaskCount+=1;
}
mExecutor.execute(task);
if (DEBUG_TASK) Log.d(TAG, "addTask");
}
private void TaskCompleteNotify(Runnable task) {
synchronized (mObjWaitAll) {
mTaskCount-=1;
if(mTaskCount<=0 && mNeedNotifyEnd) {
if (DEBUG_TASK) Log.d(TAG, "complete notify");
mObjWaitAll.notify();
}
}
}
public void waitAll() {
if (DEBUG_TASK) Log.d(TAG, "start wait all");
synchronized (mObjWaitAll) {
if(mTaskCount>0) {
mNeedNotifyEnd = true;
try {
mObjWaitAll.wait();
} catch (Exception e) {
}
mNeedNotifyEnd = false;
}
if (DEBUG_TASK) Log.d(TAG, "wait finish");
return;
}
}
frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java
+ import com.android.server.pm.MultiTaskDealer;
...
private void scanDirLI(File dir, final int parseFlags, int scanFlags, long currentTime) {
final File[] files = dir.listFiles();
if (ArrayUtils.isEmpty(files)) {
Log.d(TAG, "No files in app dir " + dir);
return;
}
//通过系统属性persist.pm.multitask灵活设置apk扫描多线程数
+ int iMultitaskNum = SystemProperties.getInt("persist.pm.multitask", 6);
+ final MultiTaskDealer dealer = (iMultitaskNum > 1) ? MultiTaskDealer.startDealer(
MultiTaskDealer.PACKAGEMANAGER_SCANER, iMultitaskNum) : null;
for (File file : files) {
final boolean isPackage = (isApkFile(file) || file.isDirectory())
&& !PackageInstallerService.isStageName(file.getName());
if (!isPackage) {
// Ignore entries which are not packages
continue;
}
try {
scanPackageTracedLI(file, parseFlags | PackageParser.PARSE_MUST_BE_APK,
scanFlags, currentTime, null);
} catch (PackageManagerException e) {
Slog.w(TAG, "Failed to parse " + file + ": " + e.getMessage());
// Delete invalid userdata apps
if ((parseFlags & PackageParser.PARSE_IS_SYSTEM) == 0 &&
e.error == PackageManager.INSTALL_FAILED_INVALID_APK) {
logCriticalInfo(Log.WARN, "Deleting invalid package at " + file);
removeCodePathLI(file);
}
}
if (dealer != null)
dealer.addTask(scanTask);
else
scanTask.run();
}
if (dealer != null)
dealer.waitAll();
Log.d(TAG, "end scanDirLI:"+dir);
}
除了上述优化措施,还可以在通过拿掉或缩短开机动画时间来缩短开机时长,根据设备特点,关闭不必要的系统启动服务。
总结本文内容如下:
1、系统开机主要流程:
2、bootchart开机图的生成步骤,如何简要分析
3、开机时长优化措施及数据效果
导入sepolicy优化,PKMS多线程扫描apk优化、关闭kernel log lever优化后的开机时长数据如下:
序号 | 开机时长(秒) |
---|---|
1 | 15.14 |
2 | 15.14 |
3 | 13.85 |
4 | 13.66 |
5 | 15.08 |
6 | 13.66 |
7 | 15.09 |
8 | 14.06 |
9 | 14.01 |
10 | 14.81 |
平均 | 14.45 |