แอป Android ทั้งหมดจะใช้เทรดหลักในการจัดการกับการดำเนินการของ UI การโทรเป็นเวลานาน การดำเนินการจากเทรดหลักนี้อาจทำให้เกิดการค้างและไม่ตอบสนอง สำหรับ เช่น หากแอปสร้างคำขอเครือข่ายจากเทรดหลัก UI ของแอป ถูกหยุดไว้จนกว่าจะได้รับการตอบสนองของเครือข่าย ถ้าคุณใช้ Java คุณสามารถ สร้างชุดข้อความเบื้องหลังเพิ่มเติมเพื่อจัดการการดำเนินการที่ใช้เวลานาน เทรดหลักจะยังคงจัดการการอัปเดต UI อยู่
คู่มือนี้แสดงวิธีที่นักพัฒนาซอฟต์แวร์ที่ใช้ภาษาโปรแกรม Java สามารถใช้ Thread Pool เพื่อตั้งค่าและใช้ชุดข้อความหลายรายการในแอป Android คู่มือนี้ ก็แสดงวิธีกำหนดโค้ดให้ทำงานในเทรดและวิธีสื่อสาร ระหว่างเทรดเหล่านี้ กับเทรดหลัก
ไลบรารีการเกิดขึ้นพร้อมกัน
คุณต้องเข้าใจพื้นฐานของชุดข้อความและองค์ประกอบที่เกี่ยวข้อง และกลไกต่างๆ อย่างไรก็ตาม มีห้องสมุดยอดนิยมจำนวนมากที่มีห้องสมุดระดับสูงขึ้น แอบสแตรกชันในแนวคิดเหล่านี้และยูทิลิตีที่พร้อมใช้งานสำหรับการส่งข้อมูล ระหว่างชุดข้อความได้ ไลบรารีเหล่านี้รวมถึง Guava และ RxJava สําหรับผู้ใช้ภาษาโปรแกรม Java และ Coroutines ซึ่งเราแนะนำสำหรับผู้ใช้ Kotlin
ในทางปฏิบัติ คุณควรเลือกวิธีที่เหมาะสมกับแอปและ แต่กฎการจัดชุดข้อความจะยังคงเหมือนเดิม
ภาพรวมตัวอย่าง
จากคำแนะนำเกี่ยวกับสถาปัตยกรรมแอป ตัวอย่างในหัวข้อนี้ทำให้ คำขอเครือข่ายและแสดงผลลัพธ์ไปยังเทรดหลัก จากนั้นแอป อาจแสดงผลลัพธ์นั้นบนหน้าจอ
กล่าวโดยเจาะจงคือ ViewModel
จะเรียกชั้นข้อมูลในเทรดหลักไปยัง
เรียกใช้คำขอเครือข่าย ชั้นข้อมูลทำหน้าที่ย้าย
การดำเนินการตามคำขอเครือข่ายจากเทรดหลัก และโพสต์ผลลัพธ์กลับมา
ไปยังเทรดหลักโดยใช้ Callback
หากต้องการย้ายการดำเนินการตามคำขอเครือข่ายออกจากเทรดหลัก เราจำเป็นต้องดำเนินการต่อไปนี้ สร้างชุดข้อความอื่นๆ ในแอปของเรา
สร้างชุดข้อความหลายรายการ
กลุ่มชุดข้อความ คือคอลเล็กชันที่มีการจัดการของชุดข้อความที่เรียกใช้งานใน
ขนานกันจากคิว งานใหม่จะดำเนินการในชุดข้อความที่มีอยู่
เทรดจะไม่มีการใช้งาน หากต้องการส่งงานไปยัง Thread Pool ให้ใช้
ExecutorService
โปรดทราบว่า ExecutorService
ไม่ต้องดำเนินการใดๆ
กับ Services ซึ่งเป็นคอมโพเนนต์ของแอปพลิเคชัน Android
การสร้างชุดข้อความมีราคาแพง คุณจึงควรสร้างพูลชุดข้อความเพียงครั้งเดียวในฐานะ
แอปของคุณจะเริ่มต้น อย่าลืมบันทึกอินสแตนซ์ของ ExecutorService
ในคลาส Application
หรือในคอนเทนเนอร์การแทรกแบบ Dependency
ตัวอย่างต่อไปนี้สร้าง Thread Pool ที่ประกอบด้วย 4 ชุดข้อความที่เราสามารถใช้เพื่อ
เรียกใช้งานเบื้องหลัง
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
}
ยังมีวิธีอื่นที่คุณจะกำหนดค่า Thread Pool ได้ตามความต้องการ ปริมาณงาน โปรดดูข้อมูลเพิ่มเติมที่การกำหนดค่า Thread Pool
ดำเนินการในชุดข้อความเบื้องหลัง
การส่งคำขอเครือข่ายในเทรดหลักจะทำให้เทรดต้องรอเทรด หรือ
บล็อก จนกว่าจะได้รับการตอบกลับ เนื่องจากเทรดถูกบล็อก ระบบปฏิบัติการจึงไม่สามารถ
โทรหา onDraw()
แอปของคุณหยุดทำงาน ซึ่งอาจทำให้แอปพลิเคชันไม่ทำงาน
กล่องโต้ตอบการตอบกลับ (ANR) ลองทำการดำเนินการนี้ในพื้นหลังแทน
ชุดข้อความ
ส่งคำขอ
ก่อนอื่น เราจะมาดูชั้นเรียน LoginRepository
ของเรา แล้วดูว่าผลการเรียนรู้เป็นอย่างไร
คำขอเครือข่าย
// Result.java
public abstract class Result<T> {
private Result() {}
public static final class Success<T> extends Result<T> {
public T data;
public Success(T data) {
this.data = data;
}
}
public static final class Error<T> extends Result<T> {
public Exception exception;
public Error(Exception exception) {
this.exception = exception;
}
}
}
// LoginRepository.java
public class LoginRepository {
private final String loginUrl = "https://example.com/login";
private final LoginResponseParser responseParser;
public LoginRepository(LoginResponseParser responseParser) {
this.responseParser = responseParser;
}
public Result<LoginResponse> makeLoginRequest(String jsonBody) {
try {
URL url = new URL(loginUrl);
HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.setRequestMethod("POST");
httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
httpConnection.setRequestProperty("Accept", "application/json");
httpConnection.setDoOutput(true);
httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8"));
LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream());
return new Result.Success<LoginResponse>(loginResponse);
} catch (Exception e) {
return new Result.Error<LoginResponse>(e);
}
}
}
makeLoginRequest()
เป็นแบบพร้อมกันและบล็อกชุดข้อความการโทร ในการสร้างแบบจำลอง
การตอบสนองต่อคำขอเครือข่าย เรามีคลาส Result
เป็นของเราเอง
เรียกใช้คำขอ
ViewModel
จะเรียกใช้คำขอเครือข่ายเมื่อผู้ใช้แตะ เช่น
ปุ่ม:
public class LoginViewModel {
private final LoginRepository loginRepository;
public LoginViewModel(LoginRepository loginRepository) {
this.loginRepository = loginRepository;
}
public void makeLoginRequest(String username, String token) {
String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
loginRepository.makeLoginRequest(jsonBody);
}
}
เมื่อใช้โค้ดก่อนหน้า LoginViewModel
จะบล็อกเทรดหลักเมื่อสร้าง
คำขอเครือข่าย เราใช้ Thread Pool ที่เราสร้างไว้ในการย้ายแล้วได้
การดำเนินการกับเทรดในเบื้องหลัง
จัดการการแทรกทรัพยากร Dependency
ขั้นแรก ตามหลักการแทรกทรัพยากร Dependency LoginRepository
ใช้อินสแตนซ์ของ ผู้ดำเนินการ ซึ่งตรงข้ามกับ ExecutorService
เนื่องจาก
กำลังรันโค้ดและไม่ได้จัดการชุดข้อความ:
public class LoginRepository {
...
private final Executor executor;
public LoginRepository(LoginResponseParser responseParser, Executor executor) {
this.responseParser = responseParser;
this.executor = executor;
}
...
}
เมธอด execute() ของโอเปอเรเตอร์จะใช้Runnable Runnable
คือ
อินเทอร์เฟซ Single Abstract Method (SAM) ที่มีเมธอด run()
ที่ดำเนินการใน
ชุดข้อความเมื่อเรียกใช้
ทำงานในพื้นหลัง
มาสร้างฟังก์ชันอีกรายการหนึ่งที่ชื่อว่า makeLoginRequest()
ที่ย้ายฟังก์ชัน
การดำเนินการกับเธรดเบื้องหลังและไม่สนใจการตอบกลับในขณะนี้:
public class LoginRepository {
...
public void makeLoginRequest(final String jsonBody) {
executor.execute(new Runnable() {
@Override
public void run() {
Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody);
}
});
}
public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) {
... // HttpURLConnection logic
}
...
}
ภายในเมธอด execute()
เราได้สร้าง Runnable
ใหม่ด้วยบล็อกของโค้ด
เราต้องการที่จะดำเนินการในเทรดเบื้องหลัง ในกรณีของเรา คือเครือข่ายซิงโครนัส
เมธอด ภายในองค์กร ExecutorService
จะจัดการ Runnable
และ
เรียกใช้ในชุดข้อความที่มีอยู่
ข้อควรพิจารณา
เทรดใดก็ตามในแอปของคุณสามารถทำงานควบคู่ไปกับเทรดอื่นๆ ได้ ซึ่งรวมถึงเทรดหลัก เทรด ดังนั้นคุณจึงควรตรวจสอบว่าโค้ดปลอดภัยของเทรด โปรดสังเกตว่าใน ตัวอย่างเช่น เราจะหลีกเลี่ยงการเขียนไปยังตัวแปรที่แชร์กันระหว่างชุดข้อความ ข้อมูลที่เปลี่ยนแปลงไม่ได้แทน ซึ่งเป็นแนวทางปฏิบัติที่ดี เนื่องจากแต่ละชุดข้อความจะทำงานร่วมกับ อินสแตนซ์ของข้อมูลเอง และช่วยให้เราหลีกเลี่ยงความซับซ้อนของการซิงค์
หากต้องการแชร์สถานะระหว่างชุดข้อความ คุณต้องจัดการการเข้าถึงอย่างระมัดระวัง จากชุดข้อความโดยใช้กลไกการซิงค์ เช่น ล็อก รายการนี้อยู่นอก ขอบเขตของคู่มือนี้ โดยทั่วไปคุณควรหลีกเลี่ยงการแชร์สถานะที่เปลี่ยนแปลงได้ ระหว่างชุดข้อความทุกครั้งที่เป็นไปได้
สื่อสารกับเทรดหลัก
ในขั้นตอนก่อนหน้า เราไม่สนใจการตอบกลับคำขอเครือข่าย หากต้องการแสดง
ผลลัพธ์บนหน้าจอ LoginViewModel
จำเป็นต้องทราบเกี่ยวกับผลลัพธ์นั้น สามารถทำได้โดย
โดยใช้Callback
ฟังก์ชัน makeLoginRequest()
ควรใช้ Callback เป็นพารามิเตอร์เพื่อให้
จึงสามารถคืนค่าแบบไม่พร้อมกัน Callback ที่มีผลลัพธ์จะเรียกว่า
เมื่อใดก็ตามที่คำขอเครือข่ายดำเนินการเสร็จสมบูรณ์หรือเกิดข้อผิดพลาด ใน Kotlin เราสามารถ
ใช้ฟังก์ชันลำดับที่สูงกว่า อย่างไรก็ตาม ใน Java เราต้องสร้าง Callback ใหม่
เพื่อให้มีฟังก์ชันการทำงานเหมือนกัน:
interface RepositoryCallback<T> {
void onComplete(Result<T> result);
}
public class LoginRepository {
...
public void makeLoginRequest(
final String jsonBody,
final RepositoryCallback<LoginResponse> callback
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
callback.onComplete(result);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
callback.onComplete(errorResult);
}
}
});
}
...
}
ViewModel
ต้องใช้ Callback ทันที อาจให้ผลที่แตกต่างกันไป
ขึ้นอยู่กับผลลัพธ์ ดังนี้
public class LoginViewModel {
...
public void makeLoginRequest(String username, String token) {
String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() {
@Override
public void onComplete(Result<LoginResponse> result) {
if (result instanceof Result.Success) {
// Happy path
} else {
// Show error in UI
}
}
});
}
}
ในตัวอย่างนี้ การเรียกใช้ Callback จะดำเนินการในเทรดการเรียกใช้ ซึ่งเป็น เทรดพื้นหลัง ซึ่งหมายความว่าคุณไม่สามารถแก้ไขหรือสื่อสารโดยตรง กับเลเยอร์ UI จนกว่าคุณจะเปลี่ยนกลับไปใช้เทรดหลัก
ใช้ตัวจัดการ
คุณใช้เครื่องจัดการเพื่อกำหนดคิวการดำเนินการเพื่อดำเนินการในอุปกรณ์อื่นได้
ชุดข้อความ หากต้องการระบุเทรดที่จะเรียกใช้การดำเนินการ ให้สร้างพารามิเตอร์
Handler
กำลังใช้ Looper สำหรับชุดข้อความ Looper
คือออบเจ็กต์ที่เรียกใช้
ที่วนซ้ำข้อความสำหรับชุดข้อความที่เกี่ยวข้อง เมื่อคุณสร้าง Handler
คุณ
จากนั้นสามารถใช้เมธอด post(Runnable) เพื่อเรียกใช้บล็อกโค้ดใน
ชุดข้อความที่เกี่ยวข้อง
Looper
มีฟังก์ชันตัวช่วย getMainLooper() ซึ่งเรียกฟังก์ชัน
Looper
ของชุดข้อความหลัก คุณเรียกใช้โค้ดในเทรดหลักได้โดยใช้
Looper
เพื่อสร้าง Handler
เนื่องจากนี่เป็นกิจกรรม
ที่คุณอาจทำบ่อยๆ
คุณยังสามารถบันทึกอินสแตนซ์ของ Handler
ในที่เดียวกับที่คุณบันทึก
ExecutorService
:
public class MyApplication extends Application {
ExecutorService executorService = Executors.newFixedThreadPool(4);
Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}
คุณควรแทรกเครื่องจัดการลงในที่เก็บเนื่องจาก
ให้ความยืดหยุ่นมากกว่า ตัวอย่างเช่น ในอนาคต คุณอาจต้องการส่งผ่าน
Handler
ที่แตกต่างกันเพื่อตั้งเวลางานในชุดข้อความแยกต่างหาก หากคุณ
สื่อสารกลับไปยังชุดข้อความเดียวกัน คุณสามารถส่ง Handler
ไปยัง
เครื่องมือสร้างที่เก็บ ดังที่ปรากฏในตัวอย่างต่อไปนี้
public class LoginRepository {
...
private final Handler resultHandler;
public LoginRepository(LoginResponseParser responseParser, Executor executor,
Handler resultHandler) {
this.responseParser = responseParser;
this.executor = executor;
this.resultHandler = resultHandler;
}
public void makeLoginRequest(
final String jsonBody,
final RepositoryCallback<LoginResponse> callback
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
notifyResult(result, callback);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
notifyResult(errorResult, callback);
}
}
});
}
private void notifyResult(
final Result<LoginResponse> result,
final RepositoryCallback<LoginResponse> callback,
) {
resultHandler.post(new Runnable() {
@Override
public void run() {
callback.onComplete(result);
}
});
}
...
}
หรือหากต้องการความยืดหยุ่นมากขึ้น คุณสามารถส่ง Handler
ไปยัง
ฟังก์ชัน:
public class LoginRepository {
...
public void makeLoginRequest(
final String jsonBody,
final RepositoryCallback<LoginResponse> callback,
final Handler resultHandler,
) {
executor.execute(new Runnable() {
@Override
public void run() {
try {
Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
notifyResult(result, callback, resultHandler);
} catch (Exception e) {
Result<LoginResponse> errorResult = new Result.Error<>(e);
notifyResult(errorResult, callback, resultHandler);
}
}
});
}
private void notifyResult(
final Result<LoginResponse> result,
final RepositoryCallback<LoginResponse> callback,
final Handler resultHandler
) {
resultHandler.post(new Runnable() {
@Override
public void run() {
callback.onComplete(result);
}
});
}
}
ในตัวอย่างนี้ Callback ที่ส่งผ่านไปยัง makeLoginRequest
ของที่เก็บ
เรียกใช้ในเทรดหลัก ซึ่งหมายความว่าคุณสามารถปรับเปลี่ยน UI ได้โดยตรง
จาก Callback หรือใช้ LiveData.setValue()
เพื่อสื่อสารกับ UI
กำหนดค่า Thread Pool
คุณสร้าง Thread Pool ได้โดยใช้ฟังก์ชันตัวช่วยของผู้ดำเนินการ ด้วยการตั้งค่าที่กำหนดไว้ล่วงหน้า ดังที่แสดงในโค้ดตัวอย่างก่อนหน้านี้ หรือ หากต้องการปรับแต่งรายละเอียด ของกลุ่มชุดข้อความ คุณสามารถสร้าง โดยใช้ ThreadPoolExecutor โดยตรง คุณกําหนดค่าสิ่งต่อไปนี้ได้ รายละเอียด:
- ขนาดเริ่มต้นและขนาดพูลสูงสุด
- แสดงเวลาและหน่วยเวลา เวลาที่ยังคงทำงานอยู่คือระยะเวลาสูงสุดที่ ชุดข้อความจะยังคงไม่มีการใช้งานก่อนที่จะปิดลง
- คิวอินพุตที่มี
Runnable
งาน คิวนี้ต้องใช้เมธอด อินเทอร์เฟซ blockQueue หากต้องการปฏิบัติตามข้อกำหนดของแอป ให้ทำดังนี้ เลือกจากการใช้งานคิวที่มีอยู่ หากต้องการดูข้อมูลเพิ่มเติม โปรดดูชั้นเรียน ภาพรวมสำหรับ ThreadPoolExecutor
นี่คือตัวอย่างที่ระบุขนาด Thread Pool โดยอิงตามจำนวนรวมของ โปรเซสเซอร์ Core, เวลาการทำงาน Keep 1 วินาที และคิวอินพุต
public class MyApplication extends Application {
/*
* Gets the number of available cores
* (not always the same as the maximum number of cores)
*/
private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
// Instantiates the queue of Runnables as a LinkedBlockingQueue
private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();
// Sets the amount of time an idle thread waits before terminating
private static final int KEEP_ALIVE_TIME = 1;
// Sets the Time Unit to seconds
private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
// Creates a thread pool manager
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
NUMBER_OF_CORES, // Initial pool size
NUMBER_OF_CORES, // Max pool size
KEEP_ALIVE_TIME,
KEEP_ALIVE_TIME_UNIT,
workQueue
);
...
}