Android实例剖析笔记(四)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介:   NoteEditor深入分析  首先来弄清楚“日志编辑“的状态转换,通过上篇文章的方法来做下面这样一个实验,首先进入“日志编辑“时会触发onCreate和onResume,然后用户通过Option Menu选择”Edit title”后,会触发onSaveInstanceState和onPause,最后,用户回到编辑界面,则再次触发onResume。

  NoteEditor深入分析

  首先来弄清楚“日志编辑“的状态转换,通过上篇文章的方法来做下面这样一个实验,首先进入“日志编辑“时会触发onCreate和onResume,然后用户通过Option Menu选择”Edit title”后,会触发onSaveInstanceState和onPause,最后,用户回到编辑界面,则再次触发onResume。

  最终通过LogCat可以得到下图

  那么下面就按照上述顺序对此类进行剖析。首先是onCreate方法,一开始先获取导致进入“日志编辑”界面的intent,分析其操作类型可得知是“编辑日志”还是“新增日志”。

        final  Intent intent  =  getIntent();
        
//  Do some setup based on the action being performed.
         final  String action  =  intent.getAction();

若是编辑日志,则设置当前状态为编辑,并保存待编辑日志的URI.

             mState  =  STATE_EDIT;
            mUri 
=  intent.getData();

  若是“新增日志”,则设置当前状态为“新增”,并通过content provider向数据库中新增一个“空白日志”,后者返回“空白日志”的URI.

          mState  =  STATE_INSERT;
            mUri 
=  getContentResolver().insert(intent.getData(),  null );

  然后不管是编辑新增,都需要从数据库中读取日志信息(当然,若是新增,读出来的肯定是空数据)。

mCursor  =  managedQuery(mUri, PROJECTION,  null null null );

  最后,类似于web应用中使用的Session,这里也将日志文本保存在InstanceState中,因此,若此activity的实例此前是处于stop状态,则我们可以从它那取出它原本的文本数据.

         if  (savedInstanceState  !=   null
        {
            mOriginalContent 
=  savedInstanceState.getString(ORIGINAL_CONTENT);
        }

  第二个来分析onResume函数,首先把游标置于第一行(也只有一行)

            mCursor.moveToFirst();

  然后取出“正文”字段,这时有一个比较有趣的技巧,“设置文本”并不是调用setText,而是调用的setTextKeepState,后者相对于前者有一个优点,就是当界面此前stop掉,现在重新resume回来,那么此前光标所在位置仍然得以保存。而若使用setText,则光标会重置到行首。

             String note  =  mCursor.getString(COLUMN_INDEX_NOTE);
            mText.setTextKeepState(note);

最后,将当前编辑的正文保存到一个字符串变量中,用于当activity被暂停时使用。

             if  (mOriginalContent  ==   null
            {
                mOriginalContent 
=  note;
            }

  通过前面的图可以得知,activity被暂停时,首先调用的是onSaveInstanceState函数。

outState.putString(ORIGINAL_CONTENT, mOriginalContent);

  这里就仅仅将当前正编辑的正文保存到InstanceState中(类似于Session)。最后来看onPause函数,这里首先要考虑的是若activity正要关闭,并且编辑区没有正文,则将此日志删除。

             if  (isFinishing()  &&  (length  ==   0 &&   ! mNoteOnly) 
            {
                setResult(RESULT_CANCELED);
                deleteNote();
            } 

  否则的话,就更新日志信息

                ContentValues values  =   new  ContentValues();
                
if  ( ! mNoteOnly) 
                {
                    values.put(Notes.MODIFIED_DATE, System.currentTimeMillis());
                    
if  (mState  ==  STATE_INSERT)
                    {
                        String title 
=  text.substring( 0 , Math.min( 30 , length));
                        
if  (length  >   30
                        {
                            
int  lastSpace  =  title.lastIndexOf( '   ' );
                            
if  (lastSpace  >   0
                            {
                                title 
=  title.substring( 0 , lastSpace);
                            }
                        }
                        values.put(Notes.TITLE, title);
                    }
                }
                values.put(Notes.NOTE, text);
                getContentResolver().update(mUri, values, 
null null );
            }
        }

  在生成Option Menu的函数onCreateOptionsMenu中,我们再一次看到下面这段熟悉的代码了:

Intent intent  =   new  Intent( null , getIntent().getData());
intent.addCategory(Intent.CATEGORY_ALTERNATIVE);
menu.addIntentOptions(Menu.CATEGORY_ALTERNATIVE, 
0 0 ,
new  ComponentName( this , NoteEditor. class ),  null , intent,  0 null );

  这种生成动态菜单的机制在Android实例剖析笔记(二)这篇文章中已经介绍过了,就不赘述了。最后,来看下放弃日志和删除日志的实现,由于还没有接触到底层的content provider,这里都是通过getContentResolver()提供的update,delete,insert来向底层的content provider发出请求,由后者完成实际的数据库操作。

     private   final   void  cancelNote() 
    {
        
if  (mCursor  !=   null )
        {
            
if  (mState  ==  STATE_EDIT) 
            {
                
//  Put the original note text back into the database
                mCursor.close();
                mCursor 
=   null ;
                ContentValues values 
=   new  ContentValues();
                values.put(Notes.NOTE, mOriginalContent);
                getContentResolver().update(mUri, values, 
null null );
            }
            
else   if  (mState  ==  STATE_INSERT) 
            {
                
//  We inserted an empty note, make sure to delete it
                deleteNote();
            }
        }
        setResult(RESULT_CANCELED);
        finish();
    }
    
private   final   void  deleteNote() 
    {
        
if  (mCursor  !=   null
        {
            mCursor.close();
            mCursor 
=   null ;
            getContentResolver().delete(mUri, 
null null );
            mText.setText(
"" );
        }
    }

  剖析NotePadProvider

      NotePadProvider就是所谓的content provider,它继承自android.content.ContentProvider,也是负责数据库层的核心类,主要提供五个功能:

  1)查询数据

  2)修改数据

  3)添加数据

  4)删除数据

  5)返回数据类型

  这五个功能分别对应下述五个可以重载的方法:

public   int  delete(Uri uri, String selection, String[] selectionArgs) 
{
       
return   0 ;
}
public  String getType(Uri uri) 
{
       
return   null ;
}
public  Uri insert(Uri uri, ContentValues values) 
{
       
return   null ;
}
public   boolean  onCreate() 
{
       
return   false ;
}
public  Cursor query(Uri uri, String[] projection, String selection,
           String[] selectionArgs, String sortOrder)
{
       
return   null ;
}
public   int  update(Uri uri, ContentValues values, String selection,
           String[] selectionArgs) 
{
       
return   0 ;
}

  这些都要你自己实现,不同的实现就是对应不同的content-provider。但是activity使用content-provider不是直接创建一个对象,然后调用这些具体方法。

而是调用managedQuery,getContentResolver().delete,update等来实现,这些函数其实是先找到符合条件的content-provider,然后再调用具体content-provider的函数来实现,那又是怎么找到content-provider,就是通过uri中的authority来找到content-provider,这些都是通过系统完成,应用程序不用操心,这样就达到了有效地隔离应用和内容提供者的具体实现的目的。

  有了以上初步知识后,我们来看NotePadProvider是如何为上层提供数据库层支持的。下面这三个字段指明了数据库名称,数据库版本,数据表名称。

private   static   final  String DATABASE_NAME  =   " note_pad.db " ;
private   static   final   int  DATABASE_VERSION  =   2 ;
private   static   final  String NOTES_TABLE_NAME  =   " notes " ;

      实际的数据库操作其实都是通过一个私有静态类DatabaseHelper实现的,其构造函数负责创建指定名称和版本的数据库,onCreate函数则创建指定名称和各个数据域的数据表(就是简单的建表SQL语句)。onUpgrade负责删除数据表,再重新建表。

private   static   class  DatabaseHelper  extends  SQLiteOpenHelper 
    {
        DatabaseHelper(Context context) 
        {
            
super (context, DATABASE_NAME,  null , DATABASE_VERSION);
        }
        @Override
        
public   void  onCreate(SQLiteDatabase db) 
        {
            db.execSQL(
" CREATE TABLE  "   +  NOTES_TABLE_NAME  +   "  ( "
                    
+  Notes._ID  +   "  INTEGER PRIMARY KEY, "
                    
+  Notes.TITLE  +   "  TEXT, "
                    
+  Notes.NOTE  +   "  TEXT, "
                    
+  Notes.CREATED_DATE  +   "  INTEGER, "
                    
+  Notes.MODIFIED_DATE  +   "  INTEGER "
                    
+   " ); " );
        }
        @Override
        
public   void  onUpgrade(SQLiteDatabase db,  int  oldVersion,  int  newVersion) 
        {
            Log.w(TAG, 
" Upgrading database from version  "   +  oldVersion  +   "  to  "
                    
+  newVersion  +   " , which will destroy all old data " );
            db.execSQL(
" DROP TABLE IF EXISTS notes " );
            onCreate(db);
        }
}

Android实例剖析笔记(一)这篇文章中我们已经见识到了getType函数的用处了,也正是通过它的解析,才能区分开到底是对全部日志还是对某一条日志进行操作。

public  String getType(Uri uri) 
{
        
switch  (sUriMatcher.match(uri))
{
        
case  NOTES:
            
return  Notes.CONTENT_TYPE;
        
case  NOTE_ID:
            
return  Notes.CONTENT_ITEM_TYPE;
        
default :
            
throw   new  IllegalArgumentException( " Unknown URI  "   +  uri);
        }
}

  上面的sUriMatcher.match是用来检测uri是否能够被处理,而sUriMatcher.match(uri)返回值其实是由下述语句决定的。

        sUriMatcher  =   new  UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(NotePad.AUTHORITY, 
" notes " , NOTES);
        sUriMatcher.addURI(NotePad.AUTHORITY, 
" notes/# " , NOTE_ID);

sNotesProjectionMap这个私有字段是用来在上层应用使用的字段和底层数据库字段之间建立映射关系的,当然,这个程序里两处对应的字段都是一样(但并不需要一样)。

private   static  HashMap < String, String >  sNotesProjectionMap;
    
static  
    {
        sNotesProjectionMap 
=   new  HashMap < String, String > ();
        sNotesProjectionMap.put(Notes._ID, Notes._ID);
        sNotesProjectionMap.put(Notes.TITLE, Notes.TITLE);
        sNotesProjectionMap.put(Notes.NOTE, Notes.NOTE);
        sNotesProjectionMap.put(Notes.CREATED_DATE, Notes.CREATED_DATE);
        sNotesProjectionMap.put(Notes.MODIFIED_DATE, Notes.MODIFIED_DATE);
}

  数据库的增,删,改,查操作基本都一样,具体可以参考官方文档,这里就仅仅以删除为例进行说明。一般可以分为三步来完成,首先打开数据库

        SQLiteDatabase db  =  mOpenHelper.getWritableDatabase();

  然后根据URI指向的是日志列表还是某一篇日志,到数据库中执行删除动作

     switch  (sUriMatcher.match(uri)) {
        
case  NOTES:
            count 
=  db.delete(NOTES_TABLE_NAME, where, whereArgs);
            
break ;
        
case  NOTE_ID:
            String noteId 
=  uri.getPathSegments().get( 1 );
            count 
=  db.delete(NOTES_TABLE_NAME, Notes._ID  +   " = "   +  noteId
                    
+  ( ! TextUtils.isEmpty(where)  ?   "  AND ( "   +  where  +   ' ) '  :  "" ), whereArgs);
            
break ;
        }

  最后,一定记得通知上层:其传递下来的URI在底层数据库中已经发生了变化。

        getContext().getContentResolver().notifyChange(uri,  null );

  对NotePad的改进

  首先我想指出NotePad的一个bug,其实这个小bug在2月份就有人向官方报告了,参见http://code.google.com/p/android/issues/detail?id=1909。NoteEditor类中的变量mNoteOnly根本就是没有用处的,因为它始终都是false,没有任何变化,所以可以删除掉。第二点是在NoteEditor类中,有下面这样的语句:

setResult(RESULT_OK, ( new  Intent()).setAction(mUri.toString()));
setResult(RESULT_CANCELED);

  可到底想展示什么技术呢?实际上并没有完整展现出来,这里我对其进行修改后来指明。参见http://code.google.com/p/android/issues/detail?id=1671)。首先在NotesList类中增加一个变量

private   static   final   int  REQUEST_INSERT  =   100 ; // 请求插入标识符

  然后修改onOptionsItemSelected函数如下:

   @Override
    
public   boolean  onOptionsItemSelected(MenuItem item) 
    {
        
switch  (item.getItemId())
        {
        
case  MENU_ITEM_INSERT:
            
this .startActivityForResult( new  Intent(Intent.ACTION_INSERT, getIntent().getData()), REQUEST_INSERT);
            
return   true ;
        }
        
return   super .onOptionsItemSelected(item);
    }

     最后重载onActivityResult函数来处理接收到的activity result。

     protected   void  onActivityResult( int  requestCode,  int  resultCode, Intent data)
    {
        
if (requestCode  ==  REQUEST_INSERT)
        {
            
if (resultCode == RESULT_OK)
            {
                Log.d(TAG, 
" OK!!! " );
            }
            
else   if (resultCode == RESULT_CANCELED)
            {
                Log.d(TAG, 
" CANCELED!!! " );
            }
        }
    }

  试试,当你在NoteEditor中保存或放弃日志时,观察LogCat,你可以看到下面这样的画面:

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
1月前
|
Web App开发 安全 程序员
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
多年的互联网寒冬在今年尤为凛冽,坚守安卓开发愈发不易。面对是否转行或学习新技术的迷茫,安卓程序员可从三个方向进阶:1)钻研谷歌新技术,如Kotlin、Flutter、Jetpack等;2)拓展新功能应用,掌握Socket、OpenGL、WebRTC等专业领域技能;3)结合其他行业,如汽车、游戏、安全等,拓宽职业道路。这三个方向各有学习难度和保饭碗指数,助你在安卓开发领域持续成长。
60 1
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
|
22天前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
73 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
6月前
|
Android开发
Android应用实例(一)之---有道辞典VZ.0
Android应用实例(一)之---有道辞典VZ.0
42 2
|
3月前
|
JavaScript 前端开发 Java
FFmpeg开发笔记(四十七)寒冬下安卓程序员的几个技术转型发展方向
IT寒冬使APP开发门槛提升,安卓程序员需转型。选项包括:深化Android开发,跟进Google新技术如Kotlin、Jetpack、Flutter及Compose;研究Android底层框架,掌握AOSP;转型Java后端开发,学习Spring Boot等框架;拓展大前端技能,掌握JavaScript、Node.js、Vue.js及特定框架如微信小程序、HarmonyOS;或转向C/C++底层开发,通过音视频项目如FFmpeg积累经验。每条路径都有相应的书籍和技术栈推荐,助你顺利过渡。
59 3
FFmpeg开发笔记(四十七)寒冬下安卓程序员的几个技术转型发展方向
|
3月前
|
编解码 安全 Ubuntu
Android Selinux 问题处理笔记
这篇文章是关于处理Android系统中SELinux权限问题的笔记,介绍了如何通过分析SELinux拒绝的日志、修改SELinux策略文件,并重新编译部署来解决权限问题,同时提供了一些SELinux的背景知识和实用工具。
60 0
|
4月前
|
API Android开发
Android 监听Notification 被清除实例代码
Android 监听Notification 被清除实例代码
|
5月前
|
安全 Java Android开发
使用Unidbg进行安卓逆向实例讲解
使用Unidbg进行安卓逆向实例讲解
128 2
|
6月前
|
安全 Linux Android开发
FFmpeg开发笔记(十六)Linux交叉编译Android的OpenSSL库
该文介绍了如何在Linux服务器上交叉编译Android的FFmpeg库以支持HTTPS视频播放。首先,从GitHub下载openssl源码,解压后通过编译脚本`build_openssl.sh`生成64位静态库。接着,更新环境变量加载openssl,并编辑FFmpeg配置脚本`config_ffmpeg_openssl.sh`启用openssl支持。然后,编译安装FFmpeg。最后,将编译好的库文件导入App工程的相应目录,修改视频链接为HTTPS,App即可播放HTTPS在线视频。
107 3
FFmpeg开发笔记(十六)Linux交叉编译Android的OpenSSL库
|
5月前
|
Java API Android开发
技术经验分享:Android源码笔记——Camera系统架构
技术经验分享:Android源码笔记——Camera系统架构
54 0
|
6月前
|
Java 测试技术 开发工具
Android 笔记:AndroidTrain , Lint , build(1),只需一篇文章吃透Android多线程技术
Android 笔记:AndroidTrain , Lint , build(1),只需一篇文章吃透Android多线程技术
下一篇
无影云桌面