العمل غير المتزامن مع سلاسل محادثات Java

تستخدم جميع تطبيقات Android سلسلة محادثات رئيسية لمعالجة عمليات واجهة المستخدم. الاتصال منذ فترة طويلة العمليات من سلسلة المحادثات الرئيسية هذه إلى تجميد البيانات وعدم الاستجابة. بالنسبة على سبيل المثال، إذا قدّم تطبيقك طلب شبكة من سلسلة التعليمات الرئيسية، ستتحول واجهة مستخدم تطبيقك يتم تجميده حتى يتلقى استجابة الشبكة. إذا كنت تستخدم Java، يمكنك إنشاء سلاسل محادثات في الخلفية إضافية لمعالجة العمليات طويلة الأمد أثناء تواصل سلسلة التعليمات الرئيسية معالجة تحديثات واجهة المستخدم.

يوضح هذا الدليل كيف يمكن للمطورين الذين يستخدمون لغة برمجة Java استخدام سلسلة المحادثات لإعداد سلاسل محادثات متعددة واستخدامها في تطبيق Android. هذا الدليل أيضًا كيفية تحديد التعليمات البرمجية لتشغيلها في سلسلة محادثات وكيفية التواصل بين إحدى هذه السلاسل وسلسلة المحادثات الرئيسية.

مكتبات التزامن

من المهم فهم أساسيات سلاسل المحادثات والعناصر الأساسية والآليات الأخرى. مع ذلك، هناك العديد من المكتبات المشهورة التي تقدّم خدمات ذات مستوًى أعلى. التجريدات على هذه المفاهيم والأدوات الجاهزة للاستخدام لتمرير البيانات بين سلاسل المحادثات. وتشمل هذه المكتبات Guava وRxJava لمستخدمي لغة برمجة Java وCoroutines، والذي نوصي به لمستخدمي Kotlin.

من الناحية العملية، يجب اختيار الأسلوب الأنسب لتطبيقك على الرغم من أنّ قواعد سلاسل المحادثات لم تتغيّر.

نظرة عامة على الأمثلة

استنادًا إلى دليل بنية التطبيقات، تجعل الأمثلة في هذا الموضوع الشبكة وعرض النتيجة إلى سلسلة التعليمات الرئيسية، حيث يقوم التطبيق قد تعرض تلك النتيجة على الشاشة.

على وجه التحديد، تطلب السمة ViewModel طبقة البيانات في سلسلة التعليمات الرئيسية تشغيل طلب الشبكة. تكون طبقة البيانات هي المسؤولة عن نقل تنفيذ طلب الشبكة خارج سلسلة التعليمات الرئيسية ثم نشر النتيجة إلى سلسلة التعليمات الرئيسية باستخدام استدعاء.

لنقل تنفيذ طلب الشبكة من سلسلة التعليمات الرئيسية، نحتاج إلى إنشاء سلاسل أخرى في التطبيق.

إنشاء سلاسل محادثات متعدّدة

مجموعة سلاسل المحادثات هي مجموعة مُدارة من سلاسل المحادثات التي تشغِّل المهام في موازٍ من قائمة انتظار. يتم تنفيذ المهام الجديدة في سلاسل المحادثات الحالية مثل تصبح السلاسل خاملة. لإرسال مهمة إلى مجموعة سلاسل محادثات، استخدِم ExecutorService. يُرجى العِلم أنّه ليس مطلوبًا اتخاذ أي إجراء من "ExecutorService". مع الخدمات، مكون تطبيق Android.

إنّ إنشاء سلاسل المحادثات أمر مكلف، لذا يجب إنشاء مجموعة سلاسل محادثات مرة واحدة فقط يبدأه تطبيقك. تأكَّد من حفظ مثيل ExecutorService إما في الفئة Application أو في حاوية إدخال تبعية. ينشئ المثال التالي مجموعة سلاسل محادثات مكونة من أربع سلاسل يمكننا استخدامها لتشغيل مهام الخلفية.

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
}

هناك طرق أخرى يمكنك من خلالها ضبط مجموعة سلاسل محادثات استنادًا إلى التوقّعات عبء العمل. للحصول على مزيد من المعلومات، يمكنك الاطّلاع على ضبط مجموعة سلاسل المحادثات.

التنفيذ في سلسلة محادثات في الخلفية

يؤدي تقديم طلب عبر الشبكة في سلسلة التعليمات الرئيسية إلى انتظار سلسلة المحادثات، أو block، حتى يتلقى ردًا. وبما أنّ سلسلة المحادثات محظورة، لا يمكن لنظام التشغيل طلب 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 سلسلة المحادثات الرئيسية عند إنشاء طلب الشبكة. يمكننا استخدام مجموعة سلاسل المحادثات التي أنشأنا مثيلاً لها لنقل عملية التنفيذ إلى سلسلة محادثات في الخلفية.

التعامل مع إدخال التبعية

أولاً، وباتّباع مبادئ إدخال التبعية، LoginRepository يأخذ مثيل Executor بدلاً من ExecutorService لأنه تنفيذ الرمز البرمجي وعدم إدارة سلاسل المحادثات:

public class LoginRepository {
    ...
    private final Executor executor;

    public LoginRepository(LoginResponseParser responseParser, Executor executor) {
        this.responseParser = responseParser;
        this.executor = executor;
    }
    ...
}

تستخدم طريقة execute() في التنفيذ طريقة Runnable. Runnable هو واجهة طريقة تجريدية واحدة (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 إلى معرفتها. يمكننا القيام بذلك عن طريق باستخدام عمليات معاودة الاتصال.

يجب أن تستخدم الدالة makeLoginRequest() استدعاء كمعلمة بحيث فيمكنها إرجاع قيمة بشكل غير متزامن. يُطلق على عملية الاسترداد التي تتضمّن النتيجة عند اكتمال طلب الشبكة أو عند حدوث إخفاق. في Kotlin، يمكننا تستخدم دالة ترتيب أعلى. ومع ذلك، في Java، ينبغي لنا إنشاء معاودة اتصال جديدة الواجهة على نفس الوظيفة، وهي:

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" تنفيذ طلب معاودة الاتصال الآن. يمكن أن يختلف منطقية اعتمادًا على النتيجة:

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
                }
            }
        });
    }
}

في هذا المثال، يتم تنفيذ عملية الاستدعاء في سلسلة عمليات الاستدعاء، وهي سلسلة محادثات في الخلفية. وهذا يعني أنّه لا يمكنك تعديل المحتوى أو التواصل معه مباشرةً مع طبقة واجهة المستخدم إلى أن يتم التبديل مرة أخرى إلى سلسلة التعليمات الرئيسية.

استخدام المعالجات

يمكنك استخدام معالج لإضافة إجراء إلى قائمة انتظار لتنفيذه على نطاق . لتحديد سلسلة المحادثات التي يتم تنفيذ الإجراء عليها، أنشئ Handler يتم استخدام تكرار لسلسلة المحادثات. 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);
            }
        });
    }
}

في هذا المثال، يتم تمرير طلب الاستدعاء إلى makeLoginRequest الخاص بالمستودع. في سلسلة التعليمات الرئيسية. وهذا يعني أنّه يمكنكم تعديل واجهة المستخدم مباشرةً من رد الاتصال أو استخدام LiveData.setValue() للتواصل مع واجهة المستخدم.

ضبط مجموعة سلاسل محادثات

يمكنك إنشاء مجموعة سلاسل محادثات باستخدام إحدى الدوال المساعدة لأداة التنفيذ. بإعدادات محددة مسبقًا، كما هو موضح في مثال الرمز السابق. بدلاً من ذلك، إذا أردت تخصيص تفاصيل مجموعة سلاسل المحادثات، يمكنك إنشاء مثال يستخدم ThreadPoolExecutor مباشرةً. يمكنك ضبط ما يلي التفاصيل:

  • الحجم الأولي والأقصى لحجم المسبح.
  • الحفاظ على الوقت والوحدة الزمنية. الاحتفاظ بمدة البقاء هو الحد الأقصى للمدة التي يمكن أن تظل سلسلة المحادثات غير مستخدَمة قبل إيقافها.
  • قائمة انتظار إدخال تحتوي على Runnable مهمة يجب أن تنفذ قائمة الانتظار هذه واجهة Blocklock. لاستيفاء متطلبات تطبيقك، يمكنك: اختر من بين عمليات تنفيذ قائمة الانتظار المتاحة. لمعرفة المزيد من المعلومات، يُرجى الاطّلاع على الصف. نظرة عامة حول ThreadPoolExecutor.

إليك مثال يحدد حجم مجموعة سلاسل المحادثات بناءً على العدد الإجمالي نوى معالج البيانات، وفترة البقاء الثابتة لمدة ثانية واحدة، وقائمة انتظار الإدخال.

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
    );
    ...
}