GithubHelp home page GithubHelp logo

summary's Introduction

说明

这个项目实际上是一个空项目,我主要用它来记录一些自己实际工作和学习中总结的知识点,有部份是自己碰到的还没有解决的问题。所有的这些都是以issues的形式来管理的。如果你在看的过程中有任何问题可以加comments。如果你知道我这里面某个问题的答案也希望你能不吝赐教,非常感谢。你可以点右边的Issues或点击这里查看我的所有记录。

summary's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

summary's Issues

[Design-Pattern]-Observer

观察者模式:定义了对象之间的一对多依赖关系,当一个对象改变状态时,它的所有依赖者都会收到通知并进行相应的操作。
两个角色主题观察者,它们之间是一对多的关系。在主题中包含有注册移除观察者的操作和通知观察者状态变化的操作。观察者中会有相应的响应方法,用于在接收到通知后执行。
代码:

  • 主题
public interface Subject {
    public void registerObserver(Observer o);
    public void removeObserver(Observer o);
    public void notifyObservers();
}
  • 观察者
public interface Observer {
    public void execute(); //execute方法可以根据需要定义相关的传入参数,将主题中的数据传给观察者。
}
  • 一个主题的实现
public class ConcretSubject implements Subject{
    private ArrayList<Observer> observerList;

    public ConcretSubject() {
        observerList = new ArrayList<Observer>();
    }

    @Override
    public void registerObserver(Observer o) {
        observerList.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        observerList.remove(o);             
    }

    @Override
    public void notifyObservers() {
        for(Observer observer : observerList) {
            observer.execute();
        }        
    }
}

  • 一个观察者的实现
public class ConcretObserver implements Observer{
    @Override
    public void execute() {
        System.out.println("Observer execute.....");        
    }
}

*一个测试类

public class TestObserver {
    public static void main(String args []) {
        Subject subject = new ConcretSubject();
        Observer observer = new ConcretObserver();
        subject.registerObserver(observer);
        subject.notifyObservers();        
    }
}

还记得以前学Java awt/swing的时候,接触到的事件监听模型,和观察者多少有些类似,也写在下面:

import java.util.EventListener;
import java.util.EventObject;

public class EventTest {
    public static void main(String args[]) {
        ConcretEventSource e = new ConcretEventSource();
        e.setEventListener(new ConcretListener());
        e.fireEvent();
    }
}

// 事件
class ConcretEvent extends EventObject {
    public ConcretEvent(Object source) {
        super(source);
    }
}

// 监听者
class ConcretListener implements EventListener {
    // 监听器的行为,传进来的参数是事件对象
    public void listenerBehave(EventObject e) {
        ConcretEventSource source = (ConcretEventSource) e.getSource();
        System.out.println("listener execute");
    }
}

// 事件源对象
class ConcretEventSource {
    ConcretListener listener;

    public void setEventListener(ConcretListener listener) {
        this.listener = listener;
    }

    public void fireEvent() {
        ConcretEvent e = new ConcretEvent(this);
        System.out.println("发出一个事件");
        listener.listenerBehave(e);
    }
}

[Android] 那些零碎的知识点

Android 中那些零碎的知识点

1 IntentService

IntentServiceService的子类,用来处理同步的请求。在IntentService中会开一个工作线程来处理请求。我们可以通过类似于启动Service的方来来启动一个IntentService, IntentService每创建一次,就会新开启一个工作线程来执行请求。执行完一个intent请求对应的工作后,如果没有新的请求到达,则自动停止IntentService, 否则会执行下一个请求。所有需要在后台运行的逻辑我们需要写在onHandleIntent方法中。感觉网上有些博客没说得太清楚,这里自己写个Demo, 加深一下印象

来看这么一段代码:

MyIntentService

public class MyIntentService extends IntentService {
    private final static String TAG = "MyIntentService";
    public MyIntentService() {
        super(TAG);

    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i(TAG, "onCreate pid:" + android.os.Process.myPid() + "    thread id:" + Thread.currentThread().getId());

    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.i(TAG, "onHandleIntent pid:" + android.os.Process.myPid() + "    thread id:" + Thread.currentThread().getId());
    }
}

调用者

假设这个代码是当用户点击一个按钮后执行的。

Log.i(TAG, "caller pid:" + android.os.Process.myPid() + "    thread id:" + Thread.currentThread().getId());
startService(new Intent(AActivity.this, MyIntentService.class));
try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
startService(new Intent(AActivity.this, MyIntentService.class));

运行后发现

06-11 09:36:39.113    1680-1680/? I/AActivity﹕ caller pid:1680    thread id:1
06-11 09:36:42.118    1680-1680/? I/MyIntentService﹕ onCreate pid:1680    thread id:1
06-11 09:36:42.119    1680-1703/? I/MyIntentService﹕ onHandleIntent pid:1680    thread id:163
06-11 09:36:42.119    1680-1703/? I/MyIntentService﹕ onHandleIntent pid:1680    thread id:163

在这个例子通过观察线程id,发现虽然启动了两次这个IntentService,但IntentService只创建了一次。

如果我们将调用的代码改成

Log.i(TAG, "caller pid:" + android.os.Process.myPid() + "    thread id:" + Thread.currentThread().getId());
startService(new Intent(AActivity.this, MyIntentService.class));

同样假设这个代码是当用户点击一个按钮时执行。若用户连续点击这个按钮两次,运行结果发现

06-11 09:45:35.454    1784-1784/? I/AActivity﹕ caller pid:1784    thread id:1
06-11 09:45:35.469    1784-1784/? I/MyIntentService﹕ onCreate pid:1784    thread id:1
06-11 09:45:35.469    1784-1807/? I/MyIntentService﹕ onHandleIntent pid:1784    thread id:169
06-11 09:45:35.773    1784-1784/? I/AActivity﹕ caller pid:1784    thread id:1
06-11 09:45:35.777    1784-1784/? I/MyIntentService﹕ onCreate pid:1784    thread id:1
06-11 09:45:35.777    1784-1809/? I/MyIntentService﹕ onHandleIntent pid:1784    thread id:170

在这个例子中,会发现,当IntentService处理完一个请求后,确实会将自己销毁。若此下一个请求来了,会再重新创建一个。所以发现两次的thread id不一样。

IntentService和Service一个重大的差别是在于,IntentService是会开一个工作线程来处理作务,而Service不会,因此Service中不能执行非常耗时的操作,否则会抛异常。下面便是一种验证方式:

@Override
public void onStart(Intent intent, int startId) {
    super.onStart(intent, startId);
    Log.i(TAG, "onStart");
    //以下代码写在Service的onStart方法中会抛异常,但写在IntentService的onHandleIntent中不会。
    try {
        Thread.sleep(20000); 
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

2 PendingIntent

PendingIntent 是对Intent的一个包装,用来处理未来发生的事。PendingIntent中会保存当前app的Context, 即使当前app已经停止了,仍然可以通过PendingIntent中的Context来执行Intent,通常与AlarmManagerNotificationManager在一起使用。这里有一个示例:

public void testPendingIntent() {
    NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    long when = System.currentTimeMillis();// 通知发生的时间为系统当前时间
    int icon = android.R.drawable.stat_notify_chat;
    //第一个参数为图标 , 第二个参数为短暂提示标题 , 第三个为通知时间
    Notification notification = new Notification(icon, "TEST Notification", when);
    notification.defaults = Notification.DEFAULT_SOUND;// 发出默认声音
    notification.flags = Notification.FLAG_AUTO_CANCEL;// 点击后自动清除通知
    Intent openintent = new Intent(this, BActivity.class);
    PendingIntent contentIntent = PendingIntent.getActivity(this, 0, openintent, 0);
    notification.setLatestEventInfo(this, "This is title", "Content", contentIntent);
    manager.notify(0, notification);// 第一个参数为自定义的通知唯一标识
}

3 输入字符转换

之前有这么一个需求,某个EditText中接收的英文字母只能是大写的。当看到这个需求,我们的第一反应是设对这个EditText设置其键盘弹出属性android:inputType="textCapCharacters"。但是既使这样,用户是可以切换到小写模式下。当然或许我们还可以尝试其它的属性,但是想要统一各种机子上输入法的控制是比较难的,确实有些机子的输入法被产商弄得很奇葩,不受你的控制。我们可以用以下这种方式,自动将其转为大写的样式。

定义一个Util类

public class InputLowerToUpperTrans extends ReplacementTransformationMethod {
    @Override
    protected char[] getOriginal() {
        char[] lower = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z' };
        return lower;
    }

    @Override
    protected char[] getReplacement() {
        char[] upper = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z' };
        return upper;
    }
}

调用处

etTest.setTransformationMethod(new InputLowerToUpperTrans());

4 LocalBroadcastManager

在Android中,对于大部分app而言,发送广播时,只希望广播被自己app内部的receiver监听到,此时我们可以采用LocalBroadcastManager发送广播,安全性高,效率也高。
具体使用:
1. 写一个BroadcastReceiver, 和通常写法一样

public class MyReceiver extends BroadcastReceiver{
    public final static String TAG = "MyReceiver";

    @Override
    public void onReceive(Context context, Intent intent) {
        Log.i(TAG, "------onReceive------");
    }

}

2. 注册我把它写到onResume里面

@Override
protected void onResume() {
    super.onResume();        
    myReceiver = new MyReceiver();
    IntentFilter filter = new IntentFilter();  
    filter.addAction(ACTION); //ACTION是自己定义的一个成员变量
    LocalBroadcastManager.getInstance(MainActivity.this).registerReceiver(myReceiver, filter);
}

3. 解注册

@Override
protected void onStop() {
    super.onStop();
    LocalBroadcastManager.getInstance(MainActivity.this).unregisterReceiver(myReceiver);
}

4. 发广播

Intent intent = new Intent();
intent.setAction(ACTION);//ACTION是自己定义的一个成员变量
LocalBroadcastManager.getInstance(MainActivity.this).sendBroadcast(intent);

[Android]ViewPager中图片显示的优化

ViewPager中图片显示的优化

首先直接看这么一段代码:

Activity

public class TestViewPagerActivity extends Activity {
    private ViewPager viewPager;
    private ImageView imageView;
    private int res[] = {R.drawable.image_1, R.drawable.image_2, R.drawable.image_3, R.drawable.image_4};

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_view_pager);
        viewPager = (ViewPager) findViewById(R.id.viewpager);
        viewPager.setAdapter(adapter);
    }

    private PagerAdapter adapter = new PagerAdapter() {
        @Override
        public int getCount() {
            return 4;
        }

        @Override
        public boolean isViewFromObject(View view, Object o) {
            return view == o;
        }

       @Override
        public Object instantiateItem(ViewGroup container, int position) {
            LayoutInflater inflater = (LayoutInflater) container.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            View view = inflater.inflate(R.layout.view_pager_item, null);
            imageView = (ImageView) view.findViewById(R.id.image);
            imageView.setImageResource(res[position]);
            container.addView(view);
            return view;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }
    };
}

布局文件

activity_test_view_pager.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context="com.example.fred.testas.TestViewPagerActivity">
    <android.support.v4.view.ViewPager
        android:id="@+id/viewpager"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
    />
</RelativeLayout>

view_pager_item.xml

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

在我们的项目中,有一个功能就是采用这种方式来做的。但是从收集到的Crash report中反现这地方出现过几次Crash,于是想着看能不能优化这一块。于是我便采有Android Studio中的Memory Monitor工具进行分析。当采用上面的代码跑在Device上时,App起动时Memory Monitor上显示 Allocated(20.92M), Free(4.15M), 几次滑动后,发现迅速的增长到Allocated(23.25M), Free(15.47M)。接下来再不断的滑动图片,内存的占用会不断的增长,但增长很缓慢。
试着进行如下优化:

1.从网上查询得知当我们在使用imageView.setBackgroundResource,imageView.setImageResource, 或者 BitmapFactory.decodeResource 这样的方法来设置图片,这些方法最终都是通过java层的createBitmap来完成,会消耗更多内存。
改用先通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的 source,decodeStream最大的秘密在于其直接调用JNI>>nativeDecodeAsset()来完成decode,无需再使用java层的createBitmap,从而节省了java层的空间。
此时,我将instantiateItem方法的实现改为:

public Object instantiateItem(ViewGroup container, int position) {
    LayoutInflater inflater = (LayoutInflater) container.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View view = inflater.inflate(R.layout.view_pager_item, null);
    imageView = (ImageView) view.findViewById(R.id.image);
    InputStream is = getResources().openRawResource(res[position]);
    BitmapFactory.Options options = new  BitmapFactory.Options();
    //修改图片属性
    options.inPreferredConfig =  Bitmap.Config.RGB_565;
    Bitmap btp =  BitmapFactory.decodeStream(is, null,  options);
    imageView.setImageBitmap(btp);
    container.addView(view);
    return view;
}

发现,App启动时Memory Monitor显示 Allocated(16.06M), Free(9.01M)。不断滑动,发现内存也增长的比较快,大概每滑动两次,会增长0.15M左右。当Memory Monitor上显示 Allocated(25.12M), Free(0.13M)时,再滑动一次,发现GC触发了,内存回收。于是,显示Allocated(16.01M), Free(11.03M)。整体上看这种方式占用的内存是要少一些。看来这种方式是有效的。

针对内存不断上升的这种情况,看能不能从destroyItem方法上找找思路,于是想着在destroyItem方法中强制回收Bitmap,进行如下代码改动。

public void destroyItem(ViewGroup container, int position, Object object) {
    View view = (View)object;
    imageView = (ImageView) view.findViewById(R.id.image);
    BitmapDrawable bitmapDrawable = ((BitmapDrawable)imageView.getDrawable());
    if (bitmapDrawable != null && bitmapDrawable.getBitmap() != null) {
        bitmapDrawable.getBitmap().recycle();
    }
    container.removeView((View) object);
}

结果发现,效果并不明显,和没有改动之前的效果基本上是一样的。此处暂且放一下,等会再说。

2.另外一种思路是这样,首先就先需要用到的Bitmap全部加载进来,这样就会保证操作的只会是这么几个bitmap.

public class TestViewPagerActivity extends Activity {
    private ViewPager viewPager;
    private ImageView imageView;
    private int res[] = {R.drawable.image_1, R.drawable.image_2, R.drawable.image_3, R.drawable.image_4};
    private List<Bitmap> bitmapList = new ArrayList<Bitmap>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_view_pager);
        viewPager = (ViewPager) findViewById(R.id.viewpager);
        viewPager.setAdapter(adapter);
        for (int resId : res) {
            InputStream is = getResources().openRawResource(resId);
            BitmapFactory.Options options = new  BitmapFactory.Options();
            options.inPreferredConfig =  Bitmap.Config.RGB_565;
            Bitmap btp =  BitmapFactory.decodeStream(is, null,  options);
            bitmapList.add(btp);
        }

    }

    private PagerAdapter adapter = new PagerAdapter() {
        @Override
        public int getCount() {
            return 4;
        }

        @Override
        public boolean isViewFromObject(View view, Object o) {
            return view == o;
        }

        @Override
        public Object instantiateItem(ViewGroup container, int position) {
            LayoutInflater inflater = (LayoutInflater) container.getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            View view = inflater.inflate(R.layout.view_pager_item, null);
            imageView = (ImageView) view.findViewById(R.id.image);
            imageView.setImageBitmap(bitmapList.get(position));
            container.addView(view);
            return view;
        }

        @Override
        public void destroyItem(ViewGroup container, int position, Object object) {
            container.removeView((View) object);
        }
    };
}

程序一启动时, Allocated(16.35M), Free(8.72M). 左右滑动,每滑动两次,内存增长0.01M。整体感觉第二种思路内存的消耗是要小一些。

关于图片性能处理的总结可以看这里

[Android]回调机制在Android项目中的应用

Android项目中难免会涉及到一些比较耗时的操作,如从网络上取数据,读本地的文件等等。Android 也推荐我们去新开一个线程完成这些操作,而不要在主线程中执行。当这些操作完成之后再调用主线程去更新UI。很明显当....之后.....这种类型的逻辑便是一个回调的模式。本文以一个从网络上取数据的例子来解释如何应用回调机制来实现一种比较好的设计。

Android中对比较耗时的操作通常采用两种方式来实现,一种是利用Thread + Handler,新开一个Thread去处理操作,操作完成后,采用handler发送message,然后更新UI。另一种方式是采用AsyncTask来实现,在AsyncTask中的doInBackground方法中去完成耗时的操作,当然这个方法中的代码会运行在另一个线程中。操作完成后在onPostExecute方法中进行UI的更新操作。两种方式无好坏之分,但个人倾向于采用AsyncTask的实现方式,觉得更简洁清晰一些。

直接上代码:

  • 回调接接口的定义,为什么要这样定义在后面会有解释,当然可以根据项目的实际需要来自己进行适当修改。
public interface IFeedback {
    boolean onFeedback(String key, boolean isSuccess, Object object);
}
  • 两个AsyncTask的设计
    • GetBookTask
public class GetBookTask extends AsyncTask<String, Void, Object> {
    public static final String TAG = GetBookTask.class.getSimpleName();
    public static final String FEED_BACK_KEY = "GetBookTask";
    private IFeedback feedback;
    private boolean isSuccess = false; 
    public GetBookTask(IFeedback feedback) {
        this.feedback = feedback;
    }
    @Override
    protected Object doInBackground(String... params) {
        Log.i(TAG, "task executing....");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        isSuccess = true;
        return null;
    }
    protected void onPostExecute(Object result) {
        super.onPostExecute(result);
        feedback.onFeedback(FEED_BACK_KEY, this.isSuccess,
                result);
    }
}
    • GetFookTask
public class GetFoodTask extends AsyncTask<String, Void, Object>  {
    public static final String TAG = GetFoodTask.class.getSimpleName();
    public static final String FEED_BACK_KEY = "GetFoodTask";
    private IFeedback feedback;
    private boolean isSuccess = false;    
    public GetFoodTask(IFeedback feedback) {
        this.feedback = feedback;
    }
    @Override
    protected Object doInBackground(String... params) {
        Log.i(TAG, "task executing....");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        isSuccess = true;
        return null;
    }
    protected void onPostExecute(Object result) {
        super.onPostExecute(result);
        feedback.onFeedback(FEED_BACK_KEY, this.isSuccess,
                result);
    }
}
  • Activity
public class MainActivity extends Activity {
    public static final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new GetBookTask(feedback).execute(new String[]{""});
        new GetFoodTask(feedback).execute(new String[]{""});
    }
    private final IFeedback feedback = new IFeedback(){
        @Override
        public boolean onFeedback(String key, boolean isSuccess, Object object) {
           if (GetBookTask.FEED_BACK_KEY.equals(key)) {
               if (isSuccess) {
                   Log.i(TAG, "GetBookTask execute success and update UI");
                   return true;
               }
           }
           if (GetFoodTask.FEED_BACK_KEY.equals(key)) {
               if(isSuccess) {
                   Log.i(TAG, "GetFoodTask execute success and update UI");
                   return true;
               }
           }
           return false;
        }       

    };    
}

总结一下

  • 关于回调接口的设计:IFeedback中只有一个回调方法onFeedback,该方法有三个参数,第一个参数的类型是String,用来区分是哪一个AsyncTask的回调;第二个参数是用来表示对应的AsyncTask的执行是否成功了;第三个参数是一个Object,代表的是AsyncTask执行后返回给主线程的数据。
  • 关于相应的AsyncTask的设计:每个AsyncTask中多了三个属性,分别是FEED_BACK_KEY, feedback对象,状态标志位isSuccess。将一个feedback对象作为构造函数的参数传递给AsyncTask,默认将FEED_BACK_KEY的值设置为Task的类名,在AsyncTaskdoInBackground方法中根据执行情况设置isSuccess的值,在onPostExecute中调用回调接口的onFeedback方法,将对应的三个属性值作为参数传递进去。
  • Activity中,由于一个Activity可能会调用多个AsyncTask,此时FEED_BACK_KEY的作用就体现出来了。根据FEED_BACK_KEY来判断是哪一个Task的回调。
  • 注意回调方法(指的是上文中onFeedback方法执行的线程)。特别是,如果onFeedback方法中有刷UI的操作,那就更要注意。在本文中onFeedback方法的调用是在AsyncTask类中的onPostExecute方法中,而这个方法是在主线程中运行,所以刷UI不会有任何问题。但有可能存在着这么一种情况,onFeedback方法的调用不是在主线程中运行,而是在某个子线程中运行,而此时onFeedback方法中又包含了刷UI的操作,那么问题就大的。这一点是值得注意的。如下方代码新开一个线程从Server拿数据,拿 到数据后,直接调onFeedback方法,此时的onFeedback方法是在这个新线程中运行的,如果这个方法中有刷UI的操作,那就不好玩了。
 new Thread() {
            @Override
            public void run() {
                try {
                    HttpResponse response = new DefaultHttpClient().execute(new HttpGet("http://www.baidu.com"));
                    //parse response
                    feedback.onFeedback(key, isSuccess, object);
                } catch (ClientProtocolException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (IOException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }.start();        

[Design Pattern] 命令模式

命令模式

以下内容是在学习《Head First设计模式》时做的笔记,会根据自己的理解做部份修改。
命令模式将请求封装成命令对象,将请求的对象和执行请求的对象解耦。

示例一

电灯有两种状态,开和关。对于这种情况,封装了两个命令类型,LightOnCommandLightOffCommand。看代码

Command接口的定义

package command;

public interface Command {
    public void execute();
    public void undo();
}
Light类
public class Light {
    public void on() {
        System.out.println("light on");
    }
    public void off() {
        System.out.println("light off");
    }
}

两个命令

public class LightOnCommand implements Command {
    Light light;

    public LightOnCommand(Light light) {
        this.light = light;
    }

    @Override
    public void execute() {
        light.on();
    }

    @Override
    public void undo() {
        light.off();
    }

}
package command;

public class LightOffCommand implements Command {
    Light light;
    public LightOffCommand(Light light) {
        this.light = light;
    }
    @Override
    public void execute() {
        light.off();
    }

    @Override
    public void undo() {
        light.on();
    }

}

调用

package command;

public class Controller {
    public static void main(String args[]) {
        Light light = new Light();
        LightOnCommand lightOnCommond = new LightOnCommand(light);
        lightOnCommond.execute();
        lightOnCommond.undo();
    }
}

对于存在多种状态的情况

如电风扇有多种转速,1,2,3档,和关闭 四种状态。此时在做undo状态处理时就需要知道上一个状态了。

风扇类

package command;

public class CeilingFan {
    public static final int HIGH = 3;
    public static final int MEDIUM = 2;
    public static final int LOW = 1;
    public static final int OFF = 0;
    String location;
    int speed;

    public CeilingFan(String location) {
        this.location = location;
        speed = OFF;
    }

    public void high() {
        speed = HIGH;
        System.out.println("Set speed-->HIGH");
    }

    public void medium() {
        speed = MEDIUM;
        System.out.println("Set speed-->MEDIUM");

    }

    public void low() {
        speed = LOW;
        System.out.println("Set speed-->LOW");
    }

    public void off() {
        speed = OFF;
        System.out.println("Set CeilingFan off");

    }

    public int getSpeed() {
        return speed;
    }

}

风扇的状态

* 状态 1

package command;

public class CeilingFanHighCommand implements Command {
    CeilingFan ceilingFan;
    int prevSpeed;

    public CeilingFanHighCommand(CeilingFan ceilingFan) {
        this.ceilingFan = ceilingFan;
    }

    public void execute() {
        prevSpeed = ceilingFan.getSpeed();
        ceilingFan.high();
    }

    @Override
    public void undo() {
        if (prevSpeed == CeilingFan.HIGH) {
            ceilingFan.high();
        } else if (prevSpeed == CeilingFan.MEDIUM) {
            ceilingFan.medium();
        } else if (prevSpeed == CeilingFan.LOW) {
            ceilingFan.low();
        } else if (prevSpeed == CeilingFan.OFF) {
            ceilingFan.off();
        }

    }
}
  • 状态 2
package command;

public class CeilingFanMediumCommand implements Command {
    CeilingFan ceilingFan;
    int prevSpeed;
    @Override
    public void execute() {
        prevSpeed = ceilingFan.getSpeed();
        ceilingFan.medium();
    }

    @Override
    public void undo() {
        if (prevSpeed == CeilingFan.HIGH) {
            ceilingFan.high();
        } else if (prevSpeed == CeilingFan.MEDIUM){
            ceilingFan.medium();
        } else if (prevSpeed == CeilingFan.LOW) {
            ceilingFan.low();
        } else if (prevSpeed == CeilingFan.OFF) {
            ceilingFan.off();
        }
    }

}

调用

package command;

public class Controller {
    public static void main(String args[]) {
        CeilingFan ceilingFan = new CeilingFan("Living Room");
        CeilingFanHighCommand ceilingFanHighCommand = new CeilingFanHighCommand(ceilingFan);
        ceilingFanHighCommand.execute();
        System.out.println(ceilingFan.speed);
        ceilingFanHighCommand.undo();
        System.out.println(ceilingFan.speed);
    }
}

宏的使用

关于宏我们可以简单理解成就是执行一系列的命令。比如当进一进房间的时间,先要开灯,再开电扇,那么我们可以采用命令来做。

  • 首先我们需要定义一个宏命令。
package command;

public class MacroCommand implements Command {
    Command[] commands;

    public MacroCommand(Command[] commands) {
        this.commands = commands;
    }

    @Override
    public void execute() {
        for (Command command : commands) {
            command.execute();
        }
    }
    //假设撤销操作是逆向的。
    @Override
    public void undo() {
        int size = commands.length;
        for (int i = size - 1; i >= 0; i--) {
            commands[i].undo();
        }
    }

}
  • 使用
package command;

public class Controller {
    public static void main(String args[]) {
        CeilingFan ceilingFan = new CeilingFan("Living Room");
        CeilingFanHighCommand ceilingFanHighCommand = new CeilingFanHighCommand(
                ceilingFan);
        Light light = new Light();
        LightOnCommand lightOnCommand = new LightOnCommand(light);
        Command[] partyCommands = { lightOnCommand, ceilingFanHighCommand };
        MacroCommand macroCommand = new MacroCommand(partyCommands);
        macroCommand.execute();
        macroCommand.undo();
    }
}

可以看到输出结果是:

light on
Set speed-->HIGH
Set CeilingFan off
light off

[Android] Android程序中的数据加密存储(AES)

采用AES算法对Android程序中的数据进行加密存储

在开发android程序时,难免会在本地存储一些数据,对于一些敏感数据,我们需要对其进行加密存储,下面记录一下我们采用的一个加密算法。

首先了解一下Android中的四种存储方式。

四种存储方式

1 SharedPreference

SharedPreference中的数据是以key-value的集合需要保存,有些类似于java中的Hashtable。只是SharedPreference中只能存储一些简单的数据类型。同一个Application中可以指定多个sharedPreference,也可以只使用同一个sharedPreference文件。

getSharedPreferences(String name, int mode)方法是根据第一个参数名来区分sharedPreference文件。
getPreferences(int mode) 是返回系统默认的sharedPreference文件。

不管采用哪种方式,sharedPreference存在手机中的位子是在/data/data/{packagename}/shared_prefs/目录下。

2 Internal Storage

Internal Storage(内部存储)并不是将数据存在内存中,内部存储在手机中的位子是/data/data/{packagename}files/,内部存储中的文件只能被自己的应用访问到。当一个应用卸载之后,内部存储中的这些文件也被删除。
获取内部存储的路径可以用

File file = getFilesDir();
Log.i(TAG, "file path:" + file.getAbsolutePath());

结果显示

extFile private path:/data

3 External storage

External storage(外部存储), 我们通常理解的外部存储就是类似于U盘,移动硬盘等。但现在Android设备的配置越来越高, 外部存储已集成到手机内部。外部存储中的文件是可以被用户或者其他应用程序访问和修改。和内部存储不一样,外部存储通常不一定存在,在使用时应该先进行检测。Google给出了这么一段代码:

/* Checks if external storage is available for read and write */
public boolean isExternalStorageWritable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }
    return false;
}

/* Checks if external storage is available to at least read */
public boolean isExternalStorageReadable() {
    String state = Environment.getExternalStorageState();

   if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}

对于外部存储,有两种不同类型的文件。

  • public files
    文件是可以被自由访问,且文件的数据对其他应用或者用户来说都是由意义的,当应用被卸载之后,文件仍然保留。
  • private files
    由于private files 也存在于外部存储中, 这种类型的文件只能被其他程序访问,只不过一个应用私有的文件对其他应用其实是没有访问价值的(恶意程序除外)。当应用卸载时,这些文件也会被删除。像我们app下载的一些额外的资源便类似于这种文件。

获取public files的文件路径

File extFilePublic = Environment.getExternalStoragePublicDirectory("test");
Log.i(TAG, "extFile public path:" + extFilePublic.getAbsolutePath());

结果显示

extFile public path:/storage/emulated/0/test

获取private files的文件路径

File extFile = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
Log.i(TAG, "extFile private path:" + extFile.getAbsolutePath());

结果显示

extFile private path:/storage/emulated/0/Android/data/{packageName}/files/Pictures

4 Sqlite

数据存到android自带的sqlite数据库中,这个每个app都会用到。sqlite数据库文件存在内部存储中, 只会被自己的应用程序访问,其存储路径位于/data/data/{packagename}/databases/。关于sqlite的操作,这里不做说明,我的这篇博客里面有具体的操作,可以参看。

通过上面的分析可以看到,对于SharedPreference, inter storage, sqlite中的数据我们都可以用adb 工具查看,都位于/data目录下。所以对于应用中存储的数据,一旦拿到了手机,便可以拿到任何应用的存储数据。因此,对于敏感信息,必须进行加密。

加密

下面以SharedPreference为例,进行数据的加密操作,我们采用的是对称加密算法中的AES算法

    public static byte[] encrypt(byte[] key, byte[]data) {
        SecretKeySpec sKeySpec = new SecretKeySpec(key, "AES");
        Cipher cipher;
        byte[] encryptedData = null;
        try {
            cipher = Cipher.getInstance("AES");
            cipher.init(Cipher.ENCRYPT_MODE, sKeySpec);
            encryptedData = cipher.doFinal(data);
        } catch (Exception e) {
            Log.i(TAG, "encrypt exception" + e.getMessage());
        }
        return encryptedData;
    }

    public static byte[] decrypt(byte[] key, byte[]data) {
        SecretKeySpec sKeySpec = new SecretKeySpec(key, "AES");
        Cipher cipher;
        byte [] decryptedData = null;
        try {
            cipher = Cipher.getInstance("AES");
            cipher.init(Cipher.DECRYPT_MODE, sKeySpec);
            decryptedData = cipher.doFinal(data);
        } catch (Exception e) {
            Log.i(TAG, "decrypt exception" + e.getMessage());
        }
        return decryptedData;
    }

    public static byte[] generateKey(byte[] randomNumberSeed) {
        SecretKey sKey = null;
        KeyGenerator keyGen;
        try {
            keyGen = KeyGenerator.getInstance("AES");
            SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
            random.setSeed(randomNumberSeed);
            sKey = keyGen.generateKey();
        } catch (NoSuchAlgorithmException e) {
            Log.i(TAG, "generateKey exception" + e.getMessage());
        }
        return sKey.getEncoded();
    }

    //调用的demo
    public static void testAES() {
        String randomNumberSeed = "aaaa";
        String data = "Hello World";
        byte [] key = generateKey(randomNumberSeed.getBytes());
        byte [] encryptData = encrypt(key, data.getBytes());
        String str = Base64.encodeToString(encryptData, Base64.DEFAULT);
        Log.i(TAG, "encrypt data:" + str);
        byte [] decodeData = Base64.decode(str, Base64.DEFAULT);
        Log.i(TAG, "encrypt data:" + new String(decrypt(key, decodeData)));
    }

在加密和解密的时候我们都依赖于一个key, 这个key的生成会决定着加密效果。key可以保存在内存中或者以二进制的形式写入到文件,每次当程序启动时,从文件读取到内存中。key的生成,我们可以自定义一些规则,比如当前用户的密码+特定字符串,当前deviceId+特定字符串等等。这个就自定义了。

[Design-Pattern]-Singleton 其实单例也没那么简单

单例模式:简单来说就是在系统中维持某个类的一个实例,并且自行实例化。通常分为两种:饿汉式单例和懒汉式单例。

饿汉式单例:在类初始化的时候就生成实例。

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance() {
        return instance;
    }
}

懒汉式单例:在需要实例的时候才初始化。

public class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            return new Singleton();
        }
        return instance;        
    }
}

其实,对于懒汉式单例的这种实现方式还存在着一个问题,那就是在多线程情况下会出现线程安全性问题。很自然我们联想到要采用同步的机制。为此,将代码改成如下方式[即对getInstance方法进行同步]:

public class Singleton {
    private static Singleton instance = null;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            return new Singleton();
        }
        return instance;        
    }
}

当然,由于添加了sychronized,肯定会对性能产生影响,但是如果getInstance()方法调用不是很频繁,或者应用程序能够接受由此带来的负担,采用这种方式也OK. 饿汉式的单例由于是在JVM加载这个类时就会创建一个单例。可以保证在任何线程访问,实例就已经初始化好了。

在HeadFirst 设计模式中提到一种双重检查加锁的方式来实现单例。即采用valatile关键字。 注意,valatile关键适用于jdk1.5或1.5之后版本。代码如下:

public class Singleton {
    private volatile static Singleton instance = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

注:

  • volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
  • getInstance()方法中需要使用同步锁synchronized (Singleton.class)防止多线程同时进入造成instance被多次实例化。
  • 在上面在synchronized (Singleton.class)外又添加了一层if,这是为了在instance已经实例化后下次进入不必执行synchronized (Singleton.class)获取对象锁,从而提高性能。

[Android]Activity生命周期的总结

在Activity的整个生命周期中,Activity只有三种稳定状态: Resumed, Paused, Stopped. Resumed状态下,Activity是处于和用户交互状态。Paused状态下,Activity是部份遮盖,此时不会响应用户的行为。不会执行任何代码(不代表后台线程不执行). Stopped 状态,Activity对用户不可见,不会执行任何代码。

创建一个新的Activity

其生命周期回调方法的执行顺序是: onCreate—>onStart()—>onResume()
实际上 activity 在onStart()方法被调用后就已是对用户可见。只是在onResume()执行完后才能和用户交互。

Activity的销毁

系统通常是在执行了onPause() 和onStop() 之后再调用onDestory(),除非你的程序在onCreate()方法里面就调用了finish()方法。比如在Splash屏,我们会在onCreate()方法里面调用finish()方法,这样系统会直接就调用onDestory()方法,其它生命周期方法则都不会执行(onPause(), onStop()).

Activity的暂停

当activity被其它的组件挡住,如Dialog,导致其部分可见且不能和用户交互,此时activity将会进入paused状态。在onPause()方法中,我们应该进行资源的释放操作(停止动画)。当activity处于暂停状态时, Activity 实例是驻留在内存中的。
从paused状态恢复到可交互状态 会调用onResume()方法。

Activity 的停止和重启

(1)􏰅􏰆􏳿􏵹􏲂􏷕􏱋􏰅􏰄􏿬􏰃􏱡􏱵􏱶􏰿􏰒当用户打开最近使用app菜单并切换你的App到另一个App时,这个时候􏰏􏱯􏸸􏸹􏰄􏰗􏰠􏲹􏰅􏰆􏰘􏳘􏹔􏸒􏺍􏰇􏰈􏰄􏶋􏱽􏳙􏵅􏰍􏱃􏰣􏰤􏲂􏷕􏱋􏰅􏳙 􏵅􏰄􏰙你的App是被停止的,如果用户通过手机主界面的启动程序图标或者最近使用程序的窗口回到你的app,那么你的activity会重启。
(2) 由ActivityA 启动了Activity B, 此时Activity A会被Stop, 用户点击Back 按钮后,Activity A会被重启。
整个流程中Activity A生命周期方法调用如下:
onStop() —> onRestart() —> onStart() —> onResume()
无论什么原因导致Activity()的停止,系统总是会在onStop()之前调用onPause()。
当Activity的onStop()方法调用了,Activity将不再可见,并且应该释放那些不再需要的所有资源,一旦一个Activity停止了,系统会在不再需要这个activity时摧毁它的实例。在极端情况下,系统会直接杀死app 进程,并且不执行activity的onDestory()方法,因此资源的释放应该放在onStop()方法中。
onStart() 方法:用户进入到这个activity之前需要一段时间,所以onStart()方法是一个比较好的地方来验证某些必须的系统特性是否可用。

Activity的重新创建

导致Activity被destroy的行为:(a)用户按Back键,(2)程序中执行了finish()方法来结束自己,(3) 系统在Activity处于stop状态又长时间没有被使用时,或者前台Activity需要更多资源时,系统将这个后台进程关闭,以达到管理内存的目的。

针对第三种情况,因为Activity的停止是由系统造成的,系统会为用保存的数据记录(Activity被Destory时的状态)来重新创建一个新的实例。默认情况下,系统会采用Bundle对象保留每个视图对象的信息。

为了让用户可以保存额外更多的数据,系统提供了一个onSaveInstance()方法。 当Activity被异常destory时,系统会调用这个方法,保存数据信息,存在Bundle中。之后想重新创建这个Activity实例时,之前的那个Bundle对象会被传递到Activity的onRestoreInstanceState()方法和onCreate()方法中。

通常来讲,有这么几种情况会调用onSaveInstanceState()方法:

  • 跳转到其它的activity(不手动调用finish方法)

  • 按Home 键

  • 按下电源按键(关闭屏幕显示)时

  • 屏幕方向切换时,例如从竖屏切换到横屏时

  • 长按HOME键,选择运行其他的程序时。

    因为这几种情况下 activity都有可能被destroy。需要注意的是按物理Back键,执行退栈操作,onSaveInstance方法不会执行。

Acitivity 的恢复

1.可以在onCreate方法中拿到保存的数据, 但需要判断是Bundle否为nul。

if (null != saveInstanceState) {

}

2.在onRestoreInstanceState()方法中恢复状态信息,此时不用检查Bundle对象是否为null,因为系统仅仅会在存在需要恢复信息时才会调用onRestoreInstanceState()方法。也就是说,onRestoreInstanceState()方法执行时, Bundle肯定不会为null的。

关于onRestoreInstanceState方法,需要注意一下
首先明确一点是这个方法和onSaveInstance方法并不一定配对出现。也就是说,当一个Activity切换到不可见状态时执行了onSaveInstance方法,再由不可见状态切换到可见状态时不一定会执行onRestoreInstanceState方法。onRestoreInstanceState被调用的前提是,activity 确实被系统销毁了。比如,当前的机上显示的是Activity A, 当用户按HOME键后我们可以观察到onSaveInstanceState方法执行了,接着用户又返回到Activity A, 而此时,Activity A并没有被销毁,所以onRestoreInstanceState不会执行。来做两个实验:

实验一

由 activity A 跳转到 activity B 时,不手动调用activity A 的finish方法。

  • activity A的生命周期方法如下:

    onCreate -> onStart -> onResume -> onPause -> onSaveInstanceState -> onStop

  • activity B的生命周期方法如下:

    onCreate -> onStart -> onResume

接着点物理Back键

  • 会发现 activity B的生命周期方法执行如下

    onPause -> onStop -> onDestory

  • activity A的生命周期方法执行如下

    onRestart -> onStart -> onResume

此时发现onRestoreInstanceState方法也没有执行。

实验二

如果我们在activity A 屏上旋转屏幕,会发现其生命周期方法执行如下

onPause -> onSaveInstanceState -> onStop -> onDestroy -> onCreate -> onStart -> onRestoreInstanceState -> onResume

此处因为activity重新创建了,故onRestoreInstanceState方法执行了

[Android]如何判断某款Android设备是否被Root过

Root过的Android设备,可以让用户拥有最高的权限。判断设备是否被Root过也是根据这一点来做的。通过检测系统中的SU命令。
代码如下:

private static boolean isRooted() {
    return findBinary("su");
}

public static boolean findBinary(String binaryName) {
    boolean found = false;
    if (!found) {
        String[] places = {"/sbin/", "/system/bin/", "/system/xbin/", "/data/local/xbin/",
                "/data/local/bin/", "/system/sd/xbin/", "/system/bin/failsafe/", "/data/local/"};
        for (String where : places) {
            if ( new File( where + binaryName ).exists() ) {
                found = true;
                break;
            }
        }
    }
    return found;
}

代码摘自http://stackoverflow.com/questions/19288463/how-to-check-if-android-phone-is-rooted

[Android] HTTP缓存的使用

在做Web的时候,有这么一个体会。当我们第二次开一个网页的速度通常比第一次要快。举个例子,当我们用浏览器打开百度时,第一次打开百度,从浏览器开发者工具中我们可以看到(下面的图片太小了,点击才能看得清)
screen shot 2015-07-21 at 9 27 49 pm
对于很多资源,服务器端返回的响应状态码是200。以bd_logo1.png资源为例,我们看它的响应状态:
screen shot 2015-07-21 at 9 37 13 pm

但是,当我们再一次访问百度时(刷新页面), 我们发现此时大部份资源的状态码是304. 如下图:
screen shot 2015-07-21 at 9 39 37 pm

同样,看我们关注的那个bd_logo1.png资源,其状态
screen shot 2015-07-21 at 9 41 45 pm

比对上面的两次操作。对于第一次操作,浏览器在向服务器请求这个图片资源时,http请求返回的状态码是200,其http request的header中没有太多的信息,其http response的header中有了两个我们比较关心的属性。

ETag:"1ec5-502264e2ae4c0"
Last-Modified:Wed, 03 Sep 2014 10:00:27 GMT

在第二次请求时,其http request的header中添加了两个属性

If-Modified-Since:Wed, 03 Sep 2014 10:00:27 GMT
If-None-Match:"1ec5-502264e2ae4c0"

第二次的请求,http response的header中,有一个 ETag:"1ec5-502264e2ae4c0", 但是并没有Last-Modified属性,同时第二次的http请求返回状态码是304

当服务器返回的状态码是304时,则是告诉客户端,请求的资源没有任何变化,直接从本地缓存中取。

总结一下,对于第一次的http请求,服务器返回的状态码是200, 在返回我们需要的资源的同时会返回两个属性ETagLast-Modified。在第二次请求时,虽说请求的是同一个资源,但会在http请求头中加入两个属性If-Modified-SinceIf-None-Match。其中If-Modified-Since的值便是第一次请求资源时拿到的Last-Modified属性值,If-None-Match的值便是第一次请求资源时拿到的ETag值。当服务器拿到这两个属性后会再次和所请求的资源进行一个匹配,如果请求的资源这两个属性没有任何变化,便会返回一个304状态码给客户端,不会返回资源实体的内容,从而节省带宽,提高响应速度。

在Android程序开发中,偶尔我们也需要利用这个原理。比如,由于某个特定业务需求,我们程序偶尔需要从服务器上的某个路径下载一个文本文件,而这个文本文件中内容变化不是非常频繁,可能几个月变一次,此时我们便可以利用这个机制了。以下给出一段简单的代码:

import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import com.aug.summary.R;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class TestVolleyActivity extends Activity {
    private Button btnSend;
    private static String lastModified;
    private static String eTag;
    private static String date;
    private static final String TAG = "TestVolley";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_volley);
        intView();
    }


    private View.OnClickListener listener = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            new Thread() {
                public void run() {
                    try {
                        URL url = new URL("https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/static/protocol/https/global/img/icons_f0338722.png");
                        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                        if (lastModified != null) {
                            connection.addRequestProperty("If-Modified-Since", lastModified);
                        }
                        if(eTag != null) {
                            connection.addRequestProperty("If-None-Match", eTag);
                        }
                        String responseMessage = connection.getResponseMessage();
                        lastModified = connection.getHeaderField("Last-Modified");
                        eTag = connection.getHeaderField("ETag");
                        Log.i(TAG, "last-modified:" + lastModified);
                        Log.i(TAG, "eTag:" + eTag);
                        Log.i(TAG, connection.getResponseCode() + "  " + connection.getResponseMessage() + "  content length:" + connection.getContentLength());

                    } catch (MalformedURLException e) {
                        e.printStackTrace();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }.start();

        }
    };
    public void intView() {
        btnSend = (Button) findViewById(R.id.btn_send_http_by_volly);
        btnSend.setOnClickListener(listener);
    }

}

第一次点击按钮时Log如下

07-21 10:50:12.320 2075-2198/com.aug.summary I/TestVolley﹕ last-modified:Tue, 23 Jun 2015 08:24:13 GMT
07-21 10:50:12.320 2075-2198/com.aug.summary I/TestVolley﹕ eTag:"37a6-5192b1d838540"
07-21 10:50:12.320 2075-2198/com.aug.summary I/TestVolley﹕ 200 OK content length:14246

第二次点击按钮时Log如下

07-21 10:51:33.837 2075-2611/com.aug.summary I/TestVolley﹕ last-modified:Tue, 23 Jun 2015 08:24:13 GMT
07-21 10:51:33.837 2075-2611/com.aug.summary I/TestVolley﹕ eTag:"37a6-5192b1d838540"
07-21 10:51:33.837 2075-2611/com.aug.summary I/TestVolley﹕ 304 Not Modified content length:0

[Matlab][基本矩阵运算]

上周,某人突然在QQ上问我一个MATLAB的计算问题。当时也愣住了,都这么久了,快一年都没有用到,忘光了。晚上回到家中,打开电脑,全盘搜索才搜到之前的自己学习MATLAB时记下的笔记。为了避免再出现这种情况,决定把笔记搬到这里来。这一年都没有机会写MATLAB,想想还是蛮怀念当时用MATLAB做毕业设计的日子,各种滋味。不怀旧了,进正题。


注意>>后面代表的是输入的命令

列向量的声明 :在方括号内把数值用分号(;)隔开,对元素的个数没有限制 a=[2;1;4]
行向量的声明:采用空格或逗号(,)将数值隔开,例如: v=[2,0,4]
转置: 采用单引号(')代表转置操作,将列向量转为行向量的例子为:

>>a=[2;1;4];
>>y=a'
输出:y=
2 1 4
  • 向量与数相乘(向量乘法):
>>c=3;      
>>a=[1 2 3];
>>b=c*a
b=
3 6 9 
  • 向量的相加或相减:有时可能会使用两个向量进行相加或相减来创建第三个向量,要求两个向量之间必须类型相同,长度相同。
>>A=[1;4;5];
>>B=[2;3;3];
>>C=A+B
C=
    3
    7
    8
  • 从已存变量创建大向量,MATLAB允许把向量合并在一起创建新向量
    >>A=[1;4;5]
    >>B=[2;3;3]
    >>D=[A;B]
    D=
        1
        4
        5
        2
        3
        3
  • 创建等差元素向量,创建一个首元素为xi,末元素为xe的向量x的语法如下:
    x=[xi:q:xe]; 差值为q
>>x=[0:0.1:1];
>>y=x.^2;     %对向量x进行乘方操作
  • 对向量的乘方前面必须加句号(.),对于数字的乘方则可不必 x=2; y=x^3这都是没有问题的。

linspace命令:我们可以采用linespace命令创建行向量,这向量含有a到b之间间隔相等的n个元素。linespace(a,b) 创建了a,b之间含有100个等差元素的向量。而linspace(a,b,n)创建了a,b之间含有n个等差元素的向量。不管哪种形式,MATLAB自动确定元素之间的增量。
logspace(a,b,n)还允许创建n个对数值相隔相同的行向量。创建的是10的a次方和10的b次方之间n个对数值等差的向量。

    >>logspace(1,2,5)
    ans=
        10.000  17.7828 31.6228 56.2341 100.000

特征化向量:

  • 几个函数:length, max, min, sum, abs --绝对值,即在原位置返回向量中每个元素的绝对值。
  • 向量的数量积(点乘)--两个向量的对应坐标的乘积的和,使用数组乘法(.*)来完成,先计算相乘后的矩阵,再求和。或直接采用dot(a,b)命令计算。向量的数量积得出来的是一个数值。
    >>J=[0;3;4]
    >>b=J.*J   %注意此处的.与前面的J之间不要有空格
    >>sum(b) %得出结果是25
  • 向量的模等于各元素的平方相加后的平方根,注意区分。
  • 向量的向量积也就是叉乘:
    |a1 b1 c1| 
    |a2 b2 c2| 
    =(b1c2-b2c1,c1a2-a1c2,a1b2-a2b1) 
  • 引用向量中的一个或多个元素。向量v的第i个元素可以用v(i)来引用。
    >>A=[12;17;-2;0;4]
    >>A(2)
    ans=
        17
  • 直接有冒号--如(:)来引用向量,等于告诉MATLAB列出向量的所有元素。
    >>A(:)
    ans=
        12
        17
        -2
        0
        4
     选出向量中某一范围的元素
     >>v=A(2:4)
     v=
            17 
            -2
            0

矩阵基本操作

  • 创建矩阵:
    `

    A=[-1,6;7,11]
    `

  • 矩阵相加:如果两个矩阵行数和列数都相等,那么它们可以进行相加相减
    >>A=[5 1;0 9];
    >>B=[2,-2;1 1];
    >>A+B
  • 对于矩阵可以使用数组乘法,使用与向量相乘相同的符号(.*),注意这不是矩阵乘法。
    >>C=A.*B
    C=
        10 -2
        0  9
    A.*B操作实际上是进行元素与元素相乘。
  • 矩阵相乘:>> A*B 要求A是一个m_p矩阵,而B是p_n矩阵,相乘后产生m*n矩阵。所以即使A,B能进行矩阵相乘但不一定能进行数组相乘,数组相乘要求行数,列数都必须相匹配。
  • 数与矩阵相加:
        >> A = [1 2 3 4]; 
        >> b = 2; 
        >> C = b + A 
        C = 
            3     4     5     6 
  • 对数组进行左除与右除,要求两数组结构相同,数组中的元素对应相除.
    • 右除:
    >>A=[2 4 6 8];B=[2 2 3 1];
    >>C=A ./B
    C=
        1 2 2 8
* 左除:
    C=A .\B(此时与C=B./A相同).
  • 矩阵的乘方:
    >>B=[2 4;-1 6]
    >>B .^2
    ans=
        4 16
        1 36

特殊类型矩阵:

  • 单元矩阵是一个对角线为非零元素其它元素为零的方形矩阵。要创建n*n的单元矩阵, 采用eye(n)命令:
    >>eye(4)
    ans = 
     1     0     0     0 
     0     1     0     0 
     0     0     1     0 
     0     0     0     1 

zero():要创建n_n的零矩阵,输入zero(n)创建m_n的矩阵,输入zero(m,n),
ones():创建元素都为1的矩阵。

  • 引用矩阵元素:
    >> A = [1 2 3; 4 5 6; 7 8 9] 
    用A(m,n)选出第m行n列的元素
    >>A(2,3)
    ans=
        6
    要引用第i列的所有元素,输入A(:,i).
    >>A(:,2)
    ans=
            2
            5
            8
    要引用第二行输入A(2,:)
    要选出从第i列到第j列之间的所有元素,输入A(:,i:j)同样返回第i行到第j行之间的所有元素输入A(i:j,:);
    选出更具体的,例如选出第二到第三行同时处于第一和第二列的元素,写成:
    >>A(2:3,1:2)
    ans =
        4       5 
        7       8
    也可以通过这样来改变元素的值。
    >>A(1,1)=-8 %将第一行第一列的元素值改为-8.
    删除A的第二行:
    >>A(2,:)=[]

行列式与线性系统求解

  • 计算行列式的值 det(A)
    >>A=[1 3;4 5];
    >>det(A)
    ans=
        -7
  • 用MATLAB解线性方程:首先计算行列式的值,如果行列式不为零,那么解存在。解是列向量:
    例如:
    5x + 2y - 9z = -18 
    -9x - 2y + 2z = -7 
    6x + 7y + 3z = 29 

     A=[5 2 -9;-9 -2 2;6 7 3]
     B=[-18;-7;29]
     det(A)得出437不为零故解存在。
     >>A \ b     %为什么要这样做,为什么?
     ans =
            1.000
            2.000
            3.000
  • 求矩阵的秩:矩阵的秩是矩阵行或列的数值线性独立的度量。如果一个向量线性独立于另外一些向量组,那意味着这一个向量不能写成它们的线性组合。
    • 求矩阵的秩使用Rank()函数。对于带有 n 个未知量的 m个线性系统方程:
      Ax = b
      把 b 连结A上构成了增广矩阵:
      [A b]
      当且仅当rank(A) = rank(A b)时系统有解。如果秩等于 n,那么系统有唯一解,但如果秩小于 n,那么系统有无数解。如果我们用 r 来表示秩,那么未知量的 r 就可以表示成其它 变量的 n-r 的线性组合。
  • 求逆矩阵与伪逆矩阵: inv(A)求矩阵A的可逆矩阵。
    矩阵A有逆矩阵的充分必要条件是|A|不等于0;如果det(A)=0,那么逆矩阵不存在,我们说此矩阵是一个奇异矩阵。
    用逆矩阵解线性方程组:
    3x-2y=5
    6x-2y=2

    1.A=[3 -2;6 -2] b=[5;2]
    2.计算det(A)得出值为6,因此逆矩阵是存在的
    3.求方程组的解
    >> x=inv(A)*b
* 如果方程组中方程的个数比未知数要少,此时方程组称为欠定,这意味着方程组有无限解,只时只有一些未知数能够确定下来,不能确定的未知数可以赋任意值。这时求解方程可借助于---伪逆矩阵 即"广义增广矩阵"
  • 简化梯形矩阵
    rref(A)的函数使用Gauss-Jordan消元法产生矩阵A降行后的梯形形式。
    >>A=[1 2;4 7];
    >>rref(A)
    ans=
        1   0
        0   1
  • 魔方矩阵(幻方)是一个n*n矩阵,矩阵的元素从1到n的平方 之间,并且行元素的和等于列元素的和。
  • 矩阵的分解: MATLAB可以快速对矩阵进行LU、QR或SVD分解

[PHP]MongoDB与Yii搭建API服务-1

[PHP]MongoDB与Yii搭建API服务-1

在上一个项目中,我们采用MongoDB和Yii框架来为手机端提供API,那是我第一次学习并采用Yii来开发项目,确实深刻的体会到Yii的便捷和高效。代码写起来感觉非常的清晰,今天来对上一个项目里面的东西进行总结一下。主要是总结MongoDB和Yii的操作,当然也会写一些边边角角的东西。当前是采用Yii 1.1, MongoDB的扩展是1.3.6

如何整合

  1. 首先需在配置php对mongodb的扩展,此处就不说了。然后我们需要mongodb对yii的扩展,将相关代码拷到Yii应用的 protected/extensions目录下。
  2. 在Yii应用的config/main.php中引入mongodb的扩展并配置连接参数,如下:
    'import'=>array(
        'application.models.*',
        'application.components.*',
        'ext.mongo.*',
    ),
    'components' => array(
      ...
         'mongodb'=>array(
             'class' => 'EMongoDB' ,
             'connectionString' => 'mongodb://irunning:abc123_@localhost:27017/irunning',
             'dbName' => 'irunning',
             'fsyncFlag' => true,
             'safeFlag'=>true,
             'useCursor'=>false,
         ),
   )

到此,我们的配置便完成了,接着就可以开始使用Yii和MongoDB

Yii操作MongoDB

Model定义

我们先定义一个BaseModel, 在这个Model中定义部分公共的属性。如下:

<?php
class BaseModel extends EMongoDocument
{
    public $_id;
    public $created_time;
    public $updated_time;
    public $deleted;
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    public function getCollectionName()
    {
        return null;
    }
    public function rules()
    {
        return array(
            array('_id', 'default', 'value' => new MongoId()),
            array('deleted', 'default', 'value' => Constant::UNDELETE),
            array('created_time', 'default', 'value' => new MongoDate()),
            array('updated_time', 'default', 'value' => new MongoDate()),
        );
    }
    public function attributeLabels()
    {
    }
}

然后,我们来定义一个实际会使用的Model--> User.

class User extends BaseModel {
    public $name;
    public $password;
    public $nickname;
    public $email;
    public $height;
    public $weight;
    public $gender;
    public $register_time;
    public $last_login_time;

    public static function model($className=__CLASS__) {
        return parent::model($className);
    }


    /**
     * @return string the associated collection name
     */
    public function getCollectionName() {
        return 'user';
    }

    public function rules() {
        return  array_merge(parent::rules(), array(
            ...
        ));
    }
}

定义好了Model之后,我们可以像操作其它AR一样来操作了。

对MongoDB的操作

对MongoDB的操作是通过Model来完成的。在EmongoDocument中定义了不少操作DB的方法,如:

  • findByPk($userId, $criteria)
  • save
  • update
  • delete
  • ......
$user = new User();
$user->_id = new MongoId();
$user->name = "user1";
......
$user->save();
EmongoCriteria的使用

EmongoCriteria是用来封装条件的,在我们进行CRUD操作时都会使用到,需要掌握好。其使用方式如下:
第一种方式是使用array来指定查询条件:

//指定查询条件
$condition = array(
    'name'=>$username,
);
//new 一个EmongoCriteria实例
$criteria = new EmongoCriteria();
//将查询条件设定到criteria中。
$criteria->setConditions($condition);
//用model执行find方法。
$user = User::model->find($criteria);

另一种方式是直接对EmongoCriteria添加条件,如下:

$criteria = new EmongoCriteria();
$criteria->select(array('name', 'age', 'password')); //指定查询的列
$criteria->addCond('name', '==', $username);
//$criteria->name = $username;当然也可以这么写,和上一行的效果是一样的
//条件是可以不断追加的
$criteria->addCond('deleted', '==', 0);
$criteria->addCond('start_time', '>=', $startTime);
$criteria->addCond('start_time', '<', $endTime);

//用model执行find方法。
$user = User::model->find($criteria);

1.多个条件的组合。例如我要查询用户名是'user1',age是18的用户,只需要将上面的$condition稍做修改即可:

$condition = array(
    'name'=>'user1',
    'age'=> 18
);

2.如果是表示的关系

$condition = array(
    '$or' => array(
        'age' => 18,
        'age' => 28
    )
)

3.分页和排序

分页和排序的操作也是非常的简单,只要在$criteria对象上接着添加方法便好

$criteria->setLimit($pageSize);//每页多少条数目
$criteria->setOffset($startIndex); //起始位置
$criteria->setSort(array('start_time'=>EMongoCriteria::SORT_DESC));

如果是采用array来指定分页和排序的参数,要注意array的层次结构。以下使用一个官方的例子

$array = array(
    'conditions'=>array(
        // field name => operator definition
        'FieldName1'=>array('greaterEq' => 10), // Or 'FieldName1'=>array('>=', 10)
        'FieldName2'=>array('in' => array(1, 2, 3)),
        'FieldName3'=>array('exists'),
    ),
    'limit'=>10,
    'offset'=>25,
    'sort'=>array('fieldName1'=>EMongoCriteria::SORT_ASC,
                  'fieldName4'=>EMongoCriteria::SORT_DESC),
);
$criteria = new EMongoCriteria($array);
$user = User::model()->find($criteria);

4.统计数量User::model()->count($criteria)

MongoDBRef的使用

MongDBRef是用来表示两个表之间字段的关联关系。
假设有这么一个场景,员工和部门之间是一对多的关系。我们有一个Employeemodel和一个Deptmodel,在Employeemodel中会有一个$dept的属性,这个$dept属性并不是一个Dept类型的实例,它是一个MongoDBRef类型,有点类似于关系数据库中的外键。或以把它理解成是一个dept_id指向的是Dept表中的的_id。于是有以下代码:

class Employee extends BaseModel {
    public $user_id;
    public $name;
    public $email;
    public $dept;

    //于是添加一条中奖记录的代码如下:
    public function addEmployee($userId, $name, $email, $deptId) {
        $employee = new Employee();
        $employee->user_id = $userId;
        $employee->name= $name;
        $employee->email = $email;
        $employee->dept = MongoDBRef::Create('dept', $deptId); //dept是Dept这个Model对应的collection.
        //给个时间
        $currentTime = gettimeofday();
        $userAward->created_time = new MongoDate($currentTime['sec'], $currentTime['usec']);
        $userAchievement->save()
     }
    public function getEmployee($name) {
        $criteria = new EMongoCriteria();
        $criteria->addCond('name', '==', $name);
        $employee = $this->findAll($criteria);
        foreach ($employee as $value) {
            $deptObj = MongoDBRef::get($value->db, $value->dept);
            $item['name'] = $deptObj['name'];
            $item['description'] = $deptObj['description'];
            .....
        }
    }     
}

[Android][CustomView-advance]

在Android中每一个View的渲染都分为两个阶段。即布局阶段绘画阶段。分别对应的有onMeasureonDraw方法需要我们去重写。本文的内容是在学习Expert Android这本书时所做的笔记,以自定义一个Circle为例,要求每次点击Circle的时候,Circle会增大一倍,同时能够实现状态的保存。老实说,我确实没怎么看懂,先记录一下吧,以后再琢磨琢磨。先上完整的代码:

public class MyCicleView extends View implements OnClickListener {
    public static String tag = "CircleView";

    private int defRadius = 20;
    private int strokeColor = 0xFFFF8C00;
    private int strokeWidth = 10;

    public MyCicleView(Context context) {
        super(context);
        initCircleView();
    }

    public MyCicleView(Context context, int inStrokeWidth, int inStrokeColor) {
        super(context);
        strokeColor = inStrokeColor;
        strokeWidth = inStrokeWidth;
        initCircleView();
    }

    public MyCicleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyCicleView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        TypedArray t = context.obtainStyledAttributes(attrs,
                R.styleable.MyCircleView, defStyle, 0);
        strokeColor = t.getColor(R.styleable.MyCircleView_strokeColor,
                strokeColor);
        strokeWidth = t.getInt(R.styleable.MyCircleView_strokeWidth,
                strokeWidth);
        t.recycle();
        initCircleView();
    }

    public void initCircleView() {
        this.setMinimumHeight(defRadius * 2);
        this.setMinimumWidth(defRadius * 2);
        this.setOnClickListener(this);
        this.setClickable(true);
        this.setSaveEnabled(true);
    }

    @Override
    public void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        Log.d(tag, "onDraw called");

        int w = this.getWidth();
        int h = this.getHeight();
        int t = this.getTop();
        int l = this.getLeft();

        int ox = w / 2;
        int oy = h / 2;
        int rad = Math.min(ox, oy) / 2;
        canvas.drawCircle(ox, oy, rad, getBrush());
    }

    private Paint getBrush() {
        Paint p = new Paint();
        p.setAntiAlias(true);
        p.setStrokeWidth(strokeWidth);
        p.setColor(strokeColor);
        p.setStyle(Paint.Style.STROKE);
        return p;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        logSpec(MeasureSpec.getMode(widthMeasureSpec));
        Log.d(tag, "size:" + MeasureSpec.getSize(widthMeasureSpec));

        setMeasuredDimension(getImprovedDefaultWidth(widthMeasureSpec),
                getImprovedDefaultHeight(heightMeasureSpec));
    }

    private void logSpec(int specMode) {
        if (specMode == MeasureSpec.UNSPECIFIED) {
            Log.d(tag, "mdoe: unspecified");
            return;
        }
        if (specMode == MeasureSpec.AT_MOST) {
            Log.d(tag, "mdoe: at msot");
            return;
        }
        if (specMode == MeasureSpec.EXACTLY) {
            Log.d(tag, "mdoe: exact");
            return;
        }
    }

    private int getImprovedDefaultHeight(int measureSpec) {
        // int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            return hGetMaximumHeight();
        case MeasureSpec.EXACTLY:
            return specSize;
        case MeasureSpec.AT_MOST:
            return hGetMinimumHeight();
        }
        Log.e(tag, "unknown specmode");
        return specSize;
    }

    private int getImprovedDefaultWidth(int measureSpec) {
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            return hGetMaximumWidth();
        case MeasureSpec.EXACTLY:
            return specSize;
        case MeasureSpec.AT_MOST:
            return hGetMinimumWidth();
        }
        Log.e(tag, "unknown specmode");
        return specSize;
    }

    protected int hGetMinimumHeight() {
        return this.getSuggestedMinimumHeight();
    }

    protected int hGetMinimumWidth() {
        return this.getSuggestedMinimumWidth();
    }

    protected int hGetMaximumHeight() {
        return defRadius * 2;
    }

    protected int hGetMaximumWidth() {
        return defRadius * 2;
    }

    public void onClick(View v) {
        // increase the radius
        defRadius *= 1.2;
        adjustMinimumHeight();
        requestLayout();
        invalidate();
    }

    private void adjustMinimumHeight() {
        this.setMinimumHeight(defRadius * 2);
        this.setMinimumWidth(defRadius * 2);
    }

    @Override
    protected void onRestoreInstanceState(Parcelable p) {
        this.onRestoreInstanceStateStandard(p);
        this.initCircleView();
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        return this.onSaveInstanceStateStandard();
    }

    private void onRestoreInstanceStateStandard(Parcelable state) {
        if (!(state instanceof SavedState)) {
            super.onRestoreInstanceState(state);
            return;
        }
        // it is our state
        SavedState ss = (SavedState) state;
        // Peel it and give the child to the super class
        super.onRestoreInstanceState(ss.getSuperState());

        defRadius = ss.defRadius;
    }

    private Parcelable onSaveInstanceStateStandard() {
        Parcelable superState = super.onSaveInstanceState();
        SavedState ss = new SavedState(superState);
        ss.defRadius = this.defRadius;
        return ss;
    }

    public static class SavedState extends BaseSavedState {
        int defRadius;

        SavedState(Parcelable superState) {
            super(superState);
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            super.writeToParcel(out, flags);
            out.writeInt(defRadius);
        }

        private SavedState(Parcel in) {
            super(in);
            defRadius = in.readInt();
        }

        @Override
        public String toString() {
            return "CircleView defRadius:" + defRadius;
        }

        @SuppressWarnings("hiding")
        public static final Parcelable.Creator<SavedState> CREATOR = new Parcelable.Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
}

布局阶段

布局阶段会经历两个过程,即MeasureLayoutMeasure阶段是获得View的Size,即HeightWidth。在此阶段我们需要覆盖onMeasure方法。讲到Measure不得不提的是View.MeasureSpec的三个mode. 此处摘自Android 官方文档View.MeasureSpec.

  • UNSPECIFIED
    • The parent has not imposed any constraint on the child. It can be whatever size it wants. 父视图不对子视图有任何约束,它可以达到所期望的任意尺寸。比如ListView、ScrollView。
  • EXACTLY
    • The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be。
      当我们将控件的layout_width或layout_height指定为具体数值时如andorid:layout_width="50dp",或者为match_parent时,都是控件大小已经确定的情况,都是精确尺寸。
  • AT_MOST
    • The child can be as large as it wants up to the specified size. 当控件的layout_width或layout_height指定为WRAP_CONTENT时,控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。
  • Measure阶段,View尺寸的计算是由measure(int,int)方法来决定的,通过查API文档会发现此方法是一个final的方法,不能被覆盖。为此,如果说要自定义View的尺寸,我们可以采用覆盖onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法。在Measure阶段结束之前,每一个View都将会存储自身的尺寸,当然每个View的尺寸会受到父View的约束。
  • 实例化layout,调用requestLayout方法。当View确定自身已经不适合当前区域时会自己调用这个方法。

关于Android中View的绘制,有一篇不错的文章:View 绘制流程

绘画阶段

绘画阶段,我们需要重写onDraw方法。在绘画阶段,我们会用到两个对象CanvasPaint, Canvas决定画出来的View的形状,Paint决定字体颜色样式等。

  • 强制一个View进行draw行为时,调用invalidate()方法。

对事件的响应

  • 当一个事件发生,并传递给View了,这个View将会接收这个事件,并把它交给相应的Listener进行处理。
  • 如果这个事件可能会造成View边界(尺寸)的改变,View将会调用其requestLayout()方法。
  • 同样,如果事件可能造成View外观的改变,View将会调用其invalidate()方法。
  • requestLayout()invalidate()方法任何一个被调用了,Android框架将会执行measuring,layout,drawing整个视图树。

对于事件的响应,该类实现View.OnClickListener接口,实现onClick(View v)方法。在onClick(View v)方法中首先完成特定的需求,然后调用requestLayout()invalidate()方法,完成图形的重新渲染。

状态的保存

对于Android中的View,在默认情况下是会自动保存其状态,这也就是为什么当我们在一个文本输入框中输入文字,然后按HOME键,返回到桌面,再进入程序时。可以看到之前输入的文字还在。但Android自动保存状态是有一个前提的,即在Layout文件中必须要指定ViewID。在本类中,作者采用的是一种内部类的实现方式,在自定义View的内部包含一个用来维护状态的内部类(其实就是保存半径的信息)。

[Android][Performance]SparseArray和HashMap

缘起: 在做Android开发时,如果在代码中使用了

HashMap<Integer, Object> map = new HashMap<Integer, Object>();

Lint工具来检查时,Lint便会报出一个警告,要我们采用SparseArray来代替HashMap用来提高性能。这个警告会在HashMapKeyInteger类型时出现。
相比HashMap,采用SparseArray可以从两个方面来提高程序的性能。第一,HashMap的键采用的是Integer类型,而SparseArray的键采用的是int 类型,不需要Auto Boxing。第二,SparseArray中存储元素是采用两个数组,一个用来存Key,另一个用来存Value。而HashMap中采用的是了一个Entry来存储。在存储时,会先根据元素的hash值来得到其存放的位置下标。因此HashMap中分配的空间肯定比HashMap中实际元素所需要的空间多。从这一点来看,SparseArrayHashMap更少空间。

读了一下SparseArray的源代码,其实现也非常的简洁。在使用时,通常就直接new一个SparseArray。默认会分配10个元素的空间。如下:

    public SparseArray() {
        this(10);
    }
    public SparseArray(int initialCapacity) {
        if (initialCapacity == 0) {
            mKeys = ContainerHelpers.EMPTY_INTS;
            mValues = ContainerHelpers.EMPTY_OBJECTS;
        } else {
            initialCapacity = ArrayUtils.idealIntArraySize(initialCapacity);
            mKeys = new int[initialCapacity];
            mValues = new Object[initialCapacity];
        }
        mSize = 0;
    }

采用put(int key, E value)方法向里面添加新的键值对。如果键值已经存在,那么该键值对应的value将会被新的value所替换。我们来分析一下SparseArray中的put(int key, E value)方法的代码:

    public void put(int key, E value) {
        //二分法查找当前key是否存在。
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
        //如果当前key已经存在,那就替换当前key所对应的value.
        if (i >= 0) {
            mValues[i] = value;
        } else {
            i = ~i;
            //如果是新添加的元素,并且下标 i 所对应 value类型是DELETED(该处元素应该已删除)
            if (i < mSize && mValues[i] == DELETED) {
                mKeys[i] = key;
                mValues[i] = value;
                return;
            }
           //如果先前有delete操作,并且没有执行gc(),则调用gc()
            if (mGarbage && mSize >= mKeys.length) {
                gc();

                // Search again because indices may have changed.
                i = ~ContainerHelpers.binarySearch(mKeys, mSize, key);
            }
            // 对数组进行扩容
            if (mSize >= mKeys.length) {
                int n = ArrayUtils.idealIntArraySize(mSize + 1);

                int[] nkeys = new int[n];
                Object[] nvalues = new Object[n];

                // Log.e("SparseArray", "grow " + mKeys.length + " to " + n);
                System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length);
                System.arraycopy(mValues, 0, nvalues, 0, mValues.length);

                mKeys = nkeys;
                mValues = nvalues;
            }

            if (mSize - i != 0) {
                // Log.e("SparseArray", "move " + (mSize - i));
                System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i);
                System.arraycopy(mValues, i, mValues, i + 1, mSize - i);
            }

            mKeys[i] = key;
            mValues[i] = value;
            mSize++;
        }
    }
  • SparseArray中值得注意一点的是它的gc()方法。对于已经标记为DELETE的元素,gc()在运行时,便会释放它所占据的空间
    private void gc() {
        // Log.e("SparseArray", "gc start with " + mSize);
        int n = mSize;
        int o = 0;
        int[] keys = mKeys;
        Object[] values = mValues;
        for (int i = 0; i < n; i++) {
            Object val = values[i];
            if (val != DELETED) {
                if (i != o) {
                    keys[o] = keys[i];
                    values[o] = val;
                    values[i] = null;
                }
                o++;
            }
        }
        mGarbage = false;
        mSize = o;
        // Log.e("SparseArray", "gc end with " + mSize);
    }
  • SparseArray的很多方法中都会有调gc()方法的。如:
    public int size() {
        if (mGarbage) {
            gc();
        }

        return mSize;
    }

    public int keyAt(int index) {
        if (mGarbage) {
            gc();
        }

        return mKeys[index];
    }
  • 在调用gc()之前会先判断mGarbage是否为true, 如果mGarbagetrue,gc()方法将会被调用。mGarbage的值设置为true发生在delete(int key)removeAt(int index)方法中。如下:
    public void delete(int key) {
        int i = ContainerHelpers.binarySearch(mKeys, mSize, key);

        if (i >= 0) {
            if (mValues[i] != DELETED) {
                mValues[i] = DELETED;
                mGarbage = true;
            }
        }
    }

    public void removeAt(int index) {
        if (mValues[index] != DELETED) {
            mValues[index] = DELETED;
            mGarbage = true;
        }
    }
  • SparseArray针对的是键为int, 值是object这种类型的数据结构。此外,还有SparseBooleanArray, LongSparseArray分别针对键为boolean,long,值为object的数据结构。
  • 由于SparseArray是的底层是采用数组实现的,所以数组的一些缺点(You Know),它也无法避免。
  • 关于HashMap的源码解析,我觉得可以参看csdn上的这篇文章

[Design-Pattern]-TemplateMethod

模板方法:模板方法是在一个方法中定义一个算法的骨架,这个骨架中中可能会有多个算法,将骨架中部分算法的实现交给子类,模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤(很多时候用hook来实现,后面会讲到)。
示例: 一个简单的例子,泡茶和泡咖啡的操作都是一样的(1)烧水 (2)加原料tea/coffee (3)根据需要是否加糖。[就这么多,不搞复杂]由于操作流程一样,刚三步操作就是一个模板。将以上三个步骤抽取到一个方法中,此方法便是模板方法。将模板方法所在的类定义为抽象类。

  • 模板方法类:
abstract class TemplatePatternClass {
    final void prepare() { //这便是一个模板方法,定义成final,不让子类覆盖
        boilWater();
        addIngredient();
        if(needSugar()) {
            addSugar();
        }
    }
    public void boilWater() {
        System.out.println("------boil water ----");
    }
    public void addSugar() {
        System.out.println("------add Sugar ----");
    }
    abstract void addIngredient();//此方法的实现由子类完成。
    boolean needSugar() { //needSugar是一个hook方法,子类可以通过override它来修改模板方法的逻辑
        return false;
    }
}
  • 实现类1
public class Coffee extends TemplatePatternClass {

    @Override
    void addIngredient() {
       System.out.println("----------add coffee--------");
    }

    @Override
    boolean needSugar(){
        return true;
    }
}
  • 实现类2
public class Tea extends TemplatePatternClass {

    @Override
    void addIngredient() {
        System.out.println("----------add tea-----------");
    }
    @Override
    boolean needSugar(){ //Tea中不需要加糖,所以此处override needSugar这个hook方法,这将改变模板方法中的部分逻辑
        return false;
    }
}

  • 调用类
public class TestTemplatePattern {
    public static void main(String args[]) {
        Tea tea = new Tea();
        tea.prepare();
        System.out.println("****************************************");
        Coffee coffee = new Coffee();
        coffee.prepare();
    }
}
  • 执行结果
------boil water ----
----------add tea-----------
****************************************
------boil water ----
----------add coffee--------
------add Sugar ----


模板方法在jdbc操作中的使用

通常我们操作jdbc 从数据库中取数据,代码会这样写

public List<User> getUserByDept(int deptId) {
    Connection conn = null;
    PreparedStatement stmt = null;
    ResultSet rs = null;
    String sql = "SELECT id, name, dept_id FROM user WHERE dept_id = ? "; 
    List<User> userList = new ArrayList<User>();
    try {
        conn = JDBCUtil.getConnection(); //JDBCUtil是自己写的一个工具类用来连db和释放资源
        stmt = conn.prepareStatement(sql);
        stmt.setInt(1, deptId);
        rs = stmt.executeQuery();
        while (rs.next()) {
            User user = new User();
            user.setId(rs.getInt("id"));
            user.setName(rs.getString("name"));
            user.setDeptId(rs.getInt("dept_id"));
            userList.add(user);
        }
    } catch(SQLException e) {
        //Log to file.
    } finally {
        JDBCUtil.release(rs, stmt, conn);
    }
    return userList;
}

以上代码是根据某一个条件查询User,如果接着有一个需求,需要去根据出版社查Book, 我们的代码写法肯定也一样,都是

    1. 先获取Connection,
    1. 再获取Statement,
    1. 利用Statement设置查询条件,
    1. 执行查询
    1. 处理结果集(提取数据封装成对象)
    1. 关闭资源
      每一次查询的步骤都是这个样子,这已经是一个固定的模式了。很显然,此处便应该用模板方法这一设计模式。针对不同的查询,只有第3步(设置查询条件)和第5步(处理结果集)不一样。于是我们定义一个接口,接口中定义两个方法用来处理第3步和第5步的操作。

于是写出了如下代码:
回调接口

public interface JDBCCallBack<T> {
        //将结果集转成对象
    public T rsToObject(ResultSet rs) throws SQLException;
       //设置查询参数
    public void setParameter(PreparedStatement stmt) throws SQLException;
}

模板方法

public class JDBCTemplate<T> {
    public List<T>  query(String sql, JDBCCallBack <T>callBack) {
        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;
        List<T> data = new ArrayList<T>();
        try {
            conn = JDBCUtil.getConnection();
            stmt = conn.prepareStatement(sql);
            callBack.setParameter(stmt);
            rs = stmt.executeQuery();
            while (rs.next()) {
                T obj =  callBack.rsToObject(rs);
                data.add(obj);
            }
        } catch(SQLException e) {
            //Log to file.
        } finally {
            JDBCUtil.release(rs, stmt, conn);
        }
        return data;
    }
}

我们把公共部分都抽取到JDBCTemplate类中的query 方法中。

于是,原来的查询User的方法便可以简写成下面这个样子

public List<User> getUserByDept(final int deptId) {
    String sql = "SELECT id, name, dept_id FROM user WHERE dept_id = ? "; 
    return new JDBCTemplate<User>().query(sql, new JDBCCallBack<User>() {
        @Override
        public User rsToObject(ResultSet rs) throws SQLException {
            User user = new User();
            user.setId(rs.getInt("id"));
            user.setName(rs.getString("name"));
            user.setDeptId(deptId);
            return user;
        }
        @Override
        public void setParameter(PreparedStatement stmt) throws SQLException{
            stmt.setInt(1, deptId);
        }           
    });
}

[Android]一个关于子线程中刷UI的问题

有如下一段代码,定义了一个很普通的Activity,然后在其onCreate()方法中启了一个子线程去刷UI.

一段代码

Activity

public class TestUIThreadActivity extends Activity {
    private TextView tvTest;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_ui_thread);
        tvTest = (TextView)findViewById(R.id.tv_test);
        new ThreadOne().start();
    }
    private class ThreadOne extends Thread {
        @Override
        public void run() {
            tvTest.setText(“ABC");
        }
    }

}

很普通的布局文件

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:my="http://schemas.android.com/apk/res/com.example.test"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
        android:id="@+id/tv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text=“AAA" />

</RelativeLayout>

程序运行的结果是ABC显示出来了,虽然是在子线程中刷的UI,但是android系统没有报任何异常信息。如果将子线程的启动放在onResume()方法中,结果也是一样。

但是如果我们把程序这样改一下,在程序中添加一个Button,点击Button的时候才会去刷新UI,更改文字的内容,结果又会是什么呢?

第二段代码

Activity中

public class TestUIThreadActivity extends Activity {
    private TextView tvTest;
    private Button btnTest;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_ui_thread);
        tvTest = (TextView)findViewById(R.id.tv_test);
        btnTest = (Button)findViewById(R.id.btn_test);
        btnTest.setOnClickListener(listener);

    }
    private View.OnClickListener listener = new View.OnClickListener() {

        @Override
        public void onClick(View view) {
            new ThreadOne().start();
        }
    };

    private class ThreadOne extends Thread {
        @Override
        public void run() {
            tvTest.setText("bbb");
        }
    }
}

布局文件中

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:my="http://schemas.android.com/apk/res/com.example.test"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
        android:id="@+id/tv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="AAA" />
    <Button
        android:id="@+id/btn_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/tv_test"
        android:text="Click Me"/>
</RelativeLayout>

运行时发现有异常抛出:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

我们期待的在子线程中不能刷UI的异常出现了。 其实如果在第一段代码中,让子线程延迟5秒这个样子再去刷UI,同样也会报这个异常。那么为什么第一段代码中直接刷UI不会出现异常呢?我们很容易想到,当我们直接在onCreate方法中调用子线程刷UI时,此时UI层都还没有展示出来。所有UI的更新,都会调用到相应组件的invalidate()方法,最后会调用到ViewRootImpl中的invalidateChildInParent方法,在这个方法中会调checkThread()。直接执行时,是因为ViewRootImpl还没有创建出来。ViewRootImpl的创建是在onResume方法完成。

[Java] 浮点型数据精度丢失问题

最近在项目中碰到这么一个问题:
有这么一段代码:

 int num = (int) (Float.parse(str) * 100).

想像一下,如果str是"0.53",最终的输出结果会是多少? 53,非也,运行一看52.
原因很简单,当我们在利用浮点型数据进行运算时,很容易造成精度丢失。
再看一下这两行代码:

  System.out.println(0.53f);
  System.out.println(0.53f * 100);

第一行中,由于0.53f没有参与运算,最终打印出来的结果仍是0.53, 第二行是将0.53f和整型的100进行运算,最终输出的结果是52.999996

如果说有代码像这个样子:

float f = 0.01f;
double d = f;
Log.i(TAG, "d:" + d);

大家可能会猜到,输出的d值可能不是0.01, 实际上通常是0.009999xxxx,这是一个类型转换导致的。但上面的两个例子可能会隐蔽一些。在写代码的时候,对于这种问题一定要提高警惕。

浮点型数据运算过程中精度丢失这个问题并不仅仅存在于java语言中,在其它的语言中也是广泛存在的。浮点十进制值通常没有完全相同的二进制表示形式。 这是 CPU 所采用的浮点数据表示形式的副作用。 为此,可能会经历一些精度丢失,并且一些浮点运算可能会产生意外的结果。

在我们的项目中,货币的计算,精确到小数点后面两位。向服务器端提交数据的时候,提交的是美分,将金额转为整型再提交上去,因此会有一个乘以100的操作,写一段程序统计了一下两位小数乘以100后的结果。
代码如下:

    public class TestFloat {
        public static void main(String args[]) {
            float i = 0.01f;
            int j = 0;
            while (i < 1.0f) {            
                j = j + 1;
                i = j / 100.0f;
                System.out.println(j + "   " +  i + "   " + i*100);
            }
        }
    }

输出结果如下:

    1   0.01   1.0
    2   0.02   2.0
    3   0.03   3.0
    4   0.04   4.0
    5   0.05   5.0
    6   0.06   6.0
    7   0.07   7.0
    8   0.08   8.0
    9   0.09   9.0
    10   0.1   10.0
    11   0.11   11.0
    12   0.12   12.0
    13   0.13   13.0
    14   0.14   14.0
    15   0.15   15.000001
    16   0.16   16.0
    17   0.17   17.0
    18   0.18   18.0
    19   0.19   19.0
    20   0.2   20.0
    21   0.21   21.0
    22   0.22   22.0
    23   0.23   23.0
    24   0.24   24.0
    25   0.25   25.0
    26   0.26   26.0
    27   0.27   27.000002
    28   0.28   28.0
    29   0.29   29.0
    30   0.3   30.000002
    31   0.31   31.0
    32   0.32   32.0
    33   0.33   33.0
    34   0.34   34.0
    35   0.35   35.0
    36   0.36   36.0
    37   0.37   37.0
    38   0.38   38.0
    39   0.39   39.0
    40   0.4   40.0
    41   0.41   41.0
    42   0.42   42.0
    43   0.43   43.0
    44   0.44   44.0
    45   0.45   45.0
    46   0.46   46.0
    47   0.47   47.0
    48   0.48   48.0
    49   0.49   49.0
    50   0.5   50.0
    51   0.51   51.0
    52   0.52   52.0
    53   0.53   52.999996
    54   0.54   54.000004
    55   0.55   55.0
    56   0.56   56.0
    57   0.57   57.0
    58   0.58   58.0
    59   0.59   58.999996
    60   0.6   60.000004
    61   0.61   61.0
    62   0.62   62.0
    63   0.63   63.0
    64   0.64   64.0
    65   0.65   65.0
    66   0.66   66.0
    67   0.67   67.0
    68   0.68   68.0
    69   0.69   69.0
    70   0.7   70.0
    71   0.71   71.0
    72   0.72   72.0
    73   0.73   73.0
    74   0.74   74.0
    75   0.75   75.0
    76   0.76   76.0
    77   0.77   77.0
    78   0.78   78.0
    79   0.79   79.0
    80   0.8   80.0
    81   0.81   81.0
    82   0.82   82.0
    83   0.83   83.0
    84   0.84   84.0
    85   0.85   85.0
    86   0.86   86.0
    87   0.87   87.0
    88   0.88   88.0
    89   0.89   89.0
    90   0.9   90.0
    91   0.91   91.0
    92   0.92   92.0
    93   0.93   93.0
    94   0.94   94.0
    95   0.95   95.0
    96   0.96   96.0
    97   0.97   97.0
    98   0.98   98.0
    99   0.99   99.0
    100   1.0   100.0

请注意,对于0.530.95这两个数字,在乘以100之后,得到分别是52.999996, 58.999996,这是float类型,如果在代码中采用强转,采用代码int num = (int) (Float.parse(str) * 100) 将float转为int,那问题就大了。52.999996将变为52, 58.999996将变为58.

在Java中如果涉及到精确的数据计算,尤其是在货币,金融行业,Java是推荐使用String 和BigDecimal配合完成数据的计算。(http://docs.oracle.com/javase/1.5.0/docs/api/java/math/BigDecimal.html). BigDecimal中提供各种计算方法,使用非常方便。对于两个数的相加,代码如下:

 public static float add (String str1, String str2) {
        BigDecimal number1 = new BigDecimal(str1);
        BigDecimal number2 = new BigDecimal(str1);
        return number1.add(number2).floatValue();
    }

[Algorithm] 常见排序算法的总结

好久没看看算法这些东西了,今天突然想看一下,先看几个常见的排序算法,自己写写。找找当年的感觉。

算法的分类

常见的排序算法通常可以划分为插入排序,交换排序,选择排序三大类,每一类中又有多个子类:
1.选择排序:直接选择排序
2.插入排序:直接插入排序,折半插入排序,希尔排序
3.交换排序:冒泡排序,快速排序

  • 以下代码中如没有特殊说明,int [] a是待排序数组,int n 是数组的长度。最初采用C写,所以传了一个数组长度进去,如果用java实际上就没必要了。
直接选择排序

**:
第一趟:程序将记录定位在第1个数据上,将第1个数据和后面的每个数据进行比较,如果第1个数据大于后面某个数据,交换它们,第一趟结束后,最大数在最末。
第二趟:程序将记录定位在第2个数据上,将第2个数据和后面的第个数据进行比较,如果第2个数据大于后面某个数据,交换它们
....
依次类推了。

/**
 * @param a
 *            array
 * @param n
 *            the length of array
 */
public static void selectSort(int a[], int n) {
    int i, j, temp;
    for (i = 0; i < n - 1; i++) {
        for (j = i + 1; j < n; j++) {
            if (a[i] > a[j]) {
                temp = a[i];
                a[i] = a[j];
                a[j] = temp;
            }
        }
    }
}
修正后直接选择排序

**:直接选择排序的一个缺点是每一趟中都会进行多次的数据交换,而实际上每一趟都只是从子序列中选出一个最大值或最小值到末尾。因此,其实可以只需要找出本趟中的最小数或最大数,然后和子序列的第一个元素或最后一个元素交换就行。
如对于序列[3,1,2,5,4]:
第一趟,找出最小值1,和序列的第一值比较,1<3,将1放到首位,序列变为[1,3,2,5,4]
第二趟, 从子序列[3,2,5,4]中找出最小值2,和子序列的第一个值比较2<3,将2放到首位,序列变为[1,2,3,5,4]
第三趟,从子序列[3,5,4]中找出最小值3,和子序列的第一个值比较3=3,所以不用移动。序列还是[1,2,3,5,4]
第四趟, 从子序列[5,4]中找出最小值4,和子序列的第一个值比较4<5,故将二者对换,序列变为[1,2,3,4,5].
排序结束。

public static void ajustSelectSort(int a[], int n) {
    int i, j, k, minIndex;
    for (i = 0; i < n - 1; i++) {
        minIndex = i;
        for (j = i + 1; j < n; j++) {
            if (a[j] < a[minIndex]) {
                minIndex = j;
            }
        }
        if (minIndex != i) {
            k = a[i];
            a[i] = a[minIndex];
            a[minIndex] = k;
        }
    }
}
插入排序

**:依次将待排序的数组元素按其关键字的大小插入前面的有序序列。
第一趟:将第2个位置上的元素插入到前面的有序序列,此时,前面只有一个元素,当然是有序的。
第二趟:将第3个位置上的元素插入到前面的有序序列,
.......

public static void insertSort(int a[], int n) {
    int i, j, k, t;
    for (j = 1; j < n; j++) {
        t = a[j];
        k = j - 1;
        while ((k >= 0) && (a[k] > t)) {
            a[k + 1] = a[k];
            k = k - 1;
        }
        a[k + 1] = t;
    }
}
折半插入排序

**:对于简单插入而言,当第i-1趟需要将第i个元素插入到前面的0i-1个元素序列中,它总是从i-1个元素开始逐个比较每个元素,直到找到它的位置,这显然没有利用前面的0i-1个元素已有序的特点。折半插入排序则是对这一缺点的改进,其**可参看这个 ,代码记录如下:

public static void binaryInsertSort(int a[], int n) {
    int i, j, low, high, temp, mid;
    for (i = 1; i < n; i++) {
        temp = a[i];
        low = 0;
        high = i - 1;
        while (low <= high) {
            mid = (low + high) / 2;
            if (temp > a[mid]) {
                low = mid + 1;
            } else {
                high = mid - 1;
            }
        }
        for (j = i; j > low; j--) {
            a[j] = a[j - 1];
        }
        a[low] = temp;
    }
}
希尔排序

希尔排序实际上是分组的插入排序。先取一个小于n的整数d1作为第一个增量,把文件的全部记录分成d1个组。所有距离为d1的倍数的记录放在同一个组中。先在各组内进行直接插入排序;然后,取第二个增量d2<d1重复上述的分组和排序,直至所取的增量dt=1(dt<dt-l<…<d2<d1),即所有记录放在同一组中进行直接插入排序为止。维基百科中对此有详细的讲解

public static void shellSort(int[] array) {
    int i, j, d, t;
    int length = array.length;
    for (d = length / 2; d >= 1; d /= 2) {
        // 子序列中相隔增量为d的数据元素直接进行插入排序
        for (j = d; j < length; j++) {
            t = array[j];
            i = j - d;
            while ((i >= 0) && array[i] > t) {
                array[i + d] = array[i];
                i = i - d;
            }
            array[i + d] = t;
            System.out.println("增量为" + d + "   元素-->"
                    + Arrays.toString(array));
        }
    }
}
冒泡排序

**:
第一趟,依次比较0和1,1和2,2和3...,n-2和n-1索引的元素,如果发现前一数据大于后一数据,交换它们,经过第1趟,最大的元素排到最后。
第二趟,依次比较1和2,2和3,3各4...,n-2和n-1索引的元素,如果发现前一数据大于后一数据,交换它们,经过第2趟,第2大的元素排到倒数第2位。
.......
所以:直接选择排序和冒泡排序的一个很重要的区别在于:冒泡排序每次比较的是相邻的元素。

public static void bubbleSort(int a[], int n) {
    int i, j, temp;
    for (i = 0; i < n - 1; i++) {
        for (j = 0; j < n - 1 - i; j++) {
            if (a[j] > a[j + 1]) {
                temp = a[j];
                a[j] = a[j + 1];
                a[j + 1] = temp;
            }
        }
    }
}
修正后的冒泡排序

**:冒泡排序可以每一次将最大的元素移到最末,同时也可以理顺部分元素。其有一个很重要的特点,一旦某一趟没有交换发生,即可提前结束排序。因此,我们采用一个标识flag,对一趟排序的初始值为0, 对于每一趟排序,当发生交换时,就将flag设为1。如果一趟排序后,发现没有flag还是为0,即没有发生交换,则可提前退出排序。

public static void ajustBubbleSort(int a[], int n) {
    int i, j, k, flag;
    for (i = 0; i < n; i++) {
        flag = 0;
        for (j = 0; j < n - 1 - i; j++) {
            if (a[j] > a[j + 1]) {
                k = a[j + 1];
                a[j + 1] = a[j];
                a[j] = k;
                flag = 1;
            }
        }
        if (flag == 0) {
            break;
        }
    }
}
快速排序

快速排序使用分治法(Divide and conquer)策略来把一个串行(list)分为两个子串行(sub-lists)。
:以数组中的某一项元素x为枢轴(pivot),把数组分成两部分,前一部分的所有元素均≤x,后一部分的所有元素均≥x;
:对前后两部分数组采用同样的办法处理。
其步骤为:

  1. 从数列中挑出一个元素,称为 基准(pivot)
  2. 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
  3. 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
    示意图如下,摘自维基百科:
    image
    代码如下:
public static void quickSort(int[] array, int low, int upper) {
    if (low < upper) {
        int i = low, j = upper;
        int temp = array[low];
        while (i < j) {
            while (i < j && array[j] > temp)
                j--;
            if (i < j) {
                array[i] = array[j];
                i++;
            }
            while (i < j && array[i] < temp)
                i++;
            if (i < j) {
                array[j] = array[i];
                j--;
            }
            array[i] = temp;
            quickSort(array, low, i - 1);
            quickSort(array, i + 1, upper);
        }
    }
}

[Android]一种Adroid应用程序屏幕适配的思路

一种Adroid应用程序屏幕适配的思路

在Android中做屏幕适配一直都是一个比较麻烦的活。今天总结一下我们的一种屏幕适配的思路。文章的侧重点是如何用同一个Android项目去同时适配手机和平板。

基本的几个概念

1. px

像素--用来计算数码影像的最小单位。我们通常说的Nexus 5 屏幕分辨率是1080*1920,指的便是这款手机的宽是1080px, 高为1920px。注意px均为整数,不存在小数的情况。

2. 英寸

很好理解,通常我们所说的5寸屏,指的便是屏幕的尺寸是5英寸。测量的是屏幕的对角线长度。

3. dpi

dpi是Dots Per Inch的缩写,即每英寸的点数,此处的点指的是像素,它表示的是每英寸所包含的像素个数。比如320X480分辨率的手机,宽2英寸,高3英寸, 每英寸包含的像素点的数量为320/2=160dpi(横向)或480/3=160dpi(纵向),160就是这部手机的dpi,横向和纵向的这个值都是相同的,原因是大部分手机屏幕使用正方形的像素点,dpi用来描述屏幕密度。
也可以通过计算屏幕对角线的分辨率再除以屏幕尺寸这种方式。

4. dp

也称为dip, device independent pixels 独立设备像素。px = dpi / 160 * dp

图片

这一部份不是我想说的重点,简单提一下:
以下内容部份摘自 http://www.stormzhang.com/android/2014/05/16/android-screen-adaptation/
android中的图片资源都是放在drawable目录下,具体有以下这么几个文件夹

  • drawable-ldpi (dpi = 120)
  • drawable-mdpi (dpi = 160)
  • drawable-hdpi (dpi = 240)
  • drawable-xhdpi (dpi = 320)
  • drawable-xxhdpi (dpi = 480)

注意Android系统会根据屏幕的密度去从对应的文件夹中寻找图片,如果没有寻找到,会再考虑从其它的文件中寻找。

布局

接下来,看一下,如何用同一个Android项目去适配手机和平板。
android 中允许用指定availabe width这种方式(w<N>dp)来布局。指定最小的屏幕的宽度。需要注意一点的是,此处的宽度会随着屏幕的旋转而发生变化。当我们的app中提供了几个这种配置的布局文件夹,Android系统在使用时会选择最接近但不超过屏幕尺寸的那个文件夹中的布局文件。注意,此处对屏幕尺寸的比较是基于dp的。相关内容可以参看(这里)[http://wear.techbrood.com/guide/topics/resources/providing-resources.html]
举两个例子:

  1. 假设我们现在有一个屏幕密度为160dpi的标准屏平板,横放时宽度是1024dp,此时如果我们的layout文件只有layout-w960dp, layout-w1280dp,会发现这个平板会采用layout-w960dp里面的布局。
  2. 如果有一个屏幕密度为320dpi的平板,屏幕的分辨率为1280*720。如果此时布局文件有layout-w960dp, layout-w400dp, layout-nodpi, layout这么四个文件夹。最终在横屏时,它会采用layout-w960dp 因为其横屏时屏幕的宽度为1280/2 = 640dp;在竖屏时, 它会采用layout-nodpi,因为720/2=360,layout-w960dp, layout-w400dp肯定是不会采用了。只会从剩下的layout-nodpi和layout里面选。最终发现,选择的是layout-nodpi里面的布局。

回到刚开始的问题,我们如何用同一个Android项目去适配手机和平板

我们的思路是这样子的:

  1. 对于手机,我们采用一套布局,将其放到layout文件夹中。
  2. 对于平板,根据当前比较火的平板的尺寸,定义了4套布局,分别是layout-w1280dp, layout-w1024dp, layout-w960dp, layout-nodpi。添加一个layout-nodpi 是为了handle可能会匹配不上的情况,即如果有一个平板,假设其width的dp大于1280。此时,如果没有layout-nodpi,程序会一启动就挂。
  3. 针对同一个屏,phone的布局文件名和tablet中的布局文件名会不一样。如针对主屏, MainActivity, phone上的布局文件为activity_main, tablet上的布局文件为activity_main_hd。这样在使用时就不会乱。
  4. 平板上统一强制采用横屏的方式显示。

测试时会用到的一些代码

1. 获得屏幕的一些基本信息

DisplayMetrics metric = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metric);
Log.i("MutiScreen", "width:" + metric.widthPixels + "  height:" + metric.heightPixels
                + "  density:" + metric.density + "   dpi:" + metric.densityDpi);

2. 如何区分平板和手机

public static boolean isTablet(Context context) {
    return (context.getResources().getConfiguration().screenLayout
                & Configuration.SCREENLAYOUT_SIZE_MASK)
                >= Configuration.SCREENLAYOUT_SIZE_LARGE;
}

[Android]WeakReference在Android中的使用

#最近因为各种机缘接触到了WeakReference,今天在此总结一下。
Java中将对象的引用分为强引用软引用弱引用虚引用这四种级别,从而使程序能更加灵活的控制对象的生命周期。先简单介绍一下四种引用:

  • 强引用:平时我们编程的时候例如:Object object=new Object();那object就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空 间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
  • 软引用(SoftReference):如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
  • 弱引用(WeakReference)::如果一个对象只具有弱引用,那就类似于可有可物的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,**一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。**不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
  • 虚引用(PhantomReference)虚引用顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。 虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。为一个对象设置虚引用关联的叭一目的就是能在这个对象被收集器回收时收到一个系统通知。

弱引用(WeakReference)

一个简单的弱引用的例子,从中我们可以看到WeakReference的基本使用。

public class TestWeakReference {
    public static void main(String args[]) {
        String abc = new String ("abc");
        //构造一个WeakReference对象
        WeakReference<String> weakRef = new WeakReference<String> (abc);
        abc = null;
        System.out.println("before GC: " + weakRef.get());
        System.gc();
        System.out.println("After GC: " + weakRef.get());             
    }
}

在测试的时候加上-verbose:gc 参数,观察运行结果。某次运行结果如下:

before GC: abc
[Full GC 229K->137K(5056K), 0.0122251 secs]
After GC: null

由此可见,当GC执行后,WeakReference类型的引用被回收了。由于代码System.gc()只是说向JVM发出调用GC的命令,但是至于GC什么时候执行,是不由我们的代码所控制,因此,从理论上讲这段程序的运行结果可能会不出现[Full GC 229K->137K(5056K), 0.0122251 secs],即GC没有执行,但是这种情况出现了,那么一定会有After GC: abc,就不是After GC: null

WeakReference在Android中的使用

由于WeakReference类型的对象在GC被调用时会被回收。依据这一特性,在Android开发过程中很多地方会用到这一特性。像Universal-Image-loader中就用到了这个来做图片缓存操作。下面给出另一个使用场景。
Android开发中我们经常会使用到Handler,一个很常见的情况便是,当前的Activity已经被销毁了,当是此Activity中定义的Handler还在后台运行着(Handler只有将MessageQueue中的消息全部处理完毕才会被销毁)如果Handler中有更新UI的操作,那么由于Activity已被销毁,此时更新UI肯定会造成程序的Crash。此时我们便想如果有一种方式,若Activity已经被销毁,Handler便不再处理消息。借助WeakReference可以实现这一需求。当然,如果在Handler中仅仅只是处理UI的相关操作,可以先判断所操作的UI元素是否为NULL,如果为NULL就不再处理,那么不采用这种方式也可以。

public class SampleActivity extends Activity {

    private static class MyHandler extends Handler {
      private final WeakReference<SampleActivity> mActivity;

      public MyHandler(SampleActivity activity) {
        mActivity = new WeakReference<SampleActivity>(activity);
      }

      @Override
      public void handleMessage(Message msg) {
        SampleActivity activity = mActivity.get();
        if (activity != null) {
          // Handle message
        }
      }
    }
}

上面代码,在handler中保留一个WeakReference类型的引用,引用的真实类型是外部的Activity。这样既不影响外部Activity的生命周期,同时也便于对消息的处理。至于为什么此处的Handler要声明成一个static 类型,其目的是为了避免Memory Leak。这个在博客中有清楚的讲解。这个作者写的文章真是不错,可以好好看看。

  • 本文中的对java中四种引用类型的总结也是摘自互联网,不记的出处了。Thanks.

[Database][EAV模型]

EAV(Entity–Attribute–Value,实体-属性-值),一种数据库模型,使用eav建模的好处是可以动态为数据模型增加或移除属性。

  • 传统的关系表模型将所有的属性组成为一张表的字段,这样的优点在于,数据的读取简单,然而,一旦出现业务的变更,属性的增加和删除,就需要对相应的数据表进行更改。
  • EAV数据库模型将模型的属性以记录的形式存放在数据表中,即使需要新增和移除属性,仅仅需要删除数据表中相应的记录即可,而无需修改数据库结构。因此,这种模型可以很方便的支持对关系型数据的实体属性进行无限扩展。
  • 简单来理解,它就是将传统的关系型数据表中的列转成行了。
  • 目前来讲,我所接触的这种模型用的最多的就是进行系统的配置。比如,我们对一个Web 应用定义一个config表。其结构便是
ID key value
1 language english
2 debug false

表中除了主键ID之外,只有两个字段:keyvalue,每次需要对系统添加配置参数时,直接在该表中添加一行便OK.

[Android]StrictMode的使用

Android从2.3增加了StrictMode这个类,用来帮助开发者发现现程序潜在的,但比较隐蔽的一些问题(如费时的IO操作,不小心在主线程中访问网络),提高程序的健壮性。StrictMode提供了两种策略对Android程序进行检查,一种是常规策略,另一种是对JVM的检测。

  • 常规策略中检测的主要是对磁盘的IO操作,网络的访问,执行速度较慢的代码。配置如下:
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                 .detectDiskReads() //检测磁盘读的status
                 .detectDiskWrites() //检测磁盘写的status
                 .detectNetwork()   //检测网络
                 .penaltyLog() //检测情况在logcat中显示
                 .build());

如果不想写detectDiskReads(), detectDiskWrites(),detectNetwork(),有一个方法名叫detectAll(),直接用这一个就好。

  • JVM的检测则主要是对内存泄露。配置如下:
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .penaltyLog()
                 .penaltyDeath()
                 .build());

当开发者违反某类规则时,每种策略都会有不同的方法令开发者知道当时的情况。相关的违反情况可以记录在LogCat中。

[Security]采用消息验证码提高移动端和服务器数据交互的安全性

采用消息验证码提高移动端和服务器数据交互的安全性

以前一直都有一个疑问,如果客户端在和服务器端进行通信时,如果在中途客户端发送给服务器端的数据被不法分子拦截到了,然后不法分子修改了我的请求数据,再发送给服务器。那岂不是很危险,有没有什么方式来避免这种情况。举个例子,假设我登录网上银行进行转账操作,我想向'Milo'转1000元,于是我的操作向Server端送的数据是{from:'Fred', to:'Milo', amount: 1000},假设有不法分子'Abhi'截取到了这个数据报,然后把它改成{from:'Fred', to:'Abhi', amount: 90000}。于是情况就糟高了,银行在完全不知情的情况下,把我的钱转出了90000给'Abhi'。接着各种纠纷便来了。如何解决这个问题呢?

我记得之前在做一个APP,我们会调用到腾讯微博的API, 在腾讯微博中有一个上传多张图片(记得不清楚)的API,是属于一个高级接口,在调用时,腾讯要求开发者将请求的参数字段名的升序进行排列,然后采用SHA1算法生成消息摘要,或者说进行签名,将签名数据附在URL后传过去。当时,自己还不是很理解为什么要这样,后来在实际项目中也碰到了采用这种方式,提高数据交互的安全性。在此记录一下。

还是回到最初的这个问题,我们可以采用如下思路,当A想要将数据传递给B时,在发送数据的同时,把对应的消息摘要也发送过去。当B接收到数据时,根据传过来的数据重新计算消息摘要,然后比对A传过来的消息摘要,如果二者一致,则认为当前请求的数据是安全的,然后进行相应的处理。消息摘要的生成可以采用多种算法,如HmacHash256, MD5,SHA1等。

具体做法如下:

  1. 在开发时,移动端和服务器端的开发人员可以约定一个规则生成数据的消息摘要,最简单就是按照请求参数的数据进行升序排列,生成消息摘要。如数据{from:'Fred', to:'Milo', amount: 1000},首先按照参数的升序,依次是amount, from, to 对应的字段,假设我们为了使可读性更好,决定在每两个字段间用竖线(|)隔开。依照这个规则,构造原始数据为1000|Fred|Milo
  2. 选择一个消息摘要算法生成消息摘要。我们采用HmacHash256,它的安全性较高,其密钥为ABCDEFGHIJK, 数据1000|Fred|Milo生成的消息摘要为6ffa87395f4679512163b502be3921db5b085fae05f46a7fe22f111b7dadd586
  3. 当我们向服务器端发送数据时,假设其API为: http://chinabank.com/tranfer. 我们采用POST方式提交数据,请求体是一个JSON, 原本为{from:'Fred', to:'Milo', amount: 1000},由于我们采用了消息摘要,于是此时的请求体为{from:'Fred', to:'Milo', amount: 1000, signature:'6ffa87395f4679512163b502be3921db5b085fae05f46a7fe22f111b7dadd586'}。当服务器端拿到这个数据后。会重新利用HmacHash256算法,采用ABCDEFGHIJK生成摘要,然后将生成的摘要请求体中的signature进行比对,如果相同则处理本次请求,如果不同,则认为是非法请求,丢弃。
  4. 需要注意的是,HmacHash256算法是已公开的。因此,安全性取决于密钥,这也就是柯克霍夫原则(数据的安全基于密钥而不是算法的保密)。而且Client端和Server端采用的密钥是一致的,这个是一定不要泄露的。

以下是一段采用HmacHash256实现消息验证码的的代码片断:

String macKey = "ABCDEFGHIJK";
String macData = "the data string";

Mac mac = Mac.getInstance("HmacSHA256");
byte[] secretByte = macKey.getBytes("UTF-8");
byte[] dataBytes = macData.getBytes("UTF-8");
SecretKey secret = new SecretKeySpec(secretByte, "HmacSHA256");

mac.init(secret);
byte[] doFinal = mac.doFinal(dataBytes);
String result = "";
for (int i = 0; i < doFinal.length; i++) {
    result += Integer.toHexString(
            (0x000000ff & doFinal[i]) | 0xffffff00).substring(6);
}
System.out.println(result);

将byte数组转成String, 也可采用如下代码,其原理可以参看Java中byte与16进制字符串的互换原理

public static String bytesToHexString(byte[] src) {
    StringBuilder stringBuilder = new StringBuilder("");
    if (src == null || src.length <= 0) {
        return null;
    }
    for (int i = 0; i < src.length; i++) {
        int v = src[i] & 0xFF;
        String hv = Integer.toHexString(v);
        if (hv.length() < 2) {
            stringBuilder.append(0);
        }
        stringBuilder.append(hv);
    }
    return stringBuilder.toString();
}

我们是这么做的

在我们的项目中采用了类似上面的思路做了一个安全的enhancement。不过和上面的思路有一些区别。我们不仅会把这个加密后的数据signature传给Server端,同时还会把当前时间timestamp传递过去。为了和最初设计的请求体结构保持一致,我们将signaturetimestamp放在http请求的Header中传递。当Server端接收到数据时:

  1. 首先从Header中提取timestamp,如果发现timestamp的值和和当前时间相差较大如(20分钟),便返回一个错误的消息如:{error_code:101010, error_msg:"request too old"}。同时,timestamp也是signature明文的组成部份,这样可以避免重放攻击。
  2. 如果发现timestamp的值是在可以接受范围之类的,则开始提取请求体中的参数,按一定的顺序排列,然后再进行提取其消息散列码。
  3. 比对生成的消息散列码和http请求Header中的signature值,如果相同,则认为本次请求是合法,然后进行相应的业务处理,如果不相同,则认为本次请求是非法的,丢弃本次请求,或者执行其它的操作。

[PHP]MongoDB与Yii搭建API服务-2

MongoDB与Yii搭建API服务-2

在上一篇文章中主要记录了Yii对MongoDB的基本操作,本文章主要记录整个API设计的一个大概结构。我做PHP较少,不知道更好的设计是什么样子,但是从觉得当前的设计来看,结构还是蛮清晰的,感觉还是不错的。以前做javaWeb的时候,结构差不多也是这样设计,但和PHP相比,java确实是重一些。在此之前,推荐一个不错的API设计的文章,点这里

Model的设计

在上一篇文章中其实已经介绍过了Model的设计,此时再回顾一下。首先,我们会定义一个BaseModel,这个里面定义了一些公用的属性和rules,其它的实体类需要继承自它。通常在Model中会添加deleted, created_time, updated_timeMongoDB中的主键是_id,这BaseModel中定义了,其它子类中也不用再写了。由于BaseModel没有对应的collection,所以getCollectionName方法必须返回nullrules里面定义的是一些基本的验证规则。在子类中,可以将这些rules Merge进去。

<?php
class BaseModel extends EMongoDocument
{
    public $_id;
    public $created_time;
    public $updated_time;
    public $deleted;
    public static function model($className=__CLASS__)
    {
        return parent::model($className);
    }
    public function getCollectionName()
    {
        return null;
    }
    public function rules()
    {
        return array(
            array('_id', 'default', 'value' => new MongoId()),
            array('deleted', 'default', 'value' => Constant::UNDELETE),
            array('created_time', 'default', 'value' => new MongoDate()),
            array('updated_time', 'default', 'value' => new MongoDate()),
        );
    }
    public function attributeLabels()
    {
    }
}

User类.

class User extends BaseModel {
    public $name;
    public $password;
    public $nickname;
    public $email;
    public $height;
    public $weight;
    public $gender;
    public $register_time;
    public $last_login_time;

    public static function model($className=__CLASS__) {
        return parent::model($className);
    }


    /**
     * @return string the associated collection name
     */
    public function getCollectionName() {
        return 'user';
    }

    public function rules() {
        return  array_merge(parent::rules(), array(
            ... //User类中属性的验证规则
        ));
    }
}

Error Message的设计

作为API端,为移动端提供数据,在正常情况下会直接返回移动端所需要的数据。但由于其响应结果会受移动端传过来的参数,网络,Server等从多因素的影响,肯定会有Error Message的输出。很容易理解一个例子就是,当用户登录时,如果输错了用户名和密码,则不能进入系统,此时的API返回的便是一个user name or password error
在Server端返回数据的时候,通常会返回一个json字符串,一般来讲我们会定义这个字符串的返回格式:

{status:"fail", error_code: 2010001, error_msg: "user name or pwd error"};

error_code:错误代码,在开发过程中可以约定,error_code由6位整数组成,第一位代表error的级别,如1表示系统级别的error(DB error), 2代表服务级别的错误。第二位和第三位代表某个feature, 如10代表用户模块。后三位代表具体的错误,001代表用户名和密码错误,002代表该帐号已锁定,等等。

error_msg:是对错误的简单描述,这个error_msg通常是给程序员看的,所以里面的内容不太友好也没有关系。当移动端如果接收到这个error_msg时,不会把它显示出来给最终用户,移动端的程序应该读取server端返回的error_code, 然后根据这个error_code加载相应的错误消息返回给最终用户。
所以一般来讲,我们会在{Yii}/protected/components下定义两个类,一个是ErrorCode, 一个是ErrorMsg。两个类中的属性都是常量,如下:

class ErrorCode {
    // System level error here
    const SERVER_ERROR = 101001;
    const DB_ERROR = 101002;
    const ILLEGAL_PARAMETER = 101003;
    .....

    const USER_NAME_EXISTED = 2010001;
}
class ErrorMsg {
    // System level error here
    const SERVER_ERROR = "Server error";
    const DB_ERROR = "Database error";
    const ILLEGAL_PARAMETER = "Illegal params";
    .....

    const USER_NAME_EXISTED = "user name existed";
}

Controller的设计

Controller是API端和移动端数据交换的接口,接收移动端的请求,并做相应处理后,将结果返回给移动端。作何一个项目会有多个Controller, 在设计Controller之前,我们可以分析一下,大部份Controller都会做这么几件事:

  1. 接收移动端的请求数据。
  2. 返回结果给移动端。
  3. 对于大多数Controller中的方法(对应的是一个URL),一定是只有用户登录后才可以访问,比如,修改密码。而大部份API不会做Session,此时可能就需要一个userId或者token之类的东西,表示当前用户的一个身份。
  4. .....

于是,想着和Model设计一样的方式,也定义一个BaseController, BaseController是继承自CController.

针对以上的几个问题:

1.假设我们和移动端的开发约定,移动端如果需要传参数,只能发post请求,且请求体必须是格式良好的json, 于是我们就可以添加一个方法getRequestParams()来获取请求的数据.

protected function getRequestParams() {
    $params = Yii::app()->request->getRawBody();
    if(isset($params)) {
        try{
            $params = CJSON::decode($params);
            return is_array($params) ? $params : array();
        } catch (Exception $e) {
            Yii::log('Param parse error:' . $params, CLogger::LEVEL_ERROR, 'BaseController');
            //给前端返回一个error msg.
        }
    } else {
        return array();
    }
}

2.API返回给移动端的数据全部都是json格式,于是定义一个response方法。

protected function response($response) {
    header('Content-type: application/json');
    echo CJSON::encode($response);
    Yii::app()->end();
}

3.针对特定API的访问。在Yii的CController中有一个beforeAction方法,从名字上看就知道这个方法一定是在每一个Actioin调用之前执行。为此我们覆盖这个方法, 在里面定义我们自己的规则:

public function beforeAction($action) {
    $this->initRequestUser();
    return true;
} 

private function initRequestUser() {
    $id = Yii::app()->request->getQuery('user_id');
    $actionItem = $this->getRoute();

    if('login' == $actionItem
        || 'register' == $actionItem
        || ......
        ) {

       return ;
    }
    //对于其它的URL如果没有传user_id这个参数,就认为是非法参数,不做处理。
    if(!isset($id)) {
            $this->error(Yii::app()->request->requestUri, ErrorCode::EMPTY_USERID, ErrorMsg::EMPTY_USERID);
    }
}

在API的设计时,我们是将user_id这个参数直接带在url上,并没有做为请求体中的参数(json)传过来。如修改密码的url为/api/changpassword/ab33397997cd7989e9d0。其中ab33397997cd7989e9d0便是user_id。在方法initRequestUser中id的获取$id = Yii::app()->request->getQuery('user_id'); ,是因为我们采用的URL重写规则是

'urlManager'=>array(
    'urlFormat'=>'path',
    'caseSensitive'=>false,
    'showScriptName'=>false,
    'rules'=>array(
             '<controller:\w+>/<action:\w+>/<user_id:.*?>'=>'<controller>/<action>',
             '<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
    ),
)

[Android]AchartEngine实现tooltip的功能

在上一个项目中,我们采用了AChartEngine来画图,实现了一种类似于Web界面上的tooltip的功能。在做Web时实现这种功能就很简单了,像Highcharts这种专业的报表插件直接就内嵌有这种功能。但AChartEngine中默认是没有的,不过我们可以自己写一个。在我们的App中做的要是精细一些,此处就实现一个粗糙版本,但足以说明实现的思路。首先我们先看最终实现的效果如下:
screen shot 2014-11-16 at 9 08 59 pm
横坐标中用来显示日期,纵坐标显示跑步的里程,点击折线上的某点时显示的是里程和对应消耗的热量。

Activity的实现

本例中的Activity实现就很简单了,主要是Mock一下数据。然后调用自己定的AchartEngine类来生成图表,最后将生成的图表添加到界面中去,让其显示出来。

public class TestAchartEngineActivity extends Activity {
    private LinearLayout llWraper;

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

    private void initView() {
        llWraper = (LinearLayout)findViewById(R.id.ll_wraper);
        //模拟数据
        String[] times = new String [] {"11.11", "11.12", "11.13", "11.14", "11.15", "11.16", "11.17"};
        double[] mileages = new double [] {2.5, 3.2,4.3,5.8,4.6,3.9,3.3};
        double[] carolies = new double [] {3.5, 4.9,4.3,3.8,5.6,4.9,4.3};
        //生成图表。
        View view = new AChartLinear(mileages, carolies, times, llWraper).execute(this);
        llWraper.addView(view);
    }
}

AChartLinear 类的实现

public class AChartLinear {
    private XYMultipleSeriesRenderer renderer;
    private GraphicalView chartView;

    private String[] times;
    private double[] mileages;
    private double[] carolies;
    // 定义x轴上只会有7个刻度
    private double[] x = { 0, 1, 2, 3, 4, 5, 6 };

    private int[] margin = { 0, 0, 0, 0 };

    //tooltip图片的尺寸为74和51dp,因为我们是将tooltip图片(尺寸是111*76)放在hdpi下面。
    private int toolTipWidth = 74;
    private int toolTipHeight = 51;

    private PopupWindow popup = null;
    private View layout;
    private View viewParent;

    private void initPopupWindow(Context context) {
        if (popup != null) {
            popup.dismiss();
        }

        popup = new PopupWindow(layout, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    public AChartLinear(double[] mileages, double[] carolies, String[] times, View viewParent) {
        this.mileages = mileages;
        this.carolies = carolies;
        this.times = times;
        this.viewParent = viewParent;
    }

    public View execute(Context context) {

        renderer = getLinearRenderer(context);
        chartView = ChartFactory.getLineChartView(context, getlinearDataSet(x, mileages), renderer);

        initToolTip(context);
        return chartView;

    }

    private void initToolTip(Context context) {
        renderer.setClickEnabled(true);
        // 设置可点击的触控范围
        renderer.setSelectableBuffer(DensityUtil.dip2px(context, 20));

        final Context contextTemp = context;
        toolTipHeight = DensityUtil.dip2px(contextTemp, toolTipHeight);
        toolTipWidth = DensityUtil.dip2px(contextTemp, toolTipWidth);

        LayoutInflater inflater = LayoutInflater.from(contextTemp);
        layout = inflater.inflate(R.layout.home_tooltip, null);
        // 获取toolTip中两个TextView,这两个TextView的值会不断变化
        final TextView distanceTotal = (TextView) layout.findViewById(R.id.distance_total);
        final TextView calorieTotal = (TextView) layout.findViewById(R.id.calorie_total);

        chartView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                MotionEvent motionEvent = MotionEvent.obtain(event.getDownTime(), event.getEventTime(), MotionEvent.ACTION_DOWN, event.getX(), event.getY(),
                        event.getMetaState());
                chartView.onTouchEvent(motionEvent);
                if (event.getAction() == MotionEvent.ACTION_DOWN) {
                    SeriesSelection seriesSelection = ((GraphicalView) v).getCurrentSeriesAndPoint();
                    //当点击的位置是对应某一个点时,开始获取该点处的数据,并且弹出PopupWindow.
                    if (seriesSelection != null) {
                        distanceTotal.setText(seriesSelection.getValue() + "");
                        calorieTotal.setText((int) carolies[seriesSelection.getPointIndex()] + "");
                        Log.i("data", seriesSelection.getValue() + " " + carolies[seriesSelection.getPointIndex()]);
                        //以下代码是为了计算tooltip弹出的位置。
                        // 实际点击处的x,y坐标
                        double[] clickPoint = chartView.toRealPoint(0);

                        double xValue = seriesSelection.getXValue();// 基准点的x坐标
                        double yValue = seriesSelection.getValue();// 基准点的y坐标

                        double xPosition = event.getRawX() - event.getX() + margin[1] + ((event.getX() - margin[1]) * xValue / clickPoint[0]);
                        double yPosition = event.getRawY() - event.getY() + margin[0]
                                + ((event.getY() - margin[0]) * (renderer.getYAxisMax() - yValue) / (renderer.getYAxisMax() - clickPoint[1]));
                        int xOffset = (int) (xPosition - toolTipWidth / 2);
                        // 减去7个dip是为了让poupup和点之间的距离高一点。
                        int yOffset = (int) (yPosition - toolTipHeight - DensityUtil.dip2px(contextTemp, 7)); 
                        initPopupWindow(contextTemp);

                        popup.showAtLocation(viewParent, Gravity.NO_GRAVITY, xOffset, yOffset);

                    } else { //当点击的位置不是图表上折点的位置时,如果上一次点击弹出的popup还存在,就把它dismiss掉。
                        if (popup != null) {
                            popup.dismiss();
                        }
                    }
                }
                return true;
            }
        });
    }

    public XYMultipleSeriesRenderer getLinearRenderer(Context context) {
        XYMultipleSeriesRenderer renderer = new XYMultipleSeriesRenderer();
        // 以下四个margin分别代表了:top, left, bottom, right
        margin[0] = DensityUtil.dip2px(context, 10);
        margin[1] = DensityUtil.dip2px(context, 25);
        margin[2] = DensityUtil.dip2px(context, 15);
        margin[3] = DensityUtil.dip2px(context, 20);
        renderer.setMargins(margin);

        XYSeriesRenderer r = new XYSeriesRenderer();
        r.setLineWidth(DensityUtil.dip2px(context, 1.5f));
        r.setColor(Color.parseColor("#32b2cd")); // 设置折线的颜色

        r.setPointStyle(PointStyle.CIRCLE);//设置表中点的样式。
        r.setPointStrokeWidth(DensityUtil.dip2px(context, 2));

        renderer.addSeriesRenderer(r);

        renderer.setXLabels(0); // 设置不显示x轴上的原始刻度
        //自定义X轴上的刻度
        for (int i = 0; i < times.length; i++) {
            renderer.addXTextLabel(i, times[i]);
        }
        renderer.setXAxisMin(0);// x轴最小值
        renderer.setXAxisMax(6);
        renderer.setYLabels(7); // 定义Y轴的7个刻度

        renderer.setYAxisMin(0);// y轴最小值
        renderer.setYAxisMax(Math.ceil(getMaxY(mileages)));//Y轴的最大值由传入的参数决定

        renderer.setLabelsTextSize(DensityUtil.dip2px(context, 12));
        renderer.setShowTickMarks(false);
        renderer.setAxesColor(Color.parseColor("#3c3c3c")); // 设置X轴的颜色
        renderer.setXLabelsColor(Color.parseColor("#3c3c3c"));
        renderer.setYLabelsColor(0, Color.parseColor("#3c3c3c"));

        renderer.setYLabelsAlign(Align.RIGHT);
        renderer.setYLabelsVerticalPadding(-5);
        renderer.setYLabelsPadding(5);
        renderer.setShowGrid(true);// 显示网格
        renderer.setShowCustomTextGrid(true);
        renderer.setFitLegend(true);// 调整合适的位置
        renderer.setPanEnabled(false, false); // x,y轴不可拖动
        renderer.setGridColor(Color.parseColor("#D8D8D8")); // 网格
        renderer.setShowLegend(false); 
        renderer.setInScroll(true);
        renderer.setBackgroundColor(Color.TRANSPARENT); // 设置背景色透明
        renderer.setMarginsColor(Color.argb(0x00, 0xff, 0x00, 0x00));
        renderer.setApplyBackgroundColor(true); // 使背景色生效
        renderer.setPointSize(DensityUtil.dip2px(context, 5));//设置点的大小


        return renderer;
    }
    //生成画图表所需要的数据集合。
    private XYMultipleSeriesDataset getlinearDataSet(double[] x, double[] mileages) {
        XYMultipleSeriesDataset dataset = new XYMultipleSeriesDataset();
        XYSeries series = new XYSeries("value");
        int len = x.length;
        for (int i = 0; i < len; i++) {
            series.add(x[i], mileages[i]);
        }
        dataset.addSeries(series);
        return dataset;
    }
    //获取Y轴的最大值。
    private double getMaxY(double[] mileages) {
        int len = mileages.length;
        int i;
        double max = 1;
        for (i = 0; i < len; i++) {
            if (max < mileages[i]) {
                max = mileages[i];
            }
        }
        return max;
    }

    public PopupWindow getPopup() {
        return this.popup;
    }

}

此类的实现略显复杂,其思路是:我们调用的是该类中的execute方法来实现绘画图表的需求。该execute方法做了这么几件事:首先,构造render, reader中会定义图表的样式,坐标轴的刻度等。其次,构造数据集合,AchartEngine会利用这个数据集合来生成图表。再次,调用AchartEngine中的getLineChartView方法来生成图表。最后,完成tooltip的基本的配置,这个是由initToolTip方法来实现的。
图表的基本样式
图表的样式定义是由方法public XYMultipleSeriesRenderer getLinearRenderer(Context context) {决定的。都是对render属性的设置,没有太复杂的东西,代码中相应的地方也都有注释,此处就不说明。唯一需要注意一点的是,如果要自定义坐标轴上的文字,需要用setXLabels(0)这种方式,这个是针对X轴的设置,对Y轴的设置也是一样的。在本例中,我们的X轴显示的是日期,故采用如下实现:

    renderer.setXLabels(0); // 设置不显示x轴上的原始刻度
        //自定义X轴上的刻度
        for (int i = 0; i < times.length; i++) {
            renderer.addXTextLabel(i, times[i]);
        }

关于Y轴刻度分布的问题
由于Y轴的最大值是不固定的,所以Y轴的刻度分布就不太好弄。为此,我们会先计算传进来的Y轴的最大值,然后根据Y轴最大值来确定其刻度分布。

关于toolTip的渲染
这个是实现该功能的关键点,对应的是initToolTip方法。首先明确一点的是,当我们点击图表上折线的拐点时,会弹出tooTip,所以第一步,我们需要捕捉点击事件。
设计一个点击范围,也就是说当我在这个拐点方圆多少距离时的点击才算是一个有效的点击。代码如下:

renderer.setClickEnabled(true);
 // 设置可点击的触控范围
renderer.setSelectableBuffer(DensityUtil.dip2px(context, 20));

获取点击处的数据信息,该数据信息会显示在toolTip中。此处图表中拐点的数据表示的是distance而由于我们设计时calories的数据信息与distance是严格对应的,因此,只要知道该distance的下标,也就可以得到calories的数值了。

SeriesSelection seriesSelection = ((GraphicalView) v).getCurrentSeriesAndPoint();
//当点击的位置是对应某一个点时,开始获取该点处的数据,并且弹出PopupWindow.
if (seriesSelection != null) {
    distanceTotal.setText(seriesSelection.getValue() + "");
    calorieTotal.setText((int) carolies[seriesSelection.getPointIndex()] + "");
}

toolTip的位置坐标
最关键一点,便是toolTip的出现位置。便是这一段代码的工作了.

//以下代码是为了计算tooltip弹出的位置。
// 实际点击处的x,y坐标
double[] clickPoint = chartView.toRealPoint(0);

double xValue = seriesSelection.getXValue();// 基准点的x坐标
double yValue = seriesSelection.getValue();// 基准点的y坐标

double xPosition = event.getRawX() - event.getX() + margin[1] + ((event.getX() - margin[1]) * xValue / clickPoint[0]);
double yPosition = event.getRawY() - event.getY() + margin[0]
        + ((event.getY() - margin[0]) * (renderer.getYAxisMax() - yValue) / (renderer.getYAxisMax() - clickPoint[1]));
int xOffset = (int) (xPosition - toolTipWidth / 2);
// 减去7个dip是为了让poupup和点之间的距离高一点。
int yOffset = (int) (yPosition - toolTipHeight - DensityUtil.dip2px(contextTemp, 7)); 
initPopupWindow(contextTemp);
popup.showAtLocation(viewParent, Gravity.NO_GRAVITY, xOffset, yOffset);

由于AChartEngine中并没有API让我们获取点击的拐点在屏幕上的位置,但是我们可以采用曲线救国的策略。其思路是,通过AChartEngine我们可以知道我们点击处对应的x,y坐标。并且通过Android API 我们是可以知道当前点击的物理位置。有了这两点信息,我们就可以根据比例计算得出拐点的物理位置信息。有了拐点的物理位置信息,我们便可以定位toolTip。等我有时间再补一张图说明一下,就更清晰了。

暴露toolTip
代码中,我们设计了一个方法,向外暴露toolTip。主要是针对当我们点击图表的外部时,我们希望dismiss掉这个toolTip。如果不暴露出去,外面是不能操作的。

public PopupWindow getPopup() {
    return this.popup;
}

toolTip的布局

布局就很简单了,同样,那个背景就不上传了。这种背景在网上很容易找到的。本文中的背景图片尺寸是111*76。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/toast_tooltip"
    android:layout_width="74.0dip"
    android:layout_height="51.0dip"
    android:background="@drawable/home_bkg_tip_111x76"
    android:gravity="top"
    android:paddingTop="5.0dp" >

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_gravity="left"
        android:layout_weight="1.0"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/distance_total"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:gravity="center_horizontal"
            android:text="27.5"
            android:textSize="12sp" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:gravity="center_horizontal"
            android:text="Km"
            android:textSize="10sp" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1.0"
        android:gravity="right"
        android:orientation="vertical" >

        <TextView
            android:id="@+id/calorie_total"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:gravity="center_horizontal"
            android:text="100"
            android:textSize="12sp" />

        <TextView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:gravity="center_horizontal"
            android:text="Cal"
            android:textSize="10sp" />
    </LinearLayout>

</LinearLayout>

[Java]Java对象中各成员的初始化顺序

今天在看书时,碰到了这么一个问题。有如下这么一段代码:

public class TestInitialize {
    public static void main(String args[]) {
        System.out.println(B.value);
    }   
}

class A {
    static int value = 100;
    static {
        System.out.println("A static block execute.");
    }   
}

class B extends A {
    static {
        System.out.println("B static block execute.");
    }
    public B() {
        System.out.println("B constructor execute.");
    }
}

本以为输出的结果中至少会有B static block execute.这么一段。后来执行后发现,结果如下:

A static block execute.
100

原来当访问一个java类或接口中的静态域时,只有真正声明这个域的类或接口才会被初始化。由于value是A类中的属性,B继承A。B.value直接访问的是类A中声明的静态域value。所以JVM只会初始化A.

既然看到这里,那就总结一下Java对象中各成员的初始化顺序。
1.父类静态成员和静态初始化块,按在代码中出现的顺序依次执行。
2.子类静态成员和静态初始化块,按在代码中出现的顺序依次执行。
3.父类实例成员和实例初始化块,按在代码中出现的顺序依次执行。
4.执行父类的构造方法。
5.子类实例成员和实例初始化块,按在代码中出现的顺序依次执行。
6.执行子类的构选方法。

代码验证

public class TestInitialize {
    public static void main(String args[]) {
        new B();
    }
}

class A {
    static int value = 100;
    /*
     * 注意,在初始化的过程中,静态代码块和静态域的出现顺序很重要,虚拟机会严格按照在源代码中的出现顺序来执行初始化操作。
     * 同样,对于成员变量和普通代码块也是一样的。
     */
    static {
        System.out.println("print static value in static block. static value:" + value);
        System.out.println("A static block1 execute.");
    }
    static {
        System.out.println("A static block2 execute.");
    }
    public int a;

    {
        System.out.println("A common block execute.");
    }

    public A() {
        System.out.println("A constructor execute.");
    }

}

class B extends A {
    static {
        System.out.println("B static block execute.");
    }

    {
        System.out.println("B common block execute.");
    }

    public B() {
        System.out.println("B constructor execute.");
    }
}

输出结果如下:

print static value in static block. static value:100
A static block1 execute.
A static block2 execute.
B static block execute.
A common block execute.
A constructor execute.
B common block execute.
B constructor execute.

代码很容易懂,这里就不解释了。

[Android] 一种移动APP中DAO访问的设计思路

今天把之前项目中关于SQLite的使用进行一个总结,相关的基本的概念性的东西就不说了。此处主要理一下,在一个APP中DB访问这一层的一种设计思路。直接上代码,先来一个直观的感觉。

一种设计思路

public class DBUtils extends SQLiteOpenHelper {
    private static final String TAG = "DBUtils";
    private static int version = 1;
    private static DBUtils instance = null;
    private static HashMap<String, String> tables = null;
    private static String DB_NAME = "test.db";
    private static final String DROP_TABLE_SQL = "DROP TABLE IF EXISTS %1$s";

    interface IUser {
        public static final String TABLE_NAME = "user";
        public static final String COLUMN_ID = "id";
        public static final String COLUMN_NAME = "name";
        public static final String COLUMN_EMAIL = "email";

        public static final String CREATE_TABLE_SQL = "CREATE TABLE IF NOT EXISTS %1$s("
                + COLUMN_ID
                + " INTEGER PRIMARY KEY AUTOINCREMENT,"
                + COLUMN_NAME
                + " TEXT,"
                + COLUMN_EMAIL           
                + " TEXT" + ")";
    }

    public static DBUtils getInstance(Context context) {
        if (instance == null) {
            instance = new DBUtils(context);
        }
        if (tables == null) {
            tables = new HashMap<String, String>();
            tables.put(IUser.TABLE_NAME, IUser.CREATE_TABLE_SQL);
        }
        return instance;
    }

    private DBUtils(Context context) {
        super(context, DB_NAME, null, version);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        try {
            db.beginTransaction();
            for (String str : tables.keySet()) {
                db.execSQL(tables.get(str));
            }
            db.setTransactionSuccessful();
        } catch (Exception e) {
            Log.i(TAG, "Create table error");
        } finally {
            db.endTransaction();
        }       
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        dropAllTables();
        onCreate(db);
    }
    public void dropAllTables() {
        SQLiteDatabase db = instance.getWritableDatabase();
        try {
            for (String tableName : tables.keySet()) {
                db.execSQL(String.format(DROP_TABLE_SQL, tableName));
            }
            db.setTransactionSuccessful();
        } catch (Exception e) {
            Log.i(TAG, "Create table error");
        } finally {
            db.endTransaction();
        }     
    }

    public void addUser(User user) {
        ContentValues values = new ContentValues();
        values.put(IUser.COLUMN_NAME, user.getName());
        values.put(IUser.COLUMN_EMAIL, user.getEmail());
        SQLiteDatabase db = instance.getWritableDatabase();
        db.insert(IUser.TABLE_NAME, null, values);
    }

    public void deleteByEmail(String email) {
        SQLiteDatabase db = instance.getWritableDatabase();
        db.delete(IUser.TABLE_NAME, "\"" + IUser.COLUMN_EMAIL + " = ? " + "\"", new String[] {email});
    }

    public void updateUserById (int id) {
        ContentValues values = new ContentValues();
        values.put(IUser.COLUMN_NAME, "userName");
        values.put(IUser.COLUMN_EMAIL, "userEmail");
        SQLiteDatabase db = instance.getWritableDatabase();
        db.update(IUser.TABLE_NAME, values, "id = ?", new String [] {id});
    }

}

相关解释

  • 我们在做DAO层是这样考虑的,所有的与数据库相关的操作全部整合在在这一个类中。刚开始,不是很理解为什么这样做。可能是因为之前做Web做了蛮长时间,一直觉得不适应,始终觉得应该是为每一个相应的model定义一个DAO访问类,虽然在我们的另一个项目中也是这样做的。但后来觉得,不应该把做Web的思路带到做移动APP中来,做Web时,每一个model都有对应的表,都会做数据的持久化,写入到数据库中,但做移动APP的开发,SQLite是用来做数据的缓存的。并不是每一个model都需要做缓存。当然如果一个APP要缓存的数据非常多,那就不能采用这种设计方式了。否则这个类以后代码将会非常多,很难维护。但对于一个小型的APP,这种设计是完全可以搞定的。
  • 代码中的private static int version = 1是定义数据库的版本号,在做数据库的升级时会使用到这个属性。
  • 采用IUser这种方式来定义一个表的相关属性,表名,表中的字段,建表语句全部都以这个接口的属性这种形式存在。如果有多张表,类中就有多个这种形式的接口。
  • 整个类是采用单例的设计,实际上此处的单例还可以优化,可参看另一篇文章。在初始化本类实例的时候,也会对属性tables进行初始化。tables是一个HashMap,其key是表名,value是对应的建表的语句。这样便于在创建表和删除表时进行管理。
  • 属性DB_NAME指整个APP的DB名称,与其物理文件是对应的。
  • 在类的onCreate方法里面,执行创建表的操作。
  • onUpgrade方法会在数据库升级时触发。这个里面很常见的一个操作是先清空表,再重新新建表。

SQLite 的基本操作

关于SQLIte的基本操作,网上一搜就一大堆。有过数据库开发经验的人可以说是马上就上手。不想做过多的说明。总结几点(没有测试,只做思路说明):
插入

采用ContentValues,每一个ContentValues实例实际上就是对应表里面的一条数据。如下操作:

public void addUser(User user) {
    ContentValues values = new ContentValues();
    values.put(IUser.COLUMN_NAME, user.getName());
    values.put(IUser.COLUMN_EMAIL, user.getEmail());
    SQLiteDatabase db = instance.getWritableDatabase();
    db.insert(IUser.TABLE_NAME, null, values);
}

删除

public void deleteByEmail(String email) {
    SQLiteDatabase db = instance.getWritableDatabase();
    db.delete(IUser.TABLE_NAME, "\"" + IUser.COLUMN_EMAIL + "= ? " + "\"", new String[] {email});
}

如果是删除一整张表中的数据就用

instance.getWritableDatabase().delete(IUser.TABLE_NAME, null, null);

更新

实际上和插入的操作差不多。

ContentValues values = new ContentValues();
values.put(IUser.COLUMN_NAME, "userName");
values.put(IUser.COLUMN_EMAIL, "userEmail");
SQLiteDatabase db = instance.getWritableDatabase();
db.update(IUser.TABLE_NAME, values, "id = ?", new String [] {id});

查询

查询操作实际上就是利用Cursor.

Cursor localCursor = instance.getReadableDatabase().query(IUser.TABLE_NAME, null, "email = ?", new String[] { email }, null, null, null);

然后再就是移动Cursor,取值。

说明

在SQLite的使用过程中,没有必要每一次在操作完数据库后就关闭数据库连接,否则会造成莫名的Exception.通常应用中操作数据库都是采用新开一个线程来执行,有可能有两个线程都在操作数据库。第一个线程操作完后若关闭了数据库连接,第二个线程还正在操作数据库,就会出问题了。当然关闭数据库连接出现的Exception并不局限于这种情况,但是我们在使用时,没有手动关闭数据库连接是不会有问题,一旦手动关闭就会出现一些莫名其妙的问题。其中缘由还需Rearch。

今天无意中发现了这篇博客,可能和之前碰到的关闭数据库连接会产生Crash有些关系,先记录在这, 抽时间研究一下[2015-06-03]

[Android][Security] SSL Pining

起因

上周大家都安静的等待着release,美国客户那边突然要求我们在项目的这个milestone里把certificate pinning加进去。按道理来说,在release前几天不应该再加这种effort比较大的task。后来知道,是因为那边公司的上层要视察,强烈要求。看来领导视察这种活动是普遍存在啊,美国也不例外。没办法,客户就是上帝,我们就硬着头皮做task,连续加班熬夜搞了两晚上。在此,小总结一下。

在我们上网的时候,如果涉及到支付相关的操作,比如说在网上用支付宝买东西,你会发现地址栏中的URL是以https开头的;用Google的很多产品也会发现URL是以https开头;访问本网页,同样,URL也是采用https开头。这是因为这些服务提供商为了保证用户数据的安全性采用的一种措施。HTTPS可以理解是加密了的HTTP请求,在传输过程中会对数据进行加密。现在当你访问一些正规的网站时,若URL是以HTTPS开头的,很多浏览器的地址栏会有一个绿色的锁的标识,表示当前的访问是安全的。如下图:
image

SSL 与 HTTPS

SSL(Secure Sockets Layer 安全套接层)是为网络通信提供安全及数据完整性的一种安全协议。需要注意一点是,它是在传输层对网络连接进行加密。SSL协议的优势在于它是与应用层协议独立无关的。高层的应用层协议(例如:HTTP、FTP、Telnet等等)能透明的建立于SSL协议之上。SSL协议在应用层协议通信之前就已经完成加密算法、通信密钥的协商以及服务器认证工作。在此之后应用层协议所传送的数据都会被加密,从而保证通信的私密性。SSL协议是基于非对称加密算法的。关于非对称加密,稍后会讲到。
HTTPS(Hypertext Transfer Protocol Secure)安全超文本传输协议是一个属于应用层的协议,HTTPS是以安全为目标的HTTP通道,简单讲是HTTP的安全版。即HTTP下加入SSL层,HTTPS的安全基础是SSL。

非对称加密
了解非对称加密之前先了解一下对称加密。简单来讲,对称加密的算法的加密和解密都是采用同一个密钥。如数据A,通过使用密钥B,加密成为密文C。任何人,只要获得了密钥B,就能够对截获的密文C解密,还原出源数据A。
而非对称加密算法需要两个密钥:公开密钥(publickey)和私有密钥(privatekey)。公开密钥与私有密钥是一对,如果用公开密钥对数据进行加密,只有用对应的私有密钥才能解密;如果用私有密钥对数据进行加密,那么只有用对应的公开密钥才能解密。因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。

HTTPS通信流程

1,客户端向服务端发出请求,服务端将公钥(以及服务端证书)响应给客户端;
2,客户端接收到服务器端端公钥与证书,验证证书是否在信任域内,不信任则结束通信,信任则使用服务端传过来的公钥生成一个"预备主密码",返回给服务端。
3,服务端接收客户端传过来的"预备主密码"密文,使用私钥解密。非对称加密的安全性也就在于此了,第三方无法获取到"预备主密码"的明文,因为除了服务端,其他任何人是没有私钥的。
4,双方使用"预备主密码"生成用于会话的"主密码"。确认后,结束本次握手,停止使用非对称加密。
5,双方使用"主密码"对称加密传输数据,直到本次会话结束。

需要注意的

1 HTTPS是基于SSL的,在SSL通信过程中,客户端是基于数字证书判断服务器是否可信,并采用证书的公钥与服务器进行加密通信。
2 在客户端是维护着一个证书的列表。以浏览器为例,浏览器中都保存着一个证书列表。我们都可以通过查看浏览器的设置选项来查看浏览器中内嵌的证书。客户端在验证证书的时候会将服务器端的证书和本地的证书进行匹配,如果匹配成功,那么就认为当前访问的证站是可信的,便继续访问获取站点的数据。

如果想更多的了解SSL/HTTPS, 可以看以下几个链接:

看一下证书

Android中,当我们采用HTTPS与服务器进行通信时,我们会向服务器发一个HTTPS请求。一个HTTPS请求可以分为两部分,第一部份是建立连接,拿到证书,进行证书的验证;第二部份则是读取Server端的数据。一段代码,来点直观印象:

  • 封装自己的SSLSocketFactory
public class MySSLSocketFactory extends SSLSocketFactory{
    public static final String TAG = "MySSLSocketFactory";
    SSLContext sslContext = SSLContext.getInstance("TLS");

    public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException,
            KeyManagementException, KeyStoreException,
            UnrecoverableKeyException {
        super(truststore);
        TrustManager tm = new X509TrustManager() {
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                // TODO Auto-generated method stub
                return null;
            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String authType)
                    throws CertificateException {
                //此处返回的证书在一个证书链中,该证书链包含了多个证书。若自己公司向第三方证书机构申  
               //请了证书,那么chain[0]是自己公司的证书,数组后面的几个元素是第三方证书机构的证书。在实际项目中我们比对的是chain[0].
                for (int i = 0 ; i < chain.length; i ++) {
                    Log.i("Certificate" + (i + 1), chain[0].getPublicKey().toString());
                }
            }

            @Override
            public void checkClientTrusted(X509Certificate[] chain, String authType)
                    throws CertificateException {
            }
        };
        sslContext.init(null, new TrustManager[] { tm }, null);
    }
    @Override
    public Socket createSocket() throws IOException {
        return sslContext.getSocketFactory().createSocket();
    }
    @Override
    public Socket createSocket(Socket socket, String host, int port,
            boolean autoClose) throws IOException, UnknownHostException {
        return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
    }

   public static HttpClient getHttpClient () {
       try {
           KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
           trustStore.load(null, null);
           HttpParams params = new BasicHttpParams();
           HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
           HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);

           MySSLSocketFactory sf = new MySSLSocketFactory(trustStore);
           //信任所有的主机,这是一种很不安全的做法,此时我们为了连接上主机并拿到证书,先这么做。
           sf.setHostnameVerifier(MySSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

           SchemeRegistry registry = new SchemeRegistry();
           registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
           registry.register(new Scheme("https", sf, 443));
           ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);
           return new DefaultHttpClient(ccm, params);
       } catch (Exception e) {
           e.printStackTrace();
           return new DefaultHttpClient();
       }
   }
}
  • 调用的Activity
public class MainActivity extends Activity {
    private Button btnPin;
    private String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.i("SSL", "password");
        btnPin = (Button)findViewById(R.id.btn_pin);
        btnPin.setOnClickListener(clickListener);
}

    private View.OnClickListener clickListener = new View.OnClickListener() {

        @Override
        public void onClick(View view) {
            new Thread() {
                @Override
                public void run() {
                    HttpClient client = MySSLSocketFactory.getHttpClient(); 
                    // create
                    client.getParams().setParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, 5000);
                    client.getParams().setParameter(CoreConnectionPNames.SO_TIMEOUT, 5000);
                    HttpGet get = new HttpGet("https://github.com/fred-ye/summary/issues/22");
                    try {
                        Log.i(TAG, "execute--->");
                        client.execute(get);
                    } catch (UnknownHostException e) {
                        Log.i(TAG, "UnknownHostException--->" + e.getMessage());
                    } catch (ConnectException e) {
                        Log.i(TAG, "ConnectException--->" + e.getMessage());
                    } catch (Exception e) {
                        Log.i(TAG, e.getMessage());
                    }
                }
            }.start();
        }
    };
}
  • 执行后的结果如下:
    image

右侧内容便是server端返回的公钥。

证书绑定Certificate Pinning

Certificate Pinning也就是SSL Pinning。其原理是在本地(客户端)保存有一份证书,当访问Server的时候,将本地证书和从Server端获取到的证书进行比对,如果比对成功,继续完成请求数据的操作。采用这种方式,服务器端可以自己实现一个证书,同时在Server端也保留一份。但有一个缺点便是,当Server端的证书一旦更新,客户端(app)便需要更新证书了。之前的app可能就不能用了。

从Server端获取证书的操作我们已经实现了,接着看如何进行证书的校验。需要重写我们自定义的MySSLSocketFactory类, 其实主要是我们的重写X509TrustManager的实现方式。改动如下:

import android.util.Log;

import org.apache.http.HttpVersion;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;

import java.io.IOException;
import java.math.BigInteger;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPublicKey;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

public class MySSLSocketFactory extends SSLSocketFactory {
    private static final String TAG = "SSL";
    SSLContext sslContext = SSLContext.getInstance("TLS");

    public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException,
            KeyManagementException, KeyStoreException, UnrecoverableKeyException {
        super(truststore);
        TrustManager tm = new X509TrustManager() {
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }

            @Override
            public void checkClientTrusted(X509Certificate[] chain,
                                           String authType) throws java.security.cert.CertificateException {
            }

            @Override
            public void checkServerTrusted(java.security.cert.X509Certificate[] chain,
                                           String authType) throws java.security.cert.CertificateException {
                if (null == chain || 0 == chain.length) {
                    throw new CertificateException("Certificate chain is invalid.");
                } else if (null == authType || 0 == authType.length()) {
                    throw new CertificateException("Authentication type is invalid.");
                } else {
                    for (X509Certificate cert : chain) {
                        cert.checkValidity();
                        String publicKeyStr = getPublicKeyStr(cert.getPublicKey());
                        //TDDO 开始进行公钥的比对,将拿到的公钥和本地hard code的一个公钥进行比对。通常我们会将公钥进行md5或sha摘要运算后存在本地,
                        // 如果比对失败就抛一个CertificateException出去,此处就省略比对逻辑。
                        Log.i(TAG, "SSL public key:" +  publicKeyStr);

                    }
                }
                throw new CustomAbortHttpRequestException("Check certificate finished, abort http request!");
            }
        };
        sslContext.init(null, new TrustManager[]{tm}, null);
    }

    @Override
    public Socket createSocket(Socket socket, String host, int port, boolean autoClose)
            throws IOException {
        return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
    }

    @Override
    public Socket createSocket() throws IOException {
        return sslContext.getSocketFactory().createSocket();
    }

    /**
     * get new HttpClient
     *
     * @return
     */
    public static HttpClient getNewHttpClient() {
        DefaultHttpClient defaultHttpClient = null;
        try {
            KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
            trustStore.load(null, null);

            MySSLSocketFactory sf = new MySSLSocketFactory(trustStore);
            sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);

            HttpParams params = new BasicHttpParams();
            HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
            HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);

            SchemeRegistry registry = new SchemeRegistry();
            registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
            registry.register(new Scheme("https", sf, 443));

            ClientConnectionManager clientConnectionManager = new ThreadSafeClientConnManager(params, registry);

            defaultHttpClient = new DefaultHttpClient(clientConnectionManager, params);
        } catch (Exception e) {
            Log.e(TAG, "init check pin HttpClient exception: " + e.getMessage());
        }

        return defaultHttpClient;
    }
    //获取public key 的值。
    public static String getPublicKeyStr(PublicKey publicKey) {
        RSAPublicKey rsaPublicKeyKey = (RSAPublicKey) publicKey;
        BigInteger module = new BigInteger(rsaPublicKeyKey.getModulus().toString());
        return module.toString(16);
    }
}
  • 关于代码
   throw new RuntimeException("Check certificate finished, abort http request!");

这个是为了当我们一校验完证书之后,得到了校验结果,立马终止当前的请求,提高响应速度。因为我们的目的只在证书校验。

如上面所说的那样,这种做法存在着一个问题,就是必须要在本地hard code一个public key 用来与实时拿到的证书的public key进行比对。如果server端的证书换了,这种做法就悲剧了。这种做法其实在乌云上面有一个实现方式。采用这种方式一个问题就是如何更安全的存储这个值。在这个链接中也提到了,就是将证书存到keystore中。

在项目中,我们的app会和三家公司的server进行通信,如果有一台server的证书换了,我们就要升级app了。后来我们换了另外一种实现方式,避免server换证书导致的app不能用这个问题。

大概思路如下:

  1. 我们app本地在内存中hard code一个值,这个值是由一个RSA算法产生的public key.
  2. app每次在启动的时候会从我们指定的一个url上下载一段文本,这段文本中包含了三个server的public key。当然这三个public key都是经过一定处理后拼接成一个字符串。对这个字符串采用RSA加密,使用私钥加密,这个私钥是和步聚1中的公钥对应的。将加密后的数据存到这个文本中的。拿到这段文本后,我们会用步骤1里面的public key对其进行解密,这样我们便拿到了三个server的public key。
  3. 每次当我们访问api是,我们可以从checkServerTrusted这个方法中拿到public key, 然后与2中得到的三个server的public key进行比对,如果一致,则认为SSL Pinning成功,否则便是失败,终止请求。

RSA 算法

RSA算法用得还是蛮多的,这里简单记录一下,在Android中如何用RSA算法进行加密和解密。

   public void testRSA() throws Exception {
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); 
        kpg.initialize(1024);
        KeyPair kp = kpg.genKeyPair(); //生成密钥对
        PublicKey publicKey = kp.getPublic(); //获得公钥
        PrivateKey privateKey = kp.getPrivate(); //获得私钥
        String publicKeyStr = Base64.encodeToString(publicKey.getEncoded(), Base64.DEFAULT);
        Log.i(TAG, "publicKeyStr:" + publicKeyStr);
        String privateKeyStr = Base64.encodeToString(privateKey.getEncoded(), Base64.DEFAULT);
        Log.i(TAG, "privateKeyStr:" + privateKeyStr);
        String encryptedData = encrypteWithPrivateKey("HelloWorld", privateKeyStr);
        decryptWithPublicKey(encryptedData, publicKeyStr);
    } 
    public String encrypteWithPrivateKey(String plaintext, String privateKeystr) throws GeneralSecurityException {
        byte [] keyBytes = Base64.decode(privateKeystr, Base64.DEFAULT);
        PKCS8EncodedKeySpec x509KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        Key privateKey = keyFactory.generatePrivate(x509KeySpec);
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, privateKey);
        byte [] data = cipher.doFinal(plaintext.getBytes());
        String encryptedData = Base64.encodeToString(data, Base64.DEFAULT);
        Log.i(TAG, "encrypted string:" + encryptedData) ;
        return encryptedData;
    }
    public void decryptWithPublicKey(String data, String publicKeyStr)
            throws GeneralSecurityException {
        byte [] keyBytes = Base64.decode(publicKeyStr, Base64.DEFAULT);
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        Key publicKey = keyFactory.generatePublic(x509KeySpec);
        String publicKeyStr2= Base64.encodeToString(publicKey.getEncoded(), Base64.DEFAULT);
        Log.i(TAG, "publicKeyStr2:" + publicKeyStr2);
        Cipher cipher = Cipher.getInstance("RSA");
        //在实际开发过程中有碰到过BadPaddingException, 这是因为不同平台(java和android)RSA算法的加密模式和填充方式有点差异导致。采用下面一行代码就可以了。
       // Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        cipher.init(Cipher.DECRYPT_MODE, publicKey);
        byte [] dataBytes = Base64.decode(data, Base64.DEFAULT);
        Log.i(TAG, "decrypted string:" + new String(cipher.doFinal(dataBytes))) ;
    }

我们的应用场景

我们当前的项目是一款移动支付项目。会调用银行提供的接口,数据提交到银行的操作是调银行那边提供的一个jar包里的方法,应该是采用Socket通信,将数据发到银行后台的Server,为了安全,我们在调银行的接口之前先检测一下银行提供的一个URL的证书。确认安全后再调用提交数据的方法,将数据提交上去。

假设现在用户要购买一个商品,通过我们的app已经输入了卡号等信息,走到了最后一步,只要点确认这个按钮便会调api,将请求支付的信息发给银行,银行就要开始扣钱了。我们的程序设计是这样子的:

  1. 向这个api对应的server发请求,拿证书,做SSL Pinning。 若SSL Pinning成功了,接着做第二步。 如果失败了,直接告诉用户"证书错误"。
  2. 将交易信息提交给这个api,若提交成功,本次交易成功;若提交失败,本次交易失败。
    也就是说,我们的每次做一个操作,会发两次api请求,第一次是做证书验证,第二次才是将业务数据发送出去。两次http请求构造httpclient的SSLSocketFactory是不一样的,第一个里面需要检测证书,第二个里面就没有必要做证书检测了。其实如果说没有调用第三方的jar里面的东西,直接采用http提交数据,我们只需要发一次请求就可以,就是在发这个请求的同时进行证书验证。

[Android] AchartEngine实现虚线图表

之前一个项目中采用了AchartEngine这个框架实现报表功能,但由于UX设计时,图表中的线是虚线,但AchartEngine中默认是没有虚线表格这种样式的。为了满足UX的设计,于是只有读源码,通过修改源码的方式来实现。因为我们项目中画的图表利用的是Achartengine中的XYChart这个类,通过研究发现,只需要做如下修改,便可以达到想要的效果:

  • 找到drawXTextLabels方法,将代码块
if (showCustomTextGridX) {
   paint.setColor(mRenderer.getGridColor(0));
   canvas.drawLine(xLabel, bottom, xLabel, top, paint);
}

改为如下:

if (showCustomTextGridX) {
    paint.setColor(mRenderer.getGridColor(0));
    PathEffect effect = new DashPathEffect(new float[] {5, 5, 5, 1}, 1);
    paint.setPathEffect(effect);
    canvas.drawLine(xLabel, bottom, xLabel, top, paint);
    paint.setPathEffect(null);
}
  • 将390行处的
if (showCustomTextGridY) {
    paint.setColor(mRenderer.getGridColor(i));
    canvas.drawLine(left, yLabel, right, yLabel, paint);
}

改为

if (showCustomTextGridY) {
    paint.setColor(mRenderer.getGridColor(i));
    PathEffect effect = new DashPathEffect(new float[] { 2, 1, 2, 1 }, 1);
    paint.setPathEffect(effect);
   canvas.drawLine(left, yLabel, right, yLabel, paint);
}

最终修改后的XYChart文件如下 :

/**
 * Copyright (C) 2009 - 2013 SC 4ViewSoft SRL
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.achartengine.chart;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.SortedMap;

import org.achartengine.model.Point;
import org.achartengine.model.SeriesSelection;
import org.achartengine.model.XYMultipleSeriesDataset;
import org.achartengine.model.XYSeries;
import org.achartengine.renderer.BasicStroke;
import org.achartengine.renderer.DefaultRenderer;
import org.achartengine.renderer.SimpleSeriesRenderer;
import org.achartengine.renderer.XYMultipleSeriesRenderer;
import org.achartengine.renderer.XYMultipleSeriesRenderer.Orientation;
import org.achartengine.renderer.XYSeriesRenderer;
import org.achartengine.util.MathHelper;

import android.graphics.Canvas;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Join;
import android.graphics.Paint.Style;
import android.graphics.PathEffect;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Typeface;

/**
 * The XY chart rendering class.
 */
public abstract class XYChart extends AbstractChart {
    /** The multiple series dataset. */
    protected XYMultipleSeriesDataset mDataset;
    /** The multiple series renderer. */
    protected XYMultipleSeriesRenderer mRenderer;
    /** The current scale value. */
    private float mScale;
    /** The current translate value. */
    private float mTranslate;
    /** The canvas center point. */
    private Point mCenter;
    /** The visible chart area, in screen coordinates. */
    private Rect mScreenR;
    /** The calculated range. */
    private final Map<Integer, double[]> mCalcRange = new HashMap<Integer, double[]>();

    /**
     * The clickable areas for all points. The array index is the series index,
     * and the RectF list index is the point index in that series.
     */
    private Map<Integer, List<ClickableArea>> clickableAreas = new HashMap<Integer, List<ClickableArea>>();

    protected XYChart() {
    }

    /**
     * Builds a new XY chart instance.
     *
     * @param dataset
     *            the multiple series dataset
     * @param renderer
     *            the multiple series renderer
     */
    public XYChart(XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer) {
        mDataset = dataset;
        mRenderer = renderer;
    }

    // TODO: javadoc
    protected void setDatasetRenderer(XYMultipleSeriesDataset dataset, XYMultipleSeriesRenderer renderer) {
        mDataset = dataset;
        mRenderer = renderer;
    }

    /**
     * The graphical representation of the XY chart.
     *
     * @param canvas
     *            the canvas to paint to
     * @param x
     *            the top left x value of the view to draw to
     * @param y
     *            the top left y value of the view to draw to
     * @param width
     *            the width of the view to draw to
     * @param height
     *            the height of the view to draw to
     * @param paint
     *            the paint
     */
    @Override
    public void draw(Canvas canvas, int x, int y, int width, int height, Paint paint) {
        paint.setAntiAlias(mRenderer.isAntialiasing());
        int legendSize = getLegendSize(mRenderer, height / 5, mRenderer.getAxisTitleTextSize());
        int[] margins = mRenderer.getMargins();
        int left = x + margins[1];
        int top = y + margins[0];
        int right = x + width - margins[3];
        int sLength = mDataset.getSeriesCount();
        String[] titles = new String[sLength];
        for (int i = 0; i < sLength; i++) {
            titles[i] = mDataset.getSeriesAt(i).getTitle();
        }
        if (mRenderer.isFitLegend() && mRenderer.isShowLegend()) {
            legendSize = drawLegend(canvas, mRenderer, titles, left, right, y, width, height, legendSize, paint, true);
        }
        int bottom = y + height - margins[2] - legendSize;
        if (mScreenR == null) {
            mScreenR = new Rect();
        }
        mScreenR.set(left, top, right, bottom);
        drawBackground(mRenderer, canvas, x, y, width, height, paint, false, DefaultRenderer.NO_COLOR);

        if (paint.getTypeface() == null
                || (mRenderer.getTextTypeface() != null && paint.getTypeface().equals(mRenderer.getTextTypeface()))
                || !paint.getTypeface().toString().equals(mRenderer.getTextTypefaceName())
                || paint.getTypeface().getStyle() != mRenderer.getTextTypefaceStyle()) {
            if (mRenderer.getTextTypeface() != null) {
                paint.setTypeface(mRenderer.getTextTypeface());
            } else {
                paint.setTypeface(Typeface.create(mRenderer.getTextTypefaceName(), mRenderer.getTextTypefaceStyle()));
            }
        }
        Orientation or = mRenderer.getOrientation();
        if (or == Orientation.VERTICAL) {
            right -= legendSize;
            bottom += legendSize - 20;
        }
        int angle = or.getAngle();
        boolean rotate = angle == 90;
        mScale = (float) (height) / width;
        mTranslate = Math.abs(width - height) / 2;
        if (mScale < 1) {
            mTranslate *= -1;
        }
        mCenter = new Point((x + width) / 2, (y + height) / 2);
        if (rotate) {
            transform(canvas, angle, false);
        }

        int maxScaleNumber = -Integer.MAX_VALUE;
        for (int i = 0; i < sLength; i++) {
            maxScaleNumber = Math.max(maxScaleNumber, mDataset.getSeriesAt(i).getScaleNumber());
        }
        maxScaleNumber++;
        if (maxScaleNumber < 0) {
            return;
        }
        double[] minX = new double[maxScaleNumber];
        double[] maxX = new double[maxScaleNumber];
        double[] minY = new double[maxScaleNumber];
        double[] maxY = new double[maxScaleNumber];
        boolean[] isMinXSet = new boolean[maxScaleNumber];
        boolean[] isMaxXSet = new boolean[maxScaleNumber];
        boolean[] isMinYSet = new boolean[maxScaleNumber];
        boolean[] isMaxYSet = new boolean[maxScaleNumber];

        for (int i = 0; i < maxScaleNumber; i++) {
            minX[i] = mRenderer.getXAxisMin(i);
            maxX[i] = mRenderer.getXAxisMax(i);
            minY[i] = mRenderer.getYAxisMin(i);
            maxY[i] = mRenderer.getYAxisMax(i);
            isMinXSet[i] = mRenderer.isMinXSet(i);
            isMaxXSet[i] = mRenderer.isMaxXSet(i);
            isMinYSet[i] = mRenderer.isMinYSet(i);
            isMaxYSet[i] = mRenderer.isMaxYSet(i);
            if (mCalcRange.get(i) == null) {
                mCalcRange.put(i, new double[4]);
            }
        }
        double[] xPixelsPerUnit = new double[maxScaleNumber];
        double[] yPixelsPerUnit = new double[maxScaleNumber];
        for (int i = 0; i < sLength; i++) {
            XYSeries series = mDataset.getSeriesAt(i);
            int scale = series.getScaleNumber();
            if (series.getItemCount() == 0) {
                continue;
            }
            if (!isMinXSet[scale]) {
                double minimumX = series.getMinX();
                minX[scale] = Math.min(minX[scale], minimumX);
                mCalcRange.get(scale)[0] = minX[scale];
            }
            if (!isMaxXSet[scale]) {
                double maximumX = series.getMaxX();
                maxX[scale] = Math.max(maxX[scale], maximumX);
                mCalcRange.get(scale)[1] = maxX[scale];
            }
            if (!isMinYSet[scale]) {
                double minimumY = series.getMinY();
                minY[scale] = Math.min(minY[scale], (float) minimumY);
                mCalcRange.get(scale)[2] = minY[scale];
            }
            if (!isMaxYSet[scale]) {
                double maximumY = series.getMaxY();
                maxY[scale] = Math.max(maxY[scale], (float) maximumY);
                mCalcRange.get(scale)[3] = maxY[scale];
            }
        }
        for (int i = 0; i < maxScaleNumber; i++) {
            if (maxX[i] - minX[i] != 0) {
                xPixelsPerUnit[i] = (right - left) / (maxX[i] - minX[i]);
            }
            if (maxY[i] - minY[i] != 0) {
                yPixelsPerUnit[i] = (float) ((bottom - top) / (maxY[i] - minY[i]));
            }
            // the X axis on multiple scales was wrong without this fix
            if (i > 0) {
                xPixelsPerUnit[i] = xPixelsPerUnit[0];
                minX[i] = minX[0];
                maxX[i] = maxX[0];
            }
        }

        boolean hasValues = false;
        // use a linked list for these reasons:
        // 1) Avoid a large contiguous memory allocation
        // 2) We don't need random seeking, only sequential reading/writing, so
        // linked list makes sense
        clickableAreas = new HashMap<Integer, List<ClickableArea>>();
        for (int i = 0; i < sLength; i++) {
            XYSeries series = mDataset.getSeriesAt(i);
            int scale = series.getScaleNumber();
            if (series.getItemCount() == 0) {
                continue;
            }

            hasValues = true;
            XYSeriesRenderer seriesRenderer = (XYSeriesRenderer) mRenderer.getSeriesRendererAt(i);

            // int originalValuesLength = series.getItemCount();
            // int valuesLength = originalValuesLength;
            // int length = valuesLength * 2;

            List<Float> points = new ArrayList<Float>();
            List<Double> values = new ArrayList<Double>();
            float yAxisValue = Math.min(bottom, (float) (bottom + yPixelsPerUnit[scale] * minY[scale]));
            LinkedList<ClickableArea> clickableArea = new LinkedList<ClickableArea>();

            clickableAreas.put(i, clickableArea);

            synchronized (series) {
                SortedMap<Double, Double> range = series.getRange(minX[scale], maxX[scale],
                        seriesRenderer.isDisplayBoundingPoints());
                int startIndex = -1;

                for (Entry<Double, Double> value : range.entrySet()) {
                    double xValue = value.getKey();
                    double yValue = value.getValue();
                    if (startIndex < 0 && (!isNullValue(yValue) || isRenderNullValues())) {
                        startIndex = series.getIndexForKey(xValue);
                    }

                    // points.add((float) (left + xPixelsPerUnit[scale]
                    // * (value.getKey().floatValue() - minX[scale])));
                    // points.add((float) (bottom - yPixelsPerUnit[scale]
                    // * (value.getValue().floatValue() - minY[scale])));
                    values.add(value.getKey());
                    values.add(value.getValue());

                    if (!isNullValue(yValue)) {
                        points.add((float) (left + xPixelsPerUnit[scale] * (xValue - minX[scale])));
                        points.add((float) (bottom - yPixelsPerUnit[scale] * (yValue - minY[scale])));
                    } else if (isRenderNullValues()) {
                        points.add((float) (left + xPixelsPerUnit[scale] * (xValue - minX[scale])));
                        points.add((float) (bottom - yPixelsPerUnit[scale] * (-minY[scale])));
                    } else {
                        if (points.size() > 0) {
                            drawSeries(series, canvas, paint, points, seriesRenderer, yAxisValue, i, or, startIndex);
                            ClickableArea[] clickableAreasForSubSeries = clickableAreasForPoints(points, values,
                                    yAxisValue, i, startIndex);
                            clickableArea.addAll(Arrays.asList(clickableAreasForSubSeries));
                            points.clear();
                            values.clear();
                            startIndex = -1;
                        }
                        clickableArea.add(null);
                    }
                }

                int count = series.getAnnotationCount();
                if (count > 0) {
                    paint.setColor(seriesRenderer.getAnnotationsColor());
                    paint.setTextSize(seriesRenderer.getAnnotationsTextSize());
                    paint.setTextAlign(seriesRenderer.getAnnotationsTextAlign());
                    Rect bound = new Rect();
                    for (int j = 0; j < count; j++) {
                        float xS = (float) (left + xPixelsPerUnit[scale] * (series.getAnnotationX(j) - minX[scale]));
                        float yS = (float) (bottom - yPixelsPerUnit[scale] * (series.getAnnotationY(j) - minY[scale]));
                        paint.getTextBounds(series.getAnnotationAt(j), 0, series.getAnnotationAt(j).length(), bound);
                        if (xS < (xS + bound.width()) && yS < canvas.getHeight()) {
                            drawString(canvas, series.getAnnotationAt(j), xS, yS, paint);
                        }
                    }
                }

                if (points.size() > 0) {
                    drawSeries(series, canvas, paint, points, seriesRenderer, yAxisValue, i, or, startIndex);
                    ClickableArea[] clickableAreasForSubSeries = clickableAreasForPoints(points, values, yAxisValue, i,
                            startIndex);
                    clickableArea.addAll(Arrays.asList(clickableAreasForSubSeries));
                }
            }
        }
        // draw stuff over the margins such as data doesn't render on these
        // areas
        drawBackground(mRenderer, canvas, x, bottom, width, height - bottom, paint, true, mRenderer.getMarginsColor());
        drawBackground(mRenderer, canvas, x, y, width, margins[0], paint, true, mRenderer.getMarginsColor());
        if (or == Orientation.HORIZONTAL) {
            drawBackground(mRenderer, canvas, x, y, left - x, height - y, paint, true, mRenderer.getMarginsColor());
            drawBackground(mRenderer, canvas, right, y, margins[3], height - y, paint, true,
                    mRenderer.getMarginsColor());
        } else if (or == Orientation.VERTICAL) {
            drawBackground(mRenderer, canvas, right, y, width - right, height - y, paint, true,
                    mRenderer.getMarginsColor());
            drawBackground(mRenderer, canvas, x, y, left - x, height - y, paint, true, mRenderer.getMarginsColor());
        }

        boolean showLabels = mRenderer.isShowLabels() && hasValues;
        boolean showGridX = mRenderer.isShowGridX();
        boolean showTickMarks = mRenderer.isShowTickMarks();
        // boolean showCustomTextGridX = mRenderer.isShowCustomTextGridX();
        boolean showCustomTextGridY = mRenderer.isShowCustomTextGridY();
        if (showLabels || showGridX) {
            List<Double> xLabels = getValidLabels(getXLabels(minX[0], maxX[0], mRenderer.getXLabels()));
            Map<Integer, List<Double>> allYLabels = getYLabels(minY, maxY, maxScaleNumber);

            int xLabelsLeft = left;
            if (showLabels) {
                paint.setColor(mRenderer.getXLabelsColor());
                paint.setTextSize(mRenderer.getLabelsTextSize());
                paint.setTextAlign(mRenderer.getXLabelsAlign());
                // if (mRenderer.getXLabelsAlign() == Align.LEFT) {
                // xLabelsLeft += mRenderer.getLabelsTextSize() / 4;
                // }
            }
            drawXLabels(xLabels, mRenderer.getXTextLabelLocations(), canvas, paint, xLabelsLeft, top, bottom,
                    xPixelsPerUnit[0], minX[0], maxX[0]);
            drawYLabels(allYLabels, canvas, paint, maxScaleNumber, left, right, bottom, yPixelsPerUnit, minY);
            if (showLabels) {
                paint.setColor(mRenderer.getLabelsColor());
                for (int i = 0; i < maxScaleNumber; i++) {
                    Log.i("Achart-Source", "----1-----");
                    Align axisAlign = mRenderer.getYAxisAlign(i);
                    Double[] yTextLabelLocations = mRenderer.getYTextLabelLocations(i);
                    for (Double location : yTextLabelLocations) {
                        if (minY[i] <= location && location <= maxY[i]) {
                            float yLabel = (float) (bottom - yPixelsPerUnit[i] * (location.doubleValue() - minY[i]));
                            String label = mRenderer.getYTextLabel(location, i);
                            paint.setColor(mRenderer.getYLabelsColor(i));
                            paint.setTextAlign(mRenderer.getYLabelsAlign(i));
                            if (or == Orientation.HORIZONTAL) {
                                if (axisAlign == Align.LEFT) {
                                    if (showTickMarks) {
                                        canvas.drawLine(left + getLabelLinePos(axisAlign), yLabel, left, yLabel, paint);
                                    }
                                    drawText(canvas, label, left, yLabel - mRenderer.getYLabelsVerticalPadding(),
                                            paint, mRenderer.getYLabelsAngle());
                                } else {
                                    if (showTickMarks) {
                                        canvas.drawLine(right, yLabel, right + getLabelLinePos(axisAlign), yLabel,
                                                paint);
                                    }
                                    drawText(canvas, label, right, yLabel - mRenderer.getYLabelsVerticalPadding(),
                                            paint, mRenderer.getYLabelsAngle());
                                }

                                if (showCustomTextGridY) {
                                    paint.setColor(mRenderer.getGridColor(i));
                                    PathEffect effect = new DashPathEffect(new float[] { 2, 1, 2, 1 }, 1);
                                    paint.setPathEffect(effect);
                                    canvas.drawLine(left, yLabel, right, yLabel, paint);
                                }
                            } else {
                                if (showTickMarks) {
                                    canvas.drawLine(right - getLabelLinePos(axisAlign), yLabel, right, yLabel, paint);
                                }
                                drawText(canvas, label, right + 10, yLabel - mRenderer.getYLabelsVerticalPadding(),
                                        paint, mRenderer.getYLabelsAngle());
                                if (showCustomTextGridY) {
                                    paint.setColor(mRenderer.getGridColor(i));
                                    canvas.drawLine(right, yLabel, left, yLabel, paint);
                                }
                            }
                        }
                    }
                }
            }

            if (showLabels) {
                paint.setColor(mRenderer.getLabelsColor());
                float size = mRenderer.getAxisTitleTextSize();
                paint.setTextSize(size);
                paint.setTextAlign(Align.CENTER);
                if (or == Orientation.HORIZONTAL) {
                    drawText(canvas, mRenderer.getXTitle(), x + width / 2, bottom + mRenderer.getLabelsTextSize() * 4
                            / 3 + mRenderer.getXLabelsPadding() + size, paint, 0);
                    for (int i = 0; i < maxScaleNumber; i++) {
                        Align axisAlign = mRenderer.getYAxisAlign(i);
                        if (axisAlign == Align.LEFT) {
                            drawText(canvas, mRenderer.getYTitle(i), x + size, y + height / 2, paint, -90);
                        } else {
                            drawText(canvas, mRenderer.getYTitle(i), x + width, y + height / 2, paint, -90);
                        }
                    }
                    paint.setTextSize(mRenderer.getChartTitleTextSize());
                    drawText(canvas, mRenderer.getChartTitle(), x + width / 2, y + mRenderer.getChartTitleTextSize(),
                            paint, 0);
                } else if (or == Orientation.VERTICAL) {
                    drawText(canvas, mRenderer.getXTitle(), x + width / 2,
                            y + height - size + mRenderer.getXLabelsPadding(), paint, -90);
                    drawText(canvas, mRenderer.getYTitle(), right + 20, y + height / 2, paint, 0);
                    paint.setTextSize(mRenderer.getChartTitleTextSize());
                    drawText(canvas, mRenderer.getChartTitle(), x + size, top + height / 2, paint, 0);
                }
            }
        }
        if (or == Orientation.HORIZONTAL) {
            drawLegend(canvas, mRenderer, titles, left, right, y + (int) mRenderer.getXLabelsPadding(), width, height,
                    legendSize, paint, false);
        } else if (or == Orientation.VERTICAL) {
            transform(canvas, angle, true);
            drawLegend(canvas, mRenderer, titles, left, right, y + (int) mRenderer.getXLabelsPadding(), width, height,
                    legendSize, paint, false);
            transform(canvas, angle, false);
        }
        if (mRenderer.isShowAxes()) {
            paint.setColor(mRenderer.getXAxisColor());
            canvas.drawLine(left, bottom, right, bottom, paint);
            paint.setColor(mRenderer.getYAxisColor());
            boolean rightAxis = false;
            for (int i = 0; i < maxScaleNumber && !rightAxis; i++) {
                rightAxis = mRenderer.getYAxisAlign(i) == Align.RIGHT;
            }
            if (or == Orientation.HORIZONTAL) {
                canvas.drawLine(left, top, left, bottom, paint);
                if (rightAxis) {
                    canvas.drawLine(right, top, right, bottom, paint);
                }
            } else if (or == Orientation.VERTICAL) {
                canvas.drawLine(right, top, right, bottom, paint);
            }
        }
        if (rotate) {
            transform(canvas, angle, true);
        }
    }

    protected List<Double> getXLabels(double min, double max, int count) {
        return MathHelper.getLabels(min, max, count);
    }

    protected Map<Integer, List<Double>> getYLabels(double[] minY, double[] maxY, int maxScaleNumber) {
        Map<Integer, List<Double>> allYLabels = new HashMap<Integer, List<Double>>();
        for (int i = 0; i < maxScaleNumber; i++) {
            allYLabels.put(i, getValidLabels(MathHelper.getLabels(minY[i], maxY[i], mRenderer.getYLabels())));
        }
        return allYLabels;
    }

    protected Rect getScreenR() {
        return mScreenR;
    }

    protected void setScreenR(Rect screenR) {
        mScreenR = screenR;
    }

    private List<Double> getValidLabels(List<Double> labels) {
        List<Double> result = new ArrayList<Double>(labels);
        for (Double label : labels) {
            if (label.isNaN()) {
                result.remove(label);
            }
        }
        return result;
    }

    /**
     * Draws the series.
     *
     * @param series
     *            the series
     * @param canvas
     *            the canvas
     * @param paint
     *            the paint object
     * @param pointsList
     *            the points to be rendered
     * @param seriesRenderer
     *            the series renderer
     * @param yAxisValue
     *            the y axis value in pixels
     * @param seriesIndex
     *            the series index
     * @param or
     *            the orientation
     * @param startIndex
     *            the start index of the rendering points
     */
    protected void drawSeries(XYSeries series, Canvas canvas, Paint paint, List<Float> pointsList,
            XYSeriesRenderer seriesRenderer, float yAxisValue, int seriesIndex, Orientation or, int startIndex) {
        BasicStroke stroke = seriesRenderer.getStroke();
        Cap cap = paint.getStrokeCap();
        Join join = paint.getStrokeJoin();
        float miter = paint.getStrokeMiter();
        PathEffect pathEffect = paint.getPathEffect();
        Style style = paint.getStyle();
        if (stroke != null) {
            PathEffect effect = null;
            if (stroke.getIntervals() != null) {
                effect = new DashPathEffect(stroke.getIntervals(), stroke.getPhase());
            }
            setStroke(stroke.getCap(), stroke.getJoin(), stroke.getMiter(), Style.FILL_AND_STROKE, effect, paint);
        }
        // float[] points = MathHelper.getFloats(pointsList);
        drawSeries(canvas, paint, pointsList, seriesRenderer, yAxisValue, seriesIndex, startIndex);
        if (isRenderPoints(seriesRenderer)) {
            ScatterChart pointsChart = getPointsChart();
            if (pointsChart != null) {
                pointsChart.drawSeries(canvas, paint, pointsList, seriesRenderer, yAxisValue, seriesIndex, startIndex);
            }
        }
        paint.setTextSize(seriesRenderer.getChartValuesTextSize());
        if (or == Orientation.HORIZONTAL) {
            paint.setTextAlign(Align.CENTER);
        } else {
            paint.setTextAlign(Align.LEFT);
        }
        if (seriesRenderer.isDisplayChartValues()) {
            paint.setTextAlign(seriesRenderer.getChartValuesTextAlign());
            drawChartValuesText(canvas, series, seriesRenderer, paint, pointsList, seriesIndex, startIndex);
        }
        if (stroke != null) {
            setStroke(cap, join, miter, style, pathEffect, paint);
        }
    }

    private void setStroke(Cap cap, Join join, float miter, Style style, PathEffect pathEffect, Paint paint) {
        paint.setStrokeCap(cap);
        paint.setStrokeJoin(join);
        paint.setStrokeMiter(miter);
        paint.setPathEffect(pathEffect);
        paint.setStyle(style);
    }

    /**
     * The graphical representation of the series values as text.
     *
     * @param canvas
     *            the canvas to paint to
     * @param series
     *            the series to be painted
     * @param renderer
     *            the series renderer
     * @param paint
     *            the paint to be used for drawing
     * @param points
     *            the array of points to be used for drawing the series
     * @param seriesIndex
     *            the index of the series currently being drawn
     * @param startIndex
     *            the start index of the rendering points
     */
    protected void drawChartValuesText(Canvas canvas, XYSeries series, XYSeriesRenderer renderer, Paint paint,
            List<Float> points, int seriesIndex, int startIndex) {
        if (points.size() > 1) { // there are more than one point
            // record the first point's position
            float previousPointX = points.get(0);
            float previousPointY = points.get(1);
            for (int k = 0; k < points.size(); k += 2) {
                if (k == 2) { // decide whether to display first two points'
                              // values or
                              // not
                    if (Math.abs(points.get(2) - points.get(0)) > renderer.getDisplayChartValuesDistance()
                            || Math.abs(points.get(3) - points.get(1)) > renderer.getDisplayChartValuesDistance()) {
                        // first point
                        drawText(canvas, getLabel(renderer.getChartValuesFormat(), series.getY(startIndex)),
                                points.get(0), points.get(1) - renderer.getChartValuesSpacing(), paint, 0);
                        // second point
                        drawText(canvas, getLabel(renderer.getChartValuesFormat(), series.getY(startIndex + 1)),
                                points.get(2), points.get(3) - renderer.getChartValuesSpacing(), paint, 0);

                        previousPointX = points.get(2);
                        previousPointY = points.get(3);
                    }
                } else if (k > 2) {
                    // compare current point's position with the previous
                    // point's, if they
                    // are not too close, display
                    if (Math.abs(points.get(k) - previousPointX) > renderer.getDisplayChartValuesDistance()
                            || Math.abs(points.get(k + 1) - previousPointY) > renderer.getDisplayChartValuesDistance()) {
                        drawText(canvas, getLabel(renderer.getChartValuesFormat(), series.getY(startIndex + k / 2)),
                                points.get(k), points.get(k + 1) - renderer.getChartValuesSpacing(), paint, 0);
                        previousPointX = points.get(k);
                        previousPointY = points.get(k + 1);
                    }
                }
            }
        } else { // if only one point, display it
            for (int k = 0; k < points.size(); k += 2) {
                drawText(canvas, getLabel(renderer.getChartValuesFormat(), series.getY(startIndex + k / 2)),
                        points.get(k), points.get(k + 1) - renderer.getChartValuesSpacing(), paint, 0);
            }
        }
    }

    /**
     * The graphical representation of a text, to handle both HORIZONTAL and
     * VERTICAL orientations and extra rotation angles.
     *
     * @param canvas
     *            the canvas to paint to
     * @param text
     *            the text to be rendered
     * @param x
     *            the X axis location of the text
     * @param y
     *            the Y axis location of the text
     * @param paint
     *            the paint to be used for drawing
     * @param extraAngle
     *            the text angle
     */
    protected void drawText(Canvas canvas, String text, float x, float y, Paint paint, float extraAngle) {
        float angle = -mRenderer.getOrientation().getAngle() + extraAngle;
        if (angle != 0) {
            // canvas.scale(1 / mScale, mScale);
            canvas.rotate(angle, x, y);
        }
        drawString(canvas, text, x, y, paint);
        if (angle != 0) {
            canvas.rotate(-angle, x, y);
            // canvas.scale(mScale, 1 / mScale);
        }
    }

    /**
     * Transform the canvas such as it can handle both HORIZONTAL and VERTICAL
     * orientations.
     *
     * @param canvas
     *            the canvas to paint to
     * @param angle
     *            the angle of rotation
     * @param inverse
     *            if the inverse transform needs to be applied
     */
    private void transform(Canvas canvas, float angle, boolean inverse) {
        if (inverse) {
            canvas.scale(1 / mScale, mScale);
            canvas.translate(mTranslate, -mTranslate);
            canvas.rotate(-angle, mCenter.getX(), mCenter.getY());
        } else {
            canvas.rotate(angle, mCenter.getX(), mCenter.getY());
            canvas.translate(-mTranslate, mTranslate);
            canvas.scale(mScale, 1 / mScale);
        }
    }

    /**
     * The graphical representation of the labels on the X axis.
     *
     * @param xLabels
     *            the X labels values
     * @param xTextLabelLocations
     *            the X text label locations
     * @param canvas
     *            the canvas to paint to
     * @param paint
     *            the paint to be used for drawing
     * @param left
     *            the left value of the labels area
     * @param top
     *            the top value of the labels area
     * @param bottom
     *            the bottom value of the labels area
     * @param xPixelsPerUnit
     *            the amount of pixels per one unit in the chart labels
     * @param minX
     *            the minimum value on the X axis in the chart
     * @param maxX
     *            the maximum value on the X axis in the chart
     */
    protected void drawXLabels(List<Double> xLabels, Double[] xTextLabelLocations, Canvas canvas, Paint paint,
            int left, int top, int bottom, double xPixelsPerUnit, double minX, double maxX) {
        int length = xLabels.size();
        boolean showLabels = mRenderer.isShowLabels();
        boolean showGridY = mRenderer.isShowGridY();
        boolean showTickMarks = mRenderer.isShowTickMarks();
        for (int i = 0; i < length; i++) {
            double label = xLabels.get(i);
            float xLabel = (float) (left + xPixelsPerUnit * (label - minX));
            if (showLabels) {
                paint.setColor(mRenderer.getXLabelsColor());
                if (showTickMarks) {
                    canvas.drawLine(xLabel, bottom, xLabel, bottom + mRenderer.getLabelsTextSize() / 3, paint);
                }
                drawText(canvas, getLabel(mRenderer.getLabelFormat(), label), xLabel,
                        bottom + mRenderer.getLabelsTextSize() * 4 / 3 + mRenderer.getXLabelsPadding(), paint,
                        mRenderer.getXLabelsAngle());
            }

            if (showGridY) {
                paint.setColor(mRenderer.getGridColor(0));
                canvas.drawLine(xLabel, bottom, xLabel, top, paint);
            }
        }
        drawXTextLabels(xTextLabelLocations, canvas, paint, showLabels, left, top, bottom, xPixelsPerUnit, minX, maxX);
    }

    /**
     * The graphical representation of the labels on the Y axis.
     *
     * @param allYLabels
     *            the Y labels values
     * @param canvas
     *            the canvas to paint to
     * @param paint
     *            the paint to be used for drawing
     * @param maxScaleNumber
     *            the maximum scale number
     * @param left
     *            the left value of the labels area
     * @param right
     *            the right value of the labels area
     * @param bottom
     *            the bottom value of the labels area
     * @param yPixelsPerUnit
     *            the amount of pixels per one unit in the chart labels
     * @param minY
     *            the minimum value on the Y axis in the chart
     */
    protected void drawYLabels(Map<Integer, List<Double>> allYLabels, Canvas canvas, Paint paint, int maxScaleNumber,
            int left, int right, int bottom, double[] yPixelsPerUnit, double[] minY) {
        Orientation or = mRenderer.getOrientation();
        boolean showGridX = mRenderer.isShowGridX();
        boolean showLabels = mRenderer.isShowLabels();
        boolean showTickMarks = mRenderer.isShowTickMarks();
        for (int i = 0; i < maxScaleNumber; i++) {
            paint.setTextAlign(mRenderer.getYLabelsAlign(i));
            List<Double> yLabels = allYLabels.get(i);
            int length = yLabels.size();
            for (int j = 0; j < length; j++) {
                double label = yLabels.get(j);
                Align axisAlign = mRenderer.getYAxisAlign(i);
                boolean textLabel = mRenderer.getYTextLabel(label, i) != null;
                float yLabel = (float) (bottom - yPixelsPerUnit[i] * (label - minY[i]));
                if (or == Orientation.HORIZONTAL) {
                    if (showLabels && !textLabel) {
                        paint.setColor(mRenderer.getYLabelsColor(i));
                        if (axisAlign == Align.LEFT) {
                            if (showTickMarks) {
                                canvas.drawLine(left + getLabelLinePos(axisAlign), yLabel, left, yLabel, paint);
                            }
                            drawText(canvas, getLabel(mRenderer.getLabelFormat(), label),
                                    left - mRenderer.getYLabelsPadding(),
                                    yLabel - mRenderer.getYLabelsVerticalPadding(), paint, mRenderer.getYLabelsAngle());
                        } else {
                            if (showTickMarks) {
                                canvas.drawLine(right, yLabel, right + getLabelLinePos(axisAlign), yLabel, paint);
                            }
                            drawText(canvas, getLabel(mRenderer.getLabelFormat(), label),
                                    right + mRenderer.getYLabelsPadding(),
                                    yLabel - mRenderer.getYLabelsVerticalPadding(), paint, mRenderer.getYLabelsAngle());
                        }
                    }
                    if (showGridX) {
//                        paint.setColor(mRenderer.getGridColor(i));
//                        canvas.drawLine(left, yLabel, right, yLabel, paint);
                        paint.setColor(mRenderer.getGridColor(0));
                        PathEffect effect = new DashPathEffect(new float[] {
                         5, 5, 5, 1}, 1);
                        paint.setPathEffect(effect);
                        canvas.drawLine(left, yLabel, right, yLabel, paint);
                        paint.setPathEffect(null);
                    }
                } else if (or == Orientation.VERTICAL) {
                    if (showLabels && !textLabel) {
                        paint.setColor(mRenderer.getYLabelsColor(i));
                        if (showTickMarks) {
                            canvas.drawLine(right - getLabelLinePos(axisAlign), yLabel, right, yLabel, paint);
                        }
                        drawText(canvas, getLabel(mRenderer.getLabelFormat(), label),
                                right + 10 + mRenderer.getYLabelsPadding(),
                                yLabel - mRenderer.getYLabelsVerticalPadding(), paint, mRenderer.getYLabelsAngle());
                    }
                    if (showGridX) {
                        paint.setColor(mRenderer.getGridColor(i));
                        if (showTickMarks) {
                            canvas.drawLine(right, yLabel, left, yLabel, paint);
                        }
                    }
                }
            }
        }
    }

    /**
     * The graphical representation of the text labels on the X axis.
     *
     * @param xTextLabelLocations
     *            the X text label locations
     * @param canvas
     *            the canvas to paint to
     * @param paint
     *            the paint to be used for drawing
     * @param left
     *            the left value of the labels area
     * @param top
     *            the top value of the labels area
     * @param bottom
     *            the bottom value of the labels area
     * @param xPixelsPerUnit
     *            the amount of pixels per one unit in the chart labels
     * @param minX
     *            the minimum value on the X axis in the chart
     * @param maxX
     *            the maximum value on the X axis in the chart
     */
    protected void drawXTextLabels(Double[] xTextLabelLocations, Canvas canvas, Paint paint, boolean showLabels,
            int left, int top, int bottom, double xPixelsPerUnit, double minX, double maxX) {
        boolean showCustomTextGridX = mRenderer.isShowCustomTextGridX();
        boolean showTickMarks = mRenderer.isShowTickMarks();
        if (showLabels) {
            paint.setColor(mRenderer.getXLabelsColor());
            for (Double location : xTextLabelLocations) {
                if (minX <= location && location <= maxX) {
                    float xLabel = (float) (left + xPixelsPerUnit * (location.doubleValue() - minX));
                    paint.setColor(mRenderer.getXLabelsColor());
                    if (showTickMarks) {
                        canvas.drawLine(xLabel, bottom, xLabel, bottom + mRenderer.getLabelsTextSize() / 3, paint);
                    }
                    drawText(canvas, mRenderer.getXTextLabel(location), xLabel, bottom + mRenderer.getLabelsTextSize()
                            * 4 / 3, paint, mRenderer.getXLabelsAngle());
                    if (showCustomTextGridX) {
                        paint.setColor(mRenderer.getGridColor(0));
                        PathEffect effect = new DashPathEffect(new float[] {
                         5, 5, 5, 1}, 1);
                        paint.setPathEffect(effect);
                        canvas.drawLine(xLabel, bottom, xLabel, top, paint);
                        paint.setPathEffect(null);
                    }
                }
            }
        }
    }

    // TODO: docs
    public XYMultipleSeriesRenderer getRenderer() {
        return mRenderer;
    }

    public XYMultipleSeriesDataset getDataset() {
        return mDataset;
    }

    public double[] getCalcRange(int scale) {
        return mCalcRange.get(scale);
    }

    public void setCalcRange(double[] range, int scale) {
        mCalcRange.put(scale, range);
    }

    public double[] toRealPoint(float screenX, float screenY) {
        return toRealPoint(screenX, screenY, 0);
    }

    public double[] toScreenPoint(double[] realPoint) {
        return toScreenPoint(realPoint, 0);
    }

    private int getLabelLinePos(Align align) {
        int pos = 4;
        if (align == Align.LEFT) {
            pos = -pos;
        }
        return pos;
    }

    /**
     * Transforms a screen point to a real coordinates point.
     *
     * @param screenX
     *            the screen x axis value
     * @param screenY
     *            the screen y axis value
     * @return the real coordinates point
     */
    public double[] toRealPoint(float screenX, float screenY, int scale) {
        double realMinX = mRenderer.getXAxisMin(scale);
        double realMaxX = mRenderer.getXAxisMax(scale);
        double realMinY = mRenderer.getYAxisMin(scale);
        double realMaxY = mRenderer.getYAxisMax(scale);
        if (!mRenderer.isMinXSet(scale) || !mRenderer.isMaxXSet(scale) || !mRenderer.isMinXSet(scale)
                || !mRenderer.isMaxYSet(scale)) {
            double[] calcRange = getCalcRange(scale);
            realMinX = calcRange[0];
            realMaxX = calcRange[1];
            realMinY = calcRange[2];
            realMaxY = calcRange[3];
        }
        if (mScreenR != null) {
            return new double[] { (screenX - mScreenR.left) * (realMaxX - realMinX) / mScreenR.width() + realMinX,
                    (mScreenR.top + mScreenR.height() - screenY) * (realMaxY - realMinY) / mScreenR.height() + realMinY };
        } else {
            return new double[] { screenX, screenY };
        }
    }

    public double[] toScreenPoint(double[] realPoint, int scale) {
        double realMinX = mRenderer.getXAxisMin(scale);
        double realMaxX = mRenderer.getXAxisMax(scale);
        double realMinY = mRenderer.getYAxisMin(scale);
        double realMaxY = mRenderer.getYAxisMax(scale);
        if (!mRenderer.isMinXSet(scale) || !mRenderer.isMaxXSet(scale) || !mRenderer.isMinXSet(scale)
                || !mRenderer.isMaxYSet(scale)) {
            double[] calcRange = getCalcRange(scale);
            realMinX = calcRange[0];
            realMaxX = calcRange[1];
            realMinY = calcRange[2];
            realMaxY = calcRange[3];
        }
        if (mScreenR != null) {
            return new double[] { (realPoint[0] - realMinX) * mScreenR.width() / (realMaxX - realMinX) + mScreenR.left,
                    (realMaxY - realPoint[1]) * mScreenR.height() / (realMaxY - realMinY) + mScreenR.top };
        } else {
            return realPoint;
        }
    }

    @Override
    public SeriesSelection getSeriesAndPointForScreenCoordinate(final Point screenPoint) {
        if (clickableAreas != null)
            for (int seriesIndex = clickableAreas.size() - 1; seriesIndex >= 0; seriesIndex--) {
                // series 0 is drawn first. Then series 1 is drawn on top, and
                // series 2
                // on top of that.
                // we want to know what the user clicked on, so traverse them in
                // the
                // order they appear on the screen.
                int pointIndex = 0;
                if (clickableAreas.get(seriesIndex) != null) {
                    RectF rectangle;
                    for (ClickableArea area : clickableAreas.get(seriesIndex)) {
                        if (area != null) {
                            rectangle = area.getRect();
                            if (rectangle != null && rectangle.contains(screenPoint.getX(), screenPoint.getY())) {
                                return new SeriesSelection(seriesIndex, pointIndex, area.getX(), area.getY());
                            }
                        }
                        pointIndex++;
                    }
                }
            }
        return super.getSeriesAndPointForScreenCoordinate(screenPoint);
    }

    /**
     * The graphical representation of a series.
     *
     * @param canvas
     *            the canvas to paint to
     * @param paint
     *            the paint to be used for drawing
     * @param points
     *            the array of points to be used for drawing the series
     * @param seriesRenderer
     *            the series renderer
     * @param yAxisValue
     *            the minimum value of the y axis
     * @param seriesIndex
     *            the index of the series currently being drawn
     * @param startIndex
     *            the start index of the rendering points
     */
    public abstract void drawSeries(Canvas canvas, Paint paint, List<Float> points, XYSeriesRenderer seriesRenderer,
            float yAxisValue, int seriesIndex, int startIndex);

    /**
     * Returns the clickable areas for all passed points
     *
     * @param points
     *            the array of points
     * @param values
     *            the array of values of each point
     * @param yAxisValue
     *            the minimum value of the y axis
     * @param seriesIndex
     *            the index of the series to which the points belong
     * @return an array of rectangles with the clickable area
     * @param startIndex
     *            the start index of the rendering points
     */
    protected abstract ClickableArea[] clickableAreasForPoints(List<Float> points, List<Double> values,
            float yAxisValue, int seriesIndex, int startIndex);

    /**
     * Returns if the chart should display the null values.
     *
     * @return if null values should be rendered
     */
    protected boolean isRenderNullValues() {
        return false;
    }

    /**
     * Returns if the chart should display the points as a certain shape.
     *
     * @param renderer
     *            the series renderer
     */
    public boolean isRenderPoints(SimpleSeriesRenderer renderer) {
        return false;
    }

    /**
     * Returns the default axis minimum.
     *
     * @return the default axis minimum
     */
    public double getDefaultMinimum() {
        return MathHelper.NULL_VALUE;
    }

    /**
     * Returns the scatter chart to be used for drawing the data points.
     *
     * @return the data points scatter chart
     */
    public ScatterChart getPointsChart() {
        return null;
    }

    /**
     * Returns the chart type identifier.
     *
     * @return the chart type
     */
    public abstract String getChartType();

}

[Android][开源**客户端]学习1--Start

今天决定开始好好研究几个开源的项目。之前朋友推荐过开源**的客户端写得还不错,代码量不大,结构也很清晰。源码可以直接下载下来,于是决定好好看一下。首先看其net.oschina.app包的结构。
image

  • AppStart.java是程序的入口,用来启动界面的动画,同时负责跳转到主Activity中,即net.oschina.app.ui包下面的Main.java类中。
  • AppManager 是用来对整个应用程序的Activity进行管理。此处有几点需要记录一下:
    • 该类采用单例模式进行实现。此类中的单例模式其实不太严密,但对于手机应用,这样实现完全可以满足要求。关于单例模式的实现可以参看我的另一篇文章[Design-Pattern]-Singleton
    • 除了保留自身的一个引用外private static AppManager instance;(为了单例模式的实现),该类只有一个成员变量即一个Activity 栈。即private static Stack<Activity> activityStack;
    • 类中提供了多个对整个应用程序的Activity进行管理的方法,如添加Activity,移除Activity,退出应用程序等。如:
//添加Activity到堆栈  
public void addActivity(Activity activity) {
    if (activityStack == null) {
        activityStack = new Stack<Activity>();
    }
    activityStack.add(activity);
}

//结束当前Activity(堆栈中最后一个压入的) 
public void finishActivity() {
    Activity activity = activityStack.lastElement();
    finishActivity(activity);
}

//结束指定的Activity
public void finishActivity(Activity activity) {
    if (activity != null) {
        activityStack.remove(activity);
        activity.finish();
        activity = null;
    }
}
// 退出应用程序
public void AppExit(Context context) {
    try {
        finishAllActivity();
        ActivityManager activityMgr = (ActivityManager) context
                .getSystemService(Context.ACTIVITY_SERVICE);
        activityMgr.restartPackage(context.getPackageName());
        System.exit(0);
    } catch (Exception e) {
    }
}
  • AppConfig中定义了一些应用相关的配置信息,这些信息最终都会写到properties文件中。
  • AppContext中用来放应用程序的上下文信息,用来存储全局的配置。并提供了从网络上获取应用所需要的信息的业务相关的方法。如getUserBlogListgetBlogListgetPostList等多种方法。
  • AppException 应用程序的异常类,用来捕获异常并提示错误信息。主要是访问网络和IO相关操作的异常。包括将异常信息存储到文件,发送异常报告到服务器,弹出友好的出错提示等方法。该异常类实现了Thread.UncaughtExceptionHandler接口。关于Thread.UncaughtExceptionHandler接口的使用,请参看:[Android]异常的捕获 Thread.UncaughtExceptionHandler

[Android]Fragment的基本使用

Anroid中Fragment的基本使用

Fragment是在android 3.0之后才开始有的组件,所以如果App支持的最小版本是在android 3.0之前,需要引入Support Library. FragmentActivity是Support Library提供的一个特殊的Activity,用来在API 11以下的系统中处理fragment,如果我们App的最低版本大于等于11,可以使用普通的Activity.

Fragment的创建

创建Fragment,唯一一个需要重写的回调方法就是onCreateView()。Fragment是一个UI组件,每一个Fragment的实例都必须与一个FragmentActivity关联起来,最简单的一种方式便是在Activity的布局文件中定义每一个fragment来实现这种关联。即采用<fragment android:name=“com.test.TestFragment">采用这种方式,Fragment是不能被动态移除的,如果想在用户交互时把fragment切入与切出,就必须在activity启动后,再将fragment添加到Activity 中。
采用这种方式时,Fragment所在的Activity必须要是继承自FragmentActivity.

Fragment的操作

在Activity的生命周期内我们可以添加,移除,替换Activity,所有的这些操作是通来FragmentManager, FragmentTransaction来执行的。
Fragment的添加
(1)在Activity中添加一个<FrameLayout>用来放置Fragment。
(2)New 一个Fragment。
(3)调用getSupportFragmentManager().beginTransaction()方法获得一个Transaction,调用 Transaction的add()方法添加一个fragment。
(4) 操作完成后,调用transaction的commit()方法提交更改。

*Fragment的替换 *
Fragment的替换和Fragment的添加基本上没有什么区别,只是调用的是replace()方法而不是add()方法。

Fragment的生命周期

Fragment有自己的生命周期,但它的生命周期依赖于它所在的Activity. 当activity被销毁时,它里面所有的Fragment都将被销毁。
需要注意一点,不要为Fragment添加新的构造方法,因为系统在初始化Fragment 时会调用Fragment的无参构造方法,如果没有找到,将会抛一个exception。虽然可以采用在自定义的构造方法中再调一次无参的构造方法也可以完成Fragment的初始化,通常不建议这样做,自定义构造方法无非就是为了为Fragment初始传参。但采用fragment.setArguments() 这种方式更加合适。

Fragment的生命周期方法:

onAttach(Activity)
onCrate(Bundle)
onCreateView()
onActivityCreate()
onStart()
onResume()

销毁时的操作:

onPause :Fragment不能和用户交互,因为activity 也处于paused状态,或者Activity对Fragment进行某些操作。
onStop() Fragment不可见
onDestoryView() : fragment开始清理resource
onDestory(): 清理Activity的状态
onDetach(): Fragment彻底与activity分离。

[Tool] Git的基本使用

初始化一个Git仓库

  1. 新建一个文件夹,可命名为TestGit
  2. 进入该文件夹 cd TestGit
  3. 初始化Git仓库 git init 可以看到在目录下会有一个.git的文件,在linux 下采用ls -a查看。该文件是一个隐藏文件,在windows下,请注意选择文件夹选项中的显示隐藏文件这个选项,具体操作不清楚的可以百度。
  4. 至此,一个Git仓库便初始化完成,可采用git status来查看状态。

添加文件

  1. TestGit目录下新建一个文件test1.txt
  2. 新建完成后可采用git status查看当前Git仓库的状态。
  3. 将文件添加到git管理系统中去, 采用git add test1.txt
  4. 再用git status查看当前Git仓库的状态。
    对于批量添加某种类型文件,git是支持通配符的,如添加所有的.txt文件,采用git add '*.txt'

查看历史commit记录

git log

提交更改

git commit -m "commit comments"

将本地git仓库和服务器端的git仓库绑定起来

git remote add origin SERVER_URL
#将本地的git仓库中的文件push到服务器端
git push -u origin master

-u 是告诉git 管理系统记住git push的参数,此处指的就是如果采用push就push到master分支上去。采用这个命令后,再次再使用git push时,便会自动的push到master分支上。

从Server端拿代码

git pull origin master # origin指的是从远程server拿, master指的是分支

当我们在用git pull时,经常遇到这种情况,如果我本地修改了或添加了文件,在没有commit/add之前是不让pull的,此时可先将本地修放入stash中,运行git stash, 再执行git pull 从server端拿代码,完成后再执行git stash popstash中取出最后一次的缓存。此时便会看到你所做的修改和从server端刚拿下来的数据合到了一起,你要做的便是解决冲突了。
对于刚从server上拿下来的代码,如果想查看文件之间的差异采用

git diff HEAD

git分支相关的操作
在开发过程中,整个团队可以采用master分支用来放代码,个人可以根据自己的需要创建分支。创建分支的好处是自己可以自由的进行commit操作,方便管理,等到开发完一个feature后,再将自己的分支merge到主分支上。

_创建一个分支的命令_

git branch branch_name.

_切换分支_

git checkout branch_name

_合并分支_

  • 首先切换到主分支上
git checkout master
  • 执行merge操作
git merge branch_name

Merge完后若要删除分支可采用git branch -d branch_name

几个不错的学习Git的资源

  1. git的入门看这里
  2. 学习git的分支操作看这里
  3. 一个国人写的git教程,很不错,值得推荐,点这里

[Android][Security]采用ProGuard进行代码混淆

当我们开发一款APP发布之后,用户一但获得apk文件,其实可以通过逆向工程(反编译)的方式获取我们APP中的代码和各种资源文件。资源文件泄露还好,代码却是一个APP的核心,更包含了一些商业机密。代码的安全性保护通常会采用代码混淆的方式。最近在项目中用到了这个,也是折腾了一两天,今天在此总结一下:

  • ProGuard 不仅可以对代码进行混淆的处理,还可以对文件进行压缩,优化。在Android 2.3之后,Google便将ProGuard整合到Android project中。 当创建一个新的Android工程时,在工程目录的根路径下,会出现一个proguard的配置文件proguard-project.txt,关于ProGuard的配置便在这个文件中完成。
  • 在使用ProGuard功能时,首先在Android工程根目录下的default.properties文件中声明启用ProGuard功能。如下:
# Project target.
target=android-19
# Use proguard
proguard.config=proguard-project.txt
  • 接着便是重点,对ProGuard进行各种配置。在网上搜索,通常可以发现有很多ProGuard的配置模板,列举了一些比较common的配置。便很多时候需要根据自己项目的需要进行特殊的配置。直接总结我在一个项目中的配置,用代码说话。
# 混淆优化,通常设置为5次便可以了。
-optimizationpasses 5

#When not preverifing in a case-insensitive filing system, such as Windows. Because this tool unpacks your processed jars, you should then use:
#中文意思大概是 不会产生 a.class, A.class这种文件,因为某些系统,如windows是不区分大小写的
-dontusemixedcaseclassnames

#指定不去忽略非公共的库类。
-dontskipnonpubliclibraryclasses

#不预校验, 预校验和dex compiler 和the Dalvik VM相关。
-dontpreverify

#在执行过程中如有异常,输出更详细的堆栈信息。
-verbose

#The -optimizations option disables some arithmetic simplifications that Dalvik 1.0 and 1.5 can't handle. Note that the Dalvik VM also can't handle aggressive overloading (of static fields).
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*

#维护注释,通常我们代码中会用到注释的。
-keepattributes *Annotation*

#Keep classes that are referenced on the AndroidManifest
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class com.android.vending.licensing.ILicensingService

#对于Android系统中会用到的native的方法的维护,由于native方法是由系统提供,不能混淆名称。
-keepclasseswithmembernames class * {
    native <methods>;
}

#对于自写义View的维护
-keep public class * extends android.view.View {
    public <init>(android.content.Context);
    public <init>(android.content.Context, android.util.AttributeSet);
    public <init>(android.content.Context, android.util.AttributeSet, int);
    public void set*(...);
}

-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet);
}

-keepclasseswithmembers class * {
    public <init>(android.content.Context, android.util.AttributeSet, int);
}

# 对于代码中使用到的enum的维护
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

#To keep parcelable classes (to serialize - deserialize objects to sent through Intents)
-keep class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator *;
}

#代码中用到了泛型,一定要加上这个,否则会报类型转换错误
-keepattributes Signature
#如果使用到WebView和javaScript调用,加上这个。
-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}
  • Comments:
    • 在实际操作过程中发现,对于Android项目中配置的Activity,Service等等,其实如果不配置
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service

也不会出现类名混淆的情况,这是因为Android系统对这些在AndroidManifest.xml文件中配置的Activity,Service已经做了处理,默认就是不混淆。所以实际上,我们的配置文件proguard-project.txt中可以不写这些的。

参考:http://stackoverflow.com/questions/5068251/android-what-are-the-recommended-configurations-for-proguard
如果想查看混淆后的效果,可能用dex2jar来反编译打包好的apk,三步:

  • 解压apk, 从中找classes.dex文件。
  • 运行dex2jar.bat classes.dex 命令,生成classes_dex2jar.jar文件
  • jd-gui.exe工具查看类文件。

dex2jar的使用可参看这里,或者百度。这个比较简单,不表。

[Android][Service]

服务的生命周期

服务的生命周期与其启动的方式有关。服务的启动方式有两种,即StartBind.

采用start方式

  • 当采用context.startService方式启动服务,与之相关的生命周期方法的执行顺序如下:
   onCreate() --> onStart() --> onDestory()
  • onCreate()方法在服务被创建时调用,该方法只会被调用一次,无论调用多少次startService()bindService方法,Service只会被创建一次。
  • onStart()方法只有在采用context.startService()方法启动服务时才会回调该方法。该方法在Service开始运行时被调用,多次调用startService()方法尽管不会多次创建服务,但onStart()方法会被多次调用。
  • onDestory()方法是在服务被终止时调用。

采用Bind方式

  • 当采用context.bindService()方法启动服务时,与之相关的生命周期方法执行顺序如下:
   onCreate() --> onBind() --> onUnbind() --> onDestory()
  • 同样,onBind()只会在采用context.bindService()方法启动服务时才会被回调。该方法在调用者与服务绑定时被调用,当调用者与服务已经绑定,多次调用context.bindService()方法不会导致该方法多次调用。
  • onUnbind()只有采用Bind方法启动服务时会回调该方法,该方法在调用者与服务解除绑定时被调用。
  • 如果采用startService方法启动服务,然后调用bindService()方法绑定到服务,再调用unBindService()方法解除绑定,最后调用bindService()方法再次绑定到服务,触发的生命周期方法如下:
onCreate() --> onStart() --> onBind() -->onUnbind() --> onRebind()

[Android] Activity处于不可交互状态或不可见状态时消息的处理

在开发过程中经常会碰到这种情况,某一个Activity已经处于不可交互状态或者不可见状态时,这个Activity中定义的Handler却还没有处理完相关的任务,或者是启动的一个线程,线程尚未执行完毕。在通常这种情况下,由于有尚未处理的任务存在,该Handler不会被销毁,任务会执行。但如果这个Handler中涉及到刷新UI的相关操作,那么可能就有点麻烦了,特别是如果此时Activity已经是不可见状态,或已经被回收了,刷新UI变会抛出异常,导致程序崩溃。需要注意的是,当activity处于不可见状态,或者已销毁,对其刷UI的操作时并一定会出现异常,得看具体是做什么样的刷UI操作,Android真是神奇

下面给出一个例子,以下代码中Activity的布局非常简单,里面就只有一个按钮,当点击这个按钮后会有一个刷UI的操作,用来显示一个DialogFragment,只是这个操作会在6秒钟之后执行。代码如下:

public class TestPauseHandler extends Activity {
    private static final String TAG = "TestPauseHandler";
    private Button btnTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_pause_handler);
        btnTest = (Button)findViewById(R.id.btn_test);
        btnTest.setOnClickListener(listener);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Log.i(TAG, "-----------onSaveInstanceState---------");
    }

    protected void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "-----------onDestory---------");
    }

    private View.OnClickListener listener = new View.OnClickListener() {

        @Override
        public void onClick(View v) {
            new Handler().postDelayed(new Runnable() {

                @Override
                public void run() {
                    DialogFragment dialogFragment = new DialogFragment();
                    dialogFragment.show(getFragmentManager(),"dialog"); //show DialogFragment的源码里其实会执行Transaction的commit
                }
            }, 6000);
        }
    };
}

如果我们这样玩它:
1.点击这个按钮后,接着点HOME按钮,或者切到系统的Setting屏,我们会发现该activity的onSaveInstanceState方法执行了,然后过了一会,程序就Crash了。程序报的异常是

05-20 10:31:27.598    2445-2445/com.fred.testactivity E/AndroidRuntime﹕ FATAL EXCEPTION: main
    Process: com.fred.testactivity, PID: 2445
    java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState
            at android.app.FragmentManagerImpl.checkStateLoss(FragmentManager.java:1328)
            at android.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1346)
            at android.app.BackStackRecord.commitInternal(BackStackRecord.java:728)
            at android.app.BackStackRecord.commit(BackStackRecord.java:704)

如果在点了按钮后, 按Back键,结束这个activity, 回到桌面, 会发现onDestroy方法调用了,过了一会,程序抛出异常。

    Process: com.fred.testactivity, PID: 2482
    java.lang.IllegalStateException: Activity has been destroyed
            at android.app.FragmentManagerImpl.enqueueAction(FragmentManager.java:1350)
            at android.app.BackStackRecord.commitInternal(BackStackRecord.java:728)
            at android.app.BackStackRecord.commit(BackStackRecord.java:704)
            at android.app.DialogFragment.show(DialogFragment.java:230)
            at com.fred.testactivity.TestPauseHandler$1$1.run(TestPauseHandler.java:49)

2.我们将刷UI操作的run方法中代码换成

Dialog dialog = new Dialog(TestPauseHandler.this);
dialog.show();

点击按钮后,接着点HOME按钮,或者切到系统的Setting屏,我们会发现该activity的onSaveInstanceState方法执行了,点Recent apps键,重新回到这个app, 发现dialog弹出来了,程序没有crash。

但是,如果在点了按钮后, 按Back键,结束这个activity, 回到桌面, 会发现onDestroy方法调用了,过了一会,程序抛出异常

Process: com.fred.testactivity, PID: 2331
    android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@15b4466a is not valid; is your activity running?
            at android.view.ViewRootImpl.setView(ViewRootImpl.java:562)
            at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:272)
            at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
            at android.app.Dialog.show(Dialog.java:298)

3.将run方法中代码换成

btnTest.setText("Click again");

只是修改了Button上面的文字,接着做实验。
点击按钮后,接着点HOME按钮,或者切到系统的Setting屏,我们会发现该activity的onSaveInstanceState方法执行了,点Recent apps键,重新回到这个app, 发现dialog弹出来了,程序没有crash。

点击按钮后, 按Back键,结束这个activity, 回到桌面, 会发现onDestroy方法调用了,程序依旧没有Crash。

Android真是神奇,当activity处于不可见状态,或者已销毁,对其刷UI的操作时并一定会出现异常,得看具体是做什么样的刷UI操作。 想知道具体原因就得去看源码了。

在Android中刷UI的操作其实也是通过发送消息进行的。于是我们希望的结果是,在当前Activity处于不可交互状态时,若有消息没有处理完,先将消息缓存着,等到以后当此Activity恢复可交互状态时,再处理消息。
Stackoverflow上面有这么一个问答,有高手给出了这么一个解决方案,非常优雅。在我们的项目中也用得到了采用。核心代码如下:

/**
 * Message Handler class that supports buffering up of messages when the
 * activity is paused i.e. in the background.
 */
public abstract class PauseHandler extends Handler {

    /**
     * Message Queue Buffer
     */
    final Vector<Message> messageQueueBuffer = new Vector<Message>();

    /**
     * Flag indicating the pause state
     */
    private boolean paused;

    /**
     * Resume the handler
     */
    final public void resume() {
        paused = false;

        while (messageQueueBuffer.size() > 0) {
            final Message msg = messageQueueBuffer.elementAt(0);
            messageQueueBuffer.removeElementAt(0);
            sendMessage(msg);
        }
    }

    /**
     * Pause the handler
     */
    final public void pause() {
        paused = true;
    }

    /**
     * Notification that the message is about to be stored as the activity is
     * paused. If not handled the message will be saved and replayed when the
     * activity resumes.
     * 
     * @param message
     *            the message which optional can be handled
     * @return true if the message is to be stored
     */
    protected abstract boolean storeMessage(Message message);

    /**
     * Notification message to be processed. This will either be directly from
     * handleMessage or played back from a saved message when the activity was
     * paused.
     * 
     * @param message
     *            the message to be handled
     */
    protected abstract void processMessage(Message message);

    /** {@inheritDoc} */
    @Override
    final public void handleMessage(Message msg) {
        if (paused) {
            if (storeMessage(msg)) {
                Message msgCopy = new Message();
                msgCopy.copyFrom(msg);
                messageQueueBuffer.add(msgCopy);
            }
        } else {
            processMessage(msg);
        }
    }
}

其思路是:

  • 采用一个标识位paused来标识当前Activity的状态。如果paused的值是true,代表当前的Activity处于不可交互状态。
  • paused的值只会在两个位置发生改变,一个是onResume方法执行时将paused设为false,另一个是在onPause方法执行时,将paused设为true.
  • 在使用时,自定义一个Handler 继承PauseHandler,覆盖其processMessage方法。
  • 在使用自定义Handler的外部类(Activity或Fragment)的onResumeonPause方法中也调用handler相应的onResumeonPause方法。

一个采用PauseHandler的实现如下:

public class TestPauseHandler extends Activity {
    private static final String TAG = "TestPauseHandler";
    private Button btnTest;
    private ConcreteTestHandler handler = new ConcreteTestHandler();


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_pause_handler);
        btnTest = (Button) findViewById(R.id.btn_test);
        btnTest.setOnClickListener(listener);

        handler.setActivity(this);
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Log.i(TAG, "-----------onSaveInstanceState---------");
    }

    protected void onDestroy() {
        super.onDestroy();
        //清除所有的callback和message.
        handler.removeCallbacksAndMessages(null);
        Log.i(TAG, "-----------onDestory---------");
    }

    private View.OnClickListener listener = new View.OnClickListener() {

        @Override
        public void onClick(View v) {
            Message message = Message.obtain();
            handler.sendMessageDelayed(message, 6000);
        }
    };

    @Override
    protected void onResume() {
        super.onResume();
        handler.resume();
    }

    @Override
    protected void onPause() {
        super.onPause();
        handler.pause();
    }

    static class ConcreteTestHandler extends PauseHandler {
        protected Activity activity;

        public void setActivity(Activity activity) {
            this.activity = activity;
        }

        @Override
        protected boolean storeMessage(Message message) {
            return true;
        }

        @Override
        protected void processMessage(Message message) {
            final Activity activity = this.activity;
            if (activity != null) {
                DialogFragment dialogFragment = new DialogFragment();
                dialogFragment.show(activity.getFragmentManager(), "dialog"); //show DialogFragment的源码里其实会执行Transaction的commit
                Log.i(TAG, "PauseHandler processMessage execute -->");
            }
        }
    }
}

采用这种方式,问题便可以完美的解决

[Android]异常的捕获 Thread.UncaughtExceptionHandler

Android开发中,由于生产环境的复杂性,各种各样的问题难免会造成App的Crash。为此,我们通常需要定义一个CrashHandler来从全局的角度去捕获各种异常。

Android开发中,从功能来说有两种类型的线程,主线程(UI线程)和子线程。由于线程的本质特性,我们不能捕获从线程中逃逸出来的异常,一旦异常逃出任务的run()方法,便会向外传播。如下:

public class TestExceptionThread {
    public static void main(String args[]) {
        ExecutorService exec = Executors.newCachedThreadPool();
        exec.execute(new ExceptionThread());
    }
}
class ExceptionThread implements Runnable {
    public void run () {
        throw new RuntimeException();
    }
}

毫无疑问,这段代码执行的时候肯定会报错。异常信息如下:

Exception in thread "pool-1-thread-1" java.lang.RuntimeException
    at ExceptionThread.run(TestExceptionThread.java:13)
    at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.lang.Thread.run(Unknown Source)

于是,我们将代码做一下改动,添加一个try ... catch试试,改动如下:

public class TestExceptionThread {
    public static void main(String args[]) {
        ExecutorService exec = Executors.newCachedThreadPool();
        try {
            exec.execute(new ExceptionThread());
        } catch (RuntimeException e) {
            System.out.println("Exception has been handler");
        }
    }
}

运行程序,运行的结果和上面一下,异常信息还是没有捕获到。

此时,就该用Thread.UncaughtExceptionHandler了。它是Java SE5中提供的一个接口,允许在每个Thread对象上附着一个异常处理器,该接口中的uncaughtException()方法会在线程因未捕获到的异常而临近死亡前被调用。

第一种方式

代码如下:

public class TestExceptionThread {
    public static void main(String args[]) {
        try {
            Thread t = new Thread(new ExceptionThread());
            t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
            t.start();
        } catch (RuntimeException e) {
            System.out.println("Exception has been handler");
        }
    }
}

class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("got unCaughtException");
    }

}
class ExceptionThread implements Runnable {
    @Override
    public void run () {
        throw new RuntimeException();
    }
}

输出结果是

got unCaughtException

但是,如果我们将main(String args[])方法中的内容改成这样:

    public static void main(String args[]) {
        ExecutorService exec = Executors.newCachedThreadPool();
        Thread t = new Thread(new ExceptionThread());
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        exec.execute(t);
    }

发现运行结果会是这样:

Exception in thread "pool-1-thread-1" java.lang.RuntimeException
    at ExceptionThread.run(TestExceptionThread.java:31)
    at java.lang.Thread.run(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(Unknown Source)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)
    at java.lang.Thread.run(Unknown Source)

也就是说: 如果这样写,线程中的异常还是没有捕捉到。

第二种方式

  • 接着看下面这段代码,将Thread交给线程池来处理,同时我们开始使用ThreadFactory来创建线程,并为创建的线程指定uncaughtExceptionHandler.
public class TestExceptionThread {
    public static void main(String args[]) {
        ExecutorService exec = Executors.newCachedThreadPool(new HandlerThreadFactory());
        Thread t = new Thread(new ExceptionThread());
        exec.execute(t);
    }
}
class HandlerThreadFactory implements ThreadFactory{

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(r);
        System.out.println("Create thread t");
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        System.out.println("eh=" + t.getUncaughtExceptionHandler());
        return t;
    }
}
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("got unCaughtException");
    }
}
class ExceptionThread implements Runnable {
    @Override
    public void run () {
        System.out.println("eh=" + Thread.currentThread().getUncaughtExceptionHandler());
        throw new RuntimeException();
    }
}

运行结果如下:

Create thread t
eh=MyUncaughtExceptionHandler@10d448
eh=MyUncaughtExceptionHandler@10d448
got unCaughtException

第三种方式

我们可以采用在Thread类中设置一个静态域,代码如下:

public class TestExceptionThread {
    public static void main(String args[]) {
        Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        Thread t = new Thread(new ExceptionThread());
        t.start();
    }
}

class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("got unCaughtException");
    }
}

class ExceptionThread implements Runnable {
    @Override
    public void run () {
        throw new RuntimeException();
    }
}

同样会发现,异常会捕捉到。需要注意一点是这个处理器只有在不存在线程专有的末捕获异常处理器的情况下才会被调用。系统会检查线程专有版本,如果没有发现,则检查线程组是否有其专有的uncaughtException()方法,如果也没有,再调用defaultUncaughtExceptionHandler。

  • 本文中的代码借鉴于Java 编程**(第4版)P642-P644

Thread.UncaughtExceptionHandler在Android中的应用

  • 自定义一个CrashHandler
public class CrashHandler implements UncaughtExceptionHandler{
    private static CrashHandler INSTANCE ;

    private CrashHandler(){
    }
    //一个简单的单例模式
    public static synchronized CrashHandler getInstance(){
        if (INSTANCE == null) {
            INSTANCE = new CrashHandler();
        }
        return INSTANCE;
    }

    @Override
    public void uncaughtException(Thread t, Throwable e) {
        //一些自定义的需要,比如,将crash的异常信息发给服务器。
    }
}
  • 在应用程序的Application类的onCreate()方法中注册CrashHandler
    如下:
public class MyApplication extends Application {
    @Override  
    public void onCreate() {  
        super.onCreate();  
        CrashHandler handler = CrashHandler.getInstance();  
        Thread.setDefaultUncaughtExceptionHandler(handler);  
    }  
}

完成

[Android] ListView和Adapter使用的最佳实践

一个ListView使用的好坏全都依赖于Adapter的实现,Adapter为ListView提供数据源,并渲染View。以下我们来分析一下,一个好的Adapter应该如何实现。首先,看这个例子:

public class MyAdapter extends BaseAdapter{
    private List<MyListItem> list;
    private Context context;
    public MyAdapter(Context context) {
        this.context = context;
    }
    @Override
    public int getCount() {
        return list.size();
    }

    @Override
    public Object getItem(int position) {
        return list.get(position);
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        convertView = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
        ImageView ivTest = (ImageView) convertView.findViewById(R.id.iv_test);
        TextView tvDesc = (TextView) convertView.findViewById(R.id.tv_desc);
        //此处是采用UniversalImageLoader来加载图片。
        ImageLoader.getInstance().displayImage(list.get(position).getImageUrl(), ivTest);
        tvDesc.setText(list.get(position).getDesc());
        Log.i("Adapter", "convertView : " + convertView);
        return convertView;
    }

    public void setList(List<MyListItem> list) {
        this.list = list;
    }

}

代码量很少。我们来一下其getView方法,每一次在渲染一个Item的时候,便会加载一次list_item这个布局文件,由此可见,效率是很低的。
于是针对getView作如下改进:

   @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (null == convertView) {
            convertView = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
        }
        ImageView ivTest = (ImageView) convertView.findViewById(R.id.iv_test);
        TextView tvDesc = (TextView) convertView.findViewById(R.id.tv_desc);

        ImageLoader.getInstance().displayImage(list.get(position).getImageUrl(), ivTest);
        tvDesc.setText(list.get(position).getDesc());
        Log.i("Adapter", "convertView : " + convertView);
        return convertView;
    }

显然,这样一改进了之后,只会在convertView不存在的情况下,才会去渲染一个新的,如果之前存在过,就采用之前的。这样下来。比如一个ListView中有100条记录,但屏幕上同时只能显示10个,系统则只需要构造10个convertView就可以了。通过看打印出来的Log,便可以证实这一点。

第一个可以优化的地方已经被我们优化了。接着还是看getView这个方法,还有一个可以优化的地方便是findViewById()这个方法的使用。在Android源码中,findViewById()这个方法的实现如下:

    public View findViewById(int id) {
        if (this.id == id) {
            return this;
        }
        for(View child : children) {
            View view = child.findViewById(id);
            if (view != null) {
                return view;
            }
        }
        return null;
    }

由此可以,对某一个View调用其findViewById()方法,便会遍历这个View中的所有子元素进行查找,这也是一种非常耗时的操作。接着,我们便对这个问题进行优化。
接着便出现了ViewHolder这个思路了。其思路在于,当我们已经通过findViewById()这个方法获得View之后,便将这个View存到一个ViewHolder的对象中。下次使用时,直接从ViewHolder对象中取。优化后的代码如下:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (null == convertView) {
            holder = new ViewHolder();
            convertView = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
            holder.ivTest = (ImageView) convertView.findViewById(R.id.iv_test);
            holder.tvDesc = (TextView) convertView.findViewById(R.id.tv_desc);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        ImageLoader.getInstance().displayImage(list.get(position).getImageUrl(), holder.ivTest);
        holder.tvDesc.setText(list.get(position).getDesc());
        Log.i("Adapter", "convertView : " + convertView);
        return convertView;
    }

    private static class ViewHolder {
        public ImageView ivTest;
        public TextView tvDesc;
    }

其它的关于Adapter优化的思路。在文章一文章二中提到了另外一种方式:

public class ViewHolder {
    // I added a generic return type to reduce the casting noise in client code
    @SuppressWarnings("unchecked")
    public static <T extends View> T get(View view, int id) {
        SparseArray<View> viewHolder = (SparseArray<View>) view.getTag();
        if (viewHolder == null) {
            viewHolder = new SparseArray<View>();
            view.setTag(viewHolder);
        }
        View childView = viewHolder.get(id);
        if (childView == null) {
            childView = view.findViewById(id);
            viewHolder.put(id, childView);
        }
        return (T) childView;
    }
}

个人觉得,在Adapter中采用这种方式,只能说是优化代码的结构,并不能优化效率。同时,也会增加代码的复杂度。
最后这个Adapter的代码如下:

public class MyAdapter extends BaseAdapter {
    private List<MyListItem> list;
    private Context context;

    public MyAdapter(Context context) {
        this.context = context;
    }

    @Override
    public int getCount() {
        return list.size();
    }

    @Override
    public Object getItem(int position) {
        return list.get(position);
    }

    @Override
    public long getItemId(int position) {
        return 0;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (null == convertView) {
            holder = new ViewHolder();
            convertView = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
            holder.ivTest = (ImageView) convertView.findViewById(R.id.iv_test);
            holder.tvDesc = (TextView) convertView.findViewById(R.id.tv_desc);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        ImageLoader.getInstance().displayImage(list.get(position).getImageUrl(), holder.ivTest);
        holder.tvDesc.setText(list.get(position).getDesc());
        Log.i("Adapter", "convertView : " + convertView);
        return convertView;
    }

    private static class ViewHolder {
        public ImageView ivTest;
        public TextView tvDesc;
    }

    public void setList(List<MyListItem> list) {
        this.list = list;
    }

}

[Android] 图片性能处理的一些总结

以下内容基本上都是来自于Google Android 的官方文档,其中有部份自己从网上搜集的,同时有少部份是根据自己在实际使用过程中总结的一些知识点。关于Google Android 官方文档国内不能访问这个问题,其实国内有部份良心企业为我们做了些镜像,如这个:http://wear.techbrood.com/training/displaying-bitmaps/load-bitmap.html

在做图片处理之前,我们需要知道图片的内存存储:
在Android 2.3.3(API Level 10)以及之前,Bitmap的backing pixel 数据存储在native memory, 与Bitmap本身是分开的,Bitmap本身存储在dalvik heap 中。导致其pixel数据不能判断是否还需要使用,不能及时释放,容易引起OOM错误。 从Android 3.0(API 11)开始,pixel数据与Bitmap一起存储在Dalvik heap中。所以在Android 2.3.3以及之前,建议使用Bitmap.recycle()方法,及时释放资源。

1. 图片大小的计算。

在Android 中图片有四种属性,分别是:
ALPHA_8:每个像素占用1byte内存
ARGB_4444:每个像素占用2byte内存
ARGB_8888:每个像素占用4byte内存 (默认)
RGB_565:每个像素占用2byte内存(没有alpha属性)
对于一张图片,假设其尺寸为2592x1936 pixels,默认情况下加载进来,Android系统需要为其分配2592_1936_4 bytes的空间,也就要19M。

2.在加载图片之前获取图片的Dimensions和Type.

我们可以采用BitmapFactory中的decodeByteArray(), decodeFile(),decodeResource() 去加载图片,如果直接加载,Android系统会为图片分配内存,容易出现OOM. 通过设置BitmapFactory.Options中的inJustDecodeBounds属性为true, 先获取Bitmap 的outWidth, outHeight, outMimeType(图片的类型,如image/jpeg, image/png等)选项。再根据获取到的选项,看是否在显示图片之前需要进行相应的处理(缩放)。

3.关于图片的缩放

图片的缩放是通过设置options.inSampleSize来实现。当inSampleSize = 2时,意味着图片的宽高都缩小为原来的1/2, 那么整体上图片的体积就变为原来的1/4.
Google的官方文档中给出了具体的操作示例。

计算inSampleSize.
public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}
缩放图片
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // First decode with inJustDecodeBounds=true to check dimensions
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    // Calculate inSampleSize
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // Decode bitmap with inSampleSize set
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}
调用
mImageView.setImageBitmap(decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

这个方法是为一个100*100像素的ImageView提供合适的bitmap. 并不意味着生成的bitmap的尺寸会是100 * 100,这一点需要注意。
读者也可以用如下代码来进行试验:

            InputStream is = getResources().openRawResource(R.drawable.image_1);
            BitmapFactory.Options options = new  BitmapFactory.Options();
            Bitmap btp =  BitmapFactory.decodeStream(is, null,  options);
            Log.i("TEST", "pre width:" + btp.getWidth() + "   height:" + btp.getHeight());

            Bitmap bitmap = decodeSampledBitmapFromResource(getResources(), res[position], 100, 100);

            Log.i("TEST", "width:" + bitmap.getWidth() + "   height:" + bitmap.getHeight());

4. 图片的缓存

图片的缓存Google推荐的是采用LruCache这个类,不要再采用SoftReference或WeakReference了,因为自从2.3之后垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。Google给出的例子是这样写的:

LruCache的初始化
private LruCache<String, Bitmap> mMemoryCache;

@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    // Get max available VM memory, exceeding this amount will throw an
    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
    // int in its constructor.
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);

    // Use 1/8th of the available memory for this memory cache.
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            // The cache size will be measured in kilobytes rather than
            // number of items. 返回缓存图片的大小, 每向 lru cache中添加一个item,这个方法便会执行一次
            return bitmap.getByteCount() / 1024;
        }
    };
    ...
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}
加戴图片的方法
public void loadBitmap(int resId, ImageView imageView) {
    final String imageKey = String.valueOf(resId);

    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
    if (bitmap != null) {
        mImageView.setImageBitmap(bitmap);
    } else {
        mImageView.setImageResource(R.drawable.image_placeholder);
        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
        task.execute(resId);
    }
}
实际加载图片的Task
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    ...
    // Decode image in background.
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(
                getResources(), params[0], 100, 100));
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
    ...
}

5. 采用MAT来定位内存泄露的图片

在上次做我们App性能优化问题的时候,发现在App中泄露的内存很大一部份是因为Memory Leak导致的Activity没有被回收,而在没有被回收的内存中,很大一部份是图片引起的。如果能定位到是哪一张图片的内存泄露,那么就可以有针对性的去检查代码了。在stackoverflow上有一篇文章记录着如利用MAT + GIMP 去定位内存泄露的图片。链接在这里. MAT的使用就不说了,下面把stackoverflow上找到的从MAT中导出图片数据,并用GIMP定位图片的Solution贴出来(此处不想翻译了):

  1. First, you need to download and install GIMP
  2. Next, find your Bitmap object in MAT, right-click on mBuffer field, in the popup menu choose "Copy" -> "Save Value To File" menu item and save value of this array to some file
  3. give extension .data to that file
  4. launch GIMP, choose "File" -> "Open", select your .data file and click Open button
  5. "Load Image from Raw Data" dialog will appear. Here you need to set correct parameters for your bitmap
  6. first, choose "Image type" as "RGB Alpha" (most Android resources have this image type, but you may need to experiment with other image types)
  7. second, set correct Width and Height for your bitmap (correct dimensions can be found in the memory dump)
  8. At that point you should already observe preview of original image. If you didn't, you can try to change some other parameters in "Load Image from Raw Data" dialog.

NOTE: to get a width and height of image you can look at mWidth and mHeight fields in MAT.

[Android]一次关于SingleTask的填坑

一次关于SingleTask的填坑

这个milestone客户那边做了一个功能,在做这个功能的时候,那边的开发把我们app中的activity的launchmode给改了。之前我们都是采用standard模式的,整个app中维持着一个activity,每次跳屏前会将当前的activity finish掉。下次再进到这个屏,重新执行onCreate,创建这个activity。同时我们有很多初始化UI和数据的代码写在onCreate方法中。现在因为修改了activity的launchmode,导致了每次进到一个屏时,其activity的onCreate方法不一定执行。从而引起了一堆的UI和数据不一致的问题。

在谈到这些问题之前,我们先来自一下activity的launchmode。都知道activity的启动模式有四种,Google推荐我们尽量不要去修改activity的launchmode,对于大多数的应用standard模式就可以适用。这里我们只讲singleTask这一个launchmode.

例子

假设有四个activity。如果采用默认的launchmode, 依次启动 A->B->C->D。栈中将会保留这四个Activity,如果再由D启动A,A启动B, 则栈中顺序会是A->B->C->D->A->B。 这个很好理解。

如果我把B的launchmode 改为singleTask. 依次启动A->B->C->D。栈中仍会保留这四个activity,如果再由D启动A,会发现栈中有5个activity.

采用adb shell dump sys activity命令,我们可以查看当前activity和栈的情况:

Running activities (most recent first):
TaskRecord{423f84e0 #81 A=com.example.fredye.myapplication U=0 sz=5}
Run #5: ActivityRecord{42635c08 u0 com.example.fredye.myapplication/.lanuchmode.ActivityA t81}
Run #4: ActivityRecord{42930618 u0 com.example.fredye.myapplication/.lanuchmode.ActivityD t81}
Run #3: ActivityRecord{430e61d8 u0 com.example.fredye.myapplication/.lanuchmode.ActivityC t81}
Run #2: ActivityRecord{42ee02c0 u0 com.example.fredye.myapplication/.lanuchmode.ActivityB t81}
Run #1: ActivityRecord{427133c8 u0 com.example.fredye.myapplication/.lanuchmode.ActivityA t81}

如果再由A启动B, 此时有意思的事便发生了。同样,我们打印出当前Activity和栈的情况:

Running activities (most recent first):
TaskRecord{423f84e0 #81 A=com.example.fredye.myapplication U=0 sz=2}
Run #2: ActivityRecord{42ee02c0 u0 com.example.fredye.myapplication/.lanuchmode.ActivityB t81}
Run #1: ActivityRecord{427133c8 u0 com.example.fredye.myapplication/.lanuchmode.ActivityA t81}

会发现栈中只有两个activity。我们在ActivityB上指定了launchmode是singleTask,于是在第二次启动ActivityB时,发现当前task里面已经有ActivityB了,便将ActivityB移动栈顶,同时销毁ActivityC和ActivityD。在这个过程中ActivityB的onCreate方法是不会执行的。只会执行其onNewIntent方法。[注意此时所有的Activity都还是在一个task中]

问题来了

1. UI 问题

之前我们的App中每次在Activity跳转的时候都会去finish当前的Activity,确保应用中只存在一个Activity。因此有部份UI的初始化操作是直接放到onCreate方法中做的。singleTask的引入,导致了Activity的onCreate方法不一定执行,因此UI初始化的问题便出现了。

2. 数据不一致问题

如果项目中存在这种代码,那就要小心了

  public class ActivityA extends Activity {
      private String data;
      protected void onCreate(Bundle saveInstanceState) {
          data = (MyApplication)getApplication().getData();
      }
  }

对于属性data, 如果在应用中的其它地方对它赋值,会导致在ActivityA中使用的data不是最新的。同样,Root Cause是因为onCreate方法不会每次都执行。

如果碰到采用intent传值也要小心。比如在A activity中有这么一段代码:

Intent intent = new Intent(AActivity.this, BActivity.class);
intent.putExtra("date", new Date().toLocaleString());
startActivity(intent);

由A activity去启动BActivity时用 intent传了一个参数,如果BActivity被设置成了singleTask,由上面的分析,我们知道BActivity中的onCreate方法不一定会执行,因此我们没有在onCreate方法中取intent中的数据,而是从onResume方法里面去取。看起来好像没有问题,但是后来发现,当我们取值的时候,我们发现数据一直都没有更新。此时我们需要进行另外一个操作,重写BActivity中的onNewIntent方法,代码如下:

    //如果当前Activity的启动模式是singleTask, 重定onNewIntent方法可以保证接收到的其它Activity传过来的值是最新的。
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        setIntent(intent);
    }

3. 用户行为的问题

在上面例子中,由于ActivityB设置了launchmode为singleTask, 从而导致了ActivityC和ActivityD被销毁的问题。此时,如果用户按Back键,会发现回退时屏幕出现的顺序和启动的顺序没有匹配上,上面例子中是不能返回到ActivityC屏和ActivityD屏。

关于1, 2 两个问题,当了解了singleTask的引入导致activity的生命周期回调方法的没有按我们预期的执行这个问题,解决起来也就好弄了。关于第3点,用户行为的问题,这个就暂时没有找到比较好的方法去弄了。

官方对singleTask的定义

The system creates a new task and instantiates the activity at the root of the new task. However, if an instance of the activity already exists in a separate task, the system routes the intent to the existing instance through a call to its onNewIntent() method, rather than creating a new instance. Only one instance of the activity can exist at a time. Although the activity starts in a new task, the Back button still returns the user to the previous activity.

实际上我们在使用时发现,对于设置了"singleTask"启动模式的Activity,它在启动的时候,会先检测系统中属性值affinity等于它的属性值taskAffinity的task是否存在;如果存在这样的task,它就会在这个task中启动,否则就开启一个新的task。因此,如果我们想要设置了"singleTask"启动模式的Activity在新的task中启动,就要为它设置一个独立的taskAffinity属性值,taskAffinity默认情况下是应用的包名。下面的代码中,应用程序的包名是com.fred.testactivity,我们将BActivity的启动模式设置成singleTask, 同时设置其taskAffinity属性为com.fred.testactivity.BActivity 。当BActivity启动时,会发现多了一个task 。代码如下:

<activity
    android:name=".BActivity"
    android:label="@string/title_activity_b"
    android:launchMode="singleTask"
    android:taskAffinity="com.fred.testactivity.BActivity">
</activity>

补充:android中的task和stack

首先我们需要明白任何一个Android app是由多个Activity组成的. 每一个Activity可以启动自己app应用中的activity或者其它app应用中的Activity.每一个Activity可以声明它所要处理的意图。当声明处理同一个意图的Activity有多个时,系统就会弹出一个窗口让用户进行选择。

一个task便是一系列Activity的集合。当用户在APP A中发出了一个发送邮件的意图,系统打开了邮件客户端,邮件发送完后,又回到了APP A, 给用户的感觉发送邮件就是在APP A中完成的一样。这个便是由于发送邮件的Activity, 和APP A中的activity在同一个Task中。Task中的activity是交给一个stack来管理的,stack中activity顺序是按照它们启动的先后顺序。

桌面是大多数task启动的位置,当用户点击了一个app 的 icon时,这个app 的task便由后台转到前台。如果当前的app 没有对应的task, 便创建一个新的task,同时启动它的”main” activity,同时将其放入stack中。如果当前的activity启动另外一个activity, 新的activity便会放到栈顶,之前的activity会被stop, 系统会保留它的状态。当用户点击Back键时,当前activty会出栈(会被destory, 它的onDestory方法会被调用),它的前一个activity会被Resume。如果用户不断的按Back键,该Task中的Activity将会一个接一个的出栈,当这个栈中的activity都出栈了,这个task也就不存在了。

Google的提醒

我们可以通过设置launchmode和affinity来管理Task, 但Google给了我们一个提醒 --大多数应用不要改变Activity的启动模式, 原文如下

Caution: Most applications should not interrupt the default behavior for activities and tasks. If you determine that it's necessary for your activity to modify the default behaviors, use caution and be sure to test the usability of the activity during launch and when navigating back to it from other activities and tasks with the Back button. Be sure to test for navigation behaviors that might conflict with the user's expected behavior.

[Android] Android中Parcelable的使用

之所以采用Parcelable是因为在两个Activity之间传递对象,采用Parcelable会比Serializable的效率要高。因为Serializable序列化采用大量反射,且会产生很多临时变量,从而引起频繁的GC。Parcelable不能使用在要将数据存储在磁盘上的情况,因为Parcelable在外界有变化的情况下不能很好的保证数据的持续性。

定义实体

package com.example.test.entity;

import android.os.Parcel;
import android.os.Parcelable;

public class Book implements Parcelable {
    private String name;
    private String author;

    public Book() {

    }

    public Book(String name, String author) {
        this.name = name;
        this.author = author;
    }
    public String getName() {
        return this.name;
    }
    public String getAuthor() {
        return this.author;
    }

    @Override
    public int describeContents() {
        return 0;
    }
    //通过writeToParcel将你的对象映射成Parcel对象
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeString(author);
    }

    public static final Parcelable.Creator<Book> CREATOR = new Creator<Book>() {
        //通过createFromParcel将Parcel对象映射成需要的对象
        @Override
        public Book createFromParcel(Parcel source) {
            // TODO Auto-generated method stub
            return new Book(source);
        }

        @Override
        public Book[] newArray(int size) {
            return null;
        }

    };
    //在从Parcelable中取值会调用这个方法,方法中提取属性的顺序必须是要和writeToParcel中一致。
    public Book(Parcel in) {
        name = in.readString();
        author = in.readString();
    }
}

调用方式,如在第一个Activity中

Intent intent = new Intent(MainActivity.this, SecondActivity.class);
intent.putExtra("book", new Book("book_name", "author"));
startActivity(intent);

在第二个Activity中

Intent intent = getIntent();
Book book = intent.getParcelableExtra("book");
Log.i("Second", book.getAuthor() + "   " + book.getName());

[JavaSE][线程池]

在编程世界中池通常是用来提前创建资源。如数据库连接池,会提前创建一些数据库连接。同理,线程池是提前创建线程,放到空闲队列中,然后对线程资源进行复用,减少频繁创建和销毁对象引起的资源消耗,提高系统的性能。

  • Jdk1.5以上提供了线程池。
    Executor是一个执行线程的工具, 只有一个方法void execute(Runnable command)
    线程池接口是ExecutorService. 继承自Executor,提供了了管理和终止线程的方法。submit, shutdown等方法。可以跟踪一个或多个异步任务执行状况。

Executors类:相当于是一个工具类,提供了一些静态方法,生成一些常用的线程池。
两个常用的方法:

  • new SingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,相当于单线程串行执行所有任务。如果一个线程因为异常结束,那么会有一个新的线程替代它。此外,该线程池保证所有任务的执行顺序按照任务的提交顺序执行。
  • newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
ExecutorService ex = Executors.newFixedThreadPool(2);
ex.executor(thread1);
ex.executor(thread2);
ex.executor(thread3); //可以看到只有前两个线程执行完了,第三个线程才会执行。
  • newCachedThreadPool 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者JVM)能够创建的最大线程大小。
  • newScheduledThreadPool 创建一个无固定大小的线程池,此线程池支持定时以及周期性执行任务的需求。

在项目中有这么一个需求。我们的一个页面上有四个按钮,每点一个按钮便会发一次请求去Server端拿数据。如果用户快速的点击,那么就会起多个线程。但实际上最终显示在页面上的只会是最后一次请求所拿到的数据。因此我们可以采用这种思路去做,如果用户是快速不断的切换按钮,每次点击都起一个线程,我们将线程交给线程池来管理。那么对于尚未执行的线程,就将其从线程池中清除掉,以下就是示例代码:

singleThreadPool = Executor.newFixedThreadPool(1);
ThreadPoolExecutor pool = (ThreadPoolExecutor)singleThreadPool;
pool.getQueue().clear();
pool.execute(thread);

[Android]一个选择图片的Demo

分享一个之前写的一个选择图片的Feature.

Activity 类

public class PickPictureActivity extends Activity {

    private static final int CODE_OPEN_GALLERY = 1;
    final static int KITCAT_REQUEST_PICK = 1001;
    private static final String TAG = "PickPictureActivity";

    private ArrayList<String> imageUrls = new ArrayList<String>();
    private GridView gvImages;
    public ImageLoader imageLoader = ImageLoader.getInstance();
    private PickPictureAdapter adapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_pick_picture);
        gvImages = (GridView) findViewById(R.id.gv_add_pic);
        ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(getApplicationContext())
                                                                      .memoryCache(new WeakMemoryCache()).build();
        imageLoader.init(config);
        adapter = new PickPictureAdapter(PickPictureActivity.this);
        gvImages.setAdapter(adapter);
        Log.i("Activity", "onCreate execute");

    }
    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        imageUrls = savedInstanceState.getStringArrayList("imageUrls");
    }


    /**
     * Pick a existing picture from gallery.
     */
    public void pickPhotoFromGalley() {
        Log.i("PicPicture", "pic size" + imageUrls.size());
        if (android.os.Build.VERSION.SDK_INT < 19) {
            Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null);
            intent.setType("image/*");
            intent.putExtra("return-data", true);
            startActivityForResult(intent, CODE_OPEN_GALLERY);
        } else {
            Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI);
            intent.setType("image/*");
            startActivityForResult(intent, KITCAT_REQUEST_PICK);
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        Log.i("Activity", "onActivityResult size: " + imageUrls.size() );
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode != RESULT_CANCELED && null != data) {
            Uri uri = data.getData();
            Log.i(TAG, "uri:" + uri);
            if (null != getRealPathFromURI(uri)) {
                Log.i(TAG, getRealPathFromURI(uri));
                imageUrls.add("file://" + getRealPathFromURI(uri));
                adapter.clearItems();
                adapter.addAllItems(imageUrls);
                gvImages.setAdapter(adapter);


            }
        }
    }

    public String getRealPathFromURI(Uri contentUri) {
        Cursor cursor = null;
        try {
            String[] proj = { MediaStore.Images.Media.DATA };
            cursor = getContentResolver().query(contentUri, proj, null, null, null);
            int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
            cursor.moveToFirst();
            return cursor.getString(column_index);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        outState.putStringArrayList("imageUrls", imageUrls);
        super.onSaveInstanceState(outState);
        Log.i("Activity", "onSaveInstanceState execute");
    }

}

值得说明的几点

  • 本例子中图片的加载采用的是Universal-Image-Loader。通常在项目中关于它的配置和初始化应该写到AApp的Application中去,此处为了简单,就直接写在Activity中的,在实际项目中不可以这样写。
    • 在打开Gallery,选择图片时,系统有可能会将Activity销毁,为此需要覆盖其onSaveInstanceState(Bundle outState)方法,否则有可能在选好图片后,返回到当前Activity时发现之前选的图片丢失了。

      Adapter

public class PickPictureAdapter extends BaseAdapter {
    private Context context;
    private List<String> imageList = new ArrayList<String>();
    private ViewHolder holder;

    public PickPictureAdapter(Context context) {
        this.context = context;
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (null == convertView) {
            holder = new ViewHolder();
            LayoutInflater inflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            convertView = inflater.inflate(R.layout.pick_picture_item, null, false);
            holder.ivAddPic = (ImageView)convertView.findViewById(R.id.iv_add_pic);
            holder.ivPic = (ImageView)convertView.findViewById(R.id.iv_my_image);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }
        if (position == imageList.size()) {
            holder.ivAddPic.setVisibility(View.VISIBLE);
            holder.ivPic.setVisibility(View.GONE);
            holder.ivAddPic.setOnClickListener(listener);
        } else {
            holder.ivPic.setVisibility(View.VISIBLE);
            holder.ivAddPic.setVisibility(View.GONE);
            if (null != getItem(position)) {
                ((PickPictureActivity) context).imageLoader.displayImage(imageList.get(position), holder.ivPic);
            }
        }
        return convertView;
    }

    private View.OnClickListener listener = new View.OnClickListener() {

        @Override
        public void onClick(View arg0) {
            PickPictureActivity activity = (PickPictureActivity) context;
            activity.pickPhotoFromGalley();
        }
    };

    class ViewHolder {
        public ImageView ivPic;
        public ImageView ivAddPic;
    }

    @Override
    public int getCount() {
        Log.i("Adapter", "image count:" + imageList.size());
        return imageList.size() + 1;
    }

    @Override
    public Object getItem(int position) {
        if (getCount() > 0) {
            return imageList.get(position);
        }
        return null;
    }

    @Override
    public long getItemId(int arg0) {
        return 0;
    }
    public void clearItems() {
        imageList.clear();
        notifyDataSetChanged();
    }
    public void addAllItems(List<String> refreshList) {
        imageList.addAll(refreshList);
        notifyDataSetChanged();
    }
}

看一下我们的 pic_picture_item.xml

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

    <ImageView
        android:id="@+id/iv_my_image"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:src="@drawable/icon_default"
        android:scaleType="fitXY"
        android:visibility="gone"
        />
    <ImageView
        android:id="@+id/iv_add_pic"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:padding="20dp"
        android:background="@drawable/pick_picture_normal"
        android:src="@drawable/icon_add"
        android:visibility="gone"
        />

</RelativeLayout>

样式定义pick_picture_normal.xm

针对添加按钮周围的边框

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
    <solid android:color="@null" />
    <stroke
        android:dashGap="4dp"
        android:dashWidth="8dp"
        android:width="2dp"
        android:color="#c2c2c2" />
</shape>

上图

screenshot_2014-11-16-23-09-48

[Java] ThreadLocal类

最近在看Universal-Image-Loader源码的时候又碰到了ThreadLocal,依稀记得以前用ThreadLocal写过点东西,于是翻看一下以前的笔记回顾一下,在此处也记录一下。

原理

ThreadLocal,线程局部变量,用来保存与当前线程绑定的一些数据。java规范里面是这么说的。ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread。在类中ThreadLocal的实例通常是private static的 用来与线程绑定,一个线程一个。我们主要是使用里面的get()和set()方法。

ThreadLocal中的方法get()set(T value)方法实际上都是对当前Thread类中的一个Map进行操作。我们可以看到在Thread类中维护着这么一个Map。该Map的类型是ThreadLocal类中的一个静态内部类。

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

应用

根据ThreadLocal是线程独享的这个特性。我们可以利用ThreadLocal中的set(T value)方法将一个与数据库的连接设置进去。那么就可以保证每一个线程一个连接。(数据库连接不能在线程间共享【连接时线程不安全的】,会破坏事务的原子性和隔离性。)

直接上代码:

public class JdbcUtil {
    // 该集合用来保存数据的.
    private static Properties info = new Properties();
    static {
        try {
            InputStream is = JdbcUtil.class
                    .getResourceAsStream("/cfg/config.properties");
            // 用info调用load()方法,可完成读文件,解析字符串等任务,并将内容添加到集合中。
            info.load(is);
            // 用了io流之后,一定要记得关流.
            is.close();
        } catch (Exception e) {
            // 在静态代码块中只能抛出这一个异常。ExceptionInInitializerError(e);
            throw new ExceptionInInitializerError(e);
        }
    }
    /*
     * 注:tl 是final,保证其不会指向其它的对象。ThreadLocal的实例通常都是private static final
     * 一个线程一个数据库连接。
     */
    private static final ThreadLocal<Connection> tl = new ThreadLocal<Connection>();

    public static Connection getConnection() throws Exception {
        // 刚开始由于没有设置值,所以拿到的是null. tl.get()返回的是一个对象。
        Connection conn = tl.get();
        if (conn == null) {
            Class.forName(info.getProperty("driver"));
            conn = DriverManager.getConnection(info.getProperty("url"),
                    info.getProperty("username"), info.getProperty("password"));
            // 将连接放到ThreadLocal中。
            tl.set(conn);
        }
        return conn;
    }

    // 释放资源。但是有一点不完整。如果在加一个参数PreparedStatement pstm,那就完善了。
    public static void release(ResultSet rs, Statement stm, Connection conn) {
        if (rs != null)
            try {
                rs.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        if (stm != null)
            try {
                stm.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        if (conn != null)
            try {
                conn.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
    }
    //为了测试方便就把测试方法写在这了。
    public static void main(String args[]) throws Exception {
        System.out.println(getConnection());
    }
}

[Android]当WebView碰上LinearLayout

直接看代码:

第一种方式:WebView外层套一个LinearLayout

Activity

public class TestWebViewActivity extends Activity {
    private LinearLayout llWraper;
    private WebView webView;
    private static final String TAG = "TestWebViewActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_webview);
        llWraper = (LinearLayout)findViewById(R.id.ll_wraper);
        webView = (WebView)findViewById(R.id.webView_test);
        new Handler().postDelayed(new GetMatricRunnable(), 5000);
    }

    class GetMatricRunnable implements Runnable {

        @Override
        public void run () {
            Log.i(TAG, "LinearLayout Width:" + llWraper.getWidth() + "  Height:" + llWraper.getHeight());
            Log.i(TAG, "WebView Width:" + webView.getWidth() + "  Height:" + webView.getHeight();
        }
    }

}

布局文件

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:my="http://schemas.android.com/apk/res/com.example.test"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <LinearLayout
        android:id="@+id/ll_wraper"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <WebView
            android:id="@+id/webView_test"
            android:layout_width="500dp"
            android:layout_height="match_parent" />
    </LinearLayout>

</RelativeLayout>
  • 运行的结果如下(本人测式的手机是小米2A,分辨率是720*1280):
    screen shot 2014-11-02 at 7 07 22 pm

第二种方式:WebView外层套一个RelativeLayout

Activity

public class TestWebViewActivity extends Activity {
    private RelativeLayout rlWraper;
    private WebView webView;
    private static final String TAG = "TestWebViewActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_webview);
        rlWraper = (RelativeLayout)findViewById(R.id.rl_wraper);
        webView = (WebView)findViewById(R.id.webView_test1);
        new Handler().postDelayed(new GetMatricRunnable(), 5000);
    }
    class GetMatricRunnable implements Runnable {
        @Override
        public void run () {
            Log.i(TAG, "RelativeLayout Width:" + rlWraper.getWidth() + "  Height:" + rlWraper.getHeight());
            Log.i(TAG, "WebView Width:" + webView.getWidth() + "  Height:" + webView.getHeight());
        }
    }
}

布局文件

<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" >

    <RelativeLayout
        android:id="@+id/rl_wraper"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <WebView
            android:id="@+id/webView_test1"
            android:layout_width="500dp"
            android:layout_height="match_parent" />
    </RelativeLayout>

</RelativeLayout>

*运行结果如下:

screen shot 2014-11-02 at 7 12 01 pm

由此可见,若WebView的外层是LinearLayout时,WebView的宽度可以不受设备的实际宽度的影响,给它设置多大的值,它便能渲染成多大。若外层是RelativeLayout时,则会受设备的实际宽度的影响。这是为什么呢?

--》 To Be Continue

[Javascript][为什么不能用SetTimeout和setInterval来精确控制流程执行的时间]

明天端午放假,今晚翻开自己的笔记本(非laptop),看看以前写的笔记,看到了与本文题目相关的内容,于是想记录下来。OK, 进入正题


在回答这个问题之前,先了解一下浏览器的一些基本的东西。在一个浏览器中至少会有三个线程在跑:

  • 浏览器的GUI渲染线程,用来渲染浏览器的页面
  • Javascript执行引擎,它的优先级比GUI渲染线程要高
  • 事件触发线程。

此处可以再研究一下

注意:Javascript脚本只会在一个线程中执行。
于是,有代码如下:

function blockMethod() {
    for (var i = 0; i < 100000000000; i++ ) { 
        i ++; 
        i--;
    }
}

setTimeout(function c(){ alert("aaa") }, 2000);
blockMethod(); 

最初是打算在2秒钟之后弹出aaa,,然后执行blockMethod。实际上发现,aaa没有在2秒钟之后执行,而是过了很久一会再执行。这是因为当程序执行到setTimeout时,javascript执行引擎会让function c两秒钟之后执行,先执行blockMethod这个方法,然而由于blockMethod的执行比较耗时,阻塞了javascript线程,导致只有当blockMethod执行完毕后,alert("aaa")才会过两秒执行。

[Android][CustomView-basic]

在Android开发中难免会需要用到自定义的View.本文结合实际项目中的一个自定义的TextView来阐明自定义组件开发的步骤。
需求: 自定义一个TextView用来显示文字,要求当文字的长度超过TextView给定长度时,自动缩减显示的文字。可以省略尾部的文字或者省略中间的文字。

在attr.xml中定义CustomView的属性
<resources>     
    <declare-styleable name="CustomerTextView">    
        <attr name="ellipsize" format="string"/>    
    </declare-styleable>    
</resources>  

代码

public class CustomerTextView extends TextView{
    private boolean isEllipsize;
    private TruncateAt where;

    public CustomerTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //采用TypedArray来获取自定义属性,使用完后记得recycle.
        final TypedArray t = context.obtainStyledAttributes(attrs, R.styleable.CustomerTextView);
        final String ellipsize = t.getString(R.styleable.CustomerTextView_ellipsize);
        if (null != ellipsize && "end".equals(ellipsize)) {
            isEllipsize = true;
            where = TextUtils.TruncateAt.END;
        } else if (null != ellipsize && "middle".equals(ellipsize)) {
            isEllipsize = true;
            where = TextUtils.TruncateAt.MIDDLE;
        } 
        t.recycle();
    }

    @Override
    public boolean onPreDraw() {
        if (isEllipsize) {
           setText(TextUtils.ellipsize(getText(), this.getPaint(), this.getLayoutParams().width - 5, where)); 
        }
        return super.onPreDraw();
    }
}

在布局文件中的使用

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:myEllipise="http://schemas.android.com/apk/res/com.example.test" 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

     <com.example.test.widget.CustomerTextView
        android:layout_width="100dp"    
        android:layout_height="30dp"  
        android:text="abcdefghijklmnopqrst"     
        myEllipise:ellipsize="middle"
        />

      <com.example.test.widget.CustomerTextView
          android:layout_width="100dp"
          android:layout_height="30dp"
          android:layout_alignParentLeft="true"
          android:layout_alignParentTop="true"
          android:layout_marginTop="65dp"
          android:text="abcdefghijklmnopqrst"
          myEllipise:ellipsize="end" />

</RelativeLayout>

** 注意布局文件中的第三行 xmlns:myEllipise="http://schemas.android.com/apk/res/com.example.test"res/后接的是本应用的包名。

  • 最终效果如下图所示

image

[Android][NFC start]

1.什么是NFC?
NFC(Near Field Communication) 是一种短距离的无线通讯技术。通常要求设备之间的距离要小于4cm.其数据的传输速度在106kbit/s到848kbit/s。和其它的无线技术(Bluetooth, WiFi)不一样的是NFC的功耗更低,NFC通讯的发生不一定要求接收者和发起者都是带电的。如利用手机读取公交卡中的余额。一个具有NFC硬件的android设备,当屏幕处于非锁定状态的时候,都会扮演一个发起者的角色,当检测到一个NFC tag的时候,便会调用相应的Activity去处理。

  1. Android中关于NFC的两个包
    2.1. android.nfc包中的几个类:NfcManger, NfcAdapter, NdefMessage, NdefRecord, Tag.

NfcManager 可以获得该android设备的NFC adapters. 而大多数的android设备都只会有一个NFC adapter, 所以多数情况下,我们只需要调用getDefaultAdapater(Context)方法便可以获得 adapter.
NfcAdapter: NfcAdapter中封装了intent.可以通过NfcAdapter获得intent中的数据。
NdefMessage: NDEF是一个数据结构用来在NFC tags中高效的存储数据。如文本,URL, 或其它的 MIME类型。NdefMessage是一个用来传递、读取数据的容器。一个NdefMessage对象包含了0个或多个NdefRecords. NDEF message中定义第一个NDEF record用来负责传递tag到activity.
Tag: Tag代表了一个被动的NFC taget.如公交卡之类的东东。当一个tag被检测到,一个Tag对象也就创建了,并且封装到intent中,NFC分发系统会将这个intent送到相应的Activity进行处理。调用Tag的getTechList()方法可以检测到这个Tag所支持的technology.

2.2 android.nfc.tech包,该包中包含了对tag进行属性查询,IO操作的类。TagTechnology, NfcA, NfcB, NfcF,NfcVIsoDep等。[看API吧]

3.开发一个NFC的应用:
3.1 权限的声明:

<uses-permission android:name="android.permission.NFC" />
<uses-sdk android:minSdkVersion="10"/>
<uses-feature android:name="android.hardware.nfc" android:required="true" />

3.2 对于处理NFC的Activity的配置:

<intent-filter>
  <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
  <data android:mimeType="mime/type" />
</intent-filter>

<intent-filter>
  <action android:name="android.nfc.action.TECH_DISCOVERED"/>
  <meta-data android:name="android.nfc.action.TECH_DISCOVERED"
                android:resource="@xml/nfc_tech_filter.xml" />
</intent-filter>

<intent-filter>
  <action android:name="android.nfc.action.TAG_DISCOVERED"/>
</intent-filter>

3.3关于Tag的分发系统:
在高版本的android API中,有一个很好的图画出了Tag分发的流程,无奈Google最近都是访问不了。只有从电脑里找一个低版本的看看。

当一个android设备检测到一个NFC tag时,系统会去寻找最合适的Activity来handle这个intent.而不会问用户去选取某个应用程序。因为当设备在检测NFC tag时,设备与NFC tag
之间的距离非常近,如果让用户再去选择程序,用户很容易移动手机,导致距离拉长,连接中断。Android平台提供了两种机制让用户的Activity正确响应某个NFC tag: 采用
intent 分发系统,和 前台activity 分发系统。

intent分发系统会检查所有activity的 intent filter 和数据类型,通过这种方式来定位响应的activity.如果多个Activity定义了相同的intent filter 和 data, 系统将会让用户选择某一个。

  • android.nfc.action.NDEF_DISCOVERED:这是NFC中最高优先级的intent.当检测到一个NDEF payload时执行。当然可以顺便指定处理的数据类型。如下:
<intent-filter>
    <action android:name="android.nfc.action.NDEF_DISCOVERED"/>
    <data android:mimeType="text/plain" />
</intent-filter>
  • android.nfc.action.TECH_DISCOVERED: 当检测到的Tag是已知类型,如果NDEF_DISCOVERED intent没有启动,或者没有被任何activity处理,此时这个intent将会启动。
  • TECH_DISCOVERED intent要求在xml文件中定义支持的technology. 如果在AndroidManifest.xml文件中对某个Activity声明了 android.nfc.action.TECH_DISCOVERED intent.
    如:
<activity>
.....
 <intent-filter>
    <action android:name="android.nfc.action.TECH_DISCOVERED"/>
</intent-filter>

<meta-data android:name="android.nfc.action.TECH_DISCOVERED"
    android:resource="@xml/nfc_tech_filter" />
...
</activity>

则需要在xml中(对应于上面的 nfc_tech_filter)进行定义,如下:

<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.IsoDep</tech>
        <tech>android.nfc.tech.NfcA</tech>        
        <tech>android.nfc.tech.NfcB</tech>
        <tech>android.nfc.tech.NfcF</tech>
        <tech>android.nfc.tech.NfcV</tech>
        <tech>android.nfc.tech.Ndef</tech>
        <tech>android.nfc.tech.NdefFormatable</tech>
        <tech>android.nfc.tech.MifareClassic</tech>
        <tech>android.nfc.tech.MifareUltralight</tech>
    </tech-list>
</resources>
  • android.nfc.action.TAG_DISCOVERED: 如果NDEF_DISCOVERED和TECH_DISCOVERED都没有执行,或者检测到的Tag是未知类型的。

一个Demo:

  1. AndroidManifest.xml文件:
 <activity
     android:name="com.example.testnfc.MainActivity"
     android:label="@string/app_name"
     android:launchMode="singleTop" >
     <intent-filter>
         <action android:name="android.intent.action.MAIN" />

         <category android:name="android.intent.category.LAUNCHER" />
     </intent-filter>
     <intent-filter>
         <action android:name="android.nfc.action.TECH_DISCOVERED" />
     </intent-filter>

     <meta-data
         android:name="android.nfc.action.TECH_DISCOVERED"
         android:resource="@xml/nfc_tech_filter" />

     <intent-filter>
         <action android:name="android.nfc.action.TAG_DISCOVERED" />

         <category android:name="android.intent.category.DEFAULT" />
     </intent-filter>

 </activity>

2.nfc_tech_filter.xml

<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
    <tech-list>
        <tech>android.nfc.tech.IsoDep</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NfcV</tech>
    </tech-list>
    <tech-list>
        <tech>android.nfc.tech.NfcF</tech>
    </tech-list>
</resources>
  1. 一个Activity
public class MainActivity extends Activity {
    NfcAdapter nfcAdapter;  
    TextView promt;  

    public static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        promt = (TextView) findViewById(R.id.promt);  
        // 获取默认的NFC控制器  
        nfcAdapter = NfcAdapter.getDefaultAdapter(this);  
        if (nfcAdapter == null) {  
            promt.setText("设备不支持NFC!");  
            finish();  
            return;  
        }  
        if (!nfcAdapter.isEnabled()) {  
            promt.setText("请在系统设置中先启用NFC功能!");  
            finish();  
            return;  
        }  
    }
    @Override
    protected void onResume() {
        super.onResume();
        Log.i("onResume","------------" + getIntent().getAction());
      //得到是否检测到ACTION_TECH_DISCOVERED触发  
        if (NfcAdapter.ACTION_TECH_DISCOVERED.equals(getIntent().getAction())) {  

           //取出封装在intent中的TAG  
            Tag tag = getIntent().getParcelableExtra(NfcAdapter.EXTRA_TAG);  
            Log.i("tagFromIntent","---------" + tag.toString());
        }          
    }
}

采用武汉市公交卡进行测试,发现对于这种卡采用的tech是 [android.nfc.tech.IsoDep, android.nfc.tech.NfcA]
// TBD

[PHP]XAMPP在Mac环境下的配置

XAMPP在Mac环境下的配置

最近在想接着学学PHP,于是准备在电脑上搭一个环境,在网上搜资料,发现相关的帖子很多,但很多描述都是错误的,真是害人啊,浪费我不少时间。后来参看这个博客终于搭好了,感谢作者。在此处也记录一下具体的步骤,方便以后查找。

下载和安装

1.可以点击此处下载
2.安装XAMPP, 此处不多讲解,双击安装就好,系统会将其安装到我们的Application路径下,如下图
screen shot 2014-11-22 at 3 38 10 pm

启用虚拟路径的配置

  1. 打开 /Applications/XAMPP/xamppfiles/etc/httpd.conf 文件,找到其中的
# Virtual hosts
#Include /Applications/XAMPP/etc/extra/httpd-vhosts.conf

将此处的注释解开。意思是会启用httpd-vhosts.conf文件,这样我们才能配置虚拟路径。

添加虚拟路径

首先在/Applications/XAMPP/xamppfiles/etc/extra/httpd-vhosts.conf 文件先把已存在的两个虚拟路径的配置注释掉,或者删掉,然后在末尾添加

# localhost
<VirtualHost *:80>
    ServerName localhost
    DocumentRoot "/Applications/XAMPP/xamppfiles/htdocs"
    <Directory "/Applications/XAMPP/xamppfiles/htdocs">
        Options Indexes FollowSymLinks Includes execCGI
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

# My custom host
<VirtualHost *:80>
    ServerName mysite.com
    DocumentRoot "/Users/fred/Documents/tech/php"
    <Directory "/Users/fred/Documents/tech/php">
        Options Indexes FollowSymLinks Includes ExecCGI
        AllowOverride All
        Require all granted
    </Directory>
</VirtualHost>

第一个虚拟路径是针对XAMPP的,用来访问XAMPP的控制台。第二个才是我们自己的站点。

修改hosts文件

  sudo vi /etc/hosts

我的改成这个样子:
screen shot 2014-11-22 at 3 52 45 pm

注:修改hosts文件需要root权限。
修改完成后重新启动apache。访问mysite.com,发现有403错误,这是因为apache默认是以daemon 的角色运行,于是打开httpd.conf 文件,找到

#User/Group: The name (or #number) of the user/group to run httpd as.
#It is usually good practice to create a dedicated user and group for
#running httpd, as with most system services.
User daemon
Group daemon

   User daemon

换成

  User Fred

注明:我当前使用的帐号名是Fred。到此配置完成,再访问mysite.com发现可以访问了。

[DataBase]MongoDB的基本操作

MongoDB的基本操作

MongoDB的启动

进入mongodb安装文件的根目录,运行mongod --dbpath=, dbpath=是指定数据库的位置。其使用如下所示:

FreddeMacBook-Air:software fred$ cd mongodb
FreddeMacBook-Air:mongodb fred$ ls
GNU-AGPL-3.0        THIRD-PARTY-NOTICES data
README          bin
FreddeMacBook-Air:mongodb fred$ bin/mongod --dbpath=./data

至此,数据库启动成功。
在windows下对于--dbpath后接的路径最好加上引号,如:

I:\fred\mongodb>mongod --dbpath="I:/fred/mongodb/data"

当然也可以在启动的时候指定日志的输出,代码如下:

FreddeMacBook-Air:mongodb fred$ bin/mongod --dbpath=./data --logpath= /Users/fred/Documents/tech/software/mongodb/logs/mongodb.log

让mongodb在后台运行,加上-fork参数

bin/mongod --dbpath=../data --logpath=../logs/mongodb.log -fork

其实和Mysql有些类似,Mysql启动的时候也是先运行mysqld启动数据库,然后再运行mysql命令进行数据库的相关操作。Mongo数据库也是一样,运行mongod启动数据库后,接着再开一个命令行窗口,运行mongo命令来执行数据库的相关操作。

退出mongo输入exit或者直接Ctrl + C

数据库的基本操作

默认情况下,登录到mongodb中,自动连接到test数据库,如下:

FreddeMacBook-Air:bin fred$ ./mongo
MongoDB shell version: 2.6.5
connecting to: test
> show dbs;
admin  (empty)
local  0.078GB
test   0.453GB
> exit;

采用show dbs查看当前有多少个数据库, use db_name(如:amin)是切换数据库, show collections是查看当前数据库下有多少个collection. mongo中的collection类似于关系型数据库中的表。

数据库和Collection的创建

在MongoDB中,数据库和Collection都是隐式创建的。无需手动创建,要使用时自动创建。如下:

> show dbs;
admin  (empty)
local  0.078GB
test   0.453GB
> use aa
switched to db aa
> show dbs;
admin  (empty)
local  0.078GB
test   0.453GB
> aa.c1.insert({name:'admin',password:'password1'});
2014-12-28T22:32:53.749+0800 ReferenceError: aa is not defined
> use aa
switched to db aa
> db.c1.insert({name:'admin',password:'password1'});
WriteResult({ "nInserted" : 1 })
> show dbs;
aa     0.078GB
admin  (empty)
local  0.078GB
test   0.453GB
> show collections
c1
system.indexes
> 

最初我的数据库中只有admin,local,test三个数据库,但是当我执行了切换到数据库aa,并执行db.c1.insert({name:'admin',password:'password1'}后,再执行show dbs发现aa数据库已经创建好了,如果用show collections会发现collection c1也创建了。

  • 删除集合 db.c1.drop()
  • 删除数据库 db.dropDatabase()
  • 显式创建数据库 db.createCollection("c1")

MongoDB的数据类型

MongoDB支持string, integer, boolean, double ,null, array object等基本数据类型,还有data, object id, binary data, regular expression, code

插入

采用insert方法,插入的内容是json串,如下:

db.c1.insert({name:"user1"});
db.c1.insert({name:"user1"});
  • 对于insert方法,若主键相同,则插入不成功,也可用save方法,save方法是主键相同则修改,不同则插入。如果不指定_id,mongodb会自动为我们生成一个。
  • 一次插入多条记录
for(i =1; i< 30; i++) {
    db.c1.insert({name:"user"+i});
}

更新

采用update方法,使用如下:

db.collection.update( criteria, objNew, upsert, multi )

update()函数接受以下四个参数:

  • criteria : update的查询条件,类似sql update查询内where后面的。
  • objNew : update的对象和一些更新的操作符(如$,$inc...)等,也可以理解为sql update查询内set后面的
  • upsert : 这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入。默认是false,代表如果记录不存在,也不新增。
    multi : mongodb默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。
    摘自这里.

常见的操作示例:db.c1.update({name:"user3"}, {$set:{name:"user300"}});将 name值为user3改为user300.注意:默认情况下,只会更新第一个符合条件的记录。若想更新行:db.c5.update({name:"user1",{$set:{name:"user111"}},0 ,1)

操作符 说明
{$inc: {field : value}} 为某个字段添加指定值
{$set : {field : value}} 为某个字段设置值,当field不存在时,添加字段。
{$unset : {field : value}} 删除给定的字段
{$push : {field : value}} 将值追加到field中。
{$pushAll : {field : value_array}} 将数组追加到field中
{$pop : {field : index}} 删除数组中第index元素,index从1开始
{$pull : {field : value}} 删除数组中符合条件的记录
{$pullAll : {field : value_array}} 和$pull类似,只是此时的value_array是一个数组,可以同时删除多个。
{$rename: {old_field_name : new_field_name}} 修改字段名

说明:

  1. 对于$push:要求field类型是一个数组,如果field已经存在,就把value追加给field, 如果filed原来不存在,就新增一个字段,并赋值。如果field存在,但不是一个数组,将报错。
  • db.c1.update({name:"user1",{$inc:{age:1}}})
  • db.c2.update({name:"user1"}, {$pull:{fruit: {$nin:['apple']}}});

删除

删除采用remove方法。

  1. 删除所有记录 db.c3.remove({})
  2. 删除指定条件记录 db.c3.remove({name: "user1"})

查询

  1. 查询某个collection中所有数据 db.c1.find()db.c1.find({})效果是一样的.

  2. 可以指定查询条件:db.c1.find({name:"user5"});

  3. 指定返回的列:db.c1.find({name:"user5", {name:1}),在默认情况下,会返回_id这一列,如果不想返回这一列,可以写成db.c1.find({name:"user5", {name:1, _id: 0})

  4. 查找时的条件限制。如

    用途 操作符
    大于(>) $gt (grater than)
    小于(<) $lt (lower than)
    小于等于(<=) $lte
    大于等于(>=) $gte
    不等于 $ne
    字段是否存在 $exists
    数据存在于 $in
    数据不存在于 $nin
    或者 $or
    全部包含 $all
    数组长度 $size
    • db.c1.find({age:{$lt:19}})
    • db.c1.find({age:{$lt:19, $gt:3}})指定区间
    • db.c1.find({age:{exists:1}})
    • db.c1.find({age:{$in:[1,3,5]}}) $in操作类似于传统关系型数据库中的in
    • db.c1.find({age:{$nin:[1,3,5]}})
    • db.c1.find({$or: [{name:"user1"},{name:"user2"}]});
    • $all操作类似于$in,但$all要全数组里面的值全部包含在记录中。如db.c1.find({a:{$all:[1,2,3]}})返回的记录中字段a中必须要包含[1,2,3]。
    • db.c1.find({a:{$size:4}}})字段a是一个数组,且该数组有4个元素。
  5. 数量统计 db.c1.find().count()

  6. 排序:db.c1.sort({age:1})按age的升序排列,db.c1.sort({age:-1})按age的降序排列。

  7. 分页

    1. db.c1.find().limit(4) 从0开始输出4个。
    2. db.c1.find().skip(5).limit(5)跳过前5个,再取5个,相当于mysql中的SELECT * FROM USER LIMIT 5,5.
    3. 一个综合一点的例子:db.c1.find().sort({age:-1}).skip(2).limit(5).count(1);注意当查询语句中有skip, limit时,默认情况下,count会忽略这些条件,因此此处必须要设置count(1),否则前面的条件会没有利用上。
  8. 正则表达式: MongoDB中同样支持正则表达式查询,写得少,先空着哦。

  9. 去重: disctinct, db.c1.distinct("name")查询不同的name.

  10. 游标的使用

var cur = db.c1.find();
cur.forEach(function(x){print(tojson(x))});
  1. 关于null的查询
> db.c2.insert({name:"user1", nickname:"big god"});
WriteResult({ "nInserted" : 1 })
> db.c2.insert({name:"user2", nickname: null});
WriteResult({ "nInserted" : 1 })
> db.c3.insert({name:"user3"});
WriteResult({ "nInserted" : 1 })

查询:

> db.c2.find({nickname: null});
{ "_id" : ObjectId("54a4de74458e722da12097e9"), "name" : "user2", "nickname" : null }
> db.c2.find({nickname:{$exists: false}});
> db.c2.find({nickname:{type:10}});

  1. $slice
    • db.c1.find({},{post:{$slice: 2}}); //得到前两条post
    • db.c1.find({},{post:{$slice: -2}}); //得到后两条post
    • db.c1.find({},{post:{$slice: [20, 10]}}); //从第20条开始,取10条。
> db.c1.find();
{ "_id" : ObjectId("54a4db30458e722da12097e5"), "name" : "user1", "post" : [ 1, 2 ] }
{ "_id" : ObjectId("54a4db41458e722da12097e6"), "name" : "user2", "post" : [ 3, 4, 5 ] }
{ "_id" : ObjectId("54a4db57458e722da12097e7"), "name" : "user3", "post" : [ 6, 7, 8 ] }
> db.c1.find({},{post:{$slice: 2}}); //得到前两条post.
{ "_id" : ObjectId("54a4db30458e722da12097e5"), "name" : "user1", "post" : [ 1, 2 ] }
{ "_id" : ObjectId("54a4db41458e722da12097e6"), "name" : "user2", "post" : [ 3, 4 ] }
{ "_id" : ObjectId("54a4db57458e722da12097e7"), "name" : "user3", "post" : [ 6, 7 ] }

索引

MongoDB中会对_id字段默认建立了索引。

  • 查看数据库中的所有索引
> db.system.indexes.find();
{ "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.c1" }
{ "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.c2" }
{ "v" : 1, "key" : { "_id" : 1 }, "name" : "_id_", "ns" : "test.c3" }
  • 建立索引用ensureIndex, db.c1.ensureIndex({name:1});
  • 建立唯一索引db.c1.ensureIndex({personal_id:1},{unique:true})
  • 查看某个collection上的索引db.c1.getIndexKeys()db.c1.getIndexes()
  • 建立索引的目的是为了加快查询速度,可以用explain()方法观察查询的状态,如db.c1.find({name:"user1"}).explain()
  • 删除索引db.c1.dropIndex({age:1})
  • 删除collection上的所有索引 db.c1.dropIndexes()删除所有的索引,但id的索引除外,id索引是不能删除的,但存在于system表中。

固定集合

MongoDB中有着固定大小的集合(普通集合是没有大小限制的,随着数据的增多会不断的增大),以LRU(Least Recently Used最近最少使用)规则和插入顺序进行age-out(老化移除)处理,自动维护集合中对象的插入顺序,在创建是要预指定大小,如果空间用完,新添加的对象将会取代集合中最旧的对象,永远保持最新的数据。

  1. 插入速度极快
  2. 按插入顺序的查询输出速度极快
  3. 能够在插入最新数据时,淘汰最早的数据。
    可用来储存日专信息,缓存少量文档。

固定集合需要显式的创建使用·createCollection

db.createCollection("c2", {capped:true,size:10000,max:5});//大小是10000字节,最多5条。
db.c2.stats();
db.c2.insert({name:"user1"});
db.c2.insert({name:"user2"});
db.c2.insert({name:"user3"});
db.c2.insert({name:"user4"});
db.c2.insert({name:"user5"});
db.c2.insert({name:"user6"});

执行以上代码,会发现user1这条记录已经被移掉了。

将普通集合转为固定集合:db.runCommand({convertTocapped:"c1",size:1000,max:3});

性能优化

  1. 创建索引
  2. 限定返回结果条数
  3. 查询使用到的字段,不查询所有字段
  4. 使用固定集合
  5. 使用慢查询日志。db.c1.find({name:"user1"}).explain()

用户的管理

mongo中是有一个超级管理员,还有某个数据库的管理员两种。超级管理员要放到admin表中。
在mongo中要添加 --auth 增加安全性,用户授权。

添加用户用addUser,为用户授权采用auth。对于addUser,如果用户名相同,则相当于是修改密码的操作。

> db.system.users.find();
> use admin
switched to db admin
> db.addUser("admin", "admin");
WARNING: The 'addUser' shell helper is DEPRECATED. Please use 'createUser' instead
Successfully added user: { "user" : "admin", "roles" : [ "root" ] }
> db.auth("admin","admin");
1
> db.system.users.find();
{ "_id" : "admin.admin", "user" : "admin", "db" : "admin", "credentials" : { "MONGODB-CR" : "7c67ef13bbd4cae106d959320af3f704" }, "roles" : [ { "role" : "root", "db" : "admin" } ] }
> db.system.users.remove({user:"admin"});//删除某个用户

对于某个database添加用户,可以采用

> use test
switched to db test
> db.addUser("admin", "admin", true);//true表示只读

PHP 操作MongoDB

连接mongodb (conn.php)

<?php
    $conn = new Mongo("mongodb://irunning:[email protected]:27017/irunning");
    $db = $conn->irunning;

查询 (find.php)

<?php
    include "conn.php";
    $user = $db->user;
    $condiction = array();
    $rs = $user->find($condiction);
    foreach ($rs as $val) {
        $id = $val['_id'];
        $username = $val['name'];
        echo "<a href=user.php?id=$id>$username</a></br>";
    }
    $conn->close();

详细 (user.php)

<?php
    include "conn.php";
    $user = $db->user;
    $oid = new MongoId($_GET['id']);
    $arr = array("_id" => $oid);
    $rs = $user->find($arr);
    foreach ($rs as $val) {
        print_r($val);
    }
    $conn->close();

插入(insert.php)

<?php
    include "conn.php";
    $user = $db->user;
    $array = array("name"=>'leo', "password"=>"password1", "age"=>"30");
    if($user->insert($array)) {
        echo "<script>location = 'find.php'</script>";
    } else {
        echo "insert fail";
    }
    $conn->close();

更新(update.php)

<?php
    include "conn.php";
    $user = $db->user;
    $arr = array("name"=>"leo");
    $valueArr = array('$set'=>array("password"=>"abc123_", "age"=>3));
    $opts = array("upsert"=> 0 , "multiple"=>1);
    if ($user->update($arr, $valueArr, $opts)) {
        echo "<script>location = 'find.php'</script>";
    } else {
        echo "update fail"; 
    }
    $conn->close();

删除(delete.php)

<?php
    include "conn.php";
    $user = $db->user;
    $arr = array("name"=>"leo");
    if ($user->remove($arr)) {
        echo "<script>location = 'find.php'</script>";
    } else {
        echo "delete fail";
    }
    $conn->close();

[Android][开源**客户端]学习2-采用commons-httpclient与服务器进行交互

  • commons-httpclientapache下面的一个开源项目,用来模似浏览器与服务器进行交互。OS China Android App就是采用这个开源项目与服务器进行通信。
  • OS China Android App中与服务器的通信的方法全部都在net.oschina.app.api包下的ApiClient类中,全部都是静态的方法。查看源代码
  • 除了一些Business相关的方法外,OS China Android Appcommons-httpclient方法进行了封装,使调用更加方便。如下:
  • 获取HttpClient对象
private static HttpClient getHttpClient() {        
    HttpClient httpClient = new HttpClient();
    // 设置 HttpClient 接收 Cookie,用与浏览器一样的策略
    httpClient.getParams().setCookiePolicy(CookiePolicy.BROWSER_COMPATIBILITY);
    // 设置 默认的超时重试处理策略
    httpClient.getParams().setParameter(HttpMethodParams.RETRY_HANDLER, new DefaultHttpMethodRetryHandler());
    // 设置 连接超时时间
    httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(TIMEOUT_CONNECTION);
    // 设置 读数据超时时间 
    httpClient.getHttpConnectionManager().getParams().setSoTimeout(TIMEOUT_SOCKET);
    // 设置 字符集
    httpClient.getParams().setContentCharset(UTF_8);
    return httpClient;
}   
  • 获取GetMethod
private static GetMethod getHttpGet(String url, String cookie, String userAgent) {
    GetMethod httpGet = new GetMethod(url);
    // 设置 请求超时时间
    httpGet.getParams().setSoTimeout(TIMEOUT_SOCKET);
    httpGet.setRequestHeader("Host", URLs.HOST);
    httpGet.setRequestHeader("Connection","Keep-Alive");
    httpGet.setRequestHeader("Cookie", cookie);
    httpGet.setRequestHeader("User-Agent", userAgent);
    return httpGet;
}
  • 获取PostMethod
private static PostMethod getHttpPost(String url, String cookie, String userAgent) {
    PostMethod httpPost = new PostMethod(url);
    // 设置 请求超时时间
    httpPost.getParams().setSoTimeout(TIMEOUT_SOCKET);
    httpPost.setRequestHeader("Host", URLs.HOST);
    httpPost.setRequestHeader("Connection","Keep-Alive");
    httpPost.setRequestHeader("Cookie", cookie);
    httpPost.setRequestHeader("User-Agent", userAgent);
    return httpPost;
}
  • 发送Get请求
private static InputStream http_get(AppContext appContext, String url) throws AppException {    
    //System.out.println("get_url==> "+url);
    String cookie = getCookie(appContext);
    String userAgent = getUserAgent(appContext);

    HttpClient httpClient = null;
    GetMethod httpGet = null;

    String responseBody = "";
    int time = 0;
    do{
        try 
        {
            httpClient = getHttpClient();
            httpGet = getHttpGet(url, cookie, userAgent);           
            int statusCode = httpClient.executeMethod(httpGet);
            if (statusCode != HttpStatus.SC_OK) {
                throw AppException.http(statusCode);
            }
            responseBody = httpGet.getResponseBodyAsString();
            //System.out.println("XMLDATA=====>"+responseBody);
            break;              
        } catch (HttpException e) {
            time++;
            if(time < RETRY_TIME) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e1) {} 
                continue;
            }
            // 发生致命的异常,可能是协议不对或者返回的内容有问题
            e.printStackTrace();
            throw AppException.http(e);
        } catch (IOException e) {
            time++;
            if(time < RETRY_TIME) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e1) {} 
                continue;
            }
            // 发生网络异常
            e.printStackTrace();
            throw AppException.network(e);
        } finally {
            // 释放连接
            httpGet.releaseConnection();
            httpClient = null;
        }
    }while(time < RETRY_TIME);

    responseBody = responseBody.replaceAll("\\p{Cntrl}", "");
    if(responseBody.contains("result") && responseBody.contains("errorCode") && appContext.containsProperty("user.uid")){
        try {
            Result res = Result.parse(new ByteArrayInputStream(responseBody.getBytes()));   
            if(res.getErrorCode() == 0){
                appContext.Logout();
                appContext.getUnLoginHandler().sendEmptyMessage(1);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }           
    }
    return new ByteArrayInputStream(responseBody.getBytes());
}
  • 发送POST 请求
private static InputStream _post(AppContext appContext, String url, Map<String, Object> params, Map<String,File> files) throws AppException {
    //System.out.println("post_url==> "+url);
    String cookie = getCookie(appContext);
    String userAgent = getUserAgent(appContext);

    HttpClient httpClient = null;
    PostMethod httpPost = null;

    //post表单参数处理
    int length = (params == null ? 0 : params.size()) + (files == null ? 0 : files.size());
    Part[] parts = new Part[length];
    int i = 0;
    if(params != null)
    for(String name : params.keySet()){
        parts[i++] = new StringPart(name, String.valueOf(params.get(name)), UTF_8);
        //System.out.println("post_key==> "+name+"    value==>"+String.valueOf(params.get(name)));
    }
    if(files != null)
    for(String file : files.keySet()){
        try {
            parts[i++] = new FilePart(file, files.get(file));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        //System.out.println("post_key_file==> "+file);
    }

    String responseBody = "";
    int time = 0;
    do{
        try 
        {
            httpClient = getHttpClient();
            httpPost = getHttpPost(url, cookie, userAgent);         
            httpPost.setRequestEntity(new MultipartRequestEntity(parts,httpPost.getParams()));              
            int statusCode = httpClient.executeMethod(httpPost);
            if(statusCode != HttpStatus.SC_OK) 
            {
                throw AppException.http(statusCode);
            }
            else if(statusCode == HttpStatus.SC_OK) 
            {
                Cookie[] cookies = httpClient.getState().getCookies();
                String tmpcookies = "";
                for (Cookie ck : cookies) {
                    tmpcookies += ck.toString()+";";
                }
                //保存cookie   
                if(appContext != null && tmpcookies != ""){
                    appContext.setProperty("cookie", tmpcookies);
                    appCookie = tmpcookies;
                }
            }
            responseBody = httpPost.getResponseBodyAsString();
            //System.out.println("XMLDATA=====>"+responseBody);
            break;          
        } catch (HttpException e) {
            time++;
            if(time < RETRY_TIME) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e1) {} 
                continue;
            }
            // 发生致命的异常,可能是协议不对或者返回的内容有问题
            e.printStackTrace();
            throw AppException.http(e);
        } catch (IOException e) {
            time++;
            if(time < RETRY_TIME) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e1) {} 
                continue;
            }
            // 发生网络异常
            e.printStackTrace();
            throw AppException.network(e);
        } finally {
            // 释放连接
            httpPost.releaseConnection();
            httpClient = null;
        }
    }while(time < RETRY_TIME);

    responseBody = responseBody.replaceAll("\\p{Cntrl}", "");
    if(responseBody.contains("result") && responseBody.contains("errorCode") && appContext.containsProperty("user.uid")){
        try {
            Result res = Result.parse(new ByteArrayInputStream(responseBody.getBytes()));   
            if(res.getErrorCode() == 0){
                appContext.Logout();
                appContext.getUnLoginHandler().sendEmptyMessage(1);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }           
    }
    return new ByteArrayInputStream(responseBody.getBytes());
}
  • 有那么几点需要注意:
    • 每次请求完成后一定要记得调用相应的releaseConnection()方法,释放连接资源,减少客户端和服务器的资源消耗。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.