七、Android 唯一标识符
唯一标识符可用于一系列任务,从允许用户设置广告偏好、唯一标识用户,到标识特定设备。当涉及到由 Android 权限和 Google Play 开发者政策强制执行的唯一标识符的使用时,也存在强烈的隐私问题。
为了解决用户隐私问题,从 Android 10 (API 级别 29)开始,在如何从 Android 应用中访问硬件唯一标识符方面有了很大的变化。也就是说,应用必须是设备或配置文件的所有者,拥有特殊的运营商权限,或拥有READ_PRIVILEGED_PHONE_STATE特权权限,才能访问不可重置的设备标识符。READ_PRIVILEGED_PHONE_STATE permission仅适用于使用设备平台(系统应用)密钥 1 签名的应用。
Google Play 广告 ID
截至 Android KitKat (API level 4.4),Google Play 广告 ID 2 可用于唯一识别设备用户。当在设备上可用时,出于广告目的使用任何其他设备唯一标识符都是违反 Google Play 开发者计划政策的。广告 ID 对最终用户的好处是它既可重置又可用于定制个性化广告。返回的 ID 的一个例子是9fdbfa02-7f28-422e-944e-f02393a9360e的字符串表示。由于广告 ID 由 Google Play 服务 API 提供,这意味着它只能在具有 Google Play 服务的设备上使用。
可以通过将以下库路径添加到 build.gradle 文件的 dependencies 标签来使用广告 ID:
implementation 'com.google.android.gms:play-services-ads:7.0.0'
检索广告 ID(这不能在主线程上完成——这个例子使用了一个 AsyncTask 详见 第九章:
void getAdvertisingID(final Context context){
AsyncTask.execute(new Runnable() {
@Override
public void run() {
Info adInfo = null;
try {
adInfo = AdvertisingIdClient.getAdvertisingIdInfo(context);
} catch (IOException e) {
e.printStackTrace();
} catch (GooglePlayServicesNotAvailableException e) {
e.printStackTrace();
} catch (GooglePlayServicesRepairableException e) {
e.printStackTrace();
}
String adId = adInfo != null ? adInfo.getId() : null;
Log.v("Advertising ID",adId);
}
});
}
Android ID(安全设置 Android ID–SSAID)
这是一个唯一的 64 位数字(例如,ce79870fa5cbfb56),是 Android 在广告之外的活动中识别设备用户的首选方法。这个唯一标识符在所有版本的 Android 上都可用,不需要任何额外的权限,并且作为设备出厂重置的一部分被重置。
访问 安卓 ID :
Log.v("Android ID",Settings.Secure.getString(this.getContentResolver(), Settings.Secure.ANDROID_ID));
SIM 序列号
SIM 序列号用于国际识别,通常分为 19 位数字 3 。这可以分解为两位数字的电信 ID、两位数字的国家代码、两位数字的网络代码、四位数字的生产月份和年份、两位数字的交换机配置代码、六位数字的 SIM 号码和一个校验位。
SIM 序列号在 Android Pie (API 级别 28)及以下版本的 Android 上可用,在 Android 10 及以上版本中受限于READ_PRIVILEGED_PHONE_STATE权限。使用 Android 10 之前的 SIM 序列号需要READ_PHONE_STATE运行时权限。
检索 SIM 序列号:
TelephonyManager telephonyManager = (TelephonyManager) getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
Log.v("SIM Serial Number", telephonyManager.getSimSerialNumber());
电话号码
为使用电话的设备编写应用时,电话号码可以用作唯一标识符。该标识符在所有版本的 Android 上都可用,并与设备上的 SIM 卡(如果有)绑定,需要READ_PHONE_STATE或READ_PRIVILEGED_PHONE_STATE permission才能访问。
正在检索 的电话号码 :
TelephonyManager telephonyManager = (TelephonyManager) getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
Log.v("Phone Number", telephonyManager.getLine1Number());
IMEI 和美国
为了唯一识别设备和防止被盗,所有移动设备都被指定了一个 IMEI 或 MEID 号码。取决于设备的网络将取决于设备具有哪个标识符。如果在 GSM(全球移动系统)系统上,设备将具有 IMEI 号码,如果在 CDMA(码分多址)系统上,设备将具有 MEID 号码。两者的主要区别在于 IMEI 是 14 位数字,而 MEID 是 15 位数字。与其他硬件标识符类似,IMEIs 和 MEIDs 都可以在 Android 10 之前的设备上使用,如果应用具有READ_PHONE_STATE权限,就可以读取这些标识符。但是,从 Android 10 开始,一个应用需要READ_PRIVILEGED_PHONE_STATE 。
Pre 及包含 Android N (25)访问:
TelephonyManager telephonyManager = (TelephonyManager) getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
Log.v("Device ID", telephonyManager.getDeviceId());
贴吧(含)安卓 O (26):
TelephonyManager telephonyManager = (TelephonyManager) getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE);
Log.v("IMEI", telephonyManager.getImei());
Log.v("MEID", telephonyManager.getMeid());
Footnotes 1
" Android 安全性第一部分:应用签名和权限……"5 月 4 日。2015, https://boundarydevices.com/android-security-part-1-application-signatures-permissions/ 。5 月 11 日访问。2020.
2
"广告 ID -播放控制台帮助-谷歌支持." https://support.google.com/googleplay/android-developer/answer/6048248?hl=en-GB 。5 月 21 日访问。2020.
3
“我的 SIM 卡有序列号吗?和 IMEI 一样吗?." https://justaskthales.com/us/does-my-sim-card-have-serial-number-it-same-imei/ 。5 月 11 日访问。2020.
八、混淆和加密
记录
Android 中的Log类可用于在 logcat 中创建日志消息(可通过adb logcat命令访问);这些日志有几种不同的级别,它们是
log . wtf--“多么可怕的失败”(被视为极端错误)
Log.e -错误
Log.w -警告
Log.i -信息
Log.d -调试
Log.v -详细
如上所述,这些日志消息可以通过 logcat 读取。Logcat 是 Android 的日志系统,记录从系统消息到堆栈跟踪的所有内容。应用可以通过使用Log类写入 logcat,反过来,这些消息可以通过使用adb logcat命令或在 Android Studio 等程序中查看。
无论您选择哪种级别,所有日志级别都将显示在 logcat 中。例如,以下日志程序代码在指定 debug 的同时,将被记录到 logcat 中,而不考虑构建类型(即,它将被记录在发布构建中)。同样值得记住的是,调试日志消息将被编译到发布应用中。例如,下面我们可以看到 Java 中的日志消息和 Smali(dal vik 字节码的可读表示)中的反汇编发布版本之间的比较。
在 Java 中 :
Log.d(TAG, "I am a normal debug log message");
In Smali:
iget-object p1, p0, Lcom/example/logger/MainActivity;->TAG:Ljava/lang/String;
const-string v0, "A log using is loggable"
invoke-static {p1, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I
标准测井
开发人员不想使用标准日志记录有几个原因。这些归结为安全性和性能,其中日志应该对恶意参与者隐藏,并且应该在最终用户的设备上避免日志泛滥。
标准日志:
Log.d(TAG, "I am a normal debug log Message");
最终常量变量
限制发布代码中日志语句数量的一种支持方式是使用 Gradle prebuild 生成的 Gradle BuildConfig文件。生成该文件时,如果构建调试版本,将下面一行设置为true,如果构建发布版本,将下面一行设置为false。
build config 文件中的调试值:
public static final boolean DEBUG = Boolean.parseBoolean("true");
实现调试常量:
if (BuildConfig.DEBUG){
Log.d(TAG,"This is a log that won't be compiled in a release build.");
}
当为了发布而构建并设置为 false 时,Java 编译器会发现最终变量不可能为 true,因此不会编译 if 语句中的代码。这意味着日志不会显示在 logcat 中,也意味着日志字符串不会像普通的日志消息一样存在于应用的源代码中。
如果不使用 Gradle,也可以实现与使用BuildConfig.DEBUG类似的效果。这可以通过使用一个最终布尔值来完成,在调试时将其设置为true,在发布版本中设置为false。
设置一个 自定义调试常数 :
final boolean SHOULD_LOG = false;
if (SHOULD_LOG){
Log.d(TAG," A log that should never happen...");
}
使用。可记录
检查是否应该显示日志消息的另一种方法是使用内置在Log类中的.isLoggable方法。该方法检查为特定标记设置的日志级别(应用的默认设置是INFO)。日志级别在一个层次结构中工作,如本节顶部所列。这意味着如果日志级别设置为 Verbose,那么它上面的所有级别也将是true。与使用BuildConfig,不同的是,这个值可以通过编程来改变,这个字符串将被编译到应用的代码库中。
log . is logtable 示例:
if (Log.isLoggable(TAG,Log.DEBUG)){
Log.d(TAG,"A log using is loggable");
}
这个日志级别可以通过 shell 使用来设置
setprop log.tag.
动态检查是否可调试
这里讨论的限制写入 logcat 的日志数量的最后一种技术是通过动态检查应用是否处于调试状态。如上所述,由于该值可以改变,日志和字符串将被编译到构建的发布应用中。
动态检查应用是否处于调试状态的示例:
boolean isDebuggable = ( 0 != ( getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE ) );
if (isDebuggable){
Log.d(TAG,"This log will check programmatically if the app is debuggable.");
}
阿帕尔德
最常见的 Android 混淆工具是 ProGuard,DexGuard 1 是高级替代工具。ProGuard 分析和优化的是 Java 字节码,而不是直接的 Java/Kotlin 代码库。ProGuard 实现了一组技术 2 ,它们是:
收缩 -识别并删除不可达或未使用的死代码。包括类、字段、方法和属性。
优化器——对代码和代码流进行优化,以改变性能。
Obfuscator——将代码库的某些方面(例如,类、字段和方法)重命名为故意模糊和无意义的名称。
预验证器——对字节码执行预验证检查,如果检查成功,类文件会用预验证信息进行注释。这是 Java Micro Edition 和 Java 6 及更高版本所必需的。
启用 ProGuard
将gradle.build文件中的buildTypes标签编辑为minifyEnabled true。
例如:
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
ProGuard 映射文件
在使用 Gradle 进行发布构建时,遵循上述步骤之后,Java 字节码将已经被 ProGuard 分析过了。ProGuard 将提供所经历阶段的日志文件。它保存在应用项目根目录的以下相对路径中。
映射文件相对位置:
app/build/outputs/mapping/release/mapping.txt
该文件显示了 ProGuard 已经实现的更改。下面是该文件一部分的示例。在这个例子中可以看到,MainActivity中的函数showString已经被允许列出,而 MainActivity 中另一个名为 loadMe 的函数还没有被允许列出,现在被重命名为n。
映射文件示例:
com.example.java_dexloadable.MainActivity -> com.example.java_dexloadable.MainActivity:
java.lang.String loadMe() -> n
1:1:java.lang.String com.example.java_dexloadable.StringsClass.stringGetter(int):0:0 -> showString
1:1:void showString(android.content.Context,int):0 -> showString
2:2:void showString(android.content.Context,int):0:0 -> showString
如果一个函数或类不在这个映射中,那么它已经被缩小了(意味着它没有在代码中被使用),或者它没有被混淆(由于被允许列出)。
ProGuard 允许列表
默认情况下,ProGuard 会收缩、优化和混淆 Java 字节码中的所有内容。这可以通过编辑位于应用目录根目录下app\proguard-rules.pro的 ProGuard 文件来控制。该文件可以重命名和移动,并在gradle.build文件中指定。
下面的示例规则允许-列出类 com.example.java_dexloadable.MainActivity 中的 showString 函数。这里你需要指定类和函数的访问级别(公共的,私有的,包私有的,等等)。)以及函数的参数:
-keep class com.example.java_dexloadable.MainActivity {
public showString(android.content.Context, int);
}
下面的例子也是一样;然而,在这个例子中, MainActivity 中的所有函数都被允许列出:
-keep class com.example.java_dexloadable.MainActivity {
public *;
}
不同类型的保留
在前面的两个示例中,使用了 keep 关键字。有几种不同类型的 3 关键字。这些总结在表 8-1 中。
表 8-1
保留的程序类型
| |
没有规则
|
-保持
|
-保留类成员
|
-保留姓名
|
| --- | --- | --- | --- | --- |
| 缩班 | -好的 | x | -好的 | -好的 |
| 收缩成员 | -好的 | x | x | -好的 |
| 混淆类 | -好的 | x | -好的 | x |
| 混淆成员 | -好的 | x | x | x |
入口点
ProGuard 自动将允许列表(也称为白名单)入口点指向一个应用(例如,MAIN或LAUNCHER类别的活动)。重要的是要记住,作为反射的一部分使用的入口点不会自动允许列出,所以如果使用 refection,这些入口点必须手动允许列出。然而,这将最小化混淆的有效性,因为纯文本组件的频率会更高。由 ProGuard 自动添加到允许列表的入口点通常包括具有 main 方法、applets、MIDlets、activities 等的类。这也包括调用本机 C 代码的类。
示例规则
在下面的例子中,Java 包名是java_dexloadable,,所有的规则都被添加到了Proguard-rules.pro文件中。
保留(允许列表)MainActivity 类中的所有方法:
-keep class com.example.java_dexloadable.MainActivity {
public *;
}
保留(允许列出)MainActivity 类中的 showString 函数以及 MainActivity 类本身:
-keep class com.example.java_dexloadable.MainActivity {
public showString(android.content.Context, int);
}
保留(允许列表)顶层包下的所有内容(不应使用):
-keep class com.example.java_dexloadable.** { *; }
保留(允许列出)函数字符串,但不保留类字符串 class 本身:
-keepclassmembers class com.example.java_dexloadable.StringsClass {
public stringGetter(int);
}
将整个包重新打包成一个根:
-repackageclasses
不执行程序收缩步骤:
--dontshrink
公钥/证书锁定
公钥锁定允许应用将特定的加密公钥与给定的 web 服务器相关联。这反过来用于降低中间人攻击的可能性。
执行公钥锁定时,需要所连接的 web 服务器的公钥。
有两种相当简单的方法可以做到这一点——要么使用下面的 openssl 命令,要么使用下面的代码并从错误消息中提取公钥:
openssl x509 -in cert.crt -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
在下面的代码中,使用前面消息中的公钥散列作为您的散列。在 Android 中,联网不能在主线程上执行,因此它需要在长期运行的服务、异步任务或线程中执行(详见第九章)。
将以下依赖项添加到 build.gradle 文件中,因为这个示例使用 OkHTTP 库。还要确保应用具有互联网权限,并且没有在主线程上进行联网:
implementation("com.squareup.okhttp3:okhttp:4.9.0")
证书锁定示例:
String hostname = "google.com";
CertificatePinner certPinner = new CertificatePinner.Builder()
.add(
hostname,
"sha256/MeCugOOsbHh2GNsYG8FO7wO7E4rjtmR7o0LM4iXHJlM="
)
.build();
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.certificatePinner(certPinner)
.build();
HttpUrl.Builder urlBuilder = HttpUrl.parse("https://"+hostname).newBuilder();
String url = urlBuilder.build().toString();
MediaType JSON = MediaType.parse("application/json; charset=utf-8");
RequestBody body = RequestBody.create(JSON, "{\"test\":\"testvalue\"}");
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
Log.v(TAG,request.toString());
Response response = null;
try {
response = okHttpClient.newCall(request).execute();
ResponseBody jsonData = response.body();
Log.v(TAG, jsonData.toString());
} catch (IOException e) {
e.printStackTrace();
}
return Result.success();
AES 加密
AES 使用对称算法,这意味着加密和解密使用相同的密钥。下面是一个用 Java 实现 AES-256 加密的轻量级例子。
下面是 Java 中 AES-256 加密方法的一个例子:
try {
Cipher cipher = null;
cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
KeyGenerator keygen = null;
keygen = KeyGenerator.getInstance("AES");
keygen.init(256);
SecretKey key = keygen.generateKey();
String plainTextString = "I am a plain text";
String cipherTextAsString = "N/A";
String newPlainTextAsString = "N/A";
byte[] plainText = plainTextString.getBytes();
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] cipherText = new byte[0];
cipherText = cipher.doFinal(plainText);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
cipherTextAsString = new String(cipherText, StandardCharsets.UTF_8);
}
IvParameterSpec iv = new IvParameterSpec(cipher.getIV());
cipher.init(Cipher.DECRYPT_MODE, key, iv);
byte[] newPlainText = cipher.doFinal(cipherText);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
newPlainTextAsString = new String(newPlainText, StandardCharsets.UTF_8);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Log.v(getApplicationContext().getPackageName(), "The plaintext '" + plainTextString + "' encrypted is " + Base64.getEncoder().encodeToString(cipherText) + " and decrypted is '" + newPlainTextAsString);
}
}catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
} catch (InvalidAlgorithmParameterException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
}
Footnotes 1
" dex guard vs . ProGuard | guard square . "2017 年 4 月 13 日, https://www.guardsquare.com/en/blog/dexguard-vs-proguard 。于 5 月 12 日访问。2020.
2
"保护手册|简介|保护广场." https://www.guardsquare.com/en/products/proguard/manual/introduction 。5 月 11 日访问。2020.
3
“区分不同的程序...-jebware . com " 2017 年 11 月 14 日, https://jebware.com/blog/?p=418 。5 月 11 日访问。2020.
九、服务、启动器和组件
长期运行的服务
在 Android 中,有几种方法可以在当前活动结束后一次性或周期性地运行“作业”。这里讨论的技术将在以下两种类型之间变化:
将强绑定到当前活动,比如AsyncTask,意思是如果活动结束,那么任务被垃圾回收。
没有将强绑定到当前活动,例如JobScheduler,其中任务甚至在父活动本身被垃圾收集后仍继续。
扳机
触发器是长期运行的服务如何分配任务和运行的机制,它们是服务的初始入口点。大多数触发器将为服务提供某种形式的持久性。这些触发可以在表 9-1 中看到。
表 9-1
长期运行的服务触发器
|
引发
|
描述
|
| --- | --- |
| 目的 | 从代码中直接触发,通常来自用户交互。 |
| 手机闹钟服务 | 在未来的特定时间触发,一次性或重复发生。 |
| 广播接收器 | 收到特定广播消息时触发。例如,BootComplete、PowerConnected或自定义接收器。 |
| 传感器回调 | 收到特定传感器值时触发。 |
| 工作管理器 | 根据 API 等级使用JobScheduler或AlarmManager和BootComplete。 |
| 作业调度程序 | 从 API level 21 (Android L)开始,AlarmManager的更智能实现允许根据网络、空闲和充电状态运行。也瞌睡顺从。 |
服务
有几种类型的“服务”可以运行,为应用提供长期运行的后台工作。这些可以在表 9-2 中看到。
表 9-2
长期运行的服务类型
|
服务
|
描述
|
| --- | --- |
| 工作管理器 | 封装启动器和服务元素(考虑到底层实现被抽象时的向后兼容性)。并发工作之间的最小间隔为 15 分钟,每个工人最多只能运行 10 分钟。WorkManagers重启后也会自动保持(如果许可的话,使用BootComplete广播接收器)。 |
| 作业调度程序 | 封装了启动器和服务元素。这些都是高度可定制的,可以根据网络、空闲和电池状态等环境因素运行工作。除此之外,它们还可以被定义为以特定的时间间隔或特定的时间段运行,并在重启后持续运行(如果许可可用,使用BootComplete广播接收器)。工作之间的最小间隔是 15 分钟。 |
| 服务 | 有许多类型的服务;然而,最常见的一种是意向服务,其中工作请求按顺序运行。后续请求(对服务的意图)会一直等到第一个操作完成。 |
| 线 | 主要用于不想在 UI 线程上工作的作业(例如网络),但是,只要它们的父线程没有被杀死,就可以用于长期运行的后台工作。线程被绑定到父应用。 |
| 异步任务 | 绑定到父Activity的生命,意味着如果活动结束或者被杀死,那么AsyncTask也是。这样做的好处是更容易将工作从AsyncTask推回到 UI 线程。 |
| 前台服务 | 从 API 26 开始,后台服务(如意向服务)被限制为仅在应用处于前台时运行。取而代之的是前台服务,在前台服务运行时,它们必须向用户显示一个持续的通知(例如,一个音乐应用在播放时显示一个音乐播放器)。 |
IntentService、AlarmManager 和 BootComplete
如前所述,IntentService 1 是一种不能直接与 UI 交互的服务。IntentService中的工作请求按顺序运行,请求将一直等待,直到当前操作完成。在IntentService上运行的操作不能被中断。
一个AlarmManager 2 是 Android 中的一种机制,允许代码在后台线程中延迟和继续运行。一个AlarmManager可以被配置为在未来的特定时间以预先配置的时间间隔运行。AlarmManager还有一个setAlarmClock选项,允许它在设备处于低功耗空闲或打盹模式时触发。
可以设置一个BroadcastReceiver来监听引导完成意图 3 ,如第四章所述。该意图在设备重启后启动时发送。反过来,在接收到BootComplete意图后启动AlarmManager将意味着设备重启后后台服务继续运行。
从 Android Oreo 8(API 26 级)开始,Android 服务不再能从后台进程启动。 4 这意味着在 Android 8+中,要使用JobSchedulers或前台服务。请记住,虽然该功能仅适用于针对 Android 8+的应用,但它可以由用户在设置 5 页面中启用,这也对服务如何在后台运行提出了许多额外的限制。
下面的函数将设置一个 报警管理器 按照 waitBeforeRepeatInMinutes 参数的定义每 x 分钟重复一次:
public void startPeriodicWork(long waitBeforeRepeatInMinutes){
// Construct an intent that will execute the AlarmReceiver
Intent intent = new Intent(context, AlarmReceiver.class);
// Create a PendingIntent to be triggered when the alarm goes off
final PendingIntent pIntent = PendingIntent.getBroadcast(context, AlarmReceiver.REQUEST_CODE,
intent, PendingIntent.FLAG_UPDATE_CURRENT);
// Setup periodic alarm every every half hour from this point onwards
long firstMillis = System.currentTimeMillis(); // alarm is set right away
AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
// First parameter is the type: ELAPSED_REALTIME, ELAPSED_REALTIME_WAKEUP, RTC_WAKEUP
// Interval can be INTERVAL_FIFTEEN_MINUTES, INTERVAL_HALF_HOUR, INTERVAL_HOUR, INTERVAL_DAY
if (alarm != null) {
alarm.setInexactRepeating(AlarmManager.RTC_WAKEUP, firstMillis,
waitBeforeRepeatInMinutes * 60 * 1000, pIntent);
}
}
接下来,创建 AlarmManager BroadcastReceiver 类。在 Android manifest 中设置 process 属性,这样如果应用关闭了 6 ,它将继续保持活动状态。作为其中的一部分,将 BroadcastReceiver 添加到 AndroidManifest.xml 文件中。
android:process=":remote" /> 在 警报接收者 中增加以下内容。java: public class AlarmReceiver extends BroadcastReceiver { public static final int REQUEST_CODE = 12345; // Triggered by the Alarm periodically (starts the service to run task) @Override public void onReceive(Context context, Intent intent) { int tid = Process.myTid(); Log.v("TaskScheduler", "Started Alarm Receiver with tid "+ tid); TaskManager taskManager = new TaskManager(context); taskManager.oneOffTask(); } } 接下来,为 BootComplete 添加 BroadcastReceiver。将以下内容添加到 AndroidManifest.xml 文件中:
然后创建BootReceiver.java类:
public class BootReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
int tid = Process.myTid();
Log.v("TaskScheduler", "Started Boot Complete Receiver with tid "+ tid);
TaskManager taskManager = new TaskManager(context);
taskManager.startPeriodicWork(5);
}
}
最后创造出IntentService;为此,创建一个名为 ServiceManager.java: 的文件
public class ServiceManager extends IntentService {
public ServiceManager() {
super("ServiceTest"); //Used to name the worker thread, important only for debugging.
}
@Override
protected void onHandleIntent(Intent intent) {
int tid = Process.myTid();
Log.v("TaskScheduler", "Started Service with tid "+ tid);
String val = intent.getStringExtra("foo");
//todo Add the work to be performed here.
}
}
将此服务添加到 AndroidManifest.xml 文件:
android:exported="false"/> 前台服务 Android 中有一系列不同类型的服务 7 ,从启动的服务(运行在 UI 线程中)到IntentService(运行在自己的线程中)再到绑定的服务(只要有一个活动绑定到它就运行)。 截至 Android 8 Oreo (API 26),Android 应用运行后台服务有限制,除非应用本身在前台。在这种情况下,应该使用startForegroundService()方法,而不是使用context.startService()方法。在此之后,服务有 5 秒钟的时间向用户显示通知并调用startForeground(1, notification)方法,该方法在服务期间一直存在,直到stopForeground(true)和stopSelf()方法被调用。前台服务像任何其他服务一样扩展了Service类,并且除了遵循前面的规则和限制之外,以相同的方式进行操作。 应该使用如下代码来标识应该使用后台服务还是前台服务: Intent intent = new Intent(context, ServiceManager.class); //replace with an appropriate intent if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { context.startForegroundService(intent); }else{ context.startService(intent); } 由于通知是前台服务启动的一部分,这意味着它的副产品是建立一个通知通道 8 (针对 Android 8.0 - API 级别 26 及以上)。添加通知通道是为最终用户提供细粒度访问的一种方式,允许他们更改通知设置并决定应用中的哪些通知通道应该可见。 以下显示了设置通知通道的示例: public static void createNotificationChannel(Context context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { int importance = NotificationManager.IMPORTANCE_DEFAULT; NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance); channel.setDescription(CHANNEL_DESC); NotificationManager notificationManager = context.getSystemService(NotificationManager.class); if (notificationManager != null) { notificationManager.createNotificationChannel(channel); } } } 发送通知: Intent notificationIntent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); Notification notification = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID) .setContentTitle("Notification Title") .setContentText("Notification Text") .setSmallIcon(R.mipmap.ic_launcher) .setContentIntent(pendingIntent) .build(); startForeground(1, notification); } 从 Android 9 (API level 28)开始,除了如下将您的服务添加到 Android 清单中,您还需要添加 FOREGROUND_SERVICE 权限: android:exported="false"/> 作业调度程序 从 Android 5 L (API 21)开始,当设备有更多可用资源时,作业调度器作为一种任务批处理作业的方式被引入。作为一个整体,多项工作可以由JobSchedulers ,来完成,他们会将这些任务分成几批。这意味着所分配的工作可能不会按预期执行;然而,它将在该时间前后发生(例如,被指示每 15 分钟执行一次的任务可能在一次运行的 14 分钟后执行,而在另一次运行的 16 分钟后执行)。 JobSchedulers 最强大的功能之一是,如果设置或未设置特定的标准,例如,没有网络连接、电池正在充电或设备处于空闲状态,它们允许工作延期。还有一个选项是用setPeriodic和setPersisted来运行周期性的工作,并在重启后继续运行(如果应用拥有 RECEIVE_BOOT_COMPLETED 权限)。setOverrideDeadline选项还允许在运行一次性工作的最大时间内,允许在强制运行工作之前等待一段时间。 添加以下内容。如果不想要周期工作器,则删除 setPeriodic 和 setPersisted 标记: @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public void startJobScheduler(){ ComponentName serviceComponent = new ComponentName(context, JobSchedulerManager.class); JobInfo.Builder builder = new JobInfo.Builder(0, serviceComponent); //builder.setMinimumLatency(1 * 1000); // wait at least /Can't call setMinimumLatency() on a periodic job/ //builder.setOverrideDeadline(3 * 1000); // maximum delay //Can't call setOverrideDeadline() on a periodic job. builder.setPeriodic(1000); //runs over time builder.setPersisted(true); // persists over reboot //builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED); // require unmetered network //builder.setRequiresDeviceIdle(true); // device should be idle //builder.setRequiresCharging(false); // we don't care if the device is charging or not JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); if (jobScheduler != null) { jobScheduler.schedule(builder.build()); } } 然后做一个名为 JobSchedulerManager.java 的类: @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public class JobSchedulerManager extends JobService { @Override public boolean onStartJob(JobParameters jobParameters) { int tid = Process.myTid(); Log.v("TaskScheduler", "Started Job Scheduler with tid "+ tid); //todo perform work here // returning false means the work has been done, return true if the job is being run asynchronously return true; } @Override public boolean onStopJob(JobParameters params) { return false; } } 将下面的类添加到 AndroidManifest.xml 文件中: android:permission="android.permission.BIND_JOB_SERVICE"/> 一个JobScheduler服务必须用先前的权限来保护,这样只有具有该权限的应用才能给该服务分配任务。如果在清单中声明了作业服务,但未使用此权限进行保护,则该服务将被系统忽略。 工作经理 如 Android 文档所述, 9 WorkManagers从 API 14 开始向后兼容。API 14-22 上使用了BroadcastReceiver和AlarmManager的组合,API 23+上使用了JobScheduler。使用WorkManager而不是AlarmManager的一个主要缺点是对它们的运行时间有限制(这是从在幕后使用JobScheduler继承而来的);这包括WorkManager运行时间不得超过 10 分钟,并且在当前工作开始至少 15 分钟后才能执行另一项连续工作。这样做的原因是为了遵守瞌睡限制。 使用工作管理器时,将以下依赖项添加到 gradle.build 文件中: def work_version = "2.3.3" // (Java only) implementation "androidx.work:work-runtime:$work_version" // Kotlin + coroutines implementation "androidx.work:work-runtime-ktx:$work_version" 下面的代码执行任务并启动一个工作管理器: PeriodicWorkRequest work = new PeriodicWorkRequest.Builder( com.example.taskscheduler.managers.WorkManager.class, 15, TimeUnit.MINUTES) .build(); //update path to match your created WorkManager.java class WorkManager.getInstance().cancelAllWork(); WorkManager.getInstance().enqueue(work); 最后创建一个名为 WorkManager.java 的类: public class WorkManager extends Worker { Context context; public WorkManager(@NonNull Context context, @NonNull WorkerParameters workerParams) { super(context, workerParams); this.context = context; } @Override public Result doWork() { int tid = Process.myTid(); Log.v("TaskScheduler", "Worker started with tid "+ tid); // Todo run your work here. return Result.success(); } } 穿线 在main应用线程(由系统为每个应用创建)之外,一个应用可以有多个额外的执行线程。然而,线程的设置相当简单,因为AsyncTasks被绑定到它们的父线程(通常是一个Activity)的生命中。在这种情况下,如果线程的父线程被破坏(即,被用户从任务堆栈中移除),则该线程受到垃圾收集(其中移除未使用的资源以为其他组件回收内存)。 启动线程: public void startThread(){ Thread thread = new ThreadManager(); thread.start(); } 制作一个名为 ThreadManager.java 的 java 类: public class ThreadManager extends Thread{ public ThreadManager() { super(); } @Override public void run() { long tid = getId(); // Todo do work here. Log.v("TaskScheduler", "Starting a new thread "+ tid); while (true){ Log.v("TaskScheduler", "In a thread: " + tid); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } } 异步任务 在 Android R–API level 30 中不推荐使用(建议使用标准的java.util.concurrent或 Kotlin 并发实用程序)– AsyncTasks 允许在单独的线程上短期运行(一次仅运行几秒钟)代码,同时还可以访问 UI 线程。当构造一个AsyncTask对象时,需要提供三种类型;这些是: 传递到 AsyncTask 执行中的参数的类型 后台计算期间使用的进度单位的类型 后台方法的结果的类型 必须在主线程上加载、创建和执行——从 API 级开始,这是自动完成的。 调用异步任务: AsyncTask 取消异步任务: task.cancel(true); 创建一个 AsyncTask 类(作为活动类的私有或包私有子类): class myAsyncTask extends AsyncTask private Context mContext; public myAsyncTask(Context context) { this.mContext = context; } @Override protected Void doInBackground(final String... strings) { final Context context = this.mContext; runOnUiThread(new Runnable() { @Override public void run() { for (String text:strings) { Toast.makeText(context, text, Toast.LENGTH_SHORT).show(); } } }); return null; } } 电池和安全对长期运行服务的影响 长期运行的服务可以为用户提供无数有用的功能;然而,由于电池和安全问题,有无数种方法可以提前终止长期运行的服务。这随服务类型的不同而不同;但是,这通常是由电源管理限制引起的。 10 这些包括以下运行时的限制: 十二模式【11】 App 待机桶 12 应用背景限制 应用电池优化 瞌睡 Doze 是在 Android 6 (API level 23)中引入的,当设备空闲时(意味着设备最近一段时间没有接收用户交互),Doze 通过将应用运行时划分到维护窗口中来充当电池节省工具。这些应用可以在其中运行后台任务的窗口开始时很频繁,但是,随着时间的推移,设备空闲的时间越长,这些窗口就会变得越来越不同。这意味着虽然一个AlarmManager可能被分配每 5 分钟运行一次的任务,但是瞌睡限制会阻止它定期运行。 报警管理器的限制: setExact()和setWindow()报警管理器报警被推迟到下一个维护窗口。 setAndAllowWhileIdle()和setExactAndAllowWhileIdle()将在打盹维护窗口期间正常启动。setAlarmClock()也将在维护窗口期间启动,系统在警报触发前不久退出休眠。 作业计划程序的限制: 作业调度程序或工作管理器被挂起。 其他限制: 网络访问、唤醒锁、Wi-Fi 扫描和同步适配器会被忽略和暂停。 您可以使用下面的命令测试一个应用如何在 Doze 模式下运行(在 API 级别 23 以上: adb shell dumpsys deviceidle force-idle 通过运行以下命令可以退出空闲模式: adb shell dumpsys deviceidle unforce 应用备用桶 Android 9 (API 级别 28)增加了另一个省电功能。此功能将所有应用分配到四个存储桶之一。每个制造商可以为如何将应用放入每个桶中设置自己的标准(Android 文档强调“机器学习”技术可以用于支持这一决策过程)。反过来,为什么应用被分配特定的存储桶的确切原理是未知的。 这些桶是 活动 -应用当前正在使用或最近使用过,包括应用是否已启动活动或正在运行前台服务,或者用户已点击应用的通知: 工作 -无限制 警报 -无限制 工作集-app 正常使用: 作业 -最多延迟 2 小时 警报 -最多延迟 6 分钟 频繁 -经常使用该应用,但不是每天都使用: 作业 -最多延迟 8 小时 警报 -最多延迟 30 分钟 稀有 -不常用的应用: 作业 -最多延迟 24 小时 警报 -最多延迟 2 小时 联网 -最多延迟 24 小时 Never -应用已安装,但从未运行: 如果应用从未运行过,组件将被禁用。 修改组件的状态 应用组件是用前面提到的AndroidManifest.xml file. A编写的,有四种主要类型的组件,它们是 活动 服务 广播接收机 内容供应器 静态修改组件状态 可以通过在 Android 清单中编辑组件条目的android:enabled="false"标签来静态修改组件。以下活动SecondaryActivity已经通过其在 Android 清单中的条目被默认禁用。 设置组件的启用属性: android:enabled="false" /> 动态修改组件 组件可以通过编程设置为三种主要状态,它们是 组件 _ 启用 _ 状态 _ 默认 将组件设置为清单中定义的默认状态。 组件已启用状态已启用 显式启用组件。 组件 _ 启用 _ 状态 _ 禁用 显式禁用组件。禁用的组件不能使用或启动。 有两种其他的组成状态;然而,这些不能用setComponentEnabledSetting方法设置。这些是 组件 _ 启用 _ 状态 _ 禁用 _ 用户 显式禁用该组件,并且可以由用户在适当的系统用户界面中重新启用。 组件 _ 启用 _ 状态 _ 禁用 _ 直到 _ 使用 这种状态意味着组件应该被识别为禁用的(即,在启动器中不显示活动),直到用户明确地试图在它应该被设置为启用的地方使用它。 启用组件 : PackageManager packageManager = getApplicationContext().getPackageManager(); ComponentName componentName = new ComponentName(getApplicationContext(), SecondaryActivity.class); packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_ENABLED,PackageManager.DONT_KILL_APP); 返回组件的状态: PackageManager packageManager = getApplicationContext().getPackageManager(); ComponentName componentName = new ComponentName(getApplicationContext(), SecondaryActivity.class); int componentState = packageManager.getComponentEnabledSetting(componentName); 禁用一个组件 : PackageManager packageManager = getApplicationContext().getPackageManager(); ComponentName componentName = new ComponentName(getApplicationContext(), SecondaryActivity.class); packageManager.setComponentEnabledSetting(componentName, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,PackageManager.DONT_KILL_APP); 创建 Android 启动器 Android Launcher 在 API 级别 1 中实现,是 Android 的一个组件,它允许 Android 应用作为 Android 设备主屏幕上的基本活动(如图 9-2 所示)。这些主屏幕可以由各个原始设备制造商设置;然而,其他著名的发射器包括 Facebook Home。必须在 Android 设备的设置菜单中设置一个启动器,如图 9-1 所示。 图 9-2 示例启动器 图 9-1 启动器设置 创建启动器应用 首先将以下属性添加到 AndroidManifest.xml 文件中的 activities 活动标记: android:launchMode="singleTask" 然后给同一个活动标签的意图过滤器添加两个类别: 在这个阶段,该应用将作为一个启动器,并且可以从 Android 设备的设置中选择作为主屏幕。下面详细介绍了几个在创建启动器时有用的附加技术。 附加功能 检索应用列表: private List Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); List return pkgAppsList; } 检索应用的图标: public static Drawable getActivityIcon(Context context, String packageName, String activityName) { PackageManager pm = context.getPackageManager(); Intent intent = new Intent(); intent.setComponent(new ComponentName(packageName, activityName)); ResolveInfo resolveInfo = pm.resolveActivity(intent, 0); return resolveInfo.loadIcon(pm); } 设置图像视图。将一个 ImageView 对象添加到您的活动中,名称为 imageView: android:id="@+id/imageView" android:layout_width="129dp" android:layout_height="129dp" android:foregroundGravity="center_vertical" app:srcCompat="@android:drawable/ic_dialog_alert" android:layout_gravity="center" /> 为 ImageView : 创建一个点击监听器 ImageView chromeIcon = (ImageView) findViewById(R.id.imageView); chromeIcon.setImageDrawable(getActivityIcon(getApplicationContext(),"com.android.chrome", "com.google.android.apps.chrome.Main")); ImageView img = findViewById(R.id.imageView); img.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent launchIntent = getPackageManager().getLaunchIntentForPackage("com.android.chrome"); startActivity(launchIntent); } }); 设置壁纸。将下面的代码添加到 styles.xml 的名称为 AppTheme 的样式标记中: 隐藏系统界面 : private void hideSystemUI() { View decorView = getWindow().getDecorView(); decorView.setSystemUiVisibility( View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN); Footnotes 1 “创建后台服务| Android 开发者。” https://developer.android.com/training/run-background-service/create-service 。5 月 11 日访问。2020. 2 " Android . app . alarm manager-Android 开发者。"2019 年 12 月 27 日, https://developer.android.com/reference/android/app/AlarmManager 。5 月 11 日访问。2020. 3 “广播概述| Android 开发人员。”2019 年 6 月 3 日, https://developer.android.com/guide/components/broadcasts 。5 月 11 日访问。2020. 4 “Android 8.0:Java . lang . illegalstateexception:不允许....” https://stackoverflow.com/questions/46445265/android-8-0-java-lang-illegalstateexception-not-allowed-to-start-service-inten 。5 月 21 日访问。2020. 5 “后台执行限制|安卓开发者。”2020 年 2 月 13 日, https://developer.android.com/about/versions/oreo/background 。5 月 11 日访问。2020. 6 “我是否应该在我的接收器中使用 Android:process = ":remote….” https://stackoverflow.com/questions/4311069/should-i-use-android-process-remote-in-my-receiver 。5 月 21 日访问。2020. 7 “服务概述| Android 开发人员。”2019 年 6 月 3 日, https://developer.android.com/guide/components/services 。5 月 11 日访问。2020. 8 "创建和管理通知渠道…" https://developer.android.com/training/notify-user/channels 。5 月 11 日访问。2020. 9 “使用 WorkManager | Android 开发人员安排任务。” https://developer.android.com/topic/libraries/architecture/workmanager 。5 月 11 日访问。2020. 10 “电源管理限制| Android 开发人员。”2018 年 6 月 3 日, https://developer.android.com/topic/performance/power/power-details 。5 月 11 日访问。2020. 11 “针对瞌睡和应用待机进行优化……” https://developer.android.com/training/monitoring-device-state/doze-standby 。5 月 11 日访问。2020. 12 “应用待机桶|安卓开发者。”2018 年 6 月 3 日, https://developer.android.com/topic/performance/appstandby 。5 月 11 日访问。2020. 十、反射和类加载 反射 当谈到拆开 Android 应用并让它们在适合你的状态下运行时,反射是许多王牌之一。简单地说,反射是一个 API,可以用来在运行时访问、检查和修改对象——这包括字段、方法、类和接口(如图 10-1 所示)。 图 10-1 Java 反射图 下面列出了这些组件的摘要: 类 -类是一个蓝图/模板,当使用时,可以从它们创建单独的对象。例如,可以用一个installedRam变量、getRAM()方法和setRAM()方法创建一个Computer类。使用这个类可以创建一个对象,例如,Computer myComputer = new Computer();然后方法setRAM()可以用在myComputer对象上,例如myComputer.setRAM(32);. 方法——方法是一段代码,具有特定的用途,在被调用时运行,可以是类的一部分,也可以是独立的。方法可以传递一系列类型化参数,并且可以返回指定类型的变量。当作为类的一部分时,方法可以是静态的或实例的。实例方法需要在使用之前创建其类的对象,而静态方法不依赖于已初始化的对象。例如,类计算机可能有一个将两个数相加并返回结果的sum静态方法,以及一个为所创建对象的特定实例设置 ram 变量的setRAM()实例方法。 构造函数 -构造函数是一种特殊类型的方法,作为对象(如类)初始化的一部分,用来设置变量和调用方法。例如,House 类可能有一个构造函数方法,它将三个变量作为参数:hight、numberOfRooms和hasGarden。然后可以用House myHouse = new House(10, 2, false);.创建一个房子对象 接口——接口是一个抽象类,它包含一组带有空体的方法。例如,拥有一个生物接口可能有像move()、speak()和eat()这样的方法,这些方法都需要根据实现接口的职业(生物类型)来填充。 在下面的例子中,将使用两个类来帮助显示一系列不同的反射技术。如果不使用这些示例类,请替换代码示例中适用的引用: 一个 助手类 演示反思: public class Loadable { private final static String description = "This is a class that contains an assortment of access modifiers to test different types of reflection."; private Context context; private long uniqueId = 0; private long time = 0; private DeviceData deviceData = new DeviceData(); public void setDeviceInfo() { deviceData.setDeviceInfo(); } public long getTime() { return time; } private Loadable(Context context, long uniqueId) { this.context = context; this.uniqueId = uniqueId; } private void setTime(){ this.time = System.currentTimeMillis(); } private static String getDeviceName(){ return android.os.Build.MODEL; } protected static Loadable construct(Context context){ final int uniqueId = new Random().nextInt((1000) + 1); Loadable loadable = new Loadable(context, uniqueId); loadable.setDeviceInfo(); return loadable; } } 助手类 支持加载时呈现一系列功能: public class DeviceData { String version = ""; // OS version String sdkLevel = ""; // API Level String device = ""; // Device String model = ""; // Model String product = ""; // Product public void setDeviceInfo(){ version = System.getProperty("os.version"); sdkLevel = android.os.Build.VERSION.SDK; device = android.os.Build.DEVICE; model = android.os.Build.MODEL; product = android.os.Build.PRODUCT; } @Override public String toString() { return "DeviceData{" + "version='" + version + '\'' + ", sdkLevel='" + sdkLevel + '\'' + ", device='" + device + '\'' + ", model='" + model + '\'' + ", product='" + product + '\'' + '}'; } } 创建类的实例 在下面的例子中,使用反射,创建了一个新的DeviceData类实例,并且在记录这些字段之一的初始化状态之前运行了一个对setDeviceInfo方法的调用(以填充它的字段)。 初始化一个类 : try { Object initialisedDeviceData= DeviceData.class.newInstance(); initialisedDeviceData.getClass().getDeclaredMethod("setDeviceInfo").invoke(initialisedDeviceData); String model = (String) initialisedDeviceData.getClass().getDeclaredField("model").get(initialisedDeviceData); Log.v(TAG, model); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } 。getDeclaredMethod 与进行了比较。获取方法 在下面的例子中,我们看到了方法getMethods和getDeclaredMethods之间的区别——这对于getFields和getDeclaredFields也是一样的。getMethods将返回一个数组,该数组包含类或接口的public方法,以及任何从超类或超接口继承的方法(超类/超接口是一个可以从中创建多个子对象的对象)。getDeclaredMethods另一方面,返回类或接口的所有声明的方法(不仅仅是public)。 这里的主要区别是,如果需要访问私有方法,将使用getDeclaredMethods方法,然后用.setAccessible方法设置可访问性,而如果需要访问superclasses或superinterfaces, getMethods的方法,将改为使用。 getMethods()示例: for (Method method : Loadable.class.getMethods()){ Log.v(TAG, method.getName()); } getDeclaredMethods()示例: for (Method method : Loadable.class.getDeclaredMethods()){ method.setAccessible(true); Log.v(TAG, method.getName()); } 静态方法 在静态方法的情况下,使用反射不需要类的实例。 静态方法示例: try { Method getDeviceName = Loadable.class.getDeclaredMethod("getDeviceName"); getDeviceName.setAccessible(true); Log.v(TAG,(String) getDeviceName.invoke(Loadable.class)); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } 私有构造函数 在类的构造函数是私有的情况下,反射仍然可以用来构造类和访问它的字段和方法。 谈论构造函数时的一个额外的怪癖是,当一个成员变量在一个类中定义时——比如String myMemberVariable = android.os.Build.VERSION.SDK;——它被编译器移动到该类的构造函数中。1 下面是一个用私有构造函数构造类的例子: try { Constructor> constructor = Loadable.class.getDeclaredConstructor(Context.class, long.class); constructor.setAccessible(true); Object instance = constructor.newInstance(getApplicationContext(), (Object) 12); // constructor takes a context and an id. Field uniqueIdField = instance.getClass().getDeclaredField("uniqueId"); uniqueIdField.setAccessible(true); long uniqueId = (long) uniqueIdField.get(instance); Log.v(TAG, ""+uniqueId); } catch (InstantiationException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } 将类初始化为其他类的字段 下面的例子使用反射两次:第一次初始化一个类并获得对它的一个字段的访问,第二次在那个字段(它是一个自己的类)上使用反射来访问它的一个字段(它是一个字符串)。 实例类 实例: try { // The loadable class has a static method that can be used to construct it in this example, however, if the constructor isn't public, // this can also be done with the private constructor example. // and can be done as in the public class example. Object instance = Loadable.class.getDeclaredMethod("construct", Context.class) .invoke(Loadable.class, getApplicationContext()); // Retrieve the field device data which is the class we're looking to get the data of. Field devicdDataField = instance.getClass().getDeclaredField("deviceData"); devicdDataField.setAccessible(true); Object initialisedDeviceData = devicdDataField.get(instance); // After accessing the value from the field we're looking to access the filds of we can use the same type of reflection again after getting it's class. Field modelField = initialisedDeviceData.getClass().getDeclaredField("device"); modelField.setAccessible(true); String model = (String) modelField.get(initialisedDeviceData); Log.v(TAG,model); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } 类别加载 Java 类加载器 2 是 Java 运行时环境(JRE)的一个组件,它将 Java 类加载到 Java 虚拟机(JVM)/Dalvik 虚拟机(DVM)/Android 运行时(ART)中。不是所有的类都被同时加载,也不是用同一个类加载器。上下文方法getClassLoader()可以用来获取当前的类加载器。Android 中有几种类型的类加载,它们是: PathClassLoader -这是 Android 系统为其系统和应用类加载器使用的。 DexClassLoader -加载包含一个.dex文件的文件类型(如.jar和.apk或.dex文件直接加载)。这些.dex文件(Dalvik 可执行文件)包含 Dalvik 字节码。 URL class loader——这是用来通过 URL 路径检索类或资源的。以/结尾的路径被假定为目录,否则它们被假定为.jar文件。 下面使用 Dalvik 可执行文件和DexClassLoader 执行类加载。 检索当前的类加载器: ClassLoader loader = getApplicationContext().getClassLoader(); 从 API 级(Android O)开始,可以直接从内存中读取一个 dex 文件。为此,读取文件的 ByteBuffer,并使用 MemoryDexClassLoader 中的类。下面是一个将文件读入字节数组的帮助函数: private static byte[] readFileToByteArray(File file){ FileInputStream fis = null; byte[] bArray = new byte[(int) file.length()]; try{ fis = new FileInputStream(file); fis.read(bArray); fis.close(); }catch(IOException ioExp){ ioExp.printStackTrace(); } return bArray; } 内存中 dex 类加载 : dexLoader = new InMemoryDexClassLoader(ByteBuffer.wrap(readFileToByteArray(filePath)), loader); 另一种方法是直接从文件中加载 dex 文件。DexClassLoader 类采用。dex 文件,optimizedDirectory - where。odex(优化的 dex 文件)存储在 Android API level 26 之前,librarySearchPath - a 字符串列表(由 File.pathSeparator 分隔;)声明包含本地库的目录,以及 parent -父类加载器。 dexLoader = new DexClassLoader(filePath, dexCacheDirectory.getAbsolutePath(), null, loader); 创建一个 dex 类加载器后,选择要加载的类,作为一个字符串: loadedClass = dexLoader.loadClass("me.jamesstevenson.dexloadable.MainActivity"); //alter path for your use case 在这个阶段,未初始化的类可以正常使用,如反射部分所述。下面展示了如何安全地初始化这个类: initialisedClass = loadedClass != null ? loadedClass.newInstance() : null; 在初始化这个类 之后,可以调用一个特定的方法,作为一个字符串,它的响应可以像前面用标准反射所做的那样返回: method = loadedClass != null ? loadedClass.getMethod("loadMeAndIllTakeContext", Context.class) : null; Object methodResponse = method != null ? method.invoke(initialisedClass, getApplicationContext()) : null; Footnotes 1 “我是否应该在我的接收器中使用 Android:process = ":remote….” https://stackoverflow.com/questions/4311069/should-i-use-android-process-remote-in-my-receiver 。5 月 21 日访问。2020. 2 " Catherine22/ClassLoader:加载 apk 或类...——GitHub。” https://github.com/Catherine22/ClassLoader 。于 5 月 16 日访问。2020. 十一、安卓外壳 Android 基于 Linux 构建,这意味着当使用 adb (Android 的专有命令行工具,允许与设备通信)时,您可以发出常见的 Linux 命令(如 ls 、 cd 、 whoami 等)。)以及几个 Android 操作系统特有的命令。 以下是通过外壳进行基本设备输入的几个例子: input text "Hello World" input swipe 50 050 450 100 #coordinates for swipe action input tap 466 17 #coordinates for tap service call phone 1 s16 098765432 service call statusbar 1 service call statusbar 2 要求 root 以下内容在所有其他活动之上显示引导映像。这不会阻止活动在动画后面的前景中运行。 /system/bin/bootanimation 通过 svc 命令控制系统属性(需要 root): svc -l svc bluetooth enable/ disable svc wifi enable/ disable svc nfc enable/ disable svc data enable/ disable svc power reboot svc power shutdown svc power stayon true #[true|false|usb|ac|wireless] svc usb getFunctions [function] #Possible values of [function] are any of 'mtp', 'ptp', 'rndis', 'midi' screen cap 命令 拍摄屏幕照片并保存到设备上的某个位置。类似地,screenrecord 命令记录最多 3 分钟的屏幕,并保存到磁盘: screencap -p /sdcard/screen.png screenrecord /sdcard/MyVideo.mp4 列出所有正在运行的进程: top top | grep chrome 在设备上安装应用,需要 root。g 权限在没有用户交互的情况下接受所有运行时权限(这个选项在 Android 6.0 之前不存在,运行时权限也不存在)。 pm install -g /data/local/tmp/one.apk 返回设备上可用的输入设备列表。这可以包括音频按钮、电源按钮、触摸屏、指纹读取器和鼠标。 uinput-fpc - Finger print sensor fts - screen gpio-keys - volume button qpnp_pon - volume / power buttons ls /dev/input/ -l lsof | grep input/event # or get the name of the inputs and see when an event occurs on that input getevent -l # Return feedback if an input is in use. Useful for identifying if the screen is in use. cat /dev/input/event2 # Send an event to one of these inputs. For example on my device the below sets the volume to 0. sendevent /dev/input/event0 0 0 0 通过 Monkey 测试工具(一个 UI fuzzer)启动一个应用。将数字 1 替换为随机触摸输入的次数,作为测试的一部分: monkey -p com.android.chrome 1 如果您知道活动名称,您可以使用活动管理器启动应用: am start -n com.android.chrome/com.google.android.apps.chrome.Main 以下返回制造商、设备名称、版本、名称和日期,以及用户和释放键: getprop ro.build.fingerprint # i.e. google/blueline/blueline:9/PQ3A.190605.003/5524043:user/release-keys # Returns the kernel version uname -a # Also returns the kernel version as well as the device architecture. cat /proc/version 访问应用的内存(需要 root 用户): #As Root access the locations used by applications as their internal storage. cd /data/user/0 # For example accessing the saved offline pages in Chrome and storing it in the data/local/tmp directory for it to be pulled off device later. su cd /data/user/0 cd com.android.chrome/cache/Offline Pages/archives cp 91-a05c-b3f3384516f4.mhtml /data/local/tmp/page.mhtml chmod 777 /data/local/tmp/page.mhtml 重启设备。应用需要 android.permission.REBOOT 权限或成为 root: /system/bin/reboot reboot svc power reboot svc power shutdown 以 root 身份读写挂载一个文件系统。 在老设备上 这可以用来设置系统应用目录读写。 busybox mount -o remount,rw /system 中断允许接口设备与处理器通信: cat /proc/interrupts | grep volume Dumpsys 提供系统服务信息: dumpsys -l dumpsys input dumpsys meminfo service call procstats 1 以编程方式运行命令 使用 runtime 类,可以以编程方式运行 shell 命令。如果命令要求的权限级别高于应用所拥有的权限级别,命令将会失败,例如,试图在没有 android.permission.REBOOT 权限的情况下重新启动设备。 运行单个命令: String filesLocation = getApplicationContext().getDataDir().getAbsolutePath(); try { Runtime.getRuntime().exec("touch "+filesLocation+"/test.txt"); } catch (IOException e) { e.printStackTrace(); } ```* # 十二、反编译和反汇编 Android 应用 Android 应用要么用 Java 编写,要么用 Kotlin 编写。当构建一个应用时,它们被编译成 Dalvik 字节码——用`dex` (Dalvik 可执行文件)表示。这个 Dalvik 字节码是二进制的,因此不可读。既然如此,如果逆向工程师想要分析一个已经编译好的 Android 应用,他们只能选择反编译或反汇编 Dalvik 可执行文件。图 12-1 突出显示了创建和逆向工程一个 Android 应用的过程。  图 12-1 软件开发人员和逆向工程流程视图 ## 反编译器 java 第一种选择是使用工具将 Dalvik 字节码反编译成人类可读的 Java。这个 Java 比真正的 Java 更像伪代码,因为它是反编译器对 Dalvik 程序集所代表的内容的“最佳猜测”。虽然 Java 开发人员更熟悉这种视图,但它通常不是最佳选择,因为它不仅不代表实际的应用代码,而且也不可运行或重新编译。像`dex2jar`和`jadx`这样的工具可以用来反编译 Dalvik 可执行文件。Jadx 可用于将 Jadx 项目导出到 Gradle 项目,进而允许将项目加载到 Android Studio 中。 APKTool 可用于提取。来自 APK 的 dex 文件: ```java apktool -s d 用 JADX 反编译并查看 APK 或 Dex 文件的反编译 Java: jadx -e 反汇编的 Dalvik 字节码(Smali) 可以使用反汇编器将 Dalvik 字节码还原为人类可读的自身表示,而不是反编译成伪 Java。Dalvik 字节码更常用的这种形式叫做 Smali。对 Smali 来说,反汇编的好处是一个dex文件可以被反汇编、读取、修改、重组和提交,并且仍然处于完全运行的状态。 apk tool 等工具可以用来反汇编 dalvik 字节码: apktool d 由于其性质,Smali 比 Java 或 Kotlin 有更大的代码占用空间。例如,以下 Java 中的 Toast 代码(一个简单的 Android 弹出消息)是 Smali 中相同代码的一半大小。 Java: Context context = getApplicationContext(); CharSequence text = "I'm a Toast"; int duration = Toast.LENGTH_SHORT; Toast toast = Toast.makeText(context, text, duration); toast.show(); 型式:?? .line 13 const-string v0, "I'm a Toast!" .line 14 .local v0, "text":Ljava/lang/String; const/4 v1, 0x1 .line 16 .local v1, "duration":I invoke-virtual {p0}, Lcom/example/simpletoastapp/MainActivity;->getApplicationContext()Landroid/content/Context; move-result-object v2 move-object v3, v0 check-cast v3, Ljava/lang/CharSequence; invoke-static {v2, v3, v1}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast; move-result-object v2 .line 17 .local v2, "toast":Landroid/widget/Toast; invoke-virtual {v2}, Landroid/widget/Toast;->show()V 从运行的设备中提取 apk 为了分析(进而反汇编或反编译)一个 Android 应用,您可能需要首先从设备中提取它。ADB shell 可以用来做这件事。 下面使用包管理器列出设备上的所有包 id: pm list packages pm list packages | grep chrome 接下来,可以再次使用包管理器列出所需包的基本 APK 的路径(例如包路径是/data/app/com . Android . chrome-6 PIH 3g 1 et 8 uqozatukwptq = =/base . apk): pm path 查看此命令返回的目录不需要特殊权限。但是,它的父目录(/data/app)没有非 root 的读取权限,这意味着设备上的应用不能以这种方式枚举。 最后,提取 APK 最简单的方法是使用 adb,如下所示: adb pull 还值得记住的是,诸如 APK 囤积者、 1 之类的工具是免费和开源的,可以用于从设备中大量提取 apk。 Footnotes 1 https://github.com/user1342/APK-Hoarder【APK 囤积者| Github】。于 2020 年 12 月 27 日访问。 十三、总结 本书的目的是为您提供一个参考指南,其中包含了对与 Android 操作系统和其他 Android 安全元素密切合作的 Android 软件开发人员有用的信息。这本书涵盖了从应用沙箱和 Dalvik 虚拟机到 Android 应用的存储类型,以及如何对已经编译好的 Android 应用进行逆向工程。 重要的是要记住,尽管本书中的核心原则在未来许多年都将继续适用,但随着新版本 Android 的发布,一些方面可能会发生变化。在这种情况下,在继续使用这本书作为参考指南的同时,也要回顾分散在整本书中的脚注,以建立在所涵盖的领域之上。 还有大量其他令人惊叹的资源来支持你在 Android 编程、内部和逆向工程方面的知识;以下是其中的一部分: 麦蒂·斯通 -安卓 App 逆向工程 1011 乔纳森·莱文 -安卓内部2 克里斯蒂娜·巴兰 -安卓恶意软件分析| YouTube3 克里斯蒂娜·巴兰 -安卓恶意软件分析|领英学习4 Ira R. Forman 和 Nate Forman - Java 反思在行动|曼宁5 安卓文档 |安卓开发者6 除了这些资源,我还想特别提到 JD,他是这个领域的研究员和软件工程师,没有他我不会被鼓励写这本书。 想了解更多关于我的信息和资源,请访问我的网站 https://JamesStevenson.me/ 。 Footnotes 1 《安卓应用逆向工程 101 | Ragingrock》https://ragingrock.com/AndroidAppRE/2020 年 12 月 27 日访问。 2 “Android Internals | NewAndroidBook”http://newandroidbook.com/2020 年 12 月 27 日访问。 3 “安卓恶意软件分析| YouTube”https://www.youtube.com/channel/UCRHFnRniDEGJCZgsEgtUPxA2020 年 12 月 27 日访问。 4 “Android 恶意软件分析| LinkedIn 学习”https://www.lynda.com/Android-tutorials/Learning-Android-Malware-Analysis/2812563-2.html2020 年 12 月 27 日访问。 5 行动中的 Java 反思| Manning "https://www.manning.com/books/java-reflection-in-action2021 年 1 月 1 日访问。 6 Android 文档| Android 开发者" https://developer.android.com/ 访问日期:2020 年 12 月 27 日。