用PHP開發Android應用的可行性探究(PHPDroid)

圖片描述
基於Android上的PHP(好比我打包的PHPDroid),寥寥幾行PHP代碼,就能實現一個支持無線局域網用瀏覽器訪問的Android手機的Shell,用於執行命令和PHP代碼.javascript

你們之前可能都據說過PFA(PHP for Android)這麼一個東西,能夠下結論的是,這個項目其實已經再也不活躍失敗了,由於其依賴的SL4A(Scripting Layer for Android)已經中止更新和維護了,並且它依賴SL4A的運行模式也跟我下面提到的PHPDroid的本地Browser/Server模式大相徑庭.php

我的在Ubuntu上用交叉編譯工具鏈musl-cross-compilers參考DroidPHP的教程構建了適用於Linux ARM架構的二進制PHP解釋器.其中DroidPHP的教程用的是arm-none-linux-gnueabi那一套工具鏈,我發現很差使,後來我又看到了PocketMine,一個用PHP7構建的MineCraft Android版服務器端,它用的是musl-cross-compilers,因此我也改用這套工具鏈.我主要作了一些修改,好比修改libc庫的shell路徑,以及DNS相關的resolv.conf和hosts位置,主要是爲了讓PHP能在Android上跑.css

如今有了可以運行在Android上的PHP解釋器,接下來就是看看能用什麼方法構建App.由於PHP解釋器從5.4開始內置了一個CLI HTTP Server,這是一個C實現的支持PHP編程的單進程Web服務器,很輕量,官方定位是開發測試.並且它還支持一個特性,就是能夠啓動時給它指定一個PHP路由腳本:
php -S 127.0.0.2:8080 -t /www auth.php
好比我打包的PHPDroid裏的auth.php是這樣的:html

<?php
//如下代碼的意思就是,若是UserAgent不匹配,就輸出Forbidden並退出
$ua = dirname(__FILE__).'/ua';
if( isset($_SERVER['HTTP_USER_AGENT']) 
    && file_exists($ua) 
    && $_SERVER['HTTP_USER_AGENT'] === trim(file_get_contents($ua)) ) {
    return false;
} else {
    exit('Forbidden');
}

咱們還知道,PHP還內置了SQLite引擎,一個文件就是一個數據庫,管理起來很是方便,可視化管理工具能夠用免費開源跨平臺的SQLiteStudio.如今有了HTTP服務器和SQLite數據庫,我編譯時還啓用了一些經常使用的擴展,以下:java

bcmath calendar Core ctype curl date dom exif filter ftp gd hash json libxml mbstring mcrypt openssl pcre PDO pdo_sqlite Reflection session SimpleXML sockets SPL sqlite3 standard swoole xml xmlreader xmlwriter zip zlib

有了上面這些東西,咱們該怎麼進行圖形化交互呢?我想你們都想到了,那就是Android內置的WebView.我的認爲,瀏覽器是有史以來最成功的跨平臺GUI應用.Android從4.4開始,WebView底層由Chromium驅動,性能也是至關不錯的.若是你要調用一些Java編寫的本地功能,WebView一直都提供有一個addJavascriptInterface的特性,能夠把Java對象注入到WebView中供JS調用,好比:jquery

Java代碼:
webview.addJavascriptInterface(new Object() {
    @JavascriptInterface
    public String getprop(String prop) {
        return java.lang.System.getProperties().getProperty(prop);;
    }
}, "android");

WebView裏的HTML代碼:
<a href="javascript:void(0)" onclick="alert(android.getprop('os.name'))">os.name</a>

也就是說,你用WebView也是能夠進行一些本地API調用的,固然你得先用Java寫好那些功能.linux

好了,如今PHPDroid的整個運行模式就比較清晰了,應用啓動時,執行start.sh隨機生成UserAgent並記錄下來(ua.php),找到可用端口並記錄下來(port.php),而後啓動PHP內置HTTP服務器並記錄PID(用於關閉),Java裏啓動WebView前讀取隨機生成UserAgent並setUserAgentString設置,PHP內置服務器的路由auth.php會對UserAgent進行比對,拒絕本機上其餘應用(好比瀏覽器)發出的請求.
各個腳本以下:android

start.sh
#!/system/bin/sh
cd $1/php/bin
chmod 700 busybox
if [ ! -f php ]; then
    ./busybox xz -d file.tar.xz
    ./busybox tar xf file.tar && rm file.tar
    chmod 700 lib/ld php watcher qrencode
    ./busybox sed -i "s@/data/data/net.php.phpdroid@$1@g" php.ini
    ./busybox sed -i "s@/storage/self/primary@$2@g" php.ini
    cp resolv.conf $1/php/www
    cp hosts $1/php/www
fi
#隨機生成UserAgent
./php -c php.ini ua.php
#獲取可用端口
./php -c php.ini port.php
#開發調試時把網站根目錄複製到SD卡,方便修改
# -t $1/php/www  實際應用的網站根目錄
# -t $2/phpdroid 開發調試的網站根目錄
if [ ! -d $2/phpdroid ]; then cp -R $1/php/www $2/phpdroid; fi
#啓動PHP服務
$1/php/bin/php \
-c php.ini \
-S 127.0.0.2:`cat $1/php/bin/port` \
-t $2/phpdroid \
$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

stop.sh
#!/system/bin/sh
ua=$1/php/bin/ua
if [ -e $ua ]; then
    rm $ua
fi
port=$1/php/bin/port
if [ -e $port ]; then
    rm $port
fi
pid=$1/php/bin/pid
if [ -e $pid ]; then
    kill -9 `cat $pid`
    rm $pid
fi
pid=$1/php/bin/pid_watcher
if [ -e $pid ]; then
    kill -9 `cat $pid`
    rm $pid
fi
return 0

ua.php
<?php
$ua = 'Mozilla/5.0 (Linux; Android; PHPDroid) AppleWebKit (KHTML, like Gecko) Chrome Mobile Safari';
file_put_contents(dirname(__FILE__).'/ua', $ua.' '.sha1(uniqid(mt_rand(), true)));

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);

MainActivity.java 在 onCreate 時啓動 WebView 以下:
webview = new WebView(this);
webview.getSettings().setUserAgentString(ua);
webview.getSettings().setJavaScriptEnabled(true);
webview.setWebChromeClient(new WebChromeClient());
webview.setWebViewClient(new WebViewClient() {
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        view.loadUrl(url);
        return true;
    }
});
webview.loadUrl("http://127.0.0.2:" + port);
setContentView(webview);

好了,看到這裏,你們基本已經明白我所說的用PHP開發Android應用的思路.
你們感興趣能夠下載我打包好的APK來體驗一下:
phpdroid_20160703.apk(5.8M)
phpdroid_20160703.7z(4.7M)
apk裏包含PHP最新的7.0.8和高性能網絡編程擴展Swoole,
另外還有BusyBox和生成二維碼的qrencode.
7z包是項目源代碼,主要就是MainActivity.java和assets數據.
提示下,我打包的APK在start.sh裏把網站根目錄調整到了SD卡的phpdroid目錄下,主要上方便你們體驗時USB鏈接手機把本身寫的PHP文件放到裏面運行測試.git

附:開頭題圖(Shell)的PHP代碼:github

<?php
if(isset($_POST['cmd'])) {
    $pre = 'export PATH='.get_cfg_var('app_dir').'/php/bin:$PATH'."\n"
          .'alias php="'.get_cfg_var('app_dir').'/php/bin/lib/ld --library-path '.get_cfg_var('app_dir').'/php/bin/lib '.get_cfg_var('app_dir').'/php/bin/php"'."\n";
    echo shell_exec($pre.trim($_POST['cmd']))."\n";
    @var_export($GLOBALS['_SERVER']);
    exit();
}
header('Content-Type: text/html; charset=utf-8');
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0">
<title>PHPDroid Web Shell</title>
<style>
* {
    margin: 0;
    padding: 0;
}
body {
    background: #2D2D2D;
}
#con {
    width: 100%;
    margin: 0 auto;
}
#con .cmd {
    display: block;
    width: 96%;
    padding: 6px 2%;
    margin: 0 auto;
    height: 160px;
    border: none;
    border-top: 3px solid #E66000;
    border-radius: 0;
    resize: none;
    background: #F9F9F9;
    color: #2D2D2D;
    font-size: 14px;
    font-family: monospace;
    line-height: 1.5em;
    outline: none;
    word-break: break-all;
}
#con .run {
    display: block;
    width: 100%;
    margin: 0 auto;
    background: #E66000;
    border: none;
    box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.4);
    text-decoration: none;
    color: #FFF;
    font-size: 20px;
    text-align: center;
    line-height: 3em;
    letter-spacing: 0.2em;
    outline: none;
}
#con .run:active {
    box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.4);
}
.ret {
    width: 98%;
    padding: 6px 1%;
    margin: 0 auto;
    min-height: 160px;
    background: #2D2D2D;
    color: #FFF;
    font-size: 14px;
    font-family: monospace;
    line-height: 1.5em;
    overflow: auto;
}
</style>
</head>
<body>
<div id="con">
<textarea class="cmd" spellcheck="false">
type php
php -v
php -r '
echo date("Y年m月d日 H時i分s秒")."\n";
'
ps|busybox head -n 1
ps|busybox grep php
</textarea>
<button class="run"">運行命令</button>
</div>
<pre class="ret"></pre>
<script src="jquery.js"></script>
<script>
$('.ret').css('min-height',$(window).height()-247);
$(window).resize(function(){
    $('.ret').css('min-height',$(window).height()-247);
});
$('#con .cmd').focus();
$('#con').on('click', '.run', function(){
    $('.ret').text('');
    $.ajax({
        url: '<?php echo parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); ?>',
        type: 'POST',
        data: {cmd: $('#con .cmd').val()},
        success: function(data){
            $('.ret').text(data);
        }
    });
});
</script>
</body>
</html>
相關文章
相關標籤/搜索