Android开源: 快用Parceler来优雅的进行Bundle数据存取!

2,516 阅读13分钟

前言

在平时开发过程中。使用Bundle进行数据存储是个很常见的操作了。但是用的时候。却有许多不方便的地方:

1. 支持的数据类型有限

Bundle所支持的数据类型相当有限!所以我们经常会遇到如下的窘境:

public class ExampleActivity extends Activity {
	Entity entity;// 需要传递个实体类过来
}

// 然额Entity是个普通的实体类。
public class Entity {
	...
}

很多人一遇到这种问题,就说,很简单嘛!序列化一下嘛!

虽然说序列化操作很简单,但是这也是含有工作量的不是?

所以我不想每次传递数据前,都要去考虑这个类是否是需要进行序列化操作,心累~~

2. 存取api不统一

每次要使用Bundle进行数据存取时,那也是心累得一逼:

每次进行存取的时候。要根据你当前的数据类型。在Bundle的一堆putXXX或者getXXX方法中找正确的方法进行存取。

虽然Android同是也提供了Intent类,对Bundle的put/get方法进行了大量的重构,然而也并不能做到了完全的存取api统一的效果。putStringArrayListExtra、putIntegerArrayListExtra随处可见~

所以我想要的:

  • 别管我是要存啥数据,总之我给你一个key,一个value。你直接给我存好!
  • 别管我是要取啥数据,总之我给你一个key, 一个type。你直接给我取好!

3. 跨页面传递时。部分数据类型存取时类型不匹配

大家都知道:在进行界面跳转。使用Intent进行传值,会触发使用系统的序列化与反序列化操作:

但是相信很多人都没发现的是:系统的序列化操作,对于部分的数据类型来说,被反序列化之后,会丢失其真正的类型,不清楚的可以通过以下简单代码进行测试:

在启动页面前:

Intent intent = new Intent(this, SampleActivity.class);
// 传递一个StringBuffer
intent.putExtra("stringbuffer", (Serializable) new StringBuffer("buffer"));
startActivity(intent);

然后在目标页进行接收:

StringBuffer result = 
	(StringBuffer) getIntent().getSerializableExtra("stringbuffer");

乍一看,没毛病,但是如果你一运行。就会出现下面这个异常:

Caused by: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.StringBuffer

What the Fuck!!! 神马鬼?!

可以发现。虽然我们存入的时候是StringBuffer,但是取出来之后,就变成了String了。导致前后不一致,出现crash。

这里我列出了目前我已发现的、存在此种问题的一些数据类型:

由于这种数据不匹配的问题。在不知情的情况下。可能就会引入一些不可预期的问题。甚至导致线上crash。

我才不想在每次进行数据传递的时候,都去先注意一下数据是否为上表中所包含的类型。也是累。。。

所以,我需要一款能直接兼容处理好此种数据格式不匹配问题的框架

Bundle的自动注入

Bundle的存取操作应该可以说是非常常用的api了,使用频率应该仅次于View。但是目前市面上却没有一款类似于ButterKnife一样,有专门针对性的对Bundle数据做自动注入的框架,就算有类似功能的。却也大部分都是为适配别的功能所做的特殊兼容功能。且这种功能性一般也较为简陋。

需求

基于以上背景。我建立了一个专用于对Bundle进行数据操作的处理框架:Parceler(https://github.com/JumeiRdGroup/Parceler)

Parceler框架支持以下特性:

  • 超级精简:总共方法数不到100
  • 可以直接存取任意数据类型
  • 存取api统一
  • 自动兼容修复类型不匹配问题
  • 支持定制数据转换器,满足更多数据适配需求
  • 在Bundle与实体类间进行双向数据注入
  • 生成Bundle创建器,避免出现手写key值的硬编码
  • 提供IntentLauncher,方便的进行跨页面跳转传值

依赖

// 加入jitpack仓库依赖
maven { url 'https://jitpack.io' }

// 添加依赖:
annotationProcessor "com.github.yjfnypeu.Parceler:compiler:1.3.5"
compile "com.github.yjfnypeu.Parceler:api:1.3.5"

注意:如果当前你的运行时环境不支持编译时注解,则可以不使用annotationProcessor进行注解处理器依赖。

配置数据转换器:用于支持存取任意类型数据

上面提到:bundle支持的数据类型非常有限,所以框架提供了数据转换器来兼容更多数据的使用:

public interface BundleConverter {
    // 当从bundle中读取出的值data(如JSON串)不与指定类型type(如普通Bean类)匹配时,
    // 触发到此进行转换后再返回,转换为指定的type类型实例。
    Object convertToEntity(Object data, Type type);
    // 当指定数据data(普通Bean类)不能直接被放入Bundle中时
    // 触发到此进行转换后在存储,转换为指定的中转数据,比如说JSON。
    Object convertToBundle(Object data);
}

因为常见的数据通信格式就是json,所以框架内置有常用的数据转换器:FastJsonConverterGsonConverter

请注意,框架本身并没有直接依赖fastjson或者gson,所以这里需要根据你当前项目中使用的是哪种JSON数据处理框架来手动选择使用的转换器:

比如我们当前项目中所使用的是fastjson:

Parceler.setDefaultConverter(FastJsonConverter.class);

若是你需要使用别的中转数据格式进行适配兼容(比如xml/protobuf等),可以通过自己继承上方的BundleConverter接口进行定制后进行使用。

统一存取api

Parceler的数据存取操作。主要核心是通过BundleFactory类来进行使用。可通过以下方式进行BundleFactory类创建:

// 此处传入Bundle对象。提供以对数据进行存取操作。
// 若bundle为null,则将创建个默认的空bundle容器使用
BundleFactory factory = Parceler.createFactory(bundle);
...
// 在操作完成之后。使用getBundle()方法获取操作后的Bundle实例。
Bundle bundle = factory.getBundle();

然后即可使用此BundleFactory对任意数据进行存取:

// 将指定数据value使用key值存入bundle中
factory.put(key, value);
// 将指定key值的数据从bundle中取出,并转换为指定type数据类型再返回
T t = factory.get(key, Class<T>);

就是这么简单!再也不用在进行数据存取的时候。去纠结该用什么api进行操作了!

BundleFactory进行存取时的流程如下图所示:

BundleFactory还添加了一些额外的配置,让你使用起来更加方便:

1. 容错处理

BundleFactory.ignoreException(isIgnore)

当配置ignore为true时(默认为false): 代表此时若进行put、get操作。在存取过程中若出现异常时,将不会抛出异常。

2. 设置数据转换器

虽然上面我们已经通过Parceler.setDefaultConverter设置了默认的数据转换器了,但是有时候只有一个默认转换器是不够的。

比如说默认转换器是使用的JSON数据,但是当前传递过来的数据又是xml。这个时候就需要针对此数据设置个单独的转换器:

BundleFactory.setConverter(converter);

示例代码:

Parceler.createFactory(bundle)
	.setConverter(XmlConverter.class);// 指定此时需要使用XmlConverter
	.put(key, xml)
	.setConverter(null)// 指定此时需要恢复使用默认转换器
	...;

3. 设置强制数据转换

BundleFactory.setForceConverter(isForce);

设置此强制数据转换为true之后,存储的流程将会变成如下所示:

可以看到,当设置了强制数据转换后,进行存储时就只会判断是否是基本数据类型或者String类型了。而其他的复杂参数,都将会被强制使用转换器,转为对应的中转数据(JSON)进行传递。

这种设计主要针对的是在组件化或者插件化环境下使用的时候,比如在进行跨组件、跨插件甚至跨进程通信时。会是很有用的一种特性。

以插件化为例,我们来举个栗子先:

假设我们当前插件A中存在以下一个实体类:

public class User extends Serializable{
	public long uid;
	public String username;
	public String password;
}

这个时候我们插件B中有个页面需要使用到此实体类中的数据。但是插件B中并没有此User类,这个时候就可以开启强制转换:

User user = ...
Bundle bundle = Parceler.createFactory(source)
		.setForceConverter(true)// 开启强制转换
		.put("user", user)// 添加user实例
		.getBundle();

// TODO 跨插件传递bundle数据

由于我们这里开启了强制转换。所以最终传递到插件B中的user应该是个JSON串,这个时候。就可以在插件B中创建个对应的实体类,定义好自身插件需要使用到的数据即可:

public class UserCopy {
	public long uid;
}

然后在目标页中将此数据读取出来即可:

// 取出传递过来的Bundle数据
Bundle bundle = getBundle();
// 创建Factory。并配置参数
BundleFactory factory = Parceler.createFactory(bundle);
// 通过Factory从Bundle中读取数据并自动转换
UserCopy user = factory.get("user", UserCopy.class);

其实如果使用后面介绍的注解方式进行读取,那将会更加简单:

public class TargetActivity extends Activity {
	@Arg// 添加此注解即可实现自动注入
	UserCopy user;
}

这样做有以下几点好处:

  1. 当需要跨域数据共享时,不再需要把共享的数据实体类下沉到基础组件中去。
  2. 对于数据提供方来说:我只要把数据确定传递出去即可。不用关心是否此数据需要进行跨域传递
  3. 对于数据接收方来说:只要你传递过来的json数据有我需要的数据。我可以读取就行

使用注解完成自动数据注入

Parceler框架提供使数据 在Bundle与实体类之间进行双向数据注入 功能:

我们直接以下方为示例代码来做说明,框架提供@Arg与@Converter此两种注解:

// 任意的实体类。也可以是抽象类
public class UserInfo {

	// 直接使用于成员变量之上。代表此成员变量数据可被注入
	@Arg 
	String username;
	
	// 指定此成员变量使用的key
	@Arg(“rename”)
	int age;
	
	// 结合Converter注解做数据转换兼容。
	@Converter(FastJsonConverter.class)
	@Arg
	Address address
	
	// more codes 
	...
}

在对成员变量添加了注解之后。我们即可对这些成员变量进行双向数据注入了 (bundle <==> entity)

仍然以上方所定义的class为例:(bundle与entity需要均不为null)

bundle ==> entity

UserInfo info = getUserInfo();
// 从bundle中读取数据并注入到info类中的对应字段中去
Parceler.toEntity(info, bundle);

等价于:

Parceler.createFactory(bundle)
	.put("username", info.username)
	// 使用了@Arg("rename")做key重命名
	.put("rename", info.age)
	// 下一个数据需要使用指定的转换器
	.setConverter(FastJsonConverter.class)
	// 使用指定转换器
	.put("address", info.address)
	// 使用完再切换为默认转换器使用。
	.setConverter(null);

entity ==> bundle

UserInfo info = getUserInfo();
// 从info中读取添加了Arg注解的字段的值。并注入到bundle中去存储。
Parceler.toBundle(info, bundle);

等价于:

BundleFactory factory = Parceler.createFactory(bundle);
info.username = factory.get("username", String.class);
info.age      = factory.get("rename", int.class);
// address指定了使用的转换器
factory.setConverter(FastJsonConverter.class);
info.address  = factory.get("address", Address.class);
// 使用后恢复为默认转换器
factory.setConverter(null);

使用场景示例

最常见的使用场景就是在进行Activity跳转传值时使用:

发起注入操作可放置于基类中进行使用。所以可以将注入操作添加在Activity基类中:

// 将注入器配置到基类中。一次配置,所有子类共同使用
public abstract class BaseActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 启动时从intent中读取数据并注入到当前类中。
        Parceler.toEntity(this,getIntent());
    }

    // ============可用以下方式方便的进行数据现场保护==========
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        // 将当前类中的使用注解的成员变量的值注入到outState中进行保存。
        Parceler.toBundle(this,outState);
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        // 需要恢复现场时。将数据从saveInstanceState中读取并注入当前类中。恢复现场
        Parceler.toEntity(this,savedInstanceState);
    }
}

然后就可以愉快的在各种子类中方便的进行使用了:

public class UserActivity extends BaseActivity {

	// 直接使用。
	@Arg
	User user;
	@Arg
	Address address;
	@Arg
	int age;
	
	...

}

使用BundleBuilder, 避免key值硬编码

public class UserActivity extends BaseActivity{
    @Arg
    String name;
}

以此类为例。当你需要传递name到这个UserActivity的时候。你可能会需要手动写上对应的key值:

bundle.putStringExtra("name", "HelloKitty");

但是这样就存在一个问题:因为name是个硬编码,所以当你修改目标类的name字段名时,你可能无法发现这边还有个硬编码需要进行修改。所以这个时候就很容易出问题!

这个时候就可以用BundleBuilder注解来帮助进行key值的自动组装了。避免硬编码:

// 添加此注解到目标类
@BundleBuilder
public class UserActivity extends BaseActivity {
    @Arg
    String name;
}

添加了此BundleBuilder注解后,就会在编译时生成对应的XXXBundleBuilder类,你就可以使用此类进行Bundle数据创建了。不需要再进行手写key值:

Bundle bundle = UserActivityBundleBuilder.setName(name).build();

PS: 请注意。此BundleBuilder可添加于任意类之上,不限于Activity等组件。

使用IntentLauncher,方便的进行跨页面跳转传值

解决了key值的硬编码问题。框架还提供了IntentLauncher。用于结合生成的BundleBuilder对象。方便的进行Intent启动, 仍以上述UserActivity为例:

// 创建Builder对象
IBundleBuilder builder = UserActivityBundleBuilder.create(bundle)
            .setName(name);

// 使用IntentLauncher进行页面跳转。
// 支持Activity、Service、BroadcastReceicer
IntentLauncher.create(builder)
        .requestCode(1001)
        .start(context);

原理与性能优化

相信有很多小伙伴看了上方介绍。都有一个顾虑:看上方这种使用介绍,肯定使用了很多反射api吧!不会影响性能么?

老实讲,性能是肯定是有一定影响的。没有什么第三方封装框架可以真的不输于原生api的性能,这是不可能的!当然也不是说性能不重要。毕竟我们是客户端,性能问题还是很重要的,所以在框架内部。我也做了多项优化。以达到性能影响最小化:

  1. 内部使用的反射api尽量避开了那种真正耗时的反射api。框架内部主要使用的是一些用来简单判断数据类型的api。这类api对性能相比直接反射获取、设置值,要小得多。 这点可以参考框架的BundleHandle类

  2. 对于数据注入功能来说。正常来说我们是通过编译时注解在编译时生成了对应的数据注入器类。且对生成的注入器代码的方法数做了严格的限制! 以尽量避免大量使用时生成的方法数过多造成的影响。而对于部分使用环境来说。可能不支持使用编译时注解(虽然这种情况少。但是还是有的),框架也提供了对应的运行时注入器供使用。

    • 生成的数据注入器的方法数框架做了严格的限制!以尽量避免大量使用时生成的方法数过多造成的影响。
    • 对于部分使用环境来说。可能不支持使用编译时注解(虽然这种情况少。但是还是有的),框架也提供了对应的运行时注入器供使用: RuntimeInjector
  3. 框架内部对容易造成性能影响的点。都做了对应的缓存处理。已达到最佳运行的效果!如:

    • 每个实体类所对应的数据注入器的实例
    • 每个实体类中使用了@Arg注解的成员变量的真正数据类型type。
    • 使用的数据转换器。
    • 注入扫描时自动过滤系统包名。

结语

更多用法特性,欢迎star查看~