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

初识volatile

$
0
0

有时候仅仅为了一个或者两个的实例域就是用synchronized的话,开销就会很大,而Java为我们提供了另一种同步的免锁机制,volatile。Volatile可以看成是synchronized的轻量级,功能也仅仅是synchronized的一部分,认识volatile之前,先认识Java内存模型和Java的原子性,可见性,有序性

Java的内存模型


在Java虚拟机中,每一个线程私有的就是JVMstack ,本地方法栈,PC,而共享的就是Java堆和方法区(包含常量池)。Java创建的对象都在堆中分配,所有的实例域,静态变量,数组的元素都在堆中,都是被线程共享的。

而Java线程之间的通信是通过共享内存来实现的,也就是隐式通信,而线程通信是由Java内存模型控制的(JMM),JMM决定着一个线程对一个共享变量的写入修改何时对另一个线程可见(也就是另一个线程得到那个最新的修改值),JMM定义了线程与主存之间的抽象关系:线程之间的共享变量存储在主存中,每个线程在自己的栈中(栈中有栈帧)都有一个私有的本地内存空间,其他线程不可访问,本地内存保存着这个共享变量的拷贝。线程不能直接对共享变量直接写,先写入本地内存,然后由JMM决定什么时候将本地的值回写打主存中。

本地内存是JMM的一个抽象概念,并不真实存在,抽象示意图:


先看个例子:

public class Main {

   
public staticvoid main(String[] args) {
        Test test =
new Test(5);

        
MyThread myThread =new MyThread(test);
       
MyRunnablemyRunnable = new MyRunnable(test);

       
Thread thread = new Thread(myRunnable);

       
myThread.setName("MyThread");
       
thread.setName("MyRunnable");
       
thread.start();
        
myThread.start();
   
}
}

public class Test {

   
public int test;

    public
Test(int test) {
       
this.test = test;
   
}
}

public class MyThread extends Thread {
    Test
test;

    public
MyThread(Test test) {
       
this.test = test;
   
}

    
@Override
   
public void run() {
        System.
out.println(Thread.currentThread().getName()+" " +test.test);
       
test.test = 10;
        try
{
            System.
out.println(Thread.currentThread().getName()+" test.test= 10;");
           
System.out.println(Thread.currentThread().getName()+" 睡觉");
           
Thread.currentThread().sleep(2000);
           
System.out.println(Thread.currentThread().getName()+" " +test.test);
       
} catch (InterruptedException e) {
            e.printStackTrace()
;
       
}
        System.
out.println(Thread.currentThread().getName()+" " +test.test);
   
}
}

public class MyRunnable implements Runnable {

    Test
test;

    public
MyRunnable(Test test) {
       
this.test = test;
   
}

   
@Override
   
public void run() {

        System.
out.println(Thread.currentThread().getName()+" " +test.test);
       
test.test = 20;
//        try {
           
System.out.println(Thread.currentThread().getName()+" test.test= 20;");
 
System.out.println(Thread.currentThread().getName()+""+test.test);
   
}
}

输出:(这个输出随机性很大,运行很多次才偶然得到)

MyRunnable 5

MyRunnable test.test = 20;

MyRunnable 20

MyThread 5

MyThread test.test = 10;

MyThread 睡觉

MyThread 10

MyThread 10

结果说明:在main线程创建了线程MyThread和MyRunnable两个线程,实例test的域原始值是5。由输出可以知道,MyRunnable线程先执行完之后MyThread线程才开始执行,可以看到,在MyRunnable线程中,我们把test对象的域改为了20,但是在MyThread线程中打印出来的值还是5,要么是MyRunnable的值没写回内存,要么就是MyThread的本地值是一开始test域没改之前的值(没更新MyRunnable改过的值)。这就很好的印证了上面那幅图

原子性

如果学过操作系统或者数据库都会有这样的概念,在生活中,A转100给B执行的语句就是

         A = A -100;         

         B= B+100;

那么在数据库写入的时候就会用事务管理来操作,保证其原子操作,也就是害怕执行了         A = A -100;         

突然故障导致不能往下执行,那么A就会无端端少了100,而B也没有得到这100,原子操作就是要么执行完全部,要么全部不执行,在数据库中,事务管理器针对上面的情况就会恢复A的值,也就是让A重新+100,恢复以前的值。Java的原子性也就是差不多的意思。

例子:

①X = 100;

②Y=X +100;

①  就是原子操作,对基本数据类型的写入和读取都是原子性操作,即这些操作要么不执行,要么执行,不能被打断

②  而就不是原子操作了,因为涉及到X的读取,X+100后赋值给Y,虽然这两个分开就是原子操作,但是结合一起就不是原子操作了,这两个过程可能被打断

原子就是不能再分的最小的例子,所以对应操作也是不能再分和打断

java.util.concurrent.atomic包含AtomicBoolean,AtomicLong和AtomicReference这些原子类仅供开发并发工具的系统程序员使用,应用程序员不应该使用这些类。

可见性

其实可见性在上面的代码例子中已经可以体现,意思就是线程1修改了共享变量的值之后,其他的线程可以知道这个变量值已经修改,并且能够得到这个新值

有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性,更多的了解可以看这里

重排序并不是Java特有的,在操作系统也是会有的,为了性能优化而重排序是很正常的

Volatile关键字

当一个变量定义为volatile之后,它具备两项特性

①可见性

②禁止指令重排序优化

可见性demo:

public class Main {

    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread();

        myThread.start();
        Thread.currentThread().sleep(1000);
        System.out.println("sleep 完成");
        myThread.setFlag(true);

    }}
public class MyThread extends Thread {

    boolean flag = false;

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
    @Override
    public void run() {
        while (!flag) {
        }
    }
}

结果:

结果说明:

每个线程都有自己的工作(本地内存)内存,myThread线程读取了flag的值到自己的工作内存中,拷贝了一份,然后一直进入死循环,而当main线程也有自己的工作内存,它拿到了flag的值,改变了flag的值,但是或许还没来得及将它写会到住内存中就已经结束了,而myThread一直不知道flag的值已经改变,所以一直循环下去

加上volatile关键字

volatile boolean flag = false;

①  使用volatile修饰的变量,当变量改变之后,会立马强制写会内存

②  当某个线程修改了这个变量,其他线程使用前,会直接从内存中读取一次

 Volatile不具有原子性(一般)

public class Main {

    volatile static int flag = 0;
    public static void increment() {
        flag++;
    }

    public static void main(String[] args) throws InterruptedException {

        final Main test = new Main();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                        test.increment();
                };
            }.start();
        }
        //保证前面的线程都执行完
        while(Thread.activeCount()>2)
            Thread.yield();
        System.out.println(test.flag);

    }
}

这里是jdk1.8所以while(Thread.activeCount()>2)如果还是1.7的话就改为大于1

输出的结果大多数情况下都是小于10000,因为自增操作是不具备原子性的,它包括读取变量的原始值、进行加1操作、写入工作内存。那么就是说自增操作的三个子操作可能会分割开执行。

假如某个时刻变量inc的值为10,线程1对变量进行自增操作,线程1先读取了变量inc的原始值,然后线程1被阻塞了;然后线程2对变量进行自增操作,线程2也去读取变量inc的原始值,由于线程1只是对变量inc进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量inc的缓存行无效,所以线程2会直接去主存读取inc的值,发现inc的值时10,然后进行加1操作,并把11写入工作内存,最后写入主存。然后线程1接着进行加1操作,由于已经读取了inc的值(不会再次去主存读数据),注意此时在线程1的工作内存中inc的值仍然为10,所以线程1对inc进行加1操作后inc的值为11,然后将11写入工作内存,最后写入主存。那么两个线程分别进行了一次自增操作后,inc只增加了1。

自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。

正确使用 volatile 变量的条件

您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:

·       对变量的写操作不依赖于当前值。

·       该变量没有包含在具有其他变量的不变式中。

实际上,这些条件表明,可以被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。

第一个条件的限制使 volatile 变量不能用作线程安全计数器。虽然增量操作(x++)看上去类似一个单独操作,实际上它是一个由读取-修改-写入操作序列组成的组合操作,必须以原子方式执行,而 volatile 不能提供必须的原子特性。实现正确的操作需要使 x 的值在操作期间保持不变,而 volatile 变量无法实现这点。(然而,如果将值调整为只从单个线程写入,那么可以忽略第一个条件。)

大多数编程情形都会与这两个条件的其中之一冲突,使得 volatile 变量不能像 synchronized 那样普遍适用于实现线程安全。清单 1 显示了一个非线程安全的数值范围类。它包含了一个不变式 —— 下界总是小于或等于上界。

参考:

http://www.jianshu.com/p/4377b3245a2c

http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

作者:lijinxiong520 发表于2016/9/13 22:24:40 原文链接
阅读:79 评论:0 查看评论

Android学习-新闻客户端养成记(二)

$
0
0

新闻客户端需要的 布局文件

                                          
                            新闻主界面                   侧滑菜单界面                                图片新闻界面                      视频列表界面
                       
                            天气预报界面                    新闻详情界面    

以上几个界面是项目所需要的核心界面,布局摆放并不怎么难,为了比例协调,我用了很多的layout_weight 属性 ;

控制界面mainWindows.xml  这是项目的主界面布局文件
<pre name="code" class="java"><?xml version="1.0" encoding="utf-8"?>
//<strong style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"></strong><pre name="code" class="java" style="display: inline !important;">DrawerLayout  控件可轻松实现侧滑功能 

<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/drawerLayout" android:layout_width="match_parent" android:layout_height="match_parent" >  
   //碎片会显示在这里
    <FrameLayout
        android:id="@+id/frame_content"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >
    </FrameLayout>

    <!-- android:layout_gravity="start" -->

    <LinearLayout
        android:id="@+id/menuLayout"
        android:layout_width="200dp"
        android:layout_height="match_parent"
     
        android:layout_gravity="start"   //决定抽屉菜单的位置  start 为左边
        android:background="@color/menu_layout_bg"
        android:clickable="false"
        android:orientation="vertical"
        android:padding="10dp" >

        <RelativeLayout
            android:id="@+id/menuTop"
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:orientation="horizontal" >

            <ImageButton
                android:id="@+id/userImg"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_margin="10dp"
                android:background="@drawable/user_img_shape"
                android:contentDescription="@string/app_name"
                android:src="@drawable/people1" />

            <TextView
                android:id="@+id/userName"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignTop="@id/userImg"
                android:layout_margin="10dp"
                android:layout_toRightOf="@id/userImg"
                android:text="@string/unLogin"
                android:textSize="20sp" />
        </RelativeLayout>

        <LinearLayout
            android:id="@+id/menuMain"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@drawable/menu_btn_shape"
            android:clickable="true"
            android:orientation="horizontal"
            android:paddingBottom="10dp"
            android:paddingTop="10dp" >

            <ImageView
                android:id="@+id/img1"
                android:layout_width="50dp"
                android:layout_height="wrap_content"
                android:contentDescription="@string/app_name"
                android:src="@drawable/menu_main" />

            <TextView
                android:id="@+id/tv1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="40dp"
                android:text="@string/menu_main"
                android:textAppearance="?android:attr/textAppearanceMedium" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#000" />

        <LinearLayout
            android:id="@+id/menuPic"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@drawable/menu_btn_shape"
            android:clickable="true"
            android:orientation="horizontal"
            android:paddingBottom="10dp"
            android:paddingTop="10dp" >

            <ImageView
                android:id="@+id/imageView1"
                android:layout_width="50dp"
                android:layout_height="wrap_content"
                android:contentDescription="@string/app_name"
                android:src="@drawable/menu_pic_icon" />

            <TextView
                android:id="@+id/textView1"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="40dp"
                android:text="@string/menu_picture"
                android:textAppearance="?android:attr/textAppearanceMedium" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#000" />

        <LinearLayout
            android:id="@+id/menuVideo"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@drawable/menu_btn_shape"
            android:clickable="true"
            android:orientation="horizontal"
            android:paddingBottom="10dp"
            android:paddingTop="10dp" >

            <ImageView
                android:id="@+id/imageView2"
                android:layout_width="50dp"
                android:layout_height="wrap_content"
                android:contentDescription="@string/app_name"
                android:src="@drawable/menu_video_icon" />

            <TextView
                android:id="@+id/textView2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="40dp"
                android:text="@string/menu_video"
                android:textAppearance="?android:attr/textAppearanceMedium" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#000" />

        <LinearLayout
            android:id="@+id/menuWeather"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@drawable/menu_btn_shape"
            android:clickable="true"
            android:orientation="horizontal"
            android:paddingBottom="10dp"
            android:paddingTop="10dp" >

            <ImageView
                android:id="@+id/imageView3"
                android:layout_width="50dp"
                android:layout_height="wrap_content"
                android:contentDescription="@string/app_name"
                android:src="@drawable/menu_weather_icon" />

            <TextView
                android:id="@+id/textView3"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="40dp"
                android:text="@string/menu_weather"
                android:textAppearance="?android:attr/textAppearanceMedium" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#000" />

        <LinearLayout
            android:id="@+id/menuCollect"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@drawable/menu_btn_shape"
            android:clickable="true"
            android:orientation="horizontal"
            android:paddingBottom="10dp"
            android:paddingTop="10dp" >

            <ImageView
                android:id="@+id/collectIv"
                android:layout_width="50dp"
                android:layout_height="wrap_content"
                android:contentDescription="@string/app_name"
                android:src="@drawable/menu_collect" />

            <TextView
                android:id="@+id/collectTv"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="40dp"
                android:text="@string/menu_collect"
                android:textAppearance="?android:attr/textAppearanceMedium" />
        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="#000" />

        <LinearLayout
            android:id="@+id/menuMore"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@drawable/menu_btn_shape"
            android:clickable="true"
            android:orientation="horizontal"
            android:paddingBottom="10dp"
            android:paddingTop="10dp" >

            <ImageView
                android:id="@+id/imageView4"
                android:layout_width="50dp"
                android:layout_height="wrap_content"
                android:contentDescription="@string/app_name"
                android:src="@drawable/menu_more" />

            <TextView
                android:id="@+id/textView4"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_vertical"
                android:layout_marginLeft="40dp"
                android:text="@string/menu_more"
                android:textAppearance="?android:attr/textAppearanceMedium" />
        </LinearLayout>
    </LinearLayout>

</android.support.v4.widget.DrawerLayout>



新闻主页布局文件activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/LinearLayout1"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="${relativePackage}.${activityClass}" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="2.5"
        android:background="#FFF"
        android:gravity="center_vertical"
        android:orientation="horizontal" >

        <HorizontalScrollView
            android:id="@+id/horizontalScrollView1"
            android:layout_width="0dp"
            android:layout_height="match_parent"
            android:layout_weight="1"
            android:scrollbars="none" >

            <LinearLayout
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center"
                android:orientation="horizontal" >

                <eNews.customview.ActionBarView 
 
                    android:id="@+id/actionBar"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:gravity="center" >
                </eNews.customview.ActionBarView>
            </LinearLayout>
        </HorizontalScrollView>

        <TextView
            android:id="@+id/channelManageBtn"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:gravity="center"
            android:text="@string/add_channel"
            android:textSize="25sp" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:background="#dadada" >
    </LinearLayout>

    <ScrollView
        android:id="@+id/scrollView1"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="23.5" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical" >

            <android.support.v4.view.ViewPager
                android:id="@+id/topViewPager"
                android:layout_width="match_parent"
                android:layout_height="200dp" >
            </android.support.v4.view.ViewPager>

            <eNews.customview.NewsListView      <span style="font-family: Arial, Helvetica, sans-serif;">//使用自定义ListView 避免与 </span><span style="font-family: Arial, Helvetica, sans-serif;">HorizontalScrollView 冲突</span>

                android:id="@+id/newsListView"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="@color/list_view_bg"
                android:divider="@color/list_view_divier"
                android:dividerHeight="1dp"
                android:padding="5dp" >
            </eNews.customview.NewsListView>
        </LinearLayout>
    </ScrollView>

</LinearLayout>

图片新闻布局文件picture_news.xml

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

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="6"
        android:background="@drawable/actionbar_shape"
        android:orientation="horizontal" >

        <ImageButton
            android:id="@+id/backBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_margin="5dp"
            android:background="@drawable/back_btn_shape"
            android:gravity="center" />

        <TextView
            android:id="@+id/textView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:gravity="center"
            android:text="@string/picture_title"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:textColor="#FFF" />
    </RelativeLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="4.5"
        android:background="#FFF"
        android:gravity="center" >

        <eNews.customview.ActionBarView
            android:id="@+id/pictureNewsactionBar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center" >
        </eNews.customview.ActionBarView>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="2dp"
        android:background="#dadada" >
    </LinearLayout>
   //图片新闻列表
    <ListView
        android:id="@+id/pictureList"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="51"
        android:divider="@color/chanel_item_color"
        android:dividerHeight="1dp" >
    </ListView>

</LinearLayout>

视频新闻布局


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

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="6"
        android:background="@drawable/actionbar_shape"
        android:orientation="horizontal" >

        <ImageButton
            android:id="@+id/backBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginLeft="10dp"
            android:background="@drawable/back_btn_shape"
            android:gravity="center"
            android:padding="5dp" />

        <TextView
            android:id="@+id/textView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:gravity="center"
            android:text="@string/video_title"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:textColor="#FFF" />
    </RelativeLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="4.5"
        android:background="#FFF"
        android:gravity="center" >
        <eNews.customview.ActionBarView
            android:id="@+id/videoNewsactionBar"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center" >
        </eNews.customview.ActionBarView>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="2dp"
        android:background="#dadada" >
    </LinearLayout>

    <ListView
        android:id="@+id/videoList"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="51.5" >
    </ListView>

</LinearLayout>


图片详情布局
<?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" >

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="@drawable/actionbar_shape"
        android:orientation="horizontal" >

        <ImageButton
            android:id="@+id/backBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_margin="5dp"
            android:background="@drawable/back_btn_shape"
            android:gravity="center" />

        <TextView
            android:id="@+id/textView1"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:gravity="center"
            android:text="@string/picture_detail_title"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:textColor="#FFF" />

        <ImageButton
            android:id="@+id/actionbar_more"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:layout_margin="5dp"
            android:background="@drawable/actionbar_more_icon"
            android:gravity="center" />
    </RelativeLayout>

    <android.support.v4.view.ViewPager
        android:id="@+id/pictureViewPager"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="9" >
    </android.support.v4.view.ViewPager>

</LinearLayout>


新闻详情布局
<?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" >

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:background="@drawable/actionbar_shape"
        android:orientation="horizontal" >

        <ImageButton
            android:id="@+id/backBtn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_margin="5dp"
            android:background="@drawable/back_btn_shape"
            android:gravity="center" />

        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:gravity="center"
            android:text="@string/news_detail_title"
            android:textAppearance="?android:attr/textAppearanceMedium"
            android:textColor="#FFF" />
        <ImageButton
            android:id="@+id/actionbar_more"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_alignParentRight="true"
            android:layout_margin="5dp"
            android:background="@drawable/actionbar_more_icon"
            android:gravity="center" />
    </RelativeLayout>

    <ScrollView
        android:id="@+id/scrollView1"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="9" >

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical" >

            <TextView
                android:id="@+id/news_detail_text"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </LinearLayout>
    </ScrollView>

</LinearLayout>


自定义ListView 控件 避免与scrollView 冲突
package eNews.customview;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.ListView;

/**
 * 
 * @author 王凯
 * @date 2016-9-12 新闻列表控件
 */
public class NewsListView extends ListView {

	public NewsListView(Context context, AttributeSet attrs) {
		super(context, attrs);
		// TODO Auto-generated constructor stub
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		// TODO Auto-generated method stub
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	}
}
源代码下载
做好以上布局用以后,还是不能显示出数据,因为还没有为列表控件做适配器,现在只是一个空壳而已




作者:w1143408997 发表于2016/9/13 22:36:09 原文链接
阅读:108 评论:0 查看评论

Android之sdcard保存数据

$
0
0

Android中的sdcard是一个外部存储目录,是一个应用程序的私有目录,只有当前应用程序有权限访问读写,其他应用无权限访问。一般用来存放一些安全性不高,但比较大的数据。

使用Sdcard注意事项:
1.权限问题:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
2.通过 Environment获取sdcard的路径:

Environment.getExternalStorageDirectory().getPath();

3.使用前需要判断sdcard状态
if(!Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED)){
 //sdcard状态是没有挂载的情况
Toast.makeText(mContext, "sdcard不存在或未挂载", Toast.LENGTH_SHORT).show();
return ;
} 


4.需要判断sdcard剩余空间:
File sdcard_file = Environment.getExternalStorageDirectory(); //获得sdcard目录对象
long usableSpace = sdcard_file.getUsableSpace(); //获取该目录下可用空间的大小
//将long类型的文件大小格式转换为M或G
String usableSpaceSize = Formatter.formatFileSize(this, usableSpace);
//假如下载一个电影需要100M,用户手机不足100M,则提示用户
if (usableSpace < 100*1024*1024) {
Toast.makeText(mContext, "sdcard剩余空间不足,剩余空间为:usableSpaceSize", 0).show();
return;
}

5.下面通过模拟一个登录页面,并实现保存用户名和密码的小案例,来详细介绍sdcard存储数据的过程:

①布局文件代码:

<span style="font-size:18px;"><LinearLayout 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=".MainActivity"
    android:orientation="vertical" >

    <EditText
        android:id="@+id/et_username"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="请输入用户名"/>
    <EditText
        android:id="@+id/et_password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="15dp"
        android:layout_marginBottom="15dp"
        android:inputType="textPassword"
        android:hint="请输入密码"/>

    <RelativeLayout 
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <CheckBox 
            android:id="@+id/cb_rem"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:text="记住密码"/>
        <Button 
            android:id="@+id/btn_login"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:text="登 录"/>
    </RelativeLayout>
</LinearLayout></span>


②MainActivity的代码:

<span style="font-size:18px;">package com.example.savedatatosd;

import java.io.File;
import java.util.Map;

import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.os.Environment;
import android.text.TextUtils;
import android.text.format.Formatter;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

public class MainActivity extends Activity implements OnClickListener {

	private EditText et_password;
	private EditText et_username;
	private CheckBox cb_rem;
	private Button btn_login;
	private Context mContext;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		mContext = this;
		//初始化控件
		et_password = (EditText) findViewById(R.id.et_password);
		et_username = (EditText) findViewById(R.id.et_username);
		cb_rem = (CheckBox) findViewById(R.id.cb_rem);
		btn_login = (Button) findViewById(R.id.btn_login);
		
		//给登录按钮设置监听事件
		btn_login.setOnClickListener(this);
		
		//再次进入应用时,先判断用户是否点击了保存按钮,如果保存的话,则回显出保存的内容
		Map<String, String> userInfo = UserInfoUtils.getUserInfo(); 
		if (userInfo != null) { //用户信息不为空,则说明保存了信息
			et_username.setText(userInfo.get("username"));
			et_password.setText(userInfo.get("password"));
			//设置按钮为选择状态
			cb_rem.setChecked(true);
		}
	}

	@Override
	public void onClick(View v) {
		switch (v.getId()) {
		case R.id.btn_login:
			//获取用户输入的内容
			String password = et_password.getText().toString().trim();
			String username = et_username.getText().toString().trim();
			//记录CheckBox的选中状态
			boolean isChecked = cb_rem.isChecked();
			
			//1,判空
			if (TextUtils.isEmpty(password) || TextUtils.isEmpty(username)) {
				Toast.makeText(mContext, "用户名密码不能为空", 0).show();
				return;
			}
			//2,判断可用空间是否满足存储数据
			File sdcard_file = Environment.getExternalStorageDirectory(); //获得sdcard目录对象
			long usableSpace = sdcard_file.getUsableSpace(); //获取该目录下可用空间的大小
			//将long类型的文件大小格式转换为M或G
			String usableSpaceSize = Formatter.formatFileSize(this, usableSpace);
			//假如下载一个电影需要100M,用户手机不足100M,则提示用户
			if (usableSpace < 100*1024*1024) {
				Toast.makeText(mContext, "sdcard剩余空间不足,剩余空间为:usableSpaceSize", 0).show();
				return;
			}
			
			//如果上面的两个条件都满足,则执行下面的代码
			//如果CheckBox被选中,则保存用户信息
			if (isChecked) {
				//因为要保存到sdcard上,所以先判断sdcard是否挂载了
				if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
					//如果sdcard每个挂载,则提示用户
					Toast.makeText(mContext, "sdcard没有挂载或不可用", 0).show();
				}
				
				boolean result = UserInfoUtils.saveUserInfo(username, password);
				if (result) {
					Toast.makeText(mContext, "用户名密码保存成功", 0).show();
				}else {
					Toast.makeText(mContext, "用户名密码保存失败", 0).show();
				}
			}
			break;
		}
	}
}</span>

③实现往sdcard中存、数据的工具类:

<span style="font-size:18px;">package com.example.savedatatosd;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import android.os.Environment;

public class UserInfoUtils {

	/*
	 * 保存用户信息到sdcard
	 * 参数为用户输入的用户名和密码
	 */
	public static boolean saveUserInfo(String username, String password) {
		try {
			//封装传过来的用户名和密码,"##"为分割符,一般用正则表达式或file.separator()
			String userInfo = username + "##" + password;
			//指定保存的路径(通过API获得)
			String path = Environment.getExternalStorageDirectory().getPath();
			System.out.println(path);//我用的是genymotion虚拟机,保存路径为: /storage/emulated/0
			//创建file
			File file = new File(path, "userinfo.txt");
			//创建文件输出流,把文件写到sdcard
			FileOutputStream fos = new FileOutputStream(file);
			fos.write(userInfo.getBytes());
			fos.close();
			return true; //如果保存成功,则返回true
		} catch (Exception e) {
			e.printStackTrace();
		}
		return false;
	}
	/*
	 * 从sdcard中取出数据
	 */
	public static Map<String, String> getUserInfo(){
		try {
			//读取的路径、文件和保存时的相同
			//指定保存的路径
			String path = Environment.getExternalStorageDirectory().getPath();
			//创建file
			File file = new File(path, "userinfo.txt");
			//使用缓冲流把数据读出(也可以使用内存流),这里使用缓存流,是因为其有readLine方法,可以逐行读取
			FileInputStream fis = new FileInputStream(file);
			BufferedReader br = new BufferedReader(new InputStreamReader(fis));
			//读取一行
			String readLine = br.readLine();
			//对读取的内容按照标记进行分割
			String[] split = readLine.split("##");
			//把分割后的数据保存到HashMap集合中
			HashMap<String, String>map = new HashMap<String, String>();
			map.put("username", split[0]);
			map.put("password", split[1]);
			//关流
			br.close();
			fis.close();
			//把map返回回去
			return map;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
}</span><span style="font-size:14px;">
</span>

④输入用户名和密码,选中“记住密码”,点击“登录按钮”,退出后,下次再进入用户名和密码便会回显出来,说明往sdcard中存取数据的功能实现了,效果图如下:




作者:ljw124213 发表于2016/9/13 22:40:21 原文链接
阅读:86 评论:0 查看评论

UGUI内核大探究(三)输入模块

$
0
0

UGUI内核大探究(一)EventSystem我们探究了事件系统,UGUI内核大探究(二)执行事件中我们介绍了事件是如何执行的。那么事件是如何产生的呢?这就涉及到BaseInputModule、PointerInputModule、StandaloneInputModule、TouchInputModule这些类。今天我们就探究输入模块的原理。

按照惯例,附上UGUI源码下载地址

BaseInputModule是一个抽象类,是所有输入模块类的基类。PointerInputModule也是一个抽象类,继承自BaseInputModule,是StandaloneInputModule和TouchInputModule的基类。而StandaloneInputModule是在PC、Mac&Linux上的具体实现,而TouchInputModule是在IOS、Android等移动平台上的具体实现。之所以这种继承结构,我认为是向开发者提供了扩展,方便开发者实现新型设备的输入模块。

UGUI内核大探究(二)执行事件里我们讲到,EventInterface声明了很多接口,每个接口都对应着一个事件。这些事件是由输入模块产生的,而归根结底大部分是通过Input这个类的各种属性和静态方法获取了数据才生成了事件。


当鼠标或触摸进入、退出当前对象时执行pointerEnterHandler、pointerExitHandler。

在鼠标或者触摸按下、松开时执行pointerDownHandler、pointerUpHandler

在鼠标或触摸松开并且与按下时是同一个响应物体时执行pointerClickHandler

在鼠标或触摸位置发生偏移(偏移值大于一个很小的常量)时执行beginDragHandler

鼠标或者触摸按下且当前对象可以响应拖拽事件时执行initializePotentialDrag

对象正在被拖拽且鼠标或触摸移动时执行dragHandler

对象正在被拖拽且鼠标或触摸松开时执行endDragHandler

鼠标或触摸松开且对象未响应pointerClickHandler情况下,如果对象正在被拖拽,执行dropHandler

当鼠标滚动差值大于零执行scrollHandler

当输入模块切换到StandaloneInputModule时执行updateSelectedHandler。(不需要Input类)

当鼠标移动导致被选中的对象改变时,执行selectHandler和deselectHandler。

导航事件可用情况下,按下上下左右键,执行moveHandler,按下确认键执行submitHandler,按下取消键执行cancelHandler

关于如何获取当前对象,参考UGUI内核大探究(一)EventSystem

本文没有贴出任何源码,一个原因是输入模块本身并不直接参与到UI交互当中去(通过EventSystem),我认为Unity官方也不希望开发者直接使用它们。另一个原因是四个类中并没有大段的结构型代码,都是一些细节的具体实现,理解起来很容易,分析起来很乏味(实现起来很痛苦)

作者:ecidevilin 发表于2016/9/13 22:45:22 原文链接
阅读:95 评论:0 查看评论

GPS-coordinate-system

$
0
0

本文主要介绍关于GPS及其相关数据计算方法,GPS经常用于移动机器人及其他移动过程中的定位和地图显示。本文先介绍一些关于GPS参考坐标等相关知识。

GPS 数据简介

实际的GPS系统会以一定格式输出很多数据,如时间,精度因子,卫星编号,信噪比等等,但对于无人机控制而言,最为重要,也是最常使用的还是经度(Longitude),纬度(Latitude)以及高度(height)三组数据。GPS的接收机接受到的数据为经度纬度和高度,高度可以进一步转化为相对为地理大地水准面(geoid (e.g., EGM96)(essentially, mean sea level))的高度。

直接通过GPS获取的飞行器的位置坐标基于WGS-84(World Geodetic System-1984)坐标系,简称Geodetic或G坐标系。

PG=[λ,ϕ,h]T

为什么需要单独建立一个坐标系呢?地球表面地势复杂,有山有海,高低不平。需要建立一个简单而精确的近似数学模型,大家决定采用椭球体作为地球的近似。而G坐标系就描述了一个椭球体。

相关坐标系

Inertial Frame

该坐标系表示牛顿定律作用的坐标系。因此一个惯性坐标系没有加速度,只有一个人匀速度。惯性坐标系是任意的,惯性传感器输出的测量值是相对于该坐标系的。

ECI

地心惯性坐标系(Earth centered inertial (ECI) frame)在特定初始时间,以地心为原点,the inertial x and z axes point toward the vernal equinox and along the Earth spin axis。注意ECEF相对该坐标系一角速度
ωiie=[0,0,ωie]

ωis(1+365.25cycle365.2524hr)(2πrad/cycle3600sec/hr)

ECEF

一种以地心为原点的地固坐标系(也称地球坐标系),是一种笛卡儿坐标系。原点 O (0,0,0)为地球质心,z 轴与地轴平行指向北极点,x 轴指向本初子午线与赤道的交点,y 轴垂直于xOz平面(即东经90度与赤道的交点)构成右手坐标系。如下图所示。

ECEF Rectangular Coordinates

[x,y,z]系统来表示该系统,

The Earth Geoid and Gravity Model 大地水准面和重力模型

重力是万有引力和离心力的合力。这样表述不太准确,确切的说地球表面的物体跟随地球自传的角加速度并没有指向地心。离心力在赤道上最大,在两极最小为0。geodetic surface定义为处处垂直与重力。他和实际的地球表面不同,他可以用一个理想椭球体近似。



理想椭球体的重心在地球的质心重合,长半轴和地球的自转轴重合。

Geodetic坐标系

大部分导航设备输出的数据 ϕ λh,也就是经度纬度和高度。

Name Sysmbol Value Units
Equatorial radius a 6378137 m
Reciprocal flattening 1f 298.257223563
Angular rate ωie 7.292115 x 10~5 rad/s
Gravitational constant GM 3.986004418 x 1014 m2s2

由此可以可得


对于任意的一个机器,geoid高度N 就是沿着椭球的法向量从重心到表面的距离。
orthometric height H表示高出geoid的部分,实际海拔是altitude or geodetic height can be expressed as h=H+N. 给出该椭球体的基本参数:长半轴,短半轴,第一偏心率,第二偏心率,扁率,曲率半径(米):
a=6378137; b=a(1f);
第一偏心率

c=a2b2a2

第二偏心率
c=a2b2b2

GPS输出的高度不是海拔(Alt)么?这里怎么是h高度呢?GPS硬件直接获取的高度是相对于G坐标系中椭球表面的高度。而海拔是相对于公海平面的高度,它与地球表面形状和重力分布相关。相对于大地水准面的高度才是海拔,也就是图中的H参数。

hH+M

M(大地水准偏差)作为GPS输出高度h和当地海拔之间的偏差,一般在正负100m以内。

从Geodetic到ECEF坐标系的数据转换

我们通过G坐标系下的三个参数:经度,维度,高度,可以获得飞行器在椭球表面的位置坐标。但进行导航计算时,我们需要把数据换算到NED坐标系下。要完成从G系到NED的数据转换还需要一个过渡过程:G坐标系到ECEF坐标系下的数据转换。

PE=[XE,YE,ZE]

N=a1e2sin2ϕ

PTE=(N+h)cosϕcosλ(N+h)cosϕsinλ(N(1e2)+h)sinψ

其中N是我们常说的曲率半径(m)。通过上面的计算公式就可以实现从G坐标系到ECEF坐标的数据转换。

从ECEF到NED坐标系的数据转换

对于商用无人机,相比于它在椭球中的信息,我们更关心它在平面中的位置向量、速度向量。将NED坐标系看做导航中最重要的坐标系并不为过,NED坐标系也经常被直接称为导航坐标系(Navigation coordinate)或者地面坐标系(ground coordinate)。

ecef

首先要获取NED坐标系中的参考原点,一般也就是无人机GPS星数达到要求后的起始位置。这也是为什么无人机产品要在星数足够之后才能起飞,试想一下如果起始位置没有定准,就算在飞行过程中星数足够,获取的飞行位置信息也够精确,但结果却可能造成一键返航位置与起始位置偏差巨大。
下面给出参考原点的坐标信息以及从ECEF到NED的转换计算:

PE,ref=[XE,ref,YE,ref,ZE,ref]

PN=RN/E(PEPE,ref)

从ECEF到NED坐标系的旋转矩阵如下:
结合上面两部分的计算方法,成功地实现了GPS输出的位置数据到NED坐标系下的转换。换句话说,我们获得了进行无人机控制器设计所必须的外环位置状态信息。
但外环信息一共有六个,还有三个速度状态呢?通过GPS可以获得ECEF坐标系下的速度向量。与G系类似,相比于飞行器在ECEF这个三维坐标系的速度向量,我们更关心相对于NED坐标系的速度向量:

地理坐标系和地心坐标系

前者的z轴垂直与椭球面向下(这样并没有指向地心当然在赤道上除外,之后的文章中会解释)后者的z轴指向地心,两者均随着地球运动,x轴指向正北(true north),y轴指向正东。地理坐标系的原点随着机器运动,因此3轴也在旋转,注意true north and magnetic north是两个不同的方向,

The local geodetic frame

north, east, down rectangular coordinate system。 该切平面固定与地球表面的一点,也就是坐标系的原点,x指向北,z指向下垂直与椭球面,y轴指向东。因此该坐标系经常用于静止的系统是,地理坐标系和该坐标系是重合的,如果系统是运动的,该坐标系是固定的。该坐标系经常用于局部导航吸引。

## 基体坐标系

基体坐标系固定于机器上,经常原点位于重心,x轴指向前方roll,z轴向下yaw,y指向右pitch。

height relative to an ellipsoidal Earth model

earth ellipsoid

WGS 80 vs ECEF, ENU

Convert geodetic coordinates to Earth-centered Earth-fixed (ECEF) coordinates
mathworks

p = lla2ecef(lla)
p = lla2ecef(lla, model)
p = lla2ecef(lla, f, Re)

Calculate Earth-centered Earth-fixed (ECEF) position from geodetic latitude, longitude, and altitude above planetary ellipsoid

mathworks
cef-coordinate-systems-gps
Note that the extension of the normal towards the interior of the ellipsoid will not intersect the center of the Earth except in special cases such $$
reference-frame-transformations-gps

refer

  1. https://en.wikipedia.org/wiki/ECEF
  2. http://what-when-how.com/gps-with-high-rate-sensors/specific-vector-transformations-gps-part-1/
作者:wendox 发表于2016/9/13 22:54:55 原文链接
阅读:53 评论:0 查看评论

Android7.0写给开发者的一封信(官网同步翻译)

$
0
0

Android7.0写给开发者的一封信(官网同步翻译)

版权声明:转载必须注明本文转自严振杰的博客: http://blog.yanzhenjie.com

先说明这个标题和内容的关系,主要是Android7.0对于开发者的说明的文章,利用Google翻译,加上一点自己的理解,第一次做这种羞羞的事,做的不好地方大家将就着看看,不喜欢的请轻喷。

Android N(Nougat牛轧糖)在2016年8月22如期发布,在它正式发布前就向SDK推送了Deveoper Preview,Android N的很多新特性也被广大的开发者朋友挖掘出来了,那么官网上也有几篇关于Android N的变化的文章,下面就和大家一起来学习一下Android N的一些先关知识。

。如果你自认为你的英语还可以,推荐你阅读官网的文章:

如果想了解更多关于Android7.0的知识,可以阅读Android 7.0行为变化(官网同步翻译)


Android N为开发者介绍新功能

Android N目前任然是开发状态,但现在Android N Developer Preview已经可以进行试用了。以下部分重点介绍面向开发者的一些新功能。

请务必查阅行为变更(第二章节)以了解平台行为变更可能影响到你的App的地方,看看开发者指南,了解有关关键功能的更多信息,并下载Sample参考后获取新API的详细信息。

多窗口支持

在 Android N 中,Google为该平台引入了一个新的而且非常需要的多任务处理功能 —— 多窗口支持。

现在,用户可以一次在屏幕上打开两个应用。

  • 在运行Android N的手机和平板电脑上,用户可以并排运行两个应用,或者处于分屏模式时一个应用位于另一个应用之上。用户可以通过手指拖动两个应用之间的分隔线来调整应用的边框大小。
  • 在Android TV设备上,应用可以将自身置于画中画模式,从而让它们可以在用户浏览或与其他应用交互时继续显示内容。

多窗口支持为我们提供了新的吸引用户的方式,特别是在平板电脑和其他更大屏幕的设备上。 你甚至可以在应用中启用拖放,从而使用户可以方便地将内容拖放到你的应用或从你的APP中拖出内容 —— 这个操作是一个增强用户体验的好方法。

在你的应用中添加多窗口支持并配置多窗口显示的处理方式也不难。比如,你可以指定你的Activity允许缩放的最小尺寸,从而防止用户用手指拖动调整APP边框时将Activity调整的过于小。当然,你也可以禁用APP的多窗口显示,这么可以确保你的APP一直多整屏显示。至于详细的代码实现请参考开发者文档

在分屏模式下运行的应用一个例子,中间有一根黑色的嫌,拖动可以调整APP在屏幕上的大小:

在分屏模式下运行的应用

增强通知功能

在Android N中,Google又抛弃了在L和M中的卡片式,Google重新设计了通知,使其更加易用并且速度更快。变更的内容包括:

  • 模板更新:Google更新的通知模板中,更加注重整体样式和头像(hero image and avatar,我理解可能不太对,欢迎大家指正)。开发者只需调整少量的代码就能充分利用新模板。
  • 消息样式自定义:你可以使用MessageStyle类自定义更多与你的通知相关的用户界面标签。你也可以配置消息、会话标题和内容视图。
  • 捆绑通知:系统会将消息组合在一起(例如,按消息主题)并显示组。用户可以适当地进行Dismiss或Archive等操作。如果你在Android Wear已实现这样的通知,我想你已经很熟悉这种模式了。
  • 直接回复:对于实时通信的App(类似微信、WhatsApp),Android N系统支持在通知栏直接回复,以便用户可以直接在通知界面中快速回复短信。
  • 自定义视图:你可以使用两个新的API在通知中使用利用系统装饰元素自定义视图,如通知标题和操作。

具体如何实现新通知,请参阅通知指南。下面是快速回复的预览图。

快速回复1:
快速回复1

快速回复2:
快速回复2

快速回复3:
快速回复3

Profile-guided JIT/AOT编译

在Android N中,系统添加了Just in Time (JIT)编译器,对ART进行代码分析,让它可以在应用运行时持续提升Android应用的性能。JIT编译器对Android运行组件当前的Ahead of Time (AOT)编译器进行了补充,有助于提升运行时性能,节省存储空间,加快应用更新和系统更新速度。

Profile-guided编译让Android运行组件能够根据应用的实际使用以及设备上的情况管理每个应用的 AOT/JIT编译。例如,ART维护每个APP的热方法的profile,并且可以预编译和缓存这些方法以实现最佳性能。对于应用的其他部分,在实际使用之前不会进行编译。

除提升应用的关键部分的性能外,profile-guided编译还有助于减少整个 RAM占用,包括关联的二进制文件。此功能对于低内存设备尤其重要。

ART在管理profile-guided编译时,可最大程度降低对设备电池的消耗。仅当设备处于空闲状态或者充电时才进行编译,从而可以通过提前执行该工作节约时间和省电。

快速的应用安装路径

ART的JIT编译器最实际的好处之一是APP安装和系统更新的速度。即使在Android 6.0中需要几分钟进行优化和安装的大型应用,现在只需几秒钟就可以完成安装。因为省去了优化步骤,系统更新也变得更快。

随时随地低电耗模式

Android 6.0时就推出了低电耗模式,即设备处于空闲状态时,通过推迟应用的CPU和网络活动以实现省电目的的系统模式,比如,当设备放在桌子上或你的兜兜里时。

而在Android N中,低电耗模式又优化了一下,即任何时候都可以省电。只要屏幕关闭了一段时间,且设备没有插入电源,低电耗模式就会对应用使用熟悉的CPU和网络限制。它的意思就是说用户即使将设备放入兜兜里也可以省电。

低电耗模式适用于延长电池寿命,即使设备未是非静止状态:

低电耗模式适用于延长电池寿命,即使设备未是非静止状态

屏幕关闭一会儿后,设备在使用电池时,低电耗模式将限制网络连接,同时延迟work和async。在短暂的维护时间范围后,系统会允许应用访问网络,并执行延迟的wrok和async。打开屏幕或将设备插入电源会使设备退出低电耗模式。

当设备再次处于静止状态时,屏幕关闭且使用电池一段时间,低电耗模式针对 PowerManager.WakeLockAlarmManager警报和GPS/Wi-Fi扫描后申请禁用所有的全部CPU和网络。

调整APP的低电耗模式的最佳实践在移动设备和非移动设备上都是一样,因此,如果你已经更新了你的代码合理的处理了低电耗模式,那就没什么好说的。 如果没有,那可以你可以看官网的这篇文章将应用调整到低电耗模式

项目瘦身(Project Svelte)——后台优化

Project Svelte在持续改善,在整个Android生态系统中最大程度的减少Android系统和App使用的RAM。在Android N中,Project Svelte特别侧重优化在后台运行的App。

后台处理是大多数App相当重要的一部分。处理的好,可以实现非常棒的用户体验——即时、快速和情境感知。如果处理不得当,后台处理会毫无必要地消耗RAM(和耗电),同时影响其他App的性能。

自Android 5.0发布以来,JobScheduler(作业调度器)已成为执行后台工作的首选方式,其工作方式有利于用户。应用可以在安排Job的同时允许系统基于内存、电源和连接情况进行优化。JobScheduler可最简洁的实现后台最优控制,Google想要所有App都使用它,JobScheduler将会非常有利于系统的优化。以后我也会出一片关于具体代码实现的系列文章,届时欢迎大家关注。

另一个非常好的选择是 GCMNetworkManager(Google Play 服务的一部分),其在旧版Android中提供类似的Job安排和兼容性。

在Android N中Google继续扩展了JobScheduler和 GCMNetworkManager,以符合更多个用例——例如,现在开发者可以基于Content Providers改变或者安排后台工作。同时,Android N也开始弃用了一些较旧的模式,这些被弃用的模式会降低系统性能,特别是低内存设备的系统性能。

在Android N中,删除了三个常用隐式广播——CONNECTIVITY_ACTIONACTION_NEW_PICTUREACTION_NEW_VIDEO——因为这些广播可能会一次唤醒多个应用的后台进程,同时会耗尽内存和电池。如果你的App现在用到了这些广播,建议你利用Developer Preview调试JobScheduler,用JobScheduler方案是来替换上面的几个广播。

如需了解具体的代码实现详情,请查看后台优化文档

SurfaceView

Android 7.0带来的同步移动SurfaceView类,它提供电池性能在某些情况下比TextureView更好:当渲染视频或3D内容、应用与滚动和移动视频位置时使用更加少的电。

SurfaceView被解析合成到屏幕上时更加省电(battery-efficient),因为它在专用硬件上被解析合成,和App
窗口上的其它内容是相互独立的。结果就是,它想比TextureView在被解析屏幕上的整个过程中消耗非常小。

SurfaceView对象内容的位置是和App内容的位置同步更新的。这个改变的结果是简单的平移、缩放一个Video时不再产生黑条。

在Android7.0开始Google推荐我们使用SurfaceView代替TextureView

Data Saver

在移动设备的整个声明周期中,移动流量(窝蜂数据)划的成本通常会超出设备本身的成本。对于许多用户来说,移动流量(窝蜂数据)是他们最想节约的一个成本。

Android N 推出了 Data Saver模式,这是一项新的系统服务,有助于减少应用使用的移动流量(窝蜂数据),无论是在漫游,账单周期即将结束,还是使用少量的预付费移动流量(窝蜂数据)包。 Data Saver让用户可以控制应用使用移动流量(窝蜂数据)的方式,同时让开发者打开 Data Saver时可以提供更多有效的其它服务。

当设备用移动流量(窝蜂数据)上网时,用户可以在Settings中启用Data Saver,系统会自动屏蔽后台流量消耗,同时通知运行在前台的尽可能使用较少的流量,比如通过限制流媒体服务使用的比特率、降低图片质量、延迟加载、预缓冲等方法来实现。用户可以将特定App加入白名单,这样可允许这些App在后台时使用流量,即使在打开Data Saver时也如是。

Android 7.0扩展了ConnectivityManager,用户可以用来检查Data Saver是否开启或者监听Data Saver的改变状态。 所有应用都应检查用户是否已启用Data Saver并努力限制前台和后台流量消耗。

Vulkan API

Android N将一项新的3D渲染APIVulkan™ 集成到平台中。就像 OpenGL™ ES一样,Vulkan是3D图形和渲染的开放标准,由 Khronos Group维护。

Vulkan从驱动器中最小化CPU开销,并且让你的App更直接地控制GPU操作。 Vulkan还允许多个线程同时执行工作来获得更好的并行化,如命令缓冲区构建。

Vulkan开发工具和库都已经在Android NDK。它们包括:

  • Headers
  • Validation layers (debug libraries)
  • SPIR-V shader compiler
  • SPIR-V runtime shader compilation library

Vulkan 仅适用于已启用Vulkan硬件的设备上的App,比如Nexus 5X、Nexus 6P和NexusPlayer。Google也正在和它的合作厂商积极的合作改善来支持更多的设备。

如需要了解更多信息,请参阅API 文档

快速设置Title的API

“快速设置”通常用于简单直接的从通知栏显示关键设置和操作。在Android 7.0中已扩展“快速设置”的范围,更加方便和快速。

Google为其它的“快速设置”快提供了更多空间,用户可以像操作ViewPager一样通过向左或向右滑动跨分页的显示区域访问它们。同时用户可以通过手指拖拽来控制需要显示哪些“快捷设置”和他们显示的位置。

对于开发者说,Android 7.0添加了一个新的API,从而让我们定义自己的“快速设置”图块,使用户可以轻松访问我们App中的关键控件和操作。也就是说我们可以像系统一样在通知懒下拉后在地方设置像系统一样的图标,让用户可以更加便捷的操作我们应用中的关键功能。

而且我们应该对于用户急需或者频繁使用的的操作使用“快捷设置”图块,并且不应该把“快捷设置”图块当作启动App的快捷方式。

我们在App内定义图块后,用户可以手动的拖放图块到通知栏。

如需创建应用图块的更多信息,请参阅可下载的API参考中的android.service.quicksettings.Tile

号码屏蔽

Android 7.0现在支持在平台中进行号码屏蔽,系统framework提供API,让服务提供商可以维护屏蔽的号码列表。 默认短信应用、默认手机应用和服务提供商应用可以对屏蔽的号码列表进行读取和写入操作。其他应用则无法访问此列表。

通过使号码屏蔽成为平台的标准功能,Android为App提供相同的方式来支持在不同的设备上的号码屏蔽。应用可以利用这个功能的其他优势包括:

  • 屏蔽已屏蔽的来电号码发出的短信
  • 通过Backup & Restore(备份和还原)功能可以在重置系统后和跨设备保留屏蔽的号码
  • 多个应用可以使用相同的屏蔽号码列表

另外,在Android设备上的运营商App可以读取设备上屏蔽的号码列表,并为用户在服务端执行屏蔽,通过任何介质(如VOIP端点或转接电话)阻止用户不需要的来电和短信。

如需了解详细信息,请参阅可下载的API参考中的android.provider.BlockedNumberContract

来电过滤

Android 7.0 允许默认的手机App过滤来电。手机上的App具体操作的方式是实现新的CallScreeningService,该方法允许手机App基于来电的Call.Details执行大量操作,例如:

  • 拒绝来电
  • 不允许来电到达通话记录
  • 不向用户显示来电通知
    如需了解详细信息,请参阅可下载的API 参考中的android.telecom.CallScreeningService

多区域设置支持、多语言

Android 7.0现在允许用户在设置中选择多个区域设置,以更好地支持多语言。App可以使用新的API获取用户的当前的的区域设置,然后为多区域设置的用户提供更成熟的用户体验——例如以多个语言显示搜索结果,并且不会以用户了解的语言翻译网页。

除多区域设置支持外,Android 7.0还扩展了用户可用的语言的范围。它针对常用语言提供超过25种的变体,如英语、西班牙语、法语和阿拉伯语。它还针对100多种新语言添加了部分支持。

App可以通过调用LocaleList.GetDefault()获取用户设置的区域设置列表。为支持扩展的区域设置数量,Android 7.0改变了解析资源的方式。请务必使用新的资源解析逻辑测试和验证你的App是否能如期运行。

如需有关新资源解析行为和应遵循的最佳做法的更多信息,请参阅多语言支持

新增的表情符号(Emojis表情)

Android 7.0引入更多表情符号和表情符号相关功能,包括肤色表情符号和支持变量选择符。如果你的App支持表情符号,请遵循以下准则,以便能充分利用这些表情符号相关功能优势。

  • 在插入之前,检查设备是否包含表情符号。 若要检查系统字体中有哪些表情符号,使用hasGlyph(String)方法。
  • 检查表情符号是否支持变量选择符。变量选择符使您能够呈现一些彩色或黑白的表情符号。在移动设备上,应用应呈现彩色的表情符号,而不是黑白的。但是如果你的App在文本中嵌入表情符号,那你应该使用黑白变量。若要确定表情符号是否有变量,使用变量选择符。如需有关支持变量的字符的完整清单,请参阅变量的Unicode文档中的emoji variation sequences section(表情符号变量序列)部分。
  • **检查表情符号是否支持肤色。**Android 7.0允许用户按照他们的喜好修改表情符号呈现的肤色。键盘应用应为有多个肤色的表情符号提供可视化的指示,并应允许用户选择他们喜欢的肤色。若要确定哪些系统表情符号有肤色修改器,使用hasGlyph(String)方法。 您可以通过读取Unicode文档来确定哪些表情符号使用肤色。

Android 中的 ICU4J API

Android 7.0目前在Android framework(位于android.icu软件包下)中提供ICU4J API的子集。迁移很简单,主要是需要从com.java.icu命名空间更改为android.icu。如果您已在您的应用中使用ICU4J捆绑包,切换到Android framework中提供的android.icu API可以很大程度上缩小apk的体积。

如果要了解有关 Android ICU4J API 的更多信息,请参阅ICU4J 支持

WebView

WebView在各个系统中的支持不一样,是众多开发者吐槽的对象,在Android 7.0中终于优化这个View。

在Android 7.0中使用Chrome version 51,当开发者启用多进程的WebView时,WebView讲运行在一个单独的沙箱中。

Google在以后的版本中会积极收录对WebView的兼容性和能行方面的问题,在Android 7.0这个版本开始WebView启动时间回归,使用的总的运行内存和网页渲染的性能。

如果开发者在多进程的模式下发现新的问题,大家可以提交到Google的Chrominum bug追踪器

Javascript在加载页面之前运行

从Android 7.0开始,JavaScript上下文会在每一个新页面被加载时重置,目前的其它版本是在第一次加载html时在一个新的WebView的实例中加载,所以只要第一次加载后以后的所有页面都会有这个属性,在7.0中我们应该在每一次加载页面时重新加载一下我们JavaScript对象。

在不安全的来源定位

从Android 7.0开始,WebView中定位只能在安全的来源中使用(也就是https),这样做是为了保护用户的隐私信息,因为http是不安全的。

WebView现在还是在定期更新,所以我们可以使用WebView Beta(β)通道测试App兼容性。如要要在Android 7.0测试预发布版本的WebView,下载并安装Chrome开发版或者Chrome测试版,并选择它作为WebView的实现,Google的文档说他们的开发人员这么说的(一脸懵逼)。我们可以通过Chromium bug追踪器报告问题,Google会在新发布的WebView中修复我们反馈的问题。

OpenGL™ ES 3.2 API

Android 7.0添加了对OpenGL ES 3.2的framework接口平台支持,包括:

  • 来自Android support(AEP)的所有扩展(EXT_texture_sRGB_decode 除外)。
  • 针对 HDR 的浮点帧缓冲和延迟着色。
  • BaseVertex绘图调用可实现更好的批处理和流媒体服务。
  • 强大的缓冲区访问控制,可减少WebGL开销。

Android 7.0提供了适用于OpenGL ES 3.2GLES32的framework API。使用OpenGL ES 3.2时,请务必在Manifest.xml中通过标记和android:glEsVersion声明属性。

如需了解有关使用 OpenGL ES 的信息,包括如何在运行时检查设备支持的 OpenGL ES 版本,请参阅OpenGL ES API 指南

Android TV 录制

Android N通过新的录制API添加了从Android TV输入服务录制和播放内容的功能。构建是在现有的API上做了支持,TV 输入服务可以控制能够录制的渠道数据、保存录制的会话的方式,同时可通过录制的内容管理用户交互。

如需了解详细信息,请参阅Android TV 录制 API

Android for Work

Android for Work针对运行Android N的设备添加了许多新功能和API。由于官网这部分时有更新,这里不做更细的介绍,请参阅Android for Work更新。

无障碍增强功能(人脸追踪、眼球追踪、点扫描)

Android N 现在针对新的设备设置直接在欢迎屏幕上提供“Vision Settings”。这使用户可以更容易发现和配置他们设备上的无障碍功能,包括放大手势、字体大小、显示屏尺寸和 TalkBack。

随着这些无障碍功能更为突出,在启用这些功能后,我们的用户可能更有可能使用App。发布App前请务必启用这些设置后测试你的App。 你可以通过Settings > Accessibility启用它们。

还是在 Android N 中,无障碍服务现在可以帮助具有动作障碍的用户触摸屏幕。全新的 API允许使用人脸追踪、眼球追踪、点扫描等功能构建服务,以满足这些用户的需求。

如需了解详细信息,请参阅可下载的API 参考中的android.accessibilityservice.GestureDescription

直接启动

直接启动可以缩短设备启动时间,让注册的应用具有一些有限的功能,即使在意外重启后也是有效的。例如,如果当用户睡觉时加密的设备重启,那么注册的警报、消息和来电现在可以和往常一样继续通知用户。这也意味着重启后无障碍服务会立即可用。

在Android N中,直接启动充分利用基于文件的加密,对系统和App启用更加细化的加密策略。系统针对选定的系统数据和显式注册的应用数据使用设备加密的存储。默认情况下,凭据加密的存储可用于所有其他系统数据、用户数据、应用及应用数据。

启动时,系统在受限的模式中启动,仅访问设备加密的数据,不会对应用或数据进行常规访问。如果你的App的组件要在这个模式运行,你可以通过在清单文件中设置标记注册它们。重启后,系统通过广播LOCKED_BOOT_COMPLETED Intent激活注册的组件。系统确保注册过的应用数据在解锁前可用。所有其他数据在用户确认锁定屏幕凭据进行解密前均不可用。

如需了解详细信息,请参阅直接启动

密钥认证

使用硬件支持的密钥库,可更安全地在 Android设备上创建、存储和使用加密密钥。它们免受Linux内核、潜在的Android漏洞的攻击,也可防止从已取得根权限的设备提取密钥。

为了让硬件支持的密钥库使用起来更简单和更安全,Android N 引入了密钥认证。应用和关闭的设备可使用密钥认证来确定RSA或EC密钥对是否受硬件支持、密钥对的属性是怎么样的,以及其使用和有效性有何限制。

应用和关闭的设备服务可以通过X.509认证证书(必须由有效的认证密钥签署)请求有关密钥对的信息。认证密钥是一个ECDSA签署密钥,它在出厂时被注入设备的硬件支持的密钥库。因此,有效的认证密钥签署的认证证书可确认硬件支持的密钥库是否存在,以及该密钥库中密钥对的详细信息。

为确保设备使用安全的官方Android出厂映像,密钥认证要求设备bootloader可信执行环境(TEE)提供以下信息:

  • 设备上安装的操作系统版本和补丁级别。
  • 验证的启动公钥和锁定状态。
    如需了解有关硬件支持的密钥库功能的详细信息,请参阅硬件支持的密钥库指南。

除密钥认证外,Android N 还推出了指纹绑定密钥,在指纹注册时不会撤销。

网络安全性配置

在Android N中,通过使用说明性“网络安全性配置”(而不是使用传统的易出错的编程API(例如,X509TrustManager)),应用可以安全地自定义其安全(HTTPS、TLS)连接的行为,无需修改任何代码。

支持的功能:

  • 自定义信任锚。让App可以针对安全连接自定义哪些证书颁发机构 (CA) 值得信赖。例如,信任特定的自签署证书或限制应用信任的公共CA集合。
  • 仅调试重写。让App开发者可以安全调试其应用的安全连接,而不会增加安装基础的风险。
  • 明文流量选择退出。让App可以防止自身意外使用明文流量。
  • 证书固定。这是一项高级功能,让App可以针对安全连接限制哪些服务器密钥受信任。
    如需了解详细信息,请参阅网络安全性配置

默认受信任的证书颁发机构

默认情况下,针对Android 7.0的App仅信任系统提供的证书,且不再信任用户添加的证书颁发机构(CA)。 如果面向 Android 7.0的应用希望信任用户添加的CA,则应使用网络安全性配置以指定信任用户 CA 的方式。

APK signature scheme v2

Android 7.0 引入一项新的应用签名方案APK Signature Scheme v2,它能让App更快速的安装,保护更多针对未授权APK文件的更改。在默认情况下,Android Studio 2.2Android Gradle 2.2插件会使用APK Signature Scheme v2和传统签名方案来签署您的应用。

虽然Google建议我们的App采用APK Signature Scheme v2,但这项新方案并非强制性的。如果你的App在使用APK Signature Scheme v2时不能正确构建,那么你可以不用。 不用这个方案的话Android Studio 2.2Android Gradle 2.2插件仅使用传统签名方案来签署你的App。若要仅用传统方案签署,打开Module的build.gradle文件,然后将行v2 SigningEnabled false添加到您的版本签名配置中:

android {
...
defaultConfig { ... }
    signingConfigs {
        release {
            storeFile file("myreleasekey.keystore")
            storePassword "password"
            keyAlias "MyReleaseKey"
            keyPassword "password"
            v2SigningEnabled false
        }
    }
}

注意:如果你使用APK Signature Scheme v2签署您的应用,并对应用进行了进一步更改,则应用的签名将无效。 出于这个原因,如要你要使用zipalign工具的话,请在使用APK Signature Scheme v2 之前使用。

如需更多信息,请阅读介绍如何在 Android Studio 中签署一项应用以及如何使用 Android Gradle插件来为签署应用配置构建文件

作用域目录访问

在 Android 7.0中,App可以使用新的API请求访问特定的外部存储目录,包括可移动媒体上的目录,如 SD卡。新API大大简化了应用访问标准外部存储目录的方式,如Pictures 目录。App(如图库)就可以使用这些API(而不是使用READ_EXTERNAL_STORAGE),它已经授予所有存储目录的访问权限,从而让用户可以导航到目录。

此外,新的API简化了用户向应用授予外部存储访问权限的步骤。当您使用新的API时,系统使用一个简单的权限UI,其清楚地详细介绍应用正在请求访问的目录。

关于运行时权限管理可以看我的这篇文章
如需了解详细信息,请参阅作用域目录访问开发者文档。

键盘快捷键辅助工具

在Android 7.0中,用户可以按“Alt + /”触发“键盘快捷键”窗口,它会显示系统和对焦的应用中可用的所有快捷键。这些快捷键是从应用菜单(如果有并可以用)中自动检索到的,但开发者可以提供自己的屏幕微调快捷键。您可以通过重写新 Activity.onProvideKeyboardShortcuts()的方法来进行这项操作,如可下载的API参考中所述。

若要在你的App的任何地方主动触发键盘快捷键辅助工具,为相关Activity调用Activity.requestKeyboardShortcutsHelper()。

VR 支持

Android N 添加了新的VR模式的平台支持和优化,便于开发者能为用户打造高质量移动VR体验。新版针对开发者提供了大量性能增强特性,包括单一缓冲区渲染以及允许VR应用访问某个专属的CPU核心。在你的App中,您可以享受到专为 VR 设计的平滑头部跟踪和立体声通知功能。最重要的是,Android N的图形延时非常低。如需有关构建面向的Android 7.0的VR应用的完整信息,请参阅面向 Android 的 Google VR SDK

打印服务增强

在 Android N 中,打印服务的开发者现在可以公开关于个别打印机和打印作业的其他信息。

在列出各打印机时,打印服务现在可以通过两种方式来设置按打印机的图标:

  • 您可以通过调用PrinterInfo.Builder.setResourceIconId()设置源于资源 ID 的图标。
  • 您可以通过调用PrinterInfo.Builder.setHasCustomPrinterIcon(),并针对使用android.printservice.PrinterDiscoverySession.onRequestCustomPrinterIcon()设置来自网络的图标。

此外,您还可以通过调用PrinterInfo.Builder.setInfoIntent()根据打印机活动,显示其他相关信息。

您可以通过分别调用android.printservice.PrintJob.setProgress()android.printservice.PrintJob.setStatus()在打印任务通知中指示打印任务的进度和状态。

如需有关这些方法的详细信息,请参阅可下载的API 参考

FrameMetricsListener API

FrameMetricsListener API允许应用监测它的UI渲染性能。API通过公开流式传输Pub/Sub API来提供此能力,以传递应用当前窗口的帧计时信息。返回的数据相当于adb shell(https://developer.android.com/tools/help/shell.html#shellcommands) dumpsys gfxinfo framestats显示的数据,但不限定于在过去的120帧内。

你可以使用FrameMetricsListener来衡量生产中的交互级UI性能,而且不需要USB连接。API允许在比adb shell dumpsys gfxinfo更高的粒度上收集数据。因为系统可以从应用中的特定交互中收集数据,因此更高的粒度变得可行;系统不需要采集关于完整应用性能的全局概要或清除任何全局状态。你可以使用这种能力来针对App的真实使用案例收集性能数据和捕捉UI性能回馈。

若要监测一个窗口,实现FrameMetricsListener.onMetricsAvailable()callback方法,并在Window上注册。如需了解详细信息,请参阅可下载的API参考中的FrameMetricsListener类文档。

API 提供了一个包含计时数据的FrameMetrics对象,其渲染子系统会在一帧长度的声明周期内报告各种时间点。支持的指标有:UNKNOWN_DELAY_DURATIONINPUT_HANDLING_DURATIONANIMATION_DURATIONLAYOUT_MEASURE_DURATIONDRAW_DURATIONSYNC_DURATIONCOMMAND_ISSUE_DURATIONSWAP_BUFFERS_DURATIONTOTAL_DURATIONFIRST_DRAW_FRAME

虚拟文件(Virtual Files)

在较早的Android系统版本中,你的App可以使用存储访问框架来允许用户从他们的云存储帐户中选择文件,如 Google 云端硬盘。但是,不能表示没有直接字节码表示的文件;每个文件都必须提供一个输入流。

Android 7.0在存储访问框架中增加了“虚拟文件”的概念。虚拟文件功能可以让您的DocumentsProvider返回可与ACTION_VIEW Intent使用的文件URI,即使它们没有直接字节码表示。Android 7.0还允许你给用户文件(虚拟或其他类)提供备用的格式。

为获得你App中的虚拟文件的URI,首先你应该创建一个Intent以打开文件选择器App。由于应用不能使用 openInputStream()方法来直接打开一个虚拟文件,因此如果你添加了CATEGORY_OPENABLE,那么你的App不会接收到任何虚拟文件。

在用户选择之后,系统会调用onActivityResult()方法。你的App可以检索虚拟文件的URI,并得到一个输入流,这个一段事例代码:

// Other Activity code ...

  final static private int REQUEST_CODE = 64;

  // We listen to the OnActivityResult event to respond to the user's selection.
  @Override
  public void onActivityResult(int requestCode, int resultCode,
    Intent resultData) {
      try {
        if (requestCode == REQUEST_CODE &&
            resultCode == Activity.RESULT_OK) {

            Uri uri = null;

            if (resultData != null) {
                uri = resultData.getData();

                ContentResolver resolver = getContentResolver();

                // Before attempting to coerce a file into a MIME type,
                // check to see what alternative MIME types are available to
                // coerce this file into.
                String[] streamTypes =
                  resolver.getStreamTypes(uri, "*/*");

                AssetFileDescriptor descriptor =
                    resolver.openTypedAssetFileDescriptor(
                        uri,
                        streamTypes[0],
                        null);

                // Retrieve a stream to the virtual file.
                InputStream inputStream = descriptor.createInputStream();
            }
        }
      } catch (Exception ex) {
        Log.e("EXCEPTION", "ERROR: ", ex);
      }
  }

如需有关访问用户文件的更多信息,请参阅存储访问框架指南。

如果想了解更多关于Android7.0的知识,可以阅读Android 7.0行为变化(官网同步翻译)


版权声明:转载必须注明本文转自严振杰的博客: http://blog.yanzhenjie.com

作者:yanzhenjie1003 发表于2016/9/13 22:59:21 原文链接
阅读:273 评论:0 查看评论

Android 7.0行为变化—开发者应该关注的(官网同步翻译)

$
0
0

Android 7.0行为变化—开发者应该关注的(官网同步翻译)

版权声明:转载必须注明本文转自严振杰的博客: http://blog.yanzhenjie.com

如果想了解更多Android7.0的内容,可以顺便再看看Android7.0写给开发者的一封信(官网同步翻译)

如果你的引文够好,推荐你阅读官网文章:
Android 7.0 Behavior Changes

Android N 除了提供诸多新特性和功能外,还对系统和 API 行为做出了各种变更。本文重点介绍你应该了解并在开发应用时加以考虑的一些重要变更。

如果您之前发布过Android App,请注意你的App可能受到这些平台变更的影响。


电池和内存

Android N包括旨在延长设备电池寿命和减少RAM使用的系统行为变更。这些变更可能会影响您的应用访问系统资源,以及您的系统通过特定隐式Intent与其他应用互动的方式。

低电耗模式

Android 6.0(API leve 23)引入了低电耗模式,当用户设备未插接电源、处于静止状态且屏幕关闭时,该模式会推迟CPU和网络访问,从而延长电池寿命。而Android 7.0则通过在设备未插接电源且屏幕关闭状态下、但不一定要处于静止状态(例如用户外出时把手持设备装在兜兜里)时应用部分CPU和网络限制,进一步增强了低电耗模式。

低电耗模式如何应用第一级系统活动限制以延长电池寿命的图示:
低电耗模式

当设备处于充电状态且屏幕已关闭一定时间后,设备会进入低电耗模式并应用第一部分限制:关闭应用网络访问、推迟Work和Sync。如果进入低电耗模式后设备处于静止状态达到一定时间,系统则会对PowerManager.WakeLockAlarmManager闹铃GPSWi-Fi扫描应用低电耗模式限制。无论是应用部分限制还是全部低电耗模式限制,系统都会唤醒设备提供一个短时间的维护窗口,在此窗口展示期间,应用程序可以访问网络并执行所有被推迟的Work/Sync。

低电耗模式如何在设备处于静止状态达到一定时间后应用第二级系统活动限制的图示:
低电耗模式

请注意,激活屏幕或插接设备电源时,系统将退出低电耗模式并取消之前的限制。此项新增的行为不会影响有关使你的App适应Android 6.0(API Leve23)中所发布旧版本低电耗模式的建议和最佳实践,如低电耗模式和应用待机模式优化中提到的内容。你还是应该遵循这些建议(例如使用 Google Cloud Messaging (GCM) 发送和接收消息),并且兼容新的低电耗模式。

Project Svelte:后台优化

Android 7.0删除了三个隐式广播,优化内存使用和优化电量消耗。这个变化是非常必要的,因为隐式广播会在后台频繁启动已注册侦听这些广播的应用。删除这些广播可以显著提升设备性能和用户体验。

移动设备会经历频繁的连接变更,例如在Wi-Fi和移动数据之间切换时。目前,可以通过在Manifest.xml中注册一个BroadcastRecevier来监听隐式CONNECTIVITY_ACTION广播,让应用能够监控这些变更。由于很多App会注册接收这个广播,因此单次网络切换即会导致所有应用被唤醒并同时处理此广播。

同样的道理,应用可以注册接收来自其他应用(例如相机)的隐式ACTION_NEW_PICTUREACTION_NEW_VIDEO广播。当用户使用相机应用拍摄照片时,这些应用即会被唤醒以处理广播。

为缓解缓解上面的问题,Android 7.0采用了下面优化措施:

  • 针对Android 7.0开发的应用不会收CONNECTIVITY_ACTION广播,即使它们已经在Manifest.xml中注册了接受这个广播的BroadcastRecevier因为不会收到。在前台运行的应用如果使用BroadcastReceiver接收通知,则仍可以在主线程中侦听CONNECTIVITY_CHANGE
  • 应用无法发送或接收ACTION_NEW_PICTUREACTION_NEW_VIDEO广播。这个改变会影响到所有的App,不仅仅是针对Android 7.0开发的App。

如果你的App使用了任何隐式Intent,您仍需要尽快移除它们的依赖关系,以正确适配Android 7.0的设备。 Android framework提供多个解决方案来缓解对这些隐式广播的需求。例如JobScheduler API提供了一个稳健可靠的机制来安排满足指定条件(例如连入无限流量网络)时所执行的网络操作。您甚至可以使用JobScheduler来适应 Content providers变化。

如需了解有关 Android 7.0中后台优化以及如何改写应用的详细信息,请参阅后台优化

权限更改

Android 7.0 做了一些权限更改,这些更改可能会影响到你的App。

系统权限更改

为了提高私有文件的安全性,面向 Android 7.0或更高版本的App私有目录被限制访问(0700)。此设置可防止私有文件的元数据泄漏,如它们的大小或是否存在(状态)。此权限策略的更改有多重副作用:

应用间共享文件

对于针对Android 7.0的应用,Android framework执行的StrictMode API禁止向你的App外公开file://URI。如果一个包含文件URI的Intent发送到你的应用之外,App会发生FileUriExposedException异常。

若要在应用间共享文件,您应发送一项content://URI,并授予URI临时访问权限。进行此授权的最简单方式是使用FileProvider类。如需有关权限和共享文件的更多信息,请参阅共享文件

无障碍改进

为提高平台对于视力不佳或视力受损用户的可用性,Android 7.0做出了一些更改。这些更改正常情况下不需要改你的代码,不过你要仔细检查并用你的App测试这些功能,以评估它们对用户体验的潜在影响。

屏幕缩放

Android 7.0支持用户设置显示尺寸,以放大或缩小屏幕上的所有元素,从而提升设备对视力不佳用户的可访问性。用户无法将屏幕缩放至低于最小屏幕宽度sw320dp,该宽度是Nexus 4的宽度,也是常规中等大小手机的宽度。

Android 7.0 系统映像正常大小运行效果:
正常大小运行效果

Android 7.0 系统映像增大显示尺寸后的效果:
增大显示尺寸后的效果

当设备密度发生更改时,系统会以如下方式通知正在运行的应用:

  • 如果是面向API leve 23或更低版本系统的应用,系统会自动终止其所有后台进程。也就是说如果用户切换后离开你的App,打开“Settings”更改Display size设置,则系统会像处理内存不足的情况一样终止该应用。如果应用具有任何前台进程,则系统会如处理运行时变更中所述将配置变更通知给这些进程,就像对待设备屏幕方向变更一样,具体大家可以再看看这个超链接。
  • 如果是针对Android 7.0的App,则其所有进程(前台和后台)都会收到有关配置变更的通知,如处理运行时变更中所讲的那样。
    大多数App并不需要进行任何更改即可支持此功能,不过前提是这些应用遵循Android最佳实践。具体要检查的事项:
    • 在屏幕宽度为 sw320dp 的设备上测试你的App,并确保其正常运行。
    • 当设备Config发生变更时,更新任何与密度相关的缓存信息,例如缓存位图或从网络加载的资源。当应用从暂停状态恢复运行时,检查Config的变化。
      注:如果你要缓存与配置相关的数据,则最好也包括相关元数据,例如该数据对应的屏幕尺寸或像素密度。保存这些元数据便于你在Config变更后决定是否需要刷新缓存数据。
    • 避免用像素单位指定尺寸,因为像素不会随屏幕密度缩放。应改为使用dp等单位

设置向导中的视觉设置

Android 7.0 在“Welcome”页面中加入了“Vision Settings”,用户可以在新设备上操作`无障碍功能设置`: `Magnification gesture`、`Font size`、`Display size`和`TalkBack`。这个变化增加了与不同屏幕设置相关的错误的可见性。要测试此功能的影响,你应该在启用这些设置的状态下测试应用,可以在`Settings > Accessibility`中找到这些设置。

NDK 应用链接至平台库

Android 7.0做了一些命名空间更改,以阻止加载非公开API。如果你使用NDK,则只能使用Android平台提供的公开 API。在下一个官方发布的Android 版本上使用非公开API会导致应用崩溃。 为提醒你使用了非公开API,在Android 7.0的设备上运行的App会在有App调用非公开API时在日志消息输出中生成一个错误。此错误还会作为消息显示在设备屏幕上,以帮助增强你对此情况的认识。你应该检查应用代码以删除使用非公开API,并使用预览版设备或模拟器全面测试App。 如果您的App依赖平台库,则请参见NDK文档,了解使用公开API等效替换普通私有API的修复方案。你还可以链接至平台库,而无需实现此应用,如果App使用的库是平台的一部分(例如libpng),但不属于NDK,则更可如此。这种情况下,请确保您的APK包含您打算链接到的所有.so 文件。

注意:有些第三方库可能会链接至非公开API。如果您的应用使用这些库,那么当您的应用在下一个官方发布的 Android 版本上运行时可能会出现崩溃现象,so,你也要认真检查使用的第三方库是否使用了非公开api,例如百度地图。

App不应该依赖或使用不属于NDK的原生库,因为这些库可能会发生更改,或者从一个Android版本迁移至另一版本的时候发生更改。例如,从OpenSSL切换至BoringSSL即属于此类更改。此外,不同的设备可能提供不同级别的兼容性,因为不属于NDK中的平台库没有兼容性要求。如果你必须在较旧设备上访问非NDK库,则请依据 Android API 级别进行加载。 为帮助您诊断此类问题,下面列举了一些在您试图使用 Android N 开发应用时可能遇到的 Java 和 NDK 错误: Java 错误示例:
java.lang.UnsatisfiedLinkError: dlopen failed: library "/system/lib/libcutils.so"
    is not accessible for the namespace "classloader-namespace"
NDK 错误示例:
dlopen failed: cannot locate symbol "__system_property_get" referenced by ...
以下是遇到这类错误的应用的一些典型修复: * 可以使用标准JNI函数来替代使用`libandroid_runtime.so`中的`getJavaVM`和`getJNIEnv`:
AndroidRuntime::getJavaVM -> GetJavaVM from <jni.h>
AndroidRuntime::getJNIEnv -> JavaVM::GetEnv or
JavaVM::AttachCurrentThread from <jni.h>.
  • 可以使用公开alternative __system_property_get来替代使用libcutils.so中的property_get符号。如需这样做,请使用 __system_property_get及以下include函数:
#include <sys/system_properties.h>
  • 应使用应用本地版本来替代使用libcrypto.so中的SSL_ctrl符号。例如,你应在.so文件中静态链接libcyrpto.a,或者在应用中包含你自己的来自BoringSSLOpenSSL的动态libcrypto.so

Android for Work

Android 7.0包含一些针对面向Android for Work的应用的变更,包括对证书安装、密码重置、二级用户管理、设备标识符访问权限的变更。如果你是要针对Android for Work环境开发应用,则应仔细检查这些变更并相应地修改你的App代码。

  • 你必须先安装授权证书安装程序,然后DPC才能对其进行设置。对于面向7.0 SDK的个人资料和设备所有者应用,您应在设备策略控制器(DPC)调用DevicePolicyManager.setCertInstallerPackage()之前安装授权证书安装程序。如果尚未安装此安装程序,则系统会引发IllegalArgumentException

  • 针对设备管理员的重置密码限制现在也适用于个人资料所有者。设备管理员无法再使用DevicePolicyManager.resetPassword()来清除或更改已经设置的密码。设备管理员仍可以设置密码,但只能在设备没有密码、PIN或图案时这样做。

  • 即使设置了限制,设备所有者和个人资料所有者仍可以管理帐户。而且,即使具有DISALLOW_MODIFY_ACCOUNTS用户限制,设备所有者和个人资料所有者仍可调用Account Management API

  • 设备所有者可以更轻松地管理二级用户。当设备在设备所有者模式下运行时,系统将自动设置DISALLOW_ADD_USER限制。这样可以防止用户创建非托管二级用户。此外,CreateUser()createAndInitializeUser()方法已弃用,取而代之的DevicePolicyManager.createAndManageUser()方法。

  • 设备所有者可以访问设备标识符。设备所有者可以使用DevicePolicyManagewr.getWifiMacAddress()访问设备的Wi-Fi MAC地址。如果设备上从未启用Wi-Fi,则此方法将返回一个null值。

  • 工作模式设置控制工作应用访问。当工作模式关闭时,系统启动器通过使工作应用显示为灰色来指示它们不可用。启用工作模式会再次恢复正常行为。

如需了解有关Android 7.0中针对Android for Work所做变更的详细信息,请参阅Android for Work 更新

注解保留

Android 7.0在注解可见性被忽略时修复错误。这种问题将启用本不应被允许的运行时访问注解。 这些注解包括:
* VISIBILITY_BUILD:仅应编译时可见。
* VISIBILITY_SYSTEM:运行时应可见,但仅限基本系统。
如果你的App依赖这种行为,请在注解中添加一项运行时必须可用的保留政策。你可通过使用@Retention(RetentionPolicy.RUNTIME) 这样做。

其他重要说明

  • 如果一个针对较低API级别开发的App在Android 7.0上运行,那么在用户更改显示尺寸时,系统将终止此App进程。App必须能够正常处理此情景。否则,当用户从最近使用记录中恢复运行App时,App将会出现崩溃现象。您应测试应用以确保不会发生此行为。要进行此测试,您可以通过DDMS手动终止应用,可以造成相同的崩溃现象。在屏幕密度发生更改时,系统不会自动终止针对Android 7.0及更高版本开发的App;不过这些App仍可能对配置变更做出不良响应。

  • Android 7.0上的应用应能够正常处理配置变更,并且在后续启动时不会出现崩溃现象。你可以通过更改字体大小 (Setting > Display > Font size) 并随后从最近使用记录中恢复运行应用,来验证App行为。

  • 由于之前的Android版本中的一项错误,系统没有对主线程上的一个TCP Socket的写入操作严格检查。Android 7.0修复了这个系统错误。之前有这种行为的App将会引发android.os.NetworkOnMainThreadException。一般情况下,不建议在主线程上执行网络操作,因为这些操作通常都有可能导致ANR和卡顿,这个应该是中所周知的,大家一般不会犯。

  • Debug.startMethodTracing()方法族现在默认在你的共享的存储空间上的软件包特定目录中存储输出,而非 SD卡顶级。这意味着应用不再需要请求WRITE_EXTERNAL_STORAGE权限就可以使用这些API。

  • 许多平台API现在开始检查在Binder事务间发送的大负载,系统现在会将TransactionTooLargeExceptions再次作为RuntimeExceptions引发,而不再只是默默记录或不抛出这个错误。一个常见例子是在Activity.onSaveInstanceState()上存储过多数据,导致ActivityThread.StopInfo在你的App面向 Android 7.0时引发RuntimeException

  • 如果应用向View post Runnable任务,并且View未附加到窗口,系统会用View为Runnable任务排队;在 View附加到窗口之前,Runnable任务不会执行。 此行为会修复以下错误:

    • 如果一个App是从并非预期Window UI线程的其他线程发布到View,则Runnable可能会因此运行错误。
    • 如果Runnable任务是从并非looper thread的其他线程发布,则应用可能会曝光Runnable任务。
  • 如果Android 7.0上有DELETE_PACKAGES权限的应用尝试删除一个软件包,但另一项应用已经安装了这个软件包,则系统可能要求用户确认。在这种情况下,应用在调用PackageInstaller.uninstall() 时的返回状态应为STATUS_PENDING_USER_ACTION

如果想了解更多Android7.0的内容,可以顺便再看看Android7.0写给开发者的一封信(官网同步翻译)


版权声明:转载必须注明本文转自严振杰的博客: http://blog.yanzhenjie.com

作者:yanzhenjie1003 发表于2016/9/13 23:01:14 原文链接
阅读:363 评论:2 查看评论

刮奖效果控件--ScratchView

$
0
0

    最近看了一篇公众号推送的文章,关于Android刮奖效果的自定义控件,感觉蛮有意思,所以就模仿他也写了一个自定义ScratchView,人家的文笔不错,所以建议可以先看他的文章:ScratchView:一步步打造万能的 Android 刮奖效果控件;然后再来阅读我的,那为什么还要看我的呢?因为我针对内存优化和监听时机两个方面做了改进。

    考虑如果你没看刚才那位的文章,那后面说的优化可能就云里雾里,所以还是说一下ScratchView的实现细节吧。ScratchView的实现思路就是通过绘制和被覆盖view一样大小但是不同透明度的蒙层,重写touch事件的响应来修改蒙层的透明度,实现刮奖的效果。

1.蒙层效果。

    蒙层的本质是一个bitmap,通过不同颜色的画笔Paint,在onDraw的时候把bitmap绘制到画布Canvas上。下面这段是mask画笔的初始化,设置抗锯齿和防抖,最后是设置mask的颜色。

 

    在view的宽和高确定之后,创建一个等大的bitmap,然后把这个bitmap绘制到画布上。

 

    因为是自定义view,所以考虑可以在xml中设置这个view的一些属性,比如这里的蒙层的颜色属性。这里可以自定义它的一些属性,在attr.xml中增加这个ScratchView的属性,每条attr都是一个自定义属性,如下图。

 

    在java代码中获取这些设置的属性,需要在构造函数中通过TypedArray来获取。需要考虑和属性的值的类型一一对应。

 

    蒙层如果只是一种颜色,会比较单调,因此会考虑增加一些图案,比如logo在蒙层上面,这也称为水印效果。要实现水印效果,可以使用BitmapDrawable,利用它的TileMode来实现不同的水印效果。比如镜面对称的MIRROR,简单重复的REPEAT等。

 

    现在我们展示一下,有蒙层的效果图。

 

2.刮奖效果。

    刮奖效果本质是监听手指的touch事件,根据move事件,把经过的像素点的透明度设置为0,从而达到被擦除的效果。

    新建一个擦除画笔,设置画笔的Xfermode模式,里面有一种PorterDuff.Mode.CLEAR模式,在很多画板程序里实现橡皮擦效果,这里也借鉴。

 

   对touch事件的监听,只需要处理DOWNMOVEUP事件即可。

 

    对于DOWN事件,需要重置擦除的路径,然后记录新的起点。对于MOVE事件,需要记录移动的路径,当然这里有最小的移动距离,然后利用擦除画笔,把路径上的点透明度修改掉,达到擦除效果。对于UP事件,需要重置擦除路径。

 

    现在我们展示下擦除效果。

 

3.设置监听。

    考虑到刮奖,当大部分的图案显示出来的时候,用户就不会继续刮下去,这时候,程序也应该有一个处理,提示中奖或者其他动作。因此,这里设置用户刮奖的时候,显示擦除部分的占比,当占比超过一个默认值时,就认为刮奖完成,需要给外面一个回调的接口。

 

    已擦除部分和整个view的占比计算是一个比较重要的点。因为mask是用bitmap实现的,而bitmap是可以获取到每个像素点的透明度,所以可以通过一个数组存这个bitmap的每个像素点的透明度,每次擦除的时候都遍历一次,然后计算占比,就可以获得已擦除部分的占比。

 

    mPixels就是mask的所有像素点透明度保存的数组,调用bitmapgetPixels可以获取当前bitmap的透明度并保存到这个int数组里面。通过计算已擦除数量mErasePixelCount和总的数量mTotalErasePixel的比例,得到实际占比。并把这个比例回调给监听者。

    另外,这里是在touch事件的UP事件时候才处理是否擦除已完成的回调,而且通过设置mIsCompleted这个布尔值,保证回调只触发一次。

   这个的效果需要配合其他观察者一起使用,就不展示了。

4.重置和清除效果。

    重置的实现,本质是通过把maskbitmap重新绘制一遍;同理清除效果本质是把maskbitmap的所有像素点透明度置位0

 

5.内存优化。

    最初运行程序,每次重置都会发生内存稳定增加。如下图:

 

    分析发现,因为重置是会调用createMaskBitmap函数,而这个函数内部重新new bitmap,同时还会把存bitmap像素点透明度的数组也会重新new一遍。

    这里直接给出内存优化后的代码。

 

    这里主要考虑一下两点:第一点,bitmap能不能不new,答案是肯定的,但是需要判断,如果viewsize改变了,或者bitmap被回收了,就需要重新new,但是如果没有的话,我们直接复用之前的bitmap,不要再new。第二点,bitmap的像素点透明度的数组可不可以复用,答案也是肯定的,也只有在viewsize发生变化的时候才需要去new。所以优化后的代码会增加这两个判断。

    优化后的内存如下图所示,只有在第一次new bitmap的时候发生内存增加,后面操作基本没有发生内存比较大幅度的变化。

 

     最后的整个工程代码,稍后会传。

 

作者:u012321815 发表于2016/9/13 23:13:48 原文链接
阅读:82 评论:0 查看评论

Android下AIDL机制详解

$
0
0

AIDL全名Android Interface Definition Language,是一种接口定义语言,也是Android系统的一种跨进程通信机制。从AIDL的名字就可以看出来,AIDL做的就是在服务提供进程和服务使用进程之间的协商好的接口,双方通过该接口进行通信。本文将以一个例子来讲述AIDL的使用方式和流程,在下一篇文章中我将从代码层面对AIDL进行分析。

AIDL实例

文章中所涉及的例子来源于android开发之AIDL用法_进程间通信原理详解 一文。首先我们创建一个AIDL文件,并创建了一个接口方法:

interface forService {  
    void registerTestCall(forActivity cb);  
    void invokCallBack();  
} 

这里多说一句,在AIDL文件中并不是所有的数据都可以使用,能够使用的数据类型包括如下几种:
* 基本数据类型(int, long, char, boolean, double等)
* String和CharSequence
* List:只支持ArrayList,并且里面的元素都能被AIDL支持
* Map:只支持HashMap,里面的每个元素能被AIDL支持
* Parcelable:所有实现Parcelable接口的对象
* AIDL: 所有AIDL接口本身也能在AIDL文件中使用
另外AIDL中除了基本数据类型意外,其他数据类型必须标上方向:in、out或者inout。其实AIDL文件和interface很像,其作用本质也是定义了一个接口,就像双方制定的一个协议一样,在通信时必须遵守该协定才能正常通信。

远程服务端Service实现

package com.styleflying.AIDL;  
import android.app.Service;  
import android.content.Intent;  
import android.os.IBinder;  
import android.os.RemoteCallbackList;  
import android.os.RemoteException;  
import android.util.Log;  
public class mAIDLService extends Service { 
    ....  
    @Override  
    public void onCreate() {  
        Log("service create");  
    }   

    @Override  
    public IBinder onBind(Intent t) {  
        Log("service on bind");  
        return mBinder;  
    }   
    @Override  
    public boolean onUnbind(Intent intent) {  
        Log("service on unbind");  
        return super.onUnbind(intent);  
    }  
    public void onRebind(Intent intent) {  
        Log("service on rebind");  
        super.onRebind(intent);  
    }  
    private final forService.Stub mBinder = new forService.Stub() {  
        @Override  
        public void invokCallBack() throws RemoteException  
        {  
            callback.performAction();  

        }  
        @Override  
        public void registerTestCall(forActivity cb) throws RemoteException  
        {  
            callback = cb;  

        }  

    };  
} 

这里注意一下onBind()函数,该函数返回了一个IBinder类型,IBinder说明AIDL底层是基于Android Binder机制实现的,Binder机制的具体实现细节放到下一篇博客在做详细介绍。onBind函数实际返回的是mBinder类型,而该类型实际是生命了一个forService .stub类型,并在声明中对AIDL文件定义的接口方法做了具体的实现。所以这里的mBinder可以理解为一个包含了AIDL所定义接口具体实现的类,而这个类最终将传递给客户端,供其调用。

客户端代码

    package com.styleflying.AIDL;  
    .... 
    public class mAIDLActivity extends Activity {  
        private static final String TAG = "AIDLActivity";    
        forService mService;  
        private ServiceConnection mConnection = new ServiceConnection() {  
            public void onServiceConnected(ComponentName className,  
                    IBinder service) {  
                mService = forService.Stub.asInterface(service);  
                try {  
                    mService.registerTestCall(mCallback);}  
                catch (RemoteException e) {  

                }  
                }  
            public void onServiceDisconnected(ComponentName className) {  
                Log("disconnect service");  
                mService = null;  
                }  
            };     
    }  

ServiceConnection是客户端发起的一个指向服务端的连接,而在连接成功时(onServiceConnected被调用时),通过
mService =forService.Stub.asInterface(service)获取到了服务端传递过来的包含有AIDL规定接口具体实现的AIDL对象(即service端onBind返回的对象,该对象原本是一个forService.stub对象通过调用asInterface接口获取到了对应的AIDL对象),接下来就可以利用mService调用对应的方法了。然后在连接断开时再释放即可。

AIDL源码解析

上文即是一个AIDL的使用实例,利用AIDL可以轻松的实现在Android端的跨应用通信。但知其然还要知其所以然,这样简单的使用显然无法透彻的了解AIDL通信的原理。上文我们已经提到了AIDL实际底层利用的是Android Binder机制进行通信,在本文中我们将从代码层面继续剖析AIDL机制,而在下一篇博客中讲解Binder机制的原理。虽然我们定义的AIDL文件只有寥寥数行,但是真正的运行起来的AIDL代码却远远不止这点,实际上大部分的工作IDE都帮我们完成了,以上文中的forService为例,在我们编写完AIDL文件后,IDE会生成其对应的java文件forService.java

package ...;  
import java.lang.String;  
import android.os.RemoteException;  
import android.os.IBinder;  
import android.os.IInterface;  
import android.os.Binder;  
import android.os.Parcel;  
public interface forService extends android.os.IInterface  
{  
/** Local-side IPC implementation stub class. */  
public static abstract class Stub extends android.os.Binder implements com.styleflying.AIDL.forService  
{  
private static final java.lang.String DESCRIPTOR = "com.styleflying.AIDL.forService";  
/** Construct the stub at attach it to the interface. */  
public Stub()  
{  
this.attachInterface(this, DESCRIPTOR);  
}  
/** 
 * Cast an IBinder object into an forService interface, 
 * generating a proxy if needed. 
 */  
public static com.styleflying.AIDL.forService asInterface(android.os.IBinder obj)  
{  
if ((obj==null)) {  
return null;  
}  
android.os.IInterface iin = (android.os.IInterface)obj.queryLocalInterface(DESCRIPTOR);  
if (((iin!=null)&&(iin instanceof com.styleflying.AIDL.forService))) {  
return ((com.styleflying.AIDL.forService)iin);  
}  
return new com.styleflying.AIDL.forService.Stub.Proxy(obj);  
}  
public android.os.IBinder asBinder()  
{  
return this;  
}  
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException  
{  
switch (code)  
{  
case INTERFACE_TRANSACTION:  
{  
reply.writeString(DESCRIPTOR);  
return true;  
}  
case TRANSACTION_registerTestCall:  
{  
data.enforceInterface(DESCRIPTOR);  
com.styleflying.AIDL.forActivity _arg0;  
_arg0 = com.styleflying.AIDL.forActivity.Stub.asInterface(data.readStrongBinder());  
this.registerTestCall(_arg0);  
reply.writeNoException();  
return true;  
}  
case TRANSACTION_invokCallBack:  
{  
data.enforceInterface(DESCRIPTOR);  
this.invokCallBack();  
reply.writeNoException();  
return true;  
}  
}  
return super.onTransact(code, data, reply, flags);  
}  
private static class Proxy implements com.styleflying.AIDL.forService  
{  
private android.os.IBinder mRemote;  
Proxy(android.os.IBinder remote)  
{  
mRemote = remote;  
}  
public android.os.IBinder asBinder()  
{  
return mRemote;  
}  
public java.lang.String getInterfaceDescriptor()  
{  
return DESCRIPTOR;  
}  
public void registerTestCall(com.styleflying.AIDL.forActivity cb) throws android.os.RemoteException  
{  
android.os.Parcel _data = android.os.Parcel.obtain();  
android.os.Parcel _reply = android.os.Parcel.obtain();  
try {  
_data.writeInterfaceToken(DESCRIPTOR);  
_data.writeStrongBinder((((cb!=null))?(cb.asBinder()):(null)));  
mRemote.transact(Stub.TRANSACTION_registerTestCall, _data, _reply, 0);  
_reply.readException();  
}  
finally {  
_reply.recycle();  
_data.recycle();  
}  
}  
public void invokCallBack() throws android.os.RemoteException  
{  
android.os.Parcel _data = android.os.Parcel.obtain();  
android.os.Parcel _reply = android.os.Parcel.obtain();  
try {  
_data.writeInterfaceToken(DESCRIPTOR);  
mRemote.transact(Stub.TRANSACTION_invokCallBack, _data, _reply, 0);  
_reply.readException();  
}  
finally {  
_reply.recycle();  
_data.recycle();  
}  
}  
}  
static final int TRANSACTION_registerTestCall = (IBinder.FIRST_CALL_TRANSACTION + 0);  
static final int TRANSACTION_invokCallBack = (IBinder.FIRST_CALL_TRANSACTION + 1);  
}  
public void registerTestCall(com.styleflying.AIDL.forActivity cb) throws android.os.RemoteException;  
public void invokCallBack() throws android.os.RemoteException;  
}  

代码比较复杂,而且一看就是系统自动生成的,看起来很不规范。不用着急,我们一点一点来啃。首先这个类在内部实现了一个抽象类Stub,这个词看起来是不是很熟悉,对了,它就是在Service端出现的那个stub。stub直译过来是存根,也就是Service保留在本地的一个凭证。forService类中所有的逻辑都在stub中实现。而在stub中又有一个子类称为proxy(代理),这个类名更好理解,他是service的代理,需要用到代理的地方只有远程的调用者,即客户端。所以proxy就是从服务端传递到客户端的对象,客户端正是通过这个代理来执行AIDL中的接口方法的。

Stub

接下来我们深入两个类来看其代码,首先stub通过android.os.IInterfaceattachInterface方法来完成自我构造。接下来是asInterface方法,客户端正是利用这个方法来获取到远程服务对象的。在该方法中,首先是在本地查询是否有DESCRIPTOR所描述的类,如果存在直接返回;如果不存在就返回proxy。这说明当客户端在调用该方法获取远程服务时,实际上服务端首先是会检查服务端是否和客户端在同一个进程中,如果在则直接返回自身,如果不是则返回proxy代理。
onTransact是指令的执行的函数,也即执行AIDL接口定义的函数实际上最终都会执行到这里。data是传入数据,reply是返回数据,两个数据都是Parcel类说明在AIDL的通信过程中数据必须经过序列化操作。不同的指令执行不同的switch分支:1)读取数据;2)执行指令;3)返回数据。

Proxy

读懂了Stub的源码,Proxy的源码就更加简单了,Proxy类中有一个mRemote属性,该属性就是远端的服务端stub。当客户端利用proxy代理执行对应的方法时,proxy的执行逻辑都是:1)声明传入数据和返回结果两个序列化对象;2)写入传入数据;3)执行方法,执行的逻辑就是调用mRemote的onTransact方法在远端执行方法;4)执行完成后读取返回结果

以上就是从代码层面来解析AIDL机制的实现原理,其实如果读懂了代码,AIDL的实现原理也不难,stub-proxy模式也是一个常用的设计模式。但是数据和指令究竟是怎么在两个进程之间进行传递的,源码中却没有体现,这就需要了解AIDL的底层实现机制Binder了。

作者:asiaLIYAZHOU 发表于2016/9/13 23:28:59 原文链接
阅读:84 评论:0 查看评论

Swift枚举详解

$
0
0
枚举为一系相关联的值定义了一个公共的组类型.同时能够让你在编程的时候在类型安全的情况下去使用这些值。 如果你对C语言很熟悉,你肯定知道在C语言中枚举类型就是一系列具有被指定有关联名称的的整数值.但在Swift中枚举类型就更加灵活了,并且你不必给枚举类型中的每个成员都赋值。如果把一个值(假设值为"raw")提供给所有的枚举类型当中的成员,那么这个值可以是一个字符串,一个字符,一个整数或者说是一个浮点数. 作为选择,枚举中的成员可以被特别指定为任何不同于其他成员的可以存储的类型,就像是其他语言当中的联合或者变体。你能够将一些相关联的成员定义成一个公共的集合作为枚举的一部分,每个部分都有不同的集合类型并且使用了恰当的类型。

枚举语法

你可以用enum开始并且用大括号包含整个定义体来定义一个枚举:
  1. enum SomeEnumeration {
  2. // 在这里定义枚举
  3. }
这里有一个例子,定义了一个包含四个方向的罗盘:
  1. enum CompassPoint {
  2. case North
  3. case South
  4. case East
  5. case West
  6. }
枚举中定义的变量(像上例中North, South, East, West)是枚举的成员变量(或者说成员).关键字case是用来标明这一行将要定义一个新的成员变量
注意:与C或者Objective-C不同的是,在Swift语言中枚举类型的成员初始的时候不会被默认赋值成整数值,在CompassPoint这个例子中,North, South, East, West默认不会隐式的等于0,1,2,3。取而代之的是不同的枚举成员将要用什么类型以及赋值什么值都是可以自己控制的,可以在定义CompassPoint这个枚举的时候指定.
多个成员还可以用一行来定义,他们之间用逗号分割:
  1. enum Plant{
  2. case Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
  3. }
每个枚举的定义都是定义了一个全新的类型,就像Swfit中的其他的类型一样,枚举的名称(像上边的CompassPoint, Plant)应该是以一个大写字母开头,让他们是单数类型而不是复数类型,从而让他们可以不言而喻:
  1. var directionToHead = CompassPoint.West
当directionToHead在初始化过程中被赋值成CompassPoint中的某一个可能的值的时候,它的类型就可以被推测出来来了。一旦directionToHead被声明成是CompassPoint类型,那么你就可以简短的使用逗号表达式来给它赋值成其他的CompassPoint当中的值了:
  1. directionToHead = .East
directionToHead的类型是已知的了,所以你可以忽略它的类型来给他赋值了。这样使得在使用显示类型的枚举值时代码具有很高的可读性。

使用Switch语句来匹配枚举值

你可以通过switch语句来访问单独的某个枚举值:
  1. directionToHead = .South
  2. switch directionToHead {
  3. case .North:
  4. println("Lots of planets have a north")
  5. case .South:
  6. println("Watch out for penguins")
  7. case .East:
  8. println("Where the sun rises")
  9. case .West:
  10. println("Where the skies are blue")
  11. }
  12. // 输出"Watch out for penguins”
你可以这样阅读这段代码:考虑directionToHead的值,如果它等于.North那么就输出"Lost of planets have a north",如果它等于.South,那么就输出"Watch out for penguins"。

就和在控制流程那一章所讲,一个switch语句被用到判断枚举值的时候,必须要包括所有的枚举成员。假设.West被忽略了,将会导致编译出错,因为它没有考虑到枚举的所有的枚举成员,我们需要全面性的确保枚举的所有成员不被忽略掉.

如果给考虑每个枚举的成员不合适,你可以提供一个default来覆盖其他没有明确处理的成员:
  1. let somePlanet = Planet.Earth
  2. switch somePlanet {
  3. case .Earth:
  4. println("Mostly harmless")
  5. default:
  6. println("Not a safe place for humans")
  7. }
  8. // 输出 "Mostly harmless"

关联值

在上一节的示例中显示了一个枚举的成员是如何在自己的权利界定(和类型)的值。你可以设置一个常量或变量的值为Planet.Earth ,然后检查这个值。然而,如果在保留成员值的同时能够存储其它类型的关联值将会变得更有意义。这使您能够在保存成员值的同时存储额外的自定义信息,并且允许每次你在代码中使用这些成员值的时候改变这些关联值。在Swift中当你定义一个枚举成员的时候,你可以给他关联任何的类型,而且如果需要的话每个成员可以有不同的关联类型。枚举类型的这个特性和其他语言当中的辨别联合,标记联合或者变体很像。

举个例子,设想一个库存跟踪系统想要通过两种不同的条形码来跟踪产品。一些产品用UPC-A格式的一维条形码标识的,使用0到9的数字。每个条形码当中有一个标识“数字系统“的数字,然后是10个“标识符"数字,最后边一个用来做“检查”的数字,以确保这个条形码被正确的扫描识别:


另一些产品是用QR编码格式的二维码标识的,这种条形码可以使用任何ISO 8859-1的字符而且最大可以编码一个2953字符长度的字符串:


如果能够用一个有三个整型的元组来存储UPC-A格式的条形码,然后用一个可以存储任意长度的字符串来存储QR格式的条形码,那么对于一个库存跟踪系统来说,将会是再便捷不过的了。

在Swift语言中,一个可以定义两个格式的产品条形码的枚举看起来是这样的:
  1. enum Barcode {
  2. case UPCA(Int, Int, Int)
  3. case QRCode(String)
  4. }
你可以这样阅读这段代码:定义了一个叫做Barcode的枚举类型,它可以有一个UPCA成员,这个成员关联了一个包含三个整型数值的元组,同时这个枚举类型还有一个QRCode成员,关联了一个字符串。

这个定义不会生成任何的整型或者字符串值,他只是定义了当一个不可变变量或者变量等于Barcode.UPCA或者Barcode.QRCode的时候它被关联的值的类型

这样一来可以用任意其中一个类型来生成一个新的条形码了:
  1. var productBarcode = Barcode.UPCA(8, 85909_51226, 3)
这个例子生成了一个新的变量叫做productBarcode,这个变量被关联了一个Barcode.UPCA类型的元组,这个元组的值为(8, 8590951226, 3),被提供做“标识”的值用下划线分割了-85909_51226,这样做是为了更好的被以条形码读出来。

同样的产品还可以被赋值为另一个条形码类型:
  1. productBarcode = .QRCode("ABCDEFGHIJKLMNOP")
在这一点上,原来的Barcode.UPCA以及关联到的整型值被一个新类型Barcode.QRCode以及与其关联的字符串给替换了。Barcode类型的不可变变量以及可变变量可以存储.UPCA或者.QRCode类型(同时还有与其相关联的值),但是每次都只能存储这两个类型当中的一个。

同样,这两个不同的类型可以用Switch语句来做检查。同时,在switch语句中他们相关联的值也可以被获取到。你可以把关联的值当做不可变变量(用let来开头),或者可变变量(用var开头)以在switch的控制体当中使用。
  1. switch productBarcode { case .UPCA(let numberSystem, let identifier, let check): println("UPC-A with value of \(numberSystem), \(identifier), \(check).") case .QRCode(let productCode): println("QR code with value of \(productCode).") }
如果一个枚举成员关联的所有值都被当做不可变变量或者可变变量来使用,那么你可以在成员名称之前只放一个let或者var来达到目的,简要示例:
  1. switch productBarcode {
  2. case let .UPCA(numberSystem, identifier, check):
  3. println("UPC-A with value of \(numberSystem), \(identifier), \(check).")
  4. case let .QRCode(productCode):
  5. println("QR code with value of \(productCode).")
  6. }
  7. // 输出 "QR code with value of ABCDEFGHIJKLMNOP.”

原始值

在上一节当中条形码的例子展示了一个枚举类型的成员怎么声明他们可以存储不同类型的关联值。不同于关联值,枚举类型的成员还可以预设置默认值(我们叫他原始值),这些值的类型是相同的。

这里有一个枚举成员存储一个ASCII值的例子:
  1. enum ASCIIControlCharacter: Character {
  2. case Tab = "\t"
  3. case LineFeed = "\n"
  4. case CarriageReturn = "\r"
  5. }
这里定义了一个原始值为字符类型的枚举类型ASCIIControlCharacter,而且包含了一些我们常用的控制字符。关于字符类型,你可以在字符串和字符那个章节找到更多的描述。

请注意,原始值与关联值不同。原始值应该是在你定义枚举的代码中被设置为预填充值的,就像上述三个ASCII码。对于一个特定的枚举成员的原始值始终是相同的。关联值是当你创建一个基于枚举的成员的新的常量或变量的时候设置的,并且每次都可以是不同的。

原始值可以是字符串,字符或者其他任何的整型或者浮点型等数字类型。每个原始值在他属的枚举类型定义的时候都应该是不同的。如果原始值是整数类型,那么当其他枚举成员没有设置原始值的时候,他们的原始值是这个整型原始值自增长设置的。

下边这个枚举类型是对之前的Plant枚举类型的改良,新的枚举类型有一个整型的原始值来标识他们在距离太阳的顺序:
  1. enum Planet: Int {
  2. case Mercury = 1, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
  3. }
自增长的意思就是Planet.Venus的原始值会被设置成2,以此类推。

可以通过枚举成员的toRaw()方法来获取他的原始值:
  1. let earthsOrder = Planet.Earth.toRaw()
  2. //eathsOrder is 3
使用枚举成员的fromRaw()方法来尝试通过一个原始值来寻找他所对应的枚举成员。下面这个例子介绍了怎么通过Uranus的原始值7找到Uranus的:
  1. let possiblePlanet = Planet.fromRaw(7)
  2. // possiblePlanet is of type Planet? and equals Planet.Uranus”
然而不是所有的整型值都可以找到一个对应的Planet,正是如此,fromRaw()将会返回一个非强制类型的枚举成员。上边展示的例子当中possiblePlanet是一个Planet?类型,或者是一个非强制Planet类型。

如果你尝试通过原始值9来寻找他对应的Planet,那么fromRaw()返回给你的非强制Planet类型将会是nil:
  1. let positionToFind = 9
  2. if let somePlanet = Planet.fromRaw(positionToFind) {
  3. switch somePlanet {
  4. case .Earth:
  5. println("Mostly harmless")
  6. default:
  7. println("Not a safe place for humans")
  8. }
  9. } else {
  10. println("There isn't a planet at position \(positionToFind)")
  11. }
  12. // 输出 "There isn't a planet at position 9”
这个例子尝试使用非强制类型绑定来试着用原始值9来获取一个Planet。表达式if let somePlanet = Planet.fromRaw(9)用来检索一个Planet,如果能够检索到Planet将会将这个非强制Planet赋值给somePlanet。在这个示例中,通过原始值9不可能检索到对应的Planet,所以else包含的语句将会被执行。
作者:agonie201218 发表于2016/9/14 0:05:56 原文链接
阅读:73 评论:0 查看评论

BLE(Bluetooth Low Energy)---first part

$
0
0

   

      原文地址:https://developer.android.com/guide/topics/connectivity/bluetooth-le.html#terms


    (本人是技术宅,翻译时候,只要以简洁易懂为准,看过一部分后,如果感觉我翻译的不够文艺,文法,咋样咋样的,请不要往下看了,你可以去看原文了,就别在这儿浪费时间了~)


     安卓4.3(api level>=18)引入了内置的平台支持低功耗设备。通过这套API,应用能发现设备,查询设备,读写一些特性(不知道该翻译成啥,先这样)。与传统蓝牙相比,低功耗的BLE提供了明显更少的耗电量。这允许安卓app和BLE设备在低电量要求下,例如,贴身的传感器,心率设备,健身设备等。

 

   Key Terms and Concepts


      一些概念性的东西先说下:


      Generic Attribute Profile (GATT)——GATT是一个发送和接收短距离数据的规范。所有低耗能应用基本上基于GATT。

          蓝牙SIG为低耗能设备定义了很多规范。一个规范说明了一个设备该如何工作在应用中。一个设备可以实现很多个规范。例如,一个设备能包含心率设备和一个电池探测器。


      Attribute Protocol (ATT)——GATT是构筑在ATT协议上的。经常俩货放在一起玩儿。ATT是被优化后跑在BLE设备上的。到结束的时候,它尽量使用更少的字节。每一个特性都是用UUID唯一标识的,用这个128位做唯一标识。这个特性被规范成了characteristics and services(下面再说这俩货)

        Characteristic一个特征包含一个单独的value和一个0-n的描述来描述特性的值。一个特性能被理解为一个类型,类似一个类。


        Descriptor—描述被特性定义,用来形容一个特征的值。例如,一个描述可能被指定成一个人类可读的描述,一个特性可以接受的值的段,或者是一个特性的具体的值。


          Service一个服务是一个组特性的集合。例如,你可以有一组服务叫“”心率啥啥啥“”。里面包括心率测量特性。你可以再蓝牙4.0官网找到一些官方提供的服务。


       先睡了,等中秋回家养肉的时候,再继续更。

 

 

作者:lhc2207221755 发表于2016/9/14 0:07:16 原文链接
阅读:115 评论:0 查看评论

[HTML5之APP实战]基于ionic开发的一款KTV移动应用

$
0
0

这里写图片描述

摘要

前期一直介绍web 移动开发的一些基本知识,而没有给大家演示过一个项目,今天给大家给大家详细介绍一下如何利用HTML5来完成一个移动APP应用.在正式介绍前,希望大家能搞明白以下几个问题.

请大家思考以下几个问题?

问题一 HTML5中用于移动开发框架有哪些?
问题二 现在流行的框架是什么,优势?
问题三 在企业实际项目中,用的最多移动网页框架是什么?
问题四 如何有效利用框架来快速写移动页面或APP?
问题五 利用该框架开发出来的APP性能到底如何?
问题六 开发出来的web APP如何上线

(注意:只有弄清楚了上面几个问题,整体去把控,才能在最短的时间内来开发出我们想要的东西出来.)

具体说明

下面就上面几个问题进行说明,让大家多多了解,有说的不对的地方,希望大家多多包涵指正,相互学习,一起进步.

问题一 HTML5中用于移动开发常用的比较好用的框架有哪些?

这个问题我在之前的几篇文章已经详细说明了,大家可以回过头去看看.

企业HTML5前端开发最需要的技能及技术难点分析
这里写图片描述

用于HTML5移动开发的几大移动APP开发框架

上面是之前关于移动框架的一个总结.
下面流行的框架给大家说明一下.

问题二 现在流行的框架是什么,优势?

目前主要前端流行的框架主要:

1. angular框架

2. vue框架

3. react框架

4. ant-design框架

5. ionic框架

1. angular框架

这里写图片描述

1.1 框架:

angular是个MVVM的框架

1.2 运用场景

angular的最主要的场景就是单页应用,或是有大量数据绑定的情况。

1.3 特点

  1. Angular的数据观测采用的是脏检查(dirty checking)机制。
  2. 每一个指令都会有一个对应的用来观测数据的对象,叫做watcher;
  3. 一个作用域中会有很多个watcher。
  4. 每当界面需要更新时,Angular会遍历当前作用域里的所有watcher,对它们一一求值,然后和之前保存的旧值进行比较。

2. vue框架

这里写图片描述

2.1 框架:

Vue.js采用的则是基于依赖收集的观测机制。从原理上来说,和老牌MVVM框架Knockout是一样的。
这里写图片描述

2.2 运用场景

应用的场景比较广,只想用vue功能的话就用

2.3 特点

与Angular不同的是,Vue.js的API里并没有繁杂的module、controller、scope、factory、service等概念,一切都是以“ViewModel 实例”为基本单位.

2.4 基本原理

  1. 将原生的数据改造成 “可观察对象”。一个可观察对象可以被取值,也可以被赋值。
  2. 在watcher的求值过程中,每一个被取值的可观察对象都会将当前的watcher注册为自己的一个订阅者,并成为当前watcher的一个依赖。
  3. 当一个被依赖的可观察对象被赋值时,它会通知所有订阅自己的watcher重新求值,并触发相应的更新。
  4. 依赖收集的优点在于可以精确、主动地追踪数据的变化,不存在上述提到的脏检查的两个问题。

3. react框架

4. ant-design框架

5. ionic框架

这三个框架就不介绍了,之前已经说过了.大家可以之前的文章看看.
下面我们来看看用ionic来做一移动APP.

下面我们来看看用ionic来做一移动APP的效果

首页

代码片段

/*倒计时*/
    function mytime(){
        var D1 = new Date();
        var D2 = new Date(2016,5,12);
        var time = document.getElementById("ti");
        var dis = D2.getTime()-D1.getTime();
        var ds = Math.floor(dis/1000);
        var dd=Math.floor(ds/(60*60*24)); 
        var dh=Math.floor((ds-(dd*60*60*24))/(60*60));
        var dm=Math.floor((ds-(dd*60*60*24)-(dh*60*60))/60);
        var dS=ds-(dd*60*60*24)-(dh*60*60)-(dm*60);
            time.innerHTML=dh+":"+dm+":"+dS+"";
        }
    setInterval(function(){mytime()},1000);
    /*滚动*/
    $(function(){
    scroll({dom:".con_news",inter:2000,time:500,num:1})
    function scroll(opt){
        var ul = $(opt.dom+" .ul");
        var inter = opt.inter;
        var time = opt.time;
        var num = opt.num;
        var dis = ul.children("li").outerHeight()*num;
        var stopId;
        auto();
        bindDom();

        function animated(){
            ul.animate({"margin-top":24*-1},time,resetIn)
        }
        function resetIn(){
            var first = $(opt.dom+" .ul>li:lt("+num+")");
            ul.append(first);
            ul.css("margin-top",0);
        }
        function auto(){
            stopId = setInterval(function(){
                animated();
            },inter)
        }
        function bindDom(){
            $(opt.dom).hover(function(){
                clearInterval(stopId);
            },function(){
                auto();
            })
        }
    }
})

这里写图片描述

代码片段

<script type="text/javascript">
            $(function(){
                $(".lasthide").hide();
                $(".other a").click(function(){
                    if($(".other a").html()=="查看其他1个团购"){
                        $(".other a").html("没有更多了");
                    }else{
                        $(".other a").html("查看其他1个团购");
                    }
                    $(".lasthide").toggle();

                })
            })
        </script>

这里写图片描述

代码片段

........
$(".disc").click(function(){
                    $(".discshow").slideToggle();
                    $(".disc").toggleClass("ro");
                    $(".disc a").toggleClass("purper");
                    $(".nearshow").hide();
                    $(".near").removeClass("rotate");
                    $(".near a").removeClass("roya");
                })
                $(".discshow li").click(function(){
                    var ind = $(".discshow li").index($(this));
                    $(".discshow li.active").removeClass("active");
                    $(".discshow li:eq("+ind+")").addClass("active");
                })
........

这里写图片描述

代码片段

.......
var app = angular.module("myapp",["ionic"])
        .controller("myctrl",function($scope){

            $scope.onright = function(){
                $(".meal_cont").show();
                $(".meal_title li:eq(1)").removeClass("active");
                $(".meal_title li:eq(0)").addClass("active");
                $(".meal_tip").hide();
            }
            $scope.onleft = function(){
                $(".meal_title li:eq(0)").removeClass("active");
                $(".meal_title li:eq(1)").addClass("active");
                $(".meal_cont").hide();
                $(".meal_tip").show();

            }
        })
        .......

这里写图片描述

代码片段

<style>
        body{font-family: "Microsoft YaHei";background-color: #f2f2f2;}
        a{text-decoration: none;}
            .bar-linear{background: linear-gradient(to right,#E44BF1,#8225A7)}
            #header{line-height: 44px;color: #fff;font-size: 15px;padding: 0 15px;color: #fff;}
            #header .back{font-size:26px;}
            #header .title{color: #fff;font-size: 17px;font-family: "Microsoft YaHei";}
            .content{padding-bottom: 50px;}
            .item-thumbnail-left > img:first-child{left: 15px;}
            .list h3{font-weight: normal;color: #333;font-size: 16px;margin-top: 7px;font-family: "Microsoft YaHei";}
            .list h3 .item-note{color: #999;font-size: 13px;}
            .list p{color: #666;font-weight: normal;margin-top: 9px;}
            .list .item{border-color: #fff;}
            .list .item a{display: block;}
            .list{border-bottom: 1px solid #e0e0e0;}
        </style>

这里写图片描述

关键代码

        <style>
        body{font-family: "Microsoft YaHei";background-color: #f2f2f2;}
        a{text-decoration: none;}
        h3,.button{font-family: "微软雅黑";}
        .fl{float: left;}
        .fr{float: right;}
        .bar-linear{background: linear-gradient(to right,#E44BF1,#8225A7)}
        #header{line-height: 44px;color: #fff;font-size: 15px;padding: 0 10px 0 16px;color: #fff;}
        #header .back{font-size:25px;}
        #header a{color: #fff;}
        #header .title{color: #fff;font-size: 17px;font-family: "Microsoft YaHei";}
        /*content*/

        .date{color: #666;height: 37px;line-height: 37px;font-size: 15px;text-align: center;}
        .chatDetail{width: 100%;height: auto;padding-left: 16px;padding-right: 16px;}
        .chatDetail .chat{height: 51px;width: 100%;float: right;}
        .chat img{width: 51px;height: auto;}
        .chat div{position: relative;}
        .chat1{position: relative;left: 0;top: 0;width: 100%;height: auto;margin-top: 15px;}
        .chat1 img{width: 51px;height: auto;}
        .chatcon{background-color: #ccc;border-radius: 4px;padding: 7px;position: relative;color: #333;}
        .chaC{position: absolute;right: 65px;top: 7px;}
        .chaC:before{content: "";display: inline-block;width: 0;height: 0;border: 5px solid transparent;border-left-color: #ccc;position: absolute;top: 41%;right: -10px;z-index: 60;}
        .chat1 .chaT{position: absolute;left: 65px;top: 7px;}
        .chat1 .chaT:before{content: "";display: inline-block;width: 0;height: 0;border: 5px solid transparent;border-right-color: #ccc;position: absolute;top: 41%;left: -10px;z-index: 60;}
        /*footer*/
        .footer{padding-left: 16px;padding-right: 16px;}
        .footer label,.footer input{background-color: #ccc;}
        .footer .icon{font-size: 27px;color: #cecece;margin-right: 12px;margin-top: 3px;}
        .footer .button{background:#663399;color: #fff;}
        </style>

问题六 开发出来的web APP如何上线

这个主要用到的是APICloud第三方平台,只要具备iOS开发者账号或谷歌开发者账号,我们把代码提交上去,就可以打包成相应APP应用程序.大家可以尝试一下.

结束

相应的项目代码,大家可以关注我的公众号号,上面有相关github下载地址.

微信公众号

这里写图片描述

个人微信

这里写图片描述

作者:BaiHuaXiu123 发表于2016/9/14 1:12:41 原文链接
阅读:117 评论:0 查看评论

Android Multimedia框架总结(九)Stagefright框架之数据处理及到OMXCodec过程

$
0
0

转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼:http://blog.csdn.net/hejjunlin/article/details/52532085

不知不觉到第九篇了,感觉还有好多好多没有写,路漫漫其修远兮 ,吾将上下而求索,上篇主要介绍了Stagefright框架及AwesomePlayer的数据解析器,最后我们说道,涉及parse及decode部分,将在本篇中介绍,看下今天的Agenda:

  • 两张图看数据走向
  • AwesomePlayer中prepare过程
  • AwesomePlayer初始化音视频解码器过程
  • Stagefright的Decode过程
  • Stagefright处理数据过程
  • 数据由源到最终解码后的流程

两张图看数据走向

一切从MediaSource中来,一切又从MediaSource中去
Audio:
这里写图片描述

Video:
这里写图片描述

AwesomePlayer中prepare过程

首先我们开始看下AwesomePlayer的prepare的执行过程:

这里写图片描述
这里写图片描述

以上代码总结为:prepare过程调用了prepareAsync_l函数,在prepareAsync_l中执行new AwesomeEvent,并将AwesomePlayer调用onPrepareAsyncEvent的结果返回AwesomeEvent的构造作为参数。
接着分析AwesomeEvent的过程: 启动mQueue,作为event handler

这里写图片描述

上面new AwesomeEvent会执行onPrepareAsyncEvent函数,我们看下该函数的做了什么?

这里写图片描述
这里写图片描述

以上代码总结为:会将AV(音视频)进行分处理,于是有了AwesomePlayer::initVideoDecoder及AwesomePlayer::initAudioDecoder()函数。
本文出自逆流的鱼:http://blog.csdn.net/hejjunlin/article/details/52532085

初始化音视频解码器过程

我们先看下initVideoDecoder,即初始化视频解码器:

这里写图片描述
这里写图片描述

接着看下初始化音频解码器,看下几个变量的声明:

这里写图片描述

接着看代码如下:

这里写图片描述
这里写图片描述

对上面代码进行总结:
Stagefright调用AwesomePlayer的prepare后,AwesomePlayer调用自身的prepareAsync进行初始化化音视频解码器,这两个方法里面都会OMXCodec::Create,接下来看下这个过程。

Stagefright的Decode过程

经过“数据流的封装”得到的两个MediaSource,其实是两个OMXCodec。AwesomePlayer和mAudioPlayer都是从MediaSource中得到数据进行播放。AwesomePlayer得到的是最终需要渲染的原始视频数据,而mAudioPlayer读取的是最终需要播放的原始音频数据。也就是说,从OMXCodec中读到的数据已经是原始数据了。
OMXCodec是怎么把数据源经过parse、decode两步以后转化成原始数据的。从OMXCodec::Create这个构造方法开始,下面看它的代码:

这里写图片描述
这里写图片描述
这里写图片描述

以上代码总结为:对应参数分析:

  • IOMX &omx指的是一个OMXNodeInstance对象的实例。
  • MetaData &meta这个参数由MediaSource.getFormat获取得到。这个对象的主要成员就是一个KeyedVector(uint32_t, typed_data) mItems,里面存放了一些代表MediaSource格式信息的名值对。
  • bool createEncoder指明这个OMXCodec是编码还是解码。
  • MediaSource &source是一个MediaExtractor(数据解析器)。
  • char *matchComponentName指定一种Codec用于生成这个OMXCodec。
    先使用findMatchingCodecs寻找对应的Codec,找到以后为当前IOMX分配节点并注册事件监听器:omx->allocateNode(componentName, observer, &node)。最后,把IOMX封装进一个OMXCodec:

这里写图片描述

这样就得到了OMXCodec。

  • AwesomePlayer中得到这个OMXCodec后,接着看initVideoDecoder/initAudioDecoder,这里看initAudioDecoder方法,是把 mAudioSource = mOmxSource,赋值,接着调用mAudioSource->start()进行初始化。 OMXCodec初始化主要是做两件事:
    • 向OpenMAX发送开始命令。mOMX->sendCommand(mNode, OMX_CommandStateSet, OMX_StateIdle)
    • 调用allocateBuffers()分配两个缓冲区,存放在Vector mPortBuffers[2]中,分别用于输入和输出。
  • 然后在现个initxxxDecoder方法中会调用(mAudioSource->start()/mVideoSource->start())

这里写图片描述

触发MediaSource的子类VideoSource及AudioSource调用start()方法后,它的内部就会开始从数据源获取数据并解析,等到缓冲区满后便停止。在AwesomePlayer里就可以调用MediaSource的read方法读取解码后的数据。

  • 对于mVideoSource来说,读取的数据:mVideoSource->read(&mVideoBuffer, &options)交给显示模块进行渲染,mVideoRenderer->render(mVideoBuffer);
  • 对mAudioSource来说,用mAudioPlayer对mAudioSource进行封装,然后由mAudioPlayer负责读取数据和播放控制。

  • AwesomePlayer调用OMXCode读取ES数据,并且进行解码的处理

  • OMXCodec调用MediaSource的read函数来获取音视频的数据
  • OMXCodec调用Android的IOMX接口,其实就是Stagefrightdecode中的 OMX实现

本文出自逆流的鱼:http://blog.csdn.net/hejjunlin/article/details/52532085

这个过程就是prepare的过程,重点是解码把流放到Buffer中
接下来,当java层调用start方法时,通过mediaplayerservice,再传到StagefrightPlayer中,引用AwesomePlayer,这样就调到AwesomePlayer的play方法,看代码:

这里写图片描述
这里写图片描述

  • 当AwesomePlayer调用play后,通过mVideoSource->read(&mVideoBuffer, &options)读取数据。mVideoSource->read(&mVideoBuffer, &options)具体是调用OMXCodec.read来读取数据。而OMXCodec.read主要分两步来实现数据的读取:
  • (1) 通过调用drainInputBuffers()对mPortBuffers[kPortIndexInput]进行填充,这一步完成 parse。由OpenMAX从数据源把demux后的数据读取到输入缓冲区,作为OpenMAX的输入。
  • (2) 通过fillOutputBuffers()对mPortBuffers[kPortIndexOutput]进行填充,这一步完成 decode。由OpenMAX对输入缓冲区中的数据进行解码,然后把解码后可以显示的视频数据输出到输出缓冲区。
    AwesomePlayer通过mVideoRenderer->render(mVideoBuffer)对经过parse和decode 处理的数据进行渲染。一个mVideoRenderer其实就是一个包装了IOMXRenderer的AwesomeRemoteRenderer:

这里写图片描述

Stagefright处理数据过程

  • Audioplayer为AwesomePlayer的成员,audioplayer通过callback来驱动数据的获取,awesomeplayer则是通过 videoevent来驱动。二者有个共性,就是数据的获取都抽象成mSource->read()来完成,且read内部把parse和decode绑在一起。Stagefright AV同步部分,audio完全是callback驱动数据流,注意是video部分在onVideoEvent里会获取audio的时间戳,是传统的AV时间戳做同步。

  • AwesomePlayer的Video主要有以下几个成员:

    • mVideoSource(解码视频)
    • mVideoTrack(从多媒体文件中读取视频数据)
    • mVideoRenderer(对解码好的视频进行格式转换,android使用的格式为RGB565)
    • mISurface(重绘图层)
    • mQueue(event事件队列)
  • stagefright运行时的Audio流程如下:

    • 首先设置mUri的路径
    • 启动mQueue,创建一个线程来运行 threadEntry(命名为TimedEventQueue,这个线程就是event调度器)
    • 打开mUri所指定的文件的头部,则会根据类型选择不同的分离器(如MPEG4Extractor)
    • 使用 MPEG4Extractor对MP4进行音视频轨道的分离,并返回MPEG4Source类型的视频轨道给mVideoTrack
    • 根据 mVideoTrack中的编码类型来选择解码器,avc的编码类型会选择AVCDecoder,并返回给mVideoSource,并设置mVideoSource中的mSource为mVideoTrack
    • 插入onVideoEvent到Queue中,开始解码播放
    • 通过mVideoSource对象来读取解析好的视频buffer

如果解析好的buffer还没到AV时间戳同步的时刻,则推迟到下一轮操作

1、mVideoRenderer为空,则进行初始化(如果不使用 OMX会将mVideoRenderer设置为AwesomeLocalRenderer)
2、通过mVideoRenderer对象将解析好的视频buffer转换成RGB565格式,并发给display模块进行图像绘制
3、将onVideoEvent重新插入event调度器来循环

本文出自逆流的鱼:http://blog.csdn.net/hejjunlin/article/details/52532085

Stagefright数据由源到最终解码后的流程

可以对照《Android Multimedia框架总结(八)Stagefright框架之AwesomePlayer及数据解析器》中那个图,这里不贴了,

  • 设置DataSource,数据源可以两种URI和FD。URI可以http://,rtsp://等。FD是一个本地文件描述符,能过FD,可以找到对应的文件。
  • 由DataSource生成MediaExtractor。通过sp extractor = MediaExtractor::Create(dataSource);来实现。 MediaExtractor::Create(dataSource)会根据不同的数据内容创建不同的数据读取对象。
  • 通过调用setVideoSource由MediaExtractor分解生成音频数据流(mAudioTrack)和视频数据流(mVideoTrack)。
  • onPrepareAsyncEvent()如果DataSource是URL的话,根据地址获取数据,并开始缓冲,直到获取到mVideoTrack和mAudioTrack。mVideoTrack和mAudioTrack通过调用initVideoDecoder()和initAudioDecoder()来生成 mVideoSource和mAudioSource这两个音视频解码器。然后调用postBufferingEvent_l()提交事件开启缓冲。
  • 数据缓冲的执行函数是onBufferingUpdate()。缓冲区有足够的数据可以播放时,调用play_l()开始播放。play_l()中关键是调用了postVideoEvent_l(),提交了 mVideoEvent。这个事件执行时会调用函数onVideoEvent()。这个函数通过调用 mVideoSource->read(&mVideoBuffer, &options)进行视频解码。音频解码通过mAudioPlayer实现。
  • 视频解码器解码后通过mVideoSource->read读取一帧帧的数据,放到mVideoBuffer中,最后通过 mVideoRenderer->render(mVideoBuffer)把视频数据发送到显示模块。当需要暂停或停止时,调用cancelPlayerEvents来提交事件用来停止解码,还可以选择是否继续缓冲数据。

第一时间获得博客更新提醒,以及更多android干货,源码分析,欢迎关注我的微信公众号,扫一扫下方二维码或者长按识别二维码,即可关注。

这里写图片描述

如果你觉得好,随手点赞,也是对笔者的肯定,也可以分享此公众号给你更多的人,原创不易

作者:hejjunlin 发表于2016/9/14 3:02:36 原文链接
阅读:56 评论:0 查看评论

【Android】Android插件开发 —— 打开插件的Activity(Hook系统方法)

$
0
0

Android打开插件中Activity的实现原理


摘要

Android打开插件Activity的方式有很多种,类名固定的可以使用预注册的方式。代理也是一种很好的方式,同时代理的方式也可以用于打开插件中的Service。这两种方式在之前的博客中都有分享:

这两种方式都有一些弊端,这篇文章要分享的是如何更好地打开插件中的Activity,采用Instrumentation注入的方式。


1. Activity是如何被实例化的

1.1 在哪一个类、哪一个方法中实例化的

当开发者实现一个Activity时,不能自己添加一个带参数的构造方法。如果添加了,也需要实现一个无参构造方法。原因是在调用Context.startActivity(…)后,系统会利用反射的方式根据activityClass实例化一个Activity对象:

Activity activity = (Activity)clazz.newInstance();

其中clazz就是传入的Activity的子类。这一行代码是在Instrumentaion.java中的newActivity(…)方法中的。也就是说,Activity的实例化是在Instrumentation这个类里面,通过反射的方式进行的。

1.2 谁持有了Instrumentation的引用

每个Activity对象都持有了一个mInstrumentation的变量,该变量并不是Activity自己创建的,而是由ActivityThread传递给Activity的。而在ActivityThread中,持有了一个名为mInstrumentation的变量。因此,创建Activity的Instrumentation的对象,是被ActivityThread持有着。


2. 如何偷梁换柱打开插件的Activity

2.1 Activity是否在AndroidManifest.xml注册的校验

如果一个Activity没有注册,想要打开它,会抛出异常:

Unable to find explicit activity class XXXXActivity have you declared this activity in your AndroidManifest.xml?

这个异常是Instrumentation的checkStartActivityResult方法中抛出的:

/** @hide */
public static void checkStartActivityResult(int res, Object intent) {
    if (res >= ActivityManager.START_SUCCESS) {
        return;
    }

    switch (res) {
        case ActivityManager.START_INTENT_NOT_RESOLVED:
        case ActivityManager.START_CLASS_NOT_FOUND:
            if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
                throw new ActivityNotFoundException(
                        "Unable to find explicit activity class "
                        + ((Intent)intent).getComponent().toShortString()
                        + "; have you declared this activity in your AndroidManifest.xml?");
            throw new ActivityNotFoundException(
                    "No Activity found to handle " + intent);
        ...(以下省略)
    }
}

很明显的一件事,插件中的Activity是没有在AndroidManifest.xml中注册的,直接打开肯定抛异常崩溃。读者可能会想:我要是直接重写这个方法,不管什么情况全部不抛异常,校验不就通过了吗?但是很抱歉,这个方法是static类型的,子类无法重写。这个条件我们没有办法解决,只能绕过去。

2.2 如何绕过Activity的注册校验

Instrumentation有一个很重要的事:checkStartActivityResult(…)方法 是在 newActivity(…)方法之前执行的。请看这两个信息:

  1. 当开发者使用一个已经注册过的Activity去接受校验时,肯定能通过校验;
  2. 实例化出来的Activity一定是经过校验的那一个Activity吗?不一定。

让我们看一下Instrumentation#newActivity(…)方法的具体实现(有两个重载实现):
第一个:

public Activity newActivity(Class<?> clazz, Context context, 
        IBinder token, Application application, Intent intent, ActivityInfo info, 
        CharSequence title, Activity parent, String id,
        Object lastNonConfigurationInstance) throws InstantiationException, 
        IllegalAccessException {

    Activity activity = (Activity)clazz.newInstance();
    ActivityThread aThread = null;
    activity.attach(context, aThread, this, token, 0, application, intent,
            info, title, parent, id,
            (Activity.NonConfigurationInstances)lastNonConfigurationInstance,
            new Configuration(), null);
    return activity;
}

第二个:

public Activity newActivity(ClassLoader cl, String className,
        Intent intent)
        throws InstantiationException, IllegalAccessException,
        ClassNotFoundException {
    return (Activity)cl.loadClass(className).newInstance();
}

注意参数列表中有一个 Intent intent 参数,这个参数就是我们在startActivity(Intent intent);传入的那个intent。两个重载方法的参数列表中都包含了这个intent。
所以绕过注册验证的思路大致就出来了:

  1. 第一步:
    Intent intent = new Intent(this, 某个已经注册的ActivityA);
    intent.putString(“插件中的的Activity的类名”);
  2. 系统在接受startActivity后,校验”已经注册Activity”是否注册,结果肯定是已经注册,顺利通过;
  3. 接着来到newActivity(…)方法准备实例化Activity
    我们通过反射的方式,事先替换掉newActivity(…)方法,改为我们自己的方法。在我们自己的newActivity方法中做以下工作:
Created with Raphaël 2.1.0开始从intent中获得通过校验的Activity的类,赋值给replyActivityClassA == ActivityA ?从intent中获得插件中的Activity的类名,赋值给pluginActivityClassName根据类名反射获得对应的类, pluginActivityClass由C实例化出插件Activity对象pluginActivity返回说明并不是要打开插件中的Activity结束yesno

最后将pluginActivity就打开我们插件中的Activity了。修改后的代码如下:
其中,SonaInner.getRelayActivity() 就是事先约定好的中继Activity,也就是上面说得ActivityA。
另外,PluginManager.loadClassFromPlugin(…) 方法可以简单理解为是Class.forName()

package com.mzdxl.sona.hooks;

import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextThemeWrapper;

import com.mzdxl.sona.Log;
import com.mzdxl.sona.PluginManager;
import com.mzdxl.sona.SonaInner;

import java.lang.reflect.Field;
import java.lang.reflect.Method;

/**
 * @author 郑海鹏
 * @since 2016/5/16 21:57
 */
public class InstrumentationHook extends android.app.Instrumentation{

    /* -------------------------------------------------------------
                                              Fields
    ------------------------------------------------------------- */
    public static final String PLUGIN_ACTIVITY_NAME = "plugin_activity";
    public static final String PLUGIN_PATH = "plugin_path";

    protected String pluginPath;


    /* -------------------------------------------------------------
                      System Override / Implements Methods
    ------------------------------------------------------------- */
    @Override
    public Activity newActivity(Class<?> clazz, Context context,
                                IBinder token, Application application, Intent intent, ActivityInfo info,
                                CharSequence title, Activity parent, String id,
                                Object lastNonConfigurationInstance) throws InstantiationException,
            IllegalAccessException {
        Log.i("CustomInstrumentation#newActivity 执行了!code 1");

        Activity handleResult = createActivity(intent);
        if (handleResult != null){
            return handleResult;
        }

        return super.newActivity(clazz, context, token, application, intent, info, title, parent, id, lastNonConfigurationInstance);
    }

    @Override
    public Activity newActivity(ClassLoader cl, String fromClassName, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        Log.i("CustomInstrumentation#newActivity 执行了!code 2");

        Activity handleResult = createActivity(intent);
        if (handleResult != null){
            handleResult.attach(context, null, this, token, 0, application, intent,
                info, title, parent, id,
                (Activity.NonConfigurationInstances)lastNonConfigurationInstance,
                new Configuration(), null);
            return handleResult;
        }`

        return super.newActivity(cl, fromClassName, intent);
    }

    @Override
    public void callActivityOnCreate(Activity activity, Bundle icicle) {
        super.callActivityOnCreate(activity, icicle);
        injectResources(activity);
    }

    /* -------------------------------------------------------------
                                           Methods
    ------------------------------------------------------------- */
    /**
     * 根据Intent中的class判断是否存在中继Activity,如果目标Activity是中继Activity则打开插件
     */
    @SuppressWarnings("unchecked")
    protected Activity createActivity(Intent intent){
        SonaInner.checkInit();

        // 获得Intent中要打开的Activity
        String className = intent.getComponent().getClassName();

        // 判断该Activity是否是中继Activity
        if (SonaInner.getRelayActivity().getName().equals(className)) {
            // 如果是中继Activity,取出真实想启动的插件的Activity的类名、插件的位置
            String pluginActivityName = intent.getStringExtra(PLUGIN_ACTIVITY_NAME);
            pluginPath = intent.getStringExtra(PLUGIN_PATH);
            // 实例化Activity
            Class<? extends Activity> PluginActivity;
            try {
                if (pluginPath == null){
                    // 如果插件保存地址为null,不从插件中找
                    PluginActivity = (Class<? extends Activity>) Class.forName(pluginActivityName);
                }else{
                    PluginActivity = PluginManager.loadClassFromPlugin(getContext(), pluginPath, pluginActivityName);
                }
                return PluginActivity == null ? null : PluginActivity.newInstance();
            } catch (Exception e) {
                e.printStackTrace();
                Log.e("Intent中传入的插件的Class名无法实例化, 或者Class名不是一个Activity的类名,或者对应插件中不包含这个Activity。");
            }
        }
        return null;
    }

    /**
     * 注入插件的资源
     */
    protected void injectResources(Activity activity){
        if (pluginPath == null){
            return;
        }

        // 获取Activity的Resource资源
        Resources hostResource = activity.getApplication().getResources();

        // 获取插件的Resource
        try {
            // 获得系统assetManager
            AssetManager assetManager = AssetManager.class.newInstance();
            // 将插件地址添加到资源地址
            Method method_addAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            method_addAssetPath.setAccessible(true);
            method_addAssetPath.invoke(assetManager, pluginPath);

            // 获得新的完整的资源
            Resources resources = new Resources(assetManager, hostResource.getDisplayMetrics(), hostResource.getConfiguration());
            Field field_mResources = ContextThemeWrapper.class.getDeclaredField("mResources");
            field_mResources.setAccessible(true);
            field_mResources.set(activity, resources);
        } catch (Exception e) {
            e.printStackTrace();
            Log.e("复制插件Resource时出现异常");
        }
    }
}

2.3 如何用自己的newActivity方法替换系统方法

在1.2中已经提到了,Instrumentation的对象是保存在ActivityThread里的。很不幸,ActivityThread对象我们无法直接获得到,同时连ActivityThread这个类我们在代码中都不能直接调用。ActivityThread有一个静态方法:

public static ActivityThread currentActivityThread() {
    return sCurrentActivityThread;
}

该方法返回了当前的ActivityThread。
我们可以通过反射的方式,先获得ActivityThread,再获得currentActivityThread方法,最后调用这个方法就可以获得ActivityThread的变量(设为activityThread)了。最后把我们自己的Instrumentation对象替换掉activityThread里的mInstrumentation就可以了。代码如下:
其中:

  • ActivityThread_method_currentActivityThread 就是ActivityThread的currentActivityThread方法;
  • activityThread就是当前的ActivityThread。
  • InstrumentationHook 是我们自定义的Instrumentaion,修改了系统的newActivity。
package com.mzdxl.sona;

import android.content.res.AssetManager;

import com.mzdxl.sona.hooks.InstrumentationHook;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * @author 郑海鹏
 * @since 2016/5/17 10:25
 */
public class HookManager {
    /* -------------------------------------------------------------
                                              Fields
    ------------------------------------------------------------- */
    static Class<?> ActivityThread;
    static Method ActivityThread_method_currentActivityThread;
    static Object obj_activityThread;
    static Method AssetManager_method_addAssetPath;


    /* -------------------------------------------------------------
                                        Static Methods
    ------------------------------------------------------------- */
    /**
     * 初始化操作,获得一些基本的类、变量、方法等。
     */
    public static void init() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // 获得ActivityThread类
        ActivityThread = Class.forName("android.app.ActivityThread");

        // 获得ActivityThread#currentActivityThread()方法
        ActivityThread_method_currentActivityThread = ActivityThread.getDeclaredMethod("currentActivityThread");

        // 根据currentActivityThread方法获得ActivityThread对象
        obj_activityThread = ActivityThread_method_currentActivityThread.invoke(ActivityThread);

        // 获得AssetManager#addAssetPath()方法
        AssetManager_method_addAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
        AssetManager_method_addAssetPath.setAccessible(true);
    }

    /**
     * 注入Sona的Instrumentation
     */
    public static void injectInstrumentation() throws NoSuchFieldException, IllegalAccessException {
        Log.i("开始注入Sona的Instrumentation。");
        // 获得ActivityThread类中的Instrumentation字段
        Field field_instrumentation = obj_activityThread.getClass().getDeclaredField("mInstrumentation");
        field_instrumentation.setAccessible(true);
        // 创建出一个新的Instrumentation
        InstrumentationHook obj_custom_instrumentation = new InstrumentationHook();
        // 用Instrumentation字段注入Sona的Instrumentation变量
        field_instrumentation.set(obj_activityThread, obj_custom_instrumentation);
    }

}

最后我们需要在自己Application中的onCreate()方法中调用HookManager.init() 和 HookManager.injectInstrumentation()就可以替换为我们自己的Instrumentation了。

如果读者不太清楚如何加载插件中的类到内存中等细节,可以参考前面两篇文章。

作者:H28496 发表于2016/9/14 3:04:30 原文链接
阅读:64 评论:0 查看评论

独立看门狗

$
0
0

一,看门狗简介

由于单片机会受到来自外界电磁场的干扰,造成程序跑飞使系统陷入死循环,造成不可预知的后果
于是有了用于检测单片机程序运行状态的模块或芯片,叫做”看门狗”(watchdog)

看门狗作用:检测系统,当系统跑飞时复位系统,重新执行程序


二,STM32看门狗

STM32有两个看门狗:独立看门狗和窗口看门狗

独立看门狗:

 驱动:由LSI驱动,即使主时钟发生故障它仍有效

 适用场景:
      作为主程序之外能够完全独立工作
      对时间精度要求较低

窗口看门狗:

 驱动:由APB1时钟分频后得到时钟驱动

 适用场景:
      在精确计时窗口起作用的程序

三,独立看门狗原理

启动:键值寄存器IWDG_KR写入0xCCCC开始启用独立看门狗
过程:计时器从重装载值(由寄存器IWDG_RLR配置,默认0xFFF)开始递减,当递减到0x000时产生复位信号IWDG_RESET
喂狗:键值寄存器IWDG_KR写入0xAAAA,重装载寄存器IWDG_RLR的值会重新加载到递减计数器,避免看门狗复位
复位:如果程序异常跑飞,不能完成喂狗,系统复位

这里写图片描述

如图:
    独立看门狗时钟LSI通过预分频(IWDG_PR)
    向减值寄存器IWDG_KR写入0xCCCC,开启独立看门狗,此时重装载值写入递减计数器
    在任何时间向IWDG_KR写入0xAAAA完成喂狗,重装载寄存器IWDG_RLR的值会重新加载到递减计数器,避免看门狗复位
    如果在0xFFF递减到0x000前未完成喂狗,系统复位

四,独立看门狗相关寄存器

键值寄存器IWDG_KR: 0~15位有效

这里写图片描述

写入0x5555取消IWDG_PR和IWDG_RLR写保护
写入0xCCCC开启独立看门狗
写入0xAAAA喂狗

预分频寄存器IWDG_PR:0~2位有效。具有写保护功能,要操作先取消写保护

这里写图片描述

重装载寄存器IWDG_RLR:0~11位有效。具有写保护功能,要操作先取消写保护。

这里写图片描述

0-11位[11:0]有效,[12:31]位保留 默认值,最大值为0xFFF
当IWDG_KR写入0xAAAA是,重装载值会被传送到计数器中
IWDG_SR寄存器RVU位为0时才能对IWDG_RLR寄存器进行修改

状态寄存器IWDG_SR:0~1位有效

这里写图片描述


五,独立看门狗超时时间

这里写图片描述

看门狗时钟源 : 由LSI经过IWDG_PR分频提供
     LSI时钟频率=40MHz
     IWDG_PR预分频系数 P = 4 * 2^prer
     所以看门狗时钟频率 = 40/P

IWDG_RLR计数器重装载值,每经过一个看门狗时钟周期-1
看门狗时钟周期 = P/40

看门狗移除时间:
     Tout = ((4 * 2^prer) * RLR) / 40

最短时间 : 一个看门狗时钟周期  即IWDG_RLR=0x001
最长时间 : IWDG_RLR寄存器最大值(0xFFF) * 看门狗时钟周期

六,独立看门狗相关库函数


void IWDG_WriteAccessCmd(uint16_t IWDG_WriteAccess); // 取消写保护:0x5555使能
void IWDG_SetPrescaler(uint8_t IWDG_Prescaler);    // 设置预分频系数:写PR
void IWDG_SetReload(uint16_t Reload);          // 设置重装载值:写RLR
void IWDG_ReloadCounter(void);              // 喂狗:写0xAAAA到KR
void IWDG_Enable(void);                  // 使能看门狗写0xCCCC到KR
FlagStatus IWDG_GetFlagStatus(uint16_t IWDG_FLAG);  // 状态:重装载/预分频 更新

七,独立看门狗操作步骤

//取消寄存器写保护:
    IWDG_WriteAccessCmd();
//设置独立看门狗的预分频系数,确定时钟:
    IWDG_SetPrescaler();
//设置看门狗重装载值,确定溢出时间:
    IWDG_SetReload();
//使能看门狗
    IWDG_Enable();
//应用程序喂狗:
    IWDG_ReloadCounter();
//溢出时间计算:
    Tout=((4×2^prer)×rlr) /40

八,独立看门狗代码:

功能:
        独立看门狗设置1秒溢出,按下WK_UP按键喂狗
        如果不喂狗可以看到每1秒程序重启一次
        现象LED0闪烁,因为每次启动程序有500毫秒延时
        若连续快速按下WK_UP,使看门狗不进入溢出,LED0常亮

HAEDWARE/IWDG下新建Iwdg.h头文件

#ifndef __WDG_H
#define __WDG_H
#include “sys.h” //因为使用了u8 u16
/**
 * 看门狗初始化
 *     prer     预分频系数
 *     rlr      重装载值
 */
void IWDG_Init(u8 prer,u16 rlr);
void IWDG_Feed(void);  // 喂狗
#endif

头文件添加到项目配置
这里写图片描述

HAEDWARE/IWDG下新建Iwdg.c实现函数

在stm32f10x_iwdg.h中找到:

/** @defgroup IWDG_Exported_Functions
  * @{
  */
void IWDG_WriteAccessCmd(uint16_t IWDG_WriteAccess);     // 取消写保护
void IWDG_SetPrescaler(uint8_t IWDG_Prescaler);          // 设置预分频系数
void IWDG_SetReload(uint16_t Reload);                    // 设置重装载值
void IWDG_ReloadCounter(void);                           // 喂狗
void IWDG_Enable(void);                                  // 使能独立看门狗
FlagStatus IWDG_GetFlagStatus(uint16_t IWDG_FLAG);       // 获取状态
#include "wdg.h"

//看门狗初始化
void IWDG_Init(u8 prer,u16 rlr)
{
    IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);  // 取消IWDG_PR WDG_RLR写保护

    IWDG_SetPrescaler(prer);  // 设置预分频系数

    IWDG_SetReload(rlr);      // 设置重装载值

    IWDG_ReloadCounter();     // 喂狗-使第一次从重装载值开始计数,默认首次0xFFF

    IWDG_Enable();            // 使能看门狗
}

// 喂狗
void IWDG_Feed(void)
{
     IWDG_ReloadCounter();    // reload
}

USER/main.编写主函数

#include "led.h"         // LED
#include "delay.h"       // 延时函数
#include "key.h"         // 按键
#include "sys.h"
#include "usart.h"
#include "wdg.h"         // 独立看门狗

 int main(void)
 {
    delay_init();        // 延时函数初始化
    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // NVIC中断优先级分组
    uart_init(115200);   // 设置USART波特率115200
    LED_Init();          // LED端口初始化 - 默认LED0熄灭
    KEY_Init();          // 按键端口初始化

    delay_ms(500);       // 延时500ms - 复位时LED闪烁
    IWDG_Init(4, 625);   // 独立看门狗初始化-设置溢出时间1S-预分频系数4,重装载值625
    LED0=0;              // LED0点亮
    while(1)
    {
        if(KEY_Scan(0)==WKUP_PRES) // 按下按键WK_UP
        {
            IWDG_Feed(); // 喂狗
        }
        delay_ms(10);
    };
}

以上代码实现:
    独立看门狗设置1秒溢出,按下WK_UP按键喂狗
    如果不喂狗可以看到每1秒程序重启一次
    现象LED0闪烁,因为每次启动程序有500毫秒延时
    若连续快速按下WK_UP,使看门狗不进入溢出,LED0常亮
作者:ABAP_Brave 发表于2016/9/14 16:45:00 原文链接
阅读:128 评论:0 查看评论

Qt之高级网络操作(HTTP/FTP快速上手)

$
0
0

简述

Qt Network 模块中提供了一些高级别的类,例如:QNetworkRequest、QNetworkReply 和 QNetworkAccessManager,使用常见的协议执行网络操作。

支持的协议

在进行网络请求之前,首先,要查看 QNetworkAccessManager 支持的协议:

QNetworkAccessManager *manager = new QNetworkAccessManager(this);
qDebug() << manager->supportedSchemes();

通过调用 supportedSchemes(),列出了支持的所有 URL schemes:

(“ftp”, “file”, “qrc”, “http”, “https”, “data”)

下面,我们主要以 HTTP/FTP 为例。

请求

构建一个请求非常简单,本例中,我们尝试获取某个网页,以 CSDN 为例:

QString baseUrl = "http://www.csdn.net/";

QNetworkRequest request;
request.setUrl(QUrl(baseUrl));

现在,名为 request 的 QNetworkRequest 请求对象就构建成功了,为了获取所有想要的信息,我们需要将该请求发送出去。

QNetworkReply *pReplay = manager->get(request);

这时,会获取一个名为 pReplay 的 QNetworkReply 响应对象。等待响应完成,就可以从这个对象中获取所有想要的信息。

QByteArray bytes = pReplay->readAll();
qDebug() << bytes;

完整的源码:

// URL
QString baseUrl = "http://www.csdn.net/";

// 构造请求
QNetworkRequest request;
request.setUrl(QUrl(baseUrl));

QNetworkAccessManager *manager = new QNetworkAccessManager(this);
// 发送请求
QNetworkReply *pReplay = manager->get(request);

// 开启一个局部的事件循环,等待响应结束,退出
QEventLoop eventLoop;
QObject::connect(manager, &QNetworkAccessManager::finished, &eventLoop, &QEventLoop::quit);
eventLoop.exec();

// 获取响应信息
QByteArray bytes = pReplay->readAll();
qDebug() << bytes;

因为请求的过程是异步的,所以此时使用 QEventLoop 开启一个局部的事件循环,等待响应结束,事件循环退出。

注意:开启事件循环的同时,程序界面将不会响应用户操作(界面被阻塞)。

简便的 API 意味着所有 HTTP 请求类型都是显而易见的。那么其他 HTTP 请求类型:POST,PUT 又是如何的呢?都是一样的简单:

QNetworkReply *pPostReplay = manager->post(request, QByteArray());
QNetworkReply *pPutReplay = manager->put(request, QByteArray());

漂亮,对吧?但这也仅是冰山一角。

传递 URL 参数

你也许经常想为 URL 的查询字符串(query string)传递某种数据。如果你是手工构建 URL,那么数据会以键/值对的形式置于 URL 中,跟在一个问号的后面。例如:http://www.zhihu.com/search?type=content&q=Qt。Qt 提供了 QUrlQuery 类,可以很便利地提供这些参数。

举例来说,如果你想传递 type=contentq=Qthttp://www.zhihu.com/search,可以使用如下代码:

// URL
QString baseUrl = "http://www.zhihu.com/search";
QUrl url(baseUrl);

// key-value 对
QUrlQuery query;
query.addQueryItem("type", "content");
query.addQueryItem("q", "Qt");

url.setQuery(query);

通过打印输出该 URL,你能看到 URL 已被正确编码:

qDebug() << url.url();
// "http://www.zhihu.com/search?type=content&q=Qt"

更多参考:Qt之QUrlQuery

代理

说到代理,我们顺便介绍下 Fiddler(或其它 Web/HTTP 调试工具),Fiddler 是一个 HTTP 协议调试代理工具。

所以呢,在发送请求的时候,如果要通过 Fiddler 分析,就必须设置代理,主要使用 QNetworkProxy :

QNetworkProxy proxy;
proxy.setType(QNetworkProxy::HttpProxy);
proxy.setHostName("127.0.0.1");
proxy.setPort(8888);
manager->setProxy(proxy);

更多参考:Qt之QNetworkProxy(网络代理)

更加复杂的 POST 请求

httpbin 是一个使用 Python + Flask 编写的 HTTP 请求和响应服务,主要用于测试 HTTP 库。

通常,你想要发送一些编码为表单形式的数据 - 非常像一个 HTML 表单。要实现这个,只需简单地传递一个QByteArray 给 data 参数。你的数据在发出请求时会自动编码为表单形式:

// URL
QString baseUrl = "http://httpbin.org/post";
QUrl url(baseUrl);

// 表单数据
QByteArray dataArray;
dataArray.append("key1=value1&");
dataArray.append("key2=value2");

// 构造请求
QNetworkRequest request;
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
request.setUrl(url);

QNetworkAccessManager *manager = new QNetworkAccessManager(this);

// 发送请求
manager->post(request, dataArray);

这里写图片描述

还可以使用 json 参数直接传递,然后它就会被自动编码:

// URL
QString baseUrl = "http://httpbin.org/post";
QUrl url(baseUrl);

// Json数据
QJsonObject json;
json.insert("User", "Qter");
json.insert("Password", "123456");

QJsonDocument document;
document.setObject(json);
QByteArray dataArray = document.toJson(QJsonDocument::Compact);

// 构造请求
QNetworkRequest request;
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setUrl(url);

QNetworkAccessManager *manager = new QNetworkAccessManager(this);

// 连接信号槽
connect(manager, SIGNAL(finished(QNetworkReply *)), this, SLOT(replyFinished(QNetworkReply *)));

// 发送请求
manager->post(request, dataArray);

这里写图片描述

为了不阻塞界面,我们不再使用 QEventLoop,而用 QNetworkAccessManager 对应的信号,当响应结束就会发射 finished() 信号,将其链接到对应的槽函数上即可。

定制请求头

如果你想为请求添加 HTTP 头部,只要简单地调用 setHeader() 就可以了。

例如,发送的请求时,使用的 User-Agent 是 Mozilla/5.0 , 为了方便以后追踪版本信息,可以将软件的版本信息写入到 User-Agent 中。

QNetworkRequest request;
request.setHeader(QNetworkRequest::UserAgentHeader, "my-app/0.0.1");

User-Agent:包含发出请求的用户信息。

这里写图片描述

当然,除了 User-Agent 之外,QNetworkRequest::KnownHeaders 还包含其他请求头,它就是为 HTTP 头部而生的。根据 RFC 2616, HTTP 头部是大小写不敏感。

如果 QNetworkRequest::KnownHeaders 不满足需要,使用 setRawHeader()。

响应内容

要获取响应的内容,可以调用 readAll(),由于上述的 POST 请求返回的数据为 Json 格式,将响应结果先转化为 Json,然后再对其解析:

void replyFinished(QNetworkReply *reply)
{
    // 获取响应信息
    QByteArray bytes = reply->readAll();

    QJsonParseError jsonError;
    QJsonDocument doucment = QJsonDocument::fromJson(bytes, &jsonError);
    if (jsonError.error != QJsonParseError::NoError) {
        qDebug() << QStringLiteral("解析Json失败");
        return;
    }

    // 解析Json
    if (doucment.isObject()) {
        QJsonObject obj = doucment.object();
        QJsonValue value;
        if (obj.contains("data")) {
            value = obj.take("data");
            if (value.isString()) {
                QString data = value.toString();
                qDebug() << data;
            }
        }
    }
}

响应的内容可以是 HTML 页面,也可以是字符串、Json、XML等。最上面所发送的 GET 请求 获取的就是 CSDN 的首页 HTML。

响应状态码

我们可以检测响应状态码:

QVariant variant = pReplay->attribute(QNetworkRequest::HttpStatusCodeAttribute);
if (variant.isValid())
    qDebug() << variant.toInt();
// 200    

HTTP 状态码请参考:

最常见的是 200 OK,表示请求已成功,请求所希望的响应头或数据体将随此响应返回。

响应头

进入 Response Headers:

这里写图片描述

可以看到包含很多,和上面一样,使用任意 QNetworkRequest::KnownHeaders 来访问这些响应头字段。例如,Content Type:

QVariant variant = pReplay->header(QNetworkRequest::ContentTypeHeader);
if (variant.isValid())
    qDebug() << variant.toString();
// "text/html; charset=utf-8"

如果 QNetworkRequest::KnownHeaders 不满足需要,可以使用 rawHeader()。例如,响应包含了登录后的 TOKEN,位于原始消息头中:

if (reply->hasRawHeader("TOKEN"))
    QByteArray byte = reply->rawHeader("TOKEN");

它还有一个特殊点,那就是服务器可以多次接受同一header,每次都使用不同的值。但 Requests 会将它们合并,这样它们就可以用一个映射来表示出来,参见 RFC 7230

A recipient MAY combine multiple header fields with the same field name into one “field-name: field-value” pair, without changing the semantics of the message, by appending each subsequent field value to the combined field value in order, separated by a comma.

接收者可以合并多个相同名称的 header 栏位,把它们合为一个 "field-name: field-value" 配对,将每个后续的栏位值依次追加到合并的栏位值中,用逗号隔开即可,这样做不会改变信息的语义。

错误

如果请求的处理过程中遇到错误(如:DNS 查询失败、拒绝连接等)时,则会产生一个 QNetworkReply::NetworkError。

例如,我们将 URL 改为这样:

QString baseUrl = "http://www.csdnQter.net/";

发送请求后,由于主机名无效,必然会发生错误,根据 error() 来判断:

QNetworkReply::NetworkError error = pReplay->error();
switch (error) {
case QNetworkReply::ConnectionRefusedError:
    qDebug() << QStringLiteral("远程服务器拒绝连接");
    break;
case QNetworkReply::HostNotFoundError:
    qDebug() << QStringLiteral("远程主机名未找到(无效主机名)");
    break;
case QNetworkReply::TooManyRedirectsError:
    qDebug() << QStringLiteral("请求超过了设定的最大重定向次数");
    break;
deafult:
    qDebug() << QStringLiteral("未知错误");
}
// "远程主机名未找到(无效主机名)"

这时,会产生一个 QNetworkReply::HostNotFoundError 错误。

注意: QNetworkReply::TooManyRedirectsError 是 5.6 中引进的,默认限制是 50,可以使用 QNetworkRequest::setMaxRedirectsAllowed() 来改变。

如果要获取到可读的错误信息,使用:

QString strError = pReplay->errorString();
qDebug() << strError;
// "Host www.csdnqter.net not found"

准备好了吗?赶快去试试吧!

作者:u011012932 发表于2016/9/14 17:03:58 原文链接
阅读:131 评论:1 查看评论

Android图片选择器,支持图片选择和拍照

$
0
0

Android图片选择器

本文是我的第二篇博客,如果有错误的地方,希望大家多多指点
本篇博客的主要内容是仿微信图片选择器,主要步骤有如下几点

  • 图片加载类(单例)
  • 获取手机本地图片
  • 可以设置图片选择个数

运行效果图 这里写图片描述

第一步:核心方法:图片加载工具类 ImageLoader

public class ImageLoader {

    private static ImageLoader mInstance;
    /**
     * 图片缓存的核心对象
     */
    private LruCache<String, Bitmap> mLruCache;
    /**
     * 线程池
     */
    private ExecutorService mThreadPool;
    private static final int DEFAULT_THREAD_COUNT = 1;
    /**
     * 队列的调度方式
     */
    private Type mType = Type.FIFO;
    /**
     * 任务队列
     */
    private LinkedList<Runnable> mTaskQueue;
    /**
     * 后台轮询线程
     */
    private Thread mPoolThread;
    private Handler mPoolThreadHandler;
    /**
     * UI线程的handler
     */
    private Handler mUIHandler;

    private Semaphore mSemaphorePoolThreadHandler = new Semaphore(0);
    private Semaphore mSemaphoreThreadPool;

    public  enum Type {
        FIFO, LIFO;
    }

    private ImageLoader(int threadCount, Type type) {
        init(threadCount, type);
    }

    /**
     * 初始化
     * 
     * @param threadCount
     * @param type
     */
    private void init(int threadCount, Type type) {
        mPoolThread = new Thread() {
            @Override
            public void run() {
                Looper.prepare();
                mPoolThreadHandler = new Handler() {
                    public void handleMessage(android.os.Message msg) {
                        // 线程池去取出一个任务进行执行
                        mThreadPool.execute(getTask());
                        try {
                            mSemaphoreThreadPool.acquire();
                        } catch (InterruptedException e) {
                            // TODO Auto-generated catch block
                            e.printStackTrace();
                        }
                    }

                };
                // 释放一个信号量
                mSemaphorePoolThreadHandler.release();
                Looper.loop();
            }
        };
        mPoolThread.start();
        // 获取我们应用的最大可用内存
        int maxMemory = (int) Runtime.getRuntime().maxMemory();
        int cacheMemory = maxMemory / 4;
        mLruCache = new LruCache<String, Bitmap>(cacheMemory) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                // TODO Auto-generated method stub
                return value.getRowBytes() * value.getHeight();
            }
        };
        mThreadPool = Executors.newFixedThreadPool(threadCount);
        mTaskQueue = new LinkedList<Runnable>();
        mType = type;
        mSemaphoreThreadPool = new Semaphore(threadCount);
    }

    /**
     * 从任务队列取出一个方法
     * 
     * @return
     */
    private Runnable getTask() {
        if (mType == Type.FIFO) {
            return mTaskQueue.removeFirst();
        } else if (mType == Type.LIFO) {
            return mTaskQueue.removeLast();
        }
        return null;
    };

    public static ImageLoader getInstance() {
        if (mInstance == null) {
            synchronized (ImageLoader.class) {
                if (mInstance == null) {
                    mInstance = new ImageLoader(DEFAULT_THREAD_COUNT, Type.LIFO);
                }
            }
        }
        return mInstance;
    }

    public static ImageLoader getInstance(int threadCount, Type type) {
        if (mInstance == null) {
            synchronized (ImageLoader.class) {
                if (mInstance == null) {
                    mInstance = new ImageLoader(threadCount, type);
                }
            }
        }
        return mInstance;
    }

    public void LoadImage(final String path, final ImageView imageView) {
        imageView.setTag(path);
        if (mUIHandler == null) {
            mUIHandler = new Handler() {
                public void handleMessage(android.os.Message msg) {
                    // 获取得到图片.为imageview回调设置图片
                    ImgBeanHolder holder = (ImgBeanHolder) msg.obj;
                    Bitmap bm = holder.bitmap;
                    if(bm!=null){
                        bm = Bitmap.createScaledBitmap(bm, 200, 200, true);
                    }
                    ImageView imageView = holder.imageView;
                    String path = holder.path;
                    if (imageView.getTag().toString().equals(path)) {
                        imageView.setImageBitmap(bm);
                    }
                };
            };
        }
        // 根据path在缓存中获取bitmap
        Bitmap bm = getBitmapFromLruCache(path);
        if (bm != null) {
            referashBitmap(path, imageView, bm);
        } else {
            addTasks(new Runnable() {

                @Override
                public void run() {
                    // 加载图片
                    // 图片的压缩
                    // 1获得图片需要显示的大小
                    ImageSize imageSize = getImageViewSize(imageView);
                    // 2.压缩图片
                    Bitmap bm = decodeSampledBitmapFromPath(path, imageSize.width, imageSize.height);
                    // 3.把图片加入到缓存
                    addBitmapToLruCache(path, bm);
                    referashBitmap(path, imageView, bm);
                    mSemaphoreThreadPool.release();
                }
            });
        }
    }

    private void referashBitmap(final String path, final ImageView imageView, Bitmap bm) {
        Message message = Message.obtain();
        ImgBeanHolder holder = new ImgBeanHolder();
        holder.bitmap = bm;
        holder.path = path;
        holder.imageView = imageView;
        message.obj = holder;
        mUIHandler.sendMessage(message);
    }

    /**
     * 将图片加入到缓存
     * 
     * @param path
     * @param bm
     */
    protected void addBitmapToLruCache(String path, Bitmap bm) {
        if (getBitmapFromLruCache(path) == null) {
            if (bm != null) {
                mLruCache.put(path, bm);
            }
        }

    }

    /**
     * 根据图片需要显示的宽高进行啊压缩
     * 
     * @param path
     * @param width
     * @param height
     * @return
     */
    protected Bitmap decodeSampledBitmapFromPath(String path, int width, int height) {
        // h获取图片的宽和高,并不把图片加载到内存
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(path);
        options.inSampleSize = caculateInSampleSize(options, width, height);
        // 使用获取到的InSampleSize再次解析图片
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeFile(path, options);
        return bitmap;
    }

    /**
     * 根据需求的宽和高及实际的宽和高计算SampleSize
     * 
     * @param options
     * @param width
     * @param height
     * @return
     */
    private int caculateInSampleSize(Options options, int reqWidth, int reqHeight) {
        int width = options.outWidth;
        int height = options.outWidth;
        int inSampleSize = 1;
        if (width > reqWidth || height > reqHeight) {
            int widthRadio = Math.round(width * 1.0f / reqWidth);
            int hegihtRadio = Math.round(height * 1.0f / reqHeight);

            inSampleSize = Math.max(widthRadio, hegihtRadio);

        }
        return inSampleSize;
    }

    /**
     * 根据ImageView返回图片的大小
     * 
     * @param imageView
     */
    @SuppressLint("NewApi")
    protected ImageSize getImageViewSize(ImageView imageView) {
        ImageSize imageSize = new ImageSize();
        DisplayMetrics displayMetrics = imageView.getContext().getResources().getDisplayMetrics();

        LayoutParams lp = imageView.getLayoutParams();
        int width = imageView.getWidth();// (lp.width ==
                                            // LayoutParams.WRAP_CONTENT ? 0 :
                                            // imageView.getWidth());
        if (width <= 0) {
            width = lp.width;// 获取imageview在layout中声明的宽度
        }
        if (width <= 0) {
            width = imageView.getMaxWidth();
        }
        if (width <= 0) {
            width = displayMetrics.widthPixels;
        }

        int height = imageView.getHeight();
        if (height <= 0) {
            height = lp.height;// 获取imageview在layout中声明的高度
        }
        if (height <= 0) {
            height = imageView.getMaxHeight();
        }
        if (height <= 0) {
            height = displayMetrics.heightPixels;
        }

        imageSize.width = width;
        imageSize.height = height;
        return imageSize;
    }

    private synchronized void addTasks(Runnable runnable) {
        mTaskQueue.add(runnable);
        // if(mPoolThreadHandler==null)
        try {
            if (mPoolThreadHandler == null) {
                mSemaphorePoolThreadHandler.acquire();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mPoolThreadHandler.sendEmptyMessage(0x110);
    }

    /**
     * 
     * @param path
     * @return
     */
    private Bitmap getBitmapFromLruCache(String path) {
        return null;
    }

    private class ImageSize {
        int width;
        int height;
    }

    private class ImgBeanHolder {
        Bitmap bitmap;
        ImageView imageView;
        String path;
    }
}

第二步:主界面设计

主界面布局

<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:background="@drawable/set_bg"
    tools:context="${relativePackage}.${activityClass}" >

    <RelativeLayout
        android:id="@+id/top"
        android:layout_width="match_parent"
        android:layout_height="50dip"
        android:background="#0079cd" >

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:src="@drawable/return_icon" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_centerInParent="true"
            android:gravity="center"
            android:text="图片"
            android:textColor="#FFFFFF"
            android:textSize="19sp" />

        <TextView
            android:id="@+id/select_image"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentRight="true"
            android:layout_centerInParent="true"
            android:layout_marginBottom="15dp"
            android:layout_marginLeft="15dp"
            android:layout_marginRight="10dp"
            android:background="@drawable/finish_btn"
            android:gravity="center"
            android:text="完成(0/3)"
            android:textColor="#FFFFFF"
            android:textSize="16sp" />

    </RelativeLayout>

    <GridView
        android:id="@+id/id_gridView"
        android:layout_marginTop="3dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/top"
        android:cacheColorHint="@android:color/transparent"
        android:horizontalSpacing="3dip"
        android:numColumns="3"
        android:stretchMode="columnWidth"
        android:verticalSpacing="3dp" >
    </GridView>

    <RelativeLayout
        android:id="@+id/id_bottom_ly"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_alignParentBottom="true"
        android:background="#88000000"
        android:clickable="true" >

        <TextView
            android:id="@+id/id_dir_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentLeft="true"
            android:layout_centerVertical="true"
            android:paddingLeft="10dp"
            android:text="所有图片"
            android:textColor="#e5e5e5"
            android:textSize="15sp" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginLeft="5dip"
            android:layout_toRightOf="@+id/id_dir_name"
            android:src="@drawable/more_select" />

        <ImageView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_marginRight="5dip"
            android:layout_toLeftOf="@+id/id_dir_num"
            android:src="@drawable/split_line" />

        <TextView
            android:id="@+id/id_dir_num"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentRight="true"
            android:layout_centerVertical="true"
            android:paddingRight="10dp"
            android:text="预览"
            android:textSize="15sp"
            android:textColor="@android:color/white" />
    </RelativeLayout>

</RelativeLayout>

定义列表

Markdown Extra 定义列表语法:
项目1
项目2
定义 A
定义 B
项目3
定义 C

定义 D

定义D内容

代码块

代码块语法遵循标准markdown代码,例如:

@requires_authorization
def somefunc(param1='', param2=0):
    '''A docstring'''
    if param1 > param2: # interesting
        print 'Greater'
    return (param2 - param1 + 1) or None
class SomeClass:
    pass
>>> message = '''interpreter
... prompt'''

脚注

生成一个脚注1.

目录

[TOC]来生成目录:

数学公式

使用MathJax渲染LaTex 数学公式,详见math.stackexchange.com.

  • 行内公式,数学公式为:Γ(n)=(n1)!nN
  • 块级公式:

x=b±b24ac2a

更多LaTex语法请参考 这儿.

UML 图:

可以渲染序列图:

Created with Raphaël 2.1.0张三张三李四李四嘿,小四儿, 写博客了没?李四愣了一下,说:忙得吐血,哪有时间写。

或者流程图:

Created with Raphaël 2.1.0开始我的操作确认?结束yesno
  • 关于 序列图 语法,参考 这儿,
  • 关于 流程图 语法,参考 这儿.

离线写博客

即使用户在没有网络的情况下,也可以通过本编辑器离线写博客(直接在曾经使用过的浏览器中输入write.blog.csdn.net/mdeditor即可。Markdown编辑器使用浏览器离线存储将内容保存在本地。

用户写博客的过程中,内容实时保存在浏览器缓存中,在用户关闭浏览器或者其它异常情况下,内容不会丢失。用户再次打开浏览器时,会显示上次用户正在编辑的没有发表的内容。

博客发表后,本地缓存将被删除。 

用户可以选择 把正在写的博客保存到服务器草稿箱,即使换浏览器或者清除缓存,内容也不会丢失。

注意:虽然浏览器存储大部分时候都比较可靠,但为了您的数据安全,在联网后,请务必及时发表或者保存到服务器草稿箱

浏览器兼容

  1. 目前,本编辑器对Chrome浏览器支持最为完整。建议大家使用较新版本的Chrome。
  2. IE9以下不支持
  3. IE9,10,11存在以下问题
    1. 不支持离线功能
    2. IE9不支持文件导入导出
    3. IE10不支持拖拽文件导入


  1. 这里是 脚注内容.
作者:waa_0618 发表于2016/9/14 17:15:48 原文链接
阅读:110 评论:0 查看评论

二维码的生成细节和原理

$
0
0

二维码又称QR Code,QR全称Quick Response,是一个近几年来移动设备上超流行的一种编码方式,它比传统的Bar Code条形码能存更多的信息,也能表示更多的数据类型:比如:字符,数字,日文,中文等等。这两天学习了一下二维码图片生成的相关细节,觉得这个玩意就是一个密码算法,在此写一这篇文章 ,揭露一下。供好学的人一同学习之。

关于QR Code Specification,可参看这个PDF:http://raidenii.net/files/datasheets/misc/qr_code.pdf 

基础知识

首先,我们先说一下二维码一共有40个尺寸。官方叫版本Version。Version 1是21 x 21的矩阵,Version 2是 25 x 25的矩阵,Version 3是29的尺寸,每增加一个version,就会增加4的尺寸,公式是:(V-1)*4 + 21(V是版本号) 最高Version 40,(40-1)*4+21 = 177,所以最高是177 x 177 的正方形。

下面我们看看一个二维码的样例:

定位图案
  • Position Detection Pattern是定位图案,用于标记二维码的矩形大小。这三个定位图案有白边叫Separators for Postion Detection Patterns。之所以三个而不是四个意思就是三个就可以标识一个矩形了。
  • Timing Patterns也是用于定位的。原因是二维码有40种尺寸,尺寸过大了后需要有根标准线,不然扫描的时候可能会扫歪了。
  • Alignment Patterns 只有Version 2以上(包括Version2)的二维码需要这个东东,同样是为了定位用的。
功能性数据
  • Format Information 存在于所有的尺寸中,用于存放一些格式化数据的。
  • Version Information 在 >= Version 7以上,需要预留两块3 x 6的区域存放一些版本信息。
数据码和纠错码
  • 除了上述的那些地方,剩下的地方存放 Data Code 数据码 和 Error Correction Code 纠错码。

数据编码

我们先来说说数据编码。QR码支持如下的编码:

Numeric mode 数字编码,从0到9。如果需要编码的数字的个数不是3的倍数,那么,最后剩下的1或2位数会被转成4或7bits,则其它的每3位数字会被编成 10,12,14bits,编成多长还要看二维码的尺寸(下面有一个表Table 3说明了这点)

Alphanumeric mode 字符编码。包括 0-9,大写的A到Z(没有小写),以及符号$ % * + – . / : 包括空格。这些字符会映射成一个字符索引表。如下所示:(其中的SP是空格,Char是字符,Value是其索引值) 编码的过程是把字符两两分组,然后转成下表的45进制,然后转成11bits的二进制,如果最后有一个落单的,那就转成6bits的二进制。而编码模式和字符的个数需要根据不同的Version尺寸编成9, 11或13个二进制(如下表中Table 3)

Byte mode, 字节编码,可以是0-255的ISO-8859-1字符。有些二维码的扫描器可以自动检测是否是UTF-8的编码。

Kanji mode 这是日文编码,也是双字节编码。同样,也可以用于中文编码。日文和汉字的编码会减去一个值。如:在0X8140 to 0X9FFC中的字符会减去8140,在0XE040到0XEBBF中的字符要减去0XC140,然后把结果前两个16进制位拿出来乘以0XC0,然后再加上后两个16进制位,最后转成13bit的编码。如下图示例:

Extended Channel Interpretation (ECI) mode 主要用于特殊的字符集。并不是所有的扫描器都支持这种编码。

Structured Append mode 用于混合编码,也就是说,这个二维码中包含了多种编码格式。

FNC1 mode 这种编码方式主要是给一些特殊的工业或行业用的。比如GS1条形码之类的。

简单起见,后面三种不会在本文 中讨论。

下面两张表中,

  • Table 2 是各个编码格式的“编号”,这个东西要写在Format Information中。注:中文是1101
  • Table 3 表示了,不同版本(尺寸)的二维码,对于,数字,字符,字节和Kanji模式下,对于单个编码的2进制的位数。(在二维码的规格说明书中,有各种各样的编码规范表,后面还会提到)

下面我们看几个示例,

示例一:数字编码

在Version 1的尺寸下,纠错级别为H的情况下,编码: 01234567

1. 把上述数字分成三组: 012 345 67

2. 把他们转成二进制:  012 转成 0000001100;  345 转成 0101011001;  67 转成 1000011。

3. 把这三个二进制串起来: 0000001100 0101011001 1000011

4. 把数字的个数转成二进制 (version 1-H是10 bits ): 8个数字的二进制是 0000001000

5. 把数字编码的标志0001和第4步的编码加到前面:  0001 0000001000 0000001100 0101011001 1000011

示例二:字符编码

在Version 1的尺寸下,纠错级别为H的情况下,编码: AC-42

1. 从字符索引表中找到 AC-42 这五个字条的索引 (10,12,41,4,2)

2. 两两分组: (10,12) (41,4) (2)

3.把每一组转成11bits的二进制:

(10,12) 10*45+12 等于 462 转成 00111001110
(41,4) 41*45+4 等于 1849 转成 11100111001
(2) 等于 2 转成 000010

4. 把这些二进制连接起来:00111001110 11100111001 000010

5. 把字符的个数转成二进制 (Version 1-H为9 bits ): 5个字符,5转成 000000101

6. 在头上加上编码标识 0010 和第5步的个数编码:  0010 000000101 00111001110 11100111001 000010

结束符和补齐符

假如我们有个HELLO WORLD的字符串要编码,根据上面的示例二,我们可以得到下面的编码,

编码 字符数 HELLO WORLD的编码
0010 000001011 01100001011 01111000110 10001011100 10110111000 10011010100 001101

我们还要加上结束符:

编码 字符数 HELLO WORLD的编码 结束
0010 000001011 01100001011 01111000110 10001011100 10110111000 10011010100 001101 0000
按8bits重排

如果所有的编码加起来不是8个倍数我们还要在后面加上足够的0,比如上面一共有78个bits,所以,我们还要加上2个0,然后按8个bits分好组:

00100000   01011011   00001011   01111000   11010001   01110010   11011100   01001101   01000011   01000000

补齐码(Padding Bytes)

最后,如果如果还没有达到我们最大的bits数的限制,我们还要加一些补齐码(Padding Bytes),Padding Bytes就是重复下面的两个bytes:11101100 00010001 (这两个二进制转成十进制是236和17,我也不知道为什么,只知道Spec上是这么写的)关于每一个Version的每一种纠错级别的最大Bits限制,可以参看QR Code Spec的第28页到32页的Table-7一表。

假设我们需要编码的是Version 1的Q纠错级,那么,其最大需要104个bits,而我们上面只有80个bits,所以,还需要补24个bits,也就是需要3个Padding Bytes,我们就添加三个,于是得到下面的编码:

00100000 01011011 00001011 01111000 11010001 01110010 11011100 01001101 01000011 01000000 11101100 00010001 11101100

上面的编码就是数据码了,叫Data Codewords,每一个8bits叫一个codeword,我们还要对这些数据码加上纠错信息。

纠错码

上面我们说到了一些纠错级别,Error Correction Code Level,二维码中有四种级别的纠错,这就是为什么二维码有残缺还能扫出来,也就是为什么有人在二维码的中心位置加入图标。

错误修正容量
L水平 7%的字码可被修正
M水平 15%的字码可被修正
Q水平 25%的字码可被修正
H水平 30%的字码可被修正

那么,QR是怎么对数据码加上纠错码的?首先,我们需要对数据码进行分组,也就是分成不同的Block,然后对各个Block进行纠错编码,对于如何分组,我们可以查看QR Code Spec的第33页到44页的Table-13到Table-22的定义表。注意最后两列:

  • Number of Error Code Correction Blocks :需要分多少个块。
  • Error Correction Code Per Blocks:每一个块中的code个数,所谓的code的个数,也就是有多少个8bits的字节。

举个例子:上述的Version 5 + Q纠错级:需要4个Blocks(2个Blocks为一组,共两组),头一组的两个Blocks中各15个bits数据 + 各 9个bits的纠错码(注:表中的codewords就是一个8bits的byte)(再注:最后一例中的(c, k, r )的公式为:c = k + 2 * r,因为后脚注解释了:纠错码的容量小于纠错码的一半)

下图给一个5-Q的示例(因为二进制写起来会让表格太大,所以,我都用了十进制,我们可以看到每一块的纠错码有18个codewords,也就是18个8bits的二进制数)

数据 对每个块的纠错码
1 1 67 85 70 134 87 38 85 194 119 50 6 18 6 103 38 213 199 11 45 115 247 241 223 229 248 154 117 154 111 86 161 111 39
2 246 246 66 7 118 134 242 7 38 86 22 198 199 146 6 87 204 96 60 202 182 124 157 200 134 27 129 209 17 163 163 120 133
2 1 182 230 247 119 50 7 118 134 87 38 82 6 134 151 50 7 148 116 177 212 76 133 75 242 238 76 195 230 189 10 108 240 192 141
2 70 247 118 86 194 6 151 50 16 236 17 236 17 236 17 236 235 159 5 173 24 147 59 33 106 40 255 172 82 2 131 32 178 236

注:二维码的纠错码主要是通过Reed-Solomon error correction(里德-所罗门纠错算法)来实现的。对于这个算法,对于我来说是相当的复杂,里面有很多的数学计算,比如:多项式除法,把1-255的数映射成2的n次方(0<=n<=255)的伽罗瓦域Galois Field之类的神一样的东西,以及基于这些基础的纠错数学公式,因为我的数据基础差,对于我来说太过复杂,所以我一时半会儿还有点没搞明白,还在学习中,所以,我在这里就不展开说这些东西了。还请大家见谅了。(当然,如果有朋友很明白,也繁请教教我)

最终编码

穿插放置

如果你以为我们可以开始画图,你就错了。二维码的混乱技术还没有玩完,它还要把数据码和纠错码的各个codewords交替放在一起。如何交替呢,规则如下:

对于数据码:把每个块的第一个codewords先拿出来按顺度排列好,然后再取第一块的第二个,如此类推。如:上述示例中的Data Codewords如下:

块 1 67 85 70 134 87 38 85 194 119 50 6 18 6 103 38  
块 2 246 246 66 7 118 134 242 7 38 86 22 198 199 146 6  
块 3 182 230 247 119 50 7 118 134 87 38 82 6 134 151 50 7
块 4 70 247 118 86 194 6 151 50 16 236 17 236 17 236 17 236

我们先取第一列的:67, 246, 182, 70

然后再取第二列的:67, 246, 182, 70, 85,246,230 ,247

如此类推:67, 246, 182, 70, 85,246,230 ,247 ………  ……… ,38,6,50,17,7,236

对于纠错码,也是一样:

块 1 213 199 11 45 115 247 241 223 229 248 154 117 154 111 86 161 111 39
块 2 87 204 96 60 202 182 124 157 200 134 27 129 209 17 163 163 120 133
块 3 148 116 177 212 76 133 75 242 238 76 195 230 189 10 108 240 192 141
块 4 235 159 5 173 24 147 59 33 106 40 255 172 82 2 131 32 178 236

和数据码取的一样,得到:213,87,148,235,199,204,116,159,…… …… 39,133,141,236

然后,再把这两组放在一起(纠错码放在数据码之后)得到:

67, 246, 182, 70, 85, 246, 230, 247, 70, 66, 247, 118, 134, 7, 119, 86, 87, 118, 50, 194, 38, 134, 7, 6, 85, 242, 118, 151, 194, 7, 134, 50, 119, 38, 87, 16, 50, 86, 38, 236, 6, 22, 82, 17, 18, 198, 6, 236, 6, 199, 134, 17, 103, 146, 151, 236, 38, 6, 50, 17, 7, 236, 213, 87, 148, 235, 199, 204, 116, 159, 11, 96, 177, 5, 45, 60, 212, 173, 115, 202, 76, 24, 247, 182, 133, 147, 241, 124, 75, 59, 223, 157, 242, 33, 229, 200, 238, 106, 248, 134, 76, 40, 154, 27, 195, 255, 117, 129, 230, 172, 154, 209, 189, 82, 111, 17, 10, 2, 86, 163, 108, 131, 161, 163, 240, 32, 111, 120, 192, 178, 39, 133, 141, 236

这就是我们的数据区。

Remainder Bits

最后再加上Reminder Bits,对于某些Version的QR,上面的还不够长度,还要加上Remainder Bits,比如:上述的5Q版的二维码,还要加上7个bits,Remainder Bits加零就好了。关于哪些Version需要多少个Remainder bit,可以参看QR Code Spec的第15页的Table-1的定义表。

画二维码图

Position Detection Pattern

首先,先把Position Detection图案画在三个角上。(无论Version如何,这个图案的尺寸就是这么大)

Alignment Pattern

然后,再把Alignment图案画上(无论Version如何,这个图案的尺寸就是这么大)

关于Alignment的位置,可以查看QR Code Spec的第81页的Table-E.1的定义表(下表是不完全表格)

下图是根据上述表格中的Version8的一个例子(6,24,42)

Timing Pattern

接下来是Timing Pattern的线(这个不用多说了)

Format Information

再接下来是Formation Information,下图中的蓝色部分。

Format Information是一个15个bits的信息,每一个bit的位置如下图所示:(注意图中的Dark Module,那是永远出现的)

这15个bits中包括:

  • 5个数据bits:其中,2个bits用于表示使用什么样的Error Correction Level, 3个bits表示使用什么样的Mask
  • 10个纠错bits。主要通过BCH Code来计算

然后15个bits还要与101010000010010做XOR操作。这样就保证不会因为我们选用了00的纠错级别和000的Mask,从而造成全部为白色,这会增加我们的扫描器的图像识别的困难。

下面是一个示例:

关于Error Correction Level如下表所示:

关于Mask图案如后面的Table 23所示。

Version Information

再接下来是Version Information(版本7以后需要这个编码),下图中的蓝色部分。

Version Information一共是18个bits,其中包括6个bits的版本号以及12个bits的纠错码,下面是一个示例:

而其填充位置如下:

数据和数据纠错码

然后是填接我们的最终编码,最终编码的填充方式如下:从左下角开始沿着红线填我们的各个bits,1是黑色,0是白色。如果遇到了上面的非数据区,则绕开或跳过。

掩码图案

这样下来,我们的图就填好了,但是,也许那些点并不均衡,如果出现大面积的空白或黑块,会告诉我们扫描识别的困难。所以,我们还要做Masking操作(靠,还嫌不复杂)QR的Spec中说了,QR有8个Mask你可以使用,如下所示:其中,各个mask的公式在各个图下面。所谓mask,说白了,就是和上面生成的图做XOR操作。Mask只会和数据区进行XOR,不会影响功能区。(注:选择一个合适的Mask也是有算法的

其Mask的标识码如下所示:(其中的i,j分别对应于上图的x,y)

下面是Mask后的一些样子,我们可以看到被某些Mask XOR了的数据变得比较零散了。

Mask过后的二维码就成最终的图了。

好了,大家可以去尝试去写一下QR的编码程序,当然,你可以用网上找个Reed Soloman的纠错算法的库,或是看看别人的源代码是怎么实现这个繁锁的编码。


作者:LOSER_LOSER1 发表于2016/9/14 17:38:49 原文链接
阅读:137 评论:0 查看评论

扫一扫

$
0
0

扫一扫这个功能我们并不陌生,微信QQ加好友扫一扫,路边搞推广的扫一扫,吃饭付钱扫一扫。看来这个扫一扫应用的地方还真不少啊!打开手机看了看安装的APP,社交类APP有扫一扫,支付类的APP有扫一扫,连浏览器都有扫一扫。看来这个扫一扫不可小视!说不定哪天在你的项目中你的boss就要你给他加个扫一扫的功能呢!对于iOS开发要想做扫一扫这个功能,并不是很难,我们需要做的就是处理二维码(QRCode)而已。在iOS7之前需要借助第三方库ZBar和ZXing。在iOS7以后用自带API就能轻松搞定。不用在项目中导入臃肿的第三方库和担心编译出问题了。

二维码是个什么鬼?

二维条码是指在一维条码的基础上扩展出另一维具有可读性的条码,使用黑白矩形图案表示二进制数据,被设备扫描后可获取其中所包含的信息。----维基百科

详细的生成细节和原理可以看看二维码的生成细节和原理这篇文章。

扫描二维码

首先我们来想一想具体的步骤,大概流程应该是:1.打开设备的摄像头-->2.进行二维码图像捕获-->3.获取捕获的图像进行解析-->4.取得解析结果进行后续处理。这些流程需要用到AVFoundation这个库,注意导入。

//获取一个AVCaptureDevice对象,可以理解为打开摄像头这样的动作
AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
//获取一个AVCaptureDeviceInput对象,将上面的'摄像头'作为输入设备
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:_device error:nil];
//拍完照片以后,需要一个AVCaptureMetadataOutput对象将获取的'图像'输出,以便进行对其解析
AVCaptureMetadataOutput *output = [[AVCaptureMetadataOutput alloc]init];
//获取输出需要设置代理,在代理方法中获取
[output setMetadataObjectsDelegate:self queue:dispatch_get_main_queue()];
//设置输出类型,如AVMetadataObjectTypeQRCode是二维码类型,下面还增加了条形码。如果扫描的是条形码也能识别
output.metadataObjectTypes = @[AVMetadataObjectTypeEAN13Code,
                                    AVMetadataObjectTypeEAN8Code,
                                    AVMetadataObjectTypeCode128Code,
                                    AVMetadataObjectTypeQRCode];

上面完成了捕获的设置,但是并未正在开始'扫描',要完成一次扫描的过程,需要用到AVCaptureSession这个类,这个session类把一次扫描看做一次会话,会话开始后才是正在的'扫描'开始,具体代码如下。

AVCaptureSession *session = [[AVCaptureSession alloc]init];
[session setSessionPreset:AVCaptureSessionPresetHigh];//扫描的质量
if ([session canAddInput:input]){
   [session addInput:input];//将输入添加到会话中
}  
if ([session canAddOutput:output]){
   [session addOutput:output];//将输出添加到会话中
}

接下来我们要做的不是立即开始会话(开始扫描),如果你现在调用会话的startRunning方法的话,你会发现屏幕是一片黑,这时由于我们还没有设置相机的取景器的大小。设置取景器需要用到AVCaptureVideoPreviewLayer这个类。具体代码如下:

AVCaptureVideoPreviewLayer *preview =[AVCaptureVideoPreviewLayer layerWithSession:session];
preview.videoGravity = AVLayerVideoGravityResize;
[preview setFrame:self.view.bounds];//设置取景器的frame
[self.view.layer insertSublayer:preview atIndex:0];

接下来我们就可以调用session的startRunning方法了,这时我们的扫描就真正开始了。想要获得扫描的结果,需要实现session的代理方法- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection,代码如下:

- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputMetadataObjects:(NSArray *)metadataObjects fromConnection:(AVCaptureConnection *)connection
{
    [self.session stopRunning];//停止会话
    [self.preview removeFromSuperlayer];//移除取景器

    if (metadataObjects.count > 0) {
        AVMetadataMachineReadableCodeObject *obj = metadataObjects[0];
        NSString *result = obj.stringValue;//这就是扫描的结果啦
        //对结果进行处理...
    }
}

如果要做到为用户考虑的话,还得加入照明的功能或者设置兴趣区域。

  • 设设置兴趣区域

使用过微信中的扫一扫的话,你应该发现在扫描界面中间有一个矩形限定框,这个框就是兴趣区域了。这个兴趣区域是AVCaptureMetadataOutputrectOfInterest属性。rectOfInterest的值的范围都是0-1,是按比例取值而不是实际尺寸,所以设置的时候要注意的一点。还要注意rectOfInterest都是按照横屏来计算的,所以当竖屏的情况下x轴和y轴要交换一下。代码如下:

CGSize size = self.view.bounds.size;
CGSize transparentAreaSize = CGSizeMake(200,200);
CGRect cropRect = CGRectMake((size.width - transparentAreaSize.width)/2, (size.height - transparentAreaSize.height)/2, transparentAreaSize.width, transparentAreaSize.height);
output.rectOfInterest = CGRectMake(cropRect.origin.y/size.width,
                                              cropRect.origin.x/size.height,
                                              cropRect.size.height/size.height,
                                              cropRect.size.width/size.width);
  • 加入照明功能

加入照明功能能让用户在光照条件不好的情况下顺利的进行进行扫描操作,代码如下:

AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
NSError *error;
if (device.hasTorch) {  // 判断设备是否有闪光灯
    BOOL b = [device lockForConfiguration:&error];
    if (!b) {
       if (error) {
          NSLog(@"lock torch configuration error:%@", error.localizedDescription);
       }
       return;
    }
    device.torchMode = (device.torchMode == AVCaptureTorchModeOff ? AVCaptureTorchModeOn : AVCaptureTorchModeOff);
    [device unlockForConfiguration];
    }

从图片中读取二维码

从图片中直接读取二维码的功能在iOS7上面苹果没有实现,不过在iOS上已经填补了这一功能。主要用到的是读取主要用到CoreImage。

废话不多说,直接上代码。

+ (NSString *)scQRReaderForImage:(UIImage *)qrimage{
    CIContext *context = [CIContext contextWithOptions:nil];
    CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
    CIImage *image = [CIImage imageWithCGImage:qrimage.CGImage];
    NSArray *features = [detector featuresInImage:image];
    CIQRCodeFeature *feature = [features firstObject];
    NSString *result = feature.messageString;
    return result;
}

既然讲到要从相册获取照片,那么顺便把从相册获取照片也讲一下吧。

从相册获取照片主要用到的是UIImagePickerController,这是苹果给我们分装好的一个相册选取的控制器。实现起来也是很简单的。

- (void)readerImage{
    UIImagePickerController *photoPicker = [[UIImagePickerController alloc] init];
    photoPicker.delegate = self;
    photoPicker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary;
    photoPicker.view.backgroundColor = [UIColor whiteColor];
    [self presentViewController:photoPicker animated:YES completion:NULL];
}

当我们从照片库选择取了照片后要带调用UIImagePickerController的代理方法获取选择的照片。

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info{
    [self dismissViewControllerAnimated:YES completion:^{
      //code is here ...  
    }];

    UIImage *srcImage = [info objectForKey:UIImagePickerControllerOriginalImage];
    NSString *result = [QRCScanner scQRReaderForImage:srcImage];//调用上面讲过的方法对图片中的二维码进行处理
    [self.navigationController popViewControllerAnimated:YES];
}

生成二维码

生成二维码和从图片中读取二维码一样要用到CoreImage,具体步骤如下:

- (UIImage *)makeQRCodeForString(NSString *)string{
    NSString *text = string;
    NSData *stringData = [text dataUsingEncoding: NSUTF8StringEncoding];
    //生成
    CIFilter *qrFilter = [CIFilter filterWithName:@"CIQRCodeGenerator"];
    [qrFilter setValue:stringData forKey:@"inputMessage"];
    [qrFilter setValue:@"M" forKey:@"inputCorrectionLevel"];
    //二维码颜色
    UIColor *onColor = [UIColor redColor];
    UIColor *offColor = [UIColor blueColor];
    //上色,如果只要白底黑块的QRCode可以跳过这一步
    CIFilter *colorFilter = [CIFilter filterWithName:@"CIFalseColor"
                                          keysAndValues:
                             @"inputImage",qrFilter.outputImage,
                             @"inputColor0",[CIColor colorWithCGColor:onColor.CGColor],
                             @"inputColor1",[CIColor colorWithCGColor:offColor.CGColor],
                             nil];
    //绘制
    CIImage *qrImage = colorFilter.outputImage;
    CGSize size = CGSizeMake(300, 300);
    CGImageRef cgImage = [[CIContext contextWithOptions:nil] createCGImage:qrImage     fromRect:qrImage.extent];
    UIGraphicsBeginImageContext(size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetInterpolationQuality(context, kCGInterpolationNone);
    CGContextScaleCTM(context, 1.0, -1.0);//生成的QRCode就是上下颠倒的,需要翻转一下
    CGContextDrawImage(context, CGContextGetClipBoundingBox(context), cgImage);
    UIImage *codeImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    CGImageRelease(cgImage);

    return [UIImage imageWithCIImage:qrImage];
}

当然如果你不想写这个代码的话,你也可以使用一个轻量级的开源代码libqrencode来帮你实现。它的使用非常简单,导入UIImage+MDQRCode这个扩展后,使用UIImage的类方法就可以调用了。

+ (UIImage *)mdQRCodeForString:(NSString *)qrString size:(CGFloat)size;
+ (UIImage *)mdQRCodeForString:(NSString *)qrString size:(CGFloat)size fillColor:(UIColor *)fillColor;


作者:LOSER_LOSER1 发表于2016/9/14 17:40:21 原文链接
阅读:148 评论:0 查看评论

Android开发 date工具类

$
0
0
/**
 * 日期时间工具类
 */
package com.zyl.vincent.utils;

import java.io.Serializable;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

/**
 * 
 * 日期操作工具类,主要实现了日期的常用操作。
 * <p>
 * 在工具类中经常使用到工具类的格式化描述,这个主要是一个日期的操作类,所以日志格式主要使用 SimpleDateFormat的定义格式.
 * <p>
 * 格式的意义如下: 日期和时间模式 <br>
 * 日期和时间格式由日期和时间模式字符串指定。在日期和时间模式字符串中,未加引号的字母 'A' 到 'Z' 和 'a' 到 'z'
 * 被解释为模式字母,用来表示日期或时间字符串元素。文本可以使用单引号 (') 引起来,以免进行解释。"''"
 * 表示单引号。所有其他字符均不解释;只是在格式化时将它们简单复制到输出字符串,或者在分析时与输入字符串进行匹配。
 * <p>
 * 定义了以下模式字母(所有其他字符 'A' 到 'Z' 和 'a' 到 'z' 都被保留): <br>
 * <table>
 * <tr>
 * <td>字母</td>
 * <td>日期或时间元素</td>
 * <td>表示</td>
 * <td>示例</td>
 * <td></tr>
 * <tr>
 * <td>G</td>
 * <td>Era</td>
 * <td>标志符</td>
 * <td>Text</td>
 * <td>AD</td>
 * <td></tr>
 * <tr>
 * <td>y</td>
 * <td>年</td>
 * <td>Year</td>
 * <td>1996;</td>
 * <td>96</td>
 * <td></tr>
 * <tr>
 * <td>M</td>
 * <td>年中的月份</td>
 * <td>Month</td>
 * <td>July;</td>
 * <td>Jul;</td>
 * <td>07</tr>
 * <tr>
 * <td>w</td>
 * <td>年中的周数</td>
 * <td>Number</td>
 * <td>27</td>
 * <td></tr>
 * <tr>
 * <td>W</td>
 * <td>月份中的周数</td>
 * <td>Number</td>
 * <td>2</td>
 * <td></tr>
 * <tr>
 * <td>D</td>
 * <td>年中的天数</td>
 * <td>Number</td>
 * <td>189</td>
 * <td></tr>
 * <tr>
 * <td>d</td>
 * <td>月份中的天数</td>
 * <td>Number</td>
 * <td>10</td>
 * <td></tr>
 * <tr>
 * <td>F</td>
 * <td>月份中的星期</td>
 * <td>Number</td>
 * <td>2</td>
 * <td></tr>
 * <tr>
 * <td>E</td>
 * <td>星期中的天数</td>
 * <td>Text</td>
 * <td>Tuesday;</td>
 * <td>Tue</tr>
 * <tr>
 * <td>a</td>
 * <td>Am/pm</td>
 * <td>标记</td>
 * <td>Text</td>
 * <td>PM</td>
 * <td></tr>
 * <tr>
 * <td>H</td>
 * <td>一天中的小时数(0-23)</td>
 * <td>Number</td>
 * <td>0</tr>
 * <tr>
 * <td>k</td>
 * <td>一天中的小时数(1-24)</td>
 * <td>Number</td>
 * <td>24</td>
 * <td></tr>
 * <tr>
 * <td>K</td>
 * <td>am/pm</td>
 * <td>中的小时数(0-11)</td>
 * <td>Number</td>
 * <td>0</td>
 * <td></tr>
 * <tr>
 * <td>h</td>
 * <td>am/pm</td>
 * <td>中的小时数(1-12)</td>
 * <td>Number</td>
 * <td>12</td>
 * <td></tr>
 * <tr>
 * <td>m</td>
 * <td>小时中的分钟数</td>
 * <td>Number</td>
 * <td>30</td>
 * <td></tr>
 * <tr>
 * <td>s</td>
 * <td>分钟中的秒数</td>
 * <td>Number</td>
 * <td>55</td>
 * <td></tr>
 * <tr>
 * <td>S</td>
 * <td>毫秒数</td>
 * <td>Number</td>
 * <td>978</td>
 * <td></tr>
 * <tr>
 * <td>z</td>
 * <td>时区</td>
 * <td>General</td>
 * <td>time</td>
 * <td>zone</td>
 * <td>Pacific</td>
 * <td>Standard</td>
 * <td>Time;</td>
 * <td>PST;</td>
 * <td>GMT-08:00</tr>
 * <tr>
 * <td>Z</td>
 * <td>时区</td>
 * <td>RFC</td>
 * <td>822</td>
 * <td>time</td>
 * <td>zone</td>
 * <td>-0800</td>
 * <td></tr>
 * </table>
 * 
 * 模式字母通常是重复的,其数量确定其精确表示:
 * 
 */
public class DateUtil implements Serializable {
    /**
     * 
     */
    private static final long serialVersionUID = -3098985139095632110L;

    private DateUtil() {
    }

    /**
     * 格式化日期显示格式yyyy-MM-dd
     * 
     * @param sdate
     *            原始日期格式
     * @return yyyy-MM-dd格式化后的日期显示
     */
    public static String dateFormat(String sdate) {
        return dateFormat(sdate, "yyyy-MM-dd");
    }

    /**
     * 格式化日期显示格式
     * 
     * @param sdate
     *            原始日期格式
     * @param format
     *            格式化后日期格式
     * @return 格式化后的日期显示
     */
    public static String dateFormat(String sdate, String format) {
        SimpleDateFormat formatter = new SimpleDateFormat(format);
        java.sql.Date date = java.sql.Date.valueOf(sdate);
        String dateString = formatter.format(date);

        return dateString;
    }

    /**
     * 求两个日期相差天数
     * 
     * @param sd
     *            起始日期,格式yyyy-MM-dd
     * @param ed
     *            终止日期,格式yyyy-MM-dd
     * @return 两个日期相差天数
     */
    public static long getIntervalDays(String sd, String ed) {
        return ((java.sql.Date.valueOf(ed)).getTime() - (java.sql.Date
                .valueOf(sd)).getTime())
                / (3600 * 24 * 1000);
    }

    /**
     * 起始年月yyyy-MM与终止月yyyy-MM之间跨度的月数
     * 
     * @return int
     */
    public static int getInterval(String beginMonth, String endMonth) {
        int intBeginYear = Integer.parseInt(beginMonth.substring(0, 4));
        int intBeginMonth = Integer.parseInt(beginMonth.substring(beginMonth
                .indexOf("-") + 1));
        int intEndYear = Integer.parseInt(endMonth.substring(0, 4));
        int intEndMonth = Integer.parseInt(endMonth.substring(endMonth
                .indexOf("-") + 1));

        return ((intEndYear - intBeginYear) * 12)
                + (intEndMonth - intBeginMonth) + 1;
    }

    public static Date getDate(String sDate, String dateFormat) {
        SimpleDateFormat fmt = new SimpleDateFormat(dateFormat);
        ParsePosition pos = new ParsePosition(0);

        return fmt.parse(sDate, pos);
    }

    /**
     * 取得当前日期的年份,以yyyy格式返回.
     * 
     * @return 当年 yyyy
     */
    public static String getCurrentYear() {
        return getFormatCurrentTime("yyyy");
    }

    /**
     * 自动返回上一年。例如当前年份是2007年,那么就自动返回2006
     * 
     * @return 返回结果的格式为 yyyy
     */
    public static String getBeforeYear() {
        String currentYear = getFormatCurrentTime("yyyy");
        int beforeYear = Integer.parseInt(currentYear) - 1;
        return "" + beforeYear;
    }

    /**
     * 取得当前日期的月份,以MM格式返回.
     * 
     * @return 当前月份 MM
     */
    public static String getCurrentMonth() {
        return getFormatCurrentTime("MM");
    }

    /**
     * 取得当前日期的天数,以格式"dd"返回.
     * 
     * @return 当前月中的某天dd
     */
    public static String getCurrentDay() {
        return getFormatCurrentTime("dd");
    }

    /**
     * 返回当前时间字符串。
     * <p>
     * 格式:yyyy-MM-dd
     * 
     * @return String 指定格式的日期字符串.
     */
    public static String getCurrentDate() {
        return getFormatDateTime(new Date(), "yyyy-MM-dd");
    }

    /**
     * 返回给定时间字符串。
     * <p>
     * 格式:yyyy-MM-dd
     * 
     * @param date
     *            日期
     * @return String 指定格式的日期字符串.
     */
    public static String getFormatDate(Date date) {
        return getFormatDateTime(date, "yyyy-MM-dd");
    }

    /**
     * 根据制定的格式,返回日期字符串。例如2007-05-05
     * 
     * @param format
     *            "yyyy-MM-dd" 或者 "yyyy/MM/dd"
     * @return 指定格式的日期字符串。
     */
    public static String getFormatDate(String format) {
        return getFormatDateTime(new Date(), format);
    }

    /**
     * 返回当前时间字符串。
     * <p>
     * 格式:yyyy-MM-dd HH:mm:ss
     * 
     * @return String 指定格式的日期字符串.
     */
    public static String getCurrentTime() {
        return getFormatDateTime(new Date(), "yyyy-MM-dd HH:mm:ss");
    }

    /**
     * 返回给定时间字符串。
     * <p>
     * 格式:yyyy-MM-dd HH:mm:ss
     * 
     * @param date
     *            日期
     * @return String 指定格式的日期字符串.
     */
    public static String getFormatTime(Date date) {
        return getFormatDateTime(date, "yyyy-MM-dd HH:mm:ss");
    }

    /**
     * 根据给定的格式,返回时间字符串。
     * <p>
     * 格式参照类描绘中说明.
     * 
     * @param format
     *            日期格式字符串
     * @return String 指定格式的日期字符串.
     */
    public static String getFormatCurrentTime(String format) {
        return getFormatDateTime(new Date(), format);
    }

    /**
     * 根据给定的格式与时间(Date类型的),返回时间字符串<br>
     * 
     * @param date
     *            指定的日期
     * @param format
     *            日期格式字符串
     * @return String 指定格式的日期字符串.
     */
    public static String getFormatDateTime(Date date, String format) {
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        return sdf.format(date);
    }

    /**
     * 取得指定年月日的日期对象.
     * 
     * @param year
     *            年
     * @param month
     *            月注意是从1到12
     * @param day
     *            日
     * @return 一个java.util.Date()类型的对象
     */
    public static Date getDateObj(int year, int month, int day) {
        Calendar c = new GregorianCalendar();
        c.set(year, month - 1, day);
        return c.getTime();
    }

    /**
     * 取得指定分隔符分割的年月日的日期对象.
     * 
     * @param args
     *            格式为"yyyy-MM-dd"
     * @param split
     *            时间格式的间隔符,例如“-”,“/”
     * @return 一个java.util.Date()类型的对象
     */
    public static Date getDateObj(String args, String split) {
        String[] temp = args.split(split);
        int year = new Integer(temp[0]).intValue();
        int month = new Integer(temp[1]).intValue();
        int day = new Integer(temp[2]).intValue();
        return getDateObj(year, month, day);
    }

    /**
     * 取得给定字符串描述的日期对象,描述模式采用pattern指定的格式.
     * 
     * @param dateStr
     *            日期描述
     * @param pattern
     *            日期模式
     * @return 给定字符串描述的日期对象。
     */
    public static Date getDateFromString(String dateStr, String pattern) {
        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
        Date resDate = null;
        try {
            resDate = sdf.parse(dateStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return resDate;
    }

    /**
     * 取得当前Date对象.
     * 
     * @return Date 当前Date对象.
     */
    public static Date getDateObj() {
        Calendar c = new GregorianCalendar();
        return c.getTime();
    }

    /**
     * 
     * @return 当前月份有多少天;
     */
    public static int getDaysOfCurMonth() {
        int curyear = new Integer(getCurrentYear()).intValue(); // 当前年份
        int curMonth = new Integer(getCurrentMonth()).intValue();// 当前月份
        int mArray[] = new int[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30,
                31 };
        // 判断闰年的情况 ,2月份有29天;
        if ((curyear % 400 == 0)
                || ((curyear % 100 != 0) && (curyear % 4 == 0))) {
            mArray[1] = 29;
        }
        return mArray[curMonth - 1];
        // 如果要返回下个月的天数,注意处理月份12的情况,防止数组越界;
        // 如果要返回上个月的天数,注意处理月份1的情况,防止数组越界;
    }

    /**
     * 根据指定的年月 返回指定月份(yyyy-MM)有多少天。
     * 
     * @param time yyyy-MM
     * @return 天数,指定月份的天数。
     */
    public static int getDaysOfCurMonth(final String time) {
        String[] timeArray = time.split("-");
        int curyear = new Integer(timeArray[0]).intValue(); // 当前年份
        int curMonth = new Integer(timeArray[1]).intValue();// 当前月份
        int mArray[] = new int[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30,
                31 };
        // 判断闰年的情况 ,2月份有29天;
        if ((curyear % 400 == 0)
                || ((curyear % 100 != 0) && (curyear % 4 == 0))) {
            mArray[1] = 29;
        }
        if (curMonth == 12) {
            return mArray[0];
        }
        return mArray[curMonth - 1];
        // 如果要返回下个月的天数,注意处理月份12的情况,防止数组越界;
        // 如果要返回上个月的天数,注意处理月份1的情况,防止数组越界;
    }

    /**
     * 返回指定为年度为year月度为month的月份内,第weekOfMonth个星期的第dayOfWeek天。<br>
     * 00 00 00 01 02 03 04 <br>
     * 05 06 07 08 09 10 11<br>
     * 12 13 14 15 16 17 18<br>
     * 19 20 21 22 23 24 25<br>
     * 26 27 28 29 30 31 <br>
     * 2006年的第一个周的1到7天为:05 06 07 01 02 03 04 <br>
     * 2006年的第二个周的1到7天为:12 13 14 08 09 10 11 <br>
     * 2006年的第三个周的1到7天为:19 20 21 15 16 17 18 <br>
     * 2006年的第四个周的1到7天为:26 27 28 22 23 24 25 <br>
     * 2006年的第五个周的1到7天为:02 03 04 29 30 31 01 。本月没有就自动转到下个月了。
     * 
     * @param year
     *            形式为yyyy <br>
     * @param month
     *            形式为MM,参数值在[1-12]。<br>
     * @param weekOfMonth
     *            在[1-6],因为一个月最多有6个周。<br>
     * @param dayOfWeek
     *            数字在1到7之间,包括1和7。1表示星期天,7表示星期六<br>
     *            -6为星期日-1为星期五,0为星期六 <br>
     * @return <type>int</type>
     */
    public static int getDayofWeekInMonth(String year, String month,
            String weekOfMonth, String dayOfWeek) {
        Calendar cal = new GregorianCalendar();
        // 在具有默认语言环境的默认时区内使用当前时间构造一个默认的 GregorianCalendar。
        int y = new Integer(year).intValue();
        int m = new Integer(month).intValue();
        cal.clear();// 不保留以前的设置
        cal.set(y, m - 1, 1);// 将日期设置为本月的第一天。
        cal.set(Calendar.DAY_OF_WEEK_IN_MONTH, new Integer(weekOfMonth)
                .intValue());
        cal.set(Calendar.DAY_OF_WEEK, new Integer(dayOfWeek).intValue());
        // System.out.print(cal.get(Calendar.MONTH)+" ");
        // System.out.print("当"+cal.get(Calendar.WEEK_OF_MONTH)+"\t");
        // WEEK_OF_MONTH表示当天在本月的第几个周。不管1号是星期几,都表示在本月的第一个周
        return cal.get(Calendar.DAY_OF_MONTH);
    }

    /**
     * 根据指定的年月日小时分秒,返回一个java.Util.Date对象。
     * 
     * @param year 年
     * @param month 月 0-11
     * @param date 日
     * @param hourOfDay 小时 0-23
     * @param minute 分 0-59
     * @param second 秒 0-59
     * @return 一个Date对象。
     */
    public static Date getDate(int year, int month, int date, int hourOfDay,
            int minute, int second) {
        Calendar cal = new GregorianCalendar();
        cal.set(year, month, date, hourOfDay, minute, second);
        return cal.getTime();
    }

    /**
     * 根据指定的年、月、日返回当前是星期几。1表示星期天、2表示星期一、7表示星期六。
     * 
     * @param year
     * @param month
     *            month是从1开始的12结束
     * @param day
     * @return 返回一个代表当期日期是星期几的数字。1表示星期天、2表示星期一、7表示星期六。
     */
    public static int getDayOfWeek(String year, String month, String day) {
        Calendar cal = new GregorianCalendar(new Integer(year).intValue(),
                new Integer(month).intValue() - 1, new Integer(day).intValue());
        return cal.get(Calendar.DAY_OF_WEEK);
    }

    /**
     * 根据指定的年、月、日返回当前是星期几。1表示星期天、2表示星期一、7表示星期六。
     * 
     * @param date
     *            "yyyy/MM/dd",或者"yyyy-MM-dd"
     * @return 返回一个代表当期日期是星期几的数字。1表示星期天、2表示星期一、7表示星期六。
     */
    public static int getDayOfWeek(String date) {
        String[] temp = null;
        if (date.indexOf("/") > 0) {
            temp = date.split("/");
        }
        if (date.indexOf("-") > 0) {
            temp = date.split("-");
        }
        return getDayOfWeek(temp[0], temp[1], temp[2]);
    }

    /**
     *  返回当前日期是星期几。例如:星期日、星期一、星期六等等。
     * @param date 格式为 yyyy/MM/dd 或者 yyyy-MM-dd
     * @return 返回当前日期是星期几
     */
    public static String getChinaDayOfWeek(String date){
        String[] weeks = new String[]{"星期日","星期一","星期二","星期三","星期四","星期五","星期六"};
        int week = getDayOfWeek(date);
        return weeks[week-1];
    }
    /**
     * 根据指定的年、月、日返回当前是星期几。1表示星期天、2表示星期一、7表示星期六。
     * 
     * @param date
     *           
     * @return 返回一个代表当期日期是星期几的数字。1表示星期天、2表示星期一、7表示星期六。
     */
    public static int getDayOfWeek(Date date) {
        Calendar cal = new GregorianCalendar();
        cal.setTime(date);
        return cal.get(Calendar.DAY_OF_WEEK);
    }

    /**
     * 返回制定日期所在的周是一年中的第几个周。<br>
     * created by wangmj at 20060324.<br>
     * 
     * @param year
     * @param month
     *            范围1-12<br>
     * @param day
     * @return int 
     */
    public static int getWeekOfYear(String year, String month, String day) {
        Calendar cal = new GregorianCalendar();
        cal.clear();
        cal.set(new Integer(year).intValue(),
                new Integer(month).intValue() - 1, new Integer(day).intValue());
        return cal.get(Calendar.WEEK_OF_YEAR);
    }

    /**
     * 取得给定日期加上一定天数后的日期对象.
     * 
     * @param date
     *            给定的日期对象
     * @param amount
     *            需要添加的天数,如果是向前的天数,使用负数就可以.
     * @return Date 加上一定天数以后的Date对象.
     */
    public static Date getDateAdd(Date date, int amount) {
        Calendar cal = new GregorianCalendar();
        cal.setTime(date);
        cal.add(GregorianCalendar.DATE, amount);
        return cal.getTime();
    }

    /**
     * 取得给定日期加上一定天数后的日期对象.
     * 
     * @param date
     *            给定的日期对象
     * @param amount
     *            需要添加的天数,如果是向前的天数,使用负数就可以.
     * @param format
     *            输出格式.
     * @return Date 加上一定天数以后的Date对象.
     */
    public static String getFormatDateAdd(Date date, int amount, String format) {
        Calendar cal = new GregorianCalendar();
        cal.setTime(date);
        cal.add(GregorianCalendar.DATE, amount);
        return getFormatDateTime(cal.getTime(), format);
    }

    /**
     * 获得当前日期固定间隔天数的日期,如前60天dateAdd(-60)
     * 
     * @param amount
     *            距今天的间隔日期长度,向前为负,向后为正
     * @param format
     *            输出日期的格式.
     * @return java.lang.String 按照格式输出的间隔的日期字符串.
     */
    public static String getFormatCurrentAdd(int amount, String format) {

        Date d = getDateAdd(new Date(), amount);

        return getFormatDateTime(d, format);
    }

    /**
     * 取得给定格式的昨天的日期输出
     * 
     * @param format
     *            日期输出的格式
     * @return String 给定格式的日期字符串.
     */
    public static String getFormatYestoday(String format) {
        return getFormatCurrentAdd(-1, format);
    }

    /**
     * 返回指定日期的前一天。<br>
     * @param sourceDate 
     * @param format  yyyy MM  dd  hh mm  ss
     * @return  返回日期字符串,形式和formcat一致。
     */
    public static String getYestoday(String sourceDate, String format) {
        return getFormatDateAdd(getDateFromString(sourceDate, format), -1,
                format);
    }

    /**
     * 返回明天的日期,<br>
     * @param format
     * @return 返回日期字符串,形式和formcat一致。
     */
    public static String getFormatTomorrow(String format) {
        return getFormatCurrentAdd(1, format);
    }

    /**
     * 返回指定日期的后一天。<br>
     * @param sourceDate
     * @param format
     * @return 返回日期字符串,形式和formcat一致。
     */
    public static String getFormatDateTommorrow(String sourceDate, String format) {
        return getFormatDateAdd(getDateFromString(sourceDate, format), 1,
                format);
    }

    /**
     * 根据主机的默认 TimeZone,来获得指定形式的时间字符串。
     * @param dateFormat
     * @return  返回日期字符串,形式和formcat一致。
     */
    public static String getCurrentDateString(String dateFormat) {
        Calendar cal = Calendar.getInstance(TimeZone.getDefault());
        SimpleDateFormat sdf = new SimpleDateFormat(dateFormat);
        sdf.setTimeZone(TimeZone.getDefault());

        return sdf.format(cal.getTime());
    }

    /**
     * @deprecated 不鼓励使用。
     * 返回当前时间串 格式:yyMMddhhmmss,在上传附件时使用
     * 
     * @return String
     */
    public static String getCurDate() {
        GregorianCalendar gcDate = new GregorianCalendar();
        int year = gcDate.get(GregorianCalendar.YEAR);
        int month = gcDate.get(GregorianCalendar.MONTH) + 1;
        int day = gcDate.get(GregorianCalendar.DAY_OF_MONTH);
        int hour = gcDate.get(GregorianCalendar.HOUR_OF_DAY);
        int minute = gcDate.get(GregorianCalendar.MINUTE);
        int sen = gcDate.get(GregorianCalendar.SECOND);
        String y;
        String m;
        String d;
        String h;
        String n;
        String s;
        y = new Integer(year).toString();

        if (month < 10) {
            m = "0" + new Integer(month).toString();
        } else {
            m = new Integer(month).toString();
        }

        if (day < 10) {
            d = "0" + new Integer(day).toString();
        } else {
            d = new Integer(day).toString();
        }

        if (hour < 10) {
            h = "0" + new Integer(hour).toString();
        } else {
            h = new Integer(hour).toString();
        }

        if (minute < 10) {
            n = "0" + new Integer(minute).toString();
        } else {
            n = new Integer(minute).toString();
        }

        if (sen < 10) {
            s = "0" + new Integer(sen).toString();
        } else {
            s = new Integer(sen).toString();
        }

        return "" + y + m + d + h + n + s;
    }

    /**
     * 根据给定的格式,返回时间字符串。 和getFormatDate(String format)相似。
     * 
     * @param format  yyyy  MM dd  hh mm ss
     * @return 返回一个时间字符串
     */
    public static String getCurTimeByFormat(String format) {
        Date newdate = new Date(System.currentTimeMillis());
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        return sdf.format(newdate);
    }

    /**
     * 获取两个时间串时间的差值,单位为秒
     * 
     * @param startTime
     *            开始时间 yyyy-MM-dd HH:mm:ss
     * @param endTime
     *            结束时间 yyyy-MM-dd HH:mm:ss
     * @return 两个时间的差值(秒)
     */
    public static long getDiff(String startTime, String endTime) {
        long diff = 0;
        SimpleDateFormat ft = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        try {
            Date startDate = ft.parse(startTime);
            Date endDate = ft.parse(endTime);
            diff = startDate.getTime() - endDate.getTime();
            diff = diff / 1000;
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return diff;
    }

    /**
     * 获取小时/分钟/秒
     * 
     * @param second
     *            秒
     * @return 包含小时、分钟、秒的时间字符串,例如3小时23分钟13秒。
     */
    public static String getHour(long second) {
        long hour = second / 60 / 60;
        long minute = (second - hour * 60 * 60) / 60;
        long sec = (second - hour * 60 * 60) - minute * 60;

        return hour + "小时" + minute + "分钟" + sec + "秒";

    }

    /**
     * 返回指定时间字符串。
     * <p>
     * 格式:yyyy-MM-dd HH:mm:ss
     * 
     * @return String 指定格式的日期字符串.
     */
    public static String getDateTime(long microsecond) {
        return getFormatDateTime(new Date(microsecond), "yyyy-MM-dd HH:mm:ss");
    }

    /**
     * 返回当前时间加实数小时后的日期时间。
     * <p>
     * 格式:yyyy-MM-dd HH:mm:ss
     * 
     * @return Float 加几实数小时.
     */
    public static String getDateByAddFltHour(float flt) {
        int addMinute = (int) (flt * 60);
        Calendar cal = new GregorianCalendar();
        cal.setTime(new Date());
        cal.add(GregorianCalendar.MINUTE, addMinute);
        return getFormatDateTime(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
    }

    /**
     * 返回指定时间加指定小时数后的日期时间。
     * <p>
     * 格式:yyyy-MM-dd HH:mm:ss
     * 
     * @return 时间.
     */
    public static String getDateByAddHour(String datetime, int minute) {
        String returnTime = null;
        Calendar cal = new GregorianCalendar();
        SimpleDateFormat ft = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date;
        try {
            date = ft.parse(datetime);
            cal.setTime(date);
            cal.add(GregorianCalendar.MINUTE, minute);
            returnTime = getFormatDateTime(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return returnTime;

    }

    /**
     * 获取两个时间串时间的差值,单位为小时
     * 
     * @param startTime
     *            开始时间 yyyy-MM-dd HH:mm:ss
     * @param endTime
     *            结束时间 yyyy-MM-dd HH:mm:ss
     * @return 两个时间的差值(秒)
     */
    public static int getDiffHour(String startTime, String endTime) {
        long diff = 0;
        SimpleDateFormat ft = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        try {
            Date startDate = ft.parse(startTime);
            Date endDate = ft.parse(endTime);
            diff = startDate.getTime() - endDate.getTime();
            diff = diff / (1000 * 60 * 60);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return new Long(diff).intValue();
    }

    /**
     * 返回年份的下拉框。
     * @param selectName
     *            下拉框名称
     * @param value
     *            当前下拉框的值
     * @param startYear
     *            开始年份
     * @param endYear
     *            结束年份
     * @return 年份下拉框的html
     */
    public static String getYearSelect(String selectName, String value,
            int startYear, int endYear) {
        int start = startYear;
        int end = endYear;
        if (startYear > endYear) {
            start = endYear;
            end = startYear;
        }
        StringBuffer sb = new StringBuffer("");
        sb.append("<select name=\"" + selectName + "\">");
        for (int i = start; i <= end; i++) {
            if (!value.trim().equals("") && i == Integer.parseInt(value)) {
                sb.append("<option value=\"" + i + "\" selected>" + i
                        + "</option>");
            } else {
                sb.append("<option value=\"" + i + "\">" + i + "</option>");
            }
        }
        sb.append("</select>");
        return sb.toString();
    }

    /**
     * 返回年份的下拉框。
     * @param selectName
     *            下拉框名称
     * @param value
     *            当前下拉框的值
     * @param startYear
     *            开始年份
     * @param endYear
     *            结束年份
     *            例如开始年份为2001结束年份为2005那么下拉框就有五个值。(2001、2002、2003、2004、2005)。
     * @return 返回年份的下拉框的html。
     */
    public static String getYearSelect(String selectName, String value,
            int startYear, int endYear, boolean hasBlank) {
        int start = startYear;
        int end = endYear;
        if (startYear > endYear) {
            start = endYear;
            end = startYear;
        }
        StringBuffer sb = new StringBuffer("");
        sb.append("<select name=\"" + selectName + "\">");
        if (hasBlank) {
            sb.append("<option value=\"\"></option>");
        }
        for (int i = start; i <= end; i++) {
            if (!value.trim().equals("") && i == Integer.parseInt(value)) {
                sb.append("<option value=\"" + i + "\" selected>" + i
                        + "</option>");
            } else {
                sb.append("<option value=\"" + i + "\">" + i + "</option>");
            }
        }
        sb.append("</select>");
        return sb.toString();
    }

    /**
     * 返回年份的下拉框。
     * @param selectName
     *            下拉框名称
     * @param value
     *            当前下拉框的值
     * @param startYear
     *            开始年份
     * @param endYear
     *            结束年份
     * @param js
     *            这里的js为js字符串。例如 " onchange=\"changeYear()\" "
     *            ,这样任何js的方法就可以在jsp页面中编写,方便引入。
     * @return 返回年份的下拉框。
     */
    public static String getYearSelect(String selectName, String value,
            int startYear, int endYear, boolean hasBlank,String js) {
        int start = startYear;
        int end = endYear;
        if (startYear > endYear) {
            start = endYear;
            end = startYear;
        }
        StringBuffer sb = new StringBuffer("");

        sb.append("<select name=\"" + selectName + "\" " + js + ">");
        if (hasBlank) {
            sb.append("<option value=\"\"></option>");
        }       
        for (int i = start; i <= end; i++) {
            if (!value.trim().equals("") && i == Integer.parseInt(value)) {
                sb.append("<option value=\"" + i + "\" selected>" + i
                        + "</option>");
            } else {
                sb.append("<option value=\"" + i + "\">" + i + "</option>");
            }
        }
        sb.append("</select>");
        return sb.toString();
    }
    /**
     * 返回年份的下拉框。
     * @param selectName
     *            下拉框名称
     * @param value
     *            当前下拉框的值
     * @param startYear
     *            开始年份
     * @param endYear
     *            结束年份
     * @param js
     *            这里的js为js字符串。例如 " onchange=\"changeYear()\" "
     *            ,这样任何js的方法就可以在jsp页面中编写,方便引入。
     * @return 返回年份的下拉框。
     */
    public static String getYearSelect(String selectName, String value,
            int startYear, int endYear, String js) {
        int start = startYear;
        int end = endYear;
        if (startYear > endYear) {
            start = endYear;
            end = startYear;
        }
        StringBuffer sb = new StringBuffer("");
        sb.append("<select name=\"" + selectName + "\" " + js + ">");
        for (int i = start; i <= end; i++) {
            if (!value.trim().equals("") && i == Integer.parseInt(value)) {
                sb.append("<option value=\"" + i + "\" selected>" + i
                        + "</option>");
            } else {
                sb.append("<option value=\"" + i + "\">" + i + "</option>");
            }
        }
        sb.append("</select>");
        return sb.toString();
    }
    /**
     * 获取月份的下拉框
     * 
     * @param selectName
     * @param value
     * @param hasBlank
     * @return 返回月份的下拉框。
     */
    public static String getMonthSelect(String selectName, String value,
            boolean hasBlank) {
        StringBuffer sb = new StringBuffer("");
        sb.append("<select name=\"" + selectName + "\">");
        if (hasBlank) {
            sb.append("<option value=\"\"></option>");
        }
        for (int i = 1; i <= 12; i++) {
            if (!value.trim().equals("") && i == Integer.parseInt(value)) {
                sb.append("<option value=\"" + i + "\" selected>" + i
                        + "</option>");
            } else {
                sb.append("<option value=\"" + i + "\">" + i + "</option>");
            }
        }
        sb.append("</select>");
        return sb.toString();
    }

    /**
     * 获取月份的下拉框
     * 
     * @param selectName
     * @param value
     * @param hasBlank
     * @param js
     * @return 返回月份的下拉框。
     */
    public static String getMonthSelect(String selectName, String value,
            boolean hasBlank, String js) {
        StringBuffer sb = new StringBuffer("");
        sb.append("<select name=\"" + selectName + "\" " + js + ">");
        if (hasBlank) {
            sb.append("<option value=\"\"></option>");
        }
        for (int i = 1; i <= 12; i++) {
            if (!value.trim().equals("") && i == Integer.parseInt(value)) {
                sb.append("<option value=\"" + i + "\" selected>" + i
                        + "</option>");
            } else {
                sb.append("<option value=\"" + i + "\">" + i + "</option>");
            }
        }
        sb.append("</select>");
        return sb.toString();
    }

    /**
     * 获取天的下拉框,默认的为1-31。
     * 注意:此方法不能够和月份下拉框进行联动。
     * @param selectName
     * @param value
     * @param hasBlank
     * @return 获得天的下拉框
     */
    public static String getDaySelect(String selectName, String value,
            boolean hasBlank) {
        StringBuffer sb = new StringBuffer("");
        sb.append("<select name=\"" + selectName + "\">");
        if (hasBlank) {
            sb.append("<option value=\"\"></option>");
        }
        for (int i = 1; i <= 31; i++) {
            if (!value.trim().equals("") && i == Integer.parseInt(value)) {
                sb.append("<option value=\"" + i + "\" selected>" + i
                        + "</option>");
            } else {
                sb.append("<option value=\"" + i + "\">" + i + "</option>");
            }
        }
        sb.append("</select>");
        return sb.toString();
    }

    /**
     * 获取天的下拉框,默认的为1-31
     * 
     * @param selectName
     * @param value
     * @param hasBlank
     * @param js
     * @return 获取天的下拉框
     */
    public static String getDaySelect(String selectName, String value,
            boolean hasBlank, String js) {
        StringBuffer sb = new StringBuffer("");
        sb.append("<select name=\"" + selectName + "\" " + js + ">");
        if (hasBlank) {
            sb.append("<option value=\"\"></option>");
        }
        for (int i = 1; i <= 31; i++) {
            if (!value.trim().equals("") && i == Integer.parseInt(value)) {
                sb.append("<option value=\"" + i + "\" selected>" + i
                        + "</option>");
            } else {
                sb.append("<option value=\"" + i + "\">" + i + "</option>");
            }
        }
        sb.append("</select>");
        return sb.toString();
    }

    /**
     *  计算两天之间有多少个周末(这个周末,指星期六和星期天,一个周末返回结果为2,两个为4,以此类推。)
     * (此方法目前用于统计司机用车记录。)
     * @param startDate
     *            开始日期 ,格式"yyyy/MM/dd"
     * @param endDate
     *            结束日期 ,格式"yyyy/MM/dd"
     * @return int 
     */
    public static int countWeekend(String startDate, String endDate) {
        int result = 0;
        Date sdate = null;
        Date edate = null;
        sdate = getDateObj(startDate, "/"); // 开始日期
        edate = getDateObj(endDate, "/");// 结束日期
        // 首先计算出都有那些日期,然后找出星期六星期天的日期
        int sumDays = Math.abs(getDiffDays(startDate, endDate));
        int dayOfWeek = 0;
        for (int i = 0; i <= sumDays; i++) {
            dayOfWeek = getDayOfWeek(getDateAdd(sdate, i)); // 计算每过一天的日期
            if (dayOfWeek == 1 || dayOfWeek == 7) { // 1 星期天 7星期六
                result++;
            }
        }
        return result;
    }

    /**
     * 返回两个日期之间相差多少天。
     * 
     * @param startDate
     *            格式"yyyy/MM/dd"
     * @param endDate
     *            格式"yyyy/MM/dd"
     * @return 整数。
     */
    public static int getDiffDays(String startDate, String endDate) {
        long diff = 0;
        SimpleDateFormat ft = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        try {
            Date sDate = ft.parse(startDate + " 00:00:00");
            Date eDate = ft.parse(endDate + " 00:00:00");
            diff = eDate.getTime() - sDate.getTime();
            diff = diff / 86400000;// 1000*60*60*24;
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return (int) diff;

    }

    /**
     * 返回两个日期之间的详细日期数组(包括开始日期和结束日期)。
     * 例如:2007/07/01 到2007/07/03 ,那么返回数组
     * {"2007/07/01","2007/07/02","2007/07/03"}
     * @param startDate 格式"yyyy/MM/dd"
     * @param endDate  格式"yyyy/MM/dd"
     * @return 返回一个字符串数组对象
     */
    public static String[] getArrayDiffDays(String startDate,String endDate){
        int LEN = 0; //用来计算两天之间总共有多少天
        //如果结束日期和开始日期相同
        if(startDate.equals(endDate)){
            return new String[]{startDate};
        }
        Date sdate = null;
        sdate = getDateObj(startDate, "/"); // 开始日期
        LEN = getDiffDays(startDate,endDate);
        String[] dateResult = new String[LEN+1];
        dateResult[0]=startDate;
        for(int i=1;i<LEN+1;i++){
            dateResult[i] = getFormatDateTime( getDateAdd(sdate, i),"yyyy/MM/dd");
        }

        return dateResult;
    }

    /**
     * 时间戳转换成日期格式字符串
     * @param seconds 精确到秒的字符串
     * @param format
     * @return
     */
    public static String timeStamp2Date(String seconds,String format) {
        if(seconds == null || seconds.isEmpty() || seconds.equals("null")){
            return "";
        }
        if(format == null || format.isEmpty()) format = "yyyy-MM-dd HH:mm:ss";
        SimpleDateFormat sdf = new SimpleDateFormat(format);
        return sdf.format(new Date(Long.valueOf(seconds+"000")));
    }
    /**
     * 日期格式字符串转换成时间戳
     * @param date_str 字符串日期
     * @param format 如:yyyy-MM-dd HH:mm:ss
     * @return
     */
    public static String date2TimeStamp(String date_str,String format){
        try {
            SimpleDateFormat sdf = new SimpleDateFormat(format);
            return String.valueOf(sdf.parse(date_str).getTime()/1000);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * 取得当前时间戳(精确到秒)
     * @return
     */
    public static String timeStamp(){
        long time = System.currentTimeMillis();
        String t = String.valueOf(time/1000);
        return t;
    }
}
作者:pkandroid 发表于2016/9/14 17:53:11 原文链接
阅读:147 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>