前言
基于安卓平台的序列化与反序列化封装组件Parceler (https://github.com/johncarl81/parceler),实现了鸿蒙化迁移和重构,代码已经开源到(https://gitee.com/isrc_ohos/parceler_ohos),目前已经获得了很多人的Star和Fork ,欢迎各位下载使用并提出宝贵意见!
背景
序列化是指将Java对象转换为字节序列的过程,本质上是把实体对象状态按照一定的格式写入到有序字节流;而反序列化则是将字节序列转换为Java对象的过程,本质上是从有序字节流重建对象,恢复对象状态。当两个Java进程相互通信时,就需要使用Java序列化与反序列化方法来实现进程间的对象传送。鸿蒙中Parceler_ohos组件可将不同种类型的数据进行序列化和反序列化封装,从而达到进程间对象传送的效果。
组件效果展示
Parceler_ohos组件支持多种数据的序列化和反序列化处理,包括基础数据类、数组、Map类、Set类、以及序列化数据类,各类型中包含的具体数据类如下:
- 基础数据类:int、float、String、boolean......;
- 数组:PlainArray、PlainBooleanArray;
- Map类:Map、HashMap、LinkedHashMap......;
- Set类:Set、HashSet、SortedSet......;
- 序列化数据类:Sequensable、Serializable。
组件以上述数据中的五种为例进行序列化和反序列化演示,分别是:int、float、String、plainArray、Sequenceable。
用户点击组件图标后进入“Parceler测试”主界面,该界 面包含“int测试”、“float测试”、“String测试”、“plainArray测试”、“Sequenceable测试”五个按钮。点击上述各按钮可跳转至相应类型数据的序列化和反序列化测试界面。由于这五种数据的测试步骤相同,接下来就以组件中int数据的测试效果为例进行展示。
点击“int测试”按钮进入“int格式测试”界面;再点击“开始测试”按钮,即可将事先设定好的整型测试数据“34258235”转换成序列化字节流,并将序列化结果显示在文字“序列化:”的下方;然后将字节流反序列化并输出,输出内容为序列化之前的对象“34258235”;点击“返回”按钮后则可跳转回主界面,上述测试效果如图1所示。
图1 int数据序列化与反序列化测试
Sample解析
Sample工程文件中的MainMenu文件用于构建组件应用的主界面,其他文件用于构建上述组件效果展示的五种数据的序列化和反序列化测试界面,其对应关系如图2所示。由于各种数据的测试步骤相同,下文将以int数据的序列化和反序列化测试为例进行详细讲解,其他数据类型不再赘述。
图 2 Sample中各文件与测试界面中按钮的对应关系
MainMenu文件
主界面的布局比较简单,主要是设置进入五种数据测试界面的按钮,具体步骤如下:
- 第1步:声明五种数据的测试按钮和标题。
- 第2步:创建页面布局。
- 第3步:为int数据测试按钮设置监听。
- 第4步:创建int数据测试按钮。
1、声明五种数据的测试按钮和标题
声明用于显示标题“Parceler测试”的Text文本,以及用于跳转到具体测试界面的5个Button按钮,并将它们分别按类型命名。
- private Text title;//标题“Parceler测试”
- private Button IntTestButton;//int数据测试按钮
- private Button FloatTestButton;//float数据测试按钮
- private Button StringTestButton;//String数据测试按钮
- private Button PlainArrayTestButton;//PlainArray数据测试按钮
- private Button SequenceableTestButton;//Sequenceable数据测试按钮
2、创建页面布局
创建一个纵向显示的整体页面布局,其宽度和高度都跟随父控件变化而调整,上下左右四个方向的填充间距依次是10/32/10/80,并设置背景颜色为白色。
- private DirectionalLayout directionalLayout = new DirectionalLayout(this);
- ......
- directionalLayout.setWidth(ComponentContainer.LayoutConfig.MATCH_PARENT);
- directionalLayout.setHeight(ComponentContainer.LayoutConfig.MATCH_PARENT);
- directionalLayout.setOrientation(Component.VERTICAL);
- directionalLayout.setPadding(10, 32, 10, 80);//填充间距
- ShapeElement element = new ShapeElement();
- element.setShape(ShapeElement.RECTANGLE);
- element.setRgbColor(new RgbColor(255, 255, 255)); //白色背景
- directionalLayout.setBackground(element);
3、为int数据测试按钮设置监听
对int数据测试按钮设置onClick()点击监听事件,实现点击按钮可跳转至int数据测试界面的效果。具体代码如下。
- //初始化int按钮监听
- Component.ClickedListener intTestListener = new Component.ClickedListener() {
- @Override
- public void onClick(Component component) {
- AbilitySlice intSlice = new IntTest();
- Intent intent = new Intent();
- present(intSlice,intent); // 跳转至int数据测试界面
- }
- };
4、创建int数据测试按钮
Sample中包含的5个测试按钮除了显示的文本信息和按钮监听事件不同以外,其他属性完全一致,这些属性包括:按钮颜色、倒角、显示文本大小、对齐方式、按钮内填充距离等。
- private DirectionalLayout.LayoutConfig layoutConfig = new DirectionalLayout.LayoutConfig(ComponentContainer.LayoutConfig.MATCH_CONTENT,ComponentContainer.LayoutConfig.MATCH_CONTENT);
- ......
- //设置按钮属性
- ShapeElement background = new ShapeElement();
- background.setRgbColor(new RgbColor(0xFF51A8DD));//创建按钮颜色
- background.setCornerRadius(25);//创建按钮边框弧度
- layoutConfig.alignment = LayoutAlignment.HORIZONTAL_CENTER; //对齐方式
- layoutConfig.setMargins(0,100,0,0);//按钮的边界位置
- ......
- //创建处理int数据的按钮
- IntTestButton = new Button(this); //创建按钮
- IntTestButton.setLayoutConfig(layoutConfig);
- IntTestButton.setText("int测试"); //按钮文本
- IntTestButton.setTextSize(80); //文本大小
- IntTestButton.setBackground(background); //按钮背景
- IntTestButton.setPadding(10, 10, 10, 10); //按钮填充间距
- IntTestButton.setClickedListener(intTestListener); //按钮的监听
- directionalLayout.addComponent(IntTestButton); //按钮添加到布局
IntTest文件
上文中我们已经对主界面布局的实现进行了介绍,接下来就具体以int型数据序列化和反序列化测试为例,为大家讲解如何使用Parceler_ohos组件。该组件处理int类型的数据共分为4个步骤:
- 第1步:导入相关类。
- 第2步:创建布局。
- 第3步:设置输入数据和输出数据。
- 第4步:创建测试按钮。
1、导入相关类
在IntTest文件中,通过import关键字导入Parcels类,该类提供了数据序列化和反序列化实现的具体方法。
- import org.parceler.Parcels;
2、创建布局
创建一个纵向显示的int数据测试页面布局,宽度和高度都跟随父控件变化而调整,上下左右四个方向的填充间距依次是32/32/80/80,并设置背景颜色为白色。
- private DirectionalLayout directionalLayout = new DirectionalLayout(this);
- ......
- //布局属性
- directionalLayout.setWidth(ComponentContainer.LayoutConfig.MATCH_PARENT); directionalLayout.setHeight(ComponentContainer.LayoutConfig.MATCH_PARENT);
- directionalLayout.setOrientation(Component.VERTICAL);
- directionalLayout.setPadding(32, 32, 80, 80);
- //ShapeElement设置背景
- ShapeElement element = new ShapeElement();
- element.setShape(ShapeElement.RECTANGLE);
- element.setRgbColor(new RgbColor(255, 255, 255));
- directionalLayout.setBackground(element);
3、设置输入数据和输出数据
首先设置一个int数据作为输入数据,将测试数据的值34258235赋给int类对象intIn,并使用setText()方法将其以文本的形式显示在界面上,格式为:“输入:34258235”。然后分别实例化两个Text类对象,一个是wrappedOutput,用来显示输入数据的序列化输出,格式为"序列化:xxx",另一个是output,用来显示上述序列化结果的反序列化输出,格式为"输出:xxx"。
- //设定输入数据
- intIn = 34258235;
- input.setText("输入:" + intIn);
- directionalLayout.addComponent(input);
- //初始化序列化后输出
- wrappedOutput = new Text(this);
- wrappedOutput.setText("序列化:");
- directionalLayout.addComponent(wrappedOutput);
- //初始化反序列化后输出
- output = new Text(this);
- output.setText("输出:");
- directionalLayout.addComponent(output);
4、创建测试按钮
界面上触发序列化和反序列化操作的按钮为“开始测试”按钮。当该按钮被点击后,会调用wrap()方法对输入数据执行序列化操作,并将得到的字节流信息显示在上述第3步实现的“序列化:”文本后方;调用unwrap()方法将序列化后的字节流完成反序列化操作,并将得的整型对象输出在上述第3步实现的的“输出:”文本后方。
- private DirectionalLayout.LayoutConfig layoutConfig = new DirectionalLayout.LayoutConfig(ComponentContainer.LayoutConfig.MATCH_CONTENT,ComponentContainer.LayoutConfig.MATCH_CONTENT); //宽和高跟随父控件
- ......
- //创建“开始测试”按钮
- beginTestButton = new Button(this);
- layoutConfig.alignment = LayoutAlignment.HORIZONTAL_CENTER; //居中
- layoutConfig.setMargins(0,100,0,0);
- beginTestButton.setLayoutConfig(layoutConfig);
- beginTestButton.setText("开始测试"); //文本
- beginTestButton.setTextSize(80); //文本大小
- ShapeElement background = new ShapeElement();
- background.setRgbColor(new RgbColor(0xFF51A8DD));
- background.setCornerRadius(25); //倒角
- beginTestButton.setBackground(background); //背景
- beginTestButton.setPadding(10, 10, 10, 10); //填充距离
- beginTestButton.setClickedListener(beginTestListener); //监听
- directionalLayout.addComponent(beginTestButton); //按钮添加到布局
- //按钮监听
- public void onClick(Component Component) {
- //序列化测试
- intWrapped = Parcels.wrap(intIn);
- //反序列化测试
- intOut = Parcels.unwrap(intWrapped);
- ......
- }
Library解析
Parceler_ohos组件能够针对不同类型的数据,进行序列化和反序列化的操作。在使用Parceler_ohos组件实现序列化和反序列化操作时,只需分别调用Pacels类中的wrap()和unWrap()方法,并将待操作数据作为唯一参数传入即可,具体使用方法在上文Sample解析中已有详细讲解,此处不再进行赘述。接下来分别从Parceler_ohos组件的序列化和反序列化方法原理解析、以及转换器原理这两个方面进行讲解。
1、序列化和反序列化方法原理解析
(1)wrap()方法实现序列化
实现序列化操作的原理和函数调用关系可以参考下图3。
图3 序列化操作原理示意图
在Parcels类中,通过方法重载分别实现了两种wrap()方法。第一种wrap()方法是直接被开发者调用的方法,仅有一个输入数据作为参数,在判断输入数据不为空后,获取输入数据的具体数据类型并调用第二种wrap()方法。
- //wrap()方法一
- @SuppressWarnings("unchecked")
- public static <T> Sequenceable wrap(T input) {
- if(input == null){//空判断,判断输入数据是否为空
- return null;//若输入数据为空,则返回空值
- }
- return wrap(input.getClass(), input);//调用wrap()方法二
- }
第二种wrap()方法有两个参数:输入数据类型和数据本身。在判断输入数据不为空后,将输入数据类型inputType作为ParcelCodeRepository类get()方法的参数,返回一个Parcelable工厂类对象parcelableFactory;再通过parcelableFactory调用buildParcelable()方法,执行具体的序列化操作。
- //wrap()方法二
- @SuppressWarnings("unchecked")
- public static <T> Sequenceable wrap(Class<? extends T> inputType, T input) {
- if(input == null){//空判断,判断输入数据是否为空
- return null;//若输入数据为空,则返回空值
- }
- //ParcelCodeRepository类获取值赋给工厂类对象
- ParcelableFactory parcelableFactory = REPOSITORY.get(inputType);
- return parcelableFactory.buildParcelable(input);//工厂类对象执行具体序列化操作
- }
其中,parcelableFactory.buildParcelable(input)方法具体执行过程是:
a.实现泛型类接口
实现ParcelableFactory泛型类接口;再将输入数据作为入参传入buildParcelable(input)方法。
- public interface ParcelableFactory<T> {//ParcelableFactory泛型类接口
- String BUILD_PARCELABLE = "buildParcelable";
- Sequenceable buildParcelable(T input);//将Parcelable和输入数据一起作为入参
- }
b.调用参数类型与输入数据类型匹配的buildParcelable(input)方法
在Libray的NonParcelRepository类中,有多个类实现了ParcelableFactory的接口,所以有多种参数形式的buildParcelable(input)方法,分别对应多种输入的数据类型,如boolean、char、List、Integer、set等。当输入数据为int类型时,就需要调用参数类型为Integer的buildParcelable(input)方法,并返回新实例化的IntegerParcelable类对象。
- private static class IntegerParcelableFactory implements Parcels.ParcelableFactory<Integer>{
- @Override
- public Sequenceable buildParcelable(Integer input) {//参数类型为Integer
- return new IntegerParcelable(input);//返回新实例化的类对象,输入数据作为入参
- }
- }
c.实例化IntegerParcelable类对象
实例化转换器类对象CONVERTER;再通过IntegerParcelable类的构造方法完成实例化操作,此步骤需要调用IntegerParcelable类父类的构造函数,以输入数据和转换器CONVERTER作为入参。
- public static final class IntegerParcelable extends ConverterParcelable<Integer> {
- private static final NullableParcelConverter<Integer> CONVERTER = new NullableParcelConverter<Integer>() {...//实例化转换器类对象
- @Override
- public void nullSafeToParcel(Integer input, Parcel parcel) {
- parcel.writeInt(input);
- }
- };...
- public IntegerParcelable(Integer value) {//IntegerParcelable类构造方法
- super(value, CONVERTER);//调用父类构造函数
- }...
- }
d.调用父类构造函数,实现转换器类接口
在调用父类ConverterParcelable的构造函数时,先在相应转换器类中实现TypeRangeParcelConverter类接口中的toParcel()和fromParcel()方法,然后调用相应转换器类的toParcel()方法完成序列化操作,其中具体转换器类实现原理会在下文进行详细讲解,此处不进行赘述。
- //转换器TypeRangeParcelConverter类接口
- public interface TypeRangeParcelConverter<L, U extends L> {
- void toParcel(L input, ohos.utils.Parcel parcel);
- U fromParcel(ohos.utils.Parcel parcel);
- }
- @Override//执行序列化操作
- public boolean marshalling(Parcel parcel) {
- converter.toParcel(value, parcel);//调用转换器类的toParcel()方法完成序列化操作
- return false;
- }
(2) unwrap()方法实现反序列化
实现反序列化操作,在判断输入数据不为空后,将输入数据的值赋给接口ParcelWrapper的泛型类对象,再通过类对象调用getParcel()方法,获取输入数据的反序列化结果。
- @SuppressWarnings("unchecked")
- public static <T> T unwrap(Sequenceable input) {
- if(input == null){//空判断,判断输入数据是否为空
- return null;//若输入数据为空,则返回空值
- }
- ParcelWrapper<T> wrapper = (ParcelWrapper<T>) input;
- return wrapper.getParcel();
- }
其中,wrapper.getParcel()方法具体执行过程是:实现ParcelWrapper泛型类接口,并实现getParcel()方法来执行具体的反序列化操作;最后返回反序列化后的“@Parcel”实例。
- public interface ParcelWrapper<T> {//实现ParcelWrapper泛型类接口
- String GET_PARCEL = "getParcel";
- T getParcel();//需实现getParcel()方法执行具体反序列化操作,并返回@Parcel实例
- }
2、转换器处理类原理
在上文讲解序列化方法wrap()时提到Parceler_ohos组件的转换器处理类是Converter类。这些类负责处理数据集合的各种复杂或冗长繁复的工作,如判空检查和数据集合迭代,并被打包在名为converter的包中,以便在API的相应包下更容易地找到数据集合的转换。这些转换器处理类能够对多种数据类型进行转换,如基础数据类、Map类和Set类等,其具体转换原理大同小异,接下来就以字符串数组转换器CharArrayParcelConverter类为例进行详细讲解。
CharArrayParcelConverter类中包含两个主要方法,即toParcel()和fromParcel(),其实质是分别负责对Parcel类对象进行写和读操作。在toParcel()方法中,先判断字符串数据是否为空,若为空则将数组数据写入到Parcel类对象中,写入数据为NULL;否则写入数据为字符串数组,包括其长度和数据本身。
- public class CharArrayParcelConverter implements ParcelConverter<char[]> {
- private static final int NULL = -1;
- @Override
- public void toParcel(char[] array, Parcel parcel) {
- if (array == null) {//判断字符串数组是否为空
- parcel.writeInt(NULL);//为空则写入NULL空数据
- } else {//不为空则写入字符串数组
- parcel.writeInt(array.length);//写入数据长度
- parcel.writeCharArray(array);//写入数组数据
- }
- }
- ...
- }
在fromParcel()方法中,先从Parcel类对象中读取数组的长度,判断长度是否为空,若为空则获取到空值;否则实例化一个新的字符串数组,通过Parcel类对象调用readCharArray()方法读取数组中的数据,以刚才新实例化的数组作为入参,这样就可以将读取到的数据存入新实例化的数组中,方便后续对读取到的数据的使用和处理,再返回新数组即可完成读取。
- ...
- @Override
- public char[] fromParcel(Parcel parcel) {
- char[] array;
- int size = parcel.readInt();//读取Parcel类对象中的数组长度
- if (size == NULL) {//若长度为空
- array = null;//则获取到空值
- } else {//若长度不为空
- array = new char[size];//实例化新数组
- parcel.readCharArray(array);//读取数组数据,并存入新数组中
- }
- return array;//返回新数组
- }
- }