PHPDroid: 基于WebView和PHP内置HTTP服务器开发Android应用

原创
2016/04/09 10:34
阅读数 1.7W

基于Android上的PHP(比如我打包的PHPDroid),寥寥几行PHP代码,就能实现一个支持无线局域网用浏览器访问的Android手机的Shell,用于执行命令和PHP代码.


个人在Ubuntu上使用交叉编译工具链  arm-none-linux-gnueabimusl-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_20170117.apk 我打包的APK包(5MB),无Swoole
phpdroid_20170117.7z 项目源代码(Android Studio)
phpdroid_20160703.apk(5.8M)
phpdroid_20160703.7z(4.7M)
apk里包含PHP-7.0.8和高性能网络编程扩展Swoole,
另外还有BusyBox和生成二维码的qrencode.
7z包是项目源代码,主要就是MainActivity.java和assets数据.

我提供的APK包里PHP根目录默认是SD卡下的phpdroid,
你把你的PHP脚本放到里面打开应用就可以查看了.
如果你是自己下载Android项目构建,那你的PHP脚本就放在:
AndroidStudioProjects/phpdroid/app/src/main/assets/php/www
如果你不想PHP脚本放在SD卡以免被别人修改,那你就修改start.sh:
把 -t $2/phpdroid 改为 -t $1/php/www 即可.
这样APK安装后PHP脚本就是放在:
/data/data/net.php.phpdroid/php/www/index.php

这里需要说明的是,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)里却不会立即显示,需要重启手机才能看到.

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

关于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常用命令了.

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清理上次应用可能意外退出遗留下来的东西.

 

 

 

展开阅读全文
加载中

作者的其它热门文章

打赏
16
68 收藏
分享
打赏
47 评论
68 收藏
16
分享
返回顶部
顶部