安卓软件内部原理快速参考-二-

108 阅读32分钟

安卓软件内部原理快速参考(二)

原文:Android Software Internals Quick Reference

协议:CC BY-NC-SA 4.0

九、服务、启动器和组件

长期运行的服务

在 Android 中,有几种方法可以在当前活动结束后一次性或周期性地运行“作业”。这里讨论的技术将在以下两种类型之间变化:

  • 强绑定到当前活动,比如AsyncTask,意思是如果活动结束,那么任务被垃圾回收。

  • 没有将强绑定到当前活动,例如JobScheduler,其中任务甚至在父活动本身被垃圾收集后仍继续。

扳机

触发器是长期运行的服务如何分配任务和运行的机制,它们是服务的初始入口点。大多数触发器将为服务提供某种形式的持久性。这些触发可以在表 9-1 中看到。

表 9-1

长期运行的服务触发器

|

引发

|

描述

| | --- | --- | | 目的 | 从代码中直接触发,通常来自用户交互。 | | 手机闹钟服务 | 在未来的特定时间触发,一次性或重复发生。 | | 广播接收器 | 收到特定广播消息时触发。例如,BootCompletePowerConnected或自定义接收器。 | | 传感器回调 | 收到特定传感器值时触发。 | | 工作管理器 | 根据 API 等级使用JobSchedulerAlarmManagerBootComplete。 | | 作业调度程序 | 从 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 文件中。

<receiver android:name=".receivers.AlarmReceiver"
    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 文件中:

<receiver android:name=".receivers.BootReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

然后创建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 文件:

<service android:name=".managers.ServiceManager"
    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 权限:

<service android:name=".managers.ForegroundServiceManager"
    android:exported="false"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

作业调度程序

从 Android 5 L (API 21)开始,当设备有更多可用资源时,作业调度器作为一种任务批处理作业的方式被引入。作为一个整体,多项工作可以由JobSchedulers ,来完成,他们会将这些任务分成几批。这意味着所分配的工作可能不会按预期执行;然而,它将在该时间前后发生(例如,被指示每 15 分钟执行一次的任务可能在一次运行的 14 分钟后执行,而在另一次运行的 16 分钟后执行)。

JobSchedulers 最强大的功能之一是,如果设置或未设置特定的标准,例如,没有网络连接、电池正在充电或设备处于空闲状态,它们允许工作延期。还有一个选项是用setPeriodicsetPersisted来运行周期性的工作,并在重启后继续运行(如果应用拥有 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 文件中:

<service android:name=".managers.JobSchedulerManager"
    android:permission="android.permission.BIND_JOB_SERVICE"/>

一个JobScheduler服务必须用先前的权限来保护,这样只有具有该权限的应用才能给该服务分配任务。如果在清单中声明了作业服务,但未使用此权限进行保护,则该服务将被系统忽略。

工作经理

如 Android 文档所述, 9 WorkManagers从 API 14 开始向后兼容。API 14-22 上使用了BroadcastReceiverAlarmManager的组合,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<String, Void, Void> task = new myAsyncTask(getApplicationContext()).execute("example string");

取消异步任务:

task.cancel(true);

创建一个 AsyncTask 类(作为活动类的私有或包私有子类):

class myAsyncTask extends AsyncTask<String, Void, Void> {
    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 清单中的条目被默认禁用。

设置组件的启用属性:

<activity android:name=".SecondaryActivity"
    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 所示。

img/509502_1_En_9_Fig2_HTML.jpg

图 9-2

示例启动器

img/509502_1_En_9_Fig1_HTML.jpg

图 9-1

启动器设置

创建启动器应用

首先将以下属性添加到 AndroidManifest.xml 文件中的 activities 活动标记:

android:launchMode="singleTask"

然后给同一个活动标签的意图过滤器添加两个类别:

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.HOME" />

在这个阶段,该应用将作为一个启动器,并且可以从 Android 设备的设置中选择作为主屏幕。下面详细介绍了几个在创建启动器时有用的附加技术。

附加功能

检索应用列表:

private List<ResolveInfo> getListOfApplications(Context context){
    Intent mainIntent = new Intent(Intent.ACTION_MAIN, null);
    mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);
    List<ResolveInfo> pkgAppsList = context.getPackageManager().queryIntentActivities( mainIntent, 0);
    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:

<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 的样式标记中:

<item name="android:windowShowWallpaper">true</item>
<item name="android:windowBackground">@android:color/transparent</item>

隐藏系统界面 :

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 所示)。

img/509502_1_En_10_Fig1_HTML.png

图 10-1

Java 反射图

下面列出了这些组件的摘要:

  • -类是一个蓝图/模板,当使用时,可以从它们创建单独的对象。例如,可以用一个installedRam变量、getRAM()方法和setRAM()方法创建一个Computer类。使用这个类可以创建一个对象,例如,Computer myComputer = new Computer();然后方法setRAM()可以用在myComputer对象上,例如myComputer.setRAM(32);.

  • 方法——方法是一段代码,具有特定的用途,在被调用时运行,可以是类的一部分,也可以是独立的。方法可以传递一系列类型化参数,并且可以返回指定类型的变量。当作为类的一部分时,方法可以是静态的或实例的。实例方法需要在使用之前创建其类的对象,而静态方法不依赖于已初始化的对象。例如,类计算机可能有一个将两个数相加并返回结果的sum静态方法,以及一个为所创建对象的特定实例设置 ram 变量的setRAM()实例方法。

  • 构造函数 -构造函数是一种特殊类型的方法,作为对象(如类)初始化的一部分,用来设置变量和调用方法。例如,House 类可能有一个构造函数方法,它将三个变量作为参数:hightnumberOfRoomshasGarden。然后可以用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 与进行了比较。获取方法

在下面的例子中,我们看到了方法getMethodsgetDeclaredMethods之间的区别——这对于getFieldsgetDeclaredFields也是一样的。getMethods将返回一个数组,该数组包含类或接口的public方法,以及任何从超类或超接口继承的方法(超类/超接口是一个可以从中创建多个子对象的对象)。getDeclaredMethods另一方面,返回类或接口的所有声明的方法(不仅仅是public)。

这里的主要区别是,如果需要访问私有方法,将使用getDeclaredMethods方法,然后用.setAccessible方法设置可访问性,而如果需要访问superclassessuperinterfaces, 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 命令(如 lscdwhoami 等)。)以及几个 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 应用的过程。

![img/509502_1_En_12_Fig1_HTML.png](https://p9-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/c25aac0626ce438b9ee86553534ffc00~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1772448707&x-signature=jU5e6gsgXKFVZHwrMdLvqu0bICs%3D)12-1

软件开发人员和逆向工程流程视图

## 反编译器 java

第一种选择是使用工具将 Dalvik 字节码反编译成人类可读的 Java。这个 Java 比真正的 Java 更像伪代码,因为它是反编译器对 Dalvik 程序集所代表的内容的“最佳猜测”。虽然 Java 开发人员更熟悉这种视图,但它通常不是最佳选择,因为它不仅不代表实际的应用代码,而且也不可运行或重新编译。像`dex2jar`和`jadx`这样的工具可以用来反编译 Dalvik 可执行文件。Jadx 可用于将 Jadx 项目导出到 Gradle 项目,进而允许将项目加载到 Android Studio 中。

APKTool 可用于提取。来自 APK 的 dex 文件:

```java
apktool  -s d <apk path>

用 JADX 反编译并查看 APK 或 Dex 文件的反编译 Java:

jadx -e <apk or dex file path>

反汇编的 Dalvik 字节码(Smali)

可以使用反汇编器将 Dalvik 字节码还原为人类可读的自身表示,而不是反编译成伪 Java。Dalvik 字节码更常用的这种形式叫做 Smali。对 Smali 来说,反汇编的好处是一个dex文件可以被反汇编、读取、修改、重组和提交,并且仍然处于完全运行的状态。

apk tool 等工具可以用来反汇编 dalvik 字节码:

apktool d <path>

由于其性质,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 <Package ID>

查看此命令返回的目录不需要特殊权限。但是,它的父目录(/data/app)没有非 root 的读取权限,这意味着设备上的应用不能以这种方式枚举。

最后,提取 APK 最简单的方法是使用 adb,如下所示:

adb pull <package base APK path>

还值得记住的是,诸如 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 日。