精通安卓性能优化-第五章(一)

第五章 多线程与同步

第一章介绍了主线程或者叫UI线程的思想,大多数的事件在这里被处理。尽管不阻止你在主线程执行所有的代码,应用通常使用多于一个线程。事实上,即使你没有自己创建新线程,几个线程被创建并且作为应用的一部分。比如,当应用运行在基于Android 3.1的Galaxy Tab 10.1,Eclipse的DDMS视图显示了这些线程:
(1) main
(2) HeapWorker
(3) GC(Garbage Collector)
(4) Signal Catcher
(5) JDWP(Java Debug Wire Protocol)
(6) Compiler
(7) Binder Thread #1
(8) Binder Thread #2

现在,我们只讨论了列表中的第一项,主线程,所以你很可能对额外的七个没有预期。好消息是,你不需要考虑其他的线程,大多数主要为了housekeeping而存在;另外,你对他们没有多少控制。你应该集中于主线程,在主线程不执行长时间的操作,保持应用的响应。
Android产生的特殊的housekeeping线程依赖于应用运行于哪个版本的Android设备上。比如,Android 2.1产生6个线程(和上面列出的8个不同),因为在Android 2.3或者以上的版本GC运行在一个独立线程,JIT编译器在android 2.2引入。
在本章你将学习如何创建自己的线程,他们之间如何通信,线程间的对象如何安全的共享,及如何使得你的代码利用设备多线程的优势。我们将review在安卓应用中使用多线程如何避免普遍的陷阱。

Threads

一个Thread对象,即Java定义的线程类的一个实例,是有自己调用栈的一个执行单元。应用可以很简单的创建额外的线程,如Listing 5-1所示。当然,你的应用可以自由的在主线程之外创建额外的线程执行一些操作;为了保持应用的响应,你经常需要这么做。

Listing 5-1 创建两个线程

// run()方法被简单的重写
Thread thread1 = new Thread("cheese1") {
    @Override
    public void run() {
        Log.i("thread1", "I like Munster");
    }
};

// 或者可以传递一个Runnable对象给Thread构造器
Thread thread2 = new Thread(new Runnable() {
    public void run() {
        Log.i("thread2", "I like Roquefort");
    }
}, "cheese2");

// 记得调用start(),否则这些线程不会被孵化出,任何事情不会发生
thread1.start();
thread2.start();

执行这段代码很可能得到不同的结果。因为每个线程是一个独立的执行单元,两个线程有同样的默认优先级,尽管thread1首先start没有什么可以保证"I like Munster" 比"I like Roquefort" 更早的显示。实际的结果依赖于调度,和实现相关(理解这里的实现可能是指VM的实现)。

NOTE:一个通常的错误是调用run()方法而不是start()方法。这将导致Thread对象(或者Runnable对象)的run()方法在当前线程调用,换句话说,不会产生新的线程。

上面两个线程只是简单的启动,没有期望结果发送给孵化出他们的线程。尽管有些时候这是期望的效果,你经常需要去获取一些执行在不同的线程里面的排列结果。比如,你的应用为了保持应用的响应,需要在一个单独的线程计算一个Fibonacci数,但是期望使用计算结果更新用户界面。这个场景在Listing 5-2给出,mTextView是TextView控件的一个引用,onClick()是用户点击按钮的一个调用,或者一个普通的View(在XML 布局文件中看android:onClick属性)

Listing 5-2 计算Fibonacci数的工作线程

public void onClick(View v) {
    new Thread(new Runnable() {
        public void run() {
            // 注意这里的"final"关键字(尝试移除它观察发生了什么)
            final BigInteger f = Fibonacci.recursiveFasterPrimitiveAndBigInteger(100000);
            mTextView.post(new Runnable() {
                public void run() {f.toString());
                    mTextView.setText
                }
            });
        }
    }, "fibonacci").start();
}

尽管这可以工作的很好,它同样也是令人费解的,使得代码很难去阅读和维护。你可能会尝试简化Listing 5-2的代码,使用Listing 5-3的代码代替。不幸的是,这是一个不好的想法,因为这将简单的抛出一个CalledFromWrongThreadException异常,原因是Android UI工具包仅可以从UI线程调用。异常的描述是说只有创建View层级的原始线程可以接触到它的View。因此应用需要手动的保证TextView.setText()在UI线程调用,比如post一个Runnable对象到UI线程。

Listing 5-3 非UI线程的无效调用

public void onClick (View v) {
    new Thread(new Runnable() {
        public void run() {
            BigInteger f = Fibonacci.recursiveFasterPrimitiveAndBigInteger(100000);
            mTextView.setText(f.toString()); // 将会抛出异常
        }
    }, "fibonacci").start();
}

TIP: 为了方便调试,命名你产生的线程是一个好的习惯。如果没有指定名字,将会自动产生一个新的。可以通过调用Thread.getName()得到线程名字。

每个线程,不管它是如何产生的,会有一个优先级。调度器根据优先级去决定执行哪个线程,即哪个线程去使用CPU。你可以通过调用Thread.setPriority()去改变一个线程的优先级,如Listing 5-4所示。

Listing 5-4 设置一个线程的优先级

Thread thread = new Thread("thread name") {
    @Override
    public void run() {
        // 在这里做一些事情
    }
};
thread.setPriority(Thread.MAX_PRIORITY);    // 最高优先级 (比UI线程优先级高)
thread.start();

如果没有指定,使用默认优先级。线程类定义了3个常量:
(1) MIN_PRIORITY(1)
(2) NORM_PRIORITY(5)-默认的优先级
(3) MAX_PRIORITY(10)

如果你的应用尝试去设置一个线程的优先级为一个越界的值,即小于1或者大于10,将会抛出一个IllegalArgumentException异常。
Android提供了另外一种方式去设置线程的优先级,基于Linux的优先级,通过在android.os包中的Process.setThreadPriority的API。定义了如下8个优先级:
(1) THREAD_PRIORITY_AUDIO(-16)) 
(2) THREAD_PRIORITY_BACKGROUND(10)
(3) THREAD_PRIORITY_DEFAULT(0)
(4) THREAD_PRIORITYDISPLAY(-4)
(5) THREAD_PRIORITY_FORGROUND(-2)
(6) THREAD_PRIORITY_LOWEST(19)
(7) THREAD_PRIORITY_URGENT_AUDIO(-19)
(8) THREAD_PRIORITY_URGENT_DISPLAY(-8)

同样可以使用Process.THREAD_PRIORITY_LESS_FAVORABLE(+1)和Process.THREAD_PRIORITY_MORE_FAVORABLE(-1)。比如设置一个线程的优先级,比默认级别稍微高一点,可以设置为(THREAD_PRIORITY_DEFAULT + THREAD_PRIORITY_MORE_FAVORABLE)。

TIP: 使用THREAD_PRIORITY_LESS_FAVORABLE和THREAD_PRIORITY_MORE_FAVORABLE而不是+1和-1,不需要去记住是否一个更高的数量意味着更高的优先级。同样的,避免混合调用Thread.setPriority()和Process.setThreadPriority,因为这将使得你的代码迷惑。注意Linux的优先级从-20(最高)到19(最低),而Thread的优先级从1(最低)到10(最高)。

当你决定去改变线程的优先级的时候认真一些。增加某个线程的优先级可能导致这个线程更快的执行,但是对其他线程有负面的影响,可能会导致他们不能按需要得到CPU资源,因此打断用户的体验。如果对你的应用有意义,考虑实现一个优先级算法。
尽管在Android创建线程去执行一个后台任务是琐细的事,如Listing 5-1所示,更新用户的界面可以非常乏味:需要将结果发送给主线程,因为必须从UI线程调用View方法。

AsyncTask

你的应用经常需要去处理Listing 5-2中的事情:
(1) UI线程接收到事件
(2) 非UI线程执行对事件的响应
(3) 根据操作结果更新UI
为了简化这个通用的模式,安卓1.5及以上定义了AsyncTask类。AsyncTask类允许你的应用简单的执行一个后台操作,并在UI线程发布结果。线程、Runnable对象、其他相关的对象被隐藏起来。Listing 5-5给出了如何使用AsyncTask去实现Listing 5-2的序列。

Listing 5-5 使用AsyncTask

public void onClick (View v) {
    // AsyncTask<Params, Progress, Result> 匿名类
    new AsyncTask<Integer, Void, Biginteger>() {
        @Override
        protected BigInteger doInBackground(Integer... params) {
            return Fibonacci.recursiveFasterPrimitiveAndBigInteger(params[0]);
        }

        @Override
        protected void onPostExecute(BigInteger result) {
            mTextView.setText(result.toString());
        }
    }.execute(100000);
}

doInBackground()是一个抽象方法,需要被实现。尽管你不需要去重写onPostExecute(),它的一个主要目的允许发布结果到UI线程。下面的AsyncTask protected方法,都是从UI线程调用的:
(1) onPreExecute()
(2) onProgressUpdate(Progress…values)
(3) onPostExecute(Result result)
(4) onCancelled()
(5) onCancelled(Result result) 在Android 3.0引入 

onProgressUpdate()方法当在doInBackground()中调用publishProgress()时候被调用。这个方法允许当后台操作在处理的时候去更新UI。一个经典的实例后台下载文件的时候更新progress bar。Listing 5-6给出了如何下载多个文件。

Listing 5-6 下载多个文件

AsycTask<String, Object, Void> task = new AsyncTask<String, Object, Void>() {
    private ByteArrayBuffer downloadFile(String urlString, byte[] buffer) {
        try {
            URL url = new URL(urlString);
            URLConnection connection = url.openConnection();
            InputStream is = connection.getInputStream();
            // Log.i(TAG, "InputStream: " + is.getClass().getName()); // 如果你很严肃的话
            
            // is = new BufferedInputStream(is);  // 可选,尝试有或者没有的效果
            ByteArrayBuffer baf = new ByteArrayBuffer(640 * 1024);
            int len;
            while ((len = is.read(buffer)) != -1) {
                baf.append(buffer, 0, len);
            }
            
            return baf;
        } catch (MalformedURLException e) {
            return null;
        } catch (IOException e) {
            return null;
        }
    }
    
    @Override
    protected Void doInBackground(String... params) {
        if (params != null && params.length > 0) {
            byte[] buffer = new byte[4 * 1024]; // 尝试不同的大小 (1的话性能将会非常差)
            for (String url : params) {
                long time = System.currentTimeMillis();
                ByteArrayBuffer baf = downloadFile(url, buffer);
                time = System.currentTimeMillis() - time;
                publishProgress(url, baf, time);
            }
        } else {
            publishProgress(null, null);
        }
        
        return null;    // 我们不关心结果,但是必须要返回
    }
    
    @Override
    protected void onProgressUpdate(Object... values){
        // values[0]是URL(String) , values[1]是buffer(ByteArrayBuffer),values[2]是间隔
        String url = (String) values[0];
        ByteArrayBuffer buffer = (ByteArrayBuffer) values[1];
        if (buffer != null) {
            long time = (Long) values[2];
            Log.i(TAG, "Downloaded " + url + " (" + buffer.length() + " bytes) in " + time + " milliseconds");
        } else {
            Log.w(TAG, "Could not download " + url);
        }
        
        // 更新UI
    }
};


String url1 = "http://www.google.com/index.html";
String url2 = "http://d.android.com/reference/android/os/AsyncTask.html";
task.execute(url1, url2);
// task.execute("http://d.android.com/resources/articles/painless-threading.html");  // 尝试去获取异常

Listing 5-6的示例简单的将文件下载到内存(一个ByteArrayBuffer对象)。如果你期望把文件保存到持久存储器,同样需要在非主线程执行。另外,示例演示了下载一个文件之后再下载另外一个文件。依赖于你的需要,并行下载多个文件可能更好。

NOTE:AsyncTask对象必须在UI线程创建,并且仅可以被执行一次。

doInBackground()任务什么时候被执行依赖于Android版本。在Android1.6以前,任务将被串行执行,所以只需要一个后台线程。Android 1.6开始,后台线程被一个线程池代替,为了达到更好的性能,允许多个任务并行执行。然而,并行执行几个任务,当同步没有合适的实现,或者任务执行或者完成在一个特定的顺序(可能不是开发者期望的顺序),可能会导致严重的问题。结果,安卓开发组计划回到一个后台线程的模式,默认在Honeycomb。为了继续允许应用并行执行task,executeOnExecutor() API在Honeycomb被引入,留给应用开发者更新应用的时间。这个新的API可以和AsyncTask.SERIAL_EXECUTOR串行执行,或者THREAD_POOL_EXECUTOR并行执行。
这些变化表示并行执行需要一个认真的设计和完全的测试。Android开发组低估了潜在的问题或者过高估计了应用处理他们的能力,在Android1.6切换到一个线程池,导致在Honeycomb回退到一个单线程模式的决定。有经验的开发者为了更好的性能仍然有能力去并行执行任务,应用的质量将会提升。
当处理后台任务和用户界面更新,AsyncTask类可以简化你的代码,不意味着全部替换Android定义的线程之间的通信基本类。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值