本文出自兇殘的程式員的部落格
連結:https://blog.csdn.net/qian520ao/article/details/81908505
目錄:
1、應用啟動速度
2、視覺最佳化
2.1啟動主題最佳化
預設情況
透明主題最佳化
設定閃屏圖片主題
3、程式碼最佳化
3.1冷啟動耗時統計
adb 命令統計
系統日誌統計
3.2程式碼最佳化
Application 最佳化
閃屏頁業務最佳化
廣告頁最佳化
4、最佳化效果
5、啟動視窗
6、總結
1、應用啟動速度
一個應用App的啟動速度能夠影響使用者的首次體驗,啟動速度較慢(感官上)的應用可能導致用戶再次開啟App的意圖下降,或者解除安裝放棄該應用程式。
本文將從兩個方向最佳化應用的啟動速度 :
-
視覺體驗最佳化
-
程式碼邏輯最佳化
2、視覺最佳化
谷歌開發檔案:
https://developer.android.com/topic/performance/vitals/launch-time
應用程式啟動有三種狀態,每種狀態都會影響應用程式對使用者可見所需的時間:冷啟動,熱啟動和溫啟動。
在冷啟動時,應用程式從頭開始。在其他狀態下,系統需要將正在執行的應用程式從後臺執行到前臺。我們建議您始終根據冷啟動的假設進行最佳化。這樣做也可以改善熱啟動和溫啟動的效能。
在冷啟動開始時,系統有三個任務。這些任務是:
1、載入並啟動應用程式。
2、啟動後立即顯示應用程式空白的啟動視窗。
3、建立應用程式行程。
一旦系統建立應用程式行程,應用程式行程就會負責下一階段。
這些階段是:
1、建立app物件.
2、啟動主執行緒(main thread).
3、建立應用入口的Activity物件.
4、填充載入佈局Views
5在螢幕上執行View的繪製過程.measure -> layout -> draw
應用程式行程完成第一次繪製後,系統行程會交換當前顯示的背景視窗,將其替換為主活動。此時,使用者可以開始使用該應用程式。
因為App應用行程的建立過程是由手機的軟硬體決定的,所以我們只能在這個建立過程中視覺最佳化。
啟動主題最佳化
冷啟動階段 :
1、載入並啟動應用程式。
2、啟動後立即顯示應用程式空白的啟動視窗。
3、建立應用程式行程。
所謂的主題最佳化,就是應用程式在冷啟動的時候(1~2階段),設定啟動視窗的主題。
因為現在 App 應用啟動都會先進入一個閃屏頁(LaunchActivity) 來展示應用資訊。
1、預設情況
如果我們對App沒有做處理(設定了預設主題),並且在 Application 初始化了其它第三方的服務(假設需要載入2000ms),那麼冷啟動過程就會如下圖 :系統預設會在啟動應用程式的時候 啟動空白視窗 ,直到 App 應用程式的入口 Activity 建立成功,檢視繪製完畢。( 大概是onWindowFocusChanged方法回呼的時候 )
2、透明主題最佳化
為瞭解決啟動視窗白屏問題,許多開發者使用透明主題來解決這個問題,但是治標不治本。
雖然解決了上面這個問題,但是仍然有些不足。
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowFullscreen">trueitem>
<item name="android:windowIsTranslucent">trueitem>
style>
(無白屏,不過從點選到App仍然存在視覺延遲~)
3、設定閃屏圖片主題
為了更順滑無縫銜接我們的閃屏頁,可以在啟動 Activity 的 Theme中設定閃屏頁圖片,這樣啟動視窗的圖片就會是閃屏頁圖片,而不是白屏。
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowBackground">@mipmap/launchitem> //閃屏頁圖片
<item name="android:windowFullscreen">trueitem>
<item name=“android:windowContentOverlay”>@nullitem>
style>
這樣設定的話,就會在冷啟動的時候,展示閃屏頁的圖片,等App行程初始化載入入口 Activity (也是閃屏頁) 就可以無縫銜接。
其實這種方式並沒有真正的加速應用行程的啟動速度,而只是透過使用者視覺效果帶來的最佳化體驗。
3、程式碼最佳化
當然上面使用設定主題的方式最佳化使用者體驗效果治標不治本,關鍵還在於對程式碼的最佳化。
首先我們可以統計一下應用冷啟動的時間。
冷啟動耗時統計
adb 命令統計
參考如何計算 App 的啟動時間
http://www.androidperformance.com/2015/12/31/How-to-calculation-android-app-lunch-time/
adb命令 : adb shell am start -S -W 包名/啟動類的全限定名 , -S 表示重啟當前應用
更多adb命令
https://github.com/mzlogin/awesome-adb
C:AndroidDemo>adb shell am start -S -W com.example.moneyqian.demo/com.example.moneyqian.demo.MainActivity
Stopping: com.example.moneyqian.demo
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.moneyqian.demo/.MainActivity }
Status: ok
Activity: com.example.moneyqian.demo/.MainActivity
ThisTime: 2247
TotalTime: 2247
WaitTime: 2278
Complete
1、ThisTime最後一個 Activity 的啟動耗時(例如從 LaunchActivity – >MainActivity「adb命令輸入的Activity」 , 只統計 MainActivity 的啟動耗時)
2、TotalTime:啟動一連串的 Activity 總耗時.(有幾個Activity 就統計幾個)
3、WaitTime:應用行程的建立過程 + TotalTime .
在第①個時間段內,AMS 建立 ActivityRecord 記錄塊和選擇合理的 Task、將當前Resume 的 Activity 進行 pause.
在第②個時間段內,啟動行程、呼叫無介面 Activity 的 onCreate() 等、 pause/finish 無介面的 Activity.
在第③個時間段內,呼叫有介面 Activity 的 onCreate、onResume.
//ActivityRecord
private void reportLaunchTimeLocked(final long curTime) {
``````
final long thisTime = curTime - displayStartTime;
final long totalTime = stack.mLaunchStartTime != 0 ?
(curTime - stack.mLaunchStartTime) : thisTime;
}
最後總結一下 : 如果需要統計從點選桌面圖示到 Activity 啟動完畢,可以用WaitTime作為標準,但是系統的啟動時間最佳化不了,所以最佳化冷啟動我們只要在意 ThisTime 即可。
系統日誌統計
另外也可以根據系統日誌來統計啟動耗時,在Android Studio中查詢已用時間,必須在logcat檢視中禁用過濾器(No Filters)。因為這個是系統的日誌輸出,而不是應用程式的。你也可以檢視其它應用程式的啟動耗時。
過濾displayed輸出的啟動日誌.
程式碼最佳化
根據上面啟動時間的輸出統計,我們就可以先記錄最佳化前的冷啟動耗時,然後再對比最佳化之後的啟動時間。
Application 最佳化
Application 作為 應用程式的整個初始化配置入口,時常擔負著它不應該有的負擔~
有很多第三方元件(包括App應用本身)都在 Application 中搶佔先機,完成初始化操作。
但是在 Application 中完成繁重的初始化操作和複雜的邏輯就會影響到應用的啟動效能通常,有機會最佳化這些工作以實現效能改進,這些常見問題包括:
-
複雜繁瑣的佈局初始化
-
阻塞主執行緒 UI 繪製的操作,如 I/O 讀寫或者是網路訪問.
-
Bitmap 大圖片或者 VectorDrawable載入
-
其它佔用主執行緒的操作
我們可以根據這些元件的輕重緩急之分,對初始化做一下分類 :
1、必要的元件一定要在主執行緒中立即初始化(入口 Activity 可能立即會用到)
2、元件一定要在主執行緒中初始化,但是可以延遲初始化。
3、元件可以在子執行緒中初始化。
放在子執行緒的元件初始化建議延遲初始化 ,這樣就可以瞭解是否會對專案造成影響!
所以對於上面的分析,我們可以在專案中 Application 的載入元件進行如下最佳化 :
將Bugly,x5核心初始化,SP的讀寫,友盟等元件放到子執行緒中初始化。(子執行緒初始化不能影響到元件的使用)
new Thread(new Runnable() {
@Override
public void run() {
//設定執行緒的優先順序,不與主執行緒搶資源
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//子執行緒初始化第三方元件
Thread.sleep(5000);//建議延遲初始化,可以發現是否影響其它功能,或者是崩潰!
}
}).start();
將需要在主執行緒中初始化但是可以不用立即完成的動作延遲載入(原本是想在入口 Activity 中進行此項操作,不過元件的初始化放在 Application 中統一管理為妙.)
handler.postDelayed(new Runnable() {
@Override
public void run() {
//延遲初始化元件
}
}, 3000);
閃屏頁業務最佳化
最後還剩下那些為數不多的元件在主執行緒初始化動作,例如埋點,點選流,資料庫初始化等,不過這些消耗的時間可以在其它地方相抵。
需求背景 : 應用App通常會設定一個固定的閃屏頁展示時間,例如2000ms,所以我們可以根據使用者手機的執行速度,對展示時間做出調整,但是總時間仍然為 2000ms。
閃屏頁政展示總時間 = 元件初始化時間 + 剩餘展示時間。
也就是2000ms的總時間,元件初始化了800ms,那麼就再展示1200ms即可。
我們先瞭解一下 Application的啟動過程,圖片摘自 :
如何統計Android App啟動時間
https://www.jianshu.com/p/59a2ca7df681
雖然這個以下圖片的原始碼並不是最新原始碼(5.0原始碼),不過不影響整體流程。(7.0,8.0方法名會有所改變)。
冷啟動的過程中系統會初始化應用程式行程,建立Application等任務,這時候會展示一個 啟動視窗 Starting Window,上面分析了過,如果沒有最佳化主題的話,那麼就是白屏。
如果要瞭解更多啟動過程原始碼,可以看我的部落格 :
Launcher 啟動 Activity 的工作過程
https://blog.csdn.net/qian520ao/article/details/78156214
分析原始碼後,我們可以知道 Application 初始化後會呼叫 attachBaseContext() 方法,再呼叫 Application 的 onCreate(),再到入口 Activity的建立和執行 onCreate() 方法。所以我們就可以在 Application 中記錄啟動時間。
//Application
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
SPUtil.putLong("application_attach_time",
System.currentTimeMillis());//記錄Application初始化時間
}
有了啟動時間,我們得知道入口的 Acitivty 顯示給使用者的時間(View繪製完畢),在部落格( View的工作流程)中瞭解到,在onWindowFocusChanged()的回呼時機中表示可以獲取使用者的觸控時間和View的流程繪製完畢,所以我們可以在這個方法裡記錄顯示時間。
https://blog.csdn.net/qian520ao/article/details/78657084
//入口Activity
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
long appAttachTime = SPUtil.getLong("application_attach_time");
long diffTime = System.currentTimeMillis() - appAttachTime;//從application到入口Acitity的時間
//所以閃屏頁展示的時間為 2000ms - diffTime.
}
所以我們就可以動態的設定應用閃屏的顯示時間,儘量讓每一部手機展示的時間一致,這樣就不會讓手機配置較低的使用者感覺漫長難熬的閃屏頁時間(例如初始化了2000ms,又要展示2000ms的閃屏頁時間.),最佳化使用者體驗。
廣告頁最佳化
閃屏頁過後就要展示金主爸爸們的廣告頁了。
因為專案中廣告頁圖片有可能是大圖,APng動態圖片,所以需要將這些圖片下載到本地檔案,下載完成後再顯示,這個過程往往會遇到以下兩個問題 :
1、廣告頁的下載,由於這個是一個非同步過程,所以往往不知道載入到頁面的合適時機。
2、廣告頁的儲存,因為儲存是 I/O 流操作,很有可能被使用者中斷,下次拿到破損的圖片。
因為不清楚使用者的網路環境,有些使用者下載廣告頁可能需要一段時間,這時候又不可能無限的等候。所以針對這個問題我們可以開啟 IntentService 用來下載廣告頁圖片。
1、在入口 Acitivity 中開啟 IntentService 來下載廣告頁。 或者是其它非同步下載操作。
2、在廣告頁圖片 檔案流完全寫入後 記錄圖片大小,或者記錄一個標識。
在下次的廣告頁載入中可以判斷是否已經下載好了廣告頁圖片以及圖片是否完整,否則刪除並且再次下載圖片。
另外因為在閃屏頁中仍然有 剩餘展示時間,所以在這個時間段裡如果使用者已經下載好了圖片並且圖片完整,就可以顯示廣告頁。否則進入主 Activity , 因為 IntentService 仍然在後臺繼續默默的下載並儲存圖片~
4、最佳化效果
最佳化前 : (小米6)
Displayed | LaunchActivity | MainActivity |
---|---|---|
+2s526ms | +1s583ms | |
+2s603ms | +1s533ms | |
+2s372ms | +1s556ms |
最佳化後 : (小米6)
Displayed | LaunchActivity | MainActivity |
---|---|---|
+995ms | +1s191ms | |
+911ms | +1s101ms | |
+903ms | +1s187ms |
透過手上 小米6,小米 mix2s,還有小米 2s的啟動測試,發現最佳化後App冷啟動的啟動速度均提升了60% !!! ,並且我們可以再看一下手機冷啟動時候的記憶體情況 :
最佳化前 : 伴隨著大量物件的建立回收,15s內系統GC 5次。
記憶體使用波瀾蕩漾。
最佳化後 : 趨於平穩上升狀態建立物件,15s內系統GC 2次。(後期業務拓展加入新功能,所以程式碼量增加。)之後總記憶體使用平緩下降。
Other :應用使用的系統不確定如何分類的記憶體。
Code :應用用於處理程式碼和資源(如 dex 位元組碼、已最佳化或已編譯的 dex 碼、.so 庫和字型)的記憶體。
Stack : 應用中的原生堆疊和 Java 堆疊使用的記憶體。 這通常與您的應用執行多少執行緒有關。
Graphics :圖形緩衝區佇列向螢幕顯示畫素(包括 GL 錶面、GL 紋理等等)所使用的記憶體。 (請註意,這是與 CPU 共享的記憶體,不是 GPU 專用記憶體。)
Native :從 C 或 C++ 程式碼分配的物件記憶體。即使應用中不使用 C++,也可能會看到此處使用的一些原生記憶體,因為 Android 框架使用原生記憶體代表處理各種任務,如處理影象資源和其他圖形時,即使編寫的程式碼採用 Java 或 Kotlin 語言。
Java :從 Java 或 Kotlin 程式碼分配的物件記憶體。
Allocated :應用分配的 Java/Kotlin 物件數。 它沒有計入 C 或 C++ 中分配的物件。
更多檢視 :
https://developer.android.google.cn/studio/profile/memory-profiler?hl=zh-cn
5、啟動視窗
最佳化完我們的程式碼後,分析一下啟動視窗的原始碼。基於 android-25 (7.1.1)
啟動視窗是由 WindowManagerService 統一管理的 Window視窗,一般作為冷啟動頁入口 Activity 的預覽視窗,啟動視窗由 ActivityManagerService 來決定是否顯示的,並不是每一個 Activity 的啟動和跳轉都會顯示這個視窗。
WindowManagerService 透過視窗管理策略類 PhoneWindowManager 來建立啟動視窗。
圖片摘自 老羅的原始碼分析
拿我之前原始碼分析的文章中的啟動流程圖來看看大致 :
Launcher 啟動 Activity 的工作過程
https://blog.csdn.net/qian520ao/article/details/78156214
直奔主題,在 ActivityStarter的startActivityUnchecked()方法中,呼叫了ActivityStack(Activity 狀態管理)的startActivityLocked()方法。此時Activity 還在啟動過程中,視窗並未顯示。
先上一張流程圖,展示了啟動視窗的顯示過程。
首先,由 Activity 狀態管理者ActivityStack開始執行顯示啟動視窗的流程。
//ActivityStack
final void startActivityLocked(ActivityRecord r, boolean newTask, boolean keepCurTransition,
ActivityOptions options) {
``````
if (!isHomeStack() || numActivities() > 0) {//HOME_STACK表示Launcher桌面所在的Stack
// 1.首先當前啟動棧不在Launcher的桌面棧裡,並且當前系統已經有啟用過Activity
boolean doShow = true;
if (newTask) {
// 2.要將該Activity元件放在一個新的任務棧中啟動
if ((r.intent.getFlags() & Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) != 0) {
resetTaskIfNeededLocked(r, r);
doShow = topRunningNonDelayedActivityLocked(null) == r;
}
} else if (options != null && options.getAnimationType()
== ActivityOptions.ANIM_SCENE_TRANSITION) {
doShow = false;
}
if (r.mLaunchTaskBehind) {
//3. 熱啟動,不需要啟動視窗
mWindowManager.setAppVisibility(r.appToken, true);
ensureActivitiesVisibleLocked(null, 0, !PRESERVE_WINDOWS);
} else if (SHOW_APP_STARTING_PREVIEW && doShow) {
``````
//4. 顯示啟動視窗
r.showStartingWindow(prev, showStartingIcon);
}
} else {
// 當前啟動的是桌面Launcher (開機啟動)
// If this is the first activity, don't do any fancy animations,
// because there is nothing for it to animate on top of.
``````
}
}
首先判斷當前要啟動的 Activity 不在Launcher棧裡
要啟動的 Activity 是否處於新的 Task 裡,並且沒有轉場動畫
如果是熱/溫啟動則不需要啟動視窗,直接設定App的Visibility
接下來呼叫ActivityRecord的showStartingWindow()方法來設定啟動視窗並且改變當前視窗的狀態。
如果 App 的應用行程建立完成,並且入口 Activity 準備就緒,就可以根據 mStartingWindowState 來判斷是否需要關閉啟動視窗。
//ActivityRecord
void showStartingWindow(ActivityRecord prev, boolean createIfNeeded) {
final CompatibilityInfo compatInfo =
service.compatibilityInfoForPackageLocked(info.applicationInfo);
final boolean shown = service.mWindowManager.setAppStartingWindow(
appToken, packageName, theme, compatInfo, nonLocalizedLabel, labelRes, icon,
logo, windowFlags, prev != null ? prev.appToken : null, createIfNeeded);
if (shown) {
mStartingWindowState = STARTING_WINDOW_SHOWN;
}
}
WindowManagerService 會對當前 Activity 的token和主題進行判斷。
//WindowManagerService
@Override
public boolean setAppStartingWindow(IBinder token, String pkg,
int theme, CompatibilityInfo compatInfo,
CharSequence nonLocalizedLabel, int labelRes, int icon, int logo,
int windowFlags, IBinder transferFrom, boolean createIfNeeded) {
synchronized(mWindowMap) {
//1. 啟動視窗也是需要token的
AppWindowToken wtoken = findAppWindowToken(token);
//2. 如果已經設定過啟動視窗了,不繼續處理
if (wtoken.startingData != null) {
return false;
}
if (theme != 0) {
AttributeCache.Entry ent = AttributeCache.instance().get(pkg, theme,
com.android.internal.R.styleable.Window, mCurrentUserId);
//3. 一堆程式碼對主題判斷,不符合要求則不顯示啟動視窗(如透明主題)
if (windowIsTranslucent) {
return false;
}
if (windowIsFloating || windowDisableStarting) {
return false;
}
``````
}
//4. 建立StartingData,並且透過Handler傳送訊息
wtoken.startingData = new StartingData(pkg, theme, compatInfo, nonLocalizedLabel,
labelRes, icon, logo, windowFlags);
Message m = mH.obtainMessage(H.ADD_STARTING, wtoken);
mH.sendMessageAtFrontOfQueue(m);
}
return true;
}
啟動視窗也需要和 Activity 擁有同樣令牌 token ,雖然啟動視窗可能是白屏,或者一張圖片,但是仍然需要走繪製流程已經透過WMS顯示視窗。
StartingData物件用來表示啟動視窗的相關資料,描述了啟動視窗的檢視資訊。
如果當前 Activity 是透明主題或者是浮動視窗等,那麼就不需要啟動視窗來過渡啟動過程,所以在上面視覺最佳化中的設定透明主題就沒有顯示白色的啟動視窗。
顯示啟動視窗也是一件心急火燎的事情,WMS的內部類H (handler) 處於主執行緒處理訊息,所以需要將當前Message放置佇列頭部。
PS : 為什麼需要透過 Handler 傳送訊息 ?
你可以在各大服務Service中見到 Handler 的身影,並且它們可能都有一個很弔的命名 H ,因為可能呼叫這個服務的某個執行方法處於子執行緒中,所以 Handler 的職責就是將它們切換到主執行緒中,並且也可以統一管理排程。
更多 Handler 瞭解可以查閱文章 :
你真的瞭解Handler?
https://blog.csdn.net/qian520ao/article/details/78262289
//WindowManagerService --> H
public void handleMessage(Message msg) {
switch (msg.what) {
case ADD_STARTING: {
final AppWindowToken wtoken = (AppWindowToken)msg.obj;
final StartingData sd = wtoken.startingData;
View view = null;
try {
final Configuration overrideConfig = wtoken != null && wtoken.mTask != null
? wtoken.mTask.mOverrideConfig : null;
view = mPolicy.addStartingWindow(wtoken.token, sd.pkg, sd.theme,
sd.compatInfo, sd.nonLocalizedLabel, sd.labelRes, sd.icon, sd.logo,
sd.windowFlags, overrideConfig);
} catch (Exception e) {
Slog.w(TAG_WM, "Exception when adding starting window", e);
}
``````
} break;
}
在當前的handleMessage方法中,會處於主執行緒處理訊息,拿到token和StartingData啟動資料後,便透過mPolicy.addStartingWindow()方法將啟動視窗新增到WIndow上。
mPolicy為PhoneWindowManager,控制著啟動視窗的新增刪除和修改。
在PhoneWindowManager對啟動視窗進行配置,獲取當前Activity設定的主題和資源資訊,設定到啟動視窗中。
//PhoneWindowManager
@Override
public View addStartingWindow(IBinder appToken, String packageName, int theme,
CompatibilityInfo compatInfo, CharSequence nonLocalizedLabel, int labelRes,
int icon, int logo, int windowFlags, Configuration overrideConfig) {
//可以透過SHOW_STARTING_ANIMATIONS設定不顯示啟動視窗
if (!SHOW_STARTING_ANIMATIONS) {
return null;
}
WindowManager wm = null;
View view = null;
//1. 獲取背景關係Context和主題theme以及標題
Context context = mContext;
if (theme != context.getThemeResId() || labelRes != 0) {
try {
context = context.createPackageContext(packageName, 0);
context.setTheme(theme);
} catch (PackageManager.NameNotFoundException e) {
// Ignore
}
}
//2. 建立PhoneWindow 用來顯示
final PhoneWindow win = new PhoneWindow(context);
win.setIsStartingWindow(true);
//3. 設定當前視窗type和flag,原始碼註釋中描述的很清晰...
win.setType(
WindowManager.LayoutParams.TYPE_APPLICATION_STARTING);
win.setFlags(...);
``````
view = win.getDecorView();
//4. WindowManager的繪製流程
wm.addView(view, params);
return view.getParent() != null ? view : null;
}
如果theme和labelRes的值不為0,那麼說明開發者指定了啟動視窗的主題和標題,那麼就需要從當前要啟動的Activity中獲取這些資訊,並設定到啟動視窗中。
和其它視窗一樣,啟動視窗也需要透過PhoneWindow來設定佈局資訊DecorView。所以在上面視覺最佳化中的設定閃屏圖片主題的啟動視窗顯示的就是圖片內容。
啟動視窗和普通視窗的不同之處在於它是 fake window ,不需要觸控事件
最後透過WindowManger走View的繪製流程(measure-layout-draw)將啟動視窗顯示出來,最後會請求WindowManagerService為啟動視窗新增一個WindowState物件,真正的將啟動視窗顯示給使用者,並且可以對啟動視窗進行管理。
更多WindowManager的addView流程可以查閱 :
View的工作流程
https://blog.csdn.net/qian520ao/article/details/7865708
6、總結
至此應用程式的啟動最佳化和啟動視窗的原始碼分析已經總結完畢,在專案的開發中要知其然而之所以然 ,並且對原始碼的分析有助於我們瞭解原理和解決問題的根源。
●編號350,輸入編號直達本文
●輸入m獲取到文章目錄
Java程式設計
更多推薦《18個技術類公眾微信》
涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。