天天看點

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

前言

在平時開發過程中。使用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,是以架構内置有常用的資料轉換器:FastJsonConverter與GsonConverter。

請注意,架構本身并沒有直接依賴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()
        .start(context);
           

原理與性能優化

相信有很多小夥伴看了上方介紹。都有一個顧慮:看上方這種使用介紹,肯定使用了很多反射api吧!不會影響性能麼?

老實講,性能是肯定是有一定影響的。沒有什麼第三方封裝架構可以真的不輸于原生api的性能,這是不可能的!當然也不是說性能不重要。畢竟我們是用戶端,性能問題還是很重要的,是以在架構内部。我也做了多項優化。以達到性能影響最小化:

  1. 内部使用的反射api盡量避開了那種真正耗時的反射api。架構内部主要使用的是一些用來簡單判斷資料類型的api。這類api對性能相比直接反射擷取、設定值,要小得多。 這點可以參考架構的BundleHandle類。
  2. 對于資料注入功能來說。正常來說我們是通過編譯時注解在編譯時生成了對應的資料注入器類。且對生成的注入器代碼的方法數做了嚴格的限制! 以盡量避免大量使用時生成的方法數過多造成的影響。而對于部分使用環境來說。可能不支援使用編譯時注解(雖然這種情況少。但是還是有的),架構也提供了對應的運作時注入器供使用。
    • 生成的資料注入器的方法數架構做了嚴格的限制!以盡量避免大量使用時生成的方法數過多造成的影響。
    • 對于部分使用環境來說。可能不支援使用編譯時注解(雖然這種情況少。但是還是有的),架構也提供了對應的運作時注入器供使用: RuntimeInjector
  3. 架構内部對容易造成性能影響的點。都做了對應的緩存處理。已達到最佳運作的效果!如:
    • 每個實體類所對應的資料注入器的執行個體
    • 每個實體類中使用了@Arg注解的成員變量的真正資料類型type。
    • 使用的資料轉換器。
    • 注入掃描時自動過濾系統包名。

結語

更多用法特性,歡迎star檢視~