我的个人博客

哪有什么胜利可言,挺着就代表着一切


  • 首页

  • 归档

SQLite的使用和实现一个简单的数据库框架

发表于 2019-06-11 | 分类于 Android

SQLite的基本知识

SQLite常用的数据类型

字段 作用
char(n) 固定n长度的字串
varchar(n) 长度不固定的字符串,n表示最大的长度
nchar(n) 同char,不同的是可以用来存储中文
nvarchar(n) 同varchar,不同的是可以用来存储中文
text 存储文本
blob 存储二进制文件
int 整形
integer 整形
bigint 整形
float 单精度类型
double 双精度浮点

这里int、integer、bigint的具体区别,还没弄明白。如果有哪个大佬了解,请在评论区指导一下😄😄

创建和删除数据表

创建表的为语法:

1
2
3
4
5
6
7
create table database_name.table_name(
column1 datatype primary key(one or more columns),
column2 datatype,
column3 datatype,
.....
columnN datatype,
);

删除数据表的语法为

1
drop table database_name.table_name;

插入数据

插入数据表的语法为:

1
2
3
4
5
insert into table_name [(column1, column2, column3,...columnN)]  
values (value1, value2, value3,...valueN);
或
//注意:这种方式要确保值的顺序与列在表中的顺序一致
insert into table_name values (value1,value2,value3,...valueN);

删除数据

删除数据的语法为:

1
delete from table_name [条件];

如果没有删除数据的条件,默认删除所有数据;如果指定了条件,则删除符合条件的
数据

更新数据

语法为:

1
2
update table_name
set column1 = value1, column2 = value2...., columnN = valueN [条件];

查询数据

语法为:

1
2
3
4
5
//查询指定字段(列)的值
SELECT column1, column2, columnN FROM table_name;
或
//查询所有字段的值
SELECT * FROM table_name;

SQLite的逻辑运算符

运算符 描述
AND AND 运算符允许在一个 SQL 语句的 WHERE 子句中的多个条件的存在
BETWEEN BETWEEN 运算符用于在给定最小值和最大值范围内的一系列值中搜索值
EXISTS EXISTS 运算符用于在满足一定条件的指定表中搜索行的存在
IN IN 运算符用于把某个值与一系列指定列表的值进行比较
NOT IN IN 运算符的对立面,用于把某个值与不在一系列指定列表的值进行比较
LIKE LIKE 运算符用于把某个值与使用通配符运算符的相似值进行比较
GLOB GLOB 运算符用于把某个值与使用通配符运算符的相似值进行比较。GLOB 与 LIKE 不同之处在于,它是大小写敏感的
NOT NOT 运算符是所用的逻辑运算符的对立面。比如 NOT EXISTS、NOT BETWEEN、NOT IN,等等。它是否定运算符
OR OR 运算符用于结合一个 SQL 语句的 WHERE 子句中的多个条件
IS NULL NULL 运算符用于把某个值与 NULL 值进行比较
IS IS 运算符与 = 相似
IS NOT IS NOT 运算符与 != 相似
` ` 连接两个不同的字符串,得到一个新的字符串
UNIQUE UNIQUE 运算符搜索指定表中的每一行,确保唯一性(无重复)

where

where用来过滤数据的,例如select * from employee where salary >= 65000;是指查询工资高于65000的员工的数据,使用where salary >= 65000;来过滤数据

and/or

and相当于逻辑与运算,只有条件全为真时,结果才为真;or相当于逻辑或运算,只要其中一个条件为真,结果就为真。

LIKE

LIKE运算符是用来匹配通配符指定模式的文本值。如果搜索表达式与模式表达式匹配,LIKE 运算符将返回真(true),也就是 1。这里有两个通配符与 LIKE 运算符一起使用:

  • 百分号 (%)
  • 下划线 (_)

百分号(%)代表零个、一个或多个数字或字符。下划线(_)代表一个单一的数字或字符。这些符号可以被组合使用。

下面一些实例演示了 带有 ‘%’ 和 ‘_’ 运算符的 LIKE 子句不同的地方:
|语句|描述|
|—-|—-|
|WHERE SALARY LIKE ‘200%’|查找以 200 开头的任意值|
|WHERE SALARY LIKE ‘%200%’|查找任意位置包含 200 的任意值|
|WHERE SALARY LIKE ‘00%’|查找第二位和第三位为 00 的任意值|
|WHERE SALARY LIKE ‘2
%_%’|查找以 2 开头,且长度至少为 3 个字符的任意值|
|WHERE SALARY LIKE ‘%2’|查找以 2 结尾的任意值|
|WHERE SALARY LIKE ‘_2%3’|查找第二位为 2,且以 3 结尾的任意值|
|WHERE SALARY LIKE ‘2___3’|查找长度为 5 位数,且以 2 开头以 3 结尾的任意值|

GLOB

GLOB 运算符是用来匹配通配符指定模式的文本值。如果搜索表达式与模式表达式匹配,GLOB 运算符将返回真(true),也就是 1。与 LIKE 运算符不同的是,GLOB 是大小写敏感的,对于下面的通配符,它遵循 UNIX 的语法。

  • 星号 (*)
  • 问号 (?)

星号(*)代表零个、一个或多个数字或字符。问号(?)代表一个单一的数字或字符。这些符号可以被组合使用。

语句 描述
WHERE SALARY GLOB ‘200*’ 查找以 200 开头的任意值
WHERE SALARY GLOB ‘200‘ 查找任意位置包含 200 的任意值
WHERE SALARY GLOB ‘?00*’ 查找第二位和第三位为 00 的任意值
WHERE SALARY GLOB ‘2??’ 查找以 2 开头,且长度至少为 3 个字符的任意值
WHERE SALARY GLOB ‘*2’ 查找以 2 结尾的任意值
WHERE SALARY GLOB ‘?2*3’ 查找第二位为 2,且以 3 结尾的任意值
WHERE SALARY GLOB ‘2???3’ 查找长度为 5 位数,且以 2 开头以 3 结尾的任意值

LIMIT

子句用于限制由 SELECT 语句返回的数据数量

ORDER BY

子句是用来基于一个或多个列按升序或降序顺序排列数据。

ORDER BY 子句的基本语法如下:

1
2
3
4
SELECT column-list 
FROM table_name
[WHERE condition]
[ORDER BY column1, column2, .. columnN] [ASC | DESC];//ASC升序排序,DESC降序排序

GROUP BY

子句用于与 SELECT 语句一起使用,来对相同的数据进行分组。
在 SELECT 语句中,GROUP BY 子句放在 WHERE 子句之后,放在 ORDER BY 子句之前

HAVING

子句允许指定条件来过滤将出现在最终结果中的分组结果。
WHERE 子句在所选列上设置条件,而 HAVING 子句则在由 GROUP BY 子句创建的分组上设置条件。

DISTINCT

关键字与 SELECT 语句一起使用,来消除所有重复的记录,并只获取唯一一次记录。
有可能出现一种情况,在一个表中有多个重复的记录。当提取这样的记录时,DISTINCT 关键字就显得特别有意义,它只获取唯一一次记录,而不是获取重复记录。

1
select distinct name from company;

创建数据库

操作数据库,要使用SQLiteOpenHelper,由于SQLiteOpenHelper是抽象类,使用要实现它,并重写它的 onCreate(), onUpgrade()方法

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
public class MyDatabase extends SQLiteOpenHelper {
//创建表
public static final String CreateTable_my="create Table user(" +
"id primary key, name text, sex text , age integer, password text)";

Context myContext;
/**
*
* @param context
* @param name 创建数据库的名字
* @param factory 用于返回自定义的Cursor,一般填null
* @param version 表示当前数据库的版本号,可用于对数据库进行升级
*/
public Mydatabase(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
myContext=context;
}

@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL(CreateTable_my);//创建表
Toast.makeText(myContext, "数据表创建成功", Toast.LENGTH_SHORT).show();
}

@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

}
}

创建MyDatabase的对象

1
2
//first.db是数据库名
private Mydatabase base=new Mydatabase(DatabaseActivity.this,"first.db",null,1);

生成数据库(会在/data/data/<package name>/databases/目录下建立数据库)

1
2
3
base.getWritableDatabase();
//或者
base.getReadableDatabase()

两个方法的不同处:

getWritableDatabase()返回一个可对数据库进行读写操作的对象,会抛出异常

getReadableDatabase()返回一个以只读方法打开的数据库,不会抛出异常

添加表

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
public class Mydatabase extends SQLiteOpenHelper {
//用户表
public static final String CreateTable_user="create Table user(" +
"id primary key, name text, sex text , age integer, password text)";

//创建另一个表,班级表
public static final String CreateTable_me="create Table clazz(" +
"id primary key, className text, teacher text)";

Context myContext;

/**
*
* @param context
* @param name 创建数据库的名字
* @param factory 用于返回自定义的Cursor,一般填null
* @param version 表示当前数据库的版本号,可用于对数据库进行升级
*/
public Mydatabase(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
myContext=context;
}

@Override
public void onCreate(SQLiteDatabase sqLiteDatabase) {
sqLiteDatabase.execSQL(CreateTable_user);
sqLiteDatabase.execSQL(CreateTable_me);
Toast.makeText(myContext, "数据库创建成功", Toast.LENGTH_SHORT).show();
}
//当 version 中的值改变时,会调用这个方法,通过这个方法,删除原来的表,再调用onCreate()方法生成两个表
@Override
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
sqLiteDatabase.execSQL("drop table if exists user");
sqLiteDatabase.execSQL("drop table if exists clazz");
onCreate(sqLiteDatabase);
Toast.makeText(myContext, "成功", Toast.LENGTH_SHORT).show();
}
}

操作表

  • 添加数据
1
2
3
4
5
6
7
8
9
10
//通过getWritableDatabase()获取SQLiteOpenHelper来操作数据库
SQLiteDatabase database= base.getWritableDatabase();
ContentValues values=new ContentValues();
values.put("name","小明");
values.put("sex","男");
values.put("password","12hjfgikldgislk");
values.put("age",18);
//参数 1.表的名字 2.用于未指定添加数据的情况下给某些可为空的列自动赋值NULL
//3. ContentValues 对象
database.insert("user",null,values);
  • 更新数据
1
2
3
4
5
SQLiteDatabase database= base.getWritableDatabase();
ContentValues values=new ContentValues();
values.put("name","小军");
//后面的两个参数是操作的限制条件
database.update("user",values,"age=?",new String[]{"18"});
  • 删除数据
1
2
3
SQLiteDatabase database= base.getWritableDatabase();
//后面的两个参数是操作的限制条件,用来约束删除哪几行,如果不指定就删除所有行
database.delete("user",null,null);
  • 查询数据
1
2
3
4
5
6
7
8
9
10
11
SQLiteDatabase database=base.getWritableDatabase();
Cursor cursor= database.query("user",null,null,null,null,null,null);
if (cursor.moveToFirst()){
do{
Log.d("name:",cursor.getString(cursor.getColumnIndex("name")));
Log.d("age:",cursor.getString(cursor.getColumnIndex("age")));
Log.d("sex:",cursor.getString(cursor.getColumnIndex("sex")));
Log.d("password:",cursor.getString(cursor.getColumnIndex("password")));
}while (cursor.moveToNext());
}
cursor.close();

query的参数如下:

  • 使用sql直接对数据库进行操作
1
2
3
SQLiteDatabase database=base.getWritableDatabase();
database.execSQL();
database.rawQuery();//只有查询数据的时候才调用这个方法

仿照LitePal实现一个简易的数据库框架SimpleDatabase

SimpleDatabase的使用

  1. 先在asset文件中创建my_database.xml

my_database.xml如下:

1
2
3
4
5
<?xml version="1.0" encoding="UTF-8" ?>
<database name="test.db" version="1">
<!--class属性是数据表Bean的全路径 -->
<table class="com.example.mylibrary.Employee"/>
</database>

Employee的源码如下

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
public class Employee {
private int id;

private String name;

private char sex;

public int getId() {
return id;
}

public void setId(int id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public char getSex() {
return sex;
}

public void setSex(char sex) {
this.sex = sex;
}

@Override
public String toString() {
//使用id作为员工的唯一标识
return Integer.toString(id);
}
}

注意:SimpleDatabase通过toString来区别两个对象是否为同一对象,如Employee就使用id作为标识符。

  1. 在AndroidManifest.xml中加入android:name="com.example.databaselibrary.MyApplication"

    1
    2
    3
    4
    <application
    ...
    android:name="com.example.databaselibrary.MyApplication"
    >
  2. 使用SimpleDatabase

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
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
SimpleDatabase.newInstance().create();//初始化

Employee employee =new Employee();
employee.setId(1);
employee.setName("a");
employee.setSex('男');
SimpleDatabase.saveAndUpdate(employee);

Employee employee1 =new Employee();
employee1.setId(2);
employee1.setName("b");
employee1.setSex('男');
SimpleDatabase.saveAndUpdate(employee1);

Employee employee2 =new Employee();
employee2.setId(3);
employee2.setName("c");
employee2.setSex('女');
SimpleDatabase.saveAndUpdate(employee2);

List<Employee> l=SimpleDatabase.select(Employee.class,null,null,null,null,null,null);
for (int i = 0; i <l.size() ; i++) {
Employee e=l.get(i);
Log.d("===============",e.getName());
Log.d("===============",e.getSex()+"");
Log.d("===============",e.getId()+"");
}
}
}

实现原理

首先读取配置信息,获取数据库和表的信息

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
/**
* 解析xml文件
*/
public class XMLParser {

private final static String RESOURCES="my_database.xml";//配置数据库信息的xml名字

private final static String TABLE="table";//xml属性常量

final static String VERSION="version";//xml属性常量

final static String DATABASE="database";//xml属性常量

private final static String NAME="name";//xml属性常量

private Context context;

private Map<String,String> map=null;//用来存储数据库信息

private List<String> tables=null;//用来存储表信息

public XMLParser(){
init();
}

private void init(){
context=MyApplication.getContext();
map=new HashMap<>(2);
tables=new ArrayList<>();

}

//解析数据
public void parse() throws IOException, XmlPullParserException {

XmlPullParserFactory factory=XmlPullParserFactory.newInstance();

XmlPullParser xmlPullParser=factory.newPullParser();
//从asset文件下读取my_database.xml的信息
xmlPullParser.setInput(new InputStreamReader(context.getAssets().open(RESOURCES)));
int type=xmlPullParser.getEventType();
while(type!=XmlPullParser.END_DOCUMENT){
if (xmlPullParser.getEventType()==XmlResourceParser.START_TAG){//如果为开始标签
String name=xmlPullParser.getName();
switch (name){
case DATABASE://标签为<database>
parseDatabase(xmlPullParser);
break;
case TABLE://标签为<table>
parseTable(xmlPullParser);
break;
}
}
xmlPullParser.next();//下一个标签
type=xmlPullParser.getEventType();
}

}

//解析数据库信息
private void parseDatabase(XmlPullParser xmlPullParser) {
String databaseName=null;
String version=null;
if (xmlPullParser.getAttributeCount()==2){
String value_1=xmlPullParser.getAttributeName(0);
if (NAME.equals(value_1)){
databaseName=xmlPullParser.getAttributeValue(0);
version=xmlPullParser.getAttributeValue(1);
}else {
databaseName=xmlPullParser.getAttributeValue(1);
version=xmlPullParser.getAttributeValue(0);
}
}else{
throw new MyException("database标签的参数错误");
}
map.put(DATABASE,databaseName);
map.put(VERSION,version);
}

//解析表格信息
private void parseTable(XmlPullParser xmlPullParser) {
String className=null;
if (xmlPullParser.getAttributeCount()==1){
className=xmlPullParser.getAttributeValue(0);
}else
throw new MyException("table参数错误");
tables.add(className);
}

public Map<String, String> getMap() {
return map;
}
public List<String> getTables() {
return tables;
}
}

创建数据库的类

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
public class MyDatabase extends SQLiteOpenHelper {

private onDatabaseUpdateListener listener=null;

private static final String TAG = "MyDatabase";

public MyDatabase(Context context, String name, SQLiteDatabase.CursorFactory factory, int version,onDatabaseUpdateListener listener) {
super(context, name, factory, version);
this.listener=listener;
}

@Override
public void onCreate(SQLiteDatabase db) {
String[] createTables = listener.onCreate();
for (String s: createTables){
db.execSQL(s);
Log.d("======建表语句",s);
}
Log.d("======","onCreate执行");
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
String[] deleteTable = listener.update(db);
Log.d("======","onUpgrade执行");
if (deleteTable !=null){
for (String s: deleteTable){
db.execSQL(s);
Log.d("=====删表语句",s);
}
}
onCreate(db);
listener.onCreateLater(db);
}

interface onDatabaseUpdateListener{
String[] update(SQLiteDatabase db);//数据库版本更新时调用
String[] onCreate();//创建新的表时调用
void onCreateLater(SQLiteDatabase db);//创建完表时调用
}
}

完成数据库操作的实现类

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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
/**
* 实现数据的操作和数据库的创建
*/
public class SimpleDatabase implements MyDatabase.onDatabaseUpdateListener{

private final static String NAME="SimpleDatabase.xml";

private final static String OLD="old";

private final static String TABLE="table_";

private final static String NUMBER="number";

//MyDatabaseHelper是一个辅助类,用来生成创建数据库和表所需要的数据
private static MyDatabaseHelper databaseHelper=null;

private static SQLiteDatabase db=null;

private Map<String,Cursor> savedData=null;

String simpleNames[]=null;

private static final String TAG = "SimpleDatabase";

private static SimpleDatabase simpleDatabase=new SimpleDatabase();

public SimpleDatabase(){
init();
}

private void init(){
databaseHelper=new MyDatabaseHelper();
}

/**
* 查询指定的数据
*/
public static<T> List<T> select(Class<T> clazz,String columnNames[],String where,
String args[],String groupBy, String having, String orderBy){
List<T> list = new ArrayList<>();
Cursor cursor= db.query(clazz.getSimpleName(),columnNames,where,args,groupBy,having,orderBy);
while(cursor.moveToNext()){
try {
T t = clazz.newInstance();
Field fields[]=clazz.getDeclaredFields();
for (Field f:fields) {
f.setAccessible(true);
String fieldName = f.getName();
String fieldValue = cursor.getColumnName(cursor.getColumnIndex(fieldName));
//由于getColumnName()只会返回String类型,所以这里需要getInitialTypeValue()
//获取初始类型的值
f.set(t,getInitialTypeValue(f.getType().getSimpleName(),fieldValue));
}
list.add(t);
} catch (InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
}
cursor.close();
return list;
}

private static Object getInitialTypeValue(String type,String value){
switch (type){
case "int":
case "integer":
return Integer.valueOf(value);
case "boolean":
return Boolean.valueOf(value);
case "float":
return Float.valueOf(value);
case "double":
return Double.valueOf(value);
case "String":
case "Character":
case "char":
return value;
}
return null;
}

/**
* 查询指定的数据
*/
private static Cursor select(Object obj,String columnNames[],String where,String args[]){
String tableName=obj.getClass().getSimpleName();
return db.query(tableName,columnNames,where,args,null,null,null);
}

/**
* 如果不存在数据库就创建,如果已经存在,则直接结束
*/
public void create(){
String name=databaseHelper.getName();
String version=databaseHelper.getVersion();
if (databaseHelper.check(getOldVersion(),Integer.valueOf(version)))//如果需要更新
saveDataInSharedPreferences(Integer.valueOf(version));
Log.d("=========","name"+name);
MyDatabase database = new MyDatabase(MyApplication.getContext(), name, null, Integer.valueOf(version), SimpleDatabase.this);
db= database.getWritableDatabase();
}

public static SimpleDatabase newInstance() {
return simpleDatabase;
}

/**
* 存储批量数据
* @param list
* @throws IllegalAccessException
*/
public static void save(List<Object> list) {
for (Object o:list) {
save(o);
}
}

/**
* 存储单个数据到表中
* @param o
* @throws IllegalAccessException
*/
public static void save(Object o) {
Class clazz=o.getClass();
Field fields[]=clazz.getDeclaredFields();
ContentValues values=new ContentValues();
values.put("simple_database_id",o.toString());
for (Field f:fields) {
try {
f.setAccessible(true);
if (f.get(o)!=null&&!"serialVersionUID".equals(f.getName())) {
values.put(f.getName(), f.get(o).toString());
Log.d("========value", f.get(o).toString());
}
} catch (IllegalAccessException e) {
e.printStackTrace();
Log.wtf(TAG,"类中所以的数据应该设置值");
}
}
db.insert(clazz.getSimpleName(),null,values);
}

public static void saveAndUpdate(Object o){
Class clazz=o.getClass();
String id=o.toString();
Field fields[]=clazz.getDeclaredFields();
ContentValues values=new ContentValues();
values.put("simple_database_id",o.toString());
for (Field f:fields) {
try {
f.setAccessible(true);
if (f.get(o)!=null&&!"serialVersionUID".equals(f.getName())) {
values.put(f.getName(), f.get(o).toString());
Log.d("========value", f.get(o).toString());
}
} catch (IllegalAccessException e) {
e.printStackTrace();
Log.wtf(TAG,"类中所以的数据应该设置值");
}
}
Cursor cursor=select(o,null,"simple_database_id=?",new String[]{id});
if (cursor.getCount()==0){//插入
db.insert(clazz.getSimpleName(),null,values);
}else {//更新
db.update(clazz.getSimpleName(),values,"simple_database_id=?",new String[]{id});
}
}


/**
* 删除表中所有的数据
* @param o
*/
public static void delete(Object o){
Class clazz=o.getClass();
delete(clazz.getSimpleName(),"simple_database_id=?",o.toString());
}


/**
* 删除表中指定的数据
* @param name
* @param where
* @param arg
*/
private static void delete(String name,String where,String... arg){
db.delete(name,where,arg);
}

/**
* 如果版本更新,则存储最新的版本
* @param version 版本号
*/
private static void saveDataInSharedPreferences(int version){
//获取SharedPreferences的Editor对象来执行储存操作
SharedPreferences.Editor editor=MyApplication.getContext().getSharedPreferences(NAME,0).edit();
editor.putInt(OLD,version);
editor.apply();//最后一定要调用这个方法,完成数据的储存
}

/**
*存储过去的表名
* @param names
*/
private void saveDataInSharedPreferences(String[] names){
//获取SharedPreferences的Editor对象来执行储存操作
SharedPreferences.Editor editor=MyApplication.getContext().getSharedPreferences(NAME,0).edit();
for (int i=0;i<names.length;i++){
editor.putString(TABLE+i,names[i]);
}
editor.putInt(NUMBER,names.length);
editor.apply();//最后一定要调用这个方法,完成数据的储存
}


/**
* 获取上一次的数据库的版本
* @return
*/
private static int getOldVersion(){
SharedPreferences get=MyApplication.getContext().getSharedPreferences(NAME,0);
return get.getInt(OLD,0);
}


private String[] getOldTableName(){
SharedPreferences get=MyApplication.getContext().getSharedPreferences(NAME,0);
int length=get.getInt(NUMBER,-1);
if (length==-1)
Log.wtf(TAG,"原有表格不存在");
String names[]=new String[length];
for (int i=0;i<length;i++){
names[i]=get.getString(TABLE+i,"");
}
return names;
}

@Override
public String[] update(SQLiteDatabase db) {//在删除表之前把表的数据保存起来
simpleNames=getOldTableName();
savedData=new HashMap<>(simpleNames.length);
for (String name:simpleNames) {
Cursor cursor=db.query(name,null,null,null,null,null,null);
savedData.put(name,cursor);
}
Cursor cursor=savedData.get(simpleNames[0]);
if (cursor.moveToFirst()){
String sex=cursor.getString(cursor.getColumnIndex("sex"));
Log.d("===============update","sex="+sex);
}
return databaseHelper.getDeleteTable();
}

@Override
public String[] onCreate() {
saveDataInSharedPreferences(databaseHelper.getSimpleTableName());
return databaseHelper.getCreateTable();
}

@Override
public void onCreateLater(SQLiteDatabase db) {
recoverAllData(db);
}

/**
* 恢复所有的数据
*/
private void recoverAllData(SQLiteDatabase db){
List<String> deleteTable=checkWhichTableDisappear(databaseHelper.getTables());
List<String> nowTable=Arrays.asList(simpleNames);
nowTable.remove(deleteTable);
for (int i=0;i<nowTable.size();i++){
Cursor cursor=savedData.get(nowTable.get(i));
ContentValues values=new ContentValues();
if (cursor.moveToFirst()){
do{
String columnNames[]=cursor.getColumnNames();
for (int j=0;j<columnNames.length;j++)
values.put(columnNames[j],cursor.getString(cursor.getColumnIndex(columnNames[j])));
}while (cursor.moveToNext());
db.insert(nowTable.get(i),null,values);
}
}
for (String n:simpleNames) {//释放所有的资源
savedData.get(n).close();
}
}

/**
* 检查有哪些表被删除
* @param newTable
* @return
*/
private List<String> checkWhichTableDisappear(List<String> newTable){
String deleteTable[]=new String[simpleNames.length];
for (int i=0,j=0;i<simpleNames.length;i++){
if (!newTable.contains(simpleNames[i])){
deleteTable[j]=simpleNames[i];
j++;
}
}
return Arrays.asList(deleteTable);
}
}

SimpleDatabase的主要作用是在my_database.xml中的配置更改时,能自动更新数据库;插入和更新时,通过saveAndUpdate(Object o)使用对象来实现插入和更新操作(当数据库中不存在同一条数据时,就插入;当数据库中存在同一条数据时,就更新);查询时,通过

List<T> select(Class<T> clazz,String columnNames[],String where,String args[],String groupBy, String having, String orderBy)获取包含查询结果对象的集合;删除时,通过delete(Object o)使用对象来实现删除操作。

实现原理:

SimpleDatabase通过实现MyDatabase的onDatabaseUpdateListener接口,监听MyDatabase的onCreate和onUpgrade方法。在onCreate被调用时,调用onDatabaseUpdateListener.onCreate来存储之前的表名(如果修改了配置文件的话),并返回创建表的sql语句集合(可能创建多个表),之后在MyDatabase.onCreate中创建表。当onUpgrade被调用时,调用onDatabaseUpdateListener.update来存储当前数据库中的数据,并返回删除表的sql语句集合,删除成功后创建新的表,之后调用onDatabaseUpdateListener.onCreateLater方法将之前存储的数据重新存储到数据库中。

SimpleDatabase中的select、delete、saveAndUpdate方法是通过反射实现的,具体可以看注释。

其他类的实现很简单,具体可以看源码:

  • MyApplication类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    * 获取系统的context
    */
    public class MyApplication extends Application {
    private static Context context;
    @Override
    public void onCreate() {
    super.onCreate();
    context = getApplicationContext();
    }
    public static Context getContext(){
    return context;
    }
    }
  • MyDatabaseHelper类

    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
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    /**
    * 生成创建数据库和表所需要的数据
    */
    public class MyDatabaseHelper {

    private String name=null;

    private String version=null;

    private List<String> tables=null;//存储完整类名

    private Map<String,Table[]> maps=null;

    private String createTable[]=null;//存储建表语句

    private String deleteTable[]=null;//存储删除表的语句

    private boolean ok=false;

    private static final String TAG = "MyDatabaseHelper";

    public MyDatabaseHelper(){
    init();
    }

    /**
    * 初始化数据
    */
    private void init(){
    XMLParser xmlParser=null;
    xmlParser=new XMLParser();
    try {
    xmlParser.parse();
    } catch (IOException | XmlPullParserException e) {
    e.printStackTrace();
    }
    name=xmlParser.getMap().get(XMLParser.DATABASE);
    version=xmlParser.getMap().get(XMLParser.VERSION);
    tables=xmlParser.getTables();
    maps=new HashMap<>(tables.size());
    }

    /**
    * 检查是否需要更新
    * @param old 之前的版本
    * @param now 现在的版本
    */
    public boolean check(int old,int now){
    if (now>old) {
    try {
    parseTable();
    ok=true;
    return true;
    } catch (ClassNotFoundException e) {
    e.printStackTrace();
    }
    }
    return false;
    }

    /**
    * 解析类的数据,在version改变时调用
    * @throws ClassNotFoundException
    */
    private void parseTable() throws ClassNotFoundException {
    for (String name:tables){
    Class table=Class.forName(name);
    Field[] field=table.getDeclaredFields();
    Table info[]=new Table[field.length];
    for (int i=0;i<field.length;i++){
    Table t=new Table();
    t.setProperty(field[i].getName());
    t.setType(field[i].getType().getSimpleName());
    info[i]=t;
    }
    maps.put(name,info);
    }
    }
    /**
    * 生成建表语句
    */
    private void generateTable(){
    for (int i=0;i<tables.size();i++){
    Table table[]=maps.get(tables.get(i));
    StringBuilder stringBuilder=new StringBuilder();
    String simpleName=getSimpleName(tables.get(i));
    stringBuilder.append("create table "+simpleName+"( ");
    for (int j=0;j<table.length;j++){
    Table t=table[j];
    if (t!=null)
    if (!Table.OTHER.equals(t.getType()))
    stringBuilder.append(" , "+t.getProperty()+" "+t.getType());
    }
    String string=stringBuilder.append(")").toString();
    string=string.replaceFirst(",","");
    createTable[i]=string;
    }
    }

    /**
    * 生成删除表的语句
    */
    private void deleteTable() {
    for (int i = 0; i < tables.size(); i++) {
    deleteTable[i]="drop table if exists "+getSimpleName(tables.get(i));
    }
    }

    /**
    * 获取简单类名,不包括包
    * @param name 带有包名的类名
    * @return 不包含包名的类名
    */
    private String getSimpleName(String name){
    int position= name.lastIndexOf('.');
    return name.substring(position+1);
    }



    public String getName() {
    return name;
    }

    public String getVersion() {
    return version;
    }

    public String[] getCreateTable() {
    if (!ok)
    Log.e(TAG,"必须先调用check()");
    createTable=new String[tables.size()];
    generateTable();
    return createTable;
    }

    public String[] getDeleteTable() {
    if (!ok)
    Log.e(TAG,"必须先调用check()");
    deleteTable=new String[tables.size()];
    deleteTable();
    return deleteTable;
    }

    public List<String> getTables() {
    return tables;
    }

    public String[] getSimpleTableName() {

    String simpleTableName[]=new String[tables.size()];

    for (int i=0;i<tables.size();i++) {

    String simpleName = getSimpleName(tables.get(i));

    simpleTableName[i] = simpleName;

    }

    return simpleTableName;

    }
    }
  • MyException

    1
    2
    3
    4
    5
    public class MyException extends RuntimeException {
    public MyException(String message) {
    super(message);
    }
    }
  • Table类

    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
    /**
    * 存储每个字段对应的属性和名字
    */
    public class Table {
    final static String INTEGER="integer";

    final static String TEXT="text";

    final static String REAL="real";

    final static String BLOB="blob";

    final static String INT="int";

    final static String CHAR="char";

    final static String FLOAT="float";

    final static String DOUBLE="double";

    final static String STRING="String";

    final static String BOOLEAN="boolean";

    final static String OTHER="other";

    private String property;//对应的属性

    private String type;//对应的属性的类型

    public String getProperty() {
    return property;
    }

    public void setProperty(String property) {
    this.property = property;
    }

    public String getType() {
    return type;
    }

    public void setType(String type) {
    checkProperty(type);
    }

    private void checkProperty(String property){
    switch (property){
    case INT:
    case BOOLEAN:
    type=INTEGER;
    break;
    case FLOAT:
    case DOUBLE:
    type=REAL;
    break;
    case STRING:
    case CHAR:
    type=TEXT;
    break;
    default:
    type=OTHER;
    break;
    }
    }
    }

参考菜鸟教程

TCP/IP的学习总结

发表于 2019-06-05 | 分类于 计算机网络

这篇文章是自己所学的《计算机网络》这门课程的总结,如果有任何错误欢迎指正。

TCP/IP模型


应用层:具体应用程序实现层

运输层:让应用程序之间实现通信

网络层(或网际层):主要利用IP协议转发分包数据

网络接口层:TCP/IP对网络接口层未作定义

网络层和运输层的功能通常由操作系统提供,一般通过操作系统的Socket接口

网络层

网络层中最重要的就是IP协议。如下图,通过IP协议可以实现在不同的网络中进行通信,而不用关系下层的网络接口层中所使用的协议。

IP协议是无连接的、尽最大努力交付的不可靠协议

IP地址

IP地址是IP协议用来识别不同主机的地址,类似于MAC地址。

两级IP 地址


net-id代表网络号,host-id代表主机号

三级IP 地址

以A类地址为例,一个A类地址可以连2^24个主机,但是我只需要连接2^20个主机,而使用B类地址又不足,这种情况就造成了浪费;再加上IP地址快耗尽的问题,就出现了三级IP 地址。

如下图,通过从主机号借用若干个位作为子网号subnet-id,而主机号 host-id也就相应减少了若干个位。通过这种方式就可以对IP地址进行更精细地划分,从而减少IP地址地浪费。而且划分子网纯属一个单位内部的事情单位对外仍然表现为没有划分子网的网络,从而兼容以前的两级IP 地址。

子网掩码

通过子网掩码来获取IP地址中的子网部分,如图IP地址与子网掩码进行与运算得到子网的网络地址:

无分类编址(CIDR)

由于IPv4的地址即将耗尽,就采用无分类编址(CIDR)消除了传统的 A 类、B 类和 C 类地址以及划分子网的概念,从而使用各种长度的“网络前缀”来代替分类地址中的网络号和子网号。

网络层的其他协议

ICMP协议

IP协议并不是一个可靠的协议,它不保证数据被送达,那么,自然的,保证数据送达的工作应该由其他的模块来完成。其中一个重要的模块就是ICMP(网络控制报文)协议。ICMP不是高层协议,而是IP层的协议。

当传送IP数据包发生错误。比如主机不可达,路由不可达等等,ICMP协议将会把错误信息封包,然后传送回给主机。给主机一个处理错误的机会,这 也就是为什么说建立在IP层以上的协议是可能做到安全的原因。

RAP协议

RAP协议是通过目的IP地址来获取目的MAC地址的协议。

IP协议数据报格式

版本:IP协议的版本,占4位

生存时间(TTL):数据报在网络中可通过的路由器数,IP数据包每穿过一个路由器,该数据包的TTL数值就会减少1,当该数据包的TTL成为零,它就会被自动抛弃。

协议:0x11代表UDP,0x06代表TCP

运输层

TCP与UDP的区别

TCP UDP
可靠性 可靠 不可靠
连接性 面向连接 无连接
报文 面向字节流 面向报文
流量控制 有 无
堵塞控制 有 无
传播速度 慢 快
应用场景 对数据准确性要求高的场景 对数据效率要求高的,比如视频通话等。

面向报文和面向字节流的区别:


如图,面向报文的传输方式是应用层交给UDP多长的报文,UDP发送多长的报文,即一次发送一个报文。因此,应用程序必须选择合适大小的报文。若报文太长,则IP层需要分片,降低效率;若太短,会使IP数据报太小。


如图,面向字节流的话,虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序看成是一连串的无结构的字节流。TCP有一个缓冲,当应用程序传送的数据块太长,TCP就可以把它划分短一些再传送。

TCP

三次握手和四次挥手

TCP是面向连接的运输层协议,因此需要先建立连接,在进行数据传输。TCP建立连接的方式是三次握手,而切断连接是通过四次挥手。
三次握手和四次挥手

为什么要三次握手?

为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。

具体例子——“已失效的连接请求报文段”的产生在这样一种情况下:

客户端发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达服务器。

本来这是一个早已失效的报文段。但服务器收到此失效的连接请求报文段后,就误认为是客户端再次发出的一个新的连接请求。于是就向客户端发出确认报文段,同意建立连接。

假设不采用“三次握手”,那么只要服务器发出确认,新的连接就建立了。由于现在客户端并没有发出建立连接的请求,因此不会理睬服务器的确认,也不会向服务器发送数据。但服务器却以为新的运输连接已经建立,并一直等待客户端发来数据。这样,服务器的很多资源就白白浪费掉了。采用“三次握手”的办法可以防止上述现象发生。

例如刚才那种情况,客户端不会向服务器的确认发出确认。服务器由于收不到确认,就知道客户端并没有要求建立连接。”

为什么要四次分手?

TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议。TCP是全双工模式,这就意味着,当主机1发出FIN报文段时,只是表示主机1已经没有数据要发送了,主机1告诉主机2,它的数据已经全部发送完毕了;

但是,这个时候主机1还是可以接受来自主机2的数据;当主机2返回ACK报文段时,表示它已经知道主机1没有数据发送了,但是主机2还是可以发送数据到主机1的;当主机2也发送了FIN报文段时,这个时候就表示主机2也没有数据要发送了,就会告诉主机1,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。

为什么要等待2MSL?

  • 保证TCP协议的全双工连接能够可靠关闭
  • 保证这次连接的重复数据段从网络中消失

    MSL:报文段最大生存时间,它是任何报文段被丢弃前在网络内的最长时间。

转载TCP/IP · Android校招面试指南

流量控制

流量控制(flow control)就是让发送方的发送速率不要太快,既要让接收方来得及接收,也不要使网络发生拥塞。
利用滑动窗口机制可以很方便地在 TCP 连接上实现流量控制。

滑动窗口机制:滑动窗口机制就是发送方和接收方各自维护一个发送窗口和接受窗口。当网络堵塞时,接收方可以减小接受窗口的大小,同时在确定报文中设置发送窗口的大小,当发送方接受到确定报文时,可以减小发送窗口的大小,从而减少发送速率。同理,当网络通畅时,可以提高发送速率。


TCP为每一个连接设有一个持续计时器(persistence timer)。当TCP连接中的发送方收到接收方的零窗口通知时(即rwnd=0时),发送方就启动持续计时器。若持续计时器设置的时间到期,发送方就发送一个零窗口控测报文段(携1字节的数据)给接收方。如果接收方可以接收数据,就重新开始发送数据;如果接收方不能接收数据,就重新设置持续计时器。

堵塞控制

在某段时间,若对网络中某资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏,就产生拥塞

堵塞控制和流量控制的区别?

  • 拥塞控制是一个全局性的过程,涉及到所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。
  • 流量控制往往指在给定的发送端和接收端之间的点对点通信量的控制。流量控制所要做的就是抑制发送端发送数据的速率,以便使接收端来得及接收。

堵塞控制的方法:

  1. 慢开始和拥塞避免
  2. 快重传和快恢复
慢开始和拥塞避免

发送方维护一个叫做拥塞窗口的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。
发送方控制拥塞窗口的原则是:只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减小一些,以减少到网络中的分组数。

如上图,慢开始算法:

使用慢开始算法后,每经过一个传输轮次,拥塞窗口 cwnd 就加倍。
一个传输轮次所经历的时间其实就是往返时间 RTT。传输轮次更加强调:把拥塞窗口 cwnd 所允许发送的报文段都连续发送出去,并收到了对已发送的最后一个字节的确认。
例如,拥塞窗口 cwnd = 4,这时的往返时间 RTT 就是发送方连续发送 4 个报文段,并收到这 4 个报文段的确认,总共经历的时间。

拥塞避免算法

拥塞窗口 cwnd 缓慢地增大,即每经过一个往返时间 RTT 就把发送方的拥塞窗口 cwnd 加 1,而不是加倍,使拥塞窗口 cwnd 按线性规律缓慢增长。

由于指数爆炸的性质,就必须设置慢开始的门限 ssthresh ,其用法如下:

  1. 当 cwnd < ssthresh 时,使用慢开始算法。
  2. 当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
  3. 当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞避免算法。

无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有按时收到确认),就要把慢开始门限 ssthresh 设置为出现拥塞时的发送方窗口值的一半(但不能小于2)。
然后把拥塞窗口 cwnd 重新设置为 1,执行慢开始算法。
这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。如下图:

快重传和快恢复

快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认。这样做可以让发送方及早知道有报文段没有到达接收方。
发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段。
不难看出,快重传并非取消重传计时器,而是在某些情况下可更早地重传丢失的报文段。如下图:

快恢复

  1. 当发送端收到连续三个重复的确认时,就执行“乘法减小”算法,把慢开始门限 ssthresh 减半。但接下去不执行慢开始算法。
  2. 由于发送方现在认为网络很可能没有发生拥塞,因此现在不执行慢开始算法,即拥塞窗口 cwnd 现在不设置为 1,而是设置为慢开始门限 ssthresh 减半后的数值,然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。

注意发送方的发送窗口的上限值应当取为接收方窗口 rwnd 和拥塞窗口 cwnd 这两个变量中较小的一个

当 rwnd < cwnd时,是接收方的接收能力限制发送窗口的最大值。

当 cwnd < rwnd时,则是网络的拥塞限制发送窗口的最大值。

应用层

DNS

DNS 是一个 域名 和 IP 相互映射的分布式数据库,通过DNS我们就可以通过域名更方便的访问互联网
详细的见DNS基础知识

HTTP

HTTP是超文本传输协议,用于客户端和服务器端之间的通信,属于TCP/IP中的应用层。
详细的见见《图解HTTP》核心知识总结

参考资料:

  • TCP/IP · Android校招面试指南
  • 《计算机网络》
  • 《图解TCP/IP》

Android应用之间数据的交互-获取系统应用的数据

发表于 2019-05-29 | 分类于 Android

在Android应用开发中我们常常需要和其他应用进行交互,之前对这些问题没有仔细了解过,现在来做一下总结。

Android应用之间数据的交互方式:

  • 获取系统应用的数据
  • 提供数据给其他应用
  • 应用之间的分享

下面介绍获取系统应用的数据

实例分析

以获取联系人数据为例,代码如下:

1
2
3
4
5
6
7
8
9
10
11
Cursor cursor=getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);
if (cursor!=null){
while(cursor.moveToNext()){
//获取联系人的名字
String name=cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
//获取联系人的电话号码
String number=cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
Log.d("============"," name: "+name+" number="+number);
}
cursor.close();
}

ContentResolver类的query(Uri uri,String[] projection,String selection,String[] selectionArgs,String sortOrder)方法参数的介绍:

uri用来指明要访问的数据的位置

projection访问的列,如果为null,则表示访问所有列

selection用于指定查询条件

selectionArgs查询参数

sortOrder排序顺序

基本流程

那么ContentResolver是怎么通过query方法来获取联系人的数据的呢?下面我们来分析ContentResolver的源码:

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
//1
public final @Nullable Cursor query(@RequiresPermission.Read @NonNull Uri uri,
@Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder) {
return query(uri, projection, selection, selectionArgs, sortOrder, null);
}
//2
public final @Nullable Cursor query(@RequiresPermission.Read @NonNull Uri uri,
@Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder,
@Nullable CancellationSignal cancellationSignal) {
//把查询参数封装在Bundle中
Bundle queryArgs = createSqlQueryBundle(selection, selectionArgs, sortOrder);
return query(uri, projection, queryArgs, cancellationSignal);
}
//3
public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri,
@Nullable String[] projection, @Nullable Bundle queryArgs,
@Nullable CancellationSignal cancellationSignal) {
Preconditions.checkNotNull(uri, "uri");
//获取不稳定的提供者 IContentProvider是IPC接口用来与内容提供者进行数据交互
IContentProvider unstableProvider = acquireUnstableProvider(uri);
if (unstableProvider == null) {
return null;
}
IContentProvider stableProvider = null;
Cursor qCursor = null;
try {
...//省略些不重要的源码
try {
qCursor = unstableProvider.query(mPackageName, uri, projection,
queryArgs, remoteCancellationSignal);
} catch (DeadObjectException e) {
// The remote process has died... but we only hold an unstable
// reference though, so we might recover!!! Let's try!!!!
// This is exciting!!1!!1!!!!1
unstableProviderDied(unstableProvider);
stableProvider = acquireProvider(uri);//第一次创建的是不稳定的,如果失败就重试
if (stableProvider == null) {
return null;
}
qCursor = stableProvider.query(
mPackageName, uri, projection, queryArgs, remoteCancellationSignal);
}
if (qCursor == null) {
return null;
}

// Force query execution. Might fail and throw a runtime exception here.
qCursor.getCount();//调用这个方法判断是否查询出错
long durationMillis = SystemClock.uptimeMillis() - startTime;
maybeLogQueryToEventLog(durationMillis, uri, projection, queryArgs);

// Wrap the cursor object into CursorWrapperInner object.
final IContentProvider provider = (stableProvider != null) ? stableProvider
: acquireProvider(uri);
final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider);//CursorWrapperInner是内部类
stableProvider = null;
qCursor = null;
return wrapper;
} catch (RemoteException e) {
// Arbitrary and not worth documenting, as Activity
// Manager will kill this process shortly anyway.
return null;
} finally {//释放资源
...
}
}

public final IContentProvider acquireUnstableProvider(Uri uri) {
if (!SCHEME_CONTENT.equals(uri.getScheme())) {//SCHEME_CONTENT为content,如果uri的scheme为null则返回null
return null;
}
String auth = uri.getAuthority();
if (auth != null) {
return acquireUnstableProvider(mContext, uri.getAuthority());//这里调用的是ApplicationContentResolver的acquireUnstableProvider方法
}
return null;
}

在ContentResolver的源码中query有三个重载方法1,2,3,在实例中我们调用重载方法1,方法1中则直接调用方法2。在方法2中,封装了数据到Bundle中,并调用了方法3,真正的实现在方法3中。方法3中的核心方法是acquireUnstableProvider(Uri uri),它获取IContentProvider(一个IPC 接口),并通过IContentProvider来获取联系人的数据。而acquireUnstableProvider(Uri uri)调用的是acquireUnstableProvider(Context c, String name)方法,它是个抽象方法,具体的实现类是ApplicationContentResolver。

ApplicationContentResolver的acquireUnstableProvider方法源码如下:

1
2
3
4
5
6
@Override
protected IContentProvider acquireUnstableProvider(Context c, String auth) {
return mMainThread.acquireProvider(c,
ContentProvider.getAuthorityWithoutUserId(auth),
resolveUserIdFromAuthority(auth), false);
}

在ApplicationContentResolver的acquireUnstableProvider方法调用了ActivityThread的acquireProvider方法,源码如下:

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
public final IContentProvider acquireProvider(
Context c, String auth, int userId, boolean stable) {
// 判断缓存中是否有对应的IContentProvider,有则直接返回
final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);
if (provider != null) {
return provider;
}

// There is a possible race here. Another thread may try to acquire
// the same provider at the same time. When this happens, we want to ensure
// that the first one wins.
// Note that we cannot hold the lock while acquiring and installing the
// provider since it might take a long time to run and it could also potentially
// be re-entrant in the case where the provider is in the same process.
ContentProviderHolder holder = null;
try {
//通过AMS获取ContentProviderHolder, 处理在不同进程的情况,
//如果在同一进程就让`holder.provider = null;`
holder = ActivityManager.getService().getContentProvider(
getApplicationThread(), auth, userId, stable);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
//失败
if (holder == null) {
Slog.e(TAG, "Failed to find provider info for " + auth);
return null;
}

// Install provider will increment the reference count for us, and break
// any ties in the race.
//如果在同一进程,则进行相应的处理
holder = installProvider(c, holder, holder.info,
true /*noisy*/, holder.noReleaseNeeded, stable);
return holder.provider;
}

ApplicationContentResolver通过调用ActivityThread的acquireProvider方法来获取ContentProvider;ActivityThread先会判断缓存中是否存在要求的IContentProvider,
如果存在就直接返回,如果不存在就调用AMS的getContentProviderImpl()和installProvider来获取。

关于AMS获取IContentProvider的详细流程,可以看ContentProvider启动过程分析

ContactsContract详解

ContactsContract是存储联系人的数据表的常量字段,它有几个常用的内部类如下:

  • ContactsContract.Data:数据表的常量,包含与原始联系人关联的数据点。 数据表的每一行通常用于存储单条联系信息(例如电话号码)及其相关元数据(例如,它是工作号码还是家庭号码)
  • ContactsContract.CommonDataKinds:用于定义存储在ContactsContract.Data表中的公共数据类型的容器。
  • ContactsContract.Contacts:联系人表的常量,包含代表同一个人的每个原始联系人聚合的记录

联系人数据是存储在数据库中(具体位置在data/data/com.android.providers.contacts/databases中),根据其MIME类型来判断其位置所代表的意义。如
ContactsContract.CommonDataKinds.Phone.NUMBER和ContactsContract.CommonDataKinds.Email.ADDRESS都表示
data1,但是在数据库中不同的MIME类型的data1表示不同的数据。

数据库中data表的字段和数据如下(字段太多,只截取了部分)

MIME类型如下

通过MIME类型来获取所需要的数据

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
Uri p = ContactsContract.Data.CONTENT_URI;
Cursor id = getActivity().getContentResolver().query(p,new String[]{ContactsContract.Data.CONTACT_ID},null,null,null);
while (id.moveToNext()){
Cursor cursor=getActivity().getContentResolver().query(p, null,ContactsContract.Data.CONTACT_ID+"= ?", new String[]{id.getString(id.getColumnIndex(ContactsContract.Data.CONTACT_ID))}, null);
Person person = new Person();
byte[] image = null;
StringBuilder builder = new StringBuilder();
while (cursor.moveToNext()){
String mime = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.MIMETYPE));
Log.d("===========","mime = "+mime);
if (mime.equals(ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE)){
builder.append("name = "+cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)));
builder.append("number = "+cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)));
}else if (mime.equals(ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE)){
//二进制数据一般放在data15中
image=cursor.getBlob(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Photo.DATA15));
}else if (mime.equals(ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE)){
builder.append("email = "+cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS)));
}else if (mime.equals(ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE)){
builder.append("address = "+cursor.getString(cursor.getColumnIndex(ContactsContract.
CommonDataKinds.StructuredPostal.STREET)));
}
}
String con=builder.toString();
Log.d("===========",con);
cursor.close();
}
id.close();

ContentResolver()还有insert、delete、update方法,其用法与query类似,这里不再介绍。

获取其他系统应用数据

CalendarContract

CalendarContract是存储日历和事件相关信息字段表,与ContactsContract类似。CalendarContract也有几个内部类,常用的是CalendarContract.Events,包含诸如事件标题,位置,开始时间,结束时间等信息。CalendarContract.Events的使用如下:

1
2
3
4
5
6
7
8
9
10
11
    Uri p = CalendarContract.Events.CONTENT_URI;
Cursor cursor=getActivity().getContentResolver().query(p, null,null, null, null);
while (cursor.moveToNext()){
StringBuilder builder = new StringBuilder();
builder.append("标题 : "+cursor.getString(cursor.getColumnIndex(CalendarContract.Events.TITLE))+"\n")
.append("起始时间 :"+cursor.getString(cursor.getColumnIndex(CalendarContract.Events.DTSTART))+"\n")
.append("结束时间 :"+cursor.getString(cursor.getColumnIndex(CalendarContract.Events.DTEND))+"\n")
.append("描述 : "+cursor.getString(cursor.getColumnIndex(CalendarContract.Events.DESCRIPTION))+"\n");
contents.add(builder.toString());
}
cursor.close();

更多关于CalendarContract的使用,可以查看官方文档。

MediaStore

MediaStore包含内部和外部存储设备上所有可用媒体的元数据。其内部类如下:

  • MediaStore.Audio:集装箱所有的音频内容。
  • MediaStore.Files:媒体提供程序表,包含媒体存储中所有文件的索引,包括非媒体文件。
  • MediaStore.Images:包含所有可用图像的元数据
  • interface MediaStore.MediaColumns:大多数MediaProvider表的公共字段
  • MediaStore.Video:包含所有可用视频的元数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<String> contents = new ArrayList<>();
Uri p1 = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Cursor cursor1=getActivity().getContentResolver().query(p1, null,null, null, null);
while (cursor1.moveToNext()){
StringBuilder builder = new StringBuilder();
builder.append("文件名 : "+cursor1.getString(cursor1.getColumnIndex(MediaStore.Images.Media.TITLE))+"\n")
.append("描述 :"+cursor1.getString(cursor1.getColumnIndex(MediaStore.Images.Media.DESCRIPTION))+"\n")
.append("大小 :"+cursor1.getString(cursor1.getColumnIndex(MediaStore.Images.Media.SIZE))+"\n")
.append("位置 : "+cursor1.getString(cursor1.getColumnIndex(MediaStore.Images.Media.DATA))+"\n")
.append("文件修改时间:"+cursor1.getString(cursor1.getColumnIndex(MediaStore.Images.Media.DATE_MODIFIED))+"\n")
.append("DISPLAY_NAME :"+cursor1.getString(cursor1.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME))+"\n")
.append("时间 : "+cursor1.getString(cursor1.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN))+"\n");
contents.add(builder.toString());
}
cursor1.close();

Settings

Settings与XXXContract不同,它是通过xml文件来存储数据,在文件/data/system/users/0/目录下,获取设置的方式如下:

1
2
3
4
5
6
7
8
9
10
StringBuilder builder = new StringBuilder();
ContentResolver contentResolver=getActivity().getContentResolver();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
builder.append("wifi是否开启:"+Settings.Global.getString(contentResolver,Settings.Global.WIFI_ON)+"\n")
.append("数据流量是否开启:"+Settings.Global.getString(contentResolver,Settings.Global.DATA_ROAMING)+"\n")
.append(Settings.System.NOTIFICATION_SOUND+" "+Settings.System.getString(contentResolver,Settings.System.NOTIFICATION_SOUND)).append("\n")
.append(Settings.System.SCREEN_BRIGHTNESS+" "+Settings.System.getString(contentResolver,Settings.System.SCREEN_BRIGHTNESS)).append("\n")
.append(Settings.System.TEXT_SHOW_PASSWORD+" "+Settings.System.getString(contentResolver,Settings.System.TEXT_SHOW_PASSWORD)).append("\n");
}
Log.d("==============",builder.toString());

更多关于Setting的内容可以看Android系统APP之SettingsProvider

注意:以上的操作都是需要申请权限的

异步处理

如果请求数据的操作太过耗时,可能造成ANR,因此需要异步处理。异步处理的方式有:

  • Thread + Handler
  • AsyncTask
  • Loader
  • RxJava

参考文章:

  • https://juejin.im/entry/5bfcf8566fb9a049ba414362
  • https://blog.csdn.net/myfriend0/article/details/59107989

java内存溢出异常

发表于 2019-05-22

java内存区域

发表于 2019-05-22 | 分类于 jvm

java内存区域

程序计数器

程序计数器占用一小块内存空间,用来指示当前线程所执行的字节码行号。字节码解释器工作时就是通过改变
这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都
需要依赖这个计数器来完成。

每一条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为
“线程私有”的内存。

如果线程执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果正在执行的是native
方法,这个计数器值则为空。此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

虚拟机栈

虚拟机栈和程序计数器一样也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型:每个方法在执行的同时
都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程中就对应着一个栈帧
在虚拟机栈中入栈到出栈的过程。

经常有人把Java内存分为堆内存和栈内存;这里所指的栈是指虚拟机栈或者虚拟机栈中的局部变量表部分

局部变量表存放了编译期可知的各种基本数据类型、对象引用(reference类型,它不等同于对象本身,可能是一个指向对象起始地址的引用指针,有可能是指向一个代表对象的句柄或其他与此对象相关
的位置)和returnAddress类型(指向了一条字节码指令的地址)

本地方法栈

虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的native方法服务

Java堆

对于大多数应用来说,Java堆是Java虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,几乎
所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的主要区域,因此很多时候也被称为GC堆

当前主流的虚拟机都是按照可扩展来实现的(通过-Xmx和-Xms控制)如果在堆中没有内存完成实例分配,并且堆也无法再扩展
时,将抛出OutOfMemoryError异常

方法区

方法区是各个线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

运行时常量池

运行时常量池是方法区的一部分。一般来说,除了保存Class文件中描述的符号引用外,还会把翻译出来的直接引用也存储在运行时常量池中

注意:并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用
的比较多的是String类的intern()方法

直接内存

直接内存并不是虚拟机运行时内存的一部分,也不是jvm规范中定义的内存区域。它是由本机直接分配内存,不受Java堆大小的限制(但是
受本机总线程的限制)。

应用场景:NIO

注意:在为服务器虚拟机配置参数时,不能忽略这部分内存,否则可能引发OutOfMemoryError异常

HotSpot虚拟机在Java堆中对象的分配、布局和访问的全过程

《图解HTTP》核心总结

发表于 2019-05-22 | 分类于 计算机网络

转载《图解HTTP》核心总结

HTTP协议的简介

HTTP是超文本传输协议,用于客户端和服务器端之间的通信,属于TCP/IP中的应用层。

HTTP协议的基础知识

客户端和服务器端

客户端是服务请求方,服务器端是服务提供方。

URI和URL

URI:URI是统一资源标识符;

URL:是统一资源定位器;

URI的格式(了解即可)

[scheme:][//user:password@]host[:port][/path][?query][#fragment]

如上是URI的具体格式,下面介绍其意义:

  • scheme::是协议方案,比如http:,https:,file:等,此项可选可不选
  • [//[user:password@]:指定用户名和密码作为从服务器获取资源时必要的登陆信息,此项是可选项
  • host:服务器地址,例如www.runoob.com
  • [:port]:服务器端口号,例如:8080,此项是可选项
  • [/path]:指定服务器上的文件路径来定位特指的资源
  • [?query]:查询字符串,例如?id=123&pas=123
  • [#fragment]:片段标识符

DNS

见DNS基础知识

HTTP的特点

HTTP通过请求和响应来达成通信

如图:

HTTP 是无状态协议

HTTP 是无状态协议,它不对之前发生过的请求和响应的状态进行管理。也就是说,无法根据之前的状态进行本次的请求处理。

假设要求登录认证的 Web 页面本身无法进行状态的管理(不记录已 登录的状态),那么每次跳转新页面不是要再次登录,就是要在每次 请求报文中附加参数来管理登录状态。

HTTP使用 Cookie 的状态管理,Cookie 技术通过在请求和响应报文中写入 Cookie 信 息来控制客户端的状态。

Cookie 会根据从服务器端发送的响应报文内的一个叫做 Set-Cookie 的首部字段信息,通知客户端保存Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入 Cookie 值后发送出 去。服务器端发现客户端发送过来的 Cookie 后,会去检查究竟是从哪一 个客户端发来的连接请求,然后对比服务器上的记录,最后得到之前 的状态信息。

如下图:

HTTP报文

HTTP报文是HTTP请求和响应的单位。用于请求的报文叫做请求报文,而用于响应的报文叫做
响应报文。

报文的结构如图

报文主体和实体主体的差异 :

实体作为请求或响应的有效载荷数据(补充项)被传输,其内容由实体首部和实体主体组成。HTTP 报文的主体用于传输请求或响应的实体主体。
通常,报文主体等于实体主体。只有当传输中进行编码操作时,实体 主体的内容发生变化,才导致它和报文主体产生差异

HTTP首部字段的结构

首部字段名: 字段值

如果有多个值,则首部字段名: 字段值1, 字段值2

如果HTTP首部字段重复了会如何?

当HTTP报文首部中出现中出现了两个或两个以上具有相同首部字段名时会怎么样?这种情况
在规范中尚未明确,根据浏览器内部处理逻辑的不同,结果可能并不一致。

HTTP首部字段分为四个部分:

  • 通用首部字段:请求报文和响应报文两方都会使用的首部字段
  • 请求首部字段:从客户端向服务器端发送请求报文时使用的首部。补充了请求的附加内容、客户端信息、响应内容相关优先级等信息。
  • 响应首部字段:从服务器端向客户端返回响应报文时使用的首部。补充了响应的附加内容,也会为客户端附加额外的内容信息
  • 实体首部字段:针对请求报文和响应报文的实体部分使用的首部。补充了资源内容的更新时间等与实体有关的信息

实例分析

现在以请求菜鸟教程的报文为例来进行分析:

请求报文

1
2
3
4
5
6
7
8
9
10
11
GET /HTTP/1.1        //请求行
Host: www.runoob.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Referer: https://cn.bing.com/
Connection: keep-alive
Cookie: Hm_lvt_3eec0b7da6548cf07db3bc477ea905ee=1554627142,1556799533; _ga=GA1.2.579069872.1554627144; Hm_lpvt_3eec0b7da6548cf07db3bc477ea905ee=1556799533; _gid=GA1.2.1514513796.1556799533; _gat_gtag_UA_84264393_2=1
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0

响应报文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
HTTP/1.1 200 OK      //状态行
Server: Tengine
Content-Type: text/html
Content-Length: 150124
Connection: keep-alive
Date: Thu, 02 May 2019 09:27:08 GMT
Content-Encoding: gzip
Vary: Accept-Encoding
X-M-Log: QNM:xs446;QNM3:1
X-M-Reqid: jVAAADsHtCKO05oV
X-Powered-By: HHVM/3.28.1
X-Qnm-Cache: Hit
Ali-Swift-Global-Savetime: 1556270140
Via: cache16.l2cn1807[0,200-0,H], cache37.l2cn1807[1,0], cache9.cn1066[0,200-0,H], cache5.cn1066[1,0]
Age: 10328
X-Cache: HIT TCP_MEM_HIT dirn:11:466942761
X-Swift-SaveTime: Thu, 02 May 2019 10:36:54 GMT
X-Swift-CacheTime: 86400
Timing-Allow-Origin: *
EagleId: db971b1915567995564074785e

请求行和状态行

请求行:请求行中包含用于请求的方法,请求的URI和HTTP版本

状态行:状态行包含响应结果的状态码,原因短语和HTTP版本

在请求报文中GET /HTTP/1.1是请求行,其中GET代表请求的方法,/HTTP/1.1代表HTTP版本,在这个请求中未出现请求的URI。

在响应报文中HTTP/1.1 200 OK是状态行,其中HTTP/1.1是请求的版本,200是响应结果的状态码,而OK
则是原因短语。

请求的方法

GET:GET方法用来访问以被URI识别的资源。指定的资源经服务器解析后返回响应的内容。如下图

POST: POST方法用来传输实体的主体。如下图

其他方法如PUT HEAD DELETE OPTIONS TRACE CONNECT一般不使用。下面简单介绍一下:

方法 作用 不使用的原因
PUT 用来传输文件 PUT方法不采用任何验证机制,存在安全问题
HEAD 和GET方法一样,不过只返回响应首部 —
DELETE 用来删除文件 DELETE不采用任何验证机制,存在安全问题
OPTIONS 查询针对请求URI指定的资源支持的方法 —-
TRACE 让WEB服务器端将之前的请求通信返回给客户端 容易引发跨站追踪(XST)攻击
CONNECT 要求在与代理服务器通信时建立隧道,实现用隧道协议进行TCP通信 —-

状态码

2XX

200 ok

表示从客户端发来的请求在服务器端被正常处理了

204 Not Content

该状态码代表服务器接收的请求已经被成功处理,但是返回的响应报文中不包含实体的主体部分

206 Partial Content

该状态码表示客户端进行了范围请求,而服务器成功执行了这部分的GET请求

3XX

301 Moved Permanently

永久性重定向,该状态码表示请求的资源已被分配到新的URI,以后应该
使用现在所指的URI。

302 Found

临时重定向。该状态码表明请求的资源已被分配了新的URI,希望用户本次能使用新的URI访问。

303 See Other

该状态码表示由于请求资源存在另一个URI,应该使用GET方法定向获取请求的资源

304 Not Modified

该状态码表示客户端发送附带条件请求时,服务器端允许请求访问资源,但因为请求未满足条件的情况后,
直接返回304.

307 Temporary Redirect

临时重定向,和302相似。但是307不会从Post变成GET

4XX

400 Bad Request

该状态码表示请求报文中存在语法错误。当错误发生时,需要修改请求的内容后再次发送请求

401 Unauthorized

该状态码表示发送的请求需要通过HTTP认证的认证信息。另外若之前已经进行过一次请求,则表示用户认证失败

403 Forbidden

该状态码表明对请求资源的访问被服务器拒绝了

404 Not Found

该状态码表明服务器上无法找到请求的资源

5XX

500 Internal Server Error

该主题码表示服务器在执行请求时发生了错误

503 Service Unavailable

该状态码表明服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。

HTTP所有的首部字段

HTTP/1.1定义了如下47种首部地段,如下

通用首部字段:

请求首部字段:

响应首部字段:

实体首部字段:

非HTTP/1.1首部字段(不属于47种中的字段):Cookie、 Set-Cookie、 Content-Disposition等。

端到端首部和逐跳首部

HTTP将使用 缓存代理和非缓存代理 的首部字段分成两种类型:端到端首部(End-to-end Header)、逐跳首部(Hop-by-hop Header)

缓存代理

代理转发响应时,缓存代理(Caching Proxy)会预先将资源的副本 (缓存)保存在代理服务器上。
当代理再次接收到对相同资源的请求时,就可以不从源服务器那里获 取资源,而是将之前缓存的资源作为响应返回。

透明代理

转发请求或响应时,不对报文做任何加工的代理类型被称为透明代理 (Transparent Proxy)。反之,对报文内容进行加工的代理被称为非 透明代理。

1
注意:这里的非缓存代理不等于透明代理;它们划分的标准不一样。`缓存代理`和`非缓存代理`是通过是否使用缓存来划分的;而`透明代理`和`非透明代理`是通过是否会修改报文来划分。

端到端首部:分在此类别中的首部会转发给请求/响应对应的最终接收目标(即全程有效),且必须保存在由缓存生成的响应中,另外规定它必须被转发

逐跳首部:分在此类别的首部只对单次转发有效,会因为通过缓存或代理而不再转发。在HTTP/1.1和以后版本中,如果要使用逐跳首部,需要通过Connection首部字段

逐跳首部字段为:

  • Connection
  • Keep-Alive(新增字段,可忽略)
  • Proxy-Authenticate
  • Proxy-Authorization
  • Trailer
  • TE
  • Transfer-Encoding
  • Upgrade

其他字段都为端到端首部

通用首部字段

在实例的请求报文中出现的通用首部

1
2
Connection: keep-alive
Cache-Control: max-age=0

在实例的响应报文中出现的通用首部

1
2
3
Connection: keep-alive
Date: Thu, 02 May 2019 09:27:08 GMT
Via: cache16.l2cn1807[0,200-0,H], cache37.l2cn1807[1,0], cache9.cn1066[0,200-0,H], cache5.cn1066[1,0]

Cache-Control

通过指定首部字段Cathe-Control的指令,就可以操作缓存的工作机制,如图:

请求报文中Cache-Control: max-age=0表示缓存服务器通常需要将请求转发给源服务器。

Connection

Connection的作用:

  1. 控制不再转发给代理的首部字段
  2. 管理持久化连接
控制不再转发给代理的首部字段

代理服务器的基本行为就是接收客户端发送的请求后转发给其他服务器。代理不改变请求 URI,会直接发送给前方持有资源的目标服务器。上面介绍的缓存代理是代理服务器作用的一种。

管理持久化连接
1
2
Connection: close //表明服务器想断开连接
Connection: Keep-Alive //保持持久化连接(默认)

Via

用来追踪客户端与服务器端之间的请求和响应报文的传输路径。

如Via: cache16.l2cn1807[0,200-0,H], cache37.l2cn1807[1,0], cache9.cn1066[0,200-0,H], cache5.cn1066[1,0]

Date

首部字段Date表明创建HTTP报文的时间,如Date: Thu, 02 May 2019 09:27:08 GMT就表示创建报文的时间为2019年5月2日,星期二。

Pragma

Pragma是历史遗留字段,仅作为与HTTP/1.0的向后兼容而定义。

Trailer

用来说明在报文主体后记录了哪些首部字段

Transfer-Encoding

规定传输报文主体时采用的编码方式(仅对分块传输编码有效)

Upgrade

用来检测HTTP协议及其他协议是否可使用更高的版本进行通信。

Warning

告诉用户一些与缓存有关的问题的警告

请求首部字段

请求报文(去除了通用首部字段)

1
2
3
4
5
6
7
8
9
Host: www.runoob.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate, br
Referer: https://cn.bing.com/
Cookie: Hm_lvt_3eec0b7da6548cf07db3bc477ea905ee=1554627142,1556799533; _ga=GA1.2.579069872.1554627144; Hm_lpvt_3eec0b7da6548cf07db3bc477ea905ee=1556799533; _gid=GA1.2.1514513796.1556799533; _gat_gtag_UA_84264393_2=1
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0

Host

告诉服务器请求的资源所处的互联网主机名和端口号,如Host: www.runoob.com。Host是唯一必须包含在请求内的首部字段

User-Agent

用来将创建请求的浏览器和用户代理名称等信息传达给服务器。

例如上面的请求首部:

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:66.0) Gecko/20100101 Firefox/66.0

Accept

通知服务器可以处理的媒体类型及媒体类型的相对优先级

例如Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

媒体类型

文本文件

text/html, text/plain, text/css …
application/xhtml+xml, application/xml …

图片文件

image/jpeg, image/gif, image/png …

视频文件

video/mpeg, video/quicktime …

应用程序使用的二进制文件

application/octet-stream, application/zip

优先级

使用q=来额外表示权重值,用;进行分隔。权重值的范围是0~1(可精确到小数点后3位),且1为最大值。不指定
权重值时,默认权重为q=1.0.当服务器提供多个内容时会首先返回权重值最高的媒体类型。

Accept-Charset

Accept-Charset首部字段可用来通知服务器用户代理支持的字符集及字符集的相对优先顺序。另外可一次性指定多种字符集。与首部字段
Accept相同的可用权重q值来表示相对优先级

Accept-Encoding

Accept-Encoding首部字段用来告知服务器用户代理支持的内容编码及内容编码的优先级顺序。可一次性指定多种内容编码,可用权重q来指定优先级
例如Accept-Encoding: gzip, deflate, br

Accept-Language

首部字段Accept-Language用来告诉服务器用户代理能够处理的语言,可以指定多个语言,可以使用q来决定优先级。
例如Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2

Authorization

用来告诉服务器,用户代理的认证信息

Expect

告知服务器期望出现某种特定的行为

From

告诉服务器使用用户代理的用户的电子邮箱地址

If-Match

告诉服务器匹配资源所用的实体标记(ETag)值,服务器会对比If-Match的字段值和资源的ETag值,仅
当两者一致时才执行请求.反之则返回状码412

If-Modified-Since

告诉服务器若If-Modified-Since字段值早于资源更新的时间,则希望能处理该请求。
反之如果在If-Modified-Since指定的时间日期之后,如果请求的资源都没有更新,则
返回的状态码 304

If-None-Match

与If-Match相反

If-Range

告诉服务器若指定的If-Range字段值(ETag值或者时间)和请求资源的ETag值或时间相一致时,则作为范围请求处理。
反之,则返回全体资源。如图:

If-Unmodified-Since

与If-Modified-Since作用相反

Range

对于只需获取部分资源的范围请求,包含首部字段Range即可告知服务器资源的指定范围。
接收到附带Range首部字段请求的服务器,会在处理请求之后返回206.无法处理该范围的
请求时,则会返回200的响应及全部资源。

Referer

告诉服务器请求的原始URI

例如:Referer: https://cn.bing.com/说明我是从微软必应请求的

Upgrade-Insecure-Requests

Upgrade-Insecure-Requests是一个请求首部,用来向服务器端发送信号,表示客户端优先选择加密及带有身份验证的响应。不属于47种报文首部字段。

TE

告诉服务器,客户端能够处理的传输编码方式及相对优先级

Transfer-Encoding,TE,Content-Encoding,Accept-Encoding四个首部字段的区别

  • Transfer-Encoding:用于指定传输报文主体时使用的编码方式,属于逐跳首部,即只在两个节点间有效。
  • TE:用于告知服务器客户端能够处理的编码方式和相对优先级,属于逐跳首部,即只在两个节点间有效。
  • Content-Encoding:用于指定报文主体已经采用的编码方式,属于端到端首部,即在整个传输过程中有效。
  • Accept-Encoding:用于告知服务器客户端能够处理的编码方式和相对优先级,属于端到端首部,即在整个传输过程中有效。

很显然,Transfer-Encoding和TE是一组,Content-Encoding和Accept-Encoding是一组。
根本区别在于,Content-Encoding和Accept-Encoding限制的是报文主体在整个传输过程中使用的编码方式,全局有效;Transfer-Encoding和TE限制的是报文主体在两个节点间(源服务器和代理服务器之间、代理服务器和客户端之间等)传输时使用的编码方式,只在两节点间有效。

转自区分Transfer-Encoding,TE,Content-Encoding,Accept-Encoding四个首部字段

响应首部字段

响应报文

1
2
3
4
5
6
7
8
9
10
11
12
13
Server: Tengine
Vary: Accept-Encoding
X-M-Log: QNM:xs446;QNM3:1
X-M-Reqid: jVAAADsHtCKO05oV
X-Powered-By: HHVM/3.28.1
X-Qnm-Cache: Hit
Ali-Swift-Global-Savetime: 1556270140
Age: 10328
X-Cache: HIT TCP_MEM_HIT dirn:11:466942761
X-Swift-SaveTime: Thu, 02 May 2019 10:36:54 GMT
X-Swift-CacheTime: 86400
Timing-Allow-Origin: *
EagleId: db971b1915567995564074785e

Accept-Ranges

首部字段Accept-Ranges是用来告诉客户端服务器是否能处理范围请求,以指定获取服务器端
某个部分的资源。可以指定的值为bytes(表示接受范围请求)和none(表示不接受范围请求).

Age

告诉客户端,源服务器在多久前创建了响应。字段值的单位为秒。

ETag

主要用于检验缓存是否改变。它告诉客户端实体标识,是一种可将资源以字符串的形式做唯一标识的方式。服务器会为每份资源分配对应的ETag值

另外当资源更新时,ETag值也需要更新。当资源被缓存时,就会被分配唯一的标识。若在下载过程中
出现连接的中断,再连接的情况,都会依照ETag值来指定资源。

ETag中有强ETag值和弱ETag值

强ETag值,无论实体发生多么细微的变化都会改变其值。

弱ETag值,只用于提示资源是否相同。只有资源发生了根本改变,产生差异时才会改变ETag值。这时,
会在字段值最开始处附加 W/

Location

将响应接受方引领到某个与请求URI位置不同的资源。基本上,该字段会配合3XX的响应,提供重定向的URI。

Proxy-Authenticate

会把由代理服务器所要求的认证信息发生给客户端

Retry-After

告诉客户端应该在多久之后再发送请求。主要配合状态码503响应或3xx响应一起使用。字段值可以指定为具体的日期时间,也可以是创建响应后的秒数

Server

告诉客户端当前服务器上安装的HTTP服务器应用程序的信息

Vary

对缓存进行控制。具体见这里

WWW-Authenticate

用于HTTP访问认证

实体首部字段

Allow

告诉客户端支持的请求方法

Content-Encoding

告知客户端服务器对实体的主体部分选用的内容编码方式。内容编码是指在不丢失实体信息的前提下所进行的压缩

主要采用以下4种内容编码的方式

  • gzip
  • compress
  • deflate
  • identity

Content-Language

告诉客户端,实体使用的语言

Content-Length

表明实体主体部分的大小,单位是字节。对实体主体进行内容编码传输时,不能再使用Content-Length
首部字段

Content-Location

给出报文主体部分相对应的URI。和首部字段Location不同,Content-Location表示的是报文主体返回资源对应的URI

Content-MD5

首部字段 Content-MD5 是一串由 MD5 算法生成的值,其目的在于检 查报文主体在传输过程中是否保持完整,以及确认传输到达。

Content-Range

针对范围请求,返回响应时使用的首部字段Content-Range,能告诉客户端作为响应返回的实体的哪个部分
符合范围请求。字段值以字节为单位,表示当前发送部分及整个实体大小。

Content-Type

说明实体主体内对象的媒体类型,和首部字段Accept一样,字段值用type/subtype形式赋值

Http Header里的Content-Type一般有这三种:

  • application/x-www-form-urlencoded:数据被编码为名称/值对(这是标准的编码格式)。
  • multipart/form-data: 数据被编码为一条消息,页上的每个控件对应消息中的一个部分。
  • text/plain: 数据以纯文本形式(text/json/xml/html)进行编码,其中不含任何控件或格式字符(postman软件里标的是RAW)

form的enctype属性为编码方式,常用有两种:application/x-www-form-urlencoded和multipart/form-data,默认为application/x-www-form-urlencoded。

当action为get时候,浏览器用x-www-form-urlencoded的编码方式把form数据转换成一个字串(name1=value1&name2=value2...),然后把这个字串追加到url后面,用?分割,加载这个新的url。

当action为post时候,浏览器把form数据封装到http body中,然后发送到server。 如果没有type=file的控件,用默认的application/x-www-form-urlencoded就可以了。 但是如果有type=file的话,就要用到multipart/form-data了。

当action为post且Content-Type类型是multipart/form-data,浏览器会把整个表单以控件为单位分割,并为每个部分加上Content-Disposition(form-data或者file),Content-Type(默认为text/plain),name(控件name)等信息,并加上分割符。

转自Http Header里的Content-Type

Expires

会将资源失效的日期告诉客户端

Last-Modified

指明资源最终修改的时间

为Cookie服务的首部字段

Set-Cookie

开始状态管理所使用的Cookie信息(响应首部字段)

Cookie

告诉服务器,当客户端想获取HTTP状态管理支持时,就会在请求中包含从服务器接受到的Cookie。接收到多个
Cookie时,同时可以以多个Cookie形式发送

其他首部字段

HTTP首部字段是可以自行扩展的。所以在WEB服务器和浏览器应用上,会出现各种非标准的首部字段

X-Frame-Options

属于响应首部,用于控制网站内容在其他web网站的Frame标签内的显示问题。其主要目的是为了防止
点击劫持攻击

有两个可指定的字段值
DENY:拒绝
SAMEORIGIN:仅在同源域名下的页面匹配时许可

X-XSS-Protection

属于响应首部,它是针对跨站脚本攻击(XSS)的一种对策,用于控制浏览器XSS防护机制开关。

字段值:
0:将XSS过滤设置为无效状态
1:将XSS过滤设置为有效状态

DNT

属于请求字段,意味拒绝个人信息被采集,是表示拒绝被精准广告追踪的一种方法

字段值
0:同意被追踪
1:拒绝被追踪

P3P

属于响应首部,通过利用 P3P(The Platform for Privacy Preferences,在线隐私偏好平台)技术,可以让 Web 网站上 的个人隐私变成一种仅供程序可理解的形式,以达到保护用户隐私的 目的。

协议中对X-前缀的废除

在HTTP等多种协议中,通过给非标准参数加上前缀X-,来区别于标准参数,并使那些非标准参数作为扩展变成可能(如上面的响应首部)。但是这种简单粗暴的做法没有任何好处,因此被提议停止。然而对已经在使用中的X-前缀来说,不应该要求其变更。

HTTP的缺点

  • 通信使用明文(不加密),内容可能被窃听
  • 不验证通信方的身份,因此有可能遭受伪装
  • 无法证明报文的完整性,所以内容可能被篡改

HTTP+ 加密 + 认证 + 完整性保护 =HTTPS

HTTPS 并非是应用层的一种新协议。只是 HTTP 通信接口部分用 SSL(Secure Socket Layer)和 TLS(Transport Layer Security)协议代 替而已。
通常,HTTP 直接和 TCP 通信。当使用 SSL 时,则演变成先和 SSL 通 信,再由 SSL 和 TCP 通信了。简言之,所谓 HTTPS,其实就是身披 SSL 协议这层外壳的 HTTP

在采用 SSL 后,HTTP 就拥有了 HTTPS 的加密、证书和完整性保护 这些功能。

SSL 是独立于 HTTP 的协议,所以不光是 HTTP 协议,其他运行在应 用层的 SMTP 和 Telnet 等协议均可配合 SSL 协议使用。可以说 SSL 是 当今世界上应用最为广泛的网络安全技术。

加密

加密和解密同用一个密钥的方式称为共享密钥加密(Common key crypto system),也被叫做对称密钥加密

共享密钥加密的困境

以共享密钥方式加密时必须将密钥也发给对方。可究竟怎样才能 安全地转交?在互联网上转发密钥时,如果通信被监听那么密钥 就可会落入攻击者之手,同时也就失去了加密的意义。另外还得 设法安全地保管接收到的密钥。

公开密钥加密使用一对非对称的密钥。一把叫做私有密钥 (private key),另一把叫做公开密钥(public key)。顾名思 义,私有密钥不能让其他任何人知道,而公开密钥则可以随意发 布,任何人都可以获得。

HTTPS 采用混合加密机制

HTTPS 采用共享密钥加密和公开密钥加密两者并用的混合加密 机制。若密钥能够实现安全交换,那么有可能会考虑仅使用公开 密钥加密来通信。

但是公开密钥加密与共享密钥加密相比,其处 理速度要慢。
所以应充分利用两者各自的优势,将多种方法组合起来用于通 信。在交换密钥环节使用公开密钥加密方式,之后的建立通信交 换报文阶段则使用共享密钥加密方式。

证明公开密钥正确性的证书

遗憾的是,公开密钥加密方式还是存在一些问题的。那就是无法证明 公开密钥本身就是货真价实的公开密钥。比如,正准备和某台服务器 建立公开密钥加密方式下的通信时,如何证明收到的公开密钥就是原 本预想的那台服务器发行的公开密钥。或许在公开密钥传输途中,真 正的公开密钥已经被攻击者替换掉了。

为了解决上述问题,可以使用由数字证书认证机构(CA,Certificate Authority)和其相关机关颁发的公开密钥证书

服务器会将这份由数字证书认证机构颁发的公钥证书发送给客户端,
以进行公开密钥加密方式通信。

公钥证书也可叫做数字证书或直接称 为证书。
接到证书的客户端可使用数字证书认证机构的公开密钥,对那张证书 上的数字签名进行验证,一旦验证通过,客户端便可明确两件事:

  • 认证服务器的公开密钥的是真实有效的数字证书认证机构。
  • 服务器的公开密钥是值得信赖的。

此处认证机关的公开密钥必须安全地转交给客户端。使用通信方式 时,如何安全转交是一件很困难的事,因此,多数浏览器开发商发布 版本时,会事先在内部植入常用认证机关的公开密钥。

HTTPS 的通信步骤

  • 步骤 1: 客户端通过发送 Client Hello 报文开始 SSL 通信。报文中包 含客户端支持的 SSL 的指定版本、加密组件(Cipher Suite)列表(所 使用的加密算法及密钥长度等)。
  • 步骤 2: 服务器可进行 SSL 通信时,会以 Server Hello 报文作为应
    答。和客户端一样,在报文中包含 SSL 版本以及加密组件。服务器的 加密组件内容是从接收到的客户端加密组件内筛选出来的。
  • 步骤 3: 之后服务器发送 Certificate 报文。报文中包含公开密钥证 书。
  • 步骤 4: 最后服务器发送 Server Hello Done 报文通知客户端,最初阶 段的 SSL 握手协商部分结束。
  • 步骤 5: SSL 第一次握手结束之后,客户端以 Client Key Exchange 报 文作为回应。报文中包含通信加密中使用的一种被称为 Pre-master secret 的随机密码串。该报文已用步骤 3 中的公开密钥进行加密。
  • 步骤 6: 接着客户端继续发送 Change Cipher Spec 报文。该报文会提 示服务器,在此报文之后的通信会采用 Pre-master secret 密钥加密。
  • 步骤 7: 客户端发送 Finished 报文。该报文包含连接至今全部报文的 整体校验值。这次握手协商是否能够成功,要以服务器是否能够正确 解密该报文作为判定标准。
  • 步骤 8: 服务器同样发送 Change Cipher Spec 报文。
  • 步骤 9: 服务器同样发送 Finished 报文。
  • 步骤 10: 服务器和客户端的 Finished 报文交换完毕之后,SSL 连接 就算建立完成。当然,通信会受到 SSL 的保护。从此处开始进行应用 层协议的通信,即发送 HTTP 请求。
  • 步骤 11: 应用层协议通信,即发送 HTTP 响应。
  • 步骤 12: 最后由客户端断开连接。断开连接时,发送 close_notify 报 文。上图做了一些省略,这步之后再发送 TCP FIN 报文来关闭与 TCP 的通信。
    在以上流程中,应用层发送数据时会附加一种叫做 MAC(Message Authentication Code)的报文摘要。MAC 能够查知报文是否遭到篡 改,从而保护报文的完整性。

SSL 和 TLS HTTPS 使用 SSL(Secure Socket Layer) 和 TLS(Transport Layer Security)这两个协议。 SSL 技术最初是由浏览器开发商网景通信公司率先倡导的,开发 过 SSL3.0 之前的版本。目前主导权已转移到 IETF(Internet Engineering Task Force,Internet 工程任务组)的手中。 IETF 以 SSL3.0 为基准,后又制定了 TLS1.0、TLS1.1 和 TLS1.2。TSL 是以 SSL 为原型开发的协议,有时会统一称该协议 为 SSL。当前主流的版本是 SSL3.0 和 TLS1.0。 由于 SSL1.0 协议在设计之初被发现出了问题,就没有实际投入 使用。SSL2.0 也被发现存在问题,所以很多浏览器直接废除了 该协议版本。

DNS详解

发表于 2019-05-22 | 分类于 计算机网络

见DNS基础知识

android的存储路径

发表于 2019-04-26 | 分类于 android

Android的储存

  • 内部储存
1
2
3
4
5
6
context.getFilesDir();
//创建/data/user/0/com.example.androidbase/files文件夹,随着应用位置的改变,位置可能改变
context.getCacheDir();
//创建/data/user/0/com.example.androidbase/cache文件夹当需要空间时,系统会自动删除其中的文件(先删除存在最长时间的)
Environment.getDataDirectory()
//使用Environment来获取

注意:读写上述的方法返回的文件对象是,不用申请权限,当用户卸载app时,系统会删除data目录下所有关于该app的文件

  • 外部储存的私有目录
1
2
3
4
context.getExternalCacheDir()://一般存放临时缓存数据,对应 清除缓存
context.getExternalFilesDir(String type)://一般放一些长时间保存的数据,对应 清除数据
//使用Environment来获取,需要获取权限来操作
Environment.getExternalStorageDirectory();

与上述的对应方法的不同点

  • 系统一般不会清理在里面的文件
  • 用户可能操作在里面的文件,使用时应该加个非空判断
  • 获取WRITE_EXTERNAL_STORAGE的其他应用可以操作该文件

注意: 随应用卸载后删除

  • type的取值
    type:(可以为null,为null时: /storage/emulated/0/Android/data/com.example.androidbase/files)
1
2
3
4
5
6
7
DIRECTORY_MUSIC, 为DIRECTORY_MUSIC时:/storage/emulated/0/Android/data/com.example.androidbase/files/Music
DIRECTORY_PODCASTS,
DIRECTORY_RINGTONES,
DIRECTORY_ALARMS,
DIRECTORY_NOTIFICATIONS,
DIRECTORY_PICTURES,
DIRECTORY_MOVIES.
  • 外部储存的公共目录
1
2
3
4
5
6
7
/**
* 需要申请权限
* Type的值看上面(不能为null),注意要创建文件时不能使用 File的 mkdir() 和 mkdirs() 方法,它们只会创建目录,可以使用
*createNewFile()或者不使用 都可以创建文件
*
*/
Environment.getExternalStoragePublicDirectory(String type);
  • 因为外部存储可能是不可用的,比如遇到SD卡被拔出等情况时。因此在访问之前应对其可用性进行检查。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*检查外部存储是否可以读写*/
public boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}

/* 检查外部存储是否可读 */
public boolean isExternalStorageReadable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state) ||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return true;
}
return false;
}
  • 查询剩余空间

如果事先知道想要保存的文件大小,可以通过执行getFreeSpace() or getTotalSpace() 来判断是否有足够的空间来保存文件,从而避免发生IOException。那些方法提供了当前可用的空间还有存储系统的总容量。然而,系统并不能保证可以写入通过getFreeSpace()查询到的容量文件, 如果查询的剩余容量比我们的文件大小多几MB,或者说文件系统使用率还不足90%,这样则可以继续进行写的操作,否则最好不要写进去。

并没有强制要求在写文件之前去检查剩余容量。我们可以尝试先做写的动作,然后通过捕获 IOException 。这种做法仅适合于事先并不知道想要写的文件的确切大小。例如,如果在把PNG图片转换成JPEG之前,我们并不知道最终生成的图片大小是多少。

  • 手动删除文件

在不需要使用某些文件的时候应删除它。删除文件最直接的方法是直接执行文件的delete()方法。

myFile.delete();

如果文件是保存在internal storage,我们可以通过Context来访问并通过执行deleteFile()进行删除

Context.deleteFile(fileName);

更详细的可以参考:

  • Android存储(1)– 你还在乱用Android存储嘛!!!
  • Android 存储路径你了解多少

java注解

发表于 2019-04-13 | 分类于 java

元注解

元注解是一种可以注解到注解上的注解,是一种基本的注解,元注解有:

1
2
3
4
5
@Retention :解释这个注解的存活时间
@Documented :将注解中的元素包含到Javadoc中去
@Target :指定注解的应用场景
@Inherited :指定注解是否会被子类使用,例如:重写父类的方法时,如果要重写的方法有注解,则重写的方法也会出现该注解
@Repeatable() :可重复(Java 1.8 才加进来的)

@Retention

@Retention取值如下:

  • RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。例如(@Override)
  • RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。 例如(@NotNull)
  • RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们.例如Retrofit2中的(@GET())

@Target

@Target的取值如下

1
2
3
4
5
6
7
8
ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
ElementType.CONSTRUCTOR 可以给构造方法进行注解
ElementType.FIELD 可以给属性进行注解
ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
ElementType.METHOD 可以给方法进行注解
ElementType.PACKAGE 可以给一个包进行注解(使用方法很特殊)
ElementType.PARAMETER 可以给一个方法内的参数进行注解
ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举如果不声明则可以应用于所有的注解

@Repeatable()

表示注解的值是否可以取多个

@Inherited

指定注解是否会被子类使用,比如如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解

@Documented

与文档有关的元注解,在生成javadoc文档的时候将该Annotation也写入到文档中

注解的定义

注解的属性

注解的属性也叫做成员变量。注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型

需要注意的是,在注解中定义属性时它的类型必须是 8 种基本数据类型外加 类、接口、注解及它们的数组。

java日期和时间api

发表于 2019-04-11 | 分类于 java

概述

java8之后推出了新的time api.

java.time包下有5个包组成(大部分人用到基础包和format包就足够了)

  • java.time – 包含值对象的基础包
  • java.time.chrono – 提供对不同的日历系统的访问
  • java.time.format – 格式化和解析时间和日期
  • java.time.temporal – 包括底层框架和扩展特性
  • java.time.zone – 包含时区支持的类

注意:java.time包下所有类都是不可变的、线程安全的

规范

java的Date和Time API规范要求Java使用的时间尺度为:

  • 每天86400秒
  • 每天正午与官方时间精准匹配
  • 在时间点上,以精确的方式与官方时间接近匹配

Instant

在java中,Instant表示时间线上的某一点。、该时间线的原点为:穿过伦敦格林威治皇家天文台的本初子午线所处时区 的1970年1月1日的午夜`

Instant的常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Instant now() //获取当前时间

long getEpochSecond()
long toEpochMilli()
int getNano()

Instant plusMillis(long millisToAdd)//返回当前时间增加指定毫秒数后的时间
Instant plusNanos(long millisToAdd)//返回当前时间增加指定纳秒数后的时间
Instant plusSeconds(long millisToAdd)//返回当前时间增加指定秒数后的时间

boolean isAfter(Instant otherInstant);
boolean instant.isBefore(Instant otherInstant);

MIN //最小值,代表原点前的十亿年
MAX //最大值,代表公元 1000 000 000年的12月31日

实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 public static void main(String[] args) throws Exception {
Instant instant = Instant.now();
System.out.println(instant.toString());
System.out.println(instant.getEpochSecond());//返回从原点到现在的秒数
System.out.println(instant.getNano());//当前一秒内的第几纳秒
System.out.println(instant.toEpochMilli());//返回从原点到现在的毫秒数
System.out.println(instant.plusMillis(10000));
System.out.println(instant.plusNanos(1000));;
System.out.println(instant.plusSeconds(1000));;
}

结果:

2019-04-11T01:38:22.067Z
1554946702
67000000
1554946702067
2019-04-11T01:38:32.067Z
2019-04-11T01:38:22.067001Z
2019-04-11T01:55:02.067Z

Duration

Duration是两个时刻之间的时间量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Instant start = Instant.now();
run();//延时方法
Instant end = Instant.now();
Duration t = Duration.between(start,end);
System.out.println(t.toDays());//获取时间经过的天数
System.out.println(t.toHours());//获取时间经过的小时数
System.out.println(t.toMinutes());//获取时间经过的分钟数
System.out.println(t.getSeconds());//获取时间经过的秒数
System.out.println(t.toMillis());//获取时间经过的毫秒数
System.out.println(t.toNanos());//获取时间经过的纳秒数
System.out.println(t.toString());
System.out.println(t.getNano());//和Instant.getNano()方法相同

Duration plus = t.plusDays(1);//在当前Duration加上指定天数的时间,其他plusXXX()不再介绍
Duration min = t.minusDays(1);//在当前Duration减去指定天数的时间,其他minusXXX()不再介绍

本地时间

转载java 8的java.time包(非常值得推荐)

  • LocalDateTime:存储了日期和时间
  • LocalDate:只存储了日期
  • LocalTime:只存储了时间,如:14:02:43.455。(后面的.455表示毫秒值的最后三位,使用.withNano(0)可把毫秒值设为0)

java.time.LocalDateTime

此类显示的是年月日时分秒(默认的格式为:2017-01-01T01:01:01.555)

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
LocalDateTime now = LocalDateTime.now();
System.out.println(now.toString());
System.out.println(now.getYear());
System.out.println(now.getMonthValue());
System.out.println(now.getDayOfMonth());
System.out.println(now.getHour()); //24小时制
System.out.println(now.getMinute());
System.out.println(now.getSecond());
System.out.println(now.getNano()); //毫秒值的后三位作为前三位后面补6个零

打印的结果为:
2017-03-21T20:26:18.317
2017
3
21
20
26
18
317000000

//能够自定义时间
LocalDateTime time = LocalDateTime.of(2017, 1, 1, 1, 1,1);
System.out.println(time); //2017-01-01T01:01:01

//使用plus方法增加年份
LocalDateTime time = LocalDateTime.of(2017, 1, 1, 1, 1,1);

//改变时间后会返回一个新的实例nextYearTime
LocalDateTime nextYearTime = time.plusYears(1);

System.out.println(nextYearTime); //2018-01-01T01:01:01

//使用minus方法减年份
LocalDateTime time = LocalDateTime.of(2017, 1, 1, 1, 1,1);
LocalDateTime lastYearTime = time.minusYears(1);
System.out.println(lastYearTime); //2016-01-01T01:01:01

//使用with方法设置月份
LocalDateTime time = LocalDateTime.of(2017, 1, 1, 1, 1,1);
LocalDateTime changeTime = time.withMonth(12);
System.out.println(changeTime); //2017-12-01T01:01:01

//判断当前年份是否闰年
System.out.println("isLeapYear :" + time.isLeapYear());

//判断当前日期属于星期几
LocalDateTime time = LocalDateTime.now();
DayOfWeek dayOfWeek = time.getDayOfWeek();
System.out.println(dayOfWeek); //WEDNESDAY

LocalDate

此类显示的是年月日(默认的格式为:2017-01-01)
用法与LocalDateTime类大致一样

LocalTime

此类显示的是时分秒和毫秒值的后三位(21:26:35.693)
用法与LocalDateTime类大致一样

TemporalAdjusters

TemporalAdjusters是调节器,通过with方法来调节时间

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
public static TemporalAdjuster ofDateAdjuster(UnaryOperator<LocalDate> dateBasedAdjuster);

public static TemporalAdjuster firstDayOfMonth();

public static TemporalAdjuster lastDayOfMonth()

public static TemporalAdjuster firstDayOfNextMonth();

public static TemporalAdjuster firstDayOfYear();

public static TemporalAdjuster lastDayOfYear();

public static TemporalAdjuster firstDayOfNextYear()

public static TemporalAdjuster firstInMonth(DayOfWeek dayOfWeek);

public static TemporalAdjuster lastInMonth(DayOfWeek dayOfWeek);

public static TemporalAdjuster dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek);

public static TemporalAdjuster next(DayOfWeek dayOfWeek);

public static TemporalAdjuster nextOrSame(DayOfWeek dayOfWeek);

public static TemporalAdjuster previous(DayOfWeek dayOfWeek);

public static TemporalAdjuster previousOrSame(DayOfWeek dayOfWeek);
1
2
LocalDate now = LocalDate.now();
System.out.println(now.with(TemporalAdjusters.firstDayOfMonth()));

DateTimeFormatter

转载java 8的java.time包(非常值得推荐)

此类的功能与SimpleDateFormat类的功能类似,此类也是线程安全的,在写成时间处理工具类时,可作为静态成员变量,而不用每次都new一个SimpleDateFormat实例,此类是用来创建日期显示的模板,然后对于日期的格式化和解析还是使用LocalDateTime等类的parse静态方法和format方法,其模板属性格式是和SimpleDateFormat一样的,请看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
G 年代标志符
y 年
M 月
d 日
h 时 (12小时制)
H 时 (24小时制)
m 分
s 秒
S 毫秒
E 星期几
D 一年中的第几天
F 一月中第几个星期(以每个月1号为第一周,8号为第二周为标准计算)
w 一年中第几个星期
W 一月中第几个星期(不同于F的计算标准,是以星期为标准计算星期数,例如1号是星期三,是当月的第一周,那么5号为星期日就已经是当月的第二周了)
a 上午 / 下午 标记符
k 时 (24小时制,其值与H的不同点在于,当数值小于10时,前面不会有0)
K 时 (12小时值,其值与h的不同点在于,当数值小于10时,前面不会有0)
z 时区

对于此类使用先来个简单的新旧api对比演示:

Date转String

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  //使用Date和SimpleDateFormat
SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("G yyyy年MM月dd号 E a hh时mm分ss秒");

String format = simpleDateFormat.format(new Date());

System.out.println(format);
//打印: 公元 2017年03月21号 星期二 下午 06时38分20秒


//使用jdk1.8 LocalDateTime和DateTimeFormatter
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter pattern =
DateTimeFormatter.ofPattern("G yyyy年MM月dd号 E a hh时mm分ss秒");
String format = now.format(pattern);
System.out.println(format);
//打印: 公元 2017年03月21号 星期二 下午 06时38分20秒

String转Date

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 //使用Date和SimpleDateFormat
SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

Date date = simpleDateFormat.parse("2017-12-03 10:15:30");

System.out.println(simpleDateFormat.format(date));
//打印 2017-12-03 10:15:30

//使用jdk1.8 LocalDateTime和DateTimeFormatter
DateTimeFormatter pattern =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

//严格按照ISO yyyy-MM-dd验证,03写成3都不行
LocalDateTime dt = LocalDateTime.parse("2017-12-03 10:15:30",pattern);

System.out.println(dt.format(pattern));
12…7
lichukuan

lichukuan

62 博客
12 分类
© 2019 lichukuan
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4