#分析程序的方法
在“借鉴“阶段的时候,需要掌握一些分析开源程序的方法。我总结了几条分析开源程序的方法,供大家参考。
##先看文档了解程序功能
很多人可能习惯拿到代码就开始看,这时候对程序整体一点不了解,是十分痛苦的,我们大脑对整个程序都是陌生的,很容易触发缘脑的阻碍机制。我们要先说服缘脑, 先大概了解程序的功能,如果有文档的话,先看文档,这样先了解功能后再去看源码,对大脑来说就不会那么陌生,减少缘脑的阻碍。
有些开源程序还在文档中讲了程序的编程思想、架构原理的。如果我们不看文档光想通过看代码看出编程思想是比较困难的,先看文档了解编程思想再去看代码就比较容易理解。
每种编程语言都有将注释生成文档的工具,比如PHP有phpdoc, Java有JavaDoc , iOS有AppleDoc。大家需要了解这些工具,有时候虽然程序没有稳定,但程序注释符合一定规范,可以用这些工具把注释生成一份文档。
##断点调试
我们的程序运行速度是非常快的,几毫秒就执行完了, 根本看不清楚执行过程, 有没有一种方法,能放慢程序的运行过程,让我们看清楚程序执行的每一步?这样就方便我们分析程序执行的每个过程。
断点调试就能做到这样的效果, 程序每执行一行代码都会暂停,在编辑器中显示出执行这行代码时各个变量的值、调用栈等信息(如图1-15)。等我们点击下一步按钮,程序才会执行下一行代码。这样我们可以一步一步分析程序。
![](https://box.kancloud.cn/2016-01-12_5695156127350.png)
图1-15 netbeans和xdebug结合调试
PHP做断点调试需要xdebug扩展结合一个IDE编辑器(如netbeans,phpstorm等编辑器)。其他编程语言也有相应的断点调试方法,大家可以在搜索引擎搜索对应工具进行学习。
##内置函数
很多内置的函数也有利于分析程序代码,我以PHP为例,列举几个用内置函数分析程序代码的方法。
* 用debug_backtrace看调用栈
调用栈能显示出程序的调用过程,比如一个程序先执行了A函数,A函数中又调用了B函数 ,B函数中又调用了C函数, 那么C函数的调用栈显示出来就是 A->B->C 的执行顺序。比如执行下面的示例代码
```
<?php
function a(){
b('hello');
}
function b($arg){
c();
}
function c(){
var_dump(debug_backtrace());
}
a();
```
我们在c函数中, 用debug_backtrace 获得调用栈并用var_dump输出。debug_backtrace函数返回值是一个数组,数组中记录了每一步调用信息,包括文件、行数、执行的函数名、函数传参参数。这个调用栈要从下往上看:先看数组最后一个元素,它是第一步程序执行过程;倒数第二个元素为第二步程序执行过程。运行结果如下:
```
array(3) {
[0]=>
array(4) {
["file"]=>
string(27) "/Users/luofei/test/test.php"
["line"]=>
int(6)
["function"]=>
string(1) "c"
["args"]=>
array(0) {
}
}
[1]=>
array(4) {
["file"]=>
string(27) "/Users/luofei/test/test.php"
["line"]=>
int(3)
["function"]=>
string(1) "b"
["args"]=>
array(1) {
[0]=>
&string(5) "hello"
}
}
[2]=>
array(4) {
["file"]=>
string(27) "/Users/luofei/test/test.php"
["line"]=>
int(13)
["function"]=>
string(1) "a"
["args"]=>
array(0) {
}
}
}
```
debug_backtrace获得调用栈非常详细,包括每个传参的值都显示出来了。但有时候我们不需要这么详细,可以使用 debug_print_backtrace函数打出一个简单的调用栈。这个函数自己有输出行为,不需要用var_dump打印。 将上面示例代码 `var_dump(debug_backtrace())` 改为 `debug_print_backtrace()` , 再运行程序,我们得到如下结果:
```
#0 c() called at [/Users/luofei/test/test.php:6]
#1 b(hello) called at [/Users/luofei/test/test.php:3]
#2 a() called at [/Users/luofei/test/test.php:13]
```
调用栈帮助我们快速分析程序的执行流程,比如我们在分析一些开源的MVC框架时, 很想知道核心代码是哪儿调用Controller的, 这时候我们就可以在Controller中 用debug_backtrace 打印出调用栈来分析,比我们一行一行的找代码快很多。
* 用get_included_files看加载了哪些文件
很多开源程序都有一些共通之处,比如一般都有配置文件,数据库DB类等。 如果一个刚拿到一个陌生的开源程序,我们想快速找到它的配置文件,可以用get_included_files 显示出程序加载了哪些文件,然后根据文件名可以快速找到配置文件的位置,配置文件的文件名一般都叫“config”。 比如我们打印出了ThinkPHP5加载的所有文件,从这个文件列表中我们能开始发现ThinkPHP5的项目配置文件地址应该为`/thinkphp5/application/config.php`
```
array(20) {
[0]=>
string(20) "/thinkphp5/index.php"
[1]=>
string(29) "/thinkphp5/thinkphp/start.php"
[2]=>
string(28) "/thinkphp5/thinkphp/base.php"
[3]=>
string(44) "/thinkphp5/thinkphp/library/think/loader.php"
[4]=>
string(43) "/thinkphp5/thinkphp/library/think/error.php"
[5]=>
string(35) "/thinkphp5/thinkphp/mode/common.php"
[6]=>
string(44) "/thinkphp5/thinkphp/library/think/config.php"
[7]=>
string(34) "/thinkphp5/thinkphp/convention.php"
[8]=>
string(41) "/thinkphp5/thinkphp/library/think/app.php"
[9]=>
string(33) "/thinkphp5/application/config.php"
[10]=>
string(35) "/thinkphp5/application/database.php"
[11]=>
string(32) "/thinkphp5/application/route.php"
[12]=>
string(41) "/thinkphp5/thinkphp/library/think/log.php"
[13]=>
string(53) "/thinkphp5/thinkphp/library/think/log/driver/file.php"
[14]=>
string(43) "/thinkphp5/thinkphp/library/think/cache.php"
[15]=>
string(55) "/thinkphp5/thinkphp/library/think/cache/driver/file.php"
[16]=>
string(42) "/thinkphp5/thinkphp/library/think/lang.php"
[17]=>
string(45) "/thinkphp5/thinkphp/library/think/session.php"
[18]=>
string(49) "/thinkphp5/application/index/controller/index.php"
[19]=>
string(46) "/thinkphp5/thinkphp/library/think/response.php"
}
```
* 变量的输出方法
输出变量时,echo函数会有一些问题, echo调试时如果变量是一个空字符串,看不见输出的内容,经常会误以为是程序没有执行到调试的地方。还有,用echo如果是要输出的变量是对象或数组只会打印出变量的类型,不知道变量的内部结构。
用var_dump调试不会有这些问题, 如果是输出空字符串,var_dump也会有显示:`string(0) ""` ,不会让人误以为程序没有执行。出数组或对象的时候,var_dump也能输出对象的内部结构。所以建议大家用var_dump调试而不用echo。
有时候不能直接输出调试信息,比如在线上环境调试时,如果输出调试信息,正式使用产品的用户也能看见了。这时候你可以会把调试信息写到日志文件中。
写日志文件时不要用覆盖的方式,程序执行了很多次但最能看见最后一次结果,追加的方式能看见每一次的执行结果。PHP设置文件写文件的方式:file_put_contents 设置第三个参数为FILE_APPEND。
如果写入文件是一个对象或数组,我们要用var_export,将变量导出再写入日志文件,否则无法看见变量的内部结构。 下面代码演示如何将一个数组以追加的方式写入文件。
```
$arr=[1,2,3,4];
file_put_contents('/tmp/log.txt',var_export($arr,true),true);
```
##SocketLog
像上面说的不能直接用var_dump输出调试信息的情况,以前需要写日志文件来调试,有了SocketLog比用日志文件更方便。它可以把调试信息实时的打印到浏览器控制台,可以打印字符串、对象、数组等各种变量类型,可以灵活定义打印字符串的样式,可以打印调用栈,还方便分析开源程序,有助于我们二次开发开源产品。
github地址:http://github.com/SocketLog ,我们按官方文档安装好SocketLog,然后运行官方的例子可以看见简单的效果。
示例代码:
```
slog('msg','log'); //一般日志
slog('msg','error'); //错误日志
slog('msg','info'); //信息日志
slog('msg','warn'); //警告日志
slog('msg','trace');// 输入日志同时会打出调用栈
slog('msg','alert');//将日志以alert方式弹出
slog('msg','log','color:red;font-size:20px;');//自定义日志的样式,第三个参数为css样式
```
用浏览器查看的效果如下:
![](https://box.kancloud.cn/2016-01-12_5695156177738.png)
图1-16 SocketLog打印日志的效果
如图1-16,我们并没有把调试信息打印到网站的正文,而是打印到了chrome浏览器的控制台中,还可以输出不同样式的日志。需要打开chrome浏览器的控制器才能看见日志,window下可以按F12打开, Mac下同时按下“⌘+alt+i” 可以打开控制台。
SocketLog调试的原理是什么呢? 如图1-17
![](https://box.kancloud.cn/2016-01-12_569515618854c.png)
图1-17 SocketLog运行原理
当PHP程序无法直接把调试信息输出到浏览器时,我们借助了WebSocket ,搭建一个WebSocket服务,PHP将日志传送给WebSocket, WebSocket再将日志发送给chrome浏览器。所以要使用SocketLog,需要启动websocket服务器同时浏览器需要安装一个接收日志的插件。
我们还可以把程序执行的所有SQL语句打印出来,从而有助于我们分析开源程序,我以OneThink的程序为例为大家做说明
![](https://box.kancloud.cn/2016-01-12_569515619ecaf.png)
图1-18 用SocketLog分析OneThink程序
如图1-18,我们用SocketLog打出OneThink的SQL语句后,当我们访问每个页面,都知道了这个页面执行了哪些SQL语句,并且点开每条SQL语句能显示出执行SQL语句的调用栈,这很方便我们找到自己想要的代码。 假设我们在做OneThink的二次开发,想在自己新增的程序里面也读取OneThink的文章,读取文章这种操作肯定OneThink已经封成函数了,我们如何能快速找到这个函数?如上图所示,只需要访问一下文章详情页,然后看哪条SQL语句像是在读取文章。```SELECT `id`,`parse`,`content`,`template`,`bookmark` FROM `onethink_document_article` WHERE ( `id` = 1 ) LIMIT 1 ,``` 这条SQL很像是在读取文章,我们点开这条SQL语句的调用栈,很快就会发现 DocummentModel::detail方法就是我们想找的代码, 这比一行一行的去找代码快多了。
很多开源程序都做了对数据库操作的封装,一般叫着Db类,程序对数据库进行操作都要调用这个类,我们只要找到这个Db类,加上SocketLog调试,将SQL打印出来,就能到达上图显示效果。以OneThink为了,需要修改ThinkPHP/Library/Think/Db.class.php文件的debug方法加上代码 `slog($this->queryStr,$this->_linkID);` debug方法是每次数据库操作都会执行的方法, $this->queryStr 就是这次数据库操作的SQL语句, slog的第二个参数$this->_linkID传递的是数据库对象,当第二个参数为数据库对象是, SocketLog会对SQL语言性能进行分析并打出调用栈。
SocketLog是我以前发现调试API十分麻烦时所开发的工具,程序员本来就是有创造性的,不要忍受自己觉得麻烦的地方,你可以自己开发工具解决问题,你可以做到自动化。SocketLog为我们团队带来很多便利,我们团队现在如果没有SocketLog都快感觉不能工作了。
SocketLog还能做微信调试,如图1-19
![](https://box.kancloud.cn/2016-01-12_56951561c80b5.png)
图1-19 用SoketLog调试微信
我们在开发微信公众号的时候,接口出错时微信上面只会提示“该公众号暂时无法提问服务,请稍候再试”, 然后并不知道出错原因,这给开发带来很多麻烦,而我们将微信API配上SocketLog后,可以把调试信息和程序报错打印到浏览器的控制台上。如上图所示,我们很快知道API出错是因为程序报错`Call to undefined function...` 调用了一个不存在的函数。
要做微信调试, SocketLog需要设置force_client_id 这个配置项,从而将调试信息打到指定的浏览器,具体如何使用大家可再参考一下官方文档。
##整理思维
再做开源程序分析时,会看很大量的代码,如果光是靠脑力记,会很累,越记越乱。如何把脑袋里烦乱的思绪理顺? 这时候可以用思维导图工具xmind,如图1-20是用Xmind整理的ThinkPHP5的执行流程。
![](https://box.kancloud.cn/2016-01-12_569515622ef9d.png)
图1-20 用Xmind整理ThinkPHP执行流程
Xmind的每一个分支都是可以拖动的, 我们先把杂乱内容统统列到xmind上, 然后再拖动进行归类,从而理清思绪。
另外,在整理程序类和类之间关系时,可以画UML图形(如图1-21)
![](https://box.kancloud.cn/2016-01-12_569515625e64d.png)
图1-21 用UML图形分析类和类的关系
UML图形能表示出类与类是继承、组合还是聚合等关系,可以使用staruml等软件要画UML图形,如果你之前对UML不了解,建议再网上找更多相关的资料来学习一下。