Fork me on GitHub Android《第一行代码》简要笔记(二) - 冰路梦 | binglumeng

Android《第一行代码》简要笔记(二)

Posted by 冰路梦 on 2017-06-07

Android《第一行代码》简要笔记 二

第六章、数据存储

IT行业方向众多,编程语言种类繁多,然而程序设计的核心则无外乎算法数据,这两年概念火热的云计算、大数据、机器学习、人工智能等等,原理也就是基于以上两点。

​ Android中数据的存储大致分为五类:1、文件。2、SharedPreferences。3、Sqlite数据库。4、Content Provider。5、网络存储。

6.1、文件存储

文件存储,是基于Java IO技术的一个基本存储方式。Android中提供了openFileOutput()方法用于存储文件到App自身data目录下:/data/data/<package_name>/files/,在操作文件时,不用指定这个路径。

openFileOutput()有两个参数,一就是file文件名,二是操作模式。Android 4.2之前还有三种模式,现在只剩下MODE_PRIVATEMODE_APPEND两种。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void save(){
String str = "Data to save";
FileOutputStream out = null;
BufferedWriter writer = null;
try{
out = openFileOutput("myData",Context.MODE_PRIVATE);
writer = new BufferWriter(new OutputStreamWriter(out));
writer.write(str);
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(writer!=null){
writer.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
}
  • 从文件读取数据

openFileInput()方法,接收一个参数,就是文件名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public String load(){
FileInputStream in = null;
BufferedReader reader = null;
StringBuilder content = new StringBuilder();
try{
in = openFileInput("myData");//上面存储的文件名称
reader = new BufferedReader(new InputStreamReader(in));
String line = "";
while((line = reader.readline())!= null){
content.append(line);
}
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(reader!=null){
reader.close();
}
}catch(IOException e){
e.printStackTrace();
}
}
return content.toString();
}

EditText:将光标移动到指定位置edit.setSelection(int position);

6.2、SharedPreferences

SharedPreferences利用键值对的形式保存轻量型的数据。有三种获取SP对象的方式:

  • Context中getSharedPreferences();

    两个参数,一是sp的名称,存放与/data/data/<package_name>/shared_perfs/下;二是操作模式,先只有MODE_PRIVATE可用,4.2版本之后废弃MODE_WORLD_WRITEABLEMODE_WORLD_READABLE。6.0版本废弃MODE_MULTI_PROCESS模式。

  • Activity中getPreferences();

    类似上面,只不过将文件名默认本包名。

  • PreferenceManager中getDefaultSharedPreferences();

    静态方法,接收context,包名为文件名。

SharedPreferences使用方法,三步骤:

  1. 获取Editor对象SharedPreferences.Editor
  2. editor.putString()等操作。
  3. editor.apply();或commit();

其中commit会立即提交,而apply会等合适时机操作。

从SP读取数据只需要使用SP的对象的getString()之类的方法就可以。

6.3、SQLite数据库

SQLite是一个轻量型的数据库,内部一切字符串形式存储(好象是?)。Android中使用了sqlite并做了很好的封装。

SQLiteOpenHelper是Android提供的操作数据库的帮助类。其包含getReadableDatabase()getWritableDatabase()两个操作实例。

  • getReadableDatabase()

    获得数据库的读写操作,当磁盘空间已满,则返回只读模式的对象。

  • getWritableDatabase()

    获取数据库的读写操作,当磁盘空间已满,则会抛出异常

SQLiteOpenHelper有两个构造函数,代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyDatabaseHelper extends SQLiteOpenHelper {
public static final String CREATE_BOOK = "create table book ( id integer primary key autoincrement,author text,price real,pages integer,name text)";
private Context mContext;
//构造函数
public MyDatabaseHelper(Context context,String name,SQLiteDatabase.CursorFactory factory,int version){
super.(context,name,factory,version);
mContext = context;
}
@Override
public void onCreate(SQLiteDatabase db){
db.exeSQL(CREATE_BOOK);
Toast.makeText(mContext,"Create table book Successed !",Toast.LENGTH_SHORT).show();
}
@Override
public void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion){
//数据库版本升级使用
}
}

在其他调用MyDatabaseHelper

1
2
3
4
5
6
...
//创建helper对象,参数分别是context、数据库名称、Cursor对象、版本号
MyDatabaseHelper helper = new MyDatabaseHelper(this,"BookStore.db",null,1);
...
//获取数据库操作类对象
helper.getWritableDatabase();
  • 升级数据库

数据库创建在onCreate()方法中,只创建一次,若是已经存在,哪怕改代码使得创建语句不同,依然不能创建新的。若是卸载删除,再新建,会使之前数据丢失。所以要使用onUpgrade()方法升级数据库。

1
2
3
4
5
6
7
8
9
10
public class MyDatabaseHelper extends SQLiteOpenHelper {
...
@Override
public void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion){
//升级数据库,先删除原有表,否则创建不了。
db.exeSQL("drop table if exists book");
db.exeSQL("drop table if exists category");
onCreate(db);
}
}

6.4、数据库CRUD

数据库的曾删改查CreateRetrieve查询、更新UpdateDelete删除。SQL: Structured Query Language

  • insert

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    ...
    SQLiteDatabase db = helper.getWritableDatabase();//此处的helper是上面的MyDatabaseHelper对象
    ContentVaules values = new ContentValues();
    //insert the first data
    values.put("name","Head First Java");
    values.put("author","Jim Green");
    ...
    db.insert("book",null,values);//插入数据,第一个参数是表名
    //清空values的数值,才能第二次使用
    values.clear();
    values.put("name","《第一行代码》");
    values.put("author","郭霖");
    ...
    db.insert("book",null,values);
    ...
  • update

    1
    2
    3
    4
    ...
    //参数依次是表名、更新值、更新的列名、对应的数据,也就是选择出这个条件的条目
    db.update("book",values,"name = ? ",new String[]{"Head First Java"});
    ...
  • delete

    1
    2
    3
    4
    ...
    //此语句表示,删除页码大于500页的那本书
    db.delete("book","page > ? ",new String[]{"500"});
    ...
  • search

    数据库最为常用的功能便是查询数据,SQLite提供了query方法,最短的也有7个参数:表名、指定列(默认全部)、约束条件、约束条件值、group by、filter、orderby。返回cursor对象。

    • 查询所有数据

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      ...
      Cursor cursor = db.query("book",null,null,null,null,null,null);
      if(cursor.moveToFirst()){
      do{
      //遍历Cursor对象的数据
      String name = cursor.getString(cursor.getColumnIndex("name"));
      String author = cursor.getString(cursor.getColumnIndex("author"));
      ...
      }while(cursor.moveToNext());
      cursor.close();
      }

    Android同时也提供了原始SQL语句的操作方式:

    1
    2
    3
    4
    5
    6
    db.exeSQL("insert into book(name,author,pages,price) values (?,?,?,?)",new String[]{"Head First Java","Jim Green","435","20.50"});
    db.exeSQL("update book set price = ? where name = ?" ,new String[]{"15.7","Head First Java"});
    //此方法用?站位,避免sql注入危险。
    db.exeSQL("delete from book where pages > ?",new String[]{"500"});
    //注意,查询使用的是rawQuery
    db.rawQuery("select * from book",null);

6.5、LitePal开源框架

Android 开源的ORM(对象关系映射)模式的数据库框架比较多,GreenDAO、xUtils3、LitePal等。本书介绍的是LitePal。

使用步骤:

  1. 在AndroidStudio中配置依赖

    1
    2
    3
    4
    5
    dependencies{
    ...
    compile 'org.litepal.android:core:1.4.1'
    ...
    }
  2. 在assets目录下创建litepal.xml

    1
    2
    3
    4
    5
    6
    <litepal>
    <dbname value="BookStore" ></daname>
    <version value="1"></version>
    <list>
    </list>
    </litepal>
  3. 在AnroidManifest.xml中配置Application

    1
    2
    3
    4
    5
    6
    7
    ...
    <application
    android:name="org.litpal.LitePalApplication"
    ...
    >
    ...
    </application>

    所谓的对象关系映射模式,就是将Java Bean对象,直接映射到数据库中的表结构。

  • Java Bean:
1
2
3
4
5
6
7
8
9
10
public class Book {
private ind id;
private String name;
private String author;
private int pages;
private double price;
//下面就是get、set
...
}
  • 此时在litepal.xml中的list中配置需要映射的Java Bean类
1
2
3
4
...
<list>
<mapping class="com.example.litepaltest.Book"></mapping>
</list>
  • 在Java代码中创建

    1
    2
    3
    4
    ...
    //如此便创建了数据库
    LitePal.getDatabase();
    ...

修改Book的成员属性信息,或者新增一个Cagegory表,只需要在litepal.xml中做修改版本号,以及添加对应类名到list中即可。重新运行上面的getDatabase();

1
2
3
4
5
6
...
<version value="2"></version>
<list>
<mapping class="com.example.litepaltest.Book"></mapping>
<mapping class="com.example.litepaltest.Category"></mapping>
</list>

一、向数据库操作数据才是真正的意义所在,所以Book需要继承DataSupport才可以。

1
2
3
public class Book extends DataSupport{
...
}

此时在Activity之中可以

1
2
3
4
5
6
...
Book book = new Book();
book.setName("Head First Java");
book.setAuthor("Jim Green");
...
book.save();//调用save()方法,即可完成相应表格的初始化。

二、更新

1
2
3
4
5
6
7
8
...
Book book = new Book();
book.setName("Head First Java");
book.setAuthor("Jim Green");
...
book.save();//调用save()方法,即可完成相应表格的初
book.setAuthor("Haha");
book.save();//如此也算更新了,但是这个方法比较笨。

用条件语句:

1
2
3
4
Book book = new Book();
book.setPrice(27.9);
book.setAuthor("Haha");
book.updateAll("name = ?","Head First Java");

//注意的是,updateAll不能用于设置默认值,比如整数型默认为0或0.0,引用型默认null,LitePal统一方法setToDefault()

1
book.setToDefault();//将所有数据设置为默认。

三、删除数据

1
DataSupport.deleteAll(Book.class,"price > ? ","15");

四、查询

1
2
3
4
5
6
7
8
9
10
11
12
List<Book> list = DataSupport.findAll(Book.class);
DataSupport.findFirst(Book.class);
DataSupport.findLast(Book.class);
//根据api的高级用法
DataSupport.select("name","author").find(Book.class);
DataSupport.where("pages > ? ","400").find(Book.class);
DataSupport.order("price desc ").find(Book.class);
DataSupport.limit(3).find(Book.class);
//指定偏移量
DataSupport.limit(3).offset(1).find(Book.class);//从第二条开始查询,三条数据
//LitePal依然支持原始的SQL语句
Cursor cursor = DataSupport.findBySQL("selece * form Book");

第七章、Content Provider

Android中为了保护数据的安全共享,使用了ContentProvider这个组件。Android应用开发涉及权限较多,6.0以后加入了运行时权限,危险类的权限就需要在代码运行时做对应申请和处理。

权限组名 权限名称
CALENDAR READ_CALENDAR、WRITE_CALENDAR
CAMERA CAMERA
CONTACTS READ_CONTACTS、WRITE_CONTACTS、GET_ACCOUNTS
LOCATION ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION
MICROPHONE RECORD_AUDIO
PHONE READ_PHONE_STATE、CALL_PHONE、READ_CALL_LOG、WRITE_CALL_LOG、ADD_VOICEMAIL、USE_SIP、PROCESS_OUTGING_CALLS
SENSORS BODY_SENSORS
SMS SEND_SMS、RECEIVE_SMS、READ_SMS、RECEIVE_WAP_PUSH、RECEIVE_MMS
STORAGE READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE

以上均被视为危险权限,需要申请运行时权限处理。同一组权限,有一个申请了,其他的自动拥有。

1、运行时权限申请

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class MainActivity extends AppCompatActivity {
...
@Override
protected void onCreate(Bundle savedInstanceState){
...
btn.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
//此处,模拟申请拨打电话权限
if(ContextCompat.checkSelfPermission(MainActivity.this,Manifest.permission.CALL_PHONE)!=PackageManager.PERMISSION_GRANTED){
ActivityCompat.requestPermission(MainActivity.this,new String[]{Manifest.permission.CALL_PHONE},1);//此处的1,表示requestCode,可以自定义。
}
}else{
call();
}
});
}
//拨打电话
private void call(){
Intent intent = new Intent(Intent.ACTION_CALL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);
}
//运行时权限的处理
@Override
public void onRequestPermissionResult(int requestCode,String[] permissions,int[] grantResults){
switch(requestCode){
case 1://上面的请求码
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
call();
}else{
Toast.makeText(this,"You denied the permission ", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}
}

2、Content Provider

可以将Content Provider理解为一个封装很好的SQLite数据操作,其接受的参数不是表名,而是一个uri

分为authoritypath两部分。前面在加上协议头,完整的就是协议://authority/path如:

content://com.example.app.provider/table1

使用Uri.parse将如上uri解析为uri对象。读取Provider需要使用ContentResolver

1
2
3
4
5
6
7
Cursor cursor = getContentResolver().query(
uri,//uri
projection,//指定的列名
selection,//where条件
selectionArgs,//占位符对应的实际数据数组
sortOrder//查询结果的排序方式
)

接下来使用cursor遍历数据,添加数据都类似sqlite操作。

  • 读取联系人示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
private void readContacts(){
Cursor cursor = null;
try{
//查询联系人
cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
if(cursor != null){
while(cursor.moveToNext()){
//获取联系人姓名,这里使用了系统提供的联系人Phone类的uri常量
String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
...
}
}
}
}

创建Provider对外共享数据

  1. 继承ContentProvider
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
> public class MyProvider extends ContentProvider {
> public static final int TABLE1_DIR = 0;
> public static final int TABLE1_ITEM = 1;
> public static final int TABLE2_DIR = 2;
> public static final int TABLE2_ITEM = 3;
>
> private static UriMatcher uriMatcher;
>
> static {
> uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
> uriMatcher.addURI("com.example.app.provider","table1",TABLE1_DIR);
> uriMatcher.addURI("com.example.app.provider","table1/#",TABLE1_ITEM);
> uriMatcher.addURI("com.example.app.provider","table2",TABLE2_DIR);
> uriMatcher.addURI("com.example.app.provider","table2/#",TABLE2_ITEM);
> }
> ...
> @Override
> public boolean onCreate(){
> return false;
> }
> @Override
> public Cursor query(Uri uri,String[] projection,String selection,String[] selectionArgs,String sortOrder){
> //根据不同的uri来查询不同
> switch(uriMatcher.match(uri)){
> case TABLE1_DIR:
> //查询表1中所有数据
> break;
> case TABLE1_ITEM:
> //查询表1中单条数据
> break;
> case TABLE2_DIR:
>
> break;
> case TABLE2_ITEM:
>
> break;
> default:
> break;
> }
> return null;
> }
> @Override
> public Uri insert(Uri uri,ContentValues values){
> return null;
> }
> @Override
> public int update(Uri uri,ContentValues values,String selection,String[] selectionArgs){
> return 0;
> }
> @Override
> public int delete(Uri,String selection,String[] selectionArgs){
> return 0;
> }
> //返回的是MIME类型
> @Override
> public String getType(Uri uri){
> switch(uriMatcher.match(uri)){
> case TABLE1_DIR:
> return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1"
> break;
> case TABLE1_ITEM:
> return "vnd.android.cursor.item/vnd.com.example.app.provider.table1"
> break;
> case TABLE2_DIR:
>
> break;
> case TABLE2_ITEM:
>
> break;
> default:
> break;
> }
> return null;
> }
> }
>

>

匹配MIME类型可以用*#分别表示任意长度字符、任意长度的数字。MIME类型有3部分组成:

  • vnd开头
  • 如果是路径结尾,则加上android.cursor.dir/,若是id结尾,加上android.cursor.item/
  • 最后接上vnd.<authority>.<path>

示例:对应content://com.example.app.provider/table1这个URI的MIME类型如下:

1
vnd.android.cursor.dir/vnd.com.example.app.provider.table1

示例:对应content://com.example.app.provider/table1/1这个URI的MIME类型如下:

1
vnd.android.cursor.item/vnd.com.example.app.provider.table1

注意看清,是item和dir的区别

==provider==在AndroidManifest中注册标签,除了android:name外,还有个==android:authorities="com.example.app.provider"==比较重要

1
2
3
4
5
6
<!-- android:authorities 指定的是上面定义的authority-->
<provider androd:name=".MyProvider"
android:authorities="com.example.app.provider"
android:enable="true"
android:exported="true">
</provider>
  • Git指令
1
2
3
4
5
git status
git diff file1 file2
git checkout file # 撤销未提交,没有add的修改
git reset HEAD file # 回退已经add提交的修改
git log