Android開源: 快用Parceler來優雅的進行Bundle數據存取!

前言

在平時開發過程當中。使用Bundle進行數據存儲是個很常見的操做了。可是用的時候。卻有許多不方便的地方:java

1. 支持的數據類型有限

Bundle所支持的數據類型至關有限!因此咱們常常會遇到以下的窘境:git

public class ExampleActivity extends Activity {
	Entity entity;// 須要傳遞個實體類過來
}

// 然額Entity是個普通的實體類。
public class Entity {
	...
}
複製代碼

不少人一遇到這種問題,就說,很簡單嘛!序列化一下嘛!github

雖說序列化操做很簡單,可是這也是含有工做量的不是?json

因此我不想每次傳遞數據前,都要去考慮這個類是不是須要進行序列化操做,心累~~api

2. 存取api不統一

每次要使用Bundle進行數據存取時,那也是心累得一逼:跨域

每次進行存取的時候。要根據你當前的數據類型。在Bundle的一堆putXXX或者getXXX方法中找正確的方法進行存取。緩存

雖然Android同是也提供了Intent類,對Bundle的put/get方法進行了大量的重構,然而也並不能作到了徹底的存取api統一的效果。putStringArrayListExtra、putIntegerArrayListExtra隨處可見~性能優化

因此我想要的:框架

  • 別管我是要存啥數據,總之我給你一個key,一個value。你直接給我存好!
  • 別管我是要取啥數據,總之我給你一個key, 一個type。你直接給我取好!

3. 跨頁面傳遞時。部分數據類型存取時類型不匹配

你們都知道:在進行界面跳轉。使用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的自動注入

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查看~

相關文章
相關標籤/搜索