深入探索Android IPC:解锁进程间通信的奥秘

一、Android IPC 基础概念

在 Android 开发中,IPC(Inter-Process Communication)即进程间通信,是指在不同进程之间进行数据交互和通信的机制。由于 Android 系统的每个应用都运行在独立的进程中,且拥有独立的内存空间,当应用需要与其他应用或系统服务进行通信时,就需要使用 IPC。例如,一个音乐播放应用可能需要与系统的音频服务进行通信,以实现播放、暂停、切换歌曲等操作,这就涉及到了 IPC。

1.1 进程与线程

进程是程序的一次动态执行过程,对应从代码加载、执行至执行完毕的完整过程,也是进程本身从产生、发展至消亡的过程。每个进程都有自己独立的内存空间和系统资源,其内部数据和状态完全独立。例如,当我们打开手机上的微信应用时,系统就会为微信创建一个进程,该进程拥有独立的内存空间,用于存储微信运行所需的数据和代码。

线程是进程中执行运算的最小单位,一个进程可以包含多个线程。线程必须在某个进程内执行,是进程内部的一个执行单元,可完成一个独立任务的顺序控制流程。比如,在微信中,可能有一个线程负责接收和处理网络消息,另一个线程负责更新界面显示,这些线程都运行在微信进程中。

进程和线程的关系紧密。一个进程至少要有一个线程,资源分配给进程,同一进程的所有线程共享该进程的所有资源,而处理机实际分配给线程,即真正在处理机上运行的是线程。例如,在一个视频播放应用中,播放视频的任务可能由一个线程负责,而更新播放进度条的任务由另一个线程负责,它们都共享视频播放进程的资源,如内存、文件句柄等。多线程程序可以带来更好的用户体验,避免因程序执行过慢而导致计算机出现死机或者白屏的情况,还能最大限度地提高计算机系统的利用效率,如迅雷的多线程下载,通过多个线程同时下载文件的不同部分,加快下载速度。

1.2 多进程模式

多进程模式指的是一个应用中存在多个进程的情况。在 Android 中,开启多进程模式主要通过在 AndroidManifest.xml 文件中为四大组件(Activity、Service、Receiver、ContentProvider)指定 android:process 属性来实现。例如:

xml 复制代码
<activity android:name=".MainActivity" />

<activity android:name=".SecondActivity" android:process=":remote" />

<service android:name=".TestService" android:process="com.example.remote" />

上述代码中,SecondActivity 指定了 android:process=":remote",表示它运行在一个名为 ":remote" 的私有进程中,而 TestService 指定了 android:process="com.example.remote",表示它运行在一个名为 "com.example.remote" 的全局进程中。进程名以 ":" 开头的进程属于当前应用的私有进程,其他应用的组件不能和它运行在同一个进程中;不以 ":" 开头的进程属于全局进程,其他应用通过 shareUID 方式可以和它跑在同一个进程中。两个应用通过 ShareUID 运行在同一个进程中,需要满足两个应用有相同的 UID 并且签名相同。

多进程模式的运行机制是,Android 会为每个进程分配一个独立的虚拟机,不同虚拟机在内存分配上有不同的地址空间。这就导致在不同虚拟机中访问同一个类的对象会产生多个副本,运行在不同进程中的四大组件,只要它们之间通过内存来共享数据,都会共享失败。例如,在一个应用中,有一个类的静态成员变量,在一个进程中修改了这个变量的值,在另一个进程中访问这个变量时,其值并不会改变,因为它们处于不同的进程空间。使用多进程还会造成静态成员和单例模式完全失效、线程同步机制完全失效、SharePreference 的可靠性下降以及 Application 会多次创建等问题 。如在一个多进程应用中,原本的单例模式无法保证在不同进程中只有一个实例,线程同步机制在不同进程中也无法发挥作用,因为不同进程锁的不是同一个对象。

二、IPC 基础概念与序列化

在进行 IPC 通信时,数据的传递是必不可少的。而在传递复杂对象时,就需要对对象进行序列化,将对象转换为字节流,以便在不同进程之间传输。Android 中主要有两种序列化方式,分别是实现 Serializable 接口和 Parcelable 接口。

2.1 Serializable 接口

Serializable 接口是 Java 提供的一个序列化接口,它是一个空接口,仅用于标识一个类可以被序列化。当一个类实现了 Serializable 接口,就表示该类的对象可以被转换为字节序列,从而实现对象的存储和传输。其主要作用包括:

  • 对象存储:将对象的状态保存到存储介质(如文件)中,以便在需要时可以重新创建出相同状态的对象。例如,在一个游戏应用中,游戏的当前进度、玩家的角色信息等对象可以通过 Serializable 接口进行序列化并保存到本地文件,下次玩家打开游戏时可以从文件中读取并反序列化这些对象,恢复游戏的状态。
  • 对象传输:在网络通信中,将对象转换为字节流进行传输。比如,在一个社交应用中,用户的个人信息对象(包含用户名、头像、简介等)需要通过网络发送给服务器进行存储或其他处理,就可以利用 Serializable 接口将该对象序列化后在网络上传输。

使用 Serializable 接口非常简单,只需在类的定义中声明实现该接口即可。例如:

java 复制代码
import java.io.Serializable;

public class User implements Serializable {
    private static final long serialVersionUID = 6477564458830772334L;
    private int userId;
    private String userName;
    private int age;

    public User(int userId, String userName, int age) {
        this.userId = userId;
        this.userName = userName;
        this.age = age;
    }

    @Override
    public String toString() {
        return "User=[userName=" + userName + ",age=" + age + ",userId=" + userId + "]";
    }
}

在上述代码中,User 类实现了 Serializable 接口,并声明了一个 serialVersionUID。serialVersionUID 是一个序列化版本号,用于在反序列化时验证类的版本兼容性。如果在序列化和反序列化过程中,类的 serialVersionUID 不一致,会抛出 InvalidClassException 异常。可以使用自动生成的 serialVersionUID,也可以手动指定,如上述代码中手动指定为 6477564458830772334L。

接下来是对象的序列化和反序列化示例代码:

java 复制代码
import java.io.*;

public class SerializableTest {
    public static void main(String[] args) {
        // 序列化
        User user = new User(1, "test", 20);
        File file = new File("user.txt");
        try {
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
            out.writeObject(user);
            out.close();
            System.out.println("序列化成功:");
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IOException:" + e.toString());
        }

        // 反序列化
        try {
            ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
            User user1 = (User) in.readObject();
            in.close();
            System.out.println("反序列化成功:" + user1.toString());
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("反序列化失败IOException:" + e.toString());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            System.out.println("反序列化失败ClassNotFoundException:" + e.toString());
        }
    }
}

在上述代码中,首先创建了一个 User 对象,然后通过 ObjectOutputStream 将其序列化并写入到文件 user.txt 中。接着,通过 ObjectInputStream 从文件中读取字节流并反序列化为 User 对象,从而实现了对象的存储和恢复。

2.2 Parcelable 接口

Parcelable 接口是 Android 特有的序列化接口,主要用于在 Android 组件(如 Activity、Service 等)之间传递数据,尤其是在 Intent 传递对象和 IPC 通信中使用。它的设计目的是为了提高序列化和反序列化的效率,适用于在内存中进行数据传递的场景。

使用 Parcelable 接口需要实现以下几个方法:

  • describeContents:该方法用于描述对象的内容,返回值通常为 0,只有当对象中包含文件描述符时才返回 1。例如:
java 复制代码
@Override
public int describeContents() {
    return 0;
}
  • writeToParcel:该方法用于将对象的属性写入 Parcel 对象中,实现对象的序列化。在写入时,需要按照一定的顺序将属性写入,以便在反序列化时能够正确读取。例如:
java 复制代码
@Override
public void writeToParcel(Parcel dest, int flags) {
   dest.writeInt(userId);
   dest.writeString(userName);
   dest.writeInt(age);
}
  • CREATOR:这是一个静态常量,用于创建和反创建对象。它需要实现 Parcelable.Creator 接口中的两个方法:createFromParcel用于从 Parcel 中读取数据并创建对象,newArray用于创建指定长度的对象数组。例如:
java 复制代码
public static final Creator<User> CREATOR = new Creator<User>() {
    @Override
    public User createFromParcel(Parcel source) {
        int userId = source.readInt();
        String userName = source.readString();
        int age = source.readInt();
        return new User(userId, userName, age);
    }

    @Override
    public User[] newArray(int size) {
        return new User[size];
    }
};

下面是一个完整的实现 Parcelable 接口的示例:

java 复制代码
import android.os.Parcel;
import android.os.Parcelable;

public class User implements Parcelable {
    private int userId;
    private String userName;
    private int age;

    public User(int userId, String userName, int age) {
        this.userId = userId;
        this.userName = userName;
        this.age = age;
    }

    // 从Parcel中读取数据并创建对象
    protected User(Parcel in) {
        userId = in.readInt();
        userName = in.readString();
        age = in.readInt();
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(userId);
        dest.writeString(userName);
        dest.writeInt(age);
    }

    public static final Creator<User> CREATOR = new Creator<User>() {
        @Override
        public User createFromParcel(Parcel source) {
            return new User(source);
        }

        @Override
        public User[] newArray(int size) {
            return new User[size];
        }
    };

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User=[userName=" + userName + ",age=" + age + ",userId=" + userId + "]";
    }
}

在 Activity 中使用 Parcelable 传递对象的示例代码如下:

java 复制代码
        // 在发送方Activity中
        Intent intent = new Intent(this, SecondActivity.class);
        User user = new User(1, "test", 20);
        intent.putExtra("user", user);
        startActivity(intent);

// 在接收方Activity中
        User receivedUser = getIntent().getParcelableExtra("user");
        if (receivedUser!= null) {
            System.out.println("接收的用户信息:" + receivedUser.toString());
        }

在上述代码中,在发送方 Activity 中创建了一个 User 对象,并通过 Intent 将其传递给接收方 Activity。在接收方 Activity 中,通过getIntent().getParcelableExtra("user")获取传递过来的 User 对象,实现了对象在不同 Activity 之间的传递。

2.3 两者对比

Serializable 接口和 Parcelable 接口各有优缺点,在不同的场景下应选择不同的序列化方式。

  • 性能方面:Parcelable 的性能要优于 Serializable。Parcelable 是基于 Android 的 Parcel 机制实现的,它在序列化和反序列化过程中直接使用内存操作,不需要进行大量的 I/O 操作,因此速度更快。而 Serializable 使用 Java 的默认序列化机制,在序列化和反序列化时会产生大量的临时变量,引起频繁的 GC 操作,性能开销较大。例如,在一个需要频繁在 Activity 之间传递对象的应用中,如果使用 Serializable 接口,由于频繁的 GC 操作,可能会导致应用的卡顿,而使用 Parcelable 接口则可以避免这种情况,提高应用的流畅性。
  • 实现难度方面:Serializable 接口的实现非常简单,只需在类中声明实现该接口并添加 serialVersionUID 即可,对开发者的要求较低。而 Parcelable 接口的实现相对复杂,需要手动实现describeContents、writeToParcel方法以及CREATOR常量,编写的代码量较多,对开发者的要求较高。比如,对于一个简单的 JavaBean 类,使用 Serializable 接口可能只需要添加两行代码,而使用 Parcelable 接口则需要编写多个方法和常量。
  • 使用场景方面:Parcelable 主要用于在 Android 内存中进行数据传递,如在 Intent 传递对象、Binder 通信等场景中,因为它的高效性可以满足对性能要求较高的场景。而 Serializable 更适用于将对象持久化到存储设备(如文件)或通过网络进行传输,因为它是 Java 标准的序列化接口,通用性更强,并且在这些场景下对性能的要求相对较低。例如,在将用户的配置信息保存到本地文件时,可以使用 Serializable 接口,因为文件的读写操作本身就比较耗时,Serializable 接口的性能劣势在这种情况下影响不大;而在 Activity 之间传递一个包含大量数据的对象时,为了提高效率,应使用 Parcelable 接口。

总的来说,在 Android 开发中,如果是在内存中进行数据传递,优先选择 Parcelable 接口;如果是进行对象的持久化存储或网络传输,应选择 Serializable 接口。

三、Android IPC 的实现方式

3.1 Binder 机制

Binder 机制是 Android 系统中一种基于 C/S 架构的进程间通信(IPC)机制,它允许不同进程间进行通信。这种机制的核心在于其独特的驱动层实现,简化了进程间通信的复杂性,使得跨进程通信看起来就像是进行本地方法调用一样简单 。

在 Binder 机制中,存在服务端和客户端两个主要角色。服务端会实现一个 Binder 对象,该对象包含了服务端可以提供给客户端调用的方法接口。然后,服务端通过 ServiceManager 将这个 Binder 对象注册为一个服务,这样客户端就可以通过 ServiceManager 查询到这个服务,并获取到 Binder 对象的代理(Proxy)。客户端通过这个代理对象,就可以像调用本地方法一样调用服务端提供的方法,从而实现进程间的通信。

Binder 机制的工作原理可以概括为以下几个步骤:

  1. 服务端创建并实现 Binder 对象:服务端定义好可以提供给客户端调用的方法接口,创建一个 Binder 对象来实现这些接口。例如,一个音乐播放服务的服务端,会创建一个 Binder 对象,实现播放、暂停、切换歌曲等方法接口。
  1. 服务端注册服务:服务端通过 ServiceManager 将 Binder 对象注册为一个服务,将自己暴露给客户端。比如,音乐播放服务的服务端将创建好的 Binder 对象注册到 ServiceManager,告知 ServiceManager 自己可以提供音乐播放相关的服务。
  1. 客户端查询服务并获取代理:客户端通过 ServiceManager 查询所需服务,并获取到 Binder 对象的代理。假设一个音乐播放应用的客户端需要使用音乐播放服务,它就会通过 ServiceManager 查询音乐播放服务,并获取到对应的 Binder 代理对象。
  1. 客户端调用服务端方法:客户端通过代理对象调用服务端的方法,完成进程间通信。客户端拿到代理对象后,就可以像调用本地方法一样调用音乐播放服务的播放、暂停等方法,代理对象会将这些调用转发给服务端的 Binder 对象,从而实现跨进程通信。

Binder 机制在 Android 系统中的应用非常广泛,它不仅是应用进程与系统服务进程进行通信的基础,也是实现四大组件(Activity、Service、BroadcastReceiver、ContentProvider)之间通信的重要手段。例如,当一个应用启动一个 Service 时,就可能通过 Binder 机制与 Service 进行通信,传递参数、获取服务状态等。在系统层面,很多系统服务如 ActivityManagerService、WindowManagerService 等都是通过 Binder 机制与应用进程进行交互的。

Binder 机制的优点显著:

  • 高性能:Binder 采用了内存映射(mmap)技术,减少了数据拷贝次数,提高了通信效率。在传统的进程间通信方式中,如管道、Socket 等,数据需要在用户空间和内核空间之间多次拷贝,而 Binder 通过内存映射,在内核空间创建数据接收的缓存空间,使得数据可以直接从发送方进程的用户空间映射到接收方进程的用户空间,减少了数据拷贝的开销,从而提高了通信效率。
  • 安全性高:Binder 机制具有系统内置的权限管理,可以确保应用和服务只能访问它们允许访问的数据。在 Binder 通信过程中,每个进程都有自己的 UID 和 PID,Binder 驱动会根据这些信息对通信进行权限验证,只有具有相应权限的进程才能进行通信和访问数据,这有效防止了非法访问和数据泄露,保障了系统的安全性。
  • 面向对象:Binder 机制基于接口定义,采用面向对象的方式进行设计,使得通信接口更加清晰、易于理解和维护。开发者可以像定义普通的 Java 接口一样定义 Binder 接口,通过接口来规范服务端和客户端之间的通信,提高了代码的可维护性和可扩展性。

然而,Binder 机制也存在一些缺点:

  • 学习成本较高:Binder 机制涉及到操作系统内核、驱动等底层知识,其原理和使用相对复杂,对于开发者来说,理解和掌握 Binder 机制需要花费一定的时间和精力,学习成本较高。
  • 实现复杂度高:Binder 机制的实现涉及到多个组件和复杂的交互过程,如 ServiceManager、Binder 驱动、客户端和服务端等,在实际开发中,实现一个基于 Binder 的通信功能需要编写较多的代码,处理各种细节问题,实现复杂度较高。例如,在编写服务端代码时,需要创建 Binder 对象、实现接口方法、注册服务等;在客户端代码中,需要查询服务、获取代理对象、处理远程调用的异常等,这些都增加了开发的难度和工作量。

3.2 AIDL(Android Interface Definition Language)

AIDL(Android Interface Definition Language)即 Android 接口定义语言,是一种用于在 Android 设备上实现两个进程之间进行进程通信的接口定义语言。它内部主要通过 Binder 机制来实现,适用于进程之间交互频繁、通信数据量小的场景。在使用 AIDL 进行跨进程通信的时候,通常将请求通信的一方称之为客户端(Client),客户端的主要工作就是发送数据;而接收通信数据的一方称之为服务端(Server),服务端主要工作是处理客户端发送过来的数据,并通过回调(Callback)的方式返回客户端数据,实现双向通信。

AIDL 支持以下数据类型:

  • Java 的 8 种基本类型:即 int、long、char、short、byte、boolean、float、double。
  • String 和 CharSequence:这两种常用的字符串类型也在 AIDL 支持范围内。
  • List:其中的每个元素都必须是 AIDL 支持的。客户端实际接收的具体类始终是 ArrayList,但生成的方法使用的是 List 接口。例如,可以定义一个 List类型的参数在 AIDL 接口中传递。
  • Map:其中的每个元素都必须是 AIDL 支持的。客户端实际接收的具体类始终是 HashMap,但生成的方法使用的是 Map 接口。比如,可以使用 Map<String, Integer> 类型在 AIDL 中进行数据传递。
  • Parcelable:必须要显式 import,即使它跟.aidl 是同一个包下。当需要传递自定义的复杂对象时,可通过实现 Parcelable 接口来使其能在 AIDL 中使用。
  • AIDL 接口:必须要显式 import,即使它跟.aidl 是同一个包下。在 AIDL 中可以定义接口,实现更复杂的通信逻辑。

AIDL 中的方法可有零、一或多个参数,可有返回值或 void。除了基本数据类型(默认为 in,不能是其他方向),其他类型的参数必须标上方向:

  • in:表示输入型参数,由客户端赋值,服务端接收并使用该参数。
  • out:表示输出型参数,由服务端赋值,客户端接收服务端返回的该参数值。
  • inout:表示输入输出型参数,可由客户端或服务端赋值,既可以作为输入参数传递给服务端,也可以在服务端处理后作为输出参数返回给客户端。

下面通过一个具体的代码示例来展示如何使用 AIDL 实现跨进程通信:

  1. 定义 AIDL 文件:在项目的 main 目录下创建 aidl 文件夹,然后在其中新建一个.aidl 文件,例如定义一个名为 IMyAidlInterface.aidl 的文件,内容如下:
java 复制代码
// IMyAidlInterface.aidl
package com.example.aidl;

// 声明任何非默认类型都需要导入
import com.example.aidl.User;

interface IMyAidlInterface {
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString);

    // 自定义方法,接收一个User对象
    void sendUser(User user);
}

上述代码中,定义了一个 AIDL 接口 IMyAidlInterface,包含一个系统自动生成的 basicTypes 方法(可根据需求保留或删除),以及一个自定义的 sendUser 方法,用于接收一个 User 对象。其中 User 是一个自定义的实现了 Parcelable 接口的类,用于在进程间传递数据。

  1. 生成 AIDL 对应的 Java 文件:定义好 AIDL 文件后,点击 Make Project 按钮,Android Studio 会自动生成对应的 Java 文件。在生成的 Java 文件中,核心部分是一个继承自 Binder 的 Stub 抽象类,它是实现 AIDL 接口的关键。例如,生成的部分代码如下:
java 复制代码
public interface IMyAidlInterface extends android.os.IInterface {
    public static abstract class Stub extends android.os.Binder implements IMyAidlInterface {
        private static final java.lang.String DESCRIPTOR = "com.example.aidl.IMyAidlInterface";

        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        public static IMyAidlInterface asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin!= null) && (iin instanceof IMyAidlInterface))) {
                return ((IMyAidlInterface) iin);
            }
            return new IMyAidlInterface.Stub.Proxy(obj);
        }

        @Override
        public android.os.IBinder asBinder() {
            return this;
        }

        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_basicTypes: {
                    data.enforceInterface(DESCRIPTOR);
                    int _arg0;
                    _arg0 = data.readInt();
                    long _arg1;
                    _arg1 = data.readLong();
                    boolean _arg2;
                    _arg2 = data.readInt()!= 0;
                    float _arg3;
                    _arg3 = data.readFloat();
                    double _arg4;
                    _arg4 = data.readDouble();
                    java.lang.String _arg5;
                    _arg5 = data.readString();
                    this.basicTypes(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5);
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_sendUser: {
                    data.enforceInterface(DESCRIPTOR);
                    com.example.aidl.User _arg0;
                    if ((0!= data.readInt())) {
                        _arg0 = com.example.aidl.User.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    this.sendUser(_arg0);
                    reply.writeNoException();
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

        private static class Proxy implements IMyAidlInterface {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            @Override
            public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(anInt);
                    _data.writeLong(aLong);
                    _data.writeInt(aBoolean? 1 : 0);
                    _data.writeFloat(aFloat);
                    _data.writeDouble(aDouble);
                    _data.writeString(aString);
                    mRemote.transact(Stub.TRANSACTION_basicTypes, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }

            @Override
            public void sendUser(com.example.aidl.User user) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    if (user!= null) {
                        _data.writeInt(1);
                        user.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    mRemote.transact(Stub.TRANSACTION_sendUser, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }
        }

        static final int TRANSACTION_basicTypes = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_sendUser = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }

    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws android.os.RemoteException;

    void sendUser(com.example.aidl.User user) throws android.os.RemoteException;
}
  1. 实现服务端:在服务端创建一个 Service,并在其中实现 AIDL 接口。例如:
java 复制代码
package com.example.aidlserver;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

public class AIDLService extends Service {
    private static final String TAG = "AIDLService";

    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder();
    }

    class MyBinder extends IMyAidlInterface.Stub {
        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
            Log.d(TAG, "basicTypes: anInt = " + anInt + ", aLong = " + aLong + ", aBoolean = " + aBoolean + ", aFloat = " + aFloat + ", aDouble = " + aDouble + ", aString = " + aString);
        }

        @Override
        public void sendUser(User user) throws RemoteException {
            if (user != null) {
                Log.d(TAG, "sendUser: user = " + user.toString());
            }
        }
    }
}

在上述服务端代码中,创建了一个 AIDLService,在 onBind 方法中返回一个实现了 IMyAidlInterface 接口的 MyBinder 对象。MyBinder 类重写了 AIDL 接口中的方法,在方法中对客户端传递过来的数据进行处理,这里只是简单地打印日志。

  1. 配置服务端 Service:在 AndroidManifest.xml 文件中注册服务端的 Service,如下:
xml 复制代码
<service
    android:name=".AIDLService"
    android:enabled="true"
    android:exported="true">
</service>
  1. 实现客户端:在客户端绑定服务,并通过 AIDL 接口与服务端进行通信。例如:
java 复制代码
package com.example.aidlclient;

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.example.aidl.IMyAidlInterface;
import com.example.aidl.User;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private IMyAidlInterface mAidlInterface;

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mAidlInterface = IMyAidlInterface.Stub.asInterface(service);
            try {
                mAidlInterface.basicTypes(1, 2L, true, 3.0f, 4.0, "Hello AIDL");

                User user = new User(1, "张三", 20);
                mAidlInterface.sendUser(user);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mAidlInterface = null;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button bindButton = findViewById(R.id.bind_button);
        bindButton.setOnClickListener(v -> {
            Intent intent = new Intent();
            intent.setComponent(new ComponentName("com.example.aidlserver", "com.example.aidlserver.AIDLService"));
            bindService(intent, mServiceConnection, BIND_AUTO_CREATE);
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mServiceConnection!= null) {
            unbindService(mServiceConnection);
        }
    }
}

在客户端代码中,创建了一个 ServiceConnection 对象,在 onServiceConnected 方法中获取到服务端的 IMyAidlInterface 接口实例,然后通过该接口调用服务端的方法,传递基本数据类型和自定义的 User 对象。在 onCreate 方法中,点击绑定按钮时,通过 bindService 方法绑定服务端的 Service。在 onDestroy 方法中,解除对服务的绑定。

通过以上步骤,就完成了使用 AIDL 实现跨进程通信的过程,客户端可以向服务端发送数据,服务端可以接收并处理数据,实现了不同进程之间的交互。

3.3 Messenger

Messenger 是一种基于消息传递的进程间通信工具,它的底层实现是对 Binder 的一个简单封装,与 Handler 类似,可以用它来发送和处理消息 。

四、多进程模式与 IPC

4.1 多进程模式的开启

在 Android 中,开启多进程模式主要通过在 AndroidManifest.xml 文件中为四大组件(Activity、Service、Receiver、ContentProvider)指定 android:process 属性来实现。例如:

xml 复制代码
<activity android:name=".MainActivity" />
<activity android:name=".SecondActivity" android:process=":remote" />
<service android:name=".TestService" android:process="com.example.remote" />

上述代码中,SecondActivity 指定了 android:process=":remote",表示它运行在一个名为 ":remote" 的私有进程中,而 TestService 指定了 android:process="com.example.remote",表示它运行在一个名为 "com.example.remote" 的全局进程中。进程名以 ":" 开头的进程属于当前应用的私有进程,其他应用的组件不能和它运行在同一个进程中;不以 ":" 开头的进程属于全局进程,其他应用通过 shareUID 方式可以和它跑在同一个进程中。两个应用通过 ShareUID 运行在同一个进程中,需要满足两个应用有相同的 UID 并且签名相同 。

4.2 多进程模式带来的问题

多进程模式虽然能解决一些特定的问题,但也会带来一系列的问题,主要包括以下几个方面:

  • 静态成员和单例模式失效:由于每个进程都有自己独立的虚拟机和内存空间,不同进程中访问同一个类的对象会产生多个副本。这就导致在一个进程中修改了类的静态成员变量或单例对象的状态,在其他进程中是无法感知到的,因为它们访问的是不同的副本。例如,在一个进程中定义了一个单例类来管理用户的登录状态,当在这个进程中用户登录成功后修改了单例对象中的登录状态,而在另一个进程中获取这个单例对象时,其登录状态仍然是未登录,因为两个进程中的单例对象是不同的。
  • 线程同步机制失效:线程同步机制(如 synchronized 关键字、Lock 接口等)是基于同一个对象的锁机制来实现的。在多进程模式下,不同进程中的对象是相互独立的,即使使用相同的同步代码,也无法保证不同进程之间的线程同步。例如,在一个进程中使用 synchronized 关键字对一个对象进行加锁,以保证同一时间只有一个线程可以访问该对象的某个方法,但在另一个进程中,同样的代码无法对这个对象进行有效的同步控制,因为它们是不同进程中的不同对象。
  • SharedPreferences 可靠性下降:SharedPreferences 是 Android 中用于存储简单键值对数据的一种方式,其底层是通过读写 XML 文件来实现的。在多进程模式下,多个进程同时读写 SharedPreferences 文件时,可能会出现数据不一致或丢失的情况。例如,当一个进程正在写入 SharedPreferences 文件时,另一个进程也尝试读取或写入,就可能导致数据的冲突和错误。这是因为 SharedPreferences 本身并不支持多进程并发读写,在多进程环境下其可靠性会受到影响。
  • Application 多次创建:当一个组件运行在新的进程中时,系统会为该进程分配独立的虚拟机,这就相当于启动一个新的应用,自然会创建新的 Application 实例。例如,在一个应用中,定义了一个自定义的 Application 类,并在其中进行了一些初始化操作,如数据库连接的初始化、全局变量的设置等。当开启多进程后,每个进程都会创建一个新的 Application 实例,导致这些初始化操作被重复执行,可能会造成资源的浪费和冲突 。

4.3 解决方案

针对多进程模式带来的上述问题,可以采取以下相应的解决方案:

  • 使用 Intent 传递数据:在不同进程的组件之间传递数据时,可以通过 Intent 来实现。Intent 可以携带基本数据类型、实现了 Parcelable 或 Serializable 接口的对象等。例如,在一个 Activity 中启动另一个进程中的 Activity,并传递一个 User 对象,可以使用如下代码:
java 复制代码
        // 发送方Activity
        Intent intent = new Intent(this, SecondActivity.class);
        User user = new User(1, "张三", 20);
        intent.putExtra("user", user);
        startActivity(intent);

// 接收方Activity
        User receivedUser = getIntent().getParcelableExtra("user");
        if (receivedUser!= null) {
            Log.d("SecondActivity", "接收的用户信息:" + receivedUser.toString());
        }

这样可以在不同进程的 Activity 之间传递复杂对象,解决数据共享的部分问题。

  • 使用文件共享:通过文件来共享数据是一种简单有效的方式。不同进程可以读取和写入同一个文件,从而实现数据的共享。例如,可以将一些配置信息、缓存数据等存储在文件中,不同进程通过读取和写入文件来获取和更新这些数据。在使用文件共享时,需要注意文件的读写权限和并发访问的问题,可以通过加锁等方式来保证数据的一致性。例如,使用 Java 的 RandomAccessFile 类来进行文件的读写操作,并使用 synchronized 关键字对文件操作进行同步:
java 复制代码
public class FileUtil {
    private static final String FILE_PATH = "data.txt";

    public static synchronized void writeToFile(String data) {
        try (RandomAccessFile file = new RandomAccessFile(FILE_PATH, "rw")) {
            file.seek(file.length());
            file.writeBytes(data + "\n");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static synchronized String readFromFile() {
        StringBuilder data = new StringBuilder();
        try (RandomAccessFile file = new RandomAccessFile(FILE_PATH, "r")) {
            String line;
            while ((line = file.readLine())!= null) {
                data.append(line).append("\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return data.toString();
    }
}
  • 使用 AIDL:AIDL(Android Interface Definition Language)是一种用于实现进程间通信的接口定义语言,它基于 Binder 机制实现。通过 AIDL,可以定义跨进程通信的接口,实现不同进程之间的方法调用和数据传递。例如,在一个音乐播放应用中,服务端进程提供音乐播放的服务,客户端进程可以通过 AIDL 接口调用服务端的播放、暂停、切换歌曲等方法。具体实现步骤如前文所述,包括定义 AIDL 文件、实现服务端和客户端等。
  • 使用 ContentProvider:ContentProvider 是 Android 提供的一种跨进程数据共享的机制,它可以对外提供数据访问接口,其他应用可以通过 ContentResolver 来访问其提供的数据。例如,系统的联系人应用通过 ContentProvider 将联系人数据暴露出来,其他应用可以通过 ContentResolver 查询联系人信息。在使用 ContentProvider 时,需要在 AndroidManifest.xml 中注册,并实现其 query、insert、update、delete 等方法来提供数据的访问操作。例如,创建一个自定义的 ContentProvider 来提供用户数据的查询:
java 复制代码
public class UserContentProvider extends ContentProvider {
    private static final String AUTHORITY = "com.example.userprovider";
    private static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/users");

    @Override
    public boolean onCreate() {
        // 初始化操作
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        // 从数据库或其他数据源查询数据
        SQLiteDatabase db = getContext().openOrCreateDatabase("users.db", Context.MODE_PRIVATE, null);
        return db.query("users", projection, selection, selectionArgs, null, null, sortOrder);
    }

    // 其他方法的实现(insert、update、delete等)
}

然后在 AndroidManifest.xml 中注册:

xml 复制代码
<provider
    android:name=".UserContentProvider"
    android:authorities="com.example.userprovider"
    android:exported="true" />

在其他应用中,可以通过 ContentResolver 来查询数据:

java 复制代码
ContentResolver resolver = getContentResolver();
Cursor cursor = resolver.query(UserContentProvider.CONTENT_URI, null, null, null, null);
if (cursor!= null) {
    while (cursor.moveToNext()) {
        // 处理查询结果
    }
    cursor.close();
}
  • 使用 Messenger:Messenger 是一种基于消息传递的进程间通信方式,它的底层也是基于 Binder 机制实现的。通过 Messenger,客户端可以向服务端发送消息,服务端可以处理这些消息并返回结果。例如,在一个应用中,客户端进程可以通过 Messenger 向服务端进程发送计算任务的消息,服务端进程计算完成后将结果返回给客户端。具体实现步骤包括创建 Messenger、绑定服务、发送和接收消息等。例如,服务端创建一个 Messenger 并处理客户端发送的消息:
java 复制代码
public class MessengerService extends Service {
    private static final int MSG_SUM = 1;

    private final Messenger mMessenger = new Messenger(new IncomingHandler());

    @Override
    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }

    private static class IncomingHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_SUM:
                    int num1 = msg.arg1;
                    int num2 = msg.arg2;
                    int result = num1 + num2;
                    Messenger replyMessenger = msg.replyTo;
                    Message replyMessage = Message.obtain(null, MSG_SUM);
                    replyMessage.arg1 = result;
                    try {
                        replyMessenger.send(replyMessage);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }
}

客户端绑定服务并发送消息:

java 复制代码
public class MessengerClientActivity extends AppCompatActivity {
    private Messenger mService;
    private boolean mBound;

    private final Messenger mMessenger = new Messenger(new IncomingHandler());

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mService = new Messenger(service);
            mBound = true;

            Message message = Message.obtain(null, MessengerService.MSG_SUM);
            message.arg1 = 3;
            message.arg2 = 5;
            message.replyTo = mMessenger;
            try {
                mService.send(message);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mService = null;
            mBound = false;
        }
    };

    private static class IncomingHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MessengerService.MSG_SUM:
                    int result = msg.arg1;
                    Log.d("MessengerClient", "计算结果:" + result);
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_messenger_client);

        Intent intent = new Intent(this, MessengerService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mBound) {
            unbindService(mConnection);
            mBound = false;
        }
    }
}

通过以上这些方法,可以在一定程度上解决多进程模式带来的问题,实现不同进程之间的有效通信和数据共享。在实际开发中,需要根据具体的业务需求和场景选择合适的解决方案 。

五、IPC 的应用场景与案例分析

5.1 应用场景

在 Android 开发中,IPC 有着广泛的应用场景,以下是一些常见的场景:

  • 系统服务调用:当应用需要使用系统提供的服务时,如 ActivityManagerService、WindowManagerService、NotificationManager 等,就需要通过 IPC 与这些系统服务进行通信。例如,应用调用 ActivityManagerService 来启动一个新的 Activity,或者获取正在运行的应用列表;通过 NotificationManager 来发送通知等,这些操作都涉及到进程间通信,因为应用进程和系统服务进程是不同的进程。
  • 进程间数据共享:在一些复杂的应用中,可能会存在多个进程,这些进程之间需要共享数据。例如,一个新闻客户端应用,可能有一个进程负责网络数据的获取,另一个进程负责数据的存储和展示。负责网络数据获取的进程获取到新闻数据后,需要通过 IPC 将数据传递给负责展示的进程,以实现数据的共享和展示。
  • 跨应用通信:不同应用之间也可能需要进行通信和数据交互。比如,一个支付应用和电商应用之间,当用户在电商应用中进行支付操作时,电商应用需要通过 IPC 调用支付应用的支付功能,将支付相关的数据传递给支付应用,支付应用完成支付操作后再将支付结果通过 IPC 返回给电商应用 。
  • 多模块解耦:在大型应用开发中,为了实现模块的解耦和独立开发,可能会将不同的功能模块放在不同的进程中。例如,一个社交应用可能将消息推送模块、用户管理模块、聊天模块等分别放在不同的进程中,这些模块之间通过 IPC 进行通信,实现功能的协同工作,同时降低模块之间的耦合度,提高代码的可维护性和扩展性。

5.2 案例分析

以一个音乐播放应用为例,该应用包含两个进程,一个是主进程,负责界面展示和用户交互;另一个是音乐播放服务进程,负责音乐的播放、暂停、切换等操作。

在这个案例中,使用 AIDL 来实现主进程和音乐播放服务进程之间的通信。

  1. 定义 AIDL 文件:在服务端(音乐播放服务进程)定义一个 AIDL 文件,例如 IMusicPlayer.aidl,内容如下:
java 复制代码
// IMusicPlayer.aidl
package com.example.musicplayer;

interface IMusicPlayer {
    void play();
    void pause();
    void next();
    void previous();
}

上述 AIDL 文件定义了一个音乐播放的接口,包含播放、暂停、下一首、上一首等方法。

  1. 实现服务端:在音乐播放服务进程中创建一个 Service,并实现 IMusicPlayer 接口。例如:
java 复制代码
package com.example.musicplayer;

import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;

public class MusicPlayerService extends Service {
    private static final String TAG = "MusicPlayerService";
    private MediaPlayer mediaPlayer;

    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder();
    }

    class MyBinder extends IMusicPlayer.Stub {
        @Override
        public void play() throws RemoteException {
            if (mediaPlayer == null) {
                mediaPlayer = MediaPlayer.create(MusicPlayerService.this, R.raw.music);
                mediaPlayer.start();
            } else {
                mediaPlayer.start();
            }
            Log.d(TAG, "音乐开始播放");
        }

        @Override
        public void pause() throws RemoteException {
            if (mediaPlayer != null && mediaPlayer.isPlaying()) {
                mediaPlayer.pause();
                Log.d(TAG, "音乐暂停播放");
            }
        }

        @Override
        public void next() throws RemoteException {
            // 这里简单模拟下一首,实际应用中需要切换音乐资源等操作
            Log.d(TAG, "切换到下一首音乐");
        }

        @Override
        public void previous() throws RemoteException {
            // 这里简单模拟上一首,实际应用中需要切换音乐资源等操作
            Log.d(TAG, "切换到上一首音乐");
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 初始化MediaPlayer等操作
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mediaPlayer != null) {
            mediaPlayer.release();
            mediaPlayer = null;
        }
    }
}

在上述代码中,MusicPlayerService 创建了一个 MyBinder 类,实现了 IMusicPlayer 接口中的方法,在这些方法中实现了音乐播放、暂停等实际操作。

  1. 配置服务端 Service:在 AndroidManifest.xml 文件中注册服务端的 Service,如下:
xml 复制代码
<service
    android:name=".MusicPlayerService"
    android:enabled="true"
    android:exported="true">
</service>
  1. 实现客户端:在主进程中绑定服务,并通过 AIDL 接口与服务端进行通信。例如:
java 复制代码
package com.example.musicplayerclient;

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import com.example.musicplayer.IMusicPlayer;

public class MainActivity extends AppCompatActivity {
    private IMusicPlayer musicPlayer;

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            musicPlayer = IMusicPlayer.Stub.asInterface(service);
            try {
                musicPlayer.play();
                Toast.makeText(MainActivity.this, "音乐开始播放", Toast.LENGTH_SHORT).show();
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            musicPlayer = null;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button playButton = findViewById(R.id.play_button);
        Button pauseButton = findViewById(R.id.pause_button);

        playButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (musicPlayer!= null) {
                    try {
                        musicPlayer.play();
                        Toast.makeText(MainActivity.this, "音乐开始播放", Toast.LENGTH_SHORT).show();
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                } else {
                    Intent intent = new Intent();
                    intent.setComponent(new ComponentName("com.example.musicplayer", "com.example.musicplayer.MusicPlayerService"));
                    bindService(intent, connection, BIND_AUTO_CREATE);
                }
            }
        });

        pauseButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (musicPlayer!= null) {
                    try {
                        musicPlayer.pause();
                        Toast.makeText(MainActivity.this, "音乐暂停播放", Toast.LENGTH_SHORT).show();
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (connection!= null) {
            unbindService(connection);
        }
    }
}

在客户端代码中,MainActivity 通过 ServiceConnection 绑定音乐播放服务,在 onServiceConnected 方法中获取到 IMusicPlayer 接口实例,然后就可以调用接口中的方法来控制音乐的播放和暂停。当点击播放按钮时,如果已经绑定了服务,则直接调用 play 方法;如果未绑定服务,则先绑定服务再调用 play 方法。当点击暂停按钮时,直接调用 pause 方法。

通过这个案例可以看出,使用 AIDL 实现 IPC 能够有效地实现不同进程之间的通信和功能交互,满足了音乐播放应用中界面展示和音乐播放服务分离的需求,提高了应用的可维护性和扩展性。

六、总结

Android IPC 在现代移动应用开发中占据着至关重要的地位。从系统服务调用到进程间数据共享,再到跨应用通信以及大型应用的多模块解耦,IPC 机制为 Android 应用的功能实现和架构设计提供了坚实的基础。不同的 IPC 实现方式,如 Binder 机制、AIDL、Messenger 等,各自有着独特的优势和适用场景,开发者可以根据具体的业务需求进行合理选择。

随着移动应用的不断发展,对 IPC 的性能、安全性和易用性提出了更高的要求。未来,Android IPC 可能会朝着更加高效、安全和便捷的方向发展。例如,在性能方面,可能会进一步优化数据传输和处理的效率,减少通信开销;在安全性方面,会加强权限管理和数据加密,防止数据泄露和非法访问;在易用性方面,可能会提供更加简洁、直观的 API,降低开发者的使用门槛 。同时,随着新技术的不断涌现,如 5G 网络、物联网等,Android IPC 也将面临新的机遇和挑战,需要不断创新和发展,以满足日益增长的应用需求。

相关推荐
BD_Marathon8 小时前
【MySQL】函数
android·数据库·mysql
西西学代码8 小时前
安卓开发---耳机的按键设置的UI实例
android·ui
maki07713 小时前
虚幻版Pico大空间VR入门教程 05 —— 原点坐标和项目优化技巧整理
android·游戏引擎·vr·虚幻·pico·htc vive·大空间
千里马学框架13 小时前
音频焦点学习之AudioFocusRequest.Builder类剖析
android·面试·智能手机·车载系统·音视频·安卓framework开发·audio
fundroid17 小时前
掌握 Compose 性能优化三步法
android·android jetpack
TeleostNaCl17 小时前
如何在 IDEA 中使用 Proguard 自动混淆 Gradle 编译的Java 项目
android·java·经验分享·kotlin·gradle·intellij-idea
旷野说19 小时前
Android Studio Narwhal 3 特性
android·ide·android studio
maki0771 天前
VR大空间资料 01 —— 常用VR框架对比
android·ue5·游戏引擎·vr·虚幻·pico
xhBruce1 天前
InputReader与InputDispatcher关系 - android-15.0.0_r23
android·ims
领创工作室1 天前
安卓设备分区作用详解-测试机红米K40
android·java·linux