在平时开发过程当中。使用Bundle进行数据存储是个很常见的操做了。可是用的时候。却有许多不方便的地方:java
Bundle所支持的数据类型至关有限!因此咱们常常会遇到以下的窘境:git
public class ExampleActivity extends Activity {
Entity entity;// 须要传递个实体类过来
}
// 然额Entity是个普通的实体类。
public class Entity {
...
}
复制代码
不少人一遇到这种问题,就说,很简单嘛!序列化一下嘛!github
虽说序列化操做很简单,可是这也是含有工做量的不是?json
因此我不想每次传递数据前,都要去考虑这个类是不是须要进行序列化操做,心累~~api
每次要使用Bundle进行数据存取时,那也是心累得一逼:跨域
每次进行存取的时候。要根据你当前的数据类型。在Bundle的一堆putXXX或者getXXX方法中找正确的方法进行存取。缓存
虽然Android同是也提供了Intent类,对Bundle的put/get方法进行了大量的重构,然而也并不能作到了彻底的存取api统一的效果。putStringArrayListExtra、putIntegerArrayListExtra随处可见~性能优化
因此我想要的:框架
你们都知道:在进行界面跳转。使用Intent进行传值,会触发使用系统的序列化与反序列化操做:maven
可是相信不少人都没发现的是:系统的序列化操做,对于部分的数据类型来讲,被反序列化以后,会丢失其真正的类型,不清楚的能够经过如下简单代码进行测试:
在启动页面前:
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的存取操做应该能够说是很是经常使用的api了,使用频率应该仅次于View。可是目前市面上却没有一款相似于ButterKnife同样,有专门针对性的对Bundle数据作自动注入的框架,就算有相似功能的。却也大部分都是为适配别的功能所作的特殊兼容功能。且这种功能性通常也较为简陋。
基于以上背景。我创建了一个专用于对Bundle进行数据操做的处理框架:Parceler(https://github.com/JumeiRdGroup/Parceler)
Parceler框架支持如下特性:
// 加入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,因此框架内置有经常使用的数据转换器:FastJsonConverter与GsonConverter。
请注意,框架自己并无直接依赖fastjson或者gson,因此这里须要根据你当前项目中使用的是哪一种JSON数据处理框架来手动选择使用的转换器:
好比咱们当前项目中所使用的是fastjson:
Parceler.setDefaultConverter(FastJsonConverter.class);
复制代码
如果你须要使用别的中转数据格式进行适配兼容(好比xml/protobuf等),能够经过本身继承上方的BundleConverter接口进行定制后进行使用。
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还添加了一些额外的配置,让你使用起来更加方便:
BundleFactory.ignoreException(isIgnore)
复制代码
当配置ignore为true时(默认为false): 表明此时若进行put、get操做。在存取过程当中若出现异常时,将不会抛出异常。
虽然上面咱们已经经过Parceler.setDefaultConverter设置了默认的数据转换器了,可是有时候只有一个默认转换器是不够的。
好比说默认转换器是使用的JSON数据,可是当前传递过来的数据又是xml。这个时候就须要针对此数据设置个单独的转换器:
BundleFactory.setConverter(converter);
复制代码
示例代码:
Parceler.createFactory(bundle)
.setConverter(XmlConverter.class);// 指定此时须要使用XmlConverter
.put(key, xml)
.setConverter(null)// 指定此时须要恢复使用默认转换器
...;
复制代码
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;
}
复制代码
这样作有如下几点好处:
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)
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);
复制代码
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;
...
}
复制代码
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等组件。
解决了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的性能,这是不可能的!固然也不是说性能不重要。毕竟咱们是客户端,性能问题仍是很重要的,因此在框架内部。我也作了多项优化。以达到性能影响最小化:
内部使用的反射api尽可能避开了那种真正耗时的反射api。框架内部主要使用的是一些用来简单判断数据类型的api。这类api对性能相比直接反射获取、设置值,要小得多。 这点能够参考框架的BundleHandle类。
对于数据注入功能来讲。正常来讲咱们是经过编译时注解在编译时生成了对应的数据注入器类。且对生成的注入器代码的方法数作了严格的限制! 以尽可能避免大量使用时生成的方法数过多形成的影响。而对于部分使用环境来讲。可能不支持使用编译时注解(虽然这种状况少。可是仍是有的),框架也提供了对应的运行时注入器供使用。
框架内部对容易形成性能影响的点。都作了对应的缓存处理。已达到最佳运行的效果!如:
更多用法特性,欢迎star查看~