【Android 逆向】启动 DEX 字节码中的 Activity 组件 ( 替换 LoadedApk 中的类加载器 | 加载 DEX 文件中的 Activity 类并启动成功 )
文章目录
前言
在 上一篇博客 【Android 逆向】启动 DEX 字节码中的 Activity 组件 ( DEX 文件准备 | 拷贝资源目录下的文件到内置存储区 | 配置清单文件 | 启动 DEX 文件中的组件 | 执行结果 ) 的代码基础上 , 使用类加载器加载 com.example.dex_demo.MainActivity2
组件前 , 先替换 LoadedApk 的类加载器 , 就可以成功加载 DEX 文件了 , 该操作类似于热修复 ;
/**
* 不修改类加载器的前提下 , 运行 Dex 字节码文件中的组件
*
* @param context
* @param dexFilePath
*/
private void startDexActivityWithoutClassLoader(Context context, String dexFilePath) {
// 优化目录
File optFile = new File(getFilesDir(), "opt_dex");
// 依赖库目录 , 用于存放 so 文件
File libFile = new File(getFilesDir(), "lib_path");
// 初始化 DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(
dexFilePath, // Dex 字节码文件路径
optFile.getAbsolutePath(), // 优化目录
libFile.getAbsolutePath(), // 依赖库目录
context.getClassLoader() // 父节点类加载器
);
// 加载 com.example.dex_demo.DexTest 类
// 该类中有可执行方法 test()
Class<?> clazz = null;
try {
clazz = dexClassLoader.loadClass("com.example.dex_demo.MainActivity2");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// 启动 com.example.dex_demo.MainActivity2 组件
if (clazz != null) {
context.startActivity(new Intent(context, clazz));
}
}
一、替换 LoadedApk 中的类加载器
参考 【Android 逆向】加壳的 Android 应用启动流程 | 使用反射替换 LoadedApk 中的类加载器流程 二、使用反射替换 LoadedApk 中的类加载器流程 博客章节 , 使用反射替换 LoadedApk 的类加载器 ;
1、获取 ActivityThread 实例对象
首先 , 获取 ActivityThread 实例对象 , 该类全局单例 , 获取其类 , 就可以获取实例对象 ;
// I. 获取 ActivityThread 实例对象
// 获取 ActivityThread 字节码类 , 这里可以使用自定义的类加载器加载
// 原因是 基于 双亲委派机制 , 自定义的 DexClassLoader 无法加载 , 但是其父类可以加载
// 即使父类不可加载 , 父类的父类也可以加载
Class<?> ActivityThreadClass = null;
try {
ActivityThreadClass = dexClassLoader.loadClass(
"android.app.ActivityThread");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// 获取 ActivityThread 中的 sCurrentActivityThread 成员
// 获取的字段如下 :
// private static volatile ActivityThread sCurrentActivityThread;
// 获取字段的方法如下 :
// public static ActivityThread currentActivityThread() {return sCurrentActivityThread;}
Method currentActivityThreadMethod = null;
try {
currentActivityThreadMethod = ActivityThreadClass.getDeclaredMethod(
"currentActivityThread");
// 设置可访问性 , 所有的 方法 , 字段 反射 , 都要设置可访问性
currentActivityThreadMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
// 执行 ActivityThread 的 currentActivityThread() 方法 , 传入参数 null
Object activityThreadObject = null;
try {
activityThreadObject = currentActivityThreadMethod.invoke(null);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
2、获取 LoadedApk 实例对象
然后 , 从 ActivityThread 实例对象中 , 获取 LoadedApk 成员 ;
// II. 获取 LoadedApk 实例对象
// 获取 ActivityThread 实例对象的 mPackages 成员
// final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
Field mPackagesField = null;
try {
mPackagesField = ActivityThreadClass.getDeclaredField("mPackages");
// 设置可访问性 , 所有的 方法 , 字段 反射 , 都要设置可访问性
mPackagesField.setAccessible(true);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
// 从 ActivityThread 实例对象 activityThreadObject 中
// 获取 mPackages 成员
ArrayMap mPackagesObject = null;
try {
mPackagesObject = (ArrayMap) mPackagesField.get(activityThreadObject);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
// 获取 WeakReference<LoadedApk> 弱引用对象
WeakReference weakReference = (WeakReference) mPackagesObject.get(this.getPackageName());
// 获取 LoadedApk 实例对象
Object loadedApkObject = weakReference.get();
3、替换 LoadedApk 实例对象中的 mClassLoader 类加载器
最后 , 替换 LoadedApk 实例对象中的 mClassLoader 类加载器 ;
// III. 替换 LoadedApk 实例对象中的 mClassLoader 类加载器
// 加载 android.app.LoadedApk 类
Class LoadedApkClass = null;
try {
LoadedApkClass = dexClassLoader.loadClass("android.app.LoadedApk");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// 通过反射获取 private ClassLoader mClassLoader; 类加载器对象
Field mClassLoaderField = null;
try {
mClassLoaderField = LoadedApkClass.getDeclaredField("mClassLoader");
// 设置可访问性
mClassLoaderField.setAccessible(true);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
// 替换 mClassLoader 成员
try {
mClassLoaderField.set(loadedApkObject, dexClassLoader);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
二、完整代码示例
下面代码中
// 替换 LoadedApk 中的 类加载器 ClassLoader
// 然后使用替换的类加载器加载 DEX 字节码文件中的 Activity 组件
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
startDexActivityWithoutReplacedClassLoader(this, mDexPath);
}
就是先替换 LoadedApk 中的 类加载器 ClassLoader , 然后使用替换的类加载器加载 DEX 字节码文件中的 Activity 组件 ;
完整代码示例 :
package com.example.classloader_demo;
import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.ArrayMap;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import dalvik.system.DexClassLoader;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
/**
* Dex 文件路径
*/
private String mDexPath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 打印类加载器及父节点
classloaderLog();
// 拷贝 dex 文件
mDexPath = copyFile2();
// 测试 DEX 文件中的方法
testDex(this, mDexPath);
// 拷贝 dex2 文件
//mDexPath = copyFile2();
// 启动 DEX 中的 Activity 组件 , 此处启动会失败
//startDexActivityWithoutClassLoader(this, mDexPath);
// 替换 LoadedApk 中的 类加载器 ClassLoader
// 然后使用替换的类加载器加载 DEX 字节码文件中的 Activity 组件
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
startDexActivityWithoutReplacedClassLoader(this, mDexPath);
}
}
/**
* 打印当前的类加载器及父节点
*/
private void classloaderLog() {
// 获取当前 Activity 的 类加载器 ClassLoader
ClassLoader classLoader = MainActivity.class.getClassLoader();
// 打印当前 Activity 的 ClassLoader 类加载器
Log.i(TAG, "MainActivity ClassLoader : " + classLoader);
// 获取 类加载器 父类
ClassLoader parentClassLoader = classLoader.getParent();
// 打印当前 Activity 的 ClassLoader 类加载器 的父类
Log.i(TAG, "MainActivity Parent ClassLoader : " + parentClassLoader);
}
/**
* 将 app\src\main\assets\classes.dex 文件 ,
* 拷贝到 /data/user/0/com.example.classloader_demo/files/classes.dex 位置
*/
private String copyFile() {
// DEX 文件
File dexFile = new File(getFilesDir(), "classes.dex");
// DEX 文件路径
String dexPath = dexFile.getAbsolutePath();
Log.i(TAG, "开始拷贝文件 dexPath : " + dexPath);
// 如果之前已经加载过 , 则退出
if (dexFile.exists()) {
Log.i(TAG, "文件已经拷贝 , 退出");
return dexPath;
}
try {
InputStream inputStream = getAssets().open("classes.dex");
FileOutputStream fileOutputStream = new FileOutputStream(dexPath);
byte[] buffer = new byte[1024 * 4];
int readLen = 0;
while ((readLen = inputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, readLen);
}
inputStream.close();
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
Log.i("HSL", "classes.dex 文件拷贝完毕");
}
return dexPath;
}
/**
* 将 app\src\main\assets\classes2.dex 文件 ,
* 拷贝到 /data/user/0/com.example.classloader_demo/files/classes2.dex 位置
*/
private String copyFile2() {
// DEX 文件
File dexFile = new File(getFilesDir(), "classes2.dex");
// DEX 文件路径
String dexPath = dexFile.getAbsolutePath();
Log.i(TAG, "开始拷贝文件 dexPath : " + dexPath);
// 如果之前已经加载过 , 则退出
if (dexFile.exists()) {
Log.i(TAG, "文件已经拷贝 , 退出");
return dexPath;
}
try {
InputStream inputStream = getAssets().open("classes2.dex");
FileOutputStream fileOutputStream = new FileOutputStream(dexPath);
byte[] buffer = new byte[1024 * 4];
int readLen = 0;
while ((readLen = inputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, readLen);
}
inputStream.close();
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
Log.i("HSL", "classes2.dex 文件拷贝完毕");
}
return dexPath;
}
/**
* 测试调用 Dex 字节码文件中的方法
*
* @param context
* @param dexFilePath
*/
private void testDex(Context context, String dexFilePath) {
// 优化目录
File optFile = new File(getFilesDir(), "opt_dex");
// 依赖库目录 , 用于存放 so 文件
File libFile = new File(getFilesDir(), "lib_path");
// 初始化 DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(
dexFilePath, // Dex 字节码文件路径
optFile.getAbsolutePath(), // 优化目录
libFile.getAbsolutePath(), // 依赖库目录
context.getClassLoader() // 父节点类加载器
);
// 加载 com.example.dex_demo.DexTest 类
// 该类中有可执行方法 test()
Class<?> clazz = null;
try {
clazz = dexClassLoader.loadClass("com.example.dex_demo.DexTest");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// 获取 com.example.dex_demo.DexTest 类 中的 test() 方法
if (clazz != null) {
try {
// 获取 test 方法
Method method = clazz.getDeclaredMethod("test");
// 获取 Object 对象
Object object = clazz.newInstance();
// 调用 test() 方法
method.invoke(object);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
/**
* 不修改类加载器的前提下 , 运行 Dex 字节码文件中的组件
*
* @param context
* @param dexFilePath
*/
private void startDexActivityWithoutClassLoader(Context context, String dexFilePath) {
// 优化目录
File optFile = new File(getFilesDir(), "opt_dex");
// 依赖库目录 , 用于存放 so 文件
File libFile = new File(getFilesDir(), "lib_path");
// 初始化 DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(
dexFilePath, // Dex 字节码文件路径
optFile.getAbsolutePath(), // 优化目录
libFile.getAbsolutePath(), // 依赖库目录
context.getClassLoader() // 父节点类加载器
);
// 加载 com.example.dex_demo.DexTest 类
// 该类中有可执行方法 test()
Class<?> clazz = null;
try {
clazz = dexClassLoader.loadClass("com.example.dex_demo.MainActivity2");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// 启动 com.example.dex_demo.MainActivity2 组件
if (clazz != null) {
context.startActivity(new Intent(context, clazz));
}
}
/**
* 替换 LoadedApk 中的 类加载器 ClassLoader
*
* @param context
* @param dexFilePath
*/
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private void startDexActivityWithoutReplacedClassLoader(Context context, String dexFilePath) {
// 优化目录
File optFile = new File(getFilesDir(), "opt_dex");
// 依赖库目录 , 用于存放 so 文件
File libFile = new File(getFilesDir(), "lib_path");
// 初始化 DexClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(
dexFilePath, // Dex 字节码文件路径
optFile.getAbsolutePath(), // 优化目录
libFile.getAbsolutePath(), // 依赖库目录
context.getClassLoader() // 父节点类加载器
);
//------------------------------------------------------------------------------------------
// 下面开始替换 LoadedApk 中的 ClassLoader
// I. 获取 ActivityThread 实例对象
// 获取 ActivityThread 字节码类 , 这里可以使用自定义的类加载器加载
// 原因是 基于 双亲委派机制 , 自定义的 DexClassLoader 无法加载 , 但是其父类可以加载
// 即使父类不可加载 , 父类的父类也可以加载
Class<?> ActivityThreadClass = null;
try {
ActivityThreadClass = dexClassLoader.loadClass(
"android.app.ActivityThread");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// 获取 ActivityThread 中的 sCurrentActivityThread 成员
// 获取的字段如下 :
// private static volatile ActivityThread sCurrentActivityThread;
// 获取字段的方法如下 :
// public static ActivityThread currentActivityThread() {return sCurrentActivityThread;}
Method currentActivityThreadMethod = null;
try {
currentActivityThreadMethod = ActivityThreadClass.getDeclaredMethod(
"currentActivityThread");
// 设置可访问性 , 所有的 方法 , 字段 反射 , 都要设置可访问性
currentActivityThreadMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
// 执行 ActivityThread 的 currentActivityThread() 方法 , 传入参数 null
Object activityThreadObject = null;
try {
activityThreadObject = currentActivityThreadMethod.invoke(null);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
// II. 获取 LoadedApk 实例对象
// 获取 ActivityThread 实例对象的 mPackages 成员
// final ArrayMap<String, WeakReference<LoadedApk>> mPackages = new ArrayMap<>();
Field mPackagesField = null;
try {
mPackagesField = ActivityThreadClass.getDeclaredField("mPackages");
// 设置可访问性 , 所有的 方法 , 字段 反射 , 都要设置可访问性
mPackagesField.setAccessible(true);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
// 从 ActivityThread 实例对象 activityThreadObject 中
// 获取 mPackages 成员
ArrayMap mPackagesObject = null;
try {
mPackagesObject = (ArrayMap) mPackagesField.get(activityThreadObject);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
// 获取 WeakReference<LoadedApk> 弱引用对象
WeakReference weakReference = (WeakReference) mPackagesObject.get(this.getPackageName());
// 获取 LoadedApk 实例对象
Object loadedApkObject = weakReference.get();
// III. 替换 LoadedApk 实例对象中的 mClassLoader 类加载器
// 加载 android.app.LoadedApk 类
Class LoadedApkClass = null;
try {
LoadedApkClass = dexClassLoader.loadClass("android.app.LoadedApk");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// 通过反射获取 private ClassLoader mClassLoader; 类加载器对象
Field mClassLoaderField = null;
try {
mClassLoaderField = LoadedApkClass.getDeclaredField("mClassLoader");
// 设置可访问性
mClassLoaderField.setAccessible(true);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
// 替换 mClassLoader 成员
try {
mClassLoaderField.set(loadedApkObject, dexClassLoader);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
//------------------------------------------------------------------------------------------
// 加载 com.example.dex_demo.DexTest 类
// 该类中有可执行方法 test()
Class<?> clazz = null;
try {
clazz = dexClassLoader.loadClass("com.example.dex_demo.MainActivity2");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
// 启动 com.example.dex_demo.MainActivity2 组件
if (clazz != null) {
context.startActivity(new Intent(context, clazz));
}
}
}
三、执行结果
执行结果 :
2021-12-12 11:49:06.816 26145-26145/com.example.classloader_demo I/MainActivity: MainActivity ClassLoader : dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.example.classloader_demo-Ij2qh4a32d4VghdA5YWXdA==/base.apk"],nativeLibraryDirectories=[/data/app/com.example.classloader_demo-Ij2qh4a32d4VghdA5YWXdA==/lib/arm64, /system/lib64]]]
2021-12-12 11:49:06.816 26145-26145/com.example.classloader_demo I/MainActivity: MainActivity Parent ClassLoader : java.lang.BootClassLoader@6457c5
2021-12-12 11:49:06.817 26145-26145/com.example.classloader_demo I/MainActivity: 开始拷贝文件 dexPath : /data/user/0/com.example.classloader_demo/files/classes2.dex
2021-12-12 11:49:06.879 26145-26145/com.example.classloader_demo I/HSL: classes2.dex 文件拷贝完毕
2021-12-12 11:49:07.939 26145-26145/com.example.classloader_demo I/lassloader_dem: The ClassLoaderContext is a special shared library.
2021-12-12 11:49:07.942 26145-26145/com.example.classloader_demo I/DexTest: DexTest : Hello World!!!
2021-12-12 11:49:07.943 26145-26145/com.example.classloader_demo I/lassloader_dem: The ClassLoaderContext is a special shared library.
2021-12-12 11:49:07.945 26145-26145/com.example.classloader_demo W/lassloader_dem: Accessing hidden method Landroid/app/ActivityThread;->currentActivityThread()Landroid/app/ActivityThread; (light greylist, reflection)
2021-12-12 11:49:07.945 26145-26145/com.example.classloader_demo W/lassloader_dem: Accessing hidden field Landroid/app/ActivityThread;->mPackages:Landroid/util/ArrayMap; (light greylist, reflection)
2021-12-12 11:49:07.945 26145-26145/com.example.classloader_demo W/lassloader_dem: Accessing hidden field Landroid/app/LoadedApk;->mClassLoader:Ljava/lang/ClassLoader; (light greylist, reflection)
2021-12-12 11:49:07.995 26145-26145/com.example.classloader_demo W/lassloader_dem: Accessing hidden method Landroid/graphics/Insets;->of(IIII)Landroid/graphics/Insets; (light greylist, linking)
2021-12-12 11:49:08.008 26145-26220/com.example.classloader_demo I/Adreno: QUALCOMM build : 6fb5a5b, Ife855c4895
Build Date : 08/21/18
OpenGL ES Shader Compiler Version: EV031.25.00.00
Local Branch : Googledrop_0815
Remote Branch :
Remote Branch :
Reconstruct Branch :
2021-12-12 11:49:08.008 26145-26220/com.example.classloader_demo I/Adreno: Build Config : S L 4.0.10 AArch64
2021-12-12 11:49:08.011 26145-26220/com.example.classloader_demo I/Adreno: PFP: 0x005ff110, ME: 0x005ff066
2021-12-12 11:49:08.013 26145-26220/com.example.classloader_demo I/ConfigStore: android::hardware::configstore::V1_0::ISurfaceFlingerConfigs::hasWideColorDisplay retrieved: 1
2021-12-12 11:49:08.014 26145-26220/com.example.classloader_demo I/ConfigStore: android::hardware::configstore::V1_0::ISurfaceFlingerConfigs::hasHDRDisplay retrieved: 0
2021-12-12 11:49:08.014 26145-26220/com.example.classloader_demo I/OpenGLRenderer: Initialized EGL, version 1.4
2021-12-12 11:49:08.015 26145-26145/com.example.classloader_demo W/ActivityThread: handleWindowVisibility: no activity for token android.os.BinderProxy@463dd0
2021-12-12 11:49:08.036 26145-26145/com.example.classloader_demo I/MainActivity2: com.example.dex_demo.MainActivity2 onCreate 执行成功 !!!
启动的 DEX 中的 Activity 组件如下 :
package com.example.dex_demo;
import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity2 extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i("MainActivity2", "com.example.dex_demo.MainActivity2 onCreate 执行成功 !!!");
}
}
在上面的日志中 , 成功打印出了 com.example.dex_demo.MainActivity2 onCreate 执行成功 !!!
内容 , 说明 com.example.dex_demo.MainActivity2
执行成功 ;
四、博客资源
CSDN 下载 : https://download.csdn.net/download/han1202012/61623486
相关文章
- android onresume方法,非静态方法’onResume’Android Studio
- android 读取本地数据库db文件(Android sqlite)
- android退出app代码,Android应用退出代码各种方式
- android bioset 进程,kthrotlds(WatchDogs变种)查杀方法「建议收藏」
- android 短信验证码的实现
- android toast全屏,Android Toast实现全屏显示
- 【Android RTMP】RTMP 数据格式 ( FLV 视频格式分析 | 文件头 Header 分析 | 标签 Tag 分析 | 视频标签 Tag 数据分析 )
- 【Android 热修复】热修复原理 ( 热修复框架简介 | 将 Java 字节码文件打包到 Dex 文件 )
- 【Android 逆向】类加载器 ClassLoader ( 使用 DexClassLoader 动态加载字节码文件 | 拷贝 DEX 文件到内置存储 | 加载并执行 DEX 字节码文件 )
- 【Android 逆向】启动 DEX 字节码中的 Activity 组件 ( DEX 文件准备 | 拷贝资源目录下的文件到内置存储区 | 配置清单文件 | 启动 DEX 文件中的组件 | 执行结果 )
- 【错误记录】Android Studio 编译时 lint 检查报错 ( WARNING: DSL element ‘android.dataBinding.enabled‘ is obsolet )
- 【Android 应用开发】使用蒲公英 SDK 收集崩溃日志信息 ( 导入依赖 | 申请 Key | 集成代码 | 清单文件配置 | 手动上传日志 | 手动检查更新 )
- 【Android Gradle 插件】Gradle 构建机制 ① ( 空白工程 Gradle 构建文件 | IntelliJ IDEA 工程构建文件 | Android Studio 工程构建文件 )
- 【Kotlin 协程】Flow 异步流 ② ( 使用 Flow 异步流持续获取不同返回值 | Flow 异步流获取返回值方式与其它方式对比 | 在 Android 中使用 Flow 异步流下载文件 )
- 【Android OpenCV】Visual Studio 创建支持 OpenCV 库的 CMake 工程 ③ ( CMake 工程中配置 OpenCV 库文件 | 拷贝 OpenCV 函数库文件 )
- Android开发中遇到的问题(三)——eclipse创建android项目无法正常预览布局文件详解手机开发
- [android] 采用pull解析xml文件详解手机开发
- android 打造不同的Seekbar详解手机开发
- Android动态替换Application实现详解手机开发
- Android中使用pull解析器操作xml文件的解决办法
- Android实现歌曲播放时歌词同步显示具体思路
- android封装抓取网页信息的实例代码