gtest框架
解析gtest框架运行机制
1、前言
Google test是一款开源的白盒单元测试框架,据说目前在Google内部已在几千个项目中应用了基于该框架的白盒测试。
最近的工作是在搞一个基于gtest框架搭建的自动化白盒测试项目,该项目上线也有一段时间了,目前来说效果还是挺不错的。
侯捷先生在《STL源码剖析》中说过一句话:”会用STL,是一种档次。对STL原理有所了解,又是一个档次。追踪过STL源码又是一个档次。第三种档次的人用起STL来,虎虎生风之势绝非第一档次的人能够望其项背。“
我认为使用一种框架时也是一样,只有当你知道框架内部是如何运行的,不仅知其然,还知其所以然,才能避免一些坑,使测试框架用起来更效率。
就拿平常项目中用的最简单的一个测试demo(test_foo.cpp)来说吧
1 int foo(int a, int b)
2 {
3 return a + b;
4 }
5
6 class TestWidget : public testing::Environment
7 {
8 public:
9 virtual void SetUp();
10 virtual void TearDown();
11 };
12
13 TEST(Test_foo, test_normal)
14 {
15 EXPECT_EQ(2, foo(1, 1));
16 }
17
18 int main(int argc, char const *argv[])
19 {
20 testing::AddGlobalTestEnvironment(new TestSysdbg);
21 testing::InitGoogleTest(&argc, argv);
22 return RUN_ALL_TESTS();
23 return 0;
24 }
你可知道gtest是如何调用被测接口,如何输出测试结果的吗?本文主要解答这个问题。
2、解析
2.1、从预处理开始
需要提到的是Gtest中用到了许多的宏技巧以及c++的模板元技巧。
先不看源码中TEST宏的定义,直接用下面指令单独调用预处理器对源文件进行预处理:
cpp test_foo.cpp test_foo.i –I/ gtest/gtest-1.6/
打开生成的经过预处理的文件test_foo.i
1 class Test_foo_test_normal_Test : public ::testing::Test
2 {
3 public:
4 Test_foo_test_normal_Test() {}
5
6 private:
7 virtual void TestBody();
8 public:
9 virtual void SetUp();
10 virtual void TearDown();
11 };
12
13 class Test_foo_test_normal_Test : public ::testing::Test
14 {
15 public:
16 Test_foo_test_normal_Test() {}
17
18 private:
19 virtual void TestBody();
20 static ::testing::TestInfo* const test_info_ __attribute__ ((unused));
21 Test_foo_test_normal_Test(Test_foo_test_normal_Test const &);
22 void operator=(Test_foo_test_normal_Test const &);
23 };
24
25 ::testing::TestInfo* const Test_foo_test_normal_Test
26 ::test_info_ =
27 ::testing::internal::MakeAndRegisterTestInfo(
28 "Test_foo", "test_normal", __null, __null,
29 (::testing::internal::GetTestTypeId()),
30 ::testing::Test::SetUpTestCase,
31 ::testing::Test::TearDownTestCase,
32 new ::testing::internal::TestFactoryImpl<Test_foo_test_normal_Test>);
33
34 void Test_foo_test_normal_Test::TestBody()
35 {
36 switch (0)
37 case 0:
38 default:
39 if (const ::testing::AssertionResult gtest_ar =
40 (::testing::internal::
41 EqHelper<(sizeof(::testing::internal::IsNullLiteralHelper(2)) == 1) >
42 ::Compare("2", "foo(1, 1)", 2, foo(1, 1)))) ;
43 else
44 ::testing::internal::AssertHelper(::testing::TestPartResult::kNonFatalFailure,
45 "test_foo.cpp", 17, gtest_ar.failure_message()) = ::testing::Message();
46 }
47
48 int main(int argc, char *argv[])
49 {
50 testing::AddGlobalTestEnvironment(new TestWidget);
51 testing::InitGoogleTest(&argc, argv);
52 return (::testing::UnitTest::GetInstance()->Run());
53 return 0;
54 }
我们可以看到TEST宏经过预处理器处理后展开为:
1、定义了一个继承自::testing::test类的新类Test_foo_test_normal_Test,该类的名字为TEST宏两个形参的拼接而成。
2、TEST宏中的测试代码被展开并定义为生成类的成员函数TestBody的函数体。
3、生成类的静态数据成员test_info_被初始化为函MakeAndRegisterTestInfo的返回值。具体意义后面介绍。
2.2、MakeAndRegisterTestInfo函数
从上面来看MakeAndRegisterTestInfo函数是一个比较关键的函数了,从字面意思上看就是生成并注册该测试案例的信息,在头文件gtest.cc中可以找到关于它的定义,他是一个testing命名空间中的嵌套命名空间internal中的非成员函数:
1 TestInfo* MakeAndRegisterTestInfo(
2 const char* test_case_name, const char* name,
3 const char* type_param,
4 const char* value_param,
5 TypeId fixture_class_id,
6 SetUpTestCaseFunc set_up_tc,
7 TearDownTestCaseFunc tear_down_tc,
8 TestFactoryBase* factory) {
9 TestInfo* const test_info =
10 new TestInfo(test_case_name, name, type_param, value_param,
11 fixture_class_id, factory);
12 GetUnitTestImpl()->AddTestInfo(set_up_tc, tear_down_tc, test_info);
13 return test_info;
14 }
其中形参的意义如下:
test_case_name:测试套名称,即TEST宏中的第一个形参。
name:测试案例名称。
type_param:测试套的附加信息。默认为无
value_param:测试案例的附加信息。默认为无
fixture_class_id:test fixture类的id
set_up_tc :函数指针,指向函数SetUpTestCaseFunc
tear_down_tc:函数指针,指向函数TearDownTestCaseFunc
factory:指向工厂对象的指针,该工厂对象创建上面TEST宏生成的测试类的对象
我们看到在MakeAndRegisterTestInfo函数体中定义了一个TestInfo对象,该对象包含了一个TEST宏中标识的测试案例的测试套名称、测试案例名称、测试套附加信息、测试案例附加信息、创建测试案例类对象的工厂对象的指针这些信息。
下面大家可能就会比较好奇所谓的工厂对象,可以在gtest-internal.h中找带它的定义
1 template <class TestClass>
2 class TestFactoryImpl : public TestFactoryBase {
3 public:
4 virtual Test* CreateTest() { return new TestClass; }
5 };
TestFactoryImpl类是一个模板类,它的作用就是单纯的生产对应于模板形参类型的测试案例对象。因为模板的存在也大大简化了代码,否则可能就要写无数个TestFactoryImpl类了,呵呵。
1 GetUnitTestImpl()->AddTestInfo(set_up_tc, tear_down_tc, test_info);
乍一看似乎是对test_info对象的一些熟悉信息进行设置。究竟是怎么样呢?源码面前,了无秘密,我们还是得去找到它的源码,在gtest-internal-inl中可以找到它的定义
1 inline UnitTestImpl* GetUnitTestImpl() {
2 return UnitTest::GetInstance()->impl();
3 }
可以看到它的实现也是非常简单,关键还是在UnitTest类的成员函数GetInstance和返回类型的成员函数impl,我们继续追踪下去
1 class GTEST_API_ UnitTest {
2 public:
3 // Gets the singleton UnitTest object. The first time this method
4 // is called, a UnitTest object is constructed and returned.
5 // Consecutive calls will return the same object.
6 static UnitTest* GetInstance();
7
8 internal::UnitTestImpl* impl() { return impl_; }
9 const internal::UnitTestImpl* impl() const { return impl_; }
10
11 private:
12 mutable internal::Mutex mutex_;
13 internal::UnitTestImpl* impl_;
14 }
15
16 UnitTest * UnitTest::GetInstance() {
17 #if (_MSC_VER == 1310 && !defined(_DEBUG)) || defined(__BORLANDC__)
18 static UnitTest* const instance = new UnitTest;
19 return instance;
20 #else
21 static UnitTest instance;
22 return &instance;
23 }
根据代码和注释可知GetInstance是Unitest类的成员函数,它仅仅是生成一个静态的UniTest对象然后返回。实际上这么做是为了实现UniTest类的单例(Singleton)实例。而impl只是单纯的返回UniTest的UnitTestImpl类型的指针数据成员impl_。
再联系之前的代码,通过UnitTestImpl类的AddTestInfo设置Test_Info类对象的信息。其实绕了一圈,最终就是通过AddTestInfo设置Test_info类对象的信息,自然地,我们需要知道AddTestInfo的实现啦:
1 void AddTestInfo(Test::SetUpTestCaseFunc set_up_tc,
2 Test::TearDownTestCaseFunc tear_down_tc,
3 TestInfo* test_info) {
4 GetTestCase(test_info->test_case_name(),
5 test_info->type_param(),
6 set_up_tc,
7 tear_down_tc)->AddTestInfo(test_info);
8 }
我们看到是通过GetTestCase函数实现的
1 TestCase* UnitTestImpl::GetTestCase(const char* test_case_name,
2 const char* type_param,
3 Test::SetUpTestCaseFunc set_up_tc,
4 Test::TearDownTestCaseFunc tear_down_tc) {
5 // Can we find a TestCase with the given name?
6 const std::vector<TestCase*>::const_iterator test_case =
7 std::find_if(test_cases_.begin(), test_cases_.end(),
8 TestCaseNameIs(test_case_name));
9
10 if (test_case != test_cases_.end())
11 return *test_case;
12
13 // No. Let's create one.
14 TestCase* const new_test_case =
15 new TestCase(test_case_name, type_param, set_up_tc, tear_down_tc);
16
17 // Is this a death test case?
18 if (internal::UnitTestOptions::MatchesFilter(String(test_case_name),
19 kDeathTestCaseFilter)) {
20 // Yes. Inserts the test case after the last death test case
21 // defined so far. This only works when the test cases haven't
22 // been shuffled. Otherwise we may end up running a death test
23 // after a non-death test.
24 ++last_death_test_case_;
25 test_cases_.insert(test_cases_.begin() + last_death_test_case_,
26 new_test_case);
27 } else {
28 // No. Appends to the end of the list.
29 test_cases_.push_back(new_test_case);
30 }
31
32 test_case_indices_.push_back(static_cast<int>(test_case_indices_.size()));
33 return new_test_case;
34 }
我们看到其实并不是一开始猜测的设置Test_Info对象的信息,而是判断包含Test_info对象中的测试套名称、测试案例名称等信息的TestCase对象的指针是否在一个vector向量中,若存在就返回这个指针;若不存在就把创建一个包含这些信息的TestCase对象的指针加入到vector向量中,并返回这个指针。
至于vector向量test_cases_是UnitTestImpl中的私有数据成员,在这个向量中存放了整个测试项目中所有包含测试套、测试案例等信息的TestCase对象的指针。
紧接着我们看到从GetTestCase返回的TestCase指针调用TestCase类中的成员函数AddTestInfo,我们可以找到它的定义如下:
1 void TestCase::AddTestInfo(TestInfo * test_info) {
2 test_info_list_.push_back(test_info);
3 test_indices_.push_back(static_cast<int>(test_indices_.size()));
4 }
我们看到,调用这个函数的目的是在于将Test_info对象添加到test_info_list_中,而test_info_list_是类TestCase中的私有数据成员,它也是一个vector向量。原型为
1 std::vector<TestInfo*> test_info_list_;
该向量保存着整个项目中所有包含测试案例对象各种信息的Test_Info对象的指针。
而test_indices_也是类TestCase中的私有数据成员,保存着test_info_list中每个元素的索引号。它仍然是一个vector向量,原型为
1 std::vector<int> test_indices_;
2.3、TEST宏
此时,我们再来看看TEST宏的具体定义实现:
1 #if !GTEST_DONT_DEFINE_TEST
2 # define TEST(test_case_name, test_name) GTEST_TEST(test_case_name, test_name)
3 #endif
4
5 #define GTEST_TEST(test_case_name, test_name)\
6 GTEST_TEST_(test_case_name, test_name, \
7 ::testing::Test, ::testing::internal::GetTestTypeId())
8
9 #define TEST_F(test_fixture, test_name)\
10 GTEST_TEST_(test_fixture, test_name, test_fixture, \
11 ::testing::internal::GetTypeId<test_fixture>())
可以看到,TEST宏和事件机制对于的TEST_F宏都是调用了GTEST_TEST_宏,我们再追踪这个宏的定义
1 #define GTEST_TEST_(test_case_name, test_name, parent_class, parent_id)\
2 class GTEST_TEST_CLASS_NAME_(test_case_name, test_name) : public parent_class {\
3 public:\
4 GTEST_TEST_CLASS_NAME_(test_case_name, test_name)() {}\
5 private:\
6 virtual void TestBody();\
7 static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;\
8 GTEST_DISALLOW_COPY_AND_ASSIGN_(\
9 GTEST_TEST_CLASS_NAME_(test_case_name, test_name));\
10 };\
11 \
12 ::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_case_name, test_name)\
13 ::test_info_ =\
14 ::testing::internal::MakeAndRegisterTestInfo(\
15 #test_case_name, #test_name, NULL, NULL, \
16 (parent_id), \
17 parent_class::SetUpTestCase, \
18 parent_class::TearDownTestCase, \
19 new ::testing::internal::TestFactoryImpl<\
20 GTEST_TEST_CLASS_NAME_(test_case_name, test_name)>);\
21 void GTEST_TEST_CLASS_NAME_(test_case_name, test_name)::TestBody()
我们终于看到了在预处理展开中得到的案例类的定义和注册案例类对象信息的定义代码啦。唯一的疑问在于类的名字是GTEST_TEST_CLASS_NAME_,从字面意思可以指定这宏就是获得类的名字
1 #define GTEST_TEST_CLASS_NAME_(test_case_name, test_name) \
2 test_case_name##_##test_name##_Test
可以看到宏GTEST_TEST_CLASS_NAME的功能就是把两个参数拼接为一个参数。
2.4、RUN_ALL_TESTS宏
我们的测试程序就是从main函数中的RUN_ALL_TEST的调用开始的,在gtest.h中可以找到该宏的定义
1 #define RUN_ALL_TESTS()\
2 (::testing::UnitTest::GetInstance()->Run())
RUN_ALL_TESTS就是简单的调用UnitTest的成员函数GetInstance,我们知道GetInstance就是返回一个单例(Singleton)UnitTest对象,该对象调用成员函数Run
1 int UnitTest::Run() {
2 impl()->set_catch_exceptions(GTEST_FLAG(catch_exceptions));
3
4 return internal::HandleExceptionsInMethodIfSupported(
5 impl(),
6 &internal::UnitTestImpl::RunAllTests,
7 "auxiliary test code (environments or event listeners)") ? 0 : 1;
8 }
Run函数也是简单的调用HandleExceptionsInMethodIfSupported函数,追踪它的实现
1 template <class T, typename Result>
2 Result HandleExceptionsInMethodIfSupported(
3 T* object, Result (T::*method)(), const char* location) {
4
5 if (internal::GetUnitTestImpl()->catch_exceptions()) {
6 ...... //异常处理省略
7 } else {
8 return (object->*method)();
9 }
10 }
我们看到HandleExceptionsInMethodIfSupported是一个模板函数,他的模板形参具现化为调用它的UnitTestImpl和int,也就是T = UnitTestImpl, Result = int。在函数体里调用UnitTestImpl类的成员函数RunAllTests
1 bool UnitTestImpl::RunAllTests() {
2 ......
3 const TimeInMillis start = GetTimeInMillis(); //开始计时
4 if (has_tests_to_run && GTEST_FLAG(shuffle)) {
5 random()->Reseed(random_seed_);
6 ShuffleTests();
7 }
8 repeater->OnTestIterationStart(*parent_, i);
9
10 if (has_tests_to_run) {
11 //初始化全局的SetUp事件
12 repeater->OnEnvironmentsSetUpStart(*parent_);
13 //顺序遍历注册全局SetUp事件
14 ForEach(environments_, SetUpEnvironment);
15 //初始化全局TearDown事件
16 repeater->OnEnvironmentsSetUpEnd(*parent_);
17 //
18 // set-up.
19 if (!Test::HasFatalFailure()) {
20 for (int test_index = 0; test_index < total_test_case_count();
21 test_index++) {
22 GetMutableTestCase(test_index)->Run(); //TestCase::Run
23 }
24 }
25 // 反向遍历取消所有全局事件.
26 repeater->OnEnvironmentsTearDownStart(*parent_);
27 std::for_each(environments_.rbegin(), environments_.rend(),
28 TearDownEnvironment);
29 repeater->OnEnvironmentsTearDownEnd(*parent_);
30 }
31 elapsed_time_ = GetTimeInMillis() - start; //停止计时
32 ......
33 }
如上面代码所示,UnitTestImpl::RunAllTests主要进行全局事件的初始化,以及变量注册。而真正的执行部分在于调用GetMutableTestCase
1 TestCase* UnitTest::GetMutableTestCase(int i) {
2 return impl()->GetMutableTestCase(i); //impl返回UnitTestImpl类型指针
3 }
4
5 TestCase* UnitTestImpl:: GetMutableTestCase(int i) {
6 const int index = GetElementOr(test_case_indices_, i, -1);
7 return index < 0 ? NULL : test_cases_[index];
8 }
经过两次调用返回vector向量test_cases_中的元素,它的元素类型为TestCase类型。然后调用TestCase::Run
1 void TestCase::Run() {
2 ...... //省略
3 const internal::TimeInMillis start = internal::GetTimeInMillis();
4 for (int i = 0; i < total_test_count(); i++) {
5 GetMutableTestInfo(i)->Run(); //调用TestCase::GetMutableTestInfo
6 } //以及Test_Info::Run
7 ...... //省略
8 }
9
10 TestInfo* TestCase::GetMutableTestInfo(int i) {
11 const int index = GetElementOr(test_indices_, i, -1);
12 return index < 0 ? NULL : test_info_list_[index];
13 }
看到又转向调用TestCase::GetMutableTestInfo,返回向量test_info_list_的元素。而它的元素类型为Test_info。进而又转向了Test_info::Run
1 void TestInfo::Run() {
2 ...... //省略
3 Test* const test = internal::HandleExceptionsInMethodIfSupported(
4 factory_, &internal::TestFactoryBase::CreateTest,
5 "the test fixture's constructor");
6 ...... //省略
7 test->Run(); // Test::Run
8 ...... //省略
9 }
可以看到在TestInfo::Run中调用了HandleExceptionsInMethodIfSupported,通过上文中的分析可以得知该函数在这个地方最终的作用是调用internal::TestFactoryBase::CreateTest将factor_所指的工厂对象创建的测试案例对象的地址赋给Test类型的指针test。所有最后调用了Test::Run。
1 void Test::Run() {
2 if (!HasSameFixtureClass()) return;
3
4 internal::UnitTestImpl* const impl = internal::GetUnitTestImpl();
5 impl->os_stack_trace_getter()->UponLeavingGTest();
6 internal::HandleExceptionsInMethodIfSupported(this, &Test::SetUp, "SetUp()");
7 // We will run the test only if SetUp() was successful.
8 if (!HasFatalFailure()) {
9 impl->os_stack_trace_getter()->UponLeavingGTest();
10 internal::HandleExceptionsInMethodIfSupported(
11 this, &Test::TestBody, "the test body");
12 }
13
14 // However, we want to clean up as much as possible. Hence we will
15 // always call TearDown(), even if SetUp() or the test body has
16 // failed.
17 impl->os_stack_trace_getter()->UponLeavingGTest();
18 internal::HandleExceptionsInMethodIfSupported(
19 this, &Test::TearDown, "TearDown()");
20 }
在Test::Run函数体中我们看到通过HandleExceptionsInMethodIfSupported调用TestBody,我们先来看看Test中TestBody的原型声明
1 virtual void TestBody() = 0;
TestBody被声明为纯虚函数。一切都明朗了,在上文中通过test调用Test::Run,进而通过test::调用TestBody,而test实际上是指向继承自Test类的案例类对象,进而发生了多态,调用的是Test_foo_test_normal_Test::TestBody,也就是我们最初在TEST或者TEST_F宏中所写的测试代码。
如此遍历,就是顺序执行我们所写的每一个TEST宏的函数体啦。
3、总结
经过对预处理得到的TEST宏进行逆向跟踪,到正向跟踪RUN_ALL_TESTS宏,了解了gtest的整个运行过程,里面涉及到一下GOF设计模式的运用,比如工厂函数、Singleton、Impl等。仔细推敲便可发现gtest设计层层跳转,虽然有些复杂,但也非常巧妙,很多地方非常值得我们自己写代码的时候学习的。
另外本文没有提到的地方如断言宏,输出log日志等,因为比较简单就略过了。断言宏和输出log就是在每次遍历调用TestBody的时候进行相应的判断和输出打印,有兴趣的童鞋可以自行研究啦。
最后再简单将gtest的运行过程简述一遍:
1、整个测试项目只有一个UnitTest对象,因而整个项目也只有一个UnitTestImpl对象。
2、每一个TEST宏生成一个测试案例类,继承自Test类。
3、对于每一个测试案例类,由一个工厂类对象创建该类对象。
4、由该测试案例类对象创建一个Test_Info类对象。
5、由Test_Info类对象创建一个Test_case对象
6、创建的Test_case对象的指针,并将其插入到UnitTestImpl对象的一个vector向量的最后一个位置。
7、对每一个TEST宏进行2-6步骤,那么唯一一个UnitTestImpl对象中的vector向量成员中的元素,按顺序依次指向每一个包含测试案例对象信息的TestCase对象。
8、执行RUN_ALL_TESTS宏,开始执行用例。从头往后依次遍历UnitTestImpl对象中vector向量的中的元素,对于其中的每一个元素指针,经过一系列间的方式最终调用其所对应的测试案例对象的TestBody成员函数,即测试用例代码。
(完)
gtest框架的更多相关文章
- gtest框架使用
gtest文档说明: 由于公司单元测试的需要,自己花了大半天时间下载了一个gtest框架,使用了一些测试例子,总览了coderzh的玩转gtest测试框架,又看了几篇gtest博客,写下了以下内容,作 ...
- linux下使用gtest框架进行c/c++单元测试
linux下使用gtest框架进行c/c++单元测试 前言 关于此次开发工具的选择,因为我最近尝试在linux下使用vim进行c/c++编程,且之前已经对vim进行了相关的配置,所以这里应作业要求直接 ...
- 解析gtest框架运行机制
前言 Google test是一款开源的白盒单元测试框架,据说目前在Google内部已在几千个项目中应用了基于该框架的白盒测试. 最近的工作是在搞一个基于gtest框架搭建的自动化白盒测试项目,该项目 ...
- Google C++单元测试框架---Gtest框架简介(译文)
一.设置一个新的测试项目 在用google test写测试项目之前,需要先编译gtest到library库并将测试与其链接.我们为一些流行的构建系统提供了构建文件: msvc/ for Visual ...
- Google C++单元测试框架GoogleTest(总)
之前一个月都在学习googletest框架,对googletest的文档都翻译了一遍,也都发在了之前的博客里,另外其实还有一部分的文档我没有发,就是GMock的CookBook部分:https://g ...
- Google C++单元测试框架GoogleTest---GTest的Sample1和编写单元测试的步骤
如果你还没有搭建gtest框架,可以参考我之前的博客:http://www.cnblogs.com/jycboy/p/6001153.html.. 1.The first sample: sample ...
- 编写优美的GTest测试案例
http://www.cnblogs.com/coderzh/archive/2010/01/09/beautiful-testcase.html 使用gtest也有很长一段时间了,这期间也积累了一些 ...
- Ubuntu 16.04 c++ Google框架单元测试
环境:Ubuntu 16.04 在github网站上下载gtest框架:终端输入git clone https://github.com/google/googletest.git 然后找到 gool ...
- Gtest:Using visual studio 2017 cross platform feature to compile code remotely
参考:使用Visual Studio 2017作为Linux C++开发工具 前言 最近在学Gtest单元测试框架,由于平时都是使用Source Insight写代码,遇到问题自己还是要到Linux下 ...
随机推荐
- ftk学习记录(形成全屏幕套件)
[声明:版权全部.欢迎转载,请勿用于商业用途. 联系信箱:feixiaoxing @163.com] 好久不写博客了.今天续上. 可是,我们还是看一下上一期的执行结果, watermark/2/te ...
- java.lang.Runnable接口
大家都知道使用线程的2种方式,一是继承Thread类,二是实现Runnable接口.实际上,即使你实现了Runnable接口,终于还是要构造一个Thread类的对象.看过Thread源码发现,事实上这 ...
- Java日志性能那些事(转)
在任何系统中,日志都是非常重要的组成部分,它是反映系统运行情况的重要依据,也是排查问题时的必要线索.绝大多数人都认可日志的重要性,但是又有多少人仔细想过该怎么打日志,日志对性能的影响究竟有多大呢?今天 ...
- Scrapy研究和探索(五岁以下儿童)——爬行自己主动多页(抢别人博客所有文章)
首先.在教程(二)(http://blog.csdn.net/u012150179/article/details/32911511)中,研究的是爬取单个网页的方法.在教程(三)(http://blo ...
- Unity最优化摘要
我们的游戏已经wp8.ios和android平台上的线. 这是我第一次做Unity工程,过程中遇到很多困难和挫折,但是,我和小伙伴探路,现在.该游戏已经上线一段时间.而且很稳定.为Unity.我一直在 ...
- HTTP必知必会(转)
HTTP协议作为网络传输的基本协议,有着广泛的应用.HTTP协议的完整内容很多,但是其核心知识却又简单精炼.学习者应该掌握其基本结构,并且能够举一反三.这篇文章所列的,就是在实际开发中必须知道必须掌握 ...
- 体验VS2015正式版
初次体验VS2015正式版,安装详细过程. 阅读目录 介绍 安装 介绍 纽约时间7月20日,微软发布了vs 2015 正式版,换算到我们的北京时间就是晚上了,今天回到家里,就下下来了,装上去 ...
- oracle11g的dmp文件导入oracle10g当误差:头验证失败---解决
原创作品,离 "深蓝的blog" 博客.欢迎转载,转载时请务必注明出处.否则追究版权法律责任. 深蓝的blog:http://blog.csdn.net/huangyanlong/ ...
- Linux入门介绍
Linux入门介绍 一.Linux 初步介绍 Linux的优点 免费的,开源的 支持多线程,多用户 安全性好 对内存和文件管理优越 系统稳定 消耗资源少 Linux的缺点 操作相对困难 一些专业软件以 ...
- linux_根据关键词_路径下递归查找code
1:进入想查找的项目根目录 2:根据关键词查找 find . -name "*" |xargs grep -F '10.26'