Android手机截屏程序
一:程序运行的大致思路
1、运行程序,显示一个Activity界面,点击按钮,显示一个浮窗。这里用到一个显示浮窗的小技术。
2、在显示浮窗的同时,会启动一个server服务,这个服务很重要,因为在这里会建立java端和android底层(即c语言端)的通信机制。这个地方比较抽象。以后再来解释,你就记住它是一个通信机制,相当于客户端和服务器端的关系
3、点击一下浮窗,就会向android底层发送消息,开始截屏,这里的向android底层发送消息,采用了android源代码里面的通信机制,我直接就把android源码拿来用了。
4、开始截屏,这里的截屏程序也是用的android自带的截屏程序,也是android源码,但是我在这里做了很多的工作。因为截屏出来的图片,我需要保存为png格式。
5、在保存png格式的图片的时候,我又使用了第三方的一个png库。
6、最后的运行效果相当于点击浮窗,开始截屏,再点击浮窗,停止截屏,图片会自动保存到/sdcard/DCIM/这个目录下面。
二:改程序在做的过程中使用到的一些技术
1、要用到android的应用程序开发的基本知识,这里就不多说了
2、用到了ndk开发技术
3、既然使用ndk开发,那C/C++的技术就不得不用了
4、因为做ndk开发,我觉的就相当于在Linux系统下面做c语言的开发,那Linux开发中使用的一些东西也就需要了。
5、最后一点,你的手机需要root。因为截屏的基本思路就读取屏幕像素在内存中的映射,所有需要直接读取内存中的内容,root是必须的。
三:程序开发的具体过程
1、从现在起,我会一步一步的把这个程序再重新做一遍,目的就是希望能把之前学习到的东西再回顾一遍,温故而知新。
2、先建立一个截屏项目工程,名字随便吧,我的叫screenshot如图
3、在Activity_main.xml文件中添加一个Button控件,如图
在MainActivity.java这个类里面做一些简单的初始化,为Button按钮添加点击响应事件,显示浮窗。因为显示浮窗,程序会直接跳到手机的桌面,所以这里有一个小技术,从应用程序直接跳到手机桌面,代码如下。
/**
*返回到主桌面类似按下Home按钮
*
*@paramcontext
*/
publicstaticvoidbackToHomePage(Contextcontext){
Intenti=newIntent(Intent.ACTION_MAIN);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.addCategory(Intent.CATEGORY_HOME);
context.startActivity(i);
}
4、要新建一个ScreenCaptureServer类,这个类就非常重要了,它主要有这几个作用,首先要创建一个浮窗,其次就是要建立与android底层的连接,然后要维护与android底层的通信。最后也是最重要的,建立截屏程序的独立线程。首先做一个浮窗,很简单,这里就不多说了,具体可以看我的另一篇博客-----android之浮窗篇。
5、实现android的java端与底层的c端通信。因为这里是调用的系统的通信代码,好多我也没有搞清楚。不过现在到是能用,在该程序里面,java端充当的是服务器端,它随service的启动开始运行,不停的监听来自客户端(c端的连接)。下面是服务器线程的具体代码。
/**
*服务器端的线程
*
*@authorjeck
*
*/
privateclassServerSocketThreadextendsThread{
/**
*线程运行的标示
*/
privatebooleankeeprunning=true;
/**
*本地服务socket
*/
privateLocalServerSocketlocalServerSocket;
@Override
publicvoidrun(){
try{
//创建一个本地socket
localServerSocket=newLocalServerSocket("screen_shot_socket");
}catch(Exceptione){
e.printStackTrace();
keeprunning=false;
}
//通过while循环,轮训从客户端发过来的连接请求
while(keeprunning){
Log.d(TAG,"Waittingforclienttoconnect!!");
try{
//监听客户端的连接
LocalSocketinteractClientSocket=localServerSocket
.accept();
//因为有可能在等待客户端连接的时候,accept阻塞了。Activity被finish掉,所以有必要在检测一次
if(keeprunning){
Log.d(TAG,"nowclientcoming!!!");
//开始为客户端服务
newInteractClientSocketThread(interactClientSocket)
.start();
}
}catch(Exceptione){
e.printStackTrace();
keeprunning=false;
}
}
//如果断开连接,那就关闭服务
if(localServerSocket!=null){
try{
localServerSocket.close();
}catch(Exceptione){
e.printStackTrace();
}
}
}
///**
//*停止线程
//*/
//publicvoidstopRunning(){
//
//keeprunning=false;
//}
}
6、接下来的是service里面的第三个功能,就是维护与客户端(c端)的通信了。这里无非就是监听连接,连接到了就发送消息。因为要配合我的主要功能,截屏,所有我这里的逻辑是启动service,开启服务器监听,如果有客户端连接,会向客户端发送一个数字2,如果客户端收到2,会向服务器端发送2222表示连接成功,然后如果你点击了我们的小浮窗,会向客户端发送数字1,表示开始截屏,客户端收到消息会想服务器端发送1111表示我客户端已经收到消息了。然后你再点击一次小浮窗,服务器端会发送0,表示停止截屏,那客户端收到0会停止截屏,并向服务器端反馈0000.这就是我的通信机制。下面我把代码粘出来。
/**
*服务器与客户端直接的通信线程
*
*@authorjeck
*
*/
privateclassInteractClientSocketThreadextendsThread{
/**
*本地socket
*/
privateLocalSocketinteractClientSocket;
/**
*输入流
*/
privateInputStreaminputStream=null;
/**
*输出流
*/
privateOutputStreamoutputStream=null;
/**
*从客户端发来的消息
*/
privateStringBuilderreceiveFromClientString=newStringBuilder();
/**
*输入缓存区
*/
privatechar[]readBuffer=newchar[4096];
/**
*输入字节数
*/
privateintreadBytes=-1;
/**
*构造函数
*/
publicInteractClientSocketThread(LocalSocketinteractClientSocket){
this.interactClientSocket=interactClientSocket;
}
/**
*从客户端读取数据
*
*@return
*/
privatebooleanreadDataFromClient(){
booleanreadResult=false;
//从本地连接中获取输入流
try{
inputStream=interactClientSocket.getInputStream();
//读数据
InputStreamReaderinputStreamReader=newInputStreamReader(
inputStream);
//从输入流中读取数据
while((readBytes=inputStreamReader.read(readBuffer))!=-1){
StringtmpStr=newString(readBuffer,0,readBytes);
receiveFromClientString.append(tmpStr).append("\n");
}
if(receiveFromClientString.toString()!=null){
if(receiveFromClientString.toString().startsWith("0000")
||receiveFromClientString.toString().startsWith(
"1111")){
captureState=2;
}
//显示client发送的消息
Log.d(TAG,receiveFromClientString.toString());
//读取时间成功
readResult=true;
}
}catch(IOExceptione){
e.printStackTrace();
}
returnreadResult;
}
/**
*向客户端写数据
*
*@return
*/
privatebooleanwriteDataToClient(StringwriteContent){
booleanwriteResult=false;
try{
outputStream=interactClientSocket.getOutputStream();
//如果点击了开始录屏,则发送消息
if(writeContent!=null&&!"".equals(writeContent)){
outputStream.write(writeContent.getBytes());
}
writeResult=true;
}catch(IOExceptione){
e.printStackTrace();
writeResult=false;
}
returnwriteResult;
}
@Override
publicvoidrun(){
try{
switch(captureState){
case0:
//停止截屏
writeDataToClient(STOP_CAPTURE_SCREEN);
break;
case1:
//开始录屏
writeDataToClient(START_CAPTURE_SCREEN);
break;
case2:
//等待连接
writeDataToClient(KEEP_CONNECTION);
break;
}
readDataFromClient();
}catch(Exceptione){
e.printStackTrace();
Log.d(TAG,"receivedatafailed!!");
}finally{
if(outputStream!=null){
try{
outputStream.close();
}catch(Exceptione){
e.printStackTrace();
}
}
if(inputStream!=null){
try{
inputStream.close();
}catch(Exceptione){
e.printStackTrace();
}
}
}
}
}
7、接下来是最关键的部分,呵呵,其实那个地方都很关键,缺少了任何一个地方,这个程序也跑不起来。废话不多说了,现在简绍截屏进程了。这进程的工作原理是这样的。1需要手机root权限,在获取手机root权限之后,通过ndk的混合编译器,编译一个exe文件,在root权限下面,通过代码执行这个exe程序。换句话说就是相当于在java端,执行exe文件。因为android系统是基于Linux操作系统的,所以你的exe想要直接执行,必须获取一点的权限,就算你在Linux系统下面直接写代码,那你执行./xxxx程序的时候,也是需要权限的。2就是要创建一个进程了(process)。你执行了exe程序,就相当于你创建了一个进程,所以要把这个进程获取出来,以便操作。最后这些东西都是在一个单独的线程中运行的,来代码。
/**
*响应点击截屏按钮
*/
privatebooleanscreenCapture(){
booleanresult=false;
try{
//创建log对象
screenLog=newStringBuilder();
//创建一个进程
logcatProcess=RuntimeHelper.getLogcatProcess(this);
//创建一个缓冲
bufferedReader=newBufferedReader(newInputStreamReader(
logcatProcess.getInputStream()),8192);
Stringline;
while((line=bufferedReader.readLine())!=null){
Log.d(TAG,line);
screenLog.append(line).append("\n");
if(line.startsWith("Success")){
result=true;
}
}
}catch(Exceptione){
e.printStackTrace();
result=false;
}
returnresult;
}
/**
*使用异步线程执行截屏操作
*
*@authorjeck
*
*/
privateclassScreenCaptureTaskextendsAsyncTask<Void,Void,Boolean>{
@Override
protectedBooleandoInBackground(Void...params){
returnscreenCapture();
}
@Override
protectedvoidonPostExecute(Booleanresult){
if(result){
//将日志保存在SD卡上
//try{
////Utils.saveCaptureLog(screenLog.toString());
//}catch(Exceptione){
//e.printStackTrace();
//}
Toast.makeText(getApplicationContext(),"截屏成功",
Toast.LENGTH_LONG).show();
}
}
}
8、以上就是我们这个截屏程序的java端的所有代码了,这只是一个好的开始。
在java端的功能就是显示一个浮窗,然后点击浮窗,会开始和C端进行交互。
C端涉及的东西就都是C语言的了,这里面都是ndk的知识了,这里首先要编写一个.cpp文件,就是我们的通信程序,因为这里用的是android源代码,所以这个程序目前只适合android4.1系统的。其他版本暂时没有测试。下面这是我的exe程序的主要代码。
#include<stdio.h>
#include<sys/system_properties.h>
#include<dlfcn.h>
#include<android/log.h>
#include<sys/socket.h>
#include<cutils/sockets.h>
#include<sys/un.h>
#include<unistd.h>
#include<stddef.h>
#include<pthread.h>
#include<string.h>
#include"screen_capture_image.h"
#defineTAG"--screen_capture-->"
intsocketID;
/*
*获取当前的系统版本
*/
intgetCurrentSDKVersion(){
intsdk;
charc[PROP_VALUE_MAX];
if(__system_property_get("ro.build.version.sdk",c)>0){
sscanf(c,"%d",&sdk);
}else{
sdk=8;
}
returnsdk;
}
/*
*Class:com_rdtd_jni_SendMessageFromClientJNI
*Method:startHeartBead
*Signature:()I
*/
intconnection_to_server(){
charpath[]="screen_shot_socket";
socketID=socket_local_client(path,ANDROID_SOCKET_NAMESPACE_ABSTRACT,
SOCK_STREAM);
//如果连接失败
if(socketID<0){
returnsocketID;
}else{
return1;
}
}
voidclose_connection(){
if(socketID){
close(socketID);
}
}
intread_data_from_server(){
//0表示停止录屏,1表示开始录屏,2表示连接中,-1表示读取数据出错
intresult;
//读取的字数
intread_result;
charreadBuffer[2];
memset(readBuffer,0,2);
read_result=read(socketID,readBuffer,2);
if(read_result){
result=atoi(readBuffer);
//printf("---->readdatasuccess:%d\n",result);
}else{
result=-1;
}
returnresult;
}
intwrite_data_to_server(constchar*str){
intwrite_result;
write_result=write(socketID,str,strlen(str));
if(write_result){
close_connection();
return1;
}else{
printf("writedatafailed!\n");
close_connection();
return0;
}
}
void*begin_capture(void*){
longresult=1;
//设置截屏开始的标示
set_capture_flag_png(1);
//截屏并保存成png图片,现在在联想的机器上是不行的。一直报的是找不到libpng。so文件
screen_capture_png();
return(void*)result;
}
/*
*截屏程序的入口,截屏参数都放在这里面。
*argv[1]表示宽度,argv[2]表示高度
*默认帧率15帧每秒,宽度和高度是手机屏幕的宽高
*/
intmain(intargc,char*argv[]){
intcount=0;
pthread_tthread_id;
void*thread_result;
//循环读写数据
while(connection_to_server()){
//先读取数据
intread_result=read_data_from_server();
printf("---->receivemessage=%d\n",read_result);
__android_log_print(ANDROID_LOG_DEBUG,TAG,"receivemessage=%d",
read_result);
switch(read_result){
case0:
//停止录制
write_data_to_server("0000---------->");
set_capture_flag_png(0);
if(pthread_join(thread_id,&thread_result)==-1){
printf("waitingthreadfailed!\n");
}else{
if((long)thread_result==0){
printf("screen_capreturnfailed\n");
}else{
printf("Success------------------>\n");
//这个地方必须返回,否则保存的图片都是黑屏图片,不知道是为什么,呵呵,应该是没有关闭文件,只有函数返回了,系统自动关闭文件
exit(0);
}
}
break;
case1:
//开始录制
write_data_to_server("1111---------->");
if(pthread_create(&thread_id,NULL,begin_capture,NULL)==-1){
printf("createthreadfailed!\n");
}
break;
case2:
//连接中
write_data_to_server("2222---------->");
break;
case-1:
//读取数据出错
printf("---->readdatafailed!\n");
gotoexit;
}
sleep(3);
}
exit:close_connection();
return0;
}
这个cpp文件就会被ndk编译成我们在上面提到的exe文件,就是Linux系统下面的可执行文件。只有这个文件,我们能做的就是和java端发个信息而已,还是不能截屏的,需要截屏的程序在下面,也是用的android源代码,不过基本让我改的没有源代码的味道了。
在这里,我使用了第三方的png库,我把它编译成静态库,然后链接到我的cpp文件中,最后把cpp文件编译成exe文件,再在代码中执行exe文件。下面是截屏的主要代码。
/*
*Copyright(C)TheAndroidOpenSourceProject
*
*LicensedundertheApacheLicense,Version2.0(the"License");
*youmaynotusethisfileexceptincompliancewiththeLicense.
*YoumayobtainacopyoftheLicenseat
*
*/licenses/LICENSE-2.0
*
*Unlessrequiredbyapplicablelaworagreedtoinwriting,software
*distributedundertheLicenseisdistributedonan"ASIS"BASIS,
*WITHOUTWARRANTIESORCONDITIONSOFANYKIND,eitherexpressorimplied.
*SeetheLicenseforthespecificlanguagegoverningpermissionsand
*limitationsundertheLicense.
*/
#include<errno.h>
#include<unistd.h>
#include<stdio.h>
#include<fcntl.h>
#include<ui/PixelFormat.h>
#include<zlib.h>
#include<libpng/png.h>
#include<time.h>
#include<stdlib.h>
#include<malloc.h>
#include<pthread.h>
#include<linux/fb.h>
#include<sys/ioctl.h>
#include<sys/mman.h>
#include<gui/SurfaceComposerClient.h>
#include"screen_capture_image.h"
#include<android/log.h>
//#ifdefANDROID_KK//4.4
//#include<binder/ProcessState.h>
//#include<gui/ISurfaceComposer.h>
//#else
#include<binder/IMemory.h>
//#endif
usingnamespaceandroid;
#defineTAG"--screen_capture-->"
//截屏标识
staticintcapture_flag=0;
//录音标识
staticintrecord_flag=0;
intPushTime=0;
//获取当前时间(microsecond)
int64_tgetCurrentTime(){
structtimevaltv;
gettimeofday(&tv,NULL);
return(1000000LL*tv.tv_sec)+tv.tv_usec;
}
//#ifdefANDROID_KK
//staticuint32_tDEFAULT_DISPLAY_ID=ISurfaceComposer::eDisplayIdMain;
//#endif
//显示错误信息
voiderror(constchar*msg){
fprintf(stderr,"%s:%s:\n",msg,strerror(errno));
exit(1);
}
typedefstructparam{
void*fb_in;
FILE*fb_out;
intwidth;
intheight;
int64_tusedTime;
size_tbuffer_size;
param*next;
}image_info;
//获取当前日期,以秒为单位,现在我一秒之内可以截取3张图片,他的名字当然一样了
char*getLocalTime(){
charcurrentTime[128];
memset(currentTime,0,128);
int64_ttime=getCurrentTime();
sprintf(currentTime,"%lld",time);
returncurrentTime;
}
//创建视频路径
voidcreate_video_path(char*path){
memset(path,0,256);
strcpy(path,"/sdcard/DCIM/Record_");
strcat(path,getLocalTime());
strcat(path,".mp4");
printf("---->path=%s\n",path);
}
//创建视频路径
voidcreate_image_path(char*path){
memset(path,0,256);
strcpy(path,"/sdcard/DCIM/capture_");
strcat(path,getLocalTime());
strcat(path,".png");
printf("---->path=%s\n",path);
}
//创建截屏图片信息的节点
image_info*create_image_info_node(constvoid*in,FILE*out,intw,inth,
size_tsize,int64_tut){
image_info*newInfo=(image_info*)malloc(sizeof(image_info));
//分配内存
newInfo->fb_in=malloc(size);
//拷贝内存
memcpy(newInfo->fb_in,in,size);
newInfo->fb_out=out;
newInfo->buffer_size=size;
newInfo->width=w;
newInfo->height=h;
newInfo->usedTime=ut;
newInfo->next=NULL;
printf("---->createanewimagenode\n");
returnnewInfo;
}
//释放内存
voidrelease_image_node(image_info*node){
if(node!=NULL){
free(node->fb_in);
node->fb_in=NULL;
free(node);
node=NULL;
printf("---->successtoreleaseaimagenode\n");
}
}
/*
*fb_base屏幕左上角的第一个像素的内存地址
*fb_out生成的png保存的地址
*w屏幕的宽度
*h屏幕的高度
*fpng的文件格式
*/
//voidtake_screenshot(char*fb_base,FILE*fb_out,intw,inth,intf){
inttake_screenshot(image_info*argu){
longresult=0;
//printf("take_screenshotisrunning!\n");
//image_info*argu=(image_info*)param;
//png结构
png_structppng;
//pnginfo
png_infopinfo;
structfb_var_screeninfovinfo;
//因为是一行一行的扫描屏幕,r是行数
unsignedintr;
//每一行的长度,是右屏幕的宽度(像素)*每一个像素的大小(32位4个字节)
unsignedintrowlen;
//每一个像素所占的大小,4个字节
unsignedintbytespp=4;
//创建一个png结构
png=png_create_write_struct(PNG_LIBPNG_VER_STRING,NULL,NULL,NULL);
//printf("1--->png_create_write_structisruned\n");
if(png==NULL){
ALOGE("failedpng_create_write_struct\n");
}
png_init_io(png,argu->fb_out);
//printf("2--->png_init_ioisruned\n");
info=png_create_info_struct(png);
//printf("3--->png_create_info_structisruned\n");
if(info==NULL){
ALOGE("failedpng_create_info_struct\n");
png_destroy_write_struct(&png,NULL);
}
if(setjmp(png_jmpbuf(png))){
ALOGE("failedpngsetjmp\n");
png_destroy_write_struct(&png,NULL);
}
//设置png的各种信息
png_set_IHDR(png,info,argu->width,argu->height,8,
PNG_COLOR_TYPE_RGB_ALPHA,PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_BASE,PNG_FILTER_TYPE_BASE);
//printf("4--->png_set_IHDRisruned\n");
png_write_info(png,info);
//printf("5--->png_write_infoisruned\n");
//计算每一行的长度
rowlen=argu->width*bytespp;
//新建一个临时变量,保存图片的内存块地址
png_byteptemp=(png_bytep)argu->fb_in;
//循环扫描屏幕,一行一行的读取数据
for(r=0;r<argu->height;r++){
//将第r行写到png结构中
png_write_row(png,temp);
//计算下一行的起始位置
temp+=rowlen;
}
//写png的信息
png_write_end(png,info);
//printf("6--->png_write_endisruned\n");
png_destroy_write_struct(&png,NULL);
//printf("7--->png_destroy_write_structisruned\n");
//保存完图片,就把文件关闭掉
if(fclose(argu->fb_out)==-1){
error("closefilefailed!");
}else{
//printf("---->successtosaveaimage\n");
temp=NULL;
result=1;
}
returnresult;
}
//保存图片,返回保存成功的图片数量,如果不等于24,则说明保存失败,成功返回1,失败,返回0
void*save_image(void*start){
longresult=0;
image_info*p=(image_info*)start;
image_info*next=NULL;
while(p!=NULL){
//先将下一个节点的指针保存在next里面
next=p->next;
//直接保存,如果成功,则释放该节点所占的内存
if(take_screenshot(p)==1){
release_image_node(p);
}else{
error("saveimagefailed");
}
//最后再将next节点复制给p,继续操作
p=next;
}
result=1;
return(void*)result;
}
/*截屏程序*/
image_info*screen_shot(){
staticint64_tbase_time=getCurrentTime();
//截屏的开始时间和结束时间
int64_tbeginTime=0,usedTime=0;
constvoid*base;
size_tsize;
uint32_twidth,height;
FILE*fb_out=NULL;
//#ifdefANDROID_KK
//ProcessState::self()->startThreadPool();
//#endif
//
//#ifdefANDROID_KK
//int32_tdisplayId=DEFAULT_DISPLAY_ID;
//#endif
ScreenshotClientscreenshot;
beginTime=getCurrentTime();
//#ifdefANDROID_KK
//sp<IBinder>display=SurfaceComposerClient::getBuiltInDisplay(displayId);
//if(display!=NULL&&screenshot.update(display)==NO_ERROR){
//#else
if(screenshot.update()==NO_ERROR){
//#endif
base=screenshot.getPixels();
size=screenshot.getSize();
width=screenshot.getWidth();
height=screenshot.getHeight();
//计算截图时间
usedTime=getCurrentTime()-beginTime;
printf("---->Image:width=%d,height=%d,usedtime=%lld\n",
screenshot.getWidth(),screenshot.getHeight(),usedTime);
charimage_path[256];
create_image_path(image_path);
fb_out=fopen(image_path,"w");
returncreate_image_info_node(base,fb_out,width,height,size,
usedTime);
}
returnNULL;
}
//截屏并生成视频链表
void*capture_and_link(void*){
image_info*start=NULL;
image_info*newNode=NULL;
image_info*tail=NULL;
//开始截屏并生成截屏链表
while(capture_flag){
//保存视频流
newNode=screen_shot();
if(newNode!=NULL){
if(start==NULL){
start=newNode;
}
if(tail!=NULL){
tail->next=newNode;
}
tail=newNode;
}else{
printf("---->screen_shotreturnNULL!\n");
returnNULL;
}
//睡3秒,不然录制的太快
sleep(3);
}
return(void*)start;
}
#ifdef__cplusplus
extern"C"{
#endif
//设置截屏标志
voidset_capture_flag_png(intflag){
capture_flag=flag;
record_flag=flag;
}
//截屏并保存为图片
voidscreen_capture_png(){
//截屏线程,只负责截屏
pthread_tcapture_thread;
if(pthread_create(&capture_thread,NULL,capture_and_link,NULL)==-1){
error("createcapture_threadfailed!");
}
//截屏之后返回的结果,是一个包含截屏图片信息的链表,这个链表保存的东西关系到整个截屏程序的成败
void*capture_result;
if(pthread_join(capture_thread,&capture_result)==-1){
error("waitingcapture_threadfailed!");
}
//截屏完了,开始保存图片
pthread_tsave_thread;
if(pthread_create(&save_thread,NULL,save_image,capture_result)==-1){
error("createsave_threadfailed!");
}
//等待保存图片的线程
void*save_result;
if(pthread_join(save_thread,&save_result)==-1){
error("waitingsave_threadfailed!");
}
if((long)save_result==1){
printf("---->successtosaveimage!\n");
}
}
#ifdef__cplusplus
}
#endif
到此为止,这个截屏程序的主要代码是都将完了,虽然说将的很简单,但是做起来一点都不简单,里面涉及的东西还是很多的。这需要你对ndk编程很熟悉才可能把这个程序顺利的运行起来