张墨轩的技术宅

不忘初心,方得始终

浅析微信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文件相关知识


Android so 调试的几种途径

一:直接使用gdb进行调试,最原始。
二:用ndk-gdb工具进行调试。
三:在Eclipse中add native support后图形界面调试。
四:在Eclipse中利用Debug configurations=>c/c++ Remote Application进行调试(本质就是gdb调试,这时Eclipse作为gdb前端的一个图形外壳而已)
五:  利用IDA的远程调试功能
其中要考虑二种情况:
一:有源码,比如调试自己开发的程序。
二:无源码,比如逆向别人开发的程序。


关于ADB的一些有趣认知

当你有一台PC机假设是Windows系统,和一台android手机,你想让两者之间进行交流该怎么办呢?我们首先想到的肯定是通过wifi将二个设备连接到同一个局域网内,或者直接wifi-direct,但是可能的情况是环境里没有wifi,或者PC机压根就没有网卡等等情况,导致wifi连接并不可行。所以android手机和PC机默认连接方式是通过USB线缆将二者连接在一起,通过USB进行通讯。
OK,说到这里既然物理的通讯链路已经解决了,那么站在软件的角度来说是个什么样子呢?
ADB由两个物理文件组成:
     1. adb或adb.exe,运行于PC端,包括Linux、Windows、Mac OS等系统之中,通常是x86架构上。
     2. adbd,运行于Android设备的底层Linux之中,ARMv5架构上。
从运行实体角度来看adb主要分为三部分:
     1. adb client  (在adb.exe中实现,运行在PC机上)
     2. adb server (在adb.exe中实现,运行在PC机上)
     3. adbd (在adbd中实现,是一个linux deamon程序,它实现了adb service的功能,运行在android手机上)
交互流程:
      adb client  <==> adb server <==> adbd 
      [PC端]                 [PC端]                [android手机端]
下面为了行文方便,PC端统称为adb, 手机端称为adbd.
PC机上的adb(打开PC机的usb接口)与android上的adbd(打开手机的usb接口)进行通讯,从而达到PC机控制手机的目的。
我们来看看adb的功能,比较常用的功能如下:
第一:交互式shell。可以在PC机上登录到android手机(本质是linux),对android手机进行控制,就像通过ssh远程登录到linux下进行操作一样。其实原理上就是PC上的adb请求android手机上的adbd执行命令,然后把命令的执行结果返回给PC机上的adb,当然中间的通讯数据肯定是通过USB线缆进行传输的。
第二:文件传输。可以将PC上的文件push到手机上,也可以将手机上的文件pull到PC上。当然文件传输数据包同样是走的USB线缆。
第三:安装APK。原理上就是PC机首先把APK文件push到手机上,然后PC发<安装APK>的shell命令给adbd,说穿了还是adb与adbd通讯,通过USB线缆。
第四:调试辅助功能。这个功能是最让新手迷糊的一个功能,也是相对比较难理解的部分,所以要重点说说。adb是android debug bridge的缩写,证明这个程序本身就是以调试为重点的,既然是bridge,那就应当充当一个桥接的作用,事实上adb确实是起到了桥接的作用,接下来我们就来讨论一下。我们知道android上主要由两大类程序构成,JAVA程序和原生C/C++程序。下面分别讨论:

1. JAVA程序调试,了解JAVA调试体系的人都知道 JPDA(Java Platform Debugger Architecture),它是 Java 平台调试体系结构的缩写,通过 JPDA 提供的 API,开发人员可以方便灵活的搭建 Java 调试应用程序。 JPDA 主要由三个部分组成:Java 虚拟机工具接口(JVMTI),Java 调试线协议(JDWP),以及 Java 调试接口(JDI)。
其中JDI由调试前端(front-end)实现,比如jdb工具,比如eclipseIDE,它的两个插件org.eclipse.jdt.debug.ui和org.eclipse.jdt.debug与其强大的调试功能密切相关,其中前者是eclipse调试工具界面的实现,而后者则是JDI的一个完整实现。JVMTI由后端(back-end)实现,比如android中的Davlik虚拟机就实现了JVMTI。 说穿了调试过程就是运行在PC上的前端(如jdb工具,eclipseIDE等) 与 运行在android手机上的后端(Davlik虚拟机)之间通过网络进行通信,通信用的协议就是JDWP。交互如下:

    jdb,eclipseIDE等 <==网络通信,用的JDWP协议==>  Davlik虚拟机 <==调试==> JAVA程序
   [PC机中]                                           [android手机中] 
从运行角度讲就是PC机中的jdb,eclipseIDE等进程与android手机中执行JAVA程序的Davlik虚拟机进程之间通过网络socket进行通信,其中Davlik虚拟机进程会bind并且listen某个端口作为调试通信用,而jdb,eclipseIDE等进程会去connect这个端口。

2.  原生C/C++程序,像有些APK中的so文件,android中的各种deamon进程都是属于这种,调试原生C/C++程序需要用到GDB,同样分为前端和后端,比如NDK中包含的
\toolchains\arm-linux-androideabi-4.6\prebuilt\windows\bin\arm-linux-androideabi-gdb.exe 就是一个前端 运行在PC机上,
而\prebuilt\android-arm\gdbserver\gdbserver就是一个后端,运行在android手机上,它是真正的调试器,用于调试原生C/C++程序。

运行在PC机上的arm-linux-androideabi-gdb.exe 与 运行在android手机上gdbserver 之间通过网络socket进行通信,其中gdbserver进程会bind并且listen某个端口
作为调试通信用,arm-linux-androideabi-gdb.exe进程会去connect这个端口。交互如下:

    arm-linux-androideabi-gdb.exe <==网络通信==>  gdbserver  <==调试==> 原生C/C++程序
   [PC机中]                                     [android手机中]        [android手机中] 

根据上面对JAVA程序调试和原生C/C++程序调试的分析我们可以得知,不管是哪种程序的调试,说的直白些,本质上就是 运行在PC中的A进程 与 运行在android手机中的B进程 之间的网络socket通信的过程。 既然本质是网络socket通讯,那问题就来了,前面已经说过PC和手机之间是通过USB线缆连接的,那怎么样才能进行网络socket通讯呢?ADB使用了一个巧妙的方法用USB模拟了网络通讯。

比如命令:
adb forward tcp:6100 tcp:7100 // PC上所有6100端口通信数据将被重定向到手机端7100端口server上

这个时候adb server会bind和listen 6100这个端口,PC上的其它应用程序,比如A程序,可以connect localhost:6100 ,adb server就会将这个连接请求通过USB线缆传给手机端
的adbd,adbd就会去connect localhost:7100,手机端的listen 7100端口的 B程序就会接到连接请求。交互如下:
A程序 ==连接本机6100==> adb server ==USB线缆==> adbd ==连接本机7100==> B程序
[--------------PC机中---------------]    [----------android手机中----------]
看上去就好像 A程序 直接与 B程序 通讯一样。
这样adb就完成了bridge的功能,通过USB线缆将原本不能进行socket连接的PC与手机,巧妙的桥接到了一起。
这种方法还有个特点:就是多条连接可以复用一条链路
比如android手机中 有三个程序 B1,B2,B3 分别listen 8001 8002 8003
那么我们可以
adb forward tcp:7001 tcp:8001 
adb forward tcp:7002 tcp:8002 
adb forward tcp:7003 tcp:8003
然后 PC机器中 假设有三个程序A1,A2,A3:
程序A1=>connect localhost:7001=>adb server=USB=>adbd=>connect localhost:8001=>B1
程序A2=>connect localhost:7002=>adb server=USB=>adbd=>connect localhost:8002=>B2
程序A3=>connect localhost:7003=>adb server=USB=>adbd=>connect localhost:8003=>B3
虽然实际有三条链路,但是却是共享了一条USB链路进行通信。
文章看到这里,估计就有很多小伙伴会在想,既然一切都是为了网络socket通信,那么如果PC机和手机已经连接到了同一个局域网内,双方之间完全可以直接进行网络socket通信了,是不是就不用那么麻烦了,是不是adb都可以不要了。从某种意义上来说,在这样的情况下 确实可以不用adb了,交互式shell可以直接在手机上装一个ssh server,然后PC上直接ssh,windows下还可以用SecureCRT等工具,文件传输可以直接SCP,调试直接可以用jdb,gdb和手机进行连接调试。事实上互联网上这方面的资料也有很多。但是你如果直接用eclipseIDE的话那还是需要adb,因为eclipseIDE内部会使用adb的功能。既然双方之间已经可以连通,这个时候adb如果再用USB线缆的话感觉有点多此一举,能不能不用USB线缆呢?答案是肯定的。adb支持用wifi连接代替USB连接。首先打开手机的wifi设置,使其连接到网络。然后,需要在手机上对adb连接端口进行设
置,这里需要有root权限的终端(terminal)应用,这种类型的应用在各个Market都有不少,选择一个适合的就可以了。
然后,在手机中打开这个终端(terminal)应用,比如输入如下命令:
        su
        setprop service.adb.tcp.port 5555
        stop adbd
        start adbd
接着,可以查看一下你的手机的IP地址,最后,在你的PC上,进入到<Android-SDK>/platforms目录,运行如下命令:
adb connect 手机的IP地址:5555 (例如:adb connect 192.168.1.115:5555)
如果前面的配置正确无误,则可以得到:connected to 192.168.1.115:5555的输出。
这样,就可以使用adb来对手机进行调试了,此时也可以在Eclipse的设备列表中看到已经连接的手机设备了。

从进程的角度来说 这个就是 运行在PC端的adb.exe进程和运行在手机端的adbd进程之间通过wifi建立的一条socket链路而已。但是站在adb的角度来说,我们要把这条socket链路看成是一条虚拟的USB线缆。要站在更高的高度来理解,这条socket链路就是一根USB线缆:)。 不管adb和adbd之间是通过何种模式通信,他们之间的行为都是一致的,不论是USB线缆链接,还是wifi socket链接。
最后,我们来看看android模拟器,在没有真实手机的情况下可以用android模拟器来调试程序,那么android模拟器和真实的android手机之间在连接方式上有什么区别呢?
android模拟器是qemu-base的,它运行起来后会绑定二个端口,绑定端口会从TCP:5554端口开始,比如机器上运行了2个模拟器,那么先运行的模拟器将绑定5554,5555二个端口,第二个运行的模拟器将绑定5556,5557二个端口,假如有第三个,第四个模拟器要运行就以此类推。其中奇数类的端口就是用于adb连接的。比如PC机上的ADB会去连接第一个模拟器进程的5555的端口,从而建立连接。同理,站在adb的角度来说,我们要把这条socket链路看成是一条虚拟的USB线缆。要站在更高的高度来理解,这条socket链路就是一根USB线缆:)。 
这里顺带讲讲偶数类的端口,比如模拟器绑定的5554端口,这个端口是用于控制台的,ddms会通过这个端口连接上模拟器,这样程序员可以通过ddms动态的设置模拟器中的电话状态,比如模拟一个电话呼入,可以设置GPS的数值,从而模拟手机在移动等等。


软件生产流程

一. 代码的编写(操作系统平台,工具), HOST
二. 代码的编译(操作系统平台,工具), HOST
三. 程序的部署(工具,方法), HOST==>TARGET
四. 调试运行(操作系统平台,工具), TARGET
五. 软件的签名,发布,推广(更偏向业务,而不是纯粹技术)
典型的开发类型:
一. exe (pc机windows系统上跑)
二. app (苹果手机IOS系统上跑), 分为标准开发(没有越狱)和越狱开发(可以使用各种奇淫异技)二种类型,开发语言包括Objective-C,SWIFT,C/C++
三. apk (android手机android系统上跑) ,分为标准开发(没有root)和root开发(可以使用各种奇淫异技)二种类型, 开发语言包括JAVA,C/C++
四. arm-linux操作系统移植 (arm硬件板子上跑)


ARM学习的一些经验

我将ARM知识分成四个层次:
一:ARM架构
    先上一个简表:
架构处理器家族
ARMv1ARM1
ARMv2ARM2ARM3
ARMv3ARM6, ARM7
ARMv4StrongARMARM7TDMIARM9TDMI
ARMv5ARM7EJARM9EARM10EXScale
ARMv6ARM11ARM Cortex-M
ARMv7ARM Cortex-AARM Cortex-MARM Cortex-R
ARMv8Cortex-A50[9]
可见每一种架构都有若干种具体的实现,对ARM架构的学习主要是学习ARM指令,内存模型,中断模型等架构相关的知识,这个时候并没有牵扯到外设或者外设通讯协议啥的.
二:ARM内核
    比如ARM9系列(基于ARMv4,ARMv5架构). ARM内核是基于某个ARM架构版本下的具体实现,主要区别在于性能还有是否支持相关特性如MMU,Jazelle,SIMD,Thumb-2,VFP
等等.
三:基于ARM核的SOC或者MCU
     一般来说把面向高端的,面向应用处理的叫做SOC,把面向低端的,主要面向单片机市场的叫做MCU,下面为了行文方便 统一用SOC来代替.
比如三星的S3C2410就是一块包含了ARM920T内核的SOC,框图如下:

它的外形看起来就是一块芯片,和一块CPU长的差不多,所以很多人习惯叫它CPU,其实从严格意义上来说叫它CPU并不准确,它还包括了各种外设控制芯片功能,比如USB,SPI,IIS,LCD,FLASH,DMA,UART等控制器,你可以将它理解为 将CPU还有各种外设控制芯片全部做到了这块小小的芯片上,所以它并不只是一个CPU而已,而是一套片上系统,所以我们称呼这块芯片叫做SOC(片上系统)更加恰当. 对于SOC的学习,一般是首先要选定一块特定的产品比如S3C2410进行学习,而仅仅学习ARM内核知识是远远不够的,还必须要学习各种外设控制的方法和与各种外设通讯的协议等等.

四.基于SOC的板上系统.
  这个就是我们常常见到的电路板了,比如将各种外设如RAM,FLASH,LCD等,加上SOC如S3C2410,按设计焊接到一块PCB板上,调试好电路并且刷好程序就可以开始工作了.
而事实上学习的目的最终也是想设计并制作出这样一块电路板而已.

总结:
比如当你拆开一台手机你至少会看到有块PCB主板,这块主板上面肯定有各种芯片和元器件,其中肯定有一块是SOC(假设是S3C2410),那么这块SOC里面肯包含了ARM920T的内核,而这内核又属于ARMv4T架构

关系如下:
PCB主板==包含==>SOC==包含==>ARM内核==属于==>ARM架构

不同的ARM书籍侧重的层次都不尽相同,所以事先一定要了解清楚到底是想学什么.


«1234567»

Powered By Z-Blog 2.2 Prism Build 140101

Copyright phonegap.me Rights Reserved.