Android 手机的分辨率越来越大,从矮穷锉到百富美都是如此.现在矮穷锉也是HD的屏, 而百富美一般是2K屏, 8核CPU, 3GRAM.然并卵,我们依然会发现大多数的开机动画并不是很流畅,而比较流畅的,大都要么画面简单有的只是showlogo, 要么降低动画的分辨率/FPS.比如本来是百富美的HW(2K屏),却用矮穷锉的开机动画(HD/10fps开机动画).当然,这里有androiddesign 的问题,也有手机厂商的设置animation内容的问题.本文将提供如何在矮穷锉HW上实现真正百富美HW才有的功能(FHD,或者2K 30FPS).
在android 中,开机动画并不仅仅是开机动画,而还包含开机铃声.开机动画都压缩在bootanimation.zip中,而开机铃声, 在L以及以前的版本中, 可以指定某一目录下的audio 文件, 也可以把开机铃声放到bootanimation.zip中, 而M 就只能放到bootanimation.zip中除非修改android的code.本文只讨论动画.事实上所谓的开机动画, 其实就是几十张或上百张的PNG或者JPG 图片按照一定的顺序, FPS播放出来. 所有的开机动画都压缩在bootanimation.zip中,其位置是/system/media/bootanimation.zip. 现在我们打开bootanimation.zip来看看究竟有些什么内容.
+bootanimation.zip
| | +part0
| |480_854_00000.png
| |480_854_00001.png
| |480_854_00002.png
| |480_854_00003.png
| |480_854_00004.png
| |480_854_00005.png
|+------------------
| +part1
| | 480_854_00000.png
| |480_854_00001.png
| |480_854_00002.png
| |480_854_00003.png
| |480_854_00004.png
| |480_854_00005.png
……
|+------------------
| +partn
| | 480_854_00000.png
| |480_854_00001.png
| |480_854_00002.png
| |480_854_00003.png
| |480_854_00004.png
| |480_854_00005.png
|+------------------
| | audio.wav
|+------------------
| desc.txt
+--------------------
|+------------------
| audio_conf.txt
+--------------------
这里我们可以看到在bootanimation.zip中由part1,part2,part3 … partn, audio.wav
, desc.txt和audio_conf.txt 构成.其中audio.wav和audio_conf.txt是和开机动画的audio相关, 我们在本文略去不表.而part和desc.txt与开机动画有关.每一个part 又由一系列的png或jpg格式的图片按照数字或字母的大小顺序排列而成. desc.txt则是描述怎样处理这些图片.
#typicaldesc.txt
480854 10
p1 0 part0
p0 0 part1
这里是典型的矮穷锉的配置.但是百富美的架构也和这个相同, 只是内容不同而已.
“480” 表示这些图片的宽度是480.
“854” 表示这些图片的高度是854.
“10” 表示开机动画的目标fps 是10.
“p” 这个flag表示某一个part的图片的处理方式,bootanimation 程序在处理某part的图片时会参考这个标志.P表示bootanimation可以随时退出当处理完一张图片后,如果bootanimation可以退出. ”C” 则表示必须把这部分的图片处理完毕才可以退出.事实上这个标识只要不为C, 则其行为完全一致.
“1”这个flag表示这part的图片需要循环处理多少次. 1表示处理循环处理一次.”0” 表示无限循环.
“0” 这个flag 表示播放完这部分所有图片后需要pause多久时间.”0” 表示不需要pause. “part0”表示动画的第一部分内容.
“part1” 表示动画的第二部分内容.
“partn” 表示动画的第n+1部分内容.
这里我们讨论bootanimation程序的退出时机,正如前面讨论的,bootanimation程序会根据part 描述的第一个标识来处理怎样退出bootanimation.我们先假定此时bootanimation满足退出条件(propservice.bootanim.exit 为1) bootanimation 在三种条件下可以退出.
开始处理part的新循环或新part. 如果这部分part的处理标识不是”c”则可以退出.否则不能退出.
开始处理新的一帧, 如果这部分part的处理标识不是”c”则可以退出.否则不能退出.
处理完一次循环后, 如果这部分part的处理标识是”c”,并且这部分是无限循环,则可以退出.如果不是无限循环则不能退出, 需要完全处理完这部分的图片才可能退出.
总之如果我们设置part的处理标识设置为”c”,则需要好好考虑和组织我们animation的图片.一步小心可能会把百富美的HW的性能搞成矮穷锉HW的性能.(开机时间过长).不建议设置此标识位为”C”.
frameworks\base\cmds\bootanimation\bootAnimation.cpp
bool BootAnimation::movie()
{
….
elseif (sscanf(l, " %c %d %d %s #%6s", &pathType, &count,&pause, path, color) >= 4)
{ Animation::Part part;
part.playUntilComplete = pathType == 'c';
}
….
for(int r=0 ; !part.count || r
// Exitany non playuntil complete parts immediately
if(exitPending() &&!part.playUntilComplete) //check exit condition 1
break;
….
for(size_t j=0 ; j
{
……
}
// For infinite parts, we've now played them at least once,so perhaps exit
if(exitPending()&& !part.count) //check exit condition 3
break;
}
我们知道boot animation在开机中占有重要地位,它也会影响开机时间.下面我们来讨论开机动画的工作流程.
在开机动画的生命周期中,至少有5个不同的进程参与其中.他们分别是:init ,surfaceflinger, system server,bootanimation 和Lancher.
Kernel将会启动init 进程当kernel启动完成后.
Init进程将会启动那些定义在init.rc 中的进程.如zygote,surfaceflinger等.
Sufacefinger进程会启动bootanimation 进程当surfaceflinger 进程启动时.(SurfaceFlinger::startBootAnim()).
Bootanimation进程处理开机动画.同时zygote 进程启动systemserver 进程. AMS ,WMS 将会在systemserver 进程里启动,进而启动launcher 进程.
Launcher进程的UI线程将会call AMS的activityIdle() 当Launcher 进程完成了启动,并且没有其它event 需要处理.注意这是Android framwork的功能.每一个app启动完成后,在其主线程有空闲的时候(没有需要处理的event)都会call AMS的activityIdle().这个idle handler 是在handleResumeActivity中注册的.所以当app 启动完成后都会call AMS的activityIdle(). 在AMS的activityIdle()中会调用WindowManagerService. performEnableScreen() call.
frameworks\base\services\core\java\com\android\server\am\ActivityThread.java
final voidhandleResumeActivity(IBinder token,
boolean clearHide, booleanisForward, boolean reallyResume)
{
….
f(!r.onlyLocalRequest) {
r.nextIdle = mNewActivities;
mNewActivities = r;
if (localLOGV) Slog.v(
TAG, "Scheduling idlehandler for " + r);
Looper.myQueue().addIdleHandler(new Idler());
}
}
Idler 定义.
private classIdler implements MessageQueue.IdleHandler {
@Override
public final boolean queueIdle() {
ActivityClientRecord a =mNewActivities;
boolean stopProfiling = false;
if (mBoundApplication != null&& mProfiler.profileFd != null
&&mProfiler.autoStopProfiler) {
stopProfiling = true;
}
if (a != null) {
mNewActivities = null;
IActivityManager am =ActivityManagerNative.getDefault();
ActivityClientRecord prev;
do {
if (localLOGV) Slog.v(
TAG, "Reportingidle of " + a +
" finished=" +
(a.activity != null&& a.activity.mFinished));
if (a.activity != null&& !a.activity.mFinished) {
try {
am.activityIdle(a.token,a.createdConfig, stopProfiling); //这里会callAMS. activityIdle by binder
a.createdConfig =null;
} catch(RemoteException ex) {
// Ignore
}
}
prev = a;
a = a.nextIdle;
prev.nextIdle = null;
} while (a != null);
}
if (stopProfiling) {
mProfiler.stopProfiling();
}
ensureJitEnabled();
return false;
}
}
WindowManagerService.performEnableScreen()将通过发送IBinder.FIRST_CALL_TRANSACTION.从而callsurfaceFlinger. bootFinished().如果比较顺利的话.注意这里也是很容易由白富美变为矮穷锉如果Keyguard和Wallpaper处理得不好的话.开机动画和开机时间会边长.
frameworks\base\services\core\java\com\android\server\wm\WindowManagerService.java
public voidperformEnableScreen() {
synchronized(mWindowMap) {
……
// Don't enable the screen untilall existing windows have been drawn.
//这里会check Keyguard和Wallpaper.
if (!mForceDisplayEnabled &&checkWaitingForWindowsLocked()) {
return;
}
if (!mBootAnimationStopped) {
// Do this one time.
try {
IBinder surfaceFlinger =ServiceManager.getService("SurfaceFlinger");
if (surfaceFlinger != null){
//Slog.i(TAG,"******* TELLING SURFACE FLINGER WE ARE BOOTED!");
Parcel data = Parcel.obtain();
data.writeInterfaceToken("android.ui.ISurfaceComposer");
surfaceFlinger.transact(IBinder.FIRST_CALL_TRANSACTION,// BOOT_FINISHED
data, null, 0);
data.recycle();
}
} catch (RemoteException ex) {
Slog.e(TAG, "Bootcompleted: SurfaceFlinger is dead!");
}
mBootAnimationStopped = true;
}
….
}
private booleancheckWaitingForWindowsLocked(){
…..
final WindowList windows =getDefaultWindowListLocked();
final int N = windows.size();
for (int i=0; i WindowState w = windows.get(i); if (w.isVisibleLw() &&!w.mObscured && !w.isDrawnLw()) { return true; } if (w.isDrawnLw()) { if (w.mAttrs.type ==TYPE_BOOT_PROGRESS) { haveBootMsg = true; } else if (w.mAttrs.type ==TYPE_APPLICATION) { haveApp = true; } else if (w.mAttrs.type ==TYPE_WALLPAPER) { haveWallpaper = true; } else if (w.mAttrs.type ==TYPE_STATUS_BAR) { haveKeyguard =mPolicy.isKeyguardDrawnLw(); } } } // If we are turning on the screen toshow the boot message, // don't do it until the boot messageis actually displayed. if (!mSystemBooted &&!haveBootMsg) { return true; } …. // If we are turning on the screenafter the boot is completed // normally, don't do so until we havethe application and // wallpaper. if (mSystemBooted && ((!haveApp&& !haveKeyguard) || (wallpaperEnabled &&!haveWallpaper))) { return true; } return false; }
如前所述Bootanimation 会在关键节点去check 是否需要exit. 当且仅当属性“service.bootanim.exit“ 为1 时, bootanimation 会退出.
前面我们分析了开机动画的组织架构以及工作流程, 现在就到了最重要的时刻了.我们来看看矮穷锉如何逆袭百富美.那么哪些是矮穷锉的行为, 典型的行为有如下两点:
Kernel logo 的时间太长 (10s以上), 而开机动画的时间太短.
如果出现这种现象,给人的第一印象便是这台手机的CPU太TM 矮穷锉了. 我们来看看问题到底在哪.我们知道android 原来的开机动画design 为只有开机动画而没有开机铃声或者开机铃声不在bootanimation里处理. 所以其bootanimation 进程的user 定义为Graphic ,随着消费者要求的提高就出现了需要在bootanimation进程中处理audio,这样就需要修改bootanimation进程的用户为media. 因为auido service 只能media 或root 用户能访问.
那么问题到底出现在哪里?我们来看看bootanimation 进程的定义. 大多数手机的定义如下:
service bootanim/system/bin/bootanimation
class core
user media
group graphics audio
disabled
oneshot
其user 是media.我们接着看其Bootanimation构造函数的定义.
Bootanimation会new SurfaceComposerClient().
frameworks\base\cmds\bootanimation\bootAnimation.cpp
BootAnimation::BootAnimation(): Thread(false), mZip(NULL)
{
mSession = new SurfaceComposerClient();
}
b.SurfaceComposerClient的定义如下:
frameworks\native\libs\gui\SurfaceComposerClient.cpp
SurfaceComposerClient::SurfaceComposerClient()
: mStatus(NO_INIT),mComposer(Composer::getInstance())
{
}
c.在onFirstRef()会去连接surfacefinger.
voidSurfaceComposerClient::onFirstRef() {
sp
if (sm != 0) {
sp
if (conn != 0) {
mClient = conn;
mStatus = NO_ERROR;
}
}
}
d.连接surfaceflinger时,会check permission.现在**部分来了
frameworks\native\services\surfaceflinger\ SurfaceFlinger.cpp
status_tSurfaceFlinger::onTransact(
uint32_t code, constParcel& data, Parcel* reply, uint32_t flags)
{
switch (code) {
case CREATE_CONNECTION:
case CREATE_DISPLAY:
case SET_TRANSACTION_STATE:
case BOOT_FINISHED:
case CLEAR_ANIMATION_FRAME_STATS:
case GET_ANIMATION_FRAME_STATS:
case SET_POWER_MODE:
{
// codes that require permissioncheck
IPCThreadState* ipc =IPCThreadState::self();
const int pid =ipc->getCallingPid();
const int uid = ipc->getCallingUid();
if ((uid != AID_GRAPHICS && uid != AID_SYSTEM) &&
!PermissionCache::checkPermission(sAccessSurfaceFlinger, pid, uid)){
ALOGE("Permission Denial:"
"can't accessSurfaceFlinger pid=%d, uid=%d", pid, uid);
return PERMISSION_DENIED;
}
break;
…..
}
}
e.我们从上面的代码中看到在surfaceflinger的很多操作例如连接,创建display和设置power 等都需要检查permission。除非进程的user 是graphics 和system。然而几乎在android的所有版本中,media 用户都是有ACCESS_SURFACE_FLINGER 的permission的。其定义在平台的permission文件中。
frameworks\base\data\etc\platform.xml
另一方面,permission的check是在javaservice permission service。当bootanimation开始运行的时候permission 还没有看是运行。Permisison service运行起来还需一段时间(8-9s)。因此开机动画在这里被阻塞了。其结果就是百富美变成了矮穷锉。如果我们要从矮穷锉完美的逆袭为百富美,只需做如下改动即可。我们为什么我们不把bootanimation 改为root 用户,我想你懂的。
- if ((uid !=AID_GRAPHICS && uid != AID_SYSTEM) &&
!PermissionCache::checkPermission(sAccessSurfaceFlinger, pid, uid))
+ if ((uid != AID_GRAPHICS && uid != AID_SYSTEM&&uid != AID_MEDIA) &&
!PermissionCache::checkPermission(sAccessSurfaceFlinger,pid, uid))
开机动画fps 太低,不流程。
我们知道很多开机动画要么是手机厂商logo或者手机的宣传片,要么是运行商的宣传片。可以说是手机的脸面,在现如今的刷脸时代,没有一个好的面孔是很难找到高大帅的。那么是什么原因导致矮穷锉满天下。我们还是看看code吧。
frameworks\base\cmds\bootanimation\bootAnimation.cpp
boolBootAnimation::movie()
{
….
for (size_t i=0 ; i … for (int r=0 ; !part.count ||r ….. for (size_t j=0 ; j …. initTexture(frame); } return false; } status_t BootAnimation::initTexture(Texture* texture, AssetManager&assets, const char* name) { SkBitmap bitmap; SkImageDecoder::DecodeMemory(asset->getBuffer(false),asset->getLength(), &bitmap, kUnknown_SkColorType,SkImageDecoder::kDecodePixels_Mode); asset->close(); delete asset; …. glTexParameteriv(GL_TEXTURE_2D,GL_TEXTURE_CROP_RECT_OES, crop); glTexParameterx(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER, GL_NEAREST); glTexParameterx(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameterx(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameterx(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T, GL_REPEAT); return NO_ERROR; } 看出点什么名堂出来没。好像没有什么问题吧?是的,功能上是没有什么问题。那么从性能上看是否有问题的?对的,这里用的是SW decoder SkImageDecoder::DecodeMemory(),而且是单线程那么其解码效率如何?如果你巧合用了百富美平台,又巧合的对SW decoder进行了优化如neon指令做了优化,那么恭喜你。你已经没有此问题的烦恼。对此问题如何解决呢?聪明的你一定想到两种方案。一种是比较low的方案,继续往矮穷锉的方向发展,那就是降低开机动画的分辨率来提高解码速度从而提高开机动画的fps。另一种就是向百富美看其,用多线程来进行解码,把解码好的数据放到一个buffer queue中,需要用到解码的frame时直接从bufferqueue里拿而不是解一个frame,画一个frame。充分利用多核的计算能力。