SegmentFault 人云思云最新的文章
2015-10-29T14:20:32+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
Mac软件之iTerm2
https://segmentfault.com/a/1190000003925096
2015-10-29T14:20:32+08:00
2015-10-29T14:20:32+08:00
人云思云
https://segmentfault.com/u/rysy
4
<blockquote><p>iterm2是一个更好替代Mac自带终端与iterm的软件</p></blockquote>
<p><a href="https://link.segmentfault.com/?enc=QikbCfXCvyBiLgv%2B%2F26Deg%3D%3D.WdRu%2BKEUuwkVtddmVldW1E0mDnhEc3w9gFr7lCTw2%2FIdwhOfz%2F%2BVP8JgCs5NpaD6" rel="nofollow">iterm2官方文档</a></p>
<h2>基本</h2>
<h3>1. 快捷键汇总</h3>
<table>
<thead><tr>
<th align="left">快捷键</th>
<th align="left">作用</th>
</tr></thead>
<tbody>
<tr>
<td align="left">⌘ + n</td>
<td align="left">新建窗口</td>
</tr>
<tr>
<td align="left">⌘ + t</td>
<td align="left">新建标签页</td>
</tr>
<tr>
<td align="left">⌘ + w</td>
<td align="left">关闭当前标签页</td>
</tr>
<tr>
<td align="left">⌘ + enter</td>
<td align="left">进入与返回全屏模式</td>
</tr>
<tr>
<td align="left">⌘ + r</td>
<td align="left">换到新一屏,其实是滚到新的一屏,并没有清空</td>
</tr>
<tr>
<td align="left">⌘ + d</td>
<td align="left">横着分屏</td>
</tr>
<tr>
<td align="left">⌘ + shift + d</td>
<td align="left">竖着分屏</td>
</tr>
<tr>
<td align="left">⌘ + 方向键</td>
<td align="left">切换标签页</td>
</tr>
<tr>
<td align="left">⌘ + opt + 方向键</td>
<td align="left">切换到指定位置的分屏</td>
</tr>
<tr>
<td align="left">⌘ + ]和⌘ + [</td>
<td align="left">在最近使用的分屏直接切换</td>
</tr>
<tr>
<td align="left">⌘ + ;</td>
<td align="left">历史命令自动完成</td>
</tr>
<tr>
<td align="left">⌘ + shift + h</td>
<td align="left">会列出剪切板历史</td>
</tr>
<tr>
<td align="left">⌘ + 数字</td>
<td align="left">查找 ,所查找的内容会被自动复制 ,输入查找的部分字符,tab后匹配,shift+tab前匹配</td>
</tr>
<tr>
<td align="left">⌘ + /</td>
<td align="left">找到当前光标位置,有时会很有用</td>
</tr>
<tr>
<td align="left">shift + ⌘ + s</td>
<td align="left">保存当前窗口快照</td>
</tr>
<tr>
<td align="left">⌘ + opt + b</td>
<td align="left">快照回放</td>
</tr>
<tr>
<td align="left">⌘ + opt + e</td>
<td align="left">打开标签Exposé,并支持搜索</td>
</tr>
<tr>
<td align="left">⌘ + —/+/0</td>
<td align="left">调整字体大小</td>
</tr>
</tbody>
</table>
<h3>2. 光标、文本快键键汇总</h3>
<ul>
<li><p><strong>ctrl + u</strong>: 清空当前行</p></li>
<li><p><strong>ctrl + a</strong>: 到行首</p></li>
<li><p><strong>ctrl + e</strong>: 行末</p></li>
<li><p><strong>ctrl + f/b</strong>: 前进后退,相当于左右方向键</p></li>
<li><p><strong>ctrl + p:</strong> 上一条命令,相当于方向键上</p></li>
<li><p><strong>ctrl + r</strong>: 搜索命令历史</p></li>
<li><p><strong>ctrl + d</strong>: 删除当前字符</p></li>
<li><p><strong>ctrl + h</strong>: 删除之前的字符</p></li>
<li><p><strong>ctrl + w</strong>: 删除光标前的单词</p></li>
<li><p><strong>ctrl + k</strong>: 删除到文本末尾</p></li>
<li><p><strong>ctrl + t</strong>: 交换光标处文本</p></li>
</ul>
<h3>3. 功能详细</h3>
<h4>Exposé 标签</h4>
<p>按<code>⌘ + opt + e</code> 打开标签Exposé,并支持搜索。如图:</p>
<p><img src="/img/bVqDfD" alt="clipboard.png" title="clipboard.png"></p>
<h4>快照保存和回放</h4>
<p><code>shift + ⌘ + s:</code> 保存当前窗口快照。<br><code>⌘ + opt + b</code>: 快照回放。<br>你可以对你的操作根据时间轴进行回放。可以拖动下方的时间轴,也可以按左右方向键。如图:快照回放</p>
<p><img src="/img/bVqDfH" alt="clipboard.png" title="clipboard.png"></p>
<h4>选中即复制</h4>
<p>iterm2有2种好用的选中即复制模式。</p>
<ul>
<li><p>一种是用鼠标,在iterm2中,选中某个路径或者某个词汇,那么,iterm2就自动复制了。</p></li>
<li><p>另一种是无鼠标模式,command+f,弹出iterm2的查找模式,输入要查找并复制的内容的前几个字母,确认找到的是自己的内容之后,输入tab,查找窗口将自动变化内容,并将其复制。如果输入的是shift+tab,则自动将查找内容的左边选中并复制。</p></li>
</ul>
<p><img src="/img/bVqDfJ" alt="clipboard.png" title="clipboard.png"></p>
<h4>屏幕切割</h4>
<ul>
<li><p>command+d:垂直分割;</p></li>
<li><p>command+shift+d:水平分割</p></li>
</ul>
<p><img src="/img/bVqDfM" alt="clipboard.png" title="clipboard.png"></p>
<h4>系统热键</h4>
<p>如下图,设置好系统热线之后,将在正常的浏览器或者编辑器等窗口的上面,以半透明窗口形式直接调出iterm2 shell。</p>
<p><img src="/img/bVqDfO" alt="clipboard.png" title="clipboard.png"></p>
<p>按下同样的系统热键之后,将自动隐藏。这样非常有利于随时随地处理。</p>
<h4>剪切历史</h4>
<p>输入command+shift+h,iterm2将自动列出剪切板的历史记录。如果需要将剪切板的历史记录保存到磁盘,在Preferences > General > Save copy/paste history to disk.中设置。</p>
<p><img src="/img/bVqDfQ" alt="clipboard.png" title="clipboard.png"></p>
<h2>iTerm2自动登录远程服务器</h2>
<p>在iTerm2下ssh不能自动登录,不自动登录每次输入命令太麻烦了。这里介绍一个采取expect脚本的方式实现iTerm2下ssh自动登录。</p>
<h4>1. 新建一个expect脚本 login.exp</h4>
<pre><code>#!/usr/bin/expect
if { [llength $argv] < 4 } {
puts "Usage: $argv0 ip port user passwd"
exit 1
}
set ip [lindex $argv 0]
set port [lindex $argv 1]
set user [lindex $argv 2]
set passwd [lindex $argv 3]
set timeout 30
spawn ssh -q -l$user -p$port $ip
expect {
"assword:" {
send "$passwd\r"
}
"FATAL" {
puts "\nCONNECT ERROR: $ip occur FATAL ERROR!!!\n"
exit 1
}
"No route to host" {
puts "\nCONNECT ERROR: $ip No route to host!!!\n"
exit 1
}
}
puts "\n--> Connected: $ip, pls enjoy yourself!\n"
interact</code></pre>
<p>该脚本需要四个参数,</p>
<ol>
<li><p>远程地址</p></li>
<li><p>远程端口</p></li>
<li><p>远程用户名</p></li>
<li><p>用户密码</p></li>
</ol>
<h4>2. 将expect脚本copy到$PATH下(例如/usr/local/bin)</h4>
<pre><code>cp login.exp /usr/local/bin/login.exp</code></pre>
<p>或者自己拷贝过去。</p>
<h4>3. 在iterm2中设置登录脚本,用command+o的方式呼出profiles,点击Edit Profiles</h4>
<p><img src="/img/bVqDfX" alt="clipboard.png" title="clipboard.png"></p>
<p>接着新建一个Profile,这里以我的webserver为例</p>
<p><img src="/img/bVqDf3" alt="clipboard.png" title="clipboard.png"></p>
<p>在红色涂抹的部分分别制定设置脚本、以及上面的四个参数。如</p>
<blockquote><p>login.exp 地址 端口 用户名 密码</p></blockquote>
<p>之间用空格分离。</p>
<h4>4.使用</h4>
<p>配好后,只要command+o的方式呼出profiles,双击需要打开的Profile。</p>
<h3>参考</h3>
<p><a href="https://link.segmentfault.com/?enc=wAkgBQJTW%2Fk8Px060MevVw%3D%3D.JHA8dFyHD6ICL8dV9Q%2BgxCZMXs%2FFsburpv733MK6cFhHYpPcJvt1nk%2BwgCuF1gM%2B" rel="nofollow">iTerm2新手应知特色功能</a><br><a href="https://link.segmentfault.com/?enc=FjT5hZWghP%2Fvj8tSO0ZsPw%3D%3D.yfoFOFPWQhD3vOP%2FKMsXAGZh6M3W7AGnvuzVo7avhO%2Fy%2F1jsh9gwgCmBLkAHujVE" rel="nofollow">打造好用的终端 —— iTerm2 别样设置</a></p>
JavaScript中的立即执行函数
https://segmentfault.com/a/1190000003902899
2015-10-24T22:19:17+08:00
2015-10-24T22:19:17+08:00
人云思云
https://segmentfault.com/u/rysy
27
<p>注:此文只在理解立即执行函数,不在所谓原创,文中大量引用<a href="https://link.segmentfault.com/?enc=i0fs8Npu2ETgo9WdcDGsFg%3D%3D.y78wnmmaOev6RhNW900lz3OsY5LSlYtKl2jPLqmosBdqJ9sEgIcIXm%2BZTUnRdTXvOQHxPNxZvRhrV7OscyLzxg%3D%3D" rel="nofollow">阮一峰的JavaScript标准参考教程</a>、<a href="https://link.segmentfault.com/?enc=hRcOHf700fTxb04ZVc11rA%3D%3D.IDp2CZDj767ewTsledInD3rOWe%2Fqb23oAEK96X1TyAckn38DcrDUdjeMHsfA8RmANRgFeyLOQPQWr7jPsx1HLXrUSiCxUmN4V0oUG1PAY%2B4%3D" rel="nofollow">MDN的JavaScript 参考文档</a>和<a href="https://link.segmentfault.com/?enc=c3kmSNZBniwLr9%2Fu9Eo8fQ%3D%3D.HeSaVa%2Fceuo6TcRzkiaI4h%2ByOksKPCBzcRDUG1yjzJTUmT54GTyDhqhW9iizo7NZbFi9KDg8I7bVygZIH4zMCQ%3D%3D" rel="nofollow">深入理解JavaScript系列(4):立即调用的函数表达式</a>的内容。</p>
<h2>描述</h2>
<p>立即执行函数通常有下面两种写法:</p>
<pre><code>(function(){
...
})();</code></pre>
<pre><code>(function(){
...
}());</code></pre>
<p>在Javascript中,一对圆括号“()”是一种运算符,跟在函数名之后,表示调用该函数。比如,print()就表示调用print函数。</p>
<p>这个写法和我们想象的写法不一样(知道的人当然已经习以为常)<br>很多人刚开始理解立即执行函数的时候,觉得应该是这样的:</p>
<pre><code>function (){ ... }();
//或者
function fName(){ ... }();</code></pre>
<p>然而事实却是这样:<code>SyntaxError: Unexpected token (</code>。这是为什么呢?</p>
<h2>解释</h2>
<p>要理解立即执行函数,需要先理解一些函数的基本概念:<code>函数声明</code>、<code>函数表达式</code>,因为我们定义一个函数通常都是通过这两种方式</p>
<p><strong>函数声明 (function 语句)</strong></p>
<pre><code>function name([param[, param[, ... param]]]) {
statements
}</code></pre>
<p>name:函数名;<br>param:被传入函数的参数的名称,一个函数最多可以有255个参数;<br>statements:这些语句组成了函数的函数体。</p>
<p><strong>函数表达式 (function expression)</strong></p>
<p>函数表达式和函数声明非常类似,它们甚至有相同的语法。</p>
<pre><code>function [name]([param] [, param] [..., param]) {
statements
}</code></pre>
<p>name:函数名,可以省略,<code>省略函数名的话,该函数就成为了匿名函数</code>;<br>param:被传入函数的参数的名称,一个函数最多可以有255个参数;<br>statements:这些语句组成了函数的函数体。</p>
<p>下面我们给出一些栗子说明:</p>
<pre><code>// 声明函数f1
function f1() {
console.log("f1");
}
// 通过()来调用此函数
f1();
//一个匿名函数的函数表达式,被赋值给变量f2:
var f2 = function() {
console.log("f2");
}
//通过()来调用此函数
f2();
//一个命名为f3的函数的函数表达式(这里的函数名可以随意命名,可以不必和变量f3重名),被赋值给变量f3:
var f3 = function f3() {
console.log("f2");
}
//通过()来调用此函数
f3();</code></pre>
<p><strong>上面所起的作用都差不多,但还是有一些差别</strong><br>1、函数名和函数的变量存在着差别。函数名不能被改变,但函数的变量却能够被再分配。函数名只能在函数体内使用。倘若在函数体外使用函数名将会导致错误:</p>
<pre><code>var y = function x() {};
alert(x); // throws an erro</code></pre>
<p>2、函数声明定义的函数可以在它被声明之前使用</p>
<pre><code>foo(); // alerts FOO!
function foo() {
alert('FOO!');
}</code></pre>
<p>但函数声明非常容易(经常是意外地)转换为函数表达式。当它不再是一个函数声明:</p>
<ul>
<li><p>成为表达式的一部分</p></li>
<li><p>不再是函数或者script自身的“源元素” (source element)。在script或者函数体内“源元素”并非是内嵌的语句(statement)</p></li>
</ul>
<pre><code>var x = 0; // source element
if (x == 0) { // source element
x = 10; // 非source element
function boo() {} // 非 source element
}
function foo() { // source element
var y = 20; // source element
function bar() {} // source element
while (y == 10) { // source element
function blah() {} // 非 source element
y++; //非source element
}
}</code></pre>
<p>Examples:</p>
<pre><code>// 函数声明
function foo() {}
// 函数表达式
(function bar() {})
// 函数表达式
x = function hello() {}
if (x) {
// 函数表达式
function world() {}
}
// 函数声明
function a() {
// 函数声明
function b() {}
if (0) {
//函数表达式
function c() {}
}
}</code></pre>
<p>现在我们来解释上面的<code>SyntaxError: Unexpected token (</code>:<br>产生这个错误的原因是,Javascript引擎看到function关键字之后,认为后面跟的是函数定义语句,不应该以圆括号结尾。<br>解决方法就是让引擎知道,圆括号前面的部分不是函数定义语句,而是一个表达式,可以对此进行运算。所以应该这样写:</p>
<pre><code>(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();</code></pre>
<p>这两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称IIFE。</p>
<pre><code>注意,上面的两种写法的结尾,都必须加上分号。</code></pre>
<p>推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面三种写法。</p>
<pre><code>var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();</code></pre>
<p>甚至像这样写:</p>
<pre><code>!function(){ /* code */ }();
~function(){ /* code */ }();
-function(){ /* code */ }();
+function(){ /* code */ }();</code></pre>
<p>new关键字也能达到这个效果:</p>
<pre><code>new function(){ /* code */ }
new function(){ /* code */ }() // 只有传递参数时,才需要最后那个圆括号。</code></pre>
<h2>使用</h2>
<p>那我们通常为什么使用函数立即表达式呢,以及我如何使用呢?</p>
<p>通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。<br>它的目的有两个:</p>
<ul>
<li><p>一是不必为函数命名,避免了污染全局变量;</p></li>
<li><p>二是IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。</p></li>
</ul>
<pre><code>// 写法一
var tmp = newData;
processData(tmp);
storeData(tmp);
// 写法二
(function (){
var tmp = newData;
processData(tmp);
storeData(tmp);
}());</code></pre>
<p>上面代码中,写法二比写法一更好,因为完全避免了污染全局变量。</p>
<p>最后在举一个真实的栗子:在JavaScript的OOP中,我们可以通过IIFE来实现一个单例(关于单例的优化不再此处讨论)</p>
<pre><code>// 创建一个立即调用的匿名函数表达式
// return一个变量,其中这个变量里包含你要暴露的东西
// 返回的这个变量将赋值给counter,而不是外面声明的function自身
var counter = (function () {
var i = 0;
return {
get: function () {
return i;
},
set: function (val) {
i = val;
},
increment: function () {
return ++i;
}
};
} ());
// counter是一个带有多个属性的对象,上面的代码对于属性的体现其实是方法
counter.get(); // 0
counter.set(3);
counter.increment(); // 4
counter.increment(); // 5
counter.i; // undefined 因为i不是返回对象的属性
i; // 引用错误: i 没有定义(因为i只存在于闭包)</code></pre>
LayoutInflater详解
https://segmentfault.com/a/1190000003813755
2015-10-02T00:53:39+08:00
2015-10-02T00:53:39+08:00
人云思云
https://segmentfault.com/u/rysy
2
<p>在日常开发中经常会用到通过资源id去获取view的场景,LayoutInflater这时非常有用。这与我们经常用的findViewById()不一样。</p>
<ul>
<li><p>LayoutInflater通常用于动态载入的界面,使用LayoutInflater的inflate方法动态接入layout文件;</p></li>
<li><p>findViewById通常用于在已经载入的界面,使用findViewById()方法来获得其中的界面元素。</p></li>
</ul>
<h2>一 LayoutInflater实例</h2>
<p><strong>获得LayoutInflater实例方式</strong></p>
<ol><li><p>通过系统服务获取布局加载器</p></li></ol>
<pre><code class="java">LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);</code></pre>
<ol><li><p>通过activity中的getLayoutInflater()方法</p></li></ol>
<pre><code class="java">LayoutInflater inflater = getLayoutInflater();</code></pre>
<ol><li><p>通过LayoutInflater的from静态方法</p></li></ol>
<pre><code>LayoutInflater inflater = LayoutInflater.from(this)</code></pre>
<p><code>这三种方式本质都是调用Context.getSystemService()</code>。</p>
<p><strong>getLayoutInflater()源码分析</strong><br>Activity 的 getLayoutInflater() 方法是调用 PhoneWindow 的getLayoutInflater()方法,看一下该源代码:</p>
<pre><code class="java">public PhoneWindow(Context context) {
super(context);
//可以看出它其实是调用 LayoutInflater.from(context)。
mLayoutInflater = LayoutInflater.from(context);
} </code></pre>
<p><strong>LayoutInflater.from(context)源码分析</strong></p>
<pre><code class="java">public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
} </code></pre>
<p>LayoutInflater.from(context)内部是调用context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)获得布局加载器.</p>
<h2>二 inflate方法</h2>
<p>既然已经获取到LayoutInflater实例,加载布局主要还是LayoutInflater的inflate方法。<br>LayoutInflater类提供的下面四种inflate方法:</p>
<ul>
<li><p>public View inflate (int resource, ViewGroup root)</p></li>
<li><p>public View inflate (int resource, ViewGroup root, boolean attachToRoot)</p></li>
<li><p>public View inflate (XmlPullParser parser, ViewGroup root)</p></li>
<li><p>public View inflate (XmlPullParser parser, ViewGroup root, boolean attachToRoot)</p></li>
</ul>
<p>我们先看下源码:</p>
<pre><code>public View inflate(int resource, ViewGroup root) {
//如果root不为null,attachToRoot为true,否则attachToRoot为false
return inflate(resource, root, root != null);
}
public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
if (DEBUG) System.out.println("INFLATING from resource: " + resource);
XmlResourceParser parser = getContext().getResources().getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}</code></pre>
<p>下图可以看到inflate的调用关系:</p>
<p><img src="/img/bVqaik" alt="图片描述" title="图片描述"></p>
<p>具体处理加载逻辑的是<em>inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)</em>方法,具体XmlPullParser的解析,不是我们讨论的主题。我们这里主要讨论的是上面两个方法的参数变化影响。因为我们通常是调用上面连个方法来加载布局。<br>说的更明白一点就是对 <code>ViewGroup root</code>, <code>boolean attachToRoot</code>这两个参数的讨论。传入ViewGroup root, boolean attachToRoot的值不同,会出现什么结果呢?</p>
<p>先初略解释下这两个参数:</p>
<ul>
<li><p>ViewGroup root:指实例的布局所要放入的根视图。</p></li>
<li><p>boolean attachToRoot:指是否附加到传入的根视图。</p></li>
</ul>
<p>继续看下inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)源码,然后在分析每种情况。</p>
<pre><code>// 这里加载我们设置的resource,临时标记为temp
final View temp = createViewFromTag(root, name, attrs, false);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// 如果root不是null,会根据resource最外面的layout创建layout params来匹配root(如果需要)
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 如果attachToRoot为false,为temp设置布局
temp.setLayoutParams(params);
}
......
//如果root != null && attachToRoot,把temp添加到root中,并设置params
if (root != null && attachToRoot) {
root.addView(temp, params);
}
//如果root == null || !attachToRoot,直接返回temp。
if (root == null || !attachToRoot) {
result = temp;
}
......
return result;</code></pre>
<p><strong>这里稍微分析代码:</strong></p>
<p>第1段代码:动态载入resource</p>
<pre><code>final View temp = createViewFromTag(root, name, attrs, false)</code></pre>
<p>第2段代码:创建ViewGroup的LayoutParams</p>
<pre><code>ViewGroup.LayoutParams params = null;
params = root.generateLayoutParams(attrs);</code></pre>
<p>第3段代码:当root不为空,attachToRoot为false时,为temp设置layout属性,当该view以后被添加到父view当中时,这些layout属性会自动生效</p>
<pre><code>if (root != null) {
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
// 如果attachToRoot为false,为temp设置布局
temp.setLayoutParams(params);
}</code></pre>
<p>第4段代码:</p>
<pre><code>// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}</code></pre>
<p>只有当<code>root != null && attachToRoot)</code>时,root会把temp添加到root中,给加载的布局文件的指定一个父布局,即root。否则temp不会添加到root,即<code>root == null || !attachToRoot</code>时,最后直接return temp。</p>
<p><strong>这里总结一下:</strong></p>
<pre><code>public View inflate(int resource, ViewGroup root, boolean attachToRoot)</code></pre>
<ol>
<li><p>如果root为null,attachToRoot为任何值都毫无意义,只会单纯的加载布局文件。</p></li>
<li><p>如果root不为null,attachToRoot设为true,root会把temp添加到root中,此时在temp布局文件中的根view的layout属性会生效。</p></li>
<li><p>如果root不为null,attachToRoot设为false,此时view并没有添加到root,但是view的layout属性被保存了下来,以后如果调用<em>addView(View child)</em>,layout属性会自动生效。可以见下面源码:ViewGroup</p></li>
</ol>
<pre><code>public void addView(View child, int index) {
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
//这里便是从child中获取LayoutParams
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
//这段代码是不是和刚才inflate中代码很相似,对,这里就是把view和layout属性添加到parent中。
addView(child, index, params);
}</code></pre>
<p><code>注1</code>:View类也提供了两个静态方法,作用一样</p>
<pre><code>View.inflate(int resource, ViewGroup root);
View.inflate(int resource, ViewGroup root, boolean attachToRoot);</code></pre>
<h2>三 Demo演示</h2>
<p>以后遇到inflate的布局文件的layout属性失效,我想大家应该知道什么原因了。</p>
<p>理论上应该写个demo演示的,其实demo已写好,就是不想截图了。这里就偷懒用ListView的item的布局举个例子,我想大家写Adapter应该很熟悉了。<br>item.xml</p>
<pre><code><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="60dip"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="test" />
</LinearLayout></code></pre>
<p>Adapter</p>
<pre><code>public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = inflate(R.layout.item, null);
}
return convertView;
}</code></pre>
<p>这样写,android:layout_height的60dip失效了,对不对。<br>换成下面:</p>
<pre><code>public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = inflate(R.layout.item, parent,false);
}
return convertView;
}</code></pre>
<p>是不是60dip生效了。</p>
<hr>
<p>算了再贴个Demo吧,图就不贴了。<br>activity xml</p>
<pre><code><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/root_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
</RelativeLayout></code></pre>
<p>inflate view</p>
<pre><code><Button xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="100dp"
android:layout_height="100dp"
android:orientation="vertical"
android:text="test"/></code></pre>
<p>activity:部分代码需要注释</p>
<pre><code>//情况1
View inflateView = getLayoutInflater().inflate(R.layout.inflate_test, null);
//情况2
View inflateView = getLayoutInflater().inflate(R.layout.inflate_test, null);
rootView.addView(inflateView);
//情况3
View inflateView = getLayoutInflater().inflate(R.layout.inflate_test, rootView,false);
//情况2
View inflateView = getLayoutInflater().inflate(R.layout.inflate_test, rootView,false);
rootView.addView(inflateView);
//情况4
View inflateView = getLayoutInflater().inflate(R.layout.inflate_test, rootView,true);</code></pre>
MYSQL-索引
https://segmentfault.com/a/1190000003072424
2015-08-10T17:33:38+08:00
2015-08-10T17:33:38+08:00
人云思云
https://segmentfault.com/u/rysy
76
<h2>概述</h2>
<p>用来加快查询的技术很多,其中最重要的是索引。通常索引能够快速提高查询速度。如果不适用索引,MYSQL必须从第一条记录开始然后读完整个表直到找出相关的行。表越大,花费的时间越多。但也不全是这样。本文讨论索引是什么以及如何使用索引来改善性能,以及索引可能降低性能的情况。</p>
<h2>索引的本质</h2>
<p>MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。提取句子主干,就可以得到索引的本质:索引是数据结构。</p>
<blockquote>
<p>数据库查询是数据库的最主要功能之一。我们都希望查询数据的速度能尽可能的快,因此数据库系统的设计者会从查询算法的角度进行优化。最基本的查询算法当然是顺序查找(linear search),这种复杂度为O(n)的算法在数据量很大时显然是糟糕的,好在计算机科学的发展提供了很多更优秀的查找算法,例如二分查找(binary search)、二叉树查找(binary tree search)等。如果稍微分析一下会发现,每种查找算法都只能应用于特定的数据结构之上,例如二分查找要求被检索数据有序,而二叉树查找只能应用于二叉查找树上,但是数据本身的组织结构不可能完全满足各种数据结构(例如,理论上不可能同时将两列都按顺序进行组织),所以,在数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法。这种数据结构,就是索引。</p>
</blockquote>
<h2>索引的存储分类</h2>
<p>索引是在MYSQL的存储引擎层中实现的,而不是在服务层实现的。所以每种存储引擎的索引都不一定完全相同,也不是所有的存储引擎都支持所有的索引类型。MYSQL目前提供了一下4种索引。</p>
<ul>
<li>B-Tree 索引:最常见的索引类型,大部分引擎都支持B树索引。</li>
<li>HASH 索引:只有Memory引擎支持,使用场景简单。</li>
<li>R-Tree 索引(空间索引):空间索引是MyISAM的一种特殊索引类型,主要用于地理空间数据类型。</li>
<li>Full-text (全文索引):全文索引也是MyISAM的一种特殊索引类型,主要用于全文索引,InnoDB从MYSQL5.6版本提供对全文索引的支持。</li>
</ul>
<blockquote>
<p>Mysql目前不支持函数索引,但是能对列的前面某一部分进行索引,例如标题title字段,可以只取title的前10个字符进行索引,这个特性可以大大缩小索引文件的大小,但前缀索引也有缺点,在排序Order By和分组Group By 操作的时候无法使用。用户在设计表结构的时候也可以对文本列根据此特性进行灵活设计。<br><strong>语法</strong>:create index idx_title on film (title(10))</p>
</blockquote>
<p><strong>MyISAM、InnoDB引擎、Memory三个常用引擎类型比较</strong></p>
<table>
<thead><tr>
<th>索引</th>
<th>MyISAM引擎</th>
<th>InnoDB引擎</th>
<th>Memory引擎</th>
</tr></thead>
<tbody>
<tr>
<td>B-Tree 索引</td>
<td>支持</td>
<td>支持</td>
<td>支持</td>
</tr>
<tr>
<td>HASH 索引</td>
<td>不支持</td>
<td>不支持</td>
<td>支持</td>
</tr>
<tr>
<td>R-Tree 索引</td>
<td>支持</td>
<td>不支持</td>
<td>不支持</td>
</tr>
<tr>
<td>Full-text 索引</td>
<td>不支持</td>
<td>暂不支持</td>
<td>不支持</td>
</tr>
</tbody>
</table>
<h3>B-TREE索引类型</h3>
<ul>
<li>
<strong>普通索引</strong><br>
这是最基本的索引类型,而且它没有唯一性之类的限制。普通索引可以通过以下几种方式创建:<br>
(1)创建索引: CREATE INDEX 索引名 ON 表名(列名1,列名2,...);<br>
(2)修改表: ALTER TABLE 表名ADD INDEX 索引名 (列名1,列名2,...);<br>
(3)创建表时指定索引:CREATE TABLE 表名 ( [...], INDEX 索引名 (列名1,列名 2,...) ); </li>
<li>
<strong>UNIQUE索引</strong><br>
表示唯一的,不允许重复的索引,如果该字段信息保证不会重复例如身份证号用作索引时,可设置为unique:<br>
(1)创建索引:CREATE UNIQUE INDEX 索引名 ON 表名(列的列表);<br>
(2)修改表:ALTER TABLE 表名ADD UNIQUE 索引名 (列的列表);<br>
(3)创建表时指定索引:CREATE TABLE 表名( [...], UNIQUE 索引名 (列的列表) ); </li>
<li>
<strong>主键:PRIMARY KEY索引</strong><br>
主键是一种唯一性索引,但它必须指定为“PRIMARY KEY”。<br>
(1)主键一般在创建表的时候指定:“CREATE TABLE 表名( [...], PRIMARY KEY (列的列表) ); ”。<br>
(2)但是,我们也可以通过修改表的方式加入主键:“ALTER TABLE 表名ADD PRIMARY KEY (列的列表); ”。<br>
每个表只能有一个主键。 (主键相当于聚合索引,是查找最快的索引)<br><code>注:不能用CREATE INDEX语句创建PRIMARY KEY索引</code>
</li>
</ul>
<h2>索引的设置语法</h2>
<h3>一 设置索引</h3>
<p>在执行CREATE TABLE语句时可以创建索引,也可以单独用CREATE INDEX或ALTER TABLE来为表增加索引。</p>
<p><strong>1.ALTER TABLE</strong> - ALTER TABLE用来创建<strong>普通索引</strong>、<strong>UNIQUE索引</strong>或<strong>PRIMARY KEY索引</strong>。</p>
<ul>
<li>ALTER TABLE table_name ADD <strong>INDEX</strong> index_name (column_list)</li>
<li>ALTER TABLE table_name ADD <strong>UNIQUE</strong> (column_list)</li>
<li>ALTER TABLE table_name ADD <strong>PRIMARY KEY</strong> (column_list)</li>
</ul>
<p><strong>2.CREATE INDEX</strong> - CREATE INDEX可对表增加普通索引或UNIQUE索引。</p>
<ul>
<li>CREATE INDEX index_name ON table_name (column_list)</li>
<li>CREATE UNIQUE INDEX index_name ON table_name (column_list)</li>
</ul>
<h3>二 删除索引</h3>
<p>可利用ALTER TABLE或DROP INDEX语句来删除索引。类似于CREATE INDEX语句,DROP INDEX可以在ALTER TABLE内部作为一条语句处理,语法如下。</p>
<ul>
<li>DROP INDEX index_name ON talbe_name</li>
<li>ALTER TABLE table_name DROP INDEX index_name</li>
<li>ALTER TABLE table_name DROP PRIMARY KEY</li>
</ul>
<p>其中,前两条语句是等价的,删除掉table_name中的索引index_name。<br>
第3条语句只在删除PRIMARY KEY索引时使用,<strong>因为一个表只可能有一个PRIMARY KEY索引,因此不需要指定索引名</strong>。如果没有创建PRIMARY KEY索引,但表具有一个或多个UNIQUE索引,则MySQL将删除第一个UNIQUE索引。</p>
<blockquote>
<p>如果从表中删除了某列,则索引会受到影响。对于多列组合的索引,如果删除其中的某列,则该列也会从索引中删除。如果删除组成索引的所有列,则整个索引将被删除。</p>
</blockquote>
<h3>三 查看索引</h3>
<pre><code>mysql> show index from tblname;
mysql> show keys from tblname;
</code></pre>
<ul>
<li>
<strong>Table</strong>:表的名称</li>
<li>
<strong>Non_unique</strong>:如果索引不能包括重复词,则为0。如果可以,则为1</li>
<li>
<strong>Key_name</strong>:索引的名称</li>
<li>
<strong>Seq_in_index</strong>:索引中的列序列号,从1开始</li>
<li>
<strong>Column_name</strong>:列名称</li>
<li>
<strong>Collation</strong>:列以什么方式存储在索引中。在MySQL中,有值‘A’(升序)或NULL(无分类)。</li>
<li>
<strong>Cardinality</strong>:索引中唯一值的数目的估计值。通过运行ANALYZE TABLE或myisamchk -a可以更新。基数根据被存储为整数的统计数据来计数,所以即使对于小型表,该值也没有必要是精确的。基数越大,当进行联合时,MySQL使用该索引的机会就越大。</li>
<li>
<strong>Sub_part</strong>:如果列只是被部分地编入索引,则为被编入索引的字符的数目。如果整列被编入索引,则为NULL。</li>
<li>
<strong>Packed</strong>:指示关键字如何被压缩。如果没有被压缩,则为NULL。</li>
<li>
<strong>Null</strong>:如果列含有NULL,则含有YES。如果没有,则该列含有NO。</li>
<li>
<strong>Index_type</strong>:用过的索引方法(BTREE, FULLTEXT, HASH, RTREE)。</li>
<li>
<strong>Comment</strong>:更多评注。</li>
</ul>
<h2>索引选择性</h2>
<h3>一 索引选择原则</h3>
<p><strong>1. 较频繁的作为查询条件的字段应该创建索引</strong><br><strong>2. 唯一性太差的字段不适合单独创建索引,即使频繁作为查询条件</strong><br><strong>3. 更新非常频繁的字段不适合创建索引</strong></p>
<blockquote>
<p>当然,并不是存在更新的字段就适合创建索引,从判定策略的用语上也可以看出,是"非常频繁"的字段。到底什么样的更新频率应该算是"非常频繁"呢?每秒?每分钟?还是每小时呢?说实话,还真难定义。很多时候是通过比较同一时间段内被更新的次数和利用该字段作为条件的查询次数来判断的,如果通过该字段的查询并不是很多,可能几个小时或是更长才会执行一次,更新反而比查询更频繁,那这样的字段肯定不适合创建索引。反之,如果我们通过该字段的查询比较频繁,但更新并不是特别多,比如查询几十次或更多才可能会产生一次更新,那我个人觉得更新所带来的附加成本也是可以接受的。</p>
</blockquote>
<p><strong>4. 不会出现在 WHERE 子句中的字段不该创建索引</strong></p>
<h3>二 索引选择原则细述</h3>
<ul>
<li>性能优化过程中,选择在哪个列上创建索引是最非常重要的。可以考虑使用索引的主要有 两种类型的列:<strong>在where子句中出现的列</strong>,<strong>在join子句中出现的列</strong>,而不是在SELECT关键字后选择列表的列;</li>
<li>索引列的基数越大,索引的效果越好。例如,存放出生日期的列具有不同的值,很容易区分行,而用来记录性别的列,只有"M"和"F",则对此进行索引没有多大用处,因此不管搜索哪个值,都会得出大约一半的行,( <code>见索引选择性注意事项对选择性解释</code>;)</li>
<li>
<p>使用短索引,如果对字符串列进行索引,应该指定一个前缀长度,可节省大量索引空间,提升查询速度;</p>
<blockquote>
<p>例如,有一个CHAR(200)列,如果在前10个或20个字符内,多数值是唯一的,那么就不要对整个列进行索引。对前10个或者20个字符进行索引能够节省大量索引空间,也可能会使查询更快。较小的索引涉及的磁盘IO较少,较短的值比较起来更快。更为重要的是,对于较短的键值,所以高速缓存中的快能容纳更多的键值,因此,MYSQL也可以在内存中容纳更多的值。这样就增加了找到行而不用读取索引中较多快的可能性。</p>
</blockquote>
</li>
<li><p>利用最左前缀</p></li>
</ul>
<h3>三 索引选择注意事项</h3>
<p>既然索引可以加快查询速度,那么是不是只要是查询语句需要,就建上索引?答案是否定的。因为索引虽然加快了查询速度,但索引也是有代价的:索引文件本身要消耗存储空间,同时索引会加重插入、删除和修改记录时的负担,另外,MySQL在运行时也要消耗资源维护索引,因此索引并不是越多越好。</p>
<p><strong>一般两种情况下不建议建索引:</strong></p>
<ol>
<li>
<p>表记录比较少,例如一两千条甚至只有几百条记录的表,没必要建索引,让查询做全表扫描就好了;</p>
<blockquote>
<p>至于多少条记录才算多,这个个人有个人的看法,我个人的经验是以2000作为分界线,记录数不超过 2000可以考虑不建索引,超过2000条可以酌情考虑索引。</p>
</blockquote>
</li>
<li>
<p>索引的选择性较低。所谓索引的选择性(Selectivity),是指不重复的索引值(也叫基数,Cardinality)与表记录数(#T)的比值:<br><code>Index Selectivity = Cardinality / #T</code><br>
显然选择性的取值范围为(0, 1],选择性越高的索引价值越大,这是由B+Tree的性质决定的。例如,上文用到的employees.titles表,如果title字段经常被单独查询,是否需要建索引,我们看一下它的选择性:</p>
<blockquote>
<p><code>SELECT count(DISTINCT(title))/count(*) AS Selectivity FROM employees.titles;</code></p>
</blockquote>
<pre><code>+-------------+
| Selectivity |
+-------------+
| 0.0000 |
+-------------+
</code></pre>
<p><strong>title的选择性不足0.0001(精确值为0.00001579),所以实在没有什么必要为其单独建索引。</strong></p>
</li>
<li><p>MySQL只对一下操作符才使用索引:<strong><code><,<=,=,>,>=,between,in</code></strong>, 以及某些时候的<code>like(不以通配符%或_开头的情形)</code>。</p></li>
<li><p>不要过度索引,只保持所需的索引。每个额外的索引都要占用额外的磁盘空间,并降低写操作的性能。 在修改表的内容时,索引必须进行更新,有时可能需要重构,因此,索引越多,所花的时间越长。</p></li>
</ol>
<h3>四 索引的弊端</h3>
<p>索引的益处已经清楚了,但是我们不能只看到这些益处,并认为索引是解决查询优化的圣经,只要发现 查询运行不够快就将 WHERE 子句中的条件全部放在索引中。</p>
<blockquote>
<p>确实,索引能够极大地提高数据检索效率,也能够改善排序分组操作的性能,但有不能忽略的一个问题就是索引是完全独立于基础数据之外的一部分数据。假设在Table ta 中的Column ca 创建了索引 idx_ta_ca,那么任何更新 Column ca 的操作,MySQL在更新表中 Column ca的同时,都须要更新Column ca 的索引数据,调整因为更新带来键值变化的索引信息。而如果没有对 Column ca 进行索引,MySQL要做的仅仅是更新表中 Column ca 的信息。这样,最明显的资源消耗就是增加了更新所带来的 IO 量和调整索引所致的计算量。此外,Column ca 的索引idx_ta_ca须要占用存储空间,而且随着 Table ta 数据量的增加,idx_ta_ca 所占用的空间也会不断增加,所以索引还会带来存储空间资源消耗的增加。</p>
</blockquote>
<h2>引用</h2>
<ul>
<li><a rel="nofollow" href="http://tech.meituan.com/mysql-index.html">美团-MySQL索引原理及慢查询优化</a></li>
<li><a rel="nofollow" href="http://blog.codinglabs.org/articles/theory-of-mysql-index.html">MySQL索引背后的数据结构及算法原理</a></li>
<li><a rel="nofollow" href="http://book.51cto.com/art/200906/132452.htm">索引的利弊与如何判定,是否需要索引</a></li>
</ul>
MySQL日志管理
https://segmentfault.com/a/1190000003072237
2015-08-10T17:01:38+08:00
2015-08-10T17:01:38+08:00
人云思云
https://segmentfault.com/u/rysy
8
<blockquote>
<p>参照:系统是CentOS6.4 ,MySQL版本为5.5.32</p>
</blockquote>
<p>MySQL 服务器上一共有六种日志:错误日志,查询日志,慢查询日志,二进制日志,事务日志,中继日志。</p>
<h2>一 错误日志</h2>
<p>错误日志不仅仅记录错误信息,它记录的事件有:<br>
- 服务器启动和关闭过程中的信息<br>
- 服务器运行过程中的错误信息<br>
- 事件调度器运行一个事件时产生的信息<br>
- (如果被配置为从服务器)启动从服务器进程时产生的信息</p>
<h3>查看错误日志文件的路径</h3>
<p>在mysql数据库中,错误日志功能是默认开启的。</p>
<pre><code>xml</code><code>mysql> SHOW VARIABLES LIKE 'log_error%';
+---------------+-----------------------------------------------+
| Variable_name | Value |
+---------------+-----------------------------------------------+
| log_error | /opt/lampstack-5.4.22-0/mysql/data/mysqld.log |
+---------------+-----------------------------------------------+
</code></pre>
<h3>配置错误日志</h3>
<p>编辑 my.cnf(一般在mysql目录下),修改 log-error 参数,如果没有就新增:<br>
- <strong>错误日志文件配置</strong></p>
<pre><code># Error Logging.
log-error="filename.log"
</code></pre>
<ul>
<li>
<strong>相关配置变量说明</strong><br><code>log_error={1 | 0 | /PATH/TO/ERROR_LOG_FILENAME}</code><br>
定义错误日志文件。作用范围为全局或会话级别,可用于配置文件,属非动态变量。<br><code>log_warnings = {1|0}</code><br>
决定是否将警告信息记录入错误日志。</li>
</ul>
<h2>二 查询日志</h2>
<p>其中查询日志记录查询操作,默认情况下查询日志是关闭的。<code>开启查询日志会增加很多磁盘 I/O, 所以如非出于调试目的,不建议开启查询日志</code>。</p>
<h3>查看查询日志是否启用及查询日志的路径</h3>
<pre><code>xml</code><code>mysql> SHOW VARIABLES LIKE 'general_log%';
+------------------+--------------------------------------------------+
| Variable_name | Value |
+------------------+--------------------------------------------------+
| general_log | OFF |
| general_log_file | /opt/lampstack-5.4.22-0/mysql/data/localhost.log |
+------------------+--------------------------------------------------+
</code></pre>
<h3>配置查询日志</h3>
<p>编辑 my.cnf,修改 general-log 参数为 1,同时设定 log-output 参数(日志输出类型)和 general_log_file 参数(查询日志路径):</p>
<pre><code># General logging.:
log-output=FILE
general-log=1
general_log_file="filename.log"
</code></pre>
<p>保存 my.cnf 更改,重启 MySQL 服务。</p>
<h2>三 慢查询日志</h2>
<p>慢查询是指执行时长(包括等待CPU/IO的时间)超过 long_query_time 这个变量定义的时长的查询。<code>慢查询日志开销比较小,可以用于定位性能问题,建议开启。</code></p>
<h3>查看慢查询日志是否启用及慢查询日志的路径</h3>
<pre><code>xml</code><code>mysql> SHOW VARIABLES LIKE 'slow_query_log%';
+---------------------+--------------------------------------------------+
| Variable_name | Value |
+---------------------+--------------------------------------------------+
| slow_query_log | OFF |
| slow_query_log_file | /usr/local/var/mysql/upstreamdeMac-mini-slow.log |
+---------------------+--------------------------------------------------+
</code></pre>
<h3>配置慢查询日志</h3>
<p>编辑 my.cnf ,设置 log_slow_queries 参数为 1,同时设定 log-output 参数(日志输出类型)、slow-query-log_file 参数(慢查询日志路径)和 long_query_time 参数:</p>
<pre><code>log</code><code># Slow logging.
log-output=FILE
log_slow_queries=1 //MySQL 5.6将此参数修改为了slow_query_log
slow_query_log_file="filename.log"
long_query_time=10 //慢查的时长单位为秒,可以精确到小数点后6位(微秒)
</code></pre>
<p>保存 my.cnf 更改,重启 MySQL 服务。</p>
<h3>关闭慢查询日志</h3>
<p>设置 log_slow_queries 参数为 0:</p>
<pre><code># Slow logging.
log-output=NONE
log_slow_queries=0 //MySQL 5.6将此参数修改为了slow_query_log
slow_query_log_file="filename.log"
long_query_time=10
</code></pre>
<p>保存 my.cnf 更改,重启 MySQL 服务。</p>
<h2>四 二进制日志</h2>
<p>二进制日志记录 MySQL 数据库中所有与更新相关的操作,即二进制日志记录了所有的 DDL(数据定义语言)语句和 DML(数据操纵语言)语句,但是不包括数据查询语句。常用于恢复数据库和主从复制。</p>
<h3>查看 log_bin 状态</h3>
<pre><code>xml</code><code>mysql> SHOW VARIABLES LIKE 'log_bin%';
+---------------------------------+-------+
| Variable_name | Value |
+---------------------------------+-------+
| log_bin | OFF |
| log_bin_basename | |
| log_bin_index | |
| log_bin_trust_function_creators | OFF |
| log_bin_use_v1_row_events | OFF |
+---------------------------------+-------+
</code></pre>
<h3>启用二进制日志功能</h3>
<p>编辑 my.cnf ,在 [mysqld] 下添加</p>
<pre><code># Binary Logging.
log-bin="filename-bin"
</code></pre>
<p>保存 my.cnf 更改,重启 MySQL 服务。</p>
<h5>其他相关配置:</h5>
<p><code>max_binlog_size={4096 .. 1073741824} ;</code><br>
设定二进制日志文件上限,单位为字节,最小值为4K,最大值为1G,默认为1G。某事务所产生的日志信息只能写入一个二进制日志文件,因此,实际上的二进制日志文件可能大于这个指定的上限。作用范围为全局级别,可用于配置文件,属动态变量。</p>
<h3>查看日志文件</h3>
<p>在data目录下有一个mysql-bin.index便是索引文件,以mysql-bin开头并以数字结尾的文件为二进制日志文件。<br><strong>查看所有的二进制文件:</strong></p>
<pre><code>xml</code><code>mysql> show binary logs;
+------------------+-----------+
| Log_name | File_size |
+------------------+-----------+
| mysql-bin.000001 | 276665 |
+------------------+-----------+
1 row in set (0.03 sec)
</code></pre>
<p><strong>查看当前正在使用的二进制文件:</strong></p>
<pre><code>xml</code><code>mysql> show master status;
+------------------+----------+--------------+------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+------------------+----------+--------------+------------------+
| mysql-bin.000003 | 107 | | |
+------------------+----------+--------------+------------------+
1 row in set (0.00 sec)
</code></pre>
<h3>二进制日志滚动</h3>
<p>当 MySQL 服务进程启动、当前二进制日志文件的大小已经超过上限时、执行 FLUSH LOG 时,MySQL 会创建一个新的二进制日志文件。新的编号大1的日志用于记录最新的日志,而原日志名字不会被改变。</p>
<p>手动滚动命令:<code>flush logs;</code></p>
<h3>查看日志详细</h3>
<p>查看binlog日志有几种方式:<br>
1. 使用show binlog events方式可以获取当前以及指定binlog的日志,<br>
2. 使用mysqlbinlog命令行。</p>
<h4>1. show binlog events方式</h4>
<ul>
<li>
<strong>只查看第一个binlog文件的内容(show binlog events)</strong> </li>
</ul>
<pre><code>xml</code><code>mysql> show binlog events; #默认会返回mysql-bin.000001的日志
#太多,我就不截屏了
</code></pre>
<ul>
<li>
<strong>查看指定binlog文件的内容(show binlog events in 'binname.xxxxx')</strong> </li>
</ul>
<pre><code>mysql> show binlog events in 'mysql-bin.000002';
</code></pre>
<p><img alt="Alt text"></p>
<ul>
<li>
<strong>获取指定位置binlog的内容(show binlog events from xxx)</strong> </li>
</ul>
<pre><code>mysql> show binlog events in 'mysql-bin.000002' from 107;
</code></pre>
<h4>2. mysqlbinlog命令行</h4>
<blockquote>
<p>mysqlbinlog在mysql/bin目录</p>
</blockquote>
<h5>I 查看binlog日志</h5>
<ul>
<li><strong>查看指定的binlog日志</strong></li>
</ul>
<pre><code>shell</code><code>[root@localhost bin]# ./mysqlbinlog ../data/mysql-bin.000003
</code></pre>
<ul>
<li><p><strong>按时间查看二进制日志</strong><br>
mysqlbinlog ../data/mysql-bin.000003 --start-datetime="2015-3-11 17:00:00"<br>
mysqlbinlog ../data/mysql-bin.000003 --stop-datetime="2015-3-12 17:30:00"<br>
mysqlbinlog ../data/mysql-bin.000003 --start-datetime="2015-3-11 17:00:00" --stop-datetime="2015-3-12 17:30:00"</p></li>
<li><p><strong>按字节数查看二进制日志</strong><br>
mysqlbinlog ../data/mysql-bin.000003 --start-position=20<br>
mysqlbinlog ../data/mysql-bin.000003 --stop-position=200<br>
mysqlbinlog ../data/mysql-bin.000003 --start-position=20 --stop-position=200</p></li>
<li><p><strong>过滤insert、update操作</strong><br>
mysqlbinlog ../data/mysql-bin.000003 | grep insert</p></li>
</ul>
<h5>II 查看binlog日志并输出</h5>
<p>下面参考:<a rel="nofollow" href="http://blog.csdn.net/leshami/article/details/41962243">使用mysqlbinlog提取二进制日志</a></p>
<pre><code>c、提取指定position位置的binlog日志并输出到压缩文件
# mysqlbinlog --start-position="120" --stop-position="332" /opt/data/APP01bin.000001 |gzip >extra_01.sql.gz
d、提取指定position位置的binlog日志导入数据库
# mysqlbinlog --start-position="120" --stop-position="332" /opt/data/APP01bin.000001 | mysql -uroot -p
e、提取指定开始时间的binlog并输出到日志文件
# mysqlbinlog --start-datetime="2014-12-15 20:15:23" /opt/data/APP01bin.000002 --result-file=extra02.sql
f、提取指定位置的多个binlog日志文件
# mysqlbinlog --start-position="120" --stop-position="332" /opt/data/APP01bin.000001 /opt/data/APP01bin.000002|more
g、提取指定数据库binlog并转换字符集到UTF8
# mysqlbinlog --database=test --set-charset=utf8 /opt/data/APP01bin.000001 /opt/data/APP01bin.000002 >test.sql
h、远程提取日志,指定结束时间
# mysqlbinlog -urobin -p -h192.168.1.116 -P3306 --stop-datetime="2014-12-15 20:30:23" --read-from-remote-server mysql-bin.000033 |more
i、远程提取使用row格式的binlog日志并输出到本地文件
# mysqlbinlog -urobin -p -P3606 -h192.168.1.177 --read-from-remote-server -vv inst3606bin.000005 >row.sql
</code></pre>
<h3>expire_logs_days 参数</h3>
<p>在 my.cnf 中配置 <code>expire_logs_days</code> 参数指定二进制日志的有效天数,MySQL 会自动删除过期的二进制日志。expire_logs_days 设置在服务器启动或者 MySQL 切换二进制日志时生效,因此,如果二进制日志没有增长和切换,服务器不会清除老条目。</p>
<pre><code># 二进制日志的有效天数
expire_logs_days = 5
</code></pre>
<h3>清除二进制日志</h3>
<ul>
<li><strong>清除所有日志(<code>不存在主从复制关系</code>)</strong></li>
</ul>
<pre><code>mysql> RESET MASTER;
</code></pre>
<ul>
<li><strong>清除指定日志之前的所有日志</strong></li>
</ul>
<pre><code>mysql> PURGE MASTER LOGS TO 'mysql-bin.000003';
</code></pre>
<ul>
<li><strong>清除某一时间点前的所有日志</strong></li>
</ul>
<pre><code>mysql> PURGE MASTER LOGS BEFORE '2015-01-01 00:00:00';
</code></pre>
<ul>
<li><strong>清除 n 天前的所有日志</strong></li>
</ul>
<pre><code>mysql> PURGE MASTER LOGS BEFORE CURRENT_DATE - INTERVAL 10 DAY;
</code></pre>
<p><strong>警告</strong></p>
<blockquote>
<p><code>由于二进制日志的重要性,请仅在确定不再需要将要被删除的二进制文件,或者在已经对二进制日志文件进行归档备份,或者已经进行数据库备份的情况下,才进行删除操作,且不要使用 rm 命令删除。</code></p>
</blockquote>
<h2>五 事务日志</h2>
<p>出于性能和故障恢复的考虑,MySQL 服务器不会立即执行事务,而是先将事务记录在日志里面,这样可以将随机IO转换成顺序IO,从而提高IO性能。</p>
<p>事物日志通常是一组至少两个固定大小的文件,当其中一个写满时,MySQL会将事务日志写入另一个日志文件(先清空原有内容)。当 MySQL 从崩溃中恢复时,会读取事务日志,将其中已经 commit 的事务写入数据库,没有 commit 的事务 rollback 。</p>
<p><strong>事务日志由存储引擎(innodb)管理,一般不需要手动干预。</strong></p>
<h2>六 中继日志</h2>
<p>中继日志用于主从复制架构中的从服务器上,从服务器的 slave 进程从主服务器处获取二进制日志的内容并写入中继日志,然后由 IO 进程读取并执行中继日志中的语句。</p>
<h2>日志分析工具</h2>
<ul>
<li>
<strong>mysqldumpslowmysql</strong>:官方提供的慢查询日志分析工具</li>
<li>
<strong>mysqlsla</strong>:hackmysql.com 推出的一款日志分析工具(该网站还维护了 mysqlreport,mysqlidxchk 等比较实用的mysql 工具)。 整体来说,功能非常强大。输出的数据报表非常有利于分析慢查询的原因,包括执行频率、数据量、查询消耗等。 </li>
<li>
<strong>myprofi</strong>:纯 php 写的一个开源分析工具.项目在 <a rel="nofollow" href="http://myprofi.sourceforge.net/">sourceforge</a> 上。功能上,列出了总的慢查询次数和类型、去重后的 sql 语句、执行次数及其占总的 slow log 数量的百分比。从整体输出样式来看,比 mysql-log-filter 还要简洁,省去了很多不必要的内容。对于只想看 sql 语句及执行次数的用户来说,比较推荐。 </li>
<li>
<strong>mysql-log-filter</strong>:<a rel="nofollow" href="http://code.google.com/p/mysql-log-filter/">google code</a> 上找到的一个分析工具,提供了 python 和 php 两种可执行的脚本。 特色功能除了统计信息外,还针对输出内容做了排版和格式化,保证整体输出的简洁。喜欢简洁报表的朋友,推荐使用一下。</li>
</ul>
<h2><a rel="nofollow" href="http://tech.uc.cn/?p=716">MySQL Innodb日志机制深入分析</a></h2>
<h2>参考</h2>
<p><a rel="nofollow" href="http://icy1900.me/2014/10/26/MySQL%E6%97%A5%E5%BF%97%E7%AE%A1%E7%90%86.html">MySQL 日志管理</a><br><a rel="nofollow" href="http://www.tianfeiyu.com/?p=698">MySQL日志管理</a><br><a rel="nofollow" href="http://blog.csdn.net/leshami/article/details/41962243">使用mysqlbinlog提取二进制日志</a></p>
Linux IO模式及 select、poll、epoll详解
https://segmentfault.com/a/1190000003063859
2015-08-07T16:08:25+08:00
2015-08-07T16:08:25+08:00
人云思云
https://segmentfault.com/u/rysy
756
<blockquote>
<p>注:本文是对众多博客的学习和总结,可能存在理解错误。请带着怀疑的眼光,同时如果有错误希望能指出。</p>
</blockquote>
<p>同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案是不同的。所以先限定一下本文的上下文。</p>
<pre><code>本文讨论的背景是Linux环境下的network IO。
</code></pre>
<h2>一 概念说明</h2>
<p>在进行解释之前,首先要说明几个概念:<br>
- 用户空间和内核空间<br>
- 进程切换<br>
- 进程的阻塞<br>
- 文件描述符<br>
- 缓存 I/O</p>
<h3>用户空间与内核空间</h3>
<p>现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。</p>
<h3>进程切换</h3>
<p>为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。</p>
<p>从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:<br>
1. 保存处理机上下文,包括程序计数器和其他寄存器。<br>
2. 更新PCB信息。<br>
3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。<br>
4. 选择另一个进程执行,并更新其PCB。<br>
5. 更新内存管理的数据结构。<br>
6. 恢复处理机上下文。</p>
<p>注:<strong>总而言之就是很耗资源</strong>,具体的可以参考这篇文章:<a rel="nofollow" href="http://guojing.me/linux-kernel-architecture/posts/process-switch/">进程切换</a></p>
<h3>进程的阻塞</h3>
<p>正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。<code>当进程进入阻塞状态,是不占用CPU资源的</code>。</p>
<h3>文件描述符fd</h3>
<p>文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。</p>
<p>文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。</p>
<h3>缓存 I/O</h3>
<p>缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。</p>
<p><strong>缓存 I/O 的缺点:</strong><br>
数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。</p>
<h2>二 IO模式</h2>
<p>刚才说了,对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:<br>
1. 等待数据准备 (Waiting for the data to be ready)<br>
2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)</p>
<p>正式因为这两个阶段,linux系统产生了下面五种网络模式的方案。<br>
- 阻塞 I/O(blocking IO)<br>
- 非阻塞 I/O(nonblocking IO)<br>
- I/O 多路复用( IO multiplexing)<br>
- 信号驱动 I/O( signal driven IO)<br>
- 异步 I/O(asynchronous IO)</p>
<p>注:由于signal driven IO在实际中并不常用,所以我这只提及剩下的四种IO Model。</p>
<h3>阻塞 I/O(blocking IO)</h3>
<p>在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:<br><img src="/img/bVm1c3" alt="clipboard.png"></p>
<p>当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。</p>
<blockquote>
<p>所以,blocking IO的特点就是在IO执行的两个阶段都被block了。</p>
</blockquote>
<h3>非阻塞 I/O(nonblocking IO)</h3>
<p>linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:<br><img src="/img/bVm1c4" alt="clipboard.png"></p>
<p>当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。</p>
<blockquote>
<p>所以,nonblocking IO的特点是用户进程需要<strong>不断的主动询问</strong>kernel数据好了没有。</p>
</blockquote>
<h3>I/O 多路复用( IO multiplexing)</h3>
<p>IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。</p>
<p><img src="/img/bVm1c5" alt="clipboard.png"></p>
<p><code>当用户进程调用了select,那么整个进程会被block</code>,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。</p>
<blockquote>
<p>所以,I/O 多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。</p>
</blockquote>
<p>这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。</p>
<p>所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)</p>
<p>在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。</p>
<h3>异步 I/O(asynchronous IO)</h3>
<p>inux下的asynchronous IO其实用得很少。先看一下它的流程:<br><img src="/img/bVm1c8" alt="clipboard.png"></p>
<p>用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。</p>
<h3>总结</h3>
<h4>blocking和non-blocking的区别</h4>
<p>调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。</p>
<h4>synchronous IO和asynchronous IO的区别</h4>
<p>在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:<br>
- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;<br>
- An asynchronous I/O operation does not cause the requesting process to be blocked;</p>
<p>两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。</p>
<p>有人会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。</p>
<p>而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。</p>
<p><strong>各个IO Model的比较如图所示:</strong><br><img src="/img/bVm1c9" alt="clipboard.png"></p>
<p>通过上面的图片,可以发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。</p>
<h2>三 I/O 多路复用之select、poll、epoll详解</h2>
<p>select,poll,epoll都是IO多路复用的机制。I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。(这里啰嗦下)</p>
<h3>select</h3>
<pre><code>int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
</code></pre>
<p>select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。</p>
<p>select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。</p>
<h3>poll</h3>
<pre><code>int poll (struct pollfd *fds, unsigned int nfds, int timeout);
</code></pre>
<p>不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。</p>
<pre><code>struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
</code></pre>
<p>pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。</p>
<blockquote>
<p>从上面看,select和poll都需要在返回后,<code>通过遍历文件描述符来获取已经就绪的socket</code>。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。</p>
</blockquote>
<h3>epoll</h3>
<p>epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。</p>
<h4>一 epoll操作过程</h4>
<p>epoll操作过程需要三个接口,分别如下:</p>
<pre><code>int epoll_create(int size);//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
</code></pre>
<p><strong>1. int epoll_create(int size);</strong><br>
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大,这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值,<code>参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议</code>。<br>
当创建好epoll句柄后,它就会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。</p>
<p><strong>2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);</strong><br>
函数是对指定描述符fd执行op操作。<br>
- epfd:是epoll_create()的返回值。<br>
- op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。<br>
- fd:是需要监听的fd(文件描述符)<br>
- epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:</p>
<pre><code>struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
</code></pre>
<p><strong>3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);</strong><br>
等待epfd上的io事件,最多返回maxevents个事件。<br>
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。</p>
<h4>二 工作模式</h4>
<p> epoll对文件描述符的操作有两种模式:<strong>LT(level trigger)</strong>和<strong>ET(edge trigger)</strong>。LT模式是默认模式,LT模式与ET模式的区别如下:<br>
<strong>LT模式</strong>:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,<code>应用程序可以不立即处理该事件</code>。下次调用epoll_wait时,会再次响应应用程序并通知此事件。<br>
<strong>ET模式</strong>:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,<code>应用程序必须立即处理该事件</code>。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。</p>
<h5>1. LT模式</h5>
<p>LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。</p>
<h5>2. ET模式</h5>
<p>ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)</p>
<p>ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。</p>
<h5>3. 总结</h5>
<p><strong>假如有这样一个例子:</strong><br>
1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符<br>
2. 这个时候从管道的另一端被写入了2KB的数据<br>
3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作<br>
4. 然后我们读取了1KB的数据<br>
5. 调用epoll_wait(2)......</p>
<p><strong>LT模式:</strong><br>
如果是LT模式,那么在第5步调用epoll_wait(2)之后,仍然能受到通知。</p>
<p><strong>ET模式:</strong><br>
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。</p>
<p>当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,<br>
读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取:</p>
<pre><code>while(rs){
buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
if(buflen < 0){
// 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读
// 在这里就当作是该次事件已处理处.
if(errno == EAGAIN){
break;
}
else{
return;
}
}
else if(buflen == 0){
// 这里表示对端的socket已正常关闭.
}
if(buflen == sizeof(buf){
rs = 1; // 需要再次读取
}
else{
rs = 0;
}
}
</code></pre>
<blockquote>
<p><strong>Linux中的EAGAIN含义</strong></p>
</blockquote>
<p>Linux环境下开发经常会碰到很多错误(设置errno),其中EAGAIN是其中比较常见的一个错误(比如用在非阻塞操作中)。<br>
从字面上来看,是提示再试一次。这个错误经常出现在当应用程序进行一些非阻塞(non-blocking)操作(对文件或socket)的时候。</p>
<p>例如,以 O_NONBLOCK的标志打开文件/socket/FIFO,如果你连续做read操作而没有数据可读。此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。<br>
又例如,当一个系统调用(比如fork)因为没有足够的资源(比如虚拟内存)而执行失败,返回EAGAIN提示其再调用一次(也许下次就能成功)。</p>
<h4>三 代码演示</h4>
<p>下面是一段不完整的代码且格式不对,意在表述上面的过程,去掉了一些模板代码。</p>
<pre><code>#define IPADDRESS "127.0.0.1"
#define PORT 8787
#define MAXSIZE 1024
#define LISTENQ 5
#define FDSIZE 1000
#define EPOLLEVENTS 100
listenfd = socket_bind(IPADDRESS,PORT);
struct epoll_event events[EPOLLEVENTS];
//创建一个描述符
epollfd = epoll_create(FDSIZE);
//添加监听描述符事件
add_event(epollfd,listenfd,EPOLLIN);
//循环等待
for ( ; ; ){
//该函数返回已经准备好的描述符事件数目
ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
//处理接收到的连接
handle_events(epollfd,events,ret,listenfd,buf);
}
//事件处理函数
static void handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
{
int i;
int fd;
//进行遍历;这里只要遍历已经准备好的io事件。num并不是当初epoll_create时的FDSIZE。
for (i = 0;i < num;i++)
{
fd = events[i].data.fd;
//根据描述符的类型和事件类型进行处理
if ((fd == listenfd) &&(events[i].events & EPOLLIN))
handle_accpet(epollfd,listenfd);
else if (events[i].events & EPOLLIN)
do_read(epollfd,fd,buf);
else if (events[i].events & EPOLLOUT)
do_write(epollfd,fd,buf);
}
}
//添加事件
static void add_event(int epollfd,int fd,int state){
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}
//处理接收到的连接
static void handle_accpet(int epollfd,int listenfd){
int clifd;
struct sockaddr_in cliaddr;
socklen_t cliaddrlen;
clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);
if (clifd == -1)
perror("accpet error:");
else {
printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port); //添加一个客户描述符和事件
add_event(epollfd,clifd,EPOLLIN);
}
}
//读处理
static void do_read(int epollfd,int fd,char *buf){
int nread;
nread = read(fd,buf,MAXSIZE);
if (nread == -1) {
perror("read error:");
close(fd); //记住close fd
delete_event(epollfd,fd,EPOLLIN); //删除监听
}
else if (nread == 0) {
fprintf(stderr,"client close.\n");
close(fd); //记住close fd
delete_event(epollfd,fd,EPOLLIN); //删除监听
}
else {
printf("read message is : %s",buf);
//修改描述符对应的事件,由读改为写
modify_event(epollfd,fd,EPOLLOUT);
}
}
//写处理
static void do_write(int epollfd,int fd,char *buf) {
int nwrite;
nwrite = write(fd,buf,strlen(buf));
if (nwrite == -1){
perror("write error:");
close(fd); //记住close fd
delete_event(epollfd,fd,EPOLLOUT); //删除监听
}else{
modify_event(epollfd,fd,EPOLLIN);
}
memset(buf,0,MAXSIZE);
}
//删除事件
static void delete_event(int epollfd,int fd,int state) {
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}
//修改事件
static void modify_event(int epollfd,int fd,int state){
struct epoll_event ev;
ev.events = state;
ev.data.fd = fd;
epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}
//注:另外一端我就省了
</code></pre>
<h4>四 epoll总结</h4>
<p>在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而<strong>epoll事先通过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知</strong>。(<code>此处去掉了遍历文件描述符,而是通过监听回调的的机制</code>。这正是epoll的魅力所在。)</p>
<p><strong>epoll的优点主要是一下几个方面:</strong><br>
1. 监视的描述符数量不受限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。select的最大缺点就是进程打开的fd是有数量限制的。这对 于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache就是这样实现的),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。</p>
<ol>
<li>IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。</li>
</ol>
<blockquote>
<p>如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle- connection,就会发现epoll的效率大大高于select/poll。</p>
</blockquote>
<h2>参考</h2>
<p><a rel="nofollow" href="http://www.cnblogs.com/Anker/p/3269106.html">用户空间与内核空间,进程上下文与中断上下文[总结]</a><br><a rel="nofollow" href="http://guojing.me/linux-kernel-architecture/posts/process-switch/">进程切换</a><br><a rel="nofollow" href="https://zh.wikipedia.org/wiki/%E6%96%87%E4%BB%B6%E6%8F%8F%E8%BF%B0%E7%AC%A6">维基百科-文件描述符</a><br><a rel="nofollow" href="http://www.ibm.com/developerworks/cn/linux/l-cn-directio/">Linux 中直接 I/O 机制的介绍</a><br><a rel="nofollow" href="http://blog.csdn.net/historyasamirror/article/details/5778378">IO - 同步,异步,阻塞,非阻塞 (亡羊补牢篇)</a><br><a rel="nofollow" href="http://www.cnblogs.com/bigwangdi/p/3182958.html">Linux中select poll和epoll的区别</a><br><a rel="nofollow" href="http://www.cnblogs.com/Anker/archive/2013/08/14/3258674.html">IO多路复用之select总结</a><br><a rel="nofollow" href="http://www.cnblogs.com/Anker/archive/2013/08/15/3261006.html">IO多路复用之poll总结</a><br><a rel="nofollow" href="http://www.cnblogs.com/Anker/archive/2013/08/17/3263780.html">IO多路复用之epoll总结</a></p>
闭包
https://segmentfault.com/a/1190000002963465
2015-07-06T11:19:18+08:00
2015-07-06T11:19:18+08:00
人云思云
https://segmentfault.com/u/rysy
6
<h2>闭包</h2>
<blockquote>
<p>在计算机科学中,闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。 --- <a rel="nofollow" href="https://zh.wikipedia.org/wiki/%E9%97%AD%E5%8C%85_%28%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6%29">维基百科</a></p>
</blockquote>
<p>其实这段引用已经说明了闭包的本质:<strong>引用了自由变量的函数</strong>,<strong>自由变量将和这个函数一同存在</strong>——这是理解闭包的关键。</p>
<h3>一 原理解释</h3>
<p>函数式编程语言的基础是lambda演算,而闭包又是从函数式编程衍生而来。下面先从Lambda演算理解下函数式思维。(<strong>不感兴趣可直接跳过</strong>)</p>
<h4>Lambda演算</h4>
<blockquote>
<p><a rel="nofollow" href="https://zh.wikipedia.org/wiki/%CE%9B%E6%BC%94%E7%AE%97">Lambda演算</a>是一套用于研究函数定义、应用和递归的形式系统。它包括一条变换规则(变量替换)和一条函数定义方式,Lambda演算之通用在于,任何一个可计算函数都能用这种形式来表达和求值。因而,它是等价于图灵机的。尽管如此,Lambda演算强调的是变换规则的运用,而非实现它们的具体机器。可以认为这是一种更接近软件而非硬件的方式。Lambda演算对函数式编程语言有巨大的影响,比如Lisp和Haskell。</p>
</blockquote>
<h5>非形式化的描述</h5>
<blockquote>
<p>在lambda演算中,每个表达式都代表一个函数,这个函数有一个参数,并且返回一个值。不论是参数和返回值,也都是一个单参的函数。可以这么说,lambda演算中,只有一种“类型”,那就是这种单参函数。</p>
</blockquote>
<p><strong>注</strong>:在函数式编程语言中,函数可是一等公民。</p>
<p>函数是通过<strong>λ表达式</strong>匿名地定义的,这个表达式说明了此函数将对其参数进行什么操作。例如,“加2”函数f(x)= x + 2可以用lambda演算表示为<strong>λx.x + 2</strong> (或者λy.y + 2,参数的取名无关紧要)</p>
<p>λ演算中函数只有一个参数,那有两个参数的函数怎么表达呢?可以通过lambda演算这么表达:<strong>一个单一参数的函数的返回值又是一个单一参数的函数</strong>(<code>闭包吗?</code>)。<br>
例如,函数f(x, y) = x + y可以写作:</p>
<pre><code>λx.λy.x + y ----->λx. (λ y. + x y)
</code></pre>
<p>上面这个转化就叫<a rel="nofollow" href="https://zh.wikipedia.org/wiki/%E6%9F%AF%E9%87%8C%E5%8C%96">currying</a>,它展示了,我们如何实现加法(假设+这个符号已经具有相加的功能)。</p>
<p><strong>其实就是我们现在意义上的闭包——你调用一个函数,这个函数返回另一个函数,返回的函数中存储保留了调用函数的变量。currying是闭包的鼻祖。</strong>(如果理解困难,下面会用编程语言实现上面的演算)</p>
<h4>闭包解释</h4>
<p>闭包被广泛使用于函数式编程语言,慢慢很多命令式语言也开始支持闭包。<strong>在函数中可以(嵌套)定义另一个函数时</strong>,如果<strong>内部的函数引用了外部的函数的变量</strong>,则可能产生闭包。运行时,一旦外部的函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。</p>
<p>典型的支持闭包的语言中,通常将<strong>函数</strong>当作<strong>第一类对象</strong>——在这些语言中,函数通常有下列特性:<br>
- 可以将函数赋值给一个变量<br>
- 函数可以作为参数传递<br>
- 函数的返回值可以是一个函数</p>
<p>例如以下<strong>Scheme</strong>(Lisp的一个方言)代码:</p>
<pre><code>(define (f x) (lambda (y) (+ x y)))
</code></pre>
<p>在这个例子中,lambda表达式<strong><em>(lambda (y) (+ x y))</em></strong>出现在函数<strong><em>f</em></strong>中。当这个lambda表达式被执行时,Scheme创造了一个包含此表达式以及对<strong>x变量</strong>的引用的闭包,其中x变量在lambda表达式中是自由变量。</p>
<p>下面是用<strong>ECMAScript</strong> (JavaScript)写的同一个例子:</p>
<pre><code>function f(x){
return function(y) {
return x + y;
};
}
</code></pre>
<p>其中f返回的匿名函数与其自由变量x组成了一个闭包。</p>
<p>上述代码在nodeJS环境中执行:</p>
<pre><code>console.log( (f(7)) (2) );
//9
</code></pre>
<p>首先用第一个参数(7)代替最外层函数的参数(x),然后用第二个参数(2)代替第二层函数的参数(y),最终得到计算结果。</p>
<p><strong>注意:</strong>这个运算执行了两个函数:<strong>f</strong>和<strong>匿名函</strong>数。f的作用域为(f 7),这就是说,当(f 7)执行后,f这个函数就结束了,而x是f的私有变量,理论上x应该被释放了,然后x在f函数执行结束后并没有被释放,而是继续被匿名函数继续使用。支持这种机制的语言称为支持<strong>闭包机制</strong>(<code>在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部函数的变量,即使已经离开了外部函数的环境,自由变量(外部函数的变量)也和内部函数一同存在,则产生闭包</code>)。</p>
<h3>二 闭包的实现</h3>
<p>通过上面的原理解释我们提出了这样一个问题(虽然这样的问题不干扰你理解闭包):</p>
<blockquote>
<p>如果一个函数定义在栈中,那么当函数返回时,定义在函数中的局部变量就不复存在了, 那为什么内部的函数可以访问外部函数的变量?即使外部函数执行完,外部函数的变量也能和内部函数一同存在?</p>
</blockquote>
<p><strong>下面以JavaScript闭包实现举例。</strong></p>
<pre><code>javascript</code><code><script type="text/javascript" language="javascript">
function a(){
var i=0;
return function b() {
alert(++i);
}
}
c=a();
c();
</script>
</code></pre>
<p>上面是一个使用闭包的简单示例,代码执行完毕后,函数对象并不会被垃圾回收机制回收<sup><a>1</a></sup>,函数内的临时变量能够得以长期存在,而这个变量只能够被闭包函数修改,在外部是无法访问和修改的。(这个其实前面已经说过,这里有点啰嗦)</p>
<p>JavaScript中将作用域链描述为一个<strong>对象列表</strong>,<strong>不是绑定的栈</strong>。每次调用JavaScript函数的时候(函数也是对象),都会为之创建一个新的对象用来保存局部变量,把这个对象添加至作用域链中。</p>
<p>每个函数关联都有一个执行上下文场景(Execution Context) ,然后执行环境会创建一个活动对象(call object),该对象包含了两个重要组件,环境记录,和外部引用(指针)。环境记录包含了函数内部声明的局部变量和参数变量,外部引用指向了外部函数对象的上下文执行场景。这样的数据结构就构成了一个单向的链表,每个引用都指向外层的上下文场景。最后形成如下图的结构:<br><img src="/img/bVmA56" alt="图片描述"><br>
注:此图盗用,此图网站已不能打开。</p>
<p><strong>如上图和代码所示</strong>:a返回函数b的引用给c,函数b的作用域链包含了对函数a的活动对象的引用,也就是说b可以访问到a中定义的所有变量和函数。函数b被c引用,函数b又依赖函数a,因此函数a在返回后不会被GC回收。</p>
<h3>三 闭包的作用</h3>
<p>以上述代码为例:<br>
- <strong>保护函数内的变量安全</strong>:函数a中i只有函数b才能访问,而无法通过其他途径访问到,因此保护了i的安全;<br>
- <strong>在内存中维持一个变量</strong>:函数a中i的一直存在于内存中,因此每次执行c(),都会给i自加1;<br>
- <strong>通过保护变量的安全实现JS私有属性和私有方法</strong>(不能被外部访问)。</p>
<p><strong>Singleton 单件:</strong>(盗用<a rel="nofollow" href="http://coolshell.cn/articles/6731.html">理解Javascript的闭包</a>的例子)</p>
<pre><code>var singleton = function () {
var privateVariable;
function privateFunction(x) {
...privateVariable...
}
return {
firstMethod: function (a, b) {
...privateVariable...
},
secondMethod: function (c) {
...privateFunction()...
}
};
}();
</code></pre>
<p>这个单件通过闭包来实现。通过闭包完成了私有的成员和方法的封装。匿名主函数返回一个对象。对象包含了两个方法,方法1可以方法私有变量,方法2访问内部私有函数。需要注意的地方是匿名主函数结束的地方的'()’,如果没有这个'()’就不能产生单件。因为匿名函数只能返回了唯一的对象,而且不能被其他地方调用。这个就是利用闭包产生单件的方法。</p>
<h3>四 概念混淆之匿名函数</h3>
<p>匿名函数指的是没有函数名称的函数,它和闭包没有关系,只是闭包中函数可以通过匿名函数编写,当然匿名函数不止会出现在闭包中。<br>
如下面一个javascript代码示例:</p>
<pre><code>var f = function(name){
//函数体
};
</code></pre>
<p>函数表达式是创建了一个匿名函数,然后将匿名函数赋值给一个变量f。</p>
<h2>参考</h2>
<p><a rel="nofollow" href="http://coolshell.cn/articles/6731.html">理解Javascript的闭包</a><br><a rel="nofollow" href="http://blog.codinglabs.org/articles/closure-perspective-of-abstract-mathematic-and-functional-language.html">闭包漫谈(从抽象代数及函数式编程角度)</a><br><a rel="nofollow" href="https://zh.wikipedia.org/wiki/%E9%97%AD%E5%8C%85_%28%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6%29">维基百科-闭包</a><br><a rel="nofollow" href="https://zh.wikipedia.org/wiki/%CE%9B%E6%BC%94%E7%AE%97">维基百科-λ演算</a><br><a rel="nofollow" href="http://liujiacai.net/blog/2014/10/12/lambda-calculus-introduction/">编程语言的基石——Lambda calculus</a></p>
<h2>脚注</h2>
<hr>
<ol>
<li>
<p><strong>注:Javascript的垃圾回收机制:</strong><br>
在Javascript中,如果一个对象不再被引用,那么这个对象就会被GC回收。如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。因为函数a被b引用,b又被a外的c引用,这就是为什么函数a执行后不会被回收的原因。 <a>↩</a></p>
</li>
</ol>
Gradle构建Android项目
https://segmentfault.com/a/1190000002910311
2015-06-16T16:33:47+08:00
2015-06-16T16:33:47+08:00
人云思云
https://segmentfault.com/u/rysy
3
<h2>签名</h2>
<p>gradle本身支持直接签名,只需要在releas部分添加如下代码即可</p>
<pre><code>signingConfigs {
debug {
}
release {
storeFile file("../yourapp.keystore")
storePassword "your password"
keyAlias "your alias"
keyPassword "your password"
}
}
buildTypes {
debug {
minifyEnabled false
zipAlignEnabled false
shrinkResources false
signingConfig signingConfigs.debug
}
release {
minifyEnabled true//混淆编译
zipAlignEnabled true
//移除无用的资源文件
shrinkResources true
signingConfig signingConfigs.release
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
</code></pre>
<p>一般填上上面的代码即可执行签名,但是这种方式不太安全,建议不要在build.gradle文件中写上签名文件的密码,因为build.gradle文件一般都会集成到代码的版本控制中,这样所有人都会有签名文件的密码。</p>
<p>所以应该把签名文件的密码隔离起来,写到一个配置文件中,此配置文件不包含在代码版本控制中,这样其他开发者就不会知道签名文件的密码。</p>
<p>gradle配置文件一般以.properties结束,我们先新建一个signing.properties文件,内容如下:</p>
<pre><code>STORE_FILE=yourapp.keystore
STORE_PASSWORD=your password
KEY_ALIAS=your alias
KEY_PASSWORD=your password
</code></pre>
<blockquote>
<p><strong><code>注意没有字符串双引号""</code></strong></p>
</blockquote>
<p>接下在guild.gradle文件中读取signing.properties配置文件,读取的代码如下:</p>
<pre><code>File propFile = file('signing.properties');
if (propFile.exists()) {
def Properties props = new Properties()
props.load(new FileInputStream(propFile))
if (props.containsKey('STORE_FILE') && props.containsKey('STORE_PASSWORD') &&
props.containsKey('KEY_ALIAS') && props.containsKey('KEY_PASSWORD')) {
android.signingConfigs.release.storeFile = file(props['STORE_FILE'])
android.signingConfigs.release.storePassword = props['STORE_PASSWORD']
android.signingConfigs.release.keyAlias = props['KEY_ALIAS']
android.signingConfigs.release.keyPassword = props['KEY_PASSWORD']
} else {
android.buildTypes.release.signingConfig = null
}
} else {
android.buildTypes.release.signingConfig = null
}
</code></pre>
<p>代码很简单,就是读取文件,然后拿到签名需要的四个变量值分别赋值即可。</p>
<h2>多渠道打包</h2>
<p><a rel="nofollow" href="http://tech.meituan.com/mt-apk-packaging.html">美团Android自动化之旅—生成渠道包</a></p>
<p>由于国内Android市场众多渠道,为了统计每个渠道的下载及其它数据统计,就需要我们针对每个渠道单独打包。<br>
gradle的多渠道打包很简单,因为gradle已经帮我们做好了很多基础功能。</p>
<p>下面以友盟统计为例说明,一般友盟统计在AndroidManifest.xml里面会有这么一段声明:</p>
<pre><code><meta-data
android:name="UMENG_CHANNEL"
android:value="CHANNEL_ID" />
</code></pre>
<p>其中<strong>CHANNEL_ID</strong>就是友盟的渠道标示,多渠道的实现一般就是通过修改<strong>CHANNEL_ID</strong>值来实现的。</p>
<p><strong>接下来将一步一步来实现多渠道版本打包。</strong></p>
<p>1.在AndroidManifest.xml里配置PlaceHolder,用与在build.gradle文件中来替换成自己想要设置的值</p>
<pre><code><meta-data
android:name="UMENG_CHANNEL"
android:value="${UMENG_CHANNEL_VALUE}" />
</code></pre>
<p>2.在build.gradle设置productFlavors,修改PlaceHolder的值</p>
<pre><code>productFlavors {
playStore {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "playStore"]
}
miui {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "miui"]
}
wandoujia {
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"]
}
}
</code></pre>
<p>或者批量修改</p>
<pre><code>productFlavors {
playStore {}
miui {}
wandoujia {}
}
//批量处理
productFlavors.all {
flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
</code></pre>
<p>按照上面两步即可编译打多渠道包了,命令是 <code>./gradlew assembleRelease</code>,可以打包所有的多渠道包。</p>
<p>通过下面这张图可以看到gradle可以执行的task。<br><img alt="Alt text"><br>
如果只是想打单渠道包,则执行相应的task即可,如<code>gradle assemblePalyStoreRelease</code>就是打PlayStore渠道的Release版本。</p>
<p>3.如果希望可以对最终的文件名做修改,如需要针对不同的需求生成不同的文件。而修改文件名也很简单,参考以下代码即可实现</p>
<pre><code>def releaseTime() {
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}
android{
applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith('.apk')) {
File outputDirectory = new File(outputFile.parent);
def fileName
if (variant.buildType.name == "release") {
fileName = "app_v${defaultConfig.versionName}_${releaseTime()}_${variant.productFlavors[0].name}.apk"
} else {
fileName = "app_v${defaultConfig.versionName}_${packageTime()}_debug.apk"
}
output.outputFile = new File(outputDirectory, fileName)
}
}
}
}
</code></pre>
<blockquote>
<p>此方法有一定局限性,就是渠道包多了之后编译花费的时间会很长,这里推<a rel="nofollow" href="http://tech.meituan.com/mt-apk-packaging.html">荐美团打包</a>的第三种方法。</p>
</blockquote>
<h2>buildConfigField自定义配置</h2>
<p>大家可能会遇到下面这种情况,就是Beta版本服务器和Release版本服务器通常不在一台服务器上,而测试希望可以同时发布两个服务器的版本用于测试,这个时候我们就需要修改代码,然后一个一个老老实实的发包。gradle提供buildConfigField配合多渠道打不同服务器版本的方法。<br>
其实用法很简单,首先在相应的节点加上定义,比如:</p>
<pre><code>buildTypes {
debug {
buildConfigField "boolean", "LOG_DEBUG", "true"//是否输出LOG信息
buildConfigField "String", "API_HOST", "\"http://api.test.com\""//API Host
}
}
</code></pre>
<p>然后在代码中通过BuildConfig.LOG_DEBUG或者BuildConfig.API_HOST调用即可。</p>
<h2>dex突破65535的限制</h2>
<p>随着项目的一天天变大,慢慢的都会遇到单个dex最多65535个方法数的瓶颈,如果是ANT构建的项目就会比较麻烦,但是Gradle已经帮我们处理好了,而添加的方法也很简单,总共就分三步 :<br>
1.首先是在defaultConfig节点使能多DEX功能</p>
<pre><code>android {
defaultConfig {
// dex突破65535的限制
multiDexEnabled true
}
}
</code></pre>
<p>2.然后就是引入multidex库文件</p>
<pre><code>dependencies {
compile 'com.android.support:multidex:1.0.0'
}
</code></pre>
<p>3.最后就是你的AppApplication继承一下MultiDexApplication即可。</p>
<h2>完整的gradle脚本</h2>
<p>一份项目中使用的完整的gradle文件配置</p>
<pre><code>// 声明是Android程序
apply plugin: 'com.android.application'
// 定义一个打包时间
def releaseTime() {
return new Date().format("yyyy-MM-dd", TimeZone.getTimeZone("UTC"))
}
android {
// 编译SDK的版本
compileSdkVersion 21
// build tools的版本
buildToolsVersion '21.1.2'
defaultConfig {
// 应用的包名
applicationId "com.**.*"
minSdkVersion 14
targetSdkVersion 21
versionCode 1
versionName "1.0"
// dex突破65535的限制
multiDexEnabled true
// 默认是umeng的渠道
manifestPlaceholders = [UMENG_CHANNEL_VALUE: "umeng"]
}
// 移除lint检查的error
lintOptions {
abortOnError false
}
//签名配置
signingConfigs {
debug {
// No debug config
}
release {
storeFile file("../yourapp.keystore")
storePassword "your password"
keyAlias "your alias"
keyPassword "your password"
}
}
buildTypes {
debug {
// buildConfigField 自定义配置默认值
buildConfigField "boolean", "LOG_DEBUG", "true"
buildConfigField "String", "API_HOST", "\"http://api.test.com\""//API Hos
versionNameSuffix "-debug"
minifyEnabled false
//是否zip对齐
zipAlignEnabled false
shrinkResources false
signingConfig signingConfigs.debug
}
release {
// buildConfigField 自定义配置默认值
buildConfigField "boolean", "LOG_DEBUG", "false"
buildConfigField "String", "API_HOST", "\"http://api.release.com\""//API Host
//// 是否进行混淆
minifyEnabled true
zipAlignEnabled true
// 移除无用的resource文件
shrinkResources true
//混淆规则文件
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith('.apk')) {
// 输出apk名称为boohee_v1.0_2015-06-15_wandoujia.apk
def fileName = "boohee_v${defaultConfig.versionName}_${releaseTime()}_${variant.productFlavors[0].name}.apk"
output.outputFile = new File(outputFile.parent, fileName)
}
}
}
}
}
// 友盟多渠道打包
productFlavors {
wandoujia {}
_360 {}
baidu {}
xiaomi {}
tencent {}
taobao {}
...
}
productFlavors.all { flavor ->
flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name]
}
}
dependencies {
// 编译libs目录下的所有jar包
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:support-v4:21.0.3'
compile 'com.jakewharton:butterknife:6.0.0'
...
}
</code></pre>
<h2>其他整理</h2>
<p><a rel="nofollow" href="http://stormzhang.com/devtools/2015/01/05/android-studio-tutorial5/">Android Studio系列教程五--Gradle命令详解与导入第三方包</a><br><a rel="nofollow" href="http://blog.csdn.net/hymking/article/details/44041693">ZipAlign对apk进行优化</a></p>
<h4>参考</h4>
<p><a rel="nofollow" href="http://frank-zhu.github.io/android/2015/06/15/android-release_app_build_gradle/">安卓集成发布详解(二)</a><br><a rel="nofollow" href="http://rinvay.github.io/android/2015/03/26/Gradle-Plugin-User-Guide%28Translation%29/#106">Gradle插件用户指南(译)</a><br><a rel="nofollow" href="http://stormzhang.com/devtools/2015/01/15/android-studio-tutorial6/">Android Studio系列教程六--Gradle多渠道打包</a><br><a rel="nofollow" href="http://blog.isming.me/2014/05/20/android4gradle/">使用Gradle构建Android项目</a><br><a rel="nofollow" href="http://blog.isming.me/2014/11/21/use-gradle-new/">使用gradle构建android项目(续)</a></p>
Android代码混淆ProGuard
https://segmentfault.com/a/1190000002910305
2015-06-16T16:30:07+08:00
2015-06-16T16:30:07+08:00
人云思云
https://segmentfault.com/u/rysy
2
<blockquote>
<p>Android ADT主要通过ProGuard工具来提供代码混淆.</p>
</blockquote>
<h2>1. ProGuard是什么</h2>
<p>ProGuard是一个工具,用来混淆和优化Java代码。<br>
工作方式:移除无效的代码,将代码中的类名、函数名替换为晦涩难懂的名字。<br><code>注意</code>:它只能混淆Java代码,Android工程中Native代码,资源文件(图片、xml),它是无法混淆的。</p>
<h2>2. 如何开启ProGuard</h2>
<p>修改Android工程根目录下的project.properties文件,把proguard.config=....这一行前面的注释“#”去掉。</p>
<pre><code>proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
</code></pre>
<p>系统默认的配置已经涵盖了许多通用的细节,如果你还有额外的配置,可以添加在 <code>proguard-project.txt</code> 文件中。</p>
<p><code>注意</code>: 只有在生成release版本的apk时,混淆配置才会起作用,debug版本的apk不会进行混淆。</p>
<h2>3. 哪些内容需要手动配置</h2>
<p>(1) 只在 AndroidManifest.xml 引用的类<br>
(2) 通过JNI回调方式被调用的函数<br>
(3) 运行时动态调用的函数或者成员变量<br>
(4) 当然,如果你不确定哪些需要手动配置,可以以默认的配置生成程序,当运行中发现ClassNotFoundException异常时,即可找到哪个类不该被混淆。</p>
<h2>4. 手动配置的规则</h2>
<p>EXAMPLE:假设Android工程中有一个接口和一个类:</p>
<pre><code>package com.ticktick.example;
public interface TestInterface {
public void test();
}
public class Test {
private String mTestString;
private final int mMinValue;
private final int mMaxValue;
public Test( int min, int max){
mMinValue = min;
mMaxValue = max;
}
public int getMinValue() {
return mMinValue;
}
public int getMaxValue() {
return mMaxValue;
}
public void setTestString(String testStr ) {
mTestString = testStr;
}
}
</code></pre>
<h4>不混淆某个类</h4>
<p>例如:不混淆Test类:</p>
<pre><code>-keep public class com.ticktick.example.Test
</code></pre>
<h4>不混淆某个类的构造函数</h4>
<p>例如:不混淆Test类的构造函数:</p>
<pre><code>-keepclassmembers class
com.ticktick.example.Test {
public <init>(int,int);
}
</code></pre>
<h4>不混淆某个包所有的类或指定的类</h4>
<p>例如,不混淆package com.ticktick.example下的所有类/接口</p>
<pre><code>-keep class com.ticktick.example.** { * ; }
</code></pre>
<p>例如,不混淆com.ticktick.example.Test类</p>
<pre><code>-keep class com.ticktick.example.Test { * ; }
</code></pre>
<h4>不混淆某个类的特定的函数</h4>
<p>例如:不混淆com.ticktick.example.Test类的setTestString函数</p>
<pre><code>-keepclassmembers class
com.ticktick.example.Test {
public void setTestString(java.lang.String);
}
</code></pre>
<h4>不混淆某个类的子类,某个接口的实现</h4>
<p>例如:不混淆com.ticktick.example.Test类的子类</p>
<pre><code>-keep public class * extends com.ticktick.example.Test
</code></pre>
<p>例如:不混淆com.ticktick.example.TestInterface的实现</p>
<pre><code>-keep class * implementscom.ticktick.example.TestInterface {
public static final com.ticktick.example.TestInterface$Creator *;
}
</code></pre>
<h4>添加第三方依赖包</h4>
<p>例如:添加android-support-v4.jar依赖包</p>
<pre><code>-libraryjars ./libs/android-support-v4.jar #声明lib文件
-dontwarn android.support.v4.**{*;} #不提示警告
-keep class android.support.v4.**{*;} #不进行混淆
-keep interface android.support.v4.**{*;}
</code></pre>
<p><code>注意</code>: 需要添加dontwarn,因为默认情况下proguard会检查每一个引用是否正确,<br>
但是第三方库里往往有些不会用到的类,没有正确引用,所以如果不配置的话,系统会报错。</p>
<h2>5. 混淆后的调试信息解析</h2>
<blockquote>
<p>当代码混淆之后,输出的Log信息也会带有混淆内容,比如函数名和类名会被替换为晦涩难懂的名字,而与代码中的不一致。<br>
因此,ProGuard工具还提供了恢复混淆内容的工具和文件。<br>
当你开启了ProGuard混淆后,每次生成release版的apk时,Andriod工程的根目录下会对应生成一个proguard文件夹,该文件夹下的mapping.txt文件记录了混淆后的名字与混淆前的名字的对应关系,通过该文件,我们反向得到恢复后的Log信息。<br>
假设Log文件名为log.txt,则恢复混淆的命令为:<br>
$retrace.sh -verbose mapping.txt log.txt<br>
注1:retrace.sh命令位于 /tools/proguard/目录下<br>
注2:你需要保存每一个release版本的mapping.txt,因为每一次release的混淆结果和映射关系都不一样。</p>
</blockquote>
<h2>6. 需要做混淆保护的内容</h2>
<h4>一般四大组件都会默认不被混淆</h4>
<pre><code>-keep public class * extends Android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class com.android.vending.licensing.ILicensingService
</code></pre>
<h4>自定义组件的类也要设置为不被混淆,否则找不到控件</h4>
<p>方案一、直接保护view的子类,因为自定义控件都是继承view</p>
<pre><code>-keep public class * extends android.app.Dialog
-keep public class * extends android.view
</code></pre>
<p>方案二、具体保护某个自定义控件</p>
<pre><code>-keep public class com.viewpagerindicator.TitlePageIndicator
</code></pre>
<h4>被调用的第三方jar包的类等</h4>
<p>如果你使用了第三方的包,你需要使用一下配置,让ProGuard知道库中的一些类并不是在所有的API中可用:</p>
<pre><code>xml</code><code>-libraryjars libs/roboguice-2.0.jar
-dontwarn roboguice.**
</code></pre>
<h2>7.需要特别处理的第三方包</h2>
<ul>
<li>
<a rel="nofollow" href="https://code.google.com/p/google-gson/source/browse/trunk/examples/android-proguard-example/proguard.cfg?r=878">GSON</a><br>
(<a rel="nofollow" href="http://google-gson.googlecode.com/svn/trunk/examples/android-proguard-example/proguard.cfg">http://google-gson.googlecode.com/svn/trunk/examples/android-proguard-example/proguard.cfg</a>)</li>
<li><a rel="nofollow" href="http://jakewharton.github.io/butterknife/">butterknife</a></li>
<li>dontwarn butterknife.internal.**</li>
<li>keep class **$$ViewInjector { *; }</li>
<li>keepnames class * { @butterknife.InjectView *;}</li>
</ul>