App内存优化-实践

945 阅读23分钟

多进程的优点

系统为每个应用分配一定大小的内存,从之前的 16M 到 32M、48M,甚至更高。但毕竟有限。

进程是资源分配的基本单位。也就是说,一个应用有对个进程,那这个应用可以获得更多的内存。

所以,开启多进程可以分担主进程的内存消耗,常见音乐类 APP 的后台播放,应用的推送服务等。

多进程的不足

1、数据共享问题

Android 系统为每个进程分配独立的虚拟机,不同的虚拟机之间数据不能共享,即使是静态成员还是单例模式。

2、线程同步机制失效

不同进程锁的不是同一个对象,无法保证线程同步了。

3、SharedPreferences 可靠性下降

SharedPreferences 还没有增加对多进程的支持。

4、Application 多次创建

当一个组件跑在新的进程中,系统要在创建进程的同时为其分配独立的虚拟机,自然就会创建新的 Application。这就导致了 application 的 onCreate方法重复执行全部的初始化代码。因此,可以根据进程需要进行最小的业务初始化。

多进程注意事项

  • 静态成员和单例模式会失效
  • 线程同步机制失效
  • SharePreference 稳定性不能保证,使用 ContentProvider 封装,对外提供数据服务
  • Application 会多次创建,需要注意多进程间使用的对象是否初始化 在 web 进程中调用主进程功能都需要注意 Context 和数据的读取,否则会出现空指针的问题.

Application 多次创建

不同进程共同的初始化业务逻辑 :

public class AppInitialization {

    /**
     * 不同进程共同的初始化代码
     * @param application
     */
    public void onAppCreate(Application application) {
        // init
    }

简单工厂模式 :

根据进程名进行对应进程的初始化逻辑。

public class AppInitFactory {
 
    public static AppInitialization getAppInitialization(String processName) {
        AppInitialization appInitialization;
        if (processName.endsWith(":second")) {
            appInitialization = new SecondApplication();
        } else if (processName.endsWith(":third")) {
            appInitialization = new ThirdApplication();
        } else {
            appInitialization = new AppInitialization();
        }
        return appInitialization;
    }

    static class SecondApplication extends AppInitialization {

        @Override
        public void onAppCreate(Application application) {
            super.onAppCreate(application);
            // init
        }
    }

    static class ThirdApplication extends AppInitialization {
        @Override
        public void onAppCreate(Application application) {
            super.onAppCreate(application);
            // init
        }
    }

具体调用代码 :

public class MyApplication extends Application {

    private static final String TAG = "MyApplication";

    @Override
    public void onCreate() {
        super.onCreate();
        String currentProcessName = getCurrentProcessName();
        Log.e(TAG, "currentProcessName : " + currentProcessName );
        AppInitialization appInitialization =                                AppInitFactory.getAppInitialization(currentProcessName);
        if (appInitialization != null) {
            appInitialization.onAppCreate(this);
        }
    }

    /**
     * 获取当前进程名称
     * @return
     */
    private String getCurrentProcessName() {
        String currentProcessName = "";
        int pid = android.os.Process.myPid();
        ActivityManager manager = (ActivityManager) this.getSystemService(Context.ACTIVITY_SERVICE);
        for (ActivityManager.RunningAppProcessInfo processInfo : manager.getRunningAppProcesses()) {
            if (processInfo.pid == pid) {
                currentProcessName = processInfo.processName;
                break;
            }
        }
        return currentProcessName;
    }
}

Web 独立进程

WebView 拆分为独立进程运行,从而减轻主进程内存压力很有必要,当内存紧张时,系统则会自动杀死 web 进程.拆分为多进程后,主要问题在于进程间通讯与主进程保活.

拆分 Web 进程

独立进程分为两种模式,私有独立进程 和 全局独立进程 两种模式,开始方式也很简单.

 <!-- 私有独立进程:与主进程同ShareUID,共享data目录、组件信息、共享内存数据  -->
 <activity
            android:name=".WebActivity"
            android:process=":web"/>
<!-- 全局独立进程:与主进程不同ShareUID -->
 <activity
            android:name=".WebActivity"
            android:process=".web"/>

如果只与本应用通信,不需要为全局独立进程.

当打开网页时,发送请求应该带用户信息,原来的信息存储方式是SharePreference,但是这种方式对于多进程调用时,容易出现不稳定的情况,并且它的多进程调用方式已经被标记为废弃,所以为了保证稳定性,使用ContentProvider将其封装,供不同的进程调用.

/**
 * 在 web 进程里只是获取用户信息,当web里返回要登录信息时,跳转至主进程里的登录页面,所以只实现了 query 方法,更新 SharePreference 依旧使用了单进程读写模式.
 */
public class UserProvider extends ContentProvider {

    private static String sAuthoriry = BuildConfig.APPLICATION_ID + ".UserProvider";

    @Override
    public boolean onCreate() {
        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        //这个 name 获取的就是 xml 的文件名,默认取 uri 的 path 字段的第一个
        if (!sAuthoriry.equals(uri.getAuthority())) {
            return null;
        }
        Bundle bundle = new Bundle();
        if (getContext() != null) {
            bundle.putString("user", SharedPreferUtil.get(getContext(), Constants.EXTRA_USER_CACHE, ""));
        }
        return new BundleCursor(bundle);
    }

    private static final class BundleCursor extends MatrixCursor {
        private Bundle mBundle;

        public BundleCursor(Bundle extras) {
            super(new String[]{}, 0);
            mBundle = extras;
        }

        @Override
        public Bundle getExtras() {
            return mBundle;
        }

        @Override
        public Bundle respond(Bundle extras) {
            mBundle = extras;
            return mBundle;
        }
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        throw new UnsupportedOperationException("No external call");
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        throw new UnsupportedOperationException("No external call");
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        throw new UnsupportedOperationException("No external call");
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        throw new UnsupportedOperationException("No external call");
    }
}

AndroidManifest.xml 配置

<!-- 由于只与本应用通信所以就没有配置权限 -->
 <provider
            android:name=".provider.UserProvider"
            android:authorities="${applicationId}.UserProvider"
            android:exported="true" />

这样就配置好了多进程读取 SharePreference 的 ContentProvider,凡是读取用户信息的地方都需替换为如下方式

@Nullable
public static User getUser() {
    String authority = "content://" + BuildConfig.APPLICATION_ID + ".UserProvider";
    Uri uri = Uri.parse(authority);
    //由于多进程模式,所以 Application 会多次初始化,MyApplication.getInstance() 的初始化不能写在某个进程里,如果这样则其他进程获取不到实例,导致这里会出现NPE
    Cursor cursor = MyApplication.getInstance().getContentResolver().query(uri, null, null, null, null);
    if (cursor != null) {
        Bundle args = cursor.getExtras();
        cursor.close();
        if (args != null) {
            return User.stringToUser(args.getString("user"));
        }
    }
    return null;
}

通过以上配置,就可以在不同进程里获取User对象.

我们业务中有个逻辑是分享网页,当用户点击网页分享按钮,调起分享页面,然后分享至微信,分享成功返回后调用 js,所以需要在微信回调中通知网页,使用BroadcastReceiver通知网页执行 js 脚本.

主进程保活

在完成进程拆分后测试中发现,当主进程占用一百多 MB 时红米 Note3 机器打开网页进程,再消耗一百多 MB时,系统会自动杀死主进程,导致返回到主进程会再次加载,为了避免这种问题发生,只能在网页进程启动后,将主进程置为前台进程. 进程保活话题如果要展开谈,可以写好多东西,这里只介绍我们应用的方法.逻辑很简单就是在主进程中启动一个前台 Service,然后再启动一个相同 ID 的 Service,最后停止一个 Service,这样通知栏里便不会出现通知,而应用在前台 oom_adj 值较高,进程不会被杀死.

public class KeepLiveService extends Service {
    public static final int NOTIFICATION_ID = 0x11;

    public KeepLiveService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onCreate() {
        super.onCreate();
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            //API 18 以下,直接发送 Notification 并将其置为前台
            startForeground(NOTIFICATION_ID, new Notification());
        } else {
            //API 18 以上,发送 Notification 并将其置为前台后,启动 InnerService
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.drawable.push);
            startForeground(NOTIFICATION_ID, builder.build());
            ContextCompat.startForegroundService(getApplicationContext(), new Intent(this, InnerService.class));
        }
    }

    public static class InnerService extends Service {

        public InnerService() {
        }

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

        @Override
        public void onCreate() {
            super.onCreate();
            //发送与 KeepLiveService 中I D 相同的 Notification,然后取消自己的前台显示
            Notification.Builder builder = new Notification.Builder(this);
            builder.setSmallIcon(R.drawable.push);
            startForeground(NOTIFICATION_ID, builder.build());
            stopSelf();
        }

    }
}

当然 Service 也必须在 AndroidManifest 中注册.

  • compleVerison 27 targetVersion 26 如果再为更高的版本则通知拦会显示出应用正在后台运行,给用户造成不好的体验
  • 为了在用户体验和内存消耗间平衡,在 Application 的 onTrimMemory中,当 level 值大于等于 TRIM_MEMORY_MODERATE 且 Web 进程在后台后,主动杀死 web 进程.

跨进程通讯

进程间的通信方式:

  • 四大组件间传递Bundle
  • 使用文件共享方式,多进程读写一个相同的文件,获取文件内容进行交互
  • AIDL
  • 使用Messenger,一种轻量级的跨进程通讯方案,底层使用AIDL实现
  • 使用ContentProvider,常用于多进程共享数据,比如系统的相册,音乐等,我们也可以通过ContentProvider访问到
  • Socket

Android的进程与进程之间通讯,有些不需要我们额外编写通讯代码,例如:把选择图片模块放到独立的进程,我们仍可以使用startActivityForResult方法,将选中的图片放到Bundle中,使用Intent传递即可。

但是对于把“消息推送Service”放到独立的进程,这个业务就稍微复杂点了,这个时候可能会发生Activity跟Service传递对象,调用Service方法等一系列复杂操作。

由于各个进程运行在相对独立的内存空间,所以它们是不能直接通讯的,因为程序里的变量、对象等初始化后都是具有内存地址的,举个简单的例子,读取一个变量的值,本质是找到变量的内存地址,取出存放的值。不同的进程,运行在相互独立的内存(其实就可以理解为两个不同的应用程序),显然不能直接得知对方变量、对象的内存地址,这样的话也自然不能访问对方的变量,对象等。此时两个进程进行交互,就需要使用跨进程通讯的方式去实现。简单说,跨进程通讯就是一种让进程与进程之间可以进行交互的技术。

AIDL是Android提供给我们的标准跨进程通讯API。Messenger也是使用AIDL实现的一种跨进程方式,Messenger顾名思义,就像是一种串行的消息机制,它是一种轻量级的IPC方案,可以在不同进程中传递Message对象,我们在Message中放入需要传递的数据即可轻松实现进程间通讯。但是当我们需要调用服务端方法,或者存在并发请求,那么Messenger就不合适了。而四大组件传递Bundle,这个就不需要解释了,把需要传递的数据,用Intent封装起来传递即可。

AIDL实现一个多进程消息推送

像图片选择这样的多进程需求,可能并不需要我们额外编写进程通讯的代码,使用四大组件传输Bundle就行了,但是像推送服务这种需求,进程与进程之间需要高度的交互,此时就绕不过进程通讯这一步了。 下面我们就用即时聊天软件为例,手动去实现一个多进程的推送例子,具体需求如下:

  • UI和消息推送的Service分两个进程;
  • UI进程用于展示具体的消息数据,把用户发送的消息,传递到消息Service,然后发送到远程服务器;
  • Service负责收发消息,并和远程服务器保持长连接,UI进程可通过Service发送消息到远程服务器,Service收到远程服务器消息通知UI进程;
  • 即使UI进程退出了,Service仍需要保持运行,收取服务器消息。

实现思路

  • 创建UI进程(下文统称为客户端);
  • 创建消息Service(下文统称为服务端);
  • 把服务端配置到独立的进程(AndroidManifest.xml中指定process标签);
  • 客户端和服务端进行绑定(bindService);
  • 让客户端和服务端具备交互的能力。(AIDL使用)

Step0. AIDL调用流程概览

开始之前,我们先来概括一下使用AIDL进行多进程调用的整个流程:

  • 客户端使用bindService方法绑定服务端;
  • 服务端在onBind方法返回Binder对象;
  • 客户端拿到服务端返回的Binder对象进行跨进程方法调用;

Step1.客户端使用bindService方法绑定服务端

创建客户端和服务端,把服务端配置到另外的进程

  • 创建客户端 -> MainActivity;
  • 创建服务端 -> MessageService;
  • 把服务端配置到另外的进程 -> android:process=”:remote”

上面描述的客户端、服务端、以及把服务端配置到另外进程,体现在AndroidManifest.xml中,如下所示:

<manifest ...>
    <application ...>
        <activity android:name=".ui.MainActivity"/>
        <service
            android:name=".service.MessageService"
            android:enabled="true"
            android:exported="true"
            android:process=":remote" />
    </application>
</manifest>

绑定MessageService到MainActivity

创建MessageService 此时的MessageService就是刚创建的模样,onBind中返回了null,下一步中我们将返回一个可操作的对象给客户端。

public class MessageService extends Service {
    public MessageService() {
    }
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

客户端MainActivity调用bindService方法绑定MessageService

这一步其实是属于Service组件相关的知识,在这里就比较简单地说一下,启动服务可以通过以下两种方式:

  • 使用bindService方法 -> bindService(Intent service, ServiceConnection conn, int flags);
  • 使用startService方法 -> startService(Intent service);

bindService & startService区别:

使用bindService方式,多个Client可以同时bind一个Service,但是当所有Client unbind后,Service会退出,通常情况下,如果希望和Service交互,一般使用bindService方法,使用onServiceConnected中的IBinder对象可以和Service进行交互,不需要和Service交互的情况下,使用startService方法即可。

正如上面所说,我们是要和Service交互的,所以我们需要使用bindService方法,但是我们希望unbind后Service仍保持运行,这样的情况下,可以同时调用bindService和startService(比如像本例子中的消息服务,退出UI进程,Service仍需要接收到消息),代码如下:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setupService();
    }
    /**
     * unbindService
     */
    @Override
    protected void onDestroy() {
        unbindService(serviceConnection);
        super.onDestroy();
    }
    /**
     * bindService & startService
     */
    private void setupService() {
        Intent intent = new Intent(this, MessageService.class);
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
        startService(intent);
    }
    ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.d(TAG, "onServiceConnected");
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.d(TAG, "onServiceDisconnected");
        }
    };
}

Stpe2.服务端在onBind方法返回Binder对象

首先,什么是Binder?

要说Binder,首先要说一下IBinder这个接口,IBinder是远程对象的基础接口,轻量级的远程过程调用机制的核心部分,该接口描述了与远程对象交互的抽象协议,而Binder实现了IBinder接口,简单说,Binder就是Android SDK中内置的一个多进程通讯实现类,在使用的时候,我们不用也不要去实现IBinder,而是继承Binder这个类即可实现多进程通讯。

其次,这个需要在onBind方法返回的Binder对象从何而来?

在这里就要引出本文中的主题了——AIDL 多进程中使用的Binder对象,一般通过我们定义好的 .adil 接口文件自动生成,当然你可以走野路子,直接手动编写这个跨进程通讯所需的Binder类,其本质无非就是一个继承了Binder的类,鉴于野路子走起来麻烦,而且都是重复步骤的工作,Google提供了 AIDL 接口来帮我们自动生成Binder这条正路。

定义AIDL接口

很明显,接下来我们需要搞一波上面说的Binder,让客户端可以调用到服务端的方法,而这个Binder又是通过AIDL接口自动生成,那我们就先从AIDL搞起,搞之前先看看注意事项,以免出事故:

AIDL支持的数据类型:

Java 编程语言中的所有基本数据类型(如 int、long、char、boolean 等等) String和CharSequence Parcelable:实现了Parcelable接口的对象 List:其中的元素需要被AIDL支持,另一端实际接收的具体类始终是 ArrayList,但生成的方法使用的是 List 接口 Map:其中的元素需要被AIDL支持,包括 key 和 value,另一端实际接收的具体类始终是 HashMap,但生成的方法使用的是 Map 接口

其他注意事项:

在AIDL中传递的对象,必须实现Parcelable序列化接口; 在AIDL中传递的对象,需要在类文件相同路径下,创建同名、但是后缀为.aidl的文件,并在文件中使用parcelable关键字声明这个类; 跟普通接口的区别:只能声明方法,不能声明变量; 所有非基础数据类型参数都需要标出数据走向的方向标记。可以是 in、out 或 inout,基础数据类型默认只能是 in,不能是其他方向。

创建一个AIDL接口,接口中提供发送消息的方法(Android Studio创建AIDL:项目右键 -> New -> AIDL -> AIDL File),代码如下:

package com.example.aidl;
import com.example.aidl.data.MessageModel;
interface MessageSender {
    void sendMessage(in MessageModel messageModel);
}

被“in”标记的参数,就是接收实际数据的参数,这个跟我们普通参数传递一样的含义。在AIDL中,“out” 指定了一个仅用于输出的参数,换而言之,这个参数不关心调用方传递了什么数据过来,但是这个参数的值可以在方法被调用后填充(无论调用方传递了什么值过来,在方法执行的时候,这个参数的初始值总是空的),这就是“out”的含义,仅用于输出。而“inout”显然就是“in”和“out”的合体了,输入和输出的参数。区分“in”、“out”有什么用?这是非常重要的,因为每个参数的内容必须编组(序列化,传输,接收和反序列化)。in/out标签允许Binder跳过编组步骤以获得更好的性能。

上述的MessageModel为消息的实体类,该类在AIDL中传递,实现了Parcelable序列化接口,代码如下:

public class MessageModel implements Parcelable {
    private String from;
    private String to;
    private String content;
    ...
    Setter & Getter
    ...
    @Override
    public int describeContents() {
        return 0;
    }
    //...
    序列化相关代码
    //...
}

手动实现Parcelable接口比较麻烦,安利一款AS自动生成插件android-parcelable-intellij-plugin 创建完MessageModel这个实体类,别忘了还有一件事要做:”在AIDL中传递的对象,需要在类文件相同路径下,创建同名、但是后缀为.aidl的文件,并在文件中使用parcelable关键字声明这个类“。代码如下:

package com.example.aidl.data;
parcelable MessageModel;

我们刚刚新增的3个文件:

  • MessageSender.aidl -> 定义了发送消息的方法,会自动生成名为MessageSender.Stub的Binder类,在服务端实现,返回给客户端调用
  • MessageModel.java -> 消息实体类,由客户端传递到服务端,实现了Parcelable序列化
  • MessageModel.aidl -> 声明了MessageModel可在AIDL中传递,放在跟MessageModel.java相同的包路径下

在服务端创建MessageSender.aidl这个AIDL接口自动生成的Binder对象,并返回给客户端调用,服务端MessageService代码如下:

public class MessageService extends Service {
    private static final String TAG = "MessageService";
    public MessageService() {
    }
    IBinder messageSender = new MessageSender.Stub() {
        @Override
        public void sendMessage(MessageModel messageModel) throws RemoteException {
            Log.d(TAG, "messageModel: " + messageModel.toString());
        }
    };
    @Override
    public IBinder onBind(Intent intent) {
        return messageSender;
    }
}

MessageSender.Stub是Android Studio根据我们MessageSender.aidl文件自动生成的Binder对象(至于是怎样生成的,下文会有答案),我们需要把这个Binder对象返回给客户端。

客户端拿到Binder对象后调用远程方法

调用步骤如下:

  • 在客户端的onServiceConnected方法中,拿到服务端返回的Binder对象;
  • 使用MessageSender.Stub.asInterface方法,取得MessageSender.aidl对应的操作接口;
  • 取得MessageSender对象后,像普通接口一样调用方法即可。
public class MainActivity extends AppCompatActivity {
    private MessageSender messageSender; 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        setupService();
    }
    //...
    
    private void setupService() {
        Intent intent = new Intent(this, MessageService.class);
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
        startService(intent);
    }
    ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //使用asInterface方法取得AIDL对应的操作接口
            messageSender = MessageSender.Stub.asInterface(service);
            //生成消息实体对象
            MessageModel messageModel = new MessageModel();
            messageModel.setFrom("client user id");
            messageModel.setTo("receiver user id");
            messageModel.setContent("This is message content");
            //调用远程Service的sendMessage方法,并传递消息实体对象
            try {
                messageSender.sendMessage(messageModel);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };
}

在客户端中我们调用了MessageSender的sendMessage方法,向服务端发送了一条消息,并把生成的MessageModel对象作为参数传递到了服务端,最终服务端打印的结果如下:

客户端与服务端通信过程

我们先来回顾一下从客户端发起的调用流程:

  • MessageSender messageSender = MessageSender.Stub.asInterface(service);
  • messageSender.sendMessage(messageModel);

抛开其它无关代码,客户端调跨进程方法就这两个步骤,而这两个步骤都封装在 MessageSender.aidl 最终生成的 MessageSender.java 源码

public interface MessageSender extends android.os.IInterface {
   
    public static abstract class Stub extends android.os.Binder implements com.example.aidl.MessageSender {
        private static final java.lang.String DESCRIPTOR = "com.example.aidl.MessageSender";
        /**
         * 把IBinder对象转换为 com.example.aidl.MessageSender 接口
         * 判断IBinder是否处于相同进程,相同进程返回Stub实现的com.example.aidl.MessageSender接口
         * 不同进程,则返回Stub.Proxy实现的com.example.aidl.MessageSender接口
         */
        public static com.example.aidl.MessageSender asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.example.aidl.MessageSender))) {
                return ((com.example.aidl.MessageSender) iin);
            }
            return new com.example.aidl.MessageSender.Stub.Proxy(obj);
        }
        /**
         * 同一进程时,不会触发
         *
         * 不同进程时,asInterface会返回Stub.Proxy,客户端调用 messageSender.sendMessage(messageModel)
         * 实质是调用了 Stub.Proxy 的 sendMessage 方法,从而触发跨进程数据传递,
         * 最终Binder底层将处理好的数据回调到此方法,并调用我们真正的sendMessage方法
         */
        @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_sendMessage: {
                    data.enforceInterface(DESCRIPTOR);
                    com.example.aidl.data.MessageModel _arg0;
                    if ((0 != data.readInt())) {
                        _arg0 = com.example.aidl.data.MessageModel.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    this.sendMessage(_arg0);
                    reply.writeNoException();
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }
        private static class Proxy implements com.example.aidl.MessageSender {
            private android.os.IBinder mRemote;
            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }
            /**
             * Proxy中的sendMessage方法,并不是直接调用我们定义的sendMessage方法,而是经过一顿的Parcel读写,
             * 然后调用mRemote.transact方法,把数据交给Binder处理,transact处理完毕后会调用上方的onTransact方法,
             * onTransact拿到最终得到的参数数据,调用由我们真正的sendMessage方法
             */
            @Override
            public void sendMessage(com.example.aidl.data.MessageModel messageModel) 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 ((messageModel != null)) {
                        _data.writeInt(1);
                        messageModel.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    //调用Binder的transact方法进行多进程数据传输,处理完毕后调用上方的onTransact方法
                    mRemote.transact(Stub.TRANSACTION_sendMessage, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }
        }
        static final int TRANSACTION_sendMessage = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    }
    public void sendMessage(com.example.aidl.data.MessageModel messageModel) throws android.os.RemoteException;
}

从客户端的sendMessage开始,整个AIDL的调用过程如上图所示,asInterface方法,将会判断onBind方法返回的Binder是否存处于同一进程,在同一进程中,则进行常规的方法调用,若处于不同进程,整个数据传递的过程则需要通过Binder底层去进行编组(序列化,传输,接收和反序列化),得到最终的数据后再进行常规的方法调用。

敲黑板:对象跨进程传输的本质就是 序列化,传输,接收和反序列化 这样一个过程,这也是为什么跨进程传输的对象必须实现Parcelable接口

跨进程的回调接口

在上面我们已经实现了从客户端发送消息到跨进程服务端的功能,接下来我们还需要将服务端接收到的远程服务器消息,传递到客户端。有同学估计会说:“这不就是一个回调接口的事情嘛”,设置回调接口思路是对的,但是在这里使用的回调接口有点不一样,在AIDL中传递的接口,不能是普通的接口,只能是AIDL接口,所以我们需要新建一个AIDL接口传到服务端,作为回调接口。

新建消息收取的AIDL接口MessageReceiver.aidl:

package com.example.aidl;
import com.example.aidl.data.MessageModel;
interface MessageReceiver {
    void onMessageReceived(in MessageModel receivedMessage);
}

接下来我们把回调接口注册到服务端去,修改我们的MessageSender.aidl:

package com.example.aidl;
import com.example.aidl.data.MessageModel;
import com.example.aidl.MessageReceiver;
interface MessageSender {
    void sendMessage(in MessageModel messageModel);
    void registerReceiveListener(MessageReceiver messageReceiver);
    void unregisterReceiveListener(MessageReceiver messageReceiver);
}

以上就是我们最终修改好的aidl接口,接下来我们需要做出对应的变更:

  • 在服务端中增加MessageSender的注册和反注册接口的方法;
  • 在客户端中实现MessageReceiver接口,并通过MessageSender注册到服务端。
public class MainActivity extends AppCompatActivity {
    private MessageSender messageSender;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        //...
    }
    /**
     * 1.unregisterListener
     * 2.unbindService
     */
    @Override
    protected void onDestroy() {
    	//解除消息监听接口
        if (messageSender != null && messageSender.asBinder().isBinderAlive()) {
            try {
                messageSender.unregisterReceiveListener(messageReceiver);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        unbindService(serviceConnection);
        super.onDestroy();
    }
    //消息监听回调接口
    private MessageReceiver messageReceiver = new MessageReceiver.Stub() {
        @Override
        public void onMessageReceived(MessageModel receivedMessage) throws RemoteException {
            Log.d(TAG, "onMessageReceived: " + receivedMessage.toString());
        }
    };
    ServiceConnection serviceConnection = new ServiceConnection() {
    
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //使用asInterface方法取得AIDL对应的操作接口
            messageSender = MessageSender.Stub.asInterface(service);
            //生成消息实体对象
            MessageModel messageModel = new MessageModel();
            //...
            try {
                //把接收消息的回调接口注册到服务端
                messageSender.registerReceiveListener(messageReceiver);
                //调用远程Service的sendMessage方法,并传递消息实体对象
                messageSender.sendMessage(messageModel);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };
}

客户端主要有3个变更:

  • 增加了messageReceiver对象,用于监听服务端的消息通知;
  • onServiceConnected方法中,把messageReceiver注册到Service中去;
  • onDestroy时候解除messageReceiver的注册。

服务端MessageServie进行变更:

public class MessageService extends Service {
    private static final String TAG = "MessageService";
    private AtomicBoolean serviceStop = new AtomicBoolean(false);
    //RemoteCallbackList专门用来管理多进程回调接口
    private RemoteCallbackList<MessageReceiver> listenerList = new RemoteCallbackList<>();
    public MessageService() {
    }
    IBinder messageSender = new MessageSender.Stub() {
        @Override
        public void sendMessage(MessageModel messageModel) throws RemoteException {
            Log.e(TAG, "messageModel: " + messageModel.toString());
        }
        @Override
        public void registerReceiveListener(MessageReceiver messageReceiver) throws RemoteException {
            listenerList.register(messageReceiver);
        }
        @Override
        public void unregisterReceiveListener(MessageReceiver messageReceiver) throws RemoteException {
            listenerList.unregister(messageReceiver);
        }
    };
    @Override
    public IBinder onBind(Intent intent) {
        return messageSender;
    }
    @Override
    public void onCreate() {
        super.onCreate();
        new Thread(new FakeTCPTask()).start();
    }
    @Override
    public void onDestroy() {
        serviceStop.set(true);
        super.onDestroy();
    }
    //模拟长连接,通知客户端有新消息到达
    private class FakeTCPTask implements Runnable {
        @Override
        public void run() {
            while (!serviceStop.get()) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                MessageModel messageModel = new MessageModel();
                messageModel.setFrom("Service");
                messageModel.setTo("Client");
                messageModel.setContent(String.valueOf(System.currentTimeMillis()));
                /**
                 * RemoteCallbackList的遍历方式
                 * beginBroadcast和finishBroadcast一定要配对使用
                 */
                final int listenerCount = listenerList.beginBroadcast();
                Log.d(TAG, "listenerCount == " + listenerCount);
                for (int i = 0; i < listenerCount; i++) {
                    MessageReceiver messageReceiver = listenerList.getBroadcastItem(i);
                    if (messageReceiver != null) {
                        try {
                            messageReceiver.onMessageReceived(messageModel);
                        } catch (RemoteException e) {
                            e.printStackTrace();
                        }
                    }
                }
                listenerList.finishBroadcast();
            }
        }
    }
}

服务端主要变更:

  • MessageSender.Stub实现了注册和反注册回调接口的方法;
  • 增加了RemoteCallbackList来管理AIDL远程接口;
  • FakeTCPTask模拟了长连接通知客户端有新消息到达。(这里的长连接可以是XMPP,Mina,Mars,Netty等,这里弄个假的意思意思,有时间的话咱开个帖子聊聊XMPP)

这里还有一个需要讲一下的,就是RemoteCallbackList,为什么要用RemoteCallbackList,普通ArrayList不行吗?当然不行,不然干嘛又整一个RemoteCallbackList,registerReceiveListener 和 unregisterReceiveListener在客户端传输过来的对象,经过Binder处理,在服务端接收到的时候其实是一个新的对象,这样导致在 unregisterReceiveListener 的时候,普通的ArrayList是无法找到在 registerReceiveListener 时候添加到List的那个对象的,但是它们底层使用的Binder对象是同一个,RemoteCallbackList利用这个特性做到了可以找到同一个对象,这样我们就可以顺利反注册客户端传递过来的接口对象了。RemoteCallbackList在客户端进程终止后,它能自动移除客户端所注册的listener,它内部还实现了线程同步,所以我们在注册和反注册都不需要考虑线程同步,的确是个666的类。

DeathRecipient

不知道你有没有感觉到,两个进程交互总是觉得缺乏那么一点安全感…比如说服务端进程Crash了,而客户端进程想要调用服务端方法,这样就调用不到了。此时我们可以给Binder设置一个DeathRecipient对象,当Binder意外挂了的时候,我们可以在DeathRecipient接口的回调方法中收到通知,并作出相应的操作,比如重连服务等等。

DeathRecipient的使用如下:

  • 声明DeathRecipient对象,实现其binderDied方法,当binder死亡时,会回调binderDied方法;
  • 给Binder对象设置DeathRecipient对象。

在客户端MainActivity声明DeathRecipient:

/**
    * Binder可能会意外死忙(比如Service Crash),Client监听到Binder死忙后可以进行重连服务等操作
    */
   IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
       @Override
       public void binderDied() {
           Log.d(TAG, "binderDied");
           if (messageSender != null) {
               messageSender.asBinder().unlinkToDeath(this, 0);
               messageSender = null;
           }
           //// TODO: 2017/2/28 重连服务或其他操作
           setupService();
       }
   };
   
   ServiceConnection serviceConnection = new ServiceConnection() {
       @Override
       public void onServiceConnected(ComponentName name, IBinder service) {
           //...
           try {
               //设置Binder死亡监听
               messageSender.asBinder().linkToDeath(deathRecipient, 0);
           } catch (RemoteException e) {
               e.printStackTrace();
           }
       }
       //...
   };

Binder中两个重要方法:

  • linkToDeath -> 设置死亡代理 DeathRecipient 对象;
  • unlinkToDeath -> Binder死亡的情况下,解除该代理。

此外,Binder中的isBinderAlive也可以判断Binder是否死亡。

权限验证

介绍两种常用验证方法:

  • 在服务端的onBind中校验自定义permission,如果通过了我们的校验,正常返回Binder对象,校验不通过返回null,返回null的情况下客户端无法绑定到我们的服务;
  • 在服务端的onTransact方法校验客户端包名,不通过校验直接return false,校验通过执行正常的流程。

自定义permission,在Androidmanifest.xml中增加自定义的权限:

<permission
    android:name="com.example.aidl.permission.REMOTE_SERVICE_PERMISSION"
    android:protectionLevel="normal" />
<uses-permission android:name="com.example.aidl.permission.REMOTE_SERVICE_PERMISSION" />

服务端检查权限的方法:

IBinder messageSender = new MessageSender.Stub() {
    //...
    @Override
    public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        /**
         * 包名验证方式
         */
        String packageName = null;
        String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
        if (packages != null && packages.length > 0) {
            packageName = packages[0];
        }
        if (packageName == null || !packageName.startsWith("com.example.aidl")) {
            Log.d("onTransact", "拒绝调用:" + packageName);
            return false;
        }
        return super.onTransact(code, data, reply, flags);
    }
};
@Override
public IBinder onBind(Intent intent) {
    //自定义permission方式检查权限
    if (checkCallingOrSelfPermission("com.example.aidl.permission.REMOTE_SERVICE_PERMISSION") == PackageManager.PERMISSION_DENIED) {
        return null;
    }
    return messageSender;
}