Android的功夫,在Android之外。
這句話我很認同,Android Framework只不過是對底層系統的封裝,要想深入理解它,必須熟悉JNI、讀得懂C++、理解Java虛擬機、Linux系統甚至彙編、指令集等等。但是並不意味這你作爲一個開發就一定都得會這些,我相信等你學完上述那些知識,可能已經換一個操作平臺了,當然如果你真的掌握了這些,下一代操作系統是什麼已經不重要了。今天我們要講的知識點雖然是爲了解決當下要解決的問題,但是追根溯源其實是java,甚至是網絡協議,C++,等層面的知識,但知識都是觸類旁通的。
說這個問題首先我先說下這個業務的使用場景。隨着互聯網的發展進入了下半場,有以前的app大而且多的局面滿滿的走向精而細的劃分,每一個app的如何基於大數據統計用戶行爲是衡量一款產品的優劣標準之一,因爲這些數據驅動老闆、產品、市場、運營的業務決策,深度瞭解你的用戶行爲,評估營銷效果,優化產品體驗,提升運營效率,在探索不同業務的關鍵行爲中,洞察指標背後掩藏的故事。對產品的定位和改進是非常重要的因素,接下來我就圍繞我們開發無埋點統計中遇到的問題跟大家交流產品的統計數據遇到的解決方案:
- 本博客不介紹如何實現無埋點統計,這裏我講的是數據的處理,至於無埋點如何插樁,這個點不是今天我們要討論的,以後有機會可以跟大家詳細的分享安卓AOP的相關知識點
- 如何實現後臺與前端的界面統一,可以再web端動態的控制選擇埋點,這個在以前的博文稍微有介紹:就是頻繁的截屏
- 基於第二個問題,頻繁的截屏必然產生大量的數據,數據如何進行通訊
今天的文章就是圍繞第三個問題進行的拓展和延伸
本地截屏的大量數據如何進行傳輸:
思路:
- 因爲產品在開發完版本以後讓老闆、產品、市場、運營可以再後端web端動態控制,他們不懂程序,只知道圖形化的操作,那麼我們需要把這個app界面的圖形化操作傳到服務器,讓其進行選擇,可以通過本地的adb命令Socket的IPC進程通信完成但是這裏的侷限性是手機和服務器是不同的機器,如何通信?
- 可能有人說socket可以支持TCP/IP通信,是的,沒問題,如果僅僅是這個問題那麼也沒有必要寫博客了,今天主要是要根據這個我們能不能做成socket如何通過IPC完成數據的上傳工作
圍繞這個問題之前我們先回顧下一些知識點:
首先說下線程間通信的幾種方式:
1:使用管道流Pipes
“管道”是java.io包的一部分。它是Java的特性,而不是Android特有的。一條“管道”爲兩個線程建立一個單向的通道。生產者負責寫數據,消費者負責讀取數據。
public class SecAct extends Activity {
private static final String TAG = "PipeExampleActivity";
private EditText editText;
PipedReader r;
PipedWriter w;
private Thread workerThread;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_sec);
r = new PipedReader();
w = new PipedWriter();
try {
w.connect(r);
} catch (IOException e) {
e.printStackTrace();
}
editText = (EditText) findViewById(R.id.edit_text);
editText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence charSequence, int start, int before, int count) {
try {
if (count > before) {
w.write(charSequence.subSequence(before, count).toString());
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void afterTextChanged(Editable editable) {
}
});
workerThread = new Thread(new TextHandlerTask(r));
workerThread.start();
}
@Override
protected void onDestroy() {
super.onDestroy();
workerThread.interrupt();
try {
r.close();
w.close();
} catch (IOException e) {
}
}
private static class TextHandlerTask implements Runnable {
private final PipedReader reader;
public TextHandlerTask(PipedReader reader) {
this.reader = reader;
}
@Override
public void run() {
while (true) {
try {
int i;
while ((i = reader.read()) != -1) {
char c = (char) i;
Log.d(TAG, "char = " + c);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
2:共享內存
說到這個我們就得聊到java的內存模型了。
java內存模型是什麼呢?它規範了java虛擬機如何與計算機內存的協同工作
- 堆爲JVM內所有的線程共享,存在內存中所有的對象和數組數據
- 棧爲每個線程所有,棧中存放了當前方法的調用信息以及基本數據類型和引用數據類型的數據
java中的堆,堆在虛擬機啓動的時候創建。堆佔用的內存是垃圾回收器回收,不用我們手動回收。
JVM沒有規定死使用哪種回收機制,不同的虛擬機可以實現不同的回收算法。
堆中包含了java程序創建的所有的對象, 不論是哪個線程。
一個對象的成員變量隨着這個對象自身存放在堆上。不管這個成員變量是基本類型還是引用類型。
java中的棧
棧在線程創建的時候創建,它和C語言的棧類似,在一個方法中,你創建的局部變量和部分結果都會保存在棧中,並在方法調用和返回中起作用。當前的棧只對當前的線程可見,即便兩個線程執行同樣的代碼,這兩個線程仍然會在自己的線程棧中創建一個本地副本。
因此每一個線程擁有每個本地變量的獨有版本。
棧中保存方法調用棧、基本類型的數據、以及對象的引用。
計算機中的內存、寄存器、緩存
一個現代計算機通常由兩個或者多個 CPU,每個 CPU 都包含一系列的寄存器,CPU 在寄存器上執行操作的速度遠大於在主存上執行的速度。
每個 CPU 可能還有一個 CPU 緩存層。CPU 訪問緩存層的速度快於訪問主存的速度,但通常比訪問內部寄存器的速度還要慢一點。
通常情況下,當一個 CPU 需要讀取主存時,它會將主存的部分讀到 CPU 緩存中。它甚至可能將緩存中的部分內容讀到它的內部寄存器中,然後在寄存器中執行操作。
當 CPU 需要將結果寫回到主存中去時,它會將內部寄存器的值刷新到緩存中,然後在某個時間點將值刷新回主存。
多線程可能出現的問題
通過上述介紹,我們可以知道,如果多個線程共享一個對象,每個線程在自己的棧中會有對象的副本。
如果線程 A 對對象中的某個變量進行修改後還沒來得及寫回主存,線程 B 也對該變量進行了修改,那最後刷新回主內存後的值一定和期望的值不一致。
就好比我和你同時開發同一模塊代碼,我下筆如有神不一會兒搞定了註冊登錄並且提交,你沒有從服務器拉代碼就矇頭狂寫,最後一 pull 代碼,就會發現自己寫的好多都跟服務器上的衝突了!
競態條件與臨界區
當多個線程操作同一資源時,如果對資源的訪問順序敏感,就稱存在競態條件。導致競態條件發生的代碼區稱作臨界區。
在臨界區中使用適當的同步就可以避免競態條件,比如 synchronized, 顯式鎖和原子操作類等。
內存可見性
我寫的代碼你無法立即看到,這就是所謂的“內存可見性”問題。
爲了讓線程 A 對變量做的修改線程 B 立即可以看到,我們可以使用 volatile 修飾變量或者對修改操作使用同步。
當線程訪問某一個對象時候值的時候:
首先通過對象的引用找到對應在堆內存的變量的值;
然後把堆內存變量的具體值 load 到線程工作內存中,建立一個變量副本;
之後線程就不再和對象在堆內存變量值有任何關係,而是直接修改副本變量的值,在修改完之後也不會立即同步修改共享堆內存中該變量的值;
直到某一個時刻(線程退出之前),自動把線程變量副本的值回寫到對象在堆中變量。這樣在堆中的對象的值就產生變化了。
多個線程共享同一份內存,就是說,一個變量可以同時被多個線程所訪問。這裏要特別注意同步和原子操作的問題。
synchronized(this) {
while(isConditionFullfilled == false) {
wait();
}
notify();
}
因爲如果不加同步或者原子性可能會出現,這個線程修改了,另外一個線程沒法讀到。
3:使用Hander和Message
上面講的是線程間通信的問題,接下來再來說說進程間通信的問題。而且今天主要給大家講解android有哪些可以跨進程通信的機制,android系統歸根到底還是一個Linux系統,Linux系統有着非常成熟完善的跨進程通信的機制,比如:管道,System V,Socket等
linux下的進程通信手段基本上是從Unix平臺上的進程通信手段繼承而來的
下面分別講解這幾個,以及爲什麼有了這些還要共有跨進程機制還有哪些特有的機制.
- 管道:
- 共享內存,System V(https://blog.csdn.net/qq_35535992/article/details/52926543)
- Socket
Socket是目前用於最廣泛的進程間通信的機制,他與其他Linux通信機制不同的地方在於除了可以用於單機內的進程間通信以外,還可以用於不同機器的進程間通信,但是Socket本身不支持同時等待或者超時處理,所以他不能直接用來多進程之間的相互實時通信。在我們開發的項目中使用的Socket的進程通信方法是,建立一個進程專門用於通訊服務器(Server)來中轉各個進程間的通信,它首先啓動一個用來監視連接要求的listening Socket,並把它的描述(Descriptor)號加入到一個事先定義好的fd_set的集合中,這個fd_set的集合用來存放listening Socket和後來生成的通信Socket的描述號。Server運用system call select來實時檢查是否有數據到達這個集合中的任何一個socket,如果有數據到達listening Socket,則這一定是客戶端發起的連接請求,於是生成一個新的通信Socket與該客戶端連接,將生成的Socket描述號加入到fd_set的集合中,將客戶端的ID號和與之對應的Socket的描述號記錄在ID登記表中。如果有數據到達某個通信Socket,則這一定是某個客戶端發起的通信請求,讀出數據並取出收信客戶端ID號,在ID登記表中找到與之對應的Socket描述號,將數據通過對應Socket傳送到收信客戶端。
其他各個進程作爲作爲客戶端,(client)。客戶端的動作是首先建立通信Socket連接服務器端,然後通過通信Socket進行送信和收信。
首先給出Server端的程序,在這裏假設有兩個客戶端要進行實時通信,ClientA向ClientB發送字符1,ClientB向ClientA發送字符2。
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <sys/time.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <netinet/in.h>
int main()
{
int rcd ;
struct sockaddr_un server_sockaddr ;
int backlog ;
ushort ci ;
int watch_fd_list[3] ;
fd_set catch_fd_set ;
fd_set watchset ;
int new_cli_fd ;
int maxfd;
int socklen ,server_len;
struct sockaddr_un cli_sockaddr ;
struct {
char module_id ; /* Module ID */
int cli_sock_fd ; /* Socket ID */
} cli_info_t[2] ;
for (ci=0;ci<=1;ci++)
cli_info_t[ci].cli_sock_fd=-1;
for (ci=0;ci<=2;ci++)
watch_fd_list[ci]=-1;
int server_sockfd,client_sockfd;
server_sockfd = socket( AF_UNIX, SOCK_STREAM, 0 ) ;
server_sockaddr.sun_family = AF_UNIX ;
strcpy( server_sockaddr.sun_path, "server_socket" ) ;
server_len=sizeof(server_sockaddr);
rcd = bind( server_sockfd, ( struct sockaddr * )&server_sockaddr, server_len ) ;
backlog = 5 ;
rcd = listen( server_sockfd, backlog ) ;
printf("SERVER::Server is waitting on socket=%d \n",server_sockfd);
watch_fd_list[0]=server_sockfd;
FD_ZERO( &watchset ) ;
FD_SET( server_sockfd, &watchset ) ;
maxfd=watch_fd_list[0];
在上面的程序中,Server生成listening Socket(server_sockfd),初始化Socket監視集合(watchset),並將listening Socket放入Socket監視集合中。
while (1){
char ch;
int fd;
int nread;
catch_fd_set=watchset;
rcd = select( maxfd+1, &catch_fd_set, NULL, NULL, (struct timeval *)0 ) ;
在上面的程序中,Server運用系統調用函數 select來實時檢查是否有數據到達Socket監視集合中的任何一個socket。
if ( rcd < 0 ) {
printf("SERVER::Server 5 \n");
exit(1);
}
if ( FD_ISSET( server_sockfd, &catch_fd_set ) ) {
socklen = sizeof( cli_sockaddr ) ;
new_cli_fd = accept( server_sockfd, ( struct sockaddr * )
&( cli_sockaddr ), &socklen ) ;
printf(" SERVER::open communication with Client %s on socket %d\n",
cli_sockaddr.sun_path,new_cli_fd);
for (ci=1;ci<=2;ci++){
if(watch_fd_list[ci] != -1) continue;
else{
watch_fd_list[ci] = new_cli_fd;
break;
}
}
FD_SET(new_cli_fd , &watchset ) ;
if ( maxfd < new_cli_fd ) {
maxfd = new_cli_fd ;
}
for ( ci=0;ci<=1;ci++){
if(cli_info_t[ci].cli_sock_fd == -1) {
cli_info_t[ci].module_id=cli_sockaddr.sun_path[0];
cli_info_t[ci].cli_sock_fd=new_cli_fd;
break;
}
}
continue;
}
在上面的程序中,Server運用系統調用函數FD_ISSET來檢查是否有客戶端的連接請求到達Listening Socket, 如果返回值大於0,Server生成一個新的通信Socket (new_cli_fd)與客戶端連接。將新生成的通信Socket放入Socket監視集合中(FD_SET)。將客戶端的信息(ID號和Socket描述號)保存在註冊表cli_info_t中
for ( ci = 1; ci<=2 ; ci++ ) {
int dst_fd = -1 ;
char dst_module_id;
char src_module_id;
int i;
if (watch_fd_list[ ci ]==-1) continue;
if ( !FD_ISSET( watch_fd_list[ ci ], &catch_fd_set ) ) {
continue ;
}
ioctl(watch_fd_list[ ci ],FIONREAD,&nread);
if (nread==0){
continue;
}
read( watch_fd_list[ ci ], &dst_module_id, 1 ) ;
for (i=0;i<=1;i++){
if(cli_info_t[i].module_id == dst_module_id)
dst_fd= cli_info_t[i].cli_sock_fd;
if(cli_info_t[i].cli_sock_fd==watch_fd_list[ ci ])
src_module_id= cli_info_t[i].module_id;
}
read( watch_fd_list[ ci ], &ch, 1 ) ;
printf("SERVER::char=%c to Client %c on socket%d\n",ch, dst_module_id,dst_fd);
write(dst_fd,&src_module_id, 1 ) ;
write(dst_fd,&ch, 1 ) ;
}
}
}
在上面的程序中,如果有數據到達某個通信Socket,Server則讀出數據並取出收信客戶端ID號。在ID登記表中找到收信客戶端對應的Socket描述號。並將數據通過對應Socket傳送到收信客戶端
給出客戶端 ClientA的程序
ClientB的程序只需將 char dst_module_id='B'; 改爲char dst_module_id='A'; char ch='1'; 改爲char char ch='2';既可。
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(){
int client_sockfd;
int len;
struct sockaddr_un server_sockaddr,cli_sockaddr;
int result;
char dst_module_id='B';
char ch='1';
char src_module_id;
client_sockfd= socket(AF_UNIX,SOCK_STREAM,0);
cli_sockaddr.sun_family = AF_UNIX ;
strcpy( cli_sockaddr.sun_path, "A" ) ;
bind(client_sockfd,(struct sockaddr * )&cli_sockaddr, sizeof( cli_sockaddr ) ) ;
server_sockaddr.sun_family=AF_UNIX;
strcpy( server_sockaddr.sun_path, "server_socket" ) ;
len=sizeof(server_sockaddr);
result = connect(client_sockfd,( struct sockaddr * )&server_sockaddr,len);
if (result <0){
printf("ClientA::error on connecting \n");
exit(1);
}
printf("ClientA::succeed in connecting with server\n");
sleep(10);
write(client_sockfd,&dst_module_id,1);
write(client_sockfd,&ch,1);
read (client_sockfd,&src_module_id,1);
read (client_sockfd,&ch,1);
printf("ClientA::char from Client %c =%c\n", src_module_id,ch);
close (client_sockfd);
}
下面是樣本程序的執行結果
[root@zhou test]# ./server &
[3] 4301
[root@zhou test]# SERVER::Server is waitting on socket=3
./clientA & ./clientB &
[4] 4302
[5] 4303
ClientA::succeed in connecting with server
SERVER::open communication with Client A on socket 4
[root@zhou test]# SERVER::open communication with Client B on socket 5
ClientB::succeed in connecting with server
SERVER::char=1 to Client B on socket5
ClientB::char from Client A =1
SERVER::char=2 to Client A on socket4
ClientA::char from Client B =2
爲什麼使用
AF_UNIX Socket服務器呢?
- 它更容易,因爲端口不能被其他任何東西使用
- 減少開銷
- 偵聽端口往往會使USB綁定在某些設備上無法使用
- 更好的隔離; 在設備上運行的常規應用程序無法連接到抽象Socket,也無法通過網絡連接
- 使所有端口免費供其他程序使用
- 某些設備(例如三星)不允許您在/ data / local / tmp中創建常規Socket文件,因此無法使用Socket文件
爲什麼不用AF_INET而使用AF_UNIX?
TCP/IP四層模型的通信原理
發送方和依賴方依賴IP:port來標識,將本地的socket綁定到對應的IP端口上,發送數據時,指定IP端口,經過Internet,可以通過此IP端口最終找到接收方,接收數據時,可以從對方的數據包中找到對方的ip,
發送方通過系統調用send()將原始數據發送到操作系統內核緩衝區中。內核緩衝區從上到下依次經過TCP層、IP層、鏈路層的編碼,分別添加對應的頭部信息,經過網卡將一個數據包發送到網絡中。經過網絡路由到接收方的網卡。網卡通過系統中斷將數據包通知到接收方的操作系統,再沿着發送方編碼的反方向進行解碼,即依次經過鏈路層、IP層、TCP層去除頭部、檢查校驗等,最終將原始數據上報到接收方進程。
AF_UNIX域socket的通信過程。
典型的本地ipc,類似於管道,依賴路徑名標識發送方和接收方,即發送數據時,指定接收方綁定的路徑名,操作系統根據該路徑名直接找到對應的接收方,並將原始數據直接拷貝到接受方的內核緩衝區,並上報給接受方的進程進行處理,同樣接收方可以從收到的數據包獲取發送方的路徑名,並通過此路徑向其發送數據。
相同點:
操作系統提供的接口socket(),bind(),connect(),accept(),send(),recv(),以及用來對其進行多路複用事件檢測的select(),poll(),epoll()都是完全相同的。收發數據的過程中,上層應用感知不到底層的差別。
不同點:
1:建立的socket傳遞的地址域不同,以及bind()的地址結構稍有區別
socket傳遞不同的地址域AF_INET和AF_UNIX
bind的地址結構分別爲sockaddr_in(指定IP端口)和sockaddr_un(指定的路徑名)
2:AF_INET需要經過多個協議的編解碼,消耗系統的cpu,並且數據傳輸需要經過網卡,收到網卡寬帶的限制。
AF_UNIX數據到達內核緩衝區,由內核根據指定路徑名找到接收方socket對應的內核緩衝區,直接將數據拷貝過去,不經過協議層的編解碼,節省cpu,並且不經過網卡,因此不受網卡寬帶的限制。
3:AF_UNIX的傳輸速率遠遠的大於AF_INET
4:AF_INET不可以作爲本機的跨進程通信,同樣的可以用於不同機器的通信,其就是爲了在不同機器間進行網絡互聯傳遞數據而生,而AF_UNIX僅僅可以用於本機內進程間的通信。
使用場景:
AF_UNIX由於其對系統cpu的較少消耗,不受限於網卡帶寬,及高效的傳遞速率,本機通信則首選AF_UNIX域。
AF_INET則用於跨機器之間的通信。
這裏其實我有個拓展就是我們安卓的設備也可以變相的完成不同機器的通信?如果去操作呢?
我們可以通過端口的轉發完成本地pc機器鏈接到手機設備,完成數據傳輸,但是這個必須得通過usb接口
然後在把這個pc的端口開發出去和服務器鏈接可以完成這個變相的進程間通信
使用TCP端口進行forward之外,我們還可以使用unix domain socket進行forward:
$ adb forward localfilesystem:socket dev:/dev/block/mmcblk0p6
好啦,基本上完成了我們本地的數據傳輸。完成了這些以後產品交接完以後我們可以讓老闆、產品、市場、運營在局域網內開放本地端口就可以做數據的傳輸,當然這裏至於如何開放端口等一些命令,我們會在手機端的鏈接的時候通過腳本自動完成,無需擔心: