关于数据库升级的其他文章:

数据库升级完整解决方案

Android版本升级同时Sqlite数据库的升级及之前数据的保留

--------------------------------------------------------------------------------------------------------------------------------------------------------

问题:Android数据库更新并保留原来的数据如何实现

Andoird的SQLiteOpenHelper类中有一个onUpgrade方法。帮助文档中只是说当数据库升级时该方法被触发。经过实践,解决了我一连串的

疑问:
1. 帮助文档里说的“数据库升级”是指什么?
你开发了一个应用,当前是1.0版本。该程序用到了数据库。到1.1版本时,你在数据库的某个表中增加了一个字段。那么软件1.0版本用的数据库在软件1.1版本就要被升级了(当然这里的升级包括两个概念,一个是应用升级还有就是数据库升级)

关于应用升级我们知道直接在AndroidManifest.xml文件中修改即可。

关于数据库升级我们就需要在代码中修改:

[java] view plain copy
  1. privatestaticfinalStringDBNAME="ads.db";
  2. privatestaticfinalintVERSION=1;
  3. publicDBOpenHelper(Contextcontext){
  4. super(context,DBNAME,null,VERSION);
  5. }
我们在使用数据库的时候,都会自定义一个Helper类,需要实现SQLiteOpenHelper类(是个抽象类),然后实现他的onCreate和onUpgrade方法。上面的一段代码片段就是我们自定义的Helper类中的,这里我们会看到在构造方法中,我们会调用父类(就是SQLiteOpenHelper类的构造方法),这里需要传递Context变量、数据库名称、以及数据库的版本号(第三个参数是一个工厂类,可以传递null)。所以我们就可以在这个地方进行数据库的升级了,当然我们一般第一次把数据库的版本号设置成1,以后递增即可,这个的原因,后面在分析源码的时候会提到。

2. 数据库升级应该注意什么?

软件的1.0版本升级到1.1版本时,数据库中老的数据不能丢。那么在1.1版本的应用中就要有地方能够检测出来新的数据库与老的数据库不兼容。并且能够有办法把1.0应用中的数据库升级到1.1应用时能够使用的数据库。换句话说,要在1.0应用的数据库中的那个表中增加那个字段,并赋予这个字段默认值。


3. 应用如何知道数据库需要升级?
SQLiteOpenHelper类的构造函数有一个参数是int version,它的意思就是指数据库版本号。比如在应用1.0版本中,我们使用
SQLiteOpenHelper访问数据库时,该参数为1,那么数据库版本号1就会写在我们的数据库中。到了1.1版本,我们的数据库需要发生变化,那么我们1.1版本的程序中就要使用一个大于1的整数来构造SQLiteOpenHelper类,用于访问新的数据库,比如2。当我们的1.1新程序读取1.0版本的老数据库时,就发现老数据库里存储的数据库版本是1,而我们新程序访问它时填的版本号为2,系统就知道数据库需要升级。


4. 何时触发数据库升级?如何升级?

当系统在构造SQLiteOpenHelper类的对象时,如果发现版本号不一样,就会自动调用onUpgrade函数,让你在这里对数据库进行升级。根据上述场景,在这个函数中把老版本数据库的相应表中增加字段,并给每条记录增加默认值即可。

新版本号和老版本号都会作为onUpgrade函数的参数传进来,便于开发者知道数据库应该从哪个版本升级到哪个版本。

升级完成后,数据库会自动存储最新的版本号为当前数据库版本号。


下面就从源码的角度分析一下,执行流程:

首先来看一下SQLiteOpenHelper.java类

[java] view plain copy
  1. publicSQLiteOpenHelper(Contextcontext,Stringname,CursorFactoryfactory,intversion){
  2. this(context,name,factory,version,null);
  3. }

我们看到了,这个构造方法,就是我们在子类中调用的,第三个参数是一个游标工厂类,可以传递null。再看一下他的其他构造方法:

[java] view plain copy
  1. publicSQLiteOpenHelper(Contextcontext,Stringname,CursorFactoryfactory,intversion,
  2. DatabaseErrorHandlererrorHandler){
  3. if(version<1)thrownewIllegalArgumentException("Versionmustbe>=1,was"+version);
  4. mContext=context;
  5. mName=name;
  6. mFactory=factory;
  7. mNewVersion=version;
  8. mErrorHandler=errorHandler;
  9. }

这里我们会看到一个信息,就是会判断版本号,如果版本号小于1的话直接抛异常了,所以我们一般将版本号设置成1。构造结束了,下面来看一下他的onCreate和onUpgrade方法:

[java] view plain copy
  1. publicabstractvoidonCreate(SQLiteDatabasedb);
  2. publicabstractvoidonUpgrade(SQLiteDatabasedb,intoldVersion,intnewVersion);

这两个方法都是抽象的,需要子类去实现他们。

好吧,那下面我们该看哪个方法呢?当然是看我们使用到的方法了。我们一般在使用数据库的时候会用到的代码:

[java] view plain copy
  1. DBOpenHelperopenHelper=newDBOpenHelper(this);
  2. SQLiteDatabasedb=openHelper.getWritableDatabase();

初始化自定义的Helper类,然后获取一个数据库对象SQLiteDatabase。那么我们就来看看你getWritableDatabase方法:

[java] view plain copy
  1. publicSQLiteDatabasegetWritableDatabase(){
  2. synchronized(this){
  3. returngetDatabaseLocked(true);
  4. }
  5. }

这里用到了同步机制了,接着看getDatabaseLocked方法:

[java] view plain copy
  1. privateSQLiteDatabasegetDatabaseLocked(booleanwritable){
  2. if(mDatabase!=null){
  3. if(!mDatabase.isOpen()){
  4. //Darn!TheuserclosedthedatabasebycallingmDatabase.close().
  5. mDatabase=null;
  6. }elseif(!writable||!mDatabase.isReadOnly()){
  7. //Thedatabaseisalreadyopenforbusiness.
  8. returnmDatabase;
  9. }
  10. }
  11. if(mIsInitializing){
  12. thrownewIllegalStateException("getDatabasecalledrecursively");
  13. }
  14. SQLiteDatabasedb=mDatabase;
  15. try{
  16. mIsInitializing=true;
  17. if(db!=null){
  18. if(writable&&db.isReadOnly()){
  19. db.reopenReadWrite();
  20. }
  21. }elseif(mName==null){
  22. db=SQLiteDatabase.create(null);
  23. }else{
  24. try{
  25. if(DEBUG_STRICT_READONLY&&!writable){
  26. finalStringpath=mContext.getDatabasePath(mName).getPath();
  27. db=SQLiteDatabase.openDatabase(path,mFactory,
  28. SQLiteDatabase.OPEN_READONLY,mErrorHandler);
  29. }else{
  30. db=mContext.openOrCreateDatabase(mName,mEnableWriteAheadLogging?
  31. Context.MODE_ENABLE_WRITE_AHEAD_LOGGING:0,
  32. mFactory,mErrorHandler);
  33. }
  34. }catch(SQLiteExceptionex){
  35. if(writable){
  36. throwex;
  37. }
  38. Log.e(TAG,"Couldn'topen"+mName
  39. +"forwriting(willtryread-only):",ex);
  40. finalStringpath=mContext.getDatabasePath(mName).getPath();
  41. db=SQLiteDatabase.openDatabase(path,mFactory,
  42. SQLiteDatabase.OPEN_READONLY,mErrorHandler);
  43. }
  44. }
  45. onConfigure(db);
  46. finalintversion=db.getVersion();
  47. if(version!=mNewVersion){
  48. if(db.isReadOnly()){
  49. thrownewSQLiteException("Can'tupgraderead-onlydatabasefromversion"+
  50. db.getVersion()+"to"+mNewVersion+":"+mName);
  51. }
  52. db.beginTransaction();
  53. try{
  54. if(version==0){
  55. onCreate(db);
  56. }else{
  57. if(version>mNewVersion){
  58. onDowngrade(db,version,mNewVersion);
  59. }else{
  60. onUpgrade(db,version,mNewVersion);
  61. }
  62. }
  63. db.setVersion(mNewVersion);
  64. db.setTransactionSuccessful();
  65. }finally{
  66. db.endTransaction();
  67. }
  68. }
  69. onOpen(db);
  70. if(db.isReadOnly()){
  71. Log.w(TAG,"Opened"+mName+"inread-onlymode");
  72. }
  73. mDatabase=db;
  74. returndb;
  75. }finally{
  76. mIsInitializing=false;
  77. if(db!=null&&db!=mDatabase){
  78. db.close();
  79. }
  80. }
  81. }

这个方法的东西就有点多了,貌似也是最核心的部分。

首先我们看到一个字段mDatabase,是个SQLiteDatabase类型的

[java] view plain copy
  1. privateSQLiteDatabasemDatabase;

下面来看一下开始部分的代码:

[java] view plain copy
  1. if(mDatabase!=null){
  2. if(!mDatabase.isOpen()){
  3. //Darn!TheuserclosedthedatabasebycallingmDatabase.close().
  4. mDatabase=null;
  5. }elseif(!writable||!mDatabase.isReadOnly()){
  6. //Thedatabaseisalreadyopenforbusiness.
  7. returnmDatabase;
  8. }
  9. }

如果mDatabase不为null的话,然后在判断如果这个数据库是否是关闭的状态,如果关闭了就将其设置null。如果没有关闭,就判断他的状态是可读还是可写的状态,然后返回一个实例即可.


继续下面的代码:

[java] view plain copy
  1. SQLiteDatabasedb=mDatabase;
  2. try{
  3. mIsInitializing=true;
  4. if(db!=null){
  5. if(writable&&db.isReadOnly()){
  6. db.reopenReadWrite();
  7. }
  8. }elseif(mName==null){
  9. db=SQLiteDatabase.create(null);
  10. }else{
  11. try{
  12. if(DEBUG_STRICT_READONLY&&!writable){
  13. finalStringpath=mContext.getDatabasePath(mName).getPath();
  14. db=SQLiteDatabase.openDatabase(path,mFactory,
  15. SQLiteDatabase.OPEN_READONLY,mErrorHandler);
  16. }else{
  17. db=mContext.openOrCreateDatabase(mName,mEnableWriteAheadLogging?
  18. Context.MODE_ENABLE_WRITE_AHEAD_LOGGING:0,
  19. mFactory,mErrorHandler);
  20. }
  21. }catch(SQLiteExceptionex){
  22. if(writable){
  23. throwex;
  24. }
  25. Log.e(TAG,"Couldn'topen"+mName
  26. +"forwriting(willtryread-only):",ex);
  27. finalStringpath=mContext.getDatabasePath(mName).getPath();
  28. db=SQLiteDatabase.openDatabase(path,mFactory,
  29. SQLiteDatabase.OPEN_READONLY,mErrorHandler);
  30. }
  31. }

这段代码主要是初始化一个SQLiteDatabase对象。


继续:

[java] view plain copy
  1. finalintversion=db.getVersion();
  2. if(version!=mNewVersion){
  3. if(db.isReadOnly()){
  4. thrownewSQLiteException("Can'tupgraderead-onlydatabasefromversion"+
  5. db.getVersion()+"to"+mNewVersion+":"+mName);
  6. }
  7. db.beginTransaction();
  8. try{
  9. if(version==0){
  10. onCreate(db);
  11. }else{
  12. if(version>mNewVersion){
  13. onDowngrade(db,version,mNewVersion);
  14. }else{
  15. onUpgrade(db,version,mNewVersion);
  16. }
  17. }
  18. db.setVersion(mNewVersion);
  19. db.setTransactionSuccessful();
  20. }finally{
  21. db.endTransaction();
  22. }
  23. }

这段代码就是我们这次的主要研究对象,也会解决我们的很多疑问。代码的逻辑很简单:

首先他获取到当前数据库的版本号,这里的db就是之前代码中创建的一个SQLiteDatabase对象,然后调用它的getVersion方法获取版本号,查看getVersion的源码:

[java] view plain copy
  1. publicintgetVersion(){
  2. return((Long)DatabaseUtils.longForQuery(this,"PRAGMAuser_version;",null)).intValue();
  3. }


这里看到是一个DatabaseUtils类的longForQuery方法,继续找到这个方法的源码:

[java] view plain copy
  1. publicstaticlonglongForQuery(SQLiteDatabasedb,Stringquery,String[]selectionArgs){
  2. SQLiteStatementprog=db.compileStatement(query);
  3. try{
  4. returnlongForQuery(prog,selectionArgs);
  5. }finally{
  6. prog.close();
  7. }
  8. }

找到这个方法了,其实他内部很简单,就是执行一个sql语句,sql语句为:

[java] view plain copy
  1. PRAGMAuser_version;
这里查询的结果是long类型的,所以我们在定义数据库的版本的时候一般是整数,递增也是整数。

那么当我们首次创建数据库的时候他的版本值是多少呢?其实这个可以猜的。可能为0。那么我们就来做个验证吧。这里的验证不是用代码的方式了,而是借助于一个软件:Sqlite Expert

这个软件用起来还是比较简单的。我们新建一个数据库,然后执行上面的SQL语句,结果如下图:

【android】当数据库需要更新时我们该怎么办?_第1张图片

果然第一次得到的版本号是0,好的下面继续来看一下之前的代码分析,之前分析到了,获取数据库的版本号,然后就是和之前的版本进行比较,如果不相等


这里又会做一个判断,判断当前的数据库是否为只读的,如果是只读的话,是不能进行后续的更新操作,抛个异常。

[java] view plain copy
  1. if(db.isReadOnly()){
  2. thrownewSQLiteException("Can'tupgraderead-onlydatabasefromversion"+
  3. db.getVersion()+"to"+mNewVersion+":"+mName);
  4. }

如果不是只读的话,继续下面的代码:

[java] view plain copy
  1. db.beginTransaction();
  2. try{
  3. if(version==0){
  4. onCreate(db);
  5. }else{
  6. if(version>mNewVersion){
  7. onDowngrade(db,version,mNewVersion);
  8. }else{
  9. onUpgrade(db,version,mNewVersion);
  10. }
  11. }
  12. db.setVersion(mNewVersion);
  13. db.setTransactionSuccessful();
  14. }finally{
  15. db.endTransaction();
  16. }

开启一个事务。这里会判断如果获取到的数据库的版本为0,那么就执行onCreate方法,如果版本号有增加就会执行onUpgrade方法,如果版本号有递减的话,就会执行onDowngrade方法,抛异常了。

[java] view plain copy
  1. publicvoidonDowngrade(SQLiteDatabasedb,intoldVersion,intnewVersion){
  2. thrownewSQLiteException("Can'tdowngradedatabasefromversion"+
  3. oldVersion+"to"+newVersion);
  4. }

然后设置数据库的版本为最新的值。结束事务。


这里在使用的时候遇到一个问题,就是我在onUpgrade方法中调用了onCreate方法,但是此时onCreate方法会报异常,当然我们将其捕获了(在外面使用Helper的时候捕获的),但是每次打开应用的时候,onUpgrade方法都会执行。当时一直在找原因。没头绪呀。

其实这里就可以找到原因,因为当onUpgrade方法报异常(因为在onUpgrade方法中调用了onCreate方法,当onCreate方法报异常时,onUpgrade方法没有捕获到这个异常就还会报异常)之后,后续代码就不执行了,那么下面的设置数据库最新版本号的代码也不会执行了,所以每次打开app的时候,会进行版本号的比对,结果还是会执行onUpgrade方法,这个方法还是会报异常,所以会出现每次打开app的时候onUpgrade方法都会执行一次。下面来看一下

[java] view plain copy
  1. packagecom.sohu.sqlitedemo;
  2. importandroid.content.Context;
  3. importandroid.database.sqlite.SQLiteDatabase;
  4. importandroid.database.sqlite.SQLiteDatabase.CursorFactory;
  5. importandroid.database.sqlite.SQLiteOpenHelper;
  6. importandroid.util.Log;
  7. importandroid.widget.Toast;
  8. publicclassDBOpenHelperextendsSQLiteOpenHelper{
  9. privatestaticfinalStringDBNAME="ads.db";
  10. privatestaticfinalintVERSION=1;
  11. publicDBOpenHelper(Contextcontext){
  12. super(context,DBNAME,null,VERSION);
  13. }
  14. @Override
  15. publicvoidonCreate(SQLiteDatabasedb){
  16. Log.i("DEMO","oldVersion,onCreate()");
  17. db.execSQL("CREATETABLEIFNOTEXISTSofflineBanner("//
  18. +"idintegerprimarykeyautoincrement,"//
  19. +"vidVARCHAR(100),"//
  20. //+"adsequencesinteger,"
  21. //+"isofflineadsinteger,"
  22. +"ImpressionVARCHAR(2000),"//
  23. +"DurationVARCHAR(50),"//
  24. +"ClickThroughVARCHAR(500),"//
  25. +"ClickTrackingVARCHAR(500),"//
  26. +"MediaFileVARCHAR(500),"//
  27. +"creativeViewVARCHAR(500),"//
  28. +"startVARCHAR(500),"//
  29. +"firstQuartileVARCHAR(500),"//
  30. +"midpointVARCHAR(500),"//
  31. +"thirdQuartileVARCHAR(500),"//
  32. +"completeVARCHAR(500),"//
  33. +"sdkTrackingVARCHAR(2000),"//
  34. +"sdkClickVARCHAR(2000),"//
  35. +"timeVARCHAR(50));");//
  36. //这行代码会报异常
  37. db.execSQL("CREATETABLEofflinePause("//
  38. +"idintegerprimarykeyautoincrement,"//
  39. +"vidVARCHAR(100),"//
  40. +"ImpressionVARCHAR(2000),"//
  41. +"StaticResourceVARCHAR(500),"//
  42. +"NonLinearClickThroughVARCHAR(500),"//
  43. +"sdkTrackingVARCHAR(2000),"//
  44. +"sdkClickVARCHAR(2000),"//
  45. +"timeVARCHAR(50));");//
  46. db.execSQL("CREATETABLEIFNOTEXISTSdownload_url("//
  47. +"idintegerprimarykeyautoincrement,"//
  48. +"urlvarchar(500),"//
  49. +"statusinteger,"//
  50. +"lengthinteger)");//
  51. }
  52. @Override
  53. publicvoidonUpgrade(SQLiteDatabasedb,intoldVersion,intnewVersion){
  54. Log.i("DEMO","oldVersion="+oldVersion+",newVersion"+newVersion);
  55. if(oldVersion!=newVersion){
  56. db.execSQL("ALTERTABLEofflineBannerADDCOLUMNadsequenceinteger;");
  57. db.execSQL("ALTERTABLEofflineBannerADDCOLUMNisofflineadinteger;");
  58. db.execSQL("DROPTABLEIFEXISTSdownload_url");
  59. }
  60. //执行方法会报异常
  61. onCreate(db);
  62. }
  63. }


报异常的原因很简单。当我需要更新的时候,就会执行onUpgrade方法,这里又执行了onCreate方法,在创建pause表的时候会报异常,因为这个表已经存在了。

【android】当数据库需要更新时我们该怎么办?_第2张图片

当我们把代码外面加上try...catch的时候:

[java] view plain copy
  1. try{
  2. //开始使用数据库
  3. DBOpenHelperopenHelper=newDBOpenHelper(this);
  4. SQLiteDatabasedb=openHelper.getWritableDatabase();
  5. db.close();
  6. }catch(Exceptione){
  7. }

问题会解决,但是onUpgrade方法就会执行多次:

【android】当数据库需要更新时我们该怎么办?_第3张图片
所以还是要解决一下onCreate方法中的异常问题,那个异常就是因为表存在了,所以建立表的时候需要判断表是否已经存在了。这样就可以了。

上面从源码的角度了解了原理,下面来看一下实例:

做Android应用,不可避免的会与SQLite打交道。随着应用的不断升级,原有的数据库结构可能已经不再适应新的功能,这时候,就需要对SQLite数据库的结构进行升级了。 SQLite提供了ALTER TABLE命令,允许用户重命名或添加新的字段到已有表中,但是不能从表中删除字段。

并且只能在表的末尾添加字段,比如,为 Subscription添加两个字段:
[java] view plain copy
  1. ALTERTABLESubscriptionADDCOLUMNActivationBLOB;
  2. ALTERTABLESubscriptionADDCOLUMNKeyBLOB;

另外,如果遇到复杂的修改操作,比如在修改的同时,需要进行数据的转移,那么可以采取在一个事务中执行如下语句来实现修改表的需求。

1. 将表名改为临时表
[java] view plain copy
  1. ALTERTABLESubscriptionRENAMETO__temp__Subscription;

2. 创建新表
[java] view plain copy
  1. CREATETABLESubscription(OrderIdVARCHAR(32)PRIMARYKEY,UserNameVARCHAR(32)NOTNULL,ProductIdVARCHAR(16)
  2. NOTNULL);

3. 导入数据 
 
[java] view plain copy
  1. INSERTINTOSubscriptionSELECTOrderId,“”,ProductIdFROM__temp__Subscription;
或者  
[java] view plain copy
  1. INSERTINTOSubscription()SELECTOrderId,“”,ProductIdFROM__temp__Subscription;
* 注意 双引号”” 是用来补充原来不存在的数据的
4. 删除临时表 
 
[java] view plain copy
  1. DROPTABLE__temp__Subscription;
通过以上四个步骤,就可以完成旧数据库结构向新数据库结构的迁移,并且其中还可以保证数据不会应为升级而流失。
当然,如果遇到减少字段的情况,也可以通过创建临时表的方式来实现。


Android应用程序更新的时候如果数据库修改了字段需要更新数据库,并且保留原来的数据库数据:
这是原有的数据库表

[java] view plain copy
  1. CREATE_BOOK="createtablebook(bookIdintegerprimarykey,bookNametext);";
然后我们增加一个字段:
[java] view plain copy
  1. CREATE_BOOK="createtablebook(bookIdintegerprimarykey,bookNametext,bookContenttext);";
首先我们需要把原来的数据库表重命名一下
[java] view plain copy
  1. CREATE_TEMP_BOOK="altertablebookrenameto_temp_book";
然后把备份表中的数据copy到新创建的数据库表中
[java] view plain copy
  1. INSERT_DATA="insertintobookselect*,''from_temp_book";(注意''是为新加的字段插入默认值的必须加上,否则就会出错)

然后我们把备份表干掉就行啦。
[java] view plain copy
  1. DROP_BOOK="droptable_temp_book";

然后把数据库的版本后修改一下,再次创建数据库操作对象的时候就会自动更新(注:更新的时候第一个创建的操作数据的对象必须是可写的,也就是通过这个方法getWritableDatabase()获取的数据库操作对象)

然后在onUpgrade()方法中执行上述sql语句就OK

[java] view plain copy
  1. publicclassDBserviceextendsSQLiteOpenHelper{
  2. privateStringCREATE_BOOK="createtablebook(bookIdintegerprimarykey,bookNametext);";
  3. privateStringCREATE_TEMP_BOOK="altertablebookrenameto_temp_book";
  4. privateStringINSERT_DATA="insertintobookselect*,''from_temp_book";
  5. privateStringDROP_BOOK="droptable_temp_book";
  6. publicDBservice(Contextcontext,Stringname,CursorFactoryfactory,intversion){
  7. super(context,name,factory,version);
  8. }
  9. @Override
  10. publicvoidonCreate(SQLiteDatabasedb){
  11. db.execSQL(CREATE_BOOK);
  12. }
  13. @Override
  14. publicvoidonUpgrade(SQLiteDatabasedb,intoldVersion,intnewVersion){
  15. switch(newVersion){
  16. case2:
  17. db.execSQL(CREATE_TEMP_BOOK);
  18. db.execSQL(CREATE_BOOK);
  19. db.execSQL(INSERT_DATA);
  20. db.execSQL(DROP_BOOK);
  21. break;
  22. }
  23. }
  24. }


总结:这次遇到的问题,开始的时候不知道怎么解决,那我们就继续去看那个操蛋的代码吧,这里的源代码其实不难的~~,但是我们要养成看源代码的习惯。

转自:http://blog.csdn.net/jiangwei0910410003/article/details/39670813

更多相关文章

  1. Android使用AttributeSet自定义控件的方法
  2. 让Android不播放关机动画,而是显示一个关机进度条的方法
  3. android下拉菜单spinner的使用方法
  4. Android第五期 - 更新自己的apk本地与网络两种方法
  5. Android HandlerThread使用方法
  6. android-2.2以下杀进程方法:restartPackage();
  7. 【Android 开发】:UI控件之拖动条控件 SeekBar的使用方法
  8. android中的sqlite数据库加密
  9. Android NullPointerException解决方法

随机推荐

  1. Android(安卓)之 五大布局案例
  2. 自定义动画(animation)时set中的各元素效
  3. android dumpsys
  4. Android日记抓取并保存
  5. Android(安卓)控件使用参数集锦
  6. Android控件笔记——在界面中显示及输入
  7. Android(安卓)API中文文档Button
  8. 相对布局相关属性
  9. android中TextView内容过长加省略号
  10. [Android(安卓)博客]收集一些不错的,关于a