CSS 学习第三天

上一天内容回顾

css 属性笔试常问:

  • 加粗,倾斜,下划线
1
2
3
font-weight:blod;
font-style:italic;
text-decoration:underline;
  • 背景色,前景色
1
2
background-color:red;
color:red;
  • class 和 id 的区别

class 用于 css 的,id 用于 js 的。

1.class 页面上可以重复。id 页面上唯一,不能重复。
2.一个标签可以存在多个 class,之间用空格隔开。但是,只能存在一个 id。

  • 选择器

先说说 IE6 能够兼容的选择器:标签选择器,id 选择器,类选择器,后代,交集选择器,通配符。

1
2
3
4
5
6
7
p
#box
.spec
div p
div.spec
div,p
*

IE7 能够兼容的:儿子选择器,下一个兄弟选择器:

1
2
div>p
h3+p

IE8 能够兼容的:

1
2
ul li:first-child
ul li:last-child
  • css 的两个性质

1) 继承性:父类有的属性,子类继承;
2) 层叠性:层叠性是一种能力,能够处理冲突。当不同的选择器,对同一个标签的同一个样式,有不同的值,这时,听谁的?这时候就发生了冲突。css 有着严格的处理冲突的机制:

选择上了,数权重,(id 的数量,类的数量,标签的数量)。如果权重一样,谁写在后面听谁的。
没有选择上,通过继承影响,就近原则,谁描述的近听谁的。如果描述的一样近,比选择器权重,如果权重也一样,谁写在后面听谁的。

!important 标记

1
2
3
4
5
6
7
8
9
10
11
<style type="text/css">
p{
color:red !important;
}
#paral{
color:blue;
}
.spec{
color:green;
}
</style>

我们可以通过 !important 属性来提高权重。这也就意味着该权重无限大。

Note1: 书写该属性的时候,一定要保证书写正确,不能忘记书写感叹号,另外,不能将 !important 写在分号外面。
还有一点需要注意的是:!important 针对属性,而不是整个选择器。

Note2: !important 无法提升继承的权重,该是 0 还是 0

访问 ContentProvider

应用通过 ContentResolver 对象和拥有 ContentProvider 对象可自动处理跨进程通信。

注意:想要访问 ContentProvider,你需要在应用的清单文件中请求特定的权限。

例如,如果你想从 用户字典 ContentProvider 中获取字词以及其区域设置的列表,需要通过调用 ContentResovler.query()query() 方法会调用用户字典 ContentProvider 中定义的 ContentProvider.query() 方法。

1
2
3
4
5
6
7
8
// Queries the user dictionary and returns results
mCursor = getContentResolver().query(
UserDictionary.Words.CONTENT_URI, // The content URI of the words table
mProjection, // The column to return for each row
mSelectionClause, // Selection criteria
mSelectionArgs, // Selection criteria
mSortOrder // The sort order for the returned rows
);

内容 URI

内容 URI 用来在 ContentProvider 程序中标识数据的 URI.这个 URI 包括整个提供程序的符号名称和一个指向表名称的路径。此 URI 也是你通过 ContentResolver 访问 ContentProvider 的重要参数之一。

ContentResolver 对象会分析出 URI 的授权,并通过该授权与包含 ContentProvider 对象应用的系统表进行比较,比对无误后,ContentResolver 就可以将查询的参数分配给正确的 ContentProvider 了。

以上面的示例代码为例:

1
content://user_dictionary/words

其中,user_dictionary 就是 ContentProvider 提供的授权,words 字符串是表的路径。content:// 始终显示,并被解释为 内容 URI.

大部分 ContentProvider 允许你在 URI 的末尾追加 ID 值来访问表中具体行。例如下面的例子,我们可以检索 _ID 为 4 的行:

1
Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI, 4);

通过追加 ID,我们不光能够执行查询操作,还可以执行更新和删除操作。

注意:UriUri.Builder 类都分装了 根据字符串构建规范格式的 URI 对象的方法,我们可以直接拿来用。 ContentUris 类也封装了 可以将 ID 值轻松追加到 URI 后的方法。上面的示例代码中就是通过 ContentUris.withAppendId() 方法将 ID 追加到用户字典内容的 URI 后面的。

通过 ContentProvider 查询数据

想要从 ContentProvider 中查询数据,你需要执行以下操作:

1.请求对 ContentProvider 读取的读取访问权限;
2.定义将查询操作发送给提供程序的代码。

请求读取访问权限

你需要在清单文件中定义提供者程序准确的权限名称,这样用户在安装你的应用时,会隐式授予此权限。比如,用户字典提供程序 在其清单文件中定义了权限 android.permission.READ_USER_DICTIONARY,这样,如果你想要获取它的相关数据,你就需要请求该权限。

构建查询

在获取了相关权限后,我们需要来构建查询,看下面的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
// A "projection" defines the columns that will be returned for each row.
String[] mProjection = {
UserDictionary.Words._ID, // Contract class constant for the _ID column name.
UserDictionary.Words.WORD, // Contract class constant for the word column name.
UserDictionary.Words.LOCALE // Contract class constant for the locale column name.
};

// Defines a string a contain the selection clause.
String mSelectionClause = null;

// Initializes an array to contain selection arguments.
String[] mSelectionArgs = {""};

上面的代码中定义了一些访问 用户字典 ContentProvider 变量。

再来看看下面的代码,同样是以 用户字典 为例,告诉了我们如何使用 ContentResolver.query().

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

// Gets a word from the UI.
mSearchString = mSearchWord.getText().toString();

// Remember to insert code here to check for invalid or malicious input.

// If the word is the empty string, gets everything.
if (TextUtils.isEmpty(mSearchString)) {
// Setting the selection clause to null will return all words.
mSelctionClause = null;
mSelectionArgs[0] = "";
} else {
// Constructs a selection clause that matches the word that the user entered.
mSelectionClause = UserDictionary.Words.WORD + " = ?";
mSelectionArgs[0] = mSearchString;

// Moves the user's input string to the selection arguments.
mSelectionArgs[0] = mSearchString;
}

// Does a query against the table and returns a Cursor object.
mCursor = getContentReslover().query(
UserDictionary.Words.CONTENT_URI, // The content URI of the words table
mProjection, // The columns to renturn for each row
mSelectionClause, // Either nll, or the word the user entered
mSelectionArgs, // Either empty, or the string the user entered
mSortOrder // The sort order for the returned rows
);

// Some providers return null if an error occurs, others throw an exception.
if (null == mCursor) {
/**
* Insert code here to handle the error.Be sure not to use the cursor! You may want to
* call android.util.Log.e() to log this error.
*/

// If the Cursor is empty, the provider found no matches.
} else if (mCursor.getCount() < 1) {
/**
* Insert code here to notify the user that the search was unsuccessful.This isn't necessarily
* an error.You may want to offer the user the option to insert a new row, or re-type the
* serch term
*/

} else {
// Insert code here to do something with the result.
}

上面的查询就类似于下面的 SQL:

1
SELECT _ID, word, locale FROM words WHERE word = <userinput> ORDER BY word ASC;

防止恶意输入

如果 ContentProvider 提供的数据位于数据库中,那么我们在拼写 SQL 的时候,可能会被 SQL 注入:

我们来看下面的这个示例代码:

1
2
// 将用户的输入拼接成 SQL 的查询子句
String mSelectionClause = "var = " + mUserInput;

上面的这段代码会允许用户将恶意的 SQL 串连到 SQL 语句上。例如,用户可以为 mUserInput 输入 “nothing;DROP TABLE *;”,

那么就会生成这样的语句 var = nothing; DROP TABLE *;这样会给我们的应用带来灾难性的后果。

那么,我们应该如何避免此类问题的出现?

通过将 作为可替代参数的选择子句以及一个选择参数的数组。执行这样的操作,用户输入将会受到查询约束,而不是解释为 SQL 语句的一部分。这样就可以避免恶意的 SQL 注入。

下面是正确的写法:

1
String mSelectionClause = "var = ?";

选择参数数组:

1
String[] selectionArgs = {""};

将用户输入置于选择参数数组中:

1
2
// Set the selection argument to the user's input
selectionArgs[0] = mUserInput;

Note:即使我们的 ContentProvider 提供的数据不在数据库中,我们也应该将 作为可替换选择子句和选择参数数组来提供问号值作为首选方式。

显示查询结果

ContentResolver.query() 方法总是会返回一个 Cursor。我们可以通过使用 Cursor 方法,来访问查询结果中的相关数据信息。

但如果没有与查询条件相匹配的行,则提供程序会返回 Cursor.getCount() 为 0(空游标)的 Cursor 对象。

下面是将 ContentProvider 返回的 Cursor 通过 SimpleCursorAdapterListView 关联示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Defines a list of columns to retrieve from the Cursor and load into an output row
String[] mWordListColumns = {
UserDictionary.Words.WORD, // Contract class constant containing the word column name
UserDictionary.Words.LOCALE // Contract class constant containing the word column name
};

// Defines a list of view IDs that will receive the Cursor columns for each row
int[] mWordListItems = {R.id.dictword, R.id.locale};

// Creates a new SimpleCursorAdapter
mCursorAdapter = new SimpleCursorAdapter(
getApplicaitonContext(), // The application's Context object
R.layout.wordlistrow, // A layout in XML for one row in the ListView
mCursor, // The result from the query
mWordListColumns, //
mWordListItems, // An interger array of view IDs in the row layout
0
);


// Sets the adapter for the ListView
mWordList.setAdapter(mCursorAdapter);

从查询结果中获取数据

如果我们不光是想显示查询的数据,而是想从 Cursor 中做一些其他操作,你需要在 Cursor 中循环访问行:

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
// Determine the column index of the column named "word"
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);

/**
* Only executes if the cursor is valid. The User Dictionary Provider returns null
* if an internal error occurs. Other providers may throw an Excpetion instead of
* returning null.
*/

if (mCursor != null) {

/**
* Moves to the next row in the cursor. Before the first movement in the cursor,
* the "row pointer" is -1, and if you try to retrieve data at that position you
* will get an exception.
*/

while (mCursor.moveToNext) {

// Gets the value from the column.
newWord = mCursor.getString(index);

// Insert code here to process the retrieved word.

// ...

// end of while loop

}

} else {

// Insert code here to report an error if the cursor is null or the provider threw an exception.
}

Cursor 实现了包含了很多用于从对象中检索不同类型的数据的 “获取” 方法。例如,上面的示例代码中使用的 getString() 方法。另外,我们还可以通过 getType() 方法来获取指示列的数据类型的值。

内容提供程序权限

提供程序的应用可以指定第三方应用访问提供程序所必须的权限。这样,其他程序会请求访问提供程序所需的权限。如果,提供程序未提供任何权限,则其他应用无法访问提供程序的数据。当然这是针对不同进程的程序请求而言,对于在同一个进程的组件是具有完整的读写权限的。

比如,我们想要访问用户字典程序需要 android.permission.READ_USER_DICTIONARY 权限才能从中检索数据。
通过 ContentProvider 我们可以对其进行插入、更新,如果你想进行删除数据的操作,你还需要申请 android.permission.WRITE_USER_DICTIONARY 权限。

如何通过 ContentProvider 进行增删改操作

我们不但能够通过 ContentProvider 来进行查询操作,还能通过它来进行增删改操作。

我们通过调用 ContentResolver.insert() 方法来插入数据。它会插入数据后并返回内容 URI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Defines a new Uri object that receives the result of the insertion.
Uri mNewUri;

...

// Defines an object to contain the new values to insert
ContentValues mNewValues = new ContentValues();

mNewValues.put(UserDictionary.Words.APP_ID, "example.user");
mNewValues.put(UserDictionary.Words.LOCALE, "en_US");
mNewValues.put(UserDictionary.Words.WORD, "insert");
mNewValues.put(UserDictionary.Words.FREQUENCY, "100");

mNewUri = getContentResolver().insert(
UserDictionary.Words.CONTENT_URI, // the user dictionary content URI
mNewValues // the values to insert
);

我们不需要添加 _ID 列,系统会自动维护此列。ContentProvider 会向添加的每个行分配唯一的 _ID 值,并用以作为主键。

insert() 方法会放回一下形式的 URI 给我们:

1
content://user_dictionary/words/<id_value>

这个 <id_value> 就是新插入的行的 _ID 内容。

注意:要想从 Uri 中获取 _ID 的值,请调用 ContentUris.parseId().

通过 ContentResolver.update() 来执行更新操作。我么只需将需要 ContentValues 对象传入。而对于想要清除的字段内容,只需将值置为 null 即可。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Defines an object to contain the new values to insert
ContentValues mNewValues = new ContentValues();

// Defines selection criteria for the rows you want to update
String mSelectionClause = UserDictionary.Words.LOCALE + "LIKE ?";
String[] mSelectionArgs = {"en_%"};

// Defines a variable to contain the number of updated rows
int mRowsUpdated = 0;

...

/**
* Sets the updated value and updates the selected words.
*/

mUpdateValues.putNull(UserDictionary.Words.LOCALE);

mRowsUpdated = getContentResolver().update(
UserDictionary.Words.CONTENT_URI, // the user dictionary content URI
mUpdateValues, // the columns to update
mSelectionClause, // the columns to select on
mSelectionArgs // the value to compare to
);

注意: 你应该在 ContentResolver.update() 时检查用户输入,以防止 恶意输入

删除

删除操作与查询操作类似,我们只需要指定条件即可,delete() 方法会将 删除的行数 返回给我们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Defines selection criteria for the rows you want to delete
String mSelectionClause = UserDictionary.Words.APP_ID + "LIKE ?";
String[] mSelectionArgs = {"user"};

// Defines a variable to contain the number of row deleted
int mRowsDeleted = 0;

...


// Deletes the words that match the selection criteria
mRowsDeleted = getContentResolver().delete(
UserDictionary.Words.CONTENT_URI, // the user dictionary content URI
mSelectionClause, // the columns to select on
mSelectionArgs // the value to compare to
);

ContentProvider 数据类型

ContentProvider 可以提供不同类型的数据:

  • 整型
  • 长整型
  • 浮点型
  • 长浮点型(双倍)

ContentProvider 还会维护其定义的每个内容的 MIME 数据类型信息。在使用包含复杂数据结构或文件的 ContentProvider 时,通常需要 MIME 类型。例如,联系人的 ContentProvider 中的 ContactsContract.Data 表会使用 MIME 类型来标记每行中存储的联系人数据类型。想要获取与内容 URI 对应的 MIME 类型,请调用 ContentResolver.getType();

ContentProvider 的替代形式

ContentProvider 的三种替代形式在应用开发过程中很是重要:

  • 批量访问: 你可以通过 ContentProviderOperation 类中的方法创建一批访问调用,再通过 ContentResolver.applyBatch() 应用他们。
  • 异步查询: 你应该在单独线程中执行查询。执行方式之一是通过 CursorLaoder 对象。
  • 通过 Intent 访问数据: 虽然我们无法向 ContentProvider 发送 Intent,但是可以向 ContentProvider 的应用发送 Intent,后者通常具有修改 ContentProvider 数据的最佳配置。

批量访问

批量访问 ContentProvider 适用于插入大量行,或通过同一方法调用在多个表中插入行,或者用于跨进程界限将一组操作事务处理执行。

想要在 “批量模式” 下访问 ContentProvider,你可以创建 ContentProviderOperation 对象数组,然后使用 ContentResolver.applyBatch() 将其分派给内容提供程序。

我们需要将 ContentProvider授权 传递给此方法,而不是特定内容的 URI.这样可以使数组中的每个 ContentProviderOperation 对象都能适用于其他表。

ContactsContract.RawContacts 协定类的说明包括展示批量注入的代码段。

通过 Intent 访问数据

我们可以通过 Intent 对 ContentProvider 进行间接的访问。即使我们的应用不具备访问权限。我们可以从具有权限的应用中获取回结果 Intent,或者通过激活具有权限的应用,然后让用户在其中工作。

通过临时权限获取访问权限:

即便我们没有适当的访问权限,还是可以通过以下方式来访问 ContentProvider 中的数据:

将 Intent 发送至具有权限的应用,然后接受到包含 URI 权限的结果 Intent. 这些就是特定内容的 URI 权限,此权限的生命周期将持续到 接受该权限的组件结束为止。具有永久权限的应用将通过在结果 Intent 中设置标志来授予临时权限:

  • 读取权限FLAG_GRANT_READ_URI_PERMISSION
  • 写入权限FLAG_GRANT_WRITE_URI_PERMISSION

注意:这些标志不会为其授权包含在内容 URI 中的提供程序提供常规的读取或写入访问权限。访问权限仅适用于 URI 本身。

ContentProvider 使用 <provider> 元素的 android:grantUriPermission 属性以及 <provider> 元素的 <grant-uri-permission> 在清单文件中定义内容 URIURI 权限。详情参见《安全与权限》

举个例子,即使你没有 READ_CONTACTS 权限,也可以在联系人的 ContentProvider 中检索联系人的数据。比如说你希望在一个 向联系人发送电子生日祝福 的应用中执行此操作。你更希望让用户控制应用所使用的联系人,而不是请求 READ_CONTACTS,让你的应用能够访问用户所有的联系人和信息,这样做是不友好的,用户也很有可能因为此举而删掉你的应用。

如何做到呢?

1.使用 startActivityForResult() 发送包含操作 ACTION_PICK 和 联系人 MIME 类型 CONTENT_ITEM_TYPE 的 Intent 对象。

2.通过此 Intent 与 联系人 应用的 选择 ActivityIntent 过滤器相匹配,来显示 联系人 Activity.

3.用户在 联系人 界面上选择需要发送电子生日祝福的联系人。这样,该 Activity 会调用 setResult(resultcode, intent) 来设置用于返回的 Intent. Intent 包含用户选择的联系人的内容 URI,以及 extras 标志 FLAG_GRANT_READ_URI_PERMISSION。这些标志会为你的应用授予读取内容 URI 所指向的联系人的数据的 URI 权限。接着,联系人选择 Activity 会调用 finish() 以返回对应用的控制。

4.在 onActivityResult() 方法中,我们会接受到联系人应用选择界面所返回的 Intent.

5.通过 Intent 中的内容 URI,我们可以读取来自联系人 ContentProvider 的联系人数据,即使你未在清单文件中请求对该提供程序的永久读取访问权限。

6.最后,我们就可以在不获取联系人权限的前提下,向某个联系人发送生日祝福的功能了。

小结:这么麻烦,有必要吗?直接在清单文件中申请永久权限不就行了吗?用得着这么大费周章吗?
这么做是非常有必要的,你需要知道的是,用户对于 APP 申请读取敏感信息权限是非常反感的,这样做,很可能导致用户拒绝安装此应用,进而流失用户。

使用其他应用:

第二种方法允许用户修改你无权访问的数据的简单方法是激活具有权限的应用,让用户在其中执行工作。

例如,日历应用 接受 ACTION_INSERT Intent,这让你可以激活应用的插入 UI.你可以在此 Intent 中传递额外的数据,由于定期事件具有复杂的语法,因此将事件插入日历提供程序的首选方式是激活具有 ACTION_INSERT 的日历应用,然后让用户在其中插入事件。

协定类

协定类的作用是帮助应用使用内容 URI,列名称,Intent 操作以及内容提供程序的其他功能的常量。然而协定类未自动包含在提供程序中。开发者需要在 ContentProvider 中定义它们,然后使其可用于其他开发者。Android 平台中包含的许多提供程序都在软件包 android.provider 中具有对应的协定类。

例如,用户字典提供程序具有包含内容 URI 和 列名称常量的协定类 UserDictionary.字词表的内容 URI 在常量 UserDictionary.Words.CONTENT_URI 中定义。 UserDictionary.Words 类包含列名称常量,上面的示例代码就使用了相关常量。

联系人的 ContentProviderContactsContract 也是一个协定类。

MIME 类型引用

ContentProvider 可以返回标准的 MIME 媒体类型和 自定义 的 MIME 类型字符串。

MIME 类型具有的格式如下:

1
type/subtype

例如,text/htmlContentProvider 一旦返回此类型,则意味着使用该 URI 查询会返回包含 HTML 标记的文本。

自定义 MIME 类型(一般是特定供应商会使用)字符串具有更加复杂的类型和子类型,类型值始终为:

1
vnd.android.cursor.dir

上面表示 多行

1
vnd.android.cursor.item

上面表示 单行

子类型 特定于提供程序。Android 内置提供程序通常具有简单的子类型。例如,当联系人应用为电话号码创建行时,它会在行中设置以下 MIME 类型:

1
vnd.android.cursor.item/phone_v2

上面的子类型就是 phone_v2.

其他程序的开发者可能会根据提供程序的授权和表名称创建自己的子类型模式。例如,假设提供程序包含了列车时刻表。且授权是 com.example.trains,并包含表 Line1,Line2 和 Line3.在响应 Line1 的内容 URI

1
content://com.example.trains/Line1

时,提供程序会返回 MIME 类型:

1
vnd.android.cursor.dir/vnd.example.line1

在响应表 Line2 中的第五行的内容的 URI:

1
content://com.example.trains/Line2/5

时,会返回的 MIME 类型为:

1
vnd.android.cursor.item/vnd.example.line2

基本上大多数 ContentProvider 都会为其使用的 MIME 类型定义协定类常量。例如,联系人提供程序的协定类 ContactsContract.RawContacts 会为单个原始联系人行的 MIME 类型定义常量 CONTENT_ITEM_TYPE.

具体参见:《内容 URI》.

本文学习自:
https://developer.android.com/guide/topics/providers/content-provider-basics.html

文章有帮助到您?不妨打赏博主一碗拉面或一杯咖啡的小费吧 :-D!