Launcher源码分析之一

        桌面上显示的各应用、快捷方式及widget图标,其所在屏幕、位置、所占大小等信息都存储在数据库中。Launcher启动时,首先会将这些数据加载到内存,之后再显示到桌面相应的位置上。整个流程完整不可分割,但为了条理清晰及出于个人习惯,在本文讲述时,我还是将其分为了三个层次(如图1所示),需注意的两点是:
(1)这个层次的结构是根据Launcher数据的存储、加载到显示来划分的

(2)这三个层次包含在整个应用――LauncherApplication之中

1. 应用--LauncherApplication

<application
        android:name="com.android.launcher2.LauncherApplication"
        android:label="@string/application_name"
        android:icon="@drawable/ic_launcher_home"
        android:hardwareAccelerated="@bool/config_hardwareAccelerated"
        android:largeHeap="@bool/config_largeHeap">
       
        ………………………

</application>

代码段1 AndroidManifest.xml

通过Manifest.xml可知,Launcher的应用环境的实现类为LauncherApplication.java,launcher启动时首先会实例化此类,执行其onCreate方法:

    public void onCreate() {
        super.onCreate();

        // set sIsScreenXLarge and sScreenDensity *before* creating icon cache
        final int screenSize = getResources().getConfiguration().screenLayout &
                Configuration.SCREENLAYOUT_SIZE_MASK;
        sIsScreenLarge = screenSize == Configuration.SCREENLAYOUT_SIZE_LARGE ||
            screenSize == Configuration.SCREENLAYOUT_SIZE_XLARGE;
        sScreenDensity = getResources().getDisplayMetrics().density;

        mIconCache = new IconCache(this);
        mModel = new LauncherModel(this, mIconCache);

        // Register intent receivers
        IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
        filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
        filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
        filter.addDataScheme("package");
        registerReceiver(mModel, filter);
        filter = new IntentFilter();
        filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE);
        filter.addAction(Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE);
        filter.addAction(Intent.ACTION_LOCALE_CHANGED);
        filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
        registerReceiver(mModel, filter);
        filter = new IntentFilter();
        filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED);
        registerReceiver(mModel, filter);
        filter = new IntentFilter();
        filter.addAction(SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED);
        registerReceiver(mModel, filter);

        // Register for changes to the favorites
        ContentResolver resolver = getContentResolver();
        resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI, true,
                mFavoritesObserver);
        new Thread() {
			      @Override
			      public void run() {
				      Log.d("LauncherApplication", "start initCacheAllApps");
				      mModel.initCacheAllApps(LauncherApplication.this);
			      }

		    }.start();
    }

代码段2 LauncherApplication.java的onCreate方法

从代码中可以看出,LauncherApplication完成了三件事:

  • 实例化LauncherModel对象,并为其注册广播接收器
  • 注册数据库监听器,监听数据库表favorite的变化
  • 启动一条新线程执行LauncherModel.initCacheAllApps方法

每一个Launcher都拥有唯一一个LauncherModel,从图1中所示的结构可以看到其处于数据库层与Launcher实体显示层,加载数据并在上层显示,起到了承上启下的作用。LauncherApplication中注册了数据库监听器监听favorite表的变化,而此表存储了各应用及widget在launcher中的显示信息(稍后详述),此表发生变化(说明桌面上的图标发生或即将发生变化)后将引起launcher的重新加载(如代码3所示,LauncherModel.startLoader方法将会加载Launcher,后文会提及)。

    /**
     * Receives notifications whenever the user favorites have changed.
     */
    private final ContentObserver mFavoritesObserver = new ContentObserver(new Handler()) {
        @Override
        public void onChange(boolean selfChange) {
            mModel.startLoader(LauncherApplication.this, false);
        }
    }

代码段3 内容监听

2.数据库--launcher.db

 图2 launcher.db中的favorites表

Launcher中关于应用、快捷方式的显示信息存在于数据库Launcher.db中的favorite表(如图2所示),其中每一条记录代表一个显示项,部分字段含义如下。

  • container:此应用或快捷方式的存放目录(hotseat、desktop或者文件夹)
  • screen:图标所在的屏幕,Launcher中的Workspace部分由若干个屏幕(CellLayout)组成,字段screen即标明了是哪个CellLayout;
  • cellX、cellY:图标显示在CellLayout中的位置。每一个CellLayout由4*4个Cell组成,而cellX、cellY即标明了哪一个cell;
  • spanX、spanY:这两个字段指明了图标所占据屏幕的大小,spanX标明了图标横向位置占据cell的数目,而spanY则是纵向cell数目;


显然,对于每一个应用,根据数据库中存储的信息便可以准确无误的将其显示正确的位置上。此层涉及到的文件为LauncherProvider.java和LauncherSetting.java,其中前者为launcher所对应的ContentProvider,其定义了对于favorites表的创建、初始化以及增删改查等操作。在其onCreate函数中存在如下语句:  

    if (!convertDatabase(db)) {
        // Populate favorites table with initial favorites
        loadFavorites(db, R.xml.default_workspace);
    }

其中方法loadFavorites(db, R.xml.default_workspace)的作用是初始化favorites表,即当其第一次创建尚未存入数据时,会根据default_workspace.xml配置的信息对favorite进行初始化,这也是我们定制Launcher的开始,default_workspace.xml内容代码4所示:

<favorites xmlns:launcher="http://schemas.android.com/apk/res/com.android.launcher">
    <!-- Far-left screen [0] -->

    <!-- Left screen [1] -->
    <appwidget
        launcher:packageName="com.android.settings"
        launcher:className="com.android.settings.widget.SettingsAppWidgetProvider"
        launcher:screen="1"
        launcher:x="0"
        launcher:y="3"
        launcher:spanX="4"
        launcher:spanY="1" />

    <!-- 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" />

    <!-- Right screen [3] -->
    <favorite
        launcher:packageName="com.android.gallery3d"
        launcher:className="com.android.gallery3d.app.Gallery"
        launcher:screen="3"
        launcher:x="1"
        launcher:y="3" />
    <favorite
        launcher:packageName="com.android.settings"
        launcher:className="com.android.settings.Settings"
        launcher:screen="3"
        launcher:x="2"
        launcher:y="3" />

    <!-- Far-right screen [4] -->

    <!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
    <favorite
        launcher:packageName="com.android.contacts"
        launcher:className="com.android.contacts.activities.DialtactsActivity"
        launcher:container="-101"
        launcher:screen="0"
        launcher:x="0"
        launcher:y="0" />
    <favorite
        launcher:packageName="com.android.contacts"
        launcher:className="com.android.contacts.activities.PeopleActivity"
        launcher:container="-101"
        launcher:screen="1"
        launcher:x="1"
        launcher:y="0" />
    <favorite
        launcher:packageName="com.android.mms"
        launcher:className="com.android.mms.ui.ConversationList"
        launcher:container="-101"
        launcher:screen="3"
        launcher:x="3"
        launcher:y="0" />
    <favorite
        launcher:packageName="com.android.browser"
        launcher:className="com.android.browser.BrowserActivity"
        launcher:container="-101"
        launcher:screen="4"
        launcher:x="4"
        launcher:y="0" />
</favorites>

代码4 default_workspace.xml

 

3.模型层--LauncherModel.java

所谓的模型层命名并不确切(只是我对其的一种称呼而已),模型层中涉及到的核心文件只有一个:LauncherModel.java。它是连接数据库层与上层显示的中间环节,主要完成以下几个工作:

  • Launcher启动时,加载数据库数据并显示
  • Launcher运行过程中,完成维护更新(比如安装应用、删除应用时,更新数据库及界面显示)
  • 向上层提供操作数据库的接口,如添加、删除、更新应用或快捷方式等
3.1 启动过程

在Launcher启动时,LauncherModel中相关的代码执行过程如下。

步骤1:Launcher的onCreate方法中调用LauncherModel的startLoader方法;
步骤2:startLoader方法中实例化一个LoaderTask线程并运行;
步骤3:LoaderTask调用loadAndBindWorkspace方法完成桌面各项的加载与显示;
步骤4:LoaderTask调用loadAndBindAllApps方法完成所有应用的加载与显示。
注:步骤3和4顺序可能互换。

显然在此过程中,loadAndBindWorkspace和loadAndBindAllApps完成了主要的功能,首先来分析下loadAndBindWorkspace所完成的工作。其代码如下:

private void loadAndBindWorkspace() {
    // Load the workspace
    if (DEBUG_LOADERS) {
        Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded);
    }

    if (!mWorkspaceLoaded) {
        loadWorkspace();
        synchronized (LoaderTask.this) {
            if (mStopped) {
                return;
            }
            mWorkspaceLoaded = true;
        }
    }

    // Bind the workspace
    bindWorkspace();
}

代码段5 loadAndBindWorkspace方法

从代码中可以看出,loadAndBindWorkspace通过调用loadWorkspace和bindWorkspace来分别完成数据的加载与界面的显示。loadWorkspace的代码相对简单,通过读取数据库表favorite初始化成员属性:sItemsIdMap,sWorkspaceItems等,这几个成员属性为List或Map,类型用于存储数据库中加载的数据,这几个成员变量的声明如下:

    // < only access in worker thread >
    private AllAppsList mAllAppsList;

    // sItemsIdMap maps *all* the ItemInfos (shortcuts, folders, and widgets) created by
    // LauncherModel to their ids
    static final HashMap<Long, ItemInfo> sItemsIdMap = new HashMap<Long, ItemInfo>();

    // sItems is passed to bindItems, which expects a list of all folders and shortcuts created by
    //       LauncherModel that are directly on the home screen (however, no widgets or shortcuts
    //       within folders).
    static final ArrayList<ItemInfo> sWorkspaceItems = new ArrayList<ItemInfo>();

    // sAppWidgets is all LauncherAppWidgetInfo created by LauncherModel. Passed to bindAppWidget()
    static final ArrayList<LauncherAppWidgetInfo> sAppWidgets =
        new ArrayList<LauncherAppWidgetInfo>();

    // sFolders is all FolderInfos created by LauncherModel. Passed to bindFolders()
    static final HashMap<Long, FolderInfo> sFolders = new HashMap<Long, FolderInfo>();

    // sDbIconCache is the set of ItemInfos that need to have their icons updated in the database
    static final HashMap<Object, byte[]> sDbIconCache = new HashMap<Object, byte[]>();

代码段6 LauncherModel中的成员属性


mAllAppsList:此成员属性是在loadAndBindAllApps中初始化的,为类AllAppsList实例。此类封装系统中所有应用程序的信息及相关操作List以及相关的操作(稍后再详述),供上层调用。Launcher中即涉及到对于数据库的操作,又涉及到UI操作,因此使用两个线程来工作,其中主线程中用于UI操作,另一线程(worker thread)用于进行数据库操作。注释<only access in worker thread>表明:mAllAppsList中的数据并不是用于进行UI操作的,只能在worker线程中访问。进行数据库操作的线程Worker thread定义如下
                private static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader");
                static {
                        sWorkerThread.start();
                }
                private static final Handler sWorker = new Handler(sWorkerThread.getLooper());
sItemsIdMap:Launcher中显示的每一项(包括应用、快捷方式以及widget)都是ItemInfo的一个实例,ItemInfo的子类ShortcutInfo、ApplicationInfo、FolderInfo以及LauncherAppWidgetInfo分别代表了快捷方式、应用、文件夹以及widget。sItemsIdMap则存储了系统中所有ItemInfo(即包括所有的应用、快捷方式、文件夹及widget)到其id的映射。
sWorkspaceItems:此List中存储的ItemInfo将会被直接显示到home screen上。在LauncherModel中声明了一组回调函数并由Launcher来实现,sWorkspaceItems中的元素会被传递给其中的bindItems完成显示。
sAppWidgets、sFolders、sDbIconCache不再详述。

public interface Callbacks {
        public boolean setLoadOnResume();
        public int getCurrentWorkspaceScreen();
        public void startBinding();
        public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end);
        public void bindFolders(HashMap<Long,FolderInfo> folders);
        public void finishBindingItems();
        public void bindAppWidget(LauncherAppWidgetInfo info);
        public void bindAllApplications(ArrayList<ApplicationInfo> apps);
        public void bindAppsAdded(ArrayList<ApplicationInfo> apps);
        public void bindAppsUpdated(ArrayList<ApplicationInfo> apps);
        public void bindAppsRemoved(ArrayList<ApplicationInfo> apps, boolean permanent);
        public void bindPackagesUpdated();
        public boolean isAllAppsVisible();
        public void bindSearchablesChanged();
}

代码段7 LauncherModel中声明的回调函数

loadWorkspace函数执行完成后,bindWorkspace被调用。此方法依次通过回调函数startBinding、bindItems、bindFolders、bindAppWidget、finishBindingItems等由Launcher.java完成最终的显示(至于Launcher.java中这些方法的实现,将在后续文章中分析)。

loadAndBindAllApps与loadAndBindWorkspace实现过程相似,不再缀述。

3.2 数据维护及更新

我们需要注意LauncherModel.java继承了BroadcastReceiver,这就意味着它会接收某些广播并做相应的操作,首先看下它的onReceive方法。

    /**
     * Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and
     * ACTION_PACKAGE_CHANGED.
     */
    @Override
    public void onReceive(Context context, Intent intent) {
        if (DEBUG_LOADERS) Log.d(TAG, "onReceive intent=" + intent);

        final String action = intent.getAction();

        if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
                || Intent.ACTION_PACKAGE_REMOVED.equals(action)
                || Intent.ACTION_PACKAGE_ADDED.equals(action)) {
            final String packageName = intent.getData().getSchemeSpecificPart();
            final boolean replacing = intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);

            int op = PackageUpdatedTask.OP_NONE;

            if (packageName == null || packageName.length() == 0) {
                // they sent us a bad intent
                return;
            }

            if (Intent.ACTION_PACKAGE_CHANGED.equals(action)) {
                op = PackageUpdatedTask.OP_UPDATE;
            } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
                if (!replacing) {
                    op = PackageUpdatedTask.OP_REMOVE;
                }
                // else, we are replacing the package, so a PACKAGE_ADDED will be sent
                // later, we will update the package at this time
            } else if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
                if (!replacing) {
                    op = PackageUpdatedTask.OP_ADD;
                } else {
                    op = PackageUpdatedTask.OP_UPDATE;
                }
            }

            if (op != PackageUpdatedTask.OP_NONE) {
                enqueuePackageUpdated(new PackageUpdatedTask(op, new String[] { packageName }));
            }

        } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
            // First, schedule to add these apps back in.
            String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
            enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_ADD, packages));
            // Then, rebind everything.
            startLoaderFromBackground();
        } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) {
            String[] packages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
            enqueuePackageUpdated(new PackageUpdatedTask(
                        PackageUpdatedTask.OP_UNAVAILABLE, packages));
        } else if (Intent.ACTION_LOCALE_CHANGED.equals(action)) {
            // If we have changed locale we need to clear out the labels in all apps/workspace.
            forceReload();
        } else if (Intent.ACTION_CONFIGURATION_CHANGED.equals(action)) {
             // Check if configuration change was an mcc/mnc change which would affect app resources
             // and we would need to clear out the labels in all apps/workspace. Same handling as
             // above for ACTION_LOCALE_CHANGED
             Configuration currentConfig = context.getResources().getConfiguration();
             if (mPreviousConfigMcc != currentConfig.mcc) {
                   Log.d(TAG, "Reload apps on config change. curr_mcc:"
                       + currentConfig.mcc + " prevmcc:" + mPreviousConfigMcc);
                   forceReload();
             }
             // Update previousConfig
             mPreviousConfigMcc = currentConfig.mcc;
        } else if (SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED.equals(action) ||
                   SearchManager.INTENT_ACTION_SEARCHABLES_CHANGED.equals(action)) {
            if (mCallbacks != null) {
                Callbacks callbacks = mCallbacks.get();
                if (callbacks != null) {
                    callbacks.bindSearchablesChanged();
                }
            }
        }
    }

代码段8 LauncherModel的onReceive方法

显然,根据代码,当系统应用发生更新(ACTION_PACKAGE_CHANGED)、安装新应用(ACTION_PACKAGE_ADDED)、卸载应用(ACTION_PACKAGE_REMOVED)以及其他配置(安装、卸载SD卡等)时,将会启动一条新的线程PackageUpdatedTask执行相应的更新操作,我们主要看下应用变化时的执行流程。

        在PackageUpdatedTask的run方法中,根据onReceive中传来的参数,更新应用列表mAllAppsList,如下

            switch (mOp) {
                case OP_ADD:
                    for (int i=0; i<N; i++) {
                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]);
                        mAllAppsList.addPackage(context, packages[i]);
                    }
                    break;
                case OP_UPDATE:
                    for (int i=0; i<N; i++) {
                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.updatePackage " + packages[i]);
                        mAllAppsList.updatePackage(context, packages[i]);
                    }
                    break;
                case OP_REMOVE:
                case OP_UNAVAILABLE:
                    for (int i=0; i<N; i++) {
                        if (DEBUG_LOADERS) Log.d(TAG, "mAllAppsList.removePackage " + packages[i]);
                        mAllAppsList.removePackage(packages[i]);
                    }
                    break;
            }

代码段9 安装或卸载应用时,更新mAllAppsList

之后再通过回调Launcher,完成界面更新。

            if (added != null) {
                final ArrayList<ApplicationInfo> addedFinal = added;
                mHandler.post(new Runnable() {
                    public void run() {
                        Callbacks cb = mCallbacks != null ? mCallbacks.get() : null;
                        if (callbacks == cb && cb != null) {
                            callbacks.bindAppsAdded(addedFinal);
                        }
                    }
                });
            }
            if (modified != null) {
                final ArrayList<ApplicationInfo> modifiedFinal = modified;
                mHandler.post(new Runnable() {
                    public void run() {
                        Callbacks cb = mCallbacks != null ? mCallbacks.get() : null;
                        if (callbacks == cb && cb != null) {
                            callbacks.bindAppsUpdated(modifiedFinal);
                        }
                    }
                });
            }
            if (removed != null) {
                final boolean permanent = mOp != OP_UNAVAILABLE;
                final ArrayList<ApplicationInfo> removedFinal = removed;
                mHandler.post(new Runnable() {
                    public void run() {
                        Callbacks cb = mCallbacks != null ? mCallbacks.get() : null;
                        if (callbacks == cb && cb != null) {
                            callbacks.bindAppsRemoved(removedFinal, permanent);
                        }
                    }
                });
            }

代码段10 安装或卸载应用时,更新界面

如此一来,当新安装或卸载一个应用之后,launcher会得到实时的更新。

3.3 数据库操作接口

LauncherModel对LauncherProvider中的数据库操作进行了封装,提供了如下接口:

  • addItemToDatabase(Context context, final ItemInfo item, final long container, final int screen, final int cellX, final int cellY, final boolean notify)
  • deleteItemFromDatabase(Context context, final ItemInfo item)
  • modifyItemInDatabase(Context context, final ItemInfo item, final long container, final int screen, final int cellX, final int cellY, final int spanX, final int spanY)
  • moveItemInDatabase(Context context, final ItemInfo item, final long container, final int screen, final int cellX, final int cellY)
  • 。。。。

通过这些接口,上层可以方便的进行launcher的更新。比如,当我们移动一个应用图标时,只需要分两步做:一是调用moveItemInDatabase(或是其他功能相同的接口)完成数据库的更新;二是对workspace中相应的view进行移动(这部分的内容将在后续介绍)。

 注:本文所用代码为android原生代码

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值