前述:作为手机的入口级应用,Launcher承担着展示手机重要数据资源的任务,这也决定了Launcher是一个数据驱动型的app。本文基于Android4.4 kitkat Launcher源码,从数据的搜集、存储、加载、绑定等方面来介绍Launcher的启动流程。我们先抽象出这一过程的模型:
启动阶段有两个重要的数据来源:Launcher本身database 和 PackageManager。前者提供桌面上存储的快捷方式、widgets、文件夹等数据;后者主要提供主菜单中所有已安装App和Widget数据。Launcher启动阶段的任务简而言之就是:把上图中左边的数据最终转变成右侧我们熟悉的桌面。说到这里,也许你脑子里应该已经浮现那个我们再熟悉不过的异步加载模型了吧。没错,那是Android的马克思主义普遍原理,但实际上,中国国情的客观现实是复杂的~
我把Launcher的启动流程整体分成三个阶段:Provider启动、Application启动和Activity启动。其中第三个阶段最为重要。下面我们就逐一介绍这三个阶段!
一、LauncherProvider启动
LauncherProvider类是一个基于数据库的ContentProvider,在它的启动阶段,完成了Launcher数据库的创建工作,当然,其他程序也可以通过它共享Launcher数据。这个阶段,我们需要关心两点:
(1)只有在Launcher首次启动或数据被清空时,这个阶段才会执行,数据库的创建包括两个重要的表:favorites和workspaceScreens;前者存放桌面图标数据,后者存放屏幕(Screen)相关数据,当然,此时这两个table还都是空的。但是无论是哪个厂商的手机,我们拿到后首次开机,桌面都不可能是空的,这就是下面提到的图标定制。
(2)LauncherProvider提供了一个初始化favorites表数据的方法:loadDefaultFavoritesIfNecessary(int rId)。该方法主要是用来定制launcher首次启动时桌面上的图标,在第三个阶段初期被调用。做法就是,先把Launcher首次启动时要展现在桌面的app数据写入一个xml文件,当favorites表是刚创建的空表时,将xml中的数据写入其中。这个xml文件在4.4的默认名称是:default_workspace.xml,位于xml 文件夹。里面的数据格式如:
-
<!-- Middle screen [2] -->
-
<appwidget
-
launcher:packageName="com.android.deskclock"
-
launcher:className="com.android.alarmclock.AnalogAppWidgetProvider"
-
launcher:screen="2"
-
launcher:x="1"
-
launcher:y="0"
-
launcher:spanX="2"
-
launcher:spanY="2" />
-
<favorite
-
launcher:packageName="com.android.camera"
-
launcher:className="com.android.camera.Camera"
-
launcher:screen="2"
-
launcher:x="0"
-
launcher:y="3" />
声明了某个widget或快捷方式(shortcut)所在屏(screen),第几行(y),第几列(x),以及对应的Component信息。
综上,LauncherProvider主要是负责Launcher数据库的创建并提供数据初始化的方法,作为Launcher启动的第一阶段,只在首次启动Launcher或清空数据时会执行。
二、LauncherApplication启动
Launcher不使用默认的Application,而是用自定义的LauncherApplication类,这个阶段完成了Launcher进程启动,主要是创建一些全局性质的变量。在这里我们重点关心两个地方:
(1)LauncherModel类(Launcher中最重要类之一)的实例化,LauncherModel是Launcher本地数据层的管理者,在启动阶段由它负责收集所有数据,并最终CallBack控制层Launcher.java,完成View层的初始化工作;启动之后,用户在桌面上(View层)的任何操作,比如挪动一个图标、新建一个文件夹等等,都会由LauncherModel反馈到数据库更新数据!同时它还是一个BroadcastReceiver,监听手机应用安装包的变动、手机环境的变化(比如configuration、searchable变化)等事件,并反馈给View层更新。下面要讲的第三个阶段也全是由LauncherModel完成的。如果说Launcher是数据驱动的,那LauncherModel就那台驱动机。
(2)提供布局管理者实例化方法,launcher3通过DynamicGrid类统一管理布局相关信息,为的是自动适配目前Android繁多的屏幕尺寸。它的真正实例化是在第三个阶段之前,目的是为后续的数据写入和View的组织方式根据屏幕尺寸做限制。比如它定义了桌面图标是几行几列的。
综上,这个阶段可以看作是为下一步正式数据加载做一些准备工作。
三、Launcher Activity启动
Launcher启动真正复杂繁琐的工作是在这个阶段完成的。Launcher虽然有135个类,View形式繁多,但是它仅有一个Activity:Launcher.java,也是Launcher的最高控制层,负责model层和view层的重要交互工作。下面我们分6个步骤来详细介绍:
1.LauncherModel启动加载任务
在Launcher的主Activity Launcher.java类onCreate()的时候LauncherModel.startLoader(true,mWorkspace.getCurrentPage())触发整个launcher的后台数据加载工作。Launcher的数据加载和绑定共有两个线程完成:主线程和sWorkerThread子线程。sWorkerThread负责数据加载工作,而主线程负责view相关的绑定工作。
2.加载和绑定桌面(workspace)
(1)首先将sWorkerThread子线程的优先级设为NORMAL_PRIORITY,保证workspace数据加载工作能够及时进行,否则进入launcher后会长时间显示一个空白的桌面;
(2)sWorkerThread子线程加载workspace
首先调用LauncherProvider的loadFavoritesIfNecessary()方法初始化favorites表,在第一阶段时说过,如果此时favorties不是刚创建的空表,是不会进行重新初始化favorites表的,然后将favorites表中的数据加载到内存,放入对应的变量中保存,对应关系如下:
sBgWorkspaceItems : 桌面上所有application和shortcut(包括folder里的)
sBgAppWidgets : 桌面上所有widgets
sBgFolders : 桌面上所有folders
sBgItemsIdMap : 桌面上所有itemInfo(包括以上三项,且每个item对应一个id)
sBgWorkspaceScreens : 桌面上所有屏
(3)主线程绑定workspace
根据上面变量中的数据绑定对应的桌面view。LauncherModel是通过调用注册的CallBack去完成这些绑定工作的,这里Callback就是Launcher.java,它在onCreate是被注册,具体Callback的回调方法如下:
-
public interface Callbacks {
-
public boolean setLoadOnResume();
-
public int getCurrentWorkspaceScreen();
-
public void startBinding();
-
public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end,
-
boolean forceAnimateIcons);
-
public void bindScreens(ArrayList<Long> orderedScreenIds);
-
public void bindAddScreens(ArrayList<Long> orderedScreenIds);
-
public void bindFolders(HashMap<Long,FolderInfo> folders);
-
public void finishBindingItems(boolean upgradePath);
-
public void bindAppWidget(LauncherAppWidgetInfo info);
-
public void bindAllApplications(ArrayList<AppInfo> apps);
-
public void bindAppsAdded(ArrayList<Long> newScreens,
-
ArrayList<ItemInfo> addNotAnimated,
-
ArrayList<ItemInfo> addAnimated,
-
ArrayList<AppInfo> addedApps);
-
public void bindAppsUpdated(ArrayList<AppInfo> apps);
-
public void bindComponentsRemoved(ArrayList<String> packageNames,
-
ArrayList<AppInfo> appInfos,
-
boolean matchPackageNamesOnly);
-
public void bindPackagesUpdated(ArrayList<Object> widgetsAndShortcuts);
-
public void bindSearchablesChanged();
-
public boolean isAllAppsButtonRank(int rank);
-
public void onPageBoundSynchronously(int page);
-
public void dumpLogsToLocalData();
-
}
a.首先解绑workspace上当前items(针对重新加载);
b.分离当前屏的items和非当前屏items,优先绑定当前屏数据;
c.清空workspace当前子views;
d.根据sBgWorkspaceScreens绑定桌面上的screens;
e.根据sBgWorkspaceItems、sBgAppWidgets、sBgFolders分离出的当前屏items绑定当前屏的items;
f.根据sBgWorkspaceItems、sBgAppWidgets、sBgFolders分离出的非当前屏绑定非当前屏items;
g.绑定完毕,执行因绑定workspace被延后的工作,比如添加小部件或shortcut等
具体这些绑定工作是如何完成的,我们仅以bindItems方法为例来分析,其他绑定工作类似,不一一介绍,Just look the fucking code:
-
/**
-
* Bind the items start-end from the list.
-
*
-
* Implementation of the method from LauncherModel.Callbacks.
-
*/
-
public void bindItems(final ArrayList<ItemInfo> shortcuts, final int start, final int end,
-
final boolean forceAnimateIcons) {
-
//这里依据launcher当前的状态,判断是否需要延后到onResume时再执行
-
Runnable r = new Runnable() {
-
public void run() {
-
bindItems(shortcuts, start, end, forceAnimateIcons);
-
}
-
};
-
if (waitUntilResume(r)) {
-
return;
-
}
-
-
// Get the list of added shortcuts and intersect them with the set of shortcuts here
-
final AnimatorSet anim = LauncherAnimUtils.createAnimatorSet();
-
final Collection<Animator> bounceAnims = new ArrayList<Animator>();
-
final boolean animateIcons = forceAnimateIcons && canRunNewAppsAnimation();
-
Workspace workspace = mWorkspace;
-
long newShortcutsScreenId = -1;
-
for (int i = start; i < end; i++) {
-
final ItemInfo item = shortcuts.get(i);
-
//如果这个图标是位于Dockbar上的,但是当前设备不支持Dockbar,跳过
-
// Short circuit if we are loading dock items for a configuration which has no dock
-
if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT &&
-
mHotseat == null) {
-
continue;
-
}
-
-
switch (item.itemType) {
-
//如果是应用或者是快捷方式
-
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
-
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
-
ShortcutInfo info = (ShortcutInfo) item;
-
//这里根据数据库传来的item信息,创建对应的View
-
View shortcut = createShortcut(info);
-
-
/*
-
* TODO: FIX collision case
-
*/
-
if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
-
CellLayout cl = mWorkspace.getScreenWithId(item.screenId);
-
if (cl != null && cl.isOccupied(item.cellX, item.cellY)) {
-
throw new RuntimeException("OCCUPIED");
-
}
-
}
-
// 将这个View放到桌面上的指定位置
-
workspace.addInScreenFromBind(shortcut, item.container, item.screenId, item.cellX,
-
item.cellY, 1, 1);
-
if (animateIcons) {
-
// Animate all the applications up now
-
shortcut.setAlpha(0f);
-
shortcut.setScaleX(0f);
-
shortcut.setScaleY(0f);
-
bounceAnims.add(createNewAppBounceAnimation(shortcut, i));
-
newShortcutsScreenId = item.screenId;
-
}
-
break;
-
// 如果是文件夹
-
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
-
//根据传来的item信息,创建一个文件夹并放到桌面指定的位置
-
FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this,
-
(ViewGroup) workspace.getChildAt(workspace.getCurrentPage()),
-
(FolderInfo) item, mIconCache);
-
workspace.addInScreenFromBind(newFolder, item.container, item.screenId, item.cellX,
-
item.cellY, 1, 1);
-
break;
-
default:
-
throw new RuntimeException("Invalid Item Type");
-
}
-
}
-
//下面是为这个绑定工作润色,也就是加上动画,就是你在launcher看到的,有新的图标出现时,有一个由小变大的动画过程
-
if (animateIcons) {
-
// Animate to the correct page
-
if (newShortcutsScreenId > -1) {
-
long currentScreenId = mWorkspace.getScreenIdForPageIndex(mWorkspace.getNextPage());
-
final int newScreenIndex = mWorkspace.getPageIndexForScreenId(newShortcutsScreenId);
-
final Runnable startBounceAnimRunnable = new Runnable() {
-
public void run() {
-
anim.playTogether(bounceAnims);
-
anim.start();
-
}
-
};
-
if (newShortcutsScreenId != currentScreenId) {
-
// We post the animation slightly delayed to prevent slowdowns
-
// when we are loading right after we return to launcher.
-
mWorkspace.postDelayed(new Runnable() {
-
public void run() {
-
mWorkspace.snapToPage(newScreenIndex);
-
mWorkspace.postDelayed(startBounceAnimRunnable,
-
NEW_APPS_ANIMATION_DELAY);
-
}
-
}, NEW_APPS_PAGE_MOVE_DELAY);
-
} else {
-
mWorkspace.postDelayed(startBounceAnimRunnable, NEW_APPS_ANIMATION_DELAY);
-
}
-
}
-
}
-
//刷新
-
workspace.requestLayout();
-
}
通过上面代码里的中文注释,基本知道绑定工作无非就是将从数据库读来的数据生成对应的view,然后把这些view放到桌面(workspace)指定的位置。至于各种view的创建过程,因种类繁多,不一一分析了。
3.此时大量的绑定工作都已post到主线程去执行了,为了保证绑定工作顺利执行,降低sWorkerThread子线程优先级为THREAD_PRIORITY_BACKGROUND,并且等待主线程空闲下来,然后再启动主菜单的加载和绑定工作。
4.加载和绑定主菜单
(1)子线程加载主菜单数据,通过packageManager query已安装的所有应用到内存mBgAllAppsList.data
(2)主线程根据mBgAllAppsList.added绑定主菜单数据
这个过程和上面加载和绑定桌面原理是类似的,区别就是数据来源不同,绑定的位置不同,不详细介绍了。
5.主菜单的数据加载工作完毕后,将sWorkerThread的优先级由后台提高到默认THREAD_PRIORITY_DEFAULT,保证后续工作的进行。
6.如果采用无主菜单设计,将主菜单中有但是桌面没有的app添加到桌面上
(1)子线程执行:对比mBgAllAppsList.data和sBgItemsIdMap找出桌面上不存在的apps
(2)子线程执行:先添加到db的favorites表
(3)主线程:再绑定到workspace,绑定工作类似
至此,launcher启动完毕!Launcher的启动流程基本可以带出Launcher的整体架构,下面给出一张Launcher主架构图:
从MVC角度分析:LauncherModel为Model层,Workspace等自定义View为View层,Launcher则扮演了Controller层。
阅读(1899) | 评论(0) | 转发(0) |