张墨轩的技术宅

不忘初心,方得始终

特殊字符转义的一点体会

在编程的过程中经常会碰到一些特殊字符需要进行字符转义才能使程序正常运行,那为什么要进行字符转义才能使程序正常运行呢,我们举几个例子:

例子一:
比如在C语言中 我们想定义一个字符串如 hello,"moses" 那么如果写成 
char *str = "hello,"moses",are you ok"; 
那么程序铁定出错,编译不过,因为双引号"在C语言中是作为定义字符串开始和结束的一个特殊符号来用的,C编译器遇到上面这样的代码直接晕倒,那么必须像下面这样写:
char *str = " hello,\"moses\",are you ok";  
加上转义符号\才能让C编译器正确进行认知,简单来说就是,对于C编译器来讲,在字符串中【\"】才等于一个双引号【“】。
例子二:
比如在JS语言中我们定义一个JSON 如
{"content" : "hello,"moses",are you ok"}
同样JS解析引擎也会出错,因为双引号"在JS中也是作为定义字符串开始和结束的一个特殊符号来用的,那么必须像下面这样写:
{"content" : "hello,\"moses\",are you ok"}
加上转义符号\才能让JS解析引擎正确进行认知,简单来说就是,对于JS解析引擎来讲,在字符串中【\"】才等于一个双引号【“】。
例子三:
比如在SQL中,这里假设是MYSQL,我们定义一个SQL语句:
select * from mytable where content like 'hello,'moses',are you ok';
SQL解析引擎会出错,因为单引号'在SQL中是做为字符串开始和结束的一个特殊符号来用的,必须像下面这样写:
select * from mytable where content like 'hello,\'moses\',are you ok';
加上转义符号\才能让SQL解析引擎正确进行认知,简单来说就是,对于SQL解析引擎来讲,在字符串中【\'】才等于一个单引号【’】。
例子四:
比如在HTML中,我们定义如下一个标签:
<input type="text" value="hello,"moses",are you ok">
大家可以在浏览器中试一试,输出会被截断,并不能完整的输出 hello,"moses" 这样的字符串,因为双引号在这里是用来定义标签属性的,所以必须像下面这样写:
<input type="text" value="hello,&quot;moses&quot;,are you ok">
这里需要注意的是HTML里面的转义符号是&,而不是符号\。在这个例子上为了在标签属性中表示一个双引号【"】必须要用【&quot;】,当然也可以用【&#34;】来表示,前面那种叫实体名称,后面这种叫实体编号,具体请自行google或者baidu。这里需要强调的是在HTML中是否转义需要根据上下文来,比如:
<html>
<body>
<input type="text" value="hello,&quot;moses&quot;,are you ok">
hello,"moses",are you ok
</body>
</html>
这段代码在浏览器中是能正确显示的,因为第二处字符串并没有和任何特殊字符有冲突。当然如果你一定要转义表示那也没任何问题。如下:
<html>
<body>
<input type="text" value="hello,&quot;moses&quot;,are you ok">
hello,&quot;moses&quot;,are you ok
</body>
</html>
你会发现它在浏览器中也能正确显示,并且显示和前面的一模一样。
这里举了四个例子,总结起来,其实都是一次人机交互的过程,这里的“机器”分别指的是【C编译器】【JS解析引擎】【SQL解析引擎】【HTML解析引擎】,当我们“人”想告诉“机器”,比如告诉【JS解析引擎】我要输入的是一个双引号时,因为双引号对【JS解析引擎】有特殊含义,所以我们必须输入【\"】这样的字符序列给到【JS解析引擎】,这样【JS解析引擎】才会明白我们“人”其实只是想输入一个双引号【"】。
人=>输入序列\"=>JS解析引擎=>理解成=>双引号"
这里需要重点强调的就是关于转义字符问题对于不同的开发语言有着不同的规则,虽然也有很多相同的地方,但是不能预先做假设,需要查阅相关文档。
另外在实际开发过程中经常是各种语言混合开发的,比如做一个网站最常见的形式是 服务端用PHP+MySQL开发,客户端是HTML+JS。服务端PHP代码里面可能会包含SQL语句,而客户端的HTML代码里面可能也会混编入JS代码。那么这个时候我们就必须要有非常清晰的认知,对于一段包含有特殊字符的字符序列来说什么时候由【PHP解析引擎】处理,什么时候由【SQL解析引擎】处理,什么时候由【HTML解析引擎】处理,什么时候由【JS解析引擎】处理,必须搞的明明白白,否则就可能会发生莫名其妙的错误。我们来看一次简单的网页输出过程,很可能是:
【PHP解析引擎】=>sql语句序列=>【SQL解析引擎】=>返回各种结果字符序列=>【PHP解析引擎】=>网络输出给浏览器=>【HTML解析引擎】=>js代码字符序列=>【JS解析引擎】=>执行结果字符序列=>【HTML解析引擎】=>电脑屏幕
简单来说可以理解成一种引擎的输出很可能是作为另一种引擎的输入。其实这也符合计算机系统的基本概念,计算机系统本质上就是输入+处理+输出。
这里我们举几个例子:
我们先来看看例子一,这是一段HTML+JS混编代码,代码如下:
<html>
<body>
<input type="button" value="button" onclick="alert("hello,"moses",are you ok");">
</body>
</html>
这段代码包含了HTML代码也包含了JS代码,这段代码的功能是当点击按钮会弹出一个对话框,对话框的内容是:hello,"moses",are you ok
但是当点击按钮的时候,铁定出错,肯定不会弹出对话框,这么多双引号,估计都晕了,哈哈。既然点击按钮执行的是JS代码,那么首先肯定是想当然的利用JS语言的转义规则进行处理,代码如下:
<html>
<body>
<input type="button" value="button" onclick="alert(\"hello,\"moses\",are you ok\");">
</body>
</html>
结果还是不行,而且错得一塌糊涂,看样子想当然是不行的了,还的仔细捋一捋把来龙去脉搞清楚才能解决。首先我们必须得先明白,上面的代码是首先被【HTML解析引擎】解析后才会交给【JS解析引擎】处理,那么看看上面的代码被【HTML解析引擎】解析后是个什么样子,因为符号\并不是【HTML解析引擎】的转义符,所以最后交给JS执行的代码是alert(\,不出错才怪。既然是先被【HTML解析引擎】解析,那么我们肯定得先用HTML的转义规则来处理,修改后的代码如下:
<html>
<body>
<input type="button" value="button" onclick="alert(&quot;hello,&quot;moses&quot;,are you ok&quot;);">
</body>
</html>
可惜的是这样修改后还是不能正常工作,那是为什么呢,仔细分析一下就可以得知,这段代码被【HTML解析引擎】解析出来的JS代码实际如下:
alert(“hello,”moses“,are you ok”);
这行代码交给【JS解析引擎】后肯定会出错,原因就是中间那两个双引号,必须要按JS语言的规则转义,应该修改成下面这样子:
alert(“hello,\”moses\“,are you ok”);
这样才能正常工作,那么现在我们把HTML和JS两种规则结合起来处理后,得到了最终如下的整合代码:
<html>
<body>
<input type="button" value="button" onclick="alert(&quot;hello,\&quot;moses\&quot;,are you ok&quot;);">
</body>
</html>
至此,程序终于能按照期望运行了。当然实际上我们可以通过双引号和单引号配合使用来解决问题,但是这里我们为了很好的说明问题的来龙去脉,所以不会考虑其他途径。
接下来看看例子二,这是一段PHP+SQL混编代码,为了更好的说明原理和问题,这里全部使用单引号,代码如下:
<?php
$con = mysql_connect('localhost','root','123456');
mysql_select_db('db_test', $con);
mysql_query('INSERT INTO T_Test (F_Content) VALUES ('hello,'moses',are you ok')');
mysql_close($con);
?>
代码的目的是将字符序列 hello,'moses',are you ok 插入数据库中,但是上面这代码肯定是错的,因为对于【PHP解析引擎】来说单引号是作为定义字符串开始和结束的一个特殊字符,所以这里肯定必须进行转义处理,而PHP也是用符号\来作为转义符的,所以我们将代码改成如下样子:
<?php
$con = mysql_connect('localhost','root','123456');
mysql_select_db('db_test', $con);
mysql_query('INSERT INTO T_Test (F_Content) VALUES (\'hello,\'moses\',are you ok\')');
mysql_close($con);
?>
不幸的是,这段代码还是无法正常工作,那是为什么呢,我们来分析一下原因。首先要说的是这段代码是先交给【PHP解析引擎】解析后再把其中的SQL语句交给MySQL的【SQL解析引擎】来处理,有了这个前提后就比较好分析了,我们先来看看通过【PHP解析引擎】解析后出来的代码是什么样子:
解析之前:
INSERT INTO T_Test (F_Content) VALUES (\'hello,\'moses\',are you ok\')
解析之后:
INSERT INTO T_Test (F_Content) VALUES ('hello,'moses',are you ok')
然后解析之后的SQL语句会交给【SQL解析引擎】来处理,答案很明显了,肯定出错,因为在【SQL解析引擎】中单引号也是定义字符串开始和结束的特殊符号。那么这里要重点强调的是:
【PHP解析引擎】解析是没问题的,但是在【SQL解析引擎】解析的时候出错了。
然后接下来我们继续修改代码:
<?php
$con = mysql_connect('localhost','root','123456');
mysql_select_db('db_test', $con);
mysql_query('INSERT INTO T_Test (F_Content) VALUES (\'hello,\\'moses\\',are you ok\')');
mysql_close($con);
?>
一运行,代码还是出错,这又是为什么呢,那是因为中间\\'这样的语法出现了问题,前面那个\将后面的\给转义了从而失去了转义的特性导致单引号没被转义,所以直接在【PHP解析引擎】解析的时候就出错了,那么这里要重点强调的是:
这次是【PHP解析引擎】解析的时候就已经出问题了,压根和【SQL解析引擎】没啥关系。
只能继续修改代码了,修改后的代码如下:
<?php
$con = mysql_connect('localhost','root','123456');
mysql_select_db('db_test', $con);
mysql_query('INSERT INTO T_Test (F_Content) VALUES (\'hello,\\\'moses\\\',are you ok\')');
mysql_close($con);
?>
这次终于成功了,顺利执行完毕,数据也正常写入成功。我们来分析一下这次为什么可以:
【PHP解析引擎】解析前的代码:
INSERT INTO T_Test (F_Content) VALUES (\'hello,\\\'moses\\\',are you ok\')
【PHP解析引擎】解析后的代码:
INSERT INTO T_Test (F_Content) VALUES ('hello,\'moses\',are you ok')
然后再把这句SQL交给【SQL解析引擎】解析,根据转义规则,【SQL解析引擎】也非常清楚的明白要写入的数据里面包含有2个单引号,而不会发生冲突,最终数据终于成功的写入了。
如图:
最后要说的是在实际开发中其实有很多办法可以解决类似的问题,比如单引号和双引号配合使用,比如magic_quotes_gpc=on,addslashes和stripslashes,mysql_real_escape_string等等函数都能比较好的解决问题。
好了,具体的技术细节就说到这里了,那么接下来我们从宏观架构的角度来简单讨论一下这个话题。在项目的实际开发中要考虑的转义字符不单单只有引号,还有诸如<,>,&等等很多很多,这里只讨论几个最常见的。这些转义字符如果不做处理,可能会导致很大的安全隐患,比如古老的SQL注入技术,比如XSS攻击,这里举个最简单的例子,比如在一个文章发布系统里面不做任何特殊处理,那么有人在提交的文章里面加入一个<script src='x.js'>这样的标签那就麻烦大了,所以要做处理。那么到底什么时候做处理呢,我们先看一幅图:
这是现在最为常见的系统架构,一个软件可能会有不同平台的客户端,大家都统一通过RESTfulAPI进行通讯,一个典型的例子就是微信,它有PC端,Android端,IOS端,WEB端等等。
我们在设计这样的系统的时候 RESTfulAPI可能会用PHP,Java,NodeJS等语言开发,Android端开发有Java,IOS上有Objective-C,PC上用C/C++,.Net,Delphi等等,WEB端有HTML+JS。
你会发现这么多语言各有各的规则,所以最好的办法就是在保证安全的前提下输入的数据不做转换直接进入到数据库,输出的数据也不做转换直接给到客户端,然后各种客户端都根据自己的实际情况自行处理,这里打个简单的比方: 
比如用户输入一段字符序列 【hello<hr>"<br>】,那么最终进入数据库的也要是同样的序列,而不应该是【hello&lt;hr&gt;&quot;&lt;br&gt;】,因为这样的转换HTML认得,但是其他客户端比如C/C++就不认得,就会造成各种困扰,所以最终的处理过程应该留给各个客户端自行处理。


五种MySQL管理工具简单介绍

我在弄MySQL的过程中接触到了一些管理工具,这里把它们作为笔记记录下来,因为自己搞的东西实在太多太杂,要是不做笔记搞不好几年后又都忘记啦,哈哈哈。这里不对这些工具做任何评论。什么工具好用,大家仁者见仁,智者见智。
一:Navicat for MySQL
Navicat for MySQL 是一套管理和开发 MySQL 或 MariaDB 的理想解决方案。它使你以单一程序同时连接到 MySQL 和 MariaDB。这个功能齐备的前端软件为数据库管理、开发和维护提供了直观而强大的图形界面。
官方地址为:http://www.navicat.com.cn/products/navicat-for-mysql
下面是它的软件截图:
二:SQLyog
SQLyog是业界著名的Webyog公司出品的一款简洁高效、功能强大的图形化MySQL数据库管理工具。使用SQLyog可以快速直观地让您从世界的任何角落通过网络来维护远端的MySQL数据库。
官方地址为:https://www.webyog.com/
下面是它的软件截图:
三:MySQL Workbench
MySQL Workbench是一款专为MySQL设计的ER/数据库建模工具。它是著名的数据库设计工具DBDesigner4的继任者。你可以用MySQL Workbench设计和创建新的数据库图示,建立数据库文档,以及进行复杂的MySQL 迁移。
官方地址为:http://dev.mysql.com/downloads/workbench/
下面是它的软件截图:
四:phpMyAdmin
phpMyAdmin怕是大家最熟悉的一款管理工具了,它是由php开发的一套开源的web端管理工具,通过web方式控制和操作MySQL数据库。
官方地址为:http://www.phpmyadmin.net/
下面是它的软件截图:
五:MySQL自带的命令行工具
MySQL自带的命令行工具,虽然使用起来没有图形工具那么直观,但是如果熟悉MySQL命令的话,其实也挺不错。
下面是它的软件截图:


云之讯融合通讯开放平台Nodejs版本SDK

最近用到云之讯平台的短信和语音验证码功能,因为没有在他们官网上发现nodejs版本的SDK,所以我按照他们的PHP版本实现了一个nodejs版本的SDK供大家使用。原版PHPSDK也一并上传了,方便大家对比,另外就是XML协议部分并未实现。

在您的nodejs项目中用 npm install ucpaas-sdk --save 命令进行安装。

示例代码如下:
var ucpaasClass = require('ucpaas-sdk/lib/ucpaasClass');
var options = {
    accountsid: 'XXXX193c69eaXXXXXXbe89017fcXXXXX',
    token: 'XXXXXXdfe88a37XXXXXX288ccaXXXXXX'
};
var ucpaas = new ucpaasClass(options);

//开发者账号信息查询
ucpaas.getDevinfo(function (status, responseText) {
    console.log('code: ' + status + ', text: ' + responseText);
});

//语音验证码
var appId = 'XXXXXX2fd25eXXXXb8XXXbaXXXXXXX7a';
var verifyCode = '1256';
var to = '18612345678';
ucpaas.voiceCode(appId, verifyCode, to, function (status, responseText) {
    console.log('code: ' + status + ', text: ' + responseText);
});

//短信验证码
var appId = 'XXXXXX2fd25eXXXXb8XXXbaXXXXXXX7a';
var to = '18612345678';
var templateId = '16021';
var param = '1256,5';
ucpaas.templateSMS(appId, to, templateId, param, function (status, responseText) {
    console.log('code: ' + status + ', text: ' + responseText);
});

//双向回拨
var appId = 'XXXXXX2fd25eXXXXb8XXXbaXXXXXXX7a';
var fromClient = '63314039999129';
var to = '13412345678';
var fromSerNum = '4008800800';
var toSerNum = '18612345678';
ucpaas.callBack(appId, fromClient, to, fromSerNum, toSerNum, function (status, responseText) {
    console.log('code: ' + status + ', text: ' + responseText);
});

//申请client账号
var appId = 'XXXXXX2fd25eXXXXb8XXXbaXXXXXXX7a';
var clientType = '0';
var charge = '0';
var friendlyName = '';
var mobile = '13412345678';
ucpaas.applyClient(appId, clientType, charge, friendlyName, mobile, function (status, responseText) {
    console.log('code: ' + status + ', text: ' + responseText);
});

//释放client账号
var appId = 'XXXXXX2fd25eXXXXb8XXXbaXXXXXXX7a';
var clientNumber = '63314039999129';
ucpaas.releaseClient(clientNumber, appId, function (status, responseText) {
    console.log('code: ' + status + ', text: ' + responseText);
});

//分页获取Client列表
var appId = 'XXXXXX2fd25eXXXXb8XXXbaXXXXXXX7a';
ucpaas.getClientList(appId, '0', '100', function (status, responseText) {
    console.log('code: ' + status + ', text: ' + responseText);
});

//以Client账号方式查询Client信息
var appId = 'XXXXXX2fd25eXXXXb8XXXbaXXXXXXX7a';
ucpaas.getClientInfo(appId, '63314039999129', function (status, responseText) {
    console.log('code: ' + status + ', text: ' + responseText);
});

//以手机号码方式查询Client信息
var appId = 'XXXXXX2fd25eXXXXb8XXXbaXXXXXXX7a';
ucpaas.getClientInfoByMobile(appId, '18612345678', function (status, responseText) {
    console.log('code: ' + status + ', text: ' + responseText);
});

//应用话单下载
var appId = 'XXXXXX2fd25eXXXXb8XXXbaXXXXXXX7a';
ucpaas.getBillList(appId, 'week', function (status, responseText) {
    console.log('code: ' + status + ', text: ' + responseText);
});

代码已经提交到github上,您可以在如下地址访问到:
https://github.com/phonegapX/ucpaas

同时也已经发布到了npmjs.com上,可以直接在node中用 npm install ucpaas-sdk 命令进行安装。

这次具体改写过程也比较简单,没有太多好说的,唯一一个小知识点就是如何用JS中的闭包来模拟php类中的私有变量和私有函数
比如php中:
<?php
class Ucpaas
{
    private $timestamp;
    public function __construct()
    {
        $this->timestamp = 100;
    }
    private function getResult()
    {
        return $this->timestamp;
    }
    public function getDevinfo()
    {
        return getResult();
    }
}
?>
这个PHP类包含了一个私有变量$timestamp,一个构造函数,一个public函数,一个private函数,那么对应JS怎么写比较好呢,如下:
(function () {
    //模拟PHP中的私有变量
    var timestamp;
    //构造函数
    function Ucpaas() {
        timestamp = 100;
    }
    //模拟private函数
    function getResult() {
        return timestamp;
    }
    //模拟public函数
    Ucpaas.prototype.getDevinfo = function() {
        return getResult();
    };
    //导出类
    module.exports = Ucpaas;
}());

这样就很好的利用了JS中闭包的特性来模拟了一个PHP类。

最后附上云之讯的网址:
http://www.ucpaas.com/


APK【解放电源键 1.6.0】逆向全过程

android手机在双击点亮屏幕功能出来以前,为了点亮屏幕得频繁的使用电源键,时间长了电源键容易损坏,所以聪明的人们就开发出用音量键来点亮屏幕的apk程序,安装这样的apk后,点击音量+键或者音量-键都可以点亮屏幕,这样就大大提高了电源键的使用寿命。最近突然对这类功能有点好奇,所以利用空闲时间研究了其中一个apk,叫做【解放电源键 1.6.0】,看了软件的关于信息,作者叫王龙,向他致敬。另外此程序需要root权限支持。首先来看看包结构,如图:
其中比较可疑的是assets/athena.dat文件,经证实这是一个可执行文件,只是取了一个.dat的后缀名迷惑人而已,利用ps查看进程可以得知,如图:
另外利用cat查看内存布局也确定lib/armeabi/librpkjni.so文件也已经被加载,如图:
那么到此分析的重点就在classes.dex,librpkjni.so,athena.dat三个文件上面。
对于dex文件有三个比较好的工具进行反汇编。
一个是 baksmali 或者直接用apktool(内部集成了baksmali ),它会将dex反汇编成smali文件。其实就是文本文件可以直接查看,我用设置了语法高亮的Notepad++查看。效果如图:
二个是用JEB,这是个收费软件 官方网站以前是http://www.android-decompiler.com/ 后来改成了https://www.pnfsoftware.com/ 效果如图:
三是用IDA,IDA Pro从6.1版本开始支持Android。包括Dalvik指令集的反汇编、原生库(ARM/Thumb代码)的反汇编、原生库(ARM/Thumb代码)的动态调试等,IDA 6.6新添加了对dex文件的动态调试支持,具体可以查阅相关文档。效果如图:

这三种工具对Dalvik指令的语法解析上都有些许不同,至于选择何种工具,可以根据自己的喜好来。 而对于librpkjni.so和athena.dat这2个原生程序的反汇编那毫无疑问肯定是用IDA了。
接下来正式进入流程,这里我们只关心关键点,至于其他的就不做过多说明了。通过分析AndroidManifest.xml文件可以得知入口activity是 com.wujianai.rpk.activity.Main,另外有一个服务
是 com.wujianai.rpk.service.RPKService。先看 com.wujianai.rpk.activity.Main的onCreate函数:

.method public onCreate(Bundle )V
          .registers 8
          .param p1, "savedInstanceState"
          .prologue
////////////////////////////////////////////////////
代码省略。。。。。。
////////////////////////////////////////////////////
000000A0  iget-object              v3, p0, Main->serviceIntent :Intent
000000A4  iget-object              v4, p0, Main->serviceConnection :ServiceConnection
000000A8  const/4                  v5, 0x1
000000AA  invoke-virtual           Main->bindService (Intent, ServiceConnection, I)Z, p0, v3, v4, v5
000000B0  invoke-direct            Main->hasRoot ()Z, p0
000000B6  move-result              v3
000000B8  if-nez                   v3, :C2
:BC
000000BC  invoke-direct            Main->doNoRoot ()V, p0
:C2
000000C2  return-void
.end method
这里会启动RPKService服务,同时会调用hasRoot函数检查是否有root权限。
来看看hasRoot函数是如何检查是否有root权限的:
.method private hasRoot()Z
          .registers 8
00000000  const/4                  v6, 0x0
          .prologue
00000002  const/16                 v4, 0x400
00000006  new-array                v0, v4, [C
:A
          .local v0, buf:[C
0000000A  invoke-static            Runtime->getRuntime ()Runtime
00000010  move-result-object       v4
00000012  const-string             v5, "su -c ls"
00000016  invoke-virtual           Runtime->exec(String )Process, v4, v5
0000001C  move-result-object       v2
          .local v2, exec:Ljava/lang/Process;
0000001E  new-instance             v3, InputStreamReader
00000022  invoke-virtual           Process->getErrorStream ()InputStream, v2
00000028  move-result-object       v4
0000002A  invoke-direct            InputStreamReader-><init> (InputStream)V, v3, v4
          .local v3, r:Ljava/io/InputStreamReader;
00000030  invoke-virtual           InputStreamReader->read([C)I, v3, v0
:36
00000036  move-result              v4
00000038  const/4                  v5, 0xFFFFFFFFFFFFFFFF
0000003A  if-ne                    v4, v5, :42
:3E
0000003E  const/4                  v4, 0x1
:40
00000040  return                   v4
:42
00000042  move                     v4, v6
00000044  goto                     :40
:46
00000046  move-exception           v4
00000048  move-object              v1, v4
          .local v1, e:Ljava/io/IOException;
0000004A  move                     v4, v6
0000004C  goto                     :40
          .catch IOException {:A .. :36} :46
.end method

原来是看"su -c ls"命令能否执行成功来判断的:)
 
接下来看看RPKService服务启动后会干些什么:
.method public run()V
          .registers 6
          .prologue
00000000  new-instance             v1, StringBuilder
00000004  invoke-direct            StringBuilder-><init> ()V, v1
:A
          .local v1, res:Ljava/lang/StringBuilder;
0000000A  invoke-static            Runtime->getRuntime ()Runtime
00000010  move-result-object       v2
00000012  new-instance             v3, StringBuilder
00000016  const-string             v4, "su -c ./"
0000001A  invoke-direct            StringBuilder-><init> (String)V, v3, v4
00000020  iget-object              v4, p0, RPKService$3->val$file :File
00000024  invoke-virtual           File->getAbsolutePath ()String, v4
0000002A  move-result-object       v4
0000002C  invoke-virtual           StringBuilder->append (String) StringBuilder, v3, v4
00000032  move-result-object       v3
00000034  invoke-virtual           StringBuilder->toString ()String, v3
0000003A  move-result-object       v3
0000003C  invoke-virtual           Runtime->exec(String )Process, v2, v3
:42
00000042  return-void
:44
00000044  move-exception           v2
00000046  move-object              v0, v2
          .local v0, ex:Ljava/lang/Exception;
00000048  const-string             v2, "wanghelong"
0000004C  invoke-virtual           StringBuilder->toString ()String, v1
00000052  move-result-object       v3
00000054  invoke-static            Log->e(String , String)I, v2, v3
0000005A  goto                     :42
          .catch Exception {:A .. :42} :44
.end method

.method public runScipt()V
          .registers 5
          .prologue
00000000  new-instance             v0, File
00000004  const-string             v2, "bin"
00000008  const/4                  v3, 0x0
0000000A  invoke-virtual           RPKService->getDir (String, I) File, p0, v2, v3
00000010  move-result-object       v2
00000012  const-string             v3, "athena.dat"
00000016  invoke-direct            File-><init> (File, String)V, v0, v2, v3
          .local v0, file:Ljava/io/File;
0000001C  new-instance             v1, RPKService$3
00000020  invoke-direct            RPKService$3-><init> (RPKService, File)V, v1, p0, v0
          .local v1, thread:Ljava/lang/Thread;
00000026  invoke-virtual           Thread->start()V, v1
0000002C  return-void
.end method
.method private static copyRawFile (Context, InputStream, File , String)V
          .registers 10
          .annotation system Throws
              value = {
                  IOException,
                  InterruptedException
              }
          .end annotation
          .param p0, "ctx"
          .param p1, "inputStream"
          .param p2, "file"
          .param p3, "mode"
          .prologue
00000000  new-instance             v2, FileOutputStream
00000004  invoke-direct            FileOutputStream-><init> (File)V, v2, p2
          .local v2, out:Ljava/io/FileOutputStream;
0000000A  const/16                 v3, 0x400
0000000E  new-array                v0, v3, [B
:12
          .local v0, buf:[B
00000012  invoke-virtual           InputStream->read([B)I, p1, v0
00000018  move-result              v1
          .local v1, len:I
0000001A  if-gtz                   v1, :7C
:1E
0000001E  invoke-virtual           FileOutputStream->close()V, v2
00000024  invoke-virtual           InputStream->close()V, p1
0000002A  invoke-static            Runtime->getRuntime ()Runtime
00000030  move-result-object       v3
00000032  new-instance             v4, StringBuilder
00000036  const-string             v5, "chmod "
0000003A  invoke-direct            StringBuilder-><init> (String)V, v4, v5
00000040  invoke-virtual           StringBuilder->append (String) StringBuilder, v4, p3
00000046  move-result-object       v4
00000048  const-string             v5, " "
0000004C  invoke-virtual           StringBuilder->append (String) StringBuilder, v4, v5
00000052  move-result-object       v4
00000054  invoke-virtual           File->getAbsolutePath ()String, p2
0000005A  move-result-object       v5
0000005C  invoke-virtual           StringBuilder->append (String) StringBuilder, v4, v5
00000062  move-result-object       v4
00000064  invoke-virtual           StringBuilder->toString ()String, v4
0000006A  move-result-object       v4
0000006C  invoke-virtual           Runtime->exec(String )Process, v3, v4
00000072  move-result-object       v3
00000074  invoke-virtual           Process->waitFor ()I, v3
0000007A  return-void
:7C
0000007C  const/4                  v3, 0x0
0000007E  invoke-virtual           FileOutputStream->write([B, I, I)V, v2, v0, v3, v1
00000084  goto                     :12
.end method

.method private initAssets ()V
          .registers 7
00000000  const-string             v5, "athena.dat"
          .prologue
00000004  invoke-virtual           RPKService->getAssets ()AssetManager, p0
0000000A  move-result-object       v0
          .local v0, assetManager:Landroid/content/res/AssetManager;
0000000C  new-instance             v2, File
00000010  const-string             v3, "bin"
00000014  const/4                  v4, 0x0
00000016  invoke-virtual           RPKService->getDir (String, I) File, p0, v3, v4
0000001C  move-result-object       v3
0000001E  const-string             v4, "athena.dat"
00000022  invoke-direct            File-><init> (File, String)V, v2, v3, v5
          .local v2, file:Ljava/io/File;
00000028  invoke-virtual           File->exists ()Z, v2
0000002E  move-result              v3
00000030  if-nez                   v3, :4A
:34
00000034  const-string             v3, "athena.dat"
00000038  invoke-virtual           AssetManager->open(String )InputStream, v0, v3
0000003E  move-result-object       v3
00000040  const-string             v4, "777"
00000044  invoke-static            RPKService->copyRawFile (Context, InputStream, File, String )V, p0, v3, v2, v4
:4A
0000004A  return-void
:4C
0000004C  move-exception           v3
0000004E  move-object              v1, v3
          .local v1, e:Ljava/lang/Exception;
00000050  invoke-virtual           Exception->printStackTrace ()V, v1
00000056  goto                     :4A
          .catch Exception {:34 .. :4A} :4C
.end method

.method private onServiceStart ()V
          .registers 1
          .prologue
00000000  invoke-direct            RPKService->initAssets ()V, p0
00000006  invoke-virtual           RPKService->runScipt ()V, p0
0000000C  return-void
.end method
总结起来就一句话:释放athena.dat这个可执行文件然后修改成可执行属性后再执行它。
RPKService服务还会启动一个线程:
.method public onStartCommand(Intent , I, I)I
          .registers 5
          .param p1, "intent"
          .param p2, "flags"
          .param p3, "startId"
          .prologue
00000000  invoke-super             Service->onStartCommand (Intent, I, I)I, p0, p1, p2, p3
00000006  iget-object              v0, p0, RPKService->thread :Thread
0000000A  invoke-virtual           Thread->isAlive ()Z, v0
00000010  move-result              v0
00000012  if-nez                   v0, :20
:16
00000016  iget-object              v0, p0, RPKService->thread :Thread
0000001A  invoke-virtual           Thread->start()V, v0
:20
00000020  const/4                  v0, 0x3
00000022  return                   v0
.end method
线程是在RPKService的构造函数里面定义的:
.method public run()V
          .registers 5
          .prologue
00000000  const/4                  v1, 0xFFFFFFFFFFFFFFFE
00000002  invoke-static            Process->setThreadPriority (I)V, v1
00000008  const/4                  v0, 0x0
:A
          .local v0, wakeLock:Landroid/os/PowerManager$WakeLock;
0000000A  iget-object              v1, p0, RPKService$2->this$0 :RPKService
0000000E  invoke-static            RPKService->access$3 (RPKService) RPKJNILoad, v1
00000014  move-result-object       v1
00000016  invoke-virtual           RPKJNILoad->fun1()V, v1
0000001C  invoke-static            Thread->interrupted ()Z
00000022  move-result              v1
00000024  if-eqz                   v1, :2A
:28
00000028  return-void
:2A
0000002A  iget-object              v1, p0, RPKService$2->this$0 :RPKService
0000002E  invoke-static            RPKService->access$4 (RPKService) PowerManager, v1
00000034  move-result-object       v1
00000036  const                    v2, 0x1000000A
0000003C  const-string             v3, "RPKService"
00000040  invoke-virtual           PowerManager->newWakeLock (I, String)PowerManager$WakeLock , v1, v2, v3
00000046  move-result-object       v0
00000048  const-wide/16            v1, 0x1388
0000004C  invoke-virtual           PowerManager$WakeLock->acquire (J)V, v0, v1, v2
00000052  goto                     :A
.end method
这个线程里面会调用librpkjni.so中的fun1函数,此时线程会阻塞在这个函数当中,一直等待,直到这个函数返回,线程就会调用PowerManager->newWakeLock 点亮屏幕(终于看到核心点了)。这里的PowerManager是电源管理器,在之前由getSystemService(Context.POWER_SERVICE)赋值。那么问题来了,librpkjni.so中的fun1函数内部到底做了什么呢?为了搞清楚这个问题我们接下来要开始分析librpkjni.so和athena.dat这两个原生程序了。
先看看librpkjni.so中的fun1函数(JNI相关的知识可自行查询资料),如图:
将其还原成伪代码大概就是下面这个样子:
其实本质上就是在做进程间通讯,和一个名叫com.my.MyService的服务通讯,而这个服务就是在athena.dat进程中实现的(android服务是一个复杂的系统,包含java层面的服务,原生层面的服务,服务管理器等等,具体的细节请自行查阅相关资料)。记住athena.dat进程是以root权限运行的,它会监听linux输入子系统,判断用户是否有按音量键,如图:
通过打开/dev/input/event系列设备进行监听,一旦用户按了音量键athena.dat进程就会捕获到,这样librpkjni.so中的fun1函数就会返回,应用中等待的线程就会继续执行调用
PowerManager->newWakeLock 点亮屏幕。完成一次点亮屏幕的过程。
最后总结一下:站在linux进程的角度来讲本程序运行后会产生2个进程,一个APK主进程,一个athena.dat进程(root权限运行),APK主进程中会调用librpkjni.so中的fun1函数与
athena.dat进程通讯并且一直等待通知。而在athena.dat进程中会监听linux输入子系统来判断用户是否按了电源键,如果用户按了电源键后 APK主进程中的librpkjni.so中的fun1函数
就会结束等待,代码就会执行到PowerManager->newWakeLock 从而点亮屏幕。具体详细可参阅源代码。
全部代码已经上传到github上,为了演示和编译的方便,源代码中进程通讯部分没有利用andorid的binder机制,只是简单的用了管道来实现,因为要实现
android服务需在android源代码环境下进行开发和编译,比较麻烦,所以就简单处理之。
补充说明:原版程序在android5.x的系统上是失效的,原因是athena.dat进程运行不起来,手动运行此文件会报错:error: only position independent executables (PIE) are supported.
原因是google给android5.0增加了新的安全特性,不支持PIE(position independent executables)的程序都不能运行。所以编译的时候要加上-pie -fPIE 的选项,这样才能在android5.x系统上正常运行。
源代码的github地址:
https://github.com/phonegapX/com.wujianai.rpk


浅析微信x5内核inspect调试

最近做一个基于微信公众号的产品,用到了微信的x5浏览器调试的功能,这里简单的做个总结。站在微信客户端角度来讲,微信公众号其实就是由一系列的网页组成的程序,它运行在微信内置的浏览器中,产品的调试就是网页的调试,这里就涉及到一个远程调试网页的问题。我们需要在开发机上远程调试位于手机上的微信内置浏览器中的网页。庆幸的是现在这已经不是什么大问题,腾讯X5浏览服务中已经提供了相应的解决方案,相关资料在 
在分析腾讯X5的方案前,我们先回顾一下google的解决方案:
利用google chrome浏览器支持远程调试也已经有蛮长一段时间了,它本身也在不断的升级和变化,我们先来看看它以前的模式,然后再看看现在的模式。
以前的模式的资料在:
这里需要重点说明的部分是我们在进行远程调试时 在开发机的chrome浏览器中运行的调试工具其实本身就是个web程序,名字叫做 WebInspector 它是由html,css,javascript写成的,chrome浏览器自带了这个web程序,路径在chrome-devtools://devtools/bundled/devtools.html,新版的chrome浏览器路径已经改成了chrome-devtools://devtools/bundled/inspector.html,可以自行打开看看,你如果不信这是网页程序的话,你可以打开后再按f12呼出chrome原生的网页调试工具,你会神奇的发现屏幕上2个一模一样的调试工具,其中一个调试工具正在调试另外一个调试工具。如图:
这个调试工具本身就是个web程序,那么我们也可以把它当一个网站放到互联网上去访问,google确实这样做了,比如你可以访问:
这个调试工具,严格说应该叫做调试前端工具,它由html,css,javascript实现,怎么跟后端的被调试程序通讯呢,这里使用的是websocket的方法。通过文档我们知道调试前要执行命令
adb forward tcp:9222 localabstract:chrome_devtools_remote  通过adb将9222端口转发到手机端去。流程如下:

WebInspector(html,css,javascript实现,运行在浏览器中) ==>websocket==>adb守护进程==>手机端adbd==>被调试程序
chrome后来又推出了新的支持远程调试的方法,新的方法号称不再需要adb了,操作也更加简单,与chrome浏览器集成度更高了。资料参考:
虽然号称不需要adb,其实也只是chrome内部集成了adb的功能而已,所以外部就不需要adb这个程序配合了。当然如果外部存在adb守护进程的话,chrome还是会尝试连接外部adb的5037端口建立连接。 同时既然不需要adb了那也不存在9222端口进行转发到手机了,那做为调试前端的WebInspector又怎么跟后端被调试程序进行通讯了,答案是chrome暴露出来的js内部接口,
WebInspector通过javascript调用chrome内部的调试功能,直接与chrome浏览器通讯。
WebInspector==>内部接口==>chrome浏览器==>手机端adbd==>被调试程序

或者是

WebInspector==>内部接口==>chrome浏览器==>5037端口==>adb守护进程==>手机端adbd==>被调试程序

最后看看远程调试微信webview是如何实现的,其实用的就是chrome远程调试老的方法。

第一步在手机上安装TbsSuiteNew.apk,这个app关键的功能就是将微信的浏览器内核换成一个暴露了9222调试端口的x5浏览器内核。
第二步是运行调试包里面的inspector.py,打开这个inspector.py文件看看代码,就知道其实主要就做了两件事情,第一就是利用adb forward命令将本机9222端口转发到手机端的9222端口上,然后就是启动一个简单的web服务器,它会监听9223端口,这个web服务器的目的就是让浏览器能够通过http://localhost:9223/inspector.html这样的地址可以访问到我们前面说到的WebInspector这个由html,css,javascript实现的调试前端工具。如图:
有兴趣的人可以自己去研读这些代码。
第三步在开发机中用chrome浏览器打开http://localhost:9222,因为9222端口已经转发到了手机上的x5内核,所以这个时候手机上的x5内核就会返回待调试的页面给开发机上的chrome浏览器,然后用户选中某个要调试的页面,又会打开http://localhost:9223/inspector.html?host=localhost:9222&page=2这样的页面,前面已经说过,这个时候就已经运行起了WebInspector这个调试前端工具,然后WebInspector会通过websocket的方式连接本地的9222端口,因为这个端口已经转发到了远端手机上的x5内核的9222的端口上,到此调试会话已经建立起来,用户就可以进行调试工作了。事实上如果手机和开发机都连接在同一个内网中,那么不用adb也没问题,假设我们的手机ip地址为192.168.104,那么这个时候我们只需要先将http://localhost:9222 替换成http://192.168.1.104:9222。 然后把具体调试页也换成如 http://localhost:9223/inspector.html?host=192.168.1.104:9222&page=2 这样子就可以了。
下面是调试"这个调试工具"的截图,可以清楚的看到通过websocket连到了9222端口:
流程如下:
WebInspector(开发机)==>websocket(本机9222端口)==>adb守护进程(开发机)==>adbd(手机端)==>腾讯X5内核(手机端)
或者是
WebInspector(开发机)==>websocket(192.168.1.104:9222端口)==>腾讯X5内核(手机端)

总结:

WebKit 是一个开源的浏览器引擎,apple和google等都在使用它,虽然现在google推出了blink,但是它也是一个webkit的派生品,微信的x5内核也是一个webkit的派生品,所以如果有兴趣进一步了解,完全可以把webkit源代码下载下来研究一下,Inspector的大部分代码都在WebCore/inspector下,有兴趣可以研读。
一些有用的站点和资料:
http://alpha.publicore.net/_/brackets/src/LiveDevelopment/Inspector/inspector.html
https://developer.chrome.com/devtools/docs/debugger-protocol
http://x5.tencent.com/index


nodejs中process对象浅析

本文是基于win7 32位系统。 通过阅读nodejs项目的源代码可以看到,nodejs由 c/c++和javascript俩部分实现,其中js部分的核心为node.js文件。 既然分为2部分 那么中间通讯配合的问题如何解决,其中有一个很关键的对象process就与此有关。我们来看看此对象。 这个对象在js层面是一个全局对象可以直接使用。他的实现代码在src\node.cc中 主要看看其中的SetupProcessObject函数,下面是部分代码截图:

这些原生函数都对应了js中process对象的方法。比如js代码 process. binding( 'xxx') 实际上就调用了c/c++代码中的Binding函数,同理process.dlopen实际上是调用了c/c++代码中的DLOpen函数.而DLOpen中又调用了uv_dlopen, 对于windows版本的uv_dlopen函数位于deps\uv\src\win\dl.c中,如图:
最终调用了LoadLibaryExW加载dll文件。
回过头来再看看 process. binding 这个函数,那么这个函数到底是干什么用的呢?这个函数实际上是用来绑定原生代码中的内置模块用的,简单来说就是js代码利用process. binding( 'xxx') 获取由c/c++实现的内置模块'xxx'的引用,然后js代码就可以通过这个引用访问内置模块'xxx'所提供的各种功能了。
我们知道node可以加载两种模块 一种由js代码编写的模块,后缀名为.js, 还有一种由c/c++实现的原生模块,后缀名为.node。我们看看nodejs中是如何处理的,实现代码在
lib\moudle.js中,如图:
其中加载.json文件比较简单,这里不做讨论。 我们先看看如何加载.js模块。 可以看到代码将进入module._compile函数,在_compile函数中又经过层层调用获取了内置模块
'contextify'的引用,代码如:var binding = process. binding( 'contextify'); 然后通过这个引用调用runInThisContext等内置模块中提供的功能。此函数位于src\node_contextify.cc中,
如图:
RunInThisContext函数:
RunInThisContext函数中又将调用EvalMachine函数,然后EvalMachine函数中会利用v8引擎执行js代码。至此.js模块加载完毕。

接下来看看如何加载.node原生模块,这个就比较简单了,直接调用process.dlopen就加载原生模块了。如前所叙.node模块其实就是原生模块,比如在windows下面.node模块其实就是dll文件,linux下面其实就是so文件。process.dlopen(js代码)=>DLOpen(c/c++)=>uv_dlopen(c/c++)=>LoadLibaryExW(windows版本)。
自此,我们在js代码中就可以通过全局对象process直接或间接的加载各种各样的模块了,也正因为有了如此能力node平台基本上是无所不能了。
那么js代码中这个全局对象process本身又是怎么获取到的了,答案就在node.js这个文件中。代码如下:
整个文件的入口是个函数,函数只有一个参数 就是process,然后程序会将这个process保存为全局对象,代码如下:
那这个process是哪里传进来的了,这个js入口函数又是由谁来调用的呢,调用代码就在src/node.cc中的LoadEnvironment函数中:
void LoadEnvironment(Environment* env) {
  HandleScope handle_scope(env->isolate());

  V8::SetFatalErrorHandler(node::OnFatalError);
  V8::AddMessageListener(OnMessage);

  // Compile, execute the src/node.js file. (Which was included as static C
  // string in node_natives.h. 'natve_node' is the string containing that
  // source code.)

  // The node.js file returns a function 'f'
  atexit(AtExit);

  TryCatch try_catch;

  // Disable verbose mode to stop FatalException() handler from trying
  // to handle the exception. Errors this early in the start-up phase
  // are not safe to ignore.
  try_catch.SetVerbose( false);

  Local<String> script_name = FIXED_ONE_BYTE_STRING(env->isolate(), "node.js");
  Local<Value> f_value = ExecuteString(env, MainSource(env), script_name);
  if (try_catch.HasCaught())  {
    ReportException(env, try_catch);
    exit(10);
  }
  assert(f_value->IsFunction());
  Local<Function> f = Local<Function>::Cast(f_value);

  // Now we call 'f' with the 'process' variable that we've built up with
  // all our bindings. Inside node.js we'll take care of assigning things to
  // their places.

  // We start the process this way in order to be more modular. Developers
  // who do not like how 'src/node.js' setups the module system but do like
  // Node's I/O bindings may want to replace 'f' with their own function.

  // Add a reference to the global object
  Local<Object> global = env->context()->Global();

#if defined HAVE_DTRACE || defined HAVE_ETW
  InitDTrace(env, global);
#endif

#if defined HAVE_PERFCTR
  InitPerfCounters(env, global);
#endif

  // Enable handling of uncaught exceptions
  // (FatalException(), break on uncaught exception in debugger)
  //
  // This is not strictly necessary since it's almost impossible
  // to attach the debugger fast enought to break on exception
  // thrown during process startup.
  try_catch.SetVerbose( true);

  NODE_SET_METHOD(env->process_object(), "_rawDebug", RawDebug);

  Local<Value> arg = env->process_object();
  f->Call(global, 1, &arg);
}
参见代码最后 f->Call(global, 1, &arg); 就是调用之前的js入口函数,其中arg就是传给js层面的process对象,  Local<Value> arg = env->process_object();
当然c/c++与js之间具体的交互细节就属于v8这个js引擎的工作了,这个又是后话了。


简单解析child_process模块fork方法调用流程

本文测试环境是win7 32位。 先下载nodejs源代码以备查看之用。同时安装好node-inspector环境并配置好,命令:  npm install -g node-inspector
写一段测试代码如下:
var childProcess = require('child_process');
var n = childProcess. fork( './son.js');
保存为parent.js 然后调试运行 node --debug-brk  parent.js  并且打开GoogleChrome进行调试
其中第一行 "(function (exports, require, module, __filename, __dirname) {" 是nodejs内核对我们编写的js代码的包装,将我们写的js代码包装到一个函数内,相关详细可自行查阅资料。
开始跟踪fork方法,跟踪堆栈如下:
首先fork将进入exports.fork。 我们打开源代码查看,源码位于child_process.js中
exports.fork = function (modulePath /*, args, options*/) {

 
 // Get options and args arguments.
 
 var options, args, execArgv;
 
 if ( util. isArray( arguments[ 1])) {
   
 args = arguments [1 ];
   
 options = util ._extend ({}, arguments [2 ]);
  }
 else if (arguments [1 ] && typeof arguments[1] !== 'object' ) {
   
 throw new TypeError( 'Incorrect value of args option');
  }
 else {
   
 args = [];
   
 options = util ._extend ({}, arguments [1 ]);
  }

 
 // Prepare arguments for fork:
 
 execArgv = options .execArgv || process.execArgv;
 
 args = execArgv. concat([modulePath], args);

 
 // Leave stdin open for the IPC channel. stdout and stderr should be the
  // same as the parent's if silent isn't set.
 
 options. stdio = options. silent ? [ 'pipe', 'pipe', 'pipe', 'ipc'] :
      [
0, 1, 2, 'ipc' ];

 
 options. execPath = options. execPath || process. execPath;

 
 return spawn(options.execPath, args, options);
};

流程将从exports.fork=>spawn中。

打开spawn函数,代码仍然位于child_process.js中:
var spawn = exports. spawn = function( /*file, args, options*/) {
 
 var opts = normalizeSpawnArguments.apply( null, arguments);
 
 var options = opts.options;
 
 var child = new ChildProcess();

 
 child. spawn({
   
 file: opts.file,
   
 args: opts.args,
   
 cwd: options .cwd ,
   
 windowsVerbatimArguments : !!options .windowsVerbatimArguments ,
   
 detached: !!options .detached ,
   
 envPairs: opts.envPairs ,
   
 stdio: options .stdio ,
   
 uid: options .uid ,
   
 gid: options .gid
 
 });

 
 return child;
};

流程将从exports.fork=>spawn=>ChildProcess. spawn中

接下来看 ChildProcess. spawn函数,代码仍然位于child_process.js中,此函数比较长 这里只贴出关键代码:
var Process = process. binding( 'process_wrap').Process;
function ChildProcess() {
  .......  
 
 this. _handle = new Process();
  .......
}
ChildProcess. prototype. spawn = function(options) {
  .......
 
 var err = this._handle.spawn(options);
  .......
}

流程将进入this. _handle. spawn中,此函数已经属于原生函数了,原理可自行了解process. binding的实现。
代码位于src\process_wrap.cc中

  NODE_SET_PROTOTYPE_METHOD(constructor, "spawn", Spawn);

  static void Spawn(const FunctionCallbackInfo<Value>& args) {
         .......
         int err = uv_spawn(env->event_loop(), &wrap->process_, &options);
       ......
  }

流程将进入uv_spawn函数,此函数位于deps\uv\src\win\process.c中

int uv_spawn(uv_loop_t* loop,
             uv_process_t* process,
             const uv_process_options_t* options) {
  .........
  if (!CreateProcessW(application_path,
                     arguments,
                     NULL,
                     NULL,
                     1,
                     process_flags,
                     env,
                     cwd,
                     &startup,
                     &info)) {
    /* CreateProcessW failed. */
    err = GetLastError();
    goto done;
  }
  .........
}

这下子终于看到windows下创建进程的api函数CreateProcessW了。
总结:
exports.fork(js层面)=>spawn(js层面)=>ChildProcess. spawn(js层面)=>Spawn(原生层面)=>uv_spawn(原生层面)=>CreateProcessW(操作系统api)


通俗讲解 Hybrid App 界面的几种构成模式

本文将从android平台的角度来讨论。我们知道在android上一个activity代表一页屏幕,现在假设有一个app由 <登录> <列表> <详细内容> 三个页面构成,那么对于一个
native app来说 一般情况下会用三个activity来分别实现 <登录>  <列表>  <详细内容> 如图:
有了上面native app的基本概念以后,接下来我们说说Hybrid App有多少种模式。先看第一种,同样可以用三个activity,然后每个activity里面内嵌一个webkit视图,每个视图分别加载各自的html文件,如
<登录.html>  <列表.html> <详细内容.html> 如图:
那么不同界面之间,比如说从<登录界面>切换到<列表界面>还是利用android原生的方式,是从一个activity切换到另外一个activity。如果要做转场动画或者特效也是android原生方式,所以从这个角度来看,用户体验接近native app。
然后我们再说说Hybrid App的第二种模式。我们可以只用一个activity,这个activity包含一个webkit视图,通过加载不同的html页面来达到效果,这也是最传统的模式。如图:
在这种模式下界面之间的切换是利用web本身的方式例如<a href='xxx'>超链接,或者javascript编程方式等进行跳转,界面之间的转场动画或者特效也是只能由css,javascript等来实现,利用浏览器本身来进行渲染。那么用户体验比原生会有些差距,特别是在低端配置的机器上面。
接下来说说第三种模式,同样是只用一个activity,这个activity包含一个webkit视图,但是将三个html做成一个html。其实也就是单页Web 应用 (single-page application 简称为 SPA)  比如将 <登录.html>  <列表.html> <详细内容.html> 合成一个页面 <单页.html> 如果用jquerymobile这样的库来实现,内容大概类似于:
<html>
<body>
<div id='page-1'>
     登录界面
</div>
<div id='page-2'>
     列表界面
</div>
<div  id='page-3'>
      详细内容界面
</div>
</body>
</html>

如图:
在这种模式下和上面那种情况类似。界面之间的切换,转场动画或者特效等也是利用web本身的方式html,css,javascript等完成,利用浏览器本身来进行渲染。而且SPA因为将多个页面都做在一个页面里面会导致这个页面体积比较大,webkit第一次加载这个页面的时候可能时间会比较长,不过一般来说hybrid app的页面资源一般都放在本地,所以可能影响并不大。总的来说用户体验比原生会有些差距,特别是在低端配置的机器上面。

最后我们来说说第四种模式,这也是最后一种模式,这种模式做出来的效果也是最接近原生app的体验效果。
三个界面还是用三个activity来表示,但是每个activity可能会包含若干个webkit视图用于显示界面的不同部分。比如<界面头部.html>,<界面底部导航条.html>,
<左边面板.html>,<正文展示区.html> 等。文字表述起来不便, 直接上图简单明了。如下:
大家有没有发现前面说到的那几种模式同一界面同一时间肯定只能由某一web页面来表现,在同一个界面中要想实现某种效果,比如从界面左边拉出一个控制面板,只能用html,css,javascript来完成,由webkit渲染。这样效果肯定和原生有差距,所以我们可以根据实际情况同一个界面用多个web页面来组合完成。这样一些工作可以在原生层面来执行,这样用户体验就会有所提高。
补充:
用户体验的好坏是评判一个app非常重要的因素,那么还有什么方式可以进一步缩小hybrid app与native app之间的差距呢?答案就是尽可能的多用原生功能,少用javascript等去实现,比方说在界面上弹出一个菜单列表那么这个时候可以用javascript配合css,html等去实现,当然我们也可以换一种方式,我们可以封装一个组件,这个组件的功能是弹出一个原生菜单列表,然后我们只需要用javascript去调用这个弹出菜单组件就可以了。
国外比较火的Hybrid App框架有phonegap(cordova),国内也有apicloud,appcan等,这些产品做出来的app都逃不出本文所描述的范畴,大家可以亲自去尝试。


GapDebug简易分析

最近做前端开发,用到了GapDebug,觉得这工具挺有意思的,就大概研究了下,这里简单总结一下(我的开发环境是win7,设备是nexus5,android5.0)。
GapDebug运行后将自己变成了一个本地web服务器。当打开菜单点击调试器,会弹出调试器,这个调试器其实就是chrome浏览器(事实上其他webkit内核的浏览器也可以),会访问GapDebug(注意此时GapDebug可以看成本地web服务器)提供的页面 。如http://localhost:8080/gapdebug/index.html。调试工具本身就是web页面构成。操作调试工具其实本质上就是操作网页,这时网页会通过ajax访问GapDebug进程,然后GapDebug进程收到访问请求又会去访问adb守护进程,然后adb守护进程又会通过usb线缆或者是tcpip与我的nexus5手机上的adbd守护进程通讯,然后adbd与包含有webkit控件的app通讯。如下:
chrome浏览器(调试工具网页)<==>GapDebug(看成web服务器)<==>adb(默认是5037端口)<==>adbd(手机端)<==>包含webkit的app(手机端)
这里顺便提下chrome这个强大的浏览器,本身也带有远程调试web功能,在浏览器运行chrome://inspect/#devices页面 就可以进行操作,流程都大同小异。如下:
chrome浏览器(chrome://inspect/#devices)<==>adb(默认是5037端口)<==>adbd(手机端)<==>包含webkit的app(手机端)

这里还有一种简单的方式,命令行中运行  adb forward tcp:9222 localabstract:chrome_devtools_remote  然后打开浏览器输入http://localhost:9222 就可以远程调试了。
数据流程如下:
chrome浏览器(http://localhost:9222)<==>adb(9222端口)<==>adbd(手机端)<==>包含webkit的app(手机端)

补充:
1. 想用上面几种调试方法记得先翻墙,因为包含有调试工具页面的url都被墙了,如 appspot.com
2. 另外有兴趣可以用chrome浏览器打开chrome-devtools://devtools/bundled/devtools.html 看看,这个就是网页版的调试工具,强大!!!
3. 如果是调试网页,移动设备需要安装Chrome for Android ,且安卓系统须为Android 4.0+
4. 如果要调试自己app中嵌入的webkit  需要系统为Android 4.4+ 并且原生应用内的WebView须进行相应的调试配置,在代码中加入WebView.setWebContentsDebuggingEnabled(true)。
5. 远程调试要求桌面版Chrome浏览器版本要高于安卓移动设备的Chrome版本号。有条件的最好使用Chrome 的金丝雀特别版Chrome Canary (Mac/Windows)或者Chrome桌面开发版Chrome Dev channel release (Linux)。
6. GapDebug只所以能调试PhoneGap打包的app,本质上还是用了google官方提供的调试功能,所以要求系统是Android 4.4及其以上版本。

参考:


android-arm逆向学习宏观知识点

一. linux原生层面:

包含三个层次:
1. c/c++语言层面
2. 汇编语言层面
3. 二进制指令层面
正向流程:
c/c++源文件==>gcc编译器==>s汇编源文件==>as汇编器==>包含arm二进制指令的elf格式文件[==>objcopy==>bin格式纯指令]
反向流程:
包含arm二进制指令的elf格式文件==>IDA, objdump==>arm反汇编输出==>人工阅读理解或者IDA-F5插件==>c/c++源文件
学习重点:
1. elf格式及其相关知识
2. 汇编语言(arm-asm语法,gun-asm语法)
3. arm指令二进制级别的编码和构成
4. 反汇编工具的学习和使用


二. android层面:
包含五个层次:
1. java语言层面
2. java汇编语言层面(暂称)
3. java字节码(二进制指令)层面
4. dalvik汇编语言层面(暂称)
5. dalvik字节码(二进制指令)层面
正向流程:
java源文件==>java编译器==>包含java字节码的class格式文件==>dx工具==>包含dalvik字节码的DEX格式文件
dalvik汇编源文件(smali格式的汇编语法)==>smali汇编工具==>包含davlik字节码的DEX格式文件
反向流程:
包含dalvik字节码的DEX格式文件==>baksmali工具==>dalvik反汇编(smali格式的汇编语法文件)==>人工阅读理解==>java源文件
包含dalvik字节码的DEX格式文件==>IDA工具==>dalvik反汇编(IDA格式的汇编语法)==>人工阅读理解==>java源文件
包含dalvik字节码的DEX格式文件==>dex2jar工具==>java字节码(多class文件打包进一个jar包)==>jd-gui工具==>java源文件
学习重点:
1. DEX格式及其相关知识
2. dalvik汇编语言(smali语法)
3. dalvik字节码二进制级别的编码和构成
4. 各种工具的学习和使用

补充:
android目前有两种执行环境:
1. dalvik运行时
2. art运行时
在Dalvik运行时中,APK在安装的时候,安装服务PackageManagerService会通过守护进程installd调用一个工具dexopt对打包在APK里面包含有Dex字节码的classes.dex进行优化,优化得到的文件保存在/data/dalvik-cache目录中,并且以.odex为后缀名,表示这是一个优化过的Dex文件。在ART运行时中,APK在安装的时候,同样安装服务PackageManagerService会通过守护进程installd调用另外一个工具dex2oat对打包在APK里面包含有Dex字节码进翻译。这个翻译器实际上就是基于LLVM架构实现的一个编译器,它的前端是一个Dex语法分析器。翻译后得到的是一个ELF格式的oat文件,这个oat文件同样是以.odex后缀结束,并且也是保存在/data/dalvik-cache目录中。
学习重点:
1. odex格式及其相关知识
2. ELF格式的oat文件相关知识


«1234567»

Powered By Z-Blog 2.2 Prism Build 140101

Copyright phonegap.me Rights Reserved.