Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android資訊 >> 很少有人會告訴你的 Android 開發基本常識

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

編輯:Android資訊

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

軟件開發流程

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

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

注:

  • 可以查看這個答案了解一個APP從創意到上線的具體流程,開發一個APP有多難?
  • 可以查看筆戈科技的這篇文章了解一個手機(平板或其它電子產品也差不多)的誕生需要哪些環節,一個手機的誕生過程

提問的智慧

大多數工作都是以結果為導向的,特別是軟件開發這個職業,績效考核、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,常見的編譯工具有:ant、gradle,但這兩種編譯工具都是需要通過手動敲命令來完成編譯功能(當然你也可以自己寫腳本來實現編譯自動化),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的精靈表單思想,這樣就可以避免圖片命名的煩惱。

工具推薦

  • 代碼對比:Beyond compare
  • 屏幕取色:ColorPix
  • 梯子:紅杏
  • 思維導圖: mindmanager
  • 在線工具:在線工具

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

下圖為Android應用開發第三方解決方案匯總,有些可以借助第三方平台搞定的就盡量不要自己搞,一是可以節省成本,二是你沒人家專業,原文鏈接:Android應用開發第三方解決方案

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

  1. 上一頁:
  2. 下一頁:
Copyright © Android教程網 All Rights Reserved