一、安装的Activity

在Android,通过发送Intent可以启动应用的安装过程,如下所示:

Uri uri = Uri.fromFile(new File(filename));Intent inent = new Intent(Intent.ACTION_VIEW);intent.SetDataAndType(uri, application/vnd.android.package-archive);startActivity(intent);

在Android的系统应用PackageInstaller中有一个PackageInstallActivity会响应这个Intent。在这个Activity中有两个重要的成员变量mPm和mInstaller,分别是ApplicationPackageManger和PackageInstaller的实例对象,这两个对象也是PackageManagerService和PackageInstallerService在应用中的代理对象。

PackageInstallerAcivity中创建这两个对象的代码如下:

protected void onCreate(Bundler icicle) {super.onCreate(icicle);mPm = getPackageManager();mInstaller = mPm.getPackageInstaller();......}


二、管理安装会话

PackageManagerInstallerService是Android5.0新加入的服务,主要用于管理安装会话(Installer session)。在Android5.0中,可以通过PackageManagerInstallerService来分配一个SessionId,这个系统唯一的ID代表一次安装过程,如果一个应用的安装分成几个阶段来完成,即使设备重启了,也可以通过这个ID来继续安装过程。

PackageManagerInstallerService中提供的接口createSession来创建一个Session:

    @Override    public int createSession(SessionParams params, String installerPackageName, int userId) {        try {            return createSessionInternal(params, installerPackageName, userId);        } catch (IOException e) {            throw ExceptionUtils.wrap(e);        }    }

这个方法返回一个系统唯一值作为SessionID,如果希望再次使用这个Session,可以通过接口openSession方法来打开它,代码如下:

    @Override    public IPackageInstallerSession openSession(int sessionId) {        try {            return openSessionInternal(sessionId);        } catch (IOException e) {            throw ExceptionUtils.wrap(e);        }    }

openSession返回一个IPackageInstallerSession对象,它是Binder服务PackageInstallerSession的IBinder对象。每个Install Session都会在SystemServer中有一个对应的PackageInstallerSession对象。在PackageInstallerService中mSessions数组保存了所有PackageInstallerSession对象,定义如下:

private final SparseArray mSessions = new SparseArray<>();

当系统启动时,PackageManageService初始化时会创建PackageManagerInstallerService服务,在这个服务的初始化函数中,会读取/data/system目录下的install_sessions.xml文件,这个文件保存系统未完成的Install Session。然后PackagemanagerInstallerService会根据文件的内容创建PackageInstallerSession对象并插入mSessions中。

PackageInstallerSession中保存了应用安装相关的数据。例如,安装包路径,安装进度、中间数据保存的目录等。


三、应用安装过程

应用可以调用PackageManager的installPackage方法来开始安装过程,最终会调用到PackageManagerService的installPackage或者installPackageAsUser来执行安装过程,整个安装过程比较复杂。

整个安装过程可以分成两个阶段:

1.第一阶段把需要安装的应用复制到/data/app目录下

2.第二阶段是对apk文件扫描优化,装载到内存中。


3.1 复制文件

PackageManagerService的installPackage方法只是用当前用户安装应用,最后也会调用installPackageAsUser

    @Override    public void installPackage(String originPath, IPackageInstallObserver2 observer,            int installFlags, String installerPackageName, VerificationParams verificationParams,            String packageAbiOverride) {        installPackageAsUser(originPath, observer, installFlags, installerPackageName, verificationParams,                packageAbiOverride, UserHandle.getCallingUserId());    }

下面我们就来看看installPackageAsUser方法:

    @Override    public void installPackageAsUser(String originPath, IPackageInstallObserver2 observer,            int installFlags, String installerPackageName, VerificationParams verificationParams,            String packageAbiOverride, int userId) {        mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INSTALL_PACKAGES, null);//检查调用进程的权限//检查调用进程的永华是否有权限安装应用        final int callingUid = Binder.getCallingUid();        enforceCrossUserPermission(callingUid, userId, true, true, "installPackageAsUser");//检查指定的用户是否被限制安装应用        if (isUserRestricted(userId, UserManager.DISALLOW_INSTALL_APPS)) {            try {                if (observer != null) {                    observer.onPackageInstalled("", INSTALL_FAILED_USER_RESTRICTED, null, null);                }            } catch (RemoteException re) {            }            return;        }        if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) {            installFlags |= PackageManager.INSTALL_FROM_ADB;        } else {            // Caller holds INSTALL_PACKAGES permission, so we're less strict            // about installerPackageName.            installFlags &= ~PackageManager.INSTALL_FROM_ADB;            installFlags &= ~PackageManager.INSTALL_ALL_USERS;        }        UserHandle user;        if ((installFlags & PackageManager.INSTALL_ALL_USERS) != 0) {//给所有用户安装            user = UserHandle.ALL;        } else {            user = new UserHandle(userId);        }        verificationParams.setInstallerUid(callingUid);        final File originFile = new File(originPath);        final OriginInfo origin = OriginInfo.fromUntrustedFile(originFile);        final Message msg = mHandler.obtainMessage(INIT_COPY);        msg.obj = new InstallParams(origin, observer, installFlags,//保存参数到InstallParamsm,发送消息                installerPackageName, verificationParams, user, packageAbiOverride);        mHandler.sendMessage(msg);    }

installPackageAsUser先检查调用进程是否有安装应用的权限,再检查调用进程所属的用户是否有权限安装应用,最后检查指定的用户是否被限制安装应用。如果参数installFlags带有INSTALL_ALL_USERS,则该应用将给系统中所有用户安装,否则只给指定用户安装。
安装应用实践比较长,因此不可能在一个函数中完成。上面函数把数据保存在installParams然后发送了INIT_COPY消息。

下面我们再来看看消息处理:

        void doHandleMessage(Message msg) {            switch (msg.what) {                case INIT_COPY: {                    HandlerParams params = (HandlerParams) msg.obj;                    int idx = mPendingInstalls.size();                    if (DEBUG_INSTALL) Slog.i(TAG, "init_copy idx=" + idx + ": " + params);                    // If a bind was already initiated we dont really                    // need to do anything. The pending install                    // will be processed later on.                    if (!mBound) {                        // If this is the only one pending we might                        // have to bind to the service again.                        if (!connectToService()) {//绑定DefaultContainerService                            Slog.e(TAG, "Failed to bind to media container service");                            params.serviceError();                            return;                        } else {//连接成功把安装信息保存到mPendingInstalls                            // Once we bind to the service, the first                            // pending request will be processed.                            mPendingInstalls.add(idx, params);                        }                    } else {//如果已经绑定好了                        mPendingInstalls.add(idx, params);                        // Already bound to the service. Just make                        // sure we trigger off processing the first request.                        if (idx == 0) {                            mHandler.sendEmptyMessage(MCS_BOUND);                        }                    }                    break;                }

INIT_COPY消息的处理将绑定DefaultContainerService,因为这是一个异步的过程,要等待绑定的结果通过onServiceConnected返回,所以这里的安装参数放到了mPendingInstalls列表中。如果这个Service以前就绑定好了,现在就不需要再绑定,安装信息也会先放到mPendingInstalls。如果有多个安装请求同时到达,这里通过mPendingInstalls列表对他们进行排队。如果列表中只有一项,说明没有更多的安装请求,因此这种情况下回立即发出MCS_BOUND消息。而onServiceConnected方法同样是发出MCS_BOUND消息:

    class DefaultContainerConnection implements ServiceConnection {        public void onServiceConnected(ComponentName name, IBinder service) {            if (DEBUG_SD_INSTALL) Log.i(TAG, "onServiceConnected");            IMediaContainerService imcs =                IMediaContainerService.Stub.asInterface(service);            mHandler.sendMessage(mHandler.obtainMessage(MCS_BOUND, imcs));        }        public void onServiceDisconnected(ComponentName name) {            if (DEBUG_SD_INSTALL) Log.i(TAG, "onServiceDisconnected");        }    };

看下MCS_BOUND的消息处理

                case MCS_BOUND: {                    if (DEBUG_INSTALL) Slog.i(TAG, "mcs_bound");                    if (msg.obj != null) {                        mContainerService = (IMediaContainerService) msg.obj;                    }                    if (mContainerService == null) {//没有连接成功                        // Something seriously wrong. Bail out                        Slog.e(TAG, "Cannot bind to media container service");                        for (HandlerParams params : mPendingInstalls) {                            // Indicate service bind error                            params.serviceError();//通知出错了                        }                        mPendingInstalls.clear();                    } else if (mPendingInstalls.size() > 0) {                        HandlerParams params = mPendingInstalls.get(0);                        if (params != null) {                            if (params.startCopy()) {//执行安装                                // We are done...  look for more work or to                                // go idle.                                if (DEBUG_SD_INSTALL) Log.i(TAG,                                        "Checking for more work or unbind...");                                // Delete pending install                                if (mPendingInstalls.size() > 0) {                                    mPendingInstalls.remove(0);//工作完成,删除第一项                                }                                if (mPendingInstalls.size() == 0) {//如果没有安装消息了,延时发送10秒MCS_UNBIND消息                                    if (mBound) {                                        if (DEBUG_SD_INSTALL) Log.i(TAG,                                                "Posting delayed MCS_UNBIND");                                        removeMessages(MCS_UNBIND);                                        Message ubmsg = obtainMessage(MCS_UNBIND);                                        // Unbind after a little delay, to avoid                                        // continual thrashing.                                        sendMessageDelayed(ubmsg, 10000);                                    }                                } else {                                    // There are more pending requests in queue.                                    // Just post MCS_BOUND message to trigger processing                                    // of next pending install.                                    if (DEBUG_SD_INSTALL) Log.i(TAG,                                            "Posting MCS_BOUND for next work");                                    mHandler.sendEmptyMessage(MCS_BOUND);//还有消息继续发送MCS_BOUND消息                                }                            }                        }                    } else {                        // Should never happen ideally.                        Slog.w(TAG, "Empty queue");                    }                    break;                }

如果结束了我们看看MCS_UNBIND消息的处理

                case MCS_UNBIND: {                    // If there is no actual work left, then time to unbind.                    if (DEBUG_INSTALL) Slog.i(TAG, "mcs_unbind");                    if (mPendingInstalls.size() == 0 && mPendingVerification.size() == 0) {                        if (mBound) {                            if (DEBUG_INSTALL) Slog.i(TAG, "calling disconnectService()");                            disconnectService();//断开连接                        }                    } else if (mPendingInstalls.size() > 0) {                        // There are more pending requests in queue.                        // Just post MCS_BOUND message to trigger processing                        // of next pending install.                        mHandler.sendEmptyMessage(MCS_BOUND);                    }                    break;                }

MCS_UNBIND消息的处理,如果处理的时候发现mPendingInstalls又有数据了,还是发送MCS_BOUND消息继续安装,否则断开和DefaultContainerService的连接,安装结束。
下面我们看执行安装的函数startCopy:

        final boolean startCopy() {            boolean res;            try {                if (DEBUG_INSTALL) Slog.i(TAG, "startCopy " + mUser + ": " + this);                if (++mRetries > MAX_RETRIES) {//重试超过4次退出                    Slog.w(TAG, "Failed to invoke remote methods on default container service. Giving up");                    mHandler.sendEmptyMessage(MCS_GIVE_UP);                    handleServiceError();                    return false;                } else {                    handleStartCopy();                    res = true;                }            } catch (RemoteException e) {                if (DEBUG_INSTALL) Slog.i(TAG, "Posting install MCS_RECONNECT");                mHandler.sendEmptyMessage(MCS_RECONNECT);//安装出错,发送重新连接                res = false;            }            handleReturnCode();            return res;        }

handleStartCopy和copyApk代码就不分析了。

handleStartCopy函数先通过DefaultContainerService调用了getMinimallPackageInfo来确定安装位置是否有足够的空间,并在PackageInfoLite对象的recommendedIntallLocation记录错误原因。发现空间不够,会调用installer的freecache方法来释放一部分空间。

再接下来handleStartCopy有很长一段都在处理apk的校验,这个校验过程是通过发送Intent ACTION_PACKAGE_NEEDS_VERIFICATION给系统中所有接受该Intent的应用来完成。如果无需校验,直接调用InstallArgs对象的copyApk方法。

而copyApk方法同样是调用DefaultContainerService的copyPackage将应用的文件复制到/data/app下,如果还有native动态库,也会把包在apk文件中的动态库提取出来。

执行完copyApk后,应用安装到了data/app目录下了。


3.2 装载应用

接下来是第二阶段的工作,把应用的格式装换成oat格式,为应用创建数据目录。最后把应用信息装载进PackageManagerService的数据结构中。

接着上面startCopy方法最后会调用handleReturnCode方法,代码如下:

        @Override        void handleReturnCode() {            // If mArgs is null, then MCS couldn't be reached. When it            // reconnects, it will try again to install. At that point, this            // will succeed.            if (mArgs != null) {                processPendingInstall(mArgs, mRet);            }        }

我们继续看下processPendingInstall函数。

    private void processPendingInstall(final InstallArgs args, final int currentStatus) {        // Queue up an async operation since the package installation may take a little while.        mHandler.post(new Runnable() {            public void run() {                mHandler.removeCallbacks(this);//防止重复调用                 // Result object to be returned                PackageInstalledInfo res = new PackageInstalledInfo();                res.returnCode = currentStatus;                res.uid = -1;                res.pkg = null;                res.removedInfo = new PackageRemovedInfo();                if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {//如果安装成功了                    args.doPreInstall(res.returnCode);                    synchronized (mInstallLock) {                        installPackageLI(args, res);//装载安装的应用                    }                    args.doPostInstall(res.returnCode, res.uid);                }..........                if (!doRestore) {                    // 发送POST_INSTALL消息                    Message msg = mHandler.obtainMessage(POST_INSTALL, token, 0);                    mHandler.sendMessage(msg);                }            }        });    }

processPendingInstall方法post了一个消息,这样安装过程以异步的方式继续执行。在post消息的处理中,首先调用installPackageLI来装载应用,然后很大的代码在执行备份,备份是通过BackupManagerService来完成的。备份完成后,通过发送POST_INSTALL消息来继续处理。而这个消息的处理主要就是在发送广播,应用安装完成后要通知系统中其他的应用开始处理,比如Launcher中需要增加应用的图标等。

我们来分析下installPackageLI方法:

    private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {        final int installFlags = args.installFlags;        String installerPackageName = args.installerPackageName;        File tmpPackageFile = new File(args.getCodePath());        boolean forwardLocked = ((installFlags & PackageManager.INSTALL_FORWARD_LOCK) != 0);        boolean onSd = ((installFlags & PackageManager.INSTALL_EXTERNAL) != 0);        boolean replace = false;        final int scanFlags = SCAN_NEW_INSTALL | SCAN_FORCE_DEX | SCAN_UPDATE_SIGNATURE;        // Result object to be returned        res.returnCode = PackageManager.INSTALL_SUCCEEDED;        if (DEBUG_INSTALL) Slog.d(TAG, "installPackageLI: path=" + tmpPackageFile);        // Retrieve PackageSettings and parse package        final int parseFlags = mDefParseFlags | PackageParser.PARSE_CHATTY                | (forwardLocked ? PackageParser.PARSE_FORWARD_LOCK : 0)                | (onSd ? PackageParser.PARSE_ON_SDCARD : 0);        PackageParser pp = new PackageParser();        pp.setSeparateProcesses(mSeparateProcesses);        pp.setDisplayMetrics(mMetrics);        final PackageParser.Package pkg;        try {            pkg = pp.parsePackage(tmpPackageFile, parseFlags);//解析apk文件        } catch (PackageParserException e) {            res.setError("Failed parse during installPackageLI", e);            return;        }

这里先调用parsePackage解析apk文件,这个之前分析过,我们就不再分析了。

继续分析processPendingInstall函数

if ((installFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) {//如果安装的升级应用,继续使用以前的老的包名                String oldName = mSettings.mRenamedPackages.get(pkgName);                if (pkg.mOriginalPackages != null                        && pkg.mOriginalPackages.contains(oldName)                        && mPackages.containsKey(oldName)) {                    // This package is derived from an original package,                    // and this device has been updating from that original                    // name.  We must continue using the original name, so                    // rename the new package here.                    pkg.setPackageName(oldName);//设置老的包名                    pkgName = pkg.packageName;                    replace = true;                    if (DEBUG_INSTALL) Slog.d(TAG, "Replacing existing renamed package: oldName="                            + oldName + " pkgName=" + pkgName);                } else if (mPackages.containsKey(pkgName)) {                    // This package, under its official name, already exists                    // on the device; we should replace it.                    replace = true;                    if (DEBUG_INSTALL) Slog.d(TAG, "Replace existing pacakge: " + pkgName);                }            }

继续分析

        if (systemApp && onSd) {//不能将系统应用装载sd卡            // Disable updates to system apps on sdcard            res.setError(INSTALL_FAILED_INVALID_INSTALL_LOCATION,                    "Cannot install updates to system apps on sdcard");            return;        }        if (!args.doRename(res.returnCode, pkg, oldCodePath)) {//重命名出错            res.setError(INSTALL_FAILED_INSUFFICIENT_STORAGE, "Failed rename");            return;        }

继续分析

        if (replace) {//如果安装的是升级包,调用replacePackageLI            replacePackageLI(pkg, parseFlags, scanFlags | SCAN_REPLACING, args.user,                    installerPackageName, res);        } else {//如果是新应用,调用installNewPackageLI继续处理            installNewPackageLI(pkg, parseFlags, scanFlags | SCAN_DELETE_DATA_ON_FAILURES,                    args.user, installerPackageName, res);        }        synchronized (mPackages) {            final PackageSetting ps = mSettings.mPackages.get(pkgName);            if (ps != null) {                res.newUsers = ps.queryInstalledUsers(sUserManager.getUserIds(), true);            }        }

下面我们分析下installNewPackageLI函数

    private void installNewPackageLI(PackageParser.Package pkg,            int parseFlags, int scanFlags, UserHandle user,            String installerPackageName, PackageInstalledInfo res) {        // Remember this for later, in case we need to rollback this install        String pkgName = pkg.packageName;        if (DEBUG_INSTALL) Slog.d(TAG, "installNewPackageLI: " + pkg);        boolean dataDirExists = getDataPathForPackage(pkg.packageName, 0).exists();        synchronized(mPackages) {            if (mSettings.mRenamedPackages.containsKey(pkgName)) {                // A package with the same name is already installed, though                // it has been renamed to an older name.  The package we                // are trying to install should be installed as an update to                // the existing one, but that has not been requested, so bail.                res.setError(INSTALL_FAILED_ALREADY_EXISTS, "Attempt to re-install " + pkgName                        + " without first uninstalling package running as "                        + mSettings.mRenamedPackages.get(pkgName));                return;            }            if (mPackages.containsKey(pkgName)) {                // Don't allow installation over an existing package with the same name.                res.setError(INSTALL_FAILED_ALREADY_EXISTS, "Attempt to re-install " + pkgName                        + " without first uninstalling.");                return;            }        }        try {            PackageParser.Package newPackage = scanPackageLI(pkg, parseFlags, scanFlags,//调用scanPackageLI                    System.currentTimeMillis(), user);            updateSettingsLI(newPackage, installerPackageName, null, null, res);            // delete the partially installed application. the data directory will have to be            // restored if it was already existing            if (res.returnCode != PackageManager.INSTALL_SUCCEEDED) {                // remove package from internal structures.  Note that we want deletePackageX to                // delete the package data and cache directories that it created in                // scanPackageLocked, unless those directories existed before we even tried to                // install.                deletePackageLI(pkgName, UserHandle.ALL, false, null, null,                        dataDirExists ? PackageManager.DELETE_KEEP_DATA : 0,                                res.removedInfo, true);            }        } catch (PackageManagerException e) {            res.setError("Package couldn't be installed in " + pkg.codePath, e);        }    }

这里和上篇博客分析扫描apk文件类似,我们来看下这个函数scanPackageLI

    private PackageParser.Package scanPackageLI(PackageParser.Package pkg, int parseFlags,            int scanFlags, long currentTime, UserHandle user) throws PackageManagerException {        boolean success = false;        try {            final PackageParser.Package res = scanPackageDirtyLI(pkg, parseFlags, scanFlags,                    currentTime, user);            success = true;            return res;        } finally {            if (!success && (scanFlags & SCAN_DELETE_DATA_ON_FAILURES) != 0) {                removeDataDirsLI(pkg.packageName);            }        }    }

scanPackageLI函数主要调用了scanPackageDirtyLI函数,这个函数前面分析过了就不分析了。


我们再来看下在processPendingInstall函数中调用完installPackageLI函数之后,发送了一个POST_INSTALL消息,我们来看下这个消息的处理

                case POST_INSTALL: {                    if (DEBUG_INSTALL) Log.v(TAG, "Handling post-install for " + msg.arg1);                    PostInstallData data = mRunningInstalls.get(msg.arg1);                    mRunningInstalls.delete(msg.arg1);                    boolean deleteOld = false;                    if (data != null) {                        InstallArgs args = data.args;                        PackageInstalledInfo res = data.res;                        if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {//安装成功                            final String packageName = res.pkg.applicationInfo.packageName;                            res.removedInfo.sendBroadcast(false, true, false);                            Bundle extras = new Bundle(1);                            extras.putInt(Intent.EXTRA_UID, res.uid);                            // Now that we successfully installed the package, grant runtime                            // permissions if requested before broadcasting the install.                            if ((args.installFlags                                    & PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS) != 0) {                                grantRequestedRuntimePermissions(res.pkg, args.user.getIdentifier(),                                        args.installGrantPermissions);                            }                            // Determine the set of users who are adding this                            // package for the first time vs. those who are seeing                            // an update.                            int[] firstUsers;                            int[] updateUsers = new int[0];                            if (res.origUsers == null || res.origUsers.length == 0) {                                firstUsers = res.newUsers;                            } else {                                firstUsers = new int[0];                                for (int i=0; i AVAILABLE");                                    }                                    int[] uidArray = new int[] { res.pkg.applicationInfo.uid };                                    ArrayList pkgList = new ArrayList(1);                                    pkgList.add(packageName);                                    sendResourcesChangedBroadcast(true, true,                                            pkgList,uidArray, null);                                }                            }                            if (res.removedInfo.args != null) {                                // Remove the replaced package's older resources safely now                                deleteOld = true;                            }                            // If this app is a browser and it's newly-installed for some                            // users, clear any default-browser state in those users                            if (firstUsers.length > 0) {                                // the app's nature doesn't depend on the user, so we can just                                // check its browser nature in any user and generalize.                                if (packageIsBrowser(packageName, firstUsers[0])) {                                    synchronized (mPackages) {                                        for (int userId : firstUsers) {                                            mSettings.setDefaultBrowserPackageNameLPw(null, userId);                                        }                                    }                                }                            }                            // Log current value of "unknown sources" setting                            EventLog.writeEvent(EventLogTags.UNKNOWN_SOURCES_ENABLED,                                getUnknownSourcesSettings());                        }                        // Force a gc to clear up things                        Runtime.getRuntime().gc();                        // We delete after a gc for applications  on sdcard.                        if (deleteOld) {                            synchronized (mInstallLock) {                                res.removedInfo.args.doPostDeleteLI(true);                            }                        }                        if (args.observer != null) {                            try {                                Bundle extras = extrasForInstallResult(res);                                args.observer.onPackageInstalled(res.name, res.returnCode,                                        res.returnMsg, extras);                            } catch (RemoteException e) {                                Slog.i(TAG, "Observer no longer exists.");                            }                        }                    } else {                        Slog.e(TAG, "Bogus post-install token " + msg.arg1);                    }                } break;

这样安装应用的流程就讲完了。


更多相关文章

  1. C语言函数的递归(上)
  2. Android跨进程通信IPC之16——Binder之native层C++篇--获取服务
  3. android之GSON解析JSON
  4. 从Process xxxx (pid xxx) has died分析
  5. Android(安卓)开发中的 Handler ,Thread ,Message ,Runnable 的
  6. Android(4.X)学习笔记
  7. Android之Handler详解(四)
  8. 【Android】通过软引用实现图片缓存,防止内存溢出
  9. 用PC应用程序通过USB读写Andriod里面的数据

随机推荐

  1. 初涉Android蓝牙开发
  2. Android百度地图(一):百度地图定位sdk 类
  3. android task与back stack 开发文档翻译
  4. 说不懂Android系统构架,太亏了!
  5. Android(安卓)imageView图片按比例缩放
  6. Android(安卓)GWES之Android窗口管理
  7. Android学习 2 -建立模拟器
  8. Android(安卓)文件布局一些细节备忘
  9. Android(安卓)Support v4、v7、v13的区别
  10. 初涉Android蓝牙开发