Android开发的IPC主板模式设计
一、认识Android的IPC主板模式
系统架构设计最关键的任务就是组合(或称整合),而且最好是能与众不同、深具创新性组合。Android就擅用了主板模式,以通用性接口实践跨进程的IPC通信机制。由于Android是开源开放的系统,其源代码可成为大家观摩的范本。首先,其主板模式提供了IBinder通用性接口。如下图:
Android定义一个Binder父类来实现<通用性>的IBinder接口。如下图:
然后,以Java来撰写这个实现类,其Java代码如下:
// Android的源代码 // Binder.java // ------------------------------------------------------------- public class Binder implements IBinder { // .......... private int mObject; public Binder() { init(); // 其它代码 } public final boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { // 其它代码 boolean r = onTransact(code, data, reply, flags); return r; } private boolean execTransact(int code, int dataObj, int replyObj, int flags) { Parcel data = Parcel.obtain(dataObj); Parcel reply = Parcel.obtain(replyObj); boolean res; res = onTransact(code, data, reply, flags); // 其它代码 return res; } protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { } private native final void init(); } // End
这个Binder抽象父类的主要函数:
transact()函数-- 用来实作IBinder的transact()函数接口。
execTransact()函数-- 其角色与transact()函数是相同的,只是这是用来让C/C++本地程序来调用的。
onTransact()函数-- 这是一个抽象函数,让应用子类来覆写(Override)的。上述的transact()和execTransact()两者都是调用onTransact()函数来实现反向调用(IoC, Inversion of Control)的。
init()函数-- 这是一个本地(Native)函数,让JNI模块来实现这个函数。Binder()构造函数(Constructor)会调用这个init()本地函数。
这Binder.java是抽象类,它含有一个抽象)函数:onTransact()。于是,这个软件主板提供了两个接口:CI和接口。如下图:
这是标准型的主板模式。此图里的Binder抽象父类和两个接口,整合起来成为一个典型的软件主板。如下图:
这个Binder软件主板是用来整合两个进程里的软件模块(如类),所以我们称之为:。如下图:
基于这个主板,我们就能开始进行组合了。此时,可设计一个子类,并且装配到主板的接口上。如下图:
图 1 Binder进程间通信模型
Client和Server均通过函数ioctl与Binder驱动进行数据交互。ioctl是Linux中用于控制I/O设备的函数,提供了一种同时向设备发送控制参数和数据的手段。它是一个可变参数的函数,原型为:
<??#65533;"http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4KPHByZSBjbGFzcz0="brush:java;">int ioctl(int fd, int cmd, ...);
fd是打开/dev/binder设备后得到的文件描述符,cmd是对设备的控制命令。该函数执行成功返回0,否则返回-1。
3.相关数据结构
当函数ioctl的第二个参数cmd为BINDER_WRITE_READ时,表示向Binder驱动发送一条读取或者写入/dev/binder设备的命令,Binder驱动会将对设备的读写“翻译”为对共享
内存区的读写。这条命令是Client和Server进行进程间通信时最重要、使用最频繁的控制命令。
传入BINDER_WRITE_READ的同时,会传入一个binder_write_read结构体的指针作为ioctl的第三个参数,该结构中的read_buffer和write_buffer字段分别指向将要读取或者写入的缓冲区。这两个缓冲区中的数据都是以“数据类型+数据内容”的格式顺序存放的,而且多条不同类型的数据连续存放,如图2所示。write_buffer中数据类型以“BC_”开头,而read_buffer中数据类型以 “BR_”开头,图2中以write_buffer中的数据为例。在所有的数据类型中,又以BC(R)_REPLY和BC(R)_TRANSACTION 最为重要:通过BC_TRANSACTION/BC_REPLY这对命令,发送方将数据发往接受方;通过BR_TRANSACTION /BR_REPLY,接收方读取发送方发来的数据。
数据的内容是一个binder_transaction_data结构。
图2 Binder IPC中各数据结构的关系
binder_transaction_data结构是对进程间通信数据的封装,可以看作网络通信中的一个数据包。其中的 sender_uid,sender_pid成员变量指明了此数据发送方的用户ID和进程ID,buffer成员变量指向进程间通信最核心的有效负载数据,data_size是有效负载数据的长度。在Binder机制中,sender_uid和sender_pid是在内核中由Binder驱动填入的,无法被伪造,保证了身份标记的可靠性,由此可见Binder进程间通信机制进行是安全的。
4.Binder之间的数据行为关系
Client和Server使用Binder机制进行进程间通信时,通过分析Client发往Server的数据或者分析Server读取的Client的请求数据,便可以识别出Client的具体行为。
例如,当Client想要得到定位信息,请求LocationServer获取定位数据时,会访问LocationServer的ILocationManager接口,发往LocationServer中的有效负载数
据中包含“android.location.ILocationManager”字符串。所以我们分析LocationServer读取的Client发来的请求数据,判断其中是否包含“android.location.ILocationManager”,
我们就可以知道Client是否正在试图访问用户的地理位置信息。
由于Android系统中每个应用程序都有自己唯一的UID,因此根据binder_transaction_data中的sender_uid,我们就可以获取Client具体代表的应用程序。这样
就获得了具体软件的具体行为。
通过对Android系统的分析,我们发现:虽然系统提供的服务多达几十种,但是实际上只有三个Server进程负责管理
Android系统Server进程与管理的服务
服务进程 管理的服务
com.android.phone 与通信功能相关的短信、电话服务
mediaserver 与媒体功能相关的视频、音频服务
system_server 其他服务,如地理位置、蓝牙、网络连接、程序安装卸载等
最近在做一个Android的项目,其包含一个消息推送的后台服务。由于该服务可能会有重要的信息推送,因此并不希望当APP程序退出、APP程序被一键清理、APP被强制停止等用户操作发生时,这个后台服务也随之被杀死。这个问题也就是所谓的“内存永驻”。关于这个问题,网上有很多说法,如调用startforehand函数以提高service的优先级、在service中创建一个不能被删掉的notification(或者产生一个其他的与用户界面交互的UI控件)、在service的onDestroy函数中重启这个服务、修改onstartcommand函数的返回值等等。这些方法,笔者都一一试过,但都没有效果。但是,我们可以看到市面上也确实存在一些App在一定的时间后可以自动重启,说明仍然是存在方法可以完成这项任务的。
文章中介绍的方法涉及到Android的JNI编程,主要思想就是通过调用native函数创建一个子进程。父子进程相互监听,若子进程死去,父进程妥善处理后重新创建新的子进程;若父进程死去,子进程使用AM命令重启父进程。这种思想唯一的缺陷就是如何保证父子进程不被同时杀死的情况。子进程能不能被杀死,只能用实验来证明。
首先笔者按照文章介绍的,整理了代码,并将相关代码植入到自己的项目中。
步骤1)编写Watcher类。它为上面的Java程序调用提供必要的接口,声明需要native语言实现的的具体函数。native语言主要是指C/C++语言。上层的Java程序只需要创建一个Watcher类并调用它的createAppMonitor(String userId)函数即可。
public class Watcher { private static final String PACKET = "com.example.dameonservice"; private String mMonitoredService = "com.example.mqtt.MQTTSubscribeService"; private volatile boolean bHeartBreak = false; private Context mContext; private boolean mRunning = true; public void createAppMonitor(String userId) { if(!createWatcher(userId)) { Log.e("Watcher", "<<Monitor created failed>>"); } } public Watcher(Context context) { mContext = context; } /*创建一个监视子进程 *userId 当前进程的用户ID,子进程重启当前进程时需要用到当前进程的用户ID *return 若子进程创建成功返回TRUE,否则返回FALSE */ private native boolean createWatcher(String userId); /* 让当前进程连接到监视进程 * return 连接成功返回TRUE,否则返回FALSE */ private native boolean connectToMonitor(); /*向监视进程发送任意信息 * msg 发给monitor的信息 * return 实际发送的字节数 */ private native int sendMsgToMonitor(String msg); static { System.loadLibrary("monitor"); //这里要和后面的Android.mk中模块名对应 } }
2)编译上面的文件会在bin/classes 目录下生成相对应的Watcher.class文件,通过DOs界面进入该bin/classes 目录下,通过javah命令生成C/C++对应的头文件。
“javah 包名+类名” 得到以下头文件:
#include <jni.h>
/* Header for class
com_example_dameonservice_Watcher */
#ifndef _Included_com_example_dameonservice_Watcher
#define
_Included_com_example_dameonservice_Watcher
#ifdef __cplusplus
extern "C"
{
#endif
/*
* Class: com_example_dameonservice_Watcher
*
Method: createWatcher
* Signature:
(Ljava/lang/String;)Z
*/
JNIEXPORT jboolean JNICALL
Java_com_example_dameonservice_Watcher_createWatcher
(JNIEnv *, jobject,
jstring);
/*
* Class: com_example_dameonservice_Watcher
* Method:
connectToMonitor
* Signature: ()Z
*/
JNIEXPORT jboolean JNICALL
Java_com_example_dameonservice_Watcher_connectToMonitor
(JNIEnv *,
jobject);
/*
* Class: com_example_dameonservice_Watcher
* Method:
sendMsgToMonitor
* Signature: (Ljava/lang/String;)I
*/
JNIEXPORT jint
JNICALL Java_com_example_dameonservice_Watcher_sendMsgToMonitor
(JNIEnv *,
jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif
3)创建JNI文件夹,将得到的头文件移到该文件夹下,继续在该文件夹下创建与上面得到的头文件同名的C/C++文件,然后实现头文件中提到的方法。(具体实现太多,这里就不再贴出来了)
4)添加Android.mk文件。这个文件的格式基本是统一的。只需要修改LOCAL_MODULE和LOCAL_SRC_FILES两处即可。如果你还有添加Log打印函数,还要在这里添加 “LOCAL_LDLIBS := -lm -llog”。
下面一张图来说明整体的文件结构分布:
其中com_example_dameonservice_Watcher.c和com_example_dameonservice_Watcher.cpp内容相同。process.cpp定义一些辅助类。
实验结果:
这当然是大家最关心的。测试的手机选用的小米,感觉 小米在这一块的优化还是很不错的,所以用它来试试。最终的测试结果是:被杀死的服务概率性地可以重启成功,且失败的概率更大。通过Log分析,不能重启的时候是因为子进程也死掉了。截止到笔者写下这篇文章,还没有抓住其中的规律。一键清理和子进程的被杀死没有绝对的对应关系。而且即使是在App运行的时候,也会发现子进程会被杀死,然后又被父进程重启。子进程被杀死是重启失败的主要原因。但现在的现象无法确定子进程被杀死的确切原因,有一种可能是被系统杀死了,但这样的不确定性太大,对效果也不能有很好的保证。
虽然没有完美解决问题,但至少比前面的办法强很多,至少它也重启成功过。这个方法感觉继续优化一下还是可以做好的。
Android实现双进程守护
做过android开发的人应该都知道应用会在系统资源匮乏的情况下被系统杀死!当后台的应用被系统回收之后,如何重新恢复它呢?网上对此问题有很多的讨论。这里先总结一下网上流传的各种解决方案,看看这些办法是不是真的可行。
1.提高优先级
这个办法对普通应用而言,应该只是降低了应用被杀死的概率,但是如果真的被系统回收了,还是无法让应用自动重新启动!
2.让service.onStartCommand返回START_STICKY
通过实验发现,如果在adb shell当中kill掉进程模拟应用被意外杀死的情况(或者用360手机卫士进行清理操作),如果服务的onStartCommand返回START_STICKY,在eclipse的进程管理器中会发现过一小会后被杀死的进程的确又会出现在任务管理器中,貌似这是一个可行的办法。但是如果在系统设置的App管理中选择强行关闭应用,这时候会发现即使onStartCommand返回了START_STICKY,应用还是没能重新启动起来!
3.android:persistent="true"
网上还提出了设置这个属性的办法,通过实验发现即使设置了这个属性,应用程序被kill之后还是不能重新启动起来的!
4.让应用成为系统应用
实验发现即使成为系统应用,被杀死之后也不能自动重新启动。但是如果对一个系统应用设置了persistent="true",情况就不一样了。实验表明对一个设置了persistent属性的系统应用,即使kill掉会立刻重启。一个设置了persistent="true"的系统应用,在android中具有core service优先级,这种优先级的应用对系统的low memory killer是免疫的!
OK,说了半天,只有core service优先级的应用才能保证在被意外杀死之后做到立刻满血复活。而普通应用要想成为系统应用就必须要用目标机器的签名文件进行签名,但这样又造成了应用无法保证兼容所有不同厂商的产品。那么该怎么办呢?这里就来说一说双进程守护。网上也有人提到过双进程守护的办法,但是很少能搜索到类似的源码!如果从进程管理器重观察会发现新浪微博或者360卫视都有两个相关的进程,其中一个就是守护进程,由此可以猜到这些商业级的软件也采用了双进程守护的办法。
什么是双进程守护呢?顾名思义就是两个进程互相监视对方,发现对方挂掉就立刻重启!不知道应该把这样的一对进程是叫做相依为命呢还是难兄难弟好呢,但总之,双进程守护的确是一个解决问题的办法!相信说到这里,很多人已经迫切的想知道如何实现双进程守护了。这篇文章就介绍一个用NDK来实现双进程保护的办法,不过首先说明一点,下面要介绍的方法中,会损失不少的效率,反应到现实中就是会使手机的耗电量变大!但是这篇文章仅仅是抛砖引玉,相信看完之后会有更多高人指点出更妙的实现办法。
需要了解些什么?
这篇文章中实现双进程保护的方法基本上是纯的NDK开发,或者说全部是用C++来实现的,需要双进程保护的程序,只需要在程序的任何地方调用一下JAVA接口即可。下面几个知识点是需要了解的:
1.linux中多进程;
2.unix domain套接字实现跨进程通信;
3.linux的信号处理;
4.exec函数族的用法;
其实这些东西本身并不是多复杂的技术,只是我们把他们组合起来实现了一个双进程守护而已,没有想象中那么神秘!在正式贴出代码之前,先来说说几个实现双进程守护时的关键点:
1.父进程如何监视到子进程(监视进程)的死亡?
很简单,在linux中,子进程被终止时,会向父进程发送SIG_CHLD信号,于是我们可以安装信号处理函数,并在此信号处理函数中重新启动创建监视进程;
2.子进程(监视进程)如何监视到父进程死亡?
当父进程死亡以后,子进程就成为了孤儿进程由Init进程领养,于是我们可以在一个循环中读取子进程的父进程PID,当变为1就说明其父进程已经死亡,于是可以重启父进程。这里因为采用了循环,所以就引出了之前提到的耗电量的问题。
3.父子进程间的通信
有一种办法是父子进程间建立通信通道,然后通过监视此通道来感知对方的存在,这样不会存在之前提到的耗电量的问题,在本文的实现中,为了简单,还是采用了轮询父进程PID的办法,但是还是留出了父子进程的通信通道,虽然暂时没有用到,但可备不时之需!
OK, 下面就贴上代码!首先是Java部分,这一部分太过简单,只是一个类,提供了给外部调用的API接口用于创建守护进程,所有的实现都通过native方法在C++中完成!
package com.example.dameonservice;
import java.util.ArrayList;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningServiceInfo;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.util.Log;
/**
* 监视器类,构造时将会在Native创建子进程来监视当前进程,
* @author wangqiang
* @date 2014-04-24
*/
public class Watcher
{
//TODO Fix this according to your service
private static final String PACKAGE = "com.example.dameonservice/";
private String mMonitoredService = "";
private volatile boolean bHeartBreak = false;
private Context mContext;
private boolean mRunning = true;
public void createAppMonitor(String userId)
{
if( !createWatcher(userId) )
{
Log.e("Watcher", "<<Monitor created failed>>");
}
}
public Watcher( Context context)
{
mContext = context;
}
private int isServiceRunning()
{
ActivityManager am=(ActivityManager)mContext.getSystemService(Context.ACTIVITY_SERVICE);
ArrayList<RunningServiceInfo> runningService = (ArrayList<RunningServiceInfo>)am.getRunningServices(1024);
for( int i = 0; i < runningService.size(); ++i )
{
if( mMonitoredService.equals(runningService.get(i).service.getClassName().toString() ))
{
return 1;
}
}
return 0;
}
/**
* Native方法,创建一个监视子进程.
* @param userId 当前进程的用户ID,子进程重启当前进程时需要用到当前进程的用户ID.
* @return 如果子进程创建成功返回true,否则返回false
*/
private native boolean createWatcher(String userId);
/**
* Native方法,让当前进程连接到监视进程.
* @return 连接成功返回true,否则返回false
*/
private native boolean connectToMonitor();
/**
* Native方法,向监视进程发送任意信息
* @param 发给monitor的信息
* @return 实际发送的字节
*/
private native int sendMsgToMonitor(String msg);
static
{
System.loadLibrary("monitor");
}
}
代码中很多属性都是测试时用的,懒得去掉,其实有些都没用到。只需要关心createAppMonitor这个对外接口就可以了,它要求传入一个当前进程的用户ID,然后会调用createWatcher本地方法来创建守护进程。还有两个方法connectToMonitor用于创建和监视进程的socket通道,sendMsgToMonitor用于通过socket向子进程发送数据。由于暂时不需要和子进程进行数据交互,所以这两个方法就没有添加对外的JAVA接口,但是要添加简直是轻而易举的事!
Ok,JAVA只是个壳,内部的实现还得是C++,为了让程序更加的面向对象,在实现native时,我们用一个ProcessBase基类来对父子进程进行一个抽象,把父子进程都会有的行为抽象出来,而父子进程可以根据需要用自己的方式去实现其中的接口,先来看看这个抽象了父子进程共同行为的ProcessBase基类:
#ifndef _PROCESS_H
#define _PROCESS_H
#include <jni.h>
#include <sys/select.h>
#include <unistd.h>
#include <sys/socket.h>
#include <pthread.h>
#include <signal.h>
#include <sys/wait.h>
#include <android/log.h>
#include <sys/types.h>
#include <sys/un.h>
#include <errno.h>
#include <stdlib.h>
#include "constants.h"
#define LOG_TAG "Native"
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)
/**
* 功能:对父子进程的一个抽象
* @author wangqiang
* @date 2014-03-14
*/
class ProcessBase
{
public:
ProcessBase( );
/**
* 父子进程要做的工作不相同,留出一个抽象接口由父子进程
* 自己去实现.
*/
virtual void do_work() = 0;
/**
* 进程可以根据需要创建子进程,如果不需要创建子进程,可以给
* 此接口一个空实现即可.
*/
virtual bool create_child() = 0;
/**
* 捕捉子进程死亡的信号,如果没有子进程此方法可以给一个空实现.
*/
virtual void catch_child_dead_signal() = 0;
/**
* 在子进程死亡之后做任意事情.
*/
virtual void on_child_end() = 0;
/**
* 创建父子进程通信通道.
*/
bool create_channel();
/**
* 给进程设置通信通道.
* @param channel_fd 通道的文件描述
*/
void set_channel(int channel_fd);
/**
* 向通道中写入数据.
* @param data 写入通道的数据
* @param len 写入的字节数
* @return 实际写入通道的字节数
*/
int write_to_channel( void* data, int len );
/**
* 从通道中读数据.
* @param data 保存从通道中读入的数据
* @param len 从通道中读入的字节数
* @return 实际读到的字节数
*/
int read_from_channel( void* data, int len );
/**
* 获取通道对应的文件描述符
*/
int get_channel() const;
virtual ~ProcessBase();
protected:
int m_channel;
};
只是很简单的一个类,相信看看注释就知道是什么意思了,比如父子进程可能都需要捕获他的子孙死亡的信号,于是给一个catch_child_dead_signal函数,如果对子进程的死活不感兴趣,可以给个空实现,忽略掉就可以了,谁叫他大逆不道呢?由于用了纯虚函数,所以ProcessBase是一个抽象类,也就是说它不能有自己的实例,只是用来继承的,它的子孙后代可以用不同的方式实现它里面的接口从而表现出不一样的行为,这里父进程和子进程的行为就是有区别的,下面就先为诸君奉上父进程的实现:
/**
* 功能:父进程的实现
* @author wangqiang
* @date 2014-03-14
*/
class Parent : public ProcessBase
{
public:
Parent( JNIEnv* env, jobject jobj );
virtual bool create_child( );
virtual void do_work();
virtual void catch_child_dead_signal();
virtual void on_child_end();
virtual ~Parent();
bool create_channel();
/**
* 获取父进程的JNIEnv
*/
JNIEnv *get_jni_env() const;
/**
* 获取Java层的对象
*/
jobject get_jobj() const;
private:
JNIEnv *m_env;
jobject m_jobj;
};
以上是定义部分,其实JNIEnv和jobject基本上没用到,完全可以给剃掉的,大家就当这两个属性不存在就是了!实现部分如下:
#include "process.h"
#include "Utils.h"
extern ProcessBase *g_process;
extern const char* g_userId;
extern JNIEnv* g_env;
//子进程有权限访问父进程的私有目录,在此建立跨进程通信的套接字文件
static const char* PATH = "/data/data/com.example.dameonservice/my.sock";
//服务名称
static const char* SERVICE_NAME = "com.example.dameonservice/com.example.dameonservice.MyService";
bool ProcessBase::create_channel( )
{
}
int ProcessBase::write_to_channel( void* data, int len )
{
return write( m_channel, data, len );
}
int ProcessBase::read_from_channel( void* data, int len )
{
return read( m_channel, data, len );
}
int ProcessBase::get_channel() const
{
return m_channel;
}
void ProcessBase::set_channel( int channel_fd )
{
m_channel = channel_fd;
}
ProcessBase::ProcessBase()
{
}
ProcessBase::~ProcessBase()
{
close(m_channel);
}
Parent::Parent(JNIEnv *env, jobject jobj) : m_env(env)
{
LOGE("<<new parent instance>>");
m_jobj = env->NewGlobalRef(jobj);
}
Parent::~Parent()
{
LOGE( "<<Parent::~Parent()>>" );
g_process = NULL;
}
void Parent::do_work()
{
}
JNIEnv* Parent::get_jni_env() const
{
return m_env;
}
jobject Parent::get_jobj() const
{
return m_jobj;
}
/**
* 父进程创建通道,这里其实是创建一个客户端并尝试
* 连接服务器(子进程)
*/
bool Parent::create_channel()
{
int sockfd;
sockaddr_un addr;
while( 1 )
{
sockfd = socket( AF_LOCAL, SOCK_STREAM, 0 );
if( sockfd < 0 )
{
LOGE("<<Parent create channel failed>>");
return false;
}
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_LOCAL;
strcpy( addr.sun_path, PATH );
if( connect( sockfd, (sockaddr*)&addr, sizeof(addr)) < 0 )
{
close(sockfd);
sleep(1);
continue;
}
set_channel(sockfd);
LOGE("<<parent channel fd %d>>", m_channel );
break;
}
return true;
}
/**
* 子进程死亡会发出SIGCHLD信号,通过捕捉此信号父进程可以
* 知道子进程已经死亡,此函数即为SIGCHLD信号的处理函数.
*/
static void sig_handler( int signo )
{
pid_t pid;
int status;
//调用wait等待子进程死亡时发出的SIGCHLD
//信号以给子进程收尸,防止它变成僵尸进程
pid = wait(&status);
if( g_process != NULL )
{
g_process->on_child_end();
}
}
void Parent::catch_child_dead_signal()
{
LOGE("<<process %d install child dead signal detector!>>", getpid());
struct sigaction sa;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sa.sa_handler = sig_handler;
sigaction( SIGCHLD, &sa, NULL );
}
void Parent::on_child_end()
{
LOGE("<<on_child_end:create a new child process>>");
create_child();
}
bool Parent::create_child( )
{
pid_t pid;
if( (pid = fork()) < 0 )
{
return false;
}
else if( pid == 0 ) //子进程
{
LOGE("<<In child process,pid=%d>>", getpid() );
Child child;
ProcessBase& ref_child = child;
ref_child.do_work();
}
else if( pid > 0 ) //父进程
{
LOGE("<<In parent process,pid=%d>>", getpid() );
}
return true;
}
这里先要说明一下三个全局变量:
g_process是父进程的指针;
g_userId是父进程用户ID,由Java侧传递过来,我们需要把它用全局变量保存起来,因为子进程在重启父进程的时候需要用到用户ID,否则会有问题,当然这里也得益于子进程能够继承父进程的全局变量这个事实!
g_env是JNIEnv的指针,把这个变量也作为一个全局变量,是保留给子进程用的;
父进程在create_child中用fork创建了子进程,其实就是一个fork调用,然后父进程什么都不做,子进程创建一个Child对象并调用其do_work开始做自己该做的事!
父进程实现了catch_child_dead_signal,在其中安装了SIG_CHLD信号处理函数,因为他很爱他的儿子,时刻关心着他。而在信号处理函数sig_handler中,我们留意到了wait调用,这是为了防止子进程死了以后变成僵尸进程,由于我们已经知道父进程最多只会创建一个子监视进程,所以wait就足够了,不需要waitpid函数亲自出马!而信号处理函数很简单,重新调用一下on_child_end,在其中再次create_child和他亲爱的夫人在make一个小baby就可以了!
最后要说说create_channel这个函数,他用来创建和子进程的socket通道,这个编程模型对于有网络编程经验的人来说显得非常亲切和熟悉,他遵循标准的网络编程客户端步骤:创建socket,connect,之后收发数据就OK了,只是这里的协议用的是AF_LOCAL,表明我们是要进行跨进程通信。由于域套接字用的不是IP地址,而是通过指定的一个文件来和目标进程通信,父子进程都需要这个文件,所以这个文件的位置指定在哪里也需要注意一下:在一个没有root过的手机上,几乎所有的文件都是没有写入权限的,但是很幸运的是linux的子进程共享父进程的目录,所以把这个位置指定到/data/data/下应用的私有目录就可以做到让父子进程都能访问这个文件了!
接下来是子进程的实现了,它的定义如下:
/**
* 子进程的实现
* @author wangqiang
* @date 2014-03-14
*/
class Child : public ProcessBase
{
public:
Child( );
virtual ~Child();
virtual void do_work();
virtual bool create_child();
virtual void catch_child_dead_signal();
virtual void on_child_end();
bool create_channel();
private:
/**
* 处理父进程死亡事件
*/
void handle_parent_die();
/**
* 侦听父进程发送的消息
*/
void listen_msg();
/**
* 重新启动父进程.
*/
void restart_parent();
/**
* 处理来自父进程的消息
*/
void handle_msg( const char* msg );
/**
* 线程函数,用来检测父进程是否挂掉
*/
void* parent_monitor();
void start_parent_monitor();
/**
* 这个联合体的作用是帮助将类的成员函数做为线程函数使用
*/
union
{
void* (*thread_rtn)(void*);
void* (Child::*member_rtn)();
}RTN_MAP;
};
#endif
注意到里面有个union,这个联合体的作用是为了辅助把一个类的成员函数作为线程函数来传递给pthread_create,很多时候我们都希望线程能够像自己人一样访问类的私有成员,就像一个成员函数那样,用friend虽然可以做到这一点,但总感觉不够优美,由于成员函数隐含的this指针,使我们完全可以将一个成员函数作为线程函数来用。只是由于编译器堵死了函数指针的类型转换,所以这里就只好用一个结构体。
废话不多说,看看子进程的实现:
bool Child::create_child( )
{
//子进程不需要再去创建子进程,此函数留空
return false;
}
Child::Child()
{
RTN_MAP.member_rtn = &Child::parent_monitor;
}
Child::~Child()
{
LOGE("<<~Child(), unlink %s>>", PATH);
unlink(PATH);
}
void Child::catch_child_dead_signal()
{
//子进程不需要捕捉SIGCHLD信号
return;
}
void Child::on_child_end()
{
//子进程不需要处理
return;
}
void Child::handle_parent_die( )
{
//子进程成为了孤儿进程,等待被Init进程收养后在进行后续处理
while( getppid() != 1 )
{
usleep(500); //休眠0.5ms
}
close( m_channel );
//重启父进程服务
LOGE( "<<parent died,restart now>>" );
restart_parent();
}
void Child::restart_parent()
{
LOGE("<<restart_parent enter>>");
/**
* TODO 重启父进程,通过am启动Java空间的任一组件(service或者activity等)即可让应用重新启动
*/
execlp( "am",
"am",
"startservice",
"--user",
g_userId,
"-n",
SERVICE_NAME, //注意此处的名称
(char *)NULL);
}
void* Child::parent_monitor()
{
handle_parent_die();
}
void Child::start_parent_monitor()
{
pthread_t tid;
pthread_create( &tid, NULL, RTN_MAP.thread_rtn, this );
}
bool Child::create_channel()
{
int listenfd, connfd;
struct sockaddr_un addr;
listenfd = socket( AF_LOCAL, SOCK_STREAM, 0 );
unlink(PATH);
memset( &addr, 0, sizeof(addr) );
addr.sun_family = AF_LOCAL;
strcpy( addr.sun_path, PATH );
if( bind( listenfd, (sockaddr*)&addr, sizeof(addr) ) < 0 )
{
LOGE("<<bind error,errno(%d)>>", errno);
return false;
}
listen( listenfd, 5 );
while( true )
{
if( (connfd = accept(listenfd, NULL, NULL)) < 0 )
{
if( errno == EINTR)
continue;
else
{
LOGE("<<accept error>>");
return false;
}
}
set_channel(connfd);
break;
}
LOGE("<<child channel fd %d>>", m_channel );
return true;
}
void Child::handle_msg( const char* msg )
{
//TODO How to handle message is decided by you.
}
void Child::listen_msg( )
{
fd_set rfds;
int retry = 0;
while( 1 )
{
FD_ZERO(&rfds);
FD_SET( m_channel, &rfds );
timeval timeout = {3, 0};
int r = select( m_channel + 1, &rfds, NULL, NULL, &timeout );
if( r > 0 )
{
char pkg[256] = {0};
if( FD_ISSET( m_channel, &rfds) )
{
read_from_channel( pkg, sizeof(pkg) );
LOGE("<<A message comes:%s>>", pkg );
handle_msg( (const char*)pkg );
}
}
}
}
void Child::do_work()
{
start_parent_monitor(); //启动监视线程
if( create_channel() ) //等待并且处理来自父进程发送的消息
{
listen_msg();
}
}
子进程在他的do_work中先创建了一个线程轮询其父进程的PID,如果发现变成了1,就会调用restart_parent,在其中调用execlp,执行一下am指令启动JAVA侧的组件,从而实现父进程的重启!这里请留意一下execlp中给am传入的参数,带了--user并加上了之前我们在全局变量中保存的user id,如果不加这个选项,就无法重启父进程,我在这花费了好长时间哦!
子进程剩余的工作很简单,创建通道,监听来自父进程的消息,这里我们用select来监听,由于实际上只有一个客户端(父进程),所以用select有点脱裤子放屁,把简单问题复杂化的嫌疑,但是实际上也没啥太大影响!
有了以上的实现,JNI的实现就相当的简单了:
#include "process.h"
#include "Utils.h"
/**
* 全局变量,代表应用程序进程.
*/
ProcessBase *g_process = NULL;
/**
* 应用进程的UID.
*/
const char* g_userId = NULL;
/**
* 全局的JNIEnv,子进程有时会用到它.
*/
JNIEnv* g_env = NULL;
extern "C"
{
JNIEXPORT jboolean JNICALL Java_com_example_dameonservice_Watcher_createWatcher( JNIEnv*, jobject, jstring);
JNIEXPORT jboolean JNICALL Java_com_example_dameonservice_Watcher_connectToMonitor( JNIEnv*, jobject );
JNIEXPORT jint JNICALL Java_com_example_dameonservice_Watcher_sendMsgToMonitor( JNIEnv*, jobject, jstring );
JNIEXPORT jint JNICALL JNI_OnLoad( JavaVM* , void* );
};
JNIEXPORT jboolean JNICALL Java_com_example_dameonservice_Watcher_createWatcher( JNIEnv* env, jobject thiz, jstring user )
{
g_process = new Parent( env, thiz );
g_userId = (const char*)jstringTostr(env, user);
g_process->catch_child_dead_signal();
if( !g_process->create_child() )
{
LOGE("<<create child error!>>");
return JNI_FALSE;
}
return JNI_TRUE;
}
JNIEXPORT jboolean JNICALL Java_com_example_dameonservice_Watcher_connectToMonitor( JNIEnv* env, jobject thiz )
{
if( g_process != NULL )
{
if( g_process->create_channel() )
{
return JNI_TRUE;
}
return JNI_FALSE;
}
}
把上面这些代码整合起来,一个双进程守护的实现就完成了,只需要调用一下Watcher.java的createAppMonitor,你的应用就会有一个守护进程来监视,被杀死后也会立刻重新启动起来!是不是很有意思呢?
一、创建SQLite数据库和表
我们可以通过SQLiteDatabase.openOrCreateDatabase()来创建一个数据库实例。
SQLiteDatabase db = openOrCreateDatabase(dbName, MODE_PRIVATE, null);
// openOrCreateDatabase(String name, int mode, CursorFactory factory)
// 第一个参数为创建数据库的名称
// 第二个参数为创建数据库的权限,其权限同内部文件存储数据权限相同。默认为MODE_PRIVATE。
// 第三个参数为CursorFactory对象,用于查询时返回Cursor的子类对象;或者传入null使用默认的factory构造。
在创建表的时候,我们可以使用一条SQL语句来完成。
cmd = "CREATE TABLE IF NOT EXISTS " + tableName + " (name VARCHAR, passwd VARCHAR)";
db.execSQL(cmd);
这样,我们可以发现在“/data/data/[PACKAGE_NAME]/databases”目录下生成了一个“myDB.db”数据库文件。
完整代码:
import android.database.sqlite.SQLiteDatabase;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private Button btnDB;
private Button btnTable;
private String dbName = "myDB";
private String cmd = "";
private SQLiteDatabase db;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnDB = (Button)findViewById(R.id.btnDB);
btnTable = (Button)findViewById(R.id.btnTable);
btnDB.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
db = openOrCreateDatabase(dbName,MODE_PRIVATE,null);
}
});
btnTable.setOnClickListener(new View.OnClickListener() {
String tableName = "User";
@Override
public void onClick(View view) {
cmd = "CREATE TABLE IF NOT EXISTS " + tableName + " (name VARCHAR, passwd VARCHAR)";
db.execSQL(cmd);
}
});
}
}
二、添加、删除、修改
(1)SQL语句方法
cmd = "INSERT INTO " + tableName + " values ('Amy','123456')";
db.execSQL(cmd);
cmd = "UPDATE " + tableName + " SET passwd='654321' WHERE name='AMY')";
db.execSQL(cmd);
cmd = "DELETE FROM " + tableName + " WHERE name='Amy'";
db.execSQL(cmd);
(2)另一种方法
db.insert(String table, String nullColumnHack, ContentValues values);
db.update(String table, Contentvalues values, String whereClause, String whereArgs);
db.delete(String table, String whereClause, String whereArgs);
以上三个方法的第一个参数都是表示要操作的表名;insert中的第二个参数表示如果插入的数据每一列都为空的话,需要指定此行中某一列的名称,系统将此列设置为NULL,不至于出现错误;insert中的第三个参数是ContentValues类型的变量,是键值对组成的Map,key代表列名,value代表该列要插入的值;update的第二个参数也很类似,只不过它是更新该字段key为最新的value值,第三个参数whereClause表示WHERE表达式,比如“age > ? and age < ?”等,最后的whereArgs参数是占位符的实际参数值;delete方法的参数也是一样。
实例:
btnTable.setOnClickListener(new View.OnClickListener() {
String tableName = "User";
@Override
public void onClick(View view) {
cmd = "CREATE TABLE IF NOT EXISTS " + tableName + " (name VARCHAR, passwd VARCHAR)";
db.execSQL(cmd);
ContentValues cv = new ContentValues();
cv.put("name", "Amy");
cv.put("passwd", "123456");
db.insert(dbName, null, cv);
// cmd = "INSERT INTO " + tableName + " values ('Amy','123456')";
// db.execSQL(cmd);
cv = new ContentValues();
cv.put("passwd", "654321");
db.update(dbName, cv, "name=?", new String[]{"Amy"});
// cmd = "UPDATE " + tableName + " SET passwd='654321' WHERE name='AMY')";
// db.execSQL(cmd);
db.delete(dbName, "name=?", new String[]{"Amy"});
// cmd = "DELETE FROM " + tableName + " WHERE name='Amy'";
// db.execSQL(cmd);
}
});
三、数据库查询
对数据库的查询可以通过db.query()来实现,query方法一般包含8个参数:
db.query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit);
// table为查询表的名称
// columns为查询的字段名
// selection为查询的条件
// selectionArgs为查询条件的值
// groupBy为分组字段值
// having为分组后筛选条件
// orderBy为排序字段名
// limit为查询结果返回记录条数
查询的的结果通过Cursor返回。代表数据集的游标。
Cursor cursor = db.query(tableName, null, null, null, null, null, null);
String str = "";
if(cursor.getCount()!=0){ // 查询符合条件的记录个数
cursor.moveToFirst(); // 移动到第一个记录
for(int i=0; i<cursor.getCount();i++){
str += cursor.getString(0)+" "+cursor.getString(1)+"\n";
cursor.moveToNext(); // 移动到下一个记录
}
}
new AlertDialog.Builder(MainActivity.this).setTitle("Query")
.setMessage(str).setNegativeButton("OK",null).show();
四、SQLiteOpenHelper
除了常规的管理方法之外,Android SDK还提供了另外一种管理数据库的方法,SQLiteOpenHelper。它提供了一套自动执行的机制来创建、更新、打开数据库。
首先,我们继承SQLiteOpenHelper类,创建MyDBHelper类。
public class MyDBHelper extends SQLiteOpenHelper {
private String tableName = "User";
public MyDBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
public void onCreate(SQLiteDatabase db) {
String cmd = "CREATE TABLE IF NOT EXISTS " + tableName + " (name VARCHAR, passwd VARCHAR)";
db.execSQL(cmd);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
String cmd = "UPDATE " + tableName + " SET passwd='654321' WHERE name='AMY')";
db.execSQL(cmd);
}
public String showTable(){
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query(tableName, null, null, null, null, null, null);
String str = "";
if(cursor.getCount()!=0){ // 查询符合条件的记录个数
cursor.moveToFirst(); // 移动到第一个记录
for(int i=0; i<cursor.getCount();i++){
str += cursor.getString(0)+" "+cursor.getString(1)+"\n";
cursor.moveToNext(); // 移动到下一个记录
}
}
return str;
}
}
然后,我们便可以在MainActivity内使用我们定义的SQLiteOpenHelper类的方法。
btnShow = (Button) findViewById(R.id.btnShow);
btnShow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dbHelper = new MyDBHelper(MainActivity.this, dbName, null, 1);
new AlertDialog.Builder(MainActivity.this).setTitle("MyDBHelper")
.setMessage(dbHelper.showTable()).setNegativeButton("OK", null).show();
}
});
完整代码如下:
MainActivity.java
import android.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private Button btnShow;
private String dbName = "myDB";
private MyDBHelper dbHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
btnShow = (Button) findViewById(R.id.btnShow);
btnShow.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dbHelper = new MyDBHelper(MainActivity.this, dbName, null, 1);
new AlertDialog.Builder(MainActivity.this).setTitle("MyDBHelper")
.setMessage(dbHelper.showTable()).setNegativeButton("OK", null).show();
}
});
}
}
MyDBHelper.java
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
public class MyDBHelper extends SQLiteOpenHelper {
private String tableName = "User";
public MyDBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
public void onCreate(SQLiteDatabase db) {
String cmd = "CREATE TABLE IF NOT EXISTS " + tableName + " (name VARCHAR, passwd VARCHAR)";
db.execSQL(cmd);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
String cmd = "UPDATE " + tableName + " SET passwd='654321' WHERE name='AMY')";
db.execSQL(cmd);
}
public String showTable(){
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query(tableName, null, null, null, null, null, null);
String str = "";
if(cursor.getCount()!=0){ // 查询符合条件的记录个数
cursor.moveToFirst(); // 移动到第一个记录
for(int i=0; i<cursor.getCount();i++){
str += cursor.getString(0)+" "+cursor.getString(1)+"\n";
cursor.moveToNext(); // 移动到下一个记录
}
}
return str;
}
}
另一篇 android开发之使用SQLite数据库存储
Android 集成了 SQLite 数据库
Android 在运行时(run-time)集成了 SQLite,所以每个 Android 应用程序都可以使用 SQLite 数据库。对于熟悉 SQL 的开发人员来时,在 Android 开发中使用 SQLite 相当简单。但是,由于 JDBC 会消耗太多的系统资源,所以 JDBC 对于手机这种内存受限设备来说并不合适。因此,Android 提供了一些新的 API 来使用 SQLite 数据库,Android 开发中,程序员需要学使用这些 API。
数据库存储在 data/< 项目文件夹 >/databases/ 下。
Android 开发中使用 SQLite 数据库
Activites 可以通过 Content Provider 或者 Service 访问一个数据库。下面会详细讲解如果创建数据库,添加数据和查询数据库。
创建数据库
Android 不自动提供数据库。在 Android 应用程序中使用 SQLite,必须自己创建数据库,然后创建表、索引,填充数据。Android 提供了 SQLiteOpenHelper 帮助你创建一个数据库,你只要继承 SQLiteOpenHelper 类,就可以轻松的创建数据库。SQLiteOpenHelper 类根据开发应用程序的需要,封装了创建和更新数据库使用的逻辑。SQLiteOpenHelper 的子类,至少需要实现三个方法:
构造函数,调用父类 SQLiteOpenHelper 的构造函数。这个方法需要四个参数:上下文环境(例如,一个 Activity),数据库名字,一个可选的游标工厂(通常是 Null),一个代表你正在使用的数据库模型版本的整数。
onCreate()方法,它需要一个 SQLiteDatabase 对象作为参数,根据需要对这个对象填充表和初始化数据。
onUpgrage() 方法,它需要三个参数,一个 SQLiteDatabase 对象,一个旧的版本号和一个新的版本号,这样你就可以清楚如何把一个数据库从旧的模型转变到新的模型。
下面示例代码展示了如何继承 SQLiteOpenHelper 创建数据库:
public class DatabaseHelper extends SQLiteOpenHelper {
DatabaseHelper(Context context, String name, CursorFactory cursorFactory, int version)
{
super(context, name, cursorFactory, version);
}
@Override
public void onCreate(SQLiteDatabase db) {
// TODO 创建数据库后,对数据库的操作
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO 更改数据库版本的操作
}
@Override
public void onOpen(SQLiteDatabase db) {
super.onOpen(db);
// TODO 每次成功打开数据库后首先被执行
}
}
接下来讨论具体如何创建表、插入数据、删除表等等。调用 getReadableDatabase() 或 getWriteableDatabase() 方法,你可以得到 SQLiteDatabase 实例,具体调用那个方法,取决于你是否需要改变数据库的内容:
db=(new DatabaseHelper(getContext())).getWritableDatabase();
return (db == null) ? false : true;
上面这段代码会返回一个 SQLiteDatabase 类的实例,使用这个对象,你就可以查询或者修改数据库。
当你完成了对数据库的操作(例如你的 Activity 已经关闭),需要调用 SQLiteDatabase 的 Close() 方法来释放掉数据库连接。
创建表和索引
为了创建表和索引,需要调用 SQLiteDatabase 的 execSQL() 方法来执行 DDL 语句。如果没有异常,这个方法没有返回值。
例如,你可以执行如下代码:
db.execSQL("CREATE TABLE mytable (_id INTEGER PRIMARY KEY
AUTOINCREMENT, title TEXT, value REAL);");
这条语句会创建一个名为 mytable 的表,表有一个列名为 _id,并且是主键,这列的值是会自动增长的整数(例如,当你插入一行时,SQLite 会给这列自动赋值),另外还有两列:title( 字符 ) 和 value( 浮点数 )。 SQLite 会自动为主键列创建索引。
通常情况下,第一次创建数据库时创建了表和索引。如果你不需要改变表的 schema,不需要删除表和索引 . 删除表和索引,需要使用 execSQL() 方法调用 DROP INDEX 和 DROP TABLE 语句。
给表添加数据
上面的代码,已经创建了数据库和表,现在需要给表添加数据。有两种方法可以给表添加数据。
像上面创建表一样,你可以使用 execSQL() 方法执行 INSERT, UPDATE, DELETE 等语句来更新表的数据。execSQL() 方法适用于所有不返回结果的 SQL 语句。例如:
db.execSQL("INSERT INTO widgets (name, inventory)"+
VALUES ('Sprocket', 5)");
另一种方法是使用 SQLiteDatabase 对象的 insert(), update(), delete() 方法。这些方法把 SQL 语句的一部分作为参数。示例如下:
ContentValues cv=new ContentValues();
cv.put(Constants.TITLE, "example title");
cv.put(Constants.VALUE, SensorManager.GRAVITY_DEATH_STAR_I);
db.insert("mytable", getNullColumnHack(), cv);
update()方法有四个参数,分别是表名,表示列名和值的 ContentValues 对象,可选的 WHERE 条件和可选的填充 WHERE 语句的字符串,这些字符串会替换 WHERE 条件中的“?”标记。update() 根据条件,更新指定列的值,所以用 execSQL() 方法可以达到同样的目的。
WHERE 条件和其参数和用过的其他 SQL APIs 类似。例如:
String[] parms=new String[] {"this is a string"};
db.update("widgets", replacements, "name=?", parms);
delete() 方法的使用和 update() 类似,使用表名,可选的 WHERE 条件和相应的填充 WHERE 条件的字符串。
查询数据库
类似 INSERT, UPDATE, DELETE,有两种方法使用 SELECT 从 SQLite 数据库检索数据。
1 .使用 rawQuery() 直接调用 SELECT 语句;
使用 query() 方法构建一个查询。
Raw Queries
正如 API 名字,rawQuery() 是最简单的解决方法。通过这个方法你就可以调用 SQL SELECT 语句。例如:
Cursor c=db.rawQuery(
"SELECT name FROM sqlite_master WHERE type='table' AND name='mytable'", null);
在上面例子中,我们查询 SQLite 系统表(sqlite_master)检查 table 表是否存在。返回值是一个 cursor 对象,这个对象的方法可以迭代查询结果。
如果查询是动态的,使用这个方法就会非常复杂。例如,当你需要查询的列在程序编译的时候不能确定,这时候使用 query() 方法会方便很多。
Regular Queries
query() 方法用 SELECT 语句段构建查询。SELECT 语句内容作为 query() 方法的参数,比如:要查询的表名,要获取的字段名,WHERE 条件,包含可选的位置参数,去替代 WHERE 条件中位置参数的值,GROUP BY 条件,HAVING 条件。
除了表名,其他参数可以是 null。所以,以前的代码段可以可写成:
String[] columns={"ID", "inventory"};
String[] parms={"snicklefritz"};
Cursor result=db.query("widgets", columns, "name=?",parms, null, null, null);
使用游标
不管你如何执行查询,都会返回一个 Cursor,这是 Android 的 SQLite 数据库游标,使用游标,你可以:
通过使用 getCount() 方法得到结果集中有多少记录;
通过 moveToFirst(), moveToNext(), 和 isAfterLast() 方法遍历所有记录;
通过 getColumnNames() 得到字段名;
通过 getColumnIndex() 转换成字段号;
通过 getString(),getInt() 等方法得到给定字段当前记录的值;
通过 requery() 方法重新执行查询得到游标;
通过 close() 方法释放游标资源;
例如,下面代码遍历 mytable 表
Cursor result=db.rawQuery("SELECT ID, name, inventory FROM mytable");
result.moveToFirst();
while (!result.isAfterLast()) {
int id=result.getInt(0);
String name=result.getString(1);
int inventory=result.getInt(2);
// do something useful with these
result.moveToNext();
}
result.close();
结束语
如果你想要开发 Android 应用程序,一定需要在 Android 上存储数据,使用 SQLite 数据库是一种非常好的选择。本文介绍了如何在 Android 应用程序中使用 SQLite 数据库 ,主要介绍了在 Android 应用程序中使用 SQLite 创建数据库和表、添加数据、更新和检索数据,还介绍了比较常用的 SQLite 管理工具,通过阅读本文,你可以在 Android 中轻松操作 SQLite 数据库。
SharedPreferences简介
SharedPreferences是Android平台上一个轻量级的存储类,用来保存应用的一些常用配置,比如Activity状态,Activity暂停时,将此activity的状态保存到SharedPereferences中;当Activity重载,系统回调方法onSaveInstanceState时,再从SharedPreferences中将值取出。
SharedPreferences提供了java常规的Long、Int、String等类型数据的保存接口。
SharedPreferences类似过去Windows系统上的ini配置文件,但是它分为多种权限,可以全局共享访问。
提示最终是以xml方式来保存,整体效率来看不是特别的高,对于常规的轻量级而言比SQLite要好不少,如果真的存储量不大可以考虑自己定义文件格式。xml处理时Dalvik会通过自带底层的本地XML Parser解析,比如XMLpull方式,这样对于内存资源占用比较好。
操作模式
SharedPreferences数据的四种操作模式
Context.MODE_PRIVATE
Context.MODE_APPEND
Context.MODE_WORLD_READABLE
Context.MODE_WORLD_WRITEABLE
Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容
Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件.
Context.MODE_WORLD_READABLE和Context.MODE_WORLD_WRITEABLE用来控制其他应用是否有权限读写该文件.
MODE_WORLD_READABLE:表示当前文件可以被其他应用读取.
MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入
SharedPreferences是Android中最容易理解的数据存储技术,实际上SharedPreferences处理的就是一个key-value(键值对)SharedPreferences常用来存储一些轻量级的数据。这类似于C++中Map的数据存储方式(实际上在最后生成的.xml文件内,就是以Map格式存储的)。
获取SharedPreferences的两种方式:
1、调用Context对象的getSharedPreferences()方法
2、调用Activity对象的getPreferences()方法
两种方式的区别:
调用Context对象的getSharedPreferences()方法获得的SharedPreferences对象可以被同一应用程序下的其他组件共享。
调用Activity对象的getPreferences()方法获得的SharedPreferences对象只能在该Activity中使用。
其中,getSharedPreferences()的方法原型为:
getSharedPreferences(String name, int mode);
// name: 生成xml文件的名称
// MODE_PRIVATE: 为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容
// MODE_APPEND: 模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件.
// MODE_WORLD_READABLE: 表示当前文件可以被其他应用读取,不推荐使用
// MODE_WORLD_WRITEABLE: 表示当前文件可以被其他应用写入,不推荐使用
使用SharedPreferences存储数据的方法如下:
//实例化SharedPreferences对象(第一步)
SharedPreferences sp = getSharedPreferences("test", MODE_PRIVATE);
//实例化SharedPreferences.Editor对象(第二步)
SharedPreferences.Editor editor = mySharedPreferences.edit();
//用putString的方法保存数据
editor.putString("UserName", etName.getText().toString());
editor.putString("Password", etPassword.getText().toString());
//提交当前数据
//editor.apply();
editor.commit();
//使用toast信息提示框提示成功写入数据
Toast.makeText(MainActivity.this, "注册成功", Toast.LENGTH_LONG).show();
使用SharedPreferences读取数据的方法如下:
SharedPreferences sp = getSharedPreferences(strLogInfo, MODE_APPEND);
String name = sp.getString("UserName", "");
String passwd = sp.getString("Password","");
if(etName.getText().toString().equals(name) && etPassword.getText().toString().equals(passwd)){
Toast.makeText(MainActivity.this, "登陆成功", Toast.LENGTH_LONG).show();
} else{
Toast.makeText(MainActivity.this, "登录失败", Toast.LENGTH_LONG).show();
}
在使用SharedPreferences之后,程序会在“/data/data/包名/shared_prefs/xxx.xml”生成的一个XML文件。文件名取决于getSharedPreferences的第一个参数名。
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="UserName">Name</string>
<string name="Password">Password</string>
</map>
完整代码如下:
import android.content.SharedPreferences;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private EditText etName;
private EditText etPassword;
private Button btnLogin;
private Button btnLogup;
private String strLogInfo = "test";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
etName = (EditText)findViewById(R.id.etName);
etPassword = (EditText)findViewById(R.id.etPassword);
btnLogin = (Button)findViewById(R.id.btnLogin);
btnLogup = (Button)findViewById(R.id.btnLogup);
btnLogup.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SharedPreferences sp = getSharedPreferences(strLogInfo, MODE_APPEND);
SharedPreferences.Editor editor = sp.edit();
editor.putString("UserName", etName.getText().toString());
editor.putString("Password", etPassword.getText().toString());
editor.commit();
Toast.makeText(MainActivity.this, "注册成功", Toast.LENGTH_LONG).show();
}
});
btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
SharedPreferences sp = getSharedPreferences(strLogInfo, MODE_APPEND);
String name = sp.getString("UserName", "");
String passwd = sp.getString("Password","");
if(etName.getText().toString().equals(name) && etPassword.getText().toString().equals(passwd)){
Toast.makeText(MainActivity.this, "登陆成功", Toast.LENGTH_LONG).show();
} else{
Toast.makeText(MainActivity.this, "登录失败", Toast.LENGTH_LONG).show();
}
}
});
}
}
其实Android提供Intent让我们打开系统的相机,但是系统相机跟自己app风格不搭,而且用起来体验不好。所以我使用了SDK提供的camera API自定义了一个相机,并且在相机界面上面添加了参考线,有助于用户将题目拍正,提高ocr的识别率。
1、绘制参考线的代码
public class ReferenceLine extends View {
private Paint mLinePaint;
public ReferenceLine(Context context) {
super(context);
init();
}
public ReferenceLine(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ReferenceLine(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mLinePaint = new Paint();
mLinePaint.setAntiAlias(true);
mLinePaint.setColor(Color.parseColor("#45e0e0e0"));
mLinePaint.setStrokeWidth(1);
}
@Override
protected void onDraw(Canvas canvas) {
int screenWidth = Utils.getScreenWH(getContext()).widthPixels;
int screenHeight = Utils.getScreenWH(getContext()).heightPixels;
int width = screenWidth/3;
int height = screenHeight/3;
for (int i = width, j = 0;i < screenWidth && j<2;i += width, j++) {
canvas.drawLine(i, 0, i, screenHeight, mLinePaint);
}
for (int j = height,i = 0;j < screenHeight && i < 2;j += height,i++) {
canvas.drawLine(0, j, screenWidth, j, mLinePaint);
}
}
}
2、自定义相机代码
这里主要是要创建一个SurfaceView,将摄像头的预览界面放到SurfaceView中显示。
package com.bbk.lling.camerademo.camare;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.PictureCallback;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.widget.RelativeLayout;
import android.widget.Toast;
import com.bbk.lling.camerademo.utils.Utils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @Class: CameraPreview
* @Description: 自定义相机
* @author: lling(www.cnblogs.com/liuling)
* @Date: 2015/10/25
*/
public class CameraPreview extends SurfaceView implements
SurfaceHolder.Callback, AutoFocusCallback {
private static final String TAG = "CameraPreview";
private int viewWidth = 0;
private int viewHeight = 0;
/** 监听接口 */
private OnCameraStatusListener listener;
private SurfaceHolder holder;
private Camera camera;
private FocusView mFocusView;
//创建一个PictureCallback对象,并实现其中的onPictureTaken方法
private PictureCallback pictureCallback = new PictureCallback() {
// 该方法用于处理拍摄后的照片数据
@Override
public void onPictureTaken(byte[] data, Camera camera) {
// 停止照片拍摄
try {
camera.stopPreview();
} catch (Exception e) {
}
// 调用结束事件
if (null != listener) {
listener.onCameraStopped(data);
}
}
};
// Preview类的构造方法
public CameraPreview(Context context, AttributeSet attrs) {
super(context, attrs);
// 获得SurfaceHolder对象
holder = getHolder();
// 指定用于捕捉拍照事件的SurfaceHolder.Callback对象
holder.addCallback(this);
// 设置SurfaceHolder对象的类型
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
setOnTouchListener(onTouchListener);
}
// 在surface创建时激发
public void surfaceCreated(SurfaceHolder holder) {
Log.e(TAG, "==surfaceCreated==");
if(!Utils.checkCameraHardware(getContext())) {
Toast.makeText(getContext(), "摄像头打开失败!", Toast.LENGTH_SHORT).show();
return;
}
// 获得Camera对象
camera = getCameraInstance();
try {
// 设置用于显示拍照摄像的SurfaceHolder对象
camera.setPreviewDisplay(holder);
} catch (IOException e) {
e.printStackTrace();
// 释放手机摄像头
camera.release();
camera = null;
}
updateCameraParameters();
if (camera != null) {
camera.startPreview();
}
setFocus();
}
// 在surface销毁时激发
public void surfaceDestroyed(SurfaceHolder holder) {
Log.e(TAG, "==surfaceDestroyed==");
// 释放手机摄像头
camera.release();
camera = null;
}
// 在surface的大小发生改变时激发
public void surfaceChanged(final SurfaceHolder holder, int format, int w,
int h) {
// stop preview before making changes
try {
camera.stopPreview();
} catch (Exception e){
// ignore: tried to stop a non-existent preview
}
// set preview size and make any resize, rotate or
// reformatting changes here
updateCameraParameters();
// start preview with new settings
try {
camera.setPreviewDisplay(holder);
camera.startPreview();
} catch (Exception e){
Log.d(TAG, "Error starting camera preview: " + e.getMessage());
}
setFocus();
}
/**
* 点击显示焦点区域
*/
OnTouchListener onTouchListener = new OnTouchListener() {
@SuppressWarnings("deprecation")
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
int width = mFocusView.getWidth();
int height = mFocusView.getHeight();
mFocusView.setX(event.getX() - (width / 2));
mFocusView.setY(event.getY() - (height / 2));
mFocusView.beginFocus();
} else if (event.getAction() == MotionEvent.ACTION_UP) {
focusOnTouch(event);
}
return true;
}
};
/**
* 获取摄像头实例
* @return
*/
private Camera getCameraInstance() {
Camera c = null;
try {
int cameraCount = 0;
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
cameraCount = Camera.getNumberOfCameras(); // get cameras number
for (int camIdx = 0; camIdx < cameraCount; camIdx++) {
Camera.getCameraInfo(camIdx, cameraInfo); // get camerainfo
// 代表摄像头的方位,目前有定义值两个分别为CAMERA_FACING_FRONT前置和CAMERA_FACING_BACK后置
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
try {
c = Camera.open(camIdx); //打开后置摄像头
} catch (RuntimeException e) {
Toast.makeText(getContext(), "摄像头打开失败!", Toast.LENGTH_SHORT).show();
}
}
}
if (c == null) {
c = Camera.open(0); // attempt to get a Camera instance
}
} catch (Exception e) {
Toast.makeText(getContext(), "摄像头打开失败!", Toast.LENGTH_SHORT).show();
}
return c;
}
private void updateCameraParameters() {
if (camera != null) {
Camera.Parameters p = camera.getParameters();
setParameters(p);
try {
camera.setParameters(p);
} catch (Exception e) {
Camera.Size previewSize = findBestPreviewSize(p);
p.setPreviewSize(previewSize.width, previewSize.height);
p.setPictureSize(previewSize.width, previewSize.height);
camera.setParameters(p);
}
}
}
/**
* @param p
*/
private void setParameters(Camera.Parameters p) {
List<String> focusModes = p.getSupportedFocusModes();
if (focusModes
.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
p.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
}
long time = new Date().getTime();
p.setGpsTimestamp(time);
// 设置照片格式
p.setPictureFormat(PixelFormat.JPEG);
Camera.Size previewSize = findPreviewSizeByScreen(p);
p.setPreviewSize(previewSize.width, previewSize.height);
p.setPictureSize(previewSize.width, previewSize.height);
p.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
if (getContext().getResources().getConfiguration().orientation != Configuration.ORIENTATION_LANDSCAPE) {
camera.setDisplayOrientation(90);
p.setRotation(90);
}
}
// 进行拍照,并将拍摄的照片传入PictureCallback接口的onPictureTaken方法
public void takePicture() {
if (camera != null) {
try {
camera.takePicture(null, null, pictureCallback);
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 设置监听事件
public void setOnCameraStatusListener(OnCameraStatusListener listener) {
this.listener = listener;
}
@Override
public void onAutoFocus(boolean success, Camera camera) {
}
public void start() {
if (camera != null) {
camera.startPreview();
}
}
public void stop() {
if (camera != null) {
camera.stopPreview();
}
}
/**
* 相机拍照监听接口
*/
public interface OnCameraStatusListener {
// 相机拍照结束事件
void onCameraStopped(byte[] data);
}
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
viewWidth = MeasureSpec.getSize(widthSpec);
viewHeight = MeasureSpec.getSize(heightSpec);
super.onMeasure(
MeasureSpec.makeMeasureSpec(viewWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(viewHeight, MeasureSpec.EXACTLY));
}
/**
* 将预览大小设置为屏幕大小
* @param parameters
* @return
*/
private Camera.Size findPreviewSizeByScreen(Camera.Parameters parameters) {
if (viewWidth != 0 && viewHeight != 0) {
return camera.new Size(Math.max(viewWidth, viewHeight),
Math.min(viewWidth, viewHeight));
} else {
return camera.new Size(Utils.getScreenWH(getContext()).heightPixels,
Utils.getScreenWH(getContext()).widthPixels);
}
}
/**
* 找到最合适的显示分辨率 (防止预览图像变形)
* @param parameters
* @return
*/
private Camera.Size findBestPreviewSize(Camera.Parameters parameters) {
// 系统支持的所有预览分辨率
String previewSizeValueString = null;
previewSizeValueString = parameters.get("preview-size-values");
if (previewSizeValueString == null) {
previewSizeValueString = parameters.get("preview-size-value");
}
if (previewSizeValueString == null) { // 有些手机例如m9获取不到支持的预览大小 就直接返回屏幕大小
return camera.new Size(Utils.getScreenWH(getContext()).widthPixels,
Utils.getScreenWH(getContext()).heightPixels);
}
float bestX = 0;
float bestY = 0;
float tmpRadio = 0;
float viewRadio = 0;
if (viewWidth != 0 && viewHeight != 0) {
viewRadio = Math.min((float) viewWidth, (float) viewHeight)
/ Math.max((float) viewWidth, (float) viewHeight);
}
String[] COMMA_PATTERN = previewSizeValueString.split(",");
for (String prewsizeString : COMMA_PATTERN) {
prewsizeString = prewsizeString.trim();
int dimPosition = prewsizeString.indexOf('x');
if (dimPosition == -1) {
continue;
}
float newX = 0;
float newY = 0;
try {
newX = Float.parseFloat(prewsizeString.substring(0, dimPosition));
newY = Float.parseFloat(prewsizeString.substring(dimPosition + 1));
} catch (NumberFormatException e) {
continue;
}
float radio = Math.min(newX, newY) / Math.max(newX, newY);
if (tmpRadio == 0) {
tmpRadio = radio;
bestX = newX;
bestY = newY;
} else if (tmpRadio != 0 && (Math.abs(radio - viewRadio)) < (Math.abs(tmpRadio - viewRadio))) {
tmpRadio = radio;
bestX = newX;
bestY = newY;
}
}
if (bestX > 0 && bestY > 0) {
return camera.new Size((int) bestX, (int) bestY);
}
return null;
}
/**
* 设置焦点和测光区域
*
* @param event
*/
public void focusOnTouch(MotionEvent event) {
int[] location = new int[2];
RelativeLayout relativeLayout = (RelativeLayout)getParent();
relativeLayout.getLocationOnScreen(location);
Rect focusRect = Utils.calculateTapArea(mFocusView.getWidth(),
mFocusView.getHeight(), 1f, event.getRawX(), event.getRawY(),
location[0], location[0] + relativeLayout.getWidth(), location[1],
location[1] + relativeLayout.getHeight());
Rect meteringRect = Utils.calculateTapArea(mFocusView.getWidth(),
mFocusView.getHeight(), 1.5f, event.getRawX(), event.getRawY(),
location[0], location[0] + relativeLayout.getWidth(), location[1],
location[1] + relativeLayout.getHeight());
Camera.Parameters parameters = camera.getParameters();
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
if (parameters.getMaxNumFocusAreas() > 0) {
List<Camera.Area> focusAreas = new ArrayList<Camera.Area>();
focusAreas.add(new Camera.Area(focusRect, 1000));
parameters.setFocusAreas(focusAreas);
}
if (parameters.getMaxNumMeteringAreas() > 0) {
List<Camera.Area> meteringAreas = new ArrayList<Camera.Area>();
meteringAreas.add(new Camera.Area(meteringRect, 1000));
parameters.setMeteringAreas(meteringAreas);
}
try {
camera.setParameters(parameters);
} catch (Exception e) {
}
camera.autoFocus(this);
}
/**
* 设置聚焦的图片
* @param focusView
*/
public void setFocusView(FocusView focusView) {
this.mFocusView = focusView;
}
/**
* 设置自动聚焦,并且聚焦的圈圈显示在屏幕中间位置
*/
public void setFocus() {
if(!mFocusView.isFocusing()) {
try {
camera.autoFocus(this);
mFocusView.setX((Utils.getWidthInPx(getContext())-mFocusView.getWidth()) / 2);
mFocusView.setY((Utils.getHeightInPx(getContext())-mFocusView.getHeight()) / 2);
mFocusView.beginFocus();
} catch (Exception e) {
}
}
}
}
3、Activity中使用自定义相机
public class TakePhoteActivity extends Activity implements CameraPreview.OnCameraStatusListener,
SensorEventListener {
private static final String TAG = "TakePhoteActivity";
public static final Uri IMAGE_URI = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
public static final String PATH = Environment.getExternalStorageDirectory()
.toString() + "/AndroidMedia/";
CameraPreview mCameraPreview;
CropImageView mCropImageView;
RelativeLayout mTakePhotoLayout;
LinearLayout mCropperLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置横屏
// setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
// 设置全屏
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_take_phote);
// Initialize components of the app
mCropImageView = (CropImageView) findViewById(R.id.CropImageView);
mCameraPreview = (CameraPreview) findViewById(R.id.cameraPreview);
FocusView focusView = (FocusView) findViewById(R.id.view_focus);
mTakePhotoLayout = (RelativeLayout) findViewById(R.id.take_photo_layout);
mCropperLayout = (LinearLayout) findViewById(R.id.cropper_layout);
mCameraPreview.setFocusView(focusView);
mCameraPreview.setOnCameraStatusListener(this);
mCropImageView.setGuidelines(2);
mSensorManager = (SensorManager) getSystemService(Context.
SENSOR_SERVICE);
mAccel = mSensorManager.getDefaultSensor(Sensor.
TYPE_ACCELEROMETER);
}
boolean isRotated = false;
@Override
protected void onResume() {
super.onResume();
if(!isRotated) {
TextView hint_tv = (TextView) findViewById(R.id.hint);
ObjectAnimator animator = ObjectAnimator.ofFloat(hint_tv, "rotation", 0f, 90f);
animator.setStartDelay(800);
animator.setDuration(1000);
animator.setInterpolator(new LinearInterpolator());
animator.start();
View view = findViewById(R.id.crop_hint);
AnimatorSet animSet = new AnimatorSet();
ObjectAnimator animator1 = ObjectAnimator.ofFloat(view, "rotation", 0f, 90f);
ObjectAnimator moveIn = ObjectAnimator.ofFloat(view, "translationX", 0f, -50f);
animSet.play(animator1).before(moveIn);
animSet.setDuration(10);
animSet.start();
isRotated = true;
}
mSensorManager.registerListener(this, mAccel, SensorManager.SENSOR_DELAY_UI);
}
@Override
protected void onPause() {
super.onPause();
mSensorManager.unregisterListener(this);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
Log.e(TAG, "onConfigurationChanged");
super.onConfigurationChanged(newConfig);
}
public void takePhoto(View view) {
if(mCameraPreview != null) {
mCameraPreview.takePicture();
}
}
public void close(View view) {
finish();
}
/**
* 关闭截图界面
* @param view
*/
public void closeCropper(View view) {
showTakePhotoLayout();
}
/**
* 开始截图,并保存图片
* @param view
*/
public void startCropper(View view) {
//获取截图并旋转90度
CropperImage cropperImage = mCropImageView.getCroppedImage();
Log.e(TAG, cropperImage.getX() + "," + cropperImage.getY());
Log.e(TAG, cropperImage.getWidth() + "," + cropperImage.getHeight());
Bitmap bitmap = Utils.rotate(cropperImage.getBitmap(), -90);
// Bitmap bitmap = mCropImageView.getCroppedImage();
// 系统时间
long dateTaken = System.currentTimeMillis();
// 图像名称
String filename = DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken)
.toString() + ".jpg";
Uri uri = insertImage(getContentResolver(), filename, dateTaken, PATH,
filename, bitmap, null);
cropperImage.getBitmap().recycle();
cropperImage.setBitmap(null);
Intent intent = new Intent(this, ShowCropperedActivity.class);
intent.setData(uri);
intent.putExtra("path", PATH + filename);
intent.putExtra("width", bitmap.getWidth());
intent.putExtra("height", bitmap.getHeight());
intent.putExtra("cropperImage", cropperImage);
startActivity(intent);
bitmap.recycle();
finish();
super.overridePendingTransition(R.anim.fade_in,
R.anim.fade_out);
// doAnimation(cropperImage);
}
private void doAnimation(CropperImage cropperImage) {
ImageView imageView = new ImageView(this);
View view = LayoutInflater.from(this).inflate(
R.layout.image_view_layout, null);
((RelativeLayout) view.findViewById(R.id.root_layout)).addView(imageView);
RelativeLayout relativeLayout = ((RelativeLayout) findViewById(R.id.root_layout));
// relativeLayout.addView(imageView);
imageView.setX(cropperImage.getX());
imageView.setY(cropperImage.getY());
ViewGroup.LayoutParams lp = imageView.getLayoutParams();
lp.width = (int)cropperImage.getWidth();
lp.height = (int) cropperImage.getHeight();
imageView.setLayoutParams(lp);
imageView.setImageBitmap(cropperImage.getBitmap());
try {
getWindow().addContentView(view, lp);
} catch (Exception e) {
e.printStackTrace();
}
/*AnimatorSet animSet = new AnimatorSet();
ObjectAnimator translationX = ObjectAnimator.ofFloat(this, "translationX", cropperImage.getX(), 0);
ObjectAnimator translationY = ObjectAnimator.ofFloat(this, "translationY", cropperImage.getY(), 0);*/
TranslateAnimation translateAnimation = new TranslateAnimation(
0, -cropperImage.getX(), 0, -(Math.abs(cropperImage.getHeight() - cropperImage.getY())));// 当前位置移动到指定位置
RotateAnimation rotateAnimation = new RotateAnimation(0, -90,
Animation.ABSOLUTE, cropperImage.getX() ,Animation.ABSOLUTE, cropperImage.getY());
AnimationSet animationSet = new AnimationSet(true);
animationSet.addAnimation(translateAnimation);
animationSet.addAnimation(rotateAnimation);
animationSet.setFillAfter(true);
animationSet.setDuration(2000L);
imageView.startAnimation(animationSet);
// finish();
}
/**
* 拍照成功后回调
* 存储图片并显示截图界面
* @param data
*/
@Override
public void onCameraStopped(byte[] data) {
Log.i("TAG", "==onCameraStopped==");
// 创建图像
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
// 系统时间
long dateTaken = System.currentTimeMillis();
// 图像名称
String filename = DateFormat.format("yyyy-MM-dd kk.mm.ss", dateTaken)
.toString() + ".jpg";
// 存储图像(PATH目录)
Uri source = insertImage(getContentResolver(), filename, dateTaken, PATH,
filename, bitmap, data);
//准备截图
try {
mCropImageView.setImageBitmap(MediaStore.Images.Media.getBitmap(this.getContentResolver(), source));
// mCropImageView.rotateImage(90);
} catch (IOException e) {
Log.e(TAG, e.getMessage());
}
showCropperLayout();
}
/**
* 存储图像并将信息添加入媒体数据库
*/
private Uri insertImage(ContentResolver cr, String name, long dateTaken,
String directory, String filename, Bitmap source, byte[] jpegData) {
OutputStream outputStream = null;
String filePath = directory + filename;
try {
File dir = new File(directory);
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(directory, filename);
if (file.createNewFile()) {
outputStream = new FileOutputStream(file);
if (source != null) {
source.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
} else {
outputStream.write(jpegData);
}
}
} catch (FileNotFoundException e) {
Log.e(TAG, e.getMessage());
return null;
} catch (IOException e) {
Log.e(TAG, e.getMessage());
return null;
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (Throwable t) {
}
}
}
ContentValues values = new ContentValues(7);
values.put(MediaStore.Images.Media.TITLE, name);
values.put(MediaStore.Images.Media.DISPLAY_NAME, filename);
values.put(MediaStore.Images.Media.DATE_TAKEN, dateTaken);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.DATA, filePath);
return cr.insert(IMAGE_URI, values);
}
private void showTakePhotoLayout() {
mTakePhotoLayout.setVisibility(View.VISIBLE);
mCropperLayout.setVisibility(View.GONE);
}
private void showCropperLayout() {
mTakePhotoLayout.setVisibility(View.GONE);
mCropperLayout.setVisibility(View.VISIBLE);
mCameraPreview.start(); //继续启动摄像头
}
private float mLastX = 0;
private float mLastY = 0;
private float mLastZ = 0;
private boolean mInitialized = false;
private SensorManager mSensorManager;
private Sensor mAccel;
@Override
public void onSensorChanged(SensorEvent event) {
float x = event.values[0];
float y = event.values[1];
float z = event.values[2];
if (!mInitialized){
mLastX = x;
mLastY = y;
mLastZ = z;
mInitialized = true;
}
float deltaX = Math.abs(mLastX - x);
float deltaY = Math.abs(mLastY - y);
float deltaZ = Math.abs(mLastZ - z);
if(deltaX > 0.8 || deltaY > 0.8 || deltaZ > 0.8){
mCameraPreview.setFocus();
}
mLastX = x;
mLastY = y;
mLastZ = z;
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
}
actiity中注册了SensorEventListener,也就是使用传感器监听用户手机的移动,如果有一定距离的移动,则自动聚焦,这样体验好一点。
我对比了一下小猿搜题和学霸君两款app的拍照功能,个人感觉小猿搜题的体验要好一些,因为从主界面进入拍照界面,连个界面没有一个旋转的过渡,而学霸君就有一个过渡,有一丝丝的影响体验。也就是说学霸君的拍照界面是横屏的,在activity的onCreate方法里面调用了setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE)来设置全屏,而切换界面的时候又从竖屏切换为横屏,就会有个过渡的效果,影响了体验。
个人猜测小猿搜题是将拍照界面的activity设置为竖屏,而将摄像头直接旋转90度,这样就强制用户横屏拍摄,当然,拍完之后还要将图片旋转回来。所以我参考小猿搜题来实现的,毕竟体验为王嘛。
如上图(其实是竖屏),红色圈起来的其实是放到底部,然后将屏幕中间的文字旋转90度(带有动画,起了提示用户横屏拍照的作用),就给人的感觉是横屏的。了。
还有一点就是小猿搜题拍完照到截图过渡的很自然,感觉很流畅,估计是拍照和截图放在同一个activity中的,如果是两个activty,涉及到界面切换,肯定不会那么自然。所以我也将拍照和截图放在一个界面,拍照完就将自定义相机隐藏,将截图界面显示出来,这样切换就很流畅了。
项目中截图的功能我是从github上面找的一个开源库cropper:https://github.com/edmodo/cropper
因为ocr图片识别的代码是公司的,所以识别的功能没有添加到demo里面去。
相关文章
- 今天小编在这里就来给各位photoshop的这一款软件的使用者们来说一说设计一幅大鱼海棠动画片海报制作的实例教程,各位想知道具体制作步骤的使用者们,那么各位就快来看看...2016-09-14
- 下面我们来看一篇关于Android子控件超出父控件的范围显示出来方法,希望这篇文章能够帮助到各位朋友,有碰到此问题的朋友可以进来看看哦。 <RelativeLayout xmlns:an...2016-10-02
- ps软件是一款非常不错的图片处理软件,有着非常不错的使用效果。这次文章要给大家介绍的是ps怎么制作倒影,一起来看看设计倒影的方法。 用ps怎么做倒影最终效果̳...2017-07-06
- 这篇文章主要为大家介绍了JavaScript设计模式中的装饰者模式,对JavaScript设计模式感兴趣的小伙伴们可以参考一下...2016-01-21
- 神马是“解释器模式”?先翻开《GOF》看看Definition:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。在开篇之前还是要科普几个概念: 抽象语法树: 解释器模式并未解释如...2014-06-07
- 这篇文章主要介绍了Postgresl 如何选择正确的关闭模式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-01-18
Android开发中findViewById()函数用法与简化
findViewById方法在android开发中是获取页面控件的值了,有没有发现我们一个页面控件多了会反复研究写findViewById呢,下面我们一起来看它的简化方法。 Android中Fin...2016-09-20- 如果我们的项目需要做来电及短信的功能,那么我们就得在Android模拟器开发这些功能,本来就来告诉我们如何在Android模拟器上模拟来电及来短信的功能。 在Android模拟...2016-09-20
- 这篇文章主要介绍了C语言程序设计第五版谭浩强课后答案(第二章答案),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2021-04-02
- 夜神android模拟器如何设置代理呢?对于这个问题其实操作起来是非常的简单,下面小编来为各位详细介绍夜神android模拟器设置代理的方法,希望例子能够帮助到各位。 app...2016-09-20
- 为了增强android应用的用户体验,我们可以在一些Button按钮上自定义动态的设置一些样式,比如交互时改变字体、颜色、背景图等。 今天来看一个通过重写Button来动态实...2016-09-20
- 如果我们要在Android应用APP中加载html5页面,我们可以使用WebView,本文我们分享两个WebView加载html5页面实例应用。 实例一:WebView加载html5实现炫酷引导页面大多...2016-09-20
- 今天小编在这里就来给Photoshop的这一款软件的使用者们来说下计商务名片的5种常见思路,各位想知道的使用者,那么下面就快来跟着小编一起看一看吧。 给各位Photosho...2016-09-14
- 深入理解Android中View和ViewGroup从组成架构上看,似乎ViewGroup在View之上,View需要继承ViewGroup,但实际上不是这样的。View是基类,ViewGroup是它的子类。本教程我们深...2016-09-20
- 下面我们来看一篇关于Android自定义WebView网络视频播放控件开发例子,这个文章写得非常的不错下面给各位共享一下吧。 因为业务需要,以下代码均以Youtube网站在线视...2016-10-02
- java开发的Android应用,性能一直是一个大问题,,或许是Java语言本身比较消耗内存。本文我们来谈谈Android 性能优化之MemoryFile文件读写。 Android匿名共享内存对外A...2016-09-20
- 很多集成的PHP环境(PHPnow WAMP Appserv等)自带的MySQL貌似都没有开启MySQL的严格模式,何为MySQL的严格模式,简单来说就是MySQL自身对数据进行严格的校验(格式、长度、类型等),比如一个整型字段我们写入一个字符串类型的数...2013-10-04
- TextView默认是横着显示了,今天我们一起来看看Android设置TextView竖着显示如何来实现吧,今天我们就一起来看看操作细节,具体的如下所示。 在开发Android程序的时候,...2016-10-02
android.os.BinderProxy cannot be cast to com解决办法
本文章来给大家介绍关于android.os.BinderProxy cannot be cast to com解决办法,希望此文章对各位有帮助呀。 Android在绑定服务的时候出现java.lang.ClassCastExc...2016-09-20- 最近在网上看到了新版的360安全卫士,感觉界面还不错,于是用WPF制作了一个,时间有限,一些具体的控件没有制作,用图片代替了。感兴趣的朋友一起跟着小编学习WPF实现类似360安全卫士界面的程序源码分享...2020-06-25