【Launcher开发】数据库分析

前言

    在Launcher项目中,WorkSpace及HotSeat的所有图标如ShortcutInfo、Folder、Widget等都需要用到持久化技术以根据用户喜好排列这些图标,并能在下次打开时很方便的找到目标应用或功能。Android持久化技术分为文件存储、SharedPreferences、数据库存储,这几种数据存储方式也各有优劣,文件储存一般用于存储图片、网络请求数据等文本数据或二进制数据,SharedPreferences则是用于存放简单的键值数据,而数据库则适于存放复杂的关系型数据。而对于Launcher应用则使用的是SQLite数据库存储方式,其运算速度快,占用内存小,方便增删改查。

    与Launcher数据库有关的类如下:

  1. LauncherProvider,Launcher应用图标的数据库内容提供者(为了方便其它应用访问,谷歌使用了四大组件之一的ContentProvider对外共享数据);在此提供者内部即是DatabaseHelper(继承自SQLiteOpenHelper),负责数据库的创建和版本升级。
  2. LauncherSettings,由内部类Favorites负责提供一些Uri去操作Provider,以及数据库中对应字段的字段名称。
  3. LauncherModel,模型层,负责通过Provider从数据库中存取数据。
数据库的创建

    我们先由LauncherProvider内部的数据库的构建开始看起:

/**
     * 数据库
     */
    private static class DatabaseHelper extends SQLiteOpenHelper {
        private static final String TAG_FAVORITES = "favorites";
        private static final String TAG_FAVORITE = "favorite";
        private static final String TAG_CLOCK = "clock";
        private static final String TAG_SEARCH = "search";
        private static final String TAG_APPWIDGET = "appwidget";
        private static final String TAG_SHORTCUT = "shortcut";
        private static final String TAG_FOLDER = "folder";
        private static final String TAG_EXTRA = "extra";

        private final Context mContext;
        private final AppWidgetHost mAppWidgetHost;
        private long mMaxId = -1;

        DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
            mContext = context;
            mAppWidgetHost = new AppWidgetHost(context, Launcher.APPWIDGET_HOST_ID);

            // In the case where neither onCreate nor onUpgrade gets called, we read the maxId from
            // the DB here
            if (mMaxId == -1) {
                mMaxId = initializeMaxId(getWritableDatabase());
            }
        }
        ...
}

    其中mMaxId是用于标识一个自增长字段_id的当前的最大值,并且在每次添加新数据时都会调用DatabaseHelper的generateNewId方法使mMaxId+1,在DatabaseHelper中initializeMaxId即是在使用数据库Helper之前保证获取到当前数据库中的最大值,并作为成员变量存在使添加新数据时保证能正确自增。

    当我们使用getWritableDatabase或getReadableDatabase获取数据库实例时,这两方法其实都会调用getDatabaseLocked方法,在此方法中如果实例不存在,则会调用mContext的openOrCreateDatabase创建DataBase,然后根据version==0(即是否第一次创建)来执行onCreate、onDowndrade、onUpdate方法。

    db.beginTransaction();
    try {
         if (version == 0) {
             onCreate(db);
         } else {
             if (version > mNewVersion) {
                 onDowngrade(db, version, mNewVersion);
             } else {
                 onUpgrade(db, version, mNewVersion);
             }
          }
          b.setVersion(mNewVersion);
          db.setTransactionSuccessful();
      } finally {
          db.endTransaction();
      }

    由此可知道数据库第一次执行时即version==0时会调用onCreate方法。而其他version则根据版本升降分别调用onDowngrade或onUpgrade方法。

    所以我们进入Launcher数据库DatabaseHelper的onCreate中:

@Override
        public void onCreate(SQLiteDatabase db) {
            if (LOGD) Log.d(TAG, "creating new launcher database");

            mMaxId = 1;
            final UserManager um =
                    (UserManager) mContext.getSystemService(Context.USER_SERVICE);
            // Default profileId to the serial number of this user.
            long userSerialNumber = um.getSerialNumberForUser(
                    android.os.Process.myUserHandle());
            //表名favorites
            db.execSQL("CREATE TABLE favorites (" +
                    "_id INTEGER PRIMARY KEY," +
                    "title TEXT," +
                    "intent TEXT," +
                    "container INTEGER," +
                    "screen INTEGER," +
                    "cellX INTEGER," +
                    "cellY INTEGER," +
                    "spanX INTEGER," +
                    "spanY INTEGER," +
                    "itemType INTEGER," +
                    "appWidgetId INTEGER NOT NULL DEFAULT -1," +
                    "isShortcut INTEGER," +
                    "iconType INTEGER," +
                    "iconPackage TEXT," +
                    "iconResource TEXT," +
                    "icon BLOB," +
                    "uri TEXT," +
                    "displayMode INTEGER," +
                    "profileId INTEGER DEFAULT " + userSerialNumber +
                    ");");

            // Database was just created, so wipe any previous widgets
            if (mAppWidgetHost != null) {
                mAppWidgetHost.deleteHost();
                sendAppWidgetResetNotify();
            }

            if (!convertDatabase(db)) {
                // Set a shared pref so that we know we need to load the default workspace later
                setFlagToLoadDefaultWorkspaceLater();
            }
        }

    可以看到第一次获取数据库时会创建表favorites,此表正是用于持久化存储WorkSpace和HotSeat中各应用排列信息。下面了解下下各字段含义:

    _id:用于标识区分各个应用图标,是表favorites的主键,当添加数据时通过generateNewId使_id值增加。

    title:在WorkSpace(HotSeat中一般会隐藏掉)中展示的应用快捷图标的标题。

    intent:当点击桌面图标时的负责启动应用的intent,它通过Intent.toUri()转换为String存储,在使用时通过Intent.parseUri()转换为intent。

    container:指的是当前数据所在的容器类型,在Launcher中有两种container类型:1.CONTAINER_DESKTOP(-100),2.CONTAINER_HOTSEAT(-101)。

    screen:用于标识当前数据所在的页。当container为-100时,则screen的值表现为我们桌面的页数的值,当container为-101即当前快捷图标处于HotSeat时,则为默认值-1。

    cellX:当前快捷图标所在页(CellLayout)的X位置,即快捷图标在当前页横向的第X个位置。

    cellY:当前快捷图标所在页(CellLayout)的Y位置,即快捷图标在当前页纵向的第Y个位置。

    spanX:当前快捷图标的在所在页(CellLayout)的横向范围信息,如果当前图标为application、shortcut、folder则为1,表示图标横向上占据一个cell的位置范围。如果当前图标为Widget,则横向占据范围可能为多个cell。

    spanY:当前快捷图标的在所在页(CellLayout)的纵向范围信息,如果当前图标为application、shortcut、folder则为1,表示图标纵向上占据一个cell的位置范围。如果当前图标为Widget,则纵向占据范围可能为多个cell。

    itemType:当前快捷图标的类型,分为以下几种。

        1.ITEM_TYPE_APPLICATION:用于标识应用程序,为默认入口。当我们创建应用时需指定,反映到Launcher中就是此type类型。

        2.ITEM_TYPE_SHORTCUT:应用程序针对的单个页面发送的创建Shortcut快捷方式的intent,即为此类型。

        3.ITEM_TYPE_LIVE_FOLDER:Launcher中没有用到,谷歌已抛弃此type。

        4.ITEM_TYPE_APPWIDGET:用于标识此快捷图标为Widget组件。

        5.ITEM_TYPE_WIDGET_CLOCK:用于标识此快捷图标为时钟组件。

        6.ITEM_TYPE_WIDGET_SEARCH:用于标识此快捷图标为搜索组件。

        7.ITEM_TYPE_WIDGET_PHOTO_FRAME:用于标识此快捷图标为相册组件。

    appWidgetId:在表中定义为默认值为-1的非空整型字段,如果appWidgetId不为-1,说明此快捷图标是桌面小组件Widget。用于标识唯一的Widget控件,在AppWidgetHost(在Launcher中表现为LauncherAppWidgetHost)内部作为键(key)区分SpareArray中的各个AppWidgetHostView(在Launcher中表现为LauncherAppWidgetHostView),且在widget系统管理服务AppWidgetServiceImpl中appWidgetId同packageName一道作为区分各个Widget的标识。

    isShortcut:用于区分是否应用程序通过intent创建的快捷图标。如果值为0,则表示当前数据非应用程序创建的快捷图标,值1则反之。

    iconType:用于标识当前快捷图标的图标类型是资源类型ICON_TYPE_RESOURCE(值为0)还是bitmap类型ICON_TYPE_BITMAP(值为1)。如果是资源类型,则需要通过PackageManager的getResourcesForApplication方法获取Resources,再通过Resources获取bitmap。

    iconPackage:如果iconType标识为资源类型,则此字段才有用,用于标识资源所在包的包名。

    iconResource:如果iconType标识为资源类型,则此字段才有用,用于标识资源图片的id。

    icon:如果iconType为bitmap类型,此字段才有用,用于存放二进制图片数据。

    uri:当ITEM_TYPE为LIVE_FOLDER时才有用,当前此字段已不再使用。

    displayMode:当ITEM_TYPE为LIVE_FOLDER时才有用,当前此字段已不再使用。

数据库的使用

    在Launcher中数据库DatabaseHelper并没有被直接使用到,而是以内容提供者即LauncherProvider的方式供外界访问,完成代码隔离。

    可以看见要访问此Launcher快捷图标信息,需要使用固定字符串的authorities值,且需要读写Settings的权限。

    LauncherProvider作为数据库操作的包装类对外提供了经过包装的增删改查功能。在其构造方法中创建了DatabaseHelper实例,并且把当前Provider设置给LauncherApplication,这样就可以通过LauncherApplication.getLauncherProvider()获取内容提供者。以下是LauncherProvider的增删改查实现。

  • 查询
@Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {

        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);
        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        qb.setTables(args.table);

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        Cursor result = qb.query(db, projection, args.where, args.args, null, null, sortOrder);
        result.setNotificationUri(getContext().getContentResolver(), uri);

        return result;
    }
  • 增加
@Override
    public Uri insert(Uri uri, ContentValues initialValues) {
        SqlArguments args = new SqlArguments(uri);

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        final long rowId = dbInsertAndCheck(mOpenHelper, db, args.table, null, initialValues);
        if (rowId <= 0) return null;

        uri = ContentUris.withAppendedId(uri, rowId);
        sendNotify(uri);

        return uri;
    }
  • 删除
@Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int count = db.delete(args.table, args.where, args.args);
        if (count > 0) sendNotify(uri);

        return count;
    }
  • 修改
@Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        SqlArguments args = new SqlArguments(uri, selection, selectionArgs);

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int count = db.update(args.table, values, args.where, args.args);
        if (count > 0) sendNotify(uri);

        return count;
    }

    当然,在LauncherSetttings内部也为使用者提供了几种Uri。外界可通过这些Uri来访问LauncherProvider所管理的数据库。

/**
         * favorites表的uri,带通知内容观察者功能
         */
        static final Uri CONTENT_URI = Uri.parse("content://" +
                LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES +
                "?" + LauncherProvider.PARAMETER_NOTIFY + "=true");
        /**
         * favorites表的uri,不带通知内容观察者的功能
         */
        static final Uri CONTENT_URI_NO_NOTIFICATION = Uri.parse("content://" +
                LauncherProvider.AUTHORITY + "/" + LauncherProvider.TABLE_FAVORITES +
                "?" + LauncherProvider.PARAMETER_NOTIFY + "=false");

        /**
         * 为CRUD某行数据而准备的uri可通过此方法获取
         * @param id 具体的行id,即表中的_id值
         * @param notify 是否带通知内容观察者功能
         * @return 返回某一行的uri
         */
        static Uri getContentUri(long id, boolean notify) {
            return Uri.parse("content://" + LauncherProvider.AUTHORITY +
                    "/" + LauncherProvider.TABLE_FAVORITES + "/" + id + "?" +
                    LauncherProvider.PARAMETER_NOTIFY + "=" + notify);
        }

    我们知道,Launcher应用是通过LauncherModel的loadAndBindWorkspace方法来加载WorkSpace的应用图标信息,并绑定到桌面上的。而其中loadWorkspace即是通过内容提供者LauncherProvider从数据库中获取数据。

private void loadWorkspace() {
            final long t = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;

            final Context context = mContext;
            final ContentResolver contentResolver = context.getContentResolver();
            final PackageManager manager = context.getPackageManager();
            final AppWidgetManager widgets = AppWidgetManager.getInstance(context);
            final boolean isSafeMode = manager.isSafeMode();

            // Make sure the default workspace is loaded, if needed
            mApp.getLauncherProvider().loadDefaultFavoritesIfNecessary(0, false);

            synchronized (sBgLock) {
                sBgWorkspaceItems.clear();//管理workspace或者hotseat
                sBgAppWidgets.clear();//管理widget
                sBgFolders.clear();//管理文件夹
                sBgItemsIdMap.clear();//所有info管理
                sBgDbIconCache.clear();

                final ArrayList itemsToRemove = new ArrayList();
                /*查询本地数据库信息*/
                final Cursor c = contentResolver.query(
                        LauncherSettings.Favorites.CONTENT_URI, null, null, null, null);
            ...省略
}

    从上可以看到,

        1.调用了LauncherProvider的loadDefaultFavoritesIfNecessary方法。其内部通过一个存放在SharedPreferences中的布尔值来判断当前是否是Launcher的第一次加载,如果是,则解析R.xml.default_workspace文件(此文件中包含默认的快捷图标)到Workspace中。从以下可以看见,默认会加载几个widget和快捷图标到WorkSpace中,且指定了HotSeat默认图标为电话、联系人、短信、浏览器。如果我们自己开发Launcher,可以通过修改Launcher的R.xml.default_workspace文件来达到显示何种默认桌面的问题。


    

    
    

    
    
    

    
    
    

    

    
    
    
    
    

        2.隐式的使用了LauncherProvder进行本地数据库快捷图标信息的查询功能,查询出数据库中的数据显示到Workspace中。此隐式使用在LauncherModel中很常用,使用方式大同小异。

    当然,LauncherModel不止在进入Launcher调用startLoader时才会通过LauncherProvider使用到数据库,当用户通过拖拽调整或删除图标的onDrop中也会使用到。LauncherModel中提供了很多详尽的方法来满足数据库的CRUD的操作。此处仅列举LauncherModel中使用频率较高的几个方法供读者参考。

  • additemToDatabase方法
/**
     *
     * 加到数据库同时同步到集合中
     * Add an item to the database in a specified container. Sets the container, screen, cellX and
     * cellY fields of the item. Also assigns an ID to the item.
     */
    static void addItemToDatabase(Context context, final ItemInfo item, final long container,
            final int screen, final int cellX, final int cellY, final boolean notify) {
        item.container = container;
        item.cellX = cellX;
        item.cellY = cellY;
        // We store hotseat items in canonical form which is this orientation invariant position
        // in the hotseat
        if (context instanceof Launcher && screen < 0 &&
                container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
            //如果在HotSeat中那么screen的值就是cellX。
            item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY);
        } else {
            item.screen = screen;
        }

        final ContentValues values = new ContentValues();
        final ContentResolver cr = context.getContentResolver();
        //在container,cellX,cellY,screen赋值后,把itemInfo的各成员添加到values中
        item.onAddToDatabase(context, values);

        LauncherApplication app = (LauncherApplication) context.getApplicationContext();
        //其内部其实就是maxId+1
        item.id = app.getLauncherProvider().generateNewId();
        values.put(LauncherSettings.Favorites._ID, item.id);
        //更新cellX,cellY。
        item.updateValuesWithCoordinates(values, item.cellX, item.cellY);

        final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
        Runnable r = new Runnable() {
            public void run() {
                String transaction = "DbDebug    Add item (" + item.title + ") to db, id: "
                        + item.id + " (" + container + ", " + screen + ", " + cellX + ", "
                        + cellY + ")";
                Launcher.sDumpLogs.add(transaction);
                Log.d(TAG, transaction);
                //具体执行数据库的CURD操作
                cr.insert(notify ? LauncherSettings.Favorites.CONTENT_URI :
                        LauncherSettings.Favorites.CONTENT_URI_NO_NOTIFICATION, values);

                // Lock on mBgLock *after* the db operation
                synchronized (sBgLock) {
                    checkItemInfoLocked(item.id, item, stackTrace);
                    sBgItemsIdMap.put(item.id, item);//sBgItemsIdMap存放所有的info appwidget,folder,shortcut...
                    switch (item.itemType) {
                        case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
                            sBgFolders.put(item.id, (FolderInfo) item);//sBgFolders存放所有FolderInfo
                            // Fall through
                        case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                        case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                            if (item.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
                                    item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
                                sBgWorkspaceItems.add(item);//存放所有ShortCutInfo
                            } else {
                                if (!sBgFolders.containsKey(item.container)) {
                                    // Adding an item to a folder that doesn't exist.
                                    String msg = "adding item: " + item + " to a folder that " +
                                            " doesn't exist";
                                    Log.e(TAG, msg);
                                    Launcher.dumpDebugLogsToConsole();
                                }
                            }
                            break;
                        case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
                            sBgAppWidgets.add((LauncherAppWidgetInfo) item);//存放所有AppWidgetInfo
                            break;
                    }
                }
            }
        };
        runOnWorkerThread(r);
    }
  •     moveItemInDatabase方法
 /**
     * Move an item in the DB to a new 
     */
    static void moveItemInDatabase(Context context, final ItemInfo item, final long container,
            final int screen, final int cellX, final int cellY) {
        String transaction = "DbDebug    Modify item (" + item.title + ") in db, id: " + item.id +
                " (" + item.container + ", " + item.screen + ", " + item.cellX + ", " + item.cellY +
                ") --> " + "(" + container + ", " + screen + ", " + cellX + ", " + cellY + ")";
        Launcher.sDumpLogs.add(transaction);
        Log.d(TAG, transaction);
        item.container = container;
        item.cellX = cellX;
        item.cellY = cellY;

        // We store hotseat items in canonical form which is this orientation invariant position
        // in the hotseat
        if (context instanceof Launcher && screen < 0 &&
                container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
            item.screen = ((Launcher) context).getHotseat().getOrderInHotseat(cellX, cellY);
        } else {
            item.screen = screen;
        }

        final ContentValues values = new ContentValues();
        values.put(LauncherSettings.Favorites.CONTAINER, item.container);
        values.put(LauncherSettings.Favorites.CELLX, item.cellX);
        values.put(LauncherSettings.Favorites.CELLY, item.cellY);
        values.put(LauncherSettings.Favorites.SCREEN, item.screen);

        updateItemInDatabaseHelper(context, values, item, "moveItemInDatabase");
    }
    /**
     * 把itemInfo更新到db中,并且重新判断itemType来选择当前itemInfo该存放的List
     * @param context
     * @param values
     * @param item
     * @param callingFunction
     */
    static void updateItemInDatabaseHelper(Context context, final ContentValues values,
                                           final ItemInfo item, final String callingFunction) {
        final long itemId = item.id;
        final Uri uri = LauncherSettings.Favorites.getContentUri(itemId, false);
        final ContentResolver cr = context.getContentResolver();

        final StackTraceElement[] stackTrace = new Throwable().getStackTrace();
        Runnable r = new Runnable() {
            public void run() {
                cr.update(uri, values, null, null);

                // Lock on mBgLock *after* the db operation
                synchronized (sBgLock) {
                    checkItemInfoLocked(itemId, item, stackTrace);

                    if (item.container != LauncherSettings.Favorites.CONTAINER_DESKTOP &&
                            item.container != LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
                        // Item is in a folder, make sure this folder exists
                        if (!sBgFolders.containsKey(item.container)) {
                            // An items container is being set to a that of an item which is not in
                            // the list of Folders.
                            String msg = "item: " + item + " container being set to: " +
                                    item.container + ", not in the list of folders";
                            Log.e(TAG, msg);
                            Launcher.dumpDebugLogsToConsole();
                        }
                    }

                    // Items are added/removed from the corresponding FolderInfo elsewhere, such
                    // as in Workspace.onDrop. Here, we just add/remove them from the list of items
                    // that are on the desktop, as appropriate
                    ItemInfo modelItem = sBgItemsIdMap.get(itemId);
                    if (modelItem.container == LauncherSettings.Favorites.CONTAINER_DESKTOP ||
                            modelItem.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) {
                        switch (modelItem.itemType) {
                            case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                            case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                            case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
                                if (!sBgWorkspaceItems.contains(modelItem)) {
                                    sBgWorkspaceItems.add(modelItem);
                                }
                                break;
                            default:
                                break;
                        }
                    } else {
                        sBgWorkspaceItems.remove(modelItem);
                    }
                }
            }
        };
        runOnWorkerThread(r);
    }
  • deleteItemFromDatabase方法
/**
     * Removes the specified item from the database 和list
     * @param context
     * @param item
     */
    static void deleteItemFromDatabase(Context context, final ItemInfo item) {
        final ContentResolver cr = context.getContentResolver();
        final Uri uriToDelete = LauncherSettings.Favorites.getContentUri(item.id, false);

        Runnable r = new Runnable() {
            public void run() {
                String transaction = "DbDebug    Delete item (" + item.title + ") from db, id: "
                        + item.id + " (" + item.container + ", " + item.screen + ", " + item.cellX +
                        ", " + item.cellY + ")";
                Launcher.sDumpLogs.add(transaction);
                Log.d(TAG, transaction);

                cr.delete(uriToDelete, null, null);

                // Lock on mBgLock *after* the db operation
                synchronized (sBgLock) {
                    switch (item.itemType) {
                        case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
                            sBgFolders.remove(item.id);
                            for (ItemInfo info: sBgItemsIdMap.values()) {
                                if (info.container == item.id) {
                                    // We are deleting a folder which still contains items that
                                    // think they are contained by that folder.
                                    String msg = "deleting a folder (" + item + ") which still " +
                                            "contains items (" + info + ")";
                                    Log.e(TAG, msg);
                                    Launcher.dumpDebugLogsToConsole();
                                }
                            }
                            sBgWorkspaceItems.remove(item);
                            break;
                        case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
                        case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
                            sBgWorkspaceItems.remove(item);
                            break;
                        case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET:
                            sBgAppWidgets.remove((LauncherAppWidgetInfo) item);
                            break;
                    }
                    sBgItemsIdMap.remove(item.id);
                    sBgDbIconCache.remove(item);
                }
            }
        };
        runOnWorkerThread(r);
    }
结语

    本文主要介绍了Launcher快捷图标的排列信息的数据库存储方式及字段含义,以及LauncherModel如何获取的这些排列信息,希望能对读者有帮助。




你可能感兴趣的:(安卓,Launcher源码阅读)