基於Android上的PHP(好比我打包的PHPDroid),寥寥幾行PHP代碼,就能實現一個支持無線局域網用瀏覽器訪問的Android手機的Shell,用於執行命令和PHP代碼.php
我的在Ubuntu上使用交叉編譯工具鏈 arm-none-linux-gnueabi 或 musl-cross-compilers(推薦) 按照 DroidPHP 的教程
cross_compile_php.txt
這是我使用musl-cross-compilers交叉編譯Android版PHP7的詳細筆記.
構建了適用於Android(ARM架構)和樹莓派Raspbian(ARM架構基於Debian的Linux發行版)的PHP解釋器(cli,cli-server).
從圖中能夠看到,PHP進程的內存(RSS)內存佔用不到5MB,WebView的內存佔用超過56MB.
照着Linux C man文檔inotify的例程給PHPDroid寫了個C程序(watcher),
在App卸載刪除文件時,捕獲IN_DELETE_SELF事件,退出PHP進程.
下載地址:
phpdroid_20160703.apk(5.8M)
phpdroid_20160703.7z(4.7M)
apk裏包含PHP-7.0.8和高性能網絡編程擴展Swoole,
另外還有BusyBox和生成二維碼的qrencode.
7z包是項目源代碼,主要就是MainActivity.java和assets數據.
這裏須要說明的是,BusyBox並非PHP必備的東西,
打包它只是爲了方便PHP可以調用裏面經常使用的GNU/Linux命令,好比xz.
爲了減小APK大小,用xz極限壓縮PHP,應用首次運行時再調用busybox的xz解壓,從而減小APK大小.
須要強調的是,包裏的PHP是路徑無關的,運行也不須要root權限,
只要維持assets/php/的目錄結構,放到你的應用裏也能正常運行.
網站根目錄位於assets/php/www.
PHPer在PC上開發時,只需執行:
php -S 127.0.0.2:8181 -t /path/to/assets/php/www
而後打開瀏覽器的手機模式訪問 127.0.0.2:8181 就能夠了.
phpdroid_20160413.7z改動說明:
爲了方便開發者在電腦經過MTP連手機時就能修改PHP文件,因此把網站根目錄調整到外部存儲.
網站根目錄:好比小米和華爲執行
String www_dir = Environment.getExternalStorageDirectory().getAbsolutePath()+"/"+getPackageName();
Log.d("PHPDroid", www_dir);
返回的是:
/storage/emulated/0/net.php.phpdroid
phpdroid.apk在啓動時會自動建立這個目錄,並寫入一個文件index.php.
建立的這個目錄在手機的文件管理器能即時看到,但電腦文件管理器(MTP)裏卻不會當即顯示,須要重啓手機才能看到.java
PHPDroid基本工做原理:
Java啓動PHP內置的HTTP服務器,而後開一個WebView訪問這個PHP驅動的HTTP服務.
其中,WebView用於實現人機交互,能夠用傳統的HTML/CSS/jQuery技術進行圖形界面編程.
PHP則負責跟本地文件系統,SQLite數據庫,網絡進行交互.
須要強調的是,PHPDroid追求的不是像Java App那樣可以訪問Android系統提供的API.
PHPDroid的優點在於用傳統的Web開發技術HTML/CSS/JS/PHP/SQL就能開發基於WebView的本地WebApp.
PHPDroid內置的本地PHP不能訪問Android提供給Java的API,
但能夠操做本地文件系統(應用目錄/SD卡)和SQLite以及進行網絡交互.
好比獲取一個新聞列表,WebView經過AJAX訪問本地PHP,PHP再經過cURL訪問遠程服務器.
遠程服務器返回JSON,裏面包含新聞的標題,摘要,縮略圖網址,本地PHP轉成數組後循環輸出到WebView.
可見這個本地PHP既是WebView的服務器端,又是遠程服務器的客戶端,是WebView和遠程服務器數據交互的中轉站.
固然WebView也能夠經過JSONP遠程獲取數據.
把WebView和本地PHP看作一個總體,那它就是一個不能調用Android API的本地WebApp.
畢竟Android是Linux內核,一切皆文件的思想仍是在那裏的.
只要有權限,PHP讀取一些系統數據(好比/proc/cpuinfo)並無問題.
若是你要訪問Android Java API,能夠addJavascriptInterface注入Java對象到WebView供JS調用:
webview.addJavascriptInterface(new MyClass(this), "myClass");
PHPDroid詳細工做原理:
phpdroid/app/src/main/java/net/php/phpdroid/MainActivity.java
MainActivity在onCreate首次啓動時複製:
/data/app/net.php.phpdroid.apk/assets/php/
到:
/data/data/net.php.phpdroid/php/
而後Runtime.getRuntime().exec執行PHP服務啓動腳本:
/data/data/net.php.phpdroid/php/bin/start.sh
#!/system/bin/sh
cd $1/php/bin
chmod 700 busybox
if [ ! -f php ]; then
./busybox xz -d php.xz
./busybox xz -d watcher.xz
chmod 700 php
chmod 700 watcher
fi
#隨機生成UserAgent
./php -c php.ini ua.php
#獲取可用端口
./php -c php.ini port.php
#建立文件/storage/self/primary/net.php.phpdroid/index.php
./php -c php.ini -d www_dir="$2" www.php
#啓動PHP服務
$1/php/bin/php \
-c $1/php/bin/php.ini \
-d app_dir="$1" \
-d upload_tmp_dir="$1/php/tmp" \
-d session.save_path="$1/php/tmp" \
-S 127.0.0.2:`cat $1/php/bin/port` \
-t $2 \
$1/php/bin/auth.php \
>/dev/null 2>&1 &
#記錄PHP的PID
echo $! > pid
#監聽,發現文件auth.php被刪除,則關閉PHP進程
$1/php/bin/watcher $1/php/bin/auth.php >/dev/null 2>&1 &
#記錄watcher的PID
echo $! > pid_watcher
return 0
這個腳本的做用就是,隨機生成用於標記WebView的UserAgent,獲取127.0.0.2上的可用端口,
而後啓動PHP服務器,記錄其PID,用於在kill關閉.
關於PHP內置HTTP服務器的介紹,請看:
https://wiki.php.net/rfc/builtinwebserver
其中:
/data/data/net.php.phpdroid/php/bin/ua.php
<?php
file_put_contents(dirname(__FILE__).'/ua', sha1(uniqid(mt_rand(), true)));
/data/data/net.php.phpdroid/php/bin/port.php
<?php
//PHP用 fsockopen 檢測端口是否被佔用,返回可用端口.
$port = 8181;
while ( $fp = @fsockopen('127.0.0.2', $port, $errno, $errstr, 1) ) {
fclose($fp);
$port++;
}
file_put_contents(dirname(__FILE__).'/port', $port);
/data/data/net.php.phpdroid/php/bin/auth.php
<?php
$ua = dirname(__FILE__).'/ua';
if( isset($_SERVER['HTTP_USER_AGENT'])
&& file_exists($ua)
&& $_SERVER['HTTP_USER_AGENT'] === trim(file_get_contents($ua)) ) {
//每次請求都執行getprop net.dns1獲取手機DNS並寫入resolv_php.conf供glibc庫使用.
if( ($dns1 = filter_var(trim(shell_exec('getprop net.dns1')), FILTER_VALIDATE_IP)) !== false ) {
$dns = file_get_contents(dirname(__FILE__).'/resolv_php.conf.default');
file_put_contents(dirname(__FILE__).'/resolv_php.conf', 'nameserver '.$dns1."\n".$dns);
}
gethostbyname('localhost'); //觸發PHP進程打開resolv_php.conf,要求resolv_php.conf跟auth.php在同一目錄
return false;
} else {
exit('Forbidden');
}
PHP服務在處理每一個請求以前,都會執行auth.php文件,
若是ua(UserAgent)不匹配,程序就會exit退出.
Android上一個應用對應一個用戶,每一個應用目錄只容許應用所屬用戶進行訪問,
因此除非手機被root,不然其餘應用是無法讀取PHPDroid應用目錄裏的數據的.
應用MainActivity.java裏讀取ua文件並設置爲WebView的UserAgent,因此可以訪問PHP服務.
手機上的其餘應用,好比瀏覽器,由於沒有讀取其餘應用目錄好比 /data/data/net.php.phpdroid 的權限,
也就沒法讀取PHPDroid生成的ua,天然也就沒法訪問PHP服務.
MainActivity.java
webview.getSettings().setUserAgentString(ua);
webview.loadUrl("http://127.0.0.2:" + port);linux
關於DNS解析,glibc默認訪問的是/etc/resolv.conf
#define _PATH_RESCONF "/etc/resolv.conf"
在編譯glibc時,我改爲了相對路徑:
#define _PATH_RESCONF "./resolv_php.conf"
/data/data/net.php.phpdroid/php/bin/resolv_php.conf
# 百度公共DNS http://dudns.baidu.com/
nameserver 180.76.76.76
# CNNIC公共DNS http://www.sdns.cn/
nameserver 1.2.4.8
nameserver 210.2.4.8
靜態連接了glibc庫的PHP,在執行auth.php裏的gethostbyname('localhost')操做時,
會觸發訪問auth.php所在目錄下的resolv_php.conf,從而進行DNS.
更好的方法應該是調用Android的getprop net.dns1獲取本地DNS,而後加入到resolv_php.conf裏.
但奇怪的是,在adb shell裏執行 getprop net.dns1 能正確輸出,
一套在PHP的 echo shell_exec('getprop net.dns1'); 就沒有輸出了.
執行 echo shell_exec('vmstat'); 調用其餘命令是能正常輸出的.
這個問題發生在小米(Android 6)上,華爲(Android 4)上是正常的.
關於glibc的編譯,我還把調用命令的/bin/sh改爲了Android的/system/bin/sh,
這樣PHP的shell_exec等函數才能正常運行.
sed -i "s{/bin/sh{/system/bin/sh{" ./libio/oldiopopen.c
sed -i "s{/bin/sh{/system/bin/sh{" ./libio/iopopen.c
sed -i "s{/bin/sh{/system/bin/sh{" ./posix/tst-vfork3.c
sed -i "s{/bin/sh{/system/bin/sh{" ./posix/bug-regex9.c
sed -i "s{/bin/sh{/system/bin/sh{" ./sysdeps/posix/system.c
sed -i "s{/bin/sh{/system/bin/sh{" ./sysdeps/generic/paths.h
sed -i "s{/bin/sh{/system/bin/sh{" ./sysdeps/unix/sysv/linux/paths.h
位於PHP裏的proc_open函數也要進行相似修改:
sed -i "s{/bin/sh{/system/bin/sh{" ext/standard/proc_open.c
這樣PHP就能夠愉快地調用Android和BusyBox裏提供的GNU/Linux經常使用命令了.git
MainActivity在onKeyDown按下返回鍵KEYCODE_BACK退出應用時:
會調用stop.sh關閉PHP服務,stop.sh內容以下:
#!/system/bin/sh
ua=$1/php/bin/ua
if [ -r $ua ]; then
rm $ua
fi
port=$1/php/bin/port
if [ -r $port ]; then
rm $port
fi
pid=$1/php/bin/pid
if [ -r $pid ]; then
kill -9 `cat $pid`
rm $pid
fi
pid=$1/php/bin/pid_watcher
if [ -r $pid ]; then
kill -9 `cat $pid`
rm $pid
fi
return 0
就是把ua,port這兩個文件刪掉,而且關閉PHP和watcher進程.
其實MainActivity在啓動時也會調用stop.sh清理上次應用可能意外退出遺留下來的東西.
github