Android 逆向?qū)W習(xí)詳解及實(shí)例
斷斷續(xù)續(xù)的總算的把a(bǔ)ndroid開(kāi)發(fā)和逆向的這兩本書看完了,雖然沒(méi)有java,和android開(kāi)發(fā)的基礎(chǔ),但總體感覺(jué)起來(lái)還是比較能接收的,畢竟都是觸類旁通的。當(dāng)然要深入的話還需要對(duì)這門語(yǔ)言的細(xì)節(jié)特性和奇技淫巧進(jìn)行挖掘。
這里推薦2本書,個(gè)人覺(jué)得對(duì)android開(kāi)發(fā)入門和android逆向入門比較好的教材:
《google android 開(kāi)發(fā)入門與實(shí)戰(zhàn)》
《android 軟件安全與逆向分析》
1. 我對(duì)android逆向的認(rèn)識(shí)
因?yàn)橹坝幸恍﹚indows逆向的基礎(chǔ),在看android逆向的時(shí)候感覺(jué)很多東西都是能共通的。但因?yàn)閍ndroid程序本身的特性,還是有很多不同的地方。
1.1 反編譯
android程序使用java語(yǔ)言編寫,從java到android虛擬機(jī)(Dalvik)的dex代碼(可以看成是android虛擬機(jī)的機(jī)器碼)需要一個(gè)中間語(yǔ)言的轉(zhuǎn)換過(guò)程。類似.NET的IL中間虛擬指令。而我們知道,.NET的IL中間代碼之所以能很容易的"反編譯"回C#源代碼,是因?yàn)槌薎L中間語(yǔ)言,還包含了大量的META元數(shù)據(jù),這些元數(shù)據(jù)使我們可以很容易的一一對(duì)應(yīng)的反編譯回C#的源代碼。java的中間語(yǔ)言.class文件也是類似的道理,我們可以使用工具直接從dex機(jī)器碼反編譯回java源代碼。
1.2 逆向分析手段
windows的逆向分析中,我們可以使用OD或者C32ASM來(lái)分析匯編指令(當(dāng)然OD還可以動(dòng)態(tài)調(diào)試),或者使用IDA + F5(hex Ray反編譯插件)來(lái)靜態(tài)的分析源代碼(C/C++)
在android逆向分析過(guò)程中:
1) 我們可以使用ApkTool(本質(zhì)上是BakSmali反匯編引擎)對(duì)apk文件進(jìn)行反匯編,得到各個(gè)類、方法、資源、布局文件...的smali代碼,我們可以直接通過(guò)閱讀smali代碼來(lái)分析程序的代碼流,進(jìn)行關(guān)鍵點(diǎn)的修改或者代碼注入。
2) 我們可以從apk中提取.dex文件,使用dex2jar工具對(duì)dex進(jìn)行反匯編,得到j(luò)ar包(java虛擬指令),然后使用jd-gui等工具再次反編譯,得到j(luò)ava源代碼,從源碼級(jí)的高度來(lái)審計(jì)代碼,更快的找到關(guān)鍵點(diǎn)函數(shù)或者判斷,然后再回到smali層面,對(duì)代碼進(jìn)行修改。這種方法更傾向于輔助性的,最終的步驟我們都要回到smali層面來(lái)修改代碼。
3) 使用IDA Pro直接分析APK包中的.dex文件,找到關(guān)鍵點(diǎn)代碼的位置,記下文件偏移量,然后直接對(duì).dex文件進(jìn)行修改。修改完之后把.dex文件重新導(dǎo)入apk中。這個(gè)時(shí)候要注意修改dex文件頭中DexHeader中的checksum字段。將這個(gè)值修復(fù)后,重新導(dǎo)入apk中,并刪除apk中的META-INF文件夾,重新簽名即可完成破解。
1.3 android與C的結(jié)合
在學(xué)習(xí)android逆向的時(shí)候感覺(jué)遇到的最難的問(wèn)題就是分析原生代碼,即JNI代碼。開(kāi)發(fā)者使用android NDK編寫C/C++代碼供android的java代碼調(diào)用(通過(guò)java的代碼轉(zhuǎn)接層來(lái)完成接口的轉(zhuǎn)換)。
使用android NDK編寫的C/C++代碼最終會(huì)生成基于ARM的ARM ELF可執(zhí)行文件,我們想要分析軟件的功能就必須掌握另一項(xiàng)技能,ARM匯編,ARM匯編個(gè)人感覺(jué)雖然和x86匯編類似,不過(guò)由于IDA Pro對(duì)ARM匯編沒(méi)有反編譯功能以及貌似沒(méi)有工具能動(dòng)態(tài)調(diào)試ARM代碼(我網(wǎng)上沒(méi)找到),導(dǎo)致我們只能直接硬看ARM代碼,加上往往伴隨著復(fù)雜的密碼學(xué)算法等等,導(dǎo)致對(duì)Native Code的逆向相對(duì)來(lái)說(shuō)比較困難,對(duì)基本功的要求比較高。
1.4 關(guān)于分析android程序
1) 了解程序的AndroidManifest.xml。在程序中使用的所有activity(交互組件)都需要在AndroidManifest.xml文件中手動(dòng)聲明。包括程序啟動(dòng)時(shí)默認(rèn)啟動(dòng)的主activity,通過(guò)研究這個(gè)AndroidManifest.xml文件,我們可以知道該程序使用了多少的activity,主activity是誰(shuí),使用了哪些權(quán)限,使用了哪些服務(wù),做到心中有數(shù)。
2) 重點(diǎn)關(guān)注Application類
這本來(lái)和1) AndroidManifest.xml是一起的,但是分出來(lái)說(shuō)是因?yàn)檫@個(gè)思路和windows下的逆向思路有相通之處。
在windows exe的數(shù)據(jù)目錄表中如果存在TLS項(xiàng),那程序在加載后會(huì)首先執(zhí)行這個(gè)TLS中的代碼,執(zhí)行完之后才進(jìn)行main主程序入口。
在android 中Application類比程序中其他的類啟動(dòng)的都要早。
3) 定位關(guān)鍵代碼
3.1) 信息反饋法(關(guān)鍵字查找法)
通過(guò)運(yùn)行程序,查找程序UI中出現(xiàn)的提示消息或標(biāo)題等關(guān)鍵字,到String.xmlzhong中查找指定字符串的di,然后到程序中查找指定的id即可。
3.2) 特征函數(shù)法
這種做法的原理和信息反饋法類似,因?yàn)椴还苣闾崾臼裁聪?,就必然?huì)調(diào)用相應(yīng)的API函數(shù)來(lái)顯示這個(gè)字符串,例如Toast.MakeText().show()
例如在程序中搜索Toast就有可能很快地定位到調(diào)用代碼
3.3) 代碼注入法
代碼注入法屬于動(dòng)態(tài)調(diào)試的方法,我們可以手動(dòng)修改smali反匯編代碼,加入Log輸入,配合LogCat來(lái)查看程序執(zhí)行到特定點(diǎn)時(shí)的狀態(tài)數(shù)據(jù)。
3.4) 棧跟蹤法
棧跟蹤法屬于動(dòng)態(tài)調(diào)試方法,從原理上和我們用OD調(diào)試時(shí)查看call stack的思想類似。我們可以在smali代碼中注入輸出運(yùn)行時(shí)的棧跟蹤信息,然后查看棧上的函數(shù)調(diào)用序列來(lái)理解方法的執(zhí)行流程(因?yàn)槊總€(gè)函數(shù)的執(zhí)行都會(huì)在棧上留下記錄)
3.5) Method Profiling
Method Profiling,方法剖析(這是書上的叫法,我更愿意叫BenchMark測(cè)試法),它屬于一種動(dòng)態(tài)調(diào)試方法,它主要用于熱點(diǎn)分析和性能優(yōu)化。在DDMS中有提供這個(gè)功能,它除了可記錄每個(gè)函數(shù)所占用的CPU時(shí)間外,還能夠跟蹤所有的函數(shù)調(diào)用關(guān)系。
1.5 關(guān)于android的代碼混淆和加殼
java語(yǔ)言編寫的代碼本身就很容易被反編譯,google為此在android 2.3的SDK中正式加入了ProGuard代碼混淆工具,只要正確的配置好project.properties與proguard.cfg兩個(gè)文件即可使用ProGuard混淆軟件。
java語(yǔ)言由于語(yǔ)言自身的特殊性,沒(méi)有外殼保護(hù)這個(gè)概念,只能通過(guò)混淆方式對(duì)其進(jìn)行保護(hù)。對(duì)android NDK編寫的Native Code倒是可以進(jìn)行加殼,但目前貌似只能進(jìn)行ups的壓縮殼保護(hù)
2. CrackMe_1 分析學(xué)習(xí)
2.1 運(yùn)行一下程序,收集一些基本信息
只有一個(gè)輸入框,那說(shuō)明這個(gè)驗(yàn)證碼的輸入來(lái)自別的地方,因?yàn)槲覀冎?,不管你的加密算法是啥,總是要有一個(gè)函數(shù)輸入源的,我們?cè)赨I界面上輸入的相當(dāng)于是結(jié)果,而輸入源應(yīng)該來(lái)自于別的地方,計(jì)算完之后和我們?cè)赨I上輸入的結(jié)果進(jìn)行對(duì)比,大致是這個(gè)思路。
2.2 分析
使用apktool反編譯apk文件。查看AndroidManifest.xml文件。了解到主activity為:Main。
接著我們從apk中提取.dex文件。用dex2jar->jd-gui來(lái)查看java源代碼。
看到里面很多的a,b,c方法,基本上可以判定是配ProGuard混淆了,不過(guò)問(wèn)題也不大,雖然顯示的是無(wú)意義的函數(shù)名但是不影響我們分析代碼流程。
2.2.1 類b的分析
從OnCreate()的代碼來(lái)看,我們首先從類b開(kāi)始分析:
類 b 提供了一個(gè)公共的構(gòu)造函數(shù) public b(Context paramContext), 一個(gè)私有的成員函數(shù)private String b(), 以及一個(gè)公有成員函數(shù) public final void a()。
b(): 通過(guò)TelephonyManager獲取設(shè)備相關(guān)的一些信息,然后通過(guò)PackageManager獲取到自身的簽名。然后把這些字符串拼接起來(lái)返回給調(diào)用者。
TelephonyManager localTelephonyManager = (TelephonyManager)this.a.getSystemService("phone"); String str1 = localTelephonyManager.getDeviceId(); String str2 = localTelephonyManager.getLine1Number(); String str3 = localTelephonyManager.getDeviceSoftwareVersion(); String str4 = localTelephonyManager.getSimSerialNumber(); String str5 = localTelephonyManager.getSubscriberId(); Object localObject = ""; PackageManager localPackageManager = this.a.getPackageManager(); try { String str6 = localPackageManager.getPackageInfo("com.lohan.crackme1", 64).signatures[0].toCharsString(); localObject = str6; return str1 + str2 + str3 + str4 + str5 + (String)localObject; } a(): SharedPreferences localSharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.a); SharedPreferences.Editor localEditor; if (!localSharedPreferences.contains("machine_id")) localEditor = localSharedPreferences.edit(); try { localEditor.putString("machine_id", b()); localEditor.commit(); return; }
a()調(diào)用方法b()獲取字符串,然后通過(guò)SharedPreferences.Editor將這個(gè)字符串值存儲(chǔ)到鍵machine_id,可以理解為機(jī)器碼。也就是說(shuō),這個(gè)加密函數(shù)的輸入是本機(jī)的機(jī)器碼。
經(jīng)過(guò)上面的分析,類b對(duì)外提供方法a,功能就是生成"機(jī)器碼"并存儲(chǔ)到系統(tǒng)中,對(duì)應(yīng)的鍵為machine_id。
2.2.2 類c的分析
類c提供的方法較多,我們逐個(gè)分析。
1) 構(gòu)造函數(shù)
Java代碼
public c(Context paramContext) { a = paramContext; b = "f0d412b5530e1f9841aab434d989cc77"; c = "4ec407446b872351e613111339daae9"; }
把參數(shù)環(huán)境上下文Context本地化,并聲明了兩個(gè)字符串。
2) public static boolean b()
Java代碼
MessageDigest localMessageDigest = MessageDigest.getInstance("MD5"); localMessageDigest.update(paramString.getBytes(), 0, paramString.length()); return new BigInteger(1, localMessageDigest.digest()).toString(16);
通過(guò)MessageDigest計(jì)算paramString 的MD5值。
3) public static boolean b()
Java代碼
PackageManager localPackageManager = a.getPackageManager(); try { String str = b(new String(localPackageManager.getPackageInfo("com.lohan.crackme1", 64).signatures[0].toChars())); if (!str.equals(b)) { boolean bool = str.equals(c); if (!bool); } else { return false; } }
通過(guò) getPackageManager 獲取自身的簽名,如果簽名與構(gòu)造函數(shù)中的兩個(gè)字符串b(f0d412b5530e1f9841aab434d989cc77)或者c(4ec407446b872351e613111339daae9)任意一個(gè)相等,那么返回false,否則返回true。
4) public static int a(String paramString)
Java代碼
try { if (b()) return 0; SharedPreferences localSharedPreferences = PreferenceManager.getDefaultSharedPreferences(a); if (b(localSharedPreferences.getString("machine_id", "")).equals(paramString)) { if (b()) return 0; SharedPreferences.Editor localEditor = localSharedPreferences.edit(); localEditor.putString("serial", paramString); localEditor.commit(); return 1; } }
可以看出這段代碼的功能為計(jì)算機(jī)器碼的 MD5,如果與傳入的參數(shù)paramString一致,那么通過(guò)SharedPreferences存入到serial(機(jī)器碼的MD5值paramString)字段中。 當(dāng)然還有調(diào)用b方法進(jìn)行一些判斷,自身的簽名不能是已知的兩個(gè)。
5) public static boolean a()
Java代碼
SharedPreferences localSharedPreferences = PreferenceManager.getDefaultSharedPreferences(a); if (!localSharedPreferences.contains("serial")) return false; String str = localSharedPreferences.getString("serial", ""); if (str.equals("")) return false; return a(str) >= 0;
這個(gè)其實(shí)就是上面的 int a(String paramString)的包裝函數(shù),通過(guò)SharedPreferences獲取serial字段(機(jī)器碼的MD5值),并傳給這個(gè)方法,返回相應(yīng)的返回值(判斷結(jié)果)。
2.2.3 類a分析
可以看到,類a是一個(gè)CountDownTimer:
Schedule a countdown until a time in the future, with regular notifications on intervals along the way. Example of showing a 30 second countdown in a text field:(android Developer)
從onFinish函數(shù)我們看出這個(gè)類的功能是倒計(jì)時(shí)6秒,然后調(diào)用c.a(),也就是判斷我們輸入的serial是否等于"機(jī)器碼"的MD5值。如果不能通過(guò),就設(shè)置TextView內(nèi)容提示注冊(cè)。
2.2.4 類Main分析
1) 在onCreate(),先初始化b和c的類。然后調(diào)用b.a()生成并存儲(chǔ)"機(jī)器碼",然后調(diào)用c.a(),也就是判斷是否已經(jīng)存儲(chǔ)了serial,并判斷是否能通過(guò)算法校驗(yàn)。如果不能通過(guò),則什么都不做,這就是啟動(dòng)時(shí)檢測(cè)注冊(cè)狀態(tài)的做法,即如果你之前已經(jīng)注冊(cè)了,那在之后的登錄后就會(huì)自動(dòng)識(shí)別出來(lái),但是我們?nèi)绻堑谝淮螁?dòng)且沒(méi)有注冊(cè),那這里就什么也不做。
如果能通過(guò),則調(diào)用自身的方法a()。而自身的方法a()又調(diào)用了c.b()方法,即檢查我們輸入的serial和機(jī)器碼的MD5值是否相同,如果相同則什么也不做,如果不同就把下面的按鈕和TextView等UI控件給隱藏了。并啟動(dòng)倒計(jì)時(shí)類a.start()。即二次驗(yàn)證。
ps:
這里要注意的是,由于程序使用了ProGuard來(lái)混淆代碼,所以用jd-gui翻譯出來(lái)的代碼全都是從a,b,c開(kāi)始計(jì)數(shù),而且經(jīng)常是變量、類、方法的命名混合了起來(lái)。我們?cè)诳磈ava代碼的時(shí)候遇到難懂的地方要結(jié)合smali代碼一起看,這樣才能獲取比較準(zhǔn)確的對(duì)程序代碼流的把握。
2) public void onClick(View paramView)
Java代碼
if (c.a(((EditText)findViewById(2131034114)).getText().toString()) == 0) { Toast.makeText(this, 2130968577, 0).show(); return; } Toast.makeText(this, 2130968578, 0).show();
判斷我們通過(guò)UI輸入的serial是否和"機(jī)器碼"的MD5值相同,如果不相同則彈出提示Invalid serial!(可以通過(guò)ID值反查出對(duì)應(yīng)的字符串),如果相同則彈出Thanks for purchasing!
通過(guò)以上分析,我們來(lái)綜合一下思路:
程序啟動(dòng)時(shí)會(huì)做一些初始化的工作,然后生成本地對(duì)應(yīng)的機(jī)器碼并保存在SharedPreferences中。
檢查當(dāng)前的SharedPreferences中是否已經(jīng)保存了serial鍵值對(duì),并檢查正確性,即檢查是否上一次已經(jīng)注冊(cè)了。如果沒(méi)有這個(gè)鍵值對(duì),說(shuō)明還沒(méi)注冊(cè),如果存在這個(gè)鍵值對(duì)且正確性也符合,代碼接下來(lái)會(huì)繼續(xù)檢查APK自身的簽名是否為代碼中定義的那兩個(gè),如果相等則什么都不做(即依然不通過(guò)檢查),如果不等則代碼繼續(xù)執(zhí)行倒計(jì)時(shí)6秒的類a, 6秒后再次檢查一次serial鍵值對(duì)。
對(duì)于那個(gè)按鈕點(diǎn)擊事件,onClick(),它獲取用戶通過(guò)UI輸入的serial,并檢測(cè)是否和"機(jī)器碼"的MD5值相等,如果相等則存進(jìn)SharedPreferences中的鍵值對(duì)中。
以上基本就是這個(gè)程序的代碼思路了。我們可以看到,作者這里使用了雙重保護(hù)的思路,即不僅要你輸入的serial相同,而且對(duì)你的APK的簽名也有限制。
3. 破解思路
3.1 單純的破解,用代碼注入的方法得到注冊(cè)碼。
經(jīng)過(guò)分析,我們知道應(yīng)該在b.smali的155行:
move-result-object v2 這里代碼注入,因?yàn)檫@個(gè)b()的作用就是獲取當(dāng)前"機(jī)器碼"(注意,這里獲取的是沒(méi)有MD5之前的"機(jī)器碼",因?yàn)槌绦蛑械腗D5都是臨時(shí)算出來(lái)的)
我們?cè)谶@里加入:
const-string v3, "SN"
invoke-static {v3, v2}, Landroid/util/Log;->v(Ljava/lang/String;Ljava/lang/String;)I
重新回編譯smalli代碼。
在命令行中執(zhí)行 adb logcat -s SN:v ,然后再啟動(dòng)程序
會(huì)在命令行中看到一大串字符串,這些字符串就是我們要的機(jī)器碼
將這些字符串計(jì)算MD5值之后,就可以完成破解了。
3.2 讀取程序?qū)?yīng)的文件
我們知道,所謂的SharedPreferences本質(zhì)上是保存在當(dāng)前程序空間下的/data/data/<package name>/shared_prefs/<package name>_preferences.xml文件中的。
我們可以通過(guò)adb連接上去,直接讀取這個(gè)文件的內(nèi)容。
可以看到,和我們通過(guò)代碼注入的方式得到的機(jī)器碼是相同的。
3.3 編寫注冊(cè)機(jī)
這種方法是最好的,編寫注冊(cè)機(jī)要求我們對(duì)目標(biāo)程序的代碼有全盤的認(rèn)識(shí),然后模擬原本的算法或者逆向原本的算法寫出注冊(cè)機(jī)
我們用Eclipse重新生成一個(gè)新的工程 com.lohan.crackme。注意,工程的報(bào)名必須和目標(biāo)程序的包名一致,這樣我們的注冊(cè)機(jī)運(yùn)行后得到的APK簽名才會(huì)是一樣的。
核心算法如下:
Java代碼
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); setTitle("crackMe1_keyGen"); final Context context = getApplicationContext(); //獲取UI控件 txt_machineCode = (TextView) findViewById(R.id.machineCode); txt_apkSig = (TextView) findViewById(R.id.apkSig); txt_serial = (TextView) findViewById(R.id.serial); btn_Go = (Button) findViewById(R.id.ok); //設(shè)置監(jiān)聽(tīng)事件 btn_Go.setOnClickListener(new OnClickListener(){ public void onClick(View v) { //計(jì)算機(jī)器碼 TelephonyManager localTelephonyManager = (TelephonyManager) context.getSystemService("phone"); String str1 = localTelephonyManager.getDeviceId(); String str2 = localTelephonyManager.getLine1Number(); String str3 = localTelephonyManager.getDeviceSoftwareVersion(); String str4 = localTelephonyManager.getSimSerialNumber(); String str5 = localTelephonyManager.getSubscriberId(); Object localObject = ""; PackageManager localPackageManager = context.getPackageManager(); try { String str6 = localPackageManager.getPackageInfo("com.lohan.crackme1", 64).signatures[0].toCharsString(); localObject = str6; String str_result = str1 + str2 + str3 + str4 + str5 + (String)localObject; //得出機(jī)器碼 txt_machineCode.setText(str_result); //計(jì)算當(dāng)前APK的簽名 txt_apkSig.setText(str6); //計(jì)算注冊(cè)碼 MessageDigest localMessageDigest = null; try { localMessageDigest = MessageDigest.getInstance("MD5"); } catch (NoSuchAlgorithmException e) { // TODO Auto-generated catch block e.printStackTrace(); } localMessageDigest.update(str_result.getBytes(), 0, str_result.length()); String str_serial = new BigInteger(1, localMessageDigest.digest()).toString(16); txt_serial.setText(str_serial); } catch (PackageManager.NameNotFoundException localNameNotFoundException) { while (true) localNameNotFoundException.printStackTrace(); } } });
破解結(jié)果
APK:
4. 總結(jié)
至此,這個(gè)android的CrackeMe_1就算破解完成了。這段時(shí)間的android學(xué)習(xí)也算暫時(shí)告一段落,移動(dòng)無(wú)線安全是未來(lái)的新方向,在不遠(yuǎn)的將來(lái),基于android平臺(tái)的各種應(yīng)用和軟件不僅僅是手機(jī)甚至是各種的互聯(lián)終端都將進(jìn)入人們的視野,無(wú)線安全的研究應(yīng)該也會(huì)慢慢成為熱點(diǎn)。
我也希望下次再研究android安全的時(shí)候能有更深入的認(rèn)識(shí)和體會(huì)。
有興趣的同學(xué)可以看下本文,謝謝大家對(duì)本站的支持!
相關(guān)文章
Android 中ActionBar+fragment實(shí)現(xiàn)頁(yè)面導(dǎo)航的實(shí)例
這篇文章主要介紹了Android 中ActionBar+fragment實(shí)現(xiàn)頁(yè)面導(dǎo)航的實(shí)例的相關(guān)資料,希望通過(guò)本文能幫助到大家實(shí)現(xiàn)這樣的功能,需要的朋友可以參考下2017-09-09android實(shí)用工具類分享(獲取內(nèi)存/檢查網(wǎng)絡(luò)/屏幕高度/手機(jī)分辨率)
這篇文章主要介紹了android實(shí)用工具類,包括獲取內(nèi)存、檢查網(wǎng)絡(luò)、屏幕高度、手機(jī)分辨率、獲取版本號(hào)等功能,需要的朋友可以參考下2014-03-03Android Retrofit 中文亂碼問(wèn)題的解決辦法
這篇文章主要介紹了Android Retrofit 中文亂碼問(wèn)題的解決辦法的相關(guān)資料,希望通過(guò)本文能幫助到大家,讓大家遇到這種問(wèn)題及時(shí)的解決,需要的朋友可以參考下2017-10-10Android自定義view實(shí)現(xiàn)進(jìn)度條指示效果
這篇文章主要為大家詳細(xì)介紹了Android自定義view實(shí)現(xiàn)進(jìn)度條指示效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-01-01Android仿360桌面手機(jī)衛(wèi)士懸浮窗效果
這篇文章主要介紹了Android仿360手機(jī)衛(wèi)士懸浮窗效果的桌面實(shí)現(xiàn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05Android 四種動(dòng)畫效果的調(diào)用實(shí)現(xiàn)代碼
在這里, 我將每種動(dòng)畫分別應(yīng)用于四個(gè)按鈕為例,需要的朋友可以參考下2013-01-01Android使用Room操作數(shù)據(jù)庫(kù)流程詳解
谷歌推薦使用Room操作數(shù)據(jù)庫(kù),Room在 SQLite 上提供了一個(gè)抽象層,在充分利用 SQLite強(qiáng)大功能的同時(shí),能夠流暢地訪問(wèn)數(shù)據(jù)庫(kù)2022-11-11Android-如何將RGB彩色圖轉(zhuǎn)換為灰度圖方法介紹
本文將詳細(xì)介紹Android-如何將RGB彩色圖轉(zhuǎn)換為灰度圖方法,需要了解更多的朋友可以參考下2012-11-11