2016年1月28日 星期四

【Android】Gson 使用介紹

Gson是Google在2008年就推出的一套Library

目的是提供開發者快速的將JSON字串(String)轉換成物件(Object)

以及將物件(Object)快速的轉換成JSON字串(String)

目前release到v2.8.1




Gradle:

//gson
compile 'com.google.code.gson:gson:2.8.1'



假設我們有這樣一組JSON字串要轉換成Object

[
    {
        "UserName":"Anson",
        "UserAge":20
    },
    {
        "UserName":"Kevin",
        "UserAge":30
    }
]

可以看出是由一個JSONArray包住兩個JSONObject
JSONObject裡面有兩個屬性,分別是UserName 與 UserAge
於是乎我們便定義了這樣的一個 Model 來裝這些資料

public class User {
    private String UserName;
    private int UserAge;

    public User(String userName, int userAge) {
        UserName = userName;
        UserAge = userAge;
    }

    public String getUserName() {
        return UserName;
    }

    public int getUserAge() {
        return UserAge;
    }
}



使用Gson將 JSON字串 轉換成 Array
只需要簡單一行就搞定!

String jsonString = "[{\"UserName\":\"Anson\",\"UserAge\":20},{\"UserName\":\"Kevin\",\"UserAge\":30}]";
User[] userArray = new Gson().fromJson(jsonString, User[].class);

如果想轉成 List 呢?

String jsonString = "[{\"UserName\":\"Anson123123\",\"UserAge\":20},{\"UserName\":\"Kevin\",\"UserAge\":30}]";
java.lang.reflect.Type listType = new TypeToken<Collection<User>>(){}.getType();
List<User> userList = new Gson().fromJson(jsonString , listType);



那如果是物件轉JSON字串呢?
也是很簡單~~

ArrayList<User> userList = new ArrayList();
for (int i=0;i<2;i++){
    User user = new User("Test"+i,i);
    userList.add(user);
}
String jsonString = new Gson().toJson(userList);
System.out.println(jsonString);

輸出結果:

[{"UserName":"Test0","UserAge":0},{"UserName":"Test1","UserAge":1}]



有發現上面的User class中

在第2.3行我們所定義的屬性為 UserNameUserAge

名子跟JSON裡面的屬性一樣

但這不符合JAVA中的命名規範(小寫開頭)

那如果我們將class中的屬性改成userNameuserAge

在進行轉換時就會失敗~因為大小寫與JSON字串不一致

於是我們可以加上Gson提供的一個很方便的功能

@SerializedName("對應的JSON屬性名稱")

我們可以將 model 改成這個樣子...

public class User {
    @SerializedName("UserName")
    private String name;
    
    @SerializedName("UserAge")
    private int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }
    
    public int getAge() {
        return age;
    }
}

這樣子就等於告訴Gson,在轉換JSON字串時需要將 Model 內的

nameUserName
ageUserAge

以及在 JSON字串 轉 物件 時,將

UserNamename
UserAgeage






來試試複雜一點的例子吧:

{
    "Id":123456,
    "Name":"張三",
    "Gender":true,
    "Phones":[
        {"Type":"Landline","Number":"02-2800-0000"},
        {"Type":"Mobile","Number":"0900-000-000"}
    ],
    "Hobbies":[
        "釣魚","睡覺","打籃球"
    ]
}

這組 JSON 裡面可以看出,這是在描述一個人
他叫做張三,性別男(假設男為true,女為false),電話有兩支,一支市話一支手機
興趣有三個,分別是釣魚.睡覺.打籃球
來定義 Model 吧:

public class Guest {
    @SerializedName("Id")
    private int id;

    @SerializedName("Name")
    private String name;

    @SerializedName("Gender")
    private boolean gender;

    @SerializedName("Phones")
    private List<Phone> phoneList;

    @SerializedName("Hobbies")
    private List<String> hobbyList;

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public boolean getGender() {
        return gender;
    }

    public List<Phone> getPhoneList() {
        return phoneList;
    }

    public List<String> getHobbyList() {
        return hobbyList;
    }

    @Override
    public String toString() {
        return new Gson().toJson(this, Guest.class);
    }

    public static class Phone {
        enum Type {
            Landline, Mobile
        }

        @SerializedName("Type")
        private Type type;

        @SerializedName("Number")
        private String number;

        public Type getType() {
            return type;
        }

        public String getNumber() {
            return number;
        }
    }
}

在這個 Guest 這個 class 中
先使用 SerializedName 將原本 JSON字串 的屬性名稱轉換成我們想要的名子
接著在42行定義了一個 inner class 來裝電話的資料(當然不一定要使用內部類)
可以看到43行 列舉(enum) 也是可以在 gson 轉換中直接使用的
在38行我 override 了 toString 方法,目的是印 log 比較方便
定義好了 Model 後,接著的資料轉換就輕鬆多了!

String jsonString = "{\"Id\":123456,\"Name\":\"張三\",\"Gender\":true,\"Phones\":[{\"Type\":\"Landline\",\"Number\":\"02-2800-0000\"},{\"Type\":\"Mobile\",\"Number\":\"0900-000-000\"}],\"Hobbies\":[\"釣魚\",\"睡覺\",\"打籃球\"]}";
Guest guest = new Gson().fromJson(jsonString, Guest.class);
System.out.println(guest.toString());

也是一行輕鬆搞定

輸出結果:

{"Gender":true,"Hobbies":["釣魚","睡覺","打籃球"],"Id":123456,"Name":"張三","Phones":[{"Number":"02-2800-0000","Type":"Landline"},{"Number":"0900-000-000","Type":"Mobile"}]}



與Gson類似的Library還可以參考FasterXML jackson

2015年8月29日 星期六

【Android】Serializable vs Parcelable



我們都知道在Android內如果要傳送資料必須透過Intent或Bundle

在送基本型態(int,String,boolean..)時應該沒有什麼爭議

但是如果要送一個 Object 呢?

這時你就有兩個選擇了,你可以使用
● Serializable
● Parcelable

稍微解釋一下這兩個interface

Serializable 是 Java 的 interface
Parcelable 是 Android 的 interface
在使用他們時,你的 Class 必須 implements 他們

既然都可以達到資料傳送的目的
那為什麼Android還要特地出一個 Parcelable 給大家使用呢?

當然是因為 performance 的問題
Serializable 在使用上非常方便
只需要 implements 他就結束了,但是
Serializable 在Java就一直被大家所詬病
他是利用反射來實例化物件
但這種方法會造成大量的temporary objects(臨時物件)
VM也要花時間對這些對象進行garbage collection(GC)

當然 Parcelable 就不是利用反射了
而是我們在 implement 他時就實作他的method
透過泛型直接給定他Class


下面有個簡單的Sample
讓我們實際測試看看兩者之間的效能落差



1.專案配置


很簡單,就一個Layout 與 Activity 與兩個 Model Object



2.Layout


<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:layout_marginBottom="20dp"
        android:padding="10dp"
        android:text="測試執行50000次的\nParcelable/Serializable效能差異"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:padding="10dp"
        android:text="Parcelable 所花時間(ms):"
        android:id="@+id/pTimeTv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:padding="10dp"
        android:text="Serializable 所花時間(ms):"
        android:id="@+id/sTimeTv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <TextView
        android:textColor="#cc0000"
        android:padding="10dp"
        android:text="效能落差(ms):"
        android:id="@+id/parcelableTimeTv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="執行測試"
        android:id="@+id/button"
        />
</LinearLayout>


3.Java Code


DataP

import android.os.Parcel;
import android.os.Parcelable;

public class DataP implements Parcelable{
    private int a;
    private String b;
    private boolean c;

    public DataP(int a, String b, boolean c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }

    public String getB() {
        return b;
    }

    public void setB(String b) {
        this.b = b;
    }

    public boolean isC() {
        return c;
    }

    public void setC(boolean c) {
        this.c = c;
    }

    protected DataP(Parcel in) {
        a = in.readInt();
        b = in.readString();
    }

    public static final Creator<DataP> CREATOR = new Creator<DataP>() {
        @Override
        public DataP createFromParcel(Parcel in) {
            return new DataP(in);
        }

        @Override
        public DataP[] newArray(int size) {
            return new DataP[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(a);
        dest.writeString(b);
    }
}

DataS

import java.io.Serializable;

public class DataS implements Serializable {
    private int a;
    private String b;
    private boolean c;

    public DataS(int a, String b, boolean c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

    public int getA() {
        return a;
    }

    public void setA(int a) {
        this.a = a;
    }

    public String getB() {
        return b;
    }

    public void setB(String b) {
        this.b = b;
    }

    public boolean isC() {
        return c;
    }

    public void setC(boolean c) {
        this.c = c;
    }
}

MainActivity
在兩個for迴圈內做5萬次的 put 與 get (單純模擬)
最後把執行時間相減來做出比較

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final Button button = (Button)findViewById(R.id.button);
        final TextView pTimeTv = (TextView)findViewById(R.id.pTimeTv);
        final TextView sTimeTv = (TextView)findViewById(R.id.sTimeTv);
        final TextView parcelableTimeTv = (TextView)findViewById(R.id.parcelableTimeTv);

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Bundle bundle = new Bundle();

                long pStart = System.currentTimeMillis();
                for(int i=0;i<50000;i++){
                    DataP p1 = new DataP(i,String.valueOf(i),true);
                    bundle.putParcelable("keyP" + i, p1);
                    DataP p2 = bundle.getParcelable("keyP"+i);
                }
                long pEnd = System.currentTimeMillis();
                long sStart = System.currentTimeMillis();
                for(int i=0;i<50000;i++){
                    DataS s1 = new DataS(i,String.valueOf(i),true);
                    bundle.putSerializable("keyS" + i,s1);
                    DataS s2 = (DataS)bundle.getSerializable("keyS"+i);
                }
                long sEnd = System.currentTimeMillis();

                long pSub = pEnd - pStart;
                long sSub = sEnd - sStart;
                long parcelableSub = (sSub>pSub)?(sSub-pSub):(pSub-sSub);

                pTimeTv.setText("Parcelable 所花時間(ms):"+pSub);
                sTimeTv.setText("Serializable 所花時間(ms):"+sSub);
                parcelableTimeTv.setText("效能落差(ms):"+parcelableSub);
            }
        });
    }
}


4.總結


從我的不專業測試得知結果為
創建5萬個物件(序/反列化)兩者之間時間差為0.7
Parcelable 大約是 Serializable 的兩倍

試想如果今天是10萬或20萬個物件呢,整體速度都會被拖慢!
你的App使用起來就會很卡頓!
所以還是多使用 Parcelable 吧!

兩者優缺點比較:

Serializable:
● 程式碼簡潔,閱讀容易
● 反序列化時需要強制轉型
● 速度較慢

Parcelable:
● 程式碼較複雜
● 速度較快,佔用資源較少

國外的說法是兩者之間效能差約10倍

個人是覺得沒那麼誇張
不過兩倍以上是有的





2015年8月21日 星期五

【Android】SignalR 使用介紹

Blogger

這篇要來介紹的是 如何在Android使用SignalR完成簡易聊天室

我們要完成上圖畫面

Server 端的實作可以參考我的這篇文章
MVC - 使用SignalR完成簡易聊天室



1.Add SignalR Lib


別懷疑!微軟沒有上傳SignalR的Maven位置
原始碼請至github下載(有點雷.趕時間別去踩)
或是你可以跟我一樣來這裡直接下載jar檔引用
除了要引用signalr-client-sdk還有Gson也要


Manifest記得要加上網路權限

<uses-permission android:name="android.permission.INTERNET"/>


2.Layout


兩個檔案,主畫面與listview的item


先看activity_main的layout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <ListView
        tools:listitem="@layout/item"
        android:id="@+id/chatLv"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:divider="#ddd"
        android:dividerHeight="1dp"
        />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_marginBottom="2dp"
        android:layout_marginTop="2dp"
        android:background="#000" />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center_vertical"
        android:orientation="horizontal"
        android:padding="5dp">
        <EditText
            android:id="@+id/messageEt"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:padding="5dp"
            />
        <Button
            android:id="@+id/sendBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dp"
            android:text="SEND"
            />
    </LinearLayout>
</LinearLayout>

item的layout

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:padding="5dp"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/nameTv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="3dp"
        android:text="Name"
        />
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=":"
        />
    <TextView
        android:id="@+id/messageTv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="3dp"
        android:text="Message"
        />
</LinearLayout>


2.Java Code


這邊有三個檔案,稍微解釋一下:
MainActivity(主畫面)
ChatAdapter(ListView Adapter)
ChatData(資料容器)


先說MainActivity,裡面的四個常數的意義
HUB_URL:就是你signalR的DomainName+/signalr
HUB_NAME:Hub檔案名稱
HUB_EVENT_NAME:Hub觸發的事件名稱
HUB_METHOD_NAME:Call的method名稱
下面這張圖可以清楚對照,如果不知道藍框裡面是什麼東西
請先看這篇


public class MainActivity extends Activity {
    private static final String HUB_URL = "[你的url]/signalr";
    private static final String HUB_NAME = "你的Hub name";
    private static final String HUB_EVENT_NAME = "你的事件名稱";
    private static final String HUB_METHOD_NAME = "你的Call method name";
    private SignalRFuture<Void> mSignalRFuture;
    private HubProxy mHub;
    private String mName;

    private ChatAdapter mChatAdapter;
    private ListView mChatLv;
    private EditText mMessageEt;
    private Button mSendBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mChatLv = (ListView)findViewById(R.id.chatLv);
        mMessageEt = (EditText)findViewById(R.id.messageEt);
        mSendBtn = (Button)findViewById(R.id.sendBtn);
        mName = "Android-"+System.currentTimeMillis();
        mChatAdapter = new ChatAdapter(this,0,new ArrayList<ChatData>(),mName);

        mChatLv.setAdapter(mChatAdapter);
        mSendBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    String message = mMessageEt.getText().toString();
                    mHub.invoke(HUB_METHOD_NAME, mName, message).get();
                    mMessageEt.setText("");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                }
            }
        });

        HubConnection connection = new HubConnection(HUB_URL);
        mHub = connection.createHubProxy(HUB_NAME);
        mSignalRFuture = connection.start(new ServerSentEventsTransport(connection.getLogger()));
        //可以理解為訊息or事件監聽器
        mHub.on(HUB_EVENT_NAME, new SubscriptionHandler2<String, String>() {
            @Override
            public void run(String name,String message) {
                //使用AsyncTask來更新畫面
                new AsyncTask<String,Void,ChatData>(){
                    @Override
                    protected ChatData doInBackground(String... param) {
                        ChatData chatData = new ChatData(param[0],param[1]);
                        return chatData;
                    }
                    @Override
                    protected void onPostExecute(ChatData chatData) {
                        mChatAdapter.add(chatData);
                        mChatLv.smoothScrollToPosition(mChatAdapter.getCount()-1);
                        super.onPostExecute(chatData);
                    }
                }.execute(name,message);
            }
        }, String.class,String.class);

        //開啟連線
        try {
            mSignalRFuture.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onDestroy() {
        //關閉連線
        mSignalRFuture.cancel();
        super.onDestroy();
    }
}

ChatData資料容器

public class ChatData {
    private String name;
    private String message;

    public ChatData(String name, String message) {
        this.name = name;
        this.message = message;
    }

    public String getName() {
        return name;
    }
    public String getMessage() {
        return message;
    }
}

ChatAdapter,這邊比較需要講一下的只有
我將自己的聊天內容標註為紅色而已

public class ChatAdapter extends ArrayAdapter<ChatData>{
    private String mName;
    public ChatAdapter(Context context, int resource, List<ChatData> objects,String mName) {
        super(context, resource, objects);
        this.mName = mName;
    }

    private class ViewHolder {
        TextView nameTv;
        TextView messageTv;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ChatData chatData = getItem(position);
        ViewHolder holder;
        if(convertView==null){
            holder = new ViewHolder();
            convertView = LayoutInflater.from(getContext()).inflate(R.layout.item, null);
            holder.nameTv = (TextView) convertView.findViewById(R.id.nameTv);
            holder.messageTv = (TextView) convertView.findViewById(R.id.messageTv);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        holder.nameTv.setText(chatData.getName());
        holder.messageTv.setText(chatData.getMessage());

        if(chatData.getName().equals(mName)){
            holder.nameTv.setTextColor(Color.RED);
        }
        return convertView;
    }
}


3.完成了!來看效果吧


android與brower對話