Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

C++中的智能指针

$
0
0

       智能指针就是智能,自动化地管理指针所指向的动态资源的释放,那仫既然已经有了指针的概念那仫为什仫还会有智能指针呢?请看下面一种场景

    

void fun()
{
	int *ptr=new int(1);
	if(1)
		throw 1;
	delete ptr;
}
void test1()
{
	try
	{
		fun();
	}
	catch(...)
	{
		cout<<"未知异常"<<endl;
	}
}

 

    

      在上述场景中我们发现如果在程序中出现异常进而抛出异常,会出现内存泄漏的问题(ptr无法释放),此时我们就需要一个能够出作用域就自动释放的内存空间的指针,而智能指针的提出就是为了解决异常安全的问题;智能指针其实并不是指针,它一个类,但是它却做着和指针类似的事儿:在构造函数中完成资源的分配和初始化,在析构函数中完成资源的清理,保证资源的正确初始化和释放.这也就提出了一个和智能指针类似的概念-RAII(Resource Acquisition Initialization),资源分配及初始化,定义一个类来封装资源的分配和释放

       auto_ptr的模拟实现

      

template<typename T>
class AutoPtr
{
public:
	AutoPtr(T *ptr)
		:_ptr(ptr)
	{
		cout<<"AutoPtr()"<<endl;
	}
	AutoPtr(AutoPtr<T>& ap)   //权限转移
	{
		_ptr=ap._ptr;
		ap._ptr=0;
	}
	AutoPtr<T>& operator=(AutoPtr<T>& ap)
	{
		if(_ptr != ap._ptr)
		{
			delete _ptr;
			_ptr=ap._ptr;
			ap._ptr=0;
		}
		return *this;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	void Reset(T *ptr)  //重置
	{
		delete _ptr;
		_ptr=ptr;
	}
	~AutoPtr()
	{
		if(_ptr != 0)
		{
			cout<<"~AutoPtr()"<<endl;
			delete _ptr;
			_ptr=0;
		}
	}
private:
	T* _ptr;
	//bool owner;   //不安全
};


 

      auto_ptr是通过权限转移的方法解决多个指针指向同一块空间而该空间不会被释放多次的问题的.所谓的权限转移主要体现在拷贝构造和赋值操作符重载上,权限转移其实就是当需要以一个已有的对象初始化另一个对象或者需要给有一个对象赋值时我们可以使得维护该对象的智能指针为空,而另一个维护该对象的智能指针指向该空间.

       scoped_ptr的模拟实现  

       

template<typename T>
class ScopedPtr
{
public:
	ScopedPtr(T *ptr)
		:_ptr(ptr)
	{}
	T& operator*()
	{
		return *_ptr;
	}
	T* operator->()
	{
		return _ptr;
	}
	~ScopedPtr()
	{
		if(_ptr != 0)
		{
			delete _ptr;
			_ptr=0;
		}
	}
protected:
	ScopedPtr(ScopedPtr<T>& sp);
	ScopedPtr<T>& operator=(ScopedPtr<T>& ap);
private:
	T *_ptr;
};


 

      scoped_ptr是一个防拷贝,防赋值的智能指针,所以scoped_ptr的拷贝构造函数和赋值操作符重载可声明为private或者protected即可不需实现,但是不可声明为public.因为如果申明为public不实现,虽然在该类域中达到了防拷贝,防赋值的效果,但是如果继承之后实现了呢?此时依然可以在继承类中拷贝构造和赋值,所以不可声明为public.

      shared_ptr的模拟实现

     

template<typename T>
class SharedPtr
{
public:
	SharedPtr(T *ptr)
		:_ptr(ptr)
		,_pcount(new int(1))
	{
		cout<<"构造"<<endl;
	}
	SharedPtr(const SharedPtr<T>& sp)
		:_ptr(sp._ptr)
		,_pcount(sp._pcount)
	{
		cout<<"拷贝构造"<<endl;
		++(*_pcount);
	}
	SharedPtr<T>& operator=(SharedPtr<T>& sp)
	{
		cout<<"赋值操作符"<<endl;
		if(_ptr != sp._ptr)
		{
			if(--(*_pcount) == 0)
			{
				delete _ptr;
				delete _pcount;
			}
			_ptr=sp._ptr;
			_pcount=sp._ptr;
			++(*_pcount);
		}
		return *this;
	}
	T* operator->()
	{
		return _ptr;
	}
	T& operator*()
	{
		return *_ptr;
	}
	T* Get()
	{
		return _ptr;
	}
	int GetCount()
	{
		return *_pcount;
	}
	~SharedPtr()
	{
		cout<<"析构了"<<endl;
		if(--(*_pcount) == 0)
		{
			cout<<"删除了"<<endl;
			delete _ptr;
			_ptr=0;
			delete _pcount;
			_pcount=0;
		}
	}
private:
	T *_ptr;
	int *_pcount;
};


 

        shared_ptr解决多个指针指向同一块空间会被析构多次这个问题的方法是引用计数,

 在shared_ptr的模拟实现中设置了一个pcount指针用来记录该空间被多少个指针所指向,当然在释放该空间时只要该pcount不为0则只需要将该pcount空间的内容减减就可以了.是不是觉得shared_ptr很厉害呢?实际上当使用智能指针时最常用的就是shared_ptr,但是它也存在问题-循环引用的问题

        请看下面一种场景:

       

struct Node
{
	~Node()
	{
		cout<<"~Node()"<<endl;
	}
	int _data;
	boost::shared_ptr<Node> _next;
	boost::shared_ptr<Node> _prev;
};
void testshared_ptr()  //循环引用的问题
{
	boost::shared_ptr<Node> sp1(new Node);
	boost::shared_ptr<Node> sp2(new Node);
	cout<<sp1.use_count()<<endl;  //1
	cout<<sp2.use_count()<<endl;  //1
	sp1->_next=sp2;
	sp2->_prev=sp1;
	cout<<sp1.use_count()<<endl;  //2
	cout<<sp2.use_count()<<endl;  //2
}


 

       在定义双向结点结构时,我们知道sp1,sp2,_next,_prev都是shared_ptr类型的智能指针,我们把这两块空间称为n1,n2吧!sp1和sp2的_prev都指向n1,sp2和sp1的_next都指向n2,我们知道shared_ptr是采用引用计数的方法,那仫既然有两个相同类型的智能指针都指向同一块空间,而且空间n1,n2都是由两个相同类型的智能指针指向的,那仫这两个空间的引用计数器的内容都为2,那仫此时又会出现什仫问题呢?

      

       如上图所示,当销毁该空间时会出现内存泄漏的问题,因为该块空间n1分别是由两个同类型的指针维护的,当销毁n1时需要n2的引用计数降下来,而销毁n2时需要n1的引用计数降下来,这就导致了无限循环的问题,这种场景让我想到了踢皮球游戏:你踢给我,我踢给你,最后谁也得不到了.当然到最后这两块空间都是无法释放的.为了解决shared_ptr中的循环引用问题,又引入了一个新的智能指针-weak_ptr

      

struct Node
{
	~Node()
	{
		cout<<"~Node()"<<endl;
	}
	int _data;
	boost::weak_ptr<Node> _next;
	boost::weak_ptr<Node> _prev;
};
void testshared_ptr()  //循环引用的问题
{
	boost::shared_ptr<Node> sp1(new Node);
	boost::shared_ptr<Node> sp2(new Node);
	cout<<sp1.use_count()<<endl;  //1
	cout<<sp2.use_count()<<endl;  //1
	sp1->_next=sp2;
	sp2->_prev=sp1;
	cout<<sp1.use_count()<<endl;  //1
	cout<<sp2.use_count()<<endl;  //1
}


 

     weak_ptr是弱指针,它使得该引用计数不会增长;而且weak_ptr不可以单独使用,必须与shared_ptr配套使用,因为weak_ptr的引入就是为了解决shared_ptr循环引用的问题

     shared_ptr还存在线程安全的问题,有兴趣的童鞋可参考http://blog.csdn.net/jiangfuqiang/article/details/8292906

作者:qq_34328833 发表于2016/9/4 14:58:52 原文链接
阅读:83 评论:0 查看评论

TextView显示丰富多彩的文字

$
0
0

TextView是用于文字的控件,一般可以在布局文件中设置text属性或者在代码中使用setText()方法。但如果想做到格式化文字,比如像网页中将其中的URL、手机号码等等显示不同颜色,并且设置点击事件,可以直接跳转到浏览器或者电话,该如何实现呢?本篇博客重点介绍这些应该如何实现。

使用autoLink属性

TextView的autoLink属性用于控制文本中的URL、Email地址等是否自动被发现并转化为可点击的链接。默认是”none”。可以设置none、web。email、phone、map以及all值,如果想设置多个值则使用”|”。
想了解更多的可以参考官方文档

使用autoLink显示URL和手机号码

 <TextView
        android:id="@+id/url_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:autoLink="all"
        android:text="@string/url_text" />

    <TextView
        android:layout_below="@id/url_tv"
        android:autoLink="all"
        android:text="@string/tel_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

以下为字符串:

<string name="url_text"><a href="http://www.baidu.com">百度</a>  http://www.baidu.com</string>
<string name="tel_text">tel:13007147721</string>

下图为运行效果,并且红色链接都是可以点击的,点击URL可以跳转到浏览器,点击手机号跳转到拨号器
使用autoLink显示URL和手机号码

使用textColorLink改变链接颜色

textColorLink可以以”@[+][package:]type:name”或”?[package:][type:]name”引用别的资源或主题属性,也可以直接设置颜色值。下面将链接改为蓝色,比较下效果:

  <TextView
        android:textColorLink="#0000FF"
        android:id="@+id/url_tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:autoLink="all"
        android:text="@string/url_text" />

    <TextView
        android:layout_below="@id/url_tv"
        android:autoLink="all"
        android:text="@string/tel_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

效果图:
使用textColorLink改变默认链接颜色
可以看到上面的链接颜色变为绿色,而下面还是默认的粉红色。

局限

只能显示url、email、phone等链接,像上述文字中将”百度”二字设为超链接,并不能点击。

使用Html文字

Html类用于将HTML字符串处理成可显示的格式化文字,但是并不是所有的标签都支持。那么我们只需要编辑Html文本,再设置给TextView显示就行了。

 private String html = "<a href=\"http://www.baidu.com\">百度</a>"
            +" <a href=\"tel:13007147721\">13007147721</a>"
            +" <h1>H 1标题<h1>"
            +" <h2>H 1标题<h2>"
            +" <h3>H 1标题<h3>"
            +" <h4>H 1标题<h4>"
            +"<img src=\"smail.png\"><img>";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        urlTextView = (TextView) findViewById(R.id.url_tv);
        urlTextView.setText(Html.fromHtml(html));
        urlTextView.setMovementMethod(LinkMovementMethod.getInstance());

    }

上述代码将html字符串转成Html文本后再设置给TextView,如果需要链接可以被点击,需要调用setMovementMethod(LinkMovementMethod.getInstance())。效果如下:
image
“百度”和“13007147721”称为超链接,并且可以点击,h1-h6标题是可以用的,其他标签没有做验证。img标签可以使用,但是上面可以看到我们想使用本地的图片但是没有显式出来。那么需要怎么做呢?

如何使用Html显式图片

Html类的文档可以参考
Html类有两个接口,一个ImageGetter用于为标签获取图片,一个TagHandler用于处理那些Html类不能处理的标签。
ImageGetter只有一个接口:
image
其中参数source就是img标签中的src属性的值。下面的代码用于显示上面的html文字中的图片

       //显示图片
        Spanned html = Html.fromHtml(text, new Html.ImageGetter() {
            @Override
            public Drawable getDrawable(String s) {
                Drawable drawable = getResources().getDrawable(R.drawable.smail);
                drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
                return drawable;
            }
        }, new Html.TagHandler() {
            @Override
            public void handleTag(boolean b, String s, Editable editable, XMLReader xmlReader) {

            }
        });

        urlTextView.setText(html);

有一点需要记住的是获取到Drawable,一定要记得调用setBounds方法设置图片的边界,否则不能显示图片。
效果如下:
这里写图片描述
如果有很多需要显示的项目中的文件,那么可以每个src中的属性设为R.drawable.xxx,那么ImageGetter的getDrawable就可以根据资源Id去获取图片了。

Html显示图片的实质

查看API,可以看到Html的转换方法的返回都是Spanned类型,那么Spanned类型是什么呢?下一部分将详细介绍Spanned格式化文字。

局限

可以格式化很多文本样式,但是需要将文字添加html标签,并且有些标签不支持。

使用Spanned格式化文字

类结构

Spanned类结构图
CharSequence是一组可读的Char序列,提供了操作Char序列的接口。
Spanned可以在文本范围内添加标记。不是所有文本类都有可变的标记和文字。Spannable接口有添加或移除标记,而Editable有可变的文字。
Spannable用于添加标记和移除标记。并不是所有的Spannable都有可变的文字,Editable拥有可变的文字。
SpannableString文本不可变,但是可以添加标记和移除标记。
Editable是文本可以改变接口。
SpannableStringBuilder是Editable的实现类。

SpannableString用法

使用方法为新建一个SpannableString对象,并将文本传入,再调用setSpan()方法设置各个部分的样式,setSpan的声明如下:

void setSpan (Object what, int start, int end, int flags)

其中Object代表标记类型,可以使用的对象为CharacterStyle、ParagraphStyle的实现类,具体可以参考官方文档;start表示文本该标记作用的起始位置,end表示该标记作用的结束位置,flags参数可以参考Spanned类。我使用的都是SPAN_INCLUSIVE_EXCLUSIVE,该参数包含起始位置,不包含结束位置。下面为SpannableString的用法,几乎使用了所有的CharacterStyle。下面的代码分别设置了链接、前景、背景、删除线、下划线绝对尺寸、相对尺寸、图片、字体。

private String text = "百度"
            + "13007147721"
            + "前景"
            + "背景"
            + "删除线"
            + "下划线"
            + "图片"
            + "H1H2H3H4H5H6"
            + "H1H2H3H4H5H6"
            + "X3X2X1"
            + "字体"
            +"正常字体";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        urlTextView = (TextView) findViewById(R.id.url_tv);

        SpannableString spannableString = new SpannableString(text);
        //设置URL链接
        spannableString.setSpan(new URLSpan("http://www.baidu.com"), 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置电话号码链接
        spannableString.setSpan(new URLSpan("tel:13007147721"), 2, 13, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置前景色
        spannableString.setSpan(new ForegroundColorSpan(Color.parseColor("#0000FF")), 13, 15, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置背景色
        spannableString.setSpan(new BackgroundColorSpan(Color.RED), 15, 17, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置删除线
        spannableString.setSpan(new StrikethroughSpan(), 17, 20, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //下划线
        spannableString.setSpan(new UnderlineSpan(), 20, 23, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置图片
        spannableString.setSpan(new ImageSpan(this, BitmapFactory.decodeResource(getResources(), R.drawable.smail)), 23, 25, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置文本大小

        //绝对尺寸
        spannableString.setSpan(new AbsoluteSizeSpan(100), 25, 27, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new AbsoluteSizeSpan(90), 27, 29, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new AbsoluteSizeSpan(80), 29, 31, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new AbsoluteSizeSpan(70), 31, 33, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new AbsoluteSizeSpan(60), 33, 35, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new AbsoluteSizeSpan(50), 35, 37, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //相对尺寸
        spannableString.setSpan(new RelativeSizeSpan(6), 37, 39, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new RelativeSizeSpan(5), 39, 41, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new RelativeSizeSpan(4), 41, 43, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new RelativeSizeSpan(3), 43, 45, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new RelativeSizeSpan(2), 45, 47, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new RelativeSizeSpan(1), 47, 49, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置X方向的改变
        spannableString.setSpan(new ScaleXSpan(3), 49, 51, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new ScaleXSpan(2), 51, 53, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new ScaleXSpan(1), 53, 55, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置字体
        spannableString.setSpan(new TypefaceSpan("monospace"), 55, 57, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);


        urlTextView.setText(spannableString);
        urlTextView.setMovementMethod(LinkMovementMethod.getInstance());

    }

效果图:
SpannableString用法

SpannableString添加多个格式化对象效果会如何?

对于同一部分文字,只添加同一类型的对象一个,效果会将每个格式化对象的效果累加。下面是测试代码,对于“百度”两词,设置URLSpan、ForegroundColorSpan、BackgroundColorSpan。

//SpannableString设置多个Span
        SpannableString spannableString = new SpannableString(text);

        //设置URLSpan
        URLSpan baiduSpan = new URLSpan("http://www.baidu.com");
        spannableString.setSpan(baiduSpan, 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        //设置前景
        spannableString.setSpan(new ForegroundColorSpan(Color.BLUE), 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        //设置背景
        spannableString.setSpan(new BackgroundColorSpan(Color.RED), 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        urlTextView.setText(spannableString);
        urlTextView.setMovementMethod(LinkMovementMethod.getInstance());

下图是效果:
设置多个Span
可以看到“百度”前景蓝色,背景红色,并且可以点击出百度首页。
对于同一部分文字,如果每个类型的对象都添加两个,那么效果会是怎样呢?先看代码和效果,其中代码中,分别设置了两个URLSpan、ForegroundColorSpan、BackgroundColorSpan。

//SpannableString设置多个Span
        SpannableString spannableString = new SpannableString(text);

        //设置URLSpan
        URLSpan baiduSpan = new URLSpan("http://www.baidu.com");
        URLSpan taobaoSpan = new URLSpan("http://www.baidu.com");
        spannableString.setSpan(baiduSpan, 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(taobaoSpan, 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        //设置前景
        spannableString.setSpan(new ForegroundColorSpan(Color.BLUE), 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new ForegroundColorSpan(Color.WHITE), 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        //设置背景
        spannableString.setSpan(new BackgroundColorSpan(Color.RED), 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new BackgroundColorSpan(Color.BLUE), 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        urlTextView.setText(spannableString);
        urlTextView.setMovementMethod(LinkMovementMethod.getInstance());

效果如下图:
相同的Span设置两个
可以看到文字颜色为白色,背景为黑色,可以看出是后设置的起了效果;但是点击后还是显示百度首页,而没有显示淘宝首页,说明URLSpan是前面那个起了效果。

SpannableString删除标记

SpannableString可以调用removeSpan来删除标记。下面的示例用于删除上述代码中的百度URLSpan,看下效果。

//SpannableString设置多个Span
        SpannableString spannableString = new SpannableString(text);

        //设置URLSpan
        URLSpan baiduSpan = new URLSpan("http://www.baidu.com");
        URLSpan taobaoSpan = new URLSpan("http://www.baidu.com");
        spannableString.setSpan(baiduSpan, 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(taobaoSpan, 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        //设置前景
        spannableString.setSpan(new ForegroundColorSpan(Color.BLUE), 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new ForegroundColorSpan(Color.WHITE), 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        //设置背景
        spannableString.setSpan(new BackgroundColorSpan(Color.RED), 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new BackgroundColorSpan(Color.BLUE), 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //删除百度URLSpan
        spannableString.removeSpan(baiduSpan);

        urlTextView.setText(spannableString);
        urlTextView.setMovementMethod(LinkMovementMethod.getInstance());

效果如上图,但是点击事件将出现淘宝首页。

SpannableString用法总结

SpannableString可以设置多种Span用来格式化文字,但是文字内容一旦设置后就不可以更改。如果对同一文字设置多个效果是可以的,但是有些效果是会覆盖之前的效果,比如ForgroundColorSpan等,而有的效果会使用第一个,比如URLSpan。

SpannableStringBuilder用法

SpannableStringBuilder用法和SpannableString用法类型。下面是将SpannableString替换为SpannableStringBuilder的代码和效果图:

  //SpannableStringBuilder基本用法
        SpannableStringBuilder spannableString = new SpannableStringBuilder(text);
        URLSpan baiduSpan = new URLSpan("http://www.baidu.com");
        //设置URL链接
        spannableString.setSpan(baiduSpan, 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置电话号码链接
        spannableString.setSpan(new URLSpan("tel:13007147721"), 2, 13, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置前景色
        spannableString.setSpan(new ForegroundColorSpan(Color.parseColor("#0000FF")), 13, 15, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置背景色
        spannableString.setSpan(new BackgroundColorSpan(Color.RED), 15, 17, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置删除线
        spannableString.setSpan(new StrikethroughSpan(), 17, 20, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //下划线
        spannableString.setSpan(new UnderlineSpan(), 20, 23, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置图片
        spannableString.setSpan(new ImageSpan(this, BitmapFactory.decodeResource(getResources(), R.drawable.smail)), 23, 25, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置文本大小

        //绝对尺寸
        spannableString.setSpan(new AbsoluteSizeSpan(100), 25, 27, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new AbsoluteSizeSpan(90), 27, 29, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new AbsoluteSizeSpan(80), 29, 31, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new AbsoluteSizeSpan(70), 31, 33, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new AbsoluteSizeSpan(60), 33, 35, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new AbsoluteSizeSpan(50), 35, 37, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //相对尺寸
        spannableString.setSpan(new RelativeSizeSpan(6), 37, 39, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new RelativeSizeSpan(5), 39, 41, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new RelativeSizeSpan(4), 41, 43, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new RelativeSizeSpan(3), 43, 45, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new RelativeSizeSpan(2), 45, 47, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new RelativeSizeSpan(1), 47, 49, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置X方向的改变
        spannableString.setSpan(new ScaleXSpan(3), 49, 51, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new ScaleXSpan(2), 51, 53, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(new ScaleXSpan(1), 53, 55, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        //设置字体
        spannableString.setSpan(new TypefaceSpan("monospace"), 55, 57, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);

        urlTextView.setText(spannableString);
        urlTextView.setMovementMethod(LinkMovementMethod.getInstance());

SpannableStringBuilder用法

SpannableStringBuilder的特别之处

SpannableStringBuilder设置完文本内容后,可以改变文本内容。下面的代码在上述代码之后再加入更改文字、插入文字的操作,并从新设置TextView的文字。

 //改变文字
        spannableString.replace(0, 2, "淘宝");
        spannableString.insert(2, "电话号码:", 0, 5);

        urlTextView.setText(spannableString);

效果图如下:
SpannableStringBuilder修改文本内容
可以看到显示内容已经变为了“淘宝电话号码:13007147721”,但是对于电话号码而言,链接文字多了“电话号码:”部分,而其余部分效果并没有收到影响。下面试着将这些改变。首先将已有的链接对象移除后,再添加新的Span对象。将淘宝的链接改为”http://www.taobao.com“;将电话号码多余的效果去除

 //更改效果
        spannableString.removeSpan(baiduSpan);
        spannableString.removeSpan(telSpan);
        spannableString.setSpan(new URLSpan("http://www.taobao.com"), 0, 2, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        spannableString.setSpan(telSpan, 7, 18, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
        urlTextView.setText(spannableString);

效果如下:
SpannableStringBuilder修改效果
点击淘宝后会出现淘宝首页。

SpannableStringBuilder总结

用法和SpannableString用法相同,只不过增加了可以更改文本内容的接口。

总结

TextView显示丰富多彩的文字有很多中方式,如果只是一些简单的url、手机号,那么可以使用autoLink属性;如果文本本身就是html文档,那么可以使用Html类将文本转成Spanned后显示;如果想实现普通文本的格式化,那么应该使用SpannableString或SpannableStringBuilder。

作者:qq_19431333 发表于2016/9/4 15:30:13 原文链接
阅读:68 评论:0 查看评论

深入分析AsyncTask

$
0
0

1. 什么是AsyncTask

AsyncTask 即 asynchronous task,异步任务。

AsyncTask实际上是围绕Thread和Handler设计的一个辅助类,在内部是对Thread和Handler的一种封装。AsyncTask的异步体现在由后台线程进行运算(访问网络等比较耗时的操作),然后将结果发布到用户界面上来更新UI,使用AsyncTask使得我不用操作Thread和Handler。

2. AsyncTask的简单使用

new AsyncTask<String,String,String>(){
    //// 运行在主线程中,做预备工作/////
    onPreExecute(){

    }
    // 运行在子线程中,做耗时操作
    String doingBackGround(String s){

    }
    // 运行在主线程中,耗时操作完成,更新UI
    onPostExecute(String s){

    }

}.execute(String);

AsyncTask用法比较简单,Google设计这个类就是为了方便我们进行类似Handler这样的异步操作。

如上代码,一般使用AsyncTask只要重写里面的三个方法,onPreExecute和onPostExecute不是抽象方法,不是必须实现,实现这两种方法一般能让代码的逻辑更加清晰。onPreExecute运行在主线程中,做一些准备工作。onPostExecute同样运行在主线程中,用于在耗时操作完成后,更新UI。另外,还有一个onProgressUpdate方法,用于在后台任务执行过程中来实时地更新UI。
doingBackGround则是抽象方法,必须实现,我们使用AsyncTask时希望将一些耗时操作放在子线程中,doingBackGround中逻辑就相当于我们在Thread-Handler中Thread中的run方法中实现的逻辑。
execute方法用于启动执行任务

3. 从源码角度看AsyncTask的设计

接下来进入这篇文章的重点,我们从源码角度来分析AsyncTask是如何实现的。

AsyncTask的execute方法

public final AsyncTask<Params, Progress, Result> execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }
上面是execute方法,发现execute其实内部调用的executeOnExecutor方法,调用executeOnExecutor方法传递了两个参数,这里第一个传递了一个默认的执行器,关于这个sDefaultExecutor我们再来讲,我们现在来看AsyncTask的流程设计。 我们来看这个executeOnExecutor方法,这里将不重要的代码略去了,其实这里面的逻辑比较清晰。
 public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
            Params... params) {
        .....
        .....

        mStatus = Status.RUNNING;
        ////////////////////////////////
        // 第一步:在主线程中执行准备操作////
        onPreExecute();
        // 第二步:把params参数赋值给mWorker
        mWorker.mParams = params;
        // 第三步:用线程池执行mFuture
        exec.execute(mFuture);
        ///////////////////////
        return this;
    }
第一步,onPreExecute与上面的介绍一样,进行准备工作,这个就没有必要分析,如果我们没有重写,就不会做相关的准备。我们主要第二步和第三步,第二步,把params参数赋值给mWorker,params是execute中传递过来的参数,同时也是泛型中第一个参数,将params赋值给mWorker,那么mWorker是什么呢?

AsyncTask的构造方法

我们继续看源码:主意到mWorker是在AsyncTask的构造方法中创建的。

public AsyncTask() {
        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
               .....
            }
        };

        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
               .....
            }
        };
    }
首先,mWorker在构造方法中创建,它是一个匿名内部类,那WorkerRunnable是个什么东西呢?
 private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
        Params[] mParams;
    }
我们发现WorkerRunnable其实就是一个Callable,同时在execute方法中mFuture也在这里创建了出来,这里会将mWorker传递到FutureTask中去,那么将mFuture传进去做了什么什么操作呢?
 public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        //////这里将mWorker传递进来,其实就是callable///
        /////////////
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }
FutureTask是java.util.concurrent,FutureTask继承了Future,通过 Future 接口,可以尝试取消尚未完成的任务,查询任务已经完成还是取消了,以及提取(或等待)任务的结果值。 在AsyncTask的构造方法中,将mWorker传进来,即将callable传进来,因为mWorker就是callable。 这样在上面的executeOnExecutor中的第三步中,==exec.execute(mFuture)== 用线程池来执行mFuture,其实就是执行mFuture中的run方法,我们来看FutureTask中的run方法:

FutureTask中的run方法

 public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                   //① 调用callable中的call方法,其实就是mWorker中的call方法
                   //并且将结果赋值给result
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    //② 调用自己内部的set方法设置结果
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }
在FutureTask中的run方法,我们需要关注两个地方,第一个,就是上面代码片段的①处,这里调用了mWorker中的call方法,这样我们再回头来看mWorker中的call方法。
mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);

                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                //noinspection unchecked
                return postResult(doInBackground(mParams));
            }
        };
 private Result postResult(Result result) {
        @SuppressWarnings("unchecked")
        Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
                new AsyncTaskResult<Result>(this, result));
        message.sendToTarget();
        return result;
    }
在mWorker中call方法中主要就是执行耗时操作,正是doInBackground方法,并且将执行的结果result返回回去,用postResult对doInBackground进行包裹则是为了运用Handler机制来更新UI。 接下来我们看FutureTask中run方法中的②处,调用了FutureTask自己的set方法。
protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }
 private void finishCompletion() {
        // assert state > COMPLETING;
        for (WaitNode q; (q = waiters) != null;) {
            if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
                for (;;) {
                    Thread t = q.thread;
                    if (t != null) {
                        q.thread = null;
                        LockSupport.unpark(t);
                    }
                    WaitNode next = q.next;
                    if (next == null)
                        break;
                    q.next = null; // unlink to help gc
                    q = next;
                }
                break;
            }
        }
        //① 调用了FutureTask中的done方法
        done();

        callable = null;        // to reduce footprint
    }
由set方法,调用finishCompletion,主要看finishCompletion的逻辑,我们只关注finishCompletion代码的①处,这里调用了done方法, 这样我们来看done方法中的逻辑。
mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                try {
                    postResultIfNotInvoked(get());
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occured while executing doInBackground()",
                            e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                }
            }
        };
在Future中调用了postResultIfNotInvoked,其实这里将这段处理逻辑抽取到方法中去了,在android2.3即以前的源码都是没有抽取的,这也是使得现在的逻辑更加清晰。 ==java.util.concurrent.atomic.AtomicBoolean ( 在这个Boolean值的变化的时候不允许在之间插入,保持操作的原子性==) 由于在mWorker中的call和在mFuture的done方法都会调用postResult来更新UI,由于是线程操作,不能保证先后顺序,所以需要使用AtomicBoolean来保持操作的原子性。其实在2.3上的代码不是这样处理的,2.3上将更新UI的操作都放在mFuture中的done方法中。
private void postResultIfNotInvoked(Result result) {
        final boolean wasTaskInvoked = mTaskInvoked.get();
        if (!wasTaskInvoked) {
            postResult(result);
        }
    }
最后,我们来看postResult方法。
  private Result postResult(Result result) {
        @SuppressWarnings("unchecked")
        Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT,
                new AsyncTaskResult<Result>(this, result));
        message.sendToTarget();
        return result;
    }
这里就是我们非常熟悉的代码了,使用Message发送消息给Handler来更新UI。 在AsyncTask中定义了一个InternalHandler,如果耗时操作执行完毕,就会执行finish(result.mData[0]),如果结果正在执行,则会onProgressUpdate来更新进度,这个onProgressUpdate正是我们前面说到的需要实现的更新进度的方法。
private static class InternalHandler extends Handler {
        @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult result = (AsyncTaskResult) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    // There is only one result
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }
AsyncTaskResult类的只是一个封装
  @SuppressWarnings({"RawUseOfParameterizedType"})
    private static class AsyncTaskResult<Data> {
        final AsyncTask mTask;
        final Data[] mData;

        AsyncTaskResult(AsyncTask task, Data... data) {
            mTask = task;
            mData = data;
        }
    }
private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            //////耗时操作执行完毕,更新UI///////////
            //onPostExecute运行在主线程
            onPostExecute(result);
            //////////////////////////////////////////
        }
        mStatus = Status.FINISHED;
    }
#

这样我们关于AsyncTask的流程终于走通了,为什么onPreExecute和onPostExecute运行在主线程,而doingBackGround为什么运行在子线程中,这个逻辑是不是就变得清晰了。上面贴了好多代码,一直都是在分析代码的意思,至于关于设计的思想感觉现在的自己还体悟不够。

4. AsyncTask在上面遗留的问题

关于AsyncTask中executeOnExecutor中的sDefaultExecutor

项目中问题场景:

操作步骤>>>>>>>>>>>
- 设置安全中,选择指纹。
- 解锁方式选择图案。
- 在选择您的图案界面,点击确定,需要三到五秒才能跳转到下一界面。

问题分析:在设置解锁方式为为图案时,第二次绘制图案后,Settings源码中使用了AsyncTask来将一些比较耗时的验证操作放在子线程中去处理(参看下面的部分代码),由于android原生设计是使用
AsyncTask中的一个参数的方法,一个参数的方法采用的是默认的执行器,即串行执行器

==frameworks/base/core/java/com/android/internal/widget/LockPatternChecker.java==

public static AsyncTask<?, ?, ?> verifyPattern(final LockPatternUtils utils,
            final List<LockPatternView.Cell> pattern,
            final long challenge,
            final int userId,
            final OnVerifyCallback callback) {
        AsyncTask<Void, Void, byte[]> task = new AsyncTask<Void, Void, byte[]>() {
            private int mThrottleTimeout;
            @Override
            protected byte[] doInBackground(Void... args) {
                try {
                    return utils.verifyPattern(pattern, challenge, userId);
                } catch (RequestThrottledException ex) {
                    mThrottleTimeout = ex.getTimeoutMs();
                    return null;
                }
            }
            @Override
            protected void onPostExecute(byte[] result) {
                callback.onVerified(result, mThrottleTimeout);
            }
        };
        ////////////默认使用的串行的执行器//////////////
        task.execute();
        ///////////////////////////////////////////////
        return task;
    }
public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
private static class SerialExecutor implements Executor {
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
        Runnable mActive;

        public synchronized void execute(final Runnable r) {
            mTasks.offer(new Runnable() {
                public void run() {
                    try {
                        r.run();
                    } finally {
                        scheduleNext();
                    }
                }
            });
            if (mActive == null) {
                scheduleNext();
            }
        }

        protected synchronized void scheduleNext() {
            if ((mActive = mTasks.poll()) != null) {
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }
    }
我们发现SerialExecutor即是串行执行器,它的作用是保证任务执行的顺序,也就是它可以保证提交的任务确实是按照先后顺序执行的。它的内部有一个队列用来保存所提交的任务,保证当前只运行一个,这样就可以保证任务是完全按照顺序执行的。如果发现异步任务还未执行,可能被我们发现SerialExecutor即是串行执行器顺序的使用线程执行。因为应用中可能还有其他地方使用AsyncTask,所以到我们的AsyncTask也许会等待到其他任务都完成时才得以执行而不是调用executor()之后马上执行。

如果executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR,params)则不一样。

 /**
     * An {@link Executor} that can be used to execute tasks in parallel.
     */
    public static final Executor THREAD_POOL_EXECUTOR
            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                    TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);
API中的解释:能够并行的执行任务。THREAD_POOL_EXECUTOR是一个数量为corePoolSize的线程池,具体线程池的数量是依据CPU的核心来设置的,如果超过这个数量的线程个数就需要等待

SerialExecutor是按顺序执行,THREAD_POOL_EXECUTOR则一定程度上能保证并行执行。

以上就是关于AsyncTask的全部内容,希望能对你有些帮助,贴了好多源码,如果想真正弄清楚,还是得自己去阅读阅读源码,整理这个也不容易,前前后后花了大概一个星期。
最后是AsyncTask的时序图,画的不太好,凑合看吧,O(∩_∩)O哈哈~
AsyncTask时序图

作者:H_Gao 发表于2016/9/4 15:38:40 原文链接
阅读:89 评论:1 查看评论

无人机攒机基础1

$
0
0

机架

轴距(飞机大小)

机架定义的飞机的基本外观以及飞机的大小。主要参数为轴距。 F450(轴距450mm)是比较合适的,推荐,机架比较小,安装的时候比较简单,没有那么多螺丝,可以买DJI旗舰店的动力套餐,然后去买机架就行。我在第一次攒飞机的时候,希望能够把TK1的板子放在上边,这样我就买了一个大轴距(650mm)飞机,差不多M100差不多大。这样一来就麻烦了。一个电动起落架不是特别稳,固定的不牢固,而且螺丝太多了。

机架材料

机架材料常见的有塑料、玻纤维、碳纤维等。
玻纤材料是以玻璃纤维作为增强材料,树脂及填充剂作为基础物质的复合材料。碳纤材料是以碳纤维作为增强材料,树脂及填充剂作为基础物质的复合材料。碳纤维的强度优于玻纤维,且重量更轻,但价格比玻纤维贵不少。

飞控

pixhawk/APM

pixhawk vs apm?
http://www.rcgroups.com/forums/showthread.php?t=2019471
http://www.blogodrone.com/2015/07/pixhawk-versus-apm-comparison-review.html
pixhawk github
pxihawk dev documentation

PIXHAWK introduction

Pixhawk是一款低成本高性能自动驾驶仪。其继承了APM和PX4的优点,并在其基础上改进。这个项目源于ETH Zurich的计算机视觉与几何实验室、自主系统实验室和自动控制实验室的PIXHAWK项目。它具有来自ST公司先进的处理器和传感器技术,以及NuttX实时操作系统,能够实现惊人的性能,灵活性和可靠性控制任何自主飞行器。

Pixhawk系统的优点

包括:集成多线程,类似Unix / Linux类的编程环境;全新的自动驾驶功能,如任务和飞行
行为的Lua脚本;一个自定义PX4驱动层以确保在所有任务具有严格的时序。这些先进的功
能确保在你的飞行器上不存在任何限制。Pixhawk允许现有的APM和PX4能够无缝地过渡到
该系统,并降低进入门槛的新用户参加自主飞行的精彩世界。
对于初次使用pix的用户来说,建议您循环渐进的完成 pix的入门使用:
0、首先,了解PIX的接口作用和硬件连接顺序。
1、安装对应操作系统的驱动,以及安装地面站控制软件(mp)。
2、仅连接 USB 线学会固件的下载;
3、用USB连接飞控,逐步了解地面站控制软件的详细功能的基础上初始化飞控。
4、组装飞机。
5、数传、飞控、遥控、地面站软件的整体调整。
6、完成基本的初始化功能。
7、组装飞机,完成各类安全检查后试飞;
8、PID参数调整;
9、pix各类高阶应用。

传感器

Invensense MPU6000 三轴加速度计/陀螺仪
ST Micro L3GD20 16位陀螺仪
ST Micro LSM303D 14位加速度计/磁力计
MS5611 MEAS 气压计

pixhawk interface

  • 5个UART串口,1个支持大功率,两个有硬件流量控制
  • Spektrum DSM/DSM2/DSM­X 卫星输入
  • Futaba SBUS输入(输出正在完善中)
  • PPM sum 信号
  • RSSI(PWM或者电压)输入
  • I2C, SPI, 2个CAN, USB
  • 3.3 与 6.6 ADC 输入

Pixhawk飞控系统的组成部分:

  • 一颗性能强劲的32位处理器,还有一颗附加故障保护备用控制器,外加超大的储存空间。
  • 主控制器STM32F427 32位微处理器: 168 MHz,252 MIPS,Cortex M4核心与浮点单元。
  • 2M闪存储存程序和256K运行内存。
  • 独立供电的32位STM32F103备用故障保护协处理器,在主处理器失效时可实现手动恢复。
  • micro SD储存卡槽,用于数据日志和其他用途。
  • 各种恰到好处的传感器。
  • 三轴16位ST Micro L3GD20H陀螺仪,用于测量旋转速度。
  • 三轴14位加速度计和磁力计,用于确认外部影响和罗盘指向。
  • 可选择外部磁力计,在需要的时候可以自动切换。
  • MEAS MS5611气压计,用来测量高度。
  • 内置电压电流传感器,用于确认电池状况。
  • 可外接UBLOX LEA GPS,用于确认飞机的绝对位置。
  • 各种可扩展I/O接口和专用接口。
  • 14个PWM舵机或电调输出。
  • 5个UART(串口),一个支持大功率,2个有硬件流量控制。
  • 两个CAN I/O接口(一个有内部3.3V收发,一个在扩充接口上)。
  • 兼容Spektrum DSM / DSM2 / DSM­XÂ? 卫星接收机输入: 允许使用Specktrum遥控接收机。
  • 兼容Futaba S.BUSÂ?输入和输出。
  • PPM sum 信号输入。
  • RSSI(PWM或电压)输入。
  • I2C和SPI串口。
  • 两个3.3V和一个6.6V电压模拟信号输入。
  • 内置microUSB接口以及外置microUSB接口扩展。
  • 包含它自己的板载微控制器和FMU栈。
  • 具有冗余设计和扩展保护的综合供电系统。
  • Pixhawk是由一个集成有电压电流传感器输出的协同系统供电。
  • 良好的二极管控制器,提供自动故障切换和冗余供电输入。
  • 可支持高压(最高10V)大电流(10A+)舵机。
  • 所有的外接输出都有过流保护,所有的输入都有防静电保护。

pixhawk 其他特性。

  • 提供额外的安全按钮可以实现安全的马达激活/关闭。
  • LED状态指示器与驱动可以支持高亮度外接彩色LED指示灯表明飞行状态。
  • 通过高能多种提示音的压电声音指示器可以得知实时飞行状态。
  • 可支持带外壳与内置磁力计的高性能UBLOX GPS。

PM 电源模块

电源模块(PM)电流电压传感器,检测电池输出电流和电压并且为飞控供电。 工作参数 支持2S到6S工作电压,最高支持90A工作电流。 提供了一个稳定的5.37v和2.25A飞控供电。 检测电池的电压、电流,当电压低于飞控设置最低保护电压时触发飞控的低电压保护。 飞控固件更准确地弥补与其他对罗盘的干扰。

GPS

GPS模块提供位置信息用于定位和自主飞行。罗盘模块及指南针提供方向信息,与飞控罗盘组成双罗盘,减少外界对罗盘干扰。

GPS和罗盘模块安装要求

保持GPS天线朝上,且上方不能有遮挡物,且箭头指向飞行器正前方与飞控方向一致安装位置必须远离磁场干扰,GPS和罗盘模块安装在架安上面,附近的直流电源线和电池和模块都会产生强烈干扰,尽可能远离,电源和地线应尽可能地扭在一起。GPS和罗盘模块应附近不能含铁金属物体,(用尼龙或非磁性不锈钢五金、尼龙或铝支架安装模块)。

遥控/接收(RC radio)

多轴航模属于遥控设备,所以你需要一个遥控器。它应该至少有4通道,包括油门,横滚,俯仰,偏摆,也就是油加XYZ轴的移动。更多的通道更有利于操控,比如自稳飞行,打开/关闭灯光等等。我使用的DEV10,只用了其中4个通道,其他的都没有用。不用买的功能特别多。

日本手和美国手

因为航模活动较早开展的地区在日本与北美地区,因此这两个地区,对控制器中的两个摇杆四个通道的功能定义有不同。如日本手,左摇杆:上下升降舵,左右为方向舵;右摇杆:上下为油门舵,左右是副翼舵。美国手,左摇杆:上下为油门舵,左右方向舵右摇杆:上下为升降舵,左右为副翼舵。这个主要根据个人喜好,就像是开车,有左舵车,主要中国大 陆与北美地区,右舵车,主要英联邦国家和一些地区,如英国与中国香港地区。从理论安全角度来说,在球,左舵车要好些,但是更多的主要还是习惯的养成的问题。美国手与日本手的优劣势,通常是在一些超高难度动作的操作中,才有操作速度上的差异,通常大家,固定翼用日本手较多。

品牌

国产有天地飞、华科尔、福斯,进口有Futaba(日本) 、JR(日本)、ROBBE(德国)、Graupner(德国),早期国内电子水平基础较差,控的质量一般,现在国产控质量也相当稳定,性价比很高,遥控器主要追求实用稳定,不宜过分追求高级配置,国产以天地飞,天六天
九中文界面操作简单,价格实惠,配套的接收机也便宜,一套遥控与接收机可不超过1000元,选择FutabaT8FG 14SG , JR 9X XG7XG11 ,价格一套起步1500以上,且接收机昂贵。新手入航模可天地飞六起步,后续根据所玩飞机的需要再进行升级,通常固定翼飞机,直升机,大多数只需要六通道就可以控制,甚至早期四通道即可,即油门、 副翼、升降、方向。

PPM编码器

PPM编码的编码器允许多达8个PWM(脉冲宽度调制)信号转为PPM(脉冲位置调制)信号。PPM编码器将普通接收机的PWM信号转化成PIXHAWK飞控可识别的PPM信号。

PPM供电是由飞控RC端口供电。并且PPM可以给小功率接收机供电。
ppm-channel
5通道飞行模式设置值范围
模式1: 0<= 1230
模式2: 1230 -­ 1360
模式3: 1361 ­- 1490
模式4: 1491 -­ 1620
模式5: 1621 -­ 1750
模式6: >= 1750

数传

3DR的数传有2种频率的一种433,一种是915的,前者容易受到对讲机干扰,后者容易受到手机基站干扰。我在飞机上装了一个2.4G的AP,有因为机载computer, 所以决定不用数传和图传,后来发现这样是不行的,至少数传是必须的。使用数传在飞机校准的时候很方便,不用拖着一根线,可以实现飞机空中调整飞机的PID参数,使得飞机震动减小。

无线网桥

参数

Dimensions 136 x 20 x 39 mm
Weight 0.1 kg
Operating Frequency 2412-2462 MHz
Range Indoor Up to 200 m
Range Outdoor Up to 500 m
Max. Power Consumption 8 W
Power Supply (PoE) 15V, 0.8A Power Adapter (Included)
Power Method Passive Power over Ethernet (Pairs 4, 5+; 7, 8 Return)
Operating Temperature -20 to 70° C
Operating Humidity 5 to 95% Condensing

详细参见数据手册picom2hp_DS.pdf

无线网桥的设置

机载的网桥作为station,固定IP为192.168.1.30,地面站的网桥作为AP接入点,固定IP为192.168.1.31。进入网桥的设置,打开浏览器,键入网址192.168.1.10,输入账号和密码为ubnt。一个设置为station,一个设置为AP、接入点。详细参见Bullet_Pico_Loco_NS_QSG.pdf

无线网桥的供电

供电在12v-24v都可以,因此3S-6S的电池都可以。地面站电池 使用SKYRC充电器,每次充一块,可同时插入电压报警器,该工具显示了当前电池电压,电压在(4.1841.9)×3=(12.5412.57)V。电池充满之后充电器指示灯由红色变为黄色。飞机端可以直接从电池取电,或者从分电板引出。

电池

锂电池全称为“锂聚合物电池”(Li­polymer,又称高分子锂电池),它比镉、氢电池更轻,可以为航模提供更长的续航时间,同时也具有较高的安全性,所以被广泛采用。锂聚合物电池的显著特点是放电倍率大,它可以通过放电来满足无人机在不同环境下的使用要求。一块锂聚合物电池的放电电流可以达到几百A,而手机的电池放电电流还不到1A,与之相比要小很多。
电池参数S的含义在说到航模电池时,我们一般会说*S电池。其中的S即为几片串联的意思,单节锂电池标称电压是3.6v充电电压4.2v。3S电池满电就是12.6=4.2x3V,安全低电压就是10.8V, 4S电池满电就是16.8V,安全低电压就是14.4V。

容量

描述电池容量的单位有Ah(安•时)、mAh(毫安•时)和Wh(瓦•时——类似于家里常说的kWh,即多少度,1度=1000Wh)【注意:中间是点•,不是 / 】。换算关系:1Ah=1000mAh

C rating(放电倍率)

单位C,量纲为1/h,即“时”的倒数,此参数表示电池的放电能力、放电快慢。c rating 越大,放电能力越强,因此输出的最大电流越大。另外一方面就是该数值越大,保持电压的能力就越强。电池”C数”问题 ,这是代表电池的放电能力,这个C,在电池专业术语里叫”放电倍率”,即放电电流数值除以电池容量数值的倍数, 最大电流=电池容量*放电倍率C值。

The most important difference between batteries with a different C-rating, is the fact that the higher rated batteries suffer less from voltage drops under load.

电池的使用建议

  1. 使用平衡充。
    锂聚合物电池由多片电芯串联而成,因此需要将每一片电池的电压充到同一水平,电路中如果有个别电池电压过低,其它电池就会为它充电,总电压或总电流就会低于我们设定的要求,造成好电池的损坏,所以在日常充电时必须用平衡充。目前市面上常用的平衡充有B6、A6、A9等。
  2. 不过充过放电池。
    航模电池充满后电压单片4.15V~4.20V比较合适,用后的最低电压为单片 3.7V以上(切记不要过放),长期不用的保存电压最好为3.9V。航模电池的单片电芯电压3.7 V是额定电压,实际电压为2.75~4.2V,电池容量是4.2 V放电至2.75 V所获得的电量。电压低于2.75V电池会膨胀,内部的化学液体会结晶,可能会刺穿内部结构层造成短路,甚至会让锂电电压变为零。同样,当充电高于4.2V时会造成过度充电,电池内部化学反应过于激烈,使电池鼓气膨胀。电池的放电曲线表明,刚开始放电时,电压下降比较快,但放电到3.7~3.9V之间时,电压下降较慢。
    但一旦降至3.7V以后,电压下降速度就会加快,控制不好容易导致过放,轻则损伤电池,重则电压太低造成炸机。

切记不要将航模电池单片电芯电压用2.75V,此时电池已不能提供给飞机有效电力进行飞行。航模电池的最低电压一般就是3.4V~3.5V,为了安全飞行,小编建议将单片报警电压设为3.6V,如达到这个电压,或接近此电压,飞手就要马上执行返航或降落动作,尽可能避免因电池电压不足导致炸机。

Ground Station Control

  • Mission Planner C#开发,用的人最多,windows下使用。
  • QGroundControl, c++开发,跨平台使用。

天线的基本知识

天线的基本功能是辐射和接收无线电波。发射时,把高频电流转换为电磁波;接收时,把电磁波转为高频电流。(无线电波属于电磁波中频率相对较低的部分)。天线按工作频段可划分为长波、中波、波、超短波和微波等;按方向性可划分为全向天线和定向天线。无线电波的传播方式主要有贴地传播(波)、通过电离层反射传播(天波)以及沿直线传播(直接从发信天线传到收信点,有时有地面反射波)。

多旋翼上与天线相关的设备主要是遥控、图传和数传等,通过发射天线和接收电线进行数据传输。传播方式上它们主要采用沿直线传播,工作频段属于超短波范围。因为对需要实时控制的多旋翼来说,接接收的信号最可靠,尽管长波中波绕射能力极强,但易受空间环境制约而不稳定。

天线的安装和使用

无线电波对绝缘体有穿透,对导体产生反射和绕射。机架都是绝缘体,不会影响信号的传播,但在机姿态变化时,有些导体部件可能恰好处于接收和发射天线之间,当距离远、信号弱时可能造成信号中断,机载天线最佳的安装位置是在机身下部,这样受机载设备屏蔽的几率最小。

发射天线与接收天线要实现信号完美的传递,最佳状态是平行放置。追求远距离的无人机,接收和射天线一般采用垂直方式安装。追求垂直高度的无人机,接收和发射天线一般水平放置。但水平放置接天线,容易受到飞机姿态和方位影响,所以尽量采用双接收天线的接收机,
两支天线呈90度角水平安装以达到最佳的接收效果。

全向天线和的定向天线

全向天线在360度泛围内都有均匀的场强分布,辐射范围像一个苹果。它的垂直天线主要针对水平方的目标,水平天线主要针对垂直高度上和窄范围内的水平目标。多旋翼上最常见的全向天线就是棒子天线,蘑菇天线、三叶草四叶草天线都属于全向天线范畴,飞穿越的时候用到的比较多。

定向天线
主要介绍下抛物面天线和平板天线。抛物面天线的场强分布类似手电,一部分是直接射出的,另一部分是反射器反射形成的。抛物面天线的效率是最高的,定向性也是最强的,如果用在多旋翼上,一般要装在高精度的跟踪云台上才可以。

平板天线的特性和八木天线基本相同,好的平板天线虽然结构较复杂,但体积效率比高,安装简单是很多模友的FPV首选天线,也需要配合跟踪云台使用,它一般用在高频上,低频的话全向用的多,低绕过障碍物能力比高频要好。

关于全向天线和定向天线,有模友举了一个很形象的例子:全向天线就像个灯泡,平板天线是给灯上加了个有反射效果的灯罩,而抛物面天线是给灯泡上加了个回光罩,成了射灯。
平板天线和全向天线一起使用需要注意几个问题:一是全向天线不要放在平板天线的前面,防止压干扰。二是频率要错开,相差越大越好,同时还要注意倍频干扰。增益、方向图与波瓣

天线的方向图、增益和波瓣宽度是表征天线性能的主要参数指标。天线增益用来衡量天线朝一个特方向收发信号的能力,增益值以dbi为单位,增益与天线方向图、波瓣宽度有密切的关系。天线的增益和主瓣覆盖的宽度是反比关系,方向图的主瓣越窄,副瓣越小,增益越高。

天线增益是无源现象,跟功率无关,能量的总值不会超过发射的总功率。虽然增益天线不能改变能量,但它能改变信号强度。高增益天线和普通天线的区别在于高增益天线通过增加某个方向上的信号强使距离更远,但牺牲了波瓣宽度;普通天线距离近,但覆盖范围大。举个简单的例子,卫星天线的增益高,但几乎只对正前方有效。

如何给大疆的遥控器增程?其实原理很简单:第一种方法就是在天线上套上一个增益板,其实就是个抛物面,上面加上一层锡箔,以减小波瓣的方式换取更大的增程。第二种方法就是直接更换天线,将来的天线更换为高增益天线。

本文中没有关于电调和电机的介绍,关于这些内容会在下一篇中介绍。

refer

  1. http://bbs.5imx.com/forum.php?mod=viewthread&tid=710421
  2. http://www.exuav.com/forum.php?mod=viewthread&tid=736&extra=page%3D1%26filter%3Dtypeid%26typeid%3D41
作者:wendox 发表于2016/9/4 15:55:28 原文链接
阅读:93 评论:0 查看评论

AndroidManifest文件详解

$
0
0

一、清单文件概述


1、所有的应用程序必须要有清单文件

在manifest节点下需要声明当前应用程序的包名


2、包名:声明包的名字,必须唯一

如果两个应用程序的包名和签名都相同,后安装的会覆盖先安装的


3、声明的程序的组件(4大组件)

其中比较特殊的是广播接收者,可以不在清单文件中配置,可以通过代码进行注册


4、声明程序需要的权限:保护用户的隐私


5、可以控制服务在单独的进程中的,四大组件都可以配置这个属性process
在组件节点配置process, 如:android:process="xxx.ooo.xxx", 比如说:处理图片的时候,会很耗内存,就需要在单独的新的进程中,可以减少内存溢出的几率

6、描述了package中暴露 的组件(activities, services, 等等),他们各自的实现类以及各种能被处理的数据和启动位置。

除了能声 明程序中的Activities, ContentProviders, Services, 和Intent Receivers,还能指定permissions和 instrumentation(安全控制和测试)


二、AndroidManifest.xml结构


<?xmlversion="1.0"encoding="utf-8"?>
<manifest>
<application>
<activity>
<intent-filter>
<action/>
<category/>
</intent-filter>
</activity>
<activity-alias>
<intent-filter></intent-filter>
<meta-data/>
</activity-alias>
<service>
<intent-filter></intent-filter>
<meta-data/>
</service>
<receiver>
<intent-filter></intent-filter>
<meta-data/>
</receiver>
<provider>
<grant-uri-permission/>
<meta-data/>
</provider>
<uses-library/>
</application>
<uses-permission/>
<permission/>
<permission-tree/>
<permission-group/>
<instrumentation/>
<uses-sdk/>
<uses-configuration/>
<uses-feature/>
<supports-screens/>
</manifest>


三、各个节点的详细介绍


上面就是整个am(androidManifest).xml的结构,下面以外向内按结构顺序开始阐述


1、第一层(<Manifest>):(属性)


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.woody.test"
android:sharedUserId="string"
android:sharedUserLabel="string resource"
android:versionCode="integer"
android:versionName="string"
android:installLocation=["auto" | "internalOnly" | "preferExternal"] >
</manifest>


A、xmlns:android

定义android命名空间,一般为http://schemas.android.com/apk/res/android,这样使得Android中各种标
准属性能在文件中使用,提供了大部分元素中的数据。

B、package
指定本应用内java主程序包的包名,它也是一个应用进程的默认名称

C、sharedUserId
表明数据权限,因为默认情况下,Android给每个APK分配一个唯一的UserID,所以是默认禁止不同APK访问共
享数据的。若要共享数据,第一可以采用Share Preference方法,第二种就可以采用sharedUserId了,将不

同APK的sharedUserId都设为一样,则这些APK之间就可以互相共享数据了。


特殊用法:获取系统权限
(1)在AndroidManifest.xml中添加android:sharedUserId="android.uid.system"
(2)在Android.mk文件里面添加LOCAL_CERTIFICATE := platform(使用系统签名)
(3)在源码下面进行mm编译

这样生成的apk能够获取system权限,可以在任意system权限目录下面进行目录或者文件的创建,以及访问其他apk资源等(注意创建的文件(夹)只有创建者(比如system,root除外)拥有可读可写权限-rw-------)。


扩展
(1)系统中所有使用android.uid.system作为共享UID的APK,都会首先在manifest节点中增加android:sharedUserId="android.uid.system",然后在Android.mk中增加LOCAL_CERTIFICATE := platform, 可以参见Settings等

(2)系统中所有使用android.uid.shared作为共享UID的APK,都会在manifest节点中增加android:sharedUserId="android.uid.shared",然后在Android.mk中增加LOCAL_CERTIFICATE := shared, 可以参见Launcher等

(3)系统中所有使用android.media作为共享UID的APK,都会在manifest节点中增加android:sharedUserId="android.media",然后在Android.mk中增加LOCAL_CERTIFICATE := media, 可以参见Gallery等。

D、sharedUserLabel
一个共享的用户名,它只有在设置了sharedUserId属性的前提下才会有意义

E、versionCode
是给设备程序识别版本(升级)用的必须是一个interger值代表app更新过多少次,比如第一版一般为1,之后
若要更新版本就设置为2,3等等。。。

F、versionName
这个名称是给用户看的,你可以将你的APP版本号设置为1.1版,后续更新版本设置为1.2、2.0版本等

G、installLocation
安装参数,是Android2.2中的一个新特性,installLocation有三个值可以选择:internalOnly、auto、 preferExternal
选择preferExternal,系统会优先考虑将APK安装到SD卡上(当然最终用户可以选择为内部ROM存储上,如果SD存储已满,也会安装到内部存储上)
选择auto,系统将会根据存储空间自己去适应
选择internalOnly是指必须安装到内部才能运行
(注:需要进行后台类监控的APP最好安装在内部,而一些较大的游戏APP最好安装在SD卡上。现默认为安装在内部,如果把APP安装在SD卡上,首先得设置你的level为8,并且要配置android:installLocation这个参数的属性为preferExternal)

2、第二层(<Application>):属性
一个AndroidManifest.xml中必须含有一个Application标签,这个标签声明了每一个应用程序的组件及其属

性(如icon,label,permission等)


<application android:allowClearUserData=["true" | "false"]
android:allowTaskReparenting=["true" | "false"]
android:backupAgent="string"
android:debuggable=["true" | "false"]
android:description="string resource"
android:enabled=["true" | "false"]
android:hasCode=["true" | "false"]
android:icon="drawable resource"
android:killAfterRestore=["true" | "false"]
android:label="string resource"
android:manageSpaceActivity="string"
android:name="string"
android:permission="string"
android:persistent=["true" | "false"]
android:process="string"
android:restoreAnyVersion=["true" | "false"]
android:taskAffinity="string"
android:theme="resource or theme" >
</application>


A、android:allowClearUserData('true' or 'false')
用户是否能选择自行清除数据,默认为true,程序管理器包含一个选择允许用户清除数据。当为true时,用户可自己清理用户数据,反之亦然

B、android:allowTaskReparenting('true' or 'false')
是否允许activity更换从属的任务,比如从短信息任务切换到浏览器任务

C、android:backupAgent
这也是Android2.2中的一个新特性,设置该APP的备份,属性值应该是一个完整的类名,如com.project.TestCase,此属性并没有默认值,并且类名必须得指定(就是个备份工具,将数据备份到云端的操作)

D、android:debuggable
这个从字面上就可以看出是什么作用的,当设置为true时,表明该APP在手机上可以被调试。默认为false,在false的情况下调试该APP,就会报以下错误:
Device XXX requires that applications explicitely declare themselves as debuggable in their manifest.
Application XXX does not have the attribute 'debuggable' set to TRUE in its manifest and cannot be debugged.

E、android:description/android:label
此两个属性都是为许可提供的,均为字符串资源,当用户去看许可列表(android:label)或者某个许可的详细信息(android:description)时,这些字符串资源就可以显示给用户。label应当尽量简短,之需要告知用户该许可是在保护什么功能就行。而description可以用于具体描述获取该许可的程序可以做哪些事情,实际上让用户可以知道如果他们同意程序获取该权限的话,该程序可以做什么。我们通常用两句话来描述许可,第一句描述该许可,第二句警告用户如果批准该权限会可能有什么不好的事情发生

F、android:enabled
Android系统是否能够实例化该应用程序的组件,如果为true,每个组件的enabled属性决定那个组件是否可以被enabled;如果为false,它覆盖组件指定的值;所有组件都是disabled。

G、android:hasCode('true' or 'false')
表示此APP是否包含任何的代码,默认为true,若为false,则系统在运行组件时,不会去尝试加载任何的APP代码。一个应用程序自身不会含有任何的代码,除非内置组件类,比如Activity类,此类使用了AliasActivity类,当然这是个罕见的现象(在Android2.3可以用标准C来开发应用程序,可在AndroidManifest.xml中将此属性设置为false,因为这个APP本身已经不含有任何的JAVA代码了)

H、android:icon
这个很简单,就是声明整个APP的图标,图片一般都放在drawable文件夹下

I、android:killAfterRestore

J、android:manageSpaceActivity

K、android:name
为应用程序所实现的Application子类的全名。当应用程序进程开始时,该类在所有应用程序组件之前被实例化。若该类(比方androidMain类)是在声明的package下,则可以直接声明android:name="androidMain",但此类是在package下面的子包的话,就必须声明为全路径或android:name="package名称.子包名成.androidMain"

L、android:permission
设置许可名,这个属性若在<application>上定义的话,是一个给应用程序的所有组件设置许可的便捷方式,当然它是被各组件设置的许可名所覆盖的

M、android:presistent
该应用程序是否应该在任何时候都保持运行状态,默认为false。因为应用程序通常不应该设置本标识,持续模式仅仅应该设置给某些系统应用程序才是有意义的。

N、android:process

应用程序运行的进程名,它的默认值为<manifest>元素里设置的包名,当然每个组件都可以通过设置该属性来覆盖默认值。如果你想两个应用程序共用一个进程的话,你可以设置他们的android:process相同,但前提条件是他们共享一个用户ID及被赋予了相同证书的时候


O、android:restoreAnyVersion

同样也是android2.2的一个新特性,用来表明应用是否准备尝试恢复所有的备份,甚至该备份是比当前设备上更要新的版本,默认是false


P、android:taskAffinity
拥有相同的affinity的Activity理论上属于相同的Task,应用程序默认的affinity的名字是<manifest>元素中设定的package名

Q、android:theme
是一个资源的风格,它定义了一个默认的主题风格给所有的activity,当然也可以在自己的theme里面去设置它,有点类似style。

3、第三层(<Activity>):属性


<activity android:allowTaskReparenting=["true" | "false"]
android:alwaysRetainTaskState=["true" | "false"]
android:clearTaskOnLaunch=["true" | "false"]
android:configChanges=["mcc", "mnc", "locale",
"touchscreen", "keyboard", "keyboardHidden",
"navigation", "orientation", "screenLayout",
"fontScale", "uiMode"]
android:enabled=["true" | "false"]
android:excludeFromRecents=["true" | "false"]
android:exported=["true" | "false"]
android:finishOnTaskLaunch=["true" | "false"]
android:icon="drawable resource"
android:label="string resource"
android:launchMode=["multiple" | "singleTop" |
"singleTask" | "singleInstance"]
android:multiprocess=["true" | "false"]
android:name="string"
android:noHistory=["true" | "false"] 
android:permission="string"
android:process="string"
android:screenOrientation=["unspecified" | "user" | "behind" |
"landscape" | "portrait" |
"sensor" | "nosensor"]
android:stateNotNeeded=["true" | "false"]
android:taskAffinity="string"
android:theme="resource or theme"
android:windowSoftInputMode=["stateUnspecified",
"stateUnchanged", "stateHidden",
"stateAlwaysHidden", "stateVisible",
"stateAlwaysVisible", "adjustUnspecified",
"adjustResize", "adjustPan"] > 
</activity>


(注:有些在application中重复的就不多阐述了)


1、android:alwaysRetainTaskState
是否保留状态不变, 比如切换回home, 再从新打开,activity处于最后的状态。比如一个浏览器拥有很多状态(当打开了多个TAB的时候),用户并不希望丢失这些状态时,此时可将此属性设置为true

2、android:clearTaskOnLaunch
比如 P 是 activity, Q 是被P 触发的 activity, 然后返回Home, 重新启动 P,是否显示 Q

3、android:configChanges
当配置list发生修改时, 是否调用 onConfigurationChanged() 方法 比如 "locale|navigation|orientation".
这个我用过,主要用来看手机方向改变的. android手机在旋转后,layout会重新布局, 如何做到呢?
正常情况下. 如果手机旋转了.当前Activity后杀掉,然后根据方向重新加载这个Activity. 就会从onCreate开始重新加载.
如果你设置了 这个选项, 当手机旋转后,当前Activity之后调用onConfigurationChanged() 方法. 而不跑onCreate方法等.


4、android:excludeFromRecents

是否可被显示在最近打开的activity列表里,默认是false


5、android:finishOnTaskLaunch
当用户重新启动这个任务的时候,是否关闭已打开的activity,默认是false,如果这个属性和allowTaskReparenting都是true,这个属性就是王牌。Activity的亲和力将被忽略,该Activity已经被摧毁并非re-parented

6、android:launchMode(Activity加载模式)

在多Activity开发中,有可能是自己应用之间的Activity跳转,或者夹带其他应用的可复用Activity。可能会希望跳转到原来某个Activity实例,而不是产生大量重复的Activity。这需要为Activity配置特定的加载模式,而不是使用默认的加载模式


Activity有四种加载模式:
standard、singleTop、singleTask、singleInstance(其中前两个是一组、后两个是一组),默认为standard

standard:就是intent将发送给新的实例,所以每次跳转都会生成新的activity。
singleTop:也是发送新的实例,但不同standard的一点是,在请求的Activity正好位于栈顶时(配置成singleTop的Activity),不会构造新的实例
singleTask:和后面的singleInstance都只创建一个实例,当intent到来,需要创建设置为singleTask的Activity的时候,系统会检查栈里面是否已经有该Activity的实例。如果有直接将intent发送给它。
singleInstance:
首先说明一下task这个概念,Task可以认为是一个栈,可放入多个Activity。比如启动一个应用,那么Android就创建了一个Task,然后启动这个应用的入口Activity,那在它的界面上调用其他的Activity也只是在这个task里面。那如果在多个task中共享一个Activity的话怎么办呢。举个例来说,如果开启一个导游服务类的应用程序,里面有个Activity是开启GOOGLE地图的,当按下home键退回到主菜单又启动GOOGLE地图的。应用时,显示的就是刚才的地图,实际上是同一个Activity,实际上这就引入了singleInstance。

singleInstance模式就是将该Activity单独放入一个栈中,这样这个栈中只有这一个Activity,不同应用的intent都由这个Activity接收和展示,这样就做到了共享。当然前提是这些应用都没有被销毁,所以刚才是按下的HOME键,如果按下了返回键,则无效


7、android:multiprocess
是否允许多进程,默认是false

8、android:noHistory

当用户从Activity上离开并且它在屏幕上不再可见时,Activity是否从ActivityStack中清除并结束。默认是false,Activity不会留下历史痕迹


9、android:screenOrientation
activity显示的模式
默认为unspecified:由系统自动判断显示方向
landscape横屏模式,宽度比高度大
portrait竖屏模式, 高度比宽度大
user模式,用户当前首选的方向
behind模式:和该Activity下面的那个Activity的方向一致(在Activity堆栈中的)
sensor模式:有物理的感应器来决定。如果用户旋转设备这屏幕会横竖屏切换

nosensor模式:忽略物理感应器,这样就不会随着用户旋转设备而更改了


10、android:stateNotNeeded

activity被销毁或者成功重启时是否保存状态


11、android:windowSoftInputMode
activity主窗口与软键盘的交互模式,可以用来避免输入法面板遮挡问题,Android1.5后的一个新特性。

这个属性能影响两件事情:


【A】当有焦点产生时,软键盘是隐藏还是显示


【B】是否减少活动主窗口大小以便腾出空间放软键盘


各值的含义:
【A】stateUnspecified:软键盘的状态并没有指定,系统将选择一个合适的状态或依赖于主题的设置
【B】stateUnchanged:当这个activity出现时,软键盘将一直保持在上一个activity里的状态,无论是隐藏还是显示
【C】stateHidden:用户选择activity时,软键盘总是被隐藏
【D】stateAlwaysHidden:当该Activity主窗口获取焦点时,软键盘也总是被隐藏的
【E】stateVisible:软键盘通常是可见的
【F】stateAlwaysVisible:用户选择activity时,软键盘总是显示的状态
【G】adjustUnspecified:默认设置,通常由系统自行决定是隐藏还是显示
【H】adjustResize:该Activity总是调整屏幕的大小以便留出软键盘的空间

【I】adjustPan:当前窗口的内容将自动移动以便当前焦点从不被键盘覆盖和用户能总是看到输入内容的部分


4、第四层(<intent-filter>)

结构图:


<intent-filter android:icon="drawable resource"
android:label="string resource"
android:priority="integer" >
<action />
<category />
<data />
</intent-filter>


intent-filter属性
android:priority(解释:有序广播主要是按照声明的优先级别,如A的级别高于B,那么,广播先传给A,再
传给B。优先级别就是用设置priority属性来确定,范围是从-1000~1000,数越大优先级别越高)

Intent filter内会设定的资料包括action,data与category三种。也就是说filter只会与intent里的这三种
资料作对比动作

action属性

action很简单,只有android:name这个属性。常见的android:name值为android.intent.action.MAIN,表明此activity是作为应用程序的入口。


category属性
category也只有android:name属性。常见的android:name值为android.intent.category.LAUNCHER(决定应用
程序是否显示在程序列表里)

data属性


<data android:host="string"
android:mimeType="string"
android:path="string"
android:pathPattern="string"
android:pathPrefix="string"
android:port="string"
android:scheme="string"/>


【1】每个<data>元素指定一个URI和数据类型(MIME类型)。它有四个属性scheme、host、port、path,对应于URI的每个部分:scheme://host:port/path
scheme的值一般为"http",host为包名,port为端口号,path为具体地址。如:http://com.test.project:200/folder/etc

其中host和port合起来构成URI的凭据(authority),如果host没有指定,则port也会被忽略。要让authority有意义,scheme也必须要指定。要让path有意义,scheme+authority也必须要指定


【2】mimeType(指定数据类型),若mimeType为'Image',则会从content Provider的指定地址中获取image类型的数据。还有'video'啥的,若设置为video/mp4,则表示在指定地址中获取mp4格式的video文件


【3】而pathPattern和PathPrefix主要是为了格式化path所使用的

5、第四层<meta-data>


<meta-data android:name="string"
android:resource="resource specification"
android:value="string"/>


这是该元素的基本结构.可以包含在<activity> <activity-alias> <service> <receiver>四个元素中。
android:name(解释:元数据项的名字,为了保证这个名字是唯一的,采用java风格的命名规范,如com.woody.project.fried)
android:resource(解释:资源的一个引用,指定给这个项的值是该资源的id,该id可以通过方法Bundle.getInt()来从meta-data中找到。)
android:value(解释:指定给这一项的值。可以作为值来指定的数据类型并且组件用来找回那些值的Bundle方法:[getString],[getInt],[getFloat],[getString],[getBoolean])

6、第三层<activity-alias>属性


<activity-alias android:enabled=["true" | "false"]
android:exported=["true" | "false"]
android:icon="drawable resource"
android:label="string resource"
android:name="string"
android:permission="string"
android:targetActivity="string">
<intent-filter/>
<meta-data/>
</activity-alias>


<activity-alias>是为activity创建快捷方式的,如下实例:

<activity android:name=".shortcut">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<activity-alias android:name=".CreateShortcuts" android:targetActivity=".shortcut"
android:label="@string/shortcut">
<intent-filter>
<action android:name="android.intent.action.CREATE_SHORTCUT" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity-alias>


其中android:targetActivity是指向对应快捷方式的activity,如上述的shortcut(此Activity名)
android:label是指快捷方式的名称,而快捷方式的图标默认是给定的application图标

7、第三层<service>

【1】service与activity同级,与activity不同的是,它不能自己启动的,运行在后台的程序,如果我们退出应用时,Service进程并没有结束,它仍然在后台运行。比如听音乐,网络下载数据等,都是由service运行的


【2】service生命周期:Service只继承了onCreate(),onStart(),onDestroy()三个方法,第一次启动Service时,先后调用了onCreate(),onStart()这两个方法,当停止Service时,则执行onDestroy()方法,如果Service已经启动了,当我们再次启动Service时,不会在执行onCreate()方法,而是直接执行onStart()方法


【3】service与activity间的通信

Service后端的数据最终还是要呈现在前端Activity之上的,因为启动Service时,系统会重新开启一个新的进程,这就涉及到不同进程间通信的问题了(AIDL),Activity与service间的通信主要用IBinder负责。


【4】


<service android:enabled=["true" | "false"]
android:exported[="true" | "false"]
android:icon="drawable resource"
android:label="string resource"
android:name="string"
android:permission="string"
android:process="string">
</service>


service标签内的属性之前已有描述,在此不重复了~

8、第三层<receiver>


receiver的属性与service一样,这里就不显示了
BroadcastReceiver:用于发送广播,broadcast是在应用程序之间传输信息的一种机制,而BroadcastReceiver是对发送出来的 Broadcast进行过滤接受并响应的一类组件

9、第三层<provider>属性


<provider android:authorities="list"
android:enabled=["true" | "false"]
android:exported=["true" | "false"]
android:grantUriPermissions=["true" | "false"]
android:icon="drawable resource"
android:initOrder="integer"
android:label="string resource"
android:multiprocess=["true" | "false"]
android:name="string"
android:permission="string"
android:process="string"
android:readPermission="string"
android:syncable=["true" | "false"]
android:writePermission="string">
<grant-uri-permission/>
<meta-data/>
</provider>


ContentProvider(数据存储)
【1】android:authorities:

标识这个ContentProvider,调用者可以根据这个标识来找到它


【2】android:grantUriPermission:

对某个URI授予的权限


【3】android:initOrder

10、第三层<uses-library>
用户库,可自定义。所有android的包都可以引用

11、第一层<supports-screens>


<supports-screens android:smallScreens=["true" | "false"]
android:normalScreens=["true" | "false"]
android:largeScreens=["true" | "false"]
android:anyDensity=["true" | "false"] />


这是在android1.6以后的新特性,支持多屏幕机制
各属性含义:这四个属性,是否支持大屏,是否支持中屏,是否支持小屏,是否支持多种不同密度

12、第二层<uses-configuration />与<uses-feature>性能都差不多


<uses-configuration android:reqFiveWayNav=["true" | "false"]
android:reqHardKeyboard=["true" | "false"]
android:reqKeyboardType=["undefined" | "nokeys" | "qwerty" | "twelvekey"]
android:reqNavigation=["undefined" | "nonav" | "dpad" | "trackball" |
"wheel"]
android:reqTouchScreen=["undefined" | "notouch" | "stylus" | "finger"] />
<uses-feature android:glEsVersion="integer"
android:name="string"
android:required=["true" | "false"] />


这两者都是在描述应用所需要的硬件和软件特性,以便防止应用在没有这些特性的设备上安装。

13、第二层<uses-sdk />


<uses-sdk android:minSdkVersion="integer"
android:targetSdkVersion="integer"
android:maxSdkVersion="integer"/>


描述应用所需的api level,就是版本,目前是android 2.2 = 8,android2.1 = 7,android1.6 = 4,android1.5=3,在此属性中可以指定支持的最小版本,目标版本以及最大版本

14、第二层<instrumentation />


<instrumentation android:functionalTest=["true" | "false"]
android:handleProfiling=["true" | "false"]
android:icon="drawable resource"
android:label="string resource"
android:name="string"
android:targetPackage="string"/>


定义一些用于探测和分析应用性能等等相关的类,可以监控程序。在各个应用程序的组件之前instrumentation类被实例化

android:functionalTest(解释:instrumentation类是否能运行一个功能测试,默认为false)


15、<permission>、<uses-permission>、<permission-tree />、<permission-group />区别~
最常用的当属<uses-permission>,当我们需要获取某个权限的时候就必须在我们的manifest文件中声明,此<uses-permission>与<application>同级。通常情况下我们不需要为自己的应用程序声明某个权限,除非你提供了供其他应用程序调用的代码或者数据。这个时候你才需要使用<permission> 这个标签。很显然这个标签可以让我们声明自己的权限。比如:<permission android:name="com.teleca.project.MY_SECURITY" . . . />
那么在activity中就可以声明该自定义权限了,如:

<application . . .>
<activity android:name="XXX" . . . >
android:permission="com.teleca.project.MY_SECURITY"> </activity>
</application>


当然自己声明的permission也不能随意的使用,还是需要使用<uses-permission>来声明你需要该权限


<permission-group> 就是声明一个标签,该标签代表了一组permissions


<permission-tree>是为一组permissions声明了一个namespace
作者:johnWcheung 发表于2016/9/4 15:57:00 原文链接
阅读:111 评论:0 查看评论

android高级界面

$
0
0

RadioButton与CheckBox

  • RadioGroup 和RadioButton

RadioButton一定要放在RadioGroup
Checked=“true”当按钮选中后
勾选事件监听
OnCheckedChangeListener
练习
石头,剪刀,布
MainActivity.java

import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.RadioButton;
import android.widget.TextView;
public class MainActivity extends Activity implements OnClickListener {
    TextView tv;
    Button btn_begin;
    RadioButton radiobtn[]=new RadioButton[3];
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        radiobtn[0] =(RadioButton) findViewById(R.id.radio0);
        radiobtn[1]=(RadioButton) findViewById(R.id.radio1);
        radiobtn[2]=(RadioButton) findViewById(R.id.radio2);
        tv=(TextView) findViewById(R.id.textView1);
        btn_begin=(Button) findViewById(R.id.btn_begin);
        btn_begin.setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        //生成随机数
         int random=(int) (Math.random()*10+10);

         new AsyncTask<Integer, Integer, String>() {
             //后台线程
             @Override
                protected String doInBackground(Integer... params) {
                     int num=params[0];
                     int i=0;
                     while(i<num){
                         i++;
                         publishProgress(i%radiobtn.length);
                         try {
                            Thread.sleep(200);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                     }
                     return radiobtn[i%radiobtn.length].getText().toString();
                }
             protected  void onProgressUpdate(Integer... values) {

                 int count=values[0];
                 radiobtn[count].setChecked(true);
             };

             protected void onPostExecute(String result) {
                 super.onPostExecute(result);
                 tv.setText(result);
             };
        }.execute(random);
    }
}

Activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.android_829_radiobutton.MainActivity" >
    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="70dp"
        android:textSize="30sp"
        android:text="猜拳" />
    <RadioGroup
        android:id="@+id/radioGroup1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/textView1"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="16dp" >
        <RadioButton
            android:id="@+id/radio0"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:checked="true"
            android:text="石头" />
        <RadioButton
            android:id="@+id/radio1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="剪刀" />
        <RadioButton
            android:id="@+id/radio2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="布" />
    </RadioGroup>
    <Button
        android:id="@+id/btn_begin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@+id/radioGroup1"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="27dp"
        android:text="开始" />
</RelativeLayout>

菜单Menu

PopupMenu
MainActivity.java

import android.app.Activity;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.PopupMenu;
import android.widget.PopupMenu.OnMenuItemClickListener;
public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.button1).setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                //创建PopupMenu
                PopupMenu pop=new PopupMenu(MainActivity.this, v);
                //解析文件
                getMenuInflater().inflate(R.menu.main, pop.getMenu());
                //显示
                pop.show();
                //监听PopupMenu菜单
                pop.setOnMenuItemClickListener(new OnMenuItemClickListener() {

                    @Override
                    public boolean onMenuItemClick(MenuItem item) {
                        int id=item.getItemId();
                        if(id==R.id.item2){
                            finish();
                        }
                        return false;
                    }
                });
            }
        });
    }
}

main.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="com.example.android_829_menu.MainActivity" >
    <item
        android:id="@+id/item0"
        android:orderInCategory="100"
        android:showAsAction="never"
        android:title="登录"/>
    <item
        android:id="@+id/item1"
        android:orderInCategory="100"
        android:showAsAction="never"
        android:title="注册"/>
    <item
        android:id="@+id/item2"
        android:orderInCategory="100"
        android:showAsAction="never"
        android:title="退出"/>
</menu>

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.android_829_menu.MainActivity" >
    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="137dp"
        android:text="Button" />
</RelativeLayout>

OptionMenu
点击menu按钮,显示菜单

@Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.main, menu);
 //添加子菜单  
        SubMenu sub1=menu.addSubMenu("设置");  
        sub1.add(1,SET_ITEM1,300,"设置声音");  
      sub1.add(1,SET_ITEM2,400,"设置桌面");  
      SubMenu sub2=menu.addSubMenu("选择");  
      sub2.add(1,SET_ITEM3,300,"选择一");  
      sub2.add(1,SET_ITEM4,400,"选择二");  

        return true;
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        if (id == R.id.item0) {
            return true;
        }else if(id==R.id.item1){
            finish();
            return true;
        }else if(id==R.id.item2){
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

ContextMenu
长按控件,显示菜单

TextView tv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv=(TextView) findViewById(R.id.textView1);
        //注册tv,当用户长按tv时,触发上下文菜单
        registerForContextMenu(tv);
    }
  //创建上下文菜单
    @Override
    public void onCreateContextMenu(ContextMenu menu, View v,
            ContextMenuInfo menuInfo) {
        // TODO Auto-generated method stub
        super.onCreateContextMenu(menu, v, menuInfo);

        menu.addSubMenu(0, 1, 0, "收藏");
        menu.addSubMenu(0, 2, 0, "删除");

        Toast.makeText(MainActivity.this, "长按后", Toast.LENGTH_SHORT).show();
    }
   @Override
    public boolean onContextItemSelected(MenuItem item) {
           if(item.getItemId() == 1){
                Toast.makeText(this, "收藏", Toast.LENGTH_SHORT).show();
            }else if(item.getItemId() == 2){
                Toast.makeText(this, "删除", Toast.LENGTH_SHORT).show();
            }
           return super.onContextItemSelected(item);
    }

对话框

对话框是在当前界面弹出的一个小窗口,用于显示重要提示信息,提示用户输入信息,确认信息,或者显示某种状态,如下载进度,退出提示等等。一般情况下,用户要与对话框进行交互,然后返回到被遮盖的界面以继续运行当前的应用程序。
AlertDialog常用方法
要创建一个AlertDialog就要用到AlertDialog.Builder中的create()方法
1. setTitle:为对话框设置标题
2. setIcon:为对话框设置图标
3. setMessage:为对话框设置内容
4. setView:为对话框设置自定义样式
5. setItems:设置对话框要显示的一个list
6. setMutiChoiceItems:设置对话框显示一系列的复选框
7. setSingleChoiceItems:设置单选按钮
8. setNeutralButton:普通按钮
9. setPositiveButton:确认按钮
10. setNegativeButton:取消按钮
ProgressDialog

MainActivity.java
import android.app.Activity;
import android.app.ProgressDialog;
import android.app.AlertDialog.Builder;
import android.content.DialogInterface;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;


public class MainActivity extends Activity implements OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.button1).setOnClickListener(this);
        findViewById(R.id.button2).setOnClickListener(this);
        findViewById(R.id.button3).setOnClickListener(this);
        findViewById(R.id.button4).setOnClickListener(this);
        findViewById(R.id.button5).setOnClickListener(this);
        findViewById(R.id.button6).setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        int id=v.getId();
        if(id==R.id.button1){
            //默认对话框
            onCreateNormalDialog();
        }else if(id==R.id.button2){
            //单选按钮对话框
            onCreateSingleChoiceItems();
        }else if(id==R.id.button3){
            //多选按钮对话框
            onCreateMutiChoiceItems();
        }else if(id==R.id.button4){
            //列表按钮对话框
            onCreateItems();
        }else if(id==R.id.button5){
            //自定义对话框
            createDialog();
        }else if(id==R.id.button6){
            CreateProgressDialog();
        }
    }
    private void CreateProgressDialog() {
        final ProgressDialog pd=ProgressDialog.show(this, "搜索网络", "请耐心等待...");
        new Thread(new Runnable() {

            @Override
            public void run() {

                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                //结束ProgressDialog
                pd.dismiss();
            }
        }).start();
    }

    private void createDialog() {
        Builder builder=new Builder(this);
        builder.setTitle("注册");
        //自定义布局
        View view =LayoutInflater.from(this).inflate(R.layout.register, null);
        builder.setView(view);
        builder.setPositiveButton("确定", null);
        builder.setNegativeButton("取消", null);
        builder.create();
        builder.show();
    }
    private void onCreateItems() {
        Builder b=new Builder(this);

        b.setIcon(R.drawable.ic_launcher);
        b.setTitle("部门成员列表");
        String items[]={"项目经理","策划","测试","美工","程序员"};
        b.setItems(items, new DialogInterface.OnClickListener() {

            @Override
            public void onClick(DialogInterface dialog, int which) {
              Log.d("Tag", "which="+which);
            }
        });

        b.create();
        b.show();
    }

    private void onCreateMutiChoiceItems() {
        Builder b=new Builder(this);
        b.setTitle("爱好");
        b.setIcon(R.drawable.ic_launcher);

        String items[]={"篮球","足球","乒乓球","排球"};

        b.setMultiChoiceItems(items, null, new DialogInterface.OnMultiChoiceClickListener() {

            @Override
            public void onClick(DialogInterface dialog, int which, boolean isChecked) {
                Log.d("Tag", "which="+which+",isChecked="+isChecked);
            }
        });
        b.create();
        b.show();
    }
    private void onCreateSingleChoiceItems() {
        Builder builder =new Builder(this);

        builder.setTitle("请选择性别");
        builder.setIcon(R.drawable.ic_launcher);
        String items[]={"男","女","保密"};
        builder.setSingleChoiceItems(items, 2, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Log.d("Tag", "which="+which);
            }
        });

        builder.create();
        builder.show();
    }
    private void onCreateNormalDialog() {
        Builder builder=new Builder(this);

        builder.setTitle("退出");
        builder.setIcon(R.drawable.ic_launcher);
        builder.setMessage("确定要退出吗?");
        builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {

            @Override
            public void onClick(DialogInterface dialog, int which) {
                finish();
            }
        });
        builder.setNeutralButton("中间", null);
        builder.setNegativeButton("取消", null);
        builder.create();
        builder.show();
    }
}

Register.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <EditText
        android:id="@+id/editText1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10" 
        android:hint="输入用户名">

        <requestFocus />
    </EditText>

    <EditText
        android:id="@+id/editText2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:inputType="textPassword"
        android:hint="输入密码" />

    <EditText
        android:id="@+id/editText3"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:ems="10"
        android:inputType="textPassword" 
        android:hint="输入密码"/>

</LinearLayout>

activity_main.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.android_829_alertdialog.MainActivity" >
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:orientation="vertical" >
        <Button
            android:id="@+id/button1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="普通对话框" />
        <Button
            android:id="@+id/button2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="单选按钮对话框" />
        <Button
            android:id="@+id/button3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="多选按钮对话框" />
        <Button
            android:id="@+id/button4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="列表对话框" />
        <Button
            android:id="@+id/button5"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="自定义对话框" />
        <Button
            android:id="@+id/button6"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="进度条对话框" />
    </LinearLayout>
</RelativeLayout>

PopupWindow
PopupWindow这个类用来实现一个弹出框,可以使用任意布局View作为其内容,这个弹出框悬浮在当前activity之上。
构造PopupWindow
注意:生成一个PopupWindow必须设置三个条件:View contentView,int Width,int height, 少任意一个就不能弹出PopupWindow。

作者:u013238646 发表于2016/9/4 16:02:18 原文链接
阅读:94 评论:0 查看评论

Android工具类--date工具类

$
0
0

将毫秒转成时间,当前时间, 获取当前时间的字符串,获取指定时间的字符串,只到日期,获得当前日期的前段日期,格式化时间,格式化日期,获取指定日期之后的日期字符串,获得当前系统日期与本周一相差的天数,获得某日前后的某一天, 将 yyyy-MM-dd HH:mm 格式字符串转换为时间,根据日期字符串,返回今天,昨天或日期,返回当前日期所在星期,比较两个日期前后 ,得到两个日期之间的年,获取年龄等等


import java.sql.Timestamp;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

public class TimestampTool {

	/**
	 * 将毫秒转成时间
	 */
	public static String getTimeMillisToDate(long l) {
		Timestamp d = new Timestamp(l);
		return d.toString().substring(0, 19);
	}

	/**
	 * 当前时间
	 * 
	 * @return Timestamp
	 */
	public static Timestamp crunttime() {
		return new Timestamp(System.currentTimeMillis());
	}

	/**
	 * 获取当前时间的字符串
	 * 
	 * @return String ex:2006-07-07
	 */
	public static String getCurrentDate() {
		Timestamp d = crunttime();
		return d.toString().substring(0, 10);
	}

	/**
	 * 获取当前时间的字符串
	 * 
	 * @return String ex:2006-07-07 22:10:10
	 */
	public static String getCurrentDateTime() {
		Timestamp d = crunttime();
		return d.toString().substring(0, 19);
	}

	public static String getWeekDay() {
		Calendar date = Calendar.getInstance();
		date.setTime(crunttime());
		return new SimpleDateFormat("EEEE").format(date.getTime());
	}

	/**
	 * 获取指定时间的字符串,只到日期
	 * 
	 * @param t
	 *            Timestamp
	 * @return String ex:2006-07-07
	 */
	public static String getStrDate(Timestamp t) {
		return t.toString().substring(0, 10);
	}

	/**
	 * 获取指定时间的字符串
	 * 
	 * @param t
	 *            Timestamp
	 * @return String ex:2006-07-07 22:10:10
	 */
	public static String getStrDateTime(Timestamp t) {
		return t.toString().substring(0, 19);
	}

	/**
	 * 获得当前日期的前段日期
	 * 
	 * @param days
	 * @return String
	 */
	public static String getStrIntervalDate(String days) {
		SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
		Calendar cal = Calendar.getInstance();
		cal.add(Calendar.DATE, -Integer.parseInt(days));
		String strBeforeDays = sdf.format(cal.getTime());
		return strBeforeDays;
	}

	/**
	 * 格式化时间
	 * 
	 * @param dt
	 *            String -> yyyy-MM-dd hh:mm:ss
	 * @return java.util.Date.Date -> yyyy-MM-dd hh:mm:ss
	 */
	public static Date parseDateTime(String dt) {
		Date jDt = new Date();
		try {
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
			if (dt.length() > 10) {
				jDt = sdf.parse(dt);
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return jDt;
	}

	/**
	 * 格式化时间yyyy-MM-dd HH:mm:ss
	 * 
	 * @param date
	 *            java.util.Date
	 * @return String -> yyyy-MM-dd HH:mm:ss
	 */
	public static String parseDateTime(Date date) {
		String s = null;
		if (date != null) {
			try {
				SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
				s = f.format(date);
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return s;
	}

	/**
	 * 格式化日期
	 * 
	 * @param dt
	 *            String -> yyyy-MM-dd
	 * @return java.util.Date.Date -> yyyy-MM-dd
	 */
	public static Date parseDate(String dt) {
		Date jDt = new Date();
		try {
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
			if (dt.length() >= 8) {
				jDt = sdf.parse(dt);
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return jDt;
	}

	/**
	 * 格式化时间yyyy-MM-dd
	 * 
	 * @param date
	 *            java.util.Date
	 * @return String -> yyyy-MM-dd
	 */
	public static String parseDate(Date date) {
		String s = null;
		try {
			if (date != null) {
				SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd");
				s = f.format(date);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return s;
	}

	/**
	 * 
	 * @param dt
	 * @return String
	 */
	public static String getLongDateFromShortDate(String dt) {
		String strDT = dt;
		try {
			if (strDT != null && strDT.length() <= 10) {
				strDT = dt.trim() + " 00:00:00";
			}
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return strDT;
	}

	/**
	 * 
	 * @param dt
	 * @return String
	 */
	public static String getShortDateToHHMM(String dt) {
		String jDt = dt;
		try {
			if (jDt != null && jDt.length() <= 10) {
				jDt = jDt + " 00:00";
			}
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
			jDt = sdf.parse(jDt).toLocaleString();
		} catch (Exception ex) {
			ex.printStackTrace();
		}
		return jDt;
	}

	/**
	 * 
	 * @param dateStr
	 * @return String
	 */
	public static String formatDateToHHMM(String dateStr) {
		String resultDate = null;
		try {
			if (dateStr.length() > 10) {
				SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:ss");
				Date date = sdf.parse(dateStr);
				resultDate = sdf.format(date);
			} else
				resultDate = dateStr;
		} catch (ParseException e) {
			e.printStackTrace();
		}
		return resultDate;
	}

	/**
	 * 返回日期 格式:2006-07-05
	 * 
	 * @param str
	 * @return Timestamp
	 */
	public static Timestamp date(String str) {
		Timestamp tp = null;
		if (str.length() <= 10) {
			String[] string = str.trim().split("-");
			int one = Integer.parseInt(string[0]) - 1900;
			int two = Integer.parseInt(string[1]) - 1;
			int three = Integer.parseInt(string[2]);
			tp = new Timestamp(one, two, three, 0, 0, 0, 0);
		}
		return tp;
	}

	// 获取指定日期之后的日期字符串 如 2007-04-15 后一天 就是 2007-04-16
	public static String getNextDay(String strDate, int day) {
		if (strDate != null && !strDate.equals("")) {
			Calendar cal1 = Calendar.getInstance();
			String[] string = strDate.trim().split("-");
			int one = Integer.parseInt(string[0]) - 1900;
			int two = Integer.parseInt(string[1]) - 1;
			int three = Integer.parseInt(string[2]);
			cal1.setTime(new Date(one, two, three));
			cal1.add(Calendar.DAY_OF_MONTH, day);
			SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
			return formatter.format(cal1.getTime());
		} else {
			return null;
		}
	}

	// 获取指定日期之后的日期字符串 如 2007-02-28 后一年 就是 2008-02-29 (含闰年)
	public static String getNextYear(String strDate, int year) {
		Calendar cal1 = Calendar.getInstance();
		String[] string = strDate.trim().split("-");
		int one = Integer.parseInt(string[0]) - 1900;
		int two = Integer.parseInt(string[1]) - 1;
		int three = Integer.parseInt(string[2]);
		cal1.setTime(new Date(one, two, three));
		cal1.add(Calendar.YEAR, year);
		SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
		return formatter.format(cal1.getTime());
	}

	/**
	 * 返回时间和日期 格式:2006-07-05 22:10:10
	 * 
	 * @param str
	 * @return Timestamp
	 */
	public static Timestamp datetime(String str) {
		Timestamp tp = null;
		if (str != null && str.length() > 10) {
			String[] string = str.trim().split(" ");
			String[] date = string[0].split("-");
			String[] time = string[1].split(":");
			int date1 = Integer.parseInt(date[0]) - 1900;
			int date2 = Integer.parseInt(date[1]) - 1;
			int date3 = Integer.parseInt(date[2]);
			int time1 = Integer.parseInt(time[0]);
			int time2 = Integer.parseInt(time[1]);
			int time3 = Integer.parseInt(time[2]);
			tp = new Timestamp(date1, date2, date3, time1, time2, time3, 0);
		}
		return tp;
	}

	/**
	 * 返回日期和时间(没有秒) 格式:2006-07-05 22:10
	 * 
	 * @param str
	 * @return Timestamp
	 */
	public static Timestamp datetimeHm(String str) {
		Timestamp tp = null;
		if (str.length() > 10) {
			String[] string = str.trim().split(" ");
			String[] date = string[0].split("-");
			String[] time = string[1].split(":");
			int date1 = Integer.parseInt(date[0]) - 1900;
			int date2 = Integer.parseInt(date[1]) - 1;
			int date3 = Integer.parseInt(date[2]);
			int time1 = Integer.parseInt(time[0]);
			int time2 = Integer.parseInt(time[1]);
			tp = new Timestamp(date1, date2, date3, time1, time2, 0, 0);
		}
		return tp;
	}

	/**
	 * 获得当前系统日期与本周一相差的天数
	 * 
	 * @return int
	 */
	private static int getMondayPlus() {
		Calendar calendar = Calendar.getInstance();
		// 获得今天是一周的第几天,正常顺序是星期日是第一天,星期一是第二天......
		int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); // 星期日是第一天
		return (dayOfWeek == 1) ? -6 : 2 - dayOfWeek;
	}

	/**
	 * 获得距当前时间所在某星期的周一的日期 例: 0-本周周一日期 -1-上周周一日期 1-下周周一日期
	 * 
	 * @param week
	 *            int
	 * @return java.util.Date
	 */
	public static Date getMondayOfWeek(int week) {
		int mondayPlus = getMondayPlus(); // 相距周一的天数差
		GregorianCalendar current = new GregorianCalendar();
		current.add(GregorianCalendar.DATE, mondayPlus + 7 * week);
		return current.getTime();
	}

	/**
	 * 获得某日前后的某一天
	 * 
	 * @param date
	 *            java.util.Date
	 * @param day
	 *            int
	 * @return java.util.Date
	 */
	public static Date getDay(Date date, int day) {
		GregorianCalendar c = new GregorianCalendar();
		c.setTime(date);
		c.add(GregorianCalendar.DATE, day);
		return c.getTime();
	}

	/**
	 * 获得距当前周的前后某一周的日期
	 * 
	 * @param week
	 *            int
	 * @return String[]
	 */
	public static String[] getDaysOfWeek(int week) {
		String[] days = new String[7];
		Date monday = getMondayOfWeek(week); // 获得距本周前或后的某周周一
		Timestamp t = new Timestamp(monday.getTime());
		days[0] = getStrDate(t);
		for (int i = 1; i < 7; i++) {
			t = new Timestamp(getDay(monday, i).getTime());
			days[i] = getStrDate(t);
		}
		return days;
	}

	/***
	 * MCC的UTC时间转换,MCC的UTC不是到毫秒的
	 * 
	 * @param utc
	 * @return java.util.Date
	 */
	public static Date mccUTC2Date(long utc) {
		Date d = new Date();
		d.setTime(utc * 1000); // 转成毫秒
		return d;
	}

	// 将长时间格式字符串转换为时间 yyyy-MM-dd HH:mm:ss
	public static Date strToDateLong(String strDate) {
		SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		ParsePosition pos = new ParsePosition(0);
		Date strtodate = (Date) formatter.parse(strDate, pos);
		if (strtodate == null) {
			formatter = new SimpleDateFormat("yyyy-MM-dd");
			strtodate = (Date) formatter.parse(strDate, pos);
		}
		return strtodate;
	}

	// 将 yyyy-MM-dd HH:mm 格式字符串转换为时间
	public static Date strToDateTime(String strDate) {
		SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm");
		ParsePosition pos = new ParsePosition(0);
		Date strtodate = (Date) formatter.parse(strDate, pos);
		if (strtodate == null) {
			formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
			strtodate = (Date) formatter.parse(strDate, pos);
		}
		return strtodate;
	}

	// 根据输入的字符串返回日期字符串 2006-07-07 22:10 2006-07-07
	public static String getStrDate(String str) {
		if (str.length() > 10) {
			String[] string = str.trim().split(" ");
			return string[0];
		} else {
			return getCurrentDate();
		}
	}

	// 获取当前时间的字符串 2006-07-07 22:10:10 2006-07-07_221010
	public static String getStrDateTime() {
		Timestamp d = crunttime();
		return d.toString().substring(0, 19).replace(":", "").replace(" ", "_");
	}

	// 根据日期字符串,返回今天,昨天或日期
	public static String getDayOrDate(String str) {
		if (str != null && !str.equals("")) {
			if (getNextDay(str, 0).equals(getCurrentDate())) {
				str = "今天";
			} else if (getNextDay(str, 1).equals(getCurrentDate())) {
				str = "昨天";
			}
		}
		return str;
	}

	// 返回当前日期所在星期,2对应星期一
	public static int getMonOfWeek() {
		Calendar cal1 = Calendar.getInstance();
		cal1.setTime(new Date());
		return cal1.get(Calendar.DAY_OF_WEEK);
	}

	public static void main(String[] args) {
		System.out.println(System.currentTimeMillis());
	}

	/**
	 * 获取当前日期之前的日期字符串 如 2007-04-15 前5月 就是 2006-11-15
	 */
	public static String getPreviousMonth(int month) {
		Calendar cal1 = Calendar.getInstance();
		cal1.setTime(new Date());
		cal1.add(Calendar.MONTH, -month);
		SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
		return formatter.format(cal1.getTime());

	}

	public static String getStrYear(int year) {
		Calendar cal1 = Calendar.getInstance();
		cal1.setTime(new Date());
		cal1.add(Calendar.YEAR, -year);
		SimpleDateFormat formatter = new SimpleDateFormat("yyyy");
		return formatter.format(cal1.getTime()) + "年份";
	}

	/**
	 * 比较两个日期前后 可以大于或等于
	 * 
	 * @param starDate
	 * @param endDate
	 * @return
	 */
	public static boolean compareTwoDays(String starDate, String endDate) {
		Calendar cal_start = Calendar.getInstance();
		Calendar cal_end = Calendar.getInstance();
		cal_start.setTime(parseDate(starDate));
		cal_end.setTime(parseDate(endDate));
		return cal_end.after(cal_start);
	}

	public static int getDaysBetween(java.util.Calendar d1,
			java.util.Calendar d2) {
		if (d1.after(d2)) { // swap dates so that d1 is start and d2 is end
			java.util.Calendar swap = d1;
			d1 = d2;
			d2 = swap;
		}
		int days = d2.get(java.util.Calendar.DAY_OF_YEAR)
				- d1.get(java.util.Calendar.DAY_OF_YEAR);
		int y2 = d2.get(java.util.Calendar.YEAR);
		if (d1.get(java.util.Calendar.YEAR) != y2) {
			d1 = (java.util.Calendar) d1.clone();
			do {
				days += d1.getActualMaximum(java.util.Calendar.DAY_OF_YEAR);
				d1.add(java.util.Calendar.YEAR, 1);
			} while (d1.get(java.util.Calendar.YEAR) != y2);
		}
		return days;
	}

	// 得到两个日期之间的年
	public static int dateDiffYear(String starDate, String endDate) {
		int result = 0;
		Calendar d1 = Calendar.getInstance();
		Calendar d2 = Calendar.getInstance();
		d1.setTime(parseDate(starDate));
		d2.setTime(parseDate(endDate));

		// 日期大小翻转
		if (d1.after(d2)) { // swap dates so that d1 is start and d2 is end
			java.util.Calendar swap = d1;
			d1 = d2;
			d2 = swap;
		}
		int yy = d2.get(Calendar.YEAR) - d1.get(Calendar.YEAR);
		int mm = d2.get(Calendar.MONTH) - d1.get(Calendar.MONTH);
		if (mm < 0) {
			result = yy - 1;
		}
		if (mm > 0) {
			result = yy;
		}
		if (mm == 0) {
			if ((d2.getTimeInMillis() - d1.getTimeInMillis()) >= 0) {
				result = yy;
			} else {
				result = yy - 1;
			}
		}
		return result;
	}

	// 获取年龄
	public static int getAgeByBirth(String starDate) {
		return dateDiffYear(starDate, getCurrentDate());
	}
}


作者:dl10210950 发表于2016/9/4 16:16:57 原文链接
阅读:85 评论:0 查看评论

无人机攒机基础2

$
0
0

电机

航模电机分无刷电机和有刷电机两种。无刷电机与有刷电机相比,缺少了交替变换电磁场的换向电刷,在运转时摩擦力减小,噪音降低,运转时不产生电火花,在目前的无人机市场上已成为主导,本章我们将介绍主要无刷电机。 无刷电机通过电子调速器(俗称电调)将输入的直流电变成三相交流电,利用三相交流电产生的旋转磁场驱动转子转动。电调通过从遥控器接收机接收控制信号,控制电机的转速,完成无人机的俯仰、横滚等动作。
bldc
上图展示了无刷电机的工作原理。

电机KV值

电机的重要参数是KV值,它指电机每增加1V电压,电机空转的转速每分钟增加的次数,我们可以用一个公式来表示:电机的转速(空载)=KV值*电压 例如,KV1000的电机,在10V电压下,它的转速(空载)就是 10000转/分钟。 KV值高代表马达的内阻小,电流大,转速快,相同电压下爆发出来的功率高,拥有很好的极限转速,反之亦然。但受到电机的设计与材料限制,电机会有一个功率上限。 电机的KV值越大所产生的扭力就越小,KV值越低产生的扭力越大,所以KV值决定配什么样的桨。通常KV值高的电机配小的高速桨,KV值低的电机配大的低速桨,依实际情况而定。

比如KV值2100的电机,在11.1V的电压下,转速(空转)2100*11.1=23310转每分钟,此时它可能刚好适合用5030桨,如果你硬给它配8060桨,电机可能也转得动,但电机和电调可能会烧掉。因为在这种情况下要带动8060桨,需要更大的扭力,而转速越快,提供的扭力就小。并非KV值2100的电机在任何情况下都适合带5030桨,如果电压高了,转速就更高了,扭力会减小,就需要换小桨。反之,如果在电压7.4V的情况下,就可以用稍大点的桨。

电机效率值

电机效率的标注方式是:G/W(克/每瓦) ,但电机的功率和拉力并不是成正比的,比如飓风3508KV380电机在111W的时候拉力可能是1010G,但244W时可能只有1570G。

大多数电机在3A~5A的电流下效率是最高的。正常飞行中效率通常保持在8G/1W以上,以保证续航能力。 以3508电机为例,假定参数显示的最大拉力为1890克,那么以它为电机的四旋翼理论的最大拉力就是7560克,但在实际飞行中,这样的电机组合肯定带不动7560克的机身。因为四轴的升力除了把自身抬起来之外,还要用一部分力来前进后退,左右横滚。除此之外还要考虑最关键的抗风。 建议是保留60%左右的升力来做这些飞行动作和抗风,避免因电池电压降低后因升力不足而炸鸡。仍以3508电机为例,其四个最大拉力是7560克,整机重量不超过最大拉力的40%,即3024克。保留的拉力值太少,会导致电机高负荷运行,既降低效率,又增加电机自身震动,影响飞控自稳,大风一起容易侧飘或炸机。

电调

无刷电机与有刷电机相比取消了碳刷结构,所以无刷电机需要一个能替代碳刷功能的部件,它就是电调。

电调的全称是电子调速器(electronic speed controller ,简称ESC),电调对应使用的电机类型不同,分为无刷电调和有刷电调。有刷电机转动时可以不用电调,但如果没有电调就无法控制电机的转速。而无刷电机必须要有电调,否则不能转动。

电调的功能

电调可以将输入的直流电转换为三相交流电以供电机使用,并根据飞控发出的控制信号控制电机的电压及电流大小,从而驱动电机实现需要的转速输出。电调具有电压变化器的作用,它通过转换电池电压以供接收机和飞控板工作。电机运转时的电流很大,当电机正常工作时,如果没有电调的存在,飞控板根本无法承受这样大的电流。

电调的连接

无刷电调的连接如下图所示:输入线与电池连接,信号线输入线与接收机连接,三根信号输出线(有刷两根)与电机连接。
这里写图片描述
电调一般有电源输出功能,即在信号线的正负极之间,有5V左右的电压输出,通过信号线为接收机供电,接收机再为舵机等控制设备供电。电调的参数A电调最常见的参数A指电调可稳定调整输出的电流量。例如30A的电调最大可稳定输出30A的电流,如果超标准使用,会导致电调烧毁。很多模友问:既然电调有最大电流的限制,那么选个大电流的电调就是不是就可以一劳永逸。其实不然,电调的大小与输出的电流成正比,电调的调整输出电流值越大,电调的重量也越大,这等于增加了无人机额外的重量。对于初学者而言,组装小四轴时使用20A~30A的电调基本就够用了。

BEC和UBEC的区别

BEC的全称是Battey Elimination Circuit,中文翻译成免电池电路。早期的航模接收机和舵机需要一个5V或6V的单独电池供电,模友们想到在电调里内置一个电路模块,将12V电池输出的电压转换到5V­6V给接收机和舵机使用,以便把2个电源并成1个电源(电机还是用12V供电的),这就是BEC(免电池电路)的由来。

BEC大多采用线性稳压方式,它的优点是线路简单、体积小,只要一个稳压管就可以;但缺点是转换效率不高,稳压的时候能量损耗大(线性稳压效率一般只有65%­70%),所以在工作过程中稳压管会很烫(电调发烫的主要热量就来自这个稳压管,真正控制电机的MOS开关管其实发热量不大的)。由于其效率不高,输出电流最大也就1A左右。

由于线性稳压的固有缺点,内置的BEC无法满足新的电流要求,模友们又想到把内置的BEC搬出来,单独做个体积更大输出电流更强的稳压模块,以满足大功率舵机的需求,它就是UBEC(Ultra Battery Elimination Circuit)。稳压模块独立后,体积限制已不成问题,索性就不采用低效率的线性稳压了,改用开关电源的方式来稳压。开关电源的优点是转换效率高(做得好的甚至能达到98%),稳压过程损耗小,发热降低。开关电源虽然有诸多优点,但元件过多使它体积偏大,并产生较强的电磁干扰,一般UBEC的说明书上都会建议将UBEC放置得离接收机越远越好。有模友反映,UBEC除了产生较强的电磁辐射外,其电源输出也并不十分纯净,有电源波纹存在,这样对PPM的遥控方式影响就很大,所以建议还是用PCM遥控方式比较好。(PPM与PCM的区别在遥控器篇会有介绍)

电调的编程

电调有很多功能模式,电调的编程可以直接将电调连接至遥控接收机的油门输出通道(通常是3通道),按说明书的介绍在遥控器上通过搬动摇杆进行设置。通过遥控器设置电调,一定要接上电机,因为说明书上说的“滴滴”类的声音,是通过电机发出来的。另外还可以通过厂家的编程卡来进行设置(需要单独购买),这个方法简单,无需接遥控器。
电调有快速响应和慢速响应的区别,四轴无人机需要快速响应的电调,可以通过编程来设置响应速度。为了保险起见,一定要将购买的电调设置一致,否则容易造成电调的启动模式不一致,使电机有的转得快,有的转得慢。

电池和电调的搭配

电池和电调进行搭配时需要注意几个原则:
一是电池的电压一定不能超过电调的最高承载电压。
二是电池的电流持续输出应大于电调的最大持续电流输出。例如以2200mAh/20C电池为例,它的电流输出是2200*20/1000=44A,如果电调小于44A,则配合起来没问题。
三是电机工作电压由电调决定,而电调电压由电池输出决定,所以电池的电压要等于或小于电机的最大电压。
四是电调最大电压不能超过电机能承受的最大电压。

电机和电调的搭配

在确定了适合所选定的无人机的电机后,需要依据选用的电机确定它的最大电流,然后挑选电调和电池。电调的输出电流必须大于电机的最大电流,电池输出电流一样要大于电机的最大电流,越大越好。例如,电机带螺旋桨的最大负载电流是20A,就必须选取输出20A以上电流的电调,越大越保险。
电池的选取和电调一样,它的输出电流是越大越好。电池的放电电流达不到电调的电流时电调就发挥不了最高性能,而且电池会发热,产生爆炸,所以一般情况都需要电池的电流大于电调的电流。

refer

  1. http://www.exuav.com/forum.php?mod=viewthread&tid=664&extra=page%3D1%26filter%3Dtypeid%26typeid%3D41
  2. http://www.exuav.com/forum.php?mod=viewthread&tid=650&extra=page%3D1%26filter%3Dtypeid%26typeid%3D41
  3. https://en.wikipedia.org/wiki/Brushless_DC_electric_motor
作者:wendox 发表于2016/9/4 16:21:32 原文链接
阅读:90 评论:0 查看评论

Open GL ES 三角形绘制

$
0
0

Open GL ES 三角形绘制

画三角形是open GL ES中最简单的入门项目,下面讲解具体的流程,方便自己总结工具类,没有别的意思。

在Android项目中引进open GL

为了使用OpenGL ES 2.0 API,需要添加如下声明:

<uses-feature android:glEsVersion="0x00020000" android:required="true" />

创建GLSurfaceView

GLSurfaceView是用来放置图形view的容器。所有的东西都是绘制在GLSurfaceView上面的,就相当于画布的概念,
这里先实现一个GLSurfaceView。扩展自GLSurfaceView,实现自己的MyGLSurfaceView

    public class MyGLSurfaceView extends GLSurfaceView {

        private Context mContext;

        public MyGLSurfaceView(Context context) {
            super(context);
        }

        public MyGLSurfaceView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    }  

创建Render

Renderer类(渲染器类),即 GLSurfaceView.Renderer的实现类,它控制了与它相关联的 GLSurfaceView 上绘制什么。
需要实现一下接口:

 @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {

    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {

    }

    @Override
    public void onDrawFrame(GL10 gl) {

    }
  • onSurfaceCreated()函数在surface创建的时候调用,所以初始化的工作在里面完成。
  • onSurfaceChanged()函数在surface发生改变的时候调用;
  • onDrawFrame()函数是完成surfaceview上面显示内容的绘制,每一帧的绘制都会去调用。

基础参数设置

    //给shader中的变量传参数时候用到的
    private final int mStrideBytes = 7 * 4;    //3 + 4   3表示坐标, 4表示颜色  一共7个float变量,每个变量4字节//一次性读取 7 x 4个字节
    private final int mPositionOffset = 0;   //顶点坐标的偏移量
    private final int mPositionDataSize = 3;   //3个为一组,表示一个顶点坐标
    private final int mColorOffset = 3;    //颜色数据的变异量为3, 也就是每次读取数据,从第三个开始是表示颜色的
    private final int mColorDataSize = 4;   //4个数据都是表示颜色的
  • mStrideBytes是指定buffer在读取数据的时候一次读取多少,7表示个数,7个数据, 4表示字节,7*4表示一次读取多少字节的数据。
    比如:
 -0.5f, -0.25f, 0.0f,    //point
1.0f, 0.0f, 0.0f, 1.0f, //color
  • mPositionOffset表示顶点坐标的偏移量
  • mPositionDataSize表示顶底每个顶点坐标用多少个数据表示,三个:-0.5f, -0.25f, 0.0f, //point
  • mColorOffset读取颜色数据时的偏移量,因为顶点坐标用3个数据表示,所以偏移量为3
  • mColorDataSize表示多少个数据表示一个颜色,4个参数分别为ARGB

顶点坐标和颜色坐标

//数据
    private final float vertexData[] = {
            // X, Y, Z,
            // R, G, B, A
            -0.5f, -0.25f, 0.0f,    //point
            1.0f, 0.0f, 0.0f, 1.0f, //color

            0.5f, -0.25f, 0.0f,     //point
            0.0f, 0.0f, 1.0f, 1.0f, //color

            0.0f, 0.559016994f, 0.0f,//point
            0.0f, 1.0f, 0.0f, 1.0f     //color
    };

可以看到,每组数据有7个,前面3个表示位置坐标,后面4个表示颜色值,可以结合前面的参数设置来理解。
这个数据是程序传入openGL的数据。

创建Buffer存放数据

    //数据的buffer
    private FloatBuffer mShaderDateBuffer;
    private FloatBuffer getVertexBuffer(float[] data) {
        //先 创建内存地址
        ByteBuffer vbb = ByteBuffer.allocateDirect(data.length * 4);   //每个float是4个字节
        //ByteOrder.nativeOrder()返回本地jvm运行的硬件的字节顺序.使用和硬件一致的字节顺序可能使buffer更加有效.
        vbb.order(ByteOrder.nativeOrder());
        FloatBuffer vertexBuffer = vbb.asFloatBuffer();
        vertexBuffer.put(data);
        vertexBuffer.position(0);

        return vertexBuffer;
    }

上面代码中的注释已经很清楚了,里面的代码大多数是固定的写法。

Matrix的设置

openGL 中有三个类型份额举证,分别是:
* Model Matrix 模型矩阵
* View Matrix 视图矩阵
* Projection Matrix 投影矩阵
由于这几个概念很绕,是个人都要糊弄一会儿才能搞清楚,下面就这几个矩阵,好好的糊弄糊弄。
所谓的坐标变换,就是将一个坐标系下的坐标,在另外一个坐标系中表示出来。
下图中世界坐标系下的相机:
坐标系中的相机
将相机定位在(1,0,1,1)(注意这个地方是用4维向量来表示,最后一个维度取值只能为0或者1,1表示点,0表示向量)。
照相机的指向可以用 n = (-1,0,-1,0) 用原点坐标(0,0,0,1)减去相机坐标得到的。同时设置照相机的观察正向和世界坐标系下的
y轴的方向一致, v = (0,1,0,0), 此时,利用向量的叉乘可以得到 相机坐标系下的第三个坐标轴的方向,u = n x v ;
计算得到u = (1,0,-1,0) ,这样照相机自己构成的坐标系为(u, v, n, P).

照相机坐标系
这个东西就是将坐标从世界坐标转换到相机坐标的矩阵,那世界坐标的原点为例(0,0,0,1), 有
原点从世界坐标转换到相机坐标

在opengl中,数据从用户构建的局部坐标系,经过一系列的处理,最终渲染在屏幕上面,主要经过了一下过程:

Open gl中只定义了裁剪坐标系、规范化设备坐标系以及屏幕坐标系,而局部坐标系、世界坐标系和相机坐标系是为了
方便用户的,用户在OpenGL中的转换如下:

从坐标来看,就是一下过程

下图中茶壶在Model Matrix中的定义
Model Matrix下的茶壶
世界坐标系下的茶壶:
世界坐标系下的茶壶
从世界坐标系到相机坐标系

为什么要将世界坐标系转化到相机坐标系,我们最终看到的就是相机拍到的,而不是上帝视角下看到的一切(纯属个人理解,不喜勿喷)。
从相机坐标系到裁剪坐标系,通过投影完成的。分为正交投影和透视投影两种。
最后读下来还是感觉很晕,的确很晕。那这些东西和上面提到的三个矩阵有什么关系呢?
基本上就是: Model Matrix是模型坐标系转换到世界坐标系用,View Matrix就是视图坐标系,用来转换到相机坐标系用的,Projection Matrix转化裁剪坐标系的。

ES中坐标系矩阵的计算

计算模型矩阵,里面调用了rotateM接口

    private float[] mModelMatrix = new float[16];       //模型矩阵
    private void initModelMatrix() {
        // Do a complete rotation every 10 seconds.
        long time = SystemClock.uptimeMillis() % 10000L;
        float angleInDegrees = (360.0f / 10000.0f) * ((int) time);

        // Draw the triangle facing straight on.
        // 模型矩阵设为单位矩阵
        Matrix.setIdentityM(mModelMatrix, 0);
        // angleInDegrees是旋转的角度,(0.0f, 0.0f, 1.0f)是模型矩阵
        Matrix.rotateM(mModelMatrix, 0, angleInDegrees, 0.0f, 0.0f, 1.0f);
    }

计算ViewMatrix,需要设置相机的位置,相机观察的方向,以及相机的观察正向向量

    private float[] mViewMatrix = new float[16];   //视图矩阵
    private void initViewMatrix() {
        //放置eye眼睛的位置
        final float eyeX = 0.0f;
        final float eyeY = 0.0f;
        final float eyeZ = 1.5f;

        //设置look方向
        // look也成为center, center到eye所形成的向量,称为视线方向,与真正的视线看过去的方向相反
        final float lookX = 0.0f;
        final float lookY = 0.0f;
        final float lookZ = -5.0f;

        //设置up坐标
        //eye的位置本身只代表一个坐标而已,但是 look向量和up向量结合右手螺旋准则才能唯一的确定一个坐标系
        //这个坐标系就是eye看到的坐标系,两者垂直知识为了与常用的三位坐标系一样
        final float upX = 0.0f;
        final float upY = 1.0f;
        final float upZ = 0.0f;

        //经过计算的到了 viewMatrix,
        Matrix.setLookAtM(mViewMatrix, 0, eyeX, eyeY, eyeZ, lookX, lookY, lookZ, upX, upY, upZ);

    }

投影矩阵,设置关于远近 以及投影屏幕的大小等属性。

    private float[] mProjectionMatrix = new float[16];   //投射矩阵
    private void initProjectionMatrix(int width, int height) {
        // Set the OpenGL viewport to the same size as the surface.
        GLES20.glViewport(0, 0, width, height);
        // Create a new perspective projection matrix. The height will stay the same
        // while the width will vary as per aspect ratio.
        final float ratio = (float) width / height;
        final float left = -ratio;
        final float right = ratio;
        final float bottom = -1.0f;
        final float top = 1.0f;
        final float near = 1.0f;
        final float far = 10.0f;

        Matrix.frustumM(mProjectionMatrix, 0, left, right, bottom, top, near, far);
    }

Shader数据传入

使用工具类:

    public static int compileShader(final int shaderType, final String shaderSource) {
        // 创建shader句柄
        int shaderHandle = GLES20.glCreateShader(shaderType);
        if (shaderHandle != 0) {
            // Pass in the shader source.
            // 使用glShaderSource()分别将顶点着色程序的源代码字符数组绑定到顶点着色器对象,将片段着色程序的源代码字符数组绑定到片段着色器对象;
            // 绑定作用
            GLES20.glShaderSource(shaderHandle, shaderSource);

            // Compile the shader.
            // 编译
            GLES20.glCompileShader(shaderHandle);

            // Get the compilation status.
            // 得到计算结果
            final int[] compileStatus = new int[1];
            GLES20.glGetShaderiv(shaderHandle, GLES20.GL_COMPILE_STATUS, compileStatus, 0);

            // If the compilation failed, delete the shader.
            // 结果审查
            if (compileStatus[0] == 0) {
                Log.e(TAG, "Error compiling shader: " + GLES20.glGetShaderInfoLog(shaderHandle));
                GLES20.glDeleteShader(shaderHandle);
                shaderHandle = 0;
            }
        }

        if (shaderHandle == 0) {
            throw new RuntimeException("Error creating shader.");
        }

        return shaderHandle;
    }

shaderType为两种,一种是GLES20.GL_VERTEX_SHADER,一种是GLES20.GL_FRAGMENT_SHADER。最终返回的是shader的句柄,后面要用到这个句柄传入参数,
计算。
链接程序
vertexShader

    uniform mat4 u_MVPMatrix;   //应用程序传入的变换矩阵 ,MVP 是modle view projection的意思,通过这个来计算最终的坐标
    attribute vec4 a_Position;  //应用程序传入的 顶点的坐标
    attribute vec4 a_Color;     //应用程序传入的 顶点颜色的坐标

    varying vec4 v_Color;   //这个变量会传到 fragment shader中处理

    void main() {
        v_Color = a_Color;
        gl_Position = u_MVPMatrix * a_Position;
    }

fragmentShader

    precision mediump float;   //精度

    varying vec4 v_Color;   //这个名字一定要和在 vertex shader中声明的一样

    void main() {
        gl_FragColor = v_Color;
    }

变量,这些变量是在shader中定义的,这里的名称和c语言中定义的名称一致。通过这个个名称获取变量,传入参数

private String[] mAttributes = {
            "u_MVPMatrix",
            "a_Position",
            "a_Color"
    };
    public static int createAndLinkProgram(final int vertexShaderHandle, final int fragmentShaderHandle, final String[] attributes) {
        // 创建程序句柄 
        int programHandle = GLES20.glCreateProgram();

        if (programHandle != 0) {
            // Bind the vertex shader to the program.
            // 绑定shader到program
            GLES20.glAttachShader(programHandle, vertexShaderHandle);

            // Bind the fragment shader to the program.
            GLES20.glAttachShader(programHandle, fragmentShaderHandle);

            // Bind attributes
            // 绑定参数到program
            if (attributes != null) {
                final int size = attributes.length;
                for (int i = 0; i < size; i++) {
                    GLES20.glBindAttribLocation(programHandle, i, attributes[i]);
                }
            }

            // Link the two shaders together into a program.
            GLES20.glLinkProgram(programHandle);

            // Get the link status.
            final int[] linkStatus = new int[1];
            GLES20.glGetProgramiv(programHandle, GLES20.GL_LINK_STATUS, linkStatus, 0);

            // If the link failed, delete the program.
            if (linkStatus[0] == 0) {
                Log.e(TAG, "Error compiling program: " + GLES20.glGetProgramInfoLog(programHandle));
                GLES20.glDeleteProgram(programHandle);
                programHandle = 0;
            }
        }

        if (programHandle == 0) {
            throw new RuntimeException("Error creating program.");
        }

        return programHandle;
    }

最后一步,绘制图形

onSurfaceCreated函数主要是做初始化用的。

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //清屏指令
        GLES20.glClearColor(0f, 0f, 0f, 0f);
        //初始化相机的位置
        initViewMatrix();

        initShader();
    }

    private void initShader() {
        int vertexShaderHandle = ShaderHelper.compileShader(GLES20.GL_VERTEX_SHADER, TextResourceReader.readTextFileFromResource(mContext, R.raw.vertex_shader));
        int fragmentShaderHandle = ShaderHelper.compileShader(GLES20.GL_FRAGMENT_SHADER, TextResourceReader.readTextFileFromResource(mContext, R.raw.fragment_shader));

        int programHandle = ShaderHelper.createAndLinkProgram(vertexShaderHandle, fragmentShaderHandle, mAttributes);

        //者三个变量是我们在glsl文件中定义的三个变量,现在链接程序之后把他们取出来用,是为了后面赋值
        mMVPMatrixHandle = GLES20.glGetUniformLocation(programHandle, "u_MVPMatrix");
        mPositionHandle = GLES20.glGetAttribLocation(programHandle, "a_Position");
        mColorHandle = GLES20.glGetAttribLocation(programHandle, "a_Color");

        GLES20.glUseProgram(programHandle);
    }

onSurfaceChanged()函数

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        initProjectionMatrix(width, height);
    }

onDrawFrame()绘制

    @Override
    public void onDrawFrame(GL10 gl) {

        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
        initModelMatrix();
        drawFrame(mShaderDateBuffer);

    }
    private void drawFrame(final FloatBuffer frameBuffer) {
        //移动到 表示坐标的起始位置
        frameBuffer.position(mPositionOffset);
        //glVertexAttribPointer(positionSlot, 2, GL_FLOAT, GL_FALSE, stride, pCoords);
        /* 为顶点着色器位置信息赋值,
            1.positionSlot表示顶点着色器位置属性(即,Position);就是在glsl文件中声明的attribute变量
            2.表示每一个顶点信息由几个值组成,这个值必须位1,2,3或4;
            3.GL_FLOAT表示顶点信息的数据类型;
            4.GL_FALSE表示不要将数据类型标准化(即fixed-point);
            5.stride表示数组中每个元素的长度;pCoords表示数组的首地址
        */
        GLES20.glVertexAttribPointer(mPositionHandle, mPositionDataSize, GLES20.GL_FLOAT, false,
                mStrideBytes, frameBuffer);
        //开启顶点属性数组
        GLES20.glEnableVertexAttribArray(mPositionHandle);

        //定位到color数据首地址
        frameBuffer.position(mColorOffset);
        GLES20.glVertexAttribPointer(mColorHandle, mColorDataSize, GLES20.GL_FLOAT, false,
                mStrideBytes, frameBuffer);
        //开启顶点属性数组
        GLES20.glEnableVertexAttribArray(mColorHandle);

        Matrix.multiplyMM(mMVPMatrix, 0, mViewMatrix, 0, mModelMatrix, 0);

        // This multiplies the modelview matrix by the projection matrix, and stores the result in the MVP matrix
        // (which now contains model * view * projection).
        Matrix.multiplyMM(mMVPMatrix, 0, mProjectionMatrix, 0, mMVPMatrix, 0);

        GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mMVPMatrix, 0);
        GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 3);

    }
GLES20.glVertexAttribPointer(mPositionHandle, mPositionDataSize, GLES20.GL_FLOAT, false,
                mStrideBytes, frameBuffer);

mPositionHandle是GLES20.glGetAttribLocation(programHandle, "a_Position");绑定的变量,mPositionDataSize表示position坐标的数据个数,
也就是多少个数据表示一个坐标,mStrideBytes表示每次读取多少字节数据。
同样地,GLES20.glVertexAttribPointer(mColorHandle, mColorDataSize, GLES20.GL_FLOAT, false,mStrideBytes, frameBuffer);
是传入color数据。
Matrix.multiplyMM是计算MVP矩阵。

glDrawArrays参数详解

在OpenGl中所有的图形都是通过分解成三角形的方式进行绘制。
绘制图形通过GL10类中的glDrawArrays方法实现,

该方法原型:
glDrawArrays(int mode, int first,int count)

*参数1:有三种取值
1.GL_TRIANGLES:每三个顶之间绘制三角形,之间不连接
2.GL_TRIANGLE_FAN:以V0V1V2,V0V2V3,V0V3V4,……的形式绘制三角形
3.GL_TRIANGLE_STRIP:顺序在每三个顶点之间均绘制三角形。这个方法可以保证从相同的方向上所有三角形均被绘制。以V0V1V2,V1V2V3,V2V3V4……的形式绘制三角形
*参数2:从数组缓存中的哪一位开始绘制,一般都定义为0
*参数3:顶点的数量

MainActivity

MainActivity中增加opengl版本支持相关代码。

    //GLSurfaceView
    private MyGLSurfaceView mGLSurfaceView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        initGL();
        final ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
        final boolean supportsEs2 = configurationInfo.reqGlEsVersion >= 0x20000;
        if (supportsEs2)
        {
            // Request an OpenGL ES 2.0 compatible context.
            mGLSurfaceView.setEGLContextClientVersion(2);
            // Set the renderer to our demo renderer, defined below.
            mGLSurfaceView.setRenderer(new MyRender(this));
        }
        else
        {
            // This is where you could create an OpenGL ES 1.x compatible
            // renderer if you wanted to support both ES 1 and ES 2.
            return;
        }
        setContentView(mGLSurfaceView);
    }
    //初始化 opengl相关
    private void initGL() {
        mGLSurfaceView = new MyGLSurfaceView(this);
    }
    //在下面的两个方法中,必须有对  GLSurfaceView的处理,当Activity暂停时,在onPause中处理
    //同样在onResume中有相应的恢复处理
    //下面是最长见的处理方法
    @Override
    protected void onPause() {
        super.onPause();
        mGLSurfaceView.onPause();
    }
    @Override
    protected void onResume() {
        super.onResume();
        mGLSurfaceView.onResume();
    }

总结

画一个三角形和写一个Hello World一样难!!!

References

open GL空间坐标系的理解

作者:aiamjay1 发表于2016/9/4 16:38:37 原文链接
阅读:93 评论:0 查看评论

Android数据存储详解

$
0
0
目录:
    1.概述
    2.SQLite
        2.1 概述:
        2.2 数据类型+存储位置
        2.3 数据操作及其相关方法
        2.4 代码示例      
    3.SharePreferences
        3.1 概述
        3.2 操作模式+存储位置
        3.4 代码示例               
    4.File
        4.1 概述
        4.2 操作模式+存储位置
        4.3 手机内存+sdcard保存 
    5.Content provider   
    6.网络存储
    
1.概述
    数据存储在App开发和使用中是必不可少的,比如我们会从网络中保存图片、视频等到本地,同时我们的应用在用户使用过程中会
对用户的行为做一些记录,以此达到增加用户体验的效果,至于保存哪些用户数据需要由需求而定。
    当然我们数据存储的方式也不止一种,在android平台下包含:SQLite,SharePreferences,File,Content provider,网络存储等5种
存储方式,至于选用哪一种存储方式比较合理,就需要我们在实际开发中综合考虑,当然,我们在后面也会对每一种存储方式适合存储
什么样的数据做一些简单的概述。

2.SQLite
    2.1 概述:
        SQLite属于轻型嵌入式关系型数据库,占用资源比较低,应用场景主要在存储一些需要具备一些保密性,数据量比较大,同时
逻辑关系比较复杂的数据,比如:在天气应用中记录用户时区与位置,在闹钟应用中存储一些用户定义任务等等。

    2.2 数据类型+存储位置
        (1)数据类型:NULL,INTEGER(整型),TEXT(字符串文本),REAL(浮点型),BLOB(二进制型)
        (2)默认存储位置:/data/data/<packageName>/databases
        
    2.3 数据操作及其相关方法
        (1) 对于数据库的操作位于android.database.sqlite包下,主要的类有:SQLiteOpenHelper(数据库帮助类)和SQLiteDatabase(数据库
    类),SQLiteOpenHelper类主要用户数据库创建于版本管理。而SQLiteDatabase类当然主要用于数据库的常规操作(增删改查)和数据库的维护
        
        (2) SQLiteOpenHelper常用方法
        
    /*构造方法参数:
    *   1.Context:上下文
    *   2.name:需要创建的数据库名
    *   3.SQLiteDatabase.CursorFactory:提供创建Cursor对象,默认为null
    *   4.version:数据库版本
    *   5.errorHandler:数据库中断时的错误报告处理,null,为默认处理方式
    * */
    public MySqliteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) {}

    public MySqliteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {}

    //返回链接的数据库名
    public String getDatabaseName() {}

    //调用时间:在数据库需要onDowngrade的时候调用与onUpgrade类似
    //方法在事务中执行,如果出现异常,数据会回滚
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
 
    //数据库已经被打开的时候调用
    public void onOpen(SQLiteDatabase db) {}

    //在数据库链接正在配置是调用,用于开启也些相关配置
    public void onConfigure(SQLiteDatabase db) {}
    
    //关闭数据库链接
    public synchronized void close() {}
    
    //获取一个可读的数据库对象
    public SQLiteDatabase getReadableDatabase() {}
    
    //获取一个可读写的数据库对象
    public SQLiteDatabase getWritableDatabase() {}
    
    //数据库第一次创建时调用
    public void onCreate(SQLiteDatabase db) {}
    
    //调用时间:在数据库需要更新的时候调用
    //方法在事务中执行,如果出现异常,数据会回滚                
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}

   
        (3) SQLiteDatabase常用方法
        
    数据库操作型方法:
        //创建数据库,该数据库在关闭之后数据将消失
        public static SQLiteDatabase create(CursorFactory factory) {}
        
        //打开或者创建指定文件路径的数据库
        public static SQLiteDatabase openOrCreateDatabase(File file, CursorFactory factory) {}
        
        //打开指定文件路径的数据库
        public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags) {}
        
        //删除指定路径的数据库
        public static boolean deleteDatabase(File file) {}
      
    数据表与数据处理型方法:
    
        //执行sql语句,可实现增删改查,与表的创建,删除
        public void execSQL(String sql, Object[] bindArgs) throws SQLException {}
        public void execSQL(String sql) throws SQLException {}
        
        //删除指定数据
        public int delete(String table, String whereClause, String[] whereArgs) {}
        
        //查询数据
         public Cursor query(boolean distinct, String table, String[] columns,
            String selection, String[] selectionArgs, String groupBy,
            String having, String orderBy, String limit) {}
            
        //执行已定义的sql语句,返回结果Cursor
        public Cursor rawQuery(String sql, String[] selectionArgs) {}
        
        //插入数据
        public long insert(String table, String nullColumnHack, ContentValues values) {}
        
        //更新数据
         public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {}
  
    事务处理型方法:    
        //表示事务执行成功
        public void setTransactionSuccessful() {}
        //结束某个事务
        public void endTransaction() {}
        //开启事务
        private void beginTransaction(){}
          
    2.4 代码示例
(1) MySqliteOpenHelper.java
package com.example.database;

import android.content.Context;
import android.database.DatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

/**
 * Created by elimy on 2016-09-03.
 */
public class MySqliteOpenHelper extends SQLiteOpenHelper {
    private final String DEBUG ="DEBUG";

    //如果数据库名和版本固定,不需要改变可以如下设置,调用时只需要传入上下文即可
    private static final String DEFULT_DB_NAME ="first.db";
    private static final int DEFULT_VERSION = 1;
    public MySqliteOpenHelper(Context context) {
        super(context, DEFULT_DB_NAME, null, DEFULT_VERSION);
    }

    /*构造方法参数:
    *   1.Context:上下文
    *   2.name:需要创建的数据库名
    *   3.SQLiteDatabase.CursorFactory:提供创建Cursor对象,默认为null
    *   4.version:数据库版本
    *   5.errorHandler:数据库中断时的错误报告处理,null,为默认处理方式
    * */
    public MySqliteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) {
        super(context, name, factory, version, errorHandler);
    }

    public MySqliteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    /*
    * 作用:返回链接的数据库名
    * */
    @Override
    public String getDatabaseName() {
        return super.getDatabaseName();
    }

    /*
    *调用时间:在数据库需要onDowngrade的时候调用与onUpgrade类似
    * 方法在事务中执行,如果出现异常,数据会回滚
    * */
    @Override
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.d(DEBUG,"onDowngrade()");
        super.onDowngrade(db, oldVersion, newVersion);
    }
    /*
    *调用时间:数据库已经被打开的时候调用
    * */
    @Override
    public void onOpen(SQLiteDatabase db) {
        Log.d(DEBUG,"onOpen()");
        super.onOpen(db);
    }

    /*
    *在数据库链接正在配置是调用,用于开启也些相关配置
    * */
    @Override
    public void onConfigure(SQLiteDatabase db) {
        Log.d(DEBUG,"onConfigure()");
        super.onConfigure(db);
    }
    /*
    *关闭数据库链接
    * */
    @Override
    public synchronized void close() {
        Log.d(DEBUG,"close()");
        super.close();
    }
    /*
    * 获取一个可读的数据库对象
    * */
    @Override
    public SQLiteDatabase getReadableDatabase() {
        Log.d(DEBUG,"getReadableDatabase()");
        return super.getReadableDatabase();
    }
    /*
    * 获取一个可读写的数据库对象
    * */
    @Override
    public SQLiteDatabase getWritableDatabase() {
        Log.d(DEBUG,"getWritableDatabase()");
        return super.getWritableDatabase();
    }
    /*
    * 数据库第一次创建时调用
    * */
    @Override
    public void onCreate(SQLiteDatabase db) {
        //创建数据表users
        db.execSQL("create table users(" +
                "u_id INTEGER primary key autoincrement," +
                "u_name TEXT," +
                "u_password TEXT," +
                "u_sex TEXT," +
                "u_age INTEGER)");
        Log.d(DEBUG,"onCreate()");
    }
    /*
    *调用时间:在数据库需要更新的时候调用
    * 方法在事务中执行,如果出现异常,数据会回滚
    * */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.d(DEBUG,"onUpgrade()");
    }
}

(2) SqliteActivity.java
package com.example.database;

import android.content.ContentValues;
import android.content.DialogInterface;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class SqliteActivity extends AppCompatActivity implements View.OnClickListener {
    private Button add,update,delete,select;
    private MySqliteOpenHelper helper;
    private SQLiteDatabase db;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sqlite);
        //初始化数据
        add = (Button) findViewById(R.id.add);
        update = (Button) findViewById(R.id.update);
        select = (Button) findViewById(R.id.select);
        delete = (Button) findViewById(R.id.delete);
        //注册监听器
        add.setOnClickListener(this);
        update.setOnClickListener(this);
        select.setOnClickListener(this);
        delete.setOnClickListener(this);

        //通过MySqliteOpenHelper的带一个参数构造函数,初始化helper对象
        helper = new MySqliteOpenHelper(SqliteActivity.this);
    }

    public  void insertData(){
        //开启一个可写的数据库对象
        db = helper.getWritableDatabase();
        //插入数据集(键值对)初始化
        ContentValues values = new ContentValues();
        values.put("u_name","Elimy");
        values.put("u_password","456");
        values.put("u_sex","男");
        values.put("u_age",26);
        //插入数据
        //table:表名, nullColumnHack:如果可为空的字段未赋值则设置为空,
        // ContentValues:数据集
        long back = db.insert("users",null,values);
        if (back !=-1){
            Toast.makeText(SqliteActivity.this,"添加成功",Toast.LENGTH_SHORT).show();
            Log.d("add","添加成功");
        }else {
            Log.d("add","添加失败");
        }
        //释放数据库连接对象
        db.close();
    }
    /*
    * 更新数据
    * */
    public  void updateData(){
        //开启一个可写的数据库对象
        db = helper.getWritableDatabase();
        //插入数据集(键值对)初始化
        ContentValues values = new ContentValues();
        values.put("u_name","Andy");
        values.put("u_password","789");
        values.put("u_sex","女");
        values.put("u_age",26);
        //更新数据
        long back = db.update("users",values,"u_id=?",new String[]{"1"});
        if (back !=0){
            Toast.makeText(SqliteActivity.this,"修改成功",Toast.LENGTH_SHORT).show();
            Log.d("update","修改成功");
        }else {
            Log.d("update","修改失败");
        }
        //释放数据库连接对象
        db.close();
    }
    /*
    * 查询数据
    * */
    public  void selectData(){
        //开启一个可写的数据库对象
        db = helper.getReadableDatabase();
        //查询
        Cursor cursor = db.query("users",new String[]{"u_name","u_password","u_sex","u_age"},null,null,null,null,null);
        while (cursor.moveToNext()){
            Log.d("users",cursor.getString(cursor.getColumnIndex("u_name"))+"+"+cursor.getString(cursor.getColumnIndex("u_password"))+"+"+cursor.getString(cursor.getColumnIndex("u_sex"))+"+"+cursor.getString(cursor.getColumnIndex("u_age")));
        }
        //释放数据库连接对象
        db.close();
    }
    /*
    * 删除数据
    * */
    public  void deleteData(){
        //开启一个可写的数据库对象
        db = helper.getWritableDatabase();
        //删除数据
        int back = db.delete("users","u_id=?",new String[]{"1"});
        if (back == 0){
            Log.d("delete","删除失败");
        }else {
            Log.d("delete","删除成功");
        }
        //释放数据库连接对象
        db.close();
    }
    /*
    * 点击事件监听方法
    * */
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.add :
                insertData();
                break;
            case R.id.update:
                updateData();
                break;
            case R.id.select:
                selectData();
                break;
            case R.id.delete:
                deleteData();
                break;
            default:
                break;
        }
    }
}

(3) activity_sqlite.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.database.SqliteActivity">

    <Button
        android:id="@+id/add"
        android:text="插入数据"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <Button
        android:id="@+id/update"
        android:layout_below="@+id/add"
        android:text="修改数据"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <Button
        android:id="@+id/select"
        android:text="查询数据"
        android:layout_below="@+id/update"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <Button
        android:id="@+id/delete"
        android:text="删除数据"
        android:layout_below="@+id/select"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
</RelativeLayout>

(4) 操作和相应的Log打印(ps:之前添加过一条)
添加数据:
    09-03 08:39:37.976 9594-9594/? D/DEBUG: getWritableDatabase()
    09-03 08:39:37.980 9594-9594/? D/DEBUG: onConfigure()
    09-03 08:39:37.980 9594-9594/? D/DEBUG: onOpen()
    09-03 08:39:37.992 9594-9594/? D/add: 添加成功

修改数据:
    09-03 08:40:45.972 9594-9594/? D/DEBUG: getWritableDatabase()
    09-03 08:40:45.972 9594-9594/? D/DEBUG: onConfigure()
    09-03 08:40:45.972 9594-9594/? D/DEBUG: onOpen()
    09-03 08:40:45.980 9594-9594/? D/update: 修改成功

查询数据:
    09-03 08:42:22.672 9594-9594/? D/DEBUG: getReadableDatabase()
    09-03 08:42:22.676 9594-9594/? D/DEBUG: onConfigure()
    09-03 08:42:22.676 9594-9594/? D/DEBUG: onOpen()
    09-03 08:42:22.676 9594-9594/? D/users: Andy+789+女+26
    09-03 08:42:22.676 9594-9594/? D/users: Elimy+456+男+26

删除数据+查询数据:
    09-03 08:43:17.040 9594-9594/? D/DEBUG: getWritableDatabase()
    09-03 08:43:17.040 9594-9594/? D/DEBUG: onConfigure()
    09-03 08:43:17.044 9594-9594/? D/DEBUG: onOpen()
    09-03 08:43:17.052 9594-9594/? D/delete: 删除成功
    09-03 08:43:23.084 416-565/system_process W/ThrottleService: unable to find stats for iface rmnet0
    09-03 08:43:23.496 9594-9594/? D/DEBUG: getReadableDatabase()
    09-03 08:43:23.496 9594-9594/? D/DEBUG: onConfigure()
    09-03 08:43:23.496 9594-9594/? D/DEBUG: onOpen()
    09-03 08:43:23.496 9594-9594/? D/users: Elimy+456+男+26

3.SharePreferences
    3.1 概述
        SharePreferences是一种轻量级的存储数据方式,以xml键值对的形式存储数据,应用场景主要是存储应用的配置信息
        
    3.2 操作模式+存储位置
        (1)操作模式:
            MODE_PRIVATE:文件仅应用程序自己能访问。

            MODE_WORLD_READABLE:文件除了自己访问外还可以被其它应该程序读取(推荐使用Content provider)

            MODE_WORLD_WRITEABLE:文件除了自己访问外还可以被其它应该程序读取和写入(推荐使用Content provider)
            
            MODE_APPEND:在文件尾部追加
            
        (2)默认存储位置:/data/data/<packageName>/shared_prefs
    
    3.4 代码示例(实现登陆功能,将用户名、密码以及是否保存密码数据保存到SharePreferences文件中,当然实际开发为了账户安全
需慎重使用)

(1) MainActivity.java
package com.example.sharepreferences;

import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private EditText edit_userNmae,edit_password;
    private Button cancel,login,read;
    private CheckBox save;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化控件
        edit_userNmae = (EditText) findViewById(R.id.editText_name);
        edit_password = (EditText) findViewById(R.id.editText_pas);
        cancel = (Button) findViewById(R.id.cancel);
        login = (Button) findViewById(R.id.login);
        save = (CheckBox) findViewById(R.id.check);
        read = (Button) findViewById(R.id.read);
        //注册监听
        cancel.setOnClickListener(this);
        login.setOnClickListener(this);
        read.setOnClickListener(this);

    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.cancel:
                Toast.makeText(MainActivity.this,"取消登录",Toast.LENGTH_SHORT).show();
                break;
            case R.id.login:
                //获取用户输入数据
                String user_name = edit_userNmae.getText().toString().trim();
                String password = edit_password.getText().toString().trim();
                Boolean isSavePas = save.isChecked();
                //创建SharedPreferences对象,默认以应用程序包名为文件名
                // SharedPreferences preferences = getPreferences(MODE_PRIVATE);
                SharedPreferences preferences = getSharedPreferences("info",this.MODE_PRIVATE);
                //PreferenceManager实例化,默认以应用程序包名为文件名
                //SharedPreferences preferences2 = PreferenceManager.getDefaultSharedPreferences(MainActivity.this);
                //获取编辑类实例
                SharedPreferences.Editor editor = preferences.edit();
                //put数据
                editor.putString("username",user_name);
                editor.putString("password",password);
                editor.putBoolean("isSavePas",isSavePas);
                //提交事务
                editor.commit();
                break;
            case R.id.read:
                //声明并初始化SharedPreferences
                SharedPreferences backPreferences = getSharedPreferences("info",this.MODE_PRIVATE);
                //获取preference中的数据
                String backName = backPreferences.getString("username","");
                String backPassword = backPreferences.getString("password","");
                Boolean backChecked = backPreferences.getBoolean("isSavePas",false);
                //打印显示
                Log.d("back",backName+"+"+backPassword+"+"+backChecked);
                break;
            default:
                break;
        }
    }
}

(2) activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.sharepreferences.MainActivity">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="登陆"
        android:layout_centerHorizontal="true"
        android:id="@+id/title"
        android:layout_alignParentTop="true" />
<RelativeLayout
    android:id="@+id/edit_layout"
    android:layout_marginTop="10dp"
    android:layout_below="@+id/title"
    android:layout_centerHorizontal="true"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:layout_alignBaseline="@+id/editText_name"
        android:text="用户名:"
        android:id="@+id/user_name"

        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="密码:"
        android:layout_alignBaseline="@+id/editText_pas"
        android:id="@+id/password"
        android:layout_below="@+id/user_name"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:inputType="textPersonName"
        android:hint="请输入用户名"
        android:ems="10"
        android:id="@+id/editText_name"
        android:layout_alignParentTop="true"
        android:layout_toRightOf="@+id/user_name"
        android:layout_toEndOf="@+id/user_name" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:inputType="textPassword"
        android:ems="10"
        android:layout_alignLeft="@+id/editText_name"
        android:layout_toRightOf="@+id/password"
        android:id="@+id/editText_pas"
        android:layout_below="@+id/editText_name"
        android:layout_centerHorizontal="true" />
    <CheckBox
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="保存密码"
        android:id="@+id/check"
        android:layout_below="@+id/password"
        android:checked="false" />
</RelativeLayout>
    <RelativeLayout
        android:layout_centerHorizontal="true"
        android:layout_marginTop="10dp"
        android:layout_below="@+id/edit_layout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/relativeLayout">

    </RelativeLayout>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="登陆"
        android:id="@+id/login"
        android:layout_below="@+id/relativeLayout"
        android:layout_toRightOf="@+id/title"
        android:layout_alignRight="@+id/edit_layout"
        android:layout_alignEnd="@+id/edit_layout" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="取消"
        android:id="@+id/cancel"
        android:layout_alignTop="@+id/relativeLayout"
        android:layout_alignLeft="@+id/edit_layout"
        android:layout_alignStart="@+id/edit_layout"
        android:layout_toStartOf="@+id/title"
        android:layout_toLeftOf="@+id/title" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="读取数据"
        android:id="@+id/read"
        android:layout_below="@+id/cancel"
        android:layout_marginTop="50dp"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true" />


</RelativeLayout>

(3) 效果截图


(4) Log打印(点击登陆,再点击读取数据)
    09-03 13:21:30.503 3745-3745/com.example.sharepreferences D/back: elimy+456+true
  
(5) DDMS查看手机文件截图
    
    
4.File
    4.1 概述
        File文件存储,在android应用做,我们除了一些简单的数据和关系型数据需要通过Sharepreference和sqlite存储以外,通常
还有大数据型文件存储的需求,比如文本文件、图片文件、视频文件等,这时候File文件存储就比较符合需求,当然默认存储位置在手机
内存中,由于内存有限,大多数情况会有存储到sdcard的需求。
    
    4.2 操作模式+存储位置
            MODE_PRIVATE:文件仅应用程序自己能访问(默认)。

            MODE_WORLD_READABLE:文件除了自己访问外还可以被其它应该程序读取(推荐使用Content provider)

            MODE_WORLD_WRITEABLE:文件除了自己访问外还可以被其它应该程序读取和写入(推荐使用Content provider)
            
            MODE_APPEND:在文件尾部追加
            
        (2)默认存储位置:/data/data/<packageName>/files
        
    4.3 手机内存+sdcard保存文件
    
(1)MainActivity.java
package com.example.file;

import android.content.Context;
import android.os.Bundle;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Toast;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private EditText editTitle,editContent;
    private Button savePhone,saveSD;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //初始化控件
        editTitle = (EditText) findViewById(R.id.edit_title);
        editContent = (EditText) findViewById(R.id.edit_content);
        savePhone = (Button) findViewById(R.id.phone_save);
        saveSD = (Button) findViewById(R.id.sdcard_save);
        //注册监听
        savePhone.setOnClickListener(this);
        saveSD.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        //获取用户输入数据
        String title = editTitle.getText().toString().trim();
        String content = editContent.getText().toString().trim();
        switch (v.getId()){
            case R.id.phone_save:
                if (title != null && content!= null){
                    try {
                        //通过文件输出流写入文件到手机内存
                        //声明并初始化FileOutputStream
                        //参数:
                        //    name:文件名
                        //    mode:操作模式
                        FileOutputStream fos =this.openFileOutput("test.txt", Context.MODE_APPEND);
                        //写入title
                        fos.write(title.getBytes());
                        //写入换行符
                        fos.write("\n".getBytes());
                        //写入coontent
                        fos.write(content.getBytes());
                        //关闭文件流
                        fos.close();
                        Toast.makeText(MainActivity.this,"保存到手机默认地址成功!",Toast.LENGTH_SHORT).show();
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }else {
                    Toast.makeText(MainActivity.this,"请填写内容再保存!",Toast.LENGTH_SHORT).show();
                }
                break;
            case R.id.sdcard_save:
                if (title !=null && content!= null){
                       //判断sdcard是否挂载
                        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
                          //获取sdcard根目录
                            File sdcard_dir = Environment.getExternalStorageDirectory();
                            Log.d("sdcard_dir",sdcard_dir.toString());
                            //实例化saveFile
                            File saveFile = new File(sdcard_dir,"test.txt");

                            try {
                                //实例化指向目的文件位置的FileOutputStream
                                FileOutputStream fos=new FileOutputStream(saveFile,true);
                                //写入内容
                                fos.write(title.getBytes());
                                fos.write("\n".getBytes());
                                fos.write(content.getBytes());
                                //关闭文件流
                                fos.close();
                                Toast.makeText(MainActivity.this,"保存sdcard成功!",Toast.LENGTH_SHORT).show();
                            } catch (FileNotFoundException e) {
                                e.printStackTrace();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                }else {
                    Toast.makeText(MainActivity.this,"请填写内容再保存!",Toast.LENGTH_SHORT).show();
                }
                break;
            default:
                break;
        }
    }
}

(2)布局activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.file.MainActivity">



    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="标题:"
        android:id="@+id/title"
        android:layout_alignBaseline="@+id/edit_title"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />
    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/edit_title"
        android:hint="请输入标题"
        android:layout_toRightOf="@+id/title"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true" />

<RelativeLayout
    android:layout_marginTop="20dp"
    android:layout_below="@+id/title"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:id="@+id/Content_Layout">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="内容:"
        android:layout_alignBaseline="@+id/edit_content"
        android:id="@+id/content"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />
    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:inputType="textMultiLine"
        android:ems="10"
        android:hint="请输入内容"
        android:id="@+id/edit_content"
        android:layout_toRightOf="@+id/content"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true" />
</RelativeLayout>
<LinearLayout
    android:layout_marginTop="80dp"
    android:layout_below="@+id/Content_Layout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <Button
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        android:text="保存到手机"
        android:id="@+id/phone_save" />

    <Button
        android:layout_width="0dp"
        android:layout_weight="1"
        android:layout_height="wrap_content"
        android:text="保存到sdcard"
        android:id="@+id/sdcard_save" />

</LinearLayout>


</RelativeLayout>

(3)访问sdcard需要注册权限
    <!--写文件到sdcard权限-->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <!--在sdcard创建和删除文件的权限-->
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>

(4))手机内存和sdcard保存文件效果图+验证截图

                                        (1)布局效果图                                                                                                         (2)sdcard验证保存成功截图


                                          (3) 手机内存查看验证截图


5.Content provider
    Content provider也被封为四大组件之一,是一种应用之间提供数据共享的机制,其实他的存储方式也是通过数据库或者其他存储
方式实现的,他只是提供一个共外界访问的接口,方便外界访问而已。之前我写一篇关于Content provider的文章,这里又不做总结了。
http://blog.csdn.net/qq_28057577/article/details/51454326

6.网络存储
    所谓的网络存储也就是通过网络请求获取数据,后面在网络部分进行学习,在此暂时就不学习了。

推荐文章:http://www.cnblogs.com/hanyonglu/archive/2012/03/01/2374894.html
作者:qq_28057577 发表于2016/9/4 17:36:50 原文链接
阅读:103 评论:0 查看评论

java/android 设计模式学习笔记(19)---状态模式

$
0
0

  这篇博客我们来介绍一下状态模式(State Pattern),也是行为型设计模式之一。状态模式的行为是由状态来决定的,不同的状态下有不同的行为。状态模式和策略模式的结构类图几乎完全一样,但它们的目的、本质却完全不一样。状态模式的行为是平行的、不可替换的,策略模式的行为是彼此独立、可相互替换的。状态模式把对象的行为包装在不同的状态对象里,每一个状态对象都有一个共同的抽象状态基类;而策略模式可以想象成是除了继承之外的一种弹性替代方案,如果你使用继承定义了一个类的行为,你将被这个行为困住,甚至要修改它都很难,有了策略模式,你可以通过组合不同的对象来改变行为。状态模式的意图是让一个对象在其内部状态发生改变的时候,其行为也随之改变。
  转载请注明出处:http://blog.csdn.net/self_study/article/details/52432486
  PS:对技术感兴趣的同鞋加群544645972一起交流。

设计模式总目录

  java/android 设计模式学习笔记目录

特点

  当一个对象的内在状态改变时允许改变其行为,这个对象看起来像是改变了其类。
  状态模式的使用场景:

  • 一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为;
  • 代码中包含大量与状态有关的条件语句,例如,一个操作中含有庞大的多分枝语句(if-else 或者 switch-case),且这些分支依赖于该对象的状态。
状态模式将每一个条件分支放入一个独立的类中,这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象可以不依赖于其他对象而独立变化,这样通过多态来去除过多的、重复的 if-else 等分支语句。

UML类图

  这里写图片描述
  状态模式的 uml 类图有三个角色:

  • Context:环境类,定义客户感兴趣的接口,维护一个 State 子类,这个实例定义了对象的当前状态;
  • State:抽象状态类或者状态接口,定义一个或者一组接口,表示该状态下的行为;
  • ConcreteStateA、ConcreteStateB:具体状态类,每一个具体的状态类实现抽象的 State 中定义的接口,从而达到不同状态下的不同行为。
  据此我们可以写出状态模式的通用代码:
状态接口以及相关子类:
State.class

public interface State {
    void doSomething();
}

ConcreteStateA.class、ConcreteStateB.class、NullState.class

public class ConcreteStateA implements State {
    @Override
    public void doSomething() {
        System.out.print("this is ConcreteStateA's function\n");
    }
}
public class ConcreteStateB implements State{
    @Override
    public void doSomething() {
        System.out.print("this is ConcreteStateB's function\n");
    }
}
public class NullState implements State{
    @Override
    public void doSomething() {
        //do nothing
    }
}

Context类以及测试代码:
Context.class

public class Context {
    private State state = new NullState();

    void setState(State state) {
        this.state = state;
    }

    void doSomething() {
        state.doSomething();
    }

    public static void main(String[] args) {
        Context context = new Context();
        context.setState(new ConcreteStateA());
        context.doSomething();
        context.setState(new ConcreteStateB());
        context.doSomething();
    }
}

最后结果:

this is ConcreteStateA's function
this is ConcreteStateB's function

示例与源码

  在 Android 源码中也有很多状态模式的例子,MediaPlayer 和 WifiStateEngine 等,这里我们来简单看看 MediaPlayer 的状态机:
  这里写图片描述
椭圆代表 MediaPlayer 对象可能驻留的状态。弧线表示驱动 MediaPlayer 在各个状态之间迁移的播放控制操作。这里有两种类型的弧线。由一个箭头开始的弧代表同步的方法调用,而以双箭头开头的代表的弧线代表异步方法调用。MediaPlayer在这我就不详细介绍了,网上资料很多,感兴趣的可以去查阅一下。
  这里再介绍一下状态机,又称为有限状态自动机 (FSM:Finite State Machine),是表示有限多个状态以及在这些状态之间转移和动作的数学模型。状态存储关于过去的信息,它反映从系统开始到现在时刻输入的变化;转移指示状态变更,用必须满足来确使转移发生的条件来描述它;动作是在给定时刻要进行的活动描述,详细的看看这篇博客:有限状态机(FSM)的Java 演示
  实际 Android 开发过程中,我们一般会去根据实际情况去先构造一个状态图,定义每个状态和每个状态之间切换的事件,类似于上图的 MediaPlayer,然后将该信息录入进入状态机,当目前的状态接收到一个非法的跳转事件时,可以抛出异常,这样就能保证一切按照预先设定好的方向进行。

Demo

  我们这里仍然以 wiki 上的 demo 为例,来实现一堆字符串的一个大小写间隔打印:
Statelike.class

interface Statelike {
    void writeName(StateContext context, String name);
}

StateLowerCase.class 和 StateMultipleUpperCase.class

class StateLowerCase implements Statelike {
    @Override
    public void writeName(final StateContext context, final String name) {
        System.out.println(name.toLowerCase());
        context.setState(new StateMultipleUpperCase());
    }
}
class StateMultipleUpperCase implements Statelike {
    /** Counter local to this state */
    private int count = 0;

    @Override
    public void writeName(final StateContext context, final String name) {
        System.out.println(name.toUpperCase());
        /* Change state after StateMultipleUpperCase's writeName() gets invoked twice */
        if(++count > 1) {
            context.setState(new StateLowerCase());
        }
    }

}

StateContext.class

class StateContext {
    private Statelike myState;
    StateContext() {
        setState(new StateLowerCase());
    }

    /**
     * Setter method for the state.
     * Normally only called by classes implementing the State interface.
     * @param newState the new state of this context
     */
    void setState(final Statelike newState) {
        myState = newState;
    }

    public void writeName(final String name) {
        myState.writeName(this, name);
    }

    public static void main(String[] args) {
        final StateContext sc = new StateContext();

        sc.writeName("Monday");
        sc.writeName("Tuesday");
        sc.writeName("Wednesday");
        sc.writeName("Thursday");
        sc.writeName("Friday");
        sc.writeName("Saturday");
        sc.writeName("Sunday");
    }
}

最后结果:

monday
TUESDAY
WEDNESDAY
thursday
FRIDAY
SATURDAY
sunday

例子也很简单,一目了然。

总结

  状态模式的关键点在于不同的状态下对于统一行为有不同的响应,这其实就是一个将 if-else 用多态来实现的一个具体实例。在 if-else 或者 switch-case 形势下根据不同的状态进行判断,如果是状态 A 那么执行方法 A,状态 B 执行方法 B,但这种实现使得逻辑耦合在一起,易于出错不易维护,通过状态模式能够很好的消除这类“丑陋”的逻辑处理,当然并不是任何出现 if-else 的地方都应该通过状态模式重构,模式的运用一定要考虑所处的情景以及你要解决的问题,只有符合特定的场景才建议使用对应的模式。和程序状态机(PSM)不同,状态模式用类代表状态,状态的转换可以由 State 类或者 Context 类控制。
  优点:

  • 通过将每个状态封装进一个类,将以后所做的修改局部化;
  • 将所有与一个特定状态相关的行为封装到一个对象中,繁琐的状态判断转换成结构清晰的状态类族,在避免代码膨胀的同时增加可维护性和可扩展性。
缺点当然也很明显,也是绝大部分设计模式的通病,类数目的增多。

源码下载

  https://github.com/zhaozepeng/Design-Patterns/tree/master/StatePattern

引用

https://en.wikipedia.org/wiki/State_pattern
http://blog.csdn.net/jason0539/article/details/45021055
http://blog.csdn.net/shulianghan/article/details/38487967
http://blog.csdn.net/eager7/article/details/8517827

作者:zhao_zepeng 发表于2016/9/4 19:14:59 原文链接
阅读:123 评论:0 查看评论

RxJava响应式编程之初级了解

$
0
0

据说现在流行的开发模式是 Retrofit+RxJava+MVP+ButterKnife

如果想要简单学习ButterKnife、MVP模式,可以参考我以前的例子
使用butterknife注解框架
Android—MVP设计模式高级(三)

今天我就简单来学习下RxJava的相关知识
以前我也只是听说过RxJava,RxJava这个到底是什么东西呢?
呵呵,它其实是一个库,所以我们使用里面的方法,得需要下载库,所以我们需要在AS中进行配置

1.RxJava 地址以及添加

github地址:
https://github.com/ReactiveX/RxJava
或者
https://github.com/ReactiveX/RxAndroid

依赖库添加:
compile ‘io.reactivex:rxjava:1.1.6’
或者
compile ‘io.reactivex:rxandroid:1.2.1’

2.RxJava是什么类型的库?它的原理是什么?

RxJava 在 GitHub 主页上的自我介绍是 “a library for composing asynchronous and event-based programs using observable sequences for the Java VM”(一个在 Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库)。这就是 RxJava ,概括得非常精准。

Rx:函数响应式编程,也许这个词对你我都很只可意会,不可言传,先抛开Rx不说,我们接触到的类似的这样的思路,大概有接口回调、Handler通讯、广播通讯、还有一个开源的EventBus、以及ContentPorivider里面的观察者模式、AsyncTask 我们朦胧中也许就大概了解RxJava是怎么个东西了。

RxJava 的异步实现,是通过一种扩展的观察者模式来实现的。
至于观察者我就拿ContentProvider来说吧,比如我们在一个ContentProvider中有一个insert方法,插入完毕后,去通知某个监听该URI变化的界面

getContext().getContentResolver().notifyChange(URI, null);

比如在MainActivity中去registerContentObserver注册一个内容观察者

private static final Uri URI = Uri  
            .parse("content://com.example.contentprovider.MyContentProvider/student");  
 contentResolver.registerContentObserver(uri, true, observer);
 private ContentObserver observer = new ContentObserver(null) {  
        public void onChange(boolean selfChange) {  
            // 说明数据有改变,重新查询一直所有记录  
            Uri uri = Uri.parse("content://com.example.contentprovider.MyContentProvider/student");  
            Cursor cursor = contentResolver.query(uri, null, null, null, null);  
            Log.e("TAG", "onChange()  count=" + cursor.getCount());  
        };  
    };  

那么对于RxJava 的观察者模式呢?
RxJava 有四个基本概念:Observable (被观察者)、 Observer (观察者)、 subscribe (订阅)、事件。
对比ContentProvider,RxJava里面的被观察者就是某个Uri(也就是某个数据库),观察者就是某个界面(比如MainActivity),订阅就是registerContentObserver,事件就是insert方法

Rxjava本质主要就是异步任务 外层构建了一个观察者的设计模式 它更简洁 我们调用api方法 几乎看不到 方法里面的 分线程数据操作 它只是利用了 一种观察者的设计模式 来进行 主分线程的通讯 来进行响应操作

我们来看看RxJava的察者模式流程交互图:
这里写图片描述

与传统观察者模式不同, RxJava 的事件回调方法除了普通事件 onNext() (相当于 insert)之外,还定义了两个特殊的事件:onCompleted() 和 onError()。

我们来看下Observable是如何定义的?

     Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                Log.d(TAG, "call: threadId:" + Thread.currentThread().getId());
                subscriber.onStart();
                subscriber.onNext("Hello World!");
                subscriber.onCompleted();
            }
        })

我们看看create源码里面做了什么?

   public static <T> Observable<T> create(OnSubscribe<T> f) {
        return new Observable<T>(hook.onCreate(f));
    }

可以看到,这里传入了一个 OnSubscribe 对象作为参数。OnSubscribe 会被存储在返回的 Observable 对象中,当 Observable 被订阅的时候,OnSubscribe 的 call() 方法会自动被调用,事件序列就会依照设定依次触发(对于上面的代码,就是观察者Subscriber 将会被调用一次 onNext() 和一次 onCompleted())。这样,由被观察者调用了观察者的回调方法,就实现了由被观察者向观察者的事件传递,即观察者模式。

然后在看Observer中做了什么操作?

subscribe(new Observer<String>() {
                    @Override
                    public void onCompleted() {
                        Log.d(TAG, "onCompleted: threadId:" + Thread.currentThread().getId());
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e(TAG, "onError: threadId:" + Thread.currentThread().getId());
                    }

                    @Override
                    public void onNext(String s) {
                        Log.d(TAG, "onNext: threadId:" + Thread.currentThread().getId());
                        Log.i(TAG, "onNext: s = " + s);
                    }
                });

onCompleted(): 事件队列完结。RxJava 不仅把每个事件单独处理,还会把它们看做一个队列。RxJava 规定,当不会再有新的 onNext() 发出时,需要触发 onCompleted() 方法作为标志。
onError(): 事件队列异常。在事件处理过程中出异常时,onError() 会被触发,同时队列自动终止,不允许再有事件发出。
在一个正确运行的事件序列中, onCompleted() 和 onError() 有且只有一个,并且是事件序列中的最后一个。需要注意的是,onCompleted() 和 onError() 二者也是互斥的,即在队列中调用了其中一个,就不应该再调用另一个。

接下来我们将全部的代码贴出来,看看Log日志打印:

同步方式

package com.example.administrator.myapplication;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;

import rx.Observable;
import rx.Observer;
import rx.Subscriber;

public class MainActivity extends Activity {
    private String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                Log.d(TAG, "call: threadId:" + Thread.currentThread().getId());
                subscriber.onStart();
                subscriber.onNext("Hello World!");
                subscriber.onCompleted();
            }
        })
                .subscribe(new Observer<String>() {
                    @Override
                    public void onCompleted() {
                        Log.d(TAG, "onCompleted: threadId:" + Thread.currentThread().getId());
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e(TAG, "onError: threadId:" + Thread.currentThread().getId());
                    }

                    @Override
                    public void onNext(String s) {
                        Log.d(TAG, "onNext: threadId:" + Thread.currentThread().getId());
                        Log.i(TAG, "onNext: s = " + s);
                    }
                });
    }
}
call: threadId:1
onNext: threadId:1
onNext: s = Hello World!
onCompleted: threadId:1

从上可以看出,事件的处理和结果的接收都是在同一个线程里面处理的。但是,Rxjava的意义何在,异步呢?别急,看以下代码的处理,你就会发现了,异步原来是这么的简单。
异步方式
我们将上面的代码稍微改下,增加2行代码

        Observable.create(new Observable.OnSubscribe<String>() {
            @Override
            public void call(Subscriber<? super String> subscriber) {
                Log.d(TAG, "call: threadId:" + Thread.currentThread().getId());
                subscriber.onStart();
                subscriber.onNext("Hello World!");
                subscriber.onCompleted();
            }
        })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<String>() {
                    @Override
                    public void onCompleted() {
                        Log.d(TAG, "onCompleted: threadId:" + Thread.currentThread().getId());
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e(TAG, "onError: threadId:" + Thread.currentThread().getId());
                    }

                    @Override
                    public void onNext(String s) {
                        Log.d(TAG, "onNext: threadId:" + Thread.currentThread().getId());
                        Log.i(TAG, "onNext: s = " + s);
                    }
                });

我们添上如下2行代码

.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())

我们看下Log输出

03-08 07:20:18.101 22734-22734/? I/MainActivity: testFunction: threadId:1
03-08 07:20:18.123 22734-22755/? D/MainActivity: call: threadId:180
03-08 07:20:18.142 22734-22734/? D/MainActivity: onNext: threadId:1
03-08 07:20:18.142 22734-22734/? I/MainActivity: onNext: s = Hello World!
03-08 07:20:18.143 22734-22734/? D/MainActivity: onCompleted: threadId:1

看见了没,第二行log日志threadId与其它的threadId很明显的不一样啊,说明我们在处理事件的时候,发生在了一个新的线程里面,而结果的接收,还是在主线程里面操作的。怎么样,只要添加两句话,异步立马就实现了,异步处理耗时操作,就是这么easy。

我们简单看下源码

 public final Observable<T> subscribeOn(Scheduler scheduler) {
        if (this instanceof ScalarSynchronousObservable) {
            return ((ScalarSynchronousObservable<T>)this).scalarScheduleOn(scheduler);
        }
        return create(new OperatorSubscribeOn<T>(this, scheduler));
    }

什么意思呢?
Scheduler scheduler参数就是执行订阅操作,返回一个源观察到的修改,使其订阅发生在指定的线程
observeOn(AndroidSchedulers.mainThread())
句话说,observeOn() 指定的是它之后的操作所在的线程。

打印字符串数组 from和just方式

以上是RxJava的很基础很简单的一个用法,那么我们接着往下看,比如我们有一组需求把一个String数组的字符串,单个打印出来,我们用Rxjava怎么实现呢?看代码:

Log.i(TAG, "testFunction: threadId:" + Thread.currentThread().getId());
        Observable.from(new String[]{"one","two","three","four"})
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<String>() {
                    @Override
                    public void onCompleted() {
                        Log.d(TAG, "onCompleted: threadId:" + Thread.currentThread().getId());
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e(TAG, "onError: threadId:" + Thread.currentThread().getId());
                    }

                    @Override
                    public void onNext(String s) {
                        Log.i(TAG, "onNext: s = " + s);
                    }
                });

看Log输出日志如下:

testFunction: threadId:1
onNext: s = one
onNext: s = two
onNext: s = three
onNext: s = four
onCompleted: threadId:1

From操作符用来将某个对象转化为Observable对象,并且依次将其内容发射出去。这个类似于just,但是just会将这个对象整个发射出去。比如说一个含有3个字符串的数组,使用from就会发射4次,每次发射一个数字,而使用just会发射一次来将整个的数组发射出去。

 Log.i(TAG, "testFunction: threadId:"+Thread.currentThread().getId());
        Observable.just("one", "two", "three", "four")
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<String>() {
                    @Override
                    public void onCompleted() {
                        Log.d(TAG, "onCompleted: threadId:" + Thread.currentThread().getId());
                    }

                    @Override
                    public void onError(Throwable e) {
                        Log.e(TAG, "onError: threadId:" + Thread.currentThread().getId());
                    }

                    @Override
                    public void onNext(String s) {
                        Log.i(TAG, "onNext: s = " + s);
                    }
                });
03-08 08:09:25.743 32155-32155/? I/MainActivity: testFunction: threadId:1
03-08 08:09:25.784 32155-32155/? I/MainActivity: onNext: s = one
03-08 08:09:25.785 32155-32155/? I/MainActivity: onNext: s = two
03-08 08:09:25.785 32155-32155/? I/MainActivity: onNext: s = three
03-08 08:09:25.785 32155-32155/? I/MainActivity: onNext: s = four
03-08 08:09:25.785 32155-32155/? D/MainActivity: onCompleted: threadId:1

一对一转换

 Log.i(TAG, "testFunction: threadId:"+Thread.currentThread().getId());
        Observable.just("1", "2", "3", "4")
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .map(new Func1<String, Integer>() {

                    @Override
                    public Integer call(String s) {
                        Log.i(TAG, "call: s = "+s);
                        return Integer.parseInt(s);
                    }
                })
                .subscribe(new Action1<Integer>() {
                    @Override
                    public void call(Integer integer) {
                        Log.i(TAG, "call: integer = "+integer);
                    }
                });
testFunction: threadId:1
call: s = 1
call: integer = 1
call: s = 2
call: integer = 2
call: s = 3
call: integer = 3
call: s = 4
call: integer = 4

简单说一下Func1,其中的T表示传入的参数类型,R表示方法返回的参数类型。源码如下:

public interface Func1<T, R> extends Function {
    R call(T t);
}

上例中还有一个叫做 Action1的类。也是 RxJava 的一个接口,用于包装含有无参数的方法。 Func1 和 Action 的区别在于, Func1 包装的是有返回值的方法。另外,和 ActionX 一样, FuncX 也有多个,用于不同参数个数的方法。FuncX 和 ActionX 的区别在 FuncX 包装的是有返回值的方法。

可以看到,map() 方法将参数中的 String 对象转换成一个 Integer对象后返回,而在经过 map() 方法后,事件的参数类型也由 String 转为了 Integer。这种直接变换对象并返回的,是最常见的也最容易理解的变换。不过 RxJava 的变换远不止这样,它不仅可以针对事件对象,还可以针对整个事件队列,这使得 RxJava 变得非常灵活。

封装Observable一对多转换
map转换,是一对一的转换,像示例当中,我们把string转成int,但是当我们需要一对多的转换,该怎么做呢?比如说,定义一个学生类:

package com.example.administrator.myapplication;

import java.util.List;

public class Student {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    //////////////////////////
    private List<String> courses;


    public List<String> getCourses() {
        return courses;
    }

    public void setCourses(List<String> courses) {
        this.courses = courses;
    }
}

        Student student1 = new Student();
        student1.setName("safly");
        List<String> courses = new ArrayList<>();
        courses.add("语文");
        courses.add("数学");
        courses.add("英语");
        student1.setCourses(courses);

        Student student2 = new Student();
        student2.setName("wyf");
        List<String> courses2 = new ArrayList<>();
        courses2.add("化学");
        courses2.add("地理");
        courses2.add("政治");
        student2.setCourses(courses2);



        Observable.just(student1,student2)
                .subscribe(new Action1<Student>() {
                    @Override
                    public void call(Student student) {
                        Log.i(TAG, "call: name = "+student.getName());
                        List<String> course = student.getCourses();
                        for(String str:course){
                            Log.i(TAG, "call: str = "+str);
                        }
                    }
                });

这里我们没有进行转换,直接just发送过来,没有用到转换,然后在call中进行直接输出了

call: name = safly
call: str = 语文
call: str = 数学
call: str = 英语
call: name = wyf
call: str = 化学
call: str = 地理
call: str = 政治

我们用转换来试试看?
这里我们用到了flatmap这一函数,按通俗的一点理解:我们首先把Student转成了Observable,然后呢,又把student.getCourses()转成string挨个打印出来,结果如下:

 Observable.just(student1,student2)
                  .flatMap(new Func1<Student, Observable<String>>() {
                      @Override
                      public Observable<String> call(Student student) {
                       Log.i(TAG, "Observable " );
                          return Observable.from(student.getCourses());
                      }
                  })
                .subscribe(new Action1<String>() {
                    @Override
                    public void call(String s) {
                        Log.i(TAG, "call: s = "+s);     
                    }
                });

输出

 Observable
call: s = 语文
call: s = 数学
call: s = 英语
 Observable
call: s = 化学
call: s = 地理
call: s = 政治

我们还记得Observable.from嘛?
From操作符用来将某个对象转化为Observable对象

  public static <T> Observable<T> from(Iterable<? extends T> iterable) {
        return create(new OnSubscribeFromIterable<T>(iterable));
    }
作者:u013210620 发表于2016/9/4 19:29:19 原文链接
阅读:201 评论:0 查看评论

Linux内核调试技术——进程上下文R状态死锁监测

$
0
0

前一篇博文介绍了内核监测D状态死锁的hung task机制,本文介绍另一种死锁状态的监测手段——R状态死锁监测。R状态死锁指的是某一任务一直处于TASK_RUNNING态且一直占用着CPU,从而导致其他进程得不到调度而饿死的情况。一般情况下,R状态死锁较可能是由于程序出现死循环导致的,可以出现在内核态的进程上下文中(内核配置为非抢占式,soft lockup),也可以出现在中断上下文中的中断处理程序中(hard lockup)。异常的程序一直运行,CPU无法调度到其他的任务运行,对于单CPU的设备,则直接的表现就是“死机”。这种死锁现象较难定位,内核也同样提供了一种检测手段来检测这种死锁并向用户发出告警——LOCKUP_DETECTOR,它可支持监测进程上下文和中断上下文中的R状态死锁(SOFTLOCKUP_DETECTOR和HARDLOCKUP_DETECTOR),由于HARDLOCKUP_DETECTOR需要nmi中断的支持且目前的arm32环境并不支持,本文仅分析其中SOFTLOCKUP_DETECTOR中的原理及实现方式,并给出一个示例。


一、lockup detector机制分析

lockup detector机制在内核代码的kernel/watchdog.c中实现,本文以Linux 4.1.15版本源码为例进行分析。首先了解其背后的设计原理:利用进程上下文、中断、nmi中断的不同优先级实现死锁监测。它们3者的优先级关系为“进程上下文 < 中断 < nmi中断”,其中进程上下文优先级最低,可通过中断来进行监测进程的运行状态,nmi中断的优先级最高,它是一种不可屏蔽的中断,在中断上下文中发生死锁时,nmi中断处理也可正常进入,因此可用来监测中断中的死锁。不过可惜的是目前绝大多数的arm32芯片都不支持nmi中断,也包括我手中树莓派的bcm2835芯片。从程序的命名中就可以看出,该程序其实实现了一种软看门狗的功能,下面给出整体的软件流程框图:


该程序为每个cpu创建了一个进程和一个高精度定时器,其中进程用来喂狗,定时器用来唤醒喂狗进程和检测是否存在死锁进程,在检测到死锁进程后就触发报警,接下来详细分析源代码:

void __init lockup_detector_init(void)
{
	set_sample_period();

	if (watchdog_enabled)
		watchdog_enable_all_cpus();
}
首先入口函数lockup_detector_init(),该函数会在内核启动流程中按如下路径调用:start_kernel() --> rest_init() --> kernel_init()(启内核线程)--> kernel_init_freeable() --> lockup_detector_init()。该函数首先计算高精度定时器的到期时间(即喂狗时间),该值为监测超时时间值的1/5,默认为4s(20s/5),然后判断开关标识来确定是否启用监测机制,该标识在没有启用hard lockup detect的情况下默认为SOFT_WATCHDOG_ENABLED,表示开启soft lockup detect。于此同时内核也提供了如下的__setup接口,可从内核启动参数cmd line中设置值和开关:

static int __init softlockup_panic_setup(char *str)
{
	softlockup_panic = simple_strtoul(str, NULL, 0);

	return 1;
}
__setup("softlockup_panic=", softlockup_panic_setup);

static int __init nowatchdog_setup(char *str)
{
	watchdog_enabled = 0;
	return 1;
}
__setup("nowatchdog", nowatchdog_setup);

static int __init nosoftlockup_setup(char *str)
{
	watchdog_enabled &= ~SOFT_WATCHDOG_ENABLED;
	return 1;
}
__setup("nosoftlockup", nosoftlockup_setup);
此处假定开启soft lockup detect,接下来调用watchdog_enable_all_cpus()函数,该函数会尝试为每个CPU创建一个喂狗任务(并不会立即启动主函数执行):
static int watchdog_enable_all_cpus(void)
{
	int err = 0;

	if (!watchdog_running) {
		err = smpboot_register_percpu_thread(&watchdog_threads);
		if (err)
			pr_err("Failed to create watchdog threads, disabled\n");
		else
			watchdog_running = 1;
	} else {
		/*
		 * Enable/disable the lockup detectors or
		 * change the sample period 'on the fly'.
		 */
		update_watchdog_all_cpus();
	}

	return err;
}
该函数首先判断是否已经启动了任务,若没有则调用smpboot_register_percpu_thread()函数来创建任务,否则则调用update_watchdog_all_cpus()函数来更新定时器的到期时间。首先分析前一个分支,看一下watchdog_threads结构体的实现:

static struct smp_hotplug_thread watchdog_threads = {
	.store			= &softlockup_watchdog,
	.thread_should_run	= watchdog_should_run,
	.thread_fn		= watchdog,
	.thread_comm		= "watchdog/%u",
	.setup			= watchdog_enable,
	.cleanup		= watchdog_cleanup,
	.park			= watchdog_disable,
	.unpark			= watchdog_enable,
};
该结构注册了许多的回调函数,先简单了解一下:(1)softlockup_watchdog是一个全局的per cpu指针,它用来保存创建任务的进程描述符task_struct结构;(2)watchdog_should_run()是任务运行的判断函数,它会判断进程是否需要调用thread_fn指针指向的函数运行;(3)watchdog()是任务运行的主函数,该函数实现线程喂狗的动作;(4)setup回调函数watchdog_enable会在任务首次启动时调用,该函数会创建高精度定时器,用来激活喂狗任务和监测死锁超时;(5)cleanup回调函数用来清除任务,它会关闭定时器;(6)最后的park和unpark回调函数用于暂停运行和恢复运行任务。(7)thread_comm是任务名字,cpu0是watchdog/0,cpu1是watchdog/1,以此类推。

下面来简单看一下smpboot_register_percpu_thread()函数是如何为每个cpu创建任务的,同时又在何处调用上述的那些回调函数的(kernel/smpboot.c):

int smpboot_register_percpu_thread(struct smp_hotplug_thread *plug_thread)
{
	unsigned int cpu;
	int ret = 0;

	get_online_cpus();
	mutex_lock(&smpboot_threads_lock);
	for_each_online_cpu(cpu) {
		ret = __smpboot_create_thread(plug_thread, cpu);
		if (ret) {
			smpboot_destroy_threads(plug_thread);
			goto out;
		}
		smpboot_unpark_thread(plug_thread, cpu);
	}
	list_add(&plug_thread->list, &hotplug_threads);
out:
	mutex_unlock(&smpboot_threads_lock);
	put_online_cpus();
	return ret;
}
EXPORT_SYMBOL_GPL(smpboot_register_percpu_thread);
函数遍历所有的online cpu然后为其创建指定的任务,然后将他们添加到hotplug_threads中去(该链表是用来遍历用的);

static int
__smpboot_create_thread(struct smp_hotplug_thread *ht, unsigned int cpu)
{
	struct task_struct *tsk = *per_cpu_ptr(ht->store, cpu);
	......

	tsk = kthread_create_on_cpu(smpboot_thread_fn, td, cpu,
				    ht->thread_comm);				
	......
	
	return 0;
}
可以看出,为每个cpu创建的任务并不是直接调用前文中注册的thread_fn()回调函数,而是调用了smpboot_thread_fn()函数,该函数会维护任务运行的几个状态,视状态的不同调用不同的注册回调处理函数:
static int smpboot_thread_fn(void *data)
{
	struct smpboot_thread_data *td = data;
	struct smp_hotplug_thread *ht = td->ht;

	while (1) {
		set_current_state(TASK_INTERRUPTIBLE);
		preempt_disable();
		if (kthread_should_stop()) {
			__set_current_state(TASK_RUNNING);
			preempt_enable();
			if (ht->cleanup)
				ht->cleanup(td->cpu, cpu_online(td->cpu));
			kfree(td);
			return 0;
		}

		if (kthread_should_park()) {
			__set_current_state(TASK_RUNNING);
			preempt_enable();
			if (ht->park && td->status == HP_THREAD_ACTIVE) {
				BUG_ON(td->cpu != smp_processor_id());
				ht->park(td->cpu);
				td->status = HP_THREAD_PARKED;
			}
			kthread_parkme();
			/* We might have been woken for stop */
			continue;
		}

		BUG_ON(td->cpu != smp_processor_id());

		/* Check for state change setup */
		switch (td->status) {
		case HP_THREAD_NONE:
			__set_current_state(TASK_RUNNING);
			preempt_enable();
			if (ht->setup)
				ht->setup(td->cpu);
			td->status = HP_THREAD_ACTIVE;
			continue;

		case HP_THREAD_PARKED:
			__set_current_state(TASK_RUNNING);
			preempt_enable();
			if (ht->unpark)
				ht->unpark(td->cpu);
			td->status = HP_THREAD_ACTIVE;
			continue;
		}

		if (!ht->thread_should_run(td->cpu)) {
			preempt_enable_no_resched();
			schedule();
		} else {
			__set_current_state(TASK_RUNNING);
			preempt_enable();
			ht->thread_fn(td->cpu);
		}
	}
}
这个函数是一个大循环,在每次循环中都会首先依次判断是否需要停止本任务、是否需要park本任务,如果是则进行相应的处理,可以看到这里就会调用前文中注册的cleanup()和park()回调函数;如果不需要stop和park则接下来按照状态机处理,对于初次运行的任务,这里会调用setup()回调进行相应的初始化动作;最后对于在正常运行中的最一般情况下,会调用thread_should_run回调判断是否需要调用注册主函数,视判断的返回值情况调用thread_fn()函数。下面来看前文中注册的setup回调watchdog_enable():

static void watchdog_enable(unsigned int cpu)
{
	struct hrtimer *hrtimer = raw_cpu_ptr(&watchdog_hrtimer);

	/* kick off the timer for the hardlockup detector */
	hrtimer_init(hrtimer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
	hrtimer->function = watchdog_timer_fn;

	/* Enable the perf event */
	watchdog_nmi_enable(cpu);

	/* done here because hrtimer_start can only pin to smp_processor_id() */
	hrtimer_start(hrtimer, ns_to_ktime(sample_period),
		      HRTIMER_MODE_REL_PINNED);

	/* initialize timestamp */
	watchdog_set_prio(SCHED_FIFO, MAX_RT_PRIO - 1);
	__touch_watchdog();
}
值得注意的是,这个函数是每个online的cpu都会运行的,首先从per cpu变量中获取本cpu的高精度定时器指针hrtimer并初始化之,注册定时器到期调用函数watchdog_timer_fn(),然后启动定时器,指定到期时间就是前文中计算的sample_period(4s),最后调整当前进程的调度策略为FIFO实时进程并提高优先级,这么做是为了保证本喂狗任务能够以较高的优先级运行,以免无法及时喂狗而出现误报的情况。函数最后调用__touch_watchdog()函数执行首次喂狗动作。

static void __touch_watchdog(void)
{
	__this_cpu_write(watchdog_touch_ts, get_timestamp());
}
这里的watchdog_touch_ts也是一个per cpu变量,每个cpu维护自己独有的。这里将当前系统计时的时钟值(单位ns)以约等于的形式转换的秒单位的值,然后刷新到watchdog_touch_ts中,以此模拟喂狗的动作。

定时器初始化完成后,接下来smpboot_thread_fn()函数就会调用thread_should_run()回调函数watchdog_should_run():

static int watchdog_should_run(unsigned int cpu)
{
	return __this_cpu_read(hrtimer_interrupts) !=
		__this_cpu_read(soft_lockup_hrtimer_cnt);
}
此处只是比较了两个变量,当这两个变量不相等时才会调用thread_fn()回调,否则将任务设置为TASK_INTERRUPTIBLE状态然后调度(睡眠)。那这两个变量值合适才会不一样呢?下面来分析另外一条路,注意前文中的定时器已经启动了,来看一下sample_period时间到期后的调用函数,这个函数比较长,分段来看:

/* watchdog kicker functions */
static enum hrtimer_restart watchdog_timer_fn(struct hrtimer *hrtimer)
{
	unsigned long touch_ts = __this_cpu_read(watchdog_touch_ts);
	struct pt_regs *regs = get_irq_regs();
	int duration;
	int softlockup_all_cpu_backtrace = sysctl_softlockup_all_cpu_backtrace;

	/* kick the hardlockup detector */
	watchdog_interrupt_count();

	/* kick the softlockup detector */
	wake_up_process(__this_cpu_read(softlockup_watchdog));

	/* .. and repeat */
	hrtimer_forward_now(hrtimer, ns_to_ktime(sample_period));
首先获取最后一次的喂狗时间并保存在touch_ts中,然后调用watchdog_interrupt_count()函数累加hrtimer_interrupts值,显然该值表示的是当前cpu触发定时器中断的次数。

然后尝试唤醒已经睡眠的喂狗线程(注意,由于这里改变了hrtimer_interrupts值,前文中的watchdog_should_run自然就会返回TRUE了,那么就可以执行注册的主函数了)。接着本函数继续注册下一次的定时器到期时间。

	if (touch_ts == 0) {
		if (unlikely(__this_cpu_read(softlockup_touch_sync))) {
			/*
			 * If the time stamp was touched atomically
			 * make sure the scheduler tick is up to date.
			 */
			__this_cpu_write(softlockup_touch_sync, false);
			sched_clock_tick();
		}

		/* Clear the guest paused flag on watchdog reset */
		kvm_check_and_clear_guest_paused();
		__touch_watchdog();
		return HRTIMER_RESTART;
	}
这个判断在kgdb的调试中会用到,正常情况下不会进入,不做分析,继续往下:

	/* check for a softlockup
	 * This is done by making sure a high priority task is
	 * being scheduled.  The task touches the watchdog to
	 * indicate it is getting cpu time.  If it hasn't then
	 * this is a good indication some task is hogging the cpu
	 */
	duration = is_softlockup(touch_ts);
这里调用is_softlockup()函数返回当前时刻是否已经超过了“看门狗”的到期时间:
static int is_softlockup(unsigned long touch_ts)
{
	unsigned long now = get_timestamp();

	if (watchdog_enabled & SOFT_WATCHDOG_ENABLED) {
		/* Warn about unreasonable delays. */
		if (time_after(now, touch_ts + get_softlockup_thresh()))
			return now - touch_ts;
	}
	return 0;
}
这里首先判断是否开启了soft lockup detect,是且已经超时的情况下(默认的超时时间是20s)返回超时时间间隔,否则返回0。下面来看一下超时的情况下会执行哪些处理:

	if (unlikely(duration)) {
		......
		/* only warn once */
		if (__this_cpu_read(soft_watchdog_warn) == true) {
			/*
			 * When multiple processes are causing softlockups the
			 * softlockup detector only warns on the first one
			 * because the code relies on a full quiet cycle to
			 * re-arm.  The second process prevents the quiet cycle
			 * and never gets reported.  Use task pointers to detect
			 * this.
			 */
			if (__this_cpu_read(softlockup_task_ptr_saved) !=
			    current) {
				__this_cpu_write(soft_watchdog_warn, false);
				__touch_watchdog();
			}
			return HRTIMER_RESTART;
		}
soft_watchdog_warn标识会在已经出现了一次看门狗超时的情况下置位,此处的用意是对于同一个死锁进程,内核只做一次报警动作,如果死锁的进程发生了改变,那该标识会重新设置为false,将可以重新触发报警。
		if (softlockup_all_cpu_backtrace) {
			/* Prevent multiple soft-lockup reports if one cpu is already
			 * engaged in dumping cpu back traces
			 */
			if (test_and_set_bit(0, &soft_lockup_nmi_warn)) {
				/* Someone else will report us. Let's give up */
				__this_cpu_write(soft_watchdog_warn, true);
				return HRTIMER_RESTART;
			}
		}
softlockup_all_cpu_backtrace是一个开关,用来表示是否需要在一个cpu超时时打印所有cpu的backtrace信息,可以通过sysctrl进行控制。此处的用以是为了避免多个cpu再检测到死锁是同时调用trigger_allbutself_cpu_backtrace函数打印所有cpu的backtrace信息,因为在同一时刻只需要调用一次就可以了。
		pr_emerg("BUG: soft lockup - CPU#%d stuck for %us! [%s:%d]\n",
			smp_processor_id(), duration,
			current->comm, task_pid_nr(current));
		__this_cpu_write(softlockup_task_ptr_saved, current);
		print_modules();
		print_irqtrace_events(current);
		if (regs)
			show_regs(regs);
		else
			dump_stack();
这里就开始依次打印出内核模块信息,中断信息,中断栈信息和backtrace信息,然后记录下了触发死锁的任务描述符task_struct。

		if (softlockup_all_cpu_backtrace) {
			/* Avoid generating two back traces for current
			 * given that one is already made above
			 */
			trigger_allbutself_cpu_backtrace();

			clear_bit(0, &soft_lockup_nmi_warn);
			/* Barrier to sync with other cpus */
			smp_mb__after_atomic();
		}
这里同前面相呼应,调用trigger_allbutself_cpu_backtrace()函数打印出了所有cpu的backtrace信息,这个函数是arch架构相关的。
		add_taint(TAINT_SOFTLOCKUP, LOCKDEP_STILL_OK);
		if (softlockup_panic)
			panic("softlockup: hung tasks");
		__this_cpu_write(soft_watchdog_warn, true);
最后如果设置了panic标识,则直接触发panic,否则置位了报警标识,后续针对触发本次报警的死锁任务将不再报警。分析完超时的处理方式,回过头分析一下前文中的喂狗进程是如何运行的。
static void watchdog(unsigned int cpu)
{
	__this_cpu_write(soft_lockup_hrtimer_cnt,
			 __this_cpu_read(hrtimer_interrupts));
	__touch_watchdog();

	/*
	 * watchdog_nmi_enable() clears the NMI_WATCHDOG_ENABLED bit in the
	 * failure path. Check for failures that can occur asynchronously -
	 * for example, when CPUs are on-lined - and shut down the hardware
	 * perf event on each CPU accordingly.
	 *
	 * The only non-obvious place this bit can be cleared is through
	 * watchdog_nmi_enable(), so a pr_info() is placed there.  Placing a
	 * pr_info here would be too noisy as it would result in a message
	 * every few seconds if the hardlockup was disabled but the softlockup
	 * enabled.
	 */
	if (!(watchdog_enabled & NMI_WATCHDOG_ENABLED))
		watchdog_nmi_disable(cpu);
}
这里首先将hrtimer_interrupts的值付给soft_lockup_hrtimer_cnt,这样在本次喂狗结束后到下一次定时器到期前,该函数不会投入运行,进程将进入睡眠状态。该函数剩下的就很简单了,直接调用__touch_watchdog()函数执行喂狗动作。


以上就是进程上下文中的R状态死锁的核心监测代码,该程序还提供了一些可以通过sysctrl控制启停和超时时间等的接口,比较简单就不分析了。从以上实现可以看出其本质就是利用了hr定时器中断处理函数周期性的唤醒进程执行软喂狗动作,同时自身则检测软看门狗是否超时。在正常的情况下,当前cpu的定时器中唤醒的喂狗进程一定是能够得到调度的(视cpu负荷情况可能略有延时),即是不可能超过设定的超时时间的,但是如果当前cpu中的某一个进程占用cpu时间超过了设定的超时时间(20s),就会直接导致软看门狗超时并触发一次报警动作,如果这个进程一直不释放cpu(例如while循环),那么也只会报警一次,反之会重新开启报警功能。


二、示例演示

演示环境:树莓派b(Linux 4.1.15)
1、首先确认启用内核配置

    Kernel hacking  --->

            Debug Lockups and Hangs  --->

                   [*] Detect Hard and Soft Lockups

                   [*]   Panic (Reboot) On Soft Lockups(可选)

2、然后确认内核调度策略配置

    Kernel Features  --->

            Preemption Model (Voluntary Kernel Preemption (Desktop))  

                   ( ) No Forced Preemption (Server)                
                   (X) Voluntary Kernel Preemption (Desktop)  

                   ( ) Preemptible Kernel (Low-Latency Desktop)

注意调度策略需要配置为非抢占式的内核,若是抢占式的,则测试程序会无效(因为其他内核进程可能会主动抢占死锁的进程)。

3、编写演示程序

#include <linux/module.h>  
#include <linux/kernel.h>  
#include <linux/init.h>  

static int __init rlock_init(void)
{
	while(1);
	
	return 0;
}

static void __exit rlock_exit(void) 
{
	return;
}

module_init(rlock_init);  
module_exit(rlock_exit);  
MODULE_LICENSE("GPL");  
本程序非常的简单,编写一个模块程序并在模块初始化函数中执行while(1)循环即可,以此来触发insmod进程在rlock_init()函数中陷入R状态死锁。
在树莓派中加载该模块后直接中断就”挂死“了,然后再约20s后内核打印如下:

root@apple:~# insmod rlock.ko 
[   60.254450] NMI watchdog: BUG: soft lockup - CPU#0 stuck for 23s! [insmod:515]
[   60.261684] Modules linked in: rlock(O+) sg bcm2835_gpiomem bcm2835_wdt(O) uio_pdrv_genirq uio
[   60.270344] CPU: 0 PID: 515 Comm: insmod Tainted: G           O    4.1.15 #8
[   60.277382] Hardware name: BCM2708
[   60.280783] task: c591df60 ti: c5eaa000 task.ti: c5eaa000
[   60.286189] PC is at rlock_init+0xc/0x10 [rlock]
[   60.290812] LR is at do_one_initcall+0x90/0x1e8
[   60.295342] pc : [<bf02e00c>]    lr : [<c0009558>]    psr: 60000013
[   60.295342] sp : c5eabdc8  ip : c5eabdd8  fp : c5eabdd4
[   60.306803] r10: 00000000  r9 : 00000124  r8 : bf02e000
[   60.312020] r7 : bf02c0a4  r6 : c5eed660  r5 : c0bbd6e8  r4 : c0bbd6e8
[   60.318539] r3 : 00000000  r2 : c6c01f00  r1 : 60000013  r0 : 60000013
[   60.325058] Flags: nZCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment user
[   60.332183] Control: 00c5387d  Table: 05828008  DAC: 00000015
[   60.337924] CPU: 0 PID: 515 Comm: insmod Tainted: G           O    4.1.15 #8
[   60.344958] Hardware name: BCM2708
[   60.348410] [<c0016660>] (unwind_backtrace) from [<c0013524>] (show_stack+0x20/0x24)
[   60.356168] [<c0013524>] (show_stack) from [<c0526c54>] (dump_stack+0x20/0x28)
[   60.363398] [<c0526c54>] (dump_stack) from [<c0010ae4>] (show_regs+0x1c/0x20)
[   60.370547] [<c0010ae4>] (show_regs) from [<c0097444>] (watchdog_timer_fn+0x160/0x1a4)
[   60.378482] [<c0097444>] (watchdog_timer_fn) from [<c006495c>] (__run_hrtimer+0x68/0x1c4)
[   60.386668] [<c006495c>] (__run_hrtimer) from [<c00651b0>] (hrtimer_interrupt+0x104/0x270)
[   60.394942] [<c00651b0>] (hrtimer_interrupt) from [<c001f394>] (bcm2708_timer_interrupt+0x38/0x48)
[   60.403911] [<c001f394>] (bcm2708_timer_interrupt) from [<c0059e5c>] (handle_irq_event_percpu+0x5c/0x200)
[   60.413481] [<c0059e5c>] (handle_irq_event_percpu) from [<c005a038>] (handle_irq_event+0x38/0x48)
[   60.422359] [<c005a038>] (handle_irq_event) from [<c005ca64>] (handle_level_irq+0x98/0x114)
[   60.430712] [<c005ca64>] (handle_level_irq) from [<c0059760>] (__handle_domain_irq+0x7c/0xdc)
[   60.439244] [<c0059760>] (__handle_domain_irq) from [<c00107b4>] (handle_IRQ+0x2c/0x30)
[   60.447251] [<c00107b4>] (handle_IRQ) from [<c0009340>] (asm_do_IRQ+0x18/0x1c)
[   60.454485] [<c0009340>] (asm_do_IRQ) from [<c052b738>] (__irq_svc+0x38/0xb0)
[   60.461613] Exception stack(0xc5eabd80 to 0xc5eabdc8)
[   60.466670] bd80: 60000013 60000013 c6c01f00 00000000 c0bbd6e8 c0bbd6e8 c5eed660 bf02c0a4
[   60.474845] bda0: bf02e000 00000124 00000000 c5eabdd4 c5eabdd8 c5eabdc8 c0009558 bf02e00c
[   60.483010] bdc0: 60000013 ffffffff
[   60.486515] [<c052b738>] (__irq_svc) from [<bf02e00c>] (rlock_init+0xc/0x10 [rlock])
[   60.494271] [<bf02e00c>] (rlock_init [rlock]) from [<c0009558>] (do_one_initcall+0x90/0x1e8)
[   60.502721] [<c0009558>] (do_one_initcall) from [<c007ad04>] (do_init_module+0x6c/0x1c0)
[   60.510819] [<c007ad04>] (do_init_module) from [<c007c620>] (load_module+0x1690/0x1d34)
[   60.518827] [<c007c620>] (load_module) from [<c007cda0>] (SyS_init_module+0xdc/0x130)
[   60.526662] [<c007cda0>] (SyS_init_module) from [<c000f800>] (ret_fast_syscall+0x0/0x54)
[   60.534745] Kernel panic - not syncing: softlockup: hung tasks
[   60.540577] CPU: 0 PID: 515 Comm: insmod Tainted: G           O L  4.1.15 #8
[   60.547613] Hardware name: BCM2708
[   60.551033] [<c0016660>] (unwind_backtrace) from [<c0013524>] (show_stack+0x20/0x24)
[   60.558781] [<c0013524>] (show_stack) from [<c0526c54>] (dump_stack+0x20/0x28)
[   60.566005] [<c0526c54>] (dump_stack) from [<c0523958>] (panic+0x90/0x1fc)
[   60.572885] [<c0523958>] (panic) from [<c009746c>] (watchdog_timer_fn+0x188/0x1a4)
[   60.580464] [<c009746c>] (watchdog_timer_fn) from [<c006495c>] (__run_hrtimer+0x68/0x1c4)
[   60.588648] [<c006495c>] (__run_hrtimer) from [<c00651b0>] (hrtimer_interrupt+0x104/0x270)
[   60.596917] [<c00651b0>] (hrtimer_interrupt) from [<c001f394>] (bcm2708_timer_interrupt+0x38/0x48)
[   60.605881] [<c001f394>] (bcm2708_timer_interrupt) from [<c0059e5c>] (handle_irq_event_percpu+0x5c/0x200)
[   60.615450] [<c0059e5c>] (handle_irq_event_percpu) from [<c005a038>] (handle_irq_event+0x38/0x48)
[   60.624326] [<c005a038>] (handle_irq_event) from [<c005ca64>] (handle_level_irq+0x98/0x114)
[   60.632680] [<c005ca64>] (handle_level_irq) from [<c0059760>] (__handle_domain_irq+0x7c/0xdc)
[   60.641211] [<c0059760>] (__handle_domain_irq) from [<c00107b4>] (handle_IRQ+0x2c/0x30)
[   60.649218] [<c00107b4>] (handle_IRQ) from [<c0009340>] (asm_do_IRQ+0x18/0x1c)
[   60.656450] [<c0009340>] (asm_do_IRQ) from [<c052b738>] (__irq_svc+0x38/0xb0)
[   60.663578] Exception stack(0xc5eabd80 to 0xc5eabdc8)
[   60.668633] bd80: 60000013 60000013 c6c01f00 00000000 c0bbd6e8 c0bbd6e8 c5eed660 bf02c0a4
[   60.676806] bda0: bf02e000 00000124 00000000 c5eabdd4 c5eabdd8 c5eabdc8 c0009558 bf02e00c
[   60.684972] bdc0: 60000013 ffffffff
[   60.688477] [<c052b738>] (__irq_svc) from [<bf02e00c>] (rlock_init+0xc/0x10 [rlock])
[   60.696227] [<bf02e00c>] (rlock_init [rlock]) from [<c0009558>] (do_one_initcall+0x90/0x1e8)
[   60.704671] [<c0009558>] (do_one_initcall) from [<c007ad04>] (do_init_module+0x6c/0x1c0)
[   60.712765] [<c007ad04>] (do_init_module) from [<c007c620>] (load_module+0x1690/0x1d34)
[   60.720771] [<c007c620>] (load_module) from [<c007cda0>] (SyS_init_module+0xdc/0x130)
[   60.728607] [<c007cda0>] (SyS_init_module) from [<c000f800>] (ret_fast_syscall+0x0/0x54)
PANIC: softlockup: hung tasks


三、总结

R状态死锁是在进程上下文或中断上下文中出现的一种长期占用cpu的非正常现象,在不易复现的环境中比较难以定位。本文分析了内核提供的监测其中在进程上下文中死锁的SOFTLOCKUP_DETECTOR机制原理及实现方式。开发人员可通过这种机制较为有效的定位问题。



作者:luckyapple1028 发表于2016/9/4 19:44:25 原文链接
阅读:97 评论:0 查看评论

android编译系统分析(三)-make

$
0
0

Android编译系统分析系列文章:

android编译系统分析一<source build/envsetup.sh与lunch>

Android编译系统<二>-mm编译单个模块

android编译系统分析(三)-make

android编译系统(四)-实战:新增一个产品


这篇博客的目标是摸清楚默认编译整个android系统时代码的流程。

当我们执行make的时候,会查找当前的Makefie文件或者makefile文件并且执行,在android顶级源码目录下面,确实有个Makefile,它之后一行内容:

### DO NOT EDIT THIS FILE ###
include build/core/main.mk
### DO NOT EDIT THIS FILE ###
因此,正真执行的是build/core/main.mk

一.依赖浅析

当我们执行make命令的时候,如果没有传入一个目标,那么就会执行默认的目标。注意,我们在编译android系统的时候,只需要执行make就可以了,那么很显然它会执行默认的目标了,那么默认的目标是什么呢?

在build/core/main.mk中:

# This is the default target.  It must be the first declared target.
.PHONY: droid
DEFAULT_GOAL := droid
$(DEFAULT_GOAL):

在main.mk开始不久,就出现了一个伪目标,即便你看不懂Makefile也没有关系,注释上说的很清楚了,他就是默认的目标了。而且这个默认的目标是一个伪目标。make工具遇到伪目标以后,会检查解析伪目标的依赖,如果伪目标存在依赖,就会检查这些依赖,如果这些依赖是伪目标,继续检查这个伪目标的依赖,如果不是伪目标,就会生成这个目标。

阅读一个Makefile,理清目标的依赖关系很重,下图列出了部分重要的以来关系:


在对依赖关系有个了解之后,我们开始顺着make的加载流程,看看它到底做了什么。

首先,我觉得很重要的就是加载特定产品的配置信息。

二.配置产品信息

首先,大致的流程如下图所示:


在product_config.mk中:

ifneq ($(strip $(TARGET_BUILD_APPS)),)
# An unbundled app build needs only the core product makefiles.
all_product_configs := $(call get-product-makefiles,\
    $(SRC_TARGET_DIR)/product/AndroidProducts.mk)
else
# Read in all of the product definitions specified by the AndroidProducts.mk
# files in the tree.
all_product_configs := $(get-all-product-makefiles)
endif

1.AndoridProducts.mk 使用get-all-product-makefiles获取所有的AndoridProducts.mk文件:

define get-all-product-makefiles
$(call get-product-makefiles,$(_find-android-products-files))
endef
调用_find-android-products-files获取所有的AndroidProducts.mk,然后交由get-product-makefiles函数处理。

define _find-android-products-files
$(shell test -d device && find device -maxdepth 6 -name AndroidProducts.mk) \
  $(shell test -d vendor && find vendor -maxdepth 6 -name AndroidProducts.mk) \
  $(SRC_TARGET_DIR)/product/AndroidProducts.mk
endef
define get-product-makefiles
$(sort \
  $(foreach f,$(1), \
    $(eval PRODUCT_MAKEFILES :=) \
    $(eval LOCAL_DIR := $(patsubst %/,%,$(dir $(f)))) \
    $(eval include $(f)) \
    $(PRODUCT_MAKEFILES) \
   ) \
  $(eval PRODUCT_MAKEFILES :=) \
  $(eval LOCAL_DIR :=) \
 )
endef
可以看到最终处理的结果是加载了AndroidProducts.mk, 返回了一个排好顺序的PRODUCT_MAKEFILES。

这里把所有的AndroidProducts.mk都加载进来了,但是我们只需要我们产品的配置信息呀,所以接着做一个查找,找到属于我们产品的AndroidProducts.mk:

# Find the product config makefile for the current product.
# all_product_configs consists items like:
# <product_name>:<path_to_the_product_makefile>
# or just <path_to_the_product_makefile> in case the product name is the
# same as the base filename of the product config makefile.
current_product_makefile :=
all_product_makefiles :=
$(foreach f, $(all_product_configs),\
    $(eval _cpm_words := $(subst :,$(space),$(f)))\
    $(eval _cpm_word1 := $(word 1,$(_cpm_words)))\
    $(eval _cpm_word2 := $(word 2,$(_cpm_words)))\
    $(if $(_cpm_word2),\
        $(eval all_product_makefiles += $(_cpm_word2))\
        $(if $(filter $(TARGET_PRODUCT),$(_cpm_word1)),\
            $(eval current_product_makefile += $(_cpm_word2)),),\
        $(eval all_product_makefiles += $(f))\
        $(if $(filter $(TARGET_PRODUCT),$(basename $(notdir $(f)))),\
            $(eval current_product_makefile += $(f)),)))
_cpm_words :=
_cpm_word1 :=
_cpm_word2 :=
current_product_makefile := $(strip $(current_product_makefile))
all_product_makefiles := $(strip $(all_product_makefiles))

2.current_product_makefile

最终找到的结果存储在current_product_makefile中。关于它的值,这里举例说明:

加入我们在lunch的时候选择了 5:

     1. aosp_arm-eng
     2. aosp_arm64-eng
     3. aosp_mips-eng
     4. aosp_mips64-eng
     5. aosp_x86-eng
     6. aosp_x86_64-eng
那么经过以上查找current_product_makefile就等于device/generic/x86/mini_x86.mk

3.加载产品配置文件

ifneq (,$(filter product-graph dump-products, $(MAKECMDGOALS)))
# Import all product makefiles.
$(call import-products, $(all_product_makefiles))
else
# Import just the current product.
ifndef current_product_makefile
$(error Can not locate config makefile for product "$(TARGET_PRODUCT)")
endif
ifneq (1,$(words $(current_product_makefile)))
$(error Product "$(TARGET_PRODUCT)" ambiguous: matches $(current_product_makefile))
endif
$(call import-products, $(current_product_makefile))
endif  # Import all or just the current product makefile

# Sanity check
$(check-all-products)
在import-products中导入产品的配置信息,这里就是device/generic/x86/mini_x86.mk。

4然后获取TARGET_DEVICE的值:

# Find the device that this product maps to.
TARGET_DEVICE := $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_DEVICE)
此时,TARGET_DEVICE = mini_x86.mk;

5获取要拷贝的文件

# A list of words like <source path>:<destination path>[:<owner>].
# The file at the source path should be copied to the destination path
# when building  this product.  <destination path> is relative to
# $(PRODUCT_OUT), so it should look like, e.g., "system/etc/file.xml".
# The rules for these copy steps are defined in build/core/Makefile.
# The optional :<owner> is used to indicate the owner of a vendor file.
PRODUCT_COPY_FILES := \
    $(strip $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_COPY_FILES))

这个变量也很重要,它存储了需要拷贝的文件。格式为 <source path>:<destination path>,在build/core/Makefile一开始就会先拷贝这个变量指定的文件。

6.加载BoardConfig.mk

又回到envsetup.mk中:

# Boards may be defined under $(SRC_TARGET_DIR)/board/$(TARGET_DEVICE)
# or under vendor/*/$(TARGET_DEVICE).  Search in both places, but
# make sure only one exists.
# Real boards should always be associated with an OEM vendor.
board_config_mk := \
	$(strip $(wildcard \
		$(SRC_TARGET_DIR)/board/$(TARGET_DEVICE)/BoardConfig.mk \
		$(shell test -d device && find device -maxdepth 4 -path '*/$(TARGET_DEVICE)/BoardConfig.mk') \
		$(shell test -d vendor && find vendor -maxdepth 4 -path '*/$(TARGET_DEVICE)/BoardConfig.mk') \
	))
ifeq ($(board_config_mk),)
  $(error No config file found for TARGET_DEVICE $(TARGET_DEVICE))
endif
ifneq ($(words $(board_config_mk)),1)
  $(error Multiple board config files for TARGET_DEVICE $(TARGET_DEVICE): $(board_config_mk))
endif
include $(board_config_mk)
ifeq ($(TARGET_ARCH),)
  $(error TARGET_ARCH not defined by board config: $(board_config_mk))
endif

BoardConfig.mk中配置了重要的板级信息,比如cpu架构等。

至此,配置一个产品所需的AndroidProducts.mk,具体产品的配置文件,比如这里的mini_x86.mk以及BoardConfig.mk都加载进来了。


三.加载所有模块

加载完单板信息,make又回到main.mk中,不就就发现了ONE_SHOT_MAKEFILE变量的判断:

1ONE_SHOT_MAKEFILE

ifneq ($(ONE_SHOT_MAKEFILE),)
# We've probably been invoked by the "mm" shell function
# with a subdirectory's makefile.
include $(ONE_SHOT_MAKEFILE)
# Change CUSTOM_MODULES to include only modules that were
# defined by this makefile; this will install all of those
# modules as a side-effect.  Do this after including ONE_SHOT_MAKEFILE
# so that the modules will be installed in the same place they
# would have been with a normal make.
CUSTOM_MODULES := $(sort $(call get-tagged-modules,$(ALL_MODULE_TAGS)))
FULL_BUILD :=
# Stub out the notice targets, which probably aren't defined
# when using ONE_SHOT_MAKEFILE.
NOTICE-HOST-%: ;
NOTICE-TARGET-%: ;

# A helper goal printing out install paths
.PHONY: GET-INSTALL-PATH
GET-INSTALL-PATH:
	@$(foreach m, $(ALL_MODULES), $(if $(ALL_MODULES.$(m).INSTALLED), \
		echo 'INSTALL-PATH: $(m) $(ALL_MODULES.$(m).INSTALLED)';))

else # ONE_SHOT_MAKEFILE

ifneq ($(dont_bother),true)
#
# Include all of the makefiles in the system
#

# Can't use first-makefiles-under here because
# --mindepth=2 makes the prunes not work.
subdir_makefiles := \
	$(shell build/tools/findleaves.py --prune=$(OUT_DIR) --prune=.repo --prune=.git $(subdirs) Android.mk)

$(foreach mk, $(subdir_makefiles), $(info including $(mk) ...)$(eval include $(mk)))

endif # dont_bother

endif # ONE_SHOT_MAKEFILE
如果这个变量定义了,那么,就是编译一个模块,在上一篇博客中已将分析过了,如果没有定义,就说明是编译整个系统。

MAKECMDGOALS是make的一个环境变量,当我们执行make的时候并没有设置它,因此它为空。所以dont_bother不等于true,因此,就会加载所有的Android.mk.这里使用
一个python脚本查找系统中所有的Android.mk,然后Include进来。
四 收集所有要安装的模块
在main.mk中继续往下看:

3.1FULL_BUILD

ifdef FULL_BUILD
  # The base list of modules to build for this product is specified
  # by the appropriate product definition file, which was included
  # by product_config.mk.
  product_MODULES := $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_PACKAGES)
  # Filter out the overridden packages before doing expansion
  product_MODULES := $(filter-out $(foreach p, $(product_MODULES), \
      $(PACKAGES.$(p).OVERRIDES)), $(product_MODULES))

  # Resolve the :32 :64 module name
  modules_32 := $(patsubst %:32,%,$(filter %:32, $(product_MODULES)))
  modules_64 := $(patsubst %:64,%,$(filter %:64, $(product_MODULES)))
  modules_rest := $(filter-out %:32 %:64,$(product_MODULES))
  # Note for 32-bit product, $(modules_32) and $(modules_64) will be
  # added as their original module names.
  product_MODULES := $(call get-32-bit-modules-if-we-can, $(modules_32))
  product_MODULES += $(modules_64)
  # For the rest we add both
  product_MODULES += $(call get-32-bit-modules, $(modules_rest))
  product_MODULES += $(modules_rest)

  $(call expand-required-modules,product_MODULES,$(product_MODULES))

  product_FILES := $(call module-installed-files, $(product_MODULES))
  ifeq (0,1)
    $(info product_FILES for $(TARGET_DEVICE) ($(INTERNAL_PRODUCT)):)
    $(foreach p,$(product_FILES),$(info :   $(p)))
    $(error done)
  endif
else
  # We're not doing a full build, and are probably only including
  # a subset of the module makefiles.  Don't try to build any modules
  # requested by the product, because we probably won't have rules
  # to build them.
  product_FILES :=
endif
在执行make的时候,FULL_BUILD:=true
product_MODULES是所有产品配置文件中添加的要打包进系统镜像中的模块,它只是一个名字,比如上篇博客分析过的screencap。
product_FILES获取对应模块的.INSTALLED的值。
define module-installed-files
$(foreach module,$(1),$(ALL_MODULES.$(module).INSTALLED))
endef
在加载单个模块的时候,会给每一个模块生成另外两个值:
$(ALL_MODULES.$(target)).BUILT
$(ALL_MODULES.$(target)).INSTALLED
它们在base_rule.mk中生成:
ALL_MODULES.$(my_register_name).BUILT := \
    $(ALL_MODULES.$(my_register_name).BUILT) $(LOCAL_BUILT_MODULE)
ifneq (true,$(LOCAL_UNINSTALLABLE_MODULE))
ALL_MODULES.$(my_register_name).INSTALLED := \
    $(strip $(ALL_MODULES.$(my_register_name).INSTALLED) $(LOCAL_INSTALLED_MODULE))
ALL_MODULES.$(my_register_name).BUILT_INSTALLED := \
    $(strip $(ALL_MODULES.$(my_register_name).BUILT_INSTALLED) $(LOCAL_BUILT_MODULE):$(LOCAL_INSTALLED_MODULE))
endif

$(ALL_MODULES.$(target)).BUILT代表的一般是out/target/product/xxx/obj下编译生成的模块。
$(ALL_MODULES.$(target)).INSTALLED代表的是out/target/product/xxx/system下生成的模块。

3.2 全部安装模块

modules_to_install := $(sort \
    $(ALL_DEFAULT_INSTALLED_MODULES) \
    $(product_FILES) \
    $(foreach tag,$(tags_to_install),$($(tag)_MODULES)) \
    $(CUSTOM_MODULES) \
  )

ALL_DEFAULT_INSTALLED_MODULES是系统默认要安装的模块,product_FILES是特定产品附加的要安装的模块,foreach找到的是特定
TAG的模块,以及加上CUSTOM_MODULES,这样modules_to_install就是全部的要安装的模块了。
ALL_DEFAULT_INSTALLED_MODULES := $(modules_to_install)
include $(BUILD_SYSTEM)/Makefile

然后把modules_to_install的值全部赋给ALL_DEFAULT_INSTALLED_MODULES,接着加载build/core/Makefile。这个Makefile会使用
ALL_DEFAULT_INSTALLED_MODULES变量最终生成所有的镜像文件。生成镜像文件的过程放在下一节讨论。


四.编译所有模块

依赖关系我们在一开始就做了简单的梳理,现在开始分析编译所有模块的依赖关系。

从droid目标定义的地方来看,没有看到它的依赖,但我们向下搜索,就会发现:

.PHONY: apps_only
apps_only: $(unbundled_build_modules)

droid: apps_only
# Building a full system-- the default is to build droidcore
droid: droidcore dist_files

我们会发现它有出现了两个依赖,那它到底依赖哪一个呢?

droid依赖哪一个取决于ifneq ($(TARGET_BUILD_APPS),)是否成立,也就是有没有给TARGET_BUILD_APPS赋值过,源码如下:

ifneq ($(TARGET_BUILD_APPS),)
  # If this build is just for apps, only build apps and not the full system by default.

  unbundled_build_modules :=
  ifneq ($(filter all,$(TARGET_BUILD_APPS)),)
    # If they used the magic goal "all" then build all apps in the source tree.
    unbundled_build_modules := $(foreach m,$(sort $(ALL_MODULES)),$(if $(filter APPS,$(ALL_MODULES.$(m).CLASS)),$(m)))
  else
    unbundled_build_modules := $(TARGET_BUILD_APPS)
  endif

...

.PHONY: apps_only
apps_only: $(unbundled_build_modules)

droid: apps_only

else # TARGET_BUILD_APPS
  $(call dist-for-goals, droidcore, \
    $(INTERNAL_UPDATE_PACKAGE_TARGET) \
    $(INTERNAL_OTA_PACKAGE_TARGET) \
    $(BUILT_OTATOOLS_PACKAGE) \
    $(SYMBOLS_ZIP) \
    $(INSTALLED_FILES_FILE) \
    $(INSTALLED_BUILD_PROP_TARGET) \
    $(BUILT_TARGET_FILES_PACKAGE) \
    $(INSTALLED_ANDROID_INFO_TXT_TARGET) \
    $(INSTALLED_RAMDISK_TARGET) \
   )
# Building a full system-- the default is to build droidcore
droid: droidcore dist_files

endif # TARGET_BUILD_APPS

我们期望的是整个系统的编译,所以,droid依赖的是droidcore 和 dist_files

4.1droidcore的定义:

# Build files and then package it into the rom formats
.PHONY: droidcore
droidcore: files \
	systemimage \
	$(INSTALLED_BOOTIMAGE_TARGET) \
	$(INSTALLED_RECOVERYIMAGE_TARGET) \
	$(INSTALLED_USERDATAIMAGE_TARGET) \
	$(INSTALLED_CACHEIMAGE_TARGET) \
	$(INSTALLED_VENDORIMAGE_TARGET) \
	$(INSTALLED_FILES_FILE)

可以droidcore又是一个伪目标,它又依赖于files 等一系列目标,从名字来看,这些目标应该是systemimage,userdataimage,recoryimage等,也就是说,droidcore的最终目的就是生成system.img,userdata.img等系统镜像文件。

看到变量的定义就明白了:

1.boot.img:

INSTALLED_BOOTIMAGE_TARGET := $(PRODUCT_OUT)/boot.img

2.recovery.img:

INSTALLED_RECOVERYIMAGE_TARGET := $(PRODUCT_OUT)/recovery.img

3.userdata.img:

INSTALLED_USERDATAIMAGE_TARGET := $(BUILT_USERDATAIMAGE_TARGET)

  --->BUILT_USERDATAIMAGE_TARGET := $(PRODUCT_OUT)/userdata.img

4.cache.img

INSTALLED_CACHEIMAGE_TARGET := $(BUILT_CACHEIMAGE_TARGET)

 --->BUILT_CACHEIMAGE_TARGET := $(PRODUCT_OUT)/cache.img

5.vendor.img

INSTALLED_VENDORIMAGE_TARGET := $(BUILT_VENDORIMAGE_TARGET)

BUILT_VENDORIMAGE_TARGET := $(PRODUCT_OUT)/vendor.img

因此,droidcore的最终目的就是生成这些.Img文件。

dist_files的定义:

# dist_files only for putting your library into the dist directory with a full build.
.PHONY: dist_files
从定义来看,dist_files也是个伪目标,并且它没有任何依赖,作用是完整编译系统的时候拷贝库文件。


4.2.files

它的第一个目标是files:

# All the droid stuff, in directories
.PHONY: files
files: prebuilt \
        $(modules_to_install) \
        $(INSTALLED_ANDROID_INFO_TXT_TARGET)、

1.1files又依赖了三个目标,第一个是prebuilt:

# -------------------------------------------------------------------
# This is used to to get the ordering right, you can also use these,
# but they're considered undocumented, so don't complain if their
# behavior changes.
.PHONY: prebuilt
prebuilt: $(ALL_PREBUILT)

prebuilt又是一个伪目标,它又依赖于ALL_PREBUILT变量指向的目标,ALL_PREBUILT是一些预编译模块:

Android.mk (makefile\frameworks\base\cmds\bmgr):ALL_PREBUILT += $(TARGET_OUT)/bin/bmgr
Android.mk (makefile\frameworks\base\cmds\ime):ALL_PREBUILT += $(TARGET_OUT)/bin/ime
Android.mk (makefile\frameworks\base\cmds\input):ALL_PREBUILT += $(TARGET_OUT)/bin/input
Android.mk (makefile\frameworks\base\cmds\pm):ALL_PREBUILT += $(TARGET_OUT)/bin/pm
Android.mk (makefile\frameworks\base\cmds\svc):ALL_PREBUILT += $(TARGET_OUT)/bin/svc


4.3modules_to_install 

modules_to_install := $(sort \
    $(ALL_DEFAULT_INSTALLED_MODULES) \
    $(product_FILES) \
    $(foreach tag,$(tags_to_install),$($(tag)_MODULES)) \
    $(CUSTOM_MODULES) \
  )

这个变量之前已经分析过,它包含所有的要安装的模块,make会为这个目标生成依赖关系链,也就是会给其中的每一个模块生成依赖关系链,然后编译每一个模块,这个过程在上一节中已经说过了。
至此,所有应该编译的模块都已经被编译,剩下的就是打包镜像文件了。这将在下一节讨论。


作者:u011913612 发表于2016/9/4 20:12:59 原文链接
阅读:65 评论:0 查看评论

Android网络编程TCP、UDP(三)——UDP实例:搜索局域网所有的设备

$
0
0

接上面的UDP,本篇主要讨论如何在局域网中搜索所有的设备,这个需求在物联网应用的比较多,也比较综合,特把名字加在标题中了。最后一块是网络编程的常见问题。

3.6 实例:在局域网内搜索设备

假设你家里安装了智能家居,所有的设备都是通过Wifi连接自己家里的局域网(至于这些设备没有界面操作,如何连接wifi?有一个比较流行的牛逼技术,叫SmartConfig)。现在这些设备连入到局域网了,那如何通过Android搜索到这些设备?

模拟主机效果图:
这里写图片描述

模拟设备效果图:
这里写图片描述

3.6.1 原理分析

这些设备在局域网内,肯定是通过DHCP(Dynamic Host Configuration Protocol,动态主机配置协议)来获取内网IP的。也就是说每个设备的IP都不是固定的。而我们主要目的就是要获取这些设备的IP地址。

也许你说,把手机设置成一个固定的内网IP,然后让这些设备来连接这个固定IP。看上去OK啊,但万一这个IP被占用了,怎办?

每个设备的IP会变,但通信端口我们肯定可以固定。这就可以运用上面的UDP广播(或组播)技术。具体流程:

  1. 主机(Android手机)发送广播信息,并指定对方接收端口为devicePort;
  2. 自己的发送端口为系统分配的hostPort,封装在DatagramSocket中,开始监听此端口。防丢失,一共发三次,每次发送后就监听一段时间;
  3. 设备监听devicePort端口,收到信息后。首先解析数据验证是否是自己人(协议)发过来的,否,扔;是,则通过数据报获取对方的IP地址与端口hostPort;
  4. 设备通过获取到的IP地址与端口hostPort,给主机发送响应信息;
  5. 主机收到设备的响应,就可以知道设备的IP地址了。同时主机返回确认信息给设备,防止设备发给主机的响应信息丢失,毕竟是UDP;
  6. 有了IP地址,就可以为所欲为了,比如:建立安全连接TCP。

本解决方法有以下特点:

  • 灵活性高。主机使用系统自动分配端口,不用担心端口被其他软件占用;
  • 搜索迅速。使用了UDP广播,所有局域网内的设备几乎同时可以接受到信息;
  • 连接安全。为了避免UDP的不安全性,使用了类似TCP的三次握手;
  • 数据安全。加入了协议,提高了数据的安全性。

下面是广播实现的代码,当然也可以用组播来实现。组播因为组播地址的原因,可以进一步加强安全性,代码中把广播的网络那块改成组播就好了。(组播参考:Android网络编程TCP、UDP(二)

3.6.2 代码实现

主机——搜索类:

import android.util.Log;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.Set;

/**
 * 设备搜索类
 * Created by zjun on 2016/9/3.
 */
public abstract class DeviceSearcher extends Thread {
    private static final String TAG = DeviceSearcher.class.getSimpleName();

    private static final int DEVICE_FIND_PORT = 9000;
    private static final int RECEIVE_TIME_OUT = 1500; // 接收超时时间
    private static final int RESPONSE_DEVICE_MAX = 200; // 响应设备的最大个数,防止UDP广播攻击

    private static final byte PACKET_TYPE_FIND_DEVICE_REQ_10 = 0x10; // 搜索请求
    private static final byte PACKET_TYPE_FIND_DEVICE_RSP_11 = 0x11; // 搜索响应
    private static final byte PACKET_TYPE_FIND_DEVICE_CHK_12 = 0x12; // 搜索确认

    private static final byte PACKET_DATA_TYPE_DEVICE_NAME_20 = 0x20;
    private static final byte PACKET_DATA_TYPE_DEVICE_ROOM_21 = 0x21;

    private DatagramSocket hostSocket;
    private Set<DeviceBean> mDeviceSet;

    private byte mPackType;
    private String mDeviceIP;

    DeviceSearcher() {
        mDeviceSet = new HashSet<>();
    }

    @Override
    public void run() {
        try {
            onSearchStart();
            hostSocket = new DatagramSocket();
            // 设置接收超时时间
            hostSocket.setSoTimeout(RECEIVE_TIME_OUT);

            byte[] sendData = new byte[1024];
            InetAddress broadIP = InetAddress.getByName("255.255.255.255");
            DatagramPacket sendPack = new DatagramPacket(sendData, sendData.length, broadIP, DEVICE_FIND_PORT);

            for (int i = 0; i < 3; i++) {
                // 发送搜索广播
                mPackType = PACKET_TYPE_FIND_DEVICE_REQ_10;
                sendPack.setData(packData(i + 1));
                hostSocket.send(sendPack);

                // 监听来信
                byte[] receData = new byte[1024];
                DatagramPacket recePack = new DatagramPacket(receData, receData.length);
                try {
                    // 最多接收200个,或超时跳出循环
                    int rspCount = RESPONSE_DEVICE_MAX;
                    while (rspCount-- > 0) {
                        recePack.setData(receData);
                        hostSocket.receive(recePack);
                        if (recePack.getLength() > 0) {
                            mDeviceIP = recePack.getAddress().getHostAddress();
                            if (parsePack(recePack)) {
                                Log.i(TAG, "@@@zjun: 设备上线:" + mDeviceIP);
                                // 发送一对一的确认信息。使用接收报,因为接收报中有对方的实际IP,发送报时广播IP
                                mPackType = PACKET_TYPE_FIND_DEVICE_CHK_12;
                                recePack.setData(packData(rspCount)); // 注意:设置数据的同时,把recePack.getLength()也改变了
                                hostSocket.send(recePack);
                            }
                        }
                    }
                } catch (SocketTimeoutException e) {
                }
                Log.i(TAG, "@@@zjun: 结束搜索" + i);
            }
            onSearchFinish(mDeviceSet);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (hostSocket != null) {
                hostSocket.close();
            }
        }

    }

    /**
     * 搜索开始时执行
     */
    public abstract void onSearchStart();

    /**
     * 搜索结束后执行
     * @param deviceSet 搜索到的设备集合
     */
    public abstract void onSearchFinish(Set deviceSet);

    /**
     * 解析报文
     * 协议:$ + packType(1) + data(n)
     *  data: 由n组数据,每组的组成结构type(1) + length(4) + data(length)
     *  type类型中包含name、room类型,但name必须在最前面
     */
    private boolean parsePack(DatagramPacket pack) {
        if (pack == null || pack.getAddress() == null) {
            return false;
        }

        String ip = pack.getAddress().getHostAddress();
        int port = pack.getPort();
        for (DeviceBean d : mDeviceSet) {
            if (d.getIp().equals(ip)) {
                return false;
            }
        }
        int dataLen = pack.getLength();
        int offset = 0;
        byte packType;
        byte type;
        int len;
        DeviceBean device = null;

        if (dataLen < 2) {
            return false;
        }
        byte[] data = new byte[dataLen];
        System.arraycopy(pack.getData(), pack.getOffset(), data, 0, dataLen);

        if (data[offset++] != '$') {
            return false;
        }

        packType = data[offset++];
        if (packType != PACKET_TYPE_FIND_DEVICE_RSP_11) {
            return false;
        }

        while (offset + 5 < dataLen) {
            type = data[offset++];
            len = data[offset++] & 0xFF;
            len |= (data[offset++] << 8);
            len |= (data[offset++] << 16);
            len |= (data[offset++] << 24);

            if (offset + len > dataLen) {
                break;
            }
            switch (type) {
                case PACKET_DATA_TYPE_DEVICE_NAME_20:
                    String name = new String(data, offset, len, Charset.forName("UTF-8"));
                    device = new DeviceBean();
                    device.setName(name);
                    device.setIp(ip);
                    device.setPort(port);
                    break;
                case PACKET_DATA_TYPE_DEVICE_ROOM_21:
                    String room = new String(data, offset, len, Charset.forName("UTF-8"));
                    if (device != null) {
                        device.setRoom(room);
                    }
                    break;
                default: break;
            }
            offset += len;
        }
        if (device != null) {
            mDeviceSet.add(device);
            return true;
        }
        return false;
    }

    /**
     * 打包搜索报文
     * 协议:$ + packType(1) + sendSeq(4) + [deviceIP(n<=15)]
     *  packType - 报文类型
     *  sendSeq - 发送序列
     *  deviceIP - 设备IP,仅确认时携带
     */
    private byte[] packData(int seq) {
        byte[] data = new byte[1024];
        int offset = 0;

        data[offset++] = '$';

        data[offset++] = mPackType;

        seq = seq == 3 ? 1 : ++seq; // can't use findSeq++
        data[offset++] = (byte) seq;
        data[offset++] = (byte) (seq >> 8 );
        data[offset++] = (byte) (seq >> 16);
        data[offset++] = (byte) (seq >> 24);

        if (mPackType == PACKET_TYPE_FIND_DEVICE_CHK_12) {
            byte[] ips = mDeviceIP.getBytes(Charset.forName("UTF-8"));
            System.arraycopy(ips, 0, data, offset, ips.length);
            offset += ips.length;
        }

        byte[] result = new byte[offset];
        System.arraycopy(data, 0, result, 0, offset);
        return result;
    }


    /**
     * 设备Bean
     * 只要IP一样,则认为是同一个设备
     */
    public static class DeviceBean{
        String ip;      // IP地址
        int port;       // 端口
        String name;    // 设备名称
        String room;    // 设备所在房间

        @Override
        public int hashCode() {
            return ip.hashCode();
        }

        @Override
        public boolean equals(Object o) {
            if (o instanceof DeviceBean) {
                return this.ip.equals(((DeviceBean)o).getIp());
            }
            return super.equals(o);
        }

        public String getIp() {
            return ip;
        }

        public void setIp(String ip) {
            this.ip = ip;
        }

        public int getPort() {
            return port;
        }

        public void setPort(int port) {
            this.port = port;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public String getRoom() {
            return room;
        }

        public void setRoom(String room) {
            this.room = room;
        }
    }
}

主机——demo核心代码:

private List<DeviceSearcher.DeviceBean> mDeviceList;
private void searchDevices_broadcast() {
    new DeviceSearcher() {
        @Override
        public void onSearchStart() {
            startSearch(); // 主要用于在UI上展示正在搜索
        }

        @Override
        public void onSearchFinish(Set deviceSet) {
            endSearch(); // 结束UI上的正在搜索
            mDeviceList.clear();
            mDeviceList.addAll(deviceSet);
            mHandler.sendEmptyMessage(0); // 在UI上更新设备列表
        }
    }.start();
}

设备——设备等待搜索类:

import android.content.Context;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.util.Log;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;

/**
 * 设备等待搜索类
 * Created by zjun on 2016/9/4.
 */
public abstract class DeviceWaitingSearch extends Thread {
    private final String TAG = DeviceWaitingSearch.class.getSimpleName();

    private static final int DEVICE_FIND_PORT = 9000;
    private static final int RECEIVE_TIME_OUT = 1500; // 接收超时时间,应小于等于主机的超时时间1500
    private static final int RESPONSE_DEVICE_MAX = 200; // 响应设备的最大个数,防止UDP广播攻击

    private static final byte PACKET_TYPE_FIND_DEVICE_REQ_10 = 0x10; // 搜索请求
    private static final byte PACKET_TYPE_FIND_DEVICE_RSP_11 = 0x11; // 搜索响应
    private static final byte PACKET_TYPE_FIND_DEVICE_CHK_12 = 0x12; // 搜索确认

    private static final byte PACKET_DATA_TYPE_DEVICE_NAME_20 = 0x20;
    private static final byte PACKET_DATA_TYPE_DEVICE_ROOM_21 = 0x21;

    private Context mContext;
    private String deviceName, deviceRoom;

    public DeviceWaitingSearch(Context context, String name, String room) {
        mContext = context;
        deviceName = name;
        deviceRoom = room;
    }

    @Override
    public void run() {
        DatagramSocket socket = null;
        try {
            socket = new DatagramSocket(DEVICE_FIND_PORT);
            byte[] data = new byte[1024];
            DatagramPacket pack = new DatagramPacket(data, data.length);
            while (true) {
                // 等待主机的搜索
                socket.receive(pack);
                if (verifySearchData(pack)) {
                    byte[] sendData = packData();
                    DatagramPacket sendPack = new DatagramPacket(sendData, sendData.length, pack.getAddress(), pack.getPort());
                    Log.i(TAG, "@@@zjun: 给主机回复信息");
                    socket.send(sendPack);
                    Log.i(TAG, "@@@zjun: 等待主机接收确认");
                    socket.setSoTimeout(RECEIVE_TIME_OUT);
                    try {
                        socket.receive(pack);
                        if (verifyCheckData(pack)) {
                            Log.i(TAG, "@@@zjun: 确认成功");
                            onDeviceSearched((InetSocketAddress) pack.getSocketAddress());
                            break;
                        }
                    } catch (SocketTimeoutException e) {
                    }
                    socket.setSoTimeout(0); // 连接超时还原成无穷大,阻塞式接收
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (socket != null) {
                socket.close();
            }
        }
    }

    /**
     * 当设备被发现时执行
     */
    public abstract void onDeviceSearched(InetSocketAddress socketAddr);

    /**
     * 打包响应报文
     * 协议:$ + packType(1) + data(n)
     *  data: 由n组数据,每组的组成结构type(1) + length(4) + data(length)
     *  type类型中包含name、room类型,但name必须在最前面
     */
    private byte[] packData() {
        byte[] data = new byte[1024];
        int offset = 0;
        data[offset++] = '$';
        data[offset++] = PACKET_TYPE_FIND_DEVICE_RSP_11;

        byte[] temp = getBytesFromType(PACKET_DATA_TYPE_DEVICE_NAME_20, deviceName);
        System.arraycopy(temp, 0, data, offset, temp.length);
        offset += temp.length;

        temp = getBytesFromType(PACKET_DATA_TYPE_DEVICE_ROOM_21, deviceRoom);
        System.arraycopy(temp, 0, data, offset, temp.length);
        offset += temp.length;

        byte[] retVal = new byte[offset];
        System.arraycopy(data, 0, retVal, 0, offset);

        return retVal;
    }

    private byte[] getBytesFromType(byte type, String val) {
        byte[] retVal = new byte[0];
        if (val != null) {
            byte[] valBytes = val.getBytes(Charset.forName("UTF-8"));
            retVal = new byte[5 + valBytes.length];
            retVal[0] = type;
            retVal[1] = (byte) valBytes.length;
            retVal[2] = (byte) (valBytes.length >> 8 );
            retVal[3] = (byte) (valBytes.length >> 16);
            retVal[4] = (byte) (valBytes.length >> 24);
            System.arraycopy(valBytes, 0, retVal, 5, valBytes.length);
        }
        return retVal;
    }

    /**
     * 校验搜索数据
     * 协议:$ + packType(1) + sendSeq(4)
     *  packType - 报文类型
     *  sendSeq - 发送序列
     */
    private boolean verifySearchData(DatagramPacket pack) {
        if (pack.getLength() != 6) {
            return false;
        }

        byte[] data = pack.getData();
        int offset = pack.getOffset();
        int sendSeq;
        if (data[offset++] != '$' || data[offset++] != PACKET_TYPE_FIND_DEVICE_REQ_10) {
            return false;
        }
        sendSeq = data[offset++] & 0xFF;
        sendSeq |= (data[offset++] << 8 );
        sendSeq |= (data[offset++] << 16);
        sendSeq |= (data[offset++] << 24);
        return sendSeq >= 1 && sendSeq <= 3;
    }

    /**
     * 校验确认数据
     * 协议:$ + packType(1) + sendSeq(4) + deviceIP(n<=15)
     *  packType - 报文类型
     *  sendSeq - 发送序列
     *  deviceIP - 设备IP,仅确认时携带
     */
    private boolean verifyCheckData(DatagramPacket pack) {
        if (pack.getLength() < 6) {
            return false;
        }

        byte[] data = pack.getData();
        int offset = pack.getOffset();
        int sendSeq;
        if (data[offset++] != '$' || data[offset++] != PACKET_TYPE_FIND_DEVICE_CHK_12) {
            return false;
        }
        sendSeq = data[offset++] & 0xFF;
        sendSeq |= (data[offset++] << 8 );
        sendSeq |= (data[offset++] << 16);
        sendSeq |= (data[offset++] << 24);
        if (sendSeq < 1 || sendSeq > RESPONSE_DEVICE_MAX) {
            return false;
        }

        String ip = new String(data, offset, pack.getLength() - offset, Charset.forName("UTF-8"));
        Log.i(TAG, "@@@zjun: ip from host=" + ip);
        return ip.equals(getOwnWifiIP());
    }

    /**
     * 获取本机在Wifi中的IP
     */
    private String getOwnWifiIP() {
        WifiManager wm = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
        if (!wm.isWifiEnabled()) {
            return "";
        }

        // 需加权限:android.permission.ACCESS_WIFI_STATE
        WifiInfo wifiInfo = wm.getConnectionInfo();
        int ipInt = wifiInfo.getIpAddress();
        String ipAddr = int2Ip(ipInt);
        Log.i(TAG, "@@@zjun: 本机IP=" + ipAddr);
        return int2Ip(ipInt);
    }

    /**
     * 把int表示的ip转换成字符串ip
     */
    private String int2Ip(int i) {
        return String.format("%d.%d.%d.%d", i & 0xFF, (i >> 8) & 0xFF, (i >> 16) & 0xFF, (i >> 24) & 0xFF);
    }
}

设备——demo核心代码:

private void initData() {
    new DeviceWaitingSearch(this, "日灯光", "客厅"){
        @Override
        public void onDeviceSearched(InetSocketAddress socketAddr) {
            pushMsgToMain("已上线,搜索主机:" + socketAddr.getAddress().getHostAddress() + ":" + socketAddr.getPort());
        }
    }.start();
}

四、常见问题

4.1 局域网内无法通信

因为用了电脑作为测试设备,包括Java工程和Android模拟器,之前就知道Java工程中要网络通信要关防火墙,但使用的时候,发现Android模拟器、C工程、和Socket网络工具都可以通信,就Java工程不行。

尝试了很多方法找原因,如在命令行执行下面的命令,然而无终而返:

  1. 查看局域网中其他运行的电脑:net view
  2. 路由追踪:tracert (ip)
    eg:tracert baidu.com
    tracert 192.168.1.10
  3. 显示当前TCP/IP网络连接:netstat

最后终于找到解决办法,在“防火墙”的“允许的应用”中需要设置权限。把“Java(TM) Platform SE binary”勾上,并把后面的“专用”和“公用”网络都勾选上:
这里写图片描述

4.2 局域网内只有有线连接的设备可以通信,无线设备却无法通信

其实问题详细情况是这样的:无线Wifi连接的设备不能与无线设备通信(内网IP通信),只能与有线设备通信;而有线设备一切正常。

这问题也很郁闷,查了资料也没有找到解决办法。但个人感觉这问题肯定是路由器的问题,因为局域网的控制系统就是路由器。幸运的是,我有两个一模一样的路由器,另一个路由器应该的。然后两台路由器,分别连两台电脑,通过电脑对路由器界面进行对比(英文是硬伤啊⊙﹏⊙b)。

最后锁定了这个东西“Wireless Isolation within SSID”,就是连接SSID的设备都独立,无法进行局域网内通信。曾经手滑了一下,点成Enable。改回Disabled,兄弟间就别分开了:
这里写图片描述


本来一篇想搞定的,结果写了三篇,目录还是按原来一篇的来写,有点儿乱(^__^) ……

网络编程,终于赶在这个周末结束前告一段落。

作者:a10615 发表于2016/9/4 22:20:26 原文链接
阅读:44 评论:0 查看评论

BottomSheet底部动作条使用

$
0
0

底部动作条

底部动作条(Bottom Sheets)是一个从屏幕底部边缘向上滑出的一个面板,使用这种方式向用户呈现一组功能。底部动作条呈现了简单、清晰、无需额外解释的一组操作。

使用环境

底部动作条(Bottom Sheets)特别适合有三个或者三个以上的操作需要提供给用户选择、并且不需要对操作有额外解释的情景。如果只有两个或者更少的操作,或者需要详加描述的,可以考虑使用菜单(Menu)或者对话框替代。

底部动作条(Bottom Sheets)可以是列表样式的也可以是宫格样式的。宫格布局可以增加视觉的清晰度。

你可以使用底部动作条(Bottom Sheets)展示和其 app 相关的操作,比如做为进入其他 app 的入口(通过 app 的 icon 进入)。

我们来看看官方展示的效果:


行为

显示底部动作条的时候,动画应该从屏幕底部边缘向上展开。根据上一步的内容,向用户展示用户上一步的操作之后能够继续操作的内容,并提供模态[1]的选择。点击其他区域会使得底部动作条伴随下滑的动画关闭掉。如果这个窗口包含的操作超出了默认的显示区域,这个窗口需要可以滑动。滑动操作应当向上拉起这个动作条的内容,甚至可以覆盖整个屏幕。当窗口覆盖整个屏幕的时候,需要在上部的标题栏左侧增加一个收起按钮。

添加依赖:

compile 'com.android.support:design:24.2.0'

BottomSheet使用例子:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:layout_width="match_parent"
        android:layout_height="80dp"
        android:onClick="click"
        android:text="BottomSheet" />

    <android.support.v4.widget.NestedScrollView
        android:id="@+id/bottom_sheet"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_behavior="@string/bottom_sheet_behavior">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:gravity="center"
            android:orientation="vertical">

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:minHeight="50dp"
                android:gravity="center_vertical"
                android:drawableLeft="@mipmap/ic_launcher"
                android:text="BottomSheet布局" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:minHeight="50dp"
                android:drawableLeft="@mipmap/ic_launcher"
                android:text="BottomSheet布局" />
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:minHeight="50dp"
                android:drawableLeft="@mipmap/ic_launcher"
                android:text="BottomSheet布局" />
            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:minHeight="50dp"
                android:drawableLeft="@mipmap/ic_launcher"
                android:text="BottomSheet布局" />
        </LinearLayout>

    </android.support.v4.widget.NestedScrollView>
</android.support.design.widget.CoordinatorLayout>
页面代码:

public class MainActivity extends AppCompatActivity {

    private BottomSheetBehavior<View> bottomSheet;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

        init();
    }

    private void init() {
        bottomSheet.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
            @Override
            public void onStateChanged(@NonNull View bottomSheet, int newState) {
                //newState 有四个状态 :
                //展开 BottomSheetBehavior.STATE_EXPANDED
                //收起 BottomSheetBehavior.STATE_COLLAPSED
                //拖动 BottomSheetBehavior.STATE_DRAGGING
                //下滑 BottomSheetBehavior.STATE_SETTLING
            }

            @Override
            public void onSlide(@NonNull View bottomSheet, float slideOffset) {
            //这里是拖拽中的回调,slideOffset为0-1 完全收起为0 完全展开为1
            }
        });
    }
}
当然BottomSheet这种效果是高度可扩展的,你可以在布局中实现你想要的任何效果。

BottomSheetDialog

BottomSheetDialog的使用也很简单,直接上代码:
public class BottomSheetDialogActivity extends AppCompatActivity{
    private List<String> mList;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bottomsheet);
        initData();
    }

    private void initData() {
        mList = new ArrayList<>();
        for(int i=0; i<20; i++){
            mList.add("item "+i);
        }
    }

    public void click1(View view){
        final BottomSheetDialog bottomSheetDialog = new BottomSheetDialog(this);
        //创建recyclerView
        RecyclerView recyclerView = new RecyclerView(this);
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(linearLayoutManager);
        RecyclerAdapter recyclerAdapter = new RecyclerAdapter(mList,this);
        recyclerView.setAdapter(recyclerAdapter);
        recyclerAdapter.setOnItemClickListener(new RecyclerAdapter.OnItemClickListener() {
            @Override
            public void onItemClickListener(View item, int position) {
                Toast.makeText(BottomSheetDialogActivity.this, "item "+position, Toast.LENGTH_SHORT).show();
                bottomSheetDialog.dismiss();
            }
        });

        bottomSheetDialog.setContentView(recyclerView);
        bottomSheetDialog.show();
    }
}
adapter
public class RecyclerAdapter  extends RecyclerView.Adapter<RecyclerAdapter.ViewHolder>{

    private List<String> list;
    private Context mContext;
    private OnItemClickListener onItemClickListener;

    public RecyclerAdapter(List<String> list, Context mContext) {
        this.list = list;
        this.mContext = mContext;
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View inflate = LayoutInflater.from(mContext).inflate(R.layout.item_layou, parent, false);
        return new ViewHolder(inflate);
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, final int position) {
        holder.tv.setText(list.get(position));
        holder.tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(onItemClickListener!=null){
                    onItemClickListener.onItemClickListener(v,position);
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return list.size();
    }

    public static class ViewHolder extends RecyclerView.ViewHolder{
        TextView tv;
        public ViewHolder(View itemView) {
            super(itemView);
            tv = (TextView) itemView.findViewById(R.id.item_tv);
        }
    }

    public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
        this.onItemClickListener = onItemClickListener;
    }

    public interface OnItemClickListener{
        void onItemClickListener(View item, int position);
    }
}
item布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:orientation="vertical">

    <TextView
        android:id="@+id/item_tv"
        android:layout_width="match_parent"
        android:layout_height="50dp" />
</LinearLayout>

其它可以实现的效果还有很多,大家可以根据具体情况自行修改。







作者:xiangzhihong8 发表于2016/9/4 22:21:26 原文链接
阅读:60 评论:0 查看评论

Android中的指纹识别

$
0
0

转载请注明出处:http://blog.csdn.net/wl9739/article/details/52444671

最近项目需要使用到指纹识别的功能,查阅了相关资料后,整理成此文。

指纹识别是在Android 6.0之后新增的功能,因此在使用的时候需要先判断用户手机的系统版本是否支持指纹识别。另外,实际开发场景中,使用指纹的主要场景有两种:

  • 纯本地使用。即用户在本地完成指纹识别后,不需要将指纹的相关信息给后台。
  • 与后台交互。用户在本地完成指纹识别后,需要将指纹相关的信息传给后台。

由于使用指纹识别功能需要一个加密对象(CryptoObject)该对象一般是由对称加密或者非对称加密获得。上述两种开发场景的实现大同小异,主要区别在于加密过程中密钥的创建和使用,一般来说,纯本地的使用指纹识别功能,只需要对称加密即可;而与后台交互则需要使用非对称加密:将私钥用于本地指纹识别,识别成功后将加密信息传给后台,后台开发人员用公钥解密,以获得用户信息。

下面先简单介绍一下对称加密和非对称加密的相关概念,然后对两种开发方式的实现分别进行讲解。

对称加密、非对称加密和签名

在正式使用指纹识别功能之前,有必要先了解一下对称加密和非对称加密的相关内容。

  • 对称加密:所谓对称,就是采用这种加密方法的双方使用方式用同样的密钥进行加密和解密。密钥是控制加密及解密过程的指令。算法是一组规则,规定如何进行加密和解密。因此加密的安全性不仅取决于加密算法本身,密钥管理的安全性更是重要。因为加密和解密都使用同一个密钥,如何把密钥安全地传递到解密者手上就成了必须要解决的问题。

  • 非对称加密:非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。 非对称加密算法实现机密信息交换的基本过程是:甲方生成一对密钥并将其中的一把作为公用密钥向其它方公开;得到该公用密钥的乙方使用该密钥对机密信息进行加密后再发送给甲方;甲方再用自己保存的另一把专用密钥对加密后的信息进行解密。

  • 签名:在信息的后面再加上一段内容,可以证明信息没有被修改过。一般是对信息做一个hash计算得到一个hash值,注意,这个过程是不可逆的,也就是说无法通过hash值得出原来的信息内容。在把信息发送出去时,把这个hash值加密后做为一个签名和信息一起发出去。

由以上内容可以了解到,对称加密和非对称加密的特点如下:

  • 对称加密的优点是速度快,适合于本地数据和本地数据库的加密,安全性不如非对称加密。常见的对称加密算法有DES、3DES、AES、Blowfish、IDEA、RC5、RC6。
  • 非对称加密的安全性比较高,适合对需要网络传输的数据进行加密,速度不如对称加密。非对称加密应用于SSH, HTTPS, TLS,电子证书,电子签名,电子身份证等等

指纹识别的对称加密实现

使用指纹识别的对称加密功能的主要流程如下:

  1. 使用 KeyGenerator 创建一个对称密钥,存放在 KeyStore 里。
  2. 设置 KeyGenParameterSpec.Builder.setUserAuthenticationRequired() 为true,
  3. 使用创建好的对称密钥初始化一个Cipher对象,并用该对象调用 FingerprintManager.authenticate() 方法启动指纹传感器并开始监听。
  4. 重写 FingerprintManager.AuthenticationCallback 的几个回调方法,以处理指纹识别成功(onAuthenticationSucceeded())、失败(onAuthenticationFailed()onAuthenticationError())等情况。

创建密钥

创建密钥要涉及到两个类:KeyStore 和 KeyGenerator。

KeyStore 是用于存储、获取密钥(Key)的容器,获取 KeyStore的方法如下:

try {
    mKeyStore = KeyStore.getInstance("AndroidKeyStore");
} catch (KeyStoreException e) {
    throw new RuntimeException("Failed to get an instance of KeyStore", e);
}

而生成 Key,如果是对称加密,就需要 KeyGenerator 类。获取一个 KeyGenerator 对象比较简单,方法如下:

// 对称加密, 创建 KeyGenerator 对象
try {
    mKeyGenerator = KeyGenerator
            .getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
    throw new RuntimeException("Failed to get an instance of KeyGenerator", e);
}

获得 KeyGenerator 对象后,就可以生成一个 Key 了:

 try {
    keyStore.load(null);
    KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(defaultKeyName,
            KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setUserAuthenticationRequired(true)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        builder.setInvalidatedByBiometricEnrollment(true);
    }
    keyGenerator.init(builder.build());
    keyGenerator.generateKey();
} catch (CertificateException | NoSuchAlgorithmException | IOException | InvalidAlgorithmParameterException e) {
    e.printStackTrace();
}

创建并初始化 Cipher 对象

Cipher 对象是一个按照一定的加密规则,将数据进行加密后的一个对象。调用指纹识别功能需要使用到这个对象。创建 Cipher 对象很简单,如同下面代码那样:

Cipher defaultCipher;
try {
    defaultCipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
            + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
    throw new RuntimeException("创建Cipher对象失败", e);
}

然后使用刚才创建好的密钥,初始化 Cipher 对象:

 try {
    keyStore.load(null);
    SecretKey key = (SecretKey) keyStore.getKey(keyName, null);
    cipher.init(Cipher.ENCRYPT_MODE, key);
    return true;
} catch (IOException | NoSuchAlgorithmException | CertificateException | UnrecoverableKeyException | KeyStoreException | InvalidKeyException e) {
    throw new RuntimeException("初始化 cipher 失败", e);
}

使用指纹识别功能

真正到了使用指纹识别功能的时候,你会发现其实很简单,只是调用 FingerprintManager 类的的方法authenticate()而已,然后系统会有相应的回调反馈给我们,该方法如下:

public void authenticate(CryptoObject crypto, CancellationSignal cancel, int flags, AuthenticationCallback callback, Handler handler) 

该方法的几个参数解释如下:

  • 第一个参数是一个加密对象。还记得之前我们大费周章地创建和初始化的Cipher对象吗?这里的 CryptoObject 对象就是使用 Cipher 对象创建创建出来的:new FingerprintManager.CryptoObject(cipher)
  • 第二个参数是一个 CancellationSignal 对象,该对象提供了取消操作的能力。创建该对象也很简单,使用 new CancellationSignal() 就可以了。
  • 第三个参数是一个标志,默认为0。
  • 第四个参数是 AuthenticationCallback 对象,它本身是 FingerprintManager 类里面的一个抽象类。该类提供了指纹识别的几个回调方法,包括指纹识别成功、失败等。需要我们重写。
  • 最后一个 Handler,可以用于处理回调事件,可以传null。

完成指纹识别后,还要记得将 AuthenticationCallback 关闭掉:

public void stopListening() {
    if (cancellationSignal != null) {
        selfCancelled = true;
        cancellationSignal.cancel();
        cancellationSignal = null;
    }
}

重写回调方法

调用了 authenticate() 方法后,系统就会启动指纹传感器,并开始扫描。这时候根据扫描结果,会通过FingerprintManager.AuthenticationCallback类返回几个回调方法:

// 成功
onAuthenticationSucceeded()
// 失败
onAuthenticationFaile()
// 错误
onAuthenticationError()

一般我们需要重写这几个方法,以实现我们的功能。关于onAuthenticationFaile()onAuthenticationError()的区别,后面会讲到。

指纹识别的非对称加密实现

其实流程和上面的流程差不多:

  1. 使用 KeyPairGenerator 创建一个非对称密钥。
  2. 使用创建好的私钥进行签名,使用该签名创建一个加密对象,并将该对象作为 FingerprintManager.authenticate() 方法的一个参数,启动指纹传感器并开始监听。
  3. 重写 FingerprintManager.AuthenticationCallback 类的几个回调方法,以处理指纹识别成功(onAuthenticationSucceeded())、失败(onAuthenticationFailed()onAuthenticationError())等情况。

可以看见,指纹识别的非对称加密方式和对称加密方式的实现流程是差不多的,它们之间最明显的差别是在于密钥的生成与使用。

创建密钥

这里要使用 KeyPairGenerator 来创建一组非对称密钥,首先是获取 KeyPairGenerator 对象:

// 非对称加密,创建 KeyPairGenerator 对象
try {
    mKeyPairGenerator =  KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore");
} catch (NoSuchAlgorithmException | NoSuchProviderException e) {
    throw new RuntimeException("Failed to get an instance of KeyPairGenerator", e);
}

得到了 KeyPairGenerator 对象后,就可以创建 KeyPair(密钥对)了:

try {
    // Set the alias of the entry in Android KeyStore where the key will appear
    // and the constrains (purposes) in the constructor of the Builder
    mKeyPairGenerator.initialize(
            new KeyGenParameterSpec.Builder(KEY_NAME,
                    KeyProperties.PURPOSE_SIGN)
                    .setDigests(KeyProperties.DIGEST_SHA256)
                    .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1"))
                    // Require the user to authenticate with a fingerprint to authorize
                    // every use of the private key
                    .setUserAuthenticationRequired(true)
                    .build());
    mKeyPairGenerator.generateKeyPair();
} catch (InvalidAlgorithmParameterException e) {
    throw new RuntimeException(e);
}

签名

指纹识别的对称加密实现中使用了Cipher对象来创建CryptoObject对象,而在这里,我们将会使用私钥进行签名,用签名对象来创建CryptoObject对象:

// 使用私钥签名
try {
    mKeyStore.load(null);
    PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_NAME, null);
    mSignature.initSign(key);
    return true;
} catch (KeyPermanentlyInvalidatedException e) {
    return false;
} catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException
        | NoSuchAlgorithmException | InvalidKeyException e) {
    throw new RuntimeException("Failed to init Cipher", e);
}

同样的,调用new FingerprintManager.CryptoObject(mSignature)方法创建一个CryptoObject对象。

调用指纹识别方法

这里的使用方法和前面“指纹识别的对称加密实现”中的调用方法是一样的,都是调用FingerprintManager.authenticate()方法。这里就不再叙述。

监听回调

监听回调也和之前的类似,唯一不同的是,我们在识别成功后需要和后台进行交互,也就是onAuthenticationSucceeded()中处理的逻辑不一样。

实际应用中的注意事项

判断用户是否可以使用指纹识别功能

一般来说,为了增加安全性,要求用户在手机的“设置”中开启了密码锁屏功能。当然,使用指纹解锁的前提是至少录入了一个指纹。

// 如果没有设置密码锁屏,则不能使用指纹识别
if (!keyguardManager.isKeyguardSecure()) {
    Toast.makeText(this, "请在设置界面开启密码锁屏功能",
            Toast.LENGTH_LONG).show();
}
// 如果没有录入指纹,则不能使用指纹识别
if (!fingerprintManager.hasEnrolledFingerprints()) {
    Toast.makeText(this, "您还没有录入指纹, 请在设置界面录入至少一个指纹",
            Toast.LENGTH_LONG).show();
}

这里用到了两个类:KeyguardManagerFingerprintManager,前者是屏幕保护的相关类。后者是指纹识别的核心类。

关于指纹识别回调方法

前面说到AuthenticationCallback类里面的几个回调方法,其中有三个是我们开发中需要用到的:

onAuthenticationError()
onAuthenticationSucceeded()
onAuthenticationFailed()

关于这三个回调方法,有几点需要注意的:

  1. 当指纹识别失败后,会调用onAuthenticationFailed()方法,这时候指纹传感器并没有关闭,系统给我们提供了5次重试机会,也就是说,连续调用了5次onAuthenticationFailed()方法后,会调用onAuthenticationError()方法。

  2. 当系统调用了onAuthenticationError()onAuthenticationSucceeded()后,传感器会关闭,只有我们重新授权,再次调用authenticate()方法后才能继续使用指纹识别功能。

  3. 当系统回调了onAuthenticationError()方法关闭传感器后,这种情况下再次调用authenticate()会有一段时间的禁用期,也就是说这段时间里是无法再次使用指纹识别的。当然,具体的禁用时间由手机厂商的系统不同而有略微差别,有的是1分钟,有的是30秒等等。而且,由于手机厂商的系统区别,有些系统上调用了onAuthenticationError()后,在禁用时间内,其他APP里面的指纹识别功能也无法使用,甚至系统的指纹解锁功能也无法使用。而有的系统上,在禁用时间内调用其他APP的指纹解锁功能,或者系统的指纹解锁功能,就能立即重置指纹识别功能。

示例代码

最后, Android Sample 里面关于指纹的示例代码地址如下:

对称加密方式:android-FingerprintDialog

非对称加密方式:android-AsymmetricFingerprintDialog

参考链接:New in Android Samples: Authenticating to remote servers using the Fingerprint API

作者:wl9739 发表于2016/9/5 23:01:55 原文链接
阅读:69 评论:0 查看评论

RxJava之辅助操作符

$
0
0

Observable的创建源于数据源,如何从Observable转换回数据源呢?观察者订阅后,如何推迟Observable发射数据呢?观察者订阅后,在Observable调用观察者的方法前,做一些其他的事情又该如何做呢?…带着思考的问题,让我们看下辅助操作符带给我们的无限遐想!

delay

流程图

这里写图片描述

概述

delay操作符让原Observable在发射每项数据之前都暂停一段指定的时间段,其效果是Observable发射的数据项在时间上向前整体平移了一个增量。

在RxJava中,通过delay()和delaySubscription()实现delay.

在RxJava中,delay()操作符有很多变体,下面一一介绍其变体:
这里写图片描述
delay(long,TimeUnit)和delay(long,TimeUnit, scheduler)变体接受一个定义时长的参数(包括时长和时间单位)。每当原Observable发射一项数据,delay就启动一个定时器,当定时器过了给定的时间段时,delay创建并返回一个Observable,其与原Observable发射相同的数据项。

delay(long,TimeUnit)和delay(long,TimeUnit, scheduler)默认在computation调度器上执行,可以通过参数指定使用其它的调度器。
这里写图片描述
delay(Func1))变体不采用时间参数,而是接受一个函数参数,针对原Observable发射的数据,创建并返回一个Observable,暂且命名为_Observable,同时订阅这个_Observable。当任何一个_Observable终止时,delay()返回的_Observable就发射与其关联的那项数据。该变体默认不在任何特定的调度器上执行。
这里写图片描述
delay(Func0,Func1)变体接收两个函数参数,Func0函数对每一项数据使用一个Observable作为原始Observable的延时定时器。

API

Javadoc: delay(Func1)
Javadoc: delay(Func0,Func1)
Javadoc: delay(long,TimeUnit)
Javadoc: delay(long,TimeUnit, scheduler)

示例代码

1.delay(long,TimeUnit)

Observable.create(new Observable.OnSubscribe<Student>() {
    @Override
    public void call(Subscriber<? super Student> subscriber) {
        Log.i(TAG, "call:" + String.valueOf(SystemClock.uptimeMillis()));
        subscriber.onNext(getListOfStudent().get(0));
        Log.i(TAG, "call:" + String.valueOf(SystemClock.uptimeMillis()));
        subscriber.onNext(getListOfStudent().get(1));
        Log.i(TAG, "call:" + String.valueOf(SystemClock.uptimeMillis()));
        subscriber.onNext(getListOfStudent().get(2));
    }
}).delay(1, TimeUnit.SECONDS)
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<Student>() {

            @Override
            public void onStart() {
                super.onStart();
                mAdaStudent.clear();

                Log.i(TAG, "onStart:" + String.valueOf(SystemClock.uptimeMillis()));
            }

            @Override
            public void onCompleted() {
                Log.i(TAG, "do onCompleted");
            }

            @Override
            public void onError(Throwable e) {
                Log.i(TAG, "do onError");
            }

            @Override
            public void onNext(Student student) {
                Log.i(TAG, "do onNext");
                Log.i(TAG, "onNext:" + String.valueOf(SystemClock.uptimeMillis()));
                mAdaStudent.addData(student);
            }
        });

2.

  ****
 .delay(new Func1<Student, Observable<Student>>() {
        @Override
        public Observable<Student> call(Student student) {
            return Observable.timer(2, TimeUnit.SECONDS)
                    .flatMap(new Func1<Long, Observable<Student>>() {
                        @Override
                        public Observable<Student> call(Long aLong) {
                            return Observable.just(student);
                        }
                    });
        }
    })

    ***

Log打印

1.delay(long,TimeUnit)
onStart:680479501
call:680479502
call:680479502
call:680479503
do onNext
onNext:680480503
do onNext
onNext:680480505
do onNext
onNext:680480506
2.delay(Func1)
onStart:681525874
call:680479512
call:681525914
call:681525916
do onNext
onNext:681527964
do onNext
onNext:681527966
do onNext
onNext:681527966

示例解析

1.通过delay(1,TimeUnit.Seconds),将原Observable发射数据时间推迟1秒。从Log打印看出,call:xxx与onNext:XXX时间相比相差1000毫秒左右,意味着观察者比原Observable发射的数据时间相差1S,与之前设定的时间延迟一致。
2.从示例代码2中可以看出,delay(Func1),创建一个新的_Observable,此_Observable需与原Obsevable发射的数据相关联。_Observable设置了延迟两秒发射,_Observable终止时,发射与其相关联的数据。从Log打印看出,call:xxx与onNext:XXX时间相比相差2000毫秒左右.

do

流程图

这里写图片描述

概述

do操作符作为注册一个动作作为原Observable生命周期事件的一种占位符

在RxJava中实现了很多do操作符的变体,下面一一介绍:
这里写图片描述
doOnEach操作符可注册一个回调,它产生的Observable每发射一项数据就会调用它一次。可以以Action的形式传递参数给它,这个Action接受一个onNext的变体Notification作为它的唯一参数,也可以传递一个Observable给doOnEach,这个Observable的onNext会被调用,就好像它订阅了原Observable一样。
这里写图片描述
doOnNext操作符类似于doOnEach(Action1),但是它的Action不是接受一个Notification参数,而是接受发射的数据项。
这里写图片描述
doOnSubscribe操作符注册一个动作,当观察者订阅它生成的Observable它就会被调用。
这里写图片描述
doOnUnsubscribe操作符注册一个动作,当观察者取消订阅它生成的Observable它就会被调用。
这里写图片描述
doOnCompleted 操作符注册一个动作,当它产生的Observable正常终止调用onCompleted时会被调用。
这里写图片描述
doOnError 操作符注册一个动作,当它产生的Observable异常终止调用onError时会被调用。
这里写图片描述
doOnTerminate 操作符注册一个动作,当它产生的Observable终止之前会被调用,无论是正常还是异常终止。

API

Javadoc: doOnEach(Action1)
Javadoc: doOnEach(Observer)

Javadoc: doOnNext(Action1)

Javadoc: doOnSubscribe(Action0))

Javadoc: doOnUnsubscribe(Action0)

Javadoc: doOnError(Action0)

Javadoc: doOnTerminate(Action0)

示例代码

1.正常执行,无onError通知

Observable.just(new Student(1, "create - 1", 20), new Student(1, "create - 1", 20))
        .subscribeOn(Schedulers.io())
        .doOnEach(new Action1<Notification<? super Student>>() {
            @Override
            public void call(Notification<? super Student> notification) {
                Log.i(TAG, "doOnEach");
            }
        })

        .doOnNext(new Action1<Student>() {
            @Override
            public void call(Student student) {
                Log.i(TAG, "doOnNext");
            }
        })
        .doOnSubscribe(new Action0() {
            @Override
            public void call() {
                Log.i(TAG, "doOnSubscribe");
            }
        })
        .doOnUnsubscribe(new Action0() {
            @Override
            public void call() {
                Log.i(TAG, "doOnUnsubscribe");
            }
        })
        .doOnError(new Action1<Throwable>() {
            @Override
            public void call(Throwable throwable) {
                Log.i(TAG, "doOnError");
            }
        })

        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<Student>() {

            @Override
            public void onStart() {
                super.onStart();
                Log.i(TAG, "onStart");
            }

            @Override
            public void onCompleted() {
                Log.i(TAG, "onCompleted");

            }

            @Override
            public void onError(Throwable e) {
                Log.i(TAG, "onError");
            }

            @Override
            public void onNext(Student student) {
                Log.i(TAG, "onNext");
                Log.i(TAG, "onNext - student:" + student.toString());
                mAdaStudent.addData(student);
                //unsubscribe();
            }
        }); 

2.有onError通知

Observable.create(new Observable.OnSubscribe<Student>() {
    @Override
    public void call(Subscriber<? super Student> subscriber) {
        subscriber.onNext(new Student(1001, "do - 1", 20));
        subscriber.onError(new Throwable("do"));
    }
})***

Log打印

1.
doOnSubscribe

doOnEach
doOnNext
onNext
onNext - student:Student{id='1'name='create - 1', age=20}

doOnEach
doOnNext
doOnEach
onNext
onNext - student:Student{id='1'name='create - 1', age=20}

onCompleted
doOnUnsubscribe

2.有onError通知
onStart
doOnSubscribe

doOnNext

doOnError
doOnError

doOnUnsubscribe

示例解析

这里写图片描述
细看上面流程图,可以了解到各个do变体在Observable整个声明周期中执行过程。
但是对于doOnCompleted和doOnTerminate只是针对于设定的Observable的生命周期相关联,假如设置应采用就近原则,意味着其与临着最近的Observable的生命周期相关联。

materialize

流程图

这里写图片描述

概述

materialize操作符操作符将原Observable的onNext和终止通知onCompleted或onError都转换为一个Observable发射的Notification对象的序列。而一个合法的有限的Obversable将调用它的观察者的onNext方法零次或多次,然后调用观察者的onCompleted或onError正好一次。

materialize默认不在任何特定的调度器 (Scheduler) 上执行。

API

Javadoc: materialize()

dematerialize

流程图

这里写图片描述

概述

dematerialize操作符是materialize操作符的逆向过程,将原Observable发射的Notification对象还原成Observable的通知。

dematerialize默认不在任何特定的调度器 (Scheduler) 上执行。

API

Javadoc: dematerialize()

observeOn

流程图

这里写图片描述

概述

observeOn操作符指定Observable在一个特定的调度器上发送通知给观察者 (调用观察者的onNext, onCompleted, onError方法)。
这里写图片描述
当遇到一个异常时ObserveOn会立即向前传递这个onError终止通知,它不会等待Observable接受任何之前它已经收到但还没有发射的数据项。这可能意味着onError通知会跳到(并丢弃)原Observable发射的数据项前面,正如图例上展示的。

API

Javadoc: observeOn(Scheduler)

subscribeOn

流程图

这里写图片描述

概述

SubscribeOn操作符指定Observable在一个特定的调度器上运转。

API

Javadoc: subscribeOn(Scheduler))
Javadoc: unsubscribeOn(Scheduler))

serialize

流程图

这里写图片描述

概述

一个Observable可以异步调用它的观察者的方法,可能是从不同的线程调用。这样会让Observable调用冲突,它可能会在某一个onNext调用之前尝试调用onCompleted或onError方法,或者从两个不同的线程同时调用onNext方法。使用Serialize操作符,你可以纠正这个Observable的行为,保证它的行为是正确的且是同步的。

serialize操作符默认不在任何特定的调度器上执行。

API

Javadoc: serialize())

subscribe

概述

subscribe操作符连接观察者和Observable的桥梁。一个观察者要想接收到Observable发射的数据项,或者想要从Observable接收错误和完成通知,它首先必须使用该操作符订阅Observable。

Subscribe操作符的参数是Subscriber一般实现可能会接受一到三个方法(然后由观察者组合它们),或者接受一个实现了包含这三个方法的接口的对象(有时叫做Observer或Subscriber).

subscribe方法返回一个实现了Subscription接口的对象。这个接口包含unsubscribe和isSubscribe方法,任何时刻你都可以调用unsubscribe()方法来取消观察者订阅Observable,同时可以调用isSubscribe判断该观察者是否已订阅该Observable。

API

Javadoc: subscribe()
Javadoc: subscribe(Action1))
Javadoc: subscribe(Action1,Action1)
Javadoc: subscribe(Action1,Action1,Action0)
Javadoc: subscribe(Observer)
Javadoc: subscribe(Subscriber)

timeInterval

流程图

这里写图片描述

概述

timeInterval操作符将拦截原Observable发射的数据项,替换为发射表示相邻发射物时间间隔的对象。但,未创建与原Observable发射最后一项数据和发射onCompleted通知之间时长对应的时间间隔的对象。

timeInterval默认在immediate调度器上执行,但可通过传参数指定执行的调度器。

API

Javadoc: timeInterval()
Javadoc: timeInterval(Scheduler)

timeout

流程图

这里写图片描述

概述

timeout操作符在原Observable了指定的一段时长没有发射任何数据时,将会发送一个onError通知终止这个Observable。

在RxJava中有很多timeout操作符的变体,以满足各种情况下的需求。下面一一描述:
这里写图片描述
timeout(long,TimeUnit)变体接受时间参数和时间单位参数,来设定间隔时长,每当原Observable发射了一项数据,timeout就启动一个计时器,如果计时器超过了指定指定的时长而原Observable没有发射另一项数据,timeout就抛出TimeoutException,以一个错误通知终止Observable。
这里写图片描述
timeout(long,TimeUnit,Observable)和timeout(long,TimeUnit,Observable,Scheduler)变体不会发送错误通知终止,而是重新订阅备用的Observable并发射其数据项。她们默认在computation调度器上执行,而timeout(long,TimeUnit,Observable,Scheduler)变体可以通过Scheduler参数指定执行调度器。
这里写图片描述
timeout(Func1)变体在原Observable发送一个数据时,不是启动一个计时器,而是创建并返回一个_Observable,若_Observable终止时,原Observable还没有发送数据,timeout就抛出TimeoutException,以一个错误通知终止Observable。该变体默认在immediate调度器上执行。
这里写图片描述
timeout(Func0,Func1)变体类似于timeout(Func1),不同的是不会发送错误通知终止,而是重新订阅备用的Observable并发射其数据项。该变体默认在immediate调度器上执行。

API

Javadoc: timeout(long,TimeUnit)

Javadoc: timeout(long,TimeUnit,Observable)
Javadoc: timeout(long,TimeUnit,Observable,Scheduler)

Javadoc: timeout(Func1))

Javadoc: timeout(Func0,Func1))

示例代码

Observable.interval(2, TimeUnit.SECONDS)
        .timeout(1, TimeUnit.SECONDS, Observable.just(new Long(-1)))
        .subscribeOn(Schedulers.io())
        .flatMap(aLong -> {
            if (aLong == -1) {
                return Observable.just(new Student(1, new String("timeout - 1"), 20));
            }
            return Observable.just(new Student(aLong.intValue(), new String("timeout - 1"), 20));
        })
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(student -> {
            mAdaStudent.addData(student);
            Log.i(TAG, student.toString());
        });

Log打印

Student{id='1'name='timeout - 1', age=20}

示例解析

看示例代码,创建了一个每隔2秒发送一个long类型的Observable,但其将timeout(long,TimeUnit,Observable)变体设置间隔时间为1s,同时设定备用Observable为Observable.just(new Long(-1)).使用flapmap操作符转换为Obeservable。根据上述设定,Observable明显会超时,发送onError通知。但是通过Log打印可以看出,onError通知被拦截,而是使用了备用的Observable发射数据。

timestamp

流程图

这里写图片描述

概述

timestamp操作符将原Observable发射的T类型数据的转换为一个发射类型为Timestamped的数据的Observable,每一项都包含数据的原始发射时间,同时将其发射出去。

该操作符默认在immediate调度器上执行,但是可以通过参数指定其它的调度器。

API

Javadoc: timestamp())
Javadoc: timestamp(Scheduler))

示例代码

Observable.from(getListOfStudent())
            .subscribeOn(Schedulers.io())
            .take(2)
            .timestamp()
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(new Action1<Timestamped<Student>>() {
                @Override
                public void call(Timestamped<Student> stu) {
                    Log.i(TAG, stu.toString());
                }
            });

Log打印

Timestamped(timestampMillis = 1473076584097, value = Student{id='1'name='A', age=23})
Timestamped(timestampMillis = 1473076584098, value = Student{id='2'name='B', age=33})

示例解析

示例中,截取了Observable的前两个数据用来发射,同时使用timestamp操作符将发射的每个数据增加时间戳,在订阅时,参数的数据类型的泛型为Timestamped。看Log打印,Timestamped的成员中,timestampMillis为时间戳,而value为原Observable发射的数据。

using

流程图

这里写图片描述

概述

using操作符让你可以指示Observable创建一个只在它的生命周期内存在的资源,当Observable终止时这个资源会被自动释放。

using操作符接受三个参数:

  • 一个用户创建一次性资源的工厂函数
  • 一个用于创建Observable的工厂函数
  • 一个用于释放资源的函数

当一个观察者订阅using返回的Observable时,using将会使用Observable工厂函数创建观察者要观察的Observable,同时使用资源工厂函数创建一个你想要创建的资源。当观察者取消订阅这个Observable时,或者当观察者终止时(无论是正常终止还是因错误而终止),using使用第三个函数释放它创建的资源。

该操作符默认不在任何特定的调度器上执行。

API

Javadoc: using(Func0,Func1,Action1))

示例代码

Observable.from(getListOfStudent())
        .subscribeOn(Schedulers.io())
        .using(new Func0<Integer>() {
            @Override
            public Integer call() {
                return new Random().nextInt(10);
            }
        }, new Func1<Integer, Observable<Student>>() {
            @Override
            public Observable<Student> call(Integer integer) {
                return Observable.just(new Student(integer, "using -  " + integer, 20 + integer));
            }
        }, new Action1<Integer>() {
            @Override
            public void call(Integer integer) {
                integer = null;
            }
        }).observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Action1<Student>() {
            @Override
            public void call(Student stu) {
                Log.i(TAG, stu.toString());
                mAdaStudent.addData(stu);
            }
        });

Log打印

Student{id='4'name='using -  4', age=24}

示例解析

示例中,Observable.from(getListOfStudent())创建了一个Observable对象,理论上应该打印getListOfStudent()获取的student列表,但是使用using操作符创建了一个在原Observable生命周期内的一次性资源,同时使用资源创建了新的Observable,暂时称为_observable.现看Log打印,只是打印了_observable发射的数据,意味着观察者订阅原Observable后,并未收到原Observable发射的数据,应该是丢弃了,只是收到了_observable发射的数据。

to

流程图

这里写图片描述

概述

to操作符将Observable转换为另一个对象或数据结构的Observable。

在RxJava实现中,to操作符可以将Observable或者Observable发射的数据序列转换为另一个对象或数据结构,它们中的一些会阻塞直到Observable终止,然后生成一个等价的对象或数据结构;另一些返回一个发射那个对象或数据结构的Observable。为满足各种情况需求,to操作符有多种变体,例如getIterator、toFuture、toIterable、toList、toMap、toMultiMap、toSortedList。

getIterator

流程图

这里写图片描述

概述

getIterator操作符只能用于BlockingObservable的子类,如果要使用该操作符,首先必须把原Observable转换为一个BlockingObservable。可以BlockingObservable.from或the Observable.toBlocking使用这两个操作符进行转换。

该操作符将Observable转换为一个Iterator,可以通过它迭代原始Observable发射的数据集。

API

Javadoc: BlockingObservable.getIterator()

示例代码

Iterator<Student> iterator  = Observable.from(getListOfStudent())
        .subscribeOn(Schedulers.io())
        .toBlocking()
        .getIterator();

while (iterator.hasNext()) {
    Log.i(TAG, iterator.next().toString());
}

Log打印

Student{id='1'name='A', age=23} 
Student{id='2'name='B', age=33} 
Student{id='3'name='C', age=24} 
Student{id='4'name='D', age=24} 
Student{id='5'name='E', age=33} 
Student{id='6'name='F', age=23} 

示例解析

示例中,创建了Observable对象,使用操作符.toBlocking将原Observable转换为BlockingObservable,因为getIterator操作符只能用于BlockingObservable的子类。然后使用getIterator(),将原Observable转换为数据的集合,最后将集合遍历,遍历结果见Log打印。

toFuture

流程图

这里写图片描述

概述

toFuture操作符类似于getIterator操作符,也只能用于BlockingObservable。该操作符将Observable转换为一个返回单个数据项的Future,如果原Observable发射多个数据项,Future会收到一个IllegalArgumentException异常;如果原Observable没有发射任何数据,Future会收到一个NoSuchElementException异常。

如果想将发射多个数据项的Observable转换为Future,可以这样用:myObservable.toList().toBlocking().toFuture()。

API

示例代码

try {
    Future<List<Student>> future = Observable.from(getListOfStudent())
            .subscribeOn(Schedulers.io())
            .toList()
            .toBlocking()
            .toFuture();
    Log.i(TAG, future.get().toString());
} catch (InterruptedException e) {
    e.printStackTrace();
} catch (ExecutionException e) {
    e.printStackTrace();
    }

Log打印

 [Student{id='1'name='A', age=23},
  Student{id='2'name='B', age=33}, 
  Student{id='3'name='C', age=24}, 
  Student{id='4'name='D', age=24}, 
  Student{id='5'name='E', age=33}, 
  Student{id='6'name='F', age=23}]

toIterable

流程图

这里写图片描述

概述

toIterable操作符只能用于BlockingObservable。该操作符将Observable转换为一个Iterable,可以通过它迭代原Observable发射的数据集。

API

Javadoc: BlockingObservable.toIterable()

示例代码

Iterable<Student> iterable= Observable.from(getListOfStudent())
        .subscribeOn(Schedulers.io())
        .toBlocking()
        .toIterable();

for (Student stu : iterable) {
    Log.i(TAG, stu.toString());
}

Log打印

Student{id='1'name='A', age=23} 
Student{id='2'name='B', age=33} 
Student{id='3'name='C', age=24} 
Student{id='4'name='D', age=24} 
Student{id='5'name='E', age=33} 
Student{id='6'name='F', age=23} 

toList

流程图

这里写图片描述

概述

toList操作符将发射多项数据且为每一项数据调用onNext方法的Observable发射的多项数据组合成一个List,然后调用一次onNext方法传递整个列表。

如果原Observable没有发射任何数据就调用了onCompleted,toList返回的Observable会在调用onCompleted之前发射一个空列表。如果原Observable调用了onError,toList返回的Observable会立即调用它的观察者的onError方法。

toList默认不在任何特定的调度器上执行。

API

Javadoc: toList()

示例代码

Observable.from(getListOfStudent())
        .subscribeOn(Schedulers.io())
        .toList()
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<List<Student>>() {

            @Override
            public void onStart() {
                super.onStart();
                mAdaStudent.clear();
            }

            @Override
            public void onCompleted() {
                mCompositeSubscription.remove(this);
            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(List<Student> stus) {
                mAdaStudent.addData(stus);
                Log.i(TAG, stus.toString());
            }
        });

Log打印

[Student{id='1'name='A', age=23},
  Student{id='2'name='B', age=33}, 
  Student{id='3'name='C', age=24}, 
  Student{id='4'name='D', age=24}, 
  Student{id='5'name='E', age=33}, 
  Student{id='6'name='F', age=23}]

示例解析

先看Log打印,可以清晰的看到打印的是一个List,而示例代码中,创建的Observable是通过 Observable.from()创建的,而且每次调用onNext发射一个Student对象。而通过toList操作符将Observable转换为Observable

toMap

流程图

这里写图片描述

概述

toMap操作符将原Observable发射的所有数据项收集到到一个Map(默认是HashMap)然后发射这个Map。其变体可以提供一个用于生成Map的Key的函数,还可以提供一个函数转换数据项到Map存储的值(默认数据项本身就是值)。

toMap默认不在任何特定的调度器上执行。

API

Javadoc: toMap(Func1)
Javadoc: toMap(Func1,Func1)
Javadoc: toMap(Func1,Func1,Func0)

示例代码

Observable.from(getListOfStudent())
        .subscribeOn(Schedulers.io())
        .toMap(new Func1<Student, Integer>() {

            @Override
            public Integer call(Student student) {
                return student.getId();
            }
        })
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<Map<Integer, Student>>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(Map<Integer, Student> integerStudentMap) {
                Log.i(TAG, integerStudentMap.toString());
            }
        });

Log打印

{4=Student{id='4'name='D', age=24}, 
 1=Student{id='1'name='A', age=23}, 
 6=Student{id='6'name='F', age=23},
 5=Student{id='5'name='E', age=33}, 
 3=Student{id='3'name='C', age=24},
 2=Student{id='2'name='B', age=33}}

示例解析

示例代码中,Observable通过。toMap(Func1)将原Observable发送的数据保存到一个MAP中,并在参数函数中,设定sutdent的id属性作为key。细看Log打印正是如此。

toMultiMap

流程图

这里写图片描述

概述

toMultiMap操作符类似于toMap操作符,不同的是,它生成的这个Map同时还是一个ArrayList(默认是AarryList,可通过在函数参数中修改)。

toMultiMap默认不在任何特定的调度器上执行。

API

Javadoc: toMultiMap(Func1))
Javadoc: toMultiMap(Func1,Func1))
Javadoc: toMultiMap(Func1,Func1,Func0))
Javadoc: toMultiMap(Func1,Func1,Func0,Func1))

示例代码

Observable.from(getListOfStudent())
        .subscribeOn(Schedulers.io())
        .toMultimap(new Func1<Student, Integer>() {

            @Override
            public Integer call(Student student) {
                return student.getId();
            }
        })
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<Map<Integer, Collection<Student>>>() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(Map<Integer, Collection<Student>> integerCollectionMap) {
                Log.i(TAG, integerCollectionMap.toString());
            }
        });

Log打印

{4=[Student{id='4'name='D', age=24}], 
 1=[Student{id='1'name='A', age=23}], 
 6=[Student{id='6'name='F', age=23}], 
 5=[Student{id='5'name='E', age=33}], 
 3=[Student{id='3'name='C', age=24}], 
 2=[Student{id='2'name='B', age=33}]}

示例解析

示例代码中,Observable通过。toMap(Func1)将原Observable发送的数据保存到一个MAP中,并在参数函数中,设定sutdent的id属性作为key。但toMultimap操作符在将数据保存到MAP前,先将数据保存到Collection,而toMap操作符将数据直接保存到MAP中,并没有再包裹一层
Collection。

toSortedList

流程图

这里写图片描述

概述

toSortedList操作符类似于toList操作符,不同的是,它将对产生的列表排序,默认是自然升序,如果发射的数据项没有实现Comparable接口,会抛出一个异常。当然,若发射的数据项没有实现Comparable接口,可以使用toSortedList(Func2)变体,其传递函数参数作为用于比较两个数据项,这是toSortedList不会使用Comparable接口。

API

Javadoc: toSortedList()
Javadoc: toSortedList(Func2)

示例代码

Observable.from(getListOfStudent())
        .subscribeOn(Schedulers.io())
        .toSortedList(new Func2<Student, Student, Integer>() {
            @Override
            public Integer call(Student student, Student student2) {
                return student.getAge() - student2.getAge();
            }
        }) .observeOn(AndroidSchedulers.mainThread())
        .subscribe(new Subscriber<List<Student>>() {

            @Override
            public void onStart() {
                super.onStart();
                mAdaStudent.clear();
            }

            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(List<Student> students) {
                Log.i(TAG, students.toString());
                mAdaStudent.addData(students);
            }
        });

Log打印

[Student{id='1'name='A', age=23}, 
 Student{id='6'name='F', age=23}, 
 Student{id='3'name='C', age=24},
 Student{id='4'name='D', age=24}, 
 Student{id='2'name='B', age=33}, 
 Student{id='5'name='E', age=33}]
作者:IO_Field 发表于2016/9/5 23:10:05 原文链接
阅读:51 评论:0 查看评论

微信支付,支付宝支付的集成和注意部分

$
0
0

一.支付宝支付

1. 集成说明

1.1 作为当下最热门的支付宝和微信支付,相信有很多的app都很乐意的将支付宝集成在app中,毕竟只要你开发的app有一点涉及到买卖的,都需要集成支付宝或者微信支付.那么接下来我将为大家一一列举举出我在集成支付宝和微信中遇到的难点和其中出现的一些坑.

2. 集成前的准备(里面的步骤在集成文档中都有介绍)

2.1 注册支付宝帐号(账户最好采用公司邮箱注册)——附上网址

https://www.baidu.com/s?wd=%E6%94%AF%E4%BB%98%E5%AE%9D%E5%BC%80%E5%8F%91%E8%80%85%E5%B9%B3%E5%8F%B0&rsv_spt=1&rsv_iqid=0xfe0a29290002df6b&issp=1&f=3&rsv_bp=0&rsv_idx=2&ie=utf-8&tn=monline_3_dg&rsv_enter=1&rsv_sug3=14&rsv_sug1=10&rsv_sug7=100&rsv_sug2=0&prefixsug=%E6%94%AF%E4%BB%98%E5%AE%9D%E9%9B%86%E6%88%90&rsp=4&inputT=9885&rsv_sug4=10467

2.2 设置(申请)支付密码

2.3 设置密码提示问题

2.4 绑定手机

2.5 创建应用(目的 : 获取APPID帐号)

2.6 然后就等待支付宝那边审核通过了

3. 开始集成

3.1 打开集成文档

https://doc.open.alipay.com/

3.2 按步骤执行

3.2.1 点击文档中的 支付能力—->App支付—->SDK下载(https://doc.open.alipay.com/doc2/detail.htm?treeId=54&articleId=104509&docType=1)—->SDK&DEMO(将demo和sdk下载下来)
3.2.2 运行支付宝支付Demo,了解支付宝整个支付的流程(如果里面出现错误,直接百度或者查看文档,里面有详细的解释,我这里就不再一一说明了)

4. 系统交互流程(红色部分是重点部分)

这里写图片描述

4.1 流程 : 商户客户端发送请求(获取签名后的订单信息)—->商户服务端(返回签名后的订单信息sign)—->调用支付宝支付接口,发送支付请求—->返回支付结果

5. 订单 : 由服务器生成

6. 注意 : 只需得到服务器返回的sign

7. 导入相关文件(此处导入文件最好看官方文档,比较正式)

这里写图片描述

8. 导入相关库

8.1 TARGETS—->General—->Linked Frameworks and Libraries—->点击”+”导入相关的库(此处导入的文件当已开发文档中规定的为主)

8.2 填写支付宝URL Schemes : TARGETS—-> info—-> URL Types(5)—-> 点击”+”对支付宝的添加(此处当已开发文档中规定的为主)—-> 结果图如下

这里写图片描述

9. 说明

9.1 支付宝的集成本来是和Demo上代码书写的顺序一样,但是由于我们后台是第一次做支付,所以最终无法返回一个正确的sign,这样导致我们集成的时候中间有一个步骤是多余的.

10. 集成

10.1 发送请求给后台,请求需要的字串(由于我们做的app设计到需要传入10个参数给后台,才能返回结果)—-> 下面贴上请求代码

    long activityId = _activitiy.ActivityId;
        NSString *stringActivityId = [@(activityId) stringValue];         //活动ID
        NSString *userId = [AppConfig shareInstance].UserID;
        //用户ID
        [self.paramDic setObject:stringActivityId forKey:@"activityId"];
        [self.paramDic setObject:userId forKey:@"userId"];
    NSString *bodyStr = _activitiy.title;
    NSString *subjectStr = _activitiy.title;
    NSInteger total_amount = _activitiy.totalFee;
    NSDateFormatter* formatter = [NSDateFormatter new];
    [formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
    NSDictionary *lastDic = @{
                              @"body":bodyStr,
                              @"subject":subjectStr,
                              @"total_amount":[NSString stringWithFormat:@"%.2ld",(long)total_amount],
                              @"spbillCreateip":[self deviceIPAdress]
                              };
    NSMutableDictionary *bigDic = [lastDic mutableCopy];
    [bigDic addEntriesFromDictionary:dic];

10.2 发送请求(里面包含了调用支付宝的接口)

[[[SpeedService alloc]init]FetchUserPayProduct:nil paramDic:bigDic completionBlock:^(id response, NSError *error) {
        NSLog(@"++++%@",response);
        //服务器返回的订单信息

        NSDictionary *samllDic = [response objectForKey:@"result"];

        //服务器返回的签名
        NSString *sign = [samllDic objectForKey:@"sign"];
        //签名Encode编码
        NSString *locationSignEncode = [self encodeValue:sign];
        //创建订单模型对象并且赋值
        Order *order = [Order new];

        order.biz_content = [BizContent new];

        order.app_id             = [samllDic objectForKey:@"app_id"];
        order.biz_content        = [samllDic objectForKey:@"biz_content"];

        order.charset            = [samllDic objectForKey:@"charset"];
        order.method             = [samllDic objectForKey:@"method"];
        order.notify_url         = [samllDic objectForKey:@"notify_url"];
        order.sign_type          = [samllDic objectForKey:@"sign_type"];
        order.timestamp          = [samllDic objectForKey:@"timestamp"];
        order.version            = [samllDic objectForKey:@"version"];

        //订单信息Encode编码
        NSString *messageEncode = [order orderInfoEncoded:YES];

        //最终订单信息字符串Encode编码
        NSString *orderOneString = [NSString stringWithFormat:@"%@&sign=%@",
                                    messageEncode, locationSignEncode];

        [[AlipaySDK defaultService] payOrder:orderOneString fromScheme:Scheme callback:^(NSDictionary *resultDic) {
            NSLog(@"服务器返回的结果:%@",resultDic);
        }];
    }];

10.3 签名Encode编码

- (NSString*)encodeValue:(NSString*)value
{
    NSString* encodedValue = value;
    if (value.length > 0) {
        encodedValue = (__bridge_transfer  NSString*)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, (__bridge CFStringRef)value, NULL, (__bridge CFStringRef)@"!*'();:@&=+$,/?%#[]", kCFStringEncodingUTF8 );
    }
    return encodedValue;
}

10.4 注意

1. 订单信息Encode编码就不需要自己手动编码了,直接调用支付宝提供的接口,接口里面已经对排序,编码进行了设置,直接调用就行
2. 里面的集成步骤和Demo中的有所不同,你们也可以根据自己的需求,酌情的增加或者减少.

11. 支付宝如果按照上面的步骤集成,应该是没问题的,文章结尾后会贴上我集成的时候遇见的bug和坑,敬请期待(支付宝集成完毕)

二 . 微信支付

1. 集成说明 : 相对于支付宝集成,微信集成要简单的多了

2. 集成前的准备(完成一下需要的帐号信息)

2.1 微信帐号

2.2 微信帐号密码

2.3 微信支付商户号

2.4 商户平台登录帐号

2.5 商户平台登录密码

2.6 应用APPID

3. 注意 : 以上步骤在微信支付平台上都有说明,我这里就不一一介绍了

4. 微信开放平台(附上网址:只要注册过了,就可以直接登录查看相关信息)—>选择

[微信支付]APP支付开发者文档

https://www.baidu.com/s?wd=%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98%E5%B9%B3%E5%8F%B0&rsv_spt=1&rsv_iqid=0x970a0db100003d17&issp=1&f=3&rsv_bp=0&rsv_idx=2&ie=utf-8&tn=monline_3_dg&rsv_enter=1&rsv_sug3=12&rsv_sug1=9&rsv_sug7=100&rsv_sug2=0&prefixsug=%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98&rsp=8&inputT=7099&rsv_sug4=7846

5. 下载微信支付sdk

https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=11_1

这里写图片描述

6. 运行Demo(如果出现错误,直接照着官方文档修改或者百度就行—>通常下载微信支付sdk运行是不存在问题的)

7. 交互时序图(红色框是重点部分)

这里写图片描述

8. 总结 : 我们只需要给后台请求,返回需要的7个参数就可以了

9. 导入相关文件(注意 : 以微信支付开发文档为准)

这里写图片描述

10. 添加 URL Types(5)微信—-> 如果在集成微信支付之前使用了微信分享,那该步骤可以省略

TARGETS—->info—->URL Types(5)—->点击”+”添加微信URL Schemes(如下图)

这里写图片描述

11. 向微信注册AppID(代码如下)—-> 第一个空 : AppID(最好使用宏) 第二个空 : 可以随便填写

11.1 注意 : 如果在集成微信支付之前集成了微信分享,那么该句代码必须写在分享注册的代码之后

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [WXApi registerApp:WXapp_id withDescription:@"测试demo"];
}

12. 集成代码一(给服务器发送请求—->得到需要的7个参数openID,partnerId,prepayId,nonceStr,timeStamp,package,sign)

#pragma mark - 微信支付
- (void)setUpWxPay
{
    long activityId = _activitiy.ActivityId;
    NSString *stringActivityId = [@(activityId) stringValue];
    NSString *userId = [AppConfig shareInstance].UserID;
    NSString *userTel = [self.paramDic objectForKey:@"mobile"];
    if (userTel.length == 0 || userTel.length > 11) {
        [SVProgressHUD showErrorWithStatus:@"请输入正确的联系电话"];
        return;
    }
    NSString *userName = [self.paramDic objectForKey:@"name"];
    if (userName.length == 0) {
        [SVProgressHUD showErrorWithStatus:@"请输入姓名"];
        return;
    }
    NSString *userIdentity = [self.paramDic objectForKey:@"identity"];
    BOOL identity = [self isCorrect:userIdentity];
    NSString *userEmail = [self.paramDic objectForKey:@"email"];
    if (userEmail.length == 0) {
        [SVProgressHUD showErrorWithStatus:@"请输入邮箱号"];
        return;
    }
    NSInteger totalFee = _activitiy.totalFee;
    NSString *body = _activitiy.title;
    NSString *subject = _activitiy.title;
    NSString *spbillCreateip = [self deviceIPAdress];
    SpeedService *speedService = [[SpeedService alloc] init];
    NSDictionary *dict = @{
                           @"body":body,
                           @"subject":subject,
                           @"userId":userId,
                           @"activityId":stringActivityId,
                           @"name":userName,
                           @"email":userEmail,
                           @"mobile":userTel,
                           @"identity":userIdentity,
                           @"total_amount":[NSString stringWithFormat:@"%d",totalFee],
                           @"spbillCreateip":spbillCreateip
                           };
    NSLog(@"字典是 : %@",dict);
    if (identity == YES) {
        //判断是否安装了微信
        if ([WXApi isWXAppInstalled]){
            [speedService FetchUserWxPayProduct:dict completionBlock:^(id response, NSError *error) {
                if (!error) {
                    NSLog(@"返回的结果是 : %@",response);
                    NSString * str = [response objectForKey:@"code"];
                     if ([str isEqualToString:@"0000"]) {
                        NSDictionary *dic = [NSDictionary dictionaryWithDictionary:[response objectForKey:@"result"]];
                        [self configRequest:dic stringStr:nil];
                     }

                }else{

                    NSLog(@"网络超时,返回的错误信息是 : %@",error);
                }
            }];

        }else{

            [SVProgressHUD showErrorWithStatus:@"请安装微信"];
        }

    }else{
        [SVProgressHUD showErrorWithStatus:@"请输入正确的身份证号码"];
        return;
    }
}

13. 调用微信支付代码,即可完成微信支付

#pragma mark - 调用微信支付的接口
- (void)configRequest:(NSDictionary *)dic stringStr:(NSString *)stringStr{

    //需要创建这个支付对象
    PayReq *req = [[PayReq alloc] init];

    //由用户微信号和AppID组成的唯一标识,用于校验微信用户
    req.openID = [dic objectForKey:@"appid"];

    // 商家id,在注册的时候给的
    req.partnerId = [dic objectForKey:@"partnerid"];

    // 预支付订单这个是后台跟微信服务器交互后,微信服务器传给你们服务器的,你们服务器再传给你
    req.prepayId  = [dic objectForKey:@"prepayid"];

    // 随机编码,为了防止重复的,在后台生成
    req.nonceStr  = [dic objectForKey:@"noncestr"];

    // 这个是时间戳,也是在后台生成的,为了验证支付的
    NSMutableString *stamp = [dic objectForKey:@"timestamp"];
    req.timeStamp = stamp.intValue;

    // 根据财付通文档填写的数据和签名
    //这个比较特殊,是固定的,只能是即req.package = Sign=WXPay
    req.package = [dic objectForKey:@"package"];

    // 这个签名也是后台做的
    req.sign = [dic objectForKey:@"sign"];

    //发送请求到微信,等待微信返回onResp
    [WXApi sendReq:req];
}

14. 特别说明 : 由于调用微信支付的7个参数都是服务器返回来的,不需要客户端做任何事情.当然有些参数客户端也是可以自己生成的,这就留给你们自己去处理了.

三. 总结

1. 集成支付宝出现的错误 :

1.1 能拿到服务器那边的sign,但是在调用支付宝接口的时候,代码运行并不会执行支付宝接口的block块,并且打印信息直接是提示今天已经注册报道了.

—-> 1.1.1 解决办法:将info–URL Types(5)–URL Schemes中配置的长度该为高于6个字串,很有可能是因为重名的问题.(直接重命名就行)

1.2 支付宝注册时间问题 : 在8月9号之前注册的用户,使用支付宝2.0的版本会出现问题,但是可以使用支付宝1.0版本的(重申:如果解签再重新签约的话,就可以使用支付宝2.0,并且兼容支付宝1.0的sdk)

—-> 1.2.1 解决办法 : 解签,再重新签约大约花1个工作日的时间

1.3 集成支付宝的时候,给自己的服务器发送请求得到sign.当打印请求拼接的参数的时候,经过仔细对比,发现其中会出现key和value值顺序颠倒问题

—-> 1.3.1 解决办法:将原来导入的sdk删除,重新倒入一次sdk就可以解决问题(很有可能是sdk造成的问题,我重新导入就解决了这问题)

1.4 签名(sign)出现的问题: 运用服务器返回的sign拼接成字符串,真机上出现系统繁忙,请重试.

—-> 1.4.1 解决方案 : 经过和支付宝提供的demo对比,支付宝的demo是完全可以实现支付功能,但是字符串加上后台返回的sign就出现这样的错误提示,说明签名有问题,这时只能叫后台返回正确的签名.

1.5 顺序问题 :如果服务器返回的sign没问题,那么最终就是的发送给支付宝参数拼接顺序问题了,要严格按照服务器返回的字串顺序拼接,然后再请求支付宝接口.(最后的出的结论是,不需要程序员手动按照顺序拼接,直接调用支付宝提供的方法,就可以完成拼接).

2. 微信支付出现的错误 :

1.1 集成完微信支付之后,真机运行,微信支付的时候只出现一个白的确定按钮,点击的时候跳回原来的app,并且返回-2

这里写图片描述

—-> 1.1.1 解决办法 : 肯定是参数的问题,如果你能确定你这里没错,那么就是后台返回的参数有问题(我做的时候是后台将一个参数的小写写成了大写,所以出现了这样的问题.其它错误是不会造成这样的结果的.仔细检查参数就可以解决)

四. 写在最后

这段时间由于很忙,拖欠了很多博客没有更新,后续我一定会慢慢补上来.最后如果大家觉得我写的还可以,麻烦关注我的官方博客,谢谢!!!!

作者:xf931456371 发表于2016/9/6 0:13:30 原文链接
阅读:31 评论:0 查看评论

ViewGroup的dispatchTouchEvent理解

$
0
0

ViewGroup的dispatchTouchEvent理解

以下图例子说明,OFramelayout在最外层

这里写图片描述

    图1.1 view的层级关系
  1. 结论1:
    dispatchTouchEvent()返回false,后续的ACTION_MOVE、ACTION_UP等收不到。注:dispatchTouchEvent()中判断手势是
    ACTION_DOWN时,返回false,则后续的触摸事件收不到,如果返回true,在后续的ACTION_MOVE条件下,不论返回什么都能收到后续触摸响应。类推,在onTouchEvent中是一样的结论。

  2. 结论2:
    如果有对应的OnTouchListener,可以在onTouch中返回true拦截事件,使onTouchEvent()不执行。

  3. 结论3:
    在一次触摸事件中,如果onInterceptTouchEvent()中返回true,则触摸事件由该视图消费,不会再派发。如果onInterceptTouchEvent()先返回false,然后在某个条件返回true,则在起初返回false时,onInterceptTouchEvent()会被一直调用(前提条件是子视图消费了事件并返回了true),返回true后,onInterceptTouchEvent将不会再被调用,不管以后返回啥,onInterceptTouchEvent都不会再被调用。注:前提条件是该视图的dispatchTouchEvent要返回true,也就是说要能持续响应触摸事件。

看到这,大家都会觉得云里雾里,通过研读源码具体分析每个结论,以下基于android 5.0的源码。

结论一研读:

MFramelayout在dispatchTouchEvent()中的ACTION_DOWN条件下返回false,则触控事件首先进入OFramelayout的dispatchTouchEvent()即ViewGroup的dispatchTouchEvent()。进入1956行的判断,disallowIntercept是由requestDisallowInterceptTouchEvent()方法决定的。disallowIntercept为false时表示可拦截事件,然后会调用onInterceptTouchEvent()方法。disallowIntercept默认时为false。然后进入1985行的判断,由于是ACTION_DOWN,继续进入1995行,继续进入2007行,继续进入2016的for循环,进入2049的条件判断。进入dispatchTransformedTouchEvent()方法,在该方法的2405行,handled = child.dispatchTouchEvent(event),也就是调用了MFramelayout的dispatchTouchEvent()方法,由于假定条件下其返回false,dispatchTransformedTouchEvent()该方法返回false,这样就不会进入2049行的条件。然后进入到2090的条件判断,还是调用了dispatchTransformedTouchEvent()方法,然后执行super.dispatchTouchEvent(event),也就是执行View.dispatchTouchEvent()方法。关于View.dispatchTouchEvent(),请查看郭霖的博客。在OFramelayout的onTouchEvent中返回true,则2092行的handled为true,那么OFrameLayout的dispatchTouchEvent返回true。
接下来触摸事件是ACTION_MOVE,进入到OFrameLayout的dispatchTouchEvent()方法。进入到1968行,intercepted = true,则不会进入1985的条件判断,接下来进入2090行的条件判断。在这个过程中,OFrameLayout没将事件传给子视图。也就是dispatchTouchEvent()方法在ACTION_DOWN时返回false,则收不到后续的触摸事件,在这里就是MFramelayout收不到后续事件。

结论2研读:

查看郭霖的博客。Android事件分发机制完全解析,带你从源码的角度彻底理解(上),
Android事件分发机制完全解析,带你从源码的角度彻底理解(下)

结论3研读

情况一:OFramelayout的onInterceptTouchEvent返回true。

OFrameLayout的dispatchTouchEvent()接受触控事件,ACTION_DOWN时会执行1960-》2092。mFirstTouchTarget为null,其赋值在2065行的addTouchTarget()方法中。ACTION_MOVE时执行1968-》2092。所以,在一次触摸事件中,如果onInterceptTouchEvent()中返回true,则触摸事件由该视图消费,不会再派发。

情况二:OFramelayout的onInterceptTouchEvent返回false。

ACTION_DOWN时,执行1960-》1985-》1995-》2049,假设子视图消费了事件并返回了true,则会执行2065行,则mFirstTouchTarget被赋值为该子视图。
ACTION_MOVE是,在上面假设下,mFirstTouchTarget不为空,则会调用onInterceptTouchEvent()方法。执行顺序1960-》1985,由于是ACTION_MOVE,则不会进1995,接下来到2097-》2106,然后执行子视图(MFramelayout)的dispatchTouchEvent()方法。
如果子视图没有消费事件,则mFirstTouchTarget为空,则onInterceptTouchEvent()只会执行一次。
所以onInterceptTouchEvent()返回false,会被一直调用(前提条件是子视图消费了事件并返回了true)。

情况三:在一次触摸事件中,OFramelayout的onInterceptTouchEvent返回false,在某个条件再返回true。

在OFramelayout的onInterceptTouchEvent返回false时,详情参考情况二。在某个条件下onInterceptTouchEvent返回true时,假设先前子视图消费了事件并返回了true(即mFirstTouchTarget不为空),会执行1960-》2097-》2375-》2112,在2112行mFirstTouchTarget被赋值为null。触摸事件继续执行1968-》2092,onInterceptTouchEvent不会再被调用。
所以如果onInterceptTouchEvent()先返回false,然后在某个条件返回true,则在起初返回false时,onInterceptTouchEvent()会被一直调用(前提条件是子视图消费了事件并返回了true),返回true后,onInterceptTouchEvent将不会再被调用,不管以后返回啥,onInterceptTouchEvent都不会再被调用。

ViewGroup的dispatchTouchEvent源码

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                intercepted = true;
            }

            // If intercepted, start normal event dispatch. Also if there is already
            // a view that is handling the gesture, do normal event dispatch.
            if (intercepted || mFirstTouchTarget != null) {
                ev.setTargetAccessibilityFocus(false);
            }

            // Check for cancelation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if (!canceled && !intercepted) {

                // If the event is targeting accessiiblity focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildOrderedChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = customOrder
                                    ? getChildDrawingOrder(childrenCount, i) : i;
                            final View child = (preorderedList == null)
                                    ? children[childIndex] : preorderedList.get(childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if (childWithAccessibilityFocus != null) {
                                if (childWithAccessibilityFocus != child) {
                                    continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                if (preorderedList != null) {
                                    // childIndex points into presorted list, find original index
                                    for (int j = 0; j < childrenCount; j++) {
                                        if (children[childIndex] == mChildren[j]) {
                                            mLastTouchDownIndex = j;
                                            break;
                                        }
                                    }
                                } else {
                                    mLastTouchDownIndex = childIndex;
                                }
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }

                            // The accessibility focus didn't handle the event, so clear
                            // the flag and do a normal dispatch to all children.
                            ev.setTargetAccessibilityFocus(false);
                        }
                        if (preorderedList != null) preorderedList.clear();
                    }

                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }

            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue;
                        }
                    }
                    predecessor = target;
                    target = next;
                }
            }

            // Update list of touch targets for pointer up or cancel, if needed.
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }
作者:y444400 发表于2016/9/6 0:20:06 原文链接
阅读:26 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>