Android之数据存储

前言

数据存储,说实话,从学习C的C#都没有注意过,一直到用unity做游戏的时候,突然意识到,我的数据都是临时的,我怎么把数据写到硬盘里,下次用的时候再去取呢?好吧,实在有些惭愧。这次学Android,终于和数据存储这个话题,面对面了。

说到数据存储不得不说的是I/O流,开始看Android书,看到Android的三种数据存储时,一下子就蒙了。为此还特地去恶补了一下java(话说,C#里也有io流的,我怎么就没注意过呢?该会的终究还是躲不过啊~);顺便还把数据库复习了一下,好像以前学的全都忘了,呀呀哎呀~~~

看了书,数据存储算是入门了吧,我也懒得总结各个知识点了。现在觉着吧,那种把书上的东西总结到博客上的做法没啥用。也是最近学习的一些感悟吧。还是得敲代码!下面就分别用Android的文件存储、SharedPreferences存储以及数据库存储实现一个可以记录用户名的登陆界面。

正文

先把布局文件写好

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".MainActivity">



<TextView
android:id="@+id/username_txt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBaseline="@+id/edit_username"
android:text="用户名:"
android:textSize="25sp"/>

<EditText
android:id="@+id/edit_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@+id/username_txt"
android:hint="请输入您的账号"/>

<TextView
android:id="@+id/password_txt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/username_txt"
android:layout_alignBaseline="@+id/edit_password"
android:text="密码:"
android:textSize="25sp"/>

<EditText
android:id="@+id/edit_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/edit_username"
android:layout_toRightOf="@+id/username_txt"
android:hint="请输入您的密码"
android:password="true"/>

<CheckBox
android:id="@+id/isRememberPassword"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/edit_password"
android:layout_centerHorizontal="true"
android:text="记住密码"/>

<Button
android:id="@+id/btn_login"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_below="@+id/isRememberPassword"
android:layout_centerHorizontal="true"
android:text="登陆"
android:textSize="25sp"/>

</RelativeLayout>

布局没啥好说,就是一个登陆界面。

文件存储

这里的文件存储用的是java里的IO流。

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package com.demoniaccube.chobits.login;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;

public class MainActivity extends AppCompatActivity
{

private Button btn_login;
private EditText edit_username;
private EditText edit_password;
private CheckBox isRemember;
String username = "";
String password = "";

@Override
protected void onCreate(Bundle savedInstanceState)
{

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btn_login = (Button)findViewById(R.id.btn_login);
edit_username = (EditText)findViewById(R.id.edit_username);
edit_password = (EditText)findViewById(R.id.edit_password);
isRemember = (CheckBox)findViewById(R.id.isRememberPassword);

String saveText = load();
if (!TextUtils.isEmpty(saveText))//TextUtils.isEmpty()方法可以一次性进行两种空值的判断。当传入的字符串等于null或等于空字符串的时候,都会返回true。
{
String s[] = saveText.split(";");//注释①
edit_username.setText(s[0]);
if (isRemember.isChecked())
{
edit_password.setText(s[1]);
}
// else
// {
// edit_password.setText("");
// }
}
btn_login.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{

//我把username的赋值放在button监听之前为什么会出现错误呢?//注释④
username = edit_username.getText().toString();
password = edit_password.getText().toString();
if (username.equals("aaa") && password.equals("123"))
{
Toast.makeText(MainActivity.this, "登陆成功", Toast.LENGTH_SHORT).show();
} else
{
Toast.makeText(MainActivity.this, "密码或账号错误,请重新输入", Toast.LENGTH_SHORT).show();
}
}
});
}

@Override
protected void onDestroy()
{

super.onDestroy();
String str = username + ";" + password;//这里用一个“;”作为一个分隔符用于取数据
Save(str);
}

public void Save(String str)
{

FileOutputStream fileOutputStream = null;
PrintWriter printWriter = null;//注意这里printWriter和bufferedWriter的区别。注释②
try
{
fileOutputStream = openFileOutput("user", MODE_PRIVATE);
printWriter = new PrintWriter(fileOutputStream);
printWriter.write(str);
}
catch(Exception e){}
finally
{
try
{
if (printWriter != null)
{
printWriter.close();
}
}
catch (Exception ee){}
}
}


public String load()
{

FileInputStream fileInputStream = null;
BufferedReader bufferedReader = null;
String saveData = "";
try
{
fileInputStream = openFileInput("user");
bufferedReader = new BufferedReader(new InputStreamReader(fileInputStream));
while (bufferedReader.ready())//这里不用while试了一下,并不可以
{
saveData = bufferedReader.readLine();
}
//注释③

}
catch (Exception e){}
finally
{
try
{
if (bufferedReader != null)
{
bufferedReader.close();
}
}
catch(Exception ee){}
}
return saveData;
}

}

注释①

split用法的一个示例:利用分隔符分割字符串
String s = new String("01:大汽车");
String a[] = s.split(":");
System.out.println(a[0]);
System.out.println(a[1]);

注释②
PrintWriter和BufferedWriter都是继承java.io.Writer,所以很多功能都一样。
不过PrintWriter提供println()方法可以写不同平台的换行符,而BufferedWriter可以任意设定缓冲大小。
OutputStream可以直接传给PrintWriter(BufferedWriter不能接收),如:这里是
printWriter = new PrintWriter(fileOutputStream);就是直接将fileOutputStream传进来了。
而如果想用bufferedWriter,那么就得先将字节流转化为字符流:
bufferedWriter = new BufferedWriter(new OutputStreamWriter(fileOutputStream));

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 void Save(String str)
{

FileOutputStream fileOutputStream = null;
BufferedWriter bufferedWriter = null;
try
{
fileOutputStream = openFileOutput("user", MODE_PRIVATE);
bufferedWriter = new BufferedWriter(new OutputStreamWriter(fileOutputStream));
bufferedWriter.write(str);
}
catch (Exception e){}
finally
{
try
{
if (bufferedWriter != null)
{
bufferedWriter.close();//关闭缓冲区
}
}
catch (Exception ee){}
}

}

注释③
还可以像下面这样写。
(注释部分这样写是有问题的用这种方法试了一下,并不可以)书上并不是用saveData这个String,而是用了StringBuilder将文本都存在了这里,判断没变,只是存的时候用的stringBuilder.append(saveData)

1
2
3
4
5
6
StringBuilder stringBuilder = new StringBuilder();
while ((saveData = bufferedReader.readLine()) != null)
{
//saveData = bufferedReader.readLine();
stringBuilder.append(saveData);
}

于是在网上查了一下两者区别,不是很明白,先记下吧。

ready是查看流是否已经准备好被读,是一个非阻塞的方法,所以会立刻返回,由于服务器没有准备好被读,所以会立刻返回,所以读取到的都是null,那么我们用while((str = reader.readLine()) != null)进行读取呢,readLine是一个阻塞的方法,只要没有断开连接,就会一直等待,直到有东西返回,那么什么时候返回空呢,只有读到数据流最末尾,才返回null ,举例如,一般只有在读到文件末尾时才会是空,至于读取服务器端的数据,一般不会是空,那么有时候我们采用特定的字符来代表结束,在网络上,一直等待输入,即使是对方什么也不输入,只是回车,readline也不会返回null,如果I/O中断,会返回I/O异常,还是不会返回null 除非你使用的数据流有固定长度(比如文件数据流,或者ByteArrayInputStream之类),而不是网络数据流(阻塞模式)。 在I/O阻塞模式下,你写的条件循环,是不合理的: while((line = in.readLine()) != null)
应该如下: while(网络状态)//状态可由从客户端读入数据时捕捉I/O异常来获取,所以如果要编写多用户服务程序,最好使用java.nio,他对这方面的处理会比单纯的阻塞IO会更好。

同理,InputStream的available也是非阻塞的,在网络中,很可能流延迟传输(而不是到达不了,服务器端很容易使用这样的代码,而是应用发生错误:
byte[] bytes = new byte[in.available()];
in.read(bytes);
结果是错误的,因为available返回的是0
那么解决方法有很多,比如可以一个一个字节的读,或者一个缓存大小字节的读,每次读都是阻塞方式的。所以可以放心

注释④
做了一个简单实验

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
    @Override
protected void onCreate(Bundle savedInstanceState)
{

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edit_username = (EditText)findViewById(R.id.edit_username);
btn_login = (Button)findViewById(R.id.btn_login);


edit_username.setText("111");
final String sss = edit_username.getText().toString();//编译的时候会自动加上final

btn_login.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{

// System.out.println(edit_username.getText());
Log.d("xxx", edit_username.getText().toString());
Log.d("xxx", sss);
}
});
}
//运行后手动将值改为222;结果是:111;222
//可以看到一旦赋值后,值就不会变了!相当于把一开始的值保存了下来,然后判断这个保存的值,sb!

从上面可以看出如果把username放在了外面,那么edit_username.getText()将开始的空值放在了username里,此时username的值为空,那么下面判断username根本没有意义,因为还是空值。
所以,要么把username的定义赋值放在button监听中,要么不要定义username,直接以edit_username.getText()作为判断依据。但是这里需要多次用到username,所以在button监听里赋值一次最好。

其实这里还可以看出Android在OnCreate()里的代码也是可以实时监听的,因为我手动修改Edit控件,在OnCreate()里也可以打印出来。这和unity不一样。unity有明确的函数执行顺序,只有Update类的函数才可以实时运行监听。

总结

从代码可以看出文件存储比较麻烦,而且文件存储并不关心数据类型,我这里都是按字符来处理的。比如我想用布尔类型的时候,就需要转化,很是蛋疼。于是记住密码的那个控件没有用。所以这里实现的功能仅仅是记住了账号,用户登录时,还得自己填写密码。

SharedPreferences存储

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
package com.demoniaccube.chobits.login;

import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

public class MainActivity extends Activity
{

private EditText edit_username;
private EditText edit_password;
private Button btn_login;
private CheckBox cb_remember;
SharedPreferences sp = null;
SharedPreferences.Editor editor = null;

@Override
protected void onCreate(Bundle savedInstanceState)
{

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
edit_username = (EditText)findViewById(R.id.edit_username);
edit_password = (EditText)findViewById(R.id.edit_password);
btn_login = (Button)findViewById(R.id.btn_login);
cb_remember = (CheckBox)findViewById(R.id.isRememberPassword);
sp = getSharedPreferences("data", MODE_PRIVATE);
boolean isRemember = sp.getBoolean("isRem", false);
if (isRemember)
{
String username = sp.getString("user", "");
String password = sp.getString("pass", "");
edit_username.setText(username);
edit_password.setText(password);
cb_remember.setChecked(true);
}

btn_login.setOnClickListener(new View.OnClickListener()
{
@Override
public void onClick(View v)
{

String username = edit_username.getText().toString();
String password = edit_password.getText().toString();
if (username.equals("aaa") && password.equals("111"))
{
editor = sp.edit();
if (cb_remember.isChecked())
{
editor.putString("user", username);
editor.putBoolean("isRem", true);
editor.putString("pass", password);
}
else
{
editor.clear();
}
editor.commit();
Toast.makeText(MainActivity.this, "登陆成功", Toast.LENGTH_SHORT).show();
} else
{
Toast.makeText(MainActivity.this, "登陆失败", Toast.LENGTH_SHORT).show();
}
}
});
}


}

SharedPreferences存储分三步走:
一、创建SharedPreferences和SharedPreferences.Editor两个类;
二、用SharedPreferences.Editor的putXXX(“键值”,输入的值)方法来存数据;存完后要用commit()方法来提交;
三、用SharedPreferences的getXXX(“键值”,默认值)方法来取数据;

后记

数据库存储内容太多了,放在这里就有点累赘了。有时间单独写一个SQList的记录吧。
学习了一个多月的Android,接触了很多概念和类。关于UI,数据存储,线程,四大组件等等都接触了一些。现在对于Android感觉都会一点,但是写的时候又有些心有余而力不足。这只能说明一点,看的还不够细致,理解的不够透彻,练的不够多。
本来学习Android的初衷是想多了解移动端,这样做移动端游戏的时候,不至于完全不懂Android的知识。其次是,万一unity的工作找不到,还可以转战Android。不过一个月的学习,也找到了很多不足,比如数据库方面、java方面、数据存储、网络方面。感觉很多技术都是相通的。计划现在到过年的这段时间,Android和unity的时间平均分吧。每天练一点Android,复习复习unity以前记过的笔记和写过的代码。到二月低的目标是做一个关于天气的app;unity方面,复习以前的知识+学习一下unity的数据存储、网络、渲染等以前没涉及到的知识+开始着手做VR的游戏。