学会了这么神奇的模版模式,让你C++模版编程之路事半功倍

最近由于开发工作的需要,项目引入了boost::statechart的状态机,它大量了引用了CRTP,  它的全称是Curiously Recurring Template Pattern,奇异递归模版模式,C++模版编程中很常用的一种用法。那么它神奇的地方到底在哪里呢,接下来就一一来揭开它神秘的面纱。

一、奇异递归模版模式的简介

奇异递归模版模式的基本思想要点是:派生类要作为基类的模版参数。它是C++模版编程中常用的手法。理解它之后,学习模版编程过程中也会事半功倍,而不会觉得云里雾里的。

二、奇异递归模版模式的基本格式

奇异递归模版模式的基本格式如下:JCrtpDerived继承JCrtpBase,并且JCrtpDerived作为基类JCrtpBase的模版参数。通过这样的方式,基类就可以使用子类的方法。并且不需要使用到虚函数,一定程度上减少程序的开销。

template <typename T>
class JCrtpBase
{
public:
};

class JCrtpDerived : public JCrtpBase<JCrtpDerived>
{
public:

};

三、奇异递归模版模式的入门

从上面的给出的奇异递归模版模式的基本格式中可以看出,子类是作为基类的模版参数,但是如果传递给基类的模版参数不是基类的子类,那就会造成混乱错误。如下图所示,JCrtpDerived2子类继承了基类JCrtpBase,但是传递给基类的模版参数不是JCrtpDerived2。

template <typename T>
class JCrtpBase
{
public:
    void Do()
    {
        T* derived = static_cast<T *>(this);
    }

};

class JCrtpDerived1 : public JCrtpBase<JCrtpDerived1>
{
public:

};

class JCrtpDerived2 : public JCrtpBase<JCrtpDerived1>
{
public:

};

那么如何解决上面的问题呢,可以将基类的默认构造函数设置为私有,并且模版参数T设置为基类的友元。通过这样的方式,基类的构造函数只能由模版参数T调用。当创建JCrtpDerived2子类对象的时候,会调用基类的构造函数,而这时候发现JCrtpDerived2不是基类的友元,那么就无法调用基类构造函数而出错。

template <typename T>
class JCrtpBase
{
public:
    void Do()
    {
        T* derived = static_cast<T *>(this);
    }

private:
   JCrtpBase();
   friend T;
};

调用运行JCrtpDerived2,就会出现错误

JCrtpDerived1 crtp_derived1;
crtp_derived1.Do();

JCrtpDerived2 crtp_derived2;
crtp_derived2.Do();

四、奇异递归模版模式的应用场景

1、静态多态

奇异递归模版模式可以实现静态多态的效果,顾名思义,就是有多态的特性,但是不需要使用虚函数,是编译的时候确定,因此,能够减少运行时的开销。接下来就来看看两个示例。

基类JCrtpBase实现函数Do,该函数内部对象通过static_cast转换为模版参数对象,模版参数对象再调用对应的实现函数,而模版参数对象由子类来实现。

template <typename T>
class JCrtpBase
{
public:
    void Do()
    {
        T* derived = static_cast<T *>(this);
        derived->DoSomething();
    }

private:
    JCrtpBase(){}
    friend T;
};

class JCrtpDerived1 : public JCrtpBase<JCrtpDerived1>
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpDerived1";
    }

};

class JCrtpDerived2: public JCrtpBase<JCrtpDerived2>
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpDerived2";
    }
};

调用运行的效果如下所示,从中可以看出,对象调用基类的函数,而基类函数实际上又去调用子类的函数DoSomething。基于这样的思想,我们可以将通用的逻辑放在基类Do中实现,而不同的放到对应的子类函数DoSomething实现。

/// 调用   
JCrtpDerived1 crtp_derived1;
crtp_derived1.Do();

JCrtpDerived2 crtp_derived2;
crtp_derived2.Do();

/// 运行信息
[void JCrtpDerived1::DoSomething():33] I'am is JCrtpDerived1
[void JCrtpDerived2::DoSomething():43] I'am is JCrtpDerived2

这样需要注意的一点是,如果子类再被其他子类继承,那么其他子类就不能按照上面的方式实现。具体可以看下示例:JCrtpSub子类再继承JCrtpDerived1。

class JCrtpSub: public JCrtpDerived1
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpSub";
    }
};

调用运行的效果如下所示,JCrtpSub调用基类的函数Do,但是运行没有调用到JCrtpSub类自身的函数DoSomething。

/// 调用
JCrtpDerived1 crtp_derived1;
crtp_derived1.Do();

JCrtpDerived2 crtp_derived2;
crtp_derived2.Do();

JCrtpSub ctrp_sub;
ctrp_sub.Do();

/// 运行信息
[void JCrtpDerived1::DoSomething():33] I'am is JCrtpDerived1
[void JCrtpDerived2::DoSomething():43] I'am is JCrtpDerived2
[void JCrtpDerived1::DoSomething():33] I'am is JCrtpDerived1

上面的例子是子类调用基类函数,由基类再转换调用子类函数,效果类似于策略模式。下面将要说明的例子,更像多态特性,但是不需要虚函数。基类和子类都实现相同的函数DoSomething

template <typename T>
class JCrtpBase
{
public:
    void DoSomething()
    {
        static_cast<T *>(this)->DoSomething();
    }

private:
    JCrtpBase(){}
    friend T;
};

class JCrtpDerived1 : public JCrtpBase<JCrtpDerived1>
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpDerived1";
    }

};

class JCrtpDerived2: public JCrtpBase<JCrtpDerived2>
{
public:
    void DoSomething()
    {
        LOG(INFO) << "I'am is JCrtpDerived2";
    }
};

然后实现模版方法,该方法入参为基类JCrtpBase的引用,内部调用基类函数DoSomething。

template<typename T>
void DoAction(JCrtpBase<T> &ctrpbase)
{
    ctrpbase.DoSomething();
}

调用运行效果如下,向模版方法传递不同的子类,调用对应子类的函数。

/// 调用
JCrtpDerived1 crtp_derived1;
JCrtpDerived2 crtp_derived2;
DoAction(crtp_derived1);
DoAction(crtp_derived2);

// 打印信息
[void JCrtpDerived1::DoSomething():38] I'am is JCrtpDerived1
[void JCrtpDerived2::DoSomething():48] I'am is JCrtpDerived2

2、boost::statechart状态机

Boost.Statechart大量使用了CRTP,   派生类必须作为第一个参数传递给所有基类模版,Boost.Statechart状态机后续考虑作为一个专题来研究讨论。

struct Greeting : sc::simple_state< Greeting, Machine >

3、std::enable_shared_from_this特性

C++的特性enable_shared_from_this通常是用于在当你要返回一个被shared_ptr管理的对象。JObj继承enable_shared_from_this,并且JObj作为参数模版传递给enable_shared_from_this,这里就运用到了CRTP。

class JObj : public std::enable_shared_from_this<JObj>
{
public:
    std::shared_ptr<JObj> GetObj() {
        return shared_from_this();
    }
};

正确的调用方式,JObj是被shared_ptr管理,因此,如果要获取对象,那么JObj需要继承enable_shared_from_this。

std::shared_ptr<JObj> share_obj1 = std::make_shared<JObj>();
// JObj对象被shared_ptr管理,因此,如果要获取对象,那么JObj需要继承enable_shared_from_this
std::shared_ptr<JObj> share_obj2 = share_obj1->GetObj();

五、总结

到这里,奇异递归模版模式已经基本讲解完成,我们首先介绍了它的基本格式,使用注意要点,然后重点讲解了它的应用场景,包括静态多态、boost::statechart状态机、std::enable_shared_from_this特性。理解了奇异递归模版模式,不但有利于模版编程的学习,而且对于以后应用的开发也是有好处的。

最全面的android入门知识,请好好收藏

Android一款基于Linux的开放源代码的操作系统,主要用于移动设备,现在许多公司都会基于Android做各种定制开发工作。所以,在开发工作之前,需要全面熟悉了解Android的基础知识,有了基础入门知识之后,才能做好方案设计,并且利于以后更加深入的学习发展。

古人说,工欲善其事,必先利其器,所以,本文首先会介绍环境搭建流程,再简单介绍android的系统架构,接着实现第一个程序Hello World!, 让新手对android在视觉上有个比较清晰的概念。然后再介绍程序的目录功能。紧接着就是本文的重头戏,分别介绍布局管理器、android的重要程序组件、Activity的生命周期、Service的生命周期、BroadcastRecevier的应用,Intent的应用。最后再扩展的知识点,包括分辨率问题、应用程序签名。

一、环境搭建

  • 安装JDK7

  • 下载Eclipse(eclipse-jee-kepler-SR1-win32),Eclipse不需要安装, 下载解压后即可使用
  • 安装ADT扩充套件, 双击eclipse.exe,点击help -> install new software -> Add -> 在Location处输入网址:http://dl-ssl.google.com/android/eclipse/site.xml

  • 下载android SDK, 解压SDK,设定SDK: window -> preferences -> android -> 在SDK Location选择SDK路径

  • 安装mysql
  • 安装tomcat,修改默认端口号为8010,数据库驱动(mysql-connector-java-5.1.13-bin.jar ,用于tomcat和mysql之间的连接)放到目录..\Apache Software Foundation\Tomcat 8.0\lib下

修改文件tomcat-users.xml

  • 设置环境变量,计算机->右键属性—>环境变量->修改变量,进行如下设置

如果想要测试效果,那么在cmd下输入以下命令 java,javac,java-version

  • 上面搭建的环境,只是在本机上,如果开发的程序涉及到服务器端,并且需要将程序推荐给别人使用,就需要购买空间和域名(当然,现在也有免费的空间,但是受到限制,无法商用)

二、 系统架构

Android平台是基于Linux内核。上图是其系统架构。大致可以分为四层。从下到上为:

  1. Linux内核层: 包含了Linux内核和一些驱动模块(比如蓝牙驱动,USB驱动等)
  2. Libraries层: 提供动态库, Android运行时库,Dalvik虚拟机等。这一层大部分都是用C或者C++编写,也可以称为Native层(init、Audio系统、Surface系统)
  3. Framework层: 大部分用Java语言编写。是Android平台的基石。(zygote、system server等)
  4. Application层:与用户之间交互。都是用Java开发。(MediaProvider等)

Java世界与Native世界的关系

  1. JAVA通过JNI层调用Linux的系统调用来完成对应的功能
  2. JAVA世界经过JNI层通过IPC方式与Native世界交互。而IPC方法就是是Binder。
  3. JNI是Java Native Interface的是缩写, 即“Java本地调用”。简单来说,java程序中的函数可以调用Native写的函数。Native程序中的函数可以调用Java层的函数。

三、开始第一个程序

  1. 双击Eclipse, 启动Eclipse
  2. 选择File -> New -> Project, 在打开的对话框中,选择Android -> Android Application Project

  1. 程序创建完成,我们来看看效果,首先利用android 提供虚拟设备管理,创建虚拟机,并运行
  2. 右键工程, Debug as -> Android Application, 运行效果如下
  3. (如果工程暂时不要,可以选择工程,右键“Close Porject,可以提高打开esclipse的速度,当工程多的时候,这个效果特别明显)

    四、程序目录介绍

    总体目录

目录及文件 说明
Src 代码目录,代码编写、功能实现的地方
Assets 存放资源文件,例如歌曲等
Bin 存放编译生成的二进制文件
Libs
Res 布局文件
AndroidManifest.xml 关键配置文件,包括组件的声明,版本的定义,权限的声明等

代码文件说明

程序启动的时候,会调用onCreatesetContentView,进行界面的初始化,后面会详细说明activity的生命周期。onCreateOptionMenuonOptionsItemSelected是选项菜单的实现。

布局文件说明

Android提供了一组View类,用作视图的容器。每个布局实现管理器子布局的大小和位置的特定策略。

布局管理器 说明
LinearLayout 以水平或者垂直的方式组织其子控件
FrameLayout 显示单一帧
TableLayout 以行和列的方式组织其子控件
AbsoluteLayout 绝对布局,兼容性不好,不建议使用
RelativeLayout 相对布局,容器中的控件相

 

Android中,控件通常是在layout目录下定义,但是代码中如何使用呢? 这里先解释下两个定义的不同

“@+”表示新声明, “@”表示引用,例如:

“@+id/tv” 表示新声明一个id, 是id名为tv的组件

“@id/tv” 表示引用id名为tv的组件,比较常用于布局

定义编辑框
<EditText
android:id="@+id/search_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="@string/search_example" />
 
代码中引用:
private EditText text_search;          
text_search = (EditText) findViewById(R.id.search_bar);

AndroidManifest文件说明

<application></application>是进行组件的声明,例如activity, service, brocadcast等注意,如果涉及到网络的交互,要在该文件中加入如下权限:

<uses-permission android:name="android.permission.INTERNET"></uses-permission>

六、 布局管理器

LinearLayout线性布局

线性布局是最常用的一种。此布局会保持组件之间的间隔以及组件之间互相对齐。显示组件的方式有垂直于水平两种,可以通过orientation进行设定。

垂直方式:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal"
     >
    <Button
        android:layout_width="fill_parent"
        android:layout_height = "wrap_content"
        android:text = "Button1"></Button>
    <Button
        android:layout_width="fill_parent"
        android:layout_height = "wrap_content"
        android:text = "Button2"></Button>  
    <Button
        android:layout_width="fill_parent"
        android:layout_height = "wrap_content"
        android:text = "Button2"></Button>          
</LinearLayout>

水平方式:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="horizontal"
     >
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "wrap_content"
        android:text = "Button1"></Button>
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "wrap_content"
        android:text = "Button2"></Button>  
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "wrap_content"
        android:text = "Button2"></Button>          
</LinearLayout>

FrameLayout单帧布局

单帧布局新定义的组件永远放在屏幕的左上角,后一个组件总会将前一个组件覆盖

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
     >
    <Button
        android:layout_width="fill_parent"
        android:layout_height = "wrap_content"
        android:text = "Button1"></Button>
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "fill_parent"
        android:text = "Button2"></Button>  
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "wrap_content"
        android:text = "Button3"></Button>          
</FrameLayout>

TableLayout表格布局

表格布局就像一个表格。由TableRow组成,每个TableRow代表一行。

<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
     >
     <TableRow>
        <Button android:text = "Button1"></Button>
        <Button android:text = "Button2"></Button>
        <Button android:text = "Button3"></Button>             
     </TableRow>
 
     <TableRow>
        <Button android:text = "Button4"></Button>
        <Button android:text = "Button5"></Button>
        <Button android:text = "Button6"></Button>             
     </TableRow>   
 
     <TableRow>
        <Button android:text = "Button7"></Button>
        <Button android:text = "Button8"></Button>
        <Button android:text = "Button9"></Button>             
     </TableRow>            
</TableLayout>

AbsoluteLayout绝对布局

绝对布局,组件的位置可以准确指定在屏幕的x/y坐标。但是这种布局兼容性不好。

<AbsoluteLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
     >
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "fill_parent"
        android:text = "Button1"
        android:layout_x="100dp"></Button>
    <Button
        android:layout_width="fill_parent"
        android:layout_height = "wrap_content"
        android:text = "Button2"
        android:layout_y="100dp"></Button>   
</AbsoluteLayout>

RelativeLayout相对布局

相对布局,是一种比较常用的比较,每个组件都可以指定相对于其他组件或者父组件的位置(通过ID来指定)。一个组件的位置,至少要确定组件“左右”与“上下”两个位置才可以准确确定组件位置

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text = "Button1"
        android:id="@+id/btn1"></Button>
    <Button
        android:layout_width="fill_parent"
        android:layout_height = "wrap_content"
        android:text = "Button2"
        android:id="@+id/btn2"
        android:layout_below="@id/btn1"></Button>
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "wrap_content"
        android:text = "Button3"
        android:id="@+id/btn3"
        android:layout_below="@id/btn2"
        android:layout_alignRight="@id/btn2"></Button>  
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "wrap_content"
        android:text = "Button4"
        android:id="@+id/btn4"
        android:layout_below="@id/btn3"
        android:layout_alignParentRight="true"></Button>
    <Button
        android:layout_width="wrap_content"
        android:layout_height = "wrap_content"
        android:text = "Button5"
        android:id="@+id/btn5"
        android:layout_below="@id/btn4"
        android:layout_centerHorizontal="true"></Button>
</RelativeLayout>

七、程序组件简介

Activity简介

  • 应用程序中的每个屏幕显示都通过几次和扩展基类Activity来实现
  • Activity利用View来实现应用程序的GUI。应用程序通过GUI向用户显示信息,用户通过GUI向应用程序发出指令和响应

    Service简介

  • Service是具有一段较长生命周期且没有用户界面的程序
  • Service继承自android.app.Service类
  • Service不能自己启动
  • 启动和关闭Service的流程
StartService()启动service
stopService() 关闭service
stopSelf() service自身调用关闭
bindservice() 将context对象(如activity)绑定到指定的service
这样的话,context对象消亡,service也会停止运行

BroadcastReceiver简介

  • BroadcastReceiver是用户接受广播通知的组件
  • BroadcastReceiver是对发送出来的Broadcast进行过滤接收并响应的一类组件
  • 如果想要接受到广播,首先要注册BroadcastReceiver,注册的方式有两种,一种是静态的在AndroidManifest.xml中用<receiver>标签声明注册,并设置过滤器。另一种方式,动态的设置一个IntentFilter对象,然后在需要注册的地方调用registerReceiver,取消注册的地方调用unregisterReceiver方法。
  • 如何发生广播呢?首次,在要发送信息的地方,封装一个Intent对象,然后调用sendBroadcast方法吧Intetn对象以广播的形式发送出去。这样的话,所有已经注册的BroadcastReceiver会检查注册时的IntentFilter是否与发送的Intent向匹配,如果匹配则调用onRecevie方。

    ContentProvider简介

    ContentProvider能将应用程序特定的数据提供给另一个应用程序使用。

    Intent连接组件的纽带

    Intent是一种运行时绑定机制,它能在程序运行的过程中连接两个不同的组件

    Intent的主要组成部分:

组成 描述
组件名称 Intent目标组件的名称
Action(动作) Intent所触发动作名字的字符串
Data(数据) 描述Intent要操作的数据URI和数据类型
Category(类别) 对被请求组件的额外描述信息
Extra(附加信息) 附加额外信息

八、Activity的生命周期

Activity生命周期的七个函数:

函数 说明
onCreate Activity初次创建时被调用,一般在这里创建view, 初始化布局,设置监听器。如果Activity首次调用,那么其后会调用onStart,

如果Activity是停止后重新刷新,那么其后调用onRestart()

onStart() 当Activity对用户即将可见时被调用,其后调用onResume()
onRestart() 当Activity停止后重新显示被调用,其后调用onStart()
onResume() 当用户能在界面中进行操作的时候调用
onPause() 当系统要启动一个其他的Activity时调用,这个方法被用来停止动画和其他占用CPU资源的事情
onStop() 当另一个Activity恢复并遮盖住当前Activity,导致其对用户不再可见时调用
onDestory() 当前的Activity被销毁前所调用的最后一个方法,或者进程终止时调用

为了详细说明生命周期的变化,创建“MyFirstProject”的项目,添加两个类“MainActivity.java”和“OtherActivity.java”,这两个类都继承Activity,并实现了上面的七 函数,在每个生命周期函数中添加了一个Log打印语句,方便观察周期变化。MainActivity添加了一个按钮,用于跳转到OtherActivity

MainActivity.java关键代码
public class MainActivity extends Activity implements OnClickListener {
    private Button btn;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        Log.v("MainActivity", "onCreate");
        btn = (Button) findViewById(R.id.Main_btn);
        btn.setOnClickListener(this);
    }
 
    @Override
    public void onClick(View arg0) {
        if (arg0 == btn) {
            Intent intent = new Intent();
            intent.setClass(this, OtherActivity.class);
            this.startActivity(intent);
        }
    }
 
    @Override
    protected void onDestroy() {
        // TODO Auto-generated method stub
        super.onDestroy();
        Log.v("MainActivity", "onDestroy");
    }
 
    @Override
    protected void onPause() {
        // TODO Auto-generated method stub
        super.onPause();
        Log.v("MainActivity", "onPause");
    }
 
    @Override
    protected void onRestart() {
        // TODO Auto-generated method stub
        super.onRestart();
        Log.v("MainActivity", "onRestart");
    }
 
    @Override
    protected void onResume() {
        // TODO Auto-generated method stub
        super.onResume();
        Log.v("MainActivity", "onResume");
    }
 
    @Override
    protected void onStart() {
        // TODO Auto-generated method stub
        super.onStart();
        Log.v("MainActivity", "onStart");
    }
 
    @Override
    protected void onStop() {
        // TODO Auto-generated method stub
        super.onStop();
        Log.v("MainActivity", "onStop");
    }
 
}
OtherActivity.java 关键代码
public class OtherActivity extends Activity implements OnClickListener {
    private Button btn;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.other);
        Log.v("OtherActivity", "onCreate");
        btn = (Button) findViewById(R.id.Other_btn);
        btn.setOnClickListener(this);
    }
 
    @Override
    public void onClick(View arg0) {
        if (arg0 == btn) {
            this.finish();
        }
    }
 
    @Override
    protected void onDestroy() {
        // TODO Auto-generated method stub
        super.onDestroy();
        Log.v("OtherActivity", "onDestroy");
    }
 
    @Override
    protected void onPause() {
        // TODO Auto-generated method stub
        super.onPause();
        Log.v("OtherActivity", "onPause");
    }
 
    @Override
    protected void onRestart() {
        // TODO Auto-generated method stub
        super.onRestart();
        Log.v("OtherActivity", "onRestart");
    }
 
    @Override
    protected void onResume() {
        // TODO Auto-generated method stub
        super.onResume();
        Log.v("OtherActivity", "onResume");
    }
 
    @Override
    protected void onStart() {
        // TODO Auto-generated method stub
        super.onStart();
        Log.v("OtherActivity", "onStart");
    }
 
    @Override
    protected void onStop() {
        // TODO Auto-generated method stub
        super.onStop();
        Log.v("OtherActivity", "onStop");
    }
 
}

 效果图如下所示

1)首次启动项目,进入MainActivity

02-16 08:35:49.673: V/MainActivity(1303): onCreate
02-16 08:35:49.673: V/MainActivity(1303): onStart
02-16 08:35:49.693: V/MainActivity(1303): onResume

2)按下手机上的“Back”键

02-16 08:40:21.593: V/MainActivity(1303): onPause
02-16 08:40:24.413: V/MainActivity(1303): onStop
02-16 08:40:24.413: V/MainActivity(1303): onDestroy

3)重新打开程序,单击手机上的Home

02-16 08:42:55.133: V/MainActivity(1303): onPause
02-16 08:43:03.983: V/MainActivity(1303): onStop

4)单击程序图标

02-16 08:44:21.963: V/MainActivity(1303): onRestart
02-16 08:44:21.963: V/MainActivity(1303): onStart
02-16 08:44:21.973: V/MainActivity(1303): onResume

上面的情况是单个Activity的时候,下面讲述两个Activity的情况

(1)打开OtherActivity

02-16 08:47:38.743: V/MainActivity(1303): onPause
02-16 08:47:41.423: V/OtherActivity(1303): onCreate
02-16 08:47:41.473: V/OtherActivity(1303): onStart
02-16 08:47:41.473: V/OtherActivity(1303): onResume
02-16 08:47:43.833: V/MainActivity(1303): onStop

2)在OtherActivity,按下“Back”按钮或者按下“关闭当前Activity”按钮

02-16 08:51:22.223: V/OtherActivity(1303): onPause
02-16 08:51:22.583: V/MainActivity(1303): onRestart
02-16 08:51:22.583: V/MainActivity(1303): onStart
02-16 08:51:22.593: V/MainActivity(1303): onResume
02-16 08:51:24.493: V/OtherActivity(1303): onStop
02-16 08:51:24.493: V/OtherActivity(1303): onDestroy

OtherActivity的主题风格,设置成对话框的形式,效果图如下所示

1)打开OtherActivity

02-16 09:04:23.103: V/MainActivity(1390): onPause
02-16 09:04:23.743: V/OtherActivity(1390): onCreate
02-16 09:04:23.743: V/OtherActivity(1390): onStart
02-16 09:04:23.753: V/OtherActivity(1390): onResume

从上面可以看出,如果新打开的Activity不能完全覆盖前面的Activity,  那么前面的Activity就不会调用onStop这个生命周期。

九、Service的生命周期

下面通过创建项目,在service的各个状态回调方法中加入log信息,了解其生命周期

  • 首先创建service类
public class SampleService extends Service {
    final String TAG = "Service";
    
    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG, "onBind");
        return null;
    }
    @Override
    public boolean onUnbind (Intent intent) {
        Log.d(TAG, "onUnbind");
        return super.onUnbind(intent);
    }
    @Override
    public void onRebind (Intent intent) {
        super.onRebind(intent);
        Log.d(TAG, "onRebind");
    }
    @Override
    public void onCreate () {
        super.onCreate();
        Log.d(TAG, "onCreate");
    }
    @Override
    public void onDestroy () {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
    }
    @Override
    public void onStart (Intent intent, int startId) {
        super.onStart(intent, startId);
        Log.d(TAG, "onStart");
    }
}
  •   Activity设置监听按钮
public class MainActivitySampleService extends Activity {
    OnClickListener listener;
    ServiceConnection connection;
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.ch6ex1);
        // 定义ServiceConnection对象,用于绑定Service
        connection = new ServiceConnection(){
            @Override
            public void onServiceConnected(ComponentName arg0, IBinder arg1) {
            }
            @Override
            public void onServiceDisconnected(ComponentName arg0) {
            }
        };
        /*定义Button的点击监听器*/
        listener = new OnClickListener(){
            @Override
            public void onClick(View v) {
                Intent i = new Intent(MainActivitySampleService.this,SampleService.class);
                switch (v.getId()) {
                case R.id.startService:
                    startService(i);
                    break;
                case R.id.stopService:
                    stopService(i);
                    break;
                case R.id.bindService:
                    bindService(i, connection, BIND_AUTO_CREATE);
                    break;
                case R.id.unbindService:
                    unbindService(connection);
                    break;
                default:
                    break;
                }
            }
        };
        /*设置点击监听器*/
     findViewById(R.id.startService).setOnClickListener(listener);
        findViewById(R.id.stopService).setOnClickListener(listener);
        findViewById(R.id.bindService).setOnClickListener(listener);
        findViewById(R.id.unbindService).setOnClickListener(listener);
    } 
}

  • 按下四个按钮的流程信息
StartService:
03-29 14:46:33.801: D/Service(17346): onCreate
03-29 14:46:33.802: D/Service(17346): onStart
 
stopService:
03-29 14:47:31.801: D/Service(17346): onDestroy
 
bindService:
03-29 14:48:13.689: D/Service(17346): onCreate
03-29 14:48:13.713: D/Service(17346): onBind
 
unbindService:
03-29 14:48:41.871: D/Service(17346): onUnbind
03-29 14:48:41.871: D/Service(17346): onDestroy

十、BroadcastRecevier的应用

下面通过一个简单的例子来说明,如何处理广播消息。利用android系统启动完毕时,会发送一个action为ACTION_BOOT_COMPLETED的Intent,来实现开机自启动服务。

  • 首次,为了能够接收广播,需要在AndroidManifest.xml中,加入权限
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
  • 然后创建类MyBootRecevier并在onReceiver方法中启动服务
public class MyBootReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        Intent i = new Intent(context,SampleService.class);
        context.startService(i);
    }
}
  • 最后,采用静态注册的方法注册MyBootReceiver
<receiver android:name=".MyBootReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

十一、 Intent的应用

1、 利用intent在两个Activity之间传递数据

首先,创建intent, bundle对象,bundle存入数据,并通过intent将数据传递给RoutePlanActivity
        Int iLatitude = 10;
        Int iLongtitude = 20;
        Intent intent = new Intent();
        Bundle bundle = new Bundle();
        bundle.putInt("latitude", iLatitude);
        bundle.putInt("longtitude", iLongtitude);
        intent.putExtras(bundle);      
        intent.setClass(MainContentHuntSheActivity.this, RoutePlanActivity.class);
        startActivity(intent); 
 
RoutePlanActivity接收到消息,进行如下处理
Bundle myBundle = this.getIntent().getExtras();
int iLatitude   = myBundle.getInt("latitude");
int iLongtitude = myBundle.getInt("longtitude");

2、 通过intent,实现信息分享

// 分享短串结果
 Intent it = new Intent(Intent.ACTION_SEND);
   
 String str = "冬日暖曲,一款时尚音乐软件,界面简洁实用哦!";
 it.putExtra(Intent.EXTRA_TEXT, str);
 it.setType("text/plain");
 ((Activity)mainActionView.getContext()).startActivity(Intent.createChooser(it, "将短串分享到"));

十二、分辨率问题

Android资源文件中,各个文件存放的分辨率

目录 说明
drawable-ldpi 240×320
drawable-ldpi 320×480
drawable-hdpi 480×800、480×854
drawable-xhdpi 至少960*720
drawable-xxhdpi 1280×720

长度单位dp、sp和px的区别

  • dp也就是dip,这个和sp基本类似。

如果设置表示长度、高度等属性时可以使用dp或sp。但如果设置字体,需要使用sp。

  • dp是与密度无关,sp除了与密度无关外,还与scale无关。如果屏幕密度为160,这时dp和sp和px是一样的。
1dp=1sp=1px,但如果使用px作单位,如果屏幕大小不变(假设还是3.2寸),而屏幕密度变成了320。那么原来TextView的宽度设成160px,在密度为320的3.2寸屏幕里看要比在密度为160的3.2寸屏幕上看短了一半。
但如果设置成160dp或160sp的话。系统会自动将width属性值设置成320px的。也就是160 * 320 / 160。其中320 / 160可称为密度比例因子。
  • 如果使用dp和sp,系统会根据屏幕密度的变化自动进行转换

十三、应用程序签名

Android应用程序要发布,并被别人使用,需要进行签名,下面将说明如何进行签名

  • 生产私钥,Android的SDK中,有个工具keytool.exe,专门用来生产私钥。打开cmd, 进入工具keytool.exe的所在目录,执行以下命令,那么,在当前目录就会生成文件test.keystore
keytool -genkey -dname "CN=Zijun Li,OU=Zijun Li,O=Zijun Li,L=shenzhen,S=guangdong,C=0086" -storepass 密码 -keystore test.keystore -keyalg RSA -keypass 密码 -validity 15000
  • 然后在eclipse中,选中项目,右击鼠标,选择Android Tools ->Export Unsigned Application Package…,然后,按照提示执行,最后导出的apk,即为签名的apk,可以提供给别人使用或者上传应用商店

十四、推荐学习

  • 通过android SDK中的api demo进行学习
  • 反编译别人的apk,学习别人的代码,目前有两种方法(方法一,dex2jar和jd-gui; 方法二,apktool)

 

解决内存管理问题的最佳利器-valgrind

Valgrind是一个动态分析工具,能够自动检测许多内存管理问题、线程bug,  并且能够分析程序的状况。它内部支持多个工具集,包括内存错误检测器,线程错误检测器,缓存分析器、堆分析器等,默认使用的是内存检测器(memcheck),  它是使用最多的一个内存检测工具。当然,你也可以基于Valgrind自己建立新的工具。

Valgrind支持的平台有:x86/Linux、AMD/64Linux、PPC32/Linux、PPC64LE/Linux、S390X/Linux、ARM/Linux(supported since ARMv7)、ARM64/Linux、MIPS32/Linux、MIPS64/Linux、X86/Solaris、 AMD64/Solaris、 X86/illumos、 AMD64/illumos、X86/Darwin (10.10, 10.11)、 AMD64/Darwin (10.10, 10.11)、ARM/Android、ARM64/Android、 MIPS32/Android、X86/Android

Valgrind是开源免费的软件,基于GNU General Public License, version 2.

一、快速入门

Valgrind工具集中最受欢迎的是memcheck,  它满足大部分的场景。memcheck能够检测内存相关的错误,并且是采用C/C++编译的程序,程序运行过程中奔溃或者不可预料的行为都可以使用Valgrind中的memcheck来进行检测。

使用Valgrind前,采用-g选项编译程序,这样memcheck才能够提取到具体的行号信息,同时可以使用-O0优化选项,但是如果使用-O1选项,那么显示的行号信息可能就不准确;不推荐使用-O2选项,如果使用的话,memcheck偶尔上报不是真的存在的未初始化的错误信息

命令行一般的使用格式如下所示,–leak-check=yes是打开内存泄露的检测器,

valgrind --leak-check=yes myprog arg1 arg2

下面提供一个C++例子,该例子有内存泄露和访问不存在地址的两个错误

#include <string>

void f(void)
{
    int* x = new int[10](); 
    x[10] = 0; // 访问不存在地址       
} // 内存泄露,没有释放内存                   

int main(void)
{
    f();
    return 0;
}

错误信息描述如下,表示访问不存在地址,第一行“Invalid write of size 4”表明什么类型错误,写数据到内存中,而该内存是不应该访问的。1066表示进程id号。如果错误的堆栈信息显示不够显示,那么可以加上选项–num-callers,再加上层级数量,比如–num-callers=20。

==1066== Invalid write of size 4
==1066==    at 0x100000F55: f() (example_02.cpp:6)
==1066==    by 0x100000F83: main (example_02.cpp:11)
==1066==  Address 0x100dea808 is 0 bytes after a block of size 40 alloc'd
==1066==    at 0x1000AC086: malloc (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1066==    by 0x100179627: operator new(unsigned long) (in /usr/lib/libc++abi.dylib)
==1066==    by 0x100000F33: f() (example_02.cpp:5)
==1066==    by 0x100000F83: main (example_02.cpp:11)

内存泄露的错误信息提示描述如下, 它会告诉你内存分配的位置,但是它不能告诉你内存为什么泄露。

==1122== 40 bytes in 1 blocks are definitely lost in loss record 14 of 42
==1122==    at 0x1000AC086: malloc (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1122==    by 0x100179627: operator new(unsigned long) (in /usr/lib/libc++abi.dylib)
==1122==    by 0x100000F33: f() (example_02.cpp:5)
==1122==    by 0x100000F83: main (example_02.cpp:11)

一般有几种内存泄露的类型,比较重要的两种是definitely lost和possibly lost,definitely lost是确定内存泄露,需要修复它,possibly lost可能存在内存泄露,需要仔细确认。

==1122== LEAK SUMMARY:
==1122==    definitely lost: 40 bytes in 1 blocks
==1122==    indirectly lost: 0 bytes in 0 blocks
==1122==      possibly lost: 72 bytes in 3 blocks
==1122==    still reachable: 200 bytes in 6 blocks
==1122==         suppressed: 18,127 bytes in 153 blocks
==1122== Reachable blocks (those to which a pointer was found) are not shown.
==1122== To see them, rerun with: --leak-check=full --show-leak-kinds=all

另外memcheck比较经常会上报没有初始化值的信息,但是要定位到错误信息的根本原因是比较困难的,对此,可以添加参数–track-origins=yes来获取更多的信息,但是,这样会使得memcheck运行的更慢。

Conditional jump or move depends on uninitialised value(s)

二、memcheck的错误信息

memcheck是内存错误的检测器,他可以检测C/C++常见的下列错误问题

  1. 访问不应该访问的内存,例如堆溢出、栈溢出、访问已经释放的内存
  2. 使用没有定义的值,例如值没有初始化
  3. 不正确的释放堆内存,例如重复释放内存,malloc/new/new[] 和 free/delete/delete[]没有一一对应使用
  4. 使用memcpy函数,源地址和目的地址重叠
  5. 向内存分配函数中,传递一个不正确的参数,例如负数
  6. 内存泄露
  • 非法读写错误,例如读取已经释放内存的地址,为了获取更多的信息,可以加上–read-var-info=yes的选项
==1178== Invalid read of size 16
==1178==    at 0x101321A50: qstricmp(char const*, char const*) (in /Users/lizijun/Qt5.13.0/5.13.0/clang_64/lib/QtCore.framework/Versions/5/QtCore)
==1178==    by 0x101539A81: QTimerInfoList::activateTimers() (in /Users/lizijun/Qt5.13.0/5.13.0/clang_64/lib/QtCore.framework/Versions/5/QtCore)
  • 使用没有定义的值,例如定义了变量,但是没有初始化,如果信息不够详细,可以添加参数–track-origins=yes来获取更多的信息
#include <string>
#include <iostream>

int main(void)
{
    int i_number;
    std::cout << i_number << std::endl;
    return 0;
}
==1189== Conditional jump or move depends on uninitialised value(s)
==1189==    at 0x1003D83C5: __vfprintf (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1003FF058: __v2printf (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1003E434A: _vsnprintf (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1003E43A7: vsnprintf_l (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1003D53B2: snprintf_l (in /usr/lib/system/libsystem_c.dylib)
==1189==    by 0x1000D4D22: std::__1::num_put<char, std::__1::ostreambuf_iterator<char, std::__1::char_traits<char> > >::do_put(std::__1::ostreambuf_iterator<char, std::__1::char_traits<char> >, std::__1::ios_base&, char, long) const (in /usr/lib/libc++.1.dylib)
==1189==    by 0x1000C8F27: std::__1::basic_ostream<char, std::__1::char_traits<char> >::operator<<(int) (in /usr/lib/libc++.1.dylib)
==1189==    by 0x100000D0D: main (example_03.cpp:7)
  • 非法释放地址,例如重复释放内存
#include <string>
#include <iostream>

int main(void)
{
    char *p_data = new char[64]();
    delete []p_data;
    delete []p_data;
    return 0;
}
==1212== Invalid free() / delete / delete[] / realloc()
==1212==    at 0x1000AC463: free (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1212==    by 0x100000F7D: main (example_04.cpp:8)
==1212==  Address 0x100dea7e0 is 0 bytes inside a block of size 64 free'd
==1212==    at 0x1000AC463: free (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1212==    by 0x100000F62: main (example_04.cpp:7)
==1212==  Block was alloc'd at
==1212==    at 0x1000AC086: malloc (in /usr/local/Cellar/valgrind/3.15.0/lib/valgrind/vgpreload_memcheck-amd64-darwin.so)
==1212==    by 0x100179627: operator new(unsigned long) (in /usr/lib/libc++abi.dylib)
==1212==    by 0x100000F2A: main (example_04.cpp:6)
  • 调用申请和释放内存的方法不匹配,例如malloc申请内存,但是使用delete来释放,对某些系统来说是不允许的,因此,为了保证程序健壮,使用malloc,那么对应使用free; 使用new,那么对应使用delete; 使用new [], 那么对应使用delete []。
Mismatched free() / delete / delete []

三、Valgrind调用QtCreator程序

mac系统通过QtCreator创建程序之后,也可以采用Valgrind在终端上检测QtCreator生成的程序。

首先进入QtCreator编译生成的文件目录

接着选择build开头的目录,右键弹出的列表选择“服务”->”新建位于文件夹位置的终端窗口”来启动终端,  终端输入如下所示的命令来使用Valgrind测试QtCreator编译生成的程序JQtTestStudy.app

四、局限性

  1. Memcheck并不完美,它也会出现误报,但是它有99%的准确性,对于它提示的信息我们应该警惕。
  2. memcheck不能检测每一种内存错误,比如它不能检测到对静态分配或堆栈上的数组的超出范围的读写,但是它还是能够检测出使得你程序奔溃的错误,例如段错误segmentation fault

    五、总结

程序开发过程中,可能会遇到崩溃的问题,如果代码量很多的时候,我们可能会使用gdb来查看coredump信息,但是有时候gdb的信息比较简单,没有更加详细的堆栈信息,那么就可以考虑使用Valgrind进行分析。最近,工作中遇到一个问题,程序运行过程中,会偶发崩溃问题,使用gdb查看coredump信息,显示是重复释放内存,但是堆栈信息很少,一直找不到位置,后来使用Valgrind来查看程序,仔细查看从Valgrind提供的堆栈信息,很快找到问题的位置,原因确实是重复释放内存。

温馨提示:Valgrind经常上报了很多错误提示信息,这个可能是同样一个地方调用了多次,所以,如果解决了一个地方的问题,错误提示信息就会全部消失,需要耐心仔细。

析构函数的妙用, 让你明白流方式输出日志的实现原理

学习面向对象(如C++编程语言),那么肯定了解析构函数,它在对象销毁的时候被调用,通常我们在构造函数中申请资源,在析构函数中释放资源。那么析构函数在实现以流方式输出日志中有什么妙用呢?接下来请让我一步步为你揭开这层迷雾。

C/C++语言日志输出模式一般有两种,一种类似printf的方式,另一种类似std::cout的方式,这里说的流方式输出日志指的就是类似std::cout的方式,并且自定义日志输出的格式,同时既可以将日志输出到终端,也可以将日志输出到文件。

printf("%s  %d \n", "this is my log", 1);
std::cout << "this is my log " << 1 << std::endl;

一、格式化字符串的输出流

C++语言提供了ostringstream模版,它支持格式化字符串输出流。

  • 首先让我们看看ostringstream的简单使用,定义ostringstream变量oss,然后将当前的线程id以十六进制的方式写入ostringstream变量,  再调用ostringstream的函数str(),将其转换为std::string字符串之后,打印输出到终端。
#include <sstream>

std::ostringstream oss;
oss << std::hex << std::this_thread::get_id();
LOG(INFO) << oss.str();

输出的信息如下所示,当前的线程id是以十六进制的格式输出。

[2019-11-30 22:03:50,124554] [bool JDebugCPPAttr::TestOstringstream():277] 0x7fff9e22c380
  • 上面是ostringstream的简单使用方法,那么下面将说明如何构造输出函数名称和行号的字符串。通过利用系统提供的宏定义__func__和__LINE__来构造所需字符串信息。
std::ostringstream oss2;
oss2 << "[" << __func__ << ":" << __LINE__ << "]";
std::cout << oss2.str() << std::endl;

从输出的格式内容看,ostringstream按照预期的效果输出了正确的字符串格式。

[TestOstringstream:281]

二、资源获取即初始化

RAII全称是“Resource Acquisition is Initialization”,资源获取即初始化”,简单来说,就是说在构造函数中申请分配资源,在析构函数中释放资源。经常使用的方式是:构造函数中通过new申请内存,析构函数中通过delete释放内存。

  • 基于RAII的思想,我们实现资源管理的管理类,管理类ResourceManager构造函数接受std::function类型的变量, 将其赋值给类的私有成员变量exit_handle,析构函数内调用exit_handle,  那么如果想要实现满足RAII, 那么只要构建释放资源的std::function类型的变量,然后传递给 ResourceManager。
class ResourceManager
{
public:
    explicit ResourceManager(std::function<void()> fun):exit_handle(fun)
    {
        std::cout << "call constructor" << std::endl;
    }
    
    ~ResourceManager()
    {
        std::cout << "call destructor " << std::endl;
        exit_handle();
    }
    
private:
    std::function<void()> exit_handle;
};

申请创建内存,然后再创建ResourceManager对象,构造函数的入参是一个匿名函数,函数的功能是释放创建的内存。

{
    int *p_data = new int();
    ResourceManager( [&]()
                   {
                       std::cout << "delete p_data" << std::endl;
                       delete p_data;
                   });
}

运行程序之后,输出打印信息

call constructor
call destructor 
delete p_data
  • 同样的方式,我们可以创建文件之后,再创建ResourceManager对象,构造函数的参数功能是释放文件句柄。
{
    std::ofstream ofs("test.txt");
    ResourceManager( [&]
                   {
                       std::cout << "close ofs" << std::endl;
                       ofs.close();
                   });
}

运行程序之后,输出打印信息

call constructor
call destructor 
close ofs
  • 从上面的两个例子中,可以看出都是利用对象在销毁时会调用析构函数的原理来实现,简单来说,申请资源之后,紧接着设置释放资源,等到申请的资源使用完成之后,资源管理对象在退出作用域之后,就会调用析构函数来释放资源,这样做的好处是,我们不必关注资源什么时候进行释放的问题,同时一定程度上也防止忘记释放资源。

三、利用析构函数来实现日志输出

结合std::ostringstream可以格式化输出流的功能和对象销毁时调用析构函数的原理,我们就可以实现自定义格式,并以流方式输出日志的功能。

  • 实现JWriter类来格式化日志信息并输出,这里我们只是简单输出到终端,当然,你也可以将自定义格式的日志信息写入文件或者写入队列,再由线程将队列中的日志信息写入文件。
  • JWriter类的构造函数接受三个参数:日志等级、函数名称、行号;并且重载了operator<<运算符
///类定义
class JWriter
{
public:
    explicit JWriter(const std::string &strLevel, const std::string &strFun, int iLine);
    ~JWriter();

    // 重载operator<<运算符
    template <typename T>
    inline JWriter& operator<<(const T& log) {
        m_log << log;
        return *this;
    }

private:
    std::string GetSysTimeToMs();

private:
    std::ostringstream m_log;
};


///类实现
#include <iostream>
#include <thread>
#include <chrono>
#include <sys/timeb.h>

JWriter::JWriter(const std::string &strLevel, const std::string &strFun, int iLine)
{
    m_log <<"["<< GetSysTimeToMs() << "]" << "[" << strFun << ":" << iLine << "]" << "[" << strLevel << "] ";
}


JWriter::~JWriter()
{
    m_log << std::endl;
    /// 这里可以实现将日志输出到终端或者写入文件
    std::cout << m_log.str();
}

std::string JWriter::GetSysTimeToMs()
{
    time_t timep;
    struct timeb tb;

    time (&timep);
    char tmp[128] ={0};
    strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M:%S",localtime(&timep) );

    char tmp2[128] ={0};
    ftime(&tb);
    snprintf(tmp2,sizeof(tmp2),"%d",tb.millitm);

    std::ostringstream buffer;
    buffer << tmp << "." << tmp2 ;

    return buffer.str();
}

  • 那么如何来使用JWriter类,使用效果又是怎样呢?其实很简单,定义如下所示的宏,该宏只接受日志等级的字符串参数。
#define MyLogJ(LEVEL) JWriter(LEVEL, __func__, __LINE__)
  • 调用方式如下所示,它跟我们熟悉使用的std::cout的方式是一样一样的,只是std::cout换成了我们实现的MyLogJ()宏,因此,不存在需要花费时间来学习它的使用的问题。
MyLogJ("INFO") << "hello " << 123;
MyLogJ("INFO") << "hello " << " world";
  • 如下所示输出的效果,它首先输出日期时间,然后是函数名和对应行号以及日志等级,最后才输出用户输入的日志信息。这样的格式,通常是比较美观,并且利于问题的定位,当然,你也可以根据个人的喜好来修改JWriter的构造函数来自定义自己的日志格式。
[2019-12-01 10:26:00.657][TestMyLog:266][INFO] hello 123
[2019-12-01 10:26:00.657][TestMyLog:267][INFO] hello  world

四、总结

自定义日志格式并以流方式输出的功能已经介绍结束,它是利用了std::ostringstream可以格式化输出流的功能,并且在构造函数格式日志信息,析构函数最后处理日志信息,同时重载了operator<<运算符。

析构函数不只是用于释放资源,我们可以利用它的特性来做其他的运用,就如本文介绍的一样,利用了析构函数实现了流方式的日志功能,如果没有,单纯利用构造函数很难实现流方式的日志功能。当然,析构函数可能还有其他妙用,这需要我们不断去发掘。