很少有人會告訴你的 Android 開發基本常識

張明雲發表於2015-10-25

本文介紹Android開發過程中的一些基本常識,大多是一些流程、專業術語和解決問題的方法等。

軟體開發流程

一個完整的軟體開發流程離不開策劃、互動、視覺、軟體、測試、維護和運營這七個環節,這七個環節並不是孤立的,它們是開發一款成功產品的前提,但每一項也都可以形成一個學科,是一個獨立的崗位,隨著敏捷開發的流行,以及來到了體驗為王的時代,現代軟體開發更多的是注重效率和敏捷,而不是循規蹈矩的遵循這些開發流程,比如軟體開發的崗位不再僅僅是個技術崗位,它需要去參與前期的設計和評審、可以在視覺和互動方面提出自己的見解,在開發的過程中需要自測程式儘快解決現存問題,運營和維護的過程中也需要軟體的幫助。可見現代軟體開發對開發者的綜合素質(這並不是facebook所講的全棧工程師)越來越高,自稱為碼農或者程式猿顯然是不合理的,因為這個過程是腦力勞動和體力腦動並存,稱呼自己為工程師顯得更為合理。

  • 策劃:需求收集(通過使用者調研、灰度釋出、大資料分析、競品分析、領導拍腦袋等方式獲取需求)、需求整理(將需求歸類、劃分優先順序等)、將需求轉換成解決方案(輸出設計文件);
  • 互動:從心理學(利用人性的弱點)、人性化(心智)、個性化的角度將解決方案轉換成可互動的功能和介面(需要輸出互動文件),比如載入等待、訊息提示、頁面佈局、頁面內和頁面間的互動邏輯、頁面切換動畫等等,這個過程中一般會使用Axure或者PowerPoint來製作互動文件;
  • 視覺:根據互動圖,使用PhotoShop來做視覺效果,在Android上的圖片格式大多是png和jpg,對於需要螢幕適配,程式又適合做螢幕適配的地方可以使用九圖,格式為*.9.png。
  • 軟體:根據視覺和互動效果將需求轉化為具體的實現,在實現的過程中可能會因為需求、互動或者視覺的變動導致軟體實現的變動,因為策劃、互動、視覺這每一個環節都可能會有資訊失真的現象,或者是由於市場環境的變化、獲取資訊不夠準確、領導拍腦袋等等情況導致軟體始終處於被動狀態,所以現在會提倡敏捷開發、結對程式設計、程式設計、同行評審、單元測試來提高程式的靈活性和穩定性;
  • 測試:軟體達到可互動的標準後,需要將可互動的程式提供測試,其中灰度釋出(使用者測試)、自測(開發自測)、SQA(品質保證)都算是測試;
  • 維護和運營:通過測試程式達到穩定標準後,軟體就可以上線了,軟體上線後,需要去維護,使用者反饋的問題要及時解決、使用者有疑問要及時解答;根據後臺統計資訊、抓住可運營的節日、民族文化需要做運營來提高使用者使用產品的粘度,讓更多的使用者知道、使用產品都是運營應該做的。

注:

提問的智慧

大多數工作都是以結果為導向的,特別是軟體開發這個職業,績效考核、KPI這些都是在考核你工作的成果,所以工作更多地是需要你解決問題的能力,至於學習這個事情,還是在工作之外的時間去做吧。對於提高解決問題能力我有兩個建議:

  • 學會學習和思考:學習的過程中要廣度和深度並存,Android應用開發本身對技術功底的要求不高(因為很多底層的東西都被google、框架、開原始碼給封裝起來了,多數時候你只需要看ReadMe或者API知道怎麼用就可以了),更多地是在你遇到問題的時候知道這個問題能夠通過什麼方法和方式來解決。書要看,但多逛逛論壇、QQ群、Github、StackOverflow、CSDN部落格專欄對自己都是有益的。
  • 學會提問:你身邊有很多資源,比如同事、StackOverflow、QQ技術交流群、搜尋引擎,當你遇到問題的時候完全可以利用身邊的資源來解決遇到的問題,如果一個問題在一個小時之內自己都不能夠解決它,我就會通過搜尋引擎、Github、QQ技術交流群、同事、StackOverflow(以上排序是按優先順序排列的)來解決它。如果你需要好的答案你就需要有好的提問,特別是在QQ群或者論壇,在提問的過程中需要體現出你的思考,能夠通過搜尋引擎解決的問題堅決不問他人,這是對別人的尊重,在這裡推薦幾個連結,認真看會對你有莫大的幫助:

如何用好 Google 等搜尋引擎?

程式設計師應該如何提問?

提問的智慧

Smart Questions

解決bug的方法

為了寫這一項我專門在知乎上提過一個問題:

你有哪些解決bug的技巧?

在知道如何快速解決bug之前,你需要知道什麼是bug。沒有完成策劃、互動、視覺要求的功能,這不叫bug,這叫功能缺陷;一個功能完成後不能正常使用也不叫bug,因為它根本還沒達到可測試的標準。我認為當你的程式達到可測試標準之後發現的問題才叫bug。綜合我自己解決bug的經驗和知乎上的回答,總結常見的解決bug的方法有(你想要高效解決bug的前提是你能夠快速定位到缺陷所在的位置,所以以下方法多數講的是如何快速定位問題,至於真正解決bug,需要你自己修改程式才行):

  • 斷點除錯:

以Eclipse為例:

1、打斷點:

(1)打斷點:

很少有人會告訴你的Android開發基本常識

打斷點

(2)清除斷點:

很少有人會告訴你的Android開發基本常識

清除斷點

2、啟動除錯模式的兩種方式:

(1)通過debug as啟動除錯程式:右鍵工程名–>Debug AS –>Android Application –>模擬器或者真機會彈出……watching for the debugger……的提示框,不要點選等待其自動消失 –> 此時已經進入除錯模式,操作程式到達打斷點的地方。

(2)在程式執行過程中,在DDMS檢視下選中要除錯的程式,啟動除錯模式:

很少有人會告訴你的Android開發基本常識

DDMS檢視進入除錯模式

3、除錯:請自行嘗試F5、F6、F7、F8這幾個除錯的快捷鍵;

4、watch成員變數:在除錯的過程中,比如在執行for、while、do while迴圈、遞迴、系統回撥等程式時可以通過watch來觀察成員變數或者方法返回值的變化情況,watch的方法:

很少有人會告訴你的Android開發基本常識

watch

注:更多關於在Eclipse IDE中除錯Android程式的知識請參見:Android eclipse中程式除錯

列印:

列印除錯的方法對於迴圈、非同步載入、遞迴、JNI等程式碼段非常有用,特別是在迴圈中,在迴圈次數非常大時,通過打斷點除錯顯然是一件費力的事情,這時候列印就顯得更“智慧”了,我通常會通過下面封裝的列印除錯類來輸出列印資訊,這個類可以列印print、log、行號、檔名、StrictMode等資訊,當不需要列印資訊時,只需要將DEBUG_MODE改為false就可以了:

import android.content.Context;
    import android.os.StrictMode;
    import android.util.Log;
    import android.widget.Toast;

    /**
     * 除錯列印類
     * 
     * */
    public class DebugUtils{
        private DebugUtils( ){

        }

        public static void println( String printInfo ){
            if( Debug.DEBUG_MODE && null != printInfo ){
                System.out.println( printInfo );
            }
        }

        public static void print( String printInfo ){
            if( Debug.DEBUG_MODE && null != printInfo ){
                System.out.print( printInfo );
            }
        }

        public static void printLogI( String logInfo ){
            printLogI( TAG, logInfo );
        }

        public static void printLogI( String tag, String logInfo ){
            if( Debug.DEBUG_MODE && null != tag && null != logInfo ){
                Log.i( tag, logInfo );
            }
        }

        public static void printLogE( String logInfo ){
            printLogE( TAG, logInfo );
        }

        public static void printLogE( String tag, String logInfo ){
            if( Debug.DEBUG_MODE && null != tag && null != logInfo ){
                Log.e( tag, logInfo );
            }
        }

        public static void printLogW( String logInfo ){
            printLogW( TAG, logInfo );
        }

        public static void printLogW( String tag, String logInfo ){
            if( Debug.DEBUG_MODE && null != tag && null != logInfo ){
                Log.w( tag, logInfo );
            }
        }

        public static void printLogD( String logInfo ){
            printLogD( TAG, logInfo );
        }

        public static void printLogD( String tag, String logInfo ){
            if( Debug.DEBUG_MODE && null != tag && null != logInfo ){
                Log.d( tag, logInfo );
            }
        }

        public static void printLogV( String logInfo ){
            printLogV( TAG, logInfo );
        }

        public static void printLogV( String tag, String logInfo ){
            if( Debug.DEBUG_MODE && null != tag || null != logInfo ){
                Log.v( tag, logInfo );
            }
        }

        public static void printLogWtf( String logInfo ){
            printLogWtf( TAG, logInfo );
        }

        public static void printLogWtf( String tag, String logInfo ){
            if( Debug.DEBUG_MODE && null != tag && null != logInfo ){
                Log.wtf( tag, logInfo );
            }
        }

        public static void showToast( Context context, String toastInfo ){
            if( null != context && null != toastInfo ){
                Toast.makeText( context, toastInfo, Toast.LENGTH_LONG ).show( );
            }
        }

        public static void showToast( Context context, String toastInfo, int timeLen ){
            if( null != context && null != toastInfo && ( timeLen > 0 ) ){
                Toast.makeText( context, toastInfo, timeLen ).show( );
            }
        }

        public static void printBaseInfo( ){
            if( Debug.DEBUG_MODE ){
                StringBuffer strBuffer = new StringBuffer( );
                StackTraceElement[ ] stackTrace = new Throwable( ).getStackTrace( );

                strBuffer.append( "; class:" ).append( stackTrace[ 1 ].getClassName( ) )
                        .append( "; method:" ).append( stackTrace[ 1 ].getMethodName( ) )
                        .append( "; number:" ).append( stackTrace[ 1 ].getLineNumber( ) )
                        .append( "; fileName:" ).append( stackTrace[ 1 ].getFileName( ) );

                println( strBuffer.toString( ) );
            }
        }

        public static void printFileNameAndLinerNumber( ){
            if( Debug.DEBUG_MODE ){
                StringBuffer strBuffer = new StringBuffer( );
                StackTraceElement[ ] stackTrace = new Throwable( ).getStackTrace( );

                strBuffer.append( "; fileName:" ).append( stackTrace[ 1 ].getFileName( ) )
                        .append( "; number:" ).append( stackTrace[ 1 ].getLineNumber( ) );

                println( strBuffer.toString( ) );
            }
        }

        public static int printLineNumber( ){
            if( Debug.DEBUG_MODE ){
                StringBuffer strBuffer = new StringBuffer( );
                StackTraceElement[ ] stackTrace = new Throwable( ).getStackTrace( );

                strBuffer.append( "; number:" ).append( stackTrace[ 1 ].getLineNumber( ) );

                println( strBuffer.toString( ) );
                return stackTrace[ 1 ].getLineNumber( );
            }else{
                return 0;
            }
        }

        public static void printMethod( ){
            if( Debug.DEBUG_MODE ){
                StringBuffer strBuffer = new StringBuffer( );
                StackTraceElement[ ] stackTrace = new Throwable( ).getStackTrace( );

                strBuffer.append( "; number:" ).append( stackTrace[ 1 ].getMethodName( ) );

                println( strBuffer.toString( ) );
            }
        }

        public static void printFileNameAndLinerNumber( String printInfo ){
            if( null == printInfo || !Debug.DEBUG_MODE ){
                return;
            }
            StringBuffer strBuffer = new StringBuffer( );
            StackTraceElement[ ] stackTrace = new Throwable( ).getStackTrace( );

            strBuffer.append( "; fileName:" ).append( stackTrace[ 1 ].getFileName( ) )
                    .append( "; number:" ).append( stackTrace[ 1 ].getLineNumber( ) ).append( "/n" )
                    .append( ( null != printInfo ) ? printInfo : "" );

            println( strBuffer.toString( ) );
        }

        public static void showStrictMode( ) {
            if (DebugUtils.Debug.DEBUG_MODE) {
                StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                        .detectDiskReads().detectDiskWrites().detectNetwork().penaltyLog().build());
                StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                        .detectLeakedSqlLiteObjects().detectLeakedClosableObjects().penaltyLog().penaltyDeath().build());
            }
        }

        public static void d(String tag, String msg){
            if(DebugUtils.Debug.DEBUG_MODE){
                Log.d(tag, msg);
            }
        }

        public class Debug{
            public static final boolean DEBUG_MODE = true;
        }

        public static final String TAG = "Debug";
    }

目視法:

這適合於code review,但是不太靠譜,因為人的精力畢竟有限,有時候你多敲一個分號,縮排不對都有可能導致程式出現問題,但在程式碼量較少時是一個高效率的方法。

自動化測試:

Android的自動化測試(分白盒測試和黑盒測試)工具有:monkey、Robotium、Appium、雲端測試(比如testin),具體用法可參見:

android實用測試方法之Monkey與MonkeyRunner

Robotium

Testin

Appium中文教程

排除法:

除錯、列印、目視這三種方法適合於可以復現的問題,對於隨機問題(實際上不存在隨機問題,只是問題不那麼容易復現而已),比如線上程、音訊播放、AnsynTask、Timer切換或者結束時剛好做了相應地人為操作導致出現靈異現象。這時候可以通過排除法來排查問題,具體的方法是首先大概定位到出現問題的位置,然後將程式碼一段一段地註釋,觀察程式現象,逐步縮小出現問題的範圍。

版本管理介紹

在較大的軟體開發過程中,可能有多個軟體工程師同時開發一個專案的情況,比如有負責讀取資料、獲取網路資料等API封裝的,有負責程式架構的,有負責上層介面實現的,為了能夠最終編譯一個完成的程式出來,需要將程式碼整合,這個時候最方便的方法就是使用版本管理工具,固定時間上傳(比如每天、沒改動一個功能等等),這樣能夠實時保證伺服器上的程式碼是最完整、最新的,也可以避免由於自然災害、電腦異常導致本地電腦掛掉損失掉程式碼的問題。

常見的版本管理工具有SVN和Git,我也使用過CVS,關於版本管理工具的介紹參見:

版本控制

版本控制系統的選擇之路

git教程

git簡易指南

注:對於windows使用者來說,建議使用烏龜殼系列的版本控制客戶端,使用github的朋友可以使用github for windows客戶端:

tortoisegit

tortoisecvs

tortoisesvn

github for windows

編譯

通常我們用Eclipse或者Android Studio開發android程式時,只需要執行程式就可以在模擬器或者機器上執行程式了,但為了保證程式碼的完整性、能夠在伺服器上編譯,需要通過編譯工具將程式碼編譯成apk,常見的編譯工具有:antgradle,但這兩種編譯工具都是需要通過手動敲命令來完成編譯功能(當然你也可以自己寫指令碼來實現編譯自動化),jenkins是一個持續整合的工具,通過它可以程式碼克隆、編譯以及程式加密自動化,其實它也是通過批處理來實現的,ant、gradle和jenkins的具體用法自行谷歌,使用起來很簡單,目前android studio和github上很多功能都是通過gradle來編譯的。

專業術語介紹

以下解釋完全是本人的理解,詳細解釋可自行谷歌。

  • 版本迭代:按照需求優先順序,在保證基本功能OK後持續開發和升級,這樣能夠降低軟體開發的風險,並且能夠及時解決使用者反饋的問題,船小好掉頭嘛;
  • 敏捷開發:小步快跑,大概意思就是不要過於注重文件,要注重當面交流,能夠在實現時高保真的還原使用者的需求場景,並且能夠快速地解決使用者的需求。
  • 單元測試:白盒測試的一種,對核心方法通過寫程式來測試自己的程式,單元測試的目的是讓你有意識地降低程式間的耦合,保證每一個方法都是最小單元,但這對於測試程式邏輯是沒有幫助,這是我自己的理解。。。
  • 灰度釋出:先找一部分使用者來使用即將釋出的程式(這部分使用者可以是隨機抽取、制定年齡段、指定地區或者通過某種方式知道他是活躍使用者),在測試的過程中給與使用者一點好處讓使用者寫使用者體驗報告、反饋問題等方式來發現程式存在的問題和缺陷;
  • DA統計:也叫後臺統計,通過在程式中埋點的方式,在有網路的情況下將使用者的操作行為和資料上傳到後臺,將每個使用者的資訊都上傳回來就叫大資料,通過建模對這些資料分析就叫大資料分析。
  • 開放平臺:比如分享到QQ空間、分享到微信、訊飛語音、友盟的後臺統計、天氣、地圖等等都叫做開放平臺,它提供了一些開放的介面給開發者,方便開發者使用它的服務,開放平臺多數服務都是免費的,但有時候也可能不穩定,比如用的人少它自然就活不下去了,然後就沒有然後了。
  • 同行評審:你的同行和你一起看看你的程式碼,發現是否有問題;
  • 結對程式設計:在寫程式碼的過程中,有個人坐在你旁邊或者你坐在別人旁邊,編寫邊討論,降低程式出現邏輯和低階錯誤的概率。

Android開發資源

參見我的另一篇文章:Android開發者網址導航

建議

  • 儘量閱讀官方文件,這才是原汁原味、不失真的開發指導;
  • 即使你認為設計程式是浪費時間,你只是喜歡寫程式,至少你也得用思維導圖理清思路,思維導圖對於幫助你理解設計文件、理清思路有很大的幫助;
  • 不要用Intent傳遞大量的資料,這有可能導致ANR或者報異常;
  • 在退出頁面後,系統不一定會及時執行onDestory方法,如果你在onDestory方法裡做關閉檔案、釋放記憶體的操作可能出現退出程式又立即進入時,由於需要重新初始化這些資訊導致程式碼重入的異常;
  • 在改動JNI後,執行程式之前記得解除安裝掉已經安裝在模擬器或者真機上的該程式,如果直接執行,android不會load最新編譯的so,也就不能立即看到修改後的效果;
  • 程式碼至少每天備份一次,或者是完善一個功能就備份一次,不要堆積之後一次性備份,因為在你的程式碼出問題需要回溯程式碼時你需要從伺服器上重新取程式碼,同時也可以避免程式碼不是最新導致最後和其他人合併時不知道改了哪些地方;
  • 將列印資訊封裝成一個方法,用一個標誌位控制這個這個方法的方法體是否需要執行,這樣在由debug版釋放到release版本時,不需要傻傻地一行一行地去掉程式碼,你只需要改變標誌位的值就可以了;
  • 對於有返回值的JNI函式,即使你不返回任何值,用NDK編譯JNI的時候也不會報錯,所以在寫JNI程式碼的時候,一定要仔細檢查程式碼;
  • JNI頻繁讀寫檔案操作會影響程式的執行效能,可以考慮一次性在記憶體中申請一塊大記憶體作為快取空間,用這種空間換時間的方式可以大大提高程式的執行效率;
  • 不要指望類的finalize方法去處理需要回收和銷燬的工作,因為finalize是系統回撥的方法,呼叫時機不可預見,切記;
  • 使用檔案流、Cursor時,使用結束後記得一定要關閉,否則可能導致記憶體洩漏,嚴重的情況可能引發程式崩潰;
  • 優先使用Google搜尋引擎(少用百度),如果不能正常使用Google搜尋引擎建議通過代理、VPN、修改hosts檔案等方式搭建梯子。這裡提供一個免費的谷歌搜尋引擎
  • 對於不需要使用硬體加速的activity(沒有動畫效果、視訊播放以及各種多媒體檔案的操作都可以關掉硬體加速),在AndroidManifest.xml檔案中通過“android:hardwareAccelerated=”false””關掉硬體加速可節省應用記憶體;
  • 對於需要橫豎屏轉換的應用,又不想在橫豎屏切換的時候重新跑onCreate方法,可以在AndroidManifest.xml檔案中對應的Activity標籤下呼叫“android:configChanges=”screenSize|orientation””;
  • 為了減輕應用程式主程式的記憶體壓力,對於耗記憶體比較多的介面(比如視訊播放介面、flash播放介面等),可以在AndroidManifest.xml檔案中對應的Activity標籤下呼叫“android:process=”.processname””單開一個程式,但在退出這個介面的時候一定要在該介面的onDestory方法中呼叫System的kill方法來殺掉該程式;
  • 在res/values/arrays.xml檔案中定義的單個陣列的元素個數不宜過大,過大會導致載入資料時非常慢,有時候你需要使用陣列資源時資料有可能還沒載入完成;
  • 一個Activity中最耗費記憶體的是activity的背景(多數情況如此,特別是對於解析度很大的機器,一個介面的背景算下來都需要好幾兆記憶體),所以在程式介面較多時,可以考慮將圖片轉換成靜態的drawable,然後多個activity共用這一張背景圖;
  • 可以通過為application、activity自定義主題的方式來關掉多點觸控功能,只需要在自定義的主題下新增這兩個標籤:
    <item name="android:windowEnableSplitTouch">false</item>
      <item name="android:splitMotionEvents">false</item>
  • 很多遊戲進入時,播放的片頭動畫多數是一個視訊檔案;
  • Android單個dex檔案的方法數不能超過65536個,android使用多個dex能否避開65536方法數限制?
  • 使用模擬器genymotion代替android自帶模擬器(它需要虛擬機器vituralbox的支援,不過官網已經提供了一個整合虛擬機器的安裝包了,直接下載下來安裝即可),可以大大提高使用模擬器的體驗(流暢、快),它也可以以外掛的形式整合在Eclipse中,這是視訊教程
  • 給Application或者activity設定自定義主題時,最好不要設定為全透明,否則在activity按Home鍵回退到桌面的時候效果很渣;
  • 如果你需要取消toast顯示的功能,在一個類中你只需要例項化該類一次(也就是說將Toast定義成一個全域性的成員變數),這樣你就可以呼叫mToast.cancel()了,我把它寫成了一個靜態類:
    public class ToastUtils {
          private ToastUtils( ){
    
          }
    
          public static void showToast( Context context, String toast ){
              if( null == mToast ){
                  mToast = Toast.makeText( context, toast, Toast.LENGTH_LONG );
              }else{
                  mToast.setText( toast );
              }
    
              mToast.show( );
          }
    
          public static void cancel( ){
              if( null != mToast ){
                  mToast.cancel( );
              }
          }
    
          public static Toast mToast = null;
      }
  • 你可以定義一個靜態類來實現防止按鈕被重複點選導致重複執行一段程式碼的問題:
    /**
       * 按鈕重複點選
       * 
       * */
      public class BtnClickUtils {
          private BtnClickUtils( ){
    
          }            
    
          public static boolean isFastDoubleClick() {
              long time = System.currentTimeMillis();
              long timeD = time - mLastClickTime;
              if ( 0 < timeD && timeD < 1000) {   
                  return true;   
              }
    
              mLastClickTime = time;
    
              return false;   
          }
    
          private static long mLastClickTime = 0;
      }
  • 放在apk的assets或者raw目錄下的資料檔案最好做加密處理,在需要使用的時候才解密,這樣可以避免在apk被他人破解時資料也被破解的問題;
  • 最好不要再activity的onCreate方法裡面呼叫popupwindow的show方法,有可能由於activity沒有完全初始化導致程式異常(android.view.WindowManager$BadTokenException: Unable to add window — token null is not valid),如果非要在一進activity就顯示popupwindow,建議用handler.post、View.postDelay來處理;
  • 對於自定義View,在構造方法裡面是獲取不到檢視的寬高的(此時獲取長寬都為0),需要在onMeasure方法中或者跑了onMeasure方法後才能夠獲取到檢視的寬高,不過你可以通過在構造方法裡面強制測量檢視的寬高來實現在構造方法裡獲取檢視的寬高資訊,具體見MeasureSpec介紹及使用詳解
  • 如果你覺得在安裝Eclipse後還需要配置android開發環境很麻煩,你可以直接使用ADT Bundle,它是一個懶人套餐,下載下來就可以用了,可以在這裡下載。
  • 有時間看看阿里技術嘉年華InfoQ演講與訪談Google IO視訊,可以學習到一些解決問題、做大專案的經驗。
  • 當應用中動畫比較多,並且動畫都是通過圖片來切換的時候,可以考慮借用Cocos的精靈表單思想,這樣就可以避免圖片命名的煩惱。

工具推薦

Android應用開發第三方解決方案

下圖為Android應用開發第三方解決方案彙總,有些可以藉助第三方平臺搞定的就儘量不要自己搞,一是可以節省成本,二是你沒人家專業,原文連結:Android應用開發第三方解決方案

很少有人會告訴你的Android開發基本常識

相關文章