Launcher概述

SystemServer进程主要用于启动系统的各种服务,其中就包含了Launcher服务,LauncherAppService。

Android系统默认第一个启动的应用程序是Home应用程序,这个应用程序用来显示系统中已经安装的应用程序,这个Home应用程序就叫做Launcher。应用程序Launcher在启动过程中会请求PackageManagerService返回系统中已经安装的应用程序的信息,并将这些信息封装成一个快捷图标列表显示在系统屏幕上,这样用户可以通过点击这些快捷图标来启动相应的应用程序。

Launcher启动过程

上篇文章讲到SystemServer会分别启动bootstrap service、core service和other service。在调用startOtherService方法中:

frameworks/base/services/java/com/android/server/SystemServer.java

SystemServer#startOtherService()

 private void startOtherServices() {...//1mActivityManagerService.systemReady(new Runnable() {@Overridepublic void run() {/*** 执行各种SystemService的启动方法,各种SystemService的systemReady方法...*/Slog.i(TAG, "Making services ready");mSystemServiceManager.startBootPhase(SystemService.PHASE_ACTIVITY_MANAGER_READY);...}
...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

注释1处调用ActivityManagerService的systemReady函数。

frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java

ActivityManagerService#systemReady()

public void systemReady(final Runnable goingCallback) {...// Start up initial activity.mBooting = true;// Enable home activity for system user, so that the system can always bootif (UserManager.isSplitSystemUser()) {ComponentName cName = new ComponentName(mContext, SystemUserHomeActivity.class);try {AppGlobals.getPackageManager().setComponentEnabledSetting(cName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0, UserHandle.USER_SYSTEM);} catch (RemoteException e) {throw e.rethrowAsRuntimeException();}}startHomeActivityLocked(currentUserId, "systemReady");  //1...}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

注释1处调用了startHomeActivityLocked方法,看其名字就是说开始执行启动homeActivity的操作。

ActivityManagerService#startHomeActivityLocked()

    boolean startHomeActivityLocked(int userId, String reason) {if (mFactoryTest == FactoryTest.FACTORY_TEST_LOW_LEVEL && mTopAction == null) {  //1// We are running in factory test mode, but unable to find// the factory test app, so just sit around displaying the// error message and don't try to start anything.return false;}Intent intent = getHomeIntent();  //2ActivityInfo aInfo = resolveActivityInfo(intent, STOCK_PM_FLAGS, userId);if (aInfo != null) {intent.setComponent(new ComponentName(aInfo.applicationInfo.packageName, aInfo.name));// Don't do this if the home app is currently being// instrumented.aInfo = new ActivityInfo(aInfo);aInfo.applicationInfo = getAppInfoForUser(aInfo.applicationInfo, userId);ProcessRecord app = getProcessRecordLocked(aInfo.processName, aInfo.applicationInfo.uid, true);if (app == null || app.instrumentationClass == null) {intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);mActivityStarter.startHomeActivityLocked(intent, aInfo, reason); //3}} else {Slog.wtf(TAG, "No home screen found for " + intent, new Throwable());}return true;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

注释1处的mFactoryTest代表系统的运行模式,系统的运行模式分为三种,分别是非工厂模式、低级工厂模式和高级工厂模式,mTopAction则用来描述第一个被启动Activity组件的Action,它的值为Intent.ACTION_MAIN。因此注释1的代码意思就是mFactoryTest为FactoryTest.FACTORY_TEST_LOW_LEVEL(低级工厂模式)并且mTopAction=null时,直接返回false。 
注释2处的getHomeIntent函数如下所示。

ActivityManagerService#getHomeIntent()

    Intent getHomeIntent() {Intent intent = new Intent(mTopAction, mTopData != null ? Uri.parse(mTopData) : null); //1intent.setComponent(mTopComponent);intent.addFlags(Intent.FLAG_DEBUG_TRIAGED_MISSING);if (mFactoryTest != FactoryTest.FACTORY_TEST_LOW_LEVEL) {intent.addCategory(Intent.CATEGORY_HOME); //2}return intent;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

注释1中创建了Intent,并将mTopAction和mTopData传入。mTopAction的值为Intent.ACTION_MAIN。 
注释2如果系统运行模式不是低级工厂模式则将intent的Category设置为Intent.CATEGORY_HOME。之后被启动的应用程序就是Launcher,因为Launcher的Manifest文件中的intent-filter标签匹配了Action为Intent.ACTION_MAIN,Category为Intent.CATEGORY_HOME。Launcher的Manifest文件如下所示。

packages/apps/Launcher3/AndroidManifest.xml

<manifestxmlns:android="http://schemas.android.com/apk/res/android"package="com.android.launcher3"><uses-sdk android:targetSdkVersion="23" android:minSdkVersion="16"/>...<application...<activityandroid:name="com.android.launcher3.Launcher"android:launchMode="singleTask"android:clearTaskOnLaunch="true"android:stateNotNeeded="true"android:theme="@style/Theme"android:windowSoftInputMode="adjustPan"android:screenOrientation="nosensor"android:configChanges="keyboard|keyboardHidden|navigation"android:resumeWhilePausing="true"android:taskAffinity=""android:enabled="true"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.HOME" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.MONKEY"/></intent-filter></activity>
...</application>
</manifest>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

ActivityManagerService的startHomeActivityLocked()的注释3就是启动符合条件的应用程序,即Launcher。

frameworks/base/services/core/java/com/android/server/am/ActivityStarter.java

ActivityStarter#startHomeActivityLocked()

    void startHomeActivityLocked(Intent intent, ActivityInfo aInfo, String reason) {mSupervisor.moveHomeStackTaskToTop(HOME_ACTIVITY_TYPE, reason);startActivityLocked(null /*caller*/, intent, null /*ephemeralIntent*/,null /*resolvedType*/, aInfo, null /*rInfo*/, null /*voiceSession*/,null /*voiceInteractor*/, null /*resultTo*/, null /*resultWho*/,0 /*requestCode*/, 0 /*callingPid*/, 0 /*callingUid*/, null /*callingPackage*/,0 /*realCallingPid*/, 0 /*realCallingUid*/, 0 /*startFlags*/, null /*options*/,false /*ignoreTargetSecurity*/, false /*componentSpecified*/, null /*outActivity*/,null /*container*/, null /*inTask*/);if (mSupervisor.inResumeTopActivity) {// If we are in resume section already, home activity will be initialized, but not// resumed (to avoid recursive resume) and will stay that way until something pokes it// again. We need to schedule another resume.mSupervisor.scheduleResumeTopActivities();  //1}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

注释1调用的是scheduleResumeTopActivities()方法,这个方法其实是关于Activity的启动流程的逻辑了,这里我们就不详细说明了,关于Activity的启动流程可以参考我后面文章。

这样Launcher就会被启动起来,并执行它的onCreate函数。

Android应用程序安装

Android系统在启动的过程中,Zygote进程启动SystemServer进程,SystemServer启动PackageManagerService服务,这个服务负责扫描系统中特定的目录,找到里面的应用程序文件,即以Apk为后缀的文件,然后对这些文件进解析(其实就是解析应用程序配置文件AndroidManifest.xml的过程),并从里面得到得到应用程序的相关信息,例如得到应用程序的组件Package、Activity、Service、Broadcast Receiver和Content Provider等信息,保存到PackageManagerService的mPackages、mActivities、mServices、mReceivers等成员变量(HashMap类型)中,得到应用程序的相关信息之后,完成应用程序的安装过程。

这些应用程序只是相当于在PackageManagerService服务注册好了,如果我们想要在Android桌面上看到这些应用程序,还需要有一个Home应用程序(Android系统默认的Home应用程序就是Launcher),负责从PackageManagerService服务中把这些安装好的应用程序取出来,并以友好的方式在桌面上展现出来,例如以快捷图标的形式,接着往下看。

Launcher中应用图标显示流程

从Launcher的onCreate函数开始分析。

packages/apps/Launcher3/src/com/android/launcher3/Launcher.java

Launcher#onCreate()

    @Overrideprotected void onCreate(Bundle savedInstanceState) {...LauncherAppState app = LauncherAppState.getInstance();//1mDeviceProfile = getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE ?app.getInvariantDeviceProfile().landscapeProfile: app.getInvariantDeviceProfile().portraitProfile;mSharedPrefs = Utilities.getPrefs(this);mIsSafeModeEnabled = getPackageManager().isSafeMode();mModel = app.setLauncher(this);//2....if (!mRestoring) {if (DISABLE_SYNCHRONOUS_BINDING_CURRENT_PAGE) {mModel.startLoader(PagedView.INVALID_RESTORE_PAGE);//3} else {mModel.startLoader(mWorkspace.getRestorePage());}}
...}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

注释1处获取LauncherAppState的实例。 
注释2处调用它的setLauncher函数并将Launcher对象传入。

packages/apps/Launcher3/src/com/android/launcher3/LauncherAppState.java

LauncherAppState#setLauncher()

LauncherModel setLauncher(Launcher launcher) {getLauncherProvider().setLauncherProviderChangeListener(launcher);mModel.initialize(launcher);//1mAccessibilityDelegate = ((launcher != null) && Utilities.ATLEAST_LOLLIPOP) ? new LauncherAccessibilityDelegate(launcher) : null;return mModel;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

注释1处会调用LauncherModel的initialize函数。

packages/apps/Launcher3/src/com/android/launcher3/LauncherModel.java

LauncherModel#initialize()

public void initialize(Callbacks callbacks) {synchronized (mLock) {unbindItemInfosAndClearQueuedBindRunnables();mCallbacks = new WeakReference<Callbacks>(callbacks);}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

在initialize函数中会将Callbacks,也就是传入的Launcher封装成一个弱引用对象。因此我们得知mCallbacks变量指的就是封装成弱引用对象的Launcher,这个mCallbacks后文会用到它。

再回到Launcher的onCreate函数,在注释3处调用了LauncherModel的startLoader函数:

LauncherModel#startLoader()

...@Thunk static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader");//1static {sWorkerThread.start();}@Thunk static final Handler sWorker = new Handler(sWorkerThread.getLooper());//2
...public void startLoader(int synchronousBindPage, int loadFlags) {InstallShortcutReceiver.enableInstallQueue();synchronized (mLock) {synchronized (mDeferredBindRunnables) {mDeferredBindRunnables.clear();}if (mCallbacks != null && mCallbacks.get() != null) {stopLoaderLocked();mLoaderTask = new LoaderTask(mApp.getContext(), loadFlags); //3if (synchronousBindPage != PagedView.INVALID_RESTORE_PAGE && mAllAppsLoaded && mWorkspaceLoaded && !mIsLoaderTaskRunning) {mLoaderTask.runBindSynchronousPage(synchronousBindPage);} else {sWorkerThread.setPriority(Thread.NORM_PRIORITY);sWorker.post(mLoaderTask);//4}}}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

注释1处创建了具有消息循环的线程HandlerThread对象。 
注释2处创建了Handler,并且传入HandlerThread的Looper。Hander的作用就是向HandlerThread发送消息。 
注释3处创建LoaderTask。 
注释4处将LoaderTask作为消息发送给HandlerThread 。LoaderTask类实现了Runnable接口。

LoaderTask

private class LoaderTask implements Runnable {...public void run() {synchronized (mLock) {if (mStopped) {return;}mIsLoaderTaskRunning = true;}keep_running: {if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace");loadAndBindWorkspace();//1if (mStopped) {break keep_running;}waitForIdle();if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps");loadAndBindAllApps();//2}mContext = null;synchronized (mLock) {if (mLoaderTask == this) {mLoaderTask = null;}mIsLoaderTaskRunning = false;mHasLoaderCompletedOnce = true;}}...     }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

Launcher是用工作区的形式来显示系统安装的应用程序的快捷图标,每一个工作区都是来描述一个抽象桌面的,它由n个屏幕组成,每个屏幕又分n个单元格,每个单元格用来显示一个应用程序的快捷图标。 
注释1处调用loadAndBindWorkspace函数用来加载工作区信息。 
注释2处的loadAndBindAllApps函数是用来加载系统已经安装的应用程序信息。

LauncherModel#loadAndBindAllApps()

private void loadAndBindAllApps() {if (DEBUG_LOADERS) {Log.d(TAG, "loadAndBindAllApps mAllAppsLoaded=" + mAllAppsLoaded);}if (!mAllAppsLoaded) {loadAllApps();//1synchronized (LoaderTask.this) {if (mStopped) {return;}}updateIconCache();synchronized (LoaderTask.this) {if (mStopped) {return;}mAllAppsLoaded = true;}} else {onlyBindAllApps();}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

如果系统没有加载已经安装的应用程序信息,则会调用注释1处的loadAllApps()函数。

LauncherModel#loadAllApps()

private void loadAllApps() {
...final List<LauncherActivityInfoCompat> apps = mLauncherApps.getActivityList(null, user);  //1// Fail if we don't have any apps// TODO: Fix this. Only fail for the current user.if (apps == null || apps.isEmpty()) {return;}// Create the ApplicationInfosfor (int i = 0; i < apps.size(); i++) {LauncherActivityInfoCompat app = apps.get(i);// This builds the icon bitmaps.mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache, quietMode));  //2}
...// Huh? Shouldn't this be inside the Runnable below?final ArrayList<AppInfo> added = mBgAllAppsList.added;mBgAllAppsList.added = new ArrayList<AppInfo>();mHandler.post(new Runnable() {public void run() {final long bindTime = SystemClock.uptimeMillis();final Callbacks callbacks = tryGetCallbacks(oldCallbacks);if (callbacks != null) {callbacks.bindAllApplications(added);  //3if (DEBUG_LOADERS) {Log.d(TAG, "bound " + added.size() + " apps in " + (SystemClock.uptimeMillis() - bindTime) + "ms");}} else {Log.i(TAG, "not binding apps: no Launcher activity");}}});...}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

注释1处获取所有已经安装的符合要求的Application信息。 
注释2中将Application信息封装成AppInfo并添加到mBgAllAppsList列表中。 
注释3处会调用callbacks的bindAllApplications函数并传入AppInfo列表,在前面我们得知这个callbacks实际是指向Launcher的,因此这里调用的是Launcher的bindAllApplications函数。

下面先看看注释1如何获取Application信息:

packages/apps/Launcher3/src/com/android/launcher3/compat/LauncherAppsCompatV16.java

LauncherAppsCompatV16#getActivityList()

    public List<LauncherActivityInfoCompat> getActivityList(String packageName, UserHandleCompat user) {//1final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);mainIntent.setPackage(packageName);List<ResolveInfo> infos = mPm.queryIntentActivities(mainIntent, 0); //2List<LauncherActivityInfoCompat> list =new ArrayList<LauncherActivityInfoCompat>(infos.size());for (ResolveInfo info : infos) {list.add(new LauncherActivityInfoCompatV16(mContext, info));}return list;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

注释1处构造带有ACTION_MAIN和CATEGORY_LAUNCHER的intent。 
注释2处通过PackageManagerService.queryIntentActivities接口来取回系统中所有符合intent条件的Activity,即需要显示到桌面上的应用。(前面启动PackageManagerService时,会把系统中的应用程序都解析一遍,然后把解析得到的Activity都保存在mActivities成员变量中,这里通过这个mActivities变量的queryIntent函数返回符合intent条件的Activity,即Action类型为Intent.ACTION_MAIN,并且Category类型为Intent.CATEGORY_LAUNCHER的Activity)

回退一步,继续来看Launcher的bindAllApplications函数:

Launcher#bindAllApplications()

public void bindAllApplications(final ArrayList<AppInfo> apps) {if (waitUntilResume(mBindAllApplicationsRunnable, true)) {mTmpAppsList = apps;return;}if (mAppsView != null) {mAppsView.setApps(apps); //1}if (mLauncherCallbacks != null) {mLauncherCallbacks.bindAllApplications(apps);}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

注释1处会调用AllAppsContainerView的setApps函数,并将包含应用信息的列表apps传进去。

packages/apps/Launcher3/src/com/android/launcher3/allapps/AllAppsContainerView.java

AllAppsContainerView#setApps()

public void setApps(List<AppInfo> apps) {mApps.setApps(apps);}
  • 1
  • 2
  • 3

包含应用信息的列表apps已经传给了AllAppsContainerView,查看AllAppsContainerView的onFinishInflate函数。

AllAppsContainerView#onFinishInflate()

@Overrideprotected void onFinishInflate() {super.onFinishInflate();
...// Load the all apps recycler viewmAppsRecyclerView = (AllAppsRecyclerView) findViewById(R.id.apps_list_view);//1mAppsRecyclerView.setApps(mApps);//2mAppsRecyclerView.setLayoutManager(mLayoutManager);mAppsRecyclerView.setAdapter(mAdapter);//3mAppsRecyclerView.setHasFixedSize(true);mAppsRecyclerView.addOnScrollListener(mElevationController);mAppsRecyclerView.setElevationController(mElevationController);
...}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

onFinishInflate函数在加载完xml文件时就会调用,注释1处得到AllAppsRecyclerView用来显示App列表。 
注释2处将apps的信息列表传进去。 
注释3处为AllAppsRecyclerView设置Adapter。到这里,应用程序快捷图标的列表就会显示在屏幕上了。

Android 系统(15)---Launcher启动过程相关推荐

  1. 从源码角度看Android系统SystemServer进程启动过程

    SystemServer进程是由Zygote进程fork生成,进程名为system_server,主要用于创建系统服务. 备注:本文将结合Android8.0的源码看SystemServer进程的启动 ...

  2. 从源码角度看Android系统Zygote进程启动过程

    在Android系统中,DVM.ART.应用程序进程和SystemServer进程都是由Zygote进程创建的,因此Zygote又称为"孵化器".它是通过fork的形式来创建应用程 ...

  3. 从源码角度看Android系统init进程启动过程

    init进程是Linux系统中用户空间的第一个进程,进程号为1.Kernel启动后,在用户空间启动init进程,并调用/system/core/init.cpp中的main方法执行一些重要的工作. 备 ...

  4. Android系统完整的启动流程

    Android系统完整的启动过程,从系统层次角度可分为 Linux 系统层.Android 系统服务层.Zygote进程模型三个阶段:从开机到启动 Home Launcher 完成具体的任务细节可分为 ...

  5. Android系统启动(四) — Launcher 启动过程

    1 Launcher 概述 系统启动的最后一步是启动一个应用程序来显示系统中已经安装的应用程序,这个应用程序就叫做 Launcher.Launcher 在启动过程中会请求 PackageManager ...

  6. Android应用程序进程启动过程

    相关文章 Android系统架构与系统源码目录 Android系统启动流程(一)解析init进程启动过程 Android系统启动流程(二)解析Zygote进程启动过程 Android系统启动流程(三) ...

  7. Android 系统服务 - PMS 的启动过程

    相关文章链接: 1. Android Framework - 学习启动篇 2. Android 系统服务 - PMS 的启动过程 相关源码文件: frameworks/base/services/co ...

  8. Android Launcher启动过程

    开机启动概述: step 1: 上电开机 长按power键后引导芯片开始从固化在ROM的预设代码处执行,加载引导程序(BootLoader)到RAM. step 2: BootLoader启动 跳转到 ...

  9. Android系统启动流程(四)Launcher启动过程与系统启动流程

    相关文章 Android系统架构与系统源码目录 Android系统启动流程(一)解析init进程启动过程 Android系统启动流程(二)解析Zygote进程启动过程 Android系统启动流程(三) ...

最新文章

  1. fork() || fork() || fork() 与 fork() fork() fork()
  2. 面试官:谈谈JWT鉴权的应用场景及使用建议?
  3. Windows7 IIS 500-内部服务器错误的解决方法
  4. Java的数据类型的挑选
  5. iOS应用内付费(IAP)开发步骤列表
  6. 小鹏汽车遭多地车主维权 要求退车 孙宇晨:个人拿1000万支持维权
  7. [c++primer][12]类
  8. atitit. 集合groupby 的实现(2)---自定义linq查询--java .net php
  9. Lightroom Classic CC 2019 for Mac(lr cc 2019 mac)
  10. 全面解读人工智能、大数据和云计算的关系
  11. web开发中移动端适配
  12. 模拟复杂红绿灯交通指示程序编程显示黄灯闪烁箭头指示
  13. 一个老程序员写给换行业的朋友的信
  14. 星际战魂java_星际战甲 专精聚魂选择推荐
  15. python dataframe dropna_在Python中使用熊猫在两个DataFrame之间进行值...
  16. 团 队 作 业 ———— 随 堂 小 测
  17. VR全景打造数字化校园名片
  18. Java程序员的薪资取决于年限还是技术?
  19. ImageMagic for win
  20. 关于IntelliJ IDEA解决文件带BOM的问题

热门文章

  1. Java集合框架之Collection实例解析
  2. Android适配底部返回键等虚拟键盘的完美解决方案
  3. Android嵌入式安卓触摸屏|4418开发板平台
  4. Eclipse中activiti插件的安装
  5. C:\Windows\Installer越来越大?让我们一起来删除msp这个毒瘤
  6. xcode6的项目中虚拟键盘无法弹出
  7. 滴水穿石--mysql添加授权用户命令
  8. .net remoting与web service的区别
  9. 投資效益計算公式4/10
  10. 如何形象的解释javascript中map,foreach,reduce的区别