Android 对象序列化之 Parcelable 取代 Serializable ?
Android 存储优化系列专题
- SharedPreferences 系列
《Android 之不要滥用 SharedPreferences》
《Android 之不要滥用 SharedPreferences(2)— 数据丢失》
- ContentProvider 系列(待更)
《Android 存储选项之 ContentProvider 启动过程源码分析》
《Android 存储选项之 ContentProvider 深入分析》
- 对象序列化系列
《Android 对象序列化之你不知道的 Serializable》
《Android 对象序列化之 Parcelable 取代 Serializable ?》
《Android 对象序列化之追求性能完美的 Serial》
- 数据序列化系列(待更)
《Android 数据序列化之 JSON》
《Android 数据序列化之 Protocol Buffer 使用》
《Android 数据序列化之 Protocol Buffer 源码分析》
- SQLite 存储系列
《Android 存储选项之 SQLiteDatabase 创建过程源码分析》
《Android 存储选项之 SQLiteDatabase 源码分析》
《数据库连接池 SQLiteConnectionPool 源码分析》
《SQLiteDatabase 启用事务源码分析》
《SQLite 数据库 WAL 模式工作原理简介》
《SQLite 数据库锁机制与事务简介》
《SQLite 数据库优化那些事儿》
前言
对象序列化系列,主要内容包括:Java 原生提供的 Serializable ,更加适合 Android 平台轻量且高效的 Parcelable,以及追求性能完美的 Serial。该系列内容主要结合源码的角度分析它们各自的优缺点以及合适的使用场景。
在上一篇文章《Android 对象序列化之你不知道的 Serializable》为大家介绍了 Serializable 的简单实现机制背后复杂的计算逻辑,由于整个实现过程使用了大量反射和临时变量,而且在序列化对象的时候,不仅需要序列化当前对象本身,还需要递归序列化引用的其他对象。
虽然 Serializable 性能那么差,但是它仍然有一些优势和进阶的使用技巧,正如文中所述:“Java 原生提供的 Serializalbe 对象序列化机制,远比大多数 Java 开发人员想象的更灵活,这使得我们有更多的机会解决棘手的问题”。
说道这里如果你还对 Serializable 序列化机制不熟悉的话可以先去参考下。
Parcelable 产生的背景
由于 Java 的 Serializable 的性能较低,Android 需要重新设计一套更加轻量且高效的对象序列化和反序列化机制。Parcelable 正式在这个背景下产生的,它核心作用就是为了解决 Android 中大量跨进程通信的性能问题。
在时间开销和使用成本的权衡上,Serializable 选择了使用成本,而 Parcelable 机制则是选择性能优先。所以在它写入和读取数据都需要手动添加自定义代码,使用起来相比 Serializable 会复杂很多。但是正因为这样,Parcelable 才不需要采用反射的方式去实现序列化和反序列化(有少许反射,文中会分析到)。
Parcelable 实现机制分析
下面我们还是通过一个例子结合源码的角度开始分析:
public final class WebParams implements Parcelable { public static final String EXTRA_PARAMS_KEY = "extra_params_key"; private int age; private String name; private long serialUid; private char[] flag = new char[10]; private byte[] like = new byte[10]; public WebParams() { } protected WebParams(Parcel in) { this.age = in.readInt(); this.name = in.readString(); this.serialUid = in.readLong(); in.readCharArray(flag); in.readByteArray(like); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(age); dest.writeString(name); dest.writeLong(serialUid); dest.writeCharArray(flag); dest.writeByteArray(like); } @Override public int describeContents() { return 0; } public static final Creator CREATOR = new Creator() { @Override public WebParams createFromParcel(Parcel in) { return new WebParams(in); } @Override public WebParams[] newArray(int size) { return new WebParams[size]; } };}
通过启动 Activity 过程分析 Parcelable 序列化过程:
private void serialParcelable() { final Intent intent = new Intent(this, null); final WebParams params = new WebParams(); //...省略参数配置 //通过设置附加参数到Intent中 intent.putExtra(WebParams.EXTRA_PARAMS_KEY, params); startActivity(intent);}
熟悉这一过程的朋友过程肯定知道,startActivity 方法最终会通过 AMS(ActivityManagerService)完成跨进程通信调用,但是在通信之前先要将数据序列化后进行传输,这个过程首先会调用 ActivityManagerProxy 的 startActivity 方法
public int startActivity(IApplicationThread caller, String callingPackage, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo, Bundle options) throws RemoteException { //负责写出 Parcel data = Parcel.obtain(); //负责读取 Parcel reply = Parcel.obtain(); data.writeInterfaceToken(IActivityManager.descriptor); data.writeStrongBinder(caller != null ? caller.asBinder() : null); data.writeString(callingPackage); //我们分析Parcelable序列化重点在这里 intent.writeToParcel(data, 0); data.writeString(resolvedType); data.writeStrongBinder(resultTo); data.writeString(resultWho); data.writeInt(requestCode); data.writeInt(startFlags); if (profilerInfo != null) { data.writeInt(1); profilerInfo.writeToParcel(data, Parcelable.PARCELABLE_WRITE_RETURN_VALUE); } else { data.writeInt(0); } if (options != null) { data.writeInt(1); options.writeToParcel(data, 0); } else { data.writeInt(0); } //开始跨进程通信 mRemote.transact(START_ACTIVITY_TRANSACTION, data, reply, 0); reply.readException(); int result = reply.readInt(); reply.recycle(); data.recycle(); return result;}
其中 intent.writeToParcel 是我们要重点跟踪的方法,先来看下它的参数 Parcel 类型,其实从这里的获取方式大家也能猜测的出内部使用了复用机制,就类似于 Message.obtain。
public static Parcel obtain() { //当前缓存池,sOwnedPool是一个静态变量 final Parcel[] pool = sOwnedPool; synchronized (pool) { Parcel p; //获取可以被复用的Parcel, //POOL_SIZE默认大小为6 for (int i = 0; i < POOL_SIZE; i++) { p = pool[i]; if (p != null) { //获取到复用对象,将该位置置为null pool[i] = null; if (DEBUG_RECYCLE) { p.mStack = new RuntimeException(); } //这是一个默认辅助读写 p.mReadWriteHelper = ReadWriteHelper.DEFAULT; return p; } } } //无可复用,直接创建 return new Parcel(0);}
从 for 循环获取可复用的 Parcel 过程,不知大家是否能够看得出这一个队列的数据结构。
这里看下 POOL_ZISE 的定义:
private static final int POOL_SIZE = 6;
sOwnedPool 的定义:
private static final Parcel[] sOwnedPool = new Parcel[POOL_SIZE];
如果从复用池获取不到则直接创建 Parcel:
private Parcel(long nativePtr) { if (DEBUG_RECYCLE) { mStack = new RuntimeException(); } //初始化Parcel init(nativePtr);}//调用initprivate void init(long nativePtr) { if (nativePtr != 0) { mNativePtr = nativePtr; mOwnsNativeParcelObject = false; } else { //此时传递为0,Parcel内存区域由我们自己创建 //Native层Parcel地址指针 mNativePtr = nativeCreate(); mOwnsNativeParcelObject = true; }}
实际上 Parcel 的核心实现都在 Parcel.cpp,Java 层 Parcel 只是对 native 层接口的调用封装,我们先看下 native 层 Parcel 的创建过程:
private static native long nativeCreate();//jni注册{"nativeCreate", "()J",(void*)android_os_Parcel_create},//nativeCreate的具体实现static jlong android_os_Parcel_create(JNIEnv* env, jclass clazz){ //创建native层Parcel对象 Parcel* parcel = new Parcel(); return reinterpret_cast(parcel);}
有复用就一定有回收的逻辑,看下 Parcel 的回收逻辑:
public final void recycle() { if (DEBUG_RECYCLE) mStack = null; //释放其native内存 freeBuffer(); final Parcel[] pool; //使用new Parcel() 默认为true //表示我们对它的生命周期负责 if (mOwnsNativeParcelObject) { pool = sOwnedPool; } else { mNativePtr = 0; pool = sHolderPool; } synchronized (pool) { for (int i = 0; i < POOL_SIZE; i++) { //获取可以被缓存的位置 if (pool[i] == null) { pool[i] = this; return; } } }}
继续向下分析,执行 intent.writeToParcel 将 Parcel 作为参数,这里我们主要跟踪下 Parcelable 的写入过程,
由于采用 Intent 传递附加参数过程,最终都会保存到 Bundle 中,而 Bundle 用于实际存储数据的则是通过 Map 完成的:
//添加附加参数 public @NonNull Intent putExtra(String name, Parcelable value) { if (mExtras == null) { //创建Bundle实例 mExtras = new Bundle(); } //实际保存在Bundle中 mExtras.putParcelable(name, value); return this;}
接着看下 Bundle 的保存附加数据过程:
public void putParcelable(@Nullable String key, @Nullable Parcelable value) { unparcel(); //该mMap是一个ArrayMap实例 mMap.put(key, value); mFlags &= ~FLAG_HAS_FDS_KNOWN;}
此时将携带的附加参数进行序列化,实际是调用 Bundle 的 writeToParcel 方法,这里实际操作在其父类 BaseBundle 的 writeToParcelInner 方法中:
void writeToParcelInner(Parcel parcel, int flags) { if (parcel.hasReadWriteHelper()) { //这里(mReadWriteHelper != null) && (mReadWriteHelper != ReadWriteHelper.DEFAULT); //mReadWriteHelper默认是DEFAULT unparcel(); } final ArrayMap map; synchronized (this) { if (mParcelledData != null) { //当前Bundle是否有被解析过 if (mParcelledData == NoImagePreloadHolder.EMPTY_PARCEL) { parcel.writeInt(0); } else { int length = mParcelledData.dataSize(); parcel.writeInt(length); parcel.writeInt(mParcelledByNative ? BUNDLE_MAGIC_NATIVE : BUNDLE_MAGIC); parcel.appendFrom(mParcelledData, 0, length); } return; } map = mMap; } if (map == null || map.size() <= 0) { parcel.writeInt(0); return; } //parcel当前内存起始位置 int lengthPos = parcel.dataPosition(); //这里是附加参数的长度,临时占位 parcel.writeInt(-1); // dummy, will hold length //魔数 parcel.writeInt(BUNDLE_MAGIC); //附加数据起始位置 int startPos = parcel.dataPosition(); //写入当前所有的附加参数 parcel.writeArrayMapInternal(map); //附加数据结束位置 int endPos = parcel.dataPosition(); //重新指到lengthPos parcel.setDataPosition(lengthPos); //此时可以计算出附加数据的真实长度 int length = endPos - startPos; parcel.writeInt(length); //重新指到当前结束位置endpos parcel.setDataPosition(endPos);}
代码中已经标注了详细的注释,这里我们重点看下 Parcelable 的序列化机制parcel.writeArrayMapInternal 方法:
void writeArrayMapInternal(ArrayMap val) { if (val == null) { writeInt(-1); return; } //附件参数的长度 final int N = val.size(); //写入时Map的长度 writeInt(N); int startPos; for (int i = 0; i < N; i++) { //写入key writeString(val.keyAt(i)); //写入value,每个value类型都会额外浪费4字节(Int) writeValue(val.valueAt(i)); }}
写入当前附加参数的总长度,遍历 Map 容器,由于 key 是固定类型 String,这里我们重点关注下 writeValue 方法:
public final void writeValue(Object v) { if (v == null) { writeInt(VAL_NULL); } else if (v instanceof String) { //String类型 writeInt(VAL_STRING); writeString((String) v); } else if (v instanceof Integer) { //Integer类型 writeInt(VAL_INTEGER); writeInt((Integer) v); } else if (v instanceof Map) { //Map类型 writeInt(VAL_MAP); writeMap((Map) v); } else if (v instanceof Bundle) { // Must be before Parcelable writeInt(VAL_BUNDLE); writeBundle((Bundle) v); } else if (v instanceof PersistableBundle) { writeInt(VAL_PERSISTABLEBUNDLE); writePersistableBundle((PersistableBundle) v); } else if (v instanceof Parcelable) { //Parcelable类型 writeInt(VAL_PARCELABLE); writeParcelable((Parcelable) v, 0); } else if (v instanceof Short) { writeInt(VAL_SHORT); writeInt(((Short) v).intValue()); } else if (v instanceof Long) { writeInt(VAL_LONG); writeLong((Long) v); } //... 省略}
大家是否有注意到 Value 的写入过程,系统自己定义了一套类型映射关系,根据Value 数据类型先写四个字节的类型信息 writeInt(VAL_NULL)。看下这个类型映射关系:
private static final int VAL_NULL = -1;private static final int VAL_STRING = 0;private static final int VAL_INTEGER = 1;private static final int VAL_MAP = 2;private static final int VAL_BUNDLE = 3;private static final int VAL_PARCELABLE = 4;private static final int VAL_SHORT = 5;private static final int VAL_LONG = 6;private static final int VAL_FLOAT = 7;private static final int VAL_DOUBLE = 8;private static final int VAL_BOOLEAN = 9;private static final int VAL_CHARSEQUENCE = 10;//... 省略
这里不知大家是否有意识到,每个 Value 写入都会额外附加 4 个字节的类型信息。用于表示当前 Value 的数据类型,这在后续反序列化时要根据该数据类型进行创建实例。
看下 Parcelable 的序列化过程 writeParcelable 方法:
public final void writeParcelable(Parcelable p, int parcelableFlags) { if (p == null) { writeString(null); return; } //写入Parcelable的全限定名,反序列化时,需要根据该全限定名查找一个类:Classloader.loadClass writeParcelableCreator(p); //这里是否大家熟悉呢?其实回到了我们自定义的Parcelable中 p.writeToParcel(this, parcelableFlags);}public final void writeParcelableCreator(Parcelable p) { //写入Parceable的全限定名 String name = p.getClass().getName(); writeString(name);}
其中 p.writeToParcel 是否感到熟悉呢?其实这里就是回到了我们自定义 WebParams 的 writeToParcel 方法中:
@Overridepublic void writeToParcel(Parcel dest, int flags){ dest.writeInt(age); dest.writeString(name); dest.writeLong(serialUid); dest.writeCharArray(flag); dest.writeByteArray(like);}
此时就是我们按照按照实际要序列化内容写入到 Parcel 内存了。
分析到这里,实际上整个 Parcelable 数据序列化过程就已经一幕了然了,Parcelable 只是一个序列化规则,它向开发人员暴露 Parcel 操作对象,自行写入要序列化的数据。它的核心实现都在 native 层 Parcel.cpp,Java 层 Parcel 是对其接口的封装。
下面来分析下 Parcelable 的反序列化过程:
final Bundle extra = getIntent().getExtras();final WebParams params = extra.getParcelable(WebParams.EXTRA_PARAMS_KEY);
还是通过上面示例进行分析
@Nullablepublic T getParcelable(@Nullable String key) { //解析Parcel数据 unparcel(); //解析数据会封装在该map中 Object o = mMap.get(key); if (o == null) { return null; } try { return (T) o; } catch (ClassCastException e) { typeWarning(key, o, "Parcelable", e); return null; }}
这里我们重点跟踪下 unparcel 方法:
void unparcel() { synchronized (this) { final Parcel source = mParcelledData; if (source != null) { //这里开始从Parcel读取序列化的数据 initializeFromParcelLocked(source, /*recycleParcel=*/ true, mParcelledByNative); } else { //...忽略 } }}//在unparcel方法调用该方法private void initializeFromParcelLocked(@NonNull Parcel parcelledData, boolean recycleParcel, boolean parcelledByNative) { //如果Parcel数据为空 if (isEmptyParcel(parcelledData)) { if (mMap == null) { mMap = new ArrayMap<>(1); } else { //将Map中每个位置元素置为null, mMap.erase(); } mParcelledData = null; mParcelledByNative = false; return; } //获取附加参数的长度,这里对应写入时Map的size final int count = parcelledData.readInt(); if (count < 0) { return; } ArrayMap map = mMap; if (map == null) { //按照size创建ArrayMap map = new ArrayMap<>(count); } else { map.erase(); //调整为新的长度 map.ensureCapacity(count); } try { if (parcelledByNative) { parcelledData.readArrayMapSafelyInternal(map, count, mClassLoader); } else { parcelledData.readArrayMapInternal(map, count, mClassLoader); } } catch (BadParcelableException e) { if (sShouldDefuse) { map.erase(); } else { throw e; } } finally { mMap = map; if (recycleParcel) { recycleParcel(parcelledData); } //解析过后一定置为null //避免后续get相关内容时再次发生解析 mParcelledData = null; mParcelledByNative = false; }}
前面是一些列的数据校验和缓存设置,这里我们重点分析下 parcelledData.readArrayMapInternal 方法:
void readArrayMapInternal(ArrayMap outVal, int N, ClassLoader loader) { if (DEBUG_ARRAY_MAP) { RuntimeException here = new RuntimeException("here"); here.fillInStackTrace(); Log.d(TAG, "Reading " + N + " ArrayMap entries", here); } int startPos; //根据写入时Map长度 while (N > 0) { if (DEBUG_ARRAY_MAP) startPos = dataPosition(); //读取key String key = readString(); //读取Value Object value = readValue(loader); //追加到ArrayMap中,可以直接理解成put(key, valu) //append系统留给自己使用的 outVal.append(key, value); N--; } outVal.validate(); //此时一系列读取完毕之后,全部都保存在Bundle的Map中, //后续我们通过Bundle的get操作直接从该Map中获取}
还记得在前面分析写入 Parcel 数据时,都是通过键值对的形式,key 是固定的 String 类型,所以读取时也是先通过 readString 读取 key,紧接着 readValue 方法读取对应的 value:
public final Object readValue(ClassLoader loader) { //先读取类型 int type = readInt(); //根据value类型进行匹配 switch (type) { case VAL_NULL: //null类型 return null; case VAL_STRING: //String类型 return readString(); case VAL_INTEGER: //int类型 return readInt(); case VAL_MAP: //Map类型 return readHashMap(loader); case VAL_PARCELABLE: //Parcelable类型读取 return readParcelable(loader); case VAL_SHORT: return (short) readInt(); // ... 省略 }}
读取 value 相比 key 要麻烦一些,前面分析序列化过程写入 value 数据时,先写入该 value 数据对应的 int 类型,该类型在反序列化时会用到,此时系统就是根据该 int 值对应的 value 类型反序列化对应数据。我们以 readParcelable 类型为例:
public final T readParcelable(ClassLoader loader) { //获取对应Parcelable的Creator Parcelable.Creator<?> creator = readParcelableCreator(loader); if (creator == null) { return null; } if (creator instanceof Parcelable.ClassLoaderCreator<?>) { //Creator也可以是ClassLoaderCreator //ClassLoaderCreator是Creator的子类 Parcelable.ClassLoaderCreator<?> classLoaderCreator = (Parcelable.ClassLoaderCreator<?>) creator; return (T) classLoaderCreator.createFromParcel(this, loader); } //直接通过creatorFromParcel创建对应Parcelable //此时已经回到了自定义Parcelable中CREATOR内部类的createFromParcel方法 return (T) creator.createFromParcel(this);}
先来看 Parcelable 的 CREATOR 的获取方式:
public final Parcelable.Creator<?> readParcelableCreator(ClassLoader loader) { String name = readString(); if (name == null) { return null; } Parcelable.Creator<?> creator; synchronized (mCreators) { //系统根据ClassLoader缓存Parcelable的Creator //获取当前类加载器缓存过的Parcelable的Creator实例 HashMap> map = mCreators.get(loader); if (map == null) { map = new HashMap<>(); mCreators.put(loader, map); } //缓存中是否存在 creator = map.get(name); if (creator == null) { try { ClassLoader parcelableClassLoader = (loader == null ? getClass().getClassLoader() : loader); //反射获取该类对象 Class<?> parcelableClass = Class.forName(name, false /* initialize */, parcelableClassLoader); if (!Parcelable.class.isAssignableFrom(parcelableClass)) { //必须是Parcelable类型 throw new BadParcelableException("Parcelable protocol requires subclassing " + "from Parcelable on class " + name); } //反射获取Parcelable中CREATOR Field Field f = parcelableClass.getField("CREATOR"); if ((f.getModifiers() & Modifier.STATIC) == 0) { //必须是static的 throw new BadParcelableException("Parcelable protocol requires " + "the CREATOR object to be static on class " + name); } Class<?> creatorType = f.getType(); if (!Parcelable.Creator.class.isAssignableFrom(creatorType)) { //必须是Parcelable.Creator类型 throw new BadParcelableException("Parcelable protocol requires a " + "Parcelable.Creator object called " + "CREATOR on class " + name); } //获取到该Parcelable对应的Creator实例。 creator = (Parcelable.Creator<?>) f.get(null); } catch (IllegalAccessException e) { throw new BadParcelableException( "IllegalAccessException when unmarshalling: " + name); } catch (ClassNotFoundException e) { throw new BadParcelableException( "ClassNotFoundException when unmarshalling: " + name); } catch (NoSuchFieldException e) { throw new BadParcelableException("Parcelable protocol requires a " + "Parcelable.Creator object called " + "CREATOR on class " + name); } if (creator == null) { throw new BadParcelableException("Parcelable protocol requires a " + "non-null Parcelable.Creator object called " + "CREATOR on class " + name); } //注意,系统缓存每个使用到的Parcelable的Creator实例 //这样下次创建对应的Parcelable时,直接通过Creator实例createFromParcel创建, //避免了再次反射 map.put(name, creator); } } return creator;}
系统首先根据 Classloader(不同的 Classloader 加载的 Class 对象不相等) 获取保存 CREATOR 的 Map 容器,然后根据 value 类型的全限定名在该 Map 中查找是否已经存在对应的 CREATOR 实例,否则通过 Classloader 加载该类,并反射获取该类的 CREATOR 字段;
从这里我们可以看出:Parcelable 中为什么要包含一个 CREATOR 的字段,并且一定要声明为 static,而且系统会缓存每个已经使用过的 Parcelable 的 CREATOR 实例,便于下次反序列化时直接通过 new 创建 该 Parcelable 实例。
回到 readParcelable 方法:直接调用 CREATOR 的 createFromParcel 方法,此时就回到了我们自定义的 WebParams 中:
@Override public WebParams createFromParcel(Parcel in) { return new WebParams(in); }
然后在 WebParams 的构造方法中根据写入时顺序将其赋值给 WebParams 成员:
protected WebParams(Parcel in) { this.age = in.readInt(); this.name = in.readString(); this.serialUid = in.readLong(); in.readCharArray(flag); in.readByteArray(like);}
至于为什么一定要按照顺序读取写入时,通过分析源码,相信大家也一定能够理解,数据是按照顺序进行排列的。至此关于 Parcelable 的序列化以及反序列化的 Java 层部分就分析完了。
通过分析我们发现 Android 中 Parcelable 序列化和 Java 原生提供的 Serializalbe 序列化机制差别还是比较大的,Parcelable 只会在内存中序列化操作,并不会将数据存储到磁盘里。一般来说,如果需要持久化存储的话,一般还是不得不选择性能更差的 Serializable 方案,那有没有其它选择呢?你可以参考下篇文章《Android 对象序列化之追求性能完美的 Serial》。
总结
Parcelable 的原理还是比较简单的,它的核心实现都在 Parcel.cpp。
正如前面给大家讲到:Parcelable 时间开销和使用成本的权衡上,Parcelable 机制选择的是性能优先。所以它在写入和读取的时候,都需要手动添加自定义代码,使用起来相比 Serializable 会复杂很多。但是正因为这样,Parcelable 才不需要采用反射的方式去实现序列化和反序列化(当然,实际 Parcelable 也用到了少许反射,这里是相对 Serialzable 而言微不足道)
Parcelable 的持久化存储
虽然 Parcelable 默认不支持持久化存储,但是我们也可以通过一些取巧的方式实现,在 Parcel.java 中 marshall 接口获取 byte 数组,然后存储在文件中从而实现 Parcelable 的永久存储。
public final byte[] marshall() { return nativeMarshall(mNativePtr);}
但是它也存在一些问题:
- 系统版本的兼容性。由于 Parcelable 设计本意是在内存中使用,我们无法保证所有 Android 版本的 Parcel.cpp 实现都完全一致。如果不同系统版本实现有所差异i,或者厂商修改了实现,可能会存在问题。
- 数据前后兼容性。Parcelable 并没有版本管理的设计,如果我们类的版本出现升级,写入的顺序及字段类型的兼容都需要格外注意,这也带来了很大的维护成本。
一般来说,如果需要持久化存储的话,一般还是不得不选择性能更差的 Serializable 方案。
以上便是个人在学习 Serial 时的心得和体会,文中分析如有不妥或更好的分析结果,还请大家指出!
文章如果对你有帮助,就请留个赞吧!
更多相关文章
- Android 数据库之 SQLiteConnectionPool 源码分析
- Android 对象序列化之追求完美的 Serial
- android:inputType参数类型说明
- Android 数据库技术
- 老罗Android开发视频教程( android解析json数据 )4集集合
- Android 限制EditText只能输入数字、限制输入类型、限制输入长度
- Java语言程序设计(五)从对话框获取输入及String类型
- Java语言程序设计(四)类型转换及转义字符
- 关于接口类型的10篇课程推荐