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

Haoge發表於2017-12-19

前言

在平時開發過程中。使用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進行傳值,會觸發使用系統的序列化與反序列化操作:

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

但是相信很多人都沒發現的是:系統的序列化操作,對於部分的資料型別來說,被反序列化之後,會丟失其真正的型別,不清楚的可以通過以下簡單程式碼進行測試:

在啟動頁面前:

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。

這裡我列出了目前我已發現的、存在此種問題的一些資料型別:

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

由於這種資料不匹配的問題。在不知情的情況下。可能就會引入一些不可預期的問題。甚至導致線上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進行存取時的流程如下圖所示:

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

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之後,儲存的流程將會變成如下所示:

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

可以看到,當設定了強制資料轉換後,進行儲存時就只會判斷是否是基本資料型別或者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檢視~

相關文章