天天看點

android之手機截屏小程式Android 手機截屏程式

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按鈕添加點選響應事件,顯示浮窗。因為顯示浮窗,程式會直接跳到手機的桌面,是以這裡有一個小技術,從應用程式直接跳到手機桌面,代碼如下。

    public static void backToHomePage(Context context) {

Intent i = new Intent(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端的連接配接)。下面是伺服器線程的具體代碼。

    private class ServerSocketThread extends Thread {

private boolean keeprunning = true;

private LocalServerSocket localServerSocket;

@Override

public void run() {

    try {

// 建立一個本地socket

localServerSocket = new LocalServerSocket("screen_shot_socket");

    } catch (Exception e) {

e.printStackTrace();

keeprunning = false;

    }

    // 通過while循環, 輪訓從用戶端發過來的連接配接請求

    while (keeprunning) {

Log.d(TAG, "Waitting for client to connect !! ");

try {

    // 監聽用戶端的連接配接

    LocalSocket interactClientSocket = localServerSocket

    .accept();

    // 因為有可能在等待用戶端連接配接的時候,accept阻塞了。Activity被finish掉,是以有必要在檢測一次

    if (keeprunning) {

Log.d(TAG, "now client coming !!!");

// 開始為用戶端服務

new InteractClientSocketThread(interactClientSocket)

.start();

    }

} catch (Exception e) {

    e.printStackTrace();

    keeprunning = false;

}

    }

    // 如果斷開連接配接,那就關閉服務

    if (localServerSocket != null) {

try {

    localServerSocket.close();

} catch (Exception e) {

    e.printStackTrace();

}

    }

}

// 

// public void stopRunning() {

//

// keeprunning = false;

// }

    }

6、接下來的是service裡面的第三個功能,就是維護與用戶端(c端)的通信了。這裡無非就是監聽連接配接,連接配接到了就發送消息。因為要配合我的主要功能,截屏,所有我這裡的邏輯是啟動service,開啟伺服器監聽,如果有用戶端連接配接,會向用戶端發送一個數字2,如果用戶端收到2,會向伺服器端發送2222表示連接配接成功,然後如果你點選了我們的小浮窗,會向用戶端發送數字1,表示開始截屏,用戶端收到消息會想伺服器端發送1111表示我用戶端已經收到消息了。然後你再點選一次小浮窗,伺服器端會發送0,表示停止截屏,那用戶端收到0會停止截屏,并向伺服器端回報0000.這就是我的通信機制。下面我把代碼粘出來。

    private class InteractClientSocketThread extends Thread {

private LocalSocket interactClientSocket;

private InputStream inputStream = null;

private OutputStream outputStream = null;

private StringBuilder receiveFromClientString = new StringBuilder();

private char[] readBuffer = new char[4096];

private int readBytes = -1;

public InteractClientSocketThread(LocalSocket interactClientSocket) {

    this.interactClientSocket = interactClientSocket;

}

private boolean readDataFromClient() {

    boolean readResult = false;

    // 從本地連接配接中擷取輸入流

    try {

inputStream = interactClientSocket.getInputStream();

// 讀資料

InputStreamReader inputStreamReader = new InputStreamReader(

inputStream);

// 從輸入流中讀取資料

while ((readBytes = inputStreamReader.read(readBuffer)) != -1) {

    String tmpStr = new String(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 (IOException e) {

e.printStackTrace();

    }

    return readResult;

}

private boolean writeDataToClient(String writeContent) {

    boolean writeResult = false;

    try {

outputStream = interactClientSocket.getOutputStream();

// 如果點選了開始錄屏,則發送消息

if (writeContent != null && !"".equals(writeContent)) {

    outputStream.write(writeContent.getBytes());

}

writeResult = true;

    } catch (IOException e) {

e.printStackTrace();

writeResult = false;

    }

    return writeResult;

}

@Override

public void run() {

    try {

switch (captureState) {

case 0:

    // 停止截屏

    writeDataToClient(STOP_CAPTURE_SCREEN);

    break;

case 1:

    // 開始錄屏

    writeDataToClient(START_CAPTURE_SCREEN);

    break;

case 2:

    // 等待連接配接

    writeDataToClient(KEEP_CONNECTION);

    break;

}

readDataFromClient();

    } catch (Exception e) {

e.printStackTrace();

Log.d(TAG, "receive data failed !!");

    } finally {

if (outputStream != null) {

    try {

outputStream.close();

    } catch (Exception e) {

e.printStackTrace();

    }

}

if (inputStream != null) {

    try {

inputStream.close();

    } catch (Exception e) {

e.printStackTrace();

    }

}

    }

}

}

7、接下來是最關鍵的部分,呵呵,其實那個地方都很關鍵,缺少了任何一個地方,這個程式也跑不起來。廢話不多說了,現在簡紹截屏程序了。這程序的工作原理是這樣的。1需要手機root權限,在擷取手機root權限之後,通過ndk的混合編譯器,編譯一個exe檔案,在root權限下面,通過代碼執行這個exe程式。換句話說就是相當于在java端,執行exe檔案。因為android系統是基于Linux作業系統的,是以你的exe想要直接執行,必須擷取一點的權限,就算你在Linux系統下面直接寫代碼,那你執行./xxxx 程式的時候,也是需要權限的。2就是要建立一個程序了(process)。你執行了exe程式,就相當于你建立了一個程序,是以要把這個程序擷取出來,以便操作。最後這些東西都是在一個單獨的線程中運作的,來代碼。

    private boolean screenCapture() {

boolean result = false;

try {

    // 建立log對象

    screenLog = new StringBuilder();

    // 建立一個程序

    logcatProcess = RuntimeHelper.getLogcatProcess(this);

    // 建立一個緩沖

    bufferedReader = new BufferedReader(new InputStreamReader(

    logcatProcess.getInputStream()), 8192);

    String line;

    while ((line = bufferedReader.readLine()) != null) {

Log.d(TAG, line);

screenLog.append(line).append("\n");

if (line.startsWith("Success")) {

    result = true;

}

    }

} catch (Exception e) {

    e.printStackTrace();

    result = false;

}

return result;

    }

    private class ScreenCaptureTask extends AsyncTask<Void, Void, Boolean> {

@Override

protected Boolean doInBackground(Void... params) {

    return screenCapture();

}

@Override

protected void onPostExecute(Boolean result) {

    if (result) {

// 将日志儲存在SD卡上

// try {

// // Utils.saveCaptureLog(screenLog.toString());

// } catch (Exception e) {

// 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"

#define TAG "--screen_capture-->"

int socketID;

int getCurrentSDKVersion() {

int sdk;

char c[PROP_VALUE_MAX];

if (__system_property_get("ro.build.version.sdk", c) > 0) {

sscanf(c, "%d", &sdk);

} else {

sdk = 8;

}

return sdk;

}

int connection_to_server() {

char path[] = "screen_shot_socket";

socketID = socket_local_client(path, ANDROID_SOCKET_NAMESPACE_ABSTRACT,

SOCK_STREAM);

//如果連接配接失敗

if (socketID < 0) {

return socketID;

} else {

return 1;

}

}

void close_connection() {

if (socketID) {

close(socketID);

}

}

int read_data_from_server() {

//0表示停止錄屏,1 表示開始錄屏, 2 表示連接配接中, -1 表示讀取資料出錯

int result;

//讀取的字數

int read_result;

char readBuffer[2];

memset(readBuffer, 0, 2);

read_result = read(socketID, readBuffer, 2);

if (read_result) {

result = atoi(readBuffer);

//printf("---->read data success :%d\n", result);

} else {

result = -1;

}

return result;

}

int write_data_to_server(const char *str) {

int write_result;

write_result = write(socketID, str, strlen(str));

if (write_result) {

close_connection();

return 1;

} else {

printf("write data failed !\n");

close_connection();

return 0;

}

}

void* begin_capture(void*) {

long result = 1;

//設定截屏開始的标示

set_capture_flag_png(1);

//截屏并儲存成png圖檔,現在在聯想的機器上是不行的。一直報的是找不到libpng。so檔案

screen_capture_png();

return (void*) result;

}

int main(int argc, char *argv[]) {

int count = 0;

pthread_t thread_id;

void *thread_result;

//循環讀寫資料

while (connection_to_server()) {

//先讀取資料

int read_result = read_data_from_server();

printf("---->receive message = %d\n", read_result);

__android_log_print(ANDROID_LOG_DEBUG, TAG, "receive message = %d",

read_result);

switch (read_result) {

case 0:

//停止錄制

write_data_to_server("0000---------->");

set_capture_flag_png(0);

if (pthread_join(thread_id, &thread_result) == -1) {

printf("waiting thread failed !\n");

} else {

if ((long) thread_result == 0) {

printf("screen_cap return failed \n");

} else {

printf("Success------------------>\n");

//這個地方必須傳回,否則儲存的圖檔都是黑屏圖檔,不知道是為什麼,呵呵,應該是沒有關閉檔案,隻有函數傳回了,系統自動關閉檔案

exit(0);

}

}

break;

case 1:

//開始錄制

write_data_to_server("1111---------->");

if (pthread_create(&thread_id, NULL, begin_capture, NULL) == -1) {

printf("create thread failed !\n");

}

break;

case 2:

//連接配接中

write_data_to_server("2222---------->");

break;

case -1:

//讀取資料出錯

printf("---->read data failed !\n");

goto exit;

}

sleep(3);

}

exit: close_connection();

return 0;

}

這個cpp檔案就會被ndk編譯成我們在上面提到的exe檔案,就是Linux系統下面的可執行檔案。隻有這個檔案,我們能做的就是和java端發個資訊而已,還是不能截屏的,需要截屏的程式在下面,也是用的android源代碼,不過基本讓我改的沒有源代碼的味道了。

在這裡,我使用了第三方的png庫,我把它編譯成靜态庫,然後連結到我的cpp檔案中,最後把cpp檔案編譯成exe檔案,再在代碼中執行exe檔案。下面是截屏的主要代碼。

#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>

//#ifdef ANDROID_KK //4.4

//#include <binder/ProcessState.h>

//#include <gui/ISurfaceComposer.h>

//#else

#include <binder/IMemory.h>

//#endif

using namespace android;

#define TAG "--screen_capture-->"

//截屏辨別

static int capture_flag = 0;

//錄音辨別

static int record_flag = 0;

int PushTime = 0;

//擷取目前時間(microsecond)

int64_t getCurrentTime() {

struct timeval tv;

gettimeofday(&tv, NULL);

return (1000000LL * tv.tv_sec) + tv.tv_usec;

}

//#ifdef ANDROID_KK

//static uint32_t DEFAULT_DISPLAY_ID = ISurfaceComposer::eDisplayIdMain;

//#endif

//顯示錯誤資訊

void error(const char* msg) {

fprintf(stderr, "%s: %s:\n", msg, strerror(errno));

exit(1);

}

typedef struct param {

void *fb_in;

FILE * fb_out;

int width;

int height;

int64_t usedTime;

size_t buffer_size;

param * next;

} image_info;

// 擷取目前日期, 以秒為機關,現在我一秒之内可以截取3張圖檔,他的名字當然一樣了

char* getLocalTime() {

char currentTime[128];

memset(currentTime, 0, 128);

int64_t time = getCurrentTime();

sprintf(currentTime, "%lld", time);

return currentTime;

}

//建立視訊路徑

void create_video_path(char* path) {

memset(path, 0, 256);

strcpy(path, "/sdcard/DCIM/Record_");

strcat(path, getLocalTime());

strcat(path, ".mp4");

printf("---->path = %s\n", path);

}

//建立視訊路徑

void create_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(const void * in, FILE* out, int w, int h,

size_t size, int64_t ut) {

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("---->create a new image node \n");

return newInfo;

}

//釋放記憶體

void release_image_node(image_info* node) {

if (node != NULL) {

free(node->fb_in);

node->fb_in = NULL;

free(node);

node = NULL;

printf("---->success to release a image node \n");

}

}

//void take_screenshot(char *fb_base, FILE* fb_out, int w, int h, int f) {

int take_screenshot(image_info *argu) {

long result = 0;

//printf("take_screenshot is running! \n");

//image_info *argu = (image_info*) param;

//png結構

png_structp png;

//png info

png_infop info;

struct fb_var_screeninfo vinfo;

// 因為是一行一行的掃描螢幕,r是行數

unsigned int r;

//每一行的長度,是右螢幕的寬度(像素) * 每一個像素的大小(32位 4個位元組)

unsigned int rowlen;

//每一個像素所占的大小,4個位元組

unsigned int bytespp = 4;

//建立一個png結構

png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);

//printf("1---> png_create_write_struct is runed \n");

if (png == NULL) {

ALOGE("failed png_create_write_struct\n");

}

png_init_io(png, argu->fb_out);

//printf("2---> png_init_io is runed \n");

info = png_create_info_struct(png);

//printf("3---> png_create_info_struct is runed \n");

if (info == NULL) {

ALOGE("failed png_create_info_struct\n");

png_destroy_write_struct(&png, NULL);

}

if (setjmp(png_jmpbuf(png))) {

ALOGE("failed png setjmp\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_IHDR is runed \n");

png_write_info(png, info);

//printf("5---> png_write_info is runed \n");

//計算每一行的長度

rowlen = argu->width * bytespp;

//建立一個臨時變量,儲存圖檔的記憶體塊位址

png_bytep temp = (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_end is runed \n");

png_destroy_write_struct(&png, NULL);

//printf("7---> png_destroy_write_struct is runed \n");

//儲存完圖檔,就把檔案關閉掉

if (fclose(argu->fb_out) == -1) {

error("close file failed !");

} else {

//printf("---->success to save a image \n");

temp = NULL;

result = 1;

}

return result;

}

//儲存圖檔,傳回儲存成功的圖檔數量,如果不等于24, 則說明儲存失敗, 成功傳回1, 失敗,傳回0

void* save_image(void *start) {

long result = 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("save image failed");

}

//最後再将next節點複制給p,繼續操作

p = next;

}

result = 1;

return (void*) result;

}

image_info* screen_shot() {

static int64_t base_time = getCurrentTime();

//截屏的開始時間和結束時間

int64_t beginTime = 0, usedTime = 0;

const void* base;

size_t size;

uint32_t width, height;

FILE *fb_out = NULL;

//#ifdef ANDROID_KK

//ProcessState::self()->startThreadPool();

//#endif

//

//#ifdef ANDROID_KK

//int32_t displayId = DEFAULT_DISPLAY_ID;

//#endif

ScreenshotClient screenshot;

beginTime = getCurrentTime();

//#ifdef ANDROID_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, used time = % lld\n",

screenshot.getWidth(), screenshot.getHeight(), usedTime);

char image_path[256];

create_image_path(image_path);

fb_out = fopen(image_path, "w");

return create_image_info_node(base, fb_out, width, height, size,

usedTime);

}

return NULL;

}

//截屏并生成視訊連結清單

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_shot return NULL !\n");

return NULL;

}

//睡3秒,不然錄制的太快

sleep(3);

}

return (void*) start;

}

#ifdef __cplusplus

extern "C" {

#endif

//設定截屏标志

void set_capture_flag_png(int flag) {

capture_flag = flag;

record_flag = flag;

}

//截屏并儲存為圖檔

void screen_capture_png() {

//截屏線程,隻負責截屏

pthread_t capture_thread;

if (pthread_create(&capture_thread, NULL, capture_and_link, NULL) == -1) {

error("create capture_thread  failed !");

}

//截屏之後傳回的結果,是一個包含截屏圖檔資訊的連結清單,這個連結清單儲存的東西關系到整個截屏程式的成敗

void* capture_result;

if (pthread_join(capture_thread, &capture_result) == -1) {

error("waiting capture_thread failed !");

}

//截屏完了,開始儲存圖檔

pthread_t save_thread;

if (pthread_create(&save_thread, NULL, save_image, capture_result) == -1) {

error("create save_thread failed !");

}

//等待儲存圖檔的線程

void* save_result;

if (pthread_join(save_thread, &save_result) == -1) {

error("waiting save_thread failed !");

}

if ((long) save_result == 1) {

printf("---->success to save image !\n");

}

}

#ifdef __cplusplus

}

#endif

到此為止,這個截屏程式的主要代碼是都将完了,雖然說将的很簡單,但是做起來一點都不簡單,裡面涉及的東西還是很多的。這需要你對ndk程式設計很熟悉才可能把這個程式順利的運作起來